简介:这是一份面向 WPF 开发者的树形表格(TreeGrid)实现示例,基于 C# 与 XAML 编写,适合需要在桌面端展示层级数据、又不想引入第三方商业控件的开发者参考。资源源自 GitHub 开源项目,作者对其进行了整理打包,可用于学习树状表格的布局、节点展开折叠与数据绑定等核心思路。压缩包共 36 个文件,约 66KB,其中以 12 个 cs 源码文件为主,配合 2 个 xaml 界面文件、csproj 工程文件及 dll、resources、resx 等资源,另有 exe、pdb 等编译产物,结构完整,可直接在 Visual Studio 中打开运行。内容涵盖主窗口、层级缩进转换器、程序集信息与资源配置等模块,便于读者理解树形表格从界面到逻辑的组织方式。目前已有 272 人学习下载,适合具备一定 WPF 基础、希望快速掌握树状表格实现技巧的开发者参考借鉴。
1. 从 treegrid.zip 说起:WPF 里那棵能编辑的树状表格到底怎么落地
手上拿到一个叫treegrid.zip的包,解压出来是 WPF 的 C# 工程,核心就一件事:把树形结构和表格合到一张控件里。普通DataGrid只能平铺行,TreeView只能展开节点,可业务里大量场景要的是「左边能折叠、右边能编辑」——比如物料 BOM、组织架构带编制人数、设备台账带实时状态。这就是 treeGrid 在 WPF 里的真实需求:一棵可展开的树,每一行还带多列数据,列能排序、能编辑、能绑定。
热搜里wpf、c#、treegrid反复出现,说明搜的人多半卡在同一个地方:WPF 原生没有 TreeGrid 控件,第三方库要么收费要么风格不搭,自己拼又不知道从哪下手。这篇就按我实际做过的路子,从控件选型讲到treegrid.zip这类工程怎么跑起来、参数怎么调、坑在哪。适合已经会写 WPF 窗口、能看懂 XAML 绑定,但没做过树状表格的 C# 开发者;新手照着步骤也能复现,熟手可以直接跳到参数和避坑那两章。
2. 选型先立住:WPF 做 treeGrid 的三条路和各自代价
动手之前先把路线定死,不然写到一半发现控件不支持编辑,返工成本很高。WPF 里实现树状表格,业内常见就三条路,我按「改动量 / 可控性 / 长期维护」三个维度拆开讲。
2.1 原生组合:TreeView 套 DataGrid 的模板方案
最常见的做法是把DataGrid的某一行模板里塞一个TreeView,或者反过来用TreeView的ItemTemplate里放一个横向排列的字段区。核心思路是让层级缩进和列对齐同时成立。
<!-- 用 HierarchicalDataTemplate 让 TreeView 的每一项右侧带多列 --> <TreeView ItemsSource="{Binding Roots}"> <TreeView.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Children}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="200"/> <ColumnDefinition Width="100"/> <ColumnDefinition Width="120"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="{Binding Name}"/> <TextBlock Grid.Column="1" Text="{Binding Code}"/> <TextBlock Grid.Column="2" Text="{Binding Status}"/> </Grid> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView>这段的关键在HierarchicalDataTemplate的ItemsSource指向子集合,Grid负责把一行拆成多列。逻辑上它确实能显示树状多列,但问题也明显:列宽靠手写死,没法像DataGrid那样拖拽调整、点表头排序;编辑要自己往模板里塞TextBox并处理提交。参数上Width写死是硬伤,实际项目里我会改成共享Grid.ColumnDefinitions或者干脆放弃这条路。
提示:这条路只适合列固定、不需要排序和编辑的展示型场景,一旦业务要「改一个单元格的值回写数据库」,维护量会迅速失控。
2.2 第三方控件:HandyControl、DevExpress 这类库的 TreeGrid
热搜里出现了wpf handycontrol,说明不少人已经在用国产开源控件库。HandyControl 本身偏基础控件美化,真正带 TreeGrid 的更多是 DevExpress、Telerik 这类商业库,或者开源里的TreeListView实现。选它的理由是省事:排序、编辑、虚拟化、拖拽列宽全都现成。
代价是学习成本和授权。商业库要钱,开源库文档参差。我一般评估三点:控件是否支持ObservableCollection的层级绑定、是否支持单元格级编辑回写、虚拟化在几千行时会不会卡。参数上重点看VirtualizingPanel.IsVirtualizing和VirtualizationMode,树状结构一旦节点多,不开虚拟化滚动就是灾难。
2.3 自绘 TreeGrid:继承 Control 或拼 ItemsControl
第三条路是自己写一个TreeGrid : Control,内部用ItemsControl递归渲染,配合GridViewRowPresenter做列对齐。这条路可控性最高,treegrid.zip这类工程多半就是这种自绘或半自绘方案。核心是把「层级」和「列」两套布局解耦:层级用缩进Margin控制,列用统一的列定义集合驱动。
// 列定义集合,供自绘 TreeGrid 统一渲染表头和单元格 public class TreeGridColumn { public string Header { get; set; } // 表头文字 public string BindingPath { get; set; } // 绑定到数据源的哪个属性 public double Width { get; set; } = 120;// 默认列宽 public bool IsEditable { get; set; } // 该列是否可编辑 }BindingPath决定这一列取数据对象的哪个字段,Width是默认宽度,IsEditable控制编辑开关。自绘方案的好处是列定义集中管理,改一处全表生效;坏处是排序、编辑、键盘导航这些都要自己补,工作量不小。我的经验是:列少、交互简单就自绘,列多、要排序编辑就上成熟控件,别硬扛。
3. 把 treegrid.zip 跑起来:环境、绑定和最小可运行工程
路线定了自绘或半自绘之后,接下来是让工程真正跑起来。这一章按「建工程 → 定义数据模型 → 绑定层级 → 渲染列」的顺序走,每步都给可抄的代码。
3.1 环境准备和工程结构
先确认环境:Visual Studio 2022、.NET 6 或 .NET Framework 4.8 都行,WPF 项目模板选「WPF 应用」。treegrid.zip这类包解压后一般有.sln、主工程、可能还有一个控件库工程。打开.sln先还原 NuGet 包,再编译。如果报找不到某个控件命名空间,多半是缺了对应的 NuGet 引用,按packages.config或.csproj里的PackageReference补上。
工程结构上我习惯分三层:Models放数据模型,Controls放自绘 TreeGrid,ViewModels放绑定逻辑。这样列定义、数据、渲染互不干扰,后面加列或改绑定不用动控件代码。
3.2 数据模型:带子集合的节点类
树状表格的数据模型必须能表达层级,最常见就是每个节点带一个Children集合。
public class TreeNode : INotifyPropertyChanged { public string Name { get; set; } // 节点名称,第一列 public string Code { get; set; } // 编码,第二列 private string _status; public string Status // 状态列,改值要通知 UI { get => _status; set { _status = value; OnPropertyChanged(nameof(Status)); } } public ObservableCollection<TreeNode> Children { get; set; } = new(); public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }Children用ObservableCollection是为了增删子节点时 UI 自动刷新。Status实现了INotifyPropertyChanged,因为它是可编辑列,改完要立刻反映到界面。如果所有列都只读,可以省掉通知逻辑;但只要有一列要编辑,就必须加,否则会出现「数据改了界面不动」的玄学问题。
3.3 层级绑定:HierarchicalDataTemplate 的正确写法
绑定的核心是让控件知道「子节点从哪来」。用HierarchicalDataTemplate时,ItemsSource指向Children,模板内容决定一行长什么样。
<Window.Resources> <HierarchicalDataTemplate DataType="{x:Type local:TreeNode}" ItemsSource="{Binding Children}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" Width="200"/> <TextBlock Text="{Binding Code}" Width="100"/> <TextBox Text="{Binding Status, UpdateSourceTrigger=PropertyChanged}" Width="120"/> </StackPanel> </HierarchicalDataTemplate> </Window.Resources>DataType指定这个模板作用于哪种类型,ItemsSource绑定子集合,StackPanel横向排列各列。UpdateSourceTrigger=PropertyChanged是关键参数:默认TextBox是失焦才回写,改成PropertyChanged后每敲一个字就更新数据源,编辑体验才跟得上。代价是频繁触发通知,数据量大时要注意性能。
3.4 列渲染:用共享列宽对齐表头和内容
自绘方案里最容易翻车的是表头和内容对不齐。解决办法是把列宽定义抽出来,表头和每一行都引用同一份。
// 共享列定义,表头和行都从这里取宽度 public static class ColumnDefs { public static readonly double[] Widths = { 200, 100, 120 }; }XAML 里表头和行模板都按这个数组设Width,改一处两边同步。如果列宽要支持拖拽,就得把Widths换成可通知的属性,并在拖拽结束时回写。这一步不做,用户拖了表头列宽、内容不跟着变,体验直接崩。
4. 参数与交互调优:编辑、排序、虚拟化三个必调点
工程能跑只是起点,真正决定好不好用的是编辑回写、排序和虚拟化这三块。这一章逐个拆参数和实现。
4.1 单元格编辑:从只读到可改的回写链路
可编辑列要打通「UI 输入 → 数据源 → 持久化」整条链路。绑定层用UpdateSourceTrigger=PropertyChanged保证实时回写,数据层用INotifyPropertyChanged保证界面刷新,持久化层在属性 setter 里或统一提交时落库。
public string Status { get => _status; set { if (_status == value) return; // 值没变不触发,避免无谓刷新 _status = value; OnPropertyChanged(nameof(Status)); MarkDirty(); // 标记该节点已修改,供批量保存 } }if (_status == value) return;这行是血泪经验:不加的话,某些绑定场景会形成「改值 → 通知 → 再改值」的循环,界面卡死。MarkDirty用来收集脏节点,保存时只提交改过的,比全量提交高效得多。
4.2 排序:点表头对树状结构排序的边界
平铺表格排序简单,树状表格排序麻烦在「排的是同级还是全树」。常见做法是只对同一父节点下的兄弟节点排序,保持层级不乱。
// 对某个节点的子集合按指定字段排序 public static void SortChildren(TreeNode node, string field, bool asc) { var sorted = asc ? node.Children.OrderBy(c => GetValue(c, field)).ToList() : node.Children.OrderByDescending(c => GetValue(c, field)).ToList(); node.Children.Clear(); foreach (var c in sorted) node.Children.Add(c); }GetValue用反射按字段名取值,OrderBy/OrderByDescending控制升降序。注意Clear再Add会触发多次集合变更通知,节点多时建议用支持批量更新的集合或先挂起通知。排序字段是字符串时,中文排序要指定StringComparer,否则按 Unicode 码点排,结果不符合直觉。
4.3 虚拟化:几千行不卡的开关和参数
树状表格行数一多,不开虚拟化滚动就卡。WPF 的虚拟化对平铺列表支持好,对树状结构要额外配置。
<TreeView VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" ScrollViewer.CanContentScroll="True">IsVirtualizing打开虚拟化,VirtualizationMode=Recycling复用容器而不是每次重建,滚动更顺。CanContentScroll=True让滚动按项而不是按像素,虚拟化才生效。这三个参数缺一个,虚拟化都可能不工作。注意树状结构展开全部节点时,虚拟化效果会打折,因为展开的节点都要参与布局,实际项目里我会限制默认展开层级。
5. 避坑与排查:treeGrid 在 WPF 里最容易翻车的五件事
这一章全是踩过的坑,按「现象 → 原因 → 解决」写,遇到对应症状直接对号入座。
5.1 展开节点后界面卡死几秒
现象:节点一多,点展开要等好几秒才响应。原因:Children集合在展开时一次性创建大量子对象,且没开虚拟化,布局一次性算完。解决:开虚拟化三参数,子集合改成懒加载——只在展开时才去查数据库或构造子节点,别在初始化时把整棵树建出来。
5.2 编辑单元格后数据没保存
现象:界面上改了值,切走再切回来变回原样。原因:绑定没设UpdateSourceTrigger=PropertyChanged,或者数据模型没实现INotifyPropertyChanged,又或者改了没调保存。解决:先确认绑定参数,再确认模型通知,最后确认脏数据有没有提交。三步逐一排查,别一上来就怀疑控件。
5.3 表头和内容列对不齐
现象:表头三列,内容行错位半列。原因:表头和行用了两套独立的列宽定义,或者行模板里混了Margin导致实际宽度偏移。解决:抽共享列定义,表头和行都引用同一份;检查行模板有没有多余的Padding、Margin,缩进用的Margin要算进第一列宽度里。
5.4 中文排序结果不对
现象:按名称排序,结果顺序看着乱。原因:默认字符串比较按 Unicode 码点,中文不适用。解决:排序时传StringComparer.CurrentCulture或StringComparer.Create(new CultureInfo("zh-CN"), false),让比较器按中文习惯排。
5.5 大量节点时内存持续上涨
现象:反复展开折叠,内存只涨不降。原因:每次展开都新建子对象,旧对象没释放;或者事件订阅没取消,节点被事件持有无法回收。解决:子节点缓存复用,别每次重建;节点销毁时取消事件订阅;用内存分析工具确认是不是绑定泄漏。
6. 进阶:把 treeGrid 做成可复用控件并验证它真的对
做到能跑、能编辑、不卡之后,下一步是把它抽成可复用控件,并且有一套验证方法确认它没坏。这一章讲封装技巧和验证手段。
封装的核心是把「列定义」和「数据源」做成依赖属性,让外部只传数据、不碰内部渲染。
public class TreeGridControl : Control { public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register(nameof(Columns), typeof(ObservableCollection<TreeGridColumn>), typeof(TreeGridControl), new PropertyMetadata(null)); public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(nameof(ItemsSource), typeof(IEnumerable), typeof(TreeGridControl), new PropertyMetadata(null)); public ObservableCollection<TreeGridColumn> Columns { get => (ObservableCollection<TreeGridColumn>)GetValue(ColumnsProperty); set => SetValue(ColumnsProperty, value); } public IEnumerable ItemsSource { get => (IEnumerable)GetValue(ItemsSourceProperty); set => SetValue(ItemsSourceProperty, value); } }Columns和ItemsSource做成依赖属性后,XAML 里就能像用原生控件一样绑定,外部完全不用关心内部怎么渲染。参数上PropertyMetadata可以带默认值和变更回调,列集合变了自动重绘。
验证方面,我一般做三层:单元测试测数据模型的排序和脏标记逻辑;UI 测试用自动化工具模拟展开、编辑、保存,确认回写正确;性能测试灌几千个节点,量展开耗时和内存。下面这张表是我常用的验证清单。
| 验证项 | 方法 | 通过标准 |
|---|---|---|
| 层级绑定 | 构造三层节点,检查展开 | 子节点正确显示 |
| 编辑回写 | 改单元格,读数据源 | 值同步更新 |
| 排序 | 点表头,检查同级顺序 | 顺序符合预期 |
| 虚拟化 | 灌 5000 节点滚动 | 无明显卡顿 |
| 内存 | 反复展开折叠 100 次 | 内存稳定不持续涨 |
最后说个我自己的习惯:每次改完控件,先拿一个只有三层、每层三个节点的小数据集跑一遍,确认基本交互没坏,再上大数据集压测。小数据集能快速定位逻辑错误,大数据集才暴露性能问题,顺序反了会浪费很多时间在性能调优上,结果发现是绑定写错了。这套 treeGrid 方案值不值得做,取决于你的业务是不是真的需要「树 + 表」同时成立——如果只是展示层级,TreeView 够了;如果只是平铺数据,DataGrid 够了;只有当两者都要,才值得投入把 treeGrid 做扎实。希望帮到你。
本文还有配套的精品资源,点击获取