在本章中,我们将首先考虑 Windows Presentation Foundation (WPF) 为我们提供的现有控件,并了解如何使用它们来创建我们需要的布局。 我们将研究修改这些控件的多种方法,以避免创建新控件。
我们将检查现有控件中内置的各个级别的功能,然后发现如何在需要时最好地声明我们自己的控件。 我们将深入研究我们拥有的各种选项,并确定何时最好使用每个选项。 让我们直接看一下各种布局控件。
研究内置控件
.NET Framework 中包含多种控件。 它们涵盖了最常见的场景,并且我们很少需要在典型的基于表单的应用程序中创建自己的控件。 所有 UI 控件的功能往往都是由大量公共基类构建的。
所有控件将共享提供核心级功能的相同核心级基类,然后共享许多派生的框架级类,这些类提供与 WPF 框架关联的功能,例如数据绑定、样式设置和模板。 让我们看一个例子。
继承框架能力
与应用程序框架中的基类一样,内置 WPF 控件也具有继承层次结构,每个连续的基类都提供一些附加功能。 让我们以 Button 类为例。 以下是 Button 控件的继承层次结构:
System.Object
System.Windows.Threading.DispatcherObject
System.Windows.DependencyObject
System.Windows.Media.Visual
System.Windows.UIElement
System.Windows.FrameworkElement
System.Windows.Controls.Control
System.Windows.Controls.ContentControl
System.Windows.Controls.Primitives.ButtonBase
System.Windows.Controls.Button
与 .NET Framework 中的每个对象一样,我们从 Object 类开始,它为所有类提供低级服务。 其中包括对象比较、最终确定以及输出每个对象的可自定义字符串表示形式的能力。
接下来是 DispatcherObject 类,它为每个对象提供线程关联性并将它们与 Dispatcher 对象关联起来。 Dispatcher 类管理各个线程的工作项的优先级队列。 只有创建关联的 Dispatcher 对象的线程才能直接访问每个 DispatcherObject,这使得派生类能够强制执行线程安全。
在 DispatcherObject 类之后,我们有 DependencyObject 类,它使所有派生类能够使用 WPF 属性系统并声明依赖项属性。 我们调用来访问和设置它们的值的 GetValue 和 SetValue 方法也是由 DependencyObject 类提供的。
接下来是 Visual 类,它的主要作用是提供渲染支持。 UI 中显示的所有元素都将扩展 Visual 类。 除了渲染每个对象之外,它还计算它们的边界框并提供对命中测试、剪切和转换的支持。
UIElement 类扩展了 Visual 类,它为其所有派生类提供了许多核心服务。 其中包括事件和用户输入系统以及确定元素的布局外观和渲染行为的能力。
接下来是 FrameworkElement 类,它提供了第一个框架级成员,构建在它扩展的核心级类的基础上。 FrameworkElement 类支持通过 DataContext 属性进行数据绑定并通过 Style 属性进行样式设置。
它还提供与对象生命周期相关的事件、将核心级布局系统升级为完整布局系统以及改进对动画的支持等。 如果我们创建自己的基本元素,这通常是我们可能想要扩展的最低级别的类,因为它使派生类能够参与大部分 WPF UI 功能。
Control 类扩展了 FrameworkElement 类,并且是大多数 WPF UI 元素的基类。 它通过使用其 ControlTemplate 功能和许多与外观相关的属性来提供外观模板。 其中包括颜色属性,例如背景、前景和 BorderBrush,以及对齐和字体属性。
ContentControl 类是 Control 类的扩展,它使控件能够将任何 CLR 类型的一个对象作为其内容。 这意味着我们可以将数据对象或 UI 元素设置为内容,但如果数据对象是自定义类型,我们可能需要为它们提供 DataTemplate。
Button 类扩展的一长串父类中的最后一个类是 ButtonBase 类。 事实上,这是 WPF 中所有按钮的基类,它为按钮添加了有用的功能。 这包括自动将某些键盘事件转换为鼠标事件,以便用户无需使用鼠标即可与按钮交互。
Button 类本身仅通过三个相关的 bool 属性对其继承成员添加了很少的内容; 两个指定按钮是否为默认按钮,另一个指定按钮是否为取消按钮。 我们很快就会看到一个这样的例子。 它还有另外两个受保护的重写方法,当单击按钮或为其创建自动化对等点时,将调用这些方法。
虽然 WPF 使我们能够将现有控件修改到很少需要创建自己的控件的程度,但了解这种继承层次结构非常重要,以便我们可以在需要时扩展满足我们要求的适当且最轻量级的基类 到。
例如,如果我们想创建自己的自定义按钮,则扩展 ButtonBase 类(而不是 Button 类)通常更有意义,如果我们想创建完全独特的控件,则可以扩展 FrameworkElement 类。 现在我们已经很好地了解了可用控件的构成,接下来让我们看看 WPF 布局系统如何显示它们。
将其放在线上
在 WPF 中,布局系统负责获取要显示的每个元素的大小,将它们定位在屏幕上,然后绘制它们。 由于控件可以包含在其他控件中,因此布局系统以递归方式工作,每个子控件的整体位置由其父面板控件的位置确定。
布局系统首先在所谓的测量通道中测量每个面板中的每个子项。 在此过程中,每个面板都会调用每个子元素的 Measure 方法,并指定它们理想的空间大小; 这决定了 UIElement.DesiredSize 属性值。 请注意,这不一定是给他们多少空间。
在测量过程之后是排列过程,每个面板调用每个子元素的排列方法。 在此过程中,面板根据其 DesiredSize 值生成其每个子元素的边界框。 布局系统将调整这些尺寸以添加任何所需的边距或可能需要的其他调整。
它向面板的 ArrangeOverride 方法的输入参数返回一个值,并且每个面板在返回可能调整的值之前执行其自己的特定布局行为。 布局系统在将执行返回到面板并完成布局过程之前执行任何剩余的所需调整。
在开发应用程序时,我们需要小心,以确保不会不必要地触发布局系统的额外通道,因为这可能会导致性能不佳。 当添加或删除集合中的项目、对元素应用转换或调用 UIElement.UpdateLayout 方法(这会强制执行新的布局传递)时,可能会发生这种情况。
Containing controls(包含控件)
现有的控件大多可以分为两大类:为其他控件提供布局支持的控件和构成可见 UI 的控件,并按第一类控件排列在其中。 第一类控件当然是面板,它们提供了多种在 UI 中排列子控件的方法。
有些提供调整大小功能,而另一些则不提供,有些比其他更高效,因此使用正确的面板来完成手头的工作非常重要。 此外,不同的面板提供不同的布局行为,因此最好了解可用的面板是什么以及它们各自在布局方面为我们提供了什么。
所有面板都扩展了抽象Panel 类,并且扩展了FrameworkElement 类,因此它具有该类的所有成员和功能。 但是,它不扩展 Control 类,因此无法继承其属性。 因此,它添加了自己的背景属性,使用户能够为面板各个项目之间的间隙着色。
Panel 类还提供了一个 Children 属性,该属性表示每个面板中的项目,尽管除非创建自定义面板,否则我们通常不会与此属性进行交互。 相反,我们可以通过直接在 XAML 中的面板元素中声明我们的子元素来填充此集合。
我们之所以能够做到这一点,是因为 Panel 类在其类定义的 ContentPropertyAttribute 属性中指定了 Children 属性。 虽然 ContentControl 的 Content 属性通常使我们能够添加单个内容项,但我们也能够将多个项目添加到面板中,因为它们的 Children 属性(设置为内容)是一个集合。
我们可能需要使用的另一个 Panel 类属性是 IsItemsHost 属性,它指定面板是否用作 ItemsControl 元素的项目的容器。 默认值为 false,因此显式将此属性设置为 false 是没有意义的。 事实上,只有在非常特殊的情况下才需要它。
这种情况是当我们在 ControlTemplate 中替换 ItemsControl 的默认面板或其派生类之一(例如 ListBox)时。 通过在 ControlTemplate 中的面板元素上将此属性设置为 true,我们告诉 WPF 将生成的集合元素放置在面板中。 让我们看一个简单的例子:
<ItemsControl ItemsSource="{Binding Users}">
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<StackPanel Orientation="Horizontal" IsItemsHost="True" />
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
在这个简单的示例中,我们将 ItemsControl 元素的默认内部项目面板替换为水平 StackPanel。 请注意,这是永久替换,如果不提供新的 ControlTemplate,任何人都无法对其进行进一步更改。 然而,有一种更简单的方法可以达到相同的结果,我们在第 4 章“精通数据绑定”中看到了一个示例:
<ItemsControl ItemsSource="{Binding Users}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
在这个替代示例中,我们只是通过 ItemsControl 的 ItemsPanel 属性为 ItemsControl 提供一个新的 ItemsPanelTemplate。 使用此代码,内部面板仍然可以轻松更改,而不需要提供新的 ControlTemplate,因此当我们不希望其他用户能够交换内部面板时,我们使用第一种方法,否则,我们使用 这个方法。
Panel 类还声明了一个 ZIndex 附加属性,子元素可以使用该属性来指定面板内的分层顺序。 具有较高值的子元素将出现在具有较低值的元素上方或前面,尽管在不与其子元素重叠的面板中会忽略此属性。 我们将在下一节中看到这样的示例,因此现在让我们重点关注从 Panel 类派生的面板以及它们为我们提供的内容。
Canvas
Canvas 类使我们能够使用 Canvas.Top、Canvas.Left、Canvas.Bottom 和 Canvas.Right 附加属性的组合显式定位子元素。 这与旧的 Windows 窗体系统的控件放置有些相似。
但是,在使用 WPF 时,我们通常不会在 Canvas 中布局 UI 控件。 相反,我们倾向于更多地使用它们来显示形状、构建图表、显示动画或绘制应用程序。 举个例子:
<Canvas Width="256" Height="109" Background="Black">
<Canvas.Resources>
<Style TargetType="{x:Type Ellipse}">
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
<Setter Property="Stroke" Value="Black" />
<Setter Property="StrokeThickness" Value="3" />
</Style>
</Canvas.Resources>
<Canvas Canvas.Left="3" Canvas.Top="3" Background="Orange"
Width="123.5" Height="50">
<Ellipse Canvas.Top="25" Canvas.Left="25" Fill="Cyan" />
</Canvas>
<Canvas Canvas.Left="129.5" Canvas.Top="3" Background="Orange"
Width="123.5" Height="50" Panel.ZIndex="1" />
<Canvas Canvas.Left="3" Canvas.Top="56" Background="Red" Width="250"
Height="50" ClipToBounds="True">
<Ellipse Canvas.Top="-25" Canvas.Left="175" Fill="Lime" />
</Canvas>
<Ellipse Canvas.Top="29.5" Canvas.Left="103" Fill="Yellow" />
</Canvas>
这个例子展示了一些重要的点,所以在讨论之前让我们先看看这段代码的视觉输出:
左上角的矩形是一个画布的输出,右上角和底部的矩形来自另外两个画布实例。 它们都包含在具有黑色背景的父画布元素内。 三个内部画布间隔开,以达到每个画布都有边框的效果。 它们按照左上、右上、下的顺序声明,最后声明的元素是中间的圆圈。
左边的圆圈正在左上角的画布中绘制,我们可以看到它与画布的明显底部边框重叠的位置,这表明它没有被其父画布剪切。 但是,它被下部画布元素剪切,这表明稍后声明的 UI 元素将显示在较早声明的元素的顶部。
尽管如此,要声明的第二个画布正在裁剪中间的圆圈,这是最后声明的元素。 这表明,将元素上的 Panel.ZIndex 属性设置为任何正数都会将该元素置于所有其他未显式设置此属性的元素之上。 此属性的默认值为零,因此将此属性设置为 1 的元素将呈现在所有未为其显式设置值的元素之上。
要声明的下一个元素是底部矩形,并在其中声明右圆。 现在,由于该元素是在顶部画布之后声明的,因此您可能认为右侧的圆圈将与右上角的画布重叠。 虽然通常会出现这种情况,但我们的示例不会发生这种情况,原因有两个。
第一个原因,正如我们刚刚发现的,是因为右上面板的 ZIndex 属性值比下面板高,第二个原因是因为我们将 UIElement.ClipToBounds 属性设置为 true,该属性由 画布面板,以确定是否应剪辑可能位于面板边界之外的任何子项的视觉内容。
这通常与动画一起使用,以使视觉效果隐藏在面板边界之外,然后滑入视图以响应某些事件。 我们可以看出右侧的圆圈已被其父面板剪切,因为我们可以看到其明显的顶部边框,该边框超出了其边界。
最后一个要声明的元素是中间的圆圈,我们可以看到,除了具有较高 ZIndex 属性值的重叠画布元素之外,它与所有其他元素都重叠。 请注意,Canvas 面板不会对其子面板执行任何类型的大小调整,因此它通常不用于生成表单类型 UI。。
DockPanel
DockPanel 类主要用在控件层次结构的顶层,以布置顶层控件。 它为我们提供了将控件停靠到屏幕各个部分的能力,例如,停靠在顶部的菜单、左侧的上下文菜单、底部的状态栏以及其余部分的主视图内容控件。 屏幕:
仅使用以下 XAML 即可轻松实现上图中所示的布局:
<DockPanel>
<DockPanel.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontSize" Value="14" />
</Style>
<Style TargetType="{x:Type Border}">
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="BorderThickness" Value="1" />
</Style>
</DockPanel.Resources>
<Border Padding="0,3" DockPanel.Dock="Top">
<TextBlock Text="Menu Bar" />
</Border>
<Border Padding="0,3" DockPanel.Dock="Bottom">
<TextBlock Text="Status Bar" />
</Border>
<Border Width="100" DockPanel.Dock="Left">
<TextBlock Text="Context Menu" TextWrapping="Wrap" />
</Border>
<Border>
<TextBlock Text="View" />
</Border>
</DockPanel>
我们使用 DockPanel.Dock 附加属性指定面板中每个元素停靠的位置。 我们可以指定面板的左侧、右侧、顶部和底部。 剩余空间通常由未显式设置 Dock 属性之一的最后一个子级填充。 但是,如果这不是我们想要的行为,那么我们可以将 LastChildFill 属性设置为 false。
DockPanel 将自动调整自身大小以适应其内容,除非指定了其尺寸(显式使用 Width 和 Height 属性或通过父面板隐式指定)。 如果它及其子项都指定了尺寸,则某些子项可能无法提供足够的空间而无法正确显示,因为最后一个子项是唯一可以由 DockPanel 调整大小的子项。 还应该注意的是,该面板不与其子元素重叠。
另请注意,声明子级的顺序将影响为每个子级提供的空间和位置。 例如,如果我们希望菜单栏占据屏幕顶部,上下文菜单占据剩余的左侧,视图和状态栏占据剩余空间,我们可以在状态栏之前声明上下文菜单 酒吧:
...
<Border Padding="0,3" DockPanel.Dock="Top">
<TextBlock Text="Menu Bar" />
</Border>
<Border Width="100" DockPanel.Dock="Left">
<TextBlock Text="Context Menu" TextWrapping="Wrap" />
</Border>
<Border Padding="0,3" DockPanel.Dock="Bottom">
<TextBlock Text="Status Bar" />
</Border>
<Border>
<TextBlock Text="View" />
</Border>
...
这种细微的变化将导致以下布局:
Grid
在布局典型 UI 控件时,网格面板是迄今为止最常用的面板。 它是最通用的,使我们能够执行许多技巧来最终得到我们需要的布局。 它提供了灵活的基于行和列的布局系统,我们可以使用它来构建具有流畅布局的 UI。 当用户调整应用程序窗口大小时,流体布局能够做出反应并更改大小。
网格是少数可以根据可用空间调整其所有子元素大小的面板之一,这使其成为性能最密集的面板之一。 因此,如果我们不需要它提供的功能,我们应该使用性能更高的面板,例如 Canvas 或 StackPanel。
Grid 面板的子项都可以设置其 Margin 属性,以使用绝对坐标进行布局,与 Canvas 面板的方式类似。 但是,应尽可能避免这种情况,因为这会破坏 UI 的流畅性。 相反,我们通常使用网格的 RowDefinitions 和 ColumnDefinitions 集合以及 Grid.Row 和 Grid.Column 附加属性来定义所需的布局。
虽然我们可以再次对行和列的精确宽度和高度进行硬编码,但出于同样的原因,我们通常会尽量避免这样做。 相反,我们通常利用网格的大小调整行为并声明我们的行和列,主要使用两个值之一。
第一个是“自动”值,它从内容中获取其大小,第二个是默认的 * 星形大小的值,它占用所有剩余空间。 通常,我们将所有列或行设置为“自动”,但包含最重要数据的列或行除外(设置为 *)。
请注意,如果我们有多个星形大小的列,那么空间通常会在它们之间平均分配。 但是,如果我们需要对剩余空间进行不等分,那么我们可以用星号指定一个乘数,该乘数将乘以该行或列所提供的空间比例。 让我们看一个例子来帮助澄清这一点:
<Grid TextElement.FontSize="14" Width="300" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2.5*" />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.ColumnSpan="3" HorizontalAlignment="Center"
VerticalAlignment="Center" Text="Are you sure you want to continue?"
Margin="40" />
<Button Grid.Row="1" Grid.Column="1" Content="OK" IsDefault="True"
Height="26" Margin="0,0,2.5,0" />
<Button Grid.Row="1" Grid.Column="2" Content="Cancel" IsCancel="True"
Height="26" Margin="2.5,0,0,0" />
</Grid>
这个例子演示了很多点,所以在继续之前让我们看看渲染的输出:
在这里,我们有一个非常基本的确认对话框控件。 它由三列两行的网格面板组成。 请注意,单个星号大小分别用作 ColumnDefinition 和 RowDefinition 元素的默认宽度和高度值; 我们不需要显式设置它们,只需声明空元素即可。 另请注意,只有当网格面板设置了某种尺寸时,星形尺寸才会起作用,就像我们在这里所做的那样。
因此,在我们的示例中,第二列和第三列以及第一行将使用星形调整大小并占用所有剩余空间。 第一列也使用星型大小调整,但它指定的乘数值为 2.5。 因此,它将提供的空间量是其他两列的两倍半。
请注意,第一列仅用于将其他两列中的按钮推至正确位置。 虽然 TextBlock 元素是在第一列中声明的,但它不仅驻留在该列中,因为它还指定了 Grid.ColumnSpan 附加属性,这允许它分布在多个列中。 Grid.RowSpan 附加属性对行执行相同的操作。
每个元素使用 Grid.Row 和 Grid.Column 附加属性来指定它们应该在哪个单元格中呈现。但是,这些属性的默认值为零,因此,当我们想要在第一列或 行,我们可以省略这些属性的设置,就像我们示例中对 TextBlock 所做的那样。
OK 按钮已在第二行和第二列中声明,并将 IsDefault 键设置为 true,这使得用户可以通过按键盘上的 Enter 键来调用它。 它还负责按钮上的蓝色边框,我们可以使用此属性在我们自己的模板中以不同的方式设置默认按钮的样式。 “取消”按钮位于第三列中的旁边,并将 IsCancel 属性设置为 true,这样用户就可以通过按键盘上的 Esc 键来选择它。
请注意,我们可以将较低的 RowDefinition.Height 属性设置为 26,而不是在每个按钮上显式设置该值,最终结果将是相同的,因为无论如何,自动值都会根据其高度计算。 另请注意,此处在一些元素上设置的 Margin 属性仅用于间距目的,而不是用于绝对定位目的。
Grid 类声明了另外两个有用的属性。 第一个是 ShowGridLines 属性,正如您可以想象的那样,当设置为 true 时,它会显示面板中行和列的边框。 虽然对于前面的示例中的简单布局来说并不真正需要,但这在开发更复杂的布局时非常有用。 但是,由于其性能较差,因此永远不应在生产 XAML 中使用此功能:
<Grid TextElement.FontSize="14" Width="300" Margin="10"
ShowGridLines="True">
...
</Grid>
让我们看看现在可见的网格线是什么样子的:
另一个有用的属性是 IsSharedSizeScope 附加属性,它使我们能够在两个或多个网格面板之间共享大小调整信息。 我们可以通过在父面板上将此属性设置为 true,然后在内部网格面板的相关 ColumnDefinition 和/或 RowDefinition 元素上指定 SharedSizeGroup 属性来实现此目的。
为了使其发挥作用,我们需要遵守一些条件,第一个条件与范围有关。 IsSharedSizeScope 属性需要在父元素上设置,但如果该父元素位于资源模板内,并且指定 SharedSizeGroup 属性的定义元素位于该模板外部,则该属性将不起作用。 然而,它会朝相反的方向发挥作用。
需要注意的另一点是,在共享尺码信息时,不会考虑明星尺码。 在这些情况下,任何定义元素的星号值都将被读取为 Auto,因此我们通常不会在星号大小的列上设置 SharedSizeGroup 属性。 但是,如果我们将其设置在其他列上,那么我们将保留所需的布局。 让我们看一个例子:
<Grid TextElement.FontSize="14" Margin="10" IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid TextElement.FontWeight="SemiBold" Margin="0,0,0,3"
ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" SharedSizeGroup="Age" />
</Grid.ColumnDefinitions>
<TextBlock Text="Name" />
<TextBlock Grid.Column="1" Text="Comments" Margin="10,0" />
<TextBlock Grid.Column="2" Text="Age" />
</Grid>
<Separator Grid.Row="1" />
<ItemsControl Grid.Row="2" ItemsSource="{Binding Users}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type DataModels:User}">
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" SharedSizeGroup="Age" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" />
<TextBlock Grid.Column="1" Text="Star-sized column takes all
remaining space" Margin="10,0" />
<TextBlock Grid.Column="2" Text="{Binding Age}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
在此示例中,我们有一个 ItemsControl,它的数据绑定到我们之前示例中的 Users 集合的稍微编辑过的版本。 以前,所有用户名的长度都相似,因此已对其进行编辑以更清楚地演示这一点。 出于同样的原因,内部面板上的 ShowGridLines 属性也被设置为 true。
在示例中,我们首先在父 Grid 面板上将 IsSharedSizeScope Attached 属性设置为 true,然后将 SharedSizeGroup 属性应用于内部 Grid 控件的定义,这些控件在外部面板和 DataTemplate 元素内声明。 在继续之前让我们看看这段代码的渲染输出:
请注意,我们为 DataTemplate 元素内部和外部的列提供了相同数量的列和组名称,这对于此功能的工作至关重要。 另请注意,我们尚未在中间列(星形大小)上设置 SharedSizeGroup 属性。
仅对其他两列进行分组将具有与对所有三列进行分组相同的视觉效果,但不会丢失中间列上的星星大小。 但是,让我们看看如果我们还在中间列定义上设置 SharedSizeGroup 属性会发生什么:
<ColumnDefinition SharedSizeGroup="Comments" />
正如预期的那样,我们失去了中间列的星号大小,剩余空间现在已应用于最后一列:
模板中的 Grid
面板将为集合中的每个项目呈现,因此这实际上会产生多个面板,每个面板具有相同的组名,因此也具有相同的列间距。 重要的是,我们在 Grid
面板上将 IsSharedSizeScope
属性设置为 true
,这是我们希望在它们之间共享大小信息的所有内部面板的公共父级。
StackPanel
StackPanel
是 WPF 面板之一,它仅为其子项提供有限的调整大小功能。 它将自动将其每个子项的 HorizontalAlignment
和 VerticalAlignment
属性设置为 Stretch
,只要它们没有指定明确的尺寸。 仅在这些情况下,子元素将被拉伸以适应包含面板的大小。 这可以很容易地证明如下:
<Border Background="Black" Padding="5">
<Border.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Padding" Value="5" />
<Setter Property="Background" Value="Yellow" />
<Setter Property="TextAlignment" Value="Center" />
</Style>
</Border.Resources>
<StackPanel TextElement.FontSize="14">
<TextBlock Text="Stretched Horizontally" />
<TextBlock Text="With Margin" Margin="20" />
<TextBlock Text="Centered Horizontally"
HorizontalAlignment="Center" />
<Border BorderBrush="Cyan" BorderThickness="1" Margin="0,5,0,0"
Padding="5" SnapsToDevicePixels="True">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Stretched Vertically" />
<TextBlock Text="With Margin" Margin="20" />
<TextBlock Text="Centered Vertically"
VerticalAlignment="Center" />
</StackPanel>
</Border>
</StackPanel>
</Border>
这个面板实际上是一个接一个地放置每个子元素,默认情况下是垂直的,或者当它的 Orientation
属性设置为 Horizontal
时水平放置。 我们的示例使用了两个方向,因此在继续之前让我们快速查看一下它的输出:
我们的整个示例被包裹在一个黑色背景的 Border 元素中。 在其资源部分中,我们为示例中的 TextBlock 元素声明了一些样式属性。 在边框内,我们声明第一个 StackPanel 控件,其默认垂直方向。 在第一个面板中,我们有三个 TextBlock 元素和另一个包裹在边框中的 StackPanel。
第一个 TextBlock 元素会自动拉伸以适合面板的宽度。 第二个添加了边距,但否则也会在面板的宽度上拉伸。 然而,第三个将其 HorizontalAlignment 属性显式设置为 Center,因此不会拉伸以适合面板。
内部面板在其内部声明了三个 TextBlock 元素,并将其 Orientation 属性设置为 Horizontal。 因此,它的子项是水平排列的。 它的边框是彩色的,这样更容易看到它的边界。 请注意对其设置的 SnapsToDevicePixels 属性的使用。
由于 WPF 使用与设备无关的像素设置,细直线有时会跨越各个像素边界并出现抗锯齿效果。 将此属性设置为 true 将强制将元素渲染为与物理像素完全一致,使用特定于设备的像素设置并形成更清晰、更锐利的线条。
下部面板中的第一个 TextBlock 元素会自动拉伸以适合面板的高度。 与上面板中的元素一样,第二个添加了边距,但也会在面板的高度上拉伸。 然而,第三个将其 VerticalAlignment 属性显式设置为 Center,因此不会垂直拉伸以适应面板。
作为旁注,我们使用十六进制实体在一些文本字符串中添加新行。 这也可以通过使用 TextBlock.TextWrapping 属性并对每个元素硬编码宽度来实现,但这种方法显然要简单得多。
UniformGrid
UniformGrid 面板是一个轻量级面板,它提供了一种创建项目网格的简单方法,其中每个项目的大小相同。 我们可以设置它的 Row 和 Column 属性来指定我们希望网格有多少行和列。 如果我们不设置这些属性中的一个或两个,面板将根据其拥有的可用空间及其子项的大小隐式地为我们设置它们。
它还为我们提供了 FirstColumn 属性,该属性将影响第一个子项将在其中呈现的列。例如,如果我们将此属性设置为 2,则第一个子项将在第三列中呈现。 这对于日历控件来说是完美的,所以让我们看看如何使用 UniformGrid 创建以下输出:
如您所见,日历控件通常需要在前几列中有空格,因此 FirstColumn
属性可以简单地满足此要求。 让我们看看定义此日历示例的 XAML:
<StackPanel TextElement.FontSize="14" Background="White">
<UniformGrid Columns="7" Rows="1">
<UniformGrid.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Height" Value="35" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="Padding" Value="0,5,0,0" />
</Style>
</UniformGrid.Resources>
<TextBlock Text="Mon" />
<TextBlock Text="Tue" />
<TextBlock Text="Wed" />
<TextBlock Text="Thu" />
<TextBlock Text="Fri" />
<TextBlock Text="Sat" />
<TextBlock Text="Sun" />
</UniformGrid>
<ItemsControl ItemsSource="{Binding Days}" Background="Black"
Padding="0,0,1,1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="7" FirstColumn="2" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="1,1,0,0"
Background="White">
<TextBlock Text="{Binding}" Height="35"
HorizontalAlignment="Center" Padding="0,7.5,0,0" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
我们从一个 StackPanel 开始,它用于将一个 UniformGrid 面板直接堆叠在 ItemsControl 之上,而 ItemsControl 使用另一个作为其 ItemsPanel,并指定要在控件内使用的字体大小。 顶部的 UniformGrid 面板声明单行七列和一些基本的 TextBlock 样式。 它有七个子 TextBlock 项,用于输出一周中各天的名称。
ItemsControl 元素将其Background 属性设置为Black,以将不在当月的日期涂黑,并将其Padding 设置为使背景看起来像日历右侧和底部的边框。 顶部和左侧边框来自 UniformGrid 面板中的各个单元格。 ItemsControl.ItemsSource 属性是绑定到视图模型中 Days 属性的数据,所以现在让我们看一下:
private List<int> days = Enumerable.Range(1, 31).ToList();
...
public List<int> Days
{
get {
return days; }
set {
days = value; NotifyPropertyChanged(); }
}
请注意使用 Enumerable.Range 方法来填充集合。 它提供了一种简单的方法,可以根据提供的起始和长度输入参数生成连续的整数序列。 作为一种 LINQ 方法,它是使用延迟执行来实现的,并且直到实际访问时才会生成实际值。
第二个 UniformGrid 面板设置为 ItemsControl.ItemsPanel,仅指定它应具有七列,但保留根据数据绑定项的数量计算的行数。 另请注意,我们已将值 2 硬编码到 FirstColumn 属性,但在适当的控件中,我们通常会将相关月份的值数据绑定到它。
最后,我们使用 DataTemplate 来定义日历上的每一天应该是什么样子。 请注意,在此示例中,我们不需要为其 DataType 属性指定值,因为我们将数据绑定到整个数据源对象,在本例中只是一个整数。 现在让我们继续研究 WrapPanel 面板。
WrapPanel
WrapPanel 面板与 StackPanel 类似,只不过它默认会在两个方向上堆叠其子项。 它首先水平布局子项目,当第一行空间不足时,它会自动将下一个项目包装到新行上,并继续布局剩余的控件。 它使用所需数量的行重复此过程,直到呈现所有项目。
但是,它还提供了像 StackPanel 一样的 Orientation 属性,这将影响其布局行为。 如果“方向”属性从默认值“水平”更改为“垂直”,则面板的子项将从上到下垂直布局,直到第一列中没有更多空间。 然后,这些项目将换行到下一列,并以这种方式继续,直到所有项目都已呈现。
该面板还声明了 ItemHeight 和 ItemWidth 属性,使其能够限制项目的尺寸并产生类似于 UniformGrid 面板的布局行为。 请注意,这些值实际上不会调整每个子项的大小,而只是限制面板中为它们提供的可用空间。 让我们看一个例子:
<WrapPanel ItemHeight="50" Width="150" TextElement.FontSize="14">
<WrapPanel.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Width" Value="50" />
</Style>
</WrapPanel.Resources>
<Button Content="7" />
<Button Content="8" />
<Button Content="9" />
<Button Content="4" />
<Button Content="5" />
<Button Content="6" />
<Button Content="1" />
<Button Content="2" />
<Button Content="3" />
<Button Content="0" Width="100" />
<Button Content="." />
</WrapPanel>
请注意,虽然与 UniformGrid 面板的输出类似,但实际上无法使用该面板实现此示例的输出,因为其中一个子项的大小与其他项的大小不同。 让我们看看这个例子的视觉输出:
我们首先声明 WrapPanel 并指定每个子项的高度应仅为 50 像素,而面板本身的宽度应为 150 像素。 在“资源”部分中,我们将每个按钮的宽度设置为 50 像素宽,因此在将项目包装到下一行之前,可以使每行上的三个按钮彼此相邻。
接下来,我们简单地定义组成面板子项的十一个按钮,指定零按钮的宽度应是其他按钮的两倍。 请注意,如果我们将 ItemWidth 属性以及 ItemHeight 属性设置为 50 像素,则此操作将不起作用。 在这种情况下,我们会看到一半的零按钮,另一半被句点按钮覆盖,以及句点按钮当前所在的空白区域。
提供自定义布局行为
当内置面板的布局行为不能满足我们的要求时,我们可以轻松地定义具有自定义布局行为的新面板。 我们需要做的就是声明一个扩展 Panel 类的类并重写其 MeasureOverride 和 ArrangeOverride 方法。
在 MeasureOverride 方法中,我们只需对 Children 集合中的每个子项调用 Measure 方法,并传入设置为 double.PositiveInfinity 的 Size 元素。 这相当于对每个子项“设置 DesriredSize 属性,就好像您拥有可能需要的所有空间”。
在 ArrangeOverride 方法中,我们使用每个子项新确定的 DesriredSize 属性值来计算其所需的位置,并调用其 Arrange 方法将其渲染在该位置。 让我们看一个自定义面板的示例,该面板将其项目均匀地围绕圆的圆周放置:
using System;
using System.Windows;
using System.Windows.Controls;
namespace CompanyName.ApplicationName.Views.Panels
{
public class CircumferencePanel : Panel
{
public Thickness Padding {
get; set; }
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement element in Children)
{
element.Measure(
new Size(double.PositiveInfinity, double.PositiveInfinity));
}
return availableSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
if (Children.Count == 0) return finalSize;
double currentAngle = 90 * (Math.PI / 180);
double radiansPerElement = (360 / Children.Count) * (Math.PI / 180.0);
double