这个问题一个网友提出的,就前一个版本的gridview 做了一些改进,像在一个工厂设备监控项目中,我们需要按车间对设备数据进行分组展示,经过深入研究,成功实现了一个高性能的可折叠DataGridView组件。今天就来分享这个实战经验,帮你轻松解决数据分组展示的难题!
🔍 问题分析:传统DataGridView的局限性
在企业级应用开发中,我们经常面临以下挑战:
1. 数据量庞大,用户体验差
- 成千上万条数据一次性展示,页面卡顿
- 用户难以快速定位所需信息
2. 缺乏分组功能
- 无法按业务逻辑对数据进行分类
- 相关数据分散,查看不便
3. 交互性不足
- 用户无法根据需要隐藏不关心的数据
- 缺乏现代化的用户界面体验
💡 解决方案:自定义可折叠DataGridView
🎯 核心设计思路
我们的解决方案包含以下几个关键特性:
- 分组数据管理
- 可视化分组标题
- 折叠展开功能
- 实时数据更新(主要增加了这个功能)
- 自定义扩展
🔥 方案一:核心组件架构设计
首先,我们定义一个分组信息类来管理每个分组的状态:
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控件:
public partial class CollapsibleDataGridView : UserControl
{
private DataGridView dataGridView;
private List<GroupInfo> groups;
private DataTable originalDataTable;
privatestring groupColumnName;
privatebool showGroupHeaders = true;
privateconstint GROUP_HEADER_HEIGHT = 25;
// 存储每个分组的自定义文字
private Dictionary<string, string> groupCustomTexts;
// 批量更新控制标志
privatebool isBatchUpdating = false;
public CollapsibleDataGridView()
{
InitializeComponent();
InitializeDataGridView();
groups = new List<GroupInfo>();
groupCustomTexts = new Dictionary<string, string>();
}
}
💡 设计要点:
- 使用UserControl封装,便于复用
- 分离数据存储(originalDataTable)和显示逻辑
- 引入批量更新机制,避免频繁重绘造成的性能问题
🎨 方案二:DataGridView初始化与优化
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);
}
🎯 优化亮点:
- 通过反射启用双缓冲,显著减少界面闪烁
- 禁用不必要的功能,专注于数据展示
- 统一的样式设置,保证界面一致性
📊 方案三:数据分组与显示逻辑
这是整个组件的核心逻辑,负责将原始数据按分组重新组织:
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 配对使用,避免多次重绘
- 根据展开状态动态添加数据行
🎨 方案四:自定义绘制分组标题
为了实现美观的分组标题效果,我们需要自定义单元格绘制:
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 });
}
}
// 🎨 在最后一列显示自定义文字(如时间戳)
elseif (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语句管理资源
⚡ 方案五:高性能数据更新机制
在实时数据更新场景中,性能至关重要。我们实现了批量更新和精确更新机制:
// 批量更新控制
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
- 值比较避免无意义的更新
🎪 实际应用示例:设备监控系统
下面是一个完整的应用示例,模拟工厂设备实时监控:
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();
}
}
}
}
完整代码
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;
privatestring groupColumnName;
privatebool showGroupHeaders = true;
privateconstint GROUP_HEADER_HEIGHT = 25;
// 添加字典来存储每个分组的自定义文字
private Dictionary<string, string> groupCustomTexts;
// 添加标志位来控制批量更新
privatebool 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("获取或设置用于分组的列名")]
publicstring GroupColumn
{
get { return groupColumnName; }
set
{
groupColumnName = value;
if (originalDataTable != null)
{
RefreshGroups();
}
}
}
[Category("Collapsible")]
[Description("获取或设置是否显示分组标题")]
publicbool 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;
}
elseif (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 });
}
}
elseif (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
{
publicstring GroupName { get; set; }
publicbool IsExpanded { get; set; }
public List<DataRow> Rows { get; set; }
public GroupInfo()
{
Rows = new List<DataRow>();
}
}
}
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);
}
}
}

🎯 常见问题与解决方案
❓ 问题一:大数据量时性能下降
解决方案:
- 启用双缓冲减少闪烁
- 使用批量更新机制
- 精确更新单个单元格而非整行重绘
❓ 问题二:分组标题显示异常
解决方案:
- 确保辅助列正确添加和隐藏
- 在CellPainting事件中正确处理绘制逻辑
- 使用e.Handled控制默认绘制行为
❓ 问题三:折叠展开操作不流畅
解决方案:
- 使用SuspendLayout/ResumeLayout配对
- 避免在展开折叠时重新查询数据
- 利用现有分组信息快速重构显示表格
🏆 总结与最佳实践
通过这个可折叠DataGridView组件的实现,我们掌握了以下关键技术:
🔑 核心技术点:
- 自定义UserControl
- 数据虚拟化
- 自定义绘制
⚡ 性能优化经验:
- 双缓冲 + 批量更新 = 流畅体验
- 精确更新 + 异步刷新 = 高效响应
- 资源管理 + 事件优化 = 稳定运行
这个组件不仅解决了数据分组展示的问题,更重要的是展现了C#在自定义控件开发中的强大能力。无论是企业级应用还是个人项目,都能从中获得启发。
阅读原文:https://mp.weixin.qq.com/s/d5L8ilGW-AxBaHFrfTcRhg
该文章在 2025/7/1 9:20:34 编辑过