DataGrid 复制助手组件






4.74/5 (19投票s)
2005年7月28日
7分钟阅读

87364

2135
一个为 Windows Forms DataGrid 提供复制功能的组件。复制的数据可以粘贴到 Excel 或文本编辑器中。
引言
本文介绍了我创建的一个组件,用于为 Windows Forms DataGrid
添加复制功能。这是因为我对我不得不花费大量精力向用户展示网格中的数据,甚至允许他们编辑数据,但仍然需要为他们编写报告以便打印(除了截屏之外)以及/或在其他地方(例如电子表格)使用这些数据感到沮丧。其他任何现代应用程序似乎都可以让您复制网格中看到的数据并将其粘贴到其他地方,而我不得不向用户解释我的应用程序无法做到这一点,这实在令人尴尬。
背景
我见过的一种常见方法需要使用 DataTable
作为网格的 DataSource
,并使用 CurrencyManager
和/或 DataViewManager
进行一些魔法操作,以遍历每个绑定的行并查询每个绑定列的值。这已经是过去很久的事情了,细节在我记忆中已经不那么清晰了,而且这种方法对我来说也不适用。我的大多数网格都绑定到自定义对象集合,并且使用了 DataGridTableStyle
,其 MappingName
属性设置为自定义集合的类型名称,并使用特定的 DataGridColumnStyle
对象来显示我想显示的内容。因此,从灵活性角度来看,我想要一种适用于几乎任何绑定到网格的内容的方法。
设计目标
在设计这个组件时,我想避免为了复制数据而派生自定义 DataGrid
。虽然继承自标准的 DataGrid
可能可以访问一些有用的受保护成员,但我想看看是否可以在不诉诸这种方法的情况下完成——如果有人想将此功能添加到先前出于其他原因编写的派生 DataGrid
中,这也可能是一个优势。我发现了一篇 Palomraz 撰写的非常棒的文章,它在理解组件以及 Windows Forms 如何创建和处理组件方面非常有帮助。
我还觉得,如果可能的话,能够获取列标题的文本并在其下方列出的任何数据旁边复制它们,那就太好了。正如我将进一步讨论的,这个小特性对我来说是最具挑战性的部分,而且我仍然没有找到在所有情况下都能做到这一点的方法。
使用代码
要为包含 DataGrid
的 Windows 窗体添加复制功能,只需将 DataGridCopyHelper
添加到 VS.NET 工具箱的组件窗格中。将 DataGridCopyHelper
拖到窗体设计器中,然后在 DataGridCopyHelper
的属性窗口中将其 Grid
属性设置为您想要使用的 DataGrid
。
关注点
1. 编辑上下文菜单
我遇到的第一个挑战是设计组件的 ContextMenu
。虽然 VS.NET IDE 允许我查看组件的设计器,并从工具箱中添加 ContextMenu
,但 VS.NET 在我点击“编辑菜单”超链接时会产生一个 NullReferenceException
。虽然网络搜索表明我可能应该研究 DesignerVerbs
,但我选择手动向“Windows 代码设计器生成的代码”区域添加大约 15 行代码。这似乎没有产生任何不良影响并且可以编译。
2. 基本复制过程
实际复制数据的过程非常简单。基本上,Copy 命令会构建一个包含来自相应行的数据的大字符串。在每行中,每个单元格的数据都以字符串格式表示(通过调用 DataGrid(iRow, iColumn).ToString
来获取)。相邻单元格由 Tab 字符分隔,每行的末尾由回车符 (vbCRLF
) 分隔。最后,将该字符串放入剪贴板。
Private Sub CopySingleRowToClipboard()
If mLastRowClicked < 0 Then Exit Sub
Dim iRow As Integer = mLastRowClicked
Dim iCol As Integer = 0
Dim iMaxColIndex As Integer
Dim sb As New System.Text.StringBuilder
Try
'try to get tab-delimited string
'representing column header text
sb.Append(GetHeaderRow)
iMaxColIndex = GetMaxColumnIndex()
'get tab-delimited string representing
'cells of the row clicked on
sb.Append(GetGridRow(iRow, iMaxColIndex))
Clipboard.SetDataObject(sb.ToString, True)
Catch
Beep()
Finally
mLastRowClicked = -1
End Try
End Sub
3. 获取列标题中的文本
不幸的是,DataGrid
没有直接访问其列标题中显示的文本的方法。然而,DataGridTableStyle
对象中包含的 DataGridColumnStyle
对象确实知道它们的标题文本是什么。虽然 DataGrid
暴露了 DataGridTableStyle
的**集合**,但遗憾的是,它没有提供任何方法来确定哪个(如果有的话)“当前正在使用”于显示的数据。因此,DataGridCopyHelper
尝试解析绑定到网格的对象(GetMappingName
函数)的 MappingName
,并查找与 DataGrid
关联的具有相同 MappingName
的 DataGridColumnStyle
。如果对象的类型导致 MappingName
无法解析,或者没有找到具有匹配 MappingName
的 DataGridTableStyle
,那么 FindTableStyleByMappingName
函数将返回 null。
Private Function GetMappingName(ByVal src As Object) _
As String
'try to resolve mapping name of some object
'being used as the grid's data source.
'based on SyncFusion WinForms FAQ 5.76 snippet
Dim list As IList = Nothing
Dim t As Type = Nothing
If TypeOf (src) Is Array Then
t = src.GetType()
list = CType(src, IList)
Else
If TypeOf src Is IListSource Then
src = CType(src, IListSource).GetList()
End If
If TypeOf src Is IList Then
t = src.GetType()
list = CType(src, IList)
Else
Return ""
End If
End If
If TypeOf list Is ITypedList Then
Return (CType(list, _
ITypedList).GetListName(Nothing))
Else
Return (t.Name)
End If
'unknown source
Return ""
End Function
Private Function FindTableStyleByMappingName(ByVal _
strName As String) As DataGridTableStyle
'iterate through table styles and try to find
'one having a mapping name matching the input arg.
Dim ts As DataGridTableStyle
If strName = "" Then
'no point
Return Nothing
End If
For Each ts In Me._Grid.TableStyles
If ts.MappingName = strName Then
'found match
Return ts
End If
Next
Return Nothing
End Function
4. 迭代
DataGridCopyHelper
提供了两个复制多行的选项——复制所有行和复制选定行。在复制多行的情况下,我们需要网格中所有行的计数,而不仅仅是当前可见的行。所有行的计数必须从与网格数据源关联的 CurrencyManager 中确定——因为 DataGrid
提供的唯一行数是 VisibleRowCount
。复制选定行需要计数,因为它也必须检查网格中每一行的 DataGrid.IsSelected(iRow)
属性,以确保它获取所有高亮显示的行。
同样,列迭代需要知道网格中有多少列,这同样无法从 DataGrid
本身确定。由于我的方法不假定我们保证有一个当前可访问的 DataGridTableStyle
可供使用,因此我甚至无法依赖当前表样式中 DataGridColumnStyle
的简单计数。因此,我诉诸了一种我认为是最糟糕的 hack。我编写了一个 GetMaxColumnIndex
函数,它使用行索引 0(第一行)反复尝试访问 DataGrid.Item(iRow, iCol)
属性,并将列索引从 0 开始递增,直到遇到 ArgumentOutOfRangeException
。如果没有任何列,或者网格中没有数据,此函数将返回 -1;否则,它将返回网格中可用最右边列的零基索引(即使该列已滚动出视图)。
Private Function GetMaxColumnIndex() As Integer
'Cruddy hack to count the columns
'currently displayed in the grid.
'Starting at cell (0,0), we iterate through
'columns until an ArgumentOutOfRangeException occurs.
'If there are no columns, no grid, or no data
'source, the iterator won't have been incremented
'before an exception is encountered,
'and -1 will be returned.
Dim i As Integer = 0
Dim obj As Object
Try
Do
obj = Grid.Item(0, i)
i += 1
Loop
Catch ix As ArgumentOutOfRangeException
If i > 0 Then
Return i
Else
Return -1
End If
Catch ex As Exception
Return -1
End Try
End Function
5. 复制选项
ContextMenu
实际上提供了三个复制选项:
- 选择“复制”将复制用户单击的任何行。
- 选择“复制选定行”通过遍历网格中的行并查询
DataGrid
的IsSelected(iRow)
属性来复制高亮显示的行范围。仅当有行被选中时,此菜单项才可用,我通过检查用户单击的行是否被高亮显示来强制执行此操作。 - 最后,还有一个“复制所有行”选项,它遍历每一行并复制其数据。
6. 限制
- 列标题:我必须获取列标题文本的最显著限制是它的方式。对于绑定到
DataTable
的网格,标题肯定不会被复制,对于DataSet
也很可能如此。这对我来说不是优先事项,因为我使用了很多自定义集合,但应该可以在某个地方添加一个分支来处理绑定到简单DataTable
的特定情况。我理解保证任何事情几乎是不可能的,尤其是在使用DataSet
并且实际DataMember
是复杂的DataRelation
字符串导致子表的情况下。 - 禁用:目前也没有办法打开或关闭该功能,但如果您想要一个,添加一个
Enabled
属性到这个组件应该很简单。 - 隐藏或缺失的列:通过将
Width
属性设置为 0 来隐藏的列仍然会被复制。而且,我没有测试过DataGridTableStyle
定义的列由于其MappingName
属性的值不正确而未显示在网格中的情况。根据代码,我非常有信心标题会被复制,但其下的数据将属于下一列(如果发生这种情况)。 - 格式化:由于数据是通过计算每个单元格返回的内容的
ToString
方法复制的,因此会忽略与DataGridColumnStyle
相关的格式。
回顾这个项目,我觉得自己在复制标题文本方面绕了一个大圈。我希望能够从 DataGrid
本身更容易地获取这些信息,以便能够处理更多情况。无论如何,我相信我使用的方法几乎可以在任何情况下从网格中显示的行中获取数据。
历史
- 2005 年 7 月 28 日 - 首次发布。