0

在 .Net 3.5 中的 Windows 窗体上,我创建了一个菜单对象并使用 ToolStripMenuItems 填充它。其中一项附有 DropDown 对象。当鼠标悬停在父 ToolStripMenuItem 上时,DropDown 应该出现,并在鼠标离开 ToolStripMenuItem 时消失,除非它通过输入父级的 DropDown 来“离开”父级。

另外,我不希望 DropDown 在用户在其中进行选择时自动关闭,因此我将其“AutoClose”属性设置为 False。

让 DropDown 出现很容易。我刚刚为父 ToolStripMenuItem 上的“MouseEnter”事件设置了一个处理程序。但我一直试图让 DropDown 在正确的时间消失。如果我设置一个处理程序以在鼠标离开父 ToolStripMenuItem 时关闭它,则无法使用 DropDown,因为将鼠标移入 DropDown 意味着“离开”父 ToolStripMenuItem,因此 DropDown 会在用户试图将鼠标悬停在它上面!

我无法弄清楚如何检测鼠标是否真的离开了整个 ToolStripMenuItem / DropDown 程序集(在这种情况下 DropDown 应该关闭)或者通过输入 DropDown 只“离开” ToolStripMenuItem(在这种情况下DropDown 不应关闭)。

这似乎是一种常见的设计 - 当鼠标悬停/离开父元素时出现/消失的下拉菜单 - 那么它通常是如何完成的?感谢任何建议。

4

1 回答 1

1

仍然很惊讶这显然不是很久以前解决的问题,但这是我想出的解决方案:

快速总结

下面的类继承自 ToolStripMenuItem。如果您希望该项目具有当用户的鼠标悬停在其上时出现的子下拉菜单,请使用它。

我在下面使用的术语

ToolStripMenuItem:ToolStripDropDownMenu 中的一个项目。它既是 ToolStripDropDownMenu(“父菜单”)的成员,也可以通过其“DropDown”属性(“子菜单”)访问另一个 ToolStripDropDownMenu。

问题和解决方案的陈述

将鼠标悬停在 ToolStripMenuItem 上时出现的子 ToolStripDropDownMenu 通常应在鼠标离开 ToolStripMenuItem 和/或离开包含它的父 ToolStripDropDownMenu 时关闭。但是,如果鼠标离开父菜单并同时进入子菜单,它不应该关闭。在这种情况下,子菜单上的“MouseEnter”事件应该取消父菜单上的“MouseLeave”事件的正常行为(即,DropDown 不应关闭)。

当您尝试以正常、直接的方式进行设置时,问题是父菜单上的“MouseLeave”事件在子菜单上的“MouseEnter”事件之前触发,并且子菜单在鼠标进入之前关闭。

下面的解决方案将对 DropDown.Close() 的调用分流到一个单独的线程中,其中“关闭”操作会延迟几秒钟。在那个短窗口中,子 DropDown(仍在主线程上)上的“MouseEnter”事件有机会将可全局访问的字典值设置为 True。延迟之后,在单独的线程中检查此字典条目的值,并且子菜单要么关闭(通过调用线程安全的“Invoke”方法)要么不关闭。然后程序继续检查父菜单是否也需要关闭,菜单的父菜单是否需要关闭,等等。此代码允许浮动子菜单嵌套到任何合理的人想要的深度。

对于单个菜单项、它的父菜单和它的子菜单,“MouseEnter”和“MouseLeave”事件有单独的处理程序。他们都互相检查以决定正确的行动方案。

综上所述

在发布这篇文章时,我想为这个我以前无法找到太多帮助的问题提供一个优雅的工作解决方案。不过,如果有人对此有任何调整,我很想听听他们的意见。在那之前,如果它对你有帮助,请使用这个类。当您实例化它时,您需要向它发送一个字符串,该字符串将显示在它上面的文本、一个指向主窗体的指针以及一个指向要添加它的父 ToolStripDropDownMenu 的指针。之后,就像使用普通的 ToolStripMenuItem 一样使用它。如果您希望子下拉菜单项的行为类似于单选按钮(一次只能选择一个),我还添加了一个可以设置为 True 的标志。——诺埃尔·T·泰勒

Public Class ToolStripMenuItemHov
  Inherits ToolStripMenuItem

  ' A shared dictionary that reflects whether the mouse is currently
  ' inside the area of a given ToolStripDropDownMenu.
  Shared dictContainsMouse As Dictionary(Of ToolStripDropDownMenu, Boolean) = New Dictionary(Of ToolStripDropDownMenu, Boolean)

  ' A shared dictionary that maps a given ToolStripDropDown menu to
  ' the ToolStripDropDownMenu one level above it.
  Shared dictParents As Dictionary(Of ToolStripDropDownMenu, ToolStripDropDownMenu) = New Dictionary(Of ToolStripDropDownMenu, ToolStripDropDownMenu)

  ' This thread can be started from multiple places in the code; it is
  ' shared so we can check if it's already running before starting it.
  Shared t As Threading.Thread = Nothing

  ' We need to pass this in so we can use the form's "Invoke" method.
  Private oMasterForm As Form

  ' This is the DropDownMenu that contains this ToolStripMenu *item*
  Private oParentToolStripDropDownMenu As ToolStripDropDownMenu

  ' A boolean to track of whether the mouse is currently inside this
  ' menu item, as distinct from whether it's inside this item's parent
  ' ToolStripDropDownMenu (for which we use "dictParents" above).
  Private fContainsMouse As Boolean

  ' If true, only one option in the DropDown can be selected at a time.
  Private p_fWorkLikeRadioButtons As Boolean

  ' We only need this because VB doesn't support anonymous subroutines
  ' (only functions).  Silly really.
  Private Delegate Sub subDelegate()

  Public Sub New(ByVal text As String, ByRef form As Form, ByVal parentToolStripDropDownMenu As ToolStripDropDownMenu)
    Me.Text = text
    Me.oMasterForm = form
    Me.oParentToolStripDropDownMenu = parentToolStripDropDownMenu

    Me.fContainsMouse = False
    Me.p_fWorkLikeRadioButtons = False
    Me.DropDown.AutoClose = False

    dictParents(Me.DropDown) = parentToolStripDropDownMenu
    dictContainsMouse(parentToolStripDropDownMenu) = False
    dictContainsMouse(Me.DropDown) = False

    ' Set the parent's "AutoClose" property to false for correct behavior.
    Me.oParentToolStripDropDownMenu.AutoClose = False

    ' We need to know if the mouse enters or leaves this single menu item,
    ' this menu item's child DropDown, or this menu item's parent DropDown.
    AddHandler (Me.MouseEnter), AddressOf MyMouseEnter
    AddHandler (Me.MouseLeave), AddressOf MyMouseLeave
    AddHandler (Me.DropDown.MouseEnter), AddressOf childDropDown_MouseEnter
    AddHandler (Me.DropDown.MouseLeave), AddressOf childDropDown_MouseLeave
    AddHandler (Me.oParentToolStripDropDownMenu.MouseEnter), AddressOf parentDropDown_MouseEnter
    AddHandler (Me.oParentToolStripDropDownMenu.MouseLeave), AddressOf parentDropDown_MouseLeave
  End Sub

  Public ReadOnly Property checkedItem() As ToolStripMenuItem
    ' This is only useful if "p_fWorkLikeRadioButtons" is true
    Get
      Dim returnItem As ToolStripMenuItem = Nothing
      For Each item As ToolStripMenuItem In Me.DropDown.Items
        If item.Checked Then
          returnItem = item
          Exit For
        End If
      Next
      Return returnItem
    End Get
  End Property

  Public Property workLikeRadioButtons() As Boolean
    Get
      Return Me.p_fWorkLikeRadioButtons
    End Get
    Set(ByVal value As Boolean)
      Me.p_fWorkLikeRadioButtons = value
    End Set
  End Property

  Private Sub myDropDownItemClicked(ByVal source As ToolStripMenuItem, ByVal e As System.EventArgs) Handles Me.DropDownItemClicked
    If Me.workLikeRadioButtons = True Then
      For Each item As ToolStripMenuItem In Me.DropDown.Items
        If item Is source Then
          item.Checked = True
        Else
          item.Checked = False
        End If
      Next
    End If
  End Sub

  Private Sub MyMouseEnter()
    Me.fContainsMouse = True
    If Me.DropDown.Items.Count > 0 Then
      ' Setting "DropDown.Left" causes the DropDown to always appear
      ' in the correct place. Without this, it can appear too far to
      ' the left or right depending on where the user clicks on the
      ' trigger link. Interestingly, it doesn't matter what value you
      ' set it to, as long as you set it to something, so I naturally
      ' chose 74384338.
      Me.DropDown.Left = 74384338
      Me.DropDown.Show()
    End If
  End Sub

  Private Sub MyMouseLeave()
    Me.fContainsMouse = False
    If t Is Nothing Then
      t = New Threading.Thread(AddressOf maybeCloseDropDown)
      t.Start()
    End If
  End Sub

  Private Sub childDropDown_MouseEnter()
    dictContainsMouse(Me.DropDown) = True
  End Sub

  Private Sub childDropDown_MouseLeave()
    dictContainsMouse(Me.DropDown) = False
    If t Is Nothing Then
      t = New Threading.Thread(AddressOf maybeCloseDropDown)
      t.Start()
    End If
  End Sub

  Private Sub parentDropDown_MouseEnter()
    dictContainsMouse(Me.oParentToolStripDropDownMenu) = True
  End Sub

  Private Sub parentDropDown_MouseLeave()
    dictContainsMouse(Me.oParentToolStripDropDownMenu) = False
    If t Is Nothing Then
      t = New Threading.Thread(AddressOf maybeCloseDropDown)
      t.Start()
    End If
  End Sub

  ' Wait an instant and then check if the mouse is either in this
  ' menu item or in this menu item's child DropDown.  If it's not
  ' in either close the child DropDown and maybe close the parent
  ' DropDown (i.e., the DropDown that contains this menu item).
  Private Sub maybeCloseDropDown()
    Threading.Thread.Sleep(100)
    If Me.fContainsMouse = False And dictContainsMouse(Me.DropDown) = False Then
      Me.oMasterForm.Invoke(New subDelegate(AddressOf Me.DropDown.Close))
      maybeCloseParentDropDown(Me.oParentToolStripDropDownMenu)
    End If
    t = Nothing
  End Sub

  ' Recursively close parent DropDowns as long as mouse is not inside.
  Private Sub maybeCloseParentDropDown(ByRef parentDropDown As ToolStripDropDown)
    If dictContainsMouse(parentDropDown) = False Then
      Me.oMasterForm.Invoke(New subDelegate(AddressOf parentDropDown.Close))
      If dictParents.Keys.Contains(parentDropDown) Then
        maybeCloseParentDropDown(dictParents(parentDropDown))
      End If
    End If
    t = Nothing
  End Sub

End Class
于 2011-06-24T16:56:33.917 回答