WPF使用模板HierarchicalDataTemplate实现TreeView层次显示

第一阶段 显示TreeView

效果图
这里写图片描述

之前看过很多博客, 都是呼哧贴一堆代码, 呼哧创建一个类, 呼哧创建一堆函数……当时任务很紧一下子看不懂也懒得看了(主要是缺图)…现在重新梳理一下还挺清晰的

例如数据是这样的
这里写图片描述

那么数据类可能是这样的

    class Person
    {
        public int Id { get; set; }
        public int Level { get; set; }
        public string Name { get; set; }
        public int ParentId { get; set; }
    }

再套一层, 节点可能是这样的

    class Node
    {
        public int Id { get; set; }
        public int Level { get; set; }
        public string Name { get; set; }
        public int ParentId { get; set; }
        public List<Node> Children { get; set; }

        public string TreeDisplay {
            get { return $"{Name} [第{Level}世]";}
        }

        // 怎么把节点添加到List<>里面忽略...

    }

典型的一个树的结构. 要使用HierarchicalDataTemplate, 属性需要有IdParentId, 不知道是模板本身的硬性规定还是可以手动设置的

<TreeView x:Name="treeView" Grid.Row="1">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
            <StackPanel>
                <TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding TreeDisplay}" Margin="2,0,0,0"/>
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
    <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsExpanded" Value="True" />
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>

<TreeView.ItemTemplate>定义了数据绑定和展示, 绑定到object.Children, 展示object.Children[i].TreeDisplay, </TreeView.ItemContainerStyle>将所有节点全部展开

然后往控件上挂数据源

treeView.ItemsSource = new List<PersonTreeNode> { Root };

第二阶段 绑定节点修改

在树上可能有以下操作
1. 添加子节点
2. 移动子节点顺序
3. 删除子节点
4. 修改节点属性(名称等等)

按笨方法, 可能每次修改后都调用一次treeView.Items.Refresh(), 或者重新绑定一次数据源…但这显然不是正确的方法, 按上例每刷新一次大概就需要三四秒的时间
要刷新UI, 还是要实现INotify接口, 在有修改时通过事件通知UI更新

对于集合的变动, 需要实现INotifyCollectionChanged接口, 可以直接使用C#库中预定义的类, 将ChildrenList<T>修改为ObservableCollection<T>即可, 但此类没有实现IList, 所以一些LIst方法不能使用
对于属性的变动, 让Node实现INotifyPropertyChanged接口即可.

第三阶段 查找定位

网上找了一个TreeViewHelper, mark一下, 调用SelectItem(object)即可

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace XXXXXX
{
    public static class TreeViewHelper
    {
        /// <summary>
        /// Expands all children of a TreeView
        /// </summary>
        /// <param name="treeView">The TreeView whose children will be expanded</param>
        public static void ExpandAll(this TreeView treeView)
        {
            ExpandSubContainers(treeView);
        }

        /// <summary>
        /// Expands all children of a TreeView or TreeViewItem
        /// </summary>
        /// <param name="parentContainer">The TreeView or TreeViewItem containing the children to expand</param>
        private static void ExpandSubContainers(ItemsControl parentContainer)
        {
            foreach (Object item in parentContainer.Items)
            {
                TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
                if (currentContainer != null && currentContainer.Items.Count > 0)
                {
                    //expand the item
                    currentContainer.IsExpanded = true;

                    //if the item's children are not generated, they must be expanded
                    if (currentContainer.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
                    {
                        //store the event handler in a variable so we can remove it (in the handler itself)
                        EventHandler eh = null;
                        eh = new EventHandler(delegate
                        {
                            //once the children have been generated, expand those children's children then remove the event handler
                            if (currentContainer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
                            {
                                ExpandSubContainers(currentContainer);
                                currentContainer.ItemContainerGenerator.StatusChanged -= eh;
                            }
                        });

                        currentContainer.ItemContainerGenerator.StatusChanged += eh;
                    }
                    else //otherwise the children have already been generated, so we can now expand those children
                    {
                        ExpandSubContainers(currentContainer);
                    }
                }
            }
        }

        /// <summary>
        /// Searches a TreeView for the provided object and selects it if found
        /// </summary>
        /// <param name="treeView">The TreeView containing the item</param>
        /// <param name="item">The item to search and select</param>
        public static void SelectItem(this TreeView treeView, object item)
        {
            ExpandAndSelectItem(treeView, item);
        }

        /// <summary>
        /// Finds the provided object in an ItemsControl's children and selects it
        /// </summary>
        /// <param name="parentContainer">The parent container whose children will be searched for the selected item</param>
        /// <param name="itemToSelect">The item to select</param>
        /// <returns>True if the item is found and selected, false otherwise</returns>
        private static bool ExpandAndSelectItem(ItemsControl parentContainer, object itemToSelect)
        {
            //check all items at the current level
            foreach (Object item in parentContainer.Items)
            {
                TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;

                //if the data item matches the item we want to select, set the corresponding
                //TreeViewItem IsSelected to true
                if (item == itemToSelect && currentContainer != null)
                {
                    currentContainer.IsSelected = true;
                    currentContainer.BringIntoView();
                    currentContainer.Focus();

                    //the item was found
                    return true;
                }
            }

            //if we get to this point, the selected item was not found at the current level, so we must check the children
            foreach (Object item in parentContainer.Items)
            {
                TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;

                //if children exist
                if (currentContainer != null && currentContainer.Items.Count > 0)
                {
                    //keep track of if the TreeViewItem was expanded or not
                    bool wasExpanded = currentContainer.IsExpanded;

                    //expand the current TreeViewItem so we can check its child TreeViewItems
                    currentContainer.IsExpanded = true;

                    //if the TreeViewItem child containers have not been generated, we must listen to
                    //the StatusChanged event until they are
                    if (currentContainer.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
                    {
                        //store the event handler in a variable so we can remove it (in the handler itself)
                        EventHandler eh = null;
                        eh = new EventHandler(delegate
                        {
                            if (currentContainer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
                            {
                                if (ExpandAndSelectItem(currentContainer, itemToSelect) == false)
                                {
                                    //The assumption is that code executing in this EventHandler is the result of the parent not
                                    //being expanded since the containers were not generated.
                                    //since the itemToSelect was not found in the children, collapse the parent since it was previously collapsed
                                    currentContainer.IsExpanded = false;
                                }

                                //remove the StatusChanged event handler since we just handled it (we only needed it once)
                                currentContainer.ItemContainerGenerator.StatusChanged -= eh;
                            }
                        });
                        currentContainer.ItemContainerGenerator.StatusChanged += eh;
                    }
                    else //otherwise the containers have been generated, so look for item to select in the children
                    {
                        if (ExpandAndSelectItem(currentContainer, itemToSelect) == false)
                        {
                            //restore the current TreeViewItem's expanded state
                            currentContainer.IsExpanded = wasExpanded;
                        }
                        else //otherwise the node was found and selected, so return true
                        {
                            return true;
                        }
                    }
                }
            }

            //no item was found
            return false;
        }
    }
}

给TreeView设置属性
VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"
可以让其加载更快, 但对搜索定位的支持很差

另外 mark一下问题

treeview不支持多层更新的问题

public ObservableCollection<Node> Children { get; }

public void AddChildren(Node node)
{
    if (Children == null) Children = new ObservableCollection<Node>();
    Children.Add(node);
}

之前的代码是这样的, 理论上讲这样更省空间……然后在treeview里面连续新建两三层就不显示了……想了想, 自己用的一个小程序省什么内存
改成下面这样就正常了, 即在Node对象初始化时就创建集合

public ObservableCollection<Node> Children { get; } = new ObservableCollection<Node>();

public void AddChildren(Node node)
{
    Children.Add(node);
}

总结

C#类库\WPF架构本身有很多精妙的设计(相对中小规模应用来说), 但是……不自己写一遍真不知道在哪找啊……翻API文档都不知道怎么用的

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页