I see you've already gotten the HighCore treatment about how easy everything is in WPF and how much WinForms sucks. But you might be interested to know that you can do this in WinForms, too. You just do it a little bit differently. It should come as no surprise that the standard design idioms differ in WinForms and WPF; that doesn't justify one being "better" than the other, it just means you need to learn how to use the one you're using. (Although, admittedly, some of the fancier stuff is a bit more difficult to achieve using a UI framework that was invented 20 years ago with Windows itself. The power it does have is rather remarkable.)
There are two basic ways of formatting the information: everything on a single line (which I believe is what you asked for in the question) or the pieces of information on two lines where each item is basically a two-line unit (which is what HighCore's WPF solution demonstrates).
Single-Line Format
The Simplistic Approach
We'll look at putting everything on a single line first, which really is simple. You don't need columns for separation, you can just use some kind of distinctive separator character when you add the items to the combobox, such as a vertical pipe (|
) or a dash (-
) like you used in the question.
This works so well because the ComboBox.Items.Add
method accepts a parameter of type Object
, on which it just calls ToString
to get the value displayed in the control. If you pass it a string, it displays that string.
myComboBox.BeginUpdate()
For Each record In myRecordSet
myComboBox.Items.Add(String.Format("{0} | {1}", record.UniqueID, record.Name))
' or even...
myComboBox.Items.Add(String.Format("{0} ({1})", record.UniqueID, record.Name))
Next record
myComboBox.EndUpdate()
OR
An Incremental Improvement Through OOP
You can even pass a custom class to the Add
method that keeps track of the unique ID and name properties (and anything else you want) and overrides the ToString
method for display purposes.
Public Class Record
Public Property UniqueID As Long ' maybe this should be a string too
Public Property Name As String
Public Overrides Function ToString() As String
' Generate the string that will be displayed in the combobox for this
' record, just like we did above when adding it directly to the combobox,
' except that in this case, it will be dynamically generated on the fly,
' allowing you to also track state information along with each item.
Return String.Format("{0} | {1}", Me.UniqueID, Me.Name)
End Function
End Class
' ...
' (somewhere else, when you add the items to the combobox:)
myComboBox.BeginUpdate()
For Each r In myRecordSet
' Create a Record object representing this item, and set its properties.
Dim newRecord As New Record
newRecord.UniqueID = r.UniqueID
newRecord.Name = r.Name
' ...etc.
' Then, add that object to the combobox.
myComboBox.Items.Add(newRecord)
Next r
myComboBox.EndUpdate()
Fixing the Jaggies
Granted, if the first item in each set can be of variable length and you're using a variable-width font (i.e., one that is not monospaced like every UI on the planet does except code editors), the separators won't line up and you won't get two nicely-formatted columns. Instead, it looks all jumbled and ugly.
It would be nice of the ComboBox control supported tab characters that would handle lining everything up for us automatically, but unfortunately it does not. This is, regrettably, a hard limitation of the underlying Win32 control.
Fixing this ragged-edge problem is possible, but it does get a bit complicated. It requires taking over the drawing of the items in the combobox, referred to as "owner-draw".
To do this, you set its DrawMode
property to OwnerDrawFixed
and handle the DrawItem
event to manually draw the text. You'll use the TextRenderer.DrawText
method to draw the caption string (because that matches what WinForms uses internally; avoid using Graphics.DrawString
), and TextRenderer.MeasureText
if necessary to get the spacing right. The drawing code can (and should) use all of the default properties provided by the DrawItemEventArgs
passed as e
. You don't need OwnerDrawVariable
mode or to handle the MeasureItem
event because the width and height of each item cannot vary in this case.
Just to give you an idea, here's a quick-and-dirty implementation that simply divides the drop-down in half vertically:
Private Sub myComboBox_DrawItem(sender As Object, e As DrawItemEventArgs) Handles myComboBox.DrawItem
' Fill the background.
e.DrawBackground()
' Extract the Record object corresponding to the combobox item to be drawn.
Dim record As Record = DirectCast(myComboBox.Items(e.Index), Record)
Dim id As String = record.UniqueID.ToString()
Dim name As String = record.Name
' Calculate important positions based on the area of the drop-down box.
Dim xLeft As Integer = e.Bounds.Location.X
Dim xRight As Integer = xLeft + e.Bounds.Width
Dim xMid As Integer = (xRight - xLeft) / 2
Dim yTop As Integer = e.Bounds.Location.Y
Dim yBottom As Integer = yTop + e.Bounds.Height
' Draw the first (Unique ID) string in the first half.
TextRenderer.DrawText(e.Graphics, id, e.Font, New Point(xLeft, yTop), e.ForeColor)
' Draw the column separator line right down the middle.
e.Graphics.DrawLine(SystemPens.ButtonFace, xMid, yTop, xMid, yBottom)
' Draw the second (Name) string in the second half, adding a bit of padding.
TextRenderer.DrawText(e.Graphics, name, e.Font, New Point(xMid + 5, yTop), e.ForeColor, TextFormatFlags.Left)
' Finally, draw the focus rectangle.
e.DrawFocusRectangle()
End Sub
Now, this is looking pretty good. You can certainly improve on the technique used by the DrawItem
event handler method, but it works out pretty well as is, so long as the combobox is made the right size for the values it will be displaying.
Multiple-Line Format
Defining a Custom ComboBox Class
The second method, where each item is a two-line group like HighCore's WPF example, is best done by subclassing the built-in ComboBox control and taking complete control its drawing routines. But that's nothing to be afraid of, subclassing a control is a standard WinForms idiom to gain extra control over the UI. (You could, of course, implement all of this by handling events like I did above, but I think subclassing is a much cleaner approach and also promotes reuse if you want to have multiple comboboxes that all behave in a similar fashion.)
Again, you don't need OwnerDrawVariable
because the height of the items is not going to change. You'll always have two lines, so a fixed height works fine. You just need to make sure that you set the ItemHeight
property to double of its normal value because you're going to have two lines. You could do this the complicated way using TextRenderer.MeasureText
, or you could do it the easy way by just multiplying the default value by 2. I chose the latter for this demo.
Add this class into your project, and then use the MultiLineComboBox
control instead of the built-in System.Windows.Forms.ComboBox
. All of the properties and methods work the same.
Public Class MultiLineComboBox : Inherits ComboBox
Public Sub New()
' Call the base class.
MyBase.New()
' Typing a value into this combobox won't make sense, so make it impossible.
Me.DropDownStyle = ComboBoxStyle.DropDownList
' Set the height of each item to be twice its normal value
' (because we have two lines instead of one).
Me.ItemHeight *= 2
End Sub
Protected Overrides Sub OnDrawItem(e As DrawItemEventArgs)
' Call the base class.
MyBase.OnDrawItem(e)
' Fill the background.
e.DrawBackground()
' Extract the Record object corresponding to the combobox item to be drawn.
If (e.Index >= 0) Then
Dim record As Record = DirectCast(Me.Items(e.Index), Record)
' Format the item's caption string.
Dim caption As String = String.Format("ID: {0}{1}Name: {2}", record.UniqueID.ToString(), Environment.NewLine, record.Name)
' And then draw that string, left-aligned and vertically centered.
TextRenderer.DrawText(e.Graphics, caption, e.Font, e.Bounds, e.ForeColor, TextFormatFlags.Left Or TextFormatFlags.VerticalCenter)
End If
' Finally, draw the focus rectangle.
e.DrawFocusRectangle()
End Sub
End Class
Adding Fancies and Flourishes
What we've got now isn't bad, but by lavishing a bit more effort on the drawing code in OnDrawItem
, we can add some extra visual fancies and flourishes.
For example, without the selection rectangle, it would be pretty hard to tell that these are actually two-line units. That's unusual for a combobox control, so for usability reasons your application should go out of its way to make this abundantly clear. One way we might do that is by indenting the second line. You'll recall that I said that the built-in combobox control doesn't support tabs? Well that doesn't apply anymore, since we're doing the drawing ourselves now. We can emulate tabs by adding some extra padding to the beginning of the second line.
If you wanted the labels ("ID:" and "Name:") to be set apart from the actual values, you could do that, too. Perhaps you'd make the labels bold and lighten the text color.
So you see that just by playing with the drawing code, you can create almost any effect you want. We have complete control, and by wrapping it all up in a MultiLineComboBox
class that can be reused all over the place, the rest of your code doesn't even have to know that anything special is happening. Cool, right?
Avoiding All the Work
And finally, I would be remiss if I didn't point out that you could skip doing all of this work and take your pick of the variety of custom multi-line combobox controls that have already been written.
This one is pre