Skip to content

Instantly share code, notes, and snippets.

@eugrus
Last active September 8, 2026 08:25
Show Gist options
  • Select an option

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

Select an option

Save eugrus/a2a75eff6b9848ec0bb6b1119c2d43fa to your computer and use it in GitHub Desktop.
<#
.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