高亮BUG
VB.Net,在 .NET Framework 4.8 的 WinForm 下(即不是 WPF 的绘图模式、也不是 Core 或 Mono 的开发框架),使用 DataGridView
行模式,还是有个列头表现为高亮显示:
查找各种解决方式:
- 设置
ColumnHeadersDefaultCellStyle
———— 无效 - 直接修改每列的
HeaderCell.Style
———— 无效
既然有上述"解决方式",说明早期版本是有效的。至于从哪个版本开始无效,就不深究了,反正碰上了如下解决。
真·解决方式
只能在 CellPainting
事件中进行自绘了,顺便实现了列头合并功能(不需要多行列头)。
- 添加一个
RowDataGridView
用户控件,集成不需要设计,关掉直接改代码。 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
RowDataGridView.vb
Public Class RowDataGridView
Private m_ColHeadersSpan() As String
''' <summary>列头合并</summary>
''' <remarks>务必在定义<see cref="Columns"/>后修改。合并设置仅按次序、不随列定义同步调整。</remarks>
''' <returns>每列格式:
''' <list type="bullet">
''' <description>数值后的注释仅供参考</description>
''' <description>数值 1: (默认)非合并列头</description>
''' <description>数值 0: 被合并列头</description>
''' <description>其他正整数: 合并开始列头</description>
''' </list>
''' ☆ 数值正确性不检查。
''' ☆ 合并不影响自动列宽计算(即标题可能撑开合并开始列)。</returns>
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
注:
- 没有包括
New()
统一初始化行模式、是否显示行头等。 - 实现列头合并最正统的做法是给
DataGridView*Column
写继承类。但是仅为了一个属性需要给每种列类型写继承类,不如直接加属性在DataGridView
上。 - 本来想把
m_ColHeadersSpan
定义成Integer
数组,然后属性ColHeadersSpan
加注释变字符数组方便设计器中编辑;但是 Visual Studio 死活不支持。只能加属性ColHeadersSpanValue
实时解析,反正之前有列头判断,不会频繁调用。 - 继承自 VB6 的
Val()
函数容错性高,直接忽略数值之后的内容;不需要字符串拆分后转类型。
效果
对于已设计表格,只需要在 窗体.Designer.vb
中把 DataGridView
替换成 RowDataGridView
即可(注意前缀命名空间)。需要列头合并时设计器中修改 ColHeadersSpan
,比如上例表格设为
1 '机能
2 '键1
0 '值1
2 '键2
0 '值2
2 '键3
0 '值3
1 '加锁者
1 '加锁时间
1 '解锁
最终表现
自绘BUG
如果不需要列头合并,把上面 Span 相关的代码删除,不需要看本章。
那么来看看列头合并在水平滚动时的表现:
- 向右滚动到完整合并列头可见 ———— 正常
- 继续向右滚动到完整合并列头部分可见 ———— 正常
(其实看 Debug 输出,这时仅从"值3"列开始自绘,按照上面的代码,其实最左的列头没有重绘————居然还能显示半个标题!?) - 继续向右滚动
- 然后向左滚动 ———— 不正常
(和步骤2一样最左的列头没有重绘,保留了原先的图像————右滚/左滚表现不一致啊!)
各种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的效果
题外话
这其实是对象继承用Inherits
方式而不是Implements
方式带来的先天缺陷。Inherits
在实现继承时很爽(其实就是少写代码而已),但是父类一旦有变动所有继承类的行为会变化。这其实要求基类不变才能保证兼容性;想想有了DataGrid
还要来个DataGridView
,就是因为无法兼容;再看看 .NET Framework 从 1.1 到 4.8.1 那么多的版本,就是做不到低版本程序兼容高版本 Framework。
Implements
方式有不同的接口,按特定接口调用时和其他特性无关。它所谓的缺陷
- 没有
Protected
其实可以定义继承/公共两套接口来实现。 - 费代码
编译器自动完成会添加对应的方法,只要填空而已,不是很麻烦。 - 费内存
在物理内存单位是G
的时代太无聊了。实现方法增加了一些exe大小;
保留父类对象也只不过多了一个变量,对象量再怎么多还差每对象几字节,又不是内存单位M
时代。
CSDN 赶紧把 MarkDown 编辑器的维护人员拖出去鞭笞,不兼容 MarkDown 语法规则:
- 没有空行的多行文字应该是同一个段落,无需换行。
(比如本行应该紧接前面的句号)结果要很别扭地修改换行(包括删除空行、<br/>
)。 - 实时解析输入刷新预览可以,不要自动修改啊。
比如贴了一个图片的 MarkDown 代码,准备按照本地文件名进行上传,直接被替换成毫无用处的废话,连注释都没了。
严重怀疑监听了键盘消息,不仅输入法的切换异常,连输入的字符都异常了:输入*
变成(
、输入(
变成)
。。。