Import the pre-repair source tree as the history baseline. Runtime data (data/), virtualenvs, bytecode caches and logs are gitignored so local secrets and user state stay out of the repo.
106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
全局热键监听器 (Windows)
|
|
独立线程 + Win32 RegisterHotKey 消息循环,实现主窗口失焦也能触发的系统级全局快捷键。
|
|
|
|
用法:
|
|
hotkey = GlobalHotkeyThread() # 默认 Alt+S
|
|
hotkey.triggered.connect(on_triggered) # 跨线程信号,槽函数在主线程执行
|
|
hotkey.start() # 启动线程
|
|
hotkey.stop() # 注销热键并退出线程
|
|
"""
|
|
import ctypes
|
|
import sys
|
|
import threading
|
|
from ctypes import wintypes
|
|
|
|
from PyQt6 import QtCore
|
|
|
|
# Win32 常量
|
|
WM_HOTKEY = 0x0312
|
|
WM_QUIT = 0x0012
|
|
MOD_ALT = 0x0001
|
|
MOD_CONTROL = 0x0002
|
|
MOD_SHIFT = 0x0004
|
|
MOD_NOREPEAT = 0x4000 # 按住不重复触发 (Windows 7+)
|
|
VK_S = 0x53
|
|
|
|
_is_windows = sys.platform == "win32"
|
|
|
|
if _is_windows:
|
|
_user32 = ctypes.windll.user32
|
|
_kernel32 = ctypes.windll.kernel32
|
|
|
|
# 显式声明函数签名,避免 ctypes 默认 int 截断指针/句柄
|
|
_user32.RegisterHotKey.argtypes = [wintypes.HWND, ctypes.c_int, wintypes.UINT, wintypes.UINT]
|
|
_user32.RegisterHotKey.restype = wintypes.BOOL
|
|
_user32.UnregisterHotKey.argtypes = [wintypes.HWND, ctypes.c_int]
|
|
_user32.UnregisterHotKey.restype = wintypes.BOOL
|
|
_user32.GetMessageW.argtypes = [
|
|
ctypes.POINTER(wintypes.MSG), wintypes.HWND, wintypes.UINT, wintypes.UINT
|
|
]
|
|
_user32.GetMessageW.restype = wintypes.BOOL # >0 正常 / 0 WM_QUIT / -1 出错
|
|
_user32.PostThreadMessageW.argtypes = [
|
|
wintypes.DWORD, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM
|
|
]
|
|
_user32.PostThreadMessageW.restype = wintypes.BOOL
|
|
_kernel32.GetCurrentThreadId.restype = wintypes.DWORD
|
|
|
|
|
|
class GlobalHotkeyThread(QtCore.QThread):
|
|
"""在独立线程注册系统级全局热键并运行 Win32 消息循环。
|
|
|
|
收到 WM_HOTKEY 后通过 Qt 信号 triggered 通知主线程,
|
|
信号槽机制保证槽函数在主线程执行,操作 Qt 控件安全。
|
|
"""
|
|
|
|
triggered = QtCore.pyqtSignal()
|
|
|
|
def __init__(self, hotkey_id=9001, mod=MOD_ALT, vk=VK_S, parent=None):
|
|
super().__init__(parent)
|
|
self._hotkey_id = hotkey_id
|
|
self._mod = mod
|
|
self._vk = vk
|
|
self._thread_id = 0
|
|
self._ready = threading.Event() # run() 记录线程 ID 后置位
|
|
self._registered = False
|
|
|
|
def run(self):
|
|
"""线程入口:注册热键 -> 消息循环 -> 退出时注销"""
|
|
if not _is_windows:
|
|
self._ready.set()
|
|
return
|
|
|
|
self._thread_id = _kernel32.GetCurrentThreadId()
|
|
ok = _user32.RegisterHotKey(
|
|
None, self._hotkey_id, self._mod | MOD_NOREPEAT, self._vk
|
|
)
|
|
self._registered = bool(ok)
|
|
self._ready.set()
|
|
if not ok:
|
|
print(
|
|
f"[GlobalHotkey] 注册热键失败 (id={self._hotkey_id}, mod={self._mod:#x}, "
|
|
f"vk={self._vk:#x}),可能已被其他程序占用"
|
|
)
|
|
return
|
|
|
|
# Win32 消息循环:hwnd=None 取本线程所有消息
|
|
msg = wintypes.MSG()
|
|
while _user32.GetMessageW(ctypes.byref(msg), None, 0, 0) > 0:
|
|
if msg.message == WM_HOTKEY and msg.wParam == self._hotkey_id:
|
|
self.triggered.emit()
|
|
|
|
# 收到 WM_QUIT 退出循环 -> 注销热键
|
|
_user32.UnregisterHotKey(None, self._hotkey_id)
|
|
self._registered = False
|
|
|
|
def stop(self):
|
|
"""请求线程退出:向线程消息队列投递 WM_QUIT,然后等待结束"""
|
|
if not _is_windows:
|
|
return
|
|
# 等 run() 至少记录好线程 ID(注册成功或失败都行)
|
|
self._ready.wait(timeout=2.0)
|
|
if self._thread_id:
|
|
_user32.PostThreadMessageW(self._thread_id, WM_QUIT, 0, 0)
|
|
self.wait(2000)
|