Created
September 8, 2026 07:35
-
-
Save eugrus/bb2d1cb147833bae12206796554a61a0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .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