数据绑定多列组合框






4.90/5 (75投票s)
支持数据绑定的 OwnerDrawn 多列组合框类。
引言
我在工作中需要编写一个支持泛型数据绑定的多列组合框,并且我认为它对其他人也可能有用。MultiColumnComboBox
是一个完全用 C# 编写的、派生自ComboBox
的类,您可以将其绑定到任何具有多列的数据源(尽管只有一列的数据源也可以)。它也可以在非绑定模式下工作,但如果您不使用数据绑定,使用它就没有太大意义。
类用法
使用该类相当直接。一旦有了数据源,您只需设置MultiColumnComboBox
类的DataSource
属性即可。该控件不支持ComboBoxStyle.Simple
样式,并且它会坚持将DrawMode
设置为OwnerDrawVariable
。会抛出异常,这样您就不会无意中尝试打破这些限制,这非常有用,因为 Visual Studio 属性网格将不允许您更改这些值。
该类已在 Windows XP SP2 和 Windows Vista(Ultimate Edition)上进行了测试,包括从 Visual Studio 设计角度和运行时角度。以下是一些使用各种类型数据源填充控件的示例。在第一个示例中,我们使用DataTable
来填充它。
// Populate using a DataTable
DataTable dataTable = new DataTable("Employees");
dataTable.Columns.Add("Employee ID", typeof(String));
dataTable.Columns.Add("Name", typeof(String));
dataTable.Columns.Add("Designation", typeof(String));
dataTable.Rows.Add(new String[] { "D1", "Natalia", "Developer" });
dataTable.Rows.Add(new String[] { "D2", "Jonathan", "Developer" });
dataTable.Rows.Add(new String[] { "D3", "Jake", "Developer" });
dataTable.Rows.Add(new String[] { "D4", "Abraham", "Developer" });
dataTable.Rows.Add(new String[] { "T1", "Mary", "Team Lead" });
dataTable.Rows.Add(new String[] { "PM1", "Calvin", "Project Manager" });
dataTable.Rows.Add(new String[] { "T2", "Sarah", "Team Lead" });
dataTable.Rows.Add(new String[] { "D12", "Monica", "Developer" });
dataTable.Rows.Add(new String[] { "D13", "Donna", "Developer" });
multiColumnComboBox1.DataSource = dataTable;
multiColumnComboBox1.DisplayMember = "Employee ID";
multiColumnComboBox1.ValueMember = "Name";
DisplayMember
属性将决定组合框编辑框部分中可见的值。ValueMember
属性将决定哪一列将显示为粗体。如果您查看截图,可以看到这一点。在下一个示例中,我们使用自定义类型的数组。
public class Student
{
public Student(String name, int age)
{
this.name = name;
this.age = age;
}
String name;
public String Name
{
get { return name; }
}
int age;
public int Age
{
get { return age; }
}
}
// Populate using a collection
Student[] studentArray = new Student[]
{ new Student("Andrew White", 10), new Student("Thomas Smith", 10),
new Student("Alice Brown", 11), new Student("Lana Jones", 10),
new Student("Jason Smith", 9), new Student("Amamda Williams", 11)
};
multiColumnComboBox2.DataSource = studentArray;
multiColumnComboBox2.DisplayMember = multiColumnComboBox2.ValueMember = "Name";
请注意,我们将DisplayMember
和ValueMember
都设置为相同的列字段——这样做完全没问题。顺便说一句,如果您不设置ValueMember
,它将默认使用第一列。您必须设置DisplayMember
,否则您会看到一些奇怪的字符串,具体取决于特定类型的ToString
实现方式。我决定不提供默认值,因为它很可能导致显示非理想的列。在我的第三个示例中,我使用了下拉列表样式组合框,并且还使用了List<>
对象——但此时,任何阅读本文的人都应该很清楚,您基本上可以使用任何标准数据源。
// Drop-down list (non-editable)
List<Student> studentList = new List<Student>(studentArray);
使用下拉列表的主要区别在于,即使组合框未展开,您也会看到多个列。请注意,那些想要阻止此行为的人可以在OnDrawItem
方法中检查DrawItemEventArgs.State
是否具有ComboBoxEdit
标志,并相应地更改行为。就我们而言,这种行为相当好,我个人认为这是更直观的做法。最后,您可以在不进行数据绑定的情况下使用它,尽管我想不出任何您想这样做的理由。
// Trying to use as a regular combobox
multiColumnComboBox4.Items.Add("Cat");
multiColumnComboBox4.Items.Add("Tiger");
multiColumnComboBox4.Items.Add("Lion");
multiColumnComboBox4.Items.Add("Cheetah");
multiColumnComboBox4.SelectedIndex = 0;
实现细节
我首先隐藏了DrawMode
和DropDownStyle
属性,以防止用户无意中设置不支持的值。
public new DrawMode DrawMode
{
get
{
return base.DrawMode;
}
set
{
if (value != DrawMode.OwnerDrawVariable)
{
throw new NotSupportedException("Needs to be DrawMode.OwnerDrawVariable");
}
base.DrawMode = value;
}
}
public new ComboBoxStyle DropDownStyle
{
get
{
return base.DropDownStyle;
}
set
{
if (value == ComboBoxStyle.Simple)
{
throw new NotSupportedException("ComboBoxStyle.Simple not supported");
}
base.DropDownStyle = value;
}
}
我覆盖了OnDataSourceChanged
,以便可以初始化列名。
protected override void OnDataSourceChanged(EventArgs e)
{
base.OnDataSourceChanged(e);
InitializeColumns();
}
private void InitializeColumns()
{
PropertyDescriptorCollection propertyDescriptorCollection =
DataManager.GetItemProperties();
columnWidths = new float[propertyDescriptorCollection.Count];
columnNames = new String[propertyDescriptorCollection.Count];
for (int colIndex = 0; colIndex < propertyDescriptorCollection.Count; colIndex++)
{
String name = propertyDescriptorCollection[colIndex].Name;
columnNames[colIndex] = name;
}
}
我使用了DataManager
属性,该属性返回管理控件绑定对象的CurrencyManager
对象。最初,我还将宽度数组设置为 0(稍后将计算所需的宽度)。我还覆盖了OnValueMemberChanged
方法,以便我可以正确地在内部设置值成员列,我会在绘图代码中使用它来使值列以粗体文本显示。
protected override void OnValueMemberChanged(EventArgs e)
{
base.OnValueMemberChanged(e);
InitializeValueMemberColumn();
}
private void InitializeValueMemberColumn()
{
int colIndex = 0;
foreach (String columnName in columnNames)
{
if (String.Compare(columnName, ValueMember, true,
CultureInfo.CurrentUICulture) == 0)
{
valueMemberColumnIndex = colIndex;
break;
}
colIndex++;
}
}
OnMeasureItem
将为组合框中的每一行调用一次,我在这里进行宽度计算。
protected override void OnMeasureItem(MeasureItemEventArgs e)
{
base.OnMeasureItem(e);
if (DesignMode)
return;
for (int colIndex = 0; colIndex < columnNames.Length; colIndex++)
{
string item = Convert.ToString(
FilterItemOnProperty(Items[e.Index], columnNames[colIndex]));
SizeF sizeF = e.Graphics.MeasureString(item, Font);
columnWidths[colIndex] = Math.Max(columnWidths[colIndex], sizeF.Width);
}
float totWidth = CalculateTotalWidth();
e.ItemWidth = (int)totWidth;
}
这里的有趣技巧是使用FilterItemOnProperty
来获取与特定列关联的文本。宽度计算很简单,我使用CalculateTotalWidth
方法计算总宽度,该方法仅将所有单个列的宽度相加。我还为垂直滚动条添加了宽度(以防出现)。我们还必须记住覆盖OnDropDown
以相应地设置下拉宽度(请记住,这与组合框本身的宽度不同)。
protected override void OnDropDown(EventArgs e)
{
base.OnDropDown(e);
this.DropDownWidth = (int)CalculateTotalWidth();
}
现在我们来看看该类的核心——OnDrawItem
覆盖。
protected override void OnDrawItem(DrawItemEventArgs e)
{
base.OnDrawItem(e);
if (DesignMode)
return;
e.DrawBackground();
Rectangle boundsRect = e.Bounds;
int lastRight = 0;
using (Pen linePen = new Pen(SystemColors.GrayText))
{
using (SolidBrush brush = new SolidBrush(ForeColor))
{
if (columnNames.Length == 0)
{
e.Graphics.DrawString(Convert.ToString(Items[e.Index]),
Font, brush, boundsRect);
}
else
{
for (int colIndex = 0; colIndex < columnNames.Length; colIndex++)
{
string item = Convert.ToString(FilterItemOnProperty(
Items[e.Index], columnNames[colIndex]));
boundsRect.X = lastRight;
boundsRect.Width = (int)columnWidths[colIndex] + columnPadding;
lastRight = boundsRect.Right;
if (colIndex == valueMemberColumnIndex)
{
using (Font boldFont = new Font(Font, FontStyle.Bold))
{
e.Graphics.DrawString(item, boldFont, brush, boundsRect);
}
}
else
{
e.Graphics.DrawString(item, Font, brush, boundsRect);
}
if (colIndex < columnNames.Length - 1)
{
e.Graphics.DrawLine(linePen, boundsRect.Right, boundsRect.Top,
boundsRect.Right, boundsRect.Bottom);
}
}
}
}
}
e.DrawFocusRectangle();
}
尽管它是该类中最长的函数(并且可能超出 Marc Clifton 批准的单个方法最大行数限制),但它相当直接。对于每一行,它会遍历所有列,获取列文本,并绘制文本以及充当列分隔符的垂直线。
致谢
- Rama Krishna Vavilala - 为实现提供了一些出色的建议。
历史
- 2007 年 7 月 27 日 - 文章首次发布。