|
| 1 | +--- |
| 2 | +title: Handle Microsoft Copilot hardware key state changes |
| 3 | +description: Learn how to register to be activated and receive notifications when the Microsoft Copilot hardware key or Windows key + C is pressed. |
| 4 | +ms.topic: article |
| 5 | +ms.date: 10/25/2024 |
| 6 | +ms.localizationpriority: medium |
| 7 | +--- |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +# Handle Microsoft Copilot hardware key state changes |
| 12 | + |
| 13 | +This article describes how apps can register to be activated and receive notifications when the Microsoft Copilot hardware key or Windows key + C is pressed, pressed and held, and released. This feature enables apps to perform different actions depending on which key state change is detected. For example, an app may perform normal activation when the key is single-pressed, but take a screenshot when the key is pressed and held. Or, an app may begin recording audio and show a status indicator that audio is being recorded when the key is pressed and held, and then stop recording audio when the key is released. The key must be pressed and held for at least 300 ms to move into the held state. |
| 14 | + |
| 15 | +This feature extends the features of a basic Microsoft Copilot hardware key provider, which simply registers to be launched when the hardware key is pressed. For more information, see [Microsoft Copilot hardware key providers](microsoft-copliot-key-provider.md). |
| 16 | + |
| 17 | +The rest of this article will walk through creating a simple C# WinUI 3 app that responds to activation initiated by a single press or a press and hold and release of the Microsoft Copilot button. |
| 18 | + |
| 19 | +## Create a new project |
| 20 | + |
| 21 | +In Visual Studio, create a new project. For this example, in the **Create a new project** dialog, set the language filter to C# and the project type to WinUI 3 and then select the "Blank App, Packaged (WinUI 3 in Desktop). |
| 22 | + |
| 23 | +## Add a property to track the Microsoft Copilot key pressed state |
| 24 | + |
| 25 | +In this example, we will create a property called **State** that we will use to display the current activation state in the UI. In `MainWindow.xaml.cs`, inside the definition of **MainWindow** add the following code to create a string property that we can bind to in our XAML file. |
| 26 | + |
| 27 | +```csharp |
| 28 | +// MainWindow.xaml.cs |
| 29 | +public event PropertyChangedEventHandler? PropertyChanged; |
| 30 | + |
| 31 | +private void OnPropertyChanged([CallerMemberName] string propertyName = "State") |
| 32 | +{ |
| 33 | + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); |
| 34 | +} |
| 35 | + |
| 36 | +public void SetState(string state) |
| 37 | +{ |
| 38 | + State = state; |
| 39 | +} |
| 40 | + |
| 41 | +private string _state; |
| 42 | +public string State |
| 43 | +{ |
| 44 | + get => _state; |
| 45 | + set |
| 46 | + { |
| 47 | + if (_state != value) |
| 48 | + { |
| 49 | + _state = value; |
| 50 | + OnPropertyChanged(); |
| 51 | + } |
| 52 | + } |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +Add a **TextBox** control to the UI to show the current activation state of the app. Replace the default **StackPanel** element in MainPage.xaml with the following code. |
| 57 | + |
| 58 | +```xaml |
| 59 | +<!-- MainWindow.xaml --> |
| 60 | +<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> |
| 61 | + <TextBlock Name="KeyStateText" Text="{x:Bind State, Mode=OneWay}" /> |
| 62 | +</StackPanel> |
| 63 | +``` |
| 64 | + |
| 65 | +Finally, update the **MainWindow** constructor to take an argument that will set the **State** property when the window is created. |
| 66 | + |
| 67 | +```csharp |
| 68 | +// MainWindow.xaml.cs |
| 69 | +public MainWindow(string state) |
| 70 | +{ |
| 71 | + this.InitializeComponent(); |
| 72 | + |
| 73 | + _state = state; |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | + |
| 78 | +## Register for URI activation |
| 79 | + |
| 80 | +The system launches Microsoft Copilot hardware key providers using URI activation. Register a launch protocol by adding the [uap:Protocol](/uwp/schemas/appxpackage/uapmanifestschema/element-uap-protocol) element to your app manifest. For more information about how to register as the default handler for a URI scheme, see [Handle URI activation](/windows/apps/develop/launch/handle-uri-activation). |
| 81 | + |
| 82 | +The following example shows the **uap:Extension** registering the URI scheme "myapp-copilothotkey". |
| 83 | + |
| 84 | +```xml |
| 85 | +<!-- Package.appxmanifest --> |
| 86 | +... |
| 87 | + xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" |
| 88 | +... |
| 89 | + |
| 90 | +<Extensions> |
| 91 | + ... |
| 92 | + <uap:Extension Category="windows.protocol"> |
| 93 | + <uap:Protocol Name="myapp-copilothotkey"> <!-- app-defined protocol name --> |
| 94 | + <uap:DisplayName>SDK Sample URI Scheme</uap:DisplayName> |
| 95 | + </uap:Protocol> |
| 96 | + </uap:Extension> |
| 97 | + ... |
| 98 | +``` |
| 99 | + |
| 100 | +## Microsoft Copilot hardware key app extension |
| 101 | + |
| 102 | +An app must be packaged in order to register as a Microsoft Copilot hardware key provider. For information on app packaging, see [An overview of Package Identity in Windows app](/windows/apps/desktop/modernize/package-identity-overview). Microsoft Copilot hardware key providers declare their registration information within the [uap3:AppExtension](/uwp/schemas/appxpackage/uapmanifestschema/element-uap3-appextension-manual). The **Name** attribute of the extension must be set to "com.microsoft.windows.copilotkeyprovider". To support the key state changes, apps must provide some additional entries to their **uap3:AppExtension** declaration. |
| 103 | + |
| 104 | +Inside of the **uap3:AppExtension** element, add a [uap3:Properties](/uwp/schemas/appxpackage/uapmanifestschema/element-uap3-properties-manual) element with child elements **PressAndHoldStart** and **PressAndHoldStop**. The contents of these elements should be the URI of the protocol scheme registered in the manifest in the previous step. The query string arguments specify whether the URI is being launched because the user pressed and held the hot key or because the user released the hot key. The app uses these query string values during app activation to determine the correct action to take. |
| 105 | + |
| 106 | +```xml |
| 107 | +<!-- Package.appxmanifest --> |
| 108 | + |
| 109 | +<Extensions> |
| 110 | + ... |
| 111 | + <uap3:Extension Category="windows.appExtension"> |
| 112 | + <uap3:AppExtension Name="com.microsoft.windows.copilotkeyprovider" |
| 113 | + Id="MyAppId" |
| 114 | + DisplayName="App display name" |
| 115 | + Description="App description" |
| 116 | + PublicFolder="Public"> |
| 117 | + <uap3:Properties> |
| 118 | + <PressAndHoldStart>myapp-copilothotkey:?state=Down</PressAndHoldStart> |
| 119 | + <PressAndHoldStop>myapp-copilothotkey:?state=Up</PressAndHoldStop> |
| 120 | + </uap3:Properties> |
| 121 | + </ uap3:AppExtension> |
| 122 | + </uap3:Extension> |
| 123 | + ... |
| 124 | +``` |
| 125 | + |
| 126 | +## Handle URI activation |
| 127 | + |
| 128 | +To detect whether the app was activated via URI activation, call [AppInstance.GetActivatedEventArgs](/windows/windows-app-sdk/api/winrt/microsoft.windows.applifecycle.appinstance.getactivatedeventargs) and check to see if the value of the [AppActivationArguments.Kind](/windows/windows-app-sdk/api/winrt/microsoft.windows.applifecycle.appactivationarguments.kind) property is [Protocol](/windows/windows-app-sdk/api/winrt/microsoft.windows.applifecycle.extendedactivationkind). If the app was launched via protocol activation, check to see if the URI scheme is the same as the protocol name you specified in your app manifest. If all of these tests pass, then you know that your app was activated by the user pressing the Copilot hardware key. At this point you can parse the URI query string and get the *state* parameter, which will have the values you specified in the **PressAndHoldStart** and **PressAndHoldStop** elements in the app manifest. |
| 129 | + |
| 130 | +```csharp |
| 131 | +// App.xaml.cs |
| 132 | +
|
| 133 | +protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) |
| 134 | +{ |
| 135 | + var eventargs = AppInstance.GetCurrent().GetActivatedEventArgs(); |
| 136 | + string state = ""; |
| 137 | + if ((eventargs != null) && (eventargs.Kind == ExtendedActivationKind.Protocol)) |
| 138 | + { |
| 139 | + var protocolArgs = (Windows.ApplicationModel.Activation.ProtocolActivatedEventArgs)eventargs.Data; |
| 140 | + WwwFormUrlDecoder decoderEntries = new WwwFormUrlDecoder(protocolArgs.Uri.Query); |
| 141 | + state = Uri.UnescapeDataString(decoderEntries.GetFirstValueByName("state")); |
| 142 | + } |
| 143 | + state = (state == "") ? "Launched" : state; |
| 144 | + |
| 145 | + m_window = new MainWindow(state); |
| 146 | + m_window.Activate(); |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +> [!IMPORTANT] |
| 151 | +> Note that, by default, WinUI 3 apps are multi-instanced, which means that a new instance will be launched whenever the Microsoft Copilot hot key is pressed or released. This may be the desired behavior for many providers, but if you would prefer, you can update you app to use a single instance. For more information, see [Create a single-instanced WinUI app with C#](/windows/apps/windows-app-sdk/applifecycle/applifecycle-single-instance). |
| 152 | +
|
| 153 | +## Handle fast path invocation |
| 154 | + |
| 155 | +In addition to URI activation, apps can register to support fast path invocation in which a running app receives messages about Copilot hardware app through window messages. For a currently-running app, this invocation method is faster than URI activation and will provide a better user experience, since the app can begin listening for speech more quickly after the key is pressed and held. |
| 156 | + |
| 157 | +### Update the app manifest file to support fast path invocation |
| 158 | + |
| 159 | +To add support for fast path invocation, update the "com.microsoft.windows.copilotkeyprovider" extension to add the *MessageWParam* attribute to the **SingleTap**, **PressAndHoldStart**, and **PressAndHoldStop** elements. Each *MessageWParam* value must be a unique 32-bit integer, but the values used are chosen by the app. This example uses values of 0, 1, and 2, respectively. These values will be used later in the example when they are passed in the *wParam* parameter of a Windows message to determine the current pressed state of the Windows Copilot hardware key. |
| 160 | + |
| 161 | +```xml |
| 162 | +<!-- Package.appxmanifest --> |
| 163 | + |
| 164 | +<uap3:Extension Category="windows.appExtension"> |
| 165 | + <uap3:AppExtension Name="com.microsoft.windows.copilotkeyprovider" |
| 166 | + Id="MyAppId" |
| 167 | + DisplayName="App display name" |
| 168 | + Description="App description" |
| 169 | + PublicFolder="Public"> |
| 170 | + <uap3:Properties> |
| 171 | + <SingleTap FastPathValue="0"/> |
| 172 | + <PressAndHoldStart MessageWParam="1">myapp-copilothotkey://?state=Down</PressAndHoldStart> |
| 173 | + <PressAndHoldStop MessageWParam="2">myapp-copilothotkey://?state=Up</PressAndHoldStop> |
| 174 | + </uap3:Properties> |
| 175 | + </uap3:AppExtension> |
| 176 | +</uap3:Extension> |
| 177 | +``` |
| 178 | + |
| 179 | +### Access win32 APIs for window registration |
| 180 | + |
| 181 | +Fast path activation is enabled by setting a property on the [IPropertyStore](/windows/win32/api/propsys/nn-propsys-ipropertystore) associated with one of the app's windows. To do this requires access to some native Win32 APIs. This walkthrough will use the CsWin32 library, which automates the generation of C# bindings and is available as a NuGet package. |
| 182 | + |
| 183 | +In Visual Studio, in **Solution Explorer**, right-click on your project file and select **Manage NuGet packages...**. On the **Browse** tab of the NuGet package manager, search for "cswin32" and select the "Microsoft.Windows.CsWin32" package and click **Install*. |
| 184 | + |
| 185 | +After the package is installed, add a new text file in your project directory and name it "NativeMethods.txt". The CsWin32 tool will look in this file for a list of the Win32 APIs that it will generate bindings for. Put the following API names in "NativeMethods.txt". |
| 186 | + |
| 187 | +`SUBCLASSPROC` |
| 188 | + |
| 189 | +`SHGetPropertyStoreForWindow` |
| 190 | + |
| 191 | +`IPropertyStore` |
| 192 | + |
| 193 | +`SetWindowSubclass` |
| 194 | + |
| 195 | +`DefSubclassProc` |
| 196 | + |
| 197 | +### Register the window for Microsoft Copilot fastpath invocation |
| 198 | + |
| 199 | +Next we will update the **MainWindow** class to register the window to receive fastpath invocations from the Copilot hardware key. |
| 200 | + |
| 201 | +First, call [GetWindowHandle](/windows/windows-app-sdk/api/win32/microsoft.ui.xaml.window/nf-microsoft-ui-xaml-window-iwindownative-get_windowhandle) to get an [HWND](/windows/win32/winprog/windows-data-types) handle to the **MainWindow**. Call [SHGetPropertyStoreForWindow](/windows/win32/api/shellapi/nf-shellapi-shgetpropertystoreforwindow) to get the **IPropertyStore** for the window. Create a new [PROPERTYKEY](/windows/win32/api/wtypes/ns-wtypes-propertykey) and set the *fmtid* member to the GUID for Windows Copilot fastpath activation. Set the value of the property to an app-defined value, that will be passed back to the app from the system when the hardware key state changes. The app-defined value is the windows message ID which must be in the WM_APP range. For more information, see [WM_APP](/windows/win32/winmsg/wm-app). Call [SetValue](/windows/win32/api/propsys/nf-propsys-ipropertystore-setvalue) and then call [Commit](/windows/win32/api/propsys/nf-propsys-ipropertystore-commit) to commit the change to the property store. |
| 202 | + |
| 203 | +Finally, create a [SUBCLASSPROC](/windows/win32/api/commctrl/nc-commctrl-subclassproc) callback that will be called when the hardware key state changes. **WindowSubClass** is the callback implementation that will be shown in the next step. Call [SetWindowSubclass](/windows/win32/api/commctrl/nf-commctrl-setwindowsubclass) to register the callback. |
| 204 | + |
| 205 | +```csharp |
| 206 | +private HWND hWndMain; |
| 207 | +private Windows.Win32.UI.Shell.SUBCLASSPROC SubClassDelegate; |
| 208 | +public const int WM_COPILOT = 0x8000 + 0x0001; |
| 209 | + |
| 210 | +public MainWindow(string state) |
| 211 | +{ |
| 212 | + this.InitializeComponent(); |
| 213 | + |
| 214 | + hWndMain = (HWND)WinRT.Interop.WindowNative.GetWindowHandle(this); |
| 215 | + Microsoft.UI.Windowing.AppWindow appWindow = AppWindow; |
| 216 | + |
| 217 | + |
| 218 | + var propertyStoreGUID = new Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"); |
| 219 | + var hr = PInvoke.SHGetPropertyStoreForWindow((HWND)this.AppWindow.Id.Value, in propertyStoreGUID, out var propertyStore); |
| 220 | + var key = new PROPERTYKEY(); |
| 221 | + var copilotFastpathGUID = new Guid("38652BCA-4329-4E74-86F9-39CF29345EEA"); |
| 222 | + key.fmtid = copilotFastpathGUID; |
| 223 | + key.pid = 0x00000002; |
| 224 | + var value = new PROPVARIANT(); |
| 225 | + value.Anonymous.Anonymous.vt = VARENUM.VT_UINT; |
| 226 | + value.Anonymous.decVal = WM_COPILOT; |
| 227 | + ((IPropertyStore)propertyStore).SetValue(in key, in value); |
| 228 | + ((IPropertyStore)propertyStore).Commit(); |
| 229 | + |
| 230 | + SubClassDelegate = new Windows.Win32.UI.Shell.SUBCLASSPROC(WindowSubClass); |
| 231 | + bool bRet = PInvoke.SetWindowSubclass((HWND)appWindow.Id.Value, SubClassDelegate, 0, 0); |
| 232 | + |
| 233 | + _state = state; |
| 234 | +} |
| 235 | +``` |
| 236 | + |
| 237 | +### Implement the window subclass callback |
| 238 | + |
| 239 | +The last step in this example is implementing the window subclass callback that will be called whenever the app is running and the state of the Windows Copilot hardware key changes. In this example, we check that the window message is the **WM_COPILOT** value that we specified when setting the property store value in the previous step. Then we check the value of the *wParam* argument to see which of the values we specified with the **MessageWParam** attributes in the app manifest has been passed in. **SetState** is called to update the UI with the current state. |
| 240 | + |
| 241 | +```csharp |
| 242 | +private LRESULT WindowSubClass(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam, nuint uIdSubclass, nuint dwRefData) |
| 243 | +{ |
| 244 | + switch (uMsg) |
| 245 | + { |
| 246 | + case WM_COPILOT: |
| 247 | + { |
| 248 | + switch (wParam.Value) |
| 249 | + { |
| 250 | + case 0: |
| 251 | + SetState("SingleTap"); |
| 252 | + break; |
| 253 | + case 1: |
| 254 | + SetState("PressAndHold START"); |
| 255 | + break; |
| 256 | + case 2: |
| 257 | + SetState("PressAndHold END"); |
| 258 | + break; |
| 259 | + } |
| 260 | + } |
| 261 | + break; |
| 262 | + |
| 263 | + } |
| 264 | + return PInvoke.DefSubclassProc((HWND)hWnd, uMsg, wParam, lParam); |
| 265 | + |
| 266 | +} |
| 267 | +``` |
| 268 | + |
0 commit comments