XAML 自定义面板概述

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

重要 APIPanelArrangeOverrideMeasureOverride

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

Panel 基类

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

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

  • Children 属性。
  • BackgroundChildrenTransitions IsItemsHost 属性以及依赖属性标识符。 这些属性都不是虚拟属性,因此通常不会重写或替换它们。 对于自定义面板方案,通常不需要这些属性,甚至不需要读取值。
  • 布局替代 MeasureOverride ArrangeOverride 方法。 这些最初由 FrameworkElement 定义。 基 Panel 类不会重写这些,但网格实用面板具有作为本机代码实现的替代实现,并由系统运行。 为 ArrangeOverrideMeasureOverride 提供新的(或累加)实现是定义自定义面板所需的大部分工作。
  • FrameworkElement、UIElementDependencyObject 的其他所有 API,例如 HeightVisibility 等。 有时在布局重写中引用这些属性的值,但它们不是虚拟的,因此通常不会重写或替换它们。

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

Children 属性

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

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

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

维护 Children 属性值的集合类型是 UIElementCollection 类。 UIElementCollection 是一个强类型集合,它使用 UIElement 作为其强制项类型。 UIElement 是基本类型,此类型由数百个实际 UI 元素类型继承,因此此处的类型强制故意较为宽松。 但是它强制你无法拥有作为 Panel 的直接子元素的 Brush,并且通常表示仅预期在 UI 中可见且参与布局的元素将被视为 Panel 中的子元素。

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

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

重写布局方法

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

你无法从基 Panel 类获取 MeasureOverrideArrangeOverride 的任何内置布局方法行为。 Children 中的项不会自动呈现为 XAML 可视化树的一部分。 通过通过 MeasureOverride 和 ArrangeOverride 实现中的布局传递,调用在 Children 中找到的每个项的布局方法,使布局过程知道这些项。

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

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

在排列传递期间,子元素的位置和大小在 x-y 空间中确定,布局组合已准备好呈现。 代码必须在 Children 中的每个子元素上调用 Arrange,以便布局系统检测到该元素属于布局。 Arrange 调用是合成和呈现的前身;它会通知布局系统,当提交该元素进行呈现时,该元素将转到何处。

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

候选布局必须适合当前应用窗口,否则 UI 的其他部分将被剪裁。 面板通常是确定剪辑逻辑的位置。 面板逻辑可以确定 MeasureOverride 实现中可用的大小,并且可能需要将大小限制推送到子级,并在子级之间划分空间,以便一切尽可能适合。 布局的结果理想情况下是使用布局的所有部分的各种属性,但仍适合应用窗口。 这既需要面板布局逻辑的良好实现,还需要在任何使用该面板生成 UI 的应用代码的一部分进行明智的 UI 设计。 如果整个 UI 设计包含的子元素多于应用中可能适合的子元素,则任何面板设计都看起来不好。

布局系统工作的很大一部分是,任何基于 FrameworkElement 的元素在充当容器中的子级时,都已具有自身的一些固有行为。 例如,FrameworkElement 有几个 API 通知布局行为,或者需要让布局完全正常工作。 这些设置包括:

MeasureOverride

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 较小)。 如果自然大小大于为 Measure 传递的 availableSize,则 DesiredSize 将限制为 availableSize 这就是 Measure 的内部实现的行为方式,布局重写应考虑到该行为。

某些元素没有自然大小,因为它们具有高度宽度“自动”值。 这些元素使用完整的 availableSize,因为这是 自动 值所代表的内容:将元素大小调整为最大可用大小,即即时布局父级通过调用 Measure with availableSize 进行通信。 实际上,UI 的大小始终是一些度量值(即使这是顶级窗口)。最终,度量值传递将所有自动值解析为父约束,所有自动值元素都获得实际度量值(在布局完成后,可以通过检查 ActualWidth ActualHeight 来获取)。

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

即使未设置高度宽度值,TextBlock文本元素也基于其文本字符串和文本属性计算了 ActualWidth ActualHeight,并且这些维度应受面板逻辑的尊重。 剪辑文本是一种特别糟糕的 UI 体验。

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

MeasureOverride 返回值基于面板的逻辑,在调用 Measure解释 Children每个子元素的 DesiredSize 或其他大小注意事项。 如何处理子级的 DesiredSize 值,以及 MeasureOverride 返回值应如何使用它们,这要由你自己的逻辑解释决定。 通常无需修改即可添加值,因为 MeasureOverride输入通常是面板的父级建议的固定可用大小。 如果超出该大小,面板本身可能会被剪裁。 通常会将子级的总大小与面板的可用大小进行比较,并在必要时进行调整。

提示和指南

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

ArrangeOverride

ArrangeOverride 方法具有一个 Size 返回值,在呈现面板本身时,布局系统在呈现面板本身时,当 Layout 中的父级在面板上调用 Arrange 方法。 输入 finalSizeArrangeOverride 返回 的大小 通常相同。 如果不是,这意味着面板试图使自己的大小不同于布局声明中的其他参与者可用的大小。 最终大小基于以前通过面板代码运行布局的度量传递,因此返回不同大小并不是典型的原因:这意味着你故意忽略度量值逻辑。

不要返回具有无穷大组件的 Size 尝试使用此类 大小 会引发内部布局中的异常。

所有 ArrangeOverride 实现都应循环访问 Children,并在每个子元素上调用 Arrange 方法。 与度量值一样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 输入的构造函数。 Point 是应放置元素边界框左上角的点。 Size 是用于呈现该特定元素的维度。 通常使用该元素的 DesiredSize 作为此 Size 值,因为为布局中涉及的所有元素建立 DesiredSize 是布局的度量传递的目的。 (度量传递以迭代方式确定元素的一切大小,以便布局系统可以优化元素在进入排列传递后如何放置元素。

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

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

面板和控件

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

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

控制面板和面板之间的区别一个重要原因是Microsoft UI 自动化和可访问性。 面板提供视觉布局行为,而不是逻辑行为。 UI 元素在视觉上如何显示不是通常对辅助功能方案非常重要的 UI 的一个方面。 辅助功能是公开应用的各个部分,这些部分在逻辑上对理解 UI 非常重要。 当需要交互时,控件应向UI 自动化基础结构公开交互可能性。 有关详细信息,请参阅 自定义自动化对等方

其他布局 API

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

  • UpdateLayoutInvalidateMeasureInvalidateArrange 是启动布局传递的方法。 InvalidateArrange 可能不会触发度量值传递,但其他两个执行操作。 切勿从布局方法重写中调用这些方法,因为它们几乎肯定会引起布局循环。 控制代码通常不需要调用它们。 通过检测对框架定义的布局属性(如 Width 等)的更改,自动触发布局的大多数方面。
  • LayoutUpdated 是在元素布局的某些方面发生变化时引发的事件。 这不特定于面板;事件由 FrameworkElement 定义。
  • SizeChanged 是仅在布局传递完成后引发的事件,它意味着 ActualHeightActualWidth 也已改变。 这是另一个 FrameworkElement 事件。 在某些情况下,LayoutUpdated 会触发,但 SizeChanged 不会触发。 例如,内部内容可能重新排列,但元素的大小没有更改。

引用

概念