Usage of system hotkeys and window messages in C#


This article will show how to register a system hotkey for a currently running application and how to handle window messages for altering controls functionality or adding new functions to them.

So far C# has no native lib functions to do that, so Platform Invoke will be required to use some of WinAPI functions from user32.dll and kernel32.dll. 

Registering a system wide hotkey enables application to respond to that hotkey even if application has no focus or is minimalized. This can be console window or hidden form, important is only to be able to obtain a handle to it. 

Window messages are events passed by the system to the running applications. Every class derived from Control class have a WndProc() function which give an ability to process messages (this is handled automatically but we can override it). 

First of all we have to port win32 functions to C# managed code with Platform Invoke using DllImport attribute. 

public static class Win32
{
    [DllImport("kernel32.dll")]
    public static extern int GlobalAddAtom(
        string lpString
    );
    [DllImport("kernel32.dll")]
    public static extern int GlobalDeleteAtom(
        int nAtom
    );
    [DllImport("User32")]
    public static extern IntPtr SetForegroundWindow(
        IntPtr hwnd
    );
    [DllImport("User32")]
    public static extern bool RegisterHotKey(
        IntPtr hWnd,
        int id,
        int fsModifiers,
        int vk
    );
    [DllImport("User32")]
    public static extern bool UnregisterHotKey(
        IntPtr hWnd,
        int id
    );
    [DllImport("User32")]
    public static extern IntPtr GetForegroundWindow();
    [DllImport("user32")]
    public static extern IntPtr FindWindow(
        string lpClassName,
        string lpWindowName
    );
    [DllImport("user32")]
    public static extern int SendMessage(
        IntPtr hWnd,
        uint Msg,
        int wParam,
        int lParam
    );
    public const int MOD_SHIFT = 0x4;
    public const int MOD_CONTROL = 0x2;
    public const int MOD_ALT = 0x1;
    public const int MOD_WIN = 0x8;
    public const int WM_HOTKEY = 0x312;
    public const int WM_MOUSEWHEEL = 0x020A;
    public const int WM_PASTE = 0x0302;
}

To register a hotkey use RegisterHotKey() function. It has 4 parameters: a handle to the window that will receive WM_HOTKEY messages, the identifier, key modifiers and virtual-key code of the hotkey. An application must specify an id value in the range 0x0000 through 0xBFFF. A shared DLL must specify a value in the range 0xC000 through 0xFFFF. It is generally good to use GlobalAddAtom() function to generate unique id from the correct range. 

private List<int> lHKids; // list of unique ids for newly registered hotkeys
this_window_handle = this.Handle;
//this_window_handle = Win32.FindWindow(null, this.Text); // another way
// shift-ctrl-q hotkey
int uid = Win32.GlobalAddAtom((lHKids.Count + 1).ToString());
Win32.RegisterHotKey(
    this_window_handle,
    uid,
    Win32.MOD_SHIFT | Win32.MOD_CONTROL,
    Convert.ToInt16(Keys.Q)
);    
lHKids.Add(uid);
// q hotkey
uid = Win32.GlobalAddAtom((lHKids.Count + 1).ToString());
Win32.RegisterHotKey(
    this_window_handle,
    uid,
    0,
    Convert.ToInt16(Keys.Q)
);   
lHKids.Add(uid);
// ctrl-v hotkey
uid = Win32.GlobalAddAtom((lHKids.Count + 1).ToString());
Win32.RegisterHotKey(
    this_window_handle,
    uid,
    Win32.MOD_CONTROL,
    Convert.ToInt16(Keys.V)
);
lHKids.Add(uid);

To unregister hotkey call UnregisterHotKey() function: 

Win32.UnregisterHotKey(this_window_handle, lHKids[i]);
Win32.GlobalDeleteAtom(lHKids[i]); 

When hotkey is defined and key is pressed the system posts the WM_HOTKEY message to the message queue of the window with which the hotkey is associated. Override WndProc() to handle it. 

protected override void WndProc(ref Message m)
{
    try
    {
        // handle WM_HOTKEY message and hotkeys
        if ((lHKids != null) && (lHKids.Count > 0) && (m.Msg == Win32.WM_HOTKEY))
        {
            // WParam for WM_HOTKEY message holds id passed to
            // RegisterHotKey() function
            int idx = lHKids.IndexOf((int)m.WParam);
            if (idx >= 0 && (idx != 2)) // without ctrl-v hotkey
            {
                Win32.SetForegroundWindow(this_window_handle);
                MessageBox.Show(string.Format("HotKey {0} pressed", idx), "info");
            }
        }
        // respond to WM_MOUSEWHEEL message
        else if (m.Msg == Win32.WM_MOUSEWHEEL)
        {
            // use the correct control handle
            // in this case textbox instead of main form window
            Win32.SendMessage(exTextBox1.Handle, Win32.WM_PASTE, 0, 0);
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "WndProc() exception");
    }
    base.WndProc(ref m);
}

The example app code is registering 3 hotkeys: ctrl-shift-q, q itself and ctrl-v to show different calls of RegisterHotKey() function and some possibilities of what we can do with them. I.e. we can block ctrl-v paste capability (just this specific keys combination). 

The second message filtered in this example is WM_MOUSEWHEEL. Here it is used to send WM_PASTE message (so we cannot use ctrl-v but mouse wheel scroll will work) to the modified textbox control. Every control has its own handle so it is important to use the right one when we want to send messages to it.

Controls (and window forms) are receiving only messages that have a meaning for them that is why in this example WM_PASTE must be filtered in TextBox's WndProc() instead of main form's one: 

class ExTextBox : TextBox
{
    protected override void WndProc(ref Message m)
    {
        // Window messages filtering
        if (m.Msg == Win32.WM_PASTE)
        {
            string s = "WM_PASTE message received by TextBox control";
            if (!Clipboard.ContainsText()) s += "\n- clipboard data is not a text";
            MessageBox.Show(s, "info");
            // Uncomment the "return" line to block paste receiving capability
            // for this control regardless of its source.
            //return;
        }
        base.WndProc(ref m);
    }
} 

We can use messages to restrict some behavior of the control or implement new one like checking checkbox with mouse scroll and so on but unlike global hotkeys, this modification will work only when window has a focus. 

See complete example attached. 

Some useful links: 

Similar Articles