Click here to Skip to main content
15,879,326 members
Articles / Multimedia / GDI+

Customizable ComboBox Drop-Down

Rate me:
Please Sign up or sign in to vote.
4.54/5 (59 votes)
20 Nov 2014MIT3 min read 353K   20.2K   199  
A combobox control with a customizable drop-down
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Windows.Forms;

namespace CustomComboBox
{
    public enum PopupResizeMode
    {
        None = 0,

        // Individual styles.
        Left = 1,
        Top = 2,
        Right = 4,
        Bottom = 8,

        // Combined styles.
        All = (Top | Left | Bottom | Right),
        TopLeft = (Top | Left),
        TopRight = (Top | Right),
        BottomLeft = (Bottom | Left),
        BottomRight = (Bottom | Right),
    }

    public enum GripAlignMode
    {
        TopLeft,
        TopRight,
        BottomLeft,
        BottomRight,
    }

    public sealed class GripRenderer
    {
        #region Construction and destruction

        private GripRenderer()
        {
        }

        #endregion

        #region Methods

        private static void InitializeGripBitmap(Graphics g, Size size, bool forceRefresh)
        {
            if (m_sGripBitmap == null || forceRefresh || size != m_sGripBitmap.Size)
            {
                // Draw size grip into a bitmap image.
                m_sGripBitmap = new Bitmap(size.Width, size.Height, g);
                using (Graphics gripG = Graphics.FromImage(m_sGripBitmap))
                    ControlPaint.DrawSizeGrip(gripG, SystemColors.ButtonFace, 0, 0, size.Width, size.Height);
            }
        }

        public static void RefreshSystemColors(Graphics g, Size size)
        {
            InitializeGripBitmap(g, size, true);
        }

        public static void Render(Graphics g, Point location, Size size, GripAlignMode mode)
        {
            InitializeGripBitmap(g, size, false);

            // Calculate display size and position of grip.
            switch (mode)
            {
                case GripAlignMode.TopLeft:
                    size.Height = -size.Height;
                    size.Width = -size.Width;
                    break;

                case GripAlignMode.TopRight:
                    size.Height = -size.Height;
                    break;

                case GripAlignMode.BottomLeft:
                    size.Width = -size.Height;
                    break;
            }

            // Reverse size grip for left-aligned.
            if (size.Width < 0)
                location.X -= size.Width;
            if (size.Height < 0)
                location.Y -= size.Height;

            g.DrawImage(GripBitmap, location.X, location.Y, size.Width, size.Height);
        }

        public static void Render(Graphics g, Point location, GripAlignMode mode)
        {
            Render(g, location, new Size(16, 16), mode);
        }

        #endregion

        #region Properties

        private static Bitmap GripBitmap
        {
            get { return m_sGripBitmap; }
        }

        #endregion

        #region Attributes

        private static Bitmap m_sGripBitmap;

        #endregion
    }

    public class PopupDropDown : ToolStripDropDown
    {
        #region Construction and destruction

        public PopupDropDown(bool autoSize)
        {
            AutoSize = autoSize;
            Padding = Margin = Padding.Empty;
        }

        #endregion

        #region ToolStripDropDown overrides

        protected override void OnClosing(ToolStripDropDownClosingEventArgs e)
        {
            Control hostedControl = GetHostedControl();
            if (hostedControl != null)
                hostedControl.SizeChanged -= hostedControl_SizeChanged;
            base.OnClosing(e);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            GripBounds = Rectangle.Empty;

            if (CompareResizeMode(PopupResizeMode.BottomLeft))
            {
                // Draw grip area at bottom-left of popup.
                e.Graphics.FillRectangle(SystemBrushes.ButtonFace, 1, Height - 16, Width - 2, 14);
                GripBounds = new Rectangle(1, Height - 16, 16, 16);
                GripRenderer.Render(e.Graphics, GripBounds.Location, GripAlignMode.BottomLeft);
            }
            else if (CompareResizeMode(PopupResizeMode.BottomRight))
            {
                // Draw grip area at bottom-right of popup.
                e.Graphics.FillRectangle(SystemBrushes.ButtonFace, 1, Height - 16, Width - 2, 14);
                GripBounds = new Rectangle(Width - 17, Height - 16, 16, 16);
                GripRenderer.Render(e.Graphics, GripBounds.Location, GripAlignMode.BottomRight);
            }
            else if (CompareResizeMode(PopupResizeMode.TopLeft))
            {
                // Draw grip area at top-left of popup.
                e.Graphics.FillRectangle(SystemBrushes.ButtonFace, 1, 1, Width - 2, 14);
                GripBounds = new Rectangle(1, 0, 16, 16);
                GripRenderer.Render(e.Graphics, GripBounds.Location, GripAlignMode.TopLeft);
            }
            else if (CompareResizeMode(PopupResizeMode.TopRight))
            {
                // Draw grip area at top-right of popup.
                e.Graphics.FillRectangle(SystemBrushes.ButtonFace, 1, 1, Width - 2, 14);
                GripBounds = new Rectangle(Width - 17, 0, 16, 16);
                GripRenderer.Render(e.Graphics, GripBounds.Location, GripAlignMode.TopRight);
            }
        }

        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);

            // When drop-down window is being resized by the user (i.e. not locked),
            // update size of hosted control.
            if (!m_lockedThisSize)
                RecalculateHostedControlLayout();
        }

        protected void hostedControl_SizeChanged(object sender, EventArgs e)
        {
            // Only update size of this container when it is not locked.
            if (!m_lockedHostedControlSize)
                ResizeFromContent(-1);
        }

        #endregion
        
        #region Methods
        
        public new void Show(int x, int y)
        {
            Show(x, y, -1, -1);
        }

        public void Show(int x, int y, int width, int height)
        {
            // If no hosted control is associated, this procedure is pointless!
            Control hostedControl = GetHostedControl();
            if (hostedControl == null)
                return;

            // Initially hosted control should be displayed within a drop down of 1x1, however
            // its size should exceed the dimensions of the drop-down.
            {
                m_lockedHostedControlSize = true;
                m_lockedThisSize = true;

                // Display actual popup and occupy just 1x1 pixel to avoid automatic reposition.
                Size = new Size(1, 1);
                base.Show(x, y);

                m_lockedHostedControlSize = false;
                m_lockedThisSize = false;
            }

            // Resize drop-down to fit its contents.
            ResizeFromContent(width);

            // If client area was enlarged using the minimum width paramater, then the hosted
            // control must also be enlarged.
            if (m_refreshSize)
                RecalculateHostedControlLayout();

            // If popup is overlapping the initial position then move above!
            if (y > Top && y <= Bottom)
            {
                Top = y - Height - (height != -1 ? height : 0);

                PopupResizeMode previous = ResizeMode;
                if (ResizeMode == PopupResizeMode.BottomLeft)
                    ResizeMode = PopupResizeMode.TopLeft;
                else if (ResizeMode == PopupResizeMode.BottomRight)
                    ResizeMode = PopupResizeMode.TopRight;

                if (ResizeMode != previous)
                    RecalculateHostedControlLayout();
            }

            // Assign event handler to control.
            hostedControl.SizeChanged += hostedControl_SizeChanged;
        }

        protected void ResizeFromContent(int width)
        {
            if (m_lockedThisSize)
                return;

            // Prevent resizing hosted control to 1x1 pixel!
            m_lockedHostedControlSize = true;

            // Resize from content again because certain information was not available before.
            Rectangle bounds = Bounds;
            bounds.Size = SizeFromContent(width);

            if (!CompareResizeMode(PopupResizeMode.None))
            {
                if (width > 0 && bounds.Width - 2 > width)
                    if (!CompareResizeMode(PopupResizeMode.Right))
                        bounds.X -= bounds.Width - 2 - width;
            }

            Bounds = bounds;

            m_lockedHostedControlSize = false;
        }

        protected void RecalculateHostedControlLayout()
        {
            if (m_lockedHostedControlSize)
                return;

            m_lockedThisSize = true;
            
            // Update size of hosted control.
            Control hostedControl = GetHostedControl();
            if (hostedControl != null)
            {
                // Fetch control bounds and adjust as necessary.
                Rectangle bounds = hostedControl.Bounds;
                if (CompareResizeMode(PopupResizeMode.TopLeft) || CompareResizeMode(PopupResizeMode.TopRight))
                    bounds.Location = new Point(1, 16);
                else
                    bounds.Location = new Point(1, 1);

                bounds.Width = ClientRectangle.Width - 2;
                bounds.Height = ClientRectangle.Height - 2;
                if (IsGripShown)
                    bounds.Height -= 16;

                if (bounds.Size != hostedControl.Size)
                    hostedControl.Size = bounds.Size;
                if (bounds.Location != hostedControl.Location)
                    hostedControl.Location = bounds.Location;
            }
            
            m_lockedThisSize = false;
        }

        public Control GetHostedControl()
        {
            if (Items.Count > 0)
            {
                ToolStripControlHost host = Items[0] as ToolStripControlHost;
                if (host != null)
                    return host.Control;
            }
            return null;
        }

        public bool CompareResizeMode(PopupResizeMode resizeMode)
        {
            return (ResizeMode & resizeMode) == resizeMode;
        }

        protected Size SizeFromContent(int width)
        {
            Size contentSize = Size.Empty;

            m_refreshSize = false;

            // Fetch hosted control.
            Control hostedControl = GetHostedControl();
            if (hostedControl != null)
            {
                if (CompareResizeMode(PopupResizeMode.TopLeft) || CompareResizeMode(PopupResizeMode.TopRight))
                    hostedControl.Location = new Point(1, 16);
                else
                    hostedControl.Location = new Point(1, 1);
                contentSize = SizeFromClientSize(hostedControl.Size);

                // Use minimum width (if specified).
                if (width > 0 && contentSize.Width < width)
                {
                    contentSize.Width = width;
                    m_refreshSize = true;
                }
            }

            // If a grip box is shown then add it into the drop down height.
            if (IsGripShown)
                contentSize.Height += 16;

            // Add some additional space to allow for borders.
            contentSize.Width += 2;
            contentSize.Height += 2;

            return contentSize;
        }

        #endregion

        #region Win32 message processing

        #region Win32 stuff

        protected const int WM_GETMINMAXINFO = 0x0024;
        protected const int WM_NCHITTEST = 0x0084;

        protected const int HTTRANSPARENT = -1;
        protected const int HTLEFT = 10;
        protected const int HTRIGHT = 11;
        protected const int HTTOP = 12;
        protected const int HTTOPLEFT = 13;
        protected const int HTTOPRIGHT = 14;
        protected const int HTBOTTOM = 15;
        protected const int HTBOTTOMLEFT = 16;
        protected const int HTBOTTOMRIGHT = 17;

        [StructLayout(LayoutKind.Sequential)]
        internal struct MINMAXINFO
        {
            public Point reserved;
            public Size maxSize;
            public Point maxPosition;
            public Size minTrackSize;
            public Size maxTrackSize;
        }

        protected static int HIWORD(int n)
        {
            return (n >> 16) & 0xffff;
        }
        protected static int HIWORD(IntPtr n)
        {
            return HIWORD(unchecked((int)(long)n));
        }
        protected static int LOWORD(int n)
        {
            return n & 0xffff;
        }
        protected static int LOWORD(IntPtr n)
        {
            return LOWORD(unchecked((int)(long)n));
        }

        #endregion

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        protected override void WndProc(ref Message m)
        {
            if (!ProcessGrip(ref m, false))
                base.WndProc(ref m);
        }

        /// <summary>
        /// Processes the resizing messages.
        /// </summary>
        /// <param name="m">The message.</param>
        /// <returns>true, if the WndProc method from the base class shouldn't be invoked.</returns>
        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        public bool ProcessGrip(ref Message m)
        {
            return ProcessGrip(ref m, true);
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        private bool ProcessGrip(ref Message m, bool contentControl)
        {
            if (ResizeMode != PopupResizeMode.None)
            {
                switch (m.Msg)
                {
                    case WM_NCHITTEST:
                        return OnNcHitTest(ref m, contentControl);

                    case WM_GETMINMAXINFO:
                        return OnGetMinMaxInfo(ref m);
                }
            }
            return false;
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        private bool OnGetMinMaxInfo(ref Message m)
        {
            Control hostedControl = GetHostedControl();
            if (hostedControl != null)
            {
                MINMAXINFO minmax = (MINMAXINFO)Marshal.PtrToStructure(m.LParam, typeof(MINMAXINFO));

                // Maximum size.
                if (hostedControl.MaximumSize.Width != 0)
                    minmax.maxTrackSize.Width = hostedControl.MaximumSize.Width;
                if (hostedControl.MaximumSize.Height != 0)
                    minmax.maxTrackSize.Height = hostedControl.MaximumSize.Height;

                // Minimum size.
                minmax.minTrackSize = new Size(32, 32);
                if (hostedControl.MinimumSize.Width > minmax.minTrackSize.Width)
                    minmax.minTrackSize.Width = hostedControl.MinimumSize.Width;
                if (hostedControl.MinimumSize.Height > minmax.minTrackSize.Height)
                    minmax.minTrackSize.Height = hostedControl.MinimumSize.Height;

                Marshal.StructureToPtr(minmax, m.LParam, false);
            }
            return true;
        }

        private bool OnNcHitTest(ref Message m, bool contentControl)
        {
            Point location = PointToClient(new Point(LOWORD(m.LParam), HIWORD(m.LParam)));
            IntPtr transparent = new IntPtr(HTTRANSPARENT);

            // Check for simple gripper dragging.
            if (GripBounds.Contains(location))
            {
                if (CompareResizeMode(PopupResizeMode.BottomLeft))
                {
                    m.Result = contentControl ? transparent : (IntPtr)HTBOTTOMLEFT;
                    return true;
                }
                else if (CompareResizeMode(PopupResizeMode.BottomRight))
                {
                    m.Result = contentControl ? transparent : (IntPtr)HTBOTTOMRIGHT;
                    return true;
                }
                else if (CompareResizeMode(PopupResizeMode.TopLeft))
                {
                    m.Result = contentControl ? transparent : (IntPtr)HTTOPLEFT;
                    return true;
                }
                else if (CompareResizeMode(PopupResizeMode.TopRight))
                {
                    m.Result = contentControl ? transparent : (IntPtr)HTTOPRIGHT;
                    return true;
                }
            }
            else   // Check for edge based dragging.
            {
                Rectangle rectClient = ClientRectangle;
                if (location.X > rectClient.Right - 3 && location.X <= rectClient.Right && CompareResizeMode(PopupResizeMode.Right))
                {
                    m.Result = contentControl ? transparent : (IntPtr)HTRIGHT;
                    return true;
                }
                else if (location.Y > rectClient.Bottom - 3 && location.Y <= rectClient.Bottom && CompareResizeMode(PopupResizeMode.Bottom))
                {
                    m.Result = contentControl ? transparent : (IntPtr)HTBOTTOM;
                    return true;
                }
                else if (location.X > -1 && location.X < 3 && CompareResizeMode(PopupResizeMode.Left))
                {
                    m.Result = contentControl ? transparent : (IntPtr)HTLEFT;
                    return true;
                }
                else if (location.Y > -1 && location.Y < 3 && CompareResizeMode(PopupResizeMode.Top))
                {
                    m.Result = contentControl ? transparent : (IntPtr)HTTOP;
                    return true;
                }
            }
            return false;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Type of resize mode, grips are automatically drawn at bottom-left and bottom-right corners.
        /// </summary>
        public PopupResizeMode ResizeMode
        {
            get { return m_resizeMode; }
            set
            {
                if (value != m_resizeMode)
                {
                    m_resizeMode = value;
                    Invalidate();
                }
            }
        }

        /// <summary>
        /// Bounds of active grip box position.
        /// </summary>
        protected Rectangle GripBounds
        {
            get { return this.m_gripBounds; }
            set { this.m_gripBounds = value; }
        }

        /// <summary>
        /// Indicates when a grip box is shown.
        /// </summary>
        protected bool IsGripShown
        {
            get
            {
                return (ResizeMode == PopupResizeMode.TopLeft || ResizeMode == PopupResizeMode.TopRight ||
                        ResizeMode == PopupResizeMode.BottomLeft || ResizeMode == PopupResizeMode.BottomRight);
            }
        }

        #endregion

        #region Attributes

        private PopupResizeMode m_resizeMode = PopupResizeMode.None;
        private Rectangle m_gripBounds = Rectangle.Empty;

        private bool m_lockedHostedControlSize = false;
        private bool m_lockedThisSize = false;
        private bool m_refreshSize = false;

        #endregion
    }

    public class PopupControl
    {
        #region Construction and destruction

        public PopupControl()
        {
            InitializeDropDown();
        }

        #endregion

        #region Event handlers

        private void m_dropDown_Closed(object sender, ToolStripDropDownClosedEventArgs e)
        {
            if (AutoResetWhenClosed)
                DisposeHost();
        }

        #endregion

        #region Events

        public event ToolStripDropDownClosingEventHandler Closing
        {
            add
            {
                m_dropDown.Closing += value;
            }
            remove
            {
                m_dropDown.Closing -= value;
            }
        }

        #endregion

        #region Methods

        public void Show(Control control, int x, int y)
        {
            Show(control, x, y, PopupResizeMode.None);
        }

        public void Show(Control control, int x, int y, PopupResizeMode resizeMode)
        {
            Show(control, x, y, -1, -1, resizeMode);
        }

        public void Show(Control control, int x, int y, int width, int height, PopupResizeMode resizeMode)
        {
            Size controlSize = control.Size;

            InitializeHost(control);

            m_dropDown.ResizeMode = resizeMode;
            m_dropDown.Show(x, y, width, height);

            control.Focus();
        }

        public void Hide()
        {
            if (m_dropDown != null && m_dropDown.Visible)
            {
                m_dropDown.Hide();
                DisposeHost();
            }
        }

        public void Reset()
        {
            DisposeHost();
        }

        #endregion

        #region Internal methods

        protected void DisposeHost()
        {
            if (m_host != null)
            {
                // Make sure host is removed from drop down.
                if (m_dropDown != null)
                    m_dropDown.Items.Clear();

                // Dispose of host.
                m_host = null;
            }
        }

        protected void InitializeHost(Control control)
        {
            InitializeDropDown();

            // If control is not yet being hosted then initialize host.
            if (control != Control)
                DisposeHost();

            // Create a new host?
            if (m_host == null)
            {
                m_host = new ToolStripControlHost(control);
                m_host.AutoSize = false;
                m_host.Padding = Padding;
                m_host.Margin = Margin;
            }

            // Add control to drop-down.
            m_dropDown.Items.Clear();
            m_dropDown.Padding = m_dropDown.Margin = Padding.Empty;
            m_dropDown.Items.Add(m_host);
        }

        protected void InitializeDropDown()
        {
            // Does a drop down exist?
            if (m_dropDown == null)
            {
                m_dropDown = new PopupDropDown(false);
                m_dropDown.Closed += new ToolStripDropDownClosedEventHandler(m_dropDown_Closed);
            }
        }

        #endregion

        #region Properties

        public bool Visible
        {
            get { return (m_dropDown != null && m_dropDown.Visible) ? true : false; }
        }

        public Control Control
        {
            get { return (m_host != null) ? m_host.Control : null; }
        }

        public Padding Padding
        {
            get { return this.m_padding; }
            set { this.m_padding = value; }
        }

        public Padding Margin
        {
            get { return this.m_margin; }
            set { this.m_margin = value; }
        }

        public bool AutoResetWhenClosed
        {
            get { return this.m_autoReset; }
            set { this.m_autoReset = value; }
        }

        #endregion

        #region Attributes

        private ToolStripControlHost m_host;
        private PopupDropDown m_dropDown;

        private Padding m_padding = Padding.Empty;
        private Padding m_margin = new Padding(1, 1, 1, 1);

        private bool m_autoReset = 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 MIT License


Written By
Software Developer Rotorz Limited
United Kingdom United Kingdom
I have been fascinated by software and video games since a young age when I was given my first computer, a Dragon 32. Since then I have experimented with numerous methods of development ranging from point-and-click type packages to C++. I soon realized that software development was what I wanted to do.

Having invested a lot of time into programming with various languages and technologies I now find it quite easy to pickup new ideas and methodologies. I relish learning new ideas and concepts.

Throughout my life I have dabbled in game and engine development. I was awarded a first for the degree "BEng Games and Entertainment Systems Software Engineering" at the University of Greenwich. It was good to finally experience video games from a more professional perspective.

Due to various family difficulties I was unable to immediately pursue any sort of software development career. This didn't stop me from dabbling though!

Since then I formed a company to focus upon client projects. Up until now the company has primarily dealt with website design and development. I have since decided that it would be fun to go back to my roots and develop games and tools that other developers can use for their games.

We have recently released our first game on iPhone/iPad called "Munchy Bunny!" (see: http://itunes.apple.com/us/app/munchy-bunny!/id516575993?mt=8). We hope to expand the game and release to additional platforms.

Also, check out our tile system extension for Unity! (see: http://rotorz.com/tilesystem/)

Comments and Discussions