WinForms TreeView checking/unchecking 层次结构
WinForms TreeView checking/unchecking hierarchy
以下代码旨在根据需要递归检查或 un-check parent 或 child 节点。
比如在这个位置,A、G、L、T 个节点如果我们 un-check 其中任何一个节点必须取消选中。
以下代码的问题是,每当我 double-click 算法无法实现其目的的任何节点时。
tree-searching算法从这里开始:
// 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 是 child L.
- G 和 A 应该取消选中没有 children 了。
发生了什么:
如果我 single-click 任何节点,此应用程序代码都可以正常工作。如果我 double-click 一个节点,那个节点变成 checked/unchecked 但同样的变化不会反映在 parent 和 children 上。
Double-click 也会冻结应用程序一段时间。
如何解决此问题并获得预期的行为?
这些是这里要解决的主要问题:
防止AfterCkeck
事件处理程序递归地重复逻辑。
当您更改 AfterCheck
中节点的 Checked
属性 时,它会导致另一个 AfterCheck
事件,这可能会导致堆栈溢出或至少在之后不必要的检查我们算法中的事件或不可预测的结果。
修复 DoubleClick
on check-boxes TreeView
中的错误。
当您双击TreeView
中的CheckBox
时,Node
的Checked
值会改变两次,并设置为双击前的原始状态, 但 AfterCheck
事件将引发一次。
获取节点的后代和祖先的扩展方法
我们需要创建方法来获取节点的后代和祖先。为此,我们将为 TreeNode
class.
创建扩展方法
实现算法
解决上述问题后,正确的算法将产生我们期望的点击结果。这是期望值:
当你check/uncheck一个节点:
- 该节点的所有后代都应更改为相同的检查状态。
- 祖先中的所有节点,如果它们的后代中至少有一个 child 被选中,则应该被选中,否则,应该被取消选中。
在我们解决了上述问题并创建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
属性:
To prevent the event from being raised multiple times, add logic to
your event handler that only executes your recursive code if the
Action
property of the TreeViewEventArgs
is not set to
TreeViewAction.Unknown
.
private void exTreeView1_AfterCheck(object sender, TreeViewEventArgs e)
{
if (e.Action != TreeViewAction.Unknown)
{
// Changing Checked
}
}
修复 DoubleClick
on check-boxes TreeView
中的错误
正如 中提到的,TreeView
中存在一个错误,当您双击 TreeView
中的 CheckBox
时,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
事件,所以当你 check/uncheck 一个节点时:
- 该节点的所有后代都应更改为相同的检查状态。
- 祖先中的所有节点,如果它们的后代中有列表 1 child,则应进行检查,否则应取消检查。
实现如下:
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;
};
}
}
以下代码旨在根据需要递归检查或 un-check parent 或 child 节点。
比如在这个位置,A、G、L、T 个节点如果我们 un-check 其中任何一个节点必须取消选中。
以下代码的问题是,每当我 double-click 算法无法实现其目的的任何节点时。
tree-searching算法从这里开始:
// 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 是 child L.
- G 和 A 应该取消选中没有 children 了。
发生了什么:
如果我 single-click 任何节点,此应用程序代码都可以正常工作。如果我 double-click 一个节点,那个节点变成 checked/unchecked 但同样的变化不会反映在 parent 和 children 上。
Double-click 也会冻结应用程序一段时间。
如何解决此问题并获得预期的行为?
这些是这里要解决的主要问题:
防止
AfterCkeck
事件处理程序递归地重复逻辑。当您更改
AfterCheck
中节点的Checked
属性 时,它会导致另一个AfterCheck
事件,这可能会导致堆栈溢出或至少在之后不必要的检查我们算法中的事件或不可预测的结果。修复
DoubleClick
on check-boxesTreeView
中的错误。当您双击
TreeView
中的CheckBox
时,Node
的Checked
值会改变两次,并设置为双击前的原始状态, 但AfterCheck
事件将引发一次。获取节点的后代和祖先的扩展方法
我们需要创建方法来获取节点的后代和祖先。为此,我们将为
TreeNode
class. 创建扩展方法
实现算法
解决上述问题后,正确的算法将产生我们期望的点击结果。这是期望值:
当你check/uncheck一个节点:
- 该节点的所有后代都应更改为相同的检查状态。
- 祖先中的所有节点,如果它们的后代中至少有一个 child 被选中,则应该被选中,否则,应该被取消选中。
在我们解决了上述问题并创建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
属性:
To prevent the event from being raised multiple times, add logic to your event handler that only executes your recursive code if the
Action
property of theTreeViewEventArgs
is not set toTreeViewAction.Unknown
.
private void exTreeView1_AfterCheck(object sender, TreeViewEventArgs e)
{
if (e.Action != TreeViewAction.Unknown)
{
// Changing Checked
}
}
修复 DoubleClick
on check-boxes TreeView
正如 TreeView
中存在一个错误,当您双击 TreeView
中的 CheckBox
时,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
事件,所以当你 check/uncheck 一个节点时:
- 该节点的所有后代都应更改为相同的检查状态。
- 祖先中的所有节点,如果它们的后代中有列表 1 child,则应进行检查,否则应取消检查。
实现如下:
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;
};
}
}