Last active
September 8, 2026 08:25
-
-
Save eugrus/a2a75eff6b9848ec0bb6b1119c2d43fa 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 HWNDs of all open top-level windows and their controls (incl. text and control IDs). | |
| .PARAMETER TitleFilter | |
| Substring filter for the window title (case-insensitive). Empty = all windows. | |
| .PARAMETER ClassFilter | |
| Wildcard filter (like -like) for the control class, e.g. "Edit", "*Edit*", "TMemo". | |
| Empty/"*" = all classes. | |
| .PARAMETER IncludeHidden | |
| Also includes invisible windows and controls (default: visible only). | |
| .PARAMETER TopLevelOnly | |
| Only reads top-level windows, no child controls. | |
| .PARAMETER TextOnly | |
| Suppresses control rows without text. | |
| .NOTES | |
| ControlId is the dialog item ID of a child control (GetDlgCtrlID): | |
| - For dialog-template-based masks (MFC/VB6-style) it is usually identical across | |
| program restarts and therefore hardcodable - unlike HWNDs, which change every run. | |
| - Controls created without an ID report 0; window rows show $null (top-level | |
| windows have no control ID by definition). | |
| - Static labels typically report -1 (IDC_STATIC). | |
| Stability check: dump the same mask twice across a program restart and diff the | |
| ControlId column (see second example). If the IDs are identical, the fill script | |
| can map fields by ControlId instead of sorted position. | |
| .EXAMPLE | |
| HwndEnumerator.ps1 | |
| .EXAMPLE | |
| HwndEnumerator.ps1 -TitleFilter 'Adresse bearbeiten' -ClassFilter 'Edit' | Export-Csv "$env:TEMP\ids-run1.csv" -NoTypeInformation | |
| Dumps all Edit controls of the mask. After a program restart, repeat with ids-run2.csv, then: | |
| Compare-Object (Import-Csv "$env:TEMP\ids-run1.csv") (Import-Csv "$env:TEMP\ids-run2.csv") -Property ControlId, ControlText | |
| .EXAMPLE | |
| HwndEnumerator.ps1 -TitleFilter 'Create record' -ClassFilter 'Edit' -TextOnly | |
| #> | |
| param( | |
| [string]$TitleFilter = '', | |
| [string]$ClassFilter = '*', | |
| [switch]$IncludeHidden, | |
| [switch]$TopLevelOnly, | |
| [switch]$TextOnly | |
| ) | |
| if (-not ('Win32Native' -as [type])) { | |
| Add-Type -TypeDefinition @' | |
| using System; | |
| using System.Text; | |
| using System.Runtime.InteropServices; | |
| public static class Win32Native { | |
| // callback type for EnumWindows/EnumChildWindows | |
| public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); | |
| // enumerate all top-level windows | |
| [DllImport("user32.dll")] | |
| [return: MarshalAs(UnmanagedType.Bool)] | |
| public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); | |
| // enumerate all (including nested) child windows of a window | |
| [DllImport("user32.dll")] | |
| [return: MarshalAs(UnmanagedType.Bool)] | |
| public static extern bool EnumChildWindows(IntPtr hWndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam); | |
| // read window class (e.g. "Edit") | |
| [DllImport("user32.dll", CharSet=CharSet.Unicode)] | |
| public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); | |
| // get the dialog item ID of a child control (0 for top-level windows / controls without ID) | |
| [DllImport("user32.dll")] | |
| public static extern int GetDlgCtrlID(IntPtr hWnd); | |
| // WM_GETTEXTLENGTH (0x000E) / WM_GETTEXT (0x000D) | |
| [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)] | |
| public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, StringBuilder lParam); | |
| // read screen position of a window (for sorting the controls) | |
| [DllImport("user32.dll")] | |
| [return: MarshalAs(UnmanagedType.Bool)] | |
| public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); | |
| // check visibility | |
| [DllImport("user32.dll")] | |
| [return: MarshalAs(UnmanagedType.Bool)] | |
| public static extern bool IsWindowVisible(IntPtr hWnd); | |
| // get the process ID of the window | |
| [DllImport("user32.dll")] | |
| public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); | |
| [StructLayout(LayoutKind.Sequential)] | |
| public struct RECT { public int Left, Top, Right, Bottom; } | |
| } | |
| '@ | |
| } | |
| function Get-WindowText { | |
| param([IntPtr]$Hwnd) | |
| $len = [Win32Native]::SendMessage($Hwnd, 0x000E, [IntPtr]::Zero, $null).ToInt32() | |
| if ($len -lt 1) { return '' } | |
| $sb = [System.Text.StringBuilder]::new($len + 1) | |
| [Win32Native]::SendMessage($Hwnd, 0x000D, [IntPtr]($len + 1), $sb) | Out-Null | |
| return $sb.ToString() | |
| } | |
| function Get-WindowClassName { | |
| param([IntPtr]$Hwnd) | |
| $sb = [System.Text.StringBuilder]::new(256) | |
| [Win32Native]::GetClassName($Hwnd, $sb, $sb.Capacity) | Out-Null | |
| return $sb.ToString() | |
| } | |
| # collect all top-level windows | |
| $topLevel = [System.Collections.Generic.List[IntPtr]]::new() | |
| $topCallback = [Win32Native+EnumWindowsProc]{ param($h, $l) | |
| $topLevel.Add($h) | |
| return $true # continue enumeration | |
| } | |
| [Win32Native]::EnumWindows($topCallback, [IntPtr]::Zero) | Out-Null | |
| $result = foreach ($hwnd in $topLevel) { | |
| if (-not $IncludeHidden -and -not [Win32Native]::IsWindowVisible($hwnd)) { continue } | |
| $title = Get-WindowText -Hwnd $hwnd | |
| if ($TitleFilter -and ($title -notlike "*$TitleFilter*")) { continue } | |
| [uint32]$processId = 0 | |
| [Win32Native]::GetWindowThreadProcessId($hwnd, [ref]$processId) | Out-Null | |
| $windowClass = Get-WindowClassName -Hwnd $hwnd | |
| # emit the window row itself (even if no controls are wanted/found) | |
| if (-not $TextOnly) { | |
| [pscustomobject]@{ | |
| WindowHwnd = '0x{0:X}' -f $hwnd.ToInt64() | |
| ProcessId = $processId | |
| Title = $title | |
| WindowClass = $windowClass | |
| ControlHwnd = $null | |
| ControlClass = $null | |
| ControlId = $null # top-level windows have no control ID | |
| ControlText = $null | |
| X = $null | |
| Y = $null | |
| } | |
| } | |
| if ($TopLevelOnly) { continue } | |
| # collect all (including nested) child controls of this window | |
| $children = [System.Collections.Generic.List[IntPtr]]::new() | |
| $childCallback = [Win32Native+EnumWindowsProc]{ param($h, $l) | |
| $children.Add($h) | |
| return $true | |
| } | |
| [Win32Native]::EnumChildWindows($hwnd, $childCallback, [IntPtr]::Zero) | Out-Null | |
| $childInfos = foreach ($child in $children) { | |
| if (-not $IncludeHidden -and -not [Win32Native]::IsWindowVisible($child)) { continue } | |
| $class = Get-WindowClassName -Hwnd $child | |
| if ($ClassFilter -and $ClassFilter -ne '*' -and ($class -notlike $ClassFilter)) { continue } | |
| $r = New-Object Win32Native+RECT | |
| [Win32Native]::GetWindowRect($child, [ref]$r) | Out-Null | |
| [pscustomobject]@{ | |
| Hwnd = $child | |
| Class = $class | |
| Id = [Win32Native]::GetDlgCtrlID($child) # dialog item ID (0 = none) | |
| X = $r.Left | |
| Y = $r.Top | |
| } | |
| } | |
| $sortedChildren = @($childInfos | Sort-Object Y, X) | |
| foreach ($c in $sortedChildren) { | |
| $text = Get-WindowText -Hwnd $c.Hwnd | |
| if ($TextOnly -and [string]::IsNullOrEmpty($text)) { continue } | |
| [pscustomobject]@{ | |
| WindowHwnd = '0x{0:X}' -f $hwnd.ToInt64() | |
| ProcessId = $processId | |
| Title = $title | |
| WindowClass = $windowClass | |
| ControlHwnd = '0x{0:X}' -f $c.Hwnd.ToInt64() | |
| ControlClass = $c.Class | |
| ControlId = $c.Id | |
| ControlText = $text | |
| X = $c.X | |
| Y = $c.Y | |
| } | |
| } | |
| } | |
| $result |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment