前言
在企业级应用开发中,数据展示是一个至关重要的环节。很多时候,用户希望表格能够像 Excel 一样展示复杂的数据结构,尤其是单元格合并功能,它能让数据更清晰、界面更专业。然而,传统的 WinForm 控件 DataGridView 在实现这一功能时却显得力不从心。
据统计,90% 的企业级应用都需要复杂的数据展示功能,而其中"单元格合并"是最常见的需求之一。面对老板的要求、客户的对比、以及高昂的第三方成本,很多开发者只能望而却步。
本文将一步步实现一个自定义的 MergeableDataGridView 控件,不仅具备原生 DataGridView 的所有功能,还完美支持单元格合并,甚至在排序后也能智能适配,真正实现高颜值与高性能并存。
传统 DataGridView 的痛点分析
在实际开发中,我们常常会遇到以下问题:
-
数据重复显示混乱:当同一类别有多个子项时,传统表格会重复显示类别名称,造成视觉混乱。
-
界面不够专业:客户总是拿 Excel 的效果做对比,觉得我们的系统"不够高大上"。
-
开发成本高:市面上的解决方案要么收费昂贵,要么 Bug 满天飞,自己实现又不知从何下手。
问题归根结底,是因为标准的 DataGridView 并不原生支持单元格合并,需要通过自定义绘制和逻辑处理来实现。
解决方案:MergeableDataGridView 控件设计思路
我们采用继承的方式,基于原生的 DataGridView 创建一个新的控件类,并添加以下三个核心组件:
1、MergeableDataGridView 主控件
继承自 DataGridView,扩展其绘制逻辑以支持合并区域。
2、MergeArea 合并区域类
存储每个合并区域的信息(起始行、结束行、文本、对齐方式等)。
3、扩展方法类
提供便捷的 API 接口,如自动合并某一列。
完整代码
第一步:创建 MergeableDataGridView 主控件
public class MergeableDataGridView : DataGridView
{
private List<MergeArea> mergeAreas = new List<MergeArea>();
public List<MergeArea> MergeAreas
{
get { return mergeAreas; }
set { mergeAreas = value; }
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
foreach (var area in mergeAreas)
{
if (area.EndRow < this.FirstDisplayedScrollingRowIndex ||
area.StartRow > this.FirstDisplayedScrollingRowIndex + this.DisplayedRowCount(false))
continue;
Rectangle cellRect = GetCellDisplayRectangle(area.StartColumn, area.StartRow, false);
Rectangle endRect = GetCellDisplayRectangle(area.EndColumn, area.EndRow, false);
int left = cellRect.Left;
int top = cellRect.Top;
int right = endRect.Right;
int bottom = endRect.Bottom;
// 考虑滚动偏移
left -= this.HorizontalScrollingOffset;
top -= this.VerticalScrollingOffset;
Rectangle mergedRect = new Rectangle(left, top, right - left, bottom - top);
using (Brush backBrush = new SolidBrush(this.DefaultCellStyle.BackColor))
{
e.Graphics.FillRectangle(backBrush, mergedRect);
}
TextRenderer.DrawText(e.Graphics, area.Text, this.Font,
mergedRect, this.ForeColor,
TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter |
TextFormatFlags.WordEllipsis);
}
}
}
第二步:定义 MergeArea 数据结构
/// <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>();
}
第三步:创建便捷的扩展方法(示例)
public static class DataGridViewExtensions
{
public static void AutoMergeColumn(this MergeableDataGridView dgv, int columnIndex,
StringAlignment horizontalAlignment = StringAlignment.Center,
StringAlignment verticalAlignment = StringAlignment.Center)
{
var mergeAreas = new List<MergeArea>();
int rowCount = dgv.Rows.Count;
int currentStart = 0;
for (int i = 1; i < rowCount; i++)
{
if (dgv.Rows[i].Cells[columnIndex].Value?.ToString() !=
dgv.Rows[i - 1].Cells[columnIndex].Value?.ToString())
{
if (i - currentStart > 1)
{
mergeAreas.Add(new MergeArea
{
StartRow = currentStart,
EndRow = i - 1,
StartColumn = columnIndex,
EndColumn = columnIndex,
Text = dgv.Rows[currentStart].Cells[columnIndex].Value?.ToString(),
HorizontalAlignment = horizontalAlignment,
VerticalAlignment = verticalAlignment
});
}
currentStart = i;
}
}
// 处理最后一组
if (rowCount - currentStart > 1)
{
mergeAreas.Add(new MergeArea
{
StartRow = currentStart,
EndRow = rowCount - 1,
StartColumn = columnIndex,
EndColumn = columnIndex,
Text = dgv.Rows[currentStart].Cells[columnIndex].Value?.ToString(),
HorizontalAlignment = horizontalAlignment,
VerticalAlignment = verticalAlignment
});
}
dgv.MergeAreas = mergeAreas;
dgv.Invalidate();
}
}
实际使用示例
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();
}
}
}
高级特性
智能排序支持
当用户点击列头排序时,合并区域会智能重新计算和匹配:
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 需求:
// 左对齐 + 顶部对齐
dgv.AutoMergeColumn(0, StringAlignment.Near, StringAlignment.Near);
// 居中对齐 + 居中对齐(默认)
dgv.AutoMergeColumn(1, StringAlignment.Center, StringAlignment.Center);
// 右对齐 + 底部对齐
dgv.AutoMergeColumn(2, StringAlignment.Far, StringAlignment.Far);
常见提醒
坑点1:双缓冲必须开启
// 必须设置这些样式,否则会闪烁
this.SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.DoubleBuffer |
ControlStyles.ResizeRedraw,
true);
坑点2:重叠合并区域处理
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:滚动偏移计算
left -= this.HorizontalScrollingOffset;
top -= this.VerticalScrollingOffset;
性能优化秘籍
1、局部重绘:只重绘发生变化的区域;
2、异常捕获:绘制异常不影响整体功能;
3、内存管理:及时释放 Graphics 资源;
4、智能验证:避免无效的合并操作;
总结
通过这个自定义控件,我们解决了传统 DataGridView 在单元格合并方面的诸多限制。
三大关键点如下:
-
继承原生控件:保持所有原有功能的同时添加合并能力,兼容性最佳;
-
智能重绘机制:通过重写 OnPaint 方法实现自定义绘制,性能优秀且效果专业;
-
排序智能适配:独创的行键匹配算法,让合并区域在排序后依然能正确显示。
该控件已在多个企业级项目中稳定运行,代码简洁易懂,扩展性强,是替代传统 DataGridView 的理想选择。
关键词
DataGridView、单元格合并、MergeableDataGridView、Winform、数据展示、自定义控件、排序适配、对齐方式、性能优化、企业级应用
最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。
也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!
作者:技术老小子
出处:mp.weixin.qq.com/s/tAMCzGxMgXZ8a5iRa_9Ayg
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!