65.9K
CodeProject 正在变化。 阅读更多。
Home

一组 GroupBox

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (10投票s)

2010 年 12 月 13 日

CPOL

5分钟阅读

viewsIcon

57796

downloadIcon

1125

CheckGroupBox, RadioGroupBox, CollapsibleGroupBox 控件

new1.jpg

new2.jpg

引言

有时,我们会遇到以下几种情况之一:

  1. 一组选项的重要性较低,用户在大多数情况下都可以接受其默认值。理想情况下,这些不重要的选项可以折叠(就像 Visual Studio 2010 的查找和替换窗体一样),以免打扰用户。为此,Collapsible GroupBox 是一个不错的选择。
  2. 当选择了一个或多个选项时,一组选项将不可选。在这种情况下,RadioGroupBox 会起作用。
  3. 一组选项仅在特定条件下才有用。如果它们由 CheckBox 表示,则会浪费屏幕空间,也无法体现条件与这些选项之间的关系。但是,CheckGroupBox 可以做得很好。

几个月前,我遇到了第三种情况。几周前,我遇到了第一种和第二种情况。尽管该网站上已经存在这些类型的控件,但它们都存在一些缺点(将在本文后面详细介绍)。因此,我决定编写自己的版本并分享它们。在编写它们时,我考虑了许多情况,例如字体、透明颜色、动态创建,以使其易于使用。我没有组合控件,而是进行了重绘以消耗更少的内存。

CheckGroupBox

这是最容易实现的。唯一需要做的事情是在 GroupBox 的标题处绘制一个 CheckBox,然后添加两个属性(CheckedCheckState)和两个事件(CheckedChangedCheckStateChanged)并确保它们正常工作。但仍有一些值得注意的事情。

  1. CheckGroupBox 的背景颜色设置为“透明”时会发生什么?也就是说,当您绘制 GroupBox 时,必须留出足够的空间来绘制 CheckBox
  2. 如何让 GroupBoxFont 也适用于 CheckBox?乍一看,每个人都会认为这太简单了,不值一提。当您绘制 CheckBox 时,您将 GroupBoxFont 属性用作参数。我也曾想过尝试这种方法,但当文本太长无法显示在单行上时,就会出现问题。

为了解决这两个问题,我想出了一个方法,即在 GroupBoxText 开头添加一些空格字符(' '),并确保这些空格字符的像素宽度足以绘制 CheckBox 的面板。我们无法计算绘制一个 ' ' 需要多少像素宽度,也无法计算需要多少个 ' '。相反,我们应该计算每个字母的平均宽度,并计算需要多少个字母。

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);

为了获得更好的用户体验,我们还需要提高鼠标的灵敏度。

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

在实现 RadioGroupBoxCollapsiableGroupBox 时,CheckGroupBox 的两个问题再次出现。此外,RadioGroupBox 的行为应该就像一个 RadioButtonjeffb42 的实现 使用 RadioPanel 来完成此任务。在使用 RadioGroupBox 时,我们应该让 RadioItems 包含在 RadioPanel 中,并且必须调用 RadioPanel 的一个方法。这很复杂。在运行时以编程方式创建 RadioGroupBox 时,它将无法完成任务。

为了方便使用,我们可以提供一个方法来完成 Radio 任务。据我所知,该方法只能这样实现。但是,这里仍然存在一个问题,即当一个 RadioButton 被选中时,它无法取消选中 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);
                        }
                    }
                }
            }
        }
    }
}

我们都知道,当一个控件即将被添加到另一个控件时,会触发其 CreateControl 事件。因此,OnCreateControl 方法是完成 Radio 任务的正确位置。一旦将其添加到其父控件,我们就会让它订阅父控件中所有 RadioButtonCheckedChanged 事件。

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

现在,为动态创建(在运行时以编程方式创建实例)添加补充。如果一个新创建的 RadioGroupBox 被添加到父控件,它将正常工作。如果它是一个 RadioButton,父控件中所有 CheckedGroupBox 都无法收到消息以指示新添加的 RadioButton 已被选中。因此,在 OnCreateControl 方法中有一条语句,如“parent.ControlAdded += new ControlEventHandler(parent_ControlAdded);”,用于订阅新添加的 RadioButtonCheckedChanged 事件。

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;
    //}
}

动态创建离不开动态移除。假设您在运行时以编程方式移除一个 RadioGroupBox。“parent.ControlRemoved += new ControlEventHandler(parent_ControlRemoved);”用于完成动态移除。当 RadioGroupBox 被通知父控件中移除了一个 RadioButton 时,它会取消订阅该 RadioButtonCheckedChanged 事件。如果 RadioGroupBox 本身从父控件中被移除,它将取消订阅所有它已订阅的事件。

CollapsibleGroupBox

CollapsiableGroupBox 的自动折叠父控件任务是通过在 OnCreateControl 方法中让父控件订阅其 CollapsedChanged 事件来完成的。CollapsiableGroupBox 的动态创建和移除与 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;
        }
    }
}

还有另一个问题,如果 CollapsibleGroupBoxAnchor 属性同时包含 AnchorStyles.BottomAnchorStyles.Top 这两个值,则在父控件折叠后,其位置会发生变化。因此,当您折叠 AnchorStyles.Bottom 时,还应暂时移除 Anchor 属性。

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

用法

这些控件非常易于使用,只需拖放即可。最常用的属性在演示中都有说明。

修订

版本 2010-12-16(仅 CheckGroupBoxRadioGroupBox

变更

1. 在 OnPaint 方法中

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);

新建

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 是一个 bool 类型的私有成员变量,初始值为 true,用于指示是否应重新计算 _vText。如果 GroupBoxTextFont 保持不变,则无需重新计算 _vText。在此版本中,这些控件的性能略有提高。

实际上,这些更改旨在解决在某些情况下外观混乱的问题。在旧版本中,为 CheckBoxRadioButton 预留的空间可能小于 CheckBoxRadioButton 面板的宽度。请参阅语句 int w = (_toggleRect.Height + letterWidth) / letterWidth;_toggleRect.Height(12) 的初始值小于 CheckBoxRadioButton 面板的宽度(13)。如果 Font 保持不变,则 _toggleRect.Height 的值也将保持不变。但是,如果新的 Font 大小小于默认 Font 大小,问题仍然存在。因此,在新版本中,我使用一个常量(_checkPaneWidth)来存储 CheckBoxRadioButton 面板的宽度。

2. 更新 _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;
}
© . All rights reserved.