我的面板我做主 -- 淘寶UWP中自定義Panel的實現


在Windows10 UWP開發平台上內置的XMAL布局面板包括RelativePanelStackPanelGridVariableSizedWrapGridCanvas。在開發淘寶UWP應用時,遇到以下業務場景。

業務場景



場景一:淘寶商品提供的一些消費者保障服務

image

場景二:淘寶商品的SKU屬性展示

image

實現分析


系統默認的面板容器控件顯然不符合要求了。在WPF里面有WrapPanel,但是在UWP應用里面沒有,這個時候就需要自定義個Panel了來實現WrapPanel的功能,實現起來不是很復雜。在MSDN的文檔上已經給出了詳細的實現說明:Xaml自定義面板,主要就是自定義一個Panel的派生類,然后重寫(MeasureOverrideArrangeOverride)方法。

以下是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什么都沒有做,如果直接運行會是什么樣子呢?

image

這個時候界面就是一片空白。添加的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的面板顯示出來了,背景顏色是灰色,但是子控件沒有顯示。

image

接下來,實現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;
        }

運行結果:

image

子控件出來了,超出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;
        }

運行結果

image

縱向布局

遞增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;
        }

運行結果:

image

接下來的問題,就是橫向或者縱向布局的時候,判斷何時該換行了。要換行就要計算依次排列的子控件的寬和高,同時和Mypanel的大小進行比較是否超出邊界。

這其中還有個問題是MeasureOverrideArrangeOverride 都會返回一個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;
        }

運行結果

image

子控件會被裁剪部分區域,顯示不完整,以適應較小的size。

2. 如果給Measure傳遞的值較大,比子控件的大小要大。

protected override Size MeasureOverride(Size availableSize)
        {
            foreach (FrameworkElement child in Children)
            {
                child.Measure(new Size(400,400));
            }
            return availableSize;
        }

運行結果

image

結果顯示還是正常的大小,沒有變化。

總結:

說明子控件的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);
        }

運行結果:

image

灰色區域是容器的大小,各個子控件已經超出容器控件的大小范圍了,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;
        }

運行結果:

image

 

如果要實現更復雜的功能,例如要同時支持可以橫向縱向排列子控件,就需要做一些封裝了,這里就不一一展開了,最后附上有完整功能的WrapPanel實現代碼供大家參考。可以實現橫向和縱向排列。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM