dsh-gui-control 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,267 @@
1
+ using System;
2
+ using System.Text;
3
+ using System.Drawing;
4
+ using System.Drawing.Imaging;
5
+ using System.Collections.Generic;
6
+ using System.Runtime.InteropServices;
7
+
8
+ // Native wrapper for mouse / keyboard / window / cursor.
9
+ // Compiled to .build\DshGui.dll and loaded by GuiControl.ps1.
10
+ // Keep this file ASCII-only: Windows PowerShell 5.1 may read sources as ANSI.
11
+ public static class DshGui
12
+ {
13
+ [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; }
14
+ [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
15
+ [StructLayout(LayoutKind.Sequential)] public struct MOUSEINPUT { public int dx, dy; public uint mouseData, dwFlags, time; public IntPtr dwExtraInfo; }
16
+ [StructLayout(LayoutKind.Sequential)] public struct KEYBDINPUT { public ushort wVk, wScan; public uint dwFlags, time; public IntPtr dwExtraInfo; }
17
+
18
+ [StructLayout(LayoutKind.Explicit, Size = 40)]
19
+ public struct INPUT
20
+ {
21
+ [FieldOffset(0)] public uint type;
22
+ [FieldOffset(8)] public MOUSEINPUT mi;
23
+ [FieldOffset(8)] public KEYBDINPUT ki;
24
+ }
25
+
26
+ public const uint INPUT_MOUSE = 0, INPUT_KEYBOARD = 1;
27
+ public const uint MOVE = 0x0001, LEFTDOWN = 0x0002, LEFTUP = 0x0004,
28
+ RIGHTDOWN = 0x0008, RIGHTUP = 0x0010, MIDDLEDOWN = 0x0020, MIDDLEUP = 0x0040,
29
+ WHEEL = 0x0800, HWHEEL = 0x1000, ABSOLUTE = 0x8000, VIRTUALDESK = 0x4000;
30
+ public const uint KEYUP = 0x0002, UNICODE = 0x0004;
31
+
32
+ [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
33
+ [DllImport("user32.dll")] public static extern int GetSystemMetrics(int n);
34
+ [DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT p);
35
+ [DllImport("user32.dll")] public static extern uint SendInput(uint n, INPUT[] inputs, int size);
36
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
37
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
38
+ [DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr h);
39
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetClassName(IntPtr h, StringBuilder s, int n);
40
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
41
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
42
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
43
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
44
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
45
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
46
+ [DllImport("user32.dll")] public static extern void SwitchToThisWindow(IntPtr h, bool alt);
47
+ [DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr h, int x, int y, int w, int hh, bool repaint);
48
+
49
+ public delegate bool EnumProc(IntPtr h, IntPtr p);
50
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr p);
51
+
52
+ public static int Init()
53
+ {
54
+ try { SetProcessDPIAware(); } catch { }
55
+ return Marshal.SizeOf(typeof(INPUT));
56
+ }
57
+
58
+ public static void MoveAbs(int x, int y)
59
+ {
60
+ int vx = GetSystemMetrics(76), vy = GetSystemMetrics(77), vw = GetSystemMetrics(78), vh = GetSystemMetrics(79);
61
+ if (vw <= 0 || vh <= 0) { vw = GetSystemMetrics(0); vh = GetSystemMetrics(1); vx = 0; vy = 0; }
62
+ long nx = (long)(x - vx) * 65535 / Math.Max(1, vw - 1);
63
+ long ny = (long)(y - vy) * 65535 / Math.Max(1, vh - 1);
64
+ INPUT[] a = new INPUT[1];
65
+ a[0].type = INPUT_MOUSE;
66
+ a[0].mi.dx = (int)Math.Round((double)nx);
67
+ a[0].mi.dy = (int)Math.Round((double)ny);
68
+ a[0].mi.dwFlags = MOVE | ABSOLUTE | VIRTUALDESK;
69
+ SendInput(1, a, 40);
70
+ }
71
+
72
+ public static void Mouse(uint flag, int data)
73
+ {
74
+ INPUT[] a = new INPUT[1];
75
+ a[0].type = INPUT_MOUSE;
76
+ a[0].mi.dwFlags = flag;
77
+ a[0].mi.mouseData = (uint)data;
78
+ SendInput(1, a, 40);
79
+ }
80
+
81
+ // Named ScrollWheel, not Wheel: a public const WHEEL already exists and PowerShell
82
+ // resolves members case-insensitively, so Wheel() would be shadowed by the field.
83
+ public static void ScrollWheel(int delta)
84
+ {
85
+ INPUT[] a = new INPUT[1];
86
+ a[0].type = INPUT_MOUSE;
87
+ a[0].mi.dwFlags = WHEEL;
88
+ a[0].mi.mouseData = (uint)delta;
89
+ SendInput(1, a, 40);
90
+ }
91
+
92
+ public static void Key(ushort vk, bool up)
93
+ {
94
+ INPUT[] a = new INPUT[1];
95
+ a[0].type = INPUT_KEYBOARD;
96
+ a[0].ki.wVk = vk;
97
+ a[0].ki.dwFlags = up ? KEYUP : 0;
98
+ SendInput(1, a, 40);
99
+ }
100
+
101
+ public static void Char(ushort ch)
102
+ {
103
+ INPUT[] a = new INPUT[2];
104
+ a[0].type = INPUT_KEYBOARD; a[0].ki.wScan = ch; a[0].ki.dwFlags = UNICODE;
105
+ a[1].type = INPUT_KEYBOARD; a[1].ki.wScan = ch; a[1].ki.dwFlags = UNICODE | KEYUP;
106
+ SendInput(2, a, 40);
107
+ }
108
+
109
+ public static List<IntPtr> TopWindows()
110
+ {
111
+ List<IntPtr> list = new List<IntPtr>();
112
+ EnumWindows(new EnumProc(delegate (IntPtr h, IntPtr p) { list.Add(h); return true; }), IntPtr.Zero);
113
+ return list;
114
+ }
115
+
116
+ public static string Title(IntPtr h)
117
+ {
118
+ int n = GetWindowTextLength(h);
119
+ StringBuilder sb = new StringBuilder(Math.Max(2, n + 2));
120
+ GetWindowText(h, sb, sb.Capacity);
121
+ return sb.ToString();
122
+ }
123
+
124
+ public static string ClassName(IntPtr h)
125
+ {
126
+ StringBuilder sb = new StringBuilder(256);
127
+ GetClassName(h, sb, sb.Capacity);
128
+ return sb.ToString();
129
+ }
130
+
131
+ public static POINT Cursor()
132
+ {
133
+ POINT p;
134
+ GetCursorPos(out p);
135
+ return p;
136
+ }
137
+
138
+ // Screen position of the window's client area origin (excludes caption/border).
139
+ [DllImport("user32.dll")] public static extern bool ClientToScreen(IntPtr h, ref POINT p);
140
+ public static POINT ClientOrigin(IntPtr h)
141
+ {
142
+ POINT p = new POINT();
143
+ p.X = 0; p.Y = 0;
144
+ ClientToScreen(h, ref p);
145
+ return p;
146
+ }
147
+
148
+ // One-pixel color sample, packed as 0xRRGGBB.
149
+ public static int Pixel(int x, int y)
150
+ {
151
+ using (Bitmap bmp = new Bitmap(1, 1, PixelFormat.Format32bppArgb))
152
+ {
153
+ using (Graphics g = Graphics.FromImage(bmp))
154
+ {
155
+ g.CopyFromScreen(x, y, 0, 0, new Size(1, 1), CopyPixelOperation.SourceCopy);
156
+ }
157
+ Color c = bmp.GetPixel(0, 0);
158
+ return (c.R << 16) | (c.G << 8) | c.B;
159
+ }
160
+ }
161
+
162
+ // Virtual desktop bounds in physical pixels (call Init() first for DPI awareness).
163
+ public static RECT VirtualScreen()
164
+ {
165
+ RECT r;
166
+ r.Left = GetSystemMetrics(76);
167
+ r.Top = GetSystemMetrics(77);
168
+ r.Right = r.Left + GetSystemMetrics(78);
169
+ r.Bottom = r.Top + GetSystemMetrics(79);
170
+ if (r.Right <= r.Left || r.Bottom <= r.Top)
171
+ {
172
+ r.Left = 0; r.Top = 0;
173
+ r.Right = GetSystemMetrics(0); r.Bottom = GetSystemMetrics(1);
174
+ }
175
+ return r;
176
+ }
177
+
178
+ public static void Shot(int left, int top, int w, int h, string path)
179
+ {
180
+ using (Bitmap bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb))
181
+ {
182
+ using (Graphics g = Graphics.FromImage(bmp))
183
+ {
184
+ g.CopyFromScreen(left, top, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy);
185
+ }
186
+ bmp.Save(path, ImageFormat.Png);
187
+ }
188
+ }
189
+
190
+ // Downscaled capture: vision tokens scale with pixel area, so 0.5x costs a quarter.
191
+ public static void ShotScaled(int left, int top, int w, int h, double scale, string path)
192
+ {
193
+ if (scale <= 0 || scale > 1) scale = 1;
194
+ int tw = Math.Max(1, (int)(w * scale));
195
+ int th = Math.Max(1, (int)(h * scale));
196
+ using (Bitmap full = new Bitmap(w, h, PixelFormat.Format32bppArgb))
197
+ {
198
+ using (Graphics g = Graphics.FromImage(full))
199
+ {
200
+ g.CopyFromScreen(left, top, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy);
201
+ }
202
+ using (Bitmap small = new Bitmap(tw, th, PixelFormat.Format32bppArgb))
203
+ {
204
+ using (Graphics g2 = Graphics.FromImage(small))
205
+ {
206
+ g2.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
207
+ g2.DrawImage(full, 0, 0, tw, th);
208
+ }
209
+ small.Save(path, ImageFormat.Png);
210
+ }
211
+ }
212
+ }
213
+
214
+ // Capture with screen-coordinate rulers. The model reads tick labels instead of
215
+ // multiplying preview pixels by a scale factor, which is where clicks go wrong.
216
+ public static void ShotRuled(int left, int top, int w, int h, double scale, int step, string path, string note)
217
+ {
218
+ if (scale <= 0 || scale > 1) scale = 1;
219
+ if (step < 50) step = 200;
220
+ int tw = Math.Max(1, (int)(w * scale));
221
+ int th = Math.Max(1, (int)(h * scale));
222
+ int padL = 66, padT = 24, padB = 22;
223
+
224
+ using (Bitmap full = new Bitmap(w, h, PixelFormat.Format32bppArgb))
225
+ {
226
+ using (Graphics g = Graphics.FromImage(full))
227
+ {
228
+ g.CopyFromScreen(left, top, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy);
229
+ }
230
+ using (Bitmap small = new Bitmap(tw, th, PixelFormat.Format32bppArgb))
231
+ {
232
+ using (Graphics g2 = Graphics.FromImage(small))
233
+ {
234
+ g2.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
235
+ g2.DrawImage(full, 0, 0, tw, th);
236
+ }
237
+ using (Bitmap canvas = new Bitmap(tw + padL, th + padT + padB, PixelFormat.Format32bppArgb))
238
+ {
239
+ using (Graphics c = Graphics.FromImage(canvas))
240
+ {
241
+ c.Clear(Color.FromArgb(255, 18, 18, 18));
242
+ c.DrawImage(small, padL, padT);
243
+ using (Font f = new Font("Consolas", 9f))
244
+ using (Brush ink = new SolidBrush(Color.FromArgb(255, 235, 235, 235)))
245
+ using (Pen tick = new Pen(Color.FromArgb(255, 255, 170, 60)))
246
+ {
247
+ for (int x = 0; x < w; x += step)
248
+ {
249
+ int px = padL + (int)(x * scale);
250
+ c.DrawLine(tick, px, padT - 5, px, padT);
251
+ c.DrawString((left + x).ToString(), f, ink, px + 1, 2);
252
+ }
253
+ for (int y = 0; y < h; y += step)
254
+ {
255
+ int py = padT + (int)(y * scale);
256
+ c.DrawLine(tick, padL - 5, py, padL, py);
257
+ c.DrawString((top + y).ToString(), f, ink, 2, py - 7);
258
+ }
259
+ if (!string.IsNullOrEmpty(note)) c.DrawString(note, f, ink, padL + 2, padT + th + 1);
260
+ }
261
+ }
262
+ canvas.Save(path, ImageFormat.Png);
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,212 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Windows.Automation;
4
+
5
+ // UI Automation access: find controls by name, invoke them without a single mouse event.
6
+ // Works for Win32 / WinForms / WPF / UWP apps. CEF apps (bilibili client, Electron) usually
7
+ // expose nothing here, which is exactly why gui-ocr.ps1 exists as the fallback.
8
+ // ASCII only.
9
+ public static class DshUia
10
+ {
11
+ public static AutomationElement FindWindow(string titleContains)
12
+ {
13
+ var root = AutomationElement.RootElement;
14
+ if (root == null) return null;
15
+ var exact = new PropertyCondition(AutomationElement.NameProperty, titleContains);
16
+ var w = root.FindFirst(TreeScope.Children, exact);
17
+ if (w != null) return w;
18
+
19
+ var kids = root.FindAll(TreeScope.Children, Condition.TrueCondition);
20
+ foreach (AutomationElement e in kids)
21
+ {
22
+ try
23
+ {
24
+ string n = e.Current.Name;
25
+ if (!string.IsNullOrEmpty(n) && n.IndexOf(titleContains, StringComparison.OrdinalIgnoreCase) >= 0) return e;
26
+ }
27
+ catch { }
28
+ }
29
+ return null;
30
+ }
31
+
32
+ // Substring match on the accessible Name, optionally limited to one control type
33
+ // (e.g. "Button"), which also keeps the search fast on large trees.
34
+ public static List<AutomationElement> Find(AutomationElement scope, string contains, string controlType, int max)
35
+ {
36
+ var res = new List<AutomationElement>();
37
+ if (scope == null) return res;
38
+ Condition cond = Condition.TrueCondition;
39
+ if (!string.IsNullOrEmpty(controlType))
40
+ {
41
+ ControlType ct = null;
42
+ switch (controlType.ToLower())
43
+ {
44
+ case "button": ct = ControlType.Button; break;
45
+ case "edit": ct = ControlType.Edit; break;
46
+ case "text": ct = ControlType.Text; break;
47
+ case "listitem": ct = ControlType.ListItem; break;
48
+ case "tabitem": ct = ControlType.TabItem; break;
49
+ case "menuitem": ct = ControlType.MenuItem; break;
50
+ case "checkbox": ct = ControlType.CheckBox; break;
51
+ case "radiobutton": ct = ControlType.RadioButton; break;
52
+ case "combobox": ct = ControlType.ComboBox; break;
53
+ case "hyperlink": ct = ControlType.Hyperlink; break;
54
+ case "treeitem": ct = ControlType.TreeItem; break;
55
+ case "dataitem": ct = ControlType.DataItem; break;
56
+ case "image": ct = ControlType.Image; break;
57
+ case "group": ct = ControlType.Group; break;
58
+ }
59
+ if (ct != null) cond = new PropertyCondition(AutomationElement.ControlTypeProperty, ct);
60
+ }
61
+ AutomationElementCollection all;
62
+ try { all = scope.FindAll(TreeScope.Descendants, cond); }
63
+ catch { return res; }
64
+ foreach (AutomationElement e in all)
65
+ {
66
+ if (res.Count >= max) break;
67
+ try
68
+ {
69
+ string n = e.Current.Name;
70
+ if (!string.IsNullOrEmpty(n) && n.IndexOf(contains, StringComparison.OrdinalIgnoreCase) >= 0) res.Add(e);
71
+ }
72
+ catch { }
73
+ }
74
+ return res;
75
+ }
76
+
77
+ public static string Name(AutomationElement e) { try { return e.Current.Name; } catch { return ""; } }
78
+ public static string AutoId(AutomationElement e) { try { return e.Current.AutomationId; } catch { return ""; } }
79
+ public static bool IsPassword(AutomationElement e) { try { return e.Current.IsPassword; } catch { return false; } }
80
+
81
+ public static string Value(AutomationElement e)
82
+ {
83
+ try
84
+ {
85
+ object p;
86
+ if (e.TryGetCurrentPattern(ValuePattern.Pattern, out p))
87
+ return ((ValuePattern)p).Current.Value;
88
+ return "";
89
+ }
90
+ catch { return ""; }
91
+ }
92
+
93
+ public static string ClassOf(AutomationElement e) { try { return e.Current.ClassName; } catch { return ""; } }
94
+ public static string TypeOf(AutomationElement e) { try { return e.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""); } catch { return ""; } }
95
+
96
+ public static double[] Rect(AutomationElement e)
97
+ {
98
+ // Off-screen / collapsed elements report +/-Infinity here, which cannot be cast to
99
+ // int. Clamp to -1 so callers can treat them as "no usable rectangle".
100
+ try
101
+ {
102
+ var r = e.Current.BoundingRectangle;
103
+ double x = r.X, y = r.Y, w = r.Width, h = r.Height;
104
+ if (double.IsNaN(x) || double.IsInfinity(x)) { x = -1; w = 0; }
105
+ if (double.IsNaN(y) || double.IsInfinity(y)) { y = -1; h = 0; }
106
+ if (double.IsNaN(w) || double.IsInfinity(w)) { w = 0; }
107
+ if (double.IsNaN(h) || double.IsInfinity(h)) { h = 0; }
108
+ if (x < -10000 || y < -10000) { return new double[] { -1, -1, 0, 0 }; }
109
+ return new double[] { x, y, w, h };
110
+ }
111
+ catch { return new double[] { -1, -1, 0, 0 }; }
112
+ }
113
+
114
+ // Prefer the accessibility action over simulating a click: works even when the window
115
+ // is behind another window, and cannot miss by a few pixels.
116
+ public static string Activate(AutomationElement e)
117
+ {
118
+ object p;
119
+ try
120
+ {
121
+ if (e.TryGetCurrentPattern(InvokePattern.Pattern, out p))
122
+ {
123
+ ((InvokePattern)p).Invoke();
124
+ return "invoke";
125
+ }
126
+ if (e.TryGetCurrentPattern(SelectionItemPattern.Pattern, out p))
127
+ {
128
+ ((SelectionItemPattern)p).Select();
129
+ return "select";
130
+ }
131
+ if (e.TryGetCurrentPattern(TogglePattern.Pattern, out p))
132
+ {
133
+ ((TogglePattern)p).Toggle();
134
+ return "toggle";
135
+ }
136
+ }
137
+ catch (Exception ex) { return "error:" + ex.Message; }
138
+ return "none";
139
+ }
140
+
141
+ public static string SetText(AutomationElement e, string value)
142
+ {
143
+ object p;
144
+ try
145
+ {
146
+ if (e.TryGetCurrentPattern(ValuePattern.Pattern, out p))
147
+ {
148
+ ((ValuePattern)p).SetValue(value);
149
+ return "value";
150
+ }
151
+ }
152
+ catch (Exception ex) { return "error:" + ex.Message; }
153
+ return "none";
154
+ }
155
+
156
+ public static string GetText(AutomationElement e)
157
+ {
158
+ object p;
159
+ try
160
+ {
161
+ if (e.TryGetCurrentPattern(ValuePattern.Pattern, out p)) return ((ValuePattern)p).Current.Value;
162
+ if (e.TryGetCurrentPattern(TextPattern.Pattern, out p)) return ((TextPattern)p).DocumentRange.GetText(4096);
163
+ }
164
+ catch { }
165
+ try { return e.Current.Name; } catch { return ""; }
166
+ }
167
+
168
+ // Selected state for radio / checkbox / tab items. Needed to verify a pick landed.
169
+ public static bool IsSelected(AutomationElement e)
170
+ {
171
+ object p;
172
+ try
173
+ {
174
+ if (e.TryGetCurrentPattern(SelectionItemPattern.Pattern, out p))
175
+ return ((SelectionItemPattern)p).Current.IsSelected;
176
+ if (e.TryGetCurrentPattern(TogglePattern.Pattern, out p))
177
+ return ((TogglePattern)p).Current.ToggleState == ToggleState.On;
178
+ }
179
+ catch { }
180
+ return false;
181
+ }
182
+
183
+ // The web page root inside a Chromium window. Everything a web task needs hangs here.
184
+ public static AutomationElement DocumentOf(AutomationElement win)
185
+ {
186
+ if (win == null) return null;
187
+ var cond = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Document);
188
+ try
189
+ {
190
+ var d = win.FindFirst(TreeScope.Descendants, cond);
191
+ if (d != null) return d;
192
+ }
193
+ catch { }
194
+ return win;
195
+ }
196
+
197
+ // Flat dump of a subtree in tree order. Chromium pages are big, so callers cap it.
198
+ public static List<AutomationElement> Descendants(AutomationElement scope, int max)
199
+ {
200
+ var res = new List<AutomationElement>();
201
+ if (scope == null) return res;
202
+ AutomationElementCollection all;
203
+ try { all = scope.FindAll(TreeScope.Descendants, Condition.TrueCondition); }
204
+ catch { return res; }
205
+ foreach (AutomationElement e in all)
206
+ {
207
+ if (res.Count >= max) break;
208
+ res.Add(e);
209
+ }
210
+ return res;
211
+ }
212
+ }
@@ -0,0 +1,146 @@
1
+ #requires -Version 5.1
2
+ # Anchor resolution layer: named targets instead of hardcoded pixels.
3
+ #
4
+ # An anchor is resolved with a fixed priority chain, first hit wins:
5
+ # uia -> accessibility element (exact, position independent)
6
+ # ocr -> exact label found by OCR (survives resize/move)
7
+ # ocr_any -> any text inside a band (survives placeholder -> value swap)
8
+ # derive -> bounding box of OCR lines matching a regex (survives layout changes)
9
+ # point -> fraction of the window (last resort, no text available)
10
+ #
11
+ # Profiles live in apps\<name>.json. All coordinates in a profile are fractions of the
12
+ # target window (0..1), so moving/resizing the window cannot invalidate them.
13
+ # ASCII only.
14
+ $script:GuiRoot = $PSScriptRoot
15
+
16
+ function Get-AppProfile([string]$app) {
17
+ $p = Join-Path (Join-Path $script:GuiRoot 'apps') ($app + '.json')
18
+ if (-not (Test-Path $p)) { throw ('profile not found: ' + $app) }
19
+ return (Get-Content -Raw -Encoding UTF8 $p | ConvertFrom-Json)
20
+ }
21
+
22
+ function Get-FracRect($win, $f) {
23
+ # f = [rx, ry, rw, rh] as fractions of the window frame
24
+ return @{
25
+ x = [int]($win.Left + $win.Width * [double]$f[0])
26
+ y = [int]($win.Top + $win.Height * [double]$f[1])
27
+ w = [int]($win.Width * [double]$f[2])
28
+ h = [int]($win.Height * [double]$f[3])
29
+ }
30
+ }
31
+
32
+ function Get-AnchorScope($anchor, $win) {
33
+ if ($null -eq $anchor.scope) { return $null }
34
+ return Get-FracRect $win $anchor.scope
35
+ }
36
+
37
+ # OCR the window (or a sub-rect) through the normal op pipeline.
38
+ function Get-Lines([hashtable]$rect) {
39
+ $op = @{ op = 'ocr' }
40
+ if ($null -ne $rect) { $op.x = $rect.x; $op.y = $rect.y; $op.w = $rect.w; $op.h = $rect.h }
41
+ $r = Invoke-GuiOp $op
42
+ if (-not $r.ok) { return @() }
43
+ return @($r.result)
44
+ }
45
+
46
+ function Resolve-Anchor([string]$app, [string]$name, [string]$winMatch = '') {
47
+ $prof = Get-AppProfile $app
48
+ $anchor = $prof.anchors.$name
49
+ if ($null -eq $anchor) { throw ('anchor not found: ' + $app + '.' + $name) }
50
+
51
+ if ([string]::IsNullOrEmpty($winMatch)) { $winMatch = $prof.window_match }
52
+ $win = Resolve-Win $winMatch
53
+ $frame = @{ Left = [double]$win.Left; Top = [double]$win.Top
54
+ Width = [double]($win.Right - $win.Left); Height = [double]($win.Bottom - $win.Top) }
55
+
56
+ # 1) uia
57
+ if ($null -ne $anchor.uia) {
58
+ try {
59
+ $scope = [DshUia]::FindWindow($winMatch)
60
+ $els = [DshUia]::Find($scope, $anchor.uia.name, $anchor.uia.type, 5)
61
+ if ($els.Count -ge 1) {
62
+ $rr = [DshUia]::Rect($els[0])
63
+ return @{ how = 'uia'; x = [int]$rr[0]; y = [int]$rr[1]; w = [int]$rr[2]; h = [int]$rr[3]
64
+ cx = [int]($rr[0] + $rr[2] / 2); cy = [int]($rr[1] + $rr[3] / 2) }
65
+ }
66
+ }
67
+ catch { }
68
+ }
69
+
70
+ # 2) ocr (exact label)
71
+ if ($anchor.ocr) {
72
+ $scope = Get-AnchorScope $anchor $frame
73
+ try {
74
+ $op = @{ op = 'findtext'; text = $anchor.ocr }
75
+ if ($null -ne $scope) { $op += $scope } else { $op.win = $winMatch }
76
+ $r = Invoke-GuiOp $op
77
+ if ($r.ok -and @($r.result).Count -ge 1) {
78
+ $h = @($r.result)[0]
79
+ return @{ how = 'ocr'; x = $h.x; y = $h.y; w = $h.w; h = $h.h; cx = $h.cx; cy = $h.cy; text = $h.text }
80
+ }
81
+ }
82
+ catch { }
83
+ }
84
+
85
+ # 3) ocr_any (any text inside the band - the search box keeps either placeholder or value)
86
+ if ($anchor.ocr_any) {
87
+ $scope = Get-AnchorScope $anchor $frame
88
+ if ($null -ne $scope) {
89
+ $lines = @(Get-Lines $scope | Where-Object { ((($_.text -replace '\s+', ''))).Length -ge 2 })
90
+ if ($lines.Count -ge 1) {
91
+ $l = $lines[0]
92
+ return @{ how = 'ocr_any'; x = $l.x; y = $l.y; w = $l.w; h = $l.h; cx = $l.cx; cy = $l.cy; text = $l.text }
93
+ }
94
+ }
95
+ }
96
+
97
+ # 4) derive (bounding box of matching rows)
98
+ if ($anchor.derive) {
99
+ $scan = Get-FracRect $frame $anchor.scan
100
+ $lines = @(Get-Lines $scan | Where-Object { ($_.text -replace '\s+', '') -match $anchor.derive })
101
+ if ($lines.Count -ge 1) {
102
+ $pad = 0; if ($null -ne $anchor.pad) { $pad = [int]$anchor.pad }
103
+ $minX = ($lines | Measure-Object -Property x -Minimum).Minimum
104
+ $minY = ($lines | Measure-Object -Property y -Minimum).Minimum
105
+ $maxX = ($lines | ForEach-Object { $_.x + $_.w } | Measure-Object -Maximum).Maximum
106
+ $maxY = ($lines | ForEach-Object { $_.y + $_.h } | Measure-Object -Maximum).Maximum
107
+ $x = [int]($minX - $pad); $y = [int]($minY - $pad)
108
+ $w = [int]($maxX - $minX + 2 * $pad); $h = [int]($maxY - $minY + 2 * $pad)
109
+ return @{ how = 'derive'; x = $x; y = $y; w = $w; h = $h
110
+ cx = [int]($x + $w / 2); cy = [int]($y + $h / 2); rows = $lines.Count }
111
+ }
112
+ }
113
+
114
+ # 5) rect / point fallback
115
+ if ($anchor.rect) {
116
+ $r = Get-FracRect $frame $anchor.rect
117
+ return @{ how = 'rect'; x = $r.x; y = $r.y; w = $r.w; h = $r.h
118
+ cx = [int]($r.x + $r.w / 2); cy = [int]($r.y + $r.h / 2) }
119
+ }
120
+ if ($anchor.point) {
121
+ $pt = Get-FracRect $frame @($anchor.point[0], $anchor.point[1], 0, 0)
122
+ return @{ how = 'point'; x = $pt.x; y = $pt.y; w = 0; h = 0; cx = $pt.x; cy = $pt.y }
123
+ }
124
+
125
+ throw ('anchor unresolvable: ' + $app + '.' + $name)
126
+ }
127
+
128
+ # Discovery helper: dump every OCR line of a window with fractional coordinates, so a new
129
+ # app profile can be authored by copy-paste instead of trial and error.
130
+ function Get-AnchorDump([string]$app) {
131
+ $prof = Get-AppProfile $app
132
+ $win = Resolve-Win $prof.window_match
133
+ $frame = @{ Left = [double]$win.Left; Top = [double]$win.Top
134
+ Width = [double]($win.Right - $win.Left); Height = [double]($win.Bottom - $win.Top) }
135
+ $lines = @(Get-Lines @{ x = [int]$frame.Left; y = [int]$frame.Top; w = [int]$frame.Width; h = [int]$frame.Height })
136
+ $out = @()
137
+ foreach ($l in $lines) {
138
+ $out += @{
139
+ text = ($l.text -replace '\s+', '')
140
+ rx = [math]::Round(($l.cx - $frame.Left) / $frame.Width, 3)
141
+ ry = [math]::Round(($l.cy - $frame.Top) / $frame.Height, 3)
142
+ abs = ('{0},{1}' -f $l.cx, $l.cy)
143
+ }
144
+ }
145
+ return @{ window = $win.Title; size = ('{0}x{1}' -f $frame.Width, $frame.Height); lines = $out }
146
+ }