Skip to content

Instantly share code, notes, and snippets.

@cprima
Last active April 1, 2026 15:49
Show Gist options
  • Select an option

  • Save cprima/c8a85d6b6d2708cd2edc62dd5f7dfbc2 to your computer and use it in GitHub Desktop.

Select an option

Save cprima/c8a85d6b6d2708cd2edc62dd5f7dfbc2 to your computer and use it in GitHub Desktop.
Interactive SAP GUI window browser that dumps the full control tree of a chosen window.
<#
.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 '&','&amp;' -replace '"','&quot;' -replace '<','&lt;' -replace '>','&gt;'
}
# ── 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 &middot; $pngFile &middot; origin ($winLeft,$winTop) &middot; ${imgW}&times;${imgH}px physical &middot; 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?'&#10003;':'&#10007;')+' '+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
@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

.\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))

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

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 |
  

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

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='&amp;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&#x9;&#x9;Wed Ap*' aaname='NAB: auto logout (maximum user idle time exceeded)  Time&#x9;&#x9;Wed Apr  1 11:15:56 2026 Component&#x9;DPTM Release&#x9;&#x9;753 Version&#x9;&#x9;10 Return Code&#x9;-22 Counter&#x9;&#x9;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' />

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

  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

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

The line

if (-not $w.Title) { continue }   # skip untitled framework/helper windows

filters 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".

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

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.

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

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.

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

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 csv

How 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 via ConvertTo-Json
  • csv — RFC 4180 with header row via ConvertTo-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.

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

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.

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

Improvement 3 — Interaction map + screen coordinates

Each control line in the [Script] dump now includes two additional pieces of information:

Screen boundsScreenLeft, 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.

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

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 screenshot
  • SapDump_{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.

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

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 ===

@cprima
Copy link
Copy Markdown
Author

cprima commented Apr 1, 2026

Line |
 627 |          Write-Error "Script dump failed.`n$_"
     |          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Script dump failed. The variable '$n' cannot be retrieved because it has not been set.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment