The Windows Presentation Foundation has a panel I often find useful called UniformGrid. As the name suggests, the UniformGrid divides its area into cells, each of which has the same dimensions.
By default, UniformGrid automatically determines a number of rows and columns by taking the ceiling of the square root of the number of children. For example, if there are 20 children, UniformGrid
calculates 5 rows and columns (even though it might make more sense to
have 5 rows and 4 columns, or 4 rows and 5 columns). You can override
this calculation by explicitly setting the Rows or Columns property of UniformGrid to a non-zero number.
Almost always, I find myself setting either Rows or Columns to 1, in effect making a single column or row of equally sized cells. This is not like a StackPanel that continues off the screen if it has too many children, but more like a single-column or single-row Grid where every RowDefinition or ColumnDefinition has a GridLength set to Star, and hence allocates the same space.
My version of UniformGrid is called UniformStack. It doesn’t have a Rows or Columns property but it does have an Orientation property—the same property defined by StackPanel—to indicate whether the children of the panel will be arranged vertically or horizontally.
Here’s the portion of the UniformStack class that defines the single dependency property and the property-changed handler:
Example 1. Silverlight Project: Petzold.Phone.Silverlight File: UniformStack.cs (excerpt)
public class UniformStack : Panel { public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(UniformStack), new PropertyMetadata(Orientation.Vertical, OnOrientationChanged));
public Orientation Orientation { set { SetValue(OrientationProperty, value); } get { return (Orientation)GetValue(OrientationProperty); } }
static void OnOrientationChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { (obj as UniformStack).InvalidateMeasure(); }
. . . }
|
The definitions of the
dependency property and CLR property are straightforward. The
property-changed handler casts the first argument to the class type, as
usual, and then simply calls InvalidateMeasure. This is a method defined by UIElement,
and it’s basically telling the layout system: “Whatever you think you
know about how big I am, forget it. I’m a whole different size.” This
call initiates the measure
pass of layout from the root of the visual tree because the size of
thid panel could affect parent classes. The measure pass is followed
automatically by an arrange pass. (Layout passes are also initiated
whenever the size of the panel changes, or when elements are added to or
removed from the Children collection, or when an existing child changes size.)
There is also an InvalidateArrange
method, which initiates just the second half of the layout process, but
this is much rarer. Perhaps if you have a panel that dynamically moves
its elements around without itself changing size you would have occasion
to call InvalidateArrange.
The InvalidateMeasure method eventually causes a call to be made to MeasureOverride, and let’s think for a moment what needs to be done.
Consider a UniformStack with a horizontal orientation. Suppose the panel has five children, and the availableSize offered to the panel has a Width of 400 and a HeightWidth of 80 (1/5th the total available width) and a Height of 200. That’s the panel’s paradigm. of 200. Each child should be offered a size with a
But what if the Width property of availableSize is infinite? What should happen in that case?
Well, it’s not entirely clear. Certainly the panel has no choice but to offer to each child a Width of infinity. After that, one reasonable solution is to return a size from MeasureOverride with a Width that is five times the Width of the widest child.
That’s what I do here:
Example 2. Silverlight Project: Petzold.Phone.Silverlight File: UniformStack.cs (excerpt)
protected override Size MeasureOverride(Size availableSize) { if (Children.Count == 0) return new Size();
Size availableChildSize = new Size(); Size maxChildSize = new Size(); Size compositeSize = new Size();
// Calculate an available size for each child if (Orientation == Orientation.Horizontal) availableChildSize = new Size(availableSize.Width / Children.Count, availableSize.Height); else availableChildSize = new Size(availableSize.Width, availableSize.Height / Children.Count);
// Enumerate the children, and find the widest width and the highest height foreach (UIElement child in Children) { child.Measure(availableChildSize); maxChildSize.Width = Math.Max(maxChildSize.Width, child.DesiredSize.Width); maxChildSize.Height = Math.Max(maxChildSize.Height, child.DesiredSize.Height); } // Now determine a composite size that depends on infinite available width or height if (Orientation == Orientation.Horizontal) { if (Double.IsPositiveInfinity(availableSize.Width)) compositeSize = new Size(maxChildSize.Width * Children.Count, maxChildSize.Height); else compositeSize = new Size(availableSize.Width, maxChildSize.Height); } else { if (Double.IsPositiveInfinity(availableSize.Height)) compositeSize = new Size(maxChildSize.Width, maxChildSize.Height * Children.Count); else compositeSize = new Size(maxChildSize.Width, availableSize.Height); }
return compositeSize; }
|
The method begins by diving out if the panel has no children; this avoids division by zero later on.
An availableChildSize is calculated based on the Orientation property by ignoring the presence of infinity in the availableSize
for the panel. (Infinity divided by the number of children will still
be infinity, and that’s what’s required in that case.) The enumeration
of the children calls Measure on each child with that availableChildSize. The logic involving the DesiredSize of the child also ignores infinite dimensions but instead accumulates a maxChildSize.
This actually represents the width of the widest child and the height
of the tallest child; it’s possible that no single child has the same
dimensions as maxChildSize.
The final calculation of compositeSize takes into account both Orientation and the possibility of an infinite dimension. Notice that compositeSize is sometimes based on one of the availableSize dimensions; this is normally not proper but the method does it only when it knows that dimension is not infinite.
The ArrangeOverride method calls Arrange on each child with the same size (called finalChildSize in the method) but with different x and y positions relative to the panel depending on orientation:
Example 3. Silverlight Project: Petzold.Phone.Silverlight File: UniformStack.cs (excerpt)
protected override Size ArrangeOverride(Size finalSize) { if (Children.Count > 0) { Size finalChildSize = new Size(); double x = 0; double y = 0;
if (Orientation == Orientation.Horizontal) finalChildSize = new Size(finalSize.Width / Children.Count, finalSize.Height); else finalChildSize = new Size(finalSize.Width, finalSize.Height / Children.Count);
foreach (UIElement child in Children) { child.Arrange(new Rect(new Point(x, y), finalChildSize));
if (Orientation == Orientation.Horizontal) x += finalChildSize.Width; else y += finalChildSize.Height; } }
return base.ArrangeOverride(finalSize); }
|
Let’s use the UniformStack to make a bar chart!
The QuickBarChart program actually uses three UniformStack panels:
Example 4. Silverlight Project: QuickBarChart File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <petzold:UniformStack Orientation="Vertical">
<petzold:UniformStack x:Name="barChartPanel" Orientation="Horizontal" />
<petzold:UniformStack Orientation="Horizontal">
<Button Content="Add 10 Items" HorizontalAlignment="Center" VerticalAlignment="Center" Click="OnButtonClick" />
<TextBlock Name="txtblk" Text="0" HorizontalAlignment="Center" VerticalAlignment="Center" /> </petzold:UniformStack> </petzold:UniformStack> </Grid>
|
The first UniformStack with a Vertical orientation simply divides the content area into two equal areas. (See how much easier it is to use than a regular Grid?) The top half contains another UniformStack with nothing in it (yet). The bottom one contains a UniformStack with a Horizontal orientation for a Button and a TextBlock.
Clicking the Button causes the code-behind file to add 10 more Rectangle elements to the UniformStack named barChartPanel:
Example 5. Silverlight Project: QuickBarChart File: MainPage.xaml.cs (excerpt)
public partial class MainPage : PhoneApplicationPage { Random rand = new Random();
public MainPage() { InitializeComponent(); }
void OnButtonClick(object sender, RoutedEventArgs args) { for (int i = 0; i < 10; i++) { Rectangle rect = new Rectangle(); rect.Fill = this.Resources["PhoneAccentBrush"] as Brush; rect.VerticalAlignment = VerticalAlignment.Bottom; rect.Height = barChartPanel.ActualHeight * rand.NextDouble(); rect.Margin = new Thickness(0, 0, 0.5, 0);
barChartPanel.Children.Add(rect); }
txtblk.Text = barChartPanel.Children.Count.ToString(); } }
|
Notice that each Rectangle has a little half-pixel Margin
on the right so there’s at least some spacing between the bars. Still, I
think you’ll be surprised how many you can put in there before the
display logic gives up: