
In Windows, system-wide hot-keys provide a means to trigger functionality within an application regardless of which program is currently in focus. This allows you to develop applications that run from the system tray, etc. and can be displayed or perform some function (like screen capture) without the user having to click on them first.
It is relatively straightforward to implement hot-keys in your application but you do need to resort to Windows API calls. For example, in WinForms you might declare:
public class MyForm : Form
{
/// <summary>
/// Win32 API identifier for the Hot-Key Windows Message (WM_HOTKEY.)
/// </summary>
private const int HotKeyWindowsMessageId = 0x0312;
[Flags]
private enum KeyModifiers
{
None = 0,
Alt = 1,
Control = 2,
Shift = 4,
Windows = 8
}
/// <summary>Our app-specific hot-key ID</summary>
private const int HotKeyId = 0x0FFD;
/// <summary>
/// Registers a system-wide hot key.
</summary>
/// <param name="hWnd">
/// Handle to the window the hot key is being registered for.
/// (NULL not allowed.)
/// </param>
/// <param name="id">
/// Unique hot key ID. No other hot key in the calling thread should
/// have the same identifier.
/// Applications must be in the range 0x0000 through 0xBFFF.
/// DLLs must be in the range 0xC000 through 0xFFFF.
/// </param>
/// <param name="fsModifiers">
/// The control keys combination used with the virtual-key (vk) to
/// trigger the action.
/// </param>
/// <param name="vk">
/// The virtual-key code of the hot key.
/// </param>
/// <returns>
/// True indicates success. False indicates failure.
/// To get extended error information, call GetLastError().
/// </returns>
[DllImport("user32.dll", SetLastError = true)]
private static extern bool RegisterHotKey(
IntPtr hWnd, int id, ControlKeyModifiers fsModifiers, Keys vk);
public MyForm()
{
InitializeComponent();
RegisterMyAppHotKey();
}
private void RegisterMyAppHotKey()
{
RegisterHotKey(
this.Handle,
HotKeyId,
KeyModifiers.Alt | KeyModifiers.Control | KeyModifiers.Shift,
Keys.P);
}
/// <summary>
/// Processes Windows messages.
/// </summary>
/// <param name="m">The message received.</param>
private override void WndProc(ref Message m)
{
switch (m.Msg)
{
case HotKeyWindowsMessageId:
// Put the code to execute your desired hot-key functionality here.
break;
}
base.WndProc(ref m);
}
}
Code language: C# (cs)
Windows Vista and later introduced User Access Control (UAC) and User Interface Privilege Isolation (UIPI), which made registering the capability just that tiny bit more difficult. The registration itself is actually unchanged, but we then needed to authorise our app to receive specific Windows Message events from the wider system. The RegisterMyAppHotKey() method would then need to be modified, similar to that below (lots of extra definitions for only a couple of lines more in the registration method)…
/// <summary>
/// Contains extended result information obtained by calling the
/// ChangeWindowMessageFilterEx function. NOTE: The cbSize property
/// MUST be initialised to the size of the structure using
/// Marshal.SizeOf(typeof(FilterChangeResult)). Equivalent to the
/// system-defined CHANGEFILTERSTRUCT structure.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct FilterChangeResult
{
/// <summary>
/// The size of the structure, in bytes. Must be set to
/// sizeof(CHANGEFILTERSTRUCT), otherwise the function fails with
/// ERROR_INVALID_PARAMETER.
</summary>
public uint cbSize;
/// <summary>
/// If the function succeeds, this field contains the filtering status.
/// </summary>
public MessageFilterStatus extStatus;
}
/// <summary>
/// Windows message filtering action.
/// </summary>
public enum FilteringAction : uint
{
/// <summary>MSGFLT_RESET</summary>
Reset = 0,
/// <summary>MSGFLT_ALLOW</summary>
Allow = 1,
/// <summary>MSGFLT_DISALLOW</summary>
DisAllow = 2
}
/// <summary>
/// Windows message UIPI change of status outcome.
/// </summary>
public enum FilterStatus : uint
{
/// <summary>MSGFLTINFO_NONE</summary>
None = 0,
/// <summary>MSGFLTINFO_ALREADYALLOWED_FORWND</summary>
AlreadyAllowed = 1,
/// <summary>MSGFLTINFO_ALREADYDISALLOWED_FORWND</summary>
AlreadyDisAllowed = 2,
/// <summary>MSGFLTINFO_ALLOWED_HIGHER</summary>
AllowedHigher = 3
}
/// <summary>
/// Loads the specified library into the address space of the calling process.
/// </summary>
/// <param name="lpFileName">
/// The name of the library to load.
/// </param>
/// <returns>
/// If the function succeeds, the return value is a handle to the library.
/// If the function fails, the return value is NULL.
/// To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LoadLibrary(string lpFileName);
/// <summary>
/// Retrieves the address of an exported function or variable from a
/// dynamic-link library (DLL).
/// </summary>
/// <param name="hModule">
/// A handle to the DLL.
/// </param>
/// <param name="lpProcName">
/// The function or variable name to retrieve the process address for.
/// </param>
/// <returns>
/// If the function succeeds, the return value is the address of the
/// exported function or variable.
/// If the function fails, the return value is NULL.
/// To get extended error information, call GetLastError.
</returns>
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
/// <summary>
/// Frees a loaded dynamic-link library and associated resources.
/// </summary>
/// <param name="hModule">
/// A handle to the loaded library.
/// </param>
/// <returns>
/// If the function succeeds, the return value is nonzero.
/// If the function fails, the return value is zero.
/// To get extended error information, call the GetLastError function.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool FreeLibrary(IntPtr hModule);
/// <summary>
/// NOTE: This Windows API method needs to be defined as a delegate
/// and late bound at runtime so that both older operating systems
/// and Windows Vista and later are supported.
/// </summary>
private delegate bool ChangeWindowMessageFilterEx(
IntPtr hWnd, [MarshalAs(UnmanagedType.U4)] uint message,
[MarshalAs(UnmanagedType.U4)] FilteringAction action,
ref MessageFilterChangeResult pChangeFilterStruct);
private bool ChangeWindowMessageFilter(
IntPtr handle, uint message,
FilteringAction action,
ref FilterChangeResult result)
{
IntPtr library = IntPtr.Zero;
IntPtr method = IntPtr.Zero;
try
{
library = LoadLibrary("user32.dll");
if (library != IntPtr.Zero)
{
method = GetProcAddress(library, "ChangeWindowMessageFilterEx");
if (method == IntPtr.Zero)
{
return true;
}
return LoadMethod<ChangeWindowMessageFilterEx>(
handle, message, action, ref result);
}
}
catch (Exception)
{
// Suppress or handle/log errors as you deem fit!
}
finally
{
if (library != IntPtr.Zero) FreeLibrary(library);
}
}
private void RegisterMyAppHotKey()
{
RegisterHotKey(
this.Handle,
HotKeyId,
KeyModifiers.Alt | KeyModifiers.Control | KeyModifiers.Shift,
Keys.P);
FilterChangeResult result = new FilterChangeResult();
ChangeWindowMessageFilter(
this.Handle,
HotKeyWindowsMessageId,
FilteringAction.Allow,
ref result);
}
Code language: C# (cs)
All that extra code, just to get around UAC and UIPI. You should now have an application that works with both Windows XP and Windows Vista/7/etc.
You can register multiple hot-keys within a single application if you want. Just create separate hot-key identifiers (within the ranges specified in the RegisterHotKey prototype above) and then call the RegisterHotKey() on each of them. You only need to call the ChangeWindowMessageFilter() method the once – it applies to the application, not to an individual hot key.