在 Win32 应用中实现小组件提供程序 (C++/WinRT)

本文将向你详细介绍如何创建实现 IWidgetProvider 接口的简单小组件提供程序。 该接口的方法由小组件主机调用,以请求定义小组件的数据,或让小组件提供程序响应用户对小组件执行的操作。 小组件提供程序可以支持一个或多个小组件。 在此示例中,我们将定义两个不同的小组件。 其中一个小组件是模拟天气小组件,它阐释了自适应卡片框架提供的一些格式设置选项。 第二个小组件将通过维护一个计数器来演示用户操作和自定义小组件状态功能,只要用户单击小组件上显示的按钮,该计数器就会递增。

A screenshot of a simple weather widget. The widget shows some weather-related graphics an data as well as some diagnostic text illustrating that the template for the medium size widget is being displayed.

A screenshot of a simple counting widget. The widget shows a string containing the numeric value to be incremented and a button labeled Increment, as well as some diagnostic text illustrating that the template for the small size widget is being displayed.

本文中的此示例代码改编自 Windows 应用 SDK 小组件示例。 要使用 C# 实现小组件提供程序,请参阅在 win32 应用中实现小组件提供程序 (C#)

先决条件

  • 设备必须启用开发人员模式。 有关详细信息,请参阅启用用于开发的设备
  • 具有通用 Windows 平台开发工作负载的 Visual Studio 2022 或更高版本。 请确保从可选下拉列表添加 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>

注意

必须首先在任何 WinRT 标头之前包括 wil/cppwinrt.h 标头。

要正确处理关闭小组件提供程序应用,需要 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);

准备跟踪已启用的小组件

小组件提供程序可以支持一个或多个小组件。 每当小组件主机使用小组件提供程序启动操作时,它都会传递一个 ID 来标识与操作关联的小组件。 每个小组件还有一个关联的名称和一个可用于存储自定义数据的状态值。 在此示例中,我们将声明一个简单的帮助程序结构,用于存储每个固定小组件的 ID、名称和数据。 小组件还可以处于活动状态(如下面的激活和停用部分所述),我们将使用布尔值跟踪每个小组件的此状态。 将以下定义添加到 WidgetProvider.h 文件的 WidgetProvider 结构声明上方。

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

在 WidgetProvider.h 的 WidgetProvider 声明中,使用小组件 ID 作为每个条目的键,为映射添加一个成员,以维护已启用的小组件列表,。

// WidgetProvider.h
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 方法

在接下来的几个部分中,我们将实现 IWidgetProvider 接口的方法。 本文稍后将介绍其中几个方法实现中调用的帮助程序方法 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。 首先,此方法获取关联的小组件的 ID 和名称,并将帮助程序结构 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 模板中声明了一个谓词值为“inc”的操作。 小组件提供程序代码将使用此谓词值来确定要执行哪些操作来响应用户交互。

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

在 OnActionInvoked 方法中,通过检查传递到方法中的 WidgetActionInvokedArgs 的 Verb 属性来获取谓词值。 如果谓词为“inc”,则表示需要递增小组件的自定义状态中的计数。 在 WidgetActionInvokedArgs 中,获取 WidgetContext 对象,然后获取 WidgetId 以获取要更新的小组件的 ID。 在已启用的小组件映射中查找具有指定 ID 的条目,然后更新用于存储增量数的自定义状态值。 最后,使用 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 方法以通知小组件提供程序,小组件主机不再请求内容更新。 这两种方法定义了一个窗口,在该窗口中,小组件主机最感兴趣的是显示最新内容。 小组件提供程序可以随时向小组件发送更新,例如响应推送通知,但与任何后台任务一样,请务必在提供最新内容的同时,兼顾资源问题(如电池使用时间)。

Activate 和 Deactivate 按小组件调用。 此示例跟踪 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 对象的数组。 将小组件 ID、名称和自定义状态复制到帮助程序结构 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;
        }
    }
}

注册将按请求实例化 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 }
};

将以下类工厂定义添加到 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;
}

打包小组件提供程序应用

在当前版本中,只有打包的应用才能注册为小组件提供程序。 以下步骤将详细介绍打包应用并更新应用清单以将应用注册到 OS 作为小组件提供程序的过程。

创建 MSIX 打包项目

在“解决方案资源管理器”中,右键单击所需解决方案,然后选择“添加”->“新项目...”。在“添加新项目”对话框中,选择“Windows 应用程序打包项目”模板,然后单击“下一步”。 将项目名称设置为“ExampleWidgetProviderPackage”,然后单击“创建”。 出现提示时,将目标版本设置为版本 1809 或更高版本,然后单击“确定”。 接下来,右键单击“ExampleWidgetProviderPackage”项目,然后选择“添加”->“项目引用”。 选择 ExampleWidgetProvider 项目,然后单击“确定”。

将 Windows 应用 SDK 包引用添加到打包项目

需要将对 Windows 应用 SDK nuget 包的引用添加到 MSIX 打包项目。 在“解决方案资源管理器”中,双击 ExampleWidgetProviderPackage 项目以打开 ExampleWidgetProviderPackage.wapproj 文件。 在 Project 元素中添加以下 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 扩展。 这会向 OS 注册可执行文件的入口点。 此扩展是打包的应用,等效于通过设置注册表项注册 COM 服务器,它并不特定于小组件提供程序。添加以下 com:Extension 元素作为 Extension 元素的子元素。 将 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 元素粘贴到以下代码片段中,作为 Extension 元素的子元素。 请务必将 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 并选择“部署”。 在当前版本中,小组件板是唯一受支持的小组件主机。 要查看小组件,需要打开小组件板,然后选择右上角的“添加小组件”。 滚动到可用小组件的底部,应会看到在本教程中创建的模拟天气小组件和 Microsoft 计数小组件。 单击小组件以将其固定到小组件板并测试其功能。

测试小组件提供程序

固定小组件后,小组件平台将启动小组件提供程序应用程序,以便接收和发送有关小组件的相关信息。 要调试正在运行的小组件,可以将调试程序附加到正在运行的小组件提供程序应用程序,也可以将 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*/)

A screenshot showing the C++ widget provider project properties with the output type set to Windows Application

发布小组件

开发和测试小组件后,必须在 Microsoft Store 上发布应用,以便用户在其设备上安装小组件。 有关发布应用的分步指南,请参阅在 Microsoft Store 中发布应用

小组件应用商店集合

在 Microsoft Store 上发布应用后,可以请求将应用包含在小组件应用商店集合中,以帮助用户发现具有 Windows 小组件功能的应用。 若要提交请求,请参阅提交小组件信息以添加到应用商店集合

Screenshot of the Microsoft Store showing the widgets collection that allows users to discover apps that feature Windows Widgets.

实现小组件自定义

从 Windows App SDK 1.4 开始,小组件可以支持用户自定义。 实现此功能后,“自定义小组件”选项将添加到“取消固定小组件”选项上方的省略号菜单中。

A screenshot showing a widget with the customization dialog displayed.

以下步骤汇总了小组件自定义的过程。

  1. 在正常操作中,小组件提供程序使用常规小组件体验的视觉和数据 JSON 模板响应来自小组件主机的请求。
  2. 用户单击省略号菜单中的“自定义小组件”按钮。
  3. 该小组件在小组件提供程序上引发 OnCustomizationRequested 事件,以指示用户已请求小组件自定义体验。
  4. 小组件提供程序会设置一个内部标志,以指示小组件处于自定义模式。 在自定义模式下,小组件提供程序为小组件自定义 UI 而不是常规小组件 UI 发送 JSON 模板。
  5. 在自定义模式下,小组件提供程序在用户与自定义 UI 交互时接收 OnActionInvoked 事件,并根据用户的操作调整其内部配置和行为。
  6. 当与 OnActionInvoked 事件关联的操作是应用定义的“退出自定义”操作时,小组件提供程序会重置其内部标志,以指示它不再处于自定义模式,并继续发送常规小组件体验的视觉和数据 JSON 模板,反映自定义期间请求的更改。
  7. 小组件提供程序将自定义选项保存到磁盘或云中,以便在小组件提供程序的调用之间保留更改。

注意

对于使用 Windows App SDK 生成的小组件,Windows 小组件板存在一个已知 bug,导致自定义卡显示后省略号菜单变得无响应。

在典型的小组件自定义场景中,用户将选择在小组件上显示哪些数据或调整小组件的视觉呈现。 为简单起见,本部分中的示例将添加自定义行为,允许用户重置在前面的步骤中实现的计数小组件的计数器。

注意

小组件自定义仅在 Windows App 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 中获取要自定义的小组件的 ID,查找与该小组件关联的 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 模板中查找谓词“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);
        }
    }
}

现在,部署小组件时,应会在省略号菜单中看到“自定义小组件”按钮。 单击“自定义”按钮将显示自定义模板。

A screenshot showing the widgets customization UI.

单击“重置计数器”按钮将计数器重置为 0。 单击“退出自定义”按钮返回到小组件的常规行为。