在VB.Net中实现动态子菜单

在VB.Net中实现动态子菜单,.net,vb.net,dynamic,menu,mouseleave,.net,Vb.net,Dynamic,Menu,Mouseleave,在.NET3.5中的Windows窗体上,我创建了一个菜单对象,并用ToolStripMenuItems填充它。其中一个项目附加了一个下拉对象。当鼠标悬停在父ToolStripMenuItem上时,下拉列表应出现,当鼠标离开ToolStripMenuItem时,下拉列表应消失,除非它通过输入父级下拉列表“离开”父级 此外,我不希望当用户在下拉列表中进行选择时,下拉列表自动关闭,因此我将其“AutoClose”属性设置为False 让下拉列表显示起来很容易。我刚刚在父ToolStripMenuIt

在.NET3.5中的Windows窗体上,我创建了一个菜单对象,并用ToolStripMenuItems填充它。其中一个项目附加了一个下拉对象。当鼠标悬停在父ToolStripMenuItem上时,下拉列表应出现,当鼠标离开ToolStripMenuItem时,下拉列表应消失,除非它通过输入父级下拉列表“离开”父级

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

让下拉列表显示起来很容易。我刚刚在父ToolStripMenuItem上为“MouseEnter”事件设置了一个处理程序。但我一直在努力让下拉列表在合适的时间消失。如果我设置了一个处理程序,在鼠标离开父ToolStripMenuItem时关闭它,那么就不可能使用下拉列表,因为将鼠标移动到下拉列表中意味着“离开”父ToolStripMenuItem,因此一旦用户尝试将鼠标悬停在下拉列表上,下拉列表就会关闭

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


这似乎是一种常见的设计——鼠标悬停在父元素上/离开父元素时出现/消失的下拉菜单——那么它通常是如何实现的呢?感谢您的建议。

对于这显然不是一个很久以前就解决的问题,我仍然感到惊讶,但以下是我提出的解决方案:

快速总结

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

我在下面使用的术语

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

问题和解决方案的陈述

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

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

下面的解决方案将对DropDown.Close()的调用分流到一个单独的线程中,“Close”操作延迟几秒钟。在这个短窗口中,子下拉列表上的“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