定義WPF中TreeViewItem的一種Style,提供一種處理ItemsControl中類似方案的思路。 ...
最近又開始跟WPF打交道,項目裡面用到了TreeView這個控制項。然後需要有一個連線的外觀就像是這樣
二話不說,百度了一下,找到一個實現, 通道。
把代碼拷貝到項目裡面,跑了一下,看上去還不錯。但是這哥們的實現裡面有一個缺陷。我們可以看到每個節點的最後的子節點前面的豎線,繪製方式跟其他的節點是有區別的,跟上圖顯示的一樣。
拷貝的代碼,在樹中添加新的節點的時候,之前最後節點的"|_"這種風格的線在新節點插入後不會動態更新,還是會保持這種風格。這種風格在不需要改變節點的時候還是可以的,而需要增刪節點的話,那麼 就不合適了。
繼續在網上找了找沒有現成的解決方案,後來想了一下,可以試試在給一個節點添加子節點的時候,將所有子節點都刪除然後再重新添加進去這種方式來更新UI。但我沒去嘗試這個方案是否真的可以。額,不管是否可行,我都不太喜歡這個方案,所以就只能自己做了。
那麼從這裡開始描述整個實現過程。
首先是問題的根源,一組節點的最後一個節點前面的豎線的繪製方式是需要區別於其他的節點。然後這個是由於它的位置所決定的,而跟它本身的屬性是沒有一點關係的,我們也沒有什麼直接的方式來得到這個信息,能做的無非就像上面那個哥們那樣,去跑一遍index之類的方法來判定是否是最後一個節點。
根據這個邏輯,我們可以發現,單單處理TreeViewItem這個類的Style是沒有用的,或者說是有缺陷的。
那麼怎麼辦呢?
TreeViewItem本身是ItemsControl,TreeView本身也是,那麼它的繪製方式就很明顯了,無非是ItemsTemplate決定了Item的數據外觀,ItemsPanel決定瞭如何去組織Items。
我們要做的就是在繪製Items的時候,把豎線放到裡面就行了,這樣我們就有了豎線,而且我們在遍歷的時候是知道每個節點的位置信息的,這樣最後一個節點的繪製信息我們是能拿到的。
橫線的話,我們有兩個選擇,一個是也在ItemsPanel裡面去繪製;或者我們把這根線放到TreeViewItem的Style裡面。我選擇了後者,原因是,我們的“+”是在TreeViewItem裡面的。而“+”在橫線的前面。
ItemsPanel的話,我們重寫一下StackPanel就足夠了。
這樣方案基本就確定了:
1. 重寫StackPanel來繪製Item前面的豎線
2. 重寫TreeViewItem的Style來繪製“+-”號和橫線
首先們準備數據模型,來調試UI
public class ItemViewModel { public string Name { get; set; } public IList<ItemViewModel> Children { get; set; } }
TreeView的基本設置
<TreeView x:Name="treeView"> <TreeView.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Children}"> <TextBlock Text="{Binding Name}"/> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView>
TreeView和TreeViewItem的預設模板,直接從msdn拷貝,通道。
只需要在OnRender方法裡面在繪製Item的時候在前面畫一根線就行了,主要是處理一下最後一個節點前的線,只繪製1/2即可。但這麼算的話,會有一個問題,如果節點自己有子節點的話,那麼子節點的高度是會算在當前節點的,這樣一來豎線的高度是錯的,我做了一個設定,就是在子節點的內容沒有換行的情況下,所有的子節點的高度都是一樣的(實際上,基本情況也就是這樣)。這樣一來:1. 沒有子節點那麼就是1/2的子節點高度;如果有那麼就是RenderSize.Height / Items.Count / 2這種。
protected override void OnRender(DrawingContext dc) { base.OnRender(dc); if (Children.Count > 0) { TreeViewItem child; Point point; Pen pen = new Pen(LineBrush, LineThiness); Point startPoint = new Point(Offset,0), endPoint = new Point(Offset,0); for (int i = 0; i < Children.Count - 1; i++) { child = Children[i] as TreeViewItem; point = child.TranslatePoint(new Point(), this); startPoint.Y = point.Y; endPoint.Y = point.Y + child.RenderSize.Height; dc.DrawLine(pen, startPoint, endPoint); } child = Children[Children.Count - 1] as TreeViewItem; point = child.TranslatePoint(new Point(), this); startPoint.Y = point.Y; if (!child.IsExpanded || child.Items == null || child.Items.Count < 1) endPoint.Y = point.Y + child.RenderSize.Height / 2; else endPoint.Y = point.Y + child.RenderSize.Height / child.Items.Count / 2; dc.DrawLine(pen, startPoint, endPoint); } }
然後再新建一個TreeViewItem的Style,裡面畫個“+” "-"號再加一根橫線就好了
<Style x:Key="customToggleButtonStyle" TargetType="{x:Type ToggleButton}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ToggleButton}"> <Grid Background="White"> <Rectangle Fill="{x:Null}" Stroke="Black" StrokeThickness="1"/> <Rectangle Height="1" Fill="Black" VerticalAlignment="Center" /> <Rectangle Width="1" Fill="Black" HorizontalAlignment="Center" x:Name="line" Visibility="Collapsed"/> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsChecked" Value="False"> <Setter Property="Visibility" Value="Visible" TargetName="line"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
設置一下TreeViewItem中的ItemsPanel引用我們定義的Panel,就可以了,當然還有一些細節要處理,不過我就不細說了,我已經沒耐心說下去了,所有的代碼都在下麵,寫這篇文章主要是想寫了,嗯,就是這樣。
<Window x:Class="TreeViewStyleDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:TreeViewStyleDemo" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <!--Control colors.--> <Color x:Key="WindowColor">#FFE8EDF9</Color> <Color x:Key="ContentAreaColorLight">#FFC5CBF9</Color> <Color x:Key="ContentAreaColorDark">#FF7381F9</Color> <Color x:Key="DisabledControlLightColor">#FFE8EDF9</Color> <Color x:Key="DisabledControlDarkColor">#FFC5CBF9</Color> <Color x:Key="DisabledForegroundColor">#FF888888</Color> <Color x:Key="SelectedBackgroundColor">#FFC5CBF9</Color> <Color x:Key="SelectedUnfocusedColor">#FFDDDDDD</Color> <Color x:Key="ControlLightColor">White</Color> <Color x:Key="ControlMediumColor">#FF7381F9</Color> <Color x:Key="ControlDarkColor">#FF211AA9</Color> <Color x:Key="ControlMouseOverColor">#FF3843C4</Color> <Color x:Key="ControlPressedColor">#FF211AA9</Color> <Color x:Key="GlyphColor">#FF444444</Color> <Color x:Key="GlyphMouseOver">sc#1, 0.004391443, 0.002428215, 0.242281124</Color> <!--Border colors--> <Color x:Key="BorderLightColor">#FFCCCCCC</Color> <Color x:Key="BorderMediumColor">#FF888888</Color> <Color x:Key="BorderDarkColor">#FF444444</Color> <Color x:Key="PressedBorderLightColor">#FF888888</Color> <Color x:Key="PressedBorderDarkColor">#FF444444</Color> <Color x:Key="DisabledBorderLightColor">#FFAAAAAA</Color> <Color x:Key="DisabledBorderDarkColor">#FF888888</Color> <Color x:Key="DefaultBorderBrushDarkColor">Black</Color> <!--Control-specific resources.--> <Color x:Key="HeaderTopColor">#FFC5CBF9</Color> <Color x:Key="DatagridCurrentCellBorderColor">Black</Color> <Color x:Key="SliderTrackDarkColor">#FFC5CBF9</Color> <Color x:Key="NavButtonFrameColor">#FF3843C4</Color> <LinearGradientBrush x:Key="MenuPopupBrush" EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="{DynamicResource ControlLightColor}" Offset="0" /> <GradientStop Color="{DynamicResource ControlMediumColor}" Offset="0.5" /> <GradientStop Color="{DynamicResource ControlLightColor}" Offset="1" /> </LinearGradientBrush> <LinearGradientBrush x:Key="ProgressBarIndicatorAnimatedFill" StartPoint="0,0" EndPoint="1,0"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#000000FF" Offset="0" /> <GradientStop Color="#600000FF" Offset="0.4" /> <GradientStop Color="#600000FF" Offset="0.6" /> <GradientStop Color="#000000FF" Offset="1" /> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <Style x:Key="{x:Type TreeView}" TargetType="TreeView"> <Setter Property="OverridesDefaultStyle" Value="True" /> <Setter Property="SnapsToDevicePixels" Value="True" /> <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" /> <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="TreeView"> <Border Name="Border" CornerRadius="1" BorderThickness="1"> <Border.BorderBrush> <SolidColorBrush Color="{DynamicResource BorderMediumColor}" /> </Border.BorderBrush> <Border.Background> <SolidColorBrush Color="{DynamicResource ControlLightColor}" /> </Border.Background> <ScrollViewer Focusable="False" CanContentScroll="False" Padding="4"> <ItemsPresenter /> </ScrollViewer> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="ExpandCollapseToggleStyle" TargetType="ToggleButton"> <Setter Property="Focusable" Value="False" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="ToggleButton"> <Grid Width="15" Height="13" Background="Transparent"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="CheckStates"> <VisualState x:Name="Checked"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="Collapsed"> <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Hidden}" /> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="Expanded"> <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" /> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Unchecked" /> <VisualState x:Name="Indeterminate" /> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Path x:Name="Collapsed" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="1,1,1,1" Data="M 4 0 L 8 4 L 4 8 Z"> <Path.Fill> <SolidColorBrush Color="{DynamicResource GlyphColor}" /> </Path.Fill> </Path> <Path x:Name="Expanded" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="1,1,1,1" Data="M 0 4 L 8 4 L 4 8 Z" Visibility="Hidden"> <Path.Fill> <SolidColorBrush Color="{DynamicResource GlyphColor}" /> </Path.Fill> </Path> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="customToggleButtonStyle" TargetType="{x:Type ToggleButton}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ToggleButton}"> <Grid Background="White"> <Rectangle Fill="{x:Null}" Stroke="Black" StrokeThickness="1"/> <Rectangle Height="1" Fill="Black" VerticalAlignment="Center" /> <Rectangle Width="1" Fill="Black" HorizontalAlignment="Center" x:Name="line" Visibility="Collapsed"/> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsChecked" Value="False"> <Setter Property="Visibility" Value="Visible" TargetName="line"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="TreeViewItemFocusVisual"> <Setter Property="Control.Template"> <Setter.Value> <ControlTemplate> <Border> <Rectangle Margin="0,0,0,0" StrokeThickness="5" Stroke="Black" StrokeDashArray="1 2" Opacity="0" /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="{x:Type TreeViewItem}" TargetType="{x:Type TreeViewItem}"> <Setter Property="Background" Value="Transparent" /> <Setter Property="HorizontalContentAlignment" Value="{Binding Path=HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" /> <Setter Property="VerticalContentAlignment" Value="{Binding Path=VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" /> <Setter Property="Padding" Value="1,0,0,0" /> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" /> <Setter Property="FocusVisualStyle" Value="{StaticResource TreeViewItemFocusVisual}" /> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <local:CustomPanel LineBrush ="Black" Background="LightYellow" SnapsToDevicePixels="True" Orientation="Vertical" LineThiness=".5" Offset="9.5" IsItemsHost="True"/> </ItemsPanelTemplate> </Setter.Value> </Setter> <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> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="SelectionStates"> <VisualState x:Name="Selected"> <Storyboard> <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd" Storyboard.TargetProperty="(Panel.Background). (SolidColorBrush.Color)" > <EasingColorKeyFrame KeyTime="0" Value="{StaticResource SelectedBackgroundColor}" /> </ColorAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Unselected" /> <VisualState x:Name="SelectedInactive"> <Storyboard> <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd" Storyboard.TargetProperty="(Panel.Background). (SolidColorBrush.Color)"> <EasingColorKeyFrame KeyTime="0" Value="{StaticResource SelectedUnfocusedColor}" /> </ColorAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> <VisualStateGroup x:Name="ExpansionStates"> <VisualState x:Name="Expanded"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="ItemsHost"> <DiscreteObjectKeyFrame