windows-mcp 0.6.0__py3-none-any.whl → 0.6.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.
- windows_mcp/__main__.py +57 -15
- windows_mcp/desktop/service.py +182 -100
- windows_mcp/desktop/views.py +32 -15
- windows_mcp/tree/service.py +29 -29
- windows_mcp/tree/utils.py +21 -21
- windows_mcp/tree/views.py +9 -9
- windows_mcp/uia/controls.py +0 -42
- windows_mcp/uia/core.py +19 -929
- windows_mcp/vdm/core.py +147 -135
- {windows_mcp-0.6.0.dist-info → windows_mcp-0.6.1.dist-info}/METADATA +4 -5
- {windows_mcp-0.6.0.dist-info → windows_mcp-0.6.1.dist-info}/RECORD +14 -14
- {windows_mcp-0.6.0.dist-info → windows_mcp-0.6.1.dist-info}/WHEEL +0 -0
- {windows_mcp-0.6.0.dist-info → windows_mcp-0.6.1.dist-info}/entry_points.txt +0 -0
- {windows_mcp-0.6.0.dist-info → windows_mcp-0.6.1.dist-info}/licenses/LICENSE.md +0 -0
windows_mcp/desktop/views.py
CHANGED
|
@@ -6,9 +6,15 @@ from PIL.Image import Image
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
|
|
8
8
|
class Browser(Enum):
|
|
9
|
-
CHROME='
|
|
10
|
-
EDGE='
|
|
11
|
-
FIREFOX='
|
|
9
|
+
CHROME='chrome'
|
|
10
|
+
EDGE='msedge'
|
|
11
|
+
FIREFOX='firefox'
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def has_process(cls, process_name: str) -> bool:
|
|
15
|
+
if not hasattr(cls, '_process_names'):
|
|
16
|
+
cls._process_names = {f'{b.value}.exe' for b in cls}
|
|
17
|
+
return process_name.lower() in cls._process_names
|
|
12
18
|
|
|
13
19
|
class Status(Enum):
|
|
14
20
|
MAXIMIZED='Maximized'
|
|
@@ -18,9 +24,8 @@ class Status(Enum):
|
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
@dataclass
|
|
21
|
-
class
|
|
27
|
+
class Window:
|
|
22
28
|
name:str
|
|
23
|
-
runtime_id:tuple[int]
|
|
24
29
|
is_browser:bool
|
|
25
30
|
depth:int
|
|
26
31
|
status:Status
|
|
@@ -41,20 +46,32 @@ class Size:
|
|
|
41
46
|
|
|
42
47
|
@dataclass
|
|
43
48
|
class DesktopState:
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
active_desktop:dict
|
|
50
|
+
all_desktops:list[dict]
|
|
51
|
+
active_window:Optional[Window]
|
|
52
|
+
windows:list[Window]
|
|
46
53
|
screenshot:Optional[Image]=None
|
|
47
54
|
tree_state:Optional[TreeState]=None
|
|
48
55
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
def active_desktop_to_string(self):
|
|
57
|
+
desktop_name=self.active_desktop.get('name')
|
|
58
|
+
headers=["Name"]
|
|
59
|
+
return tabulate([[desktop_name]], headers=headers, tablefmt="simple")
|
|
60
|
+
|
|
61
|
+
def desktops_to_string(self):
|
|
62
|
+
headers=["Name"]
|
|
63
|
+
rows=[[desktop.get("name")] for desktop in self.all_desktops]
|
|
64
|
+
return tabulate(rows, headers=headers, tablefmt="simple")
|
|
65
|
+
|
|
66
|
+
def active_window_to_string(self):
|
|
67
|
+
if not self.active_window:
|
|
68
|
+
return 'No active window found'
|
|
52
69
|
headers = ["Name", "Depth", "Status", "Width", "Height", "Handle"]
|
|
53
|
-
return tabulate([self.
|
|
70
|
+
return tabulate([self.active_window.to_row()], headers=headers, tablefmt="simple")
|
|
54
71
|
|
|
55
|
-
def
|
|
56
|
-
if not self.
|
|
57
|
-
return 'No
|
|
72
|
+
def windows_to_string(self):
|
|
73
|
+
if not self.windows:
|
|
74
|
+
return 'No windows found'
|
|
58
75
|
headers = ["Name", "Depth", "Status", "Width", "Height", "Handle"]
|
|
59
|
-
rows = [
|
|
76
|
+
rows = [window.to_row() for window in self.windows]
|
|
60
77
|
return tabulate(rows, headers=headers, tablefmt="simple")
|
windows_mcp/tree/service.py
CHANGED
|
@@ -31,26 +31,26 @@ class Tree:
|
|
|
31
31
|
self.tree_state=None
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def get_state(self,
|
|
34
|
+
def get_state(self,active_window_handle:int|None,other_windows_handles:list[int],use_dom:bool=False)->TreeState:
|
|
35
35
|
# Reset DOM state to prevent leaks and stale data
|
|
36
36
|
self.dom = None
|
|
37
37
|
self.dom_bounding_box = None
|
|
38
38
|
start_time = time()
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
active_window_flag=False
|
|
41
|
+
if active_window_handle:
|
|
42
|
+
active_window_flag=True
|
|
43
|
+
windows_handles=[active_window_handle]+other_windows_handles
|
|
44
44
|
else:
|
|
45
|
-
|
|
45
|
+
windows_handles=other_windows_handles
|
|
46
46
|
|
|
47
|
-
interactive_nodes,scrollable_nodes,dom_informative_nodes=self.
|
|
47
|
+
interactive_nodes,scrollable_nodes,dom_informative_nodes=self.get_windowwise_nodes(windows_handles=windows_handles,active_window_flag=active_window_flag,use_dom=use_dom)
|
|
48
48
|
root_node=TreeElementNode(
|
|
49
49
|
name="Desktop",
|
|
50
50
|
control_type="PaneControl",
|
|
51
51
|
bounding_box=self.screen_box,
|
|
52
52
|
center=self.screen_box.get_center(),
|
|
53
|
-
|
|
53
|
+
window_name="Desktop",
|
|
54
54
|
xpath='',
|
|
55
55
|
value='',
|
|
56
56
|
shortcut='',
|
|
@@ -68,7 +68,7 @@ class Tree:
|
|
|
68
68
|
vertical_scrollable=scroll_pattern.VerticallyScrollable if scroll_pattern else False,
|
|
69
69
|
vertical_scroll_percent=scroll_pattern.VerticalScrollPercent if scroll_pattern and scroll_pattern.VerticallyScrollable else 0,
|
|
70
70
|
xpath='',
|
|
71
|
-
|
|
71
|
+
window_name="DOM",
|
|
72
72
|
is_focused=False
|
|
73
73
|
)
|
|
74
74
|
else:
|
|
@@ -78,26 +78,26 @@ class Tree:
|
|
|
78
78
|
logger.info(f"Tree State capture took {end_time - start_time:.2f} seconds")
|
|
79
79
|
return self.tree_state
|
|
80
80
|
|
|
81
|
-
def
|
|
81
|
+
def get_windowwise_nodes(self,windows_handles:list[int],active_window_flag:bool,use_dom:bool=False) -> tuple[list[TreeElementNode],list[ScrollElementNode],list[TextElementNode]]:
|
|
82
82
|
interactive_nodes, scrollable_nodes, dom_informative_nodes = [], [], []
|
|
83
83
|
|
|
84
84
|
# Pre-calculate browser status in main thread to pass simple types to workers
|
|
85
85
|
task_inputs = []
|
|
86
|
-
for handle in
|
|
86
|
+
for handle in windows_handles:
|
|
87
87
|
is_browser = False
|
|
88
88
|
try:
|
|
89
89
|
# Use temporary control for property check in main thread
|
|
90
90
|
# This is safe as we don't pass this specific COM object to the thread
|
|
91
91
|
temp_node = ControlFromHandle(handle)
|
|
92
|
-
if
|
|
92
|
+
if active_window_flag and temp_node.ClassName=='Progman':
|
|
93
93
|
continue
|
|
94
|
-
is_browser = self.desktop.
|
|
94
|
+
is_browser = self.desktop.is_window_browser(temp_node)
|
|
95
95
|
except Exception:
|
|
96
96
|
pass
|
|
97
97
|
task_inputs.append((handle, is_browser))
|
|
98
98
|
|
|
99
99
|
with ThreadPoolExecutor() as executor:
|
|
100
|
-
retry_counts = {handle: 0 for handle in
|
|
100
|
+
retry_counts = {handle: 0 for handle in windows_handles}
|
|
101
101
|
future_to_handle = {
|
|
102
102
|
executor.submit(self.get_nodes, handle, is_browser, use_dom): handle
|
|
103
103
|
for handle, is_browser in task_inputs
|
|
@@ -168,7 +168,7 @@ class Tree:
|
|
|
168
168
|
return False
|
|
169
169
|
return first_child.LocalizedControlType==child_control_type
|
|
170
170
|
|
|
171
|
-
def _dom_correction(self, node:Control, dom_interactive_nodes:list[TreeElementNode],
|
|
171
|
+
def _dom_correction(self, node:Control, dom_interactive_nodes:list[TreeElementNode], window_name:str):
|
|
172
172
|
if self.element_has_child_element(node,'list item','link') or self.element_has_child_element(node,'item','link'):
|
|
173
173
|
dom_interactive_nodes.pop()
|
|
174
174
|
return None
|
|
@@ -207,7 +207,7 @@ class Tree:
|
|
|
207
207
|
'bounding_box':bounding_box,
|
|
208
208
|
'xpath':'',
|
|
209
209
|
'center':center,
|
|
210
|
-
'
|
|
210
|
+
'window_name':window_name,
|
|
211
211
|
'is_focused':is_focused
|
|
212
212
|
}))
|
|
213
213
|
elif self.element_has_child_element(node,'link','heading'):
|
|
@@ -228,11 +228,11 @@ class Tree:
|
|
|
228
228
|
'bounding_box':bounding_box,
|
|
229
229
|
'xpath':'',
|
|
230
230
|
'center':center,
|
|
231
|
-
'
|
|
231
|
+
'window_name':window_name,
|
|
232
232
|
'is_focused':is_focused
|
|
233
233
|
}))
|
|
234
234
|
|
|
235
|
-
def tree_traversal(self, node: Control, window_bounding_box:Rect,
|
|
235
|
+
def tree_traversal(self, node: Control, window_bounding_box:Rect, window_name:str, is_browser:bool,
|
|
236
236
|
interactive_nodes:Optional[list[TreeElementNode]]=None, scrollable_nodes:Optional[list[ScrollElementNode]]=None,
|
|
237
237
|
dom_interactive_nodes:Optional[list[TreeElementNode]]=None, dom_informative_nodes:Optional[list[TextElementNode]]=None,
|
|
238
238
|
is_dom:bool=False, is_dialog:bool=False,
|
|
@@ -277,7 +277,7 @@ class Tree:
|
|
|
277
277
|
'horizontal_scroll_percent':scroll_pattern.HorizontalScrollPercent if scroll_pattern.HorizontallyScrollable else 0,
|
|
278
278
|
'vertical_scrollable':scroll_pattern.VerticallyScrollable,
|
|
279
279
|
'vertical_scroll_percent':scroll_pattern.VerticalScrollPercent if scroll_pattern.VerticallyScrollable else 0,
|
|
280
|
-
'
|
|
280
|
+
'window_name':window_name,
|
|
281
281
|
'is_focused':has_keyboard_focus
|
|
282
282
|
}))
|
|
283
283
|
except Exception:
|
|
@@ -365,11 +365,11 @@ class Tree:
|
|
|
365
365
|
'bounding_box':bounding_box,
|
|
366
366
|
'center':center,
|
|
367
367
|
'xpath':'',
|
|
368
|
-
'
|
|
368
|
+
'window_name':window_name,
|
|
369
369
|
'is_focused':is_focused
|
|
370
370
|
})
|
|
371
371
|
dom_interactive_nodes.append(tree_node)
|
|
372
|
-
self._dom_correction(node, dom_interactive_nodes,
|
|
372
|
+
self._dom_correction(node, dom_interactive_nodes, window_name)
|
|
373
373
|
else:
|
|
374
374
|
bounding_box=self.iou_bounding_box(window_bounding_box,element_bounding_box)
|
|
375
375
|
center = bounding_box.get_center()
|
|
@@ -381,7 +381,7 @@ class Tree:
|
|
|
381
381
|
'bounding_box':bounding_box,
|
|
382
382
|
'center':center,
|
|
383
383
|
'xpath':'',
|
|
384
|
-
'
|
|
384
|
+
'window_name':window_name,
|
|
385
385
|
'is_focused':is_focused
|
|
386
386
|
})
|
|
387
387
|
interactive_nodes.append(tree_node)
|
|
@@ -432,7 +432,7 @@ class Tree:
|
|
|
432
432
|
height=bounding_box.height())
|
|
433
433
|
self.dom=child
|
|
434
434
|
# enter DOM subtree
|
|
435
|
-
self.tree_traversal(child, window_bounding_box,
|
|
435
|
+
self.tree_traversal(child, window_bounding_box, window_name, is_browser, interactive_nodes, scrollable_nodes, dom_interactive_nodes, dom_informative_nodes, is_dom=True, is_dialog=is_dialog, element_cache_req=element_cache_req, children_cache_req=children_cache_req)
|
|
436
436
|
# Check if the child is a dialog
|
|
437
437
|
elif isinstance(child,WindowControl):
|
|
438
438
|
if not child.CachedIsOffscreen:
|
|
@@ -454,10 +454,10 @@ class Tree:
|
|
|
454
454
|
# Because this window element is modal
|
|
455
455
|
interactive_nodes.clear()
|
|
456
456
|
# enter dialog subtree
|
|
457
|
-
self.tree_traversal(child, window_bounding_box,
|
|
457
|
+
self.tree_traversal(child, window_bounding_box, window_name, is_browser, interactive_nodes, scrollable_nodes, dom_interactive_nodes, dom_informative_nodes, is_dom=is_dom, is_dialog=True, element_cache_req=element_cache_req, children_cache_req=children_cache_req)
|
|
458
458
|
else:
|
|
459
459
|
# normal non-dialog children
|
|
460
|
-
self.tree_traversal(child, window_bounding_box,
|
|
460
|
+
self.tree_traversal(child, window_bounding_box, window_name, is_browser, interactive_nodes, scrollable_nodes, dom_interactive_nodes, dom_informative_nodes, is_dom=is_dom, is_dialog=is_dialog, element_cache_req=element_cache_req, children_cache_req=children_cache_req)
|
|
461
461
|
except Exception as e:
|
|
462
462
|
logger.error(f"Error in tree_traversal: {e}", exc_info=True)
|
|
463
463
|
raise
|
|
@@ -491,11 +491,11 @@ class Tree:
|
|
|
491
491
|
window_bounding_box=node.BoundingRectangle
|
|
492
492
|
|
|
493
493
|
interactive_nodes, dom_interactive_nodes, dom_informative_nodes, scrollable_nodes = [], [], [], []
|
|
494
|
-
|
|
495
|
-
|
|
494
|
+
window_name=node.Name.strip()
|
|
495
|
+
window_name=self.app_name_correction(window_name)
|
|
496
496
|
|
|
497
|
-
self.tree_traversal(node, window_bounding_box,
|
|
498
|
-
logger.debug(f'
|
|
497
|
+
self.tree_traversal(node, window_bounding_box, window_name, is_browser, interactive_nodes, scrollable_nodes, dom_interactive_nodes, dom_informative_nodes, is_dom=False, is_dialog=False, element_cache_req=element_cache_req, children_cache_req=children_cache_req)
|
|
498
|
+
logger.debug(f'Window name:{window_name}')
|
|
499
499
|
logger.debug(f'Interactive nodes:{len(interactive_nodes)}')
|
|
500
500
|
if is_browser:
|
|
501
501
|
logger.debug(f'DOM interactive nodes:{len(dom_interactive_nodes)}')
|
windows_mcp/tree/utils.py
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import random
|
|
2
|
-
from windows_mcp.uia import Control
|
|
3
|
-
|
|
4
|
-
def random_point_within_bounding_box(node: Control, scale_factor: float = 1.0) -> tuple[int, int]:
|
|
5
|
-
"""
|
|
6
|
-
Generate a random point within a scaled-down bounding box.
|
|
7
|
-
|
|
8
|
-
Args:
|
|
9
|
-
node (Control): The node with a bounding rectangle
|
|
10
|
-
scale_factor (float, optional): The factor to scale down the bounding box. Defaults to 1.0.
|
|
11
|
-
|
|
12
|
-
Returns:
|
|
13
|
-
tuple: A random point (x, y) within the scaled-down bounding box
|
|
14
|
-
"""
|
|
15
|
-
box = node.BoundingRectangle
|
|
16
|
-
scaled_width = int(box.width() * scale_factor)
|
|
17
|
-
scaled_height = int(box.height() * scale_factor)
|
|
18
|
-
scaled_left = box.left + (box.width() - scaled_width) // 2
|
|
19
|
-
scaled_top = box.top + (box.height() - scaled_height) // 2
|
|
20
|
-
x = random.randint(scaled_left, scaled_left + scaled_width)
|
|
21
|
-
y = random.randint(scaled_top, scaled_top + scaled_height)
|
|
1
|
+
import random
|
|
2
|
+
from windows_mcp.uia import Control
|
|
3
|
+
|
|
4
|
+
def random_point_within_bounding_box(node: Control, scale_factor: float = 1.0) -> tuple[int, int]:
|
|
5
|
+
"""
|
|
6
|
+
Generate a random point within a scaled-down bounding box.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
node (Control): The node with a bounding rectangle
|
|
10
|
+
scale_factor (float, optional): The factor to scale down the bounding box. Defaults to 1.0.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
tuple: A random point (x, y) within the scaled-down bounding box
|
|
14
|
+
"""
|
|
15
|
+
box = node.BoundingRectangle
|
|
16
|
+
scaled_width = int(box.width() * scale_factor)
|
|
17
|
+
scaled_height = int(box.height() * scale_factor)
|
|
18
|
+
scaled_left = box.left + (box.width() - scaled_width) // 2
|
|
19
|
+
scaled_top = box.top + (box.height() - scaled_height) // 2
|
|
20
|
+
x = random.randint(scaled_left, scaled_left + scaled_width)
|
|
21
|
+
y = random.randint(scaled_top, scaled_top + scaled_height)
|
|
22
22
|
return (x, y)
|
windows_mcp/tree/views.py
CHANGED
|
@@ -15,10 +15,10 @@ class TreeState:
|
|
|
15
15
|
return "No interactive elements"
|
|
16
16
|
# TOON-like format: Pipe-separated values with clear header
|
|
17
17
|
# Using abbreviations in header to save tokens
|
|
18
|
-
header = "# id|
|
|
18
|
+
header = "# id|window|control_type|name|coords|focus"
|
|
19
19
|
rows = [header]
|
|
20
20
|
for idx, node in enumerate(self.interactive_nodes):
|
|
21
|
-
row = f"{idx}|{node.
|
|
21
|
+
row = f"{idx}|{node.window_name}|{node.control_type}|{node.name}|{node.center.to_string()}|{node.is_focused}"
|
|
22
22
|
rows.append(row)
|
|
23
23
|
return "\n".join(rows)
|
|
24
24
|
|
|
@@ -26,11 +26,11 @@ class TreeState:
|
|
|
26
26
|
if not self.scrollable_nodes:
|
|
27
27
|
return "No scrollable elements"
|
|
28
28
|
# TOON-like format
|
|
29
|
-
header = "# id|
|
|
29
|
+
header = "# id|window|control_type|name|coords|h_scroll|h_pct|v_scroll|v_pct|focus"
|
|
30
30
|
rows = [header]
|
|
31
31
|
base_index = len(self.interactive_nodes)
|
|
32
32
|
for idx, node in enumerate(self.scrollable_nodes):
|
|
33
|
-
row = (f"{base_index + idx}|{node.
|
|
33
|
+
row = (f"{base_index + idx}|{node.window_name}|{node.control_type}|{node.name}|"
|
|
34
34
|
f"{node.center.to_string()}|{node.horizontal_scrollable}|{node.horizontal_scroll_percent}|"
|
|
35
35
|
f"{node.vertical_scrollable}|{node.vertical_scroll_percent}|{node.is_focused}")
|
|
36
36
|
rows.append(row)
|
|
@@ -85,7 +85,7 @@ class TreeElementNode:
|
|
|
85
85
|
center: Center
|
|
86
86
|
name: str=''
|
|
87
87
|
control_type: str=''
|
|
88
|
-
|
|
88
|
+
window_name: str=''
|
|
89
89
|
value:str=''
|
|
90
90
|
shortcut: str=''
|
|
91
91
|
xpath:str=''
|
|
@@ -94,7 +94,7 @@ class TreeElementNode:
|
|
|
94
94
|
def update_from_node(self,node:'TreeElementNode'):
|
|
95
95
|
self.name=node.name
|
|
96
96
|
self.control_type=node.control_type
|
|
97
|
-
self.
|
|
97
|
+
self.window_name=node.window_name
|
|
98
98
|
self.value=node.value
|
|
99
99
|
self.shortcut=node.shortcut
|
|
100
100
|
self.bounding_box=node.bounding_box
|
|
@@ -104,14 +104,14 @@ class TreeElementNode:
|
|
|
104
104
|
|
|
105
105
|
# Legacy method kept for compatibility if needed, but not used in new format
|
|
106
106
|
def to_row(self, index: int):
|
|
107
|
-
return [index, self.
|
|
107
|
+
return [index, self.window_name, self.control_type, self.name, self.value, self.shortcut, self.center.to_string(),self.is_focused]
|
|
108
108
|
|
|
109
109
|
@dataclass
|
|
110
110
|
class ScrollElementNode:
|
|
111
111
|
name: str
|
|
112
112
|
control_type: str
|
|
113
113
|
xpath:str
|
|
114
|
-
|
|
114
|
+
window_name: str
|
|
115
115
|
bounding_box: BoundingBox
|
|
116
116
|
center: Center
|
|
117
117
|
horizontal_scrollable: bool
|
|
@@ -124,7 +124,7 @@ class ScrollElementNode:
|
|
|
124
124
|
def to_row(self, index: int, base_index: int):
|
|
125
125
|
return [
|
|
126
126
|
base_index + index,
|
|
127
|
-
self.
|
|
127
|
+
self.window_name,
|
|
128
128
|
self.control_type,
|
|
129
129
|
self.name,
|
|
130
130
|
self.center.to_string(),
|
windows_mcp/uia/controls.py
CHANGED
|
@@ -1458,48 +1458,6 @@ class Control():
|
|
|
1458
1458
|
self.SetFocus()
|
|
1459
1459
|
SendKeys(text, interval, waitTime, charMode)
|
|
1460
1460
|
|
|
1461
|
-
def GetPixelColor(self, x: int, y: int) -> Optional[int]:
|
|
1462
|
-
"""
|
|
1463
|
-
Call native `GetPixelColor` if control has a valid native handle.
|
|
1464
|
-
Use `self.ToBitmap` if control doesn't have a valid native handle or you get many pixels.
|
|
1465
|
-
x: int, internal x position.
|
|
1466
|
-
y: int, internal y position.
|
|
1467
|
-
Return int, a color value in bgr.
|
|
1468
|
-
r = bgr & 0x0000FF
|
|
1469
|
-
g = (bgr & 0x00FF00) >> 8
|
|
1470
|
-
b = (bgr & 0xFF0000) >> 16
|
|
1471
|
-
"""
|
|
1472
|
-
handle = self.NativeWindowHandle
|
|
1473
|
-
if handle:
|
|
1474
|
-
return GetPixelColor(x, y, handle)
|
|
1475
|
-
return None
|
|
1476
|
-
|
|
1477
|
-
def ToBitmap(self, x: int = 0, y: int = 0, width: int = 0, height: int = 0,
|
|
1478
|
-
captureCursor: bool = False) -> Optional[Bitmap]:
|
|
1479
|
-
"""
|
|
1480
|
-
Capture control to a `Bitmap` object.
|
|
1481
|
-
x, y: int, the point in control's internal position(from 0,0).
|
|
1482
|
-
width, height: int, image's width and height from x, y, use 0 for entire area.
|
|
1483
|
-
If width(or height) < 0, image size will be control's width(or height) - width(or height).
|
|
1484
|
-
"""
|
|
1485
|
-
return Bitmap.FromControl(self, x, y, width, height, captureCursor)
|
|
1486
|
-
|
|
1487
|
-
def CaptureToImage(self, savePath: str, x: int = 0, y: int = 0, width: int = 0, height: int = 0,
|
|
1488
|
-
captureCursor: bool = False) -> bool:
|
|
1489
|
-
"""
|
|
1490
|
-
Capture control to a image file.
|
|
1491
|
-
savePath: str, should end with .bmp, .jpg, .jpeg, .png, .gif, .tif, .tiff.
|
|
1492
|
-
x, y: int, the point in control's internal position(from 0,0).
|
|
1493
|
-
width, height: int, image's width and height from x, y, use 0 for entire area.
|
|
1494
|
-
If width(or height) < 0, image size will be control's width(or height) - width(or height).
|
|
1495
|
-
Return bool, True if succeed otherwise False.
|
|
1496
|
-
"""
|
|
1497
|
-
bitmap = Bitmap.FromControl(self, x, y, width, height, captureCursor)
|
|
1498
|
-
if bitmap:
|
|
1499
|
-
with bitmap:
|
|
1500
|
-
return bitmap.ToFile(savePath)
|
|
1501
|
-
return False
|
|
1502
|
-
|
|
1503
1461
|
def IsTopLevel(self) -> bool:
|
|
1504
1462
|
"""Determine whether current control is top level."""
|
|
1505
1463
|
handle = self.NativeWindowHandle
|