Skip to content

Instantly share code, notes, and snippets.

@anaisbetts
Created June 14, 2019 23:38
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anaisbetts/90647fdf2205ab76866410defa8434c6 to your computer and use it in GitHub Desktop.
Save anaisbetts/90647fdf2205ab76866410defa8434c6 to your computer and use it in GitHub Desktop.
namespace GlobalHotKey
{
public class GlobalKeyboardHookEventArgs : HandledEventArgs
{
public GlobalKeyboardHook.KeyboardState KeyboardState { get; private set; }
public GlobalKeyboardHook.LowLevelKeyboardInputEvent KeyboardData { get; private set; }
public GlobalKeyboardHookEventArgs(
GlobalKeyboardHook.LowLevelKeyboardInputEvent keyboardData,
GlobalKeyboardHook.KeyboardState keyboardState)
{
KeyboardData = keyboardData;
KeyboardState = keyboardState;
}
}
public class GlobalKeyboardHook : IDisposable
{
public event EventHandler<GlobalKeyboardHookEventArgs> KeyboardPressed;
IntPtr _windowsHookHandle;
IntPtr _user32LibraryHandle;
HookProc _hookProc;
public GlobalKeyboardHook()
{
_windowsHookHandle = IntPtr.Zero;
_user32LibraryHandle = IntPtr.Zero;
_hookProc = LowLevelKeyboardProc; // we must keep alive _hookProc, because GC is not aware about SetWindowsHookEx behaviour.
_user32LibraryHandle = LoadLibrary("User32");
if (_user32LibraryHandle == IntPtr.Zero) {
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to load library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
_windowsHookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, _user32LibraryHandle, 0);
if (_windowsHookHandle == IntPtr.Zero) {
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to adjust keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
}
protected virtual void Dispose(bool disposing)
{
if (disposing) {
// because we can unhook only in the same thread, not in garbage collector thread
if (_windowsHookHandle != IntPtr.Zero) {
if (!UnhookWindowsHookEx(_windowsHookHandle)) {
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to remove keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
_windowsHookHandle = IntPtr.Zero;
_hookProc -= LowLevelKeyboardProc;
}
}
if (_user32LibraryHandle != IntPtr.Zero) {
if (!FreeLibrary(_user32LibraryHandle)) {
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to unload library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
_user32LibraryHandle = IntPtr.Zero;
}
}
~GlobalKeyboardHook()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
private static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
private static extern bool FreeLibrary(IntPtr hModule);
[DllImport("USER32", SetLastError = true)]
static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
[DllImport("USER32", SetLastError = true)]
public static extern bool UnhookWindowsHookEx(IntPtr hHook);
[DllImport("USER32", SetLastError = true)]
static extern IntPtr CallNextHookEx(IntPtr hHook, int code, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)]
public struct LowLevelKeyboardInputEvent
{
public int VirtualCode;
public int HardwareScanCode;
public int Flags;
public int TimeStamp;
public IntPtr AdditionalInformation;
}
public const int WH_KEYBOARD_LL = 13;
public enum KeyboardState {
KeyDown = 0x0100,
KeyUp = 0x0101,
SysKeyDown = 0x0104,
SysKeyUp = 0x0105
}
public const int VkSnapshot = 0x2c;
public IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode != 0) {
return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}
var wparamTyped = wParam.ToInt32();
if (Enum.IsDefined(typeof(KeyboardState), wparamTyped)) {
var o = Marshal.PtrToStructure(lParam, typeof(LowLevelKeyboardInputEvent));
var p = (LowLevelKeyboardInputEvent)o;
var eventArguments = new GlobalKeyboardHookEventArgs(p, (KeyboardState)wparamTyped);
EventHandler<GlobalKeyboardHookEventArgs> handler = KeyboardPressed;
handler?.Invoke(this, eventArguments);
}
return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}
}
public class ObservableKeyboard
{
// NB: We really only can have exactly *one* global keyboard hook, so we will
// make sure that anyone trying to listen to it will get that one
static Lazy<IObservable<GlobalKeyboardHookEventArgs>> ghkObservable = new Lazy<IObservable<GlobalKeyboardHookEventArgs>>(() => {
var ret = Observable.Create<GlobalKeyboardHookEventArgs>((subj) => {
var ghk = new GlobalKeyboardHook();
ghk.KeyboardPressed += (o, e) => subj.OnNext(e);
return ghk;
});
// NB: We are firing this event for literally every keypress - we
// need to spend as little time as possible in the hook procedure
// or we risk Windows unhooking us
return ret
.SubscribeOn(RxApp.MainThreadScheduler)
.ObserveOn(RxApp.TaskpoolScheduler)
.Publish().RefCount();
}, false);
public IObservable<GlobalKeyboardHookEventArgs> ListenToLowLevelKeyboard()
{
return ghkObservable.Value;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment