首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 使用 Windows Hook 監控滑鼠移動:SetWindowsHookEx 完整實作教學

     編輯:關於C#

前言

在 WinForms 應用程式中,有時需要**全域監控滑鼠移動**,即使滑鼠不在應用程式視窗範圍內也能捕捉座標。標準的 MouseMove 事件僅限於控制項內部,無法滿足跨視窗、全螢幕監控需求。解決方案是使用 **Windows Hook (鉤子) 機制**,透過 P/Invoke 呼叫 user32.dll 的 SetWindowsHookEx 安裝全域鉤子。本文提供完整的 C# 實作範例,包含鉤子類型選擇、委派簽名、事件封裝、資源釋放、以及常見陷阱 (如委派被 GC 回收) 的解決方案。

Windows Hook 核心概念

鉤子類型常數值用途適用性
WH_MOUSE7監控滑鼠訊息 (移動、點擊、滾輪) - **特定執行緒**單一執行緒應用
WH_MOUSE_LL14**低層級滑鼠鉤子** - 全系統、不需 DLL 注入、效能較好推薦:全域滑鼠監控
WH_GETMESSAGE3監控 GetMessage/PeekMessage 訊息佇列較不精確,不推薦用於滑鼠
WH_KEYBOARD_LL13低層級鍵盤鉤子 (全域熱鍵、按鍵記錄)鍵盤監控

關鍵差異:WH_MOUSE vs WH_MOUSE_LL

  • WH_MOUSE:需將鉤子程式放入 **DLL** 注入目標行程,跨行程複雜,現代 Windows 限制較多。
  • WH_MOUSE_LL (Low-Level):**不需 DLL**,直接在安裝行程中處理,**全系統生效**,回呼在安裝執行緒的訊息迴圈中執行,適合 WinForms/WPF 應用。

完整實作:GlobalMouseHook 類別

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;
        }
    }
}

WinForms 使用範例

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);
        }
    }
}

關鍵技術細節與避坑指南

1. 委派必須保持參考 (_hookProc 欄位)

最常見錯誤:直接傳遞 `new LowLevelMouseProc(HookCallback)` 給 `SetWindowsHookEx`,委派無根引用,GC 可能在下一次回收時釋放它,導致下一次鉤子回呼時 **AccessViolationException / ExecutionEngineException / 程式直接 Crash**。

解決:在建構函式實例化委派並存入 `readonly` 欄位 `_hookProc = HookCallback;`,確保存活期間不被回收。

2. WH_MOUSE_LL 回呼執行緒語境

  • 回呼在**安裝鉤子的執行緒訊息迴圈**中同步執行 (非 ThreadPool)。
  • **嚴禁在回呼中執行耗時操作** (I/O、資料庫、Thread.Sleep、MessageBox),會導致全系統滑鼠卡頓、訊息佇列堵塞。
  • 僅做:封裝事件參數、BeginInvoke 發送至 UI 執行緒、極簡邏輯判斷。

3. 座標系統:螢幕絕對座標

  • MSLLHOOKSTRUCT.pt 為**螢幕絕對座標** (0,0 在主螢幕左上角)。
  • 轉換為視窗客戶區座標:Point clientPt = this.PointToClient(e.Point);

4. 權限與 UAC

  • 低層級鉤子**不需要管理員權限**,標準使用者即可安裝。
  • 若目標應用以**管理員身分執行**,普通權限鉤子**無法攔截**該應用的滑鼠訊息 (UIPI 限制)。解決:監控端也以管理員執行,或使用 UI Automation。

5. 多螢幕與 DPI 感知

  • 多螢幕座標為虛擬螢幕座標系,負座標可能出現在次螢幕左側/上方。
  • 設定應用為 **Per-Monitor DPI Aware** (app.manifest 或 `Application.SetHighDpiMode`),確保座標與視窗座標一致。

6. 反偵測與安全性考量

  • 全域鉤子常被惡意軟體用於鍵盤記錄,**殺毒軟體可能攔截或警告**。
  • 簽署程式碼 (Code Signing)、避免不必要的全域鉤子、提供使用者關閉選項。

替代方案比較

總結

使用 SetWindowsHookEx 配合 WH_MOUSE_LL 實作全域滑鼠監控,是 WinForms/WPF 應用跨視窗捕捉滑鼠事件的標準方案。核心要點:**委派防 GC、回呼極簡非阻塞、正確釋放鉤子、處理 DPI/多螢幕/UAC**。本文提供的 GlobalMouseHook 類別封裝了完整的安裝、事件分發、資源釋放邏輯,可直接整合至專案中。若需跨管理員權限監控或更現代化架構,請評估 UI Automation 或 Raw Input 作為替代方案。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
方案優點缺點適用場景
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 佔用、無法捕捉點擊/滾輪簡單座標顯示、非即時需求