你是否曾为Excel式的数据展示而苦恼?当面对成千上万条记录时,用户总是抱怨"找不到想要的数据"、"界面太乱了"。据统计,85%的企业应用都存在数据展示体验差的问题,而传统的DataGridView控件往往无法满足现代化的交互需求。
今天,我将带你从零开始构建一个可折叠分组的DataGridView控件,彻底解决数据展示混乱的痛点,让你的应用瞬间提升一个档次!
在实际开发中,我们经常遇到这些场景:
传统DataGridView的局限性:
✗ 不支持数据分组
✗ 无法折叠/展开
✗ 用户体验差
✗ 自定义困难
我们的解决方案核心思路:
C#// 分组信息类 - 管理每个分组的状态和数据
internal class GroupInfo
{
public string GroupName { get; set; } // 分组名称
public bool IsExpanded { get; set; } // 是否展开
public List<DataRow> Rows { get; set; } // 分组包含的数据行
public GroupInfo()
{
Rows = new List<DataRow>();
}
}
C#public partial class CollapsibleDataGridView : UserControl
{
private DataGridView dataGridView;
private List<GroupInfo> groups;
private DataTable originalDataTable;
private string groupColumnName;
private bool showGroupHeaders = true;
private const int GROUP_HEADER_HEIGHT = 25; // 分组标题行高度
// 💡 关键:初始化DataGridView设置
private void InitializeDataGridView()
{
dataGridView = new DataGridView();
dataGridView.Dock = DockStyle.Fill;
dataGridView.AllowUserToAddRows = false;
dataGridView.ReadOnly = true;
dataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
// ⚠️ 重要:设置行高度模式,避免自动调整
dataGridView.RowTemplate.Height = 22;
dataGridView.AllowUserToResizeRows = false;
// 绑定关键事件
dataGridView.CellPainting += DataGridView_CellPainting;
dataGridView.CellClick += DataGridView_CellClick;
dataGridView.RowPrePaint += DataGridView_RowPrePaint;
dataGridView.CellFormatting += DataGridView_CellFormatting;
this.Controls.Add(dataGridView);
}
}
C#// 🎯 核心方法:刷新分组数据
private void RefreshGroups()
{
if (originalDataTable == null || string.IsNullOrEmpty(groupColumnName))
return;
groups.Clear();
// 使用LINQ按指定列分组
var groupedData = originalDataTable.AsEnumerable()
.GroupBy(row => row[groupColumnName]?.ToString() ?? "")
.OrderBy(g => g.Key);
foreach (var group in groupedData)
{
var groupInfo = new GroupInfo
{
GroupName = group.Key,
IsExpanded = true, // 默认展开
Rows = group.ToList()
};
groups.Add(groupInfo);
}
RefreshDisplay();
}
// 🎨 显示数据刷新 - 创建虚拟表格结构
private void RefreshDisplay()
{
if (originalDataTable == null) return;
// 克隆原始表格结构
DataTable displayTable = originalDataTable.Clone();
// ⚡ 添加辅助列(用于标识分组信息)
displayTable.Columns.Add("__IsGroupHeader", typeof(bool));
displayTable.Columns.Add("__GroupName", typeof(string));
displayTable.Columns.Add("__IsExpanded", typeof(bool));
displayTable.Columns.Add("__GroupRowCount", typeof(int));
foreach (var group in groups)
{
if (showGroupHeaders)
{
// 添加分组标题行
DataRow headerRow = displayTable.NewRow();
// 清空所有原始列的值
for (int i = 0; i < originalDataTable.Columns.Count; i++)
{
headerRow[i] = DBNull.Value;
}
headerRow["__IsGroupHeader"] = true;
headerRow["__GroupName"] = group.GroupName;
headerRow["__IsExpanded"] = group.IsExpanded;
headerRow["__GroupRowCount"] = group.Rows.Count;
displayTable.Rows.Add(headerRow);
}
// 如果分组展开,添加数据行
if (group.IsExpanded)
{
foreach (var row in group.Rows)
{
DataRow newRow = displayTable.NewRow();
for (int i = 0; i < originalDataTable.Columns.Count; i++)
{
newRow[i] = row[i];
}
newRow["__IsGroupHeader"] = false;
newRow["__GroupName"] = group.GroupName;
displayTable.Rows.Add(newRow);
}
}
}
dataGridView.DataSource = displayTable;
HideHelperColumns(); // 隐藏辅助列
}
C#// 🎨 单元格自定义绘制事件
private void DataGridView_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
if (e.RowIndex < 0 || e.ColumnIndex < 0) return;
DataGridView dgv = sender as DataGridView;
DataRowView rowView = dgv.Rows[e.RowIndex].DataBoundItem as DataRowView;
if (rowView != null && Convert.ToBoolean(rowView["__IsGroupHeader"]))
{
// 绘制分组标题行背景
using (var brush = new SolidBrush(Color.FromArgb(230, 235, 245)))
{
e.Graphics.FillRectangle(brush, e.CellBounds);
}
// 绘制边框
using (var pen = new Pen(Color.FromArgb(200, 200, 200)))
{
e.Graphics.DrawRectangle(pen, e.CellBounds);
}
// 第一列绘制分组标题文本
if (e.ColumnIndex == 0)
{
bool isExpanded = Convert.ToBoolean(rowView["__IsExpanded"]);
string groupName = rowView["__GroupName"].ToString();
int count = Convert.ToInt32(rowView["__GroupRowCount"]);
// 🔥 关键:使用Unicode字符显示展开/折叠图标
string icon = isExpanded ? "▼" : "▶";
string text = $"{icon} {groupName} ({count} 项)";
using (var brush = new SolidBrush(Color.FromArgb(50, 50, 50)))
using (var font = new Font(dgv.Font, FontStyle.Bold))
{
var textRect = new Rectangle(e.CellBounds.X + 8, e.CellBounds.Y + 4,
e.CellBounds.Width - 16, e.CellBounds.Height - 8);
e.Graphics.DrawString(text, font, brush, textRect,
new StringFormat {
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Center
});
}
}
e.Handled = true; // 阻止默认绘制
}
}
C#// 🖱️ 单元格点击事件 - 处理展开/折叠
private void DataGridView_CellClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0) return;
DataGridView dgv = sender as DataGridView;
DataRowView rowView = dgv.Rows[e.RowIndex].DataBoundItem as DataRowView;
if (rowView != null && Convert.ToBoolean(rowView["__IsGroupHeader"]))
{
// 切换分组的展开/折叠状态
string groupName = rowView["__GroupName"].ToString();
var group = groups.FirstOrDefault(g => g.GroupName == groupName);
if (group != null)
{
group.IsExpanded = !group.IsExpanded;
RefreshDisplay(); // 刷新显示
}
}
}
// 📏 行预绘制事件 - 设置分组行样式
private void DataGridView_RowPrePaint(object sender, DataGridViewRowPrePaintEventArgs e)
{
if (e.RowIndex < 0 || e.RowIndex >= dataGridView.Rows.Count) return;
DataGridViewRow row = dataGridView.Rows[e.RowIndex];
DataRowView rowView = row.DataBoundItem as DataRowView;
if (rowView != null && Convert.ToBoolean(rowView["__IsGroupHeader"]))
{
// 设置分组标题行样式
row.Height = GROUP_HEADER_HEIGHT;
row.DefaultCellStyle.BackColor = Color.FromArgb(240, 240, 240);
row.DefaultCellStyle.Font = new Font(dataGridView.Font, FontStyle.Bold);
}
else
{
// 普通数据行样式
row.Height = 22;
row.DefaultCellStyle.BackColor = Color.White;
}
}
C#using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AppCollapsibleDataGrid
{
public partial class CollapsibleDataGridView : UserControl
{
private DataGridView dataGridView;
private List<GroupInfo> groups;
private DataTable originalDataTable;
private string groupColumnName;
private bool showGroupHeaders = true;
private const int GROUP_HEADER_HEIGHT = 25;
public CollapsibleDataGridView()
{
InitializeComponent();
InitializeDataGridView();
groups = new List<GroupInfo>();
}
private void InitializeDataGridView()
{
dataGridView = new DataGridView();
dataGridView.Dock = DockStyle.Fill;
dataGridView.AllowUserToAddRows = false;
dataGridView.AllowUserToDeleteRows = false;
dataGridView.ReadOnly = true;
dataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dataGridView.RowHeadersVisible = false;
dataGridView.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
dataGridView.BackgroundColor = Color.White;
dataGridView.GridColor = Color.LightGray;
// 重要:设置行高度模式
dataGridView.RowTemplate.Height = 22;
dataGridView.AllowUserToResizeRows = false;
dataGridView.CellPainting += DataGridView_CellPainting;
dataGridView.CellClick += DataGridView_CellClick;
dataGridView.RowPrePaint += DataGridView_RowPrePaint;
dataGridView.CellFormatting += DataGridView_CellFormatting;
this.Controls.Add(dataGridView);
}
// 公共属性
[Category("Collapsible")]
[Description("获取或设置用于分组的列名")]
public string GroupColumn
{
get { return groupColumnName; }
set
{
groupColumnName = value;
if (originalDataTable != null)
{
RefreshGroups();
}
}
}
[Category("Collapsible")]
[Description("获取或设置是否显示分组标题")]
public bool ShowGroupHeaders
{
get { return showGroupHeaders; }
set
{
showGroupHeaders = value;
RefreshDisplay();
}
}
[Category("Collapsible")]
[Description("获取内部DataGridView控件")]
public DataGridView InnerDataGridView
{
get { return dataGridView; }
}
// 设置数据源
public void SetDataSource(DataTable dataTable, string groupByColumn)
{
originalDataTable = dataTable.Copy();
groupColumnName = groupByColumn;
RefreshGroups();
}
// 刷新分组
private void RefreshGroups()
{
if (originalDataTable == null || string.IsNullOrEmpty(groupColumnName))
return;
groups.Clear();
// 按分组列分组数据
var groupedData = originalDataTable.AsEnumerable()
.GroupBy(row => row[groupColumnName]?.ToString() ?? "")
.OrderBy(g => g.Key);
foreach (var group in groupedData)
{
var groupInfo = new GroupInfo
{
GroupName = group.Key,
IsExpanded = true,
Rows = group.ToList()
};
groups.Add(groupInfo);
}
RefreshDisplay();
}
// 刷新显示
private void RefreshDisplay()
{
if (originalDataTable == null)
return;
// 创建新的DataTable用于显示
DataTable displayTable = originalDataTable.Clone();
displayTable.Columns.Add("__IsGroupHeader", typeof(bool));
displayTable.Columns.Add("__GroupName", typeof(string));
displayTable.Columns.Add("__IsExpanded", typeof(bool));
displayTable.Columns.Add("__GroupRowCount", typeof(int));
foreach (var group in groups)
{
if (showGroupHeaders)
{
// 添加分组标题行
DataRow headerRow = displayTable.NewRow();
for (int i = 0; i < originalDataTable.Columns.Count; i++)
{
headerRow[i] = DBNull.Value; // 清空所有原始列的值
}
headerRow["__IsGroupHeader"] = true;
headerRow["__GroupName"] = group.GroupName;
headerRow["__IsExpanded"] = group.IsExpanded;
headerRow["__GroupRowCount"] = group.Rows.Count;
displayTable.Rows.Add(headerRow);
}
// 如果分组是展开的,添加数据行
if (group.IsExpanded)
{
foreach (var row in group.Rows)
{
DataRow newRow = displayTable.NewRow();
for (int i = 0; i < originalDataTable.Columns.Count; i++)
{
newRow[i] = row[i];
}
newRow["__IsGroupHeader"] = false;
newRow["__GroupName"] = group.GroupName;
newRow["__IsExpanded"] = group.IsExpanded;
newRow["__GroupRowCount"] = 0;
displayTable.Rows.Add(newRow);
}
}
}
// 设置数据源
dataGridView.DataSource = displayTable;
// 隐藏辅助列
HideHelperColumns();
}
private void HideHelperColumns()
{
string[] helperColumns = { "__IsGroupHeader", "__GroupName", "__IsExpanded", "__GroupRowCount" };
foreach (string colName in helperColumns)
{
if (dataGridView.Columns.Contains(colName))
{
dataGridView.Columns[colName].Visible = false;
}
}
}
// 行预绘制事件 - 设置分组行的高度和样式
private void DataGridView_RowPrePaint(object sender, DataGridViewRowPrePaintEventArgs e)
{
if (e.RowIndex < 0 || e.RowIndex >= dataGridView.Rows.Count)
return;
DataGridViewRow row = dataGridView.Rows[e.RowIndex];
DataRowView rowView = row.DataBoundItem as DataRowView;
if (rowView != null && Convert.ToBoolean(rowView["__IsGroupHeader"]))
{
// 设置分组标题行的高度
row.Height = GROUP_HEADER_HEIGHT;
row.DefaultCellStyle.BackColor = Color.FromArgb(240, 240, 240);
row.DefaultCellStyle.Font = new Font(dataGridView.Font, FontStyle.Bold);
}
else
{
// 普通数据行
row.Height = 22;
row.DefaultCellStyle.BackColor = Color.White;
}
}
// 单元格格式化事件
private void DataGridView_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
if (e.RowIndex < 0 || e.RowIndex >= dataGridView.Rows.Count)
return;
DataGridViewRow row = dataGridView.Rows[e.RowIndex];
DataRowView rowView = row.DataBoundItem as DataRowView;
if (rowView != null && Convert.ToBoolean(rowView["__IsGroupHeader"]))
{
// 分组标题行只在第一列显示内容
if (e.ColumnIndex == 0)
{
bool isExpanded = Convert.ToBoolean(rowView["__IsExpanded"]);
string groupName = rowView["__GroupName"].ToString();
int count = Convert.ToInt32(rowView["__GroupRowCount"]);
string icon = isExpanded ? "▼" : "▶";
e.Value = $"{icon} {groupName} ({count} 项)";
e.FormattingApplied = true;
}
else
{
// 其他列显示空白
e.Value = "";
e.FormattingApplied = true;
}
}
}
// 单元格绘制事件
private void DataGridView_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
if (e.RowIndex < 0 || e.ColumnIndex < 0)
return;
DataGridView dgv = sender as DataGridView;
DataRowView rowView = dgv.Rows[e.RowIndex].DataBoundItem as DataRowView;
if (rowView != null && Convert.ToBoolean(rowView["__IsGroupHeader"]))
{
// 绘制分组标题行背景
using (var brush = new SolidBrush(Color.FromArgb(230, 235, 245)))
{
e.Graphics.FillRectangle(brush, e.CellBounds);
}
// 绘制边框
using (var pen = new Pen(Color.FromArgb(200, 200, 200)))
{
e.Graphics.DrawRectangle(pen, e.CellBounds);
}
// 如果是第一列,绘制分组标题文本
if (e.ColumnIndex == 0)
{
bool isExpanded = Convert.ToBoolean(rowView["__IsExpanded"]);
string groupName = rowView["__GroupName"].ToString();
int count = Convert.ToInt32(rowView["__GroupRowCount"]);
string icon = isExpanded ? "▼" : "▶";
string text = $"{icon} {groupName} ({count} 项)";
using (var brush = new SolidBrush(Color.FromArgb(50, 50, 50)))
using (var font = new Font(dgv.Font, FontStyle.Bold))
{
var textRect = new Rectangle(e.CellBounds.X + 8, e.CellBounds.Y + 4,
e.CellBounds.Width - 16, e.CellBounds.Height - 8);
e.Graphics.DrawString(text, font, brush, textRect,
new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center });
}
}
e.Handled = true;
}
}
// 单元格点击事件
private void DataGridView_CellClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0)
return;
DataGridView dgv = sender as DataGridView;
DataRowView rowView = dgv.Rows[e.RowIndex].DataBoundItem as DataRowView;
if (rowView != null && Convert.ToBoolean(rowView["__IsGroupHeader"]))
{
// 切换分组的展开/折叠状态
string groupName = rowView["__GroupName"].ToString();
var group = groups.FirstOrDefault(g => g.GroupName == groupName);
if (group != null)
{
group.IsExpanded = !group.IsExpanded;
RefreshDisplay();
}
}
}
// 展开所有分组
public void ExpandAll()
{
foreach (var group in groups)
{
group.IsExpanded = true;
}
RefreshDisplay();
}
// 折叠所有分组
public void CollapseAll()
{
foreach (var group in groups)
{
group.IsExpanded = false;
}
RefreshDisplay();
}
// 展开指定分组
public void ExpandGroup(string groupName)
{
var group = groups.FirstOrDefault(g => g.GroupName == groupName);
if (group != null)
{
group.IsExpanded = true;
RefreshDisplay();
}
}
// 折叠指定分组
public void CollapseGroup(string groupName)
{
var group = groups.FirstOrDefault(g => g.GroupName == groupName);
if (group != null)
{
group.IsExpanded = false;
RefreshDisplay();
}
}
// 获取所有分组信息
public List<string> GetGroupNames()
{
return groups.Select(g => g.GroupName).ToList();
}
// 获取指定分组的展开状态
public bool IsGroupExpanded(string groupName)
{
var group = groups.FirstOrDefault(g => g.GroupName == groupName);
return group?.IsExpanded ?? false;
}
}
// 分组信息类
internal class GroupInfo
{
public string GroupName { get; set; }
public bool IsExpanded { get; set; }
public List<DataRow> Rows { get; set; }
public GroupInfo()
{
Rows = new List<DataRow>();
}
}
}
C#using System.Data;
namespace AppCollapsibleDataGrid
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
SetupControls();
LoadSampleData();
}
private void SetupControls()
{
this.Size = new Size(800, 600);
this.Text = "可折叠DataGridView示例";
}
private void LoadSampleData()
{
// 创建示例数据
DataTable dataTable = new DataTable();
dataTable.Columns.Add("ID", typeof(int));
dataTable.Columns.Add("姓名", typeof(string));
dataTable.Columns.Add("部门", typeof(string));
dataTable.Columns.Add("职位", typeof(string));
dataTable.Columns.Add("薪资", typeof(decimal));
// 添加示例数据
dataTable.Rows.Add(1, "张三", "技术部", "软件工程师", 8000);
dataTable.Rows.Add(2, "李四", "技术部", "高级工程师", 12000);
dataTable.Rows.Add(3, "王五", "技术部", "架构师", 18000);
dataTable.Rows.Add(4, "赵六", "销售部", "销售代表", 6000);
dataTable.Rows.Add(5, "钱七", "销售部", "销售经理", 15000);
dataTable.Rows.Add(6, "孙八", "人事部", "人事专员", 5000);
dataTable.Rows.Add(7, "周九", "人事部", "人事经理", 10000);
dataTable.Rows.Add(8, "吴十", "财务部", "会计", 7000);
dataTable.Rows.Add(9, "郑十一", "财务部", "财务经理", 13000);
dataTable.Rows.Add(10, "王十二", "技术部", "测试工程师", 7500);
// 设置数据源,按部门分组
collapsibleGrid.SetDataSource(dataTable, "部门");
}
private void BtnLoadData_Click(object sender, EventArgs e)
{
LoadSampleData();
}
private void BtnExpandAll_Click(object sender, EventArgs e)
{
collapsibleGrid.ExpandAll();
}
private void BtnCollapseAll_Click(object sender, EventArgs e)
{
collapsibleGrid.CollapseAll();
}
}
}

这里是个重点,其实吧,想明白的单元格渲染,其实都还好写了。
C#// ❌ 错误:忘记设置Handled属性
private void DataGridView_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
// 自定义绘制代码...
// 忘记设置这行会导致重复绘制
// e.Handled = true;
}
// ✅ 正确:必须设置Handled属性
e.Handled = true;
基于这个基础框架,你可以轻松扩展:
通过本文的完整实现,我们解决了三个核心问题:
这个自定义控件不仅解决了传统DataGridView的局限性,更为你的应用带来了专业级的数据展示体验。无论是企业管理系统还是数据分析工具,都能完美胜任。
💬 互动时间:你在项目中还遇到过哪些DataGridView的使用痛点?或者你觉得这个控件还可以添加哪些实用功能?欢迎在评论区分享你的想法和经验!
觉得这个技术方案对你有帮助?请转发给更多同行,让我们一起提升C#开发的用户体验!
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!