全局快捷键的应用

在现代桌面应用开发中,全局快捷键功能是提升用户体验的重要手段。用户无需将焦点切换到应用窗口,就能通过特定的键盘组合快速触发应用功能。本文以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。

WPF应用绑定系统快捷键的更多相关文章

  1. WPF/UWP 绑定中的 UpdateSourceTrigger

    在开发 markdown-mail 时遇到了一些诡异的情况.代码是这么写的: <TextBox Text="{Binding Text, Mode=TwoWay}"/> ...

  2. 【WPF】最近在学习wpf 的绑定,,

    最近在学习wpf 的绑定,,1.简单的说就是版前端和后端用自己的方法给分开了2.baseVm 模型 baseCmd 命令3.命令传参修改的只是界面里的属性,而不修改其它的值4.前端改变后端, 后端改变 ...

  3. wpf直接绑定xml生成应用程序

    目的:在vs2010下用wpf完成一个配置工具,配置文件为xml格式,xml文件作为数据源,直接和wpf前台绑定,生成exe后,运行exe能够加载同路径下的xml配置文件并显示 xml文件在项目中的设 ...

  4. [WPF疑难]如何禁用WPF窗口的系统菜单(SystemMenu)

    原文 [WPF疑难]如何禁用WPF窗口的系统菜单(SystemMenu) [WPF疑难]如何禁用WPF窗口的系统菜单(SystemMenu) 周银辉 点击窗口左上角图标时弹出来的菜单也就是这里所说的系 ...

  5. WPF DataGrid绑定一个组合列

    WPF DataGrid绑定一个组合列 前台: <Page.Resources>        <local:InfoConverter x:Key="converter& ...

  6. Windows 8 系统快捷键热键列表收集

    值得收藏参考的 Windows 8 系统快捷键热键列表收集大全汇总,键盘党效率党必备啊! 相信不少喜欢接触新鲜软件的同学都已经给电脑安装上Windows 8 操作系统了吧!这个系统优秀与否我们暂且不讨 ...

  7. WPF DataGrid 绑定行双击行命令

    WPF DataGrid 绑定行双击行命令 <DataGrid ...> <DataGrid.InputBindings> <MouseBinding MouseActi ...

  8. WPF 模板绑定父级控件内容

    WPF 模板绑定父级控件内容 <Style TargetType="Button" x:Key="btn"> <Setter Property ...

  9. WPF多路绑定

    WPF多路绑定 多路绑定实现对数据的计算,XAML:   引用资源所在位置 xmlns:cmlib="clr-namespace:CommonLib;assembly=CommonLib&q ...

  10. WPF Bind 绑定

    原文:WPF Bind 绑定 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/koloumi/article/details/74332515 用过W ...

随机推荐

  1. bt-cloud安装指南

    1.安装PHP7.4 sudo apt update -y && upgrade -y sudo apt install php7.4-common php7.4-zip php7.4 ...

  2. spring-ai 学习系列(4)-MCP 处理过程分析

    上一节,通过1个最基本的MCP Server/Client示例,初步了解了MCP的用法.STDIO模式下,client与server同在1台机器上,client会创建1个子进程来启动server,然后 ...

  3. 一文彻底搞懂javascript中的undefined

    undefined in javascript undefined是可以说是javascript中最特殊的一个类型,许多其他语言中都没有这个类型.它表示一个变量已经声明,但还没有被赋值. let a; ...

  4. 2025年Python安装运行mayavi过程全记录

    本文从新建干净环境python 3.7说起,需安装文件有 PyQt4-4.11.4, traits-6.3.1, VTK-8.1.2, mayavi-4.7.3, PyQt5, VisualStudi ...

  5. 标准结构篇:9.1)JB5054-2000流程

    本章目的:了解这个最基础的产品设计研发流程标准 1.前言 JB5054-2000流程是2000年的研发流程,如果你自家公司的研发流程环节或提交资料比这个还少,就真的要考虑一下,到底是什么地方漏了. 2 ...

  6. win11专业版系统无法连接wifi网络的问题

    有一位雨林木风系统的用户,不知道咋地好好的把电脑升级win11 23h2官方正式版,等系统安装好后,发现电脑居然不能连接wifi网络了,也不知道发生了什么事,而且重装一次了也还是如此,那要如何是好呢? ...

  7. unity编辑器绘制扇形

    使用 UnityEditor.Handles.DrawSolidArc using UnityEngine; using UnityEditor; public class DrawSectorHan ...

  8. mysql面试精讲

    https://mp.weixin.qq.com/s/MCFHNOQnTtJ6MGVjM3DP4A

  9. 谷歌推出基于Gemini 2.0的机器人AI模型

    Gemini Robotics将AI带入物理世界 谷歌DeepMind正式推出基于Gemini 2.0的两款机器人AI模型: Gemini Robotics:先进的视觉-语言-动作(VLA)模型,新增 ...

  10. word从excel中获取数据

    '如 word开发工具不显示,文件 选项 自定义功能区 开发工具对钩选中 'Dim 字典 Dim SubArray(2, 200) As String Dim Row As Integer Dim I ...