[C# 笔记] 如何设置消息钩子 (以低级鼠标钩子为例)
<h2 id="简单入门">简单入门</h2><h3 id="1-准备函数">1. 准备函数<sup class="footnote-ref"></sup><sup class="footnote-ref"></sup><sup class="footnote-ref"></sup></h3>
<pre><code class="language-Csharp">
// 设置消息钩子
public static extern IntPtr SetWindowsHookExA(int idHook, HookProc lpfn, IntPtr hmod, int dwThreadId);
// 移除消息钩子
public static extern bool UnhookWindowsHookEx(IntPtr idHook);
// 继续运行下一个钩子 (其实是把钩子消息传递给下一个程序)
public static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam);
</code></pre>
<h3 id="2-准备结构体">2. 准备结构体<sup class="footnote-ref"></sup><sup class="footnote-ref"></sup></h3>
<pre><code class="language-csharp">// 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;
}
</code></pre>
<h3 id="3-定义委托类型">3. 定义委托类型</h3>
<pre><code class="language-csharp">// 定义一个委托类型, 给 WH_MOUSE_LL 回调函数用的
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
</code></pre>
<h3 id="4-捕捉到鼠标事件的时候-所要处理的回调函数-真正的业务逻辑代码在这">4. 捕捉到鼠标事件的时候, 所要处理的回调函数 (真正的业务逻辑代码在这)<sup class="footnote-ref"></sup></h3>
<pre><code class="language-csharp">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);
}
</code></pre>
<p>一些按键的值:</p>
<table>
<thead>
<tr>
<th>变量名</th>
<th>值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>WM_LBUTTONDOWN</td>
<td>0x0201</td>
<td>鼠标左键按下</td>
</tr>
<tr>
<td>WM_LBUTTONUP</td>
<td>0x0202</td>
<td>鼠标左键松开</td>
</tr>
<tr>
<td>WM_MOUSEMOVE</td>
<td>0x0200</td>
<td>鼠标移动</td>
</tr>
<tr>
<td>WM_MOUSEWHEEL</td>
<td>0x020A</td>
<td>鼠标滚轮</td>
</tr>
<tr>
<td>WM_RBUTTONDOWN</td>
<td>0x0204</td>
<td>鼠标右键按下</td>
</tr>
<tr>
<td>WM_RBUTTONUP</td>
<td>0x0205</td>
<td>鼠标右键松开</td>
</tr>
<tr>
<td>WM_MBUTTONDOWN</td>
<td>0x0207</td>
<td>鼠标中键按下</td>
</tr>
<tr>
<td>WM_MBUTTONUP</td>
<td>0x0208</td>
<td>鼠标中键放开</td>
</tr>
<tr>
<td>WM_XBUTTONDOWN</td>
<td>0x020B</td>
<td>鼠标侧键按下 (X1、X2都一样)</td>
</tr>
<tr>
<td>WM_XBUTTONUP</td>
<td>0x020C</td>
<td>鼠标侧键松开 (X1、X2都一样)</td>
</tr>
<tr>
<td>XBUTTON1</td>
<td>0x0001</td>
<td>鼠标侧键1的按下&松开</td>
</tr>
<tr>
<td>XBUTTON2</td>
<td>0x0002</td>
<td>鼠标侧键2的按下&松开</td>
</tr>
</tbody>
</table>
<p><strong>注意:</strong> XBUTTON1 和 XBUTTON2, 只能从 <code>tagMSLLHOOKSTRUCT.mouseData</code> 的高序字段中获取. <sup class="footnote-ref"></sup></p>
<h3 id="5-执行">5. 执行</h3>
<pre><code class="language-csharp">//把写好的回调函数, 赋值到 HookProc 这种委托类型 的变量里
public static HookProc hookproc = LLMouseProc;
//静态保存回调函数的句柄, 不然会被 GC 吃掉
public static IntPtr llmouseproc;
//开始部署消息钩子, 执行这一段函数之后, 就真正开始监听鼠标事件了
//SetWindowsHookExA: 第一个是消息类型, 第二个是 HookProc 这种委托类型的变量, 回调函数赋值在这里, 第三个和第四个正常不用管
llmouseproc = SetWindowsHookExA(14, hookproc, IntPtr.Zero, 0); //低级鼠标钩子消息类型, 值为 14
</code></pre>
<p><strong>注意:</strong> 对于一些已经 UAC 提权的应用, 该消息钩子无法捕捉到在目标应用下, 鼠标的坐标和状态, 必须将你的程序提权才能正常的捕捉到鼠标信息. <code>GetCursorPos()</code> 同理.</p>
<p>一些消息钩子类型:<sup class="footnote-ref"></sup></p>
<table>
<thead>
<tr>
<th>变量名</th>
<th>值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>WH_KEYBOARD</td>
<td>2</td>
<td>监听键盘输入消息, 需要注入</td>
</tr>
<tr>
<td>WH_KEYBOARD_LL</td>
<td>13</td>
<td>监听键盘输入消息, 不需要注入</td>
</tr>
<tr>
<td>WH_MOUSE</td>
<td>7</td>
<td>监听鼠标坐标和按键信息, 需要注入</td>
</tr>
<tr>
<td>WH_MOUSE_LL</td>
<td>14</td>
<td>监听鼠标坐标和按键信息, 不需要注入</td>
</tr>
</tbody>
</table>
<h3 id="6-结束">6. 结束</h3>
<p>在结束应用时, 记得手动把消息钩子给注销掉</p>
<pre><code class="language-CSharp">UnhookWindowsHookEx(llmouseproc);
</code></pre>
<h3 id="7-完整代码展示">7. 完整代码展示</h3>
<pre><code class="language-csharp">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);
// 设置消息钩子
public static extern IntPtr SetWindowsHookExA(int idHook, HookProc lpfn, IntPtr hmod, int dwThreadId);
// 移除消息钩子
public static extern bool UnhookWindowsHookEx(IntPtr idHook);
// 继续运行下一个钩子 (其实是把钩子消息传递给下一个程序)
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();
}
}
}
</code></pre>
<h3 id="8-大致流程">8. 大致流程</h3>
<ol>
<li>准备函数, 结构体, 委托类型;</li>
<li>编写回调函数;</li>
<li>把写好的回调函数, 赋值到 <code>HookProc</code> 这种委托类型的变量里;</li>
<li>使用 <code>SetWindowsHookExA()</code> 注册消息钩子, 并把返回的句柄保留起来;</li>
<li>使用 <code>UnhookWindowsHookEx()</code> 注销消息钩子, 结束运行.</li>
</ol>
<h2 id="进阶显示按键状态-是否按下">进阶:显示按键状态 (是否按下?)</h2>
<p>通过 <code>wParam</code> 可以获取当前按下了什么按键, 但是只会触发一次, 要让输出结果保持持久状态 (比如一直按下鼠标左键), 就得有个变量来暂存这些状态.</p>
<h3 id="1-暂存按键状态">1. 暂存按键状态</h3>
<p>首先创建个静态类用于存放按键状态</p>
<pre><code class="language-CSharp">// 按键状态
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;
}
</code></pre>
<h3 id="2-修改回调函数">2. 修改回调函数</h3>
<p>用 switch 来更新 <code>ButtonStatus</code> 类里的变量状态</p>
<pre><code class="language-CSharp">// 设置全局唯一一个 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);
}
</code></pre>
<p>其中, 侧键具体的某个按键是和滚轮偏移量一样, 存放在 <code>tagMSLLHOOKSTRUCT.mouseData</code> 的高序字中, 这就得进行移位转换.</p>
<p>只需改动这两处即可保存按键状态.</p>
<h2 id="附录">附录</h2>
<h3 id="开源">开源</h3>
<p>项目开源在: TestWindowsHook</p>
<h3 id="参考灵感来源">参考/灵感来源</h3>
<ol>
<li>C# Hook (一)</li>
<li>使用挂钩 - Win32 apps | Microsoft Learn</li>
</ol>
<h3 id="脚注">脚注</h3>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>SetWindowsHookExA 函数 (winuser.h) ↩︎</p>
</li>
<li id="fn2" class="footnote-item"><p>UnhookWindowsHookEx 函数 (winuser.h) ↩︎</p>
</li>
<li id="fn3" class="footnote-item"><p>CallNextHookEx 函数 (winuser.h) ↩︎</p>
</li>
<li id="fn4" class="footnote-item"><p>MSLLHOOKSTRUCT 结构 (winuser.h) ↩︎</p>
</li>
<li id="fn5" class="footnote-item"><p>POINT 结构 (windef.h) ↩︎</p>
</li>
<li id="fn6" class="footnote-item"><p>LowLevelMouseProc 函数 ↩︎</p>
</li>
<li id="fn7" class="footnote-item"><p>有关 XBUTTON1/2 值的获取 ↩︎</p>
</li>
<li id="fn8" class="footnote-item"><p>消息钩子类型有这些 ↩︎</p>
</li>
</ol>
</section><br><br>
来源:https://www.cnblogs.com/yuhang0000/p/19966780
頁:
[1]