Реализация динамического подменю в VB.Net - PullRequest
0 голосов
/ 20 июня 2011

В форме Windows в .Net 3.5 я создал объект меню и заполнил его ToolStripMenuItems.К одному из этих объектов прикреплен объект DropDown.DropDown должен появляться, когда указатель мыши находится над родительским ToolStripMenuItem, и исчезать, когда мышь покидает ToolStripMenuItem , если он не «покидает» родителя, вводя DropDown родительского элемента.Я не хочу, чтобы DropDown автоматически закрывался, когда пользователь делал в нем выбор, поэтому я установил для его свойства AutoClose значение False.

Отображение DropDown было простым.Я просто установил обработчик для события «MouseEnter» в родительском ToolStripMenuItem.Но я застрял, пытаясь заставить DropDown исчезнуть в нужное время.Если я настрою обработчик, чтобы закрыть его, когда мышь покидает родительский ToolStripMenuItem, становится невозможно использовать DropDown, потому что перемещение мыши в DropDown означает «оставление» родительского ToolStripMenuItem, и поэтому DropDown закрывается, как толькопользователь пытается навести на него курсор!

Я не смог выяснить, как определить, действительно ли мышь покинула всю сборку ToolStripMenuItem / DropDown (в этом случае DropDown должен закрыться) или имеет только "оставил "ToolStripMenuItem, введя DropDown (в этом случае DropDown не должен закрываться).

Это похоже на обычный дизайн - выпадающий список, который появляется / исчезает, когда мышь наводит курсор на родительский элемент или покидает его -так как это обычно делается?Благодарен за любые предложения.

1 Ответ

1 голос
/ 24 июня 2011

Все еще удивлен, что это, очевидно, не проблема, которая была решена давным-давно, но вот решение, которое я придумал:

Краткое резюме

Класс ниже наследуется от 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
...