共用方式為


在 C# Windows 應用程式中實作小工具提供者

本文會逐步引導您建立實作 IWidgetProvider 介面的簡單小工具提供者。 小工具主機會叫用這個介面的方法,以要求定義小工具的資料,或讓小工具提供者回應小工具上的使用者動作。 小工具提供者可以支援單一小工具或多個小工具。 在此範例中,我們將定義兩個不同的小工具。 其中一個小工具是模擬天氣小工具,可說明調適型卡片架構所提供的一些格式設定選項。 第二個小工具會藉由每當使用者點擊小工具上顯示的按鈕時而遞增的計數器維護,來示範使用者動作和自訂小工具狀態功能。

簡單天氣小工具的螢幕擷取畫面。該小工具顯示一些與天氣相關的圖形和資料,以及一些診斷文字,說明顯示的是中型小工具的範本。

簡單計算小工具的螢幕擷取畫面。該小工具顯示字串,其中包含要遞增的數值,以及標記為「增量」的按鈕,以及一些診斷文字,說明顯示的是小型小工具的範本。

本文中的這個範例程式碼是從 Windows 應用程式 SDK 小工具範例改編。 若要使用 C++/WinRT 實作小工具提供者,請參閱在 win32 應用程式 (C++/WinRT) 中實作小工具提供者

必要條件

  • 您的裝置必須啟用開發人員模式。 如需詳細資訊,請參閱啟用您的裝置以用於開發
  • Visual Studio 2022 或更新版本,包含通用 Windows 平台開發工作負載。 請務必從選擇性下拉式清單中新增 C++ 的元件 (v143)。

建立新的 C# 主控台應用程式

在 Visual Studio 中,建立新專案。 在 [建立新專案] 對話方塊中,將語言篩選條件設定為「C」,並將平台篩選條件設定為 Windows,然後選取 [建立新專案] 專案範本。 將新專案命名為「ExampleWidgetProvider」。 出現提示時,將目標 .NET 版本設定為 8.0。

載入專案時,在 [方案總管] 中,以滑鼠右鍵按一下專案名稱,然後選取 [屬性]。 在 [一般] 頁面上,向下捲動至 [目標作業系統],然後選取 [Windows]。 在 [目標作業系統版本] 底下,選取版本 10.0.19041.0 或更新版本。

若要更新專案以支援 .NET 8.0,請在方案總管中以滑鼠右鍵按一下專案名稱,然後選取編輯專案檔案。 在 PropertyGroup 中,加入下列 RuntimeIdentifiers 元素。

<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>

請注意,本逐步解說會使用主控台應用程式,在啟用小工具時顯示主控台視窗,以啟用簡單的偵錯。 當您準備好發佈小工具提供者應用程式時,您可以遵循將主控台應用程式轉換成 Windows 應用程式中的步驟,將主控台應用程式轉換成 Windows 應用程式。

新增 Windows 應用程式 SDK 的參考

此範例會使用最新的穩定 Windows 應用程式 SDK NuGet 套件。 在 [方案總管] 中,以滑鼠右鍵按一下 [相依性],然後選取 [管理 NuGet 套件...]。在 NuGet 套件管理員中,選取 [瀏覽] 索引標籤並搜尋「Microsoft.WindowsAppSDK」。 在 [版本] 下拉式清單中選取最新的穩定版本,然後按一下 [安裝]

新增 WidgetProvider 類別來處理小工具作業

在 Visual Studio 的 [方案總管] 中,以滑鼠右鍵按一下 ExampleWidgetProvider 專案,然後選取 [新增] > [類別]。 在 [新增類別] 對話方塊中,將類別命名為「WidgetProvider」,然後按一下 [新增]。 在產生的 WidgetProvider.cs 檔案中,更新類別定義,表示其實作 IWidgetProvider 介面。

// WidgetProvider.cs
internal class WidgetProvider : IWidgetProvider

準備追蹤已啟用的小工具

小工具提供者可以支援單一小工具或多個小工具。 每當小工具主機使用小工具提供者起始作業時,它會傳遞識別碼來識別與作業相關聯的小工具。 每個小工具也有相關聯的名稱和狀態值,可用來儲存自訂資料。 在此範例中,我們將宣告簡單的協助程式結構,以儲存每個釘選小工具的識別碼、名稱和資料。 小工具也可以處於作用中狀態,其會在下方的啟用和停用區段中討論,我們將針對每個具有布林值的小工具追蹤此狀態。 將下列定義新增至 WidgetProvider.cs 檔案,在 ExampleWidgetProvider 命名空間內,但在 WidgetProvider 類別定義外。

// WidgetProvider.cs

public class CompactWidgetInfo
{
    public string? widgetId { get; set; }
    public string? widgetName { get; set; }
    public int customState = 0;
    public bool isActive = false;

}

在 WidgetProvider.cs 的 WidgetProvider 類別定義中,在維護已啟用小工具清單的對應表中新增成員,並使用小工具識別碼作為每個項目的索引鍵。

// WidgetProvider.cs

// Class member of WidgetProvider
public static Dictionary<string, CompactWidgetInfo> RunningWidgets = new Dictionary<string, CompactWidgetInfo>(); 

宣告小工具範本 JSON 字串

此範例會宣告一些靜態字串,以定義每個小工具的 JSON 範本。 為了方便起見,這些範本會儲存在 WidgetProvider 類別的成員變數中。 如果您需要範本的一般儲存體 - 這些範本可以包含在應用程式套件的部分:存取套件檔案。 如需建立小工具範本 JSON 文件的詳細資訊,請參閱使用調適型卡片設計工具建立小工具範本

// WidgetProvider.cs

// Class members of WidgetProvider
        const string weatherWidgetTemplate = """
{
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "type": "AdaptiveCard",
    "version": "1.0",
    "speak": "<s>The forecast for Seattle January 20 is mostly clear with a High of 51 degrees and Low of 40 degrees</s>",
    "backgroundImage": "https://messagecardplayground.azurewebsites.net/assets/Mostly%20Cloudy-Background.jpg",
    "body": [
        {
            "type": "TextBlock",
            "text": "Redmond, WA",
            "size": "large",
            "isSubtle": true,
            "wrap": true
        },
        {
            "type": "TextBlock",
            "text": "Mon, Nov 4, 2019 6:21 PM",
            "spacing": "none",
            "wrap": true
        },
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "Image",
                            "url": "https://messagecardplayground.azurewebsites.net/assets/Mostly%20Cloudy-Square.png",
                            "size": "small",
                            "altText": "Mostly cloudy weather"
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "46",
                            "size": "extraLarge",
                            "spacing": "none",
                            "wrap": true
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "°F",
                            "weight": "bolder",
                            "spacing": "small",
                            "wrap": true
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Hi 50",
                            "horizontalAlignment": "left",
                            "wrap": true
                        },
                        {
                            "type": "TextBlock",
                            "text": "Lo 41",
                            "horizontalAlignment": "left",
                            "spacing": "none",
                            "wrap": true
                        }
                    ]
                }
            ]
        }
    ]
}
""";

    const string countWidgetTemplate = """
{                                                                     
    "type": "AdaptiveCard",                                         
    "body": [                                                         
        {                                                               
            "type": "TextBlock",                                    
            "text": "You have clicked the button ${count} times"    
        },
        {
                "text":"Rendering Only if Small",
                "type":"TextBlock",
                "$when":"${$host.widgetSize==\"small\"}"
        },
        {
                "text":"Rendering Only if Medium",
                "type":"TextBlock",
                "$when":"${$host.widgetSize==\"medium\"}"
        },
        {
            "text":"Rendering Only if Large",
            "type":"TextBlock",
            "$when":"${$host.widgetSize==\"large\"}"
        }                                                                    
    ],                                                                  
    "actions": [                                                      
        {                                                               
            "type": "Action.Execute",                               
            "title": "Increment",                                   
            "verb": "inc"                                           
        }                                                               
    ],                                                                  
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.5"                                                
}
""";

實作 IWidgetProvider 方法

在接下來的幾節中,我們將實作 IFeedProvider 介面的方法。 本文稍後會展示這些方法實作中呼叫的協助程式方法 UpdateWidget

注意

傳遞至 IWidgetProvider 介面回撥方法的物件只會保證在回撥內有效。 您不應該儲存這些物件的參考,因為它們在回撥內容之外的行為未定義。

CreateWidget

當使用者在小工具主機中釘選其中一個應用程式小工具時,小工具主機會呼叫 CreateWidget。 首先,此方法會取得相關聯小工具的識別碼和名稱,並將協助程式結構 CompactWidgetInfo 的新執行個體新增至已啟用小工具的集合。 接下來,我們會傳送小工具的初始範本和資料,該小工具會封裝在 UpdateWidget 協助程式方法中。

// WidgetProvider.cs

public void CreateWidget(WidgetContext widgetContext)
{
    var widgetId = widgetContext.Id; // To save RPC calls
    var widgetName = widgetContext.DefinitionId;
    CompactWidgetInfo runningWidgetInfo = new CompactWidgetInfo() { widgetId = widgetId, widgetName = widgetName };
    RunningWidgets[widgetId] = runningWidgetInfo;


    // Update the widget
    UpdateWidget(runningWidgetInfo);
}

DeleteWidget

當使用者從小工具主機取消釘選您應用程式的其中一個小工具時,小工具主機會呼叫 DeleteWidget。 發生這種情況時,我們會從已啟用的小工具清單中移除相關聯的小工具,這樣我們就不會傳送該小工具的任何進一步更新。

// WidgetProvider.cs

public void DeleteWidget(string widgetId, string customState)
{
    RunningWidgets.Remove(widgetId);

    if(RunningWidgets.Count == 0)
    {
        emptyWidgetListEvent.Set();
    }
}

在此範例中,除了移除已啟用小工具清單中指定的小工具之外,我們也會檢查清單是否現在是空的,如果是的話,我們會設定事件,稍後將用來允許應用程式在沒有任何已啟用小工具時結束。 在您的類別定義內,新增 ManualResetEvent 和公用存取函式的宣告。

// WidgetProvider.cs
static ManualResetEvent emptyWidgetListEvent = new ManualResetEvent(false);

public static ManualResetEvent GetEmptyWidgetListEvent()
{
    return emptyWidgetListEvent;
}

OnActionInvoked

當使用者與小工具範本中定義的動作互動時,小工具主機會呼叫 OnActionInvoked。 針對此範例中使用的計數器小工具,已在小工具的 JSON 範本中以 verb 值為「inc」宣告動作。 小工具提供者程式碼會使用此 verb 值來判斷回應使用者互動時要採取的動作。

...
    "actions": [                                                      
        {                                                               
            "type": "Action.Execute",                               
            "title": "Increment",                                   
            "verb": "inc"                                           
        }                                                               
    ], 
...

在 OnActionInvoked 方法,透過檢查傳遞至方法 WidgetActionInvokedArgsVerb 屬性取得 verb 值。 如果 verb 是「inc」,則我們知道將為小工具的自訂狀態遞增計數。 從 WidgetActionInvokedArgs,取得 WidgetContext 物件和 WidgetId 以取得小工具用來上傳的識別碼。 在已啟用的小工具對應中尋找具有指定識別碼的項目,然後更新用來儲存遞增數量的自訂狀態值。 最後,使用 UpdateWidget 協助程式函式,以新值更新小工具內容。

// WidgetProvider.cs

public void OnActionInvoked(WidgetActionInvokedArgs actionInvokedArgs)
{
    var verb = actionInvokedArgs.Verb;
        if (verb == "inc")
        {
            var widgetId = actionInvokedArgs.WidgetContext.Id;
            // If you need to use some data that was passed in after
            // Action was invoked, you can get it from the args:
            var data = actionInvokedArgs.Data;
            if (RunningWidgets.ContainsKey(widgetId))
            {
                var localWidgetInfo = RunningWidgets[widgetId];
                // Increment the count
                localWidgetInfo.customState++;
                UpdateWidget(localWidgetInfo);
            }
        }
}

如需調適型卡片的 Action.Execute 語法相關資訊,請參閱 Action.Execute。 如需小工具設計互動的指引,請參閱小工具互動設計指引

OnWidgetContextChanged

在目前的版本中,只有在使用者變更釘選小工具的大小時,才會呼叫 OnWidgetContextChanged。 您可以選擇根據要求的大小,將不同的 JSON 範本/資料傳回至小工具主機。 您也可以根據 host.widgetSize 的值,設計範本 JSON 使用條件式轉譯以支援所有可用的大小。 如果您不需要傳送新的範本或資料來考慮大小變更,您可以使用 OnWidgetContextChanged 進行遙測。

// WidgetProvider.cs

public void OnWidgetContextChanged(WidgetContextChangedArgs contextChangedArgs)
{
    var widgetContext = contextChangedArgs.WidgetContext;
    var widgetId = widgetContext.Id;
    var widgetSize = widgetContext.Size;
    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        UpdateWidget(localWidgetInfo);
    }
}
    

啟用和停用

呼叫 Activate 方法來通知小工具提供者,即小工具主機目前有興趣從提供者接收更新的內容。 例如,這可能表示使用者目前正主動檢視小工具主機。 呼叫 Deactivate 方法以通知小工具提供者小工具主機不再要求內容更新。 這兩種方法會定義視窗,該視窗為小工具主機最積極顯示最新的內容。 小工具提供者可以隨時將更新傳送至小工具,例如回應推送通知,但就如同任何背景工作一樣,請務必平衡提供最新內容與資源考量,如電池使用時間等。

每個小工具會呼叫 ActivateDeactivate。 本範例會追蹤 CompactWidgetInfo 協助程序結構中每個小工具的作用中狀態。 在 Activate 方法中,我們會呼叫 UpdateWidget 協助程式方法來更新小工具。 請注意,Activate Deactivate 之間的時間範圍可能很小,因此建議您盡量快速建立小工具更新程式碼路徑。

// WidgetProvider.cs

public void Activate(WidgetContext widgetContext)
{
    var widgetId = widgetContext.Id;

    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        localWidgetInfo.isActive = true;

        UpdateWidget(localWidgetInfo);
    }
}
public void Deactivate(string widgetId)
{
    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        localWidgetInfo.isActive = false;
    }
}

更新小工具

定義 UpdateWidget 協助程式方法來更新已啟用的小工具。 在此範例中,我們會檢查傳遞至方法的 CompactWidgetInfo 協助程序結構中的小工具名稱,然後根據要更新的小工具設定適當的範本和資料 JSON。 會使用要更新之小工具的範本、資料和自訂狀態來初始化 WidgetUpdateRequestOptions。 呼叫 WidgetManager::GetDefault 來取得 WidgetManager 類別的執行個體,然後呼叫 UpdateWidget 以傳送更新的小工具資料至小工具主機。

// WidgetProvider.cs

void UpdateWidget(CompactWidgetInfo localWidgetInfo)
{
    WidgetUpdateRequestOptions updateOptions = new WidgetUpdateRequestOptions(localWidgetInfo.widgetId);

    string? templateJson = null;
    if (localWidgetInfo.widgetName == "Weather_Widget")
    {
        templateJson = weatherWidgetTemplate.ToString();
    }
    else if (localWidgetInfo.widgetName == "Counting_Widget")
    {
        templateJson = countWidgetTemplate.ToString();
    }

    string? dataJson = null;
    if (localWidgetInfo.widgetName == "Weather_Widget")
    {
        dataJson = "{}";
    }
    else if (localWidgetInfo.widgetName == "Counting_Widget")
    {
        dataJson = "{ \"count\": " + localWidgetInfo.customState.ToString() + " }";
    }

    updateOptions.Template = templateJson;
    updateOptions.Data = dataJson;
    // You can store some custom state in the widget service that you will be able to query at any time.
    updateOptions.CustomState= localWidgetInfo.customState.ToString();
    WidgetManager.GetDefault().UpdateWidget(updateOptions);
}

啟動時初始化啟用的小工具清單

當我們第一次初始化小工具提供者時,最好詢問 WidgetManager 我們的提供者目前是否取用任何執行中的小工具。 這有助於在電腦重新啟動或提供者當機時,將應用程式復原到先前的狀態。 呼叫 WidgetManager.GetDefault 以取得應用程式的預設小工具管理員執行個體。 然後呼叫 GetWidgetInfos,這會傳回 WidgetInfo 物件的陣列。 將小工具識別碼、名稱和自訂狀態複製到協助程序結構 CompactWidgetInfo,並將它儲存至 RunningWidgets 成員變數。 將下列程式碼貼到 WidgetProvider 類別的類別定義中。

// WidgetProvider.cs

public WidgetProvider()
{
    var runningWidgets = WidgetManager.GetDefault().GetWidgetInfos();

    foreach (var widgetInfo in runningWidgets)
    {
        var widgetContext = widgetInfo.WidgetContext;
        var widgetId = widgetContext.Id;
        var widgetName = widgetContext.DefinitionId;
        var customState = widgetInfo.CustomState;
        if (!RunningWidgets.ContainsKey(widgetId))
        {
            CompactWidgetInfo runningWidgetInfo = new CompactWidgetInfo() { widgetId = widgetName, widgetName = widgetId };
            try
            {
                // If we had any save state (in this case we might have some state saved for Counting widget)
                // convert string to required type if needed.
                int count = Convert.ToInt32(customState.ToString());
                runningWidgetInfo.customState = count;
            }
            catch
            {

            }
            RunningWidgets[widgetId] = runningWidgetInfo;
        }
    }
}

實作 Class Factory,以依要求具現化 WidgetProvider

為了讓小工具主機與小工具提供者通訊,我們必須呼叫 CoRegisterClassObject。 此函式需要我們建立 IClassFactory 的實作,以建立 WidgetProvider 類別的類別物件。 我們將在獨立協助程式類別中實作我們的 class factory。

在 Visual Studio 的 [方案總管] 中,以滑鼠右鍵按一下 ExampleWidgetProvider 專案,然後選取 [新增] > [類別]。 在 [新增類別] 對話方塊中,命名類別為「FactoryHelper」,並按一下 [新增]

以下列程式碼取代 FactoryHelper.cs 檔案的內容。 此程式碼會定義 IClassFactory 介面,並實作其兩種方法 CreateInstanceLockServer。 此程式碼是實作 Class Factory 的典型範本,並不專屬於小工具提供者的功能,但我們指出所建立的類別物件會實作 IWidgetProvider 介面。

// FactoryHelper.cs

using Microsoft.Windows.Widgets.Providers;
using System.Runtime.InteropServices;
using WinRT;

namespace COM
{
    static class Guids
    {
        public const string IClassFactory = "00000001-0000-0000-C000-000000000046";
        public const string IUnknown = "00000000-0000-0000-C000-000000000046";
    }

    /// 
    /// IClassFactory declaration
    /// 
    [ComImport, ComVisible(false), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid(COM.Guids.IClassFactory)]
    internal interface IClassFactory
    {
        [PreserveSig]
        int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject);
        [PreserveSig]
        int LockServer(bool fLock);
    }

    [ComVisible(true)]
    class WidgetProviderFactory<T> : IClassFactory
    where T : IWidgetProvider, new()
    {
        public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject)
        {
            ppvObject = IntPtr.Zero;

            if (pUnkOuter != IntPtr.Zero)
            {
                Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION);
            }

            if (riid == typeof(T).GUID || riid == Guid.Parse(COM.Guids.IUnknown))
            {
                // Create the instance of the .NET object
                ppvObject = MarshalInspectable<IWidgetProvider>.FromManaged(new T());
            }
            else
            {
                // The object that ppvObject points to does not support the
                // interface identified by riid.
                Marshal.ThrowExceptionForHR(E_NOINTERFACE);
            }

            return 0;
        }

        int IClassFactory.LockServer(bool fLock)
        {
            return 0;
        }

        private const int CLASS_E_NOAGGREGATION = -2147221232;
        private const int E_NOINTERFACE = -2147467262;

    }
}

建立 GUID,代表小工具提供者的 CLSID

接下來,您必須建立 GUID,代表將用來識別 COM 啟用小工具提供者的 CLSID。 封裝應用程式時也會使用相同的值。 透過前往 [工具] > [建立 GUID] 在 Visual Studio 建立 GUID。 選取登錄格式選項,然後按下 [複製],然後將它貼到文字檔中,以便稍後複製它。

向 OLE 註冊小工具提供者類別物件

在可執行檔的 Program.cs 檔案中,我們將呼叫 CoRegisterClassObject 向 OLE 註冊我們的小工具提供者,讓小工具主機可以與其互動。 將 Program.cs 的內容取代為下列程式碼。 此程式碼會匯入 CoRegisterClassObject 函式並加以呼叫,並在上一個步驟中定義的 WidgetProviderFactory 介面中傳遞。 請務必更新 CLSID_Factory 變數宣告,以使用您在上一個步驟中產生的 GUID。

// Program.cs

using System.Runtime.InteropServices;
using ComTypes = System.Runtime.InteropServices.ComTypes;
using Microsoft.Windows.Widgets;
using ExampleWidgetProvider;
using COM;
using System;

[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();

[DllImport("ole32.dll")]

static extern int CoRegisterClassObject(
            [MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
            [MarshalAs(UnmanagedType.IUnknown)] object pUnk,
            uint dwClsContext,
            uint flags,
            out uint lpdwRegister);

[DllImport("ole32.dll")] static extern int CoRevokeClassObject(uint dwRegister);

Console.WriteLine("Registering Widget Provider");
uint cookie;

Guid CLSID_Factory = Guid.Parse("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
CoRegisterClassObject(CLSID_Factory, new WidgetProviderFactory<WidgetProvider>(), 0x4, 0x1, out cookie);
Console.WriteLine("Registered successfully. Press ENTER to exit.");
Console.ReadLine();

if (GetConsoleWindow() != IntPtr.Zero)
{
    Console.WriteLine("Registered successfully. Press ENTER to exit.");
    Console.ReadLine();
}
else
{
    // Wait until the manager has disposed of the last widget provider.
    using (var emptyWidgetListEvent = WidgetProvider.GetEmptyWidgetListEvent())
    {
        emptyWidgetListEvent.WaitOne();
    }

    CoRevokeClassObject(cookie);
}

請注意,此程式碼範例會匯入 GetConsoleWindow 函式,以判斷應用程式是否以主控台應用程式的形式執行,這是本逐步解說的預設行為。 如果函式傳回有效的指標,我們會將偵錯資訊寫入主控台。 否則,應用程式會以 Windows 應用程式的形式執行。 在此案例下,我們會等待在 DeleteWidget 方法中設定的事件,當已啟用的小工具提供者清單是空的,就會結束應用程式。 如需將範例主控台應用程式轉換成 Windows 應用程式的資訊,請參閱將主控台應用程式轉換成 Windows 應用程式

封裝小工具提供者應用程式

在目前的版本中,只有已封裝的應用程式可以註冊為小工具提供者。 下列步驟會引導您完成封裝應用程式並更新應用程式資訊清單的流程,以向作業系統將應用程式註冊為小工具提供者。

建立 MSIX 封裝專案

在 [方案總管] 中,以滑鼠右鍵按一下方案並選取 [新增] > [新增專案...]。在 [新增專案] 對話方塊中,選取「Windows 應用程式封裝專案」範本,然後按一下 [下一步]。 將專案名稱設定為「ExampleWidgetProviderPackage」,然後按一下 [建立]。 出現提示時,將目標版本設定為版本 1809 或更新版本,然後按一下 [確定]。 接下來,以滑鼠右鍵按一下 ExampleWidgetProviderPackage 專案,然後選取 [新增] > [專案參考]。 選取 ExampleWidgetProvider 專案,然後按一下 [確定]。

將 Windows 應用程式 SDK 套件參考新增到封裝專案

您必須將 Windows 應用程式 SDK nuget 套件的參考新增至 MSIX 封裝專案。 在 [方案總管] 中,按兩下 ExampleWidgetProviderPackage 專案,以開啟 ExampleWidgetProviderPackage.wapproj 檔案。 在專案元素中新增以下 xml。

<!--ExampleWidgetProviderPackage.wapproj-->
<ItemGroup>
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221116.1">
        <IncludeAssets>build</IncludeAssets>
    </PackageReference>  
</ItemGroup>

注意

請確定 PackageReference 元素中指定的版本 符合您在上一個步驟中所參考的最新穩定版本。

如果電腦上已安裝正確的 Windows 應用程式 SDK 版本,而且您不想在套件中搭配 SDK 執行時間,您可以在 ExampleWidgetProviderPackage 專案的 Package.appxmanifest 檔案中指定套件相依性。

<!--Package.appxmanifest-->
...
<Dependencies>
...
    <PackageDependency Name="Microsoft.WindowsAppRuntime.1.2-preview2" MinVersion="2000.638.7.0" Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" />
...
</Dependencies>
...

更新套件資訊清單

在 [方案總管] 中以滑鼠右鍵按一下 Package.appxmanifest 檔案,然後選取 [檢視程式碼] 以開啟資訊清單 xml 檔案。 接下來,您需要為我們將使用的應用程式套件延伸模組新增一些命名空間宣告。 將下列命名空間定義新增至最上層 Package 元素。

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

在 Application 元素內,建立名為 Extensions 的新空白元素。 請確定這會在 uap:VisualElements 的結尾標籤之後。

<!-- Package.appxmanifest -->
<Application>
...
    <Extensions>

    </Extensions>
</Application>

我們需要新增的第一個延伸模組是 ComServer 延伸模組。 這會向作業系統註冊可執行檔的進入點。 此延伸模組是已封裝的應用程式,相當於藉由設定登錄機碼來註冊 COM 伺服器,而且不是小工具提供者特有的。將下列 com:Extension 元素新增為 Extensions 元素的子系。 將 com:Class 元素 Id 屬性中的 GUID 變更為您在上一個步驟中產生的 GUID。

<!-- Package.appxmanifest -->
<Extensions>
    <com:Extension Category="windows.comServer">
        <com:ComServer>
            <com:ExeServer Executable="ExampleWidgetProvider\ExampleWidgetProvider.exe" DisplayName="ExampleWidgetProvider">
                <com:Class Id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" DisplayName="ExampleWidgetProvider" />
            </com:ExeServer>
        </com:ComServer>
    </com:Extension>
</Extensions>

接下來,新增將應用程式註冊為小工具提供者的延伸模組。 將 uap3:Extension 元素貼入下列程式碼片段,做為 Extensions 元素的子系。 請務必將 COM 元素的 ClassId 屬性取代為您在先前步驟中使用的 GUID。

<!-- Package.appxmanifest -->
<Extensions>
    ...
    <uap3:Extension Category="windows.appExtension">
        <uap3:AppExtension Name="com.microsoft.windows.widgets" DisplayName="WidgetTestApp" Id="ContosoWidgetApp" PublicFolder="Public">
            <uap3:Properties>
                <WidgetProvider>
                    <ProviderIcons>
                        <Icon Path="Images\StoreLogo.png" />
                    </ProviderIcons>
                    <Activation>
                        <!-- Apps exports COM interface which implements IWidgetProvider -->
                        <CreateInstance ClassId="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
                    </Activation>

                    <TrustedPackageFamilyNames>
                        <TrustedPackageFamilyName>Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe</TrustedPackageFamilyName>
                    </TrustedPackageFamilyNames>

                    <Definitions>
                        <Definition Id="Weather_Widget"
                            DisplayName="Weather Widget"
                            Description="Weather Widget Description"
                            AllowMultiple="true">
                            <Capabilities>
                                <Capability>
                                    <Size Name="small" />
                                </Capability>
                                <Capability>
                                    <Size Name="medium" />
                                </Capability>
                                <Capability>
                                    <Size Name="large" />
                                </Capability>
                            </Capabilities>
                            <ThemeResources>
                                <Icons>
                                    <Icon Path="ProviderAssets\Weather_Icon.png" />
                                </Icons>
                                <Screenshots>
                                    <Screenshot Path="ProviderAssets\Weather_Screenshot.png" DisplayAltText="For accessibility" />
                                </Screenshots>
                                <!-- DarkMode and LightMode are optional -->
                                <DarkMode />
                                <LightMode />
                            </ThemeResources>
                        </Definition>
                        <Definition Id="Counting_Widget"
                                DisplayName="Microsoft Counting Widget"
                                Description="Couting Widget Description">
                            <Capabilities>
                                <Capability>
                                    <Size Name="small" />
                                </Capability>
                            </Capabilities>
                            <ThemeResources>
                                <Icons>
                                    <Icon Path="ProviderAssets\Counting_Icon.png" />
                                </Icons>
                                <Screenshots>
                                    <Screenshot Path="ProviderAssets\Counting_Screenshot.png" DisplayAltText="For accessibility" />
                                </Screenshots>
                                <!-- DarkMode and LightMode are optional -->
                                <DarkMode>

                                </DarkMode>
                                <LightMode />
                            </ThemeResources>
                        </Definition>
                    </Definitions>
                </WidgetProvider>
            </uap3:Properties>
        </uap3:AppExtension>
    </uap3:Extension>
</Extensions>

如需所有這些元素的詳細描述和格式資訊,請參閱小工具提供者套件資訊清單 XML 格式

將圖示和其他影像新增至封裝專案

在 [方案總管] 中,以右鍵按一下 [ExampleWidgetProviderPackage],然後選取 [新增] > [新增資料夾]。 將此資料夾命名為 ProviderAssets,因為這是上一個步驟中 Package.appxmanifest 所使用的。 這就是我們將儲存小工具圖示螢幕擷取畫面的位置。 當您新增所需的圖示和螢幕擷取畫面之後,請確定影像名稱與您 Package.appxmanifest Path=ProviderAssets\ 之後的名稱相符,或小工具不會顯示在小工具主機中。

如需螢幕擷取畫面影像的設計需求,以及當地語系化螢幕擷取畫面命名慣例的相關資訊,請參閱整合小工具選擇器

測試小工具提供者

請確定您已從 [方案平台] 下拉式清單中選取符合您開發電腦的架構,例如「x64」。 在 [方案總管] 中,以右鍵按一下方案,然後選取 [建置解決方案]。 完成此動作後,以滑鼠右鍵按下您的 ExampleWidgetProviderPackage,然後選取 [部署]。 在當前版本中,唯一受支援的小工具主機是 Widgets Board。 若要查看小工具,您必須開啟 Widgets Board,然後選取右上方的 [新增小工具]。 捲動至可用的小工具底部,您應該會看到本教學課程中建立的模擬 [天氣小工具][Microsoft 計數小工具]。 按一下小工具,將它們釘選到您的小工具面板,並測試其功能。

對小工具提供者進行偵錯

釘選小工具之後,Widget 平台會啟動您的小工具提供者應用程式,以接收和傳送小工具的相關資訊。 若要偵錯執行中的小工具,您可以將偵錯工具附加至執行中的小工具提供者應用程式,或者您可以設定 Visual Studio 在啟動後自動開始偵錯小工具提供者流程。

若要附加至執行中的流程:

  1. 在 Visual Studio 按一下 [偵錯] > [附加至處理序]。
  2. 篩選流程,並尋找您想要的小工具提供者應用程式。
  3. 連結偵錯工具。

若要在一開始啟動偵錯程式時自動將偵錯程式附加至流程:

  1. 在 Visual Studio 中,按一下 [偵錯] > [其他偵錯目標] > [偵錯已安裝的應用程式套件]。
  2. 篩選套件,並尋找所需的小工具提供者套件。
  3. 選取它並核取顯示 [不要啟動] 的方塊,但在啟動程式碼時偵錯我的程式碼。
  4. 按一下 [附加]

將主控台應用程式轉換為 Windows 應用程式

若要將本逐步解說中建立的主控台應用程式轉換為 Windows 應用程式,請以滑鼠右鍵按一下 [方案總管] 中的 ExampleWidgetProvider 專案,然後選取 [屬性]。 在 [應用程式] > [一般] 下,將 [輸出類型] 從 [主控台應用程式] 變更為 [Windows 應用程式]。

顯示 C# 小工具提供者專案屬性的螢幕擷取畫面,其中輸出類型設定為 Windows 應用程式

發佈您的小工具

在開發並測試您的小工具之後,您可以在 Microsoft Store 上發佈您的應用程式,使用者才能在其裝置上安裝您的小工具。 如需發佈應用程式的逐步指引,請參閱在 Microsoft Store 中發佈您的應用程式

小工具 Store 集合

在 Microsoft Store 上發佈您的應用程式之後,您可以要求您的應用程式包含在小工具 Store 集合中,以協助使用者探索具有 Windows 小工具的應用程式。 若要提交您的要求,請參閱提交小工具資訊,以取得 Store 集合的新增功能

Microsoft Store 的螢幕擷取畫面,其中顯示小工具集合,可讓使用者探索具有 Windows Widgets 的應用程式。

實作小工具自訂

從 Windows 應用程式 SDK 1.4 開始,小工具可以支援使用者自訂。 實作此功能時,[自訂小工具] 選項會新增至 [取消釘選小工具] 選項上方的省略符號功能表。

顯示小工具的螢幕擷取畫面,其中顯示自訂對話框。

下列步驟摘要說明小工具自訂流程。

  1. 在一般作業中,小工具提供者會以範本和資料承載回應小工具主機的要求,以取得一般小工具體驗。
  2. 使用者按下省略符號功能表中的 [自訂小工具] 按鈕。
  3. 小工具會在小工具提供者上引發 OnCustomizationRequested 事件,指出使用者已要求小工具自訂體驗。
  4. 小工具提供者會設定內部旗標,指出小工具處於自訂模式。 在自訂模式中,小工具提供者會傳送小工具自訂 UI 的 JSON 範本,而不是一般小工具 UI。
  5. 在自訂模式中,小工具提供者會在使用者與自訂 UI 互動時收到 OnActionInvoked 事件,並根據使用者的動作調整其內部設定和行為。
  6. 當與 OnActionInvoked 事件相關聯的動作是應用程式定義的「結束自訂」動作時,小工具提供者會重設其內部旗標,指出它不再處於自訂模式,並繼續傳送視覺和資料 JSON 範本提供一般小工具體驗,以反映自訂期間要求的變更。
  7. 小工具提供者會將自訂選項保存到磁碟或雲端,以便在小工具提供者的調用之間保留變更。

注意

對於使用 Windows 應用程式 SDK 建置的小工具,Windows Widget Board 有已知的錯誤,其會在自訂卡片顯示之後,導致省略符號功能表變得沒有回應。

在一般小工具自訂案例中,使用者會選擇小工具上顯示的資料,或調整小工具的視覺呈現。 為了簡單起見,本節中的範例會新增自訂行為,讓使用者重設先前步驟中實作之計數小工具的計數器。

注意

只有 Windows 應用程式 SDK 1.4 和更新版本才支援小工具自訂。 請務必將專案中的參考更新為最新版的 Nuget 封裝。

更新套件資訊清單以宣告自訂支援

若要讓小工具主機知道小工具支援自訂,請將屬性 IsCustomizable 新增至小工具的 Definition 元素,並將其設定為 true。

...
<Definition Id="Counting_Widget"
    DisplayName="Microsoft Counting Widget"
    Description="CONFIG counting widget description"
    IsCustomizable="true">
...

追蹤小工具何時處於自訂模式

本文中的範例會使用協助程式結構 CompactWidgetInfo 來追蹤作用中小工具的目前狀態。 新增 inCustomization 欄位,此欄位將用來追蹤小工具主機預期我們傳送自訂 JSON 範本的時間,而不是傳送一般小工具範本。

// WidgetProvider.cs
public class CompactWidgetInfo
{
    public string widgetId { get; set; }
    public string widgetName { get; set; }
    public int customState = 0;
    public bool isActive = false;
    public bool inCustomization = false;
}

實作 IWidgetProvider2

小工具自訂功能會透過 IWidgetProvider2 介面公開。 更新 WidgetProvider 類別定義以實作這個介面。

// WidgetProvider.cs
internal class WidgetProvider : IWidgetProvider, IWidgetProvider2

新增 IWidgetProvider2 介面的 OnCustomizationRequested 回撥。 這個方法會使用與我們所使用的其他回撥相同的模式。 我們會從 WidgetContext 取得要自訂的小工具識別碼,然後找到與該小工具相關聯的 CompactWidgetInfo 協助程式,並設定 inCustomization 欄位為 true。

// WidgetProvider.cs
public void OnCustomizationRequested(WidgetCustomizationRequestedArgs customizationInvokedArgs)
{
    var widgetId = customizationInvokedArgs.WidgetContext.Id;
    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        localWidgetInfo.inCustomization = true;
        UpdateWidget(localWidgetInfo);
    }
}

現在,宣告字串變數,以定義小工具自訂 UI 的 JSON 範本。 在此範例中,我們有 [重設計數器] 按鈕和 [結束自訂] 按鈕,都可向提供者發出訊號以返回一般小工具行為。 將此定義放在其他範本定義旁邊。

// WidgetProvider.cs
const string countWidgetCustomizationTemplate = @"
{
    ""type"": ""AdaptiveCard"",
    ""actions"" : [
        {
            ""type"": ""Action.Execute"",
            ""title"" : ""Reset counter"",
            ""verb"": ""reset""
            },
            {
            ""type"": ""Action.Execute"",
            ""title"": ""Exit customization"",
            ""verb"": ""exitCustomization""
            }
    ],
    ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"",
    ""version"": ""1.5""
}";

在 UpdateWidget 中傳送自訂範本

接下來,我們將更新 UpdateWidget 協助程式方法,以將資料和視覺 JSON 範本傳送至小工具主機。 當我們更新計數小工具時,我們會根據 inCustomization 欄位的值,傳送一般小工具範本或自訂範本。 為了簡潔起見,此程式碼片段會省略與自訂無關的程式碼。

// WidgetProvider.cs
void UpdateWidget(CompactWidgetInfo localWidgetInfo)
{
    ...
    else if (localWidgetInfo.widgetName == "Counting_Widget")
    {
        if (!localWidgetInfo.inCustomization)
        {
            templateJson = countWidgetTemplate.ToString();
        }
        else
        {
            templateJson = countWidgetCustomizationTemplate.ToString();
        }
    
    }
    ...
    updateOptions.Template = templateJson;
    updateOptions.Data = dataJson;
    // You can store some custom state in the widget service that you will be able to query at any time.
    updateOptions.CustomState = localWidgetInfo.customState.ToString();
    WidgetManager.GetDefault().UpdateWidget(updateOptions);
}

回應自訂動作

當使用者在我們的自訂範本中與輸入互動時,它會呼叫與使用者與一般小工具體驗互動時相同的 OnActionInvoked 處理程式。 為了支援自訂,我們要從自訂 JSON 範本尋找 verb「reset」和「exitCustomization」。 如果動作是用於 [重設計數器] 按鈕,我們會將協助程式結構 customState 欄位中保留的計數器重設為 0。 如果動作是用於 [結束自訂] 按鈕,我們會將 inCustomization 欄位設定為 false,以便在我們呼叫 UpdateWidget 時,我們的協助程式方法會傳送一般 JSON 範本,而不是自訂範本。

// WidgetProvider.cs
public void OnActionInvoked(WidgetActionInvokedArgs actionInvokedArgs)
{
    var verb = actionInvokedArgs.Verb;
    if (verb == "inc")
    {
        var widgetId = actionInvokedArgs.WidgetContext.Id;
        // If you need to use some data that was passed in after
        // Action was invoked, you can get it from the args:
        var data = actionInvokedArgs.Data;
        if (RunningWidgets.ContainsKey(widgetId))
        {
            var localWidgetInfo = RunningWidgets[widgetId];
            // Increment the count
            localWidgetInfo.customState++;
            UpdateWidget(localWidgetInfo);
        }
    } 
    else if (verb == "reset") 
    {
        var widgetId = actionInvokedArgs.WidgetContext.Id;
        var data = actionInvokedArgs.Data;
        if (RunningWidgets.ContainsKey(widgetId))
        {
            var localWidgetInfo = RunningWidgets[widgetId];
            // Reset the count
            localWidgetInfo.customState = 0;
            localWidgetInfo.inCustomization = false;
            UpdateWidget(localWidgetInfo);
        }
    }
    else if (verb == "exitCustomization")
    {
        var widgetId = actionInvokedArgs.WidgetContext.Id;
        var data = actionInvokedArgs.Data;
        if (RunningWidgets.ContainsKey(widgetId))
        {
            var localWidgetInfo = RunningWidgets[widgetId];
            // Stop sending the customization template
            localWidgetInfo.inCustomization = false;
            UpdateWidget(localWidgetInfo);
        }
    }
}

現在,當您部署小工具時,您應該會在省略符號功能表中看到 [自訂小工具] 按鈕。 按一下 [自訂] 按鈕會顯示您的自訂範本。

小工具自訂 UI 的螢幕擷取畫面。

按一下 [重設計數器] 按鈕,將計數器重設為0。 按一下 [結束自訂] 按鈕,返回小工具的一般行為。