We recently got into a discussion where I work about the best way to explain to a user why a control is disabled.
One person argued that it made sense to leave the “disabled” controls enabled, and, when a user tried to click/use the control, pop up a messagebox explaining why it won’t work in this instance.
To me, that seemed to go against everything I’d ever learned about UI design and controls, namely, if a control is disabled, it ought to look disabled on screen and it shouldn’t do anything if you poke at it.
Still, I’d had plenty of experience myself with apps where controls were disabled and I had no idea why they were disabled, much less how to go about getting that functionality enabled. That can certainly make for a frustrating time.
Then someone suggested a tooltip or a status bar message. If you tried to click the disabled control, or hovered your mouse over it, you’d get a little, innocuous message somewhere telling you why that control was disabled.
Awesome idea!
Only one problem.
Tooltips don’t work for disabled controls. Actually, they do for menus, toolbars and likely a few others, but that’s another story.
And you don’t get MouseOver events on disabled controls.
Sigh.
Well, I couldn’t just walk away from this.
A quick google turned up a few bits:
- There was this from Roy Auchterlounie, but it’s MFC.
- But then there was this nugget from a post by Linda Lui, apparently with MS Support. It’s in C#, but it’s relatively easy to translate to VB.
The only thing was, Lui’s solution was not exactly what I’d call an encapsulated solution. As Scott Hanselman would say, dropping snippets of code like this all over my forms just doesn’t have a wonderfully fragrant code smell.
I’d messed around with Control Extenders under ASP.NET some time ago, and this seemed like the perfect excuse to try it out on a good ol’ WinForms app.
A little refactoring later, and I’ve ended up with the “Disabled Tool Tip” Extender Control. It directly inherits from the out-of-the-box tooltip in VS2008. As a result, there’s not a lot of code here. Also, it should work with VS2005, but I’m not guaranteeing as much.
Add this class to your project, recompile, then drop one onto a form.
Zip! Boom! Pow! Every control on your form should now have a “ToolTip on DisabledToolTip” property. You simply set this new property to the tooltip you want to show when the control is disabled and you’re done.
(my test rig, she is much fine, no?<g>)
A few notes about this class.
One significant issue I ran into immediately, was how do you retrieve a reference to the containing form if you’re a component sited on that form. All the obvious stuff didn’t work. There’s gotta be a more straightforward way to do it, but I failed to find it, at least with respect to a Component type control (one of those that isn’t actually sited ON the form, but rather in that little area at the bottom of the designer).
I ended up caching an instance of some control during the SetToolTip method, since this method is called during the form initialization by any control on the form that had a tooltip set for it via the designer.
Public Shadows Sub SetToolTip(ByVal control As Control, ByVal caption As String)
MyBase.SetToolTip(control, caption)
'---- if we don't have the parent form yet...
If rParentForm Is Nothing Then
'---- attempt to get it from the control
rParentForm = control.FindForm
'---- if that doesn't work
If rParentForm Is Nothing Then
'---- cache the control for use a little later
rControl = control
End If
End If
End Sub
But, you can’t use the FindForm method here, necessarily, because if the form is still being initialized, you’ll get back nothing.
So, I ended up implementing the ISupportInitialize interface, and, during the EndInit method, if I have a cached control reference, I use it at this point to retrieve the parent form via FindForm.
Public Sub EndInit() Implements ISupportInitialize.EndInit
'---- if we weren't able to retrieve the form from the control
' before, we should be able to now
If rControl IsNot Nothing Then
rParentForm = rControl.FindForm
End If
End Sub
Roundabout, yes, but it seems to work in a very stable way, and it means I don’t have to resort to MFC style subclassing and the like. I can just monitor events on the parent form via a simple WithEvents variable reference.
Anyway, the full code for the class is here. It’s short enough that I’m not going to bother with a ZIP file at this point.
And finally, as with any code you pick up off the net, I’m making no guarantees of any sort. If it works for you, great. If not. Well, I’ll certainly do my best to help if you let me know. It works for me, but it is necessarily full on, battle tested, bulletproof stuff? Uh. No.
Enjoy! If you see any improvements to be made, please share!
And please, if you post it elsewhere, give me (and Linda Lui) proper credit!
Imports System.ComponentModel
''' <summary>
''' Custom ToolTip Component that is based on a normal tooltip component but tracks tips
''' for disabled controls
''' Note that the because this is a separate extender, all the controls on a form
''' can have an "Enabled" tip (as normal) AND a "disabled" tip.
'''
''' By Darin Higgins 2008
''' Based on a code example by Linda Lui (MSFT)
'''
''' </summary>
''' <remarks></remarks>
''' <editHistory></editHistory>
Public Class DisabledToolTip
Inherits ToolTip
Implements ISupportInitialize
'---- hold onto a reference to the host form
' to monitor the mousemove
Private WithEvents rParentForm As System.Windows.Forms.Form
Private _rbActive As Boolean = True
''' <summary>
''' Active for the Disabled ToolTip has a slightly different meaning
''' than "Active" for a regular tooltip
''' </summary>
''' <value></value>
''' <remarks></remarks>
<DefaultValue(True)> _
Public Shadows Property Active() As Boolean
Get
Return _rbActive
End Get
Set(ByVal value As Boolean)
If _rbActive <> value Then
_rbActive = value
End If
End Set
End Property
'---- hold on to a control temporarily while we wait for things to
' settle
Private rControl As Control
''' <summary>
''' Shadow the settooltip function so we can intercept and save a control
''' reference. NOTE: the form MIGHT not be setup yet, so the control
''' might not know what it's parent is yet, so we cache the the first control
''' we get, and use it later, if necessary
''' </summary>
''' <param name="control"></param>
''' <param name="caption"></param>
''' <remarks></remarks>
Public Shadows Sub SetToolTip(ByVal control As Control, ByVal caption As String)
MyBase.SetToolTip(control, caption)
'---- if we don't have the parent form yet...
If rParentForm Is Nothing Then
'---- attempt to get it from the control
rParentForm = control.FindForm
'---- if that doesn't work
If rParentForm Is Nothing Then
'---- cache the control for use a little later
rControl = control
End If
End If
End Sub
Public Sub BeginInit() Implements ISupportInitialize.BeginInit
'---- Our base tooltip is disabled by default
' because we don't want to show disabled tooltips when
' a control is NOT disabled!
MyBase.Active = False
End Sub
''' <summary>
''' Supports end of initialization phase tasks for this control
''' </summary>
''' <remarks></remarks>
Public Sub EndInit() Implements ISupportInitialize.EndInit
'---- if we weren't able to retrieve the form from the control
' before, we should be able to now
If rControl IsNot Nothing Then
rParentForm = rControl.FindForm
End If
End Sub
Public Sub New(ByVal IContainer As IContainer)
MyBase.New(IContainer)
End Sub
''' <summary>
''' Monitor the MouseMove event on the host form
''' If we see it move over a disabled control
''' Check for a tooltip and show it
''' If the cursor moved off the control we're displaying
''' a tip for, hide the tip.
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
''' <remarks></remarks>
Private Sub rParentForm_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles rParentForm.MouseMove
Static ctrlWithToolTip As Control = Nothing
Dim ctrl = rParentForm.GetChildAtPoint(e.Location)
If ctrl IsNot Nothing Then
If Not ctrl.Enabled Then
If ctrlWithToolTip IsNot Nothing Then
If ctrl IsNot ctrlWithToolTip Then
'---- if we're not over the control we last showed
' a tip for, close down the tip
Me.Hide(ctrlWithToolTip)
ctrlWithToolTip = Nothing
MyBase.Active = False
End If
End If
If ctrlWithToolTip Is Nothing Then
Dim tipstring = Me.GetToolTip(ctrl)
If Len(tipstring) And Me.Active Then
'---- only enable the base tooltip if we're going to show one
MyBase.Active = True
Me.Show(tipstring, ctrl, ctrl.Width / 2, ctrl.Height / 2)
ctrlWithToolTip = ctrl
End If
End If
ElseIf ctrlWithToolTip IsNot Nothing Then
'---- if we're over an enabled control
' the tip doesn't apply anymore
Me.Hide(ctrlWithToolTip)
ctrlWithToolTip = Nothing
MyBase.Active = False
End If
ElseIf ctrlWithToolTip IsNot Nothing Then
'---- if we're not over a control at all, but we've got a
' tip showing, hide it, it's no longer applicable
Me.Hide(ctrlWithToolTip)
ctrlWithToolTip = Nothing
MyBase.Active = False
End If
End Sub
End Class