Skip to content

Instantly share code, notes, and snippets.

@dudikeleti
Created July 10, 2018 09:08
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dudikeleti/a0ce3044b683634793cf297addbf5f11 to your computer and use it in GitHub Desktop.
Save dudikeleti/a0ce3044b683634793cf297addbf5f11 to your computer and use it in GitHub Desktop.
Low-Level Global Keyboard Hook in C#
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
/// <remarks>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.</remarks>
/// </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<int, KeyValuePair<KeyCombination, HookActions>> _hookEvents;
private bool _disposed;
private KeyCombination _pressedKeys;
/// <summary>
/// Return a singleton instance of <see cref="GlobalKeyboardHook"/>
/// </summary>
public static GlobalKeyboardHook Instance
{
get
{
Interlocked.CompareExchange(ref _instance, new GlobalKeyboardHook(), null);
return _instance;
}
}
private GlobalKeyboardHook()
{
_proc = HookCallback;
_hookEvents = new Dictionary<int, KeyValuePair<KeyCombination, HookActions>>();
_hookId = SetHook(_proc);
_pressedKeys = new KeyCombination();
}
/// <summary>
/// Register a keyboard hook event
/// </summary>
/// <param name="keys">The short keys. minimum is two keys</param>
/// <param name="execute">The action to run when the key ocmbination has pressed</param>
/// <param name="message">Empty if no error occurred otherwise error message</param>
/// <param name="runAsync">True if the action should execute in the background. -Be careful from thread affinity- Default is false</param>
/// <param name="dispose">An action to run when unsubscribing from keyboard hook. can be null</param>
/// <returns>Event id to use when unregister</returns>
public int Hook(List<Key> keys, Action execute, out string message, bool runAsync = false, Action<object> 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 < 2)
{
message = "You must provide at least two keys";
return -1;
}
if (!ValidateKeys(keys))
{
message = "Unallowed key. Only 'shift', 'ctrl' and 'a' - 'z' are allowed";
return -1;
}
var kc = new KeyCombination(keys);
int id = kc.GetHashCode();
if (_hookEvents.ContainsKey(id))
{
message = "The key combination is already exist it the application";
return -1;
}
// if the action should run async, wrap it with Task
Action asyncAction = null;
if (runAsync)
asyncAction = () => Task.Run(() => execute);
_hookEvents[id] = new KeyValuePair<KeyCombination, HookActions>(kc, new HookActions(asyncAction ?? execute, dispose));
message = string.Empty;
return id;
}
private bool ValidateKeys(IEnumerable<Key> keys)
{
return keys.All(t => IsKeyValid((int)t));
}
private bool IsKeyValid(int key)
{
// 'alt' is sys key and hence is disallowed.
// a - z and shift, ctrl.
return key >= 44 && key <= 69 || key >= 116 && key <= 119;
}
/// <summary>
/// Un register a keyboard hook event
/// </summary>
/// <param name="id">event id to remove</param>
/// <param name="obj">parameter to pass to dispose method</param>
public void UnHook(int id, object obj = null)
{
if (_hookEvents == null || id < 0 || !_hookEvents.ContainsKey(id)) return;
var hook = _hookEvents[id];
if (hook.Value != null && 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 < 0)
return CallNextHookEx(_hookId, nCode, wParam, lParam);
var result = new IntPtr(0);
if (wParam == (IntPtr)WM_KEYDOWN)
{
_pressedKeys.Add(KeyInterop.KeyFromVirtualKey(Marshal.ReadInt32(lParam))); // vkCode (in KBDLLHOOKSTRUCT) is DWORD (actually it can be 0-254)
if (_pressedKeys.Count >= 2)
{
var keysToAction = _hookEvents.Values.FirstOrDefault(val => 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<object> dispose = null)
{
Exceute = excetue;
Dispose = dispose;
}
public Action Exceute { get; set; }
public Action<object> Dispose { get; set; }
}
private class KeyCombination : IEquatable<KeyCombination>
{
private readonly bool _canModify;
public KeyCombination(List<Key> keys)
{
_keys = keys ?? new List<Key>();
}
public KeyCombination()
{
_keys = new List<Key>();
_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<Key> _keys;
public bool Equals(KeyCombination other)
{
return other._keys != null && _keys != null && KeysEqual(other._keys);
}
private bool KeysEqual(List<Key> keys)
{
if (keys == null || _keys == null || keys.Count != _keys.Count) return false;
for (int i = 0; i < _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 < _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 < _keys.Count; i++)
{
if (i < _keys.Count - 1)
sb.Append(_keys[i] + " , ");
else
sb.Append(_keys[i]);
}
return sb.ToString();
}
}
}
}
@ahvahsky2008
Copy link

how use it?

@dudikeleti
Copy link
Author

It's a singleton so GlobalKeyboardHook.Instance.Hook(insert key combination and other parameters) and you can call Unhhok if you need.

@kingofnull
Copy link

Dont work!
My none working code:

static void Main() {
	string message;
	GlobalKeyboardHook.Instance.Hook(new List<System.Windows.Input.Key> { System.Windows.Input.Key.A, System.Windows.Input.Key.B},
		()=> {
			  Console.WriteLine("A-B");
		}, out message);
	Console.WriteLine(message);
	while (true) Thread.Sleep(1000);
}

@dudikeleti
Copy link
Author

dudikeleti commented Mar 18, 2022

Create a WPF desktop app and it will work for you (I checked it now on .NET Core 3.1 and .NET 6 to verify that is still working). I'm sorry but I don't have time now to make it work on other apps.

@kingofnull
Copy link

Thanks this is a very easy class for shortcuts and key combinations. I do some research and found out I should change run mode in console application. Your code works fine in a form application but to use it in a console application, run mode must be changed. My final minimal code for console application is below: (don't forget to add System.Window.Forms in refrences):

[STAThread]
static void Main()
{
    string message;
    var hookId = GlobalKeyboardHook.Instance.Hook(
        new List<System.Windows.Input.Key> {
            System.Windows.Input.Key.A,
            System.Windows.Input.Key.B
        },
        () =>
        {
            Console.WriteLine("a-b");
        },
        out message);
    Console.WriteLine(message);
    Application.Run();
    GlobalKeyboardHook.Instance.UnHook(hookId);
}

@dudikeleti
Copy link
Author

Great! I'm glad to see that it works for you :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment