Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

How to Create a Custom ComboBox from Scratch

4.81/5 (26 votes)
13 Nov 2008CPOL2 min read 134.6K   12.9K  
An article on creating a custom ComboBox control completely from scratch

Image 1

Introduction

A few weeks ago, I spent lots of time searching the web for a fully customized ComboBox which I could use in an application of mine. I didn't find any good looking free ones. I don't pretend that there is no such control created, but my search fault inspired me to build one myself. The following code example I have included is not exactly what I have in my application, but is a good point to introduce the way a custom combobox can be created.

How It Works?

If we open MSDN and search a little, we will find that the .NET ComboBox extends the ListControl class. Basically, the ComboBox consists of a TextBox and a ListBox which appears on the screen as a popup window.

So what I did was just implement the ListControl class and add a textbox and a listbox equipped with the appropriate popup controls, in .NET 2.0.

Here is the class schema:

CustComboBox/ComboBox2.jpg

There are certain methods and properties that need to be overridden, overloaded, or dismissed in order to achieve the proper function for the combobox. I don't know from where to start, but it will be better to show the basic chapters of my work, and the rest can be seen in the attached source code.

Let's start with the constructor:

C#
#region Constructor
public BNComboBox()
{
    //preparing the basic control behavior
    SetStyle(ControlStyles.AllPaintingInWmPaint, true);
    SetStyle(ControlStyles.ContainerControl, true);
    SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
    SetStyle(ControlStyles.ResizeRedraw, true);
    SetStyle(ControlStyles.Selectable, true);
    SetStyle(ControlStyles.SupportsTransparentBackColor, true);
    SetStyle(ControlStyles.UserMouse, true);
    SetStyle(ControlStyles.UserPaint, true);
    SetStyle(ControlStyles.Selectable, true);

    //setting some variables
    base.BackColor = Color.Transparent;
    _radius.BottomLeft = 2;
    _radius.BottomRight = 2;
    _radius.TopLeft = 2;
    _radius.TopRight = 6;

    this.Height = 21;
    this.Width = 95;

    //adjusting the component controls
    this.SuspendLayout();
    _textBox = new TextBox();
    _textBox.BorderStyle = System.Windows.Forms.BorderStyle.None;
    _textBox.Location = new System.Drawing.Point(3, 4);
    _textBox.Size = new System.Drawing.Size(60, 13);
    _textBox.TabIndex = 0;
    _textBox.WordWrap = false;
    _textBox.Margin = new Padding(0);
    _textBox.Padding = new Padding(0);
    _textBox.TextAlign = HorizontalAlignment.Left;
    this.Controls.Add(_textBox);
    this.ResumeLayout(false);

    //very important function that aligns the nested controls
    AdjustControls();

    //adjusting the component controls
    _listBox = new ListBox();
    _listBox.IntegralHeight = true;
    _listBox.BorderStyle = BorderStyle.FixedSingle;
    _listBox.SelectionMode = SelectionMode.One;
    _listBox.BindingContext = new BindingContext();

    _controlHost = new ToolStripControlHost(_listBox);
    _controlHost.Padding = new Padding(0);
    _controlHost.Margin = new Padding(0);
    _controlHost.AutoSize = false;

    _popupControl = new ToolStripDropDown();
    _popupControl.Padding = new Padding(0);
    _popupControl.Margin = new Padding(0);
    _popupControl.AutoSize = true;
    _popupControl.DropShadowEnabled = false;
    _popupControl.Items.Add(_controlHost);

    _dropDownWidth = this.Width;

    //exposing the listbox event handlers 
    //to the outer control - the combobox
    _listBox.MeasureItem += 
        new MeasureItemEventHandler(_listBox_MeasureItem);
    _listBox.DrawItem += new DrawItemEventHandler(_listBox_DrawItem);
    _listBox.MouseClick += new MouseEventHandler(_listBox_MouseClick);
    _listBox.MouseMove += new MouseEventHandler(_listBox_MouseMove);

    _popupControl.Closed += 
        new ToolStripDropDownClosedEventHandler(_popupControl_Closed);

    _textBox.Resize += new EventHandler(_textBox_Resize);
    _textBox.TextChanged += new EventHandler(_textBox_TextChanged);
}

#endregion

You can check the controls aligning function in the source.

In order to catch certain events on the combobox, I declared the following event handlers and delegates:

C#
public delegate void BNDroppedDownEventHandler
    (object sender, EventArgs e);
public delegate void BNDrawItemEventHandler
    (object sender, DrawItemEventArgs e);
public delegate void BNMeasureItemEventHandler
    (object sender, MeasureItemEventArgs e);
    
#region Delegates

[Category("Behavior"), 
    Description("Occurs when IsDroppedDown changes to True.")]
public event BNDroppedDownEventHandler DroppedDown;

[Category("Behavior"), 
    Description("Occurs when the SelectedIndex property changes.")]
public event EventHandler SelectedIndexChanged;

[Category("Behavior"), 
    Description("Occurs when an item/area needs to be painted.")]
public event BNDrawItemEventHandler DrawItem;

[Category("Behavior"), 
    Description("Occurs when an item's height needs to be calculated.")]
public event BNMeasureItemEventHandler MeasureItem;

#endregion

This is how I call the DrawItem event, for example:

C#
void _listBox_DrawItem(object sender, DrawItemEventArgs e)
{
    if (e.Index >= 0)
    {
        if (DrawItem != null)
        {
            DrawItem(this, e);
        }
    }
}

Painting the Control

Except changing the style of the control in the constructor, there are a number of other properties and methods to develop.

C#
public new Color BackColor
{
    get { return _backColor; }
    set 
    { 
        this._backColor = value;
        _textBox.BackColor = value;
        Invalidate(true);
    }
}

As you saw in the constructor, we set the BackColor property to Transparent, and we don't touch it any more. Instead, I use a local variable, and overload the base.BackColor, which in our case is ListControl.BackColor.

I also included four color properties and a Radius variable which can be used in the painting code to achieve a nice look and feel. Next, we have to add some mouse functionality like: handling the mouse-up, mouse-down, wheel, enter, leave, etc. Thus, the combobox can change its view when receiving focus, mouse hover, or click.

Finally, just paint the portions of the combobox:

C#
protected override void OnPaint(PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

    //content border
    Rectangle rectCont = rectContent;
    rectCont.X += 1;
    rectCont.Y += 1;
    rectCont.Width -= 3;
    rectCont.Height -= 3;
    GraphicsPath pathContentBorder = 
        CreateRoundRectangle(rectCont, Radius.TopLeft, Radius.TopRight, 
            Radius.BottomRight, Radius.BottomLeft);

    //button border
    Rectangle rectButton = rectBtn;
    rectButton.X += 1;
    rectButton.Y += 1;
    rectButton.Width -= 3;
    rectButton.Height -= 3;
    GraphicsPath pathBtnBorder = 
        CreateRoundRectangle(rectButton, 0, 
            Radius.TopRight, Radius.BottomRight, 0);

    //outer border
    Rectangle rectOuter = rectContent;
    rectOuter.Width -= 1;
    rectOuter.Height -= 1;
    GraphicsPath pathOuterBorder = 
        CreateRoundRectangle(rectOuter, Radius.TopLeft, 
            Radius.TopRight, Radius.BottomRight,
            Radius.BottomLeft);

    //inner border
    Rectangle rectInner = rectContent;
    rectInner.X += 1;
    rectInner.Y += 1;
    rectInner.Width -= 3;
    rectInner.Height -= 3;
    GraphicsPath pathInnerBorder = 
        CreateRoundRectangle(rectInner, Radius.TopLeft, 
            Radius.TopRight, Radius.BottomRight,
            Radius.BottomLeft);

    //brushes and pens
    Brush brInnerBrush = new LinearGradientBrush(
        new Rectangle(rectInner.X,rectInner.Y,rectInner.Width,
            rectInner.Height+1), 
        (hovered || IsDroppedDown || ContainsFocus)?Color4:Color2, 
            Color.Transparent,
            LinearGradientMode.Vertical);
    Brush brBackground;
    if (this.DropDownStyle == ComboBoxStyle.DropDownList)
    {
        brBackground = new LinearGradientBrush(pathInnerBorder.GetBounds(), 
            Color.FromArgb(IsDroppedDown ? 100 : 255, Color.White), 
            Color.FromArgb(IsDroppedDown?255:100, BackColor),
            LinearGradientMode.Vertical);
    }
    else
    {
        brBackground = new SolidBrush(BackColor);
    }
    Pen penOuterBorder = new Pen(Color1, 0);
    Pen penInnerBorder = new Pen(brInnerBrush, 0);
    LinearGradientBrush brButtonLeft = 
        new LinearGradientBrush(rectBtn, Color1, Color2, 
            LinearGradientMode.Vertical);
    ColorBlend blend = new ColorBlend();
    blend.Colors = new Color[] 
        { Color.Transparent, Color2, Color.Transparent };
    blend.Positions = new float[] { 0.0f, 0.5f, 1.0f};
    brButtonLeft.InterpolationColors = blend;
    Pen penLeftButton = new Pen(brButtonLeft, 0);
    Brush brButton = 
        new LinearGradientBrush(pathBtnBorder.GetBounds(),
        Color.FromArgb(100, IsDroppedDown? Color2:Color.White),
            Color.FromArgb(100, IsDroppedDown ? Color.White : Color2),
            LinearGradientMode.Vertical);

    //draw
    e.Graphics.FillPath(brBackground, pathContentBorder);
    if (DropDownStyle != ComboBoxStyle.DropDownList)
    {
        e.Graphics.FillPath(brButton, pathBtnBorder);
    }
    e.Graphics.DrawPath(penOuterBorder, pathOuterBorder);
    e.Graphics.DrawPath(penInnerBorder, pathInnerBorder);

    e.Graphics.DrawLine(penLeftButton, rectBtn.Left + 1, 
        rectInner.Top+1, rectBtn.Left + 1, rectInner.Bottom-1);    

    //Glimph
    Rectangle rectGlimph = rectButton;
    rectButton.Width -= 4;
    e.Graphics.TranslateTransform(rectGlimph.Left + 
        rectGlimph.Width / 2.0f, 
        rectGlimph.Top + rectGlimph.Height / 2.0f);
    GraphicsPath path = new GraphicsPath();
    PointF[] points = new PointF[3];
    points[0] = new PointF(-6 / 2.0f, -3 / 2.0f);
    points[1] = new PointF(6 / 2.0f, -3 / 2.0f);
    points[2] = new PointF(0, 6 / 2.0f);
    path.AddLine(points[0], points[1]);
    path.AddLine(points[1], points[2]);
    path.CloseFigure();
    e.Graphics.RotateTransform(0);

    SolidBrush br = new SolidBrush(Enabled?Color.Gray:Color.Gainsboro);
    e.Graphics.FillPath(br, path);
    e.Graphics.ResetTransform();
    br.Dispose();
    path.Dispose();

    //text
    if (DropDownStyle == ComboBoxStyle.DropDownList)
    {
        StringFormat sf  = new StringFormat(StringFormatFlags.NoWrap);
        sf.Alignment = StringAlignment.Near;

        Rectangle rectText = _textBox.Bounds;
        rectText.Offset(-3, 0);

        SolidBrush foreBrush = new SolidBrush(ForeColor);
        if (Enabled)
        {
            e.Graphics.DrawString(_textBox.Text, this.Font, 
                foreBrush, rectText.Location);
        }
        else
        {
            ControlPaint.DrawStringDisabled(e.Graphics, _textBox.Text, 
                Font, BackColor, rectText, sf);
        }
    }
    /*
    Dim foreBrush As SolidBrush = New SolidBrush(color)
    If (enabled) Then
        g.DrawString(text, font, foreBrush, rect, sf)
    Else
        ControlPaint.DrawStringDisabled(g, text, font, backColor, _
             rect, sf)
    End If
    foreBrush.Dispose()*/

    pathContentBorder.Dispose();
    pathOuterBorder.Dispose();
    pathInnerBorder.Dispose();
    pathBtnBorder.Dispose();

    penOuterBorder.Dispose();
    penInnerBorder.Dispose();
    penLeftButton.Dispose();

    brBackground.Dispose();
    brInnerBrush.Dispose();
    brButtonLeft.Dispose();
    brButton.Dispose();
}

Drop Down

Another important thing is the drop down functions. The basic property which controls the ListBox popup is IsDroppedDown.

C#
public bool IsDroppedDown
{
    get { return _isDroppedDown; }
    set 
    {
        if (_isDroppedDown == true && value == false )
        {
            if (_popupControl.IsDropDown)
            {
                _popupControl.Close();
            }
        }

        _isDroppedDown = value;

        if (_isDroppedDown)
        {
            _controlHost.Control.Width = _dropDownWidth;

            _listBox.Refresh();

            if (_listBox.Items.Count > 0) 
            {
                int h = 0;
                int i = 0;
                int maxItemHeight = 0;
                int highestItemHeight = 0;
                foreach(object item in _listBox.Items)
                {
                    int itHeight = _listBox.GetItemHeight(i);
                    if (highestItemHeight < itHeight) 
                    {
                        highestItemHeight = itHeight;
                    }
                    h = h + itHeight;
                    if (i <= (_maxDropDownItems - 1)) 
                    {
                        maxItemHeight = h;
                    }
                    i = i + 1;
                }

                if (maxItemHeight > _dropDownHeight)
                    _listBox.Height = _dropDownHeight + 3;
                else
                {
                    if (maxItemHeight > highestItemHeight )
                        _listBox.Height = maxItemHeight + 3;
                    else
                        _listBox.Height = highestItemHeight + 3;
                }
            }
            else
            {
                _listBox.Height = 15;
            }

            _popupControl.Show(this, CalculateDropPosition(), 
                ToolStripDropDownDirection.BelowRight);
        }

        Invalidate();
        if (_isDroppedDown)
            OnDroppedDown(this, EventArgs.Empty);
    }
}

There are still other properties and methods that need to be overridden. They can be seen in the attached code.

Using the Code

Using the code is as simple as using the basic ComboBox control. There are some properties that are not implemented like in the original ComboBox control, but I leave it for future development.

History

  • 13th November, 2008: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)