BoxPanel,一个自定义面板示例BoxPanel, an example custom panel

学习为自定义 Panel 类编写代码、实现 ArrangeOverrideMeasureOverride 方法,以及使用 Children 属性。Learn to write code for a custom Panel class, implementing ArrangeOverride and MeasureOverride methods, and using the Children property.

重要的 APIPanelArrangeOverrideMeasureOverrideImportant APIs: Panel, ArrangeOverride,MeasureOverride

示例代码显示了一个自定义面板实现,但我们不会花费许多时间来解释影响为不同的布局方案自定义面板方式的布局概念。The example code shows a custom panel implementation, but we don't devote a lot of time explaining the layout concepts that influence how you can customize a panel for different layout scenarios. 如果你需要关于这些布局概念以及它们可能如何应用到特定布局方案的详细信息,请参阅 XAML 自定义面板概述If you want more info about these layout concepts and how they might apply to your particular layout scenario, see XAML custom panels overview.

panel 是当 XAML 布局系统运行且呈现应用 UI 时为所包含的子元素提供布局行为的对象。A panel is an object that provides a layout behavior for child elements it contains, when the XAML layout system runs and your app UI is rendered. 你可以通过从 Panel 类派生自定义类来为 XAML 布局定义自定义面板。You can define custom panels for XAML layout by deriving a custom class from the Panel class. 通过替代 ArrangeOverrideMeasureOverride 方法、提供度量和排列子元素的逻辑来为面板提供行为。You provide behavior for your panel by overriding the ArrangeOverride and MeasureOverride methods, supplying logic that measures and arranges the child elements. 此示例派生自 PanelThis example derives from Panel. 当你从 Panel 开始时,ArrangeOverrideMeasureOverride 方法没有开始行为。When you start from Panel, ArrangeOverride and MeasureOverride methods don't have a starting behavior. 你的代码可提供网关,子元素通过此网关变为对 XAML 布局系统已知并在 UI 中获得呈现。Your code is providing the gateway by which child elements become known to the XAML layout system and get rendered in the UI. 因此,你的代码考虑到所有子元素并遵循布局系统预期的模式十分重要。So, it's really important that your code accounts for all child elements and follows the patterns the layout system expects.

你的布局方案Your layout scenario

当你定义自定义面板时,你在定义布局方案。When you define a custom panel, you're defining a layout scenario.

布局方案通过以下内容表达:A layout scenario is expressed through:

  • 当面板拥有子元素时将执行何种操作What the panel will do when it has child elements
  • 面板何时在其自己的空间中有约束When the panel has constraints on its own space
  • 面板的逻辑如何确定最终呈现子元素的 UI 布局的所有度量、放置、位置和大小How the logic of the panel determines all the measurements, placement, positions, and sizings that eventually result in a rendered UI layout of children

基于这一点,此处显示的 BoxPanel 适用于特定方案。With that in mind, the BoxPanel shown here is for a particular scenario. 为了保持代码在此示例中的重要性,我们将不详细解释方案,而是专注于所需的步骤和编码模式。In the interest of keeping the code foremost in this example, we won't explain the scenario in detail yet, and instead concentrate on the steps needed and the coding patterns. 如果你希望首先了解有关方案的详细信息,则跳到“适用于 BoxPanel 的方案”,然后返回到代码。If you want to know more about the scenario first, skip ahead to "The scenario for BoxPanel", and then come back to the code.

由从 Panel 派生开始Start by deriving from Panel

由从 Panel 派生自定义类开始。Start by deriving a custom class from Panel. 执行此操作的最简单方法可能是为此类定义一个独立的代码文件,方法是从 Microsoft Visual Studio 中的“解决方案资源管理器”中针对项目使用“添加” | “新建项” | “类”上下文菜单选项。Probably the easiest way to do this is to define a separate code file for this class, using the Add | New Item | Class context menu options for a project from the Solution Explorer in Microsoft Visual Studio. 将该类(以及文件)命名为 BoxPanelName the class (and file) BoxPanel.

类的模板文件不会从多个 using 语句开始,因为它不是专门为 Windows 应用设计的。The template file for a class doesn't start with many using statements because it's not specifically for Windows apps. 因此,首先添加 using 语句。So first, add using statements. 模板文件的开头部分还包含一些你可能不需要的 using 语句,可以将其删除。The template file also starts with a few using statements that you probably don't need, and can be deleted. 以下是可解析类型的 using 语句的建议列表,你将需要将这些语句用于典型的自定义面板代码:Here's a suggested list of using statements that can resolve types you'll need for typical custom panel code:

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 的基类。Now that you can resolve Panel, make it the base class of BoxPanel. 还可以使 BoxPanel 成为公共对象:Also, make BoxPanel public:

public class BoxPanel : Panel
{
}

在类级别上,定义一些将由几个逻辑函数共享的 intdouble 值,但是这些值无需显示为公共 API。At the class level, define some int and double values that will be shared by several of your logic functions, but which won't need to be exposed as public API. 在该示例中,它们命名为:maxrcrowcountcolcountcellwidthcellheightmaxcellheightaspectratioIn the example, these are named: maxrc, rowcount, colcount, cellwidth, cellheight, maxcellheight, aspectratio.

在你完成此操作后,完整代码文件外观如下(既然你已知道我们为何拥有它们,可以删除 using 上的注释了):After you've done this, the complete code file looks like this (removing comments on using, now that you know why we have them):

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

从现在起,我们将一次向你显示一个成员定义,无论它是方法替代还是支持内容(例如依赖属性)。From here on out, we'll be showing you one member definition at a time, be that a method override or something supporting such as a dependency property. 你可以将它们以任何顺序添加到上述框架,而且我们将不再在代码段中显示 using 语句或类范围的定义,直到我们显示最终代码。You can add these to the skeleton above in any order, and we won't be showing the using statements or the definition of the class scope again in the snippets until we show the final code.

MeasureOverrideMeasureOverride

protected override Size MeasureOverride(Size availableSize)
{
    Size returnSize;
    // 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 中的每个元素。The necessary pattern of a MeasureOverride implementation is the loop through each element in Panel.Children. 始终对这些元素中的每一个调用 Measure 方法。Always call the Measure method on each of these elements. Measure 具有类型 Size 的参数。Measure has a parameter of type Size. 你在此处传递的内容是,面板致力于获取的适用于该特定子元素的大小。What you're passing here is the size that your panel is committing to have available for that particular child element. 因此,在你可以进行循环访问并开始调用 Measure 之前,你需要知道每个单元格可以提供多少空间。So, before you can do the loop and start calling Measure, you need to know how much space each cell can devote. MeasureOverride 方法本身,你具有 availableSize 值。From the MeasureOverride method itself, you have the availableSize value. 它是面板的父元素在调用 Measure 时使用的大小,它是最初调用此 MeasureOverride 的触发器。That is the size that the panel's parent used when it called Measure, which was the trigger for this MeasureOverride being called in the first place. 因此典型的逻辑是制定一个方案,每个子元素通过此方案划分面板的整体 availableSize 的空间。So a typical logic is to devise a scheme whereby each child element divides the space of the panel's overall availableSize. 然后你将大小的每个划分传递到每个子元素的 MeasureYou then pass each division of size to Measure of each child element.

BoxPanel 划分大小的方式相当简单:它将它的空间划分为一些框,这些框很大程度上受项目数量的控制。How BoxPanel divides size is fairly simple: it divides its space into a number of boxes that's largely controlled by the number of items. 基于行列计数和可用大小调整框的大小。Boxes are sized based on row and column count and the available size. 有时因为不需要正方形中的一行或一列而将其删除,因此就行列比而言,面板变成了矩形而不是正方形。Sometimes one row or column from a square isn't needed, so it's dropped and the panel becomes a rectangle rather than square in terms of its row : column ratio. 有关如何到达此逻辑的详细信息,请跳到“面向 BoxPanel 的方案”For more info about how this logic was arrived at, skip ahead to "The scenario for BoxPanel".

那么度量传递的作用是什么?So what does the measure pass do? 它为在其中调用 Measure 的每个元素的只读 DesiredSize 属性设置值。It sets a value for the read-only DesiredSize property on each element where Measure was called. 一旦你进行排列传递,拥有 DesiredSize 值可能很重要,因为 DesiredSize 传达排列和最终呈现时可以设置和应当设置的大小。Having a DesiredSize value is possibly important once you get to the arrange pass, because the DesiredSize communicates what the size can or should be when arranging and in the final rendering. 即使你不在你自己的逻辑中使用 DesiredSize,系统仍然需要它。Even if you don't use DesiredSize in your own logic, the system still needs it.

availableSize 的高度组件未绑定时,可能使用此面板。It's possible for this panel to be used when the height component of availableSize is unbounded. 如果确实如此,则面板没有要划分的已知高度。If that's true, the panel doesn't have a known height to divide. 在此情况下,度量传递的逻辑会通知每个子元素它尚未具有绑定的高度。In this case, the logic for the measure pass informs each child that it doesn't have a bounded height, yet. 它通过为 Size.Height 为无限的子元素将 Size 传递到 Measure 调用来执行此操作。It does so by passing a Size to the Measure call for children where Size.Height is infinite. 这是合法的。That's legal. 当调用 Measure 时,逻辑为将 DesiredSize 设置为以下值中的最小值:传递到 Measure 的内容,或来自系数(例如明确设置的 HeightWidth)的该元素的自然大小。When Measure is called, the logic is that the DesiredSize is set as the minimum of these: what was passed to Measure, or that element's natural size from factors such as explicitly-set Height and Width.

备注

StackPanel 的内部逻辑也具有此行为:StackPanel 将一个无线维度值传递到子元素上的 Measure,表示在方向维度中的子元素上没有约束。The internal logic of StackPanel also has this behavior: StackPanel passes an infinite dimension value to Measure on children, indicating that there is no constraint on children in the orientation dimension. StackPanel 通常动态调整自身大小,以使在该维度中增长的堆栈容纳所有子元素。StackPanel typically sizes itself dynamically, to accommodate all children in a stack that grows in that dimension.

但是,面板本身无法从 MeasureOverride 返回带有无限值的 Size;这将在布局期间引发异常。However, the panel itself can't return a Size with an infinite value from MeasureOverride; that throws an exception during layout. 因此部分逻辑要找出任何子元素请求的最大高度,并将高度用作单元格高度,以防尚未从面板的自身大小约束中获得该高度的情况。So, part of the logic is to find out the maximum height that any child requests, and use that height as the cell height in case that isn't coming from the panel's own size constraints already. 以下是在之前的代码中引用的帮助程序函数 LimitUnboundedSize,此函数随后获取该最大单元格高度,并使用它向面板提供一个用于返回的有限高度,以及确保在启动排列传递之前 cellheight 为有限值:Here's the helper function LimitUnboundedSize that was referenced in previous code, which then takes that maximum cell height and uses it to give the panel a finite height to return, as well as assuring that cellheight is a finite number before the arrange pass is initiated:

// This method is called only if one of the availableSize dimensions of measure is infinite.
// 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;
}

ArrangeOverrideArrangeOverride

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 中的每个元素。The necessary pattern of an ArrangeOverride implementation is the loop through each element in Panel.Children. 始终对这些元素中的每一个调用 Arrange 方法。Always call the Arrange method on each of these elements.

请注意,计算不如 MeasureOverride 中多;这很正常。Note how there aren't as many calculations as in MeasureOverride; that's typical. 子元素的大小已经从面板的自身 MeasureOverride 逻辑获知,或从度量传递期间每个子元素集合的 DesiredSize 值获知。The size of children is already known from the panel's own MeasureOverride logic, or from the DesiredSize value of each child set during the measure pass. 但是,我们仍然需要确定每个子元素在面板中出现的位置。However, we still need to decide the location within the panel where each child will appear. 在典型的面板中,每个子元素应当在不同的位置呈现。In a typical panel, each child should render at a different position. 典型方案不需要创建重叠元素的面板(然而如果那是你真正所需的方案,创建带有有意重叠的面板也并非不可能)。A panel that creates overlapping elements isn't desirable for typical scenarios (although it's not out of the question to create panels that have purposeful overlaps, if that's really your intended scenario).

此面板按行和列的概念排列。This panel arranges by the concept of rows and columns. 行和列的数量已经过计算(对于度量是必需的)。The number of rows and columns was already calculated (it was necessary for measurement). 因此,现在行和列的形状以及每个单元格的已知大小有助于定义此面板包含的每个元素的呈现位置 (anchorPoint) 的逻辑。So now the shape of the rows and columns plus the known sizes of each cell contribute to the logic of defining a rendering position (the anchorPoint) for each element that this panel contains. 从度量已知的 Point 以及 Size 用作构造 Rect 的两个组件。That Point, along with the Size already known from measure, are used as the two components that construct a Rect. RectArrange 的输入类型。Rect is the input type for Arrange.

面板有时需要剪裁它们的内容。Panels sometimes need to clip their content. 如果它们执行此操作,剪裁的大小是 DesiredSize 中显示的大小,因为 Measure 逻辑将其设置为传递到 Measure 的内容的最小值,或其他自然大小系数。If they do, the clipped size is the size that's present in DesiredSize, because the Measure logic sets it as the minimum of what was passed to Measure, or other natural size factors. 因此你通常不需要在 Arrange 期间专门检查剪裁,剪裁的发生仅基于将 DesiredSize 传递到每个 Arrange 调用。So you don't typically need to specifically check for clipping during Arrange; the clipping just happens based on passing the DesiredSize through to each Arrange call.

如果定义呈现位置所需的所有信息通过其他方式已知,则你在经过循环访问时不需要始终计数。You don't always need a count while going through the loop if all the info you need for defining the rendering position is known by other means. 例如,在 Canvas 布局逻辑中,Children 集合中的位置不重要。For example, in Canvas layout logic, the position in the Children collection doesn't matter. Canvas 中定位每个元素所需的所有信息通过读取子元素的 Canvas.LeftCanvas.Top 值作为排列逻辑的一部分已知。All the info needed to position each element in a Canvas is known by reading Canvas.Left and Canvas.Top values of children as part of the arrange logic. BoxPanel 逻辑恰好需要计数以对 colcount 进行比较,因此已知何时开始一个新行并抵消 y 值。The BoxPanel logic happens to need a count to compare to the colcount so it's known when to begin a new row and offset the y value.

通常输入 finalSize 和你从 ArrangeOverride 实现返回的 Size 相同。It's typical that the input finalSize and the Size you return from a ArrangeOverride implementation are the same. 有关原因的详细信息,请参阅 XAML 自定义面板概述的“ArrangeOverride”部分。For more info about why, see "ArrangeOverride" section of XAML custom panels overview.

优化:控制行与列计数A refinement: controlling the row vs. column count

你可以按照当前现状编译并使用此面板。You could compile and use this panel just as it is now. 但是,我们将再添加一个优化。However, we'll add one more refinement. 在刚才显示的代码中,逻辑将附加行或列放置到在纵横比中最长的边上。In the code just shown, the logic puts the extra row or column on the side that's longest in aspect ratio. 但为了更好地控制单元格的形状,最好选择 4x3 单元格设置,而不是 3x4,即使面板的自身纵横比是“纵向”。But for greater control over the shapes of cells, it might be desirable to choose a 4x3 set of cells instead of 3x4 even if the panel's own aspect ratio is "portrait." 因此我们将添加一个可选的依赖属性,面板使用者可设置此属性以控制该行为。So we'll add an optional dependency property that the panel consumer can set to control that behavior. 以下是依赖属性定义,此定义非常基本:Here's the dependency property definition, which is very basic:

public static readonly DependencyProperty UseOppositeRCRatioProperty =
   DependencyProperty.Register("UseOppositeRCRatio", typeof(bool), typeof(BoxPanel), null);

public bool UseSquareCells
{
    get { return (bool)GetValue(UseOppositeRCRatioProperty); }
    set { SetValue(UseOppositeRCRatioProperty, value); }
}

以下为使用 UseOppositeRCRatio 对度量逻辑的影响。And here's how using UseOppositeRCRatio impacts the measure logic. 它所执行的所有操作实际上是更改 rowcountcolcountmaxrc 和真纵横比派生的方式,因此每个单元格都有相应的大小差异。Really all it's doing is changing how rowcount and colcount are derived from maxrc and the true aspect ratio, and there are corresponding size differences for each cell because of that. UseOppositeRCRatiotrue 时,它在将真纵横比的值用于行和列计数前反转它。When UseOppositeRCRatio is true, it inverts the value of the true aspect ratio before using it for row and column counts.

if (UseOppositeRCRatio) { aspectratio = 1 / aspectratio;}

面向 BoxPanel 的方案The scenario for BoxPanel

面向 BoxPanel 的特定方案具有这样的面板:面板中的一个如何划分空间的主要决定因素是,通过知道子项目的数量并为面板划分已知可用空间。The particular scenario for BoxPanel is that it's a panel where one of the main determinants of how to divide space is by knowing the number of child items, and dividing the known available space for the panel. 面板固有为矩形。Panels are innately rectangle shapes. 许多面板通过将该矩形空间划分为更多的矩形来操作;这是 Grid 对其单元格执行的操作。Many panels operate by dividing that rectangle space into further rectangles; that's what Grid does for its cells. 对于 Grid,单元格的大小由 ColumnDefinitionRowDefinition 值设置,而且元素通过 Grid.RowGrid.Column 附加属性声明它们进入的确切单元格。In Grid's case, the size of the cells is set by ColumnDefinition and RowDefinition values, and elements declare the exact cell they go into with Grid.Row and Grid.Column attached properties. Grid 获取良好的布局通常需要事先知道子元素的数量,以便有足够单元格且每个子元素设置其附加属性以使其适合自己的单元格。Getting good layout from a Grid usually requires knowing the number of child elements beforehand, so that there are enough cells and each child element sets its attached properties to fit into its own cell.

但是如果子元素的数量是动态的该怎么办?But what if the number of children is dynamic? 这当然可能。你的应用代码可将项目添加到集合中,以响应任何你认为足够重要且值得更新 UI 的动态运行时状况。That's certainly possible; your app code can add items to collections, in response to any dynamic run-time condition you consider to be important enough to be worth updating your UI. 如果你对备份集合/业务对象使用数据绑定,获取此类更新和更新 UI 将自动处理,因此这通常是首选技术(请参阅数据绑定概述)。If you're using data binding to backing collections/business objects, getting such updates and updating the UI is handled automatically, so that's often the preferred technique (see Data binding in depth).

但是并非所有应用方案都适合数据绑定。But not all app scenarios lend themselves to data binding. 有时你在运行时需要创建新 UI 元素并使它们可见。Sometimes, you need to create new UI elements at runtime and make them visible. BoxPanel 适用于此场景。BoxPanel is for this scenario. 变化的子项目数对 BoxPanel 不构成问题,因为它在计算中使用子元素计数,并且将现有和新子元素调整为新布局,以使它们全部适合。A changing number of child items is no problem for BoxPanel because it's using the child count in calculations, and adjusts both the existing and new child elements into a new layout so they all fit.

用于进一步扩展 BoxPanel 的高级方案(此处未显示)可同时容纳动态子元素并使用子元素的 DesiredSize 作为调整个别单元格大小的更强系数。An advanced scenario for extending BoxPanel further (not shown here) could both accommodate dynamic children and use a child's DesiredSize as a stronger factor for the sizing of individual cells. 此方案可能使用不同的行或列大小或者非网格形状来减少“浪费的”空间。This scenario might use varying row or column sizes or non-grid shapes so that there's less "wasted" space. 这需要一种战略,用于确定大小和纵横比各异的多个矩形如何同时出于视觉美学和最小大小而全部适合包含的矩形。This requires a strategy for how multiple rectangles of various sizes and aspect ratios can all fit into a containing rectangle both for aesthetics and smallest size. BoxPanel 不执行此操作,而是使用更简单的方式划分空间。BoxPanel doesn't do that; it's using a simpler technique for dividing space. BoxPanel 方法的原理是确定大于子元素计数的最小平方数。BoxPanel's technique is to determine the least square number that's greater than the child count. 例如,9 个项目可容纳到 3x3 正方形中。For example, 9 items would fit in a 3x3 square. 10 个项目需要 4x4 正方形。10 items require a 4x4 square. 不过,你可以经常调整项目,同时仍然可以删除开始正方形的一行或一列,以便节省空间。However, you can often fit items while still removing one row or column of the starting square, to save space. 在 count=10 示例中,它可容纳到 4x3 或 3x4 矩形中。In the count=10 example, that fits in a 4x3 or 3x4 rectangle.

你可能想知道为什么该面板不为 10 个项目选择 5x2,因为那将完美地容纳该项目数。You might wonder why the panel wouldn't instead choose 5x2 for 10 items, because that fits the item number neatly. 但是,在实际中,会将面板大小调整为很少有方向明显的纵横比的矩形。However, in practice, panels are sized as rectangles that seldom have a strongly oriented aspect ratio. 最小二乘技术是一种影响大小逻辑的方式,以便良好适合典型布局形状,并且不鼓励单元格形状可获取奇数纵横比的大小调整。The least-squares technique is a way to bias the sizing logic to work well with typical layout shapes and not encourage sizing where the cell shapes get odd aspect ratios.

参考Reference

概念Concepts