C# WinForms树视图检查/取消选中层次结构

C# WinForms树视图检查/取消选中层次结构,c#,.net,winforms,treeview,.net-2.0,C#,.net,Winforms,Treeview,.net 2.0,以下代码旨在根据需要递归检查或取消检查父节点或子节点 例如,在此位置,如果取消选中其中任何一个节点,则必须取消选中A、G、L和T节点 下面代码的问题是,每当我双击任何节点时,算法都无法达到其目的 树搜索算法从这里开始: // stack is used to traverse the tree iteratively. Stack<TreeNode> stack = new Stack<TreeNode>(); private void tre

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

例如,在此位置,如果取消选中其中任何一个节点,则必须取消选中AGLT节点

下面代码的问题是,每当我双击任何节点时,算法都无法达到其目的

树搜索算法从这里开始:

    // 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;
    }
预计会发生:

看一下应用程序的屏幕截图AGLT被选中。如果我取消选中,比如说,L
-T应取消选中,因为TL的子项
-GA应取消选中,因为它们将没有剩余的子项

发生了什么:

如果单击任何节点,此应用程序代码都可以正常工作。如果双击某个节点,该节点将处于选中/取消选中状态,但相同的更改不会反映在父节点和子节点上

双击还会将应用程序冻结一段时间


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

以下是需要解决的主要问题:

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

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

  • 修复
    双击
    树视图中的复选框bug

    双击
    树视图中的
    复选框时,
    节点
    选中的
    值将更改两次,并将在双击前设置为原始状态,但
    后检查
    事件将引发一次

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

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

  • 实现算法

    在解决上述问题后,正确的算法将产生我们所期望的单击结果。以下是期望:

    选中/取消选中节点时:

    • 该节点的所有子节点都应更改为相同的检查状态
    • 如果检查了祖先节点的子节点中至少有一个子节点,则应检查祖先节点中的所有节点,否则应取消选中
在我们解决了上述问题并创建了
子体
祖先
来遍历树之后,我们就可以处理
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);
});
下载

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

详细答案 防止
AfterCkeck
事件处理程序递归重复逻辑

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

要防止多次引发事件,请将逻辑添加到 事件处理程序,仅在
树的
操作
属性未设置为
TreeView.Unknown

修复
双击
树视图中的复选框bug

如中所述,
TreeView
中有一个bug,当您双击
TreeView
中的
复选框时,
节点的
选中的
值将更改两次,并将在双击之前设置为原始状态,但
后检查
事件将引发一次

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

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);
    }
}
获取节点后代和祖先的扩展方法

为了获得节点的后代和祖先,我们需要创建一些扩展方法,以便在
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;
        }
    }
}
示例

要测试解决方案,您可以使用以下数据填充
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();
}

.NET2支持 由于.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;
            }
        }
    }
}

如果是关于检查/取消检查,那么最好依靠
AfterCheck
事件,而不是鼠标事件。另请看.@yahoo.com:这是我今天从你那里看到的第二个问题,它确实没有提供足够的细节。请非常具体地描述您期望发生的事情和实际发生的事情。“…每当我双击任何节点时,都会将其视为一次单击,算法会崩溃。”-您没有指定您期望发生的事情。此外,“算法崩溃”并没有告诉我们发生了什么。您可以在检查后断开
事件处理程序。因此,双击的第一次单击将关闭它,而双击的第二次单击发生在第一次单击的处理过程中。已断开连接的事件处理程序不会在第二次单击时触发。您可能会考虑使用BooL标志而不是断开处理程序,并将每一个“点击”添加到点击事件堆栈中。如果你
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);
        });
    }
}
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();
}
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;
            }
        }
    }
}
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;
        };
    }
}