Global shortcuts in C#WinForms and WPF

Keywords: Windows Attribute Programming

introduce
Sometimes, your form or window (depending on whether you are building a WinForm or WPF application) may need to respond to hotkeys or shortcuts (which can be used interchangeably in this article), regardless of whether the application is centralized or running in the background.

In addition, if you want to register multiple hotkeys, it's a bit difficult inside or outside the application. In this article, we will create a library that allows hotkeys in application processes and other keyboard operations.

term
HotKeys: These are keys that users press on the keyboard, which may need to be handled by your application.
Local HotKeys: A class that manages regular hotkeys in an application; they have a key and or modifier (optional modifier for Local Hotkey) that only works when the application is centralized. An example is Control + C (for replication in most cases)
GlobalHotKeys: A class that manages hotkeys outside the application, regardless of whether the form or window has focus or is running in the background. They are regular hotkeys with key AND modifiers that are processed as long as the application is running (whether in background focus, minimization or runtime). An example is Window + L (for locking computers)
Chord Hot Keys: A class of advanced Local Hot Keys, more like two Local Hot Keys; BASE and CHORD. The base must have a BaseKey AND, a BaseModifier, and when they are pressed, they start Chord, which has a key and an optional Modifier. An example is Control + K, Control + D (for indentation code in Visual Studio IDE), in which case Control + K is the foundation, and Control + D is the hotkey of chord. Like Local Hot Keys, they are only the focus of the application.
HotKeyManager: This class manages the above class; it tracks its changes and raises events when the hotkey associated with it is pressed on the keyboard. In addition, the HotKeyManager class can listen for all keystrokes from the keyboard, report them to our application, and simulate keystrokes.
* Registering hotkeys means subscribing to Hotkey to Hotkey Manager.
HotKey Control: UserControl that allows users to set hotkeys by entering.

* HotKeyControl does not treat Windows keys as modifiers, but rather as exactly the same keys as in the Key (s) enumeration.

background
Before I started using. Net, I wanted to disable some keys on the keyboard, especially for Control + Alt + Delete, a computer-based test application I created. I studied programming, but found none until I led to ScanCodeMap. (Now don't get too excited, HotKey Manager can't do this.) Only the warning was that I had to restart Windows. When I want to enable or disable these keys. I also have trouble. Net implements global keyboard shortcuts and makes keys variable. This library was created to speed up, allow you to easily perform operations on the keyboard and manage multiple dynamic hotkeys at the same time, such as VLC players and other applications.

source file
WPF's HotKey Manager class has most WinForm equivalents. Although it may be easier to create classes for both platforms in the same project, if you create a program in WinForm and want to reference dll, you also need to import:

PresentationCore
Demonstration framework and
WindowsBase
This is the core library of WPF, and if you create a program in WPF, you will need to import:

System.Windows.Forms and
System.Drawing.
Versions depend on the version used when writing a project, which will block your project from unnecessary references, which is why I write libraries in two separate projects to keep code usage exactly the same. Source files (in C # but compatible with VB) contain five projects: WinForm Library in C # 2.0, WPF Library in C # 3.0 and 4.0 (just because I want to create a better control for WPF) and WinForm and WPF project.

* I'm creating another library for WinForm that converts classes into components so that they can be added and validated at design time, rather than written in code.

Go to work.
Shortcuts include modifiers (which can be one or more) and a single key. Modifier keys are: Shift key, Alt key, control key and window key (which is not exactly a modifier and should only be used when creating GlobalShortcuts). Note that this is left or right key. Right Alt key in WinForm sends "Control + Alt", WPF sends LeftCtrl, but there are some adjustments to send the same key as WinForm.

A reference is added to Microsoft. Visual Basic, which we will use to perform some string manipulation and get some keyboard status.

We define a modifier enumeration and mark it with the flags attribute, which specifies that the enumeration can be treated as a bit field, and the second enumeration defines when we want our local hotkey event.

//First, we define modifiers for Global Hotkeys and Local Hotkeys.

#region **Modifiers and constants.
    /// <summary>Define the key used as a modifier.
    /// </summary>
    [Flags]
    public enum Modifiers
    {
        /// <summary>The specified key should be treated as yes without any modifiers.
        /// </summary>

        NoModifier = 0x0000,

        /// <summary>Specify the key to press the accelerator key( ALT). 
        /// </summary>
        Alt = 0x0001,
        /// <summary>Specifies that the control key is pressed with the key.
        /// </summary>
        Ctrl = 0x0002,
        /// <summary>Specify that the relevant key is used to press Shift Key.
        /// </summary>
        Shift = 0x0004,
        /// <summary>Specify that the relevant key is used to press Window Key.
        /// </summary>
        Win = 0x0008
    }

    public enum RaiseLocalEvent
    {
        OnKeyDown = 0x100, //256. Same as WM_KEYDOWN.
        OnKeyUp = 0x101 //Another 257, the same as WM_KEYUP.
    }
#endregion 

WPF already has its modifier keys as ModifierKeys in the namespace System.Windows.Input and has flags properties set, so this is not necessary.

The flags attribute allows modifiers to be combined through XOR, so you can write a statement in WinForm:

**In C#**
Modifiers modifier = Modifiers.Control | Modifiers.Shift; 
**In VB**
Dim modifier as Modifiers = Modifiers.Control Xor Modifiers.Shift
**And in WPF,**
ModifierKeys modifier = ModifierKeys.Control | ModifierKeys.Shift;
**In VB**
Dim modifier as ModifierKeys = ModifierKeys.Control Xor ModifierKeys.Shift

The modifier is "Control + Shift"

RaiseLocalEvent enumeration will determine when a LocalHotKey event should be triggered when the key is closed (OnKeyDown) or when it is released (OnKeyUp).

public class HotKeyAlreadyRegisteredException : Exception
public class HotKeyUnregistrationFailedException : Exception
public class HotKeyRegistrationFailedException : Exception
public class HotKeyInvalidNameException : Exception

HotKey Already Registered Exception: As the name implies, this exception is thrown when you try to re-register a hotkey (with the same name, key, or modifier) using HotKey Manager. For GlobalHotKeys, this exception is thrown when the keys and modifiers of the global hotkey are being used by another application. For example, trying to register Window + L causes this exception. When using libraries, trying to register ChordHotKey with the basic keys and modifiers that LocalHotKey is using causes HotKey Already Registered Exception, while trying to register HotKey with keys and modifiers registered as basic keys and modifiers causes the same exception. Priority is given to the first registered HotKey.

HotKey UnregistrationFailedException: This exception is thrown when HotKey cannot log out. HotKey Registration Failed Exception is the same, thrown when HotKey cannot be registered, and also occurs when you try to register HotKeys (such as Control + Escape).

HotKey InvalidNameException: This exception is thrown when you try to register a HotKey with an invalid name; the hotkeys in this library are more like controls, and you need to assign a name to each hotkey, as in Visual Studio. A valid HotKey name does not start with a number or contain spaces. Check the name in the function.

public static bool IsValidHotkeyName(string text)
{
    //If the name starts with a number, contains space or is null, return false.
    if (string.IsNullOrEmpty(text)) return false;

    if (text.Contains(" ") || char.IsDigit((char)text.ToCharArray().GetValue(0)))
        return false;

    return true;
}

Of course, if you like, you can change this.

HotKey Shared Class
This is a static class that helps perform functions such as checking the name of the HotKey control discussed earlier, splitting strings into their corresponding keys and modifiers (useful for our HotKey Control) and reversing processes. It also contains the structure of an enumeration modifier. HotKeyControl will treat hotkeys as a string, which is not very useful unless we split them into their respective keys and modifiers, and the ParseShortcut function allows us to do this.

Class has static functions ParseShortcut and CombineShortcut. The former allows you to strip a shortcut by saying "Control + Alt + T" to its respective modifiers (Control, Alt) and keys (T), while the latter does the opposite.

The ParseShortcut function is an array of objects that return modifiers for the hotkey string in its lower bound and keys in its upper bound.

The function public static object [] ParseShortcut (string text) has an overload.

public static object[] ParseShortcut(string text, string separator)
{
    bool HasAlt = false; bool HasControl = false; bool HasShift = false; bool HasWin = false;

    Modifiers Modifier = Modifiers.None;        //Variables contain modifiers.
    Keys key = 0;           //Register key.

    string[] result;
    string[] separators = new string[] { separator };
    result = text.Split(separators, StringSplitOptions.RemoveEmptyEntries);

    //Iteration finds modifiers by keys.
    foreach (string entry in result)
    {
        //Find the Control Key.
        if (entry.Trim() == Keys.Control.ToString())
        {
            HasControl = true;
        }
        //Find the Alt key.
        if (entry.Trim() == Keys.Alt.ToString())
        {
            HasAlt = true;
        }
        //Find the Shift key.
        if (entry.Trim() == Keys.Shift.ToString())
        {
            HasShift = true;
        }
        //Find the Window key.
        if (entry.Trim() == Keys.LWin.ToString())
        {
            HasWin = true;
        }
    }

    if (HasControl) { Modifier |= Modifiers.Control; }
    if (HasAlt) { Modifier |= Modifiers.Alt; }
    if (HasShift) { Modifier |= Modifiers.Shift; }
    if (HasWin) { Modifier |= Modifiers.Win; }

    KeysConverter keyconverter = new KeysConverter();
    key = (Keys)keyconverter.ConvertFrom(result.GetValue(result.Length - 1));

    return new object[] { Modifier, key };
}

Note that this function uses the KeysConverter class, the TypeConverter enumerated by System.Windows.Forms.Keys, to convert strings to their Key enumeration representation.

Usage:

**In C#**
object[] Result = ParseShortcut("Control + Shift + A", " + ");
Modifiers modifier = (Modifiers)Result[0];                          //Control | Shift
Keys key = (Keys)Result[1];                                        //Keys.A
**In VB**
Dim Result() as Object = ParseShortcut("Control + Shift + A", " + ")
Dim modifier as Modifiers = CType(Result(0), Modifiers)           'Control Xor Shift
Dim key as Keys = CType(Result(0), Keys)                          'Keys.A
And to reverse this process, we use the CombineShortcut function

public static string CombineShortcut(Modifiers mod, Keys key)
{
    string hotkey = "";
    foreach (Modifiers a in new HotKeyShared.ParseModifier((int)mod))
    {
        hotkey += a.ToString() + " + ";
    }

    if (hotkey.Contains(Modifiers.None.ToString())) hotkey = "";
    hotkey += key.ToString();
    return hotkey;
}
**Usage:**

Modifiers modifier = Modifiers.Control | Modifiers.Shift;
CombineShortcut(modifier, Keys.A);                         //Control + Shift + A
Dim modifier as Modifiers = Modifiers.Control Xor Modifiers.Shift
CombineShortcut(modifier, Keys.A)                          'Control + Shift + A

Hot key control
HotKeyControl is a UserControl that extends the TextBox control to capture keys that users press when they are active. It adds the event HotKey IsSet that is triggered when the user sets HotKey, and the attributes UserKey and UserModifer (not visible in the design view), which return keys and modify the user set and the attributes ForceModifiers (visible in the design view), specifying that the user should be forced to enter changes when setting a hotkey, and accept all hotkeys if they are true.

For WinForm

The HotKey control uses the KeyDown and KeyUp events of the text box to get the keys pressed by the user. And use the reset button to clear the input HotKey.

void HotKeyControl_KeyDown(object sender, KeyEventArgs e)
{
    e.SuppressKeyPress = true;  //The forbidden key is handled by the underlying control.
    this.Text = string.Empty;  //Clear the contents of the text box
    KeyisSet = false; //At this point, the user has not specified a shortcut.

    //Make the user specify modifiers. Control, Alt or Shift.
    //If no modifier exists, clear the text box.
    if (e.Modifiers == Keys.None && forcemodifier)
    {
        MessageBox.Show("You have to specify a modifier like 'Control', 'Alt' or 'Shift'");
        this.Text = Keys.None.ToString();
        return;
    }

    //A modifier is present. Process each modifier. There are modifiers. Processing each modifier.
    //The modifier is separated by ",". So we separate them and write each into a text box.
    foreach (string modifier in e.Modifiers.ToString().Split(new Char[] { ',' }))
    {
        if (modifier != Keys.None.ToString())
            this.Text += modifier + " + ";
    }

    //KEYCODE contains the last key pressed by the user.
    //If KEYCODE contains modifiers, the user does not enter a shortcut. Therefore, KeyisSet is false
    //But if not, KeyisSet is true. But if not, KeyisSet is true.
    if (e.KeyCode == Keys.ShiftKey | e.KeyCode == Keys.ControlKey | e.KeyCode == Keys.Menu)
    {
        KeyisSet = false;
    }
    else
    {
        this.Text += e.KeyCode.ToString();
        KeyisSet = true;
    }
}

The KeyUp event determines whether HotKey has been set by checking the variable KeyisSet, if true raises the HotKeyIsSet event or clears the control.

void HotKeyControl_KeyUp(object sender, KeyEventArgs e)
{
    //In KeyUp, if KeyisSet is False, then clear the text box.
    if (KeyisSet == false)
    {
        this.Text = Keys.None.ToString();
    }
    else
    {
        if (HotKeyIsSet != null)
        {
            var ex = new HotKeyIsSetEventArgs(UserKey, UserModifier);
            HotKeyIsSet(this, ex);
            if (ex.Cancel)
            {
                KeyisSet = false;
                this.Text = Keys.None.ToString();
            }
        }
    }
}

For WPF

The source file contains two HotKey controls, one built in. Net Framework 3.0 and the other in. Net Framework 4.0.

HotKey Control uses the Preview KeyDown event and a hook to get the keys pressed by the user.

public HotKeyControl()
{
    this.GotFocus += new RoutedEventHandler(HotKeyControl_GotFocus); //Link up here.
    this.hook = new HwndSourceHook(WndProc); //Get a Windows message.
    this.LostFocus += new RoutedEventHandler(HotKeyControl_LostFocus); //Take off the hook here.
    this.ContextMenu = null; //Disable shortcuts.
    Text = Keys.None.ToString();
    this.IsReadOnly = true;
    this.PreviewKeyDown += new KeyEventHandler(HotKeyControl_PreviewKeyDown);
}
  • HotKey must be registered with HotKey Manager before it can work (triggering events)

HotKeyManager
The HotKeyManager class in WinForm will manage the GlobalHotKey, LocalHotKey, and HodKey classes to implement:

IMessageFilter: Classes will be allowed to receive Windows message callbacks.
IDisposable: All resources will be released and all HotKeys will be cancelled.
This class will receive Windows messages by adding

Application.AddMessageFilter(this);

In the constructor, you will receive Windows messages by adding stop

Application.RemoveMessageFilter(this);

In the destructor.
The class then receives the message by adding a function, which is the result of implementing IMessageFilter.

public bool PreFilterMessage(ref Message m) { }

For WPF, a Hook is added to the class to allow it to receive Windows messages.

I copied the Key enumeration from WinForm to the WPF class to allow Local and HotKeys, because Windows still sends the same key message to WinForm as the WPF application.

this.hook = new HwndSourceHook(WndProc); //Receive Windows News.
this.hwndSource = (HwndSource)HwndSource.FromVisual(window); // new WindowInteropHelper(window).Handle // if necessary InPtr. 
this.hwndSource.AddHook(hook);

The class then receives Windows messages from the function.

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { }

In the source file, HotKey Manager does not have empty navigation, because I want you to provide a form or window, depending on the situation, because a globalHotKey registered form or window handle is required for Windows registration or cancellation. If you want to use the library in your service, or if you don't want to use GlobalHotKeys, you can add an empty constructor and make the necessary changes, and the library will still work.

HotKey Manager automatically processes registered forms or windows when they are closed. You don't need to rebuild them in your application, and if you do, you don't throw exceptions. It can also be disabled, it still receives messages, it just doesn't do anything with them and in WinForm, you can set HotKey Manager to temporarily disable yourself, when another form, it may be a dialog box, because it still raises events, set DisableOnManagerFormInactive to real.

Now, when you press a key in your application, Windows sends a message containing information about the pressed keys and states (whether pressed or published) to our class because we have subscribed to them.

However, in order to press Modifiers when pressing a button, I quoted Microsoft. Visual Basic to speed up the process. You can always get the modifier on the keyboard, as follows:

Microsoft.VisualBasic.Devices.Keyboard UserKeyBoard = new Microsoft.VisualBasic.Devices.Keyboard();
bool AltPressed = UserKeyBoard.AltKeyDown;
bool ControlPressed = UserKeyBoard.CtrlKeyDown;
bool ShiftPressed = UserKeyBoard.ShiftKeyDown;

You can see that the Windows Key is not a modifier but a Key. To get the state of the Windows Key, you can use:

  short ret = Win32.GetKeyState(0x5b); //Get the state of the Window key.
            if ((ret & 0x8000) == 0x8000) LocalModifier |= Modifiers.Window;

When any key is pressed on the keyboard, the HotKey Manager traverses all registered Local HotKeys and ChordHotKeys and raises events for the event it finds.

Find Local HotKeys

Using delegates

Keys keydownCode = (Keys)(int)m.WParam & Keys.KeyCode; //Gets the keys pressed.
 LocalHotKey KeyDownHotkey = LocalHotKeyContainer.Find
 (
     delegate(LocalHotKey d)
     {
      return ((d.Key == keydownCode) && (d.Modifier == LocalModifier)
             && (d.WhenToRaise == RaiseLocalEvent.OnKeyDown));
     }
 );

using Linq,

LocalHotKey KeyDownHotkey = (from items in LocalHotKeyContainer
                where items.Key == keydownCode && items.Modifier == LocalModifier
                where items.WhenToRaise == RaiseLocalEvent.OnKeyDown
                select items).FirstOrDefault();

* When nothing is found, the search returns null.

Find ChordHotKeys
When Local HotKey cannot be found, the Manager checks whether the basic keys and modifiers of any registered HotKey match the keys pressed, which is why you cannot set the same basic keys and modifiers for any other localhotkey. If it finds one, it goes to ChordMode and waits for another key to be discarded modifier. If the second key pressed matches the chordkey and modifier of any chord, it raises the chordkey event, otherwise the leaf ChordMode gives an event that sounds like Visual Studio.

Find Global Hot Keys
To register GlobalHotKeys, there is no built-in functionality that allows this functionality, but it is built-in in Win32 API and. Net to provide a way to call non-local libraries. The methods we are interested in are defined in User32.dll, RegisterHotKey and UnRegisterHotKey.

RegisterHotKey and UnregisterHotKey.

Now, define an important method: static functions that allow us to register shortcuts.

[DllImport("user32", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
private static extern int RegisterHotKey(IntPtr hwnd, int id, int modifiers, int key);

[DllImport("user32", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
private static extern int UnregisterHotKey(IntPtr hwnd, int id);

Please note that we do not provide a methodology. This method is defined in user32.dll. We just add a method to call this method directly for our application.

1.hWnd refers to a form or window handle.
2.id is the unique identifier of the hotkey.
3. The modifier is expressed as an integer of the modifier key (shift/alt/ctrl/win) pressed by your key.
4. The key is the virtual key code of the hotkey.
When you press Global HotKey, Windows assigns its Global HotKey ID to wParam when it is registered, which can then be used to search for all Global HotKey, as we did for Local HotKey.

Other functions
HotKey Manager also supports enumerations that can iterate over a particular hotkey; global, local or chord like this:

string message = "Global HotKeys.\n";

foreach (GlobalHotKey gh in MyHotKeyManager.EnumerateGlobalHotKeys)
{
     message += string.Format("{0}{1}", Environment.NewLine, gh.FullInfo());
}

message += "\n\nLocal HotKeys.\n";

foreach (LocalHotKey lh in MyHotKeyManager.EnumerateLocalHotKeys)
{
     message += string.Format("{0}{1}", Environment.NewLine, lh.FullInfo());
}

message += "\n\nChord HotKeys.\n";

foreach (ChordHotKey ch in MyHotKeyManager.EnumerateChordHotKeys)
{
      message += string.Format("{0}{1}", Environment.NewLine, ch.FullInfo());
}

MessageBox.Show(message, "All HotKeys registered by this app.", MessageBoxButtons.OK, MessageBoxIcon.Information);

This will allow iterations to be easier because we can traverse all ChordHotKeys directly like this

Instead of implementing IEnumerable and iteration like this, you will need to use the namespace System.Collections.Generic

foreach (ChordHotKey ch in (IEnumerable<ChordHotKey>)MyHotKeyManager)
{
      message += string.Format("{0}{1}", Environment.NewLine, gh.FullInfo());
}

HotKey Manager also uses the Win32 API to get all keyboard presses, which can be disabled even if your application is not focused.

KeyboardHookEventHandler keyboardhandler = (sender, handler) =>
        {
            if (handler.Modifier == KeyboardHookEventArgs.modifiers.Shift)
                { handler.Handled = true; }
            switch (handler.Key)
            {
                case Keys.A:
                case Keys.E:
                case Keys.I:
                case Keys.O:
                case Keys.U:
                    handler.Handled = true;
                    return;
            }
        };

The above code disables the vowel keys on the keyboard, but all keys are disabled when the Shift key is pressed.

HotKey Manager, using Win32 API, can also simulate a Key press. Here, we simulate press Control + A.

MyHotKeyManager.SimulateKeyDown(Keys.Control);           //Hold down the control key
MyHotKeyManager.SimulateKeyPress(Keys.A);                //Hold down the A key.
MyHotKeyManager.SimulateKeyUp(Keys.Control);             //Release control key

The HotKey Manager hooks all keyboard messages from the method, HotKeyManager.Dispose, by canceling all registered GlobalHotKeys registered classes.

for (int i = GlobalHotKeyContainer.Count - 1; i >= 0; i--)
{
     RemoveGlobalHotKey(GlobalHotKeyContainer[i]);
}

LocalHotKeyContainer.Clear();
ChordHotKeyContainer.Clear();
KeyBoardUnHook();

Use code
Now, we add HotKeys to the HotKey Manager as follows:

**In C#**
GlobalHotKey ghkNotepad = new GlobalHotKey("ghkNotepad", Keys.N, Modifiers.Control | Modifiers.Shift);
LocalHotKey lhkNewHotkey = new LocalHotKey("lhkNewHotKey", Keys.A);
ChordHotKey chotCmd = new ChordHotKey("chotCmd", Keys.C, Modifiers.Alt, Keys.P, Modifiers.Alt);

MyHotKeyManager.AddGlobalHotKey(ghkNotepad);
MyHotKeyManager.AddLocalHotKey(lhkNewHotkey);
MyHotKeyManager.AddChordHotKey(chotCmd);
**In VB**
Dim ghkNotepad as new GlobalHotKey("ghkNotepad", Keys.N, Modifiers.Control Xor Modifiers.Shift)
Dim lhkNewHotkey as new LocalHotKey("lhkNewHotKey", Keys.A)
Dim chotCmd as new ChordHotKey("chotCmd", Keys.C, Modifiers.Alt, Keys.P, Modifiers.Alt)

MyHotKeyManager.AddGlobalHotKey(ghkNotepad)
MyHotKeyManager.AddLocalHotKey(lhkNewHotKey)
MyHotKeyManager.AddChordHotKey(chotCmd)

For HotKeyControl, users can use controls to set shortcuts, and then get associated keys and modifiers from HotKeyControl.UserKey and HotKeyControl.UserModifier.

HotKey Manager can also simulate keystrokes and add a hook to the keyboard.

You can disable keyboard keys by setting the Hadled property of keys to true. The only combination of keys that seems to be ignored is Control + Alt + Delete.

When you try to create a new HotKey Manager and use WPF in the Initialize Component method, it may cause your window to crash. You'd better do this when the window is loaded.

The library can also be extended to components so that HotKey can be added and validated at design time and run time, which will save you a lot of code.

The example in the source file is shown below.

Sources of translation: https://www.codeproject.com/articles/442285/global-shortcuts-in-winforms-and-wpf

Posted by bonkaz on Sat, 20 Apr 2019 18:27:35 -0700