Properly Handling Control.MinimumSize Property in Windows Forms Applictaions

This article explains, and demonstrates through a relatively complex example, proper way of calculating MinimumSize property value at runtime. Result is that all controls can be rendered without loss caused by insufficient size of their parent controls.


Introduction

MinimumSize property of the Control class plays significant role in resizable forms. In many cases its value can be determined in advance and set at design time. However, there are many opposite cases, where MinimumSize must be calculated at runtime.

This article explains, and demonstrates through a relatively complex example, proper way of calculating MinimumSize property value at runtime. Result is that all controls can be rendered without loss caused by insufficient size of their parent controls.

Importance of Precise MinimumSize Calculation

Every control placed on a form or inside any other control takes space on user interface. Some controls require fixed space (most of the buttons), some of them require a strip (TextBox, Label, etc.), and some require a larger rectangle for their purpose (Panel, TabControl, multiline TextBox, etc.).

Typical user interface heavily relies on Dock and Anchor properties to maintain optimal layout of controls so that usefulness of each control is maximized. However, docking and anchoring has a side effect - when container (form, panel, etc.) is reduced too much, some of the contained controls cannot be rendered without loss any more. Captions, images, and other GUI elements would not fit if controls showing them continue shrinking beyond certain point.

This is where MinimumSize property comes into frame. Every control inherits Control.MinimumSize property which should be set to such size below which control may not be reduced. This size will be taken into account when layout engine operates on parent control. When hard times come and control's bounds start shrinking, MinimumSize property shows to be of higher order than Anchor and Dock. If obeying anchoring and docking properties means that bounds of the control will go below MinimumSize, then anchoring and docking will be ignored in favour of minimum size. Only when control prospers again, in terms of its spatial bounds, Anchor and Dock will come back into play as before.

This scenario explains why MinimumSize is important. However, one may say that this property can be set arbitrarily, like 200x20 for a TextBox. That is generally true, but it is not optimal and sometimes such decision can have undesired consequences. Especially when container (e.g. form) is shrank by the user so much that every pixel counts. Some smarter way of dealing with size is then better.

Always keep on mind that when user decides to reduce size of the window as much as possible, application should give a helping hand and to reduce all controls to their absolute minimum at which controls can still operate. This is why MinimumSize properties of controls should be calculated rather than safely set to arbitrarily large values.

Simple Example

Suppose that TextBox is meant to receive person's first name. Minimum size can be experimentally calculated by performing these steps:

  1. Take a database of first names for expected culture. For example, table below lists most frequent traditional French names with their corresponding frequencies.
  2. Measure all names using some typical font, preferably the one that will be used on control. In rare case when font is not known in advance, pick any non-monospaced font, like Times New Roman.
  3. Calculate average width of all names when rendered. Don't forget to take each name's frequency into account - more frequent names affect average more than others.
  4. Finally choose name which renders in bounds closest to average.

Here is the function which picks typical name:

string PickAverageName(string[] names, int[] frequencies)
{

    Font font = new Font("Times New Roman", 12.0F);
    int sum = 0;
    int count = 0;
    List<string> typicalNames = new List<string>();

    for (int i = 0; i < names.Length; i++)
    {

        System.Drawing.Size size = System.Windows.Forms.TextRenderer.MeasureText(names[i], font);
        sum += size.Width * frequencies[i];
        count += frequencies[i];

        while (typicalNames.Count <= size.Width)
            typicalNames.Add(null);

        if (typicalNames[size.Width] == null)
            typicalNames[size.Width] = names[i];

    }

    int average = (sum + count - 1) / count;  // Take ceiling of average width

    // Now pick the first sample name at average or higher width
    while (typicalNames[average] == null)
        average++;

    return typicalNames[average];

}

For French names listed in the table, this method returns name Marguerite, which requires 75 pixels wide rectangle using Times New Roman font of size 12. This information can be used to determine minimum size of a TextBox at runtime, using very simple method:

private void SetTextBoxMinimumSize(TextBox tb, string text)
{
    System.Drawing.Size size = System.Windows.Forms.TextRenderer.MeasureText(text, tb.Font);
    tb.MinimumSize = new Size(size.Width, tb.Height);
}
...
SetTextBoxMinimumSize(textBox1, "Marguerite");

When applied to Microsoft Sans Serif font, size 8.25, this method sets TextBox's MinimumSize to 57x20 pixels. If table with French names is relevant to population using our application, then this method guarantees that approximately 50% of all names entered to TextBox will perfectly fit its bounds without clipping.

Table: Frequencies of most common traditional French names, according to University of Montreal (http://www.genealogie.umontreal.ca/en/nomsPrenoms.htm).

Control.MinimumSize Property in Windows Forms

Role of the Control.Layout Event

Layout event is fired every time when control should reposition and resize contained controls. For example, if panel contains several child controls, some of which being anchored to panel's borders, exact locations and dimensions of each control will be determined in the Layout event handler.

This can be demonstrated with a very simple test. Derive a class from Panel and override its OnLayout event, so that custom implementation does not invoke base class's implementation of OnLayout. Place CustomPanel onto a form, add other controls to CustomPanel and anchor them to different borders. Now feel free to resize the CustomPanel control and observe that Anchor properties of child controls have no effect - layout logic is not executed.

Layout event interferes with MinimumSize property value in a very simple way. When control's MinimumSize property is set, it may cause control to be enlarged (never reduced, though). This happens in cases when minimum size is larger than current size in at least one dimension. In such case, Layout event will be raised and contained controls will be repositioned. Now suppose that MinimumSize property of contained controls is also being calculated on the fly. These influence contained controls sizes and positions. As can be suspected, setting both parent and child control's MinimumSize property must therefore be performed with care.

Here is an example which demonstrates where the danger comes from. Create new Windows Forms project, open main form's source code and add these functions to it:

protected override void OnLoad(EventArgs e)
{

    base.OnLoad(e);

    Panel pnl = new Panel();
    pnl.BackColor = Color.LightYellow;
    pnl.BorderStyle = BorderStyle.FixedSingle;
    pnl.Size = new Size((ClientRectangle.Width - 10) / 2, (ClientRectangle.Height - 10) / 2);
    pnl.Left = 5;
    pnl.Top = 5;

    pnl.SizeChanged += new EventHandler(pnl_SizeChanged);

    TextBox tb = new TextBox();
    tb.Width = 2 * (pnl.ClientRectangle.Width - 10) / 3;
    tb.Left = 5;
    tb.Top = 5;
    tb.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
    Size tbSize = tb.Size;
    tb.SizeChanged += new EventHandler(tb_SizeChanged);

    pnl.Controls.Add(tb);

    this.Controls.Add(pnl);

    Console.WriteLine("Original panel size {0}", pnl.Size);
    Console.WriteLine("Original text box size {0}", tb.Size);

    // Now specify minimum sizes for panel and text box
    tb.MinimumSize = new Size(tbSize.Width * 2, tbSize.Height);
    pnl.MinimumSize = new Size(pnl.Width * 2, pnl.Height * 2);

}

void tb_SizeChanged(object sender, EventArgs e)
{
    Console.WriteLine("TextBox size changed to {0}", ((TextBox)sender).Size);
}

void pnl_SizeChanged(object sender, EventArgs e)
{
    Console.WriteLine("Panel size changed to {0}", ((Panel)sender).Size);
}

This code creates new Panel control which occupies top-left quarter of the parent form (excluding 5 pixels margin from each of the form's borders). Inside the Panel new TextBox control is added, so that it occupies 2/3 of the Panel's client width (again, excluding 5 pixels margin from the borders).

If we run the code, output will look something like this:

Original panel size {Width=137, Height=127}
Original text box size {Width=83, Height=20}
TextBox size changed to {Width=166, Height=20}
TextBox size changed to {Width=303, Height=20}
Panel size changed to {Width=274, Height=254}

Now observe the output closely. We have changed TextBox's minimum width to 166 pixels, but at that moment Panel's width is only 137 pixels. Further on, width of the panel is increased to 274 pixels, but this takes effect only after Panel's Layout event is handled. Inside Layout handler, however, TextBox grows in width by additional 137 pixels because that is exact increase in Panel's width - that is how TextBox's Anchor property is handled. As a result, TextBox grows to 303 pixels in width, which is again larger than containing Panel control.

The problem in this example is order in which MinimumSize has been set on TextBox and on Panel. The order must be inversed - first set MinimumSize of the Panel. This may raise Layout event (if Panel's size is increased). Layout event handler will resize TextBox accordingly. Only then we can set TextBox's MinimumSize value, knowing that TextBox will be enlarged only if its MinimumSize is still larger than current size.

We can exchange two lines of code that are setting MinimumSize values on TextBox and Panel:

pnl.MinimumSize = new Size(pnl.Width * 2, pnl.Height * 2);
tb.MinimumSize = new Size(tbSize.Width * 2, tbSize.Height);


If we run the code now, output will be quite different:

Original panel size {Width=137, Height=127}
Original text box size {Width=83, Height=20}
TextBox size changed to {Width=220, Height=20}
Panel size changed to {Width=274, Height=254}

TextBox's size is now changed in the Layout event, which is raised after Panel's MinimumSize is set to value larger than current size. After that, MinimumSize of TextBox is set, but that has no effect because TextBox's size is already larger than value of MinimumSize.

This little experiment leads to a conclusion. Set MinimumSize property values top-down, i.e. first to parent controls, and only then to child controls. All this to let parent control's Layout event opportunity to reposition child controls properly before their own MinimumSize properties are changed.

General Rules in Calculating MinimumSize

We have determined in previous text that order of setting MinimumSize properties should be top-down, i.e. containers first, contained controls after that. However, minimum size of container control often depends on minimum sizes of contained controls, which further depends on their own contained controls. Hence, we have an opposite conclusion here: MinimumSize value is calculated bottom-up, i.e. from the inner-most child controls to the outer-most containers.

From this analysis we come to an algorithm of setting MinimumSize of all controls in a safe and reliable way:

  1. For every control calculate minimum sizes of all relevant contained controls.

  2. Calculate own minimum size based on minimum sizes and layout of contained controls.

  3. Set own minimum size.

  4. Order contained controls to set their own minimum sizes to previously calculated values.

Observe that we are applying the algorithm only to relevant controls. Some controls have fixed size (e.g. buttons). Some of them are auto-sized (e.g. labels). These controls are not relevant and their MinimumSize property is not used. Only their actual size may be taken into account when calculating minimum size of their parent controls.

Proposed Solution

For every control to which we want to set MinimumSize, we can design one function with signature like this:

private Size SetXMinimumSize(Dictionary<Control, Size> minimumSizes, bool calculateOnly)

In this function name X is replaced by an appropriate identifier used to distinguish particular control.

This function operates in two modes.

First mode is when calculateOnly is true. In that case appropriate methods for child controls are invoked, again with calculateOnly set to true. Method calculates minimum size of the target control and adds record to minimumSizes dictionary, mapping target control to its minimum size. Minimum sizes for all child controls are already there, placed by calls to their corresponding methods.

Second operational mode is to invoke the method with calculateOnly set to false, while passing minimumSizes dictionary populated in the first run. In this mode, MinimumSize of the control is set to value from the dictionary and appropriate methods for all contained controls are invoked, setting calculateOnly to false in all calls. That will force all contained controls' MinimumSize to be set as well.

In both operational modes method returns size of the target control. This simplifies implementation because method does not have to consult the dictionary to obtain desired minimum sizes of contained controls. This will be demonstrated in example that follows.

Upside of this method is that calculation is done exactly once for each of the controls, and every MinimumSize property is set exactly once. Order of invocations guarantees that all minimum sizes will be calculated before setting the values.

Downside of this method is that setting minimum sizes may trigger event which has caused recalculation of minimum sizes in the first place. This method must never enter twice into execution, or otherwise loops can be formed. One way to prevent double entering is to place a Boolean flags at the top level, which are used to control recalculation of minimum sizes in this way:

private void RefreshMinimumSizes()
{

    _refreshMinimumSizes = true;

    if (!_refreshingMinimumSizes)
    {

        _refreshingMinimumSizes = true;
        while (_refreshMinimumSizes)
        {
            _refreshMinimumSizes = false;

            // At this place invoke methods for all
            // contained controls. Some of the methods
            // may cause this method to be re-entered,
            // which would only set _refreshMinimumSizes
            // to true and exit happily.
 
        }

        _refreshingMinimumSizes = false;

    }

}

This method is used at top level to force recalculation of all minimum sizes. If re-entered, due to some event which normally causes recalculation, method will just set the flag that minimum sizes should be calculated again and continue until current run is finished. Then, if flag is set, method will re-run, calculating new values again. Typically, second run will have no effect because recalculation is triggered in a more relaxed way than really neccessary.

When all functions that calculate minimum sizes are in place, we should add triggers which act upon conditions that require minimum sizes to be recalculated. This task cannot be automated. It is up to the designer to decide which events and other conditions require attention. For any such event, just invoke RefreshMinimumSizes from its appropriate handler and everything will be right.

Example

As a matter of demonstration, we will create a form containing main menu at the top and a status bar at the bottom. Between these two strips, split container will be docked to fill the rest of the window and to divide it with a vertical splitter.

Left panel of the split container will be filled with TabControl, which contains two TabPages. First TabPage contains a TableLayoutPanel with two rows and two columns. TableLayoutPanel is auto-sized, with size mode set to GrowAndShrink. This means that table will always have minimum size required by its content.

TableLayoutPanel contains two ComboBoxes, used to select font family and size. These controls occupy cells in the first row. ComboBoxes will have fixed size.

Cells in the second row contain one auto-size label (with column span set to 2), which shows sample message in selected font and size.

This is how the desired form should look like:

Control.MinimumSize Property in Windows Forms

What we really want to accomplish is to calculate MinimumSize properties of the form and all its contained controls so that, when form is reduced to its minimum size, it looks like this:

Control.MinimumSize Property in Windows Forms

When done correctly, there will be no spare pixel on the form.

Implementation

Code used to create and populate controls on the form will be skipped here. Anyone interested may find it in attached file, which contains complete source code of the project.

In this section, we will concentrate on code used to calculate and set MinimumSize properties of TabControl (field _tabControl), SplitContainer (field _splitContainer) and the form itself. Following methods are used to recalculate minimum sizes of these three controls. (Note that Form also derives from Control, which is used to add it to dictionary together with other controls.)

private Size SetTabControlMinimumSize(Dictionary<Control, Size> minimumSizes, bool calculateOnly)
{

    Size minSize = new Size();

    if (calculateOnly)
    {

        // When reduced to minimum, tab page must have client rectangle size equal to
        // size of contained table control.
        // Minimum size of tab control is then calculated by adding parts of tab control
        // that are located outside the client area of tab page to size of the table panel.
        TabPage page = _tabControl.TabPages[0];
        int width = _tablePanel.Width + _tabControl.Width - page.ClientRectangle.Width;
        int height = _tablePanel.Height + _tabControl.Height - page.ClientRectangle.Height;

        minSize = new Size(width, height);
        minimumSizes.Add(_tabControl, minSize);

    }
    else
    {

        minSize = minimumSizes[_tabControl];
        _tabControl.MinimumSize = minSize;

    }

    return minSize;

}

private Size SetSplitContainerMinimumSize(Dictionary<Control, Size> minimumSizes, bool calculateOnly)
{

    Size minSize = new Size();

    if (calculateOnly)
    {

        Size tabControlMinSize = SetTabControlMinimumSize(minimumSizes, true);

        int panelMinWidth = tabControlMinSize.Width;
        // Add left panel to dictionary; this entry will be used to set panel's minimum width
        minimumSizes.Add(_splitContainer.Panel1, new Size(panelMinWidth, 1));

        // Horizontally, split container consists of a border, left panel,
        // border, splitter, border, right panel and again border.
        // Sizes of borders and splitter can be calculated as part of the
        // control's width which remains when widths of client rectangles of left
        // and right panel are subtracted from total control width.
        // Minimum width of the split container control is then sum of
        // minimum sizes of two panels increased by total size of all
        // outer elements.
        int width = panelMinWidth + _splitContainer.Panel2MinSize +
                    _splitContainer.Width - _splitContainer.Panel1.ClientRectangle.Width -
                    _splitContainer.Panel2.ClientRectangle.Width;

        // Vertically, split container consists of border, panel (left or right)
        // and again border. Borders are calculated by subtracting
        // panel client rectangle height from total control height.
        int height = tabControlMinSize.Height + _splitContainer.Height -
                        _splitContainer.Panel1.ClientRectangle.Height;

        minSize = new Size(width, height);
        minimumSizes.Add(_splitContainer, minSize);

    }
    else
    {

        minSize = minimumSizes[_splitContainer];
        Size panel1MinSize = minimumSizes[_splitContainer.Panel1];

        _splitContainer.MinimumSize = minSize;
        _splitContainer.Panel1MinSize = panel1MinSize.Width;

        SetTabControlMinimumSize(minimumSizes, false);

    }

    return minSize;

}

private void SetFormMinimumSize(Dictionary<Control, Size> minimumSizes, bool calculateOnly)

    Size minSize = new Size();

    if (calculateOnly)
    {
 
        Size splitContainerMinSize = SetSplitContainerMinimumSize(minimumSizes, true);

        int width = splitContainerMinSize.Width;
        int height = splitContainerMinSize.Height + _statusBar.Height;

        Size clientSize = new Size(width, height);

        minSize = SizeFromClientSize(clientSize);
        minimumSizes.Add(this, minSize);

    }
    else
    {

        minSize = minimumSizes[this];
        this.MinimumSize = minSize;

        SetSplitContainerMinimumSize(minimumSizes, false);

    }
}

These methods clearly demonstrate why it wasn't attempted to create any general solution for the problem. Every kind of control has its own logic and all three methods substantially differ from each other.

Method which wraps-up the process is this:

private void RefreshMinimumSizes()
{

    _refreshMinimumSizes = true;

    if (!_refreshingMinimumSizes)
    {

        _refreshingMinimumSizes = true;
        while (_refreshMinimumSizes)
        {
            _refreshMinimumSizes = false;

            Dictionary<Control, Size> minimumSizes = new Dictionary<Control, Size>();
            SetFormMinimumSize(minimumSizes, true);
            SetFormMinimumSize(minimumSizes, false);

        }

        _refreshingMinimumSizes = false;

    }

}

This method initiates setting form's MinimumSize, which recursively does the same to all other controls.

At the very end, just note that triggers for minimum size recalculation are SizeChanged event on TableLayoutPanel and ClientSizeChanged event on TabPage which contains TableLayoutPanel. First event is important because changes in size of the TableLayoutPanel may require all other controls, up to the form, to be enlarged to fit the new size of the table. Second event is important because TabPage's client rectangle may change due to circumstances not related to size of the control. Namely, changed border style may cause TabPage's client rectangle to shrink so that TableLayoutPanel cannot fit inside it any more, and hence MinimumSize of the TabControl must be increased.
Once again, complete source code of the demo project is located in the attached file. Feel free to analyze it in more detail.

Conclusion

MinimumSize property derived from Control class is one of fundamental properties in many Windows Forms applications, despite the lack of interest it often suffers in general public. Precise calculation of this property helps a lot in creating user-friendly interface and improves any application.

On the contrary, setting this property to misfit value or ignoring it, may cause user to feel uncomfortably or even enraged; not happy anyway.

General advice is to always pay attention to MinimumSize property of all controls other than auto-sized and fixed-sized ones. In simpler cases rule of thumb is applicable and MinimumSize can be set at design time. Under more complex circumstances, where spatial distribution of controls depends of unpredictable conditions, like user provided values shown in our example, MinimumSize properties must be determined programmatically. In those cases, do not try to save time but perform the task correctly and it will pay back later.

This article has provided guidelines how to calculate MinimumSize of all controls in a relatively painless way, at least in a unified way, which is always helpful. Guidelines given in this article, if followed, will ensure reliable and fast calculation of MinimumSize properties in any complex user interface.