Skip to content

Commit

Permalink
restore global keyboard capture and override
Browse files Browse the repository at this point in the history
  • Loading branch information
luttje committed Mar 14, 2022
1 parent db075cc commit 17cea9c
Show file tree
Hide file tree
Showing 21 changed files with 509 additions and 190 deletions.
5 changes: 0 additions & 5 deletions KeyToJoy/AboutForm.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace KeyToJoy
Expand Down
2 changes: 1 addition & 1 deletion KeyToJoy/BindingForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public partial class BindingForm : Form
{
internal BindingOption BindingSetting { get; set; }

private List<RadioButton> radioButtonGroup = new List<RadioButton>();
private readonly List<RadioButton> radioButtonGroup = new List<RadioButton>();

internal BindingForm(BindingOption bindingOption)
:this()
Expand Down
5 changes: 0 additions & 5 deletions KeyToJoy/InitForm.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
using KeyToJoy.Input;
using SimWinInput;
using System;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace KeyToJoy
Expand Down
4 changes: 0 additions & 4 deletions KeyToJoy/Input/Binding.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace KeyToJoy.Input
{
Expand Down
5 changes: 0 additions & 5 deletions KeyToJoy/Input/BindingOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Resources;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace KeyToJoy.Input
{
Expand Down
6 changes: 1 addition & 5 deletions KeyToJoy/Input/BindingPreset.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
using KeyToJoy.Properties;
using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using SimWinInput;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace KeyToJoy.Input
Expand Down
163 changes: 163 additions & 0 deletions KeyToJoy/Input/GlobalInputHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using KeyToJoy.Input.LowLevel;

namespace KeyToJoy.Input
{
// Based on these sources:
// - https://gist.github.com/Stasonix/3181083
// - https://stackoverflow.com/a/34384189
partial class GlobalInputHook : IDisposable
{
public event EventHandler<GlobalKeyboardHookEventArgs> KeyboardInputEvent;
public event EventHandler<GlobalMouseHookEventArgs> MouseInputEvent;

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);

/// <summary>
/// The SetWindowsHookEx function installs an application-defined hook procedure into a hook chain.
/// You would install a hook procedure to monitor the system for certain types of events. These events are
/// associated either with a specific thread or with all threads in the same desktop as the calling thread.
/// </summary>
/// <param name="idHook">hook type</param>
/// <param name="lpfn">hook procedure</param>
/// <param name="hMod">handle to application instance</param>
/// <param name="dwThreadId">thread identifier</param>
/// <returns>If the function succeeds, the return value is the handle to the hook procedure.</returns>
[DllImport("USER32", SetLastError = true)]
static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);

/// <summary>
/// The UnhookWindowsHookEx function removes a hook procedure installed in a hook chain by the SetWindowsHookEx function.
/// </summary>
/// <param name="hhk">handle to hook procedure</param>
/// <returns>If the function succeeds, the return value is true.</returns>
[DllImport("USER32", SetLastError = true)]
public static extern bool UnhookWindowsHookEx(IntPtr hHook);

/// <summary>
/// The CallNextHookEx function passes the hook information to the next hook procedure in the current hook chain.
/// A hook procedure can call this function either before or after processing the hook information.
/// </summary>
/// <param name="hHook">handle to current hook</param>
/// <param name="code">hook code passed to hook procedure</param>
/// <param name="wParam">value passed to hook procedure</param>
/// <param name="lParam">value passed to hook procedure</param>
/// <returns>If the function succeeds, the return value is true.</returns>
[DllImport("USER32", SetLastError = true)]
static extern IntPtr CallNextHookEx(IntPtr hHook, int code, IntPtr wParam, IntPtr lParam);

public GlobalInputHook()
{
windowsHookHandle = IntPtr.Zero;
user32LibraryHandle = IntPtr.Zero;
hookProc = LowLevelInputHook; // 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}.");
}

foreach (var windowsHook in new int[] { WH_KEYBOARD_LL, WH_MOUSE_LL })
{
windowsHookHandle = SetWindowsHookEx(windowsHook, hookProc, user32LibraryHandle, 0);
if (windowsHookHandle == IntPtr.Zero)
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to adjust input 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 input hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
windowsHookHandle = IntPtr.Zero;

// ReSharper disable once DelegateSubtraction
hookProc -= LowLevelInputHook;
}
}

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;
}
}

~GlobalInputHook()
{
Dispose(false);
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

public const int WH_KEYBOARD_LL = 13;
public const int WH_MOUSE_LL = 14;

public IntPtr LowLevelInputHook(int nCode, IntPtr wParam, IntPtr lParam)
{
var isInputHandled = false;
var wparamTyped = wParam.ToInt32();

if (Enum.IsDefined(typeof(KeyboardState), wparamTyped))
{
object o = Marshal.PtrToStructure(lParam, typeof(LowLevelKeyboardInputEvent));
LowLevelKeyboardInputEvent p = (LowLevelKeyboardInputEvent)o;

var eventArguments = new GlobalKeyboardHookEventArgs(p, (KeyboardState)wparamTyped);

EventHandler<GlobalKeyboardHookEventArgs> handler = KeyboardInputEvent;
handler?.Invoke(this, eventArguments);

isInputHandled = eventArguments.Handled;
}
else if (Enum.IsDefined(typeof(MouseState), wparamTyped))
{
object o = Marshal.PtrToStructure(lParam, typeof(LowLevelMouseInputEvent));
LowLevelMouseInputEvent p = (LowLevelMouseInputEvent)o;

var eventArguments = new GlobalMouseHookEventArgs(p, (MouseState)wparamTyped);

EventHandler<GlobalMouseHookEventArgs> handler = MouseInputEvent;
handler?.Invoke(this, eventArguments);

isInputHandled = eventArguments.Handled;
}

return isInputHandled ? (IntPtr)1 : CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}
}
}
3 changes: 1 addition & 2 deletions KeyToJoy/Input/KeyboardBinding.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using RawKeyboardFlags = Linearstar.Windows.RawInput.Native.RawKeyboardFlags;
using Linearstar.Windows.RawInput.Native;
using System.Windows.Forms;
using Newtonsoft.Json;

Expand Down
19 changes: 19 additions & 0 deletions KeyToJoy/Input/LowLevel/GlobalKeyboardHookEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.ComponentModel;

namespace KeyToJoy.Input.LowLevel
{
// Source: https://stackoverflow.com/a/34384189
internal class GlobalKeyboardHookEventArgs : HandledEventArgs
{
public KeyboardState KeyboardState { get; private set; }
public LowLevelKeyboardInputEvent KeyboardData { get; private set; }

public GlobalKeyboardHookEventArgs(
LowLevelKeyboardInputEvent keyboardData,
KeyboardState keyboardState)
{
KeyboardData = keyboardData;
KeyboardState = keyboardState;
}
}
}
21 changes: 21 additions & 0 deletions KeyToJoy/Input/LowLevel/GlobalMouseHookEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@


using System.ComponentModel;

namespace KeyToJoy.Input.LowLevel
{
// Source: https://stackoverflow.com/a/34384189
internal class GlobalMouseHookEventArgs : HandledEventArgs
{
public MouseState MouseState { get; private set; }
public LowLevelMouseInputEvent MouseData { get; private set; }

public GlobalMouseHookEventArgs(
LowLevelMouseInputEvent mouseData,
MouseState mouseState)
{
MouseData = mouseData;
MouseState = mouseState;
}
}
}
17 changes: 17 additions & 0 deletions KeyToJoy/Input/LowLevel/KeyboardState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace KeyToJoy.Input.LowLevel
{
// Source: https://stackoverflow.com/a/34384189
public enum KeyboardState
{
KeyDown = 0x0100,
KeyUp = 0x0101,
SysKeyDown = 0x0104,
SysKeyUp = 0x0105
}
}
55 changes: 55 additions & 0 deletions KeyToJoy/Input/LowLevel/LowLevelKeyboardInputEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Runtime.InteropServices;

namespace KeyToJoy.Input.LowLevel
{
// Source: https://stackoverflow.com/a/34384189
[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.
///
/// - LLKHF_EXTENDED
/// (KF_EXTENDED >> 8)
/// Test the extended-key flag.
///
/// - LLKHF_LOWER_IL_INJECTED
/// 0x00000002
/// Test the event-injected (from a process running at lower integrity level) flag.
///
/// - LLKHF_INJECTED
/// 0x00000010
/// Test the event-injected (from any process) flag.
///
/// - LLKHF_ALTDOWN
/// (KF_ALTDOWN >> 8)
/// Test the context code.
///
/// - LLKHF_UP
/// (KF_UP >> 8)
/// Test the transition-state flag.
/// </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;
}
}
36 changes: 36 additions & 0 deletions KeyToJoy/Input/LowLevel/LowLevelMouseInputEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Runtime.InteropServices;

namespace KeyToJoy.Input.LowLevel
{
// Source: https://stackoverflow.com/a/34384189
[StructLayout(LayoutKind.Sequential)]
public struct LowLevelMouseInputEvent
{
/// <summary>
/// The x- and y-coordinates of the cursor, in per-monitor-aware screen coordinates.
/// </summary>
public Point Position;

/// <summary>
/// If the message is WM_MOUSEWHEEL, the high-order word of this member is the wheel delta. The low-order word is reserved. A positive value indicates that the wheel was rotated forward, away from the user; a negative value indicates that the wheel was rotated backward, toward the user. One wheel click is defined as WHEEL_DELTA, which is 120.
/// If the message is WM_XBUTTONDOWN, WM_XBUTTONUP, WM_XBUTTONDBLCLK, WM_NCXBUTTONDOWN, WM_NCXBUTTONUP, or WM_NCXBUTTONDBLCLK, the high-order word specifies which X button was pressed or released, and the low-order word is reserved.This value can be one or more of the following values.Otherwise, mouseData is not used.
/// </summary>
public int MouseData;

/// <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;
}
}
Loading

0 comments on commit 17cea9c

Please sign in to comment.