Click here to Skip to main content
15,892,643 members
Articles / Programming Languages / Visual Basic

OwnerDrawn ComboBox with Icons, Divider, and a generic data store

Rate me:
Please Sign up or sign in to vote.
4.65/5 (24 votes)
5 Apr 2006CPOL12 min read 90.7K   2.6K   73  
Customize the appearance and behavior of a ComboBox without using Windows messaging.
'Copyright (c) 2006 Patrick Sears
' No rights are conferred by this code, although you are allowed to use it freely.
' Just give credit where credit is due.
' 
' Remember, no warranty and no guarantee for any express purpose are given

Imports System.Windows.Forms
Imports System.IO
Imports System.Text
Imports System.ComponentModel
Imports System.Runtime.InteropServices
Imports System.Drawing

Public Class IconComboBox
    Inherits Windows.Forms.ComboBox

    Private m_CurrentItem As IconComboItem
    Private m_IconComboItemList As IconComboItemCollection

    ''' <summary>
    ''' Enumeration to indicate how the IconComboItemCollection gets changed
    ''' for various operations
    ''' </summary>
    ''' <remarks>Only used internally by IconComboBox</remarks>
    Public Enum IconComboItemCollectionChangeType
        ItemAdded = 0
        ItemRemoved
        ItemInserted
        CollectionCleared
    End Enum

    ''' <summary>
    ''' Get or set the tooltip text to be shown in the tooltip over the
    ''' combobox.
    ''' </summary>
    ''' <value>The new string to display in the tooltip.</value>
    ''' <returns>The string currently being displayed in the tooltip.</returns>
    ''' <remarks>This string defaults to the Data.ToString value when an item
    ''' in the ComboBox is selected.</remarks>
    Public Property ToolTipText() As String
        Get
            Return ToolTip1.GetToolTip(Me)
        End Get
        Set(ByVal value As String)
            ToolTip1.SetToolTip(Me, value)
        End Set
    End Property

    ''' <summary>
    ''' The collection of IconComboItems used to render the ComboBox.
    ''' </summary>
    ''' <returns>The collection of IconComboItems used to render the ComboBox.</returns>
    ''' <remarks>This property provides a similar interface to the .NET ComboBox, 
    ''' needing to call ComboBox1.Items.Add("MyStr") to add an item.</remarks>
    Public Overloads ReadOnly Property Items() As IconComboItemCollection
        Get
            Return m_IconComboItemList
        End Get
    End Property

    ''' <summary>
    ''' Gets the IconComboItem object for the currently selected item in the dropdown.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overloads Property SelectedItem() As IconComboItem
        Get
            If Me.SelectedIndex >= 0 Then
                Return m_IconComboItemList(Me.SelectedIndex)
            Else
                Return Nothing
            End If
        End Get
        Set(ByVal Value As IconComboItem)
            m_CurrentItem = Value
        End Set
    End Property

    ''' <summary>
    ''' Creates a new instance of the IconComboBox.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New()
        MyBase.New()

        InitializeComponent()

        m_IconComboItemList = New IconComboItemCollection
        AddHandler m_IconComboItemList.CollectionChanged, AddressOf m_IconComboItemList_CollectionItemsChanged
        'Set to OwnerDraw
        Me.DrawMode = Windows.Forms.DrawMode.OwnerDrawFixed
        'Must be set to DropDownList
        Me.DropDownStyle = ComboBoxStyle.DropDownList
    End Sub

    'ComboBox must have its style set to DropDownList
    Private Sub ExpCombo_DropDownStyleChanged(ByVal sender As Object, ByVal e As System.EventArgs)
        Me.DropDownStyle = ComboBoxStyle.DropDownList
    End Sub

    'Make sure font doesn't get set to more than 12 points
    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

    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

            ' Set the default ToolTip string.
            ToolTip1.SetToolTip(Me, m_CurrentItem.Data)
        End If
    End Sub

    ''' <summary>
    ''' Draws an IconComboBox item into the ComboBox in the area specified by the DrawItemEventArgs.
    ''' </summary>
    ''' <param name="e">Event argument specifying which item to draw and where to draw it.</param>
    ''' <remarks></remarks>
    Protected Overrides Sub OnDrawItem(ByVal e As System.Windows.Forms.DrawItemEventArgs)
        e.DrawBackground()
        e.DrawFocusRectangle()

        Dim bounds As Rectangle = e.Bounds
        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)

            If currentItem IsNot Nothing Then
                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
                    Dim imageSize As Size
                    Dim fileNameOnly As String = currentItem.DisplayText 'Items(e.Index).ToString()
                    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)
                End If
            Else
                Console.WriteLine("Error, the selected item is nothing")
            End If
        End If

        MyBase.OnDrawItem(e)
    End Sub

    ''' <summary>
    ''' Add a divider line to the ComboBox.  The line will be appended to the end of the list.
    ''' </summary>
    ''' <returns>The index of the added divider.</returns>
    ''' <remarks></remarks>
    Public Function AddDivider() As Integer
        Dim tempItem As New IconComboItem
        'tempItem.ItemImage = Nothing
        'tempItem.Data = Nothing
        tempItem.DisplayText = ""
        tempItem.IsDivider = True

        Return Me.Items.Add(tempItem)
    End Function

    ' I am forced to raise an event from the IconComboItemCollection to inform me when it changes - 
    ' e.g. the user calls Items.Clear, Items.Add, or Items.Remove.  I need to be notified of this so that
    ' I can remove the corresponding item from the base ComboBox object 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

#Region "Class IconComboItem"
    ''' <summary>
    ''' Object representing an entry in the IconComboBox.
    ''' </summary>
    ''' <remarks>Implements IEquatable for searching and sorting the IconComboItemCollection.</remarks>
    Public Class IconComboItem
        Implements IEquatable(Of IconComboItem)

        Private m_Icon As Icon
        Private m_Data As String
        Private m_DisplayText As String
        Private m_isDivider As Boolean

        ''' <summary>
        ''' The image to be displayed next to the text for this combo box item.
        ''' </summary>
        ''' <value>The icon to be displayed</value>
        ''' <returns>The icon currently being displayed</returns>
        ''' <remarks></remarks>
        Public Property ItemImage() As Icon
            Get
                Return m_Icon
            End Get
            Set(ByVal value As Icon)
                m_Icon = value
            End Set
        End Property

        ''' <summary>
        ''' The string representing data you want associated with this IconComboItem.
        ''' </summary>
        ''' <value>The data to be set</value>
        ''' <returns>The data currently saved</returns>
        ''' <remarks></remarks>
        Public Property Data() As String
            Get
                Return m_Data
            End Get
            Set(ByVal value As String)
                m_Data = value
            End Set
        End Property

        ''' <summary>
        ''' What to display for this item.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property DisplayText() As String
            Get
                Return m_DisplayText
            End Get
            Set(ByVal value As String)
                m_DisplayText = value
            End Set
        End Property

        ''' <summary>
        ''' Indicates whether this IconComboItem is a divider.
        ''' </summary>
        ''' <value>Boolean indicating if this item is a divider or not.  This property can only be SET
        ''' by the IconComboBox.</value>
        ''' <returns>True if this is a divider; False if it is not.</returns>
        ''' <remarks></remarks>
        Public Property IsDivider() As Boolean
            Get
                Return m_isDivider
            End Get
            ' This property is Friend so that ONLY THE IconComboBox can set this property.
            Friend Set(ByVal value As Boolean)
                m_isDivider = value
            End Set
        End Property

        Public Sub New()
        End Sub

        ''' <summary>
        ''' Create a new IconComboItem with the specified values
        ''' </summary>
        ''' <param name="argText">The text to display in the combo box</param>
        ''' <param name="argData">The string representing this IconComboItem's data</param>
        ''' <remarks></remarks>
        Public Sub New(ByVal argText As String, ByVal argData As String)
            m_DisplayText = argText
            m_Data = argData
        End Sub

        Public Shared Operator =(ByVal item1 As IconComboItem, ByVal item2 As IconComboItem) As Boolean
            Return item1.Data = item2.Data
        End Operator

        Public Shared Operator <>(ByVal item1 As IconComboItem, ByVal item2 As IconComboItem) As Boolean
            Return item1.Data <> item2.Data
        End Operator

        ''' <summary>
        ''' Returns if this IconComboItem is equal to the specified one.
        ''' </summary>
        ''' <param name="other">The IconComboItem to compare</param>
        ''' <returns>True if this item's Data property equals the other item's Data property.</returns>
        ''' <remarks></remarks>
        Public Overloads Function Equals(ByVal other As IconComboItem) As Boolean Implements System.IEquatable(Of IconComboItem).Equals
            Return m_Data = other.Data
        End Function
    End Class
#End Region

#Region "Class IconComboItemCollection"
    Public Class IconComboItemCollection

        Private m_List As New Generic.List(Of IconComboBox.IconComboItem)
        Friend Event CollectionChanged(ByVal sender As Object, ByVal e As IconComboItemCollectionChangedEventArgs)

        ''' <summary>
        ''' The number of items in the current collection
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property Count() As Integer
            Get
                Return m_List.Count
            End Get
        End Property

        ''' <summary>
        ''' Gets the IconComboItem at the specfied index
        ''' </summary>
        ''' <param name="index">The index of the IconComboItem to return</param>
        ''' <value></value>
        ''' <returns>The IconComboItem at the specified index, or nothing if the index is out of range.</returns>
        ''' <remarks></remarks>
        Default Public ReadOnly Property Item(ByVal index As Integer) As IconComboItem
            Get
                If index < m_List.Count Then
                    Return m_List(index)
                Else
                    Return Nothing
                End If
            End Get
        End Property

        ''' <summary>
        ''' Add the specified IconComboItem to the end of the collection.
        ''' </summary>
        ''' <param name="argItem">The IconComboItem to add.  Can be Null.</param>
        ''' <returns>The zero-based index where the IconComboItem was added.</returns>
        ''' <remarks></remarks>
        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

        ''' <summary>
        ''' Insert the specified IconComboItem at the specified index.
        ''' </summary>
        ''' <param name="idx"></param>
        ''' <param name="argItem"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Function Insert(ByVal idx As Integer, ByVal argItem As IconComboItem) As Boolean
            m_List.Insert(idx, argItem)

            If m_List(idx) = argItem Then
                RaiseEvent CollectionChanged(Me, New IconComboItemCollectionChangedEventArgs(idx, IconComboBox.IconComboItemCollectionChangeType.ItemInserted, argItem))
                Return True
            End If
            Return False
        End Function

        Public Function Remove(ByVal argItem As IconComboItem) As Boolean
            If m_List.Remove(argItem) Then
                RaiseEvent CollectionChanged(Me, New IconComboItemCollectionChangedEventArgs(0, IconComboBox.IconComboItemCollectionChangeType.ItemRemoved, argItem))
                Return True
            End If
            Return False
        End Function

        ''' <summary>
        ''' Clear all the items from this collection and from the combo box.
        ''' </summary>
        ''' <remarks></remarks>
        Public Sub Clear()
            m_List.Clear()
            RaiseEvent CollectionChanged(Me, New IconComboItemCollectionChangedEventArgs(-1, IconComboItemCollectionChangeType.CollectionCleared, Nothing))
        End Sub

        ''' <summary>
        ''' Determine if the collection contains the specified IconComboItem, using the 
        ''' <see>IconComboItem.Equals</see> method for comparison.
        ''' </summary>
        ''' <param name="item"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Function Contains(ByVal item As IconComboItem) As Boolean
            'For Each tempitem As IconComboItem In List
            '    If tempitem.Equals(item) Then
            '        Return True
            '    End If
            'Next
            'Return False
            'Return List.Contains(item)
            Return m_List.Contains(item)
        End Function

        ''' <summary>
        ''' Searches for the specified IconComboItem and returns the zero-based index of the first 
        ''' occurrence within the entire IconComboItemCollection.
        ''' </summary>
        ''' <param name="item">The object to locate in the IconComboItemCollection. 
        ''' The value can be null for reference types.</param>
        ''' <returns>The zero-based index of the first occurrence of item within 
        ''' the entire IconComboItemCollection, if found; otherwise, �1.</returns>
        ''' <remarks></remarks>
        Public Function IndexOf(ByVal item As IconComboItem) As Integer
            Return m_List.IndexOf(item)
        End Function
    End Class
#End Region

#Region "Friend Class IconComboItemCollectionChangedEventArgs"
    ''' <summary>
    ''' These args are used in events indicating that the IconComboItemCollection has been
    ''' changed.  This class can only be used by the IconComboBox.
    ''' </summary>
    ''' <remarks></remarks>
    Friend Class IconComboItemCollectionChangedEventArgs
        Inherits EventArgs

        Private m_ChangeType As IconComboBox.IconComboItemCollectionChangeType
        Private m_ChangedItem As IconComboBox.IconComboItem
        Private m_ChangedIndex As Integer

        ''' <summary>
        ''' Indicates how the IconComboItemCollection was changed - add, delete, remove, or clear.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property ChangeType() As IconComboBox.IconComboItemCollectionChangeType
            Get
                Return m_ChangeType
            End Get
        End Property

        ''' <summary>
        ''' The item that was changed resulting in this event.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property ChangedItem() As IconComboBox.IconComboItem
            Get
                Return m_ChangedItem
            End Get
        End Property

        ''' <summary>
        ''' The index of the changed item in the IconComboItemCollection.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property ChangedIndex() As Integer
            Get
                Return m_ChangedIndex
            End Get
        End Property

        ''' <summary>
        ''' Create a new instance of these event args with the specified arguments
        ''' </summary>
        ''' <param name="argidx">Index of the changed item</param>
        ''' <param name="argType">How the item was changed. <seealso>IconComboBox.IconComboItemCollectionChangeType</seealso></param>
        ''' <param name="argItem">The <see>IconComboItem</see> that was changed</param>
        ''' <remarks></remarks>
        Public Sub New(ByVal argidx As Integer, ByVal argType As IconComboBox.IconComboItemCollectionChangeType, ByVal argItem As IconComboBox.IconComboItem)
            m_ChangeType = argType
            m_ChangedItem = argItem
            m_ChangedIndex = argidx
        End Sub
    End Class
#End Region
End Class

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Software Developer Microsoft
United States United States
I did some stuff.. I live in Seattle.. now I work for Live Search at Microsoft Smile | :)

Comments and Discussions