ComboBox OwnerDraw in 5 minutes

A question came up the other day about how to add a separator to a ComboBox, and unfortunately the answer is that you need to use OwnerDraw (hand drawing everything for the combo box instead of letting the operating system paint itself) to achieve this. Its not as bad as it seems, OwnerDraw is really pretty straight forward once you’ve figured out the various required pieces.

Instead of using a separate item for this (which runs into problems like what happens if someone selects this item), we’ll draw the line right underneath one of the items. In order to make everything visually look the same size, we’ll increase the size of the items with separators by 3 pixels.

Turning on OwnerDraw

In order to turn on "owner draw", we need to set the DrawMode property to OwnerDrawVariable (this means each item can have a different height), and respond to the MeasureItem (to tell the combo the size of the item) and to the DrawItem (to actually paint the item) events.

this.comboBox1.DrawMode = DrawMode.OwnerDrawVariable;
this.comboBox1.MeasureItem += MeasureItemEventHandler(this.comboBox1_MeasureItem);
this.comboBox1.DrawItem += new DrawItemEventHandler(this.comboBox1_DrawItem);

Keeping track of what is a separator item

Since the comboBox takes objects as items and displays them using the ToString() method, we can be super tricky here and distinguish the items we want to have separators by wrapping them in our own class.

The only thing special about SeparatorItem is that when someone calls ToString() on the SeparatorItem, it calls ToString() on the contained object. We’ll look for these items when drawing/measuring to accommodate for the separator which should follow the item.

       comboBox1.Items.Add("Apple");
comboBox1.Items.Add(new SeparatorItem("Orange"));
comboBox1.Items.Add("Dog");
comboBox1.Items.Add(new SeparatorItem("Cat"));
comboBox1.Items.Add("Boy");
comboBox1.Items.Add("Girl");

      public class SeparatorItem {
private object data;
public SeparatorItem(object data) {
this.data = data;
}
public override string ToString() {
if (data != null) {
return data.ToString();
}
return base.ToString ();
}
}

Measuring the Item

Essentially we want to make the item as tall as the string + some extra padding on top and bottom, and if it is a SeparatorItem, add our three extra pixels for the separator.

Why three pixels? We want to have one whitespace above and one below so that when you keyboard through the combobox the separator looks evenly spaced out from the blue highlight.

        private const int separatorHeight = 3, verticalItemPadding = 4;

        private void comboBox1_MeasureItem(object sender, MeasureItemEventArgs e) {

// fetch the current item we’re painting as specified by the index
object comboBoxItem = comboBox1.Items[e.Index];

// measure the text of the item (in Whidbey consider using TextRenderer.MeasureText instead)
Size textSize = e.Graphics.MeasureString(comboBoxItem.ToString(), comboBox1.Font).ToSize();
e.ItemHeight = textSize.Height + verticalItemPadding;
e.ItemWidth = textSize.Width;

            // if we are a separator item, add in room for the separator
if (comboBoxItem is SeparatorItem) {
// one white line, one dark, one white.
e.ItemHeight += separatorHeight;
}
}

   
Drawing the Item

Here’s where it gets interesting. We need to first paint the background effects, then draw our string, then draw our line if it is a separator.

Since we’re not doing anything fancy with the background painting, we’re just going to use the e.DrawBackground() and the e.DrawFocusRectangle() methods to do the stock painting for these. If you’re doing something more intricate, you would want to avail yourself of the e.State member from the DrawItemEventArgs – this will give you gobs of information on what should be painted when. 

     private void comboBox1_DrawItem(object sender, DrawItemEventArgs e) {
object comboBoxItem = comboBox1.Items[e.Index];

e.DrawBackground();
e.DrawFocusRectangle();

The next thing we paint is the text, which we use DrawString to actually paint the text (in Whidbey consider using TextRenderer). We can directly use the ForeColor from the DrawItemEventArgs to determine what color (eg black or white highlight) we need to paint.

The only real gotcha is that we need to make sure that if it is the SeparatorItem, we subtract the three extra pixels before drawing the text vertically centered within the item bounds.

                bool isSeparatorItem = (comboBoxItem is SeparatorItem);

            // draw the text
using (Brush textBrush = new SolidBrush(e.ForeColor)) {
Rectangle bounds = e.Bounds;
// adjust the bounds so that the text is centered properly.

                // if we're a separator, remove the separator height
if (isSeparatorItem) {
bounds.Height -= separatorHeight;
}

                // Draw the string vertically centered but on the left
using (StringFormat format = new StringFormat()) {
format.LineAlignment = StringAlignment.Center;
format.Alignment = StringAlignment.Near;
// in Whidbey consider using TextRenderer.DrawText instead
e.Graphics.DrawString(comboBoxItem.ToString(), comboBox1.Font, textBrush, bounds, format);
}
}

Drawing the Separator

Finally if it is a separator item that is NOT in the combobox edit control, go ahead and fill the bottom three pixels of the item with white, then draw a line across the middle. Here is a blown up image of a separator item - the red box encompases all of e.Bounds. Notice how the last three pixels are used for painting the separator. Try to ignore the off by one bug with the separator line (perhaps you can see how to tweak the code below). ;)

             
    (Separator item, enlarged) 

        // draw the separator line
if (isSeparatorItem && e.State != DrawItemState.ComboBoxEdit) {
Rectangle separatorRect = new Rectangle(e.Bounds.Left, e.Bounds.Bottom - separatorHeight,
e.Bounds.Width, separatorHeight);

// fill the background behind the separator
using (Brush b = new SolidBrush(comboBox1.BackColor)) {
e.Graphics.FillRectangle(b, separatorRect);
}
e.Graphics.DrawLine(SystemPens.ControlText, separatorRect.Left+2, separatorRect.Top+1,
separatorRect.Right-2, separatorRect.Top+1);

}

See the entire code!