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

posted @   zhaotianff  阅读(95)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
历史上的今天:
2021-08-14 Windows中的库编程(二、导出变量、类及DllMain函数的介绍)
点击右上角即可分享
微信分享提示