Windows Forms 2.0 数据绑定:使用 .NET 进行智能客户端数据应用程序编程






4.85/5 (65投票s)
2006年2月14日
114分钟阅读

515810
第6章:使用 DataGridView 控件呈现数据。
| 
 | 
 | 
目录
- 引言
- DataGridView 概述
- 使用 DataGridView 进行基本数据绑定
- 控制网格中数据的修改
- 以编程方式构建 DataGridView
- 使用非绑定列自定义列内容
- 在虚拟模式下显示计算数据
- 使用内置列类型
- 内置标题单元格
- 处理网格数据编辑
- 自动列大小调整
- 列和行冻结
- 使用设计器定义网格
- 列重排序
- 定义自定义列和单元格类型
- 利用面向单元格的网格功能
- 使用样式进行格式化
- 我们现在在哪里?
引言
前面的章节展示了许多将数据绑定到简单绑定控件和列表绑定控件的详细示例。然而,呈现数据最常见的方式之一是使用表格形式。当数据以表格形式呈现时,用户能够快速地扫描和直观地理解大量数据。此外,用户可以通过多种方式与数据交互,包括滚动数据、根据列对数据进行排序、直接在网格中编辑数据以及选择列、行或单元格。在 .NET 1.0 中,DataGrid 控件是用于呈现表格数据的主要 Windows Forms 控件。尽管该控件功能强大,可以很好地呈现基本表格数据,但要自定义该控件的许多方面却相当困难。此外,DataGrid 控件没有向程序员公开足够的信息,以了解用户与网格的交互以及由于编程修改数据或格式化而导致的网格变化。由于这些因素以及客户要求的大量新功能,Microsoft 的 Windows 客户端团队决定在 .NET 2.0 中引入一个替代 DataGrid 的控件。这个新控件就是 DataGridView 控件,也是本章的重点。
DataGridView 概述
DataGridView 控件是一个功能非常强大、灵活且易于使用的控件,用于呈现表格数据。它比 DataGrid 控件功能更强大,也更容易自定义和交互。您可以通过适当设置控件上的数据绑定属性,让网格完成所有呈现表格数据的工作。您还可以通过非绑定列和虚拟模式等新功能来显式控制网格中数据的呈现。非绑定列允许您在单元格添加到网格时构建其内容。虚拟模式则提供了更高程度的控制,允许您等到单元格实际显示时再提供其包含的值。
您可以让网格像电子表格一样工作,这样交互和呈现的焦点就在单元格级别,而不是行或列级别。您只需在控件上设置几个属性,就可以精细地控制网格的格式和布局。最后,您可以插入多种预定义的列和单元格控件类型,或提供您自己的自定义控件,甚至可以在同一行或同一列的不同单元格中混合使用不同的控件类型。
图 6.1 展示了一个运行中的 DataGridView 控件示例,并突出显示了一些关键的视觉特性。您可以看到,该网格采用了 Windows XP 的视觉样式;它们与 .NET 2.0 中的许多 Windows Forms 控件非常相似。网格由列和行组成,列和行的交集是一个单元格。单元格是网格内呈现的基本单位,通过网格公开的属性和事件,其外观和行为可以高度自定义。行和列都有标题单元格,可用于维护网格中所呈现数据的上下文。这些标题单元格可以包含图形标志,以指示网格的不同模式或功能,例如排序、编辑、新行和选择。网格可以包含多种不同类型的单元格,如果网格未进行数据绑定,甚至可以在同一列中混合使用不同的单元格类型。

使用 DataGridView 进行基本数据绑定
开始使用 DataGridView 控件最简单的方法是在基本的数据绑定场景中使用它。为此,您首先需要获取一个数据集合,通常是通过您的业务层或数据访问层。然后,您设置网格的数据绑定属性以绑定到数据集合,如第 4 章和第 5 章所述。与其他的 Windows Forms 控件一样,在 .NET 2.0 中推荐的做法是始终将您的实际客户端数据源绑定到 BindingSource 类的实例,然后将您的控件绑定到该绑定源。以下代码展示了此过程:
private void OnFormLoad(object sender, EventArgs e) 
{
    // Create adapter to get data source
    CustomersTableAdapter adapter = new CustomersTableAdapter();
    // Bind table data source to binding source
    m_CustomersBindingSource.DataSource = adapter.GetData();
    // Bind the binding source to grid control
    m_Grid.DataSource = m_CustomersBindingSource; 
}
另外,如果绑定源绑定到一个数据集合的集合(例如数据集),那么您可以使用 DataMember 属性来精确指定要绑定的数据源部分:
private void OnFormLoad(object sender, EventArgs e) 
{
    // Create adapter to get data source
    CustomersTableAdapter adapter = new CustomersTableAdapter();
    // Get data set instance
    CustomersDataSet customers = new CustomersDataSet();
    // Fill data set
    adapter.Fill(customers);
    // Bind binding source to the data set
    m_CustomersBinding source.DataSource = customers;
    // Bind grid to the Customers table within the data source
    m_Grid.DataSource = m_CustomersBinding source;
    m_Grid.DataMember = "Customers";
}
在基本的数据绑定场景中,DataGridView 的功能与 .NET 1.0 中的 DataGrid 控件完全相同,不同之处在于 DataSource 和 DataMember 的组合必须解析为一个数据项集合,例如 DataTable 或对象集合。具体来说,它们需要解析为一个实现了 IList 接口的对象。
DataGrid 可以绑定到一个集合的集合,例如 DataSet,如果这样绑定,DataGrid 会呈现分层导航控件来遍历数据集合。然而,这个功能很少被使用,部分原因是 DataGrid 内部呈现的导航控件有些不直观,可能会让用户感到迷失。因此,开发 DataGridView 控件的 Windows 客户端团队决定不支持控件内的分层导航。DataGridView 被设计为一次只呈现一个数据集合。您仍然可以实现直观的数据分层导航,但这通常需要使用多个控件,采用前面章节讨论过的主从方法。
DataSource 属性可以设置为任何实现了以下四个接口之一的对象集合:IList、IListSource、IBindingList 或 IBindingListView。(这些接口将在第 7 章中详细讨论。)如果数据源本身是一个数据集合的集合,例如数据集或 IListSource 的实现者,则 DataMember 属性必须标识该源中的哪个数据集合要进行绑定。如果 DataSource 属性设置为 IList 的实现者(IBindingList 和 IBindingListView 都派生自它),则 DataMember 属性可以为 null(默认值)。当您将 DataGridView 绑定到一个绑定源时,BindingSource 类本身实现了 IBindingListView(以及其他几个与数据绑定相关的接口),因此您实际上可以通过绑定源将网格绑定到绑定源可以处理的任何类型的集合,包括仅实现 IEnumerable 的简单集合。
每当设置 DataSource 和/或 DataMember 属性时,网格都会遍历数据集合中的项,并刷新网格的数据绑定列。如果网格绑定到一个绑定源,绑定源所绑定的底层数据源的任何更改也会导致网格中的数据绑定列被刷新。这是因为当绑定源的底层集合发生变化时,它会向所有绑定的控件引发事件。
与 DataGridView 控件上的大多数属性一样,每当设置 DataSource 和 DataMember 属性时,它们会分别触发 DataSourceChanged 和 DataMemberChanged 事件。这使您可以挂接代码来响应网格上已更改的数据绑定。您还可以响应 DataBindingComplete 事件,因为它将在数据源或数据成员已更改并且数据绑定已更新后触发。但是,如果您试图监视数据源中的更改,通常最好监视 BindingSource 组件上的相应事件,而不是订阅网格本身的事件。如果用于处理事件的代码影响窗体上的其他控件,这一点尤其重要。因为您应该尽可能地将控件绑定到绑定源而不是数据源本身,所以绑定源是监视数据源更改的最佳位置。
控制网格中数据的修改
DataGridView 控件允许您明确控制用户是否可以在网格中编辑、删除或添加行。在网格填充数据后,用户可以通过多种方式与网格中呈现的数据进行交互,如前所述。默认情况下,这些交互包括编辑行中单元格(字段)的内容,选择一行并使用Delete键将其删除,或使用显示为网格最后一行的空行添加新行。
如果您想禁止这些交互中的任何一种,可以将 AllowUserToAddRows 或 AllowUserToDeleteRows 属性设置为 false,或者将 ReadOnly 属性设置为 true,分别对应添加、删除或编辑操作。每当这些属性的值被设置时,它们也会引发相应的 XXXChanged 属性更改事件。当您支持添加、编辑或删除时,您可能需要处理某些额外的事件来接受非绑定行或虚拟模式的新值,这将在本章后面描述。
以编程方式构建 DataGridView
使用网格最常见的方式是使用数据绑定列。当您绑定数据时,网格会根据数据项的模式或属性创建列,并为绑定集合中找到的每个数据项在网格中生成行。如果数据绑定是使用设计器静态设置的(正如本书中大多数示例所做的那样),则网格中列的类型和属性在设计时就已设置。如果数据绑定是完全动态完成的,则 AutoGenerateColumns 属性默认为 true,因此列类型是根据绑定数据项的类型动态确定的。当处理只包含非绑定数据的网格时,您可能希望以编程方式创建和填充网格。为了知道需要编写什么代码,您需要更深入地了解 DataGridView 对象模型。
首先要认识到的是,与所有 .NET 控件一样,窗体上的网格只是一个类的实例。该类包含可用于对其包含的对象模型进行编码的属性和方法。对于 DataGridView 控件,对象模型包括两个集合——Columns 和 Rows——它们包含构成网格的对象。这些对象是单元格,或者更具体地说,是派生自 DataGridViewCell 实例的对象。Columns 集合包含 DataGridViewColumn 对象的实例,而 Rows 集合包含 DataGridViewRows 的实例。
以编程方式向网格添加列
有多种方法可以以编程方式向网格添加列和行。第一步是定义构成网格的列。要定义一个列,您必须指定该列所基于的单元格模板。当向网格添加行时,该单元格模板将默认用于该列中的单元格。单元格模板是派生自 DataGridViewCell 类的实例。您可以使用 .NET 内置的单元格类型将列呈现为文本框、按钮、复选框、组合框、超链接和图像。另一种内置单元格类型用于呈现网格中的列标题。对于每种单元格类型,都有一个相应的列类型,旨在包含该单元格类型。您可以构造提供单元格类型作为模板的 DataGridViewColumn 实例,但通常情况下,您会创建一个派生列类型的实例,该实例旨在包含您想要使用的特定类型的单元格。此外,您还可以定义自己的自定义单元格和列类型(本章稍后讨论)。
现在,让我们坚持使用最常见和最简单的单元格类型,即 DataGridViewTextBoxCell——文本框单元格。这也恰好是默认的单元格类型。您可以通过以下三种方式以编程方式添加文本框列:
- 使用网格 Columns集合上Add方法的重载版本:// Just specify the column name and header text m_Grid.Columns.Add("MyColumnName", "MyColumnHeaderText"); 
- 获取一个 DataGridViewTextBoxColumn类的初始化实例。您可以通过构造一个DataGridViewTextBoxCell类的实例并将其传递给DataGridViewColumn的构造函数来实现,或者直接使用默认构造函数构造一个DataGridViewTextBoxColumn的实例。一旦列被构造,就将其添加到网格的Columns集合中:// Do this: DataGridViewTextBoxColumn newCol = new DataGridViewTextBoxColumn(); // Or this: DataGridViewTextBoxCell newCell = new DataGridViewTextBoxCell(); DataGridViewColumn newCol2 = new DataGridViewColumn(newCell); // Then add to the columns collection: m_Grid.Columns.Add(newCol); m_Grid.Columns.Add(newCol2); 如果以这种方式添加列,它们的名称和标题值默认为 null。要设置这些或其他属性,您可以在将列实例添加到Columns集合之前或之后访问其属性。您也可以通过索引访问Columns集合以获取对列的引用,然后使用该引用来访问列上任何您需要的属性。
- 将网格的 ColumnCount属性设置为某个大于零的值。这种方法主要用于快速创建一个只包含文本框单元格的网格,或者向现有网格添加更多的文本框列。// Constructs five TextBox columns // and adds them to the grid m_Grid.ColumnCount = 5; 
当您像这样设置 ColumnCount 属性时,行为取决于网格中是否已存在任何列。如果存在现有列,并且您指定的列数少于当前列数,ColumnCount 属性将从网格中删除尽可能多的列以创建您指定的列数,从最右边的列开始向左移动。这有点破坏性并且令人担忧,因为它允许您删除列而没有明确说明要删除哪些列,所以我建议避免使用 ColumnCount 属性来删除列。
但是,在添加文本框列时,如果您指定的列数多于当前列数,则会在现有列的右侧添加额外的文本框列,以使列数达到您指定的数量。这是一种在动态情况下添加一些列的紧凑方法。
以编程方式向网格添加行
一旦您向网格添加了列,您也可以以编程方式向网格添加行。在大多数情况下,这涉及到使用网格上 Rows 集合的 Add 方法。当您以这种方式添加行时,创建的行中每个单元格的类型都基于创建列时为该列指定的单元格模板。每个单元格将根据其单元格类型被分配一个默认值,通常对应于一个空单元格。
// Add a row
m_Grid.Rows.Add();
Add 方法的几个重载版本允许您在一次调用中添加多行,或者传入一个已经创建好的行。DataGridView 控件还支持创建异构列,这意味着一列可以有不同类型的单元格。要创建异构列,您必须首先构造行而不将其附加到网格上。然后,将您想要的单元格添加到行中,最后将该行添加到网格。例如,以下代码将一个组合框作为行中的第一个单元格,向组合框中添加一些项目,为其余四个单元格添加文本框,然后将该行添加到网格。
private void OnAddHeterows(object sender, EventArgs e)
{
    // Create 5 text box columns
    m_Grid.ColumnCount = 5;
    DataGridViewRow heterow = new DataGridViewRow();
    DataGridViewComboBoxCell comboCell = 
                     new DataGridViewComboBoxCell();
    comboCell.Items.Add("Black");
    comboCell.Items.Add("White");
    comboCell.Value = "White";
    heterow.Cells.Add(comboCell);
    for (int i = 0; i < 4; i++)
    {
        heterow.Cells.Add(new DataGridViewTextBoxCell());
    }
    // this row has a combo in first cell
    m_Grid.Rows.Add(heterow);
}
要以这种方式向网格添加行,网格必须已经用它将容纳的默认列集进行了初始化。此外,添加的行中的单元格数量必须与该列数匹配。在此代码示例中,通过指定列数为五,隐式添加了五个文本框列,然后通过构造单元格并在将行添加到网格之前将它们添加到行中,将第一行添加为异构行。
您还可以通过使用网格现有的列定义,使用 CreateCells 方法在行中创建默认的单元格集,从而节省一些代码,然后只替换那些您希望与默认值不同的单元格:
DataGridViewRow heterow = new DataGridViewRow();
heterow.CreateCells(m_Grid);
heterow.Cells.RemoveAt(0);
heterow.Cells.Insert(0, new DataGridViewComboBoxCell());
m_Grid.Rows.Add(heterow);
要以编程方式访问单元格的内容,您需要通过索引访问网格上的 Rows 集合以获取对该行的引用,然后通过索引访问该行上的 Cells 集合来访问单元格对象。
一旦您获得了对单元格的引用,您就可以对该单元格执行实际单元格类型所支持的任何操作。如果您想访问由单元格类型公开的特定属性或方法,您必须将该引用转换为实际的单元格类型。要更改单元格的内容,您需要根据单元格的类型将 Value 属性设置为适当的值。何为适当的值取决于它是什么类型的单元格。对于文本框、链接、按钮和标题单元格类型,过程与第 4 章中描述的 Binding 对象非常相似。基本上,您在单元格的 Value 属性上设置的值需要可以转换为字符串,并且格式化将在绘制过程中应用。要更改输出字符串的格式,请设置用于该单元格的样式的 Format 属性。该样式是 DataGridViewCellStyle 对象的实例,并作为单元格上的另一个属性公开,毫不奇怪地命名为 Style。单元格的样式包含其他可以设置的有趣属性(在后面的“使用样式进行格式化”部分中描述)。
例如,如果您想使用短日期格式将文本框单元格的内容设置为当前日期,您可以使用以下代码:
m_Grid.Rows[0].Cells[2].Value = DateTime.Now;
m_Grid.Rows[0].Cells[2].Style.Format = "d";
这将第一行第三个单元格的值设置为一个 DateTime 对象实例,该对象可转换为字符串,并将格式字符串设置为预定义的短日期格式字符串“d”(即短日期格式——MM/YYYY)。当该单元格被渲染时,它将使用格式字符串将存储的 DateTime 值转换为字符串。
使用非绑定列自定义列内容
现在您已经了解了如何以编程方式创建列和行,并用值填充它们,您可能想知道,每当您想在未绑定到数据的单元格中呈现内容时,是否都必须经历所有这些麻烦。好消息是,在大多数您想呈现非绑定数据的场景中,有一种更快的方法。您需要以编程方式创建网格中的所有列(尽管您也可以让设计器为您编写此代码,如下文所示),但您可以使用事件来使填充值变得更容易一些,尤其是在混合使用绑定数据和非绑定列时。
非绑定列是指未绑定到数据的列。您可以通过编程方式向网格添加非绑定列,并如前一节所示以编程方式或如本节所示使用事件来填充该列的单元格。您仍然可以向网格中添加自动绑定到数据源数据项中列或属性的列。您可以通过在创建列后设置其 DataPropertyName 属性来实现。然后您也可以添加非绑定列。当您将网格的 DataSource 属性设置为数据源时,网格的行将被创建,就像在纯数据绑定情况下一样,因为网格会遍历数据集合中的行或对象,为每一个创建新行。
填充非绑定列内容主要有两种方法:处理 RowsAdded 事件或处理 CellFormatting 事件。前者是设置单元格值的好地方,以便以后可以通过编程方式检索。后者是提供仅用于显示目的的值的好地方,该值不会作为网格单元格集合保留的数据的一部分存储。CellFormatting 事件也可用于在单元格中呈现值时将其转换为与实际存储在网格背后数据中的值不同的内容。
为了演示这个功能,让我们看一个简单的例子。
- 在 Visual Studio 2005 中启动一个新的 Windows 应用程序项目,并为项目添加一个 Northwind 数据库中 Customers 表的新数据源(这在第 1 章中有描述——以下是基本步骤):- 选择 数据 > 添加新数据源。
- 选择数据库作为数据源类型。
- 选择或创建一个到 Northwind 数据库的连接。
- 从数据库对象树中选择 Customers 表。
- 将数据集命名为 CustomersDataSet并完成向导。
 此时,您有了一个空的 Windows Forms 项目,并为 Customers 定义了一个类型化数据集。 
- 从工具箱中向窗体添加一个 DataGridView和一个BindingSource,分别命名为m_CustomersGrid和m_CustomersBindingSource。
- 在调用 InitializeComponents之后,将清单 6.1 中的代码添加到窗体的构造函数中。清单 6.1:动态列构造 public Form1() { InitializeComponent(); // Get the data CustomersTableAdapter adapter = new CustomersTableAdapter(); m_CustomersBindingSource.DataSource = adapter.GetData(); // Set up the grid columns m_CustomersGrid.AutoGenerateColumns = false; int newColIndex = m_CustomersGrid.Columns.Add("CompanyName", "Company Name"); m_CustomersGrid.Columns[newColIndex].DataPropertyName = "CompanyName"; newColIndex = m_CustomersGrid.Columns.Add("ContactName", "Contact Name"); m_CustomersGrid.Columns[newColIndex].DataPropertyName = "ContactName"; newColIndex = m_CustomersGrid.Columns.Add("Phone", "Phone"); m_CustomersGrid.Columns[newColIndex].DataPropertyName = "Phone"; newColIndex = m_CustomersGrid.Columns.Add("Contact", "Contact"); // Subscribe events m_CustomersGrid.CellFormatting += OnCellFormatting; m_CustomersGrid.RowsAdded += OnRowsAdded; // Data bind m_CustomersGrid.DataSource = m_CustomersBindingSource; } 此代码首先使用表适配器的 GetData方法检索 Customers 表数据。如本书前面所讨论的,表适配器是在您向项目添加数据源时与类型化数据集一起创建的。它将返回的数据表设置为绑定源的数据源。AutoGenerateColumns属性被设置为false,因为代码是以编程方式填充列集合的。然后,使用Columns集合上Add方法的重载(它接受一个列名和标题文本)向网格中添加四个文本框列。前三列通过在创建列后设置其DataPropertyName属性来设置为与 Customers 表的CompanyName、ContactName和Phone列进行数据绑定。第四列是非绑定列,此时仅通过调用Add方法创建。它将在稍后通过事件进行填充。最后,使用委托推断将感兴趣的事件连接到将处理它们的方法。使用这个新的 C# 特性,您不必显式地创建一个委托实例来为事件订阅处理程序。您只需分配一个具有与事件委托类型相匹配签名的方法名,编译器就会为您生成委托实例。在 Visual Basic 中,您使用 AddHandler运算符,其操作方式一直类似。当数据源设置在网格上并且网格被渲染时,网格会遍历数据源的行,并为每个数据源行在网格中添加一行,将绑定列单元格的值设置为数据源中的相应值。每创建一行,就会触发 RowsAdded事件。此外,当行被创建时,行中的每个单元格都会触发一系列事件。
- 添加以下方法作为 CellFormatting事件的处理程序:private void OnCellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.ColumnIndex == m_CustomersGrid.Columns["Contact"].Index) { e.FormattingApplied = true; DataGridViewRow row = m_CustomersGrid.Rows[e.RowIndex]; e.Value = string.Format("{0} : {1}", row.Cells["ContactName"].Value, row.Cells["Phone"].Value); } } 如前所述,如果您以编程方式设置单元格的显示值,可以使用 CellFormatting事件。传递给CellFormatting事件的事件参数公开了多个属性,让您知道正在呈现的是哪个单元格。您可以使用ColumnIndex来确定事件是为哪个列触发的。它通过在Columns集合中查找,与Contact列的索引进行比较。一旦确定这是您想要提供编程值的列,您就可以使用事件参数上的 RowIndex属性获取正在填充的实际行。在这种情况下,代码只是使用String.Format方法连接该行的ContactName和Phone来形成一个联系信息字符串,并将该字符串设置为Contact列的值。在其他情况下,您可能会使用 CellFormatting事件来做一些事情,比如从另一个表中查找一个值(例如使用外键),并将检索到的值作为非绑定列中显示的值。它还将事件参数上的FormattingApplied属性设置为true。这样做非常重要;它向网格发出信号,表明该列正在动态更新。如果您不这样做,并且还将该列设置为自动确定其大小(如后面部分所述),您将陷入无限循环。注意:在动态格式化单元格内容时,请始终将 FormattingApplied设置为true。如果您未将事件参数上的 FormattingApplied属性设置为CellFormatting事件,网格将不知道内容是动态确定的,并且对于某些其他的网格行为可能无法正常工作。
应该注意的是,CellFormatting 事件的代码示例从性能角度来看相当低效。首先,您不希望每次都通过名称在列集合中查找列的索引。更高效的做法是查找一次,将其存储在成员变量中,然后直接使用该索引进行比较。我在这个示例中使用了查找方法,以保持其简单易读——这样您就可以专注于网格的细节,而不是一堆以性能为导向的代码。此外,这无论如何都是一个有点刻意的例子;它只是为了演示如何创建一个非绑定列。
如果您想设置网格中非绑定列的实际存储单元格值,更好的方法是处理 RowsAdded 事件。正如您可能从名称中猜到的,当行被添加到网格时会触发此事件。通过处理此事件,您可以一次性填充行中所有非绑定单元格的值,这比为每个单元格处理 CellFormatting 事件稍微高效一些。RowsAdded 事件可以一次针对一行触发(如果您在循环中以编程方式添加行),也可以针对一批正在添加的行只触发一次(例如当您进行数据绑定或使用行集合的 AddCopies 方法时)。传递给 RowsAdded 的事件参数包含 RowIndex 和 RowCount 属性;这些属性让您可以在循环中遍历已添加的行以更新非绑定列的值。以下方法展示了另一种填充前一个示例中网格 Contact 列的方法,使用 RowsAdded 方法而不是 CellFormatting 事件:
private void OnRowsAdded(object sender, 
             DataGridViewRowsAddedEventArgs e)
{
    for (int i = 0; i < e.RowCount; i++)
    {
        DataGridViewRow row = m_CustomersGrid.Rows[e.RowIndex + i]; 
        row.Cells["Contact"].Value = string.Format("{0} : {1}", 
                                     row.Cells["ContactName"].Value, 
                                     row.Cells["Phone"].Value);
    }
}
此代码通过使用事件参数中的 RowIndex 属性和一个循环索引偏移量(最多为触发此事件时受影响的行数)来索引 Rows 集合,从而获取循环中当前正在设置的行。然后,它使用该行其他列中的现有数据来计算当前行的内容。对于真实世界的应用程序,您可以在循环中获取或计算行中非绑定列的值。
提示:使用
CellFormatting事件控制显示的单元格内容,使用RowsAdded事件控制存储的单元格内容。
CellFormatting事件会在每个单元格渲染时触发,让您有机会检查并可能修改网格中任何单元格将呈现的值。如果您想修改存储在网格单元格集合中的非绑定列的实际值,请使用RowsAdded事件在行添加时一次性设置所有非绑定列的值,而不是为每个单元格需要单独的事件触发。如果您呈现的数据纯粹是计算数据,而不是从绑定数据中检索或派生的数据,请考虑使用虚拟模式来填充这些单元格,如下一节所述。注意:在有意义的地方生成计算列。
本节中展示的示例只是简单地根据其他列的内容计算出一个新列。如果这仅用于显示目的,那么使用非绑定列可能是最好的方法。然而,如果该计算列的值可能在您的应用程序的其他任何地方需要,那么将其作为应用程序数据实体的一部分会更有意义,方法是将其设为基于数据集本身表达式的自定义数据列。
使用数据集设计器向类型化数据表添加自定义列很容易,并且有许多自定义表达式可以根据表中的其他数据计算该列的值。如果您需要根据数据库中的其他表或数据生成列数据,那么在存储过程中执行此操作可能更有意义。
最关键的是,您需要为您的计算数据选择合适的范围。不要使用带有重复代码的非绑定列来显示本可以计算一次并成为您正在使用的数据的实际成员的内容。
在虚拟模式下显示计算数据
另一个您可能希望明确控制为每个单元格提供显示值的场景是,当您处理包含计算值的数据时,特别是当这些数据与绑定数据和大数据集结合在一起时。例如,您可能需要在网格中呈现一个包含数万甚至数百万行的数据集合。在支持这样的功能之前,您确实应该首先质疑其用例,但如果您确实需要这样做,您可能不希望通过创建该数据的多个副本来耗尽系统内存。您甚至可能不想一次性将所有数据都加载到内存中,特别是如果这些数据是动态创建或计算的。但是,您会希望用户能够顺畅地滚动浏览所有这些数据以找到感兴趣的项目。
DataGridView 的虚拟模式允许您在单元格被渲染时显示其值,这样数据在不被使用时就不必保存在内存中。通过虚拟模式,您可以在设计时指定网格包含哪些列,然后在需要时提供单元格值,这样它们会在运行时显示,但不会提前显示。网格内部只会维护所显示单元格的单元格值;您按需提供单元格值。
当您选择使用虚拟模式时,您可以选择通过虚拟模式事件处理提供所有的行值,或者您可以将网格的列与数据绑定列和非绑定列混合使用。您必须按照前面“以编程方式构建 DataGridView”部分所述,定义将通过虚拟模式填充的列。如果您也数据绑定了某些列,那么行将通过数据绑定填充,您只需为那些未数据绑定的列处理虚拟模式所需的事件。如果您不使用数据绑定,您必须添加与您期望在网格中呈现的行数一样多的行,这样网格才能知道如何适当地缩放滚动条。然后,您需要一种方法来在需要时获取与虚拟模式列相对应的值。您可以通过在需要时动态计算这些值来实现,这是您使用虚拟模式的主要时机之一。您也可以使用对象集合或数据集形式的客户端缓存数据,或者您甚至可能需要往返服务器以按需获取数据。对于后一种方法,您需要一个智能的预加载和缓存策略,因为如果用户在滚动时客户端和服务器之间必须运行数据查询,应用程序可能会很快变得迟钝。如果显示的数据是计算数据,那么等到它们实际要显示时再计算值就非常有意义了。
设置虚拟模式
以下步骤描述了如何设置虚拟模式数据绑定。
- 创建一个网格,并在其中定义将使用虚拟模式的列。
- 通过将 VirtualMode属性设置为true,将网格置于虚拟模式。
- 如果您不使用数据绑定,请向网格添加您想要支持滚动的行数。最简单快捷的方法是创建一个原型行,然后使用 Rows集合的AddCopies方法添加任意数量的原型行副本。此时您不必担心单元格的内容,因为您将通过事件处理程序在网格渲染时动态提供这些内容。
- 最后一步是为网格上的 CellValueNeeded事件连接一个事件处理程序。此事件仅在网格处于虚拟模式下运行时触发,并且会在网格首次显示和用户滚动时为当前可见的每个非绑定列单元格触发。
清单 6.2 中的代码展示了一个简单的 Windows Forms 应用程序,演示了虚拟模式的使用。一个 DataGridView 对象通过设计器添加到了窗体上,并命名为 m_Grid,同时窗体上还添加了一个名为 m_GetVisitedCountButton 的按钮,用于在滚动时检查访问过的行数。
清单 6.2:虚拟模式示例
partial class VirtualModeForm : Form
{
    private List<DataObject> m_Data = new List<DataObject>();
    private List<bool> m_Visited = new List<bool>();
    public VirtualModeForm()
    {
        InitializeComponent();
        m_Grid.CellValueNeeded += OnCellValueNeeded;
        m_GetVisitedCountButton.Click += OnGetVisitedCount;
        InitData();
        InitGrid();
    }
    private void InitData()
    {
        for (int i = 0; i < 1000001; i++)
        {
            m_Visited.Add(false);
            DataObject obj = new DataObject();
            obj.Id = i;
            obj.Val = 2 * i;
            m_Data.Add(obj);
        }
    }
    private void InitGrid()
    {
        m_Grid.VirtualMode = true;
        m_Grid.ReadOnly = true;
        m_Grid.AllowUserToAddRows = false;
        m_Grid.AllowUserToDeleteRows = false;
        m_Grid.ColumnCount = 3;
        m_Grid.Rows.Add();
        m_Grid.Rows.AddCopies(0, 1000000);
        // Uncomment the next line and comment out the 
        // the rest of the method to switch to data bound mode
        // m_Grid.DataSource = m_Data;
    }
    private void OnCellValueNeeded(object sender, 
                 DataGridViewCellValueEventArgs e)
    {
        m_Visited[e.RowIndex] = true;
        if (e.ColumnIndex == 0)
        {
            e.Value = m_Data[e.RowIndex].Id;
        }
        else if (e.ColumnIndex == 1)
        {
            e.Value = m_Data[e.RowIndex].Val;
        }
        else if (e.ColumnIndex == 2)
        {
            Random rand = new Random();
            e.Value = rand.Next();
        }
    }
    private void OnGetVisitedCount(object sender, EventArgs e)
    {
        int count = 0;
        foreach (bool b in m_Visited)
        {
            if (b) count++;
        }
        MessageBox.Show(count.ToString());
    }
}
public class DataObject
{
    private int m_Id;
    private int m_Val;
    public int Val
    {
        get { return m_Val; }
        set { m_Val = value; }
    }
    
    public int Id
    {
        get { return m_Id; }
        set { m_Id = value; }
    }
}
窗体构造函数首先像往常一样调用 InitializeComponent,以调用设计器通过拖放操作和“属性”窗口设置编写的代码。对于此示例,这只是在窗体上声明并创建网格和按钮控件。然后,构造函数代码使用委托推断订阅了两个事件处理程序。
第一个事件处理程序是虚拟模式中最重要的一个——CellValueNeeded 事件。如前所述,此事件仅在网格处于虚拟模式时触发,并且会为网格中任何给定时间可见的每个非绑定列单元格调用。随着用户滚动,此事件会为通过滚动操作揭示的每个单元格再次触发。构造函数还为按钮点击订阅了一个处理程序,它让您可以看到 CellValueNeeded 事件处理程序实际被调用了多少行。
之后,构造函数调用 InitData 辅助方法,该方法使用 List 创建一个数据集合DataObject 类的实例,该类在清单 6.2 的末尾定义。DataObject 类有两个整数值,Id 和 Val,它们将在网格中显示。该集合由 InitData 辅助方法填充一百万行。每个对象的 Id 属性设置为其在集合中的索引,Val 属性设置为该索引的两倍。
提示:对于大型数据集合,优先使用对象集合。
使用 .NET 2.0 中的泛型集合可以轻松定义对象集合,这些集合本质上是轻量级的,并且比将数据存储在数据集中性能更好。对于较小的数据集,特别是当您需要支持更新、插入和删除时,类型化数据集是一个很好的选择。但对于仅仅呈现大量数据集合,与自定义对象集合相比,使用数据集会带来显著的性能和内存成本。
选择数据集或自定义集合
当您处理像清单 6.2 中那样非常大的内存数据集合时,您的主要关切之一应该是内存影响。根据本书到目前为止展示的其他示例,您可能倾向于使用类型化数据集作为您的数据集合对象。但是,在这样做之前,您应该三思。尽管 .NET 2.0 中
DataSet类(以及因此派生的类型化数据集类)的改进显著提高了其对大数据集的可伸缩性和性能,但DataSet类仍然是一个相当重量级的对象,因为它内置了所有分层、变更跟踪和更新功能。
如果您正在考虑在网格中呈现数百万行数据,并且希望让用户在网格中编辑这些行,并让数据集的更新功能将这些更改传播回您的数据存储,那么 DataSet 类可能正是您所需要的。然而,我不鼓励您这样设计您的应用程序。显示大型数据集合应该是一个专注于呈现的特殊用例——您只是让用户滚动浏览大量行来定位感兴趣的数据项。如果您需要支持对这些数据项的编辑,我建议您将用户引导到另一个窗体进行编辑。然而,您可以让他们在网格中编辑值而无需数据集的额外开销,如下所述。
像 List 这样的对象集合
初始化网格
数据初始化后,构造函数调用 InitGrid 辅助方法,该方法执行以下操作:
- 将网格设置为虚拟模式。
- 关闭编辑、添加和删除功能。
- 通过设置 ColumnCount属性向网格添加三个文本框列。
- 向网格中添加一行作为模板。
- 使用 Rows集合上的AddCopies方法添加一百万行。此方法还包含一行被注释掉的代码,可用于将VirtualMode下载示例更改为针对对象集合进行数据绑定,以便您可以看到加载时间和内存占用方面的差异。
之后,Windows Forms 事件处理接管了应用程序生命周期的其余部分。因为网格被设置为虚拟模式,所以接下来发生的事情是 OnCellValueNeeded 处理程序将开始为当前显示在网格中的每个单元格被调用。该方法被编码为根据正在渲染的前两列的行索引和列索引从数据集合中提取适当的值。对于第三列,它实际上是动态计算单元格的值,使用 Random 类生成随机数。它还在 m_Visited 集合中设置一个标志——您可以使用它来查看当用户在应用程序运行时滚动时实际渲染了多少行。
理解虚拟模式行为
如果您运行清单 6.2 中的 VirtualMode 示例应用程序,请注意,当您将鼠标悬停在网格的第三列上时,鼠标经过的单元格中的随机数会发生变化。这是因为 CellValueNeeded 事件处理程序在每次单元格绘制时都会被调用,而不仅仅是在它首次进入滚动区域时,并且 Random 类使用当前时间作为计算下一个随机数的种子值。因此,如果 CellValueNeeded 计算出的值是随时间变化的,您可能需要制定一个更智能的策略来计算这些值并缓存它们,以避免仅仅因为鼠标经过而导致网格中显示变化的值。
OnGetVisitedCount 按钮的 Click 处理程序会显示一个对话框,根据 m_Visited 集合显示渲染的行数。如果您运行 VirtualMode 示例应用程序,您可以看到一些关于虚拟模式值得注意的地方。首先是运行时最大的影响是客户端大型数据集合的加载和缓存。因此,在实际应用程序中,这是一种您可能需要考虑在单独线程上执行的操作,以避免在数据加载时占用 UI。使用 BackgroundWorker 组件是这类操作的好选择。
在处理非常大的数据集时,如果用户拖动滚动条滑块,实际上会通过分页机制和滚动条的延迟跳过大量行。因此,您只需提供实际单元格值的极小一部分,除非用户在网格中进行了大量的滚动。这就是为什么虚拟模式对于计算值特别好:您可以避免计算不会被显示的单元格值。
如果您运行此示例并滚动一会儿,然后单击“获取访问行数”按钮,您将看到实际加载了多少行。例如,我运行此应用程序并相当缓慢地从上到下滚动数据几次。在此过程中,我看到了平滑的滚动性能,看起来就像我实际上在滚动网格所代表的数百万行。然而,实际上,在我滚动时只渲染了大约 1000 行。
如果您想支持直接在网格中编辑值该怎么办?也许您只是使用虚拟模式来呈现一个具有相对少量数据的计算列,并且您想使用该列的编辑值来执行其他计算或存储编辑过的值。在虚拟模式网格中的单元格上完成编辑后,会触发另一个事件 CellValuePushed。如果网格没有将 ReadOnly 设置为 true,并且单元格属于支持编辑的类型(如文本框列),那么用户可以单击一个单元格,稍作停顿,然后再次单击以使该单元格进入编辑模式。在用户更改了值并且焦点通过鼠标或键盘操作转移到另一个单元格或控件后,该单元格的 CellValuePushed 事件将被触发。在该事件的处理程序中,您可以从单元格中收集新值,并对其进行适当的处理,例如将其写回到您的数据缓存或数据存储中。
虚拟模式总结
这就是虚拟模式的全部内容:将 VirtualMode 属性设置为 true,创建您希望网格拥有的列和行,然后为 CellValueNeeded 事件提供一个处理程序,该处理程序为正在呈现的单元格设置适当的值。如果您需要支持直接在网格中编辑值,那么还要处理 CellValuePushed 事件,并根据用户所做的更改对修改后的值进行适当的处理。希望您在应用程序中不需要经常使用虚拟模式,但对于呈现非常大的数据集合或动态计算列值来说,有它还是很好的。关于何时需要虚拟模式,没有硬性规定。如果您的应用程序存在滚动性能问题,或者您想避免在内存中为大量行保留计算值所带来的内存影响,您可以看看虚拟模式是否能解决您的问题。不过,您仍然需要考虑您的数据检索和缓存策略,以避免严重影响您应用程序在客户端机器上的性能。
使用内置列类型
使用文本框列非常直接:您将数据绑定到可以呈现为文本的内容,或者将单元格上的 Value 属性设置为可以转换为字符串的内容,然后就完成了。使用其他一些单元格类型可能不那么容易弄清楚,所以本节将逐步介绍每种内置的列类型,指出其功能以及如何使用它。
首先要认识到的是,尽管 DataGridView 中的大部分功能都体现在单元格级别,并且它能够支持类似电子表格的行为(如本章后面所述),但网格仍然主要是一个表格控件。网格中的列通常表示可以在设计时确定的信息——具体来说,就是将要呈现的数据的模式。行通常在运行时动态确定,并映射到由列指定的结构。您偶尔可能会在运行时根据动态数据模式以编程方式创建用于呈现的列,但即便如此,您也是首先定义数据的形状(列),然后提供数据(行)。
因此,对于网格能够显示的每种内置单元格类型,都有一个相应的列类型,旨在包含该类型的单元格。每种单元格类型都派生自 DataGridViewCell 类,而每个相应的列类型都派生自 DataGridViewColumn。每种列类型都公开了有助于数据绑定的属性,并且每种列类型都对应于该列所包含的单元格类型的预期内容。同样,每个派生的单元格类型可能会根据其设计用于显示的内容类型公开额外的属性。
由于每种内置列类型在细微之处都有所不同,最好逐一介绍它们。然而,由于列类型包含的所有单元格类型都派生自同一个基类,因此有许多来自基类的属性可用于控制和访问单元格内容。DataGridViewCell 基类的属性在表 6.1 中有描述。
| 属性名称 | 类型 | 描述 | 
|---|---|---|
| ColumnIndex | Int32 | 获取包含列在网格中的位置索引。 | 
| ContentBounds | 矩形 | 获取单元格内容的边界矩形。左上角将相对于单元格的左上角,宽度和高度表示单元格内可用于呈现内容的区域。 | 
| ContextMenuStrip | ContextMenuStrip | 获取或设置与单元格关联的上下文菜单。如果用户在单元格中右键单击,则会显示此属性的上下文菜单。如果在列级别分配了上下文菜单,则在单元格级别设置不同的菜单将替换用于该列其余部分的菜单,但仅限于此单元格。 | 
| DefaultNewRowValue | 对象 | 获取在没有为单元格提供值时将使用的值。基类从此属性返回 null,但派生类可以根据其预期内容提供更有意义的值。例如,当没有为图像单元格提供值时,它会返回一个红色的 X 位图。 | 
| Displayed | 布尔值 | 如果单元格当前显示在网格的可视区域内,则为 True,否则为 false。这是一个只读属性。 | 
| EditedFormattedValue | 对象 | 获取单元格临时编辑值的格式化版本,在应用了任何格式化或类型转换之后,以及在通过焦点更改到不同单元格使编辑值成为当前值之前。 | 
| EditType | 类型 | 获取在编辑模式下将在单元格中呈现的控件的类型。 | 
| ErrorIconBounds | 矩形 | 获取错误图标将被呈现的边界矩形,以便您可以基于此进行任何自定义渲染。 | 
| ErrorText | 字符串 | 获取或设置如果单元格有关联错误时可以显示的文本。 | 
| FormattedValue | 对象 | 在应用了任何格式化或类型转换后,获取单元格当前值的格式化版本。 | 
| FormattedValueType | 类型 | 在应用格式化或类型转换后,获取将设置在单元格上用于呈现的类型。 | 
| Frozen | 布尔值 | 如果包含此单元格的行或列是 Frozen的,则为 True,否则为 false。(有关此值的含义,请参见后面的“列和行冻结”部分。)这是一个只读属性。 | 
| HasStyle | 布尔值 | 如果已为此单元格显式设置了样式,则为 True,否则为 false。这是一个只读属性。 | 
| InheritedState | DataGridViewElementState | 获取描述由单元格基类提供的状态的枚举。 | 
| InheritedStyle | DataGridView CellStyle | 获取将根据网格、行、列和默认单元格样式的样式应用的样式。(有关详细信息,请参见后面关于样式的部分。) | 
| IsInEditMode | 布尔值 | 如果用户正在编辑单元格,则为 True,否则为 false。这是一个只读属性。 | 
| OwningColumn | DataGridViewColumn | 获取对单元格所在列的引用。 | 
| OwningRow | DataGridViewRow | 获取对单元格所在行的引用。 | 
| PreferredSize | 大小 | 根据单元格模板获取单元格的首选大小,可用于自定义单元格绘制。 | 
| ReadOnly | 布尔值 | 如果此单元格的内容可编辑,则为 True,否则为 false。这是一个读/写属性。 | 
| Resizable | 布尔值 | 如果包含的行或列是可调整大小的,则为 True,否则为 false。 | 
| RowIndex | Int32 | 获取包含行在网格中的索引。 | 
| Selected | 布尔值 | 如果单元格被渲染为已选中并且被标记为已选单元格集合的一部分,则为 True,否则为 false。这是一个读/写属性。 | 
| 大小 | 大小 | 获取整个单元格的大小。 | 
| 样式 | DataGridView CellStyle | 获取或设置用于呈现此单元格的 Style对象。(有关此属性的详细信息,请参阅后面关于样式的部分。) | 
| Tag | 对象 | 这个简单的占位符引用,与其他 Windows Forms 控件一样,允许您获取或设置单元格的关联对象引用。通常,tag 用作存储控件唯一标识符的地方,该标识符可用于查找场景,例如遍历单元格。 | 
| ToolTipText | 字符串 | 获取或设置当鼠标悬停在单元格上时呈现的文本。 | 
| 值 | 对象 | 这可能是任何单元格上最重要的属性。此属性允许您获取或设置单元格在显示时呈现的值,但如果设置的值与单元格的预期类型不同,则可能会自动发生格式化和类型转换。 | 
| ValueType | 类型 | 在应用格式化或类型转换之前,获取或设置对此单元格设置的值的类型。如果类型没有被显式设置,则它派生自包含列的类型。 | 
| Visible | 布尔值 | 如果包含的行和列都可见,则为 True,否则为 false。这是一个只读属性。 | 
DataGridViewColumn(本章前面讨论过)是内置列类型派生的基类。这个类也有许多有用的属性,您可以设置这些属性来驱动网格的行为,并且特定类型的列类会继承这些属性。这些属性在表 6.2 中有描述。
| 名称 | 类型 | 描述 | 
|---|---|---|
| AutoSizeMode | DataGridViewAutoSizeColumnMode | 获取或设置列的自动调整大小行为(在后面关于自动调整列大小的部分中描述)。 | 
| CellTemplate | DataGridViewCell | 获取或设置将用作新创建单元格模板的单元格类型。派生的列类型应将单元格类型的设置限制为它们设计用于包含的单元格类型。 | 
| CellType | 类型 | 获取此列设计用于包含的单元格的类型。 | 
| ContextMenuStrip | ContextMenuStrip | 获取或设置当用户在列中右键单击时将呈现的上下文菜单对象。 | 
| DataPropertyName | 字符串 | 获取或设置在数据绑定发生时将用于设置值的绑定数据上的属性名称。 | 
| DefaultCellStyle | DataGridViewCellStyle | 获取或设置将默认用于呈现列单元格的单元格样式。 | 
| DisplayIndex | Int32 | 获取或设置列在网格中的显示索引。当启用列重排序时,这可以与 ColumnIndex不同(本章后面将介绍)。 | 
| DividerWidth | Int32 | 获取或设置此列与右侧下一列之间绘制的分隔线的宽度(以像素为单位)。 | 
| FillWeight | Float | 获取或设置在 AutoSizeMode为Fill时用于确定列宽度的值。 | 
| Frozen | 布尔值 | 如果列被冻结,则为 True,否则为 false。(冻结列和行将在本章后面讨论。)这是一个读/写属性。 | 
| HeaderCell | DataGridViewCell | 获取或设置标题单元格(呈现在列的顶部)。 | 
| HeaderText | 字符串 | 获取或设置在标题单元格中作为其值呈现的文本。 | 
| InheritedAutoSizeMode | DataGridViewAutoSizeColumnMode | 获取为基列类设置的自动调整大小模式。 | 
| InheritedStyle | DataGridViewCellStyle | 获取从网格继承的样式,如果在列、行或单元格级别没有分配样式,则将使用该样式。 | 
| IsDataBound | 布尔值 | 如果网格以数据绑定模式运行(已设置数据源),则为 True,否则为 false。这是一个只读属性。 | 
| MinimumWidth | Int32 | 获取或设置用于最小宽度的像素数。这限制了在运行时调整列的大小,使其不小于此数值。 | 
| 名称 | 字符串 | 获取或设置列的名称。这用于索引网格上的列集合以及用于数据绑定目的。 | 
| ReadOnly | 布尔值 | 如果列中的单元格可以被编辑,则为 True,否则为 false。这是一个读/写属性。 | 
| Resizable | 布尔值 | 如果允许用户在运行时调整列的大小,则为 True,否则为 false。这是一个读/写属性。 | 
| Site | ISite | 获取列的 ISite接口引用(如果有)。当列托管组件时使用此属性。 | 
| SortMode | DataGridViewColumnSortMode | 获取或设置用于根据此列对网格行进行排序的排序模式。此枚举值可以设置为 NotSortable、Automatic或Programmatic。 | 
| ToolTipText | 字符串 | 获取或设置当鼠标悬停在列中的单元格上时显示的工具提示弹出文本。如果此属性在单元格级别设置,则显示为单元格设置的值。 | 
| ValueType | 类型 | 获取或设置存储在此列类型的单元格中的值的类型。 | 
| Visible | 布尔值 | 如果列将显示在网格中,则为 True,否则为 false。这是一个读/写属性。 | 
| 宽度 | Int32 | 获取或设置网格中列的宽度(以像素为单位)。 | 
有许多内置的列类型可用于 DataGridView 控件,对应于开发人员希望在网格中包含的最常见的控件类型。以下小节描述了每种内置列类型以及使用它们所涉及的内容。
DataGridViewTextBoxColumn
这是默认的列类型(如本章前面所述),它在其包含的单元格内显示文本,这些单元格的类型是 DataGridViewTextBoxCell。绑定到此列类型的数据和在单元格上设置的值必须是可以转换为字符串的类型。
如果 ReadOnly 属性为 true(默认值)并且焦点在单元格上,则此列类型支持编辑。要进入编辑模式,请按 F2、键入字符或在单元格中单击。这将嵌入一个类型为 DataGridViewTextBoxEditingControl 的独立编辑控件,该控件派生自 TextBox。此类型启用了网格值的就地编辑,就像您习惯的文本框控件一样。文本框中的值被视为临时值,直到焦点离开单元格;然后 CellParsing 事件触发,如果数据已绑定,则值被推入底层数据存储;如果处于虚拟模式,则 CellValuePushed 事件触发。
DataGridViewButtonColumn
此列类型显示 DataGridViewButtonCell 类型的单元格,这是一种花哨的只读文本单元格。按钮单元格允许您在网格中嵌入按钮按下体验,您可以用它来触发对您的应用程序有意义的任何操作。按钮单元格自身呈现时带有一个看起来像任何其他按钮控件的边框,当用户单击它时,单元格会以一个凹陷的偏移量再次呈现,从而获得类似按钮的动作。要处理“按钮单击”,您需要处理网格上的 CellClick 事件,确定被单击的是否是按钮单元格,然后根据您的应用程序采取适当的行动。这涉及到从 CellClick 事件中获取事件参数,检查其 ColumnIndex 属性与您网格中按钮列的列索引,然后根据行索引或该行中该单元格或其他单元格的内容调用按钮单击处理代码。
DataGridViewLinkColumn
与按钮列类似,这是另一种呈现文本单元格的形式,它给用户一个视觉提示,即单击它将调用某个操作。此列类型包含 DataGridViewLinkCell 类型的单元格,并将单元格中的文本呈现为超链接的外观。通常,单击链接会将用户“导航”到其他地方,因此如果您要弹出一个新窗口或根据用户单击链接来修改另一个控件的内容,您可能会使用这种列。为此,您需要像前面为按钮描述的那样处理 CellClick 事件,确定您是否在一个包含链接的单元格中,并根据该链接采取适当的操作。您将必须从单元格的内容或该行或列中的其他单元格来推断您应该采取什么操作的上下文。
DataGridViewCheckBoxColumn
到目前为止,您可能已经掌握了这种模式,正如您所猜测的,此列类型包含 DataGridViewCheckBoxCell 类型的单元格。此单元格类型呈现一个类似 CheckBox 的控件,支持像 CheckBox 控件一样的三态呈现。
此单元格类型支持的值取决于您是否将单元格或列类型设置为 ThreeState 模式。如果 ThreeState 属性设置为 false(默认值),那么值为 null 或 false 将使复选框保持未选中状态;值为 true 将选中该框。如果 ThreeState 设置为 true,那么单元格的 Value 属性可以是 null 或 CheckState 枚举值之一。如果为 null 且 ThreeState 为 true,则复选框将以不确定状态呈现(一个方块填充它)。CheckState 枚举值为 Unchecked、Checked 和 Indeterminate,这些都是不言自明的。单元格的 Value 属性可以通过访问行中 Cells 集合中的单元格的编程代码显式设置,也可以通过数据绑定设置。
DataGridViewImageColumn
毫不奇怪,此列包含 DataGridViewImageCell 类型的单元格,支持直接在网格单元格内呈现图像。此单元格类型在 DataGridView 控件中提供了一个非常方便且易于使用的功能,而这在 DataGrid 控件中曾经是相当痛苦才能实现的。除了通常的基类属性外,此列类型还公开了 Image 和 ImageLayout 属性。设置列的 Image 属性会导致该图像默认显示在该列的所有单元格中。ImageLayout 属性接受一个 DataGridViewImageCellLayout 枚举值。此枚举的值及其对网格中图像呈现的影响在表 6.3 中有描述。
除了在列级别设置默认图像外,您还可以在单元格级别设置 Value 属性,既可以通过代码显式设置,也可以通过数据绑定隐式设置。该值可以设置为任何可以转换为 Image 对象的类型,以便在单元格中显示。在 .NET 中,这意味着该值可以是 Image 或包含序列化图像的字节数组。
| 值 | Effect | 
| NotSet | 这是默认值,表示布局行为尚未明确指定。单元格的最终行为与显式设置 Normal相同。 | 
| 正常 | 图像以其原始大小呈现并居中于单元格中。根据单元格的大小,任何超出单元格边界的图像部分都将被裁剪。 | 
| Stretch | 图像在宽度和高度上都会被拉伸或收缩,以便填满单元格且不发生裁剪。不尝试保持图像的宽高比(宽度/高度)。 | 
| 缩放 | 图像被调整大小以适应单元格而不被裁剪,并且保持宽高比(宽度/高度)以避免图像失真。 | 
DataGridViewComboBoxColumn
此列类型包含 DataGridViewComboBoxCell 类型的单元格,它在单元格内呈现得像一个标准的 ComboBox 控件。这种列类型无疑是 DataGridView 最复杂的内置列类型,它公开了许多驱动其行为的属性,如表 6.4 所述。
| 名称 | 类型 | 描述 | 
|---|---|---|
| AutoComplete | 布尔值 | 如果当单元格处于编辑模式时启用 AutoComplete功能,则为 True,否则为 false。这允许用户键入字符,组合框将根据键入的字符在列表中选择匹配的项。这是一个读/写属性。 | 
| CellTemplate | DataGridViewCell | 获取或设置在列中呈现的单元格类型。该单元格类型必须是派生自 DataGridViewComboboxCell的类。 | 
| DataSource | 对象 | 获取或设置用作列数据绑定数据源的对象。将此属性设置为数据集合具有与普通组合框相同的数据绑定效果——它将显示来自该集合的项目作为下拉列表中的项目,使用 DisplayMember属性确定在该集合的项目中使用哪个数据成员或属性作为列表中的文本。 | 
| DisplayMember | 字符串 | 获取或设置在数据源中显示为列表中文本项目的数据成员或属性。 | 
| DisplayStyle | DataGridViewCombo-BoxDisplayStyle | 获取或设置列中组合框使用的样式。此枚举的值包括 ComboBox、DropDownButton和Nothing。 | 
| DisplayStyleFor-CurrentCellOnly | 布尔值 | 如果 DisplayStyle值仅适用于列中的当前单元格,则为 True;如果它用于列中的所有单元格,则为 false。这是一个读/写属性。 | 
| DropDownWidth | Int32 | 获取或设置用户单击向下箭头或按 F4 时显示的下拉列表的宽度。 | 
| FlatStyle | FlatStyle | 获取或设置 FlatStyle枚举值,该值决定了组合框在呈现时的视觉外观。 | 
| Items | ObjectCollection | 获取为单元格模板设置的对象集合。 | 
| MaxDropDownItems | Int32 | 获取或设置下拉列表中显示的最大项目数。 | 
| 已排序 | 布尔值 | 如果列表中的项目将按字母顺序排序,则为 True,否则为 false。这是一个读/写属性。 | 
| ValueMember | 字符串 | 获取或设置数据源中将与列表中的项目一起保留的数据成员或属性。这让您可以跟踪额外的信息,例如记录的主键。 | 
组合框单元格支持编辑模式,用户可以输入值以进行自动完成,或从下拉列表中选择值。在编辑模式下,此单元格类型承载一个派生自 ComboBox 控件的控件,因此当单元格切换到编辑模式时,其所有功能都会公开。
Value 属性表示组合框中当前选定的值。它可能包含组合框中显示的文本值,也可能包含所选项目的底层 ValueMember 值,具体取决于您为 DataSource、DisplayMember 和 ValueMember 属性设置的内容。从基类继承的 FormattedValue 属性始终包含在组合框中显示的所选项目的格式化文本。
数据绑定此列类型或其中的单元格,其工作方式与数据绑定独立的 ComboBox 控件完全相同。您设置 DataSource、DisplayMember 和 ValueMember 属性,数据源集合中的项目将使用被标识为显示成员的数据成员的值在下拉列表中呈现。
toCountryColumn.DataSource = m_CountriesBindingSource;
toCountryColumn.DisplayMember = "CountryName";
toCountryColumn.ValueMember = "CountryID";
本章附带的示例代码包含一个名为 ColumnTypes 的简单应用程序,它演示了代码如何与本章中描述的每种内置列类型进行交互。
内置标题单元格
标题单元格是呈现在网格顶部和左侧的单元格。它们为网格中单元格的内容提供了上下文或指南。列标题单元格的类型是 DataGridViewColumnHeaderCell,其标题文本指示列单元格的内容。当列支持排序时,单元格会包含一个向上或向下的三角形;用户可以通过单击列标题来对列进行排序。通常,标题文本是通过列上的 HeaderText 属性设置的,可以通过代码显式设置,也可以通过基于数据架构的数据绑定隐式设置。您还可以通过 HeaderCell 属性直接从列访问标题单元格,并使用其 Value 来设置显示的文本。
行标题单元格的类型是 DataGridViewRowHeaderCell。它们用三角形符号指示行选择,用铅笔符号指示编辑模式,用星号符号指示新行。行标题单元格也可以显示文本;您可以通过访问行的 HeaderCell 属性将单元格的 Value 设置为字符串值。
通过处理网格上的 CellPainting 事件实现自定义绘制,可以进一步自定义列标题和行标题。请注意,如果您进行自定义绘制,则必须自己完成标题单元格的所有绘制工作,然后将事件参数上的 Handled 属性设置为 true。
private void OnCellPainting(object sender, 
             DataGridViewCellPaintingEventArgs e)
{ 
    if (e.ColumnIndex < 0)
    {
        e.Graphics.FillRectangle(Brushes.Aqua, e.CellBounds);
        e.Handled = true;
    }
}
此代码检查正在绘制的列的索引是否小于零,这表明正在绘制的是行标题。行标题的列索引为 –1,列标题的行索引也为 –1。您不能使用这些值来索引行上的 Cells 集合,但可以在 CellPainting 事件中将它们用作标志,以了解何时正在绘制标题。
此外,您可以将 CellHeader 属性设置为派生自 DataGridViewCell 的类的实例,然后该单元格类型将在呈现标题单元格时使用。您可以从单元格基类派生自己的类,并在其中进行任何有意义的自定义绘制、格式化或样式设置。
处理网格数据编辑
您如何处理网格编辑将取决于以下几点:
- 您正在处理的列或单元格的类型
- 数据是否是数据绑定的
- 您是否处于虚拟模式
如前所述,在使用文本框列时,用户可以通过鼠标、箭头键将焦点置于单元格,或在鼠标指针位于单元格内时按 F2 键来开始编辑单元格。如果用户随后开始键入字符,单元格的当前内容将被覆盖。当他们将焦点更改到另一个单元格时,编辑过程就完成了。
您可能想要处理的第一件事是 CellParsing 事件的触发。与其对应的 CellFormatting 事件一样,此事件为您提供了一个机会,可以拦截在编辑模式下输入到单元格中的值,以便自己处理存储该值,或在存储之前将其转换为其他值。
如果单元格是数据绑定的,并且数据源支持编辑集合中的数据对象,则数据将自动被推回到基础数据源中。但是,如果单元格是按钮或链接单元格,您将无法编辑内容,因为它们不支持编辑。如果单元格是组合框单元格,则通过在下拉列表中选择一个值或在其 DisplayStyle 属性设置为 ComboBox 时覆盖当前选择来进行编辑。这会在编辑完成时(当焦点移出单元格时)更改单元格的值,并导致与在文本框单元格中键入该值相同的操作。如果网格处于虚拟模式,您需要处理 CellValuePushed 事件来获取输入的值并对其进行所需的操作。
当一个单元格切换到编辑模式时,会触发一个名为 EditingControlShowing 的事件。此事件传递一个事件参数,让您可以获取对编辑控件本身的引用。支持编辑的内置单元格类型(文本框、组合框和复选框单元格类型)会创建其普通 Windows Forms 对应控件(分别为 TextBox、ComboBox 和 CheckBox)的派生编辑控件实例,并将该控件作为子控件显示在单元格内的一个面板中。如果您创建支持编辑的自定义单元格类型,那么您也应该遵循类似的方法。通过 EditingControlShowing 事件,您可以获取正在使用的编辑控件的引用,并可以利用其事件模型来实时响应编辑。例如,如果您想在控件仍处于编辑模式且所选值尚未推送到单元格的基础值(意味着 CellParsing 事件尚未触发)时动态响应组合框列中的选择,您可以使用 EditingControlShowing 事件来建立连接。
public Form1() 
{
    InitializeComponent();
    m_Grid.EditingControlShowing += OnEditControlShowing();
}
private void OnEditControlShowing(object sender, 
             DataGridViewEditingControlShowingEventArgs e)
{ 
    if (m_Grid.CurrentCell.ColumnIndex == 2) 
    { 
        m_HookedCombo = e.Control as ComboBox;
        if (m_HookedCombo == null)
            return;
        m_HookedCombo.SelectedIndexChanged += 
                       OnCountryComboChanged;
    }
} 
void OnCountryComboChanged(object sender, EventArgs e)
{
    string countryName = 
      (string)m_Grid.CurrentCell.EditedFormattedValue;
    if (string.IsNullOrEmpty(countryName))
        return;
    DataRow[] countries = 
          m_MoneyData.Countries.Select(string.Format("CountryName" + 
          " = '{0}'", countryName));
    if (countries != null && countries.Length > 0)
    {
        MoneyDBDataSet.CountriesRow row = 
           countries[0] as MoneyDBDataSet.CountriesRow;
        int flagColIndex = m_Grid.Columns["TargetCountryFlag"].Index;
        DataGridViewCell cell = m_Grid.CurrentRow.Cells[flagColIndex];
        cell.Value = row.Flag;
    }
}
此代码执行以下操作:
- 构造函数将 OnEditControlShowing方法订阅到网格的EditControlShowing事件。
- 当 EditControlShowing事件触发时,OnEditControlShowing方法使用事件参数上的Control属性获取对嵌入在正在编辑的单元格中的ComboBox控件的引用。
- 然后,OnEditControlShowing方法将OnCountryComboChanged方法订阅到该ComboBox控件的SelectedIndexChanged事件。
- 当 SelectedIndexChanged事件触发时,OnCountryComboChanged方法使用当前单元格的EditedFormattedValue属性从包含下拉列表的单元格中检索国家/地区名称。这使您可以在单元格离开编辑模式之前获取已编辑的值。
- 然后,OnCountryComboChanged方法使用国家/地区名称检索 Countries 表中的相应行,并从Flag列中提取国旗图像。
- 最后,它将国旗图像设置为与国家/地区国旗对应的单元格的值。
请记住,Countries 表中的 Flag 列实际上是一个字节数组,其中包含已保存图像文件的位。图像列的自动格式化在这里起作用,以与第 4 章中讨论的 PictureBox 控件相同的方式呈现图像。下载代码中的 ColumnTypes 示例演示了此技术。
自动列大小调整
DataGridView 控件的新功能之一是它能够根据几个不同的标准自动计算列的宽度以适应列的内容。像网格的许多功能一样,您需要做的就是为给定列设置适当的属性——然后网格会完成其余的工作。具体来说,为您处理此问题的是 DataGridViewColumn 类的 AutoSizeMode 属性。通过将此属性设置为表 6.5 中所示的 DataGridViewAutoSizeColumnMode 枚举的枚举值之一,您可以控制如何设置网格中列的宽度。
| 值 | 列宽的计算方式 | 
|---|---|
| NotSet | 通过在网格级别上设置的 AutoSizeColumnsMode属性的值。这是默认值。 | 
| 无 | 通过设置列的 Width属性显式设置。 | 
| ColumnHeader | 仅通过标题单元格中内容的宽度。 | 
| AllCellsExceptHeader | 通过网格中最宽单元格的宽度,无论其是否显示,但忽略标题单元格内容的大小。 | 
| AllCells | 通过列中所有单元格的宽度,包括未显示的单元格和标题单元格内容。 | 
| DisplayedCellsExceptHeader | 仅基于列中显示的单元格,忽略标题单元格内容的宽度。 | 
| DisplayedCells | 通过所显示单元格中内容的宽度,包括标题单元格。 | 
| Fill | 自动计算以填充网格的显示内容区域,以便无需滚动即可查看内容。实际使用的值取决于网格中其他列的模式及其 MinimumWidth和FillWeight属性。如果网格中的所有列都设置为Fill模式,并且满足其最小宽度要求,那么每列将具有相等的宽度,填充网格,但不需要任何水平滚动。 | 
最有用的值之一是 AllCells。我建议将其作为您的默认值,除非您发现对于大型数据集使用它会影响性能,或者如果您有一些单元格值会很长。此设置可确保单元格内容永远不会换行。此外,如果您正在动态填充单元格值,请记住在 CellFormatting 事件的事件参数上设置 FormattingApplied 属性。否则,将 AutoSizeMode 设置为行值之一将导致无限循环。
作为一个使用此功能的简单示例,以下代码修改了列表 6.1 中的代码,以设置“全名”计算列的列宽:
newColIndex = 
  m_AuthorsGrid.Columns.Add("FullName", "Full Name");
m_AuthorsGrid.Columns[newColIndex].AutoSizeMode = 
         DataGridViewAutoSizeColumnMode.AllCells;
Fill 模式对于自动最大化网格空间的使用非常强大,但理解起来可能有点复杂。基本上,如果您将所有列的模式都设置为 Fill,则每列的宽度将被设置为相等,并且这些列将填充网格边界,无需水平滚动条。如果设置为 Fill 模式的任何列的 MinimumWidth 属性比使用填充算法计算的宽度更宽,则将使用 MinimumWidth 值,而其他列的宽度则会变窄,以便它们仍然全部适合网格而无需水平滚动条。如果多个列的 MinimumWidth 值使得所有列都无法显示,则无法显示的列将被设置为其最小宽度值,并显示滚动条。最小宽度的默认值仅为 5 像素,因此在使用 Fill 模式时,您肯定需要设置一个更合理的 MinimumWidth 值。
每列还有一个 FillWeight 属性,当该列的 AutoSizeMode 设置为 Fill 时生效。FillWeight 可以被认为是单个列将占用的剩余可用网格宽度的百分比,与其他设置为 Fill 的列相比。不过,它是一个权重而不是百分比,因为您可以使用总和不为 100 的值。例如,假设您想在网格中显示 Northwind Customers 表中的 CustomerID、CompanyName 和 ContactName 列。以下代码将 CustomerID 列的宽度设置为 75 像素,然后将其余两列设置为 Fill 模式,权重分别为 10 和 20。
public Form1() 
{
    InitializeComponent(); 
    m_CustomersGrid.Columns["CustomerID"].Width = 75; 
    m_CustomersGrid.Columns["CompanyName"].AutoSizeMode = 
                     DataGridViewAutoSizeColumnMode.Fill; 
    m_CustomersGrid.Columns["CompanyName"].FillWeight = 10; 
    m_CustomersGrid.Columns["ContactName"].AutoSizeMode = 
                       DataGridViewAutoSizeColumnMode.Fill;
    m_CustomersGrid.Columns["ContactName"].FillWeight = 20;
}
结果是,在 CustomerID 列占据其固定宽度的空间后,其余两列分别占据了剩余网格宽度的 33% 和 67%。图 6.2 说明了这一点。

列和行冻结
处理大量行或列数据时,滚动是不可避免的。通常在滚动数据时,很容易迷失您正在查看的行或列的上下文,特别是如果该上下文是基于其他一些行或列中的值。假设您正在浏览一个填充了产品信息的网格。如果每个产品都有很多列数据,当您向右滚动以查看当前未显示的列时,随着产品名称被滚动到屏幕左侧之外,您将失去产品名称的上下文。在这种情况下,您真正想要的是能够冻结产品名称列,以便它始终显示,而只让其余的列滚动。同样,在某些情况下,您可能需要在网格顶部呈现一行或多行,以便在向下滚动到网格中的其他行时保持原位。
使用 DataGridView 控件实现这一点很简单:您只需在任何行或列上将 Frozen 属性设置为 true 即可获得此行为。具体来说,如果您冻结一列,那么当您在网格中向右滚动时,该列及其左侧的所有列都不会滚动。同样,如果您冻结一行,那么当您在网格中向下滚动时,该行及其上方的所有行都不会滚动。如果您要冻结一列或一行,那么您可能需要向用户提供一个视觉提示,以指示冻结项与旁边未冻结项之间存在的逻辑边界。最简单的方法是将列或行上的 DividerWidth 属性设置为除默认值之外的其他值。此属性是一个整数,指定用于绘制该列或行的单元格与相邻单元格(右侧或下方)之间分隔线的像素数。
以下代码显示了一个冻结列和行并设置分隔线宽度的简单示例:
m_ProductsGrid.Columns["ProductName"].Frozen = true;
m_ProductsGrid.Columns["ProductName"].DividerWidth = 3;
m_ProductsGrid.Rows[1].Frozen = true;
m_ProductsGrid.Rows[1].DividerHeight = 3;
使用设计器定义网格
现在您已经了解了如何编写网格的大多数常见用途的代码,让我们来介绍如何避免自己编写大量代码。DataGridView 控件通过设计器智能标记、对话框和属性窗口的组合,在 Visual Studio 设计器中工作时支持非常丰富的体验。
首先,如果您已在项目中定义了数据源,则可以简单地将数据集合源(如数据表)拖到窗体设计器上,系统将创建一个 DataGridView 实例及其所有支持对象。此外,基于网格数据源属性的列定义允许您使用设计器设置其他属性,例如 AutoSizeMode。如果您选择网格并显示其智能标记(如图 6.3 所示),则可以从那里修改网格外观和行为的最常见选项。
“选择数据源”下拉列表会显示一个数据源选择窗口,类似于第 5 章中为“属性”窗口描述的窗口。呈现的数据源将仅限于那些实现了 IList 接口的数据源,因此适合绑定到网格。
“编辑列”和“添加列”链接会显示对话框,让您定义网格将包含的列,分别如图 6.4 和 6.5 所示。
“编辑列”对话框允许您添加和删除列,设置列在网格中的顺序,并在一个集中的对话框中为已定义的列设置所有设计时属性。对话框中显示的属性将根据列是绑定列还是非绑定列进行调整,并将根据列类型和单元格类型公开其他属性。如果您定义自定义列类型并将其包含在项目中,它们将作为新列的选项或通过此对话框配置列的选项显示出来。


“添加列”对话框(见图 6.5)允许您向网格添加一个新的数据绑定或非绑定列。如果您要添加数据绑定列,可以从当前选定数据源中可用的列中进行选择。您首先需要通过智能标记或属性窗口中的 DataSource 属性将数据源设置为适当的数据集合。如果您要添加非绑定列,则只需指定列的名称、列的类型和标题文本。当您单击“添加”按钮时,列会添加到网格中,并且对话框保持打开状态,以便您可以快速定义多个新列。

通过这些对话框配置列,会为您编写本章前面介绍的用于定义列和控制其运行时行为的所有代码。
DataGridView 智能标记上的“启用添加”复选框会将 AllowUserToAddRows 属性设置为 true(如果选中),这会在网格底部显示一个空的新行。这允许用户通过在单元格中键入新值来向数据集合添加新行。是否支持此功能取决于网格是否为数据绑定,如果是,则取决于基础对象集合是否支持向集合添加新项(请参见第 7 章的讨论)。同样,“启用编辑”复选框设置 ReadOnly 属性,该属性影响用户是否可以就地编辑网格内容,“启用删除”设置 AllowUserToDeleteRows 属性。“启用列重新排序”复选框设置 AllowUserToOrderColumns 属性,其行为将在下一节中描述。
“在父容器中停靠”链接仅在您首先将网格控件拖放到窗体上时才可用。它完全如其名——它只是将 Dock 属性设置为 Fill。
除了可以通过智能标记配置的常见属性和行为之外,还有许多其他属性和事件可以通过属性窗口在设计时进行配置。设置任何这些属性都会在设计器为窗体的 InitializeComponent 方法生成的局部类中生成相应的代码。最值得注意的是,您可以通过属性窗口配置任何数据绑定属性。您可能希望使用属性窗口设置样式,因为您可以在设计器中预览这些设置的结果,以确保您得到的是您期望的效果。本章末尾将更详细地讨论样式。
列重排序
列重新排序是网格的一项巧妙的内置行为,允许用户在运行时更改网格中列的显示顺序。由于应用程序的不同用户通常更关注网格中的某些列,因此用户通常要求自己设置网格中显示的列的顺序。虽然您可以通过以编程方式从网格中删除列然后将它们重新插入到新位置来支持此功能,但这需要编写大量繁琐的代码来处理一个常见的用例。因此,Windows 客户端团队很好地将此功能直接内置到网格控件中。
其工作原理是,如果 AllowUserToOrderColumns 属性设置为 true,并且用户单击并拖动列标题,网格允许他们将列拖放到他们希望显示的位置。放置位置右侧的列将向右移动一个位置,而拖动列原始位置周围的列将在列移动后移动到相邻位置。图 6.6 显示了此过程。在这种情况下,单击了 QuantityPerUnit 列并将其向左拖动。会绘制一个与您正在拖动的列标题大小相同的灰色框。当您将光标移动到另一列的一侧时,该列与相邻列之间的边框会变暗,指示如果您释放鼠标按钮,您正在拖动的列将被放置的位置。

当一列通过列重新排序被移动后,它的 ColumnIndex 不会改变,但 DisplayIndex 属性会指示它在网格中的当前显示顺序。默认情况下,网格的显示顺序不会在应用程序运行之间持久化,但您可以自己轻松地持久化该信息,并通过将显示顺序写入文件来恢复显示顺序。列表 6.3 中的代码演示了如何使用 XmlSerializer 类将数据持久化到独立存储中的文件中。
列表 6.3:持久化列的显示顺序
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    private void Form1_Load(object sender, EventArgs e) 
    {
        m_Grid.AllowUserToOrderColumns = true;
        SetDisplayOrder(); 
    } 
    private void OnFormClosing(object sender, FormClosingEventArgs e)
    {
        CacheDisplayOrder();
    }
    private void CacheDisplayOrder()
    {
        IsolatedStorageFile isoFile = 
            IsolatedStorageFile.GetUserStoreForAssembly();
        using (IsolatedStorageFileStream isoStream = 
            new IsolatedStorageFileStream("DisplayCache", 
            FileMode.Create, isoFile))
        {
            int[] displayIndices =new int[m_Grid.ColumnCount];
            for (int i = 0; i < m_Grid.ColumnCount; i++)
            {
                displayIndices[i] = m_Grid.Columns[i].DisplayIndex;
            }
            XmlSerializer ser = new XmlSerializer(typeof(int[]));
            ser.Serialize(isoStream,displayIndices); 
        }
    }
    private void SetDisplayOrder()
    {
        IsolatedStorageFile isoFile = 
          IsolatedStorageFile.GetUserStoreForAssembly();
        string[] fileNames = isoFile.GetFileNames("*");
        bool found = false;
        foreach (string fileName in fileNames)
        {
            if (fileName == "DisplayCache") 
                found = true;
        }
        if (!found) 
            return;
        using (IsolatedStorageFileStream isoStream = new 
               IsolatedStorageFileStream("DisplayCache", 
               FileMode.Open, isoFile)) 
        {
            try
            {
                XmlSerializer ser = new XmlSerializer(typeof(int[]));
                int[] displayIndicies = (int[])ser.Deserialize(isoStream);
                for (int i = 0; i < displayIndicies.Length; i++)
                {
                    m_Grid.Columns[i].DisplayIndex = displayIndicies[i];
                }
            }
            catch { } 
        }
    }
}
此代码与数据源没有任何特定关系。这里的关键方面是,窗体 Load 事件处理程序中的代码将 AllowUserToOrderColumns 属性设置为 true,从而允许通过拖放操作动态更改列的 DisplayIndex。然后,我添加了一个由 Form.Closing 事件处理程序调用的 CacheDisplayOrder 辅助方法,以及一个在窗体加载时调用的 SetDisplayOrder 辅助方法。
CacheDisplayOrder 首先收集网格中每列的所有显示索引值,并将它们放入一个整数数组中。然后,它创建一个独立存储文件流,并使用 XmlSerializer 类将该数组写入该流。SetDisplayOrder 方法执行相反的操作:它首先检查文件是否存在,如果存在,则将数组读回,并用它来设置网格中每列的 DisplayIndex。
定义自定义列和单元格类型
使用 DataGridView,您在呈现丰富数据方面已经遥遥领先于 DataGrid,因为它开箱即用地支持内置列类型。但是,您总是希望支持自定义场景以显示自定义列。幸运的是,DataGridView 使插入自定义列和单元格类型变得更加容易。
如果您只想自定义单元格的绘制过程,而不需要在列级别添加任何属性或控制,那么您有一个基于事件的选项,而不是创建新的列和单元格类型。您可以处理 CellPainting 事件并直接在单元格本身中进行绘制,通过内置的单元格类型和一些(可能很复杂的)绘制代码,您几乎可以实现任何您想要的效果。但是,如果您希望能够以可重用的方式插入您的列或单元格类型,并且与使用内置类型一样简单,那么您可以派生自己的列和单元格类型。
您应该遵循的用于插入自定义列类型的模型与您已经看到的内置类型的模型相匹配:您需要创建一个列类型和该列将包含的相应单元格类型。您可以通过直接或间接地从基类 DataGridViewColumn 和 DataGridViewCell 继承,或者通过内置类型之一来完成此操作。
详细解释这一点的最佳方法是举一个例子。假设我想实现一个自定义列类型,让我可以显示网格行所代表的项目的状态。我希望能够使用自定义枚举值设置状态,并且列中的单元格将根据单元格上设置的枚举值显示一个指示该状态的图形。为此,我定义了一个 StatusColumn 类和一个 StatusCell 类(我在这里放弃了内置类型的命名约定,即在所有类型上加上 DataGridView 前缀,因为类型名称变得太长了)。我希望这些类型能让我简单地将单元格的值设置为一个自定义枚举类型(我称之为 StatusImage)的值之一,无论是通过编程方式还是通过数据绑定。StatusImage 可以取值为 Green、Yellow 或 Red,我希望单元格根据其值显示相应的自定义图形。图 6.7 显示了具有此行为的正在运行的示例应用程序。
定义自定义单元格类型
为了实现这一点,第一步是定义自定义单元格类型。如果您要自己进行绘制,可以重写 DataGridViewCell 基类中受保护的虚方法 Paint。但是,如果您想呈现的单元格内容只是内置单元格类型之一的变体,您应该考虑从其中之一继承。在这种情况下我就是这么做的。因为我的自定义单元格仍然会呈现图像,所以 DataGridViewImageCell 类型是一个很自然的基类。不过,我的 StatusCell 类不会公开随机设置图像的功能;它旨在与枚举值一起工作。

我还希望单元格值能够处理整数,只要它们在枚举的相应数值范围内,这样我就可以支持枚举类型在数据库中存储为其相应整数值的常见情况。列表 6.4 中的代码显示了 StatusCell 类的实现。
列表 6.4:自定义单元格类
namespace CustomColumnAndCell
{
    public enum StatusImage { Green, Yellow, Red }
    public class StatusCell : DataGridViewImageCell
    {
        public StatusCell()
        {
            this.ImageLayout = DataGridViewImageCellLayout.Zoom;
        }
 
        protected override object GetFormattedValue(object value, 
                  int rowIndex, ref DataGridViewCellStyle cellStyle, 
                  TypeConverter valueTypeConverter, 
                  TypeConverter formattedValueTypeConverter, 
                  DataGridViewDataErrorContexts context)
        {
            string resource = "CustomColumnAndCell.Red.bmp";
            StatusImage status = StatusImage.Red;
            // Try to get the default value from the containing column
            StatusColumn owningCol = OwningColumn as StatusColumn;
            if (owningCol != null)
            {
                status = owningCol.DefaultStatus;
            }
            if (value is StatusImage || value is int)
            {
                status = (StatusImage)value;
            }
            switch (status)
            {
                case StatusImage.Green:
                    resource = "CustomColumnAndCell.Green.bmp";
                    break; 
                case StatusImage.Yellow:
                    resource = "CustomColumnAndCell.Yellow.bmp";
                    break;
                case StatusImage.Red:
                    resource = "CustomColumnAndCell.Red.bmp";
                    break;
                default:
                    break;
            }
            Assembly loadedAssembly = Assembly.GetExecutingAssembly();
            Stream stream = loadedAssembly.GetManifestResourceStream(resource);
            Image img = Image.FromStream(stream);
            cellStyle.Alignment = DataGridViewContentAlignment.TopCenter;
            return img;
        }
    }
}
此代码中的第一个声明是枚举 StatusImage。这是此单元格类型期望作为其 Value 属性的值类型。然后您可以看到 StatusCell 类型派生自 DataGridViewImageCell,因此我可以重用其在网格内呈现图像的能力。有一个默认状态字段和相应的属性,允许默认值直接显示。构造函数还将基类的 ImageLayout 属性设置为 Zoom,以便图像在不失真的情况下调整大小以适应单元格。
自定义单元格类型需要做的关键事情是,要么像前面提到的那样重写 Paint 方法,要么像 StatusCell 类那样重写 GetFormattedValue 方法。此方法将在每次呈现单元格时被调用,并允许您处理从其他类型到单元格预期类型的转换。我为本示例选择的 GetFormattedValue 编码方式是,首先将值设置为一个默认值,如果所有其他方法都失败,则将使用该值。然后,代码会尝试从包含列的 DefaultValue 属性中获取真正的默认值,前提是该列类型为 StatusColumn(稍后讨论)。然后,代码检查当前的 Value 属性是否是 StatusImage 枚举类型或整数,如果是整数,则将该值强制转换为枚举类型。
一旦确定了要呈现的状态值,GetFormattedValue 方法就会使用 switch-case 语句来选择与该状态值的图像相对应的适当资源名称。您可以通过将位图资源添加到 Visual Studio 项目中,并将其文件的“生成操作”属性设置为“嵌入的资源”,从而将位图资源嵌入到程序集中。然后,代码使用 Assembly 类上的 GetManifestResourceStream 方法从程序集中提取位图资源,在传递给该方法的 CellStyle 参数上设置对齐方式,然后将构造的图像作为对象从该方法返回。您从此方法返回的对象将作为格式化值传递给下游的 Paint 方法进行呈现。由于这不会覆盖 Paint 方法,因此将调用我的 DataGridViewImageCell 基类的实现,它需要一个 Image 值来呈现。
定义自定义列类型
现在您有了一个可以在网格中使用的自定义单元格类,但您还希望有一个包含 StatusCell 的自定义列类,可用于设置网格和数据绑定。如果您打算完全以编程方式使用自定义单元格类型,您可以只构造 DataGridViewColumn 基类的实例,并将 StatusCell 的实例传递给构造函数,这将把它设置为该列的 CellTemplate。然而,这种方法不允许您使用图 6.4 和 6.5 中介绍的设计器列编辑器来指定 StatusCell 的绑定或非绑定列。为了支持这一点,您需要实现一个设计器可以识别的自定义列类型。既然您要实现自己的列类型,您还希望公开一种方法来设置添加的新行的 StatusImage 的默认值。StatusColumn 类的实现如列表 6.5 所示。
列表 6.5:自定义列类
namespace CustomColumnAndCell
{
    public class StatusColumn : DataGridViewColumn
    {
        public StatusColumn() : base(new StatusCell()) { } 
        private StatusImage m_DefaultStatus = StatusImage.Red; 
        public StatusImage DefaultStatus
        {
            get { return m_DefaultStatus; }
            set { m_DefaultStatus = value; }
        }
        public override object Clone() 
        {
            StatusColumn col = base.Clone() as StatusColumn;
            col.DefaultStatus = m_DefaultStatus; return col; 
        }
        public override DataGridViewCell CellTemplate 
        {
            get
            {
                return base.CellTemplate;
            }
            set
            {
                if ((value == null) || !(value is StatusCell))
                {
                    throw new ArgumentException( "Invalid" + 
                          " cell type, StatusColumns " + 
                          "can only contain StatusCells");
                }
            }
        }
    }
}
从 StatusColumn 的实现中可以看出,您首先需要从 DataGridViewColumn 类派生。您实现一个默认构造函数,将自定义单元格类的实例传递给基类构造函数。这将基类的 CellTemplate 属性设置为该单元格类型,使其成为添加到包含您的列类型的网格中的任何行的单元格类型。
该类接下来定义了一个名为 DefaultStatus 的公共属性。这使得任何使用此列类型的人都可以设置在没有通过数据绑定或对单元格进行编程值设置的情况下,默认情况下应显示三个 StatusImage 值中的哪一个。该属性的设置器会更改跟踪当前默认值的成员变量。StatusCell.GetFormattedValue 方法会访问列上的 DefaultStatus 属性,如前所述。
在您的自定义列类型中,您需要做的另一件重要的事情是重写基类中的 Clone 方法,并在您的重写中返回一个新副本,其中所有属性都设置为与当前列实例相同的值。设计列编辑器使用此方法通过图 6.4 和 6.5 中讨论的对话框在网格中添加和编辑列。
自定义列类做的最后一件事是重写 CellTemplate 属性。如果有人试图访问 CellTemplate,代码会从基类获取它。但如果有人试图更改 CellTemplate,设置器会检查正在设置的单元格类型是否为 StatusCell。如果不是,它会引发异常,防止任何人以编程方式为此列设置不适当的单元格类型。这并不会阻止您将其他单元格类型混合到列中以创建异构网格(如前面关于以编程方式创建网格的部分所示)。
现在您已经定义了自定义单元格和列类型,您该如何使用它们呢?嗯,您可以将它们定义为 Visual Studio 中任何 Windows 应用程序项目类型的一部分,但通常当您创建这样的东西时,您是为了在各种应用程序中重用它。每当您想重用代码时,您需要将该代码放入一个类库中。因此,如果您定义一个类库项目,将刚刚讨论的类添加到类库中,以及您想要用于显示状态的图像作为项目中的嵌入式资源。这将创建一个程序集,然后您可以从任何您想在其中使用该列和单元格类型的 Windows 应用程序中引用它。您需要做的就是从您想要使用它们的 Windows Forms 项目中设置对该程序集的引用,自定义列类型将显示在“添加列”对话框中,如图 6.8 所示(在这种情况下是 StatusColumn)。
在您的 Windows Forms 应用程序中,您可以用编程方式向网格添加 StatusColumns,或者使用设计器来完成。如果您通过设计器添加列,然后在“编辑列”对话框中查看它,您会看到 DefaultStatus 出现在属性列表中,并且可以作为具有其允许值的枚举属性进行设置(参见图 6.9)。
在网格中添加了这种类型的列后,您可以用编程方式用单元格能够处理的任何一种类型的值(StatusImage 值或 StatusImage 值范围内的整数)来填充网格,或者您可以将其数据绑定到包含这些值的数据集合。这里有一个简单的示例,说明如何在包含两列的网格上以编程方式设置值:一个文本框列,


和一个 StatusColumn。请注意,您可以使用枚举值或适当的整数值来设置值。
m_Grid.Rows.Add("Beer Bottler", StatusImage.Green);
m_Grid.Rows.Add("Beer Bottle Filler", 1); //StatusImage.Yellow = 1
m_Grid.Rows.Add("Bottle capper", StatusImage.Red);
下载代码中的 CustomColumnAndCell 示例应用程序还演示了如何创建数据集并针对状态列进行数据绑定。
利用面向单元格的网格功能
您可能已经注意到,DataGridView 比其前身 DataGrid 更关注单元格级别。部分原因是网格的常见用途是列不一定决定网格内容的结构。具体来说,用户希望有类似电子表格的功能,模仿数百万人在使用 Microsoft Excel 等程序和其他电子表格应用程序时已经习惯的交互模型。
DataGridView 再次挺身而出,使支持该模型变得相当容易。您已经看到了一些单元格级事件,它们允许您控制在单元格级别显示的内容(CellFormatting 事件),以及在用户通过编辑内容(EditControlShowing 事件)或只是单击它(CellClick 事件)与单元格交互时通知您。您可以将不同的上下文菜单和工具提示设置到单元格级别,以便每个单元格都可以成为网格中从用户角度看的一个独特实体。DataGridView 实际上引发了超过 30 个事件,这些事件暴露了单元格级别的交互和修改,您可以订阅这些事件以提供面向单元格的功能。
此外,还有不同的选择模式,您可以使用它们来更改用户在网格中不同位置单击时网格高亮显示单元格、列或行的方式。网格上的 SelectionMode 属性决定了选择行为,其类型为 DataGridViewSelectionMode。DataGridView 控件支持这些选择模式(在表 6.6 中描述)。虽然您不能组合这些模式(该枚举不是 Flags 枚举类型),但您可以通过使用网格上的 SelectionMode 属性和一些额外的事件处理来实现模式的组合。无论您选择哪种模式,单击左上角的标题单元格(即行标题单元格上方和列标题单元格左侧的那个)都会选择网格中的所有单元格。
| 值 | 描述 | 
|---|---|
| CellSelect | 此模式允许您使用鼠标或键盘选择网格中的一个或多个单元格。如果您单击任何单元格,将只选择该单元格。您可以单击并拖动,您拖过的连续单元格也将被选中。如果您单击一个单元格,然后按住 Shift 键单击另一个单元格,您将选择从第一次单击到第二次单击的整个连续单元格集。您甚至可以通过按住 Ctrl 键单击单元格来选择不连续的单元格。这是默认的选择模式。 | 
| FullRowSelect | 单击网格中的任何单元格将选择包含该单元格的整行中的所有单元格,并取消选择该行外的任何单元格。 | 
| FullColumnSelect | 单击网格中的任何单元格将选择包含该单元格的整列中的所有单元格,并取消选择该列外的任何单元格。 | 
| RowHeaderSelect | 单击行标题单元格将选择整行,但除此之外,此选择模式的行为类似于 CellSelect。这是当您将网格添加到窗体时,设计器为网格设置的模式。 | 
| ColumnHeaderSelect | 单击列标题单元格将选择整列,但除此之外,此选择模式的行为类似于 CellSelect。 | 
作为一个更面向单元格的应用程序的例子,下载代码中包含一个名为 SimpleSpread 的应用程序。这个应用程序模仿一个简单的电子表格,让你对单元格中的数值进行求和。它结合了选择模式和一些事件处理,为你提供类似于大多数电子表格的选择体验——具体来说,它就像 RowHeaderSelect 和 ColumnHeaderSelect 的组合,即使你不能仅通过 SelectionMode 属性实现这一点。SimpleSpread 示例应用程序如图 6.10 所示。
如您所见,该应用程序允许您在单元格中输入数字;然后您可以选择一系列单元格并按顶部工具条控件中的“求和”按钮,它将计算总和并将结果放在所选序列的右侧或下方的下一个单元格中。如图 6.10 所示,此应用程序甚至支持选择矩形单元格组,并将在行和列方向上计算总和。逻辑远未完善到可以处理所有选择和单元格内容的组合,但它为您提供了一个很好的思路,说明如何设置类似的东西。
为了编写这段代码(如列表 6.6 所示),我必须做一些与普通 DataGridView 应用程序不同的事情。正如我所提到的,我想支持一种类似电子表格的选择模型,即您可以选择单个单元格,但选择列或行标题将分别选择整列或整行。为此,我将网格的 SelectionMode 设置为 RowHeaderSelect,在创建所有列并将其添加到网格时关闭了它们的排序功能,然后处理了 ColumnHeaderMouseClick 事件,以便在用户单击列标题时手动选择该列中的所有单元格。

列表 6.6:面向电子表格的网格列选择支持
public partial class SimpleSpreadForm : Form
{
    public SimpleSpreadForm()
    {
        InitializeComponent();
        m_Grid.SelectionMode = 
          DataGridViewSelectionMode.RowHeaderSelect;
    }
    private void OnFormLoad(object sender, EventArgs e)
    {
        int start = (int)'A';
        for (int i = 0; i < 26; i++)
        {
            string colName = ((char)(i + start)).ToString();
            int index = m_Grid.Columns.Add(colName, colName);
            m_Grid.Columns[i].SortMode = 
                      DataGridViewColumnSortMode.NotSortable;
            m_Grid.Columns[i].Width = 75;
        }
        for (int i = 0; i < 50; i++)
        {
            m_Grid.Rows.Add();
        }
    }
    private void OnColumnHeaderMouseClick(object sender, 
            DataGridViewCellMouseEventArgs e)
    {
        m_Grid.ClearSelection();
        foreach (DataGridViewRow row in m_Grid.Rows)
        {
            row.Cells[e.ColumnIndex].Selected = true;
        }
    }
    ... 
}
在这种情况下,我只是以编程方式向网格中添加了一些行和列,将列标题设置为字母表中的字母,并通过将 SortMode 属性设置为 NotSortable 来关闭列的排序功能。如果您要支持非常大的电子表格,您可能需要维护一个内存中的稀疏数组,并且只在需要时才呈现单元格(您可以使用虚拟模式来做到这一点),以避免在网格稀疏填充时维护大量单元格、其内容及其选择的开销。
为了让行号显示在行标题中,我处理了 RowAdded 事件,并在该处理程序中设置了标题单元格的值:
private void OnRowAdded(object sender, 
        DataGridViewRowsAddedEventArgs e)
{
    m_Grid.Rows[e.RowIndex].HeaderCell.Value = 
                        e.RowIndex.ToString();
}
您可能想要支持的另一种选择模式是热单元格,即当您在网格上移动鼠标时,单元格的选择会随之改变,而无需单击。要做到这一点,您只需处理 CellMouseEnter 和 CellMouseLeave 事件,分别在这些处理程序中选择和取消选择鼠标下的单元格即可。
使用样式进行格式化
我想介绍的关于 DataGridView 的最后一个主题是如何处理单元格的自定义格式化。如前所述,该网格支持丰富的格式化模型。网格中的样式采用分层模型工作,这允许您在更宏观的层面上设置样式,然后在更微观的层面上进行细化。例如,您可能设置一个适用于网格中所有单元格的默认单元格样式,但随后有一整列具有不同的单元格格式,并且该列中的选定单元格又具有另一种不同的单元格格式。您可以通过设置一系列在网格上公开的默认单元格样式属性来实现这一点,然后您可以通过在单个单元格级别设置单元格样式来对其进行细化。
如图 6.11 所示,模型中的最底层是 DefaultCellStyle 属性。默认情况下,网格中任何未被其他样式层设置样式的单元格都将使用此样式。上一层包含 RowHeadersDefaultCellStyle 和 ColumnHeadersDefaultCellStyle,它们影响标题单元格的呈现方式。再上一层是 DataGridViewColumn.DefaultCellStyle 属性,其后是 DataGridViewRow.DefaultCellStyle 属性,分别代表按列或按行的默认样式。网格还支持交替行单元格样式,通过网格上的 AlternatingRowsDefaultCellStyle 属性设置。最后,顶层将覆盖任何下层设置的层是 DataGridViewCell.CellStyle 属性。

您可以通过访问网格、列、行或单元格实例上的相应属性成员以编程方式设置这些属性。所有这些属性的类型都是 DataGridViewCellStyle,它公开了用于设置字体、颜色、对齐方式、填充和值格式的属性。您还可以通过设计器配置单元格样式。每当您通过设计器中的属性窗口或智能标记属性编辑器访问网格或列上的单元格样式属性之一时,您都会看到如图 6.12 所示的“单元格样式生成器”对话框。
使用此对话框中的属性字段,您可以为单元格如何显示其内容设置细粒度的选项,您甚至可以在对话框底部的“预览”窗格中看到它的外观。
您还可以使用网格的 CellBorderStyle、ColumnHeadersBorderStyle 和 RowHeadersBorderStyle 属性来设置单元格的边框样式。使用这些样式,您可以实现一些相当复杂的网格外观,如图 6.13 所示。在此示例中,在列和行级别设置了默认单元格样式,然后通过单个单元格选择来完成形状的填充。
但是,在使用单元格样式时,您仍然会遇到一些限制。例如,图 6.13 中所示网格的自然下一步是将已着色单元格的边框颜色设置为显示黑色边框。然而,仅通过单元格样式实际上无法实现这一点,因为可用的边框样式仅为 3D 效果,并且在网格级别应用于整个单元格,而不是单元格的单个侧面。但是,一如既往,您几乎总是可以通过自定义绘制或自定义单元格类型定义来完成您需要的工作。


我们现在在哪里?
本章介绍了 DataGridView 控件的所有主要功能。它重点关注了所涉及的代码及其功能。对于大多数常见情况,您只需在设计器中定义列,使用“数据源”窗口设置一些数据绑定,并可能混合一些自定义代码和事件处理程序即可获得所需的功能。DataGridView 控件比 .NET 1.0 中的 DataGrid 控件使用起来更简单、更灵活、功能更强大,您应该始终倾向于在新应用程序中使用 DataGridView 来呈现表格数据或需要以网格格式呈现的内容。
本章的一些关键要点是:
- 将 DataGridView的DataSource和DataMember属性设置为绑定到数据源的BindingSource是使用网格的标准方法。
- 使用设计器智能标记中的“编辑列”功能是编辑和自定义绑定和非绑定列类型和属性的最简单方法。
- 绑定列将使用类似于第 4 章中讨论的 Binding对象的自动类型转换和格式化。
- 要添加自定义单元格类型,您需要创建一个派生自 DataGridViewColumn的自定义列类型,以及一个派生自DataGridViewCell或内置派生单元格类型之一的自定义单元格类型。
接下来,我们将更深入地探讨 Windows Forms 中使用的数据绑定机制,特别是您需要理解和实现的接口,以便创建任何您想要用于数据绑定的自定义数据对象或集合。您将更好地理解数据绑定控件在设置数据绑定时寻找什么,以及它们如何使用它们找到的内容。
。如果您选择网格并显示其智能标记(如图 6.3 所示),则可以从那里修改网格外观和行为的最常见选项。“选择数据源”下拉列表会显示一个数据源选择窗口,类似于第 5 章中为“属性”窗口描述的窗口。呈现的数据源将仅限于那些实现了 IList 接口的数据源,因此适合绑定到网格。
“编辑列”和“添加列”链接会显示对话框,让您定义网格将包含的列,分别如图 6.4 和 6.5 所示。
“编辑列”对话框允许您添加和删除列,设置列在网格中的顺序,并在一个集中的对话框中为已定义的列设置所有设计时属性。对话框中显示的属性将根据列是绑定列还是非绑定列进行调整,并将根据列类型和单元格类型公开其他属性。如果您定义自定义列类型并将其包含在项目中,它们将作为新列的选项或通过此对话框配置列的选项显示出来。


“添加列”对话框(见图 6.5)允许您向网格添加一个新的数据绑定或非绑定列。如果您要添加数据绑定列,可以从当前选定数据源中可用的列中进行选择。您首先需要通过智能标记或属性窗口中的 DataSource 属性将数据源设置为适当的数据集合。如果您要添加非绑定列,则只需指定列的名称、列的类型和标题文本。当您单击“添加”按钮时,列会添加到网格中,并且对话框保持打开状态,以便您可以快速定义多个新列。

通过这些对话框配置列,会为您编写本章前面介绍的用于定义列和控制其运行时行为的所有代码。
DataGridView 智能标记上的“启用添加”复选框会将 AllowUserToAddRows 属性设置为 true(如果选中),这会在网格底部显示一个空的新行。这允许用户通过在单元格中键入新值来向数据集合添加新行。是否支持此功能取决于网格是否为数据绑定,如果是,则取决于基础对象集合是否支持向集合添加新项(请参见第 7 章的讨论)。同样,“启用编辑”复选框设置 ReadOnly 属性,该属性影响用户是否可以就地编辑网格内容,“启用删除”设置 AllowUserToDeleteRows 属性。“启用列重新排序”复选框设置 AllowUserToOrderColumns 属性,其行为将在下一节中描述。
“在父容器中停靠”链接仅在您首先将网格控件拖放到窗体上时才可用。它完全如其名——它只是将 Dock 属性设置为 Fill。
除了可以通过智能标记配置的常见属性和行为之外,还有许多其他属性和事件可以通过属性窗口在设计时进行配置。设置任何这些属性都会在设计器为窗体的 InitializeComponent 方法生成的局部类中生成相应的代码。最值得注意的是,您可以通过属性窗口配置任何数据绑定属性。您可能希望使用属性窗口设置样式,因为您可以在设计器中预览这些设置的结果,以确保您得到的是您期望的效果。本章末尾将更详细地讨论样式。
列重排序
列重新排序是网格的一项巧妙的内置行为,允许用户在运行时更改网格中列的显示顺序。由于应用程序的不同用户通常更关注网格中的某些列,因此用户通常要求自己设置网格中显示的列的顺序。虽然您可以通过以编程方式从网格中删除列然后将它们重新插入到新位置来支持此功能,但这需要编写大量繁琐的代码来处理一个常见的用例。因此,Windows 客户端团队很好地将此功能直接内置到网格控件中。
其工作原理是,如果 AllowUserToOrderColumns 属性设置为 true,并且用户单击并拖动列标题,网格允许他们将列拖放到他们希望显示的位置。放置位置右侧的列将向右移动一个位置,而拖动列原始位置周围的列将在列移动后移动到相邻位置。图 6.6 显示了此过程。在这种情况下,单击了 QuantityPerUnit 列并将其向左拖动。会绘制一个与您正在拖动的列标题大小相同的灰色框。当您将光标移动到另一列的一侧时,该列与相邻列之间的边框会变暗,指示如果您释放鼠标按钮,您正在拖动的列将被放置的位置。

当一列通过列重新排序被移动后,它的 ColumnIndex 不会改变,但 DisplayIndex 属性会指示它在网格中的当前显示顺序。默认情况下,网格的显示顺序不会在应用程序运行之间持久化,但您可以自己轻松地持久化该信息,并通过将显示顺序写入文件来恢复显示顺序。列表 6.3 中的代码演示了如何使用 XmlSerializer 类将数据持久化到独立存储中的文件中。
列表 6.3:持久化列的显示顺序
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    private void Form1_Load(object sender, EventArgs e) 
    {
        m_Grid.AllowUserToOrderColumns = true;
        SetDisplayOrder(); 
    } 
    private void OnFormClosing(object sender, FormClosingEventArgs e)
    {
        CacheDisplayOrder();
    }
    private void CacheDisplayOrder()
    {
        IsolatedStorageFile isoFile = 
            IsolatedStorageFile.GetUserStoreForAssembly();
        using (IsolatedStorageFileStream isoStream = 
            new IsolatedStorageFileStream("DisplayCache", 
            FileMode.Create, isoFile))
        {
            int[] displayIndices =new int[m_Grid.ColumnCount];
            for (int i = 0; i < m_Grid.ColumnCount; i++)
            {
                displayIndices[i] = m_Grid.Columns[i].DisplayIndex;
            }
            XmlSerializer ser = new XmlSerializer(typeof(int[]));
            ser.Serialize(isoStream,displayIndices); 
        }
    }
    private void SetDisplayOrder()
    {
        IsolatedStorageFile isoFile = 
          IsolatedStorageFile.GetUserStoreForAssembly();
        string[] fileNames = isoFile.GetFileNames("*");
        bool found = false;
        foreach (string fileName in fileNames)
        {
            if (fileName == "DisplayCache") 
                found = true;
        }
        if (!found) 
            return;
        using (IsolatedStorageFileStream isoStream = new 
               IsolatedStorageFileStream("DisplayCache", 
               FileMode.Open, isoFile)) 
        {
            try
            {
                XmlSerializer ser = new XmlSerializer(typeof(int[]));
                int[] displayIndicies = (int[])ser.Deserialize(isoStream);
                for (int i = 0; i < displayIndicies.Length; i++)
                {
                    m_Grid.Columns[i].DisplayIndex = displayIndicies[i];
                }
            }
            catch { } 
        }
    }
}
此代码与数据源没有任何特定关系。这里的关键方面是,窗体 Load 事件处理程序中的代码将 AllowUserToOrderColumns 属性设置为 true,从而允许通过拖放操作动态更改列的 DisplayIndex。然后,我添加了一个由 Form.Closing 事件处理程序调用的 CacheDisplayOrder 辅助方法,以及一个在窗体加载时调用的 SetDisplayOrder 辅助方法。
CacheDisplayOrder 首先收集网格中每列的所有显示索引值,并将它们放入一个整数数组中。然后,它创建一个独立存储文件流,并使用 XmlSerializer 类将该数组写入该流。SetDisplayOrder 方法执行相反的操作:它首先检查文件是否存在,如果存在,则将数组读回,并用它来设置网格中每列的 DisplayIndex。
定义自定义列和单元格类型
使用 DataGridView,您在呈现丰富数据方面已经遥遥领先于 DataGrid,因为它开箱即用地支持内置列类型。但是,您总是希望支持自定义场景以显示自定义列。幸运的是,DataGridView 使插入自定义列和单元格类型变得更加容易。
如果您只想自定义单元格的绘制过程,而不需要在列级别添加任何属性或控制,那么您有一个基于事件的选项,而不是创建新的列和单元格类型。您可以处理 CellPainting 事件并直接在单元格本身中进行绘制,通过内置的单元格类型和一些(可能很复杂的)绘制代码,您几乎可以实现任何您想要的效果。但是,如果您希望能够以可重用的方式插入您的列或单元格类型,并且与使用内置类型一样简单,那么您可以派生自己的列和单元格类型。
您应该遵循的用于插入自定义列类型的模型与您已经看到的内置类型的模型相匹配:您需要创建一个列类型和该列将包含的相应单元格类型。您可以通过直接或间接地从基类 DataGridViewColumn 和 DataGridViewCell 继承,或者通过内置类型之一来完成此操作。
详细解释这一点的最佳方法是举一个例子。假设我想实现一个自定义列类型,让我可以显示网格行所代表的项目的状态。我希望能够使用自定义枚举值设置状态,并且列中的单元格将根据单元格上设置的枚举值显示一个指示该状态的图形。为此,我定义了一个 StatusColumn 类和一个 StatusCell 类(我在这里放弃了内置类型的命名约定,即在所有类型上加上 DataGridView 前缀,因为类型名称变得太长了)。我希望这些类型能让我简单地将单元格的值设置为一个自定义枚举类型(我称之为 StatusImage)的值之一,无论是通过编程方式还是通过数据绑定。StatusImage 可以取值为 Green、Yellow 或 Red,我希望单元格根据其值显示相应的自定义图形。图 6.7 显示了具有此行为的正在运行的示例应用程序。
定义自定义单元格类型
为了实现这一点,第一步是定义自定义单元格类型。如果您要自己进行绘制,可以重写 DataGridViewCell 基类中受保护的虚方法 Paint。但是,如果您想呈现的单元格内容只是内置单元格类型之一的变体,您应该考虑从其中之一继承。在这种情况下我就是这么做的。因为我的自定义单元格仍然会呈现图像,所以 DataGridViewImageCell 类型是一个很自然的基类。不过,我的 StatusCell 类不会公开随机设置图像的功能;它旨在与枚举值一起工作。

我还希望单元格值能够处理整数,只要它们在枚举的相应数值范围内,这样我就可以支持枚举类型在数据库中存储为其相应整数值的常见情况。列表 6.4 中的代码显示了 StatusCell 类的实现。
列表 6.4:自定义单元格类
namespace CustomColumnAndCell
{
    public enum StatusImage { Green, Yellow, Red }
    public class StatusCell : DataGridViewImageCell
    {
        public StatusCell()
        {
            this.ImageLayout = DataGridViewImageCellLayout.Zoom;
        }
 
        protected override object GetFormattedValue(object value, 
                  int rowIndex, ref DataGridViewCellStyle cellStyle, 
                  TypeConverter valueTypeConverter, 
                  TypeConverter formattedValueTypeConverter, 
                  DataGridViewDataErrorContexts context)
        {
            string resource = "CustomColumnAndCell.Red.bmp";
            StatusImage status = StatusImage.Red;
            // Try to get the default value from the containing column
            StatusColumn owningCol = OwningColumn as StatusColumn;
            if (owningCol != null)
            {
                status = owningCol.DefaultStatus;
            }
            if (value is StatusImage || value is int)
            {
                status = (StatusImage)value;
            }
            switch (status)
            {
                case StatusImage.Green:
                    resource = "CustomColumnAndCell.Green.bmp";
                    break; 
                case StatusImage.Yellow:
                    resource = "CustomColumnAndCell.Yellow.bmp";
                    break;
                case StatusImage.Red:
                    resource = "CustomColumnAndCell.Red.bmp";
                    break;
                default:
                    break;
            }
            Assembly loadedAssembly = Assembly.GetExecutingAssembly();
            Stream stream = loadedAssembly.GetManifestResourceStream(resource);
            Image img = Image.FromStream(stream);
            cellStyle.Alignment = DataGridViewContentAlignment.TopCenter;
            return img;
        }
    }
}
此代码中的第一个声明是枚举 StatusImage。这是此单元格类型期望作为其 Value 属性的值类型。然后您可以看到 StatusCell 类型派生自 DataGridViewImageCell,因此我可以重用其在网格内呈现图像的能力。有一个默认状态字段和相应的属性,允许默认值直接显示。构造函数还将基类的 ImageLayout 属性设置为 Zoom,以便图像在不失真的情况下调整大小以适应单元格。
自定义单元格类型需要做的关键事情是,要么像前面提到的那样重写 Paint 方法,要么像 StatusCell 类那样重写 GetFormattedValue 方法。此方法将在每次呈现单元格时被调用,并允许您处理从其他类型到单元格预期类型的转换。我为本示例选择的 GetFormattedValue 编码方式是,首先将值设置为一个默认值,如果所有其他方法都失败,则将使用该值。然后,代码会尝试从包含列的 DefaultValue 属性中获取真正的默认值,前提是该列类型为 StatusColumn(稍后讨论)。然后,代码检查当前的 Value 属性是否是 StatusImage 枚举类型或整数,如果是整数,则将该值强制转换为枚举类型。
一旦确定了要呈现的状态值,GetFormattedValue 方法就会使用 switch-case 语句来选择与该状态值的图像相对应的适当资源名称。您可以通过将位图资源添加到 Visual Studio 项目中,并将其文件的“生成操作”属性设置为“嵌入的资源”,从而将位图资源嵌入到程序集中。然后,代码使用 Assembly 类上的 GetManifestResourceStream 方法从程序集中提取位图资源,在传递给该方法的 CellStyle 参数上设置对齐方式,然后将构造的图像作为对象从该方法返回。您从此方法返回的对象将作为格式化值传递给下游的 Paint 方法进行呈现。由于这不会覆盖 Paint 方法,因此将调用我的 DataGridViewImageCell 基类的实现,它需要一个 Image 值来呈现。
定义自定义列类型
现在您有了一个可以在网格中使用的自定义单元格类,但您还希望有一个包含 StatusCell 的自定义列类,可用于设置网格和数据绑定。如果您打算完全以编程方式使用自定义单元格类型,您可以只构造 DataGridViewColumn 基类的实例,并将 StatusCell 的实例传递给构造函数,这将把它设置为该列的 CellTemplate。然而,这种方法不允许您使用图 6.4 和 6.5 中介绍的设计器列编辑器来指定 StatusCell 的绑定或非绑定列。为了支持这一点,您需要实现一个设计器可以识别的自定义列类型。既然您要实现自己的列类型,您还希望公开一种方法来设置添加的新行的 StatusImage 的默认值。StatusColumn 类的实现如列表 6.5 所示。
列表 6.5:自定义列类
namespace CustomColumnAndCell
{
    public class StatusColumn : DataGridViewColumn
    {
        public StatusColumn() : base(new StatusCell()) { } 
        private StatusImage m_DefaultStatus = StatusImage.Red; 
        public StatusImage DefaultStatus
        {
            get { return m_DefaultStatus; }
            set { m_DefaultStatus = value; }
        }
        public override object Clone() 
        {
            StatusColumn col = base.Clone() as StatusColumn;
            col.DefaultStatus = m_DefaultStatus; return col; 
        }
        public override DataGridViewCell CellTemplate 
        {
            get
            {
                return base.CellTemplate;
            }
            set
            {
                if ((value == null) || !(value is StatusCell))
                {
                    throw new ArgumentException( "Invalid" + 
                          " cell type, StatusColumns " + 
                          "can only contain StatusCells");
                }
            }
        }
    }
}
从 StatusColumn 的实现中可以看出,您首先需要从 DataGridViewColumn 类派生。您实现一个默认构造函数,将自定义单元格类的实例传递给基类构造函数。这将基类的 CellTemplate 属性设置为该单元格类型,使其成为添加到包含您的列类型的网格中的任何行的单元格类型。
该类接下来定义了一个名为 DefaultStatus 的公共属性。这使得任何使用此列类型的人都可以设置在没有通过数据绑定或对单元格进行编程值设置的情况下,默认情况下应显示三个 StatusImage 值中的哪一个。该属性的设置器会更改跟踪当前默认值的成员变量。StatusCell.GetFormattedValue 方法会访问列上的 DefaultStatus 属性,如前所述。
在您的自定义列类型中,您需要做的另一件重要的事情是重写基类中的 Clone 方法,并在您的重写中返回一个新副本,其中所有属性都设置为与当前列实例相同的值。设计列编辑器使用此方法通过图 6.4 和 6.5 中讨论的对话框在网格中添加和编辑列。
自定义列类做的最后一件事是重写 CellTemplate 属性。如果有人试图访问 CellTemplate,代码会从基类获取它。但如果有人试图更改 CellTemplate,设置器会检查正在设置的单元格类型是否为 StatusCell。如果不是,它会引发异常,防止任何人以编程方式为此列设置不适当的单元格类型。这并不会阻止您将其他单元格类型混合到列中以创建异构网格(如前面关于以编程方式创建网格的部分所示)。
现在您已经定义了自定义单元格和列类型,您该如何使用它们呢?嗯,您可以将它们定义为 Visual Studio 中任何 Windows 应用程序项目类型的一部分,但通常当您创建这样的东西时,您是为了在各种应用程序中重用它。每当您想重用代码时,您需要将该代码放入一个类库中。因此,如果您定义一个类库项目,将刚刚讨论的类添加到类库中,以及您想要用于显示状态的图像作为项目中的嵌入式资源。这将创建一个程序集,然后您可以从任何您想在其中使用该列和单元格类型的 Windows 应用程序中引用它。您需要做的就是从您想要使用它们的 Windows Forms 项目中设置对该程序集的引用,自定义列类型将显示在“添加列”对话框中,如图 6.8 所示(在这种情况下是 StatusColumn)。
在您的 Windows Forms 应用程序中,您可以用编程方式向网格添加 StatusColumns,或者使用设计器来完成。如果您通过设计器添加列,然后在“编辑列”对话框中查看它,您会看到 DefaultStatus 出现在属性列表中,并且可以作为具有其允许值的枚举属性进行设置(参见图 6.9)。
在网格中添加了这种类型的列后,您可以用编程方式用单元格能够处理的任何一种类型的值(StatusImage 值或 StatusImage 值范围内的整数)来填充网格,或者您可以将其数据绑定到包含这些值的数据集合。这里有一个简单的示例,说明如何在包含两列的网格上以编程方式设置值:一个文本框列,


和一个 StatusColumn。请注意,您可以使用枚举值或适当的整数值来设置值。
m_Grid.Rows.Add("Beer Bottler", StatusImage.Green);
m_Grid.Rows.Add("Beer Bottle Filler", 1); //StatusImage.Yellow = 1
m_Grid.Rows.Add("Bottle capper", StatusImage.Red);
下载代码中的 CustomColumnAndCell 示例应用程序还演示了如何创建数据集并针对状态列进行数据绑定。
利用面向单元格的网格功能
您可能已经注意到,DataGridView 比其前身 DataGrid 更关注单元格级别。部分原因是网格的常见用途是列不一定决定网格内容的结构。具体来说,用户希望有类似电子表格的功能,模仿数百万人在使用 Microsoft Excel 等程序和其他电子表格应用程序时已经习惯的交互模型。
DataGridView 再次挺身而出,使支持该模型变得相当容易。您已经看到了一些单元格级事件,它们允许您控制在单元格级别显示的内容(CellFormatting 事件),以及在用户通过编辑内容(EditControlShowing 事件)或只是单击它(CellClick 事件)与单元格交互时通知您。您可以将不同的上下文菜单和工具提示设置到单元格级别,以便每个单元格都可以成为网格中从用户角度看的一个独特实体。DataGridView 实际上引发了超过 30 个事件,这些事件暴露了单元格级别的交互和修改,您可以订阅这些事件以提供面向单元格的功能。
此外,还有不同的选择模式,您可以使用它们来更改用户在网格中不同位置单击时网格高亮显示单元格、列或行的方式。网格上的 SelectionMode 属性决定了选择行为,其类型为 DataGridViewSelectionMode。DataGridView 控件支持这些选择模式(在表 6.6 中描述)。虽然您不能组合这些模式(该枚举不是 Flags 枚举类型),但您可以通过使用网格上的 SelectionMode 属性和一些额外的事件处理来实现模式的组合。无论您选择哪种模式,单击左上角的标题单元格(即行标题单元格上方和列标题单元格左侧的那个)都会选择网格中的所有单元格。
| 值 | 描述 | 
|---|---|
| CellSelect | 此模式允许您使用鼠标或键盘选择网格中的一个或多个单元格。如果您单击任何单元格,将只选择该单元格。您可以单击并拖动,您拖过的连续单元格也将被选中。如果您单击一个单元格,然后按住 Shift 键单击另一个单元格,您将选择从第一次单击到第二次单击的整个连续单元格集。您甚至可以通过按住 Ctrl 键单击单元格来选择不连续的单元格。这是默认的选择模式。 | 
| FullRowSelect | 单击网格中的任何单元格将选择包含该单元格的整行中的所有单元格,并取消选择该行外的任何单元格。 | 
| FullColumnSelect | 单击网格中的任何单元格将选择包含该单元格的整列中的所有单元格,并取消选择该列外的任何单元格。 | 
| RowHeaderSelect | 单击行标题单元格将选择整行,但除此之外,此选择模式的行为类似于 CellSelect。这是当您将网格添加到窗体时,设计器为网格设置的模式。 | 
| ColumnHeaderSelect | 单击列标题单元格将选择整列,但除此之外,此选择模式的行为类似于 CellSelect。 | 
作为一个更面向单元格的应用程序的例子,下载代码中包含一个名为 SimpleSpread 的应用程序。这个应用程序模仿一个简单的电子表格,让你对单元格中的数值进行求和。它结合了选择模式和一些事件处理,为你提供类似于大多数电子表格的选择体验——具体来说,它就像 RowHeaderSelect 和 ColumnHeaderSelect 的组合,即使你不能仅通过 SelectionMode 属性实现这一点。SimpleSpread 示例应用程序如图 6.10 所示。
如您所见,该应用程序允许您在单元格中输入数字;然后您可以选择一系列单元格并按顶部工具条控件中的“求和”按钮,它将计算总和并将结果放在所选序列的右侧或下方的下一个单元格中。如图 6.10 所示,此应用程序甚至支持选择矩形单元格组,并将在行和列方向上计算总和。逻辑远未完善到可以处理所有选择和单元格内容的组合,但它为您提供了一个很好的思路,说明如何设置类似的东西。
为了编写这段代码(如列表 6.6 所示),我必须做一些与普通 DataGridView 应用程序不同的事情。正如我所提到的,我想支持一种类似电子表格的选择模型,即您可以选择单个单元格,但选择列或行标题将分别选择整列或整行。为此,我将网格的 SelectionMode 设置为 RowHeaderSelect,在创建所有列并将其添加到网格时关闭了它们的排序功能,然后处理了 ColumnHeaderMouseClick 事件,以便在用户单击列标题时手动选择该列中的所有单元格。

列表 6.6:面向电子表格的网格列选择支持
public partial class SimpleSpreadForm : Form
{
    public SimpleSpreadForm()
    {
        InitializeComponent();
        m_Grid.SelectionMode = 
          DataGridViewSelectionMode.RowHeaderSelect;
    }
    private void OnFormLoad(object sender, EventArgs e)
    {
        int start = (int)'A';
        for (int i = 0; i < 26; i++)
        {
            string colName = ((char)(i + start)).ToString();
            int index = m_Grid.Columns.Add(colName, colName);
            m_Grid.Columns[i].SortMode = 
                      DataGridViewColumnSortMode.NotSortable;
            m_Grid.Columns[i].Width = 75;
        }
        for (int i = 0; i < 50; i++)
        {
            m_Grid.Rows.Add();
        }
    }
    private void OnColumnHeaderMouseClick(object sender, 
            DataGridViewCellMouseEventArgs e)
    {
        m_Grid.ClearSelection();
        foreach (DataGridViewRow row in m_Grid.Rows)
        {
            row.Cells[e.ColumnIndex].Selected = true;
        }
    }
    ... 
}
在这种情况下,我只是以编程方式向网格中添加了一些行和列,将列标题设置为字母表中的字母,并通过将 SortMode 属性设置为 NotSortable 来关闭列的排序功能。如果您要支持非常大的电子表格,您可能需要维护一个内存中的稀疏数组,并且只在需要时才呈现单元格(您可以使用虚拟模式来做到这一点),以避免在网格稀疏填充时维护大量单元格、其内容及其选择的开销。
为了让行号显示在行标题中,我处理了 RowAdded 事件,并在该处理程序中设置了标题单元格的值:
private void OnRowAdded(object sender, 
        DataGridViewRowsAddedEventArgs e)
{
    m_Grid.Rows[e.RowIndex].HeaderCell.Value = 
                        e.RowIndex.ToString();
}
您可能想要支持的另一种选择模式是热单元格,即当您在网格上移动鼠标时,单元格的选择会随之改变,而无需单击。要做到这一点,您只需处理 CellMouseEnter 和 CellMouseLeave 事件,分别在这些处理程序中选择和取消选择鼠标下的单元格即可。
使用样式进行格式化
我想介绍的关于 DataGridView 的最后一个主题是如何处理单元格的自定义格式化。如前所述,该网格支持丰富的格式化模型。网格中的样式采用分层模型工作,这允许您在更宏观的层面上设置样式,然后在更微观的层面上进行细化。例如,您可能设置一个适用于网格中所有单元格的默认单元格样式,但随后有一整列具有不同的单元格格式,并且该列中的选定单元格又具有另一种不同的单元格格式。您可以通过设置一系列在网格上公开的默认单元格样式属性来实现这一点,然后您可以通过在单个单元格级别设置单元格样式来对其进行细化。
如图 6.11 所示,模型中的最底层是 DefaultCellStyle 属性。默认情况下,网格中任何未被其他样式层设置样式的单元格都将使用此样式。上一层包含 RowHeadersDefaultCellStyle 和 ColumnHeadersDefaultCellStyle,它们影响标题单元格的呈现方式。再上一层是 DataGridViewColumn.DefaultCellStyle 属性,其后是 DataGridViewRow.DefaultCellStyle 属性,分别代表按列或按行的默认样式。网格还支持交替行单元格样式,通过网格上的 AlternatingRowsDefaultCellStyle 属性设置。最后,顶层将覆盖任何下层设置的层是 DataGridViewCell.CellStyle 属性。

您可以通过访问网格、列、行或单元格实例上的相应属性成员以编程方式设置这些属性。所有这些属性的类型都是 DataGridViewCellStyle,它公开了用于设置字体、颜色、对齐方式、填充和值格式的属性。您还可以通过设计器配置单元格样式。每当您通过设计器中的属性窗口或智能标记属性编辑器访问网格或列上的单元格样式属性之一时,您都会看到如图 6.12 所示的“单元格样式生成器”对话框。
使用此对话框中的属性字段,您可以为单元格如何显示其内容设置细粒度的选项,您甚至可以在对话框底部的“预览”窗格中看到它的外观。
您还可以使用网格的 CellBorderStyle、ColumnHeadersBorderStyle 和 RowHeadersBorderStyle 属性来设置单元格的边框样式。使用这些样式,您可以实现一些相当复杂的网格外观,如图 6.13 所示。在此示例中,在列和行级别设置了默认单元格样式,然后通过单个单元格选择来完成形状的填充。
但是,在使用单元格样式时,您仍然会遇到一些限制。例如,图 6.13 中所示网格的自然下一步是将已着色单元格的边框颜色设置为显示黑色边框。然而,仅通过单元格样式实际上无法实现这一点,因为可用的边框样式仅为 3D 效果,并且在网格级别应用于整个单元格,而不是单元格的单个侧面。但是,一如既往,您几乎总是可以通过自定义绘制或自定义单元格类型定义来完成您需要的工作。


我们现在在哪里?
本章介绍了 DataGridView 控件的所有主要功能。它重点关注了所涉及的代码及其功能。对于大多数常见情况,您只需在设计器中定义列,使用“数据源”窗口设置一些数据绑定,并可能混合一些自定义代码和事件处理程序即可获得所需的功能。DataGridView 控件比 .NET 1.0 中的 DataGrid 控件使用起来更简单、更灵活、功能更强大,您应该始终倾向于在新应用程序中使用 DataGridView 来呈现表格数据或需要以网格格式呈现的内容。
本章的一些关键要点是:
- 将 DataGridView的DataSource和DataMember属性设置为绑定到数据源的BindingSource是使用网格的标准方法。
- 使用设计器智能标记中的“编辑列”功能是编辑和自定义绑定和非绑定列类型和属性的最简单方法。
- 绑定列将使用类似于第 4 章中讨论的 Binding对象的自动类型转换和格式化。
- 要添加自定义单元格类型,您需要创建一个派生自 DataGridViewColumn的自定义列类型,以及一个派生自DataGridViewCell或内置派生单元格类型之一的自定义单元格类型。
接下来,我们将更深入地探讨 Windows Forms 中使用的数据绑定机制,特别是您需要理解和实现的接口,以便创建任何您想要用于数据绑定的自定义数据对象或集合。您将更好地理解数据绑定控件在设置数据绑定时寻找什么,以及它们如何使用它们找到的内容。

