WinForms TreeView проверка / снятие иерархии
Следующий код предназначен для рекурсивной проверки или отмены проверки родительских или дочерних узлов по мере необходимости.
Например, в этой позиции A, G, L , иt узлы должны быть непроверены, если мы снимаем проверку с любого из них.
Проблема со следующим кодом заключается в том, что всякий раз, когда я дважды щелкаю любой узел, алгоритм не достигает своей цели.
Алгоритм поиска по дереву начинается здесь:
// 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, и еще Т проверяются. Если я сниму галочку, скажите, L,
- Т должно быть непроверено как Т является ли ребенок L.
- G и еще А они должны быть бесконтрольны, так как у них не останется детей.
Что происходит:
Этот код приложения отлично работает, если я щелкаю один раз по любому узлу. Если я дважды щелкну узел, Этот узел будет отмечен/снят, но одно и то же изменение не отразится на родительском и дочернем узлах.
Двойной щелчок также замораживает приложение на некоторое время.
Как я могу исправить эту проблему и получить ожидаемое поведение?
1 ответ:
Вот основные проблемы, которые нужно решить здесь:
Запретить обработчику событий
AfterCkeckповторять логику рекурсивно.Когда вы изменяете
Checkedсвойство узла вAfterCheck, это вызывает другоеAfterCheckсобытие, которое может привести нас к переполнению стека или, по крайней мере, ненужному после событий проверки или непредсказуемому результату в нашем алгоритме.Исправлена ошибка
DoubleClickна флажках вTreeView.Когда вы дважды щелкните на
CheckBoxвTreeViewзначениеCheckedNodeизменится дважды и будет установлено в исходное состояние перед двойным щелчком, но событие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, значение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); } }Методы расширения для получения потомков и предков узла
Чтобы получить потомков и предков узла, мы необходимо создать несколько методов расширения для использования в
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; }; } }


Comments