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:
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:
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:
comboBox1.AddString("*Add/Edit Fruit");
comboBox1.SetSeparator(-1);
You can set a separator related to the text "*Add/Edit Fruit"
like this:
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:
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:
listBox1.SeparatorStyle = DashStyle.Dash;
listBox1.SeparatorMargin = 2;
Alternatively, you can set five properties in the form designer:
![Screenshot - cssepcb2.jpg](/KB/combobox/SeparatorComboListBox/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:
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:
protected override void OnDrawItem(DrawItemEventArgs e)
{
if (-1 == e.Index) return;
bool sep = false;
object o;
for (int i=0; !sep && i<_separators.Count; i++)
{
o = _separators[i];
if (o is string)
{
if ((string)this.Items[e.Index] == o as string)
sep = true;
}
else
{
int pos = (int)o;
if (pos<0) pos += Items.Count;
if (e.Index == pos) sep = true;
}
}
e.DrawBackground();
Graphics g = e.Graphics;
int y = e.Bounds.Location.Y +_separatorWidth-1;
if (sep)
{
Pen pen = new Pen(_separatorColor, _separatorWidth);
pen.DashStyle = _separatorStyle;
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:
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](/KB/combobox/SeparatorComboListBox/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.
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
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.