1. 程式人生 > >Visual->UIElement->FrameworkElement,帶來更多功能的同時也帶來了更多的限制

Visual->UIElement->FrameworkElement,帶來更多功能的同時也帶來了更多的限制

在 WPF 或 UWP 中,我們平時開發所遇到的那些 UI 控制元件或元件,都直接或間接繼承自 Framework。例如:GridStackPanelCanvasBorderImageButtonSlider。我們總會自然而然地認為這些控制元件都是有大小的,它們會在合適的位置顯示自己,通常不會超出去。但是,FrameworkElement 甚至是 Control 用得久了,都開始忘記 VisualUIElement 帶給我們的那些自由。

閱讀本文將瞭解我們熟知的那些功能以及限制的由來,讓我們站在限制之外再來審視 WPF 的視覺化樹,再來看清 WPF 各種控制元件屬性的本質。

寬度和高度

如果問 Width/Height 屬性來自誰,只要在 WPF 和 UWP 裡混了一點兒時間都會知道——FrameworkElement。隨著 FrameworkElement 的寬高屬性一起帶來的還有 ActualWidthActualHeightMinWidthMinHeightMaxWidthMaxHeight。正是這些屬性的存在,讓我們可以直觀地給元素指定尺寸——想設定多少就設定多少。

然而……當你把寬或高設定得比父容器允許的最大寬高還要大的時候呢?我們會發現,控制元件被“切掉”了。

▲ 被切掉的橢圓

然而,因佈局被“切掉”這一特性也是來自於 FrameworkElement

UIElement 佈局時即便空間不夠也不會故意去將超出邊界的部分切掉,這一點從其原始碼就能得到證明:

/// <summary>
/// This method supplies an additional (to the <seealso cref="Clip"/> property) clip geometry
/// that is used to intersect Clip in case if <seealso cref="ClipToBounds"/> property is set to "true".
/// Typcally, this is a size of layout space given to the UIElement.
/// </summary>
/// <returns>Geometry to use as additional clip if ClipToBounds=true</returns>
protected virtual Geometry GetLayoutClip(Size layoutSlotSize)
{
    if(ClipToBounds)
    {
        RectangleGeometry rect = new RectangleGeometry(new Rect(RenderSize));
        rect.Freeze();
        return rect;
    }
    else
        return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

只會在 ClipToBounds 設定為 true 的時候進行矩形切割。

然而 FrameworkElement 的切掉邏輯就複雜多了,鑑於有上百行,就只貼出連結 FrameworkElement.GetLayoutClip。其處理了各種佈局、變換過程中的情況。

由於 FrameworkElement 的出現是為了讓我們程式設計中像對待一個有固定尺寸的物體一樣,所以也在切除上模擬了這樣的空間有限的效果。

如果希望不被切掉,有兩種方法修正:

  1. 確保佈局的時候所需尺寸不大於可用尺寸(一點也不能大於,就算是 double 精度問題導致的細微偏大都不行)
    • MeasureOverride 返回的尺寸不大於引數傳入的尺寸
    • ArrangeOverride 返回的尺寸不大於引數傳入的尺寸
  2. 重寫 GetLayoutClip 方法,並返回 null(或者寫成 UIElement 那樣)

佈局系統

提及 MeasureOverrideArrangeOverride,大家都會認為這是 WPF 佈局系統給我們提供的兩個可供重寫的方法。然而,這兩個方法其實也是 FrameworkElement 才提供的。

真正佈局的方法是 MeasureArrange,而可供重寫的方法是 MeasureCoreArrangeCore。這兩組方法均來自於 UIElement,而佈局系統其實是 UIElement 引入的。

那麼 FrameworkElement 做了什麼呢?它密封了 MeasureCoreArrangeCore 這兩個佈局的重寫方法,以便能夠處理 WidthHeightMinWidthMinHeightMaxWidthMaxHeightMargin 這些屬性對佈局的影響。

你覺得 WidthHeight 屬性是元素的最終寬高嗎?我們在 寬度和高度 一節中已經說了不是,前面一段也說了不是——它們真的只是佈局屬性!然而,這真的很容易形成誤解!Width``Height 屬性其實和 MinWidth``MinHeightMaxWidth``MaxHeight 是完全一樣的用途,只是在佈局過程中為計算最終尺寸提供的佈局限制而已。只不過 MinWidth``MinHeightMaxWidth``MaxHeight 用大於和小於進行尺寸的限制,而 Width``Height 用等於進行尺寸的限制。最終的尺寸依然是 ActualWidth``ActualHeight,而這個值跟 RenderSize 其實是一個意思,因為內部獲取的就是 RenderSize

值得注意的是,ActualWidth``ActualHeightRenderSize 一樣,是佈局結束後才會更新的,開發中需要如果修改了屬性立即獲取這些值其實必然是舊的,拿這些值進行計算會造成錯誤的尺寸資料。

順便吐槽一下:其實微軟是喜歡用 Core 來作為子類重寫方法的字尾的,比如 FreezableEasingFunction 都是用 Core 字尾來處理重寫。Override 字尾純屬是因為 UIElement 把這個名字用了而已。

螢幕互動

UIElement 中存在著佈局計算,FrameworkElement 中存在著帶限制的佈局計算,這很容易讓人以為螢幕相關的座標計算會存在於 UIElement 或者 FrameworkElement 中。

然而其實 UIElement 或者 FrameworkElement 只涉及到控制元件之間的座標計算(TranslatePoint),真正涉及到螢幕座標的轉換是位於 Visual 中的,典型的是這幾個:

  • TransformToAncestor
  • TransformToDescendant
  • TransformToVisual
  • PointFromScreen
  • PointToScreen

所以其實如果希望做出非常輕量級的高效能 UI,繼承自 Visual 也是一個大膽的選擇。當然,真正遇到瓶頸的時候,繼承自 Visual 也解決不了多少問題。

樣式和模板

FrameworkElement 開始有了樣式(Style),Control 開始有了模板(Template)。而模板極大地方便了樣式定製的同時,也造成了強大的效能開銷,因為本來的一個 Visual 瞬間變成了幾個、幾十個。一般情況下這根本不會是效能瓶頸,然而當這種控制元件會一次性產生幾十個甚至數百個(例如表格)的時候,這種瓶頸就會非常明顯。

總結容易出現理解偏差的幾個點

  1. WidthHeight 屬性其實只是為佈局過程中的計算進行限制而已,跟 MinWidthMinHeightMaxWidthMaxHeight 沒有區別,並不直接決定實際尺寸。
  2. 如果發現元素佈局中被切掉了,這並不是不可避免的問題;因為切掉是 FrameworkElement 為我們引入的特性,不喜歡可以隨時關掉。
  3. 微軟對於子類重寫核心邏輯的方法喜歡使用 Core 字尾,佈局中用了 Override 只是因為名字被佔用了。
  4. Visual 就可以計算與螢幕座標之間的轉換。
  5. 模板(Template)會額外產生很多個 Visual,有可能會成為效能瓶頸。

參考資料

--------------------- 本文來自 walter lv 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/WPwalter/article/details/78619688?utm_source=copy