Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Windows Forms Layout Managers


November, 2004: Windows Forms Layout Managers

Richard is the author of Programming with Managed Extensions for Microsoft Visual C++ .NET 2003 (Microsoft Press, 2003). He can be contacted at [email protected].


Windows Forms is the .NET library for writing windows-based GUI applications. The Windows Forms library is just an abstraction over the classic Windows windowing API. Part of this abstraction is to treat windows as being forms that contain collections of controls—each control is a child window that can be positioned on the form. The default implementations of the Form and Control classes give you a basic layout mechanism, but these classes are designed to let you write your own layout manager. In this article, I outline the details of the standard layout mechanism in Windows Forms and present some code for an alternative layout manager.

Layout of Controls

The Windows Forms library comes with a straightforward layout manager: At design time, you determine the (x,y) position of the control relative to the upper left point of the control container's client area, and the z-order of the control relative to other controls. The (x,y) position is specified through the Top and Left properties or the Location property; all of these properties call the SetBounds method. Usually, the control container is a Form, but there are three other containers that you are likely to use: Panel, GroupBox, or TabPage. As the name suggests, a TabPage control contains one or more controls shown on a tabbed page; each page is itself a member of the container control, TabControl. The other two controls are used to group together controls on a form; the difference between the two is that GroupBox has a frame and caption. So, for example, if you add a Panel to a Form and add some child controls to the Panel, the child control's Left and Top properties are its position relative to the upper left point of the Panel's client area, not the Form's.

Setting the z-order is not so straightforward because it depends on the order that you add controls to the container's Controls collection. The constructor in Listing One adds three buttons to the form, each button is 60 pixels wide but, as you can see, the buttons are positioned so that their left edges are 40 pixels apart. This means that the buttons overlap. If you run this code, you'll find that the button that is inserted first appears at the top (and has a z-order of zero), the other buttons are stacked behind this button in the order that they are inserted (Figure 1).

You have a couple of options about changing the z-order. You can place any control at the top of the z-order by calling BringToFront, and to the back of the z-order with SendToBack; however, if you wish to change the order of more than one control, then the process can get complicated. To get more control, you can call the SetChildIndex method on the Controls collection. This method allows you to reposition a control anywhere in the collection and, thus, change its z-order position. The click handler in Listing One shows how to use this method to bring the button that was clicked to the top of the z-order.

Autoscaling

Another property to consider is AutoScale. This property determines whether the Windows Forms library uses the Display DPI to resize the controls. For example, if you use the Display Properties' Advanced button to change the dots per inch (DPI) from Normal (96 DPI) to Large (120 DPI), then the size of the display fonts increases and this may mean that controls such as labels or buttons become too small for their captions. If you set AutoScale to True, then the Windows Forms library uses another property—AutoScaleBaseSize—which has the average width and height of characters of the form's font. When a form is loaded, it calls the GetAutoScaleSize method to compare the size of the system font to the sizes in AutoScaleBaseSize; if these sizes differ, then the form calls the protected virtual method ScaleCore, which adjusts the height and width of the container and the height, width, and positions of all of its child controls accordingly.

I like the idea of autoscaling and wish that it was used throughout Windows: I have my laptop display DPI set to large font, but I often find dialogs where the controls are sized according to the normal display DPI. Hence, the controls are squashed together and their text is unreadable. The problem here is that developers are given the responsibility of determining the size of controls and, naturally, they do this using the fonts on their machines (or at best, the default fonts). Writing resizing and repositioning code to take into account every possible font setting is not the sort of programming that most developers want to spend their time doing. The Avalon technology in Longhorn has the concept of all controls being part of a tree of controls "inheriting" values from parent controls in the tree, so it has the promise of solving this issue.

Docking and Anchoring

The Control class has the nested class LayoutManager, which contains static methods to perform control layout. In addition to the relative position specified by the Left and Top properties, controls also specify their layout preferences through the Anchor and Dock properties.

Anchoring keeps constant the distance between the specified control edge and the container edge, even when the container is resized. For example, if you change a control's anchoring so that it is only anchored on its left side. Then, when you change the container width, the control always appears the same distance from the left edge of the container. However, when you change the container height, the control moves; in this case, the LayoutManager keeps constant the distance between the control and the center of the container. In Figure 2, I anchored the button to the left and drew a line across the middle of the form's client area. As you can see, when the form is resized, the button remains the same distance from the left edge and from the center line.

If you anchor a control to two opposing edges, then the control will be resized when the container is resized perpendicular to these edges. For example, if you anchor a control to both the left and right edges of its container, when resizing the width of the container, the LayoutManager tries to keep the left edge of the control the same distance from the left edge of the container, and it tries to keep the right edge of the control the same distance from the right edge of the container; the only way it can do this is to change the width of the control.

A control can also be docked to its container. Like anchoring, you specify an edge where the control is docked but, unlike anchoring, the control is always resized to cover the edge where it is docked. If you dock two controls to the same edge, then they will be "stacked up," one next to the other and the control with the highest z-order will be positioned closest to the edge. You can also use docking to indicate that a control should occupy the remaining area of a container not occupied by other controls.

The docking order of controls is a bit confusing. One use for docking is to have two controls in a container and to use a Splitter control to determine the relative sizes of these controls. For example, you could dock a TreeView to the left edge and add a ListView to the remaining area of the control container by setting its Dock property to Fill. A Splitter control could be used to adjust the width of the two controls; see Figure 3. In this case, the controls have to be added to the container's Controls property in the order ListView, Splitter, TreeView; that is, the control that fills the remaining space is added to the Controls collection first and the control that is nearest the left edge is added last.

Custom Layout Managers

Each control container has an event called Layout that is raised by the PerformLayout method. PerformLayout is called whenever there are changes to the Controls container (by adding or removing a control or by changing a control's index), or when the container is resized, moved, or its visibility is changed. Performing a layout whenever the contents of Controls is changed can be a bit of a pain and, if it occurs while the Control is being constructed, much of the layout work is wasted. For this reason, Control provides the SuspendLayout method to prevent the Layout event being raised until the ResumeLayout method is called.

The Layout event is raised when PerformLayout calls the virtual protected method OnLayout. Most controls do not override this method but, since it is protected and virtual, you can override it in a derived class. Listing Two shows the implementation of OnLayout for Control and, as you can see, all it does is raise the event and then call LayoutManager.OnLayout. The LayoutManager method iterates through all of the controls in the Controls collection and, if any are docked or anchored, it performs docking and/or anchoring.

If you want to handle your own layout, you have one of two choices: You can either provide a Layout event handler or you can override OnLayout. You should only consider the former if you want to make small changes to the standard layout management; for example, change the layout of controls that are neither docked nor anchored. If you want to provide a completely different type of layout, then it makes more sense to override OnLayout because you do not want to use the functionality of Control.LayoutManager.

FlowPanel Layout Manager

In the example for this article, I decided to follow the convention used in Windows Forms and create a panel control to perform layout. At runtime, a panel control is not visible—it is merely used as a container for controls. I decided that I would create a control that would provide flow layout. That is, the FlowPanel lets you add controls to it and it attempts to put the controls next to each other until the right edge of a control extends beyond the right edge of the panel; at which point, the control will be moved to the next line. I also decided to provide three alignment options: The default alignment will align the top edge of each control on a particular line, the other two options will align the centers or the bottom edge. I also wanted the control to be usable in the Visual Studio .NET Forms Designer, which meant I had to add some code to perform this integration.

To use the FlowPanel, you drag it from the Toolbox and drop it onto your form, where you can resize it, anchor it, or dock it. Then you drag other controls from the ToolBox and drop them onto the FlowPanel and these controls are added to the FlowPanel's control collection. Figure 4 shows the FlowPanel control added to the Toolbox through the Add/Remove Items context menu item. A FlowPanel control has been dropped on the right side of the form.

The panel is not visible at runtime or design time, but I wanted to provide some visual feedback at design time so that the developer could at least see its edge. To do this, I provided a control designer for the FlowPanel. The Windows Forms library provides two more: ControlDesigner and ParentControlDesigner. The class that is appropriate here is ParentControlDesigner because it is associated with a container control.

Listing Three shows the control designer and I have overridden two methods. Initialize is called after the designer is created and is passed the control to which it is associated. The other method is OnPaintAdornments, which is called when the control is created or resized and lets the designer add additional user-interface items. In this example, I use this method to draw a gray dashed line around the panel. If you look carefully at Figure 4, you see that there is a gray dashed line around the buttons on the right part of the form, which is the outline of the FlowPanel control.

Also notice that the designer is associated with a LayoutPanel, an abstract class that I have used as the base class of the FlowPanel control in Listing Four. The first reason for this class is to ensure that ResizeRedraw is set to True so that, whenever the panel is resized, it redraws the interior of the control (in this case, clears the interior). The other reason is to ensure that derived classes implement OnLayout. To do this, I provide an abstract override of the method. The designer is associated with the panel through the [Designer] attribute; note that this attribute is inherited, so any derived class also uses this designer.

Details of the FlowPanel

Listing Five shows a partial implementation for the FlowPanel. The control should be installed into the Visual Studio Toolbox so that developers can drag and drop the control. I provide an image for the control through the [ToolboxBitmap] attribute. I need to make a few comments about this attribute. The attribute associates the control with a bitmap, this bitmap should be 16×16 pixels and should be a 16-color image embedded as a .NET resource. However, I must warn you that the Toolbox does change some of the colors in the image. I cannot find any specific rules in the documentation, but from experimenting, I have found that pure green (RGB value of 0x00FF00) is treated as the background color and will be replaced with the background color of the Toolbox. Also, cyan (RGB value of 0x00FFFF) will be dithered by the Toolbox. If you look at the bottom of the Toolbox in Figure 4, you'll see the image for the FlowPanel control.

The OnLayout method performs the layout procedure (Listing Six). In this code, I define a reference line and the controls are placed relative to this line. For Top alignment, the reference line is along the top of the controls; for Middle, this line runs through the center of the controls; and for Bottom, this line is along the bottom of the controls. When OnLayout is called, it iterates through all of the controls in the Controls container and performs the layout logic on each one. This logic determines whether the control will fit on the current line. For Top alignment, the code calculates from the control's height where the next reference line will be located and a similar calculation is made for Middle alignment—except that, to determine this line, the calculation really needs the height of the control that is placed on the next line. Of course, this will only be known when a control is placed there! Thus, for Middle alignment the next "reference line" is in fact the top of the next line of controls and is also used in the calculation to determine whether the height of the current line is big enough for the controls on the line. For Bottom alignment, the code maintains the last position of the reference line so that there is a measure of the height of the current line of controls.

For Middle and Bottom alignment, a check is made to see whether the current control fits in the space allocated for the current line; if not, then the reference line is recalculated and the other controls in the line are moved so that they are relative to this new line. This is the purpose of the thisLine collection of controls. If the right edge of the current control extends past the right edge of the panel, then the control is moved to the next line of controls. For Top alignment, this is straightforward because it is the next reference line position that is maintained in the code. For Middle alignment, the next reference line position is actually the top of the control on the next line, but since the height of this control is now known, the actual position of the reference line through the center of the control can be calculated. For Bottom alignment controls, the reference line is shifted by the current control's height (and the vertical padding between controls).

Wrap Up

Figure 5 shows an example that uses the FlowPanel. On the left side, there is a GroupBox docked to the left edge containing three RadioButtons. On the right side, there is a FlowPanel with its Dock property set to Fill and this contains three buttons of varying sizes. When you change the RadioButton on the left side, the Alignment property of the FlowPanel is changed. This in turn forces the panel to perform a new layout. Figure 5 shows how the controls move according to the changes in the Alignment property.

The FlowPanel has its Dock property set to Fill, which means that, when the form is resized, the FlowPanel is resized and the layout is performed. Figure 6 shows the controls aligned with VerticalAlignment.Middle and the form's width increased so that all the controls are in a line.

DDJ



Listing One

class App : Form
{
   App()
   {
      Button b1, b2, b3;
      b1 = new Button();
      b1.Left = 0;
      b1.Width = 60;
      b1.Text = "One";
      b1.Click+=new EventHandler(click);
      b2 = new Button();
      b2.Left = 40;
      b2Width = 60;
      b2.Text = "Two";
      b2.Click+=new EventHandler(click);
      b3 = new Button();
      b3.Left = 80;
      b3.Width = 60;
      b3.Text = "Three";
      b3.Click+=new EventHandler(click);
      this.Controls.AddRange(new Control[]{b1, b2, b3});
   }
   void click(object sender, EventArgs args)
   {
      Controls.SetChildIndex((Control)sender, 0);
   }
   static void Main()
   {
      Application.Run(new App());
   }
}
Back to article


Listing Two
protected virtual void OnLayout(LayoutEventArgs levent)
{ 
   LayoutEventHandler handler;
   if (this.CanRaiseEvents)
   {
      handler = ((LayoutEventHandler) 
         base.Events[Control.EventLayout]);
      if (handler != null)
      {
         handler.Invoke(this, levent);
      } 
   }
   LayoutManager.OnLayout(this, levent);
}
Back to article


Listing Three
public class LayoutPanelDesigner : ParentControlDesigner
{
   private LayoutPanel panel;
   public override void Initialize(IComponent component)
   {
      base.Initialize(component);
      // Cache the control so that we can use it later
      panel = component as LayoutPanel;
   }
   protected override void OnPaintAdornments(PaintEventArgs e)
   {
      base.OnPaintAdornments(e);
      using (Pen pen = new Pen(Color.Gray, 1))
      {
         pen.DashStyle = DashStyle.Dash;
         e.Graphics.DrawRectangle(pen, 0, 0, panel.Width-1, panel.Height-1);
      }
   }
}
Back to article


Listing Four
[Designer(typeof(LayoutPanelDesigner))]
public abstract class LayoutPanel : Control
{
   public LayoutPanel()
   {
      ResizeRedraw = true;
   }
   protected abstract override void 
      OnLayout(LayoutEventArgs e);
}
Back to article


Listing Five
public enum VerticalAlignment : byte
{ Top=0, Middle=1, Bottom=2 }

[ToolboxBitmap(typeof(FlowPanel), "FlowPanel.bmp")]
public class FlowPanel : LayoutPanel
{
   private int vertPad = 5;
   private int horizPad = 5;
   private VerticalAlignment align = VerticalAlignment.Top;
   protected override void OnLayout(LayoutEventArgs e);
   [Category("Layout"), DefaultValue(5), 
    Description("The vertical distance " + "between controls")]
   public int VerticalPad
   {
      get{return vertPad;}
      set
      {
         vertPad = value;
         PerformLayout();
      }
   }
   [Category("Layout"), DefaultValue(5),
    Description("The horizontal distance " + "between controls")]
   public int HorizontalPad
   {
      get{return horizPad;}
      set
      {
         horizPad = value;
         PerformLayout();
      }
   }
   [Category("Layout"),
    DefaultValue(VerticalAlignment.Top),
    Description("Determines how controls " + "are aligned on a line")]
   public VerticalAlignment Alignment
   {
      get{return align;}
      set
      {
         align = value;
         PerformLayout();
      }
   }
}
Back to article


Listing Six
protected override void OnLayout(LayoutEventArgs e)
{
   if (Controls.Count > 0) 
   {
      int nextCtrlRefLine = 0;
      int prevCtrlRefLine = 0;
      int ctrlLeft = horizPad;
      int ctrlRefLine = 0;
      if (align == VerticalAlignment.Top)
         ctrlRefLine = vertPad;
      // This contains the controls on the current line                        
      ArrayList thisLine = new ArrayList();
      foreach (Control c in Controls) 
      {
         // Make invisible to stop flicker
         c.Visible = false;
         // See if right edge of control is beyond the panels right edge
         if (ctrlLeft + c.Width > this.Width) 
         {
            prevCtrlRefLine = ctrlRefLine + vertPad/2;
            switch (align)
            {
            case VerticalAlignment.Top:
               ctrlRefLine = nextCtrlRefLine;
               break;
            case VerticalAlignment.Middle:
               ctrlRefLine = nextCtrlRefLine + c.Height/2 + vertPad/2;
               nextCtrlRefLine = ctrlRefLine + c.Height/2 + vertPad/2;
               break;
            case VerticalAlignment.Bottom: ctrlRefLine += c.Height + vertPad;
               break;
            }
            ctrlLeft = horizPad;
            thisLine.Clear();
         }
         // Calculate position of control; see if reference line should change
         switch (align)
         {
            // Reference line is the top of controls
            case VerticalAlignment.Top:
            {
               if (nextCtrlRefLine < (ctrlRefLine + c.Size.Height + vertPad)) 
                  nextCtrlRefLine = ctrlRefLine + c.Size.Height + vertPad;
               c.Location = new Point(ctrlLeft, ctrlRefLine);
               break;
            }
            // The reference line is halfway between rows of controls
            case VerticalAlignment.Middle:
            {
               // If this is first control added, calculate reference lines
               if (ctrlRefLine == 0 && nextCtrlRefLine == 0)
               {
                  ctrlRefLine = c.Height/2 + vertPad;
                  nextCtrlRefLine = c.Height + 3 * vertPad/2;
               }
               // If control is too high we need to move reference line and
               // reposition the controls in the current line
               if ((ctrlRefLine + c.Height/2 + vertPad/2) > nextCtrlRefLine)
               {
                  ctrlRefLine = ctrlRefLine*2 - nextCtrlRefLine 
                     + vertPad/2 + c.Height/2;
                  nextCtrlRefLine = ctrlRefLine + vertPad/2 + c.Height/2;
                  // Re-arrange other controls
                  foreach (object o in thisLine)
                  {
                     Control ctrl = o as Control;
                     ctrl.Top = ctrlRefLine - ctrl.Height/2;
                  }
               }
               c.Location = new Point(ctrlLeft, ctrlRefLine-c.Height/2);
               thisLine.Add(c);
               break;
            }
            case VerticalAlignment.Bottom:
            {
               // If this is first control added, calculate reference lines
               if (ctrlRefLine == 0)
               {
                  ctrlRefLine = c.Height + vertPad;
                  prevCtrlRefLine = vertPad/2;
               }
               // See if we need to re-arrange the controls on this line
               if ((ctrlRefLine-c.Height-vertPad/2) < prevCtrlRefLine)
               {
                  ctrlRefLine = prevCtrlRefLine + c.Height + vertPad/2;
                  // Re-arrange other controls
                  foreach (object o in thisLine)
                  {
                     Control ctrl = o as Control;
                     ctrl.Top = ctrlRefLine - ctrl.Height;
                  }
               }
               c.Location = new Point(ctrlLeft, ctrlRefLine-c.Height);
               thisLine.Add(c);
               break;
            }
         }
         // Update the left position of the next control
         ctrlLeft = ctrlLeft + c.Size.Width + horizPad;
      }
      // Show all controls
      foreach (Control c in Controls) 
      {
         c.Visible = true;
      }
   }
}
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.