# -*- 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)