简介这份资源面向WPF桌面开发初学者与需要美化树形控件的开发者针对原生TreeView缺少连接线、层级关系不够直观的问题通过自定义Style与ControlTemplate为TreeViewItem添加Line元素实现父子节点间的连接线效果。包内共39个文件以14个cs源码、2个xaml界面文件及baml、csproj、sln等工程文件为主另含exe、config、pdb等编译与配置产物压缩包约66KB可直接用Visual Studio打开运行。核心实现包括TreeViewItem模板中Line的坐标绑定、BoolToVisConverter将HasItems转换为可见性以及资源注册与应用样式的完整流程读者可据此掌握模板定制与转换器编写思路并在此基础上扩展折线连接、箭头指示等更复杂的样式。目前已有1489人学习下载适合作为WPF控件模板与数据绑定练习的参考案例。1. WPF 的 TreeView 默认没有连接线这事到底卡在哪WPF 的 TreeView 带连接线样式是很多做桌面端配置树、目录树、组织架构树的开发者绕不开的一个需求。默认的 TreeView 只给你一个缩进加一个三角箭头层级一深用户根本分不清哪个节点属于哪个父级。尤其是做参数配置面板、设备层级管理这类场景没有连接线的树形结构读起来就是一团糊。这个标题要解决的就是在不换控件、不引第三方库的前提下用纯 XAML 把 TreeView 改造成带连接线的样式并且能直接复用到项目里。适合谁看正在用 WPF 做桌面工具、后台管理客户端、工业配置软件的人。你不需要精通控件模板但至少要能看懂 ControlTemplate 和 ItemsPanel 的基本写法。下面从原理到代码一步步拆中间会给出可直接抄的模板和几个必调参数最后说几个我踩过的坑。2. 连接线到底画在哪TreeViewItem 模板结构拆解2.1 默认模板里有什么缺了什么WPF 的 TreeViewItem 默认模板结构大致是这样的一个 Grid里面分两列第一列放展开/折叠的 ToggleButton第二列放 Header 内容下面再挂一个 ItemsPresenter 用来承载子节点。默认模板里没有任何画线的元素所以你要加连接线本质上是在模板里插入 Border 或 Rectangle然后通过绑定控制它们的显示和隐藏。关键点在于连接线分两种一种是竖直方向的线连接父节点和子节点另一种是水平方向的线从竖线延伸到每个子节点的 Header 前面。这两条线要分别处理竖线画在 ItemsPresenter 的容器上横线画在每个 TreeViewItem 的模板里。还有一个容易忽略的地方最后一个子节点不应该继续往下画竖线否则树底部会多出一截悬空的线。这个判断需要用到 ItemsControl 的 AlternationCount 或者自定义附加属性来标记「是否是最后一个节点」。常见做法是用一个转换器比较当前项的索引和父级子项总数。2.2 用 ControlTemplate 重写 TreeViewItem 的最小结构下面是一个最小可用的 TreeViewItem 模板骨架重点看注释标出的三个位置竖线容器、横线、以及展开器。Style x:KeyConnectedTreeViewItemStyle TargetTypeTreeViewItem Setter PropertyTemplate Setter.Value ControlTemplate TargetTypeTreeViewItem Grid Grid.ColumnDefinitions ColumnDefinition Width19/ ColumnDefinition WidthAuto/ /Grid.ColumnDefinitions Grid.RowDefinitions RowDefinition HeightAuto/ RowDefinition HeightAuto/ /Grid.RowDefinitions !-- 竖线画在子节点容器的左侧随展开状态显示 -- Rectangle x:NameVerticalLine Grid.Row1 Grid.Column0 Width1 HorizontalAlignmentCenter Fill#C0C0C0 VisibilityCollapsed/ !-- 横线从竖线延伸到 Header 前面 -- Rectangle x:NameHorizontalLine Grid.Row0 Grid.Column0 Height1 Width9 HorizontalAlignmentRight VerticalAlignmentCenter Fill#C0C0C0/ !-- 展开/折叠按钮 -- ToggleButton x:NameExpander Grid.Row0 Grid.Column0 ClickModePress IsChecked{Binding IsExpanded, RelativeSource{RelativeSource TemplatedParent}} Width9 Height9 HorizontalAlignmentCenter VerticalAlignmentCenter/ !-- 节点内容 -- ContentPresenter x:NamePART_Header Grid.Row0 Grid.Column1 ContentSourceHeader Margin4,2,0,2/ !-- 子节点容器 -- ItemsPresenter x:NameItemsHost Grid.Row1 Grid.Column1/ /Grid ControlTemplate.Triggers Trigger PropertyIsExpanded ValueFalse Setter TargetNameItemsHost PropertyVisibility ValueCollapsed/ Setter TargetNameVerticalLine PropertyVisibility ValueCollapsed/ /Trigger Trigger PropertyIsExpanded ValueTrue Setter TargetNameVerticalLine PropertyVisibility ValueVisible/ /Trigger Trigger PropertyHasItems ValueFalse Setter TargetNameExpander PropertyVisibility ValueHidden/ Setter TargetNameHorizontalLine PropertyVisibility ValueHidden/ /Trigger /ControlTemplate.Triggers /ControlTemplate /Setter.Value /Setter /Style逻辑说明竖线放在第二行第一列高度自动撑满子节点区域只在展开时可见。横线放在第一行第一列宽度固定 9 像素右对齐正好从竖线位置延伸到 Header 左侧。展开器用 ToggleButton绑定 IsExpanded。HasItems 为 False 时隐藏展开器和横线避免叶子节点前面出现多余的线头。参数说明竖线的 Width 设为 1 是视觉上最舒服的粗细改成 2 会显得笨重。横线的 Width 要和第一列的 ColumnDefinition 宽度匹配这里 19 像素的列宽减去展开器 9 像素剩下 10 像素横线取 9 像素留 1 像素间隙。颜色 #C0C0C0 是中性灰深色主题下要换成 #555 左右。2.3 让最后一项不画竖线附加属性方案上面模板有个问题所有展开的节点都会画竖线包括最后一个子节点。最后一个子节点下面没有兄弟了竖线应该断掉。解决办法是给 TreeViewItem 加一个附加属性 IsLastItem在模板里通过 DataTrigger 控制竖线。public static class TreeViewItemHelper { public static readonly DependencyProperty IsLastItemProperty DependencyProperty.RegisterAttached( IsLastItem, typeof(bool), typeof(TreeViewItemHelper), new PropertyMetadata(false)); public static bool GetIsLastItem(DependencyObject obj) (bool)obj.GetValue(IsLastItemProperty); public static void SetIsLastItem(DependencyObject obj, bool value) obj.SetValue(IsLastItemProperty, value); }然后在 TreeViewItem 的 Style 里加一个 Setter通过 RelativeSource 找到父级 ItemsControl比较当前项和最后一项。更稳妥的做法是在 ItemContainerStyle 里用 Converterpublic class IsLastItemConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { // values[0] 当前 TreeViewItem // values[1] 父级 ItemsControl var item values[0] as TreeViewItem; var parent values[1] as ItemsControl; if (item null || parent null) return false; var index parent.Items.IndexOf(item.DataContext); return index parent.Items.Count - 1; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) throw new NotSupportedException(); }逻辑说明这个转换器接收当前项和父级容器返回当前项是否是最后一个。在模板里用 MultiBinding 把它绑到竖线的 Visibility 上最后一个项时把竖线隐藏。注意 Items.IndexOf 用的是 DataContext如果你的数据源是嵌套集合要确保 DataContext 类型一致。参数说明这个方案在数据量小于 500 个节点时性能没问题超过 1000 个节点建议改用 AlternationCount 方案避免每次布局都调 IndexOf。3. 从零搭一个可复用的带连接线 TreeView3.1 项目结构与资源字典组织我一般会把样式拆成三个文件放在 Themes 目录下TreeViewStyle.xaml 放 TreeView 本身的样式TreeViewItemStyle.xaml 放节点模板Converters.xaml 放转换器资源。然后在 App.xaml 里合并。!-- App.xaml -- Application.Resources ResourceDictionary ResourceDictionary.MergedDictionaries ResourceDictionary SourceThemes/Converters.xaml/ ResourceDictionary SourceThemes/TreeViewItemStyle.xaml/ ResourceDictionary SourceThemes/TreeViewStyle.xaml/ /ResourceDictionary.MergedDictionaries /ResourceDictionary /Application.Resources逻辑说明拆文件的目的是让样式可复用。Converters.xaml 里放 IsLastItemConverter 和 BoolToVisibilityConverter 的实例TreeViewItemStyle.xaml 里引用这些转换器。合并顺序不能乱Converters 必须在 TreeViewItemStyle 之前加载否则 XAML 解析时会找不到资源。参数说明如果你的项目有多个主题浅色/深色把颜色值抽成 DynamicResource在主题切换时统一替换。连接线颜色建议用 #D0D0D0 到 #A0A0A0 之间太浅看不清太深抢视觉焦点。3.2 TreeView 根样式与 ItemsPanel 调整TreeView 本身的样式改动不大主要是去掉默认边框、调整 ItemsPanel 的缩进。Style x:KeyConnectedTreeViewStyle TargetTypeTreeView Setter PropertyBorderThickness Value0/ Setter PropertyBackground ValueTransparent/ Setter PropertyItemContainerStyle Value{StaticResource ConnectedTreeViewItemStyle}/ Setter PropertyItemsPanel Setter.Value ItemsPanelTemplate StackPanel IsItemsHostTrue/ /ItemsPanelTemplate /Setter.Value /Setter /Style逻辑说明ItemContainerStyle 把节点模板挂上去ItemsPanel 用 StackPanel 保证子节点垂直排列。默认的 TreeView 也是 StackPanel这里显式写出来是为了后续如果要改成 VirtualizingStackPanel 做虚拟化时有个明确的修改点。参数说明BorderThickness 设为 0 是因为连接线本身已经提供了视觉边界再加边框会显得冗余。如果你的 TreeView 放在一个带边框的容器里Background 设为 Transparent 让容器背景透出来。3.3 数据模板与层级绑定数据层用一个简单的递归模型public class TreeNode : INotifyPropertyChanged { public string Name { get; set; } public ObservableCollectionTreeNode Children { get; set; } new ObservableCollectionTreeNode(); public bool IsExpanded { get; set; } public event PropertyChangedEventHandler PropertyChanged; }XAML 里用 HierarchicalDataTemplate 绑定TreeView ItemsSource{Binding RootNodes} Style{StaticResource ConnectedTreeViewStyle} TreeView.ItemTemplate HierarchicalDataTemplate ItemsSource{Binding Children} TextBlock Text{Binding Name} Padding2,1/ /HierarchicalDataTemplate /TreeView.ItemTemplate /TreeView逻辑说明HierarchicalDataTemplate 的 ItemsSource 指向 ChildrenTreeView 会自动递归生成子节点。每个子节点都会应用 ItemContainerStyle也就是带连接线的模板。IsExpanded 绑定到 TreeViewItem 的 IsExpanded 属性实现展开状态的双向同步。参数说明Padding 设为 2,1 是让文字和连接线之间留一点呼吸空间。如果你的节点前面要加图标在 TextBlock 前面放一个 Image宽度控制在 16 像素以内否则会把 Header 推得太远。4. 连接线样式的避坑与常见问题排查4.1 线画出来了但错位列宽和 Grid 对齐问题现象竖线和横线接不上中间差几个像素或者横线跑到 Header 文字上面去了。原因TreeViewItem 模板里第一列的宽度和横线的 Width 没有对齐。默认模板的第一列宽度是 19但如果你改了展开器大小或者加了 Margin这个值就变了。另外 Grid 的 HorizontalAlignment 如果设成 Stretch横线的实际位置会偏移。解决把第一列宽度、展开器宽度、横线宽度三者固定成一组值。我一般用 19 / 9 / 9 这组横线右对齐展开器居中。改任何一个都要同步改另外两个。调试时给横线加一个红色背景肉眼确认位置后再改回灰色。4.2 展开折叠时线闪烁或残留现象快速点击展开折叠竖线有时候不消失或者闪一下才出来。原因ControlTemplate.Triggers 里对 Visibility 的切换和布局更新不在同一帧。WPF 的布局是异步的Trigger 改了 Visibility 之后ItemsPresenter 的高度变化需要一次布局传递才能生效。解决把竖线的 Visibility 绑定改成绑定到 ItemsHost 的实际高度用一个 Converter 判断高度是否大于 0。或者更简单在 Trigger 里同时设置 ItemsHost 的 Visibility 和竖线的 Visibility确保两者同步。如果还有闪烁给 TreeViewItem 加一个 ClipToBoundsTrue防止线画到容器外面。4.3 深色主题下连接线看不见现象浅色主题正常切到深色主题后连接线消失了。原因连接线颜色写死在模板里没有跟随主题变化。解决把 Fill 改成 DynamicResource在主题字典里定义 TreeViewLineBrush。浅色主题用 #C0C0C0深色主题用 #555555。注意 DynamicResource 在 ControlTemplate 里也能用但资源键必须在应用级别或窗口级别能找到。4.4 节点多了之后滚动卡顿现象树超过 500 个节点滚动时明显掉帧。原因StackPanel 不做虚拟化所有节点一次性生成。每个节点都有 Grid、Rectangle、ToggleButton视觉树很重。解决把 ItemsPanel 换成 VirtualizingStackPanel同时设置 ScrollViewer.CanContentScrollTrue 和 VirtualizingStackPanel.IsVirtualizingTrue。但虚拟化后连接线的 IsLastItem 判断会失效因为容器会被复用。这时候要改用 AlternationCount 方案或者接受虚拟化后连接线在滚动时偶尔错位用 ItemsControl.ItemContainerGenerator 在容器生成时重新计算。4.5 设计器里看不到效果现象Visual Studio 设计器里 TreeView 是空的连接线样式不显示。原因设计器不执行数据绑定ItemsSource 为空时不会生成任何 TreeViewItem。解决在 XAML 里加 d:DataContext 和示例数据或者用 d:DesignInstance 指定设计时数据源。更省事的办法是直接在 XAML 里写几个静态的 TreeViewItem 作为设计时预览运行时再换成数据绑定。5. 进阶把连接线做成可配置的样式参数到这一步基本功能已经跑通了。但实际项目里不同模块对连接线的要求不一样有的要虚线有的要加粗有的要只在特定层级显示。我一般会把连接线的样式抽成几个依赖属性挂在 TreeView 上通过 TemplateBinding 传到 TreeViewItem 模板里。具体做法是给 TreeView 加三个附加属性LineBrush、LineThickness、LineStyle。然后在 TreeViewItem 模板里用 RelativeSource 找到 TreeView绑定这三个属性。这样同一个项目里不同 TreeView 可以用不同的连接线样式不用复制多套模板。Rectangle x:NameVerticalLine Width{Binding (local:TreeViewHelper.LineThickness), RelativeSource{RelativeSource AncestorTypeTreeView}} Fill{Binding (local:TreeViewHelper.LineBrush), RelativeSource{RelativeSource AncestorTypeTreeView}}/参数说明LineThickness 默认 1虚线用 StrokeDashArray 实现但 Rectangle 不支持虚线要换成 Line 或者 Path。LineBrush 默认 #C0C0C0深色主题下通过 DynamicResource 覆盖。验证方法写一个简单的测试窗口放三个 TreeView分别设置不同的 LineBrush 和 LineThickness确认样式互不干扰。然后展开到第四层检查最后一项的竖线是否正确断开。最后切换主题确认颜色跟随变化。我自己的习惯是每次改完模板先把窗口拉窄到 300 像素看看连接线会不会被裁掉再把节点全部展开滚动到底部确认没有残留的线头。这两个动作能抓到大部分布局问题。希望帮到你。本文还有配套的精品资源点击获取