You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

C#中SetWindowsHookEx始终返回0,WH_KEYBOARD钩子设置失败求助

排查WH_KEYBOARD钩子设置的参数问题

看起来你在尝试给第三方应用挂键盘钩子时碰到了参数相关的问题,结合你给出的代码片段,我整理了几个常见的坑和修复方案:

1. 先检查委托签名的兼容性

系统对WH_KEYBOARD钩子的回调函数有严格的签名要求,你的代码里定义的HookProc委托可能存在不匹配的问题:

// 你的定义
private delegate int HookProc(int code, IntPtr wParam, IntPtr lParam);

正确的C#签名应该匹配系统的KeyboardProc原型,返回值建议用IntPtr(对应Win32的LRESULT,兼容32/64位环境):

private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

签名不匹配会直接导致参数传递错误,触发SetWindowsHookEx调用失败。

2. 补全SetWindowsHookEx的参数并修正逻辑

你的代码里SetWindowsHookEx的DllImport不完整,完整的导入应该包含所有必填参数:

[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);

这里几个参数特别容易出错:

  • hMod(模块句柄)
    • 如果是全局钩子(挂钩所有进程):WH_KEYBOARD全局钩子要求钩子函数必须放在非托管DLL中(因为系统需要把DLL注入到目标进程),C#托管代码直接做不到这点,建议改用WH_KEYBOARD_LL(钩子ID=13),这个钩子不需要注入DLL,所有事件会在你的进程里处理。
    • 如果是线程钩子(仅挂钩目标进程的单个线程):需要传IntPtr.Zero,但前提是钩子函数和目标线程在同一个进程(显然你不是这种场景)。
  • dwThreadId(线程ID)
    你代码里用了uint processHandle,但SetWindowsHookEx需要的是线程ID,不是进程句柄。你需要先用GetWindowThreadProcessId获取目标窗口对应的线程ID:
    [DllImport("user32.dll")]
    static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
    
    // 调用示例:
    uint targetProcessId;
    uint targetThreadId = GetWindowThreadProcessId(windowHandle, out targetProcessId);
    

3. 注意委托的生命周期

你定义的PaintHookProcedure(名字看起来和键盘钩子不匹配,建议改名避免混淆)必须保持类级别的引用,不能被GC回收。因为SetWindowsHookEx会保存委托的内存地址,如果委托被回收,后续钩子触发时会导致内存访问错误,甚至程序崩溃。

4. 错误排查的关键步骤

每次调用SetWindowsHookEx后,立即用Marshal.GetLastWin32Error()获取错误码,能快速定位问题:

  • 错误码126:找不到指定模块,大概率是hMod参数传错了
  • 错误码87:参数无效,可能是委托签名不对、线程ID无效或者钩子ID错误
  • 错误码0:调用成功,但后续钩子不触发,大概率是委托被GC回收了

推荐的替代方案:用WH_KEYBOARD_LL钩子

如果你的需求是监听全局键盘事件,或者针对第三方应用做键盘拦截,WH_KEYBOARD_LL是更适合C#的方案,不需要写非托管DLL。这里给你一个完整的示例:

public partial class Form1 : Form
{
    // 低级别键盘钩子的委托
    private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

    // 保持委托引用,防止GC回收
    private static LowLevelKeyboardProc _hookCallback = HookHandler;
    private static IntPtr _hookHandle = IntPtr.Zero;

    // 导入必要的Win32 API
    [DllImport("user32.dll", SetLastError = true)]
    private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll")]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll")]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetModuleHandle(string lpModuleName);

    [DllImport("user32.dll", SetLastError = true)]
    static extern IntPtr FindWindowByCaption(int ZeroOnly, string lpWindowName);

    IntPtr windowHandle;

    public Form1()
    {
        InitializeComponent();
        // 初始化钩子
        _hookHandle = SetUpKeyboardHook();
        // 获取目标窗口句柄
        windowHandle = FindWindowByCaption(0, "目标应用窗口标题");
    }

    private IntPtr SetUpKeyboardHook()
    {
        using (var currentProcess = Process.GetCurrentProcess())
        using (var currentModule = currentProcess.MainModule)
        {
            // WH_KEYBOARD_LL的钩子ID是13,最后一个参数0表示全局钩子
            return SetWindowsHookEx(13, _hookCallback, GetModuleHandle(currentModule.ModuleName), 0);
        }
    }

    private static IntPtr HookHandler(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0)
        {
            // 解析按键信息
            Keys pressedKey = (Keys)Marshal.ReadInt32(lParam);
            // 这里可以根据需要过滤目标窗口的按键事件
            // 比如判断当前激活窗口是否是你的目标窗口
            Console.WriteLine($"按键被按下:{pressedKey}");
        }
        // 一定要调用CallNextHookEx,否则其他钩子会失效
        return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        // 关闭钩子
        UnhookWindowsHookEx(_hookHandle);
        base.OnFormClosing(e);
    }
}

内容的提问来源于stack exchange,提问作者MK446

火山引擎 最新活动