Global Hotkey Recognition in WinForms

by Samuel Attard

So as part of my Google Play Music Desktop Player project I needed to listen for certain keypresses even when the application wasn't in focus. In practice I assumed this would be straight forward but I ended up scratching my head in puzzlement for hours before stumbling accross a semi-working solution and then making it work effectively.

Making it easy for you

There is a handy class which I found on a Gist and modified to work effectively

using System;  
using System.Collections.Generic;  
using System.Text;  
using System.Runtime.InteropServices;  
using System.Windows.Forms;  
using System.ComponentModel;

namespace Utilities  
{
    /// <summary>
    /// A class that manages a global low level keyboard hook
    /// </summary>
    class globalKeyboardHook
    {
        #region Constant, Structure and Delegate Definitions
        /// <summary>
        /// defines the callback type for the hook
        /// </summary>
        public delegate int keyboardHookProc(int code, int wParam, ref keyboardHookStruct lParam);

        public struct keyboardHookStruct
        {
            public int vkCode;
            public int scanCode;
            public int flags;
            public int time;
            public int dwExtraInfo;
        }

        const int WH_KEYBOARD_LL = 13;
        const int WM_KEYDOWN = 0x100;
        const int WM_KEYUP = 0x101;
        const int WM_SYSKEYDOWN = 0x104;
        const int WM_SYSKEYUP = 0x105;
        #endregion

        #region Instance Variables
        /// <summary>
        /// The collections of keys to watch for
        /// </summary>
        public List<Keys> HookedKeys = new List<Keys>();
        /// <summary>
        /// Handle to the hook, need this to unhook and call the next hook
        /// </summary>
        IntPtr hhook = IntPtr.Zero;
        #endregion

        #region Events
        /// <summary>
        /// Occurs when one of the hooked keys is pressed
        /// </summary>
        public event KeyEventHandler KeyDown;
        /// <summary>
        /// Occurs when one of the hooked keys is released
        /// </summary>
        public event KeyEventHandler KeyUp;
        #endregion

        #region Constructors and Destructors
        /// <summary>
        /// Initializes a new instance of the <see cref="globalKeyboardHook"/> class and installs the keyboard hook.
        /// </summary>
        public globalKeyboardHook()
        {
            hook();
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="globalKeyboardHook"/> is reclaimed by garbage collection and uninstalls the keyboard hook.
        /// </summary>
        ~globalKeyboardHook()
        {
            unhook();
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Installs the global hook
        /// </summary>
        private static keyboardHookProc callbackDelegate;

        public void hook()
        {
            if (callbackDelegate != null) throw new InvalidOperationException("Can't hook more than once");
            IntPtr hInstance = LoadLibrary("User32");
            callbackDelegate = new keyboardHookProc(hookProc);
            hhook = SetWindowsHookEx(WH_KEYBOARD_LL, callbackDelegate, hInstance, 0);
            if (hhook == IntPtr.Zero) throw new Win32Exception();
        }

        public void unhook()
        {
            if (callbackDelegate == null) return;
            bool ok = UnhookWindowsHookEx(hhook);
            callbackDelegate = null;
        }

        /// <summary>
        /// The callback for the keyboard hook
        /// </summary>
        /// <param name="code">The hook code, if it isn't >= 0, the function shouldn't do anyting</param>
        /// <param name="wParam">The event type</param>
        /// <param name="lParam">The keyhook event information</param>
        /// <returns></returns>
        public int hookProc(int code, int wParam, ref keyboardHookStruct lParam)
        {
            if (code >= 0)
            {
                Keys key = (Keys)lParam.vkCode;
                if (HookedKeys.Contains(key))
                {
                    KeyEventArgs kea = new KeyEventArgs(key);
                    if ((wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) && (KeyDown != null))
                    {
                        KeyDown(this, kea);
                    }
                    else if ((wParam == WM_KEYUP || wParam == WM_SYSKEYUP) && (KeyUp != null))
                    {
                        KeyUp(this, kea);
                    }
                    if (kea.Handled)
                        return 1;
                }
            }
            return CallNextHookEx(hhook, code, wParam, ref lParam);
        }
        #endregion

        #region DLL imports
        /// <summary>
        /// Sets the windows hook, do the desired event, one of hInstance or threadId must be non-null
        /// </summary>
        /// <param name="idHook">The id of the event you want to hook</param>
        /// <param name="callback">The callback.</param>
        /// <param name="hInstance">The handle you want to attach the event to, can be null</param>
        /// <param name="threadId">The thread you want to attach the event to, can be null</param>
        /// <returns>a handle to the desired hook</returns>
        [DllImport("user32.dll")]
        static extern IntPtr SetWindowsHookEx(int idHook, keyboardHookProc callback, IntPtr hInstance, uint threadId);

        /// <summary>
        /// Unhooks the windows hook.
        /// </summary>
        /// <param name="hInstance">The hook handle that was returned from SetWindowsHookEx</param>
        /// <returns>True if successful, false otherwise</returns>
        [DllImport("user32.dll")]
        static extern bool UnhookWindowsHookEx(IntPtr hInstance);

        /// <summary>
        /// Calls the next hook.
        /// </summary>
        /// <param name="idHook">The hook id</param>
        /// <param name="nCode">The hook code</param>
        /// <param name="wParam">The wparam.</param>
        /// <param name="lParam">The lparam.</param>
        /// <returns></returns>
        [DllImport("user32.dll")]
        static extern int CallNextHookEx(IntPtr idHook, int nCode, int wParam, ref keyboardHookStruct lParam);

        /// <summary>
        /// Loads the library.
        /// </summary>
        /// <param name="lpFileName">Name of the library</param>
        /// <returns>A handle to the library</returns>
        [DllImport("kernel32.dll")]
        static extern IntPtr LoadLibrary(string lpFileName);
        #endregion
    }
}

How do we use this magic

Make a file in your project and put this class in it, then in the file you want to listen for keypresses add Using Utilities to the top of your file.

Now you need to simple instantiate a globalKeyboardHook object and call HookedKeys.Add() to listen to keys. A usage example is included below

private static globalKeyboardHook gkh;  
private void InitializeForm()  
{
    gkh = new globalKeyboardHook();
    // Don't let the Garbage Man interfere
    GC.KeepAlive(gkh);

    // These are the keys you want to listen to
    gkh.HookedKeys.Add(Keys.MediaPlayPause);
    gkh.HookedKeys.Add(Keys.MediaNextTrack);
    gkh.HookedKeys.Add(Keys.A);
    gkh.HookedKeys.Add(Keys.B);

    // These are the handlers that will be called when the HookedKeys are pushed
    gkh.KeyDown += new KeyEventHandler(gkh_KeyDown);
    gkh.KeyUp += new KeyEventHandler(gkh_KeyUp);
}

void gkh_KeyUp(object sender, KeyEventArgs e)  
{
    switch (e.KeyCode.ToString())
    {
        case "MediaPlayPause":
            // Do something with Play / Pause
            break;
        case "A":
            // Do something with A
            break;
        case "MediaNextTrack":
            // Do something with next track
            break;
        case "B":
            // Do something with B
            break;
    }
    e.Handled = false;
}

void gkh_KeyDown(object sender, KeyEventArgs e)  
{
    // Use e.Handled = true to prevent other application recieving the keyPress
    // Use e.handled = false to allow other application to recieve
    e.Handled = false;
}

You have to be sure to add gkh.unhook() to your form unload method otherwise you get all sorts of bad GarbageCollector errors.

private void Form1_Closing(object sender, System.ComponentModel.CancelEventArgs e)  
{
    // Make sure we unhook once the form closes
    gkh.unhook();
}

Fair warning

You have to be careful when listening for key events, an over use of key logging might cause security warnings in Windows 8+ (apparently). I haven't personally experienced it but I have been warned.

Conclusion

This is by far the easiest way I have found to listen globally for key presses in C#, hopefully someone found this helpful and was saved the hours of struggling I went through