如何使自定义ComboBox(OwnerDrawFixed)看起来像标准的ComboBox 3D?

时间:2011-05-03 03:05:31

标签: vb.net visual-studio-2010 combobox custom-controls ondrawitem

我正在制作一个自定义的ComboBox,继承自Winforms的标准ComboBox。对于我的自定义ComboBox,我将DrawMode设置为OwnerDrawFixed,将DropDownStyle设置为DropDownList。然后我编写自己的OnDrawItem方法。但我这样结束了:

Standard vs Custom ComboBoxes

如何让我的自定义组合框看起来像标准组合?


更新1:ButtonRenderer

在四处搜索之后,我找到了ButtonRenderer class。它提供了DrawButton静态/共享方法 - 顾名思义 - 绘制正确的3D按钮。我现在正在试验它。


更新2:什么覆盖了我的控件?

我尝试使用我能想到的各种对象的图形属性,但我总是失败。最后,我尝试了表单的图形,显然有些东西覆盖了我的按钮。

以下是代码:

Protected Overrides Sub OnDrawItem(ByVal e As System.Windows.Forms.DrawItemEventArgs)
  Dim TextToDraw As String = _DefaultText
  __Brush_Window.Color = Color.FromKnownColor(KnownColor.Window)
  __Brush_Disabled.Color = Color.FromKnownColor(KnownColor.GrayText)
  __Brush_Enabled.Color = Color.FromKnownColor(KnownColor.WindowText)
  If e.Index >= 0 Then
    TextToDraw = _DataSource.ItemText(e.Index)
  End If
  If TextToDraw.StartsWith("---") Then TextToDraw = StrDup(3, ChrW(&H2500)) ' U+2500 is "Box Drawing Light Horizontal"
  If (e.State And DrawItemState.ComboBoxEdit) > 0 Then
    'ButtonRenderer.DrawButton(e.Graphics, e.Bounds, VisualStyles.PushButtonState.Default)
  Else
    e.DrawBackground()
  End If
  With e
    If _IsEnabled(.Index) Then
      .Graphics.DrawString(TextToDraw, Me.Font, __Brush_Enabled, .Bounds.X, .Bounds.Y)
    Else
      '.Graphics.FillRectangle(__Brush_Window, .Bounds)
      .Graphics.DrawString(TextToDraw, Me.Font, __Brush_Disabled, .Bounds.X, .Bounds.Y)
    End If
  End With
  TextToDraw = Nothing
  ButtonRenderer.DrawButton(Me.Parent.CreateGraphics, Me.ClientRectangle, VisualStyles.PushButtonState.Default)

  'MyBase.OnDrawItem(e)
End Sub

这是结果:

Overwritten ButtonRenderer

Me.Parent.CreateGraphics替换e.Graphics让我这样:

Clipped ButtonRenderer

执行上述操作+用Me.ClientRectangle替换e.Bounds让我知道了这一点:

Shrunk ButtonRenderer

有人能指出我的我必须用于ButtonRenderer.DrawButton方法吗?

PS:蓝色边框是由于我使用PushButtonState.Default而不是PushButtonState.Normal


我找到了答案! (见下文)

2 个答案:

答案 0 :(得分:7)

我忘记了找到答案的地方......当我记得的时候,我会编辑这个答案。

但显然,我需要设置Systems.Windows.Forms.ControlStyles标志。特别是ControlStyles.UserPaint标志。

所以,我的New()现在看起来像这样:

Private _ButtonArea as New Rectangle

Public Sub New()
  ' This call is required by the designer.
  InitializeComponent()
  ' Add any initialization after the InitializeComponent() call.
  MyBase.SetStyle(ControlStyles.Opaque Or ControlStyles.UserPaint, True)
  MyBase.DrawMode = Windows.Forms.DrawMode.OwnerDrawFixed
  MyBase.DropDownStyle = ComboBoxStyle.DropDownList
  ' Cache the button's modified ClientRectangle (see Note)
  With _ButtonArea
    .X = Me.ClientRectangle.X - 1
    .Y = Me.ClientRectangle.Y - 1
    .Width = Me.ClientRectangle.Width + 2
    .Height = Me.ClientRectangle.Height + 2
  End With
End Sub

现在我可以加入OnPaint事件:

Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
  If Me.DroppedDown Then
    ButtonRenderer.DrawButton(Me.CreateGraphics, _ButtonArea, VisualStyles.PushButtonState.Pressed)
  Else
    ButtonRenderer.DrawButton(Me.CreateGraphics, _ButtonArea, VisualStyles.PushButtonState.Normal)
  End If
  MyBase.OnPaint(e)
End Sub

注意: 是的,_ButtonArea矩形必须向所有方向放大1个像素(向上,向下,向左) ,右),否则ButtonRenderer周围会有一个1像素的“周长”,显示垃圾。让我疯了一会儿,直到我读到I must enlarge the Control's rect for ButtonRenderer.

答案 1 :(得分:1)

我自己遇到了这个问题,reply pepoluan让我开始了。我仍然认为有一些东西是缺少的,以便获得一个外观和行为类似于标准ComboBox的ComboBox,但DropDownStyle = DropDownList。

<强> DropDownArrow
我们还需要绘制DropDownArrow。我玩过ComboBoxRenderer,但它在下拉箭头区域周围绘制了一个黑色边框,因此不起作用。

我的最终解决方案是简单地绘制一个类似的箭头并将其渲染到OnPaint方法的按钮上。


热门项目行为
我们还需要确保我们的ComboBox具有类似于标准ComboBox的热门项目行为。我不知道任何简单可靠的方法来了解鼠标何时不再高于控件。因此,我建议使用一个Timer来检查每个滴答是否鼠标仍在控制之上。

修改的 刚刚添加了一个KeyUp事件处理程序,以确保在使用键盘进行选择时控件将正确更新。还对文本的呈现位置进行了微小的修正,以确保它更类似于香草组合框的文本定位。


下面是我自定义的ComboBox的完整代码。它允许您在每个项目上显示图像,并始终以DropDownList样式呈现,但希望应该很容易将代码容纳到您自己的解决方案中。

using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Design;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;

namespace CustomControls
{
    /// <summary>
    /// This is a special ComboBox that each item may conatins an image.
    /// </summary>
    public class ImageComboBox : ComboBox
    {
        private static readonly Size arrowSize = new Size(18, 20);


        private bool itemIsHot;

        /* Since properties such as SelectedIndex and SelectedItems may change when the mouser is hovering over items in the drop down list
         * we need a property that will store the item that has been selected by comitted selection so we know what to draw as the selected item.*/
        private object comittedSelection;

        private readonly ImgHolder dropDownArrow = ImgHolder.Create(ImageComboBox.DropDownArrow());

        private Timer hotItemTimer;

        public Font SelectedItemFont { get; set; }

        public Padding ImageMargin { get; set; }

        //
        // Summary:
        //     Gets or sets the path of the property to use as the image for the items
        //     in the System.Windows.Forms.ListControl.
        //
        // Returns:
        //     A System.String representing a single property name of the System.Windows.Forms.ListControl.DataSource
        //     property value, or a hierarchy of period-delimited property names that resolves
        //     to a property name of the final data-bound object. The default is an empty string
        //     ("").
        //
        // Exceptions:
        //   T:System.ArgumentException:
        //     The specified property path cannot be resolved through the object specified by
        //     the System.Windows.Forms.ListControl.DataSource property.
        [DefaultValue("")]
        [Editor("System.Windows.Forms.Design.DataMemberFieldEditor, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
        public string ImageMember { get; set; }


        public ImageComboBox()
        {
            base.SetStyle(ControlStyles.Opaque | ControlStyles.UserPaint, true);

            //All the elements in the control are drawn manually.
            base.DrawMode = DrawMode.OwnerDrawFixed;

            //Specifies that the list is displayed by clicking the down arrow and that the text portion is not editable. 
            //This means that the user cannot enter a new value. 
            //Only values already in the list can be selected.
            this.DropDownStyle = ComboBoxStyle.DropDownList;

            //using DrawItem event we need to draw item
            this.DrawItem += this.ComboBoxDrawItemEvent;

            this.hotItemTimer = new Timer();
            this.hotItemTimer.Interval = 250;
            this.hotItemTimer.Tick += this.HotItemTimer_Tick;

            this.MouseEnter += this.ImageComboBox_MouseEnter;

            this.KeyUp += this.ImageComboBox_KeyUp;

            this.SelectedItemFont = this.Font;
            this.ImageMargin = new Padding(4, 4, 5, 4);

            this.SelectionChangeCommitted += this.ImageComboBox_SelectionChangeCommitted;
            this.SelectedIndexChanged += this.ImageComboBox_SelectedIndexChanged;
        }


        private static Image DropDownArrow()
        {
            var arrow = new Bitmap(8, 4, PixelFormat.Format32bppArgb);

            using (Graphics g = Graphics.FromImage(arrow))
            {
                g.CompositingQuality = CompositingQuality.HighQuality;

                g.FillPolygon(Brushes.Black, ImageComboBox.CreateArrowHeadPoints());
            }

            return arrow;
        }

        private static PointF[] CreateArrowHeadPoints()
        {
            return new PointF[4] { new PointF(0, 0), new PointF(7F, 0), new PointF(3.5F, 3.5F), new PointF(0, 0) };
        }

        private static void DrawComboBoxItem(Graphics g, string text, Image image, Rectangle itemArea, int itemHeight, int itemWidth, Padding imageMargin
            , Brush brush, Font font)
        {
            if (image != null)
            {
                // recalculate margins so image is always approximately vertically centered
                int extraImageMargin = itemHeight - image.Height;

                int imageMarginTop = Math.Max(imageMargin.Top, extraImageMargin / 2);
                int imageMarginBotttom = Math.Max(imageMargin.Bottom, extraImageMargin / 2);

                g.DrawImage(image, itemArea.X + imageMargin.Left, itemArea.Y + imageMarginTop, itemHeight, itemHeight - (imageMarginBotttom
                    + imageMarginTop));
            }

            const double TEXT_MARGIN_TOP_PROPORTION = 1.1;
            const double TEXT_MARGIN_BOTTOM_PROPORTION = 2 - TEXT_MARGIN_TOP_PROPORTION;

            int textMarginTop = (int)Math.Round((TEXT_MARGIN_TOP_PROPORTION * itemHeight - g.MeasureString(text, font).Height) / 2.0, 0);
            int textMarginBottom = (int)Math.Round((TEXT_MARGIN_BOTTOM_PROPORTION * itemHeight - g.MeasureString(text, font).Height) / 2.0, 0);

            //we need to draw the item as string because we made drawmode to ownervariable
            g.DrawString(text, font, brush, new RectangleF(itemArea.X + itemHeight + imageMargin.Left + imageMargin.Right, itemArea.Y + textMarginTop
                , itemWidth, itemHeight - textMarginBottom));
        }


        private string GetDistplayText(object item)
        {
            if (this.DisplayMember == string.Empty) { return item.ToString(); }
            else
            {
                var display = item.GetType().GetProperty(this.DisplayMember).GetValue(item).ToString();

                return display ?? item.ToString();
            }

        }

        private Image GetImage(object item)
        {
            if (this.ImageMember == string.Empty) { return null; }
            else { return item.GetType().GetProperty(this.ImageMember).GetValue(item) as Image; }

        }

        private void ImageComboBox_SelectionChangeCommitted(object sender, EventArgs e)
        {
            this.comittedSelection = this.Items[this.SelectedIndex];
        }

        private void HotItemTimer_Tick(object sender, EventArgs e)
        {
            if (!this.RectangleToScreen(this.ClientRectangle).Contains(Cursor.Position)) { this.TurnOffHotItem(); }
        }

        private void ImageComboBox_KeyUp(object sender, KeyEventArgs e)
        {
            this.Invalidate();
        }

        private void ImageComboBox_MouseEnter(object sender, EventArgs e)
        {
            this.TurnOnHotItem();
        }

        private void ImageComboBox_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (!this.DroppedDown)
            {
                if (this.SelectedIndex > -1) { this.comittedSelection = this.Items[this.SelectedIndex]; }
                else { this.comittedSelection = null; }

            }
        }

        private void TurnOnHotItem()
        {
            this.itemIsHot = true;
            this.hotItemTimer.Enabled = true;
        }

        private void TurnOffHotItem()
        {
            this.itemIsHot = false;
            this.hotItemTimer.Enabled = false;
            this.Invalidate(this.ClientRectangle);
        }

        /// <summary>
        /// Draws overridden items.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ComboBoxDrawItemEvent(object sender, DrawItemEventArgs e)
        {
            //Draw backgroud of the item
            e.DrawBackground();
            if (e.Index != -1)
            {
                Brush brush;

                if (e.State.HasFlag(DrawItemState.Focus) || e.State.HasFlag(DrawItemState.Selected)) { brush = Brushes.White; }
                else { brush = Brushes.Black; }

                object item = this.Items[e.Index];

                ImageComboBox.DrawComboBoxItem(e.Graphics, this.GetDistplayText(item), this.GetImage(item), e.Bounds, this.ItemHeight, this.DropDownWidth
                    , new Padding(0, 1, 5, 1), brush, this.Font);
            }

        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

        // define the area of the control where we will write the text
        var topTextRectangle = new Rectangle(e.ClipRectangle.X - 1, e.ClipRectangle.Y - 1, e.ClipRectangle.Width + 2, e.ClipRectangle.Height + 2);

        using (var controlImage = new Bitmap(e.ClipRectangle.Width, e.ClipRectangle.Height, PixelFormat.Format32bppArgb))
        {
            using (Graphics ctrlG = Graphics.FromImage(controlImage))
            {
                /* Render the control. We use ButtonRenderer and not ComboBoxRenderer because we want the control to appear with the DropDownList style. */
                if (this.DroppedDown) { ButtonRenderer.DrawButton(ctrlG, topTextRectangle, PushButtonState.Pressed); }
                else if (this.itemIsHot) { ButtonRenderer.DrawButton(ctrlG, topTextRectangle, PushButtonState.Hot); }
                else { ButtonRenderer.DrawButton(ctrlG, topTextRectangle, PushButtonState.Normal); }


                // Draw item, if any has been selected
                if (this.comittedSelection != null)
                {
                    ImageComboBox.DrawComboBoxItem(ctrlG, this.GetDistplayText(this.comittedSelection), this.GetImage(this.comittedSelection)
                        , topTextRectangle, this.Height, this.Width - ImageComboBox.arrowSize.Width, this.ImageMargin, Brushes.Black, this.SelectedItemFont);
                }


                /* Now we need to draw the arrow. If we use ComboBoxRenderer for this job, it will display a distinct border around the dropDownArrow and we don't want that. As an alternative we define the area where the arrow should be drawn, and then procede to draw it. */
                var dropDownButtonArea = new RectangleF(topTextRectangle.X + topTextRectangle.Width - (ImageComboBox.arrowSize.Width
                            + this.dropDownArrow.Image.Width) / 2.0F, topTextRectangle.Y + topTextRectangle.Height - (topTextRectangle.Height
                            + this.dropDownArrow.Image.Height) / 2.0F, this.dropDownArrow.Image.Width, this.dropDownArrow.Image.Height);

                ctrlG.DrawImage(this.dropDownArrow.Image, dropDownButtonArea);

            }

            if (this.Enabled) { e.Graphics.DrawImage(controlImage, 0, 0); }
            else { ControlPaint.DrawImageDisabled(e.Graphics, controlImage, 0, 0, Color.Transparent); }

        }
        }
    }

internal struct ImgHolder
    {
        internal Image Image
        {
            get
            {
                return this._image ?? new Bitmap(1, 1); ;
            }
        }
        private Image _image;

        internal ImgHolder(Bitmap data)
        {
            _image = data;
        }
        internal ImgHolder(Image data)
        {
            _image = data;
        }

        internal static ImgHolder Create(Image data)
        {
            return new ImgHolder(data);
        }
        internal static ImgHolder Create(Bitmap data)
        {
            return new ImgHolder(data);
        }
    }

}