LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

C#实现可折叠DataGridView:让数据展示更智能,支持动态实时刷新!

admin
2025年7月1日 9:20 本文热度 44

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

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

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

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

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

2. 缺乏分组功能

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

3. 交互性不足

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

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

🎯 核心设计思路

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

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

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

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

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<stringstring> groupCustomTexts;
    // 批量更新控制标志
    privatebool isBatchUpdating = false;

    public CollapsibleDataGridView()
    
{
        InitializeComponent();
        InitializeDataGridView();
        groups = new List<GroupInfo>();
        groupCustomTexts = new Dictionary<stringstring>();
    }
}

💡 设计要点:

  • 使用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 < 0return;

    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(230235245)))
        {
            e.Graphics.FillRectangle(brush, e.CellBounds);
        }

        // 绘制边框
        using (var pen = new Pen(Color.FromArgb(200200200)))
        {
            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(505050)))
            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(808080)))
            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<stringstring> groupCustomTexts;

        // 添加标志位来控制批量更新
        privatebool isBatchUpdating = false;

        public CollapsibleDataGridView()
        
{
            InitializeComponent();
            InitializeDataGridView();
            groups = new List<GroupInfo>();
            groupCustomTexts = new Dictionary<stringstring>();
        }

        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(240240240);
                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(230235245)))
                {
                    e.Graphics.FillRectangle(brush, e.CellBounds);
                }

                using (var pen = new Pen(Color.FromArgb(200200200)))
                {
                    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(505050)))
                    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(808080)))
                    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(800600);
            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.212.545.8);
            dataTable.Rows.Add(2"注塑机-002""生产车间""注塑设备""待机"42.10.02.1);
            dataTable.Rows.Add(3"冲压机-001""生产车间""冲压设备""运行中"78.915.252.3);
            dataTable.Rows.Add(4"装配线-A""生产车间""装配设备""运行中"25.46.828.7);
            dataTable.Rows.Add(5"质检台-001""生产车间""检测设备""运行中"22.10.515.2);

            // 加工车间设备
            dataTable.Rows.Add(6"数控机床-001""加工车间""机床设备""运行中"65.88.938.4);
            dataTable.Rows.Add(7"数控机床-002""加工车间""机床设备""维护中"35.20.00.0);
            dataTable.Rows.Add(8"磨床-001""加工车间""磨削设备""运行中"58.75.632.1);
            dataTable.Rows.Add(9"车床-001""加工车间""车削设备""运行中"72.37.241.9);
            dataTable.Rows.Add(10"铣床-001""加工车间""铣削设备""待机"28.90.03.5);

            // 包装车间设备
            dataTable.Rows.Add(11"包装机-001""包装车间""包装设备""运行中"32.44.222.8);
            dataTable.Rows.Add(12"封箱机-001""包装车间""封装设备""运行中"29.73.818.5);
            dataTable.Rows.Add(13"码垛机-001""包装车间""码垛设备""故障"45.60.00.0);
            dataTable.Rows.Add(14"贴标机-001""包装车间""贴标设备""运行中"26.82.112.4);

            // 动力车间设备
            dataTable.Rows.Add(15"锅炉-001""动力车间""供热设备""运行中"28518.5125.6);
            dataTable.Rows.Add(16"空压机-001""动力车间""压缩设备""运行中"68.98.278.4);
            dataTable.Rows.Add(17"冷却塔-001""动力车间""冷却设备""运行中"35.22.545.2);
            dataTable.Rows.Add(18"变压器-001""动力车间""电力设备""运行中"65.80.0185.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组件的实现,我们掌握了以下关键技术:

🔑 核心技术点:

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

⚡ 性能优化经验:

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

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


阅读原文:https://mp.weixin.qq.com/s/d5L8ilGW-AxBaHFrfTcRhg


该文章在 2025/7/1 9:20:34 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved