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.
- python_win_windows_manager-0.1.0.dist-info/METADATA +88 -0
- python_win_windows_manager-0.1.0.dist-info/RECORD +8 -0
- python_win_windows_manager-0.1.0.dist-info/WHEEL +4 -0
- window_manager/__init__.py +6 -0
- window_manager/core.py +228 -0
- window_manager/models.py +20 -0
- window_manager/monitor.py +164 -0
- window_manager/utils.py +38 -0
|
@@ -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,,
|
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
|
window_manager/models.py
ADDED
|
@@ -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")
|
window_manager/utils.py
ADDED
|
@@ -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
|