布局系统
布局系统决定控件在界面上的位置和大小如何确定。Winform中我们通常用设计器把控件拖到绝对坐标上,再靠Anchor、Dock应付窗口缩放;WPF则采用一套容器自适应的布局模型,控件被放进各种布局面板(Panel)中,由面板负责计算每个子元素的位置和尺寸,窗口大小变化时面板自动重新排布。理解这套模型是写好WPF界面的关键。
布局原理:测量与排列
WPF的布局过程大致分为两个阶段:
Measure(测量):面板自上而下询问每个子元素“你想要多大空间”,子元素根据自己的内容和对齐方式汇报一个期望尺寸。
Arrange(排列):面板根据测量结果,给每个子元素分配最终的矩形区域,子元素在这个区域内渲染自己。
这两个阶段在窗口大小变化、内容变化时会自动重新执行,所以WPF界面天生就是自适应的,这与Winform需要手动设置Anchor和Dock才能勉强应付缩放有本质区别。
尺寸单位与尺寸值
WPF的尺寸使用的不是物理像素,而是设备无关单位DIU。如果你拿一把尺子去测量屏幕,你会发现不同缩放比例的屏幕下,一个DIU永远等于1/96英寸(约0.264mm,当然,现实可能会有一点误差,但这里是为了表达DIU的设计理念)。在高DPI屏幕上WPF会自动放大渲染,界面不会模糊或错位。
布局中尺寸值有三种写法:
| 写法 | 含义 |
|---|---|
| 数字 | 固定尺寸,如Width="100" |
Auto |
根据内容自动计算尺寸 |
* |
按比例分配剩余空间,如*、2* |
其中Auto和*主要用在Grid的行列定义中,是WPF布局的灵魂。
Grid网格布局
Grid是WPF中最重要、最常用的布局面板,它把区域划分成若干行和列,子元素通过附加属性指定自己落在哪一行哪一列。
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="标题区" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="主内容区" />
</Grid>
上面的例子定义了3行2列,第一行高度随内容自适应,第二行吃掉所有剩余空间,第三行固定30,两列按1:2的比例分配宽度。

Grid.Row和Grid.Column这类写在子元素上却属于面板的属性叫附加属性(Attached Property),它是WPF依赖属性系统的扩展能力,含义是子元素告诉Grid自己应该被放在哪里,不指定时默认为0。
跨行跨列使用Grid.RowSpan和Grid.ColumnSpan,下面是一个例子。
<Button Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Content="横跨两列" />
此外,Grid还有一个实用特性,多个子元素放在同一行同一列时会叠在一起,后写的在上层,利用这一点可以实现层叠效果。
StackPanel堆叠面板
StackPanel把子元素沿一个方向依次排列,默认纵向排列(即Orientation="Vertical"),也可设为Horizontal横向排列。它不瓜分空间,而是每个子元素在排列方向上按内容大小排列。
<StackPanel Orientation="Vertical">
<TextBlock Text="用户名:" />
<TextBox />
<TextBlock Text="密码:" />
<PasswordBox />
</StackPanel>

StackPanel简单直接,适合表单字段、按钮组这类线性排列的场景,但它不支持按比例拉伸,复杂布局还得靠Grid。
DockPanel停靠面板
DockPanel让子元素依次贴靠到某个边缘,子元素用附加属性DockPanel.Dock指定停靠方向,可取值Left、Right、Top、Bottom,最后一个子元素默认填满剩余空间,这个行为可用LastChildFill="False"关闭。
<DockPanel>
<Menu DockPanel.Dock="Top" Height="28" />
<StatusBar DockPanel.Dock="Bottom" Height="24" />
<TreeView DockPanel.Dock="Left" Width="200" />
<TextBox AcceptsReturn="True" />
</DockPanel>

停靠的顺序就是元素书写的顺序,先写的先占边缘。DockPanel非常适合搭建经典的顶部菜单、底部状态栏、左侧导航、中间内容的主窗口骨架,相当于Winform中Dock属性的面板化实现。
WrapPanel流式面板
WrapPanel与StackPanel类似,也是沿一个方向排列子元素,但一行(或一列)排满后会自动换行(或换列),类似文字的折行效果。
<WrapPanel Orientation="Horizontal">
<Button Content="按钮1" Width="80" Margin="4" />
<Button Content="按钮2" Width="80" Margin="4" />
<Button Content="按钮3" Width="80" Margin="4" />
<Button Content="按钮4" Width="80" Margin="4" />
<Button Content="按钮5" Width="80" Margin="4" />
</WrapPanel>

我们可以将程序运行起来,当缩减窗口宽度时,放不下的按钮会自动折到下一行。WrapPanel适合工具栏按钮组、标签集合等数量不定、需要自动折行的场景。
Canvas绝对定位面板
Canvas是最接近Winform布局方式的面板,子元素用附加属性Canvas.Left、Canvas.Top(或Right、Bottom)指定绝对坐标,Canvas不做任何自适应计算。
<Canvas>
<Ellipse Canvas.Left="20" Canvas.Top="20" Width="50" Height="50" Fill="Red" />
<Rectangle Canvas.Left="80" Canvas.Top="60" Width="100" Height="60" Fill="Blue" />
</Canvas>

Canvas适合绘图、图表、游戏界面等需要精确控制坐标的场景,但它丧失了自适应能力,普通表单界面不推荐使用。
UniformGrid均分网格
UniformGrid是一个简化版的Grid,所有单元格大小完全相同,通过Rows和Columns指定行列数,子元素按书写顺序依次填入单元格。
<UniformGrid Rows="2" Columns="2">
<Button Content="A" Margin="4" />
<Button Content="B" Margin="4" />
<Button Content="C" Margin="4" />
<Button Content="D" Margin="4" />
</UniformGrid>

它适合九宫格、计算器键盘这类规整的均分布局,UniformGrid中我们不需要写麻烦的RowDefinitions和附加属性,比Grid写起来简单。
对齐与边距
除了面板,控件自身的对齐和边距属性也参与布局。
| 属性 | 说明 |
|---|---|
HorizontalAlignment |
水平对齐:Left/Center/Right/Stretch |
VerticalAlignment |
垂直对齐:Top/Center/Bottom/Stretch |
Margin |
外边距,可写一个值(四周相同)、两个值(左右、上下)或四个值(左、上、右、下) |
Padding |
内边距(内容控件才有),语法同Margin |
<Button Content="居中" Width="100" Height="30"
HorizontalAlignment="Center" VerticalAlignment="Center"
Margin="0,10,0,0" />
当HorizontalAlignment为Stretch(默认值)且没有显式设置Width时,控件会拉伸填满父容器分给它的空间;Margin="10"表示四周各留10个单位的间距,Margin="10,5"表示左右10、上下5。
综合实例
下面综合运用这些面板写一个登录窗口,整体用Grid分行,表单区用一个内嵌Grid实现标签与输入框对齐,底部按钮用StackPanel横向排列。
<Window x:Class="Gacfox.Demo.MainWindow"
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:Gacfox.Demo"
mc:Ignorable="d"
Title="MainWindow">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="用户登录" FontSize="20" FontWeight="Bold"
HorizontalAlignment="Center" />
<Grid Grid.Row="1" Margin="0,16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="用户名:" VerticalAlignment="Center" />
<TextBox x:Name="usernameTextBox" Grid.Column="1" Height="26" />
<TextBlock Grid.Row="1" Text="密码:" VerticalAlignment="Center" Margin="0,6,0,0" />
<PasswordBox x:Name="passwordBox" Grid.Row="1" Grid.Column="1" Height="26" Margin="0,6,0,0" />
</Grid>
<StackPanel Grid.Row="2" Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="登录" Width="80" Click="loginButton_Click" />
<Button Content="取消" Width="80" Margin="10,0,0,0"
Click="cancelButton_Click" />
</StackPanel>
</Grid>
</Window>
