开发者

Implementing a dynamic submenu in VB.Net

On a Windows Form in .Net 3.5 I have created a menu object and populated it with ToolStripMenuItems. One of these items has a DropDown object attached. The DropDown should appear when the mouse hovers over the parent ToolStripMenuItem and disappear when the mouse leaves the ToolStripMenuItem unless it is "leaving" the parent by entering the parent's DropDown.

Also, I don't want the DropDown to automatically close when the user makes a selection in it, so I have set its "AutoClose" property to False.

Getting the DropDown to appear was easy. I just set up a handler for a "MouseEnter" event on the parent ToolStripMenuItem. But I'm stuck trying to make the DropDown disappear at the right time. If I set up a handler to close the it when the mouse leaves the parent ToolStripMenuItem, i开发者_开发百科t becomes impossible to use the DropDown, because moving the mouse into the DropDown means "leaving" the parent ToolStripMenuItem, and so the DropDown closes as soon as the user tries to hover over it!

I haven't been able to figure out how to detect if the mouse has really left the whole ToolStripMenuItem / DropDown assembly (in which case the DropDown should close) or has only "left" the ToolStripMenuItem by entering the DropDown (in which case the DropDown should not close).

This seems like a common design - a drop down that appears / disappears when the mouse hovers over / leaves the parent element - so how is it normally done? Grateful for any suggestions.


Still surprised that this is apparently not a problem that was solved a long time ago, but here's the solution I came up with:

Quick summary

The class below inherits from ToolStripMenuItem. Use it if you want the item to have a child DropDown menu that appears when the user's mouse hovers over it.

Terms I use below

ToolStripMenuItem: an item in a ToolStripDropDownMenu. It is both a member of a ToolStripDropDownMenu (the "parent menu"), and it also has access to another ToolStripDropDownMenu via its "DropDown" property (the "child menu").

Statement of the problem and soltion

The child ToolStripDropDownMenu that appears when you hover over the ToolStripMenuItem should normally close when the mouse leaves that ToolStripMenuItem and/or when it leaves the parent ToolStripDropDownMenu that contains it. However, it should not close if the mouse leaves the parent menu by entering the child menu at the same time. In that case, the "MouseEnter" event on the child menu should cancel the normal behavior of the "MouseLeave" event on the parent menu (i.e., the DropDown should not close).

The problem when you try to set this up in a normal, straightforward way is that the "MouseLeave" event on the parent menu fires before the "MouseEnter" event on the child menu, and the child menu closes before the mouse can enter it.

The solution below shunts the call to DropDown.Close() into a separate thread, where the "Close" action is delayed by a few seconds. In that short window, the "MouseEnter" event on the child DropDown (which is still on the main thread) has a chance to set a globally accessible dictionary value to True. After the delay, the value of this dictionary entry is checked in the separate thread, and the child menu is either closed (by calling the thread-safe "Invoke" method) or not. The program then goes on to check whether the parent menu also needs to be closed, whether that menu's parent menu needs to be closed, and so on. This code allows floating submenus to be nested as deep as any reasonable person would want.

There are separate handlers for "MouseEnter" and "MouseLeave" events for the individual menu item, its parent menu, and its child menu. They all check on each other to decide on the right course of action.

In conclusion

In posting this I wanted to provide an elegant working solution to this problem for which I had previously been unable to find much help. Still, iIf anyone has any tweaks for it, I'd love to hear them. Until then, please use this class if it helps you. When you instantiate it you need to send it a string for the text that will appear on it, a pointer to the main form, and a pointer to the parent ToolStripDropDownMenu to which you are adding it. After that, just use it as you would a normal ToolStripMenuItem. I also added a flag that can be set to True if you want the child DropDown menu items to behave like radio buttons (only one selectable at a time). -- Noel T. Taylor

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
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜