乘风破浪,遇见最美Windows 11之现代Windows桌面应用开发 - Windows 11小组件开发指南,成为Windows小组件服务提供商

什么是Windows 11小组件

https://learn.microsoft.com/zh-cn/windows/apps/develop/widgets/widget-service-providers

image

Windows小组件是显示与设备上安装的应用关联的文本和图形的小型UI容器。已安装的小组件显示在小组件板的网格中:当用户单击任务栏上的小组件图标、使用Windows+W快捷方式或从屏幕左边缘轻扫时覆盖Windows桌面的浮出平面。小组件通过聚合他们使用的应用中的个性化内容和快速操作,帮助用户掌握对他们很重要的内容。它们可快速易耗和可操作。小组件不打算替换应用和网站,而是提供对用户立即读取/触发的最所需信息或常用功能的无摩擦访问。设计小组件时,请考虑它将给消费者带来的价值类型。

image

Windows小组件是小型UI容器,用于显示应用或Web服务中的文本和图形。Windows小组件使用的自适应卡片格式支持对填充小组件UI的数据进行动态绑定。若要更新小组件,应用或服务将实现一个小组件服务提供商,该提供程序响应来自小组件主机的请求,并返回指定视觉模板和小组件的关联数据的JSON字符串。

目前,可以使用打包的Win32桌面应用实现小组件提供程序。计划将来的版本支持渐进式Web应用(PWA)

小组件术语

术语 定义
小组件主机 显示和管理Windows小组件的应用程序。在当前版本中,唯一的小组件主机是内置于Windows11的小组件板。
小组件板 小组件板是一个Windows11系统组件,当用户单击任务栏上的小组件图标、使用Windows+W快捷方式或从屏幕左边缘轻扫时,该组件将显示在桌面上。小组件板显示小组件,并在板上管理其布局。
小组件 小组件是一个自适应卡片,它显示来自应用的重要内容或操作。它允许用户立即访问所需的信息,而无需启动关联的应用或网站。小组件内容全天动态刷新,为用户提供一目了然且有趣的内容。小组件提供基本的交互式功能,允许用户启动关联的应用以实现更深入的参与。小组件不用于替换应用和网站。
小组件提供程序 小组件提供程序是一个Windows应用,它提供要显示在小组件中的内容。小组件提供程序拥有小组件的内容、布局和交互式元素。

小组件原则

若要创建出色的Windows小组件,请在设计和开发小组件时,请考虑以下原则:

  • 一目了然

用户可以快速查看,以充分利用小组件的价值。他们只需要单击它,如果他们想要更丰富的详细信息或更深入的交互。

  • 可靠

Surface经常使用的信息可立即节省用户重复这些步骤的时间。推动对应用的一致重新参与。

  • useful

提升最有用的相关信息和相关信息。

  • 个人

提供个性化内容并与客户建立情感联系。小组件不应包含广告。客户控制其小组件内容和布局。

  • 已设定焦点

每个小组件通常应关注一个主要任务或方案。小组件不用于替换应用和网站。

  • 新鲜

内容应基于可用上下文动态刷新。它是最新的,并在正确的时间提供正确的内容。

规划应用的小组件体验

  • 根据你对客户的了解,确定用户希望快速访问的最重要内容或最有用的操作,而无需打开应用或网站。请考虑小组件原则部分中枚举的原则,并考虑它们如何应用于你的应用。
  • 你的应用可以支持多个单独的小组件。确定要支持的不同小组件的数量,以便每个小组件侧重于特定用途。
  • 确定要为每个小组件包含的内容。单个小组件可以支持三种不同的大小;小、中、大。对于每个小组件,请考虑哪些内容将为用户和业务需求带来最大的价值。对于从小到大的每个大小,小组件的目的应保持不变,但显示的信息量应以更大的大小扩展。建议小组件提供程序实现所有小组件大小,以在自定义小组件布局时为用户提供灵活性。
  • 考虑小组件将支持的用户交互。用户可以单击小组件标题或任何在小组件上定义的单击目标。这些交互可以激活你的应用或网站中的深层链接快捷方式,这些快捷方式将用户直接转到他们感兴趣的内容,这样他们就不必从应用的根目录导航。请考虑提供的不同导航模型。
  • 应用必须实现实现后端功能的小组件提供程序,以便将小组件的布局和数据发送到要显示的小组件板。目前,可以使用打包的Win32桌面应用实现小组件提供程序。计划为将来的版本支持渐进式Web应用(PWA)。有关详细信息,请参阅小组件服务提供商。

小组件设计

小组件大小

小组件提供三个大小供用户选择。建议创建并考虑所有3种大小,并根据每种大小专门调整设计。中小型大小在动态源中更频繁地浮出水面时,可提供更好的可发现性。大型大小可用于显示更深入的信息。支持多种大小可让用户灵活地自定义他们选择固定到小组件板的小组件。

小组件原则一目了然,重点在为小组件做出的设计决策中变得更加重要。小小组件不应尝试强制所有可以舒适地适合大型小组件的功能。专注于一个用户交互或一段可在此处显示1个触摸目标的关键信息。

  • 中型

与小组件相比,中等大小的小组件允许更多的空间,因此可以包含更多功能或其他信息。中等小组件还可以提供与小组件相同的重点体验,但提供2-3触摸目标。

大尺寸允许显示更多信息,但内容仍应重点且易耗。或者,大型卡片可以突出显示一个图像或主题,并具有更沉浸式的体验。大尺寸不应超过3-4个触摸目标。

image
image
image

颜色和主题

Windows11支持两种颜色模式:浅色和深色。每种模式都由一组中性颜色值组成,这些值会自动调整以确保最佳对比度。对于支持的每个小组件大小,请确保为浅色和深色主题创建单独的设计,以便小组件无缝集成到更广泛的操作系统和用户的主题选择中。小组件背景支持使用纯色/深色背景、渐变色调或图像背景进行自定义。

image
image

选择背景色、图像和内容时,请确保有足够的颜色对比度来确保易读性和可访问性。

Web内容辅助功能指南(WCAG)2.0级别AA要求普通文本的对比度至少为4.5:1,对于大型文本,则为3:1。WCAG2.1要求图形和用户界面组件((例如表单输入边框))的对比度至少为3:1。WCAG级别AAA要求普通文本的对比度至少为7:1,对于大型文本,需要4.5:1。大文本定义为14磅(通常为18.66px)且加粗或更大,或18磅(通常为24px)或更大。

image

边距

image

每个小组件周围有一个16px的边距和一个48px属性区域,其中无法放置内容。可以位于右侧边距和下边距中的唯一组件是分页点。有关分页点定位的示例,请参阅小组件交互设计指南的分页部分。

image

对于使用容器的小组件,每个元素之间的装订线为4px。容器应触摸边距的边缘。你的内容还应使用四个Px的倍数的间距和大小调整值来实现跨不同屏幕分辨率的干净像素完美设计。

在设计内容时,还应在Windows应用的内容设计基础知识中查阅间距和装订指南。

版式

image
image

对于辅助功能,下表显示了上图中显示的表的文本。

示例 大小/线条高度 自适应卡片公式
标题 12/16epx 小型、较轻
正文 14/20epx 默认、较轻
超链接的正文() 14/20epx 默认、较浅、着色
粗正文 14/20epx 默认值,Bolder
大正文 18/24epx 中等、较轻
BodyLarget 18/24epx 中等、Bolder
副标题 20/28epx 大、较旧
Title 28/36epx 特大,较旧

SegoeUI是小组件和Windows中使用的字样。上述类型渐变包括有关如何在自适应卡片设计器中正确设置正确样式的公式。字面样式不应偏离上述指定公式。有关使用自适应卡片设计器创建小组件模板的详细信息,请参阅使用自适应卡片设计器创建小组件模板。

image

图标

个人资料图片

image

如果小组件包括显示用户配置文件(例如,社交媒体源或流)使用以下允许的圆配置文件大小之一:96x96px、48x48px、32x32px或24x24px。

工具提示

image

在小组件中截断标题文本时,可以使用工具提示。对于最佳做法,文本应整齐地适合小组件空间,不需要截断,但是,这可能并不总是发生,具体取决于语言本地化、系统文本缩放或引用内容(即文章标题、歌曲名称)。这不适用于小组件上的正文文本。

小组件状态

当小组件显示在小组件板上时,根据小组件板和应用的当前状态(例如小组件加载时间、小组件处于错误状态时或用户自定义小组件布局时)存在几种不同的状态。某些状态由应用设计和实现,而另一些状态则内置于小组件主机中。本部分显示并描述每个小组件状态。请记住,小组件同时支持浅色和深色主题,因此自定义的内置状态和状态可能根据当前主题看起来有所不同。

默认状态

image

默认状态是小组件正常运行时的外观。这是小组件的主要用户体验。为小组件的默认状态设计布局。尽管小组件默认状态的UI可能会更改以响应用户配置,但小组件的默认状态应完全实现,并且不应在用户配置之前为空。如果小组件要求用户登录,可能需要实现如下所示的已注销状态。有关为小组件创建默认状态的设计指南,请参阅小组件设计基础知识。

DO

  • 当处于默认状态时,小组件应感受到个人状态并连接到用户。
  • 小组件应显示引人入胜的内容,从而在当前时刻带来用户价值。
  • 让用户能够立即开始与小组件交互。
  • 提供一个UI,反映应用的UI,同时保留在小组件的设计约束中,以便最大程度地保持一致性并降低学习曲线。
  • 请考虑使用用户的位置为运动和建议的日历等内容预先填充数据,以添加而不是通用数据。
  • 允许元素之间有足够的呼吸空间。

不要

  • 将小组件用于通用商业产品/服务。内容应反映用户的愿望和意图。
  • 避免繁忙的复杂布局。
  • 针对每个小组件大小内舒适的信息密度和健康的负空间,以帮助浏览和浏览模型。如果有很多要包含的信息,请考虑下一个大小以显示更多内容。此外,还考虑用户浏览和使用内容是多么困难/容易。

请考虑向小组件添加惊喜+喜悦时刻,以提升体验。例如,对于家庭或日历小组件,可以通过不同的视觉处理突出显示孩子的生日。

需要身份验证的小组件注销状态

image

某些小组件方案可能要求用户必须登录或执行其他操作才能查看个性化小组件内容。当用户未登录时,应考虑显示非个性化内容。

错误状态 - 系统提供

image

如果出于某种原因,小组件板无法检索小组件的布局或数据,它将显示错误状态。Windows将显示小组件标头,其中包含错误消息和重新加载按钮。对于每个小组件,此消息将相同。

如果有缓存的内容可供显示,小组件标头将在上次刷新数据时按以下格式显示:

  • 如果小于一小时,则分钟数
  • 如果超过一小时,则舍入到最接近的小时
  • 长小组件合作伙伴名称将截断,同时显示最大15个字符的缓存消息。

内置小组件UI组件

小组件的一些UI元素内置于小组件体验中,虽然这些元素不可由小组件提供程序自定义,但必须了解这些元素是什么以及它们的行为方式。

提供的上下文菜单(系统)

image

当用户单击右上角的三点图标时,将显示上下文菜单。此菜单允许用户选择其首选小组件大小并访问小组件的配置状态。合作伙伴将使用“由___提供支持”的同一模板小组件。

属性区域

image

属性区域由小组件板根据小组件注册期间提供的小组件名称和图标呈现。有关注册小组件的详细信息,请参阅小组件提供程序包清单XML格式。

交互设计指南

导航

小组件应可概览且专注,并且应表示应用主要用途的单个方面。小组件可以提供一个或多个操作调用。当用户单击操作调用时,小组件应启动关联的应用或网站,而不是在小组件本身中实现该操作。小组件只有一个可以容纳多个交互的主页面。单击小组件中的项目绝不应将你带到该小组件的完全不同视图。例如,在天气小组件中,你可能会显示多天的天气,但单击其中一天不会内联展开详细信息,而是启动应用或Web。

下面是每个受支持的小组件大小建议的最大触摸点数。

小组件大小 最大触摸点数
小型 1
3
4

Windows小组件不支持以下导航元素:

  • 小组件中不支持透视
  • 小组件中不支持L2页面
  • 小组件内不支持垂直或水平滚动

容器

下图显示了小组件模板中容器元素的示例用法。容器将视觉元素分组到列和行中,以创建分层网格结构。

image

image

图像链接

下图显示了小组件模板中图像链接元素的示例用法。

image

分页

下图显示了小组件模板中的分页示例。分页控件可以水平或垂直对齐。导航箭头在响应光标悬停时显示。

image
image
image

超链接

下图显示了小组件模板中的超链接示例。

image

image

下拉菜单

image

如果用户与菜单或下拉列表交互,小组件可以暂时超出其小组件大小。如果用户单击菜单/下拉列表区域外,菜单行为应为轻而淡,并关闭菜单。

使用自适应卡片设计器创建小组件模板

Windows小组件的UI和交互是使用自适应卡片实现的。每个小组件都提供一个视觉模板,还可以选择使用符合自适应卡片架构的JSON文档定义的数据模板。本文将指导你完成创建简单小组件模板的步骤。

计数小组件

本文中的示例是一个简单的计数小组件,它显示一个整数值,允许用户通过单击小组件UI中的按钮来递增值。此示例模板使用数据绑定根据数据上下文自动更新UI。

应用需要实现小组件提供程序来生成和更新小组件模板和/或数据,并将其传递给小组件主机。在win32应用中实现小组件提供程序的文章提供了分步指南,指导实现小组件提供程序的计数小组件,我们将在以下步骤中生成。

自适应卡片设计器

自适应卡片设计器是一种在线交互式工具,可用于轻松生成自适应卡片的JSON模板。使用设计器,可以在生成小组件模板时实时查看呈现的视觉对象和数据绑定行为。按照链接打开设计器,该设计器将用于本演练中的所有步骤。

从预设创建空模板

在页面顶部的“选择主机应用”下拉列表中,选择“小组件板”。这将设置自适应卡的容器大小,以具有小组件支持的大小。请注意,小组件支持小型、中型和大型大小。默认模板预设的大小是小组件的正确大小。如果内容溢出边框,请不要担心,因为我们将用设计在小组件内的内容替换它。

页面底部有三个文本编辑器。一个标记的卡片有效负载编辑器包含小组件的UI的JSON定义。标记为“示例数据编辑器”的编辑器包含用于定义小组件的可选数据上下文的JSON。呈现小组件时,数据上下文动态绑定到自适应卡片。有关自适应卡片中的数据绑定的详细信息,请参阅自适应卡片模板语言。

第三个文本编辑器标记为示例主机数据编辑器。请注意,此编辑器可能折叠在页面的其他两个编辑器下方。如果是这样,请单击+以展开编辑器。小组件主机应用(如小组件板)有两个属性,指示小组件的大小和主题。这些属性命名为host.widgetSize和host.hostTheme。支持的大小为“small”、“medium”和“large”。支持的主题为“浅色”和“深色”。小组件模板可以根据这些属性的当前值动态显示不同的内容。若要查看小组件如何响应大小和主题的变化,可以在编辑器中调整这些属性的值,也可以在页面顶部的“选择主机应用”下拉列表旁边的“容器大小”和“主题”下拉列表中设置这些值。

创建新卡片

在页面左上角,单击“新建”卡。在“创建”对话框中,选择“空白卡”。现在应会看到一个空的自适应卡片。你还将注意到示例数据编辑器中的JSON文档为空。

我们将创建的计数小组件非常简单,仅包含4个TextBlock元素和一个Action.Execute类型的操作,用于定义小组件的按钮。

添加TextBlock元素

通过将四个TextBlock元素从页面左侧的“卡片元素”窗格拖动到预览窗格中的空白自适应卡片上来添加四个TextBlock元素。此时,小组件预览应如下图所示。内容再次溢出到小组件边框之外,但将在以下步骤中修复。

正在进行自适应卡片。它显示包含文本“新建TextBlock”的四行的小组件。四行文本溢出小组件的下边框。

实现条件布局

卡片有效负载编辑器已更新,以反映我们添加的TextBlock元素。将正文对象的JSON字符串替换为以下内容:

"body": [
    {
        "type": "TextBlock",
        "text": "You have clicked the button ${count} times"
    },
    {
        "type": "TextBlock",
        "text": "Rendering only if medium",
        "$when": "${$host.widgetSize==\"medium\"}"
    },
    {
        "type": "TextBlock",
        "text": "Rendering only if small",
        "$when": "${$host.widgetSize==\"small\"}"
    },
    {
        "type": "TextBlock",
        "text": "Rendering only if large",
        "$when": "${$host.widgetSize==\"large\"}"
    }
]

在自适应卡片模板语言中,该$when属性指定当关联值计算结果为true时显示包含元素。如果该值的计算结果为false,则不显示包含元素。在我们的示例中的正文元素中,将显示三个TextBlock元素之一,另外两个元素将隐藏,具体取决于属性的值$host.widgetSize。有关自适应卡片中支持的条件的详细信息,请参阅具有$when的条件布局。

现在预览应如下图所示:

image

请注意,条件语句不会反映在预览版中。这是因为设计器不会模拟小组件主机的行为。单击页面顶部的“预览模式”按钮以启动模拟。小组件预览现在如下图所示:

image

从“容器大小”下拉列表中选择“中等”,请注意预览切换为仅显示中等大小的TextBlock。预览版中的容器也会更改大小,演示如何使用预览来确保UI适合每个受支持大小的小组件容器。

绑定到数据上下文

我们的示例小组件将使用名为“count”的自定义状态属性。可以在当前模板中看到第一个TextBlock的值包含变量引用$count。当小组件在小组件板中运行时,小组件提供程序负责组装数据有效负载并将其传递给小组件主机。在设计时,可以使用示例数据编辑器对数据有效负载进行原型制作,并了解不同的值如何影响小组件的显示。将空数据有效负载替换为以下JSON。

{"count": "2"}

请注意,预览现在会将为count属性指定的值插入到第一个TextBlock的文本中。

image

添加按钮

下一步是向小组件添加按钮。在小组件主机中,当用户单击该按钮时,主机将向小组件提供程序发出请求。对于此示例,小组件提供程序将递增计数值并返回更新的数据有效负载。由于此操作需要小组件提供程序,因此无法在自适应卡片设计器中查看此行为,但仍可以使用设计器调整UI中按钮的布局。

使用自适应卡片时,使用操作元素定义交互式元素。直接在卡片有效负载编辑器中的正文元素后面添加以下JSON块。请务必在正文元素的右括号(])后面添加逗号,否则设计器将报告格式设置错误。

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

在此JSON字符串中,type属性指定正在表示的操作的类型。小组件仅支持“Action.Execute”操作类型。标题包含操作按钮上显示的文本。谓词属性是一个应用定义的字符串,小组件主机将发送到小组件提供程序以传达与操作关联的意向。小组件可以有多个操作,小组件提供程序代码将检查请求中谓词的值,以确定要执行的操作。

image

完整的小组件模板

以下代码列表显示了JSON有效负载的最终版本。

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

实现小组件提供程序(Win32应用)

先决条件

  • Windows预览体验计划(WIP)的最新开发频道Windows11生成。有关WIP自承载的详细信息,请参阅深入探讨外部测试。
  • 小组件板版本521.20060.1205.0。这将附带最新的开发通道WIP版本,可以通过打开小组件板、导航到小组件选取器以及查看位于选取器右下角的版本号进行检查。
  • 设备必须启用开发人员模式。有关详细信息,请参阅启用设备进行开发。
  • 具有通用Windows平台开发工作负荷的VisualStudio2017或更高版本。请确保从可选下拉列表中添加C++(v143)组件。

创建新的win32控制台应用

在VisualStudio中,创建新的项目。在“创建新项目”对话框中,将语言筛选器设置为“C++”,并将平台筛选器设置为Windows,然后选择Windows控制台应用程序(C++/WinRT)项目模板。将新项目命名为“ExampleWidgetProvider”。出现提示时,将应用的目标Windows版本设置为版本1809或更高版本。

添加对Windows小组件NuGet包的引用

此示例使用最新的预览版Windows应用SDKNuGet包。在解决方案资源管理器中,右键单击“引用”并选择“管理NuGet包...”。在NuGet包管理器中,选择窗口顶部附近的“包括预发布”复选框,选择“浏览”选项卡并搜索“Microsoft.WindowsAppSDK”。在版本下拉列表中选择1.2.220930.4-preview2,然后单击“安装”。

此示例还使用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标头。

添加WidgetProvider类以处理小组件操作

在VisualStudio中,右键单击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接口的方法。在本文后面的几个方法实现中调用的Helper方法UpdateWidget。在深入了解接口方法之前,请将以下行添加到WidgetProvider.cppinclude指令之后,将小组件提供程序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模板/数据返回到小组件主机,具体取决于所请求的大小。还可以设计模板JSON,以支持使用基于host.widgetSize值的条件呈现的所有可用大小。如果不需要发送新的模板或数据来考虑大小更改,则可以将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方法以通知小组件提供程序,小组件主机当前有兴趣从提供程序接收更新的内容。例如,这可能意味着用户当前正在主动查看小组件主机。调用停用方法以通知小组件提供程序小组件主机不再请求内容更新。这两种方法定义一个窗口,其中小组件主机最感兴趣的窗口显示最新的内容。小组件提供程序可以随时向小组件发送更新,例如响应推送通知,但与任何后台任务一样,与提供最新内容与电池使用时间等资源问题保持平衡非常重要。

按小组件调用激活和停用。此示例跟踪CompactWidgetInfo帮助程序结构中每个小组件的活动状态。在Activate方法中,我们调用UpdateWidget帮助程序方法来更新小组件。请注意,“激活”和“停用”之间的时间范围可能很小,因此建议尽量尽快使小组件更新代码路径。

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帮助程序方法以更新已启用的小组件。在此示例中,我们检查传递到方法的CompatWidgetInfo帮助程序结构中的小组件的名称,然后根据要更新的小组件设置相应的模板和数据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{ widgetName, widgetId };
            try
            {
                // If we had any save state (in this case we might have some state saved for Counting widget)
                // convert string to required type if needed.
                int count = std::stoi(winrt::to_string(customState));
                runningWidgetInfo.customState = count;
            }
            catch (...)
            {

            }
            RunningWidgets[widgetId] = runningWidgetInfo;
        }
    }
}

注册将按需实例化WidgetProvider的类工厂

将定义WidgetProvider类的标头添加到应用文件顶部的main.cpp包含项。我们还将在此处包括互斥体。

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

接下来,需要创建一个CLSID,用于标识小组件提供程序进行COM激活。转到“工具>创建GUID”,在VisualStudio中生成GUID。选择选项“staticconstGUID=”,然后单击“复制”,然后将其粘贴到main.cpp其中。使用以下C++/WinRT语法更新GUID定义,widget_provider_clsid设置GUID变量名称。保留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。这是特定于小组件提供程序实现的样本代码。

// 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()));

    MSG msg;
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

打包小组件提供程序应用

在当前版本中,只能将打包的应用注册为小组件提供程序。以下步骤将引导你完成打包应用和更新应用清单的过程,以将应用注册到OS作为小组件提供程序。

创建MSIX打包项目

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

更新包清单

在解决方案资源管理器右键单击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元素添加为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="80F4CB41-5758-4493-9180-4FB8D480E3F5" 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="80F4CB41-5758-4493-9180-4FB8D480E3F5" />
                    </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内容。在这里,我们将存储小组件的图标和屏幕截图。添加所需的图标和屏幕截图后,请确保图像名称与Path=ProviderAssets\中Package.appxmanifest之后的内容匹配,否则小组件不会显示在小组件主机中。

测试小组件提供程序

在解决方案资源管理器中,右键单击解决方案并选择“生成解决方案”。完成后,右键单击ExampleWidgetProviderPackage并选择“部署”。在当前版本中,唯一受支持的小组件主机是小组件板。若要查看小组件,需要打开小组件板,然后选择右上角的“添加小组件”。滚动到可用小组件底部,应会看到本教程中创建的模拟天气小组件和Microsoft计数小组件。单击小组件将其固定到小组件板并测试其功能。

参考

posted @ 2022-10-06 22:15  TaylorShi  阅读(1704)  评论(0编辑  收藏  举报