IconComboBox Table of Contents

Many, if not all, of the Windows ComboBoxes contain interesting features such as icons, margins, and dividers. What doesn't make sense is, why doesn't the ComboBox control included with .NET contain these features? For some applications, it significantly improves the user's experience to have visual indication of a feature, conceptual division, or grouping. One, it provides quick understanding of the data, and two, it just downright looks cooler than a plain old text-only combo box.

First, I do want to point to the IconComboBox control at vbAccelerator. I point this out because it uses a different approach than I do, and also because it has additional advanced features that readers might find useful. For my purposes, I could not use a control developed by someone else, for legal reasons - it must be a control I can include in my project without copyright restrictions (by the way, I grant the same right to readers here; I built this control, it's yours to use).

For my own control, I originally did not start including icons in the combo box - my initial desire was simply to be able to produce divider lines between groups in the combo box. However, when I realized how easy it was to add that feature, I included it, which required very few changes.

It is platform dependent, relying on a specific version of Windows to support a particular messaging schema.

That approach, while useful for its time, has a way of obfuscating what the code does - without knowing the inner workings of what Windows is doing when you send those messages, to the reader you might as well be telling Windows to make biscuits.

I thought .NET was supposed to eliminate the confusion of using the Win32 API?

Thus, I decided that my approach needed to make as little use of the Windows API as possible - which, in fact, it will do if you don't want to use icons in your ComboBox. It was as easy as overriding the OnDrawItem event, which was pretty surprising when I discovered the technique.

Now, I wanted to implement a ComboBox whose interface is as close as possible to the normal ComboBox. I believe my approach has done that - the only change is the item you add to the combo box, a custom item used to facilitate the custom data store.

Finally, I would like to give credit where credit is due. Some of the logic I used in the control was inspired by the Explorer ComboBox and ListView in VB.NET by Calum McLellan. While I did not end up using any of his code, except for controlling the font size and display style, it was his methodology that taught me how to implement the custom drawing.

Next, I would like to give credit for the IconExtractor class. I do not know who invented it first, because it is used in two places, so I will simply provide both credits: Associated Icons Image Control By Sergio Pereira, whose project was posted first, and FilesListBox By Eli Gazit, whose approach to custom drawing the ListBox I also used in another control I am using to create a custom file listing display (I am using a ListView instead of a ListBox though, and the logic is significantly different). I added one other function to the IconExtractor for my own uses, which extracts icons not normally found using ExtractIcon, which will be discussed below as the documentation on the function did not actually tell how to use it.

If you want to skip the detailed explanation of how the control works, simply open your Toolbox, right-click and select "Choose Items", navigate to the IconComboBox/bin/ directory, and select IconComboBox.dll. Now, you can use the control just like a regular ComboBox from the Toolbox.

You may additionally need to add IconComboBox.dll as a reference to your project. The source code contains full XML documentation, so you should have full IntelliSense support for what all the objects, methods, and functions do.

First, we will discuss the main control class, IconComboBox. This is the main class that inherits from the System.Windows.Forms.ComboBox control.

First, in order to control how the ComboBox dropdown items are drawn, the control's DrawMode must be set to Windows.Forms.DrawMode.OwnerDrawFixed. What does this mean? The OwnerDrawFixed DrawMode means that the control's items will be drawn by the current class, and that each item will be the same size.

Second, the DropDownStyle on the base ComboBox must be set to DropDownList - we don't want the associated TextBox that accompanies some ComboBoxes.

Finally, we don't want these properties changed - so we catch those events and force the styles to remain the way we want, in case a user somehow finds a way to set the styles to unacceptable values.

The IconComboBox exposes all the normal ComboBox properties, as well as two additional properties: SelectedItem and ToolTipText. SelectedItem is pretty straightforward - it returns the IconComboItem represented by the currently selected item in the ComboBox.

I would like to talk for a moment though about ToolTipText. Strangely, the standard System.Windows.Forms.ComboBox does not expose a ToolTip for you to use. It was easy enough to add one, and in the source code, you will see ToolTip1 added as a component to the IconComboBox control. Then, IconComboBox exposes the ToolTipText property, so that you may set this text string. The ToolTip's default value is the Data string for the current IconComboItem - so remember that if you want to set the ToolTip text yourself, set this property each time the SelectedIndex changes.

Next, we want to handle the SelectedIndexChanged event. Why do we want to handle this event? Normally, consumer code handles these kinds of events. In our case, since we are using a custom data store, we need to make sure that the ComboBox displays what we want it to display. We also want to provide a ToolTip to the user so that they know more about the selected item, in case the ComboBox is not wide enough to display everything (which is usually the case).

You'll notice, I implement a couple of checks that seem to be unnecessary - that Me.SelectedIndex is >= 0 and also that Me.SelectedIndex is within the array bounds of the IconComboItemCollection. If the user selected an item from the dropdown, why is that necessary?

Because the selected index can be changed programmatically, as the Demo project demonstrates. We need to ensure that any indexing operations only occur on actual IconComboItems.

Finally, notice that I check to see if the selected item is a divider. If it is, I don't want dividers selected - they don't contain any useful data. If we're at the beginning of the list, select the next item. Otherwise, select the previous item. Finally, set the ToolTip text to the Data string for the selected IconComboItem. That text can also be set using the ToolTipText property, discussed above.

Note that consumer code can never create a divider item - only the IconComboBox can do that, using a .NET 2.0 feature called "Split Properties". Basically, the IsDivider property on the IconComboItem has a "Friend" modifier on the Set operation - meaning that only code in the same assembly can Set the IsDivider property.

So how do we actually add a divider then? There is an AddDivider method exposed by IconComboBox just for that purpose, shown in Listing 3.

Listing 3 - the AddDivider method, which demonstrates that only internal code can set the IsDivider property.

Why do I bother to set the DisplayText property to the empty string? This is because the standard ComboBox cannot accept Null values for the display text - which makes sense.

We also want to handle changes to the IconComboItemCollection - remember the event handler defined in the IconComboBox constructor? Here's the code that handles those events. It is very simple and straightforward, and is the backend interface to the ComboBox we're inheriting from. Listing 4 updates the underlying ComboBox with the changes that have been made to our custom collection.

Finally, we need to discuss the real meat of this object - everything so far has just been decoration around the ComboBox. You remember that the basic method we are using here to draw the icons and divider lines is overriding the OnDrawItem method, instead of using the Windows Messaging API. Well, that part is fairly straightforward:

First, you'll notice that independent of any of my custom drawing, I am drawing the item background, focus rectangle, and calling the OnDrawItem method of my base class. This is so that all the standard behaviors you expect are implemented. Second, we're checking the index of the item being drawn - why? Because when the control has no items, e.Index is equal to -1. Now, why is OnDrawItem being called if there are no items to draw? What this is actually drawing is the underlying empty ListBox that is used in the base ComboBox. We're also checking that this item is inside our IconComboItemCollection, because again, nothing stops someone from calling this function programatically. We want to make sure we're going to draw an existing IconComboItem. There is also a check to make sure currentItem is not Null, not shown here.

If this IconComboItem is a divider, we want to draw a divider. Listing 5 draws a divider. bounds is the e.Bounds value passed into the OnDrawItem event.

Do you see how easy it is to draw the icon in the specified location, then move over a little and draw the text? There is no need to use the Windows Messaging API - this is a much more straightforward, understandable, and readable approach. Now, what is that ItemHeight value? This is a property of the underlying ComboBox; it specifies how high the ComboBox says an item should be. This value is determined by the underlying ListBox when OnMeasureItem is called - an event I don't seem to have access to. Otherwise, I would modify the height of the divider items to only be several pixels high, instead of being the height of an entire row.

This is the custom object that holds our combo box item data. It is a very simple class that exposes four properties:

ItemImage() As Icon - the icon associated with this item

Data() AsString - The full data string to save for this item

DisplayText() AsString - the string to display for this item

IsDivider() AsBoolean - returns whether or not this is a divider item; can only be set by IconComboBox.

There is an associated constructor which accepts these values, except for IsDivider, which is Set explicitly by the IconComboBox.

There is one point of interest for the IconComboItem - the fact that it implements the IEquatable(Of T) interface. Generic lists will use the Equals method exposed by the IEquatable interface to compare items. I implement this interface so that the IconComboItemCollection is able to use the Contains, IndexOf,Remove, and Sort methods of the underlying Generic.List.

Finally, there is the IconComboItemCollection. Since the preceding discussion has addressed most of this collection's functionality, I will only demonstrate how the collection notifies the IconComboBox that it has been changed:

m_List is the private variable used to implement the Generic.List(Of IconComboItem). IconComboItemCollectionChangedEventArgs is a tiny class used to deliver event arguments to event handlers; if it is not obvious here what data the event handler delivers, take a look at the source code.

I have made a list of the following items I think could be done to improve this control. If anyone has any ideas on how they can be implemented, please let me know.

How to truly generalize the custom data store so that the Data item can be anyType, instead of just a string. In my application, I needed to save the full string but only display part of it; I tried implementing the collection using Generics, but I ran into the chicken and the egg problem - how do I know what System.Type to use for the IconComboItemCollection prior to the object being instantiated?

I would like to make it so that the divider lines:

do not take up a full item's row height, and

are not highlightable as the mouse passes over the list.

I attempted to override the OnMeasureItem event, but that function never got called; I assume it is only getting called by the underlying ListBox control, which I do not appear to be able to access.

It might be nice to implement a drawing interface so that the consumer can specify a Draw method for the dividers - that way, they can draw any divider they want, instead of the hardcoded double lines I have now.

I have also thought about implmenting the icons as an ImageList and providing each IconComboItem an ImageIndex property, instead of straight assignment of the icon to the Item. This would bring the interface even closer to the ListView interface for using item icons. (Which suggests another method of implementing a ComboBox - rewrite the control entirely to use a ListView as its underlying dropdown, rather than a ListBox.)

It would be nice to provide Designer support for the control. This is kinda hard to do, and while I know the basics, creating a solid Designer framework for the control is not something I can do right now.

since you cant select the seperator, you're limited to what you can scroll to when you use up/down arrows or the mouse wheel.

I've seen other implementations out there who draw a line at the top of the list item to signify a new group, and one that draws inbetween the items so its never selectable. there needs to be some way to scroll through items despite the dividers.