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

WPF/Silverlight Layout 系統概述——Arrange

Arrange過程概述

普通基類屬性對Arrange過程的影響

我們知道Measure過程是在確定DesiredSize的大小,以便Arrange過程參考這個DesiredSize,確定給MyPanel分配多少空間,但是DesiredSize只是作為參考,在有些用例下,MyPanelParent在呼叫MyPanel.Arrange的時候,會根據父的實際策略指定MyPanel.Arrange方法的引數,而不是直接指定MyPanel.DesiredSize的大小,比如Grid。因此,最終MyPanel應該如何呈現,決定權還是在Layout系統的Arrange過程當中。那麼Arrange過程最終確定哪些資料呢?主要是MyPanel.RenderSize,MyPanel.VisualOffset以及VisualTransform三個屬性。再說的清楚點就是確定Layout Slot以及最終繪製的位置和區域。而LayoutSlot本質上就是MyPanelParent呼叫MyPanel.Arrange時,傳入的引數finalRect.

請看下面的設定:

<Window x:Class="WpfApplication1.MainWindow"

Title="MainWindow" Height="458" Width="442" 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  x:Name="myPanel1" Background="Red"/>

<my:MyPanel  x:Name="myPanel2" Background="Red"/>

</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(50, 50);//MyPanel期望50×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;

}

}

根據上面的設定,我們沒有設定一些相關的屬性,比如Width,MinWidth,MaxWidth,另外,呼叫MyPanel.Measure時傳入的值也是比較大,可見MyPanelParent給孩子MyPanel足夠的空間去安排自己的內容,而MyPanel.MeasureOverride返回了一個50×50的大小,根據『普通基類屬性對Measure過程的影響』一節對影響Measure的引數的優先順序的總結,我們推理,目前MyPanel.DesiredSize應該是50×50,因為沒有Margin。另外,Arrange的時候呼叫MyPanel.Arrange傳入的引數也正好是MyPanel.DesiredSize,MyPanel.ArrangeOverride的返回值依然是傳入的引數大小,沒有變化,因此,我們推理,執行起來的MyPanel大大小就應該是50×50的紅色矩形區域,具體請看下圖:

Arrange_Window_1

如果你嘗試修改MyPanel.HorizontalAlignment,你會發現,這個屬性不管怎麼設定,MyPanel的位置和大小始終不會改變,這究竟是為什麼呢?

其實,HorizontalAlignment是控制當MyPanel的最終希望的大小,小於父MyPanelParent分配的LayoutSlot(呼叫MyPanel.Measure方法傳入的引數finalRect)時,MyPanel被安置的位置或大小。

如果將上面的例子的MyPanel.Arrange的引數改為100×100:

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, 100,100));

x += 100;

}

return finalSize;

}

}

你看到的效果,就是另外一番景象了。這時候MyPanel期望的是50×50,LayoutSlot為100×100,那麼50×50就可以根據HorizontalAlignment或者VerticalAlignmet的設定起作用了。

HorizontalAlignment=Horizontal

HorizontalAlignment=Stretch

Arrange_Window_Hori_1 Arrange_Window_Hori_2

如果讓LayoutSlot為30×30,整個Panel又會是什麼樣子呢?可以預見的是HorizontalAlign是不會起作用了,因為期望的大小還要大於父分配的layout slot.執行後結果:

Arrange_Window_2

接下來,我們再新增修改一下設定,最後給出影響Arrange過程的因素,以及最終Render的結果。

請看程式碼:

<Window x:Class="WpfApplication1.MainWindow"

Title="MainWindow" Height="458" Width="442" 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  x:Name="myPanel1" Background="Red" HorizontalAlignment="Left" Width="90"/>

<my:MyPanel  x:Name="myPanel2" Background="Red"/>

</my:MyPanelParent>

</Canvas>

</Window>

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);

}

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 new Size(80, 80);//MyPanel希望安排的最終大小

}

}

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, 100,100));//父安排的LayoutSlot

x += 100;

}

return finalSize;

}

}

執行效果如下所示:

Arrange_Window_3

設定分析:

MyPanel.Width=90,

MyPanel.DesiredSize = 90*50(通過Measure過程之後)

MyPanel.Arrange傳入的finalRect引數為100×100,大於DesiredSize

MyPanel.ArrangeOverride返回80×80

分析結果:

最終繪製的大小是80×80,不是期望的DesiredSize=90×50(根據Measure過程分析得到),而是MyPanel.ArrangeOverride返回的80×80,可見ArrangeOverride的返回值策略跟MeasureOverride返回值的處理是不一樣的,他是多少就是多少,不會受MinWidth,Width,MaxWidth的限制。然而,不受限制這一說法並不完全正確,當MyPanel.Width小於80時,繪製的大小會跟隨Width的設定,也就是說取兩者的最小值。前面說過Arrange過程是在確定RenderSize,那麼Arrange完成後,MyPanel.RenderSize是多少呢?為MyPanel.RenderSize = 80*80,也就是ArrangeOverride的返回值;

如果,你將MyPanel.MinWidth設定為30,Width設定為50,限制MyPanel的大小,繪製區域就變小了,成了50:

Arrange_Window_4

跟Measure過程一樣,上面所做的一切必須限制在MyPanel.Arrange傳入的引數finalRect,也就是LayoutSlot當中。可以預見的是,如果父呼叫MyPanel.Arrange方法傳入的不是100×100,而是30×30,那麼,最終的可見繪製區域就是30×30.

!Arrange_Window_5.PNG!

請猜一下上面兩種修改過後,MyPanel.RenderSize變化成多少了?

有人會回答:如果設定了MyPanel.Width為50,顯示的也為50×50,MyPanel.RenderSize就為50×50;傳入MyPanel.Arrange的引數為30×30時,顯示的也是30×30,MyPanel.RenderSize就為30×30.

完全錯誤!上面兩種設定後,MyPanel.RenderSize依然是80×80,也就是ArrangeOverride的返回值。

其實RenderSize是沒有Clip的繪製區域的大小,而實際看見的繪製區域是把RenderSize經過MyPanel.Width以及LayoutSlot進行Clip之後的效果。

通過下面的流程圖,描述一下Arrange過程具體做了哪些事情:

Arrange Flow

通過上面的流程圖,我們已經瞭解了Arrange過程大體所做的事情。按照對Measure過程的闡述,總結一下Arrange過程的主要點:

1. 確定RenderSize,以及最終MyPanel被安置的空間。RenderSize就是ArrangeOverride的返回值,沒還有被裁剪過的值。

2. 確定Client區域和Ink區域,根據HorizontalAlignment和VerticalAlignment確定Ink區域的在Client區域當中的位置和所佔據的大小。Client區域是LayoutSlot減去Margin;Ink區域是ArrangeOverride返回的值被Width剪下之後,必須是兩者的最小值。

3. 確定Visual基類上面若干跟具體Render相關的設定,比如VisualOffset,VisualTransform,Clip等等。

Transform對Arrange的影響

跟Measure過程類似,Layout系統不希望ArrangeOverride關心自身Transform的影響。所有對RenderTransform以及LayoutTransform設定,在ArrangeOverride退出後,基類會處理,並且根據設定調節VisualTransform。而RenderSize,LayoutSlot都不會受Transform的影響。

Arrange過程的總結

除了上面提到的屬性或者引數對Arrange過程有影響外,其實內容,還有更多屬性影響這個過程,總結一下哪些屬性和引數會影響Arrange過程:MyPanel.Arrange傳入引數finalRect,Mypanel.MinWidth,Width,MaxWidth,Margin,DesiredSize,HorizoantalUseLayoutRounding,ClipToBounds,Clip,LayoutTransform,RenderTransform,RenderTransformOrigin。

Arrange過程相關問題回答

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

除了向Q8所說,如需要根據自己的策略,決定給孩子分配多大的空間之外,還需要保證這個finalRect引數不能為NaN,Infinity。因為,Arrange過程是最終確定孩子的LayoutSlot的時機,必須保證傳入的引數是個確定的值。

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

跟Measure過程類似。

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

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

最後,返回一個自己期望的Size,這個Size將會是RenderSize的值,不會被調整。

這裡應該注意的點:

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

2. 根據自身的策略返回一個finalSize,這個期望的值應該是小於等於Width,MaxWidth,如果沒有,基類還會強行調整,總是取最小值。

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

4. 只有當ArrangeOverrid的返回值小於LayoutSlot刨去Margin區域之後的空間,HorizontalAlignment和VeriticalAlignment才會起作用,否則不會起作用。因為,ArrangeOverride返回值,相當去內容區域,而LayoutSlot刨去Margin的區域是Client區域,而Alignment就是將內容區域安放在Client區域的過程。

Q3:ArrangeOverride的返回值有沒有什麼限制?

可以為NaN或則PositiveInfinity。

Q4:RenderSize究竟是什麼?

從程式碼實現當中說,RenderSize就是ArrangeOverride的返回值。

從邏輯上講,RenderSize就是沒有被Clip過的繪製區域的大小。最終顯示出來的大小,是RenderSize經過自己的設定,以及父的限制之後,呈現出來的大小。

Q5: ContentControlMeasureOverrideArrangeOverride過程有沒有什麼特殊之處?

沒有,他的Layout策略繼承自Control,僅僅是將Content屬性的設定作為Control的Visual孩子而已。

Q6: ControlMeasureOverrideArrangeOverride過程是什麼樣的?

MeasureOverride:Control的父給多大的空間,Control就將其全部給自己的孩子,並返回孩子的DesiredSize。

ArrangeOVerride:Control的父分配給孩子多大的空間,Control就將其全部給自己的孩子,並返回父給的內容區域作為RenderSize。

總結一點:Control的空間,就是他的孩子的空間。

Q7: ContentPresenterMeasureOverrideArrangeOverride是什麼樣子的?

跟Control一樣。

Q8: 能不能在MeasureOverride或者ArrangeOverride當中呼叫孩子的孩子,也就是孫子的Measure或者Arrange方法?

目前來看,WPF/SL是允許這樣做的。但是,通過研究Layout系統之後,發現這個系統是非常龐大和複雜,另外,Arrange的時候, 孩子的位置總是基於自己的父,另外,內容會發一些基於父的事件,比如,ChildDesiredSizeChanged之類的事件。因此,個人覺得,最好不要這樣做,誰也保證不了這樣的做法是否不會出問題,無疑中也增加了程式碼的可讀性。

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

影響WPF Layout的屬性總結以及跟Silverlight的不同之處

WPF

Silverlight

Measure過程

Margin,MinWidth,Width MaxWidth,MinHeight,Height, MaxHeight,UseLayoutRounding, LayoutTransform

Margin,MinWidth,Width, MaxWidth,MinHeight,Height, MaxHeight,UseLayoutRounding

Arrange過程

Margin,MinWidth,Width MaxWidth,MinHeight,Height, MaxHeight,UseLayoutRounding, LayoutTransform,RenderTransform,RenderTransformOrigin HorizontalAlignment,VeritcalAlignment

Margin,MinWidth,Width MaxWidth,MinHeight,Height, MaxHeight,UseLayoutRounding, RenderTransform,RenderTransformOrigin HorizontalAlignment,VeritcalAlignment,Clip

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

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