【原創】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