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:
Do you know how to enable these disabled controls?
Solution
There are two steps involved in solving the problem:
- 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
- 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.
-
Start Visual Studio and create a new Windows Forms application.
-
Create a class whose name is TransparentSheet
and add the following code:
Imports System.Security.Permissions
Public Class TransparentSheet
Inherits ContainerControl
Public Sub New()
SetStyle(ControlStyles.Opaque, True)
UpdateStyles()
AutoScaleMode = Windows.Forms.AutoScaleMode.None
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
- 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
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.
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
.
- Select <Project/Add Reference…> menu.
- Select
System.Design
under the .NET tab.
- Click OK.
- Create a class whose name is
EnhancedToolTip
and add the following code:
Imports System.ComponentModel
Imports System.ComponentModel.Design
Imports System.Drawing.Design
<ProvideProperty("ToolTipWhenDisabled", GetType(Control))> _
<ProvideProperty("SizeOfToolTipWhenDisabled", GetType(Control))> _
Public Class EnhancedToolTip
Inherits ToolTip
#Region " Required constructor "
Public Sub New(ByVal container As System.ComponentModel.IContainer)
MyBase.New()
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
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)
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
Else
TakeOffTransparentSheet(control)
End If
End Sub
Private Sub ShowToolTipWhenDisabled(ByVal control As Control)
If TypeOf control Is Form Then
Else
If control.Parent.Enabled Then
PutOnTransparentSheet(control)
Else
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
<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.
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:
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
.
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:
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.
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.
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:
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:
Me.EnhancedToolTip1.SetToolTipWhenDisabled(Me.CheckBox1, "")
Importing System.Drawing.Design
namespace is for the UITypeEditor
.
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.
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
.
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.
Private Sub PutOnTransparentSheet()
This function does the following:
- Instantiates a
TransparentSheet
control.
- Matches its location to that of the control that has been disabled.
- Unless the
SizeOfToolTipWhenDisabled
is explicitly specified, matches its size to that of the control that has been disabled.
- Adds the
TransparentSheet
on the form so that it gets displayed (even if it is transparent).
- Makes sure the displayed transparent sheet is on top of the Z-order.
- Saves it in a
Dictionary
.
- Calls
SetToolTip()
for the transparent sheet with the tooltip message.
Private Sub TakeOffTransparentSheet()
This function does the following:
- Make sure the
Dictionary
contains the key.
- Remove the transparent sheet from the form.
- Remove it from the base class.
- Dispose it.
- Remove the control from the
Dictionary
.
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.
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:
Me.EnhancedToolTip1.SetSizeOfToolTipWhenDisabled_
(Me.CheckBox1, New System.Drawing.Size(0, 0))
Public Shadows Property ToolTipTitle()
This intercepts the built-in
ToolTipTitle
property and saves it privately in
m_SavedToolTipTitle
.
Public Shadows Property ToolTipIcon()
This intercepts the built-in ToolTipIcon
property and saves it privately in m_SavedToolTipIcon
.
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.
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:
- When a
ToolTipWhenDisabled
text is assigned to a class that derives from Form
, the software crashes with an ArgumentNullException
.
- 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