【原创】WPF TreeView带连接线样式的优化(WinFrom风格)

一、前言

  之前查找WPF相关资料的时候,发现国外网站有一个TreeView控件的样式,是WinFrom风格的,样式如下,文章链接:https://www.codeproject.com/tips/673071/wpf-treeview-with-winforms-style-fomat 

【原创】WPF TreeView带连接线样式的优化(WinFrom风格)【原创】WPF TreeView带连接线样式的优化(WinFrom风格)

上面的右边的图片是用WPF实现的,看起来不错,实现的代码也比较简单,关键样式代码如下:

 1  <!-- TreeViewItem -->
 2         <Style x:Key="{x:Type TreeViewItem}" TargetType="{x:Type TreeViewItem}">
 3             <Setter Property="Background" Value="Transparent"/>
 4             <Setter Property="Padding" Value="1,0,0,0"/>
 5             <Setter Property="Template">
 6                 <Setter.Value>
 7                     <ControlTemplate TargetType="{x:Type TreeViewItem}">
 8                         <Grid>
 9                             <Grid.ColumnDefinitions>
10                                 <ColumnDefinition MinWidth="19" Width="Auto"/>
11                                 <ColumnDefinition Width="Auto"/>
12                                 <ColumnDefinition Width="*"/>
13                             </Grid.ColumnDefinitions>
14                             <Grid.RowDefinitions>
15                                 <RowDefinition Height="Auto"/>
16                                 <RowDefinition/>
17                             </Grid.RowDefinitions>
18 
19                             <!-- Connecting Lines -->
20                             <Rectangle x:Name="HorLn" Margin="9,1,0,0" Height="1" Stroke="#DCDCDC" SnapsToDevicePixels="True"/>
21                             <Rectangle x:Name="VerLn" Width="1" Stroke="#DCDCDC" Margin="0,0,1,0" Grid.RowSpan="2" SnapsToDevicePixels="true" Fill="White"/>
22                             <ToggleButton Margin="-1,0,0,0" x:Name="Expander" Style="{StaticResource ExpandCollapseToggleStyle}" IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press"/>
23                             <Border Name="Bd" Grid.Column="1" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
24                                 <ContentPresenter x:Name="PART_Header" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" MinWidth="20"/>
25                             </Border>
26                             <ItemsPresenter x:Name="ItemsHost" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"/>
27                         </Grid>
28                         <ControlTemplate.Triggers>
29 
30                             <!-- This trigger changes the connecting lines if the item is the last in the list -->
31                             <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource LineConverter}}" Value="true">
32                                 <Setter TargetName="VerLn" Property="Height" Value="9"/>
33                                 <Setter TargetName="VerLn" Property="VerticalAlignment" Value="Top"/>
34                             </DataTrigger>
35                             <Trigger Property="IsExpanded" Value="false">
36                                 <Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
37                             </Trigger>
38                             <Trigger Property="HasItems" Value="false">
39                                 <Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
40                             </Trigger>
41                             <MultiTrigger>
42                                 <MultiTrigger.Conditions>
43                                     <Condition Property="HasHeader" Value="false"/>
44                                     <Condition Property="Width" Value="Auto"/>
45                                 </MultiTrigger.Conditions>
46                                 <Setter TargetName="PART_Header" Property="MinWidth" Value="75"/>
47                             </MultiTrigger>
48                             <MultiTrigger>
49                                 <MultiTrigger.Conditions>
50                                     <Condition Property="HasHeader" Value="false"/>
51                                     <Condition Property="Height" Value="Auto"/>
52                                 </MultiTrigger.Conditions>
53                                 <Setter TargetName="PART_Header" Property="MinHeight" Value="19"/>
54                             </MultiTrigger>
55                             <Trigger Property="IsSelected" Value="true">
56                                 <Setter TargetName="Bd" Property="Background" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
57                                 <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
58                             </Trigger>
59                             <MultiTrigger>
60                                 <MultiTrigger.Conditions>
61                                     <Condition Property="IsSelected" Value="true"/>
62                                     <Condition Property="IsSelectionActive" Value="false"/>
63                                 </MultiTrigger.Conditions>
64                                 <Setter TargetName="Bd" Property="Background" Value="Green"/>
65                                 <Setter Property="Foreground" Value="White"/>
66                             </MultiTrigger>
67                             <Trigger Property="IsEnabled" Value="false">
68                                 <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
69                             </Trigger>
70                         </ControlTemplate.Triggers>
71                     </ControlTemplate>
72                 </Setter.Value>
73             </Setter>
74         </Style>

LineConvert:

 1     class TreeViewLineConverter : IValueConverter
 2     {
 3         public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 4         {
 5             TreeViewItem item = (TreeViewItem)value;
 6             ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
 7             return ic.ItemContainerGenerator.IndexFromContainer(item) == ic.Items.Count - 1;
 8         }
 9 
10         public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
11         {
12             return false;
13         }
14     }

二、存在问题

作者提到2有个Bug:

1、添加新的项目到最后一项的时候,原本是最后一项的样式不会更新,结果就是下面这张图:

2、字体大小发生改变的时候,连接线也会出现异常;

 【原创】WPF TreeView带连接线样式的优化(WinFrom风格)

上图中的TUYEN这一项的连接线没有更新

三、原因分析

  由于作者在TreeViewItem的Template中使用了DataTrigger,并且Binding自身,那么就只有在他创建的时候,会去执行LineConvert进行判断,如果结果为True,就会设置垂直连接线VerLn的样式:

1    <!-- This trigger changes the connecting lines if the item is the last in the list -->
2    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource LineConverter}}" Value="true">
3         <Setter TargetName="VerLn" Property="Height" Value="9"/>
4         <Setter TargetName="VerLn" Property="VerticalAlignment" Value="Top"/>
5    </DataTrigger>

但是在以后的程序运行过程中,DataTrigger是接收不到任务绑定的通知,自然就不会进行重绘,那垂直连接线还是老样子,不会重绘了

四、解决方案

  明白问题的原因后,自然好解决,不过我也是苦思摸索好几天,用Bing查了国外很多网站,也没有个好的方案;而先前因为墙的原因,没看到原文的评论,提到用附加属性来解决,不过代码一大串,也不如我这个方案简洁好用。

 1        <Rectangle x:Name="VerLn" Width="1" Stroke="#DCDCDC" Margin="0,0,1,0" Grid.RowSpan="2" SnapsToDevicePixels="true" Fill="White">
 2              <Rectangle.Height>
 3                    <MultiBinding Converter="{StaticResource LineConverter}">
 4                          <MultiBinding.Bindings>
 5                                <Binding  RelativeSource="{RelativeSource AncestorType=TreeView}" Path="ActualHeight" ></Binding>
 6                                <Binding  RelativeSource="{RelativeSource AncestorType=TreeView}" Path="ActualWidth"></Binding>
 7                                <Binding  RelativeSource="{RelativeSource TemplatedParent}"></Binding>
 8                                <Binding  RelativeSource="{RelativeSource Self}"></Binding>
 9                                <Binding  ElementName="Expander" Path="IsChecked"></Binding>
10                           </MultiBinding.Bindings>
11                     </MultiBinding>
12                </Rectangle.Height>
13         </Rectangle>

后台代码,LineConvert:

 1     class TreeViewLineConverter : IMultiValueConverter
 2     {
 3         public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
 4         {
 5             double height = (double) values[0];
 6 
 7             TreeViewItem item = values[2] as TreeViewItem;
 8             ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
 9             bool isLastOne = ic.ItemContainerGenerator.IndexFromContainer(item) == ic.Items.Count - 1;
10 
11             Rectangle rectangle = values[3] as Rectangle;
12             if (isLastOne)
13             {                
14                 rectangle.VerticalAlignment = VerticalAlignment.Top;
15                 return 9.0;
16             }
17             else
18             {
19                 rectangle.VerticalAlignment = VerticalAlignment.Stretch;
20                 return double.NaN;
21             }           
22         }
23 
24         public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
25         {
26             throw new NotImplementedException();
27         }
28     }

这里我对垂直线VerLn的Height属性使用了多重绑定,绑定的对象有TreeView的ActualWidth和ActualHeight,这两个是依赖属性的,只有数值发生变化,就会触发通知;垂直线的Height属性就能及时进行计算更新。

五、总结

  相对于原文下面评论,提到使用附加属性,通过监听TreeView的属性ItemContainerGenerator的ItemsChanged事件,然后每一项TreeViewItem再判断自己是不是最后一项,我的这种解决方案真的是简单也容易理解。

  在这几天的摸索过程,收获也蛮多,比如对依赖/附加属性,Adorner、路由事件,有幸拜读一些大佬的文章,才逐步加深上述功能的理解,而反观前端用Html/Css/Js就可以渲染各种各样的页面,不由得佩服,这里把TreeView的WinFrom风格样式共享出来,也希望能够帮助对WPF求知的朋友。

六、源码

1、原作者的代码:https://files.cnblogs.com/files/iDream2018/TreeViewEx.zip

2、优化后的代码:https://files.cnblogs.com/files/iDream2018/%E4%BC%98%E5%8C%96%E5%90%8ETreeViewEx.zip

发表评论

相关文章