XAML 自定义面板概述

面板是一个对象,它为它包含的子元素提供布局行为,当可扩展应用程序标记语言(XAML)布局系统运行并呈现应用 UI 时。

重要 API面板ArrangeOverrideMeasureOverride

通过从 Panel 类派生自定义类,您可以为 XAML 布局定义自定义面板。 通过重写 MeasureOverrideArrangeOverride,您可以为面板提供度量和排列子元素的逻辑,从而实现面板的行为。

面板 基类

若要定义自定义面板类,可以直接从 Panel 类派生,也可以派生自未密封的实用面板类之一,例如 GridStackPanel。 从 面板派生更容易,因为很难绕过已具有布局行为的面板的现有布局逻辑。 此外,具有行为的面板可能具有与面板布局功能无关的现有属性。

Panel 中,自定义面板继承以下 API:

  • Children 属性。
  • BackgroundChildrenTransitionsIsItemsHost 属性,以及依赖属性标识符。 这些属性都不是虚拟属性,因此通常不会重写或替换它们。 对于自定义面板方案,通常不需要使用这些属性,甚至无需读取其值。
  • 布局重写方法 MeasureOverrideArrangeOverride。 这些最初由 FrameworkElement 定义。 基类 Panel 并不重写这些,但像 Grid 这样的实用面板确实有重写实现,这些实现为系统执行的原生代码。 为 ArrangeOverrideMeasureOverride 提供新的(或增量的)实现是定义自定义面板所需的大部分工作。
  • 所有其他 API,例如 FrameworkElementUIElementDependencyObject的 API,如 HeightVisibility 等。 有时您会在布局重写中引用这些属性的值,但由于它们不是虚拟的,您通常不会重写或替换它们。

此处的重点是介绍 XAML 布局概念,以便你能考虑自定义面板在布局中应有和可能的行为方式。 如果想要直接进入并查看示例自定义面板实现,请参阅 BoxPanel(一个示例自定义面板)。

Children 属性

Children 属性与自定义面板密切相关,因为所有从 Panel 派生的类都使用该 Children 属性来存储它们包含的子元素于集合中。 被指定为 Panel 类的 XAML 内容属性,并且派生自 Panel 的所有类都可以继承 XAML 内容属性行为。 如果某个属性被指定为 XAML 的内容属性,这意味着在 XAML 标记中指定该属性时可以省略属性元素,并将这些值设置为直接标记的子项(“内容”)。 例如,如果从 Panel 派生一个不定义新行为的类 CustomPanel,则仍然可以使用此标记:

<local:CustomPanel>
  <Button Name="button1"/>
  <Button Name="button2"/>
</local:CustomPanel>

当 XAML 分析程序读取此标记时,已知 Children 是所有 Panel 派生类型的 XAML 内容属性,因此分析程序会将两个 Button 元素添加到 Children 属性的 UIElementCollection 值。 XAML 内容属性有助于简化 UI 定义的 XAML 标记中的父子关系。 有关 XAML 内容属性以及如何在分析 XAML 时填充集合属性的详细信息,请参阅 XAML 语法指南

维护 Children 属性值的集合类型是 UIElementCollection 类。 UIElementCollection 是一个强类型集合,它使用 UIElement 作为其强制项类型。 UIElement 是一种基类型,数百种实用的 UI 元素类型都继承自它,因此这里的类型约束故意被设置得比较宽松。 但它确实强制你不能将 画笔 作为 面板的直接子级,并且通常意味着只有预期在 UI 中可见并参与布局的元素才能在 面板中找到子元素。

通常,自定义面板只需使用 Children 属性 as-is的特征,即可接受 XAML 定义的任何 UIElement 子元素。 作为高级方案,当在布局重写中循环访问集合时,可以支持对子元素进行进一步的类型检查。

除了在重写中循环访问 Children 集合外,面板逻辑也可能受到 Children.Count的影响。 你可能有一些逻辑,根据项数至少部分地分配空间,而不是根据所需的大小和单个项的其他特征。

重写布局方法

布局重写方法(MeasureOverrideArrangeOverride)的基本模型是,它们应循环访问所有子元素并调用每个子元素的特定布局方法。 当 XAML 布局系统设置根窗口的视觉时,第一个布局周期开始。 由于每个父元素会在其子元素上调用布局,因此这会将对布局方法的调用传播到所有应在布局中的可能的 UI 元素。 在 XAML 布局中,有两个阶段:度量,然后排列。

没有从基 Panel 类获取 MeasureOverrideArrangeOverride 的任何内置布局方法行为。 中的项不会自动呈现为 XAML 可视化树的一部分。 您需要在您的 MeasureOverrideArrangeOverride 实现中,通过布局传递对 Children 中找到的每个项调用布局方法,以使布局过程得知这些项。

除非存在自己的继承关系,否则没有理由在布局重写中调用基础实现。 无论如何,针对布局行为的本机方法(如果存在)都会运行,不从重写中调用基本实现不会阻止本机行为的发生。

在度量值传递期间,布局逻辑通过针对该子元素调用 Measure 方法来查询每个子元素的所需大小。 调用 Measure 方法将建立 DesiredSize 属性的值。 MeasureOverride 的返回值是面板本身所需的大小。

在排列阶段,子元素的位置和大小在 x-y 空间中被确定,布局已经准备好进行渲染。 代码必须在 Children 中的每个子元素上调用 Arrange,以便布局系统检测到该元素属于布局。 Arrange 调用是组合和渲染过程的前置步骤, 它通知布局系统在将组合提交进行渲染时,该元素应该放置的位置。

许多属性和值都有助于布局逻辑在运行时的工作方式。 考虑布局过程的一种方法是,没有子元素的元素(通常是 UI 中最深层嵌套的元素)是可以首先完成度量的元素。 它们与影响其理想大小的子元素没有依赖关系。 它们可能有自己想要的大小,而这些是建议的大小,在布局实际发生之前。 然后,测量过程会继续向上遍历可视化树,直到根元素完成测量,并且所有测量值都可以最终确定。

候选布局必须适合当前应用窗口,否则 UI 的其他部分将被剪裁。 面板通常是决定剪辑逻辑的地方。 面板逻辑可以确定可从 MeasureOverride 实现中获取的大小,并且可能需要将大小限制推送到子级,并在子级之间划分空间,以便一切尽可能适合。 在理想情况下,布局的结果应当利用布局各部分的多种属性,同时仍然适应应用窗口。 这既需要面板布局逻辑的良好实现,还需要在任何使用该面板生成 UI 的应用代码的一部分进行明智的 UI 设计。 如果整个 UI 设计中包含的子元素数量超出了应用程序所能容纳的范围,那么任何面板设计都不会看起来好。

布局系统工作的很大一部分是,任何基于 FrameworkElement 的元素 在充当容器中的子级时,已经有一些自己的固有行为。 例如,有几种 FrameworkElement API,要么用于通知布局行为,要么是布局正常运行所必需的。 这些包括:

测量重写

MeasureOverride 方法的返回值由布局系统用作面板本身的起始 DesiredSize,当布局中的父级对面板调用 Measure 方法时。 方法中的逻辑选择与返回的内容一样重要,逻辑通常影响返回的值。

所有 MeasureOverride 实现都应循环访问 Children,并在每个子元素上调用 Measure 方法。 调用 Measure 方法将建立 DesiredSize 属性的值。 这可能告知面板本身需要多少空间,以及该空间如何划分在元素之间或为特定子元素调整大小。

下面是 MeasureOverride 方法的一个非常基本结构:

protected override Size MeasureOverride(Size availableSize)
{
    Size returnSize; //TODO might return availableSize, might do something else
     
    //loop through each Child, call Measure on each
    foreach (UIElement child in Children)
    {
        child.Measure(new Size()); // TODO determine how much space the panel allots for this child, that's what you pass to Measure
        Size childDesiredSize = child.DesiredSize; //TODO determine how the returned Size is influenced by each child's DesiredSize
        //TODO, logic if passed-in Size and net DesiredSize are different, does that matter?
    }
    return returnSize;
}

元素在准备好布局时通常具有自然大小。 度量值通过后,DesiredSize 可能会指示自然大小(如果为 Measure 传递的 availableSize 较小)。 如果自然大小大于 availableSize (您为 Measure提供的值),则 DesiredSize 将被限制为 availableSize。 这就是 度量值的内部实现的行为方式,布局替代应考虑到该行为。

某些元素没有自然大小,因为它们具有高度宽度“自动”值。 这些元素使用整个 availableSize,因为这就是 Auto 值所代表的:将元素调整为最大可用大小,而直接布局父元素通过调用 Measure 来传达 availableSize。 实际上,总会有某些度量标准来确定 UI 的大小(即使是在顶层窗口处)。最终,度量过程会将所有 自动 值解析为父约束,并且所有 自动 元素都将获得真实尺寸(在布局完成后,可以通过检查 ActualWidthActualHeight获取)。

将大小传递给具有至少一个无限维度 度量值 是合法的,以指示面板可以尝试调整自身大小以适应其内容的度量值。 要测量的每个子元素使用其自然大小设置其 DesiredSize 值。 然后,在排版过程中,面板通常使用该大小进行布置。

文本元素(如 TextBlock)的 ActualWidthActualHeight 是根据文本字符串和文本属性计算出来的,即使未设置任何 高度宽度 值,这些尺寸也应被面板逻辑遵循。 剪辑文本是一种特别糟糕的 UI 体验。

即使实现不使用所需的大小度量值,最好在每个子元素上调用 Measure 方法,因为调用 Measure 触发的内部和本机行为。 要使元素参与布局,每个子元素必须在测量过程中对它调用 Measure,并在排列过程中调用 Arrange 方法。 调用这些方法会在对象上设置内部标志,并填充系统在生成可视化树并呈现 UI 时所需的值(例如 DesiredSize 属性)。

MeasureOverride 返回值基于面板的逻辑,该逻辑解释 DesiredSize子元素 中每个子元素的大小注意事项(Measure 调用它们时)。 DesiredSize 子级值以及 MeasureOverride 返回值应如何使用它们,这要由你自己的逻辑解释决定。 通常情况下,你不会在不作修改的情况下直接相加这些值,因为 MeasureOverride 的输入通常是面板父级所建议的一个固定的可用大小。 如果超出该大小,面板本身可能会被剪裁。 通常会将所有子元素的总大小与面板的可用大小进行比较,并在必要时进行调整。

提示和指南

  • 理想情况下,自定义面板应该适合在 UI 结构中成为第一个关键视觉元素,可能紧接在 页面之下,用户控件 或其他作为 XAML 页面根元素的元素之下。 在 MeasureOverride 的实现中,在不检查值的情况下,不要例行返回输入的 大小。 如果返回的 大小 包含一个 无穷大 值,则可能会在运行时的布局逻辑中引发异常。 Infinity 值可以来自主应用窗口,由于该窗口是可滚动的,因此没有最大高度。 其他可滚动内容的行为可能相同。
  • MeasureOverride 实现的另一个常见错误是返回新的默认 大小(高度和宽度的值为 0)。 你可以从那个值开始,并且如果您的面板决定不应该渲染任何子元素,这甚至可能是正确的值。 但是,默认 大小 会导致面板无法由主机正确调整大小。 它请求 UI 中没有空间,因此不会获取任何空间,也不会呈现。 虽然您所有的面板代码可能正常运行,但如果面板的高度和宽度都为零,您仍然看不到面板或其内容。
  • 在重写中,请务必避免将子元素轻易强制转换为 FrameworkElement,而应使用根据布局计算得出的属性,特别是 ActualWidthActualHeight。 对于最常见的场景,可以将逻辑基于子元素的 DesiredSize 值,并不需要任何 HeightWidth 与子元素相关的属性。 对于特殊情况,如果知道元素的类型并具有其他信息(例如图像文件的自然大小),则可以使用元素的专用信息,因为它不是布局系统主动更改的值。 将布局计算属性作为布局逻辑的一部分大大增加了定义无意布局循环的风险。 这些循环会导致无法创建有效布局的条件,如果循环不可恢复,系统可能会引发 LayoutCycleException
  • 面板通常在多个子元素之间划分其可用空间,尽管空间的划分方式各不相同。 例如, Grid 实现布局逻辑,该逻辑使用 其 RowDefinitionColumnDefinition 值将空间划分为 Grid 单元格,同时支持星号大小和像素值。 如果它们是像素值,那么每个子级的可用大小已经被知道,因此这将作为网格样式 度量的输入大小传递。
  • 面板本身可以引入用于在项之间填充的保留空间。 如果执行此操作,请确保将度量作为一个独立的属性公开,与 Margin 或任何 Padding 属性区分开来。
  • 元素可能具有其 ActualWidthActualHeight 属性的值,这些值基于之前的布局处理。 如果值发生更改,应用程序的 UI 代码可以在元素上设置 LayoutUpdated 处理程序,以便在有特殊逻辑需要运行时使用,但面板逻辑通常不需要通过事件处理来检测更改。 布局系统已经在属性值变更时自动决定何时重新执行布局,并在适当情况下自动调用面板的 MeasureOverrideArrangeOverride

ArrangeOverride

ArrangeOverride 方法具有一个 Size 返回值,当布局中的父级在面板上调用 Arrange 方法时,布局系统在渲染面板本身时使用该值。 通常,输入的 finalSizeArrangeOverride 返回的 Size 是相同的。 如果不是这样,这意味着面板正在尝试将自身大小调整为与布局中其他参与者所声明的可用大小不同。 最终大小是基于先前通过面板代码运行布局的测量步骤,因此返回不同的大小并不常见:这意味着你故意忽略测量逻辑。

不要返回具有 无穷大 组件的 大小。 尝试使用这样的 尺寸 会触发内部布局的异常。

所有 ArrangeOverride 实现应遍历 Children,并在每个子元素上调用 Arrange 方法。 与 度量值一样,排列 没有返回值。 与 Measure 不同,不会将计算属性设置为结果(但是,相关元素通常会触发 LayoutUpdated 事件)。

下面是 ArrangeOverride 方法的一个非常基本的框架:

protected override Size ArrangeOverride(Size finalSize)
{
    //loop through each Child, call Arrange on each
    foreach (UIElement child in Children)
    {
        Point anchorPoint = new Point(); //TODO more logic for topleft corner placement in your panel
       // for this child, and based on finalSize or other internal state of your panel
        child.Arrange(new Rect(anchorPoint, child.DesiredSize)); //OR, set a different Size 
    }
    return finalSize; //OR, return a different Size, but that's rare
}

布局的安排过程可能在没有测量过程的情况下发生。 但是,仅当布局系统确定没有更改任何影响先前度量的属性时,才会发生这种情况。 例如,如果对齐方式发生更改,则无需重新度量该特定元素,因为其 DesiredSize 在对齐选择发生更改时不会更改。 另一方面,如果布局中任何元素的 ActualHeight 发生更改,则需要重新测量。 布局系统会自动检测到实际度量值的更改,并再次调用度量步骤,然后运行另一个安排步骤。

Arrange 的输入采用 Rect 值。 构造此 Rect 的最常见方法是使用具有 Point 输入和 Size 输入的构造函数。 是应放置元素边界框左上角的点。 Size 是用于呈现该特定元素的维度。 通常使用该元素的 DesiredSize 作为此 大小 值,因为为布局中涉及的所有元素建立 DesiredSize 是布局的度量传递的目的。 (测量过程以迭代方式确定元素的整体尺寸,这样在进入排列阶段时,布局系统可以优化元素的放置。)

ArrangeOverride 实现之间通常有所不同的是面板通过何种逻辑来确定如何排列每个子元素的 组件。 绝对定位面板(如 Canvas)使用通过 Canvas.LeftCanvas.Top 值从每个元素获取的显式放置信息。 空格分隔面板(如 Grid )通过数学运算将可用空间划分为单元格,每个单元格都具有一个 x-y 坐标值,用于确定内容放置和排列的位置。 自适应面板(如 StackPanel )可能正在扩展自身,以适应其方向维度中的内容。

除了您直接控制和传递给 排列的内容之外,布局中的元素还有其他额外的定位因素影响。 它们来自 Arrange 的内部本机实现,这适用于所有 FrameworkElement 派生类型,并由其他一些类型(例如文本元素)进行扩充。 例如,元素可以具有边距和对齐方式,有些元素可以有填充。 这些属性通常交互。 有关详细信息,请参阅 对齐、边距和填充

面板和控件

勿将本该做成自定义控件的功能放入自定义面板中。 面板的作用是通过自动布局功能来呈现其中存在的任何子元素内容。 面板可能会向内容添加修饰(类似于 边框 在呈现的元素周围添加边框的方式),或者执行其他与布局相关的调整,如填充。 但是,在扩展可视化树输出时,除了报告和使用子级的信息,不应该进一步扩展。

如果有可供用户访问的任何交互,则应编写自定义控件,而不是面板。 例如,面板不应向呈现的内容添加滚动视区,即使目标是阻止剪辑,因为滚动条、拇指等是交互式控件部件。 (内容毕竟可能有滚动条,但应保留子项的逻辑。不要通过添加滚动作为布局作来强制它。在控件中呈现内容时,可以创建一个控件,并编写一个自定义面板,该面板在该控件的可视化树中扮演重要角色。 但是控件和面板应该是不同的代码对象。

控件和面板之间区分的重要性在于微软 UI 自动化和可访问性功能。 面板提供视觉布局行为,而不是逻辑行为。 UI 元素的视觉外观通常不是辅助功能场景中重要的一个方面。 辅助功能是公开应用的各个部分,这些部分在逻辑上对理解 UI 非常重要。 当需要交互时,控件应向 UI 自动化基础结构公开交互可能性。 有关详细信息,请参阅 自定义自动化伙伴

其他布局 API

有一些其他的 API 是布局系统的一部分,但它们未在 面板中声明。 可以在面板实现或使用面板的自定义控件中使用这些内容。

  • UpdateLayoutInvalidateMeasure以及 InvalidateArrange 是启动布局过程的方法。 InvalidateArrange 可能不会触发测量过程,但另外两个会。 请勿在布局方法重写中调用这些特定方法,因为它们几乎肯定会导致布局循环。 控制代码通常不需要调用它们。 通过检测对框架定义的布局属性(如 Width 等)的更改,布局的大多数方面会自动触发。
  • LayoutUpdated 是元素布局的某些方面发生更改时触发的事件。 这不特定于面板;事件由 FrameworkElement 定义。
  • SizeChanged 是一个事件,仅在布局过程完成后才会触发,并表示由于该事件,ActualHeightActualWidth 已发生更改。 这是另一个 FrameworkElement 事件。 在某些情况下,LayoutUpdated 会被触发,但 SizeChanged 不会被触发。 例如,内部内容可能重新排列,但元素的大小没有更改。

引用

概念