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.
Prior attempts to customize the ComboBox
to include dividers and icons have relied on using the Windows API to send Windows messages to the controls to control their appearance, as in another CodeProject article from 2005, whose code looks familiar to an article from 2003.
However, I wanted to stay away from that approach, for three reasons:
- 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.
IconComboBox
consists of three objects:
IconComboBox
- The actual IconComboBox
control itself, which derives from System.Windows.Forms.ComboBox
IconComboItem
- A class representing an item in the dropdown list of the ComboBox
. IconComboItemCollection
- A Generic collection which stores the list of IconComboItem
s and provides an interface identical to:
- the regular
ComboBox.Items
interface, and - the
Generic.List(Of T)
interface.
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 ComboBox
es.
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.
Listing 1 demonstrates controlling the visual styles on the control.
Public Sub New()
MyBase.New()
InitializeComponent()
m_IconComboItemList = New IconComboItemCollection
AddHandler m_IconComboItemList.CollectionChanged, _
AddressOf m_IconComboItemList_CollectionItemsChanged
Me.DrawMode = Windows.Forms.DrawMode.OwnerDrawFixed
Me.DropDownStyle = ComboBoxStyle.DropDownList
End Sub
Private Sub ExpCombo_DropDownStyleChanged(ByVal sender _
As Object, ByVal e As System.EventArgs)
Me.DropDownStyle = ComboBoxStyle.DropDownList
End Sub
Protected Overrides Sub OnFontChanged(ByVal e As System.EventArgs)
If Me.Font.SizeInPoints > 12 Then
Me.Font = New Font(Me.Font.FontFamily, 12, Me.Font.Style)
End If
End Sub
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).
Listing 2 demonstrates handling the SelectedIndexChanged
event.
Protected Overrides Sub OnSelectedIndexChanged(ByVal e As System.EventArgs)
If Me.SelectedIndex < 0 Then
Exit Sub
End If
If Me.SelectedIndex < m_IconComboItemList.Count Then
If m_IconComboItemList(Me.SelectedIndex).IsDivider Then
If Me.SelectedIndex > 0 Then
Me.SelectedIndex -= 1
m_CurrentItem = m_IconComboItemList(Me.SelectedIndex)
ElseIf Me.SelectedIndex < m_IconComboItemList.Count - 1 Then
Me.SelectedIndex += 1
m_CurrentItem = m_IconComboItemList(Me.SelectedIndex)
End If
Else
m_CurrentItem = m_IconComboItemList(Me.SelectedIndex)
MyBase.OnSelectedIndexChanged(e)
End If
ToolTip1.SetToolTip(Me, m_CurrentItem.Data)
End If
End Sub
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 IconComboItem
s.
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.
Public Function AddDivider() As Integer
Dim tempItem As New IconComboItem
tempItem.DisplayText = ""
tempItem.IsDivider = True
Return Me.Items.Add(tempItem)
End Function
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.
Private Sub m_IconComboItemList_CollectionItemsChanged(ByVal sender _
As Object, ByVal e As IconComboItemCollectionChangedEventArgs)
If e.ChangeType = IconComboItemCollectionChangeType.ItemAdded Then
MyBase.Items.Add(e.ChangedItem.DisplayText)
ElseIf e.ChangeType = IconComboItemCollectionChangeType.ItemInserted Then
MyBase.Items.Insert(e.ChangedIndex, e.ChangedItem.DisplayText)
ElseIf e.ChangeType = IconComboItemCollectionChangeType.ItemRemoved Then
MyBase.Items.Remove(e.ChangedItem.DisplayText)
ElseIf e.ChangeType = IconComboItemCollectionChangeType.CollectionCleared Then
MyBase.Items.Clear()
End If
End Sub
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:
Protected Overrides Sub OnDrawItem(ByVal e As _
System.Windows.Forms.DrawItemEventArgs)
e.DrawBackground()
e.DrawFocusRectangle()
If e.Index > -1 AndAlso e.Index < Me.Items.Count _
AndAlso e.Index < m_IconComboItemList.Count Then
Dim currentItem As IconComboItem = m_IconComboItemList(e.Index)
...
End If
MyBase.OnDrawItem(e)
End Sub
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.
If currentItem.IsDivider Then
Dim xStart As Single = bounds.X + 10
Dim yStart As Single = Convert.ToSingle(bounds.Y + bounds.Height / 2 + 1)
Dim xEnd As Single = bounds.Width - 10
Dim yEnd As Single = yStart
e.Graphics.DrawLine(Pens.Black, xStart, yStart, xEnd, yEnd)
yStart = Convert.ToSingle(bounds.Y + bounds.Height / 2 - 1)
yEnd = yStart
e.Graphics.DrawLine(Pens.Black, xStart, yStart, xEnd, yEnd)
Else
If this item is an actual IconComboItem
, we want to draw its associated icon and then its DisplayText
. Listing 6 draws an IconComboItem
.
Dim imageSize As Size
Dim fileNameOnly As String = currentItem.DisplayText
Dim fullFileName As String = currentItem.Data
Dim fileIcon As Icon = currentItem.ItemImage
Dim imageRectangle As New Rectangle(bounds.Left + 1, _
bounds.Top + 1, ItemHeight, ItemHeight)
If fileIcon IsNot Nothing Then
e.Graphics.DrawIcon(fileIcon, imageRectangle)
End If
imageSize = imageRectangle.Size
Dim fileNameRec As New Rectangle(bounds.Left + _
imageSize.Width + 3, bounds.Top, _
bounds.Width - imageSize.Width - 3, bounds.Height)
Dim format As New StringFormat()
format.LineAlignment = StringAlignment.Center
e.Graphics.DrawString(fileNameOnly, e.Font, _
New SolidBrush(e.ForeColor), fileNameRec, format)
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() As String
- The full data string to save for this item DisplayText() As String
- the string to display for this item IsDivider() As Boolean
- 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:
Listing 7 - Raising
CollectionChanged
events from the
IconComboItemCollection
.
Public Function Add(ByVal argItem As IconComboItem) As Integer
m_List.Add(argItem)
RaiseEvent CollectionChanged(Me, New _
IconComboItemCollectionChangedEventArgs(m_List.Count _
- 1, IconComboBox.IconComboItemCollectionChangeType.ItemAdded, _
argItem))
Return m_List.Count - 1
End Function
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.
That's about it. Suggestions, comments, questions, bug fixes are welcome!
- Thursday, April 06, 2006 - Initial submission.