Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / Windows Forms

Displaying a ToolTip when the Mouse Hovers Over a Disabled Control

Rate me:
Please Sign up or sign in to vote.
4.89/5 (26 votes)
23 Dec 2009CPOL9 min read 96.8K   1.7K   70   13
This article explains how to display a tooltip when the mouse hovers over a disabled control
ToolTipWhenDisabled1.jpg

Introduction

Have you ever wondered why some feature is disabled on your favorite software? Is it because I didn't buy the professional version, or I didn't select the feature when I installed the software? How can I enable it? I really want to use it! Questions go on and on and often times you give up. Even if the software comes with a good manual or help system, it is sometimes hard to find the answers to these questions. Tooltips over the disabled visual controls would provide a perfect solution to the problem from the user's point of view. Alas, the built-in ToolTip for Windows Forms applications can't show tooltip messages when the associated control is disabled.

Background

There's a dispute among software developers whether the User Interface should display any disabled visual controls on the screen at all, but chances are we see them from time to time, even from Microsoft as shown below:

ToolTipWhenDisabled2.jpg

Do you know how to enable these disabled controls?

Solution

There are two steps involved in solving the problem:

  1. Create a transparent sheet control that can be used to cover the disabled control(s) at run time and to provide a tooltip from it, and 
  2. Extend the ToolTip class with an extender property named ToolTipWhenDisabled and embed the logic of attaching and detaching the transparent sheet, triggered by the associated control's EnabledChanged event.

Creating TransparentSheet Control

One way to implement a transparent sheet is to inherit from the ContainerControl class.

  1. Start Visual Studio and create a new Windows Forms application.

  2. Create a class whose name is TransparentSheet and add the following code:

    VB
    Imports System.Security.Permissions
    
    Public Class TransparentSheet
        Inherits ContainerControl
    
        Public Sub New()
            'Disable painting the background.
            SetStyle(ControlStyles.Opaque, True)
            UpdateStyles()
    
            'Make sure to set the AutoScaleMode property to None 
            'so that the location and size property don't automatically change 
            'when placed in a form that has different font than this.
            AutoScaleMode = Windows.Forms.AutoScaleMode.None
    
            'Tab stop on a transparent sheet makes no sense.
            TabStop = False
        End Sub
    
        Private Const WS_EX_TRANSPARENT As Short = &H20
        Protected Overrides ReadOnly Property CreateParams() _
    		As System.Windows.Forms.CreateParams
            <SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode:=True)> _
            Get
                Dim cp = MyBase.CreateParams
                cp.ExStyle = cp.ExStyle Or WS_EX_TRANSPARENT
                Return cp
            End Get
        End Property
    End Class
  3. Now, build the application. Visual Studio will put the control in the Toolbox so that you can use it just like any other visual control on the form.

Using the Code

VB
Public Sub New() 

We need to set the ControlStyles.Opaque flag and call UpdateStyles() in order to suppress background painting.

Then, we need to change the AutoScaleMode to None. Otherwise, the Location and Size properties will change, when we dynamically instantiate a TransparentSheet and put it on a form that has a different Font. Since we need to put the TransparentSheet at exactly the same location with the same size as the disabled control, this is critical.

Lastly, we disable TabStop. Otherwise, the tab will stop at the transparent sheet, which makes no sense.

VB
Protected Overrides ReadOnly Property CreateParams()

This is to override the base class's CreateParams property with the secret ingredient – WS_EX_TRANSPARENT. FxCop will complain if we don't apply SecurityPermission to the Get function. We also need to import System.Security.Permissions to apply the attribute.

You can use this class as is to provide a transparent sheet on any form. It is especially useful at design time when you want to measure the size of some rectangular area where you intend to disable all the controls inside and to provide a single tooltip message for the disabled area.

Extending the ToolTip Class

Now, the fun part. We want the ToolTip class to use the TransparentSheet when the associated control is disabled. Of course, the built-in ToolTip class can't do it, but we can do so by inheriting the ToolTip.

  1. Select <Project/Add Reference…> menu.
  2. Select System.Design under the .NET tab.
  3. Click OK.
  4. Create a class whose name is EnhancedToolTip and add the following code:
VB
Imports System.ComponentModel
Imports System.ComponentModel.Design
Imports System.Drawing.Design

''' <summary>
''' EnhancedToolTip supports the ToolTipWhenDisabled and SizeOfToolTipWhenDisabled
''' extender properties that can be used to show tooltip messages when the associated
''' control is disabled.
''' </summary>
''' <remarks>
''' EnhancedToolTip does not work with the Form and its derived classes.
''' </remarks>
<ProvideProperty("ToolTipWhenDisabled", GetType(Control))> _
<ProvideProperty("SizeOfToolTipWhenDisabled", GetType(Control))> _
Public Class EnhancedToolTip
    Inherits ToolTip

#Region " Required constructor "
    'This constructor is required for the Windows Forms Designer to instantiate
    'an object of this class with New(Me.components).
    'To verify this, just remove this constructor. Build it and then put the
    'component on a form. Take a look at the Designer.vb file for InitializeComponents(),
    'and search for the line where it instantiates this class.
    Public Sub New(ByVal container As System.ComponentModel.IContainer)
        MyBase.New()

        'Required for Windows.Forms Class Composition Designer support
        If (container IsNot Nothing) Then
            container.Add(Me)
        End If

    End Sub
#End Region

#Region " ToolTipWhenDisabled extender property support "
    Private m_ToolTipWhenDisabled As New Dictionary(Of Control, String)
    Private m_TransparentSheet As New Dictionary(Of Control, TransparentSheet)

    Public Sub SetToolTipWhenDisabled(ByVal control As Control, ByVal caption As String)
        If control Is Nothing Then
            Throw New ArgumentNullException("control")
        End If

        If Not String.IsNullOrEmpty(caption) Then
            m_ToolTipWhenDisabled(control) = caption
            If Not control.Enabled Then
                'When the control is disabled at design time, the EnabledChanged
                'event won't fire. So, on the first Paint event, we should call
                'ShowToolTipWhenDisabled().
                AddHandler control.Paint, AddressOf DisabledControl_Paint
            End If
            AddHandler control.EnabledChanged, AddressOf Control_EnabledChanged
        Else
            m_ToolTipWhenDisabled.Remove(control)
            RemoveHandler control.EnabledChanged, AddressOf Control_EnabledChanged
        End If
    End Sub

    Private Sub DisabledControl_Paint(ByVal sender As Object, ByVal e As EventArgs)
        Dim control = CType(sender, Control)
        ShowToolTipWhenDisabled(control)
        'Immediately remove the handler because we don't need it any longer.
        RemoveHandler control.Paint, AddressOf DisabledControl_Paint
    End Sub

    <Category("Misc")> _
    <Description("Determines the ToolTip shown when the mouse hovers over _
	the disabled control.")> _
    <Localizable(True)> _
    <Editor(GetType(MultilineStringEditor), GetType(UITypeEditor))> _
    <DefaultValue("")> _
    Public Function GetToolTipWhenDisabled(ByVal control As Control) As String
        If control Is Nothing Then
            Throw New ArgumentNullException("control")
        End If

        If m_ToolTipWhenDisabled.ContainsKey(control) Then
            Return m_ToolTipWhenDisabled(control)
        Else
            Return ""
        End If
    End Function

    Private Sub Control_EnabledChanged(ByVal sender As Object, ByVal e As EventArgs)
        Dim control = CType(sender, Control)
        If control.Enabled Then
            ShowToolTip(control)
        Else
            ShowToolTipWhenDisabled(control)
        End If
    End Sub

    Private Sub ShowToolTip(ByVal control As Control)
        If TypeOf control Is Form Then
            'We don't support ToolTipWhenDisabled for the Form class.
        Else
            TakeOffTransparentSheet(control)
        End If
    End Sub

    Private Sub ShowToolTipWhenDisabled(ByVal control As Control)
        If TypeOf control Is Form Then
            'We don't support ToolTipWhenDisabled for the Form class.
        Else
            If control.Parent.Enabled Then
                PutOnTransparentSheet(control)
            Else
                'If the parent control is disabled, we can't show the
                'ToolTipWhenDisabled. So, do not call PutOnTransparentSheet(),
                'otherwise, Control_EnabledChanged() event on this control
                'will be repeatedly fired because of ts.BringToFront() in
                'PutOnTransparentSheet().
            End If
        End If
    End Sub

    Private Sub PutOnTransparentSheet(ByVal control As Control)
        Dim ts As New TransparentSheet
        ts.Location = control.Location
        If m_SizeOfToolTipWhenDisabled.ContainsKey(control) Then
            ts.Size = m_SizeOfToolTipWhenDisabled(control)
        Else
            ts.Size = control.Size
        End If
        control.Parent.Controls.Add(ts)
        ts.BringToFront()
        m_TransparentSheet(control) = ts
        SetToolTip(ts, m_ToolTipWhenDisabled(control))
    End Sub

    Private Sub TakeOffTransparentSheet(ByVal control As Control)
        If m_TransparentSheet.ContainsKey(control) Then
            Dim ts = m_TransparentSheet(control)
            control.Parent.Controls.Remove(ts)
            SetToolTip(ts, "")
            ts.Dispose()
            m_TransparentSheet.Remove(control)
        End If
    End Sub
#End Region

#Region " Support for the oversized transparent sheet to cover _
 multiple visual controls. "
    Private m_SizeOfToolTipWhenDisabled As New Dictionary(Of Control, Size)

    Public Sub SetSizeOfToolTipWhenDisabled_
	(ByVal control As Control, ByVal value As Size)
        If control Is Nothing Then
            Throw New ArgumentNullException("control")
        End If

        If Not value.IsEmpty Then
            m_SizeOfToolTipWhenDisabled(control) = value
        Else
            m_SizeOfToolTipWhenDisabled.Remove(control)
        End If
    End Sub

    <Category("Misc")> _
    <Description("Determines the size of the ToolTip when the control is disabled." & _
                 " Leave it to 0,0, unless you want the ToolTip to pop up over wider" & _
                 " rectangular area than this control.")> _
    <DefaultValue(GetType(Size), "0,0")> _
    Public Function GetSizeOfToolTipWhenDisabled(ByVal control As Control) As Size
        If control Is Nothing Then
            Throw New ArgumentNullException("control")
        End If

        If m_SizeOfToolTipWhenDisabled.ContainsKey(control) Then
            Return m_SizeOfToolTipWhenDisabled(control)
        Else
            Return Size.Empty
        End If
    End Function
#End Region

#Region " Comment out this region if you are okay with the same Title/Icon _
	for disabled controls. "
    Private m_SavedToolTipTitle As String
    Public Shadows Property ToolTipTitle() As String
        Get
            Return MyBase.ToolTipTitle
        End Get
        Set(ByVal value As String)
            MyBase.ToolTipTitle = value
            m_SavedToolTipTitle = value
        End Set
    End Property

    Private m_SavedToolTipIcon As ToolTipIcon
    Public Shadows Property ToolTipIcon() As System.Windows.Forms.ToolTipIcon
        Get
            Return MyBase.ToolTipIcon
        End Get
        Set(ByVal value As System.Windows.Forms.ToolTipIcon)
            MyBase.ToolTipIcon = value
            m_SavedToolTipIcon = value
        End Set
    End Property

    Private Sub EnhancedToolTip_Popup(ByVal sender As Object, _
	ByVal e As System.Windows.Forms.PopupEventArgs) Handles Me.Popup
        If TypeOf e.AssociatedControl Is TransparentSheet Then
            MyBase.ToolTipTitle = ""
            MyBase.ToolTipIcon = Windows.Forms.ToolTipIcon.None
        Else
            MyBase.ToolTipTitle = m_SavedToolTipTitle
            MyBase.ToolTipIcon = m_SavedToolTipIcon
        End If
    End Sub
#End Region
End Class

Using the Code

VB
<ProvideProperty("ToolTipWhenDisabled", GetType(Control))> _
<ProvideProperty("SizeOfToolTipWhenDisabled", GetType(Control))> _

These two attributes tell the Windows Forms Designer that the class provides extender properties called "ToolTipWhenDisabled" and "SizeOfToolTipWhenDisabled" that every control that derives from the Control class should be decorated with these new properties.

VB
Public Sub New(ByVal container As System.ComponentModel.IContainer)

This constructor is necessary to let the Windows Forms Designer instantiate this class with this overload as shown below:

VB
Me.EnhancedToolTip1 = New WindowsApplication1.EnhancedlToolTip(Me.components)

If we omit this constructor, Visual Studio will use the default constructor to instantiate, which we want to avoid because we want the Form class to dispose EnhancedToolTip1 when the form is disposed, just like it does when it instantiates the built-in ToolTip.

VB
Public Sub SetToolTipWhenDisabled()

Windows Forms Designer places a call to this function in InitializeComponent() when you provide some text to the "ToolTipWhenDisabled on EnhancedToolTip" property in the Properties pane as shown below:

VB
Me.EnhancedToolTip1.SetToolTipWhenDisabled(Me.CheckBox1, "CheckBox is Disabled")

We make sure that the passed control is not Nothing. Then if the caption isn't an empty string, we save the passed string in a Dictionary and add the control.EnabledChanged event handler.

If the passed control is already disabled at design time, we need to change the tooltip provider to a transparent sheet, rather than the disabled control. To do that, we hook the control.Paint event.

If the value is an empty string, we remove it from the Dictionary and unhook from the control.EnabledChanged event.

VB
Private Sub DisabledControl_Paint()

This function changes the tooltip provider to a transparent sheet, at the first occurrence of the control.Paint event. Once we change the provider, we immediately unhook from the event.

VB
Public Function GetToolTipWhenDisabled()

Whenever we provide a SetXxx() function, we have to also provide a GetXxx() function in order for the extender property to work. This extender property will be available in the Properties pane as shown below:

ToolTipWhenDisabled3.jpg

Again, we check if the passed control is not Nothing and if our Dictionary already contains the control, we return the string. Otherwise we return an empty string.

By adding Localizable(True) attribute, we can easily provide messages in different languages at design time. MultilineStringEditor in System.ComponentModel.Design namespace allows us to supply a multiline string for the property in the Properties pane. This allows us to enter the desired text with decent formatting in a WYSIWYG way (in these days we don't hear this term much). DefaultValue("") prevents the Windows Forms Designer from inserting the following code in the InitializeComponent() when we don't need a tooltip for the disabled control:

VB
Me.EnhancedToolTip1.SetToolTipWhenDisabled(Me.CheckBox1, "")

Importing System.Drawing.Design namespace is for the UITypeEditor.

VB
Private Sub Control_EnabledChanged()

This event is raised whenever the associated control's Enabled property is changed at run time. We call the appropriate functions based on the property value.

VB
Private Sub ShowToolTip(ByVal control As Control)

One of the problems of using the TransparentSheet to provide a tooltip when the associated control is disabled is that it can't be used when the associated control is a class that is derived from Form. Even if the property ToolTipWhenDisabled appears for a Form in the Property pane and you can actually type in a text on it, we need to ignore it at run time. Otherwise, an ArgumentNullException would be thrown when control.Parent is accessed in PutOnTransparentSheet()or TakeOffTransparentSheet()because  Form's Parent is of course Nothing.

VB
Private Sub ShowToolTipWhenDisabled(ByVal control As Control)

Another problem is that we can't use this scheme either, if the associated control is placed on a container control such as a Form or Panel and the container is disabled. If you dynamically put any visual control on a disabled container and try to show it by using control.BringToFront() at its control.EnabledChanged event, the event will repetitively be fired by the Framework and you have no control to stop it.

VB
Private Sub PutOnTransparentSheet()

This function does the following:

  1. Instantiates a TransparentSheet control.
  2. Matches its location to that of the control that has been disabled.
  3. Unless the SizeOfToolTipWhenDisabled is explicitly specified, matches its size to that of the control that has been disabled.
  4. Adds the TransparentSheet on the form so that it gets displayed (even if it is transparent).
  5. Makes sure the displayed transparent sheet is on top of the Z-order.
  6. Saves it in a Dictionary.
  7. Calls SetToolTip() for the transparent sheet with the tooltip message.
VB
Private Sub TakeOffTransparentSheet()

This function does the following:

  1. Make sure the Dictionary contains the key.
  2. Remove the transparent sheet from the form.
  3. Remove it from the base class.
  4. Dispose it.
  5. Remove the control from the Dictionary.
VB
Public Sub SetSizeOfToolTipWhenDisabled()

This sets the extender property "SizeOfToolTipWhenDisabled". You change it from default only when you want an oversized transparent sheet so that it can cover not just one but multiple visual controls. Otherwise, leave it to 0,0. In the demo example, I set this for the RadioButton1 so that the entire region gets covered by a single transparent sheet. I put a TransparentSheet on the form at design time to measure the required size, for just convenience.

VB
Public Function GetSizeOfToolTipWhenDisabled()

This gets the extender property "SizeOfToolTipWhenDisabled". DefaultValue(GetType(Size), "0,0") prevents the Windows Forms Designer from inserting the following code in the InitializeComponent() when we don't need an oversized tooltip for the disabled control:

VB
Me.EnhancedToolTip1.SetSizeOfToolTipWhenDisabled_
	(Me.CheckBox1, New System.Drawing.Size(0, 0))
VB
Public Shadows Property ToolTipTitle()
This intercepts the built-in ToolTipTitle property and saves it privately in m_SavedToolTipTitle.

VB
Public Shadows Property ToolTipIcon()

This intercepts the built-in ToolTipIcon property and saves it privately in m_SavedToolTipIcon.

VB
Private Sub EnhancedToolTip_Popup()

This event handler function allows us to change the ToolTipTitle and ToolTipIcon just before the ToolTip pops up. I chose to show no title and icon for the disabled control, i.e., when the associated control of the event is a TransparentSheet. Make sure to call the base class's properties, rather than the shadowing, new ones.

If you want to show the same title and icon as those of the enabled controls, remove this event handler and the two shadowing properties.

UML Diagram

For those who want to see some diagram, here's the UML class diagram.

ToolTipWhenDisabled4.jpg

Thinking about the inheritance hierarchy often pays. For example, you can create an equivalent TransparentSheet by deriving from, say the UserControl class, rather than ContainerControl because UserControl is one of the classes that derives from ContainerControl, just like the Form class. However, we don't need their added capabilities for the TransparentSheet, on which we put nothing.

Conclusion

While you can put most of the EnhancedToolTip class code inside a Form class and just use the built-in ToolTip and some TransparentSheets to obtain the same results, don't do it. Don't clutter (already cluttered) form class. Refactor and move each of the functionality to the most appropriate class. Even if we don't own the source code for the ToolTip class, OOP allows us to enhance it by adding new features or modifying the existing ones by overriding and shadowing.

Last but not least, please make sure that you include why the feature is disabled and/or how the user can enable the feature in the tooltip message for the disabled controls on your User Interface. No one wants the trouble of hitting the F1 key, waiting for the Help screen coming up, and digging the help hierarchy for the hard-to-find-answer. Just hovering the mouse over the area should give the user enough information.

History

  • 12/22/2009: Added workarounds for the following two problems:
  1. When a ToolTipWhenDisabled text is assigned to a class that derives from Form, the software crashes with an ArgumentNullException.
  2. When a ToolTipWhenDisabled text is assigned to a visual control and the control's container control such as Form or Panel is disabled, the software falls in an infinite loop.
  • 12/24/2008: Initial version

License

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


Written By
Software Developer (Senior)
Japan Japan
He started his career as a PDP-11 assembly language programmer in downtown Tokyo, learning what "patience" in real life means by punching a 110 baud ASR-33 Teletype frantically. He used to be able to put in the absolute loader sequence through the switch panel without consulting the DEC programming card.

Since then, his computer language experiences include 8051 assembly, FOCAL, BASIC, FORTRAN-IV, Turbo/MS C, VB. VB.NET, and C#.

Now, he lives with his wife, two grown-up kids (get out of my place!), and two cats in Westerville, Ohio.

Comments and Discussions

 
GeneralHere is a full C# translation for those who might be interested [modified] Pin
AxelDG22-Oct-09 5:55
AxelDG22-Oct-09 5:55 
GeneralRe: Here is a full C# translation for those who might be interested [modified] Pin
AxelDG26-Oct-09 1:09
AxelDG26-Oct-09 1:09 
GeneralRe: Here is a full C# translation for those who might be interested Pin
tetsushmz26-Oct-09 11:23
tetsushmz26-Oct-09 11:23 
GeneralRe: Here is a full C# translation for those who might be interested Pin
tetsushmz23-Dec-09 7:57
tetsushmz23-Dec-09 7:57 
GeneralRe: Here is a full C# translation for those who might be interested Pin
ryowu25-Jan-10 19:19
professionalryowu25-Jan-10 19:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.