Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Flat-MultiColumn Combobox with Autocomplete

0.00/5 (No votes)
8 Sep 2007 3  
MTGCCombobox: a .NET Combobox that is flat, multicolumn and with Autocomplete feature

Contents

Introduction

Once I had to develop a program using VBA with Access, and I had the great pleasure to use the combobox supplied with it: awesome! Multicolumn, Databound, Autocompleting and really fast. Since VB5 and VB6, I was searching for something similar I could use in my applications, but no luck! Nor has the situation changed with VS.NET, and the combobox control remains the same with its limitations and bugs. After some experience in building controls, like new textboxes and folderbrowser, it's time for our multicolumn combobox (written in VB.NET).

Features

Here is a brief description of the main features of our control:

  • Multiple Column (for now, max 4) with configurable widths
  • Flat look similar to the XP combobox
  • Highlight combobox border and arrow on MouseEnter and GotFocus events
  • Autocomplete text written in the text area, based on first column values
  • Overridden DropDownStyle property to manage Autocomplete feature with DropDownList style
  • Loading the combobox via MTGCComboboxItem or through a DataTable
  • Custom colors for border (highlighted and not), arrow, dropdownlist grid, etc.
  • Custom designer used to disable/enable properties at design time, managing verbs etc.

Public Properties

Name Description
DropDownList This property is just present in the "parent" combobox, but in this one is overridden and now has 2 possible values:
  • DropDown (default one)
  • DropDownList
ManagingFastMouseMoving If true, a timer is used to manage the combobox repaint in case of fast mouse moving inside and outside the combo
ManagingFastMouseMovingInterval Timer interval (in ms) used when ManagingFastMouseMoving is set to true
NormalBorderColor This is the Border Color of the Combo when not Highlighted
ArrowColor (NEW) Color of the Arrow in the Control Box
DisabledArrowColor (NEW) Color of the Arrow in the Control Box when Enabled=False
ArrowBoxColor (NEW) Background Color of the Arrow Control Box
DisabledArrowBoxColor (NEW) Background Color of the Arrow Control Box when Enabled=False
DisabledBorderColor (NEW) Border Color of the Combo when Enabled=False
DropDownForeColor Text Color of the item selected in the DropDownList
DropDownBackColor Background Color of the item selected in the DropDownList
DropDownArrowBackColor Background Color of the Arrow box when DropDownList is open
ColumnNum Number of Columns shown in the DropDownList (max 4)
ColumnWidth Size of columns in pixel, split by ;
GridLineVertical If true, there will be a vertical line dividing every column in the DropDownList
GridLineHorizontal If true, there will be a horizontal line dividing every rows in the DropDownList
CharacterCasing Use this if you want normal, lower or upper casing text (just like in TextBoxes)
BorderStyle Set this to FlatXP if you want the Flat look, or to Fixed3D for the usual look
LoadingType There are two ways to load our combobox
  • ComboboxItem: this is a custom item derived from ListViewItem (see the Code part below to know more about it)
  • DataTable: you can pass a DataTable to the combobox and it will get the first ColumnNum columns (or the ones specified with the SourceDataString property) and show them
HighLightBorderColor Color of the combobox border when highlighted (when the mouse is over or it has the focus)
HighLightBorderOnMouseEvents Are you tired of the highlighting feature? Set this property to false and you will disable it
SourceDataTable DataTable used as source when the LoadingType property is set to DataTable
SourceDataString String Array used to specify which columns of the DataTable have to be shown (and in which order)
SelectedItem (NEW) This is the Selected Item in the combobox
SelectedValue (NEW) This is the Selected Value in the combobox

Using the code

Flat Border

To reach the Flat Border look, we have to repaint the whole border and the arrow box, so we manage the WndProc event:

 Protected Overrides Sub WndProc(ByRef m As Message)
  MyBase.WndProc(m)

  If Me.BorderStyle = TipiBordi.FlatXP Then
   Select Case m.Msg
    Case &HF, &H133
     'WM_PAINT


     'We have to find if the Mouse is Over the combo

     Dim mouseIsOver As Boolean
     Dim mousePosition As Point = Control.MousePosition
     mousePosition = PointToClient(mousePosition)
     mouseIsOver = ClientRectangle.Contains(mousePosition)

     If Me.HighlightBorderOnMouseEvents AndAlso (
      mouseIsOver OrElse Me.Focused) Then
      Dim g As Graphics = Graphics.FromHwnd(Me.Handle)

      DrawBorder(g, Me.HighlightBorderColor)
      DrawHighlightedArrow(g, False)
     Else
      Dim g As Graphics = Graphics.FromHwnd(Me.Handle)

      DrawBorder(g, m_NormalBorderColor)
      DrawNormalArrow(g, True)
     End If

    Case &H2A3
     'WM_MOUSELEAVE

     If Me.Focused Then Exit Sub
     If currentColor.Equals(m_HighlightBorderColor) Then
      Dim mouseIsOver As Boolean
      Dim mousePosition As Point = Control.MousePosition
      mousePosition = PointToClient(mousePosition)
      mouseIsOver = ClientRectangle.Contains(mousePosition)

      If Not mouseIsOver Then
       Dim g As Graphics = Graphics.FromHwnd(Me.Handle)
       DrawBorder(g, m_NormalBorderColor)
       DrawNormalArrow(g, True)
       g.Dispose()
      End If
     End If
    Case &H200
     'WM_MOUSEMOVE

     If Me.HighlightBorderOnMouseEvents = True AndAlso Not Highlighted Then
      currentColor = Me.HighlightBorderColor
      Dim g As Graphics = Graphics.FromHwnd(Me.Handle)
      DrawBorder(g, currentColor)
      DrawHighlightedArrow(g, False)
      g.Dispose()
     End If
    Case &H46
     'WM_WINDOWPOSCHANGING

     If Me.BorderStyle = TipiBordi.FlatXP Then
      'Repaint the arrow when pressed

      If Me.HighlightBorderOnMouseEvents Then
       Dim g As Graphics = Graphics.FromHwnd(Me.Handle)
       Dim pressedColorBrush As Brush = New SolidBrush(m_DropDownBackColor)
       Dim Larghezza As Integer =
       SystemInformation.VerticalScrollBarWidth - arrowWidth
       g.FillRectangle(pressedColorBrush, New Rectangle((Left - Larghezza),
       Top - 1, SystemInformation.VerticalScrollBarWidth + 1, Height + 2))
       Dim p As Pen = New Pen(HighlightBorderColor)
       g.DrawRectangle(p, (Left - Larghezza) - 1, Top - 2,
       SystemInformation.VerticalScrollBarWidth + 2, Height + 4)
       DrawArrow(g, False)
       g.Dispose()
       Me.Invalidate()
      End If
     End If
    Case Else
     Exit Select

   End Select
  End If
 End Sub

First thing to notice, this part of code is used only if the BorderStyle property is set to FlatXp.

The HighLight border is drawn when 3 conditions are true:

  • The HighlightBorderOnMouseEvents property is set to true AND
  • The mouse is over the control OR
  • The control has the focus

We draw the combobox in two steps: first the border (DrawBorder) and then the arrowbox (DrawNormalArrow and DrawHighlightedArrow). Here is the code of these procedures:

    'Calculate the location of the Arrow Box

 Private Sub ArrowBoxPosition(ByRef left As Integer,
 ByRef top As Integer, ByRef width As Integer, ByRef height As Integer)
  Dim rc As Rectangle = ClientRectangle
  width = arrowWidth
  left = rc.Right - width - 2
  top = rc.Top + 2
  height = rc.Height - 4
 End Sub

 'Draw the Flat Arrow Box when not highlighted

 Private Sub DrawNormalArrow(ByRef g As Graphics, ByVal disable As Boolean)
  If Me.BorderStyle = TipiBordi.FlatXP Then
   Dim left, top, arrowWidth, height As Integer
   ArrowBoxPosition(left, top, arrowWidth, height)

   Dim stripeColorBrush As Brush = New SolidBrush(SystemColors.Control)
   Dim Larghezza As Integer = SystemInformation.VerticalScrollBarWidth _
                                                                - arrowWidth
   If (Me.Enabled) Then
    Dim b As Brush = New SolidBrush(SystemColors.Control)
    g.FillRectangle(b, New Rectangle(left - Larghezza, top - 2,
     SystemInformation.VerticalScrollBarWidth, height + 4))
   End If

   If Me.Enabled Then
    Dim p As Pen = New Pen(m_NormalBorderColor)
    g.DrawLine(p, New Point(ClientRectangle.Right -
    SystemInformation.VerticalScrollBarWidth - 2, ClientRectangle.Top),
    New Point(ClientRectangle.Right, ClientRectangle.Top))
    g.DrawLine(p, New Point(ClientRectangle.Right -
    SystemInformation.VerticalScrollBarWidth - 2, ClientRectangle.Bottom - 1),
     New Point(ClientRectangle.Right, ClientRectangle.Bottom - 1))

    If Not disable Then
     DrawHighlightedArrow(g, True)
     g.FillRectangle(stripeColorBrush, left, top - 1,
     arrowWidth + 1, height + 2)
    Else
     g.FillRectangle(stripeColorBrush, left - 5, top - 1,
     arrowWidth + 6, height + 2)
    End If

    DrawArrow(g, False)
   Else
    Dim p As Pen = New Pen(SystemColors.InactiveBorder)
    g.DrawLine(p, New Point(ClientRectangle.Right -
    SystemInformation.VerticalScrollBarWidth - 2, ClientRectangle.Top),
    New Point(ClientRectangle.Right, ClientRectangle.Top))
    g.DrawLine(p, New Point(ClientRectangle.Right -
    SystemInformation.VerticalScrollBarWidth - 2, ClientRectangle.Bottom - 1),
    New Point(ClientRectangle.Right, ClientRectangle.Bottom - 1))

    ' Now draw the unselected background

    g.FillRectangle(stripeColorBrush, left - 5, top - 1,
    arrowWidth + 6, height + 2)

    DrawArrow(g, True)
   End If
   Highlighted = False
  End If
 End Sub

 'Draw the Flat Arrow Box when highlighted

 Private Sub DrawHighlightedArrow(ByRef g As Graphics, ByVal Delete As Boolean)
  If Me.BorderStyle = TipiBordi.FlatXP Then
   Dim left, top, arrowWidth, height As Integer
   ArrowBoxPosition(left, top, arrowWidth, height)

   If (Me.Enabled) Then
    Dim comboTextWidth As Integer =
     SystemInformation.VerticalScrollBarWidth - arrowWidth
    If (comboTextWidth < 0) Then comboTextWidth = 1
    Dim b As Brush = New SolidBrush(HighlightBorderColor)
   End If

   If Not Delete Then
    If (DroppedDown) Then
     Dim cbg As Graphics = CreateGraphics()
     Dim pressedColorBrush As Brush =
     New SolidBrush(m_DropDownArrowBackColor)
     Dim Larghezza As Integer =
     SystemInformation.VerticalScrollBarWidth - arrowWidth
     cbg.FillRectangle(pressedColorBrush, New Rectangle(
     (left - Larghezza), top - 1, SystemInformation.VerticalScrollBarWidth + 1,
      height + 2))
     Dim p As Pen = New Pen(HighlightBorderColor)
     cbg.DrawRectangle(p, (left - Larghezza) - 1, top - 2,
     SystemInformation.VerticalScrollBarWidth + 2, height + 4)
     DrawArrow(cbg, False)
     cbg.Dispose()
     Exit Sub
    Else
     If Enabled Then
      Dim b As Brush = New SolidBrush(m_DropDownBackColor)
      Dim Larghezza As Integer =
      SystemInformation.VerticalScrollBarWidth - arrowWidth
      g.FillRectangle(b, New Rectangle((left - Larghezza), top - 1,
      SystemInformation.VerticalScrollBarWidth + 1, height + 2))

      Dim pencolor As Color = customBorderColor
      If (pencolor.Equals(Color.Empty)) Then
       pencolor = BackColor
      End If
     End If
    End If
   Else
    Dim b As Brush = New SolidBrush(BackColor)
    g.FillRectangle(b, left - 1, top - 1, arrowWidth + 2, height + 2)
   End If
   If Me.Enabled Then DrawArrow(g, False)
   Highlighted = True
  End If
 End Sub

 Private Sub DrawArrow(ByVal g As Graphics, ByVal Disable As Boolean)
  If Me.BorderStyle = TipiBordi.FlatXP Then
   Dim left, top, arrowWidth, height As Integer
   ArrowBoxPosition(left, top, arrowWidth, height)

   Dim extra As Integer = 1
   If (bUsingLargeFont) Then extra = 2

   'triangle vertex of the arrow

   Dim pts(2) As Point
   pts(0) = New Point(left + arrowWidth / 2 - 2 - extra - 2,
   top + height / 2 - 1)
   pts(1) = New Point(left + arrowWidth / 2 + 3 + extra - 1,
   top + height / 2 - 1)
   pts(2) = New Point(left + arrowWidth / 2 - 1,
   (top + height / 2 - 1) + 3 + extra)

   'draw the arrow as a polygon

   If (Disable) Then
    Dim b As Brush = New SolidBrush(arrowDisableColor)
    g.FillPolygon(b, pts)
   Else
    Dim b As Brush = New SolidBrush(arrowColor)
    g.FillPolygon(b, pts)
   End If
  End If
 End Sub

 Private Sub DrawBorder(ByVal g As Graphics, ByVal DrawColor As Color)
  If Me.BorderStyle = TipiBordi.FlatXP Then
   g.DrawRectangle(New pen(Me.BackColor, 1), ClientRectangle.Left + 1,
   ClientRectangle.Top + 1, ClientRectangle.Width - 1,
   ClientRectangle.Height - 3)

   'Draw the Border

   If Me.Enabled = False Then 'combo disabilitato

    DrawColor = SystemColors.InactiveBorder
   End If

   Dim pen As pen = New pen(DrawColor, 1)
   'Border Rectangle

   g.DrawRectangle(pen, ClientRectangle.Left, ClientRectangle.Top,
   ClientRectangle.Width - 1, ClientRectangle.Height - 1)
   'Button Rectangle

   g.DrawRectangle(pen, ClientRectangle.Left, ClientRectangle.Top,
   ClientRectangle.Width - SystemInformation.VerticalScrollBarWidth - 3,
    ClientRectangle.Height - 1)
  End If
 End Sub

I think there is nothing tricky about this code, just some GDI+ work and a lot of time to reach the desired result!

Autocomplete feature (UPDATED)

This feature is based on the original "AutoComplete ComboBox in VB.NET" code by Daryl, published here on The Code Project. Thank's to Daryl!

I think the Autocomplete feature is a useful one, especially when you have a combobox filled with a lot of items (in my case, all Italian cities, it means more than 10000 items!) and you want to help the user when he is typing in it. To manage the Autocomplete, we have to catch all the events regarding keys, so OnKeyPress, OnKeyDown and OnKeyUp.

   Protected Overrides Sub OnKeyPress_
        (ByVal e As System.Windows.Forms.KeyPressEventArgs)
      'AUTOCOMPLETE: we have to know when a key has been really pressed


      If Me.DropDownStyle = CustomDropDownStyle.DropDown Then
          PressedKey = True
          If Asc(e.KeyChar) = 8 Then
              If Me.SelectedText = Me.Text Then
                  Me.SelectedIndex = -1
              End If
              If Asc(e.KeyChar) = 13 Then e.Handled = True _
        'This is used to suppress the "Beep" when Enter key is pressed

          End If
      Else
          'ReadOnly AutoComplete Management

          Dim sTypedText As String
          Dim iFoundIndex As Integer
          Dim currentText As String
          Dim Start, selLength As Integer

          If Asc(e.KeyChar) = 8 Then
              If Me.SelectedText = Me.Text Then
                  PressedKey = True
                  Me.SelectedIndex = -1
                  Exit Sub
              End If
          End If
          If Me.SelectionLength > 0 Then
              Start = Me.SelectionStart
              selLength = Me.SelectionLength

              'This is equivalent to Me.Text, but sometimes using Me.Text 

              'it doesn't work

              currentText = Me.AccessibilityObject.Value
              Dim posizione As Long
              Dim testingString As String
              posizione = InStr(Me.Text, "&")
              If posizione > 0 Then
                  'The "&" character is contained in Me.Text

                  testingString = Microsoft.VisualBasic.Left(Me.Text, _
                posizione - 1) & Microsoft.VisualBasic.Right_
                (Me.Text, Len(Me.Text) - posizione)
              Else
                  testingString = Me.Text
              End If
              If UCase(testingString) = UCase(Me.AccessibilityObject.Value) _
            Then
                  currentText = Me.Text
              End If

              currentText = currentText.Remove(Start, selLength)
              currentText = currentText.Insert(Start, e.KeyChar)
              sTypedText = currentText
          Else
              Start = Me.SelectionStart
              sTypedText = Me.Text.Insert(Start, e.KeyChar)
          End If
          iFoundIndex = Me.FindString(sTypedText)
          If (iFoundIndex >= 0) Then
              PressedKey = True
          Else
              e.Handled = True
          End If
      End If

      MyBase.OnKeyPress(e)
  End Sub

 Protected Overrides Sub OnKeyDown(ByVal e As _
                System.Windows.Forms.KeyEventArgs)
    Debug.WriteLine("OnKeyDown " & Me.Text)
    If Me.DropDownStyle = CustomDropDownStyle.DropDownList _
                AndAlso e.KeyCode = Keys.Delete Then
        If Me.Text <> Me.SelectedText Then
            e.Handled = True
        Else
            Me.SelectedIndex = -1
        End If
    End If
    If Me.DropDownStyle = CustomDropDownStyle.DropDown _
                AndAlso e.KeyCode = Keys.Delete Then
        If Me.Text = Me.SelectedText Then
            Me.SelectedIndex = -1
        End If
    End If

    MyBase.OnKeyDown(e)
 End Sub

 Protected Overrides Sub OnKeyUp(ByVal e As System.Windows.Forms.KeyEventArgs)
    'AUTOCOMPLETING


    'WARNING: With VB.NET 2003 there is a strange behaviour. 

    'This event is raised not just when any key is pressed

    'but also when the Me.Text property changes. Particularly, 

    'it happens when you write in a fast way (for example

    'you press 2 keys and the event is raised 3 times). 

    'To manage this we have added a boolean variable PressedKey that

    'is set to true in the OnKeyPress Event


    Dim sTypedText As String
    Dim iFoundIndex As Integer
    Dim oFoundItem As Object
    Dim sFoundText As String
    Dim sAppendText As String

    Debug.WriteLine("OnKeyUp " & Me.Text)
    If PressedKey Then
        'Ignoring alphanumeric chars

        Select Case e.KeyCode
            Case Keys.Left, Keys.Right, Keys.Up, Keys.Delete, _
                Keys.Down, Keys.End, Keys.Home
                Return
        End Select

        'Get the Typed Text and Find it in the list

        sTypedText = Me.Text
        If e.KeyCode <> Keys.Back Then
            iFoundIndex = Me.FindString(sTypedText)
        Else
            iFoundIndex = Me.FindStringExact(sTypedText)
        End If

        'If we found the Typed Text in the list then Autocomplete

        If iFoundIndex >= 0 AndAlso Me.Text <> "" Then

            'Get the Item from the list (Return Type depends if 

            'Datasource was bound or List Created) 

            oFoundItem = Me.Items(iFoundIndex)

            'Use the ListControl.GetItemText to resolve the Name 

            'in case the Combo was Data bound 

            sFoundText = Me.GetItemText(oFoundItem)

            'Append then found text to the typed text to preserve case

            sAppendText = sFoundText.Substring(sTypedText.Length)
            Me.Text = sTypedText & sAppendText

            'Select the Appended Text

            Me.SelectionStart = sTypedText.Length
            Me.SelectionLength = sAppendText.Length

            If e.KeyCode = Keys.Enter Then
                iFoundIndex = Me.FindStringExact(Me.Text)
                Me.SelectedIndex = iFoundIndex
                SendKeys.Send(vbTab)
                e.Handled = True
            End If
        Else
            'Forcing SelectedItem to Nothing if we can't Autocomplete

            Me.SelectedIndex = -1
            Me.SelectedItem = Nothing
        End If

    End If
    PressedKey = False
 End Sub

 Protected Overrides Sub OnLeave(ByVal e As System.EventArgs)
     'Selecting the item whose text is shown in the text area of the ComboBox

     Dim iFoundIndex As Integer
     'The Me.AccessibilityObject.Value is used instead of Me.Text to manage

     'the event when you write in the combobox text and the DropDownList

     'is open. In this case, if you click outside the combo, Me.Text maintains

     'the old value and not the current one

     Dim currentText As String
     currentText = Me.AccessibilityObject.Value
     Dim testingString As String
     Dim posizione As Long
     posizione = InStr(Me.Text, "&")
     If posizione > 0 Then
         'The "&" character is contained in Me.Text

         testingString = Microsoft.VisualBasic.Left(Me.Text, posizione - 1) _
        & Microsoft.VisualBasic.Right(Me.Text, Len(Me.Text) - posizione)
     Else
         testingString = Me.Text
     End If
     If UCase(testingString) = UCase(Me.AccessibilityObject.Value) Then
         currentText = Me.Text
     End If

     iFoundIndex = Me.FindStringExact(currentText)
     Me.SelectedIndex = iFoundIndex
     If iFoundIndex = -1 Then
         Me.SelectedItem = Nothing
     End If
     MyBase.OnLeave(e)
 End Sub

The OnKeyPress event is used only when the DropDownlist property is set to DropDownlist. In this case, the user can't type anything that is not present in the first column of the combobox: a sort of ReadOnly property. The whole Autocomplete process is based on FindString and FindStringExact methods, which return the first item in the combobox (actually the first column of the combo in our case) matching the specified string. When such an item is found, the Text value is completed with the remaining part of the string.

Multicolumn items

To have more columns in the DropDownList is reached using a custom item to load data (MTGCComboBoxItem) and then overriding the DrawItem event to show all items in the correct way.

Public Class MTGCComboBoxItem
 'Since all we need is a "Text" property for this example we can

 'Subclass by inheriting any object desired.

 'For this example, we'll use the ListViewItem

 Inherits ListViewItem
 Implements IComparable

 'each of the below public declarations will be "visible" to the outside

 'You may add as many of these declarations using whatever types you desire

 Public Col1 As String
 Public Col2 As String
 Public Col3 As String
 Public Col4 As String

 'every value of MyInfo you want to store, get's added to the NEW declaration

 Sub New(ByVal C1 As String, Optional ByVal C2 As String = "",
  Optional ByVal C3 As String = "", Optional ByVal C4 As String = "")
  MyBase.New()
  'transfer all incoming parameters to your local storage

  Col1 = C1
  Col2 = C2
  Col3 = C3
  Col4 = C4
  'and finally, pass back the Text property

  Me.Text = C1
 End Sub

 'Function used to sort the items on first element Col1

 Private Function CompareTo(ByVal obj As Object)
 As Integer Implements IComparable.CompareTo
  'every not nothing object is greater than nothing

  If obj Is Nothing Then Return 1

  'this is used to take care of late binding

  Dim other As MTGCComboBoxItem = CType(obj, MTGCComboBoxItem)

  'comparing strings

  Return StrComp(Col1, other.Col1, CompareMethod.Text)
 End Function

End Class
 

Every MTGCComboBoxItem has four sub-elements, Col1 Col2 Col3 and Col4, each one representing one of the possible columns of that row in the combobox. The function CompareTo is used to sort the items based on the value of first Column Col1.

In this way the user can load data in multiple columns, but they have to be shown when the arrow is clicked and the DropDownList appears. So the DrawItem event had to be rewritten. Here, I show just a part of it, because it's a very long procedure (refer to full source code for the full procedure):

'......

 'item selected

 e.Graphics.FillRectangle(New SolidBrush(DropDownBackColor), r)
 Select Case Me.ColumnNum
  Case 1
   If wcol1 > 0 Then
    If Me.LoadingType = CaricamentoCombo.DataTable Then
     e.Graphics.DrawString(Assegna(m_DataTable.Rows(e.Index)
     (Indice(0))).ToString, Me.Font, New SolidBrush(DropDownForeColor),
     rd.X, rd.Y, sf)
    ElseIf Me.LoadingType = CaricamentoCombo.ComboBoxItem Then
     e.Graphics.DrawString(Me.Items.Item(e.Index).Col1.ToString,
     Me.Font, New SolidBrush(DropDownForeColor), rd.X, rd.Y, sf)
    End If
    If Me.m_GridLineHorizontal Then
     e.Graphics.DrawLine(New Pen(GridLineColor, 1), rd.X, rd.Y +
     rd.Height - 1, rd.X + Me.DropDownWidth, rd.Y + rd.Height - 1)
    End If
   End If
  Case 2
   If wcol1 > 0 Then
    If Me.LoadingType = CaricamentoCombo.DataTable Then
     e.Graphics.DrawString(Assegna(m_DataTable.Rows(
     e.Index)(Indice(0))).ToString, Me.Font, New SolidBrush(
     DropDownForeColor), rd.X, rd.Y, sf)
    ElseIf Me.LoadingType = CaricamentoCombo.ComboBoxItem Then
     e.Graphics.DrawString(Me.Items.Item(e.Index).Col1.ToString,
      Me.Font, New SolidBrush(DropDownForeColor), rd.X, rd.Y, sf)
    End If
   End If
   If wcol2 > 0 Then
    If Me.m_GridLineVertical Then
     e.Graphics.DrawLine(New Pen(GridLineColor, 1), rd.X +
     CInt(wcol1) - 2, rd.Y, rd.X + CInt(wcol1) - 2, rd.Y + 15)
    End If
    e.Graphics.FillRectangle(New SolidBrush(DropDownBackColor),
    rd.X + CInt(wcol1) - 1, rd.Y, r.Width - CInt(wcol1) + 1, r.Height)
    If Me.LoadingType = CaricamentoCombo.DataTable Then
     e.Graphics.DrawString(Assegna(m_DataTable.Rows(e.Index)
     (Indice(1))).ToString, Me.Font, New SolidBrush(DropDownForeColor),
     rd.X + CInt(wcol1), rd.Y, sf)
    ElseIf Me.LoadingType = CaricamentoCombo.ComboBoxItem Then
     e.Graphics.DrawString(Me.Items.Item(e.Index).Col2.ToString,
     Me.Font, New SolidBrush(DropDownForeColor), rd.X + CInt(wcol1), rd.Y, sf)
    End If
   End If
   If Me.m_GridLineHorizontal Then
    e.Graphics.DrawLine(New Pen(GridLineColor, 1), rd.X, rd.Y +
    rd.Height - 1, rd.X + Me.DropDownWidth, rd.Y + rd.Height - 1)
   End If
  Case 3
   If wcol1 > 0 Then
    If Me.LoadingType = CaricamentoCombo.DataTable Then
     e.Graphics.DrawString(Assegna(m_DataTable.Rows(e.Index)
     (Indice(0))).ToString, Me.Font, New SolidBrush(DropDownForeColor),
      rd.X, rd.Y, sf)
    ElseIf Me.LoadingType = CaricamentoCombo.ComboBoxItem Then
     e.Graphics.DrawString(Me.Items.Item(e.Index).Col1.ToString,
     Me.Font, New SolidBrush(DropDownForeColor), rd.X, rd.Y, sf)
    End If
   End If
   If wcol2 > 0 Then
    If Me.m_GridLineVertical Then
     e.Graphics.DrawLine(New Pen(GridLineColor, 1), rd.X + CInt(wcol1)
     - 2, rd.Y, rd.X + CInt(wcol1) - 2, rd.Y + 15)
    End If
    e.Graphics.FillRectangle(New SolidBrush(DropDownBackColor), rd.X +
     CInt(wcol1) - 1, rd.Y, r.Width - CInt(wcol1) + 1, r.Height)
    If Me.LoadingType = CaricamentoCombo.DataTable Then
     e.Graphics.DrawString(Assegna(m_DataTable.Rows(e.Index)
     (Indice(1))).ToString, Me.Font, New SolidBrush(DropDownForeColor),
     rd.X + CInt(wcol1), rd.Y, sf)
    ElseIf Me.LoadingType = CaricamentoCombo.ComboBoxItem Then
     e.Graphics.DrawString(Me.Items.Item(e.Index).Col2.ToString,
      Me.Font, New SolidBrush(DropDownForeColor), rd.X + CInt(wcol1), rd.Y, sf)
    End If
   End If
   If wcol3 > 0 Then
    If Me.m_GridLineVertical Then
     e.Graphics.DrawLine(New Pen(GridLineColor, 1), rd.X + CInt(wcol1)
     + CInt(wcol2) - 2, rd.Y, rd.X + CInt(wcol1) + CInt(wcol2) - 2,
      rd.Y + 15)
    End If
    e.Graphics.FillRectangle(New SolidBrush(DropDownBackColor), rd.X
     + CInt(wcol1) + CInt(wcol2) - 1, rd.Y, r.Width - CInt(wcol1) -
      CInt(wcol2) + 1, r.Height)
    If Me.LoadingType = CaricamentoCombo.DataTable Then
     e.Graphics.DrawString(Assegna(m_DataTable.Rows(e.Index)
     (Indice(2))).ToString, Me.Font, New SolidBrush(DropDownForeColor),
     rd.X + CInt(wcol1) + CInt(wcol2), rd.Y, sf)
    ElseIf Me.LoadingType = CaricamentoCombo.ComboBoxItem Then
     e.Graphics.DrawString(Me.Items.Item(e.Index).Col3.ToString, Me.Font,
     New SolidBrush(DropDownForeColor), rd.X + CInt(wcol1)
     + CInt(wcol2), rd.Y, sf)
    End If
   End If
   If Me.m_GridLineHorizontal Then
    e.Graphics.DrawLine(New Pen(GridLineColor, 1), rd.X, rd.Y
    + rd.Height - 1, rd.X + Me.DropDownWidth, rd.Y + rd.Height - 1)
   End If
  Case 4
   If wcol1 > 0 Then
    If Me.LoadingType = CaricamentoCombo.DataTable Then
     e.Graphics.DrawString(Assegna(m_DataTable.Rows(
     e.Index)(Indice(0))).ToString, Me.Font, New SolidBrush(
     DropDownForeColor), rd.X, rd.Y, sf)
    ElseIf Me.LoadingType = CaricamentoCombo.ComboBoxItem Then
     e.Graphics.DrawString(Me.Items.Item(e.Index).Col1.ToString,
      Me.Font, New SolidBrush(DropDownForeColor), rd.X, rd.Y, sf)
    End If
   End If
   If wcol2 > 0 Then
    If Me.m_GridLineVertical Then
     e.Graphics.DrawLine(New Pen(GridLineColor, 1), rd.X +
     CInt(wcol1) - 2, rd.Y, rd.X + CInt(wcol1) - 2, rd.Y + 15)
    End If
    e.Graphics.FillRectangle(New SolidBrush(DropDownBackColor),
     rd.X + CInt(wcol1) - 1, rd.Y, r.Width - CInt(wcol1) +
     1, r.Height)
    If Me.LoadingType = CaricamentoCombo.DataTable Then
     e.Graphics.DrawString(Assegna(m_DataTable.Rows(e.Index)(Indice(1)
     )).ToString, Me.Font, New SolidBrush(DropDownForeColor), rd.X +
     CInt(wcol1), rd.Y, sf)
    ElseIf Me.LoadingType = CaricamentoCombo.ComboBoxItem Then
     e.Graphics.DrawString(Me.Items.Item(e.Index).Col2.ToString,
     Me.Font, New SolidBrush(DropDownForeColor), rd.X + CInt(wcol1), rd.Y, sf)
    End If
   End If
   If wcol3 > 0 Then
    If Me.m_GridLineVertical Then
     e.Graphics.DrawLine(New Pen(GridLineColor, 1), rd.X + CInt(wcol1) +
      CInt(wcol2) - 2, rd.Y, rd.X + CInt(wcol1) + CInt(wcol2) - 2,
       rd.Y + 15)
    End If
    e.Graphics.FillRectangle(New SolidBrush(DropDownBackColor),
     rd.X + CInt(wcol1) + CInt(wcol2) - 1, rd.Y, r.Width - CInt(wcol1)
     - CInt(wcol2) + 1, r.Height)
    If Me.LoadingType = CaricamentoCombo.DataTable Then
     e.Graphics.DrawString(Assegna(m_DataTable.Rows(e.Index)(
     Indice(2))).ToString, Me.Font, New SolidBrush(DropDownForeColor),
     rd.X + CInt(wcol1) + CInt(wcol2), rd.Y, sf)
    ElseIf Me.LoadingType = CaricamentoCombo.ComboBoxItem Then
     e.Graphics.DrawString(Me.Items.Item(e.Index).Col3.ToString, Me.Font,
     New SolidBrush(DropDownForeColor), rd.X + CInt(wcol1)
     + CInt(wcol2), rd.Y, sf)
    End If
   End If
   If wcol4 > 0 Then
    If Me.m_GridLineVertical Then
     e.Graphics.DrawLine(New Pen(GridLineColor, 1), rd.X + CInt(wcol1)
     + CInt(wcol2) + CInt(wcol3) - 2, rd.Y, rd.X + CInt(wcol1) +
     CInt(wcol2) + CInt(wcol3) - 2, rd.Y + 15)
    End If
    e.Graphics.FillRectangle(New SolidBrush(DropDownBackColor), rd.X +
     CInt(wcol1) + CInt(wcol2) + CInt(wcol3) - 1, rd.Y, r.Width -
     CInt(wcol1) - CInt(wcol2) - CInt(wcol3) + 1, r.Height)
    If Me.LoadingType = CaricamentoCombo.DataTable Then
     e.Graphics.DrawString(Assegna(m_DataTable.Rows(e.Index)(
     Indice(3))).ToString, Me.Font, New SolidBrush(DropDownForeColor),
     rd.X + CInt(wcol1) + CInt(wcol2) + CInt(wcol3), rd.Y, sf)
    ElseIf Me.LoadingType = CaricamentoCombo.ComboBoxItem Then
     e.Graphics.DrawString(Me.Items.Item(e.Index).Col4.ToString,
     Me.Font, New SolidBrush(DropDownForeColor), rd.X + CInt(wcol1)
      + CInt(wcol2) + CInt(wcol3), rd.Y, sf)
    End If
   End If
   If Me.m_GridLineHorizontal Then
    e.Graphics.DrawLine(New Pen(GridLineColor, 1), rd.X, rd.Y +
    rd.Height - 1, rd.X + Me.DropDownWidth, rd.Y + rd.Height - 1)
   End If
 End Select
 If Me.BorderStyle = TipiBordi.FlatXP Then
  'Use the border color to highlight the selected item

  If Me.GridLineHorizontal Then
   e.Graphics.DrawRectangle(New Pen(Me.HighlightBorderColor, 1),
    r.X, r.Y, r.Width - 1, r.Height - 2)
  Else
   e.Graphics.DrawRectangle(New Pen(Me.HighlightBorderColor, 1),
   r.X, r.Y, r.Width - 1, r.Height - 1)
  End If
 End If
 e.DrawFocusRectangle()
 '.......

 'Here is the code for the unselected items

 '.......

This piece of code represents the drawing of the selected item. Depending on how many columns have to be shown (the ColumnNum property), each of the four possible values is drawn through the e.Graphics.DrawString method with ascending x position, but only if the corresponding column width is greater than 0. Data is taken from the DataTable or a MTGCComboboxItem array depending on the LoadingType property. The horizontal and vertical gridlines for each element are drawn as well.

Using the control

Ok, I guess you want to know how to use this control! Add it to your VS ToolBox referencing the MTGCComboBox.dll file, then drag and drop the control to your form. First thing to notice: the text control is empty! How many times we had to manually empty it, especially with TextBoxes? Only with VS 2005 this dream will come true....

Loading Items

Let's go on! Now you can set its properties in the way you want in design mode, and after that write the code to load the combo. For example, suppose you want to load the combo with information about our five continents: name, extension and population. You will have to do something like this:

 comboContinent.BorderStyle = MTGCComboBox.TipiBordi.FlatXP
 comboContinent.LoadingType = MTGCComboBox.CaricamentoCombo.ComboBoxItem
 comboContinent.ColumnNum = 3
 comboContinent.ColumnWidth = "80;120;100"

 comboContinent.Items.Add(New MTGCComboBoxItem("Africa",
  "30,065,000 sq km", "807,419,000"))
 comboContinent.Items.Add(New MTGCComboBoxItem("America",
 "42,293,000 sq km", "830,722,000"))
 comboContinent.Items.Add(New MTGCComboBoxItem("Asia",
  "44,579,000 sq km", "3,701,000,000"))
 comboContinent.Items.Add(New MTGCComboBoxItem("Europe",
 "9,938,000 sq km", "730,916,000 "))
 comboContinent.Items.Add(New MTGCComboBoxItem("Oceania",
 "8,112,000 sq km", "31,090,000"))

The ColumnNum is set to 3 and the widths are 80 pixels for the name, 120 for extension and 100 for population. Remember, if you don't want to show a column value just set is width to 0.

ATTENTION: the ColumnWidth property code procedure automatically adds 20 pixel to DropDownWith property value if the columns' width sum is greater then DropDownWith value, to take care of possibly vertical scrollbar shown in the DropDownList!

In this case, we add each MTGCComboBoxItem with the correct values. A faster way to load the items is creating an array of MTGCComboBoxItems and then using the Combobox.Items.AddRange method to load data. Here is the code:

 Dim continentItems(4) As MTGCComboBoxItem

 continentItems(0) = New MTGCComboBoxItem("Africa",
 "30,065,000 sq km", "807,419,000")
 continentItems(1) = New MTGCComboBoxItem("America",
  "42,293,000 sq km", "830,722,000")
 continentItems(2) = New MTGCComboBoxItem("Asia",
 "44,579,000 sq km", "3,701,000,000")
 continentItems(3) = New MTGCComboBoxItem("Europe",
  "9,938,000 sq km", "730,916,000 ")
 continentItems(4) = New MTGCComboBoxItem("Oceania",
 "8,112,000 sq km", "31,090,000")

 comboContinent.Items.AddRange(continentItems)

The other way to load the combo is through a dataTable. In this case, we can create a datatable with our information and then pass it as the SourceDataTable property:

 Dim dtContinents As New DataTable("ContinentInfo")
 dtContinents.Columns.Add("Name", System.Type.GetType("System.String"))
 dtContinents.Columns.Add("Extension",
  System.Type.GetType("System.String"))
 dtContinents.Columns.Add("Population",
 System.Type.GetType("System.String"))

 Dim dr As DataRow
 dr = dtContinents.NewRow

 dr("Name") = "Africa"
 dr("Extension") = "30,065,000 sq km"
 dr("Population") = "807,419,000"

 dtContinents.Rows.Add(dr)

 dr = dtContinents.NewRow

 dr("Name") = "America"
 dr("Extension") = "42,293,000 sq km"
 dr("Population") = "830,722,000"

 dtContinents.Rows.Add(dr)
 dr = dtContinents.NewRow

 dr("Name") = "Asia"
 dr("Extension") = "44,579,000 sq km"
 dr("Population") = "3,701,000,000"

 dtContinents.Rows.Add(dr)
 dr = dtContinents.NewRow

 dr("Name") = "Europe"
 dr("Extension") = "9,938,000 sq km"
 dr("Population") = "730,916,000"

 dtContinents.Rows.Add(dr)
 dr = dtContinents.NewRow

 dr("Name") = "Oceania"
 dr("Extension") = "8,112,000 sq km"
 dr("Population") = "31,090,000"

 dtContinents.Rows.Add(dr)

 comboContinent.LoadingType =
 MTGCComboBox.CaricamentoCombo.DataTable
 comboContinent.SourceDataString = New String(2)
  {"Name", "Extension", "Population"}
 comboContinent.SourceDataTable = dtContinents

Obviously, in most cases you won't fill the DataTable manually (this is just an example), but you will load the datatable through a DataAdapter from a database and then pass it to the combobox. The SourceDataString property is not mandatory, but remember that you have to set it BEFORE setting the SourceDataTable or it won't have any effect and the first ColumnNum columns of the datatable will be shown.

Selecting items (NEW)

To select an item in the combobox you can use the ItemSelect method, that allows you to specify the value to search and the column on which this value will be searched. The method returns a Boolean value, True if the value has been found or False otherwise. The method has two Optional parameters: CaseSensitive is used to determine if the value to search is Case Sensitive or not (default False); RaiseSelectedIndexChanged is used to specify if the SelectedIndexChanged event must be raised after the selection of an item in the combobox through this method or not (default False).

Here is an example:

  'Usually, when I Select an item during the Loading of a Form, 

  'I set the SelectedIndex property to -1

  'If no value is found, but this is not mandatory


  If Not mcbo.ItemSelect(2, "Value1", False, False) Then
      mcbo.SelectedIndex = -1
  End If

Accessing items (NEW)

When an item is selected, you can access each column's value in this way:

    Dim Name, Population, Extension as String

    Name = comboContinent.SelectedItem.col1
    Extension = comboContinent.SelectedItem.col2
    Population= comboContinent.SelectedItem.col3

Points of Interest

Did you know that the classic VS combobox is really bugged? Well, after all my work I perfectly know it! Some of the bugs I've found out:

  • When you open the DropDownList of a combobox, and write a character in the text area and then you click outside the control, the dropdownlist is closed and the text area contains the first item of the combo starting with that character. But actually no item is selected (SelectedIndex = -1)
  • The MouseLeave Event is not always raised, especially if you move the mouse quite fast inside and outside the combo

Work in progress

Yeah, we want to improve this control (so we need your help to correct bugs and with advices about new features). Right now, our work is focused on:

  • Enable the DataBinding on the combo (right now the original DataBinding doesn't work)
  • Full repaint of the combo: what we do now is paint over the derived combobox, so there is a small blinking. To take care of it, we have to paint the whole combobox (including the text area and the text itself) and not call the parent painting anymore
  • Allow to use an unlimited number of columns

History

  • Version 1.0.0.0 - October 20, 2004
    • Initial Release
  • Version 1.3.0.0 - September 06, 2007
    • Minor bug fixes (Added some properties to manage colors, fixed some errors in OnKeyPress event, OnLeave event, OnKeyUp event, added the ItemSelect method)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here