win-mouse-listener 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,102 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Non-blocking Windows mouse listener (Raw Input based).
3
+
4
+ Public API mirrors the shape of the `mouse` library's event protocol but
5
+ never installs a hook, so callbacks cannot block the mouse.
6
+
7
+ Quick start
8
+ -----------
9
+ import win_mouse_listen as wm
10
+
11
+ wm.hook(lambda ev: print(ev)) # auto-starts on first hook
12
+ ...
13
+ wm.unhook_all()
14
+ wm.stop()
15
+ """
16
+ import threading as _threading
17
+
18
+ from ._types import (
19
+ LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, VERTICAL, HORIZONTAL,
20
+ ButtonEvent, WheelEvent, MoveEvent,
21
+ )
22
+ from ._listener import Listener
23
+
24
+ __version__ = '0.1.0'
25
+
26
+ __all__ = [
27
+ 'LEFT', 'RIGHT', 'MIDDLE', 'X', 'X2', 'UP', 'DOWN',
28
+ 'VERTICAL', 'HORIZONTAL',
29
+ 'ButtonEvent', 'WheelEvent', 'MoveEvent',
30
+ 'Listener',
31
+ 'start', 'stop',
32
+ 'hook', 'unhook', 'unhook_all',
33
+ 'poll', 'get_events', 'clear_buffer', 'pending_count',
34
+ 'is_pressed', 'position',
35
+ ]
36
+
37
+ _default = None
38
+ _default_lock = _threading.Lock()
39
+
40
+
41
+ def _get_default():
42
+ global _default
43
+ with _default_lock:
44
+ if _default is None:
45
+ _default = Listener()
46
+ return _default
47
+
48
+
49
+ def start():
50
+ """Start the default listener (no-op if already running)."""
51
+ _get_default().start()
52
+
53
+
54
+ def stop():
55
+ """Stop and reset the default listener."""
56
+ global _default
57
+ with _default_lock:
58
+ if _default is not None:
59
+ _default.stop()
60
+ _default = None
61
+
62
+
63
+ def hook(callback):
64
+ """Register a callback. Returns the callback for chaining.
65
+
66
+ The default listener starts automatically on first use.
67
+ """
68
+ return _get_default().hook(callback)
69
+
70
+
71
+ def unhook(callback):
72
+ _get_default().unhook(callback)
73
+
74
+
75
+ def unhook_all():
76
+ _get_default().unhook_all()
77
+
78
+
79
+ def poll():
80
+ """Non-blocking pop of one event. Returns None if empty."""
81
+ return _get_default().poll()
82
+
83
+
84
+ def get_events(max_count=None):
85
+ """Non-blocking pop of multiple events."""
86
+ return _get_default().get_events(max_count)
87
+
88
+
89
+ def clear_buffer():
90
+ _get_default().clear_buffer()
91
+
92
+
93
+ def pending_count():
94
+ return _get_default().pending_count()
95
+
96
+
97
+ def is_pressed(button=LEFT):
98
+ return _get_default().is_pressed(button)
99
+
100
+
101
+ def position():
102
+ return _get_default().position()
@@ -0,0 +1,366 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Listener implementation using Raw Input."""
3
+ import ctypes
4
+ import threading
5
+ import time as _time
6
+ import traceback as _tb
7
+ from collections import deque
8
+
9
+ from ._types import (
10
+ ButtonEvent, WheelEvent, MoveEvent,
11
+ LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN,
12
+ )
13
+ from . import _win32 as w
14
+
15
+
16
+ class Listener(object):
17
+ """Non-blocking Windows mouse listener backed by Raw Input.
18
+
19
+ Raw Input does not sit in the mouse event chain, so callbacks can be as
20
+ slow as you like without ever affecting the mouse cursor.
21
+ """
22
+
23
+ def __init__(self, buffer_size=16384, dispatch_queue_size=16384,
24
+ debug=False):
25
+ self._buffer = deque(maxlen=buffer_size)
26
+ self._dispatch_queue = deque(maxlen=dispatch_queue_size)
27
+ self._pressed = set()
28
+ self._handlers = []
29
+
30
+ self._shutdown = threading.Event()
31
+ self._ready = threading.Event()
32
+ self._running = False
33
+ self._error = None
34
+ self._debug = debug
35
+
36
+ self._hwnd = None
37
+ self._hinstance = None
38
+ self._class_name = None
39
+ self._wndproc_ref = None
40
+ self._thread = None
41
+ self._thread_id = 0
42
+ self._dispatcher = None
43
+
44
+ # ------------------------------------------------------------------ 生命周期
45
+ def start(self):
46
+ if self._running:
47
+ return
48
+ self._running = True
49
+ self._error = None
50
+ self._shutdown.clear()
51
+ self._ready.clear()
52
+
53
+ self._thread = threading.Thread(
54
+ target=self._message_loop_thread,
55
+ name='win-mouse-listen', daemon=True)
56
+ self._thread.start()
57
+
58
+ self._dispatcher = threading.Thread(
59
+ target=self._dispatcher_loop,
60
+ name='win-mouse-dispatch', daemon=True)
61
+ self._dispatcher.start()
62
+
63
+ if not self._ready.wait(timeout=3.0):
64
+ raise RuntimeError(
65
+ "Listener thread did not become ready in time")
66
+ if self._error:
67
+ raise RuntimeError(
68
+ "Failed to start listener: " + self._error)
69
+
70
+ def stop(self, timeout=2.0):
71
+ if not self._running:
72
+ return
73
+ self._shutdown.set()
74
+
75
+ if self._thread_id:
76
+ w._user32.PostThreadMessageW(self._thread_id, w.WM_QUIT, 0, 0)
77
+
78
+ if self._thread is not None:
79
+ self._thread.join(timeout=timeout)
80
+ self._thread = None
81
+ if self._dispatcher is not None:
82
+ self._dispatcher.join(timeout=timeout)
83
+ self._dispatcher = None
84
+
85
+ self._running = False
86
+
87
+ def __enter__(self):
88
+ self.start()
89
+ return self
90
+
91
+ def __exit__(self, exc_type, exc, tb):
92
+ self.stop()
93
+ return False
94
+
95
+ # ------------------------------------------------------------------ 消息循环线程
96
+ def _message_loop_thread(self):
97
+ try:
98
+ self._thread_id = w._kernel32.GetCurrentThreadId()
99
+ self._hinstance = w._kernel32.GetModuleHandleW(None)
100
+
101
+ def wnd_proc(hwnd, msg, wparam, lparam):
102
+ return self._on_message(hwnd, msg, wparam, lparam)
103
+
104
+ self._wndproc_ref = w.WNDPROC(wnd_proc)
105
+
106
+ self._class_name = 'WinMouseListen_{}_{}'.format(
107
+ id(self), self._thread_id)
108
+
109
+ wc = w.WNDCLASSEXW()
110
+ wc.cbSize = ctypes.sizeof(w.WNDCLASSEXW)
111
+ wc.style = 0
112
+ wc.lpfnWndProc = self._wndproc_ref
113
+ wc.cbClsExtra = 0
114
+ wc.cbWndExtra = 0
115
+ wc.hInstance = self._hinstance
116
+ wc.hIcon = None
117
+ wc.hCursor = None
118
+ wc.hbrBackground = None
119
+ wc.lpszMenuName = None
120
+ wc.lpszClassName = self._class_name
121
+ wc.hIconSm = None
122
+
123
+ atom = w._user32.RegisterClassExW(ctypes.byref(wc))
124
+ if not atom:
125
+ self._error = 'RegisterClassExW failed: {}'.format(
126
+ ctypes.get_last_error())
127
+ self._ready.set()
128
+ return
129
+
130
+ hwnd = w._user32.CreateWindowExW(
131
+ 0, self._class_name, '', 0,
132
+ 0, 0, 0, 0,
133
+ w.HWND_MESSAGE, None, self._hinstance, None,
134
+ )
135
+ if not hwnd:
136
+ self._error = 'CreateWindowExW failed: {}'.format(
137
+ ctypes.get_last_error())
138
+ self._ready.set()
139
+ return
140
+ self._hwnd = hwnd
141
+
142
+ rid = w.RAWINPUTDEVICE()
143
+ rid.usUsagePage = w.HID_USAGE_PAGE_GENERIC
144
+ rid.usUsage = w.HID_USAGE_GENERIC_MOUSE
145
+ rid.dwFlags = w.RIDEV_INPUTSINK
146
+ rid.hwndTarget = hwnd
147
+
148
+ ok = w._user32.RegisterRawInputDevices(
149
+ ctypes.byref(rid), 1, ctypes.sizeof(w.RAWINPUTDEVICE))
150
+ if not ok:
151
+ self._error = 'RegisterRawInputDevices failed: {}'.format(
152
+ ctypes.get_last_error())
153
+ self._ready.set()
154
+ return
155
+
156
+ # 强制为当前线程创建消息队列,确保 PostThreadMessageW 生效
157
+ msg = w.MSG()
158
+ w._user32.PeekMessageW(ctypes.byref(msg), None, 0, 0, 0)
159
+
160
+ self._ready.set()
161
+
162
+ if self._debug:
163
+ import sys
164
+ sys.stderr.write(
165
+ '[win_mouse_listen] listening hwnd={:#x} tid={}\n'
166
+ .format(hwnd, self._thread_id))
167
+ sys.stderr.flush()
168
+
169
+ while True:
170
+ ret = w._user32.GetMessageW(ctypes.byref(msg), None, 0, 0)
171
+ if ret <= 0: # 0 = WM_QUIT, -1 = error
172
+ break
173
+ w._user32.TranslateMessage(ctypes.byref(msg))
174
+ w._user32.DispatchMessageW(ctypes.byref(msg))
175
+
176
+ except Exception:
177
+ self._error = _tb.format_exc()
178
+ self._ready.set()
179
+
180
+ finally:
181
+ if self._hwnd:
182
+ try:
183
+ w._user32.DestroyWindow(self._hwnd)
184
+ except Exception:
185
+ pass
186
+ self._hwnd = None
187
+ if self._class_name and self._hinstance:
188
+ try:
189
+ w._user32.UnregisterClassW(
190
+ self._class_name, self._hinstance)
191
+ except Exception:
192
+ pass
193
+ self._class_name = None
194
+
195
+ # ------------------------------------------------------------------ WndProc
196
+ def _on_message(self, hwnd, msg, wparam, lparam):
197
+ if msg == w.WM_INPUT:
198
+ try:
199
+ self._handle_wm_input(lparam)
200
+ except Exception:
201
+ if self._debug:
202
+ _tb.print_exc()
203
+ return 0
204
+ if msg == w.WM_DESTROY:
205
+ w._user32.PostQuitMessage(0)
206
+ return 0
207
+ return w._user32.DefWindowProcW(hwnd, msg, wparam, lparam)
208
+
209
+ # ------------------------------------------------------------------ Raw Input 解析
210
+ def _handle_wm_input(self, hrawinput):
211
+ size = ctypes.c_uint(0)
212
+ header_size = ctypes.sizeof(w.RAWINPUTHEADER)
213
+
214
+ # 第一次调用:取所需缓冲区大小
215
+ res = w._user32.GetRawInputData(
216
+ hrawinput, w.RID_INPUT,
217
+ None, ctypes.byref(size), header_size,
218
+ )
219
+ if res == 0xFFFFFFFF or size.value < ctypes.sizeof(w.RAWINPUT):
220
+ return
221
+
222
+ buf = ctypes.create_string_buffer(size.value)
223
+
224
+ # 第二次调用:真正读取
225
+ res = w._user32.GetRawInputData(
226
+ hrawinput, w.RID_INPUT,
227
+ buf, ctypes.byref(size), header_size,
228
+ )
229
+ if res == 0xFFFFFFFF:
230
+ return
231
+
232
+ raw = ctypes.cast(
233
+ buf, ctypes.POINTER(w.RAWINPUT)).contents
234
+ if raw.header.dwType != w.RIM_TYPEMOUSE:
235
+ return
236
+
237
+ self._process_mouse(raw.mouse)
238
+
239
+ def _process_mouse(self, mouse):
240
+ t = _time.time()
241
+ bf = mouse.buttons.button_flags.usButtonFlags
242
+ bd = mouse.buttons.button_flags.usButtonData
243
+ flags = mouse.usFlags
244
+ dx, dy = mouse.lLastX, mouse.lLastY
245
+
246
+ events = []
247
+
248
+ if bf & w.RI_MOUSE_LEFT_BUTTON_DOWN:
249
+ events.append(ButtonEvent(DOWN, LEFT, t))
250
+ if bf & w.RI_MOUSE_LEFT_BUTTON_UP:
251
+ events.append(ButtonEvent(UP, LEFT, t))
252
+ if bf & w.RI_MOUSE_RIGHT_BUTTON_DOWN:
253
+ events.append(ButtonEvent(DOWN, RIGHT, t))
254
+ if bf & w.RI_MOUSE_RIGHT_BUTTON_UP:
255
+ events.append(ButtonEvent(UP, RIGHT, t))
256
+ if bf & w.RI_MOUSE_MIDDLE_BUTTON_DOWN:
257
+ events.append(ButtonEvent(DOWN, MIDDLE, t))
258
+ if bf & w.RI_MOUSE_MIDDLE_BUTTON_UP:
259
+ events.append(ButtonEvent(UP, MIDDLE, t))
260
+ if bf & w.RI_MOUSE_BUTTON_4_DOWN:
261
+ events.append(ButtonEvent(DOWN, X, t))
262
+ if bf & w.RI_MOUSE_BUTTON_4_UP:
263
+ events.append(ButtonEvent(UP, X, t))
264
+ if bf & w.RI_MOUSE_BUTTON_5_DOWN:
265
+ events.append(ButtonEvent(DOWN, X2, t))
266
+ if bf & w.RI_MOUSE_BUTTON_5_UP:
267
+ events.append(ButtonEvent(UP, X2, t))
268
+
269
+ if bf & w.RI_MOUSE_WHEEL:
270
+ events.append(WheelEvent(ctypes.c_short(bd).value, t))
271
+ if bf & w.RI_MOUSE_HWHEEL:
272
+ events.append(WheelEvent(ctypes.c_short(bd).value, t))
273
+
274
+ if dx != 0 or dy != 0 or (flags & w.MOUSE_MOVE_ABSOLUTE):
275
+ pt = w.POINT()
276
+ w._user32.GetCursorPos(ctypes.byref(pt))
277
+
278
+ if flags & w.MOUSE_MOVE_ABSOLUTE:
279
+ # 绝对模式(触摸板 / RDP)下 lLastX/lLastY 不是相对位移,
280
+ # 直接给 0,避免误导。
281
+ rdx = rdy = 0
282
+ else:
283
+ rdx, rdy = dx, dy
284
+
285
+ events.append(MoveEvent(pt.x, pt.y, t, rdx, rdy))
286
+
287
+ if not events:
288
+ return
289
+
290
+ buf = self._buffer
291
+ dq = self._dispatch_queue
292
+ has_handlers = bool(self._handlers)
293
+
294
+ for e in events:
295
+ if isinstance(e, ButtonEvent):
296
+ if e.event_type == UP:
297
+ self._pressed.discard(e.button)
298
+ else:
299
+ self._pressed.add(e.button)
300
+ buf.append(e)
301
+ if has_handlers:
302
+ dq.append(e)
303
+
304
+ # ------------------------------------------------------------------ 分发线程
305
+ def _dispatcher_loop(self):
306
+ while not self._shutdown.is_set():
307
+ if not self._dispatch_queue:
308
+ _time.sleep(0.001)
309
+ continue
310
+ try:
311
+ event = self._dispatch_queue.popleft()
312
+ except IndexError:
313
+ continue
314
+ for h in self._handlers:
315
+ try:
316
+ h(event)
317
+ except Exception:
318
+ if self._debug:
319
+ _tb.print_exc()
320
+
321
+ # ------------------------------------------------------------------ 公共 API
322
+ def hook(self, callback):
323
+ if callback not in self._handlers:
324
+ self._handlers = self._handlers + [callback]
325
+ if not self._running:
326
+ self.start()
327
+ return callback
328
+
329
+ def unhook(self, callback):
330
+ self._handlers = [h for h in self._handlers if h != callback]
331
+
332
+ def unhook_all(self):
333
+ self._handlers = []
334
+
335
+ def poll(self):
336
+ try:
337
+ return self._buffer.popleft()
338
+ except IndexError:
339
+ return None
340
+
341
+ def get_events(self, max_count=None):
342
+ if max_count is None:
343
+ events = list(self._buffer)
344
+ self._buffer.clear()
345
+ return events
346
+ out = []
347
+ for _ in range(max_count):
348
+ try:
349
+ out.append(self._buffer.popleft())
350
+ except IndexError:
351
+ break
352
+ return out
353
+
354
+ def clear_buffer(self):
355
+ self._buffer.clear()
356
+
357
+ def pending_count(self):
358
+ return len(self._buffer)
359
+
360
+ def is_pressed(self, button=LEFT):
361
+ return button in self._pressed
362
+
363
+ def position(self):
364
+ pt = w.POINT()
365
+ w._user32.GetCursorPos(ctypes.byref(pt))
366
+ return (pt.x, pt.y)
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Event types and constants, compatible with the `mouse` library."""
3
+ from collections import namedtuple
4
+
5
+ # Buttons
6
+ LEFT = 'left'
7
+ RIGHT = 'right'
8
+ MIDDLE = 'middle'
9
+ X = 'x'
10
+ X2 = 'x2'
11
+
12
+ # Event types
13
+ UP = 'up'
14
+ DOWN = 'down'
15
+
16
+ # Wheel directions (for future use)
17
+ VERTICAL = 'vertical'
18
+ HORIZONTAL = 'horizontal'
19
+
20
+ ButtonEvent = namedtuple('ButtonEvent', ['event_type', 'button', 'time'])
21
+ WheelEvent = namedtuple('WheelEvent', ['delta', 'time'])
22
+ MoveEvent = namedtuple('MoveEvent', ['x', 'y', 'time', 'dx', 'dy'])
23
+ MoveEvent.__new__.__defaults__ = (0, 0)
24
+
25
+
26
+ __all__ = [
27
+ 'LEFT', 'RIGHT', 'MIDDLE', 'X', 'X2',
28
+ 'UP', 'DOWN', 'VERTICAL', 'HORIZONTAL',
29
+ 'ButtonEvent', 'WheelEvent', 'MoveEvent',
30
+ ]
@@ -0,0 +1,216 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Win32 ctypes bindings for the Raw Input API.
3
+
4
+ Raw Input is a pure-listening mechanism: mouse events are processed by the
5
+ system normally, and a *copy* is delivered to our hidden window as WM_INPUT.
6
+ No hook is inserted into the input chain, so we can never block the mouse.
7
+ """
8
+ import sys
9
+ import ctypes
10
+
11
+ if not sys.platform.startswith('win'):
12
+ raise ImportError("win_mouse_listen is Windows-only.")
13
+
14
+ _user32 = ctypes.WinDLL('user32', use_last_error=True)
15
+ _kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Constants
20
+ # ---------------------------------------------------------------------------
21
+ WM_INPUT = 0x00FF
22
+ WM_DESTROY = 0x0002
23
+ WM_CLOSE = 0x0010
24
+ WM_QUIT = 0x0012
25
+
26
+ HWND_MESSAGE = ctypes.c_void_p(-3)
27
+
28
+ RIDEV_INPUTSINK = 0x00000100
29
+ RIDEV_REMOVE = 0x00000001
30
+ RID_INPUT = 0x10000003
31
+ RIM_TYPEMOUSE = 0
32
+
33
+ HID_USAGE_PAGE_GENERIC = 0x01
34
+ HID_USAGE_GENERIC_MOUSE = 0x02
35
+
36
+ MOUSE_MOVE_RELATIVE = 0x00
37
+ MOUSE_MOVE_ABSOLUTE = 0x01
38
+
39
+ RI_MOUSE_LEFT_BUTTON_DOWN = 0x0001
40
+ RI_MOUSE_LEFT_BUTTON_UP = 0x0002
41
+ RI_MOUSE_RIGHT_BUTTON_DOWN = 0x0004
42
+ RI_MOUSE_RIGHT_BUTTON_UP = 0x0008
43
+ RI_MOUSE_MIDDLE_BUTTON_DOWN = 0x0010
44
+ RI_MOUSE_MIDDLE_BUTTON_UP = 0x0020
45
+ RI_MOUSE_BUTTON_4_DOWN = 0x0040
46
+ RI_MOUSE_BUTTON_4_UP = 0x0080
47
+ RI_MOUSE_BUTTON_5_DOWN = 0x0100
48
+ RI_MOUSE_BUTTON_5_UP = 0x0200
49
+ RI_MOUSE_WHEEL = 0x0400
50
+ RI_MOUSE_HWHEEL = 0x0800
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Structures
55
+ # ---------------------------------------------------------------------------
56
+ LRESULT = ctypes.c_ssize_t
57
+ WNDPROC = ctypes.WINFUNCTYPE(
58
+ LRESULT,
59
+ ctypes.c_void_p, # HWND
60
+ ctypes.c_uint, # UINT
61
+ ctypes.c_size_t, # WPARAM
62
+ ctypes.c_ssize_t, # LPARAM
63
+ )
64
+
65
+
66
+ class POINT(ctypes.Structure):
67
+ _fields_ = [('x', ctypes.c_long), ('y', ctypes.c_long)]
68
+
69
+
70
+ class MSG(ctypes.Structure):
71
+ _fields_ = [
72
+ ('hwnd', ctypes.c_void_p),
73
+ ('message', ctypes.c_uint),
74
+ ('wParam', ctypes.c_size_t),
75
+ ('lParam', ctypes.c_ssize_t),
76
+ ('time', ctypes.c_ulong),
77
+ ('pt', POINT),
78
+ ('lPrivate', ctypes.c_ulong),
79
+ ]
80
+
81
+
82
+ class WNDCLASSEXW(ctypes.Structure):
83
+ _fields_ = [
84
+ ('cbSize', ctypes.c_uint),
85
+ ('style', ctypes.c_uint),
86
+ ('lpfnWndProc', WNDPROC),
87
+ ('cbClsExtra', ctypes.c_int),
88
+ ('cbWndExtra', ctypes.c_int),
89
+ ('hInstance', ctypes.c_void_p),
90
+ ('hIcon', ctypes.c_void_p),
91
+ ('hCursor', ctypes.c_void_p),
92
+ ('hbrBackground', ctypes.c_void_p),
93
+ ('lpszMenuName', ctypes.c_wchar_p),
94
+ ('lpszClassName', ctypes.c_wchar_p),
95
+ ('hIconSm', ctypes.c_void_p),
96
+ ]
97
+
98
+
99
+ class RAWINPUTDEVICE(ctypes.Structure):
100
+ _fields_ = [
101
+ ('usUsagePage', ctypes.c_ushort),
102
+ ('usUsage', ctypes.c_ushort),
103
+ ('dwFlags', ctypes.c_ulong),
104
+ ('hwndTarget', ctypes.c_void_p),
105
+ ]
106
+
107
+
108
+ class RAWINPUTHEADER(ctypes.Structure):
109
+ _fields_ = [
110
+ ('dwType', ctypes.c_ulong),
111
+ ('dwSize', ctypes.c_ulong),
112
+ ('hDevice', ctypes.c_void_p),
113
+ ('wParam', ctypes.c_size_t),
114
+ ]
115
+
116
+
117
+ class _RAWMOUSE_BUTTONS(ctypes.Structure):
118
+ _fields_ = [
119
+ ('usButtonFlags', ctypes.c_ushort),
120
+ ('usButtonData', ctypes.c_ushort),
121
+ ]
122
+
123
+
124
+ class _RAWMOUSE_BUTTONS_UNION(ctypes.Union):
125
+ _fields_ = [
126
+ ('ulButtons', ctypes.c_ulong),
127
+ ('button_flags', _RAWMOUSE_BUTTONS),
128
+ ]
129
+
130
+
131
+ class RAWMOUSE(ctypes.Structure):
132
+ _fields_ = [
133
+ ('usFlags', ctypes.c_ushort),
134
+ ('_padding', ctypes.c_ushort),
135
+ ('buttons', _RAWMOUSE_BUTTONS_UNION),
136
+ ('ulRawButtons', ctypes.c_ulong),
137
+ ('lLastX', ctypes.c_long),
138
+ ('lLastY', ctypes.c_long),
139
+ ('ulExtraInformation', ctypes.c_ulong),
140
+ ]
141
+
142
+
143
+ class RAWINPUT(ctypes.Structure):
144
+ _fields_ = [
145
+ ('header', RAWINPUTHEADER),
146
+ ('mouse', RAWMOUSE),
147
+ ]
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # Function signatures
152
+ # ---------------------------------------------------------------------------
153
+ _user32.DefWindowProcW.argtypes = [
154
+ ctypes.c_void_p, ctypes.c_uint, ctypes.c_size_t, ctypes.c_ssize_t,
155
+ ]
156
+ _user32.DefWindowProcW.restype = LRESULT
157
+
158
+ _user32.GetMessageW.argtypes = [
159
+ ctypes.POINTER(MSG), ctypes.c_void_p, ctypes.c_uint, ctypes.c_uint,
160
+ ]
161
+ _user32.GetMessageW.restype = ctypes.c_int
162
+
163
+ _user32.PeekMessageW.argtypes = [
164
+ ctypes.POINTER(MSG), ctypes.c_void_p,
165
+ ctypes.c_uint, ctypes.c_uint, ctypes.c_uint,
166
+ ]
167
+ _user32.PeekMessageW.restype = ctypes.c_int
168
+
169
+ _user32.TranslateMessage.argtypes = [ctypes.POINTER(MSG)]
170
+ _user32.TranslateMessage.restype = ctypes.c_int
171
+
172
+ _user32.DispatchMessageW.argtypes = [ctypes.POINTER(MSG)]
173
+ _user32.DispatchMessageW.restype = LRESULT
174
+
175
+ _user32.PostQuitMessage.argtypes = [ctypes.c_int]
176
+ _user32.PostQuitMessage.restype = None
177
+
178
+ _user32.PostThreadMessageW.argtypes = [
179
+ ctypes.c_ulong, ctypes.c_uint, ctypes.c_size_t, ctypes.c_ssize_t,
180
+ ]
181
+ _user32.PostThreadMessageW.restype = ctypes.c_int
182
+
183
+ _user32.RegisterClassExW.argtypes = [ctypes.POINTER(WNDCLASSEXW)]
184
+ _user32.RegisterClassExW.restype = ctypes.c_ushort
185
+
186
+ _user32.UnregisterClassW.argtypes = [ctypes.c_wchar_p, ctypes.c_void_p]
187
+ _user32.UnregisterClassW.restype = ctypes.c_int
188
+
189
+ _user32.CreateWindowExW.argtypes = [
190
+ ctypes.c_ulong, ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_ulong,
191
+ ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int,
192
+ ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,
193
+ ]
194
+ _user32.CreateWindowExW.restype = ctypes.c_void_p
195
+
196
+ _user32.DestroyWindow.argtypes = [ctypes.c_void_p]
197
+ _user32.DestroyWindow.restype = ctypes.c_int
198
+
199
+ _user32.RegisterRawInputDevices.argtypes = [
200
+ ctypes.POINTER(RAWINPUTDEVICE), ctypes.c_uint, ctypes.c_uint,
201
+ ]
202
+ _user32.RegisterRawInputDevices.restype = ctypes.c_int
203
+
204
+ _user32.GetRawInputData.argtypes = [
205
+ ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p,
206
+ ctypes.POINTER(ctypes.c_uint), ctypes.c_uint,
207
+ ]
208
+ _user32.GetRawInputData.restype = ctypes.c_uint
209
+
210
+ _user32.GetCursorPos.argtypes = [ctypes.POINTER(POINT)]
211
+ _user32.GetCursorPos.restype = ctypes.c_int
212
+
213
+ _kernel32.GetModuleHandleW.argtypes = [ctypes.c_wchar_p]
214
+ _kernel32.GetModuleHandleW.restype = ctypes.c_void_p
215
+
216
+ _kernel32.GetCurrentThreadId.restype = ctypes.c_ulong
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: win-mouse-listener
3
+ Version: 0.1.0
4
+ Summary: Non-blocking Windows mouse listener using Raw Input API
5
+ License: MIT
6
+ Project-URL: Homepage, https://example.invalid/win-mouse-listener
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: Microsoft :: Windows
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+
12
+ # win-mouse-listen
13
+
14
+ 基于 **Windows Raw Input API** 的非阻塞鼠标监听模块。协议与 [`mouse`](https://pypi.org/project/mouse/) 库兼容,但**不会插入鼠标事件链**,因此:
15
+
16
+ - 回调无论多慢,**都不会让鼠标卡顿**;
17
+ - **不需要管理员权限**;
18
+ - 支持按键、滚轮、移动,移动事件额外附带设备原始增量 `dx / dy`,可用于「触顶/触底持续移动」等场景。
19
+
20
+ ---
21
+
22
+ ## 为什么不用 `WH_MOUSE_LL` 钩子
23
+
24
+ `mouse` 库底层使用 `SetWindowsHookEx(WH_MOUSE_LL)`。虽然它也能全局监听,但它**位于鼠标事件链上**——系统必须等你的钩子返回,才会继续把事件传给其他程序。只要你的回调里出现了锁等待、GIL 争抢、I/O 阻塞,鼠标就会卡。
25
+
26
+ Raw Input 完全不同:鼠标事件由系统正常处理,同时复制一份 `WM_INPUT` 消息投递给你的隐藏窗口。**你的窗口处理再慢,也不会影响鼠标**。
27
+
28
+ ---
29
+
30
+ ## 安装
31
+
32
+ ```bash
33
+ pip install win-mouse-listen
34
+ ```
35
+
36
+ 或从本地 whl 安装:
37
+
38
+ ```bash
39
+ pip install dist/win_mouse_listen-0.1.0-py3-none-any.whl
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 快速开始
45
+
46
+ ```python
47
+ import time
48
+ import win_mouse_listen as wm
49
+
50
+ # 注册回调,第一次 hook 会自动启动监听
51
+ wm.hook(lambda ev: print('event:', ev))
52
+
53
+ try:
54
+ while True:
55
+ time.sleep(1)
56
+ except KeyboardInterrupt:
57
+ wm.stop()
58
+ ```
59
+
60
+ 或者使用类接口:
61
+
62
+ ```python
63
+ from win_mouse_listen import Listener
64
+
65
+ with Listener() as l:
66
+ l.hook(print)
67
+ time.sleep(10)
68
+ ```
69
+
70
+ ---
71
+
72
+ ## 事件协议
73
+
74
+ 与 `mouse` 库兼容的 namedtuple:
75
+
76
+ ```python
77
+ ButtonEvent(event_type, button, time)
78
+ WheelEvent(delta, time)
79
+ MoveEvent(x, y, time, dx, dy)
80
+ ```
81
+
82
+ - `event_type`:`'up'` 或 `'down'`
83
+ - `button`:`LEFT` / `RIGHT` / `MIDDLE` / `X` / `X2`
84
+ - `time`:Unix 时间戳(float,秒)
85
+ - `x, y`:屏幕坐标,由 `GetCursorPos` 获取,**钳制在屏幕内**
86
+ - `dx, dy`:本次事件的**设备原始增量**,即使光标已经贴到屏幕边缘,仍会持续给出同方向的非零值
87
+
88
+ > **注意**:`MoveEvent` 是 5 字段版本,与 `mouse.MoveEvent` 的 3 字段版本**不兼容 `isinstance`**。如果你的代码依赖 `mouse` 库的类型,请改用 duck typing 判断字段。
89
+
90
+ ---
91
+
92
+ ## API
93
+
94
+ ### 回调
95
+
96
+ ```python
97
+ wm.hook(callback) # 注册回调,返回 callback
98
+ wm.unhook(callback) # 注销单个回调
99
+ wm.unhook_all() # 注销所有回调
100
+ ```
101
+
102
+ 回调在独立的分发线程中执行,**阻塞回调不会影响鼠标**。
103
+
104
+ ### 生命周期
105
+
106
+ ```python
107
+ wm.start() # 显式启动(hook 会自动启动)
108
+ wm.stop() # 停止并重置默认监听器
109
+ ```
110
+
111
+ ### 非阻塞查询
112
+
113
+ ```python
114
+ wm.poll() # 取一个事件,没有则返回 None
115
+ wm.get_events(max_count=None) # 批量取事件
116
+ wm.clear_buffer() # 清空缓冲区
117
+ wm.pending_count() # 当前缓冲的事件数量
118
+ wm.is_pressed(button=LEFT) # 指定按键是否按下
119
+ wm.position() # 当前鼠标屏幕坐标 (x, y)
120
+ ```
121
+
122
+ ---
123
+
124
+ ## 典型用法:检测「触顶 / 触底持续移动」
125
+
126
+ 某些场景下需要判断鼠标已经顶到屏幕边缘,但用户仍在继续往同方向移动。传统做法是用「越界负坐标」反推增量,麻烦又容易出错。使用 `dx / dy` 直接判断即可:
127
+
128
+ ```python
129
+ import win_mouse_listen as wm
130
+
131
+ SCREEN_TOP_Y = 0
132
+ SCREEN_BOTTOM_Y = 1079
133
+
134
+ def on_move(ev):
135
+ if ev.y <= SCREEN_TOP_Y and ev.dy < 0:
136
+ print('向上越界移动', ev.dy)
137
+ elif ev.y >= SCREEN_BOTTOM_Y and ev.dy > 0:
138
+ print('向下越界移动', ev.dy)
139
+
140
+ wm.hook(on_move)
141
+ ```
142
+
143
+ > 提示:高回报率鼠标每秒会产生数百到上千个 `MoveEvent`,每个事件的 `dx/dy` 通常只有个位数。如果需要按距离阈值触发逻辑,建议在时间窗口内累加。
144
+
145
+ ---
146
+
147
+ ## 限制与注意事项
148
+
149
+ 1. **绝对模式**:触摸板、数位板、RDP 远程桌面等设备上报的是**绝对坐标**(`usFlags & MOUSE_MOVE_ABSOLUTE`)。此时 `lLastX/lLastY` 不是增量,模块会将 `dx/dy` 置 0,避免出现巨大数值。物理鼠标不受影响。
150
+
151
+ 2. **多显示器**:`x, y` 来自 `GetCursorPos`,在虚拟屏幕坐标系下,副屏位于主屏左侧或上方时 `x/y` 本来就可能为负。这和「触顶越界」是两回事。
152
+
153
+ 3. **仅支持 Windows**:模块依赖 `user32.dll`,其他平台会直接 `ImportError`。
154
+
155
+ 4. **不需要管理员**:Raw Input 对普通用户会话开放,无需提权。
156
+
157
+ ---
158
+
159
+ ## 打包
160
+
161
+ 项目根目录执行:
162
+
163
+ ```bash
164
+ pip install build
165
+ python -m build
166
+ ```
167
+
168
+ 产物在 `dist/`:
169
+
170
+ ```
171
+ dist/
172
+ ├── win_mouse_listen-0.1.0-py3-none-any.whl
173
+ └── win_mouse_listen-0.1.0.tar.gz
174
+ ```
175
+
176
+ 本地安装测试:
177
+
178
+ ```bash
179
+ pip install dist/win_mouse_listen-0.1.0-py3-none-any.whl
180
+ ```
181
+
182
+ ---
183
+
184
+ ## 项目结构
185
+
186
+ ```
187
+ win_mouse_listen/
188
+ ├── __init__.py # 公共 API 与模块级单例
189
+ ├── _types.py # 事件协议(ButtonEvent / WheelEvent / MoveEvent)
190
+ ├── _win32.py # ctypes 绑定与常量
191
+ └── _listener.py # 核心监听器实现
192
+ pyproject.toml
193
+ README.md
194
+ ```
195
+
196
+ ---
197
+
198
+ ## 设计要点
199
+
200
+ - **Raw Input 而非全局钩子**:鼠标事件不经过本模块,回调再慢也不会卡鼠标。
201
+ - **隐藏消息窗口**:`HWND_MESSAGE` 类型窗口,不出现在任务栏。
202
+ - **双重缓冲**:内部维护 `deque(maxlen=N)`,满了自动丢弃最旧事件,永不阻塞钩子线程。
203
+ - **无锁读取**:利用 CPython 中 `deque.append/popleft`、`list` 引用替换的原子性,避免在热路径上加锁。
204
+ - **分发线程独立**:用户回调在专用线程执行,隔离用户代码。
205
+
206
+ ---
207
+
208
+ ## License
209
+
210
+ MIT
@@ -0,0 +1,8 @@
1
+ win_mouse_listener/__init__.py,sha256=FR-fjfJaBoxvAdyuF6mqKbO0fgua4iVxiZq4_FNOryQ,2317
2
+ win_mouse_listener/_listener.py,sha256=JWlpmIsj7JVrV0ds5ISs42cYS4WsE1mogtxEIduiFHg,12461
3
+ win_mouse_listener/_types.py,sha256=dVVreO4Kbrlorz8gzRJXmQABF7dZICg3TZwDEhKAP_g,746
4
+ win_mouse_listener/_win32.py,sha256=oDanjiWks3L6QbgSqHXhZ1qJnXewBLKJOXkOvwmyzTk,6768
5
+ win_mouse_listener-0.1.0.dist-info/METADATA,sha256=R6vnzRl0VMsFKJQ_VhVZ6d2_UqPt5Uzvt5fS9CqWpYM,6262
6
+ win_mouse_listener-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ win_mouse_listener-0.1.0.dist-info/top_level.txt,sha256=pidTNon3_LsMDFmGANGqOq7yHr5xvHpR-lfYYsV09PU,19
8
+ win_mouse_listener-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ win_mouse_listener