开yun体育app入口登录 NET/C# 使用 SetWindowsHookEx 监听鼠标或键盘消息以及此方法的坑

一般来说,大家在需要监听全局消息的时候都会考虑SetWindowsHookEx API。或者当你需要处理一些不是自己写的窗口的消息循环时你也会考虑使用它。

如果你想了解如何使用这个API,你可以在网上找到很多这样的文章/博客/教程/文档,但大多数都不会提到使用这个API时遇到的一些陷阱。通过阅读本文,您肯定会知道如何使用这个API,同时您还将学会如何正确使用它,以避免出现一些奇怪的问题。

基本用法

如果您在阅读本文时遇到一些问题,您可以考虑在 GitHub 上克隆我的源代码并尝试一下。这里:walterlv/Walterlv.Demo.SetWindowsHookEx。

为了简单起见,首先贴出一部分可以运行的代码。您可以直接将其放入您的项目中来运行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

public partial class MainWindow : Window
{
    private readonly HookProc _mouseHook;
    private IntPtr _hMouseHook;
    public MainWindow()
    {
        InitializeComponent();
        _mouseHook = OnMouseHook;
        Loaded += OnLoaded;
    }
    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        var hModule = GetModuleHandle(null);
        // 你可能会在网上搜索到下面注释掉的这种代码,但实际上已经过时了。
        //   下面代码在 .NET Core 3.x 以上可正常工作,在 .NET Framework 4.0 以下可正常工作。
        //   如果不满足此条件,你也可能可以正常工作,详情请阅读本文后续内容。
        // var hModule = Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]);
        _hMouseHook = SetWindowsHookEx(
            HookType.WH_MOUSE_LL,
            _mouseHook,
            hModule,
            0);
        if (_hMouseHook == IntPtr.Zero)
        {
            int errorCode = Marshal.GetLastWin32Error();
            throw new Win32Exception(errorCode);
        }
    }
    private IntPtr OnMouseHook(int nCode, IntPtr wParam, IntPtr lParam)
    {
        // 在这里,你可以处理全局鼠标消息。
        return CallNextHookEx(new IntPtr(0), nCode, wParam, lParam);
    }
}

本文讨论的是使用.NET/C#来完成对SetWindowsHookEx的调用,那么P/Invoke(平台调用)自然是少不了的。因此,您还必须将以下代码添加到您的存储库中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32", SetLastError = true)]
static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("user32.dll")]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr SetWindowsHookEx(HookType hookType, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
public enum HookType : int
{
    WH_JOURNALRECORD = 0,
    WH_JOURNALPLAYBACK = 1,
    WH_KEYBOARD = 2,
    WH_GETMESSAGE = 3,
    WH_CALLWNDPROC = 4,
    WH_CBT = 5,
    WH_SYSMSGFILTER = 6,
    WH_MOUSE = 7,
    WH_HARDWARE = 8,
    WH_DEBUG = 9,
    WH_SHELL = 10,
    WH_FOREGROUNDIDLE = 11,
    WH_CALLWNDPROCRET = 12,
    WH_KEYBOARD_LL = 13,
    WH_MOUSE_LL = 14
}

设置WindowsHookEx

SetWindowsHookEx的签名如下:

1
2
3
4
5
6

HHOOK SetWindowsHookExA(
  int       idHook,
  HOOKPROC  lpfn,
  HINSTANCE hmod,
  DWORD     dwThreadId
);

传递坑模块句柄时要注意什么?

在本文开头注释掉的代码中,我使用 Marshal 直接从托管程序集中获取模块句柄。

这里需要注意的是,托管程序集无法注入到其他进程中,因此无法挂载钩子。但也有例外。 WH_KEYBOARD_LL或WH_MOUSE_LL不需要注入dll,因此可以挂载hook。

对于WH_KEYBOARD_LL和WH_MOUSE_LL,SetWindowsHookEx方法根本不使用这个模块做任何实际的事情,它只是验证一个模块。仅存在于您的流程中。

因此,可以传入其他模块:

1

var hModule = LoadLibrary("user32.dll");

也可以传入入口模块:

1

var hModule = Marshal.GetHINSTANCE(Assembly.GetEntryAssembly().GetModules()[0]);

1

var hModule = GetModuleHandle(null);

这也是我一开始在P/Invoke方法中保留LoadLibrary和GetModuleHandle方法的原因。

通过调试我们也可以发现这两者的入口模块是相同的:

入口模块句柄

至于为什么可以使用user32.dll。好吧,不管怎么说,我们在创建窗口监控消息的时候,已经调用了user32.dll的很多API了。这个dll肯定已经添加到我们的进程中了,所以当我们将其传递给参数时就可以验证它。

错误126:找不到指定的模块。

找不到指定的模块。

如果你只是将代码用作demo,可能一切都很顺利,但是当你把它放到实际项目中时,就会失败得一塌糊涂:

找不到指定的模块

这也是我在最初的P/Invoke中添加SetLassError的一个重要原因,因为这个API很容易挂掉。

检查的错误代码为 126 (0x0000007E)。

但是,我的dll存在!

我们再看一下我一开始预留的笔记:

1
2
3

// 下面代码在 .NET Core 3.x 以上可正常工作,在 .NET Framework 4.0 以下可正常工作。
// 如果不满足此条件,你也可能可以正常工作,详情请阅读本文后续内容。
var hModule = Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]);

是的,如果您遇到这样的异常,这可能意味着您正在陷入.NET Framework 4.x版本的运行时。

.NET Framework 4.0 与之前的 CLR 相比发生了重大变化。它不再假装 JIT 代码存在于非托管模块中,因此 Marshal.GetHINSTANCE 将不再起作用。

对于低级钩子,SetWindowsHookEx 需要一个有效的模块句柄来检查,但实际上执行此 API 时根本不使用此模块。因此,建议使用上一节提供的LoadLibrary函数来获取模块句柄,而不是获取当前托管模块的句柄。

解决方案,二/三:

方法1:使用LoadLibrary("user32.dll") 来获取模块句柄,而不是Marshal.GetHINSTANCE。方法二:更改模块获取入口程序集(exe)的句柄,即Assembly.GetEntryAssembly()。方法3:升级到纯.NET Core 程序错误1428:无法设置没有模块句柄的非本机钩子。

如果没有模块句柄,则无法设置非本地挂钩。

对于前面提到的126错误,从Assembly.GetExecutingAssembly更改为Assembly.GetEntryAssembly()后可能会出现此异常。

解决方案:

错误1429:该钩子只能全局设置。

该钩子过程只能全局设置。

估计找到这个地方的办法就是搜索,因为这段中文实在是太晦涩难懂了。不过我把英文贴到了上一行,相信你就差不多知道是怎么回事了。

因为你为SetWindowsHookEx方法中传入的HookType参数指定了低级类型(Low Level,HookType枚举后带有LL后缀)开yun体育app入口登录,所以此时只能全局设置hook。这意味着你的第四个参数必须传入0。

如何只处理特定窗口的消息?

消息循环属于“线程”,而不属于窗口或进程。 CreateWindowEx创建窗口时传入的消息处理函数只会处理特定窗口的消息。但通过钩子处理消息时,无法准确定位到具体窗口,只能针对消息循环所在线程。因此,要处理特定窗口的消息开yun体育官网入口登录体育,只能先获取这个窗口所在的线程。

在前面的P/Invoke中,我还预留了一个获取窗口所在线程的方法。因此,可以直接使用下面的调用来获取窗口所在线程的h​​Wnd句柄。

1

var threadId = GetWindowThreadProcessId(hWnd, out _);

最初,0作为SetWindowsHookEx的最后一个参数传入,表示全局钩子。现在,传入threadId意味着仅监视来自该线程的消息。

另外,如果您只打算处理来自单个窗口的消息,而不是该线程中的所有消息,建议使用子类化。详细内容请阅读我的另一篇博客:

为什么会导致其他进程崩溃?

你可能会发现,尽管你按照本文介绍的方法挂上了钩子,但其他程序(被钩住的程序)运行后却崩溃了。

接下来解释一下:

所有类型的HookType中,只有WH_MOUSE_LL和WH_KEYBOARD_LL不需要注入到目标进程中。其他人必须将dll注入目标进程才能完成hook。但是,.NET 程序集无法注入到其他进程中;当使用其他dll时,其中没有挂载的函数地址,注入后会导致目标进程崩溃。

所以:

如果需要挂载的进程在本进程内(最后一个参数指定的线程就是本进程内的线程),则所有类型都可以挂载;如果需要全局挂载,或者挂载其他进程云开·全站app中心手机版,那么 .NET 程序只能使用两种钩子类型:WH_MOUSE_LL 和 WH_KEYBOARD_LL;

如果你只想挂其他类型的钩子怎么办?总有一个办法:

可以考虑制作一个非托管dll专门用于hook;可以考虑使用SetWinEventHook,不需要注入到目标进程中;您可以考虑使用System.Windows.Automation来捕获一些有限的信息。其他问题

如果你还是遇到问题,你可以考虑在 GitHub 上克隆我的源代码并尝试一下。这里:walterlv/Walterlv.Demo.SetWindowsHookEx。或者通过本文末尾的联系信息与我联系。

参考

本文会经常更新,请阅读原文:避免被旧的、错误的知识误导,有更好的阅读体验。

本作品根据 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License 获得许可。欢迎转载、使用、转载,但务必保留文章署名陆毅(含链接:),不得用于商业用途。基于本文的修改作品必须在同一许可证下发布。如果您有任何疑问,请。

关键词:

客户评论

我要评论