Keyboard Hooking With C# – Redux

[9 minute read]

Many moons ago, in preparation for a major rewrite to my QuickShift1 application, I wrote a low level keyboard hook implementation in C#. The original version of QuickShift employed global hotkeys to respond to keyboard input, however that mechanism proved to offer less-than-ideal performance: When the OS recognizes a hotkey combination, it responds by posting a message to the main window of the application associated with that hotkey. It appears that because QuickShift would often go unused for extended periods and become idle, its response time to those messages would drop noticeably. Hooking the keyboard seemed the most viable method of avoiding that delay. Unfortunately, very shortly after I wrote my hook implementation, I took a job 1,600 miles away from where I lived, and QuickShift got put on the far back burner for nearly 18 months. Stupid day jobs. Always getting in the way of things.

Fast forward to the present. QuickShift finally got the overhaul it so desperately needed, and my hook implementation finally got put to some practical use. It was very exciting. But predictably I found a number of shortcomings to my original design and one pretty serious bug, leading to a significant re-write of the code. I also had two new capabilities in mind that I wanted to add, specifically for QuickShift. First, I wanted to be able to override native Windows logo key shortcuts. Second, I wanted a way to completely block out all keyboard input processing beyond the boundary of my hook.

It’s been long enough now since I first began this re-write process that my memory of exactly what changes I made to the original version and why is a bit fuzzy, but a few things do stand out in my mind. I found, for example, that returning a bool to indicate success or failure of the TrySetKeys() method was a bit useless. The new method, SetKeys(), simply throws an exception on failure. I also removed the requirement that the hook be given a descriptive name. That too seemed pretty useless. More significantly, the half dozen or so properties which provided information about the keys and modifiers applied to the hook have been consolidated into a new KeyCombination type. I did, however, stick with the original Star Trek inspired Engage() and Disengage() methods. :-)

The one glaring bug I discovered, as it turned out, had already been plainly pointed out to me in a comment posted to my original hook post. My response to that comment showed pretty clearly that I hadn’t ever used that implementation in a real application because, sure enough, the first time I gave QuickShift a trial run that lasted long enough for the garbage collector to do its thing BAM! I get the same CallbackOnCollectedDelegate exception the commenter had complained about. It was easily reproducible with a call to GC.Collect() after the application had started up. The cause? Look here:

IntPtr HookCallBack(int nCode, IntPtr wParam, IntPtr lParam)
{
	// code to process hook...
}

hookId = NativeMethods.SetWindowsHookEx(
	NativeMethods.KeyStateConstants.WH_KEYBOARD_LL,
	HookCallback,
	IntPtr.Zero,
	0);

Seems correct, right? As stated in the MSDN documentation, the second parameter of SetWindowsHookEx needs to be a pointer to the hook procedure. However, while the above code does satisfy that requirement, what I did not know is that it causes C# to create a delegate instance of HookCallback on the fly. And since that instance is not referenced anywhere that would keep it alive, the garbage collector rightly destroys it — resulting in an exception the next time the hook is invoked. Thankfully, I wasn’t the first person to run into that situation and fixing it was as simple as creating a field to hold an instance of HookCallback and passing that instance reference into SetWindowsHookEx:

public class KeyboardHook : IDisposable
{
	readonly NativeMethods.LowLevelKeyboardProc hookCallback;

	public KeyboardHook()
	{
		hookCallback = HookCallBack;
	}

	IntPtr HookCallBack(int nCode, IntPtr wParam, IntPtr lParam)
	{
		// code to process hook...
	}

	hookId = NativeMethods.SetWindowsHookEx(
		NativeMethods.KeyStateConstants.WH_KEYBOARD_LL,
		hookCallback,
		IntPtr.Zero,
		0);
}

New Features

As I already mentioned, one of my design goals was to allow the hook to override native Windows logo key shortcuts. Why? For one thing, I don’t really like some of the shortcuts provided by Windows 7. Instead of Shift + Logo + [Left or Right] to move a window, I wanted to just use Logo + [Left or Right]. Instead of pressing Logo + Down twice to get a maximized window minimized (first to restore it, then to minimize it), I just wanted to press it once. Etcetera, etcetera. For another thing, some reserved shortcuts I simply never use. Logo + P is one example. Why should it go to waste? Now, I fully expected that some key combinations would not be overridable and I was right: Logo + L and (rather obviously) Ctrl + Alt + Del are apparently handled by the OS before hooks are processed. But most shortcuts can be suppressed or overridden.

Overriding logo key shortcuts isn’t perfect, however. Let’s say I override Logo + D, which normally displays the desktop. The first thing that happens is the Logo key is pressed. The hook gets the key press message, determines it is not a hook key combination, and passes the message along for further processing. At this point, once Windows receives a Logo key down message, it basically sits and waits to see what happens next and will respond one of three ways:

  • If an additional key is pressed which, when combined with the Logo key, constitutes a valid shortcut, Windows will execute the associated command.
  • If an additional key is pressed which does not form a valid shortcut, Windows will disregard the initial Logo key press event.
  • If no other key is pressed before the Logo key is released, Windows will display the Start menu.

Next, the D key is pressed. This time the hook determines this does constitute a hook key combination, processes it as such, but doesn’t pass the message along to anyone else, including Windows. That means Windows is still waiting around for more key press messages, which gives rise to an unfortunate imperfection. After Logo + D has been pressed, the keys will be released and one of two things will happen. If D is released first, the “D key up” message is passed to Windows, causing the OS to stop waiting for additional Logo key information. But if the Logo key is released first, a “Logo key up” message is sent to Windows, and the Start menu is displayed immediately after our hook procedure executes. This is because, as described in scenario #3 above, Windows received only two messages: Logo key down and Logo key up.

This is kind of a drag because it’s so easy to release the keys in the “wrong” order and get the Start menu shoved in your face when that’s not what you wanted to happen. I spent more hours trying to circumvent this than I care to admit — 8 hours last weekend alone — to no avail. Every “solution” I came up with resulted in either permanent suppression of the Start menu or left Windows thinking the Logo key was in a pressed state when it wasn’t, resulting in Logo-based shortcuts “randomly” executing. I even tried sending Windows fake key press messages. Didn’t work. Anyway, since both these side-effects are obviously worse than the original problem, I settled for the lesser of the evils.

The desire to override Logo key shortcuts led directly to my other stated goal: blocking all keyboard input processing beyond the hook boundary. Without this ability users would not be able to type, for example, Logo + D into a textbox because the moment they did, the native Windows shortcut command would execute and display the desktop. Accordingly, the new hook has two static methods, EngageFullKeyboardLockdown() and ReleaseFullKeyboardLockdown(), and a static event called LockedDownKeyboardKeyPressed. Once a keyboard lockdown has been enabled, all keyboard input will be filtered through that event only; all other processing will be blocked. This isn’t as scary as it may sound though because as soon as the process which created the hook terminates, the hook itself is removed from the system. However, I do recommend releasing the lockdown as soon as possible, otherwise you’re sure to have some pissed off users. :-) QuickShift, for example, engages a lockdown only when a particular textbox has focus. As soon as focus is lost by exiting the textbox or deactivating the main form, the lockdown is released.

There’s one last design decision I made that’s worth mentioning. QuickShift currently offers 21 distinct actions, each of which can be assigned a keyboard shortcut. That means up to 21 hooks can be installed simultaneously. I had to ask myself: Should each hook instance have it’s own callback handler? Or should there be a single, static callback handler which processed all the hooks? I hypothesized that one handler would offer better performance, but tested both scenarios just to be sure. To my surprise, 21 instance callbacks significantly out performed the single static callback. Unfortunately, I didn’t keep the output of those tests (grrrrrr) so I can’t tell you exactly how each performed, but the static method was something like 5 to 10 times slower. No matter what optimizations I made, the instance callbacks still won out. The bottleneck appeared to be due to hashtable lookups. In the instance-callback version, I did a simple string comparison to check whether pressed keys matched a given hook’s shortcut combination. (e.g. "Ctrl + Alt + D" == "Ctrl + Alt + D"). But in the single static callback version I used a HashSet to store all hook combinations, then searched that set for a match on each callback. Doing a string comparison is apparently significantly faster. I also don’t know how those results would have changed if, for example, there were 50, 100, or 1,000 hooks installed, but in the end, I chose to use instance callbacks because QuickShift will never offer more than, say, 25 hookable actions.

Whew! Ok. With all that said, here’s the kitchen sink exposé of my new and improved hook class. Download the code here.

public partial class Form1 : Form
{
	readonly KeyboardHook hook = new KeyboardHook();
	bool isLockedDown;

	public Form1()
	{
		InitializeComponent();

		// Handle key press events when keyboard is locked down.
		KeyboardHook.LockedDownKeyboardKeyPressed +=
			KeyboardHook_LockedDownKeyboardKeyPressed;

		// Overrides Show Desktop function on Windows Vista and Windows 7.
		hook.SetKeys(new KeyCombination(KeysEx.WinLogo | KeysEx.D));

		hook.AutoRepeat = true;
		hook.Pressed += hook_Pressed;
	}

	void hook_Pressed(object sender, EventArgs e)
	{
		// Invoke is required here to avoid cross threading exceptions.
		string text = string.Format("Hook pressed event: {0}{1}",
			DateTime.Now,
			Environment.NewLine);
		Action a = () => textBoxMessages.AppendText(text);
		Invoke(a);
	}

	void KeyboardHook_LockedDownKeyboardKeyPressed(object sender,
		KeyboardLockDownKeyPressedEventArgs e)
	{
		// Invoke is required here to avoid cross threading exceptions.
		string text = string.Format("Lockdown pressed event: {0}{1}",
			DateTime.Now,
			Environment.NewLine);
		Action a = () => textBoxMessages.AppendText(text);
		Invoke(a);
	}

	void buttonStart_Click(object sender, EventArgs e)
	{
		// Activate the hook
		hook.Engage();

		buttonStart.Enabled = false;
		buttonStop.Enabled = true;
	}

	void buttonStop_Click(object sender, EventArgs e)
	{
		// Deactivate the hook
		hook.Disengage();

		buttonStart.Enabled = true;
		buttonStop.Enabled = false;
	}

	void buttonLockdown_Click(object sender, EventArgs e)
	{
		// Toggle keyboard lockdown on/off.
		if (isLockedDown)
		{
			KeyboardHook.ReleaseFullKeyboardLockdown();
			buttonLockdown.Text = "Engage Lockdown";
		}
		else
		{
			KeyboardHook.EngageFullKeyboardLockdown();
			buttonLockdown.Text = "Release Lockdown";
		}

		isLockedDown = !isLockedDown;
	}
}

Notes

  1. QuickShift was an application I wrote that no longer exists.