Click here to Skip to main content
Click here to Skip to main content

A Two Column Grid for WPF

, 8 Aug 2011 CPOL
Rate this:
Please Sign up or sign in to vote.
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

isaks
Software Developer ABB
Sweden Sweden
My name is Isak Savo and I work as a Research Engineer at ABB Corporate Research in Västerås, Sweden. My work is focused around user experience which includes a lot of prototyping of new solutions for user interfaces and user interaction.
 
While I have a background in C programming in a Linux environment, my daily work is mostly spent in Windows using C# and WPF.
Follow on   Twitter

Comments and Discussions

 
GeneralAnother approach PinmemberOlivier Levrey3-Apr-14 3:18 
QuestionDoesn't work with Visibility.Collapsed PinmemberDaveTompkins30-Sep-13 20:11 
GeneralWPF panels PinmemberWPFAdmin18-Jan-13 23:43 
QuestionNot working in case of Binding PinmemberSerge Calderara13-Nov-12 5:31 
AnswerRe: Not working in case of Binding Pinmemberisaks15-Nov-12 6:46 
Generalfor ItemsPanel [modified] Pinmemberbgaboo6-Aug-12 23:29 
GeneralRe: for ItemsPanel Pinmemberisaks6-Aug-12 23:41 
GeneralRe: for ItemsPanel Pinmemberbgaboo7-Aug-12 1:59 
GeneralMy vote of 5 Pinmembernjdnjdnjdnjdnjd9-Aug-11 4:38 
QuestionMy vote of 5 PinmemberFilip D'haene9-Aug-11 3:41 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.141015.1 | Last Updated 9 Aug 2011
Article Copyright 2011 by isaks
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid