Visually Located

XAML and GIS

Creating a WrapPanel for your Windows Runtime apps

Recently I saw a friend, Glenn Versweyveld, write a blog about showing a “tags” (eg blog tags) within an app. The blog documents how to create a horizontal list of tags that are not formed into columns and rows. The list would let the items flow naturally. He used something I never would have thought of to accomplish this. He used a RichTextBlock. This was a rather cool idea that again, I would have never thought of.

When I saw the blog I quickly asked why he did not just use a GridView or a WrapGrid/ItemsWrapGrid. His simple reply was that it did not accomplish what he wanted due to the row/column layout..

If you are on “Big Windows” the GridView lays items out into columns and rows, by filling up columns from left to right. If you are on Windows Phone the Grid View also lays items in rows and columns, but it fills up rows first instead of columns.

The right picture shows Big Windows and the left shows phone.

GridView-BigWindowsGridView-Phone

Ok, so GridView is out, how about a ListView and change the ItemsPanel to be an ItemsWrapGrid with Orientation set to Horizontal? Nope, same row/column layout with that as well. Okay, now I see why Glenn went a custom route.

I like “Plug-n-Play” solutions. I like custom controls that I can put into XAML w/o and custom work. So, while I think Glenn’s approach was rather cool, it’s just not Plug-n-Play. To accomplish this wrapping we don't need to create a custom control. We can create a new Panel that we can use for any ItemsControl.

public class WrapPanel : Panel
{
    // 
}

When creating custom panels, there are two methods you must override, MeasureOverride and ArrangeOverride. The MeasureOverride method is where the panel determines how much space it will take up. It does this by asking each element within it to measure itself and then it will return the final size. The ArrangeOverride method is where the panel takes the information from the MeasureOverride and then places each item at X and Y locations.

The MeasureOverride will find the Height of the panel and just assume that the Width is the width it is given.

protected override Size MeasureOverride(Size availableSize)
{
    // Just take up all of the width
    Size finalSize = new Size { Width = availableSize.Width };
    double x = 0;
    double rowHeight = 0d;
    foreach (var child in Children)
    {
        // Tell the child control to determine the size needed
        child.Measure(availableSize);
 
        x += child.DesiredSize.Width;
        if (x > availableSize.Width)
        {
            // this item will start the next row
            x = child.DesiredSize.Width;
 
            // adjust the height of the panel
            finalSize.Height += rowHeight;
            rowHeight = child.DesiredSize.Height;
        }
        else
        {
            // Get the tallest item
            rowHeight = Math.Max(child.DesiredSize.Height, rowHeight);
        }
    }
 
    // Add the final height
    finalSize.Height += rowHeight;
    return finalSize;
}

The ArrangeOverride will place each item at the correct X and Y location based on the size of the elements.

protected override Size ArrangeOverride(Size finalSize)
{
    Rect finalRect = new Rect(0, 0, finalSize.Width, finalSize.Height);
 
    double rowHeight = 0;
    foreach (var child in Children)
    {
        if ((child.DesiredSize.Width + finalRect.X) > finalSize.Width)
        {
            // next row!
            finalRect.X = 0;
            finalRect.Y += rowHeight;
            rowHeight = 0;
        }
        // Place the item
        child.Arrange(new Rect(finalRect.X, finalRect.Y, child.DesiredSize.Width, child.DesiredSize.Height));
 
        // adjust the location for the next items
        finalRect.X += child.DesiredSize.Width;
        rowHeight = Math.Max(child.DesiredSize.Height, rowHeight);
    }
    return finalSize;
}

This panel will now layout items from left to right and any content that doesn’t fit in the row will go to the next row.

Let’s test this out. First we’ll try using an ItemsControl

<ItemsControl ItemsSource="{Binding Items}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <controls:WrapPanel/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Background="Red" MinWidth="0" MinHeight="0" Margin="0,0,6,0">
                <TextBlock Text="{Binding}" FontSize="20"/>
            </Button>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

We get the following layout in both Phone and Windows

WrapPanel-BigWindows

If you use a ListView or ListBox you will get a slightly different layout due to the style of the ListViewItem and ListBoxItem. I’ll let you decide how you want to style those.

blog comments powered by Disqus