一组 GroupBox
CheckGroupBox, RadioGroupBox, CollapsibleGroupBox 控件
引言
有时,我们会遇到以下几种情况之一:
- 一组选项的重要性较低,用户在大多数情况下都可以接受其默认值。理想情况下,这些不重要的选项可以折叠(就像 Visual Studio 2010 的查找和替换窗体一样),以免打扰用户。为此,Collapsible
GroupBox
是一个不错的选择。 - 当选择了一个或多个选项时,一组选项将不可选。在这种情况下,
RadioGroupBox
会起作用。 - 一组选项仅在特定条件下才有用。如果它们由
CheckBox
表示,则会浪费屏幕空间,也无法体现条件与这些选项之间的关系。但是,CheckGroupBox
可以做得很好。
几个月前,我遇到了第三种情况。几周前,我遇到了第一种和第二种情况。尽管该网站上已经存在这些类型的控件,但它们都存在一些缺点(将在本文后面详细介绍)。因此,我决定编写自己的版本并分享它们。在编写它们时,我考虑了许多情况,例如字体、透明颜色、动态创建,以使其易于使用。我没有组合控件,而是进行了重绘以消耗更少的内存。
CheckGroupBox
这是最容易实现的。唯一需要做的事情是在 GroupBox
的标题处绘制一个 CheckBox
,然后添加两个属性(Checked
和 CheckState
)和两个事件(CheckedChanged
和 CheckStateChanged
)并确保它们正常工作。但仍有一些值得注意的事情。
- 当
CheckGroupBox
的背景颜色设置为“透明”时会发生什么?也就是说,当您绘制GroupBox
时,必须留出足够的空间来绘制CheckBox
。 - 如何让
GroupBox
的Font
也适用于CheckBox
?乍一看,每个人都会认为这太简单了,不值一提。当您绘制CheckBox
时,您将GroupBox
的Font
属性用作参数。我也曾想过尝试这种方法,但当文本太长无法显示在单行上时,就会出现问题。
为了解决这两个问题,我想出了一个方法,即在 GroupBox
的 Text
开头添加一些空格字符(' '),并确保这些空格字符的像素宽度足以绘制 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
在实现 RadioGroupBox
和 CollapsiableGroupBox
时,CheckGroupBox
的两个问题再次出现。此外,RadioGroupBox
的行为应该就像一个 RadioButton
。 jeffb42 的实现 使用 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
任务的正确位置。一旦将其添加到其父控件,我们就会让它订阅父控件中所有 RadioButton
的 CheckedChanged
事件。
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);
”,用于订阅新添加的 RadioButton
的 CheckedChanged
事件。
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
时,它会取消订阅该 RadioButton
的 CheckedChanged
事件。如果 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;
}
}
}
还有另一个问题,如果 CollapsibleGroupBox
的 Anchor
属性同时包含 AnchorStyles.Bottom
和 AnchorStyles.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(仅 CheckGroupBox
和 RadioGroupBox
)
变更
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
。如果 GroupBox
的 Text
和 Font
保持不变,则无需重新计算 _vText
。在此版本中,这些控件的性能略有提高。
实际上,这些更改旨在解决在某些情况下外观混乱的问题。在旧版本中,为 CheckBox
或 RadioButton
预留的空间可能小于 CheckBox
和 RadioButton
面板的宽度。请参阅语句 int w = (_toggleRect.Height + letterWidth) / letterWidth;
。_toggleRect.Height
(12) 的初始值小于 CheckBox
和 RadioButton
面板的宽度(13)。如果 Font
保持不变,则 _toggleRect.Height
的值也将保持不变。但是,如果新的 Font
大小小于默认 Font
大小,问题仍然存在。因此,在新版本中,我使用一个常量(_checkPaneWidth
)来存储 CheckBox
和 RadioButton
面板的宽度。
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;
}