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

DataGrid 复制助手组件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (19投票s)

2005年7月28日

7分钟阅读

viewsIcon

87364

downloadIcon

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 关联的具有相同 MappingNameDataGridColumnStyle。如果对象的类型导致 MappingName 无法解析,或者没有找到具有匹配 MappingNameDataGridTableStyle,那么 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 实际上提供了三个复制选项:

  1. 选择“复制”将复制用户单击的任何行。
  2. 选择“复制选定行”通过遍历网格中的行并查询 DataGridIsSelected(iRow) 属性来复制高亮显示的行范围。仅当有行被选中时,此菜单项才可用,我通过检查用户单击的行是否被高亮显示来强制执行此操作。
  3. 最后,还有一个“复制所有行”选项,它遍历每一行并复制其数据。

6. 限制

  • 列标题:我必须获取列标题文本的最显著限制是它的方式。对于绑定到 DataTable 的网格,标题肯定不会被复制,对于 DataSet 也很可能如此。这对我来说不是优先事项,因为我使用了很多自定义集合,但应该可以在某个地方添加一个分支来处理绑定到简单 DataTable 的特定情况。我理解保证任何事情几乎是不可能的,尤其是在使用 DataSet 并且实际 DataMember 是复杂的 DataRelation 字符串导致子表的情况下。
  • 禁用:目前也没有办法打开或关闭该功能,但如果您想要一个,添加一个 Enabled 属性到这个组件应该很简单。
  • 隐藏或缺失的列:通过将 Width 属性设置为 0 来隐藏的列仍然会被复制。而且,我没有测试过 DataGridTableStyle 定义的列由于其 MappingName 属性的值不正确而未显示在网格中的情况。根据代码,我非常有信心标题会被复制,但其下的数据将属于下一列(如果发生这种情况)。
  • 格式化:由于数据是通过计算每个单元格返回的内容的 ToString 方法复制的,因此会忽略与 DataGridColumnStyle 相关的格式。

回顾这个项目,我觉得自己在复制标题文本方面绕了一个大圈。我希望能够从 DataGrid 本身更容易地获取这些信息,以便能够处理更多情况。无论如何,我相信我使用的方法几乎可以在任何情况下从网格中显示的行中获取数据。

历史

  • 2005 年 7 月 28 日 - 首次发布。
DataGrid 复制助手组件 - CodeProject - 代码之家
© . All rights reserved.