65.9K
CodeProject 正在变化。 阅读更多。
Home

数据绑定多列组合框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (75投票s)

2007年7月27日

BSD

4分钟阅读

viewsIcon

366743

downloadIcon

12273

支持数据绑定的 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";

请注意,我们将DisplayMemberValueMember 都设置为相同的列字段——这样做完全没问题。顺便说一句,如果您不设置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;

实现细节

我首先隐藏了DrawModeDropDownStyle 属性,以防止用户无意中设置不支持的值。

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 日 - 文章首次发布。
© . All rights reserved.