python实现自己的全局热键
使用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()