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:
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:
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:
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!
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; } }