-
-
Save cprima/c8a85d6b6d2708cd2edc62dd5f7dfbc2 to your computer and use it in GitHub Desktop.
| <# | |
| .SYNOPSIS | |
| Interactive browser for all SAP-related windows — scripting and native Win32. | |
| .DESCRIPTION | |
| Enumerates every window that belongs to a running SAP process and presents | |
| them in a single numbered table. The user picks one; the script dumps its | |
| control tree. | |
| Two categories of window are covered: | |
| [Script] SAP GUI scripting windows — accessible via the SAP COM scripting | |
| API. Enumerated via an inline VBScript (cscript.exe bridge) to | |
| avoid the TYPE_E_CANTLOADLIBRARY error that prevents .NET COM | |
| interop from loading SAP's type library. Dump output: the full | |
| control tree (id | type | name | text), depth-first. | |
| [Win32] Native Win32 windows owned by SAP processes (saplogon.exe etc.), | |
| including session-timeout dialogs and other modal popups that are | |
| invisible to the SAP scripting API. Enumerated and dumped via | |
| Win32 P/Invoke (EnumWindows / EnumChildWindows). | |
| SAP processes searched by default: saplogon, sapgui, saplgpad. | |
| Prerequisites: | |
| - SAP GUI for Windows must be running. | |
| - Scripting must be enabled in SAP GUI options: | |
| Tools -> Options -> Accessibility & Scripting -> Scripting -> Enable scripting | |
| - cscript.exe must be available (standard on all Windows systems). | |
| .PARAMETER Format | |
| Output format for the control tree dump: text (default), json, csv, or html. | |
| html captures a screenshot of the target window and writes a self-contained | |
| HTML image map to -OutputPath. | |
| .PARAMETER OutputPath | |
| Directory where the .html and .png pair is written when -Format html is used. | |
| Defaults to $env:TEMP\SapDump. Created if absent. | |
| .PARAMETER NoOpen | |
| When -Format html is used, suppress automatically opening the HTML file in | |
| the default browser after writing. | |
| .EXAMPLE | |
| pwsh -File Dump-SapWindow.ps1 | |
| .EXAMPLE | |
| pwsh -File Dump-SapWindow.ps1 -Format json | |
| .EXAMPLE | |
| pwsh -File Dump-SapWindow.ps1 -Format html | |
| .EXAMPLE | |
| pwsh -File Dump-SapWindow.ps1 -Format html -OutputPath C:\Temp\MyDumps -NoOpen | |
| .NOTES | |
| Dump record fields (all formats): | |
| depth, id, type, name, text, screenX, screenY, width, height, actions | |
| text format: indented by depth, pipe-separated fields | |
| json format: array of objects | |
| csv format: RFC 4180 with header row | |
| html format: PNG screenshot + image map with <area> per interactive control | |
| #> | |
| #Requires -Version 5.1 | |
| [CmdletBinding()] | |
| param( | |
| [ValidateSet('text','json','csv','html')] | |
| [string]$Format = 'text', | |
| [string]$OutputPath = (Join-Path $env:TEMP 'SapDump'), | |
| [switch]$NoOpen | |
| ) | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = 'Stop' | |
| # ── Win32 P/Invoke helper ────────────────────────────────────────────────────── | |
| Add-Type @' | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Diagnostics; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| public class SapWin32 { | |
| private delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); | |
| [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc f, IntPtr lParam); | |
| [DllImport("user32.dll")] static extern bool EnumChildWindows(IntPtr parent, EnumWindowsProc f, IntPtr lParam); | |
| [DllImport("user32.dll")] static extern int GetWindowText(IntPtr h, StringBuilder s, int n); | |
| [DllImport("user32.dll")] static extern int GetClassName(IntPtr h, StringBuilder s, int n); | |
| [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid); | |
| [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h); | |
| [DllImport("user32.dll")] static extern int GetWindowLong(IntPtr h, int idx); | |
| [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint cmd); | |
| const uint GW_CHILD = 5; | |
| const uint GW_HWNDNEXT = 2; | |
| public class WinInfo { | |
| public IntPtr Hwnd; | |
| public string Title; | |
| public string ClassName; | |
| public uint Pid; | |
| public string ProcessName; | |
| public int CtrlId; | |
| } | |
| static string GetText(IntPtr h) { | |
| var sb = new StringBuilder(512); GetWindowText(h, sb, 512); return sb.ToString(); | |
| } | |
| static string GetCls(IntPtr h) { | |
| var sb = new StringBuilder(256); GetClassName(h, sb, 256); return sb.ToString(); | |
| } | |
| public static List<WinInfo> GetTopLevelWindows(string[] processNames) { | |
| var sapPids = new HashSet<uint>(); | |
| foreach (var p in Process.GetProcesses()) | |
| foreach (var n in processNames) | |
| if (p.ProcessName.ToLower().Contains(n.ToLower())) | |
| sapPids.Add((uint)p.Id); | |
| var result = new List<WinInfo>(); | |
| EnumWindows((hwnd, _) => { | |
| if (!IsWindowVisible(hwnd)) return true; | |
| uint pid; GetWindowThreadProcessId(hwnd, out pid); | |
| if (!sapPids.Contains(pid)) return true; | |
| string pname = ""; | |
| try { pname = Process.GetProcessById((int)pid).ProcessName; } catch {} | |
| result.Add(new WinInfo { | |
| Hwnd = hwnd, Title = GetText(hwnd), ClassName = GetCls(hwnd), | |
| Pid = pid, ProcessName = pname | |
| }); | |
| return true; | |
| }, IntPtr.Zero); | |
| return result; | |
| } | |
| public static List<WinInfo> GetDirectChildren(IntPtr parent) { | |
| var result = new List<WinInfo>(); | |
| var child = GetWindow(parent, GW_CHILD); | |
| while (child != IntPtr.Zero) { | |
| result.Add(new WinInfo { | |
| Hwnd = child, Title = GetText(child), ClassName = GetCls(child), | |
| CtrlId = GetWindowLong(child, -12) // GWL_ID | |
| }); | |
| child = GetWindow(child, GW_HWNDNEXT); | |
| } | |
| return result; | |
| } | |
| [StructLayout(LayoutKind.Sequential)] | |
| public struct RECT { public int Left, Top, Right, Bottom; } | |
| [DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr hwnd, IntPtr hDC, uint nFlags); | |
| [DllImport("gdi32.dll")] public static extern IntPtr CreateCompatibleDC(IntPtr hdc); | |
| [DllImport("gdi32.dll")] public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int w, int h); | |
| [DllImport("gdi32.dll")] public static extern IntPtr SelectObject(IntPtr hdc, IntPtr obj); | |
| [DllImport("gdi32.dll")] public static extern bool DeleteDC(IntPtr hdc); | |
| [DllImport("gdi32.dll")] public static extern bool DeleteObject(IntPtr obj); | |
| [DllImport("user32.dll")] public static extern IntPtr GetDC(IntPtr hwnd); | |
| [DllImport("user32.dll")] public static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc); | |
| [DllImport("gdi32.dll")] public static extern int GetDeviceCaps(IntPtr hdc, int index); | |
| [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); | |
| // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4 | |
| [DllImport("user32.dll")] public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext); | |
| } | |
| '@ | |
| # ── VBScript: enumerate SAP scripting windows (pipe-delimited) ──────────────── | |
| $vbsEnum = @' | |
| Option Explicit | |
| Dim SapGuiAuto, app, conn, ses, wnd | |
| Dim iConn, iSes, iWnd, txn, title | |
| Set SapGuiAuto = GetObject("SAPGUI") | |
| Set app = SapGuiAuto.GetScriptingEngine | |
| iConn = 0 | |
| For Each conn In app.Children | |
| iSes = 0 | |
| For Each ses In conn.Children | |
| On Error Resume Next | |
| txn = ses.Info.Transaction | |
| If Err.Number <> 0 Then txn = "" : Err.Clear | |
| On Error GoTo 0 | |
| iWnd = 0 | |
| For Each wnd In ses.Children | |
| On Error Resume Next | |
| title = wnd.Text | |
| If Err.Number <> 0 Then title = "" : Err.Clear | |
| On Error GoTo 0 | |
| WScript.Echo iConn & "|" & iSes & "|" & iWnd & "|" & txn & "|" & title & "|" & wnd.Handle | |
| iWnd = iWnd + 1 | |
| Next | |
| iSes = iSes + 1 | |
| Next | |
| iConn = iConn + 1 | |
| Next | |
| '@ | |
| # ── VBScript: dump SAP scripting control tree ───────────────────────────────── | |
| $vbsDump = @' | |
| Option Explicit | |
| Dim connIndex, sesIndex, wndIndex | |
| Dim SapGuiAuto, app, conn, ses | |
| Dim seen, iConn, iSes | |
| If WScript.Arguments.Count < 3 Then | |
| WScript.Echo "Usage: cscript dump.vbs <conn> <ses> <wnd>" | |
| WScript.Quit 1 | |
| End If | |
| connIndex = CInt(WScript.Arguments(0)) | |
| sesIndex = CInt(WScript.Arguments(1)) | |
| wndIndex = CInt(WScript.Arguments(2)) | |
| Set seen = CreateObject("Scripting.Dictionary") | |
| Set SapGuiAuto = GetObject("SAPGUI") | |
| Set app = SapGuiAuto.GetScriptingEngine | |
| Set conn = Nothing : iConn = 0 | |
| For Each conn In app.Children | |
| If iConn = connIndex Then Exit For | |
| iConn = iConn + 1 | |
| Next | |
| Set ses = Nothing : iSes = 0 | |
| For Each ses In conn.Children | |
| If iSes = sesIndex Then Exit For | |
| iSes = iSes + 1 | |
| Next | |
| DumpControls ses.FindById("wnd[" & wndIndex & "]"), 0 | |
| Sub DumpControls(obj, depth) | |
| Dim id, typ, name, text, child, hasChildren | |
| Dim sl, st, sw, sh, actions | |
| If depth > 20 Then Exit Sub | |
| id = SafeProp(obj,"Id") : typ = SafeProp(obj,"Type") | |
| name = SafeProp(obj,"Name") : text = SafeProp(obj,"Text") | |
| If id = "" Then Exit Sub | |
| If seen.Exists(id) Then Exit Sub | |
| seen.Add id, True | |
| sl = SafeNum(obj,"ScreenLeft") : st = SafeNum(obj,"ScreenTop") | |
| sw = SafeNum(obj,"Width") : sh = SafeNum(obj,"Height") | |
| actions = InteractionHint(typ) | |
| WScript.Echo depth & "|" & id & "|" & typ & "|" & name & "|" & text & "|" & sl & "|" & st & "|" & sw & "|" & sh & "|" & actions | |
| hasChildren = False | |
| On Error Resume Next | |
| hasChildren = (obj.Children.Count > 0) | |
| If Err.Number <> 0 Then Err.Clear : Exit Sub | |
| On Error GoTo 0 | |
| If Not hasChildren Then Exit Sub | |
| On Error Resume Next | |
| For Each child In obj.Children | |
| If Err.Number <> 0 Then Err.Clear : Exit For | |
| DumpControls child, depth + 1 | |
| Next | |
| On Error GoTo 0 | |
| End Sub | |
| Function InteractionHint(typ) | |
| Select Case typ | |
| Case "GuiButton": InteractionHint = "press" | |
| Case "GuiTextField", "GuiCTextField": InteractionHint = "read/write text" | |
| Case "GuiPasswordField": InteractionHint = "write text" | |
| Case "GuiComboBox": InteractionHint = "select key/value" | |
| Case "GuiCheckBox": InteractionHint = "toggle Selected" | |
| Case "GuiRadioButton": InteractionHint = "select" | |
| Case "GuiTab", "GuiTabStrip": InteractionHint = "select" | |
| Case "GuiGridView": InteractionHint = "read/write cells, select rows" | |
| Case "GuiLabel", "GuiStatusbar": InteractionHint = "read only" | |
| Case "GuiShell": InteractionHint = "interact" | |
| Case Else: InteractionHint = "" | |
| End Select | |
| End Function | |
| Function SafeNum(obj, p) | |
| On Error Resume Next | |
| Select Case p | |
| Case "ScreenLeft": SafeNum = obj.ScreenLeft | |
| Case "ScreenTop": SafeNum = obj.ScreenTop | |
| Case "Width": SafeNum = obj.Width | |
| Case "Height": SafeNum = obj.Height | |
| Case Else: SafeNum = "" | |
| End Select | |
| If Err.Number <> 0 Then Err.Clear : SafeNum = "" | |
| On Error GoTo 0 | |
| End Function | |
| Function SafeProp(obj, p) | |
| On Error Resume Next | |
| Select Case p | |
| Case "Id": SafeProp = obj.Id | |
| Case "Type": SafeProp = obj.Type | |
| Case "Name": SafeProp = obj.Name | |
| Case "Text": SafeProp = obj.Text | |
| Case Else: SafeProp = "" | |
| End Select | |
| If Err.Number <> 0 Then Err.Clear : SafeProp = "" | |
| On Error GoTo 0 | |
| End Function | |
| '@ | |
| # ── Helper: HTML-attribute encode ───────────────────────────────────────────── | |
| function ConvertTo-HtmlAttr([string]$s) { | |
| $s -replace '&','&' -replace '"','"' -replace '<','<' -replace '>','>' | |
| } | |
| # ── Helper: automation capabilities per SAP control type ───────────────────── | |
| function Get-Capabilities([string]$type) { | |
| switch ($type) { | |
| { $_ -in @('GuiButton','GuiShell','GuiMenubar','GuiMenu','GuiToolbar') } | |
| { return 'press' } | |
| { $_ -in @('GuiTextField','GuiCTextField','GuiPasswordField') } | |
| { return 'text' } | |
| 'GuiComboBox' { return 'key,text' } | |
| { $_ -in @('GuiCheckBox','GuiRadioButton','GuiTab') } | |
| { return 'selected' } | |
| 'GuiGridView' { return 'text,selected' } | |
| default { return '' } | |
| } | |
| } | |
| # ── Helper: semi-transparent overlay color per SAP control type ─────────────── | |
| function Get-ControlColor([string]$type) { | |
| switch ($type) { | |
| 'GuiButton' { return 'rgba(255,140,0,0.45)' } # orange | |
| { $_ -in @('GuiTextField','GuiCTextField','GuiPasswordField','GuiComboBox') } | |
| { return 'rgba(30,144,255,0.45)' } # blue | |
| { $_ -in @('GuiCheckBox','GuiRadioButton','GuiTab') } | |
| { return 'rgba(50,205,50,0.45)' } # green | |
| { $_ -in @('GuiGridView','GuiMenubar','GuiMenu','GuiToolbar') } | |
| { return 'rgba(148,0,211,0.45)' } # purple | |
| 'GuiShell' { return 'rgba(255,69,0,0.45)' } # red-orange (GOS / embedded toolbar) | |
| default { return 'rgba(200,200,200,0.35)' } # grey (label/status/titlebar) | |
| } | |
| } | |
| # ── Helper: capture HWND as PNG, return window rect ─────────────────────────── | |
| function Get-HwndScreenshot { | |
| param([IntPtr]$hwnd, [string]$pngPath) | |
| Add-Type -AssemblyName System.Drawing | |
| $rect = [SapWin32+RECT]::new() | |
| # Switch thread to per-monitor-aware-v2 so GetWindowRect returns physical pixels, | |
| # matching the physical-pixel coordinates that SAP's scripting API reports. | |
| $prevCtx = [SapWin32]::SetThreadDpiAwarenessContext([IntPtr]::new(-4)) | |
| [SapWin32]::GetWindowRect($hwnd, [ref]$rect) | Out-Null | |
| if ($prevCtx -ne [IntPtr]::Zero) { [SapWin32]::SetThreadDpiAwarenessContext($prevCtx) | Out-Null } | |
| $w = $rect.Right - $rect.Left | |
| $h = $rect.Bottom - $rect.Top | |
| $screenDC = [SapWin32]::GetDC([IntPtr]::Zero) | |
| $memDC = [SapWin32]::CreateCompatibleDC($screenDC) | |
| $hBmp = [SapWin32]::CreateCompatibleBitmap($screenDC, $w, $h) | |
| [SapWin32]::SelectObject($memDC, $hBmp) | Out-Null | |
| [SapWin32]::PrintWindow($hwnd, $memDC, 2) | Out-Null # PW_RENDERFULLCONTENT=2 | |
| $bmp = [System.Drawing.Bitmap]::FromHbitmap($hBmp) | |
| $bmp.Save($pngPath, [System.Drawing.Imaging.ImageFormat]::Png) | |
| $bmp.Dispose() | |
| [SapWin32]::DeleteObject($hBmp) | Out-Null | |
| [SapWin32]::DeleteDC($memDC) | Out-Null | |
| [SapWin32]::ReleaseDC([IntPtr]::Zero, $screenDC) | Out-Null | |
| # Derive DPI scale factor: physical / logical screen width. | |
| # Used by the caller to display the PNG at logical size in the browser. | |
| $hdc2 = [SapWin32]::GetDC([IntPtr]::Zero) | |
| $physW = [SapWin32]::GetDeviceCaps($hdc2, 118) # DESKTOPHORZRES (physical) | |
| $logW = [SapWin32]::GetDeviceCaps($hdc2, 8) # HORZRES (logical) | |
| [SapWin32]::ReleaseDC([IntPtr]::Zero, $hdc2) | Out-Null | |
| $scale = if ($logW -gt 0) { [double]$physW / $logW } else { 1.0 } | |
| return @{ Width = $w; Height = $h; Left = $rect.Left; Top = $rect.Top; Scale = $scale } | |
| } | |
| # ── Helper: run VBScript via cscript ────────────────────────────────────────── | |
| function Invoke-Vbs { | |
| param([string]$script, [string[]]$vbsArgs = @()) | |
| $tmp = [System.IO.Path]::GetTempFileName() -replace '\.tmp$', '.vbs' | |
| try { | |
| $script | Set-Content -Encoding ASCII $tmp | |
| $out = & cscript //nologo $tmp @vbsArgs 2>&1 | |
| if ($LASTEXITCODE -ne 0) { throw ($out -join "`n") } | |
| return $out | |
| } finally { | |
| Remove-Item $tmp -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| # ── Enumerate all SAP-related windows ───────────────────────────────────────── | |
| $sapProcessNames = @('saplogon', 'sapgui', 'saplgpad') | |
| $targets = [System.Collections.Generic.List[PSCustomObject]]::new() | |
| # 1. SAP scripting windows — enumerate first to build HWND dedup set | |
| Write-Verbose 'Enumerating SAP scripting windows via cscript...' | |
| $scriptHwnds = [System.Collections.Generic.HashSet[IntPtr]]::new() | |
| $scriptEntries = [System.Collections.Generic.List[PSCustomObject]]::new() | |
| try { | |
| $lines = @(Invoke-Vbs $vbsEnum) | |
| foreach ($line in $lines) { | |
| $p = $line -split '\|', 6 | |
| $hwnd = if ($p.Count -ge 6 -and $p[5]) { [IntPtr][long]$p[5] } else { [IntPtr]::Zero } | |
| if ($hwnd -ne [IntPtr]::Zero) { [void]$scriptHwnds.Add($hwnd) } | |
| $scriptEntries.Add([PSCustomObject]@{ | |
| Kind = 'Script' | |
| Detail = "conn=$($p[0]) ses=$($p[1]) wnd=$($p[2])" | |
| Process = 'sapgui (scripting)' | |
| Info = $p[3] | |
| Title = if ($p.Count -ge 5) { $p[4] } else { '' } | |
| _hwnd = $hwnd | |
| _conn = [int]$p[0]; _ses = [int]$p[1]; _wnd = [int]$p[2] | |
| }) | |
| } | |
| Write-Verbose "Scripting windows found: $($scriptEntries.Count)" | |
| } catch { | |
| Write-Verbose "SAP scripting windows not available: $_" | |
| } | |
| # 2. Win32 windows — skip any whose HWND is already covered by a Script entry | |
| Write-Verbose 'Enumerating Win32 windows from SAP processes...' | |
| $win32Windows = [SapWin32]::GetTopLevelWindows($sapProcessNames) | |
| foreach ($w in $win32Windows) { | |
| if (-not $w.Title) { continue } # skip untitled framework/helper windows | |
| if ($scriptHwnds.Contains($w.Hwnd)) { continue } # skip — already represented as [Script] | |
| $targets.Add([PSCustomObject]@{ | |
| '#' = $targets.Count | |
| Kind = 'Win32' | |
| Detail = "pid=$($w.Pid)" | |
| Process = $w.ProcessName | |
| Info = $w.ClassName | |
| Title = $w.Title | |
| _hwnd = $w.Hwnd | |
| _conn = -1; _ses = -1; _wnd = -1 | |
| }) | |
| } | |
| Write-Verbose "Win32-only windows added: $($targets.Count)" | |
| # 3. Append Script entries | |
| foreach ($e in $scriptEntries) { | |
| $e | Add-Member -NotePropertyName '#' -NotePropertyValue $targets.Count | |
| $targets.Add($e) | |
| } | |
| if ($targets.Count -eq 0) { | |
| Write-Error 'No SAP-related windows found.' | |
| exit 1 | |
| } | |
| # ── Present unified menu ─────────────────────────────────────────────────────── | |
| Write-Host '' | |
| $targets | Format-Table -AutoSize '#', Kind, Detail, Process, Info, Title | |
| $raw = Read-Host 'Select # to dump' | |
| if ($raw -notmatch '^\d+$' -or [int]$raw -ge $targets.Count) { | |
| Write-Error "Invalid selection: $raw" | |
| exit 1 | |
| } | |
| $sel = $targets[[int]$raw] | |
| # Store for use in Out-DumpRecords (needed by -Format html) | |
| $script:selectedHwnd = $sel._hwnd | |
| $script:selectedSlug = if ($sel.Kind -eq 'Script') { $sel.Info } else { $sel.Title } | |
| $script:selectedLabel = "[$($sel.Kind)] $($sel.Title)" | |
| Write-Host '' | |
| Write-Host "=== [$($sel.Kind)] $($sel.Detail) $($sel.Title) ===" -ForegroundColor Cyan | |
| Write-Host '' | |
| # ── Dump ─────────────────────────────────────────────────────────────────────── | |
| function Out-DumpRecords { | |
| param([PSCustomObject[]]$records) | |
| switch ($Format) { | |
| 'json' { $records | ConvertTo-Json -Depth 3 } | |
| 'csv' { $records | ConvertTo-Csv -NoTypeInformation } | |
| 'html' { | |
| if (-not (Test-Path $OutputPath)) { New-Item $OutputPath -ItemType Directory | Out-Null } | |
| # Build file stem from transaction / window title | |
| $rawSlug = ($script:selectedSlug -split '\s+')[0] -replace '[^A-Za-z0-9_-]', '' | |
| if ($rawSlug.Length -gt 32) { $rawSlug = $rawSlug.Substring(0, 32) } | |
| if (-not $rawSlug) { $rawSlug = 'window' } | |
| $stamp = Get-Date -Format 'yyyyMMdd_HHmmss' | |
| $stem = "SapDump_${rawSlug}_${stamp}" | |
| $pngFile = "$stem.png" | |
| $pngPath = Join-Path $OutputPath $pngFile | |
| $htmlPath = Join-Path $OutputPath "$stem.html" | |
| $dims = Get-HwndScreenshot -hwnd $script:selectedHwnd -pngPath $pngPath | |
| $winLeft = $dims.Left | |
| $winTop = $dims.Top | |
| $imgW = $dims.Width | |
| $imgH = $dims.Height | |
| # Display the PNG at logical (CSS) pixels so it matches the real window size on screen. | |
| $scale = $dims.Scale | |
| $dispW = [int]($imgW / $scale) | |
| $dispH = [int]($imgH / $scale) | |
| # Leaf/interactive types only — containers overlap descendants and are excluded | |
| $overlayTypes = [System.Collections.Generic.HashSet[string]]@( | |
| 'GuiButton', 'GuiTextField', 'GuiCTextField', 'GuiPasswordField', | |
| 'GuiComboBox', 'GuiCheckBox', 'GuiRadioButton', 'GuiTab', | |
| 'GuiGridView', 'GuiLabel', 'GuiStatusbar', 'GuiTitlebar', | |
| 'GuiMenubar', 'GuiMenu', 'GuiToolbar', 'GuiShell' | |
| ) | |
| # Build absolutely-positioned <div> overlays (visible, hover-able) | |
| $usr = $records | Where-Object { $_.id -match '/wnd\[\d+\]/usr$' } | Select-Object -First 1 | |
| $usrX = if ($usr) { [double]$usr.screenX } else { 0.0 } | |
| $usrY = if ($usr) { [double]$usr.screenY } else { 0.0 } | |
| # GuiGridView divs are emitted first so they sit behind all other overlays. | |
| # Same-z-index elements stack in DOM source order; earlier = lower. | |
| $gridDivs = [System.Collections.Generic.List[string]]::new() | |
| $overlayDivs = [System.Collections.Generic.List[string]]::new() | |
| foreach ($r in $records) { | |
| if (-not $overlayTypes.Contains([string]$r.type)) { continue } | |
| if (-not $r.PSObject.Properties['screenX'] -or "$($r.screenX)" -eq '') { continue } | |
| $rx = [double]$r.screenX | |
| $ry = [double]$r.screenY | |
| $rw = [double]$r.width | |
| $rh = [double]$r.height | |
| if ($rw -le 0 -or $rh -le 0 -or $rx -lt 0 -or $ry -lt 0) { continue } | |
| $isUsrChild = ($usr -and [string]$r.id -match '/usr/' -and [string]$r.id -notmatch '/wnd\[\d+\]/usr$') | |
| if ($isUsrChild) { | |
| $x1 = ($usrX - $winLeft) + (($rx - $usrX) * $scale) | |
| $y1 = ($usrY - $winTop) + (($ry - $usrY) * $scale) | |
| $sw = [int][math]::Round($rw * $scale) | |
| $sh = [int][math]::Round($rh * $scale) | |
| } | |
| else { | |
| $x1 = $rx - $winLeft | |
| $y1 = $ry - $winTop | |
| $sw = [int][math]::Round($rw) | |
| $sh = [int][math]::Round($rh) | |
| } | |
| if ($sw -le 0 -or $sh -le 0) { continue } | |
| $x1p = [math]::Round(100.0 * $x1 / $imgW, 3) | |
| $y1p = [math]::Round(100.0 * $y1 / $imgH, 3) | |
| $swp = [math]::Round(100.0 * $sw / $imgW, 3) | |
| $shp = [math]::Round(100.0 * $sh / $imgH, 3) | |
| $color = Get-ControlColor $r.type | |
| $info = ConvertTo-HtmlAttr "$($r.id) | $($r.type) | $($r.text)" | |
| $dname = ConvertTo-HtmlAttr "$($r.name)" | |
| $caps = ConvertTo-HtmlAttr (Get-Capabilities $r.type) | |
| $tid = ConvertTo-HtmlAttr $r.id | |
| $zidx = if ([string]$r.type -eq 'GuiGridView') { 1 } else { 2 } | |
| $div = " <div class=`"ov`" style=`"left:${x1p}%;top:${y1p}%;width:${swp}%;height:${shp}%;background:$color;z-index:${zidx}`" data-info=`"$info`" data-name=`"$dname`" data-caps=`"$caps`" title=`"$tid`"></div>" | |
| if ([string]$r.type -eq 'GuiGridView') { $gridDivs.Add($div) } else { $overlayDivs.Add($div) } | |
| } | |
| $overlayDivs.InsertRange(0, $gridDivs) | |
| $htmlTitle = ConvertTo-HtmlAttr $script:selectedLabel | |
| $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' | |
| $overlayHtml = $overlayDivs -join "`n" | |
| $htmlContent = @" | |
| <!DOCTYPE html> | |
| <html><head> | |
| <meta charset="utf-8"> | |
| <title>SAP Dump - $htmlTitle</title> | |
| <style> | |
| body { font-family:monospace; background:#1e1e1e; color:#d4d4d4; margin:1em } | |
| h2 { color:#9cdcfe } | |
| .wrap { position:relative; display:inline-block; line-height:0 } | |
| .ov { position:absolute; box-sizing:border-box; | |
| border:1px solid rgba(255,255,255,0.35); cursor:default } | |
| .ov:hover { border:2px solid #fff; z-index:10 } | |
| #tt { position:fixed; display:none; pointer-events:none; z-index:9999; | |
| background:#252526; border:1px solid #454545; border-radius:6px; | |
| padding:.6em .9em; box-shadow:0 4px 18px rgba(0,0,0,.7); | |
| max-width:460px; min-width:180px } | |
| #tt .tt-name { color:#4ec9b0; font-weight:bold; font-size:1.05em; | |
| margin-bottom:.45em; word-break:break-all } | |
| #tt .tt-caps { display:flex; gap:.5em; margin-bottom:.5em; flex-wrap:wrap } | |
| #tt .tt-cap { font-size:.78em; padding:1px 7px; border-radius:3px; | |
| border:1px solid; font-family:monospace } | |
| #tt .tt-id { color:#569cd6; font-size:.78em; word-break:break-all; | |
| margin-bottom:.2em } | |
| #tt .tt-row { color:#888; font-size:.78em } | |
| .legend { margin-top:.8em; font-size:.8em; display:flex; gap:1.2em; flex-wrap:wrap } | |
| .li { display:flex; align-items:center; gap:.35em } | |
| .sw { display:inline-block; width:.9em; height:.9em; | |
| border:1px solid rgba(255,255,255,.4) } | |
| </style> | |
| </head><body> | |
| <h2>$htmlTitle</h2> | |
| <p style="color:#888;font-size:.8em">$ts · $pngFile · origin ($winLeft,$winTop) · ${imgW}×${imgH}px physical · scale $([math]::Round($scale,3))</p> | |
| <div class="wrap"> | |
| <img src="$pngFile" width="$dispW" height="$dispH"> | |
| $overlayHtml | |
| </div> | |
| <div id="tt"> | |
| <div class="tt-name" id="tt-name"></div> | |
| <div class="tt-caps" id="tt-caps"></div> | |
| <div class="tt-id" id="tt-id"></div> | |
| <div class="tt-row" id="tt-type"></div> | |
| <div class="tt-row" id="tt-text"></div> | |
| </div> | |
| <div class="legend"> | |
| <span class="li"><span class="sw" style="background:rgba(255,140,0,0.6)"></span>GuiButton</span> | |
| <span class="li"><span class="sw" style="background:rgba(30,144,255,0.6)"></span>TextField / ComboBox</span> | |
| <span class="li"><span class="sw" style="background:rgba(50,205,50,0.6)"></span>CheckBox / RadioButton / Tab</span> | |
| <span class="li"><span class="sw" style="background:rgba(148,0,211,0.6)"></span>GridView / Menu / Toolbar</span> | |
| <span class="li"><span class="sw" style="background:rgba(200,200,200,0.5)"></span>Label / StatusBar / Titlebar</span> | |
| </div> | |
| <script> | |
| const tt = document.getElementById('tt'); | |
| const ttName = document.getElementById('tt-name'); | |
| const ttCaps = document.getElementById('tt-caps'); | |
| const ttId = document.getElementById('tt-id'); | |
| const ttType = document.getElementById('tt-type'); | |
| const ttText = document.getElementById('tt-text'); | |
| const capDef = [ | |
| {cap:'press', color:'#ff8c00'}, | |
| {cap:'text', color:'#1e90ff'}, | |
| {cap:'key', color:'#ffd700'}, | |
| {cap:'selected',color:'#32cd32'} | |
| ]; | |
| function showTt(d, mx, my) { | |
| const parts = (d.dataset.info||'').split('|').map(s=>s.trim()); | |
| const caps = (d.dataset.caps||'').split(','); | |
| ttName.textContent = d.dataset.name || ''; | |
| ttCaps.innerHTML = capDef.map(({cap,color}) => { | |
| const on = caps.includes(cap); | |
| const style = on | |
| ? 'border-color:'+color+';color:'+color | |
| : 'border-color:#444;color:#555'; | |
| return '<span class="tt-cap" style="'+style+'">'+(on?'✓':'✗')+' '+cap+'</span>'; | |
| }).join(''); | |
| ttId.textContent = parts[0] || ''; | |
| ttType.textContent = parts[1] || ''; | |
| ttText.textContent = parts[2] ? 'text: '+parts[2] : ''; | |
| tt.style.display = 'block'; | |
| moveTt(mx, my); | |
| } | |
| function moveTt(mx, my) { | |
| const ox = 14, oy = 14; | |
| let x = mx + ox, y = my + oy; | |
| const tw = tt.offsetWidth, th = tt.offsetHeight; | |
| if (x + tw > window.innerWidth - 8) x = mx - tw - ox; | |
| if (y + th > window.innerHeight - 8) y = my - th - oy; | |
| tt.style.left = x + 'px'; | |
| tt.style.top = y + 'px'; | |
| } | |
| document.querySelectorAll('.ov').forEach(d => { | |
| d.addEventListener('mouseenter', e => showTt(d, e.clientX, e.clientY)); | |
| d.addEventListener('mousemove', e => moveTt(e.clientX, e.clientY)); | |
| d.addEventListener('mouseleave', () => { tt.style.display = 'none'; }); | |
| }); | |
| </script> | |
| </body></html> | |
| "@ | |
| $htmlContent | Set-Content -Encoding UTF8 $htmlPath | |
| Write-Host "Saved: $htmlPath" | |
| if (-not $NoOpen) { Start-Process $htmlPath } | |
| } | |
| default { | |
| foreach ($r in $records) { | |
| $indent = ' ' * [int]$r.depth | |
| $line = "$indent$($r.id) | $($r.type) | $($r.name) | $($r.text)" | |
| $extras = @() | |
| if ($r.PSObject.Properties['screenX'] -and $r.screenX -ne '') { | |
| $extras += "[$($r.screenX),$($r.screenY) $($r.width)x$($r.height)]" | |
| } | |
| if ($r.PSObject.Properties['actions'] -and $r.actions -ne '') { | |
| $extras += $r.actions | |
| } | |
| if ($extras) { $line += " $($extras -join ' ')" } | |
| Write-Host $line | |
| } | |
| } | |
| } | |
| } | |
| if ($sel.Kind -eq 'Script') { | |
| try { | |
| $raw = Invoke-Vbs $vbsDump @("$($sel._conn)", "$($sel._ses)", "$($sel._wnd)") | |
| $records = $raw | ForEach-Object { | |
| $p = $_ -split '\|', 10 | |
| [PSCustomObject]@{ | |
| depth = [int]$p[0]; id = $p[1]; type = $p[2]; name = $p[3]; text = $p[4] | |
| screenX = $p[5]; screenY = $p[6]; width = $p[7]; height = $p[8] | |
| actions = if ($p.Count -ge 10) { $p[9] } else { '' } | |
| } | |
| } | |
| Out-DumpRecords $records | |
| } catch { | |
| Write-Error "Script dump failed.`n$_" | |
| exit 1 | |
| } | |
| } else { | |
| # Win32 — recursive tree, depth-first via GetDirectChildren | |
| $records = [System.Collections.Generic.List[PSCustomObject]]::new() | |
| $visited = [System.Collections.Generic.HashSet[IntPtr]]::new() | |
| function Build-Win32Records { | |
| param([IntPtr]$hwnd, [int]$depth, [string]$cls, [string]$title, [int]$ctrlId) | |
| if (-not $visited.Add($hwnd)) { return } | |
| $records.Add([PSCustomObject]@{ | |
| depth = $depth | |
| id = "0x$($hwnd.ToInt64().ToString('X'))" | |
| type = $cls | |
| name = "ctrlid=$ctrlId" | |
| text = $title | |
| }) | |
| foreach ($c in [SapWin32]::GetDirectChildren($hwnd)) { | |
| Build-Win32Records $c.Hwnd ($depth + 1) $c.ClassName $c.Title $c.CtrlId | |
| } | |
| } | |
| Build-Win32Records $sel._hwnd 0 $sel.Info $sel.Title 0 | |
| Out-DumpRecords $records | |
| } |
| #Requires -Version 5.1 | |
| <# | |
| .SYNOPSIS | |
| Probe DPI settings and compare SAP scripting coordinates against | |
| GetWindowRect (logical) and GetWindowRect (physical) for every open | |
| SAP window. Run on the SAP machine; paste full output as feedback. | |
| #> | |
| Add-Type -TypeDefinition @' | |
| using System; | |
| using System.Runtime.InteropServices; | |
| public class SapDpiProbe { | |
| [DllImport("user32.dll")] public static extern IntPtr GetDC(IntPtr h); | |
| [DllImport("gdi32.dll")] public static extern int GetDeviceCaps(IntPtr h, int i); | |
| [DllImport("user32.dll")] public static extern int ReleaseDC(IntPtr h, IntPtr dc); | |
| [StructLayout(LayoutKind.Sequential)] | |
| public struct RECT { public int Left, Top, Right, Bottom; } | |
| [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hwnd, out RECT r); | |
| [DllImport("user32.dll")] public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr ctx); | |
| } | |
| '@ | |
| Write-Host '' | |
| Write-Host '=== Screen DPI ===' -ForegroundColor Yellow | |
| $dc = [SapDpiProbe]::GetDC([IntPtr]::Zero) | |
| $logDpi = [SapDpiProbe]::GetDeviceCaps($dc, 88) # LOGPIXELSX | |
| $logW = [SapDpiProbe]::GetDeviceCaps($dc, 8) # HORZRES (logical px) | |
| $logH = [SapDpiProbe]::GetDeviceCaps($dc, 10) # VERTRES | |
| $physW = [SapDpiProbe]::GetDeviceCaps($dc, 118) # DESKTOPHORZRES (physical px) | |
| $physH = [SapDpiProbe]::GetDeviceCaps($dc, 117) # DESKTOPVERTRES | |
| [SapDpiProbe]::ReleaseDC([IntPtr]::Zero, $dc) | Out-Null | |
| "LOGPIXELSX (screen DC) : $logDpi (96=100%, 120=125%, 144=150%, 192=200%)" | |
| "Logical resolution : ${logW} x ${logH}" | |
| "Physical resolution : ${physW} x ${physH}" | |
| "Phys/log scale factor : $([math]::Round($physW / $logW, 4))" | |
| Write-Host '' | |
| Write-Host '=== SAP window: scripting coords vs GetWindowRect ===' -ForegroundColor Yellow | |
| # Enumerate open SAP windows via VBScript (avoids TYPE_E_CANTLOADLIBRARY) | |
| $vbs = @' | |
| On Error Resume Next | |
| Dim SapGuiAuto, app, conn, ses, wnd | |
| Set SapGuiAuto = GetObject("SAPGUI") | |
| If Err.Number <> 0 Then WScript.Echo "ERR:" & Err.Description : WScript.Quit 1 | |
| Set app = SapGuiAuto.GetScriptingEngine | |
| For Each conn In app.Children | |
| For Each ses In conn.Children | |
| For Each wnd In ses.Children | |
| WScript.Echo wnd.Handle & "|" & wnd.ScreenLeft & "|" & wnd.ScreenTop & "|" & wnd.Width & "|" & wnd.Height & "|" & wnd.Text | |
| Next | |
| Next | |
| Next | |
| '@ | |
| $tmp = [IO.Path]::GetTempFileName() -replace '\.tmp$', '.vbs' | |
| $vbs | Set-Content -Encoding ASCII $tmp | |
| $lines = @(& cscript //nologo $tmp 2>&1) | |
| Remove-Item $tmp -Force | |
| if (-not $lines) { | |
| Write-Host 'No SAP scripting windows found (SAP not running or scripting disabled).' -ForegroundColor Red | |
| } else { | |
| foreach ($line in $lines) { | |
| if ($line -match '^ERR:') { Write-Host $line -ForegroundColor Red; continue } | |
| $p = "$line" -split '\|', 6 | |
| if ($p.Count -lt 5) { continue } | |
| $hwnd = [IntPtr][long]$p[0] | |
| # GetWindowRect without DPI override (DPI-unaware = logical coordinates) | |
| $rLog = [SapDpiProbe+RECT]::new() | |
| [SapDpiProbe]::GetWindowRect($hwnd, [ref]$rLog) | Out-Null | |
| # GetWindowRect with per-monitor-aware-v2 context (physical coordinates) | |
| $prevCtx = [SapDpiProbe]::SetThreadDpiAwarenessContext([IntPtr]::new(-4)) | |
| $rPhys = [SapDpiProbe+RECT]::new() | |
| [SapDpiProbe]::GetWindowRect($hwnd, [ref]$rPhys) | Out-Null | |
| if ($prevCtx -ne [IntPtr]::Zero) { [SapDpiProbe]::SetThreadDpiAwarenessContext($prevCtx) | Out-Null } | |
| $title = if ($p.Count -ge 6) { $p[5] } else { '' } | |
| $sapL = [int]$p[1]; $sapT = [int]$p[2] | |
| $sapW = [int]$p[3]; $sapH = [int]$p[4] | |
| $logWR = $rLog.Right - $rLog.Left; $logHR = $rLog.Bottom - $rLog.Top | |
| $physWR = $rPhys.Right - $rPhys.Left; $physHR = $rPhys.Bottom - $rPhys.Top | |
| Write-Host "Window : $title" -ForegroundColor Cyan | |
| " SAP ScreenLeft/Top : ($sapL, $sapT) SAP Width/Height : ${sapW} x ${sapH}" | |
| " GetWindowRect LOGICAL : L=$($rLog.Left) T=$($rLog.Top) ${logWR} x ${logHR}" | |
| " GetWindowRect PHYSICAL : L=$($rPhys.Left) T=$($rPhys.Top) ${physWR} x ${physHR}" | |
| " SAP vs log offset : dL=$($sapL - $rLog.Left) dT=$($sapT - $rLog.Top)" | |
| " SAP vs phys offset : dL=$($sapL - $rPhys.Left) dT=$($sapT - $rPhys.Top)" | |
| "" | |
| } | |
| } | |
| Write-Host '=== done ===' -ForegroundColor Yellow |
pwsh -File Dump-SapWindow.ps1 -Verbose
# Conn Session Window Transaction Title
- ---- ------- ------ ----------- -----
0 0 0 0 SESSION_MANAGER SAP Easy Access
Select # to dump: 0
=== conn[0] ses[0] wnd[0] [SESSION_MANAGER] SAP Easy Access ===
/app/con[0]/ses[0]/wnd[0] | GuiMainWindow | wnd[0] | SAP Easy Access
/app/con[0]/ses[0]/wnd[0]/mbar | GuiMenubar | mbar |
Popup 1
<wnd app='saplogon.exe' cls='#32770' title='SAP GUI for Windows 770' aastate='moveable, focusable' />
<wnd ctrlid='65535' title='XYZ: auto logout (maximum user idle time exceeded) Reconnect to*' aaname='XYZ: auto logout (maximum user idle time exceeded) Reconnect to system if required Do you want to see the detailed error description? ' aastate='read only' automationid='65535' cls='Static' />
<wnd app='saplogon.exe' cls='#32770' title='SAP GUI for Windows 770' aastate='moveable, focusable' />
<wnd ctrlid='1' title='&Yes' aaname='Yes' aastate='focused, hot tracked, default, focusable' automationid='1' cls='Button' />
Popup2
<wnd app='saplogon.exe' cls='#32770' title='SAP GUI for Windows 770' aastate='moveable, focusable' />
<wnd ctrlid='65535' title='NAB: auto logout (maximum user idle time exceeded) Time		Wed Ap*' aaname='NAB: auto logout (maximum user idle time exceeded) Time		Wed Apr 1 11:15:56 2026 Component	DPTM Release		753 Version		10 Return Code	-22 Counter		1 ' aastate='read only' automationid='65535' cls='Static' />
<wnd app='saplogon.exe' cls='#32770' title='SAP GUI for Windows 770' aastate='moveable, focusable' />
<wnd ctrlid='2' title='OK' aaname='OK' aastate='hot tracked, focusable' automationid='2' cls='Button' />
pwsh -File Dump-SapWindow.ps1 -Verbose
# Kind Detail Process Info Title
- ---- ------ ------- ---- -----
0 Win32 pid=528 saplogon #32770 SAP GUI for Windows 770
1 Win32 pid=528 saplogon WindowsFormsSapFocus
2 Win32 pid=528 saplogon WindowsFormsSapFocus
3 Win32 pid=528 saplogon WindowsFormsSapFocus
4 Win32 pid=528 saplogon WindowsFormsSapFocus
5 Win32 pid=528 saplogon #32770 Ihr Name/Adresse ist unvollständig
6 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:00000000
7 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:00000000
8 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:00000000
9 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:00000000
10 Win32 pid=528 saplogon SAP_FRONTEND_SESSION SAP
11 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:000B0688
12 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:000B0688
13 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:000B0688
14 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:000B0688
15 Win32 pid=528 saplogon #32770 SAP Logon 770
16 Win32 pid=528 saplogon Afx:00F80000:0:00010003:00000000:007F06C3
17 Win32 pid=528 saplogon Afx:00F80000:0:00010003:00000000:007F06C3
18 Win32 pid=528 saplogon Afx:00F80000:0:00010003:00000000:007F06C3
19 Win32 pid=528 saplogon Afx:00F80000:0:00010003:00000000:007F06C3
20 Win32 pid=528 saplogon SAP_FRONTEND_SESSION SAP Easy Access
21 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:000B0688
22 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:000B0688
23 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:000B0688
24 Win32 pid=528 saplogon Afx:646B0000:0:00010003:00000000:000B0688
25 Script conn=0 ses=0 wnd=0 sapgui (scripting) SESSION_MANAGER SAP Easy Access
26 Script conn=1 ses=0 wnd=0 sapgui (scripting) S000 SAP
27 Script conn=1 ses=0 wnd=1 sapgui (scripting) S000 Ihr Name/Adresse ist unvollständig
Select # to dump: 0
=== [Win32] pid=528 SAP GUI for Windows 770 ===
0x408DC + | #32770 | ctrlid=0 | SAP GUI for Windows 770
0x1108F0 + | Button | ctrlid=2 | OK
0x50886 + | Static | ctrlid=20 |
0x3D067E + | Static | ctrlid=65535 | XYZ: Backend--Session wurde vom System beendet
The line
if (-not $w.Title) { continue } # skip untitled framework/helper windowsfilters out the noise. EnumWindows returns every top-level window owned by the SAP process, including internal plumbing that is never shown to the user.
Afx:... windows are created automatically by MFC (Microsoft Foundation Classes), the C++ UI framework SAP GUI is built on. Every MFC application registers several structural windows alongside its visible ones — frame containers, message routing sinks, sizing helpers. Their class names encode a hash of the module and internal IDs (Afx:646B0000:0:00010003:...). They have no title because they are never rendered as dialogs.
WindowsFormsSapFocus is a SAP-specific hidden window used to broker keyboard focus across SAP GUI's mixed Win32/MFC window hierarchy. No title, never visible to the user.
Both pass the IsWindowVisible check (which is why they appeared in the list) but have no title. Any window a user is supposed to interact with has a title — so Title != "" is a reliable proxy for "real dialog or session window, not framework scaffolding".
Deduplication via HWND matching
Both EnumWindows (Win32) and the SAP scripting API enumerate the same physical windows. Every SAP GUI scripting window (wnd[N]) has an underlying Win32 handle that EnumWindows also picks up, causing duplicates.
Fix: SAP GUI scripting window objects expose a .Handle property — the actual Win32 HWND. The VBScript enumerator now outputs this as a 6th pipe-delimited field. PowerShell collects all Script HWNDs into a HashSet[IntPtr] and skips any Win32 entry whose HWND is already in that set.
Result — three categories in the table:
[Win32]— pure native dialogs with no scripting counterpart: SAP Logon, session-timeout popups[Script]— windows accessible via the SAP scripting API: sessions, modal dialogs opened within a session
Why HWND and not title matching: titles can be shared across windows; HWND is a unique handle for the lifetime of the window — no false positives possible.
Which windows appear as [Win32] after deduplication
After HWND-based deduplication removes all windows already covered by the SAP scripting API, two categories of Win32-only windows remain:
SAP Logon (#32770, title "SAP Logon 770") — the connection manager application where SAP systems are configured and sessions are launched. It has no scripting session; it is the launcher, not a session.
Session-timeout popups (#32770, title "SAP GUI for Windows 770") — native dialogs raised by SAP Logon when a backend session has been terminated due to inactivity. Two variants exist: a reconnect prompt (button Yes/No) and a detailed error dialog (button OK). Both are invisible to the SAP scripting API.
Improvement 1 — Output formats (text / json / csv)
A -Format parameter has been added with three values:
pwsh -File Dump-SapWindow.ps1 # text (default)
pwsh -File Dump-SapWindow.ps1 -Format json
pwsh -File Dump-SapWindow.ps1 -Format csvHow it works:
The VBScript dumper now emits one structured pipe-delimited record per control instead of pre-indented text:
depth|id|type|name|text
PowerShell parses these records and renders them according to -Format:
text— reconstructs the indented tree view (same appearance as before)json— array of objects viaConvertTo-Jsoncsv— RFC 4180 with header row viaConvertTo-Csv
The [Win32] dump follows the same record structure (depth=0 for the root window, depth=1 for children), so all three formats apply to both window kinds consistently.
Improvement 2 — Recursive Win32 dump with depth
The [Win32] dump previously listed only direct children in a flat list with no nesting information. This was sufficient for simple dialogs but missed nested control groups.
Fix: replaced EnumChildWindows (which recurses internally but returns all descendants flat) with GetWindow(GW_CHILD) + GetWindow(GW_HWNDNEXT) to enumerate only direct children at each level. A recursive PowerShell function Build-Win32Records then walks the tree depth-first, tracking depth explicitly.
The result is a proper tree — the same structure and record format (depth|id|type|name|text) as the [Script] dump, so -Format json and -Format csv apply equally to both window kinds.
Improvement 3 — Interaction map + screen coordinates
Each control line in the [Script] dump now includes two additional pieces of information:
Screen bounds — ScreenLeft, ScreenTop, Width, Height read from the SAP scripting object. Informational only; not used for mouse interaction.
Interaction hint — derived from the control's Type:
| Type | Hint |
|---|---|
| GuiButton | press |
| GuiTextField, GuiCTextField | read/write text |
| GuiPasswordField | write text |
| GuiComboBox | select key/value |
| GuiCheckBox | toggle Selected |
| GuiRadioButton | select |
| GuiTab, GuiTabStrip | select |
| GuiGridView | read/write cells, select rows |
| GuiLabel, GuiStatusBar | read only |
In text format these appear as a suffix: [x,y wxh] action. In json and csv they are separate fields (screenX, screenY, width, height, actions).
The interaction hints are language-agnostic — they are derived from the technical Type string, which is always English regardless of the SAP UI language.
Improvement 4 — -Format html: screenshot + interactive image map
What it does
-Format html captures the target SAP window as a PNG screenshot (using PrintWindow — works even if the window is partially off-screen or obscured), then writes a self-contained HTML file that overlays a <map> image map on the screenshot. Hovering over any highlighted control region shows its scripting ID, type, and interaction hint in a status bar below the image.
Two files are written per run to the same directory:
SapDump_{slug}_{yyyyMMdd_HHmmss}.png— the screenshotSapDump_{slug}_{yyyyMMdd_HHmmss}.html— the image map page
{slug} comes from the transaction code (Script windows) or window title (Win32 windows). The timestamp prevents collisions without any counter suffix.
Default output directory: $env:TEMP\SapDump (created if absent). Override with -OutputPath.
The HTML opens automatically in the default browser. Suppress with -NoOpen.
Filter by Type — handling nested controls
SAP's scripting tree is deeply nested. Container nodes (GuiUserArea, GuiTabStrip, GuiScrollContainer, GuiContainerShell, etc.) overlap all their descendants. If every control got an <area>, hovering over a button would also match the container behind it.
Only interactive/leaf types generate an <area> element:
| Type | Interaction |
|---|---|
| GuiButton | press |
| GuiTextField / GuiCTextField | read/write text |
| GuiPasswordField | write text |
| GuiComboBox | select key/value |
| GuiCheckBox | toggle Selected |
| GuiRadioButton | select |
| GuiTab | select |
| GuiGridView | read/write cells, select rows |
| GuiLabel / GuiStatusBar | read only |
| GuiTitlebar / GuiMenubar / GuiMenu / GuiToolbar | navigate |
Containers still appear in text/json/csv dumps but are excluded from the image map.
Coordinates
The VBScript dump emits ScreenLeft, ScreenTop, Width, Height (screen-absolute). HTML generation converts to window-relative pixels:
x1 = control.ScreenLeft - window.Left
y1 = control.ScreenTop - window.Top
x2 = x1 + control.Width
y2 = y1 + control.Height
New parameters
-Format html # enables this mode
-OutputPath <dir> # default: $env:TEMP\SapDump
-NoOpen # skip Start-Process after writing
Technical note: Win32 windows
For Win32 windows (timeout popups, SAP Logon), the screenshot is still captured but no <area> elements are generated — Win32 controls have no SAP scripting coordinates.
pwsh -File .\Probe-SapDpi.ps1
=== Screen DPI ===
LOGPIXELSX (screen DC) : 96 (96=100%, 120=125%, 144=150%, 192=200%)
Logical resolution : 2731 x 1440
Physical resolution : 4096 x 2160
Phys/log scale factor : 1.4998
=== SAP window: scripting coords vs GetWindowRect ===
Window : Faktura anzeigen
SAP ScreenLeft/Top : (2431, 691) SAP Width/Height : 1689 x 1302
GetWindowRect LOGICAL : L=1621 T=461 1126 x 868
GetWindowRect PHYSICAL : L=2431 T=691 1689 x 1302
SAP vs log offset : dL=810 dT=230
SAP vs phys offset : dL=0 dT=0
=== done ===
Line |
627 | Write-Error "Script dump failed.`n$_"
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Script dump failed. The variable '$n' cannot be retrieved because it has not been set.
.\Probe-SapGuiConnect.ps1
=== SAP GUI connection probe ===
PowerShell 7.5.2 on Microsoft Windows NT 10.0.26100.0
Process bitness: 64-bit
── 1. New-Object SapROTWr.SapROTWrapper
OK → System.__ComObject
── 2. wrapper.GetROTEntry("SAPGUI")
FAIL → Error loading type library/DLL. (0x80029C4A (TYPE_E_CANTLOADLIBRARY))
── 3. sapgui.GetScriptingEngine() — normal dispatch
FAIL → Error loading type library/DLL. (0x80029C4A (TYPE_E_CANTLOADLIBRARY))
── 4. sapgui.GetScriptingEngine() — InvokeMember (late-bound)
FAIL → Error loading type library/DLL. (0x80029C4A (TYPE_E_CANTLOADLIBRARY))
── 5. Marshal.GetActiveObject("SAPGUI")
FAIL → Method invocation failed because [System.Runtime.InteropServices.Marshal] does not contain a method named 'GetActiveObject'.
── 6. Microsoft.VisualBasic.Interaction.GetObject("SAPGUI")
FAIL → Error loading type library/DLL. (0x80029C4A (TYPE_E_CANTLOADLIBRARY))
── 7. Activator.CreateInstance from ProgID
FAIL → Error loading type library/DLL. (0x80029C4A (TYPE_E_CANTLOADLIBRARY))
── 8. cscript inline VBScript bridge
OK → lines=1 first=0
lines=1 first=0
── 9. Windows PS 5.1 32-bit
OK → FAIL: Error loading type library/DLL. (Exception from HRESULT: 0x80029C4A (TYPE_E_CANTLOADLIBRARY))
FAIL: Error loading type library/DLL. (Exception from HRESULT: 0x80029C4A (TYPE_E_CANTLOADLIBRARY))
── 10. Windows PS 5.1 64-bit
OK → FAIL: Error loading type library/DLL. (Exception from HRESULT: 0x80029C4A (TYPE_E_CANTLOADLIBRARY))
FAIL: Error loading type library/DLL. (Exception from HRESULT: 0x80029C4A (TYPE_E_CANTLOADLIBRARY))