☰
C# WinForms 树形表格 TreeDataGridView 实现与二次开发指南
2026/10/19 12:19:03 网站建设 项目流程

简介:这份C#树型TreeDataGridView例子Demo代码面向有一定WinForm基础的C#开发者,解决层次化数据既要树形展开折叠、又要在节点下以表格形式展示多行字段的界面需求。由于TreeGridView并非.NET标准控件,示例通过自定义UserControl组合TreeView与DataGridView,借助AfterSelect事件联动刷新数据,并演示数据绑定、节点展开折叠状态保存、键盘导航及动态列生成等实现思路。压缩包共35个文件,约157KB,包含vbproj与sln工程文件、Form1.Designer.vb与Form1.vb等核心源码、exe可执行程序、dll与pdb调试文件、resx与resources资源文件,以及程序界面截图,方便直接运行对照学习。目前已有897人学习下载,适合需要快速掌握树形表格控件组合技巧、参考事件处理与数据绑定写法的开发者借鉴。

1. 树形表格到底能解决什么:从一份 C# 桌面端 Demo 说起

如果你做过 WinForms 的资产管理、组织架构或者 BOM 物料清单,大概率遇到过这种需求:数据天然是父子层级,但用户又希望它像 Excel 一样能排序、能编辑、能展开折叠。普通DataGridView是平的,TreeView又不能多列编辑,两个控件拼在一起同步滚动条,写过的都知道那是玄学。这份 C# 树型 TreeDataGridView 例子 Demo 代码,干的就是把「树」和「表格」缝成一个控件:父节点可以折叠展开,每一行又保留多列显示和单元格编辑能力。它适合两类人——一类是刚接触 WinForms 自定义控件、想找一个能跑起来的完整例子照着改的新手;另一类是老手,想直接拿这套节点渲染和展开逻辑,塞进自己项目里省掉重复造轮子的时间。下面我按「它是什么 → 怎么跑起来 → 核心怎么实现 → 怎么改」的顺序拆一遍。

2. 项目结构与运行环境:先把 Demo 跑起来再谈改造

2.1 目录结构与文件职责

这类 Demo 通常是一个标准的 WinForms 解决方案,结构不复杂,但每个文件都有明确分工。拿到源码后先别急着 F5,花两分钟认清谁是谁,后面改起来才不迷路。

TreeDataGridViewDemo/ ├── TreeDataGridViewDemo.sln # 解决方案文件 ├── TreeDataGridViewDemo/ │ ├── Program.cs # 程序入口,Application.Run 主窗体 │ ├── MainForm.cs # 主窗体,放 TreeDataGridView 和测试按钮 │ ├── MainForm.Designer.cs # 设计器生成的控件布局代码 │ ├── Controls/ │ │ ├── TreeDataGridView.cs # 核心自定义控件,继承 DataGridView │ │ └── TreeGridNode.cs # 节点数据模型,维护父子关系 │ ├── Models/ │ │ └── DeptInfo.cs # 示例业务实体(部门/物料等) │ └── Properties/ │ └── AssemblyInfo.cs # 程序集信息

TreeDataGridView.cs是整个项目的灵魂,它继承自DataGridView,重写了绘制和鼠标事件;TreeGridNode.cs负责节点树的数据结构,记录Parent、Children、Level和展开状态。业务实体DeptInfo只是用来演示数据绑定的,换成你自己的模型即可。

2.2 运行环境与首次编译

环境要求不高,但版本别踩坑。用 Visual Studio 2019 或 2022 打开.sln,目标框架一般是 .NET Framework 4.6 以上,或者 .NET 6/8 的 Windows 桌面工作负载。如果你机器上只装了 .NET Core 的跨平台 SDK,是跑不了 WinForms 的,得确认装了「.NET 桌面开发」工作负载。

# 命令行方式还原并编译(在解决方案根目录执行) dotnet restore TreeDataGridViewDemo.sln dotnet build TreeDataGridViewDemo.sln -c Debug # 编译成功后运行 dotnet run --project TreeDataGridViewDemo/TreeDataGridViewDemo.csproj

restore负责拉取 NuGet 依赖,这个 Demo 一般零第三方依赖,所以很快;build的-c Debug表示调试配置,方便打断点;run直接启动主窗体。如果编译报「找不到 Windows 桌面 SDK」,就是工作负载没装全,去 Visual Studio Installer 里补上即可。

2.3 主窗体里怎么挂载控件

跑起来之后,你会看到主窗体上有一个树形表格和几个测试按钮。挂载方式很朴素,就是在MainForm里 new 一个控件、设好列、绑数据。

// MainForm.cs 片段 private TreeDataGridView treeGrid; private void InitTreeGrid() { treeGrid = new TreeDataGridView(); treeGrid.Dock = DockStyle.Fill; // 铺满窗体 treeGrid.AllowUserToAddRows = false; // 树形结构不允许用户直接加空行 treeGrid.RowHeadersVisible = false; // 行头隐藏,展开图标画在第一列 treeGrid.Columns.Add("Name", "名称"); // 第一列承载树形缩进和展开图标 treeGrid.Columns.Add("Code", "编号"); treeGrid.Columns.Add("Count", "数量"); this.Controls.Add(treeGrid); }

Dock = Fill让控件自适应窗体大小;AllowUserToAddRows = false很关键,树形数据的新增必须走节点逻辑,不能让用户随手敲一个没有父级的空行;RowHeadersVisible = false是因为展开折叠图标直接画在第一列文字前面,不需要额外的行头列占位。列的顺序也有讲究,第一列必须是树列,缩进和图标都基于它计算。

3. 核心实现拆解:节点模型与自定义绘制怎么配合

3.1 TreeGridNode 节点模型设计

树形表格能不能撑住,全看节点模型设计得干不干净。核心就三件事:记住爹是谁、记住孩子有谁、记住自己展开没展开。

// TreeGridNode.cs public class TreeGridNode { public object Data { get; set; } // 绑定的业务实体 public TreeGridNode Parent { get; private set; } public List<TreeGridNode> Children { get; } = new List<TreeGridNode>(); public int Level { get; private set; } // 层级,根节点为 0 public bool IsExpanded { get; set; } = true; // 默认展开 public TreeGridNode(object data) { Data = data; } public void AddChild(TreeGridNode child) { child.Parent = this; child.Level = this.Level + 1; // 层级随父节点递增 Children.Add(child); } public bool HasChildren => Children.Count > 0; }

Level是缩进计算的依据,每深一层多缩进一个固定像素;IsExpanded控制子节点是否参与渲染;AddChild里顺手把Parent和Level都设好,避免调用方忘记赋值导致缩进错乱。这里用List<TreeGridNode>而不是数组,是因为增删子节点在业务里很常见,列表更省心。

3.2 把节点树拍平成可渲染的行

DataGridView只认行,不认树。所以每次展开折叠或者数据变化后,都要把节点树按当前展开状态「拍平」成一个线性列表,再喂给控件。

// TreeDataGridView.cs 片段 private void RebuildRows() { Rows.Clear(); // 清空现有行 foreach (var root in _rootNodes) AppendNode(root); // 递归追加 } private void AppendNode(TreeGridNode node) { int rowIndex = Rows.Add(); // 新增一行 var row = Rows[rowIndex]; row.Tag = node; // 把节点挂到行上,方便反查 row.Cells[0].Value = node.Data; // 第一列存节点数据 if (node.IsExpanded) // 只有展开才继续往下拍 foreach (var child in node.Children) AppendNode(child); }

Rows.Add()返回新行索引,row.Tag = node是这套方案的关键——行和节点通过Tag双向绑定,点击行时能立刻拿到对应节点。递归顺序决定了显示顺序,先父后子,天然就是树的先序遍历。注意Rows.Clear()会触发重绘,数据量大时建议用SuspendLayout/ResumeLayout包一层,减少闪烁。

3.3 展开折叠图标的绘制

图标不是贴图,是画出来的。重写CellPainting事件,在第一列文字前面画一个加号或减号,省去维护图片资源。

// TreeDataGridView.cs 片段 private void OnCellPainting(object sender, DataGridViewCellPaintingEventArgs e) { if (e.ColumnIndex != 0 || e.RowIndex < 0) return; // 只处理第一列 var node = Rows[e.RowIndex].Tag as TreeGridNode; if (node == null) return; e.PaintBackground(e.CellBounds, true); // 先画背景 int indent = node.Level * 16 + 4; // 层级缩进 var iconRect = new Rectangle( e.CellBounds.Left + indent, e.CellBounds.Top + (e.CellBounds.Height - 9) / 2, 9, 9); if (node.HasChildren) // 有子节点才画图标 { using (var pen = new Pen(Color.Gray)) { e.Graphics.DrawRectangle(pen, iconRect); // 方框 e.Graphics.DrawLine(pen, iconRect.Left + 2, iconRect.Top + 4, iconRect.Right - 2, iconRect.Top + 4); // 横线 if (!node.IsExpanded) // 折叠时补一竖线成加号 e.Graphics.DrawLine(pen, iconRect.Left + 4, iconRect.Top + 2, iconRect.Left + 4, iconRect.Bottom - 2); } } var textRect = new Rectangle( e.CellBounds.Left + indent + 14, e.CellBounds.Top, e.CellBounds.Width - indent - 14, e.CellBounds.Height); TextRenderer.DrawText(e.Graphics, node.Data.ToString(), e.CellStyle.Font, textRect, e.CellStyle.ForeColor, TextFormatFlags.VerticalCenter); e.Handled = true; // 告诉控件这格我自己画 }

indent按Level递增,视觉上就有了层级感;iconRect居中计算保证图标不歪;e.Handled = true必须设,否则控件会再画一遍默认内容,文字会重影。这段是整套控件的门面,改颜色、改图标大小、改缩进像素都在这里动手。

3.4 鼠标点击命中判断

图标画出来了,还得知道用户点没点中它。在CellMouseDown里算一下点击坐标是否落在图标矩形内。

// TreeDataGridView.cs 片段 private void OnCellMouseDown(object sender, DataGridViewCellMouseEventArgs e) { if (e.ColumnIndex != 0 || e.RowIndex < 0) return; var node = Rows[e.RowIndex].Tag as TreeGridNode; if (node == null || !node.HasChildren) return; int indent = node.Level * 16 + 4; var iconRect = new Rectangle(indent, 0, 9, 9); // 只比较水平位置 if (e.X >= iconRect.Left && e.X <= iconRect.Right) { node.IsExpanded = !node.IsExpanded; // 翻转展开状态 RebuildRows(); // 重建行列表 } }

这里只比较水平坐标,是因为垂直方向整行都是可点区域,没必要卡死。RebuildRows()会按新的展开状态重新拍平,界面立刻刷新。如果数据量大,可以只重建受影响的那一段,但 Demo 阶段全量重建足够清晰。

4. 避坑与常见问题:这几个坑我替你踩过了

4.1 展开后行错位、数据对不上

现象:展开一个父节点后,点击某行拿到的数据是别的节点的。原因:RebuildRows之后行索引变了,但外部还缓存着旧索引。解决:永远通过Rows[i].Tag反查节点,不要用行号做业务标识;如果外部必须记索引,重建后重新映射一遍。

4.2 图标闪烁、界面卡顿

现象:节点多的时候展开折叠明显闪屏。原因:每次Rows.Clear()和Rows.Add()都触发重绘。解决:用SuspendLayout()和ResumeLayout()把重建过程包起来,或者干脆用双缓冲,把控件DoubleBuffered属性通过反射设为 true。

4.3 单元格编辑后节点数据没更新

现象:用户改了第一列文字,但node.Data还是旧值。原因:DataGridView编辑的是单元格,不会自动回写Tag里的对象。解决:监听CellEndEdit事件,把e.Value写回节点绑定的实体,再决定要不要刷新显示。

4.4 递归太深导致栈溢出

现象:层级超过几百层时程序崩溃。原因:AppendNode是递归实现,深度过大爆栈。解决:改成用栈或队列的迭代遍历,或者限制业务上的最大层级,一般组织架构不会超过十层,但物料 BOM 有可能很深,得留个心眼。

4.5 高 DPI 下缩进和图标错位

现象:在 125% 或 150% 缩放的屏幕上,图标和文字挤在一起。原因:缩进像素写死了,没按 DPI 缩放。解决:用Graphics.DpiX / 96f算一个缩放系数,缩进和图标尺寸都乘上它,或者干脆用AutoScaleMode = Dpi让窗体统一处理。

5. 二次开发改造:从 Demo 到能进项目的控件

5.1 数据绑定换成真实业务模型

Demo 里第一列直接存node.Data,实际项目里通常要绑多个字段。改造思路是给TreeGridNode加一个Dictionary<string, object>或者泛型T,让每个节点持有完整实体,拍平的时候按列取值。

// 泛型化改造 public class TreeGridNode<T> where T : class { public T Data { get; set; } public List<TreeGridNode<T>> Children { get; } = new(); // ... 其余同上 } // 拍平时按列映射 row.Cells["Name"].Value = node.Data.GetType().GetProperty("Name")?.GetValue(node.Data); row.Cells["Code"].Value = node.Data.GetType().GetProperty("Code")?.GetValue(node.Data);

用反射取值虽然方便,但高频刷新时性能一般,我一般会加一层缓存或者让业务实体实现一个ITreeGridRow接口,直接返回列值数组,省掉反射开销。

5.2 加复选框和懒加载

树形表格经常要支持勾选父节点联动子节点。做法是在第一列前面再插一个DataGridViewCheckBoxColumn,然后在CellValueChanged里递归设置子节点勾选状态。懒加载则是把AppendNode里的递归改成「只展开一层,子节点在首次展开时才去数据库查」,适合节点数量上万的场景。

改造点改动位置难度
多字段绑定TreeGridNode 泛型化低
复选框联动CellValueChanged 事件中
懒加载AppendNode 递归改按需查询中
拖拽排序MouseDown/MouseMove 事件高

5.3 一个验证改造是否成功的小技巧

改完别急着往项目里塞,先在 Demo 里造一批极端数据:单层一千个节点、层级二十层、节点名超长。跑一遍展开折叠和编辑,看有没有卡顿、错位、数据串行。我自己的习惯是每次动完绘制逻辑,都强制走一遍「全展开 → 全折叠 → 编辑首列 → 再展开」这条链路,因为绘制和数据的耦合问题往往就藏在这几步的切换里。从那以后我每次改自定义控件,都先把这条链路跑通再提交,省得回头在业务代码里排查半天。希望这份拆解帮到你,源码拿下去直接跑,改起来心里有底。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询