Grokys
Grokys

Reputation: 16526

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

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

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?

Upvotes: 14

Views: 8663

Answers (3)

Steve Greatrex
Steve Greatrex

Reputation: 15999

This is a very interesting question, and I don't think I can answer "is layout broken" - I'm convinced it shouldn't be but looking at your example I can't work out how to "fix" it using the measure/layout passes.

I can, however, suggest the following as an alternative to get (roughly) the desired effect:

<ItemsControl>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid Rows="{Binding Items.Count,
                 RelativeSource={RelativeSource FindAncestor, AncestorType=ItemsControl}}" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ListBox ItemsSource="{StaticResource Items}"/>
    <ListBox ItemsSource="{StaticResource Items}"/>
    <ListBox ItemsSource="{StaticResource Items}"/>
    <ListBox ItemsSource="{StaticResource Items}"/>
    <ListBox ItemsSource="{StaticResource Items}"/>
</ItemsControl>

enter image description here

This will assign a uniform amount of space to each item in the list. It doesn't quite work if there are insufficient items to fill all available space, but it's almost there.

Upvotes: 0

Pavlo Glazkov
Pavlo Glazkov

Reputation: 20746

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));
}

Upvotes: 7

cosullivan
cosullivan

Reputation: 436

The problem is that you are telling your child controls that they can have the entire available size in the MeasureOverride

e.Measure(availableSize);

You need to restrict the size here to let them know that they will be resized. You should be able to implement a similar logic in your MeasureOverride.

Upvotes: -2

Related Questions