windows-mcp 0.5.7__py3-none-any.whl → 0.5.8__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.
- windows_mcp/__main__.py +314 -312
- windows_mcp/analytics.py +175 -171
- windows_mcp/desktop/config.py +20 -20
- windows_mcp/desktop/service.py +457 -457
- windows_mcp/desktop/views.py +57 -57
- windows_mcp/tree/config.py +50 -50
- windows_mcp/tree/service.py +600 -466
- windows_mcp/tree/utils.py +21 -21
- windows_mcp/tree/views.py +115 -115
- windows_mcp/uia/__init__.py +4 -0
- windows_mcp/uia/controls.py +4781 -0
- windows_mcp/uia/core.py +3269 -0
- windows_mcp/uia/enums.py +1963 -0
- windows_mcp/uia/events.py +83 -0
- windows_mcp/uia/patterns.py +2106 -0
- windows_mcp/watchdog/__init__.py +1 -0
- windows_mcp/watchdog/event_handlers.py +51 -0
- windows_mcp/watchdog/service.py +188 -0
- {windows_mcp-0.5.7.dist-info → windows_mcp-0.5.8.dist-info}/METADATA +4 -4
- windows_mcp-0.5.8.dist-info/RECORD +26 -0
- windows_mcp-0.5.7.dist-info/RECORD +0 -17
- {windows_mcp-0.5.7.dist-info → windows_mcp-0.5.8.dist-info}/WHEEL +0 -0
- {windows_mcp-0.5.7.dist-info → windows_mcp-0.5.8.dist-info}/entry_points.txt +0 -0
- {windows_mcp-0.5.7.dist-info → windows_mcp-0.5.8.dist-info}/licenses/LICENSE.md +0 -0
windows_mcp/desktop/service.py
CHANGED
|
@@ -1,458 +1,458 @@
|
|
|
1
|
-
from windows_mcp.desktop.config import BROWSER_NAMES, PROCESS_PER_MONITOR_DPI_AWARE
|
|
2
|
-
from windows_mcp.desktop.views import DesktopState, App, Size, Status
|
|
3
|
-
from windows_mcp.tree.service import Tree
|
|
4
|
-
from locale import getpreferredencoding
|
|
5
|
-
from contextlib import contextmanager
|
|
6
|
-
from typing import Optional,Literal
|
|
7
|
-
from markdownify import markdownify
|
|
8
|
-
from fuzzywuzzy import process
|
|
9
|
-
from psutil import Process
|
|
10
|
-
from time import sleep
|
|
11
|
-
from PIL import Image
|
|
12
|
-
import win32process
|
|
13
|
-
import subprocess
|
|
14
|
-
import win32gui
|
|
15
|
-
import win32con
|
|
16
|
-
import requests
|
|
17
|
-
import logging
|
|
18
|
-
import base64
|
|
19
|
-
import ctypes
|
|
20
|
-
import csv
|
|
21
|
-
import re
|
|
22
|
-
import os
|
|
23
|
-
import io
|
|
24
|
-
|
|
25
|
-
logger = logging.getLogger(__name__)
|
|
26
|
-
logger.setLevel(logging.INFO)
|
|
27
|
-
handler = logging.StreamHandler()
|
|
28
|
-
formatter = logging.Formatter('[%(levelname)s] %(message)s')
|
|
29
|
-
handler.setFormatter(formatter)
|
|
30
|
-
logger.addHandler(handler)
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
ctypes.windll.shcore.SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE)
|
|
34
|
-
except Exception:
|
|
35
|
-
ctypes.windll.user32.SetProcessDPIAware()
|
|
36
|
-
|
|
37
|
-
import
|
|
38
|
-
import pyautogui as pg
|
|
39
|
-
|
|
40
|
-
pg.FAILSAFE=False
|
|
41
|
-
pg.PAUSE=1.0
|
|
42
|
-
|
|
43
|
-
class Desktop:
|
|
44
|
-
def __init__(self):
|
|
45
|
-
self.encoding=getpreferredencoding()
|
|
46
|
-
self.tree=Tree(self)
|
|
47
|
-
self.desktop_state=None
|
|
48
|
-
|
|
49
|
-
def get_resolution(self)->tuple[int,int]:
|
|
50
|
-
return pg.size()
|
|
51
|
-
|
|
52
|
-
def get_state(self,use_vision:bool=False,use_dom:bool=False,as_bytes:bool=False,scale:float=1.0)->DesktopState:
|
|
53
|
-
sleep(0.1)
|
|
54
|
-
apps=self.get_apps()
|
|
55
|
-
active_app=self.get_active_app()
|
|
56
|
-
if active_app is not None and active_app in apps:
|
|
57
|
-
apps.remove(active_app)
|
|
58
|
-
logger.debug(f"Active app: {active_app}")
|
|
59
|
-
logger.debug(f"Apps: {apps}")
|
|
60
|
-
tree_state=self.tree.get_state(active_app,apps,use_dom=use_dom)
|
|
61
|
-
if use_vision:
|
|
62
|
-
screenshot=self.tree.get_annotated_screenshot(tree_state.interactive_nodes,scale=scale)
|
|
63
|
-
if as_bytes:
|
|
64
|
-
bytes_io=io.BytesIO()
|
|
65
|
-
screenshot.save(bytes_io,format='PNG')
|
|
66
|
-
screenshot=bytes_io.getvalue()
|
|
67
|
-
else:
|
|
68
|
-
screenshot=None
|
|
69
|
-
self.desktop_state=DesktopState(apps= apps,active_app=active_app,screenshot=screenshot,tree_state=tree_state)
|
|
70
|
-
return self.desktop_state
|
|
71
|
-
|
|
72
|
-
def get_window_element_from_element(self,element:uia.Control)->uia.Control|None:
|
|
73
|
-
while element is not None:
|
|
74
|
-
if uia.IsTopLevelWindow(element.NativeWindowHandle):
|
|
75
|
-
return element
|
|
76
|
-
element = element.GetParentControl()
|
|
77
|
-
return None
|
|
78
|
-
|
|
79
|
-
def get_active_app(self)->App|None:
|
|
80
|
-
try:
|
|
81
|
-
handle=uia.GetForegroundWindow()
|
|
82
|
-
for app in self.get_apps():
|
|
83
|
-
if app.handle!=handle:
|
|
84
|
-
continue
|
|
85
|
-
return app
|
|
86
|
-
except Exception as ex:
|
|
87
|
-
logger.error(f"Error in get_active_app: {ex}")
|
|
88
|
-
return None
|
|
89
|
-
|
|
90
|
-
def get_app_status(self,control:uia.Control)->Status:
|
|
91
|
-
if uia.IsIconic(control.NativeWindowHandle):
|
|
92
|
-
return Status.MINIMIZED
|
|
93
|
-
elif uia.IsZoomed(control.NativeWindowHandle):
|
|
94
|
-
return Status.MAXIMIZED
|
|
95
|
-
elif uia.IsWindowVisible(control.NativeWindowHandle):
|
|
96
|
-
return Status.NORMAL
|
|
97
|
-
else:
|
|
98
|
-
return Status.HIDDEN
|
|
99
|
-
|
|
100
|
-
def get_cursor_location(self)->tuple[int,int]:
|
|
101
|
-
position=pg.position()
|
|
102
|
-
return (position.x,position.y)
|
|
103
|
-
|
|
104
|
-
def get_element_under_cursor(self)->uia.Control:
|
|
105
|
-
return uia.ControlFromCursor()
|
|
106
|
-
|
|
107
|
-
def get_apps_from_start_menu(self)->dict[str,str]:
|
|
108
|
-
command='Get-StartApps | ConvertTo-Csv -NoTypeInformation'
|
|
109
|
-
apps_info,_=self.execute_command(command)
|
|
110
|
-
reader=csv.DictReader(io.StringIO(apps_info))
|
|
111
|
-
return {row.get('Name').lower():row.get('AppID') for row in reader}
|
|
112
|
-
|
|
113
|
-
def execute_command(self,command:str)->tuple[str,int]:
|
|
114
|
-
try:
|
|
115
|
-
encoded = base64.b64encode(command.encode("utf-16le")).decode("ascii")
|
|
116
|
-
result = subprocess.run(
|
|
117
|
-
['powershell', '-NoProfile', '-EncodedCommand', encoded],
|
|
118
|
-
capture_output=True,
|
|
119
|
-
errors='ignore',
|
|
120
|
-
timeout=25,
|
|
121
|
-
cwd=os.path.expanduser(path='~')
|
|
122
|
-
)
|
|
123
|
-
stdout=result.stdout
|
|
124
|
-
stderr=result.stderr
|
|
125
|
-
return (stdout or stderr,result.returncode)
|
|
126
|
-
except subprocess.TimeoutExpired:
|
|
127
|
-
return ('Command execution timed out', 1)
|
|
128
|
-
except Exception as e:
|
|
129
|
-
return ('Command execution failed', 1)
|
|
130
|
-
|
|
131
|
-
def is_app_browser(self,node:uia.Control):
|
|
132
|
-
process=Process(node.ProcessId)
|
|
133
|
-
return process.name() in BROWSER_NAMES
|
|
134
|
-
|
|
135
|
-
def get_default_language(self)->str:
|
|
136
|
-
command="Get-Culture | Select-Object Name,DisplayName | ConvertTo-Csv -NoTypeInformation"
|
|
137
|
-
response,_=self.execute_command(command)
|
|
138
|
-
reader=csv.DictReader(io.StringIO(response))
|
|
139
|
-
return "".join([row.get('DisplayName') for row in reader])
|
|
140
|
-
|
|
141
|
-
def resize_app(self,size:tuple[int,int]=None,loc:tuple[int,int]=None)->tuple[str,int]:
|
|
142
|
-
active_app=self.desktop_state.active_app
|
|
143
|
-
if active_app is None:
|
|
144
|
-
return "No active app found",1
|
|
145
|
-
if active_app.status==Status.MINIMIZED:
|
|
146
|
-
return f"{active_app.name} is minimized",1
|
|
147
|
-
elif active_app.status==Status.MAXIMIZED:
|
|
148
|
-
return f"{active_app.name} is maximized",1
|
|
149
|
-
else:
|
|
150
|
-
app_control=uia.ControlFromHandle(active_app.handle)
|
|
151
|
-
if loc is None:
|
|
152
|
-
x=app_control.BoundingRectangle.left
|
|
153
|
-
y=app_control.BoundingRectangle.top
|
|
154
|
-
loc=(x,y)
|
|
155
|
-
if size is None:
|
|
156
|
-
width=app_control.BoundingRectangle.width()
|
|
157
|
-
height=app_control.BoundingRectangle.height()
|
|
158
|
-
size=(width,height)
|
|
159
|
-
x,y=loc
|
|
160
|
-
width,height=size
|
|
161
|
-
app_control.MoveWindow(x,y,width,height)
|
|
162
|
-
return (f'{active_app.name} resized to {width}x{height} at {x},{y}.',0)
|
|
163
|
-
|
|
164
|
-
def is_app_running(self,name:str)->bool:
|
|
165
|
-
apps={app.name:app for app in self.get_apps()}
|
|
166
|
-
return process.extractOne(name,list(apps.keys()),score_cutoff=60) is not None
|
|
167
|
-
|
|
168
|
-
def app(self,mode:Literal['launch','switch','resize'],name:Optional[str]=None,loc:Optional[tuple[int,int]]=None,size:Optional[tuple[int,int]]=None):
|
|
169
|
-
match mode:
|
|
170
|
-
case 'launch':
|
|
171
|
-
response,status=self.launch_app(name)
|
|
172
|
-
sleep(1.25)
|
|
173
|
-
if status!=0:
|
|
174
|
-
return response
|
|
175
|
-
consecutive_waits=10
|
|
176
|
-
for _ in range(consecutive_waits):
|
|
177
|
-
if not self.is_app_running(name):
|
|
178
|
-
sleep(1.25)
|
|
179
|
-
else:
|
|
180
|
-
return f'{name.title()} launched.'
|
|
181
|
-
return f'Launching {name.title()} wait for it to come load.'
|
|
182
|
-
case 'resize':
|
|
183
|
-
response,status=self.resize_app(size=size,loc=loc)
|
|
184
|
-
if status!=0:
|
|
185
|
-
return response
|
|
186
|
-
else:
|
|
187
|
-
return response
|
|
188
|
-
case 'switch':
|
|
189
|
-
response,status=self.switch_app(name)
|
|
190
|
-
if status!=0:
|
|
191
|
-
return response
|
|
192
|
-
else:
|
|
193
|
-
return response
|
|
194
|
-
|
|
195
|
-
def launch_app(self,name:str)->tuple[str,int]:
|
|
196
|
-
apps_map=self.get_apps_from_start_menu()
|
|
197
|
-
matched_app=process.extractOne(name,apps_map.keys(),score_cutoff=70)
|
|
198
|
-
if matched_app is None:
|
|
199
|
-
return (f'{name.title()} not found in start menu.',1)
|
|
200
|
-
app_name,_=matched_app
|
|
201
|
-
appid=apps_map.get(app_name)
|
|
202
|
-
if appid is None:
|
|
203
|
-
return (f'{name.title()} not found in start menu.',1)
|
|
204
|
-
if appid.endswith('.exe'):
|
|
205
|
-
command=f"Start-Process '{appid}'"
|
|
206
|
-
else:
|
|
207
|
-
command=f"Start-Process shell:AppsFolder\\{appid}"
|
|
208
|
-
response,status=self.execute_command(command)
|
|
209
|
-
return response,status
|
|
210
|
-
|
|
211
|
-
def switch_app(self,name:str):
|
|
212
|
-
apps={app.name:app for app in [self.desktop_state.active_app]+self.desktop_state.apps if app is not None}
|
|
213
|
-
matched_app:Optional[tuple[str,float]]=process.extractOne(name,list(apps.keys()),score_cutoff=70)
|
|
214
|
-
if matched_app is None:
|
|
215
|
-
return (f'Application {name.title()} not found.',1)
|
|
216
|
-
app_name,_=matched_app
|
|
217
|
-
app=apps.get(app_name)
|
|
218
|
-
target_handle=app.handle
|
|
219
|
-
|
|
220
|
-
if uia.IsIconic(target_handle):
|
|
221
|
-
uia.ShowWindow(target_handle, win32con.SW_RESTORE)
|
|
222
|
-
content=f'{app_name.title()} restored from Minimized state.'
|
|
223
|
-
else:
|
|
224
|
-
self.bring_window_to_top(target_handle)
|
|
225
|
-
content=f'Switched to {app_name.title()} window.'
|
|
226
|
-
return content,0
|
|
227
|
-
|
|
228
|
-
def bring_window_to_top(self,target_handle:int):
|
|
229
|
-
foreground_handle=win32gui.GetForegroundWindow()
|
|
230
|
-
foreground_thread,_=win32process.GetWindowThreadProcessId(foreground_handle)
|
|
231
|
-
target_thread,_=win32process.GetWindowThreadProcessId(target_handle)
|
|
232
|
-
try:
|
|
233
|
-
ctypes.windll.user32.AllowSetForegroundWindow(-1)
|
|
234
|
-
win32process.AttachThreadInput(foreground_thread,target_thread,True)
|
|
235
|
-
win32gui.SetForegroundWindow(target_handle)
|
|
236
|
-
win32gui.BringWindowToTop(target_handle)
|
|
237
|
-
except Exception as e:
|
|
238
|
-
logger.error(f'Failed to bring window to top: {e}')
|
|
239
|
-
finally:
|
|
240
|
-
win32process.AttachThreadInput(foreground_thread,target_thread,False)
|
|
241
|
-
|
|
242
|
-
def get_element_handle_from_label(self,label:int)->uia.Control:
|
|
243
|
-
tree_state=self.desktop_state.tree_state
|
|
244
|
-
element_node=tree_state.interactive_nodes[label]
|
|
245
|
-
xpath=element_node.xpath
|
|
246
|
-
element_handle=self.get_element_from_xpath(xpath)
|
|
247
|
-
return element_handle
|
|
248
|
-
|
|
249
|
-
def get_coordinates_from_label(self,label:int)->tuple[int,int]:
|
|
250
|
-
element_handle=self.get_element_handle_from_label(label)
|
|
251
|
-
bounding_rectangle=element_handle.BoundingRectangle
|
|
252
|
-
return bounding_rectangle.xcenter(),bounding_rectangle.ycenter()
|
|
253
|
-
|
|
254
|
-
def click(self,loc:tuple[int,int],button:str='left',clicks:int=2):
|
|
255
|
-
x,y=loc
|
|
256
|
-
pg.click(x,y,button=button,clicks=clicks,duration=0.1)
|
|
257
|
-
|
|
258
|
-
def type(self,loc:tuple[int,int],text:str,caret_position:Literal['start','end','none']='none',clear:Literal['true','false']='false',press_enter:Literal['true','false']='false'):
|
|
259
|
-
x,y=loc
|
|
260
|
-
pg.leftClick(x,y)
|
|
261
|
-
if caret_position == 'start':
|
|
262
|
-
pg.press('home')
|
|
263
|
-
elif caret_position == 'end':
|
|
264
|
-
pg.press('end')
|
|
265
|
-
else:
|
|
266
|
-
pass
|
|
267
|
-
if clear=='true':
|
|
268
|
-
pg.sleep(0.5)
|
|
269
|
-
pg.hotkey('ctrl','a')
|
|
270
|
-
pg.press('backspace')
|
|
271
|
-
pg.typewrite(text,interval=0.02)
|
|
272
|
-
if press_enter=='true':
|
|
273
|
-
pg.press('enter')
|
|
274
|
-
|
|
275
|
-
def scroll(self,loc:tuple[int,int]=None,type:Literal['horizontal','vertical']='vertical',direction:Literal['up','down','left','right']='down',wheel_times:int=1)->str|None:
|
|
276
|
-
if loc:
|
|
277
|
-
self.move(loc)
|
|
278
|
-
match type:
|
|
279
|
-
case 'vertical':
|
|
280
|
-
match direction:
|
|
281
|
-
case 'up':
|
|
282
|
-
uia.WheelUp(wheel_times)
|
|
283
|
-
case 'down':
|
|
284
|
-
uia.WheelDown(wheel_times)
|
|
285
|
-
case _:
|
|
286
|
-
return 'Invalid direction. Use "up" or "down".'
|
|
287
|
-
case 'horizontal':
|
|
288
|
-
match direction:
|
|
289
|
-
case 'left':
|
|
290
|
-
pg.keyDown('Shift')
|
|
291
|
-
pg.sleep(0.05)
|
|
292
|
-
uia.WheelUp(wheel_times)
|
|
293
|
-
pg.sleep(0.05)
|
|
294
|
-
pg.keyUp('Shift')
|
|
295
|
-
case 'right':
|
|
296
|
-
pg.keyDown('Shift')
|
|
297
|
-
pg.sleep(0.05)
|
|
298
|
-
uia.WheelDown(wheel_times)
|
|
299
|
-
pg.sleep(0.05)
|
|
300
|
-
pg.keyUp('Shift')
|
|
301
|
-
case _:
|
|
302
|
-
return 'Invalid direction. Use "left" or "right".'
|
|
303
|
-
case _:
|
|
304
|
-
return 'Invalid type. Use "horizontal" or "vertical".'
|
|
305
|
-
return None
|
|
306
|
-
|
|
307
|
-
def drag(self,loc:tuple[int,int]):
|
|
308
|
-
x,y=loc
|
|
309
|
-
pg.sleep(0.5)
|
|
310
|
-
pg.dragTo(x,y,duration=0.6)
|
|
311
|
-
|
|
312
|
-
def move(self,loc:tuple[int,int]):
|
|
313
|
-
x,y=loc
|
|
314
|
-
pg.moveTo(x,y,duration=0.1)
|
|
315
|
-
|
|
316
|
-
def shortcut(self,shortcut:str):
|
|
317
|
-
shortcut=shortcut.split('+')
|
|
318
|
-
if len(shortcut)>1:
|
|
319
|
-
pg.hotkey(*shortcut)
|
|
320
|
-
else:
|
|
321
|
-
pg.press(''.join(shortcut))
|
|
322
|
-
|
|
323
|
-
def multi_select(self,press_ctrl:Literal['true','false']='false',elements:list[tuple[int,int]|int]=[]):
|
|
324
|
-
if press_ctrl=='true':
|
|
325
|
-
pg.keyDown('ctrl')
|
|
326
|
-
for element in elements:
|
|
327
|
-
x,y=element
|
|
328
|
-
pg.click(x,y,duration=0.2)
|
|
329
|
-
pg.sleep(0.5)
|
|
330
|
-
pg.keyUp('ctrl')
|
|
331
|
-
|
|
332
|
-
def multi_edit(self,elements:list[tuple[int,int,str]|tuple[int,str]]):
|
|
333
|
-
for element in elements:
|
|
334
|
-
x,y,text=element
|
|
335
|
-
self.type((x,y),text=text,clear='true')
|
|
336
|
-
|
|
337
|
-
def scrape(self,url:str)->str:
|
|
338
|
-
response=requests.get(url,timeout=10)
|
|
339
|
-
html=response.text
|
|
340
|
-
content=markdownify(html=html)
|
|
341
|
-
return content
|
|
342
|
-
|
|
343
|
-
def get_app_size(self,control:uia.Control):
|
|
344
|
-
window=control.BoundingRectangle
|
|
345
|
-
if window.isempty():
|
|
346
|
-
return Size(width=0,height=0)
|
|
347
|
-
return Size(width=window.width(),height=window.height())
|
|
348
|
-
|
|
349
|
-
def is_app_visible(self,app)->bool:
|
|
350
|
-
is_minimized=self.get_app_status(app)!=Status.MINIMIZED
|
|
351
|
-
size=self.get_app_size(app)
|
|
352
|
-
area=size.width*size.height
|
|
353
|
-
is_overlay=self.is_overlay_app(app)
|
|
354
|
-
return not is_overlay and is_minimized and area>10
|
|
355
|
-
|
|
356
|
-
def is_overlay_app(self,element:uia.Control) -> bool:
|
|
357
|
-
no_children = len(element.GetChildren()) == 0
|
|
358
|
-
is_name = "Overlay" in element.Name.strip()
|
|
359
|
-
return no_children or is_name
|
|
360
|
-
|
|
361
|
-
def get_apps(self) -> list[App]:
|
|
362
|
-
try:
|
|
363
|
-
desktop = uia.GetRootControl() # Get the desktop control
|
|
364
|
-
children = desktop.GetChildren()
|
|
365
|
-
apps = []
|
|
366
|
-
for depth, child in enumerate(children):
|
|
367
|
-
if isinstance(child,(uia.WindowControl,uia.PaneControl)):
|
|
368
|
-
window_pattern=child.GetPattern(uia.PatternId.WindowPattern)
|
|
369
|
-
if (window_pattern is None):
|
|
370
|
-
continue
|
|
371
|
-
if window_pattern.CanMinimize and window_pattern.CanMaximize:
|
|
372
|
-
status = self.get_app_status(child)
|
|
373
|
-
size=self.get_app_size(child)
|
|
374
|
-
apps.append(App(**{
|
|
375
|
-
"name":child.Name,
|
|
376
|
-
"depth":depth,
|
|
377
|
-
"status":status,
|
|
378
|
-
"size":size,
|
|
379
|
-
"handle":child.NativeWindowHandle,
|
|
380
|
-
"process_id":child.ProcessId
|
|
381
|
-
}))
|
|
382
|
-
except Exception as ex:
|
|
383
|
-
logger.error(f"Error in get_apps: {ex}")
|
|
384
|
-
apps = []
|
|
385
|
-
return apps
|
|
386
|
-
|
|
387
|
-
def get_xpath_from_element(self,element:uia.Control):
|
|
388
|
-
current=element
|
|
389
|
-
if current is None:
|
|
390
|
-
return ""
|
|
391
|
-
path_parts=[]
|
|
392
|
-
while current is not None:
|
|
393
|
-
parent=current.GetParentControl()
|
|
394
|
-
if parent is None:
|
|
395
|
-
# we are at the root node
|
|
396
|
-
path_parts.append(f'{current.ControlTypeName}')
|
|
397
|
-
break
|
|
398
|
-
children=parent.GetChildren()
|
|
399
|
-
same_type_children=["-".join(map(lambda x:str(x),child.GetRuntimeId())) for child in children if child.ControlType==current.ControlType]
|
|
400
|
-
index=same_type_children.index("-".join(map(lambda x:str(x),current.GetRuntimeId())))
|
|
401
|
-
if same_type_children:
|
|
402
|
-
path_parts.append(f'{current.ControlTypeName}[{index+1}]')
|
|
403
|
-
else:
|
|
404
|
-
path_parts.append(f'{current.ControlTypeName}')
|
|
405
|
-
current=parent
|
|
406
|
-
path_parts.reverse()
|
|
407
|
-
xpath="/".join(path_parts)
|
|
408
|
-
return xpath
|
|
409
|
-
|
|
410
|
-
def get_element_from_xpath(self,xpath:str)->uia.Control:
|
|
411
|
-
pattern = re.compile(r'(\w+)(?:\[(\d+)\])?')
|
|
412
|
-
parts=xpath.split("/")
|
|
413
|
-
root=uia.GetRootControl()
|
|
414
|
-
element=root
|
|
415
|
-
for part in parts[1:]:
|
|
416
|
-
match=pattern.fullmatch(part)
|
|
417
|
-
if match is None:
|
|
418
|
-
continue
|
|
419
|
-
control_type, index=match.groups()
|
|
420
|
-
index=int(index) if index else None
|
|
421
|
-
children=element.GetChildren()
|
|
422
|
-
same_type_children=list(filter(lambda x:x.ControlTypeName==control_type,children))
|
|
423
|
-
if index:
|
|
424
|
-
element=same_type_children[index-1]
|
|
425
|
-
else:
|
|
426
|
-
element=same_type_children[0]
|
|
427
|
-
return element
|
|
428
|
-
|
|
429
|
-
def get_windows_version(self)->str:
|
|
430
|
-
response,status=self.execute_command("(Get-CimInstance Win32_OperatingSystem).Caption")
|
|
431
|
-
if status==0:
|
|
432
|
-
return response.strip()
|
|
433
|
-
return "Windows"
|
|
434
|
-
|
|
435
|
-
def get_user_account_type(self)->str:
|
|
436
|
-
response,status=self.execute_command("(Get-LocalUser -Name $env:USERNAME).PrincipalSource")
|
|
437
|
-
return "Local Account" if response.strip()=='Local' else "Microsoft Account" if status==0 else "Local Account"
|
|
438
|
-
|
|
439
|
-
def get_dpi_scaling(self):
|
|
440
|
-
user32 = ctypes.windll.user32
|
|
441
|
-
dpi = user32.GetDpiForSystem()
|
|
442
|
-
return dpi / 96.0
|
|
443
|
-
|
|
444
|
-
def get_screen_size(self)->Size:
|
|
445
|
-
width, height = uia.GetScreenSize()
|
|
446
|
-
return Size(width=width,height=height)
|
|
447
|
-
|
|
448
|
-
def get_screenshot(self)->Image.Image:
|
|
449
|
-
return pg.screenshot()
|
|
450
|
-
|
|
451
|
-
@contextmanager
|
|
452
|
-
def auto_minimize(self):
|
|
453
|
-
try:
|
|
454
|
-
handle = uia.GetForegroundWindow()
|
|
455
|
-
uia.ShowWindow(handle, win32con.SW_MINIMIZE)
|
|
456
|
-
yield
|
|
457
|
-
finally:
|
|
1
|
+
from windows_mcp.desktop.config import BROWSER_NAMES, PROCESS_PER_MONITOR_DPI_AWARE
|
|
2
|
+
from windows_mcp.desktop.views import DesktopState, App, Size, Status
|
|
3
|
+
from windows_mcp.tree.service import Tree
|
|
4
|
+
from locale import getpreferredencoding
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Optional,Literal
|
|
7
|
+
from markdownify import markdownify
|
|
8
|
+
from fuzzywuzzy import process
|
|
9
|
+
from psutil import Process
|
|
10
|
+
from time import sleep
|
|
11
|
+
from PIL import Image
|
|
12
|
+
import win32process
|
|
13
|
+
import subprocess
|
|
14
|
+
import win32gui
|
|
15
|
+
import win32con
|
|
16
|
+
import requests
|
|
17
|
+
import logging
|
|
18
|
+
import base64
|
|
19
|
+
import ctypes
|
|
20
|
+
import csv
|
|
21
|
+
import re
|
|
22
|
+
import os
|
|
23
|
+
import io
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
logger.setLevel(logging.INFO)
|
|
27
|
+
handler = logging.StreamHandler()
|
|
28
|
+
formatter = logging.Formatter('[%(levelname)s] %(message)s')
|
|
29
|
+
handler.setFormatter(formatter)
|
|
30
|
+
logger.addHandler(handler)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE)
|
|
34
|
+
except Exception:
|
|
35
|
+
ctypes.windll.user32.SetProcessDPIAware()
|
|
36
|
+
|
|
37
|
+
import windows_mcp.uia as uia
|
|
38
|
+
import pyautogui as pg
|
|
39
|
+
|
|
40
|
+
pg.FAILSAFE=False
|
|
41
|
+
pg.PAUSE=1.0
|
|
42
|
+
|
|
43
|
+
class Desktop:
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self.encoding=getpreferredencoding()
|
|
46
|
+
self.tree=Tree(self)
|
|
47
|
+
self.desktop_state=None
|
|
48
|
+
|
|
49
|
+
def get_resolution(self)->tuple[int,int]:
|
|
50
|
+
return pg.size()
|
|
51
|
+
|
|
52
|
+
def get_state(self,use_vision:bool=False,use_dom:bool=False,as_bytes:bool=False,scale:float=1.0)->DesktopState:
|
|
53
|
+
sleep(0.1)
|
|
54
|
+
apps=self.get_apps()
|
|
55
|
+
active_app=self.get_active_app()
|
|
56
|
+
if active_app is not None and active_app in apps:
|
|
57
|
+
apps.remove(active_app)
|
|
58
|
+
logger.debug(f"Active app: {active_app}")
|
|
59
|
+
logger.debug(f"Apps: {apps}")
|
|
60
|
+
tree_state=self.tree.get_state(active_app,apps,use_dom=use_dom)
|
|
61
|
+
if use_vision:
|
|
62
|
+
screenshot=self.tree.get_annotated_screenshot(tree_state.interactive_nodes,scale=scale)
|
|
63
|
+
if as_bytes:
|
|
64
|
+
bytes_io=io.BytesIO()
|
|
65
|
+
screenshot.save(bytes_io,format='PNG')
|
|
66
|
+
screenshot=bytes_io.getvalue()
|
|
67
|
+
else:
|
|
68
|
+
screenshot=None
|
|
69
|
+
self.desktop_state=DesktopState(apps= apps,active_app=active_app,screenshot=screenshot,tree_state=tree_state)
|
|
70
|
+
return self.desktop_state
|
|
71
|
+
|
|
72
|
+
def get_window_element_from_element(self,element:uia.Control)->uia.Control|None:
|
|
73
|
+
while element is not None:
|
|
74
|
+
if uia.IsTopLevelWindow(element.NativeWindowHandle):
|
|
75
|
+
return element
|
|
76
|
+
element = element.GetParentControl()
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def get_active_app(self)->App|None:
|
|
80
|
+
try:
|
|
81
|
+
handle=uia.GetForegroundWindow()
|
|
82
|
+
for app in self.get_apps():
|
|
83
|
+
if app.handle!=handle:
|
|
84
|
+
continue
|
|
85
|
+
return app
|
|
86
|
+
except Exception as ex:
|
|
87
|
+
logger.error(f"Error in get_active_app: {ex}")
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def get_app_status(self,control:uia.Control)->Status:
|
|
91
|
+
if uia.IsIconic(control.NativeWindowHandle):
|
|
92
|
+
return Status.MINIMIZED
|
|
93
|
+
elif uia.IsZoomed(control.NativeWindowHandle):
|
|
94
|
+
return Status.MAXIMIZED
|
|
95
|
+
elif uia.IsWindowVisible(control.NativeWindowHandle):
|
|
96
|
+
return Status.NORMAL
|
|
97
|
+
else:
|
|
98
|
+
return Status.HIDDEN
|
|
99
|
+
|
|
100
|
+
def get_cursor_location(self)->tuple[int,int]:
|
|
101
|
+
position=pg.position()
|
|
102
|
+
return (position.x,position.y)
|
|
103
|
+
|
|
104
|
+
def get_element_under_cursor(self)->uia.Control:
|
|
105
|
+
return uia.ControlFromCursor()
|
|
106
|
+
|
|
107
|
+
def get_apps_from_start_menu(self)->dict[str,str]:
|
|
108
|
+
command='Get-StartApps | ConvertTo-Csv -NoTypeInformation'
|
|
109
|
+
apps_info,_=self.execute_command(command)
|
|
110
|
+
reader=csv.DictReader(io.StringIO(apps_info))
|
|
111
|
+
return {row.get('Name').lower():row.get('AppID') for row in reader}
|
|
112
|
+
|
|
113
|
+
def execute_command(self,command:str)->tuple[str,int]:
|
|
114
|
+
try:
|
|
115
|
+
encoded = base64.b64encode(command.encode("utf-16le")).decode("ascii")
|
|
116
|
+
result = subprocess.run(
|
|
117
|
+
['powershell', '-NoProfile', '-EncodedCommand', encoded],
|
|
118
|
+
capture_output=True,
|
|
119
|
+
errors='ignore',
|
|
120
|
+
timeout=25,
|
|
121
|
+
cwd=os.path.expanduser(path='~')
|
|
122
|
+
)
|
|
123
|
+
stdout=result.stdout
|
|
124
|
+
stderr=result.stderr
|
|
125
|
+
return (stdout or stderr,result.returncode)
|
|
126
|
+
except subprocess.TimeoutExpired:
|
|
127
|
+
return ('Command execution timed out', 1)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
return ('Command execution failed', 1)
|
|
130
|
+
|
|
131
|
+
def is_app_browser(self,node:uia.Control):
|
|
132
|
+
process=Process(node.ProcessId)
|
|
133
|
+
return process.name() in BROWSER_NAMES
|
|
134
|
+
|
|
135
|
+
def get_default_language(self)->str:
|
|
136
|
+
command="Get-Culture | Select-Object Name,DisplayName | ConvertTo-Csv -NoTypeInformation"
|
|
137
|
+
response,_=self.execute_command(command)
|
|
138
|
+
reader=csv.DictReader(io.StringIO(response))
|
|
139
|
+
return "".join([row.get('DisplayName') for row in reader])
|
|
140
|
+
|
|
141
|
+
def resize_app(self,size:tuple[int,int]=None,loc:tuple[int,int]=None)->tuple[str,int]:
|
|
142
|
+
active_app=self.desktop_state.active_app
|
|
143
|
+
if active_app is None:
|
|
144
|
+
return "No active app found",1
|
|
145
|
+
if active_app.status==Status.MINIMIZED:
|
|
146
|
+
return f"{active_app.name} is minimized",1
|
|
147
|
+
elif active_app.status==Status.MAXIMIZED:
|
|
148
|
+
return f"{active_app.name} is maximized",1
|
|
149
|
+
else:
|
|
150
|
+
app_control=uia.ControlFromHandle(active_app.handle)
|
|
151
|
+
if loc is None:
|
|
152
|
+
x=app_control.BoundingRectangle.left
|
|
153
|
+
y=app_control.BoundingRectangle.top
|
|
154
|
+
loc=(x,y)
|
|
155
|
+
if size is None:
|
|
156
|
+
width=app_control.BoundingRectangle.width()
|
|
157
|
+
height=app_control.BoundingRectangle.height()
|
|
158
|
+
size=(width,height)
|
|
159
|
+
x,y=loc
|
|
160
|
+
width,height=size
|
|
161
|
+
app_control.MoveWindow(x,y,width,height)
|
|
162
|
+
return (f'{active_app.name} resized to {width}x{height} at {x},{y}.',0)
|
|
163
|
+
|
|
164
|
+
def is_app_running(self,name:str)->bool:
|
|
165
|
+
apps={app.name:app for app in self.get_apps()}
|
|
166
|
+
return process.extractOne(name,list(apps.keys()),score_cutoff=60) is not None
|
|
167
|
+
|
|
168
|
+
def app(self,mode:Literal['launch','switch','resize'],name:Optional[str]=None,loc:Optional[tuple[int,int]]=None,size:Optional[tuple[int,int]]=None):
|
|
169
|
+
match mode:
|
|
170
|
+
case 'launch':
|
|
171
|
+
response,status=self.launch_app(name)
|
|
172
|
+
sleep(1.25)
|
|
173
|
+
if status!=0:
|
|
174
|
+
return response
|
|
175
|
+
consecutive_waits=10
|
|
176
|
+
for _ in range(consecutive_waits):
|
|
177
|
+
if not self.is_app_running(name):
|
|
178
|
+
sleep(1.25)
|
|
179
|
+
else:
|
|
180
|
+
return f'{name.title()} launched.'
|
|
181
|
+
return f'Launching {name.title()} wait for it to come load.'
|
|
182
|
+
case 'resize':
|
|
183
|
+
response,status=self.resize_app(size=size,loc=loc)
|
|
184
|
+
if status!=0:
|
|
185
|
+
return response
|
|
186
|
+
else:
|
|
187
|
+
return response
|
|
188
|
+
case 'switch':
|
|
189
|
+
response,status=self.switch_app(name)
|
|
190
|
+
if status!=0:
|
|
191
|
+
return response
|
|
192
|
+
else:
|
|
193
|
+
return response
|
|
194
|
+
|
|
195
|
+
def launch_app(self,name:str)->tuple[str,int]:
|
|
196
|
+
apps_map=self.get_apps_from_start_menu()
|
|
197
|
+
matched_app=process.extractOne(name,apps_map.keys(),score_cutoff=70)
|
|
198
|
+
if matched_app is None:
|
|
199
|
+
return (f'{name.title()} not found in start menu.',1)
|
|
200
|
+
app_name,_=matched_app
|
|
201
|
+
appid=apps_map.get(app_name)
|
|
202
|
+
if appid is None:
|
|
203
|
+
return (f'{name.title()} not found in start menu.',1)
|
|
204
|
+
if appid.endswith('.exe'):
|
|
205
|
+
command=f"Start-Process '{appid}'"
|
|
206
|
+
else:
|
|
207
|
+
command=f"Start-Process shell:AppsFolder\\{appid}"
|
|
208
|
+
response,status=self.execute_command(command)
|
|
209
|
+
return response,status
|
|
210
|
+
|
|
211
|
+
def switch_app(self,name:str):
|
|
212
|
+
apps={app.name:app for app in [self.desktop_state.active_app]+self.desktop_state.apps if app is not None}
|
|
213
|
+
matched_app:Optional[tuple[str,float]]=process.extractOne(name,list(apps.keys()),score_cutoff=70)
|
|
214
|
+
if matched_app is None:
|
|
215
|
+
return (f'Application {name.title()} not found.',1)
|
|
216
|
+
app_name,_=matched_app
|
|
217
|
+
app=apps.get(app_name)
|
|
218
|
+
target_handle=app.handle
|
|
219
|
+
|
|
220
|
+
if uia.IsIconic(target_handle):
|
|
221
|
+
uia.ShowWindow(target_handle, win32con.SW_RESTORE)
|
|
222
|
+
content=f'{app_name.title()} restored from Minimized state.'
|
|
223
|
+
else:
|
|
224
|
+
self.bring_window_to_top(target_handle)
|
|
225
|
+
content=f'Switched to {app_name.title()} window.'
|
|
226
|
+
return content,0
|
|
227
|
+
|
|
228
|
+
def bring_window_to_top(self,target_handle:int):
|
|
229
|
+
foreground_handle=win32gui.GetForegroundWindow()
|
|
230
|
+
foreground_thread,_=win32process.GetWindowThreadProcessId(foreground_handle)
|
|
231
|
+
target_thread,_=win32process.GetWindowThreadProcessId(target_handle)
|
|
232
|
+
try:
|
|
233
|
+
ctypes.windll.user32.AllowSetForegroundWindow(-1)
|
|
234
|
+
win32process.AttachThreadInput(foreground_thread,target_thread,True)
|
|
235
|
+
win32gui.SetForegroundWindow(target_handle)
|
|
236
|
+
win32gui.BringWindowToTop(target_handle)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f'Failed to bring window to top: {e}')
|
|
239
|
+
finally:
|
|
240
|
+
win32process.AttachThreadInput(foreground_thread,target_thread,False)
|
|
241
|
+
|
|
242
|
+
def get_element_handle_from_label(self,label:int)->uia.Control:
|
|
243
|
+
tree_state=self.desktop_state.tree_state
|
|
244
|
+
element_node=tree_state.interactive_nodes[label]
|
|
245
|
+
xpath=element_node.xpath
|
|
246
|
+
element_handle=self.get_element_from_xpath(xpath)
|
|
247
|
+
return element_handle
|
|
248
|
+
|
|
249
|
+
def get_coordinates_from_label(self,label:int)->tuple[int,int]:
|
|
250
|
+
element_handle=self.get_element_handle_from_label(label)
|
|
251
|
+
bounding_rectangle=element_handle.BoundingRectangle
|
|
252
|
+
return bounding_rectangle.xcenter(),bounding_rectangle.ycenter()
|
|
253
|
+
|
|
254
|
+
def click(self,loc:tuple[int,int],button:str='left',clicks:int=2):
|
|
255
|
+
x,y=loc
|
|
256
|
+
pg.click(x,y,button=button,clicks=clicks,duration=0.1)
|
|
257
|
+
|
|
258
|
+
def type(self,loc:tuple[int,int],text:str,caret_position:Literal['start','end','none']='none',clear:Literal['true','false']='false',press_enter:Literal['true','false']='false'):
|
|
259
|
+
x,y=loc
|
|
260
|
+
pg.leftClick(x,y)
|
|
261
|
+
if caret_position == 'start':
|
|
262
|
+
pg.press('home')
|
|
263
|
+
elif caret_position == 'end':
|
|
264
|
+
pg.press('end')
|
|
265
|
+
else:
|
|
266
|
+
pass
|
|
267
|
+
if clear=='true':
|
|
268
|
+
pg.sleep(0.5)
|
|
269
|
+
pg.hotkey('ctrl','a')
|
|
270
|
+
pg.press('backspace')
|
|
271
|
+
pg.typewrite(text,interval=0.02)
|
|
272
|
+
if press_enter=='true':
|
|
273
|
+
pg.press('enter')
|
|
274
|
+
|
|
275
|
+
def scroll(self,loc:tuple[int,int]=None,type:Literal['horizontal','vertical']='vertical',direction:Literal['up','down','left','right']='down',wheel_times:int=1)->str|None:
|
|
276
|
+
if loc:
|
|
277
|
+
self.move(loc)
|
|
278
|
+
match type:
|
|
279
|
+
case 'vertical':
|
|
280
|
+
match direction:
|
|
281
|
+
case 'up':
|
|
282
|
+
uia.WheelUp(wheel_times)
|
|
283
|
+
case 'down':
|
|
284
|
+
uia.WheelDown(wheel_times)
|
|
285
|
+
case _:
|
|
286
|
+
return 'Invalid direction. Use "up" or "down".'
|
|
287
|
+
case 'horizontal':
|
|
288
|
+
match direction:
|
|
289
|
+
case 'left':
|
|
290
|
+
pg.keyDown('Shift')
|
|
291
|
+
pg.sleep(0.05)
|
|
292
|
+
uia.WheelUp(wheel_times)
|
|
293
|
+
pg.sleep(0.05)
|
|
294
|
+
pg.keyUp('Shift')
|
|
295
|
+
case 'right':
|
|
296
|
+
pg.keyDown('Shift')
|
|
297
|
+
pg.sleep(0.05)
|
|
298
|
+
uia.WheelDown(wheel_times)
|
|
299
|
+
pg.sleep(0.05)
|
|
300
|
+
pg.keyUp('Shift')
|
|
301
|
+
case _:
|
|
302
|
+
return 'Invalid direction. Use "left" or "right".'
|
|
303
|
+
case _:
|
|
304
|
+
return 'Invalid type. Use "horizontal" or "vertical".'
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def drag(self,loc:tuple[int,int]):
|
|
308
|
+
x,y=loc
|
|
309
|
+
pg.sleep(0.5)
|
|
310
|
+
pg.dragTo(x,y,duration=0.6)
|
|
311
|
+
|
|
312
|
+
def move(self,loc:tuple[int,int]):
|
|
313
|
+
x,y=loc
|
|
314
|
+
pg.moveTo(x,y,duration=0.1)
|
|
315
|
+
|
|
316
|
+
def shortcut(self,shortcut:str):
|
|
317
|
+
shortcut=shortcut.split('+')
|
|
318
|
+
if len(shortcut)>1:
|
|
319
|
+
pg.hotkey(*shortcut)
|
|
320
|
+
else:
|
|
321
|
+
pg.press(''.join(shortcut))
|
|
322
|
+
|
|
323
|
+
def multi_select(self,press_ctrl:Literal['true','false']='false',elements:list[tuple[int,int]|int]=[]):
|
|
324
|
+
if press_ctrl=='true':
|
|
325
|
+
pg.keyDown('ctrl')
|
|
326
|
+
for element in elements:
|
|
327
|
+
x,y=element
|
|
328
|
+
pg.click(x,y,duration=0.2)
|
|
329
|
+
pg.sleep(0.5)
|
|
330
|
+
pg.keyUp('ctrl')
|
|
331
|
+
|
|
332
|
+
def multi_edit(self,elements:list[tuple[int,int,str]|tuple[int,str]]):
|
|
333
|
+
for element in elements:
|
|
334
|
+
x,y,text=element
|
|
335
|
+
self.type((x,y),text=text,clear='true')
|
|
336
|
+
|
|
337
|
+
def scrape(self,url:str)->str:
|
|
338
|
+
response=requests.get(url,timeout=10)
|
|
339
|
+
html=response.text
|
|
340
|
+
content=markdownify(html=html)
|
|
341
|
+
return content
|
|
342
|
+
|
|
343
|
+
def get_app_size(self,control:uia.Control):
|
|
344
|
+
window=control.BoundingRectangle
|
|
345
|
+
if window.isempty():
|
|
346
|
+
return Size(width=0,height=0)
|
|
347
|
+
return Size(width=window.width(),height=window.height())
|
|
348
|
+
|
|
349
|
+
def is_app_visible(self,app)->bool:
|
|
350
|
+
is_minimized=self.get_app_status(app)!=Status.MINIMIZED
|
|
351
|
+
size=self.get_app_size(app)
|
|
352
|
+
area=size.width*size.height
|
|
353
|
+
is_overlay=self.is_overlay_app(app)
|
|
354
|
+
return not is_overlay and is_minimized and area>10
|
|
355
|
+
|
|
356
|
+
def is_overlay_app(self,element:uia.Control) -> bool:
|
|
357
|
+
no_children = len(element.GetChildren()) == 0
|
|
358
|
+
is_name = "Overlay" in element.Name.strip()
|
|
359
|
+
return no_children or is_name
|
|
360
|
+
|
|
361
|
+
def get_apps(self) -> list[App]:
|
|
362
|
+
try:
|
|
363
|
+
desktop = uia.GetRootControl() # Get the desktop control
|
|
364
|
+
children = desktop.GetChildren()
|
|
365
|
+
apps = []
|
|
366
|
+
for depth, child in enumerate(children):
|
|
367
|
+
if isinstance(child,(uia.WindowControl,uia.PaneControl)):
|
|
368
|
+
window_pattern=child.GetPattern(uia.PatternId.WindowPattern)
|
|
369
|
+
if (window_pattern is None):
|
|
370
|
+
continue
|
|
371
|
+
if window_pattern.CanMinimize and window_pattern.CanMaximize:
|
|
372
|
+
status = self.get_app_status(child)
|
|
373
|
+
size=self.get_app_size(child)
|
|
374
|
+
apps.append(App(**{
|
|
375
|
+
"name":child.Name,
|
|
376
|
+
"depth":depth,
|
|
377
|
+
"status":status,
|
|
378
|
+
"size":size,
|
|
379
|
+
"handle":child.NativeWindowHandle,
|
|
380
|
+
"process_id":child.ProcessId
|
|
381
|
+
}))
|
|
382
|
+
except Exception as ex:
|
|
383
|
+
logger.error(f"Error in get_apps: {ex}")
|
|
384
|
+
apps = []
|
|
385
|
+
return apps
|
|
386
|
+
|
|
387
|
+
def get_xpath_from_element(self,element:uia.Control):
|
|
388
|
+
current=element
|
|
389
|
+
if current is None:
|
|
390
|
+
return ""
|
|
391
|
+
path_parts=[]
|
|
392
|
+
while current is not None:
|
|
393
|
+
parent=current.GetParentControl()
|
|
394
|
+
if parent is None:
|
|
395
|
+
# we are at the root node
|
|
396
|
+
path_parts.append(f'{current.ControlTypeName}')
|
|
397
|
+
break
|
|
398
|
+
children=parent.GetChildren()
|
|
399
|
+
same_type_children=["-".join(map(lambda x:str(x),child.GetRuntimeId())) for child in children if child.ControlType==current.ControlType]
|
|
400
|
+
index=same_type_children.index("-".join(map(lambda x:str(x),current.GetRuntimeId())))
|
|
401
|
+
if same_type_children:
|
|
402
|
+
path_parts.append(f'{current.ControlTypeName}[{index+1}]')
|
|
403
|
+
else:
|
|
404
|
+
path_parts.append(f'{current.ControlTypeName}')
|
|
405
|
+
current=parent
|
|
406
|
+
path_parts.reverse()
|
|
407
|
+
xpath="/".join(path_parts)
|
|
408
|
+
return xpath
|
|
409
|
+
|
|
410
|
+
def get_element_from_xpath(self,xpath:str)->uia.Control:
|
|
411
|
+
pattern = re.compile(r'(\w+)(?:\[(\d+)\])?')
|
|
412
|
+
parts=xpath.split("/")
|
|
413
|
+
root=uia.GetRootControl()
|
|
414
|
+
element=root
|
|
415
|
+
for part in parts[1:]:
|
|
416
|
+
match=pattern.fullmatch(part)
|
|
417
|
+
if match is None:
|
|
418
|
+
continue
|
|
419
|
+
control_type, index=match.groups()
|
|
420
|
+
index=int(index) if index else None
|
|
421
|
+
children=element.GetChildren()
|
|
422
|
+
same_type_children=list(filter(lambda x:x.ControlTypeName==control_type,children))
|
|
423
|
+
if index:
|
|
424
|
+
element=same_type_children[index-1]
|
|
425
|
+
else:
|
|
426
|
+
element=same_type_children[0]
|
|
427
|
+
return element
|
|
428
|
+
|
|
429
|
+
def get_windows_version(self)->str:
|
|
430
|
+
response,status=self.execute_command("(Get-CimInstance Win32_OperatingSystem).Caption")
|
|
431
|
+
if status==0:
|
|
432
|
+
return response.strip()
|
|
433
|
+
return "Windows"
|
|
434
|
+
|
|
435
|
+
def get_user_account_type(self)->str:
|
|
436
|
+
response,status=self.execute_command("(Get-LocalUser -Name $env:USERNAME).PrincipalSource")
|
|
437
|
+
return "Local Account" if response.strip()=='Local' else "Microsoft Account" if status==0 else "Local Account"
|
|
438
|
+
|
|
439
|
+
def get_dpi_scaling(self):
|
|
440
|
+
user32 = ctypes.windll.user32
|
|
441
|
+
dpi = user32.GetDpiForSystem()
|
|
442
|
+
return dpi / 96.0
|
|
443
|
+
|
|
444
|
+
def get_screen_size(self)->Size:
|
|
445
|
+
width, height = uia.GetScreenSize()
|
|
446
|
+
return Size(width=width,height=height)
|
|
447
|
+
|
|
448
|
+
def get_screenshot(self)->Image.Image:
|
|
449
|
+
return pg.screenshot()
|
|
450
|
+
|
|
451
|
+
@contextmanager
|
|
452
|
+
def auto_minimize(self):
|
|
453
|
+
try:
|
|
454
|
+
handle = uia.GetForegroundWindow()
|
|
455
|
+
uia.ShowWindow(handle, win32con.SW_MINIMIZE)
|
|
456
|
+
yield
|
|
457
|
+
finally:
|
|
458
458
|
uia.ShowWindow(handle, win32con.SW_RESTORE)
|