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:
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()
{
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);
base.BackColor = Color.Transparent;
_radius.BottomLeft = 2;
_radius.BottomRight = 2;
_radius.TopLeft = 2;
_radius.TopRight = 6;
this.Height = 21;
this.Width = 95;
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);
AdjustControls();
_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;
_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;
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);
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);
Rectangle rectOuter = rectContent;
rectOuter.Width -= 1;
rectOuter.Height -= 1;
GraphicsPath pathOuterBorder =
CreateRoundRectangle(rectOuter, Radius.TopLeft,
Radius.TopRight, Radius.BottomRight,
Radius.BottomLeft);
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);
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);
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);
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();
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);
}
}
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.
History
- 13th November, 2008: Initial version