標準 ListBox 僅支援單一字型顏色、無圖示。透過 DrawMode.OwnerDrawFixed 並繼承 ListBox,可實作每項獨立圖示、前景色、字型,並封裝型別安全的集合類別。本文完整示範 ListBoxEx、ListBoxExItem、ListBoxExItemCollection 實作細節。
using System;
using System.Drawing;
using System.Windows.Forms;
public class ListBoxExItem
{
public string Text { get; set; }
public int ImageIndex { get; set; } = -1;
public Color ForeColor { get; set; } = SystemColors.WindowText;
public Font Font { get; set; } = null; // null 使用 ListBox 預設字型
public object Tag { get; set; } // 攜帶任意資料
public ListBoxExItem(string text) : this(text, -1, SystemColors.WindowText) { }
public ListBoxExItem(string text, int imageIndex) : this(text, imageIndex, SystemColors.WindowText) { }
public ListBoxExItem(string text, int imageIndex, Color foreColor)
{
Text = text;
ImageIndex = imageIndex;
ForeColor = foreColor;
}
public override string ToString() => Text; // 設計工具顯示用
}
public class ListBoxExItemCollection : IList, ICollection, IEnumerable
{
private readonly ListBox.ObjectCollection _innerList;
private readonly ListBoxEx _owner;
public ListBoxExItemCollection(ListBoxEx owner)
{
_owner = owner;
_innerList = owner.Items; // 繼承自 ListBox 的 ObjectCollection
}
// IList 實作:轉型為 ListBoxExItem
public ListBoxExItem this[int index]
{
get => _innerList[index] as ListBoxExItem;
set => _innerList[index] = value;
}
object IList.this[int index] { get => this[index]; set => this[index] = (ListBoxExItem)value; }
public int Add(ListBoxExItem item) => _innerList.Add(item);
public void Insert(int index, ListBoxExItem item) => _innerList.Insert(index, item);
public void Remove(ListBoxExItem item) => _innerList.Remove(item);
public void RemoveAt(int index) => _innerList.RemoveAt(index);
public void Clear() => _innerList.Clear();
public bool Contains(ListBoxExItem item) => _innerList.Contains(item);
public int IndexOf(ListBoxExItem item) => _innerList.IndexOf(item);
public void CopyTo(Array array, int index) => _innerList.CopyTo(array, index);
public IEnumerator GetEnumerator() => _innerList.GetEnumerator();
// ICollection
public int Count => _innerList.Count;
public bool IsSynchronized => _innerList.IsSynchronized;
public object SyncRoot => _innerList.SyncRoot;
public bool IsFixedSize => _innerList.IsFixedSize;
public bool IsReadOnly => _innerList.IsReadOnly;
// 給 ObjectCollection 用的轉型
int IList.Add(object value) => Add((ListBoxExItem)value);
bool IList.Contains(object value) => Contains((ListBoxExItem)value);
int IList.IndexOf(object value) => IndexOf((ListBoxExItem)value);
void IList.Insert(int index, object value) => Insert(index, (ListBoxExItem)value);
void IList.Remove(object value) => Remove((ListBoxExItem)value);
}
public class SelectedListBoxExItemCollection : IList, ICollection, IEnumerable
{
private readonly ListBox.SelectedObjectCollection _innerList;
public SelectedListBoxExItemCollection(ListBox.SelectedObjectCollection innerList)
{
_innerList = innerList;
}
public ListBoxExItem this[int index] => _innerList[index] as ListBoxExItem;
object IList.this[int index] { get => this[index]; set => throw new NotSupportedException(); }
public int Count => _innerList.Count;
public bool IsSynchronized => _innerList.IsSynchronized;
public object SyncRoot => _innerList.SyncRoot;
public bool IsFixedSize => true;
public bool IsReadOnly => true;
public bool Contains(ListBoxExItem item) => _innerList.Contains(item);
public int IndexOf(ListBoxExItem item) => _innerList.IndexOf(item);
public void CopyTo(Array array, int index) => _innerList.CopyTo(array, index);
public IEnumerator GetEnumerator() => _innerList.Cast().GetEnumerator();
// 唯讀集合,不支援修改
int IList.Add(object value) => throw new NotSupportedException();
void IList.Clear() => throw new NotSupportedException();
void IList.Insert(int index, object value) => throw new NotSupportedException();
void IList.Remove(object value) => throw new NotSupportedException();
void IList.RemoveAt(int index) => throw new NotSupportedException();
}
public class ListBoxEx : ListBox
{
private ImageList _imageList;
private ListBoxExItemCollection _itemsCollection;
public ListBoxEx()
{
// 關鍵:啟用 OwnerDraw
DrawMode = DrawMode.OwnerDrawFixed;
ItemHeight = 24; // 固定項目高度
_itemsCollection = new ListBoxExItemCollection(this);
}
// 覆寫 Items 屬性回傳型別安全集合
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public new ListBoxExItemCollection Items => _itemsCollection;
// 型別安全的 SelectedItem
public new ListBoxExItem SelectedItem => base.SelectedItem as ListBoxExItem;
// 型別安全的 SelectedItems
public new SelectedListBoxExItemCollection SelectedItems =>
new SelectedListBoxExItemCollection(base.SelectedItems);
// 圖示清單屬性
public ImageList ImageList
{
get => _imageList;
set
{
_imageList = value;
Invalidate(); // 圖示變更時重繪
}
}
// 核心繪製邏輯
protected override void OnDrawItem(DrawItemEventArgs e)
{
e.DrawBackground(); // 繪製背景 (含選取反白)
if (e.Index < 0 || e.Index >= Items.Count)
{
base.OnDrawItem(e);
return;
}
var item = Items[e.Index];
Rectangle bounds = e.Bounds;
int x = bounds.Left;
int y = bounds.Top;
// 1. 繪製圖示
if (_imageList != null && item.ImageIndex >= 0 && item.ImageIndex < _imageList.Images.Count)
{
int imgSize = Math.Min(_imageList.ImageSize.Width, bounds.Height);
_imageList.Draw(e.Graphics, x, y, imgSize, imgSize, item.ImageIndex);
x += imgSize + 2; // 圖示與文字間距
}
// 2. 繪製文字
using var brush = new SolidBrush(item.ForeColor);
using var font = item.Font ?? e.Font;
var textBounds = new Rectangle(x, y, bounds.Right - x, bounds.Height);
TextRenderer.DrawText(e.Graphics, item.Text, font, textBounds, item.ForeColor,
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis);
e.DrawFocusRectangle(); // 繪製焦點框
base.OnDrawItem(e);
}
// 支援鍵盤搜尋 (依 Text 屬性)
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.KeyCode >= Keys.A && e.KeyCode <= Keys.Z)
{
char c = char.ToLower((char)e.KeyCode);
FindItemByText(c.ToString());
}
}
private void FindItemByText(string prefix)
{
for (int i = 0; i < Items.Count; i++)
{
if (Items[i].Text.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
SelectedIndex = i;
break;
}
}
}
}
加入 DesignerSerializationVisibility(DesignerSerializationVisibility.Content) 讓 Visual Studio 設計工具能序列化 Items 集合:
// 在表單設計器生成的程式碼中會出現:
this.listBoxEx1.Items.Add(new ListBoxExItem("項目一", 0, Color.Red));
this.listBoxEx1.Items.Add(new ListBoxExItem("項目二", 1, Color.Blue));
public class DemoForm : Form
{
private ListBoxEx _listBox;
private ImageList _imgList;
public DemoForm()
{
_imgList = new ImageList { ImageSize = new Size(16, 16), ColorDepth = ColorDepth.Depth32Bit };
_imgList.Images.Add(Properties.Resources.IconFile); // 索引 0
_imgList.Images.Add(Properties.Resources.IconFolder); // 索引 1
_imgList.Images.Add(Properties.Resources.IconError); // 索引 2
_listBox = new ListBoxEx
{
Dock = DockStyle.Fill,
ImageList = _imgList,
Font = new Font("微軟正黑體", 10)
};
// 加入項目
_listBox.Items.Add(new ListBoxExItem("重要文件.docx", 0, Color.DarkRed));
_listBox.Items.Add(new ListBoxExItem("專案資料夾", 1, Color.Navy));
_listBox.Items.Add(new ListBoxExItem("錯誤記錄.log", 2, Color.DarkOrange));
_listBox.Items.Add(new ListBoxExItem("一般文字.txt", -1, Color.Black)); // 無圖示
_listBox.SelectedIndexChanged += (s, e) =>
{
if (_listBox.SelectedItem != null)
Text = $"已選取: {_listBox.SelectedItem.Text}";
};
this.Controls.Add(_listBox);
this.Text = "ListBoxEx 示範";
this.ClientSize = new Size(400, 300);
}
}
DrawMode.OwnerDrawVariable + 覆寫 OnMeasureItemOnDrawItem 繪製 CheckBoxRenderer.DrawCheckBox,處理 OnMouseDown 切換狀態ToolTip 控制項 + OnMouseMove 顯示項目 Tag 資訊IVirtualList 模式,按需建立項目AllowDrop=true + DragDrop 事件實作項目重新排序ListBoxEx 透過 OwnerDrawFixed 實現:
ListBoxExItem 封裝 Text/ImageIndex/ForeColor/Font/TagListBoxExItemCollection 包裝 ObjectCollection,提供強型別存取OnDrawItem 繪製圖示 + 多色文字 + 焦點框此模式可直接套用至 ComboBox、ListView、TreeView 等支援 OwnerDraw 的控制項。