BoxPanel,一个自定义面板示例

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

重要 APIPanelArrangeOverrideMeasureOverride

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

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

布局方案

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

布局方案通过:

  • 当面板具有子元素时,面板将执行的操作
  • 当面板对其自己的空间有约束时
  • 面板的逻辑如何确定最终导致子级呈现的 UI 布局的所有度量、放置、位置和大小调整

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

首先从 面板派生

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

类的模板文件不会从多个 using 语句开始,因为它不是专门为 Windows 应用设计的。 因此,首先,添加 using 语句。 模板文件还以你可能不需要的几个 using 语句开头,并且可以删除。 下面是可解析典型自定义面板代码所需的类型的 using 语句的建议列表

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

现在,你可以解析 Panel,使其成为基类。BoxPanel 此外,公开 BoxPanel

public class BoxPanel : Panel
{
}

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

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

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;
}

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

MeasureOverride

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 方法。 度量值具有 Size 类型的参数。 此处要传递的内容是面板承诺可用于该特定子元素的大小。 因此,在可以执行循环并开始调用 Measure 之前,需要知道每个单元格可以投入多少空间。 在 MeasureOverride 方法本身中,你有 availableSize 值。 这是面板的父级在调用 Measure 时使用的大小,这是首先调用此 MeasureOverride 的触发器。 因此,典型的逻辑是设计一种方案,即每个子元素将面板的整体 availableSize 的空间划分。 然后将每个大小除法传递给 每个子元素的度量 值。

BoxPanel如何划分大小相当简单:它将其空间划分为大量框,这些框主要由项数控制。 框的大小基于行和列计数和可用大小。 有时不需要一个正方形中的一行或一列,因此它被丢弃,面板会变成一个矩形,而不是用其行的正方形:列比。 有关如何到达此逻辑的详细信息,请跳到“BoxPanel 的方案”。

那么,度量传递的作用是什么? 它为调用 Measure 的每个元素的只读 DesiredSize 属性设置一个值。 获取 排列传递后,具有 DesiredSize 值可能很重要,因为 DesiredSize 传达在排列和最终呈现时大小可以或应该是什么。 即使没有在自己的逻辑中使用 DesiredSize ,系统仍需要它。

当 availableSize 的高度组件未绑定时,可以使用此面板。 如果这是真的,则面板没有要划分的已知高度。 在这种情况下,度量值传递的逻辑会告知每个子级它还没有边界的高度。 为此,它通过将 Size 传递给 Measure 调用来调用 Size.Height 为无限的子级。 这是合法的。 调用 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;
}

ArrangeOverride

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 值中已知。 但是,我们仍需要确定每个子项将出现在面板中的位置。 在典型的面板中,每个子级应以不同的位置呈现。 对于典型方案,创建重叠元素的面板并不理想(尽管创建具有有目的重叠的面板不是问题,如果确实是预期方案)。

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

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

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

通常,从 ArrangeOverride 实现返回的输入 finalSize Size 相同。 有关原因的详细信息,请参阅 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自真正的纵横比,因此每个单元格都有相应的大小差异。 当 OrientationVertical (默认值),它将反转真实纵横比的值,然后再将其用于“纵向”矩形布局的行和列计数。

// 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 对其单元格执行的操作。 在 Grid 的情况下,单元格的大小由 ColumnDefinitionRowDefinition 值设置,元素声明它们使用 Grid.Row Grid.Column 附加属性进入的确切单元格。 从网格获取良好的布局通常需要事先知道子元素的数量,以便有足够的单元格和每个子元素设置其附加属性以适应其自己的单元格。

但是,如果子级数量是动态的呢? 这当然可能:你的应用代码可以将项添加到集合,以响应你认为足够重要的动态运行时条件,以便值得更新 UI。 如果使用数据绑定支持集合/业务对象,则会自动处理此类更新和更新 UI,因此通常是首选技术(请参阅 深度数据绑定)。

但并非所有应用方案都适合数据绑定。 有时,需要在运行时创建新的 UI 元素并使其可见。 BoxPanel 适用于此场景。 由于子项在计算中使用子计数,因此更改子项数量并无问题 BoxPanel ,并将现有子元素和新子元素调整为新的布局,以便它们都适合。

扩展 BoxPanel 进一步(此处未显示)的高级方案既可以容纳动态子级,又可将子级的 DesiredSize 用作单个单元格大小调整的更强因素。 此方案可能使用不同的行或列大小或非网格形状,以便“浪费”的空间更少。 这需要一种策略来说明各种大小和纵横比的多个矩形如何都适合用于美学和最小大小的包含矩形。 BoxPanel 不执行此操作,而是使用更简单的方式划分空间。 BoxPanel 方法的原理是确定大于子元素计数的最小平方数。 例如,9 个项目适合 3x3 正方形。 10 个项目需要 4x4 正方形。 但是,在仍删除起始正方形的一行或一列时,通常可以调整项,以节省空间。 在 count=10 示例中,适合 4x3 或 3x4 矩形。

你可能想知道为什么面板不会为 10 个项目选择 5x2,因为这适合项目编号整齐。 但是,在实践中,面板的大小为矩形,这些矩形很少具有强方向纵横比。 最小平方技术是一种偏置大小逻辑的方法,以便很好地处理典型的布局形状,而不是鼓励单元格形状在单元格形状获得奇纵横比的位置调整大小。

引用

概念