共用方式為


在 win32 應用中實現小工具提供者 (C++/WinRT)

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

簡單天氣小工具的螢幕快照。小工具會顯示一些天氣相關的圖形和數據,以及一些診斷文字,說明顯示中型小工具的範本。

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

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

必要條件

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

建立新的 C++/WinRT win32 主控台應用程式

在 Visual Studio 中,建立新專案。 在 [建立新專案] 對話方塊中,將語言篩選條件設定為「C++」,並將平台篩選條件設定為 Windows,然後選取 [Windows 主控台應用程式 (C++/WinRT)] 專案範本。 將新專案命名為「ExampleWidgetProvider」。 出現提示時,將應用程式的目標 Windows 版本設定為版本 1809 或更新版本。

新增 Windows 應用程式 SDK 和 Windows 實作程式庫 NuGet 套件的參考

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

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

在先行編譯標頭檔 (pch.h) 中新增下列 include 指示詞。

//pch.h 
#pragma once
#include <wil/cppwinrt.h>
#include <wil/resource.h>
...
#include <winrt/Microsoft.Windows.Widgets.Providers.h>

注意

您必須先包含 wil/cppwinrt.h 標頭,才包含任何 WinRT 標頭。

為了正確處理關閉小工具提供者應用程式,我們需要自訂 winrt::get_module_lock 實作。 我們會預先宣告 SignalLocalServerShutdown 方法,此方法將在我們的 main.cpp 檔案中定義,且會設定事件以指示應用程式結束。 將下列程式碼新增至 pch.h 檔案,就在 #pragma once 指示詞正下方,再加入其他包含項目。

//pch.h
#include <stdint.h>
#include <combaseapi.h>

// In .exe local servers the class object must not contribute to the module ref count, and use
// winrt::no_module_lock, the other objects must and this is the hook into the C++ WinRT ref counting system
// that enables this.
void SignalLocalServerShutdown();

namespace winrt
{
    inline auto get_module_lock() noexcept
    {
        struct service_lock
        {
            uint32_t operator++() noexcept
            {
                return ::CoAddRefServerProcess();
            }

            uint32_t operator--() noexcept
            {
                const auto ref = ::CoReleaseServerProcess();

                if (ref == 0)
                {
                    SignalLocalServerShutdown();
                }
                return ref;
            }
        };

        return service_lock{};
    }
}


#define WINRT_CUSTOM_MODULE_LOCK

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

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

宣告實作 IWidgetProvider 介面的類別

IWidgetProvider 介面會定義小工具主機會叫用以使用小工具提供者開始作業的方法。 以下列程式碼取代 WidgetProvider.h 檔案中的空白類別定義。 此程式碼會宣告實作 IWidgetProvider 介面的結構,並宣告介面方法的原型。

// WidgetProvider.h
struct WidgetProvider : winrt::implements<WidgetProvider, winrt::Microsoft::Windows::Widgets::Providers::IWidgetProvider>
{
    WidgetProvider();

    /* IWidgetProvider required functions that need to be implemented */
    void CreateWidget(winrt::Microsoft::Windows::Widgets::Providers::WidgetContext widgetContext);
    void DeleteWidget(winrt::hstring const& widgetId, winrt::hstring const& customState);
    void OnActionInvoked(winrt::Microsoft::Windows::Widgets::Providers::WidgetActionInvokedArgs actionInvokedArgs);
    void OnWidgetContextChanged(winrt::Microsoft::Windows::Widgets::Providers::WidgetContextChangedArgs contextChangedArgs);
    void Activate(winrt::Microsoft::Windows::Widgets::Providers::WidgetContext widgetContext);
    void Deactivate(winrt::hstring widgetId);
    /* IWidgetProvider required functions that need to be implemented */

    
};

此外,新增私用方法 UpdateWidget,這是一種協助程式方法,可將更新從提供者傳送至小工具主機。

// WidgetProvider.h
private: 

void UpdateWidget(CompactWidgetInfo const& localWidgetInfo);

準備追蹤已啟用的小工具

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

// WidgetProvider.h
struct CompactWidgetInfo
{
    winrt::hstring widgetId;
    winrt::hstring widgetName;
    int customState = 0;
    bool isActive = false;
};

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

// WidgetProvider.h
#include <unordered_map>
struct WidgetProvider : winrt::implements<WidgetProvider, winrt::Microsoft::Windows::Widgets::Providers::IWidgetProvider>
{
...
    private:
        ...
        static std::unordered_map<winrt::hstring, CompactWidgetInfo> RunningWidgets;

        

宣告小工具範本 JSON 字串

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

// WidgetProvider.h
const std::string weatherWidgetTemplate = R"(
{
    "$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 std::string countWidgetTemplate = R"(
{                                                                     
    "type": "AdaptiveCard",                                         
    "body": [                                                         
        {                                                               
            "type": "TextBlock",                                    
            "text": "You have clicked the button ${count} times"    
        },
        {
             "text":"Rendering Only if Medium",
             "type":"TextBlock",
             "$when":"${$host.widgetSize==\"medium\"}"
        },
        {
             "text":"Rendering Only if Small",
             "type":"TextBlock",
             "$when":"${$host.widgetSize==\"small\"}"
        },
        {
         "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。 在深入探討介面方法之前,請將下列幾行新增至 WidgetProvider.cpp,在 include 指示詞之後,將小工具提供者 API 提取至 winrt 命名空間,並允許存取我們在上一個步驟中宣告的對應。

注意

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

// WidgetProvider.cpp
namespace winrt
{
    using namespace Microsoft::Windows::Widgets::Providers;
}

std::unordered_map<winrt::hstring, CompactWidgetInfo> WidgetProvider::RunningWidgets{};

CreateWidget

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

// WidgetProvider.cpp
void WidgetProvider::CreateWidget(winrt::WidgetContext widgetContext)
{
    auto widgetId = widgetContext.Id();
    auto widgetName = widgetContext.DefinitionId();
    CompactWidgetInfo runningWidgetInfo{ widgetId, widgetName };
    RunningWidgets[widgetId] = runningWidgetInfo;
    
    // Update the widget
    UpdateWidget(runningWidgetInfo);
}

DeleteWidget

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

// WidgetProvider.cpp
void WidgetProvider::DeleteWidget(winrt::hstring const& widgetId, winrt::hstring const& customState)
{
    RunningWidgets.erase(widgetId);
}

OnActionInvoked

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

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

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

// WidgetProvider.cpp
void WidgetProvider::OnActionInvoked(winrt::WidgetActionInvokedArgs actionInvokedArgs)
{
    auto verb = actionInvokedArgs.Verb();
    if (verb == L"inc")
    {
        auto 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:
        auto data = actionInvokedArgs.Data();
        if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
        {
            auto& localWidgetInfo = iter->second;
            // Increment the count
            localWidgetInfo.customState++;
            UpdateWidget(localWidgetInfo);
        }
    }
}

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

OnWidgetContextChanged

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

// WidgetProvider.cpp
void WidgetProvider::OnWidgetContextChanged(winrt::WidgetContextChangedArgs contextChangedArgs)
{
    auto widgetContext = contextChangedArgs.WidgetContext();
    auto widgetId = widgetContext.Id();
    auto widgetSize = widgetContext.Size();
    if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
    {
        auto localWidgetInfo = iter->second;

        UpdateWidget(localWidgetInfo);
    }
}
    

啟用和停用

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

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

void WidgetProvider::Activate(winrt::Microsoft::Windows::Widgets::Providers::WidgetContext widgetContext)
{
    auto widgetId = widgetContext.Id();

    if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
    {
        auto& localWidgetInfo = iter->second;
        localWidgetInfo.isActive = true;

        UpdateWidget(localWidgetInfo);
    }
}

void WidgetProvider::Deactivate(winrt::hstring widgetId)
{
    if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
    {
        auto& localWidgetInfo = iter->second;
        localWidgetInfo.isActive = false;
    }
}

更新小工具

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

// WidgetProvider.cpp
void WidgetProvider::UpdateWidget(CompactWidgetInfo const& localWidgetInfo)
{
    winrt::WidgetUpdateRequestOptions updateOptions{ localWidgetInfo.widgetId };

    winrt::hstring templateJson;
    if (localWidgetInfo.widgetName == L"Weather_Widget")
    {
        templateJson = winrt::to_hstring(weatherWidgetTemplate);
    }
    else if (localWidgetInfo.widgetName == L"Counting_Widget")
    {
        templateJson = winrt::to_hstring(countWidgetTemplate);
    }

    winrt::hstring dataJson;
    if (localWidgetInfo.widgetName == L"Weather_Widget")
    {
        dataJson = L"{}";
    }
    else if (localWidgetInfo.widgetName == L"Counting_Widget")
    {
        dataJson = L"{ \"count\": " + winrt::to_hstring(localWidgetInfo.customState) + L" }";
    }

    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(winrt::to_hstring(localWidgetInfo.customState));
    winrt::WidgetManager::GetDefault().UpdateWidget(updateOptions);
}

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

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

// WidgetProvider.cpp
WidgetProvider::WidgetProvider()
{
    auto runningWidgets = winrt::WidgetManager::GetDefault().GetWidgetInfos();
    for (auto widgetInfo : runningWidgets )
    {
        auto widgetContext = widgetInfo.WidgetContext();
        auto widgetId = widgetContext.Id();
        auto widgetName = widgetContext.DefinitionId();
        auto customState = widgetInfo.CustomState();
        if (RunningWidgets.find(widgetId) == RunningWidgets.end())
        {
            CompactWidgetInfo runningWidgetInfo{ widgetId, widgetName };
            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 = std::stoi(winrt::to_string(customState));
                runningWidgetInfo.customState = count;
            }
            catch (...)
            {

            }
            RunningWidgets[widgetId] = runningWidgetInfo;
        }
    }
}

註冊 Class Factory,以依要求具現化 WidgetProvider

將定義 WidgetProvider 類別的標頭新增至應用程式 main.cpp 檔案頂端的 include。 我們也將在這裡包括 mutex。

// main.cpp
...
#include "WidgetProvider.h"
#include <mutex>

宣告將觸發應用程式結束的事件,以及將設定事件的 SignalLocalServerShutdown 函式。 在 main.cpp 中貼上下列程式碼。

// main.cpp
wil::unique_event g_shudownEvent(wil::EventOptions::None);

void SignalLocalServerShutdown()
{
    g_shudownEvent.SetEvent();
}

接下來,您必須建立 CLSID,以用來識別 COM 啟用的小工具提供者。 透過前往 [工具] > [建立 GUID] 在 Visual Studio 建立 GUID。 選取 [static const GUID =] 選項,再按下 [複製],然後將該選項貼到 main.cpp。 使用下列 C++/WinRT 語法更新 GUID 定義,並將 GUID 變數名稱設定為 widget_provider_clsid。 在封裝應用程式時,請保留 GUID 的註解版本,因為稍後您將需要此格式。

// main.cpp
...
// {80F4CB41-5758-4493-9180-4FB8D480E3F5}
static constexpr GUID widget_provider_clsid
{
    0x80f4cb41, 0x5758, 0x4493, { 0x91, 0x80, 0x4f, 0xb8, 0xd4, 0x80, 0xe3, 0xf5 }
};

將以下 Class Factory 定義新增至 main.cpp。 這主要是供小工具提供者實作非特定的未定案程式碼。 請注意,CoWaitForMultipleObjects 會在應用程式結束之前等候觸發關閉事件。

// main.cpp
template <typename T>
struct SingletonClassFactory : winrt::implements<SingletonClassFactory<T>, IClassFactory>
{
    STDMETHODIMP CreateInstance(
        ::IUnknown* outer,
        GUID const& iid,
        void** result) noexcept final
    {
        *result = nullptr;

        std::unique_lock lock(mutex);

        if (outer)
        {
            return CLASS_E_NOAGGREGATION;
        }

        if (!instance)
        {
            instance = winrt::make<WidgetProvider>();
        }

        return instance.as(iid, result);
    }

    STDMETHODIMP LockServer(BOOL) noexcept final
    {
        return S_OK;
    }

private:
    T instance{ nullptr };
    std::mutex mutex;
};

int main()
{
    winrt::init_apartment();
    wil::unique_com_class_object_cookie widgetProviderFactory;
    auto factory = winrt::make<SingletonClassFactory<winrt::Microsoft::Windows::Widgets::Providers::IWidgetProvider>>();

    winrt::check_hresult(CoRegisterClassObject(
        widget_provider_clsid,
        factory.get(),
        CLSCTX_LOCAL_SERVER,
        REGCLS_MULTIPLEUSE,
        widgetProviderFactory.put()));

    DWORD index{};
    HANDLE events[] = { g_shudownEvent.get() };
    winrt::check_hresult(CoWaitForMultipleObjects(CWMO_DISPATCH_CALLS | CWMO_DISPATCH_WINDOW_MESSAGES,
        INFINITE,
        static_cast<ULONG>(std::size(events)), events, &index));

    return 0;
}

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

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

建立 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 應用程式:

  1. 在 [方案總管] 的 ExampleWidgetProvider 專案上按一下滑鼠右鍵,然後選取 [屬性]。 導覽至 [連結器]> [系統],並將 SubSystem 從 [控制台] 變更為 [Windows]。 您也可以將 <SubSystem>Windows</SubSystem> 新增至 .vcxproj 的<Link>..</Link> 區段來完成此動作。
  2. 在 main.cpp 中,將 int main() 變更為 int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/, _In_opt_ HINSTANCE /*hPrevInstance*/, _In_ PWSTR pCmdLine, _In_ int /*nCmdShow*/)

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

發佈您的小工具

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

小工具 Store 集合

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

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

實作小工具自訂

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

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

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

  1. 在一般作業中,小工具提供者會以視覺和 JSON 範本回應小工具主機的要求,以取得一般小工具體驗。
  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">
...

更新 WidgetProvider.h

若要將自訂支援新增至本文先前步驟中建立的小工具,我們必須更新小工具提供者 WidgetProvider.h 的標頭檔案。

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

// WidgetProvider.h
struct CompactWidgetInfo
{
    winrt::hstring widgetId;
    winrt::hstring widgetName;
    int customState = 0;
    bool isActive = false;
    bool inCustomization = false;
};

更新 WidgetProvider 宣告以實行 IWidgetProvider2 介面。

// WidgetProvider.h

struct WidgetProvider : winrt::implements<WidgetProvider, winrt::Microsoft::Windows::Widgets::Providers::IWidgetProvider, winrt::Microsoft::Windows::Widgets::Providers::IWidgetProvider2>

新增 IWidgetProvider2 介面的 OnCustomizationRequested 回撥。

// WidgetProvider.h

void OnCustomizationRequested(winrt::Microsoft::Windows::Widgets::Providers::WidgetCustomizationRequestedArgs args);

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

// WidgetProvider.h
const std::string countWidgetCustomizationTemplate = R"(
{
    "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"
})";

更新 WidgetProvider.cpp

現在更新 WidgetProvider.cpp 檔案,以實作小工具自訂行為。 這個方法會使用與我們所使用的其他回撥相同的模式。 我們會從 WidgetContext 取得要自訂的小工具識別碼,然後找到與該小工具相關聯的 CompactWidgetInfo 協助程式,並設定 inCustomization 欄位為 true。

//WidgetProvider.cpp
void WidgetProvider::OnCustomizationRequested(winrt::WidgetCustomizationRequestedArgs args)
{
    auto widgetId = args.WidgetContext().Id();

    if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
    {
        auto& localWidgetInfo = iter->second;
        localWidgetInfo.inCustomization = true;

        UpdateWidget(localWidgetInfo);
    }
}

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

//WidgetProvider.cpp
void WidgetProvider::UpdateWidget(CompactWidgetInfo const& localWidgetInfo)
{
    ...
    else if (localWidgetInfo.widgetName == L"Counting_Widget")
    {
        if (!localWidgetInfo.inCustomization)
        {
            std::wcout << L" - not in customization " << std::endl;
            templateJson = winrt::to_hstring(countWidgetTemplate);
		}
        else
        {
            std::wcout << L" - in customization " << std::endl;
            templateJson = winrt::to_hstring(countWidgetCustomizationTemplate);
		}
    }
    ...
    
    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(winrt::to_hstring(localWidgetInfo.customState));
    winrt::WidgetManager::GetDefault().UpdateWidget(updateOptions);
}

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

//WidgetProvider.cpp
void WidgetProvider::OnActionInvoked(winrt::WidgetActionInvokedArgs actionInvokedArgs)
{
    auto verb = actionInvokedArgs.Verb();
    if (verb == L"inc")
    {
        auto 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:
        auto data = actionInvokedArgs.Data();
        if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
        {
            auto& localWidgetInfo = iter->second;
            // Increment the count
            localWidgetInfo.customState++;
            UpdateWidget(localWidgetInfo);
        }
    }
    else if (verb == L"reset") 
    {
        auto widgetId = actionInvokedArgs.WidgetContext().Id();
        auto data = actionInvokedArgs.Data();
        if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
        {
            auto& localWidgetInfo = iter->second;
            // Reset the count
            localWidgetInfo.customState = 0;
            localWidgetInfo.inCustomization = false;
            UpdateWidget(localWidgetInfo);
        }
    }
    else if (verb == L"exitCustomization")
    {
        auto 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:
        auto data = actionInvokedArgs.Data();
        if (const auto iter = RunningWidgets.find(widgetId); iter != RunningWidgets.end())
        {
            auto& localWidgetInfo = iter->second;
            // Stop sending the customization template
            localWidgetInfo.inCustomization = false;
            UpdateWidget(localWidgetInfo);
        }
    }
}

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

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

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