WPF TreeView固定列头
在我前面介绍控件的文章中介绍过,TreeView是一种列表控件,继承自ItemsControl。
我们先看一下TreeView的控件模板
1 <ControlTemplate TargetType="{x:Type TreeView}"> 2 <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="true"> 3 <ScrollViewer x:Name="_tv_scrollviewer_" Background="{TemplateBinding Background}" CanContentScroll="false" Focusable="false" HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"> 4 <ItemsPresenter/> 5 </ScrollViewer> 6 </Border> 7 </ControlTemplate>
核心的显示是一个ItemsPresenter。
真正的层级关系是在TreeViewItem中实现的。
我们先看一下TreeView和TreeViewItem的关系
TreeView是继承自ItemsControl控件,它的项容器控件TreeViewItem不是内容控件。每个TreeViewItem控件都是单独的ItemsControl控件,可以包含更多的TreeViewItem对象。
TreeViewItem类继承自HeaderedItemsControl类,而HeaderedItemsControl类又继承自ItemsControl类。
HeaderedItemsControl类添加Header属性,该属性用于TreeView中每个项显示的内容(通常为文本)。
然后我们再看一下TreeViewItem的控件模板
1 <ControlTemplate TargetType="{x:Type TreeViewItem}"> 2 <Grid> 3 <Grid.ColumnDefinitions> 4 <ColumnDefinition MinWidth="19" Width="Auto"/> 5 <ColumnDefinition Width="Auto"/> 6 <ColumnDefinition Width="*"/> 7 </Grid.ColumnDefinitions> 8 <Grid.RowDefinitions> 9 <RowDefinition Height="Auto"/> 10 <RowDefinition/> 11 </Grid.RowDefinitions> 12 <ToggleButton x:Name="Expander" ClickMode="Press" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource Mode=TemplatedParent}}" Style="{StaticResource ExpandCollapseToggleStyle}"/> 13 <Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Grid.Column="1" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true"> 14 <ContentPresenter x:Name="PART_Header" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> 15 </Border> 16 <ItemsPresenter x:Name="ItemsHost" Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="1"/> 17 </Grid> 18 </ControlTemplate>
大概看一下TreeViewItem的组成
一个Grid,定义了三列和两行。但实际上行定义并未用到。
第一列是用于折叠展开的ToggleButton,第二列是内容显示,核心控件是ContentPresenter,第三列是子列表,用的是一个ItemPresenter。
可以看到子列表它是第二列开始,所以前面会有一定的缩进。然后是嵌套,所以是递归缩进。
如果我们想要固定一个列头显示在前面的话,只能让子列表从第一列开始,避免递归时的缩进。然后再通过数据模板实现显示时的缩进。
我们拿前面文章中的代码进行演示
https://www.cnblogs.com/zhaotianff/p/16869172.html
首先我们将TreeViewItem控件模板中的列表放到Grid的第一列显示,并扩展3列
1 <ItemsPresenter x:Name="ItemsHost" Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="1"/>
然后在ViewModelBase中增加一个列头字段,假设显示索引
1 public class ViewModelBase : INotifyPropertyChanged 2 { 3 private int itemIndex; 4 5 public int ItemIndex 6 { 7 get => itemIndex; 8 set 9 { 10 itemIndex = value; 11 RaiseChange("ItemIndex"); 12 } 13 } 14 15 public event PropertyChangedEventHandler PropertyChanged; 16 17 public void RaiseChange(string propertyName) 18 { 19 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 20 } 21 }
再到XAML中设置数据模板(假设层级是固定的,如果不固定,后面会有动态的方法)
1 <TreeView ItemsSource="{Binding HierarchicalTestList}" ItemContainerStyle="{StaticResource TreeViewItemStyle1}"> 2 <TreeView.ItemTemplate> 3 <HierarchicalDataTemplate ItemsSource="{Binding Level1ChildList}" DataType="{x:Type local:Level1}"> 4 <!--第二级的列头和内容显示--> 5 <!--定义两列,第一列显示列头,第二列显示内容--> 6 <!--根据层级,设置Margin,用于缩进显示--> 7 <!--后面的显示方式依此类推--> 8 <Grid> 9 <Grid.ColumnDefinitions> 10 <ColumnDefinition Width="25"/> 11 <ColumnDefinition/> 12 </Grid.ColumnDefinitions> 13 14 <Label Content="{Binding ItemIndex}"></Label> 15 <Label Content="{Binding Level1Item}" Grid.Column="1"/> 16 </Grid> 17 <HierarchicalDataTemplate.ItemTemplate> 18 <HierarchicalDataTemplate ItemsSource="{Binding Level2ChildList}" DataType="{x:Type local:Level2}"> 19 <Grid> 20 <Grid.ColumnDefinitions> 21 <ColumnDefinition Width="25"/> 22 <ColumnDefinition/> 23 </Grid.ColumnDefinitions> 24 25 <Label Content="{Binding ItemIndex}"></Label> 26 <Label Content="{Binding Level2Item}" Grid.Column="1" Margin="20,0,0,0"/> 27 </Grid> 28 <HierarchicalDataTemplate.ItemTemplate> 29 <DataTemplate DataType="{x:Type local:Level3}"> 30 <Grid> 31 <Grid.ColumnDefinitions> 32 <ColumnDefinition Width="25"/> 33 <ColumnDefinition/> 34 </Grid.ColumnDefinitions> 35 36 <Label Content="{Binding ItemIndex}"></Label> 37 <Label Content="{Binding Level3Item}" Grid.Column="1" Margin="40,0,0,0"/> 38 </Grid> 39 40 </DataTemplate> 41 </HierarchicalDataTemplate.ItemTemplate> 42 </HierarchicalDataTemplate> 43 </HierarchicalDataTemplate.ItemTemplate> 44 </HierarchicalDataTemplate> 45 </TreeView.ItemTemplate> 46 </TreeView>
运行效果:
前面这种情况层级是固定的,假设层级不固定,我们可以分为以下两种情况
第一种情况是每一层的数据结构是固定的
针对这种情况,我们可以利用HierarchicalDataTemplate的嵌套功能实现。HierarchicalDataTemplate会自动递归显示。
然后再借助一个Converter,根据层级设置Margin,即可达到TreeView的层级显示效果。
首先我们定义一个节点数据,节点数据包含索引和父节点两个字段。
1 public class Level1 : ViewModelBase 2 { 3 private string displayName = ""; 4 5 public string DisplayName 6 { 7 get => displayName; 8 set 9 { 10 displayName = value; 11 RaiseChange("DisplayName"); 12 } 13 } 14 15 private ObservableCollection<Level1> children = new ObservableCollection<Level1>(); 16 17 public ObservableCollection<Level1> Children 18 { 19 get => children; 20 set 21 { 22 children = value; 23 RaiseChange("Children"); 24 } 25 } 26 27 28 private Level1 parent; 29 30 public Level1 Parent 31 { 32 get => parent; 33 set 34 { 35 parent = value; 36 RaiseChange("Parent"); 37 } 38 } 39 }
然后我们定义一个Converter,它可以根据层级设置节点数据显示的缩进。
1 public class TreeNodeMarginConverter : IValueConverter 2 { 3 public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 4 { 5 var level = GetLevel(value); 6 return new Thickness(level * 20, 0, 0, 0); 7 } 8 9 public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 10 { 11 throw new NotImplementedException(); 12 } 13 14 private int GetLevel(object item) 15 { 16 int depth = 1; 17 var level = item as Level1; 18 19 while (level.Parent != null) 20 { 21 depth++; 22 level = level.Parent; 23 } 24 25 return depth; 26 } 27 }
样式继续采用前面示例的样式,数据模板定义如下:
1 <TreeView ItemsSource="{Binding HierarchicalTestList}" ItemContainerStyle="{StaticResource TreeViewItemStyle1}" Name="tree"> 2 <TreeView.ItemTemplate> 3 <HierarchicalDataTemplate ItemsSource="{Binding Children}" DataType="{x:Type local:Level1}"> 4 <Grid> 5 <Grid.ColumnDefinitions> 6 <ColumnDefinition Width="25"/> 7 <ColumnDefinition/> 8 </Grid.ColumnDefinitions> 9 10 <Label Content="{Binding ItemIndex}"></Label> 11 <Label Content="{Binding DisplayName}" Grid.Column="1" Margin="{Binding Path=.,Converter={StaticResource TreeNodeMarginConverter}}"/> 12 </Grid> 13 </HierarchicalDataTemplate> 14 </TreeView.ItemTemplate> 15 </TreeView>
注意:这里第二个Label在设置Margin时,将值绑定到了自身,使用的Path=. ,然后再使用Converter对Margin进行转换。
运行效果如下:
第二种情况是数据结构不固定的
这种情况可以又可以使用两种方案实现
第一种方案是使用DataTemplateSelector功能
DataTemplateSelector可以根据数据对象和数据绑定元素来选择 DataTemplate。
可以参考下面的链接
https://www.cnblogs.com/zhaotianff/p/18380995
第二种方案是使用代码创建数据模板
可以参考下面的链接
https://www.cnblogs.com/zhaotianff/p/18373554
内容较多,不具体贴代码了,可以下载本文的示例代码查看
参考链接:
https://www.codeproject.com/Tips/1222013/Advanced-WPF-TreeView-with-Multi-Level-Binding-Cod
https://learn.microsoft.com/zh-cn/dotnet/api/system.windows.controls.datatemplateselector?view=netframework-4.7.2
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
2021-08-14 Windows中的库编程(二、导出变量、类及DllMain函数的介绍)