Using global shortcuts in WinForms projects

The user can trigger the program in any place with the help of the global shortcut key. However, the WinForms framework does not provide the function of global shortcut keys. To implement global shortcuts, you need to deal with the windows API. This article tells you how to use Windows API to use global shortcuts.

Understand the message loop mechanism

Brief introduction of message mechanism

How does a form work? How does it respond to user actions? Let's understand the operation mechanism of a program first.

On Windows, a desktop application is driven by Message mechanism. The Message carries information about what happened to the corresponding form. For example, the user presses a key, moves or clicks the mouse, and so on.

So what is the workflow?

  1. First, the system will create a message when the user makes some operations or other things happen. Then, send the message to the thread message queue of the current corresponding form. Wait for the application to process the message. The message will carry a handle to the form, a message number, and some additional information. This information can tell the application what happened.

  2. After the application completes initialization, it starts to establish the message processing mechanism. Get messages from the message queue by continuously looping. For those messages that have a corresponding target form, forward the message to the form handler of the corresponding form.

  3. The form handler is responsible for processing messages.

In Win Forms, the dispatch mechanism of messages

In Win Forms, application The run method implements the message processing mechanism. Let's take a look at program The following code in CS. This code is to create a form, and then pass the form into application Run method. And application The run method first displays this form, and then starts to cycle to get messages from the message queue and send messages.

[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());
}

  Application. The run method is described as follows:

Starts running a standard application message loop on the current thread and makes the specified form visible.

So, can you visually see which messages are put in our message queue? By viewing the documentation of the Application class, we found the following methods:

public static void AddMessageFilter (System.Windows.Forms.IMessageFilter value);

Add a message filter to monitor Windows messages as they are delivered to the target.

Obviously, to view the message, we need to implement a class of IMessageFilter interface. Let's write such a class as follows:

internal class MyMessageFilter : IMessageFilter
{
    public bool PreFilterMessage(ref Message m)
    {
        Console.WriteLine("MyMessageFilter: {0}", m.ToString());
        return false;
    }
}

The code is very easy to understand. However, it is worth mentioning that the meaning of returning false is to allow the message to continue to pass down. If true is returned, the message will not continue to pass down.

Next, we register the message processor in the Application.

Write the following code under the Main method:

Application.AddMessageFilter(new MyMessageFilter()); ;
Application.Run(new Form1());

The first line is our newly added code. Then, in order to appear the console window, we should choose the target platform of the program as the Windows console program. Finally, start executing the application. You should be able to see the information output in the console.

Exploring the message processing function of form

Through the message distribution mechanism established by Application, the message will be sent to the next station, that is, the message processing function of the form. In Win Forms, we can spy on the contents of these messages by rewriting the message processing function. Please see the following code:

internal class Form1 : Form
{
    protected override void WndProc(ref Message m)
    {
        Console.WriteLine("Form1 WndProc: {0}", m.ToString());
        base.WndProc(ref m);
    }
}

Summary of message mechanism

Through the above code, you should have an intuitive description of the message mechanism. Then, let's talk about our protagonist today - hotkey. When the hotkey is triggered, it is also informed to the application through the message mechanism, so of course we have to deal with the hotkey message. I believe you can write the corresponding code now.

Import related API s

The API documents for registering global hotkeys and revoking global hotkeys are as follows, which you can refer to.

RegisterHotKey

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey

UnregisterHotKey

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterhotkey

In order to introduce these two functions into our program, we need to define an enumeration class. As follows:

/// <summary>
///Enumeration that provides modifier options for hotkeys.
/// </summary>
[Flags]
public enum KeyModifiers
{
    /// <summary>
    ///No modifier keys.
    /// </summary>
    None = 0X00,

    /// <summary>
    ///ALT key.
    /// </summary>
    Alt = 0X01,

    /// <summary>
    ///CTRL key.
    /// </summary>
    Control = 0X02,

    /// <summary>
    ///SHIFT key.
    /// </summary>
    Shift = 0X04,

    /// <summary>
    ///Windows logo key.
    /// </summary>
    Windows = 0X08,

    /// <summary>
    ///It is forbidden to send messages repeatedly when the hotkey is pressed.
    /// </summary>
    NoRepeat = 0X4000
}

Then we introduce two API functions and a constant. As follows:

/// <summary>
///Import and define static classes about global hotkey functions and constants in Windows SDK.
/// </summary>
internal static class NativeMethods
{
    /// <summary>
    ///Define the message number of the message triggered by the registered hotkey using < see CREF = "registerhotkey (IntPtr, int, keymodifiers, virtualkeys)" / >.
    /// </summary>
    public const int WM_HOTKEY = 0X0312;

    /// <summary>
    ///Register the system global hotkey.
    /// </summary>
    ///< param name = "hWnd" > associated window handle. If this value is zero, it is associated with the current county seat, WM_HOTKEY messages will be put into the message queue of the current county</ param>
    ///< param name = "Id" > the identifier used to identify the hotkey</ param>
    ///< param name = "fsmodifiers" > values of modifier keys and options</ param>
    ///< param name = "VK" > virtual key code</ param>
    ///< returns > returns true for success and false for failure. For error information, call the < see CREF = "marshal. Getlastwin32error" / > method</ returns>
    /// <seealso cref="UnregisterHotKey(IntPtr, int)"/>
    /// <remarks>
    ///When the key is pressed, the system will look for the matched registered global hotkey. If the global hotkey is associated with a form, the < see CREF = "wm_hotkey" / > message will be placed in the message queue of the form. If it is not associated with a form, the < see CREF = "wm_hotkey" / > message will be sent to the corresponding thread message queue.
    ///This function cannot associate a global hotkey with a form created by another thread.
    ///If the global hotkey to be registered is already registered, calling this function will fail.
    ///If the registered global hotkey has the same form handle (hWnd) and identifier (id) as the global hotkey to be registered, the newly registered global hotkey is maintained with the old global hotkey. If the global hotkey needs to be replaced by a new global hotkey, you should first call the < see CREF = "unregisterhotkey (IntPtr, int)" / > function to revoke the registered global hotkey, and then call the function to register a new global hotkey.
    ///On Windows Server 2003: when the new global hotkey has the same form handle (hWnd) and identifier (id) as the registered global hotkey, the old global hotkey will be replaced by the new global hotkey.
    ///F12 should be reserved for debugger use.
    ///The application must specify a value between 0x0000 and 0xBFFF, and the shared class library must specify a value between 0xC000 and 0xFFFF to the id parameter.
    /// </remarks>
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern bool RegisterHotKey(IntPtr hWnd, int id, KeyModifiers fsModifiers, Keys vk);

    /// <summary>
    ///Undo the registered system global hotkey.
    /// </summary>
    ///< param name = "hWnd" > associated window handle. Must be zero if not associated with any window</ param>
    ///< param name = "Id" > identifier of the hotkey to be revoked</ param>
    ///< returns > returns true for success and false for failure. For error information, call the < see CREF = "marshal. Getlastwin32error" / > method</ returns>
    /// <seealso cref="RegisterHotKey(IntPtr, int, KeyModifiers, VirtualKeys)"/>
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern bool UnregisterHotKey(IntPtr hWnd, int id);
}

Above, we have prepared the definition code of relevant types and platform calls.

Process of using hotkeys

The process of using hotkeys is as follows:

  1. Register the required hotkeys when necessary.

  2. Release the registered hotkey when necessary.

  3. Handle the hotkey message.

Hotkey instance associated to the form

Register hotkeys

Next, we demonstrate the workflow of the hotkey associated with the form by registering a Ctrl + Shift + H hotkey. First, the way to distinguish different hotkeys is to specify different id identifiers. We first define a constant to specify the identifier of our hotkey:

/// <summary>
///Defines the identifier of the hotkey used to change the display state of the form.
/// </summary>
const int ChangeVisibleHotKeyId = 1;

Then we write the following code under the Load event of the form to register the hotkeys we need.

private void Form1_Load(object sender, EventArgs e)
{
    NativeMethods.RegisterHotKey(this.Handle, ChangeVisibleHotKeyId, KeyModifiers.Control | KeyModifiers.Shift, Keys.H);
}

Processing hotkeys

In order to make the hotkey realize the corresponding function. We should rewrite the form's handler and turn WM_HOTKEY messages are taken out and dispatched to another method to realize specific functions. The code is as follows:

    protected override void WndProc(ref Message m)
    {
        Console.WriteLine("Form1 WndProc: {0}", m.ToString());

        // Process the message according to the message id.
        switch (m.Msg)
        {
            case NativeMethods.WM_HOTKEY:
                // We take out the id of the hotkey and call the method to process the hotkey.
                this.ProcessHotKeyMessage(m.WParam.ToInt32());
                break;
            default:
                base.WndProc(ref m);
                break;
        }
    }

    /// <summary>
    ///Process hotkey messages. Here we implement the function corresponding to the hotkey.
    /// </summary>
    ///< param name = "hotkeyid" > identifier of the hotkey</ param>
    private void ProcessHotKeyMessage(int hotKeyId)
    {
        // Different hotkeys are distinguished according to different IDs.
        switch (hotKeyId)
        {
            case ChangeVisibleHotKeyId:
                this.Visible = !this.Visible;
                break;
        }
}

Undo hotkey

Finally, we cancel our registered hotkey when the form is destroyed. The code is as follows:

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
    NativeMethods.UnregisterHotKey(this.Handle, ChangeVisibleHotKeyId);
}

Above, we have completed our hotkey registration. You can execute the program to try whether it works normally.

further more

This article only shows the processing flow of hotkeys associated with forms. In another case, our program doesn't need a form, so obviously we don't need to create a form. So what should I do with this hotkey? Yes, you can process hotkey messages in messagefilter.

Complete code

The following is the complete code of this program:

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;


namespace HotKeyApp
{
    internal class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.AddMessageFilter(new MyMessageFilter()); ;
            Application.Run(new Form1());
        }
    }

    internal class Form1 : Form
    {
        /// <summary>
        ///Defines the identifier of the hotkey used to change the display state of the form.
        /// </summary>
        const int ChangeVisibleHotKeyId = 1;

        public Form1()
        {
            this.Load += Form1_Load;
            this.FormClosed += Form1_FormClosed;
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            NativeMethods.UnregisterHotKey(this.Handle, ChangeVisibleHotKeyId);
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            NativeMethods.RegisterHotKey(this.Handle, ChangeVisibleHotKeyId, KeyModifiers.Control | KeyModifiers.Shift, Keys.H);
        }

        protected override void WndProc(ref Message m)
        {
            Console.WriteLine("Form1 WndProc: {0}", m.ToString());

            // Process the message according to the message id.
            switch (m.Msg)
            {
                case NativeMethods.WM_HOTKEY:
                    // We take out the id of the hotkey and call the method to process the hotkey.
                    this.ProcessHotKeyMessage(m.WParam.ToInt32());
                    break;
                default:
                    base.WndProc(ref m);
                    break;
            }
        }

        /// <summary>
        ///Process hotkey messages. The function of our hotkey is realized here.
        /// </summary>
        ///< param name = "hotkeyid" > identifier of the hotkey</ param>
        private void ProcessHotKeyMessage(int hotKeyId)
        {
            // Different hotkeys are distinguished according to different IDs.
            switch (hotKeyId)
            {
                case ChangeVisibleHotKeyId:
                    this.Visible = !this.Visible;
                    break;
            }
        }

    }

    internal class MyMessageFilter : IMessageFilter
    {
        public bool PreFilterMessage(ref Message m)
        {
            Console.WriteLine("MyMessageFilter: {0}", m.ToString());
            return false;
        }
    }

    /// <summary>
    ///Enumeration that provides modifier options for hotkeys.
    /// </summary>
    [Flags]
    public enum KeyModifiers
    {
        /// <summary>
        ///No modifier keys.
        /// </summary>
        None = 0X00,

        /// <summary>
        ///ALT key.
        /// </summary>
        Alt = 0X01,

        /// <summary>
        ///CTRL key.
        /// </summary>
        Control = 0X02,

        /// <summary>
        ///SHIFT key.
        /// </summary>
        Shift = 0X04,

        /// <summary>
        ///Windows logo key.
        /// </summary>
        Windows = 0X08,

        /// <summary>
        ///It is forbidden to send messages repeatedly when the hotkey is pressed.
        /// </summary>
        NoRepeat = 0X4000
    }

    /// <summary>
    ///Import and define static classes about global hotkey functions and constants in Windows SDK.
    /// </summary>
    internal static class NativeMethods
    {
        /// <summary>
        ///Define the message number of the message triggered by the registered hotkey using < see CREF = "registerhotkey (IntPtr, int, keymodifiers, virtualkeys)" / >.
        /// </summary>
        public const int WM_HOTKEY = 0X0312;

        /// <summary>
        ///Register the system global hotkey.
        /// </summary>
        ///< param name = "hWnd" > associated window handle. If this value is zero, it is associated with the current county seat, WM_HOTKEY messages will be put into the message queue of the current county</ param>
        ///< param name = "Id" > the identifier used to identify the hotkey</ param>
        ///< param name = "fsmodifiers" > values of modifier keys and options</ param>
        ///< param name = "VK" > virtual key code</ param>
        ///< returns > returns true for success and false for failure. For error information, call the < see CREF = "marshal. Getlastwin32error" / > method</ returns>
        /// <seealso cref="UnregisterHotKey(IntPtr, int)"/>
        /// <remarks>
        ///When the key is pressed, the system will look for the matched registered global hotkey. If the global hotkey is associated with a form, the < see CREF = "wm_hotkey" / > message will be placed in the message queue of the form. If it is not associated with a form, the < see CREF = "wm_hotkey" / > message will be sent to the corresponding thread message queue.
        ///This function cannot associate a global hotkey with a form created by another thread.
        ///If the global hotkey to be registered is already registered, calling this function will fail.
        ///If the registered global hotkey has the same form handle (hWnd) and identifier (id) as the global hotkey to be registered, the newly registered global hotkey is maintained with the old global hotkey. If the global hotkey needs to be replaced by a new global hotkey, you should first call the < see CREF = "unregisterhotkey (IntPtr, int)" / > function to revoke the registered global hotkey, and then call the function to register a new global hotkey.
        ///On Windows Server 2003: when the new global hotkey has the same form handle (hWnd) and identifier (id) as the registered global hotkey, the old global hotkey will be replaced by the new global hotkey.
        ///F12 should be reserved for debugger use.
        ///The application must specify a value between 0x0000 and 0xBFFF, and the shared class library must specify a value between 0xC000 and 0xFFFF to the id parameter.
        /// </remarks>
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern bool RegisterHotKey(IntPtr hWnd, int id, KeyModifiers fsModifiers, Keys vk);

        /// <summary>
        ///Undo the registered system global hotkey.
        /// </summary>
        ///< param name = "hWnd" > associated window handle. Must be zero if not associated with any window</ param>
        ///< param name = "Id" > identifier of the hotkey to be revoked</ param>
        ///< returns > returns true for success and false for failure. For error information, call the < see CREF = "marshal. Getlastwin32error" / > method</ returns>
        /// <seealso cref="RegisterHotKey(IntPtr, int, KeyModifiers, VirtualKeys)"/>
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern bool UnregisterHotKey(IntPtr hWnd, int id);
    }
}

last

Finally, I hope this article is of some help to you.

reference material

Window messages (getting started with Win32 and c +) - Win32 apps | Microsoft Docs
https://docs.microsoft.com/zh-cn/windows/win32/learnwin32/window-messages

RegisterHotKey function (winuser.h) - Win32 apps | Microsoft Docs
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey

UnregisterHotKey function (winuser.h) - Win32 apps | Microsoft Docs
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterhotkey

WM_HOTKEY message (Winuser.h) - Win32 apps | Microsoft Docs
https://docs.microsoft.com/zh-cn/windows/win32/inputdev/wm-hotkey

Application class (System.Windows.Forms) | Microsoft Docs
https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.forms.application?view=netframework-4.8

Added by Mortier on Sun, 06 Mar 2022 13:13:52 +0200