Low-Level Global Keyboard Hook in C#

I had to implement a mechanism for a global low-level keyboard hook.

After a brief search on the internet, I understood the principle and I went out with a solution in hand.

First I show you how to use it, it’s very simple.

To register a new hook, just write the following:

hookId = GlobalKeyboardHook.Instance.Hook(new List { Key.LeftCtrl, Key.D }, SomeMethod, out message);

And to remove hook registration, write the following:

GlobalKeyboardHook.Instance.UnHook(hookId);

Here is the full code. You can use it as is if you want.

I tried to make the code formatted in the right way but it seems impossible so you can grab it from gist

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;

namespace KeyboardUtils
{
    ///
<summary>
    /// Provide a way to handle a global keybourd hooks
    /// This hook is called in the context of the thread that installed it.
    /// The call is made by sending a message to the thread that installed the hook.
    /// Therefore, the thread that installed the hook must have a message loop.
    /// </summary>

    public sealed class GlobalKeyboardHook : IDisposable
    {
        private const int WH_KEYBOARD_LL = 13;
        private const int WM_KEYDOWN = 0x0100;
        private const int WM_KEYUP = 0x0101;
        private LowLevelKeyboardProc _proc;
        private readonly IntPtr _hookId = IntPtr.Zero;
        private static GlobalKeyboardHook _instance;
        private Dictionary&lt;int, KeyValuePair&gt; _hookEvents;
        private bool _disposed;
        private KeyCombination _pressedKeys;

        ///
<summary>
        /// Return a singleton instance of 
        /// </summary>

        public static GlobalKeyboardHook Instance
        {
            get
            {
                Interlocked.CompareExchange(ref _instance, new GlobalKeyboardHook(), null);
                return _instance;
            }
        }

        private GlobalKeyboardHook()
        {
            _proc = HookCallback;
            _hookEvents = new Dictionary&lt;int, KeyValuePair&gt;();
            _hookId = SetHook(_proc);
            _pressedKeys = new KeyCombination();
        }

        ///
<summary>
        /// Register a keyboard hook event
        /// </summary>

        /// The short keys. minimum is two keys
        /// The action to run when the key ocmbination has pressed
        /// Empty if no error occurred otherwise error message
        /// True if the action should execute in the background. -Be careful from thread affinity- Default is false
        /// An action to run when unsubscribing from keyboard hook. can be null
        /// Event id to use when unregister
        public int Hook(List keys, Action execute, out string message, bool runAsync = false, Action dispose = null)
        {
            if (_hookEvents == null)
            {
                message = "Can't register";
                return -1;
            }

            if (keys == null || execute == null)
            {
                message = "'keys' and 'execute' can't be null";
                return -1;
            }

            if (keys.Count  Task.Run(() =&gt; execute);

            _hookEvents[id] = new KeyValuePair(kc, new HookActions(asyncAction ?? execute, dispose));
            message = string.Empty;
            return id;
        }

        private bool ValidateKeys(IEnumerable keys)
        {
            return keys.All(t =&gt; IsKeyValid((int)t));
        }

        private bool IsKeyValid(int key)
        {
            // 'alt' is sys key and hence is disallowed.
            // a - z and shift, ctrl.
            return key &gt;= 44 &amp;&amp; key = 116 &amp;&amp; key &lt;= 119;
        }

        ///
<summary>
        /// Un register a keyboard hook event
        /// </summary>

        /// event id to remove
        /// parameter to pass to dispose method
        public void UnHook(int id, object obj = null)
        {
            if (_hookEvents == null || id &lt; 0 || !_hookEvents.ContainsKey(id)) return;

            var hook = _hookEvents[id];

            if (hook.Value != null &amp;&amp; hook.Value.Dispose != null)
            {
                try
                {
                    hook.Value.Dispose(obj);
                }
                catch (Exception)
                {
                    // neet to be define if we need to throw the exception
                }
            }

            _hookEvents.Remove(id);
        }

        private IntPtr SetHook(LowLevelKeyboardProc proc)
        {
            using (Process curProcess = Process.GetCurrentProcess())
            using (ProcessModule curModule = curProcess.MainModule)
            {
                return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
            }
        }

        private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

        private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode = 2)
                {
                    var keysToAction = _hookEvents.Values.FirstOrDefault(val =&gt; val.Key.Equals(_pressedKeys));
                    if (keysToAction.Value != null)
                    {
                        keysToAction.Value.Exceute();
                        // don't try to get the action again after the execute becasue it may removed already
                        result = new IntPtr(1);
                    }
                }
            }
            else if (wParam == (IntPtr)WM_KEYUP)
            {
                _pressedKeys.Clear();
            }

            // in case we processed the message, prevent the system from passing the message to the rest of the hook chain
            // return result.ToInt32() == 0 ? CallNextHookEx(_hookId, nCode, wParam, lParam) : result;
            return CallNextHookEx(_hookId, nCode, wParam, lParam);
        }

        #region extern
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc 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

        #region IDsiposable
        private void Dispose(bool dispose)
        {
            try
            {
                if (_disposed)
                    return;

                UnhookWindowsHookEx(_hookId);
                if (dispose)
                {
                    _proc = null;
                    _hookEvents = null;
                    _pressedKeys = null;
                    GC.SuppressFinalize(this);
                }
                _disposed = true;
            }
            // ReSharper disable once EmptyGeneralCatchClause
            catch
            {
            }
        }

        public void Dispose()
        {
            Dispose(true);
        }

        ~GlobalKeyboardHook()
        {
            Dispose(false);
        }
        #endregion

        private class HookActions
        {
            public HookActions(Action excetue, Action dispose = null)
            {
                Exceute = excetue;
                Dispose = dispose;
            }

            public Action Exceute { get; set; }
            public Action Dispose { get; set; }

        }
        private class KeyCombination : IEquatable
        {
            private readonly bool _canModify;
            public KeyCombination(List keys)
            {
                _keys = keys ?? new List();
            }

            public KeyCombination()
            {
                _keys = new List();
                _canModify = true;
            }

            public void Add(Key key)
            {
                if (_canModify)
                {
                    _keys.Add(key);
                }
            }

            public void Remove(Key key)
            {
                if (_canModify)
                {
                    _keys.Remove(key);
                }
            }

            public void Clear()
            {
                if (_canModify)
                {
                    _keys.Clear();
                }
            }

            public int Count { get { return _keys.Count; } }

            private readonly List _keys;

            public bool Equals(KeyCombination other)
            {
                return other._keys != null &amp;&amp; _keys != null &amp;&amp; KeysEqual(other._keys);
            }

            private bool KeysEqual(List keys)
            {
                if (keys == null || _keys == null || keys.Count != _keys.Count) return false;
                for (int i = 0; i &lt; _keys.Count; i++)
                {
                    if (_keys[i] != keys[i])
                        return false;
                }
                return true;
            }

            public override bool Equals(object obj)
            {
                if (obj is KeyCombination)
                    return Equals((KeyCombination)obj);
                return false;
            }

            public override int GetHashCode()
            {
                if (_keys == null) return 0;

                //http://stackoverflow.com/a/263416
                //http://stackoverflow.com/a/8094931
                //assume keys not going to modify after we use GetHashCode
                unchecked
                {
                    int hash = 19;
                    for (int i = 0; i &lt; _keys.Count; i++)
                    {
                        hash = hash * 31 + _keys[i].GetHashCode();
                    }
                    return hash;
                }
            }

            public override string ToString()
            {
                if (_keys == null)
                    return string.Empty;

                var sb = new StringBuilder((_keys.Count - 1) * 4 + 10);
                for (int i = 0; i &lt; _keys.Count; i++)
                {
                    if (i <span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>&lt; _keys.Count - 1)
                        sb.Append(_keys[i] + &quot; , &quot;);
                    else
                        sb.Append(_keys[i]);
                }
                return sb.ToString();
            }
        }
    }
}

Advertisements
This entry was posted in .NET and tagged , , , , , , , , . Bookmark the permalink.

3 Responses to Low-Level Global Keyboard Hook in C#

  1. Philipp says:

    Hi, your html did not decode properly, e.g. /// &amp;lt;summary&amp;gt;

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s