在 WinForms 應用程式中,有時需要**全域監控滑鼠移動**,即使滑鼠不在應用程式視窗範圍內也能捕捉座標。標準的 MouseMove 事件僅限於控制項內部,無法滿足跨視窗、全螢幕監控需求。解決方案是使用 **Windows Hook (鉤子) 機制**,透過 P/Invoke 呼叫 user32.dll 的 SetWindowsHookEx 安裝全域鉤子。本文提供完整的 C# 實作範例,包含鉤子類型選擇、委派簽名、事件封裝、資源釋放、以及常見陷阱 (如委派被 GC 回收) 的解決方案。
| 鉤子類型 | 常數值 | 用途 | 適用性 |
|---|---|---|---|
WH_MOUSE | 7 | 監控滑鼠訊息 (移動、點擊、滾輪) - **特定執行緒** | 單一執行緒應用 |
WH_MOUSE_LL | 14 | **低層級滑鼠鉤子** - 全系統、不需 DLL 注入、效能較好 | 推薦:全域滑鼠監控 |
WH_GETMESSAGE | 3 | 監控 GetMessage/PeekMessage 訊息佇列 | 較不精確,不推薦用於滑鼠 |
WH_KEYBOARD_LL | 13 | 低層級鍵盤鉤子 (全域熱鍵、按鍵記錄) | 鍵盤監控 |
WH_MOUSE:需將鉤子程式放入 **DLL** 注入目標行程,跨行程複雜,現代 Windows 限制較多。WH_MOUSE_LL (Low-Level):**不需 DLL**,直接在安裝行程中處理,**全系統生效**,回呼在安裝執行緒的訊息迴圈中執行,適合 WinForms/WPF 應用。using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace GlobalMouseHookDemo
{
///
/// 全域滑鼠鉤子封裝 (WH_MOUSE_LL)
///
public class GlobalMouseHook : IDisposable
{
#region P/Invoke 宣告
private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
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);
// 結構體對應 lParam (MSLLHOOKSTRUCT)
[StructLayout(LayoutKind.Sequential)]
private struct MSLLHOOKSTRUCT
{
public Point pt;
public uint mouseData;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}
private const int WH_MOUSE_LL = 14;
private const int WM_MOUSEMOVE = 0x0200;
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_LBUTTONUP = 0x0202;
private const int WM_RBUTTONDOWN = 0x0204;
private const int WM_RBUTTONUP = 0x0205;
private const int WM_MOUSEWHEEL = 0x020A;
#endregion
#region 欄位與事件
private readonly LowLevelMouseProc _hookProc; // 必須保持參考,防止 GC 回收
private IntPtr _hookHandle = IntPtr.Zero;
private bool _disposed = false;
///
/// 滑鼠移動事件
///
public event EventHandler MouseMove;
///
/// 滑鼠按下事件 (左/右/中鍵)
///
public event EventHandler MouseDown;
///
/// 滑鼠釋放事件
///
public event EventHandler MouseUp;
///
/// 滑鼠滾輪事件
///
public event EventHandler MouseWheel;
#endregion
public GlobalMouseHook()
{
// 實例化委派並保持參考,關鍵!否則 GC 回收導致 Crash
_hookProc = HookCallback;
}
///
/// 安裝鉤子
///
public void Install()
{
if (_hookHandle != IntPtr.Zero) return; // 已安裝
using (var currentProcess = Process.GetCurrentProcess())
using (var currentModule = currentProcess.MainModule)
{
IntPtr hMod = GetModuleHandle(currentModule.ModuleName);
_hookHandle = SetWindowsHookEx(WH_MOUSE_LL, _hookProc, hMod, 0);
if (_hookHandle == IntPtr.Zero)
{
int errorCode = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(errorCode, "安裝全域滑鼠鉤子失敗");
}
}
}
///
/// 移除鉤子
///
public void Uninstall()
{
if (_hookHandle != IntPtr.Zero)
{
bool result = UnhookWindowsHookEx(_hookHandle);
_hookHandle = IntPtr.Zero;
if (!result)
{
int errorCode = Marshal.GetLastWin32Error();
Debug.WriteLine($"移除鉤子失敗: {errorCode}");
}
}
}
///
/// 低層級滑鼠回呼 (在應用程式訊息迴圈執行緒中呼叫)
///
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0) // HC_ACTION = 0,處理訊息
{
var hookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
var point = hookStruct.pt; // 螢幕座標
var args = new MouseHookEventArgs(point, (MouseMessage)wParam);
switch ((MouseMessage)wParam)
{
case MouseMessage.WM_MOUSEMOVE:
MouseMove?.Invoke(this, args);
break;
case MouseMessage.WM_LBUTTONDOWN:
case MouseMessage.WM_RBUTTONDOWN:
case MouseMessage.WM_MBUTTONDOWN:
MouseDown?.Invoke(this, args);
break;
case MouseMessage.WM_LBUTTONUP:
case MouseMessage.WM_RBUTTONUP:
case MouseMessage.WM_MBUTTONUP:
MouseUp?.Invoke(this, args);
break;
case MouseMessage.WM_MOUSEWHEEL:
args.WheelDelta = (short)((hookStruct.mouseData >> 16) & 0xFFFF);
MouseWheel?.Invoke(this, args);
break;
}
}
// 傳遞給下一個鉤子
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
#region IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
Uninstall();
_disposed = true;
}
}
~GlobalMouseHook() => Dispose(false);
#endregion
}
///
/// 滑鼠訊息類型
///
public enum MouseMessage
{
WM_MOUSEMOVE = 0x0200,
WM_LBUTTONDOWN = 0x0201,
WM_LBUTTONUP = 0x0202,
WM_RBUTTONDOWN = 0x0204,
WM_RBUTTONUP = 0x0205,
WM_MBUTTONDOWN = 0x0207,
WM_MBUTTONUP = 0x0208,
WM_MOUSEWHEEL = 0x020A
}
///
/// 滑鼠鉤子事件參數
///
public class MouseHookEventArgs : EventArgs
{
public Point Point { get; } // 螢幕座標
public MouseMessage Message { get; }
public short WheelDelta { get; set; } // 滾輪增量 (正=向前/向上)
public MouseHookEventArgs(Point point, MouseMessage message)
{
Point = point;
Message = message;
}
}
}
using System;
using System.Windows.Forms;
namespace GlobalMouseHookDemo
{
public partial class MainForm : Form
{
private readonly GlobalMouseHook _mouseHook = new GlobalMouseHook();
public MainForm()
{
InitializeComponent();
// 訂閱事件
_mouseHook.MouseMove += OnGlobalMouseMove;
_mouseHook.MouseDown += OnGlobalMouseDown;
_mouseHook.MouseUp += OnGlobalMouseUp;
_mouseHook.MouseWheel += OnGlobalMouseWheel;
// 安裝鉤子
try
{
_mouseHook.Install();
Text = "全域滑鼠監控啟動中...";
}
catch (Exception ex)
{
MessageBox.Show($"鉤子安裝失敗: {ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void OnGlobalMouseMove(object sender, MouseHookEventArgs e)
{
// UI 執行緒安全更新 (WinForms 控制項需 Invoke)
if (InvokeRequired)
{
BeginInvoke(new Action(() => lblPosition.Text = $"滑鼠位置: {e.Point.X}, {e.Point.Y}"));
}
else
{
lblPosition.Text = $"滑鼠位置: {e.Point.X}, {e.Point.Y}";
}
}
private void OnGlobalMouseDown(object sender, MouseHookEventArgs e)
{
lstLog.Items.Insert(0, $"[{DateTime.Now:HH:mm:ss.fff}] 按下: {e.Message} @ {e.Point}");
}
private void OnGlobalMouseUp(object sender, MouseHookEventArgs e)
{
lstLog.Items.Insert(0, $"[{DateTime.Now:HH:mm:ss.fff}] 釋放: {e.Message} @ {e.Point}");
}
private void OnGlobalMouseWheel(object sender, MouseHookEventArgs e)
{
lstLog.Items.Insert(0, $"[{DateTime.Now:HH:mm:ss.fff}] 滾輪: Delta={e.WheelDelta} @ {e.Point}");
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
_mouseHook.Dispose(); // 確保移除鉤子
base.OnFormClosing(e);
}
}
}
最常見錯誤:直接傳遞 `new LowLevelMouseProc(HookCallback)` 給 `SetWindowsHookEx`,委派無根引用,GC 可能在下一次回收時釋放它,導致下一次鉤子回呼時 **AccessViolationException / ExecutionEngineException / 程式直接 Crash**。
解決:在建構函式實例化委派並存入 `readonly` 欄位 `_hookProc = HookCallback;`,確保存活期間不被回收。
BeginInvoke 發送至 UI 執行緒、極簡邏輯判斷。MSLLHOOKSTRUCT.pt 為**螢幕絕對座標** (0,0 在主螢幕左上角)。Point clientPt = this.PointToClient(e.Point);| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
WH_MOUSE_LL (本文方案) | 全域、無需 DLL、支援所有滑鼠事件 | 回呼同步阻塞訊息迴圈、需防 GC、UAC 限制 | 全螢幕滑鼠追蹤、跨視窗拖放、螢幕錄製、自動化測試 |
WH_MOUSE + DLL 注入 | 注入目標行程,效能較好 | 複雜、需 32/64 位元對應 DLL、現代 Windows 限制多 | 舊版相容、特定行程監控 |
| UI Automation | 無需鉤子、支援跨權限、官方推薦 | API 複雜、效能較低、僅限 UI 元素 | 無障礙、自動化測試、跨管理員權限監控 |
Raw Input (WM_INPUT) | 取得原始裝置資料、高精度、多裝置區分 | 需註冊裝置、僅限前景視窗接收 | 遊戲、高精度繪圖、多滑鼠區分 |
Timer + Cursor.Position | 簡單、無 P/Invoke、無權限問題 | 輪詢延遲、CPU 佔用、無法捕捉點擊/滾輪 | 簡單座標顯示、非即時需求 |