VSTO Windows Hook keydown 事件被调用 10 次

3 c# windows hook vsto ms-word

所以,我一直在开发一个类来处理 VSTO 加载项中的 Kwyboard 输入,到目前为止,我一直在使用 Windows 钩子来做到这一点,并取得了相对的成功。

有这个代码:

    //.....
    private const int WH_KEYBOARD = 2;
    private const int WH_MOUSE = 7;

    private enum WM : uint {
        KEYDOWN = 0x0100,
        KEYFIRST = 0x0100,
        KEYLAST = 0x0108,
        KEYUP = 0x0101,
        MOUSELEFTDBLCLICK = 0x0203,
        MOUSELEFTBTNDOWN = 0x0201,
        MOUSELEFTBTNUP = 0x0202,
        MOUSEMIDDBLCLICK = 0x0209,
        MOUSEMIDBTNDOWN = 0x0207,
        MOUSEMIDBTNUP = 0x0208,
        MOUSERIGHTDBLCLK = 0x0206,
        MOUSERIGHTBTNDOWN = 0x0204,
        MOUSERIGHTBTNUP = 0x0205
    }

    private hookProcedure proc;

    private static IntPtr hookID = IntPtr.Zero;

    //Enganches

    [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    private static extern IntPtr SetWindowsHookEx(int hookId, hookProcedure proc, IntPtr hInstance, uint thread);

    [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    private static extern bool unHookWindowsHookEx(int hookId);

    [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    private static extern IntPtr CallNextHookEx(IntPtr hookId, int ncode, IntPtr wparam, IntPtr lparam);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetModuleHandle(string name);

    [DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern int GetCurrentThreadId();

    public CPInputListener() {

        proc = keyBoardCallback;

        hookID = setHook(proc);
    }


    private IntPtr setHook(hookProcedure procedure){

        ProcessModule module = Process.GetCurrentProcess().MainModule;
        uint threadId = (uint)GetCurrentThreadId();

        return SetWindowsHookEx(WH_KEYBOARD, procedure, IntPtr.Zero, threadId);
    }

    public void stopListeningAll() {
        unHookWindowsHookEx(WH_KEYBOARD);//For now
    }


    private IntPtr keyBoardCallback(int ncode, IntPtr wParam, IntPtr lParam) {

        if (ncode >= 0) {
            //LPARAM pretty useless

            Keys key = (Keys)wParam;

            KeyEventArgs args = new KeyEventArgs(key);

            onKeyDown(args);//for now

        }
        return CallNextHookEx(hookID, ncode, wParam, lParam);
    }
    //....
Run Code Online (Sandbox Code Playgroud)

我确实成功地接收了键盘输入,但这里有一个大谜团;每次按下一个键时,无论它有多快,事件 (onKeyDown) 都会被准确调用 10 次,不多不少。

如果长按该键,事件将继续被调用,但会被调用 10 次,而不是只调用一次。

到目前为止我已经尝试过

  1. 使用 wParam 在 Key Up 上调用所需的事件:似乎不起作用,在我见过的处理 Key down 和 up 事件的所有代码中,IntPtr wParam都使用了,但从该变量中我只能检索没有的键码帮助。
  2. 使用lParamor nCode:这些变量在这 10 个调用之间给出不一致的值,ncode倾向于检索 0 和 3 以及lParam一些似乎是非托管内存地址的值......

我期待什么

我确实希望 onKeyDown 在按下键时只调用一次,或者另一方面能够通过 on key up 调用该方法,我希望每次释放键只调用一次。

如何绕过这个

如果我找不到合理的答案,我正在考虑使用定制的计时器来丢弃所有这些调用并仅使用最后一个,如果其他一切都失败了,您会推荐这个吗?

非常感谢!快乐和善良!:D

hai*_*ndl 5

首先,您必须筛选正确的ncode以仅获取您应该处理的击键。(例如,您不应该处理HC_NOREMOVE。)
然后您必须使用 中的标志检查它是否是 aKeyDownKeyUp事件lParam

如果长按该键KeyDown,Win32 已经将多个事件合并为一个调用,因此您不必在这里做任何特别的事情。但是如果您只想获得最后一个KeyUp事件,那么您还必须检查另一个标志lParam

所以,这是您需要更改的代码:

private IntPtr keyBoardCallback(int ncode, IntPtr wParam, IntPtr lParam)
{
    // Feel free to move the const to a private field.
    const int HC_ACTION = 0;
    if (ncode == HC_ACTION)
    {
        Keys key = (Keys)wParam;
        KeyEventArgs args = new KeyEventArgs(key);

        bool isKeyDown = ((ulong)lParam & 0x40000000) == 0;
        if (isKeyDown)
            onKeyDown(args);
        else
        {
            bool isLastKeyUp = ((ulong)lParam & 0x80000000) == 0x80000000;
            if (isLastKeyUp)
                onKeyUp(args);
        }
    }
    return CallNextHookEx(hookID, ncode, wParam, lParam);
}
Run Code Online (Sandbox Code Playgroud)

根据评论中的要求进行编辑:
不幸的是,这些参数的文档非常稀少。

HC_ACTION可以在此处找到不处理任何其他内容的“提示” ,说明:

if (nCode < 0)  // do not process message
    return ...;

// ...
switch (nCode) 
{
    case HC_ACTION:
        // ... do something ...
        break;

    default:
        break;
}
// ...
return CallNextHookEx(...);
Run Code Online (Sandbox Code Playgroud)

另一个支持声明是:
为什么我的键盘钩子多次收到相同的按键和按键事件?

的内容lParam定义如下

typedef struct tagKBDLLHOOKSTRUCT {
    DWORD     vkCode;
    DWORD     scanCode;
    DWORD     flags;
    DWORD     time;
    ULONG_PTR dwExtraInfo;
}
Run Code Online (Sandbox Code Playgroud)

(提醒DWORD一下:x86 和 x64 平台上的大小为4 个字节。)

lParam flags可以在此处此处找到的文档。
在这个链接中,它描述了

  • 位 30 (= 0x40000000) 是先前的键状态
    1如果键导致此调用的新键状态之前按下且0向上)
  • 位 31 (= 0x80000000) 是转换状态
    现在0按下按键和1松开按键)

术语“前一个关键状态”相当令人困惑,但实际上它与当前状态正好相反(因为只有上升或下降,没有第三个状态)。

当“键盘的自动重复功能”被激活时,即当按键按下的时间足够长时,过渡状态尤其重要。

可以在此处找到另一个示例(使用 VC7):

if (HIWORD (lParam) & 0xC000)
    // Key up without autorepeat
else
    // Key down
Run Code Online (Sandbox Code Playgroud)

其中0xC000just is0x4000 || 0x8000和定义键被释放并创建了一个键启动事件。

总而言之,相当令人困惑,但仍然如此。
也许还有其他链接可以更好地描述这种情况,但我想在这样的时候,新应用程序开发“应该在小沙箱(如 UWP)中完成”,而 VSTO 肯定会死掉,让路用HTML 和 JavaScript编写的较新的 Office 加载项,没有人再关心低级挂钩了。