在Windows10 UWP開發平臺上內置的XMAL佈局面板包括RelativePanel、StackPanel、Grid、VariableSizedWrapGrid 和 Canvas。在開發淘寶UWP應用時,遇到以下業務場景。 業務場景 場景一:淘寶商品提供的一些消費者保障服務 場景二:淘寶商品的 ...
在Windows10 UWP開發平臺上內置的XMAL佈局面板包括RelativePanel、StackPanel、Grid、VariableSizedWrapGrid 和 Canvas。在開發淘寶UWP應用時,遇到以下業務場景。
業務場景
場景一:淘寶商品提供的一些消費者保障服務
場景二:淘寶商品的SKU屬性展示
實現分析
系統預設的面板容器控制項顯然不符合要求了。在WPF裡面有WrapPanel,但是在UWP應用裡面沒有,這個時候就需要自定義個Panel了來實現WrapPanel的功能,實現起來不是很複雜。在MSDN的文檔上已經給出了詳細的實現說明:Xaml自定義面板,主要就是自定義一個Panel的派生類,然後重寫(MeasureOverride 和 ArrangeOverride)方法。
以下是MSDN上對兩個方法的解釋說明
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; }
ArrangeOverride方法
ArrangeOverride 方法有 Size 返回值,當 Arrange 在面板上受到佈局中的父元素調用時,佈局系統將在呈現面板本身時使用該值。通常輸入 finalSize 和 ArrangeOverride 返回的 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 }
創建自定義Panel控制項
下麵用一個簡單的demo演示一下,就知道這兩個方法的作用了。
首先新建一個MyPanel類繼承自Panel類,將Mypanel的背景色設置成灰色,在MyPanel裡面放入6個Border控制項,每個Border控制項設置不同的背景顏色,固定Width和Height為100或者200,方便查看各個控制項的大小區域。
UI xaml:
<Page x:Class="AppArrange.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:AppArrange" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <local:MyPanel Background="Gray" HorizontalAlignment="Left" VerticalAlignment="Top"> <Border Background="Red" Width="100" Height="100"> <TextBlock Text="1" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> <Border Background="Green" BorderThickness="1" Width="100" Height="200"> <TextBlock Text="2" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> <Border Background="Yellow" Width="200" Height="100"> <TextBlock Text="3" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> <Border Background="OrangeRed" Width="100" Height="100"> <TextBlock Text="4" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> <Border Background="Orange" Width="100" Height="100"> <TextBlock Text="5" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> <Border Background="Orchid" Width="100" Height="100"> <TextBlock Text="6" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> </local:MyPanel> </Grid> </Page>
Code behind:
public class MyPanel : Panel { protected override Size MeasureOverride(Size availableSize) { return base.MeasureOverride(availableSize); } protected override Size ArrangeOverride(Size finalSize) { return base.ArrangeOverride(finalSize); } }
這個時候MeasureOverride和ArrangeOverride什麼都沒有做,如果直接運行會是什麼樣子呢?
這個時候界面就是一片空白。添加的6個Border控制項沒有顯示。Mypanel的灰色背景也沒有顯示,說明Mypanel的size是0,沒有顯示出來。
下麵來實現MeasureOverride方法,遍歷每個子控制項並調用Measure方法。
public class MyPanel : Panel { protected override Size MeasureOverride(Size availableSize) { foreach (FrameworkElement child in Children) { child.Measure(availableSize); } return availableSize; } protected override Size ArrangeOverride(Size finalSize) { return base.ArrangeOverride(finalSize); } }
這個時候Mypanel的面板顯示出來了,背景顏色是灰色,但是子控制項沒有顯示。
接下來,實現ArrangeOverride方法
protected override Size ArrangeOverride(Size finalSize) { double x = 0; double y = 0; foreach (FrameworkElement child in Children) { child.Arrange(new Rect(new Point(x, y), child.DesiredSize)); x += child.DesiredSize.Width; y += child.DesiredSize.Height; } return finalSize; }
運行結果:
子控制項出來了,超出Page範圍的會被遮住。這個時候就可以根據需要定義每個子控制項的(x,y)坐標進行佈局了。如果要橫向佈局,就遞增x坐標,如果縱向佈局就遞增y坐標。
橫向佈局
遞增x坐標
protected override Size ArrangeOverride(Size finalSize) { double x = 0; double y = 0; foreach (FrameworkElement child in Children) { child.Arrange(new Rect(new Point(x, y), child.DesiredSize)); x += child.DesiredSize.Width; // y += child.DesiredSize.Height; } return finalSize; }
運行結果
縱向佈局
遞增y坐標
protected override Size ArrangeOverride(Size finalSize) { double x = 0; double y = 0; foreach (FrameworkElement child in Children) { child.Arrange(new Rect(new Point(x, y), child.DesiredSize)); //x += child.DesiredSize.Width; y += child.DesiredSize.Height; } return finalSize; }
運行結果:
接下來的問題,就是橫向或者縱向佈局的時候,判斷何時該換行了。要換行就要計算依次排列的子控制項的寬和高,同時和Mypanel的大小進行比較是否超出邊界。
這其中還有個問題是MeasureOverride和 ArrangeOverride 都會返回一個size大小。這兩個大小有什麼不一樣嗎。
MeasureOverride 返回值:此對象在佈局過程中基於其對子對象分配大小的計算或者基於固定容器大小等其他因素而確定的它所需的大小。
ArrangeOverride 返回值:元素在佈局中排列後使用的實際大小。
MeasureOverride的輸入size和返回size
可以做幾個實驗看看實際效果:
1. 如果給Measure傳遞的值較小,例如比最小的子控制項還小:
protected override Size MeasureOverride(Size availableSize) { foreach (FrameworkElement child in Children) { child.Measure(new Size(50,50)); } return availableSize; }
運行結果
子控制項會被裁剪部分區域,顯示不完整,以適應較小的size。
2. 如果給Measure傳遞的值較大,比子控制項的大小要大。
protected override Size MeasureOverride(Size availableSize) { foreach (FrameworkElement child in Children) { child.Measure(new Size(400,400)); } return availableSize; }
運行結果
結果顯示還是正常的大小,沒有變化。
總結:
說明子控制項的DesiredSize的會受到Measure傳遞的大小的限制,過小就會被裁剪,過大,不受影響,以實際的DesiredSize顯示。Measure方法就是給控制項分配一個可以顯示的大小範圍。
下麵看看MeasureOverride返回值,實際上和Measure的作用是一樣的,給MeasureOverride方法的參數size是MyPanel的父控制項給MyPanel分配的顯示區域大小,實際上會受到MyPanel 的Width、Height、HorizontalAlignment、VerticalAlignment等設置的影響,這裡就不展開了。
如果子控制項的大小顯示區域超過了MyPanel的父控制項給MyPanel分配的顯示區域大小,子控制項的顯示區域會被裁剪。這個時候可以根據業務需要調整MeasureOverride 返回size或者調整每個子控制項的Measure輸入size,縮小子控制項,使每個子控制項都完整顯示出來。
下麵看看如果設置的MeasureOverride返回值過小是什麼效果
protected override Size MeasureOverride(Size availableSize) { foreach (FrameworkElement child in Children) { child.Measure(availableSize); } return new Size(150,150); }
運行結果:
灰色區域是容器的大小,各個子控制項已經超出容器控制項的大小範圍了,MyPanel的父控制項分配的大小是整個page的大小,在遍歷子控制項Border時分配給子控制項也是page的大小顯示區域,但是每個子控制項都設置了Width和Height,而child.Measure(availableSize)的大小比子控制項自己的size大,所以最後會顯示控制項實際的大小,不會受child.Measure(availableSize)的影響,但是MyPanel的MeasureOverride最後返回的時候,size被修改小了,這樣整個容器就顯示被修改後的大小,子控制項溢出邊界。
所以在自定義容器控制項的時候,MeasureOverride 方法返回的size應該是所有子控制項顯示區域的最小size。而計算顯示區域的最小size,應該根據子控制項的佈局方式來判斷。
ArrangeOverride的輸入size和返回size
需要註意的是:通常情況下ArrangeOverride輸入的size和返回的size一樣,如果返回的size過小,也會遇到和MeasureOverride返回的size過小一樣的顯示問題。所以不建議修改ArrangeOverride的返回size,直接將輸入size返回就可以了,ArrangeOverride方法主要作用是給子控制項定位坐標和大小,完成佈局。
有了以上的準備,應該就知道怎麼實現淘寶的業務了,主要是在水平方向上依次排列子控制項,然後自動換行。
首先在MeasureOverride裡面計運算元控制項需要顯示大小區域,在ArrangeOverride裡面根據子控制項的大小排列方式計算顯示坐標,實現自動換行。
protected override Size ArrangeOverride(Size finalSize) { double x = 0; double y = 0; double maxHeight = 0; foreach (FrameworkElement child in Children) { if (maxHeight < child.DesiredSize.Height) { maxHeight = child.DesiredSize.Height; } if ((x + child.DesiredSize.Width) > finalSize.Width) { x = 0; y += maxHeight; maxHeight = 0; } child.Arrange(new Rect(new Point(x, y), child.DesiredSize)); x += child.DesiredSize.Width; } return finalSize; }
運行結果:
如果要實現更複雜的功能,例如要同時支持可以橫向縱向排列子控制項,就需要做一些封裝了,這裡就不一一展開了,最後附上有完整功能的WrapPanel實現代碼供大家參考。可以實現橫向和縱向排列。