WPF中TreeView的+-号和连线style的一种实现
最近又开始跟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 KeyTime="0" Value="{x:Static Visibility.Visible}" /> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Collapsed" /> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Rectangle Height="1" Fill="Black" VerticalAlignment="Center" Margin="9 0 0 0"/> <ToggleButton Width="9" Height="9" x:Name="Expander" Style="{StaticResource customToggleButtonStyle}" ClickMode="Press" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"/> <Border x:Name="Bd" Grid.Column="1" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}"> <ContentPresenter x:Name="PART_Header" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/> </Border> <ItemsPresenter x:Name="ItemsHost" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Visibility="Collapsed" /> </Grid> <ControlTemplate.Triggers> <Trigger Property="HasItems" Value="false"> <Setter TargetName="Expander" Property="Visibility" Value="Hidden" /> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="HasHeader" Value="false" /> <Condition Property="Width" Value="Auto" /> </MultiTrigger.Conditions> <Setter TargetName="PART_Header" Property="MinWidth" Value="75" /> </MultiTrigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="HasHeader" Value="false" /> <Condition Property="Height" Value="Auto" /> </MultiTrigger.Conditions> <Setter TargetName="PART_Header" Property="MinHeight" Value="19" /> </MultiTrigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <Grid> <TreeView x:Name="treeView"> <TreeView.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Children}"> <TextBlock Text="{Binding Name}"/> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView> <Button VerticalAlignment="Bottom" Click="Button_Click" Content="Click"/> </Grid> </Window>
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace TreeViewStyleDemo { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); for (int i = 0; i < 3; i++) { var item = new ItemViewModel(); item.Name = "Item" + i; item.Children = new ObservableCollection<ItemViewModel>(); for (int j = 0; j < 3; j++) { var sub_item = new ItemViewModel(){ Name = "SubItem" + j}; sub_item.Children = new ObservableCollection<ItemViewModel>(); for (int k = 0; k < 3; k++) { sub_item.Children.Add(new ItemViewModel() { Name = "SubsubItem" + k }); } item.Children.Add(sub_item); } items.Add(item); } treeView.ItemsSource = items; } ObservableCollection<ItemViewModel> items = new ObservableCollection<ItemViewModel>(); private void Button_Click(object sender, RoutedEventArgs e) { items[0].Children.Add(new ItemViewModel() { Name = "Added" }); } } public class ItemViewModel { public string Name { get; set; } public IList<ItemViewModel> Children { get; set; } } public class CustomPanel : StackPanel { protected override Size ArrangeOverride(Size arrangeSize) { return base.ArrangeOverride(arrangeSize); } protected override Size MeasureOverride(Size constraint) { return base.MeasureOverride(constraint); } 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); } } public Brush LineBrush { get { return (Brush)GetValue(LineBrushProperty); } set { SetValue(LineBrushProperty, value); } } public static readonly DependencyProperty LineBrushProperty = DependencyProperty.Register("LineBrush", typeof(Brush), typeof(CustomPanel), new PropertyMetadata(null)); public double LineThiness { get { return (double)GetValue(LineThinessProperty); } set { SetValue(LineThinessProperty, value); } } public static readonly DependencyProperty LineThinessProperty = DependencyProperty.Register("LineThiness", typeof(double), typeof(CustomPanel), new PropertyMetadata(0d, new PropertyChangedCallback((o,e)=>{(o as Panel).InvalidateVisual(); }))); public double Offset { get { return (double)GetValue(OffsetProperty); } set { SetValue(OffsetProperty, value); } } public static readonly DependencyProperty OffsetProperty = DependencyProperty.Register("Offset", typeof(double), typeof(CustomPanel), new PropertyMetadata(0d, new PropertyChangedCallback((o,e)=>{ (o as Panel).InvalidateVisual(); }))); } }
好,收工
这里已经发现了一个坑,多级节点的高度计算上会有问题,果然应该用固定的行高来计算才是靠谱的么,自己根据需要改吧,我懒得弄了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具