Skip to content

Instantly share code, notes, and snippets.

@eugrus
Created September 8, 2026 07:35
Show Gist options
  • Select an option

  • Save eugrus/bb2d1cb147833bae12206796554a61a0 to your computer and use it in GitHub Desktop.

Select an option

Save eugrus/bb2d1cb147833bae12206796554a61a0 to your computer and use it in GitHub Desktop.
<#
.SYNOPSIS
Lists UI Automation elements of all open top-level windows (incl. name, value, position).
This is the WPF/XAML counterpart to Get-HwndEnumerator.ps1: WPF draws its controls into a
single HWND, so EnumChildWindows returns nothing useful. UI Automation walks the logical
tree instead.
.PARAMETER TitleFilter
Substring filter for the window name (case-insensitive). Empty = all windows.
.PARAMETER ControlTypeFilter
Wildcard filter (like -like) for the control type, e.g. "Edit", "*Button*", "DataItem".
Empty/"*" = all control types. This is the analogue of -ClassFilter.
.PARAMETER ProcessNameFilter
Wildcard filter for the owning process name (without .exe), e.g. "ramicro*".
.PARAMETER IncludeOffscreen
Also includes elements whose IsOffscreen property is true (default: on-screen only).
.PARAMETER TopLevelOnly
Only reads top-level windows, no descendants.
.PARAMETER NameOnly
Suppresses element rows that have neither a Name nor a Value.
.PARAMETER RawView
Walks the raw tree instead of the control view. Yields every element including layout-only
containers. Much slower and much noisier.
.PARAMETER MaxDepth
Maximum recursion depth below the window. Guards against runaway trees (default 12).
.PARAMETER SortByPosition
Sorts the elements of each window by Y, then X, instead of tree order.
.EXAMPLE
UIAEnumerator.ps1
.EXAMPLE
UIAEnumerator.ps1 -TitleFilter 'RA-MICRO PD' -ControlTypeFilter 'Edit' -NameOnly
.EXAMPLE
UIAEnumerator.ps1 -ProcessNameFilter 'ramicro*' -MaxDepth 6 | Format-Table -AutoSize
#>
param(
[string]$TitleFilter = '',
[string]$ControlTypeFilter = '*',
[string]$ProcessNameFilter = '',
[switch]$IncludeOffscreen,
[switch]$TopLevelOnly,
[switch]$NameOnly,
[switch]$RawView,
[int]$MaxDepth = 12,
[switch]$SortByPosition
)
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Add-Type -AssemblyName WindowsBase
$walker = if ($RawView) {
[System.Windows.Automation.TreeWalker]::RawViewWalker
} else {
[System.Windows.Automation.TreeWalker]::ControlViewWalker
}
function Get-UiaProperty {
param(
[System.Windows.Automation.AutomationElement]$Element,
[System.Windows.Automation.AutomationProperty]$Property
)
try {
$value = $Element.GetCurrentPropertyValue($Property, $true)
if ($value -eq [System.Windows.Automation.AutomationElement]::NotSupported) { return $null }
return $value
} catch {
# ElementNotAvailableException: the element vanished mid-walk
return $null
}
}
function Get-UiaValue {
param([System.Windows.Automation.AutomationElement]$Element)
$pattern = $null
try {
if ($Element.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$pattern)) {
return $pattern.Current.Value
}
if ($Element.TryGetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern, [ref]$pattern)) {
return $pattern.DocumentRange.GetText(4096)
}
} catch { }
return $null
}
function Get-UiaControlTypeName {
param([System.Windows.Automation.AutomationElement]$Element)
$controlType = Get-UiaProperty -Element $Element -Property ([System.Windows.Automation.AutomationElement]::ControlTypeProperty)
if (-not $controlType) { return '' }
# ProgrammaticName looks like "ControlType.Button"
return ($controlType.ProgrammaticName -replace '^ControlType\.', '')
}
function New-UiaRow {
param(
[System.Windows.Automation.AutomationElement]$Element,
[System.Windows.Automation.AutomationElement]$Window,
[int]$Depth
)
$rect = Get-UiaProperty -Element $Element -Property ([System.Windows.Automation.AutomationElement]::BoundingRectangleProperty)
$nativeHandle = Get-UiaProperty -Element $Element -Property ([System.Windows.Automation.AutomationElement]::NativeWindowHandleProperty)
$windowHandle = Get-UiaProperty -Element $Window -Property ([System.Windows.Automation.AutomationElement]::NativeWindowHandleProperty)
$processId = Get-UiaProperty -Element $Element -Property ([System.Windows.Automation.AutomationElement]::ProcessIdProperty)
$processName = $null
if ($processId) {
$process = Get-Process -Id $processId -ErrorAction SilentlyContinue
if ($process) { $processName = $process.ProcessName }
}
[pscustomobject]@{
WindowHwnd = if ($windowHandle) { '0x{0:X}' -f [int64]$windowHandle } else { $null }
WindowTitle = Get-UiaProperty -Element $Window -Property ([System.Windows.Automation.AutomationElement]::NameProperty)
ProcessId = $processId
ProcessName = $processName
Depth = $Depth
ControlType = Get-UiaControlTypeName -Element $Element
AutomationId = Get-UiaProperty -Element $Element -Property ([System.Windows.Automation.AutomationElement]::AutomationIdProperty)
ClassName = Get-UiaProperty -Element $Element -Property ([System.Windows.Automation.AutomationElement]::ClassNameProperty)
Name = Get-UiaProperty -Element $Element -Property ([System.Windows.Automation.AutomationElement]::NameProperty)
Value = Get-UiaValue -Element $Element
NativeHwnd = if ($nativeHandle) { '0x{0:X}' -f [int64]$nativeHandle } else { $null }
X = if ($rect -and -not [double]::IsInfinity($rect.X)) { [int]$rect.X } else { $null }
Y = if ($rect -and -not [double]::IsInfinity($rect.Y)) { [int]$rect.Y } else { $null }
Width = if ($rect -and -not [double]::IsInfinity($rect.Width)) { [int]$rect.Width } else { $null }
Height = if ($rect -and -not [double]::IsInfinity($rect.Height)) { [int]$rect.Height } else { $null }
}
}
function Get-UiaDescendant {
param(
[System.Windows.Automation.AutomationElement]$Element,
[System.Windows.Automation.AutomationElement]$Window,
[int]$Depth
)
if ($Depth -gt $MaxDepth) { return }
$child = $null
try { $child = $walker.GetFirstChild($Element) } catch { return }
while ($child) {
$isOffscreen = Get-UiaProperty -Element $child -Property ([System.Windows.Automation.AutomationElement]::IsOffscreenProperty)
$passesVisibility = $IncludeOffscreen -or -not $isOffscreen
if ($passesVisibility) {
$controlType = Get-UiaControlTypeName -Element $child
$passesType = (-not $ControlTypeFilter) -or ($ControlTypeFilter -eq '*') -or ($controlType -like $ControlTypeFilter)
if ($passesType) {
$row = New-UiaRow -Element $child -Window $Window -Depth $Depth
$hasText = -not ([string]::IsNullOrEmpty($row.Name) -and [string]::IsNullOrEmpty($row.Value))
if (-not $NameOnly -or $hasText) { $row }
}
Get-UiaDescendant -Element $child -Window $Window -Depth ($Depth + 1)
}
try { $child = $walker.GetNextSibling($child) } catch { break }
}
}
$desktop = [System.Windows.Automation.AutomationElement]::RootElement
$windows = @()
$window = $null
try { $window = $walker.GetFirstChild($desktop) } catch { }
while ($window) {
$windows += $window
try { $window = $walker.GetNextSibling($window) } catch { break }
}
$result = foreach ($window in $windows) {
$title = Get-UiaProperty -Element $window -Property ([System.Windows.Automation.AutomationElement]::NameProperty)
if ($TitleFilter -and ($title -notlike "*$TitleFilter*")) { continue }
$processId = Get-UiaProperty -Element $window -Property ([System.Windows.Automation.AutomationElement]::ProcessIdProperty)
if ($ProcessNameFilter) {
$process = Get-Process -Id $processId -ErrorAction SilentlyContinue
if (-not $process -or ($process.ProcessName -notlike $ProcessNameFilter)) { continue }
}
$isOffscreen = Get-UiaProperty -Element $window -Property ([System.Windows.Automation.AutomationElement]::IsOffscreenProperty)
if (-not $IncludeOffscreen -and $isOffscreen) { continue }
# the window row itself
if (-not $NameOnly -or $title) {
New-UiaRow -Element $window -Window $window -Depth 0
}
if ($TopLevelOnly) { continue }
$descendants = @(Get-UiaDescendant -Element $window -Window $window -Depth 1)
if ($SortByPosition) {
$descendants | Sort-Object Y, X
} else {
$descendants
}
}
$result
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment