教程:使用 WinUI 3 创建简单的照片查看器
注意
有关 WinUI 3 的优势的信息以及其他应用类型选项,请参阅框架概述。
在本主题中,我们将演练在 Visual Studio 中创建新 WinUI 3 项目的过程,然后构建一个简单应用来显示照片。 我们将使用控件、布局面板和数据绑定。 我们将同时编写 XAML 标记(声明性)和你选择的 C# 或 C++ 代码(强制性或程序性)。 使用主题标题上方的语言选取器选择 C# 或 C++/WinRT。
提示
本主题中的源代码以 C# 和 C++/WinRT 提供。 如果你是 C++ 开发人员,有关我们在此处显示的代码的工作原理的更多详细信息和概念,请参阅 C++/WinRT 文档。 相关主题包括 XAML 控件;绑定到 C++/WinRT 属性、XAML 项控件;绑定到 C++/WinRT 集合以及照片编辑器 C++/WinRT 示例应用程序。
步骤 1:安装适用于 Windows 应用 SDK 的工具
若要设置开发计算机,请参阅 WinUI 入门。 本文还将介绍创建和启动 WinUI 3 项目的说明。
重要
你会发现发行说明主题以及 Windows App SDK 发行通道主题。 每个通道都有发行说明。 请务必查看发行说明中的任何限制和已知问题,因为这些可能会影响按照本教程进行操作和/或运行我们将构建的应用的结果。
步骤 2:创建新项目
在 Visual Studio 中,通过“打包的空白应用(桌面版 WinUI 3)”项目模板创建一个你选择的新 C# 或 C++ 项目。 将项目命名为 SimplePhotos,并(以便文件夹结构与本教程中所述结构相匹配)取消选中“将解决方案和项目放在同一目录中”。 你可以针对客户端操作系统的最新版本(非预览版)。
步骤 3:复制资产文件
我们将要构建的应用以资产文件的形式随身携带图像文件;这些就是它显示的照片。 在本节中,你会将这些资产添加到项目中。 但首先需要获取文件的副本。
因此,克隆(或作为
.zip
下载)Windows 应用 SDK 示例存储库(请参阅 WindowsAppSDK-Samples)。 完成此操作后,你将找到要在文件夹\WindowsAppSDK-Samples\Samples\PhotoEditor\cs-winui\Assets\Samples
(将此文件夹同时用于 C# 和 C++/WinRT 项目)中使用的资产文件。 如果要联机在存储库中查看这些文件,则可以访问 WindowsAppSDK-Samples/Samples/PhotoEditor/cs-winui/Assets/Samples/。在文件资源管理器中,选择该“示例”文件夹,并将其复制到剪贴板。
转到 Visual Studio 中的解决方案资源管理器。 右键单击“Assets”文件夹(这是项目节点的子节点),然后单击“在文件资源管理器中打开文件夹”。 这将在文件资源管理器中打开“资产”文件夹。
粘贴(到“资产”文件夹)你刚刚复制的“示例”文件夹。
步骤 4:添加 GridView 控件
我们的应用需要显示照片的行和列。 换言之,图像的网格。 对于如下所示的 UI,要使用的主要控件是列表视图和网格视图。
打开
MainWindow.xaml
。 目前,有一个 Window 元素,其中包含 StackPanel 布局面板。 StackPanel 内部是一个 Button 控件,它连接到事件处理程序方法。所有应用的主窗口表示在运行应用时首先看到的视图。 在我们将要构建的应用中,主窗口的工作是从“示例”文件夹加载照片,并显示这些图像的平铺视图以及有关它们的各种信息。
将 StackPanel 和 Button 标记替换为以下列表中显示的 Grid 布局面板和 GridView 控件。
<Window ...> <Grid> <GridView x:Name="ImageGridView"/> </Grid> </Window>
提示
x:Name
标识 XAML 元素,以便你可以在 XAML 中的其他位置和代码隐藏中引用它。C# 中的检测示例。 打开
MainWindow.xaml.cs
,删除 myButton_Click 方法。C++/WinRT。 打开
MainWindow.xaml.h
和MainWindow.xaml.cpp
,删除 myButton_Click 方法。
可以现在构建并运行,但在此阶段窗口将为空。 为了让 GridView 控件显示内容,需要为其提供要显示的对象集合。 接下来,我们将对此着手。
有关我们刚刚提到的某些类型的背景信息,请参阅布局面板和适用于 Windows 应用的控件。
步骤 5:ImageFileInfo 模型
模型(在模型、视图和视图模型的意义上)是在某种程度上代表现实世界对象或概念(如银行帐户)的类。 这是现实事物的抽象。 在本部分中,我们将向项目添加名为 ImageFileInfo 的新类。 ImageFileInfo 将是图像文件的模型,例如照片。 本部分将使我们更接近于能够在应用的用户界面 (UI) 中显示照片。
提示
在准备下面的代码示例时,让我们引入术语“可观察”。 可以动态绑定到 XAML 控件的属性(以便每次属性值更改时 UI 都会更新)称为可观察属性。 这一想法基于称为“观察者模式”的软件设计模式。 在本教程中生成的应用中,ImageFileInfo 模型的属性不会变化。 但即便如此,我们将展示如何通过实现 INotifyPropertyChanged 接口来使 ImageFileInfo 可观察。
右键单击项目节点 (SimplePhotos),然后单击“添加”>“新建项目...”。在“C# 项”>“代码”下,选择“类”。 将名称设置为 ImageFileInfo.cs,然后单击“添加”。
将
ImageFileInfo.cs
中的内容替换为下面列出的代码。using Microsoft.UI.Xaml.Media.Imaging; using System; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Windows.Storage; using Windows.Storage.FileProperties; using Windows.Storage.Streams; namespace SimplePhotos { public class ImageFileInfo : INotifyPropertyChanged { public ImageFileInfo(ImageProperties properties, StorageFile imageFile, string name, string type) { ImageProperties = properties; ImageName = name; ImageFileType = type; ImageFile = imageFile; var rating = (int)properties.Rating; var random = new Random(); ImageRating = rating == 0 ? random.Next(1, 5) : rating; } public StorageFile ImageFile { get; } public ImageProperties ImageProperties { get; } public async Task<BitmapImage> GetImageSourceAsync() { using IRandomAccessStream fileStream = await ImageFile.OpenReadAsync(); // Create a bitmap to be the image source. BitmapImage bitmapImage = new(); bitmapImage.SetSource(fileStream); return bitmapImage; } public async Task<BitmapImage> GetImageThumbnailAsync() { StorageItemThumbnail thumbnail = await ImageFile.GetThumbnailAsync(ThumbnailMode.PicturesView); // Create a bitmap to be the image source. var bitmapImage = new BitmapImage(); bitmapImage.SetSource(thumbnail); thumbnail.Dispose(); return bitmapImage; } public string ImageName { get; } public string ImageFileType { get; } public string ImageDimensions => $"{ImageProperties.Width} x {ImageProperties.Height}"; public string ImageTitle { get => string.IsNullOrEmpty(ImageProperties.Title) ? ImageName : ImageProperties.Title; set { if (ImageProperties.Title != value) { ImageProperties.Title = value; _ = ImageProperties.SavePropertiesAsync(); OnPropertyChanged(); } } } public int ImageRating { get => (int)ImageProperties.Rating; set { if (ImageProperties.Rating != value) { ImageProperties.Rating = (uint)value; _ = ImageProperties.SavePropertiesAsync(); OnPropertyChanged(); } } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
保存并关闭
ImageFileInfo.cs
文件。
步骤 6:定义和填充图像集合的属性
在本部分中,我们将向 MainWindow 类添加新属性。 该属性(名为 Images)将是一个集合类,其中包含我们要显示的图像。
在
MainWindow.xaml.cs
中定义属性,如下所示:... using System.Collections.ObjectModel; ... namespace SimplePhotos { public sealed partial class MainWindow : Window { public ObservableCollection<ImageFileInfo> Images { get; } = new ObservableCollection<ImageFileInfo>(); ... } }
用图像填充新集合属性的代码显示在下面的 GetItemsAsync 和 LoadImageInfoAsync 方法中。 将
using
指令和两个方法实现也粘贴到MainWindow.xaml.cs
中。 这些方法是 MainWindow 类的成员,因此请将它们粘贴到其中,就像粘贴上面的 Images 属性一样。... using System.Threading.Tasks; using Windows.ApplicationModel; using Windows.Storage; using Windows.Storage.Search; ... private async Task GetItemsAsync() { StorageFolder appInstalledFolder = Package.Current.InstalledLocation; StorageFolder picturesFolder = await appInstalledFolder.GetFolderAsync("Assets\\Samples"); var result = picturesFolder.CreateFileQueryWithOptions(new QueryOptions()); IReadOnlyList<StorageFile> imageFiles = await result.GetFilesAsync(); foreach (StorageFile file in imageFiles) { Images.Add(await LoadImageInfoAsync(file)); } ImageGridView.ItemsSource = Images; } public async static Task<ImageFileInfo> LoadImageInfoAsync(StorageFile file) { var properties = await file.Properties.GetImagePropertiesAsync(); ImageFileInfo info = new(properties, file, file.DisplayName, file.DisplayType); return info; }
在本部分中,我们需要做的最后一件事是更新 MainWindow 的构造函数以调用 GetItemsAsync。
public MainWindow() { ... GetItemsAsync(); }
如果愿意,现在可以构建并运行(以确认已正常按照步骤操作),但在此阶段窗口中没有太多可显示的内容。 这是因为到目前为止我们所做的是要求 GridView 呈现 ImageFileInfo 类型的对象集合;而 GridView 尚不知道如何做到这一点。
请记住,Images 属性是 ImageFileInfo 对象的可观察集合。 GetItemsAsync 的最后一行告诉 GridView(名为 ImageGridView)其项目的来源 (ItemsSource) 是 Images 属性。 然后,GridView 的工作就是显示这些项。
但是我们还没有告诉 GridView 有关 ImageFileInfo 类的任何信息。 因此,到目前为止,最好是显示集合中每个 ImageFileInfo 对象的 ToString 值。 默认情况下,这只是类型的名称。 在下一部分中,我们将创建数据模板来定义希望显示 ImageFileInfo 对象的方式。
提示
我在上面使用了可观察集合这个术语。 在我们在本教程中构建的应用中,图像的数量不会改变(正如前文所述,每个图像的属性值也不会改变)。 但是,使用数据绑定将 UI 初始连接到数据仍然很方便,也是一种很好的做法。 这就是我们将执行的操作。
步骤 7:添加数据模板
首先,让我们使用类似于草图的占位符数据模板。 在完成对一些布局选项的探索前,这将一直有效。 之后,可以更新数据模板来显示实际照片。
提示
这实际上是一件非常实用的事情。 已经发现,如果 UI 看起来像草图(也就是低保真度),那么人们更愿意用它提出建议和/或测试快速的想法,有时涉及相当大的变化。 那是因为我们(正确地)猜测这样的改变成本不高。
另一方面,UI 看起来越完整(保真度越高),我们(再次正确地)猜测大量工作已投入其当前的外观。 这使得我们不太倾向于提出建议或尝试新想法。
打开
MainWindow.xaml
,更改 Window 的内容,使其看起来像如下所示的标记:<Window ...> <Grid> <Grid.Resources> <DataTemplate x:Key="ImageGridView_ItemTemplate"> <Grid/> </DataTemplate> </Grid.Resources> <GridView x:Name="ImageGridView" ItemTemplate="{StaticResource ImageGridView_ItemTemplate}"> </GridView> </Grid> </Window>
在布局根目录中,我们添加了简单的 DataTemplate 资源,并为其指定了
ImageGridView_ItemTemplate
键。 我们使用同一个键来设置 GridView 的 ItemTemplate。 GridView 等项控件具有 ItemTemplate 属性(正如之前看到它们具有 ItemsSource 属性)。 项目模板是数据模板;它用于显示集合中的每个项目。有关详细信息,请参阅项容器和模板。
现在,我们可对数据模板进行几次编辑,即添加和编辑其中的元素,使其更有趣和有用。 我们将给根网格的高度和宽度设置为 300,边距为 8。 然后我们将添加两个行定义,并将第二个行定义的高度设置为“自动”。
<DataTemplate x:Key="ImageGridView_ItemTemplate"> <Grid Height="300" Width="300" Margin="8"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> </Grid> </DataTemplate>
有关详细信息,请参阅对齐、边距和填充。
我们希望数据模板显示每张照片的图像、名称、文件类型、尺寸和评分。 因此,我们将分别添加一个图像控件、一些 TextBlock 控件和一个 RatingControl 控件。 我们将在 StackPanel 布局面板中布局文本。 图像最初将显示项目的类似草图的 Microsoft Store 徽标作为占位符。
完成所有这些编辑后,数据模板的外观如下所示:
<DataTemplate x:Key="ImageGridView_ItemTemplate"> <Grid Height="300" Width="300" Margin="8"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Image x:Name="ItemImage" Source="Assets/StoreLogo.png" Stretch="Uniform" /> <StackPanel Orientation="Vertical" Grid.Row="1"> <TextBlock Text="ImageTitle" HorizontalAlignment="Center" Style="{StaticResource SubtitleTextBlockStyle}" /> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <TextBlock Text="ImageFileType" HorizontalAlignment="Center" Style="{StaticResource CaptionTextBlockStyle}" /> <TextBlock Text="ImageDimensions" HorizontalAlignment="Center" Style="{StaticResource CaptionTextBlockStyle}" Margin="8,0,0,0" /> </StackPanel> <RatingControl Value="3" IsReadOnly="True"/> </StackPanel> </Grid> </DataTemplate>
立即构建项目,然后运行应用以查看带有刚刚所创建项目模板的 GridView 控件。 接下来,我们将了解项目布局方式。我们将更改一些画笔,并在各项之间添加空间。
步骤 8:编辑项容器的样式
与项控件(如 GridView)相关的另一个概念是项容器。 项容器是一个内容控件,该控件将项显示为其 Content 属性的值。 项控件根据需要创建尽可能多的项容器,以便随时显示在屏幕上可见的项。
作为控件,项容器具有样式和控件模板。 其样式和控件模板决定了项容器在其各种状态(例如选择、指针悬停和焦点)下的外观。 而且,正如我们所见,项模板(数据模板)决定了项本身的外观。
对于 GridView,其项容器的类型为 GridViewItem。
因此,在本部分中,我们将重点设计项容器的样式。 为此,我们将为 GridViewItem 创建样式资源,然后将其设置为 *GridView 的 ItemContainerStyle。 在该样式中,我们将设置项目容器的 Background 和 Margin 属性,为其提供灰色背景并在其外部留出一点边距。
在
MainWindow.xaml
中,将新的 Style 资源添加到用于放置数据模板的同一 Grid.Resources XML 元素中。<Grid> <Grid.Resources> ... <Style x:Key="ImageGridView_ItemContainerStyle" TargetType="GridViewItem"> <Setter Property="Background" Value="Gray"/> <Setter Property="Margin" Value="8"/> </Style> </Grid.Resources>
接下来,我们使用
ImageGridView_ItemContainerStyle
键设置 GridView 的 ItemContainerStyle。<GridView x:Name="ImageGridView" ... ItemContainerStyle="{StaticResource ImageGridView_ItemContainerStyle}"> </GridView>
立即创建并运行应用,然后查看其外观。 调整窗口大小时,GridView 控件会负责重新排列项,以放置在空间中的最佳位置。 在某些宽度下,应用窗口的右侧有很多空间。 如果将 GridView 和/或其内容居中,则显示效果更好。 接下来,我们将执行这项操作。
提示
若要试验一下,请尝试将 Background 和 Margin 资源库设置为不同的值,并查看一下效果如何。
步骤 9:试验布局
你可能想知道最好是将 GridView 本身居中,还是将其内容居中。 我们先尝试将 GridView 居中。
为了便于查看 GridView 在窗口中的确切位置,以及在我们试验布局时会发生什么,我们将其 Background 属性设置为红色。
<GridView x:Name="ImageGridView" ... Background="Red"> </GridView>
现在,我们将将其 HorizontalAlignment 属性设置为“居中”。
<GridView x:Name="ImageGridView" ... HorizontalAlignment="Center"> </GridView>
另请参阅对齐、边距和填充。
立即构建并运行,并试验调整窗口宽度。 可以看到 GridView 的红色背景两侧都有等量的空白空间。 因此,我们已实现将图像居中的目标。 但现在比以前更清楚的是,滚动条属于 GridView,而不是窗口。 因此,我们需要将 GridView 更改回填充窗口。 我们已经证明,需要将 GridView 中的图像居中,而不是使 GridView 在窗口中居中。
- 因此,现在删除在上一步中添加的 HorizontalAlignment 属性。
步骤 10:编辑项面板模板
项控件在所谓的项目面板中布置其项容器。 我们可以通过编辑 GridView 的项面板模板来定义使用哪种类型的面板,并在该面板上设置属性。 这就是本部分中要执行的操作。
在
MainWindow.xaml
中,将 ItemsPanelTemplate 资源添加到我们的资源字典中。 项面板的类型为 ItemsWrapGrid,我们将其 HorizontalAlignment 属性设置为“居中”。<Grid> <Grid.Resources> ... <ItemsPanelTemplate x:Key="ImageGridView_ItemsPanelTemplate"> <ItemsWrapGrid Orientation="Horizontal" HorizontalAlignment="Center"/> </ItemsPanelTemplate> </Grid.Resources>
接下来,我们使用
ImageGridView_ItemsPanelTemplate
键设置 GridView 的 ItemsPanel。<GridView x:Name="ImageGridView" ... ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"> </GridView>
在此时构建和运行,并尝试调整窗口宽度,图像两侧的 GridView 的红色背景数量相同。 由于 GridView 会填充窗口,因此滚动条与窗口边缘可很好地对齐,用户可能期望这样。
- 现在,我们已完成布局实验,请从 GridView 中删除
Background="Red"
。
步骤 11:将占位符图像替换为照片
现在可将草图提升到更高的保真度;这意味着将占位符图像替换为真实图像,并将“lorem ipsum”样式的占位符文本替换为真实数据。 我们先来处理图像。
重要
我们将用于在 Assets\Samples
文件夹中显示照片的技术涉及逐步更新 GridView 的项目。 具体来说,就是下面代码示例中 ImageGridView_ContainerContentChanging 和 ShowImage 方法中的代码,包括对 ContainerContentChangingEventArgs.InRecycleQueue 和 ContainerContentChangingEventArgs.Phase 属性的使用。 有关详细信息,请参阅 ListView 和 GridView UI 优化。 但简而言之,GridView 会(通过事件)让我们知道其中一个项目容器何时准备好显示其项。 然后,我们将跟踪项目容器处于其更新生命周期的哪个阶段,以便确定它何时准备好显示照片数据。
在
MainWindow.xaml.cs
中,向 MainWindow 添加一个名为 ImageGridView_ContainerContentChanging 的新方法。 这是一种事件处理方法,它处理的事件是 ContainerContentChanging。 我们还需要提供 ImageGridView_ContainerContentChanging 所依赖的 ShowImage 方法的实现。 将using
指令和两个方法实现粘贴到MainWindow.xaml.cs
中:... using Microsoft.UI.Xaml.Controls; ... private void ImageGridView_ContainerContentChanging( ListViewBase sender, ContainerContentChangingEventArgs args) { if (args.InRecycleQueue) { var templateRoot = args.ItemContainer.ContentTemplateRoot as Grid; var image = templateRoot.FindName("ItemImage") as Image; image.Source = null; } if (args.Phase == 0) { args.RegisterUpdateCallback(ShowImage); args.Handled = true; } } private async void ShowImage(ListViewBase sender, ContainerContentChangingEventArgs args) { if (args.Phase == 1) { // It's phase 1, so show this item's image. var templateRoot = args.ItemContainer.ContentTemplateRoot as Grid; var image = templateRoot.FindName("ItemImage") as Image; var item = args.Item as ImageFileInfo; image.Source = await item.GetImageThumbnailAsync(); } }
然后,在
MainWindow.xaml
中,使用 GridView 的 ContainerContentChanging 事件注册 ImageGridView_ContainerContentChanging 事件处理程序。<GridView x:Name="ImageGridView" ... ContainerContentChanging="ImageGridView_ContainerContentChanging"> </GridView>
步骤 12:将占位符文本替换为实际数据
在本部分中,我们将使用一次性数据绑定。 一次性绑定非常适合在运行时不会更改的数据。 这意味着一次性绑定具有高性能且易于创建。
在
MainWindow.xaml
中,找到 ImageGridView_ItemTemplate 数据模板资源。 我们将告诉数据模板,其工作是成为 ImageFileInfo 类的模板,你会记得这是 GridView 要显示的项的类型。为此,请将
x:DataType
值添加到模板,如下所示:<DataTemplate x:Key="ImageGridView_ItemTemplate" x:DataType="local:ImageFileInfo"> ...
如果不熟悉上面显示的
local:
语法(或起始 Window 标记中已有的xmlns:local
语法),请参阅 XAML 命名空间和命名空间映射。现在,我们已设置
x:DataType
,可以在数据模板中使用x:Bind
数据绑定表达式来绑定到指定的数据类型(本例中为 ImageFileInfo)的属性。在数据模板中,找到第一个 TextBlock 元素(其文本当前设置为 ImageTitle 的元素)。 替换其文本值,如下所示。
提示
可以复制并粘贴以下标记,也可以在 Visual Studio 中使用 IntelliSense。 为此,请选择引号内的当前值,然后键入
{
。 IntelliSense 会自动添加右大括号,并显示代码完成列表。 可以向下滚动到x:Bind
,然后双击它。 但是键入x:
可能更高效(注意随后x:Bind
如何被筛选至完成列表顶部),然后按 TAB 键。 现在按 SPACE 键,然后键入ImageT
(尽可能多使用属性名称ImageTitle
,使其位于完成列表顶部),再按 Tab。<TextBlock Text="{x:Bind ImageTitle}" ... />
x:Bind
表达式将 UI 属性的值与 data-object 属性的值相关联。 当然,这取决于先将x:DataType
设置为该 data-object 的类型,以便工具和运行时知道可以绑定到哪些属性。有关详细信息,请参阅 {x:Bind} 标记扩展和数据绑定深入介绍。
同样,替换其他 TextBlock 和 RatingControl 的值。 结果如下:
<TextBlock Text="{x:Bind ImageTitle}" ... /> <StackPanel ... > <TextBlock Text="{x:Bind ImageFileType}" ... /> <TextBlock Text="{x:Bind ImageDimensions}" ... /> </StackPanel> <RatingControl Value="{x:Bind ImageRating}" ... />
如果你现在构建并运行应用,而不是占位符,你将看到真实照片和真实文本(和其他数据)。 在视觉上和功能上,这个简单的小应用现已完成。 但在结尾时,我们进行最后的一点数据绑定。
步骤 13:将 GridView 绑定到图像集合(仅限 C#)
重要
仅当创建了 C# 项目时,才执行最后一步。
提示
你会发现,有些操作(通常与动态生成的 UI 相关)在 XAML 标记中无法做到。 但一般而言,如果可以在标记中采取措施,则更可取。 这在 XAML 标记表示的视图和命令式代码表示的模型(或视图模型)之间提供了更清晰的分离。 这往往会改进工具和团队成员之间的工作流。
我们当前要使用命令性代码将 GridView 的 ItemsSource 属性与 MainWindow 的 Images 属性相关联。 但是我们可改为在标记中这样做。
在 MainWindow 类中,删除(或注释掉)GetItemsAsync 的最后一行,该行会将
ImageGridView
的 ItemsSource 设置为 Images 属性的值。然后在
MainWindow.xaml
中找到名为 ImageGridView 的 GridView,并像这样添加 ItemsSource 特性。 如果需要,可使用 IntelliSense 进行此更改。<GridView x:Name="ImageGridView" ... ItemsSource="{x:Bind Images}"
此特定应用的 Images 属性值在运行时不会更改。 但由于 Images 属于 ObservableCollection<T>
类型,因此集合的内容可能发生变化(即可以添加或删除元素),并且绑定会自动注意到这些变化并更新 UI。
结束语
在本教程中,我们演练了如何使用 Visual Studio 构建可显示照片的简单 WinUI 3 应用的过程。 希望本教程可为你提供在 WinUI 3 应用中使用控件、布局面板、数据绑定和 GridView UI 优化的经验。