python实现自己的全局热键

发布时间 2023-09-05 21:59:33作者: 顺其自然,道法自然

使用keyboard模块做全局热键有时会出现莫名其妙的问题:

  • 不灵敏: 有时候按热键没有反应, 需要多按几次
  • suppress失效: 有时候并没有阻止热键传播, 别的程序仍然可以接受到热键消息, 导致热键冲突
  • 热键失效: 有时候热键会失效, 并且不能恢复, 只能重起应用
    阅读了keyboard的源码, 发现其实现比较复杂, 定位和修改难度比较大. 干脆自行实现一个简易版的.

监控按键消息

核心代码:

def start_listening():
    '''
    启动键盘监听功能
    callback: bool f('LMENU','DOWN'), 返回值如果为True, 表示阻塞消息继续传播
    '''
    global _outer_callback
    # 注册键盘钩子
    WH_KEYBOARD_LL = c_int(13)
    keyboard_callback = LowLevelKeyboardProc(_keyboard_callback)
    handle =  GetModuleHandleW(None)    # 当前进程的模块句柄
    thread_id = DWORD(0)
    keyboard_hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_callback, handle, thread_id)
    atexit.register(UnhookWindowsHookEx, keyboard_hook) # 解释器退出时解除回调
    # 进入信息循环
    msg = LPMSG()
    while not GetMessage(msg, 0, 0, 0):
        TranslateMessage(msg)
        DispatchMessage(msg)

#name_buffer = ctypes.create_unicode_buffer(32)
def _keyboard_callback(code:WPARAM,event_code:LPARAM,kb_pointer:pKBDLLHOOKSTRUCT)->bool:
    '''
    内部回调函数
    code: 表示消息类型, 似乎没有什么用
    event_code: 可能取值如下, 可以用最低位是否为1判断是down还是up消息
        0x100: 'keydown',  # WM_KeyDown for normal keys
        0x101: 'keyup',  # WM_KeyUp for normal keys
        0x104: 'keydown',  # WM_SYSKEYDOWN, used for Alt key.
        0x105: 'keyup',  # WM_SYSKEYUP, used for Alt key.
    kb_point: 指向KBDLLHOOKSTRUCT结构的指针
    返回值: 返回0(可用False表示0), 表示key消息传递给windows消息系统, 非0, 表示不传递给windows消息系统;
        而CallNextHookEx函数表示是否传递给别的勾子函数.
        参考: https://blog.csdn.net/w10800337/article/details/23995399
    '''
    # 下面是常用的提取的信息
    # vk_code = kb_pointer.contents.vk_code # 虚拟键码
    # vk_name = virtualkeys.get(vk, 'error')    # 得到虚拟键码对应的虚拟键的名称
    # scan_code = kb_pointer.contents.scan_code # 扫描码
    # is_extended = kb_pointer.contents.flags & 1   # 是否是扩展位
    # event_type = 'KEY_UP' if event_code & 0x01 else 'KEY_DOWN'   # 表示是按下还是释放
    # 下面是键的名字
    # name_ret = GetKeyNameText(scan_code << 16 | is_extended << 24, name_buffer, 1024)
    # keyname = 'Unknown'
    # if name_ret and name_buffer.value: keyname = name_buffer.value
    vk_code = kb_pointer.contents.vk_code # 虚拟键码
    vk_name = virtualkeys.get(vk_code, 'Error')    # 得到虚拟键码对应的虚拟键的名称
    event_type = 'UP' if event_code & 0x01 else 'DOWN'   # 表示是按下还是释放
    # 发布消息, 如果有一个返回True, 表示阻止消息传递
    res = pub('23090501_KeyMsg',vk_name,event_type)
    if any(res)==True: return True
    # 调用别的勾子函数
    return CallNextHookEx(c_int(0), code, event_code, kb_pointer)
  • 使用示例
import hotkey
from message import *
def on_press(key:str,etype:str)->bool:
    print(f'key:{key}; etype:{etype}')
    return True
sub('23090501_KeyMsg',on_press)
hotkey.start_listening()

增加热键

  • 核心代码
from .hotkey import start_listening
import message as msg
import Common as cmn

class HotkeyMatch:
    def __init__(self,hotkey:str):
        '''hotkey: 形如: 'ctrl+a';'''
        self.hotkey = hotkey
        self.keys = hotkey.split('+')   # 热键的列表
        self.cur_index = 0  # 当前正在匹配的键索引
    def is_match(self,key:str,etype:str)->bool:
        '''
        key: 形如: VK_A
        etype: 形如: DOWN|UP
        返回值: True: 表示热键匹配, 否则表示不匹配
        '''
        if etype!='DOWN':   # 只接受down消息
            self.cur_index = 0
            return False
        key = self._convert_key(key)    # 转换字符消息
        # 如果匹配当前字符, 则匹配索引+1
        if key==self.keys[self.cur_index]: self.cur_index += 1
        else: self.cur_index = 0
        if self.cur_index==len(self.keys): return True
        return False
    def _convert_key(self,key:str)->str:
        '''
        key: 形如: VK_A
        返回值: 形如: 'a'
        '''
        if key=='VK_LCONTROL' or key=='VK_RCONTROL': return 'ctrl'
        if key=='VK_LMENU' or key=='VK_RMENU': return 'alt'
        if key.startswith('VK_'): key = key[3:]
        return key.lower()

def add_hotkey(hotkey:str,callback:callable):
    '''
    增加一个热键
    hotkey: 形如: 'ctrl+a'
    callback: 回调函数: void f()
    '''
    hotkey_match = HotkeyMatch(hotkey)
    def on_key(key:str,etype:str)->bool:
        result = hotkey_match.is_match(key,etype)
        if result==True:    # 触发热键
            cmn.run_in_thread(callback)()   # 在新线程中运行回调函数
        return result
    msg.sub('23090501_KeyMsg',on_key)   # 订阅键盘消息
  • 使用示例
import hotkey
hotkey.add_hotkey('ctrl+a',lambda:print('ctrl+a pressed'))
hotkey.add_hotkey('alt+1',lambda:print('alt+1 pressed'))
hotkey.start_listening()