This is a quick article to show how to create a hybrid
Windows.Forms.ComboBox to solve the problem where an item's text width exceeds the width of the drop-down area.
A couple of days ago, a client of mine asked me how to accomplish this task. I was initially surprised to discover that the control did not natively expose this functionality, and then more surprised at being unable to locate a simple online example.
Using the code
Simply add the code to your project, and use the
ComboBoxToolTip control in place of any drop-down
To keep things brief, the example code does not support
.DropDownStyle = ComboBoxStyle.Simple or owner drawn combo items. It should be a straightforward exercise to extend the code to support it, but if anyone needs a little help on this, please shout.
You will need to import the
Private Class ToolTipWindow
Private Declare Function SetParent Lib "user32" _
(ByVal hWndChild As IntPtr, ByVal hWndNewParent As IntPtr) As Integer
Private Declare Function ShowWindow Lib "user32" _
(ByVal hWnd As IntPtr, ByVal nCmdShow As Integer) As Integer
Private Declare Function SendMessage Lib "user32" _
(ByVal hWnd As IntPtr, ByVal Msg As Integer, _
ByVal wParam As IntPtr, ByRef lParam As IntPtr) As IntPtr
Public Sub New()
Me.BackColor = SystemColors.Info
Public Overloads Sub Show()
If (Me.Handle.Equals(IntPtr.Zero)) Then Me.CreateControl()
Dim h As IntPtr = Me.Handle
If Not Me.Visible Then SetParent(h, IntPtr.Zero)
Protected Overrides ReadOnly Property CreateParams() As _
Const WS_EX_TOOLWINDOW As Integer = &H80
Const WS_EX_NOACTIVATE As Integer = &H8000000
Const WS_EX_TOPMOST As Integer = &H8
Dim p As CreateParams = MyBase.CreateParams
p.ExStyle = p.ExStyle Or (WS_EX_NOACTIVATE Or _
WS_EX_TOOLWINDOW Or WS_EX_TOPMOST)
p.Parent = IntPtr.Zero
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
If String.IsNullOrEmpty(Me.Text) Then Return
Using brush As Brush = New SolidBrush(Me.ForeColor)
e.Graphics.DrawString(Me.Text, Me.Font, brush, Me.ClientRectangle)
e.Graphics.DrawRectangle(SystemPens.ControlDarkDark, 0, 0, _
Me.ClientRectangle.Width - 1, Me.ClientRectangle.Height - 1)
We have two classes in this project. The first one we will look at is the one that I used to replace the
Windows.Forms.ToolTip the framework provides us with.
It's all pretty selfexplanatory stuff, so I'll just quickly cover the basics. The class inherits from
Windows.Forms.Control. I did this because it was then a simple operation to describe an appropriate class of a window (
CreateParams) and paint it (
OnPaint) without calling out to native code. Advanced users can, of course, tweak the styles even more so it paints cleaner etc., but my aim here is to keep the code as simple to follow as possible.
The points of interest here are .....
- The parent window is set to
IntPtr.Zero. This means our tooltip window can 'float' because the desktop owns it. Note that we have to do this in the
Show method (
SetParent) if the window has been hidden, otherwise the z-order falls below that of the active form.
- Our tooltip is owner-drawn, which means we can paint any style of tooltip we want.
The second class is our ComboBox control.
Public NotInheritable Class ComboBoxToolTip
Private Structure RECT
<System.Runtime.InteropServices.FieldOffset(0)> Friend Left As Integer
<System.Runtime.InteropServices.FieldOffset(4)> Friend Top As Integer
<System.Runtime.InteropServices.FieldOffset(8)> Friend Right As Integer
<System.Runtime.InteropServices.FieldOffset(12)> Friend Bottom As Integer
Private Declare Function GetWindowRect Lib "user32" _
(ByVal hWnd As IntPtr, ByRef lpRect As RECT) As Integer
Private Declare Function GetScrollPos Lib "user32" _
(ByVal hWnd As IntPtr, ByVal nBar As Integer) As Integer
Private mToolTip As ToolTipWindow
Private Function PointOfInterest(ByVal pt As Drawing.Point, _
ByVal lbhWnd As IntPtr) As Boolean
Dim dropDownRect As RECT
If Not CType(GetWindowRect(lbhWnd, dropDownRect), Boolean) Then Return False
If dropDownRect.Left <= pt.X AndAlso dropDownRect.Right >= pt.X Then
If dropDownRect.Top <= pt.Y AndAlso dropDownRect.Bottom >= pt.Y Then
Private Function IndexFromPoint(ByVal Y As Integer, ByVal lbhWnd _
As IntPtr, ByRef offset As Integer) As Integer
offset = GetScrollPos(lbhWnd, 1)
Dim n As Integer = offset - 1
If Math.Sign(Y) = -1 Then
n += CType(Math.Ceiling((Me.DropDownHeight + Y) / Me.ItemHeight), Integer)
Else : n += CType(Math.Ceiling((Y - Me.ClientSize.Height) / _
If n >= 0 AndAlso n < Me.Items.Count Then Return n Else Return -1
Private Sub RefreshToolTip(ByVal lbhWnd As IntPtr)
If Me.mToolTip Is Nothing Then Me.mToolTip = New ToolTipWindow
Dim pt As Point = Control.MousePosition
If Not Me.PointOfInterest(pt, lbhWnd) Then Return
pt = Me.PointToClient(pt)
Dim offset As Integer = -1
Dim n As Integer = Me.IndexFromPoint(pt.Y, lbhWnd, offset)
If n = -1 Then Return
Dim s As String = Me.Items(n).ToString.Replace(Environment.NewLine, "X"c)
Using g As Graphics = Me.CreateGraphics
Dim size As Size = Me.ClientSize
If g.MeasureString(s, Me.Font).Width > size.Width Then
If Math.Sign(pt.Y) = 1 Then
pt = New Point(1, 1 + size.Height + (Me.ItemHeight * (n - offset)))
Else : pt = New Point(1, 1 - Me.DropDownHeight + _
(Me.ItemHeight * (n - offset)))
pt = Me.PointToScreen(pt)
Me.mToolTip.Size = g.MeasureString(s, Me.mToolTip.Font).ToSize
Me.mToolTip.Width += 1
Me.mToolTip.Text = s
Me.mToolTip.Location = pt
Else : mToolTip.Hide()
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
Const WM_CTLCOLORLISTBOX As UInt32 = &H134
If m.Msg = WM_CTLCOLORLISTBOX Then Me.RefreshToolTip(m.LParam)
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing AndAlso Me.mToolTip IsNot Nothing Then
Me.mToolTip = Nothing
Private Sub HideToolTip(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.DropDownClosed
If Me.mToolTip IsNot Nothing Then Me.mToolTip.Hide()
The hybrid essence of this class is to keep track of when the mouse pointer moves down the Y axis of the drop-down listbox control. As it does, the index of the item the mouse is over is determined, then the width of the item text is calculated. If the text width exceeds that of the drop down listbox, the tooltip is refreshed and shown. When the tooltip is no longer needed, it is simply hidden, rather than destroyed, and reused the next time.
Probably, the trickiest bit to follow is the method that calculates the index of the item under the mouse. I used the
GetScrollPos native method to consider scroll offsets, but this is only really supported on a 32 bit machine. 64 bit safe methods are available, but that unnecessarily complicated/extended the example code, hence my choice. I, then, do a little bit of manual math to calculate the final index.
I guess the only other interesting point is my choice of message ...
WM_CTLCOLORLISTBOX. This was a simple choice given that it is the message that fits most consistently with our model, and that the
lParam value of the message is actually the window handle for the drop down list box.
There are no doubt many, but here are a few that spring to my mind ...
- I based the tooltip behaviour mainly on that of the treeview control. One thing that is missing is a timer that auto-hides the tooltip. I didn't put this in because I prefer it not to hide, but it would be nice for this to be an option for the developer.
- It might be a good idea to allow the developer the option to destroy the tooltip rather than simply hide it.
- Support for right to left locales.
Points of interest
Windows.Forms.ComboBox control is simply a wrapper for the native
CComboBox uses a
CListBox for the drop-down area. My initial approach was to subclass this window using the managed
NativeWindow class. Although this worked fine, I thought it would be a little more fun to try and keep things as much as possible in .NET managed code.
- Released to CodeProject May 2007.
- Hybrid version with owner-drawn super tooltips, article posted June 2007 here.
Freelance Software Architect and Senior .NET Developer based in the UK and specialising in solutions utilising the Microsoft Technology Stack.