在上一篇的文章中记录了如何使用MahApps.Metro界面库,本人还是比较喜欢它简约现代的风格,但是却发现MahApps.Metro中没有带CheckBox的TreeView,于是就参考资料做了一个带复选框的树形图。
树形图TreeView的特点是可以不断循环嵌套下去,如果只有固定的两级或三级并没有太大意义,因此其对应的数据结构应该也是要可循环嵌套的。同时一个通用的树形图的每个节点应该能够支持多种数据类型。此外对于带选择框CheckBox的树形图TreeView应该能够根据父节点状态改变子节点状态,也能根据子节点状态改变父节点状态。
数据结构
考虑到树形图的循环嵌套结构,首先创建循环嵌套的数据结构。
public class TreeNode : INotifyPropertyChanged
{
#region 析构函数
public TreeNode()
{
}
public TreeNode(string name,object content)
{
_name = name;
_content = content;
}
#endregion
#region 属性
private TreeNode _parent;
public TreeNode Parent
{
get { return _parent; }
set
{
if (_parent!=value)
{
_parent = value;
OnPropertyChanged("Parent");
}
}
}
private List<TreeNode> _children;
public List<TreeNode> Children
{
get { return _children; }
set
{
if (_children!=value)
{
_children = value;
OnPropertyChanged("Children");
}
}
}
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name!=value)
{
_name = value;
OnPropertyChanged("Name");
}
}
}
private bool? _isChecked=false;
public bool? IsChecked
{
get { return _isChecked; }
set
{
OnPropertyChanged("IsChecked");
}
}
private object _content;
public object Content
{
get { return _content; }
set
{
if (_content!=value)
{
_content = value;
OnPropertyChanged("Content");
}
}
}
#endregion
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
可以通过TreeNode的Children属性实现数据结构的嵌套。
注意:为了便于进行数据绑定,TreeNode类实现了接口INotifyPropertyChanged
父子节点的选择关联
简单来讲,就是选中父节点时,所有子节点都应选中;取消选中父节点时,所有子节点都应未选中 ;反之依然。这就需要在任一节点的选中状态变化时,分别检查其父节点的状态(如果其有父节点)、子节点的状态(如果其有子节点)。具体方法如下。
#region 方法
private void SetChecked(bool? value,bool checkChild,bool checkParent)
{
if (_isChecked==value)
{
return;
}
_isChecked = value;
//如果有子节点,检查子节点状态
if (checkChild&&value.HasValue&&_children!=null)
{
_children.ForEach(c => c.SetChecked(value,true,false));
}
//如果有父节点,检查父节点状态
if (checkParent&&_parent!=null)
{
_parent.CheckParent();
}
OnPropertyChanged("IsChecked");
}
private void CheckParent()
{
string checkedNames = string.Empty;
bool? _currentState = this.IsChecked;
bool? _firstState = null;
for (int i = 0; i < this.Children.Count(); i++)
{
bool? childrenState = this.Children[i].IsChecked;
if (i == 0)
{
_firstState = childrenState;
}
else if (_firstState != childrenState)
{
_firstState = null;
}
}
if (_firstState != null) _currentState = _firstState;
SetChecked(_firstState, false, true);
}
#endregion
然后将TreeNode的IsChecked属性稍作修改,如下:
private bool? _isChecked=false;
public bool? IsChecked
{
get { return _isChecked; }
set
{
SetChecked(value, true, true);
}
}
界面展示
与数据结构相对应,需要有支持元素嵌套的页面结构,使用TreeView的模板特性来解决这个问题。
<TreeView x:Name="treeElement">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type model:TreeNode}" ItemsSource="{Binding Path=Children,Mode=TwoWay}">
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsChecked}"></CheckBox>
<ContentPresenter Content="{Binding Name}"></ContentPresenter>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
测试示例
private void BindTreeView()
{
TreeNode node1 = new TreeNode("1", "");
TreeNode node2 = new TreeNode("2", "");
TreeNode node1_1 = new TreeNode("1.1", "");
TreeNode node1_2 = new TreeNode("1.2", "");
TreeNode node1_3 = new TreeNode("1.3", "");
TreeNode node1_2_1 = new TreeNode("1.2.1", "");
TreeNode node1_2_2 = new TreeNode("1.2.2", "");
node1_2.Children = new List<TreeNode>() { node1_2_1, node1_2_2 };
node1.Children = new List<TreeNode>() { node1_1, node1_2, node1_3 };
List<TreeNode> nodes = new List<TreeNode>() { node1, node2 };
this.treeElement.ItemsSource = nodes;
}
编写测试数据,并将数据绑定到前台界面上。运行后效果如下: