wpflayoutpanelmeasureoverridearrangeoverride

WPF custom panel layout


As part of a learning exercise I wanted to write my own panel that does something relatively simple.

To achieve this, I thought it would be as simple as getting all the elements desired sizes in MeasureOverride by supplying the total available size, and then in ArrangeOverride doing the test to see if they would fit as a stack, else arrange them as a uniform grid.

However, this doesn't seem to work as I would expect. With the code below, when I resize the window so that the listboxes don't fit, I don't see the scrollbars show up, it just seems to lay them out fully with them clipped? i.e its not going into "scroll" mode.

I thought that measure was just a request to get the elements desired size, and that it was perfectly acceptable to "arrange" for a smaller size in ArrangeOverride?

internal class CustomPanel : Panel
{
    protected override Size MeasureOverride(Size availableSize)
    {
        foreach (UIElement element in InternalChildren)
        {
            element.Measure(availableSize);
        }

        return availableSize;
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        double y = 0;

        if (InternalChildren.OfType<UIElement>().Sum(e => e.DesiredSize.Height) < finalSize.Height)
        {
            // stack
            foreach (UIElement element in InternalChildren)
            {
                element.Arrange(new Rect(0, y, finalSize.Width, element.DesiredSize.Height));
                y += element.DesiredSize.Height;
            }
        }
        else
        {
            double space = finalSize.Height / InternalChildren.Count;

            // uniform grid
            foreach (UIElement element in InternalChildren)
            {
                element.Arrange(new Rect(0, y, finalSize.Width, space));
                y += space;
            }
        }

        return finalSize;
    }
}
<local:CustomPanel>
    <ListBox>
        <ListBoxItem>A</ListBoxItem>
        <ListBoxItem>B</ListBoxItem>
        <ListBoxItem>C</ListBoxItem>
        <ListBoxItem>D</ListBoxItem>
        <ListBoxItem>E</ListBoxItem>
        <ListBoxItem>F</ListBoxItem>
    </ListBox>

    <ListBox>
        <ListBoxItem>A</ListBoxItem>
        <ListBoxItem>B</ListBoxItem>
        <ListBoxItem>C</ListBoxItem>
        <ListBoxItem>D</ListBoxItem>
        <ListBoxItem>E</ListBoxItem>
        <ListBoxItem>F</ListBoxItem>
    </ListBox>
</local:CustomPanel>

enter image description here


Solution

  • Example of implementation using the recommendation from Clemens.
    First, the highest elements are aligned. If this is not enough, then the next ones in height, etc. in a cycle.

        public partial class CustomPanel
        {
            private class IndexDouble(int Index, double Value)
            {
                public int Index { get; set; } = Index;
                public double Value { get; set; } = Value;
            }
    
            private static double[] Resize(in double fullHeight, in double[] heights)
            {
                ArgumentOutOfRangeException.ThrowIfLessThan(fullHeight, 0);
                ArgumentNullException.ThrowIfNull(heights);
                if (fullHeight == 0)
                    return heights.Select(_ => 0.0).ToArray();
    
                IndexDouble[] sort = heights
                    .Select((n, i) => new IndexDouble(i, n))
                    .OrderByDescending(id => id.Value)
                    .ToArray();
    
                double sum;
                while ((sum = sort.Sum(id => id.Value)) > fullHeight)
                {
                    int count = sort.TakeWhile(id => id.Value == sort[0].Value).Count();
                    if (count == heights.Length)
                    {
                        double h = fullHeight / count;
                        foreach (var id in sort)
                        {
                            id.Value = h;
                        }
                    }
                    else
                    {
                        double maxSum = sort[0].Value * count;
                        double minSum = sort[count].Value * count;
                        double h;
                        if (sum - maxSum + minSum >= fullHeight)
                        {
                            h = sort[count].Value;
                        }
                        else
                        {
                            var delta = sum - fullHeight;
                            delta /= count;
                            h = sort[0].Value - delta;
    
                        }
                        for (int i = 0; i < count; i++)
                        {
                            sort[i].Value = h;
                        }
                    }
                }
    
                var result = sort
                    .OrderBy(id => id.Index)
                    .Select(id => id.Value)
                    .ToArray();
    
                return result;
    
            }
        }
    
        public partial class CustomPanel : Panel
        {
            public CustomPanel()
            {
                Loaded += static (s, e) =>
                {
                   ((UIElement) s).InvalidateMeasure();
                };
            }
    
            protected override Size MeasureOverride(Size availableSize)
            {
                double fullHeight = 0;
                double[] heights = new double[InternalChildren.Count];
                for (int i = 0; i < InternalChildren.Count; i++)
                {
                    UIElement element = InternalChildren[i];
                    element.Measure(availableSize);
                    heights[i] = element.DesiredSize.Height;
                    fullHeight += element.DesiredSize.Height;
                }
    
                if (fullHeight > availableSize.Height)
                {
                    fullHeight = 0;
                    heights = Resize(availableSize.Height, heights);
                    for (int i = 0; i < InternalChildren.Count; i++)
                    {
                        UIElement element = InternalChildren[i];
                        element.Measure(new Size(availableSize.Width, heights[i]));
                        fullHeight += element.DesiredSize.Height;
                        heights[i] = element.DesiredSize.Height;
                    }
                }
    
    
                availableSize.Height = fullHeight;
    
                return availableSize;
            }
    
            protected override Size ArrangeOverride(Size finalSize)
            {
    
                var heights = InternalChildren
                    .Cast<UIElement>()
                    .Select(elm => elm.DesiredSize.Height)
                    .ToArray();
    
                heights = Resize(finalSize.Height, heights);
    
                double sum = 0;
                // uniform grid
                for (int i = 0; i < InternalChildren.Count; i++)
                {
                    double h = heights[i];
                    UIElement element = InternalChildren[i];
                    element.Arrange(new Rect(0, sum, finalSize.Width, h));
                    sum += h;
                }
                finalSize.Height = sum;
                return finalSize;
            }
        }
    
    

    P.S. I did not conduct any testing.

    P.S.S. During testing, a small bug was found during the first visualization. To fix it, I added a default constructor in which the "InvalidateMeasure()" call is connected to the "Loaded" event.