BoxPanel,自訂面板範例

了解為自訂 Panel 類別編寫程式碼、實作 ArrangeOverrideMeasureOverride 方法,以及使用 Children 屬性。

重要 APIPanelArrangeOverrideMeasureOverride

此範例程式碼顯示了自訂面板實作,但我們不會花很多時間說明影響如何針對不同配置場景自訂面板的配置概念。 如果您想進一步了解有關這些配置概念,以及這些概念如何套用至您的特定配置方案,請參閱 XAML 自訂面板概觀

面板是對象,當 XAML 配置系統執行並且轉譯應用程式 UI 時,它會為其包含的子元素提供配置行為。 您可以透過從 Panel 類別衍生自訂類別,來定義 XAML 配置的自訂面板。 您可以透過覆寫 ArrangeOverrideMeasureOverride 方法來為面板提供行為,提供測量和排列子元素的邏輯。 此範例衍生自 Panel。 當您從 Panel 啟動時,ArrangeOverrideMeasureOverride 方法不會有啟動行為。 您的程式碼會提供閘道,透過該閘道,XAML 配置系統能夠識別子元素,並在 UI 中轉譯。 因此,您的程式碼考慮所有子元素,並遵循配置系統期望的模式非常重要。

您的配置場景

當您定義自訂面板時,您就是在定義配置方案。

配置場景透過以下方式表達:

  • 當面板具有子元素時會做什麼
  • 當面板自身空間受到限制時
  • 面板的邏輯如何確定最終導致子項目轉譯 UI 配置的所有測量、放置、位置和大小

了解這些後,此處顯示的 BoxPanel 適用於特定場景。 為了在本範例中將程式碼放在首位,我們不會詳細解釋該場景,而是集中討論所需的步驟和編碼模式。 如果您想先了解有關該場景的更多資訊,請跳至BoxPanel 的場景」,然後返回程式碼。

首先從 Panel 衍生

首先從 Panel 衍生一個自訂類別。 最簡單的方法可能是為此類別定義單獨的程式碼檔案,使用 Microsoft Visual Studio 方案總管中專案的 新增 | 新項目 | 類別上下文功能表選項。 將類別 (和檔案) 命名為 BoxPanel

類別的範本檔案並非專供 Windows 應用程式使用,因此一開始不會有許多 using 陳述式。 首先,新增 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 公開。 在本範例中,這些名稱命名為:maxrcrowcountcolcountcellwidthcellheightmaxcellheightaspectratio

完成此動作後,完整的程式碼檔案會如下所示 (移除有關 using 的註釋,現在您知道為什麼有這些註釋了):

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 方法。 Measure 具有 Size 類型的參數。 您在此傳遞的是面板承諾可用於該特定子元素的大小。 因此,在執行迴圈並開始呼叫 Measure 之前,您需要知道每個資料格會佔用多少空間。 從 MeasureOverride 方法,您可以獲得 availableSize 值。 這是面板的父系在呼叫 Measure 時使用的大小,這是第一次呼叫此 MeasureOverride 的觸發程序。 因此,典型的邏輯是設計一種配置,其中每個子元素會劃分面板整體 availableSize 的空間。 然後,將每個大小的分區傳遞給每個子元素的 Measure

BoxPanel 劃分大小的方式相當簡單:它會將空間劃分為多個方塊,這些方塊主要由項目數量控制。 方塊會根據列數、欄數和可用大小來調整大小。 有時不需要正方形中的一列或一欄,因此會將其刪除,且面板會變成矩形,而不是根據其列欄比的正方形。 有關如何實作此邏輯的更多資訊,請跳至「BoxPanel 的場景」

那麼量值傳遞有何用途? 它會為呼叫 Measure 的每個元素的唯讀 DesiredSize 屬性設定一個值。 一旦進入排列階段,擁有 DesiredSize 值可能很重要,因為 DesiredSize 傳達了排列時和最終轉譯中的大小可以或應該是什麼。 即使您在自己的邏輯中不使用 DesiredSize,系統仍然需要它。

availableSize 的高度元件未繫結時,可以使用此面板。 如果真是如此,那麼面板就沒有已知的高度來劃分。 在這種情況下,量值階段的邏輯會通知每個子系,它還沒有限定的高度。 它會透過將 Size 傳遞給子系的 Measure 呼叫來實現此目的,其中 Size.Height 是無限的。 這是正當的。 當呼叫 Measure 時,邏輯是將 DesiredSize 設定為以下最小值:傳遞給 Measure 的內容,或該元素的自然大小,例如來自明確設定的 HeightWidth 等因素。

注意

StackPanel 的內部邏輯也有這樣的行為:StackPanel 向子系上的 Measure 傳遞一個無限維度值,表示在方向維度上對子系沒有約束。 StackPanel 通常會動態調整自身大小,以容納在該維度上成長堆疊中的所有子系。

但是,面板本身無法從 MeasureOverride 傳回無限值的 Size; 那會在配置期間擲回例外狀況。 因此,該邏輯的一部分是找出任何子系要求的最大高度,並使用該高度作為資料格高度,以防該高度尚未來自面板本身的尺寸限制。 這是前面程式碼中參考的輔助函式 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 值獲知。 但是,我們仍然需要決定每個子系會在面板中出現的位置。 在典型的面板中,每個子系都應該在不同的位置轉譯。 在典型案例中,建立重疊元素的面板並不理想 (雖然建立具有目的地重疊的面板不是問題,如果這確實是您想要的場景)。

此面板會按照列和欄的概念進行排列。 列數和欄數已經計算出來 (測量的必要項目)。 因此,現在列和欄的形狀加上每個資料格的已知大小,有助於為該面板包含的每個元素定義轉譯位置 (anchorPoint) 的邏輯。 該 Point 與經過測量已知的 Size 一起作為建構 Rect 的兩個元件。 RectArrange 的輸入類型。

面板有時需要裁剪其內容。 如果這樣做,則裁剪的大小是 DesiredSize 中存在的大小,因為 Measure 邏輯將其設定為傳遞給 Measure 的最小值或其他自然大小因素。 因此,您通常不需要在 Arrange 期間專門檢查裁剪; 裁剪只是根據將 DesiredSize 傳遞給每個 Arrange 呼叫而發生。

如果透過其他方式知道定義轉譯位置所需的所有資訊,則在執行迴圈時並不一定需要計數。 例如,在 Canvas 配置邏輯中,Children 集合中的位置並不重要。 透過讀取子系的 Canvas.LeftCanvas.Top 值作為排列邏輯的一部分,可以了解在 Canvas 中放置每個元素所需的所有資訊。 BoxPanel 邏輯恰好需要一個計數來與 colcount 進行比較,以便知道何時開始新列並位移 y 值。

通常,輸入的 finalSize 和從 ArrangeOverride 實作傳回的 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 中的測量邏輯。 事實上,它所做的只是改變 rowcountcolcountmaxrc 和真實外觀比例衍生的方式,因此每個資料格都有相應的大小差異。 當 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.RowGrid.Column 附加屬性宣稱其進入的確切資料格。 從 Grid 獲得良好的配置通常需要事先知道子元素的數量,以便有足夠的資料格,並且每個子元素設定其附加屬性以適合自己的資料格。

但如果子系的數量是動態的呢? 當然有這個可能; 您的應用程式程式碼可以將項目新增到集合中,以回應您認為重要到值得更新 UI 的任何動態執行階段條件。 如果您使用資料繫結來支援集合/業務對象,則會自動處理以取得此類更新和更新 UI,因此這通常是首選技術 (請參閱深入了解資料繫結)。

但並非所有應用程式場景都適合資料繫結。 有時,您需要在執行階段建立新的 UI 元素,並使其顯示。 BoxPanel 適用於這個案例。 子項數量的變化對於 BoxPanel 來說不是問題,因為它在會計算中使用子系計數,並將現有的和新的子元素調整到新的配置中,使其全部符合。

進一步擴展 BoxPanel 的進階方案 (此處未顯示) 既可以容納動態子系,又可以使用子系的 DesiredSize 作為調整單一資料格大小的更強因素。 這種情況可能會使用不同的列或欄大小或非方格形狀,以便減少「浪費」的空間。 這需要一種策略來確定如何將不同大小和外觀比例的多個矩形全部放入一個包含矩形中,以實現美觀和最小尺寸。 BoxPanel 不是這麼做,它是使用較簡單的技術來劃分空間。 BoxPanel 使用的技術是判斷大於子系計數的最少方形數目。 例如,3x3 的正方形可容納 9 個項目。 10 個項目需要 4x4 的正方形。 但是,您通常可以在放置項目的同時移除起始方塊的一列或欄,以節省空間。 在 count=10 範例中,適合 4x3 或 3x4 矩形。

您可能會想知道為什麼面板不會為 10 個項目選擇 5x2,因為這能整齊地符合項目號碼。 不過,在實務上,面板的大小通常為矩形,很少具有強定向的外觀比例。 最小平方法技術是一種偏向調整大小邏輯的方法,以便妥善配合典型的配置形狀,而不是鼓勵在資料格形狀具有奇數外觀比例的情況下調整大小。

參考

概念