XAML 自訂面板概觀

面板是物件,當 Extensible Application Markup Language (XAML) 配置系統執行並且轉譯應用程式 UI 時,它為其包含的子元素提供配置行為。

重要 APIPanelArrangeOverrideMeasureOverride

您可以透過從 Panel 類別衍生自訂類別,來定義 XAML 配置的自訂面板。 您可以透過覆寫 MeasureOverrideArrangeOverride 方法來為面板提供行為,提供測量和排列子元素的邏輯。

Panel 基底類別

若要定義自訂面板類別,您可以直接從 Panel 類別衍生,也可以從未密封的其中一個實用面板類別衍生,例如 GridStackPanel。 從 Panel 衍生比較容易,因為要解決已經具有配置行為的面板的現有配置邏輯可能很困難。 此外,具有行為的面板可能具有與面板配置功能無關的現有屬性。

Panel 中,您的自訂面板會繼承以下 API:

  • Children 屬性。
  • BackgroundChildrenTransitionsIsItemsHost 屬性以及相依性屬性識別碼。 這些屬性都不是虛擬的,因此您通常不會覆寫或取代它們。 對於自訂面板,您通常不需要這些屬性,甚至不需要讀取值。
  • 配置覆寫方法 MeasureOverrideArrangeOverride。 這些最初是由 FrameworkElement 定義。 基底 Panel 類別不會覆寫這些,但像 Grid 這樣的實用面板確實具有覆寫實作,這些實作會作為本機程式碼實作並由系統執行。 為 ArrangeOverrideMeasureOverride 提供新的 (或附加的) 實作是定義自訂面板所需的大量工作。
  • FrameworkElementUIElementDependencyObject 的所有其他 API,例如 HeightVisibility 等。 您有時會在配置覆寫中引用這些屬性的值,但它們不是虛擬的,因此您通常不會覆寫或取代它們。

這裡的重點是描述 XAML 配置概念,因此您可以考慮自訂面板在配置中可以和應該如何表現的所有可能性。 如果您想直接查看範例自訂面板實作,請參閱 BoxPanel,自訂面板範例

Children 屬性

Children 屬性與自訂面板相關,因為從 Panel 衍生的所有類別都使用 Children 屬性作為在集合中儲存其包含子元素的位置。 Children 指定為 Panel 類別的 XAML 內容屬性,並且從 Panel 衍生的所有類別都可以繼承 XAML 內容屬性行為。 如果將屬性指定為 XAML 內容屬性,則表示 XAML 標記在標記中指定該屬性時可以省略屬性元素,並且值將設定為直接標記子系 (「內容」)。 例如,如果您從沒有定義新行為的 Panel 衍生一個名為 CustomPanel 的類別,您仍然可以使用此標記:

<local:CustomPanel>
  <Button Name="button1"/>
  <Button Name="button2"/>
</local:CustomPanel>

當 XAML 解析器讀取此標記時,Children 已知是所有 Panel 衍生類型的 XAML 內容屬性,因此解析器會將兩個 Button 元素新增至 Children 屬性的 UIElementCollection 值中。 XAML 內容屬性有助於在 UI 定義的 XAML 標籤中簡化父子關係。 有關 XAML 內容屬性以及解析 XAML 時,如何填入集合屬性的詳細資訊,請參閱 XAML 語法指南

維護 Children 屬性值的集合類型是 UIElementCollection 類別。 UIElementCollection 是強類型集合,它使用 UIElement 作為其強制項目類型。 UIElement 是由數百種實用 UI 元素類型繼承的基底類型,因此這裡的類型強制執行是故意放寬的。 但它確實強制要求您不能將 Brush 作為 Panel 的直接子元素,並且通常代表只有預期在 UI 中可見並參與配置的元素才會在 Panel 中作為子元素找到。

通常,自訂面板透過 XAML 定義接受任何 UIElement 子元素,只需按原樣使用 Children 屬性的特性即可。 在進階案例中,當您在配置覆寫中迭代集合時,您可以支援對子元素進行進一步的類型檢查。

除了在覆寫中迴圈 Children 集合之外,您的面板邏輯也可能受到 Children.Count 的影響。 您可能具有至少部分根據項目數目,而不是所需大小和個別項目的其他特性來分配空間的邏輯。

覆寫配置方法

配置覆寫方法 (MeasureOverrideArrangeOverride) 的基本模型是它們應該迭代所有子系,並呼叫每個子元素的特定配置方法。 當 XAML 配置系統設定根視窗的視覺效果時,第一個配置週期開始。 由於每個父系都會在其子層級上呼叫配置,因此這將對配置方法的呼叫傳播到應屬於配置一部分的每個可能的 UI 元素。 在 XAML 配置中有兩個階段:測量,然後排列。

您無法從 Panel 基底類別獲得 MeasureOverrideArrangeOverride 的任何內建配置方法行為。 Children 中的項目不會自動轉譯為 XAML 視覺化樹狀結構的一部分。 您可以透過 MeasureOverrideArrangeOverride 實作中的配置傳遞,對在 Children 中找到的每個項目叫用配置方法,從而使這些項目為配置處理所知。

除非您有自己的繼承,否則沒有理由在配置覆寫中呼叫基底實作。 配置行為的原生方法 (如果存在) 無論如何都會執行,並且不會從覆寫中呼叫基低實作,不會阻止原生行為的發生。

在測量傳遞中,配置邏輯會透過呼叫子元素上的 Measure 方法來查詢每個子元素的所需大小。 呼叫 Measure 方法可決定 DesiredSize 屬性的值。 MeasureOverride 傳回值是面板本身所需的大小。

在排列傳遞中,子元素的位置和大小在 x-y 空間中確定,並為轉譯準備配置組合。 您的程式碼必須對 Children 中的每個子元素呼叫 Arrange,以便配置系統偵測到該元素屬於配置。 Arrange 呼叫是組合和轉譯的先驅; 當提交合成進行轉譯時,它會通知配置系統該元素的位置。

許多屬性和值都會影響配置邏輯在執行階段的運作方式。 一種考慮配置處理的方法是,沒有子系的元素 (通常是 UI 中嵌套最深的元素) 是可以先完成測量的元素。 它們對影響其所需大小的子元素沒有任何相依性。 它們可能有自己所需的大小,而且這些是大小建議,直到配置實際發生。 然後,測量傳遞繼續沿著視覺化樹狀結構向上移動,直到根元素具有其測量值,並且所有測量都可以完成。

候選配置必須適合目前應用程式視窗,否則部分 UI 將裁剪。 面板通常是決定裁剪邏輯的位置。 面板邏輯可以確定 MeasureOverride 實作中可用的大小,並且可能必須將大小限制推到子系上並在子系之間劃分空間,以便一切都盡可能適合。 理想情況下,配置的結果是使用配置所有部分的各種屬性,但仍然適合應用程式視窗。 這不僅需要對面板的配置邏輯進行良好的實現,還需要對使用該面板構建 UI 的任何應用程式程式碼進行明智的 UI 設計。 如果整體 UI 設計包含的子元素多於應用程式可能容納的數量,那麼任何面板設計都不會好看。

配置系統發揮作用的很大一部分原因是,任何基於 FrameworkElement 的元素在充當容器中的子元素時,都已經具有一些自己的固有行為。 例如,FrameworkElement 有多個 API,它們要麼通知配置行為,要麼需要讓配置正常運作。 包括:

MeasureOverride

MeasureOverride 方法有一個傳回值,當配置中的父系在面板上呼叫 Measure 方法時,配置系統會使用該傳回值作為面板本身的起始 DesiredSize。 方法中的邏輯選擇與其傳回的內容一樣重要,邏輯通常會影響傳回的值。

所有 MeasureOverride 實作都應迴圈執行 Children,並在每個子元素上呼叫 Measure 方法。 呼叫 Measure 方法可決定 DesiredSize 屬性的值。 這可能會告知面板本身需要多少空間,以及該空間如何在元素之間劃分,或如何針對特定子元素調整大小。

以下是 MeasureOverride 方法的基本架構:

protected override Size MeasureOverride(Size availableSize)
{
    Size returnSize; //TODO might return availableSize, might do something else
     
    //loop through each Child, call Measure on each
    foreach (UIElement child in Children)
    {
        child.Measure(new Size()); // TODO determine how much space the panel allots for this child, that's what you pass to Measure
        Size childDesiredSize = child.DesiredSize; //TODO determine how the returned Size is influenced by each child's DesiredSize
        //TODO, logic if passed-in Size and net DesiredSize are different, does that matter?
    }
    return returnSize;
}

元素在準備好進行配置時,通常具有自然大小。 在測量傳遞後,如果您為 Measure 傳遞的 availableSize 較小,則 DesiredSize 可能會指示自然尺寸。 如果自然大小大於您為 Measure 傳遞的 availableSize,則 DesiredSize 將被限制為 availableSize。 這就是 Measure 的內部實作運作方式,您的配置覆寫應該考慮到該行為。

有些元素沒有自然大小,因為它們的 HeightWidth 具有 Auto 值。 這些元素使會用完整的 availableSize,因為這就是 Auto 值所代表的含義:將元素的大小設為最大可用大小,直接配置父系透過使用 availableSize 呼叫 Measure 來進行通訊。 在實踐中,UI 的大小總會有一些測量值 (即使這是頂層視窗)。最終,測量傳遞會將所有 Auto 值解析為父系限制,並且所有 Auto 值元素都會獲得實際測量值 (配置完成後,您可以透過檢查 ActualWidthActualHeight 來取得)。

將至少具有無限維度的尺寸傳遞給 Measure 是合法的,表示面板可以嘗試調整自身尺寸以符合其內容的測量。 要測量的每個子元素會使用其自然大小來設定其 DesiredSize 值。 然後,在排列傳遞中,面板通常會使用該大小進行排列。

即使沒有設定 HeightWidth 值,文字元素 (例如 TextBlock) 也會根據其文字字串和文字屬性計算出 ActualWidthActualHeight,並且面板邏輯應遵守這些維度。 裁剪文字是特別糟糕的 UI 體驗。

即使您的實作不使用所需的大小測量,最好對每個子元素呼叫 Measure 方法,因為呼叫 Measure 會觸發內部和原生行為。 對於參與配置的元素,每個子元素都必須在測量傳遞中呼叫 Measure,並在排列傳遞中呼叫 Arrange 方法。 呼叫這些方法會在物件上設定內部標誌,並填入系統配置邏輯在建立視覺化樹狀結構和轉譯 UI 時所需的值 (例如 DesiredSize 屬性)。

MeasureOverride 傳回值是以面板邏輯為基礎,當對 Children 中的每個子元素呼叫 Measure 時,請解釋 DesiredSize 或其他大小注意事項。 如何處理來自子系的 DesiredSize 值,以及 MeasureOverride 傳回值應如何使用它們取決於您自己的邏輯解釋。 您通常不會在不進行修改的情況下新增值,因為 MeasureOverride 的輸入通常是面板父系建議的固定可用大小。 如果超過該大小,面板可能會被裁剪。 您通常會比對子系的總大小與面板的可用大小,並在必要時進行調整。

提示和指南

  • 理想情況下,自訂面板應該適合作為 UI 組合中的第一個真正的視覺效果,可能位於緊鄰 PageUserControl 或作為 XAML 頁面根目錄的另一個元素之下的層級。 在 MeasureOverride 實作中,不要在不檢查值的情況下定期回傳輸入 Size。 如果傳回的 Size 包含 Infinity 值,則可能會在執行階段配置邏輯中擲回例外狀況。 Infinity 值可以來自主應用程式視窗,該視窗是可捲動的,因此沒有最大高度。 其他可捲動內容可能具有相同的行為。
  • MeasureOverride 實作中另一個常見的錯誤是傳回新的預設 Size (高度和寬度值為 0)。 您可以從該值開始,如果您的面板確定不應轉譯任何子系,那麼它甚至可能是正確的值。 但是,預設 Size 會導致面板的主機無法正確調整大小。 它在 UI 中不要求任何空間,因此不會取得任何空間且不會轉譯。 您的所有面板程式碼可能運作良好,但如果面板是由零高度、零寬度組成的,您仍然看不到面板或其內容。
  • 在覆寫中,避免將子元素強制轉換為 FrameworkElement,並使用根據配置計算的屬性,特別是 ActualWidthActualHeight。 對於大多數常見場景,您可以讓邏輯以子系的 DesiredSize 值為基礎,並且不需要子元素任何與 HeightWidth 相關的屬性。 對於特殊情況,您知道元素的類型並具有其他資訊 (例如影像檔案的自然大小),您可以使用元素的專門資訊,因為它不是配置系統主動更改的值。 將配置計算屬性作為配置邏輯的一部分會大大增加定義無意配置迴圈的風險。 這些迴圈會導致無法建立有效配置的情況,如果迴圈無法復原,系統可能會擲回 LayoutCycleException
  • 面板通常在多個子元素之間劃分可用空間,儘管空間劃分的具體方式各不相同。 例如,Grid 實作的配置邏輯使用其 RowDefinitionColumnDefinition 值將空間劃分為 Grid 資料格,支援星形大小和像素值。 如果它們是像素值,則每個子系可用的大小是已知的,因此這就是作為方格樣式 Measure 的輸入大小傳遞的值。
  • 面板本身可以為項目之間的邊框間距引入保留空間。 如果執行此操作,請確保將測量值公開為與 Margin 或任何 Padding 屬性不同的屬性。
  • 元素的 ActualWidthActualHeight 屬性值可能會以先前的配置傳遞為基礎。 如果值發生變化,如果有特殊邏輯要執行,應用程式 UI 程式碼可以在元素上放置 LayoutUpdated 的處理程序,但面板邏輯通常不需要透過事件處理檢查來變更。 配置系統已經決定何時重新執行配置,因為配置相關的屬性更改了值,並且在適當的情況下自動呼叫面板的 MeasureOverrideArrangeOverride

ArrangeOverride

ArrangeOverride 方法有一個 Size 傳回值,當配置中的父系在面板上呼叫 Arrange 方法時,配置系統會在轉譯面板本身時使用該值。 通常輸入的 FinalSizeArrangeOverride 傳回的 Size 是相同的。 如果不是,則表示面板正在嘗試使自己的大小與配置中其他參與者聲稱可用的大小不同。 最終大小是以先前透過面板程式碼執行配置的測量通道為基礎,因此傳回不同的大小並不正常:這代表您故意忽略測量邏輯。

不要傳回具有 Infinity 元件的 Size。 嘗試使用這樣的 Size 會擲回內部配置例外狀況。

所有 ArrangeOverride 實作都應迴圈執行 Children,並在每個子元素上呼叫 Arrange 方法。 與 Measure 一樣,Arrange 沒有傳回值。 與 Measure 不同,不會將計算屬性設為結果 (但是,相關元素通常會觸發 LayoutUpdated 事件)。

以下是 ArrangeOverride 方法的基本架構:

protected override Size ArrangeOverride(Size finalSize)
{
    //loop through each Child, call Arrange on each
    foreach (UIElement child in Children)
    {
        Point anchorPoint = new Point(); //TODO more logic for topleft corner placement in your panel
       // for this child, and based on finalSize or other internal state of your panel
        child.Arrange(new Rect(anchorPoint, child.DesiredSize)); //OR, set a different Size 
    }
    return finalSize; //OR, return a different Size, but that's rare
}

配置的排列傳遞可能會在沒有測量傳遞之前發生。 不過,只有當配置系統確定沒有屬性變更而影響先前的測量時,才會發生這種情況。 例如,如果對齊方式發生變化,則無需重新測量該特定元素,因為當其對齊方式選擇發生變化時,其 DesiredSize 不會發生變化。 另一方面,如果配置中任何元素的 ActualHeight 發生變化,則需要新的測量通道。 配置系統自動偵測真實的測量變更,並再次叫用測量傳遞,然後執行另一個排列傳遞。

Arrange 的輸入採用 Rect 值。 建構此 Rect 最常見的方法是使用具有 Point 輸入和 Size 輸入的建構函式。 該 Point 是元素週框方塊左上角應放置的點。 Size 是用於轉譯該特定元素的維度。 您通常會使用該元素的 DesiredSize 作為此 Size 值,因為為配置中涉及的所有元素建立 DesiredSize 是配置測量傳遞的目的。 (測量傳遞會以迭代方式確定元素的整體大小,以便配置系統在進入排列傳遞後可以最佳化元素的放置方式。)

通常 ArrangeOverride 實作之間的不同之處在於面板決定如何排列每個子系的 Point 元件的邏輯。 絕對定位面板 (例如 Canvas) 會使用透過 Canvas.LeftCanvas.Top 值從每個元素取得的明確放置資訊。 諸如 Grid 之類的空間劃分面板將進行數學運算,將可用空間劃分為資料格,每個資料格將有一個 x-y 值來指示其內容的放置和排列位置。 調適型面板 (例如 StackPanel) 可能會擴充自身,以符合其方向維度的內容。

除了直接控制和傳遞給 Arrange 之外,配置中的元素仍然存在其他定位影響。 它們來自 Arrange 的內部本機實現,它是所有 FrameworkElement 衍生類型所共有的,並透過一些其他類型 (例如文字元素) 進行增強。 例如,元素可以具有邊界和對齊方式,有些元素可以具有邊框間距。 這些屬性經常會相互作用。 如需詳細資訊,請參閱對齊、邊界及邊框間距

面板和控制項

避免將功能放入應建置為自訂控制項的自訂面板中。 面板的作用是呈現其中存在的任何子元素內容,作為自動執行的配置的函式。 面板可能會將裝飾新增至內容 (類似於 Border 在其呈現的元素周圍添加邊框的方式),或執行其他與配置相關的調整,例如邊框間距。 但是,當將視覺化樹狀結構輸出延伸到報告和使用來自子系的資訊之外時,您就應該做到這種程度。

如果使用者可以存取任何互動,您應該撰寫自訂控制項,而不是面板。 例如,面板不應該向其呈現的內容添加捲動檢視區,即使目標是防止裁剪,因為捲軸、拇指等是互動式控制部分。 (畢竟內容可能有捲軸,但您應該將其留給子系的邏輯。請勿透過添加捲動作為配置作業來強制執行。) 您可以建立控制項並撰寫自訂面板,當涉及在該控制項中呈現內容時,該面板會在該控制項的視覺化樹狀結構中發揮重要作用。 但該控制項和面板應該是不同的程式碼物件。

控制項和面板之間的區別之所以很重要是因為 Microsoft UI 自動化和協助工具。 面板提供視覺配置行為,而不是邏輯行為。 UI 元素的視覺呈現方式對於協助工具來說並不是一個很重要的 UI 層面。 協助工具是公開應用程式元件,這些元件在邏輯上對於理解 UI 很重要。 當需要互動時,控制項應該向 UI 自動化基礎架構公開互動的可能性。 如需詳細資訊,請參閱自訂自動化對等

其他配置 API

還有一些其他 API 屬於配置系統的一部分,但不是由 Pane 宣告。 您可以在面板實作或使用面板的自訂控制項中使用它們。

  • UpdateLayoutInvalidateMeasureInvalidateArrange 是起始版面配置階段的方法。 InvalidateArrange 可能不會觸發測量傳遞,但其他兩個會觸發。 切勿從配置方法覆寫中呼叫這些方法,因為它們幾乎一定會導致配置迴圈。 控制程式碼通常也不需要呼叫它們。 大部分的配置層面都是透過偵測框架定義的配置屬性 (例如 Width 等) 的變更來自動觸發的。
  • LayoutUpdated 是元素的一些配置層面變更時所觸發的事件。 這不是面板特有的; 此事件由 FrameworkElement 定義。
  • SizeChanged 是只在版面配置階段完成後,並指出 ActualHeightActualWidth 因此有所變更時才會觸發的事件。 這是另一個 FrameworkElement 事件。 在某些情況下,LayoutUpdated 會觸發,但 SizeChanged 不會。 例如,內部內容可能會重新排列,但元素的大小沒有改變。

參考

概念