WinForms TreeView检查/取消选中层次结构

use*_*312 12 .net c# treeview .net-2.0 winforms

以下代码旨在根据需要以递归方式检查或取消检查父节点或子节点.

在此输入图像描述

例如,在此位置,如果我们取消选中其中任何一个节点,则必须取消选中A,G,LT节点.

在此输入图像描述

以下代码的问题是,每当我双击任何节点时,算法都无法实现其目的.

树搜索算法从这里开始:

    // stack is used to traverse the tree iteratively.
    Stack<TreeNode> stack = new Stack<TreeNode>();
    private void treeView1_AfterCheck(object sender, TreeViewEventArgs e)
    {
        TreeNode selectedNode = e.Node;
        bool checkedStatus = e.Node.Checked;

        // suppress repeated even firing
        treeView1.AfterCheck -= treeView1_AfterCheck;

        // traverse children
        stack.Push(selectedNode);

        while(stack.Count > 0)
        {
            TreeNode node = stack.Pop();

            node.Checked = checkedStatus;                

            System.Console.Write(node.Text + ", ");

            if (node.Nodes.Count > 0)
            {
                ICollection tnc = node.Nodes;

                foreach (TreeNode n in tnc)
                {
                    stack.Push(n);
                }
            }
        }

        //traverse parent
        while(selectedNode.Parent!=null)
        {
            TreeNode node = selectedNode.Parent;

            node.Checked = checkedStatus;

            selectedNode = selectedNode.Parent;
        }

        // "suppress repeated even firing" ends here
        treeView1.AfterCheck += treeView1_AfterCheck;

        string str = string.Empty;
    }
Run Code Online (Sandbox Code Playgroud)

司机计划

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        #region MyRegion
        private void button1_Click(object sender, EventArgs e)
        {
            TreeNode a = new TreeNode("A");
            TreeNode b = new TreeNode("B");
            TreeNode c = new TreeNode("C");
            TreeNode d = new TreeNode("D");
            TreeNode g = new TreeNode("G");
            TreeNode h = new TreeNode("H");
            TreeNode i = new TreeNode("I");
            TreeNode j = new TreeNode("J");
            TreeNode k = new TreeNode("K");
            TreeNode l = new TreeNode("L");
            TreeNode m = new TreeNode("M");
            TreeNode n = new TreeNode("N");
            TreeNode o = new TreeNode("O");
            TreeNode p = new TreeNode("P");
            TreeNode q = new TreeNode("Q");
            TreeNode r = new TreeNode("R");
            TreeNode s = new TreeNode("S");
            TreeNode t = new TreeNode("T");
            TreeNode u = new TreeNode("U");
            TreeNode v = new TreeNode("V");
            TreeNode w = new TreeNode("W");
            TreeNode x = new TreeNode("X");
            TreeNode y = new TreeNode("Y");
            TreeNode z = new TreeNode("Z");

            k.Nodes.Add(x);
            k.Nodes.Add(y);

            l.Nodes.Add(s);
            l.Nodes.Add(t);
            l.Nodes.Add(u);

            n.Nodes.Add(o);
            n.Nodes.Add(p);
            n.Nodes.Add(q);
            n.Nodes.Add(r);

            g.Nodes.Add(k);
            g.Nodes.Add(l);

            i.Nodes.Add(m);
            i.Nodes.Add(n);


            j.Nodes.Add(b);
            j.Nodes.Add(c);
            j.Nodes.Add(d);

            a.Nodes.Add(g);
            a.Nodes.Add(h);
            a.Nodes.Add(i);
            a.Nodes.Add(j);

            treeView1.Nodes.Add(a);
            treeView1.ExpandAll();

            button1.Enabled = false;
        } 
        #endregion
Run Code Online (Sandbox Code Playgroud)

预计会发生:

看一下应用程序的屏幕截图.,G ^,大号Ť被检查.如果我取消选中,比如说L,
- T应该是未选中的,因为TL的孩子.
- 应该取消选中GA,因为他们没有孩子.

怎么了:

如果我单击任何节点,此应用程序代码工作正常.如果我双击某个节点,该节点将变为选中/取消选中,但相同的更改不会反映在父节点和子节点上.

双击也会冻结应用程序一段时间.

如何解决此问题并获得预期的行为?

Rez*_*aei 12

这些是这里要解决的主要问题:

  • 防止AfterCkeck事件处理程序递归地重复逻辑.

    当您更改Checked节点的属性时AfterCheck,它会导致另一个AfterCheck事件,这可能导致我们的堆栈溢出或至少在检查事件后不必要或我们的算法中的不可预测的结果.

  • 修复DoubleClick了复选框错误TreeView.

    当您双击CheckBoxin时TreeView,该Checked值的值Node将更改两次,并且在双击之前将设置为原始状态,但该AfterCheck事件将引发一次.

  • 获取节点的后代和祖先的扩展方法

    我们需要创建方法来获取节点的后代和祖先.为此,我们将为TreeNode类创建扩展方法.

  • 实现算法

    解决上述问题后,正确的算法将导致我们期望的点击.这是期望:

    检查/取消选中节点时:

    • 该节点的所有后代都应更改为相同的检查状态.
    • 如果在其后代中至少有一个子项被选中,则应检查祖先中的所有节点,否则应取消选中.

在我们解决了上述问题并创建DescendantsAncestors遍历树之后,我们就足以处理AfterCheck事件并具有以下逻辑:

e.Node.Descendants().ToList().ForEach(x =>
{
    x.Checked = e.Node.Checked;
});
e.Node.Ancestors().ToList().ForEach(x =>
{
    x.Checked = x.Descendants().ToList().Any(y => y.Checked);
});
Run Code Online (Sandbox Code Playgroud)

下载

您可以从以下存储库下载工作示例:

详细解答

防止AfterCkeck事件处理程序递归地重复逻辑

事实上,我们不会阻止AfterCheck事件处理程序提升AfterCheck.相反,我们检测AfterCheck是由用户还是由处理程序内的代码引发的.为此,我们可以检查Action事件arg的属性:

要防止多次引发事件,请向事件处理程序添加逻辑,该事件处理程序仅ActionTreeViewEventArgs未设置为 的属性时才执行递归代码 TreeViewAction.Unknown.

private void exTreeView1_AfterCheck(object sender, TreeViewEventArgs e)
{
    if (e.Action != TreeViewAction.Unknown)
    {
        // Changing Checked
    }
}
Run Code Online (Sandbox Code Playgroud)

修复DoubleClick了复选框错误TreeView

正如也提到这个帖子,有一个错误TreeView,当你双击CheckBoxTreeView,该CheckedNode将两次改变之前双击将被设置为原来的状态,但AfterCheck事件会引发一次.

要解决此问题,您可以查看WM_LBUTTONDBLCLK消息并检查是否双击复选框,忽略它:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
public class ExTreeView : TreeView
{
    private const int WM_LBUTTONDBLCLK = 0x0203;
    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_LBUTTONDBLCLK)
        {
            var info = this.HitTest(PointToClient(Cursor.Position));
            if (info.Location == TreeViewHitTestLocations.StateImage)
            {
                m.Result = IntPtr.Zero;
                return;
            }
        }
        base.WndProc(ref m);
    }
}
Run Code Online (Sandbox Code Playgroud)

获取节点的后代和祖先的扩展方法

要获取节点的后代和祖先,我们需要创建一些扩展方法AfterCheck来实现该算法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
public static class Extensions
{
    public static List<TreeNode> Descendants(this TreeView tree)
    {
        var nodes = tree.Nodes.Cast<TreeNode>();
        return nodes.SelectMany(x => x.Descendants()).Concat(nodes).ToList();
    }
    public static List<TreeNode> Descendants(this TreeNode node)
    {
        var nodes = node.Nodes.Cast<TreeNode>().ToList();
        return nodes.SelectMany(x => Descendants(x)).Concat(nodes).ToList();
    }
    public static List<TreeNode> Ancestors(this TreeNode node)
    {
        return AncestorsInternal(node).ToList();
    }
    private static IEnumerable<TreeNode> AncestorsInternal(TreeNode node)
    {
        while (node.Parent != null)
        {
            node = node.Parent;
            yield return node;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

实现算法

使用上面的扩展方法,我将处理AfterCheck事件,因此当您选中/取消选中一个节点时:

  • 该节点的所有后代都应更改为相同的检查状态.
  • 应该检查祖先中的所有节点是否在列表中检查了其后代中的一个子节点,否则应该取消选中.

这是实施:

private void exTreeView1_AfterCheck(object sender, TreeViewEventArgs e)
{
    if (e.Action != TreeViewAction.Unknown)
    {
        e.Node.Descendants().ToList().ForEach(x =>
        {
            x.Checked = e.Node.Checked;
        });
        e.Node.Ancestors().ToList().ForEach(x =>
        {
            x.Checked = x.Descendants().ToList().Any(y => y.Checked);
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

要测试解决方案,您可以填写TreeView以下数据:

private void Form1_Load(object sender, EventArgs e)
{
    exTreeView1.Nodes.Clear();
    exTreeView1.Nodes.AddRange(new TreeNode[] {
        new TreeNode("1", new TreeNode[] {
                new TreeNode("11", new TreeNode[]{
                    new TreeNode("111"),
                    new TreeNode("112"),
                }),
                new TreeNode("12", new TreeNode[]{
                    new TreeNode("121"),
                    new TreeNode("122"),
                    new TreeNode("123"),
                }),
        }),
        new TreeNode("2", new TreeNode[] {
                new TreeNode("21", new TreeNode[]{
                    new TreeNode("211"),
                    new TreeNode("212"),
                }),
                new TreeNode("22", new TreeNode[]{
                    new TreeNode("221"),
                    new TreeNode("222"),
                    new TreeNode("223"),
                }),
        })
    });
    exTreeView1.ExpandAll();
}
Run Code Online (Sandbox Code Playgroud)

.NET 2支持

由于.NET 2没有linq扩展方法,对于那些有兴趣在.NET 2中使用该功能的人(包括原始海报),这里是.NET 2.0中的代码:

ExTreeView

using System;
using System.Collections.Generic;
using System.Windows.Forms;
public class ExTreeView : TreeView
{
    private const int WM_LBUTTONDBLCLK = 0x0203;
    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_LBUTTONDBLCLK) {
            var info = this.HitTest(PointToClient(Cursor.Position));
            if (info.Location == TreeViewHitTestLocations.StateImage) {
                m.Result = IntPtr.Zero;
                return;
            }
        }
        base.WndProc(ref m);
    }
    public IEnumerable<TreeNode> Ancestors(TreeNode node)
    {
        while (node.Parent != null) {
            node = node.Parent;
            yield return node;
        }
    }
    public IEnumerable<TreeNode> Descendants(TreeNode node)
    {
        foreach (TreeNode c1 in node.Nodes) {
            yield return c1;
            foreach (TreeNode c2 in Descendants(c1)) {
                yield return c2;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

AfterSelect

private void exTreeView1_AfterCheck(object sender, TreeViewEventArgs e)
{
    if (e.Action != TreeViewAction.Unknown) {
        foreach (TreeNode x in exTreeView1.Descendants(e.Node)) {
            x.Checked = e.Node.Checked;
        }
        foreach (TreeNode x in exTreeView1.Ancestors(e.Node)) {
            bool any = false;
            foreach (TreeNode y in exTreeView1.Descendants(x))
                any = any || y.Checked;
            x.Checked = any;
        };
    }
}
Run Code Online (Sandbox Code Playgroud)