前一节中讨论的NavigationView控件是组织用户界面布局的一个重要控件。在许多新的Windows 10应用程序中,可以看到这个控件用于主要布局。其他几个控件也定义布局。本节演示了VariableSizedWrapGrid在网格中安排自动包装的多个项,RelativePanel相对于彼此安排各项或相对于父项安排子项,自适应触发器根据窗口的大小重新排列布局。
1. StackPanel
作为其内容,如果要在只能包含一个元素的控件中包含多个元素,最简单的方式就是使用StackPanel。StackPanel是一个简单的面板,只能逐个地显示元素。StackPanel的方向可以是水平或垂直。
在下面的代码片段中,页面包含了一个StackPanel,其中包含了垂直放置的各个控件。在第一个ListBoxItem的列表框中,包含一个横向排列的StackPanel:
<Grid>
<StackPanel>
<TextBox Text="TextBox"/>
<CheckBox Content="Checkbox"/>
<CheckBox Content="Checkbox"/>
<ListBox>
<ListBoxItem>
<StackPanel Orientation="Horizontal">
<TextBlock Text="One A"/>
<TextBlock Text=" One B"/>
</StackPanel>
</ListBoxItem>
<ListBoxItem Content="Two"/>
</ListBox>
<Button Content="Button"/>
</StackPanel>
</Grid>
在下图中,可以看到StackPanel垂直显示的子控件。
2. Canvas
Canvas是一个允许显示指定控件位置的面板。它定义了相关的Left、Right、Top和Bottom属性,这些属性可以由子元素在面板中定位时使用。
<Canvas>
<TextBlock Canvas.Top="30" Canvas.Left="20" Text="Enter here"/>
<TextBox Canvas.Top="30" Canvas.Left="120" Width="100"/>
<Button Canvas.Top="70" Canvas.Left="120" Content="Click Me" Padding="4"/>
</Canvas>
下图显示了Canvas面板的结果,其中定位了子元素TextBlock、TextBox和Button。
注意:
Canvas控件最适合用于图形元素的布局,例如Shape控件。
3. Grid
Grid是一个重要的面板。使用Grid,可以在行和列中排列控件。对于每一列,可以指定一个ColumnDefinition;对于每一行,可以指定一个RowDefinition。下面的示例代码显示两列和三行。在每一列和每一行中,都可以指定宽度或高度。ColumnDefinition有一个Width依赖属性,RowDefinition有一个Height依赖属性。可以以设备独立的像素为单位定义高度和宽度,或者把它们设置为Auto,根据内容来确定其大小。Grid还允许使用 "星标大小" ,即根据具体情况指定大小,即根据可用的空间以及与其它行和列的相对位置计算行和列的空间。在为列提供可用的空间时,可以将Width属性设置为 "*"。要使某一列的空间是另一列的两倍,应指定 "2*"。下面的示例代码定义了两列和三行,列使用 "星标大小" ,第一行的大小固定,第二行和第三行使用 "星标大小" 。在计算高度时,可用空间需要减去第一行的200像素,剩余的区域在第二行和第三行中按比例1.5:1来分配。
这个Grid包含几个Rectangle控件,它们用不同的颜色使单元格的尺寸可见。因为这些控件的父控件是Grid,所以可以设置附加属性Column、ColumnSpan、Row和RowSpan。
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="200"/>
<RowDefinition Height="1.5*"/>
<RowDefinition Height=" *"/>
</Grid.RowDefinitions>
<Rectangle Fill="Blue"/>
<Rectangle Grid.Column="1" Fill="Red"/>
<Rectangle Grid.Row="1" Grid.ColumnSpan="2" Fill="Pink"/>
<Rectangle Grid.Row="2" Grid.ColumnSpan="2" Fill="Yellow"/>
</Grid>
在Grid中排列控件的结果如下图所示。
4. VariableSizedWrapGrid
VariableSizedWrapGrid是一个包装网格,如果网格可用的大小不够大,它会自动换到下一行或列。这个表格的第二个特征是允许项放在多行或多列中,这就是为什么它称为可变的原因。
下面的代码片段创建一个VariableSizedWrapGrid,其方向是Horizontal,行或列中最多有20项,行和列的大小是50:
<VariableSizedWrapGrid x:Name="grid1" MaximumRowsOrColumns="20" ItemHeight="50"
ItemWidth="50" Orientation="Horizontal"/>
VariableSizedWrapGrid填充了30个随机大小和颜色的Rectangle和TextBlock元素。根据大小,可以在网格内使用1到3行或列。项的大小使用附加属性VariableSizedWrapGrid.ColumnSpan和VariableSizedWrapGrid.RowSpan设置:
public sealed partial class VariableSizedWrapGridSample : Page
{
public VariableSizedWrapGridSample()
{
this.InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
Random r = new Random();
Grid[] items =
Enumerable.Range(0, 30).Select(i =>
{
byte[] colorBytes = new byte[3];
r.NextBytes(colorBytes);
Rectangle rect = new Rectangle
{
Height = r.Next(40, 150),
Width = r.Next(40, 150),
Fill = new SolidColorBrush(new Color
{
R = colorBytes[0],
G = colorBytes[1],
B = colorBytes[2],
A = 255
})
};
var textBlock = new TextBlock
{
Text = (i + 1).ToString(),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
var grid = new Grid();
grid.Children.Add(rect);
grid.Children.Add(textBlock);
return grid;
}).ToArray();
foreach (var item in items)
{
grid1.Children.Add(item);
Rectangle rect = item.Children.First() as Rectangle;
if (rect.Width > 50)
{
int columnSpan = ((int)rect.Width) / 50 + 1;
VariableSizedWrapGrid.SetColumnSpan(item, columnSpan);
int rowSpan = ((int)rect.Height) / 50 + 1;
VariableSizedWrapGrid.SetRowSpan(item, rowSpan);
}
}
}
运行应用程序时,可以看到矩形,它们占用了不同的窗口,如下图所示。
5. RelativePanel
RelativePanel是UWP的一个新面板,允许一个元素相对与另一个元素定位。如果使用的Grid控件定义了行和列,且需要插入一行或列,就必须修改插入行或列下面的所有行或列号。原因是所有行和列都按数字索引。使用RelativePanel就没有这个问题,它允许根据元素的相对关系放置它们。
注意:
与RelativePanel相比,Grid控件仍然有它的Auto、"*"和固定大小的优势。
下面的代码片段在RelativePanel内对齐数个TextBlock和TextBox控件、一个按钮和一个矩形。TextBox元素定位在相应TextBlock元素的右边;按钮相对于面板的底部定位,矩形与第一个TextBlock的顶部对齐,与第一个TextBox的右边对齐:
<RelativePanel>
<TextBlock x:Name="FirstNameLabel" Text="First Name" Margin="8"/>
<TextBox x:Name="FirstNameText" RelativePanel.RightOf="FirstNameLabel"
Margin="8" Width="150"/>
<TextBlock x:Name="LastNameLabel" Text="Last Name" RelativePanel.Below="FirstNameLabel"
Margin="8"/>
<TextBox x:Name="LastNameText" RelativePanel.RightOf="LastNameLabel" Margin="8"
RelativePanel.Below="FirstNameText" Width="150"/>
<Button Content="Save" RelativePanel.AlignHorizontalCenterWith="LastNameText"
RelativePanel.AlignBottomWithPanel="True" Margin="8"/>
<Rectangle x:Name="Image" Fill="Violet" Width="150" Height="250"
RelativePanel.AlignTopWith="FirstNameLabel"
RelativePanel.RightOf="FirstNameText" Margin="8"/>
</RelativePanel>
下图显示了运行应用程序时对齐控件。
6. 自适应触发器
RelativePanel是用于对齐的一个好控件。但是,为了支持多个屏幕大小,根据屏幕大小重新排列控件,可以使用自适应触发器与RelativePanel控件。例如,在小屏幕上,TextBox控件应该安排在TextBlock控件的下方,但在大的屏幕上,TextBox控件应该在TextBlock控件的右边。
在以下代码中,之前的RelativePanel改为删除RelativePanel中不应用于所有屏幕尺寸的所有附加属性,添加一个命名为OptionalImage的Rectangle控件:
<RelativePanel>
<TextBlock x:Name="FirstNameLabel" Text="First Name" Margin="8"/>
<TextBox x:Name="FirstNameText" Margin="8" Width="150"/>
<TextBlock x:Name="LastNameLabel" Text="Last Name" Margin="8"/>
<TextBox x:Name="LastNameText" Margin="8" Width="150"/>
<Button Content="Save" Margin="8"/>
<Rectangle x:Name="Image" Fill="Violet" Width="150" Height="250" Margin="8"/>
<Rectangle x:Name="OptionalImage" RelativePanel.AlignRightWithPanel="True"
Fill="Red" Width="350" Height="350" Margin="8"/>
</RelativePanel>
使用自适应触发器(当启动触发器时,可以使用自适应触发器设置MinWindowWidth),设置不同的属性值,根据应用程序可用的空间安排元素。随着屏幕尺寸越来越小,这个应用程序所需的宽度也会变小。向下移动元素,而不是向旁边移动,可以减少所需的宽度。另外,用户可以向下滚动(使用ScrollViewer控件),对于最小的窗口宽度,隐藏命名为OptionalImage的Rectangle控件:
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="WideState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="1024"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="FirstNameText.(RelativePanel.RightOf)" Value="FirstNameLabel"/>
<Setter Target="LastNameLabel.(RelativePanel.Below)" Value="FirstNameLabel"/>
<Setter Target="LastNameText.(RelativePanel.Below)" Value="FirstNameText"/>
<Setter Target="LastNameText.(RelativePanel.RightOf)" Value="LastNameLabel"/>
<Setter Target="Image.(RelativePanel.AlignTopWith)" Value="FirstNameLabel"/>
<Setter Target="Image.(RelativePanel.RightOf)" Value="FirstNameText"/>
</VisualState.Setters>
</VisualState>
<VisualState>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="720"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="FirstNameText.(RelativePanel.RightOf)" Value="FirstNameLabel"/>
<Setter Target="LastNameLabel.(RelativePanel.Below)" Value="FirstNameLabel"/>
<Setter Target="LastNameText.(RelativePanel.Below)" Value="FirstNameText"/>
<Setter Target="LastNameText.(RelativePanel.RightOf)" Value="LastNameLabel"/>
<Setter Target="Image.(RelativePanel.Below)" Value="LastNameText"/>
<Setter Target="Image.(RelativePanel.AlignHorizontalCenterWith)" Value="LastNameText"/>
</VisualState.Setters>
</VisualState>
<VisualState>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="320"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="FirstNameText.(RelativePanel.Below)" Value="FirstNameLabel"/>
<Setter Target="LastNameLabel.(RelativePanel.Below)" Value="FirstNameText"/>
<Setter Target="LastNameText.(RelativePanel.Below)" Value="LastNameLabel"/>
<Setter Target="Image.(RelativePanel.Below)" Value="LastNameText"/>
<Setter Target="OptionalImage.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<RelativePanel>
<TextBlock x:Name="FirstNameLabel" Text="First Name" Margin="8"/>
<TextBox x:Name="FirstNameText" Margin="8" Width="150"/>
<TextBlock x:Name="LastNameLabel" Text="Last Name" Margin="8"/>
<TextBox x:Name="LastNameText" Margin="8" Width="150"/>
<Button Content="Save" Margin="8" RelativePanel.AlignBottomWithPanel="True"/>
<Rectangle x:Name="Image" Fill="Violet" Width="150" Height="250" Margin="8"/>
<Rectangle x:Name="OptionalImage" RelativePanel.AlignRightWithPanel="True"
Fill="Red" Width="350" Height="350" Margin="8"/>
</RelativePanel>
</Grid>
通过ApplicationView类设置SetPreferredMinSize,可以建立应用程序所需的最小窗口宽度:
ApplicationView.GetForCurrentView().SetPreferredMinSize(
new Size { Width = 320, Height = 300 });
运行应用程序时,可以看到最小宽度的布局安排、中等宽度的布局安排和最大宽度的布局安排(见下图)。
7. XAML视图
自适应触发器可以帮助支持很多不同的窗口大小,支持应用程序的布局,以便在Xbox、HoloLens和不同分辨率的桌面上运行。如果应用程序的用户界面应该有比使用RelativePanel更多的差异,最好的选择是使用不同的XAML视图。XAML视图只包含XAML代码,并使用与相应页面相同的代码隐藏文件。可以为每个设备系列创建同一个页面的不同XAML视图。
通过创建一个文件夹DeviceFamily-Mobile,可以为移动设备定义XAML视图。设备专用的文件夹总是以DeviceFamily名称开头。支持的其他设备系列有Team、Desktop和IoT。可以使用这个设备系列的名字作为后缀,指定相应设备系列的XAML视图。使用XAML View Visual Studio项模版创建一个XAML视图。这个模板创建XAML代码,但没有代码隐藏文件。这个视图需要与应该更换视图的页面同名。
除了为Mobile XAML视图创建另一个文件夹之外,还可以在页面所在的文件夹中创建视图,但视图文件使用DeviceFamily-Mobile命名。
8. 延迟加载
为了使UI更快,可以把控件的创建延迟到需要它们时再创建。在小型设备上,可能根本不需要一些控件,但如果系统使用较大的屏幕,也比较快,就需要这些控件。在XAML应用程序的先前版本中,添加到XAML代码中的元素都被实例化。Windows 10不再是这种情况,而可以把控件的加载延迟到需要它们时加载。
可以使用延迟加载和自适应触发器,只在稍后的时间加载一些控件。一个样本场景是,用户可以把小窗口调整得更大。在小窗口中,有些控件不应该是可见的,但它们应该在更大的窗口中可见。延迟加载可能有用的另一个场景是,布局的某些部分可能需要更多时间来加载。不是让用户等待,直到显示出完整加载的布局,而可以使用延迟加载。
要使用延迟加载,需要给控件添加x:Load特性(其值为False),如下面带有Grid控件的代码片段所示。这个控件也需要分配一个名字:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid x:Name="deferGrid" x:Load="False">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Rectangle Fill="Red"/>
<Rectangle Fill="Pink" Grid.Column="1"/>
<Rectangle Fill="Blue" Grid.Row="1"/>
<Rectangle Fill="Yellow" Grid.Row="1" Grid.Column="1"/>
</Grid>
<Button Content="LoadGrid" Grid.Row="1" Tapped="{x:Bind LoadGrid}"/>
</Grid>
为了使这个延迟的控件可见,只需要调用FindName方法访问控件的标识符。这不仅使控件可见,而且会在控件可见前加载控件的XAML树:
private void LoadGrid()
{
FindName(nameof(deferGrid));
}
注意:
x:Load特性是构建版本15063中新增的。在构建版本15063之前,可以使用x:DeferLoadingStrategy特性,x:Load的优点是元素也可以在加载后卸载。
运行应用程序时,可以用Live Visual Tree窗口验证,包含deferGrid元素的树不可用,但在调用FindName方法找到deferGrid元素后,deferGrid中的子元素就添加到树中(参见下图)。
注意:
特性x:Load有大约600字节的开销,所以应该只在需要隐藏的元素上使用它。如果在容器元素上使用此特性,就只需要向应用该特性的元素支付一次开销。