使用 XAML 的回應式版面配置

XAML 版面配置系統提供自動調整元素大小、版面配置面板、視覺狀態等功能,來協助您建立回應式 UI。 有了回應式版面配置,您就可以讓應用程式在不同應用程式視窗尺寸、解析度、像素密度與方向的螢幕上看起來更美觀。 您也可以使用 XAML 調整位置、調整大小、顯示/隱藏、取代,或重新設計您應用程式的 UI,如同回應式設計技術中所討論。 以下,我們討論如何搭配 XAML 實作回應式版面配置。

具有屬性與面板的流體版面配置

回應式版面配置的基礎在於適當地使用 XAML 版面配置屬性和面板,在流體方式中進行內容的重新置放、調整大小及自動重排。

XAML 版面配置系統同時支援靜態和流體版面配置。 在靜態版面配置中,您可以為控制項提供明確的像素大小和位置。 當使用者變更其裝置的解析度或方向時,UI 不會改變。 靜態版面配置可能會在不同的尺寸規格和顯示大小上遭到裁剪。 相反的,流體的版面配置可縮小、放大和自動重排,以回應裝置上的可用視覺空間。

在實務中,您可以使用靜態和流體元素的組合來建立 UI。 您仍然會在某些地方使用靜態元素與值,但請確定整體 UI 是否回應不同的解析度、螢幕大小及檢視。

我們將在此處討論如何使用 XAML 屬性和版面配置面板,建立流體版面配置。

配置屬性

版面配置屬性控制元素的大小與位置。 若要建立流體版面配置,請針對元素使用自動或等比例調整大小,並視需要允許版面配置面板放置其子系。

以下是一些常見的版面配置屬性,以及如何加以使用來建立流體版面配置。

Height 與 Width

Height \(英文\) 與 Width \(英文\) 屬性指定元素的大小。 您可以使用以有效像素衡量的固定值,也可以使用自動或等比例調整大小。

自動調整大小會調整 UI 元素大小,以符合其的內容或父容器。 您也可以使用格線的列和欄自動調整大小。 若要使用自動調整大小,請將 UI 元素的高度和/或寬度設為自動

注意

元素是否會調整大小以符合其內容或容器,取決於父容器如何處理調整其子項的大小。 如需詳細資訊,請參閱此文章後續內容中的版面配置面板

等比例調整大小 (亦稱為「星號調整」) 會按照權重比例,將可用的空間分配給格線的列和欄。 在 XAML 中,星號值的表示方法為 * (加權星號調整則為 n *)。 例如,若要在 2 欄的版面配置中,將某一欄的寬度設定為第二欄的 5 倍,請使用 "5*" 與 "*" 來表示 ColumnDefinition 元素中的 Width 屬性。

此範例會在 4 欄的格線中結合固定、自動和等比例調整大小。

調整大小 描述
欄 1 Auto 列會根據其內容調整大小。
欄 2 * 計算出自動欄之後,欄會取得剩餘寬度的一部分。 「欄 2」的寬度將是「欄 4」的二分之一。
欄 3 44 該欄的寬度為 44 像素。
欄 4 2* 計算出自動欄之後,欄會取得剩餘寬度的一部分。 「欄 4」的寬度是「欄 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 設計工具中,結果看起來會如下所示。

A 4 column grid in the Visual Studio designer

若要在執行階段取得元素的大小,請使用唯讀 ActualHeight \(英文\) 與 ActualWidth \(英文\) 屬性,而不是 Height 與 Width。

大小限制

當您在 UI 中使用自動調整大小時,可能仍需要對元素的大小施加限制。 您可以設定 MinWidth/MaxWidthMinHeight/MaxHeight 屬性來指定值,以在允許流體調整大小時限制元素大小。

在格線中,MinWidth/MaxWidth 也可以與欄定義搭配使用,而 MinHeight/MaxHeight 可以搭配列定義使用。

對齊

使用 HorizontalAlignmentVerticalAlignment 屬性指定元素應該如何放置於其父容器內。

  • HorizontalAlignment 的值包含 LeftCenterRightStretch
  • VerticalAlignment 的值包含 TopCenterBottomStretch

透過 Stretch 對齊方式,元素會填滿父容器中提供的所有空間。 Stretch 是兩個對齊屬性的預設值。 但某些控制項,例如 Button 會以預設樣式覆寫此值。 任何可以具有子元素的元素都可以唯一地處理 HorizontalAlignment 和 VerticalAlignment 屬性的 Stretch 值。 例如,使用放置在網格中的預設 Stretch 值的元素會延展以填滿包含它的單元格。 放置在 Canvas 中的相同元素的會根據其內容調整大小。 有關每個面板如何處理 Stretch 值的詳細資訊,請參閱版面配置面板一文。

有關詳細資訊,請參閱對齊方式、邊距和填滿一文,以及 HorizontalAlignmentVerticalAlignment 參考頁面。

可視性

您可以將元素的 Visibility 屬性設定為其中一個 Visibility 列舉值,以顯示或隱藏元素:VisibleCollapsed 當元素摺疊時,它不會佔用 UI 版面配置中的任何空間。

您可以在程式碼中或視覺狀態下變更元素的 Visibility 屬性。 當元素的 Visibility 變更時,其所有子元素也會變更。 您可以透過顯示一個面板並折疊另一個面板來替換 UI 的各個部分。

提示

當您在 UI 中具有預設是 Collapsed 的元素時,仍會在啟動期間建立物件,即使其不會顯示也一樣。 您可以使用 x:Load 屬性延遲建立物件,從而延遲載入這些元素,直到它們顯示為止。 這可以改善啟動效能。 有關詳細資訊,請參閱 x:Load 屬性

樣式資源

您不必在控制項上單獨設定每個屬性值。 將屬性值分組到 Style 資源,並將 Style 套用到控制項通常會更有效率。 當您需要將相同的屬性值套用到許多控制項時尤其如此。 如需使用樣式的詳細資訊,請參閱樣式控制項

版面配置面板

若要放置視覺物件,您必須將它們放在面板或其他容器物件中。 XAML 框架提供了各種面板類別,例如 CanvasGridRelativePanelStackPanel,其可作為容器,使您能夠在其中放置和排列 UI 元素。

選擇版面配置面板時要考慮的主要事項是,面板如何放置子元素及調整其大小。 您可能還需要考慮重疊的子元素彼此分層的方式。

以下對 XAML 框架所提供面板控制項的主要功能進行了比較。

Panel 控制項 描述
Canvas \(英文\) Canvas 不支持流體的 UI;您可以控制定位和重設大小子元素的所有層面。 您通常會將其用於特殊案例,例如建立圖形或定義較大調適型 UI 的小型靜態區域。 您可以使用程式碼或視覺狀態,在運行時間重新置放元素。
  • 元素會使用 Canvas.Top 和 Canvas.Left 附加屬性來絕對定位。
  • 您可以使用 Canvas.ZIndex 附加屬性明確指定分層。
  • 會忽略 HorizontalAlignment/VerticalAlignment 的延展值。 如果未明確設定元素的大小,它會調整其內容的大小。
  • 如果大於面板,則不會以可視化方式裁剪子內容。
  • 子內容不受面板界限限制。
  • Grid \(英文\) Grid 支援子元素的流體大小調整。 您可以使用程式碼或視覺狀態來重新置放和重新重排元素。
  • 元素會使用 Grid.Row 和 Grid.Column 附加屬性,在列和欄中排列。
  • 元素可以使用 Grid.RowSpan 和 Grid.ColumnSpan 附加屬性跨越多個列和列。
  • 會遵守 HorizontalAlignment/VerticalAlignment 的延展值。 如果未明確設定元素的大小,則會延展以填滿格線單元格中的可用空間。
  • 如果子內容大於面板,則視覺上會遭到剪裁。
  • 內容大小受到面板邊界的限制,因此可捲動內容會在需要時顯示捲軸。
  • RelativePanel \(英文\)
  • 元素會根據面板的邊緣或中心,以及彼此之間的關係進行排列。
  • 使用控制面板對齊、同層級對齊和同層級位置的各種附加屬性來放置元素。
  • HorizontalAlignment/VerticalAlignment 的 Stretch 值會被忽略,除非RelativePanel 附加的對齊屬性會導致延展 (例如,元素與面板的右邊緣和左邊緣對齊)。 如果元素的大小未明確設定且未延展,則會根據其內容調整大小。
  • 如果子內容大於面板,則視覺上會遭到剪裁。
  • 內容大小受到面板邊界的限制,因此可捲動內容會在需要時顯示捲軸。
  • StackPanel \(英文\)
  • 元素垂直或水平堆疊在單一行中。
  • HorizontalAlignment/VerticalAlignment 的 Stretch 值會遵循與 Orientation 屬性相反的方向。 如果未明確設定元素的大小,它會延展以填滿可用的寬度 (如果方向為水平,則填滿高度)。 元素會根據 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 \(英文\) 包含兩個 VisualState 定義。 第一個 DefaultState 是空的。 套用時,會套用 XAML 頁面中定義的值。 第二個是 WideState,它會將 SplitViewDisplayMode 屬性變更為 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 來定義屬性變更。 它不會呼叫 GoToState,而是使用內建的 AdaptiveTrigger 狀態觸發程序來套用狀態。 當您使用狀態觸發程序時,不需要定義空的 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 中,您通常會為控制項屬性或包含該控制項之面板的附加屬性之一設定值。 設定附加屬性時,請在附加屬性名稱兩邊使用括號。

    此範例示範如何在名為 myTextBox 的 TextBox 上設定 RelativePanel.AlignHorizontalCenterWithPanel 附加屬性。 第一個 XAML 使用 ObjectAnimationUsingKeyFrames 語法,第二個使用 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 體驗的範例,請參閱狀態觸發程序範例

    視覺狀態和樣式

    您可以在視覺狀態下使用 Style 資源,將一組屬性變更套用到多個控制項。 如需使用樣式的詳細資訊,請參閱樣式控制項

    在這個來自狀態觸發程序範例的簡化 XAML 中,Style 資源會套用至 Button 以調整滑鼠或觸控輸入的大小和邊距。 如需完整的程式碼和自訂狀態觸發程式的定義,請參閱狀態觸發程序範例

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