WPF 导览 - 第二部分 (布局)






4.77/5 (101投票s)
Windows Presentation Foundation 的导览,一次介绍一个功能。
目录
- 第 1 部分 (XAML):了解 XAML 及其在 WPF 应用程序中的使用方式。
- 第二部分 (布局): 了解布局面板以及它们如何用于构建用户界面。
- 第 3 部分 (数据绑定):了解 WPF 数据绑定如何工作以及如何使用。
- 第四部分 (数据模板和触发器):了解数据模板和触发器是如何工作的以及如何使用它们。
- 第 5 部分 (样式):了解 WPF 中如何设置 UI 样式。
引言
这是关于 Windows Presentation Foundation (WPF) 入门系列文章的第二篇。在上一篇文章中,我们讨论了 XAML 以及它如何在 WPF 应用程序开发中使用。本文将重点介绍 WPF 对布局面板的丰富支持,以及它们如何在 WPF 赛马演示应用程序中使用(该应用程序可在本系列第一篇文章的顶部下载)。
本文无意提供 WPF 中整个布局系统的百科全书式回顾。Windows SDK 文档提供了大量关于如何使用和扩展 WPF 布局系统的材料,因此没有必要重复。相反,我们将简要介绍基础知识,然后探讨 WPF 赛马应用程序如何使用两个最常见的布局面板:Grid 和 StackPanel。
背景
传统的桌面应用程序用户界面开发主要基于视觉元素的绝对定位,即通过设置控件的属性来影响它们的位置和大小。有一些更高级的概念可以用来更轻松地创建适应不断变化的窗口大小的 UI,例如将控件停靠和锚定到其各自的容器。
然而,自 Windows Forms 2.0 起,在自动 UI 布局方面仍有许多不足之处。WPF 非常好地解决了这些问题,在许多情况下借鉴了 HTML 世界中一些更好的布局概念。
通过面板进行布局
Panel
类是 WPF 中所有布局面板的抽象基类。它有一个 Children
属性,其中包含对面板内 UIElement 的引用。如果将一个元素添加到面板的 Children
中,该元素的大小和/或位置将由该面板“管理”。
除了 Canvas
之外,所有 Panel 子类都提供其子元素的自动定位;Canvas
是例外。这基本上意味着,随着窗口大小的变化,面板会自动更新其子元素的位置。一些面板,如 DockPanel 和 Grid,也可以影响其子元素的大小显示。当您希望视觉元素尽可能占据屏幕空间时,例如显示照片或折线图时,这种行为非常有用。
如果您有兴趣了解所有内置的 Panel
子类及其提供的功能,请参阅本文末尾的外部链接部分以获取更多信息。
附加布局设置
WPF 架构师决定,由于布局系统是可扩展的(即您可以继承 Panel
),因此需要一种灵活的方式在面板与其子元素之间传递布局设置。将各种布局属性放在基类(如 UIElement)上会污染其 API,并且自定义面板无法遵循(您无法向 UIElement
类添加属性)。简而言之,他们需要一种方法在面板中为视觉元素设置布局属性,而该元素甚至不知道面板或其属性。
这个问题通过在 WPF 框架中引入附加属性来解决。可以为任何对象设置附加属性;它不必公开要设置的属性。例如
<DockPanel>
<Button DockPanel.Dock="Left">Alf Was Here</Button>
</DockPanel>
上面的 XAML 代码将一个 Button
放在一个 DockPanel 中。Button
被停靠在 DockPanel
的左侧,因为 Dock
附加属性被设置为 Dock
枚举的“Left”值。
有关附加属性工作原理的更多信息,请参阅本文底部的“外部链接”部分中提到的页面。在下一节中,我们将看到附加属性的实际应用。
WPF 赛马应用程序如何使用面板
WPF 赛马应用程序明确使用了两个布局面板:Grid
和 StackPanel
。我说“明确”是因为应用程序中使用的控件本身可能使用了除这两个之外的其他面板。事实上,一个相对简单的 WPF 用户界面通常会包含许多面板,无论大小。
让我们看看主窗口 XAML 文件的一个简化版本
<Window>
<Grid>
<Grid.Background>
<ImageBrush ImageSource="Resources/Background.jpg" Opacity="0.25" />
</Grid.Background>
<Grid.RowDefinitions>
<!-- The top row is for the race track. -->
<RowDefinition Height="*" />
<!-- The bottom row is for the command strip. -->
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- The 'Race Track' area. -->
<ItemsControl Grid.Row="0" ... />
<!-- The 'Command Strip' area -->
<Border Grid.Row="1">
<Grid>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Margin="10,4">Rotation: </TextBlock>
<Slider ... />
<TextBlock ... />
<TextBlock> degrees</TextBlock>
</StackPanel>
<TextBlock HorizontalAlignment="Right" Margin="10,4">
<Hyperlink>Start new race</Hyperlink>
</TextBlock>
</Grid>
</Border>
</Grid>
</Window>
上面的 XAML 包含两个 Grid
和一个 StackPanel
。下面是一张图,显示了这些面板及其包含的元素之间的空间关系。

主布局
窗口中最外层的 Grid
颜色为红色。请注意,它有两个行,在上面的图表中用黑线分开。这两个行是使用以下标记声明的:
<Grid.RowDefinitions>
<!-- The top row is for the race track. -->
<RowDefinition Height="*" />
<!-- The bottom row is for the command strip. -->
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
第一行的 Height
设置为 '*',这意味着它将尝试尽可能高。第二行的 Height
设置为 'Auto',因此它会自动调整大小以适应其包含的元素。这种设置是合理的,因为底部的命令条只需要占用其正常可见所需的空间。屏幕的其余部分应分配给上方的“赛道”。
请注意,行高未明确设置为数值。Height
属性可以根据需要设置为数值,但通常最好让 Grid
类处理尽可能多的度量计算。这样做可以使 Grid
在需要时智能地调整行大小。
接下来,我们可以看到如何指示要在哪一行放置视觉元素。Grid
暴露了几个附加属性,其中一个称为 Row
。这是该附加属性的使用方式:
<!-- The 'Race Track' area. -->
<ItemsControl Grid.Row="0" ... />
<!-- The 'Command Strip' area -->
<Border Grid.Row="1">...</Border>
ItemsControl
不需要设置 Row
附加属性,因为该属性的默认值为零。但是,Border
需要设置 Row
,这样它就不会与第一行中的 ItemsControl
重叠。
命令条区域
回顾上面的图表,我们现在可以深入了解 UI 底部看到的嵌套面板。这是描述该“命令条”区域的 XAML:
<Border Grid.Row="1">
<Grid>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Margin="10,4">Rotation: </TextBlock>
<Slider ... />
<TextBlock ... />
<TextBlock> degrees</TextBlock>
</StackPanel>
<TextBlock HorizontalAlignment="Right" Margin="10,4">
<Hyperlink>Start new race</Hyperlink>
</TextBlock>
</Grid>
</Border>
上述简化 XAML 产生了以下视觉实体:

整个命令条区域包含在一个 Border
元素中,该元素位于主 Grid
面板的底行。Border
的 Child
是另一个 Grid
面板,在上面的图表中用黄色矩形表示。该 Grid
包含两个 UIElement
:一个 StackPanel
和一个 TextBlock
。并非所有面板都可以有多个子元素,因此使用了 Grid
。顺便说一句,Grid
默认有一个行和一个列,因此声明该 Grid
的 XAML 没有指定它们。
StackPanel
(在图表中用蓝色矩形表示)是一个有趣的面板,它可以包含任意数量的子元素,但它们都会紧密地垂直或水平排列在一起(因此,它“堆叠”了子元素)。我选择使用 StackPanel
来包含 Slider
和周围的 TextBlock
s,因为这是一种轻松创建相关元素水平排列的方式。
StackPanel
会自动调整大小以适应其包含的元素。这解释了为什么在上面的图表中,蓝色矩形没有一直延伸到其父 Grid
的右侧。它刚好足够宽,可以使 TextBlock
s 和 Slider
完全可见。
在面板内定位元素
有两种常见的方法可以微调元素在面板内的位置。一种方法是设置元素的 Margin 属性,另一种方法是设置其 HorizontalAlignment 和/或 VerticalAlignment 属性。
我们可以在命令条右侧包含 Hyperlink
的 TextBlock
上看到这两种技术的应用:
<TextBlock HorizontalAlignment="Right" Margin="10,4">
<Hyperlink>Start new race</Hyperlink>
</TextBlock>
将 HorizontalAlignment
属性设置为 'Right
' 会强制 TextBlock
“推”到 Grid
的右侧。将 Margin
属性设置为 "10,4
" 是一个简写,表示它在左右两侧距离任何周围元素至少 10 个 设备无关像素 (DIPs),在上下两侧距离任何周围元素至少 4 个 DIPs。
这两个属性设置的净效果是,Hyperlink
将显示在 Grid
的右侧,但其右边缘与 Grid
的右边缘之间有一些空间。从中可以得到的体会是,面板将执行定位子元素的“粗略布局”,而子元素可以执行“润色”,将自己精确地定位到它们需要的位置。
外部链接
历史
- 2007 年 4 月 2 日 - 创建了本文。