Created
June 14, 2019 23:38
-
-
Save anaisbetts/90647fdf2205ab76866410defa8434c6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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