鼠标悬停在禁用控件上时显示工具提示
本文介绍了鼠标悬停在禁用控件上时显示工具提示的方法。

引言
你是否曾经想过为什么在你喜欢的软件中某些功能是禁用的?是因为我没有购买专业版,还是安装软件时没有选择该功能?我该如何启用它?我真的很想用它!疑问层出不穷,很多时候你都会放弃。即使软件带有很好的手册或帮助系统,有时也很难找到这些问题的答案。从用户的角度来看,禁用控件上的工具提示可以完美地解决这个问题。但是,Windows Forms 应用程序内置的 ToolTip 在关联控件被禁用时无法显示工具提示消息。
背景
软件开发者之间对于用户界面是否应该显示任何禁用的视觉控件存在争议,但我们时不时地会看到它们,甚至来自微软,如下所示。

你知道如何启用这些被禁用的控件吗?
解决方案
解决这个问题需要两个步骤:
- 创建一个透明的 Sheet 控件,可以在运行时覆盖被禁用的控件,并从中提供工具提示;
- 通过一个名为
ToolTipWhenDisabled
的 Extender 属性来扩展ToolTip
类,并嵌入附加和分离透明 Sheet 的逻辑,该逻辑由关联控件的EnabledChanged
事件触发。
创建 TransparentSheet 控件
实现透明 Sheet 的一种方法是继承 ContainerControl
类。
-
启动 Visual Studio 并创建一个新的 Windows Forms 应用程序。
-
创建一个名为
TransparentSheet
的类,并添加以下代码: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
- 现在,构建应用程序。Visual Studio 会将该控件添加到工具箱中,您可以像使用其他任何视觉控件一样在窗体上使用它。
Using the Code
Public Sub New()
我们需要设置 ControlStyles.Opaque
标志并调用 UpdateStyles()
以抑制背景绘制。
然后,我们需要将 AutoScaleMode
更改为 None
。否则,当我们动态实例化 TransparentSheet
并将其放置在具有不同 Font
的窗体上时,Location
和 Size
属性将发生变化。由于我们需要将 TransparentSheet
放置在与禁用控件完全相同的位置和大小,因此这一点至关重要。
最后,我们禁用 TabStop
。否则,Tab 键会停留在透明 Sheet 上,这是没有意义的。
Protected Overrides ReadOnly Property CreateParams()
这是通过重写基类的 CreateParams
属性来实现的,其中包含秘密武器——WS_EX_TRANSPARENT
。如果我们不对 Get
函数应用 SecurityPermission
,FxCop 会报错。我们还需要导入 System.Security.Permissions
来应用该属性。
您可以按原样使用此类来为任何窗体提供透明 Sheet。它在设计时特别有用,当您想要测量某个矩形区域的大小,您打算禁用该区域内的所有控件,并为该禁用区域提供单个工具提示消息时。
扩展 ToolTip 类
现在,有趣的部分来了。我们希望 ToolTip
类在关联控件被禁用时使用 TransparentSheet
。当然,内置的 ToolTip
类无法做到这一点,但我们可以通过继承 ToolTip
来实现。
- 选择“项目/添加引用…”菜单。
- 在“.NET”选项卡下选择
System.Design
。 - 点击“确定”。
- 创建一个名为
EnhancedToolTip
的类,并添加以下代码:
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
<ProvideProperty("ToolTipWhenDisabled", GetType(Control))> _
<ProvideProperty("SizeOfToolTipWhenDisabled", GetType(Control))> _
这两个属性告诉 Windows Forms 设计器,该类提供了名为“ToolTipWhenDisabled
”和“SizeOfToolTipWhenDisabled
”的 Extender 属性,所有派生自 Control
类的控件都应带有这些新属性。
Public Sub New(ByVal container As System.ComponentModel.IContainer)
此构造函数是必需的,以便 Windows Forms 设计器可以使用此重载来实例化此类,如下所示:
Me.EnhancedToolTip1 = New WindowsApplication1.EnhancedlToolTip(Me.components)
如果我们省略此构造函数,Visual Studio 将使用默认构造函数进行实例化,而我们希望避免这种情况,因为我们希望 Form
类在窗体被释放时释放 EnhancedToolTip1
,就像实例化内置 ToolTip
时一样。
Public Sub SetToolTipWhenDisabled()
当您在属性窗格中为“EnhancedToolTip
上的 ToolTipWhenDisabled
”属性提供文本时,Windows Forms 设计器会在 InitializeComponent()
中调用此函数,如下所示:
Me.EnhancedToolTip1.SetToolTipWhenDisabled(Me.CheckBox1, "CheckBox is Disabled")
我们确保传入的控件不为 Nothing。然后,如果标题不是空的 string
,我们将传入的 string
保存到 Dictionary
中,并添加 control.EnabledChanged
事件处理程序。
如果控件在设计时已经被禁用,我们需要将工具提示提供程序更改为透明 Sheet,而不是禁用控件。为此,我们挂钩 control.Paint
事件。
如果值是空的 string
,我们将其从 Dictionary
中删除,并取消挂钩 control.EnabledChanged
事件。
Private Sub DisabledControl_Paint()
此函数在第一次发生 control.Paint
事件时将工具提示提供程序更改为透明 Sheet。一旦我们更改了提供程序,就立即取消挂钩事件。
Public Function GetToolTipWhenDisabled()
每当我们提供一个 SetXxx()
函数时,都必须提供一个 GetXxx()
函数,以便 Extender 属性能够正常工作。此 Extender 属性将在属性窗格中可用,如下所示:

同样,我们检查传入的控件是否不为 Nothing,并且我们的 Dictionary
是否已包含该控件,如果是,则返回 string
。否则,返回一个空的 string
。
通过添加 Localizable(True)
属性,我们可以轻松地在设计时提供不同语言的消息。System.ComponentModel.Design
命名空间中的 MultilineStringEditor
允许我们在属性窗格中为属性提供多行 string
。这允许我们以所见即所得的方式(现在很少听到这个词)输入所需的文本,并进行适当的格式化。DefaultValue("") 可防止 Windows Forms 设计器在不需要为禁用控件设置工具提示时在 InitializeComponent()
中插入以下代码。
Me.EnhancedToolTip1.SetToolTipWhenDisabled(Me.CheckBox1, "")
导入 System.Drawing.Design
命名空间是为了 UITypeEditor
。
Private Sub Control_EnabledChanged()
每当关联控件的 Enabled
属性在运行时发生更改时,就会引发此事件。我们根据属性值调用适当的函数。
Private Sub ShowToolTip(ByVal control As Control)
使用 TransparentSheet
为禁用控件提供工具提示时遇到的一个问题是,它不能用于派生自 Form
的类。即使 ToolTipWhenDisabled
属性对于 Form
在属性窗格中显示,您也可以在其上键入文本,但在运行时我们需要忽略它。否则,当访问 PutOnTransparentSheet()
或 TakeOffTransparentSheet()
中的 control.Parent
时,会抛出 ArgumentNullException
,因为 Form
的 Parent
当然是 Nothing。
Private Sub ShowToolTipWhenDisabled(ByVal control As Control)
另一个问题是,如果关联控件位于容器控件(如 Form
或 Panel
)上并且容器被禁用,我们也不能使用此方案。如果您动态地将任何视觉控件放置在禁用的容器上,并在其 control.EnabledChanged
事件中尝试使用 control.BringToFront()
显示它,那么该事件将由框架重复触发,您无法控制它。
Private Sub PutOnTransparentSheet()
此函数执行以下操作:
- 实例化一个
TransparentSheet
控件。 - 将其位置设置为与被禁用控件相同。
- 除非显式指定
SizeOfToolTipWhenDisabled
,否则将其大小设置为与被禁用控件相同。 - 将
TransparentSheet
添加到窗体上,使其显示(即使它是透明的)。 - 确保显示的透明 Sheet 在 Z 顺序的顶部。
- 将其保存在
Dictionary
中。 - 为透明 Sheet 调用
SetToolTip()
,并提供工具提示消息。
Private Sub TakeOffTransparentSheet()
此函数执行以下操作:
- 确保
Dictionary
包含键。 - 从窗体中移除透明 Sheet。
- 从基类中移除它。
- 释放它。
- 从
Dictionary
中移除控件。
Public Sub SetSizeOfToolTipWhenDisabled()
这设置了 Extender 属性“SizeOfToolTipWhenDisabled
”。只有当您想要一个超大尺寸的透明 Sheet 以便覆盖多个视觉控件时,才将其从默认值更改。否则,将其保留为 0,0。在演示示例中,我为 RadioButton1
设置了这个属性,以便整个区域被一个透明 Sheet 覆盖。为了方便起见,我在设计时将一个 TransparentSheet
放置在窗体上以测量所需的大小。
Public Function GetSizeOfToolTipWhenDisabled()
这会获取 Extender 属性“SizeOfToolTipWhenDisabled
”。DefaultValue(GetType(Size), "0,0")
可防止 Windows Forms 设计器在不需要为禁用控件设置超大工具提示时在 InitializeComponent()
中插入以下代码。
Me.EnhancedToolTip1.SetSizeOfToolTipWhenDisabled_
(Me.CheckBox1, New System.Drawing.Size(0, 0))
Public Shadows Property ToolTipTitle()
这会拦截内置的 ToolTipTitle
属性,并将其私有地保存在 m_SavedToolTipTitle
中。
Public Shadows Property ToolTipIcon()
这会拦截内置的 ToolTipIcon
属性,并将其私有地保存在 m_SavedToolTipIcon
中。
Private Sub EnhancedToolTip_Popup()
此事件处理程序允许我们在 ToolTip
弹出之前更改 ToolTipTitle
和 ToolTipIcon
。我选择不显示禁用控件的标题和图标,即当事件的关联控件是 TransparentSheet
时。确保调用基类的属性,而不是遮蔽的新属性。
如果您想显示与启用控件相同的标题和图标,请移除此事件处理程序和两个遮蔽属性。
UML 图
对于那些想看图的人来说,这是 UML 类图。

思考继承层次结构通常是有益的。例如,您可以派生自 UserControl
类而不是 ContainerControl
类来创建一个等效的 TransparentSheet
,因为 UserControl
是派生自 ContainerControl
的类之一,就像 Form
类一样。然而,对于 TransparentSheet
,我们不需要它们额外的功能,因为我们不在上面放置任何东西。
结论
虽然您可以将 EnhancedToolTip
类的大部分代码放在 Form
类中,并且仅使用内置的 ToolTip
和一些 TransparentSheets
来获得相同的结果,但不要这样做。不要让 form
类变得混乱(已经很混乱了)。重构并将每个功能移到最合适的类中。即使我们不拥有 ToolTip
类的源代码,OOP 也允许我们通过重写和遮蔽来增强它,添加新功能或修改现有功能。
最后但同样重要的是,请务必在您用户界面的禁用控件的工具提示消息中包含功能被禁用的原因和/或用户如何启用该功能。没有人想费力地按 F1 键,等待帮助屏幕出现,然后在帮助层次结构中搜索难以找到的答案。只需将鼠标悬停在区域上,就应该为用户提供足够的信息。
历史
- 2009年12月22日:为以下两个问题添加了解决方法
- 当
ToolTipWhenDisabled
文本被分配给派生自Form
的类时,软件会因ArgumentNullException
而崩溃。 - 当
ToolTipWhenDisabled
文本被分配给一个视觉控件,并且该控件的容器控件(如Form
或Panel
)被禁用时,软件会陷入无限循环。
- 2008年12月24日:初始版本