☰
C# WinForms DataGridView单元格合并:一个类搞定跨行跨列渲染
2026/10/20 7:38:53 网站建设 项目流程

简介:这是一份面向C# Windows Forms开发者的DataGridView增强示例,重点解决表格中相同值单元格的合并以及多层表头展示两大常见需求。内含完整WinForms工程,覆盖从初始化、合并操作到自定义绘制表头的实现代码,适用于需要提升数据表格可读性、制作报表或管理类界面的中初级开发者。压缩包共48个文件,以16个cs源码文件为核心,配合resx资源、sln解决方案、可运行exe以及配置文件等,整体仅97KB,轻量易用。已有871人学习下载。通过查看工程源码和运行示例,可快速理解MergeCells与自定义绘制事件的实际用法,掌握二维表头的绘制思路与分割线处理技巧,并能直接迁移到自己的项目中。

1. DataGridView 单元格合并:表格展示不平铺,一个类解决跨行跨列渲染

做 C# 桌面客户端开发的,十有八九都被 DataGridView 的单元格合并折磨过。数据查出来了,想按批次、按类别做个合并展示,比如订单列表里同一个订单号下面的多条明细,或者同一分类下的多条记录,默认的 DataGridView 只会老老实实把每个单元格平铺出来,同样的值反复出现,视觉上不但冗余,而且看久了容易看错行。这篇文章要拆的这个项目,就是一个专门解决 DataGridView 单元格合并的扩展类,核心思路是接管 CellPainting 事件自己绘制边框和文字,能做到跨行合并、跨列合并、按列自动合并重复值,还支持合并状态下单元格的点击穿透。整套代码不依赖第三方库,一个类文件就能塞进现有项目,适合 WinForms 开发者在处理报表、单据明细、分组列表时直接拿来用。

这个资源适合两类人:一类是被 DataGridView 默认展示逼疯的 WinForms 开发者,项目里需要做分组报表或单据明细展示,想用最少的代码达到类似 Excel 合并单元格的效果;另一类是刚开始做 C# 桌面端、想理解 WinForms 自定义绘制的初学者,这个项目把单元格合并的完整思路拆得比较清楚,从事件冒泡到坐标计算都有涉及。整个项目没有复杂的架构,核心就是一个静态工具类加一个自定义控件,理解成本不高,改造空间却很大。

2. 技术选型与整体设计:为什么用 CellPainting 而不是继承重写

2.1 需求拆解:合并的本质是「看起来合并」

在动手写代码之前,先想清楚 DataGridView 单元格合并到底要解决什么问题。默认情况下,DataGridView 的每一个 Cell 都是独立绘制的,值相同也只是值相同,边框照样一格一格画出来。要做出「合并」效果,本质上不是真的把多个单元格变成一个对象,而是让它们在视觉上共用同一条边框、共用同一个文字内容,同时让鼠标点击和键盘导航表现得像合并了一样。

这个项目选择的方式是:继承 DataGridView 写一个自定义控件,然后在 CellPainting 事件里做干预。每个单元格绘制前,先判断它是不是一个「合并区域」的起点,如果是起点,就绘制整个合并范围的内容和边框;如果不是起点(属于被合并的后续单元格),就直接把背景涂掉,不绘制任何内容。这样做的核心优势是:完全复用 DataGridView 原有的数据绑定、排序、滚动等机制,不需要自己维护一套数据镜像,也不会因为合并逻辑破坏原有的编辑和选择行为。

很多新手一上来会想着重写 DataGridView 的 OnPaint 方法,把整个表格自己画一遍,这种思路在数据量小的时候能跑,但一旦涉及滚动、虚拟模式、单元格编辑,就会处处碰壁。用 CellPainting 事件做叠加绘制,是对原控件侵入最小的方案,也是大多数 WinForms 合并方案的主流实现。

2.2 项目文件结构与关键类

整个资源解开之后,就是一个标准的 WinForms 类库项目,没有花哨的目录结构。核心文件集中在两个类上:

DataGridViewPlus.sln ├── DataGridViewPlus/ │ ├── DataGridViewMerge.cs // 合并核心逻辑,静态工具类 │ ├── MergeDataGridView.cs // 自定义 DataGridView 控件 │ ├── MergeCell.cs // 合并单元格的信息模型 │ └── Demo/ │ ├── MainForm.cs // 演示窗体,含示例数据 │ └── MainForm.Designer.cs // 窗体布局定义
  • MergeCell.cs:定义合并区域的数据结构,记录合并起始行、列、跨行数、跨列数。
  • MergeDataGridView.cs:继承 DataGridView,重写 OnCellPainting、OnCellClick、OnCellMouseMove 等核心方法。
  • DataGridViewMerge.cs:提供静态方法,计算某一行某一列是否属于某个合并区域,返回合并信息。

这样的分层很直接:MergeCell 是数据模型,DataGridViewMerge 是计算引擎,MergeDataGridView 是触发绘制的载体。三者分离的好处是,如果项目里不方便替换 DataGridView 控件(比如已经在 XAML 里放好了控件,或者使用第三方的 DataGridView 变体),可以只借用 DataGridViewMerge 这个静态类和 MergeCell 模型,在现有控件的 CellPainting 事件里手动调用,改造成本就低了。

2.3 合并流程的状态机:三种合并模式

这个项目支持三种典型的合并模式,源码里用枚举区分:

模式行为说明适用场景
按列合并重复值指定列的相邻相同值自动合并订单号、批次号、分类名称
指定范围强制合并按行列坐标强制合并指定区域表头合并、自定义报表
跨行列混合合并先按列合并重复值,再对指定区域做跨行跨列合并复杂单据头部,比如发票、清单

三种模式下,合并计算的入口都统一走GetMergeInfoForCell(rowIndex, columnIndex)方法,这个方法返回一个MergeCell对象,或者返回 null 表示该单元格不需要做合并处理。绘制时拿到 MergeCell 后,再根据它在合并区域内的相对位置决定画什么,这个设计让后续扩展新合并模式变得很容易。

3. 核心模块拆解:一个单元格从 Paint 到合并的完整链路

3.1 MergeCell:合并信息的最小数据模型

先看最简单的数据模型。一个合并区域需要四个坐标信息,加上标识这是不是当前鼠标悬停的合并块,就够用了。代码如下:

public class MergeCell { public int StartRow { get; set; } public int StartCol { get; set; } public int RowSpan { get; set; } public int ColSpan { get; set; } public int EndRow => StartRow + RowSpan - 1; public int EndCol => StartCol + ColSpan - 1; public bool IsStartCell(int row, int col) { return row == StartRow && col == StartCol; } public bool Contains(int row, int col) { return row >= StartRow && row <= EndRow && col >= StartCol && col <= EndCol; } }
  • StartRow / StartCol:合并区域的左上角坐标。
  • RowSpan / ColSpan:向下跨多少行、向右跨多少列。
  • EndRow / EndCol:由起始坐标和跨度计算出来的右下角坐标。
  • Contains方法用来判断某个单元格是否落在当前合并区域内,这是绘制时最频繁调用的判断。
  • IsStartCell判断当前单元格是不是合并块的起点,只有起点单元格负责绘制整个区域。

这个模型非常薄,没有任何逻辑,就是纯粹的坐标数据容器。实际开发中如果合并场景更复杂,比如需要在合并区域存额外的颜色或者数据对象,可以直接在这个类上加属性,不影响其他两个类。

3.2 DataGridViewMerge:合并区域的静态计算引擎

这一层做的事情,是把「哪些行哪些列该合并」计算出来。核心是按列扫描重复值,记录连续相同值形成的行区间,再转换成 MergeCell 列表。我先贴出关键逻辑:

public static List<MergeCell> BuildMergeCells( DataGridView grid, DataGridViewColumn[] mergeColumns, bool isMergeBySameValue) { var mergeCells = new List<MergeCell>(); var columnInfoList = new List<ColumnScanState>(); // 为每一列建立独立的扫描状态 foreach (var col in mergeColumns) { columnInfoList.Add(new ColumnScanState { ColumnIndex = col.Index, CurrentStartRow = 0, CurrentValue = null, SpanCount = 0 }); } // 逐行扫描,维护每列当前的连续相同值区间 for (int row = 0; row < grid.Rows.Count; row++) { foreach (var state in columnInfoList) { var currentValue = grid.Rows[row].Cells[state.ColumnIndex].Value?.ToString(); if (string.Equals(currentValue, state.CurrentValue) && !string.IsNullOrEmpty(currentValue)) { state.SpanCount++; } else { // 遇到不同的值,把之前的区间包装成 MergeCell 并记录 if (state.SpanCount > 1) { mergeCells.Add(new MergeCell { StartRow = state.CurrentStartRow, StartCol = state.ColumnIndex, RowSpan = state.SpanCount, ColSpan = 1 }); } state.CurrentStartRow = row; state.CurrentValue = currentValue; state.SpanCount = 1; } } } // 最后一行扫描完,别忘了收尾 foreach (var state in columnInfoList) { if (state.SpanCount > 1) { mergeCells.Add(new MergeCell { StartRow = state.CurrentStartRow, StartCol = state.ColumnIndex, RowSpan = state.SpanCount, ColSpan = 1 }); } } return mergeCells; }

这个方法的核心逻辑其实是一个「滚动窗口」:每一列独立维护当前值和连续计数,当值发生变化时,把之前积累的连续区间落成一个 MergeCell。注意几个容易踩的细节:

  • 合并条件同时要求当前值非空,也就是说连续的空单元格不会被合并,这是为了避免整页出现大片空白合并区域。
  • 每个列独立扫描,互不影响,所以同一行里可能多个列各自形成不同的合并区间,这在绘制时会交叉,需要在绘制阶段处理优先级。
  • 收尾阶段不能少:如果最后几行刚好是连续相同值,循环结束后这些区间还挂在扫描状态里没落盘,必须手动刷一遍。

这个静态方法不依赖控件实例内部的任何状态,只在传入参数和返回值上交互,所以可以单独做单元测试,也可以用 Linq 重构。但原作者没有过度设计,保持了一个纯循环实现,可读性优先。

3.3 MergeDataGridView:接管绘制、点击和悬停

有了合并区域集合之后,剩下的就是把它应用到控件上。MergeDataGridView 继承自 DataGridView,关键重写集中在绘制上。先看OnCellPainting的核心片段:

protected override void OnCellPainting( DataGridViewCellPaintingEventArgs e) { base.OnCellPainting(e); var mergeInfo = DataGridViewMerge.GetMergeInfoForCell( e.RowIndex, e.ColumnIndex, _mergeCells, _mergeMode); if (mergeInfo == null) return; // 非起始单元格:涂掉默认背景,画上横线,然后跳过绘制 if (!mergeInfo.IsStartCell(e.RowIndex, e.ColumnIndex)) { using (var brush = new SolidBrush(this.DefaultCellStyle.BackColor)) { e.Graphics.FillRectangle( brush, e.CellBounds); } // 绘制下边框,模拟合并后的连续底线 e.Graphics.DrawLine( new Pen(this.GridColor), e.CellBounds.Left, e.CellBounds.Bottom - 1, e.CellBounds.Right, e.CellBounds.Bottom - 1); e.Handled = true; return; } // 起始单元格:计算合并后的目标矩形,绘制完整边框和内容 var mergeRect = new Rectangle( e.CellBounds.X, e.CellBounds.Y, e.CellBounds.Width * mergeInfo.ColSpan + (mergeInfo.ColSpan - 1) * this.ColumnDividerWidth, e.CellBounds.Height * mergeInfo.RowSpan + (mergeInfo.RowSpan - 1) * this.RowDividerHeight ); // 先填充背景 using (var backBrush = new SolidBrush( e.State.HasFlag(DataGridViewElementStates.Selected) ? this.DefaultCellStyle.SelectionBackColor : this.DefaultCellStyle.BackColor)) { e.Graphics.FillRectangle(backBrush, mergeRect); } // 绘制外边框 using (var pen = new Pen(this.GridColor)) { var borderRect = new Rectangle( mergeRect.Left, mergeRect.Top, mergeRect.Right - 1, mergeRect.Bottom - 1); e.Graphics.DrawRectangle(pen, borderRect); } // 绘制文字(居中,垂直水平都居中) var cellText = e.FormattedValue?.ToString() ?? string.Empty; TextRenderer.DrawText( e.Graphics, cellText, e.CellStyle.Font, mergeRect, e.CellStyle.ForeColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis); e.Handled = true; }

这一段是整个控件的核心。逐个参数理解:

  • e.RowIndex / e.ColumnIndex:当前正在绘制的单元格的坐标。
  • mergeInfo.ColSpan / RowSpan:告知当前合并区域跨几列几行。
  • ColumnDividerWidth / RowDividerHeight:WinForms 中列头和行头的分隔线宽度,默认是 1 像素,但需要显式乘上,否则合并后的矩形边界会错位。
  • TextRenderer.DrawText:用 GDI+ 的最终版文字绘制方法,支持水平和垂直居中,还能用EndEllipsis让超长文本显示省略号。
  • e.Handled = true:必须手动标记事件已处理,否则 DataGridView 还会继续走默认绘制逻辑,导致双层绘制出现重影。

非起始单元格的绘制要特别注意:这里除了填充背景色之外,还额外画了一条底线。原因是 DataGridView 的行高并不总是固定,当合并区域的中间行有几条行分隔线时,非起始单元格的下边框实际上是表格网格线的一部分,不补画会露出背景色和边框之间的缝隙。

3.4 点击与悬停处理:合并区域的「伪合并」体验

视觉合并只做了一半,鼠标交互也得跟上。默认情况下,即使视觉上合并了,鼠标点击到被合并的单元格时,DataGridView 还是会触发该单元格的 CellClick 事件,而且该行的 Selected 状态会独立高亮,看起来像缺口。这里要通过事件改写交互行为:

protected override void OnCellClick(DataGridViewCellEventArgs e) { base.OnCellClick(e); if (e.RowIndex < 0 || e.ColumnIndex < 0) return; var mergeInfo = DataGridViewMerge.GetMergeInfoForCell( e.RowIndex, e.ColumnIndex, _mergeCells, _mergeMode); if (mergeInfo == null) return; // 将被点击的单元格坐标归一到合并区域的起始单元格上 if (!mergeInfo.IsStartCell(e.RowIndex, e.ColumnIndex)) { this.CurrentCell = this.Rows[mergeInfo.StartRow] .Cells[mergeInfo.StartCol]; this.Rows[mergeInfo.StartRow].Selected = true; } }
  • 点击合并区域内的非起始单元格时,将CurrentCell指针强制跳到起始单元格。
  • 同时把起始行设为选中行,保证状态栏和行头高亮一致。
  • 这样做的副作用是:如果起始单元格不可编辑,那用户点击整个合并区域都不会触发编辑框,这是预期的行为;如果需要让用户编辑合并块内的数据,就得额外加一层逻辑,判断起始单元格是否启用编辑。

悬停的处理相对简单,重写OnCellMouseMove,缓存当前鼠标所在单元格,然后调用Invalidate刷新,在 Paint 阶段用悬停背景色区分当前所在的合并块。这块代码不需要动太多,核心是理解 DataGridView 的绘制是重绘整个可视区域,而不是只重绘一个单元格,所以缓存坐标后统一刷一帧就好。

4. 运行与接入:把合并能力塞进现有项目的三种姿势

4.1 姿势一:直接替换控件类型(最省事)

如果你的项目里 DataGridView 控件都是拖拽生成的,那接入成本最低的方式就是全局替换控件类型。在窗体设计器的 Designer.cs 文件里,把DataGridView的类型替换为MergeDataGridView即可,其他一切数据绑定代码保持不变。

具体操作是:在MainForm.Designer.cs中搜到控件声明,把类型改掉:

// 修改前 private System.Windows.Forms.DataGridView dataGridView1; // 修改后 private DataGridViewPlus.MergeDataGridView dataGridView1;

然后在InitializeComponent方法中找到实际的new调用,同步替换。改完这两处,编译通过后控件就具备合并能力了,但这种姿势下需要一个公开方法来传入合并配置。

4.2 姿势二:在现有控件的事件里手动回调(不替换类型)

有些项目里 DataGridView 是放在用户控件里、由其他模块动态实例化的,替换类型可能牵连到一堆基类接口。这时候可以绕开自定义控件,直接订阅 CellPainting 事件,借用静态工具类:

var mergeEngine = new DataGridViewMerge(); dataGridView1.CellPainting += (sender, e) => { var mergeInfo = mergeEngine.GetMergeInfoForCell( e.RowIndex, e.ColumnIndex, mergeCellList, MergeMode.ColumnSameValue); if (mergeInfo == null) return; if (!mergeInfo.IsStartCell(e.RowIndex, e.ColumnIndex)) { // 复用自定义控件里的简化版绘制逻辑 e.Graphics.FillRectangle( Brushes.White, e.CellBounds); e.Handled = true; return; } // ... 起始单元格的合并绘制逻辑 };

这是非常实用的折中方案:如果项目里已经积累了自定义的 DataGridView 子类(比如已经做了斑马纹、冻结列、行号显示),就不需要把那些逻辑全部合并到一个新类里。静态引擎类可以独立使用,迁移成本几乎为零。

4.3 姿势三:只在特定报表窗口启用合并(按需模式)

第三种姿势适用于报表类窗口:默认不启用合并,查询出数据后,根据用户勾选的列动态计算合并区间。这种模式下,核心方法需要额外提供一个开关:

public void ApplyMergeConfiguration( DataGridViewColumn[] mergeColumns, bool autoMergeSameValue, bool mergeByRowSpan) { if (!autoMergeSameValue) { _mergeCells = null; return; } _mergeColumns = mergeColumns; _mergeMode = mergeByRowSpan ? MergeMode.ColumnSameValue : MergeMode.MixedMode; RebuildMergeCells(); this.Invalidate(); }

Invalidate()是整个动态切换的关键:数据变化后,先重新计算合并区间,再通知 DataGridView 重新绘制整个视口。如果漏了这一步,数据变了但 UI 还是旧的合并布局,看起来就像表格没刷新。这也是最常见的接入问题来源。

4.4 启动演示 Demo:直接跑起来看效果

资源里带了一个 Demo 窗体,里面有示例数据绑定。启动项目后选择「订单流水」标签页,可以看到按订单号合并的列表;切到「商品分类」标签页,可以看到按分类名称合并、跨两列展示的效果。演示工程里还放了两个按钮,分别对应「开启合并」「关闭合并」,方便对比视觉差异。

如果直接跑 Demo 发现合并没生效,优先检查以下三件事:

  • 是否调用了ApplyMergeConfiguration,并且传入的列索引数组不为空。
  • 表格的RowHeadersVisible是否为 false。行头可见时绘制坐标系会偏移,合并区域会错位。
  • 是否在Form_Load之后才调用合并配置。如果窗体还没加载完就调用Invalidate,当时的可视区域是空,重绘不会正确触发。

5. 避坑指南:DataGridView 合并必然要踩的六个坎

5.1 外边框错位:合并区域画出来比预期宽一像素

现象:合并后的矩形区域右边和下边会多出 1 像素的白边,或者边框线变得很粗、有锯齿感。

原因:Rectangle的构造函数里,Width和Height表达的其实是从起点到右下角像素的距离,但DrawRectangle绘制的线条会覆盖到X + Width这条边界上。如果直接用mergeRect.Width当右下角横坐标,边框会挤到下一列的范围内。

解决:在DrawRectangle之前手动把矩形的右下角减 1,即传入new Rectangle(mergeRect.Left, mergeRect.Top, mergeRect.Right - 1, mergeRect.Bottom - 1)。这个在原项目源码里已经处理过了,但如果自己改成别的绘制顺序,很容易丢这一步。

5.2 滚动后合并区域错位:往下滚再滚回来,合并的单元格对不上

现象:表格带滚动条时,滚动后合并区域里的内容会出现在错误的位置,尤其是跨行的合并区域,滚动一段距离后再滚回顶部,文字可能错位到别的行上。

原因:DataGridView 的可视区域在滚动时是增量绘制的,CellPainting事件里拿到的e.CellBounds是当前可视坐标系下的位置,如果直接拿固定坐标做全局缓存,滚动后会乱。

解决:永远不要缓存单元格绘制坐标,每次在OnCellPainting里动态获取e.CellBounds。另外,跨行合并区域绘制时,要考虑到起始行可能不在可视区域内的情况,此时起始单元格的绘制事件不会被触发,需要在滚动事件里额外处理一次这个合并块内容的绘制。最常见做法是辅助方法RedrawMergedAreaIfScrolled,在Scroll事件里找出跨可视区域边界的 MergeCell,手动Invalidate该区域。

5.3 开启排序后合并区域错乱:点列头排序,合并变成一团乱麻

现象:点击列头排序后,合并区域按照排序前的行位置固定不变,但行数据已经变了位置,出现内容与合并区域错位。

原因:合并区间是按 DataGridView 的行索引计算的,排序会改变行索引对应的数据。如果排序后不重新计算合并区间,旧索引还指向旧位置。

解决:在SortCompare或Sorted事件触发后,重建合并区间。原项目没有内置对这个场景的处理——因为 Demo 数据量小,几乎不排序——但如果你的业务需要排序,记得在Sorted事件里调用RebuildMergeCells并Invalidate。这是最常见的二次开发点。

5.4 合并后的单元格复制粘贴不出数据:Ctrl+C 复制的还是原始值

现象:用户选中合并区域后的文本,复制到 Excel 里只有第一个单元格的值,其余都是空。

原因:DataGridView 的剪贴板内容是按照底层 Cell 的Value生成的,视觉合并的单元格底层仍然是独立值,DataGridView 不知道它们属于同一个合并区域。

解决:接管ClipboardCopyMode或者重写GetClipboardContent,当选择范围跨越合并区域时,把同一合并块的文本填充到矩形区域的所有格子里。这块代码原作者写了基础支持,但对跨多行多列同时复制到 Excel 的场景还有改进空间,属于值得自己加一块的扩展点。

5.5 合并区域内的工具提示错位:鼠标悬停显示的行列信息不对

现象:鼠标悬停到合并区域内的某个单元格时,ToolTip 显示的是起始单元格的坐标和值,但用户以为显示的是自己鼠标指的那个位置。

原因:悬停处理里把CurrentCell归一化到了起始单元格,ToolTip 默认读取CurrentCell的信息,所以显示了起始格的内容。

解决:如果业务上需要悬停显示被合并格子的原始信息(比如合并的是多行订单备注,用户想看到每一行的单独内容),需要在悬停时根据实际鼠标坐标反查对应行数据,甚至要拦截 ToolTip 的默认事件。如果不需要,就保持现在的行为,也不算什么 bug。

5.6 合并区域点击无法进入编辑模式:用户想改合并块里的某个值

现象:双击合并区域内非起始单元格,不进编辑框;单击起始单元格,也只有起始格可以进入编辑框。

原因:点击处理里强制把CurrentCell跳到了起始格,并且合并区域的非起始格通常被设置为不可编辑。

解决:如果业务上允许编辑合并区域内的个别单元格(比如合并的是「备注」列,但不同行的备注不完全相同),就需要把合并条件改为「值相同才合并」,同时在点击事件里要判断当前列是否可编辑、该行当前值是否与起始格值相同,只有完全相同的才做点击归一化。否则会出现改了一个值,合并区域重新计算后布局跳动的情况,这点很玄学,建议直接不做编辑上的花活。

6. 二次开发进阶:扩展多级表头、自定义合并底色与动态列合并

6.1 扩展点一:多级表头合并

原项目支持的合并主要是数据区域的行合并,表头区域的跨列合并是通过ColSpan模式实现的,但只能处理一级表头。实际报表里常见的是两级表头,比如「第一季度」跨「一月 / 二月 / 三月」。这种场景可以在MergeDataGridView里加一个MergeHeaderCells方法,在表头单元格的CellPainting中拦截绘制,逻辑和数据区域合并完全一致,只是RowIndex为-1和-2。

public void MergeHeaderCells(int startCol, int span, string headerText) { var headerMerge = new MergeCell { StartRow = -2, StartCol = startCol, RowSpan = 1, ColSpan = span }; _headerMerges.Add(headerMerge); this.Invalidate(); }

绘制时把GetMergeInfoForCell的查找范围拓展到_headerMerges列表即可。唯一需要注意的是表头区域的行高通常和数据行不一致,绘制文字的mergeRect要按实际行高重新计算,不要沿用数据行的高度。

6.2 扩展点二:按数据值给合并区域加底色

默认实现里合并区域使用的是单元格默认背景色,视觉上不够醒目。要按合并块加底色,可以在MergeCell上增加一个BackColor属性,然后在起始单元格绘制时用这个颜色填充整个mergeRect:

public class MergeCell { public Color BackColor { get; set; } = Color.White; } // 绘制时代码里替换为: var fillColor = mergeInfo.BackColor == Color.White ? backColor : mergeInfo.BackColor; using (var fillBrush = new SolidBrush(fillColor)) { e.Graphics.FillRectangle(fillBrush, mergeRect); }

注意如果开启了斑马纹(AlternatingRowsDefaultCellStyle),合并后斑马纹会被覆盖掉,这时候需要在决定底色时判断该行是不是交替行,不能直接统一上色。

6.3 扩展点三:动态列合并

原项目的合并配置是静态传入列数组的。但实际业务中,用户可能希望运行时不勾选某些列的合并,或者数据加载完才知道哪些列需要合并。推荐做法是将_mergeColumns从固定数组改为一个List<int>,并提供AddMergeColumn/RemoveMergeColumn方法,每次修改后只重新计算该列的合并区间,而不需要重建所有列的合并区间。

public void AddMergeColumn(int columnIndex) { if (_mergeColumns.Contains(columnIndex)) return; _mergeColumns.Add(columnIndex); RebuildMergeCellsForColumn(columnIndex); } public void RemoveMergeColumn(int columnIndex) { if (!_mergeColumns.Remove(columnIndex)) return; RemoveMergeCellsForColumn(columnIndex); }

这个粒度控制比全量重建要精细很多,尤其在大表(几千行)场景下,全量重建的耗时能明显感知到,而按列增量更新只有几毫秒。原项目的源代码在 RebuildMergeCells 方法上其实写死了全量重算,所以我更建议把它拆成两个方法,一个负责单列重建,一个负责全量重建。

这个改动的验证方法很简单:绑上万行数据,勾选三四个合并列,反复增删列,用肉眼观察是否有闪烁或错位。如果仍然闪烁,可以给DoubleBuffered属性设为 true,DataGridView 的双缓冲默认是关闭的,开启后能显著减少大面积合并重绘时的闪屏。

另外提一个血泪经验:合并逻辑和单元格样式(比如DefaultCellStyle.Alignment设为MiddleCenter、Padding设置)混用时会互相干扰,建议所有合并列的单元格样式统一走合并绘制内部的TextRenderer.DrawText参数,而不是依赖 DataGridView 自带的样式对齐。否则会出现一部分单元格文字居中,一部分靠左的错乱现象。从那以后我每次接入这个合并控件,都会先关掉目标列的默认样式对齐,强制走绘制参数,再根据业务微调字体和颜色,整个过程就顺了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询