У меня была эта проблема, и ответ от 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);
}
}
}