WPF應用綁定系統快捷鍵
全局快捷鍵的應用
在現代桌面應用開發中,全局快捷鍵功能是提升用戶體驗的重要手段。用戶無需將焦點切換到應用窗口,就能通過特定的鍵盤組合快速觸發應用功能。本文以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。

浙公網安備 33010602011771號