9

Winforms の標準 ComboBox から継承したカスタム ComboBox を作成しています。カスタム ComboBox では、 と を に設定しDrawModeました。次に、独自のメソッドを記述します。しかし、私は次のようになりました:OwnerDrawFixedDropDownStyleDropDownListOnDrawItem

標準とカスタムのコンボボックス

カスタム ComboBox を標準の ComboBox のようにするにはどうすればよいですか?


更新 1: ButtonRenderer

あちこち探した後、ButtonRendererクラスを見つけました。DrawButton名前が示すように、適切な 3D ボタンを描画する静的/共有メソッドを提供します。私は今それを実験しています。


更新 2: コントロールを上書きするものは何ですか?

考えられるさまざまなオブジェクトの Graphics プロパティを使用してみましたが、常に失敗します。最後に、フォームのグラフィックスを試してみましたが、どうやらボタンが上書きされているようです。

コードは次のとおりです。

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

結果は次のとおりです。

上書きされた ButtonRenderer

に置き換えるMe.Parent.CreateGraphicsと、e.Graphics次のようになりました。

Clipped ButtonRenderer

そして、上記の + を次のように置き換えるMe.ClientRectangleと、e.Bounds次のようになりました。

縮小ボタンレンダラー

メソッドに使用する必要があるグラフィックスを教えてくれる人はいますか?ButtonRenderer.DrawButton

PS: 青みがかった境界線は、PushButtonState.Normal の代わりに PushButtonState.Default を使用しているためです。


答えが見つかりました!(下記参照)

4

2 に答える 2

2

私は自分でこの問題を抱えていましたが、pepoluan からの返信で始めました。ただし、DropDownStyle=DropDownList を使用した標準の ComboBox と同様の外観と動作を備えた ComboBox を取得するには、まだいくつかのことが欠けていると思います。

DropDownArrow DropDownArrow
も描画する必要があります。ComboBoxRenderer をいじってみましたが、ドロップダウン矢印の領域の周りに暗い境界線が描画されるため、機能しませんでした。

私の最終的な解決策は、単純に同様の矢印を描画し、それを OnPaint メソッドのボタンにレンダリングすることでした。


ホット アイテム
の動作 ComboBox が標準の ComboBox と同様のホット アイテムの動作をするようにする必要もあります。マウスがコントロールの上になくなったことを知るための簡単で信頼できる方法を私は知りません。したがって、各ティックでマウスがまだコントロール上にあるかどうかをチェックするタイマーを使用することをお勧めします。

編集 キーボードを使用して選択が行われたときにコントロールが正しく更新されるように、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);
        }
    }

}
于 2016-10-06T14:00:55.063 に答える