全局快捷键的应用
在现代桌面应用开发中,全局快捷键功能是提升用户体验的重要手段。用户无需将焦点切换到应用窗口,就能通过特定的键盘组合快速触发应用功能。本文以Rouyan,开源地址:https://github.com/Ming-jiayou/Rouyan为例,说明在WPF应用中可以如何绑定系统快捷键。
全局键盘钩子
Rouyan中是在 KeySequenceService.cs 中实现的,全局键盘钩子通过 Windows API 实现,允许应用程序监听系统级的键盘事件,而不受窗口焦点限制。
1、Win32 API 导入
类中导入了必要的 Windows API 函数:
SetWindowsHookEx:安装钩子
UnhookWindowsHookEx:卸载钩子
CallNextHookEx:将钩子传递给下一个处理器
GetModuleHandle:获取模块句柄
[DllImport("user32.dll")] public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll")] public static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll")] public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll")] public static extern IntPtr GetModuleHandle(string lpModuleName);
现在先来学习一下这几个函数:
1、SetWindowsHookEx
[DllImport("user32.dll")] public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
用途:安装钩子过程到钩子链中。钩子允许应用程序拦截和处理系统消息或事件。
参数:
idHook (int):钩子类型。对于低级键盘钩子,使用常量 WH_KEYBOARD_LL = 13。
lpfn (LowLevelKeyboardProc):指向钩子过程的指针。在代码中传递 HookCallback 方法。
hMod (IntPtr):包含钩子过程的模块句柄。使用 GetModuleHandle 获取当前模块句柄。
dwThreadId (uint):要关联钩子的线程 ID。设为 0 表示全局钩子(所有线程)。
返回值:成功时返回钩子句柄 (IntPtr),失败时返回 IntPtr.Zero。
在代码中的应用:在 SetHook 方法中调用,用于安装低级键盘钩子,使应用程序能监听系统级键盘事件。
2、UnhookWindowsHookEx
[DllImport("user32.dll")] public static extern bool UnhookWindowsHookEx(IntPtr hhk);
用途:从钩子链中移除指定的钩子过程。必须在使用完毕后调用,以释放系统资源。
参数:
hhk (IntPtr):要移除的钩子句柄。由 SetWindowsHookEx 返回。
返回值:成功时返回 true,失败时返回 false。
在代码中的应用:在 Dispose 方法中调用,确保应用程序退出时正确卸载钩子,避免内存泄漏和系统级问题。
3、CallNextHookEx
[DllImport("user32.dll")] public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
用途:将钩子信息传递给钩子链中的下一个钩子过程。这是钩子链机制的核心,确保所有钩子都能处理消息。
参数:
hhk (IntPtr):当前钩子的句柄(可选,通常设为当前钩子句柄)。
nCode (int):钩子代码,指示如何处理消息。
wParam (IntPtr):消息的 WPARAM 参数。
lParam (IntPtr):消息的 LPARAM 参数。
返回值:下一个钩子过程的返回值。
在代码中的应用:在 HookCallback 方法末尾调用,确保处理完自定义逻辑后,将消息传递给系统或其他钩子。
4、GetModuleHandle
[DllImport("kernel32.dll")] public static extern IntPtr GetModuleHandle(string lpModuleName);
用途:检索指定模块的模块句柄。模块句柄用于标识 DLL 或 EXE 文件。
参数:
lpModuleName (string):模块名称(不含路径)。如果为 null,返回调用进程的主模块句柄。
返回值:成功时返回模块句柄 (IntPtr),失败时返回 IntPtr.Zero。
在代码中的应用:在 SetHook 方法中调用,获取当前进程主模块的句柄,作为 SetWindowsHookEx 的 hMod 参数,用于关联钩子到当前应用程序模块。
具体实现
先总体看一下KeySequenceService类做了什么?
1、注册/卸载全局键盘钩子
2、拦截按键并用状态机识别序列
3、将“Tab + 字母”组合映射到 8 个动作
4、保持系统钩子链
2-4就是在钩子回调中做的事。
一些常量设置
// 低级键盘钩子常量 private const int WH_KEYBOARD_LL = 13; private const int WM_KEYDOWN = 0x0100; // 按键常量(Tab + 字母 序列) private const int VK_TAB = 0x09; private const int VK_K = 0x4B; private const int VK_L = 0x4C; private const int VK_U = 0x55; private const int VK_I = 0x49; private const int VK_S = 0x53; private const int VK_D = 0x44; private const int VK_W = 0x57; private const int VK_E = 0x45; // 序列超时时间(毫秒) private const int SEQUENCE_TIMEOUT_MS = 2000;
private const int WH_KEYBOARD_LL = 13;
含义:Win32 的“低级键盘钩子”类型常量。用于安装系统范围的键盘事件回调。
低级键盘钩子是什么意思?
“低级键盘钩子”(WH_KEYBOARD_LL)是 Windows 提供的一种全局键盘事件拦截机制。通过 Win32 API 在用户态安装后,系统在键盘事件产生时会优先回调你提供的函数,让你的程序有机会观察、处理,甚至拦截按键,再将事件传递给系统或其他钩子。
用途:作为 SetWindowsHookEx 的 idHook 参数,安装键盘钩子。
private const int WM_KEYDOWN = 0x0100;
含义:键盘“按下”消息常量。
用途:在钩子回调中过滤只处理按下事件。
剩下的是虚拟键码与序列超时时间。
注册/卸载全局键盘钩子
构造阶段:准备钩子回调与委托防 GC
public delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam); public KeySequenceService() { _proc = HookCallback; } private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) { int vkCode = Marshal.ReadInt32(lParam); HandleKeyDown(vkCode); } return CallNextHookEx(_hookID, nCode, wParam, lParam); }
HookCallback 的作用是作为 WH_KEYBOARD_LL 低级键盘钩子的回调入口,按键事件一到达就被它截获、筛选并转交给序列状态机处理,最后把事件继续传给系统的下一枚钩子。
注册键盘钩子:
public void RegisterHotKeys() { try { _hookID = SetHook(_proc); if (_hookID == IntPtr.Zero) { Console.WriteLine("警告: 无法安装全局键盘钩子"); } else { Console.WriteLine("全局热键已注册:n" + "Tab+K (RunLLMPrompt1)n" + "Tab+L (RunLLMPrompt1Streaming)n" + "Tab+U (RunLLMPrompt2)n" + "Tab+I (RunLLMPrompt2Streaming)n" + "Tab+S (RunVLMPrompt1)n" + "Tab+D (RunVLMPrompt1Streaming)n" + "Tab+W (RunVLMPrompt2)n" + "Tab+E (RunVLMPrompt2Streaming)"); } } catch (Exception ex) { Console.WriteLine($"注册热键失败: {ex.Message}"); } } private IntPtr SetHook(LowLevelKeyboardProc proc) { using var curProcess = System.Diagnostics.Process.GetCurrentProcess(); using var curModule = curProcess.MainModule; if (curModule?.ModuleName != null) { return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0); } return IntPtr.Zero; }
其中核心代码是 return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);。
意思是安装低级键盘钩子并返回钩子句柄,proc就是钩子的回调方法,然后传入当前这个模块,0表示对系统范围内所有线程生效(全局钩子)。
卸载键盘钩子:
public void Dispose() { try { if (_hookID != IntPtr.Zero) { UnhookWindowsHookEx(_hookID); _hookID = IntPtr.Zero; Console.WriteLine("全局热键已卸载"); } } catch (Exception ex) { Console.WriteLine($"清理热键资源时出错: {ex.Message}"); } }
钩子回调
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) { int vkCode = Marshal.ReadInt32(lParam); HandleKeyDown(vkCode); } return CallNextHookEx(_hookID, nCode, wParam, lParam); } private void HandleKeyDown(int vkCode) { switch (_currentMode) { case HotkeyMode.None: if (vkCode == VK_TAB) { _currentMode = HotkeyMode.WaitingAfterTab; _sequenceStartTime = DateTime.Now; Console.WriteLine("检测到 Tab 键,等待按下后续字母键..."); } break; case HotkeyMode.WaitingAfterTab: if (IsTimeout()) { Console.WriteLine("按键序列超时"); } else { switch (vkCode) { case VK_K: Console.WriteLine("检测到完整组合键 Tab+K,执行 RunLLMPrompt1..."); ExecuteAction(_runLLMPrompt1); break; case VK_L: Console.WriteLine("检测到完整组合键 Tab+L,执行 RunLLMPrompt1Streaming..."); ExecuteAction(_runLLMPrompt1Streaming); break; case VK_U: Console.WriteLine("检测到完整组合键 Tab+U,执行 RunLLMPrompt2..."); ExecuteAction(_runLLMPrompt2); break; case VK_I: Console.WriteLine("检测到完整组合键 Tab+I,执行 RunLLMPrompt2Streaming..."); ExecuteAction(_runLLMPrompt2Streaming); break; case VK_S: Console.WriteLine("检测到完整组合键 Tab+S,执行 RunVLMPrompt1..."); ExecuteAction(_runVLMPrompt1); break; case VK_D: Console.WriteLine("检测到完整组合键 Tab+D,执行 RunVLMPrompt1Streaming..."); ExecuteAction(_runVLMPrompt1Streaming); break; case VK_W: Console.WriteLine("检测到完整组合键 Tab+W,执行 RunVLMPrompt2..."); ExecuteAction(_runVLMPrompt2); break; case VK_E: Console.WriteLine("检测到完整组合键 Tab+E,执行 RunVLMPrompt2Streaming..."); ExecuteAction(_runVLMPrompt2Streaming); break; default: Console.WriteLine($"检测到 Tab 后的无效按键: {vkCode}"); break; } } ResetState(); break; } // 检查超时并重置状态 if (_currentMode != HotkeyMode.None && IsTimeout()) { Console.WriteLine("按键序列超时"); ResetState(); } }
只处理键盘按下消息类型,然后根据不同的快捷键组合调用不同的方法。
private void ExecuteAction(Action action) { try { // 在UI线程上执行操作 Application.Current?.Dispatcher.BeginInvoke(new Action(() => { try { action?.Invoke(); } catch (Exception ex) { Console.WriteLine($"执行热键操作时出错: {ex.Message}"); } }), DispatcherPriority.Normal); } catch (Exception ex) { Console.WriteLine($"调度热键操作时出错: {ex.Message}"); } }
在HotkeyService中对热键做了管理:
/// <summary> /// 初始化热键服务 /// </summary> /// <param name="mainWindow">主窗口</param> public void Initialize(Window mainWindow) { try { // 初始化Tab+字母组合键服务 _keySequenceService = new KeySequenceService( ExecuteRunLLMPrompt1, ExecuteRunLLMPrompt1Streaming, ExecuteRunLLMPrompt2, ExecuteRunLLMPrompt2Streaming, ExecuteRunVLMPrompt1, ExecuteRunVLMPrompt1Streaming, ExecuteRunVLMPrompt2, ExecuteRunVLMPrompt2Streaming); _keySequenceService.RegisterHotKeys(); // 初始化全局ESC键服务 _globalEscService = new GlobalEscService(); _globalEscService.Register(); } catch (Exception ex) { Console.WriteLine($"初始化热键服务失败: {ex.Message}"); } }
把具体要执行的方法传进去:
/// <summary> /// 执行RunLLMPrompt1操作 /// 当检测到 Tab+K 组合键时调用 /// </summary> private async void ExecuteRunLLMPrompt1() { try { var homeViewModel = _container.Get<HomeViewModel>(); if (homeViewModel != null) { await homeViewModel.RunLLMPrompt1(); } else { Console.WriteLine("警告: 无法获取HomeViewModel实例"); } } catch (Exception ex) { Console.WriteLine($"执行Tab+K热键操作失败: {ex.Message}"); } }
在Bootstrapper中初始化这个热键服务:
protected override void OnLaunch() { // 初始化和获取全局快捷键服务 try { var _hotkeyService = this.Container.Get<HotkeyService>(); if (Application.Current?.MainWindow != null) { _hotkeyService.Initialize(Application.Current.MainWindow); } } catch (Exception ex) { Console.WriteLine($"初始化全局快捷键失败: {ex.Message}"); } }
然后就成功实现了按下设定的快捷键就会触发特定的方法。
用Rouyan举个例子就是按下tab + l快捷键时,就会自动弹出流式窗口,根据提示词的内容,对剪贴板中的内容进行处理,如下所示:

然后按下esc就会关闭这个窗口,实现思路是一样的,代码我写到了GlobalEscService中,关键代码如下所示:
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) { int vkCode = Marshal.ReadInt32(lParam); // 检查是否按下了ESC键 if (vkCode == VK_ESCAPE) { // 查找并关闭ShowMessageView窗口 CloseShowMessageWindow(); } } return CallNextHookEx(_hookID, nCode, wParam, lParam); } /// <summary> /// 查找并关闭ShowMessageView窗口 /// </summary> private void CloseShowMessageWindow() { // 在UI线程上执行窗口查找和关闭操作 Application.Current.Dispatcher.Invoke(() => { // 遍历所有打开的窗口 foreach (Window window in Application.Current.Windows) { // 检查是否是ShowMessageView类型的窗口 if (window is Rouyan.Pages.View.ShowMessageView showMessageWindow) { showMessageWindow.Close(); break; // 找到并关闭后退出循环 } } }); }
以上就是本期分享的全部内容,希望对你有所帮助,如果对具体实现感兴趣欢迎查看Rouyan代码,开源地址:https://github.com/Ming-jiayou/Rouyan。