Low Level Keyboard (& Mouse) Hook

Occasionally, there is a need to catch application wide keypress events and the windows forms build in keypress event handling is a little hit and miss. WndProc(ref Message m) is a virtual method that can be overridden to catch single WM_KEYPRESS events and all sorts of other low level messages, but it gets a little messy at times and generally should only be used as a last resort, when all other workarounds fail.

The simple answer is to use a low level keyboard hook, which can catch all keypresses(even when the app isn’t in focus). This is particularly useful for applications which require hotkeys to launch (toolbar apps with a special key combination to load a form), key loggers (might be a legitimate use for these?), and various other key aware applications.

The idea is simple, and it uses some Win32 Dll calls to perform the task :-

 

#region DLL Imports

[DllImport(“user32.dll”, CharSet = CharSet.Auto, SetLastError = true)]

private static extern IntPtr SetWindowsHookEx(int idHook,

    KeyboardProcedure lpfn, IntPtr hMod, uint dwThreadId);

 

[DllImport(“user32.dll”, CharSet = CharSet.Auto, SetLastError = true)]

[return: MarshalAs(UnmanagedType.Bool)]

private static extern bool UnhookWindowsHookEx(IntPtr hhk);

 

[DllImport(“user32.dll”, CharSet = CharSet.Auto, SetLastError = true)]

private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,

    IntPtr wParam, IntPtr lParam);

 

[DllImport(“kernel32.dll”, CharSet = CharSet.Auto, SetLastError = true)]

private static extern IntPtr GetModuleHandle(string lpModuleName);

#endregion

 

So, those are the calls we need at the top our our class. They are going to be used extensively in the rest of the code, so we define them immediately after the class definition.

I also declare a couple of WM message constants, relating to the windows messages for system & normal key presses.

 

private const int WH_KEYBOARD_LL = 13;

private const int WM_KEYDOWN = 0x0100;

private const int WM_SYSKEYDOWN = 0x0104;

 

And finally, the main bulk of the code :-

 

#region Member Variables

private KeyboardProcedure keyboardProcedure;

 

private KeyboardProcedure CurrentKeyboardProcedure

{

    get

    {

        if (keyboardProcedure == null)

        {

            keyboardProcedure = HookCallback;

        }

        return keyboardProcedure;

    }

    set { keyboardProcedure = value; }

}

private IntPtr keyboardHookId = IntPtr.Zero;

#endregion

 

public KeyboardHook()

{

    keyboardHookId = SetHook(CurrentKeyboardProcedure);

}

 

private IntPtr SetHook(KeyboardProcedure proc)

{

    using (Process currentProcess = Process.GetCurrentProcess())

    using (ProcessModule currentModule = currentProcess.MainModule)

    {

        return SetWindowsHookEx(WH_KEYBOARD_LL, proc,

            GetModuleHandle(currentModule.ModuleName), 0);

    }

}

 

private delegate IntPtr KeyboardProcedure(

    int nCode, IntPtr wParam, IntPtr lParam);

 

public event KeyEventHandler KeyPressDetected;

 

private void OnKeyPressDetected(object sender, KeyEventArgs args)

{

    if (KeyPressDetected != null)

    {

        KeyPressDetected(sender, args);

    }

}

 

private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)

{

    if (nCode >= 0 && (wParam ==

        (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN))

    {

        int vkCode = Marshal.ReadInt32(lParam);

 

        Keys key = ((Keys)vkCode);

 

        if (key == Keys.LControlKey ||

            key == Keys.RControlKey)

        {

            key = key | Keys.Control;

        }

 

        if (key == Keys.LShiftKey ||

            key == Keys.RShiftKey)

        {

            key = key | Keys.Shift;

        }

 

        if (key == Keys.LMenu ||

            key == Keys.RMenu)

        {

            key = key | Keys.Alt;

        }

 

       OnKeyPressDetected(null, new KeyEventArgs(key));

    }

    return CallNextHookEx(keyboardHookId, nCode, wParam, lParam);

}

 

#region IDisposable Members

 

public void Dispose()

{

    UnhookWindowsHookEx(keyboardHookId);

}

 

#endregion

 

And that seems to do the trick quite nicely for me. I have included a nice little bit of code in the HookCallBack which deals with some system key presses and correctly forwards them via a KeyEventArgs object to the OnKeyPressDetected method which then checks to see if we have listeners on our event by checking whether it is null or not, and then firing the events if listeners are active.

Incidentally, the  WH_KEYBOARD_LL that sets up the hook can be replaced with another handler to get it to catch mouse activity  :-

private const int WH_MOUSE_LL = 14;

Just change that and alter the SetWindowsHookEx function to pass it as a parameter :-

return SetWindowsHookEx(WH_MOUSE_LL, proc,

    GetModuleHandle(curModule.ModuleName), 0);

Then, change the callback function to do whatever you want.