2025-11-03
C#
00

目录

🎯 痛点分析:为什么需要可折叠数据表格?
💡 解决方案:自定义CollapsibleDataGridView
🚀 核心代码实战
📝 第一步:定义分组信息类
🎨 第二步:核心控件设计
🔄 第三步:数据分组处理
🎨 第四步:自定义绘制分组标题
🖱️ 第五步:交互事件处理
完整控件类
🎮 使用示例
⚠️ 开发注意事项
🔧 性能优化要点
🐛 常见坑点提醒
🎯 功能扩展建议
✨ 总结与展望

你是否曾为Excel式的数据展示而苦恼?当面对成千上万条记录时,用户总是抱怨"找不到想要的数据"、"界面太乱了"。据统计,85%的企业应用都存在数据展示体验差的问题,而传统的DataGridView控件往往无法满足现代化的交互需求。

今天,我将带你从零开始构建一个可折叠分组的DataGridView控件,彻底解决数据展示混乱的痛点,让你的应用瞬间提升一个档次!

🎯 痛点分析:为什么需要可折叠数据表格?

在实际开发中,我们经常遇到这些场景:

  • 员工信息按部门展示,数据量大时查找困难
  • 订单数据按时间分组,用户需要快速定位特定时段
  • 产品分类展示,希望支持展开/折叠提升浏览体验

传统DataGridView的局限性:

✗ 不支持数据分组

✗ 无法折叠/展开

✗ 用户体验差

✗ 自定义困难

💡 解决方案:自定义CollapsibleDataGridView

我们的解决方案核心思路:

  1. 继承UserControl,封装DataGridView
  2. 虚拟数据结构,添加分组信息列
  3. 自定义绘制,实现分组标题行
  4. 事件驱动,支持展开/折叠交互

🚀 核心代码实战

📝 第一步:定义分组信息类

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(); } } }

image.png

⚠️ 开发注意事项

🔧 性能优化要点

  1. 避免频繁刷新:数据量大时,合并多个操作后统一刷新
  2. 内存管理:及时释放绘制相关的GDI+对象
  3. 事件处理:避免在绘制事件中执行耗时操作

🐛 常见坑点提醒

这里是个重点,其实吧,想明白的单元格渲染,其实都还好写了。

C#
// ❌ 错误:忘记设置Handled属性 private void DataGridView_CellPainting(object sender, DataGridViewCellPaintingEventArgs e) { // 自定义绘制代码... // 忘记设置这行会导致重复绘制 // e.Handled = true; } // ✅ 正确:必须设置Handled属性 e.Handled = true;

🎯 功能扩展建议

基于这个基础框架,你可以轻松扩展:

  • 多级分组:支持部门→小组的层级结构
  • 自定义图标:使用图片替代Unicode字符
  • 排序功能:保持分组状态的同时支持排序
  • 搜索过滤:在分组基础上添加搜索功能

✨ 总结与展望

通过本文的完整实现,我们解决了三个核心问题:

  1. 数据组织:通过虚拟表格结构实现灵活的分组显示
  2. 用户体验:自定义绘制提供现代化的交互界面
  3. 开发效率:封装完整的API,一行代码即可实现分组功能

这个自定义控件不仅解决了传统DataGridView的局限性,更为你的应用带来了专业级的数据展示体验。无论是企业管理系统还是数据分析工具,都能完美胜任。

💬 互动时间:你在项目中还遇到过哪些DataGridView的使用痛点?或者你觉得这个控件还可以添加哪些实用功能?欢迎在评论区分享你的想法和经验!

觉得这个技术方案对你有帮助?请转发给更多同行,让我们一起提升C#开发的用户体验!

本文作者:技术老小子

本文链接:

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