WinForms树形视图中的层次勾选/取消勾选

13
以下代码旨在递归检查或取消检查所需的父节点或子节点。

enter image description here

例如,在这个位置,如果我们取消选中其中任何一个节点,{{A}}、{{G}}、{{L}} 和 {{T}} 节点都必须被取消选中。

enter image description here

以下代码存在问题,每当我双击任何节点时,算法都无法实现其目的。
树搜索算法从这里开始:
    // 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;
    }

驱动程序

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

期望发生的事情:

请查看应用程序的屏幕截图。选中AGLT。如果我取消选择,比如说L,那么:
- T应该被取消选择,因为TL的子节点。
- GA应该被取消选择,因为它们将没有剩余的子节点。

实际发生的事情:

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

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

我该如何解决这个问题并获得期望的行为?


如果涉及到勾选/取消勾选,最好依赖于 AfterCheck 事件而不是鼠标事件。同时请参考这篇帖子 - Reza Aghaei
@ yahoo.com:这是今天我从你那里看到的第二个问题,它真的没有提供足够的细节。请非常具体地描述您期望发生的事情以及实际发生的事情。 - Sam Axe
每当我双击任何节点时,它都被视为单击,导致算法崩溃。您没有指定您期望发生什么。此外,“算法崩溃”并没有告诉我们发生了什么。 - Sam Axe
您可以断开 AfterCheck 事件处理程序。这样,双击的第一次点击会将其关闭,而双击的第二次点击会在第一次点击的处理过程中发生。由于已经断开了事件处理程序,因此不会触发第二次点击的事件处理程序。您可以考虑使用布尔标志代替断开处理程序,并将每个“点击”添加到单击事件堆栈中。 - Sam Axe
我没有检查过你的算法。如果你确定你的算法是正确的,在修复我在帖子中提到的前两个问题后,你可以坚持使用自己的算法。无论如何,我的代码按预期工作。 - Reza Aghaei
1个回答

14
这里需要解决的主要问题是:
  • 防止AfterCkeck事件处理程序递归重复逻辑。

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

  • 修复TreeView中选框CheckBoxDoubleClick错误。

    当您在TreeView中双击CheckBox时,NodeChecked值将更改两次,并将设置为双击之前的原始状态,但AfterCheck事件将只触发一次。

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

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

  • 实现算法

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

    当您选中/取消选中一个节点时:

    • 该节点的所有后代应更改为相同的选中状态。
    • 如果其后代中至少有一个子节点被选中,则其所有祖先节点都应被选中,否则应取消选中。
在我们解决了上述问题并创建了“Descendants”和“Ancestors”来遍历树之后,我们就可以处理"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 事件处理程序引发 AfterCheck。相反,我们检测 AfterCheck 是由用户还是由处理程序内部的代码引发的。为此,我们可以检查事件参数的 Action 属性:

为了防止事件被多次触发,需要在事件处理程序中添加逻辑,只有当TreeViewEventArgsAction属性不等于TreeViewAction.Unknown时才执行递归代码。
private void exTreeView1_AfterCheck(object sender, TreeViewEventArgs e)
{
    if (e.Action != TreeViewAction.Unknown)
    {
        // Changing Checked
    }
}

在TreeView中修复CheckBox双击bug

正如this post中所提到的,TreeView存在一个bug,当您在TreeView中双击CheckBox时,NodeChecked值将会改变两次,并且会被设置为双击前的原始状态,但是AfterCheck事件只会触发一次。

要解决这个问题,您可以处理WM_LBUTTONDBLCLK消息,并检查是否是在CheckBox上进行了双击,如果是,则忽略它:

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;
        }
    }
}

实现算法
使用上述扩展方法,我将处理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);
        });
    }
}

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

.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;
            }
        }
    }
}

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;
        };
    }
}

1
你可以在自定义树形视图中将 m.Result = IntPtr.Zero; return; 替换为 m.Msg = 0x0201;。这样映射到两个单击事件,而不是吞噬第二个单击事件,会提供更直观的用户体验(如此处所述)。 - Funk
1
@Funk 感谢您的反馈,很有道理。我会进一步调查并可能更新那段代码 :) - Reza Aghaei
防止AfterCheck事件处理程序递归重复逻辑,修复TreeView中复选框的双击错误是完全符合.NET 2.0标准的。此外,该算法不依赖于.NET版本,但我分享的实现取决于.NET 3.5。但是,在.NET 2.0中实现它们并不困难。 - Reza Aghaei
增加了对.NET 2.0的支持 ;) - Reza Aghaei
天啊,你救了我一整天的工作时间。这真是个宝石。谢谢! - rory.ap

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接