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.

「面板」是一個物件,可在 XAML 配置系統執行和轉譯 app 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.

類別的範本檔案並非專供 Windows 應用程式使用,因此一開始不會有許多 using 陳述式。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
{
}

在類別層級定義一些會由數個邏輯函式共用,但不需要公開為公用 API 的 intdouble 值。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 期間特別檢查裁剪,系統會根據傳遞至每個 Arrange 呼叫的 DesiredSize 進行裁剪。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. 事實上,它只是變更從 maxrc 衍生 rowcountcolcount 的方式和實際的外觀比例,因此每個儲存格都有對應的大小差異。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? 上述情況絕對可能發生;App 程式碼可以將項目加入集合,以回應您認為其重要性達到需要更新 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