BoxPanel,一个自定义面板示例

了解如何为自定义 Panel 类编写代码,实现 ArrangeOverrideMeasureOverride 方法,以及使用 Children 属性。

重要 API面板ArrangeOverrideMeasureOverride

示例代码显示了自定义面板实现,但我们没有花费大量时间来解释影响如何为不同布局方案自定义面板的布局概念。 若要详细了解这些布局概念及其应用于特定布局方案的方式,请参阅 XAML 自定义面板概述

面板 是一个对象,在 XAML 布局系统运行并呈现应用 UI 时,它为包含的子元素提供布局行为。 通过从 Panel 类派生自定义类,您可以为 XAML 布局定义自定义面板。 通过重写 ArrangeOverride 方法和 MeasureOverride 方法,为面板提供行为逻辑,以便对子元素进行度量和排列。 此示例派生自 面板。 从 面板开始时,ArrangeOverrideMeasureOverride 方法没有初始行为。 你的代码提供一个网关,子元素通过该网关了解 XAML 布局系统并在 UI 中呈现。 因此,确保您的代码考虑到所有子元素并遵循布局系统所期望的模式是非常重要的。

您的布局情景

定义自定义面板时,将定义布局方案。

布局方案通过以下方式表达:

  • 当面板具有子元素时,面板将执行的操作
  • 当面板空间受到约束时
  • 面板的逻辑是如何决定所有测量、布置、定位和大小,从而最终形成子级呈现的 UI 布局。

考虑到这一点,此处显示的 BoxPanel 适用于特定方案。 为了在此示例中保持代码的首位,我们尚未详细解释方案,而是专注于所需的步骤和编码模式。 如果首先想要了解有关方案的详细信息,请跳到 “适用于 BoxPanel的方案”,然后返回到代码。

首先从 面板 派生

首先从 Panel中派生自定义类。 执行此操作最简单的方法是为这个类定义一个单独的代码文件。为此,可以在 Microsoft Visual Studio 的解决方案资源管理器 中,通过项目的 上下文菜单选项,使用"添加新项"类。 将类(和文件)命名为 BoxPanel

类的模板文件不会以许多使用 语句的 开始,因为它并不是专门针对 Windows 应用。 因此,首先,使用 语句添加 。 模板文件还以几个 开头,使用你可能不需要的 语句,并且可以删除。 以下是一个建议的 列表,使用 语句来确定您在编写典型自定义面板代码时所需的类型:

using System;
using System.Collections.Generic; // if you need to cast IEnumerable for iteration, or define your own collection properties
using Windows.Foundation; // Point, Size, and Rect
using Windows.UI.Xaml; // DependencyObject, UIElement, and FrameworkElement
using Windows.UI.Xaml.Controls; // Panel
using Windows.UI.Xaml.Media; // if you need Brushes or other utilities

现在可以解析 面板,使其成为 BoxPanel的基类。 此外,公开 BoxPanel

public class BoxPanel : Panel
{
}

在类级别,定义一些 int 值,这些值将由多个逻辑函数共享,但不需要公开为公共 API。 在此示例中,这些名称为:maxrcrowcountcolcountcellwidthcellheightmaxcellheightaspectratio

完成此操作后,完整的代码文件如下所示(使用删除有关 的注释,现在你知道我们为什么拥有它们):

using System;
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

public class BoxPanel : Panel 
{
    int maxrc, rowcount, colcount;
    double cellwidth, cellheight, maxcellheight, aspectratio;
}

从此处开始,我们将一次向你显示一个成员定义,即方法重写或支持某些内容,例如依赖属性。 可以按任意顺序将这些内容添加到上述框架中。

测量重写

protected override Size MeasureOverride(Size availableSize)
{
    // Determine the square that can contain this number of items.
    maxrc = (int)Math.Ceiling(Math.Sqrt(Children.Count));
    // Get an aspect ratio from availableSize, decides whether to trim row or column.
    aspectratio = availableSize.Width / availableSize.Height;

    // Now trim this square down to a rect, many times an entire row or column can be omitted.
    if (aspectratio > 1)
    {
        rowcount = maxrc;
        colcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc;
    } 
    else 
    {
        rowcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc;
        colcount = maxrc;
    }

    // Now that we have a column count, divide available horizontal, that's our cell width.
    cellwidth = (int)Math.Floor(availableSize.Width / colcount);
    // Next get a cell height, same logic of dividing available vertical by rowcount.
    cellheight = Double.IsInfinity(availableSize.Height) ? Double.PositiveInfinity : availableSize.Height / rowcount;
           
    foreach (UIElement child in Children)
    {
        child.Measure(new Size(cellwidth, cellheight));
        maxcellheight = (child.DesiredSize.Height > maxcellheight) ? child.DesiredSize.Height : maxcellheight;
    }
    return LimitUnboundedSize(availableSize);
}

MeasureOverride 实现的必要模式是循环访问 Panel.Children中的每个元素。 始终对每个元素调用 Measure 方法。 度量值 具有 大小类型的参数。 此处要传递的内容是面板承诺可用于该特定子元素的大小。 因此,在可以执行循环并开始调用 Measure之前,需要知道每个单元可以投入多少空间。 从 MeasureOverride 方法本身中,你可以得到 availableSize 值。 面板的父级在调用 Measure时所用的大小,是触发此次 MeasureOverride 调用的原因。 因此,典型的逻辑是设计一种方案,即每个子元素将面板的整体可用空间 划分为。 然后,将每个大小除法传递给每个子元素 度量值

BoxPanel 的尺寸划分相当简单:它将空间分为多个框,这些框的数量在很大程度上由物品数量决定。 框的大小取决于行数、列数以及可用大小。 有时正方形面板中不需要某一行或某一列,因此它被去掉,面板就会变成长宽比不是正方形而是矩形的形状。 有关得出此逻辑的更多信息,请跳到“BoxPanel 的方案”

那么,措施通过的作用是什么? 它为每个调用 Measure 的元素的只读 DesiredSize 属性设置了一个值。 一旦到达排列阶段,DesiredSize 值就显得尤为重要,因为 DesiredSize 表示在排列和最终呈现时大小可以或应当是什么。 即使没有在自己的逻辑中使用 DesiredSize,系统仍需要它。

availableSize 的高度组件没有限制时,可以使用此面板。 如果这是真的,则面板没有要划分的已知高度。 在这种情况下,度量值传递的逻辑会告知每个子级它还没有边界的高度。 为此,它通过将 尺寸 传递给 测量调用,其中子项的 尺寸.高度 是无限大的。 这是合法的。 在调用 Measure 时,逻辑是将 DesiredSize 设置为以下值中的最小值:传递给 Measure的参数,或该元素的自然大小,这由一些因素决定,例如显式设置的 高度宽度

注释

StackPanel 的内部逻辑也具有此行为:StackPanel 向子级 测量 传递无限维度值,意味着在方向维度上对子级没有约束。 StackPanel 通常动态调整自身大小,以适应在该维度中增长的堆栈中的所有子级。

但是,面板本身无法从 MeasureOverride返回一个具有无限值的 大小;这会在布局期间引发异常。 因此,逻辑的一部分是确定任意子元素请求的最大高度,并使用该高度作为单元格的高度,前提是该高度不是已经来自面板自身的大小限制。 下面是在前面代码中引用的帮助程序函数 LimitUnboundedSize,该函数采用最大单元格高度,并使用它为面板提供一个有限的返回高度,同时确保在启动排列处理之前,cellheight 是一个有限数。

// This method limits the panel height when no limit is imposed by the panel's parent.
// That can happen to height if the panel is close to the root of main app window.
// In this case, base the height of a cell on the max height from desired size
// and base the height of the panel on that number times the #rows.
Size LimitUnboundedSize(Size input)
{
    if (Double.IsInfinity(input.Height))
    {
        input.Height = maxcellheight * colcount;
        cellheight = maxcellheight;
    }
    return input;
}

排列覆盖

protected override Size ArrangeOverride(Size finalSize)
{
     int count = 1;
     double x, y;
     foreach (UIElement child in Children)
     {
          x = (count - 1) % colcount * cellwidth;
          y = ((int)(count - 1) / colcount) * cellheight;
          Point anchorPoint = new Point(x, y);
          child.Arrange(new Rect(anchorPoint, child.DesiredSize));
          count++;
     }
     return finalSize;
}

ArrangeOverride 实现的必要模式是循环访问 Panel.Children中的每个元素。 始终在每个元素上调用 Arrange 方法。

请注意,MeasureOverride中没有那么多的计算;这是典型的。 子级的大小已从面板自己的 MeasureOverride 逻辑中或从度量值传递期间每个子集的 DesiredSize 值中已知。 但是,我们仍需要确定每个子元素将出现在面板中的位置。 在典型的面板中,每个子组件应在不同的位置显示。 对于典型场景,创建重叠元素的面板并不理想(不过,如果你的预期方案确实是这样,创建带有目的性重叠的面板也是可以的)。

此面板按行和列的概念排列。 已计算行数和列数(这是测量所必需的)。 因此,现在行和列的形状加上每个单元格的已知大小都有助于定义此面板包含的每个元素的呈现位置(anchorPoint) 的逻辑。 该 以及从度量中已知的 大小,被用作构造 Rect的两个组件。 Rect排列的输入类型。

面板有时需要剪辑其内容。 如果这样做,剪裁尺寸是当前在 DesiredSize中的尺寸,因为 Measure 逻辑将其设置为传递给 Measure的最小值,或由其他自然尺寸因素决定。 因此,通常不需要在 排列期间专门检查剪辑;剪辑只是基于将 DesiredSize 传递给每个 Arrange 调用而发生的。

如果定义呈现位置所需的所有信息都以其他方式已知,则执行循环时,您并不总是需要计数。 例如,在 Canvas 布局逻辑中,Children 集合中的位置并不重要。 通过读取 Canvas.Left 和作为排列逻辑一部分的子元素 Canvas.Top 值,可以了解在 Canvas 中放置每个元素所需的所有信息。 BoxPanel 逻辑恰好需要一个计数来与 colcount 进行比较,以便知道何时开始新行并偏移 y 值。

通常,输入的 finalSize 和从 ArrangeOverride 实现中返回的 尺寸 是相同的。 有关原因的详细信息,请参阅 XAML 自定义面板概述的“ArrangeOverride”部分。

优化:控制行计数与列计数

可以像现在一样编译和使用此面板。 但是,我们将再添加一个优化。 在刚刚显示的代码中,逻辑将额外的行或列放在纵横比最长的一侧。 但是,为了更好地控制单元格形状,即使面板的纵横比是“纵向”,也最好选择 4x3 而不是 3x4 的单元格集合。因此,我们将添加一个可选的依赖属性,供面板使用者配置以控制这种行为。 下面是依赖项属性定义,这是非常基本的:

// Property
public Orientation Orientation
{
    get { return (Orientation)GetValue(OrientationProperty); }
    set { SetValue(OrientationProperty, value); }
}

// Dependency Property Registration
public static readonly DependencyProperty OrientationProperty =
        DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(BoxPanel), new PropertyMetadata(null, OnOrientationChanged));

// Changed callback so we invalidate our layout when the property changes.
private static void OnOrientationChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
    if (dependencyObject is BoxPanel panel)
    {
        panel.InvalidateMeasure();
    }
}

下面是使用 Orientation 如何影响 MeasureOverride中的度量值逻辑。 实际上,它所做的就是改变 rowcountcolcount 派生自 maxrc 和真实纵横比的方式,因此每个单元格都有相应的大小差异。 当 Orientation垂直(默认)时,它会在将真实纵横比用于“纵向”矩形布局的行和列计数之前反转其值。

// Get an aspect ratio from availableSize, decides whether to trim row or column.
aspectratio = availableSize.Width / availableSize.Height;

// Transpose aspect ratio based on Orientation property.
if (Orientation == Orientation.Vertical) { aspectratio = 1 / aspectratio; }

BoxPanel 的情境

BoxPanel 的特定方案是,它是一个面板,其中如何划分空间的主要决定因素之一是了解子项的数量,并划分面板的已知可用空间。 面板本质上是矩形形状。 许多面板通过将该矩形空间划分为进一步矩形来操作;这就是网格 对其单元格执行的操作。 在 Grid中,单元格的大小由 ColumnDefinitionRowDefinition 值决定,元素通过 Grid.RowGrid.Column 附加属性来指定它们所处的确切单元格。 要从网格 中获得良好的布局,通常需要事先了解子元素的数量,以确保有足够的单元格,并且每个子元素能够设置其附加属性,使其适合所在的单元格。

但是,如果子级数量是动态的呢? 这当然可能:你的应用代码可以将项添加到集合,以响应你认为足够重要的动态运行时条件,以便值得更新 UI。 如果您使用数据绑定与基础集合或业务对象进行连接,那么接收这样的更新和更新UI都会被自动处理,因此这通常是首选的方法(详见 数据绑定深入解析)。

但并非所有应用方案都适合数据绑定。 有时,需要在运行时创建新的 UI 元素并使其可见。 BoxPanel 适用于此方案。 子项数量的变化对 BoxPanel 来说没有问题,因为它在计算中使用子项数量,并将现有和新增加的子元素调整到一个新的布局中,以便它们都能够适应。

进一步扩展 BoxPanel 的高级方案(此处未显示)可以容纳动态子级,并使用子级的 DesiredSize 作为单个单元格大小调整的更强因素。 此方案可能使用不同的行或列大小或非网格形状,以便“浪费”的空间更少。 这需要确定一种策略,使得各种大小和纵横比的多个矩形可以在一个容纳它们的矩形中完美契合,以兼顾美观和尺寸最小化。 BoxPanel 不这样做;它使用更简单的技术来划分空间。 BoxPanel的方法是确定大于子计数的最小的平方数。 例如,9 个物品可以排列在一个 3x3 的正方形中。 10 个项目需要一个 4x4 的正方形。 即使去掉起始方块的一行或一列,也通常可以放置项目,以节省空间。 在 count=10 的示例中,它适合放入 4x3 或 3x4 的矩形中。

你可能想知道为什么小组没有选择5x2来安排10个项目,因为那样能更好地排列项目数量。 但是,在实践中,面板通常是矩形,并且通常没有明显的长宽比。 最小二乘法是一种调整大小逻辑的方法,使其更适合处理典型的布局形状,并避免在单元格形状变得不规则时进行调整。

引用

概念