Winform使用TreeView控件实现带复选框的三态树

刚接触c#,项目中用到Treeview实现三态,即选中、未选中、半选中状态,并且要求父子节点选择联动,效果类似下图

Winform使用TreeView控件实现带复选框的三态树_第1张图片


网上有很多例子,于是研究了一下,结合网上大佬们的例子,所以在这里记录一下。


首先新建一个Winform工程拖个TreeView控件,然后运行。。。

Winform使用TreeView控件实现带复选框的三态树_第2张图片

嗯。。。加点数据吧。

private void CreateTree()
{
    TreeNode anime = new TreeNode("2018一月新番");
            
    var china = anime.Nodes.Add("国创");
    pointer = china.Nodes.Add("一人之下");
            
    pointer = china.Nodes.Add("狐妖小红娘");
    pointer = china.Nodes.Add("妖神记");
    pointer = china.Nodes.Add("斗罗大陆");

    var nihonn = anime.Nodes.Add("日本");
    pointer = nihonn.Nodes.Add("OVERLORD");
    pointer = nihonn.Nodes.Add("龙王的工作");
    pointer = nihonn.Nodes.Add("紫罗兰永恒花园");
    pointer = nihonn.Nodes.Add("魔卡少女樱");
    pointer = nihonn.Nodes.Add("博多豚骨拉面团");

    this.LaoYeZha_TreeView.Nodes.Add(anime);
}

运行。。

Winform使用TreeView控件实现带复选框的三态树_第3张图片

ok,然后考虑节点选择联动,先考虑选中一个节点,子节点全选中,取消选择,子节点全都不选中。

添加treeview的AfterCheck事件。

private void LaoYeZha_TreeView_AfterCheck(object sender, TreeViewEventArgs e)
{
    //只处理鼠标点击引起的状态变化
    if(e.Action == TreeViewAction.ByMouse)
    {
        //更新子节点状态
        UpdateChildNodes(e.Node);
    }
}

private void UpdateChildNodes(TreeNode node)
{
    foreach (TreeNode child in node.Nodes)
    {
        child.Checked = node.Checked;
        UpdateChildNodes(child);
    }
}

现在实现了子节点联动了,接下来是父节点

在AfterCheck事件中添加方法

private void LaoYeZha_TreeView_AfterCheck(object sender, TreeViewEventArgs e)
{
    //只处理鼠标点击引起的状态变化
    if(e.Action == TreeViewAction.ByMouse)
    {
        //更新子节点状态
        UpdateChildNodes(e.Node);
        //更新父节点状态
        UpdateParents(e.Node);
    }
}
private void UpdateParents(TreeNode node)
{
    var parent = node.Parent;
    while (parent != null)
    {
        //设置父节点状态
        SetNodeState(parent);
        parent = parent.Parent;
    }
}

private void SetNodeState(TreeNode parent)
{
    throw new NotImplementedException();
}

接下来就是SetNodeState方法怎么写了,由于treeview的节点只有选中和不选中两种状态,没有半选中状态,因此我强行定义半选中状态为属性checked = false,ForeColor = Blue,自然选中和未选中状态的ForeColor = Black

从父节点的角度来说,这个节点的状态取决与子节点状态,

如果子节点全部选中,则为选中状态

如果子节点有选中,也有没选中,则为半选中状态

如果子节点全未选中,由于上面定义了节点的半选中状态是checked = false,因此如果子节点全未选中,还要判断子节点中有无半选中的节点,如果有,父节点是半选中状态,否则父节点是不选中状态

试着写SetNodeState方法。

private void SetNodeState(TreeNode parent)
{
    if(parent.Nodes.IsAllChecked())
    {
        //子节点全选中
        parent.Checked = true;
        parent.ForeColor = Color.Black;
    }
    else if(parent.Nodes.IsAllUnChecked())
    {
        //子节点全未选中
        parent.Checked = false;
        parent.ForeColor = Color.Black;
        //还要判断子节点中是否有半选中状态
        foreach (TreeNode child in parent.Nodes)
        {
            if (child.ForeColor == Color.Blue)
            {
                //用蓝色标记半选中状态
                parent.ForeColor = Color.Blue;
                break;
            }
        }
    }
    else
    {
        //子节点有的选中有的未选中
        parent.Checked = false;
        parent.ForeColor = Color.Blue;
    }
}

其中IsAllChecked和IsAllUnChecked只是假设存在的方法 ,逻辑就是判断子节点是否全选或者全没选。

由于节点上加了颜色属性,因此需要再修改下面的方法

private void LaoYeZha_TreeView_AfterCheck(object sender, TreeViewEventArgs e)
{
    //只处理鼠标点击引起的状态变化
    if(e.Action == TreeViewAction.ByMouse)
    {
        e.Node.ForeColor = Color.Black;
        //更新子节点状态
        UpdateChildNodes(e.Node);
        //更新父节点状态
        UpdateParents(e.Node);
    }
}
private void UpdateChildNodes(TreeNode node)
{
    foreach (TreeNode child in node.Nodes)
    {
        child.Checked = node.Checked;
        child.ForeColor = Color.Black;
        UpdateChildNodes(child);
    }
}

接下来实现IsAllChecked和IsAllUnChecked方法,为了偷懒,我给TreeNode.Nodes类加了两个扩展方法

static class TreeNodeCollectionExtern
{
    public static bool IsAllChecked(this TreeNodeCollection treeNodeCollection)
    {
        foreach (TreeNode node in treeNodeCollection)
        {
            if(node.Checked == false)
            {
                return false;
            }
        }
        return true;
    }

    public static bool IsAllUnChecked(this TreeNodeCollection treeNodeCollection)
    {
        foreach (TreeNode node in treeNodeCollection)
        {
            if (node.Checked == true)
            {
                return false;
            }
        }
        return true;
    }
}

ok,现在基本功能已经实现了,点击一个节点,子节点会相应变化,父节点的checkBox框也会变化(选中 是打勾状态,不选中没有勾,半选中没有勾并且节点变蓝)

最后一步是把半选中的checkBox框加上半选中图标。添加treeview的DrawNode事件

private void LaoYeZha_TreeView_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
    var treeview = sender as TreeView;
    var brush = Brushes.Black;
    if (e.Node.ForeColor == Color.Blue)
    {
        var location = e.Node.Bounds.Location;
        location.Offset(-11, 2);
        var size = new Size(9, 9);
        brush = Brushes.Blue;
        e.Graphics.FillRectangle(brush, new Rectangle(location,size));
    }
    //绘制文本
    e.Graphics.DrawString(e.Node.Text, treeview.Font, brush, e.Bounds.Left, e.Bounds.Top);
}

使这个事件有效需要设置TreeView的DrawMode属性为 TreeViewDrawMode.OwnerDrawText或者TreeViewDrawMode.OwnerDrawAll

因此在构造函数里添加

this.LaoYeZha_TreeView.DrawMode = System.Windows.Forms.TreeViewDrawMode.OwnerDrawText;

运行,ok,没有问题。


如果发现展开树节点的时候发现treeview左上角出现很多重叠的字,不好意思,我也不知道是为什么,求大佬解决方案。

我个人有两个暂定方案。

①添加treeview的AfterExpand事件调用tree的refrash方法

private void LaoYeZha_TreeView_AfterExpand(object sender, TreeViewEventArgs e)
{
    LaoYeZha_TreeView.Refresh();
}

②出现重叠肯定是DrawString的问题,通过打断点发现调用DrawString是有时候坐标在左上角,所以修改DrawNode事件

private void LaoYeZha_TreeView_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
    if (e.Bounds.Location.X <= 0)
    {
        return;
    }

    var treeview = sender as TreeView;
    var brush = Brushes.Black;
    if (e.Node.ForeColor == Color.Blue)
    {
        var location = e.Node.Bounds.Location;
        location.Offset(-11, 2);
        var size = new Size(9, 9);
        brush = Brushes.Blue;
        e.Graphics.FillRectangle(brush, new Rectangle(location,size));
    }
    //绘制文本
    e.Graphics.DrawString(e.Node.Text, treeview.Font, brush, e.Bounds.Left, e.Bounds.Top);
}

以上是我的暂定解决方案,由于我也不是太懂,所以有大佬知道的话,求教。

另外还有一个问题是当双击某个节点时,treeview之响应了一次AfterCheck事件,这个怎么避免我也不知道,求大神指点。

目前只知道继承TreeView忽略双击事件或转换为单击事件等可以解决。。。


最后主界面源代码,仅供参考

 public partial class Form1 : Form
    {

        public Form1()
        {
            InitializeComponent();
        }

        private TreeNode pointer
        {
            set
            {
                if (value != null)
                {
                    CreateEpisode(value);
                }
            }
        }
        private void CreateTree()
        {
            TreeNode anime = new TreeNode("2018一月新番");

            var china = anime.Nodes.Add("国创");
            pointer = china.Nodes.Add("一人之下");

            pointer = china.Nodes.Add("狐妖小红娘");
            pointer = china.Nodes.Add("妖神记");
            pointer = china.Nodes.Add("斗罗大陆");

            var nihonn = anime.Nodes.Add("日本");
            pointer = nihonn.Nodes.Add("OVERLORD");
            pointer = nihonn.Nodes.Add("龙王的工作");
            pointer = nihonn.Nodes.Add("紫罗兰永恒花园");
            pointer = nihonn.Nodes.Add("魔卡少女樱");
            pointer = nihonn.Nodes.Add("博多豚骨拉面团");

            this.LaoYeZha_TreeView.Nodes.Add(anime);
        }

        Random r = new Random();
        private void CreateEpisode(TreeNode node)
        {
            for (int i = 1; i <= r.Next(1, 10); i++)
            {
                node.Nodes.Add($"第{i}集");
            }
        }


        private void Form1_Load(object sender, EventArgs e)
        {
            CreateTree();
        }

        private void LaoYeZha_TreeView_AfterCheck(object sender, TreeViewEventArgs e)
        {
            //只处理鼠标点击引起的状态变化
            if (e.Action == TreeViewAction.ByMouse)
            {
                e.Node.ForeColor = Color.Black;
                //更新子节点状态
                UpdateChildNodes(e.Node);
                //更新父节点状态
                UpdateParents(e.Node);
            }
        }
        private void UpdateChildNodes(TreeNode node)
        {
            foreach (TreeNode child in node.Nodes)
            {
                child.Checked = node.Checked;
                child.ForeColor = Color.Black;
                UpdateChildNodes(child);
            }
        }

        private void UpdateParents(TreeNode node)
        {
            var parent = node.Parent;
            while (parent != null)
            {
                //设置父节点状态
                SetNodeState(parent);
                parent = parent.Parent;
            }
        }


        private void SetNodeState(TreeNode parent)
        {
            if (parent.Nodes.IsAllChecked())
            {
                //子节点全选中
                parent.Checked = true;
                parent.ForeColor = Color.Black;
            }
            else if (parent.Nodes.IsAllUnChecked())
            {
                //子节点全未选中
                parent.Checked = false;
                parent.ForeColor = Color.Black;
                //还要判断子节点中是否有半选中状态
                foreach (TreeNode child in parent.Nodes)
                {
                    if (child.ForeColor == Color.Blue)
                    {
                        //用蓝色标记半选中状态
                        parent.ForeColor = Color.Blue;
                        break;
                    }
                }
            }
            else
            {
                //子节点有的选中有的未选中
                parent.Checked = false;
                parent.ForeColor = Color.Blue;
            }
        }

        private void LaoYeZha_TreeView_DrawNode(object sender, DrawTreeNodeEventArgs e)
        {
            if (e.Bounds.Location.X <= 0)
            {
                return;
            }

            var treeview = sender as TreeView;
            var brush = Brushes.Black;
            if (e.Node.ForeColor == Color.Blue)
            {
                var location = e.Node.Bounds.Location;
                location.Offset(-11, 2);
                var size = new Size(9, 9);
                brush = Brushes.Blue;
                e.Graphics.FillRectangle(brush, new Rectangle(location, size));
            }
            //绘制文本
            e.Graphics.DrawString(e.Node.Text, treeview.Font, brush, e.Bounds.Left, e.Bounds.Top);
        }

        private void LaoYeZha_TreeView_AfterExpand(object sender, TreeViewEventArgs e)
        {
            LaoYeZha_TreeView.Refresh();
        }
    }
如果有错误或不足的地方,请各位大神积极指正,谢谢。






你可能感兴趣的:(c#,Winform,娱乐)