当前位置: 首页 > news >正文

[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. 消息钩子类型有这些 ↩︎

http://www.jsqmd.com/news/740156/

相关文章:

  • 为什么选择优德营造Omakase设计打造你的日料餐厅?
  • 原神游戏数据采集与分析实战指南
  • 2026 选什么降 AI 软件不踩坑?看排行前先搞懂这 3 个降 AI 平台差异。 - 我要发一区
  • GSPO算法:序列级策略优化在旅行规划中的应用
  • **2026年5月六西格玛认证排行榜|黑带VS绿带含金量与报考评价** - 众智商学院课程中心
  • Linux系统PPP拨号全攻略:从串口调试到断线自动重连的完整实现
  • 04 接雨水 单调栈
  • Ultralytics LLM:将YOLO工程哲学带入大语言模型应用开发
  • 开源桌面示波器Haasoscope:FPGA+MCU架构与Python客户端全解析
  • 深度解析applera1n:基于checkm8漏洞的iOS激活锁绕过技术实现
  • 中山AI优化提供商哪家强?原来有这些选择!
  • OBS虚拟摄像头进阶玩法:除了共享屏幕,还能在腾讯会议里玩出什么花?
  • 毕业答辩前选哪款降 AI 软件?2026 排行前 5 让 AI 率降到 5% 以下! - 我要发一区
  • 第二章、application.properties文件的配置
  • 2026年5月六西格玛绿带黑带含金量排行|报考避坑榜Top5 - 众智商学院课程中心
  • Ubuntu Server 24.04下解决SunloginClient 向日葵依赖libgconf-2-4安装问题
  • SAP SD新手避坑:VA01创建销售订单报‘无定价过程’?手把手教你用OVKK搞定配置
  • 从Pikachu靶场看企业级Web安全:这些漏洞在真实业务中如何防御?
  • MAA明日方舟自动化助手完整指南:如何一键解放双手高效长草
  • 论文 AI 率从 78% 降到 3.2%!2026 排行前 3 降 AI 软件让你赶上答辩。 - 我要发一区
  • ESXi 7.0U3迁移实战:手把手教你用命令行把旧主机配置‘克隆’到新服务器
  • 告别串口助手!手把手教你用TC264打造一个“硬件版”参数配置器
  • 【读书笔记】《你就是孩子最好的玩具》
  • 2026年05月六西格玛黑带绿带推荐榜单:含金量排行与报考避坑指南 - 众智商学院课程中心
  • 保姆级教程:在Ubuntu 22.04上从源码编译安装Eclipse Paho C库,并手把手写一个MQTT同步客户端
  • OpenClown:为AI助手配备多维度专家评审团,提升输出质量与安全性
  • ROS2 C++开发系列04:如何有效输出机器人状态
  • 别再混着用了!搞懂nvidia-docker在WSL和物理Ubuntu下的不同‘脾气’,彻底解决GPU容器启动报错
  • UAGLNet:遥感图像建筑提取的多尺度特征融合技术
  • 保姆级教程:手把手教你用ONVIF协议,把乐橙WiFi摄像头稳定添加到海康威视DS-7104N录像机