0

I'm making a custom ComboBox, and I'm struggling to make it's dropdown (a Panel) close when the user clicks outside. As everyone probably knows, using LostFocus event is not enough because when the user clicks on eg.: a scroolbar or the form itself, the control doesn't lose focus. I tried using IMessageFilter, but I don't think I understood how it works. I also tried using the Capture property, forcing the dropdown to capture the mouse, but that also forces the mouse click to happen on the dropdown (Panel) itself, meaning that if the user had clicked on an item on the dropdown, the click won't work.

Let me clarify:

I'm making a custom control, a UserControl, which is compiled to a .DLL, and can be dragged'n dropped from the Toolbox onto forms, just like any other Winforms control. The control is a Combobox. I want to make the Combobox's dropdown (which is a panel, created at runtime) close when the user clicks outside of it, just like a normal ComboBox. And what is the problem? Detecting clicks outside of the panel and then closing it.

-> Using LostFocus event: Not enough. If the user clicks on a blank space on the form, LostFocus is not fired. I know I can place a LostFocus event on my form and set the ActiveContrl to Nothing, but that would require that users (devs) of my custom ComboBox place the same code on every form they use the control.

-> Using Capture property: Has given the best results so far. There are still some problems though. When Capture is set to True, the mouse is indeed captured by the control which has that property set to True. Therefore, no mouse activity is sensed by any other control on the app, but only the control which has the mouse captured. It'll only release the mouse when the user performs a click. This leads to some problems:

  • If the user clicks on an item (a Button) on the dropdown, and the dropdown (a Panel) has its Capture property set to true, the click will be "ignored", as it'll be handled by the dropdown, and not by the Buttonon which the user actually wanted to click.
  • If the user clicks on the dropdown's scroolbar, the click will also be "ignored", just like explained above.
  • When user moves the mouse above the Buttons inside the dropdown (the ComboBox's items), they are not highlighted, because no MouseEnter is fired for them, as the mouse is Captured by the dropdown.

There is a way to solve the first issue: You can find on which button is the mouse pointing using Control.MousePosition, and then force a click on it. I haven't been able to find a way to solve the 2nd and 3rd issue though. One possible solution I thought of was setting the dropdown's Capture property to False when the mouse enters on it. So, this is how it would work:

  • User clicks on the Combobox. Dropdown opens, captures the mouse;
  • User then moves the mouse inside Combobox's dropdown. Capture is set to false, thus making it possible for the user to click on any button inside the dropdown or on the dropdown's scroolbar, and so forth. Buttons inside the dropdown are also properly highlighted as user moves the mouse above them;
  • User moves the mouse outside Combobox's dropdown, Capture is again set to true, and now any click the user perform outside of it will be "ignored", and the dropdown will be closed. Just like a normal Combobox.

But when I tried to do this, another issue arrived: when a control has its Capture property set to True, it'll constantly fire MouseEnter events. MouseEnter event doesn't use the real mouse pointer location. Even if the mouse pointer is actually outside a Control, that Control will think the mouse is actually inside of it if its Capture property is set to True.

andre_ss6
  • 1,195
  • 1
  • 13
  • 34
  • 2
    Yes, use the Capture property, just like ComboBox does. Getting the Panel's MouseDown event to fire is what you want, you can then tell from the location that it is outside of the panel. – Hans Passant May 24 '14 at 03:28
  • Ok, but what if the user wants to select an item in the dropdown? He'll have to click twice because of the Capture property. – andre_ss6 May 24 '14 at 03:32
  • You'll of course ought to do the same thing ComboBox does: automatically close the dropdown when an item is selected. – Hans Passant May 24 '14 at 03:35
  • Of course. I already do that. It seems though you didn't see the problem yet. When `Capture` = `True`, mouse clicks are handled by the control which has it set to True, meaning that if the user clicks on a button or something, the click event won't be fired on the button correspondent to the mouse pointer's location, but instead on the control which has its `Capture` property set to `True`. Therefore, if the Panel used as dropdown has `Capture` = True and the user try to click on an item inside the dropdown, the click event will fire for the Panel, and not for the item he clicked on. – andre_ss6 May 24 '14 at 03:46
  • 1
    Right, I never imagined you used buttons on the panel. You make it too hard to help you by not showing *anything*. – Hans Passant May 24 '14 at 10:11
  • Well, I thought that would be easily inferred as seen that I have a combobox and it's dropdown. Certainly there would be buttons, labels or anything clickable, which are the ComboBox's items, inside that dropdown. – andre_ss6 May 24 '14 at 14:35

1 Answers1

1

Edit2: here is the code for handling some different types of events (should work for all cases now).

Public Class Form1

    Private Shared mouseNotify() As Int32 = {&H201, &H204, &H207} ' WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN
    Private Shared scrollNotify() As Int32 = {&H114, &H115} ' WM_HSCROLL, WM_VSCROLL
    Private Shared scrollCommands() As Int32 = {0, 1, 2, 3, 4, 5} ' SB_LINEUP, SB_LINEDOWN, SB_PAGEUP, SB_PAGEDOWN, SB_THUMBTRACK, SB_THUMBPOSITION

    Private Sub baseLoad(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
        AutoScroll = True
        Controls.Add(myPanel)
        Controls.Add(myTextBox4)
        myPanel.myTextBox1.Focus()
    End Sub

    Private myTextBox4 As New customTextBox(300)
    Private myPanel As New customPanel

    Protected Overrides Sub OnScroll(ByVal se As ScrollEventArgs)
        MyBase.OnScroll(se)
        ActiveControl = Nothing
    End Sub

    Protected Overrides Sub OnMouseWheel(ByVal e As MouseEventArgs)
        MyBase.OnMouseWheel(e)
        ActiveControl = Nothing
    End Sub

    Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
        MyBase.OnResize(e)
        ActiveControl = Nothing
    End Sub

    Protected Overrides Sub OnMove(ByVal e As System.EventArgs)
        MyBase.OnMove(e)
        ActiveControl = Nothing
    End Sub

    Friend Shared Function isOverControl(ByRef theControl As Control) As Boolean
        Return theControl.ClientRectangle.Contains(theControl.PointToClient(Cursor.Position))
    End Function

    Protected Overrides Sub WndProc(ByRef m As Message)
        If mouseNotify.Contains(CInt(m.Msg)) Then
            If Not isOverControl(myPanel) Then
                ActiveControl = Nothing
            Else
                myPanel.myTextBox1.Focus()
            End If
        End If
        MyBase.WndProc(m)
    End Sub

    Friend Class customPanel : Inherits Panel

        Friend myTextBox1 As New customTextBox(20)
        Private myTextBox2 As New customTextBox(60)
        Private myTextBox3 As New customTextBox(200)

        Friend Sub New()
            AutoScroll = True
            Location = New Point(0, 100)
            Size = New Size(200, 100)
            Controls.Add(myTextBox1)
            Controls.Add(myTextBox2)
            Controls.Add(myTextBox3)
        End Sub

        Protected Overrides Sub OnLeave(ByVal e As EventArgs)
            MyBase.OnLeave(e)
            myTextBox1.Text = "false"
            myTextBox2.Text = "false"
            BackColor = Color.Green
        End Sub

        Protected Overrides Sub OnEnter(ByVal e As EventArgs)
            myTextBox1.Text = "true"
            myTextBox2.Text = "true"
            BackColor = Color.Gold
            MyBase.OnEnter(e)
        End Sub

        Protected Overrides Sub WndProc(ByRef m As Message)
            If mouseNotify.Contains(CInt(m.Msg)) Then
                If isOverControl(Me) Then Form1.WndProc(m)
            End If
            MyBase.WndProc(m)
        End Sub

    End Class

    Friend Class customTextBox : Inherits TextBox

        Friend Sub New(ByVal y As Integer)
            Location = New Point(10, y)
            Size = New Size(100, 30)
        End Sub

        Protected Overrides Sub OnLeave(ByVal e As EventArgs)
            MyBase.OnLeave(e)
            BackColor = Color.Blue
        End Sub

        Protected Overrides Sub OnEnter(ByVal e As EventArgs)
            BackColor = Color.Red
            MyBase.OnEnter(e)
        End Sub

    End Class

End Class

If it doesn't work in all cases, you may have to attach events to all the controls on your form that can't/won't receive focus on mouse events.

also, this works slightly differently depending on the type of control. richtextbox use OnHScroll and OnVscroll for example, instead of OnScroll. you can also get the thumb position with CInt(m.WParam.ToInt32 >> 16), only valid for SB_THUMBTRACK, SB_THUMBPOSITION.

also, here are some interesting techniques: Handling a click event anywhere inside a panel in C#

this was actually taken from the MSDN page for WndProc: http://msdn.microsoft.com/en-us/library/system.windows.forms.control.wndproc%28v=vs.110%29.aspx

and the page for NativeWindow Class: http://msdn.microsoft.com/en-us/library/system.windows.forms.nativewindow.aspx

Community
  • 1
  • 1
porkchop
  • 401
  • 3
  • 8
  • Well, I don't think it would be a good idea to add a timer on a usercontrol. Imagine if someone (another dev) uses it and add dozens of it to a form. It wouldn't be nice. And about checking if mouse is over a button when Capture = True, I did that yesterday, was even going to answer my own question thinking it would work, but there is a problem: When capture = true, not only the rest of the form "ignores" the mouse, but also the dropdown itself. So, while Capture = True, items inside the dropdown won't change color on mouseover and also it isn't possible to click on the dropdown's Scroolbar. – andre_ss6 May 24 '14 at 14:26
  • Trying to achieve something similar to Microsoft's combobox, which captures the mouse when the dropdown is open, but still works with Scroolbars and etc, I thought about doing this: Adding a `MouseEnter` and a `MouseLeave` to the panel that acts as the dropdown, and then, when `MouseEnter` fires, set Capture = False. But there is another problem with that: When Capture is set to true, it is like if the mouse was always inside the control which has it set to true, meaning that the `MouseEnter` event is constantly fired while Capture = True, even if the pointer is not inside the panel. – andre_ss6 May 24 '14 at 14:33
  • what i'm saying is... the user can navigate away from the panel without the mouse... so this method is hopeless anyways. you could still, however, use the .LostFocus event, and also catch scroll bars (by overriding WndProc and catching SB_THUMBTRACK and SB_THUMBPOSITION). either that or override a literal crapTon(tm) of events in all your controls. hold on, i'll add the WndProc code to the example. – porkchop May 24 '14 at 14:46
  • I don't know if this is a problem, but just as a side note, the panel (the dropdown) is created on runtime. – andre_ss6 May 24 '14 at 15:08
  • as long as you're adding it to the form's control collection, then it should work. – porkchop May 24 '14 at 15:12
  • But this code you provided on the last edit will have to be placed on all forms which has my control, right? I'm really trying to get to a solution which make it possible for the control to work without having to change code on forms on which it'll be placed on. – andre_ss6 May 24 '14 at 17:57
  • should be fixed now, my bad. included additional reference. – porkchop May 25 '14 at 03:22