新手入门WPF之TreeView控件(二)

        抱歉了各位,让你们久等了,现在我对WPF的理解更深了一步,那么,话不多说,动手开整。

        这次的的内容在上一章的基础上讲解,本章要实现TreeView的增删改查,既然想实现增删改查,那就需要重定义数据结构了,目前我在项目上碰到关于树的结构就两种,一种是递归形式的结构,一种是每个节点结构不同的非递归结构。

        先创建一个如图所示的项目,注意别创建错了,一个是启动项,一个是类库

        我就举个例子演示TreeView增删改查,不如,我想实现一个目录树,这个目录树只需要显示节点的名字就行,知道需求后,先创建一个基类,用于通知前端刷新,我取名为BindableObject,

它继承一个系统提供的通知改变的接口,看图

 可能有人会不理解BindableObject里面的代码,不急,最后补充的地方我会解释,你先这样理解:这是一个基类,我定义了一个字段(接口实现的)和一个方法(自定义的),以后就直接调用方法就能实现双向绑定。

         有了通知类后,现在就按照需求定义一个关于目录树结构的对象,如图

 让TreeModel继承通知这个基类,调用方法就能让该属性双向绑定了,因为是递归结构,所有还差一个属性(子节点)

    public class TreeModel : BindableObject
    {
        private string name;
        /// <summary>
        /// 节点名称
        /// </summary>
        public string Name
        {
            get => name;
            set
            {
                name = value;
                OnPropertyChanged();
            }
        }


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

 

关于它的介绍,上篇文章有说明。

        现在有了结构,但还缺少前端展示,那就创建如图所示的viewmodel和view 

 

 接着修改App里面的启动页面

运行测试下,看看有没有问题 ,能弹窗空白页面,那我们进行往下走。

        既然要实现目录树,那就先开始前端界面的布局,我这里的思路是把页面用grid布局,使页面

分成两行,上面一行是用StackPanel布局的按钮,下面一行是目录树

 

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <StackPanel/>
        <TreeView/>
    </Grid>

简单创建一个数据源,生成一个有内容的目录树

 

 

    public class TreeViewModel : BindableObject
    {
        #region 属性
        /// <summary>
        /// 目录树
        /// </summary>
        public ObservableCollection<TreeModel> TreeModels { get; set; } = new ObservableCollection<TreeModel>();

        public TreeViewModel()
        {
            InitTree();
        }

        /// <summary>
        /// 初始化目录树
        /// </summary>
        private void InitTree()
        {
            TreeModels.Add(new TreeModel()
            {
                Name = "我是根节点",
            });
        }
    }

把数据源绑定到前端运行一下,看看能否渲染出来 ,可以就继续。

        <TreeView
            x:Name="treeView"
            Grid.Row="1"
            ItemsSource="{Binding TreeModels}">
            </i:Interaction.Triggers>
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                    <Grid>
                        <TextBlock Text="{Binding Name}" />
                    </Grid>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>

        基本的容写好后,开始实现第一个功能,增加节点,怎么增加呢?怎么把触发的事件绑定到viewmodel呢?官方提供了一个接口ICommand,通过它,就能把viewmodel定义的命令绑定到前端,如图所示

    public class DelegateCommand : ICommand
    {
        private readonly Action _execute;
        private readonly Func<bool> _canExecute;

        public DelegateCommand(Action execute, Func<bool> canExecute = null)
        {
            if (execute == null)
                throw new ArgumentNullException(nameof(execute));

            _execute = execute;
            _canExecute = canExecute;
        }

        public virtual event EventHandler CanExecuteChanged;

        public virtual bool CanExecute(object parameter = null)
        {
            return _canExecute == null || _canExecute();
        }

        public virtual void Execute(object parameter = null)
        {
            _execute();
        }

    }

它还有个泛型,用于有参命令的实现

    public class DelegateCommand<T> : ICommand
    {
        private readonly Action<T> _execute;
        private readonly Func<T, bool> _canExecute;

        public DelegateCommand(Action<T> execute, Func<T, bool> canExecute = null)
        {
            if (execute == null)
                throw new ArgumentNullException(nameof(execute));

            _execute = execute;
            _canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            return _canExecute == null || _canExecute((T)parameter);
        }

        public void Execute(object parameter)
        {
            _execute((T)parameter);
        }

    }

这里也许也会有人迷惑,不急,这个我也放到补充地方说明,你可以这样理解,这个类它是一个管理所有命名的管理员,通过它,可以把前端产生的事件传到viewmodel,调用到指定命令下所调用的方法。

紧接着,绑定view的DataContext,

this.DataContext = new TreeViewModel();

在viewmodel创建一个命令,

在构造函数中实例化它,生成一个名为AddRoot的方法,

 

        /// <summary>
        /// 添加根节点
        /// </summary>
        /// <param name="obj"></param>
        /// <exception cref="NotImplementedException"></exception>
        private void AddRoot()
        {
            TreeModels.Add(new TreeModel()
            {
                Name = "我是根节点",
            }); ;
        }

 在前端定义一个按钮,绑定命令,如图所示

            <Button
                Margin="5"
                Command="{Binding DataContext.AddRootNode, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
                Content="添加根节点" />

 这里的绑定可能会有人不理解,没事,补充地方见,运行测试,如图所示,点击按钮后,会增加一个节点

 

         现在通过按钮是可以创建节点了,那我想要右键创建呢?我目前知道的有三种方法,这里我用通俗易懂的方法来讲解如何实现右键功能,由于现在treemodel只有一个属性,不好通过名字来显示右键对应的节点内容,比如在项目上,一般每个节点实现的右键功能不同,第一级节点有创建和删除,最后一级只有删除,没有创建,那怎么区分它是第一级还是最后一级呢?毫无疑问,可以通过添加一个属性,来区分每个节点的不同类型,所以我创建一个枚举。如图

    public enum TreeNodeType
    {
        [Description("根节点")]
        RootNode,
        [Description("子节点")]
        ChildNode
    }

 我的这个结构只有一级和无穷级,哈哈哈哈,所以每个节点的右键内容都是一样的,在treemodel中添加一个属性

 

        private TreeNodeType nodeType;
        /// <summary>
        /// 节点类型
        /// </summary>
        public TreeNodeType NodeType
        {
            get => nodeType;
            set
            {
                nodeType = value;
                OnPropertyChanged();
            }
        }

右键添加的解决了,那右键删除呢?我怎么判断我右键的节点在目录数中是属于谁的,所以咱们还需要一个用于记录这个节点属性谁的属性——父节点信息,那就再添加一个属性吧,如图

        public TreeModel? ParentNode { get; set; }

 这里有个坑,我不知道会有多少人踩,咱们补充见,哈哈哈哈。

        剩下最后一个功能的突破了,肯定有人会问,查这个功能呢?关于查这个概念,有两种理解,第一种是检索目录树(目录树很庞大的情况),还有一种是查看节点的所有信息,比如一个人的基本信息:身高、体重、性别等等,这里我就先理解为第二种,如果有人理解为第一种,给我留言,我后面扩展下,然后给你们讲解,如果是理解为第二种,那么我给你们留了一个思考题(看文字最后)。

        言归正传由于我定义的结构简单,目录树在前端的有效信息只有name,也就没必要查了。剩下的改,怎么改呢?默认的名字要怎么改呢?目前我会两种,我还是以通俗易懂的方法来讲解,至于另外一种,感兴趣的伙伴可以给我留言,我教你。

        既然要改,少不了输入框textbox,还记得咱们目录树的treeviewitem的结构吗?一个grid包着一个textblock,如果我再添加一个textbox,绑定同样的属性,会怎么样呢?肯定会乱成一锅粥呀,那如果我对textbox和textblock添加一个Visibility的属性,是不是从前端看是一个,实际上是有两个都在显示,看图

                    <Grid>
                        <TextBox
                            x:Name="textBox"
                            Text="{Binding Name}"
                            Visibility="Collapsed">                       
                         </TextBox>
                        <TextBlock Text="{Binding Name}" Visibility="Visible" />
                    </Grid>

 因此只有我们让textbox和textblock合理的显示和隐藏,就能实现改的功能了,那么,我们在去扩展两个属性吧,用来控制textbox和textblock的显示和隐藏,如图

 

        private Visibility isTextBoxVisibility = Visibility.Collapsed;
        /// <summary>
        /// 是否显示TextBox
        /// </summary>
        [JsonIgnore]
        public Visibility IsTextBoxVisibility
        {
            get => isTextBoxVisibility;
            set
            {
                isTextBoxVisibility = value;
                OnPropertyChanged();
            }
        }

        private Visibility isTextBlockVisibility = Visibility.Visible;
        /// <summary>
        /// 是否显示TextBlock
        /// </summary>
        [JsonIgnore]
        public Visibility IsTextBlockVisibility
        {
            get => isTextBlockVisibility;
            set
            {
                isTextBlockVisibility = value;
                OnPropertyChanged();
            }
        }

功能难点都突破了,那开始实现逻辑吧

        依次定义增删改的命令和方法 

        /// <summary>
        /// 添加子节点
        /// </summary>
        /// <param name="obj"></param>
        private void AddChild(TreeModel obj)
        {
            obj.Children.Add(new TreeModel()
            {
                Name = $"{obj.Name}的孩子",
                NodeType = Common.Enums.TreeNodeType.ChildNode,
                ParentNode = obj
            });
        }

        /// <summary>
        /// 删除节点
        /// </summary>
        /// <param name="obj"></param>
        private void Delete(TreeModel obj)
        {
            if (obj.NodeType == Common.Enums.TreeNodeType.RootNode)
            {
                TreeModels.Remove(obj);
                return;
            }

            obj.ParentNode.Children.Remove(obj);
        }

        /// <summary>
        /// 重命名节点
        /// </summary>
        /// <param name="obj"></param>
        /// <exception cref="NotImplementedException"></exception>
        private void ReName(TreeModel obj)
        {
            obj.IsTextBoxVisibility = Visibility.Visible;
            obj.IsTextBlockVisibility = Visibility.Collapsed;
        }

         前端绑定右键菜单

                    <HierarchicalDataTemplate.Triggers>
                        <DataTrigger Binding="{Binding NodeType}" Value="RootNode">
                            <Setter Property="ContextMenu" Value="{StaticResource root}" />
                        </DataTrigger>
                        <DataTrigger Binding="{Binding NodeType}" Value="ChildNode">
                            <Setter Property="ContextMenu" Value="{StaticResource child}" />
                        </DataTrigger>
                    </HierarchicalDataTemplate.Triggers>
    <Window.Resources>
        <ContextMenu x:Key="root">
            <MenuItem
                Command="{Binding DataContext.AddChildNode, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
                CommandParameter="{Binding}"
                Header="增加子节点" />
            <MenuItem
                Command="{Binding DataContext.DeleteNode, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
                CommandParameter="{Binding}"
                Header="删除节点" />
            <MenuItem
                Command="{Binding DataContext.ReNameNode, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
                CommandParameter="{Binding}"
                Header="重命名" />
        </ContextMenu>
        <ContextMenu x:Key="child">
            <MenuItem
                Command="{Binding DataContext.AddChildNode, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
                CommandParameter="{Binding}"
                Header="增加子节点" />
            <MenuItem
                Command="{Binding DataContext.DeleteNode, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
                CommandParameter="{Binding}"
                Header="删除节点" />
            <MenuItem
                Command="{Binding DataContext.ReNameNode, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
                CommandParameter="{Binding}"
                Header="重命名" />
        </ContextMenu>
    </Window.Resources>

运行测试,是不是发现有个问题,textbox要怎么消失呢?简单,有多种方法,事件焦点,键盘事件等等,我这里用的是enter表示我输入完了,同样定义一个命令,如图

 

 

        /// <summary>
        /// 完成重命名节点
        /// </summary>
        /// <param name="obj"></param>
        /// <exception cref="NotImplementedException"></exception>
        private void FinishReName(TreeModel obj)
        {
            obj.IsTextBoxVisibility = Visibility.Collapsed;
            obj.IsTextBlockVisibility = Visibility.Visible;
        }

再次运行测试,发现节点处会闪一下,有点奇怪,对不对?其实是,我们改了textbox内容后,textblock的内容还是原来的,并没有同步更新,所有我们要怎么解决同步的问题呢?text有一个事件TextChanged,可以实时获取用户输入的内容,那么怎么传给viewmodel呢?这里需要借助升级后的一个包Microsoft.Xaml.Behaviors.Wpf,添加命名空间    xmlns:i="http://schemas.microsoft.com/xaml/behaviors",这时又出来一个问题,我们怎么拿到对应的节点呢,如果拿不到,textbox和textblock的内容还是分开的,,这时我想到了可以通过右键的时候,获取对应的节点保存下来,成为全局变量,所以,咱们分两步,定义一个命令用于接收选择的节点,在定义一个命令用于实时同步改变的内容,如图所示

 

 

 

 

        /// <summary>
        /// 获取当前选择的节点
        /// </summary>
        /// <param name="obj"></param>
        /// <exception cref="NotImplementedException"></exception>
        private void RightUp(TreeView obj)
        {
            SelectedNode = obj.SelectedItem as TreeModel;
        }

        /// <summary>
        /// 实时更新节点名称
        /// </summary>
        /// <param name="obj"></param>
        /// <exception cref="NotImplementedException"></exception>
        private void Changed(TextBox obj)
        {
            SelectedNode.Name = obj.Text;
        }

对应前端的绑定

            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseRightButtonUp">
                    <i:InvokeCommandAction Command="{Binding DataContext.MouseRightButtonUp, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"
                                           CommandParameter="{Binding ElementName=treeView}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>


             <i:Interaction.Triggers>
                 <i:EventTrigger EventName="TextChanged">
                    <i:InvokeCommandAction Command="{Binding DataContext.TextChanged, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" 
                              CommandParameter="{Binding ElementName=textBox}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>

同时,记得修改代码(把上述的内容改为如图所示,这里的参数其实可以不需要,你们要改就自己改吧)

运行测试,看看能不能实现想要的效果,到此结束。

思考

        现在我有两个需求,第一个,如果我在treemodel添加一个description的属性来描述对应的节点(也就是查的第二种情况),请问在前端显示description的信息?第二个,如果有成千上万个节点,我不可能一个一个删除,请问通过按钮,怎么一键清空所有的目录树信息?

补充

1、BindableObject中,CallerMemberName的作用类似于nameof(),这样我们就没必要没掉用一次,写一遍nameof了,它执行的逻辑是,第一次运行时,都是null,没必要更新,如果对已有的进行更改,就会拿到最新的更改的信息,通过this指向的model更新对应的属性

2、DelegateCommand,理解了它的逻辑,也就明白了,首先它会判断每个命令是否可行,true为可行,false不可行,由于我们设置第二个参数为null,所以每次命令都为true,

之后点击按钮后,开始掉用执行体,也就是对应的方法

 3、JsonIgnore是Newtonsoft.Json包的一个用法,目的是忽略掉不需要保存的内容,至于为啥可以忽略,你们可以想想

4、关于绑定,这类用的是相对绑定,因为我们设置了DataContext是viewmodel,通过对象点的方式获取命令,不过有时并不能绑定上,需要主动的指定绑定源,AncestorType表示在谁身上找,Mode=FindAncestor表示找到一个就行

5、如果还有什么不明白,留言告诉我,当然,如果你们遇到有些自己实现不了的需要,欢迎跟我交流,我们一起进步,冲冲冲

结束

         这是我第二次写,后续我打算把一些好用的设计给你们展示下,这样吧,下一篇讲述如何在下拉框中实现一层一层的功能(描述不太准确),是对treeview的扩展应用

        还是希望各位大佬们批评指正

附赠我编写的源码GitHub - TQtong/TreeView(注意别提交代码哈)

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值