[C# 笔记] 如何设置消息钩子 (以低级鼠标钩子为例)

简单入门

1. 准备函数[1][2][3]

[DllImport("User32")]
// 设置消息钩子
public static extern IntPtr SetWindowsHookExA(int idHook, HookProc lpfn, IntPtr hmod, int dwThreadId);

[DllImport("User32")]
// 移除消息钩子
public static extern bool UnhookWindowsHookEx(IntPtr idHook);

[DllImport("User32")]
// 继续运行下一个钩子 (其实是把钩子消息传递给下一个程序)
public static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam);

2. 准备结构体[4][5]

// POINT 结构体
public struct tagPOINT
{
    public int X;
    public int Y;
}

// MSLLHOOKSTRUCT 结构体
public struct tagMSLLHOOKSTRUCT
{
    // 光标的 XY 坐标
    public tagPOINT pt;
    // 鼠标额外数据: 滚轮信息或者侧键状态
    public int mouseData;
    // 事件注入的标志
    public int flags;
    // 此消息的时间戳
    public int time;
    // 与消息关联的其他信息
    public uint dwExtraInfo;
}

3. 定义委托类型

// 定义一个委托类型, 给 WH_MOUSE_LL 回调函数用的
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

4. 捕捉到鼠标事件的时候, 所要处理的回调函数 (真正的业务逻辑代码在这)[6]

public static IntPtr LLMouseProc(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode >= 0) //不建议处理 <0 的事件, 会出问题
    {
        //把数据赋值给结构体
        tagMSLLHOOKSTRUCT tag = Marshal.PtrToStructure<tagMSLLHOOKSTRUCT>(lParam);
        short wheel = 0;
        //如果响应的是滚轮事件
        if ((int)wParam == WM_Mouse.WM_MOUSEWHEEL) //WM_Mouse.WM_MOUSEWHEEL = 0x020A
        {
            wheel = (short)(tag.mouseData >> 16); //数据在 HIWORD, 即左半, 得把左半的字节搬到右半覆盖掉, 使用 short 保留符号
        }
        tagPOINT point = tag.pt;
        string button = "";
        //判断按下的是什么按键
        switch ((int)wParam)
        {
            case 0x020A: //滚轮
                button = "Wheel";
                break;
            case 0x020B: //侧键
                button = "MouseXButton";
                break;
            case 0x0201: //左键
                button = "MouseLeft";
                break;
            case 0x0204: //右键
                button = "MouseRight";
                break;
            case 0x0207: //中键
                button = "MouseMiddle";
                break;
            default:
                button = "";
                break;
        }
        string text = "X: " + point.X + "\tY: " + point.Y + "\tTime: " + tag.time + "\tButton: " + button;
        //打印
        Console.WriteLine(text);
    }
    //记得处理完逻辑代码, 就得把消息传递给其他进程
    return CallNextHookEx(llmouseproc, nCode, wParam, lParam);
}

一些按键的值:

变量名 说明
WM_LBUTTONDOWN 0x0201 鼠标左键按下
WM_LBUTTONUP 0x0202 鼠标左键松开
WM_MOUSEMOVE 0x0200 鼠标移动
WM_MOUSEWHEEL 0x020A 鼠标滚轮
WM_RBUTTONDOWN 0x0204 鼠标右键按下
WM_RBUTTONUP 0x0205 鼠标右键松开
WM_MBUTTONDOWN 0x0207 鼠标中键按下
WM_MBUTTONUP 0x0208 鼠标中键放开
WM_XBUTTONDOWN 0x020B 鼠标侧键按下 (X1、X2都一样)
WM_XBUTTONUP 0x020C 鼠标侧键松开 (X1、X2都一样)
XBUTTON1 0x0001 鼠标侧键1的按下&松开
XBUTTON2 0x0002 鼠标侧键2的按下&松开

注意: XBUTTON1 和 XBUTTON2, 只能从 tagMSLLHOOKSTRUCT.mouseData 的高序字段中获取. [7]

5. 执行

//把写好的回调函数, 赋值到 HookProc 这种委托类型 的变量里
public static HookProc hookproc = LLMouseProc;

//静态保存回调函数的句柄, 不然会被 GC 吃掉
public static IntPtr llmouseproc;

//开始部署消息钩子, 执行这一段函数之后, 就真正开始监听鼠标事件了
//SetWindowsHookExA: 第一个是消息类型, 第二个是 HookProc 这种委托类型的变量, 回调函数赋值在这里, 第三个和第四个正常不用管
llmouseproc = SetWindowsHookExA(14, hookproc, IntPtr.Zero, 0); //低级鼠标钩子消息类型, 值为 14

注意: 对于一些已经 UAC 提权的应用, 该消息钩子无法捕捉到在目标应用下, 鼠标的坐标和状态, 必须将你的程序提权才能正常的捕捉到鼠标信息. GetCursorPos() 同理.

一些消息钩子类型:[8]

变量名 说明
WH_KEYBOARD 2 监听键盘输入消息, 需要注入
WH_KEYBOARD_LL 13 监听键盘输入消息, 不需要注入
WH_MOUSE 7 监听鼠标坐标和按键信息, 需要注入
WH_MOUSE_LL 14 监听鼠标坐标和按键信息, 不需要注入

6. 结束

在结束应用时, 记得手动把消息钩子给注销掉

UnhookWindowsHookEx(llmouseproc);

7. 完整代码展示

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TestWindowsHook
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            this.Text = Application.ProductName;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Run();
        }

        // 定义一个委托类型, 给 WH_MOUSE_LL 回调函数用的
        public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

        [DllImport("User32")]
        // 设置消息钩子
        public static extern IntPtr SetWindowsHookExA(int idHook, HookProc lpfn, IntPtr hmod, int dwThreadId);

        [DllImport("User32")]
        // 移除消息钩子
        public static extern bool UnhookWindowsHookEx(IntPtr idHook);

        [DllImport("User32")]
        // 继续运行下一个钩子 (其实是把钩子消息传递给下一个程序)
        public static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam);

        // POINT 结构体
        public struct tagPOINT
        {
            public int X;
            public int Y;
        }

        // MSLLHOOKSTRUCT 结构体
        public struct tagMSLLHOOKSTRUCT
        {
            // 光标的 XY 坐标
            public tagPOINT pt;
            // 鼠标额外数据: 滚轮信息或者侧键状态
            public int mouseData;
            // 事件注入的标志
            public int flags;
            // 此消息的时间戳
            public int time;
            // 与消息关联的其他信息
            public uint dwExtraInfo;
        }

        /// <summary>
        /// WM_Mouse消息
        /// <para>
        /// <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-lbuttondown">WM_LBUTTONDOWN消息</a><br/>
        /// <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-lbuttonup">WM_LBUTTONUP消息</a><br/>
        /// <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-mousemove">WM_MOUSEMOVE消息</a><br/>
        /// <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-mousewheel">WM_MOUSEWHEEL消息</a><br/>
        /// <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-rbuttondown">WM_RBUTTONDOWN消息</a><br/>
        /// <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-rbuttonup">WM_RBUTTONUP消息</a><br/>
        /// <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-mbuttondown">WM_MBUTTONDOWN消息</a><br/>
        /// <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-mbuttonup">WM_MBUTTONUP消息</a><br/>
        /// <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-xbuttondown">WM_XBUTTONDOWN消息</a><br/>
        /// <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-xbuttonup">WM_XBUTTONUP消息</a><br/>
        /// </para>
        /// </summary>
        public static class WM_Mouse
        {
            /// <summary>
            /// 无
            /// </summary>
            public static int NONE = 0x0000;
            /// <summary>
            /// 鼠标左键按下
            /// </summary>
            public static int WM_LBUTTONDOWN = 0x0201;
            /// <summary>
            /// 鼠标左键松开
            /// </summary>
            public static int WM_LBUTTONUP = 0x0202;
            /// <summary>
            /// 鼠标移动
            /// </summary>
            public static int WM_MOUSEMOVE = 0x0200;
            /// <summary>
            /// 鼠标滚轮
            /// </summary>
            public static int WM_MOUSEWHEEL = 0x020A;
            /// <summary>
            /// 鼠标右键按下
            /// </summary>
            public static int WM_RBUTTONDOWN = 0x0204;
            /// <summary>
            /// 鼠标右键松开
            /// </summary>
            public static int WM_RBUTTONUP = 0x0205;
            /// <summary>
            /// 鼠标中键按下
            /// </summary>
            public static int WM_MBUTTONDOWN = 0x0207;
            /// <summary>
            /// 鼠标中键放开
            /// </summary>
            public static int WM_MBUTTONUP = 0x0208;
            /// <summary>
            /// 鼠标侧键按下
            /// </summary>
            public static int WM_XBUTTONDOWN = 0x020B;
            /// <summary>
            /// 鼠标侧键松开
            /// </summary>
            public static int WM_XBUTTONUP = 0x020C;

            /// <summary>
            /// 鼠标左键关闭
            /// </summary>
            public static int MK_LBUTTON = 0x0001;
            /// <summary>
            /// 鼠标右键关闭
            /// </summary>
            public static int MK_RBUTTON = 0x0002;
            /// <summary>
            /// Shift关闭
            /// </summary>
            public static int MK_SHIFT = 0x0004;
            /// <summary>
            /// Ctrl关闭
            /// </summary>
            public static int MK_CONTROL = 0x0008;
            /// <summary>
            /// 鼠标中键关闭
            /// </summary>
            public static int MK_MBUTTON = 0x0010;
            /// <summary>
            /// 鼠标侧键1关闭
            /// </summary>
            public static int MK_XBUTTON1 = 0x0020;
            /// <summary>
            /// 鼠标侧键2关闭
            /// </summary>
            public static int MK_XBUTTON2 = 0x0040;
        }

        /// <summary>
        /// WH_MOUSE_LL 的回调函数, 真正的业务逻辑处理在这
        /// </summary>
        /// <param name="nCode"></param>
        /// <param name="wParam"></param>
        /// <param name="lParam"></param>
        /// <returns></returns>
        public static IntPtr LLMouseProc(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0) //不建议处理 <0 的事件, 会出问题
            {
                //把数据赋值给结构体
                tagMSLLHOOKSTRUCT tag = Marshal.PtrToStructure<tagMSLLHOOKSTRUCT>(lParam);
                short wheel = 0;
                //如果响应的是滚轮事件
                if ((int)wParam == WM_Mouse.WM_MOUSEWHEEL) //WM_Mouse.WM_MOUSEWHEEL = 0x020A
                {
                    wheel = (short)(tag.mouseData >> 16); //数据在 HIWORD, 即左半, 得把左半的字节搬到右半覆盖掉, 使用 short 保留符号
                }
                tagPOINT point = tag.pt;
                string button = "";
                //判断按下的是什么按键
                switch ((int)wParam)
                {
                    case 0x020A: //滚轮
                        button = "Wheel";
                        break;
                    case 0x020B: //侧键
                        button = "MouseXButton";
                        break;
                    case 0x0201: //左键
                        button = "MouseLeft";
                        break;
                    case 0x0204: //右键
                        button = "MouseRight";
                        break;
                    case 0x0207: //中键
                        button = "MouseMiddle";
                        break;
                    default:
                        button = "";
                        break;
                }
                string text = "X: " + point.X + "\tY: " + point.Y + "\tTime: " + tag.time + "\tButton: " + button;
                //打印
                Console.WriteLine(text);
            }
            //记得处理完逻辑代码, 就得把消息传递给其他进程
            return CallNextHookEx(llmouseproc, nCode, wParam, lParam);
        }

        //把写好的回调函数, 赋值到 HookProc 这种委托类型 的变量里
        public static HookProc hookproc = LLMouseProc;

        //静态保存回调函数的句柄, 不然会被 GC 吃掉
        public static IntPtr llmouseproc;

        //开始执行
        public static void Run()
        {
            //开始部署消息钩子, 执行这一段函数之后, 就真正开始监听鼠标事件了
            //SetWindowsHookExA: 第一个是消息类型, 第二个是 HookProc 这种委托类型的变量, 回调函数赋值在这里, 第三个和第四个正常不用管
            llmouseproc = SetWindowsHookExA(14, hookproc, IntPtr.Zero, 0); //低级鼠标钩子消息类型, 值为 14
        }

        //退出时, 记得把消息钩子注销掉
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            UnhookWindowsHookEx(llmouseproc);
        }

        //退出按钮
        private void button1_Click(object sender, EventArgs e)
        {
            this.Close();
        }
    }
}

8. 大致流程

  1. 准备函数, 结构体, 委托类型;
  2. 编写回调函数;
  3. 把写好的回调函数, 赋值到 HookProc 这种委托类型的变量里;
  4. 使用 SetWindowsHookExA() 注册消息钩子, 并把返回的句柄保留起来;
  5. 使用 UnhookWindowsHookEx() 注销消息钩子, 结束运行.

进阶:显示按键状态 (是否按下?)

通过 wParam 可以获取当前按下了什么按键, 但是只会触发一次, 要让输出结果保持持久状态 (比如一直按下鼠标左键), 就得有个变量来暂存这些状态.

1. 暂存按键状态

首先创建个静态类用于存放按键状态

// 按键状态
public static class ButtonStatus
{
    /// <summary>
    /// 鼠标左键
    /// </summary>
    public static bool MouseLeft = false;
    /// <summary>
    /// 鼠标右键
    /// </summary>
    public static bool MouseRight = false;
    /// <summary>
    /// 鼠标中间
    /// </summary>
    public static bool MouseMiddle = false;
    /// <summary>
    /// 鼠标侧键1
    /// </summary>
    public static bool MouseXBotton1 = false;
    /// <summary>
    /// 鼠标侧键2
    /// </summary>
    public static bool MouseXBotton2 = false;
}

2. 修改回调函数

用 switch 来更新 ButtonStatus 类里的变量状态

// 设置全局唯一一个 StringBuilder
public static StringBuilder sb = new StringBuilder();

/// <summary>
/// WH_MOUSE_LL 的回调函数, 真正的业务逻辑处理在这
/// </summary>
/// <param name="nCode"></param>
/// <param name="wParam"></param>
/// <param name="lParam"></param>
/// <returns></returns>
public static IntPtr LLMouseProc(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode >= 0) //不建议处理 <0 的事件, 会出问题
    {
        //把数据赋值给结构体
        tagMSLLHOOKSTRUCT tag = Marshal.PtrToStructure<tagMSLLHOOKSTRUCT>(lParam);
        tagPOINT point = tag.pt;
        short wheel = 0;
        
        //判断按下的是什么按键
        switch ((int)wParam)
        {
            case 0x020A: //滚轮
                sb.Append("Wheel |");
                wheel = (short)(tag.mouseData >> 16);
                break;
            case 0x020B: //侧键按下
                short xbottondown = (short)((int)tag.mouseData >> 16);
                if(xbottondown == 0x0001)
                {
                    ButtonStatus.MouseXBotton1 = true;
                }
                else if (xbottondown == 0x0002)
                {
                    ButtonStatus.MouseXBotton2 = true;
                }
                break;
            case 0x0201: //左键按下
                ButtonStatus.MouseLeft = true;
                break;
            case 0x0204: //右键按下
                ButtonStatus.MouseRight= true;
                break;
            case 0x0207: //中键按下
                ButtonStatus.MouseMiddle = true;
                break;
            case 0x020C: //侧键释放
                short xbottonup = (short)((int)tag.mouseData >> 16);
                if (xbottonup == 0x0001)
                {
                    ButtonStatus.MouseXBotton1 = false;
                }
                else if (xbottonup == 0x0002)
                {
                    ButtonStatus.MouseXBotton2 = false;
                }
                break;
            case 0x0202: //左键释放
                ButtonStatus.MouseLeft = false;
                break;
            case 0x0205: //右键释放
                ButtonStatus.MouseRight= false;
                break;
            case 0x0208: //中键释放
                ButtonStatus.MouseMiddle = false;
                break;
            default:
                 
                break;
        }
        if(ButtonStatus.MouseLeft == true)
        {
            sb.Append(" MouseLeft |");
        }
        if(ButtonStatus.MouseRight == true)
        {
            sb.Append(" MouseRight |");
        }
        if(ButtonStatus.MouseMiddle == true)
        {
            sb.Append(" MouseMiddle |");
        }
        if(ButtonStatus.MouseXBotton1 == true)
        {
            sb.Append(" MouseXBotton1 |");
        }
        if(ButtonStatus.MouseXBotton2 == true)
        {
            sb.Append(" MouseXBotton2 |");
        }
        if (sb.Length > 0)
        {
            sb.Remove(sb.Length - 1, 1);
        }
        string text = "X: " + point.X + "\tY: " + point.Y + "\tWheel: " + wheel + "\tTime: " + tag.time + "\tButton: " + sb.ToString();
        sb.Clear();
        
        //打印
        Console.WriteLine(text);
    }
    //记得处理完逻辑代码, 就得把消息传递给其他进程
    return CallNextHookEx(llmouseproc, nCode, wParam, lParam);
}

其中, 侧键具体的某个按键是和滚轮偏移量一样, 存放在 tagMSLLHOOKSTRUCT.mouseData 的高序字中, 这就得进行移位转换.

只需改动这两处即可保存按键状态.

附录

开源

项目开源在: TestWindowsHook

参考/灵感来源

  1. [C#菜鸟]C# Hook (一)
  2. 使用挂钩 – Win32 apps | Microsoft Learn

脚注


  1. SetWindowsHookExA 函数 (winuser.h) ↩︎

  2. UnhookWindowsHookEx 函数 (winuser.h) ↩︎

  3. CallNextHookEx 函数 (winuser.h) ↩︎

  4. MSLLHOOKSTRUCT 结构 (winuser.h) ↩︎

  5. POINT 结构 (windef.h) ↩︎

  6. LowLevelMouseProc 函数 ↩︎

  7. 有关 XBUTTON1/2 值的获取 ↩︎

  8. 消息钩子类型有这些 ↩︎

文章摘自:https://www.cnblogs.com/yuhang0000/p/19966780