Ayoub.A
Ayoub.A

Reputation: 2093

WPF: Sizing Canvas to contain its children

I have a task to draw multiple rectangles and manipulate them. So I'm using an ItemsControl with it's ItemsPanel as a Canvas, and a wrapping ScrollViewer:

<ScrollViewer Height="200" Grid.Row="1" >
    <ItemsControl Name="rectanglesList" Background="AliceBlue">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas VerticalAlignment="Stretch" HorizontalAlignment="Stretch"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="ContentPresenter">
                <Setter Property="Canvas.Left" Value="{Binding Y}"/>
                <Setter Property="Canvas.Top" Value="{Binding X}"/>
            </Style>
        </ItemsControl.ItemContainerStyle>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Border BorderThickness="1" BorderBrush="Black">
                    <Rectangle Width="{Binding Width}" Height="{Binding Height}" Fill="{Binding Color}"/>

                </Border>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</ScrollViewer>

Now since the Canvas height and width properties do not include it's children, the Scrollviewer won't work unless I change the ItemsControl height and width manually by calculating summing the heights and widths of the children and assigning them to the ItemsControl.

The question is now is there any property 'm missing that does this automatically?

Upvotes: 1

Views: 1004

Answers (2)

Drew Noakes
Drew Noakes

Reputation: 310907

@Clemens's answer is great. I just wanted to add some context as to why it's necessary in case it wasn't clear.

In WPF the Canvas panel does not consider its children when asked to measure itself. It is the responsibility of the parent control to ensure the canvas is sized appropriately for the items it will contain.

In the subclass @Clemens provides, the logic for considering all children is provided in MeasureOverride.

Note that this will not consider any non-FrameworkElement children. If you add a very large number of children, you may notice a performance hit during layout.

Upvotes: 1

Clemens
Clemens

Reputation: 128061

You may use a custom Canvas that overrides the MeasureOverride method

public class MyCanvas : Canvas
{
    protected override Size MeasureOverride(Size constraint)
    {
        base.MeasureOverride(constraint);

        var size = new Size();

        foreach (var child in Children.OfType<FrameworkElement>())
        {
            var x = GetLeft(child) + child.Width;
            var y = GetTop(child) + child.Height;

            if (!double.IsNaN(x) && size.Width < x)
            {
                size.Width = x;
            }

            if (!double.IsNaN(y) && size.Height < y)
            {
                size.Height = y;
            }
        }

        return size;
    }
}

and which would be used like this:

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <local:MyCanvas/>
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

It requires that the item container element, besides Canvas.Left and Canvas.Top has its Width and Height set in the ItemContainerStyle:

<ItemsControl.ItemContainerStyle>
    <Style TargetType="ContentPresenter">
        <Setter Property="Canvas.Left" Value="{Binding X}"/>
        <Setter Property="Canvas.Top" Value="{Binding Y}"/>
        <Setter Property="Width" Value="{Binding Width}"/>
        <Setter Property="Height" Value="{Binding Height}"/>
    </Style>
</ItemsControl.ItemContainerStyle>

The ItemTemplate would then just look like this:

<ItemsControl.ItemTemplate>
    <DataTemplate>
        <Border BorderThickness="1" BorderBrush="Black">
            <Rectangle Fill="{Binding Color}"/>
        </Border>
    </DataTemplate>
</ItemsControl.ItemTemplate>

or

<ItemsControl.ItemTemplate>
    <DataTemplate>
        <Rectangle StrokeThickness="1" Stroke="Black" Fill="{Binding Color}"/>
    </DataTemplate>
</ItemsControl.ItemTemplate>

You may also want to put the SCrollViewer into the ControlTemplate of the ItemsControl:

<ItemsControl.Template>
    <ControlTemplate TargetType="ItemsControl">
        <ScrollViewer HorizontalScrollBarVisibility="Auto"
                      VerticalScrollBarVisibility="Auto">
            <ItemsPresenter/>
        </ScrollViewer>
    </ControlTemplate>
</ItemsControl.Template>

Upvotes: 1

Related Questions