刚接触c#,项目中用到Treeview实现三态,即选中、未选中、半选中状态,并且要求父子节点选择联动,效果类似下图
网上有很多例子,于是研究了一下,结合网上大佬们的例子,所以在这里记录一下。
首先新建一个Winform工程拖个TreeView控件,然后运行。。。
嗯。。。加点数据吧。
private void CreateTree()
{
TreeNode anime = new TreeNode("2018一月新番");
var china = anime.Nodes.Add("国创");
pointer = china.Nodes.Add("一人之下");
pointer = china.Nodes.Add("狐妖小红娘");
pointer = china.Nodes.Add("妖神记");
pointer = china.Nodes.Add("斗罗大陆");
var nihonn = anime.Nodes.Add("日本");
pointer = nihonn.Nodes.Add("OVERLORD");
pointer = nihonn.Nodes.Add("龙王的工作");
pointer = nihonn.Nodes.Add("紫罗兰永恒花园");
pointer = nihonn.Nodes.Add("魔卡少女樱");
pointer = nihonn.Nodes.Add("博多豚骨拉面团");
this.LaoYeZha_TreeView.Nodes.Add(anime);
}
运行。。
ok,然后考虑节点选择联动,先考虑选中一个节点,子节点全选中,取消选择,子节点全都不选中。
添加treeview的AfterCheck事件。
private void LaoYeZha_TreeView_AfterCheck(object sender, TreeViewEventArgs e)
{
//只处理鼠标点击引起的状态变化
if(e.Action == TreeViewAction.ByMouse)
{
//更新子节点状态
UpdateChildNodes(e.Node);
}
}
private void UpdateChildNodes(TreeNode node)
{
foreach (TreeNode child in node.Nodes)
{
child.Checked = node.Checked;
UpdateChildNodes(child);
}
}
现在实现了子节点联动了,接下来是父节点
在AfterCheck事件中添加方法
private void LaoYeZha_TreeView_AfterCheck(object sender, TreeViewEventArgs e)
{
//只处理鼠标点击引起的状态变化
if(e.Action == TreeViewAction.ByMouse)
{
//更新子节点状态
UpdateChildNodes(e.Node);
//更新父节点状态
UpdateParents(e.Node);
}
}
private void UpdateParents(TreeNode node)
{
var parent = node.Parent;
while (parent != null)
{
//设置父节点状态
SetNodeState(parent);
parent = parent.Parent;
}
}
private void SetNodeState(TreeNode parent)
{
throw new NotImplementedException();
}
接下来就是SetNodeState方法怎么写了,由于treeview的节点只有选中和不选中两种状态,没有半选中状态,因此我强行定义半选中状态为属性checked = false,ForeColor = Blue,自然选中和未选中状态的ForeColor = Black
从父节点的角度来说,这个节点的状态取决与子节点状态,
如果子节点全部选中,则为选中状态
如果子节点有选中,也有没选中,则为半选中状态
如果子节点全未选中,由于上面定义了节点的半选中状态是checked = false,因此如果子节点全未选中,还要判断子节点中有无半选中的节点,如果有,父节点是半选中状态,否则父节点是不选中状态
试着写SetNodeState方法。
private void SetNodeState(TreeNode parent)
{
if(parent.Nodes.IsAllChecked())
{
//子节点全选中
parent.Checked = true;
parent.ForeColor = Color.Black;
}
else if(parent.Nodes.IsAllUnChecked())
{
//子节点全未选中
parent.Checked = false;
parent.ForeColor = Color.Black;
//还要判断子节点中是否有半选中状态
foreach (TreeNode child in parent.Nodes)
{
if (child.ForeColor == Color.Blue)
{
//用蓝色标记半选中状态
parent.ForeColor = Color.Blue;
break;
}
}
}
else
{
//子节点有的选中有的未选中
parent.Checked = false;
parent.ForeColor = Color.Blue;
}
}
其中IsAllChecked和IsAllUnChecked只是假设存在的方法 ,逻辑就是判断子节点是否全选或者全没选。
由于节点上加了颜色属性,因此需要再修改下面的方法
private void LaoYeZha_TreeView_AfterCheck(object sender, TreeViewEventArgs e)
{
//只处理鼠标点击引起的状态变化
if(e.Action == TreeViewAction.ByMouse)
{
e.Node.ForeColor = Color.Black;
//更新子节点状态
UpdateChildNodes(e.Node);
//更新父节点状态
UpdateParents(e.Node);
}
}
private void UpdateChildNodes(TreeNode node)
{
foreach (TreeNode child in node.Nodes)
{
child.Checked = node.Checked;
child.ForeColor = Color.Black;
UpdateChildNodes(child);
}
}
接下来实现IsAllChecked和IsAllUnChecked方法,为了偷懒,我给TreeNode.Nodes类加了两个扩展方法
static class TreeNodeCollectionExtern
{
public static bool IsAllChecked(this TreeNodeCollection treeNodeCollection)
{
foreach (TreeNode node in treeNodeCollection)
{
if(node.Checked == false)
{
return false;
}
}
return true;
}
public static bool IsAllUnChecked(this TreeNodeCollection treeNodeCollection)
{
foreach (TreeNode node in treeNodeCollection)
{
if (node.Checked == true)
{
return false;
}
}
return true;
}
}
ok,现在基本功能已经实现了,点击一个节点,子节点会相应变化,父节点的checkBox框也会变化(选中 是打勾状态,不选中没有勾,半选中没有勾并且节点变蓝)
最后一步是把半选中的checkBox框加上半选中图标。添加treeview的DrawNode事件
private void LaoYeZha_TreeView_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
var treeview = sender as TreeView;
var brush = Brushes.Black;
if (e.Node.ForeColor == Color.Blue)
{
var location = e.Node.Bounds.Location;
location.Offset(-11, 2);
var size = new Size(9, 9);
brush = Brushes.Blue;
e.Graphics.FillRectangle(brush, new Rectangle(location,size));
}
//绘制文本
e.Graphics.DrawString(e.Node.Text, treeview.Font, brush, e.Bounds.Left, e.Bounds.Top);
}
使这个事件有效需要设置TreeView的DrawMode属性为 TreeViewDrawMode.OwnerDrawText或者TreeViewDrawMode.OwnerDrawAll
因此在构造函数里添加
this.LaoYeZha_TreeView.DrawMode = System.Windows.Forms.TreeViewDrawMode.OwnerDrawText;
运行,ok,没有问题。
如果发现展开树节点的时候发现treeview左上角出现很多重叠的字,不好意思,我也不知道是为什么,求大佬解决方案。
我个人有两个暂定方案。
①添加treeview的AfterExpand事件调用tree的refrash方法
private void LaoYeZha_TreeView_AfterExpand(object sender, TreeViewEventArgs e)
{
LaoYeZha_TreeView.Refresh();
}
②出现重叠肯定是DrawString的问题,通过打断点发现调用DrawString是有时候坐标在左上角,所以修改DrawNode事件
private void LaoYeZha_TreeView_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
if (e.Bounds.Location.X <= 0)
{
return;
}
var treeview = sender as TreeView;
var brush = Brushes.Black;
if (e.Node.ForeColor == Color.Blue)
{
var location = e.Node.Bounds.Location;
location.Offset(-11, 2);
var size = new Size(9, 9);
brush = Brushes.Blue;
e.Graphics.FillRectangle(brush, new Rectangle(location,size));
}
//绘制文本
e.Graphics.DrawString(e.Node.Text, treeview.Font, brush, e.Bounds.Left, e.Bounds.Top);
}
以上是我的暂定解决方案,由于我也不是太懂,所以有大佬知道的话,求教。
另外还有一个问题是当双击某个节点时,treeview之响应了一次AfterCheck事件,这个怎么避免我也不知道,求大神指点。
目前只知道继承TreeView忽略双击事件或转换为单击事件等可以解决。。。
最后主界面源代码,仅供参考
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private TreeNode pointer
{
set
{
if (value != null)
{
CreateEpisode(value);
}
}
}
private void CreateTree()
{
TreeNode anime = new TreeNode("2018一月新番");
var china = anime.Nodes.Add("国创");
pointer = china.Nodes.Add("一人之下");
pointer = china.Nodes.Add("狐妖小红娘");
pointer = china.Nodes.Add("妖神记");
pointer = china.Nodes.Add("斗罗大陆");
var nihonn = anime.Nodes.Add("日本");
pointer = nihonn.Nodes.Add("OVERLORD");
pointer = nihonn.Nodes.Add("龙王的工作");
pointer = nihonn.Nodes.Add("紫罗兰永恒花园");
pointer = nihonn.Nodes.Add("魔卡少女樱");
pointer = nihonn.Nodes.Add("博多豚骨拉面团");
this.LaoYeZha_TreeView.Nodes.Add(anime);
}
Random r = new Random();
private void CreateEpisode(TreeNode node)
{
for (int i = 1; i <= r.Next(1, 10); i++)
{
node.Nodes.Add($"第{i}集");
}
}
private void Form1_Load(object sender, EventArgs e)
{
CreateTree();
}
private void LaoYeZha_TreeView_AfterCheck(object sender, TreeViewEventArgs e)
{
//只处理鼠标点击引起的状态变化
if (e.Action == TreeViewAction.ByMouse)
{
e.Node.ForeColor = Color.Black;
//更新子节点状态
UpdateChildNodes(e.Node);
//更新父节点状态
UpdateParents(e.Node);
}
}
private void UpdateChildNodes(TreeNode node)
{
foreach (TreeNode child in node.Nodes)
{
child.Checked = node.Checked;
child.ForeColor = Color.Black;
UpdateChildNodes(child);
}
}
private void UpdateParents(TreeNode node)
{
var parent = node.Parent;
while (parent != null)
{
//设置父节点状态
SetNodeState(parent);
parent = parent.Parent;
}
}
private void SetNodeState(TreeNode parent)
{
if (parent.Nodes.IsAllChecked())
{
//子节点全选中
parent.Checked = true;
parent.ForeColor = Color.Black;
}
else if (parent.Nodes.IsAllUnChecked())
{
//子节点全未选中
parent.Checked = false;
parent.ForeColor = Color.Black;
//还要判断子节点中是否有半选中状态
foreach (TreeNode child in parent.Nodes)
{
if (child.ForeColor == Color.Blue)
{
//用蓝色标记半选中状态
parent.ForeColor = Color.Blue;
break;
}
}
}
else
{
//子节点有的选中有的未选中
parent.Checked = false;
parent.ForeColor = Color.Blue;
}
}
private void LaoYeZha_TreeView_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
if (e.Bounds.Location.X <= 0)
{
return;
}
var treeview = sender as TreeView;
var brush = Brushes.Black;
if (e.Node.ForeColor == Color.Blue)
{
var location = e.Node.Bounds.Location;
location.Offset(-11, 2);
var size = new Size(9, 9);
brush = Brushes.Blue;
e.Graphics.FillRectangle(brush, new Rectangle(location, size));
}
//绘制文本
e.Graphics.DrawString(e.Node.Text, treeview.Font, brush, e.Bounds.Left, e.Bounds.Top);
}
private void LaoYeZha_TreeView_AfterExpand(object sender, TreeViewEventArgs e)
{
LaoYeZha_TreeView.Refresh();
}
}
如果有错误或不足的地方,请各位大神积极指正,谢谢。