VSTO Windows钩子keydown事件被调用10次

4

我正在开发一个用于处理VSTO插件中键盘输入的类,目前我已经使用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);
    }
    //....

我成功地接收到键盘输入,但是这里有个大谜团:每次按下一个键,无论速度多快,事件(onKeyDown)都会被调用10次,恰好不多也不少。
如果长按键,则事件将继续被调用,但是每次调用10次,而不是只调用一次。
到目前为止,我已经尝试过:
1. 使用wParam在KeyUp时调用所需的事件:似乎不起作用,在所有处理KeyDown和KeyUp事件的代码中,都使用IntPtr wParam,但是我只能从该变量中检索到按键代码,这并没有帮助。
2. 使用lParam或nCode:这些变量在这10个调用之间给出不一致的值,ncode倾向于检索0和3,而lParam则是一些看起来像未管理的内存地址的值...
我期望的是:
当按下键时,onKeyDown只被调用一次,或者在另一方面,能够调用onKeyUp方法,我期望它每次释放一个键时只被调用一次。
如何绕过这个问题:
如果找不到合理的答案,我想使用自定义定时器来丢弃所有那些调用,并仅使用最后一个调用,如果其他所有方法都失败了,您会推荐这样做吗?
非常感谢!保持快乐和善良!:D
1个回答

7
首先,您需要过滤正确的ncode,以获取您应该处理的按键。 (例如,您不应处理HC_NOREMOVE。)
然后,您需要使用lParam中的标志来检查它是KeyDown还是KeyUp事件。

如果按键被长按,则Win32已经将多个KeyDown事件合并为一个调用,因此您在此处无需执行任何特殊操作。 但是,如果您想仅获取最后一个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);
}

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

可以在此处找到一个“提示”,不要处理除HC_ACTION之外的任何内容。

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

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

    default:
        break;
}
// ...
return CallNextHookEx(...);

这里提出了另一个支持性的陈述:
为什么我的键盘挂钩会多次接收相同的按键和释放按键事件?

lParam的内容定义如下

typedef struct tagKBDLLHOOKSTRUCT {
    DWORD     vkCode;
    DWORD     scanCode;
    DWORD     flags;
    DWORD     time;
    ULONG_PTR dwExtraInfo;
}

(提醒一下:在x86和x64平台上,DWORD的大小都是4字节。) lParam flags的文档可以在这里这里找到。
在这些链接中描述了以下内容
  • 第30位(=0x40000000)是之前的按键状态
    (如果在调用此函数之前按键已经按下,则为1,否则为0
  • 第31位(=0x80000000)是转换状态
    (按下键时为0,松开键时为1

术语“previous key state”相当令人困惑,但实际上它只是当前状态的相反(因为只有按下或弹起两种状态,没有第三种状态)。

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

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

if (HIWORD (lParam) & 0xC000)
    // Key up without autorepeat
else
    // Key down

在编程中,0xC000代表的是0x4000 || 0x8000,表示键已释放并创建了一个键弹起事件。

总体来说相当令人困惑,但这是真实的。
也许有其他链接可以更好地描述这种情况,但我猜在像UWP这样的小沙盒中进行新应用程序开发,并且VSTO正在走向灭亡,为newer Office add-ins腾出位置,这些插件是用HTML和JavaScript编写的,在这些时代里,没有人再关心低级钩子了。


确实!它确实起作用了!非常感谢你的帮助! 现在,出于学习的目的,你介意向我解释一下(或提供任何文档),关于你是如何找出那些标志的吗? 再次非常感谢! - user7159179
@LuisMoyano 很高兴我能帮到你!为了学习的目的,我整理了所有相关文档和缺失的解释。希望这有所帮助。现在,尽情享受这些细节吧。;-) - haindl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接