python-win-windows-manager 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.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-win-windows-manager
3
+ Version: 0.1.0
4
+ Summary: Window manager and monitor for Windows
5
+ Project-URL: Homepage, https://github.com/username/repo
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: psutil>=7.2.2
8
+ Requires-Dist: pywin32>=311
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Windows Window Manager
12
+
13
+ A Python library for managing Windows windows using `win32gui` and `ctypes`. This project provides a robust interface for identifying, controlling, and monitoring windows on Windows systems.
14
+
15
+ ## Features
16
+
17
+ - **Window Identification**: Get detailed information about all open windows (Handle, Title, Class Name, PID, Process Name).
18
+ - **Window Search**: Find windows by title or class name with exact or partial matching.
19
+ - **Window Control**:
20
+ - Minimize, Maximize, Restore
21
+ - Close
22
+ - Move and Resize
23
+ - Set to Foreground
24
+ - **Real-time Monitoring**: Monitor window creation, destruction, and state changes.
25
+ - **Robust Error Handling**: Handles permissions and invalid handles gracefully.
26
+
27
+ ## Installation
28
+
29
+ This project uses `uv` for dependency management.
30
+
31
+ 1. Clone the repository.
32
+ 2. Install dependencies:
33
+ ```bash
34
+ uv sync
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Basic Example
40
+
41
+ ```python
42
+ from window_manager import WindowManager
43
+
44
+ manager = WindowManager()
45
+
46
+ # List all visible windows
47
+ windows = manager.get_all_windows(visible_only=True)
48
+ for window in windows:
49
+ print(window)
50
+
51
+ # Find a window
52
+ notepad = manager.find_windows(title="Notepad")[0]
53
+
54
+ # Minimize
55
+ manager.minimize_window(notepad.handle)
56
+
57
+ # Restore
58
+ manager.restore_window(notepad.handle)
59
+ ```
60
+
61
+ ### Monitoring Example
62
+
63
+ ```python
64
+ import time
65
+ from window_manager import WindowMonitor
66
+
67
+ def callback(event, hwnd, title):
68
+ print(f"Event: {event}, Window: {title}")
69
+
70
+ monitor = WindowMonitor(callback=callback)
71
+ monitor.start()
72
+
73
+ try:
74
+ while True:
75
+ time.sleep(1)
76
+ except KeyboardInterrupt:
77
+ monitor.stop()
78
+ ```
79
+
80
+ ## Running Tests
81
+
82
+ ```bash
83
+ uv run pytest tests
84
+ ```
85
+
86
+ ## Documentation
87
+
88
+ See [docs/API.md](docs/API.md) for detailed API documentation.
@@ -0,0 +1,8 @@
1
+ window_manager/__init__.py,sha256=FxlCI6bfeJ5cHQkSju9MsCd_vGV9f93J6CODRtNV4zc,206
2
+ window_manager/core.py,sha256=ARqSygz2h60acJRVLFf4-X4jwQn47Sv8uq-8PLa8pA8,8228
3
+ window_manager/models.py,sha256=ww4xM4rKU61YFM7MFMkQ5xBxQ_zN5vLxT4ekg4WJdSw,599
4
+ window_manager/monitor.py,sha256=xJ1m62war7U-h-g5qBHgYhhLaQnxYmsOPHi__mkIRs8,5270
5
+ window_manager/utils.py,sha256=ydugYw0ZBnOUHzMi6hJwYvIJZv3tl4GDxumkgnKSDLA,1099
6
+ python_win_windows_manager-0.1.0.dist-info/METADATA,sha256=t30vUbsvNfTUzwgI7jAkbMnZJqIpUlA8Z5LRKIOGH-0,2025
7
+ python_win_windows_manager-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ python_win_windows_manager-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,6 @@
1
+ from .core import WindowManager
2
+ from .models import WindowInfo
3
+ from .monitor import WindowMonitor
4
+ from .utils import setup_logger
5
+
6
+ __all__ = ["WindowManager", "WindowInfo", "WindowMonitor", "setup_logger"]
window_manager/core.py ADDED
@@ -0,0 +1,228 @@
1
+ import logging
2
+ import win32gui
3
+ import win32process
4
+ import win32con
5
+ import psutil
6
+ from typing import List, Optional, Tuple, Dict, Any
7
+ from .models import WindowInfo
8
+ from .utils import setup_logger
9
+
10
+ logger = setup_logger()
11
+
12
+ class WindowManagerError(Exception):
13
+ """Base exception for WindowManager errors."""
14
+ pass
15
+
16
+ class WindowManager:
17
+ """
18
+ Core class for managing Windows windows.
19
+ """
20
+
21
+ def __init__(self):
22
+ """Initializes the WindowManager."""
23
+ self._logger = logger
24
+
25
+ def get_all_windows(self, visible_only: bool = True) -> List[WindowInfo]:
26
+ """
27
+ Retrieves all open windows.
28
+
29
+ Args:
30
+ visible_only: If True, only returns visible windows.
31
+
32
+ Returns:
33
+ A list of WindowInfo objects.
34
+ """
35
+ windows: List[WindowInfo] = []
36
+
37
+ def enum_handler(hwnd, ctx):
38
+ if visible_only and not win32gui.IsWindowVisible(hwnd):
39
+ return
40
+
41
+ try:
42
+ title = win32gui.GetWindowText(hwnd)
43
+ class_name = win32gui.GetClassName(hwnd)
44
+
45
+ # Get process ID
46
+ _, pid = win32process.GetWindowThreadProcessId(hwnd)
47
+
48
+ # Get process name
49
+ try:
50
+ process = psutil.Process(pid)
51
+ process_name = process.name()
52
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
53
+ process_name = "Unknown"
54
+
55
+ # Get window rect
56
+ try:
57
+ rect = win32gui.GetWindowRect(hwnd)
58
+ except Exception:
59
+ rect = None
60
+
61
+ window_info = WindowInfo(
62
+ handle=hwnd,
63
+ title=title,
64
+ class_name=class_name,
65
+ pid=pid,
66
+ process_name=process_name,
67
+ rect=rect,
68
+ is_visible=win32gui.IsWindowVisible(hwnd)
69
+ )
70
+ windows.append(window_info)
71
+ except Exception as e:
72
+ self._logger.warning(f"Error processing window {hwnd}: {e}")
73
+
74
+ try:
75
+ win32gui.EnumWindows(enum_handler, None)
76
+ except Exception as e:
77
+ self._logger.error(f"Failed to enumerate windows: {e}")
78
+ raise WindowManagerError(f"Failed to enumerate windows: {e}")
79
+
80
+ return windows
81
+
82
+ def find_windows(self, title: Optional[str] = None, class_name: Optional[str] = None, process_name: Optional[str] = None, exact_match: bool = False) -> List[WindowInfo]:
83
+ """
84
+ Finds windows matching the given criteria.
85
+
86
+ Args:
87
+ title: The window title to search for.
88
+ class_name: The window class name to search for.
89
+ exact_match: If True, requires exact string match. Otherwise, checks for containment.
90
+
91
+ Returns:
92
+ A list of matching WindowInfo objects.
93
+ """
94
+ all_windows = self.get_all_windows(visible_only=False)
95
+ matched_windows = []
96
+
97
+ for window in all_windows:
98
+ match = True
99
+
100
+ if title:
101
+ if exact_match:
102
+ if window.title != title:
103
+ match = False
104
+ else:
105
+ if title.lower() not in window.title.lower():
106
+ match = False
107
+
108
+ if match and class_name:
109
+ if exact_match:
110
+ if window.class_name != class_name:
111
+ match = False
112
+ else:
113
+ if class_name.lower() not in window.class_name.lower():
114
+ match = False
115
+
116
+ if match and process_name:
117
+ if window.process_name is None or window.process_name.lower() != process_name.lower():
118
+ match = False
119
+
120
+ if match and (title or class_name or process_name):
121
+ matched_windows.append(window)
122
+
123
+ return matched_windows
124
+
125
+ def get_window_by_handle(self, handle: int) -> Optional[WindowInfo]:
126
+ """
127
+ Retrieves window information by handle.
128
+
129
+ Args:
130
+ handle: The window handle.
131
+
132
+ Returns:
133
+ A WindowInfo object if found, otherwise None.
134
+ """
135
+ if not win32gui.IsWindow(handle):
136
+ return None
137
+
138
+ try:
139
+ title = win32gui.GetWindowText(handle)
140
+ class_name = win32gui.GetClassName(handle)
141
+ _, pid = win32process.GetWindowThreadProcessId(handle)
142
+
143
+ try:
144
+ process = psutil.Process(pid)
145
+ process_name = process.name()
146
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
147
+ process_name = "Unknown"
148
+
149
+ try:
150
+ rect = win32gui.GetWindowRect(handle)
151
+ except Exception:
152
+ rect = None
153
+
154
+ return WindowInfo(
155
+ handle=handle,
156
+ title=title,
157
+ class_name=class_name,
158
+ pid=pid,
159
+ process_name=process_name,
160
+ rect=rect,
161
+ is_visible=win32gui.IsWindowVisible(handle)
162
+ )
163
+ except Exception as e:
164
+ self._logger.error(f"Error getting window info for handle {handle}: {e}")
165
+ return None
166
+
167
+ def close_window(self, handle: int) -> bool:
168
+ """Closes the window with the given handle."""
169
+ try:
170
+ win32gui.PostMessage(handle, win32con.WM_CLOSE, 0, 0)
171
+ self._logger.info(f"Closed window {handle}")
172
+ return True
173
+ except Exception as e:
174
+ self._logger.error(f"Failed to close window {handle}: {e}")
175
+ return False
176
+
177
+ def minimize_window(self, handle: int) -> bool:
178
+ """Minimizes the window."""
179
+ try:
180
+ win32gui.ShowWindow(handle, win32con.SW_MINIMIZE)
181
+ self._logger.info(f"Minimized window {handle}")
182
+ return True
183
+ except Exception as e:
184
+ self._logger.error(f"Failed to minimize window {handle}: {e}")
185
+ return False
186
+
187
+ def maximize_window(self, handle: int) -> bool:
188
+ """Maximizes the window."""
189
+ try:
190
+ win32gui.ShowWindow(handle, win32con.SW_MAXIMIZE)
191
+ self._logger.info(f"Maximized window {handle}")
192
+ return True
193
+ except Exception as e:
194
+ self._logger.error(f"Failed to maximize window {handle}: {e}")
195
+ return False
196
+
197
+ def restore_window(self, handle: int) -> bool:
198
+ """Restores the window."""
199
+ try:
200
+ win32gui.ShowWindow(handle, win32con.SW_RESTORE)
201
+ self._logger.info(f"Restored window {handle}")
202
+ return True
203
+ except Exception as e:
204
+ self._logger.error(f"Failed to restore window {handle}: {e}")
205
+ return False
206
+
207
+ def move_window(self, handle: int, x: int, y: int, width: int, height: int) -> bool:
208
+ """Moves and resizes the window."""
209
+ try:
210
+ win32gui.MoveWindow(handle, x, y, width, height, True)
211
+ self._logger.info(f"Moved window {handle} to ({x}, {y}, {width}, {height})")
212
+ return True
213
+ except Exception as e:
214
+ self._logger.error(f"Failed to move window {handle}: {e}")
215
+ return False
216
+
217
+ def set_foreground(self, handle: int) -> bool:
218
+ """Brings the window to the foreground."""
219
+ try:
220
+ # Sometimes setting foreground requires specific conditions or tricks
221
+ # like attaching thread input if the foreground lock timeout is set.
222
+ # For simplicity, we just try SetForegroundWindow.
223
+ win32gui.SetForegroundWindow(handle)
224
+ self._logger.info(f"Set window {handle} to foreground")
225
+ return True
226
+ except Exception as e:
227
+ self._logger.error(f"Failed to set foreground window {handle}: {e}")
228
+ return False
@@ -0,0 +1,20 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+ @dataclass
5
+ class WindowInfo:
6
+ """
7
+ Represents information about a window.
8
+ """
9
+ handle: int
10
+ title: str
11
+ class_name: str
12
+ pid: int
13
+ process_name: Optional[str] = None
14
+ rect: Optional[tuple[int, int, int, int]] = None # (left, top, right, bottom)
15
+ is_visible: bool = False
16
+
17
+ def __str__(self):
18
+ return (f"Window(handle={self.handle}, title='{self.title}', "
19
+ f"class='{self.class_name}', pid={self.pid}, "
20
+ f"process='{self.process_name}')")
@@ -0,0 +1,164 @@
1
+ import ctypes
2
+ from ctypes import wintypes
3
+ import threading
4
+ import time
5
+ import win32con
6
+ import win32api
7
+ import win32gui
8
+ from typing import Callable, Optional
9
+ from .utils import setup_logger
10
+
11
+ logger = setup_logger("WindowMonitor")
12
+
13
+ # Constants for SetWinEventHook
14
+ EVENT_MIN = 0x00000001
15
+ EVENT_MAX = 0x7FFFFFFF
16
+ EVENT_SYSTEM_FOREGROUND = 0x0003
17
+ EVENT_OBJECT_CREATE = 0x8000
18
+ EVENT_OBJECT_DESTROY = 0x8001
19
+ EVENT_OBJECT_SHOW = 0x8002
20
+ EVENT_OBJECT_HIDE = 0x8003
21
+ EVENT_OBJECT_NAMECHANGE = 0x800C
22
+ WINEVENT_OUTOFCONTEXT = 0x0000
23
+
24
+ # Callback function type
25
+ WinEventProcType = ctypes.WINFUNCTYPE(
26
+ None,
27
+ wintypes.HANDLE,
28
+ wintypes.DWORD,
29
+ wintypes.HWND,
30
+ wintypes.LONG,
31
+ wintypes.LONG,
32
+ wintypes.DWORD,
33
+ wintypes.DWORD
34
+ )
35
+
36
+ user32 = ctypes.windll.user32
37
+
38
+ class WindowMonitor:
39
+ """
40
+ Monitors window events such as creation, destruction, and state changes.
41
+ """
42
+
43
+ def __init__(self, callback: Optional[Callable[[str, int, int], None]] = None):
44
+ """
45
+ Initializes the WindowMonitor.
46
+
47
+ Args:
48
+ callback: A function to call when an event occurs.
49
+ Signature: callback(event_name, hwnd, id_object)
50
+ """
51
+ self._callback = callback
52
+ self._hook = None
53
+ self._thread = None
54
+ self._stop_event = threading.Event()
55
+ self._logger = logger
56
+
57
+ def _event_handler(self, hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime):
58
+ """
59
+ Internal callback for SetWinEventHook.
60
+ """
61
+ if idObject != 0: # OBJID_WINDOW = 0
62
+ return
63
+
64
+ event_name = "Unknown"
65
+ if event == EVENT_OBJECT_CREATE:
66
+ event_name = "Create"
67
+ elif event == EVENT_OBJECT_DESTROY:
68
+ event_name = "Destroy"
69
+ elif event == EVENT_OBJECT_SHOW:
70
+ event_name = "Show"
71
+ elif event == EVENT_OBJECT_HIDE:
72
+ event_name = "Hide"
73
+ elif event == EVENT_SYSTEM_FOREGROUND:
74
+ event_name = "Foreground"
75
+ elif event == EVENT_OBJECT_NAMECHANGE:
76
+ event_name = "NameChange"
77
+
78
+ # Only log/callback for interesting events on actual windows
79
+ # Filter out some noise if necessary, but for now report all OBJID_WINDOW
80
+
81
+ # We can try to get window title here, but be careful as the window might be destroying
82
+ title = ""
83
+ try:
84
+ if event != EVENT_OBJECT_DESTROY:
85
+ # win32gui.GetWindowText might fail if window is gone
86
+ length = user32.GetWindowTextLengthW(hwnd)
87
+ buff = ctypes.create_unicode_buffer(length + 1)
88
+ user32.GetWindowTextW(hwnd, buff, length + 1)
89
+ title = buff.value
90
+ except Exception:
91
+ pass
92
+
93
+ log_msg = f"Event: {event_name}, HWND: {hwnd}, Title: {title}"
94
+ self._logger.info(log_msg)
95
+
96
+ if self._callback:
97
+ try:
98
+ self._callback(event_name, hwnd, title)
99
+ except Exception as e:
100
+ self._logger.error(f"Error in user callback: {e}")
101
+
102
+ def _run(self):
103
+ """
104
+ The thread function that runs the message loop.
105
+ """
106
+ self._logger.info("Starting monitor thread...")
107
+
108
+ # Keep reference to the callback to prevent garbage collection
109
+ self._c_callback = WinEventProcType(self._event_handler)
110
+
111
+ # Set hook
112
+ self._hook = user32.SetWinEventHook(
113
+ EVENT_MIN,
114
+ EVENT_MAX,
115
+ 0,
116
+ self._c_callback,
117
+ 0,
118
+ 0,
119
+ WINEVENT_OUTOFCONTEXT
120
+ )
121
+
122
+ if not self._hook:
123
+ self._logger.error("Failed to set win event hook")
124
+ return
125
+
126
+ # Message loop
127
+ msg = wintypes.MSG()
128
+ while not self._stop_event.is_set():
129
+ # PeekMessage is non-blocking, so we can check stop_event
130
+ # But SetWinEventHook needs a message pump.
131
+ # GetMessage blocks.
132
+ # We can use MsgWaitForMultipleObjects or just PeekMessage with a sleep.
133
+
134
+ if user32.PeekMessageW(ctypes.byref(msg), 0, 0, 0, 1): # PM_REMOVE = 1
135
+ if msg.message == win32con.WM_QUIT:
136
+ break
137
+ user32.TranslateMessage(ctypes.byref(msg))
138
+ user32.DispatchMessageW(ctypes.byref(msg))
139
+ else:
140
+ time.sleep(0.01) # Small sleep to avoid CPU spin
141
+
142
+ # Unhook
143
+ user32.UnhookWinEvent(self._hook)
144
+ self._logger.info("Monitor thread stopped.")
145
+
146
+ def start(self):
147
+ """Starts the monitoring thread."""
148
+ if self._thread and self._thread.is_alive():
149
+ self._logger.warning("Monitor already running")
150
+ return
151
+
152
+ self._stop_event.clear()
153
+ self._thread = threading.Thread(target=self._run, daemon=True)
154
+ self._thread.start()
155
+
156
+ def stop(self):
157
+ """Stops the monitoring thread."""
158
+ if not self._thread or not self._thread.is_alive():
159
+ return
160
+
161
+ self._stop_event.set()
162
+ self._thread.join(timeout=2)
163
+ if self._thread.is_alive():
164
+ self._logger.warning("Monitor thread did not stop gracefully")
@@ -0,0 +1,38 @@
1
+ import logging
2
+ import sys
3
+ from typing import Optional
4
+
5
+ def setup_logger(name: str = "WindowManager", level: int = logging.INFO, log_file: Optional[str] = None) -> logging.Logger:
6
+ """
7
+ Sets up a logger with the specified name, level, and optional file output.
8
+
9
+ Args:
10
+ name: The name of the logger.
11
+ level: The logging level.
12
+ log_file: Optional path to a log file.
13
+
14
+ Returns:
15
+ A configured logging.Logger instance.
16
+ """
17
+ logger = logging.getLogger(name)
18
+ logger.setLevel(level)
19
+
20
+ if logger.hasHandlers():
21
+ return logger
22
+
23
+ formatter = logging.Formatter(
24
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
25
+ )
26
+
27
+ # Console handler
28
+ console_handler = logging.StreamHandler(sys.stdout)
29
+ console_handler.setFormatter(formatter)
30
+ logger.addHandler(console_handler)
31
+
32
+ # File handler
33
+ if log_file:
34
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
35
+ file_handler.setFormatter(formatter)
36
+ logger.addHandler(file_handler)
37
+
38
+ return logger