【原创】WPF TreeView带连接线样式的优化(WinFrom风格)
一、前言
之前查找WPF相关资料的时候,发现国外网站有一个TreeView控件的样式,是WinFrom风格的,样式如下,文章链接:https://www.codeproject.com/tips/673071/wpf-treeview-with-winforms-style-fomat
上面的右边的图片是用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、字体大小发生改变的时候,连接线也会出现异常;
上图中的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