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/ 

Advertisements

6 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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s