Thursday, 3 May 2012

Understanding the WPF Layout System

Many people don't understand how the WPF layout system works, or how that knowledge can help them in their projects. I intend to shine a little light on the mechanics behind all those cool layout controls in WPF.

To explain the system, I will give a step-by-step example of creating a custom panel for WPF. This panel will be similar to the StackPanel, with the difference that it will expand the last child to fill the remaining space.

Step 1: Create a new project

Create a new WPF Application project called WpfLayoutSystem.

Step 2: Add the custom panel

Add a new class to the project. Call it ExpandingStackPanel.cs.

To create your own custom panel, create a class that inherits from Panel, and override MeasureOverride and ArrangeOverride. That's it. This is the most basic panel, which doesn't add any functionality, yet.

using System;
using System.Windows;
using System.Windows.Controls;

namespace WpfLayoutSystem
{
    class ExpandingStackPanel : Panel
    {
        protected override Size MeasureOverride(Size availableSize)
        {
            return base.MeasureOverride(availableSize);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            return base.ArrangeOverride(finalSize);
        }
    }
}

Step 3: Measure

A layout pass is made up of two steps. Measuring is the first. In the measure step, each control determines how much space it needs. Panels do this by measuring the child controls, and then basing it's desired size on the child controls. Content controls base their size on their content. Each panel tells its children how much space is available, and each child tells its parent how much space it wants.

Lets examine how this works by writing an example. Here is the code for MeasureOverride.

protected override Size MeasureOverride(Size availableSize)
{
    double sumX = 0.0;
    double maxY = 0.0;
    foreach (UIElement child in this.Children)
    {
        child.Measure(new Size(Math.Max(availableSize.Width - sumX, 0.0), availableSize.Height));
        sumX += child.DesiredSize.Width;
        maxY = Math.Max(maxY, child.DesiredSize.Height);
    }
    return new Size(sumX, maxY);
}

Step 4: Arrange

The second step in the layout pass is arranging. In the arrange step, each control arranges its content or children based on the available space. Each panel tells its children how much space they have been given. Each child tells the parent how much space it actually used. You may ask why the measure and arrange passes aren't combined. Often a control is given more or less space than it asked for in the measure pass, and then it will arrange itself differently.

To see how this works, let's write the ArrangeOverride method.

protected override Size ArrangeOverride(Size finalSize)
{
    double x = 0.0;
    for (int i = 0; i < this.Children.Count - 1; i++)
    {
        UIElement child = this.Children[i];
        child.Arrange(new Rect(x, 0.0, child.DesiredSize.Width, child.DesiredSize.Height));
        x += child.DesiredSize.Width;
    }
    if (this.Children.Count > 0)
    {
        UIElement lastChild = this.Children[this.Children.Count - 1];
        lastChild.Arrange(new Rect(x, 0.0, Math.Max(finalSize.Width - x, 0.0), lastChild.DesiredSize.Height));
    }
    return finalSize;
}

Step 5: Using the panel

I used this code to test the panel. Feel free to use it with other code, but I can't claim that it will work in all scenarios. It was just designed as an example. Anyway, here is MainWindow.xaml.

<Window x:Class="WpfLayoutSystem.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfLayoutSystem"
        Title="MainWindow" Height="350" Width="525">
    <local:ExpandingStackPanel>
        <Button>A Button</Button>
        <Ellipse Width="80" Height="50" Stroke="Black" />
        <TextBlock>Some</TextBlock>
        <RadioButton>Other</RadioButton>
        <TextBox>Controls</TextBox>
        <Button>Another Button</Button>
    </local:ExpandingStackPanel>
</Window>


And here it is in action.



For your convenience, here is the entire ExpandingStackPanel class.

using System;
using System.Windows;
using System.Windows.Controls;

namespace WpfLayoutSystem
{
    class ExpandingStackPanel : Panel
    {
        protected override Size MeasureOverride(Size availableSize)
        {
            double sumX = 0.0;
            double maxY = 0.0;
            foreach (UIElement child in this.Children)
            {
                child.Measure(new Size(Math.Max(availableSize.Width - sumX, 0.0), availableSize.Height));
                sumX += child.DesiredSize.Width;
                maxY = Math.Max(maxY, child.DesiredSize.Height);
            }
            return new Size(sumX, maxY);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            double x = 0.0;
            for (int i = 0; i < this.Children.Count - 1; i++)
            {
                UIElement child = this.Children[i];
                child.Arrange(new Rect(x, 0.0, child.DesiredSize.Width, child.DesiredSize.Height));
                x += child.DesiredSize.Width;
            }
            if (this.Children.Count > 0)
            {
                UIElement lastChild = this.Children[this.Children.Count - 1];
                lastChild.Arrange(new Rect(x, 0.0, Math.Max(finalSize.Width - x, 0.0), lastChild.DesiredSize.Height));
            }
            return finalSize;
        }
    }
}


Hopefully that gave you an idea of how the layout system works in WPF.

Until next time!

1 comment: