0

I try to do a VSTO key hook as discribed here: Excel VSTO Key hook

I used the answer of Alex Butenko to create this class which (should) call OnKeyPress every time a key is pressed. The problem is, when I press one key, OnKeyPress is called twice:

static class ShortcutManager
{
    delegate int LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
    static readonly LowLevelKeyboardProc _proc = HookCallback;
    static IntPtr _hookID = IntPtr.Zero;
    const int WH_KEYBOARD = 2;
    const int HC_ACTION = 0;

    const int WM_KEYDOWN = 0x0100;


    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    static extern bool UnhookWindowsHookEx(IntPtr idHook);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll")]
    static extern short GetKeyState(int nVirtKey);

    static bool _keyHookingStarted;
    public static void Start(IShortcutDistributor dist)
    {
        m_dist = dist;
        if (!_keyHookingStarted)
        {
#pragma warning disable 0618
            _hookID = SetWindowsHookEx(WH_KEYBOARD, _proc, IntPtr.Zero, (uint)AppDomain.GetCurrentThreadId());
#pragma warning restore 0618
            _keyHookingStarted = true;
        }
    }
    public static void Stop()
    {
        m_dist = null;
        if (_keyHookingStarted)
        {
            UnhookWindowsHookEx(_hookID);
            _hookID = IntPtr.Zero;
            _keyHookingStarted = false;
        }
    }

    static IShortcutDistributor m_dist = null;

    static void OnKeyPress(uint keys)
    {
        var crtl = IsKeyDown(Keys.LControlKey) || IsKeyDown(Keys.RControlKey);
        Debug.WriteLine("Keys: "+ keys.ToString()+ " Crtl: "+ crtl.ToString());
        m_dist?.RaiseKeyPressedEvent(new KeyPressedEventArgs((Keys)keys, crtl));
    }
    static bool IsKeyDown(Keys keys)
    {
        return (GetKeyState((int)keys) & 0x8000) == 0x8000;
    }

    static int HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {

        if (nCode < 0)
        {
            return (int)CallNextHookEx(_hookID, nCode, wParam, lParam);
        }
        if (nCode == HC_ACTION)
        {
            Debug.WriteLine("nCode: " + nCode.ToString() + " wParam:" + wParam.ToString());
            OnKeyPress((uint)wParam);
        }
        return (int)CallNextHookEx(_hookID, nCode, wParam, lParam);
    }
}

So when I press crtl+q the debug output is:

nCode: 0 wParam:81
Keys: 81 Crtl: True
nCode: 0 wParam:81
Keys: 81 Crtl: True

One press of space results in this debug output:

nCode: 0 wParam:32
Keys: 32 Crtl: False
nCode: 0 wParam:32
Keys: 32 Crtl: False

The microsoft documentation https://msdn.microsoft.com/en-us/library/windows/desktop/ms644985(v=vs.85).aspx says that: "wParam is either WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN or WM_SYSKEYUP" but in my case its the same in both calls.

So, what I'm doing wrong? Am I missing something?

Gener4tor
  • 414
  • 3
  • 12
  • 40
  • This is the Function of IShortcutDistributor which distributes OnKeyPressed to the other parts of my programm. Removing it does not change my problem. – Gener4tor Aug 23 '18 at 13:19
  • I found something out: Like Alex Butenko in https://stackoverflow.com/questions/34874000/excel-vsto-key-hook I mixed up LowLevelKeyboardProc and KeyboardProc. When I use WH_KEYBOARD = 2 KeyboardProc is called. LowLevelKeyboardProc would be called when I use WH_KEYBOARD_LL = 13 is used. So this is the correct MS documentation: https://msdn.microsoft.com/en-us/library/ms644984(v=vs.85).aspx – Gener4tor Aug 23 '18 at 13:29

1 Answers1

3

Ok, it seems like Ive found what my problem was:

I mixed up LowLevelKeyboardProc and KeyboardProc.

When I use "SetWindowsHookEx(WH_KEYBOARD,..." the function is KeyboardProc and not LowLevelKeyboardProc. So this is the correct microsoft documentation:

https://msdn.microsoft.com/en-us/library/ms644984(v=vs.85).aspx

https://learn.microsoft.com/de-de/windows/desktop/inputdev/about-keyboard-input#_win32_Keystroke_Message_Flags

So lParam are the "Keystroke Message Flags". This means that the flag KF_REPEAT = 30 tells me how often the keypress is repeated. When I check if repeat = 0 I just get the first call. So this is my (hopefully correct) code:

static class ShortcutManager
{
    delegate int KeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
    static readonly KeyboardProc _proc = HookCallback;
    static IntPtr _hookID = IntPtr.Zero;
    const int WH_KEYBOARD = 2;
    const int WH_KEYBOARD_LL = 13;
    const int HC_ACTION = 0;

    const int WM_KEYDOWN = 0x0100;


    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    static extern IntPtr SetWindowsHookEx(int idHook, KeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    static extern bool UnhookWindowsHookEx(IntPtr idHook);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll")]
    static extern short GetKeyState(int nVirtKey);

    static bool _keyHookingStarted;
    public static void Start(IShortcutDistributor dist)
    {
        m_dist = dist;
        if (!_keyHookingStarted)
        {
#pragma warning disable 0618
            _hookID = SetWindowsHookEx(WH_KEYBOARD, _proc, IntPtr.Zero, (uint)AppDomain.GetCurrentThreadId());
#pragma warning restore 0618
            _keyHookingStarted = true;
        }
    }
    public static void Stop()
    {
        m_dist = null;
        if (_keyHookingStarted)
        {
            UnhookWindowsHookEx(_hookID);
            _hookID = IntPtr.Zero;
            _keyHookingStarted = false;
        }
    }

    

    static IShortcutDistributor m_dist = null;

    const int KF_REPEAT = 0x4000;

    static void OnKeyPress(uint keys)
    {
        var crtl = IsKeyDown(Keys.LControlKey) || IsKeyDown(Keys.RControlKey);
        Debug.WriteLine("Keys: "+ keys.ToString()+ " Crtl: "+ crtl.ToString());
        m_dist?.RaiseKeyPressedEvent(new KeyPressedEventArgs((Keys)keys, crtl));
    }
    static bool IsKeyDown(Keys keys)
    {
        return (GetKeyState((int)keys) & 0x8000) == 0x8000;
    }

    static int HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {

        if (nCode < 0)
        {
            return (int)CallNextHookEx(_hookID, nCode, wParam, lParam);
        }
        if (nCode == HC_ACTION)
        {
            var repeat = (HiWord(lParam) & KF_REPEAT);
            Debug.WriteLine("nCode: " + nCode.ToString() + " wParam:" + wParam.ToString() + " repeat: "+ repeat.ToString());
            if (repeat == 0)
            {
                OnKeyPress((uint)wParam);
            }
        }
        return (int)CallNextHookEx(_hookID, nCode, wParam, lParam);
    }

    private static ulong HiWord(IntPtr ptr)
    {
        if (((ulong)ptr & 0x80000000) == 0x80000000)
            return ((ulong)ptr >> 16);
        else
            return ((ulong)ptr >> 16) & 0xffff;
    }
}
Gener4tor
  • 414
  • 3
  • 12
  • 40
  • `HiWord` throws an `OverflowException` because of the `uint` cast. Changing it to `ulong` solves the problem. Good karma to you for sharing the answer, btw. –  Jul 02 '20 at 06:35
  • 1
    ok...i changed the value to ulong? But without testing...I hope this is correct. – Gener4tor Jul 02 '20 at 11:05