Files
sorrow404null a7412824e0 chore: import original project baseline
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.
2026-09-17 16:40:01 +08:00

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)