使用 C# 或 Visual Basic 创建你的第一个 Metro 风格应用

1 篇文章 0 订阅
1 篇文章 0 订阅

转载一篇Win8 Metro风格应用开发的文章,这篇文章对我入门metro开发提供了极大的指导,希望对各位初学者有所帮助。原文链接http://msdn.microsoft.com/zh-cn/library/br211380


[本文档尚属初步文档,可能会随时发生更改。]

我们将向你介绍使用 C# 或 Microsoft Visual Basic 创建 Metro 风格应用所需的基本代码和概念。你将使用可扩展标记语言 (XAML) 来定义 UI 并选择编写应用逻辑的语言。

如果你希望使用其他编程语言,请参阅:

路线图:此主题与其他主题有何关联?请参阅:使用 C# 或 Visual Basic 的 Metro 风格应用的路线图

目标

在本教程中,我们将快速浏览构建 Metro 风格应用程序时使用的功能。在创建一个简单博客阅读器应用的过程中,我们会介绍使用 XAML 进行开发时的重要概念,包括布局、控件、模板和数据绑定。我们将学习使用编译到 Microsoft Visual Studio 11 Express Beta for Windows 8 中的页面模板和导航,以快速开始我们的应用开发。然后,我们会学习如何使用自定义样式来修改应用的外观以及如何将 UI 应用到各种不同的布局和视图。最后,我们会简单讨论如何将我们的应用与 Windows 8 Consumer Preview 相集成并将其发布到 Windows 应用商店。学完本教程后,你便可以开始构建你自己的 Metro 风格应用了。

阅读本教程大约需要 30 分钟,如果你还要做练习,则要花更长的时间。

Hello World

在使用 C# 或 Visual Basic 创建 Metro 风格应用时,通常会使用 XAML 定义 UI,并用选定的语言在关联的代码隐藏文件中编写应用逻辑。使用 C# 或 Visual Basic 编写的 Metro 风格应用的 XAML UI 框架位于 Windows 运行时的 Windows.UI.Xaml.* 命名空间中。如果你使用 Windows Presentation Foundation (WPF)、Silverlight 或 Silverlight for Windows Phone 编写过应用,那么你应该已经熟悉此编程模型了,并且能够利用这一经验使用 C++、C# 或 Visual Basic 来创建你的 Metro 风格应用。

此处的示例显示了定义简单的 Hello World 应用的 UI 及其关联的代码隐藏页面的 XAML。即使这一简单的示例,也显示了若干个对基于 XAML 的编程模型而言非常重要的概念,包括部分类、布局、控件、属性和事件。

XAML
<Page
    x:Class="HelloWorld.BlankPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:HelloWorld"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
        <StackPanel>
            <Button Content="Click Me" Click="HelloButton_Click" />
            <TextBlock x:Name="DisplayText" FontSize="48" />
        </StackPanel>
    </Grid>
</Page>

C#
VB
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

namespace HelloWorld
{
    public sealed partial class BlankPage : Page
    {
        public BlankPage()
        {
            InitializeComponent();
        }

        private void HelloButton_Click(object sender, RoutedEventArgs e)
        {
            DisplayText.Text = "Hello World";
        }
    }
}

Hello World 应用是很好的入门示例。但它还不足以把我们带向赢利之路,因此,我们在探索如何构建用于 Windows 8 的 Metro 风格应用时将采用一条不同的路径。我们用一个简单的博客阅读器作为入门示例应用,它可以下载和显示 RSS 2.0 或 Atom 1.0 源中的数据。Windows 团队博客站点中的信息提要似乎正适合使用。Developing for Windows 博客就是其中之一。

Developing for Windows 博客。

在 Visual Studio 中创建 Windows Metro 风格应用

在本部分中,你将学习如何:

  • 在 Visual Studio 11 Express Beta for Windows 8 中创建新的 Windows Metro 风格项目。

Visual Studio 是一个用于开发 Windows 应用的功能强大的集成开发环境 (IDE)。它可提供下列功能:源文件管理;集成式生成、部署和启动支持;XAML、Visual Basic、C#、C++、图形和清单编辑;以及调试等。Visual Studio 有好几个版本,但我们将使用的是 Visual Studio 11 Express Beta for Windows 8。你可以随用于 Metro 风格应用的 Windows 软件开发工具包 (SDK) 免费下载 Visual Studio,以便获得构建、封装和部署你的 Metro 风格应用所需的一切。

要开始创建应用,你需要使用 C# 或 Visual Basic 创建一个新的 Windows Metro 风格项目。Visual Studio 11 Express Beta for Windows 8 包含多个用于 Windows Metro 风格项目的模板,这些模板为你提供了如何创建使用各种布局的应用的良好开端。空白应用程序项目模板提供最少的创建所有 Windows Metro 风格应用所需的文件。

BR211380.wedge(zh-cn,WIN.10).gif创建新的 Windows Metro 风格项目

  1. 安装 Visual Studio 11 Express Beta for Windows 8
  2. 选择“文件”>“新建项目”。“新建项目”对话框即会打开。
  3. “已安装”窗格中,展开“Visual C#”或“Visual Basic”。
  4. 选择“Windows Metro 风格”模板类型。
  5. 在中心窗格中,选择“空白应用程序”。
  6. 输入项目名称。

    这是一个在 Visual Studio 11 Express Beta for Windows 8 中创建的新项目。

    Visual Studio“新建项目”对话框。

  7. 单击“确定”。你的项目文件已经创建完毕。

你创建项目时,Visual Studio 创建项目文件并在“解决方案资源管理器”中显示这些文件。让我们来看看空白应用程序模板所创建的文件。

文件名称描述
Properties/AssemblyInfo (.vb or .cs)包含嵌入到所生成的集合中的名称和版本元数据。
Package.appxmanifest包含描述你的应用的元数据,包括显示名称、说明、徽标和功能。
Assets/*你可以替换的默认徽标和初始屏幕图像。
Common/LayoutAwarePage具有能够适应不同布局和视图的页面功能的基类。
Common/RichTextColumns具有用于在列中部署的多格式文本的功能的基类。
Common/StandardStyles.xaml包含应用的默认样式和模板。
App.xaml,App.xaml.* (.vb, .cs)这些文件指定应用级逻辑。显示用户界面需要使用应用类。
BlankPage.xaml用于创建用户界面的默认起始页。
BlankPage.xaml.* (.vb, .cs)包含默认起始页的逻辑的代码隐藏文件。

若要了解有关这些文件和模板的详细信息,请参阅使用模板快速开始你的 Metro 风格应用(C#、C++、Visual Basic)

指定应用功能

在本部分中,你将学习如何:

  • 使用功能
  • 在应用程序清单设计器中指定应用的功能。

Metro 风格应用在安全容器中运行,对文件系统、网络资源和硬件具有有限的访问权限。 无论用户何时从 Windows 应用商店安装应用,Windows 都会查看文件中的元数据Package.appxmanifest以确定应用需要执行哪些功能。例如,某个应用可能需要访问 Internet 中的数据、用户文档库中的文档,或用户的摄像头和麦克风。当应用安装完成后,它会向用户显示所需的功能,而用户必须授予相应的权限才能让它访问这些资源。如果应用没有请求并接收所需要的某个资源的访问权限,则当用户运行它时,系统将禁止其访问该资源。

下面列出了一些常见的功能:

功能名称描述
文档库documentsLibrary允许应用访问用户的文档库,以及添加、更改或删除文件。你的应用只能访问已在清单中声明的文件类型,不能访问家庭组计算机上的文档库。
企业身份验证enterpriseAuthentication允许应用连接至需要域凭据的 Intranet 资源。
Internet(客户端和服务器)internetClientServer允许你的应用访问 Internet 和公用网络,允许通过 Internet 连接到你的应用。对重要端口的入站访问始终会被阻止。这是 Internet(客户端)功能的一个超集。你不用同时声明两者。
Internet(客户端)internetClient允许你的应用访问 Internet 和公用网络。大部分需要 Internet 访问的应用都应使用此功能。
位置location允许你的应用访问用户的当前位置。
麦克风microphone允许你的应用访问用户的麦克风。
音乐库musicLibrary允许你的应用访问用户的音乐库,并允许添加、更改或删除文件。还允许访问家庭组计算机上的音乐库以及本地连接的媒体服务器上的音乐文件类型。
图片库picturesLibrary允许你的应用访问用户的图片库,并允许添加、更改或删除文件。还允许访问家庭组计算机上的图片库,以及本地连接的媒体服务器上的图片文件类型。
专用网络(客户端和服务器)privateNetworkClientServer允许通过你的应用对用户信任的网络(例如家庭或企业网络)进行入站和出站访问。对重要端口的入站访问始终会被阻止。
近程proximity允许你的应用访问用户的接近现场通信 (NFC) 设备。
可移动存储removableStorage允许你的应用访问可移动存储设备,例如外部硬盘驱动器或 USB 闪存驱动器,并允许添加、更改或删除文件。你的应用只能访问已在清单中声明的文件类型。你的应用不能访问家庭组计算机上的可移动存储设备。
共享用户证书sharedUserCertificates允许你的应用访问软件和硬件证书,例如智能卡证书。
文本消息传递sms允许你的应用访问文本消息传递功能。
视频库videosLibrary允许你的应用访问用户的视频库,并允许添加、更改或删除文件。还允许访问家庭组计算机上的视频库,以及本地连接的媒体服务器上的视频文件类型。
网络摄像机webcam允许你的应用访问用户的照相机。

BR211380.wedge(zh-cn,WIN.10).gif向应用添加功能

  1. 在“解决方案资源管理器”中,双击 Package.appxmanifest。此时将在“应用程序清单设计器”中打开该文件。
  2. 在“应用程序清单设计器”中,选择“功能”选项卡。
  3. 选中你的应用所需的每项功能旁边的复选框。(“Internet(客户端)”默认处于选中状态。这是我们使用博客阅读器应用所全部需要的。)
  4. 保存并关闭文件。

指定某项功能时,该功能即会列在 Capabilities 元素下的 Package.appxmanifest.xml 文件中。如我们刚才看到的一样,你通常是在应用程序清单设计器中设置功能,但如果你右键单击文件,选择“打开方式…”,并在 XML 编辑器中打开该文件,则可以在 XML 中看到此 Capabilities 元素。

XML
<Capabilities>    
    <Capability Name="internetClient" />
</Capabilities>

将数据获取到应用

在本部分中,你将学习如何:

  • 创建自定义数据类
  • 异步检索 RSS 或 Atom 数据信息提要。

既然我们的应用可以从 Internet 下载数据了,我们便可以编写代码以将博客信息提要置于其中了。Windows 团队博客以 RSS 和 Atom 两种形式展示了文章的完整文本。我们希望在阅读器应用中显示的博客数据为每篇最新博客文章的标题、作者、日期和内容。

首先,我们需要下载每篇文章的数据。幸运的是,Windows 运行时包含一组类,这些类可以为我们执行处理信息提要数据的许多工作。我们可以在 Windows.Web.Syndication命名空间中可以找到这些类。可以直接使用这些类显示 UI 中的数据。但在我们的博客阅读器中,我们将创建属于自己的数据类。这赋予了我们更多的灵活性,还可以让我们以相同的方式处理 RSS 和 Atom 信息提要。

BR211380.wedge(zh-cn,WIN.10).gif向项目添加新的类文件

  1. 选择“项目”>“添加类”。“新增项目”对话框即会打开。
  2. 输入类文件的名称。在此示例中,我们使用 FeedData
  3. 单击“添加”。此时,新类文件便已创建完毕。

在我们的博客阅读器应用中,我们使用 3 个类来容纳和检索信息提要数据。我们将所有 3 个类都放在一个名为 FeedData(.cs 或 .vb)的文件中。FeedData 类容纳有关 RSS 或 Atom 信息提要的信息。FeedItem 类容纳有关信息提要所包含的单个博客文章的信息。FeedDataSource 类包含信息提要的机会以及从网络检索信息提要的方法。以下是这些类的代码。

C#
VB
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Windows.Web.Syndication;

namespace WindowsBlogReader
{
    // FeedData
    // Holds info for a single blog feed, including a list of blog posts (FeedItem)
    public class FeedData
    {
        public string Title { get; set; }
        public string Description { get; set; }
        public DateTime PubDate { get; set; }

        private List<FeedItem> _Items = new List<FeedItem>();
        public List<FeedItem> Items
        {
            get
            {
                return this._Items;
            }
        }
    }

    // FeedItem
    // Holds info for a single blog post
    public class FeedItem
    {
        public string Title { get; set; }
        public string Author { get; set; }
        public string Content { get; set; }
        public DateTime PubDate { get; set; }
        public Uri Link { get; set; }
    }

    // FeedDataSource
    // Holds a collection of blog feeds (FeedData), and contains methods needed to
    // retreive the feeds.
    public class FeedDataSource
    {
        private ObservableCollection<FeedData> _Feeds = new ObservableCollection<FeedData>();
        public ObservableCollection<FeedData> Feeds
        {
            get
            {
                return this._Feeds;
            }
        }

        public async Task GetFeedsAsync()
        {
            Task<FeedData> feed1 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/developers/atom.aspx");
            Task<FeedData> feed2 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/windowsexperience/atom.aspx");
            Task<FeedData> feed3 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/extremewindows/atom.aspx");
            Task<FeedData> feed4 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/business/atom.aspx");
            Task<FeedData> feed5 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/bloggingwindows/atom.aspx");
            Task<FeedData> feed6 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/windowssecurity/atom.aspx");
            Task<FeedData> feed7 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/springboard/atom.aspx");
            Task<FeedData> feed8 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/windowshomeserver/atom.aspx");
            // There is no Atom feed for this blog, so we use the RSS feed.
            Task<FeedData> feed9 = 
                GetFeedAsync("http://windowsteamblog.com/windows_live/b/windowslive/rss.aspx");
            Task<FeedData> feed10 = 
                GetFeedAsync("http://windowsteamblog.com/windows_live/b/developer/atom.aspx");
            Task<FeedData> feed11 = 
                GetFeedAsync("http://windowsteamblog.com/ie/b/ie/atom.aspx");
            Task<FeedData> feed12 = 
                GetFeedAsync("http://windowsteamblog.com/windows_phone/b/wpdev/atom.aspx");
            Task<FeedData> feed13 = 
                GetFeedAsync("http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx");

            this.Feeds.Add(await feed1);
            this.Feeds.Add(await feed2);
            this.Feeds.Add(await feed3);
            this.Feeds.Add(await feed4);
            this.Feeds.Add(await feed5);
            this.Feeds.Add(await feed6);
            this.Feeds.Add(await feed7);
            this.Feeds.Add(await feed8);
            this.Feeds.Add(await feed9);
            this.Feeds.Add(await feed10);
            this.Feeds.Add(await feed11);
            this.Feeds.Add(await feed12);
            this.Feeds.Add(await feed13);
        }

        private async Task<FeedData> GetFeedAsync(string feedUriString)
        {
            // using Windows.Web.Syndication;
            SyndicationClient client = new SyndicationClient();
            Uri feedUri = new Uri(feedUriString);

            try
            {
                SyndicationFeed feed = await client.RetrieveFeedAsync(feedUri);

                // This code is executed after RetrieveFeedAsync returns the SyndicationFeed.
                // Process it and copy the data we want into our FeedData and FeedItem classes.
                FeedData feedData = new FeedData();

                feedData.Title = feed.Title.Text;
                if (feed.Subtitle.Text != null)
                {
                    feedData.Description = feed.Subtitle.Text;
                }
                // Use the date of the latest post as the last updated date.
                feedData.PubDate = feed.Items[0].PublishedDate.DateTime;

                foreach (SyndicationItem item in feed.Items)
                {
                    FeedItem feedItem = new FeedItem();
                    feedItem.Title = item.Title.Text;
                    feedItem.PubDate = item.PublishedDate.DateTime;
                    feedItem.Author = item.Authors[0].Name.ToString();
                    // Handle the differences between RSS and Atom feeds.
                    if (feed.SourceFormat == SyndicationFormat.Atom10)
                    {
                        feedItem.Content = item.Content.Text;
                        feedItem.Link = new Uri("http://windowsteamblog.com" + item.Id);
                    }
                    else if (feed.SourceFormat == SyndicationFormat.Rss20)
                    {
                        feedItem.Content = item.Summary.Text;
                        feedItem.Link = item.Links[0].Uri;
                    }
                    feedData.Items.Add(feedItem);
                }
                return feedData;
            }
            catch (Exception)
            {
                return null;
            }
        }
    }
}


检索信息提要数据

在准备好用于容纳我们的数据的数据类后,我们回过头来下载这些博客信息提要。Windows.Web.Syndication.SyndicationClient 类可检索完全解析的 RSS 或 Atom 信息提要,因此,我们不用担心解析 XML 的问题,而可以继续构建应用中更加有趣的部分。 SyndicationClient类只提供一种检索信息提要的方法,并且是异步的。异步编程模型在 Windows 运行时中常常用于帮助应用保持响应。幸运的是,程序已经为我们处理好了在使用异步方法时可能会遇到的许多复杂问题。

在 C# 和 Visual Basic 中使用 await

如果在 C# 和 Visual Basic 中使用await关键字,则以异步方式检索信息提要的代码将与以同步方式检索信息提要时所使用的代码相似。我们来看一下。

GetFeedsAsync 方法中,我们针对每个我们想要检索的博客信息提要调用 GetFeedAsync。只要可能,我们便会在 URL 中对 Atom 信息提要进行传递,因为它包含我们想要显示的作者数据。当每个博客信息提要返回时,我们将其添加到 FeedDataSource.Feeds 集合。

C#
VB
public async Task GetFeedsAsync()
{
    Task<FeedData> feed1 = 
        GetFeedAsync("http://windowsteamblog.com/windows/b/developers/atom.aspx");
    ...
    this.Feeds.Add(await feed1);
    ...
}

现在,让我们更详细地来看看 GetFeedAsync 方法,以了解 await 关键字是如何为我们提供帮助的。第一个要注意的是:我们将 async 关键字添加到方法签名中。

C#
VB
private async Task<FeedData> GetFeedAsync(string feedUriString)
{
...
}

你只能在被定义为async的方法中使用await关键字。我们将返回类型指定为 Task<FeedData>。这是告诉编译器生成表示方法检索的 FeedData 对象的 Task

在方法内,我们实例化一个 SyndicationClient 并调用其 RetrieveFeedAsync 方法来获取包含我们需要的 RSS 或 Atom 信息的 SyndicationFeed

C#
VB
SyndicationFeed feed = await client.RetrieveFeedAsync(feedUri);

此处的await关键字会告诉编译器在后台自动为我们执行多种处理。编译器会将该方法中位于此调用之后的其余部分作为此调用返回后将要执行的回调。它会紧接着将控制返回给调用会话(通常是 UI 会话),以使应用保持响应。此时,会将表示此方法的最终输出的 Task(一个FeedData 对象)返回给调用者。

RetrieveFeedAsync 返回包含我们所需数据的 SyndicationFeed 时,将执行我们的方法中其余的代码。重要的是,系统在我们从中进行原始调用的相同会话上下文(UI 会话)中执行这些代码,因此当我们需要在此代码中更新 UI 时不必担心使用调度程序。检索到 SyndicationFeed 后,我们将需要的部分复制到 FeedDataFeedItem 数据类中。

C#
VB
// This code is executed after RetrieveFeedAsync returns the SyndicationFeed.
// Process it and copy the data we want into our FeedData and FeedItem classes.
FeedData feedData = new FeedData();

feedData.Title = feed.Title.Text;
if (feed.Subtitle.Text != null)
{
    feedData.Description = feed.Subtitle.Text;
}
// Use the date of the latest post as the last updated date.
feedData.PubDate = feed.Items[0].PublishedDate.DateTime;

foreach (SyndicationItem item in feed.Items)
{
    FeedItem feedItem = new FeedItem();
    feedItem.Title = item.Title.Text;
    feedItem.PubDate = item.PublishedDate.DateTime;
    feedItem.Author = item.Authors[0].Name.ToString();
    // Handle the differences between RSS and Atom feeds.
    if (feed.SourceFormat == SyndicationFormat.Atom10)
    {
        feedItem.Content = item.Content.Text;
        feedItem.Link = new Uri("http://windowsteamblog.com" + item.Id);
    }
    else if (feed.SourceFormat == SyndicationFormat.Rss20)
    {
        feedItem.Content = item.Summary.Text;
        feedItem.Link = item.Links[0].Uri;
    }
    feedData.Items.Add(feedItem);
}
return feedData;

当我们执行到 return 语句时,我们并未真正返回 FeedData 对象。请记住,当方法紧随 await 语句返回给调用程序时,会返回一个表示该方法的最终输出结果的 Task。现在,我们就已经最终获得想要的结果了。行 return feedData; 将作为方法结果的 FeedData 对象提供给正在等待该对象的 Task

GetFeedsAsync 方法中的该行在等待 Task

C#
VB
this.Feeds.Add(await feed1);

Task 获得正在等待的 FeedData 结果后,代码执行将继续,FeedData 将被添加到 FeedDataSource.Feeds 集合中。

使用应用中的数据

为了使用我们的应用中的数据,我们在 App.xaml.cs/vb 中创建了数据源的一个静态实例。我们将实例命名为 DataSource

C#
VB
sealed partial class App : Application
{
    // Add a static instance of FeedDataSource.
    public static FeedDataSource DataSource;

    public App()
    {
        this.InitializeComponent();

        // Instantiate the data source.
        DataSource = new FeedDataSource();
    }

   ...

}

页面模板已经在它的代码隐藏文件中包含一个 OnNavigatedTo 方法的替代。我们将代码置于此方法中,以获得应用的 FeedDataSource 实例并获得源。首先,我们将 async 关键字添加到方法声明,因为我们在方法中使用 await 关键字。导航到页面时,我们检查以查看 FeedDataSource 是否已包含源。如果未包含,我们调用 FeedDataSource.GetFeedsAsync 方法。然后,将页面的 DataContext 设置为第一个源。 以下是 BlankPage.xaml.cs/vb 的相关代码。

C#
VB
        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            FeedDataSource _feedDataSource = App.DataSource;

            if (_feedDataSource.Feeds.Count == 0)
            {
                await _feedDataSource.GetFeedsAsync();
            }

            this.DataContext = (_feedDataSource.Feeds).First();
        }

在 XAML 中定义应用布局

注意 在接下来的在 XAML 中定义应用布局添加控件和内容显示数据 3 个部分中,我们将学习在 XAML 中创建用户界面的基本知识。为了学习这些基本知识,我们创建了一个简单的单页博客阅读器,以显示单个博客信息提要的文章。如果你已经有使用 XAML 的经验并且熟悉 XAML 布局、控件和数据绑定,则可以跳过这些部分且不用完成练习。但不要跳过为日期转换器添加代码的部分,稍后在我们的应用中会用到它。让我们回到添加页面和导航部分中的创建完整的 Metro 风格应用。

在本部分中,我们将学习:

  • 可以在 XAML 中使用哪些面板来定义布局
  • 如何在 Grid 中定义行和列
  • 如何使用 StackPanel

应用布局用于指定应用中每个对象的大小和位置。要定位视觉对象,你必须将其置于某个Panel控件或其他容器对象中。XAML 布局系统提供了各种 Panel 控件,例如用作你在其中排列控件的 GridCanvas StackPanel

XAML 布局系统既支持绝对布局,也支持动态布局。在绝对布局中,使用显式的 x 和 y 坐标(例如,使用 Canvas)来定位控件。在动态布局中,当应用重新调整大小时,布局容器和控件会随之自动改变大小和位置(例如使用StackPanelGrid的情况)。在实际过程中,通常通过结合使用绝对布局和动态布局的方式,以及将面板嵌入到其他面板的方式来定义应用的布局。

博客阅读器应用的典型布局如下:顶部为标题,左侧是文章列表,右侧是所选文章的内容。

布局的示例。

默认情况下,空白应用模板仅包含一个空白 Grid,它是我们的 UI 的根元素。为了指定我们的布局,我们将Grid划分为两行。首行显示博客标题。在第二行中,我们嵌入另一个Grid,将它分为两列,然后添加其他一些布局容器以显示博客内容。

XAML
    <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="140" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!-- Title -->

        <!-- Content -->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*" MinWidth="320" />
                <ColumnDefinition Width="3*" />
            </Grid.ColumnDefinitions>

            <!-- Left column -->
            <ListView x:Name="ItemListView" />

            <!-- Right column -->
            <Grid Grid.Column="1" >
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
            </Grid>
        </Grid>
    </Grid> 


我们更详细地看看此 XAML 可以做什么。若要在 Grid 中定义行,你需要在 Grid.RowDefinitions 集合中添加 RowDefinition 对象。你可以在 RowDefinition 中设置属性,以指定行的外观。添加列的方法是相同的,只不过要使用 ColumnDefinition 对象和 Grid.ColumnDefinitions 集合。

在 XAML 中,行定义的外观如下:

XAML
<Grid.RowDefinitions>
  <RowDefinition Height="140"/>
  <RowDefinition Height="*"/>
</Grid.RowDefinitions>


第一个行定义(行 0)上的 Height="140" 属性设置将顶部的行设置为 140 个与设备无关的像素的绝对高度。无论行的内容或应用的大小如何变化,此高度都不会改变。第二个行定义(行 1)上的 Height="*" 设置告诉底部行占用行 0 占用后的所有空间。这通常称为星形比例缩放。我们在第二个Grid的列定义中也使用了比例缩放。宽度设置Width="2*"Width="3*"要求Grid将自身分为 5 个相等的部分。两个部分用于第一列,三个部分用于第二列。

要在Grid中定位某个元素,你需要设置该元素的附加属性Grid.Row Grid.Column。行和列编号是从零开始的。这些属性的默认值是 0,因此如果未设置任何内容,则该元素将位于第一行第一列。

<Grid Grid.Row="1"> 元素在根 Grid 的底部行嵌入一个 Grid。该Grid被划分为两列。

元素<ListView x:Name="ItemListView">将一个ListView添加到底部的左侧列中Grid。元素<Grid Grid.Column="1">将另一个Grid添加到底部的右侧列中Grid。我们将该Grid划分为两行。设置Height="Auto"要求顶行尽可能地调整高度以适合其内容。底行则占用剩下的所有空间。

我们的 UI 中需要布局面板的最后一个部分是博客文章列表。在该列表中,我们需要按如下所示排列标题、作者和日期。

StackPanel 的示例。

当你需要在页面 UI 的一个小的子部分中自动排列连续元素时,通常使用一个 StackPanelStackPanel是一种简单的布局面板,可以将子元素按水平或垂直方向排列到单行中。你可以使用StackPanel.Orientation属性来指定子元素的方向。 Orientation属性的默认值是 Orientation.Vertical。我们使用StackPanel来排列博客文章列表中的项。我们看到在使用模板设置数据格式会用到它。StackPanel 的 XAML 如下所示。

XAML
<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}" FontSize="16" Margin="15,0,0,0"/>
</StackPanel>

添加控件和内容

在本部分中,你将学习如何:

  • 向应用添加控件

布局面板虽然至关重要,但如果其中没有内容,势必索然无味。 通过添加按钮、列表、文本、图形和图像等控件创建应用的 UI。你所使用的元素取决于你的应用要完成的功能。有关可以在我们的 Metro 风格应用中使用的控件的列表,请参阅控件列表

看看我们的博客阅读器 UI 的草稿,可以清楚地看到,我们需要显示一行文本(博客和文章标题)、多行文本(文章内容)和博客文章列表。我们通过添加TextBlock控件以显示标题,并且用一个ListView控件来显示博客文章列表。初看之下,我们似乎可以使用一个多行的TextBlockRichTextBlock来显示文章内容。但是,当我们更深入了解时,我们发现包含文章内容的字符串不是纯文本,而是 HTML 字符串。我们不想显示一堆 HTML 标记,但如果我们将字符串放在一个 TextBlock 中便会发生这种情况,因此我们使用 WebView 控件来显示 HTML。

添加控件后,我们的 UI 所用的 XAML 现在看起来类似于以下所示。

XAML
<Page
    x:Class="WindowsBlogReader.BlankPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WindowsBlogReader"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="140" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!-- Title -->
        <TextBlock x:Name="TitleText" Text="{Binding Title}"
                   VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0"/>

        <!-- Content -->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*" MinWidth="320" />
                <ColumnDefinition Width="3*" />
            </Grid.ColumnDefinitions>

            <!-- Left column -->
            <!-- The default value of Grid.Column is 0, so we do not need to set it   
                 to make the ListView show up in the first column. -->
            <ListView x:Name="ItemListView"  
                      ItemsSource="{Binding Items}"
                      Margin="60,0,0,10">
            </ListView>

            <!-- Right column -->
            <!-- We use a Grid here instead of a StackPanel so that the WebView sizes correctly. -->
            <Grid DataContext="{Binding ElementName=ItemListView, Path=SelectedItem}"
                  Grid.Column="1" Margin="25,0,0,0">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
                <TextBlock x:Name="PostTitleText" Text="{Binding Title}" FontSize="24"/>
                <WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/>
            </Grid>
        </Grid>
    </Grid>
</Page>

显示数据

在本部分中,你将学习如何:

  • 将数据绑定到 UI
  • 使用模板设置数据格式。
将数据绑定到 UI

在我们最初创建的 Hello World 应用中,我们通过在此按钮单击事件处理程序中设置 TextBlock Text 属性,更新了 UI 中的文本。

C#
VB
private void HelloButton_Click(object sender, RoutedEventArgs e)
{
    DisplayText.Text = "Hello World";
}

有时像这样在代码中设置Text属性即可奏效。但要显示数据,你通常需要使用数据绑定将数据源连接到 UI。建立绑定时以及数据源发生变化时,绑定到数据源的 UI 元素会自动反映变化。类似地,用户在 UI 元素中所做的更改也会反映在数据源中。例如,如果用户编辑了TextBox中的值,则绑定引擎将自动更新基础数据源以反映该变化。

在博客阅读器应用中,我们将标题TextBlockText属性绑定到某个源对象的Title属性,以此来显示博客标题。

XAML
<TextBlock x:Name="TitleText" Text="{Binding Title}"
           VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0" />


我们采用同样的方式显示所选博客文章的标题。

XAML
<TextBlock x:Name="PostTitleText" Text="{Binding Title}" FontSize="24"/>

且慢!如果两个TextBlock都绑定到同一个源对象,那它们如何显示不同的标题呢?答案就在每个TextBlock所绑定的DataContext中。使用 DataContext 属性可设置整个 UI 元素的默认绑定,包括其所有子元素。有时你为整个页面设置DataContext属性,而有时你需要为页面上的个别元素设置该属性。每个 XAML 级别的 DataContext 设置都会替代更高一级的所有设置。此外,你还可以通过设置个别绑定的Source属性来替代针对该绑定的任何已生效的DataContext设置。

在博客阅读器应用中,我们为代码隐藏的整个页面设置 DataContext。注意,在检索到数据信息提要之后,我们将使用这行代码设置DataContext

C#
VB
        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            ...
            this.DataContext = (_feedDataSource.Feeds).First();
        }

第一个 TextBlock 的上下文是 FeedData 对象,因此它显示 FeedData.Title 属性。

第二个TextBlock如何显示所选博客文章的标题?如下所示,第二个TextBlock位于一个Grid中。

XAML
<Grid DataContext="{Binding ElementName=ItemListView, Path=SelectedItem}" 
      Grid.Column="1" Margin="25,0,0,0">
      ...
      <TextBlock x:Name="PostTitleText" Text="{Binding Title}" FontSize="24"/>
      ...
</Grid>

GridDataContextListViewSelectedItem属性绑定。因此绑定引擎将再次为我们执行后台操作。只要ListView中的选择发生变化,StackPanelDataContext就会自动更新为所选文章。 GridDataContext会替代页面的DataContext,因此第二个TextBlock将显示所选博客文章的FeedItem.Title属性。

每个绑定都会有一个 Mode 属性来规定更新数据的方式和时间。

绑定模式描述
OneTime仅当首次创建绑定时,才会在目标上设置值,之后再也不会更新。
OneWay如果源发生变化,目标将更新。
TwoWay如果源或目标之一发生变化,则目标和源都会更新。

如果你使用 OneWayTwoWay 绑定,要使绑定收到源对象变化的通知,必须实现 INotifyPropertyChanged 接口。有关数据绑定的详细信息,请参阅 快速入门:
使用数据模板设置数据格式

比起单纯地设置绑定,在列表视图中显示我们需要的数据则稍微复杂一点。我们将 ListView 绑定到 FeedData 对象的 Items 属性,因此,正确的数据已准备就绪。但如果我们就这样运行该应用, ListView并不知道要显示什么内容,因此它只会调用自己所绑定对象中的ToString 。这为我们提供了一个如下所示的“WindowsBlogReader.FeedItem”字符串列表。很显然,这不是我们想要显示的内容。

未使用模板的绑定列表。

我们可以通过设置ListView中的DisplayMemberPath属性对它做一点改进。此操作指示列表视图调用绑定对象的指定属性(而不是对象本身)上的 ToString。如果我们设置 DisplayMemberPath=TitleListView将显示博客文章标题的列表。

XAML
<ListView x:Name="ItemListView"  
                  ItemsSource="{Binding Items}"
                  DisplayMemberPath="Title"
                  Margin="60,0,0,10">
</ListView>

这就更接近我们要显示的内容了。

一个显示了标题的绑定列表。

但我们实际想要显示的是列表中每篇文章的标题、作者和发布日期。为此,我们定义一个模板,它会确切地告诉ListView我们希望如何显示数据。用于查看数据项集合的控件是由ItemsControl类派生的。这些控件具有ItemTemplate属性,我们可以用某个DataTemplate为其赋值。 DataTemplate定义了数据的显示方式。

要点 你不能同时使用一个 DisplayMemberPath 和一个 ItemTemplate。向 ListView 添加 ItemTemplate 时,请确保删除 DisplayMemberPath 设置。

以下是使用 DataTemplate 定义的级联(而不是 DisplayMemberPath)的 ListView 的 XAML。

XAML
<ListView x:Name="ItemListView"  
          ItemsSource="{Binding Items}"
          Margin="60,0,0,10">
    <ListView.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding Title}"  
                           FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" />
                <TextBlock Text="{Binding Author}" 
                           FontSize="16" Margin="15,0,0,0"/>
                <TextBlock Text="{Binding PubDate}" 
                           FontSize="16" Margin="15,0,0,0"/>
            </StackPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

DataTemplate 内定义 UI 的位与定义任何其他 UI 的方式相同。我们使用一个StackPanel依次垂直堆放 3 个TextBlock 。然后,我们将 TextBlock Text 属性绑定到 TitleAuthorPubDate 属性。这些绑定的默认 DataContext 是显示在 ListView 中的对象,它是一个 FeedItem

以下是当我们使用模板定义列表外观时的样子。

一个使用了数据模板的绑定列表。
使用值转换器设置数据格式

在 ItemListView 的DataTemplate中,我们将PubDate属性(是一个DateTime )绑定到TextBlock.Text属性。绑定引擎会自动将 PubDate 从一个 DateTime 转换为一个字符串。但自动转换会同时显示日期和时间,而我们只想显示日期。要修复此问题,我们可以创建自己的值转换器来将DateTime转换为字符串,并且可以在其中将字符串设置为任何需要的格式。

要创建值转换器,我们先创建一个用于实现IValueConverter接口的类,然后实现ConvertConvertBack方法。转换器可以将数据从一种类型更改为另一种类型,根据文化背景转换数据,或者修改数据呈现方式的其他方面。在这里,我们创建一个日期转换器,它将转换传入的日期值并设置其格式,从而只显示日、月和年。

注意 要向项目添加新类,请选择“项目”>“添加类”。将类命名为 DateConverter(.cs 或 .vb)。

ConvertConvertBack方法还允许你传入一个参数,以便通过不同的选项使用该转换器的同一个实例。在此示例中,我们包含了一个格式设置转换器,它可以根据输入的参数生成不同格式的日期。你可以使用Binding类的ConverterParameterConvertConvertBack方法中传递一个参数。稍后,我们将修改列表视图来演示上述内容。

C#
VB
using System;
using Windows.Globalization.DateTimeFormatting;

namespace WindowsBlogReader
{
    public class DateConverter : Windows.UI.Xaml.Data.IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string culture)
        {
            if (value == null)
                throw new ArgumentNullException("value", "Value cannot be null.");

            if (!typeof(DateTime).Equals(value.GetType()))
                throw new ArgumentException("Value must be of type DateTime.", "value");
            
            DateTime dt = (DateTime)value;

            if (parameter == null)
            {
                // Date "7/27/2011 9:30:59 AM" returns "7/27/2011"
                return DateTimeFormatter.ShortDate.Format(dt);
            }           
            else if ((string)parameter == "day")
            {
                // Date "7/27/2011 9:30:59 AM" returns "27"
                DateTimeFormatter dateFormatter = new DateTimeFormatter("{day.integer(2)}");
                return dateFormatter.Format(dt);
            }
            else if ((string)parameter == "month")
            {
                // Date "7/27/2011 9:30:59 AM" returns "JUL"
                DateTimeFormatter dateFormatter = new DateTimeFormatter("{month.abbreviated(3)}");
                return dateFormatter.Format(dt).ToUpper();
            }
            else if ((string)parameter == "year")
            {
                // Date "7/27/2011 9:30:59 AM" returns "2011"
                DateTimeFormatter dateFormatter = new DateTimeFormatter("{year.full}");
                return dateFormatter.Format(dt);
            }
            else
            {
                // Requested format is unknown. Return in the original format.
                return dt.ToString();
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, string culture)
        {
            string strValue = value as string;
            DateTime resultDateTime;
            if (DateTime.TryParse(strValue, out resultDateTime))
            {
                return resultDateTime;
            }
            return Windows.UI.Xaml.DependencyProperty.UnsetValue;
        }
    }
}


使用 DateConverter 类之前,必须在我们的 XAML 中声明一个该类的实例。我们将关键字 dateConverter 作为 App.xaml 中应用资源来声明实例。在此处声明实例后,在应用的每个页面都可以使用它。

XAML
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary>
                    <local:FeedDataSource x:Key="feedDataSource"/>
               
                    <!-- Add the DateConverter here. -->
                    <local:DateConverter x:Key="dateConverter" />

                </ResourceDictionary>
                ...
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>


现在,我们便可在绑定中使用 DateConverter 了。以下是来自 ItemListView DataTemplate 中更新后的 PubDate 绑定。

XAML
<TextBlock Text="{Binding Path=PubDate, Converter={StaticResource dateConverter}}" 
           FontSize="16" Margin="15,0,0,0"/>

通过此 XAML,绑定引擎使用我们的自定义 DateConverter DateTime 更改为字符串。它返回的字符串已设置为我们需要的格式,只含有日、月和年。

在 WebView 中显示 HTML

在页面中显示博客文章的最后一步是,获取文章数据以在 WebView 控件中显示。我们已经在我们的 UI 中添加了这一控件 ContentView

XAML
<WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/>

WebView 控件为我们提供了一种在应用内承载 HTML 数据的方法。但如果我们看看它的 Source 属性,就会发现它采用 Web 页面的 Uri 进行显示。我们的 HTML 数据只不过是HTML 的字符串。它没有包含可以绑定到 Source 属性的 Uri。幸运的是,我们可以通过一种 NavigateToString 方法来传递我们的 HTML 字符串。

要实现该功能,我们需要处理ListViewSelectionChanged事件。以下是添加了 SelectionChanged 事件的 ListView 的 XAML。

XAML
<ListView x:Name="ItemListView"  
                  ItemsSource="{Binding Items}"
                  Margin="60,0,0,10"
                  SelectionChanged="ItemListView_SelectionChanged">
    ...

我们向 BlankPage.xaml.cs/vb 代码隐藏页添加事件处理程序。在事件处理程序中,我们将所选项目转换为一个 FeedItem,并从 Content 属性获取 HTML 的字符串。然后,我们将该字符串传递给 NavigateToString 方法。

C#
VB
private void ItemListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    FeedItem feedItem = e.AddedItems[0] as FeedItem;
    if (feedItem != null)
    {
        // Navigate the WebView to the blog post content HTML string.
        ContentView.NavigateToString(feedItem.Content);
    }
}

以下是该应用运行时的情况。

这是已完成的博客阅读器应用。

这是一个不错的基本页面,在大多数情况下可以正常运行。但是 Metro 风格应用需要在所有情况下都能够运行良好。它必须适应各种设备上不同的分辨率、方向和视图。例如,以下是纵向查看时页面的显示情况。

BR211380.xaml_SimpleBlogReaderPortrait(zh-cn,WIN.10).png

你可以看到,在此方式下我们基本页面的效果不如之前那么好。我们希望我们的应用看起来美观,并且能更好地反映 Windows 团队博客的风格。为了实现这些目标,我们使用 Microsoft Visual Studio 中提供的页面模板作为开始。然后,我们使用在创建此页面的过程中学到的知识来修改模板并创建我们的完整应用。

添加页面和导航

为了使我们的博客阅读器能够适用于所有 Windows 团队博客,我们必须向应用添加更多的页面并处理如何在这些页面之间进行导航。首先,我们需要一个能够列出 Windows 团队所有博客的页面。当阅读器从该页面中选择某个博客时,我们将加载该博客的文章列表。我们已创建的分页阅读器也可以完成此功能,但我们希望对它做一点改进。最后,我们需要添加一个详细信息页面,以便阅读单个博客文章,而不至于让列表视图占用空间。

页面模板

幸好,我们不需要从空白模板开始创建每个页面。Visual Studio 11 Express Beta for Windows 8 附带了一个页面模板的集合,这些模板对于各种情形都很有用。以下是可用的页面模板。

页面类型描述
组详细信息页面显示单个组的详细信息以及组中每个项目的预览。
分组项目页面显示分组的集合。
项目详细信息页面详细显示一个项目,并允许导航到相邻的项目。
项目页面显示项目的集合。
拆分页面显示项目的列表以及所选项目的详细信息。
基本页面可以适应不同方向和视图的空白页面,并且包含一个标题和返回按钮。
空白页面用于 Metro 风格应用的空白页面。

BR211380.wedge(zh-cn,WIN.10).gif向应用添加页面。

  1. 选择“项目”>“添加新项”。“添加新项”对话框即会打开。
  2. “已安装”窗格中,展开“Visual C#”或“Visual Basic”。
  3. 选择“Windows Metro 风格”模板类型。
  4. 在中心窗格中,选择要添加到项目中的页面类型。
  5. 为该页面输入一个名称。
  6. 单击“添加”。XAML 和你的页面的代码隐藏文件即被添加到项目中。

以下是“添加新项”对话框。

Visual Studio“添加新项”对话框。

针对我们的博客阅读器,我们将添加一个项目页面来显示 Windows 团队博客的列表。我们将此页面命名为 ItemsPage。 我们添加一个拆分页面来显示每个博客的文章。我们将此页面命名为 SplitPage拆分页面模板与我们为简单博客阅读器应用创建的页面相似,但更加精炼。我们针对详细信息页面(将其命名为 DetailPage)使用基本页面模板。它只有返回按钮、页面标题和一个用于显示文章内容的 WebView 控件。但并不是像我们在拆分页面中那样将来自 HTML 字符串的文章内容加载到 WebView,我们导航到文章的 URL 并显示实际的 Web 页面。完成后的应用页面看起来如下所示:

三个页面的导航示例。

当我们将页面模板添加到项目并浏览 XAML 和代码隐藏后,看起来这些页面模板帮我们做了许多工作。但实际上很容易迷失,因此,让我们更进一步了解这些页面模板并看看它们都包含什么内容。Metro 风格应用的所有页面模板都具有相同的格式。

页面模板的 XAML 包含 4 个主要部分:

资源“资源”部分定义页面的样式和数据模板。我们将在使用样式创建一致性外观部分对此进行更详细的讨论。
应用栏可在应用栏定义应用命令。我们将在添加应用栏部分对此进行更详细的讨论。
应用内容在根布局面板中定义组成应用 UI 的控件和内容。
Visual State Manager在 Visual State Manager (VSM) 中定义使应用适用于不同布局和方向的动画和过渡。我们将在适应不同的布局部分对此进行更详细的讨论。

模板页面全部从 LayoutAwarePage 类派生而来,默认情况下,这些模板页面能够比我们使用的初始 BlankPage执行更多的功能。LayoutAwarePagePage 的一个实现,为 Metro 风格应用开发启用了重要的功能:

  • 应用程序视图状态到视觉状态的映射使页面能够适应不同的分辨率、方向和视图。
  • GoBackGoHome 事件处理程序支持基本的导航。
  • 默认的视图模型为你提供了一个简单的可绑定数据源。

页面模板还使用 StandardStyles.xaml 中的样式和模板,这些样式和模板应用 Metro 风格应用的设计指南。我们将使用其中一些样式作为开始,并修改它们的副本来自定义应用的外观。

在页面之间导航

XAMLUI 框架提供了使用 Frame Page 的内置导航模型,其工作方式与在 Web 浏览器中的导航方式非常相似。Frame控件可托管 Page,并且具有导航历史记录,你可以通过该历史记录在访问过的页面中前进和后退。在导航时,你可以在页面之间传递数据。

在 Visual Studio 项目模板中,名为 rootFrame 的 Frame 被设置为应用窗口的内容。我们来看看 App.xaml.cs/vb 中的代码。

C#
VB
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
    // Create a Frame to act as navigation context and navigate to the first page
    var rootFrame = new Frame();
    rootFrame.Navigate(typeof(BlankPage));

    // Place the frame in the current Window and ensure that it is active
    Window.Current.Content = rootFrame;
    Window.Current.Activate();
}

这些代码用于创建框架,将其设置为 Window 的内容,并导航到 BlankPage。由于我们的完整应用的第一页是 ItemsPage,因此我们将调用更改为导航方法并在此处所示的 ItemsPage 中进行传递。

C#
VB
rootFrame.Navigate(typeof(ItemsPage));


加载 ItemsPage 时,我们需要获得数据源的一个实例,并检索要显示的源数据,就像我们在使用应用中的数据部分中使用 BlankPage 一样。我们将代码置于页面模板中包括的 OnNavigatedTo 方法替代中,如果尚未检索源,我们将调用 FeedDataSource.GetFeedsAsync 方法。以下是 ItemsPage.xaml.cs/vb 的相关代码。

C#
VB
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    FeedDataSource _feedDataSource = App.DataSource;

    if (_feedDataSource != null)
    {
        if (_feedDataSource.Feeds.Count == 0)
        {
            await _feedDataSource.GetFeedsAsync();
        }

        this.DefaultViewModel["Items"] = _feedDataSource.Feeds;
    }
}

当用户从集合中选取博客时,我们从项目页导航到拆分页。为了执行此导航,我们希望 GridView 项目响应单击(如按钮)操作,而不是被选定。为了使 GridView 项目可单击,我们按如下所示设置 SelectionMode IsItemClickEnabled 属性。然后我们为 GridView ItemClick 事件添加一个事件处理程序。以下是 ItemsPage.xaml 中用于 GridView 的 XAML,其中已设置属性,并已添加 ItemClick 事件。

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" />

项目页面还包含一个名为 itemListView 的列表视图,如果“调整”了应用,则会显示该列表视图来代替网格。.我们将在适应不同的布局部分中对此进行更详细的讨论。目前,我们只需对 ListView 进行与对 GridView 所做更改相同的更改,以确保它们的行为相同。

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"/>

我们向 ItemsPage.xaml.cs/vb 代码隐藏页添加事件处理程序。 在处理程序中,我们将导航到拆分页并传递所选信息提要的数据。

C#
VB
        private void itemView_ItemClick(object sender, ItemClickEventArgs e)
        {
            // Navigate to the split page, configuring the new page
            // by passing the clicked item (FeedItem) as a navigation parameter
            this.Frame.Navigate(typeof(SplitPage), e.ClickedItem);
        }

若要在页面之间导航,你可以使用 Frame 控件的 NavigateGoForward GoBack 方法。通过 Navigate(TypeName, Object) 方法可以导航并将数据对象传递到新页面。我们将使用此方法在我们的页面之间传递数据。第一个参数 typeof(BlankPage) 是我们将要导航到的页面的 Type。第二个参数是我们传递给将要导航到的页面的数据对象。在本例中,我们传递 clicked 项。

在 SplitPage.xaml.cs/vb 代码隐藏页面中,我们需要使用刚刚从项目页面传递的 FeedData 对象执行某些操作。为此,我们将覆盖PageOnNavigatedTo方法。该方法已添加到页面模板代码中,因此我们只需要对其进行修改以便与我们的数据关联。模板页面包含一个名为 DefaultViewModel 的内置视图模型,我们可以将数据与之关联。NavigationEventArgs.Parameter 属性包含从项目页面传递的数据对象。 我们将其转换回 FeedData 对象,并将信息提要数据添加至具有关键字 FeedDefaultViewModel,将 FeedData.Items 属性添加至具有关键字 ItemsDefaultViewModel。以下是更新的 OnNavigatedTo 方法。

C#
VB
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // TODO: Assign a bindable group to this.DefaultViewModel["Group"]
    // TODO: Assign a collection of bindable items to this.DefaultViewModel["Items"]
 
    FeedData feedData = e.Parameter as FeedData;
    if (feedData != null)
    {
        this.DefaultViewModel["Feed"] = feedData;
        this.DefaultViewModel["Items"] = feedData.Items;

        // Select the first item automatically unless logical page navigation is
        // being used (see the logical page navigation #region below.)
        if (!this.UsingLogicalPageNavigation()) this.itemsViewSource.View.MoveCurrentToFirst();
    }
}

在 Visual Studio 页面模板中,TODO 注释表示我们从何处将我们的数据对象添加至具有关键字 Group DefaultViewModel。由于我们使用的是关键字 Feed,因此我们需要更改页面标题中的绑定,以绑定到 Feed 属性,而不是 Group。在 SplitPage.xaml 中,更改名为 pageTitle TextBlock Text 绑定以绑定到 Feed.Title,如下所示:Text="{Binding Feed.Title}"

XAML
<TextBlock x:Name="pageTitle" Grid.Column="1" Text="{Binding Feed.Title}" 
           Style="{StaticResource PageHeaderTextStyle}"/>

要导航回项目页面,Visual Studio 页面模板包含相应的代码来处理 BackButton Click 事件,并调用 Frame.GoBack 方法。

我们需要再做一些更改才能完成向我们添加到组中的新页面添加功能的操作。将这些代码添加到应用中后,我们便可以继续对我们的应用进行风格和动画设置。

在 ItemsPage.xaml 中,页面标题绑定到具有关键字 AppName 的静态资源。将此资源中的文本更新到 Windows 团队博客 中,如下所示。

XAML
<x:String x:Key="AppName">Windows Team Blogs</x:String>

在 SplitPage.xaml 中,将名为 titlePanel 的网格更改为跨 2 个列。

XAML
<!-- Back button and page title -->
<Grid x:Name="titlePanel" Grid.ColumnSpan="2">

同样在 SplitPage.xaml 中,我们需要更改用来显示所选博客文章的标题和内容的布局。要执行此操作,需要将名为 itemDetail ScrollViewer 替换为此 ScrollViewer 布局。

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.cs/vb 中,向 ItemListView_SelectionChanged 事件处理程序添加代码,使用所选博客文章的内容填充 WebView,如此处所示。此事件处理程序位于代码的逻辑页导航区域中。

C#
VB
        void ItemListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // Invalidate the view state when logical page navigation is in effect, as a change
            // in selection may cause a corresponding change in the current logical page.  When
            // an item is selected this has the effect of changing from displaying the item list
            // to showing the selected item's details.  When the selection is cleared this has the
            // opposite effect.
            if (this.UsingLogicalPageNavigation())
            {
                this.InvalidateVisualState();
            }
                
            // Add this code to populate the web view
            //  with the content of the selected blog post.
            Selector list = sender as Selector;
            FeedItem selectedItem = list.SelectedItem as FeedItem;
            if (selectedItem != null)
            {
                this.contentView.NavigateToString(selectedItem.Content);
            }        
        }

在 DetailPage.xaml 中,我们需要将标题文本绑定到博客文章标题,并添加一个 WebView 控件以显示博客页面。要执行此操作,需要将包含返回按钮和页面标题的 Grid 替换为此 GridWebView

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.cs/vb 中,将代码添加到 OnNavigatedTo 方法改写中,以导航到博客文章并设置页面的 DataContext。此处显示更新后的 OnNavigatedTo 方法。

C#
VB
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // Add this code to navigate the web view to the selected blog post.
    FeedItem feedItem = e.Parameter as FeedItem;
    if (feedItem != null)
    {
        this.contentView.Navigate(feedItem.Link);
        this.DataContext = feedItem;
    }
}

添加应用栏

博客阅读器应用中的但部分导航都发生在用户在 UI 中选取项目时。但在拆分页面上,我们必须提供一种方法,让用户转到博客文章的详细信息视图。我们可以在页面的某个位置放置一个按钮,但这会影响核心应用体验,即阅读。我们转而将按钮放在应用栏中,只有当用户需要时才会显示应用栏。

应用栏是 UI 的一部分,默认情况下处于隐藏状态,当用户从屏幕边缘轻扫或与应用交互时,可以显示或隐藏屏幕栏。它可以向用户显示导航、命令和工具。应用栏可以显示在页面顶部、页面底部,也可同时显示在页面顶部和底部。我们建议你将导航放在顶部应用栏中,将工具和命令放在底部应用栏中。

要在 XAML 中添加应用栏,我们需要将一个 AppBar 控件指定给 Page TopAppBar BottomAppBar 属性。 我们将添加一个顶部应用栏,其中包含一个按钮可以导航到详细信息页面。StandardStyles.xaml 文件包含适用于常见场景的各种应用栏按钮样式。我们将使用这些样式作为创建按钮样式的指南。我们将样式放置在 SplitPage.xaml 的 UserControl.Resources 部分,并按如下所示将 Page.TopAppBar xaml 添加到资源部分之后。

XAML
<UserControl.Resources>
...
    <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="&#xE12B;"/>
    </Style>
</UserControl.Resources>

<Page.TopAppBar>
   <AppBar Padding="10,0,10,0">
        <Grid>

            <Button Click="ViewDetail_Click" HorizontalAlignment="Right" 
                    Style="{StaticResource WebViewAppBarButtonStyle}"/>
        </Grid>
    </AppBar>
</Page.TopAppBar>

通过设置 IsSticky IsOpen 属性可以控制应用栏显示和消失的方式和时间。还可以通过处理 Opened Closed 事件响应打开或隐藏的应用栏。

为处理详细信息页面的导航,我们将此代码添加到 SplitPage.xaml.cs/vb 中。

C#
VB
private void ViewDetail_Click(object sender, RoutedEventArgs e)
{
    FeedItem selectedItem = this.itemListView.SelectedItem as FeedItem;
    if (selectedItem != null && this.Frame != null)
    {
        this.Frame.Navigate(typeof(DetailPage), selectedItem);
    }
}


添加动画和过渡

当我们谈论动画时,通常会联想到屏幕上蹦蹦跳跳的物体。但在 XAML 中,动画基本上只是一种更改对象的属性值的一种方法。这让动画具有多种用途,而不仅仅是一堆跳动的球。在我们的博客阅读器应用中,我们使用动画使 UI 适用于不同的布局和方向。我们将在下一部分对此进行深入探讨,但首先,我们需要了解动画的工作方式。

要使用动画,我们需要将它放在一个Storyboard中。当 Storyboard 运行时,属性会按照动画规定发生变化。Storyboard 中可以包含一个或多个动画。每个动画会指定一个目标对象、该对象上要更改的属性,以及该属性的新值。

在我们的博客阅读器应用中,我们有一个名为 itemListView ListView。以下是一个当 Storyboard 运行时将 itemListView Visibility 属性更改为 Visible 的动画。

XAML
<Storyboard>
    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" 
                                   Storyboard.TargetProperty="Visibility" >
        <DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
    </ObjectAnimationUsingKeyFrames>
</Storyboard>


添加主题动画

Windows 8 使用动画和过渡来改进用户的 UI 体验。我们想在应用中具有相同的体验,以符合 Windows 8 的风格。所幸的是,我们可以在应用中使用内置的主题动画主题过渡,以与 Windows 8 中的主题动画和主题过渡相匹配。我们可以在 Windows.UI.Xaml.Media.Animation 命名空间中找到它们。

主题动画是一个预定义的动画,我们可以将其放在一个 Storyboard 中。 PopInThemeAnimation 在页面加载时使 Web 视图从右到左滑入。增加 FromHorizontalOffset 属性的值会使效果更好。在此,我们将 PopInThemeAnimation 放入 Storyboard,并使其成为 DetailPage.xaml 中的资源。因为返回按钮和标题在各个页面中均位于相同的位置,我们并不需要将它们弹入,所以我们将动画的目标设置为围绕在我们的 Web 内容周围的 Border。这样便会使 Border 和其中的所有内容具有动画效果。

XAML
<UserControl.Resources>
    <Storyboard x:Name="PopInStoryboard">
        <PopInThemeAnimation  Storyboard.TargetName="contentViewBorder" 
                              FromHorizontalOffset="400"/>
    </Storyboard>
</UserControl.Resources>


在代码隐藏页面中,我们在OnNavigatedTo方法中启动 Storyboard,这样当用户导航到详细信息页面时便可将弹入动画应用于 Border。以下是更新后的 OnNavigatedTo 方法。

C#
VB
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // Run the PopInThemeAnimation 
    Windows.UI.Xaml.Media.Animation.Storyboard sb = 
        this.FindName("PopInStoryboard") as Windows.UI.Xaml.Media.Animation.Storyboard;
    if (sb != null) sb.Begin();

    // Add this code to navigate the web view to the selected blog post.
    FeedItem feedItem = e.Parameter as FeedItem;
    if (feedItem != null)
    {
        this.contentView.Navigate(feedItem.Link);
        this.DataContext = feedItem;
    }
}


要使用主题动画,我们仍然必须将其放入某个Storyboard中,然后在发生特定事件时对该Storyboard进行控制。有时,我们可以改用主题过渡来为 UI 元素设置动画。主题过渡是一个完整的动画集,还是一个可以附加到 UI 元素的预封装行为的 Storyboard。此处,我们使用一个 ContentThemeTransition 来为 SplitPage.xaml 中的博客文章标题文本设置动画。 ContentThemeTransitionContentControl一起使用,并且会在控件内容发生更改时自动触发。

XAML
        <TextBlock x:Name="itemTitle" Text="{Binding Title}" 
                   Style="{StaticResource SubheaderTextStyle}">
            <TextBlock.Transitions>
                <TransitionCollection>
                    <ContentThemeTransition />
                </TransitionCollection>
            </TextBlock.Transitions>
        </TextBlock>

我们将ContentThemeTransition添加到TransitionCollection中,然后将后者设置为TextBlockTransitions属性值。当 TextBlock 的内容发生更改时, ContentThemeTransition将自动触发并运行。动画是预定义的,无需我们执行任何操作即可运行。我们只需将其附加到TextBlock 中即可。

有关详细信息以及主题动画和过渡的完整列表,请参阅快速入门:动画

使用样式创建一致性外观

我们希望让博客阅读器应用的外观和感觉类似于 Windows 团队博客网站。我们希望用户在该网站和我们的应用之间切换时能够拥有无缝的使用体验。Windows Metro 风格 UI 的默认黑色主题与 Windows 团队博客网站不太匹配。这在详细信息页面上尤为明显,在该页面上我们会将实际的博客页面加载到一个WebView中,如下所示:

采用深色主题的详细信息页面。

为了给我们的应用提供一个一致的外观以便在需要时进行更新,我们使用画笔和样式。 Brush可以让我们在一个位置定义一种外观,然后将它用于任何需要的地方。 Style可以让我们设置控件的各属性值,然后在整个应用中重用这些设置。

在深入了解详细信息之前,我们先来看看如何使用画笔在我们的应用页面中设置背景色。应用的每个页面都有一个设置了 Background 属性的 Grid,以定义页面的背景色。我们可以单个设置每个页面的背景,如下所示。

XAML
<Grid Background="Blue">

但有一种更好的方法是将一个 Brush 定义为资源,并用它来定义我们所有页面的背景色。我们在 Visual Studio 模板中就是这么做的,如我们在此处看到的一样。

XAML
<Grid Background="{StaticResource ApplicationPageBackgroundBrush}">

我们将对象和值定义为资源,以使它们可重用。若要将对象或值用作资源,我们必须设置其 x:Key 属性。我们使用此关键字来指代来自 XAML 的资源。此处,背景被设置为具有键 ApplicationPageBackgroundBrush 的资源,该键是定义 SolidColorBrush 的系统。

若要更改页面的背景,我们需要使用 App.xaml 中的键 WindowsBlogBackgroundBrush 定义一个新的 SolidColorBrush。针对这一新画笔,我们设置了 Color#FF0A2562,这是一种与 http://windowsteamblog.com 站点上的颜色很搭配的好看的蓝色。

XAML
// Add this brush to the resource dictionary in App.xaml.
<SolidColorBrush x:Key="WindowsBlogBackgroundBrush" Color="#FF0A2562"/>


在应用的每个页面中,我们使用此新画笔更改背景。

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 定义。

XAML
...
<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 中,我们希望网格框中的文本具有默认的外观,但我们希望 FontSize 更大一些。我们可以通过在本地进行设置来改写 FontSize,如下所示:<TextBlock Text="{Binding Title}" Style="{StaticResource BasicTextStyle}" FontSize="26.667" />。但还有一种更好的方法:创建一个基于 BasicTextStyle 的新 Style,并更改其中的 FontSize。此处介绍了我们如何为网格标题文本定义新样式。

XAML
<!-- This XAML gets added to the app with the other grid styles later. -->
<Style x:Key="GridTitleTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BasicTextStyle}">
    <Setter Property="FontSize" Value="26.667"/>
    <Setter Property="Margin" Value="12,0,12,2"/>
</Style>

BasedOn="{StaticResource BasicTextStyle}" 行指示新的 StyleBasicTextStyle 继承我们没有明确设置的所有属性。我们的新 Style 与默认文本样式一样,只是大一些,我们可以将其用于所有网格标题,如下所示。

XAML
<!-- This XAML is an exerpt from the DefaultGridItemTemplate that we add later. -->
<TextBlock Text="{Binding Title}" Style="{StaticResource GridTitleTextStyle}" />

为了让我们的应用具有与 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>
            ...
    </Application.Resources>

在 ItemsPage.xaml 中,我们添加了以下资源以定义默认视图中的网格项的外观。

XAML
<UserControl.Resources>

...
    <!-- 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>

    <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 资源而不是 Standard250x250ItemTemplate,后者是 StandardStyles.xaml 中定义的默认模板。此处显示的是更新后的 itemGridView 的 XAML。

XAML
            <GridView
                x:Name="itemGridView"
                AutomationProperties.AutomationId="ItemsGridView"
                AutomationProperties.Name="Items"
                Margin="116,0,116,46"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"

                ItemTemplate="{StaticResource DefaultGridItemTemplate}"

                SelectionMode="None"
                IsItemClickEnabled="True"
                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 模板包括用于处理对视图状态所做的变更的代码。这些代码位于 LayoutAwarePage.cs/vb 文件中,将我们的应用状态与 XAML 中定义的视觉状态相对应。由于已经提供了页面布局逻辑,因此我们只需要提供要针对每个页面的视觉状态使用的视图。

要使用 XAML 在不同的视图之间进行转换,我们需要使用 VisualStateManger 来为应用定义不同的 VisualState。此处,我们在 ItemsPage.xaml 中定义了一个 VisualStateGroup。该组有 4 个 VisualState,分别名为:FullScreenLandscapeFilledFullScreenPortraitSnapped。不能同时使用来自同一个VisualStateGroup的不同VisualState 。 每个 VisualState 都使用动画来指示应用需要在为 UI 的 XAML 中指定的基准项中进行哪些更改。

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 的页边距,以便所有内容显示都更好地与屏幕相吻合。

XAML
<!-- 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="0" Value="{StaticResource PortraitBackButtonStyle}"/>
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridView" 
                                       Storyboard.TargetProperty="Margin">
            <DiscreteObjectKeyFrame KeyTime="0" Value="96,0,86,56"/>
        </ObjectAnimationUsingKeyFrames>
    </Storyboard>
</VisualState>


当用户显示两个应用,而我们的应用是其中较窄的一个时,使用 Snapped 状态。在这种状态下,我们的应用只有 320 DIP 的宽度,因而需要对布局进行更大的变更。在项页面 UI 的 XAML 中,定义了一个 GridView 和一个 ListView 并将其绑定到数据集合。默认情况下,会显示 itemGridViewScroller,而 itemListViewScroller 处于折叠状态。在 Snapped 状态中,包含 4 个动画,用于折叠 itemListViewScroller、显示 itemListViewScroller 和更改“后退”按钮和页面标题的 Style 以使其更小。

XAML
<!--
    The back button and title have different styles when snapped, and the list representation is substituted
    for the grid displayed in all other view states
-->
<VisualState x:Name="Snapped">
    <Storyboard>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="backButton" 
                                       Storyboard.TargetProperty="Style">
            <DiscreteObjectKeyFrame KeyTime="0" 
                                    Value="{StaticResource SnappedBackButtonStyle}"/>
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="pageTitle" 
                                       Storyboard.TargetProperty="Style">
            <DiscreteObjectKeyFrame KeyTime="0" 
                                    Value="{StaticResource SnappedPageHeaderTextStyle}"/>
        </ObjectAnimationUsingKeyFrames>

        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListScrollViewer" 
                                       Storyboard.TargetProperty="Visibility">
            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridScrollViewer"
                                       Storyboard.TargetProperty="Visibility">
            <DiscreteObjectKeyFrame KeyTime="0" 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 资源而不是 Standard80ItemTemplate,后者是 StandardStyles.xaml 中定义的默认视图。此处显示的是更新后的 itemListView 的 XAML。

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 模板:FilledSnapped 视图,以及当屏幕宽度小于 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>

要使用此数据模板,我们需要更新使用该模板的视觉状态。在 SnappedFilled 视觉状态的 XAML 中,我们发现了针对 itemListView ItemTemplate 属性的动画。接着,我们更改了该值,以使用 NarrowListItemTemplate 资源而不使用默认的 Standard80ItemTemplate 资源。此处显示的是更新后的动画 XAML。

XAML
<VisualState x:Name="Filled">
    <Storyboard>
    ....
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="ItemTemplate">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource NarrowListItemTemplate}"/>
        </ObjectAnimationUsingKeyFrames>
    ....
    </Storyboard>
</VisualState>
...
<VisualState x:Name="Snapped">
    <Storyboard>
    ....
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" Storyboard.TargetProperty="ItemTemplate">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource NarrowListItemTemplate}"/>
        </ObjectAnimationUsingKeyFrames>
    ....
    </Storyboard>
</VisualState>

我们还将拆分页面的项目详细信息部分替换为我们自己的、使用 WebView 的详细信息部分。由于我们进行了此更改,因此 Snapped_Detail 视觉状态目标元素中的某些动画将不再存在。这些动画在我们使用此视觉状态时会导致错误,因此我们需要将其删除。在 SplitPage.xaml 中,我们从 Snapped_Detail 视觉状态中删除了这些动画。

XAML
<VisualState x:Name="Snapped_Detail">
    <Storyboard>
    ...
        <!--<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>

在 DetailPage.xaml 中,我们只需要在 Snapped 视图中调整我们的 WebView 页边距,即可使用所有可用空间。在 Snapped 视觉状态的 XAML 中,我们添加了一个动画来更改 contentViewBorder 上的 Margin 属性,如下所示。

XAML
<VisualState x:Name="Snapped">
    <Storyboard>
    ...
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentViewBorder" Storyboard.TargetProperty="Margin">
            <DiscreteObjectKeyFrame KeyTime="0" Value="20,5,20,20"/>
        </ObjectAnimationUsingKeyFrames>

    </Storyboard>
</VisualState>

添加初始屏幕和徽标

我们的应用带给用户的第一印象来自于徽标和初始屏幕。徽标显示在 Windows 应用商店和“开始”屏幕上。初始屏幕在用户启动应用时立即显示,并在应用初始化其资源时为用户提供即时反馈。当应用的第一个页面准备就绪可以显示时,它就会关闭。

初始屏幕由一种背景色和一个 620 x 300 像素的图像组成。我们在 Package.appxmanifest 文件中设置这些值。双击此文件在清单编辑器中打开它。在清单编辑器的“应用程序 UI”选项卡中,我们设置了初始屏幕图像的路径和背景色。项目模板提供了一个名为 SplashScreen.png 的默认空白图像。我们将该图像替换为我们自己的初始屏幕图像,以清晰地标识我们的应用并立即将用户吸引到我们的应用中来。我们还可以用自己的徽标文件替换模板徽标文件来指定徽标。以下是我们的博客阅读器的初始屏幕。

初始屏幕图像。

基本初始屏幕可以适用于我们的博客阅读器,但你也可以使用 SplashScreen 类的属性和方法扩展该初始屏幕。你可以使用 SplashScreen 类获取初始屏幕的坐标,然后利用这些坐标定位该应用的第一个页面。还可以掌握初始屏幕消失的时间,以确定启动应用的任何内容进入动画的时机。

总结

我们学习了如何使用 Visual Studio 11 Express Beta for Windows 8 中的内置页面模板构建完整的多页面应用,还学习了如何在页面之间导航和传递数据。我们学习了如何使用样式和模板以使我们的应用符合 Windows 团队博客网站的风格。我们还学习了如何使用主题动画、应用栏和初始屏幕来使应用更适合 Windows 8 的个性化内容。 最后,我们学习了如何根据各种布局和方向来调整我们的应用,从而让它始终保持美观。

现在,我们基本上可以将我们的应用提交到 Windows 应用商店了。下面提供了一些链接,以便你了解将应用提交到 Windows 应用商店需要执行的后续步骤的详细信息:


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码介绍 MetroForWinForm(win8风格模版) using System; using System.Drawing; using System.Globalization; using System.Windows.Forms; using MetroFramework.Forms; namespace MetroFramework.Demo { public partial class MainForm : MetroForm { public MainForm() { InitializeComponent(); metroStyleManager.Theme = MetroThemeStyle.Default; metroStyleManager.Style = MetroColorStyle.Teal; } private void metroTileSwitch_Click(object sender, EventArgs e) { var m = new Random(); int next = m.Next(0, 13); metroStyleManager.Style = (MetroColorStyle)next; } private void metroTile1_Click(object sender, EventArgs e) { metroStyleManager.Theme = metroStyleManager.Theme == MetroThemeStyle.Light ? MetroThemeStyle.Dark : MetroThemeStyle.Light; } private void metroButton1_Click(object sender, EventArgs e) { MetroTaskWindow.ShowTaskWindow(this, "SubControl in TaskWindow", new TaskWindowControl(), 10); } private void metroButton2_Click(object sender, EventArgs e) { MetroMessageBox.Show(this, "Do you like this metro message box?", "Metro Title", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Asterisk); } private void metroButton5_Click(object sender, EventArgs e) { metroContextMenu1.Show(metroButton5, new Point(0, metroButton5.Height)); } private void metroButton6_Click(object sender, EventArgs e) { MetroMessageBox.Show(this, "This is a sample MetroMessagebox `OK` only button", "MetroMessagebox", MessageBoxButtons.OK, MessageBoxIcon.Information); } private void metroButton10_Click(object sender, EventArgs e) { MetroMessageBox.Show(this, "This is a sample MetroMessagebox `OK` and `Cancel` button", "MetroMessagebox", MessageBoxButtons.OKCancel, MessageBoxIcon.Information); } private void metroButton7_Click(object sender, EventArgs e) { MetroMessageBox.Show(this, "This is a sample MetroMessagebox `Yes` and `No` button", "MetroMessagebox", MessageBoxButtons.YesNo, MessageBoxIcon.Question); } private void metroButton8_Click(object sender, EventArgs e) { MetroMessageBox.Show(this, "This is a sample MetroMessagebox `Yes`, `No` and `Cancel` button", "MetroMessagebox", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); } private void metroButton11_Click(object sender, EventArgs e) { MetroMessageBox.Show(this, "This is a sample MetroMessagebox `Retry` and `Cancel` button. With warning style.", "MetroMessagebox", MessageBoxButtons.RetryCancel, MessageBoxIcon.Warning); } private void metroButton9_Click(object sender, EventArgs e) { MetroMessageBox.Show(this, "This is a sample MetroMessagebox `Abort`, `Retry` and `Ignore` button. With Error style.", "MetroMessagebox", MessageBoxButtons.AbortRetryIgnore, MessageBoxIcon.Error); } private void metroButton12_Click(object sender, EventArgs e) { MetroMessageBox.Show(this, "This is a sample `default` MetroMessagebox ", "MetroMessagebox"); } private void metroButton4_Click(object sender, EventArgs e) { var testform = new TestForm1(); testform.ShowDialog(); } private void metroButton4_Click_1(object sender, EventArgs e) { metroTextBox2.Focus(); } } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值