你是否遇到过这样的场景:老板要求在系统中展示复杂的数据报表,需要合并相同类别的单元格,就像Excel那样?传统的DataGridView让你抓狂,要么显示效果丑陋,要么实现复杂到让人崩溃。
据统计,90%的企业级应用都需要复杂的数据展示功能,而单元格合并是其中最常见的需求之一。今天,我将带你手把手打造一个支持单元格合并的高性能DataGridView控件,让你的应用界面瞬间提升一个档次!
在企业开发中,我们经常面临这些头疼问题:
🔸 数据重复显示混乱
当同一类别有多个子项时,传统表格会重复显示类别名称,用户体验极差。
🔸 界面不够专业
客户总是拿Excel的效果来对比,觉得我们的系统"不够高大上"。
🔸 开发成本高
网上的解决方案要么收费,要么bug一堆,自己写又不知道从何下手。
我们的解决方案包含三个核心组件:
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链接)
}
}
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();
}
}
}

当用户点击列头排序时,合并区域会智能重新计算和匹配:
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);
C#// 必须设置这些样式,否则会闪烁
this.SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.DoubleBuffer |
ControlStyles.ResizeRedraw, true);
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));
}
C#// 计算合并区域位置时要考虑滚动偏移
left -= this.HorizontalScrollingOffset;
top -= this.VerticalScrollingOffset;
通过这个自定义控件,我们完美解决了DataGridView单元格合并的痛点问题。三个关键要点:
🔸 继承原生控件:保持所有原有功能的同时添加合并能力,兼容性最佳。
🔸 智能重绘机制:通过重写OnPaint方法实现自定义绘制,性能优秀且效果专业。
🔸 排序智能适配:独创的行键匹配算法,让合并区域在排序后依然能正确显示。
这套解决方案已在多个企业级项目中稳定运行,代码简洁易懂,扩展性强。你再也不用为复杂的表格展示需求而烦恼了!
💬 互动讨论:你在项目中遇到过哪些复杂的数据展示需求?这个控件还可以增加什么功能?欢迎在评论区分享你的想法和使用经验!
🔥 觉得有用请转发给更多同行,让更多C#开发者受益!完整源码已上传GitHub,关注我获取下载链接。
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!