如何使WPF的TreeView节点之间有连线
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使WPF的TreeView节点之间有连线相关的知识,希望对你有一定的参考价值。
参考技术A IntroductionWPF default TreeView is very good, but many people still want it to have lines join each of its child elements, like Windows Forms TreeView , including me. I have searched on the internet and have some examples, but they were not designed well enough.
Now, I myself designed a TreeView with style as WinForms. Hope this will help many people!
Source Code
All you need is an XAML file and a code behind.
First, you need draw Toggle Button: From Triangle button to
Plus-Minus button: draw a rectangle with dark border, then draw two
lines, one vertical line and one horizontal line. When TreeViewItem is expanded, the vertical line will hide:
<!-- Toggle Button -->
<Style x:Key="ExpandCollapseToggleStyle" TargetType="ToggleButton">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid Width="15" Height="13" SnapsToDevicePixels="True">
<!-- Rectangle 9x9 pixels -->
<Rectangle Width="9" Height="9"
Stroke="#919191" SnapsToDevicePixels="true">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0.5,2" StartPoint="0.5,0">
<GradientStop Color="White" Offset="0"/>
<GradientStop Color="Silver" Offset="0.5"/>
<GradientStop Color="LightGray" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<!-- Vertical line inside rectangle -->
<Rectangle x:Name="ExpandPath" Width="1"
Height="5" Stroke="Black" SnapsToDevicePixels="true"/>
<!-- Horizontal line inside rectangle -->
<Rectangle Width="5" Height="1"
Stroke="Black" SnapsToDevicePixels="true"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Visibility"
TargetName="ExpandPath" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
In the above code, you can see a trigger, it will make the vertical
line inside toggle button hide if item is expanded, or show if its
children collapsed.
Then, you need to draw vertical and horizontal connecting lines between nodes: You need to redesign TreeViewItem control. Add these connecting lines:
<!-- Horizontal line -->
<Rectangle x:Name="HorLn" Margin="9,1,0,0" Height="1"
Stroke="#DCDCDC" SnapsToDevicePixels="True"/>
<!-- Vertical line -->
<Rectangle x:Name="VerLn" Width="1" Stroke="#DCDCDC"
Margin="0,0,1,0" Grid.RowSpan="2" SnapsToDevicePixels="true"
Fill="White"/>
to your TreeViewItem template like this:
<!-- TreeViewItem -->
<Style x:Key="x:Type TreeViewItem" TargetType="x:Type TreeViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="x:Type TreeViewItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="19" Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<!-- Connecting Lines -->
<!-- Horizontal line -->
<Rectangle x:Name="HorLn" Margin="9,1,0,0" Height="1" Stroke="#DCDCDC" SnapsToDevicePixels="True"/>
<!-- Vertical line -->
<Rectangle x:Name="VerLn" Width="1"
Stroke="#DCDCDC" Margin="0,0,1,0" Grid.RowSpan="2"
SnapsToDevicePixels="true" Fill="White"/>
<!-- Insert Toggle Button -->
<ToggleButton Margin="-1,0,0,0" x:Name="Expander"
Style="StaticResource ExpandCollapseToggleStyle"
IsChecked="Binding Path=IsExpanded,
RelativeSource=RelativeSource TemplatedParent" ClickMode="Press"/>
<Border Name="Bd" Grid.Column="1"
Background="TemplateBinding Background"
BorderBrush="TemplateBinding BorderBrush"
BorderThickness="TemplateBinding BorderThickness"
Padding="TemplateBinding Padding" SnapsToDevicePixels="True">
<ContentPresenter x:Name="PART_Header"
ContentSource="Header"
HorizontalAlignment="TemplateBinding HorizontalContentAlignment"
MinWidth="20"/>
</Border>
<ItemsPresenter x:Name="ItemsHost" Grid.Row="1"
Grid.Column="1" Grid.ColumnSpan="2"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Then, you need put the class TreeViewLineConverter to your namespace. This class will change the connecting lines if the item is the last in the list:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace TreeViewEx
public partial class MainWindow : Window
public MainWindow()
InitializeComponent();
class TreeViewLineConverter : IValueConverter
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
TreeViewItem item = (TreeViewItem)value;
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
return ic.ItemContainerGenerator.IndexFromContainer(item) == ic.Items.Count - 1;
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
return false;
Insert your namespace to your XAML, i.e.:
<Window x:Class="TreeViewEx.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TreeViewEx"/>
Add this line to Window.Resources :
<local:TreeViewLineConverter x:Key="LineConverter"/>
Add trigger to TreeViewItem template, this trigger changes the connecting lines if the item is the last in the list:
<!-- This trigger changes the connecting lines if the item is the last in the list -->
<DataTrigger Binding="Binding RelativeSource=RelativeSource Self,
Converter=StaticResource LineConverter" Value="true">
<Setter TargetName="VerLn" Property="Height" Value="9"/>
<Setter TargetName="VerLn" Property="VerticalAlignment" Value="Top"/>
</DataTrigger>
The TreeView will have WinForms style now. You can add more trigger to control behavior of TreeView if you want. The full trigger can be found in the attached file.
ToDo
In WinForms TreeView , the connecting line is a dotted line. To make these lines dotted, change:
<!-- Connecting Lines -->
<Rectangle x:Name="HorLn" Margin="9,1,0,0" Height="1"
Stroke="#DCDCDC" SnapsToDevicePixels="True"/>
<Rectangle x:Name="VerLn" Width="1" Stroke="#DCDCDC"
Margin="0,0,1,0" Grid.RowSpan="2" SnapsToDevicePixels="true"
Fill="White"/>
To:
<!-- Connecting Lines -->
<Rectangle x:Name="HorLn" Margin="9,1,0,0" Height="1"
Stroke="Blue" StrokeDashCap="Square" StrokeDashArray="0,2"
StrokeDashOffset="1" SnapsToDevicePixels="True"/>
<Rectangle x:Name="VerLn" Width="1" Stroke="Blue"
StrokeDashCap="Square" StrokeDashArray="0,2" Margin="0,0,1,0"
Grid.RowSpan="2" SnapsToDevicePixels="true" Fill="White"/>
But it is not pretty, as you see. As I'm a newbie in WPF, I don't know to style these lines perfectly.
Reference
This is the code I referenced before I wrote my own:
WPF 之 TreeView节点重命名
下面的TreeView节点是通过数据双向绑定的方式,绑定到TextBlock控件和TextBox控件的Text属性上,并且让两者绑定相同的属性,同时使TextBox控件刚好完全覆盖TextBlock控件, 由于TextBlock控件和TextBox控件的区别,TextBlock控件无法实现编辑,所以我在TextBlock控件的上面覆盖了一个TextBox控件,初始状态下我们设置TextBox的Visibility属性为Collapsed,当我们点击重命名的时候,我们再设置TextBox的Visibility属性为Visible,这样我们就能够进行节点的重命名,当然当我们命名完成后(该TextBox失去焦点之后)我们再设置TextBox的Visibility属性为Collapsed,这样就完成了重命名的过程,当然我们还有很多重要的工作要做,比如如何获取HierarchicalDataTemplate中的TextBox控件这个是关键,其次TextBlock控件和TextBox控件必须同时绑定到同一属性,这样当属性值发生改变时,就能够更改TextBlock的Text属性值。
注意:TextBox的默认绑定方式Mode=TwoWay。
前端XAML代码,模板中加一个TextBlock控件和加一个TextBox控件:
<TreeView.ItemTemplate> <HierarchicalDataTemplate DataType="{x:Type localex:TreeMode}" ItemsSource="{Binding Children}"> <CheckBox Tag="{Binding Children}" IsChecked="{Binding IsChecked, Mode=TwoWay}" ToolTip="{Binding ToolTip}"> <StackPanel Orientation="Horizontal"> <Image VerticalAlignment="Center" Source="{Binding Icon}"/> <StackPanel Orientation="Vertical"> <TextBlock Text="{Binding Name, Mode=TwoWay}" HorizontalAlignment="Center" Width="Auto"/> <TextBox x:Name="renametextbox" Text="{Binding Name, Mode=TwoWay}" HorizontalAlignment="Center" Margin="0,-20,0,0" Width="Auto" Visibility="Collapsed" LostFocus="renametextbox_LostFous"/> </StackPanel> </StackPanel> <CheckBox.ContextMenu> <ContextMenu> <MenuItem Name="reNameItem" Header="重命名" Click="ReNameTreeViewItem_Click"> </MenuItem> </ContextMenu> </CheckBox.ContextMenu> </CheckBox> </HierarchicalDataTemplate> </TreeView.ItemTemplate>
后端核心代码:
//下面的部分是在鼠标指针位于此元素(TreeViewItem)上并且按下鼠标右键时发生。 private void TreeViewItem_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e) { //此处item定义的是一个类的成员变量,是一个TreeViewItem类型 item = GetParentObjectEx<TreeViewItem>(e.OriginalSource as DependencyObject) as TreeViewItem; if (item != null) { //使当前节点获得焦点 item.Focus(); //系统不再处理该操作 e.Handled = true; } } //对当前TreeViewItem进行重命名 private void ReNameTreeViewItem_Click(object sender, RoutedEventArgs e) { //获取在TreeView.ItemTemplate中定义的TextBox控件 tempTextBox = FindVisualChild<TextBox>(item as DependencyObject); //设置该TextBox的Visibility 属性为Visible tempTextBox.Visibility = Visibility.Visible; }
下面的这个函数主要是利用VisualTreeHelper.GetParent()方法获取视觉树上面的各种控件,当我们鼠标点击TreeView节点的时候,我们沿着视觉树VisualTree依次向上查找获取相应的控件,在本例中依次查找到的控件为:TextBlock-》StackPanel-》StackPanel-》ContentPresenter-》BulletDecorator-》CheckBox-》ContentPresenter-》Boarder-》Grid-》TreeViewItem,通过每一次的向上查找最终找到我们需要的TreeViewItem对象。
//获取当前TreeView的TreeViewItem public TreeViewItem GetParentObjectEx<TreeViewItem>(DependencyObject obj) where TreeViewItem : FrameworkElement { DependencyObject parent = VisualTreeHelper.GetParent(obj); while (parent != null) { if (parent is TreeViewItem) { return (TreeViewItem)parent; } parent = VisualTreeHelper.GetParent(parent); } return null; }
下面的这个函数也是非常重要的,由于我们定义的TextBox控件是在TreeView.ItemTemplate中定义的,所以无法通过this来查找当前的控件,如果无法获取当前的该控件,就无法进行下面的操作,所以这个函数也是非常重要的。和鼠标点击是沿着视觉树向上查找不同,此处我们需要沿着视觉树向下查找,直到找到我们TextBox控件为止,最终返回TextBox控件对象,这个刚好和上面的过程相反,但是这个过程也是非常重要的,具体的使用方式可以参考MSDN上面有更加具体的说明。
//获取ItemTemplate内部的各种控件 private childItem FindVisualChild<childItem>(DependencyObject obj) where childItem : DependencyObject { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++) { DependencyObject child = VisualTreeHelper.GetChild(obj, i); if (child != null && child is childItem) return (childItem)child; else { childItem childOfChild = FindVisualChild<childItem>(child); if (childOfChild != null) return childOfChild; } } return null; }
//当TextBox失去焦点时发生此事件 private void renametextbox_LostFous(object sender, RoutedEventArgs e) { tempTextBox.Visibility = Visibility.Collapsed; }
以上是关于如何使WPF的TreeView节点之间有连线的主要内容,如果未能解决你的问题,请参考以下文章
如何将 Xml 属性绑定到 Treeview 节点,同时将 XDocument 数据绑定到 WPF Treeview