using System; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows.Input; namespace MSFSPopoutPanelManager.Orchestration { 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 OnKeyboardPressed; 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; // ReSharper disable once DelegateSubtraction _hookProc -= LowLevelKeyboardProc; } } if (_user32LibraryHandle != IntPtr.Zero) { if (!FreeLibrary(_user32LibraryHandle)) // reduces reference to library by 1. { 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); } private IntPtr _windowsHookHandle; private IntPtr _user32LibraryHandle; private HookProc _hookProc; 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 { /// /// A virtual-key code. The code must be a value in the range 1 to 254. /// public int VirtualCode; /// /// A hardware scan code for the key. /// public int HardwareScanCode; /// /// The extended-key flag, event-injected Flags, context code, and transition-state flag. This member is specified as follows. An application can use the following values to test the keystroke Flags. Testing LLKHF_INJECTED (bit 4) will tell you whether the event was injected. If it was, then testing LLKHF_LOWER_IL_INJECTED (bit 1) will tell you whether or not the event was injected from a process running at lower integrity level. /// public int Flags; /// /// The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message. /// public int TimeStamp; /// /// Additional information associated with the message. /// public IntPtr AdditionalInformation; public Key Key => KeyInterop.KeyFromVirtualKey(VirtualCode); } public const int WH_KEYBOARD_LL = 13; //const int HC_ACTION = 0; public enum KeyboardState { KeyDown = 0x0100, KeyUp = 0x0101, SysKeyDown = 0x0104, SysKeyUp = 0x0105 } public IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam) { var fEatKeyStroke = false; 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); OnKeyboardPressed?.Invoke(this, eventArguments); fEatKeyStroke = eventArguments.Handled; } return fEatKeyStroke ? (IntPtr)1 : CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); } } }