Windows Hook(鉤子)是一種強大的訊息攔截機制,允許應用程式在訊息到達目標視窗前進行監控、修改或阻擋。本文系統整理 Hook 核心概念、14 種 Hook 類型、鏈結運作原理、安裝/卸載 API,並提供一個可直接用於 C# 專案的**全域鍵盤/滑鼠監控類別完整原始碼**。
Windows 以訊息驅動:使用者輸入(鍵盤/滑鼠)、系統事件、應用程式發送訊息,皆會進入系統訊息佇列,再分派至目標視窗程序。Hook 允許你在訊息分派**之前**插入自訂回呼函數,形成一條 **Hook Chain(鉤子鏈)**。
| 常數 | 值 | 用途 | 備註 |
|---|---|---|---|
| WH_KEYBOARD | 2 | 鍵盤訊息 (WM_KEYDOWN/UP) | 需 DLL 全域 |
| WH_MOUSE | 7 | 滑鼠訊息 | 需 DLL 全域 |
| WH_KEYBOARD_LL | 13 | 低階鍵盤攔截 | **免 DLL**,全域 |
| WH_MOUSE_LL | 14 | 低階滑鼠攔截 | **免 DLL**,全域 |
| WH_GETMESSAGE | 3 | GetMessage/PeekMessage 返回前 | 可修改訊息 |
| WH_CALLWNDPROC | 4 | SendMessage 呼叫前 | 不可修改訊息 |
| WH_CBT | 5 | 視窗建立/啟動/銷毀等 | 可阻擋視窗操作 |
| WH_SHELL | 10 | Shell 事件(視窗建立/工作列) | 需註冊才收到 |
| WH_JOURNALRECORD | 0 | 記錄輸入事件 | 全域,免 DLL |
| WH_JOURNALPLAYBACK | 1 | 回放輸入事件 | 全域,免 DLL |
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
idHook:Hook 類型常數。lpfn:回呼委派,**務必保持委派實例存活**(防止 GC 回收導致 Crash)。hMod:包含回呼函數的模組控制代碼。全域 Hook (非 LL) 傳 DLL Handle;區域 Hook 或 LL Hook 傳 Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]) 或 IntPtr.Zero。dwThreadId:0 = 全域;特定執行緒 ID = 區域/遠端執行緒。IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0) // HC_ACTION = 0,表示處理訊息
{
// 解析 lParam 結構體 (如 KBDLLHOOKSTRUCT, MSLLHOOKSTRUCT)
// 觸發 C# 事件
// 若設定 handled = true,可回傳 1 阻斷訊息傳遞
}
return CallNextHookEx(_hookHandle, nCode, wParam, lParam); // 傳遞給下一個 Hook
}
以下類別封裝了 `WH_KEYBOARD_LL` 與 `WH_MOUSE_LL`,提供 OnMouseActivity、KeyDown、KeyPress、KeyUp 事件,開箱即用:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace GlobalHooks
{
public class UserActivityHook : IDisposable
{
#region P/Invoke
[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int x, y; }
[StructLayout(LayoutKind.Sequential)]
private struct MouseLLHookStruct { public POINT pt; public int mouseData, flags, time, dwExtraInfo; }
[StructLayout(LayoutKind.Sequential)]
private struct KeyboardHookStruct { public int vkCode, scanCode, flags, time, dwExtraInfo; }
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern short GetKeyState(int vKey);
[DllImport("user32.dll")]
private static extern int ToAscii(int uVirtKey, int uScanCode, byte[] lpbKeyState, byte[] lpwTransKey, int fuState);
[DllImport("user32.dll")]
private static extern int GetKeyboardState(byte[] pbKeyState);
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
#endregion
#region Constants
private const int WH_MOUSE_LL = 14;
private const int WH_KEYBOARD_LL = 13;
private const int WM_MOUSEMOVE = 0x200;
private const int WM_LBUTTONDOWN = 0x201;
private const int WM_RBUTTONDOWN = 0x204;
private const int WM_MBUTTONDOWN = 0x207;
private const int WM_LBUTTONUP = 0x202;
private const int WM_RBUTTONUP = 0x205;
private const int WM_MBUTTONUP = 0x208;
private const int WM_MOUSEWHEEL = 0x020A;
private const int WM_KEYDOWN = 0x100;
private const int WM_KEYUP = 0x101;
private const int WM_SYSKEYDOWN = 0x104;
private const int WM_SYSKEYUP = 0x105;
private const byte VK_SHIFT = 0x10;
private const byte VK_CAPITAL = 0x14;
private const byte VK_NUMLOCK = 0x90;
#endregion
#region Fields & Events
private IntPtr _mouseHook = IntPtr.Zero;
private IntPtr _keyboardHook = IntPtr.Zero;
private HookProc _mouseProc;
private HookProc _keyboardProc;
public event MouseEventHandler OnMouseActivity;
public event KeyEventHandler KeyDown;
public event KeyPressEventHandler KeyPress;
public event KeyEventHandler KeyUp;
#endregion
public UserActivityHook(bool installMouse = true, bool installKeyboard = true)
{
Start(installMouse, installKeyboard);
}
public void Start(bool installMouse = true, bool installKeyboard = true)
{
if (installMouse && _mouseHook == IntPtr.Zero)
{
_mouseProc = MouseHookProc;
_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, _mouseProc,
Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0);
if (_mouseHook == IntPtr.Zero) ThrowWin32();
}
if (installKeyboard && _keyboardHook == IntPtr.Zero)
{
_keyboardProc = KeyboardHookProc;
_keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, _keyboardProc,
Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0);
if (_keyboardHook == IntPtr.Zero) ThrowWin32();
}
}
public void Stop()
{
if (_mouseHook != IntPtr.Zero) { UnhookWindowsHookEx(_mouseHook); _mouseHook = IntPtr.Zero; }
if (_keyboardHook != IntPtr.Zero) { UnhookWindowsHookEx(_keyboardHook); _keyboardHook = IntPtr.Zero; }
}
public void Dispose() => Stop();
~UserActivityHook() => Stop();
private void ThrowWin32() => throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
#region Hook Procs
private IntPtr MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && OnMouseActivity != null)
{
var info = Marshal.PtrToStructure(lParam);
MouseButtons btn = MouseButtons.None;
int clicks = 0, delta = 0;
switch ((int)wParam)
{
case WM_LBUTTONDOWN: case WM_LBUTTONUP: case WM_LBUTTONDBLCLK: btn = MouseButtons.Left; break;
case WM_RBUTTONDOWN: case WM_RBUTTONUP: case WM_RBUTTONDBLCLK: btn = MouseButtons.Right; break;
case WM_MBUTTONDOWN: case WM_MBUTTONUP: btn = MouseButtons.Middle; break;
case WM_MOUSEWHEEL: delta = (short)((info.mouseData >> 16) & 0xFFFF); break;
}
if (btn != MouseButtons.None)
clicks = (wParam == (IntPtr)WM_LBUTTONDBLCLK || wParam == (IntPtr)WM_RBUTTONDBLCLK) ? 2 : 1;
var e = new MouseEventArgs(btn, clicks, info.pt.x, info.pt.y, delta);
OnMouseActivity(this, e);
}
return CallNextHookEx(_mouseHook, nCode, wParam, lParam);
}
private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
bool handled = false;
if (nCode >= 0 && (KeyDown != null || KeyUp != null || KeyPress != null))
{
var info = Marshal.PtrToStructure(lParam);
int vk = info.vkCode;
Keys key = (Keys)vk;
if ((KeyDown != null) && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN))
{
var e = new KeyEventArgs(key);
KeyDown(this, e);
handled |= e.Handled;
}
if (KeyPress != null && wParam == (IntPtr)WM_KEYDOWN)
{
byte[] ks = new byte[256]; GetKeyboardState(ks);
byte[] buf = new byte[2];
if (ToAscii(vk, info.scanCode, ks, buf, info.flags) == 1)
{
char ch = (char)buf[0];
bool shift = (GetKeyState(VK_SHIFT) & 0x80) != 0;
bool caps = (GetKeyState(VK_CAPITAL) & 1) != 0;
if ((shift ^ caps) && char.IsLetter(ch)) ch = char.ToUpper(ch);
var e = new KeyPressEventArgs(ch);
KeyPress(this, e);
handled |= e.Handled;
}
}
if (KeyUp != null && (wParam == (IntPtr)WM_KEYUP || wParam == (IntPtr)WM_SYSKEYUP))
{
var e = new KeyEventArgs(key);
KeyUp(this, e);
handled |= e.Handled;
}
}
return handled ? (IntPtr)1 : CallNextHookEx(_keyboardHook, nCode, wParam, lParam);
}
#endregion
}
}
public partial class MainForm : Form
{
private UserActivityHook _hook;
public MainForm()
{
InitializeComponent();
_hook = new UserActivityHook(true, true);
_hook.OnMouseActivity += (s, e) => lblMouse.Text = $"{e.Button} @ ({e.X}, {e.Y})";
_hook.KeyDown += (s, e) => lblKey.Text = $"Down: {e.KeyCode}";
_hook.KeyPress += (s, e) => lblKeyPress.Text = $"Press: {e.KeyChar}";
_hook.KeyUp += (s, e) => lblKeyUp.Text = $"Up: {e.KeyCode}";
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
_hook?.Dispose();
base.OnFormClosing(e);
}
}
Windows Hook 是雙面刃:功能強大但責任重大。掌握「區域 vs 全域」、「一般 vs 低階」、「DLL 注入 vs 免 DLL」的選型邏輯,並嚴守「回呼極速返回、委派防 GC、用畢即卸載」三大鐵律,即能在 C# 安全驅馭這項底層技術,實作熱鍵、巨集錄製、使用者行為分析等進階功能。