DataGridView 真·列头不高亮 & 真·列头合并

高亮BUG

VB.Net,在 .NET Framework 4.8 的 WinForm 下(即不是 WPF 的绘图模式、也不是 Core 或 Mono 的开发框架),使用 DataGridView 行模式,还是有个列头表现为高亮显示:
DataGridView 真·列头不高亮 & 真·列头合并_第1张图片
查找各种解决方式:

  • 设置 ColumnHeadersDefaultCellStyle ———— 无效
  • 直接修改每列的 HeaderCell.Style ———— 无效

既然有上述"解决方式",说明早期版本是有效的。至于从哪个版本开始无效,就不深究了,反正碰上了如下解决。

真·解决方式

只能在 CellPainting 事件中进行自绘了,顺便实现了列头合并功能(不需要多行列头)。

  1. 添加一个RowDataGridView用户控件,集成不需要设计,关掉直接改代码。
  2. RowDataGridView.Designer.vb 按注释修改
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class RowDataGridView
    Inherits System.Windows.Forms.DataGridView                      '<- 原先是 UserControl

    'UserControl 重写释放以清理组件列表。
    <System.Diagnostics.DebuggerNonUserCode()> _
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        Try
            If disposing AndAlso components IsNot Nothing Then
                components.Dispose()
            End If
        Finally
            MyBase.Dispose(disposing)
        End Try
    End Sub

    'Windows 窗体设计器所必需的
    Private components As System.ComponentModel.IContainer

    '注意: 以下过程是 Windows 窗体设计器所必需的
    '可以使用 Windows 窗体设计器修改它。  
    '不要使用代码编辑器修改它。
    <System.Diagnostics.DebuggerStepThrough()> _
    Private Sub InitializeComponent()
        components = New System.ComponentModel.Container()
        Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font  '<- 编译错误,删除该行
    End Sub

End Class
  1. RowDataGridView.vb
Public Class RowDataGridView

    Private m_ColHeadersSpan() As String

    ''' 列头合并
    ''' 务必在定义后修改。合并设置仅按次序、不随列定义同步调整。
    ''' 每列格式:
    ''' 
    ''' 数值后的注释仅供参考
    ''' 数值 1: (默认)非合并列头
    ''' 数值 0: 被合并列头
    ''' 其他正整数: 合并开始列头
    ''' 
    ''' ☆ 数值正确性不检查。
    ''' ☆ 合并不影响自动列宽计算(即标题可能撑开合并开始列)。
    Public Property ColHeadersSpan() As String()
        Get
            If Me.ColumnCount > 0 Then
                Dim lastCount As Integer
                If m_ColHeadersSpan Is Nothing Then
                    lastCount = 0
                    ReDim m_ColHeadersSpan(Me.ColumnCount)
                Else
                    lastCount = m_ColHeadersSpan.Length
                    ReDim Preserve m_ColHeadersSpan(Me.ColumnCount)
                End If
                For i As Integer = 0 To Me.ColumnCount - 1
                    If i < lastCount Then
                        m_ColHeadersSpan(i) = $"{Val(m_ColHeadersSpan(i))} '{Me.Columns(i).HeaderText}"
                    Else
                        m_ColHeadersSpan(i) = $"1 '{Me.Columns(i).HeaderText}"
                    End If
                Next
            End If
            Return m_ColHeadersSpan
        End Get
        Set(value As String())
            m_ColHeadersSpan = value
        End Set
    End Property

    Private ReadOnly Property ColHeadersSpanValue(ByVal index As Integer) As Integer
        Get
            Return Val(m_ColHeadersSpan(index))
        End Get
    End Property

   Private Sub RowDataGridView_CellPainting(sender As Object, e As DataGridViewCellPaintingEventArgs) Handles Me.CellPainting
        If (Not Me.DesignMode) And (e.RowIndex = -1) And (e.ColumnIndex <> -1) Then
            Debug.Print($"{e.ColumnIndex} : {e.Value}")
            Dim colSpan As Integer = Me.ColHeadersSpanValue(e.ColumnIndex)
            If colSpan > 0 Then
                Dim cellRect As New Rectangle(e.CellBounds.X - 1, e.CellBounds.Y, e.CellBounds.Width, e.CellBounds.Height - 1)
                ' RowHeadersVisible = False 时最左可见列不需要向左合并网格线
                If e.CellBounds.X = 1 Then
                    cellRect.X = 1
                    cellRect.Width -= 1
                End If
                ' 添加被合并列的宽度
                For i As Integer = 1 To colSpan - 1
                    cellRect.Width += Me.Columns(e.ColumnIndex + i).Width
                Next
                Dim foreColorBrash As New SolidBrush(e.CellStyle.ForeColor)
                Dim backColorBrush As New SolidBrush(e.CellStyle.BackColor)
                Dim gridBrush As New SolidBrush(Me.GridColor)
                Dim gridLinePen As New Pen(gridBrush)

                Try
                    e.Graphics.FillRectangle(backColorBrush, cellRect)
                    e.Graphics.DrawRectangle(gridLinePen, cellRect)

                    If e.FormattedValue IsNot Nothing Then
                        Dim format As New StringFormat()
                        Select Case e.CellStyle.Alignment
                            Case DataGridViewContentAlignment.BottomLeft, DataGridViewContentAlignment.MiddleLeft, DataGridViewContentAlignment.TopLeft
                                format.Alignment = StringAlignment.Near
                            Case DataGridViewContentAlignment.BottomCenter, DataGridViewContentAlignment.MiddleCenter, DataGridViewContentAlignment.TopCenter
                                format.Alignment = StringAlignment.Center
                            Case Else
                                format.Alignment = StringAlignment.Far
                        End Select
                        Select Case e.CellStyle.Alignment
                            Case DataGridViewContentAlignment.BottomCenter, DataGridViewContentAlignment.BottomLeft, DataGridViewContentAlignment.BottomRight
                                format.LineAlignment = StringAlignment.Center
                            Case DataGridViewContentAlignment.MiddleCenter, DataGridViewContentAlignment.MiddleLeft, DataGridViewContentAlignment.MiddleRight
                                format.LineAlignment = StringAlignment.Far
                            Case Else
                                format.LineAlignment = StringAlignment.Near
                        End Select
                        cellRect.Height += 1 ' 使得垂直居中和非自绘比较一致
                        e.Graphics.DrawString(CStr(e.FormattedValue), e.CellStyle.Font, foreColorBrash, cellRect, format)
                    End If
                Finally
                    gridLinePen.Dispose()
                    gridBrush.Dispose()
                    backColorBrush.Dispose()
                    foreColorBrash.Dispose()
                End Try
            End If
            e.Handled = True
        End If
    End Sub

End Class

注:

  1. 没有包括 New() 统一初始化行模式、是否显示行头等。
  2. 实现列头合并最正统的做法是给DataGridView*Column写继承类。但是仅为了一个属性需要给每种列类型写继承类,不如直接加属性在 DataGridView 上。
  3. 本来想把 m_ColHeadersSpan 定义成 Integer 数组,然后属性 ColHeadersSpan 加注释变字符数组方便设计器中编辑;但是 Visual Studio 死活不支持。只能加属性 ColHeadersSpanValue 实时解析,反正之前有列头判断,不会频繁调用。
  4. 继承自 VB6 的 Val() 函数容错性高,直接忽略数值之后的内容;不需要字符串拆分后转类型。

效果

对于已设计表格,只需要在 窗体.Designer.vb 中把 DataGridView 替换成 RowDataGridView 即可(注意前缀命名空间)。需要列头合并时设计器中修改 ColHeadersSpan,比如上例表格设为

1 '机能
2 '键1
0 '值1
2 '键2
0 '值2
2 '键3
0 '值3
1 '加锁者
1 '加锁时间
1 '解锁

最终表现DataGridView 真·列头不高亮 & 真·列头合并_第2张图片

自绘BUG

如果不需要列头合并,把上面 Span 相关的代码删除,不需要看本章。
那么来看看列头合并在水平滚动时的表现:

  1. 向右滚动到完整合并列头可见 ———— 正常DataGridView 真·列头不高亮 & 真·列头合并_第3张图片
  2. 继续向右滚动到完整合并列头部分可见 ———— 正常
    (其实看 Debug 输出,这时仅从"值3"列开始自绘,按照上面的代码,其实最左的列头没有重绘————居然还能显示半个标题!?)DataGridView 真·列头不高亮 & 真·列头合并_第4张图片
  3. 继续向右滚动DataGridView 真·列头不高亮 & 真·列头合并_第5张图片
  4. 然后向左滚动 ———— 不正常
    (和步骤2一样最左的列头没有重绘,保留了原先的图像————右滚/左滚表现不一致啊!)DataGridView 真·列头不高亮 & 真·列头合并_第6张图片

各种DataGridView列头合并的例子没有考虑到这种BUG吧

真·实现方式

找到了原因,只需要在每个被合并列(Span=0)也进行重绘就能解决

    Private Sub SingleDataGridView_CellPainting(sender As Object, e As DataGridViewCellPaintingEventArgs) Handles Me.CellPainting
        If (Not Me.DesignMode) And (e.RowIndex = -1) And (e.ColumnIndex <> -1) Then
            Dim cellRect As New Rectangle(e.CellBounds.X - 1, e.CellBounds.Y, e.CellBounds.Width, e.CellBounds.Height - 1)
            ' RowHeadersVisible = False 时最左可见列不需要向左合并网格线
            If e.CellBounds.X = 1 Then
                cellRect.X = 1
                cellRect.Width -= 1
            End If
            ' 修正水平滚动时合并列标题半可见时的显示问题
            Dim startColIndex As Integer = e.ColumnIndex
            While Me.ColHeadersSpanValue(startColIndex) <= 0
                startColIndex -= 1
                cellRect.X -= Me.Columns(startColIndex).Width
                cellRect.Width += Me.Columns(startColIndex).Width
            End While
            Dim colSpan As Integer = Me.ColHeadersSpanValue(startColIndex)
            ' 添加被合并列的宽度(计算startColIndex时已经加了一部分)
            For i As Integer = startColIndex + 1 To startColIndex + colSpan - 1
                If i > e.ColumnIndex Then
                    cellRect.Width += Me.Columns(i).Width
                End If
            Next

            Dim foreColorBrash As New SolidBrush(e.CellStyle.ForeColor)
            Dim backColorBrush As New SolidBrush(e.CellStyle.BackColor)
            Dim gridBrush As New SolidBrush(Me.GridColor)
            Dim gridLinePen As New Pen(gridBrush)

            Try
                e.Graphics.FillRectangle(backColorBrush, cellRect)
                e.Graphics.DrawRectangle(gridLinePen, cellRect)

                If e.FormattedValue IsNot Nothing Then
                    Dim format As New StringFormat()
                    Select Case e.CellStyle.Alignment
                        Case DataGridViewContentAlignment.BottomLeft, DataGridViewContentAlignment.MiddleLeft, DataGridViewContentAlignment.TopLeft
                            format.Alignment = StringAlignment.Near
                        Case DataGridViewContentAlignment.BottomCenter, DataGridViewContentAlignment.MiddleCenter, DataGridViewContentAlignment.TopCenter
                            format.Alignment = StringAlignment.Center
                        Case Else
                            format.Alignment = StringAlignment.Far
                    End Select
                    Select Case e.CellStyle.Alignment
                        Case DataGridViewContentAlignment.BottomCenter, DataGridViewContentAlignment.BottomLeft, DataGridViewContentAlignment.BottomRight
                            format.LineAlignment = StringAlignment.Center
                        Case DataGridViewContentAlignment.MiddleCenter, DataGridViewContentAlignment.MiddleLeft, DataGridViewContentAlignment.MiddleRight
                            format.LineAlignment = StringAlignment.Far
                        Case Else
                            format.LineAlignment = StringAlignment.Near
                    End Select
                    cellRect.Height += 1 ' 使得垂直居中和非自绘比较一致
                    e.Graphics.DrawString(CStr(e.FormattedValue), e.CellStyle.Font, foreColorBrash, cellRect, format)
                End If
            Finally
                gridLinePen.Dispose()
                gridBrush.Dispose()
                backColorBrush.Dispose()
                foreColorBrash.Dispose()
            End Try
            e.Handled = True
        End If
    End Sub
  • 滚动步骤4的效果DataGridView 真·列头不高亮 & 真·列头合并_第7张图片

题外话

这其实是对象继承用Inherits方式而不是Implements方式带来的先天缺陷。Inherits在实现继承时很爽(其实就是少写代码而已),但是父类一旦有变动所有继承类的行为会变化。这其实要求基类不变才能保证兼容性;想想有了DataGrid还要来个DataGridView,就是因为无法兼容;再看看 .NET Framework 从 1.1 到 4.8.1 那么多的版本,就是做不到低版本程序兼容高版本 Framework。

Implements方式有不同的接口,按特定接口调用时和其他特性无关。它所谓的缺陷

  1. 没有Protected
    其实可以定义继承/公共两套接口来实现。
  2. 费代码
    编译器自动完成会添加对应的方法,只要填空而已,不是很麻烦。
  3. 费内存
    在物理内存单位是G的时代太无聊了。实现方法增加了一些exe大小;
    保留父类对象也只不过多了一个变量,对象量再怎么多还差每对象几字节,又不是内存单位M时代。

CSDN 赶紧把 MarkDown 编辑器的维护人员拖出去鞭笞,不兼容 MarkDown 语法规则:

  • 没有空行的多行文字应该是同一个段落,无需换行
    (比如本行应该紧接前面的句号)结果要很别扭地修改换行(包括删除空行、
    )。
  • 实时解析输入刷新预览可以,不要自动修改啊。
    比如贴了一个图片的 MarkDown 代码,准备按照本地文件名进行上传,直接被替换成毫无用处的废话,连注释都没了。
    严重怀疑监听了键盘消息,不仅输入法的切换异常,连输入的字符都异常了:输入*变成(、输入(变成)。。。

你可能感兴趣的:(.Net,.net)