Simple WrapPanel

Introduction

Windows Store and Windows Phone apps does not have a WrapPanel. The closest we get to this is the WrapGrid. In this post, I will show how to create a simple WrapPanel with an Orientation property. The panel reusable for all xaml based application. I will also explain Measure and Arrange witch is the basic mechanism for making the panel work.

The WrapPanel should position child elements from left to right, breaking content to the next line when at the edge of the panel. Positioning from right to left or from top to bottom depending on the value of the Orientation property as shown on the image below.

WrapPanel image
WrapPanel with ipsum content buttons.

WrapGrid

Windows Store and Windows Phone apps comes with WrapGrid. In some ways, it is similar to the WrapPanel but it got some limitations.

  1. You can only use WrapGrid to display items in an ItemsControl. Therefore, you cannot just create this panel and fill it with UI elements you need to wrap it in an ItemsControl first.
  2. First item in the WrapGrid dictates the size for the rest of the items. This means that the panel is cropping items unless the first item is the largest or equal size compared with the rest.

The WrapPanel does not have these two limitations. You can have items in variable sizes and it is useable outside of an ItemsControl.

Measure & Arrange

Measure and Arrange is essential when writing your own panel. Measure should measure or calculate the size needed for a control. The measure method receives an available size and it should return the size needed. In this case, the size needed is based on the panel’s children. It is also important to note when running measure, the controls must call the measure method on each child or on its content.

Arrange should then place each child by giving it the upper left position and the size it has to fill by giving the child’s Arrange method an Rect. Call arrange on each child in order for it to render.

Measure

How to measure the panel size depends on the Orientation property. Therefor I have made two separate method to handle each orientation possibility.

protected override Size MeasureOverride(Size availableSize)
{
    switch (Orientation)
    {
        case Orientation.Horizontal:
            return MeasureHorizontal(availableSize);
        case Orientation.Vertical:
            return MeasureVertical(availableSize);
        default:
            return default(Size);
    }
}

In this post, I am only going to focus on the horizontal case since they are very similar. If you choose to download the source code, you can watch the vertical case.

To measure horizontal, I start with tree variables. I need these as I loop though the children of the panel and measure its size.

  • finalHeight
    • The height of my panel when all children are measured
  • curreltLineHeight
    • Height of the current line I am measuring
  • currentWidth
    • Widht of the current line I am measuring

From the Panel control, I get the Children property. This contains all visual elements within the panel.

When calling measure on a child its desired size is available. With this, I can start to sum up the width of each child and make a line break if a child reaches outside the edge of the panel. For each child I compare its height with the previous child. Largest height gets stored as my current line height. This enables me to support children of different heights. Whenever I have a line break, I reset the current line height.

private Size MeasureHorizontal(Size availableSize)
{
    var finalHeight = 0.0;
    var currentLineHeight = 0.0;
    var currentWidth = 0.0;
    foreach (var child in Children)
    {
        child.Measure(availableSize);
        currentWidth += child.DesiredSize.Width;
        if (currentWidth > availableSize.Width)
        {
            currentWidth = child.DesiredSize.Width;
            finalHeight += currentLineHeight;
            currentLineHeight = 0.0;
        }

        currentLineHeight = Math.Max(currentLineHeight, child.DesiredSize.Height);
    }

    finalHeight += currentLineHeight;
    return new Size(availableSize.Width, finalHeight);
}

Arrange

Similar to measure, arrange depends on the orientation. This time I get the final size the panel can fill. Like a StackPanel I am not worried about exceeding the final size. Place the panel within a ScrollViewer if the content is larger than the size available to solve this.

Again, I need tree variables to keep track when looping though the children of the panel.

  • currentLineHeight
    • Height of the current line I am arranging
  • x
    • The current horizontal position. This is part of the upper right coordinate of where the next child should start
  • y
    • The current vertical position. This is the second part of the upper right coordinate of where the next child should start

The line height I need to change the vertical position each time I need to make line break. I call the method arrange on each child in order to render it. To avoid cropping the children each child gets its desired size.

private Size ArrangeHorizontal(Size finalSize)
{
    var currentLineHeight = 0.0;
    var x = 0.0;
    var y = 0.0;

    foreach (var child in Children)
    {
        if ((x + child.DesiredSize.Width) > finalSize.Width)
        {
            x = 0.0;
            y += currentLineHeight;
        }

        currentLineHeight = Math.Max(currentLineHeight, child.DesiredSize.Height);

        child.Arrange(new Rect(x, y, child.DesiredSize.Width, child.DesiredSize.Height));
        x += child.DesiredSize.Width;
    }

    y += currentLineHeight;
    return new Size(finalSize.Width, y);
}

How to use the WrapPanel

Here are some examples on how to use the WrapPanel.

<controls:WrapPanel>
    <!--Content/childs here-->
</controls:WrapPanel>

Setting the Orientation property.

<controls:WrapPanel
    Orientation="Vertical">
    <!--Content/childs here-->
</controls:WrapPanel>

Using WrapPanel in an ItemsControl.

<ItemsControl>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <controls:WrapPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

Basic rules still applies to this panel. Therefore, it respects properties like height, width and alignment set on the panel. If it does not act like you would expect try changing these values.

Also keep in mind this a simple version there is properly some unsupported scenarios but feel free to leave a comment if you spot any. I have only tested the most basic ones like those above.

I have not created a demo project for this panel. Nevertheless, you can download the code class file here.

P.S Make sure you follow me on twitter @danielvistisen for updates on new posts.

Simple WrapPanel

One thought on “Simple WrapPanel

Comments are closed.