Hotkey Editor Control for WPF

Most modern GUI applications have some sort of hotkey or shortcut system for executing common tasks using predefined keystrokes. Some daemon or background applications usually incorporate system-wide global hotkeys to access their functionality from anywhere. One way or another, at some point you will face the task of opening up these hotkeys for configuration, so that the users can define their own keystrokes. For these purposes, applications running on Windows typically use a read-only text box that captures keystrokes and previews them in form of combination of key names, separated by the plus sign, for example Ctrl+Alt+Z or Alt+P. Since WPF doesn’t offer such control out of the box, you will have to make one yourself.


WPF implements two enumerations that are useful for us System.Windows.Input.Key and System.Windows.Input.ModifierKeys. In our case, the first will specify the actual key that needs to be pressed and the second will specify what modifiers (Control, Alt, Shift, Win) are required, if any. In order not to deal with two separate enums all the time and to keep things encapsulated, let’s make a specialized struct.

On top of the original purpose of containing two enums in a single object, this struct will also be useful to convert the keystrokes into its string representation, as was discussed earlier. There are also equality comparison methods, in case you need to see if two hotkey objects are logically identical. Overriding ToString() method has the additional benefit of controlling how the object is shown when debugging, as having it show {Alt + K} is a lot more useful than {Your.Namespace.Hotkey}.

For the actual control you can either derive from the TextBox control or make your own UserControl. I chose the latter, mainly for two reasons:

  1. I can hide all the inherent textbox properties that aren’t applicable for my control.
  2. I have the mobility in case I need to replace textbox with a different control or add something on top of it.

There are a few important things straight away. First of all, the text box shouldn't allow manual text input, so I set IsReadOnly to true. Second of all, it’d be best to remove the caret, since it isn’t useful for us in any way, setting IsReadOnlyCaretVisible to false takes cares of that. We also don’t want it to keep the undo/redo history so let’s disable IsUndoEnabled as well. Finally, WPF textboxes have an inherent context menu with buttons like Copy/Cut/Paste/etc and there's certainly no need for that here. The most convenient way to disable the context menu is actually to hide it, because setting it to null doesn’t seem to do anything.

To capture keystrokes I'm processing the PreviewKeyDown event, because it also lets us disable standard textbox shortcuts such as Ctrl+A, Ctrl+C, etc. The text property is bound to the Hotkey property (which you will see in a second) using the OneWay mode. The latter is important, because there is no string to Hotkey conversion.

The code-behind of this user control will look like this:

Going from top to bottom:

HotkeyProperty – register the only DependencyProperty, letting UserControl take full advantage of WPF bindings. Make sure to use FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, since the control has to work in both ways.

HasKeyChar(Key key) is a method that returns true for keys that have a character representation, such as letter keys, number keys as opposed to keys like Tab, ArrowUp, F3. This will be useful later.

Hotkey property is the wrapper around HotkeyProperty dependency property.

HotkeyTextBox_PreviewKeyDown is where the input is processed:

  • Set e.Handled to true, so that no other handler for this textbox receives the key down event.
  • Extract the modifiers and key data.
  • Due to the role of the Alt key in Windows, when it’s the only modifier, the actual key will have to be extracted from SystemKey property instead.
  • To let the users delete the currently assigned key stroke, I chose the keys Delete, Backspace and Escape. Pressing those will clear the currently selected hotkey and return.
  • Check if the last key pressed isn’t a modifier key, because you need an actual key to finish the keystroke.
  • Typically you don’t want to let users set hotkeys on keys that are used to write text. Here’s where I use the HasKeyChar(…) method to check if the key has a literal representation and filter those keys out.
  • Finally, create a new hotkey object and update the property and bindings.

Here’s how it looks in one of my applications:

Comments