WindowsMetro 风格应用专门针对 Windows 8 Consumer Preview 所提供的用户体验量身定制。每个出色的 Metro 风格应用都遵循特定的设计准则,这使得此类应用与传统的桌面应用相比外观更漂亮、反应更灵敏、行为更直观。开始创建 Metro 风格应用之前,建议你先阅读新模型的设计理论。你可以在设计 Metro 风格的应用中找到详细信息。
在此,我们介绍了有助于你使用 C++ 开发 Metro 风格应用的基本代码和概念,此类应用的 UI 使用可扩展应用程序标记语言 (XAML) 定义。
如果你希望使用其他编程语言,请参阅:
目标
开始编写代码之前,我们先来看一看你在使用 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 类型是必需的。
#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:
#include "FeedData.h" ...// In the BlankPage class... private: void GetFeedData(Platform::String^ feedUriString); FeedData^ feedData;
- 将这些行添加到BlankPage.xaml.cpp:
// BlankPage.xaml.cpp #include <ppltasks.h> ...using namespace Windows::Web::Syndication;using namespace Concurrency;
- 调用 InitializeComponent 后,将此行添加到 BlankPage 构造函数中:
feedData = ref new FeedData();
- 将方法实现添加到 BlankPage.xaml.cpp 中。有关此代码的详细信息,请参阅 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"
之后但位于右尖括号之前,从而使整个标记如下所示:<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:
//In the BlankPage class declaration... private: void PageLoadedHandler(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
-
在你的 BlankPage.xaml.cpp 文件中为事件处理程序方法添加存根实现:
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
特性,使整个元素如下所示:<Grid Background="{StaticResource ApplicationPageBackgroundBrush}" Name="Grid1">
-
在 BlankPage.xaml 中,为 Grid1 元素定义两行,方法是使用以下 XAML 片段作为 Grid 的第一个子节点,紧跟在起始标记之后。第一个行定义(第 0 行)中的
Height="140"
属性设置将顶行设置为具有绝对高度 140 像素。无论行的内容或应用的大小如何变化,此高度都不会改变。第二个行定义(第 1 行)中的Height="*"
设置指示底行接受第 0 行确定大小后剩余的任意大小的空间。这称为比例缩放。注意 只要你键入或粘贴代码,Visual Studio 就会自动提供正确的缩进。
<Grid.RowDefinitions> <RowDefinition Height="140" /> <RowDefinition Height="*" /> </Grid.RowDefinitions>
-
紧跟在行定义后,但仍在 Grid1 内,向第一行中添加以下 TextBlock 内容控件。这将保留源的主标题,因此,我们可以为其指定较大的字体。 我们为其提供一个 x:Name 属性,以便我们可以在 C++ 代码中引用该属性,并且还会提供一个在将数据绑定到该属性后显示的临时字符串。
<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 个相等的部分。两个部分用于第一列,三个部分用于第二列。<Grid Name="Grid2" Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="2*" /> <ColumnDefinition Width="3*" /> </Grid.ColumnDefinitions> </Grid>
-
紧跟在 Grid2 的列定义后面,在结束标记之前,添加下列 ListView 控件。由于未指定任何 Grid.Column 属性,因此,控件将放入 Grid2 的第 0 列中。我们暂且将内容留空。稍后我们将为其添加一些内容和一个事件处理程序
<ListView x:Name="ItemListView"></ListView>
-
在选择 ListView 标记后,继续在 Grid2 元素内添加第三个 Grid,它包含两行。为它指定 Name 特性 Grid3,并放在 Grid2 的右列中。
Height="Auto"
设置要求顶行将其高度设置为与内容相同。底行则占用剩下的所有空间。<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 类中为此控件创建变量,也会在运行时实例化该控件并完全正常工作。
<TextBlock Text="Blog Post Title" FontSize="24"/>
-
紧靠前一个 TextBlock 后面,添加一个 WebView 并将其放在 Grid 的底行中。此控件显示文章内容,包括图形。我们使用WebView而不是TextBlock或RichTextBlock,因为源内容的格式设置为 HTML。
<WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/>
- 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 中。这指示控件将模板应用于其项目集合中的每个项目。
<ListView.ItemTemplate> <DataTemplate> </DataTemplate> </ListView.ItemTemplate>
-
在 DataTemplate 中,添加一个包含三个 TextBlock 元素的 StackPanel,每个元素表示我们希望显示的三个 FeedItem 属性之一。 因为没有为 StackPanel 指定方向,TextBlock 元素将垂直排列。现在,我们仅为 Text 属性指定一些临时字符串,只是为了提醒我们它们表示的是什么内容。当你按 F5 时它们不会显示,因为这并不是实际的项,而只是展示项显示效果的模板。
<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 现在应如下所示:
<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 特性以绑定到源标题。
<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}"
,以使起始标记现在如下所示:<ListView x:Name=”ItemListView” ItemsSource=”{Binding Path=Items}” Margin=”10,0,0,10”>
现在,我们已将 ListView 控件绑定到 FeedData 项集合,我们可以绑定到作为 FeedItem 对象的各个项,但必须将其转换为 Platform::Object^ 类型才能在集合中存储它们。
将 DataTemplate 项绑定到FeedItem 属性
-
在第一个 TextBlock 中,使用
"{Binding Path=Title}"
替换临时文本,以使元素现在如下所示:<TextBlock Text="{Binding Path=Title}" FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" />
-
在第二个 TextBlock 中,使用
"{Binding Path=Author}"
替换临时文本,以使元素现在如下所示:<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 特性,使开始标记如下所示:
<Grid Name ="Grid3" Grid.Column="1" DataContext="{Binding ElementName=ItemListView, Path=SelectedItem}">
-
使用绑定表达式
"{Binding Path=Title}"
替换 WebView 上方的 TextBlock 中的临时文本,以使元素现在如下所示:<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,并向该文件中添加此类定义:
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:#include "DateConverter.h"
尽管在我们自己的代码隐藏中并未引用该文件,但我们仍需包含该文件,因为 Visual Studio 生成过程需要该文件来生成数据绑定代码。
-
在 BlankPage.xaml 中,将该类的一个实例声明为资源。将以下 Page.Resources 节点粘贴到开始 Page 标记和 Grid1 元素之间。
<Page.Resources> <local:DateConverter x:Key="dateConverter" /> </Page.Resources>
Page 标记已具有一个 XML 命名空间映射,使我们可以访问项目中在 SimpleBlogReader 命名空间中声明的类:
xmlns:local="using:SimpleBlogReader"
。如果没有该映射,我们将无法在此处看到 DateConverter 类。 - 现在我们可以将 PubDateTextBlock 绑定到 DateConverter:
<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属性并指定属性名称以调用事件处理程序方法,以使起始标记现在如下所示:
<ListView x:Name="ItemListView" ItemsSource="{Binding Path=Items}" SelectionChanged="ItemListView_SelectionChanged" Margin="10,0,0,10">
-
如同之前所创建的事件处理程序一样,现在我们必须在代码隐藏中创建事件处理程序。首先,将此行代码添加到 BlankPage.xaml.h:
// Declaration in BlankPage.xaml.h void ItemListView_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e);
-
在 BlankPage.xaml.cpp 中添加方法实现:
// 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^ 是一种临时的变通措施,在未来版本中没有必要这么做。):
property Platform::String^ Description;// Temporary workaround: // property Windows::Foundation::DateTime PubDate; Platform::Object^ PubDate;
某些源具有说明字段(如果我们还有空间可以显示),我们可以使用上一篇博客文章的日期作为某些布局中的
PubDate
。(请注意,PubDate
没有 ^(“尖帽号”),因为 DateTime 是一个值类型。) -
将此属性添加到 FeedItem 类。
property Windows::Foundation::Uri^ Link;
我们将使用详细信息页面中的此链接直接导航到博客文章,而不是导航到内容字符串。
修改初始化数据模型的异步代码
-
将此
#include
指令添加到 app.xaml.h:#include "FeedData.h"
-
将这两个专用方法签名添加到 app.xaml.h 中的 App 类:
void InitDataSource(Platform::Collections::Vector<Object^>^ fds); FeedData^ GetFeedData(Windows::Web::Syndication::SyndicationFeed^ feed);
-
将这些
#include
指令添加到app.xaml.cpp:#include <ppltasks.h> #include "ItemsPage.xaml.h"
-
将这些
namespace
声明添加到app.xaml.cpp:using namespace Platform::Collections;using namespace Windows::Web::Syndication;using namespace Concurrency;using namespace std;
-
将下列方法实现添加到 app.xaml.cpp:
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