2024-03-01 11:28:09 +00:00
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
{
2024-03-04 03:10:13 +00:00
public event EventHandler < GlobalKeyboardHookEventArgs > OnKeyboardPressed ;
2024-03-01 11:28:09 +00:00
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 ) ;
2024-03-04 03:10:13 +00:00
2024-03-01 11:28:09 +00:00
[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
{
/// <summary>
/// A virtual-key code. The code must be a value in the range 1 to 254.
/// </summary>
public int VirtualCode ;
/// <summary>
/// A hardware scan code for the key.
/// </summary>
public int HardwareScanCode ;
/// <summary>
/// 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.
/// </summary>
public int Flags ;
/// <summary>
/// The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message.
/// </summary>
public int TimeStamp ;
/// <summary>
/// Additional information associated with the message.
/// </summary>
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 )
{
2024-03-04 03:10:13 +00:00
var fEatKeyStroke = false ;
2024-03-01 11:28:09 +00:00
2024-03-04 03:10:13 +00:00
var wParamTyped = wParam . ToInt32 ( ) ;
if ( Enum . IsDefined ( typeof ( KeyboardState ) , wParamTyped ) )
2024-03-01 11:28:09 +00:00
{
2024-03-04 03:10:13 +00:00
var o = Marshal . PtrToStructure ( lParam , typeof ( LowLevelKeyboardInputEvent ) ) ;
var p = ( LowLevelKeyboardInputEvent ) o ;
2024-03-01 11:28:09 +00:00
2024-03-04 03:10:13 +00:00
var eventArguments = new GlobalKeyboardHookEventArgs ( p , ( KeyboardState ) wParamTyped ) ;
2024-03-01 11:28:09 +00:00
2024-03-04 03:10:13 +00:00
OnKeyboardPressed ? . Invoke ( this , eventArguments ) ;
2024-03-01 11:28:09 +00:00
fEatKeyStroke = eventArguments . Handled ;
}
return fEatKeyStroke ? ( IntPtr ) 1 : CallNextHookEx ( IntPtr . Zero , nCode , wParam , lParam ) ;
}
}
}