Global hotkey registration in UWP

screenshot_octo

As promised, I am starting now to share a number of concrete scenario samples where UWP apps can light up common desktop features using desktop extensions. In this post my UWP app will register for system-wide hotkeys and handle them – even when the app is suspended.

TL;DR;

architecture

Just show me the code on github …
https://github.com/StefanWickDev/ExtensionGallery/tree/master/GlobalHotkey

Let me run the demo app from the Microsoft Store …
https://www.microsoft.com/store/apps/9NJW3LBC2Z3Q

 

Implementation

If you have followed my tutorial for desktop extensions you will find this one pretty straight forward, here are the detailed steps to implement this feature from scratch:

1. Project Setup

We are using the same project setup as explained in part 1 of the tutorial, with a UWP project, a C# console project and a Packaging project to tie it all together:

solution

Since we don’t want the console project to actually pop up a console window (or any window for that matter), we need to switch the output type from Console to Windows Application (I know, not very intuitive …):

outputtype

2. Declaring & Launching the Desktop Extension

Now we can declare the Desktop Extension in the Package.appxmanifest file. Be sure to declare this in the Packaging project, not the UWP project! While we are at it, let’s also declare the AppService. We will need it later to communicate the WM_HOTKEY event back to the UWP process.

<Extensions>
 <desktop:Extension Category="windows.fullTrustProcess"
 Executable="HotkeyWindow\HotkeyWindow.exe" />
 <uap:Extension Category="windows.appService">
 <uap:AppService Name="HotkeyConnection" />
 </uap:Extension>
</Extensions>

Now with this in place, we can launch the Desktop Extension from our UWP app. Before we launch it, we will store our ProcessId in the local settings, so the extension can pick it up and track the lifetime of the UWP. This will help us to clean up and shutdown when the UWP process goes away.

// MainPage.xaml.cs
if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0))
{
 Process process = Process.GetCurrentProcess();
 ApplicationData.Current.LocalSettings.Values["processID"] = process.Id;

 await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
}

When the extension launches, it will get to work right away and register the hot keys for our application. Before it can do so though, it needs to create a (headless) window in order to handle the incoming WM_HOTKEY messages, whenever the hot keys get pressed:

// HotkeyWindow.cs
public HotkeyAppContext()
{
 int processId = (int)ApplicationData.Current.LocalSettings.Values["processId"];
 process = Process.GetProcessById(processId);
 process.EnableRaisingEvents = true;
 process.Exited += HotkeyAppContext_Exited;
 hotkeyWindow = new HotKeyWindow();
 hotkeyWindow.HotkeyPressed += new HotKeyWindow.HotkeyDelegate(hotkeys_HotkeyPressed);
 hotkeyWindow.RegisterCombo(1001, Modifiers.Alt, Keys.S); // Alt-S = stingray
 hotkeyWindow.RegisterCombo(1002, Modifiers.Alt, Keys.O); // Alt-O = octopus
}

 

3. Communicating the WM_HOTKEY event

Once we get a HotkeyPressed event (i.e. when we process WM_HOTKEY in our headless window), we create an AppServiceConnection back to the UWP process and send the ID of the hotkey:

// HotkeyWindow.cs
private async void hotkeys_HotkeyPressed(int ID)
{
 // send the key ID to the UWP
 ValueSet hotkeyPressed = new ValueSet();
 hotkeyPressed.Add("ID", ID);

 AppServiceConnection connection = new AppServiceConnection();
 connection.PackageFamilyName = Package.Current.Id.FamilyName;
 connection.AppServiceName = "HotkeyConnection";
 AppServiceConnectionStatus status = await connection.OpenAsync();
 connection.ServiceClosed += Connection_ServiceClosed;
 AppServiceResponse response = await connection.SendMessageAsync(hotkeyPressed);
}

Now on the UWP side we handle this request and kick-off off the respective animal animation:

// MainPage.xaml.cs
private async void AppServiceConnection_RequestReceived(
                     AppServiceConnection sender,
                     AppServiceRequestReceivedEventArgs args)
{
 AppServiceDeferral messageDeferral = args.GetDeferral();
 int id = (int)args.Request.Message["ID"];
 switch(id)
 {
  case 1001://stingray
   await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
   {
    stingrayMove.Begin();
   });
   break;
   ...

 

4. NativeWindow Implementation

The underlying Win32 magic is implemented in the HotkeyWindow class, derived from NativeWindow:

// HotkeyWindow.cs
public class HotKeyWindow : NativeWindow
{
 private const int WM_HOTKEY = 0x0312;
 private const int WM_DESTROY = 0x0002;

 [DllImport("user32.dll")]
 public static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vlc);
 [DllImport("user32.dll")]
 public static extern bool UnregisterHotKey(IntPtr hWnd, int id);

 private List<Int32> IDs = new List<int>();
 public delegate void HotkeyDelegate(int ID);
 public event HotkeyDelegate HotkeyPressed;

 // creates a headless Window to register for and handle WM_HOTKEY
 public HotKeyWindow()
 {
  this.CreateHandle(new CreateParams());
  Application.ApplicationExit += new EventHandler(Application_ApplicationExit);
 }

 public void RegisterCombo(Int32 ID, Modifiers fsModifiers, Keys vlc)
 {
  if (RegisterHotKey(this.Handle, ID, (int)fsModifiers, (int)vlc))
  {
   IDs.Add(ID);
  }
 }

 private void Application_ApplicationExit(object sender, EventArgs e)
 {
  this.DestroyHandle();
 }

 protected override void WndProc(ref Message m)
 {
  switch (m.Msg)
  {
   case WM_HOTKEY: //raise the HotkeyPressed event
    HotkeyPressed?.Invoke(m.WParam.ToInt32());
    break;

   case WM_DESTROY: //unregister all hot keys
    foreach (int ID in IDs)
    {
     UnregisterHotKey(this.Handle, ID);
    }
    break;
   }
   base.WndProc(ref m);
  }
 }

5. Lifetime Considerations

There are a couple of interesting app lifetime/lifecycle considerations and options in this sample that I want to touch on:

a. Bringing the UWP back to the foreground

When the hotkey is processed in the sample, the UWP is brought to the foreground, so you can enjoy the animation. This is optional and for other scenarios you may chose to leave it in the background. At any rate, we accomplish this by calling AppListEntry.LaunchAsync() on ourselves. This API when called on a running app has the desired effect to bring the app into the foreground, even when the UWP process was suspended.

b. Lifecycle of the Desktop Extension

In this sample the extension’s lifecycle is coupled with the UWP app’s lifecycle. This makes sense here since we want the hotkeys to only be registered while the UWP is running (or suspended), not beyond that. That’s why we are tracking the UWP process and will unregister the hotkeys and shutdown the extension when the UWP goes away.

However, the extension lifetime is in your control and for a different scenario you may choose to keep the extension process alive even after the UWP process goes away.

c. Lifetime of the AppServiceConnection

On every WM_HOTKEY message we open an AppServiceConnection to the UWP, which we will then close once it has been processed. We could also choose to keep the connection around for the lifetime of the process, but this would have the unwanted effect that the UWP would never suspend when minimized and therefore use more system resources than it should.

 

Credits

C# Code for the headless window to handle WM_HOTKEY inspired my this Stackoverflow answer: https://stackoverflow.com/questions/17133656/global-hotkeys-in-windowless-net-app/17136050

Tile icon for the Store sample provided by https://creativeninjas.com/ 

19 thoughts on “Global hotkey registration in UWP

  1. Thanks for sample showing how I can setup to hook and process alt/ctrl/shift/win keyboard shortcut combinations happening when any app has focus in my uwp app.

    I downloaded sample from GitHub and built on my w10 rs5 17738.100 + vs17 15.8.1 environment. I set Package project as startup project and was able to land on debug breakpoint in GlobalHotKey | MainPage.xaml.cs | OnNavigated method but was never able to land on breakpoint in AppServiceConnection_RequestReceived handler that is supposed to fire with alt+o and alt+s keyboard shortcut combinations are entered.

    This may be due to never seeing processing stop on breakpoint I setup in AppServiceConnected method that enables the e.AppServiceConnection.RequestReceived event handler.

    q1. Any insights as to why that is not happening and what I need to do in order to reproduce the working result when I use your store app install?

    q2. Once I have q1 address would you expect HotkeyWindow.cs | HotkeyAppContext registration of hotkey / global keyboard shortcut combinations to cover scenarios where I want to enable hooking something like win + shift + left/rightarrow? If so do you accomplish that by just or’ng the modifier argument setting, e.g. Modifiers.Windows | Modifiers.Shift?

    q3. Any thoughts on whether this RegisterHotKey/UnRegisterHotKey based approach is superior and/or preferred approach to details I found in the sample at https://www.codeproject.com/Articles/19004/A-Simple-C-Global-Low-Level-Keyboard-Hook coupled with https://stackoverflow.com/questions/746188/c-how-to-capture-global-mouse-hid-events that provided insight on how to trap alt/ctrl/shift/win + global keyboard shortcuts combinations?

    Like

    1. For Q1: I cannot reproduce your problem. For me the breakpoints are being hit as expected. My system is on 17751, my VS2017 is 15.9. Maybe there was a bug in 17738. Can you try on a newer build?

      For Q2: Yes, you can use left/right arrow keys as hotkeys with modifiers. Some combinations may be prohibited/reserved by the system though. Please refer to the RegisterHotkey Win32 API. You can do whatever that API supports.

      for Q3: unless you have a specific reason to create a low-level keyboard hook (which has the potential to destabilize the users system) I would go with the RegisterHotkey API.

      Liked by 1 person

      1. Wrt q3 using low-level global keyboard / hotkey hook does seem to after hours of runtime does appear to eventually correlate with some unexpected system response to keyboard input behavior. This would seem to be a difficult to debug and confirm issue short of creating verbose logger output and parsing that looking for clues for confirmation and why.

        Flipped implementation to recommended RegisterHotKey win32 api approach you provided sample on how to use within c# uwp app context, moving from win+ to alt- keyboard shortcut setting, and so far no unexpected system responses to keyboard input from having that running in background for a while.

        Like

    2. wrt q1. status update, I did a solution | clean followed by a build and debug at which point the alt-o and alt-s global hot key events were firing and being handled. Note that I did have to change the Package project from an “Any CPU” target setting it was configured as in download to “x86” target for build to be allowed based on GlobalHotkey project having an “x86” target.

      wrt q2. I tried to hookup win + c global hotkey shortcut in this app using the following line [ hotkeyWindow.RegisterCombo(1003, Modifiers.Windows, Keys.C); ] in HotkeyWindows.cs | HotkeyAppContext constructor and it didn’t enable firing if MainPage.xaml.cs AppServiceConnected event like alt-o and alt-s keyboard shortcuts do. Am I overlooking what’s needed to get a windows key based global hotkey / keyboard shortcut enabled using this solution example?

      p.s. am I overlooking wordpress article comments section option to enable edit corrections to previously posted reply and deleting ones that I later determine to be irrelevant? w/o that it’s hard to refine questions after the fact to better communicate issue and/or ask.

      Like

      1. I saw that note about the fsModifiers.MOD_WIN value in the RegistryHotKey win32 api docs but presumed it just meant that you should only wire up win+ hotkey / keyboard shortcut for behaviors that are relevant at the OS level not application specific level.

        My understanding has always been that fsModifiers.MOD_ALT+ and fsModifiers.MOD_CTRL+ are what you use for application specific hotkey / keyboard shortcut setups.

        I’m my case I’ve created hotkey / keyboard shortcut that takes currently active window and centers + sizes it to fills ##% of screen relevant in the case of wqhd 16:9 and uwqhd 21:9 aspect ration displays where I found myself constantly moving and sizing windows when I needed to enable easy/fast reading window position and size. My other hotkey / keyboard shortcut flips the mousebuttons, vs having to navigate settings menus to accomplish this or execute custom cli utility, where I find myself constantly switching hands so I don’t end up in a specific mouse hand driven posture all day. I have a few other windows niceties I want to enable but w/o hotkey option and rather a button action selection that the uwp [ or winform ] UI your same pops up would host. Not having ability to write to hkcu registry key paths impacts a couple of those features.

        Like

  2. Hey Stefan, I really appreciate your work here and am impressed with your UWP depth of knowledge. I was wondering if you had any ideas about how to pass an extra variable with the message to the UWP window. so far I havent been able to, and I have attempted to edit the hotkeys_HotkeyPressed(int ID) function to accept a string argument as well as its ID. Then when trying to grad the string on the UWP app I realized the message is just a pid/pointer so it seems I cant pass a string this way. Wondering if you have any advice as to what the best way to pass an extra var to the UWP would be, whether that would be editing a hotkey method or the package containing the extension.

    Appreciation and thanks, GC

    Like

    1. This should be fairly straightforward, like this:
      – in the HotKeyWindow class extend the delegate to take a string in addition to the ID
      – in the HotKeyAppcontext class extend the event handler to take the string
      – after creating the ‘hotkeyPressed’ ValueSet, add the string value as another item to that ValueSet
      – now on the UWP side you can read the ID and the extra string from the ValueSet when you receive the AppService request

      Like

  3. Hi Stefan, Your tutorial clearly first rank in UWP.
    Can I know why HotkeyWindow.cs, we can safely assume First is the UWP? not the Classic Console App?
    // bring the UWP to the foreground (optional)
    IEnumerable appListEntries = await Package.Current.GetAppListEntriesAsync();
    await appListEntries.First().LaunchAsync();

    Like

    1. Yes, the UWP launches first. It then launches the console app process (MainPage.xaml.cs line 46).

      The reason for the other “LaunchAsync” call in the sample is just to bring the UWP to the foreground. Calling LaunchAsync on an already launched app brings it to the foreground (side-note: unless it’s a multi-instance then LaunchAsync would launch a new instance, so the sample code isn’t very good, but in my defense it was written before we had multi-instance UWP apps).

      Like

  4. Hi stefanwick,
    Thanks for you great sample~ It help me a lot.
    Imitating your code I wrote a similar project.
    But I have a problem that, if I minimize UWP app and wait some minutes.
    Then the background task will not handle hotkey event.

    Do I miss any key point in your sample code?
    Seems you code can continue after UWP app in minimize status some minutes.
    Thanks again.

    Like

      1. Sorry I didn’t describe it clearly. After I did more research. I find that, After a short time without any action on the application, then the WndProc can’t receive any message.

        But If I create the hotkeyWindow to a separate Windows Application. It always works fine. If I put it through desktop extension. It will fail.

        Like

      2. Thanks stefanwick! Finally I find the way to resolve this problem. As you said, it about the life cycle of AppServiceConnection.
        The reality is a bit complicated. I have multiple different life cycle AppServiceConnection. Some one is one-way, and some one is two-way communication between UWP and desktop extension.

        I think the point which I learn from you is that, create a new AppServiceConnection if we want send message from desktop extension to UWP side. Because we don’t know when does AppServiceConnection close.

        Thanks again!

        Like

  5. Thank you very much Stefan for sharing your insights into UWP!
    Is there any way to register a gamepad input as hotkey?
    Or is there maybe any other ways to read gamepadinput while an UWP app is not in focus? (In general Gamepad input should not as securityrelevant as Keyboardinput.) Beeing able to do that would be a massive improvement of the usablity of my app.
    Thanks again und Frohes neues Jahr!

    Like

    1. I am not very familiar with gamepad input and I have left the Windows team almost 3 years ago. I would recommend you ask this question on Microsoft Q&A or on Stackoverflow.

      Like

Leave a comment