pyxllib 0.3.197__py3-none-any.whl → 3.201.1__py3-none-any.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.
- pyxllib/__init__.py +14 -21
- pyxllib/algo/__init__.py +8 -8
- pyxllib/algo/disjoint.py +54 -54
- pyxllib/algo/geo.py +537 -541
- pyxllib/algo/intervals.py +964 -964
- pyxllib/algo/matcher.py +389 -389
- pyxllib/algo/newbie.py +166 -166
- pyxllib/algo/pupil.py +629 -629
- pyxllib/algo/shapelylib.py +67 -67
- pyxllib/algo/specialist.py +241 -241
- pyxllib/algo/stat.py +494 -494
- pyxllib/algo/treelib.py +145 -149
- pyxllib/algo/unitlib.py +62 -66
- pyxllib/autogui/__init__.py +5 -5
- pyxllib/autogui/activewin.py +246 -246
- pyxllib/autogui/all.py +9 -9
- pyxllib/autogui/autogui.py +846 -852
- pyxllib/autogui/uiautolib.py +362 -362
- pyxllib/autogui/virtualkey.py +102 -102
- pyxllib/autogui/wechat.py +827 -827
- pyxllib/autogui/wechat_msg.py +421 -421
- pyxllib/autogui/wxautolib.py +84 -84
- pyxllib/cv/__init__.py +5 -5
- pyxllib/cv/expert.py +267 -267
- pyxllib/cv/imfile.py +159 -159
- pyxllib/cv/imhash.py +39 -39
- pyxllib/cv/pupil.py +9 -9
- pyxllib/cv/rgbfmt.py +1525 -1525
- pyxllib/cv/slidercaptcha.py +137 -137
- pyxllib/cv/trackbartools.py +251 -251
- pyxllib/cv/xlcvlib.py +1040 -1040
- pyxllib/cv/xlpillib.py +423 -423
- pyxllib/data/echarts.py +236 -240
- pyxllib/data/jsonlib.py +85 -89
- pyxllib/data/oss.py +72 -72
- pyxllib/data/pglib.py +1111 -1127
- pyxllib/data/sqlite.py +568 -568
- pyxllib/data/sqllib.py +297 -297
- pyxllib/ext/JLineViewer.py +505 -505
- pyxllib/ext/__init__.py +6 -6
- pyxllib/ext/demolib.py +251 -246
- pyxllib/ext/drissionlib.py +277 -277
- pyxllib/ext/kq5034lib.py +12 -12
- pyxllib/ext/qt.py +449 -449
- pyxllib/ext/robustprocfile.py +493 -497
- pyxllib/ext/seleniumlib.py +76 -76
- pyxllib/ext/tk.py +173 -173
- pyxllib/ext/unixlib.py +821 -827
- pyxllib/ext/utools.py +345 -351
- pyxllib/ext/webhook.py +124 -119
- pyxllib/ext/win32lib.py +40 -40
- pyxllib/ext/wjxlib.py +91 -88
- pyxllib/ext/wpsapi.py +124 -124
- pyxllib/ext/xlwork.py +9 -9
- pyxllib/ext/yuquelib.py +1110 -1105
- pyxllib/file/__init__.py +17 -17
- pyxllib/file/docxlib.py +757 -761
- pyxllib/file/gitlib.py +309 -309
- pyxllib/file/libreoffice.py +165 -165
- pyxllib/file/movielib.py +144 -148
- pyxllib/file/newbie.py +10 -10
- pyxllib/file/onenotelib.py +1469 -1469
- pyxllib/file/packlib/__init__.py +330 -330
- pyxllib/file/packlib/zipfile.py +2441 -2441
- pyxllib/file/pdflib.py +422 -426
- pyxllib/file/pupil.py +185 -185
- pyxllib/file/specialist/__init__.py +681 -685
- pyxllib/file/specialist/dirlib.py +799 -799
- pyxllib/file/specialist/download.py +193 -193
- pyxllib/file/specialist/filelib.py +2825 -2829
- pyxllib/file/xlsxlib.py +3122 -3131
- pyxllib/file/xlsyncfile.py +341 -341
- pyxllib/prog/__init__.py +5 -5
- pyxllib/prog/cachetools.py +58 -64
- pyxllib/prog/deprecatedlib.py +233 -233
- pyxllib/prog/filelock.py +42 -42
- pyxllib/prog/ipyexec.py +253 -253
- pyxllib/prog/multiprogs.py +940 -940
- pyxllib/prog/newbie.py +451 -451
- pyxllib/prog/pupil.py +1208 -1197
- pyxllib/prog/sitepackages.py +33 -33
- pyxllib/prog/specialist/__init__.py +348 -391
- pyxllib/prog/specialist/bc.py +203 -203
- pyxllib/prog/specialist/browser.py +497 -497
- pyxllib/prog/specialist/common.py +347 -347
- pyxllib/prog/specialist/datetime.py +198 -198
- pyxllib/prog/specialist/tictoc.py +240 -240
- pyxllib/prog/specialist/xllog.py +180 -180
- pyxllib/prog/xlosenv.py +110 -108
- pyxllib/stdlib/__init__.py +17 -17
- pyxllib/stdlib/tablepyxl/__init__.py +10 -10
- pyxllib/stdlib/tablepyxl/style.py +303 -303
- pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
- pyxllib/text/__init__.py +8 -8
- pyxllib/text/ahocorasick.py +36 -39
- pyxllib/text/airscript.js +754 -744
- pyxllib/text/charclasslib.py +121 -121
- pyxllib/text/jiebalib.py +267 -267
- pyxllib/text/jinjalib.py +27 -32
- pyxllib/text/jsa_ai_prompt.md +271 -271
- pyxllib/text/jscode.py +922 -922
- pyxllib/text/latex/__init__.py +158 -158
- pyxllib/text/levenshtein.py +303 -303
- pyxllib/text/nestenv.py +1215 -1215
- pyxllib/text/newbie.py +300 -300
- pyxllib/text/pupil/__init__.py +8 -8
- pyxllib/text/pupil/common.py +1121 -1121
- pyxllib/text/pupil/xlalign.py +326 -326
- pyxllib/text/pycode.py +47 -47
- pyxllib/text/specialist/__init__.py +8 -8
- pyxllib/text/specialist/common.py +112 -112
- pyxllib/text/specialist/ptag.py +186 -186
- pyxllib/text/spellchecker.py +172 -172
- pyxllib/text/templates/echart_base.html +10 -10
- pyxllib/text/templates/highlight_code.html +16 -16
- pyxllib/text/templates/latex_editor.html +102 -102
- pyxllib/text/vbacode.py +17 -17
- pyxllib/text/xmllib.py +741 -747
- pyxllib/xl.py +42 -39
- pyxllib/xlcv.py +17 -17
- pyxllib-3.201.1.dist-info/METADATA +296 -0
- pyxllib-3.201.1.dist-info/RECORD +125 -0
- {pyxllib-0.3.197.dist-info → pyxllib-3.201.1.dist-info}/licenses/LICENSE +190 -190
- pyxllib/ext/old.py +0 -663
- pyxllib-0.3.197.dist-info/METADATA +0 -48
- pyxllib-0.3.197.dist-info/RECORD +0 -126
- {pyxllib-0.3.197.dist-info → pyxllib-3.201.1.dist-info}/WHEEL +0 -0
pyxllib/autogui/uiautolib.py
CHANGED
@@ -1,362 +1,362 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
|
-
# @Author : 陈坤泽
|
4
|
-
# @Email : 877362867@qq.com
|
5
|
-
# @Date : 2024/11/05
|
6
|
-
|
7
|
-
"""
|
8
|
-
以uiautomation为核心的相关工具库
|
9
|
-
"""
|
10
|
-
|
11
|
-
import os
|
12
|
-
import sys
|
13
|
-
import textwrap
|
14
|
-
import time
|
15
|
-
from typing import Iterable, Callable, List
|
16
|
-
import subprocess
|
17
|
-
import tempfile
|
18
|
-
|
19
|
-
import psutil
|
20
|
-
import pandas as pd
|
21
|
-
from fastcore.basics import GetAttr
|
22
|
-
|
23
|
-
from loguru import logger
|
24
|
-
# ui组件大多是树形组织结构,auto库自带树形操作太弱。没有专业的树形结构库,能搞个毛线。
|
25
|
-
from anytree import NodeMixin
|
26
|
-
|
27
|
-
import ctypes
|
28
|
-
from ctypes import wintypes
|
29
|
-
|
30
|
-
if sys.platform == 'win32':
|
31
|
-
import win32con
|
32
|
-
import win32gui
|
33
|
-
import win32process
|
34
|
-
import win32clipboard
|
35
|
-
|
36
|
-
import uiautomation as uia
|
37
|
-
from uiautomation import WindowControl
|
38
|
-
|
39
|
-
|
40
|
-
def __1_clipboard_utils():
|
41
|
-
pass
|
42
|
-
|
43
|
-
|
44
|
-
def retry_on_failure(max_retries: int = 5):
|
45
|
-
"""
|
46
|
-
一个装饰器,用于在失败时重试执行被装饰的函数。
|
47
|
-
|
48
|
-
Args:
|
49
|
-
max_retries (int): 最大重试次数。
|
50
|
-
|
51
|
-
Returns:
|
52
|
-
Callable: 包装后的函数。
|
53
|
-
"""
|
54
|
-
|
55
|
-
def decorator(func: Callable):
|
56
|
-
def wrapper(*args, **kwargs):
|
57
|
-
for attempt in range(max_retries):
|
58
|
-
try:
|
59
|
-
if func(*args, **kwargs):
|
60
|
-
return True
|
61
|
-
except Exception as e:
|
62
|
-
time.sleep(.05)
|
63
|
-
print(f"Attempt {attempt + 1} failed: {e}")
|
64
|
-
return False
|
65
|
-
|
66
|
-
return wrapper
|
67
|
-
|
68
|
-
return decorator
|
69
|
-
|
70
|
-
|
71
|
-
def set_clipboard_data(fmt: int, buf: ctypes.Array) -> bool:
|
72
|
-
"""
|
73
|
-
将数据设置到Windows剪切板中。
|
74
|
-
|
75
|
-
Args:
|
76
|
-
fmt (int): 数据格式,例如 win32clipboard.CF_HDROP。
|
77
|
-
buf (ctypes.Array): 要设置到剪切板的数据。
|
78
|
-
|
79
|
-
Returns:
|
80
|
-
bool: 操作成功返回 True,否则返回 False。
|
81
|
-
"""
|
82
|
-
try:
|
83
|
-
win32clipboard.OpenClipboard()
|
84
|
-
win32clipboard.EmptyClipboard()
|
85
|
-
win32clipboard.SetClipboardData(fmt, buf)
|
86
|
-
return True
|
87
|
-
except Exception as e:
|
88
|
-
print(f"Error setting clipboard data: {e}")
|
89
|
-
return False
|
90
|
-
finally:
|
91
|
-
win32clipboard.CloseClipboard()
|
92
|
-
|
93
|
-
|
94
|
-
def get_clipboard_files() -> List[str]:
|
95
|
-
"""
|
96
|
-
获取剪切板中的文件路径列表。
|
97
|
-
|
98
|
-
Returns:
|
99
|
-
List[str]: 包含剪切板中文件路径的列表,如果没有文件路径或操作失败,返回空列表。
|
100
|
-
"""
|
101
|
-
try:
|
102
|
-
win32clipboard.OpenClipboard()
|
103
|
-
if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_HDROP):
|
104
|
-
return list(win32clipboard.GetClipboardData(win32clipboard.CF_HDROP))
|
105
|
-
else:
|
106
|
-
return list()
|
107
|
-
finally:
|
108
|
-
win32clipboard.CloseClipboard()
|
109
|
-
|
110
|
-
|
111
|
-
@retry_on_failure(max_retries=5)
|
112
|
-
def validate_clipboard_files(file_paths: Iterable[str], fmt: int, buf: ctypes.Array) -> bool:
|
113
|
-
"""
|
114
|
-
验证剪切板中的文件路径是否与给定的文件路径一致。
|
115
|
-
|
116
|
-
Args:
|
117
|
-
file_paths (Iterable): 一个包含文件路径的可迭代对象,每个路径都是一个字符串。
|
118
|
-
fmt (int): 数据格式,例如 win32clipboard.CF_HDROP。
|
119
|
-
buf (ctypes.Array): 要验证的剪切板数据。
|
120
|
-
|
121
|
-
Returns:
|
122
|
-
bool: 如果剪切板中的文件路径与给定的文件路径一致,则返回 True
|
123
|
-
|
124
|
-
Raises:
|
125
|
-
ValueError: 如果剪切板文件路径与给定文件路径不一致。
|
126
|
-
"""
|
127
|
-
# 设置文件到剪切板
|
128
|
-
set_clipboard_data(fmt, buf)
|
129
|
-
# 验证剪切板中的文件路径是否与给定的文件路径一致
|
130
|
-
if set(get_clipboard_files()) == set(file_paths):
|
131
|
-
return True
|
132
|
-
raise ValueError("剪切板文件路径不对哇!")
|
133
|
-
|
134
|
-
|
135
|
-
def copy_files_to_clipboard(file_paths: Iterable[str]) -> bool:
|
136
|
-
"""
|
137
|
-
将一系列文件路径复制到Windows剪切板。这允许用户在其他应用程序中,如文件资源管理器中粘贴这些文件。
|
138
|
-
|
139
|
-
Args:
|
140
|
-
file_paths (Iterable): 一个包含文件路径的可迭代对象,每个路径都是一个字符串。
|
141
|
-
|
142
|
-
Returns:
|
143
|
-
bool: 如果成功将文件路径复制到剪切板,则返回 True,否则返回 False
|
144
|
-
"""
|
145
|
-
# 定义所需的 Windows 结构和函数
|
146
|
-
CF_HDROP = 15
|
147
|
-
|
148
|
-
class DROPFILES(ctypes.Structure):
|
149
|
-
_fields_ = [("pFiles", wintypes.DWORD),
|
150
|
-
("pt", wintypes.POINT),
|
151
|
-
("fNC", wintypes.BOOL),
|
152
|
-
("fWide", wintypes.BOOL)]
|
153
|
-
|
154
|
-
offset = ctypes.sizeof(DROPFILES)
|
155
|
-
length = sum(len(p) + 1 for p in file_paths) + 1
|
156
|
-
size = offset + length * ctypes.sizeof(wintypes.WCHAR)
|
157
|
-
buf = (ctypes.c_char * size)()
|
158
|
-
df = DROPFILES.from_buffer(buf)
|
159
|
-
df.pFiles, df.fWide = offset, True
|
160
|
-
for path in file_paths:
|
161
|
-
path = os.path.normpath(path)
|
162
|
-
array_t = ctypes.c_wchar * (len(path) + 1)
|
163
|
-
path_buf = array_t.from_buffer(buf, offset)
|
164
|
-
path_buf.value = path
|
165
|
-
offset += ctypes.sizeof(path_buf)
|
166
|
-
buf[offset:offset + ctypes.sizeof(wintypes.WCHAR)] = b'\0\0'
|
167
|
-
|
168
|
-
# 验证文件是否成功复制到剪切板
|
169
|
-
return validate_clipboard_files([os.path.normpath(file) for file in file_paths], CF_HDROP, buf=buf)
|
170
|
-
|
171
|
-
|
172
|
-
def __2_窗口功能():
|
173
|
-
pass
|
174
|
-
|
175
|
-
|
176
|
-
def get_windows_info():
|
177
|
-
""" 得到当前机器的全部窗口信息清单 """
|
178
|
-
window_list = []
|
179
|
-
|
180
|
-
def get_all_hwnd(hwnd, mouse):
|
181
|
-
thread_id, process_id = win32process.GetWindowThreadProcessId(hwnd)
|
182
|
-
proc = psutil.Process(process_id)
|
183
|
-
|
184
|
-
is_window = win32gui.IsWindow(hwnd)
|
185
|
-
is_enabled = win32gui.IsWindowEnabled(hwnd)
|
186
|
-
is_visible = win32gui.IsWindowVisible(hwnd)
|
187
|
-
text = win32gui.GetWindowText(hwnd)
|
188
|
-
|
189
|
-
data = {
|
190
|
-
'proc_name': proc.name(),
|
191
|
-
'process_id': process_id,
|
192
|
-
'thread_id': thread_id,
|
193
|
-
'hwnd': hwnd,
|
194
|
-
'ClassName': win32gui.GetClassName(hwnd),
|
195
|
-
'ControlTypeName': '',
|
196
|
-
'WindowText': text,
|
197
|
-
'IsWindow': is_window,
|
198
|
-
'IsWindowEnabled': is_enabled,
|
199
|
-
'IsWindowVisible': is_visible
|
200
|
-
}
|
201
|
-
|
202
|
-
if not data['proc_name'].endswith('.tmp') and is_visible:
|
203
|
-
ctrl = uia.ControlFromHandle(hwnd)
|
204
|
-
data['ControlTypeName'] = ctrl.ControlTypeName
|
205
|
-
|
206
|
-
window_list.append(data)
|
207
|
-
|
208
|
-
win32gui.EnumWindows(get_all_hwnd, 0)
|
209
|
-
return pd.DataFrame(window_list)
|
210
|
-
|
211
|
-
|
212
|
-
def find_ctrl(class_name=None, name=None, **kwargs):
|
213
|
-
if class_name is not None:
|
214
|
-
kwargs['ClassName'] = class_name
|
215
|
-
if name is not None:
|
216
|
-
kwargs['Name'] = name
|
217
|
-
ctrl = uia.WindowControl(**kwargs)
|
218
|
-
return ctrl
|
219
|
-
|
220
|
-
|
221
|
-
class UiCtrlNode(NodeMixin, GetAttr, WindowControl):
|
222
|
-
_default = 'ctrl'
|
223
|
-
|
224
|
-
def __0_构建(self):
|
225
|
-
pass
|
226
|
-
|
227
|
-
def __init__(self, ctrl, parent=None, *, build_depth=-1):
|
228
|
-
"""
|
229
|
-
:param ctrl: 当前节点
|
230
|
-
:param parent: 父结点
|
231
|
-
:param build_depth: 自动构建多少层树节点,默认-1表示构建全部节点
|
232
|
-
"""
|
233
|
-
# 初始化节点信息
|
234
|
-
self.ctrl = ctrl
|
235
|
-
# 试过了,没用,因为新找出来的都是新构建的类,找不到proxy的
|
236
|
-
# self.ctrl.proxy: 'UiCtrlNode' = self # 再给其扩展一个proxy属性,指向其升级过的对象
|
237
|
-
self.ctrl_type = ctrl.ControlTypeName
|
238
|
-
self.text = ctrl.Name
|
239
|
-
self.parent = parent # 指定父节点,用于形成树结构
|
240
|
-
|
241
|
-
# 自动递归创建子节点
|
242
|
-
self.build_children(build_depth)
|
243
|
-
|
244
|
-
@classmethod
|
245
|
-
def init_from_name(cls, class_name=None, name=None, *, build_depth=-1, **kwargs):
|
246
|
-
ctrl = find_ctrl(class_name=class_name, name=name, **kwargs)
|
247
|
-
return cls(ctrl, build_depth=build_depth)
|
248
|
-
|
249
|
-
def activate(self, check_seconds=2):
|
250
|
-
""" 激活当前窗口
|
251
|
-
"""
|
252
|
-
while True:
|
253
|
-
# todo 这种限定情况并不严谨,有概率出现重复的~
|
254
|
-
hwnd = win32gui.FindWindow(self.ctrl.ClassName, self.text)
|
255
|
-
# logger.info(hwnd)
|
256
|
-
if not hwnd:
|
257
|
-
return
|
258
|
-
|
259
|
-
if win32gui.GetForegroundWindow() != hwnd:
|
260
|
-
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
|
261
|
-
|
262
|
-
try:
|
263
|
-
# 在这里执行SetForegroundWindow,只有程序的第1次运行有效,之后就会被很多全屏类的应用占用最前置,覆盖不了了
|
264
|
-
# 为了解决这问题,就只能暴力每次都新开一个程序来执行这个SetForegroundWindow操作
|
265
|
-
subprocess.run(
|
266
|
-
[sys.executable, "-c", f"import win32gui; win32gui.SetForegroundWindow({hwnd})"],
|
267
|
-
stdout=subprocess.PIPE,
|
268
|
-
)
|
269
|
-
except Exception as e:
|
270
|
-
pass
|
271
|
-
# 理论上并不需要等待,但加个等待,有助于稳定性检测,如果当前窗口在check_seconds秒内频繁切换,
|
272
|
-
# 使用activate虽然激活了,但并不安全,只有check_seconds秒内维持稳定在这个窗口,再进行下游任务会更好
|
273
|
-
time.sleep(check_seconds)
|
274
|
-
else:
|
275
|
-
break
|
276
|
-
|
277
|
-
def build_children(self, build_depth=-1, child_node_class=None):
|
278
|
-
""" 创建并添加子节点到树中 """
|
279
|
-
if build_depth == 0:
|
280
|
-
return
|
281
|
-
self.children = [] # 删除现有的所有子结点
|
282
|
-
child_node_class = child_node_class or self.__class__
|
283
|
-
for child_ctrl in self.ctrl.GetChildren():
|
284
|
-
child_node_class(child_ctrl, parent=self, build_depth=build_depth - 1)
|
285
|
-
|
286
|
-
def __1_调试(self):
|
287
|
-
pass
|
288
|
-
|
289
|
-
def trace_rect(self, duration_per_circle=2, num_circles=1):
|
290
|
-
""" 用鼠标勾画出当前组件的矩形位置区域 """
|
291
|
-
from pyxllib.autogui.autogui import UiTracePath
|
292
|
-
|
293
|
-
rect = self.ctrl.BoundingRectangle
|
294
|
-
ltrb = [rect.left, rect.top, rect.right, rect.bottom]
|
295
|
-
UiTracePath.from_ltrb(ltrb,
|
296
|
-
duration_per_circle=duration_per_circle,
|
297
|
-
num_circles=num_circles)
|
298
|
-
|
299
|
-
def __2_功能(self):
|
300
|
-
pass
|
301
|
-
|
302
|
-
def __getattr__(self, item):
|
303
|
-
# 尝试从self.ctrl中获取属性
|
304
|
-
return getattr(self.ctrl, item)
|
305
|
-
|
306
|
-
def __getitem__(self, index):
|
307
|
-
""" 通过索引直接访问子节点
|
308
|
-
|
309
|
-
ui操作经常要各种结构化的访问,加个这个简化引用方式
|
310
|
-
"""
|
311
|
-
try:
|
312
|
-
return self.children[index]
|
313
|
-
except IndexError: # 如果出现下标错误,需要自动重新刷新所有控件
|
314
|
-
# self.parent重建是不够的,但我也不知道为什么self.root重建后就可以了
|
315
|
-
# 我的理解是重建后self自己不是都不存在了?
|
316
|
-
self.root.build_children()
|
317
|
-
# 应该在有些情况下self.root重建还能继续使用,但有些特殊情况应该会炸
|
318
|
-
return self.children[index]
|
319
|
-
|
320
|
-
def get_ctrl_hash_tag(self, level=1):
|
321
|
-
""" 生成节点的哈希字符串,以反映子树结构,一般用来对节点做分类及映射到对应处理函数 """
|
322
|
-
# 当前节点的类型标识符
|
323
|
-
hash_strs = [f"{level}{self.ctrl_type[0].lower()}"]
|
324
|
-
# 遍历所有子节点,递归生成子节点的哈希值
|
325
|
-
for child in self.children:
|
326
|
-
hash_strs.append(f"{child.get_ctrl_hash_tag(level + 1)}")
|
327
|
-
return ''.join(hash_strs)
|
328
|
-
|
329
|
-
def __3_展示(self):
|
330
|
-
pass
|
331
|
-
|
332
|
-
def _format_text(self, text):
|
333
|
-
""" 将换行替换为空格的小工具方法 """
|
334
|
-
return text.replace('\n', ' ')
|
335
|
-
|
336
|
-
def __repr__(self):
|
337
|
-
""" 用于在打印节点时显示关键信息 """
|
338
|
-
return f"UiNode(ctrl_type={self.ctrl_type}, text={self._format_text(self.text)})"
|
339
|
-
|
340
|
-
def render_tree(self):
|
341
|
-
""" 展示以self为根节点的整体内容结构 """
|
342
|
-
# 1 渲染自身
|
343
|
-
line = [self.ctrl_type]
|
344
|
-
line.append(self._format_text(self.text))
|
345
|
-
|
346
|
-
# 加上控件的坐标信息
|
347
|
-
rect = self.ctrl.BoundingRectangle
|
348
|
-
line.append(f"[{rect.left}, {rect.top}, {rect.right}, {rect.bottom}]")
|
349
|
-
|
350
|
-
# 我的hash值
|
351
|
-
tag = self.get_ctrl_hash_tag()
|
352
|
-
if len(tag) <= 64:
|
353
|
-
line.append(tag)
|
354
|
-
|
355
|
-
lines = [' '.join(line)]
|
356
|
-
|
357
|
-
# 2 子结点情况
|
358
|
-
for child in self.children:
|
359
|
-
line = child.render_tree()
|
360
|
-
line = textwrap.indent(line, ' ')
|
361
|
-
lines.append(line)
|
362
|
-
return '\n'.join(lines)
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# @Author : 陈坤泽
|
4
|
+
# @Email : 877362867@qq.com
|
5
|
+
# @Date : 2024/11/05
|
6
|
+
|
7
|
+
"""
|
8
|
+
以uiautomation为核心的相关工具库
|
9
|
+
"""
|
10
|
+
|
11
|
+
import os
|
12
|
+
import sys
|
13
|
+
import textwrap
|
14
|
+
import time
|
15
|
+
from typing import Iterable, Callable, List
|
16
|
+
import subprocess
|
17
|
+
import tempfile
|
18
|
+
|
19
|
+
import psutil
|
20
|
+
import pandas as pd
|
21
|
+
from fastcore.basics import GetAttr
|
22
|
+
|
23
|
+
from loguru import logger
|
24
|
+
# ui组件大多是树形组织结构,auto库自带树形操作太弱。没有专业的树形结构库,能搞个毛线。
|
25
|
+
from anytree import NodeMixin
|
26
|
+
|
27
|
+
import ctypes
|
28
|
+
from ctypes import wintypes
|
29
|
+
|
30
|
+
if sys.platform == 'win32':
|
31
|
+
import win32con
|
32
|
+
import win32gui
|
33
|
+
import win32process
|
34
|
+
import win32clipboard
|
35
|
+
|
36
|
+
import uiautomation as uia
|
37
|
+
from uiautomation import WindowControl
|
38
|
+
|
39
|
+
|
40
|
+
def __1_clipboard_utils():
|
41
|
+
pass
|
42
|
+
|
43
|
+
|
44
|
+
def retry_on_failure(max_retries: int = 5):
|
45
|
+
"""
|
46
|
+
一个装饰器,用于在失败时重试执行被装饰的函数。
|
47
|
+
|
48
|
+
Args:
|
49
|
+
max_retries (int): 最大重试次数。
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
Callable: 包装后的函数。
|
53
|
+
"""
|
54
|
+
|
55
|
+
def decorator(func: Callable):
|
56
|
+
def wrapper(*args, **kwargs):
|
57
|
+
for attempt in range(max_retries):
|
58
|
+
try:
|
59
|
+
if func(*args, **kwargs):
|
60
|
+
return True
|
61
|
+
except Exception as e:
|
62
|
+
time.sleep(.05)
|
63
|
+
print(f"Attempt {attempt + 1} failed: {e}")
|
64
|
+
return False
|
65
|
+
|
66
|
+
return wrapper
|
67
|
+
|
68
|
+
return decorator
|
69
|
+
|
70
|
+
|
71
|
+
def set_clipboard_data(fmt: int, buf: ctypes.Array) -> bool:
|
72
|
+
"""
|
73
|
+
将数据设置到Windows剪切板中。
|
74
|
+
|
75
|
+
Args:
|
76
|
+
fmt (int): 数据格式,例如 win32clipboard.CF_HDROP。
|
77
|
+
buf (ctypes.Array): 要设置到剪切板的数据。
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
bool: 操作成功返回 True,否则返回 False。
|
81
|
+
"""
|
82
|
+
try:
|
83
|
+
win32clipboard.OpenClipboard()
|
84
|
+
win32clipboard.EmptyClipboard()
|
85
|
+
win32clipboard.SetClipboardData(fmt, buf)
|
86
|
+
return True
|
87
|
+
except Exception as e:
|
88
|
+
print(f"Error setting clipboard data: {e}")
|
89
|
+
return False
|
90
|
+
finally:
|
91
|
+
win32clipboard.CloseClipboard()
|
92
|
+
|
93
|
+
|
94
|
+
def get_clipboard_files() -> List[str]:
|
95
|
+
"""
|
96
|
+
获取剪切板中的文件路径列表。
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
List[str]: 包含剪切板中文件路径的列表,如果没有文件路径或操作失败,返回空列表。
|
100
|
+
"""
|
101
|
+
try:
|
102
|
+
win32clipboard.OpenClipboard()
|
103
|
+
if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_HDROP):
|
104
|
+
return list(win32clipboard.GetClipboardData(win32clipboard.CF_HDROP))
|
105
|
+
else:
|
106
|
+
return list()
|
107
|
+
finally:
|
108
|
+
win32clipboard.CloseClipboard()
|
109
|
+
|
110
|
+
|
111
|
+
@retry_on_failure(max_retries=5)
|
112
|
+
def validate_clipboard_files(file_paths: Iterable[str], fmt: int, buf: ctypes.Array) -> bool:
|
113
|
+
"""
|
114
|
+
验证剪切板中的文件路径是否与给定的文件路径一致。
|
115
|
+
|
116
|
+
Args:
|
117
|
+
file_paths (Iterable): 一个包含文件路径的可迭代对象,每个路径都是一个字符串。
|
118
|
+
fmt (int): 数据格式,例如 win32clipboard.CF_HDROP。
|
119
|
+
buf (ctypes.Array): 要验证的剪切板数据。
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
bool: 如果剪切板中的文件路径与给定的文件路径一致,则返回 True
|
123
|
+
|
124
|
+
Raises:
|
125
|
+
ValueError: 如果剪切板文件路径与给定文件路径不一致。
|
126
|
+
"""
|
127
|
+
# 设置文件到剪切板
|
128
|
+
set_clipboard_data(fmt, buf)
|
129
|
+
# 验证剪切板中的文件路径是否与给定的文件路径一致
|
130
|
+
if set(get_clipboard_files()) == set(file_paths):
|
131
|
+
return True
|
132
|
+
raise ValueError("剪切板文件路径不对哇!")
|
133
|
+
|
134
|
+
|
135
|
+
def copy_files_to_clipboard(file_paths: Iterable[str]) -> bool:
|
136
|
+
"""
|
137
|
+
将一系列文件路径复制到Windows剪切板。这允许用户在其他应用程序中,如文件资源管理器中粘贴这些文件。
|
138
|
+
|
139
|
+
Args:
|
140
|
+
file_paths (Iterable): 一个包含文件路径的可迭代对象,每个路径都是一个字符串。
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
bool: 如果成功将文件路径复制到剪切板,则返回 True,否则返回 False
|
144
|
+
"""
|
145
|
+
# 定义所需的 Windows 结构和函数
|
146
|
+
CF_HDROP = 15
|
147
|
+
|
148
|
+
class DROPFILES(ctypes.Structure):
|
149
|
+
_fields_ = [("pFiles", wintypes.DWORD),
|
150
|
+
("pt", wintypes.POINT),
|
151
|
+
("fNC", wintypes.BOOL),
|
152
|
+
("fWide", wintypes.BOOL)]
|
153
|
+
|
154
|
+
offset = ctypes.sizeof(DROPFILES)
|
155
|
+
length = sum(len(p) + 1 for p in file_paths) + 1
|
156
|
+
size = offset + length * ctypes.sizeof(wintypes.WCHAR)
|
157
|
+
buf = (ctypes.c_char * size)()
|
158
|
+
df = DROPFILES.from_buffer(buf)
|
159
|
+
df.pFiles, df.fWide = offset, True
|
160
|
+
for path in file_paths:
|
161
|
+
path = os.path.normpath(path)
|
162
|
+
array_t = ctypes.c_wchar * (len(path) + 1)
|
163
|
+
path_buf = array_t.from_buffer(buf, offset)
|
164
|
+
path_buf.value = path
|
165
|
+
offset += ctypes.sizeof(path_buf)
|
166
|
+
buf[offset:offset + ctypes.sizeof(wintypes.WCHAR)] = b'\0\0'
|
167
|
+
|
168
|
+
# 验证文件是否成功复制到剪切板
|
169
|
+
return validate_clipboard_files([os.path.normpath(file) for file in file_paths], CF_HDROP, buf=buf)
|
170
|
+
|
171
|
+
|
172
|
+
def __2_窗口功能():
|
173
|
+
pass
|
174
|
+
|
175
|
+
|
176
|
+
def get_windows_info():
|
177
|
+
""" 得到当前机器的全部窗口信息清单 """
|
178
|
+
window_list = []
|
179
|
+
|
180
|
+
def get_all_hwnd(hwnd, mouse):
|
181
|
+
thread_id, process_id = win32process.GetWindowThreadProcessId(hwnd)
|
182
|
+
proc = psutil.Process(process_id)
|
183
|
+
|
184
|
+
is_window = win32gui.IsWindow(hwnd)
|
185
|
+
is_enabled = win32gui.IsWindowEnabled(hwnd)
|
186
|
+
is_visible = win32gui.IsWindowVisible(hwnd)
|
187
|
+
text = win32gui.GetWindowText(hwnd)
|
188
|
+
|
189
|
+
data = {
|
190
|
+
'proc_name': proc.name(),
|
191
|
+
'process_id': process_id,
|
192
|
+
'thread_id': thread_id,
|
193
|
+
'hwnd': hwnd,
|
194
|
+
'ClassName': win32gui.GetClassName(hwnd),
|
195
|
+
'ControlTypeName': '',
|
196
|
+
'WindowText': text,
|
197
|
+
'IsWindow': is_window,
|
198
|
+
'IsWindowEnabled': is_enabled,
|
199
|
+
'IsWindowVisible': is_visible
|
200
|
+
}
|
201
|
+
|
202
|
+
if not data['proc_name'].endswith('.tmp') and is_visible:
|
203
|
+
ctrl = uia.ControlFromHandle(hwnd)
|
204
|
+
data['ControlTypeName'] = ctrl.ControlTypeName
|
205
|
+
|
206
|
+
window_list.append(data)
|
207
|
+
|
208
|
+
win32gui.EnumWindows(get_all_hwnd, 0)
|
209
|
+
return pd.DataFrame(window_list)
|
210
|
+
|
211
|
+
|
212
|
+
def find_ctrl(class_name=None, name=None, **kwargs):
|
213
|
+
if class_name is not None:
|
214
|
+
kwargs['ClassName'] = class_name
|
215
|
+
if name is not None:
|
216
|
+
kwargs['Name'] = name
|
217
|
+
ctrl = uia.WindowControl(**kwargs)
|
218
|
+
return ctrl
|
219
|
+
|
220
|
+
|
221
|
+
class UiCtrlNode(NodeMixin, GetAttr, WindowControl):
|
222
|
+
_default = 'ctrl'
|
223
|
+
|
224
|
+
def __0_构建(self):
|
225
|
+
pass
|
226
|
+
|
227
|
+
def __init__(self, ctrl, parent=None, *, build_depth=-1):
|
228
|
+
"""
|
229
|
+
:param ctrl: 当前节点
|
230
|
+
:param parent: 父结点
|
231
|
+
:param build_depth: 自动构建多少层树节点,默认-1表示构建全部节点
|
232
|
+
"""
|
233
|
+
# 初始化节点信息
|
234
|
+
self.ctrl = ctrl
|
235
|
+
# 试过了,没用,因为新找出来的都是新构建的类,找不到proxy的
|
236
|
+
# self.ctrl.proxy: 'UiCtrlNode' = self # 再给其扩展一个proxy属性,指向其升级过的对象
|
237
|
+
self.ctrl_type = ctrl.ControlTypeName
|
238
|
+
self.text = ctrl.Name
|
239
|
+
self.parent = parent # 指定父节点,用于形成树结构
|
240
|
+
|
241
|
+
# 自动递归创建子节点
|
242
|
+
self.build_children(build_depth)
|
243
|
+
|
244
|
+
@classmethod
|
245
|
+
def init_from_name(cls, class_name=None, name=None, *, build_depth=-1, **kwargs):
|
246
|
+
ctrl = find_ctrl(class_name=class_name, name=name, **kwargs)
|
247
|
+
return cls(ctrl, build_depth=build_depth)
|
248
|
+
|
249
|
+
def activate(self, check_seconds=2):
|
250
|
+
""" 激活当前窗口
|
251
|
+
"""
|
252
|
+
while True:
|
253
|
+
# todo 这种限定情况并不严谨,有概率出现重复的~
|
254
|
+
hwnd = win32gui.FindWindow(self.ctrl.ClassName, self.text)
|
255
|
+
# logger.info(hwnd)
|
256
|
+
if not hwnd:
|
257
|
+
return
|
258
|
+
|
259
|
+
if win32gui.GetForegroundWindow() != hwnd:
|
260
|
+
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
|
261
|
+
|
262
|
+
try:
|
263
|
+
# 在这里执行SetForegroundWindow,只有程序的第1次运行有效,之后就会被很多全屏类的应用占用最前置,覆盖不了了
|
264
|
+
# 为了解决这问题,就只能暴力每次都新开一个程序来执行这个SetForegroundWindow操作
|
265
|
+
subprocess.run(
|
266
|
+
[sys.executable, "-c", f"import win32gui; win32gui.SetForegroundWindow({hwnd})"],
|
267
|
+
stdout=subprocess.PIPE,
|
268
|
+
)
|
269
|
+
except Exception as e:
|
270
|
+
pass
|
271
|
+
# 理论上并不需要等待,但加个等待,有助于稳定性检测,如果当前窗口在check_seconds秒内频繁切换,
|
272
|
+
# 使用activate虽然激活了,但并不安全,只有check_seconds秒内维持稳定在这个窗口,再进行下游任务会更好
|
273
|
+
time.sleep(check_seconds)
|
274
|
+
else:
|
275
|
+
break
|
276
|
+
|
277
|
+
def build_children(self, build_depth=-1, child_node_class=None):
|
278
|
+
""" 创建并添加子节点到树中 """
|
279
|
+
if build_depth == 0:
|
280
|
+
return
|
281
|
+
self.children = [] # 删除现有的所有子结点
|
282
|
+
child_node_class = child_node_class or self.__class__
|
283
|
+
for child_ctrl in self.ctrl.GetChildren():
|
284
|
+
child_node_class(child_ctrl, parent=self, build_depth=build_depth - 1)
|
285
|
+
|
286
|
+
def __1_调试(self):
|
287
|
+
pass
|
288
|
+
|
289
|
+
def trace_rect(self, duration_per_circle=2, num_circles=1):
|
290
|
+
""" 用鼠标勾画出当前组件的矩形位置区域 """
|
291
|
+
from pyxllib.autogui.autogui import UiTracePath
|
292
|
+
|
293
|
+
rect = self.ctrl.BoundingRectangle
|
294
|
+
ltrb = [rect.left, rect.top, rect.right, rect.bottom]
|
295
|
+
UiTracePath.from_ltrb(ltrb,
|
296
|
+
duration_per_circle=duration_per_circle,
|
297
|
+
num_circles=num_circles)
|
298
|
+
|
299
|
+
def __2_功能(self):
|
300
|
+
pass
|
301
|
+
|
302
|
+
def __getattr__(self, item):
|
303
|
+
# 尝试从self.ctrl中获取属性
|
304
|
+
return getattr(self.ctrl, item)
|
305
|
+
|
306
|
+
def __getitem__(self, index):
|
307
|
+
""" 通过索引直接访问子节点
|
308
|
+
|
309
|
+
ui操作经常要各种结构化的访问,加个这个简化引用方式
|
310
|
+
"""
|
311
|
+
try:
|
312
|
+
return self.children[index]
|
313
|
+
except IndexError: # 如果出现下标错误,需要自动重新刷新所有控件
|
314
|
+
# self.parent重建是不够的,但我也不知道为什么self.root重建后就可以了
|
315
|
+
# 我的理解是重建后self自己不是都不存在了?
|
316
|
+
self.root.build_children()
|
317
|
+
# 应该在有些情况下self.root重建还能继续使用,但有些特殊情况应该会炸
|
318
|
+
return self.children[index]
|
319
|
+
|
320
|
+
def get_ctrl_hash_tag(self, level=1):
|
321
|
+
""" 生成节点的哈希字符串,以反映子树结构,一般用来对节点做分类及映射到对应处理函数 """
|
322
|
+
# 当前节点的类型标识符
|
323
|
+
hash_strs = [f"{level}{self.ctrl_type[0].lower()}"]
|
324
|
+
# 遍历所有子节点,递归生成子节点的哈希值
|
325
|
+
for child in self.children:
|
326
|
+
hash_strs.append(f"{child.get_ctrl_hash_tag(level + 1)}")
|
327
|
+
return ''.join(hash_strs)
|
328
|
+
|
329
|
+
def __3_展示(self):
|
330
|
+
pass
|
331
|
+
|
332
|
+
def _format_text(self, text):
|
333
|
+
""" 将换行替换为空格的小工具方法 """
|
334
|
+
return text.replace('\n', ' ')
|
335
|
+
|
336
|
+
def __repr__(self):
|
337
|
+
""" 用于在打印节点时显示关键信息 """
|
338
|
+
return f"UiNode(ctrl_type={self.ctrl_type}, text={self._format_text(self.text)})"
|
339
|
+
|
340
|
+
def render_tree(self):
|
341
|
+
""" 展示以self为根节点的整体内容结构 """
|
342
|
+
# 1 渲染自身
|
343
|
+
line = [self.ctrl_type]
|
344
|
+
line.append(self._format_text(self.text))
|
345
|
+
|
346
|
+
# 加上控件的坐标信息
|
347
|
+
rect = self.ctrl.BoundingRectangle
|
348
|
+
line.append(f"[{rect.left}, {rect.top}, {rect.right}, {rect.bottom}]")
|
349
|
+
|
350
|
+
# 我的hash值
|
351
|
+
tag = self.get_ctrl_hash_tag()
|
352
|
+
if len(tag) <= 64:
|
353
|
+
line.append(tag)
|
354
|
+
|
355
|
+
lines = [' '.join(line)]
|
356
|
+
|
357
|
+
# 2 子结点情况
|
358
|
+
for child in self.children:
|
359
|
+
line = child.render_tree()
|
360
|
+
line = textwrap.indent(line, ' ')
|
361
|
+
lines.append(line)
|
362
|
+
return '\n'.join(lines)
|