分隔符组合框






4.89/5 (24投票s)
带分隔符的自定义组合框和列表框控件
引言
本文是我之前关于 MFC 派生的 CCombobox 控件类的文章“带分隔符的组合框”的延迟跟进。自发布以来,有许多读者要求用 C# 实现类似的带分隔符的组合框。在接触 C# 编程时,我发现编写一个如上截图所示的自定义组合框控件更容易。在组合框中实现分隔符的关键是,分隔符不应该被从 UI 或代码逻辑中选中。我们有两种选择:让分隔符占据一个项目空间,或者让分隔符位于项目之间而不占用额外空间。我更喜欢后者;它只是在项目之间画一条线,从而节省了整个组合框的空间。
由于组合框和列表框都派生自 Windows.Forms.ListControl,它们都拥有我自定义中可用的相同的虚拟 OnDrawItem() 和 OnMeasureItem() 函数。在这里,向组合框添加分隔符的实现也可以应用于列表框。因此,我同时提供了 SeparatorComboBox 和 SeparatorListBox,并提供了如截图所示的演示。为简单起见,以下各节我只讨论组合框。
使用 SeparatorComboBox
假设 comboBox1 是一个 SeparatorComboBox 对象。让我们添加项目作为字符串
comboBox1.AddString("All Fruits");
comboBox1.AddString("Banana");
comboBox1.AddString("Orange");
comboBox1.AddString("Pear");
comboBox1.AddString("Watermelon");
comboBox1.AddString("*Add/Edit Fruit");
为了方便您,AddString() 只是 ComboBox 中 Items.Add() 的一个包装器。接下来,像这样设置分隔符位置
comboBox1.SetSeparator(1);
comboBox1.SetSeparator(-1);
如图所示,SetSeparator(1) 在索引 1 处的“Banana”项目之前设置了一个分隔符。第二行设置在位置 -1,表示在最后一个项目处设置分隔符。即:在“*Add/Edit Fruit”之前。这是通过位置设置分隔符的方法,这是一个零基索引。然而,如果您需要通过插入、删除或排序来更新组合框,那么按位置设置就不合适了。我提供了另一种通过内容设置分隔符的方法。因此,在此示例中,而不是使用
comboBox1.AddString("*Add/Edit Fruit");
comboBox1.SetSeparator(-1);
您可以像这样设置一个与文本“*Add/Edit Fruit”相关的分隔符
comboBox1.AddStringWithSeparator("*Add/Edit Fruit");
然后,无论其位置如何,分隔符将始终附着在“*Add/Edit Fruit”上。总而言之,SeparatorComboBox 具有以下三个方法
- AddString(string s):追加一个字符串项目,等同于 Items.Add(s)。
- AddStringWithSeparator(string s):添加一个字符串项目,并在文本 s 前面添加一个分隔符。
- SetSeparator(int pos):通过零基索引位置或从底部开始的负数添加分隔符。
此外,SeparatorComboBox 还提供了五个用于视觉效果的可选属性
- DashStyle SeparatorStyle:设置 DashStyle 中定义的分隔符样式,例如实线、点线、虚线等。默认为 DashStyle.Solid。
- Color SeparatorColor:设置 Color 中定义的分隔符颜色。默认为 Color.Black。
- int SeparatorWidth:根据默认单位(例如像素)设置分隔符的宽度。默认为 1。
- int SeparatorMargin:设置分隔符两端的水平边距。默认为 1。
- bool AutoAdjustItemHeight:指示是否允许根据 SeparatorWidth 自动调整项目高度。默认为 false。
对于演示组合框,我调用了
comboBox1.SeparatorColor = Color.DarkBlue;
comboBox1.SeparatorWidth = 2;
comboBox1.AutoAdjustItemHeight = true;
我保留了 SeparatorStyle 为实线,SeparatorMargin 为 1。为了在项目之间创建更大的间隔,我将 AutoAdjustItemHeight 设置为 true。至于演示中的列表框,我使用了 SeparatorColor(黑色)、SeparatorWidth(1)、AutoAdjustItemHeight(false)的默认值,并调用了
listBox1.SeparatorStyle = DashStyle.Dash;
listBox1.SeparatorMargin = 2;
或者,您可以在窗体设计器中设置这五个属性
实现
主要工作是重写 OnDrawItem() 并绘制项目之间的线条。但在绘制之前,我们应该准备好所有分隔符的信息集合。这是 _separators ArrayList,一个异构容器,用于存储所有分隔符的位置或字符串
public void SetSeparator(int position)
{
_separators.Add(position);
}
public void AddStringWithSeparator(string s)
{
Items.Add(s);
_separators.Add(s);
}
在 OnDrawItem() 中,我搜索 _separators 以查找来自 DrawItemEventArgs 参数传入的当前索引的匹配项。这通过比较字符串或位置来完成
protected override void OnDrawItem(DrawItemEventArgs e)
{
if (-1 == e.Index) return; // Not selected
bool sep = false;
object o;
for (int i=0; !sep && i<_separators.Count; i++)
{
o = _separators[i]; // Get a separator
if (o is string) // Set by content
{
if ((string)this.Items[e.Index] == o as string)
sep = true; // Match content
}
else // Set by position
{
int pos = (int)o;
if (pos<0) pos += Items.Count; // Negative position, reversed
if (e.Index == pos) sep = true; // Match position
}
}
e.DrawBackground();
Graphics g = e.Graphics;
int y = e.Bounds.Location.Y +_separatorWidth-1; // Adjust top Location
// if _separatorWidth>1
if (sep)
{
Pen pen = new Pen(_separatorColor, _separatorWidth);
pen.DashStyle = _separatorStyle; // Apply all properties
g.DrawLine(pen, e.Bounds.Location.X+_separatorMargin, y,
e.Bounds.Location.X+e.Bounds.Width-_separatorMargin, y);
y++;
}
Brush br = DrawItemState.Selected == (DrawItemState.Selected & e.State)?
SystemBrushes.HighlightText: new SolidBrush(e.ForeColor);
g.DrawString((string)Items[e.Index], e.Font, br, e.Bounds.Left, y+1);
base.OnDrawItem(e);
}
现在,如果一个项目有分隔符,我将沿着其边界线的顶部绘制一条线,使用 _separatorColor、_separatorWidth、_separatorStyle 和 _separatorMargin 的属性。最后,无论是否绘制了分隔符,我都必须自己绘制项目文本。为了自动调整项目高度,我重写了 OnMeasureItem() 如下
protected override void OnMeasureItem(MeasureItemEventArgs e)
{
if (_autoAdjustItemHeight)
e.ItemHeight += _separatorWidth;
base.OnMeasureItem(e);
}
注意事项:由于每次添加或插入项目后会立即调用 OnMeasureItem(),因此您必须在调用 AddString() 和 AddStringWithSeparator() 之前设置 SeparatorWidth 和 AutoAdjustItemHeight。否则,您将无法获得预期的结果。此外,e.ItemHeight 是原始项目高度,您可能在代码或设计器中手动初始化它。
关注点
作为派生类,SeparatorComboBox 继承了 ComboBox 的所有公共成员。在演示中,我为 SelectedIndexChanged 事件和 TextChanged 事件创建了一个处理程序,因为我设置了 DropDown 组合框样式。此外,我还添加了 Insert 和 Delete 按钮来调用 ComboBox 的方法。
这里,代码与直接使用 ComboBox 没有区别。在尝试插入和删除时,您可以验证两种不同的分隔符:如果按位置设置,它将始终固定在指定的索引上;如果按内容设置,它将始终固定在指定文本上。
private void comboBox1_SelectedIndexChanged(object sender, System.EventArgs e)
{
textBox1.Text = "Selected: " +comboBox1.SelectedItem;
}
private void comboBox1_TextChanged(object sender, System.EventArgs e)
{
textBox1.Text = "Changed to: " +comboBox1.Text;
}
private void buttonInsert_Click(object sender, System.EventArgs e)
{
comboBox1.Items.Insert(comboBox1.Items.Count, comboBox1.Text);
}
private void buttonDelete_Click(object sender, System.EventArgs e)
{
try
{
int n = int.Parse(comboBox1.Text);
if (n>comboBox1.Items.Count-1) throw new Exception();
comboBox1.Items.RemoveAt(n);
}
catch (Exception)
{
MessageBox.Show("Please enter a valid index to delete an item.",
"Error");
}
}
这些只是微不足道的示例。为了满足您的需求,您需要微调您的框。我注意到组合框中唯一不满意的一点是,如果将 AutoAdjustItemHeight 设置为 true,则下拉列表的高度计算不正确。这导致垂直滚动条始终出现,即使项目很少。
历史
- 2007 年 5 月 28 日 - 原版本发布