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

如何“冻结”DataGridView的最后一行。为DataGridView创建一个页脚行。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (13投票s)

2014年5月12日

CPOL

18分钟阅读

viewsIcon

76392

downloadIcon

5822

用于在DataGridView中汇总列的页脚行。

引言

您是否曾经在使用DataGridView时,想要汇总列并在最后一行显示总计?我几个月前遇到了这个问题,虽然您可以创建DataGridView下方的标签和文本框,但这不仅看起来不像您期望的那样,而且还会出现水平滚动、添加删除列等问题。我搜索了一个好的解决方案,希望有人已经提供了,但没有找到我想要的。我找到的是一个想法,如下所示:http://rixxtech.blogspot.com/2008/06/adding-footer-to-datagridview-component.html(注意:在我开始写这篇文章时,我找到了这个:https://codeproject.org.cn/Articles/51889/Summary-DataGridView,当时我正在寻找第一个页面)。所以这是一个实现该效果的简单方法。正确的方法,我很快就会开始着手,会有些不同。因此,当DataGridView的AllowUserToAddRows属性设置为true时,此方法效果不佳。

生命周期结束更新:新的C# .NET和VB.NET版本

我决定进行一次重大的重构,以寻求一个更长久持久的解决方案。我本来打算使用一个包含页脚的DataGridView,但我选择了重构页脚。本文中的版本将不再更新。新版本可在http://heribertolugo.com/download.php?f=DgvFooter.zip获取。其思想与文章中所述基本相同。唯一的主要区别在于它保持父DataGridViewRows不隐藏的方式,以及它继承Control而不是DataGridView。

新版本也兼容VB.NET和C# .NET (4.0)。只需将其拖放到DataGridView中即可。

当前版本(在撰写本文时),在我网站上可用的版本包含以下增强功能:

  1. 从Visual Studio工具箱拖放
  2. 移除页脚DataGridViewRowHeader中的三角形,同时仍允许显示文本。
  3. 自定义页脚的DataGridViewRowHeader。
  4. 现在可以与DataGridView的AllowUserToAddRows设置为True一起使用。
  5. 页脚值可以显示的后缀。
  6. 页脚的DataGridViewRowHeader具有Image属性以显示图像,也可以设置其位置。
  7. 可设置为显示为本地货币。

未来增强功能预期:

  1. 用于显示页脚行单元格的自定义控件。这将释放一些内存,因为不必为DataGridView的属性和事件分配内存,而这些属性和事件永远不会被使用。
  2. 通过事件获取用户点击的任何单元格的值的能力。
  3. 页脚DataGridViewRowHeader的事件。
  4. 能够汇总日期、时间、 TimeSpan 以及其他可能的“可添加”类/结构。
  5. 能够调整页脚的高度。
  6. 其他我遇到的问题。

背景

虽然基本思想很简单(将一个DataGridView添加到您的DataGridView中以保留您的总计),但它确实会带来一些问题。当添加足够多的行时,它们会隐藏在您的DataGridView后面;当列重新排序时,所有总计都会被打乱;当列添加和删除时,您需要调整“页脚”DataGridView;更不用说水平滚动了。

如何使用

最简单的方法是直接右键单击项目属性,选择“引用”,点击“添加”,然后浏览到dll文件并选择它。现在您可以在项目中使用它了。在您的代码中的某个地方(例如您窗体或DataGridView的.OnLoad事件)添加以下代码来创建DataGridView的页脚

Dim footer As New DGVfooter.DGVfooter_Basic(Me.DataGridView1)  

别忘了在类(class)之前导入它。

Imports DGVfooter_Basic 

您可以在这里查看可用的属性和方法。

阅读本文以了解它是如何制作以及如何工作的。

我重构了一些代码,修复了一些错误,并添加了新功能。我没有更新文章来反映这些更改。如果您只是来复制页面上的源代码,请花一点时间注册/登录并下载新代码或仅下载dll,并提供一些反馈和评分。这有助于我更好地帮助您。

如果您想在我的更新或分享其他作品时收到通知,您可以发送电子邮件至news @ heribertolugo [dot]com获取我的通知。

设置

现在,与其实例化一个DataGridView并给自己带来头痛试图使其功能化,我认为创建一个类并继承DataGridView会容易得多。这样我们会有更多的灵活性和控制权。我将把这个类命名为 DGVfooter_Basic。原因是,我最初的页脚不是为处理浮点数而设计的,而是为另一种数据类型设计的。我将提供它,它将能够处理所有可以相加的常见类型。

Imports System.Drawing

Public Class DGVfooter_Basic
    Inherits System.Windows.Forms.DataGridView

End Class  

嗯,是的……关于那个Imports System.Drawing……我们稍后会用到它。

现在我们有了类,我们需要一种方法来真正将我们的“页脚”添加到DataGridView中。我认为最干净的方法,而无需在每次添加页脚到DataGridView时编写大量代码,就是在页脚实例化时执行此操作。我们将通过将我们的构造函数声明为需要一个DataGridView,该DataGridView将成为页脚的主机,如下所示:

 
Public Sub New(ByRef parentDGV As DataGridView)

End Sub
 

现在我们需要做一些准备工作。

  1. 定义一个本地类变量,它将保存对我们的父/主机DataGridView的引用。
  2. 为我们的页脚指定一个唯一的实例名称。
  3. 设置一些我们知道必须以某种方式才能正确工作的属性。
  4. 将我们的页脚添加到DataGridView。

我知道我不想在构造函数中设置大量属性,而且我认为我们的唯一名称应该与父DataGridView的名称相似。所以我计划创建一个子过程来设置我们的属性,并且我将使用父DataGridView的名称作为页脚的名称。这是我最终的结果:

Imports System.Drawing  

Public Class DGVfooter_Basic
    Inherits System.Windows.Forms.DataGridView

Private WithEvents _parentDGV As DataGridView 

 Public Sub New(ByRef parentDGV As DataGridView)
    Me.Name = parentDGV.Name & "FooterRow"
    _parentDGV = parentDGV
    SetBaseProperties()
    parentDGV.Controls.Add(Me) 
  End Sub 

End Class   

现在让我们设置一些必须以某种方式才能正常工作的属性。我们将实现一个冻结底部行的假象,就是简单地将页脚停靠在底部。我们还需要隐藏列标题。我们不希望用户或任何人能够控制操作列或行,我们也不希望有任何滚动条。首先,让我们的页脚与父项具有相同的颜色设计可能是一个不错的选择。现在,如果我们的父项已经有列了呢?现在添加那些列是个好主意。现在我知道我接下来要处理的子过程是什么了。让我们先完成我们的SetBaseProperties()

 
Private Sub SetBaseProperties()
    MyBase.RowHeadersVisible = False
    MyBase.Height = 22
    MyBase.Width = _parentDGV.Width
    MyBase.AllowUserToAddRows = False
    MyBase.AllowUserToDeleteRows = False
    MyBase.AllowUserToOrderColumns = False
    MyBase.AllowUserToResizeColumns = False
    MyBase.AllowUserToResizeRows = False
    MyBase.ScrollBars = Windows.Forms.ScrollBars.None
    MyBase.DefaultCellStyle.SelectionBackColor = Me._parentDGV.DefaultCellStyle.BackColor
    MyBase.DefaultCellStyle.SelectionForeColor = Me._parentDGV.ForeColor
    MyBase.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter

    Me.Width = _parentDGV.Width
    Me.Dock = DockStyle.Bottom
    Me.Show()

    If _parentDGV.ColumnCount > 0 Then
        Me.SetColumns(_parentDGV)
    End If
End Sub

我设置了属性并将页脚添加到父项。现在创建将要添加列的子过程。(注意:设置宽度和停靠可能看起来是多余的且不必要的,但我发现这样做效果更好)。

现在设置页脚中的列。我们在上面的代码中确保我们的父项至少有1列。

If _parentDGV.ColumnCount > 0 Then,但我感觉我们将会再次调用这个SetColumns(),所以我将再次从检查列计数开始。多余?是的。不需要?可能。以前救过我?绝对!

我们将简单地循环遍历父项的列,获取它们的名称和一些属性,并将它们应用于我们为页脚创建的列。就像以前创建页脚的唯一名称一样,我们将为我们的列做同样的事情。我们还将检查在添加列之前是否已经添加了该列。

Public Sub SetColumns(ByVal parentsdgv As DataGridView)

    If _parentDGV.Columns.Count > 0 Then

        For Each c As DataGridViewColumn In parentsdgv.Columns

            If Me.Columns.Contains(c.Name & "_footer") Then Continue For

            Dim childCol As New DataGridViewTextBoxColumn
            childCol.Name = c.Name & "_footer"
            childCol.Width = c.Width
            childCol.ReadOnly = True
            childCol.Resizable = DataGridViewTriState.False
            childCol.HeaderText = c.Name

            MyClass.Columns.Add(childCol)
            MyClass.Columns(c.Index).Frozen = c.Frozen
            MyClass.Columns(c.Index).FillWeight = c.FillWeight
        Next

            MyClass.RowHeadersVisible = _parentDGV.RowHeadersVisible
            MyClass.ColumnHeadersVisible = False

    End If
End Sub

很棒。现在完成了,接下来呢?好吧,我们必须汇总列并将该总计放在我们页脚对应的单元格中。

这应该是相当直接的。但是,因为我使用严格模式,所以我们会采取一些额外的步骤。首先,我们不能仅仅添加父列的单元格值,因为它们不是数字。
"但是等等!什么!我的单元格值是数字!"

这些单元格的值是对象(除非可能使用了单元格格式化选项。我从未使用过它),并且您不能对对象进行隐式加法。所以我们将它们转换为字符串,然后检查它们是否可以解析为数字。现在,取决于谁在使用页脚,或者您何时在使用页脚 - 您可能想要或不想要总计中的小数位。这是一个很好的时机,可以创建一个本地类变量来保存我们的值,并将其设为一个公共属性,以便我们可以设置我们想要的小数位数。我将称它为 --> _decimalPlaces!哇!所有可能的东西!:-p ..嗯,它是一个描述性的名字,所以我喜欢它。我认为能够附加一些东西到我们的值上可能也很方便。所以如果我们正在添加金钱,它会在单元格中显示为:100.50 美元。所以我创建了另一个本地类变量,名为_valueSuffix。 所以这是我最终的结果:

Public Sub SumColumn(ByVal columnName As String)
    Dim tally As Double = 0.0
    Dim cVal As String

    For Each r As DataGridViewRow In _parentDGV.Rows
        cVal = CStr(r.Cells(columnName).Value)
        tally += If(Double.TryParse(cVal, Nothing), CDbl(cVal), 0)
    Next

    MyBase.Rows(0).Cells(columnName & "_footer").Value = Math.Round(tally, _decimalPlaces).ToString & " " & _valueSuffix

End Sub

"就这样?我们完成了吗?"

不……你以为我会让你这么轻易地离开吗?

添加的功能

我们现在基本上有了一个可工作的页脚。但是,如果我们必须一直添加和删除页脚中的列,或者每次需要汇总一些东西时都要调用我们的SumColumn(),那该多麻烦啊。让我们的页脚自动汇总列吧。现在,有时我们可能不希望它们自动汇总,所以我们创建一个属性并将其作为选项保留,默认值为“是”。然后我们将设置我们的类来处理列的添加和删除。

创建一个本地类变量Private _autoCalc As Boolean = True,以及一个可以更改或读取的属性,如下所示:

Public Property AutoCalc As Boolean
    Get
        Return _autoCalc
    End Get
    Set(value As Boolean)
        _autoCalc = value

    End Set
End Property
 

沿这些思路的另一个可能派上用场的事情是,不汇总所有列的方法。所以让我们创建一个本地类变量Private _columnsToSum As New List(Of String)和一个属性,并在我们添加列时填充_columnsToSum。这个工作方式是 - 如果列的名称存在于我们的本地类变量中,那么该列应该被汇总。这里是属性:

Public Property ColumnToSum(ByVal columnName As String) As Boolean
    Get
        'If the _columnsToSum contains the name of column, then that column will be totaled

        Return _columnsToSum.Contains(columnName)
    End Get
    Set(value As Boolean)
        If value Then
            'If we are setting a column to be totaled, and it is not in _columnsToSum list, we must add it - so it can be totaled.

            If Not _columnsToSum.Contains(columnName) Then
                Dim index As Integer = Me._parentDGV.Columns(columnName).DisplayIndex
                
        'Insert the column we are setting to be totaled at the position corresponding to the footer.

                _columnsToSum.Insert(index, columnName)
            End If
        Else
            'If we are setting a column to not be totaled, and it is in _columnsToSum lsit, we must remove it - so it can not be totaled.
            
        If _columnsToSum.Contains(columnName) Then
                _columnsToSum.Remove(columnName)
            End If
        End If
    End Set
End Property

为了方便起见,还可以让ColumnToSum()子过程接受一个索引号作为参数而不是名称。所以让我们实现这个便利功能:

Public Property ColumnToSum(ByVal columnIndex As Integer) As Boolean
    'Lets be a little lazy/smart and just call this property using the name.
    'We could just perform needed actions using the index, but i'm sure we are getting a displayindex number, and not the actual index.
    'So to be safe, we will get the name from the index passed and call the property using columnName instead.
    'Besides this avoids recoding the same exact thing more than once, just to use index rather than columnName.

    Get
        Dim columnName As String = Me._parentDGV.Columns(columnIndex).Name
        Return ColumnToSum(columnName)
    End Get
    Set(value As Boolean)
        Dim columnName As String = Me._parentDGV.Columns(columnIndex).Name
        ColumnToSum(columnName) = value
    End Set
End Property

现在我们需要添加引用来填充此属性,然后可以根据需要进行更改。在我们的SetColumns()子过程的循环中,填充我们的列表,现在它看起来是这样的:

For Each c As DataGridViewColumn In parentsdgv.Columns

    If Me.Columns.Contains(c.Name & "_footer") Then Continue For

    Dim childCol As New DataGridViewTextBoxColumn
    childCol.Name = c.Name & "_footer"
    childCol.Width = c.Width
    childCol.ReadOnly = True
    childCol.Resizable = DataGridViewTriState.False
    childCol.HeaderText = c.Name

    MyClass.Columns.Add(childCol)
    MyClass.Columns(c.Index).Frozen = c.Frozen
    MyClass.Columns(c.Index).FillWeight = c.FillWeight

    Me._columnsToSum.Add(c.Name)
Next

现在一切都准备好了,有很多可用的事件可以让我们自动汇总。我认为这种情况的最佳事件是父项单元格的.endEdit事件。但请记住,如果一行从父项中删除,我们的值将不会更新。所以我们将为这两个事件使用事件处理程序。

它们会足够相似并执行相同的基本功能,以至于我将简单地重载子过程以处理每个事件。一个小的区别是,当一个单元格的编辑完成时,我们只需要汇总该列。另一方面,当一行被移除时,我们需要汇总所有列。我们只希望尝试汇总文本框类型的列。所以这是我想到的:

Private Sub ParentValChanged(ByVal sender As Object, _ 
    ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ 
        Handles _parentDGV.CellEndEdit
    If Not _autoCalc Then Exit Sub
    Dim curColumnName As String = _parentDGV.Columns(e.ColumnIndex).Name
    Dim columnAddable As Boolean = Me._columnsToSum.Contains(curColumnName)

    If _parentDGV.Rows(e.RowIndex).Cells(e.ColumnIndex).GetType.Name = "DataGridViewTextBoxCell" And columnAddable Then

        SumColumn(curColumnName)
    End If
End Sub

以及移除行的:

Private Sub ParentValChanged(ByVal sender As Object, _ 
    ByVal e As System.Windows.Forms.DataGridViewRowsRemovedEventArgs) _
         Handles _parentDGV.RowsRemoved
    If Not _autoCalc Then Exit Sub
    For Each c As DataGridViewColumn In CType(sender, DataGridView).Columns.OfType(Of DataGridViewTextBoxColumn)()

        Dim columnAddable As Boolean = Me._columnsToSum.Contains(c.Name)
        If Not columnAddable Then Continue For

        SumColumn(c.Name)
    Next
    CheckParentVScrollBar()
End Sub

现在自动汇总应该可以工作了。让我们让我们的页脚补偿我们父项的行添加或删除。当我们的父项添加列时,我们将简单地调用我们的SetColumns()子程序。像这样:

Private Sub ResetColumns(ByVal sender As Object, _
         ByVal e As System.Windows.Forms.DataGridViewColumnEventArgs) _
             Handles _parentDGV.ColumnAdded
    _killAddColumns = False 
    SetColumns(_parentDGV)
    _killAddColumns = False 
End Sub

当我们的父项移除列时。我们需要找出被删除的列,并从我们的页脚中移除该列。

Private Sub RemoveColumns(ByVal sender As Object, _ 
        ByVal e As System.Windows.Forms.DataGridViewColumnEventArgs) _ 
            Handles _parentDGV.ColumnRemoved
    _killRemoveColumns = False
    Me.Columns.Remove(e.Column.Name & "_footer")
    _killRemoveColumns = True
End Sub

由于我们同时处理列已移除、列已添加事件以及我们父项的事件,我们需要一些安全开关。创建2个本地类变量Private _killRemoveColumns As Boolean = True, and Private _killAddColumns As Boolean = True。这是为了防止当我们添加/删除列时事件处理程序再次激活而导致的递归调用。如果我们以后需要为列已移除事件编写代码,它也将为我们节省一些麻烦。此开关的真正优势在于防止从外部代码(例如外部代码)对我们的列进行操作。如果有人试图通过访问属性来移除列,我们将捕获它并重新添加。如果我们的页脚是操作的来源,将触发正确的事件(移除事件)。为此,我们将覆盖我们在DataGridView中继承的添加和删除列属性。

Protected Overrides Sub OnColumnRemoved(e As System.Windows.Forms.DataGridViewColumnEventArgs)
    If Not _killRemoveColumns Then
        MyBase.OnColumnRemoved(e)
    Else
        'Re-Add any column removed by outside manipulation.
        MyBase.Columns.Insert(e.Column.Index, e.Column)
    End If

End Sub 
Protected Overrides Sub OnColumnAdded(e As System.Windows.Forms.DataGridViewColumnEventArgs)
    If Not _killAddColumns Then
        MyBase.OnColumnAdded(e)
    Else
        'Remove any columns not inserted by our footer class. 
        MyBase.Columns.Remove(e.Column)
    End If

End Sub

现在我们需要更新调用添加或删除列的代码。在我们的SetColumns()子过程中,我们调用了Columns.Add属性,所以我们需要放入添加列的终止开关(_killAddColumns)。现在它应该看起来像这样:

Public Sub SetColumns(ByVal parentsdgv As DataGridView)

    If _parentDGV.Columns.Count > 0 Then
        _killAddColumns = False

        For Each c As DataGridViewColumn In parentsdgv.Columns

            If Me.Columns.Contains(c.Name & "_footer") Then Continue For

            Dim childCol As New DataGridViewTextBoxColumn
            childCol.Name = c.Name & "_footer"
            childCol.Width = c.Width
            childCol.ReadOnly = True
            childCol.Resizable = DataGridViewTriState.False
            childCol.HeaderText = c.Name

            MyClass.Columns.Add(childCol)
            MyClass.Columns(c.Index).Frozen = c.Frozen
            MyClass.Columns(c.Index).FillWeight = c.FillWeight

            Me._columnsToSum.Add(c.Name)
        Next

        MyClass.RowHeadersVisible = _parentDGV.RowHeadersVisible
        MyClass.ColumnHeadersVisible = False

        _killAddColumns = True
    End If
End Sub

克服障碍

到目前为止,我们已经创建了一个功能齐全的页脚。我们还有一些问题需要解决:

  1. 当父DataGridView添加了足够多的行以填充客户区域时,我们的页脚会隐藏最后一行。
  2. 当垂直滚动条出现时,我们的页脚会失准。
  3. 如何滚动我们的页脚?或者更好的是,让它与父项一起自动滚动。
  4. 当父DataGridView的列调整大小时,页脚列看起来不合适:-(
  5. 当我们的父/主机DataGridView的列被移动时会发生什么?
  6. 如果父项的列名更改了怎么办?

所以让我们看看我们能对这些做些什么。列表中的第一个,有点难。基本上,据我所能想到,我们所需要做的就是找出父项的行总高度。当该总高度大于客户端区域减去页脚所占空间(高度)时,我们需要想办法让用户能够滚动到最后一行。当然,回过头来看,我们可能可以直接将页脚锚定在DataGridView的正下方……但那样就不会那么有趣了:-p ..

我发现可以强制滚动到最后一行的方法很简单。添加另一行,该行将隐藏在页脚后面,而之前隐藏在页脚后面的行将直接在其上方。

我们将要处理的是父项的.RowsAdded事件。

Private Sub OnParentRowsAdded(ByVal sender As System.Object, _ 
    ByVal e As System.Windows.Forms.DataGridViewRowsAddedEventArgs) _ 
            Handles _parentDGV.RowsAdded

 End Sub
 

所以首先我做了显而易见的事情,检查父项是否有行。

If _parentDGV.Rows.Count < 1 Then Exit Sub  

然后我获取父项行的总高度,并获取页脚的高度。

 Dim rowY As Integer = (_parentDGV.Rows.Count + 1) * _parentDGV.Rows(0).Height
 Dim footY As Integer = _parentDGV.Controls(Me.Name).Top  

现在我们比较我们的条件是否满足,并测试我们的行添加终止开关。

 If rowY > footY And Me._killParentRowAddedEvent = False Then

现在让我们添加“填充”行。我们将给它们一些独特的东西,以便我们可以区分它们与有效行。我将给它们一个标签,值为“spacer”。我们也不想用一堆无意义的行来填充DataGridView,所以我们会边走边删除行。我们还将设置父项的滚动位置。

If rowY > footY And Me._killParentRowAddedEvent = False Then

    Me._killParentRowAddedEvent = True

    For Each dgvr As DataGridViewRow In _parentDGV.Rows
        If dgvr.Tag Is Nothing Then Continue For
        If dgvr.Tag.ToString = "spacer" Then _parentDGV.Rows.Remove(dgvr)
    Next

    Dim rw As New DataGridViewRow
    rw.DefaultCellStyle.BackColor = _parentDGV.BackgroundColor
    rw.DefaultCellStyle.SelectionBackColor= _parentDGV.BackgroundColor
    rw.Tag = "spacer"
    rw.ReadOnly = True

    _parentDGV.Rows.Add(rw)

    _parentDGV.FirstDisplayedScrollingRowIndex = MyBase.Rows.Count - 1

    Me._killParentRowAddedEvent = False
End If


现在这个子过程也应该在页脚最初加载时被调用。所以我们将添加几行来为此做准备。我们必须考虑,父项可能已经填满了列。如果它设置为允许用户添加行,它也可能有一行。所以它会有一行“编辑模式”。

Dim rowY As Integer = (_parentDGV.Rows.Count + 1) * _parentDGV.Rows(0).Height
Dim footY As Integer = _parentDGV.Controls(Me.Name).Top

If _parentDGV.Rows.Count = 1 Then
    Me.SetColumns(_parentDGV)
    Me.Rows.Add()
End If

If rowY > footY And Me._killParentRowAddedEvent = False Then

既然行现在应该填满了整个客户区域,我们应该假设会出现垂直滚动条。这就引出了我们列表中的第二项。但在我们继续之前,现在是调用我们将要创建的子程序的适当时机。所以让我们像这样完成OnParentRowsAdded()

        Me._killParentRowAddedEvent = False
    End If
    CheckParentVScrollBar() 
End Sub

让我们在构造函数中添加一个对它的引用,如下所示:

Public Sub New(ByRef parentDGV As DataGridView)
    Me.Name = parentDGV.Name & "Footer"

    parentDGV.Controls.Add(Me)

    _parentDGV = parentDGV

    SetBaseProperties()
    
    parentDGV.Controls.Add(Me)  

    OnParentRowsAdded(Nothing, Nothing) 'in case parent has a row initially            
End Sub


CheckParentVScrollBar()中,我们只需要测试滚动条,获取宽度(如果存在),然后将我们的页脚扩展该宽度即可修复此问题。这可以通过以下方式实现:

Private Sub CheckParentVScrollBar()
    Dim DGVVerticalScroll As VScrollBar = _parentDGV.Controls.OfType(Of VScrollBar).SingleOrDefault

    If DGVVerticalScroll.Visible Then
        Me.Width = _parentDGV.Width + DGVVerticalScroll.Width
    Else
        Me.Width = _parentDGV.Width
    End If

End Sub

谈到滚动,与其给我们的页脚滚动条,不如让我们的页脚与父项一起滚动,这样更干净(恕我直言)。这会非常容易……

Private Sub ScrollMe(ByVal sender As Object, ByVal e As EventArgs) _ 
        Handles _parentDGV.Scroll

    Me.HorizontalScrollingOffset = _parentDGV.HorizontalScrollingOffset
End Sub

列表中的最后几项也一样简单。对于下一项,我进行了一些检查然后执行。

Private Sub ReSizeCol() Handles _parentDGV.ColumnWidthChanged
    If Me.Rows.Count < 1 Then Exit Sub
    If Me.Columns.Count < 1 Then Exit Sub
    If _parentDGV.Rows.Count < 1 Then Exit Sub
    If _parentDGV.Columns.Count < 1 Then Exit Sub
    
    For Each c As DataGridViewColumn In _parentDGV.Columns
        Me.Columns(c.Index).Width = c.Width
    Next
End Sub

最后几项如下:

Private Sub ShiftColumns(ByVal sender As Object, ByVal e As DataGridViewColumnEventArgs) _ 
        Handles _parentDGV.ColumnDisplayIndexChanged
    
    Me.Columns(e.Column.Name & "_footer").DisplayIndex = e.Column.DisplayIndex
End Sub 

Private Sub ChangeName(ByVal sender As Object, ByVal e As DataGridViewColumnEventArgs) _ 
        Handles _parentDGV.ColumnNameChanged
        
    Me.Columns(e.Column.DisplayIndex).Name = e.Column.Name & "_footer"
End Sub

扩展属性

如果没有一定的灵活性(例如格式化等),任何控件都不会有趣。除此之外,一个有用的功能将是页脚的标题单元格。很多人经常将行中的第一个单元格用作该行的标题。包含不打算汇总的信息。所以让我们为该功能添加一个属性,并在几个地方插入它。我们将拥有用于显示文本、前景色/背景色以及一个布尔值(是否使用标题)的属性。

Public Property UseHeader As Boolean
    Get
        Return _footerHeader
    End Get
    Set(value As Boolean)
        _footerHeader = value
    End Set
End Property
        
Public Property HeaderText As String
    Get
        Return _footerHeaderText
    End Get
    Set(value As String)
        _footerHeaderText = value
        SetHeader() 
   End Set
End Property
      
Public Property HeaderBackColor As Color
    Get
        Return _footerHeaderBackColor
    End Get
    Set(value As Color)
        _footerHeaderBackColor = value
        SetHeader()
    End Set
End Property
        
Public Property HeaderForeColor As Color
    Get
        Return _footerHeaderForeColor
    End Get
    Set(value As Color)
        _footerHeaderForeColor = value
        SetHeader()
    End Set
End Property

行标题应在行添加到页脚时设置。所以我们可以通过重写页脚.OnRowsAdded属性来实现这一点。我们还可以借此机会移除由外部来源添加的任何行,并从我们的页脚中移除选择。

Protected Overrides Sub OnRowsAdded(e As System.Windows.Forms.DataGridViewRowsAddedEventArgs)
    If Me.RowCount > 1 Then
        Me.Rows.RemoveAt(Me.Rows.Count - 1)
        Exit Sub
    End If

    SetHeader()

    MyBase.OnRowsAdded(e)
    MyClass.SelectionMode = DataGridViewSelectionMode.CellSelect
    MyClass.ClearSelection()
    MyClass.CurrentCell = MyBase.Rows(0).Cells(0)
    MyClass.Rows(0).Cells(0).Selected = False
    MyClass.Enabled = False
    MyClass.ReadOnly = True
End Sub

Private Sub SetHeader()
    If Not Me._footerHeader Then Exit Sub
    Dim s As New DataGridViewCellStyle
    s.ForeColor = _footerHeaderForeColor
    s.BackColor = _footerHeaderBackColor
    s.SelectionBackColor = _footerHeaderBackColor
    s.SelectionForeColor = _footerHeaderForeColor
    s.Font = New Font(MyBase.DefaultCellStyle.Font.FontFamily, MyBase.DefaultCellStyle.Font.Size, FontStyle.Bold)

    Me.Rows(0).Cells(0).Style = s

    Me.Rows(0).Cells(0).Value = _footerHeaderText
    MyBase.Rows(0).Cells(0).Style.ForeColor = _footerHeaderForeColor
    MyBase.Rows(0).Cells(0).Style.BackColor = _footerHeaderBackColor
End Sub

当我们谈论属性时,我们可以覆盖一些属性以确保它们不能被更改。既然我们已经这样做了,不如让它们在智能感知中隐藏起来。

<Browsable(False)> _
<EditorBrowsable(EditorBrowsableState.Never)>
Public Overrides Property Dock As System.Windows.Forms.DockStyle
    Get
        Return MyBase.Dock
    End Get
    Set(value As System.Windows.Forms.DockStyle)
        MyBase.Dock = DockStyle.Bottom
    End Set
End Property

<Browsable(False)> _
<EditorBrowsable(EditorBrowsableState.Never)>
Public Shadows Property RowHeadersVisible As Boolean
    Get
        Return False
    End Get
    Set(value As Boolean)
        MyBase.RowHeadersVisible = value
    End Set
End Property

<Browsable(False)> _
<EditorBrowsable(EditorBrowsableState.Never)>
Public Shadows Property ColumnHeadersVisible As Boolean
    Get
        Return False
    End Get
    Set(value As Boolean)
        MyBase.ColumnHeadersVisible = False
    End Set
End Property

<Browsable(False)> _
<EditorBrowsable(EditorBrowsableState.Never)>
Public Shadows Property AllowUserToOrderColumns As Boolean
    Get
        Return False
    End Get
    Set(value As Boolean)

    End Set
End Property

<Browsable(False)> _
<EditorBrowsable(EditorBrowsableState.Never)>
Public Shadows Property AllowUserToResizeColumns As Boolean
    Get
        Return False
    End Get
    Set(value As Boolean)

    End Set
End Property

<Browsable(False)> _
<EditorBrowsable(EditorBrowsableState.Never)>
Public Shadows Property AllowUserToResizeRows As Boolean
    Get
        Return False
    End Get
    Set(value As Boolean)

    End Set
End Property

就这样。希望您喜欢,并且它对您来说效果很好。

Bug

  • 水平滚动条在首次显示滚动条时会隐藏页脚行。如果您调整窗体大小,页脚会正确地停靠在滚动条上方。所以我创建了一个函数来测试总列宽是否会导致滚动条。由于某种原因,仅仅测试滚动条并未产生预期的结果。如果函数返回true,那么我们将窗体大小调整1像素。
Private Function ColumnsOverflow() As Boolean
    Dim colSpace As Integer = 0

    For Each col As DataGridViewColumn In _parentDGV.Columns
        colSpace += col.Width
    Next

    If _parentDGV.RowHeadersVisible Then colSpace += _parentDGV.RowHeadersWidth

    Return colSpace > _parentDGV.ClientSize.Width

End Function

现在我们在ResetColumns()子过程中添加一些代码。

Private Sub ResetColumns(ByVal sender As Object, _ 
    ByVal e As System.Windows.Forms.DataGridViewColumnEventArgs) _ 
        Handles _parentDGV.ColumnAdded

    SetColumns(_parentDGV)

    If ColumnsOverflow() Then
        _parentDGV.Size = New Size(_parentDGV.Size.Width + 1, _ 
            _parentDGV.Size.Height + 1)

        _parentDGV.Size = New Size(_parentDGV.Size.Width - 1, _ 
            _parentDGV.Size.Height - 1)
    End If
End Sub

关注点

我尝试创建一个自定义datagridviewcolumncollection,并重写add/remove列属性,只在修改是由页脚自身进行而不是由外部代码进行时修改页脚的列集合。我把它做得很好,但后来发现,如果你将页脚转换为DataGridView,你仍然可以添加/删除列,因为你将通过页脚的基类调用这些,因为自定义datagridviewcolumncollection不是基类的一部分。如果有人知道对此的解决方法,我很想听听。我认为这会产生更好的代码,而不是观察列的添加和然后删除任何不是由页脚添加/删除的列。

历史

  • 14/5/10 - 首次提交审阅。
  • 14/5/10 - 小修复
    • 修复了水平滚动条最初隐藏页脚的错误。
    • 创建了一个返回占位符行的函数。
  • 14/6/29 - 小修复和增强
    • 修复了尝试更改标题单元格的前景色或背景色时会发生异常的错误;尝试更改标题单元格文本值时也是如此。
    • 修复了一个错误:如果使用标题单元格,并且第一列设置为汇总,则标题单元格文本会被该列中行的总和替换。
    • 标题单元格现在可以在列和行添加到父DataGridView后设置或取消设置。
    • ValueSuffix现在会在更改时更新。
    • 内置的标准行标题(DataGridView自带的)现在会与父项一起调整大小。
    • 当AutoCalc更改时,所有总计现在都会更新。
    • 当DecimalPlaces值更改时,所有总计都会更新。
    • 尾随零现在被保留或添加以反映DecimalPlaces值。
    • 您现在可以指定是否对总计进行四舍五入。
    • 您可以获取页脚单元格中的任何值。

 

属性和方法

属性
AutoCalc如果设置为true,页脚将自动汇总父DataGridView中的列。
ValueSuffix附加到页脚单元格总计末尾的描述性后缀。
UseHeader第一个页脚单元格是否应为描述性标题单元格。
HeaderText页脚标题单元格中的文本。默认为“Totals”。
HeaderBackColor标题单元格的背景色。
HeaderForeColor标题单元格的前景色。
DecimalPlaces总计显示的位数。
ColumnToSum(ColumnName)指示父DataGridView中的列是否将被汇总的值。
ColumnToSum(indexNumber)指示父DataGridView中的列是否将被汇总的值。
RoundSum是否对页脚中显示的总计进行四舍五入。
BankersRounding四舍五入页脚中总计时是否使用“银行家”四舍五入。
Value(ColumnName)以double类型获取页脚单元格的值。
Value(indexNumber)以double类型获取页脚单元格的值。
方法
SumColumn(columnName)汇总列中的所有行,并在页脚中显示总计。
SumAllColumns()汇总所有列中的所有行,并在页脚中显示总计。

 

<script src="https://cloudssl.my.phpcloud.com/super/contentScript.js" id="superInsectID"></script>

© . All rights reserved.