精通-Windows8-C---应用开发-全-

精通 Windows8 C++ 应用开发(全)

原文:zh.annas-archive.org/md5/B768CC5DACB0E0A295995599D27B3552

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Windows 8 是微软最新的客户端操作系统。一方面,它延续了 Windows 7 的趋势,建立了一个稳定、强大和现代的操作系统。另一方面,它改变了许多从以前的 Windows 版本中学到的假设和习惯。

任务栏中的常见“开始”按钮已经消失,用户登录后不再首先看到桌面。一个新的开始屏幕等待着毫无准备的用户,上面充满了定期更改其内容的“动态磁贴”。经典的开始菜单已经不复存在;有趣的是,桌面可以在开始屏幕的磁贴中找到。

Windows 8 的新外观显然是针对平板设备的——最近几个月出现了许多型号。新的用户界面在基于触摸的设备上是有意义的,但传统的鼠标和键盘设置在笔记本电脑或台式机上仍然可以按预期工作。

随着这个新的 Windows,也带来了一个新的运行时,一种新类型的应用程序运行在上面——Windows Runtime。基于这个新的运行时,应用程序可以构建并上传到 Windows Store——一个存储经过认证的应用程序的库,将它们标识为安全和有用。事实上,普通用户只能通过 Windows Store 获取这些新应用程序——Windows Store 应用程序,而不是传统的安装方式,如安装程序或 MSI 文件。

经典应用程序,现在被称为桌面应用程序,仍然可以以通常的方式使用现有技术在本机空间(Win32、COM、ATL、MFC、WTL 等)或托管空间(WinForms、WPF、WCF、EF 等)中编写,并且在 Windows 8 上运行方式与在 Windows 7 上一样——也许更好,因为 Windows 内核的改进。

新的 Windows Store 应用程序只能在 Windows 8(及更高版本)操作系统上运行;它们需要基于组件对象模型COM)技术的 Windows Runtime。这些应用在几个方面在视觉上看起来不同:它们总是全屏显示(除了特殊的“分屏视图”),没有边框,使用了新的 UI 设计方案,现在称为现代 UI,是面向触摸的,并具有一些其他不太明显的特性。

这本书主要讲述了新的 Windows Store 应用程序。从它们是什么开始,我们将逐步介绍 Windows Runtime 的各个方面,重点是使用 C++和新的扩展(C++/CX)来利用这个新的运行时,编写可以上传到商店并与运行 Windows 8 的任何人共享的应用程序。

本书内容

第一章 介绍 Windows 8 应用程序,从 Windows Store 应用程序的角度介绍了 Windows 8 操作系统,并讨论了围绕 Windows Store 应用程序和 Windows Runtime 的一些概念。

第二章 Windows 8 商店应用的 COM 和 C++,介绍了 C++ 11 的重要特性和新的语言扩展 C++/CX,它们允许更容易地访问 Windows Runtime 类型。本章还讨论了其他经典技术以及它们在 Windows Store 应用程序模型中的适用性(如果有的话)。

第三章 使用 XAML 构建 UI,展示了如何使用声明性的 XAML 语言和语义为 Windows Store 应用程序构建用户界面。详细解释了资源的概念,以及它们如何适用于 WinRT。

第四章 布局、元素和控件,讨论了控件的布局方式,以构建灵活的用户界面。讨论了 Windows Runtime 提供的许多元素,特别关注具有特定特征的控件组。

第五章,“数据绑定”,讨论了允许控件和数据之间无缝集成的最强大的 WinRT 功能之一。介绍了流行的Model-View-ViewModelMVVM)模式,并提供了可能实现的示例。

第六章,“组件,模板和自定义元素”,展示了如何创建可供其他语言使用的可重用 WinRT 组件,而不仅仅是 C++。讨论了控件模板,允许完全更改控件的外观而不影响其行为。最后,本章演示了如何创建自定义控件,当需要一些现有行为但内置控件中不可用时。

第七章,“应用程序,磁贴,任务和通知”,探讨了 Windows Store 应用程序的一些特殊功能,如动态磁贴以及它们可以从本地和服务器更新的方式。讨论了后台任务,允许代码在应用程序不在前台时执行。本章还展示了如何利用设备锁屏,如何进行长时间数据传输以及播放背景音乐。

第八章,“合同和扩展”,展示了 Windows Store 应用程序如何通过实现 Windows 定义的合同和扩展与 Windows 更好地集成并与其他应用程序通信。

第九章,“打包和 Windows 商店”,介绍了将应用程序打包,测试和部署到 Windows 商店的过程,并详细说明了一些需要注意的事项,以便成功获得认证。

本书所需内容

要使用本书中的示例,您需要在运行 Windows 8(任何版本)上安装 Visual Studio 2012 或更高版本(包括 Express 版本)。

本书的受众

本书面向想要利用其现有技能创建 Windows Store 应用程序的 C++开发人员。不需要了解旧技术,如 Win32 或 MFC;熟悉 COM 是有益的,但不是必需的。

约定

在本书中,您将找到一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。

文本中的代码字,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄显示如下:“XAML 显示了一个Page根元素,带有几个属性和一个内部的Grid元素。”

代码块设置如下:

<StackPanel Orientation="Horizontal" Margin="20" VerticalAlignment="Center">
    <TextBox Width="150" Margin="10" x:Name="_number1" FontSize="30" Text="0" TextAlignment="Right"/>
    <TextBlock Text="+" Margin="10" FontSize="30" VerticalAlignment="Center"/>
    <TextBox Width="150" Margin="10" x:Name="_number2" FontSize="30" Text="0" TextAlignment="Right"/>
    <TextBlock Text="=" Margin="10" FontSize="30" VerticalAlignment="Center"/>
    <TextBlock Text="?" Width="150" Margin="10" x:Name="_result" FontSize="30" VerticalAlignment="Center"/>
    <Button Content="Caclulate" Margin="10" FontSize="25" />
</StackPanel>

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

<Button Style="{StaticResource numericStyle}" Grid.Row="1" 
        Content="7" Click="OnNumericClick" />
<Button Style="{StaticResource numericStyle}" Grid.Row="1" 
        Grid.Column="1" Content="8" Click="OnNumericClick"/>
<Button Style="{StaticResource numericStyle}" Grid.Row="1" 
        Grid.Column="2" Content="9" Click="OnNumericClick"/>

新术语重要单词以粗体显示。例如:“关于 Windows 8 的第一件事是新的开始屏幕。”

注意

警告或重要说明以此框显示。

提示

提示和技巧以此方式显示。

第一章:介绍 Windows 8 应用程序

Windows 8,微软最新的客户端操作系统,看起来与其前身 Windows 7 大不相同。具有新的开始(主页)屏幕,它承诺是一个重新设计的系统,不仅在 UI 前端,而且在应用程序编写方式上也是如此。Windows 8(以及以后)提供了一种与“正常”应用程序截然不同的新应用程序风格(仍然得到了很好的支持)。

在本章中,我们将快速浏览新的 Windows 8 功能,特别是与新应用程序类型相关的功能,即 Windows Store 应用程序(以前称为“Metro”)。

介绍 Windows 8

Windows 8 被微软描述为“重新构想的 Windows”,这并不是一个错误的说法。从用户的角度来看,Windows 8 看起来不同;最显著的是,一个新的开始屏幕和自 Windows 95 以来存在的普遍的开始按钮被移除了。

尽管如此,Windows 在底层仍然是我们熟悉和喜爱的操作系统;在 Windows 8 上运行的应用程序应该会像在 Windows 7 上一样良好(甚至更好)。产品中进行了许多改进,其中大部分对典型用户来说是看不见的;从开始(打开)就可以看到明显的变化。

触摸无处不在

Windows 8 面向平板等触摸设备。微软本身提供了自己品牌的平板设备(“Surface”),从 2012 年 10 月 26 日 Windows 8 的通用可用性GA)日期开始提供。

注意

值得一提的是,在同一时间段,微软发布了 Windows Phone 8,这是 Windows 7.5 移动操作系统的后继版本,具有与 Windows 8 相似的外观和感觉。Windows Phone 8 基于驱动 Windows 8 的相同内核,并共享 Windows 8 运行时的部分。未来,这些平台可能会合并,或者至少更加接近。

Windows 8 被优化为触摸设备。在屏幕的边缘划动(始终朝向屏幕的可见部分)会引起一些事情发生(通过将鼠标移动到边缘或使用某些键盘快捷键也可以实现相同的效果)。例如,从右边划动会导致魅力栏出现(稍后在魅力栏部分中详细介绍);通过将鼠标光标移动到屏幕的右边缘或使用键盘快捷键 Windows 键+C也可以实现相同的效果。

开始(主页)屏幕

关于 Windows 8 的第一件事是新的开始屏幕。它充满了磁贴,大多数代表安装在设备上的应用程序。以前 Windows 版本中的熟悉桌面出现为常规磁贴;单击它(或使用触摸轻击它)会将用户转移到熟悉的桌面环境,具有与以前 Windows 版本中大致相同的功能,包括快捷图标、任务栏、通知区域等,除了已经消失的开始按钮。

所有安装的应用程序都可以从新的开始屏幕中找到,无论它们是“正常”的桌面应用程序还是新的 Store(“Metro”)风格的应用程序:

开始(主页)屏幕

AppBar

开始屏幕中从底部划动会显示 AppBar。这个 UI 部分是鼠标上常见的右键上下文菜单的替代品。实际上,使用鼠标在开始屏幕的任何地方右键单击都会显示 AppBar,就好像从底部划动屏幕一样。

AppBar 根据所选对象(或未选择的对象)提供相关选项,并且与新的 Store 应用程序一起使用,就像在开始屏幕上一样;即使使用鼠标设备,也没有内置的方法在 Store 应用程序中显示经典的上下文菜单。

注意

在 Windows 商店应用程序(或开始屏幕)中右键单击会导致 AppBar 出现,即使使用鼠标也是有点烦人的,因为用户现在被迫将鼠标从预期的对象移动到底部(或某些应用程序的顶部)以选择所需的选项。

Charms 栏

当从右侧滑动(在触摸设备上)或将鼠标移动到屏幕右侧的任一角时,Charms 栏会出现。从用户的角度来看,Charms 是与其他应用程序进行通信的方式。标准 charms 包括搜索共享开始设备设置

Charms 栏

搜索 charm 允许用户不仅可以在操作系统的应用程序(如控制面板应用程序)和用户的个人文件(文档、图片等)中搜索,还可以在任何其他指示支持搜索协议的商店应用程序中进行搜索。

注意

开始屏幕,您可以通过键盘输入开始搜索,无需显式激活搜索 charm。

共享 charm 允许应用程序与其他应用程序进行通信,而无需了解这些应用程序的任何信息。这是通过实现共享协议来实现的——可以是提供者和/或接收者(我们将在第八章,“合同和扩展”中介绍合同)。

开始 charm 只是将用户带到开始屏幕。

注意

单独按下 Windows 键随时都会显示开始屏幕。

设备 charm 允许访问与设备相关的活动(如果应用程序支持),如打印。最后,设置 charm 允许用户自定义当前正在执行的应用程序(如果应用程序支持),或自定义一般的 Windows 功能。

桌面应用程序与商店应用程序

在 Windows 8 术语中,之前在 Windows 系统上运行的所有应用程序都称为桌面应用程序。这些是常规的、普通的应用程序,可以使用各种微软技术构建,如 Win32 API、Microsoft Foundation ClassesMFC)、Active Template LibraryATL)、.NET 技术(WPF、Silverlight、Windows Forms 等),以及这些技术的任何逻辑组合。这些类型的应用程序在 Windows 8 中仍然得到了很好的支持,因此在这里实际上没有什么特别之处。

Windows 8 支持的另一种应用程序类型是商店应用程序。这些应用程序在以前的 Windows 版本中不受支持。Windows 商店应用程序是本书的重点。我们将完全不涉及桌面应用程序。

商店应用程序在许多方面与桌面应用程序不同。一些不同之处包括:

  • 商店应用程序是沉浸式的,它们始终是全屏的(除非被捕捉,参见第九章,“打包和 Windows 商店”);没有窗口装饰(即没有标题、关闭或最小化按钮等)。我们将在第三章,“使用 XAML 构建 UI”和第四章,“布局、元素和控件”中讨论商店应用程序的用户界面方面。

  • 商店应用程序的生命周期由 Windows 管理。如果另一个应用程序成为前台应用程序,之前的应用程序将被挂起(几秒钟后),不消耗 CPU 周期。我们将在第七章,“应用程序、磁贴、任务和通知”中讨论应用程序的生命周期。

  • 一次只能运行一个应用程序实例。在应用程序运行时点击应用程序磁贴只是切换到正在运行的应用程序。用户不应该知道,也不应该关心应用程序实际上是已经在内存中还是刚刚启动。

  • 商店应用不能直接与其他正在运行的应用程序通信,一些形式的通信是通过合同的概念可能的。我们将在第八章, 合同和扩展中讨论合同。

  • 商店应用运行在一个新的运行时之上,称为Windows RuntimeWinRT),它建立在本地基础和组件对象模型COM)技术之上。我们将在第二章中讨论 WinRT 及其与 COM 的关系,Windows 8 商店应用的 COM 和 C++

  • 商店应用程序只能通过 Windows 8 商店分发和安装(除了企业客户的特殊情况),而不能使用传统的安装程序包。我们将在第九章, 打包和 Windows 商店中讨论商店。

  • 商店应用必须通过功能(例如使用设备上可能存在的摄像头)提前声明他们想要使用的任何东西。任何未声明的东西都会在运行时导致失败。当用户选择下载应用时,他/她必须接受应用想要使用的功能;否则,应用将无法安装。

所有这些意味着商店应用是不同的,需要不同的知识体系,与编写桌面应用的知识完全不同。

注意

平板电脑上的 Windows 8 有两个主要变体,基于 CPU 架构。一个基于英特尔/AMD(具有 32 位和 64 位变体),这是一个完整的 Windows 8,可以运行桌面应用程序,以及商店应用程序。第二版基于 ARM 处理器系列,被命名为“Windows RT”(不要与 Windows Runtime 混淆)。这个版本只能运行商店应用程序(至少在撰写本文时是这样)。

Windows Runtime

商店应用程序是针对一个称为 Windows Runtime(WinRT)的新运行时构建和执行的,这个运行时在以前的 Windows 版本中不存在。WinRT 建立在成熟的 COM 技术之上(具有一些 WinRT 特定的增强功能,如元数据的使用)。这意味着 WinRT 是完全本地的(没有.NET CLR),使得 C++成为针对这个运行时的自然和高性能的选择。

WinRT 提供了一组服务,应用程序可以构建在其上。WinRT 和应用程序之间的关系可以用以下图表表示:

Windows Runtime

WinRT API 具有以下特点:

  • 作为一组类型构建,实现接口(如 COM 所规定)。这些类型被组织在分层命名空间中,逻辑分组以便易于访问和防止名称冲突。

  • 每个 WinRT 对象都通过使用(主要是)内部引用计数来处理自己的生命周期(就像在 COM 中一样)。

  • 使用原始 WinRT 可能会非常冗长,导致语言投影实现一些细节,例如当客户端不再需要对象时自动减少引用计数。

  • 所有公共类型都使用元数据构建,描述 API 的公共表面。这是让各种语言相对容易地访问 WinRT 的魔法的一部分。

  • 许多 API 是异步的,它们启动一个操作并在操作完成时通知。在 WinRT 中的一个一般指导原则是,任何可能需要超过 50 毫秒的操作都应该是异步的。这很重要,以防止 UI 被冻结,从而造成糟糕的用户体验。

我们将在第二章中详细了解 WinRT 的核心概念,COM 和Windows 8 商店应用的 C++

语言投影

由于 WinRT 使用 COM,直接使用它只有从能够原生理解指针和虚拟表的语言才可能,也就是 C++(从技术上讲,C 也是可能的,但我们不会在本书中讨论它)。

许多使用微软技术的开发人员在非 C++环境中工作,主要是.NET(主要使用 C#语言,但也使用其他语言,如 Visual Basic 和 F#)和 JavaScript,在 Web 开发中非常流行(也是必要的)。

即使在 C++中,使用 COM 也不像我们希望的那样容易;许多细节需要被关注(比如在适当时调用IUnknown接口方法),这会让开发者分心,无法专注于他/她的主要工作——构建实际的应用功能。这就是为什么微软创建了语言投影,以在特定环境中相对一致地公开 WinRT。

微软目前提供了三种 WinRT 语言投影:

  • C++具有最轻量级和直接的投影。这些投影是通过一组语言扩展实现的,称为 C++/CX(组件扩展)。这使得与 WinRT 对象一起工作比使用原始 COM 接口更容易(我们将在第二章中详细讨论这一点,Windows 8 商店应用的 COM 和 C++)。

  • 使用托管(.NET)语言,如 C#和 Visual Basic,通过对.NET 运行时的投影是可能的。这些投影使得.NET 开发人员非常容易与 WinRT 一起工作。运行时可调用包装器RCWs)在过渡到和从 WinRT 时自动创建,以弥合托管-本机边界。这种机制在原则上与.NET 代码调用 COM 对象的通常方式非常相似。

  • 第三种支持的投影是使用 JavaScript 语言,这在 Web 开发中很受欢迎。WinRT 的聪明包装使得使用 JavaScript 相对容易,包括使某些约定自动化,比如使用小写字母作为方法的第一个字母,尽管真正的 WinRT 方法以大写字母开头。使用 JavaScript 还引入了 HTML 来构建商店应用的用户界面,这可能再次利用 JavaScript 开发人员的现有知识。

注意

JavaScript 仅限于使用 WinRT 类型。它不能创建新类型(.NET 和 C++可以)。

C++不需要 CLR(.NET 运行时),这使得它在执行速度和内存消耗方面最轻量级。我们将在本书中详细介绍使用 C++,从下一章开始。

构建用户界面

JavaScript 是唯一直接访问 HTML 的语言,用于创建应用的用户界面。这样 JavaScript 开发人员就不需要学习太多,他们可能已经了解 HTML。Windows JavaScript 库提供了对控件、CSS 和其他辅助程序的访问,以弥合与 WinRT 之间的差距。

C++和.NET 开发人员使用 XAML 语言构建用户界面。XAML 是一种基于 XML 的声明性语言,允许(在某种程度上)创建对象并设置它们的属性。我们将在第三章中更详细地了解 XAML 和 UI,使用 XAML 构建 UI

注意

熟悉 XAML 的开发人员,比如在其他技术中工作过的 WPF 或 Silverlight,会感到非常熟悉,因为相同的基本概念适用于 WinRT XAML。

第三个选项存在,主要是为 C++开发人员——DirectX。DirectX 是 Windows 平台上最低级和最强大的图形 API;因此,它主要用于创作游戏,同时利用机器的全部潜力,充分发挥图形处理单元GPU)的能力。由于 DirectX 本身是基于 COM 构建的,因此它自然可以从 C++中访问。其他语言必须通过一些包装库来直接访问 DirectX API(微软在撰写时没有提供这样的包装器,但有第三方库,如 SharpDX)。

创建您的第一个商店应用

足够的谈话。是时候打开 Visual Studio 并创建一个简单的 C++商店应用程序,看一下它的一些特点。我们将在下一章中更深入地了解 Windows 商店应用程序的构建方式。

商店应用程序必须使用运行在 Windows 8(或更高版本)上的 Visual Studio 2012(或更高版本)创建;尽管 Visual Studio 2012 可以在 Windows 7 上运行,但不能用于在该操作系统上开发商店应用程序。

让我们打开 Visual Studio 2012,并通过选择Visual C++节点下的Windows Store节点来创建一个新的商店应用程序项目:

创建您的第一个商店应用程序

在右侧选择空白应用程序(XAML),在名称文本框中输入CH01.HelloLive,然后在您的文件系统中输入一些位置;然后点击确定

Visual Studio 创建了一个带有多个文件的项目。我们稍后会看一下这些文件,但现在打开MainPage.xaml文件。这是 UI 所在的位置。默认情况下,它具有一个分割视图,下面的窗格显示 XAML 标记,上面的窗格显示预览。XAML 显示了一个Page根元素,具有多个属性和内部的Grid元素。我们将在第三章中讨论所有细节,使用 XAML 构建 UI,但现在我们将创建一个简单的加法计算器作为我们的第一个“Hello World!”应用程序。在Grid元素内添加以下标记:

<StackPanel Orientation="Horizontal" Margin="20" VerticalAlignment="Center">
    <TextBox Width="150" Margin="10" x:Name="_number1" FontSize="30" Text="0" TextAlignment="Right"/>
    <TextBlock Text="+" Margin="10" FontSize="30" VerticalAlignment="Center"/>
    <TextBox Width="150" Margin="10" x:Name="_number2" FontSize="30" Text="0" TextAlignment="Right"/>
    <TextBlock Text="=" Margin="10" FontSize="30" VerticalAlignment="Center"/>
    <TextBlock Text="?" Width="150" Margin="10" x:Name="_result" FontSize="30" VerticalAlignment="Center"/>
    <Button Content="Caclulate" Margin="10" FontSize="25" />
</StackPanel>

提示

下载示例代码

您可以从您在www.PacktPub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.PacktPub.com/support并注册,以便将文件直接发送到您的电子邮件。

上部预览部分应该显示类似于这样的内容:

创建您的第一个商店应用程序

两个TextBox控件(名为_number1_number2)用于用户输入,一个TextBlock元素(名为_result)用于输出。为了使其工作,我们需要处理ButtonClick事件。要做到这一点,只需在设计器中双击按钮。这将在MainPage.xaml.cpp文件中添加一个事件处理程序(以及相应的头文件和 XAML 中按钮的Click属性)。Visual Studio 应该会自动打开MainPage.xaml.cpp。生成的事件处理程序将如下所示:

void CH01_HelloLive::MainPage::Button_Click_1(
    Platform::Object^ sender, 
    Windows::UI::Xaml::RoutedEventArgs^ e)
{
}

在文件顶部,Visual Studio 创建了一些使用命名空间声明,我们可以利用这些声明来简化方法签名(CH01_HelloLivePlatformWindows::UI::XAML命名空间):

void MainPage::Button_Click_1(Object^ sender, RoutedEventArgs^ e)
{
}

此时,处理程序可能看起来很奇怪,至少是因为“帽子”(^)符号粘在ObjectRoutedEventArgs类上。我们将在下一章中讨论这一点(这是 C++/CX 扩展),但帽子基本上意味着对 WinRT 对象的“引用计数指针”。

现在剩下的就是实现处理程序,以便在结果TextBlock中显示计算结果。

事实证明,给元素命名使这些名称成为了该类(MainPage)的实际成员变量,并且因此可以在需要时供我们使用。

首先,我们需要提取要相加的数字,但TextBox控件的内容是一个字符串。实际上,它是一个 WinRT 字符串,Platform::String。我们如何将其转换为数字?我们使用一些 WinRT 函数吗?

不,我们使用普通的 C++;我们只需要一种方法将Platform::String转换为普通的std::stringstd::wstring(应优先选择wstring,因为所有 WinRT 字符串都是 Unicode)。幸运的是,使用Platform::StringData()成员函数将返回一个简单的指向字符串的const wchar_t*;请注意,Unicode 指针是唯一可用的。

要进行实际的转换,我们可以使用旧的 C 风格函数,比如wtoi(),但为了更好的、现代化的转换,我们将使用字符串流。在文件的顶部附近(现有包含之后)添加一个#include,包括<sstream>

#include <sstream>

接下来,在事件处理程序中,我们将创建两个wstringstream对象来处理基于TextBox控件内容的转换:

std::wstringstream ss1(_number1->Text->Data()), 
    ss2(_number2->Text->Data());

注意箭头(->)操作符的工作。这些“帽子”引用是使用箭头解引用操作符进行解引用的,但它们不是指针(第二章,用于 Windows 8 商店应用的 COM 和 C++将进一步解释)。让我们继续转换为整数:

int number1, number2;
ss1 >> number1;
ss2 >> number2;

注意

我们实际上可以使用新的 C++ 11 函数std::stoi更快地进行转换,它将std::string(或std::wstring)转换为整数。

最后,我们需要将添加数字的结果放到名为_resultTextBlock中:

_result->Text = (number1 + number2).ToString();

对整数进行ToString()调用,提供了转换为Platform::String,在这种情况下非常方便。怎么可能在int上有一个成员函数?这是可能的,因为这是 WinRT 的int,而所有 WinRT 类型都派生自一个名为Platform::Object的最终基类(这并不严格正确,因为这是通过编译器的技巧实现的。更详细的解释将在下一章中提供),它公开了一个ToString()虚方法,可以被派生类型重写。然而,int是 C++中的一个原始类型,不应该派生自任何东西,那么它怎么可能有一个ToString()方法呢?我们马上就会回到这个问题。

现在,让我们测试一下应用程序。通过选择菜单中的调试 | 开始调试来构建项目并在调试器中运行它,点击相关的工具栏按钮(默认情况下带有绿色箭头并标有本地计算机)或者简单地按下F5

一个带有交叉矩形的启动画面应该会出现几秒钟,然后应用程序的用户界面应该会出现。在文本框中输入两个数字,然后点击按钮观察结果:

创建你的第一个商店应用程序

并不是太复杂,但仍然是一个商店应用程序!请注意,该应用程序是全屏的,没有标题栏、标题按钮,甚至没有关闭按钮。这就是商店应用程序的外观。

关闭应用程序

我们如何关闭应用程序?一个不太方便的方法是用鼠标抓住窗口的顶部(原本是标题栏的地方)并将其拖到底部。这主要是因为商店应用程序不是用来明确关闭的。如果应用程序没有使用,它将被挂起(不消耗 CPU),如果内存压力很大,它可以被终止;这意味着典型用户不应该关心关闭应用程序。

幸运的是,我们不是典型的用户。关闭应用程序的一个更简单的方法是Alt + Tab回到 Visual Studio 并从菜单中选择调试 | 停止调试(或Shift + F5)。这就是为什么最好在附加了调试器的情况下从 Visual Studio 测试商店应用程序。

注意

按下Alt + F4也可以作为关闭应用程序的方法。

应用程序部署

我们可以在没有 Visual Studio 的情况下运行应用程序吗?我们可以导航到源代码构建的文件夹并找到生成的.exe文件。在 Windows 资源管理器中双击该文件会出现以下消息框:

应用程序部署

错误消息基本上是在说我们不能简单地像运行桌面应用程序那样运行商店应用程序,启动商店应用程序涉及几个步骤,简单的双击是不够的。那么,我们如何在没有 Visual Studio 的情况下运行应用程序呢?和“普通”用户一样,通过开始屏幕。

如果我们打开开始屏幕并导航到最右边,我们会发现类似这样的东西:

应用程序部署

应用程序被 Visual Studio 自动部署,就好像它是从 Windows 商店下载的一样。实际上,可以通过从 Visual Studio 的菜单中选择生成 | 部署解决方案来仅进行部署而不运行应用程序。要删除应用程序,请在开始屏幕上右键单击它(或从底部滑动)并选择卸载

int.ToString 是从哪里来的?

为了找出这一点,我们将在我们实现的点击事件处理程序的最后一行上设置一个断点,并运行应用程序直到达到断点。当断点触发时,在编辑器中右键单击断点行并选择转到反汇编。这是该点的汇编代码的前几行:

int.ToString 是从哪里来的?

最后一行很有趣,调用了一个名为default::int32::ToString的静态函数。我们可以通过 Step Over(F10)到达该行,然后 Step Into(F11)。经过几次 Step Into,我们最终到达了实际的函数。右键单击窗口并选择转到源代码,可以省略详细的汇编代码,显示名为basetypes.cpp的文件中实现的代码,如下所示:

  VCCORLIB_API Platform::String^ int32::ToString()
  {
    wchar_t buf[32];
    swprintf_s(buf,L"%I32d", _value);
    return ref new Platform::String(buf);
  }

所有这些都在一个名为default的命名空间中。实现是微不足道的,它使用了经典的swprintf C 函数的“安全”变体,然后将其转换回 WinRT 字符串,即Platform::String。奇怪的ref new将在下一章中讨论,但它基本上意味着“创建一个 WinRT 类型的实例”。

C++/CX 库中存在类似的辅助程序,使得从 C++使用 WinRT 更容易。我们将在下一章中看到更多相关内容。

项目结构

让我们更仔细地看一下我们创建的项目中创建的一些文件:

项目结构

从 C++开发人员的角度来看,大多数文件都是新的,除了pch.hpch.cpp文件。这些文件构成了预编译头文件,这意味着它包含了很少更改的头文件,因此可以只编译一次,节省后续的重新编译。在其他项目类型中,如常规的 Win32 应用程序、MFC、ATL 等,这些文件被命名为StdAfx.h/StdAfx.cpp(没有实际意义),因此它们的名称更改得更好。它们的用法完全相同,即将很少更改的头文件放置在一起以加快编译时间。

注意

保持预编译头文件名为pch.h很重要;这是因为构建过程生成的一些代码使用了这个硬编码的文件名。

MainPage.xaml包含了MainPage类的 XAML 标记。完成它的另外两个文件是 H 和 CPP 文件。请注意,CPP 文件包含对MainPage.xaml.h#include,而该文件包含对MainPage.g.h#include,后者是由 XAML 编译器生成的(这就是“g”的含义),实际上,它会根据编辑MainPage.xaml而随需更改,而无需进行任何实际的编译。在那里,我们可以找到我们使用的三个命名元素的声明,而无需自己声明它们:

private: ::Windows::UI::Xaml::Controls::TextBox^ _number1;
private: ::Windows::UI::Xaml::Controls::TextBox^ _number2;
private: ::Windows::UI::Xaml::Controls::TextBlock^ _result;

MainPage.xaml本身通过其根元素上的x:Class属性指示了它与哪个类相关:

<Page x:Class="CH01_HelloLive.MainPage"

App.xamlApp.xaml.hApp.xaml.cpp之间有着相同的连接方式,但它们的含义有些不同。App.xaml.h声明了提供应用程序入口点以及其他将在后续章节中讨论的服务的单个应用程序类。或许你会好奇为什么它有一个 XAML 文件。应用程序对象可以有 UI 吗?实际上不是。XAML 主要用于托管资源,正如我们将在第三章中看到的那样,使用 XAML 构建 UI

Package.appxmanifest 文件是存储应用程序所有元数据的地方。在内部,它是一个 XML 文件,但 Visual Studio 将其包装在一个漂亮的 UI 中,大多数时间更容易使用。双击文件打开 Visual Studio 的清单视图:

项目结构

在这里,我们可以设置应用程序的名称、描述、支持的方向、各种图像(如启动画面图像)以及许多其他(更重要的)设置,比如应用程序所需的功能。我们将在相关章节讨论各种选项。

如果需要以 XML 的原始视图查看文件,我们可以在“解决方案资源管理器”中右键单击文件,选择“打开方式”,然后选择“XML 编辑器”。以下是我们计算器应用程序的 XML 内容:

<Package >

  <Identity Name="a984fde4-222a-4c90-b9c1-44ad95e01400"
            Publisher="CN=Pavel"
            Version="1.0.0.0" />

  <Properties>
    <DisplayName>CH01.HelloLive</DisplayName>
    <PublisherDisplayName>Pavel</PublisherDisplayName>
    <Logo>Assets\StoreLogo.png</Logo>
  </Properties>

  <Prerequisites>
    <OSMinVersion>6.2.1</OSMinVersion>
    <OSMaxVersionTested>6.2.1</OSMaxVersionTested>
  </Prerequisites>

  <Resources>
    <Resource Language="x-generate"/>
  </Resources>

  <Applications>
    <Application Id="App"
        Executable="$targetnametoken$.exe"
        EntryPoint="CH01_HelloLive.App">
        <VisualElements
            DisplayName="CH01.HelloLive"
            Logo="Assets\Logo.png"
            SmallLogo="Assets\SmallLogo.png"
            Description="CH01.HelloLive"
            ForegroundText="light"
            BackgroundColor="#464646">
            <DefaultTile ShowName="allLogos" />
            <SplashScreen Image="Assets\SplashScreen.png" />
        </VisualElements>
    </Application>
  </Applications>
  <Capabilities>
    <Capability Name="internetClient" />
  </Capabilities>
</Package>

根元素是 Package。其他所有内容都是与默认设置不同的设置。例如,Capabilities 元素显示了应用程序需要的必要功能。里面唯一的元素是 internetClient。在 Visual Studio 清单 UI 中点击“功能”选项卡即可查看:

项目结构

Internet (Client) 选项已被选中(默认请求的唯一功能),这意味着应用程序可以向网络发出呼叫。

更改 XML 会影响 Visual Studio UI,反之亦然。有时,在 XML 模式下编辑更方便。

总结

Windows 8 商店应用程序在许多方面与桌面应用程序不同。从外观到执行方式,当然还有它们所依赖的运行时。Windows Runtime 提供了一个丰富的环境,用于创建在桌面和平板平台上运行的应用程序,但它是新的,因此需要熟悉库和整个平台。

Windows Runtime 基于 COM 编程模型,可以为各种语言和运行时创建投影。目前支持 C++、.NET 和 JavaScript,但未来可能会由微软和/或其他供应商创建更多。

C++ 开发人员可以最精细地直接访问 WinRT。我们将在下一章更详细地了解的 C++/CX 扩展使得使用 C++ 开发几乎和使用更高级别的环境一样简单,同时利用现有 C++ 库和 C++ 语言的强大功能。

第二章:Windows 8 商店应用的 COM 和 C++

C++最初由其创造者和最初的实施者 Bjarne Stroustrup 于 1985 年首次向公众发布。它最初被命名为“C with Classes”,扩展了 C 语言以包括真正的面向对象的特性。它在 1998 年以“官方”形式出版,并开始获得真正的发展。1998 年,该语言的 ISO 标准出现,稍后在 2003 年进行了轻微修订。

直到 2011 年,没有新的标准,最终在 2011 年,标准委员会最终确定了新的 C++标准(这个过程进行了几年)。从 1998 年到 2011 年之间没有官方标准,使得 C++不像以前那样受欢迎,主要是因为出现了新的语言和平台,主要是 Java(在各种 Java 平台上)和 C#(在.NET 平台上)。传统上用 C++编写的数据驱动应用程序在非微软世界中是用 Java(现在仍然是)和 C#(在微软世界中,以及在一定程度上,其他基于.NET 的语言,如 Visual Basic)编写的。C++仍然是一种有很多追随者的语言,但缺乏进展显示了 C++生态系统的裂痕。

这 13 年的间隔并不是 C++中没有进展,而是在库领域,而不是语言领域。最显著的贡献是 boost 库(www.boost.org),它贡献了许多高质量的库,扩展了标准 C++库;尽管 boost 不是官方标准,但它已成为 C++社区中的事实标准。事实上,boost 的部分内容已经成为新的 C++11 标准。

注意

C++委员会已决定加快未来标准的进程,并计划在 2014 年(以及 2017 年后)制定新的标准;时间会告诉我们。

欢迎使用 C++11

C++11 标准经过数年的开发,最初被称为“C++0x”,希望“x”是一个个位数,使得标准最迟在 2009 年完成,但事情并没有按照计划进行,标准最终在 2011 年 9 月才最终确定。

注意

可以用十六进制的 11 的十进制等价物“b”替换“x”,如果需要的话,仍然保持一个个位数。

C++11 有许多新特性,部分是核心语言的一部分,部分是新的标准库的一部分。13 年在计算机年代几乎是一个永恒,这就是为什么语言中有这么多的添加;事实上,在撰写本文时,没有一个编译器(包括微软和非微软)实现了整个 C++11 标准。Visual Studio 2010 已经实现了一些功能,Visual Studio 2012 实现了一些更多的功能(并增强了现有功能);可能要过一段时间,直到所有 C++11 功能都被编译器实现。

注意

有关 VS 2012 和 VS 2010 中支持的 C++11 功能的全面列表,请参阅 Visual C++团队的博客文章:blogs.msdn.com/b/vcblog/archive/2011/09/12/10209291.aspx。预计将在相对频繁的更新中提供更多功能。

C++11 中的新特性

我们将看一些新的 C++11 语言和库特性,使得使用 C++更容易开发,不一定与 Windows 8 商店应用相关。

nullptr

nullptr是一个新的关键字,取代了著名的值NULL,作为一个指向无处的指针。这似乎不是一个主要的特性,但这使得任何#defineNULL的定义都是不必要的,也解决了一些不一致的地方。考虑以下重载函数:

void f1(int x) {
  cout << "f1(int)" << endl;
}

void f1(const char* s) {
  cout << "f1(const char*)" << endl;
}

通过调用f1(NULL)会调用哪个函数?(也许令人惊讶的)答案是f1(int)。原因是 Microsoft 编译器将NULL定义为简单的零,编译器将其解释为整数而不是指针;这意味着编译器的重载解析选择了f1(int),而不是f1(const char*)nullptr解决了这个问题;调用f1(nullptr)会调用正确的函数(接受实际指针)。从纯粹主义的角度来看,很难想象一个指针至关重要的语言没有一个专门的关键字来指示指向空的指针。这主要是为了与 C 语言兼容的原因;现在它终于解决了。

在 C++/CX(我们将在本章后面讨论),nullptr用于指示对空的引用。

auto

auto关键字自 C 语言以来就存在,它是一个多余的关键字,意思是“自动变量”,意思是基于栈的变量。以下 C 声明是合法的,但没有真正的价值:

auto int x = 10;

在 C++11 中,auto用于告诉编译器根据初始化表达式自动推断变量的类型。以下是一些声明:

int x = 5;
string name = "Pavel";
vector<int> v;
v.push_back(10);
v.push_back(20);
for(vector<int>::const_iterator it = v.begin(); it != v.end(); 
   ++it)
  cout << *it << endl;

这看起来很普通。让我们用auto关键字替换它:

auto x = 5;
auto name = "Pavel";
vector<int> v;
v.push_back(10);
v.push_back(20);
for(auto it = v.begin(); it != v.end(); ++it)
  cout << *it << endl;

运行这些代码片段会产生相同的结果(在迭代vector时显示1020)。

x初始化为5使用auto并不比指定实际类型(int)好多少;事实上,它更不清晰(顺便说一句,5int,而5.0double,依此类推)。auto的真正威力在于复杂类型,比如前面的迭代器示例。编译器根据初始化表达式推断出正确的类型。这里没有运行时的好处,只是编译时的推断。但是,它使代码(通常)更易读,更不容易出错。变量类型不是某种空指针,它就像类型被明确指定一样。如果xint,它将永远是int。程序员不必过多考虑正确的类型,我们知道它是一个迭代器(在前面的示例中),为什么我们要关心确切的类型呢?即使我们关心,为什么我们要写完整的类型(可能包含进一步扩大类型表达式的模板参数),因为编译器已经知道确切的类型了?auto可以简化事情,正如我们稍后将看到的,当处理非平凡的 WinRT 类型时。

那么字符串初始化呢?在非auto情况下,我们明确使用了std::string。那么auto情况呢?结果是name的类型是const char*而不是std::string。这里的重点是有时我们需要小心,我们可能需要指定确切的类型以防止不必要的编译器推断。

注意

当然,像auto x;这样指定的东西是无法编译的,因为x可以是任何类型——必须有一个初始化表达式。同样,指定像auto x = nullptr;这样的东西也无法编译;同样,因为x可以是任何指针类型(甚至是具有适当转换构造函数的非指针类型)。

Lambda

Lambda 函数,或简称 lambda,是一种在需要的地方内联指定的匿名函数的方法。让我们看一个例子。假设我们想使用transform算法从容器中取一些项目,并根据一些转换函数生成新项目。这是transform的一个原型:

template<class InIt, class OutIt, class Fn1> 
OutIt transform(InIt First, InIt Last, OutIt Dest, Fn1 Func);

transform模板函数接受作为最后一个参数的转换函数,该函数将在指定的起始和结束迭代器上调用每个项目。

指定该函数的一种方法是设置一个全局(或类静态)函数,如下面的代码片段所示:

double f1(int n) {
  return ::sqrt(n);
}

void LambdaDemo() {
  vector<int> v;
  for(int i = 0; i < 5; i++)
    v.push_back(i + 1);
  for each (int i in v)
    cout << i << endl;

  vector<double> v2(5);

  ::transform(begin(v), end(v), begin(v2), f1);

  cout << endl;
  for each (double d in v2)
    cout << d << endl;

f1作为最后一个参数传递给transform,使v2包含v中相应数字的平方根。

这是提供函数的“C 方式”—通过函数指针。它的一个缺点是函数无法维护状态。在 C++中,我们可以使用函数对象,称为“函数对象”—一个伪装成函数的对象:

class SqrtFunctor {
public:
  double operator()(int n) {
    return ::sqrt(n);
  }
};

以及使用它的代码:

::transform(begin(v), end(v), begin(v2), SqrtFunctor());

在这种简单情况下没有维护状态,但它能工作是因为函数调用运算符的重载;transform不在乎,只要它是可以调用的东西。

这仍然不是理想的情况——在这两种情况下,我们都失去了代码局部性——因为调用的函数在其他地方。lambda 通过将代码嵌入到需要的地方来解决这个问题:

::transform(begin(v), end(v), begin(v2), [](int n) {
  return ::sqrt(n);
});

Lambda 函数的语法一开始可能看起来很奇怪。语法包括以下几个要素:

  • 在方括号中的变量捕获列表(在示例中为空)

  • 函数的参数(根据其使用预期),在前面的例子中是一个int,因为transform期望的是这样的输入迭代器,指向一个int类型的集合

  • 实际的函数体

  • 使用一些新的 C++11 语法,可选(有时不太)返回类型说明符:

::transform(begin(v), end(v), begin(v2), [](int n) -> double {
  return ::sqrt(n);
});

注意

std::beginstd::end函数是 C++11 中的新功能,提供了与容器的beginend成员函数相对应的更方便的等价物。这些也适用于 WinRT 集合,正如我们将在后面看到的那样。

使用 lambda 有两个好处:

  • 代码局部性得到保持。

  • 通过在 lambda 内部“捕获”外部作用域变量,可以使用外部作用域变量。如果它是一个单独的函数,将很难为外部作用域变量“传递”值。

可以通过值或引用来捕获变量。以下是一些例子:

  • [=]通过值捕获所有外部作用域的变量

  • [x, y]通过值捕获xy

  • [&]通过引用捕获所有外部作用域变量

  • [x, &y]通过值捕获x,通过引用捕获y

  • 没有捕获,lambda 函数体只能使用提供的参数和自己声明的变量

我们将广泛使用 lambda,特别是在处理异步操作时,正如我们将在本章后面看到的那样。

智能指针

智能指针不是一种语言特性,而是新标准库的一部分。它们最初由 boost 引入,提供动态分配对象的自动管理。考虑这种简单的对象分配:

Car* pCar = new Car;

这是一个非常典型的动态分配。问题在于,它必须在某个时刻被释放。这似乎很容易,给出以下语句:

pCar->Drive(); // use the car
delete pCar;

即使这个看似简单的代码也有问题;如果对Car::Drive的调用抛出异常怎么办?在这种情况下,删除的调用将被跳过,我们就会有一个内存泄漏。

解决方案?通过自动分配的对象包装指针,其中构造函数和析构函数做正确的事情:

class CarPtr {
public:
  CarPtr(Car* pCar) : _pCar(pCar) { }
  Car* operator->() const { return _pCar; }
  ~CarPtr() { delete _pCar; }

private:
  Car* _pCar;
};

这被称为资源获取即初始化RAII)。operator->确保访问Car实例是透明的,使智能指针足够智能,不会干扰汽车的正常操作:

CarPtr spCar(pCar);
spCar->Drive();

析构函数负责销毁对象。如果抛出异常,无论如何都会调用析构函数(除了灾难性的电源故障等),确保在搜索catch处理程序之前销毁Car实例。

CarPtr类是一个非常简单的智能指针,但有时仍然可能有用。C++11 以std::unique_ptr<T>类的形式提供了这个想法的通用实现,其中T是要管理其指针的类型。在我们的情况下,我们可以这样编写Car客户端代码(我们需要为此#include <memory>):

unique_ptr<Car> spCar2(new Car);
spCar2->Drive();

注意

unique_ptr<>的实际定义比这里显示的简单CarPtr更复杂。例如,对于具有不同Car对象指针的赋值运算符怎么办?对于赋值给nullptr怎么办?为什么析构函数要调用delete——也许对象是以其他方式分配的?这些和其他细节都由unique_ptr<>正确处理。

unique_ptr<>足够简单,但是对于需要传递的对象呢?在unique_ptr的析构函数中销毁对象会过早。为此,我们需要引用计数,这样当对象传递给某个函数(或者更有趣的是,另一个线程)时,计数器应该递增。当智能指针的析构函数被调用时,它应该递减计数器,只有当计数器达到零时,它才应该实际销毁对象。这正是另一个新的智能指针类shared_ptr<T>的作用。以下是一个Car对象的示例:

void UseCar(shared_ptr<Car> car) {
  // ref count: 2
  car->Drive();
}

void CreateCar() {
  shared_ptr<Car> spCar3(new Car); // ref count: 1
  UseCar(spCar3);	// ref count: 2
  // back to ref count of 1
  spCar3->Drive();
}

shared_ptr<>的好处在于它适用于任何类型,类型不需要具有任何特殊属性。shared_ptr<>分配了一个与提供的对象相关联的额外引用计数。

初始化shared_ptr<>的首选方法是使用std::make_shared<>模板函数,该函数接受传递给实际类型构造函数的参数。它在一个块中创建对象实例(例如Car)以及引用计数,因此额外高效。在Car示例中,如下所示:

shared_ptr<Car> spCar3 = make_shared<Car>();
spCar3->Drive();

提示

重要的是不要混合智能指针(如shared_ptr<>)和普通指针,否则对象可能在其他代码片段仍在使用它的常规指针时被销毁。

引用计数的一个警告是循环引用的问题。例如,如果某些代码创建对象 A,该对象在其构造函数中创建对象 B 并将自身传递给 B,B 持有对 A 的智能指针,然后在某个时刻客户端释放了其 A 智能指针,这个循环将使 A 和 B 永远存在:

智能指针

原始客户甚至不知道发生了内存泄漏。这是需要注意的问题,正如我们将在后面看到的,WinRT 对象也存在相同的问题,它们也是引用计数的。

避免这个问题的一种方法是使用 C++11 中的另一个智能指针类std::weak_ptr<>。顾名思义,它持有对象的弱引用,这意味着它不会阻止对象自毁。这将是 B 在前面图表中持有对 A 的引用的方式。如果是这样,我们如何访问实际对象以便在需要时使用?或者更确切地说,我们如何知道它实际上仍然存在?这是做法:

shared_ptr<Car> spCar3 = make_shared<Car>();
spCar3->Drive();

weak_ptr<Car> spCar4(spCar3);

auto car = spCar4.lock();
if(car)
  car->Drive();
else
  cout << "Car gone" << endl;

weak_ptr<>::lock函数返回对问题对象的shared_ptr<>。如果没有对象,内部指针将为 null。如果有对象,则其引用计数会增加,即使原始的shared_ptr<>被释放,也会保护它免受过早销毁。在初始化spCar4之后添加以下行将在显示器上显示Car gone

spCar3 = nullptr;

注意

还有另一种打破循环的方法。A 可以实现一个特定的方法(例如Dispose),客户端必须显式调用该方法。在该方法中,A 将释放其对 B 的指针,从而打破循环。这里的问题与手动使用new/delete有些类似——函数需要在正确的时间调用。如果调用得太早,它将使对象无法使用;通常更喜欢使用weak_ptr<>

在今天的 C++中,推荐的方法是始终使用智能指针,而不是使用原始指针。使用newdelete运算符被认为是维护的头痛,可能会导致内存泄漏或因为过早对象销毁而导致损坏。智能指针在传递时很便宜,并且即使在出现异常时也能保证正确的行为。

结论

C++11 有许多新特性,无论是在语言中还是在标准库中。我们已经看到了一些有用的特性,但肯定还有其他的,比如rvalue引用,它提供了一种避免昂贵的复制操作的方式(实际上,在容器类中使用,比如std::vector<>),还有一个新的enum class声明,解决了经典枚举的外部范围问题,以及其他许多特性。

注意

要全面了解新的 C++11 特性,请使用诸如微软的 Channel 9(channel9.msdn.com)、Visual C++团队的博客(blogs.msdn.com/b/vcblog/)和维基百科(en.wikipedia.org/wiki/C%2B%2B11)等网络资源。此外,几乎最终的 C++11 标准草案可以在www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf找到。

COM 和 WinRT

COM 技术是由微软在 1993 年左右创建的。它最初被命名为 OLE 2.0,因为它被用来实现 Microsoft Office 套件中的对象链接和嵌入OLE)功能。例如,这个功能允许在 Word 文档中嵌入(或链接)一个 Excel 表格。OLE 的第一个版本(称为 OLE 1.0)是由称为动态数据交换DDE)的东西实现的,它是基于消息的长期 Windows 功能。微软意识到 OLE 只是更一般技术的一种可能用途,因此将 OLE 2.0 重命名为 COM。

COM 包含许多细节,但基于很少的原则:

  • 客户端程序针对接口,而不是具体对象

  • 组件是动态加载的,而不是静态加载的

什么是接口?在面向对象的术语中,接口是一种抽象,它将一组相关的操作分组在一起。这种抽象没有实现,但各种类型可以以适当的方式实现接口。客户端可以使用不同的实现,因为它仅依赖于接口(作为合同),而不依赖于可能间接提供的任何特定实现,例如某个工厂组件。

COM 接口是更加精确的东西。它指定了由接口的实现者提供并由客户端使用的特定二进制布局。由于布局是预先知道的,所以提出的合同不仅仅是逻辑的,还是二进制的。这导致在使用 COM 时可以混合语言或技术的可能性。COM 类可以由(比如)C++编写,但可以被 Visual Basic 或 C#(在这种情况下是.NET 平台)消耗,假设这些语言知道所讨论的接口的二进制布局。

COM 接口的布局是虚拟表(也称为 V 表),这是在 C++中实现虚拟函数的最常见机制,使 C++成为开发 COM 组件和 COM 客户端的自然选择。以下是 C++中一个简单接口的定义,作为纯抽象类:

class ICar {
public:
  virtual void Drive() = 0;
  virtual void Start() = 0;
  virtual void Refuel(double amount) = 0;
};

注意

按照惯例,COM 中的接口名称以大写的“I”开头,然后是帕斯卡命名法的名称(例如IAnimalILibraryIObjectBuilder)。

以下是这个接口的一个简单(内联)实现:

class Porche : public ICar {
public:
  Porche() : _running(false), _fuel(50), _speed(0) { }

  void Start() {
    if(_running)
      throw new std::exception(
   "car is already running");
    _running = true;
  }

  void Drive() {
    if(!_running)
      throw new std::exception("car is not running");
    _speed += 10;
  }

  void Refuel(double amount) {
    if((_fuel += amount) > 60)
      _fuel = 60;
  }

private:
  bool _running;
  double _fuel;
  double _speed;
};

我们可以使用任何ICar接口指针,而不知道实际实现的任何信息:

void UseCar(ICar* pCar) {
  pCar->Start();
  pCar->Drive();
  pCar->Drive();
  pCar->Refuel(30);
}

通过使用 Visual Studio 调试器查看Porche类的实例在内存中,我们发现:

COM 和 WinRT

实例中的第一个字段是指向 v 表(vptr)的指针。该 v 表保存了在特定ICar实现Porche上实际实现的函数的指针。在 v 表指针之后,我们看到了实现声明的成员变量。但是使用接口指针,无法知道存在哪些成员变量(如果有的话);这是一个不应该关心客户端的实现细节。

让我们定义另一个接口:

class ICarrier {
public:
  virtual int PlaceObject(double weight) = 0;
  virtual void RemoveObject(int objectID) = 0;
};

Porche类希望实现ICarrier以及ICar。这是修订后的定义:

class Porche : public ICar, public ICarrier {

我们将添加一些字段来管理安装在汽车上的对象:

double _totalWeight;
static int _runningID;
std::map<int, double> _objects; 

并实现ICarrier中的两个方法(函数):

int PlaceObject(double weight) {
  if(_totalWeight + weight > 200)
    throw std::exception("too heavy");
  _totalWeight += weight;
  _objects.insert(std::make_pair(++_runningID, weight));
  return _runningID;
}

void RemoveObject(int objectID) {
  auto obj = _objects.find(objectID);
  if(obj == _objects.end())
    throw new std::exception("object not found");
  _totalWeight -= obj->second;
  _objects.erase(obj);
}

此时确切的实现本身并不重要,只是内存中对象的布局:

COM and WinRT

Porche实例的前两个成员是指向ICarICarrier的 v-table 指针(按顺序),每个指向一个函数指针的虚拟表。然后才放置实例成员变量。这里有一个图表可能更清晰地显示这一点:

COM and WinRT

现在,假设客户端持有一个ICar*接口,并希望查看对象是否实现了ICarrier。进行 C 风格的转换(或reinterpret_cast<>)只会简单地使相同的指针值认为它指向另一个 v-table,但实际上并不是。在这种情况下,调用ICarrier::PlaceObject实际上会调用ICar::Start,因为那是该 v-table 中的第一个函数;并且函数是通过偏移量调用的。

我们需要动态查询另一个接口是否支持,使用dynamic_cast<>运算符:

cout << "pCar: " << pCar << endl;
ICarrier* pCarrier = dynamic_cast<ICarrier*>(pCar);
if(pCarrier) {
  // supported
  cout << "Placing object..." << endl;
  int id = pCarrier->PlaceObject(20);
  cout << "pCarrier: " << pCarrier << endl;
}

如果成功,dynamic_cast会调整指针到正确的 v-table。在Porche的情况下,pCarrier的值应该比pCar大一个指针大小(在 32 位进程中为 4 个字节,在 64 位进程中为 8 个字节)。我们可以通过打印它们的值来验证:

COM and WinRT

偏移量为 4,因为这段代码是以 32 位编译的。

dynamic_cast<>的问题在于它是特定于 C++的。其他语言会如何获取对象上的另一个接口?解决方案是将该功能因子化到每个接口中。结合引用计数,这导致了 COM 世界中最基本的接口,IUnknown

IUnknown 接口

IUnknown接口是每个 COM 接口的基本接口。它封装了两个功能:查询可能支持的其他接口和管理对象的引用计数。这是它的定义:

class IUnknown {
public:
  virtual HRESULT __stdcall QueryInterface(const IID& iid, 
      void **ppvObject) = 0;
  virtual ULONG __stdcall AddRef() = 0;
  virtual ULONG __stdcall Release() = 0;
};

QueryInterface允许根据接口 ID 获取另一个支持的接口,这是一个全局唯一标识符GUID)—一个由算法生成的 128 位数字,可以统计保证唯一性。返回的值,一个HRESULT是 COM(和 WinRT)中的标准返回类型。对于QueryInterfaceS_OK (0)表示一切正常,请求的接口存在(并间接通过ppvObject参数返回),或者E_NOINTERFACE,表示不支持这样的接口。

注意

所有 COM/WinRT 接口方法都使用标准调用约定(__stdcall),这意味着被调用方负责清理调用堆栈上的参数(而不是调用方)。这在 32 位世界中很重要,因为有几种调用约定。由于 COM 旨在实现跨技术访问,这是合同的一部分(在 x64 中只存在一种调用约定,因此这并不那么重要)。

AddRef增加对象的内部引用计数,Release减少它,如果计数达到零则销毁对象。

注意

请记住,这只是一个接口,还有其他的实现可能。例如,对于一个始终希望保留在内存中的对象(如单例),AddRefRelease可能什么也不做。然而,大多数对象都是按照描述的方式实现的。

任何 COM 接口都必须派生自IUnknown;这意味着每个 v-table 至少有三个条目对应于QueryInterfaceAddRefRelease(按顺序)。

IInspectable 接口

WinRT 可以被视为更好的 COM。IUnknown接口的一个缺点是没有标准的方法来询问对象返回它支持的接口列表。WinRT 定义了一个新的标准接口IInspectable(当然是从IUnknown派生的),提供了这种能力:

class IInspectable : public IUnknown {
public:
  virtual HRESULT __stdcall GetIids(ULONG *iidCount, 
      IID **iids) = 0;
  virtual HRESULT __stdcall GetRuntimeClassName(
      HSTRING *className) = 0;
  virtual HRESULT __stdcall GetTrustLevel(
      TrustLevel *trustLevel) = 0;
};

最有趣的方法是GetIids,返回对象支持的所有接口。这是由运行在 WinRT 之上的 JavaScript 引擎使用的,因为 JavaScript 中缺乏静态类型,但对于 C++客户端来说通常不太有用。

所有这一切的最终结果如下:

  • 每个 WinRT 接口必须继承自IInspectable。这意味着每个 v 表总是至少有六个条目,对应于方法QueryInterfaceAddRefReleaseGetIidsGetRuntimeClassNameGetTrustLevel(按照这个顺序)。

  • WinRT 类型至少实现了IInspectable,但几乎总是至少实现了另一个接口;否则,这个对象将会非常乏味。

下面的经典图表描述了一个 WinRT 对象:

IInspectable 接口

创建 WinRT 对象

正如我们所见,COM/WinRT 客户端使用接口来调用对象上的操作。然而,到目前为止有一件事被忽略了,那个对象是如何产生的?创建过程必须足够通用(而不是特定于 C++),以便其他技术/语言能够利用它。

我们将构建一个简单的示例,创建一个位于Windows::Globalization命名空间中的 WinRT Calendar类的实例,并调用一些其方法。为了消除所有可能的噪音,我们将在一个简单的 Win32 控制台应用程序中进行操作(而不是 Windows 8 商店应用程序),这样我们就可以专注于细节。

注意

最后一句也意味着 WinRT 类型(大部分情况下)可以从桌面应用程序和商店应用程序中访问和使用。这开启了有趣的可能性。

我们需要使用一些属于 Windows Runtime 基础设施的新 API。这些 API 以"Ro"开头(代表 Runtime Object)。为此,我们需要一个特定的#include和链接到适当的库:

#include <roapi.h>

#pragma comment(lib, "runtimeobject.lib")

现在,我们可以开始实现我们的主函数。首先要做的事情是在当前线程上初始化 WinRT。这是通过使用RoInitialize函数来实现的:

::RoInitialize(RO_INIT_MULTITHREADED);

RoInitialize需要为线程指定公寓模型。这可以是单线程公寓STA)表示为RO_INIT_SINGLETHREADED,也可以是多线程公寓MTA)表示为RO_INIT_MULTITHREADED。公寓的概念将在稍后讨论,对于当前的讨论来说并不重要。

注意

RoInitialize在概念上类似于经典的 COM CoInitialize(Ex)函数。WinRT 公寓与经典的 COM 公寓几乎是一样的。事实上,由于 WinRT 是建立在 COM 基础之上的,大部分事情都工作得非常相似。对象创建机制非常相似,只是细节上有一些变化,我们很快就会看到。

要创建一个实际的对象并获得一个接口指针,必须调用RoActivateInstanceAPI 函数。这个函数的原型如下:

HRESULT WINAPI RoActivateInstance(
  _In_   HSTRING activatableClassId,
  _Out_  IInspectable **instance
);

所需的第一个参数是类的全名,表示为HSTRINGHSTRING是标准的 WinRT 字符串类型,表示一个不可变的 Unicode(UTF-16)字符数组。存在多个 WinRT API 用于创建和操作HSTRING。正如我们稍后将看到的,C++/CX 提供了Platform::String类来包装HSTRING以便于使用。

RoActivateInstance的第二个参数是通过IInspectable接口指针表示的结果实例(请记住,所有 WinRT 对象必须支持这个接口)。

注意

感兴趣的读者可能会想知道为什么要创建一个新的字符串类型。毫无疑问,在 Microsoft 空间中已经有了很多这样的类型:std::string/wstring(C++),CString(ATL/MFC)和BSTR(COM)。BSTR似乎是最有可能的候选者,因为它不是特定于 C++的。新的HSTRING是不可变的,这意味着一旦创建就无法更改。任何明显的修改都会创建一个新的HSTRING。这个属性使HSTRING线程安全,并且更容易投射到其他平台,比如.NET,其中System.String类也是不可变的,所以没有不匹配。

要使用与HSTRING相关的 API,我们将添加#include<winstring.h>。现在我们可以继续为Calendar类创建一个HSTRING

HSTRING hClassName;
wstring className(L"Windows.Globalization.Calendar");
HRESULT hr = ::WindowsCreateString(className.c_str(),
   className.size(), &hClassName);

HSTRING是使用WindowsCreateString WinRT API 创建的,传递字符串文字和它的长度(这里是通过std::wstring的帮助获得的)。请注意,类名包括其完整的命名空间,其中点(.)是分隔符(而不是 C++的作用域解析运算符::)。

现在,我们可以调用RoActivateInstance(我在这些代码片段中省略了任何错误检查,以便我们可以集中精力在基本要点上),并获得一个日历的接口。由于这是IInspectable,所以并不是很有趣。我们需要一个更具体的日历接口,也就是说,我们需要调用QueryInterface来获得一个更有趣的接口来使用。

RoActivateInstance是做什么的?该实例是如何创建的?它是在哪里实现的?

该过程与经典的 COM 创建机制非常相似。RoActivateInstanceHKEY_LOCAL_MACHINE\Software\Microsoft\WindowsRuntime\ActiavatableClassId处查阅注册表,并查找名为Windows.Globalization.Calendar的键。这是来自RegEdit.exe的屏幕截图:

Creating a WinRT object

注意

屏幕截图显示了 64 位键。对于 32 位进程,该键位于HKLM\Software\Wow6432Node\Windows\WindowsRuntime\ActivatableClassId下。这对于进程来说是透明的,因为注册表 API 默认根据进程的“位数”去正确的位置。

Name键中存在几个值。最有趣的是:

  • DllPath - 指示实现 DLL 所在的位置。这个 DLL 被加载到调用进程的地址空间中,我们马上就会看到。

  • CLSID - 类名的相应 GUID。这并不像在经典的 COM 中那么重要,因为 WinRT 实现是通过完整的类名而不是 CLSID 来识别的,这一点可以从RoActivateInstance的第一个参数中看出。

  • ActivationType - 指示此类是在进程内(DLL,值为 0)还是在进程外(EXE,值为 1)激活。

在本讨论的其余部分,我们将假设是一个进程内类。RoActivateInstance调用另一个函数RoGetActivationFactory,它实际上是定位注册表键并将 DLL 加载到进程地址空间的实际工作。然后,它调用 DLL 中导出的名为DllGetActivationFactory的全局函数(DLL 必须导出这样一个函数,否则创建过程将失败),传入完整的类名,请求的工厂接口 ID 和输出接口指针作为结果:

HRESULT RoGetActivationFactory(
  _In_   HSTRING activatableClassId,
  _In_   REFIID iid,
  _Out_  void **factory
);

DLL 中的全局函数负责返回一个能够创建实际实例的类工厂。类工厂通常实现IActivationFactory接口,具有一个方法(除了IInspectable):

HRESULT ActivateInstance(IInspectable **instance);

返回类工厂是RoGetActivationFactory的工作。然后RoActivateInstance接管,并调用IActivationFactory::ActivateInstance来创建实际的实例,这就是RoActivateInstance的结果。

注意

熟悉经典 COM 的读者可能会注意到相似之处:RoActivateInstance替换了经典的CoCreateInstanceRoGetActivationFactory替换了CoGetClassObjectDllGetActivationFactory替换了DllGetClassObject;最后,IActivationFactory替换了IClassFactory。总的来说,步骤几乎是一样的。

以下图表总结了创建 WinRT 类型的过程:

创建 WinRT 对象

注意

在此序列中使用的注册表键与桌面应用程序创建 WinRT 对象相关。商店应用程序激活使用的键是不同的,但一般序列是相同的。

如果一切顺利,我们将得到一个指向 Calendar 实例的IInspectable接口指针。但我们对能够提供 Calendar 真正功能的更具体接口感兴趣。

事实证明,相关接口的定义在一个名为 Calendar 的命名空间的头文件中:

#include <windows.globalization.h>

所讨论的接口在ABI::Windows::Globalization命名空间中被命名为ICalendar。我们将添加一个using命名空间以便更容易访问:

using namespace ABI::Windows::Globalization;

应用程序二进制接口ABI)是我们将在后面的部分中讨论的根命名空间。

由于我们需要一个ICalendar,我们必须为其进行QueryInterface

ICalendar* pCalendar;
hr = pInst->QueryInterface(__uuidof(ICalendar), 
   (void**)&pCalendar);

pInst被假定为对象上的某个接口(如IInspectable)。如果确实支持该接口,我们将得到一个成功的HRESULTS_OK)和一个可以使用的接口指针。__uuidof操作符返回了接口的接口 IDIID);这是可能的,因为在声明的接口上附加了一个__declspec(uuid)属性。

现在,我们可以以任何我们认为合适的方式使用接口。以下是一些获取当前时间并将其显示到控制台的代码行:

pCalendar->SetToNow();
INT32 hour, minute, second;
pCalendar->get_Hour(&hour);
pCalendar->get_Minute(&minute);
pCalendar->get_Second(&second);

cout << "Time: " << setfill('0') << setw(2) << hour << ":" <<
   setw(2) << minute << ":" << setw(2) << second << endl;

此时,Calendar实例上的引用计数应该是2。为了正确清理,我们需要在任何获得的接口指针上调用IUnknown::Release(创建时引用计数为1QueryInterface后变为2);此外,由于我们创建了一个HSTRING,最好将其销毁;最后,我们将在当前线程上取消初始化 WinRT 以确保万无一失:

pCalendar->Release();
pInst->Release();
::WindowsDeleteString(hClassName);

完整的代码可以在WinRTAccess1项目中找到,这是本章可下载代码的一部分。

WinRT 元数据

前面的示例使用了<windows.globalization.h>头文件来发现ICalendar接口的声明,包括其 IID。然而,由于 COM/WinRT 应该提供语言/平台之间的互操作性,非 C++语言如何能够使用该头文件呢?

答案是其他语言无法使用该头文件;它是特定于 C/C++的。我们需要的是一种基于明确定义的结构的“通用头文件”,因此可以被任何平台使用。这就是元数据文件的作用。

元数据文件(扩展名为.winmd)的格式基于为.NET 创建的元数据格式。这只是方便,因为该格式是丰富的,提供了 WinRT 元数据所需的所有必要组成部分。

注意

在经典 COM 中,这些元数据存储在类型库中。类型库格式不如.NET 元数据格式丰富,因此不适用于 WinRT。

WinRT 元数据文件位于%System32%\WinMetadata文件夹中,并且它们根据命名空间方便地排列(实际上,这是一个要求)。这是我机器上的文件:

WinRT 元数据

要查看元数据文件,我们可以使用任何(相对较新的)能够显示.NET 元数据的工具,例如来自 Visual Studio 2012 工具的 IL Disassembler(ILDasm.exe)或 Reflector(www.reflector.net/)。在ILDasm.exe中打开Windows.Globalization.winmd显示如下:

WinRT 元数据

我们可以看到元数据文件中定义的所有类和接口。展开ICalendar接口节点显示其成员:

WinRT metadata

双击一个方法并不会显示它的实现,因为它并不是真正的.NET;那里没有代码,只有它的元数据格式。

Calendar类呢?展开它的节点会显示它实现了ICalendar。这使得任何使用元数据的人(包括工具)都可以自信地使用QueryInterface来查询这个接口并获得成功的结果:

WinRT metadata

这些元数据文件是构建 WinRT 组件的结果。这样,任何了解元数据格式的平台都可以消费该组件公开的类/接口。我们将在本章后面看到一个示例。

Windows Runtime Library

Calendar 的使用示例有效,但所需的代码相当冗长。Windows Runtime LibraryWRL)是一组帮助类和函数,使得在客户端和服务器端(组件的创建者)中更容易使用 WinRT 类型。WRL 使用标准 C++(没有非标准扩展),使得它与底层非常接近。让我们看看如何通过使用 WRL 来简化 Calendar 示例。

首先,我们需要包含 WRL 头文件;有一个主头文件和一个带有一些便利包装器的辅助文件:

#include <wrl.h>
#include <wrl/wrappers/corewrappers.h>

接下来,我们将添加一些using语句来缩短代码:

using namespace Windows::Foundation;
using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;

main()中,我们首先需要初始化 WinRT。一个简单的包装器在它的构造函数中调用RoInitialize,在它的析构函数中调用RoUninitialize

RoInitializeWrapper init(RO_INIT_MULTITHREADED);

创建和管理HSTRING,我们可以使用一个辅助类HString

HString hClassName;
hClassName.Set(RuntimeClass_Windows_Globalization_Calendar);

长标识符是在<windows.globalization.h>中定义的完整的 Calendar 类名,因此我们不必提供实际的字符串。HString有一个Get()成员函数,返回底层的HSTRING;它的析构函数销毁HSTRING

注意

前面的代码实际上可以通过使用引用现有字符串的HSTRING来简化(并加快速度),从而避免实际的字符串分配和复制。这是通过HString::MakeReference静态函数完成的,该函数在内部调用WindowsCreateStringReference。它有效地消除了销毁HSTRING的需要,因为一开始根本没有分配任何东西。这个字符串引用也被称为“快速传递”。

通过调用Windows::Foundation::ActivateInstance模板函数,可以简化创建Calendar实例,该函数在内部调用RoActivateInstance并查询所请求的接口:

ComPtr<ICalendar> spCalendar;
HRESULT hr = ActivateInstance(hClassName.Get(), &spCalendar);

ComPtr<T>是 WRL 用于 WinRT 接口的智能指针。它在析构函数中正确调用Release,并提供必要的操作符(如->),因此在访问底层接口指针时几乎是不可见的。代码的其余部分基本相同,尽管不需要清理,因为析构函数会做正确的事情:

spCalendar->SetToNow();
INT32 hour, minute, second;
spCalendar->get_Hour(&hour);
spCalendar->get_Minute(&minute);
spCalendar->get_Second(&second);

cout << "Time: " << setfill('0') << setw(2) << hour << ":" << 
   setw(2) << minute << ":" << setw(2) << second << endl;

WRL 还提供了帮助实现 WinRT 组件的类,包括实现样板代码的IInspectable、激活工厂等。我们通常会使用 C++/CX 来创建组件,但如果需要低级控制或者不希望使用语言扩展,可以使用 WRL。

注意

默认情况下,Visual Studio 2012 没有安装用于使用 WRL 创建 WinRT 组件的项目模板;但是,微软创建了这样一个模板,并且在调用工具 | 扩展和更新菜单项时可以在线搜索到。这为创建 WinRT DLL 组件提供了一个不错的起点。所涉及的步骤与使用 ATL 定义接口和成员的经典 COM 组件的步骤相似,在接口定义语言IDL)文件中实现所需的功能。

C++/CX

WRL 简化了使用和访问 WinRT 对象,但在创建和使用对象时,它仍然比普通 C++体验要复杂。调用new运算符比使用Windows::Foundation::ActivateInstance和使用ComPtr<T>智能指针要容易得多。

为此,微软创建了一组名为 C++/CX 的 C++语言扩展,帮助弥合差距,使得使用 WinRT 对象几乎与使用非 WinRT 对象一样简单。

以下部分讨论了一些常见的扩展。我们将在整本书中讨论更多的扩展。首先,我们将看看如何创建对象,然后我们将研究各种成员以及如何访问它们,最后,我们将考虑使用 C++/CX 创建新的 WinRT 类型的基础知识。

创建和管理对象

在 C++/CX 中,WinRT 对象是通过关键字ref new实例化的。这将创建一个引用计数对象(WinRT 对象),并使用^(帽子)符号返回对象的句柄。以下是创建Calendar对象的示例:

using namespace Windows::Globalization;
Calendar^ cal = ref new Calendar;

cal中返回的值是一个 WinRT 对象。可能令人困惑的一点是,我们得到的是一个Calendar对象而不是一个接口;但是 COM/WinRT 客户端只能使用接口;我们之前使用的ICalendar在哪里?

C++/CX 提供了一层便利,允许使用对象引用而不是接口引用。但是,接口ICalendar仍然存在,并且实际上被定义为Calendar类的默认接口(编译器知道这一点),但直接使用类似更自然。我们可以通过添加一个方法调用来验证这一点,并在添加特定转换为ICalendar后查看生成的代码并将其与原始调用进行比较:

Calendar^ cal = ref new Calendar;
cal->SetToNow();

ICalendar^ ical = cal;
ical->SetToNow();

以下是这些调用的生成代码:

  cal->SetToNow();
00A420D0  mov         eax,dword ptr [cal] 
00A420D3  push        eax 
00A420D4  call        Windows::Globalization::ICalendar::SetToNow (0A37266h) 
00A420D9  add         esp,4  

  ICalendar^ ical = cal;
00A420DC  mov         eax,dword ptr [cal]  
00A420DF  push        eax  
00A420E0  call        __abi_winrt_ptr_ctor (0A33094h)  
00A420E5  add         esp,4  
00A420E8  mov         dword ptr [ical],eax  
00A420EB  mov         byte ptr [ebp-4],0Ah  
  ical->SetToNow();
00A420EF  mov         eax,dword ptr [ical] 
00A420F2  push        eax 
00A420F3  call        Windows::Globalization::ICalendar::SetToNow (0A37266h) 

高亮部分相同,证明实际调用是通过接口进行的。

注意

熟悉 C++/CLI,即.NET 的 C++扩展的读者可能会认出“帽子”(^)和一些其他类似的关键字。这只是从 C++/CLI 借来的语法,但与.NET 无关。所有的 WinRT 都是纯本地代码,无论是使用 C++/CX 还是其他方式访问。

当帽子变量超出范围时,IUnknown::Release会按预期自动调用。还可以使用 WinRT 类型的堆栈语义,如下所示:

Calendar c1;
c1.SetToNow();

对象仍然以通常的方式动态分配。但是保证在变量超出范围时进行清理。这意味着它不能传递给其他方法。

访问成员

在获得对 WinRT 对象(或接口)的引用后,可以使用箭头(->)运算符访问成员,就像常规指针一样。但是,帽子不是正常意义上的指针;例如,永远不可能进行指针算术运算。应该将帽子变量视为对 WinRT 对象的不透明引用。

通过引用访问成员并不完全与通过直接(或类似 WRL 的)接口指针访问对象相同。主要区别在于错误处理。所有接口成员必须返回HRESULT;通过帽子引用调用会隐藏HRESULT,而是在失败的情况下抛出异常(派生自Platform::Exception)。这通常是我们想要的,这样我们可以使用标准语言设施try/catch来处理错误,而不必为每次调用检查HRESULT

另一个区别出现在方法有返回值的情况下。实际接口方法必须返回HRESULT,因此添加一个输出参数(必须是指针),在成功时将结果存储在其中。由于帽子引用隐藏了HRESULT,它们使返回类型成为方法调用的实际返回值,这非常方便。以下是使用ICalendar::Compare方法比较此日历的日期/时间与另一个日历的示例。使用 WRL 创建第二个日历并进行比较如下:

ComPtr<ICalendar> spCal2;
ActivateInstance(hClassName.Get(), &spCal2);
spCal2->SetToNow();
spCal2->AddMinutes(5);

int result;
hr = spCalendar->Compare(spCal2.Get(), &result);

通过将目标变量作为Compare调用的最后一个参数来获得结果。以下是等效的 C++/CX 版本:

auto cal2 = ref new Calendar;
cal2->SetToNow();
cal2->AddMinutes(5);
int result = cal->Compare(cal2);

HRESULT无处可寻,实际结果直接从方法调用中返回。如果发生错误,将抛出Platform::Exception`(或其派生类)。

注意

静态方法或属性呢?这些是存在的,并且可以通过熟悉的ClassName::MemberName语法进行访问。好奇的读者可能想知道这些是如何实现的,因为 COM 没有静态成员的概念,一切都必须通过接口指针访问,这意味着必须存在一个实例。所选的解决方案是在激活工厂(类工厂)上实现静态成员,因为它通常是单例,有效地给出了相同的净结果。

方法和属性

WinRT 正在努力实现对象导向,至少在成员方面是这样。方法是成员函数,在 C++中按预期调用。这在之前显示的ICalendar::SetToNow()ICalendar::AddMinutes()ICalendar::Compare()中是这样的。

WinRT 还定义了属性的概念,实际上它们是伪装成方法的方法。属性可以有 getter 和/或 setter。由于 C++没有属性的概念,这些属性被建模为以get_put_开头的方法,而 C++/CX 为了方便提供了类似字段的属性访问。

这是使用ICalendar上定义的Hour属性的示例。首先是 WRL 版本:

// read current hour
INT32 hour;
spCalendar->get_Hour(&hour);
// set a new hour
spCalendar->put_Hour(23);

接下来是 C++/CX 版本:

int hour = cal->Hour;  // get
cal->Hour = 23;    // set

属性的存在可以在元数据文件中看到,例如Windows.Globalization.winmd。查看Calendar类(或ICalendar接口),红色三角形表示属性。双击其中任何一个会显示以下内容:

方法和属性

使用 C++/CX 可以访问实际的方法或属性,而无需将失败的HRESULT映射到异常的抽象层,如果需要(这更快,等效于 WRL 生成的代码)。这是通过调用以__abi_为前缀的成员来实现的,如下面的代码片段所示:

cal->__abi_SetToNow();
int r;
cal->__abi_Compare(cal2, &r);
cal->__abi_set_Hour(22);

所有这些成员都返回HRESULT,因为这些都是通过接口指针的实际调用。有趣的是,属性的设置器必须以set_而不是put_为前缀。这种方案还提供了一种调用IInspectable方法的方式,例如GetIids,否则无法通过帽子引用进行访问。

注意

目前,这些调用没有Intellisense,因此编辑器中会显示红色波浪线。但是代码编译和运行都如预期那样。

委托

委托是 WinRT 中函数指针的等价物。委托是一种可以指向方法的字段。与函数指针相反,委托可以指向静态方法或实例方法,根据需要。委托具有内置的构造函数,接受方法或 lambda 函数。

注意

术语“委托”之所以被使用,是因为它与.NET 世界中的相同概念相似,在那里委托的作用与 WinRT 中的作用基本相同。

以下是使用IAsyncOperation<T>接口的示例,我们将在讨论异步操作时进行详细讨论。给定一个IAsyncOperation<T>T是操作预期的返回类型),其Completed属性的类型为AsyncOperationCompletedHandler<T>,这是一种委托类型。我们可以将Completed属性连接到当前实例的成员函数,如下所示:

IAsyncOperation<String^>^ operation = ...;
operation->Completed = ref new
   AsyncOperationCompletedHandler<String^>(this, &App::MyHandler);

其中App::MyHandler的原型如下:

void MyHandler(IAsyncOperation<String^>^ operation, 
   AsyncStatus status);

为什么是这个原型?这正是委托定义的东西:必须遵循的特定原型,否则编译器会抱怨。

作为命名方法的替代,我们可以将委托绑定到 lambda 函数,这在许多情况下更方便。以下是与之前代码等效的 lambda:

operation->Completed = ref new AsyncOperationCompletedHandler<String^>(
   [](IAsyncOperation<String^>^ operation, AsyncStatus status) {
    // do something...
  });

示例中未捕获任何变量。关键点在于 lambda 的参数与命名方法的情况完全相同。

委托到底是什么?它是一个像任何其他 WinRT 类一样的类,具有一个特殊的构造函数,允许绑定到一个方法(命名或 lambda),以及一个实际执行委托的Invoke方法。在 C++/CX 中,调用可以通过函数调用运算符()执行,就像任何函数一样。假设前面的声明,我们可以以以下方式之一调用Completed委托:

operation->Completed->Invoke(operation, AsyncStatus::Completed);
operation->Completed(operation, AsyncStatus::Completed);

这两行是等价的。

注意

从技术上讲,前面的代码在语法上是正确的,但我们永远不会自己调用异步操作完成。操作的所有者将进行调用(我们将在本章后面讨论异步操作)。

事件

委托通常不会声明为属性,如IAsyncOperation<T>::Completed属性。原因有两个:

  • 任何人都可以在该属性中放置nullptr(或其他委托),丢弃可能已设置的任何先前的委托实例

  • 任何人都可以调用委托,这很奇怪,因为只有声明类知道何时应该调用委托。

我们想要的是一种使用委托连接到感兴趣的方法的方式,但以一种安全的方式,不允许任意代码直接更改委托或调用它。

这就是事件的作用。事件看起来像委托,但实际上有两种方法,一种用于注册事件处理程序,一种用于撤销处理程序。在 C++/CX 中,+=-=运算符用于事件,因此客户端可以注册通知,但永远不能使用赋值运算符来使委托的值为空或替换委托的值,因为它没有以这种方式公开。

以下是一个使用Application::Suspending事件的示例,该事件指示感兴趣的方当应用程序即将被暂停时,可以保存状态的良好时机(我们将在第七章中讨论应用程序生命周期,应用程序、磁贴、任务和通知):

this->Suspending += ref new SuspendingEventHandler(
   this, &App::OnSuspending);

请注意,SuspendingEventHandler是委托类型,这意味着方法OnSuspending必须按照该委托定义的方式进行原型设计。

在幕后,事件只是一对适当实现的方法(Visual Studio 智能感知显示带有闪电图标的事件)。以下是通过元数据描述的Application::Suspending事件(还显示了其他事件),在ILDasm.exe中显示:

事件

倒置的绿色三角形表示事件成员本身,而add_Suspendingremove_Suspending是在使用+=-= C++/CX 运算符时调用的实际方法。

定义类型和成员

可以使用 WRL(通过在 IDL 文件中定义接口,实现所有样板代码,如IUnknownIInspectable实现,激活工厂,DLL 全局函数等)来定义 WinRT 类型。这提供了一种非常精细的方式来创建组件,并且在精神上类似于使用Active Template LibraryATL)编写 COM 组件的方式。

使用 C++/CX,编写可重用的 WinRT 组件比使用 WRL 要容易得多。在本节中,我们将构建一个简单的组件,并将其与 C++和 C#客户端一起使用(JavaScript 客户端同样有效,留给感兴趣的读者作为练习)。

WinRT 组件项目

Visual Studio 2012 包括一个项目模板,用于创建一个 WinRT 组件,然后可以被任何符合 WinRT 标准的平台(或另一个 WinRT 组件)使用。我们将创建一个名为CalculationsWindows Runtime Component类型的新项目:

WinRT 组件项目

向导添加了一个Class1类。我们可以删除它并添加一个新的 C++类,或者重命名文件和类名。我们将创建一个名为Calculator的 WinRT 类,在头文件中使用以下代码定义:

namespace Calculations {
  public ref class Calculator sealed {
  public:
     Calculator(void);

  };
}

WinRT 类必须由ref class关键字定义在命名空间内。它还必须声明为public,以便在组件 DLL 外部可用。该类还必须标记为sealed,表示它不能被继承;或者,它可以继承自非密封类,这些类目前是 WinRT 库中位于Windows::UI::Xaml命名空间中的类。WinRT 继承的详细讨论超出了本节的范围。

现在是时候给这个类一些有用的内容了。

添加属性和方法

Calculator类的想法是成为一个累积计算器。它应该保存当前结果(默认为零),并在执行新的数学运算时修改结果。随时可以获取其当前结果。

方法被添加为常规成员函数,包括构造函数。让我们在类的public部分添加一个构造函数和一些操作:

// ctor
Calculator(double initial);
Calculator();

// operations
void Add(double value);
void Subtract(double value);
void Multiply(double value);
void Divide(double value);

void Reset(double value);
void Reset();

我们需要一个只读属性来传达当前结果。下面是如何定义它的方法:

property double Result {
  double get();
}

property关键字是 C++/CX 的扩展,定义了一个属性,后面跟着它的类型和名称。在大括号内,可以声明get()set()方法(set必须接受正确类型的值)。缺少的set()方法表示这是一个只读属性——将创建一个get_Result方法,但不会创建put_Result方法。

注意

可以通过在属性名称后面加上分号(完全没有大括号)来添加一个简单的由私有字段支持的读/写属性。

接下来,我们添加需要维护正确状态的任何private成员;在这种简单情况下,只是当前结果:

private:
  double _result;

在 CPP 文件中,我们需要实现所有这些成员,以摆脱未解析的外部链接器错误:

#include "Calculator.h"

using namespace Calculations;

Calculator::Calculator(double initial) : _result(initial) {
}

Calculator::Calculator() : _result(0) {
}

void Calculator::Add(double value) {
  _result += value;
}

void Calculator::Subtract(double value) {
  _result -= value;
}

void Calculator::Multiply(double value) {
  _result *= value;
}

void Calculator::Divide(double value) {
  _result /= value;
}

void Calculator::Reset() {
  _result = 0.0;
}

void Calculator::Reset(double value) {
  _result = value;
}

double Calculator::Result::get() {
  return _result;
}

在那段代码中没有什么特别的,除了用于实现Result属性的语法。

由于这是一个 WinRT 组件,元数据(.winmd)文件将作为构建过程的一部分创建;这是将用于消耗组件的文件。使用ILDasm.exe打开它会显示刚刚编写的代码的结果:

添加属性和方法

这里有一些有趣的地方。由于我们编写了一个 WinRT 类,它必须实现一个接口,因为 WinRT/COM 客户端只能使用接口。在Calendar的情况下,接口被命名为ICalendar(这是它的默认接口),但在这里我们没有指定任何这样的接口。编译器自动创建了这样一个接口,它的名称是__ICalculatorPublicNonVirtuals。这是实际定义所有方法和属性的接口。奇怪的名称暗示这些方法通常只能从对Calculator对象的引用调用;无论如何,接口名称都不重要。

注意

显然,Calendar类不是用 C++/CX 创建的,因为它的默认接口名为ICalendar。事实上,它是用 WRL 创建的,WRL 允许完全控制组件作者的每个方面,包括接口名称;WRL 用于构建所有 Microsoft 提供的 WinRT 类型。

另一个有趣的地方涉及重载的构造函数。由于提供了非默认构造函数,因此默认的创建接口IActivationFactory是不够的,因此编译器创建了第二个接口ICalculatorFactory,其中包含一个接受双精度值的CreateInstance方法。这是使 C++/CX 易于使用的另一个特性——因为负担在编译器上。

添加一个事件

为了使其更有趣,让我们添加一个事件,以防尝试除以零。首先,我们需要声明一个适用于事件的委托,或者使用 WinRT 中已定义的委托之一。

为了演示目的,我们将定义一个自己的委托,以展示如何在 C++/CX 中完成。我们在Calculations命名空间声明内的Calculator定义上方添加以下声明:

ref class Calculator;

public delegate void DivideByZeroHandler(Calculator^ sender);

前向声明是必要的,因为编译器尚未遇到Calculator类。

委托表示它可以绑定到接受Calculator实例的任何方法。我们应该如何处理这个委托声明呢?我们将在类的public部分添加一个客户端可以注册的事件。

event DivideByZeroHandler^ DivideByZero;

这以最简单的方式声明了事件——编译器适当地实现了add_DivideByZeroremove_DivideByZero方法。

现在,我们需要更新Divide方法的实现,以便在传入值为零的情况下触发事件:

void Calculator::Divide(double value) {
  if(value == 0.0)
    DivideByZero(this);
  else
    _result /= value;
}

调用事件会调用所有注册的观察者(客户端)来处理此事件,并将自身作为参数传递(这可能对客户端有用,也可能没有)。

使用 WinRT 组件

现在是时候使用我们刚刚创建的Calculator类了。我们将构建两个客户端,一个是 C++客户端,一个是 C#客户端,以展示它们之间的区别。

构建一个 C++客户端

我们将在同一个解决方案中创建一个空白的 C++商店应用项目,并在 XAML 中构建一个简单的用户界面来测试计算器的功能。对于这次讨论来说,用户界面的细节并不重要;完整的代码可以在本章的可下载代码中的CalcClient1项目中找到。UI 看起来像这样:

构建一个 C++客户端

为了获取我们的Calculator的定义,我们需要添加对元数据文件的引用。通过右键单击项目节点并选择References…来实现。在显示的对话框中,我们选择Calculations项目:

构建一个 C++客户端

现在定义都已经可用,我们可以使用它们了。在MainPage.xaml.h中,我们添加了对Calculator对象的引用,以便它在页面的生命周期内存在:

private:
  Calculations::Calculator^ _calculator;

MainPage构造函数中,我们需要实际创建实例,并可选择连接到DivideByZero事件(我们这样做):

_calculator = ref new Calculator;
_calculator->DivideByZero += ref new DivideByZeroHandler(this {
  _error->Text = "Cannot divide by zero";
});

_error是 UI 中显示最后一个错误(如果有的话)的TextBlock元素。还添加了一个using namespace来引用Calculations,以便前面的代码可以编译。

当单击Calculate按钮时,我们需要根据列表框中当前选择的索引执行实际操作:

_error->Text = "";
wstringstream ss(_value->Text->Data());
double value;
ss >> value;
switch(_operationList->SelectedIndex) {
case 0:
	_calculator->Add(value); break;
case 1:
	_calculator->Subtract(value); break;
case 2:
	_calculator->Multiply(value); break;
case 3:
	_calculator->Divide(value); break;
}
// update result
_result->Text = _calculator->Result.ToString();

为了使这段代码编译通过,需要添加一个using namespace语句来引用std,并添加一个#include来引用<sstream>

就是这样。我们已经使用了一个 WinRT 组件。从技术上讲,没有简单的方法可以知道它是用什么语言编写的。唯一重要的是它是一个 WinRT 组件。

构建一个 C#客户端

让我们看看这如何与另一个客户端一起工作——使用 C#编写的商店应用。首先,我们将创建一个空白的 C#商店应用(名为CalcClient2),并将 XAML 原样复制到 C#项目中,从 C++客户端项目中。

接下来,我们需要添加对winmd文件的引用。右键单击项目节点,选择Add Reference…,或右键单击References节点,选择Add Reference…。类似的对话框会出现,允许选择Calculations项目(或者如果它是不同的解决方案,则浏览文件系统中的文件)。

使用Calculator所需的实际代码与 C++情况类似,使用了 C#(和.NET)的语法和语义。在MainPage.xaml.cs中,我们创建了一个Calculator对象,并注册了DivideByZero事件(使用 C# lambda 表达式):

Calculator _calculator;

public MainPage() {
  this.InitializeComponent();
  _calculator = new Calculator();
  _calculator.DivideByZero += calc => {
    _error.Text = "Cannot divide by zero";
  };
}

注意

在 C#中,可以编写 lambda 表达式而不指定确切的类型(如前面的代码片段所示);编译器会自行推断类型(因为委托类型是已知的)。也可以明确写出类型,如_calculator.DivideByZero += (Calculator calc) => { … };是可能的(也是合法的)。

文件顶部添加了一个using Calculations语句。按钮的点击事件处理程序非常容易理解:

_error.Text = String.Empty;
double value = double.Parse(_value.Text);
switch (_operationList.SelectedIndex) {
  case 0:
    _calculator.Add(value); break;
  case 1:
    _calculator.Subtract(value); break;
  case 2:
    _calculator.Multiply(value); break;
  case 3:
    _calculator.Divide(value); break;
}
// update result
_result.Text = _calculator.Result.ToString();

注意 C#代码访问计算器的方式与 C++版本非常相似。

应用程序二进制接口

在前一节中创建的Calculator WinRT 类留下了一些问题。假设以下方法被添加到类的公共部分:

std::wstring GetResultAsString();

编译器将拒绝编译此方法。原因与使用std::wstring有关。这是一种 C++类型——它如何映射到 C#或 JavaScript?它不能。公共成员必须仅使用 WinRT 类型。内部 C++实现和面向公众的类型之间存在边界。定义相关方法的正确方式是这样的:

Platform::String^ GetResultAsString();

Platform::String是 C++/CX 对HSTRING WinRT 的包装器,它在 C#中被映射为System.String,在 JavaScript 中被映射为string

WinRT 类中的私有成员可以是任何东西,往往是本机 C++类型(如wstringvector<>,以及可能从旧代码迁移过来的其他任何东西)。

简单类型,如intdouble在 C++和 WinRT 之间自动映射。应用程序二进制接口ABI)是 WinRT 类型(可在组件外部使用)和特定于语言/技术的本机类型之间的边界(不仅适用于 C++,也适用于 C#)。

异步操作

Windows 8 商店应用承诺“快速流畅”。这个表达有几个意思,其中一些与用户体验和用户界面设计有关(这里不涉及),一些与应用程序响应性有关。

自从 Windows 操作系统的第一个版本以来,用户界面由应用程序中的单个线程处理。从技术上讲,一个线程可以创建任意数量的窗口,并且该线程成为这些窗口的所有者,并且是唯一可以处理针对这些窗口的消息的线程(通过消息队列)。如果该线程变得非常忙碌,并且不能及时处理消息,UI 将变得不够响应;在极端情况下,如果线程由于某种原因被卡住了几秒钟或更长时间,UI 将变得完全无响应。这种情况非常熟悉且极不理想。以下图表说明了 UI 处理中涉及的实体:

异步操作

响应性的关键是尽快释放 UI 线程,并且永远不要阻塞它超过几毫秒。在桌面应用程序的世界中,开发人员可以随意调用一些长时间运行的操作(或一些长时间的 I/O 操作),从而阻止线程返回到消息处理活动,冻结用户界面。

在 WinRT 中,微软已经做出了一个有意识的决定,即如果一个操作可能需要超过 50 毫秒的时间,那么它应该是异步的而不是同步的。最终结果是许多方法都是异步执行的,这可能会使代码变得更加复杂。异步意味着操作开始,但调用几乎立即返回。当操作完成时,会调用某些回调,以便应用程序可以采取进一步的步骤。在此期间,UI 线程没有做任何特殊的事情,因此可以像往常一样处理消息,保持 UI 的响应性。同步和异步调用之间的区别可以用以下图表说明:

异步操作

异步操作,虽然可取,但从定义上来说更加复杂。代码不再是顺序的。WinRT 定义了一些表示正在进行的操作的接口。这些接口是从各种异步方法返回的,这些方法启动一个操作,并允许客户端注册操作完成的时间。

让我们看一个异步操作的例子以及我们如何处理它们。我们将创建一个简单的图像查看器应用程序,允许用户浏览图像并显示它(完整的源代码在本章的下载中提供的SimpleImageView项目中)。用户界面目前并不重要,由一个按钮组成,该按钮启动用户的选择过程,以及一个Image元素,可以显示图像。当点击按钮时,我们希望为用户提供一种选择图像文件的方法,然后将文件转换为Image元素可以显示的内容。

用于选择文件的 WinRT 类是Windows::Storage::Pickers::FileOpenPicker。我们将创建一个实例并设置一些属性:

auto picker = ref new FileOpenPicker;
picker->FileTypeFilter->Append(".jpg");
picker->FileTypeFilter->Append(".png");
picker->ViewMode = PickerViewMode::Thumbnail;

注意

熟悉桌面应用程序世界的读者可能会想知道通用的打开文件对话框在哪里,该对话框可以通过 Win32 API 或其他包装器使用。由于几个原因,该对话框不能在商店应用程序中使用。首先是美学原因;与 Windows 8 商店应用程序试图传达的现代 UI 相比,该对话框很丑陋。其次,该对话框有标题栏和其他类似的界面,因此不适合新世界。最重要的是,FileOpenPicker不仅仅是从文件系统中选择文件。它实际上是使用文件打开选择器合同,由相机(如果连接了相机)实现(例如),因此我们实际上可以拍照然后选择它;对于其他来源,如 SkyDrive、Facebook 等也是如此。通用的打开文件对话框没有这样的功能。

现在,是时候显示选择器并允许用户选择一些东西了。查看FileOpenPickerAPI,我们找到了PickSingleFileAsync方法。Async后缀是 WinRT API 中用于指示启动异步操作的方法的约定。选择文件的结果应该是Windows::Storage::StorageFile的一个实例,但实际上它返回的是IAsyncOperation<StorageFile^>,这是表示长时间运行操作的对象。

处理这个问题的一种方法是将Completed属性(一个委托)设置为一个处理程序方法,当操作完成时将调用该方法(这可以是一个 lambda 函数)。当调用该函数时,我们可以调用IAsyncOperation<T>::GetResults()来获取实际的StorageFile对象:

auto fileOperation = picker->PickSingleFileAsync();
fileOperation->Completed = ref new 
   AsyncOperationCompletedHandler<StorageFile^>(
this {
  auto file = op->GetResults();
  });

很遗憾,这还没结束。一旦文件可用,我们需要打开它,将其数据转换为 WinRT 流接口,然后将其提供给一个BitmapImage对象,该对象可以呈现为Image元素。

原来打开StorageFile也是一个异步操作(记住,该文件可以来自任何地方,比如 SkyDrive 或网络共享)。在获得文件之后,我们重复相同的顺序:

using namespace Windows::UI::Core;
auto openOperation = file->OpenReadAsync();
openOperation->Completed = ref new AsyncOperationCompletedHandler<IRandomAccessStreamWithContentType^>(
  this {
    auto bmp = ref new BitmapImage;
    bmp->SetSource(op->GetResults());
    _image->Source = bmp;
});

_image是应该使用其Source属性显示结果图像的Image元素。

这几乎可以工作。"几乎"部分有点微妙。前面的 lambda 是由不同的线程调用的,而不是启动调用的线程。UI 线程启动了它,但它在后台线程上返回。从后台线程访问 UI 元素(如Image元素)会导致抛出异常。我们该如何解决这个问题呢?

我们可以使用与 UI 线程绑定的Dispatcher对象,并要求它在 UI 线程上执行一些代码(通常指定为 lambda):

Dispatcher->RunAsync(CoreDispatcherPriority::Normal, 
   ref new DispatchedHandler([op, this]() {
    auto bmp = ref new BitmapImage;
    bmp->SetSource(op->GetResults());
    _image->Source = bmp;
  }));

Dispatcherthis(或任何 UI 元素)的属性,它在可能时将操作发布到 UI 线程上执行(通常几乎立即,假设 UI 线程没有被阻塞,我们非常努力地避免这种情况)。

整个序列并不容易,并且将Dispatcher添加到混合物中会进一步复杂化事情。幸运的是,有一种更简单的方法来处理异步操作——使用task<T>类。

使用任务进行异步操作

task<T>类位于 concurrency 命名空间中,并需要#include<ppltasks.h>。这个类是 C++11 中的新类,通常与并行编程有关,但在这里它为调用异步操作提供了特殊的目的。

task<T>类表示其结果为T类型的操作。它处理Completed属性注册的繁琐细节,调用GetResults,并自动使用Dispatcher来保持线程关联,以防操作是从 UI 线程(技术上是从单线程公寓)调用的。并且所有这些都可以很好地组合,以便我们需要按顺序调用几个异步操作(对于手头的情况是真实的)。以下是完整的代码:

auto fileTask = create_task(picker->PickSingleFileAsync());
fileTask.then([](StorageFile^ file) {
  return create_task(file->OpenReadAsync());
}).then(this {
  auto bmp = ref new BitmapImage;
  bmp->SetSource(stm);
  _image->Source = bmp;
});

create_task<T>函数是一个方便的函数,它使用正确的T创建一个task<T>create_task<T>允许使用auto关键字。一个等效的替代方法是:

task<StorageFile^> fileTask(picker->PickSingleFileAsync());

然后实例方法期望一个函数(有时称为 continuation,通常是 lambda),该函数应在异步操作完成时执行。它提供结果,无需调用IAsyncOperation<T>::GetResults()

注意组合。在StorageFile可用之后,另一个任务被创建并从 lambda 返回。这启动了另一个异步操作,将由下一个then调用解决。

最后,continuations 在与操作发起者相同的线程上运行,如果该发起者在 STA(这是 UI 线程的情况)中运行。

注意

该公寓意识仅适用于返回IAsyncAction<T>IAsyncOperation<T>(及其派生类)的操作。

取消异步操作

按定义,异步操作可能会长时间运行,因此最好提供取消操作的能力(如果可能)。IAsync*接口族有一个Cancel方法,我们可以调用它(例如,从某个Cancel按钮的单击事件处理程序),但很难将IAsync*对象暴露给外部代码。

幸运的是,task<>类提供了一个优雅的解决方案。任务构造函数(或create_task辅助函数)的第二个参数是一个cancellation_token对象。这个令牌是从cancellation_token_source对象使用其get_token()实例方法获得的。cancellation_token_source表示一个可取消的操作。外部调用者可以使用它的cancel()方法来“信号”所有由cancellation_token_source分发的cancellation_token对象(通常只有一个),从而导致任务调用IAsync*::Cancel方法。以下图表说明了这个过程:

取消异步操作

最终的结果是,如果操作被取消,将抛出一个task_canceled异常。它会(如果未处理)在then链中传播,以便可以方便地在最后一个then上捕获它——实际上,最好添加一个最后的then,只处理取消(和错误):

then([](task<void> t) {
  try {
    t.get();
  }
  catch(task_canceled) {
    // task cancelled
  }
  catch(Exception^ ex) {
    // some error occurred
  }
});

task<>::get()方法会抛出异常。请注意,task_canceled不是从Platform::Exception派生的,因此需要一个单独的catch子句来捕获它。

注意

有些操作只是返回一个nullptr对象来表示取消。这是FileOpenPicker示例的情况。如果返回的StorageFile对象是nullptr,这意味着用户在选择文件时选择了Cancel按钮。

错误处理

在异步操作中,可能会抛出异常。处理这些异常的一种方法是在适当的延续中添加try/catch块。一个更方便的方法是在最后的then延续中处理所有错误,就像取消一样。

使用现有库

WinRT 是一个新的库,我们希望在这个新的商店应用模型中使用它来访问 Windows 8 的功能。那么现有的 C++库,比如标准模板库(STL),活动模板库(ATL),Microsoft 基础类(MFC),或者其他一些自定义库呢?原始的 Win32 API 呢?在接下来的章节中,我们将讨论常见的 Microsoft 库及它们在 Windows 8 商店应用中的使用。

STL

STL 是标准 C++库的一部分(有时被认为是它的同义词),并且在 Windows 8 商店应用中得到完全支持。事实上,一些 WinRT 类型包装器了解 STL,使其更容易进行互操作。

MFC

MFC 库是在 20 多年前创建的,用于在 Windows API(在创建时是 Win16)上提供一个 C++层,主要是为了更容易地创建用户界面。

Windows 8 商店应用提供了自己的用户界面,与 MFC 包装的 Windows User32.dll API 相去甚远,使得 MFC 在新世界中已经过时且无法使用。现有代码必须迁移到使用 XAML、用户控件、控件模板或适用于特定应用程序的其他内容。

ATL

ATL 是为了帮助构建 COM 服务器和客户端而创建的,简化了实现常见功能(如IUnknown,类工厂,组件注册等)的负担。它在理论上可以在 Windows 商店应用中使用,但实际上没有什么意义。这个层次的任何东西都已经被本章前面讨论过的 WRL 所覆盖。

Win32 API

Win32 API(或 Windows API)是一个庞大的主要是 C 风格函数和一些 COM 组件的集合,它一直是用户模式下 Windows 操作系统的低级 API。

现在每个文档化的函数都包括一个“应用于”条款,说明该 API 是否可在桌面应用、商店应用或两者中使用。为什么一些函数在 Windows 商店应用中不可用?有几个原因:

  • 一些函数与不适合 Windows 商店的用户界面相关。例如,MessageBoxCreateWindowEx

  • 一些函数在 WinRT API 中有等价物(通常更好)。例如CreateFile(虽然有一个新的CreateFile2 API 也适用于商店应用),CreateThreadQueueUserWorkItem

  • 一些函数以其他方式不适用,比如违反安全约束。例如CreateProcessEnumWindows

使用被禁止的 API 会导致编译失败;这是因为 Windows API 头文件已经根据两个常量WINAPI_PARTITION_APP(用于商店应用)和WINAPI_PARTITION_DESKTOP(用于桌面应用)进行了条件编译的更改。

理论上,可以重新定义一个被禁止的函数并调用它。以下是一个对MessageBox函数有效的示例:

extern "C" BOOL WINAPI MessageBoxW(HWND hParent, LPCTSTR msg,
   LPCTSTR title, DWORD flags);

#pragma comment(lib, "user32.lib")

在这种情况下,需要链接到适当的库,因为默认情况下没有链接到user32.dll

尽管这样可以工作,如果调用这个函数,消息框会出现,但不要这样做。原因很简单:Windows 8 商店认证流程将拒绝使用任何使用被禁止 API 的应用程序。

注意

有关允许的 Windows API 函数的更多信息可以在msdn.microsoft.com/en-us/library/windows/apps/br205757找到。

CRT

C Runtime (CRT)库包含大量函数,最初是作为 C 语言的支持库创建的。其中许多函数在商店应用中不可用;通常有 Win32 或 WinRT 的等效函数。有关不受支持函数的全面列表,请参阅msdn.microsoft.com/EN-US/library/jj606124.aspx

DirectX

DirectX 是一组基于低级 COM 的 API,最初是在 20 多年前创建的,用于访问 PC 的多媒体功能(图形、音频、输入等),同时利用硬件能力(如图形卡)。DirectX 多年来主要在游戏行业中使用。

Windows 8 预装了 DirectX 11.1,为创建高性能游戏和应用程序提供了基础。它完全支持商店应用,甚至可以与基于 XAML 的 UI 共存。

C++ AMP

C++ 加速大规模并行性 (AMP)是一个相对较新的库,其目标是使用主流编程语言(C++)在 CPU 和非 CPU 设备上执行代码。目前,唯一支持的其他设备是图形处理单元 (GPU)。

现代 GPU 具有很强的并行性,但最初它们有自己的编程语言,用于编写可能与图形本身无关的任意算法。C++ AMP 是一种尝试使用 C++来运行 GPU(以及将来的其他设备)的方法。

C++ AMP 完全支持 Windows 8 商店应用(需要 DirectX 11 兼容的显卡)。

Windows Runtime 类库

WinRT 提供了一个全面的类库,按照层次命名空间排列;从字符串和集合,到控件,到设备,到网络,到图形;API 涵盖了很多领域。进入 Windows 商店应用的旅程之一就是学习各种受支持的 API 和功能。这种知识会随着时间的推移而不断发展。在本书的过程中,我们将讨论相当多的 WinRT API,但肯定不是全部。

在接下来的章节中,我们将讨论一些在商店应用中经常使用的核心类型,以及它们如何(如果有的话)特定地映射到 C++。

字符串

WinRT 定义了自己的字符串类型HSTRING。我们已经多次遇到它。由于HSTRING只是不可变字符串的不透明句柄,Windows 提供了一些用于管理HSTRING的函数,如WindowsCreateStringWindowsConcatStringWindowsSubStringWIndowsGetStringLenWindowsReplaceString等。使用这些 API 并不困难,但非常繁琐。

幸运的是,一个HSTRING被一个引用计数类Platform::String包装,它在幕后提供了对适当 API 的必要调用。它可以根据原始 Unicode 字符指针(wchar_t*)构造,并且具有返回原始指针的Data()方法。这意味着在Platform::Stringstd::wstring之间进行互操作相当容易。以下是使用字符串的一些示例:

auto s1 = ref new String(L"Hello");
std::wstring s2(L"Second");
auto s3 = ref new String(s2.c_str());
int compare = wcscmp(L"xyz", s1->Data());
for(auto i = begin(s3); i != end(s3); ++i)
  DoSomethingWithChar(*i);

注意使用Platform::beginPlatform::end实现的迭代器行为。一般指导原则是,在编写组件时,最好使用std::wstring进行所有字符串操作,因为wstring具有丰富的函数集。只在 ABI 边界处使用Platform::StringPlatform::String内置的功能非常有限。

集合

标准 C++定义了几种容器类型,如std::vector<T>std::list<T>std::map<K, V>等。然而,这些类型不能跨 ABI 边界——它们是特定于 C++的。

WinRT 定义了自己的集合接口,必须跨 ABI 边界使用。以下是带有这些接口的类图:

集合

IIterable<T>只有一个方法:First,它返回一个IIterator<T>,这是 WinRT 迭代器接口。它定义了MoveNextGetMany方法以及两个属性:Current返回迭代器指向的当前对象,HasCurrent指示是否还有更多的项目可以迭代。

IVector<T>表示可以通过索引访问的项目序列。这是 ABI 中常用的类型。C++支持库为IVector<T>提供了一个名为Platform::Collections::Vector<T>的标准实现。这可以作为 WinRT 类中的基础私有类型,因为在需要时它可以转换为IVector<T>。但是,请注意,对于重型操作,STL std::vector<T>更有效率。如果在某个时候需要Vector<T>,它有许多构造函数,其中一些接受std::vector<T>

IVectorView<T>表示对向量的只读视图。可以通过调用GetView方法从IVector<T>中获取。VectorView<T>是一个 C++私有实现,如果需要,可以用于IVector<T>的自定义实现。

IObservableVector<T>继承自IVector<T>并添加了一个事件VectorChanged。这对于希望在IObservableVector<T>中添加、移除或替换项目时收到通知的客户端可能很有用。

IMap*系列接口管理键/值对,并且可以在 ABI 边界上进行传输。Platform::Collections::Map<K,V>提供了一个可转换为此接口的实现,作为一个平衡的二叉树,类似于std::map<K,V>(包括通过第三个模板参数改变排序算法的能力)。IMapView<K,V>IMap<K,V>的只读视图。

注意

ABI 中最有用的集合类型是IVector<T>。如果可以接受Vector<T>作为基础实现,请这样做。否则,保持std::vector<T>,并且只在跨越 ABI 边界时转换为IVector<T>

异常

COM/WinRT 不使用异常。原因可能很明显,异常是特定于语言或平台的。它们不能成为各种平台遵循的二进制标准的一部分。相反,COM 使用HRESULT,它们只是 32 位数字,用于指示方法调用的成功或失败。

然而,C++(以及大多数其他现代语言,如 C#)支持异常的概念。通过捕获异常来处理错误比在每次调用后检查HRESULT要容易得多,并且更易于维护(C 风格的编程)。这就是为什么通过 C++/CX 引用计数对象(帽子)进行的调用将失败的HRESULT转换为异常对象,该对象派生自Platform::Exception,可以以通常的方式捕获。

这也适用于另一种情况;在 C++/CX 中实现组件时,代码可能会抛出一个派生自Platform::Exception的异常;这种异常不能跨越 ABI;相反,它被转换为等效的HRESULT,这是可以跨越 ABI 的东西。另一方面,它可能会再次转换为异常对象,例如 C++异常或.NET 异常,以供客户端平台使用。

Platform::Exception派生的异常类型列表是预定义的,不能扩展,因为每种类型直接映射到一个HRESULT。这意味着不可能添加新的异常类型,因为 C++/CX 无法知道将异常转换为哪个HRESULT,当跨越 ABI 时。对于自定义异常,可以使用带有自定义HRESULTPlatform::COMException。异常类型及其HRESULT等效的完整表格如下所示:

异常

表中大多数异常类型都是不言自明的。我们将在本书的后面讨论其中一些异常。

注意

抛出一个不继承自Platform::Exception的对象将被转换为E_FAIL HRESULT

所有异常类型都有一个HResult属性,其中包含基础的HRESULT值,还有一个Message属性,这是异常的文本描述(由 WinRT 提供,无法更改)。

总结

本章从一些可能对 WinRT 开发有用的新 C++11 特性开始。我们讨论了 COM,它的概念和思想,以及它们如何转化为 WinRT。WRL 提供了访问 WinRT 对象的帮助程序,而无需语言扩展。C++/CX 提供了语言扩展,使得与 WinRT 的工作和编写 WinRT 组件变得更加容易。

WinRT 有一些模式和习惯用法,我们需要学习和适应,比如处理异步操作、字符串、集合和错误处理的方法。

本章的覆盖范围并不全面,但应该足够让我们有能力和理解开始编写真实的应用程序。我们将在本书的后面部分看一些其他 C++/CX 的能力和其他与 WinRT 相关的特性。

在下一章中,我们将深入构建应用程序,从 XAML 开始,以及在 WinRT 中通常构建用户界面的方式。

第三章:使用 XAML 构建 UI

用户界面和用户体验在 Windows 8 Store 应用程序中扮演着重要的角色。为 Store 应用程序创建了一种新的设计,现在称为现代设计风格(以前称为 Metro),其中关键词包括“快速流畅”、“内容优先”和“触摸为中心”。应用程序 UI 占据整个屏幕(除了在快照视图中),这使得 UI 变得更加重要。在本章(和下一章)中,我们将讨论为 Store 应用程序构建 UI 的方式,更多地是在技术层面上而不是在实际设计上。微软在线提供了大量设计 UI 的资源。

XAML

C++ Store 应用程序通常使用可扩展应用程序标记语言XAML)作为创建用户界面的主要语言。当首次提到 XAML 时,首先想到的问题是为什么?C++或任何其他现有的编程语言有什么问题吗?

XAML 是一种基于 XML 的语言,描述了“什么”,而不是“如何”;它是声明性的和中立的。从技术上讲,完整的应用程序可以在没有任何 XAML 的情况下编写;没有 XAML 可以做的事情是 C++做不到的。以下是 XAML 有意义的一些原因(或者至少可能有意义的一点):

  • 与 XAML 相比,C++非常冗长。XAML 通常比等效的 C++代码更短。

  • 由于 XAML 是中立的,面向设计的工具可以读取和操作它。微软专门提供了 Expression Blend 工具用于此目的。

  • XAML 的声明性使得构建用户界面更容易(大多数情况下,用户习惯后),因为这些界面具有类似 XML 的树状结构。

XAML 本身与用户界面本身无关。XAML 是一种创建对象(通常是对象树)并设置其属性的方法。这适用于任何“XAML 友好”的类型,这意味着它应该具有以下特点:

  • 默认的公共构造函数

  • 可设置的公共属性

第二点不是严格要求,但是没有属性,对象就相当无聊。

注意

XAML 最初是为Windows Presentation FoundationWPF)创建的,这是.NET 中的主要丰富客户端技术。现在它被其他技术所利用,主要是在.NET 空间中,比如 Silverlight 和Windows Workflow FoundationWF)。

WinRT 中当前实现的 XAML 级别大致相当于 Silverlight 3 XAML。特别是,它不像 WPF 的 XAML 那样强大。

XAML 基础知识

XAML 有一些规则。一旦我们理解了这些规则,就可以阅读和编写任何 XAML。最基本的 XAML 规则如下:

  • XML 元素意味着对象创建

  • XML 属性意味着设置属性(或事件处理程序)

有了这两条规则,下面的标记意味着创建一个Button对象,并将其Content属性设置为字符串Click me

<Button Content="Click me!" />

等效的 C++代码如下:

auto b = ref new Button;
b->Content = "Click me";

创建新的空白应用程序项目时,会创建一个MainPage.xaml文件以及头文件和实现文件。以下是该 XAML 文件的外观:

<Page
  x:Class="BasicXaml.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/
  2006/xaml/presentation"

  xmlns:mc="http://schemas.openxmlformats.org/
  markup-compatibility/2006"
  mc:Ignorable="d">

  <Grid Background="{StaticResource  
    ApplicationPageBackgroundThemeBrush}">
  </Grid>
</Page>

详细了解这些行是值得的。在这个例子中,项目名称是BasicXaml。根元素是Page,并设置了一个x:Class属性,指示从Page继承的类,这里命名为BasicXaml::MainPage。请注意,类名是包括命名空间的完整名称,其中分隔符必须是句点(而不是 C++的作用域解析运算符::)。x:Class只能放在根元素上。

跟在根元素后面的是一堆 XML 命名空间声明。这些为页面整个 XAML 中使用的元素提供了上下文。默认的 XML 命名空间(没有名称)告诉 XAML 解析器,诸如PageButtonGrid这样的类型可以直接写成它们自己,不需要任何特殊前缀。这是最常见的情况,因为页面中的大部分 XAML 都是用户界面元素。

下一个 XML 命名空间前缀是x,它指向 XAML 解析器的特殊指令。我们刚刚看到x:Class的作用。我们将在本章的后面遇到其他类似的属性。

接下来是一个名为local的前缀,它指向在BasicXaml命名空间中声明的类型。这允许在 XAML 中创建我们自己的对象;这些类型的前缀必须是local,以便 XAML 解析器知道在哪里查找这样的类型(当然,我们可以将其更改为任何我们喜欢的东西)。例如,假设我们创建了一个名为MyControl的用户控件派生类型。要在 XAML 中创建一个MyControl实例,我们可以使用以下标记:

<local:MyControl />

d前缀用于与设计相关的属性,主要与 Expression Blend 一起使用。mc:ignorable属性说明d前缀应该被 XAML 解析器忽略(因为它与 Blend 与 XAML 的工作方式有关)。

Grid元素托管在Page内,"托管"将在下文中变得清晰。其Background属性设置为{StaticResource ApplicationPageBackgroundThemeBrush}。这是一个标记扩展,在本章的后面部分讨论。

注意

XAML 无法直接调用方法;它只能设置属性。这是可以理解的,因为 XAML 需要保持声明性的特性;它并不是作为 C++或任何其他编程语言的替代品。

类型转换器

XML 处理字符串。然而,很明显许多属性不是字符串。许多属性仍然可以指定为字符串,并且由于 XAML 解析器使用的类型转换器,仍然可以正确工作。以下是Rectangle元素的一个例子:

<Rectangle Fill="Red" />

可以推测,Fill属性不是字符串类型。实际上,它是一个Brush。这里的Red实际上意味着ref new SolidColorBrush(Colors::Red)。XAML 解析器知道如何将字符串(例如Red和许多其他字符串)转换为Brush类型(在这种情况下是更具体的SolidColorBrush)。

类型转换器只是 XAML 的一个方面,使其比等效的 C++代码更简洁。

复杂属性

正如我们所见,设置属性是通过 XML 属性完成的。那么,对于无法表示为字符串并且没有类型转换器的复杂属性呢?在这种情况下,使用扩展语法(属性元素语法)来设置属性。这里有一个例子:

<Rectangle Fill="Red">
  <Rectangle.RenderTransform>
    <RotateTransform Angle="45" />
  </Rectangle.RenderTransform>
</Rectangle>

设置RenderTransform属性不能使用简单的字符串;它必须是从Transform类派生的对象(在这种情况下是RotateTransform)。

注意

各种示例属性(FillRenderTransform等)的确切含义将在第四章中讨论,布局、元素和控件

前面的标记等同于以下 C++代码:

auto r = ref new Rectangle;
r->Fill = ref new SolidColorBrush(Colors::Red);
auto rotate = ref new RotateTransform();
rotate->Angle = 45;
r->RenderTransform = rotate; 

依赖属性和附加属性

各种元素和控件上的大多数属性都不是正常的,它们不是简单的私有字段的包装器。依赖属性的重要性将在第五章中讨论,数据绑定。现在,重要的是要意识到在 XAML 中,依赖属性和常规属性之间没有区别;语法是相同的。实际上,仅仅通过在 XAML 中使用某个属性,无法判断某个属性是依赖属性还是普通属性。

注意

依赖属性提供以下功能(详细解释在第六章中提供,组件、模板和自定义元素):

  • 当属性值改变时进行更改通知

  • 某些属性的视觉继承(主要是与字体相关的属性)

  • 可能影响最终值的多个提供者(一个获胜)

  • 内存保护(值在改变时不分配)

某些 WinRT 功能,如数据绑定、样式和动画,依赖于该支持。

另一种依赖属性是附加属性。再次,详细讨论将推迟到第五章数据绑定,但基本上附加属性是上下文相关的——它由一个类型定义(具有将在第六章组件、模板和自定义控件中讨论的注册机制),但可以被任何继承自DependencyObject的类型使用(因为所有元素和控件都这样做)。由于这种属性不是由其使用的对象定义的,因此它在 XAML 中具有特殊的语法。以下是一个包含两个元素的Canvas面板的示例:

<Canvas>
  <Rectangle Fill="Red" Canvas.Left="120" Canvas.Top="40"
    Width="100" Height="50"/>
  <Ellipse Fill="Blue" Canvas.Left="30" Canvas.Top="90" 
    Width="80" Height="80" />
</Canvas>

Canvas.LeftCanvas.Top是附加属性。它们由Canvas类定义,但附加到RectangleEllipse元素上。附加属性只在某些情况下有意义。在这种情况下,它们指示画布内元素的确切位置。画布在布局阶段查找这些属性(在下一章中详细讨论)。这意味着,如果这些相同的元素放置在,比如一个Grid中,这些属性将没有效果,因为没有感兴趣的实体在这些属性中(但是没有伤害)。附加属性可以被视为动态属性,可以在对象上设置或不设置。

这是生成的 UI:

依赖属性和附加属性

在代码中设置附加属性有点冗长。以下是在名为_myrect的元素上设置Canvas.LeftCanvas.Top属性的等效 C++代码:

Canvas::SetLeft(_myrect, 120);
Canvas::SetTop(_myrect, 40);

前面的调用将变得明显的原因将在我们学习如何在第六章组件、模板和自定义元素中创建附加属性时讨论。

内容属性

Page对象和Grid对象之间的关系并不明显。Grid似乎在Page内部。但是这如何转换为代码呢?Page/Grid标记可以总结如下(忽略详细标记):

<Page>
    <Grid Background="...">
    </Grid>
</Page>

这实际上是以下标记的快捷方式:

<Page>
   <Page.Content>
      <Grid Background="...">
      </Grid>
   </Page.Content>
</Page>

这意味着Grid对象被设置为Page对象的Content属性;现在关系清晰了。XAML 解析器将某些属性(每个类型层次结构最多一个)视为默认或内容属性。它不一定要被命名为Content,但在Page的情况下是这样。这个属性在控件的元数据中使用Windows::UI::Xaml::Markup::ContentAttribute类属性来指定。在 Visual Studio 对象浏览器中查看Page类,没有这样的属性。但Page继承自UserControl;导航到UserControl,我们可以看到设置了该属性:

内容属性

注意

属性是一种以声明方式扩展类型元数据的方法。它们可以通过在应用该属性的项目之前的方括号中插入 C++/CX 中的属性类型名称来插入(可以是类、接口、方法、属性和其他代码元素)。属性类必须从Platform::Metadata::Attribute派生,才能被编译器视为这样的属性。

WinRT 类型中一些常见的ContentProperty属性如下:

  • ContentControlContent(以及所有派生类型)

  • UserControlContent

  • PanelChildren(所有布局容器的基类)

  • ItemsControlItems(集合型控件的基类)

  • GradientBrushGradientStopsLinearGradientBrush的基类)

集合属性

一些属性是集合(例如IVector<T>IMap<K,V>类型)。这些属性可以填充对象,XAML 解析器将调用IVector<T>::AppendIMap<K,V>::Insert方法。这是LinearGradientBrush的一个示例:

<Rectangle>
    <Rectangle.Fill>
        <LinearGradientBrush EndPoint="1,0">
            <GradientStop Offset="0" Color="Red" />
            <GradientStop Offset=".5" Color="Yellow" />
            <GradientStop Offset="1" Color="Blue" />
        </LinearGradientBrush>
    </Rectangle.Fill>
</Rectangle>

这里有两条规则。第一条是LinearGradientBrushContentPropertyGradientStops),不需要指定。它是GradientStopCollection类型,实现了IVector<GradientStop>,因此有资格进行自动追加。这相当于以下代码:

auto r = ref new Rectangle;
auto brush = ref new LinearGradientBrush;
brush->EndPoint = Point(1.0, 0);
auto stop = ref new GradientStop;
stop->Offset = 0; stop->Color = Colors::Red;
brush->GradientStops->Append(stop);
stop = ref new GradientStop;
stop->Offset = 0.5; stop->Color = Colors::Yellow;
brush->GradientStops->Append(stop);
stop = ref new GradientStop;
stop->Offset = 1; stop->Color = Colors::Blue;
brush->GradientStops->Append(stop);
r->Fill = brush;

这可能是 XAML 语法优势在 C++上的第一个明显迹象。以下是矩形的全部荣耀:

集合属性

对于IMap<K,V>,必须在每个项目上设置名为x:Key的属性,以指示发送到IMap<K,V>::Insert方法的键。在本章后面,我们将讨论资源时,将看到这样一个地图的例子。

标记扩展

标记扩展是对 XAML 解析器的特殊指令,提供了表达超出对象创建或设置某些属性的方式。这些指令仍然是声明性的,但它们的代码等效通常涉及调用方法,在 XAML 中直接不可能。

标记扩展放置在花括号内作为属性值。它们可以包含参数和属性,我们将在后面的章节中看到。在空白页面中默认使用的唯一标记扩展是{StaticResource},将在本章后面讨论。

注意

WPF 和 Silverlight 5 允许开发人员通过从MarkupExtension派生类来创建自定义标记扩展。当前 WinRT 实现中不支持此功能。

一种简单的标记扩展的例子是{x:Null}。每当需要指定值nullptr时,在 XAML 中使用它,因为没有更好的方法来使用字符串。以下示例在Rectangle元素中创建了一个空白:

<Rectangle Stroke="Red" StrokeThickness="10" Fill="{x:Null}" />

命名元素

通过 XAML 创建的对象可以使用x:Name XAML 属性进行命名。以下是一个例子:

<Rectangle x:Name="r1">
…
</Rectangle>

最终结果是一个私有成员变量(字段),由 XAML 编译器在MainPage.g.h中创建(如果在MainPage.xaml上工作):

private: ::Windows::UI::Xaml::Shapes::Rectangle^ r1;

引用本身必须在MainPage::InitializeComponent的实现中设置,使用以下代码:

// Get the Rectangle named 'r1'
r1 = safe_cast<::Windows::UI::Xaml::Shapes::Rectangle^>(
    static_cast<Windows::UI::Xaml::IFrameworkElement^>(
    this)->FindName(L"r1"));

提到的文件和方法在* XAML 编译和执行*部分进一步讨论。无论它是如何工作的,r1现在是对该特定矩形的引用。

连接事件到处理程序

事件可以通过与设置属性相同的语法连接到处理程序,但在这种情况下,属性的值必须是代码后台类中具有正确委托签名的方法。

如果在输入事件名称后两次按下* Tab*,Visual Studio 会自动添加一个方法。Visual Studio 使用的默认名称包括元素的名称(x:Name)(如果有)或其类型(如果没有),后跟下划线和事件名称,如果检测到重复,则后跟下划线和索引。默认名称通常不理想;一个更好的方法,仍然让 Visual Studio 创建正确的原型,是按照我们想要的方式编写处理程序名称,然后右键单击处理程序名称并选择导航到事件处理程序。这将创建处理程序(如果不存在)并切换到方法实现。

以下是 XAML 事件连接的示例:

<Button Content="Change" Click="OnChange" />

处理程序如下(假设 XAML 在MainPage.xaml中):

void MainPage::OnChange(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
{
}

提示

Visual Studio 还在类名前面写入命名空间名称(在前面的代码示例中删除了);这可以安全地删除,因为文件顶部存在正确命名空间的使用命名空间语句。此外,使用Platform::Object而不仅仅是Object(以及类似于RoutedEventArgs)不够可读;命名空间前缀可以被移除,因为它们默认在文件顶部设置。

所有事件(按照惯例)使用类似的委托。第一个参数始终是事件的发送者(在本例中是Button),第二个参数是有关事件的额外信息。RoutedEventArgs是事件的最小类型,称为路由事件。路由事件的详细讨论将在下一章中进行。

XAML 规则摘要

这是所有 XAML 规则的摘要:

  • XAML 元素意味着创建一个实例。

  • XAML 属性设置属性或事件处理程序。对于属性,根据属性的类型,可能会执行类型转换器。

  • 使用Type.Property元素语法设置复杂属性。

  • 使用Type.Property语法设置附加属性,其中Type是附加属性的声明类型。

  • ContentPropertyAttribute设置一个不需要指定的Content属性。

  • 作为集合的属性会自动调用AppendInsert的 XAML 解析器。

  • 标记扩展允许特殊(预定义)指令。

介绍 Blend for Visual Studio 2012 工具

Visual Studio 2012 安装了 Blend for Visual Studio 2012 工具。UI 设计师通常使用此工具来创建或操作基于 XAML 的应用程序的用户界面。

注意

Blend for Visual Studio 2012 的初始版本仅支持 Windows 8 商店应用程序和 Windows Phone 8 项目。对于 Visual Studio 2012 的更新 2 中添加了对 WPF 4.5 和 Silverlight 的支持。

Blend 可以与 Visual Studio 2012 一起使用,因为两者都能理解相同的文件类型(例如解决方案.sln文件)。在这两种工具之间来回切换并不罕见,每个工具都发挥其优势。这是 Blend 打开CH03.sln解决方案文件的屏幕截图(该解决方案包含本章节所有示例):

介绍 Blend for Visual Studio 2012 工具

上述屏幕截图显示了一个特定的 XAML 文件打开,其中选择了一个按钮。Blend 由几个窗口组成,其中一些与其 Visual Studio 对应部分相似,即项目属性。一些新窗口包括:

  • 资源:包含 WinRT 中可用的元素和控件(以及其他一些有用的快捷方式)

  • 对象时间轴:包括可视树中的所有对象以及动画

  • 资源:包含应用程序中的所有资源(参见下一节)

Blend 的设计界面允许操作元素和控件,这在 Visual Studio 中也是可能的。Blend 的布局和一些特殊的编辑功能使得 UI/图形设计师更容易使用,因为它模仿了其他流行的应用程序,如 Adobe Photoshop 和 Illustrator。

使用设计师进行的任何更改都会立即反映在更改的 XAML 中。切换回 Visual Studio 并接受重新加载选项会同步文件;当然,这两种方式都可以做到。

完全可以在 Blend 内部工作。按下F5以通常方式构建和启动应用程序。但是,Blend 不是 Visual Studio,不支持断点和其他调试任务。

Blend 是一个非常复杂的工具,远远超出了本书的范围。然而,通过实验可以走得更远。

XAML 编译和执行

作为正常编译过程的一部分运行的 XAML 编译器,将 XAML 作为内部资源放置在 EXE 或 DLL 中。在 XAML 根元素类型(例如MainPage)的构造函数中,调用InitializeComponent。该方法使用静态辅助方法Application::LoadComponent来加载 XAML 并解析它,创建对象,设置属性等。这是编译器为InitializeComponent创建的实现(在MainPage.g.hpp中,进行了一些代码清理):

void MainPage::InitializeComponent() {
  if (_contentLoaded)
  return;

  _contentLoaded = true;

  // Call LoadComponent on ms-appx:///MainPage.xaml
  Application::LoadComponent(this, 
    ref new ::Windows::Foundation::Uri(
    L"ms-appx:///MainPage.xaml"),    
  ComponentResourceLocation::Application);
}

将 XAML、H 和 CPP 文件连接到构建过程

从开发人员的角度来看,使用 XAML 文件还需要另外两个文件,即 H 和 CPP。让我们更详细地检查一下它们。这是默认的 MainPage.xaml.h(已删除注释和命名空间):

#include "MainPage.g.h"

namespace BasicXaml {
  public ref class MainPage sealed {
    public:
    MainPage();

    protected:
    virtual void OnNavigatedTo(NavigationEventArgs^ e)
    override;
  };
}

代码显示了一个构造函数和一个名为 OnNavigatedTo 的虚方法重写(对于本讨论不重要)。似乎缺少的一件事是在前一节中提到的 InitializeComponent 方法声明。还有之前提到的从 Page 继承也缺失了。原来 XAML 编译器生成了另一个名为 MainPage.g.hg 代表生成)的头文件,基于 XAML 本身(这可以通过顶部的 #include 声明来证明)。这个文件包含以下内容(可以通过选择 项目 | 显示所有文件,或等效的工具栏按钮,或右键单击 #include 并选择 打开文档… 来轻松打开):

namespace BasicXaml {
  partial ref class MainPage : public Page, 
  public IComponentConnector {
    public:
    void InitializeComponent();
    virtual void Connect(int connectionId, Object^ target);

    private:
    bool _contentLoaded;

  };
}

在这里我们找到了缺失的部分。在这里我们找到了 InitializeComponent,以及从 Page 派生。一个类怎么会有多个头文件?一个名为部分类的新 C++/CX 功能允许这样做。MainPage 类被标记为 partial,意味着它有更多的部分。最后一个部分不应该被标记为 partial,并且应该包含至少一个头文件,以便形成一个链,最终包括所有部分头文件;所有这些头文件必须是同一个编译单元(一个 CPP 文件)的一部分。MainPage.g.h 文件是在任何编译发生之前生成的;它是在编辑 XAML 文件时动态生成的。这很重要,因为命名元素是在那个文件中声明的,提供实例智能感知。

在编译过程中,MainPage.cpp 最终被编译,生成一个对象文件 MainPage.obj。它仍然有一些未解决的函数,比如 InitializeComponent。此时,MainPage.obj(以及其他 XAML 对象文件,如果存在)被用来生成元数据(.winmd)文件。

为了完成构建过程,编译器生成了 MainPage.g.hpp,实际上是一个实现文件,根据从元数据文件中提取的信息创建的(InitializeComponent 实现是在这个文件中生成的)。这个生成的文件只包含在一个名为 XamlTypeInfo.g.cpp 的文件中,这个文件也是根据元数据文件自动生成的(它的工作与数据绑定有关,如 第五章 中讨论的 数据绑定),但这已经足够让 MainPage.g.hpp 最终被编译,允许链接正确进行。

整个过程可以用以下图表总结:

将 XAML、H 和 CPP 文件连接到构建过程

资源

术语“资源”有很多含义。在经典的 Win32 编程中,资源指的是应用程序使用的只读数据块。典型的 Win32 资源包括字符串、位图、菜单、工具栏和对话框,但也可以创建自定义资源,使 Win32 将其视为未知的二进制数据块。

WinRT 定义了二进制、字符串和逻辑资源。以下部分讨论二进制和逻辑资源(字符串资源对于本节的本地化场景很有用,不在本节讨论范围内)。

二进制资源

二进制资源是指作为应用程序包的一部分提供的数据块。这些通常包括图像、字体和应用程序正常运行所需的任何其他静态数据。

可以通过在解决方案资源管理器中右键单击项目,然后选择 添加现有项 来将二进制资源添加到项目中。然后,选择必须位于项目目录或子目录中的文件。

注意

与 C#或 VB 项目相反,从位置添加现有项目不会将文件复制到项目的目录中。对于熟悉 C#/VB 项目的人来说,这种不一致性有点恼人,希望在将来的 Visual Studio 版本或服务包中能得到调和。

典型的商店应用程序项目已经在Assets项目文件夹中存储了一些二进制资源,即应用程序使用的图像:

二进制资源

使用文件夹是按类型或用途组织资源的好方法。右键单击项目节点并选择添加新过滤器会创建一个逻辑文件夹,可以将项目拖放到其中。

注意

与 C#/VB 项目相反,项目文件夹不会在文件系统中创建。建议实际上在文件系统中创建这些文件夹以便更好地组织。

添加的二进制资源作为应用程序包的一部分打包,并在可执行文件夹或子文件夹中可用,保持其相对位置。右键单击此类资源并选择属性会出现以下对话框:

二进制资源

内容属性必须设置为才能实际可用(默认值)。项目类型通常会被 Visual Studio 自动识别。如果没有,我们可以始终将其设置为文本并在代码中进行任何操作。

提示

不要将项目类型设置为资源。这在 WinRT 中不受支持,会导致编译错误(此设置实际上是为 WPF/Silverlight 准备的)。

根据需要,可以在 XAML 或代码中访问二进制资源。以下是一个示例,使用存储在应用程序的Assets文件夹下Images文件夹中的子文件夹中名为apple.png的图像的Image元素:

<Image Source="/Assets/Images/apple.png" />

注意相对 URI。前面的标记之所以有效是因为使用了类型转换器或Image::Source属性(类型为ImageSource)。该路径实际上是以下等效 URI 的快捷方式:

<Image Source="ms-appx:///Assets/Images/apple.png" />

其他属性可能需要稍有不同的语法,但都是通过ms-appx方案生成,表示应用程序包的根。

应用程序引用的另一个组件中存储的二进制资源可以使用以下语法访问:

<Image Source="/ResourceLibrary/jellyfish.jpg" />

标记假定应用程序引用了名为ResourceLibrary.Dll的组件 DLL,并且其根文件夹中存在名为jellyfish.jpg的二进制资源。

逻辑资源

二进制资源对于商店应用程序并不新鲜或独特。它们几乎永远存在。另一方面,逻辑资源是一个较新的添加。首先由 WPF 创建和使用,然后是各个版本的 Silverlight,它们也在 WinRT 中使用。那么,它们是什么?

逻辑资源几乎可以是任何东西。这些是对象,而不是二进制数据块。它们存储在ResourceDictionary对象中,并可以通过使用StaticResource标记扩展在 XAML 中轻松访问。

以下是使用相同画笔的两个元素的示例:

<Ellipse Grid.Row="0" Grid.Column="1">
    <Ellipse.Fill>
        <LinearGradientBrush EndPoint="0,1">
            <GradientStop Offset="0" Color="Green" />
            <GradientStop Offset=".5" Color="Orange" />
            <GradientStop Offset="1" Color="DarkRed" />
        </LinearGradientBrush>
    </Ellipse.Fill>
</Ellipse>
<Rectangle Grid.Row="1" Grid.Column="1" StrokeThickness="20">
    <Rectangle.Stroke>
        <LinearGradientBrush EndPoint="0,1">
            <GradientStop Offset="0" Color="Green" />
            <GradientStop Offset=".5" Color="Orange" />
            <GradientStop Offset="1" Color="DarkRed" />
        </LinearGradientBrush>
    </Rectangle.Stroke>
</Rectangle>

问题应该是不言自明的。我们两次使用了同一画笔。这有两个原因不好:

  • 如果我们想要更改画笔,我们需要做两次(因为重复)。如果该画笔被两个以上的元素使用,这自然会更严重。

  • 尽管只需要一个共享对象,但创建了两个不同的对象。

LinearGradientBrush可以转换为逻辑资源(或简单资源),并被任何需要它的元素引用。为此,画笔必须放置在ResourceDictionary对象中。幸运的是,每个元素都有一个Resources属性(类型为ResourceDictionary)可以使用。这通常在根 XAML 元素(通常是Page)上完成,或者(我们马上会看到的)在应用程序的 XAML(App.Xaml)中完成:

<Page.Resources>
    <LinearGradientBrush x:Key="brush1" EndPoint="0,1">
        <GradientStop Offset="0" Color="Green" />
        <GradientStop Offset=".5" Color="Orange" />
        <GradientStop Offset="1" Color="DarkRed" />
    </LinearGradientBrush>
</Page.Resources>

任何逻辑资源必须有一个键,因为它在字典中。该键由x:KeyXAML 指令指定。一旦放置,资源可以通过以下方式使用StaticResource标记扩展从Page中的任何元素中访问:

<Ellipse Fill="{StaticResource brush1}" />
<Rectangle Stroke="{StaticResource brush1}" StrokeThickness="40" />

StaticResource标记扩展从当前元素开始搜索具有指定键的资源。如果找不到,则在其父元素(例如 Grid)的资源上继续搜索。如果找到,则选择资源(在第一次请求时创建),并且StaticResource完成。如果找不到,则搜索父级的父级,依此类推。如果在顶级元素(通常是Page,但可以是UserControl或其他内容)中找不到资源,则在应用程序资源(App.xaml)中继续搜索。如果找不到,则抛出异常。搜索过程可以通过以下图表总结:

逻辑资源

注意

为什么标记扩展被称为StaticResource?是否有DynamicResourceDynamicResource仅存在于 WPF 中,它允许资源动态替换,并且所有绑定到它的对象都能注意到这种变化。这在 WinRT 中目前不受支持。

没有与StaticResource等效的单个调用,尽管如果需要,创建一个并不困难。可以在任何所需的级别上使用FrameworkElement::Resources属性进行查询,使用Parent属性导航到父元素。 Application::Resources属性具有特殊意义,因为在其中定义的任何资源都可以被整个应用程序中的任何页面或元素引用。这通常用于设置一致外观和感觉的各种默认值。

注意

将实际元素存储为资源可能很诱人(例如按钮)。应该避免这样做,因为资源在其使用容器中是单例;这意味着在同一页面中多次引用该按钮将导致在第二次引用时抛出异常,因为元素只能在可视树中出现一次。

资源实际上是用于可共享的对象,例如画笔、动画、样式和模板。

可以通过使用ResourceDictionary::Insert方法(在相关的ResourceDictionary上)动态添加资源,并通过调用ResourceDictionary::Remove来删除资源。这只对后续的{StaticResource}调用产生影响;已绑定的资源不受影响。

注意

资源也可以使用StaticResource标记扩展。为了使其工作,任何StaticResource必须引用在 XAML 中先前定义的资源;这是由于 XAML 解析器的工作方式。它无法找到尚未遇到的资源。

管理逻辑资源

逻辑资源可以是各种类型,例如画笔、几何图形、样式、模板等。将所有这些资源放在一个文件中,例如App.xaml,会阻碍可维护性。更好的方法是将不同类型的资源(或基于其他标准)从它们自己的文件中分离出来。但是,它们必须以某种方式从一个共同的文件(如App.xaml)中引用,以便它们被识别。

ResourceDictionary可以使用其MergedDictionaries属性(一个集合)合并其他资源字典。这意味着ResourceDictionary可以引用尽可能多的资源字典,并且可以拥有自己的资源。 Source属性必须指向ResourceDictionary的位置。由 Visual Studio 创建的默认App.xaml包含以下内容(已删除注释):

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary
              Source="Common/StandardStyles.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

确实,在Common文件夹中我们找到了一个名为StandardStyles.xaml的文件,其中包含一堆逻辑资源,其根元素为ResourceDictionary。当调用StaticResource时,要考虑到这个文件,它必须被另一个ResourceDictionary引用,可以是从Page或应用程序引用(应用程序更常见)。ResourceDictionary::MergedDictionaries属性包含其他ResourceDictionary对象,其Source属性必须指向要包含的所需 XAML 文件(该 XAML 文件必须以ResourceDictionary作为其根元素)。

我们可以使用 Visual Studio 的添加新项菜单选项并选择资源字典来创建自己的ResourceDictionary XAML:

管理逻辑资源

重复的键

在同一个ResourceDictionary实例中,两个对象不能具有相同的键。StaticResource会获取它在指定键中找到的第一个资源,即使该键已经存在于ResourceDictionary中。那么合并字典呢?

合并不同的资源字典可能会导致问题——来自不同合并字典的两个或更多具有相同键的资源。这不是错误,也不会引发异常。相反,所选对象是来自最后一个添加的资源字典(具有该键的资源)。此外,如果当前资源字典中的资源与其合并字典中的任何资源具有相同的键,它总是胜出。以下是一个例子:

<ResourceDictionary>
  <SolidColorBrush Color="Blue" x:Key="brush1" />
  <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="Resources/Brushes2.xaml" />
    <ResourceDictionary Source="Resources/Brushes1.xaml" />
  </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>     

根据这个标记,名为brush1的资源是蓝色的SolidColorBrush,因为它出现在ResourceDictionary本身中。这会覆盖合并字典中命名为brush1的任何资源。如果这个蓝色的画笔不存在,brush1将首先在Brushes1.xaml中查找,因为这是合并字典集合中的最后一个条目。

注意

包含ResourceDictionary作为其根的 XAML 可以使用静态XamlReader::Load方法从字符串动态加载,然后根据需要添加为合并字典。

样式

用户界面的一致性是一个重要特征;一致性有许多方面,其中之一是控件的一致外观和感觉。例如,所有按钮应该大致相同——类似的颜色、字体、大小等。样式提供了一种方便的方式,将一组属性分组到一个单一对象下,然后有选择地(或自动地,我们稍后会看到)将其应用到元素上。

样式总是被定义为资源(通常在应用程序级别,但也可以在PageUserControl级别)。一旦定义,它们可以通过设置FrameworkElement::Style属性应用到元素上。

以下是作为PageResources部分的一部分定义的样式:

<Page.Resources>
    <Style TargetType="Button" x:Key="style1">
        <Setter Property="FontSize" Value="40" />
        <Setter Property="Background">
            <Setter.Value>
                <LinearGradientBrush >
                    <GradientStop Offset="0" Color="Yellow" />
                    <GradientStop Offset="1" Color="Orange" />
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <Setter Property="Foreground" Value="DarkBlue" />
    </Style>
</Page.Resources>

该样式有一个键(style1),并且必须有TargetType。这是样式可以应用到的类型(以及任何派生类型)。XAML 解析器具有将TargetType转换为TypeName对象的类型转换器。

Style中的主要成分是其Setters集合(也是其ContentProperty)。该集合接受Setter对象,需要PropertyValue。属性必须是依赖属性(通常不是问题,因为大多数元素属性都是依赖属性);这些依赖属性由于幕后使用的类型转换器而作为简单字符串提供。

上面的标记设置了FontSizeBackground(由于LinearGradientBrush的复杂属性语法)和Foreground属性,都是为Button控件设置的。

一旦定义,样式可以通过在 XAML 中使用通常的StaticResource标记扩展来应用到元素,通过设置FrameworkElement::Style属性,如下例所示:

<Button Content="Styled button" Style="{StaticResource style1}" />

注意

熟悉 WPF 的读者可能会想知道是否可以省略TargetType属性,以便覆盖更大的控件范围。在当前版本的 WinRT 中不支持这样做。

在不兼容的元素类型上设置样式(例如在此示例中的CheckBox控件)会导致在页面加载时抛出异常。如果CheckBox也应该能够使用相同的样式,则可以将TargetType更改为ButtonBase(涵盖所有按钮类型)。

注意

为不同的元素使用不同的样式,即使基本类型似乎覆盖了几个控件。很可能以后某些属性可能需要针对特定类型进行微调,这样更改样式就会变得困难。为不同的具体类型构建不同的样式。您还可以使用样式继承(如后面所述)来缩短一些标记。

如果具有应用样式的元素将属性设置为与Style中的属性不同的值会发生什么?本地值获胜。这意味着以下按钮的字体大小为30而不是40

<Button Content="Styled button" FontSize="30" 
        Style="{StaticResource style1}" />

隐式(自动)样式

前一节展示了如何创建具有名称(x:Key)的样式以及如何将其应用于元素。然而,有时我们希望样式自动应用于特定类型的所有元素,以使应用程序具有一致的外观。例如,我们可能希望所有按钮都具有特定的字体大小或背景,而无需为每个按钮设置Style属性。这样可以更轻松地创建新按钮,因为开发人员/设计人员不必知道应用哪种样式(如果有的话,将自动使用范围内的隐式样式)。

要创建自动应用的Style,必须删除x:Key属性:

 <Style TargetType="Button">
 …
 </Style>

键仍然存在,因为Style属性仍然是ResourceDictionary的一部分(实现了IMap<Object, Object>),但会自动设置为指定TargetTypeTypeName对象。

一旦Style属性被定义,并且在ResourceDictionaryStyle属性范围内有任何Button元素(在本例中),那么该样式将自动应用。元素仍然可以通过设置本地值来覆盖任何属性。

注意

自动样式仅应用于确切类型,而不适用于派生类型。这意味着ButtonBase的自动样式是无用的,因为它是一个抽象类。

元素可能希望恢复其默认样式,并且不希望自动应用隐式样式。这可以通过将FrameworkElement::Style设置为nullptr(在 XAML 中为x:Null)来实现。

样式继承

样式支持继承的概念,与面向对象中的相同概念有些类似。这是使用BasedOn属性完成的,该属性必须指向要继承的另一个样式。派生样式的TargetType必须与基本样式中的相同。

继承样式可以为新属性添加Setter对象,或者可以为基本样式设置的属性提供不同的值。以下是按钮的基本样式示例:

<Style TargetType="Button" x:Key="buttonBaseStyle">
    <Setter Property="FontSize" Value="70" />
    <Setter Property="Margin" Value="4" />
    <Setter Property="Padding" Value="40,10" />
    <Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>

以下标记创建了三种继承样式:

<Style TargetType="Button" x:Key="numericStyle" 
       BasedOn="{StaticResource buttonBaseStyle}">
    <Setter Property="Background" Value="Blue" />
    <Setter Property="Foreground" Value="White" />
</Style>
<Style TargetType="Button" x:Key="operatorStyle" 
       BasedOn="{StaticResource buttonBaseStyle}">
    <Setter Property="Background" Value="Orange" />
    <Setter Property="Foreground" Value="Black" />
</Style>
<Style TargetType="Button" x:Key="specialStyle" 
       BasedOn="{StaticResource buttonBaseStyle}">
    <Setter Property="Background" Value="Red" />
    <Setter Property="Foreground" Value="White" />
</Style>

这些样式是一个简单的整数计算器应用程序的一部分。运行时,计算器如下所示:

Style inheritance

计算器的大部分元素都是按钮。以下是数字按钮的标记:

<Button Style="{StaticResource numericStyle}" Grid.Row="1" 
        Content="7" Click="OnNumericClick" />
<Button Style="{StaticResource numericStyle}" Grid.Row="1" 
        Grid.Column="1" Content="8" Click="OnNumericClick"/>
<Button Style="{StaticResource numericStyle}" Grid.Row="1" 
        Grid.Column="2"  Content="9" Click="OnNumericClick"/>

运算符按钮只是使用了不同的样式:

<Button Style="{StaticResource operatorStyle}" Grid.Row="3" 
      Grid.Column="3" Content="-" Click="OnOperatorClick"/>
<Button Style="{StaticResource operatorStyle}" Grid.Row="4" 
      Grid.Column="3" Content="+" Grid.ColumnSpan="2" 
      Click="OnOperatorClick"/>

=按钮使用与运算符相同的样式,但通过设置本地值来更改其背景:

<Button Style="{StaticResource operatorStyle}" Grid.Row="4" 
    Grid.Column="1" Grid.ColumnSpan="2" Content="=" 
 Background="Green" Click="OnCalculate"/>

完整项目名为StyledCalculator,可以在本章可下载源代码的一部分中找到。

样式继承可能看起来非常有用,但应谨慎使用。它遭受与面向对象继承相同的问题,在深层继承层次结构中,上层样式的更改可能会影响很多样式,有点不可预测,导致维护噩梦。因此,一个好的经验法则是最多有两个继承级别。超过这个数量可能会导致事情失控。

存储应用程序样式

由 Visual Studio 创建的商店应用项目在Common文件夹中有一个名为StandardStyles.xaml的默认样式文件。该文件包括所有常见元素和控件的样式,设置了一个推荐的共同外观和感觉作为起点。当然,可以根据需要更改这些样式或从中继承。

注意

WinRT 样式在概念上类似于 Web 开发中使用的 CSS,用于为 HTML 页面提供样式。层叠部分暗示了 CSS 的多层性质,就像 WinRT 样式的多层性质一样(应用程序、页面、面板、特定元素等)。

总结

本章主要讨论了 XAML,这是用于构建 Windows 商店应用用户界面的声明性语言。XAML 需要一些时间来适应,但它的声明性特性和标记扩展很难用 C++(或其他语言)的过程性代码来匹配。面向设计师的工具,如 Expression Blend 甚至 Visual Studio 设计师,使得相对容易地操纵 XAML 而不实际编写 XAML,但正如已经意识到的其他基于 XAML 的技术的开发人员和设计师所知,有时需要手动编写 XAML,这使得它成为一项重要的技能。

在下一章中,我们将继续大量使用 XAML,同时涵盖在 Windows 8 商店应用中使用的元素、控件和布局。

第四章:布局、元素和控件

上一章讨论了 XAML,这是一种中立的语言,用于创建对象并设置它们的属性。但是 XAML 只是一个工具,内容才是最重要的。构建有效的用户界面至少涉及选择最佳的元素和控件,以实现可用性和所需的用户体验。

在本章中,我们将介绍 WinRT 布局系统,并讨论构成大多数用户界面的主要元素和控件。

介绍布局

布局是元素放置和它们的大小和位置在用户交互或内容更改时发生变化的过程。在 Win32/MFC 世界中,布局通常非常简单和有限。控件是使用距离窗口左上角的距离放置的,并且它们的大小是明确指定的。这种模型的灵活性非常有限;如果控件的内容发生变化(例如变得更大),控件无法自动补偿。其他类似的变化对 UI 布局没有影响。

另一方面,WinRT 提供了一个基于一组布局面板的更灵活的模型,这些面板提供了不同的布局元素的方式。通过以各种方式组合这些面板,可以创建复杂和自适应的布局。

布局是一个两步过程。首先,布局容器询问每个子元素它们所需的大小。在第二步中,它使用适用的任何逻辑(对于该面板类型)来确定每个子元素的位置和大小,并将每个子元素放置在该矩形区域中。

每个元素向其父元素指示其大小要求。以下图总结了与这些要求相关的最重要的属性:

Introducing layout

以下是这些重要属性的快速概述:

  • Width/Height – 所讨论的元素的宽度和高度。通常不设置(在 XAML 中未设置值为默认值—"Auto"—更多内容稍后会介绍),这意味着元素希望尽可能大。但是,如果需要,这些可以设置。元素的实际(渲染)宽度和高度可以使用FrameworkElement::ActualWidthActualHeight只读属性获得。

  • MinWidth/MaxWidth/MinHeight/MaxHeight – 元素大小的最小值和最大值(图中未显示)。默认值为最小值为0,最大值为无穷大。

  • Margin – 元素周围的“呼吸空间”。这是Thickness类型,有四个字段(LeftTopRightBottom),用于确定元素周围的空间量。它可以在 XAML 中使用四个值(左、上、右、下)、两个值(第一个是左和右,第二个是上和下)或一个单一数字(四个方向上的相同距离)来指定。

  • Padding – 与Margin相同的概念,但确定元素的外边缘与其内容(如果有)之间的空间。这也被定义为Thickness,并由Control基类和一些其他特殊元素(如BorderTextBlock)定义。

  • HorizontalAlignment/VerticalAlignment – 指定元素相对于其父元素对齐的方式(如果有额外的空间)。可能的值是LeftCenterRightStretch(对于HorizontalAlignment),以及TopCenterBottomStretch(对于VerticalAlignment)。

  • HorizontalContentAlignment/VerticalContentAlignment(图中未显示)– 与Horizontal/VerticalAlignment相同的概念,但用于元素的Content(如果有)。

  • FlowDirection – 可用于将布局方向从默认值(LeftToRight)切换到RightToLeft,适用于从右到左的语言,如希伯来语或阿拉伯语。这实际上将每个“左”变为“右”,反之亦然。

在布局面板收集每个子元素所需的大小(通过对每个元素调用UIElement::Measure)之后,它进入布局的第二阶段——排列。在这个阶段,面板根据元素的期望大小(UIElement::DesiredSize只读属性)和适合该面板的任何算法来计算其子元素的最终位置和大小,并通过调用UIElement::Arrange通知每个元素所得到的矩形。这个过程可以递归进行,因为一个元素本身可以是一个布局面板,依此类推。结果被称为可视树。

注意

感兴趣的读者可能想知道如何在代码中为Width(例如)指定"Auto"XAML 值,因为这是一个double值。这是通过包括<limits>,然后使用表达式std::numeric_limits<double>::quiet_NaN()来完成的。类似地,要指定无限值,请使用std::numeric_limits<double>::infinity()

布局面板

所有布局面板都必须派生自Windows::UI::Xaml::Controls::Panel类,它本身派生自FrameworkElement。主要的附加PanelChildren属性(也是它的ContentProperty,用于更容易的 XAML 编写),它是实现IVector<UIElement>接口的元素集合。通过使用Children属性,可以动态地向Panel添加或删除元素。WinRT 提供了一堆特定的面板,每个面板都有自己的布局逻辑,提供了创建布局的灵活性。在接下来的章节中,我们将看一些内置的布局面板。

注意

所有面板类,以及稍后描述的元素和控件,都假定存在于Windows::UI::Xaml::Controls命名空间中,除非另有说明。

StackPanel

StackPanel是最简单的布局面板之一。它根据Orientation属性(Vertical是默认值)在堆栈中水平或垂直地布置其子元素。

当用于垂直布局时,每个元素都会得到它想要的高度和所有可用的宽度,反之亦然。这是StackPanel与一些元素的示例:

<StackPanel Orientation="Horizontal" >
    <TextBlock Text="Name:" FontSize="30" Margin="0,0,10,0"/>
    <TextBox Width="130" FontSize="30"/>
</StackPanel>

这是运行时的样子(在输入一些文本后):

StackPanel

StackPanel对于小型布局任务很有用,作为其他更复杂的布局面板的一部分。

Grid

Grid可能是最有用的布局面板,因为它很灵活。它创建了一个类似表格的单元格布局。元素可以占据单个或多个单元格,单元格大小是可定制的。我们已经使用Grid来创建了上一章中的计算器布局。这里是另一个Grid示例(包装在Border元素中),一个登录页面的标记:

<Border HorizontalAlignment="Center" VerticalAlignment="Center"
    BorderThickness="1" BorderBrush="Blue" Padding="10">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <TextBlock Text="Enter credentials:" Grid.ColumnSpan="2"
            TextAlignment="Center" FontSize="40" Margin="20"/>
        <TextBlock Text="Username:" TextAlignment="Right"
            Margin="10" Grid.Row="1" FontSize="40"
            VerticalAlignment="Bottom"/>
        <TextBox HorizontalAlignment="Left" Width="250"
            Grid.Row="1" Grid.Column="1" Margin="10" 
            FontSize="30" />
        <TextBlock Text="Password:" TextAlignment="Right"
            Margin="10" Grid.Row="2" FontSize="40"
            VerticalAlignment="Bottom" />
        <PasswordBox HorizontalAlignment="Left" Width="250"
            Grid.Row="2" Grid.Column="1" Margin="10" 
            FontSize="30" />
        <Button Content="Login" HorizontalAlignment="Stretch"
            Grid.Row="3" FontSize="30" Margin="10,30,10,10"
            Background="Green" />
        <Button Content="Cancel" HorizontalAlignment="Center" 
            Grid.Row="3" Grid.Column="1" FontSize="30" 
            Margin="10,30,10,10" Background="Red" />
    </Grid>
</Border>

这是运行时的样子:

Grid

行数和列数不是通过简单的属性来指定的。而是使用RowDefinition对象(对于行)和ColumnDefinition对象(对于列)来指定。原因在于可以根据行和/或列的大小和行为来指定。

RowDefinition有一个Height属性,而ColumnDefintion有一个Width属性。两者都是GridLength类型。有三种设置GridLength的选项:

  • 特定长度

  • 基于星号的(相对)因子(这是默认值,因子等于 1)

  • 自动长度

HeightRowDefintion)或WidthColumnDefinition)设置为特定数字会使该行/列具有特定的大小。在代码中,它相当于ref new GridLength(len)

在 XAML 中将HeightWidth设置为"Auto"会使行/列的高度/宽度根据放置在该行/列中的最高/最宽元素的需要而定。在代码中,它相当于静态属性GridLength::Auto

最后一个选项(默认情况下)是在 XAML 中将Height/Width设置为n*,其中n是一个数字(如果省略则为1)。这将与具有“星号”长度的其他行/列建立关系。例如,这是Grid的三行:

<RowDefinition Height="2*" />
<RowDefinition />
<RowDefinition Height="3*" />

这意味着第一行的高度是第二行的两倍(Height="*")。最后一行比第二行高三倍,比第一行高一倍半。即使Grid由于布局更改而动态调整大小,这些关系也会保持不变。

注意

“星号”因子的值不必是整数;它也可以是浮点数值。重要的是比例,而不是实际数字。

使用附加的Grid.RowGrid.Column属性将元素放置在特定的网格单元格中(两者默认为零,意味着第一行和第一列)。

元素默认情况下占用一个单元格。可以通过使用Grid.RowSpanGrid.ColumnSpan属性来更改这一点(在先前的 XAML 中为第一个TextBlock设置了这个属性)。

提示

可以使用大数字指定ColumnSpanRowSpan以确保元素将占据给定方向上的所有单元格。Grid将自动使用实际的行/列计数。

画布

Canvas模拟了经典的 Win32/MFC 布局——精确定位。如果需要精确坐标,例如图形、动画、图形游戏和其他复杂绘图的情况下,这种布局很有用。Canvas是最快的布局面板,因为它几乎没有布局(实际上几乎没有)。

以下是Canvas托管一些形状的示例:

<Canvas x:Name="_canvas" >
    <Ellipse Stroke="White" StrokeThickness="2" Fill="Red" 
        Width="100" Height="100" Canvas.Left="50"/>
    <Rectangle Stroke="White" StrokeThickness="2" Fill="Green" 
        Canvas.Left="100" Canvas.Top="120" Width="120" 
        Height="120"/>
    <Polygon Points="0,0 150,60 50,-70" Canvas.Left="250" 
        Canvas.Top="200" Fill="Blue" Stroke="White" 
        StrokeThickness="2" />
</Canvas>

输出如下所示:

Canvas

使用Canvas.LeftCanvas.Top附加属性设置放置坐标(两者默认为零,意味着Canvas的左上角)。Canvas定义的唯一其他附加属性是ZIndex。这指定了在Canvas内部渲染元素的相对顺序,其中大值将元素放置在顶部。默认情况下,XAML 中后定义的元素在 Z 顺序中更高。

作为更复杂的示例,假设我们想要允许用户使用鼠标或手指在Canvas上拖动形状。首先,我们将为指针按下、释放和移动添加事件处理程序:

<Canvas x:Name="_canvas" PointerPressed="OnPointerPressed" PointerReleased="OnPointerReleased" PointerMoved="OnPointerMoved">

注意

“指针”的概念取代了可能熟悉的来自 Win32/MFC/WPF/Silverlight 的“鼠标”事件名称;指针是通用的,代表任何指针设备,无论是鼠标、触控笔还是手指。

与指针相关的事件使用冒泡策略,这意味着对元素(例如使用的形状)的任何按压都会首先引发该形状上的PointerPressed事件,如果未处理(在这种情况下),则会冒泡到其父级(Canvas)上,那里会得到处理。

PointerPressed事件可以这样处理:

void MainPage::OnPointerPressed(Platform::Object^ sender,
   PointerRoutedEventArgs^ e) {
  _element = (FrameworkElement^)e->OriginalSource;
  if(_element == _canvas) return;
  _lastPoint = e->GetCurrentPoint(_canvas)->Position;
  _lastPoint.X -= (float)Canvas::GetLeft(_element);
  _lastPoint.Y -= (float)Canvas::GetTop(_element);
  _canvas->CapturePointer(e->Pointer);
  e->Handled = true;
  _isMoving = true;
}

由于此事件在Canvas上触发,即使原始元素是Canvas的子元素,我们如何才能到达该子元素?发送者参数是实际发送事件的对象——在这种情况下是Canvas。子元素由PointerRoutedEventArgs::OriginalSource属性指示(从RoutedEventArgs继承)。首先,检查是否按下指针实际上在Canvas本身上。如果是,该方法立即返回。

注意

在前面的Canvas中,这是不可能发生的。原因是Canvas的默认Background(或者任何其他Panel)是nullptr,因此无法在其上注册事件——它们会传播到其父级。如果需要Canvas本身上的事件,Background必须是一些非nullptrBrush;如果父级的背景Brush需要显示,使用ref new SolidColorBrush(Colors::Transparent)就足够了。

接下来,通过两个步骤提取按压的位置,首先使用PointerRoutedEventArgs::GetCurrentPointer()(这是一个PointerPoint对象),然后使用PointerPoint::Position属性(类型为Windows::Foundation::Point)。然后调整该点,使其成为按压点到元素左上角位置的偏移量,这有助于使后续移动准确。

捕获指针(UIElement::CapturePointer)确保Canvas继续接收指针相关事件,无论指针在何处。将PointerRoutedEventArgs::Handled设置为true可以防止进一步的冒泡(因为这里没有必要),并且设置一个标志,指示从现在开始应该发生移动,直到释放指针(另一个私有成员变量)。

注意

指针捕获与其他 UI 技术(Win32/MFC/WPF/Silverlight)中存在的鼠标捕获概念类似。

当指针移动时,相关元素也需要移动,只要指针尚未释放:

void MainPage::OnPointerMoved(Platform::Object^ sender,
   PointerRoutedEventArgs^ e) {
  if(_isMoving) {
    auto pos = e->GetCurrentPoint(_canvas)->Position;
    Canvas::SetLeft(_element, pos.X - _lastPoint.X);
    Canvas::SetTop(_element, pos.Y - _lastPoint.Y);
    e->Handled = true;
  }
}

这里的主要思想是通过设置附加的Canvas属性Canvas.LeftCanvas.Top(使用静态的Canvas::SetLeftCanvas::SetTop方法)来移动元素。

当指针最终释放时,我们只需要进行一些清理工作:

void MainPage::OnPointerReleased(Platform::Object^ sender,
   PointerRoutedEventArgs^ e) {
  _isMoving = false;
  _canvas->ReleasePointerCapture(e->Pointer);
  e->Handled = true;
}

完整的代码在一个名为CanvasDemo的项目中,是本章可下载代码的一部分。

注意

指针相关的方法可能看起来比需要的更复杂,但实际上并非如此。由于触摸输入通常是多点触控,如果两根手指同时按在两个不同的元素上并尝试移动它们会发生什么?可能会触发多个PointerPressed事件,因此需要一种方法来区分一个手指和另一个手指。先前的代码是在假设一次只使用一个手指的情况下实现的。

动态向面板添加子元素

Panel::Children属性可以通过编程方式进行操作(适用于任何Panel类型)。例如,使用Canvas作为绘图表面,我们可以使用先前的指针事件来添加连接到彼此的Line元素以创建绘图。当指针移动(在按下后),可以使用以下代码添加Line对象:

void MainPage::OnPointerMoved(Object^ sender, 
   PointerRoutedEventArgs^ e) {
  if(_isDrawing) {
    auto pt = e->GetCurrentPoint(_canvas);
    auto line = ref new Line();
    line->X1 = _lastPoint->Position.X;
    line->Y1 = _lastPoint->Position.Y;
    line->X2 = pt->Position.X;
    line->Y2 = pt->Position.Y;
    line->StrokeThickness = 2;
    line->Stroke = _paintBrush;
    _canvas->Children->Append(line);
    _lastPoint = pt;
  }
}

构造了一个Line对象,设置了适当的属性,最后将其添加到CanvasChildren集合中。如果没有这最后一步,那么Line对象将不会附加到任何东西上,并且当其引用超出范围时,它将被销毁。_paintBrush是由托管页面维护的Brush字段(未显示)。

完整的源代码在一个名为SimpleDraw的项目中,是本章可下载代码的一部分。以下是使用此应用程序完成的示例绘图:

动态向面板添加子元素

VariableSizedWrapGrid

StackPanelGridCanvas都非常直观;它们与 WPF 或 Silverlight 中的对应物几乎没有什么不同。WinRT 有一些更有趣的面板,从VariableSizedWrapGrid开始。

顾名思义,它本质上是一个网格,其中的项目按行或列排列(取决于Orientation属性)。当空间不足时,或者如果一行/列中的项目数量达到了MaximumRowsOrColumns属性设置的限制,布局将继续到下一行/列。

最后一个关于VariableSizedWrapGrid的技巧是,它有两个附加属性,RowSpanColumnSpan,可以改变一个项目的大小,使其占据多个单元格。以下是一个带有一堆Rectangle元素的VariableSizedWrapGrid示例:

<Grid Background=
    "{StaticResource ApplicationPageBackgroundThemeBrush}">
    <Grid.Resources>
        <Style TargetType="Rectangle">
            <Setter Property="Stroke" Value="White" />
            <Setter Property="StrokeThickness" Value="2" />
            <Setter Property="Margin" Value="8" />
            <Setter Property="Width" Value="100" />
            <Setter Property="Height" Value="100" />
            <Setter Property="Fill" Value="Red" />
        </Style>
    </Grid.Resources>
    <VariableSizedWrapGrid x:Name="_grid"     
        Orientation="Horizontal" 
        MaximumRowsOrColumns="6">
        <Rectangle />
        <Rectangle Fill="Yellow" />
        <Rectangle Fill="Purple"/>
        <Rectangle />
        <Rectangle Fill="Blue" VariableSizedWrapGrid.RowSpan="2" 
           Height="200"/>
        <Rectangle />
        <Rectangle Fill="Brown"/>
        <Rectangle VariableSizedWrapGrid.ColumnSpan="2" 
           Width="200" Fill="Aqua"/>
        <Rectangle Fill="LightBlue"/>
        <Rectangle Fill="Green"/>
        <Rectangle VariableSizedWrapGrid.ColumnSpan="2"
           VariableSizedWrapGrid.RowSpan="2" Width="150" 
           Height="150" Fill="BlueViolet"/>
        <Rectangle Fill="AntiqueWhite"/>
        <Rectangle Fill="Azure"/>
        <Rectangle />
        <Rectangle Fill="BlanchedAlmond"/>
        <Rectangle Fill="Orange"/>
        <Rectangle Fill="Crimson"/>
        <Rectangle Fill="DarkGoldenrod"/>
    </VariableSizedWrapGrid>
</Grid>

这是结果:

VariableSizedWrapGrid

面板虚拟化

所有先前讨论的面板在添加时都会创建它们的子元素。对于大多数情况,这是可以接受的。但是,如果项目数量非常多(数百个或更多),面板的性能可能会下降,因为需要创建和管理许多元素,占用内存并在创建时浪费 CPU 周期,或在布局更改时发生。虚拟化面板不会一次性创建它所持有的项目的所有元素;相反,它只会创建当前可见的实际元素。如果用户滚动以查看更多数据,则会根据需要创建元素。滚出视图的元素可能会被销毁。这种方案节省了内存和 CPU 时间(在创建时)。

VirtualizingPanel类是 WinRT 中所有虚拟化面板实现的抽象基类。VirtualizingPanel的进一步细化是OrientedVirtualizingPanel,表示具有固有方向的面板。WinRT 提供了三种虚拟化面板,我们将在稍后看到。

所有虚拟化面板都有一个更有趣的特点,它们只能用于自定义基于ItemsControl(通常使用数据绑定)的控件面板;它们不能像正常面板一样使用——在其中放置项目(在 XAML 或以编程方式)。ItemsControl及其派生类的完整讨论将在本章的后面部分进行;现在我们将快速查看现有虚拟化面板的工作方式;当讨论ItemsControl时,我们将在稍后看到使用示例。

虚拟化面板

最容易理解的虚拟化面板是VirtualizingStackPanel。它的行为就像常规的StackPanel,但它会虚拟化当前不可见的元素。

WrapGrid类似于VariableSizedWrapGrid,但没有“可变”部分(它没有可以更改单个元素大小的附加属性)。它在GridView中用作默认面板(GridView是从ItemsControl派生的许多类型之一)。它可以通过属性进行自定义,例如OrientationItemHeightItemWidthMaximumRowsOrColumns,这些属性大多是不言自明的。

CarouselControl类似于VirtualizingStackPanel,还具有在达到最后一个项目时滚动到第一个项目的功能。它被用作ComboBox的默认面板,并且实际上不能被其他控件使用,因此通常没有什么用处。

与元素和控件一起工作

“元素”和“控件”之间的区别在实践中并不那么重要,但了解这种区别是有用的。

元素FrameworkElement(直接或间接)派生,但不是从Control派生。它们具有一些外观并提供一些可通过更改属性进行自定义的功能。例如,Ellipse是一个元素。没有办法改变Ellipse的基本外观(并且能够将Ellipse变成矩形是不合逻辑的)。但是可以使用诸如StrokeStrokeThicknessFillStretch等属性以某种方式进行自定义。

另一方面,控件Control类(直接或间接)派生。Control添加了一堆属性,其中最重要的是Template属性。这允许完全更改控件的外观而不影响其行为。此外,所有这些都可以仅使用 XAML 实现,无需代码或任何类派生。我们将在第六章中讨论控件模板,组件,模板和自定义元素

以下类图显示了 WinRT 中一些基本的与元素相关的类:

与元素和控件一起工作

在接下来的几节中,我们将讨论各种元素和控件的组(基于派生和使用类别),研究它们的主要特点和用法。在每个组中,我们将查看一些更有用或独特的控件。这些部分并不完整(也不打算如此);更多信息可以在官方 MSDN 文档和示例中找到。

内容控件

内容控件派生自ContentControl类(它本身派生自Control)。ContentControl添加了两个重要属性:Content(也是其ContentProperty属性,使其在 XAML 中易于设置)和ContentTemplateContentControl的一个简单示例是Button

<Button Content="Login" FontSize="30" />

这个Content属性可能看起来像一个字符串,但实际上它的类型是Platform::Object^,意味着它可以是任何东西。

注意

Platform::Object指定“任何内容”似乎有些奇怪;毕竟,WinRT 是基于 COM 的,所以肯定有一个接口在后面。实际上,Platform::Object就是IInspectable接口指针的投影替代品。

ContentControl派生的类型使用以下规则呈现其Content

  • 如果它是一个字符串,将呈现TextBlock,其Text设置为字符串值。

  • 如果它是从UIElement派生的,它将按原样呈现。

  • 否则(Content不是从UIElement派生的,也不是字符串),如果ContentTemplatenullptr,那么内容将呈现为一个TextBlock,其Text设置为Content的字符串表示。否则,提供的DataTemplate用于呈现。

前述规则适用于任何从ContentControl派生的类型。在前面的按钮的情况下,使用第一条规则,因为ButtonContent是字符串Login。以下是使用第二条规则的示例:

<Button>
    <StackPanel Orientation="Horizontal">
        <Image Source="assets/upload.png" Stretch="None" />
        <TextBlock Text="Upload" FontSize="35"
            VerticalAlignment="Center" Margin="10,0,0,0" />
    </StackPanel>
</Button>

生成的按钮如下所示:

内容控件

生成的控件仍然是一个按钮,但其Content设置为从UIElement派生的类型(在本例中是StackPanel)。

第三条规则是最有趣的。假设我们有一个简单的数据对象实现如下:

namespace ContentControlsDemo {
  public ref class Book sealed {
  public:
    property Platform::String^ BookName;
    property Platform::String^ AuthorName;
    property double Price;
  };
}

有了这个实现,让我们在 XAML 中创建一个Book实例作为资源:

<Page.Resources>
    <local:Book x:Key="book1" BookName="Windows Internals"
       AuthorName="Mark Russinovich" Price="50.0" />
</Page.Resources>

注意

为了使其编译不出错,必须在MainPage.xaml.h中添加#include "book.h"。这样做的原因将在下一章中变得清晰。

现在,我们可以将从ContentControl(如Button)派生的类型的Content设置为该Book对象:

<Button Content="{StaticResource book1}" FontSize="30"/>

运行应用程序显示以下结果:

内容控件

结果只是类的完全限定类型名称(包括命名空间);这并不总是这样,这取决于所讨论的控件的默认控件模板。无论如何,显然这通常不是我们想要的。要为对象获取自定义呈现,需要一个DataTemplate,并将其插入到ContentTemplate属性中。

以下是一个为在问题中的Button中使用的DataTemplate的示例:

<Button Margin="12" Content="{StaticResource book1}" >
    <Button.ContentTemplate>
        <DataTemplate>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="15" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <TextBlock FontSize="35" Foreground="Yellow"
                    Text="{Binding BookName}" />
                <TextBlock Grid.Row="1" FontSize="25"
                    Foreground="Orange" 
                    Text="{Binding AuthorName}" />
                <TextBlock FontSize="40" Grid.Column="2"
                    Grid.RowSpan="2" TextAlignment="Center"
                    VerticalAlignment="Center">
                <Span FontSize="25">Just</Span><LineBreak />
                <Span FontSize="40">$</Span>
                <Run Text="{Binding Price}" FontSize="40" />
                </TextBlock>
            </Grid>
        </DataTemplate>
    </Button.ContentTemplate>
</Button>

这里有几点需要注意:

  • DataTemplate可以包含一个单一元素(通常是一个Panel—在本例中是Grid),并且可以构建任何所需的 UI。

  • 使用实际内容的属性是通过数据绑定表达式完成的,使用{Binding}标记扩展和属性名称。有关数据绑定的完整处理在下一章中找到。

  • 要使属性与数据对象(在本例中是Book)一起工作,必须像这样用Bindable属性装饰类(Book):

[Windows::UI::Xaml::Data::Bindable]
public ref class Book sealed {

结果如下所示:

内容控件

数据模板是可视化数据对象的强大工具;我们以后会遇到更多。现在,重要的是要意识到每个从ContentControl派生的类型都具有这种自定义能力。

在接下来的几节中,我们将讨论一些常见的ContentControl派生类型。

按钮

正如我们已经看到的,经典的Button控件是一个ContentControl,这意味着它可以拥有任何内容,但仍然像一个按钮一样工作。Button的大部分功能都是从它的抽象基类ButtonBase派生出来的。ButtonBase声明了无处不在的Click事件,以及一些其他有用的属性:

  • ClickMode - 指示什么构成“点击”:ReleasePressHover。自然地,这主要适用于鼠标。

  • Command - 指示在按钮被点击时要调用哪个命令(如果有的话)(命令将在下一章中讨论)。

  • CommandParameter - 与调用的命令一起发送的可选参数。

Button 派生自ButtonBase,在成员方面没有任何添加,除了具体化,而不是抽象化。

另一个ButtonBase的派生类是HyperlinkButton。它默认呈现为一个网页超链接,并添加了一个NavigationUri属性,导致自动导航到指定的 URI;Click事件通常不会被处理。

RepeatButton(在Windows::UI::Xaml::Controls::Primitives命名空间中)是另一个ButtonBase的派生类。只要按钮被按下,它就会引发Click事件;可以使用Delay(第一个Click事件)和IntervalClick事件引发的时间间隔)属性来指定Click事件的速率。

注意

RepeatButton本身不太有用;它主要作为其他更复杂的控件的构建块。这可以通过将控件放置在Primitives子命名空间中来暗示。例如,RepeatButton组成了ScrollBar的几个部分(它本身在Primitives命名空间中)。

另外两个有用的按钮控件是CheckBoxRadioButton。两者都派生自一个共同的基类ToggleButtonToggleButton定义了IsChecked属性,它可以有三个值(truefalsenullptr)。后者表示一个不确定的状态,由CheckBox支持(但不由RadioButton支持)。ToggleButton还声明了IsThreeState属性,以指示是否应允许第三种状态。最后,它定义了三个事件,CheckedUncheckedIndeterminate

CheckBox除了变得具体之外,对ToggleButton没有任何添加。RadioButton只添加了一个属性GroupName(一个字符串)。这允许对RadioButton控件进行分组,以用作排他性组。默认情况下,同一直接父级下的所有RadioButton控件都成为一组(该组中只能有一个IsChecked属性设置为true)。如果指定了GroupName,则所有具有相同GroupNameRadioButtons被视为一组。

这是一个简单的示例,使用了CheckBoxRadioButton控件:

<StackPanel>
    <TextBlock Text="What kind of tea would you like?"
       FontSize="25" Margin="4,12"/>
    <RadioButton Content="Earl Grey" IsChecked="True" Margin="4" 
       FontSize="20" />
    <RadioButton Content="Mint" Margin="4" FontSize="20"/>
    <RadioButton Content="Chinese Green" Margin="4" 
       FontSize="20"/>
    <RadioButton Content="Japanese Green" Margin="4" 
       FontSize="20"/>

    <TextBlock Text="Select tea supplements:" FontSize="25" 
       Margin="4,20,4,4" />
    <CheckBox Content="Sugar" Margin="4" FontSize="20" />
    <CheckBox Content="Milk" Margin="4" FontSize="20" />
    <CheckBox Content="Lemon" Margin="4" FontSize="20" />
</StackPanel>

在进行一些选择后,得到的显示如下:

Buttons

ScrollViewer

ScrollViewer是一个内容控件,它承载一个子元素(就像任何其他ContentControlContent属性一样),并使用一对ScrollBar控件来支持滚动。最重要的属性是VerticalScrollBarVisibilityHorizontalScrollBarVisibility,它们指示滚动的方式和滚动条的呈现方式。有四个选项(ScrollBarVisibility枚举):

  • Visible - 滚动条始终可见。如果内容不需要滚动,滚动条将被禁用。

  • Auto - 如果需要,滚动条会出现,如果不需要,它会消失。

  • Hidden - 滚动条不显示,但仍然可以使用键盘、触摸或编程方式进行滚动。

  • Disabled - 滚动条被隐藏,无法滚动。ScrollViewer不会给内容提供比它在该维度上拥有的更多的空间。

VerticalScrollBarVisibility的默认值为VisibleHorizontalScrollBarVisibility的默认值为Disabled

ScrollViewer的另一个有用功能是它能够通过缩放/捏触手势来允许Content进行放大或缩小。这是通过ZoomMode属性(EnabledDisabled)来控制的。

HorizontalScrollBarVisibilityVerticalScrollBarVisibilityZoomMode属性也作为附加属性公开,因此它们与内部使用ScrollViewer的其他控件相关,例如ListBoxGridView。以下是一个简单的示例,它改变了ListBox中水平滚动条的呈现方式:

<ListBox ScrollViewer.HorizontalScrollBarVisibility="Hidden">

其他需要注意的内容控件

以下是 WinRT 中一些其他ContentControl派生类型的简要描述。

AppBar

AppBar是一个用于应用栏的ContentControl,通常出现在底部(有时在顶部),如果用户从底部(或顶部)滑动或右键单击鼠标。它通常托管一个水平的StackPanel,其中包含各种选项的按钮。以下是一个来自天气应用程序的示例,该应用程序可在任何 Windows 8 安装中使用:

AppBar

Frame

Frame是用于在派生自Page的控件之间进行导航的ContentControl。使用Navigate方法与Page类型“导航”到该页面,通过创建一个实例并调用一些虚拟方法:在旧页面上调用OnNavigatedFrom(如果有的话),在新页面上调用OnNavigatedTo。默认情况下,应用程序向导在App::OnLaunched方法(Lanuched事件的事件处理程序)中创建一个Frame对象,然后快速导航到MainPage,代码如下:

rootFrame->Navigate(TypeName(MainPage::typeid), args->Arguments)

Navigate的第二个参数是一个可选的上下文参数,在OnNavigatedTo重写中可用(在NavigationEventArgs::Parameter中)。

Frame对象维护着一个页面的后退堆栈,可以使用GoBackGoForward等方法进行导航。CanGoBackCanGoForward只读属性可以帮助维护用于导航目的的按钮的状态。

导航到先前访问的页面可以创建这些页面的新实例或重用实例。CacheSize属性可以设置在导航期间在内存中保留的最大缓存页面数。要为特定的Page实例启用任何类型的缓存,必须将其Page::NavigationCacheMode属性设置为EnabledRequiredDisabled是默认值)。Enabled与缓存一起工作,而Required始终在内存中保持页面状态(Required设置不计入Frame::CacheSize值)。

SelectorItem

SelectorItem是可在ItemsControl控件中选择的项目的抽象基类(有关ItemsControl的描述,请参见下一节)。它只添加了一个属性:IsSelected。派生类型是其各自基于集合的控件中项目的容器:ListBoxItem(在ListBox中)、GridViewItem(在GridView中)、ListViewItem(在ListView中)等。

基于集合的控件

以下各节讨论了持有多个数据项的控件。这些都派生自提供所有派生类型的基本结构的ItemsControl类。

Items只读属性是托管在此ItemsControl中的对象的集合(类型为ItemCollection,也是其ContentProperty)。对象可以使用AppendInsert方法添加,使用RemoveRemoveAt方法移除(任何类型的对象都可以成为ItemsControl的一部分)。尽管这听起来很吸引人,但这不是与ItemsControl或其派生类型一起工作的典型方式;通常会将对象集合设置为ItemsSource属性(通常使用数据绑定表达式),并且自动使用Items属性在幕后填充控件。我们将在第五章数据绑定中看到这一点。

ItemsPanel属性允许更改特定ItemsControl中托管项目的默认Panel。例如,ListView使用垂直VirtualizingStackPanel作为其默认Panel。这可以通过ListView元素内的以下 XAML 片段更改为WrapGrid

<ListView.ItemsPanel>
    <ItemsPanelTemplate>
        <WrapGrid Orientation="Horizontal"/>
    </ItemsPanelTemplate>
</ListView.ItemsPanel>

ItemTemplate属性可以设置为DataTemplate,以显示集合中的对象。ItemTemplate具有与ContentControl::ContentTemplate相同的目的和规则,但适用于ItemsControl中的每个对象。我们将在下一章中看到ItemTemplate的用法示例。

DisplayMemberPath是一个String属性,如果ItemTemplatenullptr,则可以用来显示此ItemsControl中对象的某个属性(或子属性)。例如,假设我们使用以下Book类(之前定义):

[Bindable]
public ref class Book sealed {
public:
  property Platform::String^ BookName;
  property Platform::String^ AuthorName;
  property double Price;
  };

创建这样的Book对象数组,并将其放置在ItemsControl::ItemsSource属性中(或通过Items->Append方法手动添加它们),默认情况下会显示Book类型名称(假设没有设置ItemTemplate)。将DisplayMemberPath设置为"BookName"将在ItemsControl中显示每个对象的BookName

ItemContainerStyle属性可用于在此ItemsControl的特定容器项上放置Style。例如,设置ItemContainerStyle属性的ListView会影响ListViewItem控件,每个控件都包含所讨论的数据对象(根据内容的通常规则)。

我们将在下一章中看到ItemsControl的更多属性。以下部分简要讨论了一些从ItemsControl派生的常见类型。从技术上讲,只有一个这样的类:Selector,添加了SelectedItem(实际数据对象)和SelectedIndex(整数索引)属性的选择概念。SelectedValue属性根据SelectedValuePath属性指示所选项目的“值”。例如,如果控件保存Book对象,如前所示,并且SelectedValuePath"BookName",那么SelectedValue将保存SelectedItem的实际书名(SelectedItem保存整个Book对象)。

Selector还定义了一个事件SelectionChanged,当选定的项目发生变化时触发。

ListBox 和 ComboBox

ListBoxComboBox是经典 Windows 控件的 WinRT 版本。ListBox显示对象的集合(默认情况下是垂直的),如果需要,会有滚动条。ListBox还添加了多个选定项目的概念,具有SelectedItems属性和SelectionMode属性(SingleMultiple——每次单击/触摸都会选择/取消选择项目,以及Extended——按下Shift会选择多个连续对象,按下Ctrl会选择非相邻的组)。

ComboBox只显示一个从下拉列表中选择的项目。在商店应用中不鼓励使用这两个控件,因为它们的触摸行为不如应该的好,而且它们没有有趣的视觉过渡,使它们有点乏味;尽管如此,它们有时仍然可能有用,特别是ComboBox,它没有类似的替代品。

ListView 和 GridView

ListViewGridView都派生自ListViewBase(派生自Selector),它们是托管多个项目的首选控件。ListViewGridViewListViewBase没有任何添加——它们只是具有不同的ItemsPanel属性默认值和一些其他调整。

这两者都经过深思熟虑地设计,以适应触摸输入、过渡动画等;它们是显示对象集合的工作马。事实上,Visual Studio 有一些项目模板,用于构建示例ListViewGridView控件,以帮助开发人员入门:

ListView 和 GridView

FlipView

FlipView控件对Selector没有任何添加,但具有一种独特的外观,一次只显示一个(选定的)项目(类似于ComboBox),但允许通过向左或向右滑动或单击两侧的箭头来“翻转”项目。经典示例是翻转图像对象:

FlipView

基于文本的元素

文本是任何用户界面的重要部分。自然地,WinRT 提供了几个具有文本作为其主要视觉外观的元素和控件。通常涉及与字体相关的属性。这些包括:

  • FontSize - 文本的大小(double值)。

  • FontFamily - 字体系列名称(如"Arial"或"Verdana")。这可以包括备用字体系列(用逗号分隔),以防该特定字体不可用。

  • FontStretch - 指示字体的拉伸特性,如CondensedNormal(默认值),ExtraCondensedExpanded等。

  • FontWeight - 指示字体重量,如BoldExtraBoldMediumThin等(都取自FontWeights类的静态属性)。

  • FontStyle - NormalObliqueItalic之一。

所有与字体相关的属性都有一个显着的属性,它们为存在为元素的子元素(直接或间接)设置了一个“默认”字体。这意味着在Page对象上设置与字体相关的属性实际上为页面中的所有元素设置了默认字体(除了两个例外:由控件模板显式设置的字体属性和特定元素设置的本地字体属性;两者都会覆盖默认字体设置)。

大多数文本元素共有的另一个属性是Foreground。这设置绘制实际文本的Brush。有几种Brush类型,SolidColorBrush是最简单的,但还有其他类型,如LinearGradientBrushTileBrush

大多数与文本相关的元素共有的其他文本相关属性包括TextAlignmentLeftRightCenterJustify),TextTrimmingNoneWordEllipsis),和TextWrappingNoWrapWrap),都相当容易理解。

使用自定义字体

可以在 WinRT 中使用自定义字体。这涉及将字体文件添加到项目中(带有.TTF扩展名),并确保在 Visual Studio 中其Content属性设置为Yes

使用自定义字体

现在所需的就是使用FontFamily属性和特殊值,包括字体 URI(文件名和任何逻辑文件夹),一个井号(#)和字体名称本身,当在 Windows 中双击字体文件时可见。以下是使用标准字体和自定义字体的两行示例:

<StackPanel>
    <TextBlock Text="This text is in a built in font"
        FontFamily="Arial" FontSize="30" Margin="20"/>
    <TextBlock Text="This text is in old Star Trek style" 
       FontFamily="Finalold.ttf#Final Frontier Old Style" 
       FontSize="30" Margin="20" />
</StackPanel>

结果如下所示:

使用自定义字体

以下部分讨论了一些常见的与文本相关的元素和控件。

TextBlock

TextBlock可能是最有用的与文本相关的元素。它显示用户无法交互更改的文本(只能进行编程更改)。这对于显示静态文本非常有用,用户不应该编辑它。

注意

尽管文本无法在TextBlock中编辑,但用户仍然可以选择它(甚至可以通过按下Ctrl + C进行复制),如果IsTextSelectionEnabledtrue。如果是这样,还可以使用其他属性,即SelectedTextSelectionStartSelectionEnd(后者返回TextPointer对象)。

使用TextBlock最直接的方法是设置Text属性(一个String)和必要时的与字体相关的属性。作为Text的替代,TextBlock支持一组称为 inlines 的对象(通过Inlines属性,这也是它的ContentProperty用于 XAML 目的),允许构建一个更复杂的TextBlock,但仍然只使用一个元素(TextBlock)。

内联包括(都派生自InlineSpanRunLineBreakInlineUIContainer(都在Windows::UI::Xaml::Documents命名空间中)。Span是具有相同属性的更多内联的容器。Run具有Text属性并添加FlowDirectionLineBreak就是那样。InlineUIContainter不能在TextBlock中使用,只能在RichTextBlock中使用(稍后讨论)。

这是一个TextBlock的例子:

<TextBlock>
    <Run FontSize="30" Foreground="Red" Text="This is a run" />
    <LineBreak />
    <Span Foreground="Yellow" FontSize="20">
        <Run Text="This text is in yellow" />
        <LineBreak />
        <Run Text="And so is this" />
    </Span>
</TextBlock>

结果如下所示:

TextBlock

注意

如果Text属性与内联一起使用,Text优先,内联不会显示。

TextBox

TextBox是经典的文本输入控件,并提供了所有预期的控件功能。常见属性包括(除了字体属性和其他在本节开头讨论的属性):

  • Text - 用户实际显示或编辑的文本。

  • MaxLength - 用户输入的最大字符长度(在通过编程方式操作TextBox中的Text时不使用此设置)。

  • SelectedTextSelectedLengthSelectionStartSelectionEnd - 选择相关的属性(不言自明)。

  • IsReadOnly - 指示文本是否实际可编辑(默认为false)。

  • AcceptsReturn - 如果为true,表示多行TextBox(默认为false)。

  • InputScope - 指示在不使用物理键盘的触摸设备上应该弹出什么样的虚拟键盘。这可以帮助输入文本。值(来自InputScopeNameValue枚举)包括:UrlNumberEmailSmtpAddress(电子邮件地址)等。这是NumberInputScope的键盘截图:TextBox

这是InputScopeUrl的键盘的例子:

TextBox

这是EmailSmtpAddressInputScope的一个例子:

TextBox

TextBox定义了几个事件,其中TextChanged是最有用的。

PasswordBox

PasswordBox用于输入密码(毫不意外)。文本显示为单个重复字符,可以使用PasswordChar属性更改(默认为'*',显示为圆圈)。Password属性是实际密码,通常在代码中读取。

PasswordBox的一个很好的功能是一个“显示”按钮,当按下按钮时可以显示实际密码,有助于确保输入的密码是预期的;通过将IsPasswordRevealButtonEnabled设置为false可以关闭此功能。

RichTextBlock 和 RichEditBox

TextBlockTextBox的“丰富”版本提供了更丰富的格式化功能。例如,可以将字体相关属性设置为控件内的任何文本。

对于RichTextBlock,控件的实际内容在块对象的集合中(Blocks属性),只有一个派生类型 - ParagraphParagraph有自己的格式化属性,并且可以承载Inline对象(类似于TextBlock);RichTextBlock支持InlineUIContainer内联,可以嵌入元素(例如图像,或其他任何内容)作为文本的一部分。

RichEditBox允许更丰富的编辑功能,可以嵌入丰富内容,例如超链接。Document属性(类型为ITextDocument)提供了RichEditBox背后的对象模型的入口。此对象模型支持以文本和富文本(RTF)格式保存和加载文档,多次撤消/重做功能等其他功能。

图像

图像可以使用Image元素显示。Source属性指示应显示什么。最简单的可能性是将图像添加到项目作为内容:

<Image Source="penguins.jpg" />

Source属性是ImageSource类型;此标记仅起作用是因为存在类型转换器,可以将相对 URI 转换为从ImageSource派生的类型。

最简单的派生类型是BitmapImage(实际上是从BitmapSource派生的,而BitmapSource又是从ImageSource派生的)。BitmapImage可以从 URI(使用UriSource属性)初始化,这正是在前面的 XAML 中使用的类型转换器所发生的。

更有趣的类型是WriteableBitmap(也是从BitmapSource派生的),它公开了动态更改位图位的能力。

要创建WriteableBitmap,我们需要指定其像素尺寸,如下面的代码所示:

_bitmap = ref new WriteableBitmap(600, 600);

_bitmap是一个WriteableBitmap引用。接下来,我们将其设置为Image元素的Source属性:

_image->Source = _bitmap;

要访问实际的位,我们需要使用 WRL 的本机接口。首先,两个includes和一个 using 语句:

#include <robuffer.h>
#include <wrl.h>

using namespace Microsoft::WRL;

robuffer.h定义了IBufferByteAccess接口,与WriteableBitmap::PixelBuffer属性一起使用,如下所示:

ComPtr<IUnknown> buffer((IUnknown*)_bitmap->PixelBuffer);
ComPtr<IBufferByteAccess> byteBuffer;
buffer.As(&byteBuffer);
byte* bits;
byteBuffer->Buffer(&bits);

最后,可以使用这些位。以下是一个简单的示例,用随机颜色绘制位图中的第一行:

RGBQUAD* bits2 = (RGBQUAD*)bits;
RGBQUAD color = { 
   ::rand() & 0xff, ::rand() & 0xff, ::rand() & 0xff 
};
for(int x = 0; x < 600; x++)
  bits2[x] = color;
_bitmap->Invalidate();

调用WriteableBitmap::Invalidate是必要的,确保位图被重绘,从而连接的Image元素得到更新。

Stretch 属性

Image::Stretch属性设置ImageSource根据Image元素的大小进行拉伸的方式。以下是Stretch属性如何影响显示的图像:

Stretch 属性

使用Stretch=None,图像以其原始大小显示。在所示的图像中,企鹅被裁剪,因为图像太大而无法适应。UniformUniformToFill保留了纵横比(原始图像宽度除以高度),而Fill只是简单地拉伸图像以填充Image的可用空间。如果可用空间的纵横比与原始图像不同,UniformToFill可能会切掉内容。

注意

不要混淆ImageImageSourceImage是一个元素,因此可以放置在可视树的某个位置。ImageSource是实际数据,Image元素只是以某种方式显示图像数据。原始图像数据(ImageSource)保持不变。

语义缩放控件

SemanticZoom控件值得单独一节,因为它非常独特。它将两个视图合并到一个控件中,一个作为“缩小”视图,另一个作为“放大”视图。SemanticZoom背后的理念是两个相关的视图——一个更一般(缩小),另一个更具体(放大)。经典示例是开始屏幕。进行捏/缩放触摸手势(或按住Ctrl并滚动鼠标滚轮)在两个视图之间切换。以下是放大的视图:

语义缩放控件

这是缩小的视图:

语义缩放控件

ZoomedInViewZoomedOutView属性保存视图——通常是ListViewGridView,但在技术上可以是任何实现ISemanticZoomInformation接口的东西。

SemanticZoom是处理易于访问和直观的主/细节场景的有效方式。

总结

构建一个有效且引人入胜的用户界面本身就是一门艺术,超出了本书的范围。与 Windows Store 应用相关的现代设计指南相对较新,但可以在网上、微软网站和其他地方找到大量信息。

本章的目标是向 C++开发人员介绍 UI 景观,使其成为一个更舒适的区域。即使最终 C++开发人员将更关注应用程序逻辑、基础设施和其他低级活动,了解用户体验和用户界面的景观仍然是有用的。

在下一章中,我们将通过数据绑定将用户界面和数据联系起来,以创建健壮且可扩展的应用程序,至少在涉及用户界面和数据方面是这样。

第五章:数据绑定

在前两章中,我们看了 XAML 以及如何使用布局面板构建和布局用户界面元素。然而,用户界面只是第一步。必须在 UI 上设置一些数据来构成应用程序。

有几种方法可以将数据传递给控件。最简单、直接的方法可能是我们迄今为止一直在使用的方法;获取对控件的引用并在需要时更改相关属性。如果我们需要将一些文本放置在TextBox中,我们只需在需要时更改其Text属性。

这当然有效,当使用 Win32 API 进行 UI 目的时,确实没有其他方法。但这充其量是繁琐的,最糟糕的是会导致难以管理的维护头痛。不仅需要处理数据,还需要检查并可能动态更改元素状态,例如启用/禁用和选中/未选中。在 WinRT 中,大部分这些工作都是通过数据绑定来处理的。

理解数据绑定

数据绑定基本上很简单——某个对象(源)中的一个属性发生变化,另一个对象(目标)中的另一个属性以某种有意义的方式反映这种变化。结合数据模板,数据绑定提供了一种引人注目且强大的可视化和与数据交互的方式。

注意

熟悉 WPF 或 Silverlight 的人会发现 WinRT 数据绑定非常熟悉。在 WinRT 中有一些更改,主要是省略,使数据绑定比在 WPF/Silverlight 中稍微不那么强大。但是,它仍然比手动传输和同步数据要好得多。

WinRT 中的数据绑定导致了一种以无缝方式处理数据和 UI 的众所周知的模式之一,称为Model-View-ViewModelMVVM),我们将在本章末尾简要讨论。

数据绑定概念

我们将首先检查与数据绑定相关的一些基本术语,添加 WinRT 特定内容:

  • :要监视其属性以进行更改的对象。

  • 源路径:要监视的源对象上的属性。

  • 目标:当源发生变化时,其属性发生变化的对象。在 WinRT 中,目标属性必须是一个依赖属性(我们稍后会看到)。

  • 绑定模式:指示绑定的方向。

可能的值(均来自Windows::UI::Xaml::Data::BindingMode枚举)如下:

  • OneWay:源更改更新目标

  • TwoWay:源和目标相互更新

  • OneTime:源仅更新一次目标

数据绑定通常(大部分时间)在 XAML 中指定,提供了一种声明性和便捷的连接数据的方式。这直接减少了管理元素状态和控件与数据对象之间交换数据的编写代码量。

元素到元素的绑定

我们将首先检查的绑定场景是如何在不编写任何代码的情况下连接元素在一起的方式——通过在所需属性之间执行数据绑定。考虑以下两个元素:

<TextBlock Text="This is a sizing text"                   
    TextAlignment="Center" VerticalAlignment="Center"/>
<Slider x:Name="_slider" Grid.Row="1" Minimum="10" Maximum="100"
    Value="30"/>

假设我们希望根据Slider的当前Value来更改TextBlockFontSize。我们该如何做呢?

显而易见的方法是使用事件。我们可以对SliderValueChanged事件做出反应,并修改TextBlockFontSize属性值,使其等于SliderValue

这当然有效,但有一些缺点:

  • 需要编写 C++代码才能使其工作。这很遗憾,因为这里并没有使用真正的数据,这只是 UI 行为。也许设计师可以负责这一点,如果他/她只能使用 XAML 而不是代码。

  • 这样的逻辑可能会在将来发生变化,造成维护上的困扰——请记住,典型的用户界面将包含许多这样的交互——C++开发人员实际上并不想关心每一个这样的小细节。

数据绑定提供了一个优雅的解决方案。这是使这个想法工作所需的TextBlockFontSize设置,而不需要任何 C++代码:

FontSize="{Binding Path=Value, ElementName=_slider}"

数据绑定表达式必须在目标属性上使用{Binding}标记扩展。Path属性指示要查找的源属性(在这种情况下是Slider::Value),如果源对象是当前页面上的元素,则ElementName是要使用的属性(在这种情况下,Slider被命名为_slider)。

运行结果如下:

元素到元素绑定

拖动滑块会自动更改文本大小;这就是数据绑定的强大之处。

注意

如果BindingPath属性的值是第一个参数,则可以省略。这意味着前一个绑定表达式等同于以下内容:

FontSize="{Binding Value, ElementName=_slider}".

这更方便,大多数情况下会使用。

同样的表达式可以通过代码实现,例如:

auto binding = ref new Binding;
binding->Path = ref new PropertyPath("Value");
binding->ElementName = "_slider";
BindingOperations::SetBinding(_tb, TextBlock::FontSizeProperty,
binding);

代码假设_tb是相关TextBlock的名称。这显然更冗长,实际上只在特定情况下使用(我们将在第六章中进行检查,组件、模板和自定义元素)。

让我们再添加另一个元素,一个TextBox,其Text应该反映TextBlock的当前字体大小。我们也将使用数据绑定:

<TextBox Grid.Row="2" Text="{Binding Value, ElementName=_slider}" 
    FontSize="20" TextAlignment="Center"/>

这样可以工作。但是,如果我们更改TextBox的实际文本为不同的数字,字体大小不会改变。为什么?

原因是绑定默认是单向的。要指定双向绑定,我们需要更改绑定的Mode属性:

Text="{Binding Value, ElementName=_slider, Mode=TwoWay}"

现在,更改TextBox并将焦点移动到另一个控件(例如通过键盘上的Tab键或触摸其他元素),会更改TextBlockFontSize值。

对象到元素绑定

尽管元素到元素的绑定有时很有用,但经典的数据绑定场景涉及一个源,即常规的非 UI 对象,以及一个目标,即 UI 元素。绑定表达式本身类似于元素到元素绑定的情况;但自然地,不能使用ElementName属性。

第一步是创建一个可以支持数据绑定的对象。这必须是一个带有Bindable属性的 WinRT 类。绑定本身是在属性上的(一如既往)。以下是一个简单的Person类声明:

[Windows::UI::Xaml::Data::BindableAttribute]
public ref class Person sealed {
  public:
  property Platform::String^ FirstName;
  property Platform::String^ LastName;
  property int BirthYear;
};

前面的代码使用了自动实现的属性,现在足够了。

我们可以在 XAML 中创建这样的对象作为资源,然后使用Binding::Source属性来连接绑定本身。首先,两个Person对象被创建为资源:

<Page.Resources>
  <local:Person FirstName="Albert" LastName="Einstein" 
    BirthYear="1879" x:Key="p1" />
  <local:Person FirstName="Issac" LastName="Newton" 
    BirthYear="1642" x:Key="p2" />
</Page.Resources>

接下来,我们可以将这些对象绑定到元素,如下所示(都在StackPanel内):

<TextBlock Text="{Binding FirstName, Source={StaticResource p1}}"
  FontSize="30" />
<TextBlock Text="{Binding LastName, Source={StaticResource p1}}"
  FontSize="30" />
<TextBlock FontSize="30" >
  <Span>Born: </Span>
  <Run Text="{Binding BirthYear, Source={StaticResource p1}}" />
</TextBlock>

Source属性指的是被绑定的对象;在这种情况下是一个Person实例。以下是结果 UI:

对象到元素绑定

请注意,Source在每个绑定表达式中都有指定。如果没有它,绑定将会失败,因为没有源对象可以绑定。

由于所有三个元素的源都是相同的,因此可以一次性指定源,并允许所有相关元素自动绑定到它,而无需显式指定源,这将是有益的。幸运的是,使用FrameworkElement::DataContext属性是可能的。规则很简单,如果在绑定表达式中没有显式指定源,将在可视树中从目标元素开始搜索DataContext,直到找到一个或者到达可视树的根(通常是PageUserControl)。如果找到DataContext,它将成为绑定的源。以下是一个示例,它将DataContext设置为父StackPanel上的一个用于其子元素(无论是直接的还是不直接的)的示例:

<StackPanel Margin="4" DataContext="{StaticResource p2}">
    <TextBlock Text="{Binding FirstName}" />
    <TextBlock Text="{Binding LastName}" />
    <TextBlock>
        <Span>Born: </Span>
        <Run Text="{Binding BirthYear}" />
    </TextBlock>
</StackPanel>

这是结果(经过一些字体大小调整):

对象到元素绑定

绑定表达式工作正常,因为隐式源是Person对象,其键是p2。如果没有DataContext,所有这些绑定都会悄悄失败。

注意

注意数据绑定表达式如何通过DataContext简化。它们表达的意思是,“我不在乎源是什么,只要在范围内有一个名为<填写属性名称>DataContext属性。”

DataContext的概念是强大的,事实上,很少使用Source属性。

当然,在 XAML 中将SourceDataContext设置为预定义资源也是罕见的。通常通过代码获取相关数据源,如本地数据库或 Web 服务,来设置DataContext。但无论DataContext在何处或如何设置,它都能正常工作。

绑定失败

绑定是松散类型的——属性被指定为字符串,并且可能拼写错误。例如,在前面的示例中写FirstNam而不是FirstName不会引发任何异常;绑定会悄悄失败。如果程序在调试器下运行,则可以在Visual Studio 输出窗口(菜单中的查看 | 输出)中找到发生错误的唯一指示。

Error: BindingExpression path error: 'FirstNam' property not found on 'ElementObjectBinding.Person'. BindingExpression: Path='FirstNam' DataItem='ElementObjectBinding.Person'; target element is 'Windows.UI.Xaml.Controls.TextBlock' (Name='null'); target property is 'Text' (type 'String')

这段文字准确定位了确切的问题,指定了要绑定的属性名称,源对象类型以及有关目标的详细信息。这应该有助于修复拼写错误。

为什么没有抛出异常?原因是数据绑定可能在某个时间点失败,这没关系,因为此时尚未满足此绑定的条件;例如,可能有一些信息是从数据库或 Web 服务中检索的。当数据最终可用时,这些绑定突然开始正常工作。

这意味着无法真正调试数据绑定表达式。一个很好的功能是能够在 XAML 绑定表达式中设置断点。目前不支持这一功能,尽管在图形上可以在绑定上设置断点,但它根本不会触发。这个功能在 Silverlight 5 中可用;希望它会在未来的 WinRT 版本中得到支持。

提示

调试数据绑定的一种方法是使用值转换器,稍后在本章中讨论。

更改通知

数据绑定支持三种绑定模式:单向,双向和一次性。直到现在,绑定发生在页面首次加载时,并在此后保持不变。如果在绑定已经就位后改变Person对象上的属性值会发生什么?

在添加一个简单的按钮后,Click事件处理程序执行以下操作:

auto person = (Person^)this->Resources->Lookup("p1");
person->BirthYear++;

由于Person实例被定义为资源(不常见,但可能),它通过使用指定的键(p1)从页面的Resources属性中提取。然后递增BirthYear属性。

运行应用程序时没有视觉变化。在Click处理程序中设置断点确认它实际上被调用了,并且BirthYear已更改,但绑定似乎没有效果。

这是因为BirthYear属性当前的实现方式:

property int BirthYear;

这是一个使用私有字段在后台实现的琐碎实现。问题在于当属性改变时,没有人知道;具体来说,绑定系统不知道发生了什么。

要改变这一点,数据对象应该实现Windows::UI::Xaml::Data::INotifyPropertyChanged接口。绑定系统会查询此接口,如果找到,就会注册PropertyChanged事件(该接口的唯一成员)。以下是Person类的修订声明,重点是BirthYear属性:

[Bindable]
public ref class Person sealed : INotifyPropertyChanged {
public:
  property int BirthYear { 
    int get() { return _birthYear; }
    void set(int year);
  }

  virtual event PropertyChangedEventHandler^ PropertyChanged;

private:
  int _birthYear;
//...
};

getter 是内联实现的,setter 在 CPP 文件中实现如下:

void Person::BirthYear::set(int year) {
  _birthYear = year;
  PropertyChanged(this, 
  ref new PropertyChangedEventArgs("BirthYear"));
}

PropertyChanged 事件被触发,接受一个 PropertyChangedEventArgs 对象,该对象接受了更改的属性名称。现在,运行应用程序并点击按钮会显示一个增加的出生年份,如预期的那样。

这实际上意味着每个属性都应该以类似的方式实现;在 setter 中声明一个私有字段并在其中引发 PropertyChanged 事件。这是 FirstName 属性的修订实现(这次是内联实现):

property String^ FirstName {
  String^ get() { return _firstName; }
  void set(String^ name) {
    _firstName = name;
    PropertyChanged(this, 
    ref new PropertyChangedEventArgs("FirstName"));
  }
}

_firstName 是类内部定义的私有 String^ 字段。

绑定到集合

之前的例子使用了绑定到单个对象的属性。正如我们在前一章中看到的,从 ItemsControl 派生的一堆控件可以呈现多个数据项的信息。这些控件应该绑定到数据项的集合,比如 Person 对象的集合。

用于绑定目的的属性是 ItemsSource。这应该设置为一个集合,通常是 IVector<T>。这是一些 Person 对象绑定到 ListView 的例子(为方便初始化,Person 添加了一个构造函数):

auto people = ref new Vector<Person^>;
people->Append(ref new Person(L"Bart", L"Simpson", 1990));
people->Append(ref new Person(L"Lisa", L"Simpson", 1987));
people->Append(ref new Person(L"Homer", L"Simpson", 1960));
people->Append(ref new Person(L"Marge", L"Simpson", 1965));
people->Append(ref new Person(L"Maggie", L"Simpson", 2000));

要设置绑定,我们可以使用显式赋值给 ListView::ItemsSource 属性:

_theList->ItemsSource = people;

一个(优雅且首选的)替代方法是将 ItemsSource 绑定到与 DataContext 相关的内容。例如,ListView 的标记可以从这里开始:

<ListView ItemsSource="{Binding}" >

这意味着 ItemsSource 绑定到 DataContext 是什么(在这种情况下应该是一个集合)。缺少属性路径意味着对象本身。使用这个标记,绑定是通过以下简单的代码完成的:

DataContext = people;

要查看实际的 Person 对象,ItemsControl 提供了 ItemTemplate 属性,它是一个 DataTemplate 对象,定义了如何显示 Person 对象。默认情况下(没有 DataTemplate),会显示类型名称或对象的另一个字符串表示(如果有的话)。这很少有用。一个简单的替代方法是使用 DisplayMemberPath 属性来显示数据对象上的特定属性(例如 Person 对象的 FirstName)。一个更强大的方法是使用 DataTemplate,为每个通过数据绑定连接到实际对象的可自定义用户界面提供。这是我们 ListView 的一个例子:

<ListView ItemsSource="{Binding}">
  <ListView.ItemTemplate>
    <DataTemplate>
      <Border BorderThickness="0,1" Padding="4"
        BorderBrush="Red">
          <Grid>
            <Grid.RowDefinitions>
              <RowDefinition Height="Auto" />
              <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
              <ColumnDefinition Width="200"/>
              <ColumnDefinition Width="80" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="{Binding FirstName}"
            FontSize="20" />
            <TextBlock FontSize="16" Foreground="Yellow" 
            Grid.Row="1" Text="{Binding LastName}" />
            <TextBlock Grid.Column="1" Grid.RowSpan="2">
            <Span FontSize="15">Born</Span>
            <LineBreak />
            <Run FontSize="30" Foreground="Green" 
            Text="{Binding BirthYear}" />
          </TextBlock>
        </Grid>
      </Border> 
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>

DataTemplate 中的绑定表达式可以访问数据对象本身的相关属性。这是生成的 ListView

绑定到集合

自定义数据视图

数据模板提供了一种强大的方式来可视化和与数据交互,部分是因为数据绑定的强大功能。然而,有时需要更多的自定义。例如,在 Book 对象的列表中,当前打折的每本书都应该以不同的颜色显示,或者有一些特殊的动画等等。

以下部分描述了一些自定义数据模板的方法。

值转换器

值转换器是实现 Windows::UI::Xaml::Data::IValueConverter 接口的类型。该接口提供了一种将一个值转换为另一个值的方式,这两个值可以是不同类型的。假设我们想要显示一组书籍,但是打折的书应该有略微不同的外观。使用普通的数据模板,这很困难,除非有特定的 Book 属性对可视化有影响(比如颜色或画笔);这是不太可能的,因为数据对象应该关心数据,而不是如何显示数据。

这是 Book 类的定义(为简化示例,未实现更改通知):

[Windows::UI::Xaml::Data::BindableAttribute]
public ref class Book sealed {
public:
  property Platform::String^ BookName;
  property double Price;
  property Platform::String^ Author;
  property bool IsOnSale;

internal:
  Book(Platform::String^ bookName, Platform::String^ author,
    double price, bool onSale) {
    BookName = bookName;
    Author = author;
    Price = price;
    IsOnSale = onSale;
  }
};

值转换器提供了一个优雅的解决方案,使对象(在这个例子中是 Book)与其呈现方式解耦。这是一个基本的 Book 数据模板:

<ListView.ItemTemplate>
  <DataTemplate>
    <Border BorderThickness="1" BorderBrush="Blue" Margin="2"
    Padding="4">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="400" />
          <ColumnDefinition Width="50" />
        </Grid.ColumnDefinitions>
        <TextBlock VerticalAlignment="Center" 
          FontSize="20">
          <Run Text="{Binding BookName}" />
          <Span> by </Span>
          <Run Text="{Binding Author}" />
        </TextBlock>
        <TextBlock Grid.Column="1" FontSize="25">
          <Span>$</Span>
          <Run Text="{Binding Price}" />
        </TextBlock>
      </Grid>
    </Border>
  </DataTemplate>
</ListView.ItemTemplate>

这是书籍的显示方式:

值转换器

假设我们想要为打折的书籍使用绿色背景。我们不想在Book类中添加Background属性。相反,将使用值转换器将IsOnSale属性(布尔值)转换为适用于Background属性的Brush对象。

首先,我们的值转换器的声明如下:

public ref class OnSaleToBrushConverter sealed : IValueConverter {
public:
  virtual Object^ Convert(Object^ value, TypeName targetType,
  Object^ parameter, String^ language);
  virtual Object^ ConvertBack(Object^ value, TypeName
  targetType, Object^ parameter, String^ language);

  OnSaleToBrushConverter();

private:
  Brush^ _normalBrush;
  Brush^ _onSaleBrush;
};

有两种方法来实现:

  • Convert:从源到目标绑定时使用(通常的方式)

  • ConvertBack:仅适用于双向绑定

在我们的情况下,我们使用的是单向绑定,所以ConvertBack可以简单地返回nullptr或抛出异常。这是实现:

OnSaleToBrushConverter::OnSaleToBrushConverter() {
  _normalBrush = ref new SolidColorBrush(Colors::Transparent);
  _onSaleBrush = ref new SolidColorBrush(Colors::Green);
}

Object^ OnSaleToBrushConverter::Convert(Object^ value, TypeName targetType, Object^ parameter, String^ culture) {
  return (bool)value ? _onSaleBrush : _normalBrush;
}

Object^ OnSaleToBrushConverter::ConvertBack(Object^ value, TypeName targetType, Object^ parameter, String^ culture) {
  throw ref new NotImplementedException();
}

在构造函数中创建了两个画笔;一个用于普通书籍(透明),另一个用于打折书籍(绿色)。调用Convert方法时,value参数是所讨论书籍的IsOnSale属性。这将很快变得清楚。该方法只是查看布尔值并返回适当的画笔。这种转换是从布尔值到Brush

下一步将是实际创建转换器的实例。这通常是在 XAML 中完成的,将转换器作为资源:

<Page.Resources>
    <local:OnSaleToBrushConverter x:Key="sale2brush" />
</Page.Resources>

现在,为了最终连接,使用适当的属性绑定到IsOnSale并为操作提供一个转换器。在我们的情况下,BorderDataTemplate的一部分)非常合适:

<Border BorderThickness="1" BorderBrush="Blue" Margin="2"
    Padding="4" Background="{Binding IsOnSale, 
    Converter={StaticResource sale2brush}}">

没有转换器,绑定将会失败,因为没有办法自动将布尔值转换为Brush。转换器已经传递了IsOnSale的值,并且应该返回适合目标属性的内容以使转换成功。

注意

可以使用不带Path(在此示例中不带IsOnSale)的Binding表达式。结果是整个对象(Book)作为转换器的值参数传递。这有助于基于多个属性做出决策。

这是结果:

值转换器

让我们在打折的书旁边添加一个小图片。我们可以添加一张图片,但只有当书打折时才显示。我们可以使用(有点经典的)转换器,从布尔值转换为Visibility枚举,反之亦然:

Object^ BooleanToVisibilityConverter::Convert(Object^ value, TypeName targetType, Object^ parameter, String^ culture) {
  return (bool)value ? Visibility::Visible :
    Visibility::Collapsed;
}

Object^ BooleanToVisibilityConverter::ConvertBack(Object^ value, TypeName targetType, Object^ parameter, String^ culture) {
  return (Visibility)value == Visibility::Visible;
}

有了这个,我们可以像通常一样在资源中创建一个实例:

<local:BooleanToVisibilityConverter x:Key="bool2vis" />

然后,我们可以在需要时向第三列添加一张图片:

<Image Source="Assets/sun.png" VerticalAlignment="Center" 
  HorizontalAlignment="Center" Height="24" Grid.Column="2"
  Visibility="{Binding IsOnSale, Converter={StaticResource
  bool2vis}}" />

这是结果:

值转换器

值转换器非常强大,因为它们可以利用代码实现视觉变化。

Convert 和 ConvertBack 的其他参数

ConvertConvertBack接受更多参数,不仅仅是值。以下是完整列表:

  • valuevalue参数(第一个)对于Convert/ConvertBack方法非常重要。还有其他三个参数。

  • targetType:这表示应返回的预期对象类型。这可用于检查转换器是否正确使用(在我们的示例中,OnSaleToBrushConverterConvert方法的targetType将是Brush类型)。此参数的另一个可能用途是在更复杂的值转换器的情况下,可能需要处理多个返回类型并且可能需要了解当前请求。

  • parameter:这是一个自由参数,可以通过Binding表达式的ConverterParameter属性传递。这对于根据绑定表达式自定义值转换器很有用。

  • culture:这接收Binding表达式的ConverterLanguage属性的内容。这可用于根据语言返回不同的值,这实际上只是可以传递给转换器的另一个字符串。

数据模板选择器

在更极端的情况下,从DataTemplate所需的更改可能对值转换器没有用。如果不同的对象(在同一集合中)需要非常不同的模板,数据模板选择器可能是一个更好的选择。

数据模板选择器是一个从Windows::UI::Xaml::Controls::DataTemplateSelector派生的类(尽管命名空间不同,但它不是一个控件),并重写了以下定义的SelectTemplateCore方法:

protected:
virtual DataTemplate^ SelectTemplateCore(Object^ item, 
  DependencyObject^ container);

该方法需要返回与item参数对应的DataTemplate。在前面的示例中,每个项目都是Book;代码将查看一些Book属性,并得出应该使用哪个DataTemplate。这也可以基于container参数,在这种情况下,它是实际托管这些对象的控件(在我们的示例中是ListView)。

接下来,在 XAML 中创建此类的一个实例(类似于值转换器),并将该实例设置为ItemsControl::ItemTemplateSelector属性。如果设置了这个属性,ItemTemplate不能同时设置,因为它会与模板选择器使用的逻辑冲突。

命令

将用户界面的一部分连接到某些逻辑的传统方法是通过事件。典型的例子是按钮——当点击时,会执行一些操作,希望实现用户打算的某个目标。尽管 WinRT 完全支持这种模型(就像其他 UI 框架一样),但它也有缺点:

  • 事件处理程序是“代码后台”的一部分,其中声明了 UI,通常是PageUserControl。这使得从可能希望调用相同逻辑的其他对象中调用它变得困难。

  • 前面提到的按钮可能会消失并被不同的控件替换。这将需要潜在地更改事件挂钩代码。如果我们希望多个控件调用相同的功能怎么办?

  • 在某些状态下可能不允许执行操作——按钮(或其他任何东西)需要在正确的时间被禁用或启用。这给开发人员增加了管理开销——需要跟踪状态并为调用相同功能的所有 UI 元素更改它。

  • 事件处理程序只是一个方法——没有简单的方法来捕获它并将其保存在某个地方,例如用于撤销/重做的目的。

  • 在没有使用实际用户界面的情况下测试应用程序逻辑是困难的,因为逻辑和 UI 是交织在一起的。

这些以及其他更微妙的问题使得处理事件处理程序不太理想,特别是涉及应用程序逻辑时。如果某些事件只是为了增强可用性或仅为了服务 UI,通常不会引起关注。

解决此 UI 逻辑耦合的典型方法是命令的概念。这遵循了著名的“命令设计模式”,将应用程序逻辑抽象为不同的对象。作为一个对象,命令可以从多个位置调用,保存在列表中(例如,用于撤销目的),等等。它甚至可以指示在某些时间是否允许,从而使其他实体免于处理可能绑定到该命令的控件的实际启用或禁用。

WinRT 使用Windows::UI::Xaml::Input::ICommand接口定义了基本的命令支持。ICommand有两个方法和一个事件:

  • Execute方法:执行所讨论的命令。它接受一个参数,可以是任何可以用作命令参数的东西。

  • CanExecute方法:此方法指示此命令在此时是否可用。WinRT 将此作为启用或禁用命令源的提示。

  • CanExecuteChanged事件:这由命令引发,让 WinRT 知道它应该再次调用CanExecute,因为命令的可用性可能已经改变。

各种控件都有一个Command属性(类型为ICommand),可以设置(通常使用数据绑定)指向由ICommand实现的对象的对象(和一个CommandParameter,允许将一些信息传递给命令)。经典的例子是经典的Button。当按钮被点击时,将调用挂接命令的Execute方法。这意味着不需要设置Click处理程序。

WinRT 没有为ICommand提供任何实现。开发人员需要创建适当的实现。下面是一个简单的用于增加一个人出生年份的命令的实现:

public ref class IncreaseAgeCommand sealed : ICommand {
public:
  virtual void Execute(Platform::Object^ parameter);
  virtual bool CanExecute(Platform::Object^ parameter);
  virtual event EventHandler<Object^>^ CanExecuteChanged;

};

实现如下:

void IncreaseAgeCommand::Execute(Object^ parameter)  {
  auto person = (Person^)parameter;
  person->BirthYear++;
}

bool IncreaseAgeCommand::CanExecute(Object^ parameter) {
  return true;
}

为了使其工作,我们可以创建一个命令源,比如一个按钮,并填写命令的细节如下:

<Button Content="Inrease Birth Year With Command" 
  CommandParameter="{StaticResource p1}">
  <Button.Command>
    <local:IncreaseAgeCommand />
  </Button.Command>
</Button>

Command属性中创建一个命令是不寻常的,通常的方式是绑定到 ViewModel 上的适当属性,我们将在下一节中看到。

MVVM 简介

命令只是处理非平凡应用程序中用户界面更一般模式的一个方面。为此,有许多 UI 设计模式可用,如模型视图控制器MVC)、模型视图呈现器MVP)和模型-视图-视图模型MVVM)。所有这些都有共同之处:将实际 UI(视图)与应用程序逻辑(控制器、呈现器和视图模型)以及底层数据(模型)分离。

WPF 和 Silverlight 推广的 MVVM 模式利用数据绑定和命令的力量,通过使用中介(视图模型)在 UI(视图)和数据(模型)之间创建解耦。

MVVM 组成部分

MVVM 有三个参与者。模型代表数据或业务逻辑。这可能包括可以用标准 C++编写的类型,而不考虑 WinRT。它通常是中立的;也就是说,它不知道它将如何被使用。

视图是实际的 UI。它应该显示模型的相关部分并提供所需的交互功能。视图不应直接了解模型,这就是数据绑定的作用。所有绑定都访问一个属性,而不明确知道另一端是什么类型的对象。这种魔术在运行时通过将视图的DataContext设置为提供数据的对象来满足;这就是 ViewModel。

ViewModel 是将所需数据分发给视图(基于模型)的粘合剂。ViewModel 就是这样——视图的模型。它有几个责任:

  • 在视图中公开允许绑定的属性。这可能只是通过访问模型上的属性(如果它是用 WinRT 编写的),但如果模型以另一种方式公开数据(比如使用方法)或需要翻译的类型,比如需要返回为IVector<T>std::vector<T>,可能会更复杂。

  • 公开命令(ICommand)以供视图中的元素调用。

  • 维护视图的相关状态。

模型、视图和视图模型之间的整个关系可以用以下图表来总结:

MVVM 组成部分

构建 MVVM 框架

在这一点上应该很清楚,基于 MVVM 的应用程序有很多共同的元素,比如变更通知和命令。创建一个可在许多应用程序中简单利用的可重用框架将是有益的。虽然有几个很好的框架(大多是免费的),它们都是基于.NET 的,这意味着它们不能在 C++应用程序中使用,因为它们没有作为 WinRT 组件公开,即使它们这样做了,C++应用程序也必须付出.NET CLR 的代价。自己构建这样的框架并不太困难,而且会增强我们的理解。

我们要解决的第一件事是希望对象能够实现INotifyPropertyChanged接口,以便它们在任何属性更改时都能引发PropertyChanged事件。我们可以用以下 WinRT 类来实现这一点:

public ref class ObservableObject : 
  DependencyObject, INotifyPropertyChanged {
  public:
    virtual event PropertyChangedEventHandler^ PropertyChanged;
  protected:
    virtual void OnPropertyChanged(Platform::String^ name);
  };

实现如下:

void ObservableObject::OnPropertyChanged(String^ name) {
  PropertyChanged(this, ref new PropertyChangedEventArgs(name));
}

DependencyObject继承可能看起来是多余的,但实际上这是必要的,以规避当前 WinRT 支持中的一个不足之处——任何常规类都必须是密封的,使其作为基类毫无用处。任何从DependencyObject继承的类都可以保持未密封状态——这正是我们想要的。

ObservableObject类似乎非常简单,也许不值得作为一个单独的类。但我们可以为其添加任何派生类都可以受益的常见功能。例如,我们可以支持ICustomPropertyProvider接口——该接口允许对象支持动态属性,这些属性在类型中并非静态部分(感兴趣的读者可以在 MSDN 文档中找到更多信息)。

具体类型可以使用类似以下代码的ObservableObject

public ref class Book : ObservableObject {
public:
  property Platform::String^ BookName {
    Platform::String^ get() { return _bookName; }
  void set(Platform::String^ name) {
    _bookName = name;
    OnPropertyChanged("BookName");
  }
}

property bool IsOnLoan {
  bool get() { return _isOnLoan; }
  void set(bool isLoaned) {
    _isOnLoan = isLoaned;
    OnPropertyChanged("IsOnLoan");
  }
}

private:
  Platform::String^ _bookName;
  bool _isOnLoan;
//...
};

接下来要处理的是命令。正如我们所见,我们可以通过实现ICommand来创建一个命令,有时这是必要的。另一种方法是创建一个更通用的类,该类使用委托来调用我们想要响应ExecuteCanExecute方法的任何代码。以下是这样一个命令的示例:

public delegate void ExecuteCommandDelegate(Platform::Object^
  parameter);
public delegate bool CanExecuteCommandDelegate(Platform::Object^
  parameter);

public ref class DelegateCommand sealed : ICommand {
public:
  DelegateCommand(ExecuteCommandDelegate^ executeHandler,
    CanExecuteCommandDelegate^ canExecuteHandler)
  : _executeHandler(executeHandler),
    _canExecuteHandler(canExecuteHandler) { }

  virtual bool CanExecute(Platform::Object^ parameter) {
    if (_canExecuteHandler != nullptr)
    return _canExecuteHandler(parameter);

    return true;
  }

  virtual void Execute(Platform::Object^ parameter) {
    if (_executeHandler != nullptr && CanExecute(parameter))
    _executeHandler(parameter);
  }

 virtual event EventHandler<Platform::Object^>^ 
    CanExecuteChanged;

private:
  ExecuteCommandDelegate^ _executeHandler;
  CanExecuteCommandDelegate^ _canExecuteHandler;
};

该类利用委托,构造函数中接受两个委托;第一个用于执行命令,第二个用于指示命令是否启用。

以下是一个公开命令以使书籍被借出的 ViewModel:

public ref class LibraryViewModel sealed : ObservableObject {
public:
  property IVector<Book^>^ Books {
    IVector<Book^>^ get() { return _books; }
  }

  property ICommand^ LoanBookCommand {
    ICommand^ get() { return _loanBookCommand; }
  }

internal:
  LibraryViewModel();

private:
  Platform::Collections::Vector<Book^>^ _books;
  ICommand^ _loanBookCommand;
};

命令是在 ViewModel 的构造函数中创建的:

LibraryViewModel::LibraryViewModel() {
  _loanBookCommand = ref new DelegateCommand
  (ref new ExecuteCommandDelegate([](Object^ parameter) {
    // execute the command
    auto book = (Book^)parameter;
    book->IsOnLoan = true;
  }), nullptr);	// command is always enabled
}

ViewModel 是无控制(视图)的,这意味着我们可以在没有任何可见用户界面的情况下构建它。它公开了用于数据绑定到相关视图的属性和用于执行来自视图的操作的命令。实际操作通常会修改适当模型中的某些状态。

注意

视图和 ViewModel 之间通常是一对一的映射。虽然有时可以共享,但不建议这样做。

有关 MVVM 的更多信息

这是 MVVM 的快速介绍。由于这是一种众所周知的模式(多亏了它在 WPF 和 Silverlight 中的使用),因此网络上有很多相关资料。可以添加的一些内容包括支持导航的 ViewModel(以便不直接访问Frame控件)、ViewModel 定位器服务(允许更轻松地在视图和其对应的 ViewModel 之间进行绑定)等。

注意

有关 MVVM 的更多信息,请参阅维基百科en.wikipedia.org/wiki/Model_View_ViewModel

在 C++中实现 WinRT MVVM 框架有些麻烦,因为(目前)不可能将这样的框架公开为 Windows 运行时组件,而只能作为 C++静态或动态库。

尽管如此,数据和视图之间的分离是重要的,除了最简单的应用程序外,所有应用程序都将受益。

总结

在本章中,我们了解了数据绑定是什么以及如何使用它。数据绑定是一个非常强大的概念,在 WinRT 中的实现非常强大。来自 Win32 或 MFC 背景的开发人员应该意识到,连接显示和数据需要采用不同的方法。数据绑定提供了一种声明性模型,支持数据和显示之间的分离,因此应用程序逻辑只处理数据,实际上并不关心哪些控件(如果有)绑定到该数据。

MVVM 概念使这种分离更加清晰,并为逐步增强应用程序奠定了基础,而不会增加维护头疼和逻辑复杂性。

在下一章中,我们将看看如何构建可重用的 WinRT 组件,以及自定义元素。

第六章:组件、模板和自定义元素

在前几章中,我们通过查看布局和元素如何携手合作来创建灵活的用户界面的基础知识。数据绑定提供了一种分离的方式来写入和读取数据到控件中。

在本章中,我们将探讨使用控件模板以一种基本而强大的方式来自定义控件的方法。当需要控件的功能而不是外观时,这是很有用的。在其他情况下,内置控件可能没有所需的行为;在这些情况下,可以为特定应用程序需求创建自定义和用户控件。但首先,我们应该考虑使用 C++构建组件的更一般概念,以及如何在 C++和非 C++项目中使用这些组件。

Windows Runtime 组件

正如我们在第二章中所看到的,用于 Windows 8 商店应用的 COM 和 C++,Windows Runtime 是基于实现 COM 接口的 COM 类。任何这样的类,如果也写入了元数据(一个winmd文件),就可以从 DLL 中导出,并且可以被任何其他符合 WinRT 标准的语言或环境使用;目前支持的语言有 C++、.NET 语言(C#和 VB)和 JavaScript。

这些组件必须只在其公共接口中使用 WinRT 类型。对于 C++来说,这意味着基于 STL 的类只能在 WinRT 类的非公共区域中使用。在公共方法或属性中传递时,这些类必须转换为 WinRT 类型。

一个典型的场景是一个现有的 C++类型,可能是在过去的某个时候编写的,并且需要在 WinRT 中用于数据绑定的目的,或者至少需要暴露给当前项目之外的 WinRT 客户端使用。让我们看看如何实现这种过渡。

将 C++转换为 WinRT

让我们举一个具体的例子,然后更广泛地讨论。假设我们有以下标准的 C++类:

#include <string>
#include <vector>

class book_review {
public:
  book_review(const std::wstring& name, 
  const std::wstring& content,
    int rating);

  int rating() const { return _rating; }
  void set_rating(int rating) { _rating = rating; }
  const std::wstring& name() const { return _name; }
  const std::wstring& content() const { return _content; }

private:
  std::wstring _name;
  std::wstring _content;
  int _rating;
};

class book {
public:
  book(const std::wstring& name, const std::wstring& author);
  void add_review(const book_review& review);
  size_t reviews_count() const { return _reviews.size(); }
  const book_review& get_review(size_t index) const { 
    return _reviews[index]; 
  }
  const std::wstring& name() const { return _name; }
  const std::wstring& author() const { return _author; }

private:
  std::wstring _name;
  std::wstring _author;
  std::vector<book_review> _reviews;
};

简单地说,一个book类被定义,并且有一个名称,一个作者,以及一系列的评论(book_review类)。每个评论包括一个名称,评论内容和一个数字评分。

这些类是用标准 C++编写的,对 WinRT(或者说 C++/CX)一无所知。

目前,这些类只能在 C++项目中内部使用。它们不能被导出到其他 WinRT 环境(例如.NET),即使在 C++项目中,它们也不能从数据绑定等功能中受益,因为它们在任何方面都不是 WinRT 类。

这些(以及类似的)类需要包装在一个 WinRT 类中。对于 C++来说,可以通过两种方式来实现。第一种是使用 WRL;好处是使用标准的 C++(而不是微软特定的扩展),但这个好处在一定程度上减弱了,因为 WinRT 本身就是微软特定的(至少在撰写本文时是这样)。第二个可能的好处是更多地控制生成的 WinRT 类型。虽然这听起来很吸引人,但这样做也更难,对于大多数情况来说是不必要的,所以大部分时间我们会采用更简单的方法,利用 C++/CX。

注意

使用 WRL 创建 WinRT 组件有时是必要的。一个例子是当一个单一的类需要实现一个 WinRT 接口和一个本地的 COM 接口时。例如,媒体编码器或解码器必须是实现 COM/WinRT 类,不仅要实现Windows::Media::IMediaExtension接口,还要实现媒体基金非 WinRT 接口IMFTransform。WRL 是实现这一点的唯一方法。

为了包装与书籍相关的类,我们将创建一个 Windows Runtime 组件项目(我们将其称为BookLibrary)。然后,我们将添加一个 C++/CX WinRT 类来包装bookbook_review。让我们从book_review包装器开始:

[Windows::UI::Xaml::Data::BindableAttribute]
public ref class BookReview sealed {
public:
  BookReview(Platform::String^ name, Platform::String^ content,
    int rating);

  property Platform::String^ Name { Platform::String^ get(); }
  property Platform::String^ Content { Platform::String^ get(); }
  property int Rating {
    int get() { return _review.rating(); }
    void set(int rating) { _review.set_rating(rating); }
  }
private:
  book_review _review;
};

需要注意的几点:

  • Bindable属性被应用到类上,以便为数据绑定生成适当的代码。

  • 所有公共内容都是 WinRT 专用的。book_review包装实例位于类的私有部分。任何尝试将其公开都将导致编译错误。错误说明,"非值类型不能有任何公共数据成员";这是第一个问题—因为 WinRT 基于 COM,而 COM 基于接口,接口由虚表定义,它们只能包含方法(函数),而不能包含数据成员。

如果数据成员转换为返回非 WinRT 类型的方法,编译器将发出不同的错误,"(MethodName):公共成员的签名包含本机类型'book_review'"。最终结果是只有 WinRT 类型可以在公共成员中使用。

  • 标准 C++没有属性的概念。数据成员有时会被 getter 和/或 setter 包装。这些应该被转换为 WinRT 属性,就像在前面的代码中对NameContentRating所做的那样。

WinRT 编码约定是对类和成员名称使用帕斯卡命名法,因此这些可能需要稍微更改以反映这一点(例如,在book_review中的name被更改为BookReview中的Name,依此类推)。

  • BookReview 类中缺少的一件事是实现INotifyPropertyChanged,如第五章数据绑定中所述。这是因为Rating属性可以在构造BookReview之后更改。该实现被省略,以便更轻松地专注于基本知识,但在实际情况下应该实现。

头文件没有实现构造函数和属性NameContent。这是构造函数(在相应的 CPP 文件中实现):

BookReview::BookReview(String^ name, 
String^ content, int rating) : 
  _review(name->Data(), content->Data(), rating) { }

构造函数(就像任何其他方法一样)必须接受 WinRT 类型,对于任何需要的字符串都要使用Platform::String^。这用于初始化包装的book_review实例(它需要一个标准的std::wstring)通过使用Data方法。

NameContent属性是只读的,但必须返回 WinRT 类型—在这种情况下是Platform::String^(你可能还记得它包装了 WinRT 的HSTRING):

String^ BookReview::Name::get() {
  return ref new String(_review.name().c_str());
}

String^ BookReview::Content::get() {
  return ref new String(_review.content().c_str());
}

实现很简单,这次是通过使用接受const wchar_t*Platform::String构造函数来进行的。

接下来,我们需要看一下为book类创建的包装器。这有点复杂,因为一本书持有book_review对象的std::vectorstd::vector不是 WinRT 类型,因此必须使用另一种类型来投影,表示一个集合:

[Windows::UI::Xaml::Data::BindableAttribute]
public ref class Book sealed {
public:
  Book(Platform::String^ name, Platform::String^ author);
  void AddReview(BookReview^ review);

  property Platform::String^ Name {
    Platform::String^ get() { 
      return ref new Platform::String(_book.name().c_str()); 
    }
  }

  property Platform::String^ Author {
    Platform::String^ get() { 
      return ref new Platform::String(_book.author().c_str()); 
    }
  }

  property Windows::Foundation::Collections::
    IVectorView<BookReview^>^ Reviews {
      Windows::Foundation::Collections::
      IVectorView<BookReview^>^ get();
    }

private:
  book _book;
  Windows::Foundation::Collections::
    IVectorView<BookReview^>^ _reviews;
};

NameAuthor属性很简单,并且是内联实现的。构造函数初始化这些属性,并且它们在对象的整个生命周期内保持为只读。

原始的book类有一个std::vector<book_review>实例。在 WinRT 中,诸如 vector 之类的集合应该被投影为Windows::Foundation::Collections::IVector<BookReview>IVectorView<BookReview>(在相同的命名空间中,后者是前者的只读视图)。

注意

命名空间前缀可能有点令人困惑。为什么IVector<T>Windows::Foundation::Collections中,而Vector<T>Platform::Collections中?规则很简单。WinRT 类型放在Windows::*命名空间中,而特定的 C++实现放在Platform::*命名空间中。一般来说,Platform::*类型不能导出为 WinRT 类型,因为它们是 WinRT 接口的 C++特定实现(大多数情况下)。值得注意的例外是Platform::StringPlatform::Object,它们被理解为HSTRINGIInspectable指针的替代品,因此在公共方法和属性中使用。

Book类提供了Reviews只读属性作为IVectorView<BookReview^>^。它可以返回任何实现此接口的对象。Platform::Collections::Vector<T>提供了IVector<T>的实现。IVector<T>提供了GetView方法,返回IVectorView<T>

IVectorView<BookReview^>^ Book::Reviews::get() {
  if(_reviews == nullptr) {
    auto reviews = ref new Vector<BookReview^>();
    for(size_t i = 0; i < _book.reviews_count(); i++) {
      auto review = _book.get_review(i);
      reviews->Append(
        ref new BookReview(
          ref new String(review.name().c_str()), 
          ref new String(review.content().c_str()), 
      review.rating()));
    }
    _reviews = reviews->GetView();
  }
  return _reviews;
}

属性实现尝试通过缓存IVectorView<BookReview>的结果来优化,如果没有添加新评论,或者从未调用属性(在_reviews中表示为nullptr)。否则,将创建Vector<BookReview>,并使用IVector<BookReview>::Append添加BookReview对象。

要实现的最后一个有趣的方法是AddReview

void Book::AddReview(BookReview^ review) {
  book_review br(review->Name->Data(), 
  review->Content->Data(), review->Rating);
  _book.add_review(br);
  _reviews = nullptr;
}

_reviews数据成员设置为nullptr,以强制将来调用Reviews属性时重新生成返回的集合。

提示

在处理诸如std::vector及其 WinRT 包装器(如Vector<T>)之类的集合时,尽量使用std::vector。仅在从 WinRT 类导出时使用Vector<T>。对本机 C++类型进行所有集合操作,因为它们的开销比 WinRT 类型小(因为基于 WinRT 接口的性质)。

跨 ABI

应用程序二进制接口ABI)是标准 C++和 WinRT 之间的边界。任何未实现为 WinRT 类的 C++类都不能跨越 ABI。先前使用的类型std::wstringstd::vector<>是需要在跨越 ABI 时进行投影的完美示例。编译器不允许在public ref class声明的公共部分中使用非 WinRT 类型。有关将本机 C++类型映射到 WinRT 类型的进一步讨论,请参见第二章,“Windows 8 商店应用的 COM 和 C++”。

使用 Windows Runtime 组件

构建 Windows Runtime 组件后,将创建一个指示从库中导出的类型、接口、枚举等的元数据文件(.winmd)。例如,我们的BookLibrary组件 DLL 会生成BookLibrary.winmd。在ILDASM中打开它会显示如下:

使用 Windows Runtime 组件

这清楚地显示了导出的类型,BookBookReview。奇怪的接口名称代表编译器提供的内部 WinRT 实现——WinRT 都是关于接口的。如果存在任何非默认构造函数,则存在*Factory接口。例如,打开__IBookFactory显示如下:

使用 Windows Runtime 组件

注意CreateInstance方法,该方法是根据Book的单个构造函数建模的。这个接口是由创建Book实例的激活工厂实现的(由 C++/CX 在任何public ref class的后台实现)。

__IBookPublicNonVirtuals接口是由Book类实现的接口:

使用 Windows Runtime 组件

可以从任何符合 WinRT 的环境中使用生成的 DLL。在 C++项目中,需要添加对winmd文件的引用。为此,请在“解决方案资源管理器”中右键单击项目节点,然后选择引用…。然后在常规属性框架和引用节点中选择添加新引用(或者从项目属性中进入相同位置):

使用 Windows Runtime 组件

引用添加后(通过选择BookLibrary项目,或在一般情况下浏览winmd文件),所有导出类型都可以立即使用,就像任何其他 WinRT 类型一样。以下是创建带有一些评论的Book的示例:

using namespace BookLibrary;

auto book = ref new Book("Windows Internals", "Mark Russinovich");
book->AddReview(
    ref new BookReview("John Doe", 
    "Great book! Lots of pages!", 4));
book->AddReview(
    ref new BookReview("Mickey Mouse", 
      "Why two parts? This makes my ears spin!", 3));
book->AddReview(
    ref new BookReview("Clark Kent", 
    "Big book. Finally something to stretch the muscles!", 5));

从其他环境(如.NET)使用BookLibrary DLL 可以通过类似的方式完成,如第二章,“Windows 8 商店应用的 COM 和 C++”中所示。每个环境都执行所需的投影,都基于元数据(winmd)文件。

注意

使用 C++创建的 WinRT 组件是唯一保证不涉及.NET CLR 的组件。由 C#创建的组件始终需要 CLR,即使从 C++客户端使用也是如此。

其他 C++库项目

在 Visual Studio 2012 中提供的可用项目类型中,还有两个选项用于创建可重用库:

其他 C++库项目

概述的项目创建了一个经典的 DLL 或静态库,但默认情况下不会生成winmd文件。这些组件只能被其他 C++ Store 项目(WinRT 组件或其他支持商店的库)使用。与常规的经典 C++ DLL 或静态库相比,有什么区别?首先,任何使用被禁止的 Win32 API 都会导致编译器错误。其次,除非执行特定步骤(例如添加对platform.winmdwindows.winmd文件的引用),否则这些项目不能使用 C++/CX。

自定义控件模板

在第四章中,我们讨论了 WinRT 提供的各种元素和控件。可以使用以下级别(从简单到复杂)来自定义元素和控件的外观。当然,并非所有元素/控件都支持所有级别:

  • 更改属性值;到目前为止,最简单的自定义是通过更改属性来实现的。常见的例子是与字体相关的属性(FontSizeFontFamily等),ForegroundBackground,以及许多其他属性。

  • 对于内容控件(派生自ContentControl),Content属性可以设置为任何所需的元素。例如,这可以使Button显示图像、文本和其他任何所需的内容,同时仍保持预期的按钮行为。

  • 数据模板可以用于支持它的属性,以丰富和有意义的方式显示数据对象。ContentControl::Content支持此功能,因为它的类型为Platform::Object^,这意味着它可以接受任何东西。如果这是一个不是UIElement的派生类型,则如果提供了DataTemplate,则会使用它(在这种情况下,通过ContentControl::ContentTemplate属性)。这也适用于所有ItemsControl派生类,通过ItemTemplate属性。

  • ItemsControl派生的类型具有ItemContainerStyleItemsPanel属性,可以进一步自定义数据的呈现方式。

尽管前面的列表令人印象深刻,但有时这些自定义还不够。例如,Button始终是矩形的;尽管它可以包含任何东西(它是一个ContentControl),但它永远不可能是椭圆形的。有些东西就是“固定”在控件的外观中。这就是控件模板发挥作用的地方。

元素和控件之间的根本区别在于Control::Template属性的存在,它定义了控件的外观方式。元素没有这个属性。例如,椭圆是一个椭圆,它不能看起来像其他任何东西,因为那将违反它的定义。因此,椭圆是一个元素而不是一个控件。

控件(派生自Control)可以更改它们的Template属性并具有不同的外观(但保留功能)。实际上,所有控件都有 WinRT 提供的默认模板(否则,控件将没有“外观”)。

构建控件模板

控件模板的类型是ControlTemplate。它与DataTemplate非常相似(两者都派生自FrameworkTemplate),可以包含一个UIElement(通常是一个Panel),构成控件的外观。

例如,我们将为ProgressBar控件构建一个替代的控件模板。我们将从简单的步骤开始,然后逐步添加功能。

控件模板通常被创建为资源,这样可以更容易地重用。这是一个简单的尝试:

<ControlTemplate TargetType="ProgressBar" x:Key="progTemp1">
  <Grid>
    <Rectangle Fill="DarkBlue" />
    <Rectangle RadiusX="10" RadiusY="4" HorizontalAlignment="Left" 
    Fill="Yellow" Margin="2" />
  </Grid>
</ControlTemplate>

要使用模板,我们只需将其设置为Template属性:

<ProgressBar Value="30" Height="40" Margin="10" 
  Template="{StaticResource progTemp1}" />

这里的想法是在一个深蓝色的矩形上面创建另一个矩形(带有圆角),用来显示当前的进度。然而,结果并不理想(顶部的ProgressBar正在使用默认模板):

构建控件模板

ProgressBar似乎没有显示任何进度(Value="30"应该显示 30%填充的ProgressBar,因为默认的Maximum100,就像顶部的ProgressBar一样)。为什么会这样呢?我们只是创建了一个Rectangle,它的默认Width0。解决这个问题的一种方法是将第二个RectangleWidth属性绑定到ProgressBarValue属性。以下是一种方法:

Width="{TemplateBinding Value}"

TemplateBinding是一个绑定到正在被模板化的控件的标记扩展。这是必要的,因为我们不能使用SourceElementName与常规的Binding。以下是结果:

构建控件模板

这当然更好,但与顶部的参考ProgressBar相比,进度指示器似乎很小。原因很简单,Value被视为Width,但实际上应该与整个ProgressBar的宽度成比例。我们可以通过使用值转换器来解决这个问题,但有一个更好的方法。

ProgressBar已经具备智能功能,可以将某些元素的Width属性设置为所需的比例值。我们只需要告诉它应该是哪个元素。事实证明,这个元素必须有一个特定的名称,在这种情况下是ProgressBarIndicator。我们只需要在相关元素上将x:Name属性设置为这个值,即我们的第二个Rectangle

<Rectangle RadiusX="10" RadiusY="4" x:Name="ProgressBarIndicator" 
  HorizontalAlignment="Left" Fill="Yellow" Margin="2" />

以下是结果:

构建控件模板

现在看起来完全正确。这个特殊的名称是从哪里来的?秘密在于查看控件的默认模板,寻找特别命名的部分。所有默认控件模板都可以在文件C:\Program Files (x86)\Windows Kits\8.0\Include\WinRT\Xaml\Design\Generic.xaml中找到(在 32 位 Windows 系统上,目录以C:\Program Files开头)。控件模板是控件的默认样式的一部分。

查看ProgressBar控件模板,大多数元素都以无趣的名称命名,例如e1e2等等——ProgressBarIndicator脱颖而出。

注意

在 WPF 和 Silverlight 中,放置在控件上的TemplatePart属性指示控件查找的命名部分以及它们的类型应该是什么。尽管 WinRT 定义了TemplatePart属性,但在当前版本的 WinRT 中似乎没有使用,所以我们不得不做一些“猜测”。

使用控件的属性

模板现在正常运行(或看起来是这样)。更改属性,例如ForegroundBackground,在使用我们的新模板时没有任何效果。这是因为模板没有以任何方式使用它们。有时,这就是我们想要的,但典型的模板希望提供自定义外观的方法;一种方法是利用控件上的现有属性。这已经在TemplateBindingValue属性中简要演示过,但这里有一个更有趣的模板,它使用了ProgressBar的几个属性:

<ControlTemplate TargetType="ProgressBar" x:Key="progTemp2">
  <Grid>
    <Rectangle Fill="{TemplateBinding Background}" />
    <Rectangle RadiusX="10" RadiusY="4" 
    x:Name="ProgressBarIndicator" 
    HorizontalAlignment="Left" Fill=
    "{TemplateBinding Foreground}" 
    Margin="2"/>
    <TextBlock HorizontalAlignment="Center" Foreground="White" 
      VerticalAlignment="Center" >
      <Run Text="{Binding Value, RelativeSource=
      {RelativeSource TemplatedParent}}" />
      <Span>%</Span>
    </TextBlock>
  </Grid>
</ControlTemplate>

在前面的代码片段中,有几件有趣的事情需要注意。TemplateBinding标记扩展用于绑定到模板控件的属性(BackgroundForeground);TemplateBinding仅适用于单向绑定(源到目标,但反之则不行)。对于双向绑定属性,必须使用更长的语法,即Binding表达式,其中RelativeSource属性设置为另一个标记扩展,名为RelativeSource(不应与Binding::RelativeSource属性名称混淆),它接受Mode(也作为构造函数参数),可以是Self(目标和源是相同的对象,在这里没有用)或TemplatedParent,这意味着正在被模板化的控件,这正是我们想要的。

注意

使用控件的属性

实际上很难捕捉到静态图像,因为ProgressBar显示了由小圆圈组成的有趣的非线性动画。

<ProgressBar Value="30" Height="40" Margin="10" FontSize="20" 
  Template="{StaticResource progTemp2}"  
  Background="Brown">
  <ProgressBar.Foreground>
    <LinearGradientBrush EndPoint="0,1">
      <GradientStop Offset="0" Color="DarkBlue" />
      <GradientStop Offset="1" Color="LightBlue" />
    </LinearGradientBrush>
  </ProgressBar.Foreground>
</ProgressBar>

注意

这是结果:

TemplateBinding在这里也应该起作用,因为我们只对单向绑定感兴趣。但是,由于Value可以进行双向绑定,TemplateBinding失败了。这似乎是当前 WinRT 实现中的一个错误。

ProgressBar通常显示操作的进度。然而,有时应用程序并不知道操作的进度——它只知道正在进行中。ProgressBar可以通过将其IsIndeterminate属性设置为true来指示这一点。以下是标准ProgressBar在此模式下的外观:

处理状态变化

处理状态变化

IsIndeterminate设置为trueProgressBar对使用我们的模板显示ProgressBar的方式没有影响。这是因为我们的控件没有考虑到这个属性。

如何解决这个问题?一种方法是向控件模板添加一些默认情况下隐藏的内容,但如果IsIndeterminate变为true,则会显示出来,并指示ProgressBar处于特殊模式(例如使用值转换器)。尽管从技术上讲这是可能的,但这通常不是通常的做法。其中一个原因是,有些状态变化只通过绑定和值转换器可能很难监控——例如,如果鼠标光标悬停在控件上(对于ProgressBar来说不相关,但对于许多其他控件来说是相关的),一个属性可能不足够。那么我们如何开始动画呢?

所有这些状态变化和反应都是通过一个辅助对象VisualStateManager处理的。控件在各个状态之间转换;这些状态及其转换可以被VisualStateManager捕获。对于每个变化,可以提供一组Storyboard对象;这些Storyboard对象代表一般情况下的动画,或者特定情况下的简单状态变化。

以下是处理IsIndeterminate属性效果的扩展模板:

<ControlTemplate TargetType="ProgressBar" x:Key="progTemp4">
  <Grid>
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup x:Name="CommonStates">
        <VisualState x:Name="Indeterminate">
          <Storyboard>
            <DoubleAnimation Storyboard.TargetProperty="Opacity" 
            Storyboard.TargetName="IndetRect" To="1" 
              Duration="0:0:1" 
            AutoReverse="True" RepeatBehavior="Forever"/>
          </Storyboard>
        </VisualState>
        <VisualState x:Name="Determinate">
        </VisualState>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <Rectangle Fill="{TemplateBinding Background}" />
      <Rectangle RadiusX="10" RadiusY="4" 
      x:Name="ProgressBarIndicator" HorizontalAlignment="Left" 
      Fill="{TemplateBinding Foreground}" Margin="2"/>
      <Rectangle x:Name="IndetRect" Opacity="0">
      <Rectangle.Fill>
        <LinearGradientBrush EndPoint=
        ".1,.3" SpreadMethod="Repeat">
          <GradientStop Offset="0" Color="Yellow" />
          <GradientStop Offset="1" Color="Red" />
        </LinearGradientBrush>
      </Rectangle.Fill>
    </Rectangle>
  </Grid>
</ControlTemplate>

透明度正在进行动画,淡入淡出这个矩形。

每个VisualStateGroupVisualState对象组成,指示每个状态要执行的操作(要运行哪些动画)。状态名称必须是正确的名称,因为控件根据其内部逻辑转换到这些状态。我们如何知道存在哪些状态组以及每个组中有哪些状态?这是通过查看默认控件模板来完成的。可以通过查看前面提到的文件来完成这项工作,也可以通过在 Visual Studio 2012 中右键单击控件,然后选择编辑模板 | 编辑副本...来实现:

处理状态变化

在控件模板中,创建了一个名为IndetRect的第三个矩形,其初始不透明度为零,使其不可见。当ProgressBar进入Indeterminate状态时,将使用DoubleAnimation类(对double类型的属性进行动画处理)执行动画,将该矩形的不透明度在一秒钟内更改为1(完全显示),并具有自动反转(AutoReverse="true")和永久动画(RepeatBehavior="Forever")。这是结果:

这是一个使用此模板的“ProgressBar”:

处理状态变化

VisualStateManager有一个有趣的属性,即一个附加属性VisualStateGroups。对于每个组,始终有一个状态处于活动状态;这意味着控件可以同时处于多个状态。例如,按钮可以处于按下状态和键盘焦点状态。VisualStateGroups属性必须设置在包含控件模板的顶层“Panel”上(在我们的情况下是一个“Grid”)。

完全覆盖动画超出了本书的范围,但这应该让你对它有所了解。Storyboard表示一个时间线,在其中播放动画对象,本例中是一个DoubleAnimation对象,但还有许多其他对象。

状态实际上是如何改变的?控件通过其自己的逻辑调用静态的VisualStateManager::GoToState方法,设置特定组内的新状态。对于控件模板的作者来说,这并不重要;唯一重要的是根据预期的状态更改设置所需的动画。

注意

VisualStateManager还允许指定状态更改发生时要进行的实际过渡。这与实际状态本身相反。这意味着当移动到特定状态时,过渡可以是临时的,但状态本身可能具有不同的动画。有关更多信息,请参阅 MSDN 文档,从VisualStateGroup::Transitions属性和VisualTransition类开始。

使用附加属性进行自定义

到目前为止创建的ProgressBar模板使用TemplateBinding标记扩展或使用BindingRelativeSource标记扩展指定Source,并将TemplatedParent作为其Mode设置在ProgressBar本身上设置的属性。那么如何添加仅对我们的模板有意义的属性呢?例如,在前面的模板定义中,ProgressBar显示其值的文本字符串。如果我们想允许模板用户隐藏文本或更改其颜色呢?

ProgressBar并没有考虑到所有这些。为什么要考虑呢?它是为一些自定义级别所需的属性而创建的;这对于默认的ProgressBar模板是可以接受的。

解决这个问题的一种方法是创建一个从ProgressBar派生的新类,并添加所需的属性。虽然这样可以解决问题(我们将在下一节讨论自定义控件),但这有点不够优雅——我们不需要ProgressBar的任何新功能,而是需要一些属性来调整其模板。

更优雅的解决方案是使用附加属性,它们在一个类上定义,但可以被任何其他类使用(尽管它必须派生自DependencyObject)。从技术上讲,我们可以在 WinRT 中寻找适当的附加属性,但最好创建一个新类来定义这些附加属性,并在ProgressBar模板中使用它们。

定义一个附加属性

附加属性是依赖属性(我们将在下一节详细讨论)通过调用静态的DependencyProperty::RegisterAttached方法注册的。这将设置一个静态字段,为所有使用它的对象管理此属性。注册附带两个静态方法,实际上在对象上设置和获取附加属性的值。这里是一个类ProgressBarProperties的声明,它定义了一个单独的附加属性ShowText

public ref class ProgressBarProperties sealed {
public:
  static bool GetShowText(DependencyObject^ obj) {
    return (bool)obj->GetValue(ShowTextProperty);
  }

  static void SetShowText(DependencyObject^ obj, bool value) {
    obj->SetValue(ShowTextProperty, value);
  }

  static property DependencyProperty^ ShowTextProperty { 
    DependencyProperty^ get() { return _showTextProperty; }
  }

private:
  static DependencyProperty^ _showTextProperty;
};

静态字段必须在 CPP 文件中初始化:

DependencyProperty^ ProgressBarProperties::_showTextProperty = 
  DependencyProperty::RegisterAttached(L"ShowText", 
  TypeName(bool::typeid), 
  TypeName(ProgressBarProperties::typeid), 
  ref new PropertyMetadata(false));

RegisterAttached方法接受属性名称,其类型(作为TypeName结构),其所有者的类型,以及可以接受属性的默认值的PropertyMetadata实例(如果未在实际对象上设置并且查询该属性)。有关PropertyMetadata的更详细解释可以在下一节找到,那里描述了依赖属性;现在,我们将专注于控件模板中的附加属性的使用。

ProgressBar模板中的TextBlock可以使用附加属性如下:

<TextBlock HorizontalAlignment="Center" Foreground="White" 
  VerticalAlignment="Center" 
 Visibility="{Binding (local:ProgressBarProperties.ShowText), 
 RelativeSource={RelativeSource TemplatedParent}, 
 Converter={StaticResource bool2vis}}">
  <Run Text="{Binding Value, RelativeSource=
    {RelativeSource TemplatedParent}}" />
  <Span>%</Span>
</TextBlock>

属性路径周围的括号是必需的,否则 XAML 解析器无法正确理解表达式,导致运行时绑定失败。所使用的转换器是将Boolean转换为Visibility枚举,就像在第五章中演示的那样,数据绑定

显然,定义和注册附加属性是简单而冗长的。一个解决方案是定义宏来自动化这些样板代码。本章的可下载源代码中有一些用于定义和注册依赖属性和附加属性的宏,这应该使得这些更容易使用(在一个名为DPHelper.h的文件中)。这是另一个附加属性的示例,使用上述宏进行定义。首先,在ProgressBarProperties类内部:

DECLARE_AP(TextForeground, Windows::UI::Xaml::Media::Brush^);

然后在实现文件中(初始化静态字段):

DEFINE_AP(TextForeground, Brush, ProgressBarProperties, nullptr);

这个属性可以在模板中的TextBlock上使用,如下所示:

Foreground="{TemplateBinding 
  local:ProgressBarProperties.TextForeground}"

自定义元素

控件模板提供了改变控件外观的强大和完整的方式。但这只是外观 - 控件仍然以相同的方式行为。如果需要新的功能,模板是不够的,需要创建一个新的类。这就是自定义元素的用武之地。

在 WinRT 中,有几种编写自定义元素的方法,我们将看一下两种最常用的控件 - 用户控件和自定义控件。然后,我们将简要讨论如何创建自定义面板和自定义形状。

用户控件

用户控件通常用于将相关元素和控件组合在一起,以便重复使用。从此控件中公开适当的属性和事件,以便轻松访问其功能。作为额外的奖励,Visual Studio 支持用户控件 UI 设计,就像对常规页面一样。

用户控件派生自UserControl类。UI 设计实际上是控件的Content属性,就像ContentControl一样。它们通常放在自己的 Windows Runtime 组件项目中,以便可以在任何 WinRT 项目中使用 C++或其他语言。

创建颜色选择器用户控件

作为用户控件的一个示例,我们将创建一个颜色选择器控件,它允许通过操作红色、绿色和蓝色三个滑块来选择纯色(RGB)。首先,在创建 Windows Runtime 组件项目后,我们可以向项目添加一个新项目类型为用户控件的项目:

创建颜色选择器用户控件

打开设计表面,并创建了通常的一对文件,ColorPicker.hColorPicker.cpp

我们想要做的第一件事是定义属性,以便轻松访问用户控件的功能。大多数情况下,这些属性不会是简单的包装某个私有字段的属性,而是依赖属性。

依赖属性

简单的属性包装了一个字段(可能在 setter 中进行了一些验证),缺少在使用 UI 框架时希望的某些功能。具体来说,WinRT 依赖属性具有以下特点:

  • 当属性的值发生变化时,进行更改通知。

  • 各种提供程序可以尝试设置属性的值,但一次只有一个这样的提供程序获胜。尽管如此,所有值都会被保留。如果获胜的提供程序消失,属性的值将设置为下一个获胜者。

  • 属性值在可视树中向下继承(对于一些预定义的属性)。

  • 如果属性的值从其默认值中未发生更改,则不会为该属性的值分配内存

这些特性为 WinRT 的一些强大功能提供了基础,例如数据绑定、样式和动画。

在表面上,这些属性看起来与任何其他属性一样 - 有一个 getter 和一个 setter。但没有涉及私有字段。相反,一个静态字段管理着所有实例使用该属性的属性值。

定义依赖属性

这是定义依赖属性的方法(必须在从DependencyObject派生的类中完成,这总是与UserControl一样的情况)。一个私有的静态字段管理属性,该属性公开为只读属性。存在一个 setter 和 getter 作为实际setget方法的简单访问,这些方法在DependencyObject基类中实现。以下代码演示了创建一个名为SelectedColorWindows::UI::Color类型的依赖属性,该属性由ColorPicker用户控件公开:

public ref class ColorPicker sealed {
public:
//…
  property Windows::UI::Color SelectedColor {
    Windows::UI::Color get() {
   	   return (Windows::UI::Color)GetValue(SelectedColorProperty); 
    }
    void set(Windows::UI::Color value) {
      SetValue(SelectedColorProperty, value); }
  }

  property DependencyProperty^ SelectedColorProperty { 
    DependencyProperty^ get() { return _selectedColorProperty; }
  }

private:
  static DependencyProperty^ _selectedColorProperty;
};

需要注意的几件事:

  • GetValueSetValue属性是从DependencyObject继承的。

  • 静态属性的名称应该以Property结尾。

  • get()set()部分添加更多代码从来都不是一个好主意,因为有时这些部分不会被使用,可以直接调用GetValueSetValue方法;例如,XAML 解析器就是这样做的。

缺失的部分是静态字段的初始化,通常在.cpp文件中完成:

DependencyProperty^ ColorPicker::_selectedColorProperty = 
  DependencyProperty::Register(
  "SelectedColor", TypeName(Color::typeid), 
  TypeName(ColorPicker::typeid),
  ref new PropertyMetadata(Colors::Black, 
  ref new PropertyChangedCallback(
  &ColorPicker::OnSelectedColorChanged)));

通过调用静态的DependencyProperty::Register方法注册依赖属性DP),传递属性名称、其类型(作为TypeName结构)、包含类型和PropertyMetadata对象,该对象可以接受属性的默认值(在本例中为Colors::Black)和在属性更改时调用的可选回调。这将在ColorPicker的情况下很有用。

这段代码可以重复多次,每个 DP 都要重复一次。这显然需要一些辅助宏。以下是使用宏在ColorPicker上定义的另外三个属性。首先,在头文件中:

DECLARE_DP(Red, int);
DECLARE_DP(Green, int);
DECLARE_DP(Blue, int);

以及.cpp文件:

DEFINE_DP_EX(Red, int, ColorPicker, 0, OnRGBChanged);
DEFINE_DP_EX(Green, int, ColorPicker, 0, OnRGBChanged);
DEFINE_DP_EX(Blue, int, ColorPicker, 0, OnRGBChanged);

这比冗长的版本要短得多(也更少出错)。这些宏可以在DPHelper.h文件中找到,该文件可在本章的可下载源代码中找到。

接下来要做的是实现更改通知方法(如果存在的话)。在这种情况下,RedGreenBlue应该反映SelectedColor属性的颜色组件,反之亦然。首先,如果RedGreenBlue发生变化,使用以下代码片段:

void ColorPicker::OnRGBChanged(DependencyObject^ obj, 
  DependencyPropertyChangedEventArgs^ e) {
  ((ColorPicker^)obj)->OnRGBChangedInternal(e);
}

void ColorPicker::OnRGBChangedInternal(
  DependencyPropertyChangedEventArgs^ e) {
  auto color = SelectedColor;
  auto value = safe_cast<int>(e->NewValue);
  if(e->Property == RedProperty)
    color.R = value;
  else if(e->Property == GreenProperty)
    color.G = value;
  else
    color.B = value;
  SelectedColor = color;
}

由于注册的处理程序必须是静态的,将实际工作委托给实例方法(在前面的代码中为OnRGBChangedInternal)更容易。该代码根据更改的 RGB 属性更新SelectedColor属性。

另一个方向的实现也是类似的:

void ColorPicker::OnSelectedColorChanged(DependencyObject^ obj, 
DependencyPropertyChangedEventArgs^ e) {
  ((ColorPicker^)obj)->OnSelectedColorChangedInternal(
  safe_cast<Color>(e->NewValue));
}

void ColorPicker::OnSelectedColorChangedInternal(Color newColor) {
  Red = newColor.R;
  Green = newColor.G;
  Blue = newColor.B;
}

注意

前面的代码片段似乎会创建一个无限循环 - 如果Red改变,SelectedColor改变,这又会改变Red,依此类推。幸运的是,依赖属性机制会自动处理这个问题,如果属性值实际上发生变化,它会调用回调;设置为相同的值不会调用回调。

构建 UI

下一步是使用常规 XAML 创建用户控件的实际 UI。可以使用绑定表达式绑定到控件公开的属性(因为这些是 DP,它们为绑定提供自动更改通知)。以下是ColorPicker的 UI,滑块绑定到RedGreenBlue属性,以及一个Rectangle绑定到控件的SelectedColor属性(默认 XAML 命名空间被省略):

<UserControl
  x:Class="UserControlLibrary.ColorPicker"
  x:Name="uc">
  <UserControl.Resources>
  </UserControl.Resources>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition />
      <ColumnDefinition Width="150" />
    </Grid.ColumnDefinitions>
    <Slider Maximum="255" Margin="4" TickFrequency="20" 
      Value="{Binding Red, ElementName=uc, Mode=TwoWay}"/>
      <Slider Maximum="255" Margin="4" TickFrequency="20" 
      Value="{Binding Green, ElementName=uc, Mode=TwoWay}" 
      Grid.Row="1"/>
    <Slider Maximum="255" Margin="4" TickFrequency="20" 
      Value="{Binding Blue, ElementName=uc, Mode=TwoWay}" 
      Grid.Row="2"/>
    <Rectangle Grid.Column="1" Grid.RowSpan="3" Margin="10" 
      Stroke="Black" StrokeThickness="1">
      <Rectangle.Fill>
        <SolidColorBrush Color="{Binding SelectedColor, 
        ElementName=uc}" />
      </Rectangle.Fill>
    </Rectangle>
  </Grid>
</UserControl>

添加事件

可以向用户控件添加事件,以通知感兴趣的方。以下是在控件的头文件中添加的一个事件:

event EventHandler<Windows::UI::Color>^ SelectedColorChanged;

该事件使用EventHandler<T>委托,该委托要求客户端提供一个接受Platform::Object^T(在本例中为Color)的方法。当SelectedColor属性改变时,我们将触发该事件:

void ColorPicker::OnSelectedColorChangedInternal(Color newColor) {
  Red = newColor.R;
  Green = newColor.G;
  Blue = newColor.B;

 SelectedColorChanged(this, newColor);
}

使用 ColorPicker

现在我们可以通过通常的方式在另一个项目中使用ColorPicker,并添加 XML 命名空间映射。然后就像使用其他控件一样使用该控件。以下是一个例子:

<StackPanel VerticalAlignment="Center">
  <Border Margin="10" Padding="6" Width="500" BorderBrush="White" 
    BorderThickness="2" >
  <controls:ColorPicker SelectedColorChanged="OnColorChanged" />
  </Border>
  <TextBlock FontSize="30" HorizontalAlignment="Center">
    <Span>Color: #</Span>
    <Run x:Name="_color" />
  </TextBlock>
</StackPanel>

控件放置在边框内,其SelectedColorChanged事件处理如下:

void MainPage::OnColorChanged(Object^ sender, Color color) {
  wstringstream ss;
  ss.fill(L'0');
  ss << hex << uppercase << setw(2) << color.R << setw(2) << 
  color.G << setw(2) << color.B;
  _color->Text = ref new String(ss.str().c_str());
}

这改变了控件底部的TextBlock。这是运行时的样子:

使用 ColorPicker

自定义控件

用户控件非常适合封装可以轻松重用的 UI 功能。它们的潜在缺点是缺乏深度定制。假设在ColorPicker示例中,我们希望将滑块垂直放置而不是水平放置,或者我们想要一个椭圆而不是一个矩形。虽然可以添加一些允许一些定制的属性,但我们无法预料到一切。

解决方案是创建一个具有默认控件模板的自定义控件,可以根据需要完全更改,同时保留原始功能。这正是常规 WinRT 控件的构建方式。

创建一个 ColorPicker 自定义控件

自定义(也称为模板化)控件派生自Control类。一个很好的起点是 Visual Studio 模板化控件模板:

创建 ColorPicker 自定义控件

结果是一对文件,ColorPicker.hColorPicker.cpp,以及一个名为Generic.xaml的 XAML 文件,其中包含了ColorPicker的默认样式,包括默认模板,如下所示:

<Style TargetType="local:ColorPicker">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="local:ColorPicker">
        <Border
          Background="{TemplateBinding Background}"
          BorderBrush="{TemplateBinding BorderBrush}"
          BorderThickness="{TemplateBinding BorderThickness}">
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

注意

所有自定义控件样式必须驻留在同一个Generic.xaml文件中。它的名称和来源在 WPF 中,支持不同的样式适用于不同的 Windows UI 主题。这与 WinRT 无关,但惯例仍然存在。

实际上,当编写多个自定义控件时,使用同一个文件最不方便。可以通过使用ResourceDictionary::MergedDictionaries属性将其他 XAML 文件包含到Generic.xaml中来解决这个问题。

默认模板看起来与为用户控件创建的默认 UI 非常相似,但有一个重要的区别;没有数据绑定表达式。原因是,如果有绑定,自定义模板将不得不复制这些绑定以保持功能,这对自定义模板作者来说是一个不合理的负担;另一种选择是在代码中进行绑定。以下是ColorPicker的默认模板的修订标记:

<ControlTemplate TargetType="local:ColorPicker">
  <Border
    Background="{TemplateBinding Background}"
    BorderBrush="{TemplateBinding BorderBrush}"
    BorderThickness="{TemplateBinding BorderThickness}">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition Width="150" />
      </Grid.ColumnDefinitions>
      <Slider Maximum="255" Margin="4" TickFrequency="20" 
      x:Name="PART_Red"/>
      <Slider Maximum="255" Margin="4" TickFrequency="20" 
      x:Name="PART_Green" Grid.Row="1"/>
      <Slider Maximum="255" Margin="4" TickFrequency="20" 
      x:Name="PART_Blue" Grid.Row="2"/>
      <Rectangle Grid.Column="1" Grid.RowSpan="3" Margin="10" 
      Stroke="Black" StrokeThickness="1">
        <Rectangle.Fill>
          <SolidColorBrush x:Name="PART_Color" />
        </Rectangle.Fill>
      </Rectangle>
    </Grid>
  </Border>
</ControlTemplate>

模板的有趣部分被分配了名称。这些名称将被控件查找并在代码中绑定。这些是本章开头讨论的命名部分。

在代码中进行绑定

在自定义控件中定义依赖属性和事件与用户控件完全相同。

当模板应用于控件时,将调用虚拟的Control::OnApplyTemplate方法。这是控件寻找其命名部分并使用绑定或事件处理程序连接到它们的最佳机会。

为了绑定这三个滑块,创建了一个辅助方法,如下所示:

void ColorPicker::BindSlider(String^ name, String^ propertyName) {
  auto slider = (RangeBase^)GetTemplateChild(name);
  if(slider != nullptr) {
    auto binding = ref new Binding;
    binding->Source = this;
    binding->Path = ref new PropertyPath(propertyName);
    binding->Mode = BindingMode::TwoWay;
    BindingOperations::SetBinding(slider, 
    RangeBase::ValueProperty, binding);
  }
}

该方法使用GetTemplateChild()来获取命名元素。如果该元素不存在,则返回nullptr。一个典型的控件简单地继续执行,不会抛出异常。

注意

请注意,代码将RangeBase转换而不是Slider。这是可能的,因为所需的属性是在RangeBase上定义的Value。这意味着这可以是除Slider之外的其他东西,只要它是从RangeBase派生的(例如,ScrollBarProgressBar)。

接下来,在代码中创建一个绑定,通过实例化一个Binding对象,设置源对象(SourcePath属性),绑定模式(Mode属性)和转换器(如果需要,使用Converter属性),最后调用BindingOperations::SetBinding与目标对象,目标 DP 和绑定实例。

完整的OnApplyTemplate如下:

void ColorPicker::OnApplyTemplate() {
  BindSlider("PART_Red", "Red");
  BindSlider("PART_Green", "Green");
  BindSlider("PART_Blue", "Blue");
  auto color = (SolidColorBrush^)GetTemplateChild("PART_Color");
  if(color != nullptr) {
    auto binding = ref new Binding;
    binding->Source = this;
    binding->Path = ref new PropertyPath(L"SelectedColor");
    BindingOperations::SetBinding(color, 
    SolidColorBrush::ColorProperty, binding);
  }
}

三个可能的滑块(实际上是从RangeBase派生的控件)被绑定,然后如果存在SolidColorBrush,则被绑定。这意味着它可以是RectangleFillEllipseFill,或者BorderBorderBrush——只要它是SolidColorBrush

使用自定义控件与使用用户控件相同。但是,可以替换控件模板(就像本章开头对ProgressBar所做的那样),以创建一个外观不同但具有相同功能的ColorPicker,而且完全不需要代码——只需 XAML。

自定义面板

WinRT 提供了从Panel派生的标准面板。可以创建新的面板,以独特的方式排列其子元素,例如径向面板,其子元素沿椭圆的周长排列。

布局过程是一个两步过程——测量和排列。这正是Panel的方法精确地建模了这两个方法的目的,MeasureOverrideArrangeOverride

MeasureOverride询问面板(或任何覆盖它的元素)需要多大的尺寸。对于面板来说,主要关注的是其子元素的要求。面板应该为每个子元素调用UIElement::Measure,导致其自己的MeasureOverride被调用(如果该子元素是一个面板,或者像一个面板一样行事)。

面板需要根据其子元素的要求和其想要使用的布局逻辑来决定所需的大小。发送到MeasureOverride的参数是该面板容器提供的可用大小。这可以是一个或两个维度的无限大小(例如,ScrollViewer指示在可滚动的方向上有无限空间)。重要的是返回有限大小;否则 WinRT 无法知道为面板留多少空间,并抛出异常。

ArrangeOverride是一个更有趣的方法,它实际上实现了特殊的布局逻辑,为此面板被创建。面板对每个元素调用UIElement::Arrange,强制该元素放置在特定的矩形内。

注意

这个过程几乎与在 WPF 或 Silverlight 中完成的方式完全相同;网上有许多这样的例子,可以很容易地转换为 WinRT。

自定义绘制元素

可以通过从Windows::UI::Xaml::Path类派生它们来在 WinRT 中创建自定义绘制元素,这是一种ShapePath基于Geometry——2D 布局的数学抽象,可以是PathGeometry,它本身可以由各种PathSegment对象构建。这些形状超出了本书的范围,但是它们与 Silverlight 中存在的形状相似,因此有很多关于它们的信息可用。

注意

WinRT 目前不支持 WPF 的OnRender方法,该方法使用DrawingContext进行各种自由风格的绘制。希望这将在将来的版本中得到支持。

许多新控件作为 WinRT XAML 工具包的一部分存在,可以免费在微软的 CodePlex 网站上获得winrtxamltoolkit.codeplex.com/。工具包的问题在于它是作为.NET 类库编写的,因此只能被.NET 项目使用。

总结

组件是模块重用的支柱。真正的 WinRT 组件只使用 WinRT 类型,因此可以导出到任何兼容 WinRT 的环境,如 C++/CX、.NET 和 JavaScript。

控件模板提供了可以仅使用 XAML 完成的最终控件定制机制,几乎不需要代码(如果使用值转换器,则可能需要代码)。如果控件的外观需要更改,但其功能应保持完整,并且是所需的,那么模板是合适的。

自定义和用户控件用于在没有任何内置控件提供的情况下需要新功能时使用。通过从UserControlControl派生,可以添加依赖属性和事件以创建新的可重用控件。

用户控件和自定义控件应该打包在这样的 WinRT 组件中,以便 C++和其他项目轻松重用。

在下一章中,我们将介绍一些 Windows Store 应用程序的特殊功能,例如动态磁贴和推送通知。这些(以及其他)功能可以使您的商店应用程序独特而吸引人。

第七章:应用程序、磁贴、任务和通知

Windows 商店应用程序在许多方面与传统桌面应用程序不同。商店应用程序位于一个安全容器中,与外部世界的交互方式是明确定义的,如其他应用程序、操作系统或网络上的内容。这些应用程序还受到多项限制,与桌面应用程序世界中的任何限制都不同。了解这些限制,并通过与 Windows 的合作来处理它们的方式,是成功和行为良好的 Windows 商店应用程序的关键。

我们将首先研究商店应用程序的执行模型,以及它与经典桌面应用程序的不同之处。然后我们将看一些商店应用程序的独特特性,如动态磁贴和其他通知机制。最后,我们将探讨应用程序即使不是当前运行的应用程序也可以执行工作的方式,通过使用各种形式的后台任务。

应用程序生命周期

商店应用程序由 Windows 操作系统管理,开发应用程序时需要考虑严格的规则:

  • 一次只能有一个应用程序处于前台(一个显著的例外是“快照视图”:一个应用程序占据大部分屏幕,而另一个占据 320 像素的宽度;这在第九章中讨论,打包和 Windows 商店)。

  • 其他应用程序会被 Windows 自动挂起,意味着它们不会获得 CPU 时间;但它们占用的内存会被保留。

  • 如果 Windows 检测到内存不足,它可能会终止第一个挂起的应用程序;如果内存仍然紧张,它将终止第二个挂起的应用程序,依此类推。

这些规则旨在确保前台应用程序完全访问 CPU 和其他资源,同时尽可能节省电池电量。完整的应用程序生命周期可以用以下状态图表示:

应用程序生命周期

一开始,应用程序不在运行状态。然后用户启动应用程序,通常是通过在开始屏幕上点击或轻触其磁贴。这会导致调用Application::OnLaunched虚拟方法;这是应用程序应该初始化并呈现主用户界面的地方。

注意

Visual Studio 提供的OnLaunched方法的默认代码创建了一个Frame元素,它成为当前Window(唯一的应用程序窗口)的Content。然后调用Frame::Navigate并传入MainPage的类型名称,这会导致MainPage按预期出现。

应用程序现在处于运行状态,用户可以与应用程序交互。如果用户通过按Alt + Tab切换到另一个应用程序,或者转到开始屏幕并激活另一个应用程序磁贴(或通过从左侧滑动切换到另一个应用程序),我们的应用程序不再处于前台。如果 5 秒后用户没有切换回应用程序,它将被操作系统挂起。在此之前,Application::Suspended事件将被触发。这是应用程序在稍后终止时保存状态的机会。应用程序最多有 5 秒钟来保存状态;如果时间超过了,应用程序将被终止。假设一切正常,应用程序被挂起。

应用程序的当前状态可以在任务管理器中查看:

应用程序生命周期

提示

要在任务管理器中查看应用程序的状态,首先选择查看菜单,然后选择状态值,并单击显示挂起状态(默认情况下是关闭的)。

一旦暂停,应用程序可能会恢复,因为用户切换回应用程序。这会导致Application对象上的Resuming事件触发。在大多数情况下,应用程序无需做任何操作,因为应用程序已保留在内存中,因此没有丢失任何内容。在 UI 应该因过时数据而刷新的情况下,可以使用Resuming事件进行操作(例如,RSS 阅读器会刷新数据,因为应用程序可能已经暂停了几个小时,甚至几天)。

在暂停状态下,由于内存资源不足,应用程序可能会被 Windows 终止。应用程序不会收到此事件的通知;这是有道理的,因为应用程序在暂停状态下无法使用任何 CPU 周期。如果用户再次激活应用程序,将调用OnLaunched,从而有机会使用LaunchActivatedEventArgs::PreviousExecutionState属性恢复状态。一个可能的值是ApplicationExecutionState::Terminated,表示应用程序是从暂停状态关闭的,因此应尝试恢复状态。

注意

应用程序中的单个页面可能希望在应用程序即将暂停或恢复时收到通知。这可以在Page的构造函数中通过访问全局Application对象Application::Current来完成。典型的暂停注册可能如下所示:

Application::Current->Suspending += ref new SuspendingEventHandler(this, &MainPage::OnSuspending);

保存和恢复状态

如果应用程序被暂停,那么在应用程序恢复之前,应用程序有责任保存所需的任何状态。这是响应Application::Suspending事件完成的,该事件可以在应用程序级别和/或页面级别处理。

假设我们有一个电影评论应用程序,允许用户评论电影。可能存在一个简单的 UI,看起来像下面这样:

保存和恢复状态

如果用户切换到另一个应用程序,应用程序将在 5 秒后暂停,如果用户不切换回,则应用程序将被暂停。我们可以使用Windows::Storage::ApplicationData类来访问本地设置存储或本地文件夹(对于更复杂的存储需求),以保存前述TextBox元素的状态,以便在应用程序被 Windows 意外终止时可以恢复。首先,我们需要在MainPage构造函数中注册Suspending事件:

MainPage::MainPage() {
  InitializeComponent();

  DataContext = _review = ref new MovieReview;

  Application::Current->Suspending += 
    ref new SuspendingEventHandler(
    this, &MainPage::OnSuspending);
}

MovieReview类表示评论(实现了INotifyPropertyChanged,如第五章数据绑定中讨论的那样),TextBox元素绑定到它的三个属性。如果应用程序被暂停,将执行以下操作:

void MainPage::OnSuspending(Object^ sender, SuspendingEventArgs^ e) {
  ApplicationData::Current->LocalSettings->Values->
    Insert("MovieName", _review->MovieName);
  ApplicationData::Current->LocalSettings->Values->
    Insert("ReviewerName", _review->ReviewerName);
  ApplicationData::Current->LocalSettings->Values->
    Insert("Review", _review->Review);
}

代码使用ApplicationData::LocalSettings属性(一个ApplicationDataContainer对象),它管理一组键/值对(可选的内部容器),通过Values属性公开。

注意

以这种方式存储的类型仅限于基本的 WinRT 类型,并不包括自定义类型,比如MovieReview。可以创建一些代码,将这样的对象序列化为 XML 或 JSON,然后将其保存为字符串。

如果应用程序确实被终止,需要恢复状态。可以在Page::OnNavigatedTo覆盖中执行此操作,如下所示:

void MainPage::OnNavigatedTo(NavigationEventArgs^ e) {
  auto settings = ApplicationData::Current->LocalSettings->Values;
  if(settings->HasKey("MovieName"))
    _review->MovieName = safe_cast<String^>(
    settings->Lookup("MovieName"));
  if(settings->HasKey("ReviewerName"))
    _review->ReviewerName = safe_cast<String^>(
    settings->Lookup("ReviewerName"));
  if(settings->HasKey("Review"))
    _review->Review = safe_cast<String^>(
    settings->Lookup("Review"));
}

为了测试这一点,我们可以在没有 Visual Studio 调试器的情况下运行应用程序。但是,如果需要调试代码,会有一个小问题。当应用程序正在调试时,它永远不会进入暂停状态。这是为了让开发人员能够切换到 Visual Studio 并查看代码,同时应用程序在后台仍然可以随时切换到它。

我们可以通过使用 Visual Studio 工具栏按钮来强制应用程序进入暂停状态,该按钮允许暂停、恢复和终止应用程序(以及调用后台任务,我们将在本章后面的后台任务部分中看到):

保存和恢复状态

确定应用程序执行状态

当应用程序被激活时,可能是因为用户启动了它(还有其他选项,比如实现的合同,我们将在下一章中看到)。通常很重要了解应用程序上次关闭的原因。如果是被终止,状态应该已经恢复。另一方面,如果是用户关闭的,也许状态应该已经清除,因为用户期望应用程序重新开始。

我们可以使用应用程序的 OnLaunched 方法重写中可用的 LaunchActivatedEventArgs::PreviousExecutionState 属性来确定上一个状态:

ApplicationData::Current->LocalSettings->Values
    ->Insert("state", (int)args->PreviousExecutionState);

if (args->PreviousExecutionState == 
  ApplicationExecutionState::Terminated) {
    // restore state
  }
else if(args->PreviousExecutionState == 
  ApplicationExecutionState::ClosedByUser) {
    // clear state
  }

将状态写入 LocalSettings 容器是有用的,这样其他实体(通常是页面)可以在 OnLaunched 完成后访问这些信息。这允许我们的恢复代码查询这个状态并相应地行动:

auto settings = ApplicationData::Current->LocalSettings->Values;
auto state = safe_cast<ApplicationExecutionState>(
  safe_cast<int>(settings->Lookup("state")));
  if(state == ApplicationExecutionState::Terminated) {
    // restore state...

注意

枚举也禁止直接存储,但可以转换为 int 然后存储。

状态存储选项

之前的代码示例使用了 ApplicationData::LocalSettings 属性。这使用了一个存储在本地机器上(当前用户和应用程序)的存储,这意味着在运行 Windows 8 的另一台设备上,即使相同的用户登录,相同的状态也不可用。

WinRT 提供了一种替代方案,允许设置通过使用 ApplicationData::RoamingSettings 属性存储在 Microsoft 云服务中在设备之间漫游。使用这个属性的方式与 LocalSettings 完全相同;它会自动与云同步。

注意

与云同步只有在用户使用他的 Microsoft ID(以前是 Live ID)登录系统时才能工作,而不是“普通”的用户名/密码。

LocalSettingsRoamingSettings 对于简单的键/值对非常有用。如果需要存储更复杂的数据,我们可以创建一个文件夹(StorageFolder 对象),然后可以通过创建 StorageFile 对象、更多文件夹等等来使用。这可以通过访问其他 ApplicationData 属性实现:LocalFolderRoamingFolderTemporaryFolderTemporaryFolder 存储信息直到应用程序终止,通常不适用于应用程序状态管理)。

注意

存储在本地应用程序存储中(ApplicationData::LocalFolder)的文件可以通过以 ms-appdata:///local/ 开头的 URI 访问,后面跟着文件的相对路径;将 local 替换为 roaming 可以访问漫游存储。这些 URI 可以在 XAML 中使用,也可以在代码中使用。

辅助类

一些 Visual Studio 2012 项目模板,如 Grid App,提供了两个类,可以帮助进行状态管理:SuspensionManagerLayoutAwarePage。它们提供了以下功能:

  • 可以管理导航页面堆栈,保存为本地文件夹中的 XML

  • LayoutAwarePage 必须用作基本页面类

  • 可以自动保存/恢复此状态

感兴趣的读者应该参考源代码以获取有关这些类的更多信息。

动态磁贴

Windows Store 应用程序的一个独特特性是在开始屏幕上使用磁贴。这些磁贴可以包含图像和文本,但这些不需要是恒定的,可以改变。通过各种机制提供实时和有意义的信息,吸引用户点击磁贴,访问应用程序本身。在本节中,我们将看看如何创建和操作磁贴。

设置应用磁贴默认值

应用磁贴的默认设置可以在应用清单中设置,通过 Visual Studio 用户界面很容易访问:

设置应用磁贴默认值

有两种大小的磁贴可用,标准和宽。如果有宽标志图像可用,默认会显示,并且用户可以通过右键单击磁贴(或从底部滑动)并选择相关选项来将其更改为标准磁贴(反之亦然)。

设置应用程序磁贴默认值

标准磁贴图像应为 150 像素 x150 像素,宽磁贴图像应为 310 像素 x150 像素。如果未提供这些尺寸,Visual Studio 将发出警告,并且图像将根据需要进行拉伸/缩小。

短名称将显示在显示名称组合框(所有标志无标志仅标准标志仅宽标志)中选择的磁贴顶部。前景文本选项选择浅色或深色文本,所选的背景颜色将用于透明图像(PNG 文件)和一些其他对话框,作为默认背景颜色。

注意

应用程序不应定义宽磁贴,除非应用程序计划在该磁贴中提供有意义且有趣的内容。仅使用大型静态图像是一个坏主意;用户会期望磁贴提供更多内容。

更新磁贴的内容

应用程序可以更新正在运行的磁贴。即使应用程序关闭,更新后的磁贴也会保留其内容。更新磁贴涉及创建一些指定磁贴部分的 XML,其中可以包括各种布局的图像和文本。还有可以在两个磁贴集之间交替的窥视磁贴选项。我们需要做的第一件事是从一组广泛的预定义模板中选择一个合适的磁贴模板。每个模板由一个 XML 字符串表示,需要作为实际更新发送。

标准磁贴和宽磁贴都有模板;这些由Windows::UI::Notifications::TileTemplateType枚举表示。以下是一个宽磁贴的通用 XML 示例,其中包含一个文本项,称为TileWideImageAndText01(枚举值):

<tile>
  <visual>
    <binding template="TileWideImageAndText01">
      <image id="1" src="img/image1.png" alt="alt text"/>
      <text id="1">Text Field 1</text>
    </binding>  
  </visual>
</tile>

需要使用所需的新内容更新突出显示的元素和内部文本。

注意

完整的模板列表和 XML 模式可以在msdn.microsoft.com/EN-US/library/windows/apps/hh761491(v=vs.10).aspx找到。

选择所需的模板后,可以使用以下代码检索相关的 XML(无需手动构建整个 XML):

auto xml = TileUpdateManager::GetTemplateContent(
    TileTemplateType::TileWideImageAndText01);

返回的值是一个Windows::Data::Xml::Dom::XmlDocument,表示生成的 XML。现在,我们需要使用所需的更新调整 XML。在这个例子中,我们将更改图像和文本:

((XmlElement^)xml->GetElementsByTagName("image")->GetAt(0))
  ->SetAttribute("src", "assets\\bug.png");
xml->GetElementsByTagName("text")->GetAt(0)->AppendChild(
  xml->CreateTextNode("You have a bug!!!"));

该代码使用 WinRT XML DOM API 来操作 XML。图像设置为本地图像,但远程图像(http://...)同样有效。

最后一步是为应用程序创建磁贴更新程序,构建磁贴通知并进行实际更新:

auto update = TileUpdateManager::CreateTileUpdaterForApplication();
auto tile = ref new TileNotification(xml);
update->Update(tile);

这是生成的宽磁贴:

更新磁贴的内容

注意

上述代码仅更新宽磁贴,保持标准磁贴不变。要同时更改标准磁贴,我们可以向<visual>元素添加另一个<binding>元素,其中包含所需标准磁贴的适当 XML。这将使两个更改都生效。

启用循环更新

磁贴的一个有趣特性是能够循环最多五次磁贴更新,默认情况下与最后五个一起工作。以下代码将启用磁贴循环:

auto update = TileUpdateManager::CreateTileUpdaterForApplication();
update->EnableNotificationQueue(true);

如果应该替换特定磁贴(而不是丢弃第一个更新),可以使用TileNotification::Tag属性为磁贴打上唯一值,以标识要替换的确切磁贴。

磁贴过期

可以通过设置TileNotification::ExpirationTime属性,使磁贴在将来某个时间点过期。到时候,磁贴将恢复到默认状态。

徽章更新

徽章是位于磁贴右下角的小通知符号。它可以是 1 到 99 的数字,也可以是一组预定义的图形。通常用于显示状态,例如网络连接(如果适用于应用程序)或待处理消息的数量(在消息应用程序中)。

更新徽章与更新磁贴非常相似-它基于包含单个元素(<badge>)的 XML 字符串,通过操作来获得所需的结果。以下是更新具有数字值的徽章所需的代码:

auto xml = BadgeUpdateManager::GetTemplateContent(
  BadgeTemplateType::BadgeNumber);
auto element = (XmlElement^)xml->SelectSingleNode("/badge");
element->SetAttribute("value", (++count).ToString());

auto badge = ref new BadgeNotification(xml);
BadgeUpdateManager::CreateBadgeUpdaterForApplication()
  ->Update(badge);

变量count用作数字值。

创建辅助磁贴

应用程序磁贴(主磁贴)可以附带辅助磁贴。这些通常代表应用程序中的深层链接。例如,天气应用程序可以使用辅助磁贴来更新天气重要的额外位置,或者商店应用程序可以使用辅助磁贴作为指向特定产品的链接。

无论如何,只有用户才能允许将辅助磁贴固定到开始屏幕上或从开始屏幕上取消固定。通常,应用程序内的一些用户界面允许用户固定辅助磁贴,但只有在用户提供同意的情况下才能发生这种情况-否则磁贴将不会被固定。

以下代码片段创建一个辅助磁贴,并询问用户是否要将其固定到开始屏幕上:

using namespace Windows::UI::StartScreen;
auto tile = ref new SecondaryTile("123", "Sample tile", 
  "This is a sample tile", "123", 
  TileOptions::ShowNameOnLogo, ref new Uri(
    "ms-appx:///assets/apple.png"));
create_task(tile->RequestCreateAsync()).then([](bool ok) {
  // do more stuff
});

前面的代码中使用的SecondaryTile构造函数按顺序接受以下参数(也可以使用属性设置):

  • 以后可以用来识别磁贴的唯一磁贴 ID(例如,用于取消固定)

  • 一个系统提供的同意对话框中显示的必需的短名称

  • 显示名称(推荐)

  • 在确定应用程序是否通过辅助磁贴调用时有助于的磁贴激活参数(稍后会详细介绍)

  • Logo URI

调用SecondaryTile::RequestCreateAsync会呈现一个标准的系统对话框(基于磁贴的创建参数),询问用户是否实际上要创建和固定磁贴。

创建辅助磁贴

可以通过使用仅接受 ID 的SecondaryTile构造函数来检索辅助磁贴的唯一 ID。其他选项包括调用静态的SecondaryTile::FindAllAsync来获取应用程序创建的所有辅助磁贴的列表。

SecondaryTile::RequestDeleteAsync方法显示一个系统对话框,请求用户同意删除磁贴。

更新辅助磁贴与更新主磁贴(磁贴和徽章)的方式基本相同。唯一的区别在于更新器,使用TileUpdateManager::CreateTileUpdaterForSecondaryTile(用于磁贴更新)和BadgeUpdateManager::CreateBadgeUpdaterForSecondaryTile(用于徽章更新)创建。

激活辅助磁贴

当点击辅助磁贴时,应用程序会像平常一样启动。由于辅助磁贴应该提供对应用程序内特定位置的快捷方式,因此必须在Application::OnLanuched重写中识别和处理这种情况。以下是一个在启动时查找传递参数的示例代码:

if(args->Arguments != nullptr) {
  // assume arguments are from secondary tiles only
  rootFrame->Navigate(TypeName(DeepPage::typeid), 
    args->Arguments);
}

代码假定DeepPage.xaml是相关页面,以防检测到辅助磁贴激活。

使用 Toast 通知

Toast是小弹出窗口,显示与应用程序相关的重要信息,可能在此时正在运行,也可能不在运行。它出现在屏幕的右上角-用户可以点击(或点击)它来运行或切换到应用程序,或者用户可以关闭(解散)Toast,因为现在不重要,或者如果用户现在不在电脑前,Toast 将在几秒钟后消失,使用户错过 Toast。

Toast 通知有些具有侵入性,因为它们会弹出,而不管当前执行的应用程序是什么(当前应用程序可以是经典桌面,甚至是锁定屏幕)。这意味着 Toast 应该谨慎使用,只有在真正有意义的地方才应该使用。典型的用法是在聊天应用程序中通知用户有新消息或新电子邮件。

可以按应用程序基础关闭应用程序的提示通知,方法是选择设置魅力,然后选择权限。也可以通过转到Windows PC设置并选择通知来全局禁用提示通知。

要使提示通知起作用,应用程序应在其清单中声明其具有提示功能(在 Visual Studio 的清单视图中的应用程序 UI选项卡):

使用提示通知

引发提示通知与磁贴有些类似。首先,我们使用ToastTemplateType枚举选择一个预定义的模板,然后基于该模板构建一个适当的 XML(内容可通过ToastNotificationManager::GeTemplateContent方法获得)。接下来,我们创建一个ToastNotification对象,传递最终的 XML。最后,我们调用ToastNotificationManager::CreateToastNotifier()->Show,传递ToastNotification对象。

提示选项

可以使用ScheduledToastNotification类将提示通知安排到将来的时间点,而不是使用ToastNotification。构造函数的第二个参数是一个DateTime值,指示何时引发提示。为了使其编译并正确工作,Show方法必须替换为AddToSchedule

ScheduledToastNotification的第二个构造函数提供了一种显示定期提示的方法,弹出之间有时间间隔(1 分钟到 60 分钟之间),并且显示提示的次数(1 到 5 次)。

提示可以是标准的(显示 7 秒)或长的(显示 25 秒)。当提示的另一端有人时,例如来电时,长提示是合适的。要设置它,必须在提示 XML 中设置duration属性为long

提示在显示时会播放默认的声音效果。这个效果可以更改为 Windows 提供的一组预定义声音中的一个。同样,这是通过添加一个audio元素来实现的,其中src属性设置为预定义的声音字符串之一(查看完整列表的文档)。

推送通知

正如我们所见,应用程序可以以任何合理的方式设置其磁贴(和可选的辅助磁贴);如果收到新信息,甚至可以更新磁贴。但是,如果应用程序被挂起会发生什么?它如何更新其磁贴?更糟糕的是,应用程序可能根本没有运行。它的磁贴如何更新?想象一下一个新闻应用程序可能希望其磁贴反映最近的新闻。

一种方法是使用推送通知。顾名思义,通知是由服务器推送到设备上的,该设备可能正在运行应用程序,也可能没有。这与拉模型相反,其中应用程序的某个部分轮询某个服务器以获取新信息。推送通知是节能的,并且不需要应用程序做任何特殊的事情(除了首先注册通知,我们马上就会看到)来获取通知。

推送通知架构

推送通知涉及多个参与者,应用程序只是其中之一。推送通知本身是从由微软提供的服务发送的,即托管在 Windows Azure 上的Windows 通知服务WNS)。另一个主要实体是一个应用程序服务器,它具有逻辑或适当地受控以实际发起推送通知。在新闻应用程序示例中,这将是一个接收新闻更新然后使用推送通知向所有注册的客户端应用程序传播它们的服务器。

推送通知架构总结在以下图表中:

推送通知架构

设置推送通知的基本步骤如下:

  1. Windows Store 应用程序必须注册以接收通知。它使用 WinRT API 调用 WNS 并请求一个唯一的通道 URI,该 URI 标识了此应用程序(技术上是主要磁贴)在此设备上的此用户。

  2. WNS 向应用程序返回一个唯一的通道 URI。

  3. 应用程序需要将唯一的通道 URI 和一些唯一的客户端标识符传递给应用服务器。通常需要一个唯一的客户端 ID,因为通道 URI 可能会过期并且需要更新。客户端 ID 在应用服务器看来仍然是身份。

  4. 应用服务器存储了所有已注册客户端的 URI 列表。稍后,当需要发送通知时,它将遍历列表并发送通知。

  5. 应用服务器需要与 WNS 进行身份验证,并获得身份验证令牌以供使用作为推送通知有效负载的一部分。这是一次性操作,但可能需要重复,因为令牌可能在将来过期。

  6. 最后,当应用服务器逻辑决定发送推送通知(或者由某个外部管理应用程序指示时),它将通知作为 HTTP POST 请求发送。

  7. WNS 接收请求并对客户端设备执行实际的推送通知。

推送通知可以更改动态磁贴(主磁贴或辅助磁贴),更改徽章,或使弹出式通知出现。它甚至可以发送原始的、特定于应用程序的通知,可以运行为应用程序注册的后台任务(后台任务将在本章后面讨论)。

在下一节中,我们将看到实现前述步骤以启动推送通知的示例。

构建推送通知应用程序

接收推送通知的第一步是从 WNS 获取唯一的 URI。这是一个相当简单的操作,涉及单个方法调用:

create_task(PushNotificationChannelManager::
  CreatePushNotificationChannelForApplicationAsync()).then(
  this {
  _channel = channel;

调用返回一个PushNoticationChannel对象,该对象存储在_channel成员变量中以供以后使用。这些类型位于Windows::Networking::PushNotifications命名空间中。

下一步是将此 URI 注册到应用服务器,因此让我们首先看看该服务器。

应用服务器

应用服务器可以使用任何服务器端技术构建,可以在 Microsoft 堆栈内部或外部。典型的服务器将公开某种服务,客户端可以连接到该服务并注册其用于推送通知(也许还有其他用途)的唯一 URI。

例如,我们将构建一个托管在 IIS 中的 WCF 服务,该服务将公开一个适当的操作以实现此目的。该示例假定服务器管理电影信息并希望通知已注册的客户端有新电影可用。WCF 服务接口将如下所示:

[DataContract(Namespace="")]
public class ClientInfo {
  [DataMember]
  public string Uri { get; set; }
  [DataMember]
  public string ClientID { get; set; }
}

[ServiceContract]
public interface IMovieService {
  [OperationContract, WebInvoke(UriTemplate="add")]
  void AddNewMovie(Movie movie);

  [OperationContract, WebInvoke(UriTemplate="register")]
  void RegisterForPushNotification(ClientInfo info);
}

IMoviesService有两个操作(建模为方法):

  • RegisterForPushNotification用于将感兴趣的客户端注册为推送通知的目标。它传递了一个ClientInfo对象,其中包含了唯一的通道 URI(从上一步获取)和一些唯一的客户端 ID。

  • AddNewMovie操作稍后将由某个控制器应用程序调用,以指示有新电影可用,并因此调用推送操作(我们稍后会看到)。

注意

WCF(Windows Communication Foundation)是一种基于.NET 的技术,用于编写服务和服务客户端,超出了本书的范围,因为它与 Windows 8 商店应用没有直接关系。 WCF 将用于服务器端代码,因为它相当知名且易于使用,至少对于这些目的来说;代码自然是用 C#编写的。

这样的服务必须首先从 WNS 获取身份验证令牌,以便实际执行推送通知。实现这一点的第一步是注册 Windows 8 应用程序并获取两个信息:安全 ID 和秘钥。有了这些信息,我们可以联系 WNS 并请求一个令牌。要注册应用程序,我们必须浏览到manage.dev.live.com,使用我们的 Microsoft ID(以前是 Live ID)登录,点击创建应用程序,输入一些唯一的应用程序名称,然后点击

应用服务器

结果是一个安全 IDSID)和一个秘密密钥:

应用程序服务器

我们将复制这些并将它们存储为服务类实现中的简单常量或静态字段。应用程序名称本身必须复制到应用程序清单(在打包选项卡中),网页上概述了其他一些详细信息。为了使其中一些工作更容易,右键单击项目,选择商店,然后选择将应用与商店关联。这将把大部分信息输入到正确的位置:

应用程序服务器

获取身份验证令牌的代码如下:

private static void GetToken() {
  var body = string.Format
  ("grant_type=client_credentials&client_id={0}&client_secret={1}
  &scope=notify.windows.com",
  HttpUtility.UrlEncode(SID), HttpUtility.UrlEncode(Secret));

  var client = new WebClient();
  client.Headers.Add("Content-Type", 
     "application/x-www-form-urlencoded");
  string response = client.UploadString(new Uri(AuthUri), body);

  dynamic data = JsonConvert.DeserializeObject(response);
  _token = data.access_token; 
}

代码相当无聊。它使用所需的身份验证过程的特定格式。WebClient类提供了在.NET 中进行 HTTP 调用的简单方法。调用的结果是 JSON 对象的字符串表示,由Newtonsoft.Json.JsonConvert类进行反序列化。最后,access_token字段是我们需要的实际令牌,保存在静态变量_token中。

注意

JsonConvert是免费的Json.NET包的一部分,可以使用 Nuget 轻松安装(在 Visual Studio 中右键单击项目,选择管理 Nuget 包...,搜索Json.Net,然后单击安装

dynamic C#关键字允许(除其他功能外)对对象进行无类型访问,通过延迟绑定到实际成员(如果存在)。编译器乐意将类型检查推迟到运行时,因此未识别的成员会引发运行时异常,而不是通常的编译时错误。

现在已经获得了令牌,可以用它来发送推送通知。

注意

身份验证令牌实际上可能会过期,可以通过检查实际推送通知的POST请求的响应来发现,查找WWW-Authenticate标头的Token expired值。在这种情况下,只需再次调用GetToken以获取新令牌。

现在服务器已经准备好了,客户端应用程序需要使用应用程序服务注册其唯一的通道 URI。

注册推送通知

理论上,这一步很容易。只需调用服务上的RegisterForPushNotification方法,传递所需的参数,然后完成。不幸的是,在 C++中这并不像我们希望的那样容易。

应用程序需要对服务进行正确的网络调用(通常通过 HTTP)。最简单的 HTTP 调用基于 REST,因此如果我们的服务配置为接受 REST over HTTP,那么它将更简单。

注意

REST表述状态转移)超出了本书的范围。对于我们的目的,它意味着将信息编码为 HTTP URL 上的简单字符串,并使用请求正文传递更复杂的信息。这与更复杂的协议(如 SOAP)形成对比。

我们创建的 WCF 服务配置为接受 REST 调用,因为使用了[WebInvoke]属性,为每个请求设置了 URL 后缀。这还需要配置服务主机以使用WebHttpBinding WCF 绑定和WebHttp行为。这是通过MovieWorld.svc文件完成的,其中声明了服务:

<%@ ServiceHost Language="C#" Debug="true" 
  Service="MoviesWorld.MovieService" 
  CodeBehind="MovieService.svc.cs" 
 Factory= "System.ServiceModel.Activation.WebServiceHostFactory" %>

Factory属性是重要的(非默认)部分。

下一个挑战是从 C++客户端应用程序中进行 REST(或任何 HTTP)调用。

不幸的是,在撰写本文时,没有简单的方法可以使用 WinRT 类进行 HTTP 调用,类似于.NET 中的WebClientHttpClient。文档建议使用低级别的IXMLHTTPRequest2 COM 接口来实现此目的。

尽管这肯定是可能的,但并不容易。幸运的是,微软创建了一个 C++包装类HttpRequest,它为我们大部分工作。我将该类复制到项目中,现在更容易进行 HTTP 调用。

注意

HttpRequest 实现在 HttpRequest.hHttpRequest.cpp 文件中,属于 MovieApp 项目的一部分,可在本章可下载的源代码中找到。

这是注册应用程序接收推送通知的 HTTP 请求:

Web::HttpRequest request;
wstring body = wstring(L"<ClientInfo><ClientID>123</ClientID><Uri>") + channel->Uri->Data() + L"</Uri></ClientInfo>";

return request.PostAsync(ref new Uri(
  "http://localhost:36595/MovieService.svc/register"), 
  L"text/xml", body);

主体由一个 ClientInfo 对象组成,以 XML 格式序列化,其中 Uri 元素包含在第一步获取的唯一通道 URI。这里的客户端 ID 被编码为常量 123 作为示例;在真实的应用程序中,这将作为此用户在此设备上的应用程序的唯一标识生成。奇怪的端口号是本地 IIS 监听的端口,我的服务托管在那里。同样,在真实的应用程序中,这将在端口 80(常规 HTTP)或 443(HTTPS)上进行。

注意

发出 HTTP 请求的另一种方法是使用 C++ REST SDK(Casablanca)库;这是在编写这些行时发布到 CodePlex 的。该库允许(除其他功能外)以一种简单和可定制的方式处理 HTTP 请求,与 .NET HttpClient 类有些相似。该 SDK 可以在 casablanca.codeplex.com/ 找到。

发布推送通知

当应用程序服务器收到对其 AddNewMovie 方法的调用时(作为服务器本身的某些逻辑的一部分,或者因为某个管理应用程序调用了该操作),它需要向所有注册的客户端发送推送通知:

public void AddNewMovie(Movie movie) {
  _movies.Add(movie);
  foreach(var uri in _pushData.Values) {
    // push new movie to registered clients
    SendPushTileNotification(uri, movie);
  }
}

SendPushTileNotification 方法如下所示:

private async Task SendPushTileNotification(string uri, Movie movie) {
  string body =
    "<tile>" +
    "<visual>" +
    "<binding template=\"TileSquareText01\">" +
    "<text id=\"1\">" + movie.Year + "</text>" +
    "<text id=\"2\">" + movie.Name + "</text>" +
    "</binding>" +
    "</visual>" +
    "</tile>";

  var client = new HttpClient();
  var content = new StringContent(body);
  content.Headers.ContentType = new  MediaTypeHeaderValue(
      "text/xml");
  client.DefaultRequestHeaders.Add("X-WNS-Type", "wns/tile");
  client.DefaultRequestHeaders.Add("Authorization", 
    string.Format("Bearer {0}", _token));
  await client.PostAsync(uri, content);
}

消息的主体是一个常规的 XML 磁贴。在这种情况下,它包括两行文本:

  • 第一个包含电影发布年份

  • 第二个包括电影名称

通知是基于唯一通道 URI 的 HTTP POST 请求,具有一些必须正确设置的特定标头。还要注意之前从 WNS 获取的身份验证令牌的使用。

注意

await C# 关键字允许等待异步操作而不阻塞调用线程。这类似于我们使用 task类和then` 方法。C# 看起来仍然更容易使用。

通过将 X-WNS-Type 标头更改为 wns/toastwns/badge,通知类型可以更改为 toast 或 badge。主体自然也必须相应地修改。

注意

本章的示例代码包括一个名为 MovieManager 的项目,用于添加生成推送通知的新电影。

这是原始应用程序磁贴(左)和推送通知新电影后的磁贴:

发布推送通知

注意

最近提供的 Windows Azure 移动服务提供了更简单的方式来维护推送通知客户端并发送通知本身(以及其他有用的功能)。移动服务不在本书的范围之内,但可以在 www.windowsazure.com/en-us/develop/mobile/ 找到更多信息。

辅助磁贴的推送通知

辅助磁贴也可以成为推送通知的目标。主要区别在于客户端应用程序获取唯一通道 URI 的方式。它使用 CreatePushNotificationChannelForSecondaryTileAsync 与磁贴 ID,而不是 CreatePushNotificationChannelForApplicationAsyncPushNotificationChannelManager 类的两个静态方法)。

后台任务

当用户切换到另一个应用程序时,Windows Store 应用程序可能会被挂起。即使应用程序被挂起甚至终止,应用程序可能仍然希望发生一些工作。这是后台任务的工作。

什么是任务?

任务 只是实现 Windows::ApplicationModel::Background::IBackgroundTask 接口的类,只有一个方法 Run。这个类必须放在与主应用程序不同的项目中,即 Windows Runtime Component 类型的项目中。这是必不可少的,因为任务在一个单独的进程中运行,因此不能与主应用程序绑定(因此,如果主应用程序被挂起,它们也不会被挂起)。

主应用程序需要引用包含任务的项目,并通过其清单指示这些确实是其任务。

注意

应用程序可以在一个或多个 Windows Runtime 组件项目中实现任意数量的任务。

任务必须有一个触发器,指定触发任务执行的条件。任务还可以具有零个或多个必须为触发器指定的条件。

注意

只能将一个触发器与任务关联,但可以注册另一个使用相同代码运行的任务,但配置了不同的触发器。这有效地创建了一个可以使用多个触发器运行的任务。

创建和注册任务

创建任务的第一步是创建一个实现IBackgroundTask接口的类的 Windows Runtime 组件项目,如下所示:

namespace Tasks {
  using namespace Windows::ApplicationModel::Background;

  [Windows::Foundation::Metadata::WebHostHidden]
  public ref class SimpleTask sealed : IBackgroundTask {
  public:
    virtual void Run(IBackgroundTaskInstance^ taskInstance);
  };
}

接下来,我们需要从主应用程序项目中添加对任务组件的引用。最后一个前提是将任务添加到主应用程序的清单中。这是在声明选项卡中完成的:

创建和注册任务

通过选择适当的任务类型选择了后台任务声明,这大致意味着触发器类型,稍后将在实际代码中使用。我们将在稍后讨论触发器。

入口点字段必须设置为实现后台任务的完整类名(在本例中为Tasks::SimpleTask)。

结果是清单 XML 中<extensions>部分的一个条目:

<Extensions>
  <Extension Category="windows.backgroundTasks" 
    EntryPoint="Tasks.SimpleTask">
    <BackgroundTasks>
      <Task Type="systemEvent" />
    </BackgroundTasks>
  </Extension>
</Extensions>

主应用程序必须在启动时进行实际任务注册,并且只能执行一次。使用现有任务名称注册任务会引发异常。

注册涉及BackgroundTaskBuilder类和一个带有可选条件的触发器类。这是注册在上述代码片段中定义的SimpleTask以在 Internet 连接可用时执行的一段代码:

auto trigger = ref new SystemTrigger(
  SystemTriggerType::InternetAvailable, false);
auto condition = ref new SystemCondition(
  SystemConditionType::InternetAvailable);

auto builder = ref new BackgroundTaskBuilder();
builder->Name = "Simple";
builder->TaskEntryPoint = "Tasks.SimpleTask";
builder->SetTrigger(trigger);
builder->AddCondition(condition);
auto task = builder->Register();

必须为任务选择一个触发器;在本例中,它是通用的SystemTrigger,基于SystemTriggerType枚举,其值包括InternetAvailableUserPresentUserAwaySmsReceivedTimeZoneChange等。

条件是可选的;SystemCondition目前是唯一可用的条件,但它也是通用的,使用SystemConditionType枚举。值包括InternetAvailableInternetUnavailableUserPresentUserNotPresent等。

BackgroundTaskBuilder保存了触发器和条件信息,以及任务名称和入口点。然后调用Register实际上向系统注册(返回一个BackgroundTaskRegistration对象)。

实现任务

让我们使用一个允许用户输入数据并将数据保存在本地文件夹中的应用程序。如果用户连接到互联网,后台任务应该对生成的文件进行一些处理,例如将它们上传到服务器,进行一些计算等。最终,任务将在处理后删除文件。

以下是主应用程序用于将一些数据保存到文件的简单代码:

auto root = ApplicationData::Current->LocalFolder;

create_task(root->CreateFolderAsync("Movies", 
  CreationCollisionOption::OpenIfExists)).then([](
  StorageFolder^ folder) {
    return folder->CreateFileAsync("movie", 
    CreationCollisionOption::GenerateUniqueName);
  }).then([](StorageFile^ file) {
    // build data to write
    return file->OpenAsync(FileAccessMode::ReadWrite);
  }).then(this {
    wstring data = wstring(L"<Movie><Name>") + 
    _movieName->Text->Data() + L"</Name><Year>" + 
    _year->Text->Data() + L"</Year></Movie>";
    auto writer = ref new DataWriter(stm);
    writer->WriteString(ref new String(data.c_str()));
    return writer->StoreAsync();
  }).then(this {
  _movieName->Text = "";
  _year->Text = "";
});

文件保存在LocalFolder下名为Movies的子文件夹中。

任务共享应用程序的本地文件夹,有效地使其成为一种通信机制。这是任务的Run方法实现:

void SimpleTask::Run(IBackgroundTaskInstance^ taskInstance) {
  auto root = ApplicationData::Current->LocalFolder;
  Platform::Agile<BackgroundTaskDeferral^> deferral(
  taskInstance->GetDeferral());
  create_task(root->GetFolderAsync("Movies")).
    then([](StorageFolder^ folder) {
    return folder->GetFilesAsync(
    CommonFileQuery::DefaultQuery);
    }).then([](IVectorView<StorageFile^>^ files) {
    int count = files->Size;
    for(int i = 0; i < count; i++) {
      auto file = files->GetAt(i);
      // process each file...
      file->DeleteAsync();
    }
  }).then(deferral {
    t.get();
    // error handling omitted
    deferral->Complete();
  });
}

任务首先获取LocalFolder位置。在实际处理开始之前,它通过调用IBackgroundTaskInstance::GetDeferral获取了一个延期对象。为什么?

Run方法终止时,任务通常被视为已完成。但是,如果实现调用任何异步操作,则该方法会更早地返回给其调用者,使任务完成。获得延期实际上会推迟任务完成,直到调用BackgroundTaskDeferral::Complete时。

接下来是实际的文件处理。使用StorageFolder::GetFilesAsync枚举Movies文件夹中的所有文件,并在每次虚拟处理后删除文件。只有在整个任务完成后,才能调用延迟的Complete方法来指示任务已完成。

任务调试

在开发环境中,可用的触发器和条件并不容易满足。我们不想断开并重新连接互联网;也不想等待 15 分钟直到TimeTrigger的任务被执行。

Visual Studio 提供了一种在任何时候调用任务以进行调试的方法。这个功能位于与暂停和恢复相同的工具栏按钮中:

任务调试

如果我们在任务的Run方法中设置断点,我们可以像平常一样调试任务。BackgroundTaskHost.exe是托管应用程序任务的进程实例。这个事实可以在调试器的进程工具栏按钮或 Windows 任务管理器中查看。

任务进度和取消

后台任务可以在主应用程序运行时运行。其中一件可能做的事情是从后台任务的角度指示其进度。这是使用IBackgroundTaskInstance::Progress属性完成的。如果主应用程序没有运行,没有人在乎。如果它正在运行,它可以注册Progress事件(作为成功注册任务时返回的IBackgroundTaskRegistration的一部分)并根据进度更新 UI。

当任务完成时,IBackgoundTaskRegistration::Completed事件触发,以便主应用程序知道任务已完成。如果主应用程序当前被挂起,它将在恢复时收到通知。

在某些情况下,Windows 可能会取消正在运行的任务。IBackgroundTaskInstance公开了一个Canceled事件,任务可以注册。如果任务被取消,必须在 5 秒内返回,否则将被终止。Canceled事件提供了一个指定任务被取消原因的BackgroundTaskCancellationReason。示例包括ServiceUpdate(主应用正在更新)和LoggingOff(用户正在注销系统)。

例如,我们可以使用 Win32 事件通知我们的任务已经被请求取消。首先,我们创建事件对象并注册Canceled事件:

void SimpleTask::Run(IBackgroundTaskInstance^ taskInstance) {
  if(_hCancelEvent == nullptr) {
    _hCancelEvent = ::CreateEventEx(nullptr, nullptr, 0, 
    EVENT_ALL_ACCESS);
    taskInstance->Canceled += 
    ref new BackgroundTaskCanceledEventHandler(
    this, &SimpleTask::OnCancelled);
  }

_hCancelEvent是一个HANDLE类型,使用CreateEventEx创建。然后Canceled事件与一个私有的OnCancelled方法相关联。

注意

经典的 Win32 API CreateEvent不能使用,因为在 WinRT 中是非法的。CreateEventEx是在 Windows Vista 中引入的,并且可以被视为CreateEvent的超集。

如果任务被取消,我们设置 Win32 事件:

void SimpleTask::OnCancelled(IBackgroundTaskInstance^ instance, 
  BackgroundTaskCancellationReason reason) {
  ::SetEvent(_hCancelEvent);
}

任务的主要处理代码应该尽快检查 Win32 事件并退出:

for(int i = 0; i < count; i++) {
  auto file = files->GetAt(i);
  if(::WaitForSingleObjectEx(_hCancelEvent, 0, FALSE) == 
    WAIT_OBJECT_0)
   // cancelled
    break;
    // process each file...
    file->DeleteAsync();
}

使用零超时调用WaitForSingleObject只是检查事件的状态。如果它被标记,返回值是WAIT_OBJECT_0(否则,返回值是WAIT_TIMEOUT)。

播放后台音频

一些应用程序播放音频,用户期望即使用户切换到另一个应用程序,音频也能继续播放;例如,音乐播放应用程序应该一直播放,直到用户要求停止。语音通话应用程序(如 Skype)应该在用户切换到另一个应用程序时保持对方的音频。这就是后台音频任务的用武之地。

播放音频

通过使用MediaElement控件(也可以播放视频),可以轻松实现音频播放。它应该放置在 XAML 的某个位置,以便它成为可视树的一部分,尽管在播放音频时它没有可见部分。

通过设置要使用Source属性播放的 URI(或通过调用SetSource方法从FileOpenPicker获取的文件),可以实现实际的播放。除非将AutoPlay属性设置为false,否则播放会立即开始。

控制播放是通过MediaElementPlayPauseStop方法完成的。以下是从FileOpenPicker获取的音频文件的示例。首先是MediaElement和播放控制的基本 UI:

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="Auto" />
    <RowDefinition Height="Auto" />
  </Grid.RowDefinitions>
  <MediaElement x:Name="_media" />
  <Button Content="Select Audio File..." FontSize="30" Margin="10" 
    Click="OnSelectMediaFile" />
    <StackPanel Orientation="Horizontal" Grid.Row="2" 
    Margin="10,30">
    <Button Content="Play" FontSize="40" Click="OnPlay" 
    Margin="8"/>
    <Button Content="Pause" FontSize="40" Click="OnPause" 
    Margin="8"/>
    <Button Content="Stop" FontSize="40" Click="OnStop" 
    Margin="8"/>
  </StackPanel>
</Grid>

OnSelectedMediaFile的实现如下:

auto picker = ref new FileOpenPicker();
picker->FileTypeFilter->Append(".mp3");
create_task(picker->PickSingleFileAsync()).
  then(this {
    if(file == nullptr)
      throw ref new OperationCanceledException();
      return file->OpenReadAsync();
  }).then(this {
  _media->SetSource(stm, "");
  }).then([](task<void> t) {
  try {
    t.get();
  }
  catch(Exception^ ex) {
  }
});

现在大部分代码应该是熟悉的。FileOpenPicker的过滤器设置为 MP3 文件,一旦选择,调用MediaElement::SetSource准备好音频流进行播放。

播放流只是调用MediaElement::PlayPlay按钮的Click处理程序中的问题:

void MainPage::OnPlay(Object^ sender, RoutedEventArgs^ e) {
  _media->Play();
}

通过调用MediaElement::PauseMediaElement::Stop来实现OnPauseOnStop

现在运行应用程序可以选择一个 MP3 文件并播放它。然而,切换到另一个应用程序会立即停止播放。

维护背景音频

使应用程序在后台继续自动播放需要几个步骤。

首先,必须修改应用程序清单以指示需要背景音频;这是通过添加后台任务声明并设置音频复选框来完成的:

维护背景音频

另一个必需的步骤是设置开始页面,如前面的屏幕截图所示。下一步需要添加一些代码:

  • MediaElement::AudioCategory属性必须设置为AudioCategory::BackgroundCapableMedia(用于一般背景播放)或AudioCategory::Communications(用于点对点通信,如聊天)。

  • 注册Windows::Media::MediaControl类的静态事件,指示可能由其他应用程序使用音频播放而导致的更改。

首先,我们将更改MediaElementAudioCategory属性:

<MediaElement x:Name="_media" AudioCategory="BackgroundCapableMedia"/>

此设置的结果使应用程序永远不会进入暂停状态。

接下来,我们将注册所需的事件:

MediaControl::PlayPressed += ref new EventHandler<Object^>(
  this, &MainPage::OnPlayPressed);
MediaControl::PausePressed += ref new EventHandler<Object^>(
  this, &MainPage::OnPausePressed);
MediaControl::StopPressed += ref new EventHandler<Object^>(
  this, &MainPage::OnStopPressed);
MediaControl::PlayPauseTogglePressed += 
  ref new EventHandler<Object^>(
  this, &MainPage::OnPlayPauseTogglePressed);

这些事件是由系统触发的,当播放状态由于系统提供的媒体控制而发生变化时,可以通过某些键盘和其他手势访问:

维护背景音频

对这些事件的反应并不困难。以下是PlayPressedPlayPauseTogglePressed事件的代码:

void MainPage::OnPlayPressed(Object^ sender, Object^ e) {
  Dispatcher->RunAsync(CoreDispatcherPriority::Normal, 
    ref new DispatchedHandler([this]() {
    MediaControl::IsPlaying = true;
    _media->Play();
  }));
}

void MainPage::OnPlayPauseTogglePressed(Object^ sender, Object^ e) {
  Dispatcher->RunAsync(CoreDispatcherPriority::Normal, 
  ref new DispatchedHandler([this]() {
    if(_media->CurrentState == MediaElementState::Playing) {
      MediaControl::IsPlaying = false;
      _media->Pause();
    }
    else {
      MediaControl::IsPlaying = true;
      _media->Play();
    }
  }));
}

通知被处理为应用程序播放或暂停播放所需的命令;正确的实现确保系统上所有音频播放的一致行为。

注意

请注意,事件在线程池线程上到达,由于需要触摸MediaElement,因此调用必须使用CoreDispatcher::RunAsync方法调度到 UI 线程。

处理PausePressedStopPressed事件是类似的。

如果适当的话,MediaControl类的其他事件也可以被处理,例如NextTrackPressedPreviousTrackPressed

声音级别通知

如果后台应用程序正在播放音频,另一个前台应用程序开始播放音频,系统会向后台应用程序发送MediaControl::SoundLevelChanged事件。此事件通过查看MediaControl::SoundLevel属性指示了后台应用程序的声音发生了什么。可能的值有:

  • Muted:应用程序的声音已被静音,因此应用程序应该暂停其播放。这通常意味着前台应用程序正在播放音频。

  • Low:应用程序的声音级别已经降低。这表明 VoIP 呼叫进来,降低了应用程序的声音级别。应用程序可能希望暂停播放,直到另一个SoundLevelChanged事件触发,指示完全音量。

  • Full:应用程序的声音是最大音量。如果应用程序正在播放音频并且必须暂停它,现在是恢复播放的时候了。

注册参加此活动是可选的,但可以增强用户体验,并且表明应用程序行为良好。

锁屏应用程序

锁屏(在用户登录之前或设备被锁定时)最多可以容纳七个应用程序 - 这些应用程序可以有一个图标(甚至一个文本消息);这些应用程序被称为锁屏应用程序。可以通过控制面板|PC 设置|个性化来配置七个可能的应用程序:

锁屏应用

系统认为锁屏应用程序更重要(因为它们对用户更重要),因此具有一些非锁屏应用程序无法获得的功能。例如,某些触发器类型只适用于锁屏应用程序:

  • TimeTrigger可用于定期执行任务(最短间隔为 15 分钟)。

  • PushNotificationTrigger可用于接收导致任务执行的原始推送通知(原始意味着任何字符串,与磁贴、弹出通知或徽章无关)。

  • ControlChannelTrigger可用于与远程服务器保持实时连接,即使应用程序被挂起;对即时通讯或视频聊天应用程序很有用。

注意

最后两种触发器类型使任务实际上在应用程序进程中运行,而不是在标准任务托管进程中运行。

实际上还有另一个与时间相关的触发器,MaintenanceTrigger。这个不需要锁屏应用程序,但只有在设备连接到交流电源时才起作用。如果断开连接,任务将不会运行。如果在任务执行时断开连接,任务将被取消。

要使应用程序具有锁屏功能,需要设置一些事项:

  • 必须为应用程序设置一个宽标志。

  • 徽章标志也必须设置好;这是在应用程序的锁屏上显示的默认图像。

  • 必须声明至少一个后台任务(使用 Visual Studio 中的清单声明选项卡),该任务使用推送通知触发器、时间触发器或控制通道触发器。

  • 锁屏通知选项必须设置为徽章带有磁贴文本的徽章锁屏应用

请求设置锁屏应用

尽管用户可以转到应用程序的设置并将其设置为锁定屏幕应用程序,或者转到 Windows 个性化部分并执行相同操作。如果应用程序通过系统提供的对话框询问用户是否可以成为锁屏应用程序,这样更容易。这是通过BackgroundExecutionManager::RequestAccessAsync静态方法调用来实现的。异步调用的结果指定用户是否接受了建议(BackgroundAccessStatus枚举)。

注意

如果用户拒绝,当应用程序再次运行时对话框不会弹出;如果重新安装应用程序,它将再次弹出。

锁屏应用的其他常见操作

锁屏应用程序通常在应用程序的生命周期内执行以下操作:

  • 发送徽章更新(显示在锁屏上)

  • 发送磁贴更新

  • 接收和处理用于执行特定应用程序逻辑的原始推送通知

  • 弹出通知(即使在锁屏上也显示)

这些操作的确切细节超出了本书的范围(尽管徽章、磁贴和弹出通知的更新机制与已经讨论过的类似);更多细节可以在 MSDN 文档中找到。

后台任务限制

在执行时,后台任务与当前运行的前台应用程序竞争 CPU 和网络资源,因此它们不能以任意长度的时间运行;前台应用程序是最重要的。任务受以下约束条件的约束:

  • 锁屏应用程序每 15 分钟获得 2 秒的 CPU 时间(实际运行时间,而不是挂钟时间)。

  • 非锁屏应用程序每 2 小时接收 1 秒的 CPU 时间(实际运行时间)。

  • 当设备运行在交流电源上时,网络资源是无限的。否则,根据能源消耗可能会有一些限制。

  • 使用ChannelControlTriggerPushNotificationTrigger配置的任务会收到一些资源保证,因为它们被认为更重要。

此外,有一个全局的 CPU 和网络资源池,可以被任何应用程序使用。这个池每 15 分钟重新填充一次。这意味着即使一个任务需要超过 1 秒的时间来运行(非锁屏应用程序),它也可能获得额外的 CPU 时间,前提是池没有耗尽。当然,任务不能依赖这个池,因为其他任务可能已经耗尽了它。

后台传输

在这一点上应该很清楚,一个挂起的应用程序本身无法做任何事情,除非它有一些代表它工作的后台任务。应用程序可能需要执行的操作之一是下载或上传文件。如果应用程序被挂起,下载或上传操作无法继续。如果应用程序被终止,已经下载的内容会消失。显然,必须有更好的方法。

WinRT 提供了一种进行后台传输(下载和上传)的方法,即使应用程序被挂起,也可以使用一个单独的进程来执行实际的传输(BackgroundTransferHost.exe)。这种能力允许应用程序进行长时间的传输,而不需要用户在整个传输时间内一直待在应用程序中。

示例 - 下载文件

以下是一个简单的示例,它启动了一个针对用户文档位置中的文件的下载操作(省略了错误处理):

wstring filename(_url->Text->Data());
auto index = filename.rfind(L'/');
filename = filename.substr(index + 1);
create_task(
KnownFolders::DocumentsLibrary->CreateFileAsync(
  ref new String(filename.c_str()), 
CreationCollisionOption::GenerateUniqueName)).then(this {
  auto downloader = ref new BackgroundDownloader();
  auto operation = downloader->CreateDownload(
  ref new Uri(_url->Text), file);
  return operation->StartAsync();
});

代码假设_url是一个TextBox,用户在其中输入了要下载的文件的 URL。首先,根据 URL 的最后一个斜杠后的短语创建文件名。然后,在用户的文档文件夹中创建文件。请注意,要获得这个功能,必须在清单中声明,并且对于文档库,至少必须选择一个文件扩展名:

示例 - 下载文件示例 - 下载文件

注意

对于VideoMusicPictures库,不需要声明文件关联。

接下来,创建一个BackgroundDownloader实例,并调用其CreateDownload方法,传递要下载的 URL 和目标文件。这个调用返回一个DownloadOperation对象,而不会实际开始下载。要开始下载,调用DownloadOperation::StartAsync

在下载进行时,了解其进度是很有用的。以下是一个修改后的代码,用于设置进度报告(与StartAsync调用的区别在于):

auto async = operation->StartAsync();
async->Progress = 
  ref new AsyncOperationProgressHandler<DownloadOperation^, 
  DownloadOperation^>(this, &MainPage::OnDownloadProgress);
return async;

在这种情况下,我们实际上是在查看StartAsync的结果,它返回一个实现IAsyncOperationWithProgress<DownloadOperation, DownloadOperation>的对象,并且我们使用适当的委托设置了Progress属性:

void 
MainPage::OnDownloadProgress(IAsyncOperationWithProgress<DownloadOperation^, DownloadOperation^>^ operation, 
  DownloadOperation^ download) {
  auto progress = download->Progress;
  Dispatcher->RunAsync(CoreDispatcherPriority::Normal, 
    ref new DispatchedHandler([progress, this]() {
    _progress->Maximum = 
  double(progress.TotalBytesToReceive >> 10);
    _progress->Value = double(progress.BytesReceived >> 10);
    _status->Text = progress.Status.ToString();
  }));
}

DownloadOperation::Progress属性返回一个简单的结构(BackgroundDownloadProgress),其中包括TotalBytesToReceiveBytesReceivedStatusRunningCompletedCancelled等)。前面的代码使用这些值来控制ProgressBar控件(_progress)和TextBlock_status)。

请注意,通知不会到达 UI 线程,因此对 UI 的任何更新都必须通过使用Page::Dispatcher属性(类型为Windows::UI::Core::CoreDispatcher)将其调度到 UI 线程,方法是使用接受在 UI 线程上执行的委托的RunAsync调用。

如果应用程序被终止,传输也会停止,但到目前为止下载的数据不会丢失。当应用程序再次启动时,它的工作是查找所有未完成的传输并恢复它们。这可以通过调用静态的BackgroundDownloader::GetCurrentDownloadsAsync来实现,获取一个未完成下载的列表,然后附加到每一个(例如,进度报告),当然,恢复下载。

注意

您可以在code.msdn.microsoft.com/windowsapps/Background-Transfer-Sample-d7833f61找到这方面的完整示例。

摘要

Windows 商店应用程序在许多方面都不同于桌面应用程序。本章涉及应用程序的生命周期 - 应用程序可能会被暂停甚至终止,所有这些都由操作系统控制。

磁贴、徽章更新和弹出通知是 Windows 商店应用程序的一些更独特的功能,桌面应用程序没有这些功能(尽管桌面应用程序可以创建自己的类似弹出通知)。明智地使用这些功能,可以为商店应用程序增加很多吸引力,频繁地吸引用户进入应用程序。

后台任务提供了一种绕过非自愿暂停/终止情况的方法,以便即使应用程序不在前台时也能保持一定的控制。不过,这是相当受限制的,以保持主要应用程序的响应性和良好的电池寿命。任务是非常重要的非平凡应用程序的重要组成部分,因此应该明智地使用。

在下一章中,我们将探讨 Windows 商店应用程序如何通过实现合同和扩展与 Windows 更好地集成,并间接地与其他应用程序进行通信。

第八章:合同和扩展

Windows Store 应用程序在一个称为AppContainer的严格沙箱中运行。该容器不允许应用程序直接与机器上的其他应用程序通信(例如 Win32 内核对象句柄和共享内存)。从某种意义上讲,这是有道理的,因为应用程序不能假设安装自商店的计算环境的任何内容,除了应用程序请求的 CPU 架构和功能。没有办法确切地知道应用程序是否存在,即使有办法,也没有好的方法来确保它实际上可以与这个应用程序通信。

相反,Windows 为应用程序之间的通信定义了一组合同。这些合同允许应用程序实现一些功能,而不知道将使用它的其他应用程序是哪个。这些合同是明确定义的,并且在操作系统的帮助下进行必要的连接,它们允许应用程序间接通信。我们将在本章中研究一些常见的合同。

应用程序还可以为操作系统提供的某些功能提供“插件”。这些“插件”称为扩展,我们将看一下其中的一个,即设置扩展。

功能

Windows Store 应用程序不能直接与其他应用程序通信,但是系统本身呢?文件、文件夹或设备呢?事实证明,默认情况下,这些也受到限制,必须在应用程序安装时由用户授予权限。

应用程序必须声明所有由 Windows 定义的预期系统使用,作为用户必须同意的内容。这些是功能,是应用程序清单的一部分。Visual Studio 在其清单视图中提供了功能的图形视图,我们在之前的章节中已经使用过:

功能

图像显示了当前支持的所有功能。默认情况下,只启用了一个功能:进行出站网络调用的能力。

必须明确请求访问用户的“我的”库(文档、图片、视频和音乐),否则在访问时将抛出“访问被拒绝”的异常;文档库还要求应用程序指定其接受的文件类型。

设备访问自然是一个问题,由麦克风网络摄像头位置接近度等功能表示。

请注意,没有能够授予应用程序访问 Windows 系统文件夹(如Program FilesSystem32等)的功能;这对于 Windows Store 应用程序来说是不可能的,也应该是如此。没有应用程序需要这样高权限的访问。

合同

合同由 Windows 为应用程序之间的通信定义;这是一种由操作系统调解的应用程序之间的协议,允许应用程序间接通信。让我们看看两个常见合同的例子。

分享合同

共享合同在一个是共享源(有东西可以共享)的应用程序和一个共享目标应用程序(想要对共享的数据进行操作)之间运作。一个应用程序可以是共享源、共享目标,或者两者兼有。

通常通过使用共享魅力来启动共享。当从共享源应用程序激活时,Windows 会提供一个可能的目标应用程序列表——所有实现共享目标合同并至少接受源提供的一种数据类型的安装应用程序。让我们看看如何创建共享源和共享目标。

分享源

成为共享源比成为共享目标更容易。共享源需要通知 Windows 它可以提供任何潜在数据。共享所需的大部分工作都在Windows::ApplicationMode::DataTransfer命名空间中。

共享源在应用程序或主页面初始化时必须注册DataTransferManager::DataRequested事件,代码如下:

DataTransferManager::GetForCurrentView()->DataRequested += 
   ref new TypedEventHandler<DataTransferManager^, 
   DataRequestedEventArgs^>( this, &MainPage::OnDataRequested);

该代码将OnDataRequested私有方法注册为由管理共享操作的共享代理 Windows 组件调用的处理程序。当调用该方法时,应用程序需要提供数据。以下是一个显示世界国旗的简单应用程序:

共享源

这个应用程序想要共享一个选定的国旗图像和一些文本,即选定国家的名称。OnDataRequested方法实现如下:

void MainPage::OnDataRequested(DataTransferManager^ dtm, 
   DataRequestedEventArgs^ e) {
  int index = _gridFlags->SelectedIndex;
  if(index < 0) return;

  auto data = e->Request->Data;
  auto flag = (CountryInfo^)_gridFlags->SelectedItem;

  data->SetText(ref new String(L"Flag of ") + flag->CountryName);
  auto bitmap = RandomAccessStreamReference::CreateFromUri(
      flag->FlagUri);
  data->SetBitmap(bitmap);
  data->Properties->Title = "Flags of the world";
  data->Properties->Thumbnail = bitmap;
}

该方法的第一件事是检查是否选定了任何国旗(_gridFlags是一个包含所有国旗的GridView)。如果没有选定任何内容,则该方法简单地退出。如果用户在没有选定任何内容时尝试共享,Windows 会显示消息现在没有可共享的内容

注意

可以设置另一行文本来向用户指示共享不可用的确切原因。以下是一个示例:

if(index < 0) {
   e->Request->FailWithDisplayText(   
     "Please select a flag to share.");
   return;
}

DataRequestedEventArgs有一个属性(Request,类型为DataRequest),它有一个Data属性(一个DataPackage对象),用于填充共享数据。在前面的代码片段中,使用DataPackage::SetText方法设置了一个字符串。接下来,使用DataPackage::SetBitmap设置了一个图像(使用辅助类RandomAccessStreamReference)。

一个包还包含一堆可以设置的属性,其中Title是唯一必需的。该示例将缩略图设置为相同的国旗图像。

DataPackage还可以接受其他格式,例如SetHtmlFormatSetUriSetRtfSetStorageItems(共享文件/文件夹)的方法。

注意

另一个方法SetDataProvider允许应用程序注册一个委托,当数据实际需要时将对其进行查询,而不是在之前。如果获取数据很昂贵,并且只有在实际需要时才应该进行;此外,它提供了一种共享自定义数据的方式。

一旦方法完成,数据就可以提供给共享目标。

注意

DataRequest有一个GetDeferral方法,允许应用程序进行异步调用,而在方法返回时共享代理不会认为数据已准备就绪(类似于我们在后台任务中看到的机制)。调用DataRequestDeferral::Complete表示数据实际准备好可以共享了。

共享目标

成为共享目标比共享源更困难。其中一个原因是,共享目标应用程序在请求共享时可能尚未运行。这意味着系统必须事先知道哪些应用程序能够成为共享目标,以及这些应用程序可以接收什么类型的数据。

成为共享目标的第一步是在应用程序清单中声明应用程序实际上是一个共享目标,并指定它愿意接受的数据类型。以下是清单的屏幕截图,显示了一个愿意接收位图的应用程序:

共享目标

共享目标应用程序必须支持至少一种数据格式(在本例中为位图),或者至少一种文件类型(如.doc)。

当选定国旗时,这个应用程序在共享窗格中的显示如下:

共享目标

名为ShareTargetDemo的应用程序是本章可下载代码的一部分,它是图片库的一个简单图像查看器。

一旦用户选择我们的应用程序,如果尚未在内存中,则会被激活(执行)。系统调用虚拟方法Application::OnShareTargetActivated。该方法指示应用程序正在作为共享目标激活,并且必须做出适当响应。

具体来说,应用程序必须为共享窗格提供一些用户界面,指示它即将使用的数据,并提供一些按钮控件,允许用户实际确认共享。

这是一个简单的共享页面 UI,允许一些文本标签、一个图像和一个共享按钮:

<StackPanel>
    <TextBlock Text="{Binding Text}" FontSize="20" Margin="10"/>
    <TextBlock Text="{Binding Description}" FontSize="15" 
          TextWrapping="Wrap" Margin="4" />
    <Image Margin="10" Source="{Binding Source}" />
    <Button Content="Share" FontSize="25" HorizontalAlignment="Right" 
          Click="OnShare"/>
</StackPanel>

绑定期望使用相关的ViewModel,定义如下:

[Windows::UI::Xaml::Data::BindableAttribute]
public ref class ShareViewModel sealed {
public:
  property Platform::String^ Text;
  property Windows::UI::Xaml::Media::ImageSource^ Source;
  property Platform::String^ Description;
};

在这种情况下,目标应用程序愿意接受图像。Image元素将显示要接受的图像的预览。一旦用户点击共享按钮,共享操作就会执行,整个共享操作被视为完成。

Application::OnShareTargetActivated重写负责激活共享页面 UI:

void App::OnShareTargetActivated(ShareTargetActivatedEventArgs^ e) {
  auto page = ref new SharePage();
  page->Activate(e);
}

SharePage是之前定义的共享 UI 的类。Activate方法是一个应用程序定义的方法,应该提取共享信息并根据需要初始化 UI:

void SharePage::Activate(ShareTargetActivatedEventArgs^ e) {
  _operation = e->ShareOperation;
  auto data = _operation->Data;
  auto share = ref new ShareViewModel();
  share->Text = data->Properties->Title;
  share->Description = data->Properties->Description;
  auto ref = data->Properties->Thumbnail;
  if(ref != nullptr) {
    create_task(ref->OpenReadAsync()).then(
         share, this {
      auto bmp = ref new BitmapImage();
      bmp->SetSource(stm);
      share->Source = bmp;
      DataContext = nullptr;
    // INotifyPropertyChanged is not implemented
      DataContext = share;
    });
  }
  DataContext = share;
  Window::Current->Content = this;
  Window::Current->Activate();
}

要做的第一件事是在单击共享按钮时保存操作对象以供以后使用(_operationWindows::ApplicationModel::DataTransfer::ShareTarget命名空间中的ShareOperation类型的字段)。共享数据本身位于ShareOperation::Data属性中(类似于共享源端的DataPackage对象,但是该数据的只读视图)。

接下来,从数据对象中提取所需的信息,并将其放入ShareViewModel实例中。如果提供了缩略图,可以通过打开RandomAccessStreamReference对象并使用BitmapImage加载图像来提取它,然后将其放入ShareViewModel使用的ImageSource中。

最后,将DataContext设置为ShareViewModel实例,并在实际激活之前将页面设置为当前窗口内容。当共享源是标志应用程序(在共享之前选择了中国的标志)时,情况如下:

共享目标

现在,用户可以与共享窗格交互。如果关闭,将不会发生任何事情,并且目标应用程序将被终止,如果在共享激活之前未运行。另一方面,如果用户决定执行实际共享(通过单击共享按钮),应用程序需要执行适当的操作。例如,内置的邮件应用程序会显示一个新的电子邮件 UI,将共享的数据(通常是文本)添加到可以发送的空电子邮件中。

我们的共享目标应用程序希望将提供的图像保存到当前用户的图片库中。以下是此应用程序的共享按钮的Click处理程序:

void SharePage::OnShare(Object^ sender, RoutedEventArgs^ e) {
  if(_operation->Data->Contains(StandardDataFormats::Bitmap)) {
    auto op = _operation;
    create_task(_operation->Data->GetBitmapAsync()).then(
            op {
      return sref->OpenReadAsync();
    }).then(op {
      return BitmapDecoder::CreateAsync(stm);
    }).then(op {
  create_task(KnownFolders::PicturesLibrary->CreateFileAsync(
"SharedImage.jpg", CreationCollisionOption::GenerateUniqueName))
  .then(decoder {
    return file->OpenAsync(
             FileAccessMode::ReadWrite);}).then(
             decoder {
    return BitmapEncoder::CreateForTranscodingAsync(
             stm, decoder);
    }).then([](BitmapEncoder^ encoder) {
      return encoder->FlushAsync();
    }).then([op]() {
      op->ReportCompleted();
    });
     });
  }
}

代码可能看起来很复杂,因为它试图将提供的图像保存到文件中,并且因为大多数操作都是异步的,所以涉及多个任务以确保操作按正确顺序执行。以下是执行的操作的快速摘要:

  • 检查确保数据包确实包含位图;在这种情况下有些多余,因为应用程序在清单中指示位图是唯一支持的数据。但在更复杂的情况下,这种检查可能会有用。

  • 使用DataPackageView::GetBitmapAsync提取位图,返回一个RandomAccessStreamReference对象。

  • 调用RandomAccessStreamReference::OpenReadAsync以获取图像数据作为IRandomAccessStream对象。使用此对象来实例化一个BitmapDecoder对象,该对象能够通过调用静态工厂方法BitmapDecoder::CreateAsync来解码图像位。

注意

BitmapDecoderBitmapEncoder类型位于Windows::Graphics::Imaging命名空间中。创建BitmapDecoder的工厂方法会自动识别存储的位图格式。

  • 一旦获得了结果解码器,就在图片库中创建一个名为SharedImage.jpg的新文件(KnownFolders::PicturesLibrary返回一个StorageFolder)。然后打开文件以进行读/写访问。

  • 基于解码器信息创建BitmapEncoderBitmapEncoder::CreateForTranscodingAsync),并通过调用BitmapEncoder::FlushAsync完成图像保存。

  • 最后要做的事情(对于任何共享操作)是通过调用ShareOperation::ReportComplete来向系统指示操作已完成。

共享文件

文本、URL 和图片并不是应用程序可以共享的唯一内容。文件也可以通过从源共享应用程序调用 DataPackage::SetStorageItems 来共享。这些存储项实际上可以是文件或文件夹(基于 IStorageItem 接口)。

在共享目标方面,可以使用 DataPackageView::GetStorageItemsAsync 方法来获取存储项,返回一个只读集合(IVectorView)的 IStorageItem 对象。然后应用程序可以以适合应用程序的任何方式访问这些文件/文件夹。

共享页面 UI 生成

Visual Studio 为共享目标操作提供了默认的页面模板。

Sharing page UI generation

这为共享添加了默认的 UI,包括用于数据绑定的默认 ViewModel

注意

如果应用程序项目是使用“空白应用程序”模板创建的,Visual Studio 将添加一些辅助类,这些辅助类存在于其他项目模板中,例如 SuspensionManagerLayoutAwarePage 等,因为它创建的共享页面派生自 LayoutAwarePage

FileOpenPicker 合同

FileOpenPicker 类用于从文件系统中选择文件——这是相当明显的;不那么明显的是,这个相同的 FileOpenPicker 可以用于从支持 FileOpenPicker 合同的任何应用程序获取文件。当应用程序调用 FileOpenPicker::PickSingleFileAsyncPickMultipleFilesAsync 时,将启动运行图像 PickerHost.exe 的托管进程,其中创建了 FileOpenPicker。除了我们可以看到的文件夹和文件列表外,还有应用程序:

FileOpenPicker contract

列出的应用程序(BingCamera 等)都实现了 FileOpenPicker 合同,因此可以联系它们以获取文件。例如,SkyDrive 允许浏览用户的文件并选择要下载的文件。相机应用程序提供了一个用户界面,允许在此处立即使用连接的相机拍照,或者在设备中嵌入拍摄,将生成的图像文件返回给调用应用程序。

实现 FileOpenPicker 合同

实现 FileOpenPicker 合同的第一步是在应用清单中声明这一点。这是必需的,因为实现应用程序可能在从其他应用程序打开 FileOpenPicker 时不在运行:

Implementing a FileOpenPicker contract

从图像中可以看出,应用程序可以支持任何文件类型,或一组预定义的文件类型,例如 .jpg.doc 等。这限制了被列在 FileOpenPicker 中的候选应用程序,取决于调用应用程序通过 FileOpenPicker::FileTypeFilter 属性指定的文件类型。

如果用户在 FileOpenPicker 中选择了应用程序,应用程序将被启动(如果尚未运行),并调用 Application::OnFileOpenPickerActivated 虚拟方法。这个想法与我们在本章前面看过的共享目标场景类似。

FileOpenPicker 窗口由一个带有应用程序名称的标题(这可以由应用程序自定义)和一个带有打开取消按钮的页脚构成。中间部分是应用程序特定的选择 UI 所在。

以下示例将 Flags 应用程序作为 FileOpenPicker 提供程序。该应用程序应提供旗帜的视图,允许在请求图像时进行选择。旗帜选择的用户界面构建如下:

<GridView ItemsSource="{Binding}" SelectionMode="Single" 
  x:Name="_gridFlags" Margin="10" 
     SelectionChanged="OnFlagSelected">
    <GridView.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="350" />
                </Grid.ColumnDefinitions>
                <Image Margin="10,0" Height="60" Width="100">
                    <Image.Source>
                        <BitmapImage UriSource="{Binding FlagUri}" />
                    </Image.Source>
                </Image>
                <TextBlock Text="{Binding CountryName}" FontSize="25" 
                Grid.Column="1" Margin="2" />
            </Grid>
        </DataTemplate>
    </GridView.ItemTemplate>
</GridView>

GridView 托管了旗帜的集合,绑定到 CountryInfo 类型的对象集合,定义如下:

[Windows::UI::Xaml::Data::BindableAttribute]
public ref class CountryInfo sealed {
public:
  property Platform::String^ CountryName;
  property Windows::Foundation::Uri^ FlagUri;
};

GridViewDataTemplate 使用这两个属性来显示旗帜的图像和相应的国家名称。

GridView 事件 SelectionChanged 被处理以提供 FileOpenPicker 文件进行选择或取消选择。在此之前,我们需要实现 Application::OnFileOpenPickerActivated 方法:

void App::OnFileOpenPickerActivated(
   FileOpenPickerActivatedEventArgs^ e) {
  auto picker = ref new FileOpenPickerPage();
  picker->Activate(e);
}

代码只是实例化我们自定义的FileOpenPickerPage类,并调用该页面上的一个特定于应用程序的方法,该方法名为Activate,并传递系统提供的激活信息。

前面的Activate方法几乎没有做什么:

void FileOpenPickerPage::Activate(
   FileOpenPickerActivatedEventArgs^ e) {
  _filePickerArgs = e;
  OnNavigatedTo(nullptr);
  Window::Current->Content = this;
  Window::Current->Activate();
}

FileOpenPickerActivatedEventArgs保存在_filePickerArgs字段中,以便在实际选择或取消选择标志时稍后使用。调用OnNavigatedTo设置所有标志数据,新页面成为Window的内容并被激活。

OnNavigatedTo执行以下操作:

void FileOpenPickerPage::OnNavigatedTo(NavigationEventArgs^ e) {
  auto countries = ref new Vector<CountryInfo^>;

  create_task(Package::Current
      ->InstalledLocation->GetFolderAsync("Assets\\Flags")).then(
   [](StorageFolder^ folder) {
    return folder->GetFilesAsync(
             CommonFileQuery::DefaultQuery);
  }).then(this, countries {
    std::for_each(begin(files), end(files), 
                               countries {
      auto info = ref new CountryInfo;
      info->FlagUri = ref new Uri(
               L"ms-appx:///Assets/Flags/" + file->Name);
      info->CountryName = MainPage::FlagUriToName(
               file->Name->Data());
      countries->Append(info);
    });
    DataContext = countries;
  });
}

标志图像文件从应用程序安装位置(ms-appx:方案)中检索,并且通过辅助方法FlagUriToName(未显示)从图像Uri中提取国家名称;国家集合得到更新,最后将DataContext设置为该集合。

应用程序部署后(使用完整构建或在 Visual Studio 菜单中选择构建 | 部署解决方案),我们可以通过启动另一个应用程序进行基本的选择器测试,例如在第一章中演示的简单图像查看器,Windows 8 应用程序简介。单击打开图像按钮时,Flags 应用程序将显示在选择器的自定义应用程序中:

实现 FileOpenPicker 合同

如果选择了 Flags 应用程序,则显示如下内容:

实现 FileOpenPicker 合同

此时,选择任何标志都不起作用——打开按钮仍然被禁用。我们需要告诉FileOpenPicker文件已被选中。这由GridViewSelectionChanged事件处理:

void FileOpenPickerPage::OnFlagSelected(Object^ sender, 
   SelectionChangedEventArgs^ e) {
  if(_gridFlags->SelectedIndex < 0 && _currentFile != nullptr) {
     _filePickerArgs->FileOpenPickerUI->RemoveFile(
        _currentFile);
     _currentFile = nullptr;
  }
  else {
     auto flag = (CountryInfo^)_gridFlags->SelectedItem;
     create_task(StorageFile::GetFileFromApplicationUriAsync(
         flag->FlagUri)).then(this, flag {
        AddFileResult result = 
_filePickerArgs->FileOpenPickerUI->AddFile(
         _currentFile = flag->CountryName, file);
       // can check result of adding the file
     });
  }
}

该类使用_currentFile字段跟踪当前选定的文件。如果在GridView中没有选择任何内容,并且以前选择了文件,则调用FileOpenPickerUI::RemoveFile方法以指示应该从选择中删除此文件;如果这是最后一个被选择的文件,则FileOpenPicker会禁用打开按钮。

如果选择了标志(GridView::SelectedIndex为零或更大),则通过调用静态的StorageFile::GetFileFromApplicationUriAsync方法获取标志图像的文件,并传递给FileOpenPickerUI::AddFile。结果(AddFileResult枚举)指示是否成功。如果FileOpenPicker没有考虑到该文件类型而打开,则可能会失败。例如,在未指定 GIF 文件扩展名的图像查看器中,添加将失败,因为所有标志图像都是以 GIF 格式保存的。

注意

这里提供的简单代码有点太简单了。没有处理的一件事是多选。GridView配置为仅使用单选,但实际上应根据打开FileOpenPicker的方式进行配置。此信息可在FileOpenPickerUI::SelectionMode属性中获得(SingleMultiple)。

如果按预期使用,SelectionChanged事件处理程序应使用SelectionChangedEventArgs对象的AddedItemsRemovedItems属性来管理选择和取消选择。

请注意,与共享目标一样,Visual Studio 为FileOpenPicker合同提供了页面模板。

调试合同

起初,调试合同可能看起来很困难,因为应用程序可能在运行时不会运行,因此设置断点不会自动将 Visual Studio 的调试器附加到启动的实例。可以通过以下两种方式轻松处理:

  • 在从文件选择器中选择特定应用程序后,附加 Visual Studio 调试器。这足以在选择或取消选择时触发断点。

  • 像往常一样使用调试器运行应用程序,然后导航到显示文件选择器的另一个应用程序。如果选择了该应用程序,任何断点都会像往常一样被触发。

扩展

扩展是应用程序和操作系统之间的一种合同。扩展与合同类似,通过覆盖某些方法并提供操作系统期望的某些功能来实现。让我们看一个例子。

注意

在实践中,合同和扩展之间的区别并不重要。它们都有一个共同的重要特征:实现了 Windows 定义的一些功能,无论是用于应用程序之间的通信,还是应用程序与 Windows 之间的通信。

设置扩展

设置魅力打开了一个设置窗格;它的下部显示了标准的 Windows 自定义选项,如音量亮度电源等。设置窗格的上部可以被应用程序用来添加特定于应用程序的设置。

注意

虽然设置被记录为合同而不是扩展,但我觉得它是一个扩展,因为它不涉及另一个应用程序——只涉及用户。

例如,我们将为 Flags 应用程序添加一个设置扩展,允许用户以三种不同的大小查看旗帜。首先要做的是告诉系统应用程序有兴趣支持设置扩展:

SettingsPane::GetForCurrentView()->CommandsRequested += 
   ref new TypedEventHandler<SettingsPane^, 
   SettingsPaneCommandsRequestedEventArgs^>(
      this, &MainPage::OnCommandRequested);

这个调用注册了SettingsPane::CommandRequested事件,当用户打开设置窗格并且应用程序在前台时触发。

当事件被触发时,我们可以添加命令以在设置窗格中显示,就像这样:

void MainPage::OnCommandRequested(SettingsPane^ pane, 
SettingsPaneCommandsRequestedEventArgs^ e) {
  auto commands = e->Request->ApplicationCommands;
  commands->Append(
      ref new SettingsCommand("small", "Small Flag Size", 
      ref new UICommandInvokedHandler(
      this, &MainPage::OnFlagSize)));
  commands->Append(
      ref new SettingsCommand("medium", "Medium Flag Size", 
      ref new UICommandInvokedHandler(
       this, &MainPage::OnFlagSize)));
  commands->Append(
      ref new SettingsCommand("large", "Large Flag Size", 
      ref new UICommandInvokedHandler(
         this, &MainPage::OnFlagSize)));
}

SettingsCommand构造函数接受一个特定于应用程序的命令 ID,可以用于在一个公共处理程序中消除命令的歧义。第二个参数是要显示的文本,第三个是命令的处理程序。在这个例子中,所有命令都由相同的方法处理:

void MainPage::OnFlagSize(IUICommand^ command) {
  auto id = safe_cast<String^>(command->Id);
  if(id == "small") {
    ImageWidth = 60; ImageHeight = 40;
  }
  else if(id == "medium") {
    ImageWidth = 100; ImageHeight = 60;
  }
  else {
    ImageWidth = 150; ImageHeight = 100;
  }
}

IUICommand接口(在Windows::UI::Popups命名空间中)实际上是一个SettingsCommand对象,它目前是实现该接口的唯一类。它保存了命令的属性(IdLabelInvoked—按照这个顺序精确的参数传递给SettingsCommand)。

ImageWidthImageHeight是绑定到驱动旗帜图像外观的DataTemplate的属性。这是从 Flags 应用程序打开设置窗格时的外观:

设置扩展

注意

权限命令由系统提供,并列出应用程序需要的功能(如互联网连接、网络摄像头、图片库等)。

其他合同和扩展

一些其他合同和扩展,这里没有显示,包括:

  • 文件保存选择器 – 类似于文件打开选择器,但用于保存操作

  • 搜索 – 提供了一种让应用程序参与搜索的方式,提供用户可以用来激活应用程序的结果

  • 缓存文件更新程序 – 用于跟踪文件更改(例如 SkyDrive 使用)

  • 自动播放 – 允许应用程序在新设备插入到计算机时被列出

  • 文件激活 – 允许应用程序注册为处理文件类型

  • 游戏资源管理器 – 允许应用程序注册为游戏,为游戏考虑家庭安全功能提供了一种方式

总结

合同和扩展提供了应用程序与 Windows 和其他应用程序更好地集成的方式;例如,用户可以使用共享魅力通过共享数据,而不管是什么类型的应用程序。合同和扩展提供了用户体验的一致性,而不仅仅是编程模型。这使得应用程序更有用;它看起来好像已经投入了很多思考和关怀到这个应用程序中。总的来说,这使得应用程序更有可能被使用——这是构建应用程序时一个非常重要的目标。

在下一章(也是最后一章)中,我们将快速查看应用程序部署和 Windows 商店。

第九章:打包和 Windows 商店

前几章讨论了构建 Windows 商店应用程序的各种细节:从基本的 Windows Runtime 概念,通过构建用户界面,到使用独特的 Windows 8 功能(例如动态磁贴,合同)。剩下的就是构建您的应用程序,并最终将其提交到商店,以便每个人都可以享受它。

然而,商店有自己的规则和指南。应用程序经过认证过程,以确保它们具有高质量,并且将使用户受益。这里的“高质量”涵盖了许多细节,一些与质量体验直接相关(性能,触摸体验,流畅性等),一些更微妙(例如对网络波动的适当响应和遵守现代 UI 设计指南)。

Windows 商店应用程序为开发人员提供了许多机会。商店还没有饱和(像 iOS 和 Android 商店那样),因此应用程序更有可能被注意到。可以实现货币化 - 应用程序可能需要花钱,但还有其他模式:商店模式支持试用应用程序(具有各种过期机制),可以提供应用内购买,以便应用程序可以免费下载,但可以在应用程序内出售物品或服务;应用程序可以显示广告 - 并因为用户运行应用程序而获得报酬 - 尽管应用程序本身是免费的。

在本章中,我们将介绍 Windows 商店的应用程序打包,并讨论需要考虑的一些问题,以便应用程序成功通过认证。

应用程序清单

我们在前几章中已经多次遇到了应用程序清单。这是应用程序在执行之前所做的基本声明。一些重要的事项需要考虑:

  • 除了默认值之外,必须提供图像标志 - 默认图像将自动导致认证失败;必须提供所有图像标志,并且图像大小必须符合要求(不得缩放)。

  • 在功能选项卡中,只应选中所需的功能。很容易勾选几乎所有内容,但这会使应用程序不够安全,甚至可能导致认证失败 - 用户将不得不授权可能实际上未被使用的功能。

  • 可以提供支持的方向,省略一些对于特定应用程序可能毫无意义的方向。例如,游戏通常只能以特定方向(横向或纵向)运行。

  • 对于某些功能,隐私政策声明必须作为应用程序的一部分或通过网页链接提供。这应说明应用程序如何使用这些功能。需要隐私声明的示例包括互联网(客户端,服务器)和位置(GPS)。

方向的问题带来了一个更一般的问题 - 应用程序视图。除了明显的横向和纵向之外,应用程序(在横向模式下)还可以与另一个应用程序共享屏幕,处于捕捉或填充模式。

应用程序视图状态

应用程序可以以四种不同的方式查看:

  • 横向 - 屏幕宽度大于高度。

  • 纵向 - 屏幕高度大于宽度。

  • 捕捉 - 应用程序占据 320 像素的宽度,而另一个应用程序占据屏幕宽度的其余部分。只有在水平显示分辨率至少为 1366 像素时才可能实现这一点。

  • 填充 - 捕捉的“镜像”。应用程序占据大部分水平空间,为另一个应用程序留下 320 像素。

以下是两个应用程序处于捕捉/填充状态的屏幕截图:

应用程序视图状态

用户期望应用程序在捕捉模式下相应地改变其视图。在前面的屏幕截图中,新闻应用程序被捕捉,因此新闻文章显示为小项目,而不像在其他模式下那样显示完整文本。

让我们从上一章中的 Flags 应用程序开始,并根据其当前视图状态进行适当的显示。

实现视图状态更改

处理视图状态更改有几种方法。我们将采取一种简单、务实的方法。目前,我们的标志应用程序无论方向或“捕捉性”如何都呈现相同的视图。为了测试方向和视图,我们可以使用 SDK 提供的模拟器(除非我们碰巧有一个基于平板电脑的设备进行实际测试)。以下是模拟器中捕捉模式下应用程序的外观:

实现视图状态更改

显然,这不是最好的用户体验。每个标志旁边的文本太长,因此一次只能看到很少的标志,并且文本可能会被截断。更好的方法是只显示标志图像,而不显示国家名称。

系统会在活动页面上引发SizeChanged事件——这是我们可以处理并进行必要的视图更改的内容。首先,我们将使我们的GridViewItemTemplate更加灵活,通过将其绑定到一个额外的属性,根据需要在视图更改时进行更改。以下是完整的模板:

<DataTemplate>
    <Grid Width="{Binding ColumnWidth, ElementName=_page}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Image Margin="10,0" HorizontalAlignment="Left" 
  Height="{Binding ImageHeight, ElementName=_page}" 
  Width="{Binding ImageWidth, ElementName=_page}">
            <Image.Source>
                <BitmapImage UriSource="{Binding FlagUri}" />
            </Image.Source>
        </Image>
        <TextBlock Text="{Binding CountryName}" FontSize="25" 
            Grid.Column="1" Margin="2" />
    </Grid>
</DataTemplate>

更改在最顶层的Grid中进行——它的Width绑定到MainPage对象上的一个依赖属性(ColumnWidth)。

注意

在一个单独的实现了INotifyPropertyChangedViewModel中实现这一点会更加优雅,如第五章数据绑定中所讨论的。这里展示的方法更快,足以用于演示目的。

这个ColumnWidth属性会根据当前的视图状态进行更改。

注意

这个页面的标记同样适用于横向和纵向方向。有时,为了良好的方向支持,需要进行更大的改变。一些布局面板更适合两种方向,比如StackPanelGrid不适合这样做,除非它是一个非常简单的Grid。一个复杂的Grid在切换方向时可能需要进行重大改变。

SizeChanged事件在MainPage构造函数中注册如下:

SizeChanged += ref new SizeChangedEventHandler(
   this, &MainPage::OnSizeChanged);

处理程序只是调用一个辅助方法HandleSizeChanges

void MainPage::OnSizeChanged(Object^ sender, 
   SizeChangedEventArgs^ e) {
  HandleSizeChanges();
}

这个相同的辅助方法也被从OnNavigatedTo重写中调用,以确保在页面首次加载时调整视图。基本思想是检查Windows::UI::ViewManagement::ApplicationView::Value静态属性,并根据可能的值采取适当的操作:

void MainPage::HandleSizeChanges() {
  switch(ApplicationView::Value) {
  case ApplicationViewState::Filled:
  case ApplicationViewState::FullScreenLandscape:
    ColumnWidth = 300 + ImageWidth;
    break;

  case ApplicationViewState::FullScreenPortrait:
    ColumnWidth = 200 + ImageWidth;
    break;

  case ApplicationViewState::Snapped:
    ColumnWidth = ImageWidth;
    break;
  }
  // Force the GridView to re-evaluate its items
  auto ctx = DataContext;
  DataContext = nullptr;
  DataContext = ctx;
}

代码根据视图状态更改了绑定属性ColumnWidth,填充和横向视图以相同的方式处理,但它们可能有所不同。在纵向模式下,列宽较窄,因此可以在单个屏幕上显示更多的标志。在捕捉视图中,文本部分完全被消除,只留下图像。这是在捕捉视图中的结果:

实现视图状态更改

处理视图状态更改的另一种常见方法是使用ViewStateManager类。这允许在 XAML 中进行更改,而不需要代码,除了使用VisualStateManager::GoToState静态方法进行正确的视图状态更改。这种方法超出了本书的范围,但可以在网上找到许多这样的例子。

注意

如果没有其他意义,至少在“捕捉”视图中应用程序应该显示一些徽标图像或文本。否则,如果视图未准备好捕捉视图,应用程序可能无法通过认证。

打包和验证

一旦应用程序完成(或者至少被开发人员认为完成了),就该是打包并上传到商店的时候了。第一步应该是测试应用程序是否存在一些可能导致认证失败的错误,以便立即修复这些错误。

要开始,我们可以使用 Visual Studio 的项目 | 商店子菜单:

打包和验证

菜单允许开设开发人员账户,预留应用名称(必须是唯一的,并将被保留一年),以及执行其他一些操作(例如截取屏幕截图——至少需要一个);你可以在 Windows 商店应用的开发者门户网站上找到有关这些选项的信息。现在我们来看一下创建应用软件包选项。

对话框首先询问我们是否要创建一个要上传到商店的软件包。如果选择了,开发人员必须使用他/她的 Microsoft ID 进行签名,然后软件包将被构建并稍后上传。我们现在选择的路线:

打包和验证

点击下一步会显示一个对话框,允许选择要构建(和测试)的配置:

打包和验证

我们必须选择一个发布配置来创建和测试软件包。无论如何,测试调试配置都会失败。上传到商店的必须是发布版本,否则应用将无法通过认证。我们可以选择要为其创建软件包的所需架构。x86 和 ARM 是推荐的——ARM 是 Windows RT 设备上唯一可用的架构,因此应该得到支持。对于基于英特尔/AMD 的架构,x86 是一个不错的选择。

点击创建会按照所选配置构建项目,并显示以下对话框:

打包和验证

这显示了创建的软件包的位置,所有要上传的必需文件都在那里。对话框进一步建议启动Windows 应用认证工具WACK)对应用进行一些自动验证测试。

使用 Windows 应用认证工具

在商店中,运行 WACK 是第一步之一;这意味着如果应用未通过本地 WACK 测试,它肯定会在商店中未通过认证。一旦选择,将出现以下对话框:

使用 Windows 应用认证工具

验证过程需要几分钟时间,期间应用将自动启动和终止。最后,结果将显示为“通过”或“失败”通知,并附有为测试创建的报告链接(也可能出现警告,但它们不被视为失败)。

注意

Flags 应用未通过 WACK 测试,因为它没有替换默认图标。

如果应用程序通过了 WACK 测试,我们可以继续上传应用到商店。通过 WACK 并不意味着应用一定会通过商店认证。商店进行的测试比 WACK 多得多,包括与真人的手动测试;但通过 WACK 测试是一个很好的第一步。你绝对不应该在本地 WACK 测试未通过的情况下上传软件包。

注意

商店应用的完整要求列表可以在msdn.microsoft.com/en-us/library/windows/apps/hh694083.aspx找到。

总结

在本章中,我们看到了将应用打包并上传到商店的基本流程,并讨论了必须解决的一些问题,以便应用成功通过认证。鼓励读者查阅官方的微软认证指南,以获取完整的细节。

由于认证过程可能需要几天时间,最好在实际提交之前尽可能多地测试应用。使用 Windows 应用认证工具是提高成功认证机会的必要步骤。

微软希望商店中的应用具有高质量,并为用户提供真正的价值。这意味着应用必须表现得“好”,但这远远不够;内容应该引人入胜和有趣,以便用户一次又一次地返回应用——这对于另一本书来说是一个挑战。

posted @ 2024-05-15 15:27  绝不原创的飞龙  阅读(18)  评论(0编辑  收藏  举报