使用 C++ 创建你的第一个 Metro 风格应用
来源:互联网 发布:java递归算法 编辑:程序博客网 时间:2024/05/17 23:38
WindowsMetro 风格应用专门针对 Windows 8 Consumer Preview 所提供的用户体验量身定制。每个出色的 Metro 风格应用都遵循特定的设计准则,这使得此类应用与传统的桌面应用相比外观更漂亮、反应更灵敏、行为更直观。开始创建 Metro 风格应用之前,建议你先阅读新模型的设计理论。你可以在设计 Metro 风格的应用中找到详细信息。
在此,我们介绍了有助于你使用 C++ 开发 Metro 风格应用的基本代码和概念,此类应用的 UI 使用可扩展应用程序标记语言 (XAML) 定义。
如果你希望使用其他编程语言,请参阅:
使用 JavaScript 创建你的第一个 Metro 风格应用
使用 C# 或 Visual Basic 创建你的第一个 Metro 风格应用
目标
开始编写代码之前,我们先来看一看你在使用 C++ 构建 Metro 风格应用时会用到的一些功能和设计准则。了解 Microsoft Visual Studio 11 Express Beta for Windows 8 如何为设计和开发工作提供支持也将很有帮助。了解如何以及何时使用 Visual C++ 组件扩展 (C++/CX) 来简化面向 Windows 运行时编写代码的工作也十分重要。我们的示例应用是一个博客阅读器,用于下载和显示 RSS 2.0 或 Atom 1.0 源中的数据。
本文章旨在介绍你在自行创建应用时可以遵循的步骤。完成本课程后,你将能够使用 XAML 和 C++ 构建你自己的 Metro 风格应用。
C++ 桌面应用与 Metro 风格应用对比
如果你习惯使用 C++ 编写 Windows 桌面程序,你可能会发现 Metro 风格应用编程的某些方面与这十分类似,而其他一些方面则需要了解更多知识。
相同之处
你仍然可以使用 C++ 编写代码,并且你可以访问 STL、CRT 以及任何其他 C++ 库,所不同的是,你不能直接调用某些函数,例如与文件 I/O 有关的函数。
如果你习惯使用可视化设计器,你仍然可以使用这些设计器。如果你习惯手动编写 UI 代码,则可以手动编写 XAML 的代码。
你仍然可以创建使用 Windows 操作系统类型和你自己的自定义类型的应用。
你仍然可以使用 Visual Studio 调试器、探查器和其他开发工具。
你仍然可以创建使用 Visual C++ 编译器编译为原生机器代码的应用。使用 C++ 编写的 Metro 风格应用不能在受管运行时环境中执行。
新增内容
Metro 风格应用的设计准则与桌面应用的设计准则十分不同。设计的重点不再是窗口边框、标签和对话框等。内容才是最重要的。出色的 Metro 风格应用从最开始的规划阶段就严格遵循这些准则。有关详细信息,请参阅规划你的应用。
你将使用 XAML 定义整个 UI。在 Metro 风格应用中,UI 与核心程序逻辑之间的分离比在 Microsoft 基础类 (MFC) 或 Microsoft Win32 应用中更为清晰。你在代码文件中处理行为的同时,其他用户可以在 XAML 文件中处理 UI 的外观。
尽管 Win32 仍然可用于某些功能,但你将主要面向一个易于导航且面向对象的全新 API(即 Windows 运行时)进行编程。
使用 Windows 运行时对象时,通常你会使用 C++/CX,该语言会提供可用于创建和访问 Windows 运行时对象的特殊语法,并在创建和访问过程中支持 C++ 异常处理、委派、事件和动态创建对象的自动引用计数。使用 C++/CX 时,基础 COM 和 Windows 体系结构的详细信息几乎从应用代码中完全隐藏。但如果愿意,你可以使用 Windows 运行时 C++ 模板库直接面向 COM 界面编写程序。
你的应用可以支持一些新概念(例如,挂起、超级按钮和应用栏),以便为用户提供更有凝聚力的体验。
你的应用将编译为一个程序包,其中还包含有关你的应用所包含的类型、它使用的资源以及它需要的功能(文件访问、Internet 访问和相机访问等)的元数据。
在 Windows 应用商店中,你的应用通过一个验证流程确定为安全之后,即可面向无数潜在客户发布。
简单博客阅读器,第 1 部分
我们的示例应用是一个基本的博客阅读器,用于下载和显示 RSS 2.0 或 Atom 1.0 源中的数据。
我们将分为两部分介绍该示例。首先,我们将创建一个基本的、单页版本的博客阅读器,以便于我们重点了解使用 C++ 编写 Metro 风格应用程序的一些基础知识。 在第 2 部分中,我们将使用 Visual Studio 11 Express Beta for Windows 8 中的一些预定义 XAML 模板创建一个功能更为丰富的应用版本。
我们将从基础的开始:
如何在 Visual Studio 11 Express Beta for Windows 8 中创建 Metro 风格应用项目。
如何了解创建的各种项目文件。
如何了解 Visual C++ 组件扩展以及何时使用它们。
Visual Studio 提供以下内容:源文件管理;综合的构建、部署和启动支持;XAML、Visual Basic、C#、C++、图形和清单编辑;调试及其他功能。Visual Studio 有多种版本,我们使用的是 Visual Studio 11 Express Beta for Windows 8。你可以随 Microsoft Windows 软件开发工具包 (SDK) 一起免费下载该版本,这样你就具备了构建、打包和部署 Metro 风格应用所需的所有内容。
要开始创建应用,首先创建一个使用 C++ 的 Metro 风格应用项目。 此处,我们使用最基本的模板“空白应用程序”。
创建 Metro 风格应用项目
安装 Visual Studio 11 Express Beta for Windows 8。
在菜单栏上,选择“文件”>“新建”>“项目”。将打开“新建项目”对话框。该对话框应该如下所示:
在“已安装”窗格中,展开“Visual C++”。
选择“Windows Metro 风格”模板类型。
在中心窗格中,选择“空白应用程序”。
输入项目的名称。我们将其命名为“SimpleBlogReader”。
选择“确定”按钮。已创建项目文件。
在继续之前,让我们看一下项目文件。 在“解决方案资源管理器”窗格顶部,选择“显示所有文件”图标以显示“空白应用程序”模板创建的所有项目文件。 根据你指定的项目名称,你应该会看到类似下面的内容:
你编辑的文件
让我们先看一下你可以编辑的项目文件。基本上,其中包括直接位于项目文件夹中的任意文件。
App.xaml、BlankPage.xaml
代表应用对象和 UI 默认页面的 XAML 标记文件。你可以使用 Visual Studio 设计器、Microsoft Expression Blend 或其他 XAML 设计器工具修改这些文件。大部分修改将在 BlankPage.xaml 中完成。
App.xaml.h、App.xaml.cpp
BlankPage.xaml.h、BlankPage.xaml.cpp
Application和 BlankPage 类的用户可编辑标头和实现代码隐藏文件。这些类分别对应于 app.xaml 和 BlankPage.xaml 中的 XAML 树。BlankPage.xaml.h 和 BlankPage.xaml.cpp 文件是添加与本页相关的事件处理程序和其他自定义程序逻辑的位置。应用类中的成员变量的作用域为整个应用。页面类中的变量的作用域仅为该页面。 App.xaml 没有可视的设计平面,但你仍可以在设计器中使用文档大纲和属性检查器。
Package.appxmanifest
包含描述你的应用的元数据,例如,显示名称、描述、徽标和功能。单击此项目时,它将在“清单设计器”中打开。
*.png
默认徽标和初始屏幕图像,你可以将其替换为自己的图像。
pch.h、pch.cpp
典型的 C++ 预编译头文件。可以根据需要向 pch.h 文件中添加 #include 指令。
你不能修改的文件
当你从 Visual Studio 中的 *.xaml 页面导航时,XAML 设计器或编辑器将生成这些文件。它们将启用你所编写的用于引用 XAML 元素的代码隐藏文件。它们还将使代码隐藏文件中的 Microsoft IntelliSense 保持最新。其中一些文件位于 Common 子文件夹(不在图示中展开)。你可以看一下这些文件以大致了解分部类的工作方式、声明变量的位置,等等。这些文件也可以用于调试。但是不要修改这些文件, 因为在下一次构建应用或从 XAML 页面导航到其他位置时将覆盖你所做的所有更改。
App.xaml.g.h、App.xaml.g.cpp
App.xaml.g.cpp 包含应用的主要方法和一些关联的样本代码。App.xaml.g.h 包含使操作系统在运行时将 .xaml 文件加载进内存并创建对象图的代码。不要修改这些文件。
StandardStyles.xaml
包含用于定义 Metro 风格应用的外观和感觉的预定义项模板、样式和其他元素。不要修改已具备的样式和模板。但你可以基于它们创建自定义样式(使用 BasedOn 属性),或将它们复制并粘贴到其他页面中,为副本指定一个不同的名称,然后修改副本。
LayoutAwarePage.cpp、LayoutAwarePage.h、RichTextColumns.cpp、RichTextColumns.h, and so on
处理导航和布局的基础结构代码。
BlankPage.xaml.g.h、BlankPage.xaml.g.cpp
包含为 BlankPage 和 App 类自动生成的分部类定义,以及为每个具有 x:Name 属性的 XAML 元素生成的成员变量。不要修改这些文件。
XamlTypeInfo.g.h
由 XAML 编辑器生成的 C++ 文件,用于启用 Windows 运行时以识别和加载在应用中定义并在任何 XAML 文件中引用的任何自定义类型。不要修改此文件。
代码一览
在“解决方案资源管理器”中,打开 BlankPage.xaml 并在 XAML 编辑器窗格中查看标记。请注意一个包含 <Grid>
元素的 <Page>
元素。 现在打开 BlankPage.xaml.g.h。请注意一个名为 BlankPage
的类,它是从 System.UI.Xaml.Page 派生出的,并包含一个 System.IO.Xaml.Controls.Grid 成员变量。
标记中的每个元素类型都有一个相关联的 Windows 运行时类型。在向 XAML 添加元素时,Visual Studio 会生成 C++ 源代码,使你可以编写将这些元素作为 Windows 运行时类型进行引用的代码隐藏文件。 并非在 C++ 项目代码中表示所有元素;而是仅表示那些你明确引用的元素。
让我们返回到 BlankPage.xaml.g.h。 请注意 BlankPage
声明为 partial ref class
。
partial ref class BlankPage : public Windows::UI::Xaml::Controls::Page… {…}
partial和 ref 关键字显然不是 ISO 标准 C++。它们是专门用于创建 Windows 运行时类型实例的组件扩展。ref 关键字指示该类是一个 Windows 运行时引用类型;使用 ref 使你无需编写大量下一代 COM 代码。 在类成员声明中,请注意 Object^
变量和 Grid^
变量。 “^”符号为“尖帽号”,它表示“对象句柄”。在动态内存中创建 Windows 运行时类型时使用该符号,而不使用“*”。 你也可以使用 C++ auto 关键字;编译器将推断类型。
Grid^ grid = ref new Grid(); // or: auto grid = ref new Grid(); grid->Width = 600;
从最基本的意义上说,ref 类是一个 COM 对象,它实现了 IInspectable 接口,其生命期是通过智能指针管理的。 Windows 运行时定义一个语言无关的抽象二进制接口 (ABI),它使用机器码本地运行,而不是通过虚拟机。C++/CX 可实现面向该 ABI 以一种更类似现代 C++ 的方式编程,而不是类似旧式 COM 编程。C++/CX 专门用于创建和访问 Windows 运行时类型。 ref 类中不面向 Windows 运行时的库、模型和函数完全可以使用 ISO 标准 C++ 编写。 在同一函数中混合使用 C++ 和 C++/CX 十分常见。 它们都会编译为本机 C++。
partial 关键字指示编译器在另一个代码文件中继续声明该类。该文件是 BlankPage.xaml.h。 如果程序员需要向 BlankPage
类中添加变量或函数,可以在 BlankPage.xaml.h 和 BlankPage.xaml.cpp 中执行此操作。 如果 XAML 编辑器需要添加变量或其他样本代码,它将在 *.g.h 和 *.g.cpp 文件中执行此操作。虽然类定义包含两个部分,但在进行编码和编译时,它就像一个类一样。通常,你可以安全地忽略 *.g.* 文件。这是因为“解决方案资源管理器”默认隐藏这些文件。现在,我们已了解了幕后信息,如果“显示所有文件”仍处于启用状态,请选择该图标以将其禁用,以便你可以更轻松地查找要修改的文件。
注意 如果你出于个人偏好或由于开发环境的某些限制而无法使用 C++/CX,则可以使用标准 C++ 和 Windows 运行时 C++ 模板库直接面向 COM 界面进行编程。有关详细信息,请参阅 Windows 运行时 C++ 模板库。
指定应用功能
Metro 风格应用在安全容器中运行,该容器对文件系统、网络资源和硬件具有有限的访问权限。当用户从 Windows 应用商店安装应用时,Windows 会查看 Package.appxmanifest 文件中的元数据,以确定该应用需要哪些功能。例如,某个应用可能需要访问 Internet 中的数据、用户文档库中的文档,或用户的摄相机和麦克风。当应用安装完成后,它会向用户显示所需的功能,而用户必须授予相应的权限,然后它才能访问这些资 源。如果应用没有请求并收到所需资源的权限,则在运行时禁止其访问该资源。
在应用中添加基本 Internet 功能
在“解决方案资源管理器”中,打开 Package.appxmanifest。此时将在“应用程序清单设计器”中打开该文件。
选择“功能”选项卡。
选中“Internet(客户端)”复选框(如果尚未选中)。
关闭清单设计器。
指定某个功能时,它会在 Package.appxmanifest.xml 文件中的 Capabilities
元素下列出。通常使用“应用程序清单设计器”来设置功能,但如果使用“XML 文本编辑器”打开 Package.appxmanifest.xml,你将可以看到 XML 中的 Capabilities
元素。
<Capabilities> <Capability Name="internetClient" /> </Capabilities>
将数据导入应用
在此部分中,我们介绍了:
如何创建自定义数据类。
如何异步检索 RSS 或 Atom 数据源。
由于我们的应用正确地要求 Internet 客户端访问,我们可以编写代码来将博客源获取到应用中。“Developing for Windows”(Windows 开发)博客分别以 RSS 和 Atom 两种形式显示其文章的全文。我们希望显示每篇最新博客文章的标题、作者、日期和内容。 我们可以使用 Windows.Web.Syndication 命名空间中的类来下载这些源。尽管我们也可以使用这些类显示 UI 中的数据,但我们将创建自己的数据类。这为我们提供了更大的灵活性,使我们可以按相同的方式处理 RSS 和 Atom 源。我们创建以下两个类:
FeedData 包含有关 RSS 或 Atom 源的信息。
FeedItem 包含有关源中的各篇博客文章的信息。
我们将这些类定义为公共 ref 类,以启用与显示标题、作者等 XAML 元素的数据绑定。我们使用Bindable特性指定到 XAML 编译器,此编译器动态绑定到这些类型的实例。在 ref 类中,公共数据成员公开为属性。没有特殊逻辑的属性不需要用户指定的 getter 和 setter;编译器将提供他们。在 FeedData 类中,注意我们如何使用 IVector<T> 将公共集合类型公开给其他 Windows 运行时类和组件。我们还在内部使用 Vector<T> 类作为实现 IVector 的具体类型。之后,我们将学习如何使用此类型。
创建自定义数据类
在“解决方案资源管理器”中,在 SimpleBlogReader 项目节点的快捷方式菜单上,选择“添加” > “新项目”。
从选项列表中选择 Header File (.h) 并命名为 FeedData.h。 (为了方便,我们不在此示例中使用单独的 .cpp 文件。)
将下列代码复制并粘贴到此文件中。用点时间看看此代码,自行熟悉 C++/CX 构造。请注意 collection.h 的 #include 指令,它对于具体的 Platform::Collections::Vector 类型是必需的。
C++#pragma once#include "pch.h" #include <collection.h>namespace SimpleBlogReader{ // To be bindable, a class must be defined within a namespace // and a bindable attribute needs to be applied. [Windows::UI::Xaml::Data::Bindable] public ref class FeedItem sealed { public: FeedItem(void){} ~FeedItem(void){} property Platform::String^ Title; property Platform::String^ Author; property Platform::String^ Content; // Temporary workaround (use Object^ not DateTime): // property Windows::Foundation::DateTime PubDate; property Platform::Object^ PubDate; }; [Windows::UI::Xaml::Data::Bindable] public ref class FeedData sealed { public: FeedData(void) { m_items = ref new Platform::Collections::Vector<Platform::Object^>(); } ~FeedData(void){} property Platform::String^ Title; property Windows::Foundation::Collections::IVector<Platform::Object^>^ Items { Windows::Foundation::Collections::IVector<Platform::Object^>^ get() {return m_items; } } private: Platform::Collections::Vector<Platform::Object^>^ m_items; }; }
C++ 中的异步操作:检索源数据
我们已具备了数据类,现在即可实现 GetFeedData 函数来下载博客源。 Windows.Web.Syndication.SyndicationClient 类用于检索和分析 RSS 和 Atom 源。由于此操作涉及网络 I/O,因此,将异步执行该方法。异步编程模型可在 Windows 运行时类库中找到。异步方法调用会立即向 UI 会话返回控件,从而使 UI 能够在后台线程上执行操作时保持反应灵敏。
Windows 运行时提供了一种调用异步操作并在操作完成时获取结果的方法;你可以直接面向该 API 编程。但首选方法是使用 ppltasks.h 中定义的 task class 类。该 task 类使用相同的 Windows 运行时 API,但你可以使用它来编写更为简明的代码,更便于形成异步操作链并在一个位置处理发生的任何异常。 在使用 task 类时,基本步骤始终是相同的:
通过调用 Windows 运行时 *Async 方法(如 Windows::Web::Syndication::ISyndicationClient::RetrieveFeedAsync)来创建异步操作。
将操作作为输入参数以创建 task 对象。
调用 task::then 并指定将操作返回值作为输入的 lambda。
可以选择再次调用 then 一次或多次。这些子句可以接受上一子句的返回值。
可以选择提供 final then 子句,以处理在操作链中的任意位置引发的任何异常。
添加异步下载功能
- 将这些行添加到BlankPage.xaml.h:C++
#include "FeedData.h" ...// In the BlankPage class... private: void GetFeedData(Platform::String^ feedUriString); FeedData^ feedData;
- 将这些行添加到BlankPage.xaml.cpp:C++
// BlankPage.xaml.cpp #include <ppltasks.h>...using namespace Windows::Web::Syndication;using namespace Concurrency;
- 调用 InitializeComponent 后,将此行添加到 BlankPage 构造函数中:C++
feedData = ref new FeedData();
- 将方法实现添加到 BlankPage.xaml.cpp 中。有关此代码的详细信息,请参阅 C++ 中的异步编程。C++
void BlankPage::GetFeedData(Platform::String^ feedUriString){ // Create the SyndicationClient and the target uri SyndicationClient^ client = ref new SyndicationClient(); Uri^ feedUri = ref new Uri(feedUriString); // Create the async operation. feedOp is an // IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^ auto feedOp = client->RetrieveFeedAsync(feedUri); feedOp = client->RetrieveFeedAsync(feedUri); // Create the task object and pass it the async operation. // SyndicationFeed^ is the type of the return value // that the feedOp operation will eventually produce. task<SyndicationFeed^> createFeedTask(feedOp); // Create a "continuation" that will run when the first task completes. // The continuation takes the return value of the first operation, // and then defines its own asynchronous operation by using a lambda. createFeedTask.then([this] (SyndicationFeed^ feed) -> SyndicationFeed^ { // Get the title of the feed (not the individual posts). feedData->Title = feed ->Title->ToString(); // Retrieve the individual posts from the feed. auto feedItems = feed->Items; // Iterate over the posts. You could also use // std::for_each( begin(feedItems), end(feedItems), // [this, feed] (SyndicationItem^ item) for(int i = 0; i < (int)feedItems->Size; i++) { auto item = feedItems->GetAt(i); FeedItem^ feedItem = ref new FeedItem(); feedItem->Title = item->Title->Text; // Temporary workaround: // feedItem->PubDate = item->PublishedDate; feedItem->PubDate = ref new Platform::Box<Windows::Foundation::DateTime>(item->PublishedDate); feedItem->Author = item->Authors->GetAt(0)->Name; if (feed->SourceFormat == SyndicationFormat::Atom10) { feedItem->Content = item->Content->Text; } else if (feed->SourceFormat == SyndicationFormat::Rss20) { feedItem->Content = item->Summary->Text; } feedData->Items->Append((Object^)feedItem); } this->DataContext = feedData; return feed; }).then ([] (task<SyndicationFeed^> t) { // Handle any exceptions that were raised // in the chain of operations. try { auto f = t.get(); } catch (std::exception e) { //Handle exception } });}
请注意,我们在完成
feedData
对象的填充后调用了this->DataContext = feedData
。我们必须将feedData
实例作为页面的DataContext
,以便可以将 UI 绑定到该实例。将 FeedData 作为数据上下文,我们可以将{Binding Path="Title"}
写入 XAML 标记,在启动时加载 XAML 页面并构造对象图形时,加载程序知道 “Title” 是 FeedData 实例上的 Title 属性。在此演练的第 2 部分,我们会演示可创建多个 FeedData 对象的更复杂的异步操作链。
在我们的应用启动时,我们希望其自动加载“Developing for Windows”(Windows 开发)博客。要执行此操作,最佳办法是响应通知页面加载已完成的 Loaded 事件。在方法调用中,我们将传入 Atom 源的 URL,因为作者数据包含在该源中,而不是包含在 RSS 源中。
处理 Loaded 事件
在 BlankPage.xaml 中,将语句
Loaded="PageLoadedHandler"
添加到起始Page
标记,它应紧跟在mc:Ignorable="d"
之后但位于右尖括号之前,从而使整个标记如下所示:XAML<Page x:Class="SimpleBlogReader.BlankPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:BlogReader" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Loaded="PageLoadedHandler">
将 C++ 方法签名添加到 BlankPage.xaml.h:
C++//In the BlankPage class declaration... private: void PageLoadedHandler(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
在你的 BlankPage.xaml.cpp 文件中为事件处理程序方法添加存根实现:
C++void BlankPage::PageLoadedHandler(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e){ GetFeedData("http://windowsteamblog.com/windows/b/developers/atom.aspx");}
在 XAML 中定义 UI
现在,让我们看一下如何:
直接在 XAML 中定义布局,而不使用设计器工具。
在网格中定义行和列。
为 XAML 元素创建成员变量。
创建 XAMLUI 的最简便和最强大的方式是使用 Visual Studio 所提供的模板之一,然后使用 Expression Blend 或 Visual Studio XAML 设计器等工具来进行自定义。但是,由于我们重点关注 XAML 自身的结构,因此我们将直接在 XAML 代码编辑器中操作。
通常 Metro 风格应用都包含多个页面,且每个页面都具有不同的布局。例如,博客阅读器可能具有一个用于浏览多篇博客文章并选择其中一篇的页面,以及另一个用于阅读选定 文章的页面。每个页面在其自己的代码文件中都是一个单独的 XAML 树。页面的典型根元素(至少从逻辑上讲)是 <Page>
。它所对应的 Windows 运行时类型为 Windows::UI::Xaml::Controls::Page。 Page 元素/类支持在应用中的页面之间进行基本导航。Page 将一个布局控件(或面板)作为其直接子元素。在布局控件内部,你可以放置内容控件(如 TextBlock和 ListView)来存放图片、文本等各个项。
XAML 布局系统支持绝对布局和动态布局。在绝对布局中,将使用明确的 x-y 坐标来定位控件;在动态布局中,你可以使布局容器和控件的大小和位置随应用大小的改变而自动改变。可以使用 Canvas 布局控件进行绝对定位,以及使用 Grid、StackPanel 和其他控件进行动态定位。实际上,你在定义应用的布局时,通常会结合使用绝对方法和动态方法,还可以将面板相互嵌套。
博客阅读器应用的典型布局是:顶部为标题,左侧是文章列表,右侧是选定文章的内容。下图说明了我们的布局的显示效果:
在我们创建项目时,系统为我们创建了一个名为 BlankPage.xaml 的文件。该文件具有一个 Page 元素,该元素将一个 Grid 元素作为子元素。XAML 应如下所示:
<Page x:Class="SimpleBlogReader.BlankPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Blog" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Loaded="PageLoadedHandler"> <Grid Background="{StaticResource PageBackgroundBrush}"></Grid> </Page>
x:Class 属性将此 Page 元素与 BlankPage 类关联起来,该类在 BlankPage.xaml.g.h 和 BlankPage.xaml.h 中声明。请记住我们添加了 feedData 成员变量。在 XAML 树中,xmlns 属性是 XML 命名空间;此处唯一需要关注的是 xmlns:local 命名空间,它将 FeedData 和 FeedItem 类纳入 XAML 页面的范围中,从而使我们可以在稍后将数据绑定到这些类。
若要开始我们的布局,为了方便,我们将“Name”属性添加到顶级网格:Name="Grid1"
。接下来,定义 Grid1的两行。首行显示源标题。在第二行中,让我们嵌入另一个 Grid,称为 Grid2,并将其分为两列。左列包含 ListView 控件,用于显示所有可用文章的标题、作者和日期。用户可以滚动查看该列表,然后选择一篇文章。右列包含第三个 Grid,Grid3,它在顶部行中包含 TextBlock,在底部行中包含 WebView 控件。文本块将显示博客文章的标题,WebView 将显示内容。
下面是简化的视图,其中显示了以前图片中的布局的基本结构。(不要粘贴此代码,因为它还没有完成。)
<!-- Pseudo-XAML Simplified View --> <Page> ... <Grid Name="Grid1"> <Grid.RowDefinitions... ... <!--In first row of Grid1.--> <TextBlock Grid.Row=”0”>…</TextBlock> ... <!--In second row of Grid1.--> <Grid Name="Grid2" Grid.Row=”1”> <Grid.ColumnDefinitions... <!-- In left column of Grid2. --> <ListView Grid.Column=”0”>…</ListView> <!-- In right column of Grid2. --> <Grid Name="Grid3" Grid.Column=”1”> <Grid.RowDefinitions... <TextBlock Grid.Row=”0”></TextBlock> <WebView Grid.Row=”1”/> </Grid> </Grid> </Grid> </Page>
现在,我们通过一次粘贴一部分代码来创建实际的 XAML。此练习可帮助我们了解如何构造 XAML 用户界面。
为我们的博客阅读器创建基本布局
- 在 BlankPage.xmal 中,为默认
Grid
元素添加一个Name
特性,使整个元素如下所示:XAML<Grid Background="{StaticResource ApplicationPageBackgroundBrush}" Name="Grid1">
在 BlankPage.xaml 中,为 Grid1 元素定义两行,方法是使用以下 XAML 片段作为 Grid 的第一个子节点,紧跟在起始标记之后。第一个行定义(第 0 行)中的
Height="140"
属性设置将顶行设置为具有绝对高度 140 像素。无论行的内容或应用的大小如何变化,此高度都不会改变。第二个行定义(第 1 行)中的Height="*"
设置指示底行接受第 0 行确定大小后剩余的任意大小的空间。这称为比例缩放。注意 只要你键入或粘贴代码,Visual Studio 就会自动提供正确的缩进。
XAML<Grid.RowDefinitions> <RowDefinition Height="140" /> <RowDefinition Height="*" /> </Grid.RowDefinitions>
紧跟在行定义后,但仍在 Grid1 内,向第一行中添加以下 TextBlock 内容控件。这将保留源的主标题,因此,我们可以为其指定较大的字体。 我们为其提供一个 x:Name 属性,以便我们可以在 C++ 代码中引用该属性,并且还会提供一个在将数据绑定到该属性后显示的临时字符串。
XAML<TextBlock x:Name="TitleText" Text="Main Title of Blog Feed" VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0"/>
紧跟在 TextBlock 后面,添加第二个 Grid 元素,并为它指定一个 Name 属性 Grid2。添加定义两列的子 Grid.ColumnDefinitions 元素。此处的 Grid.Row 属性引用 Grid1 行,因此,
Grid.Row="1"
表示“将此元素放在 Grid1 的第二行中”换句话说,我们在一个 Grid 中嵌入另一个 Grid。列宽度设置Width="2*"
和Width="3*"
要求 Grid2 将自身分为 5 个相等的部分。两个部分用于第一列,三个部分用于第二列。XAML<Grid Name="Grid2" Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="2*" /> <ColumnDefinition Width="3*" /> </Grid.ColumnDefinitions> </Grid>
紧跟在 Grid2 的列定义后面,在结束标记之前,添加下列 ListView 控件。由于未指定任何 Grid.Column 属性,因此,控件将放入 Grid2 的第 0 列中。我们暂且将内容留空。稍后我们将为其添加一些内容和一个事件处理程序
XAML<ListView x:Name="ItemListView"></ListView>
在选择 ListView 标记后,继续在 Grid2 元素内添加第三个 Grid,它包含两行。为它指定 Name 特性 Grid3,并放在 Grid2 的右列中。
Height="Auto"
设置要求顶行将其高度设置为与内容相同。底行则占用剩下的所有空间。XAML<Grid Name="Grid3" Grid.Column="1" > <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> </Grid>
紧靠前一个 RowDefinitions 后面,但在 Grid3 结束标记里面,添加 TextBlock 并为其提供一些临时文本。以后,我们将该 TextBlock 设置为显示 WebView 中所示的博客文章的标题。此控件不需要 x:Name 属性,因为我们不需要在 XAML 或在代码隐藏文件中引用它。 但不要担心;即使没有在 BlankPage 类中为此控件创建变量,也会在运行时实例化该控件并完全正常工作。
XAML<TextBlock Text="Blog Post Title" FontSize="24"/>
紧靠前一个 TextBlock 后面,添加一个 WebView 并将其放在 Grid 的底行中。此控件显示文章内容,包括图形。我们使用WebView而不是TextBlock或RichTextBlock,因为源内容的格式设置为 HTML。
XAML<WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/>
- XAML 树现在应如下所示:此时,你应该能够看到在设计器表面显示的用户界面。现在你也可以按 F5,查看到目前为止的显示效果。目前你还看不到任何数据,只能看到用户界面的基本大纲。按 F12,然后按 Shift-F5,返回代码编辑器。XAML
<Page x:Class="SimpleBlogReader.BlankPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:SimpleBlogReader" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Name="Grid1" Background="{StaticResource ApplicationPageBackgroundBrush}"> <Grid.RowDefinitions> <RowDefinition Height="140" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock x:Name="TitleText" Text="Main Title of Blog Feed" VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0"/> <Grid Name="Grid2" Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="2*" /> <ColumnDefinition Width="3*" /> </Grid.ColumnDefinitions> <ListView x:Name="ItemListView"></ListView> <Grid Name="Grid3" Grid.Column="1" > <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Text="Blog Post Title" FontSize="24"/> <WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/> </Grid> </Grid> </Grid> </Page>
设置 FeedItem 数据格式
基本布局已定义完成,现在我们为 ListView 项添加格式,你应该记得,这些项是 FeedItem 对象,我们在 FeedData.h 中进行了定义,在 GetFeedData 方法中进行了初始化,并将其插入了 FeedData::Items 集合中。我们希望该控件显示源中的每篇博客文章的标题、作者和发布日期。我们的想法是用户可以滚动查看这些项目,然后选择一个感兴趣的项目。在选择一个项时,右侧的 TextBlock 将使用较大字体显示文章标题,而 WebView 将显示内容。我们希望设置 ListView 项的显示格式,以使其如下所示:
要将三个 FeedItem 属性值合并为一个单元以进行显示,我们可以使用数据模板。数据模板可以定义一段或多段数据的“外观”,并将作为一个 XAML 节点实现。使用数据模板可以创建融合了文本、图形、动画以及其他 XAML 功能的新颖生动的信息表示。不过,我们只设置最基本的格式。与前面添加的标题一样,我们可以将每个属性值放在 TextBlock 中。我们可以使用每个 TextBlock 指定字体大小和其他格式属性,以及一个临时的 Text 值,稍后我们将替换该值。 要排列 TextBlock 元素,可以使用 StackPanel。StackPanel 是一个轻型布局面板,它在 XAML 中经常用于与此类似的小型布局场景。
创建源项布局
在 ListView 节点中,添加一个 ItemTemplate,它具有一个 DataTemplate 节点作为直接子节点。在类似 ListView 的控件中,DataTemplate 始终嵌套在 ItemTemplate 中。这指示控件将模板应用于其项目集合中的每个项目。
XAML<ListView.ItemTemplate> <DataTemplate> </DataTemplate> </ListView.ItemTemplate>
在 DataTemplate 中,添加一个包含三个 TextBlock 元素的 StackPanel,每个元素表示我们希望显示的三个 FeedItem 属性之一。 因为没有为 StackPanel 指定方向,TextBlock 元素将垂直排列。现在,我们仅为 Text 属性指定一些临时字符串,只是为了提醒我们它们表示的是什么内容。当你按 F5 时它们不会显示,因为这并不是实际的项,而只是展示项显示效果的模板。
XAML<StackPanel> <TextBlock Text="Post title" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" /> <TextBlock Text="Post author" FontSize="16" Margin="15,0,0,0"/> <TextBlock Text="Publication Date" FontSize="16" Margin="15,0,0,0"/> </StackPanel>
- ListView 的 XAML 现在应如下所示:XAML
<ListView x:Name="ItemListView"> <ListView.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="Post title" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" /> <TextBlock Text="Post author" FontSize="16" Margin="15,0,0,0"/> <TextBlock Text="Publication Date" FontSize="16" Margin="15,0,0,0"/> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView>
至此,我们已编写了一个用于从源下载实际数据的方法,还设计了一个显示一些临时值的 UI。下一步是添加 XAML 属性,以将实际源数据连接到 UI。这称为数据绑定。
显示数据
现在,我们看看如何将数据绑定到 UI,并使用值转换器将一个 DateTime 值转换为 String。
使用编程方式向控件中添加内容
在代码隐藏文件中,你可以通过编程方式将内容插入到控件中。例如,若要填充源标题 TextBox,我们可以在事件处理程序中编写以下代码 TitleText->Text = feedData->Title;
,这将使该文本立即更新到控件中。如果想要了解其工作方式,请注意,我们在 XAML 元素中指定了 x:Name 属性,如下所示: <TextBlock x:Name="TitleText" Text="Main Title of Blog Feed" …/>
。添加该 XAML 元素导致某个变量在 BlankPage.xaml.g.h 中声明:
// BlankPage.xaml.g.h -- Do Not Paste partial ref class BlankPage : public Windows::UI::Xaml::Controls::Page, public Windows::UI::Xaml::Markup::IComponentConnector{ ... Windows::UI::Xaml::Controls::TextBlock^ TitleText;)
...并在 BlankPage.xaml.g.cpp 中初始化:
// Get the TextBlock named 'TitleText' TitleText = safe_cast<Windows::UI::Xaml::Controls::TextBlock^> (static_cast<Windows::UI::Xaml::IFrameworkElement^>(this)->FindName("TitleText"));
具备该代码后,我们可以在 BlankPage.xaml.h 和 BlankPage.xaml.cpp 中 BlankPage 分部类的部分引用该初始化变量。
使用数据绑定向控件中添加内容
有时,在代码中动态设置 Text 属性即可奏效。但若要显示数据,通常使用数据绑定将数据源连接到 UI。建立绑定后,如果数据源发生更改,绑定到该数据源的 UI 元素可以自动反映更改内容。使用数据绑定,全部或几乎全部的代码都将在 XAML 文件中编写,而不是在代码隐藏文件中编写。数据绑定可以实现 View(或 ViewModel)与其他模块(如 Model 或 Controller)之间更为清晰的划分,这通常是向 XAML 控件中填充内容的建议方法。
绑定表达式
要将内容控件绑定到数据源,我们将为控件上的内容属性分配一个 {Binding } 表达式。对于 TextBlock,内容属性为Text。我们使用具有 Path
值的绑定表达式指示控件绑定到的内容。下面是我们用于 TitleTextTextBlock 的绑定表达式: Text="{Binding Path=Title}"
。 Path
值(此处为 Title
)的含义取决于数据上下文。
我们在代码隐藏文件的 BlankPage 构造函数中为整个 XAML 树动态设置默认数据上下文: this->DataContext = feedData;
。由于该语句,TextBlock 知道“Title”表示 feedData 实例上的 Title 属性。在该行代码中,“this”是在运行时从 XAML 树构造的 BlankPage 实例。设置 feedData 对象的数据上下文会使其成为此页面的整个对象树的默认数据上下文。 如有必要,我们可以覆盖各个元素的上下文。
将源标题绑定到 TitleTextTextBlock
修改 Text 特性以绑定到源标题。
XAML<TextBlock x:Name="TitleText" Text="{Binding Path=Title}" VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0"/>
将 ListView 项绑定到 dataFeed 的 Items 属性
要将列表视图连接到数据源,请将以下绑定表达式添加到 ItemListView:
ItemsSource="{Binding Path=Items}"
,以使起始标记现在如下所示:XAML<ListView x:Name=”ItemListView” ItemsSource=”{Binding Path=Items}” Margin=”10,0,0,10”>
现在,我们已将 ListView 控件绑定到 FeedData 项集合,我们可以绑定到作为 FeedItem 对象的各个项,但必须将其转换为 Platform::Object^ 类型才能在集合中存储它们。
将 DataTemplate 项绑定到FeedItem 属性
在第一个 TextBlock 中,使用
"{Binding Path=Title}"
替换临时文本,以使元素现在如下所示:XAML<TextBlock Text="{Binding Path=Title}" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" />
在第二个 TextBlock 中,使用
"{Binding Path=Author}"
替换临时文本,以使元素现在如下所示:XAML<TextBlock Text="{Binding Path=Author}" FontSize="16" Margin="15,0,0,0"/>
我们暂且跳过第三个文本框,因为我们需要提供一个自定义转换器来显示 DateTime 值。
让我们回想一下,在将新的 FeedItem^ 对象添加到 FeedData::Items 集合时,我们是否必须将其转换为 Platform::Object^?数据绑定如何知道这些对象是具有名为 “Title”、“Author” 和 “PubDate” 的属性的 FeedItem 对象?答案是,它既不知道,也不关心。它直接使用元数据在其集合中查找在对象上具有指定名称的属性。如果你指定的属性名称不存在,或者输错了名称,运行时结果很可能是空的 TextBox。由于运行时容易出现输入问题,因此,需要在编码时小心输入!
剩下的一个数据绑定 TextBlock 是 WebView 控件上方的控件。我们希望此 TextBlock 显示当前在 ListView 中选择的项目的标题。如果我们直接使用与 TitleText 控件相同的绑定,我们将显示相同的字符串,因为数据上下文和属性名称是相同的。为了更正这一问题,我们可以在 Grid 元素(作为 TextBlock 的直接父元素)中设置新数据上下文以覆盖页面上的默认 DataContext 属性。请注意,我们此处绑定到 XAML 元素上的属性名称,因此,我们使用 ElementName
属性指定要绑定到的元素。
将文章标题数据绑定到当前选定的项目
设置 Grid3 中的 DataContext 特性,使开始标记如下所示:
XAML<Grid Name ="Grid3" Grid.Column="1" DataContext="{Binding ElementName=ItemListView, Path=SelectedItem}">
使用绑定表达式
"{Binding Path=Title}"
替换 WebView 上方的 TextBlock 中的临时文本,以使元素现在如下所示:XAML<TextBlock Text="{Binding Path=Title}" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" />
由于我们在闭合网格元素内设置了新的数据上下文,在运行时,数据绑定机制将在当前选定的 FeedItem 而非DataFeed 对象上查找 Title 特性。
使用值转换器设置数据格式
在 ItemListViewDataTemplate 中,我们将 PubDate 属性(一个 DateTime)绑定到 TextBlock.Text属性。默认情况下,绑定引擎会将 PubDate 从一个 DateTime 转换为一个字符串。但自动转换仅生成 Windows::Foundation::DateTime 类型的名称,该名称没有提供详细信息。要生成实际日期,我们有两个选择:我们可以将 FeedItem::PubDate 类型更改为 Platform::String^ ,然后在初始化变量时进行转换,或者创建自定义值转换器并将其数据绑定到该转换器以便在运行时转换值。 我们选择后一种方法。
若要创建值转换器,我们需创建一个类,该类可实现 IValueConverter 接口,然后实现 Convert 方法并选择实现 ConvertBack 方法。转换器可以将数据从一种类型更改为另一种类型,根据文化背景转换数据,或者修改数据呈现方式的其他方面。此处,我们创建一个非常基本的日期转换器, 它可以转换传入的日期值并设置其格式,使其显示日期、月份和年份。(在此演练的第 2 部分,我们将创建一个功能更为丰富的日期转换器。)
创建实现 IValueConverter 的值转换器类
在菜单栏上,选择“项目”>“添加新项目”,然后选择“头文件”。将文件命名为 DateConverter.h,并向该文件中添加此类定义:
C++namespace SimpleBlogReader{public ref class DateConverter sealed : public Windows::UI::Xaml::Data::IValueConverter {public: virtual Platform::Object^ Convert(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ culture) { Windows::Foundation::DateTime dt = (Windows::Foundation::DateTime) value; Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ dtf = Windows::Globalization::DateTimeFormatting::DateTimeFormatter::LongDate::get(); return dtf->Format(dt); } virtual Platform::Object^ ConvertBack(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ culture) { //Not used. Left as exercise for the reader! throw ref new Platform::NotImplementedException(); } };}
- 将以下
#include
指令添加到 BlankPage.xaml.h::C++#include "DateConverter.h"
尽管在我们自己的代码隐藏中并未引用该文件,但我们仍需包含该文件,因为 Visual Studio 生成过程需要该文件来生成数据绑定代码。
在 BlankPage.xaml 中,将该类的一个实例声明为资源。将以下 Page.Resources 节点粘贴到开始 Page 标记和 Grid1 元素之间。
XAML<Page.Resources> <local:DateConverter x:Key="dateConverter" /> </Page.Resources>
Page 标记已具有一个 XML 命名空间映射,使我们可以访问项目中在 SimpleBlogReader 命名空间中声明的类:
xmlns:local="using:SimpleBlogReader"
。如果没有该映射,我们将无法在此处看到 DateConverter 类。- 现在我们可以将 PubDateTextBlock 绑定到 DateConverter:XAML
<TextBlock Text="{Binding Path=PubDate, Converter={StaticResource dateConverter}}" FontSize="16" Margin="15,0,0,0"/>
通过此 XAML,绑定引擎使用我们的自定义 DateConverter 将 DateTime 转换为一个字符串。 它返回的字符串按我们需要的方式格式化,只有日期、月份和年份。
在 WebView 中显示 HTML
若要在我们的应用中显示博客文章,我们必须获取要在 WebView 控件中显示的文章内容。 WebView 控件为我们提供了一种在我们的应用中托管 HTML 数据的方法。
我们将 WebView 添加到嵌套 Grid 的右列中,并为其提供 ContentView 的 x:Name,因为我们需要用于在我们的 BlankPage 类中引用的变量。
当我们查看 WebView 的 Source 属性时,将注意到它需要 URI 才能显示 Web 页面。我们的 HTML 数据只不过是HTML 的字符串。它没有包含可以绑定到 Source 属性的 URI。所幸的是,我们可以将自己的 FeedItem::Content 属性传递给 NavigateToString 方法。要实现该功能,我们处理 ListView 的 SelectionChanged 事件。
将 WebView 连接到选定项目的 FeedItem::Content 属性
在 XAML 文件中为 ListView 指定一个 SelectedChanged 事件处理程序。设置 SelectionChanged属性并指定属性名称以调用事件处理程序方法,以使起始标记现在如下所示:
XAML<ListView x:Name="ItemListView" ItemsSource="{Binding Path=Items}" SelectionChanged="ItemListView_SelectionChanged" Margin="10,0,0,10">
如同之前所创建的事件处理程序一样,现在我们必须在代码隐藏中创建事件处理程序。首先,将此行代码添加到 BlankPage.xaml.h:
C++// Declaration in BlankPage.xaml.h void ItemListView_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e);
在 BlankPage.xaml.cpp 中添加方法实现:
C++// Implementation in BlankPage.xaml.cpp void BlankPage::ItemListView_SelectionChanged (Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e){ FeedItem^ feedItem = safe_cast<FeedItem^>(ItemListView->SelectedItem); if (feedItem != nullptr) { // Navigate the WebView to the blog post content HTML string. ContentView->NavigateToString(feedItem->Content); }}
现在我们具有了一个基本的单页应用。如果按 F5,应显示如下图所示的一些内容。要中断应用,并返回到 Visual Studio IDE,请按 F12。
提示 为了获得更好的调试体验,请从公共 Microsoft 符号服务器下载调试符号。在主菜单上,选择“工具”,然后选择“选项”。在“选项”窗口中,展开“调试”,并选中“Microsoft 符号服务器”旁边的复选框。。第一次下载它们时可能需要花费一些时间。若要在下次按 F5 时获得更快的性能,请指定一个缓存符号的本地目录。
此处显示的是完整的 BlankPage.xaml 的 XAML 树。
<Page x:Class="SimpleBlogReader.BlankPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:SimpleBlogReader" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Loaded="PageLoadedHandler"> <Page.Resources> <local:DateConverter x:Key="dateConverter"/> </Page.Resources> <Grid Background="{StaticResource ApplicationPageBackgroundBrush}"> <Grid.RowDefinitions> <RowDefinition Height="140"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock x:Name="TitleText" Text="{Binding Path=Title}" VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0"/> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="2*"/> <ColumnDefinition Width="3*"/> </Grid.ColumnDefinitions> <ListView x:Name="ItemListView" ItemsSource="{Binding Path=Items}" Margin="10,0,0,10" SelectionChanged="ItemListView_SelectionChanged"> <ListView.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Path=Title}" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap"/> <TextBlock Text="{Binding Path=Author}" FontSize="16" Margin="15,0,0,0"/> <TextBlock Text="{Binding Path=PubDate, Converter={StaticResource dateConverter}}" FontSize="16" Margin="15,0,0,0" /> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> <Grid Grid.Column="1" DataContext="{Binding ElementName=ItemListView, Path=SelectedItem}"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock Text="{Binding Path=Title}" FontSize="24" Margin="5,0,0,0"/> <WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/> </Grid> </Grid> </Grid> </Page>
这是第 1 部分的结尾。现在,你已创建了一个适用于大多数情况的基本应用,并在该过程中学习了 XAML 的基础知识及其相关的代码隐藏文件。
简单博客阅读器,第 2 部分
我们已经了解了 XAML 和 C++/CX 的基础知识,现在来更加详细地了解一下 Metro 风格应用中包含的一些功能。首先,Metro 风格应用必须适用于所有情况。它必须适应各种设备上不同的分辨率、方向和视图。
你可以看到,我们的基本页面在以这种方式查看时显示不正常。我们希望我们的应用能够显示正常,同时能够更好地反映 Windows 团队博客的个性化内容。为实现这些目标,我们需要更复杂的页面,以及在页面间导航的方法。所幸的是,Visual Studio 提供了多个页面模板,可以实现许多我们需要的功能。为了升级应用,我们将放弃之前努力创建的空白页面,但将重复使用许多代码。更重要的是,我们在使用复杂 方法创建该页面过程中所获得的知识可以帮助我们更好地理解如何借助 Visual Studio 中提供的页面模板以简单的方式创建 Metro 风格应用。
添加页面和导航
如果要支持多个博客,我们必须向应用中添加一些页面,并处理这些页面间的导航。首先,我们需要一个列出所有 Windows 团队博客的页面;我们可以使用 Items Page 模板实现此目的。当读者从此页面中选择一个博客时,我们会将该博客的文章列表加载到另一个页面中。我们已经创建的 BlankPage.xaml 页面仍适用于此情况,但为了实现更好的效果,我们将使用 Visual Studio 中提供的 SplitPage 模板。我们还将添加一个详细信息页面,以使用户无需列表视图即可选择阅读各篇博客文章,从而节省空间。每个模板中都内置有丰富的导航支持。我们只需向每个 代码隐藏类中停用的 Navigate 和 OnNavigatedTo 方法中添加几行代码,并添加一个导航按钮以实现从拆分页面到详细信息页面的前进导航。完成所有操作后,我们即可从项目中排除原始的 BlankPage.xaml 及其相关的代码隐藏文件。
页面模板
Visual Studio 11 Express Beta for Windows 8 中包含许多页面模板,可以用于各种情况。以下是可用的页面模板。
组详细信息页面
显示一个组的详细信息以及组中各项的预览。
分组的项页面
显示分组的集合。
项详细信息页面
显示一个项的详细信息,并支持导航至相邻的项。
项页面
显示一组项。
拆分页面
显示项列表以及所选项的详细信息。
基本页面
具有布局意识、标题以及后退按钮的空页面。
空页面
Metro 风格应用的空页面。
向应用中添加新页面
在菜单栏上,选择“项目”>“添加新项目”。将打开“添加新项目”对话框。
在“已安装”窗格中,展开“Visual C++”。
选择“Windows Metro 风格”模板类型。
在中心窗格中,选择“项页面”并接受默认名称。
选择“添加”按钮。页面的 XAML 和代码隐藏文件现已添加到项目中。
重复步骤 4 和 5,但选择“拆分页面”。
重复步骤 4 和 5,但选择“基本页面”。将此页面命名为 "DetailPage"。
以下是“添加新项目”对话框。
“项 页面”将显示 Windows 团队博客的列表。“拆分页面”将在左侧显示每个博客的文章,在右侧显示选定文章的内容,这与我们之前创建的 BlankPage 类似。“基本页面”将仅显示选定文章的内容、“后退”按钮和页面标题。在此页面上,不会从 HTML 的某个字符串将文章内容加载到 WebView 中(就像在 SplitView 页面中执行的操作一样),而是导航到该文章的 URL 并显示实际的网页。执行此操作后,应用的页面将如下所示:
将页面模板添加到项目中并查看 XAML 和代码隐藏时,显然这些页面模板为我们完成了许多工作。事实上,起初可能容易迷惑,但了解每个页面模板包含三个主要部分将很有帮助:
资源
“资源”部分定义页面的样式和数据模板。我们将在使用样式创建一致性外观部分作进一步的介绍。
视觉状态管理器
“视觉状态管理器 (VSM)”中定义使应用适应不同布局和方向的动画和转换。我们将在适应不同布局部分作进一步介绍。
应用内容
构成应用 UI 的控件和内容在根布局面板中定义。
应用在页面之间导航时,它向数据传递一个指针,新页面将使用此指针来填充它的 UI。 因此,在添加导航代码之前,我们必须添加新数据模型的代码。我们将使用 Platform::Collections::Vector<Object^> 实例来存储 FeedData 对象,还将向创建数据模型的异步代码添加性能优化。我们会将此代码添加到 app.xaml.h 和 app.xaml.cpp,因为我们必须在应用启动时将 Vector 传递给 ItemsPage 实例。
修改数据类
将这两个属性添加到 FeedData.h 的 FeedData 类中(请注意,将 PubDate 声明为 Platform::Object^ 是一种临时的变通措施,在未来版本中没有必要这么做。):
C++property Platform::String^ Description;// Temporary workaround: // property Windows::Foundation::DateTime PubDate; Platform::Object^ PubDate;
某些源具有说明字段(如果我们还有空间可以显示),我们可以使用上一篇博客文章的日期作为某些布局中的
PubDate
。(请注意,PubDate
没有 ^(“尖帽号”),因为 DateTime 是一个值类型。)将此属性添加到 FeedItem 类。
C++property Windows::Foundation::Uri^ Link;
我们将使用详细信息页面中的此链接直接导航到博客文章,而不是导航到内容字符串。
修改初始化数据模型的异步代码
将此
#include
指令添加到 app.xaml.h:C++#include "FeedData.h"
将这两个专用方法签名添加到 app.xaml.h 中的 App 类:
C++void InitDataSource(Platform::Collections::Vector<Object^>^ fds);FeedData^ GetFeedData(Windows::Web::Syndication::SyndicationFeed^ feed);
将这些
#include
指令添加到app.xaml.cpp:C++#include <ppltasks.h>#include "ItemsPage.xaml.h"
将这些
namespace
声明添加到app.xaml.cpp:C++using namespace Platform::Collections;using namespace Windows::Web::Syndication;using namespace Concurrency;using namespace std;
将下列方法实现添加到 app.xaml.cpp:
C++void App::InitDataSource(Vector<Object^>^ fds){std::vector<std::wstring> urls; urls.push_back(L"http://windowsteamblog.com/windows/b/developers/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/windowsexperience/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/extremewindows/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/business/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/bloggingwindows/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/windowssecurity/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/springboard/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/windowshomeserver/atom.aspx");// There is no Atom feed for this blog, so we use the RSS feed. urls.push_back(L"http://windowsteamblog.com/windows_live/b/windowslive/rss.aspx");urls.push_back(L"http://windowsteamblog.com/windows_live/b/developer/atom.aspx");urls.push_back(L"http://windowsteamblog.com/ie/b/ie/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows_phone/b/wpdev/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx");SyndicationClient^ client = ref new SyndicationClient();std::for_each(std::begin(urls), std::end(urls), [=,this] (std::wstring url){// Create the async operation. // feedOp is an IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^ auto feedUri = ref new Uri(ref new String(url.c_str()));auto feedOp = client->RetrieveFeedAsync(feedUri);// Create the task object and pass it the async operation. // SyndicationFeed^ is the type of the return value // that the feedOp operation will eventually produce auto pOp = task<SyndicationFeed^>(feedOp)// Initialize a FeedData object with the feed info. Each // operation is independent and does not have to happen on the // UI thread. Therefore, we specify use_arbitrary. .then([this] (SyndicationFeed^ feed) -> FeedData^{return GetFeedData(feed);}, concurrency::task_continuation_context::use_arbitrary())// Append the initialized FeedData object to the list // that is the data source for the items collection. // This has to happen on the UI thread. By default, a .then // continuation runs in the same apartment thread that it was called on. // Because the actions will be synchronized for us, we can append // safely to the Vector without taking an explicit lock. .then([fds] (FeedData^ fd){fds->Append(fd);OutputDebugString(fd->Title->Data());})// The last continuation serves as an error handler. The // call to get() will surface any exceptions that were raised // at any point in the task chain. .then( [this] (concurrency::task<void> t){try {t.get();}catch(Platform::Exception^ e){//TODO handle error. OutputDebugString(e->Message->Data());}}); //end pOp task chain }); //end std::for_each }FeedData^ App::GetFeedData(SyndicationFeed^ feed){ FeedData^ feedData = ref new FeedData(); // Get the title of the feed (not the individual posts). feedData->Title = feed->Title->ToString(); if (feed->Subtitle->Text != nullptr) { feedData->Description = feed->Subtitle->Text; } // Use the date of the latest post as the last updated date. feedData->PubDate = ref new Platform::Box<Windows::Foundation::DateTime>(feed->Items->GetAt(0)->PublishedDate); // Construct a FeedItem object for each post in the feed. std::for_each( begin(feed->Items), end(feed->Items), [this, feed, feedData] (SyndicationItem^ item) { FeedItem^ feedItem = ref new FeedItem(); feedItem->Title = item->Title->Text; // Temporary workaround: // feedItem->PubDate = item->PublishedDate; feedItem->PubDate = ref new Platform::Box<Windows::Foundation::DateTime>(item->PublishedDate); //We only get first author in case of multiple entries. feedItem->Author = item->Authors->GetAt(0)->Name; if (feed->SourceFormat == SyndicationFormat::Atom10) { feedItem->Content = item->Content->Text; String^ s(L"http://windowsteamblog.com"); feedItem->Link = ref new Uri(s + item->Id); } else if (feed->SourceFormat == SyndicationFormat::Rss20) { feedItem->Content = item->Summary->Text; feedItem->Link = item->Links->GetAt(0)->Uri; } feedData->Items->Append((Object^)feedItem); }); return feedData;} //end GetFeedData
更适用于日期的 IValueConverter 类
现在正是修改 DateConverter 类的好时机,使之能够分别返回日期、月份和年份,而非将其作为一个字符串返回。在稍后为网格项和列表视图项定义的新风格中,我们需要利用这项功能。
修改 DateConverter 类
在 DateConverter.h 中,使用以下新实现替换现有的 DateConverter 类:
C++public ref class DateConverter sealed : public Windows::UI::Xaml::Data::IValueConverter { public: virtual Platform::Object^ Convert(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ culture) { if(value == nullptr) { throw ref new Platform::InvalidArgumentException(); } Windows::Foundation::DateTime dt = (Windows::Foundation::DateTime) value; Platform::String^ param = safe_cast<Platform::String^>(parameter); Platform::String^ result; if(param == nullptr) { Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ dtf = Windows::Globalization::DateTimeFormatting::DateTimeFormatter::ShortDate::get(); result = dtf->Format(dt); } else if(wcscmp(param->Data(), L"month") == 0) { Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ month = ref new Windows::Globalization::DateTimeFormatting::DateTimeFormatter("{month.abbreviated(3)}"); result = month->Format(dt); } else if(wcscmp(param->Data(), L"day") == 0) { Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ month = ref new Windows::Globalization::DateTimeFormatting::DateTimeFormatter("{day.integer(2)}"); result = month->Format(dt); } else if(wcscmp(param->Data(), L"year") == 0) { Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ month = ref new Windows::Globalization::DateTimeFormatting::DateTimeFormatter("{year.full}"); result = month->Format(dt); } else { // We don't handle other format types currently. throw ref new Platform::InvalidArgumentException(); } return result; } virtual Platform::Object^ ConvertBack(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ culture) { // Not needed in SimpleBlogReader. Left as an exercise. throw ref new Platform::NotImplementedException(); }};
- 为了使用这个类,我们在 App.xaml 的 ResourceDictionary 节点中,紧接 MergedDictionaries 结束标记添加一个对此类的引用:XAML
<local:DateConverter x:Key="dateConverter" />
在页面之间导航
XAMLUI 框架提供内置的导航模型,该模型使用 Frame 和 Page,并且其工作方式与在 Web 浏览器中的导航十分类似。Frame 控件可托管 Page,并且具有导航历史记录,你可以通过该历史记录在访问过的页面中前进和后退。在导航时,你可以在页面之间传递数据。
在 Visual Studio 项目模板中,一个名为rootFrame 的 Frame 被设置为应用窗口的内容。我们来看一下 App.xaml.cpp 中的默认代码。请注意,当应用启动时,显示的第一个页面将是在此事件处理程序中指定的页面。我们将立刻修改此代码以导航到我们的项页面。
// Default implementation. Not to be pasted into BlogReader. void App::OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs^ pArgs){ // Create a Frame to act as navigation context and navigate to the first page. auto rootFrame = ref new Frame(); TypeName pageType = { BlankPage::typeid->FullName, TypeKind::Metadata }; rootFrame->Navigate(pageType); // Place the frame in the current Window and ensure that it is active. Window::Current->Content = rootFrame; Window::Current->Activate();}
要在页面之间导航,你可以使用 Frame 控件的 Navigate、GoForward 和 GoBack 方法。通过使用 Navigate(TypeName, Object) 方法,可以导航并将数据对象传递到新页面。我们将使用该方法在我们的页面之间传递数据。 第一个参数 pageType 是我们将导航到的页面的 TypeName。我们使用静态 typeid 运算符来获取类型的 TypeName。
第二个参数是我们传递给将要导航到的页面的数据对象。在之前的单页版应用中,我们仅显示了一个博客源,因此我们传递了博客源集合中的第一个源。在此新版应用中,我们必须在不同时间传递三个对象:在应用启动时将 Vector 从 App 对象中的 rootFrame 传递到项页面;将选定 FeedData 对象从项页面传递到拆分页面;并将选定的 FeedItem 从拆分页面传递到详细信息页面。
从 App 类导航到项页面
在 App.xaml.cpp 中,使用下面这段代码替换 App::OnLaunched 方法实现,这将创建一个新的 FeedDataSource,并将其传递到项目页:
C++void App::OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs^ pArgs){ //Create the data source object. auto feedDataSource = ref new Vector<Object^>(); //Populate it asynchronously. InitDataSource(feedDataSource); // Create a Frame to act as navigation context and navigate to the first page, configuring the // new page by passing required information as a navigation parameter. auto rootFrame = ref new Frame(); TypeName pageType = { ItemsPage::typeid->FullName, TypeKind::Metadata }; rootFrame->Navigate(pageType, feedDataSource); // Place the frame in the current Window and ensure that it is active. Window::Current->Content = rootFrame; Window::Current->Activate();}
在 App::OnLaunched 中调用 Navigate 方法时,它最终将导致调用 ItemsPage::OnNavigatedTo 事件处理程序。 此处,Vector 与一个名为 “Items” 的键相关联,并被插入到类型为 Windows::Foundation::Collections::IObservableMap 的页面的 DefaultViewModel 成员中。 (每个页面都有自己的 DefaultViewModel。)在 ItemsPage.xaml.cpp 中已经为你生成了此代码:
C#void ItemsPage::OnNavigatedTo(NavigationEventArgs^ e){ DefaultViewModel->Insert("Items", e->Parameter);}
现在,按 F5 运行包含此更改的应用。请注意,尽管尚未对模板代码进行任何更改,但我们传递给 ItemsPage 的部分数据已经显示在网格区域中。
从项页面导航到拆分页面
用户从集合中选取一个博客后,我们将从项目页导航到拆分页面。要进行此导航,我们希望 GridView项目的表现类似按钮,而不是选择它们时所选的项目。为了使 GridView 项目像按钮一样响应,我们对 SelectionMode 和 IsItemClickEnabled 特性进行了设置,如下列示例所示。然后为 GridView 的 ItemClicked 事件添加了一个处理程序。在 ItemsPage.xaml 中找到
itemGridView
元素,使用以下标记取而代之:XAML<GridView x:Name="itemGridView" AutomationProperties.AutomationId="ItemsGridView" AutomationProperties.Name="Items" Margin="116,0,116,46" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" ItemTemplate="{StaticResource Standard250x250ItemTemplate}" SelectionMode="None" IsItemClickEnabled="True" ItemClick="ItemView_ItemClick" />
将这些指令添加到 ItemsPage.xaml.cpp:
C++#include "SplitPage.xaml.h" //... using namespace Windows::UI::Xaml::Interop;
将事件处理程序原型添加到 ItemsPage.xaml.h,并将实现添加到 ItemsPage.xaml.cpp:
C++//ItemsPage.xaml.h: virtual void ItemView_ItemClick(Object^ sender, Windows::UI::Xaml::Controls::ItemClickEventArgs^ e) override;void ItemsPage::ItemView_ItemClick(Object^ sender, ItemClickEventArgs^ e){ // Navigate to split page and pass the selected feed data. TypeName pageType = { SplitPage::typeid->FullName, TypeKind::Metadata }; this->Frame->Navigate(pageType, e->ClickedItem);}
项页面还包含一个名为 itemListView 的列表视图,如果调整了应用,则会显示该列表视图来代替网格。我们将在适应不同的布局部分中对此进行更详细的讨论。目前,我们只需对 ListView 进行与对 GridView 所做更改相同的更改,以确保它们的行为相同。在 ItemsPage.xaml 中找到 itemListView 并添加所需的属性,以使其如下所示:
XAML<ListView x:Name="itemListView" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" Margin="10,0,0,60" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" ItemTemplate="{StaticResource Standard80ItemTemplate}" SelectionMode="None" IsItemClickEnabled="True" ItemClick="ItemView_ItemClick"/>
现在,打开 SplitPage.xaml.cpp 并将下列行添加
SplitPage::OnNavigatedTo
方法的开头。C++FeedData^ fd = safe_cast<FeedData^>(e->Parameter);DefaultViewModel->Insert("Feed", fd);DefaultViewModel->Insert("Items", fd->Items);
注意 我们将 Items 属性分别插入到拆分页面的 DefaultViewModel 中,使 XAML 数据绑定可访问这些项。
导航返回项目页不需要额外的工作。模板包含处理 BackButton.Click 事件和调用 Frame.GoBack 方法的代码。
在此时运行应用时,请注意详细信息窗格中的博客文本将显示原始 HTML。若要修复此问题,我们需要更改用于选定博客文章的标题和内容的布局。 如果应用正在运行,请按 F12 中断应用,然后按 Shift-F5 停止调试并返回到 Visual Studio 代码编辑器。
修改拆分页面和项页面中的绑定和布局
在结束向添加到应用中的新页面添加功能之前,我们还必须进行几项更改。将此代码添加到应用后,即可开始设置应用的样式和动画。
由于在向 DefaultViewModel 中添加数据时使用了名为“Feed”的键,我们必须将页面标题中的绑定更改为绑定到 Feed 属性,而不是绑定到“Group”(默认设置)。在 SplitPage.xaml 中,更改名为 pageTitle 的 TextBlock 的文本绑定以绑定到 Feed.Title,如下所示:
XAML<TextBlock x:Name="pageTitle" Grid.Column="1" Text="{Binding Feed.Title}" Style="{StaticResource PageHeaderTextStyle}"/>
在 ItemsPage.xaml 中,页面标题被绑定到具有键 AppName 的静态资源。将此资源中的文本更新到 Windows 团队博客,如下所示:
XAML<x:String x:Key="AppName">Windows Team Blogs</x:String>
在 SplitPage.xaml 中,将名为 titlePanel 的 Grid 更改为跨 2 个列。
XAML<!-- Back button and page title --> <Grid x:Name="titlePanel" Grid.ColumnSpan="2">
向 SplitPage.xaml 中添加 WebView 控件
在 SplitPage.xaml 中,我们必须更改用于显示选定博客文章的标题和内容的布局。要执行此操作,需要将名为 itemDetail 的 ScrollViewer 替换为下列 ScrollViewer 布局。你应该已经认识到此 XAML 的大部分来源于我们之前在 BlankPage.xaml 中进行的工作。本文章的后续内容中将介绍 Rectangle 元素的用途。
XAML<!-- Details for selected item --> <ScrollViewer x:Name="itemDetail" AutomationProperties.AutomationId="ItemDetailScrollViewer" Grid.Column="1" Grid.Row="1" Padding="70,0,120,0" DataContext="{Binding SelectedItem, ElementName=itemListView}" Style="{StaticResource VerticalScrollViewerStyle}"> <Grid x:Name="itemDetailGrid"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock x:Name="itemTitle" Text="{Binding Title}" Style="{StaticResource SubheaderTextStyle}"/> <Border x:Name="contentViewBorder" BorderBrush="Gray" BorderThickness="2" Grid.Row="1" Margin="0,15,0,20"> <Grid> <WebView x:Name="contentView" /> <Rectangle x:Name="contentViewRect" /> </Grid> </Border> </Grid> </ScrollViewer>
在 SplitPage.xaml.cpp 中,修改当 ListView 选项更改时导致 WebView 更新的事件处理代码。ItemListView_SelectionChanged 函数签名和实现已经具备。我们只需添加以下行:
C++FeedItem^ fi = safe_cast<FeedItem^>(itemListView->SelectedItem);if(fi != nullptr) { contentView->NavigateToString(fi->Content);}
向 DetailPage.xaml 中添加 WebView 控件
在 DetailPage.xaml 中,我们必须将标题文本绑定到博客文章标题,并添加一个 WebView 控件来显示博客页面。要执行此操作,需要将包含返回按钮和页面标题的 Grid 替换为此 Grid 和 WebView:
XAML<!-- Back button and page title --> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Button x:Name="backButton" Click="GoBack" IsEnabled="{Binding Frame.CanGoBack, ElementName=pageRoot}" Style="{StaticResource BackButtonStyle}"/> <TextBlock x:Name="pageTitle" Grid.Column="1" Text="{Binding Title}" Style="{StaticResource PageHeaderTextStyle}"/> </Grid> <Border x:Name="contentViewBorder" BorderBrush="Gray" BorderThickness="2" Grid.Row="1" Margin="120,15,20,20"> <WebView x:Name="contentView" /> </Border>
在 DetailPage.xaml.cpp 中,向 OnNavigatedTo 方法重写中添加代码以导航到博客文章,并设置页面的 DataContext。更新后的方法如下所示:
C++void DetailPage::OnNavigatedTo(NavigationEventArgs^ e){ FeedItem^ feedItem = safe_cast<FeedItem^>(e->Parameter); if (feedItem != nullptr) { contentView->Navigate(feedItem->Link); this->DataContext = feedItem; }}
应用栏
博客阅读器应用中的大部分导航都是在用户从 UI 中选择某个项目时发生的。但在拆分页面上,我们必须提供一种方法,让用户能转到博客文章的详细信息视图。我们可以在页面上某个位置放置一个按钮,但这将干 扰核心应用体验,即阅读。因此,我们将按钮放在一个隐藏的应用栏中,该栏仅在用户需要时显示。 我们将添加一个应用栏,其中包含一个按钮,用于导航到详细信息页。
应用栏是 UI 的一部分,默认情况下是隐藏的,可在用户沿屏幕边缘轻扫、与应用互动或者单击鼠标右键时显示或消失。它可以向用户提供导航、命令和工具。应用栏可以显示在页面顶部、底部或同时显示在顶部和底部。 要在 XAML 中添加应用栏,我们需要将一个 AppBar 控件指定给 Page 的 TopAppBar 或 BottomAppBar 属性。
向拆分页面应用栏中添加按钮
StandardStyles.xaml 文件包含适用于常见场景的各种应用栏按钮样式。我们以这些样式为指导为我们的按钮创建样式。我们将样式放在 SplitPage.xaml 的
UserControl.Resources
部分:XAML<Style x:Key="WebViewAppBarButtonStyle" TargetType="Button" BasedOn="{StaticResource AppBarButtonStyle}"> <Setter Property="AutomationProperties.AutomationId" Value="WebViewAppBarButton"/> <Setter Property="AutomationProperties.Name" Value="View Web Page"/> <Setter Property="Content" Value=""/> </Style>
将此代码粘贴到 UserControl.Resources 节点之后,创建一个包含我们刚刚定义的按钮的顶部应用栏:
XAML<Page.TopAppBar> <AppBar Padding="10,0,10,0"> <Grid> <Button Click="ViewDetail_Click" HorizontalAlignment="Right" Style="{StaticResource WebViewAppBarButtonStyle}"/> </Grid> </AppBar> </Page.TopAppBar>
添加详细信息视图导航
将以下方法签名添加到 SplitPage.xaml.h 中的 SplitPage 类:
C++void ViewDetail_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
将以下
#include
指令和using
语句添加到 SplitPage.xaml.cpp 中:C++#include "DetailPage.xaml.h" ...using namespace Windows::UI::Xaml::Interop;
将此方法主体添加到 SplitPage.xaml.cpp 中:
C++void SplitPage::ViewDetail_Click(Object^ sender, RoutedEventArgs^ e){ // Navigate to the appropriate destination page, and configure the new page // by passing required information as a navigation parameter. TypeName pageType = { DetailPage::typeid->FullName, TypeKind::Metadata }; FeedItem^ fi = safe_cast<FeedItem^>(itemListView->SelectedItem); if(fi != nullptr) { Frame->Navigate(pageType, fi); }}
添加动画和过渡
当我们谈论动画时,通常会联想到屏幕上蹦蹦跳跳的物体。但在 XAML 中,动画实质上只是一种在对象上更改属性值的方法。这让动画具有多种用途,而不仅仅是一堆跳动的球。在我们的博客阅读器应用中,我们使用一些默认动画和转 换来使 UI 适应不同的布局和方向。我们可以在 Windows.UI.Xaml.Media.Animation 命名空间中找到它们。
添加主题动画
主题动画是一个预定义的动画,我们可以将其放在一个 Storyboard 中。在此,我们将 PopInThemeAnimation 放入 Storyboard,并使其成为 DetailPage.xaml 中的资源。因为返回按钮和标题在各个页面中均位于相同的位置,我们并不需要将它们弹入,所以我们将动画的目标设置为围绕在我们的 Web 内容周围的 Border。这样便会使 Border 和其中的所有内容具有动画效果。
向详细信息页面添加主题动画
将以下 XAML 片段粘贴到 DetailPage.xaml 中的
UserControl.Resources
节点:XAML<Storyboard x:Name="PopInStoryboard"> <PopInThemeAnimation Storyboard.TargetName="contentViewBorder" FromHorizontalOffset="400"/> </Storyboard>
将以下代码粘贴到 DetailPage.xaml.cpp 中的 DetailPage::OnNavigatedTo 方法的开头:
C++// Run the PopInThemeAnimation. Windows::UI::Xaml::Media::Animation::Storyboard^ sb = safe_cast<Windows::UI::Xaml::Media::Animation::Storyboard^>(this->FindName("PopInStoryboard"));if (sb != nullptr){ sb->Begin();} //... rest of method as before
添加主题转换
主题转换是一个完整的动画组和一个组合进预打包行为中的 Storyboard,我们可以将该行为附加到某个 UI 元素。 ContentThemeTransition 与 ContentControl 一起使用,并且会在控件内容发生更改时自动触发。
在我们的应用中,向在拆分页面列表视图中存放文章标题的 TextBlock 添加一个主题转换。当 TextBlock 的内容发生更改时,ContentThemeTransition 将自动触发并运行。动画是预先定义的,我们不需要执行任何操作来运行它。我们只需将其附加到 TextBlock 中即可。
向 SplitPage.xaml 添加主题转换
在 SplitPage.xaml 中,名为
pageTitle
的 TextBlock 是一个空元素标记。为了添加主题转换,我们将其嵌入到 TextBlock 中,因此需要更改 TextBlock,使之包含开始和结束标记。使用以下 XAML 节点替换现有标记:XAML<TextBlock x:Name="pageTitle" Grid.Column="1" Text="{Binding Feed.Title}" Style="{StaticResource PageHeaderTextStyle}"> <TextBlock.Transitions> <TransitionCollection> <ContentThemeTransition /> </TransitionCollection> </TextBlock.Transitions> </TextBlock>
当 TextBlock 的内容发生更改时,ContentThemeTransition 将自动触发并运行。动画是预先定义的,我们不需要执行任何操作来运行它。我们只需将其附加到 TextBlock 中即可。有关详细信息以及主题动画和过渡的列表,请参阅快速入门:动画。
使用样式创建一致性外观
我们希望让博客阅读器应用的外观和感觉类似于 Windows 团队博客网站。我们希望用户在该网站和我们的应用之间切换时能够拥有无缝的使用体验。我们的 Windows Metro 风格 UI 的默认深色主题与 Windows 团队博客网站不太匹配。这在详细信息页面上尤为明显,在该页面上我们会将实际的博客页面加载到一个中WebView,如下所示:
要使我们的应用具有可根据需要进行更新的一致外观,可使用画笔和样式。使用 Brush,我们可以在一个位置定义外观,然后在任意需要的位置使用它。使用 Style,我们可以为控件的属性设置值,并在应用中重复使用这些设置。
在深入了解详细信息之前,我们先来看一下如何使用画笔设置应用中页面的背景色。应用中的每个页面都有一个根 Grid,该根的一个 Background 属性已设置为定义页面的背景色。我们可以按如下所示单独设置每个页面的背景:
<Grid Background="Blue">
但是,更好的方法是定义一个 Brush 作为资源,并使用它来定义所有页面的背景色。下面显示了在 Microsoft Visual Studio 模板中如何执行此操作:
<Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
我们将对象和值定义为资源,以使其可以重复使用。要将对象或值用作资源,我们必须设置其 x:Key 属性。我们使用此键来从 XAML 中引用资源。此处,背景被设置为具有键 ApplicationPageBackgroundBrush
的资源,该键是在 StandardStyles.xaml 文件中定义的 SolidColorBrush。
要更改页面的背景,我们在 App.xaml 中定义一个具有相同键 ApplicationPageBackgroundBrush
的新 SolidColorBrush。对于此新画笔,我们设置 Color #FF0A2562,这是一种与 Windows 团队博客网站十分相称的合适的蓝色。
覆盖默认页面背景画笔
将此画笔添加到 App.xaml 中的资源字典:
XAML<SolidColorBrush x:Key="WindowsBlogBackgroundBrush" Color="#FF0A2562"/>
将各页中的根 Grid 元素修改为使用新画笔:
XAML// Change the Background of the root Grid in each xaml page in the app.<Grid Background="{StaticResource WindowsBlogBackgroundBrush}">
你可以在 XAML 文件中为单独的页面定义资源,也可以在 App.xaml 文件中,或在单独的资源字典 XAML 文件,如 StandardStyles.xaml 中定义。定义资源的位置决定了该资源可以使用的范围。Visual Studio 将 StandardStyles.xaml 文件创建为项目模板的一部分,并将其放在 Common 文件夹中。它是一个包含 Visual Studio 页面模板中所使用的值、样式和数据模板的资源字典。可以在多个应用之间共享一个资源字典 XAML 文件,也可以将多个资源字典合并进单个应用中。
在我们的博客阅读器应用中,我们在 App.xaml 中定义资源,以使其可以在整个应用中可用。还有一些资源在各个页面的 XAML 文件中定义。这些资源只在定义了它们的页面中可用。如果在 App.xaml 和页面中同时定义了具有相同键的资源,则页面中的资源将覆盖 App.xaml 中的资源。同样地,在 App.xaml 中定义的资源将覆盖在单独的资源字典文件中定义的具有相同键的资源。有关详细信息,请参阅快速入门:设置控件样式。
下面,我们来看一下如何在应用中使用 Style。我们的博客阅读器 UI 中的大多数文本都很相似,只有大小有所不同。文本的默认外观由 StandardStyles.xaml 中的此 Style 定义:
...<SolidColorBrush x:Key="ApplicationTextBrush" Color="#DEFFFFFF"/> ...<x:Double x:Key="ContentFontSize">14.667</x:Double> <FontFamily x:Key="ContentFontFamily">Segoe UI</FontFamily> ...<Style x:Key="BasicTextStyle" TargetType="TextBlock"> <Setter Property="Foreground" Value="{StaticResource ApplicationTextBrush}"/> <Setter Property="FontSize" Value="{StaticResource ContentFontSize}"/> <Setter Property="FontFamily" Value="{StaticResource ContentFontFamily}"/> <Setter Property="TextTrimming" Value="WordEllipsis"/> <Setter Property="TextWrapping" Value="Wrap"/> <Setter Property="Typography.StylisticSet20" Value="True"/> <Setter Property="Typography.DiscretionaryLigatures" Value="True"/> </Style>
在 Style 定义中,我们需要一个 TargetType 属性和一个或多个 Setter 的集合。我们将 TargetType 设置为一个指定 Style 将应用到的类型的字符串,在此例中为 TextBlock。如果你试图将某个 Style 应用到与 TargetType 属性不匹配的控件,就会发生异常。每个 Setter 元素都需要一个 Property 和一个 Value。这些属性设置用于指示该设置将应用于哪个控件属性,以及为该属性设置的值。
为 ItemsPage.xaml 添加风格
在 ItemsPage.xaml 中,我们希望网格框中的文本具有默认的外观,但我们希望 FontSize 更大一些。我们可以通过在本地进行设置来改写 FontSize,如下所示:
<TextBlock Text="{Binding Title}" Style="{StaticResource BasicTextStyle}" FontSize="26.667" />
。但还有一种更好的方法:创建一个基于 BasicTextStyle的 Style,并更改其中的 FontSize。 在 ItemsPage.xaml 的 UserControl.Resources 部分中,添加以下风格定义。XAML<!-- light blue --> <SolidColorBrush x:Key="BlockBackgroundBrush" Color="#FF557EB9"/> <!-- Grid Styles --> <Style x:Key="GridTitleTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BasicTextStyle}"> <Setter Property="FontSize" Value="26.667"/> <Setter Property="Margin" Value="12,0,12,2"/> </Style> <Style x:Key="GridDescriptionTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BasicTextStyle}"> <Setter Property="VerticalAlignment" Value="Bottom"/> <Setter Property="Margin" Value="12,0,12,60"/> </Style>
BasedOn="{StaticResource BasicTextStyle}"
行表示新的 Style 从 BasicTextStyle 继承我们没有明确设置的所有属性。我们可以应用Style="{StaticResource GridTitleTextStyle}"
这个属性,从而为 TextBlocks 或其他元素应用此风格。稍后在数据模板中,我们将使用这些风格。
要使我们的应用具有 Windows 团队博客网站的外观和感觉,除 Brush 和 Style 之外,我们还应使用自定义数据模板。我们已在显示数据部分介绍了数据模板。
添加日期控件模板
在 App.xaml 中,添加一个定义显示日期的方块的 ControlTemplate。在 App.xaml 中进行该定义,以使其可在 ItemsPage.xaml 和 SplitPage.xaml 中使用。
XAML<Application.Resources> <ResourceDictionary> ... <ControlTemplate x:Key="DateBlockTemplate"> <Canvas Height="86" Width="86" Margin="8,8,0,8" HorizontalAlignment="Left" VerticalAlignment="Top"> <TextBlock TextTrimming="WordEllipsis" TextWrapping="NoWrap" Width="Auto" Height="Auto" Margin="8,0,4,0" FontSize="32" FontWeight="Bold"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="month" /> </TextBlock.Text> </TextBlock> <TextBlock TextTrimming="WordEllipsis" TextWrapping="Wrap" Width="40" Height="Auto" Margin="8,0,0,0" FontSize="34" FontWeight="Bold" Canvas.Top="36"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="day" /> </TextBlock.Text> </TextBlock> <Line Stroke="White" StrokeThickness="2" X1="54" Y1="46" X2="54" Y2="80" /> <TextBlock TextWrapping="Wrap" Width="20" Height="Auto" FontSize="{StaticResource ContentFontSize}" Canvas.Top="42" Canvas.Left="60"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="year" /> </TextBlock.Text> </TextBlock> </Canvas> </ControlTemplate> ... </ResourceDictionary> </Application.Resources>
请注意,此模板定义参数“day”、“month”和“year”,这些参数将传递给我们之前在第 2 部分中创建的新 Convert 函数。
为项页面添加数据模板
在 ItemsPage.xaml 中,我们添加了以下资源以定义默认视图中网格项的外观。请注意,我们应用之前定义的新风格。
XAML<UserControl.Resources> ... <DataTemplate x:Key="DefaultGridItemTemplate"> <Grid HorizontalAlignment="Left" Width="250" Height="250"> <Border Background="{StaticResource BlockBackgroundBrush}" /> <TextBlock Text="{Binding Title}" Style="{StaticResource GridTitleTextStyle}"/> <TextBlock Text="{Binding Description}" Style="{StaticResource GridDescriptionTextStyle}" /> <StackPanel VerticalAlignment="Bottom" Orientation="Horizontal" Background="{StaticResource ListViewItemOverlayBackgroundBrush}"> <TextBlock Text="Last Updated" Margin="12,4,0,8" Height="42"/> <TextBlock Text="{Binding PubDate, Converter={StaticResource dateConverter}}" Margin="12,4,12,8" /> </StackPanel> </Grid> </DataTemplate> </UserControl.Resources>
在 ItemsPage.xaml 中,更新 itemGridView 的 ItemTemplate 属性以使用我们的 DefaultGridItemTemplate 资源,而不使用 StandardStyles.xaml 中定义的默认模板 Standard250x250ItemTemplate。
XAML<GridView x:Name="itemGridView" AutomationProperties.AutomationId="ItemsGridView" AutomationProperties.Name="Items" Margin="120,0,120,60" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" SelectionMode="None" IsItemClickEnabled="True" ItemTemplate="{StaticResource DefaultGridItemTemplate}" ItemClick="ItemView_ItemClick"/>
为拆分页面添加数据模板
在 SplitPage.xaml 中,添加以下资源以定义列表项在默认视图中的外观:
XAML<UserControl.Resources> ... <!-- green --> <SolidColorBrush x:Key="BlockBackgroundBrush" Color="#FF6BBD46"/> <DataTemplate x:Key="DefaultListItemTemplate"> <Grid HorizontalAlignment="Stretch" Width="Auto" Height="110" Margin="10,10,10,0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!-- Green date block --> <Border Background="{StaticResource BlockBackgroundBrush}" Width="110" Height="110" /> <ContentControl Template="{StaticResource DateBlockTemplate}" /> <StackPanel Grid.Column="1" HorizontalAlignment="Left" Margin="12,8,0,0"> <TextBlock Text="{Binding Title}" FontSize="26.667" TextWrapping="Wrap" MaxHeight="72" Foreground="#FFFE5815" /> <TextBlock Text="{Binding Author}" FontSize="18.667" /> </StackPanel> </Grid> </DataTemplate> ...</UserControl.Resources>
在 SplitPage.xaml 中,我们还更新了 itemListView 中的 ItemTemplate 属性,以使用我们的 DefaultListItemTemplate 资源而不是使用默认模板 Standard130ItemTemplate 。此处显示的是更新后的itemListView 的 XAML。
XAML<ListView x:Name="itemListView" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" Margin="120,0,0,60" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" SelectionChanged="ItemListView_SelectionChanged" ItemTemplate="{StaticResource DefaultListItemTemplate}"/>
在应用了我们的样式后,该应用就非常符合 Windows 团队博客网站的外观和感觉了:
通过使用样式和在其他样式基础上新建样式,我们可以为自己的应用快速定义和应用各种外观。在下个部分中,我们综合所有动画和样式知识来使应用在运行时能够流畅地适应各种布局和方向。
适应不同的布局
通常,应用会设计为以全屏幕方式横向查看。但 Metro 风格 UI 必须适应不同的方向和布局。具体来说,它必须对纵向和横向都支持。横向显示时,它必须支持“全屏幕”、“填充”和“对齐”布局。在从空白模板创建博客阅读器页面时,我们已看到它在纵向上显示不正常。在本部分,我们来了解一下如何使我们的应用在任何分辨率、任何方向均能显示正常。
在 Visual Studio 中进行开发时,你可以使用 Simulator 调试器来测试布局。只需按下 F5,即可以使用调试器工具栏来通过 Simulator 进行调试。
Visual Studio 模板包含处理视图状态更改的代码。此代码包含在 LayoutAwarePage.cs or LayoutAwarePage.vb 文件中,它会将我们的应用状态映射到 XAML 中定义的视觉状态。因为已为我们提供了页面布局逻辑,我们只需要提供要用于每种页面视觉状态的视图。
要使用 XAML 在不同视图间转换,应使用 VisualStateManger 为应用定义不同的 VisualState。此处,我们在 ItemsPage.xaml 中定义了一个 VisualStateGroup。该组中包含 4 个 VisualState,分别名为 FullScreenLandscape、Filled、FullScreenPortrait 和 Snapped。不能同时使用来自同一个 VisualStateGroup 的不同 VisualState 。 每个 VisualState 中都包含动画,用于指示应用需要对 UI 的 XAML 中指定的基准进行哪些更改。
<!--App Orientation States--> <VisualStateManager.VisualStateGroups> <VisualStateGroup> <VisualState x:Name="FullScreenLandscape" /> <VisualState x:Name="Filled"> ... </VisualState> <VisualState x:Name="FullScreenPortrait"> ... </VisualState> <VisualState x:Name="Snapped"> ... </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
当应用处于横向全屏幕视图时,使用 FullScreenLandscape 状态。因为我们正是针对此视图设计了默认的 UI,所以无需进行任何更改,这只是一个空的 VisualState。
当用户将另一个应用对齐到屏幕的一侧时,使用 Filled 状态。在此情况下,项视图页面只是移走,不需要更改。这也只是一个空的 VisualState。
当应用从横向旋转为纵向时,使用 FullScreenPortrait 状态。在此视觉状态中,有两个动画。一个用于更改“后退”按钮所用的样式,另一个用于更改 itemGridView 的页边距,以便所有内容显示都更好地与屏幕相吻合。在集合页面 UI 的 XAML 中,定义了一个 GridView 和一个 ListView 并将其绑定到数据集合。默认情况下,会显示 GridView,而 ListView 处于折叠状态。在 Portrait 状态中,包含三个动画,用于折叠 GridView 、显示 ListView 和更改“后退”按钮的 Style 以使其更小。
<!-- The entire page respects the narrower 100-pixel margin convention for portrait --> <VisualState x:Name="FullScreenPortrait"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="backButton" Storyboard.TargetProperty="Style"> <DiscreteObjectKeyFrame KeyTime="" Value="{StaticResource PortraitBackButtonStyle}"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridView" Storyboard.TargetProperty="Margin"> <DiscreteObjectKeyFrame KeyTime="" Value="100,0,90,60"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState>
当用户显示两个应用,而我们的应用是其中较窄的一个时,使用 Snapped 状态。在这种状态下,我们的应用的宽度仅为 320 设备无关像素 (DIP),因此还需要进一步更改。在项页面 UI 的 XAML 中,定义了一个 GridView 和一个 ListView 并将其绑定到数据集合。默认情况下,会显示 itemGridViewScroller,而 itemListViewScroller 处于折叠状态。在 Snapped 状态中,包含四个动画,用于折叠 itemListViewScroller 、显示 itemListViewScroller 和更改“后退”按钮的 Style 和页面标题以使其更小。
<!-- The Back button and title have different styles when they're snapped, and the list representation is substituted for the grid that's displayed in all other view states.--> <VisualState x:Name="Snapped"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="backButton" Storyboard.TargetProperty="Style"> <DiscreteObjectKeyFrame KeyTime="" Value="{StaticResource SnappedBackButtonStyle}"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="pageTitle" Storyboard.TargetProperty="Style"> <DiscreteObjectKeyFrame KeyTime="" Value="{StaticResource SnappedPageHeaderTextStyle}"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListScrollViewer" Storyboard.TargetProperty="Visibility"> <DiscreteObjectKeyFrame KeyTime="" Value="Visible"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridScrollViewer" Storyboard.TargetProperty="Visibility"> <DiscreteObjectKeyFrame KeyTime="" Value="Collapsed"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState>
在本教程的使用样式创建一致性外观部分,我们创建了用于自定义应用外观的样式和模板。默认的横向视图使用这些样式和模板。要在不同视图中保持自定义外观,还需要为这些视图创建自定义样式和模板。
为项页面对齐视图添加数据模板
在 ItemsPage.xaml 中,我们为网格项创建了一个数据模板。我们还需要为 Snapped 视图中显示的列表项提供新的数据模板。我们将此模板命名为 NarrowListItemTemplate 并将其添加到 ItemsPage.xaml 的资源部分,紧跟在 DefaultGridItemTemplate 资源之后。
XAML<UserControl.Resources> ... <!-- Used in Snapped view --> <DataTemplate x:Key="NarrowListItemTemplate"> <Grid Height="80"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Border Background="{StaticResource BlockBackgroundBrush}" Width="80" Height="80" /> <ContentControl Template="{StaticResource DateBlockTemplate}" Margin="-12,-12,0,0"/> <StackPanel Grid.Column="1" HorizontalAlignment="Left" Margin="12,8,0,0"> <TextBlock Text="{Binding Title}" MaxHeight="56" TextWrapping="Wrap"/> </StackPanel> </Grid> </DataTemplate> </UserControl.Resources>
要使 ListView 显示我们的新数据模板,应更新 itemListView 的 ItemTemplate 属性使用我们的 NarrowListItemTemplate 资源,而不使用 StandardStyles.xaml 中定义的默认模板 Standard80ItemTemplate。在 ItemsPage.xaml 中,使用以下代码片段替换 itemListView:
XAML<ListView x:Name="itemListView" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" Margin="10,0,0,60" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" ItemTemplate="{StaticResource NarrowListItemTemplate}" SelectionMode="None" IsItemClickEnabled="True" ItemClick="ItemView_ItemClick"/>
为拆分页面对齐和填充视图添加数据模板
在 SplitPage.xaml 中,我们创建一个 ListView 模板以用于 Filled 和 Snapped 视图,并在屏幕宽度小于 1366 DIP 时用于 FullScreenLandscape 视图。我们将此模板命名为 NarrowListItemTemplate并将其添加到 SplitPage.xaml 的资源部分,紧跟在 DefaultListItemTemplate 资源之后。
XAML<UserControl.Resources> ... <!-- Used in Filled and Snapped views --> <DataTemplate x:Key="NarrowListItemTemplate"> <Grid Height="80"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Border Background="{StaticResource BlockBackgroundBrush}" Width="80" Height="80"/> <ContentControl Template="{StaticResource DateBlockTemplate}" Margin="-12,-12,0,0"/> <StackPanel Grid.Column="1" HorizontalAlignment="Left" Margin="12,8,0,0"> <TextBlock Text="{Binding Title}" MaxHeight="56" Foreground="#FFFE5815" TextWrapping="Wrap"/> <TextBlock Text="{Binding Author}" FontSize="12" /> </StackPanel> </Grid> </DataTemplate> ...</UserControl.Resources>
要使用此数据模板,需更新要使用该模板的视觉状态。在 Snapped 和 Filled 视觉状态的 XAML 中,我们发现了针对 itemListView 的 ItemTemplate 属性的动画。接着,我们更改了该值,以使用 NarrowListItemTemplate 资源而不使用默认的 Standard80ItemTemplate 资源。以下是更新后的动画 XAML。
XAML<VisualState x:Name="Filled"> <Storyboard> .... <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="ItemTemplate"> <DiscreteObjectKeyFrame KeyTime="" Value="{StaticResource NarrowListItemTemplate}"/> </ObjectAnimationUsingKeyFrames> .... </Storyboard> </VisualState> ...<VisualState x:Name="Snapped"> <Storyboard> .... <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="ItemTemplate"> <DiscreteObjectKeyFrame KeyTime="" Value="{StaticResource NarrowListItemTemplate}"/> </ObjectAnimationUsingKeyFrames> .... </Storyboard> </VisualState>
我们还使用自己的详细信息部分(该部分使用 WebView)替换了拆分页面的项详细信息部分。由于进行了此更改,Snapped_Detail 视觉状态中的动画将不再存在的元素作为目标。当我们使用此视觉状态时,这些动画将导致错误,因此我们必须将其删除。在 SplitPage.xaml 中,我们从 Snapped_Detail 视觉状态中删除这些动画。
XAML<VisualState x:Name="Snapped_Detail"> <Storyboard> ... <!-- REMOVE THESE ELEMENTS: --> <!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemDetailTitlePanel" Storyboard.TargetProperty="(Grid.Row)"> <DiscreteObjectKeyFrame KeyTime="0" Value="0"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemDetailTitlePanel" Storyboard.TargetProperty="(Grid.Column)"> <DiscreteObjectKeyFrame KeyTime="0" Value="0"/> </ObjectAnimationUsingKeyFrames>--> ... <!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemSubtitle" Storyboard.TargetProperty="Style"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource CaptionTextStyle}"/> </ObjectAnimationUsingKeyFrames>--> </Storyboard> </VisualState>
调整 Snapped 视图中的 WebView 边距
在 DetailPage.xaml 中,我们只需在 Snapped 视图中调整 WebView 的边距来使用所有可用空间。在Snapped 视觉状态的 XAML 中,我们添加一个动画来更改 contentViewBorder 上的 Margin 属性的值,如下所示:
XAML<VisualState x:Name="Snapped"> <Storyboard> ... <ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentViewBorder" Storyboard.TargetProperty="Margin"> <DiscreteObjectKeyFrame KeyTime="" Value="20,5,20,20"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState>
添加初始屏幕和徽标
我们的应用带给用户的第一印象来自于初始屏幕。当用户启动应用时将显示初始屏幕,该屏幕会在应用初始化资源时为用户提供即时反馈。当应用的第一个页面准备就绪可以显示时,它就会关闭。
初始屏幕由一种背景色和一个 624 x 304 像素的图像组成。我们在 Package.appxmanifest 文件中设置这些值。你可以在清单编辑器中打开此文件。在清单编辑器的“应用程序 UI”选项卡中,我们设置了初始屏幕图像的路径和背景色。项目模板提供名为 SplashScreen.png 的默认空白图像。我们将空白图像替换为自己的初始屏幕图像,该图像可以明确标识我们的应用,并立即将用户的注意力吸引到应用上来。以下是我们的博客阅读器的初始屏幕:
基本初始屏幕可以适用于我们的博客阅读器,但你也可以使用 SplashScreen 类的属性和方法扩展该初始屏幕。你可以使用 SplashScreen 类获取初始屏幕的坐标,然后利用这些坐标定位该应用的第一个页面。还可以掌握初始屏幕消失的时间,以确定启动应用的任何内容进入动画的时机。
总结
在本文章中,我们学习了如何使用 Visual Studio 11 Express Beta for Windows 8 中的内置页面模板创建多页应用,以及如何在页面之间导航和传递数据。我们学习了如何使用样式和模板以使我们的应用符合 Windows 团队博客网站的风格。我们还学习了如何使用主题动画、应用栏和初始屏幕来使应用更适合 Windows 8 的个性化内容。 最后,我们学习了如何根据各种布局和方向来调整我们的应用,从而让它始终保持美观。
WindowsMetro 风格应用专门针对 Windows 8 Consumer Preview 所提供的用户体验量身定制。每个出色的 Metro 风格应用都遵循特定的设计准则,这使得此类应用与传统的桌面应用相比外观更漂亮、反应更灵敏、行为更直观。开始创建 Metro 风格应用之前,建议你先阅读新模型的设计理论。你可以在设计 Metro 风格的应用中找到详细信息。
在此,我们介绍了有助于你使用 C++ 开发 Metro 风格应用的基本代码和概念,此类应用的 UI 使用可扩展应用程序标记语言 (XAML) 定义。
如果你希望使用其他编程语言,请参阅:
使用 JavaScript 创建你的第一个 Metro 风格应用
使用 C# 或 Visual Basic 创建你的第一个 Metro 风格应用
目标
开始编写代码之前,我们先来看一看你在使用 C++ 构建 Metro 风格应用时会用到的一些功能和设计准则。了解 Microsoft Visual Studio 11 Express Beta for Windows 8 如何为设计和开发工作提供支持也将很有帮助。了解如何以及何时使用 Visual C++ 组件扩展 (C++/CX) 来简化面向 Windows 运行时编写代码的工作也十分重要。我们的示例应用是一个博客阅读器,用于下载和显示 RSS 2.0 或 Atom 1.0 源中的数据。
本文章旨在介绍你在自行创建应用时可以遵循的步骤。完成本课程后,你将能够使用 XAML 和 C++ 构建你自己的 Metro 风格应用。
C++ 桌面应用与 Metro 风格应用对比
如果你习惯使用 C++ 编写 Windows 桌面程序,你可能会发现 Metro 风格应用编程的某些方面与这十分类似,而其他一些方面则需要了解更多知识。
相同之处
你仍然可以使用 C++ 编写代码,并且你可以访问 STL、CRT 以及任何其他 C++ 库,所不同的是,你不能直接调用某些函数,例如与文件 I/O 有关的函数。
如果你习惯使用可视化设计器,你仍然可以使用这些设计器。如果你习惯手动编写 UI 代码,则可以手动编写 XAML 的代码。
你仍然可以创建使用 Windows 操作系统类型和你自己的自定义类型的应用。
你仍然可以使用 Visual Studio 调试器、探查器和其他开发工具。
你仍然可以创建使用 Visual C++ 编译器编译为原生机器代码的应用。使用 C++ 编写的 Metro 风格应用不能在受管运行时环境中执行。
新增内容
Metro 风格应用的设计准则与桌面应用的设计准则十分不同。设计的重点不再是窗口边框、标签和对话框等。内容才是最重要的。出色的 Metro 风格应用从最开始的规划阶段就严格遵循这些准则。有关详细信息,请参阅规划你的应用。
你将使用 XAML 定义整个 UI。在 Metro 风格应用中,UI 与核心程序逻辑之间的分离比在 Microsoft 基础类 (MFC) 或 Microsoft Win32 应用中更为清晰。你在代码文件中处理行为的同时,其他用户可以在 XAML 文件中处理 UI 的外观。
尽管 Win32 仍然可用于某些功能,但你将主要面向一个易于导航且面向对象的全新 API(即 Windows 运行时)进行编程。
使用 Windows 运行时对象时,通常你会使用 C++/CX,该语言会提供可用于创建和访问 Windows 运行时对象的特殊语法,并在创建和访问过程中支持 C++ 异常处理、委派、事件和动态创建对象的自动引用计数。使用 C++/CX 时,基础 COM 和 Windows 体系结构的详细信息几乎从应用代码中完全隐藏。但如果愿意,你可以使用 Windows 运行时 C++ 模板库直接面向 COM 界面编写程序。
你的应用可以支持一些新概念(例如,挂起、超级按钮和应用栏),以便为用户提供更有凝聚力的体验。
你的应用将编译为一个程序包,其中还包含有关你的应用所包含的类型、它使用的资源以及它需要的功能(文件访问、Internet 访问和相机访问等)的元数据。
在 Windows 应用商店中,你的应用通过一个验证流程确定为安全之后,即可面向无数潜在客户发布。
简单博客阅读器,第 1 部分
我们的示例应用是一个基本的博客阅读器,用于下载和显示 RSS 2.0 或 Atom 1.0 源中的数据。
我们将分为两部分介绍该示例。首先,我们将创建一个基本的、单页版本的博客阅读器,以便于我们重点了解使用 C++ 编写 Metro 风格应用程序的一些基础知识。 在第 2 部分中,我们将使用 Visual Studio 11 Express Beta for Windows 8 中的一些预定义 XAML 模板创建一个功能更为丰富的应用版本。
我们将从基础的开始:
如何在 Visual Studio 11 Express Beta for Windows 8 中创建 Metro 风格应用项目。
如何了解创建的各种项目文件。
如何了解 Visual C++ 组件扩展以及何时使用它们。
Visual Studio 提供以下内容:源文件管理;综合的构建、部署和启动支持;XAML、Visual Basic、C#、C++、图形和清单编辑;调试及其他功能。Visual Studio 有多种版本,我们使用的是 Visual Studio 11 Express Beta for Windows 8。你可以随 Microsoft Windows 软件开发工具包 (SDK) 一起免费下载该版本,这样你就具备了构建、打包和部署 Metro 风格应用所需的所有内容。
要开始创建应用,首先创建一个使用 C++ 的 Metro 风格应用项目。 此处,我们使用最基本的模板“空白应用程序”。
创建 Metro 风格应用项目
安装 Visual Studio 11 Express Beta for Windows 8。
在菜单栏上,选择“文件”>“新建”>“项目”。将打开“新建项目”对话框。该对话框应该如下所示:
在“已安装”窗格中,展开“Visual C++”。
选择“Windows Metro 风格”模板类型。
在中心窗格中,选择“空白应用程序”。
输入项目的名称。我们将其命名为“SimpleBlogReader”。
选择“确定”按钮。已创建项目文件。
在继续之前,让我们看一下项目文件。 在“解决方案资源管理器”窗格顶部,选择“显示所有文件”图标以显示“空白应用程序”模板创建的所有项目文件。 根据你指定的项目名称,你应该会看到类似下面的内容:
你编辑的文件
让我们先看一下你可以编辑的项目文件。基本上,其中包括直接位于项目文件夹中的任意文件。
App.xaml、BlankPage.xaml
代表应用对象和 UI 默认页面的 XAML 标记文件。你可以使用 Visual Studio 设计器、Microsoft Expression Blend 或其他 XAML 设计器工具修改这些文件。大部分修改将在 BlankPage.xaml 中完成。
App.xaml.h、App.xaml.cpp
BlankPage.xaml.h、BlankPage.xaml.cpp
Application和 BlankPage 类的用户可编辑标头和实现代码隐藏文件。这些类分别对应于 app.xaml 和 BlankPage.xaml 中的 XAML 树。BlankPage.xaml.h 和 BlankPage.xaml.cpp 文件是添加与本页相关的事件处理程序和其他自定义程序逻辑的位置。应用类中的成员变量的作用域为整个应用。页面类中的变量的作用域仅为该页面。 App.xaml 没有可视的设计平面,但你仍可以在设计器中使用文档大纲和属性检查器。
Package.appxmanifest
包含描述你的应用的元数据,例如,显示名称、描述、徽标和功能。单击此项目时,它将在“清单设计器”中打开。
*.png
默认徽标和初始屏幕图像,你可以将其替换为自己的图像。
pch.h、pch.cpp
典型的 C++ 预编译头文件。可以根据需要向 pch.h 文件中添加 #include 指令。
你不能修改的文件
当你从 Visual Studio 中的 *.xaml 页面导航时,XAML 设计器或编辑器将生成这些文件。它们将启用你所编写的用于引用 XAML 元素的代码隐藏文件。它们还将使代码隐藏文件中的 Microsoft IntelliSense 保持最新。其中一些文件位于 Common 子文件夹(不在图示中展开)。你可以看一下这些文件以大致了解分部类的工作方式、声明变量的位置,等等。这些文件也可以用于调试。但是不要修改这些文件, 因为在下一次构建应用或从 XAML 页面导航到其他位置时将覆盖你所做的所有更改。
App.xaml.g.h、App.xaml.g.cpp
App.xaml.g.cpp 包含应用的主要方法和一些关联的样本代码。App.xaml.g.h 包含使操作系统在运行时将 .xaml 文件加载进内存并创建对象图的代码。不要修改这些文件。
StandardStyles.xaml
包含用于定义 Metro 风格应用的外观和感觉的预定义项模板、样式和其他元素。不要修改已具备的样式和模板。但你可以基于它们创建自定义样式(使用 BasedOn 属性),或将它们复制并粘贴到其他页面中,为副本指定一个不同的名称,然后修改副本。
LayoutAwarePage.cpp、LayoutAwarePage.h、RichTextColumns.cpp、RichTextColumns.h, and so on
处理导航和布局的基础结构代码。
BlankPage.xaml.g.h、BlankPage.xaml.g.cpp
包含为 BlankPage 和 App 类自动生成的分部类定义,以及为每个具有 x:Name 属性的 XAML 元素生成的成员变量。不要修改这些文件。
XamlTypeInfo.g.h
由 XAML 编辑器生成的 C++ 文件,用于启用 Windows 运行时以识别和加载在应用中定义并在任何 XAML 文件中引用的任何自定义类型。不要修改此文件。
代码一览
在“解决方案资源管理器”中,打开 BlankPage.xaml 并在 XAML 编辑器窗格中查看标记。请注意一个包含 <Grid>
元素的 <Page>
元素。 现在打开 BlankPage.xaml.g.h。请注意一个名为 BlankPage
的类,它是从 System.UI.Xaml.Page 派生出的,并包含一个 System.IO.Xaml.Controls.Grid 成员变量。
标记中的每个元素类型都有一个相关联的 Windows 运行时类型。在向 XAML 添加元素时,Visual Studio 会生成 C++ 源代码,使你可以编写将这些元素作为 Windows 运行时类型进行引用的代码隐藏文件。 并非在 C++ 项目代码中表示所有元素;而是仅表示那些你明确引用的元素。
让我们返回到 BlankPage.xaml.g.h。 请注意 BlankPage
声明为 partial ref class
。
partial ref class BlankPage : public Windows::UI::Xaml::Controls::Page… {…}
partial和 ref 关键字显然不是 ISO 标准 C++。它们是专门用于创建 Windows 运行时类型实例的组件扩展。ref 关键字指示该类是一个 Windows 运行时引用类型;使用 ref 使你无需编写大量下一代 COM 代码。 在类成员声明中,请注意 Object^
变量和 Grid^
变量。 “^”符号为“尖帽号”,它表示“对象句柄”。在动态内存中创建 Windows 运行时类型时使用该符号,而不使用“*”。 你也可以使用 C++ auto 关键字;编译器将推断类型。
Grid^ grid = ref new Grid(); // or: auto grid = ref new Grid(); grid->Width = 600;
从最基本的意义上说,ref 类是一个 COM 对象,它实现了 IInspectable 接口,其生命期是通过智能指针管理的。 Windows 运行时定义一个语言无关的抽象二进制接口 (ABI),它使用机器码本地运行,而不是通过虚拟机。C++/CX 可实现面向该 ABI 以一种更类似现代 C++ 的方式编程,而不是类似旧式 COM 编程。C++/CX 专门用于创建和访问 Windows 运行时类型。 ref 类中不面向 Windows 运行时的库、模型和函数完全可以使用 ISO 标准 C++ 编写。 在同一函数中混合使用 C++ 和 C++/CX 十分常见。 它们都会编译为本机 C++。
partial 关键字指示编译器在另一个代码文件中继续声明该类。该文件是 BlankPage.xaml.h。 如果程序员需要向 BlankPage
类中添加变量或函数,可以在 BlankPage.xaml.h 和 BlankPage.xaml.cpp 中执行此操作。 如果 XAML 编辑器需要添加变量或其他样本代码,它将在 *.g.h 和 *.g.cpp 文件中执行此操作。虽然类定义包含两个部分,但在进行编码和编译时,它就像一个类一样。通常,你可以安全地忽略 *.g.* 文件。这是因为“解决方案资源管理器”默认隐藏这些文件。现在,我们已了解了幕后信息,如果“显示所有文件”仍处于启用状态,请选择该图标以将其禁用,以便你可以更轻松地查找要修改的文件。
注意 如果你出于个人偏好或由于开发环境的某些限制而无法使用 C++/CX,则可以使用标准 C++ 和 Windows 运行时 C++ 模板库直接面向 COM 界面进行编程。有关详细信息,请参阅 Windows 运行时 C++ 模板库。
指定应用功能
Metro 风格应用在安全容器中运行,该容器对文件系统、网络资源和硬件具有有限的访问权限。当用户从 Windows 应用商店安装应用时,Windows 会查看 Package.appxmanifest 文件中的元数据,以确定该应用需要哪些功能。例如,某个应用可能需要访问 Internet 中的数据、用户文档库中的文档,或用户的摄相机和麦克风。当应用安装完成后,它会向用户显示所需的功能,而用户必须授予相应的权限,然后它才能访问这些资 源。如果应用没有请求并收到所需资源的权限,则在运行时禁止其访问该资源。
在应用中添加基本 Internet 功能
在“解决方案资源管理器”中,打开 Package.appxmanifest。此时将在“应用程序清单设计器”中打开该文件。
选择“功能”选项卡。
选中“Internet(客户端)”复选框(如果尚未选中)。
关闭清单设计器。
指定某个功能时,它会在 Package.appxmanifest.xml 文件中的 Capabilities
元素下列出。通常使用“应用程序清单设计器”来设置功能,但如果使用“XML 文本编辑器”打开 Package.appxmanifest.xml,你将可以看到 XML 中的 Capabilities
元素。
<Capabilities> <Capability Name="internetClient" /> </Capabilities>
将数据导入应用
在此部分中,我们介绍了:
如何创建自定义数据类。
如何异步检索 RSS 或 Atom 数据源。
由于我们的应用正确地要求 Internet 客户端访问,我们可以编写代码来将博客源获取到应用中。“Developing for Windows”(Windows 开发)博客分别以 RSS 和 Atom 两种形式显示其文章的全文。我们希望显示每篇最新博客文章的标题、作者、日期和内容。 我们可以使用 Windows.Web.Syndication 命名空间中的类来下载这些源。尽管我们也可以使用这些类显示 UI 中的数据,但我们将创建自己的数据类。这为我们提供了更大的灵活性,使我们可以按相同的方式处理 RSS 和 Atom 源。我们创建以下两个类:
FeedData 包含有关 RSS 或 Atom 源的信息。
FeedItem 包含有关源中的各篇博客文章的信息。
我们将这些类定义为公共 ref 类,以启用与显示标题、作者等 XAML 元素的数据绑定。我们使用Bindable特性指定到 XAML 编译器,此编译器动态绑定到这些类型的实例。在 ref 类中,公共数据成员公开为属性。没有特殊逻辑的属性不需要用户指定的 getter 和 setter;编译器将提供他们。在 FeedData 类中,注意我们如何使用 IVector<T> 将公共集合类型公开给其他 Windows 运行时类和组件。我们还在内部使用 Vector<T> 类作为实现 IVector 的具体类型。之后,我们将学习如何使用此类型。
创建自定义数据类
在“解决方案资源管理器”中,在 SimpleBlogReader 项目节点的快捷方式菜单上,选择“添加” > “新项目”。
从选项列表中选择 Header File (.h) 并命名为 FeedData.h。 (为了方便,我们不在此示例中使用单独的 .cpp 文件。)
将下列代码复制并粘贴到此文件中。用点时间看看此代码,自行熟悉 C++/CX 构造。请注意 collection.h 的 #include 指令,它对于具体的 Platform::Collections::Vector 类型是必需的。
C++#pragma once#include "pch.h" #include <collection.h>namespace SimpleBlogReader{ // To be bindable, a class must be defined within a namespace // and a bindable attribute needs to be applied. [Windows::UI::Xaml::Data::Bindable] public ref class FeedItem sealed { public: FeedItem(void){} ~FeedItem(void){} property Platform::String^ Title; property Platform::String^ Author; property Platform::String^ Content; // Temporary workaround (use Object^ not DateTime): // property Windows::Foundation::DateTime PubDate; property Platform::Object^ PubDate; }; [Windows::UI::Xaml::Data::Bindable] public ref class FeedData sealed { public: FeedData(void) { m_items = ref new Platform::Collections::Vector<Platform::Object^>(); } ~FeedData(void){} property Platform::String^ Title; property Windows::Foundation::Collections::IVector<Platform::Object^>^ Items { Windows::Foundation::Collections::IVector<Platform::Object^>^ get() {return m_items; } } private: Platform::Collections::Vector<Platform::Object^>^ m_items; }; }
C++ 中的异步操作:检索源数据
我们已具备了数据类,现在即可实现 GetFeedData 函数来下载博客源。 Windows.Web.Syndication.SyndicationClient 类用于检索和分析 RSS 和 Atom 源。由于此操作涉及网络 I/O,因此,将异步执行该方法。异步编程模型可在 Windows 运行时类库中找到。异步方法调用会立即向 UI 会话返回控件,从而使 UI 能够在后台线程上执行操作时保持反应灵敏。
Windows 运行时提供了一种调用异步操作并在操作完成时获取结果的方法;你可以直接面向该 API 编程。但首选方法是使用 ppltasks.h 中定义的 task class 类。该 task 类使用相同的 Windows 运行时 API,但你可以使用它来编写更为简明的代码,更便于形成异步操作链并在一个位置处理发生的任何异常。 在使用 task 类时,基本步骤始终是相同的:
通过调用 Windows 运行时 *Async 方法(如 Windows::Web::Syndication::ISyndicationClient::RetrieveFeedAsync)来创建异步操作。
将操作作为输入参数以创建 task 对象。
调用 task::then 并指定将操作返回值作为输入的 lambda。
可以选择再次调用 then 一次或多次。这些子句可以接受上一子句的返回值。
可以选择提供 final then 子句,以处理在操作链中的任意位置引发的任何异常。
添加异步下载功能
- 将这些行添加到BlankPage.xaml.h:C++
#include "FeedData.h" ...// In the BlankPage class... private: void GetFeedData(Platform::String^ feedUriString); FeedData^ feedData;
- 将这些行添加到BlankPage.xaml.cpp:C++
// BlankPage.xaml.cpp #include <ppltasks.h>...using namespace Windows::Web::Syndication;using namespace Concurrency;
- 调用 InitializeComponent 后,将此行添加到 BlankPage 构造函数中:C++
feedData = ref new FeedData();
- 将方法实现添加到 BlankPage.xaml.cpp 中。有关此代码的详细信息,请参阅 C++ 中的异步编程。C++
void BlankPage::GetFeedData(Platform::String^ feedUriString){ // Create the SyndicationClient and the target uri SyndicationClient^ client = ref new SyndicationClient(); Uri^ feedUri = ref new Uri(feedUriString); // Create the async operation. feedOp is an // IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^ auto feedOp = client->RetrieveFeedAsync(feedUri); feedOp = client->RetrieveFeedAsync(feedUri); // Create the task object and pass it the async operation. // SyndicationFeed^ is the type of the return value // that the feedOp operation will eventually produce. task<SyndicationFeed^> createFeedTask(feedOp); // Create a "continuation" that will run when the first task completes. // The continuation takes the return value of the first operation, // and then defines its own asynchronous operation by using a lambda. createFeedTask.then([this] (SyndicationFeed^ feed) -> SyndicationFeed^ { // Get the title of the feed (not the individual posts). feedData->Title = feed ->Title->ToString(); // Retrieve the individual posts from the feed. auto feedItems = feed->Items; // Iterate over the posts. You could also use // std::for_each( begin(feedItems), end(feedItems), // [this, feed] (SyndicationItem^ item) for(int i = 0; i < (int)feedItems->Size; i++) { auto item = feedItems->GetAt(i); FeedItem^ feedItem = ref new FeedItem(); feedItem->Title = item->Title->Text; // Temporary workaround: // feedItem->PubDate = item->PublishedDate; feedItem->PubDate = ref new Platform::Box<Windows::Foundation::DateTime>(item->PublishedDate); feedItem->Author = item->Authors->GetAt(0)->Name; if (feed->SourceFormat == SyndicationFormat::Atom10) { feedItem->Content = item->Content->Text; } else if (feed->SourceFormat == SyndicationFormat::Rss20) { feedItem->Content = item->Summary->Text; } feedData->Items->Append((Object^)feedItem); } this->DataContext = feedData; return feed; }).then ([] (task<SyndicationFeed^> t) { // Handle any exceptions that were raised // in the chain of operations. try { auto f = t.get(); } catch (std::exception e) { //Handle exception } });}
请注意,我们在完成
feedData
对象的填充后调用了this->DataContext = feedData
。我们必须将feedData
实例作为页面的DataContext
,以便可以将 UI 绑定到该实例。将 FeedData 作为数据上下文,我们可以将{Binding Path="Title"}
写入 XAML 标记,在启动时加载 XAML 页面并构造对象图形时,加载程序知道 “Title” 是 FeedData 实例上的 Title 属性。在此演练的第 2 部分,我们会演示可创建多个 FeedData 对象的更复杂的异步操作链。
在我们的应用启动时,我们希望其自动加载“Developing for Windows”(Windows 开发)博客。要执行此操作,最佳办法是响应通知页面加载已完成的 Loaded 事件。在方法调用中,我们将传入 Atom 源的 URL,因为作者数据包含在该源中,而不是包含在 RSS 源中。
处理 Loaded 事件
在 BlankPage.xaml 中,将语句
Loaded="PageLoadedHandler"
添加到起始Page
标记,它应紧跟在mc:Ignorable="d"
之后但位于右尖括号之前,从而使整个标记如下所示:XAML<Page x:Class="SimpleBlogReader.BlankPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:BlogReader" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Loaded="PageLoadedHandler">
将 C++ 方法签名添加到 BlankPage.xaml.h:
C++//In the BlankPage class declaration... private: void PageLoadedHandler(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
在你的 BlankPage.xaml.cpp 文件中为事件处理程序方法添加存根实现:
C++void BlankPage::PageLoadedHandler(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e){ GetFeedData("http://windowsteamblog.com/windows/b/developers/atom.aspx");}
在 XAML 中定义 UI
现在,让我们看一下如何:
直接在 XAML 中定义布局,而不使用设计器工具。
在网格中定义行和列。
为 XAML 元素创建成员变量。
创建 XAMLUI 的最简便和最强大的方式是使用 Visual Studio 所提供的模板之一,然后使用 Expression Blend 或 Visual Studio XAML 设计器等工具来进行自定义。但是,由于我们重点关注 XAML 自身的结构,因此我们将直接在 XAML 代码编辑器中操作。
通常 Metro 风格应用都包含多个页面,且每个页面都具有不同的布局。例如,博客阅读器可能具有一个用于浏览多篇博客文章并选择其中一篇的页面,以及另一个用于阅读选定 文章的页面。每个页面在其自己的代码文件中都是一个单独的 XAML 树。页面的典型根元素(至少从逻辑上讲)是 <Page>
。它所对应的 Windows 运行时类型为 Windows::UI::Xaml::Controls::Page。 Page 元素/类支持在应用中的页面之间进行基本导航。Page 将一个布局控件(或面板)作为其直接子元素。在布局控件内部,你可以放置内容控件(如 TextBlock和 ListView)来存放图片、文本等各个项。
XAML 布局系统支持绝对布局和动态布局。在绝对布局中,将使用明确的 x-y 坐标来定位控件;在动态布局中,你可以使布局容器和控件的大小和位置随应用大小的改变而自动改变。可以使用 Canvas 布局控件进行绝对定位,以及使用 Grid、StackPanel 和其他控件进行动态定位。实际上,你在定义应用的布局时,通常会结合使用绝对方法和动态方法,还可以将面板相互嵌套。
博客阅读器应用的典型布局是:顶部为标题,左侧是文章列表,右侧是选定文章的内容。下图说明了我们的布局的显示效果:
在我们创建项目时,系统为我们创建了一个名为 BlankPage.xaml 的文件。该文件具有一个 Page 元素,该元素将一个 Grid 元素作为子元素。XAML 应如下所示:
<Page x:Class="SimpleBlogReader.BlankPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Blog" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Loaded="PageLoadedHandler"> <Grid Background="{StaticResource PageBackgroundBrush}"></Grid> </Page>
x:Class 属性将此 Page 元素与 BlankPage 类关联起来,该类在 BlankPage.xaml.g.h 和 BlankPage.xaml.h 中声明。请记住我们添加了 feedData 成员变量。在 XAML 树中,xmlns 属性是 XML 命名空间;此处唯一需要关注的是 xmlns:local 命名空间,它将 FeedData 和 FeedItem 类纳入 XAML 页面的范围中,从而使我们可以在稍后将数据绑定到这些类。
若要开始我们的布局,为了方便,我们将“Name”属性添加到顶级网格:Name="Grid1"
。接下来,定义 Grid1的两行。首行显示源标题。在第二行中,让我们嵌入另一个 Grid,称为 Grid2,并将其分为两列。左列包含 ListView 控件,用于显示所有可用文章的标题、作者和日期。用户可以滚动查看该列表,然后选择一篇文章。右列包含第三个 Grid,Grid3,它在顶部行中包含 TextBlock,在底部行中包含 WebView 控件。文本块将显示博客文章的标题,WebView 将显示内容。
下面是简化的视图,其中显示了以前图片中的布局的基本结构。(不要粘贴此代码,因为它还没有完成。)
<!-- Pseudo-XAML Simplified View --> <Page> ... <Grid Name="Grid1"> <Grid.RowDefinitions... ... <!--In first row of Grid1.--> <TextBlock Grid.Row=”0”>…</TextBlock> ... <!--In second row of Grid1.--> <Grid Name="Grid2" Grid.Row=”1”> <Grid.ColumnDefinitions... <!-- In left column of Grid2. --> <ListView Grid.Column=”0”>…</ListView> <!-- In right column of Grid2. --> <Grid Name="Grid3" Grid.Column=”1”> <Grid.RowDefinitions... <TextBlock Grid.Row=”0”></TextBlock> <WebView Grid.Row=”1”/> </Grid> </Grid> </Grid> </Page>
现在,我们通过一次粘贴一部分代码来创建实际的 XAML。此练习可帮助我们了解如何构造 XAML 用户界面。
为我们的博客阅读器创建基本布局
- 在 BlankPage.xmal 中,为默认
Grid
元素添加一个Name
特性,使整个元素如下所示:XAML<Grid Background="{StaticResource ApplicationPageBackgroundBrush}" Name="Grid1">
在 BlankPage.xaml 中,为 Grid1 元素定义两行,方法是使用以下 XAML 片段作为 Grid 的第一个子节点,紧跟在起始标记之后。第一个行定义(第 0 行)中的
Height="140"
属性设置将顶行设置为具有绝对高度 140 像素。无论行的内容或应用的大小如何变化,此高度都不会改变。第二个行定义(第 1 行)中的Height="*"
设置指示底行接受第 0 行确定大小后剩余的任意大小的空间。这称为比例缩放。注意 只要你键入或粘贴代码,Visual Studio 就会自动提供正确的缩进。
XAML<Grid.RowDefinitions> <RowDefinition Height="140" /> <RowDefinition Height="*" /> </Grid.RowDefinitions>
紧跟在行定义后,但仍在 Grid1 内,向第一行中添加以下 TextBlock 内容控件。这将保留源的主标题,因此,我们可以为其指定较大的字体。 我们为其提供一个 x:Name 属性,以便我们可以在 C++ 代码中引用该属性,并且还会提供一个在将数据绑定到该属性后显示的临时字符串。
XAML<TextBlock x:Name="TitleText" Text="Main Title of Blog Feed" VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0"/>
紧跟在 TextBlock 后面,添加第二个 Grid 元素,并为它指定一个 Name 属性 Grid2。添加定义两列的子 Grid.ColumnDefinitions 元素。此处的 Grid.Row 属性引用 Grid1 行,因此,
Grid.Row="1"
表示“将此元素放在 Grid1 的第二行中”换句话说,我们在一个 Grid 中嵌入另一个 Grid。列宽度设置Width="2*"
和Width="3*"
要求 Grid2 将自身分为 5 个相等的部分。两个部分用于第一列,三个部分用于第二列。XAML<Grid Name="Grid2" Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="2*" /> <ColumnDefinition Width="3*" /> </Grid.ColumnDefinitions> </Grid>
紧跟在 Grid2 的列定义后面,在结束标记之前,添加下列 ListView 控件。由于未指定任何 Grid.Column 属性,因此,控件将放入 Grid2 的第 0 列中。我们暂且将内容留空。稍后我们将为其添加一些内容和一个事件处理程序
XAML<ListView x:Name="ItemListView"></ListView>
在选择 ListView 标记后,继续在 Grid2 元素内添加第三个 Grid,它包含两行。为它指定 Name 特性 Grid3,并放在 Grid2 的右列中。
Height="Auto"
设置要求顶行将其高度设置为与内容相同。底行则占用剩下的所有空间。XAML<Grid Name="Grid3" Grid.Column="1" > <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> </Grid>
紧靠前一个 RowDefinitions 后面,但在 Grid3 结束标记里面,添加 TextBlock 并为其提供一些临时文本。以后,我们将该 TextBlock 设置为显示 WebView 中所示的博客文章的标题。此控件不需要 x:Name 属性,因为我们不需要在 XAML 或在代码隐藏文件中引用它。 但不要担心;即使没有在 BlankPage 类中为此控件创建变量,也会在运行时实例化该控件并完全正常工作。
XAML<TextBlock Text="Blog Post Title" FontSize="24"/>
紧靠前一个 TextBlock 后面,添加一个 WebView 并将其放在 Grid 的底行中。此控件显示文章内容,包括图形。我们使用WebView而不是TextBlock或RichTextBlock,因为源内容的格式设置为 HTML。
XAML<WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/>
- XAML 树现在应如下所示:此时,你应该能够看到在设计器表面显示的用户界面。现在你也可以按 F5,查看到目前为止的显示效果。目前你还看不到任何数据,只能看到用户界面的基本大纲。按 F12,然后按 Shift-F5,返回代码编辑器。XAML
<Page x:Class="SimpleBlogReader.BlankPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:SimpleBlogReader" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Name="Grid1" Background="{StaticResource ApplicationPageBackgroundBrush}"> <Grid.RowDefinitions> <RowDefinition Height="140" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock x:Name="TitleText" Text="Main Title of Blog Feed" VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0"/> <Grid Name="Grid2" Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="2*" /> <ColumnDefinition Width="3*" /> </Grid.ColumnDefinitions> <ListView x:Name="ItemListView"></ListView> <Grid Name="Grid3" Grid.Column="1" > <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Text="Blog Post Title" FontSize="24"/> <WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/> </Grid> </Grid> </Grid> </Page>
设置 FeedItem 数据格式
基本布局已定义完成,现在我们为 ListView 项添加格式,你应该记得,这些项是 FeedItem 对象,我们在 FeedData.h 中进行了定义,在 GetFeedData 方法中进行了初始化,并将其插入了 FeedData::Items 集合中。我们希望该控件显示源中的每篇博客文章的标题、作者和发布日期。我们的想法是用户可以滚动查看这些项目,然后选择一个感兴趣的项目。在选择一个项时,右侧的 TextBlock 将使用较大字体显示文章标题,而 WebView 将显示内容。我们希望设置 ListView 项的显示格式,以使其如下所示:
要将三个 FeedItem 属性值合并为一个单元以进行显示,我们可以使用数据模板。数据模板可以定义一段或多段数据的“外观”,并将作为一个 XAML 节点实现。使用数据模板可以创建融合了文本、图形、动画以及其他 XAML 功能的新颖生动的信息表示。不过,我们只设置最基本的格式。与前面添加的标题一样,我们可以将每个属性值放在 TextBlock 中。我们可以使用每个 TextBlock 指定字体大小和其他格式属性,以及一个临时的 Text 值,稍后我们将替换该值。 要排列 TextBlock 元素,可以使用 StackPanel。StackPanel 是一个轻型布局面板,它在 XAML 中经常用于与此类似的小型布局场景。
创建源项布局
在 ListView 节点中,添加一个 ItemTemplate,它具有一个 DataTemplate 节点作为直接子节点。在类似 ListView 的控件中,DataTemplate 始终嵌套在 ItemTemplate 中。这指示控件将模板应用于其项目集合中的每个项目。
XAML<ListView.ItemTemplate> <DataTemplate> </DataTemplate> </ListView.ItemTemplate>
在 DataTemplate 中,添加一个包含三个 TextBlock 元素的 StackPanel,每个元素表示我们希望显示的三个 FeedItem 属性之一。 因为没有为 StackPanel 指定方向,TextBlock 元素将垂直排列。现在,我们仅为 Text 属性指定一些临时字符串,只是为了提醒我们它们表示的是什么内容。当你按 F5 时它们不会显示,因为这并不是实际的项,而只是展示项显示效果的模板。
XAML<StackPanel> <TextBlock Text="Post title" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" /> <TextBlock Text="Post author" FontSize="16" Margin="15,0,0,0"/> <TextBlock Text="Publication Date" FontSize="16" Margin="15,0,0,0"/> </StackPanel>
- ListView 的 XAML 现在应如下所示:XAML
<ListView x:Name="ItemListView"> <ListView.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="Post title" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" /> <TextBlock Text="Post author" FontSize="16" Margin="15,0,0,0"/> <TextBlock Text="Publication Date" FontSize="16" Margin="15,0,0,0"/> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView>
至此,我们已编写了一个用于从源下载实际数据的方法,还设计了一个显示一些临时值的 UI。下一步是添加 XAML 属性,以将实际源数据连接到 UI。这称为数据绑定。
显示数据
现在,我们看看如何将数据绑定到 UI,并使用值转换器将一个 DateTime 值转换为 String。
使用编程方式向控件中添加内容
在代码隐藏文件中,你可以通过编程方式将内容插入到控件中。例如,若要填充源标题 TextBox,我们可以在事件处理程序中编写以下代码 TitleText->Text = feedData->Title;
,这将使该文本立即更新到控件中。如果想要了解其工作方式,请注意,我们在 XAML 元素中指定了 x:Name 属性,如下所示: <TextBlock x:Name="TitleText" Text="Main Title of Blog Feed" …/>
。添加该 XAML 元素导致某个变量在 BlankPage.xaml.g.h 中声明:
// BlankPage.xaml.g.h -- Do Not Paste partial ref class BlankPage : public Windows::UI::Xaml::Controls::Page, public Windows::UI::Xaml::Markup::IComponentConnector{ ... Windows::UI::Xaml::Controls::TextBlock^ TitleText;)
...并在 BlankPage.xaml.g.cpp 中初始化:
// Get the TextBlock named 'TitleText' TitleText = safe_cast<Windows::UI::Xaml::Controls::TextBlock^> (static_cast<Windows::UI::Xaml::IFrameworkElement^>(this)->FindName("TitleText"));
具备该代码后,我们可以在 BlankPage.xaml.h 和 BlankPage.xaml.cpp 中 BlankPage 分部类的部分引用该初始化变量。
使用数据绑定向控件中添加内容
有时,在代码中动态设置 Text 属性即可奏效。但若要显示数据,通常使用数据绑定将数据源连接到 UI。建立绑定后,如果数据源发生更改,绑定到该数据源的 UI 元素可以自动反映更改内容。使用数据绑定,全部或几乎全部的代码都将在 XAML 文件中编写,而不是在代码隐藏文件中编写。数据绑定可以实现 View(或 ViewModel)与其他模块(如 Model 或 Controller)之间更为清晰的划分,这通常是向 XAML 控件中填充内容的建议方法。
绑定表达式
要将内容控件绑定到数据源,我们将为控件上的内容属性分配一个 {Binding } 表达式。对于 TextBlock,内容属性为Text。我们使用具有 Path
值的绑定表达式指示控件绑定到的内容。下面是我们用于 TitleTextTextBlock 的绑定表达式: Text="{Binding Path=Title}"
。 Path
值(此处为 Title
)的含义取决于数据上下文。
我们在代码隐藏文件的 BlankPage 构造函数中为整个 XAML 树动态设置默认数据上下文: this->DataContext = feedData;
。由于该语句,TextBlock 知道“Title”表示 feedData 实例上的 Title 属性。在该行代码中,“this”是在运行时从 XAML 树构造的 BlankPage 实例。设置 feedData 对象的数据上下文会使其成为此页面的整个对象树的默认数据上下文。 如有必要,我们可以覆盖各个元素的上下文。
将源标题绑定到 TitleTextTextBlock
修改 Text 特性以绑定到源标题。
XAML<TextBlock x:Name="TitleText" Text="{Binding Path=Title}" VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0"/>
将 ListView 项绑定到 dataFeed 的 Items 属性
要将列表视图连接到数据源,请将以下绑定表达式添加到 ItemListView:
ItemsSource="{Binding Path=Items}"
,以使起始标记现在如下所示:XAML<ListView x:Name=”ItemListView” ItemsSource=”{Binding Path=Items}” Margin=”10,0,0,10”>
现在,我们已将 ListView 控件绑定到 FeedData 项集合,我们可以绑定到作为 FeedItem 对象的各个项,但必须将其转换为 Platform::Object^ 类型才能在集合中存储它们。
将 DataTemplate 项绑定到FeedItem 属性
在第一个 TextBlock 中,使用
"{Binding Path=Title}"
替换临时文本,以使元素现在如下所示:XAML<TextBlock Text="{Binding Path=Title}" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" />
在第二个 TextBlock 中,使用
"{Binding Path=Author}"
替换临时文本,以使元素现在如下所示:XAML<TextBlock Text="{Binding Path=Author}" FontSize="16" Margin="15,0,0,0"/>
我们暂且跳过第三个文本框,因为我们需要提供一个自定义转换器来显示 DateTime 值。
让我们回想一下,在将新的 FeedItem^ 对象添加到 FeedData::Items 集合时,我们是否必须将其转换为 Platform::Object^?数据绑定如何知道这些对象是具有名为 “Title”、“Author” 和 “PubDate” 的属性的 FeedItem 对象?答案是,它既不知道,也不关心。它直接使用元数据在其集合中查找在对象上具有指定名称的属性。如果你指定的属性名称不存在,或者输错了名称,运行时结果很可能是空的 TextBox。由于运行时容易出现输入问题,因此,需要在编码时小心输入!
剩下的一个数据绑定 TextBlock 是 WebView 控件上方的控件。我们希望此 TextBlock 显示当前在 ListView 中选择的项目的标题。如果我们直接使用与 TitleText 控件相同的绑定,我们将显示相同的字符串,因为数据上下文和属性名称是相同的。为了更正这一问题,我们可以在 Grid 元素(作为 TextBlock 的直接父元素)中设置新数据上下文以覆盖页面上的默认 DataContext 属性。请注意,我们此处绑定到 XAML 元素上的属性名称,因此,我们使用 ElementName
属性指定要绑定到的元素。
将文章标题数据绑定到当前选定的项目
设置 Grid3 中的 DataContext 特性,使开始标记如下所示:
XAML<Grid Name ="Grid3" Grid.Column="1" DataContext="{Binding ElementName=ItemListView, Path=SelectedItem}">
使用绑定表达式
"{Binding Path=Title}"
替换 WebView 上方的 TextBlock 中的临时文本,以使元素现在如下所示:XAML<TextBlock Text="{Binding Path=Title}" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" />
由于我们在闭合网格元素内设置了新的数据上下文,在运行时,数据绑定机制将在当前选定的 FeedItem 而非DataFeed 对象上查找 Title 特性。
使用值转换器设置数据格式
在 ItemListViewDataTemplate 中,我们将 PubDate 属性(一个 DateTime)绑定到 TextBlock.Text属性。默认情况下,绑定引擎会将 PubDate 从一个 DateTime 转换为一个字符串。但自动转换仅生成 Windows::Foundation::DateTime 类型的名称,该名称没有提供详细信息。要生成实际日期,我们有两个选择:我们可以将 FeedItem::PubDate 类型更改为 Platform::String^ ,然后在初始化变量时进行转换,或者创建自定义值转换器并将其数据绑定到该转换器以便在运行时转换值。 我们选择后一种方法。
若要创建值转换器,我们需创建一个类,该类可实现 IValueConverter 接口,然后实现 Convert 方法并选择实现 ConvertBack 方法。转换器可以将数据从一种类型更改为另一种类型,根据文化背景转换数据,或者修改数据呈现方式的其他方面。此处,我们创建一个非常基本的日期转换器, 它可以转换传入的日期值并设置其格式,使其显示日期、月份和年份。(在此演练的第 2 部分,我们将创建一个功能更为丰富的日期转换器。)
创建实现 IValueConverter 的值转换器类
在菜单栏上,选择“项目”>“添加新项目”,然后选择“头文件”。将文件命名为 DateConverter.h,并向该文件中添加此类定义:
C++namespace SimpleBlogReader{public ref class DateConverter sealed : public Windows::UI::Xaml::Data::IValueConverter {public: virtual Platform::Object^ Convert(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ culture) { Windows::Foundation::DateTime dt = (Windows::Foundation::DateTime) value; Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ dtf = Windows::Globalization::DateTimeFormatting::DateTimeFormatter::LongDate::get(); return dtf->Format(dt); } virtual Platform::Object^ ConvertBack(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ culture) { //Not used. Left as exercise for the reader! throw ref new Platform::NotImplementedException(); } };}
- 将以下
#include
指令添加到 BlankPage.xaml.h::C++#include "DateConverter.h"
尽管在我们自己的代码隐藏中并未引用该文件,但我们仍需包含该文件,因为 Visual Studio 生成过程需要该文件来生成数据绑定代码。
在 BlankPage.xaml 中,将该类的一个实例声明为资源。将以下 Page.Resources 节点粘贴到开始 Page 标记和 Grid1 元素之间。
XAML<Page.Resources> <local:DateConverter x:Key="dateConverter" /> </Page.Resources>
Page 标记已具有一个 XML 命名空间映射,使我们可以访问项目中在 SimpleBlogReader 命名空间中声明的类:
xmlns:local="using:SimpleBlogReader"
。如果没有该映射,我们将无法在此处看到 DateConverter 类。- 现在我们可以将 PubDateTextBlock 绑定到 DateConverter:XAML
<TextBlock Text="{Binding Path=PubDate, Converter={StaticResource dateConverter}}" FontSize="16" Margin="15,0,0,0"/>
通过此 XAML,绑定引擎使用我们的自定义 DateConverter 将 DateTime 转换为一个字符串。 它返回的字符串按我们需要的方式格式化,只有日期、月份和年份。
在 WebView 中显示 HTML
若要在我们的应用中显示博客文章,我们必须获取要在 WebView 控件中显示的文章内容。 WebView 控件为我们提供了一种在我们的应用中托管 HTML 数据的方法。
我们将 WebView 添加到嵌套 Grid 的右列中,并为其提供 ContentView 的 x:Name,因为我们需要用于在我们的 BlankPage 类中引用的变量。
当我们查看 WebView 的 Source 属性时,将注意到它需要 URI 才能显示 Web 页面。我们的 HTML 数据只不过是HTML 的字符串。它没有包含可以绑定到 Source 属性的 URI。所幸的是,我们可以将自己的 FeedItem::Content 属性传递给 NavigateToString 方法。要实现该功能,我们处理 ListView 的 SelectionChanged 事件。
将 WebView 连接到选定项目的 FeedItem::Content 属性
在 XAML 文件中为 ListView 指定一个 SelectedChanged 事件处理程序。设置 SelectionChanged属性并指定属性名称以调用事件处理程序方法,以使起始标记现在如下所示:
XAML<ListView x:Name="ItemListView" ItemsSource="{Binding Path=Items}" SelectionChanged="ItemListView_SelectionChanged" Margin="10,0,0,10">
如同之前所创建的事件处理程序一样,现在我们必须在代码隐藏中创建事件处理程序。首先,将此行代码添加到 BlankPage.xaml.h:
C++// Declaration in BlankPage.xaml.h void ItemListView_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e);
在 BlankPage.xaml.cpp 中添加方法实现:
C++// Implementation in BlankPage.xaml.cpp void BlankPage::ItemListView_SelectionChanged (Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e){ FeedItem^ feedItem = safe_cast<FeedItem^>(ItemListView->SelectedItem); if (feedItem != nullptr) { // Navigate the WebView to the blog post content HTML string. ContentView->NavigateToString(feedItem->Content); }}
现在我们具有了一个基本的单页应用。如果按 F5,应显示如下图所示的一些内容。要中断应用,并返回到 Visual Studio IDE,请按 F12。
提示 为了获得更好的调试体验,请从公共 Microsoft 符号服务器下载调试符号。在主菜单上,选择“工具”,然后选择“选项”。在“选项”窗口中,展开“调试”,并选中“Microsoft 符号服务器”旁边的复选框。。第一次下载它们时可能需要花费一些时间。若要在下次按 F5 时获得更快的性能,请指定一个缓存符号的本地目录。
此处显示的是完整的 BlankPage.xaml 的 XAML 树。
<Page x:Class="SimpleBlogReader.BlankPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:SimpleBlogReader" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Loaded="PageLoadedHandler"> <Page.Resources> <local:DateConverter x:Key="dateConverter"/> </Page.Resources> <Grid Background="{StaticResource ApplicationPageBackgroundBrush}"> <Grid.RowDefinitions> <RowDefinition Height="140"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock x:Name="TitleText" Text="{Binding Path=Title}" VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0"/> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="2*"/> <ColumnDefinition Width="3*"/> </Grid.ColumnDefinitions> <ListView x:Name="ItemListView" ItemsSource="{Binding Path=Items}" Margin="10,0,0,10" SelectionChanged="ItemListView_SelectionChanged"> <ListView.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Path=Title}" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap"/> <TextBlock Text="{Binding Path=Author}" FontSize="16" Margin="15,0,0,0"/> <TextBlock Text="{Binding Path=PubDate, Converter={StaticResource dateConverter}}" FontSize="16" Margin="15,0,0,0" /> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> <Grid Grid.Column="1" DataContext="{Binding ElementName=ItemListView, Path=SelectedItem}"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock Text="{Binding Path=Title}" FontSize="24" Margin="5,0,0,0"/> <WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/> </Grid> </Grid> </Grid> </Page>
这是第 1 部分的结尾。现在,你已创建了一个适用于大多数情况的基本应用,并在该过程中学习了 XAML 的基础知识及其相关的代码隐藏文件。
简单博客阅读器,第 2 部分
我们已经了解了 XAML 和 C++/CX 的基础知识,现在来更加详细地了解一下 Metro 风格应用中包含的一些功能。首先,Metro 风格应用必须适用于所有情况。它必须适应各种设备上不同的分辨率、方向和视图。
你可以看到,我们的基本页面在以这种方式查看时显示不正常。我们希望我们的应用能够显示正常,同时能够更好地反映 Windows 团队博客的个性化内容。为实现这些目标,我们需要更复杂的页面,以及在页面间导航的方法。所幸的是,Visual Studio 提供了多个页面模板,可以实现许多我们需要的功能。为了升级应用,我们将放弃之前努力创建的空白页面,但将重复使用许多代码。更重要的是,我们在使用复杂 方法创建该页面过程中所获得的知识可以帮助我们更好地理解如何借助 Visual Studio 中提供的页面模板以简单的方式创建 Metro 风格应用。
添加页面和导航
如果要支持多个博客,我们必须向应用中添加一些页面,并处理这些页面间的导航。首先,我们需要一个列出所有 Windows 团队博客的页面;我们可以使用 Items Page 模板实现此目的。当读者从此页面中选择一个博客时,我们会将该博客的文章列表加载到另一个页面中。我们已经创建的 BlankPage.xaml 页面仍适用于此情况,但为了实现更好的效果,我们将使用 Visual Studio 中提供的 SplitPage 模板。我们还将添加一个详细信息页面,以使用户无需列表视图即可选择阅读各篇博客文章,从而节省空间。每个模板中都内置有丰富的导航支持。我们只需向每个 代码隐藏类中停用的 Navigate 和 OnNavigatedTo 方法中添加几行代码,并添加一个导航按钮以实现从拆分页面到详细信息页面的前进导航。完成所有操作后,我们即可从项目中排除原始的 BlankPage.xaml 及其相关的代码隐藏文件。
页面模板
Visual Studio 11 Express Beta for Windows 8 中包含许多页面模板,可以用于各种情况。以下是可用的页面模板。
组详细信息页面
显示一个组的详细信息以及组中各项的预览。
分组的项页面
显示分组的集合。
项详细信息页面
显示一个项的详细信息,并支持导航至相邻的项。
项页面
显示一组项。
拆分页面
显示项列表以及所选项的详细信息。
基本页面
具有布局意识、标题以及后退按钮的空页面。
空页面
Metro 风格应用的空页面。
向应用中添加新页面
在菜单栏上,选择“项目”>“添加新项目”。将打开“添加新项目”对话框。
在“已安装”窗格中,展开“Visual C++”。
选择“Windows Metro 风格”模板类型。
在中心窗格中,选择“项页面”并接受默认名称。
选择“添加”按钮。页面的 XAML 和代码隐藏文件现已添加到项目中。
重复步骤 4 和 5,但选择“拆分页面”。
重复步骤 4 和 5,但选择“基本页面”。将此页面命名为 "DetailPage"。
以下是“添加新项目”对话框。
“项 页面”将显示 Windows 团队博客的列表。“拆分页面”将在左侧显示每个博客的文章,在右侧显示选定文章的内容,这与我们之前创建的 BlankPage 类似。“基本页面”将仅显示选定文章的内容、“后退”按钮和页面标题。在此页面上,不会从 HTML 的某个字符串将文章内容加载到 WebView 中(就像在 SplitView 页面中执行的操作一样),而是导航到该文章的 URL 并显示实际的网页。执行此操作后,应用的页面将如下所示:
将页面模板添加到项目中并查看 XAML 和代码隐藏时,显然这些页面模板为我们完成了许多工作。事实上,起初可能容易迷惑,但了解每个页面模板包含三个主要部分将很有帮助:
资源
“资源”部分定义页面的样式和数据模板。我们将在使用样式创建一致性外观部分作进一步的介绍。
视觉状态管理器
“视觉状态管理器 (VSM)”中定义使应用适应不同布局和方向的动画和转换。我们将在适应不同布局部分作进一步介绍。
应用内容
构成应用 UI 的控件和内容在根布局面板中定义。
应用在页面之间导航时,它向数据传递一个指针,新页面将使用此指针来填充它的 UI。 因此,在添加导航代码之前,我们必须添加新数据模型的代码。我们将使用 Platform::Collections::Vector<Object^> 实例来存储 FeedData 对象,还将向创建数据模型的异步代码添加性能优化。我们会将此代码添加到 app.xaml.h 和 app.xaml.cpp,因为我们必须在应用启动时将 Vector 传递给 ItemsPage 实例。
修改数据类
将这两个属性添加到 FeedData.h 的 FeedData 类中(请注意,将 PubDate 声明为 Platform::Object^ 是一种临时的变通措施,在未来版本中没有必要这么做。):
C++property Platform::String^ Description;// Temporary workaround: // property Windows::Foundation::DateTime PubDate; Platform::Object^ PubDate;
某些源具有说明字段(如果我们还有空间可以显示),我们可以使用上一篇博客文章的日期作为某些布局中的
PubDate
。(请注意,PubDate
没有 ^(“尖帽号”),因为 DateTime 是一个值类型。)将此属性添加到 FeedItem 类。
C++property Windows::Foundation::Uri^ Link;
我们将使用详细信息页面中的此链接直接导航到博客文章,而不是导航到内容字符串。
修改初始化数据模型的异步代码
将此
#include
指令添加到 app.xaml.h:C++#include "FeedData.h"
将这两个专用方法签名添加到 app.xaml.h 中的 App 类:
C++void InitDataSource(Platform::Collections::Vector<Object^>^ fds);FeedData^ GetFeedData(Windows::Web::Syndication::SyndicationFeed^ feed);
将这些
#include
指令添加到app.xaml.cpp:C++#include <ppltasks.h>#include "ItemsPage.xaml.h"
将这些
namespace
声明添加到app.xaml.cpp:C++using namespace Platform::Collections;using namespace Windows::Web::Syndication;using namespace Concurrency;using namespace std;
将下列方法实现添加到 app.xaml.cpp:
C++void App::InitDataSource(Vector<Object^>^ fds){std::vector<std::wstring> urls; urls.push_back(L"http://windowsteamblog.com/windows/b/developers/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/windowsexperience/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/extremewindows/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/business/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/bloggingwindows/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/windowssecurity/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/springboard/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows/b/windowshomeserver/atom.aspx");// There is no Atom feed for this blog, so we use the RSS feed. urls.push_back(L"http://windowsteamblog.com/windows_live/b/windowslive/rss.aspx");urls.push_back(L"http://windowsteamblog.com/windows_live/b/developer/atom.aspx");urls.push_back(L"http://windowsteamblog.com/ie/b/ie/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows_phone/b/wpdev/atom.aspx");urls.push_back(L"http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx");SyndicationClient^ client = ref new SyndicationClient();std::for_each(std::begin(urls), std::end(urls), [=,this] (std::wstring url){// Create the async operation. // feedOp is an IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^ auto feedUri = ref new Uri(ref new String(url.c_str()));auto feedOp = client->RetrieveFeedAsync(feedUri);// Create the task object and pass it the async operation. // SyndicationFeed^ is the type of the return value // that the feedOp operation will eventually produce auto pOp = task<SyndicationFeed^>(feedOp)// Initialize a FeedData object with the feed info. Each // operation is independent and does not have to happen on the // UI thread. Therefore, we specify use_arbitrary. .then([this] (SyndicationFeed^ feed) -> FeedData^{return GetFeedData(feed);}, concurrency::task_continuation_context::use_arbitrary())// Append the initialized FeedData object to the list // that is the data source for the items collection. // This has to happen on the UI thread. By default, a .then // continuation runs in the same apartment thread that it was called on. // Because the actions will be synchronized for us, we can append // safely to the Vector without taking an explicit lock. .then([fds] (FeedData^ fd){fds->Append(fd);OutputDebugString(fd->Title->Data());})// The last continuation serves as an error handler. The // call to get() will surface any exceptions that were raised // at any point in the task chain. .then( [this] (concurrency::task<void> t){try {t.get();}catch(Platform::Exception^ e){//TODO handle error. OutputDebugString(e->Message->Data());}}); //end pOp task chain }); //end std::for_each }FeedData^ App::GetFeedData(SyndicationFeed^ feed){ FeedData^ feedData = ref new FeedData(); // Get the title of the feed (not the individual posts). feedData->Title = feed->Title->ToString(); if (feed->Subtitle->Text != nullptr) { feedData->Description = feed->Subtitle->Text; } // Use the date of the latest post as the last updated date. feedData->PubDate = ref new Platform::Box<Windows::Foundation::DateTime>(feed->Items->GetAt(0)->PublishedDate); // Construct a FeedItem object for each post in the feed. std::for_each( begin(feed->Items), end(feed->Items), [this, feed, feedData] (SyndicationItem^ item) { FeedItem^ feedItem = ref new FeedItem(); feedItem->Title = item->Title->Text; // Temporary workaround: // feedItem->PubDate = item->PublishedDate; feedItem->PubDate = ref new Platform::Box<Windows::Foundation::DateTime>(item->PublishedDate); //We only get first author in case of multiple entries. feedItem->Author = item->Authors->GetAt(0)->Name; if (feed->SourceFormat == SyndicationFormat::Atom10) { feedItem->Content = item->Content->Text; String^ s(L"http://windowsteamblog.com"); feedItem->Link = ref new Uri(s + item->Id); } else if (feed->SourceFormat == SyndicationFormat::Rss20) { feedItem->Content = item->Summary->Text; feedItem->Link = item->Links->GetAt(0)->Uri; } feedData->Items->Append((Object^)feedItem); }); return feedData;} //end GetFeedData
更适用于日期的 IValueConverter 类
现在正是修改 DateConverter 类的好时机,使之能够分别返回日期、月份和年份,而非将其作为一个字符串返回。在稍后为网格项和列表视图项定义的新风格中,我们需要利用这项功能。
修改 DateConverter 类
在 DateConverter.h 中,使用以下新实现替换现有的 DateConverter 类:
C++public ref class DateConverter sealed : public Windows::UI::Xaml::Data::IValueConverter { public: virtual Platform::Object^ Convert(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ culture) { if(value == nullptr) { throw ref new Platform::InvalidArgumentException(); } Windows::Foundation::DateTime dt = (Windows::Foundation::DateTime) value; Platform::String^ param = safe_cast<Platform::String^>(parameter); Platform::String^ result; if(param == nullptr) { Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ dtf = Windows::Globalization::DateTimeFormatting::DateTimeFormatter::ShortDate::get(); result = dtf->Format(dt); } else if(wcscmp(param->Data(), L"month") == 0) { Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ month = ref new Windows::Globalization::DateTimeFormatting::DateTimeFormatter("{month.abbreviated(3)}"); result = month->Format(dt); } else if(wcscmp(param->Data(), L"day") == 0) { Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ month = ref new Windows::Globalization::DateTimeFormatting::DateTimeFormatter("{day.integer(2)}"); result = month->Format(dt); } else if(wcscmp(param->Data(), L"year") == 0) { Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ month = ref new Windows::Globalization::DateTimeFormatting::DateTimeFormatter("{year.full}"); result = month->Format(dt); } else { // We don't handle other format types currently. throw ref new Platform::InvalidArgumentException(); } return result; } virtual Platform::Object^ ConvertBack(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ culture) { // Not needed in SimpleBlogReader. Left as an exercise. throw ref new Platform::NotImplementedException(); }};
- 为了使用这个类,我们在 App.xaml 的 ResourceDictionary 节点中,紧接 MergedDictionaries 结束标记添加一个对此类的引用:XAML
<local:DateConverter x:Key="dateConverter" />
在页面之间导航
XAMLUI 框架提供内置的导航模型,该模型使用 Frame 和 Page,并且其工作方式与在 Web 浏览器中的导航十分类似。Frame 控件可托管 Page,并且具有导航历史记录,你可以通过该历史记录在访问过的页面中前进和后退。在导航时,你可以在页面之间传递数据。
在 Visual Studio 项目模板中,一个名为rootFrame 的 Frame 被设置为应用窗口的内容。我们来看一下 App.xaml.cpp 中的默认代码。请注意,当应用启动时,显示的第一个页面将是在此事件处理程序中指定的页面。我们将立刻修改此代码以导航到我们的项页面。
// Default implementation. Not to be pasted into BlogReader. void App::OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs^ pArgs){ // Create a Frame to act as navigation context and navigate to the first page. auto rootFrame = ref new Frame(); TypeName pageType = { BlankPage::typeid->FullName, TypeKind::Metadata }; rootFrame->Navigate(pageType); // Place the frame in the current Window and ensure that it is active. Window::Current->Content = rootFrame; Window::Current->Activate();}
要在页面之间导航,你可以使用 Frame 控件的 Navigate、GoForward 和 GoBack 方法。通过使用 Navigate(TypeName, Object) 方法,可以导航并将数据对象传递到新页面。我们将使用该方法在我们的页面之间传递数据。 第一个参数 pageType 是我们将导航到的页面的 TypeName。我们使用静态 typeid 运算符来获取类型的 TypeName。
第二个参数是我们传递给将要导航到的页面的数据对象。在之前的单页版应用中,我们仅显示了一个博客源,因此我们传递了博客源集合中的第一个源。在此新版应用中,我们必须在不同时间传递三个对象:在应用启动时将 Vector 从 App 对象中的 rootFrame 传递到项页面;将选定 FeedData 对象从项页面传递到拆分页面;并将选定的 FeedItem 从拆分页面传递到详细信息页面。
从 App 类导航到项页面
在 App.xaml.cpp 中,使用下面这段代码替换 App::OnLaunched 方法实现,这将创建一个新的 FeedDataSource,并将其传递到项目页:
C++void App::OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs^ pArgs){ //Create the data source object. auto feedDataSource = ref new Vector<Object^>(); //Populate it asynchronously. InitDataSource(feedDataSource); // Create a Frame to act as navigation context and navigate to the first page, configuring the // new page by passing required information as a navigation parameter. auto rootFrame = ref new Frame(); TypeName pageType = { ItemsPage::typeid->FullName, TypeKind::Metadata }; rootFrame->Navigate(pageType, feedDataSource); // Place the frame in the current Window and ensure that it is active. Window::Current->Content = rootFrame; Window::Current->Activate();}
在 App::OnLaunched 中调用 Navigate 方法时,它最终将导致调用 ItemsPage::OnNavigatedTo 事件处理程序。 此处,Vector 与一个名为 “Items” 的键相关联,并被插入到类型为 Windows::Foundation::Collections::IObservableMap 的页面的 DefaultViewModel 成员中。 (每个页面都有自己的 DefaultViewModel。)在 ItemsPage.xaml.cpp 中已经为你生成了此代码:
C#void ItemsPage::OnNavigatedTo(NavigationEventArgs^ e){ DefaultViewModel->Insert("Items", e->Parameter);}
现在,按 F5 运行包含此更改的应用。请注意,尽管尚未对模板代码进行任何更改,但我们传递给 ItemsPage 的部分数据已经显示在网格区域中。
从项页面导航到拆分页面
用户从集合中选取一个博客后,我们将从项目页导航到拆分页面。要进行此导航,我们希望 GridView项目的表现类似按钮,而不是选择它们时所选的项目。为了使 GridView 项目像按钮一样响应,我们对 SelectionMode 和 IsItemClickEnabled 特性进行了设置,如下列示例所示。然后为 GridView 的 ItemClicked 事件添加了一个处理程序。在 ItemsPage.xaml 中找到
itemGridView
元素,使用以下标记取而代之:XAML<GridView x:Name="itemGridView" AutomationProperties.AutomationId="ItemsGridView" AutomationProperties.Name="Items" Margin="116,0,116,46" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" ItemTemplate="{StaticResource Standard250x250ItemTemplate}" SelectionMode="None" IsItemClickEnabled="True" ItemClick="ItemView_ItemClick" />
将这些指令添加到 ItemsPage.xaml.cpp:
C++#include "SplitPage.xaml.h" //... using namespace Windows::UI::Xaml::Interop;
将事件处理程序原型添加到 ItemsPage.xaml.h,并将实现添加到 ItemsPage.xaml.cpp:
C++//ItemsPage.xaml.h: virtual void ItemView_ItemClick(Object^ sender, Windows::UI::Xaml::Controls::ItemClickEventArgs^ e) override;void ItemsPage::ItemView_ItemClick(Object^ sender, ItemClickEventArgs^ e){ // Navigate to split page and pass the selected feed data. TypeName pageType = { SplitPage::typeid->FullName, TypeKind::Metadata }; this->Frame->Navigate(pageType, e->ClickedItem);}
项页面还包含一个名为 itemListView 的列表视图,如果调整了应用,则会显示该列表视图来代替网格。我们将在适应不同的布局部分中对此进行更详细的讨论。目前,我们只需对 ListView 进行与对 GridView 所做更改相同的更改,以确保它们的行为相同。在 ItemsPage.xaml 中找到 itemListView 并添加所需的属性,以使其如下所示:
XAML<ListView x:Name="itemListView" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" Margin="10,0,0,60" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" ItemTemplate="{StaticResource Standard80ItemTemplate}" SelectionMode="None" IsItemClickEnabled="True" ItemClick="ItemView_ItemClick"/>
现在,打开 SplitPage.xaml.cpp 并将下列行添加
SplitPage::OnNavigatedTo
方法的开头。C++FeedData^ fd = safe_cast<FeedData^>(e->Parameter);DefaultViewModel->Insert("Feed", fd);DefaultViewModel->Insert("Items", fd->Items);
注意 我们将 Items 属性分别插入到拆分页面的 DefaultViewModel 中,使 XAML 数据绑定可访问这些项。
导航返回项目页不需要额外的工作。模板包含处理 BackButton.Click 事件和调用 Frame.GoBack 方法的代码。
在此时运行应用时,请注意详细信息窗格中的博客文本将显示原始 HTML。若要修复此问题,我们需要更改用于选定博客文章的标题和内容的布局。 如果应用正在运行,请按 F12 中断应用,然后按 Shift-F5 停止调试并返回到 Visual Studio 代码编辑器。
修改拆分页面和项页面中的绑定和布局
在结束向添加到应用中的新页面添加功能之前,我们还必须进行几项更改。将此代码添加到应用后,即可开始设置应用的样式和动画。
由于在向 DefaultViewModel 中添加数据时使用了名为“Feed”的键,我们必须将页面标题中的绑定更改为绑定到 Feed 属性,而不是绑定到“Group”(默认设置)。在 SplitPage.xaml 中,更改名为 pageTitle 的 TextBlock 的文本绑定以绑定到 Feed.Title,如下所示:
XAML<TextBlock x:Name="pageTitle" Grid.Column="1" Text="{Binding Feed.Title}" Style="{StaticResource PageHeaderTextStyle}"/>
在 ItemsPage.xaml 中,页面标题被绑定到具有键 AppName 的静态资源。将此资源中的文本更新到 Windows 团队博客,如下所示:
XAML<x:String x:Key="AppName">Windows Team Blogs</x:String>
在 SplitPage.xaml 中,将名为 titlePanel 的 Grid 更改为跨 2 个列。
XAML<!-- Back button and page title --> <Grid x:Name="titlePanel" Grid.ColumnSpan="2">
向 SplitPage.xaml 中添加 WebView 控件
在 SplitPage.xaml 中,我们必须更改用于显示选定博客文章的标题和内容的布局。要执行此操作,需要将名为 itemDetail 的 ScrollViewer 替换为下列 ScrollViewer 布局。你应该已经认识到此 XAML 的大部分来源于我们之前在 BlankPage.xaml 中进行的工作。本文章的后续内容中将介绍 Rectangle 元素的用途。
XAML<!-- Details for selected item --> <ScrollViewer x:Name="itemDetail" AutomationProperties.AutomationId="ItemDetailScrollViewer" Grid.Column="1" Grid.Row="1" Padding="70,0,120,0" DataContext="{Binding SelectedItem, ElementName=itemListView}" Style="{StaticResource VerticalScrollViewerStyle}"> <Grid x:Name="itemDetailGrid"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock x:Name="itemTitle" Text="{Binding Title}" Style="{StaticResource SubheaderTextStyle}"/> <Border x:Name="contentViewBorder" BorderBrush="Gray" BorderThickness="2" Grid.Row="1" Margin="0,15,0,20"> <Grid> <WebView x:Name="contentView" /> <Rectangle x:Name="contentViewRect" /> </Grid> </Border> </Grid> </ScrollViewer>
在 SplitPage.xaml.cpp 中,修改当 ListView 选项更改时导致 WebView 更新的事件处理代码。ItemListView_SelectionChanged 函数签名和实现已经具备。我们只需添加以下行:
C++FeedItem^ fi = safe_cast<FeedItem^>(itemListView->SelectedItem);if(fi != nullptr) { contentView->NavigateToString(fi->Content);}
向 DetailPage.xaml 中添加 WebView 控件
在 DetailPage.xaml 中,我们必须将标题文本绑定到博客文章标题,并添加一个 WebView 控件来显示博客页面。要执行此操作,需要将包含返回按钮和页面标题的 Grid 替换为此 Grid 和 WebView:
XAML<!-- Back button and page title --> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Button x:Name="backButton" Click="GoBack" IsEnabled="{Binding Frame.CanGoBack, ElementName=pageRoot}" Style="{StaticResource BackButtonStyle}"/> <TextBlock x:Name="pageTitle" Grid.Column="1" Text="{Binding Title}" Style="{StaticResource PageHeaderTextStyle}"/> </Grid> <Border x:Name="contentViewBorder" BorderBrush="Gray" BorderThickness="2" Grid.Row="1" Margin="120,15,20,20"> <WebView x:Name="contentView" /> </Border>
在 DetailPage.xaml.cpp 中,向 OnNavigatedTo 方法重写中添加代码以导航到博客文章,并设置页面的 DataContext。更新后的方法如下所示:
C++void DetailPage::OnNavigatedTo(NavigationEventArgs^ e){ FeedItem^ feedItem = safe_cast<FeedItem^>(e->Parameter); if (feedItem != nullptr) { contentView->Navigate(feedItem->Link); this->DataContext = feedItem; }}
应用栏
博客阅读器应用中的大部分导航都是在用户从 UI 中选择某个项目时发生的。但在拆分页面上,我们必须提供一种方法,让用户能转到博客文章的详细信息视图。我们可以在页面上某个位置放置一个按钮,但这将干 扰核心应用体验,即阅读。因此,我们将按钮放在一个隐藏的应用栏中,该栏仅在用户需要时显示。 我们将添加一个应用栏,其中包含一个按钮,用于导航到详细信息页。
应用栏是 UI 的一部分,默认情况下是隐藏的,可在用户沿屏幕边缘轻扫、与应用互动或者单击鼠标右键时显示或消失。它可以向用户提供导航、命令和工具。应用栏可以显示在页面顶部、底部或同时显示在顶部和底部。 要在 XAML 中添加应用栏,我们需要将一个 AppBar 控件指定给 Page 的 TopAppBar 或 BottomAppBar 属性。
向拆分页面应用栏中添加按钮
StandardStyles.xaml 文件包含适用于常见场景的各种应用栏按钮样式。我们以这些样式为指导为我们的按钮创建样式。我们将样式放在 SplitPage.xaml 的
UserControl.Resources
部分:XAML<Style x:Key="WebViewAppBarButtonStyle" TargetType="Button" BasedOn="{StaticResource AppBarButtonStyle}"> <Setter Property="AutomationProperties.AutomationId" Value="WebViewAppBarButton"/> <Setter Property="AutomationProperties.Name" Value="View Web Page"/> <Setter Property="Content" Value=""/> </Style>
将此代码粘贴到 UserControl.Resources 节点之后,创建一个包含我们刚刚定义的按钮的顶部应用栏:
XAML<Page.TopAppBar> <AppBar Padding="10,0,10,0"> <Grid> <Button Click="ViewDetail_Click" HorizontalAlignment="Right" Style="{StaticResource WebViewAppBarButtonStyle}"/> </Grid> </AppBar> </Page.TopAppBar>
添加详细信息视图导航
将以下方法签名添加到 SplitPage.xaml.h 中的 SplitPage 类:
C++void ViewDetail_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
将以下
#include
指令和using
语句添加到 SplitPage.xaml.cpp 中:C++#include "DetailPage.xaml.h" ...using namespace Windows::UI::Xaml::Interop;
将此方法主体添加到 SplitPage.xaml.cpp 中:
C++void SplitPage::ViewDetail_Click(Object^ sender, RoutedEventArgs^ e){ // Navigate to the appropriate destination page, and configure the new page // by passing required information as a navigation parameter. TypeName pageType = { DetailPage::typeid->FullName, TypeKind::Metadata }; FeedItem^ fi = safe_cast<FeedItem^>(itemListView->SelectedItem); if(fi != nullptr) { Frame->Navigate(pageType, fi); }}
添加动画和过渡
当我们谈论动画时,通常会联想到屏幕上蹦蹦跳跳的物体。但在 XAML 中,动画实质上只是一种在对象上更改属性值的方法。这让动画具有多种用途,而不仅仅是一堆跳动的球。在我们的博客阅读器应用中,我们使用一些默认动画和转 换来使 UI 适应不同的布局和方向。我们可以在 Windows.UI.Xaml.Media.Animation 命名空间中找到它们。
添加主题动画
主题动画是一个预定义的动画,我们可以将其放在一个 Storyboard 中。在此,我们将 PopInThemeAnimation 放入 Storyboard,并使其成为 DetailPage.xaml 中的资源。因为返回按钮和标题在各个页面中均位于相同的位置,我们并不需要将它们弹入,所以我们将动画的目标设置为围绕在我们的 Web 内容周围的 Border。这样便会使 Border 和其中的所有内容具有动画效果。
向详细信息页面添加主题动画
将以下 XAML 片段粘贴到 DetailPage.xaml 中的
UserControl.Resources
节点:XAML<Storyboard x:Name="PopInStoryboard"> <PopInThemeAnimation Storyboard.TargetName="contentViewBorder" FromHorizontalOffset="400"/> </Storyboard>
将以下代码粘贴到 DetailPage.xaml.cpp 中的 DetailPage::OnNavigatedTo 方法的开头:
C++// Run the PopInThemeAnimation. Windows::UI::Xaml::Media::Animation::Storyboard^ sb = safe_cast<Windows::UI::Xaml::Media::Animation::Storyboard^>(this->FindName("PopInStoryboard"));if (sb != nullptr){ sb->Begin();} //... rest of method as before
添加主题转换
主题转换是一个完整的动画组和一个组合进预打包行为中的 Storyboard,我们可以将该行为附加到某个 UI 元素。 ContentThemeTransition 与 ContentControl 一起使用,并且会在控件内容发生更改时自动触发。
在我们的应用中,向在拆分页面列表视图中存放文章标题的 TextBlock 添加一个主题转换。当 TextBlock 的内容发生更改时,ContentThemeTransition 将自动触发并运行。动画是预先定义的,我们不需要执行任何操作来运行它。我们只需将其附加到 TextBlock 中即可。
向 SplitPage.xaml 添加主题转换
在 SplitPage.xaml 中,名为
pageTitle
的 TextBlock 是一个空元素标记。为了添加主题转换,我们将其嵌入到 TextBlock 中,因此需要更改 TextBlock,使之包含开始和结束标记。使用以下 XAML 节点替换现有标记:XAML<TextBlock x:Name="pageTitle" Grid.Column="1" Text="{Binding Feed.Title}" Style="{StaticResource PageHeaderTextStyle}"> <TextBlock.Transitions> <TransitionCollection> <ContentThemeTransition /> </TransitionCollection> </TextBlock.Transitions> </TextBlock>
当 TextBlock 的内容发生更改时,ContentThemeTransition 将自动触发并运行。动画是预先定义的,我们不需要执行任何操作来运行它。我们只需将其附加到 TextBlock 中即可。有关详细信息以及主题动画和过渡的列表,请参阅快速入门:动画。
使用样式创建一致性外观
我们希望让博客阅读器应用的外观和感觉类似于 Windows 团队博客网站。我们希望用户在该网站和我们的应用之间切换时能够拥有无缝的使用体验。我们的 Windows Metro 风格 UI 的默认深色主题与 Windows 团队博客网站不太匹配。这在详细信息页面上尤为明显,在该页面上我们会将实际的博客页面加载到一个中WebView,如下所示:
要使我们的应用具有可根据需要进行更新的一致外观,可使用画笔和样式。使用 Brush,我们可以在一个位置定义外观,然后在任意需要的位置使用它。使用 Style,我们可以为控件的属性设置值,并在应用中重复使用这些设置。
在深入了解详细信息之前,我们先来看一下如何使用画笔设置应用中页面的背景色。应用中的每个页面都有一个根 Grid,该根的一个 Background 属性已设置为定义页面的背景色。我们可以按如下所示单独设置每个页面的背景:
<Grid Background="Blue">
但是,更好的方法是定义一个 Brush 作为资源,并使用它来定义所有页面的背景色。下面显示了在 Microsoft Visual Studio 模板中如何执行此操作:
<Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
我们将对象和值定义为资源,以使其可以重复使用。要将对象或值用作资源,我们必须设置其 x:Key 属性。我们使用此键来从 XAML 中引用资源。此处,背景被设置为具有键 ApplicationPageBackgroundBrush
的资源,该键是在 StandardStyles.xaml 文件中定义的 SolidColorBrush。
要更改页面的背景,我们在 App.xaml 中定义一个具有相同键 ApplicationPageBackgroundBrush
的新 SolidColorBrush。对于此新画笔,我们设置 Color #FF0A2562,这是一种与 Windows 团队博客网站十分相称的合适的蓝色。
覆盖默认页面背景画笔
将此画笔添加到 App.xaml 中的资源字典:
XAML<SolidColorBrush x:Key="WindowsBlogBackgroundBrush" Color="#FF0A2562"/>
将各页中的根 Grid 元素修改为使用新画笔:
XAML// Change the Background of the root Grid in each xaml page in the app.<Grid Background="{StaticResource WindowsBlogBackgroundBrush}">
你可以在 XAML 文件中为单独的页面定义资源,也可以在 App.xaml 文件中,或在单独的资源字典 XAML 文件,如 StandardStyles.xaml 中定义。定义资源的位置决定了该资源可以使用的范围。Visual Studio 将 StandardStyles.xaml 文件创建为项目模板的一部分,并将其放在 Common 文件夹中。它是一个包含 Visual Studio 页面模板中所使用的值、样式和数据模板的资源字典。可以在多个应用之间共享一个资源字典 XAML 文件,也可以将多个资源字典合并进单个应用中。
在我们的博客阅读器应用中,我们在 App.xaml 中定义资源,以使其可以在整个应用中可用。还有一些资源在各个页面的 XAML 文件中定义。这些资源只在定义了它们的页面中可用。如果在 App.xaml 和页面中同时定义了具有相同键的资源,则页面中的资源将覆盖 App.xaml 中的资源。同样地,在 App.xaml 中定义的资源将覆盖在单独的资源字典文件中定义的具有相同键的资源。有关详细信息,请参阅快速入门:设置控件样式。
下面,我们来看一下如何在应用中使用 Style。我们的博客阅读器 UI 中的大多数文本都很相似,只有大小有所不同。文本的默认外观由 StandardStyles.xaml 中的此 Style 定义:
...<SolidColorBrush x:Key="ApplicationTextBrush" Color="#DEFFFFFF"/> ...<x:Double x:Key="ContentFontSize">14.667</x:Double> <FontFamily x:Key="ContentFontFamily">Segoe UI</FontFamily> ...<Style x:Key="BasicTextStyle" TargetType="TextBlock"> <Setter Property="Foreground" Value="{StaticResource ApplicationTextBrush}"/> <Setter Property="FontSize" Value="{StaticResource ContentFontSize}"/> <Setter Property="FontFamily" Value="{StaticResource ContentFontFamily}"/> <Setter Property="TextTrimming" Value="WordEllipsis"/> <Setter Property="TextWrapping" Value="Wrap"/> <Setter Property="Typography.StylisticSet20" Value="True"/> <Setter Property="Typography.DiscretionaryLigatures" Value="True"/> </Style>
在 Style 定义中,我们需要一个 TargetType 属性和一个或多个 Setter 的集合。我们将 TargetType 设置为一个指定 Style 将应用到的类型的字符串,在此例中为 TextBlock。如果你试图将某个 Style 应用到与 TargetType 属性不匹配的控件,就会发生异常。每个 Setter 元素都需要一个 Property 和一个 Value。这些属性设置用于指示该设置将应用于哪个控件属性,以及为该属性设置的值。
为 ItemsPage.xaml 添加风格
在 ItemsPage.xaml 中,我们希望网格框中的文本具有默认的外观,但我们希望 FontSize 更大一些。我们可以通过在本地进行设置来改写 FontSize,如下所示:
<TextBlock Text="{Binding Title}" Style="{StaticResource BasicTextStyle}" FontSize="26.667" />
。但还有一种更好的方法:创建一个基于 BasicTextStyle的 Style,并更改其中的 FontSize。 在 ItemsPage.xaml 的 UserControl.Resources 部分中,添加以下风格定义。XAML<!-- light blue --> <SolidColorBrush x:Key="BlockBackgroundBrush" Color="#FF557EB9"/> <!-- Grid Styles --> <Style x:Key="GridTitleTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BasicTextStyle}"> <Setter Property="FontSize" Value="26.667"/> <Setter Property="Margin" Value="12,0,12,2"/> </Style> <Style x:Key="GridDescriptionTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BasicTextStyle}"> <Setter Property="VerticalAlignment" Value="Bottom"/> <Setter Property="Margin" Value="12,0,12,60"/> </Style>
BasedOn="{StaticResource BasicTextStyle}"
行表示新的 Style 从 BasicTextStyle 继承我们没有明确设置的所有属性。我们可以应用Style="{StaticResource GridTitleTextStyle}"
这个属性,从而为 TextBlocks 或其他元素应用此风格。稍后在数据模板中,我们将使用这些风格。
要使我们的应用具有 Windows 团队博客网站的外观和感觉,除 Brush 和 Style 之外,我们还应使用自定义数据模板。我们已在显示数据部分介绍了数据模板。
添加日期控件模板
在 App.xaml 中,添加一个定义显示日期的方块的 ControlTemplate。在 App.xaml 中进行该定义,以使其可在 ItemsPage.xaml 和 SplitPage.xaml 中使用。
XAML<Application.Resources> <ResourceDictionary> ... <ControlTemplate x:Key="DateBlockTemplate"> <Canvas Height="86" Width="86" Margin="8,8,0,8" HorizontalAlignment="Left" VerticalAlignment="Top"> <TextBlock TextTrimming="WordEllipsis" TextWrapping="NoWrap" Width="Auto" Height="Auto" Margin="8,0,4,0" FontSize="32" FontWeight="Bold"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="month" /> </TextBlock.Text> </TextBlock> <TextBlock TextTrimming="WordEllipsis" TextWrapping="Wrap" Width="40" Height="Auto" Margin="8,0,0,0" FontSize="34" FontWeight="Bold" Canvas.Top="36"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="day" /> </TextBlock.Text> </TextBlock> <Line Stroke="White" StrokeThickness="2" X1="54" Y1="46" X2="54" Y2="80" /> <TextBlock TextWrapping="Wrap" Width="20" Height="Auto" FontSize="{StaticResource ContentFontSize}" Canvas.Top="42" Canvas.Left="60"> <TextBlock.Text> <Binding Path="PubDate" Converter="{StaticResource dateConverter}" ConverterParameter="year" /> </TextBlock.Text> </TextBlock> </Canvas> </ControlTemplate> ... </ResourceDictionary> </Application.Resources>
请注意,此模板定义参数“day”、“month”和“year”,这些参数将传递给我们之前在第 2 部分中创建的新 Convert 函数。
为项页面添加数据模板
在 ItemsPage.xaml 中,我们添加了以下资源以定义默认视图中网格项的外观。请注意,我们应用之前定义的新风格。
XAML<UserControl.Resources> ... <DataTemplate x:Key="DefaultGridItemTemplate"> <Grid HorizontalAlignment="Left" Width="250" Height="250"> <Border Background="{StaticResource BlockBackgroundBrush}" /> <TextBlock Text="{Binding Title}" Style="{StaticResource GridTitleTextStyle}"/> <TextBlock Text="{Binding Description}" Style="{StaticResource GridDescriptionTextStyle}" /> <StackPanel VerticalAlignment="Bottom" Orientation="Horizontal" Background="{StaticResource ListViewItemOverlayBackgroundBrush}"> <TextBlock Text="Last Updated" Margin="12,4,0,8" Height="42"/> <TextBlock Text="{Binding PubDate, Converter={StaticResource dateConverter}}" Margin="12,4,12,8" /> </StackPanel> </Grid> </DataTemplate> </UserControl.Resources>
在 ItemsPage.xaml 中,更新 itemGridView 的 ItemTemplate 属性以使用我们的 DefaultGridItemTemplate 资源,而不使用 StandardStyles.xaml 中定义的默认模板 Standard250x250ItemTemplate。
XAML<GridView x:Name="itemGridView" AutomationProperties.AutomationId="ItemsGridView" AutomationProperties.Name="Items" Margin="120,0,120,60" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" SelectionMode="None" IsItemClickEnabled="True" ItemTemplate="{StaticResource DefaultGridItemTemplate}" ItemClick="ItemView_ItemClick"/>
为拆分页面添加数据模板
在 SplitPage.xaml 中,添加以下资源以定义列表项在默认视图中的外观:
XAML<UserControl.Resources> ... <!-- green --> <SolidColorBrush x:Key="BlockBackgroundBrush" Color="#FF6BBD46"/> <DataTemplate x:Key="DefaultListItemTemplate"> <Grid HorizontalAlignment="Stretch" Width="Auto" Height="110" Margin="10,10,10,0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!-- Green date block --> <Border Background="{StaticResource BlockBackgroundBrush}" Width="110" Height="110" /> <ContentControl Template="{StaticResource DateBlockTemplate}" /> <StackPanel Grid.Column="1" HorizontalAlignment="Left" Margin="12,8,0,0"> <TextBlock Text="{Binding Title}" FontSize="26.667" TextWrapping="Wrap" MaxHeight="72" Foreground="#FFFE5815" /> <TextBlock Text="{Binding Author}" FontSize="18.667" /> </StackPanel> </Grid> </DataTemplate> ...</UserControl.Resources>
在 SplitPage.xaml 中,我们还更新了 itemListView 中的 ItemTemplate 属性,以使用我们的 DefaultListItemTemplate 资源而不是使用默认模板 Standard130ItemTemplate 。此处显示的是更新后的itemListView 的 XAML。
XAML<ListView x:Name="itemListView" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" Margin="120,0,0,60" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" SelectionChanged="ItemListView_SelectionChanged" ItemTemplate="{StaticResource DefaultListItemTemplate}"/>
在应用了我们的样式后,该应用就非常符合 Windows 团队博客网站的外观和感觉了:
通过使用样式和在其他样式基础上新建样式,我们可以为自己的应用快速定义和应用各种外观。在下个部分中,我们综合所有动画和样式知识来使应用在运行时能够流畅地适应各种布局和方向。
适应不同的布局
通常,应用会设计为以全屏幕方式横向查看。但 Metro 风格 UI 必须适应不同的方向和布局。具体来说,它必须对纵向和横向都支持。横向显示时,它必须支持“全屏幕”、“填充”和“对齐”布局。在从空白模板创建博客阅读器页面时,我们已看到它在纵向上显示不正常。在本部分,我们来了解一下如何使我们的应用在任何分辨率、任何方向均能显示正常。
在 Visual Studio 中进行开发时,你可以使用 Simulator 调试器来测试布局。只需按下 F5,即可以使用调试器工具栏来通过 Simulator 进行调试。
Visual Studio 模板包含处理视图状态更改的代码。此代码包含在 LayoutAwarePage.cs or LayoutAwarePage.vb 文件中,它会将我们的应用状态映射到 XAML 中定义的视觉状态。因为已为我们提供了页面布局逻辑,我们只需要提供要用于每种页面视觉状态的视图。
要使用 XAML 在不同视图间转换,应使用 VisualStateManger 为应用定义不同的 VisualState。此处,我们在 ItemsPage.xaml 中定义了一个 VisualStateGroup。该组中包含 4 个 VisualState,分别名为 FullScreenLandscape、Filled、FullScreenPortrait 和 Snapped。不能同时使用来自同一个 VisualStateGroup 的不同 VisualState 。 每个 VisualState 中都包含动画,用于指示应用需要对 UI 的 XAML 中指定的基准进行哪些更改。
<!--App Orientation States--> <VisualStateManager.VisualStateGroups> <VisualStateGroup> <VisualState x:Name="FullScreenLandscape" /> <VisualState x:Name="Filled"> ... </VisualState> <VisualState x:Name="FullScreenPortrait"> ... </VisualState> <VisualState x:Name="Snapped"> ... </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
当应用处于横向全屏幕视图时,使用 FullScreenLandscape 状态。因为我们正是针对此视图设计了默认的 UI,所以无需进行任何更改,这只是一个空的 VisualState。
当用户将另一个应用对齐到屏幕的一侧时,使用 Filled 状态。在此情况下,项视图页面只是移走,不需要更改。这也只是一个空的 VisualState。
当应用从横向旋转为纵向时,使用 FullScreenPortrait 状态。在此视觉状态中,有两个动画。一个用于更改“后退”按钮所用的样式,另一个用于更改 itemGridView 的页边距,以便所有内容显示都更好地与屏幕相吻合。在集合页面 UI 的 XAML 中,定义了一个 GridView 和一个 ListView 并将其绑定到数据集合。默认情况下,会显示 GridView,而 ListView 处于折叠状态。在 Portrait 状态中,包含三个动画,用于折叠 GridView 、显示 ListView 和更改“后退”按钮的 Style 以使其更小。
<!-- The entire page respects the narrower 100-pixel margin convention for portrait --> <VisualState x:Name="FullScreenPortrait"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="backButton" Storyboard.TargetProperty="Style"> <DiscreteObjectKeyFrame KeyTime="" Value="{StaticResource PortraitBackButtonStyle}"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridView" Storyboard.TargetProperty="Margin"> <DiscreteObjectKeyFrame KeyTime="" Value="100,0,90,60"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState>
当用户显示两个应用,而我们的应用是其中较窄的一个时,使用 Snapped 状态。在这种状态下,我们的应用的宽度仅为 320 设备无关像素 (DIP),因此还需要进一步更改。在项页面 UI 的 XAML 中,定义了一个 GridView 和一个 ListView 并将其绑定到数据集合。默认情况下,会显示 itemGridViewScroller,而 itemListViewScroller 处于折叠状态。在 Snapped 状态中,包含四个动画,用于折叠 itemListViewScroller 、显示 itemListViewScroller 和更改“后退”按钮的 Style 和页面标题以使其更小。
<!-- The Back button and title have different styles when they're snapped, and the list representation is substituted for the grid that's displayed in all other view states.--> <VisualState x:Name="Snapped"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="backButton" Storyboard.TargetProperty="Style"> <DiscreteObjectKeyFrame KeyTime="" Value="{StaticResource SnappedBackButtonStyle}"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="pageTitle" Storyboard.TargetProperty="Style"> <DiscreteObjectKeyFrame KeyTime="" Value="{StaticResource SnappedPageHeaderTextStyle}"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListScrollViewer" Storyboard.TargetProperty="Visibility"> <DiscreteObjectKeyFrame KeyTime="" Value="Visible"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridScrollViewer" Storyboard.TargetProperty="Visibility"> <DiscreteObjectKeyFrame KeyTime="" Value="Collapsed"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState>
在本教程的使用样式创建一致性外观部分,我们创建了用于自定义应用外观的样式和模板。默认的横向视图使用这些样式和模板。要在不同视图中保持自定义外观,还需要为这些视图创建自定义样式和模板。
为项页面对齐视图添加数据模板
在 ItemsPage.xaml 中,我们为网格项创建了一个数据模板。我们还需要为 Snapped 视图中显示的列表项提供新的数据模板。我们将此模板命名为 NarrowListItemTemplate 并将其添加到 ItemsPage.xaml 的资源部分,紧跟在 DefaultGridItemTemplate 资源之后。
XAML<UserControl.Resources> ... <!-- Used in Snapped view --> <DataTemplate x:Key="NarrowListItemTemplate"> <Grid Height="80"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Border Background="{StaticResource BlockBackgroundBrush}" Width="80" Height="80" /> <ContentControl Template="{StaticResource DateBlockTemplate}" Margin="-12,-12,0,0"/> <StackPanel Grid.Column="1" HorizontalAlignment="Left" Margin="12,8,0,0"> <TextBlock Text="{Binding Title}" MaxHeight="56" TextWrapping="Wrap"/> </StackPanel> </Grid> </DataTemplate> </UserControl.Resources>
要使 ListView 显示我们的新数据模板,应更新 itemListView 的 ItemTemplate 属性使用我们的 NarrowListItemTemplate 资源,而不使用 StandardStyles.xaml 中定义的默认模板 Standard80ItemTemplate。在 ItemsPage.xaml 中,使用以下代码片段替换 itemListView:
XAML<ListView x:Name="itemListView" AutomationProperties.AutomationId="ItemsListView" AutomationProperties.Name="Items" Margin="10,0,0,60" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" ItemTemplate="{StaticResource NarrowListItemTemplate}" SelectionMode="None" IsItemClickEnabled="True" ItemClick="ItemView_ItemClick"/>
为拆分页面对齐和填充视图添加数据模板
在 SplitPage.xaml 中,我们创建一个 ListView 模板以用于 Filled 和 Snapped 视图,并在屏幕宽度小于 1366 DIP 时用于 FullScreenLandscape 视图。我们将此模板命名为 NarrowListItemTemplate并将其添加到 SplitPage.xaml 的资源部分,紧跟在 DefaultListItemTemplate 资源之后。
XAML<UserControl.Resources> ... <!-- Used in Filled and Snapped views --> <DataTemplate x:Key="NarrowListItemTemplate"> <Grid Height="80"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Border Background="{StaticResource BlockBackgroundBrush}" Width="80" Height="80"/> <ContentControl Template="{StaticResource DateBlockTemplate}" Margin="-12,-12,0,0"/> <StackPanel Grid.Column="1" HorizontalAlignment="Left" Margin="12,8,0,0"> <TextBlock Text="{Binding Title}" MaxHeight="56" Foreground="#FFFE5815" TextWrapping="Wrap"/> <TextBlock Text="{Binding Author}" FontSize="12" /> </StackPanel> </Grid> </DataTemplate> ...</UserControl.Resources>
要使用此数据模板,需更新要使用该模板的视觉状态。在 Snapped 和 Filled 视觉状态的 XAML 中,我们发现了针对 itemListView 的 ItemTemplate 属性的动画。接着,我们更改了该值,以使用 NarrowListItemTemplate 资源而不使用默认的 Standard80ItemTemplate 资源。以下是更新后的动画 XAML。
XAML<VisualState x:Name="Filled"> <Storyboard> .... <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="ItemTemplate"> <DiscreteObjectKeyFrame KeyTime="" Value="{StaticResource NarrowListItemTemplate}"/> </ObjectAnimationUsingKeyFrames> .... </Storyboard> </VisualState> ...<VisualState x:Name="Snapped"> <Storyboard> .... <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="ItemTemplate"> <DiscreteObjectKeyFrame KeyTime="" Value="{StaticResource NarrowListItemTemplate}"/> </ObjectAnimationUsingKeyFrames> .... </Storyboard> </VisualState>
我们还使用自己的详细信息部分(该部分使用 WebView)替换了拆分页面的项详细信息部分。由于进行了此更改,Snapped_Detail 视觉状态中的动画将不再存在的元素作为目标。当我们使用此视觉状态时,这些动画将导致错误,因此我们必须将其删除。在 SplitPage.xaml 中,我们从 Snapped_Detail 视觉状态中删除这些动画。
XAML<VisualState x:Name="Snapped_Detail"> <Storyboard> ... <!-- REMOVE THESE ELEMENTS: --> <!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemDetailTitlePanel" Storyboard.TargetProperty="(Grid.Row)"> <DiscreteObjectKeyFrame KeyTime="0" Value="0"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemDetailTitlePanel" Storyboard.TargetProperty="(Grid.Column)"> <DiscreteObjectKeyFrame KeyTime="0" Value="0"/> </ObjectAnimationUsingKeyFrames>--> ... <!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemSubtitle" Storyboard.TargetProperty="Style"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource CaptionTextStyle}"/> </ObjectAnimationUsingKeyFrames>--> </Storyboard> </VisualState>
调整 Snapped 视图中的 WebView 边距
在 DetailPage.xaml 中,我们只需在 Snapped 视图中调整 WebView 的边距来使用所有可用空间。在Snapped 视觉状态的 XAML 中,我们添加一个动画来更改 contentViewBorder 上的 Margin 属性的值,如下所示:
XAML<VisualState x:Name="Snapped"> <Storyboard> ... <ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentViewBorder" Storyboard.TargetProperty="Margin"> <DiscreteObjectKeyFrame KeyTime="" Value="20,5,20,20"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState>
添加初始屏幕和徽标
我们的应用带给用户的第一印象来自于初始屏幕。当用户启动应用时将显示初始屏幕,该屏幕会在应用初始化资源时为用户提供即时反馈。当应用的第一个页面准备就绪可以显示时,它就会关闭。
初始屏幕由一种背景色和一个 624 x 304 像素的图像组成。我们在 Package.appxmanifest 文件中设置这些值。你可以在清单编辑器中打开此文件。在清单编辑器的“应用程序 UI”选项卡中,我们设置了初始屏幕图像的路径和背景色。项目模板提供名为 SplashScreen.png 的默认空白图像。我们将空白图像替换为自己的初始屏幕图像,该图像可以明确标识我们的应用,并立即将用户的注意力吸引到应用上来。以下是我们的博客阅读器的初始屏幕:
基本初始屏幕可以适用于我们的博客阅读器,但你也可以使用 SplashScreen 类的属性和方法扩展该初始屏幕。你可以使用 SplashScreen 类获取初始屏幕的坐标,然后利用这些坐标定位该应用的第一个页面。还可以掌握初始屏幕消失的时间,以确定启动应用的任何内容进入动画的时机。
总结
在本文章中,我们学习了如何使用 Visual Studio 11 Express Beta for Windows 8 中的内置页面模板创建多页应用,以及如何在页面之间导航和传递数据。我们学习了如何使用样式和模板以使我们的应用符合 Windows 团队博客网站的风格。我们还学习了如何使用主题动画、应用栏和初始屏幕来使应用更适合 Windows 8 的个性化内容。 最后,我们学习了如何根据各种布局和方向来调整我们的应用,从而让它始终保持美观。
- 使用 C++ 创建你的第一个 Metro 风格应用
- 使用 C# 或 Visual Basic 创建你的第一个 Metro 风格应用
- 使用 C# 或 Visual Basic 创建你的第一个 Metro 风格应用
- win8应用开发之一:创建一个使用 C#/VB 和 XAML 的 Metro 风格应用
- 使用 HTML 控件创建出色的 Metro 风格应用
- 创建你的第一个应用
- 用javascript创建第一个windows8 metro应用
- 创建“Hello, world”应用(使用 C#/VB 和 XAML 的 Metro 风格应用)摘自官网(存档)入门必须
- 使用C#开发Metro 风格应用的路线图 -- metro应用生命周期的处理
- 你的第一个iOS应用(一) 关于创建你的第一个iOS应用
- 使用C#开发Metro 风格应用的路线图 -- 移植wp7应用到metro上
- 使用C#开发Metro 风格应用的路线图 -- 开发环境
- 使用C#开发Metro 风格应用的路线图 -- metro设计原则
- 创建你的第一个TurboGears 2.1应用
- 创建你的第一个Windows通用应用(UWP)
- 如何快速创建你的第一个django应用
- 创建你的第一个Spring Boot应用
- Metro 风格应用的导航设计
- Mybatis中的#{}和${}有什么不同?
- java.lang.IllegalStateException: getWriter() has already been called for this response 解决办法
- swift 类型推导
- dropout的理解
- java调用dll动态库文件的一般总结
- 使用 C++ 创建你的第一个 Metro 风格应用
- 319. Bulb Switcher
- Android UI美化之 shape的使用及其属性总结
- centos安装rabbitmq
- 219. Contains Duplicate II---数组中两个重复的数字的下标最多相差k
- cocos2d-js 安装方式
- maven命名规则(转)
- Linux桌面系统各种字体配置
- 偏移向量并查集——A Bug's Life