VSTO Windows Hook keydown事件被调用10次

时间:2016-11-14 22:49:36

标签: c# windows ms-word vsto hook

所以,我一直在开发一个类来处理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次,不多也不少。

如果长按键,则会持续调用该事件,但不会只调用一次。

到目前为止,我已尝试

  1. 使用wParam在Key Up上调用所需的事件:似乎不起作用,在我已经看到处理Key down和up事件的所有代码中,都使用了IntPtr wParam,但是该变量我只能检索不起作用的密钥代码。
  2. 使用lParamnCode:这些变量在这10个调用之间给出了不一致的值,ncode往往会检索0和3&{39}和{{1一些似乎是非托管内存的值......
  3. 我期待什么

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

    如何绕过此

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

    非常感谢!快乐,善良! :d

1 个答案:

答案 0 :(得分:3)

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

如果长按键,则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之外的任何其他内容here,说明:

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。)

可以找到lParam flags的文档herehere
在这个链接中描述了

  • 第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-ins编写的HTML and JavaScript,不再关心低级别的钩子了。