Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

A Separator Combo/List Box

4.89/5 (27 votes)
28 May 2007CPOL4 min read 3   2.2K  
The Custom Combo Box and List Box Control with Separators

Screenshot - cssepcb1.jpg

Introduction

This article would be a late follow-up to my previous article, A Separator Combo Box, regarding a control class derived from MFC CCombobox. Since it was posted, several readers have asked for a similar separator combo box in C#. While touching C# programming, I found it easier to write such a custom combo box control as shown in above screen shot. The main point in implementing the separator in a combo box is that a separator should not be selected either from UI or in code logic. We have two choices: let a separator occupy an item space or let a separator reside between items without extra space. I like the latter; it simply draws a line between items, saving space for the whole combo box.

Since both combo box and list box are derived from Windows.Forms.ListControl, they have the same virtual OnDrawItem() and OnMeasureItem() functions available in my customization. Here, the implementation of adding a separator to the combo box can also be applied to the list box. So, I present both SeparatorComboBox and SeparatorListBox, giving the demo as you see it in the screen shot. For simplicity, I only discuss the combo box in the following sections.

Using SeparatorComboBox

Suppose that comboBox1 is a SeparatorComboBox object. Let's add the items as a string:

C#
comboBox1.AddString("All Fruits");
comboBox1.AddString("Banana");
comboBox1.AddString("Orange");
comboBox1.AddString("Pear");
comboBox1.AddString("Watermelon");
comboBox1.AddString("*Add/Edit Fruit");

For your convenience, AddString() is simply a wrapper for Items.Add() in ComboBox. Next, set the separator positions like this:

C#
comboBox1.SetSeparator(1);
comboBox1.SetSeparator(-1);    

As shown in the image, SetSeparator(1) sets a separator before the item "Banana" at index 1. The second line sets at position -1, meaning a separator set at the last item. That is: before "*Add/Edit Fruit". This is the method to set a separator by position, a zero-based index. However, if you need to update a combo box by insertion, deletion, or sorting, then setting by position is not appropriate. I provide another method to set a separator by content. Thus, in this example, instead of using:

C#
comboBox1.AddString("*Add/Edit Fruit");
comboBox1.SetSeparator(-1);    

You can set a separator related to the text "*Add/Edit Fruit" like this:

C#
comboBox1.AddStringWithSeparator("*Add/Edit Fruit");

Then a separator is always stuck on "*Add/Edit Fruit", regardless of its position. To summarize, SeparatorComboBox has three methods as follows:

  • AddString(string s): Appends a string item, equivalent to Items.Add(s).
  • AddStringWithSeparator(string s): Adds a string item with a separator before the text s.
  • SetSeparator(int pos): Adds a separator by a zero-based index position or by a negative from the bottom.

In addition, SeparatorComboBox provides five optional properties for visual effects:

  • DashStyle SeparatorStyle: Sets the separator style defined in DashStyle, such as solid, dot, dash, etc. Default is DashStyle.Solid.
  • Color SeparatorColor: Sets the separator color defined in Color. Default is Color.Black.
  • int SeparatorWidth: Sets the separator width based on the default unit, e.g., in pixels. Default is 1.
  • int SeparatorMargin: Sets the separator horizontal margin at both ends. Default is 1.
  • bool AutoAdjustItemHeight: Indicates whether you allow automatic adjustment for the item height based on SeparatorWidth. Default is false.

For the demo combo box, I call:

C#
comboBox1.SeparatorColor = Color.DarkBlue;
comboBox1.SeparatorWidth = 2;
comboBox1.AutoAdjustItemHeight = true;

I leave SeparatorStyle as solid and SeparatorMargin to 1. To make greater intervals between items, I set AutoAdjustItemHeight to true. As for the list box in the demo, I use default values for SeparatorColor (black), SeparatorWidth (1), AutoAdjustItemHeight (false) and call:

C#
listBox1.SeparatorStyle = DashStyle.Dash;
listBox1.SeparatorMargin = 2;

Alternatively, you can set five properties in the form designer:

Screenshot - cssepcb2.jpg

Implementations

The main job is to override OnDrawItem() and draw a line between items. Yet before drawing, we should have an information collection ready for all separators. This is the _separators ArrayList, a heterogeneous container storing positions or stings for all separators:

C#
public void SetSeparator(int position)
{
    _separators.Add(position);
}

public void AddStringWithSeparator(string s)
{
    Items.Add(s);
    _separators.Add(s);
}

In OnDrawItem(), I search _separators to find a match for the current index passed from the DrawItemEventArgs parameter. This is done by comparing either a string or a position:

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

Now, if an item has a separator, I draw a line along the top of its boundary with the properties of _separatorColor, _separatorWidth, _separatorStyle, and _separatorMargin. Finally, regardless of whether a separator is drawn or not, I have to draw the item text myself. To adjust item height automatically, I override OnMeasureItem() like this:

C#
protected override void OnMeasureItem(MeasureItemEventArgs e)
{
    if (_autoAdjustItemHeight)
        e.ItemHeight += _separatorWidth;

    base.OnMeasureItem(e);
}

The caveat: Because OnMeasureItem() is getting called each time, immediately after an item is added or inserted, you must set SeparatorWidth and AutoAdjustItemHeight right before calling AddString() and AddStringWithSeparator(). Otherwise, you can't achieve expected results. Also, e.ItemHeight is the original item height that you may initialize manually in code or in the designer.

Points of interest

As a derived class, SeparatorComboBox inherits all public members from ComboBox. In the demo, I create a handler for the event SelectedIndexChanged and a handler for TextChanged, since I set the DropDown combo box style. Also, I add the Insert and Delete buttons to call ComboBox's methods.

Screenshot - cssepcb3.jpg

Here, the code is nothing different from that using ComboBox directly. When trying insertion and deletion, you can verify two different separators: if set by position it always sticks on a specified index, and if set with content it always sticks on that specified text.

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

These are just trivial samples. To fit your needs, you have to fine-tune your boxes. The only unsatisfactory aspect I noticed in the combo box is that if AutoAdjustItemHeight is set to true, the height of the dropdown list is not correctly calculated. This causes a vertical scroll bar always appearing, even with fewer items.

History

  • 28 May, 2007 - Original version posted

License

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