【原創】WPF TreeView帶連線線樣式的優化(WinFrom風格)

iDream2016發表於2020-12-16

一、前言

  之前查詢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

相關文章