これが明らかにずっと前に解決された問題ではないことにまだ驚いていますが、私が思いついた解決策は次のとおりです。
簡単なまとめ
以下のクラスは 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