WPF之TreeView操作实战

1、绑定层级数据源
绘制名称为TreeView_NodeList的TreeView控件,使用HierarchicalDataTemplate类将具有层级结构的数据源绑定至TreeView控件。

<TreeView Name="TreeView_NodeList">
            <TreeView.Resources>
                <HierarchicalDataTemplate  DataType="{x:Type local:Node}" ItemsSource="{Binding Nodes,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
                    <StackPanel Orientation="Horizontal" Margin="0,2,0,2">
                        <TextBlock Text="{Binding NodeName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" ToolTip="{Binding NodeName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                    </StackPanel>
                </HierarchicalDataTemplate>
            </TreeView.Resources>
</TreeView>

其中Node类的定义如下所示。Node对象使用NodeId作为主键进行区分,NodeId为32位的GUID。Node的属性Nodes保存节点的子节点集合。属性IsDeleted 标记节点是否被删除。

    public class Node
    {
        public Node()
        {
            this.NodeId = Guid.NewGuid().ToString();
            this.IsDeleted = false;
            this.Nodes = new List<Node>();
        }

        /// <summary>
        /// 节点ID
        /// </summary>
        public string NodeId { get; set; }

        /// <summary>
        ///  节点名称
        /// </summary>
        public string NodeName { get; set; }

        /// <summary>
        /// 节点携带的内容
        /// </summary>
        public string NodeContent { get; set; }

        /// <summary>
        /// 被删除
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// 节点类型
        /// </summary>
        public NodeType NodeType { get; set; }

        /// <summary>
        /// 子节点集合
        /// </summary>
        public List<Node> Nodes { get; set; }
    }

枚举类型的NodeType属性表示节点的类型,节点具有根节点、叶子节点和结构节点三种类别。叶子节点不支持增加节点操作、非叶子结点不支持删除节点的操作。

    public enum NodeType
    {
        RootNode,//根节点
        LeafNode,//叶子节点
        StructureNode//结构节点,仅起到组织配置文件结构的作用,不参与修改
    }

下面的方法生成TreeView使用到的示例数据。生产环境中往往通过解析xml或者ini等文件或者访问数据库得到层级数据源。

        private List<Node> GetNodeList()
        {
            Node leafOneNode = new Node();
            leafOneNode.NodeName = "叶子节点一";
            leafOneNode.NodeContent = "我是叶子节点一";
            leafOneNode.NodeType = NodeType.LeafNode;
            leafOneNode.Nodes = new List<Node>();

            Node leafTwoNode = new Node();
            leafTwoNode.NodeName = "叶子节点二";
            leafTwoNode.NodeContent = "我是叶子节点二";
            leafTwoNode.NodeType = NodeType.LeafNode;
            leafTwoNode.Nodes = new List<Node>();

            Node leafThreeNode = new Node();
            leafThreeNode.NodeName = "叶子节点三";
            leafThreeNode.NodeContent = "我是叶子节点三";
            leafThreeNode.NodeType = NodeType.LeafNode;
            leafThreeNode.Nodes = new List<Node>();

            Node secondLevelNode = new Node();
            secondLevelNode.NodeName = "二级节点";
            secondLevelNode.NodeContent = "我是二级节点";
            secondLevelNode.NodeType = NodeType.StructureNode;
            secondLevelNode.Nodes = new List<Node>() { leafOneNode, leafTwoNode, leafThreeNode };

            Node firstLevelNode = new Node();
            firstLevelNode.NodeName = "一级节点";
            firstLevelNode.NodeContent = "我是一级节点";
            firstLevelNode.NodeType = NodeType.StructureNode;
            firstLevelNode.Nodes = new List<Node>() { secondLevelNode };

            return new List<Node>() 
            {
                new Node(){NodeName="根节点",NodeContent="我是根节点",NodeType=NodeType.RootNode,Nodes=new List<Node>(){firstLevelNode}}
            };
        }

程序使用nodeList接收返回的测试数据,并在页面的Loaded事件中进行数据绑定。

 public List<Node> nodeList { get; set; }
 private void Window_Loaded(object sender, RoutedEventArgs e)
 {
    nodeList = GetNodeList();
    this.TreeView_NodeList.ItemsSource = nodeList;
    ExpandTree();
 }

2、展开所有节点
其中使用到的ExpandTree()方法实现展开TreeView的所有节点。

        private void ExpandTree()
        {
            if (this.TreeView_NodeList.Items != null && this.TreeView_NodeList.Items.Count > 0)
            {
                foreach (var item in this.TreeView_NodeList.Items)
                {
                    DependencyObject dependencyObject = this.TreeView_NodeList.ItemContainerGenerator.ContainerFromItem(item);
                    if (dependencyObject != null)//第一次打开程序,dependencyObject为null,会出错
                    {
                        ((TreeViewItem)dependencyObject).ExpandSubtree();
                    }
                }
            }
        }

执行程序,可以得到下面的执行结果。
这里写图片描述

3、增加和删除节点
下面给TreeView增加右键弹出菜单,实现“增加节点”和“删除节点”的操作。
画面代码中设置TreeView的ContextMenu属性,增加弹出菜单项。

            <TreeView.ContextMenu>
                <ContextMenu x:Name="ContextMenu_EditNode">
                    <MenuItem Header="新增节点" Name="MenuItem_AddNode" Click="MenuItem_AddNode_Click"/>
                    <MenuItem Header="删除节点" Name="MenuItem_DeleteNode" Click="MenuItem_DeleteNode_Click"/>
                </ContextMenu>
            </TreeView.ContextMenu>

下面是增加节点和删除节点的具体逻辑代码。

        /// <summary>
        /// 新增节点
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MenuItem_AddNode_Click(object sender, RoutedEventArgs e)
        {
            var currentNode = FindTheNode(nodeList, selectedNode.NodeId);
            if (currentNode != null)
            {
                if (currentNode.NodeType == NodeType.LeafNode)
                {
                    MessageBox.Show("叶子节点不支持新增节点操作!");
                }
                else
                {
                    MessageBox.Show("开始新增节点操作!");
                }
            }
        }

        /// <summary>
        /// 删除节点
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MenuItem_DeleteNode_Click(object sender, RoutedEventArgs e)
        {
            var currentNode = FindTheNode(nodeList, selectedNode.NodeId);
            if (currentNode != null)
            {
                if (currentNode.NodeType != NodeType.LeafNode)
                {
                    MessageBox.Show("非叶子节点不支持删除操作!");
                }
                else
                {
                    MessageBoxResult dr = MessageBox.Show("确定要删除这个节点吗?", "提示", MessageBoxButton.OKCancel);
                    if (dr == MessageBoxResult.OK)
                    {
                        DeleteTheNode(nodeList, currentNode);
                        MessageBox.Show("成功删除节点!");
                    }
                    else
                    {
                        return;
                    }
                }
            }
        }

其中删除节点需要使用下面的DeleteTheNode()方法,该方法使用递归思想实现删除指定节点的逻辑。

        private void DeleteTheNode(List<Node> nodeList, Node deleteNode)
        {
            foreach (Node node in nodeList)
            {
                if (node.Nodes != null && node.Nodes.Count > 0)
                {
                    DeleteTheNode(node.Nodes, deleteNode);
                }
                if (node == deleteNode)
                {
                    node.IsDeleted = true;
                }
            }
        }

不管是删除节点还是新增节点,都需要获取当前选中的节点,使用FindTheNode()方法实现。

        private Node FindTheNode(List<Node> nodeList, string nodeId)
        {
            Node findedNode = null;
            foreach (Node node in nodeList)
            {
                if (node.Nodes != null && node.Nodes.Count > 0)
                {
                    if ((findedNode = FindTheNode(node.Nodes, nodeId)) != null)
                    {
                        return findedNode;
                    }
                }
                if (node.NodeId == nodeId)
                {
                    return node;
                }
            }
            return findedNode;
        }

4、处理PreviewMouseRightButtonDown事件
因为叶子节点不支持增加节点操作,非叶子结点不支持删除节点操作,所以在点击鼠标右键时,需要检测选中节点的类型并进行控制。
为了实现这个功能,可以为TreeView绑定事件PreviewMouseRightButtonDown,在事件处理器中进行相应的控制即可。

     <TreeView.ItemContainerStyle>
                <Style TargetType="{x:Type TreeViewItem}">
                    <EventSetter Event="TreeViewItem.PreviewMouseRightButtonDown" Handler="TreeViewItem_PreviewMouseRightButtonDown"/>
                </Style>
     </TreeView.ItemContainerStyle>

下面是PreviewMouseRightButtonDown事件的事件处理器代码。

        private void TreeViewItem_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            var treeViewItem = VisualUpwardSearch<TreeViewItem>(e.OriginalSource as DependencyObject) as TreeViewItem;
            if (treeViewItem != null)
            {
                Node currentNode = treeViewItem.Header as Node;
                if (currentNode.NodeType != NodeType.LeafNode)
                {
                    MenuItem_AddNode.IsEnabled = true;
                    MenuItem_DeleteNode.IsEnabled = false;
                }
                else
                {
                    MenuItem_AddNode.IsEnabled = false;
                    MenuItem_DeleteNode.IsEnabled = true;
                }
                treeViewItem.Focus();
                e.Handled = true;
            }
        }

代码中使用到VisualUpwardSearch()方法,该方法用于寻找控件的最外层父控件。

        private DependencyObject VisualUpwardSearch<T>(DependencyObject source)
        {
            while (source != null && source.GetType() != typeof(T))
            {
                source = VisualTreeHelper.GetParent(source);
            }
            return source;
        }

5、查找下一个节点
有时候,还需要实现循环“查找下一个”的功能,在画面中增加输入框和按钮。

<TextBox x:Name="TextBox_FindNextNode" Grid.Column="1" HorizontalAlignment="Left" Height="23" Margin="49,20,-160,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" LostFocus="TextBox_FindNextNode_LostFocus"/>
<Button x:Name="Button_FindNextNode" Content="查找下一个" Grid.Column="1" HorizontalAlignment="Left" Margin="188,20,-254,0" VerticalAlignment="Top" Width="75" Click="Button_FindNextNode_Click"/>

为了实现循环“查找下一个”,定义了下面两个对象。

private int nextSearchedNodeIndex = -1;
public List<Node> searchedNodeList { get; set; }

searchedNodeList 记录根据关键字查找到的所有匹配的节点,nextSearchedNodeIndex 记录当前查看节点的位置。
搜索框失去焦点时,查找匹配的节点并保存至searchedNodeList ,同时将索引置为-1。

        private void TextBox_FindNextNode_LostFocus(object sender, RoutedEventArgs e)
        {
            if (!string.IsNullOrEmpty(this.TextBox_FindNextNode.Text.Trim()))
            {
                nextSearchedNodeIndex = -1;
                searchedNodeList = FindMatchedNodes(nodeList, this.TextBox_FindNextNode.Text.Trim());
            }
        }

点击“查找下一个”按钮,新增nextSearchedNodeIndex ,再使用nextSearchedNodeIndex 从searchedNodeList拿到当前展示的节点。这里需要注意的是一轮循环结束后,nextSearchedNodeIndex 再次置为-1,循环重新开始。

        private void Button_FindNextNode_Click(object sender, RoutedEventArgs e)
        {
            if (string.IsNullOrEmpty(this.TextBox_FindNextNode.Text.Trim()))
            {
                MessageBox.Show("请输入查找条件!");
                return;
            }
            if (searchedNodeList != null && searchedNodeList.Count > 0)
            {
                //循环查找下一个
                if (nextSearchedNodeIndex >= searchedNodeList.Count - 1)
                {
                    nextSearchedNodeIndex = -1;
                }
                nextSearchedNodeIndex += 1;
                selectedNode = searchedNodeList[nextSearchedNodeIndex];
                SelectTheCurrentNode();
            }
            else
            {
                MessageBox.Show("没有找到满足条件的节点");
            }
        }

其中,查找匹配节点的逻辑由FindMatchedNodes()方法完成,该方法同样使用递归思想来实现。

        private List<Node> FindMatchedNodes(List<Node> srcNodeList, string filterString)
        {
            List<Node> destNodeList = new List<Node>();
            foreach (Node node in srcNodeList)
            {
                if (node.NodeName.Contains(filterString))
                {
                    destNodeList.Add(node);
                }
                if (node.Nodes != null && node.Nodes.Count > 0)
                {
                    destNodeList.AddRange(FindMatchedNodes(node.Nodes, filterString));
                }
            }
            return destNodeList;
        }

其中,SelectTheCurrentNode()方法实现选中当前节点,并将焦点置于当前选中节点。这里同样离不开递归的功劳。

        private void SelectTheCurrentNode()
        {
            if (this.TreeView_NodeList.Items != null && this.TreeView_NodeList.Items.Count > 0)
            {
                foreach (var item in this.TreeView_NodeList.Items)
                {
                    DependencyObject dependencyObject = this.TreeView_NodeList.ItemContainerGenerator.ContainerFromItem(item);
                    if (dependencyObject != null)//第一次打开程序,dependencyObject为null,会出错
                    {
                        TreeViewItem tvi = (TreeViewItem)dependencyObject;
                        if ((tvi.Header as Node).NodeId == selectedNode.NodeId)
                        {
                            tvi.IsSelected = true;
                            tvi.Focus();
                        }  
                        else
                        {
                            SetNodeSelected(tvi);
                        }
                    }
                }
            }
        }
        private void SetNodeSelected(TreeViewItem Item)
        {
            foreach (var item in Item.Items)
            {
                DependencyObject dependencyObject = Item.ItemContainerGenerator.ContainerFromItem(item);
                if (dependencyObject != null)
                {
                    TreeViewItem treeViewItem = (TreeViewItem)dependencyObject;
                    if ((treeViewItem.Header as Node).NodeId == selectedNode.NodeId)
                    {
                        treeViewItem.IsSelected = true;
                        treeViewItem.Focus();
                    }
                    else
                    {
                        treeViewItem.IsSelected = false;
                        if (treeViewItem.HasItems)
                        {
                            SetNodeSelected(treeViewItem);
                        }
                    }
                }
            }
        }

6、处理SelectedItemChanged事件
操作TreeView,不然会选中节点,触发SelectedItemChanged事件,下面是SelectedItemChanged事件的处理代码。

        private void TreeView_NodeList_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            Node node = this.TreeView_NodeList.SelectedItem as Node;
            if (node != null)
            {
                selectedNode = node;
                MessageBox.Show("当前选中的节点是:" + selectedNode.NodeName);
            }
        }

本文结合shil介绍了TreeView层级数据源的绑定,选中事件的处理,展开节点、新增节点、删除节点、查找节点等常见操作。
普遍使用到递归思想,是学习递归的完美练兵场。

  • 10
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
WPF(Windows Presentation Foundation)是微软的一种用户界面技术,可用于创建各种Windows应用程序。TreeViewWPF中的一种控件,用于显示层次结构的数据。 当我们需要自定义TreeView时,可以使用WPF提供的各种功能和特性来实现。下面是一些实现自定义TreeView的方法: 1. 自定义树节点的外观:可以使用WPF的样式和模板来修改树节点的外观,例如更改节点的背景颜色、字体样式等。可以使用TreeView控件的ItemTemplate属性来为每个节点定义一个数据模板。 2. 添加功能按钮:可以为TreeView控件添加自己需要的功能按钮,比如展开/折叠按钮、添加/删除节点按钮等。可以使用WPF中的Button控件实现这些按钮,并与TreeView的事件关联。 3. 添加交互行为:可以通过使用WPF提供的命令功能,为TreeView控件添加交互行为。例如,可以为每个节点添加一个命令,当用户双击节点时执行该命令。 4. 实现节点的拖拽和放置:可以通过使用WPF的拖放功能,实现节点的拖拽和放置。可以为每个节点添加一个DragStarted事件,当用户拖动节点时触发该事件,并将节点的数据作为数据对象传递给拖动操作。同时,可以使用TreeViewDropTarget控件作为拖放目标,通过处理Drop事件实现节点的放置。 总的来说,WPF提供了丰富的功能和特性,使我们能够很容易地自定义TreeView控件。通过使用WPF的样式、模板、命令和拖放功能,我们可以实现一个完全符合我们需求的TreeView控件,并为用户提供更好的用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值