Providing Custom Layout Engines for Windows Forms

 

Chris Anderson
Microsoft Corporation

September 2001

Summary: Windows Forms uses methods and events to provide rich custom layout. (19 printed pages)

Download the NewLayout.exe sample code.

Contents

Beyond Anchoring and Docking
What's In the Sample?
A Framework
Writing a Custom Engine
AutoLayout: An Engine

Beyond Anchoring and Docking

Windows Forms contains all the methods and events needed to provide rich custom layout. What it lacks, however, is an extensible framework for writing custom reusable layout engines, as well as a set of stock layout components that provide the most common types of layout.

The default layout support in Microsoft® Windows® Forms, anchoring and docking, allows for fairly rich user interface (UI) design. However, the challenge to developers is found in the implementation of localizable content-based dialog boxes. Consider this piece of UI:

Figure 1. Rename Toolbar

Although anchoring allows developers to attach the OK and Cancel buttons to the bottom right corner of the dialog box, users remain faced with the problem of what to do when the word Cancel is translated into a 46-character-long word in some other language.

In addition, anchoring and docking are really only effective when a dialog box is resizable. Although many dialog boxes should be resizable, it is common to find fixed size dialog boxes. However, dialog boxes do need to size themselves to fit the localized content they contain.

When faced with this issue, localizers must modify the coordinates of the UI elements on the dialog box. Windows Forms provides tools that make this process straightforward. Nevertheless, it would be more efficient if this process could be avoided altogether. Currently, to adjust the size of a dialog box, one must first change the dialog box layout and strings, and then conduct test passes on the localized applications.

The ultimate goal for a UI Layout Library is to enable dynamic layout for both the application authors and the localized UI. In this way, the localization cost would consist only of the translation cost, as the resizing of the dialog box would occur automatically via the dynamic layout. In this translation process, developers would change the strings associated with a dialog box, which would adjust itself automatically to account for the new dimensions of the translated strings.

What's In the Sample?

The compressed folder file (ZIP) should contain the following files:

File Description
NewLayout.sln Main solution file for Visual Studio® .NET
Providing Custom Layout Engines for Windows Forms.doc This document
NewLayout\  
AssemblyInfo.cs Assembly attributes
LayoutEngine.cs LayoutEngine base classes
NewLayout.csproj NewLayout C# project
AutoLayout\  
AutoLayout.cs AutoLayout engine
ControlLayoutInformation.cs Layout information support classes
IControlLayoutInformation.cs Layout information support interfaces
bin\Debug\* NewLayout project compiled for debug
bin\Release\* NewLayout project compiled for retail
Examples\  
ScaleLayout.cs Simple scale layout engine example
SimpleFlowLayout.cs Simple flow layout engine example
SampleForms\  
AssemblyInfo.vb Assembly attributes
licenses.licx Designer support file (for licensed components)
RenameToolbar.resx AutoLayout example
RenameToolbar.vb AutoLayout example
SampleForms.vbproj SampleForms Visual Basic® .NET project
ScaleForm.resx ScaleLayout example
ScaleForm.vb ScaleLayout example
SimpleFlowForm.resx SimpleFlowLayout example
SimpleFlowForm.vb SimpleFlowLayout example
bin\* SampleForms project compiled for retail

To begin, expand the archive into a directory and open NewLayout.sln in Visual Studio .NET.

A Framework

All Windows Forms controls provide a Layout event, along with a host of other notifications, which enables the writing of a complex layout code. To facilitate writing reusable layout engines, we can provide a basic framework.

NewLayout.LayoutEngine

The base class for this framework will be LayoutEngine, which will provide a common set of features for all layout engines. To start, the LayoutEngine introduces two concepts: Layout Container and Layout Item (or Layout Control). A Layout Container is a visual element that contains other elements. A Layout Item is a visual element contained within a Layout Container. A single element can be both a container and an item.

One of the goals of the NewLayout framework is to provide layout engines without requiring any changes to the core object model of Windows Forms. To accomplish this, extensive use of extender providers is used. Extender providers are components that implement System.ComponentModel.IExtenderProvider. This interface, along with the ProvidePropertyAttribute, allows a component to offer properties to other components hosted inside a designer. This feature allows for a nice design time experience without requiring runtime modifications to the objects, and therefore can be applied generically by an author other than the target objects themselves.

One limitation of extender providers is that a single provider must offer the same set of extender properties to all the components that it supports extending. To help sort through this confusion, it is strongly recommended that developers separate the provided properties from the intrinsic ones by setting the CategoryAttribute for any extender properties offered from your implementation of a LayoutEngine. In this case, the CategoryAttribute should be "Layout Container" for container-related properties, and "Layout Item" for item-related properties.

[ProvideProperty("Enabled", typeof(Control))]
public abstract class LayoutEngine : Component, IExtenderProvider {
    protected LayoutEngine() {...}
    protected LayoutEngine(IContainer container) {...}

    protected IEnumerable ContainerControls { get {...} }
    protected IEnumerable ItemControls { get {...} }

    public bool GetEnabled(Control container) {...}
    public void SetEnabled(Control container, bool value) {...}

    protected virtual bool CanExtendControl(Control control) {...}
    protected virtual ContainerProperties CreateContainerProperties(
                                                       Control control) {...}
    protected virtual ItemProperties CreateItemProperties(Control control) {...}

    protected object GetContainerProperties(Control control) {...}

    protected object GetItemProperties(Control control) {...}

    protected virtual void OnBindContainer(Control container) {...}
    protected virtual void OnBindControl(Control control) {...}

    protected abstract void OnLayout(object sender, LayoutEventArgs e) {...}

    protected virtual void OnUnbindContainer(Control container) {...}
    protected virtual void OnUnbindControl(Control control) {...}

    protected class ContainerProperties {
        ...
    }
    protected class ItemProperties {
        ...
    }
}

The only required method to implement is the OnLayout method, which is called whenever a container that is enabled (see SetEnabled method) has the Layout event raised. The bind and unbind methods allow a layout engine to hook events on items and containers. For example, it is possible to hook various property change events to force a re-layout.

Writing a Custom Engine

The first sample layout engine (SimpleFlowLayout) is a simple flow-based layout. This engine organizes items in a container from left to right and top to bottom. For any container in which the layout is enabled, the SimpleFlowLayout engine will walk each child control in z-order and arrange them from left to right. When the edge of the container is reached, the engine will move down one row and continue placing the controls.

To start, SimpleFlowLayout derives from LayoutEngine and provides the basic component constructors.

public sealed class SimpleFlowLayout : LayoutEngine {
    public SimpleFlowLayout() : base() {
    }
    public SimpleFlowLayout(IContainer container) : base(container) {
    }
    ...
}

The layout engine provides a margin property for the container, so the user must define a custom container layout property class and override the creation routine.

public sealed class SimpleFlowLayout : LayoutEngine {
    ...
    protected override ContainerProperties CreateContainerProperties(
                                                           Control control) {
        return new SimpleFlowLayoutProperties();
    }

    class SimpleFlowLayoutProperties : ContainerProperties {
        int margin;
        internal int Margin { 
            get {
                return margin;
            }
            set {
                margin = value;
            }
        }
    }
}

The CreateContainerProperties method will be invoked when the container properties are requested for a given container. Next, to offer the margin property, we must implement the get and set methods for it. Also, to enable designer support for the extender provider, we need to add the ProvidePropertyAttribute.

[ProvideProperty("Margin", typeof(Control))]
public sealed class SimpleFlowLayout : LayoutEngine {
    ...
    [Category("Layout Container"), DefaultValue(0)]
    public int GetMargin(Control control) {
        return ((SimpleFlowLayoutProperties)GetContainerProperties(control)).Margin;
    }
    public void SetMargin(Control control, int value) {
        ((SimpleFlowLayoutProperties)GetContainerProperties(control)).Margin = value;
        control.PerformLayout();
    }
    ...
}

This process brings up some interesting points. First, to add additional data storage per container, we overrode the CreateContainerProperties method, thus allowing us to return an object with the margin property on it. We can then use the GetContainerProperties method to get the data associated with the container. Second, the custom attributes for an extender property are always placed on the get method. When this property is displayed in the property browser, the category for the property will be "Layout Container." By convention, all container properties should be placed in the "Layout Container" category, while item properties should be placed in the "Layout Item" category.

Since the margin property is being offered on a container, we can simply call the PerformLayout method on the container to relay out the control when the property is adjusted. When item properties are changed it is important to call PerformLayout on the parent of the item, but not on the item itself.

Finally we have the OnLayout method:

public sealed class SimpleFlowLayout : LayoutEngine {
    ...   
    protected override void OnLayout(object sender, LayoutEventArgs e) {
        Control container = (Control)sender;
        SimpleFlowLayoutProperties containerProps = 
        (SimpleFlowLayoutProperties)GetContainerProperties(container);

        // start at 0, 0
        //
        int x = 0;
        int y = 0;
        int maxLineHeight = 0;

        foreach (Control control in container.Controls) {

            // If the right edge of this control is going to exceed the right
            // edge of the container, then increment "y" and reset "x"
            //
            if (x + control.Width > container.Width && maxLineHeight > 0) {
                y += maxLineHeight;
                x = 0;
                maxLineHeight = 0;
            }

            // Determine the max height of any control on this line
            //
            maxLineHeight = Math.Max(control.Height + 
         containerProps.Margin,
                                     maxLineHeight);

            // Position the current control
            //
            control.Left = x;
            control.Top = y;
 
            // Increment "x" by the width of the control and the margin
            //
            x += control.Width + containerProps.Margin;
        }
    }
    ...
}

This simple layout implementation is missing many of the features that you might want in a rich layout engine; it offers a only basic understanding of how to extend the layout framework. To use this layout engine, drop an instance of it on a Form, then click on any control that contains other controls, and set the Enabled property for the layout engine to True.

AutoLayout: An Engine

The AutoLayout engine demonstrates a complete layout engine that can be used to write complex UI that automatically resizes based on the content of the controls.

Container Properties

Name Type Description
LayoutMode ContainerLayoutMode Determines how the children in the container will be ordered. Options are None, HorizontalFlow, or VerticalFlow.
LeftPadding Int32 Number of pixels from the left edge of the container to start positioning controls when using either HorizontalFlow or VerticalFlow.
RightPadding Int32 Number of pixels from the right edge of the container to start positioning controls when using either HorizontalFlow or VerticalFlow.
TopPadding Int32 Number of pixels from the top edge of the container to start positioning controls when using either HorizontalFlow or VerticalFlow.
BottomPadding Int32 Number of pixels from the bottom edge of the container to start positioning controls when using either HorizontalFlow or VerticalFlow.

VerticalFlow will arrange all the children in z-order from the top to the bottom of the container. HorizontalFlow will arrange all the children in z-order from the left to the right of the container. When LayoutMode is set to None, the children are not arranged when the layout event is raised.

Item Properties

Name Type Description
SizeMode ItemSizeMode Determines how this control is sized based on contents. Options are Fixed, Minimum, Maximum, or Contents.
FillContainer Boolean Determines if the control will be sized to fill the container in the opposite direction of flow.
Spring Float Spring priority. If this is > 0.0, then this determines the percentange of space in the direction of flow this control will consume.
LeftMargin Int32 Number of pixels of spacing on left edge of the item when placed in a container with HorizontalFlow or VerticalFlow.
RightMargin Int32 Number of pixels of spacing on right edge of the item when placed in a container with HorizontalFlow or VerticalFlow.
TopMargin Int32 Number of pixels of spacing on top edge of the item when placed in a container with HorizontalFlow or VerticalFlow.
BottomMargin Int32 Number of pixels of spacing on bottom edge of the item when placed in a container with HorizontalFlow or VerticalFlow.

An item's SizeMode and margins properties interact with the container when AutoLayout is enabled in the container. Margins are used only when the container's LayoutMode is HorizontalFlow or VerticalFlow. However, SizeMode may be used inside a container with a LayoutMode of None. For example, a SizeMode of Contents will cause a control to size itself to its contents, regardless of the LayoutMode of the container.

Layout Information

When performing content-based layout for a layout item, it is important to get layout information from the control. This can be accomplished in one of two ways. Either the control can implement the IControlLayoutInformation interface, or you can handle the ProvideLayoutInformation event on the ControlLayoutInformation class. The interface and the event allow you to get preferred, minimum, and maximum sizes for a given control (described in detail below).

The layout engine calls the static methods on ControlLayoutInformation to determine the minimum, maximum, and preferred size of a control. If the control doesn't implement IControlLayoutInformation then the ProvideLayoutInformation event will be raised. If the event is not handled, then ControlLayoutInformation will perform some default logic for calculating the information.

Layout Model

The layout model supported by AutoLayout is based primarily on the horizontal and vertical flow containers. You can set any control that contains other controls, such as a Panel or GroupBox, to be a horizontal or vertical flow container. When a container is set to flow, the items inside it are stacked in order. For example, consider three fixed SizeMode items inside a VerticalFlow container:

Figure 2. Three fixed SizeMode items inside a VerticalFlow container

Before diving into some of the more complex interactions between LayoutMode and SizeMode, let's look at the various size modes.

Fixed

When this SizeMode is used the item will not be dynamically resized.

Minimum

Causes the item to be sized to the ControlLayoutInformation minimum size.

Maximum

Causes the item to be sized to the ControlLayoutInformation maximum size.

Contents

This will cause the item to size based on the contents of the control. If the control contains other controls (regardless of the LayoutMode) then the item will be sized to fit the outer edges of the control.

Figure 3. Item sized to fit the outer edges of the control

If the control contains no children, the ControlLayoutInformation preferred size is used. Normally this will fit the control to the text or images contained within the control. For example, the default logic for a Button will size the button to at least 75 x 23 pixels, and a maximum of the width of the text contained inside the button.

FillContainer

When FillContainer is true, the control works together with a horizontal or vertical flow container. When SizeMode is Fixed, it will use the current size of the item as the basis for layout. Meanwhile, Contents uses the content size of the item as the basis. When an item is set to one of the fill container size modes inside a flow container, that item will be positioned by the flow logic and then sized to fill the width of the container. For example:

Figure 4. Sized to fill the width of the container

The width of the items fit the size of the container while the position is set by the flow layout. The height of the item will be set either by the current size (using FillContainer and Fixed) or by the size of the contents (using FillContainer and Contents).

The layout becomes especially interesting when a container takes on the additional role of an item of an AutoLayout parent. For example, a Panel can contain multiple Buttons in which each Button is set to FillContainer and Content SizeMode. The Panel then can have a Content SizeMode with a VerticalFlow Layout mode. This will cause the Panel to size to the largest contained Button, and force all the buttons to have the same width (but be ordered in the Panel).

Figure 5. Panel sizes to the largest contained Button

If the user changes the text of one of the controls, the container will respond by recalculating its layout, causing it to increase in size. Since all the items are set to fill, they will stretch to fill the new size of the container.

Figure 6. Items stretching to fill the new size of the container

Spring values

A Spring value gives the controls in the container the ability to fill percentages of the container in the opposite direction of FillContainer. While FillContainer moves perpendicular to the flow, Spring moves in the same direction. Spring takes into account the minimum and maximum size, and then allocates the space in the container based on the percentage of the total Spring value of all controls. Spring values can be any number and are compared to the other controls in the container. For example:

Figure 7. Spring values

Figure 7 contains three buttons in a dock bottom panel. Each button has a Spring value of 1.0. Since the total of all the combined Spring values is 3.0, and each button has the same percentage (33 percent), then each will be relatively the same size.

Figure 8. Combining layout systems

Again, you can combine Spring, FillContainer, and SizeMode to produce complex layout systems that dynamically resize with content changes to controls and when the user resizes the forms.

NewLayout.AutoLayout.AutoLayout

The NewLayout.AutoLayout.AutoLayout class derives from LayoutEngine and provides most of the functionality of the AutoLayout engine. The only public methods that this class offers are the 20 methods needed to implement the 10 extender properties.

[
// container...
ProvideProperty("LayoutMode", typeof(Control)),
ProvideProperty("LeftPadding", typeof(Control)),
ProvideProperty("RightPadding", typeof(Control)),
ProvideProperty("TopPadding", typeof(Control)),
ProvideProperty("BottomPadding", typeof(Control)),

// item...
ProvideProperty("SizeMode", typeof(Control)),
ProvideProperty("FillContainer", typeof(Control)),
ProvideProperty("Spring", typeof(Control)),
ProvideProperty("LeftMargin", typeof(Control)),
ProvideProperty("RightMargin", typeof(Control)),
ProvideProperty("TopMargin", typeof(Control)),
ProvideProperty("BottomMargin", typeof(Control))
]
public class AutoLayout : LayoutEngine {
    public AutoLayout();
    public AutoLayout(IContainer container);

    [Category("Layout Container"), DefaultValue(0)]
    public int GetLeftPadding(Control container);
    public void SetLeftPadding(Control container, int value);

    [Category("Layout Container"), DefaultValue(0)]
    public int GetRightPadding(Control container);
    public void SetRightPadding(Control container, int value);

    [Category("Layout Container"), DefaultValue(0)]
    public int GetTopPadding(Control container);
    public void SetTopPadding(Control container, int value);

    [Category("Layout Container"), DefaultValue(0)]
    public int GetBottomPadding(Control container);
    public void SetBottomPadding(Control container, int value);

    [Category("Layout Container"), DefaultValue(ContainerLayoutMode.None)]
    public ContainerLayoutMode GetLayoutMode(Control container);
    public void SetLayoutMode(Control container, ContainerLayoutMode value);

    [Category("Layout Item"), DefaultValue(ItemSizeMode.FixedSize)]
    public ItemSizeMode GetSizeMode(Control item);
    public void SetSizeMode(Control item, ItemSizeMode value);

    [Category("Layout Item"), DefaultValue(false)]
    public bool GetFillContainer(Control item);
    public void SetFillContainer(Control item, bool value);

    [Category("Layout Item"), DefaultValue(0.0f)]
    public float GetSpring(Control item);
    public void SetSpring(Control item, float value);
    
    [Category("Layout Item"), DefaultValue(0)]
    public int GetLeftMargin(Control control);
    public void SetLeftMargin(Control control, int value);

    [Category("Layout Item"), DefaultValue(0)]
    public int GetRightMargin(Control control);
    public void SetRightMargin(Control control, int value);

    [Category("Layout Item"), DefaultValue(0)]
    public int GetTopMargin(Control control);
    public void SetTopMargin(Control control, int value);

    [Category("Layout Item"), DefaultValue(0)]
    public int GetBottomMargin(Control control);
    public void SetBottomMargin(Control control, int value);
}

NewLayout.AutoLayout.ContainerLayoutMode

NewLayout.AutoLayout.ContainerLayoutMode determines how the items in a container are arranged.

public enum ContainerLayoutMode { 
    None = 0,
    HorizontalFlow,
    VerticalFlow,
}

NewLayout.AutoLayout.ItemSizeMode

NewLayout.AutoLayout.ItemSizeMode determines how an item is sized.

public enum ItemSizeMode {
    FixedSize = 0,
    MinimumSize,
    MaximumSize,
    Contents,
}

NewLayout.AutoLayout.ControlLayoutInformation

NewLayout.AutoLayout.ControlLayoutInformation provides the methods that a layout engine can call to get layout information. Also, application authors can hook the ProvideLayoutInformation event to offer layout information for controls that do not implement the IControlLayoutInformation.

public class ControlLayoutInformation {
    public static event ProvideLayoutInformationEventHandler 
      ProvideLayoutInformation;

    public static Size GetPreferredSize(Control control);
    public static Size GetMinimumSize(Control control);
    public static Size GetMaximumSize(Control control);
}

NewLayout.AutoLayout.LayoutInformationType

When handling the ProvideLayoutInformation event this enum describes the value layout sizes that are being requested.

public enum LayoutInformationType {
    PreferredSize,
    MinimumSize,
    MaximumSize,
}

NewLayout.AutoLayout.IControlLayoutInformation

A control can provide layout information by implementing this interface.

public interface IControlLayoutInformation {
    Size PreferredSize { get; }
    Size MinimumSize { get; }
    Size MaximumSize { get; }
}

NewLayout.AutoLayout.ProvideLayoutInformationEventArgs

NewLayout.AutoLayout.ProvideLayoutInformationEventArgs represents the data associated with the ProvideLayoutInformation.

public class ProvideLayoutInformationEventArgs : EventArgs {
    public ProvideLayoutInformationEventArgs(Control control,
                                    LayoutInformationType requested);

    public LayoutInformationType Requested { get; }
    public Control Control { get; }
    public Size Size { get; set; }
    public bool Handled { get; set; }
}

NewLayout.AutoLayout.ProvideLayoutInformationEventHandler

NewLayout.AutoLayout.ProvideLayoutInformationEventHandler is the delegate type with which the ProvideLayoutInformation is implemented.

public delegate void ProvideLayoutInformationEventHandler(object 
      sender, ProvideLayoutInformationEventArgs e);

Using the AutoLayout Engine

The AutoLayout engine provides a fairly simple set of properties; however, using these various properties to produce nice UI can be complex. Also, it is often necessary to add extra panels to contain controls to get the look you want.

Consider this UI:

Figure 9. Adding panels

The UI is designed like so:

Figure 10. Adding panels to UI design

Form1 is a Contents SizeMode, with HorizontalFlow layout.

Panel2 is FillContainer and Fixed SizeMode, with VerticalFlow layout.

Panel1 is Contents SizeMode, with VerticalFlow layout.

Button1, 2, and 3 are all FillContainer and Contents SizeMode.

With this setup, the TextBox controls are basically fixed size. Any increase in the content sizes of the buttons will cause the form to grow automatically. For example:

Figure 11. Increasing the content sizes

In addition, to get the correct spacing, the top, left, right, and bottom padding is set to five pixels on both panels. All Buttons and TextBoxes also have a bottom margin of five pixels, except for the Reset button, which has no bottom margin.

Future Features for AutoLayout

The AutoLayout engine is not as full-featured as you might need for some applications; some key features need to be added to this sample in the future:

  • Interaction with AutoScroll controls
  • Interaction with AutoScale controls for high DPI
    Padding and Margin values are absolute pixel values that will not scale.
  • Honor DisplayRectangle property for container controls
    This could eliminate the need for tweaking padding for items like GroupBox.
  • Content-based minimum sizing
    • It is common to want a container to be resizable with a minimum size based on the controls contained within it. Currently, containers are either exactly as big as the contents or are resizable.
    • This could also be applied at a Form level to get Min/Max track size working automatically.
  • Reverse flow
    Right-to-left horizontal, and top-to-bottom vertical, Reverse flow is useful for right-align controls.
  • Layout mode interaction
    Currently, having Spring, FillContainer, and SizeMode set generates some odd behavior. This should be cleaned up so that the layout code becomes more fault-tolerant.