Вопрос

I have a resizable container control in my layout. It should contain any number of my user controls (which are also resizeable and always 3*2 for now) in following way:

Desired layout

I hope you have got the idea. I was trying to use StackPanel but failed. It didn't centred the elements for me.

So, how do I get the desired layout using XAML? Hopefully avoid coding as much as possible...

THE CODE I ENDED UP WITH:

I believe I must share the result. I took the code from this answer WPF - How can I center all items in a WrapPanel? and added child controls resizing.

    private Size elementFinalSize = new Size(0, 0);

    private Size MeasureDesiredSize(Size containerSize)
    {
        if (base.InternalChildren.Count == 0)
        {
            return new Size(0, 0);
        }

        // NB!: We assume all the items in the panel are of the same size.
        var child = base.InternalChildren[0];
        double elementAspectRatio = child.DesiredSize.Height == 0 ? 2.0 / 3.0 : child.DesiredSize.Width / child.DesiredSize.Height;
        Size finalElementSize = new Size(0, 0);
        Size newElementSize = finalElementSize;
        for (int possibleRowsNumber = 1; ; possibleRowsNumber++)
        {
            int numberOfElementInRow = this.GetElementsNumberInRow(base.InternalChildren.Count, possibleRowsNumber);
            double maxElementHeight = containerSize.Height / possibleRowsNumber;
            double maxElementWidth = containerSize.Width / numberOfElementInRow;
            if (maxElementWidth / elementAspectRatio > maxElementHeight)
            {
                // So many elements is more in width than container size, thus use Height.
                newElementSize = new Size(maxElementHeight * elementAspectRatio, maxElementHeight);
            }
            else
            {
                // The element Height is greater than container row size, thus use Width.
                newElementSize = new Size(maxElementWidth, maxElementWidth / elementAspectRatio);
            }

            if (newElementSize.Height > finalElementSize.Height)
            {
                // With such number of row a single element would be bigger than with previous number of rows.
                finalElementSize = newElementSize;
            }
            else
            {
                // With such number of rows a single element is less than the previous biggest one, thus stop searching.
                break;
            }
        }

        return finalElementSize;
    }

    private int GetElementsNumberInRow(int elementsCount, int rowsNumber)
    {
        int x = elementsCount % rowsNumber;
        if (x == 0)
        {
            return elementsCount / rowsNumber;
        }

        int maxPossibleElementsCount = elementsCount + rowsNumber - x;
        return maxPossibleElementsCount / rowsNumber;
    }

    protected override Size MeasureOverride(Size constraint)
    {
        // This MeasureOverride functions is called the first of the three overridden.
        // We need to understand the best (maximum) size for future element and arrange items accordingly.

        // Drop the desired size to zero in order to recalculate it as soon as necessary.
        this.elementFinalSize = new Size(0, 0);

        Size curLineSize = new Size();
        Size panelSize = new Size();

        UIElementCollection children = base.InternalChildren;

        for (int i = 0; i < children.Count; i++)
        {
            UIElement child = children[i] as UIElement;

            // Flow passes its own constraint to children
            if (this.elementFinalSize.Height == 0)
            {
                child.Measure(constraint);
                this.elementFinalSize = this.MeasureDesiredSize(constraint);
                child.InvalidateMeasure();
            }

            if (curLineSize.Width + this.elementFinalSize.Width > constraint.Width) //need to switch to another line
            {
                panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
                panelSize.Height += curLineSize.Height;
                curLineSize = this.elementFinalSize;

                if (this.elementFinalSize.Width > constraint.Width) // if the element is wider then the constraint - give it a separate line                    
                {
                    panelSize.Width = Math.Max(this.elementFinalSize.Width, panelSize.Width);
                    panelSize.Height += this.elementFinalSize.Height;
                    curLineSize = new Size();
                }
            }
            else //continue to accumulate a line
            {
                curLineSize.Width += this.elementFinalSize.Width;
                curLineSize.Height = Math.Max(this.elementFinalSize.Height, curLineSize.Height);
            }
        }

        foreach (UIElement child in children)
        {
            child.Measure(this.elementFinalSize);
        }

        // the last line size, if any need to be added
        panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
        panelSize.Height += curLineSize.Height;

        return panelSize;
    }

    protected override Size ArrangeOverride(Size arrangeBounds)
    {
        int firstInLine = 0;
        Size curLineSize = new Size();
        double accumulatedHeight = 0;
        UIElementCollection children = this.InternalChildren;

        for (int i = 0; i < children.Count; i++)
        {
            if (curLineSize.Width + this.elementFinalSize.Width > arrangeBounds.Width) //need to switch to another line
            {
                ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, i);

                accumulatedHeight += curLineSize.Height;
                curLineSize = this.elementFinalSize;

                if (this.elementFinalSize.Width > arrangeBounds.Width) //the element is wider then the constraint - give it a separate line                    
                {
                    ArrangeLine(accumulatedHeight, this.elementFinalSize, arrangeBounds.Width, i, ++i);
                    accumulatedHeight += this.elementFinalSize.Height;
                    curLineSize = new Size();
                }
                firstInLine = i;
            }
            else //continue to accumulate a line
            {
                curLineSize.Width += this.elementFinalSize.Width;
                curLineSize.Height = Math.Max(this.elementFinalSize.Height, curLineSize.Height);
            }
        }

        if (firstInLine < children.Count)
            ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, children.Count);

        return arrangeBounds;
    }

    private void ArrangeLine(double y, Size lineSize, double boundsWidth, int start, int end)
    {
        double x = 0;
        if (this.HorizontalContentAlignment == HorizontalAlignment.Center)
        {
            x = (boundsWidth - lineSize.Width) / 2;
        }
        else if (this.HorizontalContentAlignment == HorizontalAlignment.Right)
        {
            x = (boundsWidth - lineSize.Width);
        }

        UIElementCollection children = InternalChildren;
        for (int i = start; i < end; i++)
        {
            UIElement child = children[i];
            child.Arrange(new Rect(x, y, this.elementFinalSize.Width, this.elementFinalSize.Height));
            x += this.elementFinalSize.Width;
        }
    }
Это было полезно?

Решение

The following will produce a nearly the same effect:

<Grid>
    <WrapPanel HorizontalAlignment="Center" VerticalAlignment="Center" >
        <Rectangle Stroke="Red" StrokeThickness="2" Width="200" Height="200"/>
        <Rectangle Stroke="Red" StrokeThickness="2" Width="200" Height="200"/>
        <Rectangle Stroke="Red" StrokeThickness="2" Width="200" Height="200"/>
    </WrapPanel>
</Grid>

but the items are left aligned so in your case your last screenshot would show the bottom rectangle aligned to the left. To fix that, you can create a custom implementation of WrapPanel just for that purpose

Edit: Of course this is just a dummy example with the rectangles and all, usually one would create a ListView/ListBox/ItemContainer (depends on the scenario) and make the ItemsPanel be a WrapPanel:

<ListView>
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel .../>
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
</ListView>
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top