本文會逐步指導您建立實作 IWidgetProvider 介面的簡單元件提供者。 小工具主機會叫用這個介面的方法,以要求定義小工具的資料,或讓小工具提供者回應小工具上的使用者動作。 小工具提供者可以支援單一小工具或多個小工具。 在此範例中,我們將定義兩個不同的小工具。 其中一個小工具是一個模擬天氣小工具,展示了 調適型卡片 框架所提供的一些格式選項。 第二個小工具將通過維護一個計數器來示範使用者操作和自訂小工具狀態功能。每當使用者點擊小工具上顯示的按鈕時,該計數器會遞增。
本文中的範例程式碼改編自 Windows 應用程式 SDK Widgets Sample。 若要使用 C++/WinRT 實作小工具提供者,請參閱在 win32 應用程式 (C++/WinRT) 中實作小工具提供者。
必要條件
- 一台可供測試 Widgets Board 的 Windows 11 裝置。
- 您的裝置必須啟用開發人員模式。 如需詳細資訊,請參閱 開發人員的設定。
- Visual Studio 2022 或更新版本,搭配 WinUI 應用程式開發工作負載。
建立新的 C# 主控台應用程式
在 Visual Studio 裡,建立一個新專案。 在建立新專案對話框中,將語言篩選器設為「C#」,平台篩選器設為Windows,然後選擇 Console App 專案範本。 將新專案命名為「ExampleWidgetProvider」。 當提示時,將目標 .NET 版本設為 8.0。
專案載入時,在 方案總管 右鍵點擊專案名稱,選擇 Properties。 在
要更新你的project以支援 .NET 8.0,請在 方案總管 中右鍵點擊project名稱,選擇 編輯Project檔案。 在 PropertyGroup 中,加入下列 RuntimeIdentifiers 元素。
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
請注意,此逐步解說採用會在啟動小工具時顯示主控台視窗的主控台應用程式,以方便偵錯。 當你準備好發佈你的小工具供應商應用程式時,你可以依照將你的控制台應用程式轉換成Windows應用程式的步驟進行操作。
新增 Windows 應用程式 SDK 的參考資料
本攻略使用 Microsoft.WindowsAppSDK 2.5.1。 在 方案總管,右鍵點擊 Dependencies,選擇 管理 NuGet 套件...。在 NuGet 套件管理器中,選擇 Browse 標籤,搜尋「Microsoft。WindowsAppSDK」。 選擇版本 2.5.1,或從 Windows 應用程式 SDK 發布頻道中支援的較新穩定版本,然後按一下 安裝。 在應用程式和封裝專案中使用相同的套件版本。 這是逐步解說中使用的版本,不是引入小工具 API 的最低版本。
新增 WidgetProvider 類別來處理小工具作業
在Visual Studio中,右鍵點擊 ExampleWidgetProvider 中的 專案,選擇 Add->Class。 在 [新增類別] 對話方塊中,將類別命名為「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 文件的資訊,請參見 用 調適型卡片 Designer 建立小工具範本。
實作 Windows 小工具的應用程式可以在支援標頭自訂的主機中自訂其小工具所顯示的標頭。 如需詳細資訊,請參閱 自訂小工具標頭區域。
注意
網頁小工具可以顯示由 URL 提供的 HTML。 提供者仍提供 Adaptive Card JSON payload 以指定該 URL,並實作 IWidgetProvider。 資源與訊息 API 則在 Windows 應用程式 SDK 1.6.3 中引入。 關於設定與主機需求,請參見 Web 小工具提供者。
// 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://raw.githubusercontent.com/microsoft/AdaptiveCards/59402c8d43f7a69a68e0b4fccc08f8a76fc98ccd/source/nodejs/adaptivecards-site/themes/adaptivecards/source/content/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://raw.githubusercontent.com/microsoft/AdaptiveCards/59402c8d43f7a69a68e0b4fccc08f8a76fc98ccd/source/nodejs/adaptivecards-site/themes/adaptivecards/source/content/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 方法
在接下來的幾節中,我們將實作 IWidgetProvider 介面的方法。 本文稍後會展示這些方法實作中呼叫的協助程式方法 UpdateWidget。
注意
傳送至 IWidgetProvider 介面的回呼方法中的物件,僅在回呼過程中有效。 您不應儲存這些物件的參考,因為它們在回呼函式之外的行為是未定的。
CreateWidget
當使用者在 widget 主機中固定其中一個應用程式的 widget 時,widget 主機會呼叫 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);
}
刪除Widget
當使用者取消釘選您應用程式的其中一個小工具時,小工具主機會呼叫 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 方法中,透過檢查傳遞至方法的 Verb 屬性,取得動詞值。 如果 verb 是「inc」,則我們知道將為小工具的自訂狀態遞增計數。 從 WidgetActionInvokedArgs 取得 WidgetContext 物件,然後再取得其 Id 屬性來識別正在更新的 widget 實例。 在我們已啟用的小工具映射中找到具有指定 ID 的項目,然後更新用於儲存遞增數量的自訂狀態值。 最後,使用 UpdateWidget 輔助函式,以新值更新 Widget 的內容。
// 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 範本/資料至 widget 主機。 您也可以根據 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 方法來通知小工具提供者,即小工具主機目前有興趣從提供者接收更新的內容。 例如,這可能表示使用者目前正主動檢視 Widget 宿主。 呼叫 Deactivate 方法以通知小工具提供者小工具主機不再要求內容更新。 這兩種方法定義了一段時間,在此期間,小工具主機最感興趣顯示最新的內容。 小工具提供者可以隨時將更新傳送至小工具,例如回應推送通知,但正如任何背景工作的特點,務必在提供最新內容的同時,考量資源的使用情況,如電池使用壽命等。
會針對每個小工具分別呼叫 Activate 和 Deactivate。 本範例會追蹤 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;
updateOptions.IsPlaceholderContent = localWidgetInfo.widgetName == "Weather_Widget";
// 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);
}
將 WidgetUpdateRequestOptions.IsPlaceholderContent 設為 true,對於尚未個人化或可執行的內容。 模擬天氣小工具顯示一個固定位置,因此此範例將其標記為佔位內容。 主辦方可能會優先使用包含客製化內容的小工具,而非此範例天氣小工具。 當您提供個人化內容時,請將該屬性設為 false。 此特性是在 Windows 應用程式 SDK 1.6.6 中新增的。
啟動時初始化已啟用的小工具列表
當我們的 widget 提供者首次初始化時,最好詢問 WidgetManager 我們的提供者目前是否正在提供任何執行中的小工具。 這有助於在電腦重新啟動或提供者當機時,將應用程式復原到先前的狀態。 呼叫 WidgetManager.GetDefault 以取得應用程式的預設 widget 管理程式執行個體。 然後呼叫 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 = widgetId, widgetName = widgetName };
if (!string.IsNullOrEmpty(customState))
{
if (int.TryParse(customState, out int count))
{
runningWidgetInfo.customState = count;
}
else
{
Console.Error.WriteLine($"Invalid saved count for widget {widgetId}; resetting to 0.");
}
}
RunningWidgets[widgetId] = runningWidgetInfo;
}
}
}
實作一個類別工廠,以便根據要求實例化 WidgetProvider。
為了讓小工具主機與小工具提供者通訊,我們必須呼叫 CoRegisterClassObject。 此函式需要我們建立 IClassFactory 的實作,以建立 WidgetProvider 類別的類別物件。 我們將在獨立協助程式類別中實作我們的 class factory。
在Visual Studio中,右鍵點擊 ExampleWidgetProvider 中的 專案,選擇 Add->Class。 在 [新增類別] 對話方塊中,命名類別為「FactoryHelper」,並按一下 [新增]。
以下列程式碼取代 FactoryHelper.cs 檔案的內容。 此程式代碼會定義 IClassFactory 介面,並實作其兩種方法 CreateInstance 和 LockServer。 此程式碼是實作 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)
{
return CLASS_E_NOAGGREGATION;
}
IntPtr provider = IntPtr.Zero;
try
{
provider = MarshalInspectable<IWidgetProvider>.FromManaged(new T());
return Marshal.QueryInterface(provider, in riid, out ppvObject);
}
catch (Exception exception)
{
Console.Error.WriteLine(exception);
return Marshal.GetHRForException(exception);
}
finally
{
if (provider != IntPtr.Zero)
{
Marshal.Release(provider);
}
}
}
int IClassFactory.LockServer(bool fLock)
{
return 0;
}
private const int CLASS_E_NOAGGREGATION = -2147221232;
}
}
建立一個 GUID,代表小工具提供者的 CLSID
接下來,您必須建立一個 GUID,以代表將用來識別 COM 啟用小工具提供者的 CLSID。 封裝應用程式時也會使用相同的值。 在 Visual Studio 中產生 GUID,請前往 Tools->Create 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);
WinRT.ComWrappersSupport.InitializeComWrappers();
Guid CLSID_Factory = Guid.Parse("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
var factory = new WidgetProviderFactory<WidgetProvider>();
Marshal.ThrowExceptionForHR(
CoRegisterClassObject(CLSID_Factory, factory, 0x4, 0x1, out uint cookie));
try
{
if (GetConsoleWindow() != IntPtr.Zero)
{
Console.WriteLine("Registered successfully. Press ENTER to exit.");
Console.ReadLine();
}
else
{
WidgetProvider.GetEmptyWidgetListEvent().WaitOne();
}
}
finally
{
int revokeResult = CoRevokeClassObject(cookie);
if (revokeResult < 0)
{
System.Diagnostics.Trace.TraceError($"CoRevokeClassObject failed: 0x{revokeResult:X8}");
}
GC.KeepAlive(factory);
}
請注意,此程式碼範例會匯入 GetConsoleWindow 函式,以判斷應用程式是否以主控台應用程式的形式執行,這是本逐步解說的預設行為。 如果函式回傳有效的指標,我們會將除錯資訊寫入主控台。 否則,應用程式會以 Windows 應用程式的身份執行。 在這種情況下,我們會等待在啟用的小工具清單為空時於 DeleteWidget 方法中設定的事件,然後退出應用程式。 關於如何將範例控制台應用程式轉換為Windows應用程式的資訊,請參見 將你的控制台應用程式轉換成Windows應用程式。
注意
等待最後一個 Widget 被刪除是此範例的簡化生命週期策略,而非 IWidgetProvider 的要求。 主機可以在小工具仍釘選時啟用並解除提供者。 在生產服務提供者中,獨立管理 COM 伺服器的生命週期,啟動時還原已儲存的小工具狀態,並在回調同時執行時同步存取共享狀態。 請參閱 IWidgetProvider 備註。
封裝小工具提供者應用程式
本逐步解說透過 MSIX 套件資訊清單註冊桌面小工具提供者,並使用 Windows 應用程式 SDK 執行階段的相依於框架部署。 以下步驟會將你的應用程式封裝並註冊其提供者。 如果你改為選擇自包含部署,請依照 部署自含式應用程式,並在使用 Windows 應用程式封裝專案時,包含兩個專案的設定。
建立 MSIX 封裝專案
在 方案總管,右鍵點擊你的解答,選擇 Add->New Project...。在 新增 project對話框中,選擇「Windows 應用程式封裝Project」範本,並點擊 Next。 將專案名稱設定為「ExampleWidgetProviderPackage」,然後按一下 [建立]。 出現提示時,將目標版本設定為版本 1809 或更新版本,然後按一下 [確定]。 接著,右鍵點擊 ExampleWidgetProviderPackage project,選擇 Add->Project reference。 選取 ExampleWidgetProvider 專案,然後按一下 [確定]。
將 Windows 應用程式 SDK 套件參考加入打包專案
你需要在 MSIX 打包專案中新增 Windows 應用程式 SDK NuGet 套件的參考。 在 方案總管,雙擊 ExampleWidgetProviderPackage 專案以開啟 ExampleWidgetProviderPackage.wapproj 檔案。 在 Project 元素中加入以下 xml。
<!--ExampleWidgetProviderPackage.wapproj-->
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.5.1">
<IncludeAssets>build</IncludeAssets>
</PackageReference>
</ItemGroup>
注意
該程式碼片段使用版本 2.5.1,而非引入 widget API 的最低版本。 如果你在前一步選擇了後續支援的穩定版本,請更新 版本 值以使其與該版本一致。
更新套件資訊清單
在 方案總管 中右鍵點擊 Package.appxmanifest 檔案,選擇 View Code 即可開啟清單 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 元素的子系。 將 Id 屬性中的 com:Class 元素的 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="Counting 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,選擇 Add->New Folder。 將此資料夾命名為 ProviderAssets,因為這是上一個步驟中 Package.appxmanifest 所使用的。 這就是我們將儲存小工具圖示和螢幕擷取畫面的位置。 當您新增所需的圖示和螢幕擷取畫面之後,請確保影像名稱與Path=ProviderAssets\中顯示的名稱相符,否則小工具將無法在小工具主機中顯示。
如需有關螢幕擷取畫面影像設計需求,以及在地化螢幕擷取畫面命名慣例的資訊,請參閱整合小工具選擇器。
測試小工具提供者
從解決方案 平台 下拉選單選擇與你的開發機器相符的架構,例如「x64」。 在 方案總管 中,右鍵點擊你的解決方案並選擇建置解決方案,然後右鍵點選 ExampleWidgetProviderPackage,選擇部署。 在 Windows 11 上,使用 Windows+W 開啟 Widgets Board,開啟 widget 選擇器,找到 Weather Widget 和 Microsoft Counting Widget。 把它們加入看板並測試它們的功能。 選擇器的控制項和位置會隨著 Windows 和 Widgets Board 的更新而有所變化。
調適型卡片 Designer 不會提供 Windows $host 值、呼叫你的提供者回調,或強制執行 Widgets Board 的渲染邊界。 請在 Widgets 面板完成下列檢查項目:
- 確認該小工具是否出現在選擇器中,並顯示預期的名稱、圖示、截圖和描述。
- 釘選小工具並驗證其初始範本與資料。
- 把它調整到套件清單中宣告的所有大小。 檢查每個尺寸是否有剪裁、隱藏或溢出的內容。
- 測試每個 Action.Execute 操作 當
$host.isUserContextAuthenticated為真時。 當其為 false 時,隱藏不支援的動作並提供適當的備用方案;在該情境下僅支援 Action.OpenUrl。 - 停止並重新啟動提供者程序,然後確認每個置頂的小工具是否已恢復為其自訂狀態。
- 解除固定小工具並確認 DeleteWidget 已被呼叫。
對於以 CLI 為優先且使用單一專案 MSIX 而非本導覽中獨立打包專案的 C# 專案,請將 EnableMsixTooling 放在提供者專案中,將 Package.appxmanifest 設為 true,並使用 Windows 應用程式 開發 CLI 來建置並註冊開發套件:
Important
Windows 應用程式開發 CLI 目前處於 公開預覽。 功能與指令可能會在最終版本前有所變動。 使用此指令前,請 winapp run --help 先檢查已安裝版本的輸出。
winapp run ExampleWidgetProvider.csproj --configuration Release --arch x64 --no-launch
這個指令在不直接啟動應用程式的情況下註冊套件。 打開 Widgets Board,透過 widget 主機啟動 COM 伺服器。
對小工具提供者進行除錯
釘選小工具之後,Widget 平台會啟動您的小工具提供者應用程式,以接收和傳送小工具的相關資訊。 要除錯正在執行的小工具,你可以把除錯器附加到執行中的小工具提供者應用程式上,或者設定 Visual Studio 在小工具提供者程序啟動後自動開始除錯。
若要附加至執行中的程序:
- 在Visual Studio中點選 Debug -> 附加到 process。
- 篩選流程,並尋找您想要的小工具提供者應用程式。
- 連結偵錯工具。
若要在一開始啟動偵錯程式時自動將偵錯程式附加至流程:
- 在Visual Studio中點選 Debug -> 其他除錯目標 -> 除錯安裝應用程式套件。
- 篩選套件,並尋找所需的小工具提供者套件。
- 選取它並勾選方塊,該方塊顯示 "不要啟動,但在啟動時偵錯我的程式碼。"
- 按一下 新增。
把你的控制台應用程式轉換成 Windows 應用程式
要將本攻略中建立的主控台應用程式轉換為 Windows 應用程式,請在 方案總管 右鍵點擊 ExampleWidgetProvider 專案,選擇 Properties。 在 Application->General 下,將 Output 類型 從「主控台應用程式」改為「Windows 應用程式」。
若要編輯專案檔案,請將 OutputType 設為 WinExe:
<OutputType>WinExe</OutputType>
在驗證最終使用者體驗或發佈之前,先做這些檢查。 當小工具主機啟動其 COM 伺服器時,主控台提供者可開啟終端機視窗。
發佈您的小工具
在你開發並測試完小工具後,你可以將應用程式發佈到 Microsoft Store,讓使用者能在自己的裝置上安裝小工具。 關於發佈應用程式的逐步指引,請參考發佈您的應用程式於Microsoft Store。
小工具商店集合
當您的應用程式在 Microsoft Store 上架後,您可以申請將應用程式納入小工具的商店集合,幫助使用者發現具備 Windows 小工具的應用程式。 若要提交您的要求,請參閱提交小工具資訊以添加到 Store 集合中。
實現小工具自訂
從 Windows 應用程式 SDK 1.4 開始,小工具可以支援使用者自訂功能。 實作此功能時,[自訂小工具] 選項會新增至 [取消釘選小工具] 選項上方的省略符號功能表。
下列步驟摘要說明小工具自訂流程。
- 在一般作業中,小工具提供者會以範本和資料承載回應小工具主機的要求,以取得一般小工具體驗。
- 使用者按下省略號選單中的 [自訂小工具] 按鈕。
- 小工具會在小工具提供者上引發 OnCustomizationRequested 事件,指出使用者已要求小工具自訂體驗。
- 小工具提供者會設定內部旗標,指出小工具處於自訂模式。 在自訂模式中,小工具提供者會傳送小工具自訂 UI 的 JSON 範本,而不是一般小工具 UI。
- 在自訂模式中,小工具提供者會在使用者與自訂 UI 互動時收到 OnActionInvoked 事件,並根據使用者的動作調整其內部設定和行為。
- 當與 OnActionInvoked 事件相關聯的動作是應用程式定義的「結束自定義」動作時,小工具提供者會重設其內部旗標,指出它不再處於自定義模式,並繼續傳送視覺效果和數據 JSON 範本,以取得一般小工具體驗,反映自定義期間要求的變更。 使用者不需按下應用程式定義的結束自定義動作,即可關閉自定義體驗。 在這種情況下,將會觸發 IWidgetProviderAnalytics.OnAnalyticsInfoReported,而 WidgetAnalyticsInfoReportedArgs.AnalyticsJson 則會有一個 interactionKind 為 "exitCustomization"。
- 小工具提供者會將自訂選項保存到磁碟或雲端,以便在小工具提供者的調用之間保留變更。
注意
已知在使用 Windows 應用程式 SDK 建構的小工具中,Windows Widget Board 存在一個錯誤,會導致自訂卡片顯示後省略選單變得無反應。
在一般小工具自訂案例中,使用者會選擇小工具上顯示的資料,或調整小工具的視覺呈現。 為了簡單起見,本節中的範例會新增自訂行為,讓使用者重設先前步驟中實作之計數小工具的計數器。
注意
小工具自訂需要 Windows 應用程式 SDK 1.4 或更新版本。 本攻略使用 Microsoft.WindowsAppSDK 版本 2.5.1。
更新套件資訊清單以宣告自訂支援
若要讓小工具主機知道小工具支援自訂,請將屬性 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
添加 OnCustomizationRequested 回撥的實作至 IWidgetProvider2 介面。 這個方法會使用與我們所使用的其他回撥相同的模式。 我們會從 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 模板傳遞到 widget 主機。 當我們更新計數小工具時,我們會根據 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 範本尋找動詞 "reset" 和 "exitCustomization"。 「重置計數器」動作會將 customState 重設為 0,並退出自訂。 「結束自訂」動作會退出,但不會重置計數器。 兩者都將 inCustomization 設為 false,讓 UpdateWidget 傳送一般的 widget 範本。
// 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);
}
}
}
現在,當您部署小工具時,您應該會在省略符號功能表中看到 [自訂小工具] 按鈕。 按一下 [自訂] 按鈕會顯示您的自訂範本。
按一下 [重設計數器] 按鈕,將計數器重設為0。 按一下 [結束自訂] 按鈕,返回小工具的一般行為。