目錄
- 溫習Measure和Arrange過程
- 現有控制項對Measure和Arrange的使用
- 自訂控制項示範Measure和Arrange
返回目錄
溫習Measure和Arrange過程
首先UIElement.Measure的參數是可用的空間(Size對象),這個空間通常代表著父控制項留給你顯示的可用空間,接著UIElement內部的MeasureCore會被調用,該方法會去決定調用MeasureOverride的大小參數。具體過程是先判斷大小屬性值是否被顯示設定,如果是的話,直接使用設定的值,這就是為什麼如果你強行設定一個控制項的Width和Height後,它總會保持設定的大小(當然如果超出了父控制項的規劃大小,WPF會根據IsClipToBounds屬性來決定是否裁剪它)。當然如果沒有被顯示設定,那麼MeasureCore會根據可用大小和當前控制項的屬性(比如Margin)來決定最終傳入MeasureOverride的可用大小參數。
接著MeasureOverride被調用,這個可以被子控制項改寫,返回的只會影響UIElement.DesiredSize屬性。
這個DesiredSize是計算後的包括Margin的結果,而且被三個條件所制約(優先順序由上到下):
- Measure中傳入的可用大小參數
- 顯示設定的屬性大小
- MeasureOverride返回的大小
讓我們做一些樣本:
<Grid Width="200" Height="200">
<TextBlock Margin="50">A</TextBlock>
</Grid>
顯然LayoutSlot大小為200*200。如果我們把TextBlock的Width和Height都設定成300,TextBlock的DesiredSize還會是200*200,顯然他被Measure中的可用大小最先制約。
如果第一條限制通過的話,那麼後續限制會被應用,比如我們把TextBlock的大小設定成10*10,那麼此時TextBlock的DesiredSize會成為:110*100。此時DesiredSize是控制項顯示設定的大小加上Margin大小。10+50*2=110。
最後如果TextBlock的大小不被設定,那麼DesiredSize就會使MeasureOverride的結果加上Margin,在我這裡結果是:107.74*115.96。
Arrange的過程則是決定UIElement.RenderSize屬性,整個過程和Measure類似但有不一樣的地方。首先,傳入Arrange的參數是LayoutSlot的位置和大小,是Rect對象。接著ArrangeCore做類似MeasureCore的工作去決定傳入ArrangeOverride大小參數,這個參數是Size,這個Size代表著最終需要顯示的可用大小。
ArrangeOverride返回的大小會影響RenderSize,但是注意不同於MeasureOverride那樣,ArrangeOverride影響的RenderSize不會受到Arrange方法的參數的限制,也就是說RenderSize只會受到顯示設定的屬性大小和ArrangeOverride傳回值這兩個限制的制約。至於MeasureOverride和ArrangeOverride的返回結果都要受到顯示設定的屬性大小影響,這個也是情理之中的。想象一下,如果你設定了元素的Width和Height但是最終他有可能不會按照規定大小去顯示,那將是多麼瘋狂的一件事啊。
返回目錄
現有控制項對Measure和Arrange的使用
為什麼要用Measure和Arrange兩個過程?我們從現有控制項的執行就已找到諸多答案。
Canvas是基於座標的控制項容器,因此他不會因為自身的大小而限制子容器的大小。看這樣一個代碼:
<Canvas Width="30">
<Button Width="300" Height="50"/>
</Canvas>
VS設計器很給力的顯示出這個複雜情況:
Canvas只有30寬度,但是Button則需要300,同時Canvas不應該限制Button大小。那麼此時在Measure階段,Canvas並沒有把MeasureOverride中的父控制項可用大小傳給Button,而是用Double.PositiveInfinity(代表無窮大)作為參數調用Button的Measure方法。然後Button設定好了自己的DesiredSize後,Canvas在Arrange過程中同樣沒有傳入自己ArrangeOverride的參數,而是直接把Button的DesiredSize作為Button的Arrange方法參數,這樣滿足Button的任何大小要求。
類似的控制項還有ScrollViewer。
還有Canvas的MeasureOverride總是返回0*0。也就是說如果你沒有直接設定Canvas的大小的話,他的DesiredSize總會是0*0。因此如果你放在StackPanel中,他不會被顯示,因為StackPanel會根據方向的不同只顯示控制項DesiredSize(另一個方向無限制),顯然0*0的DesiredSize會使Canvas不可見。如果你把Canvas放在Grid中,那麼Canvas的大小就是Grid所給的大小,因為Grid會向已有大小都交給子成員,如果你把Canvas放在ScrollViewer中,那麼Canvas也會無限大,因為ScrollViewer本身就是無限大。
也可以通過改寫MeasureOverride來建立一個DesiredSize返回最小值的Canvas,可以參考這篇文章:WPF:一個估量最小大小的Canvas
還有TextBlock總會已單行的形式延伸,只有設定了TextWrapping屬性後,TextBlock才會根據父控制項的可用空間限制來執行換行規劃。
這些成果都是Measure和Arrange配合的結果。
當然絕大多數情況,如果你不是寫自訂的Panel控制項的話,是不需要太多關心Measure和Arrange(這也使許多人都不是很瞭解Measure和Arrange)。同時WPF的諸多內建類型都有MeasureOverride和ArrangeOverride的執行,可以參考這篇文章:
WPF中內建類型的MeasureOverride和ArrangeOverride表現
比如Control類型的執行都是對第一個Visual Child進行測量。那麼我們可以直接建立一個類型繼承自Control,然後把一個Visual加入到VisualChildren中,不用改寫MeasureOverride和ArrangeOverride,一個顯示另一個控制項的自訂Control就實現了:
class ContentButton : Control
{
Button btn;
public ContentButton()
{
btn = new Button() { Content = "Mgen" };
base.AddVisualChild(btn);
}
protected override Visual GetVisualChild(int index)
{
return btn;
}
protected override int VisualChildrenCount
{
get
{
return 1;
}
}
}
把這個自訂控制項放在StackPanel中,背後的Button會直接顯示出來:
返回目錄
自訂控制項示範Measure和Arrange
下面自訂一個控制項示範有趣的Measure和Arrange。
我們建立一個在MeasureOverride中總返回50*50。然後在Arrange中總返回100*100的控制項:
class MyControl : Control
{
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
drawingContext.DrawRectangle(Brushes.Red, null, new Rect(new Point(), RenderSize));
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
base.ArrangeOverride(arrangeBounds);
return new Size(100, 100);
}
protected override Size MeasureOverride(Size constraint)
{
base.MeasureOverride(constraint);
return new Size(50, 50);
}
}
在StackPanel中,控制項會被顯示成100*50的。因為橫向StackPanel不做大小限制,所以DesiredSize被應用,而縱向對大小進行限制,RenderSize被應用。
在Grid或者其他不限制大小的容器中,大小會是100*100的,同時控制項不會根據可用控制項的改變而放大或者縮小,因為ArrangeOverride沒有使用arrangeBounds參數,而總是返回100*100的Size。
還可以建立一個縱向的StackPanel,但是不嘗試展開子成員的橫向寬度,效果如下:
代碼:
class MyPanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
var retSize = new Size();
foreach (UIElement ui in InternalChildren)
{
ui.Measure(new Size(availableSize.Width, availableSize.Height));
retSize.Height += ui.DesiredSize.Height;
retSize.Width = Math.Max(retSize.Width, ui.DesiredSize.Width);
}
return retSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
var next = new Point();
foreach (UIElement ui in InternalChildren)
{
ui.Arrange(new Rect(next, ui.DesiredSize));
next.Y += ui.RenderSize.Height;
}
return finalSize;
}
}