高亮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 RowDataGridViewInherits System.Windows.Forms.DataGridView '<- 原先是 UserControl'UserControl 重写释放以清理组件列表。<System.Diagnostics.DebuggerNonUserCode()> _Protected Overrides Sub Dispose(ByVal disposing As Boolean)TryIf disposing AndAlso components IsNot Nothing Thencomponents.Dispose()End IfFinallyMyBase.Dispose(disposing)End TryEnd 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 SubEnd Class
RowDataGridView.vb
Public Class RowDataGridViewPrivate 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()GetIf Me.ColumnCount > 0 ThenDim lastCount As IntegerIf m_ColHeadersSpan Is Nothing ThenlastCount = 0ReDim m_ColHeadersSpan(Me.ColumnCount)ElselastCount = m_ColHeadersSpan.LengthReDim Preserve m_ColHeadersSpan(Me.ColumnCount)End IfFor i As Integer = 0 To Me.ColumnCount - 1If i < lastCount Thenm_ColHeadersSpan(i) = $"{Val(m_ColHeadersSpan(i))} '{Me.Columns(i).HeaderText}"Elsem_ColHeadersSpan(i) = $"1 '{Me.Columns(i).HeaderText}"End IfNextEnd IfReturn m_ColHeadersSpanEnd GetSet(value As String())m_ColHeadersSpan = valueEnd SetEnd PropertyPrivate ReadOnly Property ColHeadersSpanValue(ByVal index As Integer) As IntegerGetReturn Val(m_ColHeadersSpan(index))End GetEnd PropertyPrivate Sub RowDataGridView_CellPainting(sender As Object, e As DataGridViewCellPaintingEventArgs) Handles Me.CellPaintingIf (Not Me.DesignMode) And (e.RowIndex = -1) And (e.ColumnIndex <> -1) ThenDebug.Print($"{e.ColumnIndex} : {e.Value}")Dim colSpan As Integer = Me.ColHeadersSpanValue(e.ColumnIndex)If colSpan > 0 ThenDim 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 ThencellRect.X = 1cellRect.Width -= 1End If' 添加被合并列的宽度For i As Integer = 1 To colSpan - 1cellRect.Width += Me.Columns(e.ColumnIndex + i).WidthNextDim 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)Trye.Graphics.FillRectangle(backColorBrush, cellRect)e.Graphics.DrawRectangle(gridLinePen, cellRect)If e.FormattedValue IsNot Nothing ThenDim format As New StringFormat()Select Case e.CellStyle.AlignmentCase DataGridViewContentAlignment.BottomLeft, DataGridViewContentAlignment.MiddleLeft, DataGridViewContentAlignment.TopLeftformat.Alignment = StringAlignment.NearCase DataGridViewContentAlignment.BottomCenter, DataGridViewContentAlignment.MiddleCenter, DataGridViewContentAlignment.TopCenterformat.Alignment = StringAlignment.CenterCase Elseformat.Alignment = StringAlignment.FarEnd SelectSelect Case e.CellStyle.AlignmentCase DataGridViewContentAlignment.BottomCenter, DataGridViewContentAlignment.BottomLeft, DataGridViewContentAlignment.BottomRightformat.LineAlignment = StringAlignment.CenterCase DataGridViewContentAlignment.MiddleCenter, DataGridViewContentAlignment.MiddleLeft, DataGridViewContentAlignment.MiddleRightformat.LineAlignment = StringAlignment.FarCase Elseformat.LineAlignment = StringAlignment.NearEnd SelectcellRect.Height += 1 ' 使得垂直居中和非自绘比较一致e.Graphics.DrawString(CStr(e.FormattedValue), e.CellStyle.Font, foreColorBrash, cellRect, format)End IfFinallygridLinePen.Dispose()gridBrush.Dispose()backColorBrush.Dispose()foreColorBrash.Dispose()End TryEnd Ife.Handled = TrueEnd IfEnd SubEnd 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.CellPaintingIf (Not Me.DesignMode) And (e.RowIndex = -1) And (e.ColumnIndex <> -1) ThenDim 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 ThencellRect.X = 1cellRect.Width -= 1End If' 修正水平滚动时合并列标题半可见时的显示问题Dim startColIndex As Integer = e.ColumnIndexWhile Me.ColHeadersSpanValue(startColIndex) <= 0startColIndex -= 1cellRect.X -= Me.Columns(startColIndex).WidthcellRect.Width += Me.Columns(startColIndex).WidthEnd WhileDim colSpan As Integer = Me.ColHeadersSpanValue(startColIndex)' 添加被合并列的宽度(计算startColIndex时已经加了一部分)For i As Integer = startColIndex + 1 To startColIndex + colSpan - 1If i > e.ColumnIndex ThencellRect.Width += Me.Columns(i).WidthEnd IfNextDim 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)Trye.Graphics.FillRectangle(backColorBrush, cellRect)e.Graphics.DrawRectangle(gridLinePen, cellRect)If e.FormattedValue IsNot Nothing ThenDim format As New StringFormat()Select Case e.CellStyle.AlignmentCase DataGridViewContentAlignment.BottomLeft, DataGridViewContentAlignment.MiddleLeft, DataGridViewContentAlignment.TopLeftformat.Alignment = StringAlignment.NearCase DataGridViewContentAlignment.BottomCenter, DataGridViewContentAlignment.MiddleCenter, DataGridViewContentAlignment.TopCenterformat.Alignment = StringAlignment.CenterCase Elseformat.Alignment = StringAlignment.FarEnd SelectSelect Case e.CellStyle.AlignmentCase DataGridViewContentAlignment.BottomCenter, DataGridViewContentAlignment.BottomLeft, DataGridViewContentAlignment.BottomRightformat.LineAlignment = StringAlignment.CenterCase DataGridViewContentAlignment.MiddleCenter, DataGridViewContentAlignment.MiddleLeft, DataGridViewContentAlignment.MiddleRightformat.LineAlignment = StringAlignment.FarCase Elseformat.LineAlignment = StringAlignment.NearEnd SelectcellRect.Height += 1 ' 使得垂直居中和非自绘比较一致e.Graphics.DrawString(CStr(e.FormattedValue), e.CellStyle.Font, foreColorBrash, cellRect, format)End IfFinallygridLinePen.Dispose()gridBrush.Dispose()backColorBrush.Dispose()foreColorBrash.Dispose()End Trye.Handled = TrueEnd IfEnd 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 代码,准备按照本地文件名进行上传,直接被替换成毫无用处的废话,连注释都没了。
严重怀疑监听了键盘消息,不仅输入法的切换异常,连输入的字符都异常了:输入*
变成(
、输入(
变成)
。。。