仍然很惊讶这显然不是很久以前解决的问题,但这是我想出的解决方案:
快速总结
下面的类继承自 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