ascript 1.3.27__py3-none-win_amd64.whl

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.
Files changed (177) hide show
  1. ascript/__init__.py +8 -0
  2. ascript/hooks/__init__.py +0 -0
  3. ascript/hooks/hook-ascript.py +2 -0
  4. ascript/windows/__init__.py +29 -0
  5. ascript/windows/daos/__init__.py +0 -0
  6. ascript/windows/daos/screen.py +143 -0
  7. ascript/windows/hardware/__init__.py +5 -0
  8. ascript/windows/hardware/keyboard.py +279 -0
  9. ascript/windows/hardware/logitech_driver.py +179 -0
  10. ascript/windows/hardware/mouse.py +308 -0
  11. ascript/windows/license/__init__.py +16 -0
  12. ascript/windows/license/achttp.cp310-win_amd64.pyd +0 -0
  13. ascript/windows/license/achttp.cp311-win_amd64.pyd +0 -0
  14. ascript/windows/license/achttp.cp312-win_amd64.pyd +0 -0
  15. ascript/windows/license/achttp.cp313-win_amd64.pyd +0 -0
  16. ascript/windows/license/achttp.cp314-win_amd64.pyd +0 -0
  17. ascript/windows/license/achttp.cp38-win_amd64.pyd +0 -0
  18. ascript/windows/license/achttp.cp39-win_amd64.pyd +0 -0
  19. ascript/windows/media/__init__.py +0 -0
  20. ascript/windows/resources/__init__.py +0 -0
  21. ascript/windows/resources/libs/logitech/x64/LogitechGkeyEnginesWrapper.dll +0 -0
  22. ascript/windows/resources/libs/logitech/x86/LogitechGkeyEnginesWrapper.dll +0 -0
  23. ascript/windows/resources/templates/__init__.py +0 -0
  24. ascript/windows/resources/templates/__init__py.win.template +51 -0
  25. ascript/windows/resources/templates/build.as.win.template +14 -0
  26. ascript/windows/resources/templates/build.py +71 -0
  27. ascript/windows/resources/templates/logo_windows.ico +0 -0
  28. ascript/windows/resources/templates/windows/main.py +50 -0
  29. ascript/windows/resources/templates/windows/pyproject.toml +44 -0
  30. ascript/windows/resources/templates/windows/res/img/logo.ico +0 -0
  31. ascript/windows/screen/__init__.py +13 -0
  32. ascript/windows/screen/as_cv.py +382 -0
  33. ascript/windows/screen/canny.py +128 -0
  34. ascript/windows/screen/code_scanner.py +72 -0
  35. ascript/windows/screen/color_algorithms.py +277 -0
  36. ascript/windows/screen/find_colors.py +251 -0
  37. ascript/windows/screen/find_images.py +217 -0
  38. ascript/windows/screen/ocr/__init__.py +4 -0
  39. ascript/windows/screen/ocr/ocr.py +223 -0
  40. ascript/windows/screen/ocr/rapidocr_hook.py +301 -0
  41. ascript/windows/screen/ocrx.py +169 -0
  42. ascript/windows/screen/screen_core.py +139 -0
  43. ascript/windows/store/__init__.py +21 -0
  44. ascript/windows/system/__init__.py +8 -0
  45. ascript/windows/system/device.py +355 -0
  46. ascript/windows/system/easy_print.py +115 -0
  47. ascript/windows/system/env.py +492 -0
  48. ascript/windows/system/exception/__init__.py +1 -0
  49. ascript/windows/system/key_value.py +145 -0
  50. ascript/windows/system/pip.py +175 -0
  51. ascript/windows/system/r.py +165 -0
  52. ascript/windows/system/runtime.py +62 -0
  53. ascript/windows/tools/__init__.py +27 -0
  54. ascript/windows/tools/__main__.py +21 -0
  55. ascript/windows/tools/action.json +33 -0
  56. ascript/windows/tools/action.py +168 -0
  57. ascript/windows/tools/colors_tool.py +391 -0
  58. ascript/windows/tools/config.py +26 -0
  59. ascript/windows/tools/dumptool.py +18 -0
  60. ascript/windows/tools/eel_api.py +495 -0
  61. ascript/windows/tools/gpplug/__init__.py +0 -0
  62. ascript/windows/tools/gpplug/plug.py +70 -0
  63. ascript/windows/tools/mouse_catch.py +168 -0
  64. ascript/windows/tools/plug_manager.py +38 -0
  65. ascript/windows/tools/tool_api.py +906 -0
  66. ascript/windows/tools/tool_window.py +56 -0
  67. ascript/windows/tools/uia_worker.py +365 -0
  68. ascript/windows/tools/web/app_store_window.html +481 -0
  69. ascript/windows/tools/web/colors.html +532 -0
  70. ascript/windows/tools/web/css/screen.css +1342 -0
  71. ascript/windows/tools/web/css/viewtree.css +357 -0
  72. ascript/windows/tools/web/flot_control.html +195 -0
  73. ascript/windows/tools/web/img/ico/favicon.png +0 -0
  74. ascript/windows/tools/web/img/ico/paintpalette.png +0 -0
  75. ascript/windows/tools/web/img/svg/find_colors.svg +1 -0
  76. ascript/windows/tools/web/img/svg/find_images.svg +1 -0
  77. ascript/windows/tools/web/img/svg/illustration-6.svg +89 -0
  78. ascript/windows/tools/web/img/svg/img_default.svg +1 -0
  79. ascript/windows/tools/web/img/svg/ocr.svg +1 -0
  80. ascript/windows/tools/web/js/screen/colors.js +466 -0
  81. ascript/windows/tools/web/js/screen/gp_plug.js +108 -0
  82. ascript/windows/tools/web/js/screen/image_mouse.js +464 -0
  83. ascript/windows/tools/web/js/screen/img_view.js +558 -0
  84. ascript/windows/tools/web/js/screen/plug.js +246 -0
  85. ascript/windows/tools/web/js/screen/screen.js +690 -0
  86. ascript/windows/tools/web/js/screen/screen_api.js +778 -0
  87. ascript/windows/tools/web/js/screen/screen_maker.js +10 -0
  88. ascript/windows/tools/web/js/utils/map.js +73 -0
  89. ascript/windows/tools/web/js/utils/upload.js +157 -0
  90. ascript/windows/tools/web/js/viewtree/niv_js.js +27 -0
  91. ascript/windows/tools/web/js/viewtree/vtree.js +1510 -0
  92. ascript/windows/tools/web/libs/bootstrap/css/bootstrap.min.css +6 -0
  93. ascript/windows/tools/web/libs/bootstrap/js/bootstrap.bundle.min.js +7 -0
  94. ascript/windows/tools/web/libs/bootstrap-icons/0-circle.svg +4 -0
  95. ascript/windows/tools/web/libs/bootstrap-icons/0-square.svg +4 -0
  96. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.css +2078 -0
  97. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.json +2052 -0
  98. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.min.css +5 -0
  99. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.scss +2090 -0
  100. ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff +0 -0
  101. ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff2 +0 -0
  102. ascript/windows/tools/web/libs/feather-icons/dist/feather.min.js +13 -0
  103. ascript/windows/tools/web/libs/feather-icons/dist/icons.json +1 -0
  104. ascript/windows/tools/web/libs/jquery/core.js +399 -0
  105. ascript/windows/tools/web/libs/jquery/jquery.js +10598 -0
  106. ascript/windows/tools/web/libs/jquery/jquery.min.js +4 -0
  107. ascript/windows/tools/web/libs/jquery/jquery.min.js.zip +0 -0
  108. ascript/windows/tools/web/libs/jquery/jquery.slim.js +8495 -0
  109. ascript/windows/tools/web/libs/jquery/jquery.slim.min.js +2 -0
  110. ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.js +2134 -0
  111. ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.min.css +16 -0
  112. ascript/windows/tools/web/libs/jquery-zTree/css/zTreeStyle.css +97 -0
  113. ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_close.png +0 -0
  114. ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_open.png +0 -0
  115. ascript/windows/tools/web/libs/jquery-zTree/img/diy/2.png +0 -0
  116. ascript/windows/tools/web/libs/jquery-zTree/img/diy/3.png +0 -0
  117. ascript/windows/tools/web/libs/jquery-zTree/img/diy/4.png +0 -0
  118. ascript/windows/tools/web/libs/jquery-zTree/img/diy/5.png +0 -0
  119. ascript/windows/tools/web/libs/jquery-zTree/img/diy/6.png +0 -0
  120. ascript/windows/tools/web/libs/jquery-zTree/img/diy/7.png +0 -0
  121. ascript/windows/tools/web/libs/jquery-zTree/img/diy/8.png +0 -0
  122. ascript/windows/tools/web/libs/jquery-zTree/img/diy/9.png +0 -0
  123. ascript/windows/tools/web/libs/jquery-zTree/img/line_conn.gif +0 -0
  124. ascript/windows/tools/web/libs/jquery-zTree/img/loading.gif +0 -0
  125. ascript/windows/tools/web/libs/jquery-zTree/img/tree-node-btn.png +0 -0
  126. ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.gif +0 -0
  127. ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.png +0 -0
  128. ascript/windows/tools/web/libs/jquery-zTree/js/jquery.ztree.all.min.js +3 -0
  129. ascript/windows/tools/web/libs/treeview/css/jquery.treeview.css +79 -0
  130. ascript/windows/tools/web/libs/treeview/img/file.png +0 -0
  131. ascript/windows/tools/web/libs/treeview/img/ico_loading.png +0 -0
  132. ascript/windows/tools/web/libs/treeview/img/icon4.png +0 -0
  133. ascript/windows/tools/web/libs/treeview/img/icon5.png +0 -0
  134. ascript/windows/tools/web/libs/treeview/img/icon6.png +0 -0
  135. ascript/windows/tools/web/libs/treeview/img/py.png +0 -0
  136. ascript/windows/tools/web/libs/treeview/img/treeview-black-line.gif +0 -0
  137. ascript/windows/tools/web/libs/treeview/img/treeview-black.gif +0 -0
  138. ascript/windows/tools/web/libs/treeview/img/treeview-default-line.gif +0 -0
  139. ascript/windows/tools/web/libs/treeview/img/treeview-default.gif +0 -0
  140. ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam-line.gif +0 -0
  141. ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam.gif +0 -0
  142. ascript/windows/tools/web/libs/treeview/img/treeview-gray-line.gif +0 -0
  143. ascript/windows/tools/web/libs/treeview/img/treeview-gray.gif +0 -0
  144. ascript/windows/tools/web/libs/treeview/img/treeview-red-line.gif +0 -0
  145. ascript/windows/tools/web/libs/treeview/img/treeview-red.gif +0 -0
  146. ascript/windows/tools/web/libs/treeview/js/jquery.treeview.edit.js +37 -0
  147. ascript/windows/tools/web/libs/treeview/js/jquery.treeview.js +250 -0
  148. ascript/windows/tools/web/license_window.html +255 -0
  149. ascript/windows/tools/web/plugs/a.html +653 -0
  150. ascript/windows/tools/web/plugs/demo.html +52 -0
  151. ascript/windows/tools/web/plugs/find_colors.html +629 -0
  152. ascript/windows/tools/web/plugs/find_images.html +699 -0
  153. ascript/windows/tools/web/plugs/rapidocr.html +954 -0
  154. ascript/windows/tools/web/vtree.html +496 -0
  155. ascript/windows/tools/webview/assets/index-HymBPl-I.css +1 -0
  156. ascript/windows/tools/webview/assets/index-PqA_m8tL.js +17 -0
  157. ascript/windows/tools/webview/index.html +13 -0
  158. ascript/windows/ui/__init__.py +6 -0
  159. ascript/windows/ui/app_store_window.py +272 -0
  160. ascript/windows/ui/draw_window.py +231 -0
  161. ascript/windows/ui/flot_test.py +297 -0
  162. ascript/windows/ui/flot_window.py +488 -0
  163. ascript/windows/ui/license_window.py +111 -0
  164. ascript/windows/ui/web_window.py +466 -0
  165. ascript/windows/utils/__init__.py +0 -0
  166. ascript/windows/utils/node_utils.py +20 -0
  167. ascript/windows/utils/screen_utils.py +280 -0
  168. ascript/windows/window/Selector.py +1015 -0
  169. ascript/windows/window/Window.py +747 -0
  170. ascript/windows/window/__init__.py +5 -0
  171. ascript/windows/window/launcher.py +213 -0
  172. ascript/windows/window/uia_runtime.py +26 -0
  173. ascript-1.3.27.dist-info/METADATA +41 -0
  174. ascript-1.3.27.dist-info/RECORD +177 -0
  175. ascript-1.3.27.dist-info/WHEEL +5 -0
  176. ascript-1.3.27.dist-info/entry_points.txt +2 -0
  177. ascript-1.3.27.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1015 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any, Dict, List, Optional, Tuple, Union
6
+
7
+ import uiautomation as auto
8
+ from PIL import Image
9
+
10
+ from .Window import Window
11
+ from .uia_runtime import prepare_comtypes_cache
12
+
13
+ prepare_comtypes_cache()
14
+
15
+ class UIElement:
16
+ """UI 元素封装类。
17
+
18
+ 将 uiautomation 的 Control 对象封装为更易用的 UI 元素,
19
+ 提供属性访问、坐标转换、点击、输入、截图等操作。
20
+ """
21
+
22
+ def __init__(self, node: auto.Control, window: Any, depth: int):
23
+ """初始化 UI 元素。
24
+
25
+ Args:
26
+ node: uiautomation 的 Control 控件对象
27
+ window: 所属的 Window 窗口对象
28
+ depth: 该元素在 UI 树中的深度层级
29
+ """
30
+ self.node = node
31
+ self.window = window
32
+ self.depth = depth
33
+ self.children: List['UIElement'] = []
34
+
35
+ # --- 1. 基础标识属性 (原生映射,不拼凑) ---
36
+ self.name: str = self._read(node, "Name", "")
37
+ self.type: str = self._read(node, "ControlTypeName", "")
38
+ self.class_name: str = self._read(node, "ClassName", "")
39
+ self.handle: int = self._read(node, "NativeWindowHandle", 0) or 0
40
+
41
+ # 资源 ID: 仅存储原生的 AutomationId
42
+ self.res_id: str = self._read(node, "AutomationId", "")
43
+
44
+ # 内存 ID: 仅存储对象在内存中的物理唯一地址 (Python Object ID)
45
+ self.memory_id: int = id(self.node)
46
+
47
+ # --- 2. 交互状态属性 ---
48
+ self.is_enabled: bool = bool(self._read(node, "IsEnabled", False))
49
+ self.is_visible: bool = not bool(self._read(node, "IsOffscreen", True))
50
+ self.is_clickable: bool = self._check_clickable(node)
51
+ self.is_password: bool = bool(self._read(node, "IsPassword", False))
52
+ self.is_focusable: bool = bool(self._read(node, "IsKeyboardFocusable", False))
53
+ self.is_focused: bool = bool(self._read(node, "HasKeyboardFocus", False))
54
+
55
+ toggle_pattern = self._get_pattern(node, "GetTogglePattern")
56
+ selection_pattern = self._get_pattern(node, "GetSelectionItemPattern")
57
+ value_pattern = self._get_pattern(node, "GetValuePattern")
58
+ self.is_checkable: bool = toggle_pattern is not None
59
+ self.is_checked: bool = bool(
60
+ self._read(toggle_pattern, "ToggleState", 0)
61
+ ) if toggle_pattern is not None else False
62
+ self.is_selected: bool = bool(
63
+ self._read(selection_pattern, "IsSelected", False)
64
+ ) if selection_pattern is not None else False
65
+ self.is_editable: bool = bool(
66
+ value_pattern is not None
67
+ and not bool(self._read(value_pattern, "IsReadOnly", True))
68
+ )
69
+
70
+ # 内容获取
71
+ self.value: str = self._get_node_value(node)
72
+ self.description: str = self._read(node, "HelpText", "")
73
+
74
+ # --- 3. 坐标转换 (窗口相对坐标) ---
75
+ try:
76
+ rect = node.BoundingRectangle
77
+ # Window.capture() returns the client area, so element overlays must
78
+ # use the same physical-coordinate origin rather than the outer frame.
79
+ w_rect = self.window.client_rect
80
+ win_l = w_rect.left if hasattr(w_rect, 'left') else w_rect[0]
81
+ win_t = w_rect.top if hasattr(w_rect, 'top') else w_rect[1]
82
+ self.relative_rect = (
83
+ rect.left - win_l, rect.top - win_t,
84
+ rect.right - win_l, rect.bottom - win_t,
85
+ )
86
+ except Exception:
87
+ self.relative_rect = (0, 0, 0, 0)
88
+
89
+ @staticmethod
90
+ def _read(node: auto.Control, attr: str, default):
91
+ try:
92
+ value = getattr(node, attr)
93
+ return default if value is None else value
94
+ except Exception:
95
+ return default
96
+
97
+ @staticmethod
98
+ def _get_pattern(node: auto.Control, method: str):
99
+ try:
100
+ getter = getattr(node, method, None)
101
+ return getter() if getter else None
102
+ except Exception:
103
+ return None
104
+
105
+ def _check_clickable(self, node: auto.Control) -> bool:
106
+ """检查控件节点是否支持点击交互。
107
+
108
+ 通过检测 InvokePattern、TogglePattern、SelectionItemPattern 来判断。
109
+
110
+ Args:
111
+ node: uiautomation 的 Control 控件对象
112
+
113
+ Returns:
114
+ 支持点击返回 True,否则返回 False
115
+ """
116
+ try:
117
+ return any([
118
+ hasattr(node, 'GetInvokePattern') and node.GetInvokePattern(),
119
+ hasattr(node, 'GetTogglePattern') and node.GetTogglePattern(),
120
+ hasattr(node, 'GetSelectionItemPattern') and node.GetSelectionItemPattern()
121
+ ])
122
+ except:
123
+ return False
124
+
125
+ def _get_node_value(self, node: auto.Control) -> str:
126
+ """获取控件的文本内容值。
127
+
128
+ 优先通过 ValuePattern 获取,若失败则对 EditControl 类型回退到 Name 属性。
129
+
130
+ Args:
131
+ node: uiautomation 的 Control 控件对象
132
+
133
+ Returns:
134
+ 控件的文本值,获取失败返回空字符串
135
+ """
136
+ try:
137
+ if hasattr(node, 'GetValuePattern'):
138
+ val = node.GetValuePattern().Value
139
+ if val and val != self.name:
140
+ return val
141
+ if self.type == "EditControl":
142
+ return node.Name or ""
143
+ except:
144
+ pass
145
+ return ""
146
+
147
+ def to_dict(self) -> Dict[str, Any]:
148
+ """将 UI 元素及其子元素递归转换为字典结构。
149
+
150
+ Returns:
151
+ 包含元素所有属性和子元素的嵌套字典
152
+ """
153
+ return {
154
+ "name": self.name,
155
+ "type": self.type,
156
+ "res_id": self.res_id, # 有就有,没有就是 ""
157
+ "memory_id": self.memory_id, # 纯数字内存地址
158
+ "class_name": self.class_name,
159
+ "handle": self.handle,
160
+ "rect": {
161
+ "left": self.relative_rect[0],
162
+ "top": self.relative_rect[1],
163
+ "right": self.relative_rect[2],
164
+ "bottom": self.relative_rect[3]
165
+ },
166
+ "enabled": self.is_enabled,
167
+ "visible": self.is_visible,
168
+ "clickable": self.is_clickable,
169
+ "value": self.value,
170
+ "description": self.description,
171
+ "password": self.is_password,
172
+ "checkable": self.is_checkable,
173
+ "checked": self.is_checked,
174
+ "editable": self.is_editable,
175
+ "focusable": self.is_focusable,
176
+ "focused": self.is_focused,
177
+ "selected": self.is_selected,
178
+ "depth": self.depth,
179
+ "child_count": len(self.children),
180
+ "children": [child.to_dict() for child in self.children]
181
+ }
182
+
183
+ @property
184
+ def child_count(self) -> int:
185
+ """获取子节点数量。
186
+
187
+ Returns:
188
+ 子元素的个数
189
+ """
190
+ return len(self.children)
191
+
192
+ @property
193
+ def center(self) -> Tuple[int, int]:
194
+ """获取元素相对于窗口的中心点坐标。
195
+
196
+ Returns:
197
+ (x, y) 形式的中心点坐标元组
198
+ """
199
+ l, t, r, b = self.relative_rect
200
+ return (l + r) // 2, (t + b) // 2
201
+
202
+ def click(self, simulate: bool = False):
203
+ """点击当前 UI 元素。
204
+
205
+ 支持底层指令点击和物理模拟点击两种模式。底层指令点击不移动鼠标,
206
+ 物理模拟点击会真实移动鼠标到元素位置。
207
+
208
+ Args:
209
+ simulate: True 为强制物理模拟点击,False 为优先使用底层指令点击(失败时自动降级)
210
+
211
+ Returns:
212
+ 返回自身(UIElement),支持链式调用
213
+ """
214
+ if not simulate:
215
+ # 1. 尝试使用 InvokePattern (最通用的底层点击)
216
+ try:
217
+ pattern = self.node.GetInvokePattern()
218
+ if pattern:
219
+ pattern.Invoke()
220
+ return self
221
+ except:
222
+ pass
223
+
224
+ # 2. 尝试使用 TogglePattern (适用于复选框、某些开关按钮)
225
+ try:
226
+ pattern = self.node.GetTogglePattern()
227
+ if pattern:
228
+ pattern.Toggle()
229
+ return self
230
+ except:
231
+ pass
232
+
233
+ # 3. 降级方案:如果上述底层接口不支持,或者明确要求 simulate=True
234
+ # 使用物理模拟点击。
235
+ # 注意:uiautomation 的 Click 默认就是移动鼠标。
236
+ # 如果不想移动鼠标但还要发物理点击,基本不可能,除非用 PostMessage。
237
+ cx, cy = self.center
238
+ if simulate:
239
+ self.node.Click() # 这里的 Click 会移动鼠标
240
+ else:
241
+ # 使用你 Window 封装的底层坐标点击逻辑
242
+ self.window.click(cx, cy)
243
+
244
+ return self
245
+
246
+ def input(self, text: str, simulate: bool = False):
247
+ """向当前 UI 元素输入文本。
248
+
249
+ 支持底层直接赋值和模拟键盘敲击两种模式。
250
+
251
+ Args:
252
+ text: 要输入的文本字符串
253
+ simulate: False 为优先使用 ValuePattern 直接设值(静默且快),
254
+ True 为使用 SendKeys 模拟键盘敲击(适合需要触发监听事件的输入框)
255
+
256
+ Returns:
257
+ 返回自身(UIElement),支持链式调用
258
+ """
259
+ if not simulate:
260
+ try:
261
+ # 1. 尝试 ValuePattern (最通用的设置文本方式)
262
+ pattern = self.node.GetValuePattern()
263
+ if pattern:
264
+ pattern.SetValue(text)
265
+ return self
266
+ except:
267
+ pass
268
+
269
+ # 2. 降级方案 或 强制模拟:先点击聚焦,再发送按键
270
+ # 注意:这里调用的是你已有的 click()
271
+ self.click(simulate=simulate)
272
+ self.node.SendKeys(text)
273
+ return self
274
+
275
+ def capture(self, save_path: str = None) -> Image:
276
+ """捕获当前 UI 元素的截图。
277
+
278
+ Args:
279
+ save_path: 图片保存路径,为 None 则不保存到文件
280
+
281
+ Returns:
282
+ Pillow Image 对象
283
+ """
284
+ # uiautomation 的 CaptureToImage 返回的是 Pillow 的 Image 对象
285
+ img = self.node.CaptureToImage()
286
+
287
+ if save_path:
288
+ img.save(save_path)
289
+
290
+ return img
291
+
292
+ def __repr__(self):
293
+ return f"<UIElement: [{self.type}] res_id: {self.res_id} memory_id: {self.memory_id}>"
294
+
295
+
296
+
297
+ class Selector:
298
+ """UI 元素选择器,支持链式查询和操作。
299
+
300
+ 提供类似 CSS 选择器的链式 API,可通过属性过滤、层级关系(子节点、父节点、兄弟节点)
301
+ 组合定位 UI 元素,并支持在查询链中插入点击和输入动作。
302
+ """
303
+
304
+ def __init__(
305
+ self,
306
+ window: Any = None,
307
+ title: str = None,
308
+ depth: int = 0xFFFFFFFF,
309
+ *,
310
+ app: Optional[str] = None,
311
+ title_re: Optional[str] = None,
312
+ class_name: Optional[str] = None,
313
+ content: Optional[Union[bool, str]] = None,
314
+ timeout: float = 10,
315
+ ):
316
+ """初始化选择器。
317
+
318
+ Args:
319
+ window: 已存在的 Window 对象实例,作为搜索的根窗口
320
+ title: 窗口标题,当 window 为 None 时通过标题自动查找窗口
321
+ depth: 最大搜索深度,默认为 0xFFFFFFFF(不限制)
322
+ app: 进程名,例如 ``chrome.exe``,运行时动态查找窗口
323
+ title_re: 用于区分同进程窗口的标题正则表达式
324
+ class_name: 用于区分同进程窗口的窗口类名
325
+ content: True 自动进入 Chromium 内容窗口;字符串指定内容窗口类名;
326
+ None 时会自动识别 Chromium/Electron 窗口
327
+ timeout: 等待应用窗口和指定内容窗口的超时时间
328
+ """
329
+ self.window = window
330
+
331
+ if self.window is None and any((app, title_re, class_name)):
332
+ self.window = self._resolve_app_window(
333
+ process_name=app,
334
+ title_re=title_re,
335
+ class_name=class_name,
336
+ content=content,
337
+ timeout=timeout,
338
+ )
339
+ elif self.window is None and title:
340
+ try:
341
+ self.window = Window.find(title)
342
+ except:
343
+ pass
344
+
345
+ self._steps: List[Dict] = []
346
+ self._current_filters: Dict[str, Any] = {}
347
+ self._tree_depth = depth
348
+ self._max_depth = depth
349
+
350
+ self._attr_map = {
351
+ 'name': 'Name',
352
+ 'type': 'ControlTypeName',
353
+ 'class_name': 'ClassName',
354
+ 'handle': 'NativeWindowHandle',
355
+ 'res_id': 'AutomationId',
356
+ 'description': 'HelpText',
357
+ 'is_enabled': 'IsEnabled',
358
+ 'is_password': 'IsPassword',
359
+ 'is_focusable': 'IsKeyboardFocusable',
360
+ 'is_focused': 'HasKeyboardFocus',
361
+ }
362
+
363
+ @classmethod
364
+ def _resolve_app_window(
365
+ cls,
366
+ *,
367
+ process_name: Optional[str],
368
+ title_re: Optional[str],
369
+ class_name: Optional[str],
370
+ content: Optional[Union[bool, str]],
371
+ timeout: float,
372
+ ) -> Window:
373
+ if not any((process_name, title_re, class_name)):
374
+ raise ValueError("app, title_re or class_name is required")
375
+ window = Window.find(
376
+ title_re=title_re,
377
+ process_name=process_name,
378
+ class_name=class_name,
379
+ timeout=timeout,
380
+ )
381
+ if window is None:
382
+ raise LookupError("target application window was not found")
383
+
384
+ root_class = str(getattr(window, "class_name", "") or "")
385
+ auto_content = content is None and root_class.startswith("Chrome_WidgetWin_")
386
+ if content or auto_content:
387
+ child_class = "Chrome_RenderWidgetHostHWND" if content in (None, True) else str(content)
388
+ content_window = window.find_child(
389
+ class_name=child_class, visible_only=True, timeout=timeout,
390
+ )
391
+ if content_window is not None:
392
+ return content_window
393
+ if content:
394
+ raise LookupError(f"application content window was not found: {child_class}")
395
+ return window
396
+
397
+ @classmethod
398
+ def app(
399
+ cls,
400
+ process_name: Optional[str] = None,
401
+ *,
402
+ title_re: Optional[str] = None,
403
+ class_name: Optional[str] = None,
404
+ content: Union[bool, str] = False,
405
+ timeout: float = 10,
406
+ depth: int = 0xFFFFFFFF,
407
+ ) -> 'Selector':
408
+ """Create a selector scoped to an application instead of a volatile HWND.
409
+
410
+ ``content=True`` automatically enters the active Chromium/Electron
411
+ renderer window. A custom descendant class name may also be supplied.
412
+ """
413
+ return cls(
414
+ app=process_name,
415
+ title_re=title_re,
416
+ class_name=class_name,
417
+ content=content,
418
+ timeout=timeout,
419
+ depth=depth,
420
+ )
421
+
422
+ def _add_filter(self, key: str, value: Any):
423
+ """添加属性过滤条件到当前查询步骤。
424
+
425
+ 如果上一步是关系跳转(child/parent/brother),则将过滤条件附加到该步骤;
426
+ 否则添加到当前待提交的过滤条件集合。
427
+
428
+ Args:
429
+ key: 过滤属性名(如 'name'、'type'、'res_id' 等)
430
+ value: 过滤属性值
431
+ """
432
+ if self._steps and self._steps[-1]['method'] in ['child', 'parent', 'brother']:
433
+ self._steps[-1]['filters'][key] = value
434
+ else:
435
+ self._current_filters[key] = value
436
+
437
+ def _flush_to_step(self):
438
+ """将当前累积的过滤条件和深度设置提交为一个 find 查询步骤。
439
+
440
+ 如果当前没有待提交的过滤条件且深度为默认值,则不执行任何操作。
441
+ """
442
+ if self._current_filters or self._max_depth != 0xFFFFFFFF:
443
+ self._steps.append({
444
+ 'method': 'find',
445
+ 'param': None,
446
+ 'filters': self._current_filters.copy(),
447
+ 'max_depth': self._max_depth
448
+ })
449
+ self._current_filters = {}
450
+ self._max_depth = 0xFFFFFFFF
451
+
452
+ def _add_relation(self, method: str, param: Any):
453
+ """添加关系跳转步骤(child/parent/brother)。
454
+
455
+ 先提交当前累积的过滤条件,再添加一个关系跳转步骤。
456
+
457
+ Args:
458
+ method: 关系类型,可选 'child'、'parent'、'brother'
459
+ param: 跳转参数(索引值),None 表示获取全部
460
+ """
461
+ self._flush_to_step()
462
+ self._steps.append({
463
+ 'method': method,
464
+ 'param': param,
465
+ 'filters': {},
466
+ 'max_depth': 0xFFFFFFFF
467
+ })
468
+
469
+ # --- 属性链式调用 ---
470
+ def name(self, val: str) -> 'Selector':
471
+ """按控件名称过滤(支持正则匹配)。
472
+
473
+ Args:
474
+ val: 名称匹配模式(正则表达式)
475
+
476
+ Returns:
477
+ Selector 自身,支持链式调用
478
+ """
479
+ self._add_filter('name', val); return self
480
+
481
+ def type(self, val: str) -> 'Selector':
482
+ """按控件类型过滤(如 'ButtonControl'、'EditControl')。
483
+
484
+ Args:
485
+ val: 控件类型名称(支持正则匹配)
486
+
487
+ Returns:
488
+ Selector 自身,支持链式调用
489
+ """
490
+ self._add_filter('type', val); return self
491
+
492
+ def class_name(self, val: str) -> 'Selector':
493
+ """按窗口类名过滤。
494
+
495
+ Args:
496
+ val: 窗口类名(支持正则匹配)
497
+
498
+ Returns:
499
+ Selector 自身,支持链式调用
500
+ """
501
+ self._add_filter('class_name', val); return self
502
+
503
+ def res_id(self, val: str) -> 'Selector':
504
+ """按 AutomationId 资源 ID 过滤。
505
+
506
+ Args:
507
+ val: AutomationId 值(支持正则匹配)
508
+
509
+ Returns:
510
+ Selector 自身,支持链式调用
511
+ """
512
+ self._add_filter('res_id', val); return self
513
+
514
+ def value(self, val: str) -> 'Selector':
515
+ """按控件的文本值过滤。
516
+
517
+ Args:
518
+ val: 文本值(支持正则匹配)
519
+
520
+ Returns:
521
+ Selector 自身,支持链式调用
522
+ """
523
+ self._add_filter('value', val); return self
524
+
525
+ def description(self, val: str) -> 'Selector':
526
+ """按控件的帮助文本描述过滤。
527
+
528
+ Args:
529
+ val: 描述文本(支持正则匹配)
530
+
531
+ Returns:
532
+ Selector 自身,支持链式调用
533
+ """
534
+ self._add_filter('description', val); return self
535
+
536
+ def handle(self, val: int) -> 'Selector':
537
+ """按原生窗口句柄过滤。
538
+
539
+ Args:
540
+ val: 窗口句柄值(精确匹配)
541
+
542
+ Returns:
543
+ Selector 自身,支持链式调用
544
+ """
545
+ self._add_filter('handle', val); return self
546
+
547
+ def enabled(self, val: bool = True) -> 'Selector':
548
+ """按是否启用状态过滤。
549
+
550
+ Args:
551
+ val: True 为只匹配启用的控件,False 为只匹配禁用的控件
552
+
553
+ Returns:
554
+ Selector 自身,支持链式调用
555
+ """
556
+ self._add_filter('is_enabled', val); return self
557
+
558
+ def visible(self, val: bool = True) -> 'Selector':
559
+ """按是否可见状态过滤。
560
+
561
+ Args:
562
+ val: True 为只匹配可见的控件,False 为只匹配不可见的控件
563
+
564
+ Returns:
565
+ Selector 自身,支持链式调用
566
+ """
567
+ self._add_filter('is_visible', val); return self
568
+
569
+ def clickable(self, val: bool = True) -> 'Selector':
570
+ """按是否可点击状态过滤。
571
+
572
+ Args:
573
+ val: True 为只匹配可点击的控件,False 为只匹配不可点击的控件
574
+
575
+ Returns:
576
+ Selector 自身,支持链式调用
577
+ """
578
+ self._add_filter('is_clickable', val); return self
579
+
580
+ def password(self, val: bool = True) -> 'Selector':
581
+ """按是否为密码输入框过滤。
582
+
583
+ Args:
584
+ val: True 为只匹配密码框,False 为只匹配非密码框
585
+
586
+ Returns:
587
+ Selector 自身,支持链式调用
588
+ """
589
+ self._add_filter('is_password', val); return self
590
+
591
+ def checkable(self, val: bool = True) -> 'Selector':
592
+ """按控件是否支持 TogglePattern 过滤。"""
593
+ self._add_filter('is_checkable', val); return self
594
+
595
+ def checked(self, val: bool = True) -> 'Selector':
596
+ """按复选框或开关当前是否选中过滤。"""
597
+ self._add_filter('is_checked', val); return self
598
+
599
+ def editable(self, val: bool = True) -> 'Selector':
600
+ """按控件是否支持可写 ValuePattern 过滤。"""
601
+ self._add_filter('is_editable', val); return self
602
+
603
+ def focusable(self, val: bool = True) -> 'Selector':
604
+ """按控件是否可获取键盘焦点过滤。"""
605
+ self._add_filter('is_focusable', val); return self
606
+
607
+ def focused(self, val: bool = True) -> 'Selector':
608
+ """按控件当前是否拥有键盘焦点过滤。"""
609
+ self._add_filter('is_focused', val); return self
610
+
611
+ def selected(self, val: bool = True) -> 'Selector':
612
+ """按 SelectionItemPattern 的选择状态过滤。"""
613
+ self._add_filter('is_selected', val); return self
614
+
615
+ def child_count(self, count: int) -> 'Selector':
616
+ """按子节点数量过滤。
617
+
618
+ Args:
619
+ count: 精确匹配的子节点数量
620
+
621
+ Returns:
622
+ Selector 自身,支持链式调用
623
+ """
624
+ self._add_filter('child_count', count); return self
625
+
626
+ def node_depth(self, depth: int) -> 'Selector':
627
+ """按元素在当前查询树中的实际深度过滤。"""
628
+ self._add_filter('_node_depth', int(depth)); return self
629
+
630
+ # --- 关系链式调用 ---
631
+ def depth(self, d: int) -> 'Selector':
632
+ """设置后续查找的最大搜索深度。
633
+
634
+ Args:
635
+ d: 最大搜索深度
636
+
637
+ Returns:
638
+ Selector 自身,支持链式调用
639
+ """
640
+ self._max_depth = d; return self
641
+
642
+ def child(self, index: Any = None) -> 'Selector':
643
+ """跳转到子节点。
644
+
645
+ Args:
646
+ index: 子节点索引(正数从1开始,负数从末尾,float 为相对偏移,None 为全部子节点)
647
+
648
+ Returns:
649
+ Selector 自身,支持链式调用
650
+ """
651
+ self._add_relation('child', index); return self
652
+
653
+ def parent(self, index: Any = None) -> 'Selector':
654
+ """跳转到父节点。
655
+
656
+ Args:
657
+ index: 向上跳转的层数,默认为 1 层
658
+
659
+ Returns:
660
+ Selector 自身,支持链式调用
661
+ """
662
+ self._add_relation('parent', index); return self
663
+
664
+ def brother(self, index: Any = None) -> 'Selector':
665
+ """跳转到兄弟节点。
666
+
667
+ Args:
668
+ index: 兄弟节点索引(正数从1开始,负数从末尾,float 为相对偏移,None 为全部兄弟)
669
+
670
+ Returns:
671
+ Selector 自身,支持链式调用
672
+ """
673
+ self._add_relation('brother', index); return self
674
+
675
+ # --- 动作插值 ---
676
+ def click(self, simulate: bool = False) -> 'Selector':
677
+ """在查询链中插入点击动作,对当前匹配的所有元素执行点击。
678
+
679
+ Args:
680
+ simulate: True 为物理模拟点击,False 为底层指令点击
681
+
682
+ Returns:
683
+ Selector 自身,支持链式调用
684
+ """
685
+ self._flush_to_step()
686
+ self._steps.append({'method': 'action', 'action_type': 'click', 'param': simulate, 'filters': {}})
687
+ return self
688
+
689
+ def input(self, msg: str) -> 'Selector':
690
+ """在查询链中插入文本输入动作,对当前匹配的所有元素执行输入。
691
+
692
+ Args:
693
+ msg: 要输入的文本字符串
694
+
695
+ Returns:
696
+ Selector 自身,支持链式调用
697
+ """
698
+ self._flush_to_step()
699
+ self._steps.append({'method': 'action', 'action_type': 'input', 'param': msg, 'filters': {}})
700
+ return self
701
+
702
+ # --- 匹配内核 ---
703
+ def _match_node(self, node: auto.Control, filters: Dict, depth: Optional[int] = None) -> bool:
704
+ """检查控件节点是否满足所有过滤条件。
705
+
706
+ 支持布尔精确匹配、整数精确匹配和字符串正则匹配(不区分大小写)。
707
+
708
+ Args:
709
+ node: uiautomation 的 Control 控件对象
710
+ filters: 过滤条件字典,键为属性名,值为期望匹配的值
711
+
712
+ Returns:
713
+ 所有条件都满足返回 True,否则返回 False
714
+ """
715
+ if not filters: return True
716
+ for key, target in filters.items():
717
+ actual = None
718
+ if key == '_node_depth':
719
+ actual = depth
720
+ elif key == 'value':
721
+ try:
722
+ p = node.GetValuePattern()
723
+ v = p.Value if p else ""
724
+ actual = v if (v and v != node.Name) else (
725
+ "" if node.ControlTypeName != "EditControl" else node.Name)
726
+ except:
727
+ actual = ""
728
+ elif key == 'is_clickable':
729
+ try:
730
+ actual = any([hasattr(node, f'Get{p}Pattern') and getattr(node, f'Get{p}Pattern')()
731
+ for p in ['Invoke', 'Toggle', 'SelectionItem']])
732
+ except:
733
+ actual = False
734
+ elif key in ('is_checkable', 'is_checked'):
735
+ pattern = UIElement._get_pattern(node, 'GetTogglePattern')
736
+ actual = pattern is not None if key == 'is_checkable' else bool(
737
+ UIElement._read(pattern, 'ToggleState', 0)
738
+ ) if pattern is not None else False
739
+ elif key == 'is_selected':
740
+ pattern = UIElement._get_pattern(node, 'GetSelectionItemPattern')
741
+ actual = bool(UIElement._read(pattern, 'IsSelected', False)) if pattern else False
742
+ elif key == 'is_editable':
743
+ pattern = UIElement._get_pattern(node, 'GetValuePattern')
744
+ actual = bool(pattern and not bool(UIElement._read(pattern, 'IsReadOnly', True)))
745
+ elif key == 'child_count':
746
+ try:
747
+ actual = len(node.GetChildren())
748
+ except:
749
+ actual = 0
750
+ elif key == 'is_visible':
751
+ try:
752
+ actual = not node.IsOffscreen
753
+ except:
754
+ actual = False
755
+ else:
756
+ native_key = self._attr_map.get(key, key)
757
+ try:
758
+ actual = getattr(node, native_key, None)
759
+ except Exception:
760
+ actual = None
761
+
762
+ if isinstance(target, bool):
763
+ if bool(actual) != target: return False
764
+ elif isinstance(target, int):
765
+ if actual != target: return False
766
+ else:
767
+ try:
768
+ if not re.search(str(target), str(actual or ""), re.IGNORECASE): return False
769
+ except:
770
+ if str(target).lower() not in str(actual or "").lower(): return False
771
+ return True
772
+
773
+ # --- 执行引擎 ---
774
+ def _execute(self, first_only: bool = False) -> List[Any]:
775
+ """执行查询链,返回所有匹配的 UIElement 列表。
776
+
777
+ 按照之前通过链式调用构建的查询步骤依次执行,
778
+ 包括属性过滤、关系跳转和动作执行,最终返回去重后的结果。
779
+
780
+ Returns:
781
+ 匹配的 UIElement 对象列表
782
+ """
783
+ self._flush_to_step()
784
+
785
+ if self.window and hasattr(self.window, 'hwnd') and self.window.hwnd:
786
+ root = auto.ControlFromHandle(self.window.hwnd)
787
+ current_pool = [(root, 0)]
788
+ else:
789
+ current_pool = [(auto.GetRootControl(), 0)]
790
+
791
+ for step_index, step in enumerate(self._steps):
792
+ next_pool = []
793
+ method = step['method']
794
+ param = step.get('param')
795
+ filters = step.get('filters', {})
796
+
797
+ if method == 'action':
798
+ action_type = step.get('action_type')
799
+ for node, d in current_pool:
800
+ try:
801
+ temp_el = UIElement(node, self.window, d)
802
+ if action_type == 'click':
803
+ temp_el.click(simulate=param)
804
+ elif action_type == 'input':
805
+ val_pat = node.GetValuePattern()
806
+ if val_pat:
807
+ val_pat.SetValue(param)
808
+ else:
809
+ node.SendKeys(param)
810
+ except Exception as e:
811
+ print(f"执行插值动作 {action_type} 失败: {e}")
812
+ continue
813
+
814
+ max_d = step.get('max_depth', 0xFFFFFFFF)
815
+
816
+ for node, d in current_pool:
817
+ if method == 'find':
818
+ candidates = [(node, 0)]
819
+ candidates.extend(self._walk_controls(node, max_d))
820
+ for c, rel_d in candidates:
821
+ if self._match_node(c, filters, d + rel_d):
822
+ next_pool.append((c, d + rel_d))
823
+ if first_only and step_index == len(self._steps) - 1:
824
+ break
825
+ else:
826
+ # --- 轴跳转逻辑 ---
827
+ if method == 'child':
828
+ try:
829
+ candidates = node.GetChildren()
830
+ except Exception:
831
+ candidates = []
832
+ new_d = d + 1
833
+ if isinstance(param, float):
834
+ picked = self._pick_by_float(candidates, node, param)
835
+ next_pool.extend([(p, new_d) for p in picked if self._match_node(p, filters, new_d)])
836
+ else:
837
+ matched = [c for c in candidates if self._match_node(c, filters, new_d)]
838
+ picked = self._pick_by_int(matched, param)
839
+ next_pool.extend([(p, new_d) for p in picked])
840
+
841
+ elif method == 'parent':
842
+ # 修改后的 parent 逻辑:支持向上跳 n 层
843
+ # param 为调用 .parent(n) 时传入的值
844
+ up_level = param if (isinstance(param, int) and param > 0) else 1
845
+ curr_p = node
846
+ for _ in range(up_level):
847
+ try:
848
+ curr_p = curr_p.GetParentControl()
849
+ except Exception:
850
+ curr_p = None
851
+ if not curr_p: break
852
+
853
+ # 跳转完成后进行属性匹配,并计算新的深度
854
+ parent_depth = max(0, d - up_level)
855
+ if curr_p and self._match_node(curr_p, filters, parent_depth):
856
+ next_pool.append((curr_p, parent_depth))
857
+
858
+ elif method == 'brother':
859
+ try:
860
+ p = node.GetParentControl()
861
+ candidates = p.GetChildren() if p else []
862
+ except Exception:
863
+ candidates = []
864
+ new_d = d
865
+ if isinstance(param, float):
866
+ picked = self._pick_by_float(candidates, node, param)
867
+ next_pool.extend([(p, new_d) for p in picked if self._match_node(p, filters, new_d)])
868
+ else:
869
+ matched = [c for c in candidates if not self._same_control(c, node) and self._match_node(c, filters, new_d)]
870
+ picked = self._pick_by_int(matched, param)
871
+ next_pool.extend([(p, new_d) for p in picked])
872
+
873
+ current_pool = next_pool
874
+ if not current_pool: break
875
+
876
+ final_list = []
877
+ seen_ids = set()
878
+ for node, depth in current_pool:
879
+ try:
880
+ rid = tuple(node.GetRuntimeId())
881
+ if rid in seen_ids: continue
882
+ seen_ids.add(rid)
883
+ except:
884
+ pass
885
+ final_list.append(UIElement(node, self.window, depth))
886
+
887
+ return final_list[:1] if first_only else final_list
888
+
889
+ def find_all(self) -> List[Any]:
890
+ """执行查询链,返回所有去重后的匹配元素。"""
891
+ return self._execute(first_only=False)
892
+
893
+ @staticmethod
894
+ def _same_control(left, right) -> bool:
895
+ try:
896
+ return bool(auto.CompareControl(left, right))
897
+ except Exception:
898
+ return left is right
899
+
900
+ @staticmethod
901
+ def _walk_controls(root, max_depth):
902
+ """Depth-first Raw View traversal with per-provider fault isolation."""
903
+ if max_depth <= 0:
904
+ return
905
+ depth = 0
906
+ try:
907
+ first = root.GetFirstChildControl()
908
+ except Exception:
909
+ first = None
910
+ levels = [first]
911
+ while depth >= 0:
912
+ current = levels[-1]
913
+ if current is None:
914
+ levels.pop()
915
+ depth -= 1
916
+ continue
917
+ yield current, depth + 1
918
+ try:
919
+ levels[depth] = current.GetNextSiblingControl()
920
+ except Exception:
921
+ levels[depth] = None
922
+ if depth + 1 < max_depth:
923
+ try:
924
+ child = current.GetFirstChildControl()
925
+ except Exception:
926
+ child = None
927
+ if child is not None:
928
+ depth += 1
929
+ levels.append(child)
930
+
931
+ def _pick_by_float(self, candidates, current, param) -> List[auto.Control]:
932
+ """通过浮点数相对偏移量从候选列表中选取控件。
933
+
934
+ 以当前控件在候选列表中的位置为基准,按 param * 10 的偏移量选取目标。
935
+
936
+ Args:
937
+ candidates: 候选控件列表
938
+ current: 当前基准控件
939
+ param: 浮点数偏移量(如 0.1 表示偏移 1 位,-0.2 表示偏移 -2 位)
940
+
941
+ Returns:
942
+ 选中的控件列表(0 或 1 个元素)
943
+ """
944
+ try:
945
+ curr_idx = -1
946
+ for i, c in enumerate(candidates):
947
+ if auto.CompareControl(c, current):
948
+ curr_idx = i;
949
+ break
950
+ if curr_idx == -1: return []
951
+ target_idx = curr_idx + int(round(param * 10))
952
+ return [candidates[target_idx]] if 0 <= target_idx < len(candidates) else []
953
+ except:
954
+ return []
955
+
956
+ def _pick_by_int(self, matched, param) -> List[auto.Control]:
957
+ """通过整数索引从已匹配的控件列表中选取。
958
+
959
+ Args:
960
+ matched: 已通过过滤条件匹配的控件列表
961
+ param: 整数索引(正数从1开始,负数从末尾),None 返回全部
962
+
963
+ Returns:
964
+ 选中的控件列表
965
+ """
966
+ if param is None: return matched
967
+ try:
968
+ idx = int(param)
969
+ if idx > 0: return [matched[idx - 1]] if idx <= len(matched) else []
970
+ if idx < 0: return [matched[idx]] if abs(idx) <= len(matched) else []
971
+ except:
972
+ pass
973
+ return []
974
+
975
+ def find(self) -> Optional[Any]:
976
+ """执行查询链,返回第一个匹配的 UIElement。
977
+
978
+ Returns:
979
+ 匹配的第一个 UIElement 对象,未找到则返回 None
980
+ """
981
+ res = self._execute(first_only=True)
982
+ return res[0] if res else None
983
+
984
+ def find_first(self) -> Optional[Any]:
985
+ """Compatibility alias used by generated inspector selectors."""
986
+ return self.find()
987
+
988
+ def get_uielement_tree(self, as_json: bool = False) -> Union[Any, str]:
989
+ """获取窗口的完整 UI 元素树。
990
+
991
+ 遍历窗口下所有控件,构建层级化的 UIElement 树结构。
992
+
993
+ Args:
994
+ as_json: True 返回 JSON 字符串,False 返回根 UIElement 对象
995
+
996
+ Returns:
997
+ UI 元素树的根节点(UIElement)或其 JSON 字符串表示;窗口无效时返回 None 或空字符串
998
+ """
999
+ if not self.window.hwnd: return "" if as_json else None
1000
+ root = auto.ControlFromHandle(self.window.hwnd)
1001
+ if not root.Exists(0): return "" if as_json else None
1002
+
1003
+ root_element = UIElement(root, self.window, 0)
1004
+ stack = {0: root_element}
1005
+ for control, depth in self._walk_controls(root, self._tree_depth):
1006
+ try:
1007
+ new_el = UIElement(control, self.window, depth)
1008
+ parent = stack.get(depth - 1)
1009
+ if parent: parent.children.append(new_el)
1010
+ stack[depth] = new_el
1011
+ except Exception as e:
1012
+ print(f"跳过异常 UIA 节点 depth={depth}: {e}")
1013
+
1014
+ if as_json: return json.dumps(root_element.to_dict(), ensure_ascii=False, indent=2)
1015
+ return root_element