如何从头开始创建自定义 ComboBox
一篇关于完全从头开始创建自定义 ComboBox 控件的文章。
引言
几周前,我花了很多时间在网上搜索一个完全自定义的 ComboBox
,我想在我的一个应用程序中使用它。 我没有找到任何好看的免费的。 我不假装没有创建这种控件,但我的搜索失败促使我自己构建一个。 我包含的以下代码示例并不完全是我在我的应用程序中拥有的内容,但它是介绍如何创建自定义 combobox
的一个好起点。
它是如何工作的?
如果我们打开 MSDN 并稍微搜索一下,我们会发现 .NET ComboBox
扩展了 ListControl
类。 基本上,ComboBox
由一个 TextBox
和一个 ListBox
组成,后者以弹出窗口的形式显示在屏幕上。
所以我所做的就是实现 ListControl
类,并添加一个 textbox
和一个 listbox
,它们配备了适当的弹出控件,在 .NET 2.0 中。
这是类模式
为了使 combobox
能够正常运行,需要重写、重载或忽略某些方法和属性。 我不知道从哪里开始,但最好展示我的工作的基础章节,其余部分可以在附带的源代码中看到。
让我们从构造函数开始
#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
您可以在源代码中检查控件对齐函数。
为了捕获 combobox
上的某些事件,我声明了以下事件处理程序和委托
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
例如,这就是我调用 DrawItem
事件的方式
void _listBox_DrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index >= 0)
{
if (DrawItem != null)
{
DrawItem(this, e);
}
}
}
绘制控件
除了在构造函数中更改控件的样式外,还有许多其他属性和方法需要开发。
public new Color BackColor
{
get { return _backColor; }
set
{
this._backColor = value;
_textBox.BackColor = value;
Invalidate(true);
}
}
正如您在构造函数中看到的,我们将 BackColor
属性设置为 Transparent
,并且我们不再触摸它。 相反,我使用一个局部变量,并重载 base.BackColor
,在我们的例子中是 ListControl.BackColor
。
我还包括了四个颜色属性和一个 Radius
变量,这些变量可以在绘制代码中使用,以实现美观的外观。 接下来,我们必须添加一些鼠标功能,例如:处理鼠标抬起、鼠标按下、滚轮、进入、离开等。 因此,当接收到焦点、鼠标悬停或单击时,combobox
可以更改其视图。
最后,只需绘制 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();
}
下拉
另一个重要的是下拉功能。 控制 ListBox
弹出窗口的基本属性是 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);
}
}
还有其他属性和方法需要重写。 它们可以在附带的代码中看到。
Using the Code
使用代码就像使用基本的 ComboBox
控件一样简单。 有些属性没有像在原始 ComboBox
控件中那样实现,但我将其留给将来的开发。
历史
- 2008 年 11 月 13 日:初始版本