0

I'm currently writing a ComboBox where I replace the suggestion dropdown with my own. For this I create a ToolStripDropDown which I fill with a listbox. This listbox is updated as the user types text in the combobox text field.

My problem is that if I set the AutoClose property of the ToolStripDropDown to true, it seems that even if the text field has the focus, the ToolStripDropDown "steals" a lot of messages including keyboard messages.

If I set the AutoClose property to false, everything is working fine. But the dropdown is not closed for example if the user clicks outside the combo.

I was wondering about doing my own "AutoClose" but I'm not sure on how to implement it. Any idea on how to do that?

Serge Weinstock
  • 1,235
  • 3
  • 12
  • 20
  • hm maybe you could also control the focus or make a "KeyPreview" for your combobox (like winforms have) – DJmRek Apr 02 '14 at 07:38
  • I've tried that but it doesn't work. I've also tried to capture all the WM_KEYDOWN, WM_KEYUP, WM_CHAR messages sent to the dropdown and send them back to the edit control of the combo (using SendMessage) but it doesn't work either. – Serge Weinstock Apr 02 '14 at 08:11
  • A really simple and maybe dirty solution: `ComboBox.LostFocus -> Toolstripdropdown.Close()` might work but i haven't tested it yet. a simple "AutoClose" i think – DJmRek Apr 02 '14 at 08:24
  • I'm already handling the LostFocus message. If I click on the parent form outside the combo, I don't get any message. – Serge Weinstock Apr 02 '14 at 08:28
  • hm i've tested it yyet and it work here. Have you some code how you handle the LostFocus of the ComboBox? – DJmRek Apr 02 '14 at 08:48
  • So i searched a little and found the following on MSDN: Best practice for userspecific control is to use `Enter` and `Leave` events if the base control using de/activate event. So maybe try this one. A secound option may capture WM_KILLFOCUS. – DJmRek Apr 02 '14 at 09:05
  • The code for WM_KILLFOCUS is on the combobox: protected override void OnLostFocus(EventArgs e) { if (!m_dropDown.Focused && !m_suggestionList.Focused) { hideDropDown(); } base.OnLostFocus(e); } public void hideDropDown() { if (m_dropDown.Visible) { m_dropDown.Close(); } } I've used spy++ to see which messages were received by the combo or the toolstrip when I click outside the combo (not on another control) and no WM_KILLFOCUS is received – Serge Weinstock Apr 02 '14 at 09:11
  • could you try it with OnLeave except OnLostFocus? – DJmRek Apr 02 '14 at 09:56
  • The implementation of AutoClose involves a truly ridiculous amount of hidden code in the ToolStripManager class. Very complicated, it uses a windows hook to spy on messages. Brittle code too, it was broken in .NET 4.5 and required a maintenance release. There is no reasonable way you can replace it. – Hans Passant Apr 02 '14 at 10:35
  • I had a look at MS code. I've created a derived class which overrides AutoClose before calling the base class method. It seems that I don't get the keyboard input because of the ToolStripDropDown.SetVisibleCore () code. I haven't understood everything but it seems that when AutoClose is set to true, the ToolStripManager watches the mouse messages (WM_(L|R|M)BUTTONDOWN)and when the user clicks outside the active dropdown, it closes it with the reason "ToolStripDropDownCloseReason.AppClicked". I should be able to do something similar. – Serge Weinstock Apr 02 '14 at 12:59

1 Answers1

1

I've found a solution. Looking at the ToolStripManager code (thanks ReSharper!), I've discovered that when AutoClose is set to true, the manager is monitoring application messages in order to detect when the user clicks outside of the dropdown. I've adapted their code in the following way:

class MyComboBox : ComboBox, IMessageFilter
{
        private ToolStripDropDown m_dropDown;

        MyComboBox()
        {
            ...
            Application.AddMessageFilter(this);
            ...
        }

        protected override void Dispose(bool disposing)
        {
            ...
            Application.RemoveMessageFilter(this);
            base.Dispose(disposing);
        }


        private const int WM_LBUTTONDOWN = 0x0201;
        private const int WM_RBUTTONDOWN = 0x0204;
        private const int WM_MBUTTONDOWN = 0x0207;
        private const int WM_NCLBUTTONDOWN = 0x00A1;
        private const int WM_NCRBUTTONDOWN = 0x00A4;
        private const int WM_NCMBUTTONDOWN = 0x00A7;

        [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        [ResourceExposure(ResourceScope.None)]
        public static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, [In, Out] ref Point pt, int cPoints);

        public bool PreFilterMessage(ref Message m)
        {
            if (m_dropDown.Visible)
            {
                switch (m.Msg)
                {
                    case WM_LBUTTONDOWN:
                    case WM_RBUTTONDOWN:
                    case WM_MBUTTONDOWN:
                    case WM_NCLBUTTONDOWN:
                    case WM_NCRBUTTONDOWN:
                    case WM_NCMBUTTONDOWN:
                        //
                        // When a mouse button is pressed, we should determine if it is within the client coordinates
                        // of the active dropdown.  If not, we should dismiss it.
                        //
                        int i = unchecked((int)(long)m.LParam);
                        short x = (short)(i & 0xFFFF);
                        short y = (short)((i >> 16) & 0xffff);
                        Point pt = new Point(x, y);
                        MapWindowPoints(m.HWnd, m_dropDown.Handle, ref pt, 1);
                        if (!m_dropDown.ClientRectangle.Contains(pt))
                        {
                            // the user has clicked outside the dropdown
                            pt = new Point(x, y);
                            MapWindowPoints(m.HWnd, Handle, ref pt, 1);
                            if (!ClientRectangle.Contains(pt))
                            {
                                // the user has clicked outside the combo
                                hideDropDown();
                            }
                        }
                        break;
                }
            }
            return false;
        }
}
Serge Weinstock
  • 1,235
  • 3
  • 12
  • 20
  • So ToolStripManager catch the messages away? O.o – DJmRek Apr 03 '14 at 06:24
  • Yes, there is a part of the message processing where it redirects all keyboard input to the active dropdown (ToolStripManager.PreFilterMessage -> case WM_KEYDOWN:case WM_KEYUP:etc...) – Serge Weinstock Apr 03 '14 at 08:49
  • Hi Serge, I bumped into same issue, though I'm implementing custom multicolumn combobox with embedded datagridview. Is there any chance that I might see your full code regrading this autosuggest window ?...Thanks in advance – LuckyLuke82 Feb 12 '18 at 11:31
  • Nevermind, I got It working, thanks for wonderful coding. +1 from me. – LuckyLuke82 Feb 12 '18 at 13:34