The blog of dlaa.me

Wrap music [A more flexible balanced WrapPanel implementation for Silverlight and WPF!]

In my last post, I told the story of a customer who asked for an easy way to make the Silverlight/WPF WrapPanel use all available space to spread its children out evenly instead of bunching them up together. The following sample shows off the default WrapPanel behavior on top - and my alternate BalancedWrapPanel behavior on the bottom:

BalancedWrapPanel, Horizontal, ItemWidth and ItemHeight set

The default WrapPanel behavior fills each horizontal (or vertical) line as much as it can before moving on to the next line, but leaves any extra space at the end of each line. BalancedWrapPanel began as a copy of the WrapPanel code (available as part of the Silverlight Toolkit) and contains a modified copy of one of the helper methods that instead distributes the unsightly chunk of extra space evenly through the entire column (or row). That was what I set out to do with BalancedWrapPanel, so I was fairly happy with the results. Unfortunately, the customer wasn't 100% satisfied...

In particular, the desire was for those items in the last line to align with the items above instead of centering like they do in my initial implementation. It's a perfectly reasonable request - and something I thought about when I first started on BalancedWrapPanel! But things are a little tricky because those orderly columns only show up when the ItemWidth and/or ItemHeight properties are set. In fact, the WrapPanel code doesn't actually have any concept of columns at all! Rather, the columns you see are a natural consequence of the algorithm laying out lots of constant-width items within constant-width bounds. So the columns are very real, but the code doesn't really know anything about them. And they don't even exist when ItemWidth/ItemHeight aren't set; despite each column of this vertically-oriented BalancedWrapPanel being vertically balanced, there are no overall rows in the horizontal direction because all the elements are different sizes:

BalancedWrapPanel, Vertical, ItemWidth and ItemHeight not set

When I was first thinking about this scenario, it seemed to me that I'd need to add some code to track the columns and then do things differently for the last line in order to keep everything aligned properly. I was afraid this additional code would overly complicate the original sample, and decided not to implement it until and unless someone asked. Besides, it's called BalancedWrapPanel, so it seemed natural that everything should be balanced! :)

But now that I had a specific request, I thought more carefully and realized that not only was it easy to align the last items, but that it was also a tad more efficient to do so! I didn't want to change the current behavior of BalancedWrapPanel (because I think that's what people expect), but I wanted to enable the new aligning behavior, too. So I added a new property to align the last items, but it only works when ItemWidth/ItemHeight are set (otherwise it has no effect because items can be all different sizes and don't line up to begin with). I considered trying to explain this technicality in the name of the new property, but everything I came up with was long and cumbersome. So the new property is simply named AlignLastItems - setting it to True changes the first example to look like this instead:

BalancedWrapPanel, Horizontal, ItemWidth and ItemHeight set, AlignLastItems set

Notice how the basic WrapPanel behavior is maintained, but the items are spread out evenly and there are no gaping holes. And there you have it - a balanced WrapPanel implementation that should work for most common scenarios. What's more, the customer is satisfied and maybe other folks will start using BalancedWrapPanel in their projects, too!

 

Click here to download the source code for BalancedWrapPanel and the Silverlight/WPF demo application.

 

PS - Please refer to my previous BalancedWrapPanel post for information about why I coded it like I did along with some other details.

PPS - As I mention above, the changes from what I'd already written were surprisingly minimal. Other than adding the AlignLastItems DependencyProperty, the only differences are highlighted below:

private void ArrangeLine(int lineStart, int lineEnd, double? directDelta, double directMaximum, double indirectOffset, double indirectGrowth)
{
    Orientation o = Orientation;
    bool isHorizontal = o == Orientation.Horizontal;
    UIElementCollection children = Children;
    double directLength = 0.0;
    double itemCount = 0.0;
    double itemLength = isHorizontal ? ItemWidth : ItemHeight;

    if (AlignLastItems && !itemLength.IsNaN())
    {
        // Length is easy to calculate in this case
        itemCount = Math.Floor(directMaximum / itemLength);
        directLength = itemCount * itemLength;
    }
    else
    {
        // Make first pass to calculate the slack space
        itemCount = lineEnd - lineStart;
        for (int index = lineStart; index < lineEnd; index++)
        {
            // Get the size of the element
            UIElement element = children[index];
            OrientedSize elementSize = new OrientedSize(o, element.DesiredSize.Width, element.DesiredSize.Height);

            // Determine if we should use the element's desired size or the
            // fixed item width or height
            double directGrowth = directDelta != null ?
                directDelta.Value :
                elementSize.Direct;

            // Update total length
            directLength += directGrowth;
        }
    }

    // Determine slack
    double directSlack = directMaximum - directLength;
    double directSlackSlice = directSlack / (itemCount + 1.0);
    double directOffset = directSlackSlice;

    // Make second pass to arrange items
    for (int index = lineStart; index < lineEnd; index++)
    {
        // Get the size of the element
        UIElement element = children[index];
        OrientedSize elementSize = new OrientedSize(o, element.DesiredSize.Width, element.DesiredSize.Height);

        // Determine if we should use the element's desired size or the
        // fixed item width or height
        double directGrowth = directDelta != null ?
            directDelta.Value :
            elementSize.Direct;

        // Arrange the element
        Rect bounds = isHorizontal ?
            new Rect(directOffset, indirectOffset, directGrowth, indirectGrowth) :
            new Rect(indirectOffset, directOffset, indirectGrowth, directGrowth);
        element.Arrange(bounds);

        // Update offset for next time
        directOffset += directGrowth + directSlackSlice;
    }
}