Как сделать пользовательский ComboBox (OwnerDrawFixed)похожим на стандартный ComboBox?

я делаю пользовательский ComboBox, унаследованный от стандартного ComboBox Winforms. Для моего пользовательского ComboBox я установил DrawMode to OwnerDrawFixed и DropDownStyle to DropDownList. Тогда я напишу свою собственную!--5--> метод. Но кончилось все так:--14-->

Standard vs Custom ComboBoxes

как сделать мой пользовательский ComboBox похожим на стандартный?


Обновление 1: ButtonRenderer

после поиска вокруг, я нашел ButtonRenderer класс. Он обеспечивает а DrawButton статический / общий метод, который-как следует из названия-рисует правильную кнопку 3D. Я сейчас с ним экспериментирую.


Update 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.По умолчанию вместо PushButtonState.Нормальный


Я Нашел Ответ! (см. ниже)

2 ответов


Я забыл, где я нашел ответ... Я отредактирую этот ответ, когда вспомню.

но, по-видимому, мне нужно установить 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-пиксельный "периметр", который показывает мусор. Какое-то время я сходил с ума, пока не прочитал это!--22-->я должен увеличить прямую кишку управления для ButtonRenderer.


У меня была эта проблема сама и ответ by pepoluan меня начал. Я все еще думаю, что несколько вещей отсутствуют, чтобы получить ComboBox с внешностью и поведением, похожими на стандартный ComboBox с DropDownStyle=DropDownList.

DropDownArrow
Нам также нужно нарисовать DropDownArrow. Я играл с ComboBoxRenderer, но он рисует темную границу вокруг области стрелки выпадающего списка, так что это не работа.

моим окончательным решением было просто нарисовать аналогичную стрелку и отобразить ее на кнопку в методе OnPaint.


Поведение Горячего Элемента
Нам также нужно обеспечить, чтобы наш ComboBox имел поведение горячего элемента, подобное стандартному ComboBox. Я не знаю простого и надежного способа узнать, когда мышь больше не находится над управлением. Поэтому я предлагаю использовать таймер, который проверяет при каждом ТИКе, находится ли мышь над управление.

редактировать Просто добавлен обработчик событий KeyUp, чтобы убедиться, что элемент управления будет обновляться правильно при выборе с помощью клавиатуры. Также сделана небольшая коррекция того, где был отрисован текст, чтобы убедиться, что он больше похож на текстовое позиционирование vanilla combobox.


Ниже приведен полный код моего настроенного 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);
        }
    }

}