0

.Net 3.5 の Windows フォームで、メニュー オブジェクトを作成し、それに ToolStripMenuItems を設定しました。これらの項目の 1 つには、DropDown オブジェクトが添付されています。DropDown は、マウスが親の ToolStripMenuItem の上に置かれたときに表示され、マウスが ToolStripMenuItem から離れたときに非表示になります。ただし、親の DropDown に入って親を「離れる」場合は除きます。

また、ユーザーが選択を行ったときに DropDown を自動的に閉じたくないので、「AutoClose」プロパティを False に設定しました。

ドロップダウンを表示するのは簡単でした。親の ToolStripMenuItem で "MouseEnter" イベントのハンドラーをセットアップしました。しかし、適切なタイミングで DropDown を非表示にしようとして困っています。マウスが親の ToolStripMenuItem を離れたときにそれを閉じるようにハンドラーを設定すると、DropDown を使用できなくなります。ユーザーはその上にカーソルを合わせようとします!

マウスが本当に ToolStripMenuItem / DropDown アセンブリ全体を離れたか (この場合、DropDown を閉じる必要があります)、または DropDown を入力して ToolStripMenuItem を「残した」だけか (この場合、ドロップダウンは閉じないでください)。

これは一般的なデザインのように思えます - マウスが親要素の上に移動する/離れると表示/非表示になるドロップダウン - 通常はどのように行われますか? 提案に感謝します。

4

1 に答える 1

1

これが明らかにずっと前に解決された問題ではないことにまだ驚いていますが、私が思いついた解決策は次のとおりです。

簡単なまとめ

以下のクラスは ToolStripMenuItem を継承しています。ユーザーのマウスがその上に置かれたときに表示される子ドロップダウン メニューを項目に持たせたい場合に使用します。

以下で使用する用語

ToolStripMenuItem: ToolStripDropDownMenu 内の項目。これは ToolStripDropDownMenu (「親メニュー」) のメンバーであり、「DropDown」プロパティ (「子メニュー」) を介して別の ToolStripDropDownMenu にアクセスすることもできます。

問題と解決策の説明

ToolStripMenuItem の上にカーソルを置いたときに表示される子 ToolStripDropDownMenu は、通常、マウスがその ToolStripMenuItem を離れたとき、および/またはそれを含む親 ToolStripDropDownMenu を離れたときに閉じます。ただし、同時に子メニューに入ってマウスが親メニューから離れた場合は閉じないでください。その場合、子メニューの「MouseEnter」イベントは、親メニューの「MouseLeave」イベントの通常の動作をキャンセルする必要があります (つまり、DropDown は閉じな​​いでください)。

これを通常の簡単な方法で設定しようとすると、子メニューの「MouseEnter」イベントの前に親メニューの「MouseLeave」イベントが発生し、マウスが入る前に子メニューが閉じてしまうという問題があります。

以下のソリューションは、DropDown.Close() の呼び出しを別のスレッドにシャントし、そこで「閉じる」アクションが数秒遅れます。その短いウィンドウで、子 DropDown (まだメイン スレッド上にある) の "MouseEnter" イベントは、グローバルにアクセス可能なディクショナリ値を True に設定する可能性があります。遅延の後、このディクショナリ エントリの値が別のスレッドでチェックされ、子メニューが (スレッド セーフな "Invoke" メソッドを呼び出すことによって) 閉じられるか、閉じられません。次にプログラムは、親メニューも閉じる必要があるかどうか、そのメニューの親メニューを閉じる必要があるかどうかなどをチェックします。このコードにより、合理的な人なら誰でも望むだけの深さでフローティング サブメニューをネストできます。

個々のメニュー項目、その親メニュー、およびその子メニューには、「MouseEnter」および「MouseLeave」イベント用の個別のハンドラーがあります。彼らは皆、お互いをチェックして、正しい行動方針を決定します。

結論は

これを投稿することで、以前はあまり助けを見つけることができなかったこの問題に対するエレガントな実用的な解決策を提供したかった. それでも、もし誰かがそれについて微調整をしているなら、ぜひ聞いてみたい. それまでは、お役に立てればこのクラスをご利用ください。インスタンス化するときは、表示されるテキストの文字列、メイン フォームへのポインター、および追加先の親 ToolStripDropDownMenu へのポインターを送信する必要があります。その後は、通常の ToolStripMenuItem と同じように使用してください。また、子の DropDown メニュー項目をラジオ ボタンのように動作させたい場合 (一度に 1 つだけ選択可能)、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
于 2011-06-24T16:56:33.917 に答える