處理 Microsoft Copilot 硬體金鑰狀態變更

本文說明應用程式如何註冊啟用並接收通知,當 Microsoft Copilot 硬體鍵或 Windows 鍵 + C 被按下、長按並放開時。 此功能使應用程式能夠根據偵測到的按鍵狀態變化執行不同的動作。 例如,一個應用程式當按鍵被單次按下時,可能會執行正常啟動,但當按鍵被長按時,則會截圖。 或者,應用程式可能會在按下並按住按鍵時開始錄音,並顯示正在錄音的狀態指示器,然後在釋放按鍵時停止錄音。 要將鍵盤按鍵移至按住狀態,必須按住至少300毫秒。

此功能擴展了基本的 Microsoft Copilot 硬體金鑰提供者的功能,該提供者只需在按下硬體鍵時註冊啟動。 欲了解更多資訊,請參閱 Microsoft Copilot 硬體金鑰提供者

本文其餘部分將逐步說明如何打造一個簡單的 C# WinUI 3 應用程式,此應用程式可回應由單次按下或按住並釋放 Microsoft Copilot 按鈕所啟動的操作。

建立一個新 project

在 Visual Studio 裡,建立一個新 project。 在這個例子中,在 「建立新專案 」對話框中,將語言篩選器設為 C#,專案類型設為 WinUI,然後選擇「WinUI 空白應用程式(封裝)」。

新增一個屬性來追蹤 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 啟用

下列範例顯示 uap:Extension 註冊 URI 協議 “myapp-copilothotkey”。

<!-- 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 app 中的套件識別概覽。 Microsoft Copilot硬體金鑰提供者會在 uap3:AppExtension 內宣告其註冊資訊。 延伸模組的 Name 屬性必須設定為 「com.microsoft.windows.copilotkeyprovider」。。 若要支援主要狀態變更,應用程式必須為其 uap3:AppExtension 宣告提供一些額外的項目。

uap3:AppExtension 元素內,新增具有子元素 PressAndHoldStartPressAndHoldStopuap3:Properties 元素。 在前一步驟中註冊於清單中的這些元素的內容應該是協議方案的 URI。 查詢字串參數指定 URI 被啟動的原因,是因為使用者按住快捷鍵,還是因為使用者鬆開了快捷鍵。 應用程式在啟動時使用這些查詢字串值來確定要採取的正確行動。 指定 SingleTap 元素是選擇性的,但有助於判斷應用程式是否從 Copilot 硬體金鑰啟動。

<!-- 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> 
        <SingleTap>myapp-copilothotkey://?state=Tap</SingleTap>
        <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 應用程式是多實例式的,也就是說每當按下或放開 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">myapp-copilothotkey://?state=Tap</SingleTap>
      <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中,在 Solution Explorer,右鍵點擊你的 project 檔案,選擇 管理 NuGet 套件...。在 NuGet package manager 的 Browse 分頁中,搜尋「cswin32」並選擇「Microsoft.Windows.CsWin32」套件,點選 *Install

套件安裝完成後,在你的project目錄新增一個文字檔,並命名為「NativeMethods.txt」。 CsWin32工具會在此檔案中尋找要生成綁定的Win32 API清單。 請將以下 API 名稱放入 "NativeMethods.txt" 中。

SUBCLASSPROC

SHGetPropertyStoreForWindow

IPropertyStore

SetWindowSubclass

DefSubclassProc

註冊 Microsoft Copilot 快速路徑調用視窗

接下來,我們將更新 MainWindow 類別,以註冊視窗,以接收來自 Copilot 硬體密鑰的快速路徑調用。

首先,呼叫 GetWindowHandle 以取得 MainWindowHWND 句柄。 呼叫 SHGetPropertyStoreForWindow 以取得視窗的 IPropertyStore 。 建立新的 PROPERTYKEY ,並將 fmtid 成員設定為適用於 Windows Copilot 快速路徑啟用的 GUID。 將屬性值設置為應用程式定義的值,當硬體鍵狀態發生變化時,系統會將該值傳回到應用程式。 應用程式定義的值是 windows 訊息識別碼,其必須位於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 應用程式