Все еще удивлен, что это, очевидно, не проблема, которая была решена давным-давно, но вот решение, которое я придумал:
Краткое резюме
Класс ниже наследуется от ToolStripMenuItem. Используйте его, если вы хотите, чтобы у элемента было дочернее меню DropDown, которое появляется при наведении на него указателя мыши.
Термины, которые я использую ниже
ToolStripMenuItem: элемент в ToolStripDropDownMenu. Он является одновременно членом ToolStripDropDownMenu («родительское меню»), а также имеет доступ к другому ToolStripDropDownMenu через свойство «DropDown» («дочернее меню»).
Постановка задачи и решение
Дочерний ToolStripDropDownMenu, который появляется при наведении курсора на ToolStripMenuItem, должен нормально закрываться, когда мышь покидает этот ToolStripMenuItem и / или когда он покидает родительский ToolStripDropDownMenu, в котором он находится. Однако не следует закрывать , если мышь покидает родительское меню, одновременно входя в дочернее меню. В этом случае событие MouseEnter в дочернем меню должно отменить нормальное поведение события MouseLeave в родительском меню (то есть DropDown не должен закрываться).
Проблема при попытке установить это обычным простым способом заключается в том, что событие MouseLeave в родительском меню запускается до события MouseEnter в дочернем меню, а дочернее меню закрывается до того, как мышь сможет введите его.
Решение, приведенное ниже, перенаправляет вызов DropDown.Close () в отдельный поток, где действие «Закрыть» задерживается на несколько секунд. В этом коротком окне событие MouseEnter в дочернем DropDown (которое все еще находится в главном потоке) имеет шанс установить глобально доступное значение словаря в True. После задержки значение этой словарной статьи проверяется в отдельном потоке, и дочернее меню либо закрывается (путем вызова поточно-безопасного метода «Вызвать»), либо нет. Затем программа продолжает проверять, нужно ли закрывать родительское меню, нужно ли закрывать родительское меню , , и так далее. Этот код позволяет размещать плавающие подменю настолько глубоко, насколько этого захочет любой разумный человек.
Существуют отдельные обработчики для событий MouseEnter и MouseLeave для отдельного элемента меню, его родительского меню и его дочернего меню. Все они проверяют друг друга, чтобы выбрать правильный курс действий.
В заключение
Публикуя это, я хотел предоставить изящное рабочее решение этой проблемы, для которой ранее я не мог найти много помощи. Тем не менее, если у кого-то есть какие-то настройки, я бы хотел их услышать. До этого, пожалуйста, используйте этот класс, если он вам помогает. Когда вы создаете его экземпляр, вам нужно отправить ему строку для текста, который появится на нем, указатель на основную форму и указатель на родительский ToolStripDropDownMenu, к которому вы добавляете его. После этого просто используйте его, как обычный инструмент ToolStripMenuItem. Я также добавил флаг, который можно установить в True, если вы хотите, чтобы дочерние пункты меню DropDown вели себя как переключатели (только один выбирается за раз). - Ноэль Т. Тейлор
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