1

I have a custom WinForms user control that looks like a combobox but instead opens a ToolStripDropDown that contains another custom user control, called NumericFilterPanel, that has a checkbox, a combobox, and a textbox.

enter image description here

The problem is that when the user click-selects an option for the combobox embedded in the dropdown control, it causes the parent dropdown to hide.

I have set ToolStripDropDown.AutoClose = false, which fixes the original problem, but now I am having difficulty detecting all the situations where the dropdown loses focus, such as when the user clicks on the parent form or switches programs. Sometimes the dropdown remains visible and topmost.

Is there a way to either keep AutoClose = true and prevent the embedded combobox from closing the parent dropdown, or is there a way to always detect when the dropdown has lost focus so I can manually close it?

  using System;
  using System.Drawing;
  using System.Windows.Forms;

  namespace mviWinControls
  {
    public partial class NumericRangeDropDown : UserControl
    {
      private const int ARROW_HEIGHT = 4;
      private Brush arrowBrush = new SolidBrush(Color.FromArgb(77, 97, 133));

      private ToolStripDropDown _dropdown;
      private ToolStripControlHost _host;
      private NumericFilterPanel _filter;

      public NumericRangeDropDown()
      {
        InitializeComponent();

        _filter = new NumericFilterPanel();
        _filter.DropDown = this;

        _host = new ToolStripControlHost(_filter);
        _host.Margin = Padding.Empty;
        _host.Padding = Padding.Empty;

        _dropdown = new ToolStripDropDown();
        _dropdown.Margin = Padding.Empty;
        _dropdown.Padding = Padding.Empty;
        _dropdown.AutoClose = false;  // Use this because panel has a combobox.  https://social.msdn.microsoft.com/Forums/windows/en-US/dd95b982-820e-4807-8a1f-79c74acab3f8/two-problems-toolstripdropdown?forum=winforms
        _dropdown.Items.Add(_host);
        _dropdown.Leave += new System.EventHandler(this.DropDown_Leave);

        this.Leave += new System.EventHandler(this.DropDown_Leave);
      }

      /// <summary> 
      /// Clean up any resources being used.
      /// </summary>
      /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
      protected override void Dispose(bool disposing)
      {
        if (disposing)
        {
          if (components != null) components.Dispose();
          if (_dropdown != null) _dropdown.Dispose();
        }
        base.Dispose(disposing);
      }

      public override string Text
      {
        get
        {
          return base.Text;
        }
        set
        {
          base.Text = value;
          _filter.SetValue(value);
        }
      }

      protected override void OnPaint(PaintEventArgs e)
      {
        //base.OnPaint(e);
        TextBox _txtDraw = new TextBox();

        _txtDraw.Width = this.Width;

        using (Bitmap bmp = new Bitmap(_txtDraw.Width, _txtDraw.Height))
        {
          _txtDraw.DrawToBitmap(bmp, new Rectangle(0, 0, _txtDraw.Width, _txtDraw.Height));
          e.Graphics.DrawImage(bmp, 0, 0);
        }

        StringFormat format = new StringFormat();
        format.Alignment = StringAlignment.Near;
        format.FormatFlags = StringFormatFlags.NoWrap;
        format.LineAlignment = StringAlignment.Center;

        using (Brush b = new SolidBrush(this.ForeColor))
          e.Graphics.DrawString(this.Text, this.Font, b, this.DisplayRectangle, format);

        Point[] arrowPoints = new Point[] { new Point(this.Width - ARROW_HEIGHT * 3 - 2, (this.Height - ARROW_HEIGHT) / 2),
                                            new Point(this.Width - ARROW_HEIGHT + 1 - 2, (this.Height - ARROW_HEIGHT) / 2),
                                            new Point(this.Width - ARROW_HEIGHT * 2 - 2, this.Height - (this.Height - ARROW_HEIGHT) / 2) };

        e.Graphics.FillPolygon(arrowBrush, arrowPoints );

      }

      private void DropDown_Leave(object sender, EventArgs e)
      {
        HideDropDown();
        this.Text = _filter.SummaryText();
      }

      private void NumericRangeDropDown_Click(object sender, EventArgs e)
      {
        if (_dropdown.Visible)
          HideDropDown();
        else
          ShowDropDown();
      }

      public void ShowDropDown()
      {
        _dropdown.Show(this, new Point(0, this.Height), ToolStripDropDownDirection.Default);
        _dropdown.BringToFront();
        //_dropdown.Focus();
        _filter.Select();
        _filter.Focus();
      }

      public void HideDropDown()
      {
        _dropdown.Close();
        this.Invalidate();
      }

    }
  }
gotorg
  • 147
  • 1
  • 10

2 Answers2

2

Here's a combobox that can automatically disable and enable the AutoClose property on the host control for you.

Source(I modified it for a combobox versus the DatePicker in their example): http://www.queasy.me/programming/questions/13919634/tool+strip+toolstripdropdownbutton+close+and+lose+window+focus

public partial class CComboBox : ComboBox
{
    private bool savedAutoClose;

    public CComboBox()
    {
        InitializeComponent();
    }

    protected override void OnDropDownClosed(EventArgs e)
    {
        if (this.Parent != null)
        {
            var dropDownHost = this.Parent.Parent as ToolStripDropDown; // recursive instead?
            if (dropDownHost != null)
                dropDownHost.AutoClose = savedAutoClose; // restore the parent's AutoClose preference
        }

        base.OnDropDownClosed(e);
    }

    protected override void OnDropDown(EventArgs e)
    {
        if (this.Parent != null)
        {
            var dropDownHost = this.Parent.Parent as ToolStripDropDown; // recursive instead?
            if (dropDownHost != null)
            {
                savedAutoClose = dropDownHost.AutoClose;
                // ensure that our parent doesn't close while the calendar is open
                dropDownHost.AutoClose = false;
            }
        }
        base.OnDropDown(e);
    }
}
MagikWorx
  • 342
  • 1
  • 10
  • Thanks, this code solves it! I just had to change the type of the parent class to match my NumericRangeDropDown class and add a property to get/set the _dropdown object's AutoClose. – gotorg Jul 26 '18 at 23:21
0

Having taken a good look at the source code, the bug (and it is a bug) lies in the fact that ToolStripManager, which sets up a message filter to catch mouse-clicks outside the active ToolStrip, checks if the click is within the bounds of a child window.

The problem is that it uses activeToolStrip.ClientRectangle to verify this, and does not check child windows of the window that was clicked. In the case of a ComboBox, the drop-down is a separate child window which floats above everything, and can actually be out of the bounds of the main combo window if the drop-down is large.

The relevant line is:

if (!activeToolStrip.ClientRectangle.Contains(pt.x, pt.y)) {

I have found another solution to temporarily disable the automatic close while the dropdown is open.

Ideally, you are supposed to use a ToolStripComboBox within a ToolStrip rather than just a bare ComboBox. However if you would like to just us a bare one, you can add events to call the relevant private methods to suspend and resume the message filter.

static class ToolStripComboBoxFilter
{
    private static Action SuspendMenuMode = (Action) typeof(ToolStripManager)
        .GetNestedType("ModalMenuFilter", BindingFlags.NonPublic)
        .GetMethod(nameof(SuspendMenuMode), BindingFlags.NonPublic | BindingFlags.Static)
        .CreateDelegate(typeof(Action));

    private static Action ResumeMenuMode = (Action)typeof(ToolStripManager)
        .GetNestedType("ModalMenuFilter", BindingFlags.NonPublic)
        .GetMethod(nameof(ResumeMenuMode), BindingFlags.NonPublic | BindingFlags.Static)
        .CreateDelegate(typeof(Action));

    public static void AddToolStripFilterEvents(this ComboBox combo)
    {
        combo.DropDown += OnDropDown;
        combo.DropDownClosed += OnDropDownClosed;
    }

    private static void OnDropDown(object sender, EventArgs e)
    {
        SuspendMenuMode();
    }

    private static void OnDropDownClosed(object sender, EventArgs e)
    {
        ResumeMenuMode();
    }
}

You can use it like this

myComboBox.AddToolStripFilterEvents();
Charlieface
  • 52,284
  • 6
  • 19
  • 43