采用 XAML 的响应式布局

XAML 布局系统提供自动调整元素、布局面板和视觉状态大小的功能,来帮助你创建响应式 UI。 利用响应式布局,你可以使应用在具有不同的应用窗口大小、分辨率、像素密度和方向的屏幕上都具有良好的外观。 你还可以使用 XAML 对应用的 UI 进行重新定位、大小调整、重新排列、显示/隐藏、替换或重新构建,如响应式设计技术中所述。 在这里,我们将讨论如何使用 XAML 实现响应式布局。

具有属性和面板的动态布局

响应式布局的基础是合理使用 XAML 布局属性和面板,以便以动态方式对内容进行重新定位、大小调整和重新排列。

XAML 布局系统支持静态布局和流畅布局。 在静态布局中,为控件提供显式像素大小和位置。 当用户更改其设备的分辨率或方向时,UI 不会更改。 静态布局可以跨不同的外形规格和显示大小进行剪裁。 另一方面,动态布局可缩小、放大和重新排列,从而响应设备上的可用视觉空间。

实际上,使用静态元素和流畅元素的组合来创建 UI。 你仍可以在某些位置使用静态元素和值,但应确保整体 UI 可对不同的分辨率、屏幕大小和视图做出响应。

在这里,我们将讨论如何使用 XAML 属性和布局面板创建动态布局。

布局属性

布局属性控制元素的大小和位置。 要创建动态布局,请对元素应用自动或成比例大小调整,并允许布局面板根据需要定位其子元素。

下面介绍了一些常见布局属性以及如何使用它们来创建动态布局。

Height 和 Width

HeightWidth 属性用于指定元素的大小。 可以使用以有效像素为单位的固定值,也可以使用自动或成比例大小调整。

自动大小调整用于调整 UI 元素的大小以适应其内容或父容器。 还可以对网格的行和列使用自动大小调整。 若要使用自动大小调整,请将 UI 元素的高度和/或宽度设置为 “自动”。

注意

元素是否根据其内容或容器调整大小取决于父容器如何处理其子级的大小调整。 有关详细信息,请参阅本文后面部分的布局面板

成比例大小调整(也称为比例缩放)按加权比例分配行和列的可用空间。 在 XAML 中,比例缩放值用 * 表示(或使用 n* 表示加权比例缩放)。 例如,若要在两列布局中指定一列比另一列宽五倍,则在 ColumnDefinition 元素中对 Width 属性分别使用“5*”和“*”

本示例将网格中的固定大小、自动大小和比例大小与 4 列组合在一起。

大小调整 说明
Column_1 Auto 该列的大小将适合其内容。
Column_2 * 计算自动列后,该列将获取剩余宽度的一部分。 Column_2宽度为Column_4的一半。
Column_3 44 列宽为 44 像素。
Column_4 2* 计算自动列后,该列将获取剩余宽度的一部分。 Column_4宽度为Column_2两倍。

默认列宽为“*”,因此无需为第二列显式设置此值。

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition/>
        <ColumnDefinition Width="44"/>
        <ColumnDefinition Width="2*"/>
    </Grid.ColumnDefinitions>
    <TextBlock Text="Column 1 sizes to its content." FontSize="24"/>
</Grid>

在 Visual Studio XAML 设计器中,结果如下所示。

Visual Studio 设计器中的 4 列网格

若要在运行时获取元素的大小,请使用只读 ActualHeightActualWidth 属性,而不是 Height 和 Width

大小约束

在 UI 中使用自动大小调整时,仍可能需要对元素的大小设置约束。 可以设置 MinWidth/MaxWidthMinHeight/MaxHeight 属性,以指定在允许动态调整大小的同时限制元素大小的值。

在网格中,MinWidth/MaxWidth 还可用于列定义,MinHeight/MaxHeight 可用于行定义。

对齐

使用 HorizontalAlignmentVerticalAlignment 属性指定元素应如何定位在其父容器中。

  • HorizontalAlignment 的值是 LeftCenterRightStretch
  • VerticalAlignment 的值是 TopCenterBottomStretch

使用 Stretch 对齐方式,元素填充父容器中提供的所有空间。 Stretch 是两个对齐属性的默认值。 但是,某些控件(如 Button)在其默认样式中重写此值。 任何可以具有子元素的元素都可以唯一地处理 HorizontalAlignment 和 VerticalAlignment 属性的 Stretch 值。 例如,使用网格拉伸中放置的默认 Stretch 值来填充包含它的单元格的元素。 放置在 Canvas 中的元素的大小与其内容相同。 有关每个面板如何处理 Stretch 值的详细信息,请参阅 “布局面板” 一文。

有关详细信息,请参阅对齐、边距和填充文章,以及 HorizontalAlignment 和 VerticalAlignment 参考页。

可见性

可以通过将元素的 Visibility 属性设置为可见性枚举值之一来显示或隐藏元素:可见折叠。 当元素处于折叠状态时,它不会占用 UI 布局中的任何空间。

可以在代码或视觉状态中更改元素的 Visibility 属性。 当元素的可见性发生更改时,其所有子元素也会更改。 可以通过在折叠另一个面板时显示一个面板来替换 UI 的各个部分。

提示

默认情况下,当 UI 中的元素是 Collapsed 时,启动时仍会创建这些对象,即使它们不可见。 可以使用 x:Load 属性延迟加载这些元素,直到显示这些元素,以延迟对象的创建。 这可以提高启动性能。 有关详细信息,请参阅 x:Load 属性

样式资源

你无需在控件上单独设置每个属性值。 通常更高效的做法是将属性值分组到 Style 资源,并将 Style 应用到控件。 当你需要将相同的属性值应用于许多控件时更是如此。 有关使用样式的详细信息,请参阅 “设置控件样式”。

布局面板

若要定位视觉对象,必须将它们放在面板或其他容器对象中。 XAML 框架提供各种面板类,例如 Canvas、GridRelativePanel 和 StackPanel,它们充当容器,使你可以定位和排列其中的 UI 元素。

选择布局面板时要考虑的主要事项是面板如何定位和调整其子元素的大小。 你可能还需要考虑重叠的子元素是如何相互分层的。

下面比较了 XAML 框架中提供的面板控件的主要功能。

Panel 控件 说明
画布 画布 不支持流畅的 UI;你可以控制定位和调整子元素大小的各个方面。 通常用于创建图形或定义较大自适应 UI 的小静态区域等特殊情况。 可以使用代码或视觉状态在运行时重新定位元素。
  • 元素是绝对使用 Canvas.Top 和 Canvas.Left 附加属性定位的。
  • 可以使用 Canvas.ZIndex 附加属性显式指定分层。
  • 将忽略 HorizontalAlignment/VerticalAlignment 的拉伸值。 如果未显式设置元素的大小,则会调整其内容的大小。
  • 如果大于面板,则不会直观地剪裁子内容。
  • 子内容不受面板边界的约束。
  • Grid Grid 支持子元素的流体调整大小。 可以使用代码或视觉状态重新定位和重排元素。
  • 元素使用 Grid.Row 和 Grid.Column 附加属性以行和列排列。
  • 元素可以使用 Grid.RowSpan 和 Grid.ColumnSpan 附加属性跨多个行和列。
  • 将尊重 HorizontalAlignment/VerticalAlignment 的拉伸值。 如果未显式设置元素的大小,则会拉伸以填充网格单元格中的可用空间。
  • 如果子内容大于面板,则会直观地剪裁。
  • 内容大小受面板边界的约束,因此可滚动内容根据需要显示滚动条。
  • RelativePanel
  • 元素相对于面板的边缘或中心排列,并彼此相关。
  • 元素使用控制面板对齐、同级对齐和同级位置的各种附加属性进行定位。
  • 除非 RelativePanel 附加对齐属性导致拉伸(例如,元素与面板的右边缘和左边缘对齐),否则将忽略 HorizontalAlignment/VerticalAlignment 的拉伸值。 如果未显式设置元素的大小且未拉伸,则它的大小会调整到其内容。
  • 如果子内容大于面板,则会直观地剪裁。
  • 内容大小受面板边界的约束,因此可滚动内容根据需要显示滚动条。
  • StackPanel
  • 元素以垂直或水平方式堆叠在单个行中。
  • HorizontalAlignment/VerticalAlignment 的拉伸值遵循方向与 Orientation 属性相反的方向。 如果未显式设置元素的大小,则它拉伸以填充可用宽度(如果方向为水平)。 在 Orientation 属性指定的方向中,元素大小为其内容。
  • 如果子内容大于面板,则会直观地剪裁。
  • 内容大小不受方向方向面板边界的约束,因此可滚动内容延伸到面板边界之外,并且不显示滚动条。 必须显式限制子内容的高度(或宽度),才能显示其滚动条。
  • VariableSizedWrapGrid
  • 元素排列在达到 MaximumRowsOrColumns 值时自动换行到新行或列的行或列中。
  • 元素是按行排列还是按列排列,均由 Orientation 属性指定。
  • 元素可以使用 VariableSizedWrapGrid.RowSpan 和 VariableSizedWrapGrid.ColumnSpan 附加属性跨多个行和列。
  • 将忽略 HorizontalAlignment 和 VerticalAlignment 的拉伸值。 元素的大小由 ItemHeight 和 ItemWidth 属性指定。 如果未设置这些属性,它们将从第一个单元格的大小获取其值。
  • 如果子内容大于面板,则会直观地剪裁。
  • 内容大小受面板边界的约束,因此可滚动内容根据需要显示滚动条。
  • 有关这些面板的详细信息和示例,请参阅 “布局”面板

    布局面板允许将 UI 组织成逻辑控件组。 将它们与适当的属性设置一起使用时,可以获得对 UI 元素自动调整大小、重新定位和重排的一些支持。 但是,当窗口大小发生重大更改时,大多数 UI 布局都需要进一步修改。 为此,可以使用视觉状态。

    采用视觉状态和状态触发器的自适应布局

    使用视觉状态可基于窗口大小或其他更改对 UI 进行重大更改。

    当应用窗口增长或收缩超过特定量时,你可能希望更改布局属性以重新定位、调整大小、重排、显示或替换 UI 的各个部分。 可以为 UI 定义不同的视觉状态,并在窗口宽度或窗口高度超过指定的阈值时应用它们。

    VisualState 定义在处于特定状态时应用于元素的属性值。 在 VisualStateManager对视觉状态进行分组,该状态在满足指定条件时应用相应的 VisualState。 AdaptiveTrigger 提供了一个简单方法来以 XAML 设置要应用状态的阈值(也称为“断点”)。 或者,可以在代码中调用 VisualStateManager.GoToState 方法以应用视觉状态。 下面的部分展示了这两种方法的示例。

    在代码中设置视觉状态

    若要从代码应用视觉状态,请调用 VisualStateManager.GoToState 方法。 例如,若要在应用窗口为特定大小时应用状态,请处理 SizeChanged 事件并调用 GoToState 以应用适当的状态。

    在这里,VisualStateGroup 包含 2 个 VisualState 定义。 第一个, DefaultState为空。 应用时,将应用 XAML 页中定义的值。 第二个 WideStateSplitViewDisplayMode 属性更改为 Inline 并打开该窗格。 如果窗口宽度大于 640 个有效像素,将在 SizeChanged 事件处理程序中应用此状态。

    注意

    Windows 不提供可使应用检测运行它的特定设备的方法。 它可以告诉你应用正在运行的设备系列(桌面等),有效分辨率以及应用可用的屏幕空间量(应用窗口的大小)。 我们建议为屏幕大小和断点定义视觉状态。

    <Page ...
        SizeChanged="CurrentWindow_SizeChanged">
        <Grid>
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup>
                    <VisualState x:Name="DefaultState">
                            <Storyboard>
                            </Storyboard>
                        </VisualState>
    
                    <VisualState x:Name="WideState">
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames
                                Storyboard.TargetProperty="SplitView.DisplayMode"
                                Storyboard.TargetName="mySplitView">
                                <DiscreteObjectKeyFrame KeyTime="0">
                                    <DiscreteObjectKeyFrame.Value>
                                        <SplitViewDisplayMode>Inline</SplitViewDisplayMode>
                                    </DiscreteObjectKeyFrame.Value>
                                </DiscreteObjectKeyFrame>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames
                                Storyboard.TargetProperty="SplitView.IsPaneOpen"
                                Storyboard.TargetName="mySplitView">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
    
            <SplitView x:Name="mySplitView" DisplayMode="CompactInline"
                       IsPaneOpen="False" CompactPaneLength="20">
                <!-- SplitView content -->
    
                <SplitView.Pane>
                    <!-- Pane content -->
                </SplitView.Pane>
            </SplitView>
        </Grid>
    </Page>
    
    private void CurrentWindow_SizeChanged(object sender, Windows.UI.Core.WindowSizeChangedEventArgs e)
    {
        if (e.Size.Width > 640)
            VisualStateManager.GoToState(this, "WideState", false);
        else
            VisualStateManager.GoToState(this, "DefaultState", false);
    }
    
    // YourPage.h
    void CurrentWindow_SizeChanged(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::SizeChangedEventArgs const& e);
    
    // YourPage.cpp
    void YourPage::CurrentWindow_SizeChanged(IInspectable const& sender, SizeChangedEventArgs const& e)
    {
        if (e.NewSize.Width > 640)
            VisualStateManager::GoToState(*this, "WideState", false);
        else
            VisualStateManager::GoToState(*this, "DefaultState", false);
    }
    
    

    在 XAML 标记中设置视觉状态

    在 Windows 10 之前,VisualState 定义需要 Storyboard 对象进行属性更改,并且必须在代码中调用 GoToState 才能应用状态。 如上例所示。 你仍然会看到许多使用此语法的示例,或者你可能具有使用该语法的现有代码。

    从 Windows 10 开始,可以使用此处所示的简化 Setter 语法,还可以在 XAML 标记中使用 StateTrigger 来应用状态。 使用状态触发器创建简单的规则,以自动触发视觉状态更改以响应应用事件。

    此示例执行与上一示例相同的操作,但使用简化 的 Setter 语法而不是 Storyboard 来定义属性更改。 它使用内置的 AdaptiveTrigger 状态触发器来应用状态,而不是调用 GoToState。 使用状态触发器时,无需定义空 DefaultState。 当不再满足状态触发器的条件时,会自动重新应用默认设置。

    <Page ...>
        <Grid>
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup>
                    <VisualState>
                        <VisualState.StateTriggers>
                            <!-- VisualState to be triggered when the
                                 window width is >=640 effective pixels. -->
                            <AdaptiveTrigger MinWindowWidth="640" />
                        </VisualState.StateTriggers>
    
                        <VisualState.Setters>
                            <Setter Target="mySplitView.DisplayMode" Value="Inline"/>
                            <Setter Target="mySplitView.IsPaneOpen" Value="True"/>
                        </VisualState.Setters>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
    
            <SplitView x:Name="mySplitView" DisplayMode="CompactInline"
                       IsPaneOpen="False" CompactPaneLength="20">
                <!-- SplitView content -->
    
                <SplitView.Pane>
                    <!-- Pane content -->
                </SplitView.Pane>
            </SplitView>
        </Grid>
    </Page>
    

    重要

    在上一个示例中,对 Grid 元素设置了 VisualStateManager.VisualStateGroups 附加属性。 使用 StateTriggers 时,始终确保 VisualStateGroups 附加到根的第一个子级,以便触发器自动生效。 (此处, Grid 是根 Page 元素的第一个子元素。

    附加属性语法

    在 VisualState 中,通常为控件属性或包含控件的面板的附加属性之一设置值。 设置附加属性时,请使用附加属性名称周围的括号。

    此示例演示如何在名为 myTextBoxTextBox 的 TextBox 上设置 RelativePanel.AlignHorizontalCenterWithPanel 附加属性。 第一个 XAML 使用 ObjectAnimationUsingKeyFrames 语法,第二个 XAML 使用 Setter 语法。

    <!-- Set an attached property using ObjectAnimationUsingKeyFrames. -->
    <ObjectAnimationUsingKeyFrames
        Storyboard.TargetProperty="(RelativePanel.AlignHorizontalCenterWithPanel)"
        Storyboard.TargetName="myTextBox">
        <DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
    </ObjectAnimationUsingKeyFrames>
    
    <!-- Set an attached property using Setter. -->
    <Setter Target="myTextBox.(RelativePanel.AlignHorizontalCenterWithPanel)" Value="True"/>
    

    自定义状态触发器

    可以扩展 StateTrigger 类,为各种方案创建自定义触发器。 例如,可以创建 StateTrigger 以基于输入类型触发不同的状态,然后在触摸输入类型时增加控件周围的边距。 或者创建一个 StateTrigger,以根据运行应用的设备系列应用不同的状态。 有关如何生成自定义触发器并使用它们从单个 XAML 视图中创建优化的 UI 体验的示例,请参阅 状态触发器示例

    视觉状态和样式

    可以使用视觉状态中的样式资源将一组属性更改应用于多个控件。 有关使用样式的详细信息,请参阅 “设置控件样式”。

    在此“状态”触发器示例中简化的 XAML 中,样式资源应用于按钮,以调整鼠标或触摸输入的大小和边距。 有关完整的代码和自定义状态触发器的定义,请参阅 State 触发器示例

    <Page ... >
        <Page.Resources>
            <!-- Styles to be used for mouse vs. touch/pen hit targets -->
            <Style x:Key="MouseStyle" TargetType="Rectangle">
                <Setter Property="Margin" Value="5" />
                <Setter Property="Height" Value="20" />
                <Setter Property="Width" Value="20" />
            </Style>
            <Style x:Key="TouchPenStyle" TargetType="Rectangle">
                <Setter Property="Margin" Value="15" />
                <Setter Property="Height" Value="40" />
                <Setter Property="Width" Value="40" />
            </Style>
        </Page.Resources>
    
        <RelativePanel>
            <!-- ... -->
            <Button Content="Color Palette Button" x:Name="MenuButton">
                <Button.Flyout>
                    <Flyout Placement="Bottom">
                        <RelativePanel>
                            <Rectangle Name="BlueRect" Fill="Blue"/>
                            <Rectangle Name="GreenRect" Fill="Green" RelativePanel.RightOf="BlueRect" />
                            <!-- ... -->
                        </RelativePanel>
                    </Flyout>
                </Button.Flyout>
            </Button>
            <!-- ... -->
        </RelativePanel>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="InputTypeStates">
                <!-- Second set of VisualStates for building responsive UI optimized for input type.
                     Take a look at InputTypeTrigger.cs class in CustomTriggers folder to see how this is implemented. -->
                <VisualState>
                    <VisualState.StateTriggers>
                        <!-- This trigger indicates that this VisualState is to be applied when MenuButton is invoked using a mouse. -->
                        <triggers:InputTypeTrigger TargetElement="{x:Bind MenuButton}" PointerType="Mouse" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="BlueRect.Style" Value="{StaticResource MouseStyle}" />
                        <Setter Target="GreenRect.Style" Value="{StaticResource MouseStyle}" />
                        <!-- ... -->
                    </VisualState.Setters>
                </VisualState>
                <VisualState>
                    <VisualState.StateTriggers>
                        <!-- Multiple trigger statements can be declared in the following way to imply OR usage.
                             For example, the following statements indicate that this VisualState is to be applied when MenuButton is invoked using Touch OR Pen.-->
                        <triggers:InputTypeTrigger TargetElement="{x:Bind MenuButton}" PointerType="Touch" />
                        <triggers:InputTypeTrigger TargetElement="{x:Bind MenuButton}" PointerType="Pen" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="BlueRect.Style" Value="{StaticResource TouchPenStyle}" />
                        <Setter Target="GreenRect.Style" Value="{StaticResource TouchPenStyle}" />
                        <!-- ... -->
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    </Page>