处理 Microsoft Copilot 硬件密钥状态更改

本文介绍如何在按下、按住和释放 Microsoft Copilot 硬件按键或 Windows 键 + C 时进行注册,以激活应用并接收通知。 此功能使应用程序能够根据检测到的键状态变化执行不同的动作。 例如,当键处于单按下状态时,应用可能会执行正常激活,但在按下并按住该键时拍摄屏幕截图。 或者,应用可以开始录制音频,并显示一个状态指示器,指示在按下并按住键时正在录制音频,然后在释放键时停止录制音频。 键必须按下并按住至少 300 毫秒才能进入保留状态。

此功能扩展了基本Microsoft Copilot 硬件密钥提供程序的功能,只需在按下硬件密钥时注册即可启动。 有关详细信息,请参阅 Microsoft Copilot 硬件密钥提供程序

本文的其余部分将详细讲解如何创建一个简单的 C# WinUI 3 应用,以响应通过单次按下或按下并按住再释放 Microsoft Copilot 按钮启动的激活。

创建新项目

在 Visual Studio 中,创建新项目。 对于此示例,在“ 创建新项目 ”对话框中,将语言筛选器设置为 C# 并将项目类型设置为 WinUI 3,然后选择“空白应用,打包(桌面版 WinUI 3)。

添加属性以跟踪Microsoft Copilot 键按下状态

在此示例中,我们将创建一个名为 State 的属性,用于在 UI 中显示当前激活状态。 在 MainWindow.xaml.csMainWindow 的定义中,添加以下代码以创建可在 XAML 文件中绑定到的字符串属性。

// MainWindow.xaml.cs
public event PropertyChangedEventHandler? PropertyChanged;

private void OnPropertyChanged([CallerMemberName] string propertyName = "State")
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

public void SetState(string state)
{
    State = state;
}

private string _state;
public string State
{
    get => _state;
    set
    {
        if (_state != value)
        {
            _state = value;
            OnPropertyChanged();
        }
    }
}

TextBox 控件添加到 UI 以显示应用的当前激活状态。 将 MainPage.xaml 中的默认 StackPanel 元素替换为以下代码。

<!-- MainWindow.xaml -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
    <TextBlock Name="KeyStateText" Text="{x:Bind State, Mode=OneWay}" />
</StackPanel>

最后,更新 MainWindow 构造函数以采用一个参数,该参数将在创建窗口时设置 State 属性。

// MainWindow.xaml.cs
public MainWindow(string state)
{
    this.InitializeComponent();

    _state = state;
}

注册 URI 激活

系统使用 URI 激活启动 Microsoft Copilot 硬件密钥提供程序。 通过将 uap:Protocol 元素添加到应用清单来注册启动协议。 有关如何注册为 URI 方案的默认处理程序的详细信息,请参阅 “处理 URI 激活”。

以下示例演示了注册 URI 方案“myapp-copilothotkey”的 uap:Extension

<!-- Package.appxmanifest -->
...
  xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
...

<Extensions> 
  ...
  <uap:Extension Category="windows.protocol">
    <uap:Protocol Name="myapp-copilothotkey"> <!-- app-defined protocol name -->
      <uap:DisplayName>SDK Sample URI Scheme</uap:DisplayName>
    </uap:Protocol>
  </uap:Extension>
  ...

Microsoft Copilot 硬件密钥应用扩展

必须打包应用才能注册为 Microsoft Copilot 硬件密钥提供程序。 有关应用打包的信息,请参阅 Windows 应用中的包标识概述。 Microsoft Copilot 硬件密钥提供程序在 uap3:AppExtension 中声明其注册信息。 扩展的 Name 属性必须设置为“com.microsoft.windows.copilotkeyprovider”。 若要支持密钥状态更改,应用必须向其 uap3:AppExtension 声明提供一些其他条目。

uap3:AppExtension 元素内部,添加包含子元素 PressAndHoldStartPressAndHoldStopuap3:Properties 元素。 这些元素的内容应为上一步骤中清单中注册的协议方案的 URI。 查询字符串参数指定是启动 URI 是因为用户按下并按住了热键,还是因为用户释放了热键。 应用在应用激活期间使用这些查询字符串值来确定要采取的正确作。

<!-- Package.appxmanifest -->

<Extensions> 
  ...
  <uap3:Extension Category="windows.appExtension"> 
    <uap3:AppExtension Name="com.microsoft.windows.copilotkeyprovider"  
      Id="MyAppId" 
      DisplayName="App display name" 
      Description="App description" 
      PublicFolder="Public"> 
      <uap3:Properties> 
        <PressAndHoldStart>myapp-copilothotkey:?state=Down</PressAndHoldStart> 
        <PressAndHoldStop>myapp-copilothotkey:?state=Up</PressAndHoldStop> 
      </uap3:Properties> 
    </ uap3:AppExtension> 
  </uap3:Extension> 
  ...

处理 URI 激活

若要检测应用是否已通过 URI 激活激活,请调用 AppInstance.GetActivatedEventArgs 并检查 AppActivationArguments.Kind 属性的值是否为 Protocol。 如果应用是通过协议激活启动的,请检查 URI 方案是否与应用清单中指定的协议名称相同。 如果所有这些测试都通过,则你知道你的应用已由用户按 Copilot 硬件密钥激活。 此时,可以分析 URI 查询字符串并获取 状态 参数,该参数将在应用清单的 PressAndHoldStartPressAndHoldStop 元素中指定值。

// App.xaml.cs

protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
    var eventargs = AppInstance.GetCurrent().GetActivatedEventArgs();
    string state = "";
    if ((eventargs != null) && (eventargs.Kind == ExtendedActivationKind.Protocol))
    {
        var protocolArgs = (Windows.ApplicationModel.Activation.ProtocolActivatedEventArgs)eventargs.Data;
        WwwFormUrlDecoder decoderEntries = new WwwFormUrlDecoder(protocolArgs.Uri.Query);
        state = Uri.UnescapeDataString(decoderEntries.GetFirstValueByName("state"));
    }
    state = (state == "") ? "Launched" : state;

    m_window = new MainWindow(state);
    m_window.Activate();
}

重要

请注意,默认情况下,WinUI 3 应用是多实例的,这意味着每当按下或释放 Microsoft Copilot 热键时,都会启动一个新实例。 这可能是许多提供程序的所需行为,但如果愿意,可以更新应用以使用单个实例。 有关详细信息,请参阅 使用 C# 创建单实例 WinUI 应用

处理快速路径调用

除了 URI 激活之外,应用还可以注册以支持快速路径调用,其中正在运行的应用通过窗口消息接收有关 Copilot 硬件应用的消息。 对于当前运行的应用,此调用方法比 URI 激活更快,并且会提供更好的用户体验,因为应用可以在按下并按住键后更快地开始侦听语音。

更新应用清单文件以支持快速路径调用

若要添加对快速路径调用的支持,请更新“com.microsoft.windows.copilotkeyprovider”扩展,将 MessageWParam 属性添加到 SingleTapPressAndHoldStart 和 PressAndHoldStop 元素。 每个 MessageWParam 值必须是唯一的 32 位整数,但应用会选择所使用的值。 此示例分别使用值 0、1 和 2。 这些值将在示例中稍后在 Windows 消息的 wParam 参数中传递时使用,以确定 Windows Copilot 硬件键的当前按下状态。

<!-- Package.appxmanifest -->

<uap3:Extension Category="windows.appExtension">
  <uap3:AppExtension Name="com.microsoft.windows.copilotkeyprovider"
    Id="MyAppId"
    DisplayName="App display name"
    Description="App description"
    PublicFolder="Public">
    <uap3:Properties>
      <SingleTap MessageWParam="0"/>
      <PressAndHoldStart MessageWParam="1">myapp-copilothotkey://?state=Down</PressAndHoldStart>
      <PressAndHoldStop MessageWParam="2">myapp-copilothotkey://?state=Up</PressAndHoldStop>
    </uap3:Properties>
  </uap3:AppExtension>
</uap3:Extension>

访问 win32 API 进行窗口注册

通过在与应用窗口之一关联的 IPropertyStore 上设置属性来启用快速路径激活。 为此,需要访问一些原生 Win32 API。 本演练将使用 CsWin32 库,该库可自动生成 C# 绑定,并可用作 NuGet 包。

在 Visual Studio 的解决方案资源管理器中,右键单击项目文件,然后选择“ 管理 NuGet 包...”。在 NuGet 包管理器的“ 浏览 ”选项卡上,搜索“cswin32”,然后选择“Microsoft.Windows.CsWin32”包,然后单击“安装”。

安装包后,在项目目录中添加新的文本文件并将其命名为“NativeMethods.txt”。 CsWin32 工具将在此文件中查找它将为其生成绑定的 Win32 API 列表。 将以下 API 名称放入“NativeMethods.txt”。

SUBCLASSPROC

SHGetPropertyStoreForWindow

IPropertyStore

SetWindowSubclass

DefSubclassProc

注册 Microsoft Copilot FastPath 调用的窗口

接下来,我们将更新 MainWindow 类以注册窗口,以接收来自 Copilot 硬件密钥的快速路径调用。

首先,调用 GetWindowHandle 以获取 MainWindowHWND 句柄。 调用 SHGetPropertyStoreForWindow 以获取窗口的 IPropertyStore 。 创建一个新的 PROPERTYKEY,并将 fmtid 成员设置为用于 Windows Copilot FastPath 激活的 GUID。 将属性的值设置为应用定义的值,当硬件密钥状态发生更改时,该值将从系统传回应用。 应用定义值是窗口消息 ID,该 ID 必须位于WM_APP范围内。 有关详细信息,请参阅 WM_APP。 调用 SetValue ,然后调用 Commit 将更改提交到属性存储区。

最后,创建一个 SUBCLASSPROC 回调,该回调将在硬件密钥状态更改时调用。 WindowSubClass 是下一步中显示的回调实现。 调用 SetWindowSubclass 注册回调。

private HWND hWndMain;
private Windows.Win32.UI.Shell.SUBCLASSPROC SubClassDelegate;
public const int WM_COPILOT = 0x8000 + 0x0001;

public MainWindow(string state)
{
    this.InitializeComponent();

    hWndMain = (HWND)WinRT.Interop.WindowNative.GetWindowHandle(this);
    Microsoft.UI.Windowing.AppWindow appWindow = AppWindow;


    var propertyStoreGUID = new Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99");
    var hr = PInvoke.SHGetPropertyStoreForWindow((HWND)this.AppWindow.Id.Value, in propertyStoreGUID, out var propertyStore);
    var key = new PROPERTYKEY();
    var copilotFastpathGUID = new Guid("38652BCA-4329-4E74-86F9-39CF29345EEA");
    key.fmtid = copilotFastpathGUID;
    key.pid = 0x00000002;
    var value = new PROPVARIANT();
    value.Anonymous.Anonymous.vt = VARENUM.VT_UINT;
    value.Anonymous.decVal = WM_COPILOT;
    ((IPropertyStore)propertyStore).SetValue(in key, in value);
    ((IPropertyStore)propertyStore).Commit();

    SubClassDelegate = new Windows.Win32.UI.Shell.SUBCLASSPROC(WindowSubClass);
    bool bRet = PInvoke.SetWindowSubclass((HWND)appWindow.Id.Value, SubClassDelegate, 0, 0);

    _state = state;
}

实现窗口子类回调

此示例的最后一步是实现窗口子类回调,每当应用运行并且 Windows Copilot 硬件密钥的状态发生变化时,都会调用该回调。 在此示例中,我们检查窗口消息是否是我们在上一步中设置属性存储值时指定的 WM_COPILOT 值。 然后,我们检查 wParam 参数的值,以查看在应用清单中使用 MessageWParam 属性指定的值中的哪一个值已传入。 调用 SetState 来更新 UI,使其反映当前状态。

private LRESULT WindowSubClass(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam, nuint uIdSubclass, nuint dwRefData)
{
    switch (uMsg)
    {
        case WM_COPILOT:
        {
            switch (wParam.Value)
            {
                case 0:
                    SetState("SingleTap");
                    break;
                case 1:
                    SetState("PressAndHold START");
                    break;
                case 2:
                    SetState("PressAndHold END");
                    break;
            }
        }
        break;

    }
    return PInvoke.DefSubclassProc((HWND)hWnd, uMsg, wParam, lParam);

}

对 Windows Copilot 硬件密钥提供程序进行签名

必须对提供程序应用进行签名才能作为 Microsoft Copilot 硬件密钥的目标来启用。 有关打包和签名应用的信息,请参阅 在 Visual Studio 中打包桌面或 UWP 应用