在 Windows 應用程式中實作全域熱鍵、鍵盤記錄或輸入攔截,需使用 Win32 API SetWindowsHookEx 安裝鍵盤鉤子 (WH_KEYBOARD 或 WH_KEYBOARD_LL)。本文提供完整的 C# P/Invoke 封裝類別、鉤子處理流程、執行緒親和性注意事項與範例應用。
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class KeyboardHook : IDisposable
{
// 鉤子類型
private enum HookType : int
{
WH_KEYBOARD = 2, // 執行緒鉤子 (需 DLL 注入)
WH_KEYBOARD_LL = 13 // 低階鉤子 (全域、無需 DLL、建議使用)
}
// 訊息常數
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private const int WM_SYSKEYDOWN = 0x0104;
private const int WM_SYSKEYUP = 0x0105;
// 委派簽名 (對應 HOOKPROC)
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
// Win32 API
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(HookType idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
// 鍵盤結構 (WH_KEYBOARD_LL 使用)
[StructLayout(LayoutKind.Sequential)]
private struct KBDLLHOOKSTRUCT
{
public uint vkCode; // 虛擬鍵碼
public uint scanCode; // 掃描碼
public uint flags; // 標誌
public uint time; // 時間戳
public IntPtr dwExtraInfo;
}
// 實例欄位
private IntPtr _hookHandle = IntPtr.Zero;
private HookProc _hookProc;
private readonly object _lock = new();
// 事件:鍵盤按下/釋放
public event EventHandler KeyDown;
public event EventHandler KeyUp;
public KeyboardHook()
{
_hookProc = HookCallback; // 必須保持委派參考,防止 GC 回收
}
public void Install()
{
if (_hookHandle != IntPtr.Zero) return;
// 使用 WH_KEYBOARD_LL (低階鉤子) - 全域、不需 DLL、當前執行緒即可
_hookHandle = SetWindowsHookEx(HookType.WH_KEYBOARD_LL, _hookProc,
GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName), 0);
if (_hookHandle == IntPtr.Zero)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
public void Uninstall()
{
if (_hookHandle != IntPtr.Zero)
{
UnhookWindowsHookEx(_hookHandle);
_hookHandle = IntPtr.Zero;
}
}
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0) // HC_ACTION = 0
{
int msg = wParam.ToInt32();
var kb = Marshal.PtrToStructure(lParam);
Keys key = (Keys)kb.vkCode;
var args = new KeyEventArgs(key);
switch (msg)
{
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
KeyDown?.Invoke(this, args);
break;
case WM_KEYUP:
case WM_SYSKEYUP:
KeyUp?.Invoke(this, args);
break;
}
// 若 Handled=true,攔截鍵盤訊息不再傳遞
if (args.Handled) return (IntPtr)1;
}
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
public void Dispose() => Uninstall();
}
// 使用範例
public class DemoForm : Form
{
private KeyboardHook _hook;
public DemoForm()
{
_hook = new KeyboardHook();
_hook.KeyDown += (s, e) =>
{
if (e.KeyCode == Keys.F12)
{
MessageBox.Show("全域熱鍵 F12 被觸發!");
e.Handled = true; // 攔截
}
};
_hook.Install();
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_hook.Dispose();
base.OnFormClosed(e);
}
}
| 特性 | WH_KEYBOARD (2) | WH_KEYBOARD_LL (13) |
|---|---|---|
| 範圍 | 特定執行緒 | 全域 (所有執行緒) |
| 需 DLL 注入 | 是 (若跨進程) | 否 |
| 效能影響 | 較低 | 較高 (每鍵盤事件切換至安裝進程) |
| 適用場景 | 同進程內部攔截 | 全域熱鍵、跨進程監聽 |
| UAC/完整性等級 | 受限 | 需相同或較高完整性等級 |