4. The Code
In this code sample, you will build a layout container extending the Panel
type that can arrange its children in either a horizontal orientation
(in rows) or a vertical one (in columns). It also automatically wraps
all its children into successive rows or columns based on available
space. The implementing type is named WrapPanel, and Listing 1 shows the code.
Listing 1. WrapPanel implementation
using System; using System.Windows; using System.Windows.Controls;
namespace Recipe5_9 { public class WrapPanel : Panel { //Orientation dependency property DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(WrapPanel), new PropertyMetadata( new PropertyChangedCallback(OrientationPropertyChangedCallback)));
public Orientation Orientation { get { return (Orientation)GetValue(OrientationProperty); } set { SetValue(OrientationProperty, value); } } private static void OrientationPropertyChangedCallback( DependencyObject target, DependencyPropertyChangedEventArgs e) { //cause the layout to be redone on change of Orientation if (e.OldValue != e.NewValue) (target as WrapPanel).InvalidateMeasure(); }
public WrapPanel() { //initialize the orientation Orientation = Orientation.Horizontal; } protected override Size MeasureOverride(Size availableSize) { double DesiredWidth = 0; double DesiredHeight = 0; double RowHeight = 0; double RowWidth = 0; double ColHeight = 0; double ColWidth = 0;
//call Measure() on each child - this is mandatory. //get the true measure of things by passing in infinite sizing foreach (UIElement uie in this.Children) uie.Measure(availableSize); //for horizontal orientation - children laid out in rows if (Orientation == Orientation.Horizontal) { //iterate over children for (int idx = 0; idx < this.Children.Count; idx++) {
//if we are at a point where adding the next child would
//put us at greater than the available width if (RowWidth + Children[idx].DesiredSize.Width >= availableSize.Width) { //set the desired width to the max of row width so far DesiredWidth = Math.Max(RowWidth, DesiredWidth); //accumulate the row height in preparation to move on to the next row DesiredHeight += RowHeight; //initialize the row height and row width for the next row iteration RowWidth = 0; RowHeight = 0; } //if on the other hand we are within width bounds if (RowWidth + Children[idx].DesiredSize.Width < availableSize.Width) { //increment the width of the current row by the child's width RowWidth += Children[idx].DesiredSize.Width; //set the row height if this child is taller //than the others in the row so far RowHeight = Math.Max(RowHeight, Children[idx].DesiredSize.Height);
} //this means we ran out of children in the middle or exactly at the end //of a row if (RowWidth != 0 && RowHeight != 0) { //account for the last row DesiredWidth = Math.Max(RowWidth, DesiredWidth); DesiredHeight += RowHeight; }
} } else //vertical orientation - children laid out in columns { //iterate over children for (int idx = 0; idx < this.Children.Count; idx++) { //if we are at a point where adding the next child would //put us at greater than the available height if (ColHeight + Children[idx].DesiredSize.Height >= availableSize.Height) {
//set the desired height to max of column height so far DesiredHeight = Math.Max(ColHeight, DesiredHeight); //accumulate the column width in preparation to //move on to the next column DesiredWidth += ColWidth; //initialize the column height and column width for the //next column iteration ColHeight = 0; ColWidth = 0; } //if on the other hand we are within height bounds if (ColHeight + Children[idx].DesiredSize.Height < availableSize.Height) { //increment the height of the current column by the child's height ColHeight += Children[idx].DesiredSize.Height; //set the column width if this child is wider //than the others in the column so far ColWidth = Math.Max(ColWidth, Children[idx].DesiredSize.Width); } } //this means we ran out of children in the middle or exactly at the end //of a column if (RowWidth != 0 && RowHeight != 0) { //account for the last row DesiredHeight = Math.Max(ColHeight, DesiredHeight); DesiredWidth += ColWidth; } } //return the desired size return new Size(DesiredWidth, DesiredHeight); }
protected override Size ArrangeOverride(Size finalSize) { double ChildX = 0; double ChildY = 0; double FinalHeight = 0; double FinalWidth = 0; //horizontal orientation - children in rows if (Orientation == Orientation.Horizontal) { double RowHeight = 0;
//iterate over children for (int idx = 0; idx < this.Children.Count; idx++) { //if we are about to go beyond width bounds with the next child if (ChildX + Children[idx].DesiredSize.Width >= finalSize.Width) { //move to next row ChildY += RowHeight; FinalHeight += RowHeight; FinalWidth = Math.Max(FinalWidth, ChildX); //shift to the left edge to start next row ChildX = 0; } //if we are within width bounds if (ChildX + Children[idx].DesiredSize.Width < finalSize.Width) { //lay out child at the current X,Y coords with //the desired width and height Children[idx].Arrange(new Rect(ChildX, ChildY, Children[idx].DesiredSize.Width, Children[idx].DesiredSize.Height)); //increment X value to position next child horizontally right after the //currently laid out child ChildX += Children[idx].DesiredSize.Width; //set the row height if this child is taller //than the others in the row so far RowHeight = Math.Max(RowHeight, Children[idx].DesiredSize.Height); } } } else //vertical orientation - children in columns { double ColWidth = 0; //iterate over children for (int idx = 0; idx < this.Children.Count; idx++) { //if we are about to go beyond height bounds with the next child if (ChildY + Children[idx].DesiredSize.Height >= finalSize.Height) { //move to next column ChildX += ColWidth;
FinalWidth += ColWidth; FinalHeight = Math.Max(FinalHeight, ChildY); //shift to the top edge to start next column ChildY = 0; } //if we are within height bounds if (ChildY + Children[idx].DesiredSize.Height < finalSize.Height) { //lay out child at the current X,Y coords with //the desired width and height Children[idx].Arrange(new Rect(ChildX, ChildY, Children[idx].DesiredSize.Width, Children[idx].DesiredSize.Height)); //increment Y value to position next child vertically right below the //currently laid out child ChildY += Children[idx].DesiredSize.Height; //set the column width if this child is wider //than the others in the column so far ColWidth = Math.Max(ColWidth, Children[idx].DesiredSize.Width); } } } //return the original final size return finalSize; } } }
|
Let's first look at the measure pass. As noted previously, in MeasureOverride(),
you are given the available size to work with, and you return the total
desired size of the container in question with all its children. You
can see in Listing 1 that you start off by calling Measure() on every child in the Children collection.
It is worth noting here that the measuring and arranging tasks are both recursive in nature. When you call Measure() on every child, the runtime ultimately calls MeasureOverride() on that child, which in turn calls Measure() on any children that child might have, and so on until MeasureOverride() gets called on every leaf element (i.e., an element without any more children). The desired size returned by MeasureOverride() at every level of recursion travels back to its parent and is available through the DesiredSize property on the child.
Once you call Measure() on each of the children in your code, and consequently populate the DesiredSize property on each of them, you then need to calculate the desired size of the entire WrapPanel based on the individual desired sizes of each child. To do that, you iterate over the Children collection and try to arrange them along rows or columns, based on the Orientation value of Horizontal or Vertical,
respectively. Note that you do not actually create any rows or columns;
rather, you simply try to calculate the size of such rows or columns.
So for example, in a Horizontal orientation, as you iterate over each child you add its width to a counter named RowWidth,
indicating the current row's width. You also keep a track of the row's
height by constantly evaluating the maximum height among the children
added to that row up to that point. Once you reach a point where the
addition of the next child would cause the row to go beyond the Width component of the DesiredSize parameter, you consider the row complete.
At this point, you track the maximum width of any such row calculated so far in a counter named DesiredWidth.
The assumption is that the children could all have different sizes. In
case they are all similarly sized, all those rows would be equal width
as well, since the rows would break off at the exact same point every
time. You also keep a measure of how much you are consuming on the Y
axis with each row, using a counter named DesiredHeight, by adding up each row's height.
If the orientation was vertical, a similar logic is
followed, with height and width interchanged. Once you have iterated
over each child, you have your desired size in the combination of the DesiredWidth and DesiredHeight counters, and you pass that out of MeasureOverride().
The arrange pass is similar in logic. You get the finalSize as the parameter to ArrangeOverride(). You break up your logic based on the Orientation setting as before. But this time, you actually lay each child out by calling the Arrange() method on the child. The UIElement.Arrange() method accepts a Rectangle and lays the element inside that Rectangle. As you iterate through each element, you increment placement coordinates (either the x value or the y
value based on whether you are laying out in rows or columns) to
position child elements one after the other, and when you reach bounds
where you have to break into the next row or column, you move by either
the row height or the column width calculated in a similar fashion, as
you did in the MeasureOverride() implementation.
The Orientation property is implemented as a dependency property of type System.Windows.Controls.Orientation that can be used to specify a horizontal or vertical layout. In OrientationPropertyChangedCallback(), you call InvalidateMeasure() on the WrapPanel instance, if the property value is being changed. InvalidateMeasure() causes the layout system to redo the layout, starting again with the measure pass.