Welcome to WindowsClient.net | Sign in | Join

Providing Custom Layout Engines for Windows Forms

Get the samples for this article.

Windows Forms provides all the needed methods and events for providing rich custom layout. However, what is lacking is an extensible framework for writing custom reusable layout engines. In addition, there needs to be a set of stock layout components that provide the most common types of layout.

Beyond Anchoring and Docking

The default layout support in Windows Forms, anchoring and docking, allows for fairly rich user interface (UI) design. However, the place where this really falls down is in the implementation of localizable content based dialogs. Consider this piece of UI:

Although anchoring will allow you to have the "OK" and "Cancel" buttons attach to the bottom right corner of the dialog, you are faced with the problem of what happens when the word "Cancel" is translated into a 46 character long word in some other language.

In addition, anchoring and docking really only work when a dialog is resizable. Although many dialogs should be resizable, it is common to have fixed size dialogs, however they do need to size to fit the localized content they contain.

When faced with this problem today, localizers have to modify the coordinates of the UI elements on the dialog. Windows Forms provides tools that make this process straight forward, however it would be more efficient if this process could be avoided completely.

The ultimate goal for a UI Layout Library would be to enable dynamic layout for the application authors but also for the localized UI. In that way the localization cost would only consist of the translation cost since the resizing would be done automatically via the dynamic layout. This translation process is one where one you change the strings associated with a dialog, the dialog automatically adjusts itself to take into account the new dimensions of the translated strings, whereas today application localizers must change dialog layout as well as strings, requiring complete test passes on the localized applications.

What is 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 start, expand the archive into a directory, and open NewLayout.sln in Visual Studio .NET.

A Framework

All Windows Forms controls provide a Layout event, and a host of other notifications, that enable 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. This class will provide a common set of features for all layout engines. To start with, 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. An important thing to note is that a single element can be both a container and 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 allows for a nice design time experience without having to make runtime modifications to the objects, and therefore can be applied generically by a different author 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 components that is supports extending. To help people with this confusion, it is strongly recommended to 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. This 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, top to bottom. For any container that layout is enabled, the SimpleFlowLayout engine will walk each child control in z-order and arrange them left to right. When the edge of the container is hit, the entire will move down one row and continue placing the controls.

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

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

Since the layout engine is going to provide a margin property for the container, then we 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();
            }
            ...
        }

There are a couple of interesting points here. First, to add additional data storage per container we overrode the CreateContainerProperties method. This allows us to return a object with the Margin property on it. We can then use 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. So 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 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 relayout the control when the property is adjusted. When item properties are changed it is important to call PerformLayour on the parent of the item, not the item itself.

Finally we have the OnLayout method itself.

        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 is a very simple layout implementation and is missing many of the features that you would want in a rich layout engine; it gives you a basic understanding of how to extend the layout framework. To use this layout engine, drop an instance of it on a Form, and 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 upon 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 interact with the container when AutoLayout is enabled in the container. Margins are only used 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. That can be accomplished 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 size for a given control (these are 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 that AutoLayout supports is primarily based on the horizontal and vertical flow containers. You can set any control that contains other controls to be a horizontal or vertical flow container - like a Panel or GroupBox. When a container is set to flow the items inside it are stacked in order. For example, consider 3 Fixed SizeMode items inside a VerticalFlow container:

Before diving into some of the more complex interactions between LayoutMode and SizeMode, lets 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 upon the contents of the control. If the control contains other controls (regardless of the LayoutMode) then the control will be 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 will use the current size of the item as the basis for layout, while Contents uses the content size of the item as the basis. When a item is set to one of the fill container size modes inside of a flow container that item will be positioned by the flow logic, and then sized to fill either the width or either of the container. For example:

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 either set by the current size (using FillContainer and Fixed) or by the size of the contents (using FillContainer and Contents).

The layout becomes really interested when you have a container that plays a role as both a container and an item of an AutoLayout parent. For example, a Panel can contain multiple Buttons, where 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 all the buttons to have the same width, but be ordered in the Panel.

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

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

Has three buttons in a dock bottom panel. Each button has a Spring of 1.0. Since the total of all the combined spring values is 3.0, and each button has the same percentage (33%), then they will all be relatively the same size.

Again, you can combine spring, fillcontents, and sizemode to produce very complex layout systems that dynamically resize both from content changes to controls and user resizing of forms.

This class derives from LayoutEngine and provides most of the functionality of the AutoLayout engine. The only public method 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

Determines how the items in a container are arranged.

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

NewLayout.AutoLayout.ItemSizeMode

Determines how an item is sized.

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

NewLayout.AutoLayout.ControlLayoutInformation

This class 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 don't 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 values layout sizes that is being requested.

        public enum LayoutInformationType {
            PreferredSize,
            MinimumSize,
            MaximumSize,
        }

NewLayout.AutoLayout.IControlLayoutInformation

A control can provide layout information itself by implementing this interface.

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

NewLayout.AutoLayout.ProvideLayoutInformationEventArgs

This is 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

This is the delegate type that the ProvideLayoutInformation is implemented with.

        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. In addition it is often necessary to add extra panels to contain controls to get the look that you want.

Consider this UI:

The UI is designed like so:


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, and any increase in the content sizes of the buttons will cause the form to automatically grow. For example:

In addition, to get the correct spacing, the Top, Left, Right, and Bottom padding is set to 5 pixels on both panels. All the Buttons and TextBoxes have a Bottom margin of 5 pixels also, except for the "Reset" button, which has no bottom margin.

Future Features for AutoLayout

Obviously the AutoLayout engine isn't as full featured as you may need for some applications. There are a couple of key features that need to be added to this sample in the future:

  • Interaction with AutoScroll controls
  • Interaction with AutoScale controls for high DPI   
    - Padding & Margin values are absolute pixel values that won't scale
  • Honor DisplayRectangle property for container controls.   
    - This could eliminate the need for tweaking padding for things like GroupBox
  • Content based minimum sizing   
    - It is common to want a container to be resizable, but to have a minimum size based upon the controls contained within it. Currently as a container you are either exactly as big as your contents, or you 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. Useful for right align controls
  • Layout mode interaction   
    - Currently you get some odd behavior with spring, fill contents, and size mode all set. Need to clean this up and get the layout code a little more fault tolerant.