Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

A Group of GroupBox

4.77/5 (10 votes)
16 Dec 2010CPOL5 min read 56.8K   1.1K  
CheckGroupBox, RadioGroupBox, CollapsibleGroupBox controls

new1.jpg

new2.jpg

Introduction

Sometimes, we encounter one of the following situations:

  1. 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.
  2. A set of options cannot be selected when once one or more options are selected. RadioGroupBox will work in this case.
  3. 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.

  1. 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.
  2. 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.

C#
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.

C#
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.

C#
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 RadioButtons contained by the parent.

C#
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);
                }

                //this will be done by  PerformAutoUpdates()
                //else if (control2 is RadioGroupBox)
                //{
                //    RadioGroupBox radioGroupBox = (RadioGroupBox)control2;
                //    radioGroupBox.CheckedChanged += new EventHandler(
                //        radioGroupBox_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.

C#
void parent_ControlAdded(object sender, ControlEventArgs e)
{
    if (e.Control is RadioButton)
    {
        RadioButton radioButton = e.Control as RadioButton;
        radioButton.CheckedChanged += radioButton_CheckedChanged;
    }
    //else if (e.Control is RadioGroupBox)
    //{
    //    RadioGroupBox radioGroupBox = e.Control as RadioGroupBox;
    //    if(radioGroupBox != this)
    //        radioGroupBox.CheckedChanged += radioGroupBox_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.

C#
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.

C#
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;
                //foreach (Control ctl in base.Controls)
                //{
                //    ctl.Visible = false;
                //}
                this.Invalidate();
            }
            else
            {
                this.Height = _fullHeight;
                //foreach (Control ctl in base.Controls)
                //{
                //    ctl.Visible = true;
                //}
                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:

C#
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:

C#
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

C#
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;
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)