WinForms TreeView для проверки/снятия отметки иерархии

Следующий код предназначен для рекурсивной проверки или проверки родительских или дочерних узлов по мере необходимости.

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

Ожидается, что это произойдет:

Взгляните на скриншот приложения. A, G, L и T проверяются. Если я сниму, скажем, L,
- T следует отключить, так как T является дочерним элементом L.
- G и A должны быть сняты, так как у них не будет детей.

Что происходит:

Этот код приложения отлично работает, если я нажимаю на любой узел. Если я дважды щелкнул узел, этот узел станет отмеченным/непроверенным, но одно и то же изменение не отразится на родительском и детском.

Двойной щелчок также заморозит приложение некоторое время.

Как я могу исправить эту проблему и получить ожидаемое поведение?

Ответ 1

Вот основные проблемы, которые необходимо решить:

  • Не позволяйте обработчику событий AfterCkeck рекурсивно повторять логику.

    Когда вы изменяете свойство Checked узла в AfterCheck, это вызывает другое событие AfterCheck, которое может привести к переполнению стека или, по крайней мере, излишнему после событий проверки или непредсказуемого результата в нашем алгоритме.

  • Исправлена ошибка DoubleClick для флажков в TreeView.

    Если дважды щелкнуть мышью CheckBox в TreeView, значение Checked в Node изменится дважды и будет установлено в исходное состояние перед двойным щелчком, но событие 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 события arg:

Чтобы предотвратить многократное возникновение события, добавьте логику в ваш обработчик событий, который выполняет ваш рекурсивный код, только если Свойство Action для TreeViewEventArgs не установлено в TreeViewAction.Unknown.

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

Исправлена ошибка DoubleClick для ошибки с флажками в TreeView

Как также упоминалось в этом посте, в TreeView есть ошибка, когда вы дважды щелкаете CheckBox в TreeView, значение Checked Node будет изменяться дважды и будет установлен в исходное состояние до двойного щелчка, но событие 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);
    }
}

Методы расширения для получения потомков и предков узла

Чтобы получить потомков и предков узла, нам нужно создать метод расширения для использования в 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);
        });
    }
}
Пример

Example

Чтобы проверить решение, вы можете заполнить 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;
        };
    }
}