5. 布局

前一节中讨论的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字节的开销,所以应该只在需要隐藏的元素上使用它。如果在容器元素上使用此特性,就只需要向应用该特性的元素支付一次开销。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值