65.9K
CodeProject 正在变化。 阅读更多。
Home

鼠标悬停在禁用控件上时显示工具提示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (26投票s)

2008年12月27日

CPOL

9分钟阅读

viewsIcon

99476

downloadIcon

1790

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

ToolTipWhenDisabled1.jpg

引言

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

背景

软件开发者之间对于用户界面是否应该显示任何禁用的视觉控件存在争议,但我们时不时地会看到它们,甚至来自微软,如下所示。

ToolTipWhenDisabled2.jpg

你知道如何启用这些被禁用的控件吗?

解决方案

解决这个问题需要两个步骤:

  1. 创建一个透明的 Sheet 控件,可以在运行时覆盖被禁用的控件,并从中提供工具提示;
  2. 通过一个名为 ToolTipWhenDisabled 的 Extender 属性来扩展 ToolTip 类,并嵌入附加和分离透明 Sheet 的逻辑,该逻辑由关联控件的 EnabledChanged 事件触发。

创建 TransparentSheet 控件

实现透明 Sheet 的一种方法是继承 ContainerControl 类。

  1. 启动 Visual Studio 并创建一个新的 Windows Forms 应用程序。

  2. 创建一个名为 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
  3. 现在,构建应用程序。Visual Studio 会将该控件添加到工具箱中,您可以像使用其他任何视觉控件一样在窗体上使用它。

Using the Code

Public Sub New() 

我们需要设置 ControlStyles.Opaque 标志并调用 UpdateStyles() 以抑制背景绘制。

然后,我们需要将 AutoScaleMode 更改为 None。否则,当我们动态实例化 TransparentSheet 并将其放置在具有不同 Font 的窗体上时,LocationSize 属性将发生变化。由于我们需要将 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 来实现。

  1. 选择“项目/添加引用…”菜单。
  2. 在“.NET”选项卡下选择 System.Design
  3. 点击“确定”。
  4. 创建一个名为 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 属性将在属性窗格中可用,如下所示:

ToolTipWhenDisabled3.jpg

同样,我们检查传入的控件是否不为 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,因为 FormParent 当然是 Nothing。

Private Sub ShowToolTipWhenDisabled(ByVal control As Control)

另一个问题是,如果关联控件位于容器控件(如 FormPanel)上并且容器被禁用,我们也不能使用此方案。如果您动态地将任何视觉控件放置在禁用的容器上,并在其 control.EnabledChanged 事件中尝试使用 control.BringToFront() 显示它,那么该事件将由框架重复触发,您无法控制它。

Private Sub PutOnTransparentSheet()

此函数执行以下操作:

  1. 实例化一个 TransparentSheet 控件。
  2. 将其位置设置为与被禁用控件相同。
  3. 除非显式指定 SizeOfToolTipWhenDisabled,否则将其大小设置为与被禁用控件相同。
  4. TransparentSheet 添加到窗体上,使其显示(即使它是透明的)。
  5. 确保显示的透明 Sheet 在 Z 顺序的顶部。
  6. 将其保存在 Dictionary 中。
  7. 为透明 Sheet 调用 SetToolTip(),并提供工具提示消息。
Private Sub TakeOffTransparentSheet()

此函数执行以下操作:

  1. 确保 Dictionary 包含键。
  2. 从窗体中移除透明 Sheet。
  3. 从基类中移除它。
  4. 释放它。
  5. 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 弹出之前更改 ToolTipTitleToolTipIcon。我选择不显示禁用控件的标题和图标,即当事件的关联控件是 TransparentSheet 时。确保调用基类的属性,而不是遮蔽的新属性。

如果您想显示与启用控件相同的标题和图标,请移除此事件处理程序和两个遮蔽属性。

UML 图

对于那些想看图的人来说,这是 UML 类图。

ToolTipWhenDisabled4.jpg

思考继承层次结构通常是有益的。例如,您可以派生自 UserControl 类而不是 ContainerControl 类来创建一个等效的 TransparentSheet,因为 UserControl 是派生自 ContainerControl 的类之一,就像 Form 类一样。然而,对于 TransparentSheet,我们不需要它们额外的功能,因为我们不在上面放置任何东西。

结论

虽然您可以将 EnhancedToolTip 类的大部分代码放在 Form 类中,并且仅使用内置的 ToolTip 和一些 TransparentSheets 来获得相同的结果,但不要这样做。不要让 form 类变得混乱(已经很混乱了)。重构并将每个功能移到最合适的类中。即使我们不拥有 ToolTip 类的源代码,OOP 也允许我们通过重写和遮蔽来增强它,添加新功能或修改现有功能。

最后但同样重要的是,请务必在您用户界面的禁用控件的工具提示消息中包含功能被禁用的原因和/或用户如何启用该功能。没有人想费力地按 F1 键,等待帮助屏幕出现,然后在帮助层次结构中搜索难以找到的答案。只需将鼠标悬停在区域上,就应该为用户提供足够的信息。

历史

  • 2009年12月22日:为以下两个问题添加了解决方法
  1. ToolTipWhenDisabled 文本被分配给派生自 Form 的类时,软件会因 ArgumentNullException 而崩溃。
  2. ToolTipWhenDisabled 文本被分配给一个视觉控件,并且该控件的容器控件(如 FormPanel)被禁用时,软件会陷入无限循环。
  • 2008年12月24日:初始版本
© . All rights reserved.