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;
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:
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 …):
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/