1. 程式人生 > >WPF/Silverlight Layout 系統概述——Measure

WPF/Silverlight Layout 系統概述——Measure

前言

在WPF/Silverlight當中,如果已經存在的Element無法滿足你特殊的需求,你可能想自定義Element,那麼就有可能會面臨重寫MeasureOverride和ArrangeOverride兩個方法,而這兩個方法是WPF/SL的Layout系統提供給使用者的自定義介面,因此,理解Layout系統的工作機制,對自定義Element是非常有必要的。那麼,究竟WPF/SL的Layout系統是怎麼工作的呢?接下來,我簡單的描述一下,然後,在後面的章節具體分析。

簡單來說,WPF的Layout系統是一個遞迴系統,他有兩個子過程,總是以呼叫父元素的Measure方法開始,以呼叫Ararnge方法結束,而進入每個子過程之後,父元素又會呼叫孩子元素的Measure,完成後,又呼叫孩子元素的Arrange方法,這樣一直遞迴下去。而對兩個子過程的一次呼叫,可以看作是一次會話,可以理解為下圖所示:

Layout Process Overview

這個會話可以用下面一段話描述:

子過程1: 父根據自己的策略給孩子一個availableSize,併發起對話,通過呼叫孩子的Measure(availableSize)方法,詢問孩子:你想要多大的空間顯示自己?孩子接到詢問後,根據父給的availableSize以及自己的一些限制,比如Margin,Width,等等,孩子回答:我想要XXX大小的空間。父拿到孩子給的期望的空間大小後,根據自己的策略開始真正給孩子分配空間,就進入第二個子過程。

子過程2: 父拿到孩子的期望空間後,再根據自己的情況,決定給孩子分配finalRect大小的矩形區域,然後他發起對話,呼叫孩子的Arrange(finalRect)給孩子說:我給你了finalRect這麼大的空間。孩子拿到這個大小後,會去佈置它的內容,並且佈置完成後,會告訴父:其實我用了XXX大小的空間來繪製我自己的內容。父知道後,什麼也沒說,還是按照分配給他的finalRect去安置孩子,如果孩子最終繪製的區域大於這個區域,就被父裁剪了。Layout過程完成。

通過上面兩個子過程的理解,或多或少對WPF的Layout系統有個初步的瞭解,接下來的章節,我具體描述Measure過程和Arrange過程具體做了哪些事情,幫助你跟深入的理解Layout系統。

預設條件

通過下面的一個預設場景,我們來展開Layout系統的講解。

假定:我們需要自定義一個Panel,型別為 *MyPanel* ,MyPanel的父為 *MyPanelParent* ,也是一個Panel;MyPanel的孩子為 *MyPanelChild* ,也是一個Panel。

切入點1:重寫MyPanelParent的MeasureOverride()和ArrangeOverride(),研究父如何影響孩子MyPanel的Layout;

切入點2:重寫MyPanel.MeasureOverride()和ArrangeOverride方法,研究自身有哪些屬性影響MyPanel的Layout,以及重寫這兩個方法時應該注意的點;

注意:後面的研究,我只基於Element的Width,也就是水平方向的維度,所有的資料都是隻設定水平方向的,垂直方向設定的跟水平方向一致,但不做描述。

Measure過程概述

1. 普通基類屬性對Measure過程的影響

請看下面的一些設定:

<Window x:Class="WpfApplication1.MainWindow"

Title="MainWindow" Height="522" Width="594" Loaded="Window_Loaded" xmlns:my="clr-namespace:WpfApplication1">

<Canvas>

<my:MyPanelParent x:Name="myPanelParent1" Height="400" Width="400" Background="Green" Canvas.Left="10" Canvas.Top="10">

<my:MyPanel Margin="10" x:Name="myPanel1" Background="Red" MinWidth="150" Width="200"  MaxWidth="250"/>

<my:MyPanel Margin="10" x:Name="myPanel2" Background="Red" MinWidth="150" Width="200" MaxWidth="250"/>

</my:MyPanelParent>

</Canvas>

</Window>

public class MyPanelParent:Panel

{

protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)

{

foreach (UIElement item in this.InternalChildren)

{

item.Measure(new Size(120, 120));//這裡是入口

}

return availableSize;

}

protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)

{

double x = 0;

foreach (UIElement item in this.InternalChildren)

{

item.Arrange(new Rect(x, 0, item.DesiredSize.Width, item.DesiredSize.Height));

x += item.DesiredSize.Width;

}

return finalSize;

}

}

public class MyPanel : Panel

{

protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)

{

foreach (UIElement item in this.InternalChildren)

{

item.Measure(availableSize);

}

return new Size(50, 50);//MyPanel 返回它期望的大小

}

protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)

{

double xCordinate = 0;

foreach (UIElement item in this.InternalChildren)

{

item.Arrange(new Rect(new Point(xCordinate, 0), item.DesiredSize));

xCordinate += item.DesiredSize.Width;

}

return finalSize;

}

}

在上面的設定之後,應用程式執行起來之後,Window的表現為:

Measure_Window_Appearance

分析一下設定:

MyPanel1.Width = 200, MyPanel1.MinWidth = 150, MyPanel1.MaxWidth = 250, MyPanel1.Margin = Thickness(10)

MyPanel1.Measure()傳入的引數為120*120,MyPanel1.MeasureOverride返回的引數為50*50

分析一下結果:

MyPanel1實際的畫出來的大小(紅色部分)是100*50

從結果可以看出,紅色的部分受多個因素的影響,有人要問,我已經設定了MyPanel.Width=200,可是怎麼畫出來的Width卻是100;MyPanel.Height沒設定,可是畫出來的卻是50,為什麼不是其他值。接下來我通過Measure的流程圖說明一下這個結果是怎麼來的:

Measure Flow1

看了上圖,有些人可能會看出一些端倪,也可能還不是很清晰,我按照自己的理解總結一下Measure過程究竟想幹什麼?

1. 第一點很清晰,MyPanelParent呼叫MyPanel.Measure的過程是想得到MyPanel.DesiredSize,MyPanelParent需要在Arrange孩子MyPanel時,參考孩子的DesiredSize,決定將孩子MyPanel安置多大的空間。

2. MyPanel.DesiredSize是包含Margin以及內容的大小空間

3. MyPanel.MeasureOverride傳入的引數constrainedSize,是基類的實現刨去Margin的大小,然後按照MyPanel對MinWidth,MaxWidth,Width的設定計算的一個MyPanel想要的值,我們自定義時在MeasureOverride當中不需要關心自己的Margin,以及其他基類上影響Layout的屬性,只要考慮在給定引數的範圍類安排自己的內容區域;MyPanel.MinWidth,Width, MaxWidth的設定都是針對內容區域的,不含Margin部分

4. 如果不設定Width,那麼可以在MeasureOverride返回的時候返回一個期望的內容區域大小,它會被MinWidth和MaxWidth再調整一下,調整後,還有待於MyPanelParent的衡量(旁白:別瞎折騰,也別玩Layout系統,都設定MinWidth,MaxWidth,就乖乖的呆在這個範圍內。)

5. 不論MyPanel怎麼設定自己的Width,MinWidth,MaxWidth,以及在MeasureOverride返回一個大小,來表明自己期望多大的空間顯示自己的內容,但這些都僅僅是期望的,期望是美好的,現實是殘酷的,這一切還必須限定在MyPanel.Measure開始時傳入的引數availableSize刨去MyPanel.Margin後的範圍內,小於這個範圍就滿足,大於這個範圍就被裁斷。(可憐呀,總是受制於父)

6. 影響Measure過程的引數和屬性存在一個優先順序的,大概如下所示:

Measure方法引數availableSize>MinWidth,Width,MaxWidth > MeasureOverride返回值

2. Transform對Measure過程的影響

通過上面的過程,我們已經大概瞭解了Measure過程的工作方式,以及各個屬性是如何影響的。但是還有一個屬性我們沒有提及,但它對Measure的過程也影響甚大,這就是LayoutTransform。通過下面的兩段分析,你會看到這個屬性的具體表現。

設定1:

<Window x:Class="WpfApplication1.MainWindow"

Title="MainWindow" Height="522" Width="594" Loaded="Window_Loaded" xmlns:my="clr-namespace:WpfApplication1">

<Canvas>

<my:MyPanelParent x:Name="myPanelParent1" Height="400" Width="400" Background="Lime" Canvas.Left="10" Canvas.Top="10">

<my:MyPanel Margin="10" x:Name="myPanel1" Background="Red" Width="200">

<my:MyPanel.LayoutTransform>

<RotateTransform Angle="90"/>

</my:MyPanel.LayoutTransform>

</my:MyPanel>

<my:MyPanel Margin="10" x:Name="myPanel2" Background="Red" MinWidth="150" MaxWidth="250"/>

</my:MyPanelParent>

</Canvas>

</Window>

public class MyPanelParent:Panel

{

protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)

{

foreach (UIElement item in this.InternalChildren)

{

item.Measure(new Size(1000, 800));

}

return availableSize;

}

protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)

{

double x = 0;

foreach (UIElement item in this.InternalChildren)

{

item.Arrange(new Rect(x, 0, item.DesiredSize.Width, item.DesiredSize.Height));

x += item.DesiredSize.Width;

}

return finalSize;

}

public class MyPanel : Panel

{

protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)

{

foreach (UIElement item in this.InternalChildren)

{

item.Measure(availableSize);

}

return new Size(80, 50);

}

protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)

{

double xCordinate = 0;

foreach (UIElement item in this.InternalChildren)

{

item.Arrange(new Rect(new Point(xCordinate, 0), item.DesiredSize));

xCordinate += item.DesiredSize.Width;

}

return finalSize;

}

}

執行的表現為:

Measure_Window_Transform

分析一下設定:

MyPanel1.LayoutTransform = new RotateTransform(90)//旋轉了90度

MyPanel1.Width = 200

MyPanel1.Margin = Thickness(10)

MyPanel1.Measure()傳入的引數為1000*800,MyPanel1.MeasureOverride返回的引數為80*50.

分析一下結果:

MyPanel1實際的畫出來的大小是50×200,明顯是被旋轉了90度。

執行起來,你會發現最終的MyPanel1.DesiredSize在Measure過程之後為70×220,也就是說,它是被Transform之後的大小,明顯是被旋轉過的。另外,觀察MyPanel.MeasureOverride傳入的引數,為200×980,根據上一節對Measure過程的分析,MeasureOverride傳入的引數寬為200是可預知的,因為我們設定了MyPanel1.Width為200,但Height為980,明顯是MyPanel.Measure傳入的寬1000減去2*10等於980,看來在進入MeasureOverride之前,Layout系統也處理了LayoutTransform對Measure過程的影響,它希望MeasureOverride不要關心自身LayoutTransform的影響。MeasureOverride結束後,返回值為80×50,根據上一節對Measure過程的分析,寬為80被調節為符合自己的設定,為200,由於高沒有設定,這個50肯定會保留,因此最後在沒有Transform之前的DesiredSize應該是220×70,然而基類會將MeasureOverride返回的大小再進行一次Transform,達到最終的DesiredSize的大小,以便Arrange的時候分配合適的空間來容納MyPanel的大小。

如果你將上面例子的MyPanel1.LayoutTransform設定成ScaleTransform:

<Window x:Class="WpfApplication1.MainWindow"

Title="MainWindow" Height="522" Width="594" Loaded="Window_Loaded" xmlns:my="clr-namespace:WpfApplication1">

<Canvas>

<my:MyPanelParent x:Name="myPanelParent1" Height="400" Width="400" Background="Lime" Canvas.Left="10" Canvas.Top="10">

<my:MyPanel Margin="10" x:Name="myPanel1" Background="Red" Width="200">

<my:MyPanel.LayoutTransform>

<ScaleTransform ScaleX="2" ScaleY="2"/>

</my:MyPanel.LayoutTransform>

</my:MyPanel>

<my:MyPanel Margin="10" x:Name="myPanel2" Background="Red" MinWidth="150" MaxWidth="250"/>

</my:MyPanelParent>

</Canvas>

</Window>

然後再觀察myPanel.MeasureOverride傳入的引數,為200×390,首先200是可預知的,因為設定了Width屬性,而390是怎麼回事呢,其實為Measure傳入的1000×800的高800減去Margin為20後得到780,然後根據LayoutTransform將高縮小2倍之後得到的390,因此傳入的引數就是200×390,可見,Layout系統,在進入MeasureOverride之前,他希望,MeasureOverride只關心內容怎麼佈置,而不需要關心基類屬性的設定對MeasureOverride的影響。由於MeasureOverride的返回值依然是80×50,可推理,80被調節為200,50被保留,沒有Transform之前的值應該是200×50。因為基類還要進行Transform,因此,內容區域的真實的大小應該是400×100,再加上Margin之後,最終的DesiredSize肯定為420*120,你可以嘗試除錯給出的程式碼。

3. Measure過程的總結

Measure過程的總結

通過上面的過程分析,我相信你或多或少對WPF的Layout系統的Measure過程有了更進一步的瞭解,其實還有一些因素影響Measure的過程,比如UseLayoutRounding屬性,在進入MeasureOverride之前和之後,基類都被將引數根據DPI進行Rounding,這個過程知道就行了,不需要在自己的MeasureOverride裡面關心。我們總結一下哪些屬性和引數會影響Measure的過程:MyPanel.Measure傳入的引數availableSize,MyPanel的MinWidth, Width, MaxWidth,Margin,UseLayoutRounding,LayoutTransform,MeasureOverride的返回值。

Measure過程相關問題解答

Q1:什麼是Layout Slot? 什麼時候能獲取到?在哪裡獲取?

Layout Slot就是呼叫Arrange方法的時候,傳入的引數finalRect,這是父分配給子的容納Margin以及內容區域的矩形空間;

當Arrange過程結束後,你可以拿到;

通過呼叫靜態類LayoutInformation.GetLayoutSlot(FrameworkElement element)方法可以拿到。

Q2:什麼是Layout Clip?什麼時候能獲取到?在哪裡獲取?

Layout Clip 只的是當內容區域要繪製的大小,大於LayoutSlot刨去Margin區域後的大小,這時候,內容區域就會被Clip,超出的部分會被Clip掉,而剩下的可顯示的部分就是Layout Clip,他是一個Geometry。

Arrange過程結束後,可以拿到;

通過呼叫靜態類LayoutInformation.GetLayoutClip(FrameworkElement element)方法可以拿到。如果內容區域可以完全顯示

在Layout Slot刨去Margin的區域內,LayoutClip為Null。

Q3:在父的MeasureOverride當中呼叫孩子的Measure方法時,傳入的引數有沒有什麼限制?

有,確保availableSize.Width和Height不是NaN;但可以是Infinity

Q4:在進入自己的MeasureOverride方法後,面對引數我該咋辦?

首先,心裡應該明白,傳入的引數已經是基類刨去自己的Margin,並且考慮了基類影響Measure過程的屬性之後的值。

其次,看自身有沒有自定義的,並且影響Layout的屬性,根據自己的內容要求,或者孩子的情況,呼叫孩子的Measure方法,並傳入希望孩子限定在多大範圍內空間。

最後,返回一個自己期望的Size。

這裡應該注意的點:

1. 呼叫孩子的Measure方法時,傳入的引數,是你限定孩子的最大空間,用來顯示孩子的Margin以及內容區域的,而孩子不管最終期望的大小有多少,都會被你給他的availableSize裁剪。

2. 根據自身的策略返回一個期望的值,這個期望的值應該是在自己的MinWidth,Width,MaxWidth限定的範圍呢,如果沒有,基類還會強行調整。

3. 基類調整後的值還會被父傳入的availableSize再次調整,返回值不能大於父傳入的引數減去Margin之後的值

Q5: MeasureOverride的返回值有沒有什麼限制?

有,除了如Q5所說,返回值會被重新調節之外,必須保證自己定義的MeasureOverride的返回值是一個確定的值,不是NaN,也不是Infinity。如果小於0時,基類會強制調節為0.

Q6:DesiredSize究竟是什麼?

DesiredSize是Measure過程結束後確定的一個大小,他是孩子期望父在Arrange的時候給他分配的大小,包含孩子的Margin區域以及內容區域。如果父在ArrangeOverride的時候,需要呼叫孩子的Arrange方法時,如果根據策略他希望滿足孩子的期望大小,那麼,呼叫孩子的Arrange方法應該傳入孩子DesiredSize大小的Rect。

Q7:孩子的DesiredSize確定後,是不是最終就可以得到這麼大的空間?

不一定。就像Q7答案所講,根據父的策略而定,如果父期望分配給孩子期望的大小,就在呼叫孩子的Arrange方法時,傳入DesiredSize大小的Rect,比如Canvas,Canvas的孩子的大小就是孩子的DesiredSize那麼大;而如果父是根據自身的設定決定,就不會參考孩子的DesiredSize,傳入的當然是自己只能分配給孩子的空間,比如UniformGrid,他根據自身的可用大小,根據行數列數均分空間,然後,均分後的空間分配給每個孩子,而不考慮孩子的DesiredSize。給孩子分配空間,這個過程是在Arrange階段的。

我們在進行WPF/Silverlight開發時,還可以藉助一些工具來助力開發過程。ComponentOne Studio Enterprise 是一款專注於企業應用的.NET全功能控制元件套包,支援WinForms、WPF、UWP、Xamarin、ASP.NET MVC等多個平臺,幫助在縮減成本的同時,提前交付豐富的桌面、Web和移動企業應用。

本文是由葡萄城技術開發團隊釋出,轉載請註明出處:葡萄城官網

瞭解開放易用的 Web 生成平臺,請前往活字格Web應用生成平臺