65.9K
CodeProject 正在变化。 阅读更多。
Home

如何从头开始创建自定义 ComboBox

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (26投票s)

2008年11月13日

CPOL

2分钟阅读

viewsIcon

135332

downloadIcon

12952

一篇关于完全从头开始创建自定义 ComboBox 控件的文章。

引言

几周前,我花了很多时间在网上搜索一个完全自定义的 ComboBox,我想在我的一个应用程序中使用它。 我没有找到任何好看的免费的。 我不假装没有创建这种控件,但我的搜索失败促使我自己构建一个。 我包含的以下代码示例并不完全是我在我的应用程序中拥有的内容,但它是介绍如何创建自定义 combobox 的一个好起点。

它是如何工作的?

如果我们打开 MSDN 并稍微搜索一下,我们会发现 .NET ComboBox 扩展了 ListControl 类。 基本上,ComboBox 由一个 TextBox 和一个 ListBox 组成,后者以弹出窗口的形式显示在屏幕上。

所以我所做的就是实现 ListControl 类,并添加一个 textbox 和一个 listbox,它们配备了适当的弹出控件,在 .NET 2.0 中。

这是类模式

CustComboBox/ComboBox2.jpg

为了使 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 日:初始版本
© . All rights reserved.