news 2025/12/14 7:47:36

WPF应用绑定系统快捷键

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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; // 找到并关闭后退出循环

}

}

});

}

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/12 18:16:52

安宝特 FME:零代码实时数据管理标杆,Safe Software 中国授权合作首选

在数字化转型加速的今天&#xff0c;企业对数据集成、管理效率的需求日益迫切。Safe Software 旗下的 FME 空间数据集成平台&#xff0c;凭借全数据兼容、零代码操作、实时响应的核心优势&#xff0c;成为 25,000 全球企业的信赖之选。而安宝特作为虹科姐妹公司&#xff0c;正…

作者头像 李华
网站建设 2025/12/13 20:47:49

GPT-5.2全面解析:从代码到长文档分析,AI助手新标杆

GPT-5.2作为OpenAI的背水一战之作&#xff0c;分为Instant、Thinking和Pro三版&#xff0c;在编程、长文档分析和视觉识别上实现突破&#xff0c;Thinking版在70.9%任务中达到人类专家水平。然而&#xff0c;其高延迟、昂贵成本及技术护城河消失等问题凸显。OpenAI面临200亿美元…

作者头像 李华
网站建设 2025/12/12 18:16:25

34、深入解析NFS服务器性能优化策略

深入解析NFS服务器性能优化策略 1. 网络问题排查与服务器性能概述 在网络环境中,数据包丢失可能由本地主机接口、网络拥塞或远程主机接口等多种原因导致。要消除所有与网络相关的影响,就必须对这些方面进行逐一检查。 当服务器无法处理新请求,或者不能有效调度和处理已接…

作者头像 李华
网站建设 2025/12/12 18:16:19

大模型学习全攻略:从理论基础到企业应用,一份资料搞定AI大模型学习

本文提供全面的AI大模型学习资料包&#xff0c;包含视频教程、学习路线、技术文档、行业报告、实战项目和面试真题。强调学习大模型对企业降本增效、产品创新及个人薪资提升、职业发展的重要性。资料由清华大学-加州理工双料博士领衔研发&#xff0c;适合零基础或有一定技术基础…

作者头像 李华
网站建设 2025/12/12 18:16:18

36、优化NFS和NIS网络性能:从拓扑到客户端调优

优化NFS和NIS网络性能:从拓扑到客户端调优 1. 网络拓扑与磁盘无客户端启动 1.1 网络拓扑的重要性 将客户端和服务器置于路由器同一侧,能显著减轻路由器负载。对于磁盘无客户端,为其配备足够内存可进行积极缓存,减少与服务器的往返次数。 1.2 跨路由器启动磁盘无客户端的…

作者头像 李华
网站建设 2025/12/12 18:16:00

金山办公基于 DeepFlow docker 模式的可观测性实践

摘要&#xff1a;本文分享了金山办公在单元化架构转型背景下&#xff0c;基于DeepFlow可观测性平台在纯Docker环境中的全栈落地实践。面对从K8s微服务架构向单元化架构的演进&#xff0c;团队通过DeepFlow实现了从基础设施到应用层的统一数据采集、性能剖析与智能诊断。文中详细…

作者头像 李华