【问题标题】:VSTO Windows Hook keydown event called 10 timesVSTO Windows Hook keydown 事件调用了 10 次
【发布时间】:2016-11-14 22:49:36
【问题描述】:

所以,我一直在开发一个类来处理 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);
    }
    //....

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

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

到目前为止我已经尝试过

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

我期待什么

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

如何绕过这个

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

非常感谢!要快乐,要善良! :D

【问题讨论】:

    标签: c# windows ms-word vsto hook


    【解决方案1】:

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

    如果按键被长按,多个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);
    }
    

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

    可以在here 找到一个“提示”不要处理除HC_ACTION 以外的任何内容,说明:

    if (nCode < 0)  // do not process message
        return ...;
    
    // ...
    switch (nCode) 
    {
        case HC_ACTION:
            // ... do something ...
            break;
    
        default:
            break;
    }
    // ...
    return CallNextHookEx(...);
    

    这里有另一个支持声明:
    Why does my keyboard hook receive the same key-up and key-down events multiple times?

    lParam的内容定义为as follows

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

    (提醒一下:DWORD 在 x86 和 x64 平台上的大小是 4 bytes。)

    lParamflags的文档可以在herehere找到。
    在这个链接中,它被描述为

    • bit 30 (=0x40000000) 是上一个键状态
      1 如果密钥 关闭,0 如果密钥 向上导致此调用的新密钥状态之前)
    • 位 31 (=0x80000000) 是过渡状态
      0 按下按键,1 按键释放现在

    前一个键状态”这个术语相当混乱,但实际上它与当前状态正好相反(因为只有向上或向下,没有第三个状态)。

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

    可以找到另一个示例(使用 VC7)here

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

    其中0xC000 就是0x4000 || 0x8000 并定义该键已释放并创建了一个键向上事件。

    总而言之,令人困惑,但确实如此。
    也许还有其他链接可以更好地描述这种情况,但我想在这样的时代,新的应用程序开发“应该”在微型沙箱(如 UWP)中“完成”,而 VSTO 肯定会死去为它让路newer Office add-insHTML and JavaScript 写的,再也没有人关心低级钩子了。

    【讨论】:

    • 确实!它确实有效!非常感谢你的帮助!现在,为了学习,您介意向我解释(或解决任何文档)您是如何找出这些标志的吗?再次感谢!
    • @LuisMoyano 很高兴我能帮助你!为了学习起见,我把所有相关的文档和遗漏的解释都放在了一起。我希望这有帮助。现在,只需享受血淋淋的细节。 ;-)
    猜你喜欢
    • 1970-01-01
    • 2020-08-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-29
    • 2017-03-20
    • 1970-01-01
    相关资源
    最近更新 更多