2.3 布局与自适应窗口
学习目标
本节学习用容器表达布局意图,而不是逐个控件指定坐标。你会掌握 Grid 的三种长度,理解 StackPanel 为什么可能让列表不断变高,并能在改变窗口尺寸时预测哪些区域应该伸缩。
布局是测量与排列
WPF 大致先测量,再排列。父容器向子控件提供可用尺寸,子控件报告期望尺寸;父容器再决定实际位置和大小。字体、内容、边距和容器算法都会参与这个过程。没有写 Width 不等于没有宽度,而是允许布局系统根据可用空间分配它。
对熟悉网页的人来说,容器依然是核心,但具体规则不同。HorizontalAlignment="Stretch" 允许控件填满父容器提供的水平区域;Margin 是外侧间距,Padding 是部分控件提供的内侧间距。固定 Width 后,即使对齐是 Stretch,控件也不会无条件填满所有空间。
常用容器各有目的:Grid 按行列分配空间;StackPanel 按一个方向堆叠;DockPanel 适合把内容停靠到边缘,最后一个子元素默认填充剩余区域;WrapPanel 适合会换行的标签或按钮;Canvas 使用坐标,常用于绘图和拖动场景。业务表单通常先选 Grid,不必把整个窗口写成层层嵌套的堆叠容器。
正确理解 Grid 长度
Grid 的长度并非全都表示像素:
| 写法 | 含义 | 常见用途 |
|---|---|---|
160 | 固定 160 个设备独立单位 | 侧栏、窄标签列 |
Auto | 根据内容的期望尺寸分配 | 标题、操作栏 |
* | 按权重分配剩余空间 | 主内容区 |
2* | 在同方向星号区域中占两份权重 | 两块内容按 2:1 分配 |
假设网格宽度有限,两列分别是 2* 与 *,扣除固定列和自动列后,它们按 2:1 分配剩余宽度。星号不是“占窗口的百分之百”。当网格本身在某方向得到无限的测量空间时,也无法期待它按有限的窗口剩余空间分配星号区域。
一个可调整大小的工作区
沿用 WpfBasics 项目,将 MainWindow.xaml 替换为下面完整文件,代码后置保持环境章节中的空构造函数即可。
<Window x:Class="WpfBasics.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="任务工作区" Width="800" Height="500"
MinWidth="620" MinHeight="360">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.ColumnSpan="2" Text="我的任务"
FontSize="24" Margin="0,0,0,16" />
<ListBox Grid.Row="1" Margin="0,0,16,0">
<ListBoxItem Content="全部任务" />
<ListBoxItem Content="待完成" />
<ListBoxItem Content="已完成" />
</ListBox>
<Border Grid.Row="1" Grid.Column="1"
BorderBrush="#AAB4C4" BorderThickness="1"
Padding="16">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="任务说明" FontSize="18"
Margin="0,0,0,12" />
<TextBox Grid.Row="1"
Text="试着调整窗口大小,观察编辑区。"
AcceptsReturn="True"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
</Grid>
</Border>
<StackPanel Grid.Row="2" Grid.ColumnSpan="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0,16,0,0">
<Button Content="保存" Padding="16,6" Margin="0,0,8,0" />
<Button Content="取消" Padding="16,6" />
</StackPanel>
</Grid>
</Window>
标题和操作栏只占内容需要的高度,中间工作区得到剩余高度。侧栏固定宽度,右侧编辑区随窗口伸缩。按钮使用水平 StackPanel,因为少量按钮本来就需要按内容宽度排列。示例没有连接保存事件,按钮用于观察布局;交互实现见下一节。
StackPanel 的无限测量
垂直 StackPanel 会在垂直方向给子控件无限的测量空间,让它们报告完整期望高度;水平方向的规则与方向相对应。这适合堆叠少量表单项,却容易使 ListBox 或 DataGrid 按全部内容扩展,无法获得预期的有限滚动区域。外面再套一个 ScrollViewer 也可能改变测量条件,影响列表自身的滚动与虚拟化。
如果列表应该填满窗口余下高度,把它放在有有限高度的 Grid 星号行中,通常就能让列表内部滚动正常工作。不要先通过设置 Height="300" 掩盖问题:这会把问题转化为高分辨率或小窗口上的空白与裁剪。
长说明文字需要 TextWrapping="Wrap",但换行同样要求可用宽度受到约束。把它放在无限宽的水平堆叠容器中,就不能指望它按窗口边界换行。字体放大和系统 DPI 设置也会影响期望尺寸,因此入门时就应该练习缩放窗口,而不仅检查默认大小。
常见错误
- 列表把底部按钮挤出窗口:检查列表是否位于垂直
StackPanel,改用有限空间中的星号行。 - 写了
Grid.Row却没有效果:该控件的直接布局父级需要是对应Grid;附加属性不会跨越中间容器替你定位。 - 控件重叠:未设置行列时默认都在第 0 行、第 0 列;网格允许重叠,声明顺序还会影响绘制层次。
- 窗口拉大但内容不变:检查控件或上层容器的固定宽高,以及对齐方式是否允许伸展。
动手练习与验收
把侧栏改成 1*,内容区改成 3*,再给侧栏添加 MinWidth="120"。测试最小尺寸、默认尺寸和最大化窗口,并输入一段超过编辑区可见高度的多行文字。
验收标准:两列在正常可用空间内大致按 1:3 伸缩;侧栏不低于最小宽度;标题和底部按钮一直可见;编辑区长内容通过自身滚动访问。最后把编辑区临时放进垂直 StackPanel,解释为什么观察到的高度行为发生变化,然后恢复网格布局。