dsh-gui-control 0.0.0-stage → 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +209 -2
- package/assets/icon-180.png +0 -0
- package/assets/screenshot-bilibili.png +0 -0
- package/assets/screenshot-calc.png +0 -0
- package/assets/screenshot-sheet.png +0 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +8 -0
- package/lib/index.js +346 -0
- package/lib/scripts/DshGui.cs +267 -0
- package/lib/scripts/DshUia.cs +212 -0
- package/lib/scripts/gui-anchors.ps1 +146 -0
- package/lib/scripts/gui-daemon.log +4 -0
- package/lib/scripts/gui-daemon.ps1 +89 -0
- package/lib/scripts/gui-lib.ps1 +68 -0
- package/lib/scripts/gui-ocr.ps1 +71 -0
- package/lib/scripts/gui-ops.ps1 +915 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -3
- package/screenshots.json +5 -0
|
@@ -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
|
+
}
|