roblox-studio-physical-operation-mcp 0.1.0__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.
- roblox_studio_physical_operation_mcp/__init__.py +13 -0
- roblox_studio_physical_operation_mcp/__main__.py +8 -0
- roblox_studio_physical_operation_mcp/log_filter.py +99 -0
- roblox_studio_physical_operation_mcp/log_utils.py +467 -0
- roblox_studio_physical_operation_mcp/server.py +602 -0
- roblox_studio_physical_operation_mcp/studio_manager.py +476 -0
- roblox_studio_physical_operation_mcp/toolbar_detector.py +513 -0
- roblox_studio_physical_operation_mcp/windows_utils.py +578 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/METADATA +273 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/RECORD +13 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/WHEEL +4 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Windows 工具模块: 窗口查找、按键发送、截图等
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
from ctypes import wintypes, Structure, Union, POINTER
|
|
7
|
+
import time
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import winreg
|
|
10
|
+
|
|
11
|
+
# 设置 DPI 感知 (必须在任何 GUI 操作之前调用)
|
|
12
|
+
# PROCESS_PER_MONITOR_DPI_AWARE = 2
|
|
13
|
+
try:
|
|
14
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
|
15
|
+
except Exception:
|
|
16
|
+
# Windows 8.1 之前的系统使用旧 API
|
|
17
|
+
try:
|
|
18
|
+
ctypes.windll.user32.SetProcessDPIAware()
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
# Windows API
|
|
23
|
+
user32 = ctypes.windll.user32
|
|
24
|
+
|
|
25
|
+
# SendInput 常量
|
|
26
|
+
INPUT_KEYBOARD = 1
|
|
27
|
+
KEYEVENTF_KEYUP = 0x0002
|
|
28
|
+
VK_F5 = 0x74
|
|
29
|
+
VK_F12 = 0x7B
|
|
30
|
+
VK_SHIFT = 0x10
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# SendInput 结构体
|
|
34
|
+
class KEYBDINPUT(Structure):
|
|
35
|
+
_fields_ = [
|
|
36
|
+
("wVk", wintypes.WORD),
|
|
37
|
+
("wScan", wintypes.WORD),
|
|
38
|
+
("dwFlags", wintypes.DWORD),
|
|
39
|
+
("time", wintypes.DWORD),
|
|
40
|
+
("dwExtraInfo", POINTER(ctypes.c_ulong))
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MOUSEINPUT(Structure):
|
|
45
|
+
_fields_ = [
|
|
46
|
+
("dx", wintypes.LONG),
|
|
47
|
+
("dy", wintypes.LONG),
|
|
48
|
+
("mouseData", wintypes.DWORD),
|
|
49
|
+
("dwFlags", wintypes.DWORD),
|
|
50
|
+
("time", wintypes.DWORD),
|
|
51
|
+
("dwExtraInfo", POINTER(ctypes.c_ulong))
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class HARDWAREINPUT(Structure):
|
|
56
|
+
_fields_ = [
|
|
57
|
+
("uMsg", wintypes.DWORD),
|
|
58
|
+
("wParamL", wintypes.WORD),
|
|
59
|
+
("wParamH", wintypes.WORD)
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class INPUT_UNION(Union):
|
|
64
|
+
_fields_ = [
|
|
65
|
+
("ki", KEYBDINPUT),
|
|
66
|
+
("mi", MOUSEINPUT),
|
|
67
|
+
("hi", HARDWAREINPUT)
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class INPUT(Structure):
|
|
72
|
+
_fields_ = [
|
|
73
|
+
("type", wintypes.DWORD),
|
|
74
|
+
("union", INPUT_UNION)
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_studio_path() -> Optional[str]:
|
|
79
|
+
"""从注册表获取 Roblox Studio 路径"""
|
|
80
|
+
try:
|
|
81
|
+
key = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r"roblox-studio\shell\open\command")
|
|
82
|
+
value, _ = winreg.QueryValueEx(key, "")
|
|
83
|
+
winreg.CloseKey(key)
|
|
84
|
+
# 值格式: "C:\...\RobloxStudioBeta.exe" %1
|
|
85
|
+
# 提取路径
|
|
86
|
+
if value.startswith('"'):
|
|
87
|
+
return value.split('"')[1]
|
|
88
|
+
return value.split()[0]
|
|
89
|
+
except FileNotFoundError:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_window_valid(hwnd: int) -> bool:
|
|
94
|
+
"""检查窗口句柄是否有效"""
|
|
95
|
+
return bool(user32.IsWindow(hwnd))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def find_window_by_title(title_contains: str) -> Optional[int]:
|
|
99
|
+
"""通过标题查找窗口"""
|
|
100
|
+
result = []
|
|
101
|
+
EnumWindowsProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
|
102
|
+
|
|
103
|
+
def enum_callback(hwnd, lparam):
|
|
104
|
+
if user32.IsWindowVisible(hwnd):
|
|
105
|
+
length = user32.GetWindowTextLengthW(hwnd)
|
|
106
|
+
if length > 0:
|
|
107
|
+
buffer = ctypes.create_unicode_buffer(length + 1)
|
|
108
|
+
user32.GetWindowTextW(hwnd, buffer, length + 1)
|
|
109
|
+
if title_contains in buffer.value:
|
|
110
|
+
result.append(hwnd)
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
user32.EnumWindows(EnumWindowsProc(enum_callback), 0)
|
|
114
|
+
return result[0] if result else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def find_window_by_pid(pid: int) -> Optional[int]:
|
|
118
|
+
"""通过 PID 查找主窗口句柄"""
|
|
119
|
+
result = []
|
|
120
|
+
EnumWindowsProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
|
121
|
+
|
|
122
|
+
def enum_callback(hwnd, lparam):
|
|
123
|
+
if user32.IsWindowVisible(hwnd):
|
|
124
|
+
window_pid = wintypes.DWORD()
|
|
125
|
+
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(window_pid))
|
|
126
|
+
if window_pid.value == pid:
|
|
127
|
+
length = user32.GetWindowTextLengthW(hwnd)
|
|
128
|
+
if length > 0:
|
|
129
|
+
buffer = ctypes.create_unicode_buffer(length + 1)
|
|
130
|
+
user32.GetWindowTextW(hwnd, buffer, length + 1)
|
|
131
|
+
title = buffer.value
|
|
132
|
+
if "Roblox Studio" in title:
|
|
133
|
+
result.append(hwnd)
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
user32.EnumWindows(EnumWindowsProc(enum_callback), 0)
|
|
137
|
+
return result[0] if result else None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def send_key(vk_code: int) -> bool:
|
|
141
|
+
"""使用 SendInput 发送按键"""
|
|
142
|
+
inputs = (INPUT * 2)()
|
|
143
|
+
|
|
144
|
+
# Key down
|
|
145
|
+
inputs[0].type = INPUT_KEYBOARD
|
|
146
|
+
inputs[0].union.ki.wVk = vk_code
|
|
147
|
+
inputs[0].union.ki.wScan = 0
|
|
148
|
+
inputs[0].union.ki.dwFlags = 0
|
|
149
|
+
inputs[0].union.ki.time = 0
|
|
150
|
+
inputs[0].union.ki.dwExtraInfo = None
|
|
151
|
+
|
|
152
|
+
# Key up
|
|
153
|
+
inputs[1].type = INPUT_KEYBOARD
|
|
154
|
+
inputs[1].union.ki.wVk = vk_code
|
|
155
|
+
inputs[1].union.ki.wScan = 0
|
|
156
|
+
inputs[1].union.ki.dwFlags = KEYEVENTF_KEYUP
|
|
157
|
+
inputs[1].union.ki.time = 0
|
|
158
|
+
inputs[1].union.ki.dwExtraInfo = None
|
|
159
|
+
|
|
160
|
+
result = user32.SendInput(2, ctypes.byref(inputs), ctypes.sizeof(INPUT))
|
|
161
|
+
return result == 2
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def send_key_combo(vk_codes: list[int]) -> bool:
|
|
165
|
+
"""发送组合键 (如 Shift+F5)"""
|
|
166
|
+
count = len(vk_codes) * 2
|
|
167
|
+
inputs = (INPUT * count)()
|
|
168
|
+
|
|
169
|
+
# 所有键按下
|
|
170
|
+
for i, vk in enumerate(vk_codes):
|
|
171
|
+
inputs[i].type = INPUT_KEYBOARD
|
|
172
|
+
inputs[i].union.ki.wVk = vk
|
|
173
|
+
inputs[i].union.ki.dwFlags = 0
|
|
174
|
+
|
|
175
|
+
# 所有键释放 (逆序)
|
|
176
|
+
for i, vk in enumerate(reversed(vk_codes)):
|
|
177
|
+
idx = len(vk_codes) + i
|
|
178
|
+
inputs[idx].type = INPUT_KEYBOARD
|
|
179
|
+
inputs[idx].union.ki.wVk = vk
|
|
180
|
+
inputs[idx].union.ki.dwFlags = KEYEVENTF_KEYUP
|
|
181
|
+
|
|
182
|
+
result = user32.SendInput(count, ctypes.byref(inputs), ctypes.sizeof(INPUT))
|
|
183
|
+
return result == count
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def send_key_to_window(hwnd: int, vk_code: int) -> bool:
|
|
187
|
+
"""将窗口置于前台并发送按键"""
|
|
188
|
+
original_hwnd = user32.GetForegroundWindow()
|
|
189
|
+
|
|
190
|
+
# 尝试将目标窗口置于前台
|
|
191
|
+
user32.SetForegroundWindow(hwnd)
|
|
192
|
+
time.sleep(0.1)
|
|
193
|
+
|
|
194
|
+
# 检查是否成功,使用 Alt 键技巧
|
|
195
|
+
if user32.GetForegroundWindow() != hwnd:
|
|
196
|
+
user32.keybd_event(0x12, 0, 0, 0) # Alt down
|
|
197
|
+
user32.SetForegroundWindow(hwnd)
|
|
198
|
+
user32.keybd_event(0x12, 0, 2, 0) # Alt up
|
|
199
|
+
time.sleep(0.1)
|
|
200
|
+
|
|
201
|
+
# 发送按键
|
|
202
|
+
success = send_key(vk_code)
|
|
203
|
+
time.sleep(0.2)
|
|
204
|
+
|
|
205
|
+
# 恢复原窗口
|
|
206
|
+
if original_hwnd and original_hwnd != hwnd:
|
|
207
|
+
user32.SetForegroundWindow(original_hwnd)
|
|
208
|
+
|
|
209
|
+
return success
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def send_key_combo_to_window(hwnd: int, vk_codes: list[int]) -> bool:
|
|
213
|
+
"""将窗口置于前台并发送组合键"""
|
|
214
|
+
original_hwnd = user32.GetForegroundWindow()
|
|
215
|
+
|
|
216
|
+
user32.SetForegroundWindow(hwnd)
|
|
217
|
+
time.sleep(0.1)
|
|
218
|
+
|
|
219
|
+
if user32.GetForegroundWindow() != hwnd:
|
|
220
|
+
user32.keybd_event(0x12, 0, 0, 0)
|
|
221
|
+
user32.SetForegroundWindow(hwnd)
|
|
222
|
+
user32.keybd_event(0x12, 0, 2, 0)
|
|
223
|
+
time.sleep(0.1)
|
|
224
|
+
|
|
225
|
+
success = send_key_combo(vk_codes)
|
|
226
|
+
time.sleep(0.2)
|
|
227
|
+
|
|
228
|
+
if original_hwnd and original_hwnd != hwnd:
|
|
229
|
+
user32.SetForegroundWindow(original_hwnd)
|
|
230
|
+
|
|
231
|
+
return success
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def capture_window(hwnd: int, output_path: str) -> bool:
|
|
235
|
+
"""使用 PrintWindow 截取窗口"""
|
|
236
|
+
try:
|
|
237
|
+
import win32gui
|
|
238
|
+
import win32ui
|
|
239
|
+
from PIL import Image
|
|
240
|
+
|
|
241
|
+
rect = win32gui.GetWindowRect(hwnd)
|
|
242
|
+
width = rect[2] - rect[0]
|
|
243
|
+
height = rect[3] - rect[1]
|
|
244
|
+
|
|
245
|
+
hwnd_dc = win32gui.GetWindowDC(hwnd)
|
|
246
|
+
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
|
|
247
|
+
save_dc = mfc_dc.CreateCompatibleDC()
|
|
248
|
+
|
|
249
|
+
bitmap = win32ui.CreateBitmap()
|
|
250
|
+
bitmap.CreateCompatibleBitmap(mfc_dc, width, height)
|
|
251
|
+
save_dc.SelectObject(bitmap)
|
|
252
|
+
|
|
253
|
+
# PrintWindow with PW_RENDERFULLCONTENT flag
|
|
254
|
+
ctypes.windll.user32.PrintWindow(hwnd, save_dc.GetSafeHdc(), 2)
|
|
255
|
+
|
|
256
|
+
bmpinfo = bitmap.GetInfo()
|
|
257
|
+
bmpstr = bitmap.GetBitmapBits(True)
|
|
258
|
+
img = Image.frombuffer('RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']),
|
|
259
|
+
bmpstr, 'raw', 'BGRX', 0, 1)
|
|
260
|
+
img.save(output_path)
|
|
261
|
+
|
|
262
|
+
win32gui.DeleteObject(bitmap.GetHandle())
|
|
263
|
+
save_dc.DeleteDC()
|
|
264
|
+
mfc_dc.DeleteDC()
|
|
265
|
+
win32gui.ReleaseDC(hwnd, hwnd_dc)
|
|
266
|
+
|
|
267
|
+
return True
|
|
268
|
+
except Exception:
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def click_at(hwnd: int, x: int, y: int, restore_focus: bool = True) -> bool:
|
|
273
|
+
"""
|
|
274
|
+
在窗口的指定坐标点击
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
hwnd: 窗口句柄
|
|
278
|
+
x: 相对于窗口左上角的 X 坐标
|
|
279
|
+
y: 相对于窗口左上角的 Y 坐标
|
|
280
|
+
restore_focus: 是否恢复原窗口焦点
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
是否成功
|
|
284
|
+
"""
|
|
285
|
+
try:
|
|
286
|
+
import win32gui
|
|
287
|
+
import win32api
|
|
288
|
+
import win32con
|
|
289
|
+
|
|
290
|
+
# 保存当前前台窗口
|
|
291
|
+
original_hwnd = user32.GetForegroundWindow() if restore_focus else None
|
|
292
|
+
|
|
293
|
+
# 获取窗口位置
|
|
294
|
+
rect = win32gui.GetWindowRect(hwnd)
|
|
295
|
+
abs_x = rect[0] + x
|
|
296
|
+
abs_y = rect[1] + y
|
|
297
|
+
|
|
298
|
+
# 将窗口置于前台
|
|
299
|
+
user32.SetForegroundWindow(hwnd)
|
|
300
|
+
time.sleep(0.1)
|
|
301
|
+
|
|
302
|
+
if user32.GetForegroundWindow() != hwnd:
|
|
303
|
+
user32.keybd_event(0x12, 0, 0, 0) # Alt down
|
|
304
|
+
user32.SetForegroundWindow(hwnd)
|
|
305
|
+
user32.keybd_event(0x12, 0, 2, 0) # Alt up
|
|
306
|
+
time.sleep(0.1)
|
|
307
|
+
|
|
308
|
+
# 移动鼠标并点击
|
|
309
|
+
win32api.SetCursorPos((abs_x, abs_y))
|
|
310
|
+
time.sleep(0.05)
|
|
311
|
+
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
|
|
312
|
+
time.sleep(0.05)
|
|
313
|
+
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
|
|
314
|
+
|
|
315
|
+
time.sleep(0.1)
|
|
316
|
+
|
|
317
|
+
# 恢复原窗口
|
|
318
|
+
if original_hwnd and original_hwnd != hwnd:
|
|
319
|
+
user32.SetForegroundWindow(original_hwnd)
|
|
320
|
+
|
|
321
|
+
return True
|
|
322
|
+
except Exception:
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def right_click_at(hwnd: int, x: int, y: int, restore_focus: bool = True) -> bool:
|
|
327
|
+
"""
|
|
328
|
+
在窗口的指定坐标右键点击
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
hwnd: 窗口句柄
|
|
332
|
+
x: 相对于窗口左上角的 X 坐标
|
|
333
|
+
y: 相对于窗口左上角的 Y 坐标
|
|
334
|
+
restore_focus: 是否恢复原窗口焦点
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
是否成功
|
|
338
|
+
"""
|
|
339
|
+
try:
|
|
340
|
+
import win32gui
|
|
341
|
+
import win32api
|
|
342
|
+
import win32con
|
|
343
|
+
|
|
344
|
+
original_hwnd = user32.GetForegroundWindow() if restore_focus else None
|
|
345
|
+
|
|
346
|
+
rect = win32gui.GetWindowRect(hwnd)
|
|
347
|
+
abs_x = rect[0] + x
|
|
348
|
+
abs_y = rect[1] + y
|
|
349
|
+
|
|
350
|
+
user32.SetForegroundWindow(hwnd)
|
|
351
|
+
time.sleep(0.1)
|
|
352
|
+
|
|
353
|
+
if user32.GetForegroundWindow() != hwnd:
|
|
354
|
+
user32.keybd_event(0x12, 0, 0, 0)
|
|
355
|
+
user32.SetForegroundWindow(hwnd)
|
|
356
|
+
user32.keybd_event(0x12, 0, 2, 0)
|
|
357
|
+
time.sleep(0.1)
|
|
358
|
+
|
|
359
|
+
win32api.SetCursorPos((abs_x, abs_y))
|
|
360
|
+
time.sleep(0.05)
|
|
361
|
+
win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0)
|
|
362
|
+
time.sleep(0.05)
|
|
363
|
+
win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0)
|
|
364
|
+
|
|
365
|
+
time.sleep(0.1)
|
|
366
|
+
|
|
367
|
+
if original_hwnd and original_hwnd != hwnd:
|
|
368
|
+
user32.SetForegroundWindow(original_hwnd)
|
|
369
|
+
|
|
370
|
+
return True
|
|
371
|
+
except Exception:
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def double_click_at(hwnd: int, x: int, y: int, restore_focus: bool = True) -> bool:
|
|
376
|
+
"""
|
|
377
|
+
在窗口的指定坐标双击
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
hwnd: 窗口句柄
|
|
381
|
+
x: 相对于窗口左上角的 X 坐标
|
|
382
|
+
y: 相对于窗口左上角的 Y 坐标
|
|
383
|
+
restore_focus: 是否恢复原窗口焦点
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
是否成功
|
|
387
|
+
"""
|
|
388
|
+
try:
|
|
389
|
+
import win32gui
|
|
390
|
+
import win32api
|
|
391
|
+
import win32con
|
|
392
|
+
|
|
393
|
+
original_hwnd = user32.GetForegroundWindow() if restore_focus else None
|
|
394
|
+
|
|
395
|
+
rect = win32gui.GetWindowRect(hwnd)
|
|
396
|
+
abs_x = rect[0] + x
|
|
397
|
+
abs_y = rect[1] + y
|
|
398
|
+
|
|
399
|
+
user32.SetForegroundWindow(hwnd)
|
|
400
|
+
time.sleep(0.1)
|
|
401
|
+
|
|
402
|
+
if user32.GetForegroundWindow() != hwnd:
|
|
403
|
+
user32.keybd_event(0x12, 0, 0, 0)
|
|
404
|
+
user32.SetForegroundWindow(hwnd)
|
|
405
|
+
user32.keybd_event(0x12, 0, 2, 0)
|
|
406
|
+
time.sleep(0.1)
|
|
407
|
+
|
|
408
|
+
win32api.SetCursorPos((abs_x, abs_y))
|
|
409
|
+
time.sleep(0.05)
|
|
410
|
+
|
|
411
|
+
# 双击
|
|
412
|
+
for _ in range(2):
|
|
413
|
+
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
|
|
414
|
+
time.sleep(0.02)
|
|
415
|
+
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
|
|
416
|
+
time.sleep(0.05)
|
|
417
|
+
|
|
418
|
+
time.sleep(0.1)
|
|
419
|
+
|
|
420
|
+
if original_hwnd and original_hwnd != hwnd:
|
|
421
|
+
user32.SetForegroundWindow(original_hwnd)
|
|
422
|
+
|
|
423
|
+
return True
|
|
424
|
+
except Exception:
|
|
425
|
+
return False
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def find_all_windows_by_pid(pid: int) -> list[dict]:
|
|
429
|
+
"""
|
|
430
|
+
查找指定进程的所有可见窗口
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
pid: 进程 ID
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
窗口信息列表,每个元素包含 hwnd, title, rect
|
|
437
|
+
"""
|
|
438
|
+
windows = []
|
|
439
|
+
EnumWindowsProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
|
440
|
+
|
|
441
|
+
def enum_callback(hwnd, lparam):
|
|
442
|
+
if user32.IsWindowVisible(hwnd):
|
|
443
|
+
window_pid = wintypes.DWORD()
|
|
444
|
+
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(window_pid))
|
|
445
|
+
if window_pid.value == pid:
|
|
446
|
+
# 获取窗口标题
|
|
447
|
+
length = user32.GetWindowTextLengthW(hwnd)
|
|
448
|
+
title = ""
|
|
449
|
+
if length > 0:
|
|
450
|
+
buffer = ctypes.create_unicode_buffer(length + 1)
|
|
451
|
+
user32.GetWindowTextW(hwnd, buffer, length + 1)
|
|
452
|
+
title = buffer.value
|
|
453
|
+
|
|
454
|
+
# 获取窗口位置
|
|
455
|
+
import win32gui
|
|
456
|
+
try:
|
|
457
|
+
rect = win32gui.GetWindowRect(hwnd)
|
|
458
|
+
# 过滤掉无效窗口 (宽高为0)
|
|
459
|
+
width = rect[2] - rect[0]
|
|
460
|
+
height = rect[3] - rect[1]
|
|
461
|
+
if width > 0 and height > 0:
|
|
462
|
+
windows.append({
|
|
463
|
+
'hwnd': hwnd,
|
|
464
|
+
'title': title,
|
|
465
|
+
'rect': rect,
|
|
466
|
+
'width': width,
|
|
467
|
+
'height': height
|
|
468
|
+
})
|
|
469
|
+
except Exception:
|
|
470
|
+
pass
|
|
471
|
+
return True
|
|
472
|
+
|
|
473
|
+
user32.EnumWindows(EnumWindowsProc(enum_callback), 0)
|
|
474
|
+
return windows
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def capture_window_with_modals(hwnd: int, pid: int, output_path: str) -> tuple[bool, list[dict]]:
|
|
478
|
+
"""
|
|
479
|
+
截取窗口,优先截取模态弹窗
|
|
480
|
+
|
|
481
|
+
通过查找同一进程的所有窗口,如果存在模态弹窗(非主窗口),
|
|
482
|
+
则只截取模态弹窗;否则截取主窗口。
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
hwnd: 主窗口句柄
|
|
486
|
+
pid: 进程 ID
|
|
487
|
+
output_path: 输出图片路径
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
(success, windows_info) - 是否成功,以及捕获到的窗口信息列表
|
|
491
|
+
"""
|
|
492
|
+
try:
|
|
493
|
+
# 查找所有同进程窗口
|
|
494
|
+
all_windows = find_all_windows_by_pid(pid)
|
|
495
|
+
|
|
496
|
+
if not all_windows:
|
|
497
|
+
# 如果没找到窗口,回退到普通截图
|
|
498
|
+
success = capture_window(hwnd, output_path)
|
|
499
|
+
return success, []
|
|
500
|
+
|
|
501
|
+
# 找出模态弹窗(非主窗口的其他窗口)
|
|
502
|
+
modal_windows = [w for w in all_windows if w['hwnd'] != hwnd]
|
|
503
|
+
|
|
504
|
+
if modal_windows:
|
|
505
|
+
# 有模态弹窗,截取第一个模态弹窗(通常是最上层的)
|
|
506
|
+
target_hwnd = modal_windows[0]['hwnd']
|
|
507
|
+
success = capture_window(target_hwnd, output_path)
|
|
508
|
+
return success, all_windows
|
|
509
|
+
else:
|
|
510
|
+
# 没有模态弹窗,截取主窗口
|
|
511
|
+
success = capture_window(hwnd, output_path)
|
|
512
|
+
return success, all_windows
|
|
513
|
+
|
|
514
|
+
except Exception as e:
|
|
515
|
+
return False, [{'error': str(e)}]
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def get_modal_windows(hwnd: int, pid: int) -> list[dict]:
|
|
519
|
+
"""
|
|
520
|
+
获取模态弹窗列表(排除主窗口和小的边框窗口)
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
hwnd: 主窗口句柄
|
|
524
|
+
pid: 进程 ID
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
模态弹窗信息列表
|
|
528
|
+
"""
|
|
529
|
+
all_windows = find_all_windows_by_pid(pid)
|
|
530
|
+
|
|
531
|
+
# 过滤:排除主窗口,排除小窗口(宽或高小于50的可能是边框)
|
|
532
|
+
modal_windows = [
|
|
533
|
+
w for w in all_windows
|
|
534
|
+
if w['hwnd'] != hwnd and w['width'] > 50 and w['height'] > 50
|
|
535
|
+
]
|
|
536
|
+
|
|
537
|
+
return modal_windows
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def close_modal_window(modal_hwnd: int) -> bool:
|
|
541
|
+
"""
|
|
542
|
+
关闭指定的模态弹窗
|
|
543
|
+
|
|
544
|
+
通过发送 WM_CLOSE 消息关闭窗口
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
modal_hwnd: 模态弹窗句柄
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
是否成功
|
|
551
|
+
"""
|
|
552
|
+
try:
|
|
553
|
+
WM_CLOSE = 0x0010
|
|
554
|
+
user32.PostMessageW(modal_hwnd, WM_CLOSE, 0, 0)
|
|
555
|
+
return True
|
|
556
|
+
except Exception:
|
|
557
|
+
return False
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def close_all_modals(hwnd: int, pid: int) -> tuple[int, list[str]]:
|
|
561
|
+
"""
|
|
562
|
+
关闭所有模态弹窗
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
hwnd: 主窗口句柄
|
|
566
|
+
pid: 进程 ID
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
(关闭数量, 关闭的窗口标题列表)
|
|
570
|
+
"""
|
|
571
|
+
modal_windows = get_modal_windows(hwnd, pid)
|
|
572
|
+
closed_titles = []
|
|
573
|
+
|
|
574
|
+
for modal in modal_windows:
|
|
575
|
+
if close_modal_window(modal['hwnd']):
|
|
576
|
+
closed_titles.append(modal['title'] or f"(hwnd: {modal['hwnd']})")
|
|
577
|
+
|
|
578
|
+
return len(closed_titles), closed_titles
|