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:
- Take a database of first names for
expected culture. For example, table below lists most frequent traditional
French names with their corresponding frequencies.
- 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.
- 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.
- 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).
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:
-
For every control calculate minimum sizes of
all relevant contained controls.
-
Calculate own minimum size based on minimum
sizes and layout of contained controls.
-
Set own minimum size.
-
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:
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:
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.