windows-mcp 0.6.0__py3-none-any.whl → 0.6.2__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.
@@ -31,26 +31,26 @@ class Tree:
31
31
  self.tree_state=None
32
32
 
33
33
 
34
- def get_state(self,active_app_handle:int|None,other_apps_handles:list[int],use_dom:bool=False)->TreeState:
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
- active_app_flag=False
41
- if active_app_handle:
42
- active_app_flag=True
43
- apps_handles=[active_app_handle]+other_apps_handles
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
- apps_handles=other_apps_handles
45
+ windows_handles=other_windows_handles
46
46
 
47
- interactive_nodes,scrollable_nodes,dom_informative_nodes=self.get_appwise_nodes(apps_handles=apps_handles,active_app_flag=active_app_flag,use_dom=use_dom)
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
- app_name="Desktop",
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
- app_name="DOM",
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 get_appwise_nodes(self,apps_handles:list[int],active_app_flag:bool,use_dom:bool=False) -> tuple[list[TreeElementNode],list[ScrollElementNode],list[TextElementNode]]:
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 apps_handles:
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 active_app_flag and temp_node.ClassName=='Progman':
92
+ if active_window_flag and temp_node.ClassName=='Progman':
93
93
  continue
94
- is_browser = self.desktop.is_app_browser(temp_node)
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 apps_handles}
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], app_name:str):
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
- 'app_name':app_name,
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
- 'app_name':app_name,
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, app_name:str, is_browser:bool,
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
- 'app_name':app_name,
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
- 'app_name':app_name,
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, app_name)
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
- 'app_name':app_name,
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, app_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)
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, app_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)
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, app_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)
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
- app_name=node.Name.strip()
495
- app_name=self.app_name_correction(app_name)
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, app_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'App name:{app_name}')
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|app|type|name|coords|focus"
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.app_name}|{node.control_type}|{node.name}|{node.center.to_string()}|{node.is_focused}"
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|app|type|name|coords|h_scroll|h_pct|v_scroll|v_pct|focus"
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.app_name}|{node.control_type}|{node.name}|"
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
- app_name: str=''
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.app_name=node.app_name
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.app_name, self.control_type, self.name, self.value, self.shortcut, self.center.to_string(),self.is_focused]
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
- app_name: str
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.app_name,
127
+ self.window_name,
128
128
  self.control_type,
129
129
  self.name,
130
130
  self.center.to_string(),
@@ -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