Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Two Column Grid for WPF

0.00/5 (No votes)
8 Aug 2011 1  
A custom panel for rows of label-control pairs useful in, for example, preferences screens.

sample-app.png

Introduction

Have you ever designed a screen where you have pairs of "Label: TextBox" in rows beneath each other?

My guess is yes, since this UI is pretty much standard in every configuration window ever designed. My guess is also that while designing this, you got frustrated with having to define new rows all the time in the Grid. Or that your StackPanels inside StackPanels didn't align the columns properly.

Wouldn't it be nice to combine the row/column layout of a grid with the simple no-need-to-define-anything style of the StackPanel?

Read on and I'll show a custom panel that achieves this.

The Problem

The Grid in WPF is extremely powerful and can achieve pretty much any layout thinkable. Unfortunately, it requires you to define all the rows and columns up front, and also forces you to specify for each of your child items in which cell of the grid they should be placed. To achieve the layout in the screenshot above, you would have to write something like this in XAML:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Label Content="Name:" />
    <TextBox Text="John Doe" VerticalAlignment="Center" Grid.Column="1" />
    <Label Content="Address:" Grid.Row="1" />
    <TextBox Text="34 Some Street, 123 45 SomeTown, Some Country" 
          VerticalAlignment="Center" Height="70" Grid.Row="1" Grid.Column="1"/>
    <Label Content="Position:" Grid.Row="2" />
    <TextBox Text="Manager" Grid.Row="2" Grid.Column="1"/>
</Grid>

What's worse is that if you want to move stuff around (or add things in the middle), you have to update all the Grid.Column="x" and Grid.Row="y" entries manually.

The Solution

Let's instead define a custom panel which has these properties:

  • No row/column definitions. Instead, implicitly assume two columns and an unbounded amount of rows.
  • No explicit positioning. Deduce position in the grid based on the order of the child elements.

child-ordering.png

As for column sizing, we define the following:

  • Column 1 will use only as much space as is needed by its widest child
  • Column 2 will use the remaining width

This has the effect that all items in column 2 will be aligned nicely which matches best practices in UI design for these kind of screens.

column-sizing.png

Furthermore, we'll add two properties to control the extra spacing between children.

  • RowSpacing - add a number of pixels between each row (default to zero)
  • ColumnSpacing - add a number of pixels between the two columns (default to zero)

The Code

Two create your own panel, all you need to do is to create a class that inherits from the Panel class. This class is abstract and requires you to override two methods:

  • MeasureOverride - Given a size constraint, measure all children and calculate a desired size for the panel
  • ArrangeOverride - Arrange all children according to the provided area given by the parent control/panel

Measuring the Children

In MeasureOverride, we start by measuring all the children that will appear in the first column:

// First, measure all the left column children
for (int i = 0; i < VisualChildrenCount; i += 2)
{
    var child = Children[i];
    child.Measure(constraint);
    col1Width = Math.Max(child.DesiredSize.Width, col1Width);
    RowHeights.Add(child.DesiredSize.Height);
}

Notice that we store the maximum width of the column 1 children. This will become the final width of the first column. We also store all the row heights so we can calculate our final height at the end. The reason we store the values in a list instead of simply adding them up is because we haven't yet measured the second column children. It's perfectly valid for the two children of a row to be of different heights.

Now, we have enough information to start measuring the second column:

// Then, measure all the right column children, they get whatever remains in width
var newWidth = Math.Max(0, constraint.Width - col1Width - ColumnSpacing);
Size newConstraint = new Size(newWidth, constraint.Height);
for (int i = 1; i < VisualChildrenCount; i += 2)
{
    var child = Children[i];
    child.Measure(newConstraint);
    col2Width = Math.Max(child.DesiredSize.Width, col2Width);
    RowHeights[i/2] = Math.Max(RowHeights[i/2], child.DesiredSize.Height);
}

The newWidth variable holds the remaining width available. It's basically the provided bounds minus the width of column 1 (minus any column spacing). Math.Max is there to make sure we don't get a negative size in case column 1 wants to occupy the entire width.

Notice also how we update the RowHeights values in case the right hand child happens to be higher than it's left hand sibling.

Finally, we have enough information to return our desired size to our parent:

return new Size(
                col1Width + ColumnSpacing + col2Width, 
                RowHeights.Sum() + ((RowHeights.Count - 1) * RowSpacing));

Arranging the Children

This is a bit more straightforward than the measure phase since we already have all the sizes ready:

/// <summary>
/// Position elements and determine the final size for this panel.
/// </summary>
/// <param name="arrangeSize">The final area where child elements should 
///                           be positioned.</param>
/// <returns>The final size required by this panel</returns>
protected override Size ArrangeOverride(Size arrangeSize)
{
    double y = 0;
    for (int i = 0; i < VisualChildrenCount; i++)
    {
        var child = Children[i];
        double height = RowHeights[i/2];
        if (i % 2 == 0)
        {
            // Left child
            var r = new Rect(0, y, Column1Width, height);
            child.Arrange(r);
        }
        else
        {
            // Right child
            var r = new Rect(Column1Width + ColumnSpacing, y, 
                             arrangeSize.Width - Column1Width - 
                             ColumnSpacing, height);
            child.Arrange(r);
            y += height;
            y += RowSpacing;
        }
    }
    return base.ArrangeOverride(arrangeSize);
}

We basically just iterate over all the children and place them in the correct location. The variable y maintains the vertical position of the current row, and each child's x position is calculated from the previously measured column widths.

That's basically it. The layout shown in the first screenshot at the top can now be achieved like this:

<local:TwoColumnGrid>
    <Label Content="Name:" />
    <TextBox Text="John Doe" VerticalAlignment="Center" />
    <Label Content="Address:" />
    <TextBox Text="34 Some Street, 123 45 SomeTown, Some Country" 
             VerticalAlignment="Center" Height="70" />
    <Label Content="Position:" />
    <TextBox Text="Manager" />
</local:TwoColumnGrid>

History

  • 2011-08-09: First version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here