2025-11-04
C#
00

目录

💥 传统DataGridView的痛点分析
🚀 解决方案:自定义MergeableDataGridView
💡 核心设计思路
🎯 完整代码实战
📝 第一步:创建MergeableDataGridView主控件
📋 第二步:定义MergeArea数据结构
🛠️ 第三步:创建便捷的扩展方法
🎮 实际使用示例
⚡ 高级特性亮点
🔥 智能排序支持
🎨 灵活的对齐方式
🚨 常见坑点提醒
⚠️ 坑点1:双缓冲必须开启
⚠️ 坑点2:重叠合并区域处理
⚠️ 坑点3:滚动偏移计算
🏆 性能优化秘籍
💡 扩展应用场景
🎯 核心要点总结

你是否遇到过这样的场景:老板要求在系统中展示复杂的数据报表,需要合并相同类别的单元格,就像Excel那样?传统的DataGridView让你抓狂,要么显示效果丑陋,要么实现复杂到让人崩溃。

据统计,90%的企业级应用都需要复杂的数据展示功能,而单元格合并是其中最常见的需求之一。今天,我将带你手把手打造一个支持单元格合并的高性能DataGridView控件,让你的应用界面瞬间提升一个档次!

💥 传统DataGridView的痛点分析

在企业开发中,我们经常面临这些头疼问题:

🔸 数据重复显示混乱

当同一类别有多个子项时,传统表格会重复显示类别名称,用户体验极差。

🔸 界面不够专业

客户总是拿Excel的效果来对比,觉得我们的系统"不够高大上"。

🔸 开发成本高

网上的解决方案要么收费,要么bug一堆,自己写又不知道从何下手。

🚀 解决方案:自定义MergeableDataGridView

💡 核心设计思路

我们的解决方案包含三个核心组件:

  1. MergeableDataGridView主控件 - 继承原生DataGridView,添加合并功能
  2. MergeArea合并区域类 - 存储合并区域的信息
  3. 扩展方法类 - 提供便捷的自动合并功能

🎯 完整代码实战

📝 第一步:创建MergeableDataGridView主控件

C#
using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; namespace AppMergeGrid { public class MergeableDataGridView : DataGridView { private List<MergeArea> mergeAreas; private bool autoRefreshMergeOnSort = true; public MergeableDataGridView() { mergeAreas = new List<MergeArea>(); // 🔥 关键优化:启用双缓冲,解决闪烁问题 this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true); this.UpdateStyles(); // 监听排序事件,实现智能重新合并 this.Sorted += OnDataGridViewSorted; this.DataSourceChanged += OnDataSourceChanged; } /// <summary> /// 💎 核心方法:合并指定区域的单元格 /// </summary> public void MergeCells(int startRow, int startCol, int endRow, int endCol, string text = "", StringAlignment horizontalAlign = StringAlignment.Center, StringAlignment verticalAlign = StringAlignment.Center) { // 参数验证 if (startRow > endRow || startCol > endCol) throw new ArgumentException("起始位置不能大于结束位置"); if (startRow < 0 || endRow >= this.Rows.Count || startCol < 0 || endCol >= this.Columns.Count) throw new ArgumentException("行列索引超出范围"); // 🚨 重要:检查并移除重叠的合并区域 RemoveOverlappingMerges(startRow, startCol, endRow, endCol); var mergeArea = new MergeArea { StartRow = startRow, StartColumn = startCol, EndRow = endRow, EndColumn = endCol, Text = text, HorizontalAlignment = horizontalAlign, VerticalAlignment = verticalAlign, OriginalRowKeys = GetRowKeys(startRow, endRow) // 智能存储行标识 }; mergeAreas.Add(mergeArea); this.Invalidate(); // 触发重绘 } /// <summary> /// 🎨 自定义绘制:这里是实现合并效果的核心 /// </summary> protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); // 先绘制基础表格 DrawMergedAreas(e.Graphics); // 再绘制合并区域 } /// <summary> /// 🖌️ 绘制合并区域的详细实现 /// </summary> private void DrawSingleMergeArea(Graphics g, MergeArea mergeArea) { try { Rectangle mergeRect = GetMergeAreaRectangle(mergeArea); if (mergeRect.IsEmpty) return; // 🎯 高质量绘制设置 g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; bool isSelected = IsMergeAreaSelected(mergeArea); // 绘制背景 Color backColor = isSelected ? this.DefaultCellStyle.SelectionBackColor : this.DefaultCellStyle.BackColor; using (Brush backBrush = new SolidBrush(backColor)) { g.FillRectangle(backBrush, mergeRect); } // 绘制边框 using (Pen borderPen = new Pen(this.GridColor, 1)) { g.DrawRectangle(borderPen, mergeRect); } // 绘制文本 if (!string.IsNullOrEmpty(mergeArea.Text)) { Color textColor = isSelected ? this.DefaultCellStyle.SelectionForeColor : this.DefaultCellStyle.ForeColor; using (Brush textBrush = new SolidBrush(textColor)) { Rectangle textRect = new Rectangle( mergeRect.X + 3, mergeRect.Y + 3, mergeRect.Width - 6, mergeRect.Height - 6); StringFormat sf = new StringFormat { Alignment = mergeArea.HorizontalAlignment, LineAlignment = mergeArea.VerticalAlignment, Trimming = StringTrimming.EllipsisCharacter }; g.DrawString(mergeArea.Text, this.DefaultCellStyle.Font ?? this.Font, textBrush, textRect, sf); } } } catch (Exception ex) { // 🛡️ 异常处理:确保绘制错误不影响整体功能 System.Diagnostics.Debug.WriteLine($"绘制合并区域出错: {ex.Message}"); } } // ... 其他辅助方法(完整代码见文末GitHub链接) } }

📋 第二步:定义MergeArea数据结构

C#
/// <summary> /// 🗂️ 合并区域信息类 /// </summary> public class MergeArea { public int StartRow { get; set; } public int StartColumn { get; set; } public int EndRow { get; set; } public int EndColumn { get; set; } public string Text { get; set; } public StringAlignment HorizontalAlignment { get; set; } = StringAlignment.Center; public StringAlignment VerticalAlignment { get; set; } = StringAlignment.Center; // 🔑 智能排序支持:存储原始行键,排序后能重新匹配 public List<string> OriginalRowKeys { get; set; } = new List<string>(); }

🛠️ 第三步:创建便捷的扩展方法

C#
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AppMergeGrid { /// <summary> /// DataGridView合并扩展方法 /// </summary> public static class MergeableDataGridViewExtensions { /// <summary> /// 自动合并指定列的相同值 - 支持对齐方式 /// </summary> /// <param name="dgv">DataGridView控件</param> /// <param name="columnIndex">要合并的列索引</param> /// <param name="horizontalAlign">水平对齐方式</param> /// <param name="verticalAlign">垂直对齐方式</param> public static void AutoMergeColumn(this MergeableDataGridView dgv, int columnIndex, StringAlignment horizontalAlign = StringAlignment.Center, StringAlignment verticalAlign = StringAlignment.Center) { if (dgv.Rows.Count == 0) return; int startRow = 0; object currentValue = dgv[columnIndex, 0].Value; for (int i = 1; i < dgv.Rows.Count; i++) { object nextValue = dgv[columnIndex, i].Value; if (!Equals(currentValue, nextValue)) { // 值不同,合并前面的连续相同值 if (i - startRow > 1) { dgv.MergeCells(startRow, columnIndex, i - 1, columnIndex, currentValue?.ToString() ?? "", horizontalAlign, verticalAlign); } startRow = i; currentValue = nextValue; } } // 处理最后一组 if (dgv.Rows.Count - startRow > 1) { dgv.MergeCells(startRow, columnIndex, dgv.Rows.Count - 1, columnIndex, currentValue?.ToString() ?? "", horizontalAlign, verticalAlign); } } /// <summary> /// 自动合并多列相同值 - 支持不同对齐方式 /// </summary> /// <param name="dgv">DataGridView控件</param> /// <param name="columnSettings">列设置数组</param> public static void AutoMergeColumns(this MergeableDataGridView dgv, params ColumnMergeSetting[] columnSettings) { foreach (var setting in columnSettings) { dgv.AutoMergeColumn(setting.ColumnIndex, setting.HorizontalAlign, setting.VerticalAlign); } } /// <summary> /// 根据条件自动合并列 /// </summary> /// <param name="dgv">DataGridView控件</param> /// <param name="columnIndex">列索引</param> /// <param name="mergeCondition">合并条件函数</param> /// <param name="horizontalAlign">水平对齐</param> /// <param name="verticalAlign">垂直对齐</param> public static void AutoMergeColumnWithCondition(this MergeableDataGridView dgv, int columnIndex, Func<object, object, bool> mergeCondition, StringAlignment horizontalAlign = StringAlignment.Center, StringAlignment verticalAlign = StringAlignment.Center) { if (dgv.Rows.Count == 0) return; int startRow = 0; object currentValue = dgv[columnIndex, 0].Value; for (int i = 1; i < dgv.Rows.Count; i++) { object nextValue = dgv[columnIndex, i].Value; if (!mergeCondition(currentValue, nextValue)) { // 条件不满足,合并前面的连续值 if (i - startRow > 1) { dgv.MergeCells(startRow, columnIndex, i - 1, columnIndex, currentValue?.ToString() ?? "", horizontalAlign, verticalAlign); } startRow = i; currentValue = nextValue; } } // 处理最后一组 if (dgv.Rows.Count - startRow > 1) { dgv.MergeCells(startRow, columnIndex, dgv.Rows.Count - 1, columnIndex, currentValue?.ToString() ?? "", horizontalAlign, verticalAlign); } } } /// <summary> /// 列合并设置 /// </summary> public class ColumnMergeSetting { public int ColumnIndex { get; set; } public StringAlignment HorizontalAlign { get; set; } = StringAlignment.Center; public StringAlignment VerticalAlign { get; set; } = StringAlignment.Center; public ColumnMergeSetting(int columnIndex, StringAlignment horizontalAlign = StringAlignment.Center, StringAlignment verticalAlign = StringAlignment.Center) { ColumnIndex = columnIndex; HorizontalAlign = horizontalAlign; VerticalAlign = verticalAlign; } } }

🎮 实际使用示例

C#
using System.Data; using System.Windows.Forms; namespace AppMergeGrid { public partial class Form1 : Form { public Form1() { InitializeComponent(); this.Size = new Size(800, 600); this.Text = "可合并单元格的DataGridView"; this.StartPosition = FormStartPosition.CenterScreen; } private void LoadSampleData() { var dt = new System.Data.DataTable(); dt.Columns.Add("产品类别", typeof(string)); dt.Columns.Add("产品名称", typeof(string)); dt.Columns.Add("数量", typeof(int)); dt.Columns.Add("单价", typeof(decimal)); dt.Columns.Add("总价", typeof(decimal)); dt.Rows.Add("电子产品", "笔记本电脑", 10, 5000, 50000); dt.Rows.Add("电子产品", "台式电脑", 5, 3000, 15000); dt.Rows.Add("电子产品", "显示器", 20, 1000, 20000); dt.Rows.Add("办公用品", "打印机", 3, 2000, 6000); dt.Rows.Add("办公用品", "复印机", 2, 8000, 16000); dt.Rows.Add("家具", "办公桌", 15, 800, 12000); dt.Rows.Add("家具", "办公桌", 25, 800, 12000); dt.Rows.Add("家具", "办公椅", 20, 500, 10000); mergeableDataGridView1.DataSource = dt; // 设置列宽 mergeableDataGridView1.Columns[0].Width = 100; mergeableDataGridView1.Columns[1].Width = 150; mergeableDataGridView1.Columns[2].Width = 80; mergeableDataGridView1.Columns[3].Width = 100; mergeableDataGridView1.Columns[4].Width = 100; MessageBox.Show("数据加载完成!点击'自动合并相同项'按钮查看效果。"); } private void AutoMergeSameValues() { mergeableDataGridView1.ClearAllMerges(); mergeableDataGridView1.AutoMergeColumn(0); // 合并第一列 mergeableDataGridView1.AutoMergeColumn(1, StringAlignment.Near, StringAlignment.Center); // 合并第二列 MessageBox.Show("已自动合并产品类别列的相同项!"); } private void btnClearMerge_Click(object sender, EventArgs e) { mergeableDataGridView1.ClearAllMerges(); MessageBox.Show("已清除所有合并!"); } private void btnLoadData_Click(object sender, EventArgs e) { LoadSampleData(); } private void btnAutoMerge_Click(object sender, EventArgs e) { AutoMergeSameValues(); } } }

image.png

⚡ 高级特性亮点

🔥 智能排序支持

当用户点击列头排序时,合并区域会智能重新计算和匹配:

C#
private void RefreshMergeAreasAfterSort() { var updatedMergeAreas = new List<MergeArea>(); foreach (var mergeArea in mergeAreas.ToList()) { // 🧠 根据原始行键重新查找新位置 var newRowIndexes = new List<int>(); foreach (var rowKey in mergeArea.OriginalRowKeys) { int newIndex = FindRowByKey(rowKey); if (newIndex >= 0) newRowIndexes.Add(newIndex); } // ✅ 只有连续的行才重新合并 if (IsContinuousRows(newRowIndexes)) { // 更新合并区域位置 UpdateMergeAreaPosition(mergeArea, newRowIndexes); updatedMergeAreas.Add(mergeArea); } } mergeAreas = updatedMergeAreas; this.Invalidate(); }

🎨 灵活的对齐方式

支持9种对齐组合,满足各种UI需求:

C#
// 左对齐 + 顶部对齐 dgv.AutoMergeColumn(0, StringAlignment.Near, StringAlignment.Near); // 居中对齐 + 居中对齐(默认) dgv.AutoMergeColumn(1, StringAlignment.Center, StringAlignment.Center); // 右对齐 + 底部对齐 dgv.AutoMergeColumn(2, StringAlignment.Far, StringAlignment.Far);

🚨 常见坑点提醒

⚠️ 坑点1:双缓冲必须开启

C#
// 必须设置这些样式,否则会闪烁 this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true);

⚠️ 坑点2:重叠合并区域处理

C#
// 新建合并前必须检查重叠 private void RemoveOverlappingMerges(int startRow, int startCol, int endRow, int endCol) { mergeAreas.RemoveAll(area => !(endRow < area.StartRow || startRow > area.EndRow || endCol < area.StartColumn || startCol > area.EndColumn)); }

⚠️ 坑点3:滚动偏移计算

C#
// 计算合并区域位置时要考虑滚动偏移 left -= this.HorizontalScrollingOffset; top -= this.VerticalScrollingOffset;

🏆 性能优化秘籍

  1. 局部重绘:只重绘发生变化的区域
  2. 异常捕获:绘制异常不影响整体功能
  3. 内存管理:及时释放Graphics资源
  4. 智能验证:避免无效的合并操作

💡 扩展应用场景

  • 📊 财务报表系统:合并相同科目的明细
  • 📋 库存管理系统:按类别合并商品展示
  • 👥 人事管理系统:按部门合并员工信息
  • 📈 数据分析平台:多维度数据分组展示

🎯 核心要点总结

通过这个自定义控件,我们完美解决了DataGridView单元格合并的痛点问题。三个关键要点:

🔸 继承原生控件:保持所有原有功能的同时添加合并能力,兼容性最佳。

🔸 智能重绘机制:通过重写OnPaint方法实现自定义绘制,性能优秀且效果专业。

🔸 排序智能适配:独创的行键匹配算法,让合并区域在排序后依然能正确显示。

这套解决方案已在多个企业级项目中稳定运行,代码简洁易懂,扩展性强。你再也不用为复杂的表格展示需求而烦恼了!


💬 互动讨论:你在项目中遇到过哪些复杂的数据展示需求?这个控件还可以增加什么功能?欢迎在评论区分享你的想法和使用经验!

🔥 觉得有用请转发给更多同行,让更多C#开发者受益!完整源码已上传GitHub,关注我获取下载链接。

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!