WPF实现一个树形列表控件

最近在用WPF写一些处理BOM数据的工具,需要一个树形列表控件用于显示BOM的层次结构,但WPF没有现成的树形列表控件,于是用DataGrid控件定制了一个。

思路

本质上,DataGrid只能显示表,因此只需要将树结构的数据源转换成一个列表,然后通过ItemsSource绑定这个列表,就可以了。为了使得列表看上去像一棵树,列表项的顺序应当始终遵循树的先序遍历的结果,并且,每一行应当根据节点到树根的层数进行适当缩进,视图上每一行前面再提供一个按钮,供用户展开或收起,这样基本就有了一个树的样子。Demo的效果就像这样:
WPF实现一个树形列表控件_第1张图片
为了更加实用,还需要考虑这些细节:

  • 视图前面的按钮要能反映展开或收起的状态,例如还未展开的显示+,已经展开的显示-,没有子节点而不能展开的也显示-(或者显示第三种标记)
  • 如果节点不能展开,那展开按钮应当不可用
  • 视图要能动态地反映树结构上节点的增删情况,例如,
    • 某个节点的展开按钮本不可用,但如果程序在这个节点下添加了一个子节点,那么这个按钮要立刻变成可用状态并且图标要变成+
    • 对于处于展开状态的数据节点,其子节点有增删时,DataGrid相应的行要动态增删,而不需要刷新整个列表或者重载入整棵树
  • 收起一个父节点后,要能保存子节点的展开还是收起的状态,以便重新展开父节点后,原来展开的子节点仍然是展开状态,这样的逻辑要一直递归到叶子节点,例如收起第一行P01,再展开,列表还应当是图上的样子,而不是仅剩未展开的P02和P03
  • 要考虑到整棵树的节点数以万计的时候,数据可能是按需加载的,例如,数据可能只加载到了第二层,用户点击展开某个节点时,才触发该节点的第三层数据的准备。(精确一点的表述是:用户点击第二层节点时,程序要显示第三层,并且加载第四层,因为这样才能知道第三层节点的展开按钮的状态)总结起来就是,数据加载永远比视图上能看到的多一层
  • 不对树节点的数据类型作过多假设,假设节点只实现了INotifyPropertyChanged接口、并且有一个ObservableCollection类型的属性用于存放子节点,属性名称不作限定
  • 要提供全局函数,例如全部展开、全部收起、展开到某个条件为止(例如只展开到第三层)

项目结构

项目文档结构如图
WPF实现一个树形列表控件_第2张图片
这里面主要文件是 TreeItem 、TreeList、Expander。

TreeList

TreeList的职责是读取一棵树的根节点,然后把整棵树(下称“数据树”)转换成一个列表,通过Items属性提供给DataGrid。并且TreeList要负责跟踪数据树的节点的增删情况,实时维护Items集合,以便数据树的变化能在DataGrid上反映出来。

TreeList独立完成这项工作比较困难,原因在于:每个数据树上的节点(下称“数据节点”)显示到表格中时,表格需要知道节点的一些信息,例如

  • 节点在树的第几层,以便计算缩进量
  • 节点当前是展开的还是没有展开
  • 节点在表格中是可见还是不可见(可不可见取决于父节点一直到根节点的路径上是不是都是展开状态)

而对数据节点的类型没有过多限定,因此假设数据节点自己不知道上述信息,不提供展开、收起这样的操作,甚至都没有一个Parent属性用于导航到自己的父节点。并且:数据节点可能是可复用的,一个节点实例可能出现在多个父节点下,而在视图上,这些节点处于不同的位置,有的展开了,有的没展开……所有这些复杂性都需要TreeList来处理。

为了减轻TreeList的工作,创建了TreeItem节点。

TreeItem

TreeItem是是对单个数据节点的封装。它封装数据节点,并且提供数据节点本身不具备的但和视图显示密切相关的属性,例如节点在树的第几层、节点是否是展开的,节点在视图上当前是否可见等,还提供了了展开、收起等视图层面的操作。

TreeItem自身带有Parent和Children属性,能够自组织成一棵树(下称视图结构树)。逻辑上,这棵树的结构与数据源完全对等,但实际处理上会有一些变通,仅用户访问到的数据节点才会构造对应的TreeItem。

为了能反映数据源的节点的变动情况,TreeItem会侦听它的数据节点的子节点变化情况,并更新自身的子节点。并且确定这个更新是否有必要通知到TreeList。TreeItem还负责在执行展开操作之前,通知TreeList产生“某节点将要展开”的事件,因为可能要通知外部程序先行加载数据节点的子节点。

Expander

Expander是一个定制控件用于实现展开、收起按钮和产生缩进。作为用户,DataGrid的第一列应当包含一个Expander,就像这样:

<DataGrid Margin="3"
                  ItemsSource="{Binding Path=TreeList.Items}" 
                  SelectedItem="{Binding TreeList.CurrentItem}"
                  AutoGenerateColumns="False" 
                  CanUserSortColumns="False">
    <DataGrid.Columns>
        <DataGridTemplateColumn Header="ID">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <StackPanel Margin="0" Orientation="Horizontal">
                        
                        <userControl:Expander DataContext="{Binding}"/>
                        <TextBlock Padding="3,0" Margin="0" Text="{Binding Tag.ID}"/>
                    StackPanel>
                DataTemplate>
            DataGridTemplateColumn.CellTemplate>
        DataGridTemplateColumn>
        <DataGridTextColumn Width="80" Header="Name" Binding="{Binding Tag.Name}"/>
        <DataGridTextColumn Width="80" Header="Category" Binding="{Binding Tag.Category}"/>
        <DataGridTextColumn Width="80" Header="Quantity" Binding="{Binding Tag.Quantity}"/>
    DataGrid.Columns>
DataGrid>

如果不包含Expander,看到的将只是一个没有缩进的简单列表

使用

项目本质上没有实现完整的树形列表控件,而仅仅实现了树形列表控件的视图模型(TreeList)。如果需要显示树形列表,则只需初始化一个TreeList,设置TopDataNode属性为数据树的根节点,然后将一个DataGrid控件的ItemsSource属性绑定到TreeList的Items属性上,DataGrid控件就变成了一个树形列表控件(前提是DataGrid.Columns中包含了一个Expander)

文中的Demo可在这里下载

你可能感兴趣的:(WPF实现一个树形列表控件)