2025-11-03
C#
00

目录

🔍 问题分析:传统DataGridView的局限性
💡 解决方案:自定义可折叠DataGridView
🎯 核心设计思路
🔥 方案一:核心组件架构设计
🎨 方案二:DataGridView初始化与优化
📊 方案三:数据分组与显示逻辑
🎨 方案四:自定义绘制分组标题
⚡ 方案五:高性能数据更新机制
🎪 实际应用示例:设备监控系统
完整代码
🎯 常见问题与解决方案
❓ 问题一:大数据量时性能下降
❓ 问题二:分组标题显示异常
❓ 问题三:折叠展开操作不流畅
🏆 总结与最佳实践

这个问题一个网友提出的,就前一个版本的gridview 做了一些改进,像在一个工厂设备监控项目中,我们需要按车间对设备数据进行分组展示,经过深入研究,成功实现了一个高性能的可折叠DataGridView组件。今天就来分享这个实战经验,帮你轻松解决数据分组展示的难题!

🔍 问题分析:传统DataGridView的局限性

在企业级应用开发中,我们经常面临以下挑战:

1. 数据量庞大,用户体验差

  • 成千上万条数据一次性展示,页面卡顿
  • 用户难以快速定位所需信息

2. 缺乏分组功能

  • 无法按业务逻辑对数据进行分类
  • 相关数据分散,查看不便

3. 交互性不足

  • 用户无法根据需要隐藏不关心的数据
  • 缺乏现代化的用户界面体验

💡 解决方案:自定义可折叠DataGridView

🎯 核心设计思路

我们的解决方案包含以下几个关键特性:

  1. 分组数据管理:按指定列对数据进行分组
  2. 可视化分组标题:显示分组名称、数据条数和状态
  3. 折叠展开功能:支持单独或批量操作
  4. 实时数据更新:优化的单元格更新机制
  5. 自定义扩展:支持分组标题自定义内容

🔥 方案一:核心组件架构设计

首先,我们定义一个分组信息类来管理每个分组的状态:

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

接下来是主要的可折叠DataGridView控件:

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; // 存储每个分组的自定义文字 private Dictionary<string, string> groupCustomTexts; // 批量更新控制标志 private bool isBatchUpdating = false; public CollapsibleDataGridView() { InitializeComponent(); InitializeDataGridView(); groups = new List<GroupInfo>(); groupCustomTexts = new Dictionary<string, string>(); } }

💡 设计要点:

  • 使用UserControl封装,便于复用
  • 分离数据存储(originalDataTable)和显示逻辑
  • 引入批量更新机制,避免频繁重绘造成的性能问题

🎨 方案二:DataGridView初始化与优化

C#
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; // 启用双缓冲减少闪烁 typeof(DataGridView).InvokeMember("DoubleBuffered", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty, null, dataGridView, new object[] { true }); // 绑定关键事件 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); // 初始化分组自定义文字 if (!groupCustomTexts.ContainsKey(group.Key)) { groupCustomTexts[group.Key] = DateTime.Now.ToString("HH:mm:ss"); } } RefreshDisplay(); } private void RefreshDisplay() { if (originalDataTable == null) return; // 暂停布局以减少重绘次数 dataGridView.SuspendLayout(); try { 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(); // 隐藏辅助列 } finally { dataGridView.ResumeLayout(); } }

💪 技术要点:

  • 使用辅助列存储分组元数据,但在界面上隐藏
  • SuspendLayout/ResumeLayout 配对使用,避免多次重绘
  • 根据展开状态动态添加数据行

🎨 方案四:自定义绘制分组标题

为了实现美观的分组标题效果,我们需要自定义单元格绘制:

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"]); 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 }); } } // 🎨 在最后一列显示自定义文字(如时间戳) else if (e.ColumnIndex == dgv.Columns.Count - 5) { string groupName = rowView["__GroupName"].ToString(); string text = GetGroupSummaryText(groupName); using (var brush = new SolidBrush(Color.FromArgb(80, 80, 80))) using (var font = new Font(dgv.Font, FontStyle.Regular)) { var textRect = new Rectangle(e.CellBounds.X + 2, 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; // 阻止默认绘制 } }

🎨 绘制技巧:

  • 使用GDI+绘制自定义背景和文字
  • 通过Handled属性控制是否执行默认绘制
  • 合理使用using语句管理资源

方案五:高性能数据更新机制

在实时数据更新场景中,性能至关重要。我们实现了批量更新和精确更新机制:

C#
// 批量更新控制 public void BeginBatchUpdate() { isBatchUpdating = true; } public void EndBatchUpdate() { isBatchUpdating = false; RefreshGroupHeaders(); // 统一刷新分组标题 } // 🚀 优化的单元格更新方法 - 通过ID精确定位 public void UpdateCellValueById(int id, string columnName, object newValue) { if (dataGridView.DataSource is DataTable displayTable) { for (int i = 0; i < dataGridView.Rows.Count; i++) { DataRowView rowView = dataGridView.Rows[i].DataBoundItem as DataRowView; if (rowView != null && !Convert.ToBoolean(rowView["__IsGroupHeader"])) { if (Convert.ToInt32(rowView["ID"]) == id && dataGridView.Columns.Contains(columnName)) { var cell = dataGridView.Rows[i].Cells[columnName]; // 🎯 只在值确实改变时才更新,避免不必要的重绘 if (!cell.Value?.Equals(newValue) == true) { cell.Value = newValue; } break; } } } } } // 异步更新分组标题,避免阻塞主线程 private void RefreshGroupHeaders() { if (dataGridView.DataSource is DataTable) { this.BeginInvoke(new Action(() => { for (int i = 0; i < dataGridView.Rows.Count; i++) { DataRowView rowView = dataGridView.Rows[i].DataBoundItem as DataRowView; if (rowView != null && Convert.ToBoolean(rowView["__IsGroupHeader"])) { // 🔥 只重绘特定单元格,不是整行 int lastColumnIndex = dataGridView.Columns.Count - 5; if (lastColumnIndex >= 0) { dataGridView.InvalidateCell(lastColumnIndex, i); } } } })); } }

⚡ 性能优化要点:

  • 批量更新减少重绘次数
  • 精确更新单个单元格而非整行
  • 使用BeginInvoke异步更新UI
  • 值比较避免无意义的更新

🎪 实际应用示例:设备监控系统

下面是一个完整的应用示例,模拟工厂设备实时监控:

C#
public partial class Form1 : Form { private DataTable dataTable; private Timer salaryUpdateTimer; private Random random; private void InitializeSalaryUpdater() { random = new Random(); salaryUpdateTimer = new Timer(); salaryUpdateTimer.Interval = 500; // 500ms更新一次 salaryUpdateTimer.Tick += SalaryUpdateTimer_Tick; salaryUpdateTimer.Start(); } private void SalaryUpdateTimer_Tick(object sender, EventArgs e) { if (dataTable != null && dataTable.Rows.Count > 0) { // 🔥 开始批量更新 collapsibleGrid.BeginBatchUpdate(); try { foreach (DataRow row in dataTable.Rows) { // 模拟设备数据变化 decimal currentTemp = Convert.ToDecimal(row["温度"]); decimal tempChange = (decimal)(random.NextDouble() * 10 - 5); decimal newTemp = Math.Max(0, currentTemp + tempChange); row["温度"] = Math.Round(newTemp, 1); // 更新界面显示 int id = Convert.ToInt32(row["ID"]); collapsibleGrid.UpdateCellValueById(id, "温度", Math.Round(newTemp, 1)); } // 更新分组标题时间戳 var groupNames = collapsibleGrid.GetGroupNames(); foreach (string groupName in groupNames) { string timeText = DateTime.Now.ToString("HH:mm:ss"); collapsibleGrid.UpdateGroupCustomText(groupName, timeText); } } finally { // 🎯 结束批量更新,统一刷新 collapsibleGrid.EndBatchUpdate(); } } } }

完整代码

C#
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Reflection; 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; // 添加字典来存储每个分组的自定义文字 private Dictionary<string, string> groupCustomTexts; // 添加标志位来控制批量更新 private bool isBatchUpdating = false; public CollapsibleDataGridView() { InitializeComponent(); InitializeDataGridView(); groups = new List<GroupInfo>(); groupCustomTexts = new Dictionary<string, string>(); } 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; // 启用双缓冲以减少闪烁 typeof(DataGridView).InvokeMember("DoubleBuffered", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty, null, dataGridView, new object[] { true }); 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); // 初始化分组自定义文字为当前时间 if (!groupCustomTexts.ContainsKey(group.Key)) { groupCustomTexts[group.Key] = DateTime.Now.ToString("HH:mm:ss"); } } RefreshDisplay(); } // 刷新显示 private void RefreshDisplay() { if (originalDataTable == null) return; // 暂停绘制以减少闪烁 dataGridView.SuspendLayout(); try { 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(); } finally { dataGridView.ResumeLayout(); } } 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 if (e.ColumnIndex == dataGridView.Columns.Count - 5) { string groupName = rowView["__GroupName"].ToString(); e.Value = GetGroupSummaryText(groupName); 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 }); } } else if (e.ColumnIndex == dgv.Columns.Count - 5) { string groupName = rowView["__GroupName"].ToString(); string text = GetGroupSummaryText(groupName); using (var brush = new SolidBrush(Color.FromArgb(80, 80, 80))) using (var font = new Font(dgv.Font, FontStyle.Regular)) { var textRect = new Rectangle(e.CellBounds.X + 2, 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 string GetGroupSummaryText(string groupName) { return groupCustomTexts.ContainsKey(groupName) ? groupCustomTexts[groupName] : DateTime.Now.ToString("HH:mm:ss"); } // 批量更新开始 public void BeginBatchUpdate() { isBatchUpdating = true; } // 批量更新结束 public void EndBatchUpdate() { isBatchUpdating = false; RefreshGroupHeaders(); } // 添加更新分组自定义文字的方法 public void UpdateGroupCustomText(string groupName, string customText) { if (groupCustomTexts.ContainsKey(groupName)) { groupCustomTexts[groupName] = customText; // 只有不在批量更新模式时才立即刷新 if (!isBatchUpdating) { RefreshGroupHeaders(); } } } // 优化的刷新分组标题方法 private void RefreshGroupHeaders() { if (dataGridView.DataSource is DataTable) { // 使用BeginInvoke异步更新UI,避免阻塞 this.BeginInvoke(new Action(() => { for (int i = 0; i < dataGridView.Rows.Count; i++) { DataRowView rowView = dataGridView.Rows[i].DataBoundItem as DataRowView; if (rowView != null && Convert.ToBoolean(rowView["__IsGroupHeader"])) { // 只重绘特定的单元格而不是整行 int lastColumnIndex = dataGridView.Columns.Count - 5; if (lastColumnIndex >= 0 && lastColumnIndex < dataGridView.Columns.Count) { dataGridView.InvalidateCell(lastColumnIndex, i); } } } })); } } 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; } // 优化的单元格更新方法 public void UpdateCellValue(int originalRowIndex, string columnName, object newValue) { if (dataGridView.DataSource is DataTable displayTable) { int displayRowIndex = -1; int dataRowCount = 0; for (int i = 0; i < dataGridView.Rows.Count; i++) { DataRowView rowView = dataGridView.Rows[i].DataBoundItem as DataRowView; if (rowView != null && !Convert.ToBoolean(rowView["__IsGroupHeader"])) { if (dataRowCount == originalRowIndex) { displayRowIndex = i; break; } dataRowCount++; } } if (displayRowIndex >= 0 && dataGridView.Columns.Contains(columnName)) { // 直接更新单元格值,避免触发整行重绘 var cell = dataGridView.Rows[displayRowIndex].Cells[columnName]; if (!cell.Value?.Equals(newValue) == true) { cell.Value = newValue; } } } } public void UpdateCellValueById(int id, string columnName, object newValue) { if (dataGridView.DataSource is DataTable displayTable) { for (int i = 0; i < dataGridView.Rows.Count; i++) { DataRowView rowView = dataGridView.Rows[i].DataBoundItem as DataRowView; if (rowView != null && !Convert.ToBoolean(rowView["__IsGroupHeader"])) { if (Convert.ToInt32(rowView["ID"]) == id && dataGridView.Columns.Contains(columnName)) { var cell = dataGridView.Rows[i].Cells[columnName]; if (!cell.Value?.Equals(newValue) == true) { cell.Value = newValue; } break; } } } } } } 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; using System.Reflection; using Timer = System.Windows.Forms.Timer; namespace AppCollapsibleDataGrid { public partial class Form1 : Form { private DataTable dataTable; private Timer salaryUpdateTimer; private Random random; public Form1() { InitializeComponent(); SetupControls(); InitializeSalaryUpdater(); LoadSampleData(); } private void SetupControls() { this.Size = new Size(800, 600); this.Text = "可折叠DataGridView示例"; } private void InitializeSalaryUpdater() { random = new Random(); salaryUpdateTimer = new Timer(); salaryUpdateTimer.Interval = 500; // 增加到500毫秒,减少更新频率 salaryUpdateTimer.Tick += SalaryUpdateTimer_Tick; salaryUpdateTimer.Start(); } private void SalaryUpdateTimer_Tick(object sender, EventArgs e) { if (dataTable != null && dataTable.Rows.Count > 0) { // 开始批量更新以减少重绘次数 collapsibleGrid.BeginBatchUpdate(); try { // 更新设备监控数据 foreach (DataRow row in dataTable.Rows) { // 模拟温度变化 decimal currentTemp = Convert.ToDecimal(row["温度"]); decimal tempChange = (decimal)(random.NextDouble() * 10 - 5); // -5到+5度变化 decimal newTemp = Math.Max(0, currentTemp + tempChange); row["温度"] = Math.Round(newTemp, 1); // 模拟压力变化(只有运行中的设备才有压力) if (row["运行状态"].ToString() == "运行中") { decimal currentPressure = Convert.ToDecimal(row["压力"]); decimal pressureChange = (decimal)(random.NextDouble() * 2 - 1); // -1到+1变化 decimal newPressure = Math.Max(0, currentPressure + pressureChange); row["压力"] = Math.Round(newPressure, 1); } // 模拟电流变化 decimal currentCurrent = Convert.ToDecimal(row["电流"]); decimal currentChange = (decimal)(random.NextDouble() * 6 - 3); // -3到+3变化 decimal newCurrent = Math.Max(0, currentCurrent + currentChange); row["电流"] = Math.Round(newCurrent, 1); // 更新界面显示 int id = Convert.ToInt32(row["ID"]); collapsibleGrid.UpdateCellValueById(id, "温度", Math.Round(newTemp, 1)); if (row["运行状态"].ToString() == "运行中") { collapsibleGrid.UpdateCellValueById(id, "压力", Math.Round(Convert.ToDecimal(row["压力"]), 1)); } collapsibleGrid.UpdateCellValueById(id, "电流", Math.Round(newCurrent, 1)); } // 更新分组汇总文字为当前监控时间 var groupNames = collapsibleGrid.GetGroupNames(); foreach (string groupName in groupNames) { string timeText = DateTime.Now.ToString("HH:mm:ss"); collapsibleGrid.UpdateGroupCustomText(groupName, $"{timeText}"); } } finally { // 结束批量更新,统一刷新UI collapsibleGrid.EndBatchUpdate(); } } } private void LoadSampleData() { 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(string)); dataTable.Columns.Add("温度", typeof(decimal)); dataTable.Columns.Add("压力", typeof(decimal)); dataTable.Columns.Add("电流", typeof(decimal)); // 生产车间设备 dataTable.Rows.Add(1, "注塑机-001", "生产车间", "注塑设备", "运行中", 85.2, 12.5, 45.8); dataTable.Rows.Add(2, "注塑机-002", "生产车间", "注塑设备", "待机", 42.1, 0.0, 2.1); dataTable.Rows.Add(3, "冲压机-001", "生产车间", "冲压设备", "运行中", 78.9, 15.2, 52.3); dataTable.Rows.Add(4, "装配线-A", "生产车间", "装配设备", "运行中", 25.4, 6.8, 28.7); dataTable.Rows.Add(5, "质检台-001", "生产车间", "检测设备", "运行中", 22.1, 0.5, 15.2); // 加工车间设备 dataTable.Rows.Add(6, "数控机床-001", "加工车间", "机床设备", "运行中", 65.8, 8.9, 38.4); dataTable.Rows.Add(7, "数控机床-002", "加工车间", "机床设备", "维护中", 35.2, 0.0, 0.0); dataTable.Rows.Add(8, "磨床-001", "加工车间", "磨削设备", "运行中", 58.7, 5.6, 32.1); dataTable.Rows.Add(9, "车床-001", "加工车间", "车削设备", "运行中", 72.3, 7.2, 41.9); dataTable.Rows.Add(10, "铣床-001", "加工车间", "铣削设备", "待机", 28.9, 0.0, 3.5); // 包装车间设备 dataTable.Rows.Add(11, "包装机-001", "包装车间", "包装设备", "运行中", 32.4, 4.2, 22.8); dataTable.Rows.Add(12, "封箱机-001", "包装车间", "封装设备", "运行中", 29.7, 3.8, 18.5); dataTable.Rows.Add(13, "码垛机-001", "包装车间", "码垛设备", "故障", 45.6, 0.0, 0.0); dataTable.Rows.Add(14, "贴标机-001", "包装车间", "贴标设备", "运行中", 26.8, 2.1, 12.4); // 动力车间设备 dataTable.Rows.Add(15, "锅炉-001", "动力车间", "供热设备", "运行中", 285, 18.5, 125.6); dataTable.Rows.Add(16, "空压机-001", "动力车间", "压缩设备", "运行中", 68.9, 8.2, 78.4); dataTable.Rows.Add(17, "冷却塔-001", "动力车间", "冷却设备", "运行中", 35.2, 2.5, 45.2); dataTable.Rows.Add(18, "变压器-001", "动力车间", "电力设备", "运行中", 65.8, 0.0, 185.7); 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(); } protected override void OnFormClosing(FormClosingEventArgs e) { salaryUpdateTimer?.Stop(); salaryUpdateTimer?.Dispose(); base.OnFormClosing(e); } } }

image.png

🎯 常见问题与解决方案

问题一:大数据量时性能下降

解决方案:

  • 启用双缓冲减少闪烁
  • 使用批量更新机制
  • 精确更新单个单元格而非整行重绘

问题二:分组标题显示异常

解决方案:

  • 确保辅助列正确添加和隐藏
  • 在CellPainting事件中正确处理绘制逻辑
  • 使用e.Handled控制默认绘制行为

问题三:折叠展开操作不流畅

解决方案:

  • 使用SuspendLayout/ResumeLayout配对
  • 避免在展开折叠时重新查询数据
  • 利用现有分组信息快速重构显示表格

🏆 总结与最佳实践

通过这个可折叠DataGridView组件的实现,我们掌握了以下关键技术:

🔑 核心技术点:

  1. 自定义UserControl:封装复杂逻辑,提供简洁API
  2. 数据虚拟化:分离存储和显示,优化内存使用
  3. 自定义绘制:通过GDI+实现个性化界面效果

⚡ 性能优化经验:

  • 双缓冲 + 批量更新 = 流畅体验
  • 精确更新 + 异步刷新 = 高效响应
  • 资源管理 + 事件优化 = 稳定运行

这个组件不仅解决了数据分组展示的问题,更重要的是展现了C#在自定义控件开发中的强大能力。无论是企业级应用还是个人项目,都能从中获得启发。


💡 你在项目中遇到过哪些DataGridView相关的挑战? 欢迎在评论区分享你的经验,或者提出遇到的问题,我们一起探讨更好的解决方案!

🚀 觉得这个方案对你有帮助? 请转发给更多需要的同行,让更多开发者受益!


关注我们,获取更多C#实战技巧和最佳实践分享!

本文作者:技术老小子

本文链接:

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