Click here to Skip to main content
15,891,184 members
Articles / Desktop Programming / Windows Forms

ComboBox with Suggestions Based on Loose Character Search

Rate me:
Please Sign up or sign in to vote.
4.78/5 (34 votes)
26 May 2014LGPL34 min read 152K   4.4K   42  
A ComboBox whose suggestion list is based on loose character search
// Copyright © Serge Weinstock 2014.
//
// This library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace SergeUtils
{
    /// <summary>
    /// A dropdown window for combos.
    /// </summary>
    [ToolboxItem(false)]
    public class DropdownControl : ToolStripDropDown, IMessageFilter
    {
        #region Properties
        /// <summary>
        /// Gets the content of the pop-up.
        /// </summary>
        public Control Content { get; private set; }
        #endregion

        #region Fields
        /// <summary>The toolstrip host</summary>
        private ToolStripControlHost m_host;
        /// <summary>The control for which we display this dropdown</summary>
        private Control m_opener;
        #endregion
        
        /// <summary>
        /// Constructor
        /// </summary>
        public DropdownControl(Control content)
        {
            if (content == null)
            {
                throw new ArgumentNullException("content");
            }
            Content = content;
            Content.Location = Point.Empty;
            m_host = new ToolStripControlHost(Content);
            // NB: AutoClose must be set to false, because otherwise the ToolStripManager would steal keyboard events
            AutoClose = false;
            // we do ourselves the sizing
            AutoSize = false;
            DoubleBuffered = true;
            ResizeRedraw = false;
            Padding = Margin = m_host.Padding = m_host.Margin = Padding.Empty;
            // we adjust the size according to the contents
            MinimumSize = Content.MinimumSize;
            content.MinimumSize = Content.Size;
            MaximumSize = Content.MaximumSize;
            content.MaximumSize = Content.Size;
            Size = Content.Size;
            TabStop = Content.TabStop = true;
            // set up the content
            Items.Add(m_host);
            // we must listen to mouse events for "emulating" AutoClose
            Application.AddMessageFilter(this);
        }

        /// <summary>
        /// Display the dropdown and adjust its size and location
        /// </summary>
        public void Show(Control opener, Size preferredSize = new Size())
        {
            if (opener == null)
            {
                throw new ArgumentNullException("opener");
            }

            m_opener = opener;

            int w = preferredSize.Width == 0 ? ClientRectangle.Width : preferredSize.Width;
            int h = preferredSize.Height == 0 ? Content.Height : preferredSize.Height;
            h += Padding.Size.Height + Content.Margin.Size.Height;

            Rectangle screen = Screen.FromControl(m_opener).WorkingArea;

            // let's try first to place it below the opener control
            Rectangle loc = m_opener.RectangleToScreen(
                new Rectangle(m_opener.ClientRectangle.Left, m_opener.ClientRectangle.Bottom,
                    m_opener.ClientRectangle.Left + w, m_opener.ClientRectangle.Bottom + h));
            Point cloc = new Point(m_opener.ClientRectangle.Left, m_opener.ClientRectangle.Bottom);
            if (!screen.Contains(loc))
            {
                // let's try above the opener control
                loc = m_opener.RectangleToScreen(
                    new Rectangle(m_opener.ClientRectangle.Left, m_opener.ClientRectangle.Top - h,
                        m_opener.ClientRectangle.Left + w, m_opener.ClientRectangle.Top));
                if (screen.Contains(loc))
                {
                    cloc = new Point(m_opener.ClientRectangle.Left, m_opener.ClientRectangle.Top-h);
                }
            }
            Width = w;
            Height = h;
            Show(m_opener, cloc, ToolStripDropDownDirection.BelowRight);
        }

        #region internals
        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (Content != null)
                {
                    Content.Dispose();
                    Content = null;
                }
                Application.RemoveMessageFilter(this);
            }
            base.Dispose(disposing);
        }

        /// <summary>
        /// On resizes, resize the contents
        /// </summary>
        protected override void OnSizeChanged(EventArgs e)
        {
            if (Content != null)
            {
                Content.MinimumSize = Size;
                Content.MaximumSize = Size;
                Content.Size = Size;
                Content.Location = Point.Empty;
            }
            base.OnSizeChanged(e);
        }
        #endregion

        #region IMessageFilter implementation
        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)]
        public static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, [In, Out] ref Point pt, int cPoints);

        /// <summary>
        /// We monitor all messages in order to detect when the users clicks outside the dropdown and the combo
        /// If this happens, we close the dropdown (as AutoClose is false)
        /// </summary>
        public bool PreFilterMessage(ref Message m)
        {
            if (Visible)
            {
                switch (m.Msg)
                {
                    case WM_LBUTTONDOWN:
                    case WM_RBUTTONDOWN:
                    case WM_MBUTTONDOWN:
                    case WM_NCLBUTTONDOWN:
                    case WM_NCRBUTTONDOWN:
                    case WM_NCMBUTTONDOWN:
                        int i = unchecked((int)(long)m.LParam);
                        short x = (short)(i & 0xFFFF);
                        short y = (short)((i >> 16) & 0xffff);
                        Point pt = new Point(x, y);
                        IntPtr srcWnd =
                            // client area: x, y are relative to the client area of the windows
                            (m.Msg == WM_LBUTTONDOWN) || (m.Msg == WM_RBUTTONDOWN) || (m.Msg == WM_MBUTTONDOWN) ?
                            m.HWnd :
                            // non-client area: x, y are relative to the desktop
                            IntPtr.Zero;
                        
                        MapWindowPoints(srcWnd, Handle, ref pt, 1);
                        if (!ClientRectangle.Contains(pt))
                        {
                            // the user has clicked outside the dropdown
                            pt = new Point(x, y);
                            MapWindowPoints(srcWnd, m_opener.Handle, ref pt, 1);
                            if (!m_opener.ClientRectangle.Contains(pt))
                            {
                                // the user has clicked outside the opener control
                                Close();
                            }
                        }
                        break;
                }
            }
            return false;
        }
        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Web Developer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions