Click here to Skip to main content
Licence CPOL
First Posted 13 Nov 2008
Views 42,530
Downloads 2,571
Bookmarked 87 times

How to create a custom ComboBox from scratch

By | 13 Nov 2008 | Article
An article on creating a custom ComboBox control completely from scratch.

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:

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:

#region Constructor
public BNComboBox()
{
    //preperaing 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:

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:

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.

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:

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.

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.

License

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

About the Author

bbbnova

Software Developer

Bulgaria Bulgaria

Member

My job is completely different from programing jobs, but software development is my hobby. I am programming since 2001. I started with VB for applications (Excel) and soon went to VB6.0, c++ MFC, c# and VB .NET 1.1,2.0,3.0 and now I love .NET 3.5.

This is my HOME page.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
Questionclose droppedDown Pinmemberalireza_shamayeli9:09 24 May '12  
QuestionpopupControl Problem PinmemberMember 39046530:44 29 Sep '09  
GeneralImprovement Pinmemberbbbnova21:42 14 Nov '08  
GeneralGood work, but drop-down listbox needs more attention. PinmemberGlynn4:55 14 Nov '08  
GeneralScroll bar not customized Pinmember_Ratish_Philip_3:50 14 Nov '08  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web01 | 2.5.120517.1 | Last Updated 13 Nov 2008
Article Copyright 2008 by bbbnova
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid