WPF的布局系統
了解元素的測量和排列方式是理解布局的第一步。在測量(measure)階段容器遍歷所有子元素,並詢問子元素它們所期望的尺寸。在排列(arrange)階段,容器在合適的位置放置子元素。理論上布局就結束了。
所有的WPF布局容器都派生自System.Windows.Controls.Panel。Panel繼承自FrameworkElement。 在Panel中有一個比較重要的屬性是UIElementCollection 類型的Children屬性,
UIElementCollection是一個有序的集合。我們可以使用繼承自Panel的類來重寫MeasureOverride(),ArrangeOverride()實現自定義面板。
常用的布局容器:
Border不是布局面板,但是經過幾個大佬的提醒,用好他真的很重要,所以我就放在第一個了。
StackPanel: 堆棧面板,水平或垂直放置元素。
WrapPanel:可換行的行中放置元素,在水平方向上從左向右放置元素,換行后也是從左向右。在垂直方向上,從上到下放置元素,在切換列后也是從上到下。
DockPanel: 根據容器的整個邊界調整元素。
Grid:在行列表格中排列元素。
UniformGrid:強制所有單元格具有相同尺寸。
Canvas:使用固定坐標絕對定位元素。
ScrollViewer:通過添加滾動條可以使當前過長布局內的內容縱向或者橫向滾動。再有限的區域內可以通過滾動呈現更多的內容。
Border不是布局面板,但是經常與布局類的面板一起配合使用,所以先介紹Border。
border的主要作用是給元素添加邊框,這個元素可以理解為一個布局面板,一個控件等等。他包含設置邊框的2個屬性,BorderBrush用來設置顏色,BorderThickness用來設置寬度。CornerRadius設置圓角。而Padding和Margin一個設置邊框和里面元素的間距,一個設置邊框和其他臨近元素的間距。而Background則是設置border所有內容的顏色。
<Window x:Class="Layout.Views.BorderExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Layout.Views" mc:Ignorable="d" Title="BorderExample" Height="450" Width="300"> <StackPanel> <Border Background="Bisque" BorderBrush="Coral" BorderThickness="3"> <Button Content="Button A" Width="120"/> </Border> <Border BorderBrush="Red" BorderThickness="3" Margin="5"> <Button Content="Button B"/> </Border> <Border BorderBrush="DarkRed" BorderThickness="3" Background="Red" Padding="5"> <Button Content="Button C"/> </Border> </StackPanel> </Window>
以下是運行效果。嘗試修改以下顏色或大小看一下邊框效果、背景色效果、分析一下哪部分是按鈕、哪部分背景色、哪部分是邊框、是按照自己的思路看一下修改效果。學習編寫代碼千萬不要着急,建議再基礎部分盡量多的投入精力。讓自己靜下來。

1、StackPanel布局
StackPanel面板可以在單行或者單列以堆棧的形式排列子元素。默認情況下StackPanel面板按從上到下的順序排列元素。如果不指定寬度、則默認寬度和StackPanel面板寬度一致,如果StackPanel寬度發生變化,則按鈕也會拉伸以適應變化。而如果設置了寬度、就不會跟面板寬度變更發生變化。但是想要設計出自己想要的好看布局,還需要更多的元素,先看一個基本的例子。
<Window x:Class="Layout.Views.StackPanelExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Layout.Views" mc:Ignorable="d" Title="StackPanelExample" Height="350" Width="500"> <StackPanel x:Name="root_spanel" > <Button Content="點我切換方向" Click="OrientationTranslator_Click"/> <Button Content="點我添加元素到面板中" Click="AddElementToPanel_Click"/> <Button x:Name="btn_FixedWidth" Content="點我手動設置寬度為120" Click="SetCurrentWidth_Click"/> <Button Content="杜文龍加油一定要深入C#啊!!!"/> </StackPanel> </Window>
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; namespace Layout.Views { /// <summary> /// StackPanelExample.xaml 的交互邏輯 /// </summary> public partial class StackPanelExample : Window { public StackPanelExample() { InitializeComponent(); } private void OrientationTranslator_Click(object sender, RoutedEventArgs e) { root_spanel.Orientation = root_spanel.Orientation == Orientation.Horizontal ? Orientation.Vertical : Orientation.Horizontal; } private void AddElementToPanel_Click(object sender, RoutedEventArgs e) { Button btn = new Button(); btn.Content = "我是新添加的Button"; root_spanel.Children.Add(btn); } private void SetCurrentWidth_Click(object sender, RoutedEventArgs e) { btn_FixedWidth.Width = 120; } } }
StackPanel面板效果。

而在熟練以后往往需要更多的參數配合使用來設計更好的UI布局。后面會出模仿現在主流的網絡客戶端比如QQ、網易音樂、電腦管家等等的例子用於實踐面板布局的設計。
以下是布局過程中常用的設置:
| 布局屬性名稱 | 注釋 |
| HorizontalAlignment | 子元素在水平方向的對其方式:有Center、Left、Right、Stretch |
| VerticalAlignment | 子元素在垂直方向的對齊方式:有Center、Top、Bottom、Stretch |
| Margin | 表示在元素的周圍添加一定的空間。值類型是Thickness格式、支持頂部、底部、左邊、右邊設置間距。 |
| MinWidth和MinHeight | 這2個屬性是設置元素的最小寬高、如果一個元素超出了其他布局容器則元素被裁剪。 |
| MaxWidth和MaxHeight | 設置元素的最大寬高、如果面板中即使有更多的區域可用,但是如果超出了Max就會等於Max |
| Width和Height | 用於顯示的設置元素的寬高、會重寫HorizontalAlignment、VerticalAlignment設置的Stretch值、但不能超出MinWidth、MinHeight和MaxWidth、MaxHeight的設置范圍。 |
你可以在剛才的StackPanel面板的代碼中使用以上表格內的屬性來測試以下會產生什么樣的變化。這里只做介紹。練習一下。后續會有模仿的布局設計。
PS:注意觀察控件被裁剪。在設計多語言的控件時、控件顯示區域被裁剪或者超出是一個非常麻煩、但是沒有技術含量的事情。因為可能有30多個國家需要適配。所以布局這章學好了,對於節省工作量很重要。
2、WrapPanel布局
WrapPanel面板可以一次排列一行或一列然后再換行繼續排列,和StackPanel一樣,可以通過設置Orientation屬性來設置當前是以水平還是垂直來排列子元素。因為是根據窗體變化演示布局排列,這個只有XAML。
<Window x:Class="Layout.Views.WrapPanelExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Layout.Views" mc:Ignorable="d" Title="WrapPanelExample" Height="150" Width="400"> <WrapPanel> <Button Content="代碼搬磚了6年。"/> <Button Content="做事情浮躁不得。"/> <Button Content="寫代碼有不明白的地方就要搞明白他。"/> <Button Content="可以多買點書,也可以多找點教程。"/> <Button Content="但是一定要靜下心。慢慢有一套自己的學習方法。"/> </WrapPanel> </Window>

3、DockPanel布局
DockPanel面板與StackPanel面板類似,但是DockPanel可以通過設置Dock附加屬性設置子元素停靠的邊。
Dock的值為Left、Right、Top、Bottom。通過設置子元素再DockPanel面板中的Dock屬性。可以修改子元素再DockPanel面板內的位置。可以通過LastChildFill設置為true來告訴DockPanel面板使最后一個元素占滿剩余控件。而設置的停靠順序會影響布局結果。我們實際看一下。
<Window x:Class="Layout.Views.DockPanelExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Layout.Views" mc:Ignorable="d" Title="DockPanelExample" Height="450" Width="800"> <DockPanel LastChildFill="True"> <Button DockPanel.Dock="Top" Content="今天學習了嗎?"/> <Button DockPanel.Dock="Left" Content="今天寫代碼了?"/> <Button DockPanel.Dock="Right" Content="隨便放點東西"/> <Button DockPanel.Dock="Right" VerticalAlignment="Center" Content="真的理解了嗎?要不要再多敲幾遍。"/> <Button DockPanel.Dock="Bottom" Content="程序員不學習寫代碼,還能干什么呢?"/> <Button DockPanel.Dock="Bottom" Content="程序員不學習寫代碼,還能干什么呢?"/> </DockPanel> </Window>
這個DockPanel根據插入元素順序的不一致。面板布局也會不一樣。自己可以嘗試玩一下。設計一些自己喜歡的。

4、Grid布局
Grid面板是把顯示內容分割到不可見的行列網格中。通過設置行列和對應的寬高占比。來進行網格布局。 Grid布局再平時使用的比較多。大部分都是用來做布局的嵌套,設計外框各個部分的比例,然后再子元素中嵌套其他布局控件。來實現區域的划分。
在使用Grid面板時,需要用到一個叫做附加依賴項屬性的參數。在布局相關的內容里不會去講什么是附加依賴項屬性,這個會在依賴項屬性中去講解,這里只有了解就行。因為這個針對於Grid布局來說是固定寫法。
我們添加一個三行三列的Grid面板。Grid.RowDefinitions和Grid.ColumnDefinitions里面的內容是我們設置的Gird的行列數。各有3個,代表我們是一個三行三列的網格。我們沒有設置寬高。就會默認為是等分的。
<Window x:Class="Layout.Views.GridPanelExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Layout.Views" mc:Ignorable="d" Title="GridPanelExample" Height="450" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> </Grid> </Window>
網格布局設計好之后。我們需要往里面放置內容。我們要使用Grid.Column、Grid.Row這2個附加依賴項屬性來實現把Button放置到不同的網格中(還記得如果遇到不熟悉的知識鼠標點上他然后F12看詳情嗎。可以先嘗試了解一下。但是要記得這章的主題是布局)。我們在每個網格中放置一個Button我們不設置Button的寬高,讓他來填充滿當前的網格容器,用於分析我們的網格布局。修改代碼如下:
<Window x:Class="Layout.Views.GridPanelExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Layout.Views" mc:Ignorable="d" Title="GridPanelExample" Height="450" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Button Grid.Column="0" Grid.Row="0" Content="Button Row 1 Column 1"/> <Button Grid.Column="1" Grid.Row="0" Content="Button Row 1 Column 2"/> <Button Grid.Column="2" Grid.Row="0" Content="Button Row 1 Column 3"/> <Button Grid.Column="0" Grid.Row="1" Content="Button Row 2 Column 1"/> <Button Grid.Column="1" Grid.Row="1" Content="Button Row 2 Column 2"/> <Button Grid.Column="2" Grid.Row="1" Content="Button Row 2 Column 3"/> <Button Grid.Column="0" Grid.Row="2" Content="Button Row 3 Column 1"/> <Button Grid.Column="1" Grid.Row="2" Content="Button Row 3 Column 2"/> <Button Grid.Column="2" Grid.Row="2" Content="Button Row 3 Column 3"/> </Grid> </Window>
我們就在XAML的一個Grid的根節點下創建了9個Button,Grid.column和Grid.Row是附加依賴項熟悉,只對當前對象的上一級Grid生效。如果不生效請檢查XAML的結構。
我們嘗試運行代碼。就會得到9個一樣大小的Button。我們嘗試拖動窗體大小。不管怎么拖動,應該都是等量變化的。
這次我們修改Grid的RowDefinition和ColumnDefinition的寬高。來使網格布局滿足一些常用的場景。比如我們要求左邊一列寬度固定。右邊一列以文本內容寬度適配,剩下的寬度區域都給中間的列。為了提高代碼可讀性,不建議省略Width="*"雖然都是一樣的。
修改如下:
<Window x:Class="Layout.Views.GridPanelExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Layout.Views" mc:Ignorable="d" Title="GridPanelExample" Height="450" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="140"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Button Grid.Column="0" Grid.Row="0" Content="Button Row 1 Column 1"/> <Button Grid.Column="1" Grid.Row="0" Content="Button Row 1 Column 2"/> <Button Grid.Column="2" Grid.Row="0" Content="Button Row 1 Column 3 ces"/> <Button Grid.Column="0" Grid.Row="1" Content="Button Row 2 Column 1"/> <Button Grid.Column="1" Grid.Row="1" Content="Button Row 2 Column 2"/> <Button Grid.Column="2" Grid.Row="1" Content="Button Row 2 Column 3 ces"/> <Button Grid.Column="0" Grid.Row="2" Content="Button Row 3 Column 1"/> <Button Grid.Column="1" Grid.Row="2" Content="Button Row 3 Column 2"/> <Button Grid.Column="2" Grid.Row="2" Content="Button Row 3 Column 3 ces"/> </Grid> </Window>
我們嘗試拖動整個窗體的大小,我們就會發現窗體按照我們預想的方式在改變每列Button布局的大小。可以按照自己的思路嘗試修改這些代碼然后跑起來看看會有什么樣的影響。主要是理解網格布局。效果如下:
如果控件超出了布局的寬度或高度那么對應的內容就會被裁剪。在這里可以嘗試多玩一下這個布局控件。看看他的變化,加深理解。嘗試在Grid的屬性上F12跳轉看看還有哪些其他屬性可以設置。
UseLayoutRounding屬性是起什么作用的?Grid.RowSpan. Grid.ColumnSpan都起什么作用。可以嘗試寫一下。調試一下。GridSplitter沒有講,項目中我好像沒要用到過。
PS: 等你實驗完畢之后思考一下,再選擇布局容器時,怎么能再得到正確布局的基礎上使用合適的布局容器。Grid和StackPanel我們都學過了。

5、UniformGrid:
UniformGrid面板的特點是每個單元格始終保持一致的大小。通過設置行列數量來分割布局。元素通過放入的前后順序被分配到不同的位置。這個再某些特定場景配合數據虛擬化和列表虛擬化使用的還是比較多的。效果圖就不貼了。可以自己跑起來試一下。
<Window x:Class="Layout.Views.UniformGridExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Layout.Views" mc:Ignorable="d" Title="UniformGridExample" Height="450" Width="800"> <UniformGrid Columns="2" Rows="2"> <Button Content="Button A"/> <Button Content="Button B"/> <Button Content="Button C"/> <Button Content="Button D"/> </UniformGrid> </Window>
6、Canvas:
Canvas是一個基於坐標的布局面板。他主要用於構建圖形工具的繪圖、Canvas知識再指定的位置放置子元素。並且子元素要提供精確的尺寸。再Canvas中需要設置Canvas.Left和Canvas.Top屬性。用來設置相對於原點的left和top。
也可以使用Canvas.Right和Canvas.Bottom。但是Canvas.left和Right不能同時使用,Canvas.Top和Canvas.Bottom也不能同時使用。使用Panel.ZIndex來設置子元素的層級關系。
<Window x:Class="Layout.Views.CanvasPanelExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Layout.Views" mc:Ignorable="d" Title="CanvasPanelExample" Height="450" Width="800"> <Canvas> <Button Content="Button A" Canvas.Left="10" Canvas.Top="10" Panel.ZIndex="2"/> <Button Content="Button B" Canvas.Left="110" Canvas.Top="100"/> <Button Content="Button C" Canvas.Left="20" Canvas.Top="3" Panel.ZIndex="1"/> </Canvas> </Window>
效果圖如下:Button A和Button C的重疊關系使用Panel.ZIndex來設置。

7、ScrollViewer
如果要再一個比較小的區域內顯示特別多的內容。就需要使用ScrollViewer來進行橫向或縱向滾動了。但是再實際使用過程中往往配合數據虛擬化和列表虛擬化來實現支持更多內容的滾動效果。不然如果內容一旦特別多。ScrollViewer下的內容又特別長。每次滾動都會觸發布局的重新測量和排列。如果不使用虛擬化,會全部重新計算所有的布局元素,會特別卡。導致使用困難。最后一個例子拉,這篇博客來來回回改了3天多。最后用一個ScroolViewer窗體結束這個DEMO拉。

