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

分隔符组合框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (24投票s)

2007年5月28日

CPOL

4分钟阅读

viewsIcon

186667

downloadIcon

2183

带分隔符的自定义组合框和列表框控件

Screenshot - cssepcb1.jpg

引言

本文是我之前关于 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;

或者,您可以在窗体设计器中设置这五个属性

Screenshot - cssepcb2.jpg

实现

主要工作是重写 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 的方法。

Screenshot - cssepcb3.jpg

这里,代码与直接使用 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 日 - 原版本发布
© . All rights reserved.