这次我们来探讨一个 WPF 中的简单而又不简单的布局问题:如何实现下图中的效果?

为什么说这个问题简单而又不简单呢?是因为单纯只是实现这个效果的话,我们完全可以使用 Grid 控件来实现。甚至如果完全不在乎优雅的话,我们还可以用 (当然这几乎在任何情况下都是不推荐的)。但是,实际这样干过的开发者相信都很清楚,使用 Canvas 来实现Grid 的实现方式几乎可以说是毫无灵活性的。很快就会被后续的界面微调的需求整得焦头烂额。
那么我们今天就来看一看,这样的需求通常有哪些做法吧。希望这篇文章中提到的某些方式能够帮助到你。
最传统的方式:Grid ¶
在介绍后面的更好的方式之前,我们有必要先来看一看最传统的方式:使用 Grid 控件。这样才能更好地分析这种方式的局限性,以及思考改进的方向。
这样的方式相信所有具备基本 WPF 基础的开发者都相当熟悉:
- 在 XAML 中定义一个 
Grid控件 - 在 
Grid中定义多个RowDefinition和ColumnDefinition - 在 
Grid中定义多个控件,并通过Grid.Row和Grid.Column来指定控件的位置 - (可选)通过 
Grid.RowSpan和Grid.ColumnSpan来指定控件的跨行和跨列 
好,然后我们就开始写 XAML 吧:
 |  | 
好的,马上痛苦面具就戴上了:为每个控件写 Grid.Row 和 Grid.Column,简直就是顶级折磨。如果你搞了十几行,结果被告知需要删掉前面某一行,或者在前面添加一行,那就更是欲哭无泪了。
Style 来统一指定上面的 TextBox 控件的 Grid.Column,并省略 Label 的 Grid.Column="0"。但这样依旧存在相当多的局限性,毕竟它最怕的就是例外情况了。所以,我们显然是不能满足于使用这样的方式的。那究竟有什么更好的方式呢?
外层使用 UniformGrid ¶
说起“自动化布局”,相信很多人在使用 Grid 时都垂涎过 UniformGrid 的功能,因为它可以让所有子控件依次填充到每个单元格中。所以,我们或许可以借助它的这一效果。比如我们想要一个两列的网格,可以:
 |  | 
但这样的局限性在于,所有子控件的大小(或者它们所处的单元格)都是完全等大的。这可能会失去灵活性,并且看起来有些呆板。那么我们还可以部分借助 UniformGrid 的功能,比如下面这样:
 |  | 
这里我们将每一行的内容放在了一个 StackPanel 中,从而让 Label 和 TextBox 出现在同一行(或者说 UniformGrid 的同一个单元格中)。这样我们就不需要再为每个控件指定 Grid.Row 和 Grid.Column 了。
为了更加简化代码,我们还声明了两个 Style,分别用于设置 StackPanel 的 Orientation 和 Label 的 Width。这样我们就可以在 StackPanel 中直接放置 Label 和 TextBox,而不需要再为 Label 设置宽度了。
但是这样做有一个明显的局限性:行高是一致的。如果我们需要不同行的高度不一致,那么这种方式就无法满足需求了。
外层使用 StackPanel ¶
为了突破上一个方式的局限性,我们可以考虑使用 StackPanel 来替代 UniformGrid。这样我们就可以为每一行的 StackPanel 设置不同的高度了。
 |  | 
但很快,我们又会被别的问题所折磨:如何高效地设置行间距?
而且可能还有另外一个小折磨:因为内外都使用了 StackPanel,所以写的 Style 可能会被它们共同使用。
StackPanel 替换为 WrapPanel,但这样并不是一个好主意,因为 WrapPanel 会让每一行的控件都尽可能地靠左对齐,并且在宽度不够时会自动换行!或许现在看着还好,但调整了窗口或父控件的尺寸,甚至修改了分辨率及缩放后,都有可能让你的界面变得一团糟。此时,我们可以请个“外援”。其实在 Win UI 3、Avalonia UI 等框架中,StackPanel 天生就比 WPF 多了一个属性:Spacing。这个属性可以让我们很方便地设置行间距。但是在 WPF 中,我们就没有这么幸运了。不过,我们可以借助自定义控件来实现这一功能。
代码也不用我们自己写,网上可以找到一些开源实现。比如这里我贴两个供大家参考:
假如我们搬运到了 local 命名空间下,那么我们就可以这样使用:
 |  | 
这样看起来就方便多了。此外,我们还有一些小技巧,来进一步提高布局的效率:
- 为 
Label设置一个固定的宽度,这样可以让所有的Label对齐 - 为 
Label设置VerticalAlignment或VerticalContentAlignment为Center,这样可以让Label和TextBox垂直居中对齐 - 为 
TextBox等控件(还比如CheckBox、ComboBox等)设置固定的宽度,这样可以让所有的TextBox对齐 
使用更高级的 Grid ¶
上面的方式其实已经相当灵活了,尤其是对于可能需要增删、调换顺序之类的情形。但是可能仍然会觉得不够爽,因为里面多出了一层 StackPanel,这写起来就还是会觉得不太爽。
所以,有没有办法让 Grid 更加智能一点,比如可以像是 WrapPanel 或者 UniformGrid 那样,自动填充控件呢?
答案当然是有的。这里给大家推荐一个 NuGet 包:WpfAutoGrid。这个 AutoGrid 就正是我们梦寐以求的控件。
WpfAutoGrid.Core它的使用也非常简单:
 |  | 
这样我们连内部的 StackPanel 都不需要了,直接将 Label 和 TextBox 放在 AutoGrid 中即可。此外,它提供了很多实用的属性,比如上面实用的 Columns、RowCount、RowHeight,还有 ColumnSpacing、RowSpacing 等。不仅如此,AutoGrid 还能正确响应子控件 ColumnSpan 等属性,可以让我们更加灵活地布局控件。
除此之外,这里我再贴一个我自己写的 GridAssist。它是一个附加属性,可以直接用于原生的 Grid 控件。使用这个附加属性,就能够自动为子控件添加 Grid.Row 和 Grid.Column 属性。
使用方式也非常简单:
 |  | 
这个附加属性的 AutoRowColumn 填写的 _,2 意思是,行数自动增加,列数固定为 2(还可以写 _,2,Auto,从而将列宽设置为 Auto)。这样我们就不需要为每个控件指定 Grid.Row 和 Grid.Column 了。然后,我们只需要将子控件按照从上到下,从左到右的顺序放置即可,不需要再引入一层 StackPanel 了。此外,它也是可以正确响应 Grid.ColumnSpan 的。
总结 ¶
在 WPF 中,布局是一个非常重要的问题。而对于多行多列的控件布局,我们可以使用 Grid、UniformGrid、StackPanel、AutoGrid 等控件或者附加属性来实现。每一种方式都有它的优势和局限性,我们可以根据实际情况来选择最适合的方式。
另外,对于上面提到的几种自定义控件或附加属性,我们完全可以直接将代码复制到自己的项目中,然后根据实际需求进行修改。这样可以更好地适应自己的项目,也可以更好地理解这些控件或属性的实现原理,还可以省去引入第三方库的麻烦。
