Introduction
Sometimes, we encounter one of the following situations:
- A set of options have little importance to the degree that users can accept their default value under most situations. Ideally these unimportant options can be collapsed (just as the Find and Replace Form of Visual Studio 2010) so as to not disturb the users. For this, Collapsible
GroupBox
is a good choice. - A set of options cannot be selected when once one or more options are selected.
RadioGroupBox
will work in this case. - A set of options are only useful under a particular condition. If they are represented by a
CheckBox
, it is a waste of screen space and does not depict the relationship between the condition and those options. However, CheckGroupBox
can do a great job.
Months ago, I encountered the third situation. Weeks ago, I encountered the first and second situation. Though these kinds of controls already existed on the site, they all have some disadvantages (which will be explained later in this article). So I decided to write my own version and share them. When I wrote them, I took many cases into consideration, such as Font, transparent color, dynamic creation, to make them easy to use. I did not combine Controls, but rather repainted to consume less memory.
CheckGroupBox
This is the simplest one to implement. The only thing that needs to be done is to paint a CheckBox
at the title of GroupBox
, then add two properties (Checked
and CheckState
) and two events (CheckedChanged
and CheckStateChanged
) and make sure they work. But there are still some things that are worth paying attention to.
- What happens when the
CheckGroupBox
's backcolor is set to "transparent?" That is to say, when you paint the GroupBox
enough vacancy must be set aside to paint a CheckBox
. - How do you make the
Font
of GroupBox
also work for CheckBox
? At first glance, everybody will think this is too easy for it to be an issue. When you paint the CheckBox
, you are using the GroupBox
's Font
property as a parameter. I also thought of trying it this way but when the text is too long to be on a single line, problems arise.
To address these two problems, I thought up a way to add some space char
(' ') at the beginning of the Text
of GroupBox
, and make sure the length of those space char
in pixels was enough to paint the pane of the CheckBox
. We can't calculate how many pixels in width we will need to paint a ' ' and calculate how many ' 's we will need. Instead, we should calculate the average width of a letter and calculate how many letters we will need.
string t = " 01%^GJWIabdfgjkwi,:\"'`~-_}]?.>\\";
int letterWidth = (int)e.Graphics.MeasureString(t, this.Font).Width / 32;
int w = (_toggleRect.Height + letterWidth) / letterWidth;
string text = new string(' ', w);
if (!string.IsNullOrEmpty(base.Text))
{
text += base.Text;
_appendToggleLength = base.Text.Length * letterWidth;
}
GroupBoxRenderer.DrawGroupBox(e.Graphics, new Rectangle(0, 0,
base.Width, base.Height), text, this.Font, flags, state);
int chkOffset = (_toggleRect.Height - 9)/2;
CheckBoxRenderer.DrawCheckBox(e.Graphics, new Point(_toggleRect.X, _toggleRect.Y +
chkOffset), _checkBoxState);
For better user experience, we also need to increase mouse sensitivity.
protected override void OnMouseUp(MouseEventArgs e)
{
Rectangle rect = new Rectangle(_toggleRect.X, _toggleRect.Y,
_toggleRect.Width + _appendToggleLength, _toggleRect.Height);
if (rect.Contains(e.Location))
this.Checked = !_checked;
else
base.OnMouseUp(e);
}
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
_checkBoxState = _checked ?
CheckBoxState.CheckedNormal :
CheckBoxState.UncheckedNormal;
this.Invalidate(_toggleRect);
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
Rectangle rect = new Rectangle(_toggleRect.X, _toggleRect.Y,
_toggleRect.Width + _appendToggleLength, _toggleRect.Height);
if (rect.Contains(e.Location))
{
_checkBoxState = _checked ?
CheckBoxState.CheckedPressed :
CheckBoxState.UncheckedPressed;
this.Invalidate(_toggleRect);
}
}
RadioGroupBox
When implementing RadioGroupBox
and CollapsiableGroupBox
, the two issues for CheckGroupBox
arise again. Besides, RadioGroupBox
should behave just like a RadioButton
. jeffb42's implementation uses a RadioPanel
to accomplish this task. When using the RadioGroupBox
, we should let the RadioItems
contained by a RadioPanel
and a method of the RadioPanel
must be invoked. It is complex. When creating a RadioGroupBox
programmatically at run time, it will fail to complete the task.
To facilitate usage, we can provide a method to accomplish the Radio
task. The method can only be implemented as follows (to my knowledge). However, here still is a problem which, when a RadioButton
checked, it can't uncheck the RadioGroupBox
.
private void PerformAutoUpdates()
{
if (_autoCheck)
{
Control parentInternal = this.Parent;
if (parentInternal != null)
{
Control.ControlCollection controls = parentInternal.Controls;
for (int i = 0; i < controls.Count; i++)
{
Control control2 = controls[i];
if ((control2 != this))
{
if (control2 is RadioButton)
{
RadioButton component = (RadioButton)control2;
if (component.AutoCheck && component.Checked)
{
component.Checked = false;
}
}
else if (control2 is RadioGroupBox)
{
RadioGroupBox component = (RadioGroupBox)control2;
if (component.AutoCheck && component.Checked)
{
TypeDescriptor.GetProperties(this)["Checked"].SetValue
(component, false);
}
}
}
}
}
}
}
We all know that when a Control is about to add to another Control its CreateControl
event will arise. So the OnCreateControl
method is the right place to accomplish the Radio
task. As soon as it is added to its parent Control, we make it subscribe the CheckedChanged
event of all the RadioButton
s contained by the parent.
protected override void OnCreateControl()
{
base.OnCreateControl();
Control parent = this.Parent;
if (parent != null)
{
parent.ControlAdded += new ControlEventHandler(parent_ControlAdded);
parent.ControlRemoved += new ControlEventHandler(parent_ControlRemoved);
Control.ControlCollection controls = parent.Controls;
for (int i = 0; i < controls.Count; i++)
{
Control control2 = controls[i];
if ((control2 != this))
{
if (control2 is RadioButton)
{
RadioButton radioButton = (RadioButton)control2;
radioButton.CheckedChanged +=
new EventHandler(radioButton_CheckedChanged);
}
}
}
}
}
void radioButton_CheckedChanged(object sender, EventArgs e)
{
if (_autoCheck)
{
RadioButton radioButton = sender as RadioButton;
if (radioButton.Checked)
{
this.Checked = false;
}
}
}
Now, add the supplement for dynamic creation (create an instance at runtime programmatically). If a newly created RadioGroupBox
is added to the parent Control, it will work well. If it is a RadioButton
, all the CheckedGroupBox
contained by the parent control can't get the message to indicate the newly added RadioButton
is checked. So there is a statement like "parent.ControlAdded += new ControlEventHandler(parent_ControlAdded);
" in the OnCreateControl
method and this is to subscribe the newly added RadioButton
's CheckedChanged
event.
void parent_ControlAdded(object sender, ControlEventArgs e)
{
if (e.Control is RadioButton)
{
RadioButton radioButton = e.Control as RadioButton;
radioButton.CheckedChanged += radioButton_CheckedChanged;
}
}
Dynamic creation can't exist without dynamic removing. Say you remove a RadioGroupBox
at run time programmatically. "parent.ControlRemoved += new ControlEventHandler(parent_ControlRemoved);
" is used to accomplish dynamic removing. When a RadioGroupBox
is notified that a RadioButton
is removed from the parent Control, it unsubscribes the RadioButton
's CheckedChanged
event. If a RadioGroupBox
itself is removed from the parent Control, it unsubscribes all the events that it has subscribed.
CollapsibleGroupBox
CollasibleGroupBox
's auto collapse its parent Control mission is accomplished by letting the parent Control subscribe its CollapsedChanged
event in the OnCreateControl
method. The CollasibleGroupBox
's dynamic creation and removing is accomplished by the same way as RadioGroupBox
.
protected override void OnCreateControl()
{
base.OnCreateControl();
if ((base.Anchor & (AnchorStyles.Bottom | AnchorStyles.Top)) == (
AnchorStyles.Bottom | AnchorStyles.Top))
{
_removeAnchor = true;
}
_control = this.Parent as Control;
if (_control != null)
{
this.CollapsedChanged += new EventHandler(CollapsibleGroupBox_CollapsedChanged);
_control.ControlRemoved += new ControlEventHandler(ctrl_ControlRemoved);
}
}
void CollapsibleGroupBox_CollapsedChanged(object sender, EventArgs e)
{
if (_collapseParent)
{
if (_collapsed)
{
_control.Height -= _collapsedHeight;
}
else
{
_control.Height += _collapsedHeight;
}
}
}
There is one more problem, if the CollapsibleGroupBox
's Anchor
property contains the two value, AnchorStyles.Bottom
and AnchorStyles.Top
at the same time its location will change after its parent Control is collapsed. So when you collapse AnchorStyles.Bottom
, the Anchor
property should also be temporarily removed.
public bool Collapsed
{
get { return _collapsed; }
set
{
if (_collapsed != value)
{
_resizingFromCollapse = true;
_collapsed = value;
if (_removeAnchor)
{
base.Anchor ^= AnchorStyles.Bottom;
}
if (_collapsed)
{
this.Height = _minHeight;
this.Invalidate();
}
else
{
this.Height = _fullHeight;
this.Invalidate(_toggleRect);
}
if (CollapsedChanged != null)
CollapsedChanged(this, new EventArgs());
_resizingFromCollapse = false;
if (_removeAnchor)
{
base.Anchor |= AnchorStyles.Bottom;
}
}
}
}
Usage
Those controls are very easy to use; just drag and drop. The most frequently used properties are narrated within the demo.
Reversion
Version 2010-12-16 (only CheckGroupBox
and RadioGroupBox
)
Changes
1 in the OnPaint method
Old:
string t = " 01%^GJWIabdfgjkwi,:\"'`~-_}]?.>\\";
int letterWidth = (int)e.Graphics.MeasureString(t, this.Font).Width / 32;
int w = (_toggleRect.Height + letterWidth) / letterWidth;
string text = new string(' ', w);
if (!string.IsNullOrEmpty(base.Text))
{
text += base.Text;
_appendToggleLength = base.Text.Length * letterWidth;
}
GroupBoxRenderer.DrawGroupBox(e.Graphics,
new Rectangle(0, 0, base.Width, base.Height), text, this.Font, flags, state);
int chkOffset = (_toggleRect.Height - 9)/2;
CheckBoxRenderer.DrawCheckBox(e.Graphics,
new Point(_toggleRect.X, _toggleRect.Y + chkOffset), _checkBoxState);
New:
if (_calc)
{
string t = " 01%^GJWIabdfgjkwi,:\"'`~-_}]?.>\\";
int letterWidth = (int)e.Graphics.MeasureString(t, this.Font).Width / 32;
int w = (_checkPaneWidth + letterWidth) / letterWidth;
string text = new string(' ', w);
if (!string.IsNullOrEmpty(base.Text))
{
_vText = text + base.Text;
_checkBoxRect.Width = _checkPaneWidth + base.Text.Length * letterWidth;
}
_calc = false;
}
GroupBoxRenderer.DrawGroupBox(e.Graphics,
new Rectangle(0, 0, base.Width, base.Height), _vText, this.Font, flags, state);
int chkOffset = (_checkBoxRect.Height - _checkPaneWidth) / 2;
chkOffset = chkOffset < 0 ? 0 : chkOffset;
CheckBoxRenderer.DrawCheckBox(e.Graphics,
new Point(_checkBoxRect.X, chkOffset), _checkBoxState);
_calc
is a private
member variable of bool
type with initial value of true
that we use it to indicate whether _vText
should be recalculated. If the GroupBox
's Text
and Font
stay unchanged, the _vText
need not to be recalculated. In this version, these Controls' performance is improved a little.
Actually, these changes are aimed to fix the problem that their appearance is messed up under some situations. In the old version, the vacancy that set aside for CheckBox
or RadioButton
may be less than the width of the pane of CheckBox
and RadioButton
. See the statement int w = (_toggleRect.Height + letterWidth) / letterWidth;
. The initial value of _toggleRect.Height
(12) is less than the width of the pane of CheckBox
and RadioButton
(13). If the Font
stays unchanged, the value of _toggleRect.Height
will stay unchanged. However, if the new Font
size is less than the default Font
size, the problem still exists. So in the new version, I use a constant(_checkPaneWidth
) to hold the width of the pane of CheckBox
and RadioButton
.
2 update _calc
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
_calc = true;
}
protected override void OnFontChanged(EventArgs e)
{
base.OnFontChanged(e);
_checkBoxRect.Height = (base.Font.Height - 5) | 1;
_calc = true;
}