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

A Windows.Forms.ComboBox with Item ToolTips for 32 bit Windows XP (SP2)

0.00/5 (No votes)
6 Jun 2007 1  
A simple overview of how to create a hybrid Drop-Down ComboBox where a tooltip is shown over an item if it is too long to display within the ListBox rectangle.

ComboBox in action

Introduction

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.

Background

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 Windows.Forms.ComboBox.

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 System.Drawing and System.Windows.Forms namespaces.

Private Class ToolTipWindow

    Inherits Control

    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()
        MyBase.New()
        Me.BackColor = SystemColors.Info
    End Sub

    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)

        ShowWindow(h, 1)

        Me.Invalidate()

    End Sub

    Protected Overrides ReadOnly Property CreateParams() As _
                        System.Windows.Forms.CreateParams
        Get

            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

            Return p

        End Get
    End Property

    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)

        MyBase.OnPaint(e)

        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)
        End Using
      
        e.Graphics.DrawRectangle(SystemPens.ControlDarkDark, 0, 0, _
                   Me.ClientRectangle.Width - 1, Me.ClientRectangle.Height - 1)

    End Sub

End Class

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 .....

  1. 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.
  2. 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

    Inherits ComboBox

    <System.Runtime.InteropServices.StructLayout(_
               Runtime.InteropServices.LayoutKind.Explicit)> _
    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
    End Structure

    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
                Return True
            End If
        End If

        Me.mToolTip.Hide()
        Return False
        
    End Function

    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) / _
                                            Me.ItemHeight), Integer)
        End If

        If n >= 0 AndAlso n < Me.Items.Count Then Return n Else Return -1

    End Function

    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)))
                End If

                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

                Me.mToolTip.Show()

            Else : mToolTip.Hide()
            End If
        End Using

    End Sub

    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)

        Const WM_CTLCOLORLISTBOX As UInt32 = &H134

        MyBase.WndProc(m)

        If m.Msg = WM_CTLCOLORLISTBOX Then Me.RefreshToolTip(m.LParam)

    End Sub

    Protected Overrides Sub Dispose(ByVal disposing As Boolean)

        If disposing AndAlso Me.mToolTip IsNot Nothing Then
            Me.mToolTip.Dispose()
            Me.mToolTip = Nothing
        End If

        MyBase.Dispose(disposing)

    End Sub

    Private Sub HideToolTip(ByVal sender As Object, _
                ByVal e As System.EventArgs) Handles Me.DropDownClosed
        If Me.mToolTip IsNot Nothing Then Me.mToolTip.Hide()
    End Sub

End Class

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.

Possible improvements

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

The Windows.Forms.ComboBox control is simply a wrapper for the native CComboBox class. 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.

History

  • Released to CodeProject May 2007.
  • Hybrid version with owner-drawn super tooltips, article posted June 2007 here.

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