Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problems with Arrange/Measure - Is Layout broken in WPF?

Tags:

c#

layout

wpf

I am trying to make what I thought would be a simple Panel in WPF, which has the following properties:

  • If the combined heights of the children are less than the available height, then all children are displayed at their desired height.

  • If the combined heights of the children are greater than the available height, all children are reduced by the same percentage height in order to fit.

My panel looks like this:

public class MyStackPanel : Panel
{
    protected override Size MeasureOverride(Size availableSize)
    {
        Size requiredSize = new Size();

        foreach (UIElement e in InternalChildren)
        {
            e.Measure(availableSize);
            requiredSize.Height += e.DesiredSize.Height;
            requiredSize.Width = Math.Max(requiredSize.Width, e.DesiredSize.Width);
        }

        return new Size(
            Math.Min(availableSize.Width, requiredSize.Width),
            Math.Min(availableSize.Height, requiredSize.Height));
    }

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

        foreach (UIElement e in InternalChildren)
        {
            requiredHeight += e.DesiredSize.Height;
        }

        double scale = 1;

        if (requiredHeight > finalSize.Height)
        {
            scale = finalSize.Height / requiredHeight;
        }

        double y = 0;

        foreach (UIElement e in InternalChildren)
        {
            double height = e.DesiredSize.Height * scale;
            e.Arrange(new Rect(0, y, finalSize.Width, height));
            y += height;
        }

        return finalSize;
    }
}

My test XAML looks like this:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="300" Width="300">
    <Window.Resources>
        <x:Array x:Key="Items" Type="{x:Type sys:String}">
            <sys:String>Item1</sys:String>
            <sys:String>Item2</sys:String>
            <sys:String>Item3</sys:String>
            <sys:String>Item4</sys:String>
        </x:Array>
    </Window.Resources>
    <local:MyStackPanel>
        <ListBox ItemsSource="{StaticResource Items}"/>
        <ListBox ItemsSource="{StaticResource Items}"/>
        <ListBox ItemsSource="{StaticResource Items}"/>
        <ListBox ItemsSource="{StaticResource Items}"/>
        <ListBox ItemsSource="{StaticResource Items}"/>
    </local:MyStackPanel>
</Window>

But the output looks like this:

Layout Problem

As you can see, the items are clipping - the list boxes should be displaying scroll bars. The child items are not respecting the size given to them in the arrange pass.

From my investigations it seems that you cannot give a smaller size to a control in the arrange pass than you gave in the measure pass.

However, I cannot do this because I need the results of measure pass to know what size to give to the children in the arrange pass.

It seems like a chicken and egg situation. Is layout in WPF broken? Surely the measure pass should be just that, a measure pass?

like image 511
Grokys Avatar asked Nov 25 '11 13:11

Grokys


1 Answers

The problem in your case is that you pass all the available space to each child to its Measure call (e.Measure(availableSize)). But you need to pass only the portion of the space that you actually going to give them. Like this:

protected override Size MeasureOverride(Size availableSize)
{
    Size requiredSize = new Size();

    var itemAvailableSize = new Size(availableSize.Width, availableSize.Height / InternalChildren.Count);

    foreach (UIElement e in InternalChildren)
    {
        e.Measure(itemAvailableSize);
        requiredSize.Height += e.DesiredSize.Height;
        requiredSize.Width = Math.Max(requiredSize.Width, e.DesiredSize.Width);
    }

    return new Size(
        Math.Min(availableSize.Width, requiredSize.Width),
        Math.Min(availableSize.Height, requiredSize.Height));
}

Update:

In case when the size that you are planning to give each individual item is not easily calculated based on availableSize and depends on other items desired size, you can do the first round of measuring on all items passing double.PositiveInfinity as Height. After that you will know how big each items wants to be and you can calculate how much space you are actually going to give to each item. Then you need to call Measure with the calculated space once again.

Here is an example:

protected override Size MeasureOverride(Size availableSize)
{
    var requiredSize = new Size();

    double allItemsHeight = 0;

    foreach (UIElement e in InternalChildren)
    {
        e.Measure(new Size(availableSize.Width, double.PositiveInfinity));
        allItemsHeight += e.DesiredSize.Height;
    }

    double scale = 1;

    if (allItemsHeight > availableSize.Height)
    {
        scale = availableSize.Height / allItemsHeight;
    }

    foreach (UIElement e in InternalChildren)
    {
        double height = e.DesiredSize.Height * scale;

        e.Measure(new Size(availableSize.Width, height));

        requiredSize.Height += e.DesiredSize.Height;
        requiredSize.Width = Math.Max(requiredSize.Width, e.DesiredSize.Width);
    }

    return new Size(
        Math.Min(availableSize.Width, requiredSize.Width),
        Math.Min(availableSize.Height, requiredSize.Height));
}
like image 136
Pavlo Glazkov Avatar answered Nov 10 '22 06:11

Pavlo Glazkov