代码之家  ›  专栏  ›  技术社区  ›  user366312

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

  •  12
  • user366312  · 技术社区  · 8 年前

    以下代码用于根据需要递归检查或取消检查父节点或子节点。

    enter image description here

    例如,在这个位置, , 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
    

    预计会发生:

    看看应用程序的屏幕截图。 , G , L T 已检查。如果我取消选中,比如, L ,
    - T 应取消选中为 T 是的孩子 L .
    - G 应该取消选中,因为他们将没有孩子。

    发生了什么事:

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

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

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

    1 回复  |  直到 7 年前
        1
  •  12
  •   Reza Aghaei    8 年前

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

    • 预防 AfterCkeck 事件处理程序以递归方式重复逻辑。

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

    • 固定 DoubleClick 在复选框中插入 TreeView .

      当你双击 CheckBox 在里面 树视图 , the 选中的 的值 Node 将更改两次,并在双击前设置为原始状态,但 事后检查 事件将引发一次。

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

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

    • 实现算法

      在解决了上述问题之后,正确的算法将产生我们所期望的点击结果。期望值如下:

      选中/取消选中节点时:

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

    在我们解决上述问题并创造 Descendants Ancestors 穿过这棵树,我们就够了 事后检查 事件并具有以下逻辑:

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

    下载

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

    详细答案

    预防 后甲板 递归重复逻辑的事件处理程序

    事实上,我们不会停止 AfterCheck 引发的事件处理程序 事后检查 . 相反,我们检测 事后检查 由用户或处理程序内的代码引发。为此,我们可以检查 Action 事件参数的属性:

    要防止多次引发事件,请将逻辑添加到 仅在以下情况下执行递归代码的事件处理程序: 行动 的属性 TreeViewEventArgs 未设置为 TreeViewAction.Unknown .

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

    固定 双击 在复选框中插入 树视图

    如中所述 this post ,有一个错误 树视图 ,当您双击 复选框 在里面 树视图 , the 选中的 的值 结点 将更改两次,并在双击前设置为原始状态,但 事后检查 事件将引发一次。

    为了解决这个问题,你可以 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);
        }
    }
    

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

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

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

    实现算法

    使用上述扩展方法,我将处理 事后检查 事件,因此当您选中/取消选中节点时:

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

    实现方法如下:

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

    .NET 2支持

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

    外部视图

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