WPF使用模板HierarchicalDataTemplate实现TreeView层次显示
第一阶段 显示TreeView
效果图
之前看过很多博客, 都是呼哧贴一堆代码, 呼哧创建一个类, 呼哧创建一堆函数……当时任务很紧一下子看不懂也懒得看了(主要是缺图)…现在重新梳理一下还挺清晰的
例如数据是这样的
那么数据类可能是这样的
class Person
{
public int Id { get; set; }
public int Level { get; set; }
public string Name { get; set; }
public int ParentId { get; set; }
}
再套一层, 节点可能是这样的
class Node
{
public int Id { get; set; }
public int Level { get; set; }
public string Name { get; set; }
public int ParentId { get; set; }
public List<Node> Children { get; set; }
public string TreeDisplay {
get { return $"{Name} [第{Level}世]";}
}
// 怎么把节点添加到List<>里面忽略...
}
典型的一个树的结构. 要使用HierarchicalDataTemplate
, 属性需要有Id
和ParentId
, 不知道是模板本身的硬性规定还是可以手动设置的
<TreeView x:Name="treeView" Grid.Row="1">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel>
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding TreeDisplay}" Margin="2,0,0,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
<TreeView.ItemTemplate>
定义了数据绑定和展示, 绑定到object.Children
, 展示object.Children[i].TreeDisplay
, </TreeView.ItemContainerStyle>
将所有节点全部展开
然后往控件上挂数据源
treeView.ItemsSource = new List<PersonTreeNode> { Root };
第二阶段 绑定节点修改
在树上可能有以下操作
1. 添加子节点
2. 移动子节点顺序
3. 删除子节点
4. 修改节点属性(名称等等)
按笨方法, 可能每次修改后都调用一次treeView.Items.Refresh()
, 或者重新绑定一次数据源…但这显然不是正确的方法, 按上例每刷新一次大概就需要三四秒的时间
要刷新UI, 还是要实现INotify接口, 在有修改时通过事件通知UI更新
对于集合的变动, 需要实现INotifyCollectionChanged
接口, 可以直接使用C#库中预定义的类, 将Children
由List<T>
修改为ObservableCollection<T>
即可, 但此类没有实现IList
, 所以一些LIst方法不能使用
对于属性的变动, 让Node
实现INotifyPropertyChanged
接口即可.
第三阶段 查找定位
网上找了一个TreeViewHelper
, mark一下, 调用SelectItem(object)
即可
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace XXXXXX
{
public static class TreeViewHelper
{
/// <summary>
/// Expands all children of a TreeView
/// </summary>
/// <param name="treeView">The TreeView whose children will be expanded</param>
public static void ExpandAll(this TreeView treeView)
{
ExpandSubContainers(treeView);
}
/// <summary>
/// Expands all children of a TreeView or TreeViewItem
/// </summary>
/// <param name="parentContainer">The TreeView or TreeViewItem containing the children to expand</param>
private static void ExpandSubContainers(ItemsControl parentContainer)
{
foreach (Object item in parentContainer.Items)
{
TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
if (currentContainer != null && currentContainer.Items.Count > 0)
{
//expand the item
currentContainer.IsExpanded = true;
//if the item's children are not generated, they must be expanded
if (currentContainer.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
{
//store the event handler in a variable so we can remove it (in the handler itself)
EventHandler eh = null;
eh = new EventHandler(delegate
{
//once the children have been generated, expand those children's children then remove the event handler
if (currentContainer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
ExpandSubContainers(currentContainer);
currentContainer.ItemContainerGenerator.StatusChanged -= eh;
}
});
currentContainer.ItemContainerGenerator.StatusChanged += eh;
}
else //otherwise the children have already been generated, so we can now expand those children
{
ExpandSubContainers(currentContainer);
}
}
}
}
/// <summary>
/// Searches a TreeView for the provided object and selects it if found
/// </summary>
/// <param name="treeView">The TreeView containing the item</param>
/// <param name="item">The item to search and select</param>
public static void SelectItem(this TreeView treeView, object item)
{
ExpandAndSelectItem(treeView, item);
}
/// <summary>
/// Finds the provided object in an ItemsControl's children and selects it
/// </summary>
/// <param name="parentContainer">The parent container whose children will be searched for the selected item</param>
/// <param name="itemToSelect">The item to select</param>
/// <returns>True if the item is found and selected, false otherwise</returns>
private static bool ExpandAndSelectItem(ItemsControl parentContainer, object itemToSelect)
{
//check all items at the current level
foreach (Object item in parentContainer.Items)
{
TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
//if the data item matches the item we want to select, set the corresponding
//TreeViewItem IsSelected to true
if (item == itemToSelect && currentContainer != null)
{
currentContainer.IsSelected = true;
currentContainer.BringIntoView();
currentContainer.Focus();
//the item was found
return true;
}
}
//if we get to this point, the selected item was not found at the current level, so we must check the children
foreach (Object item in parentContainer.Items)
{
TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
//if children exist
if (currentContainer != null && currentContainer.Items.Count > 0)
{
//keep track of if the TreeViewItem was expanded or not
bool wasExpanded = currentContainer.IsExpanded;
//expand the current TreeViewItem so we can check its child TreeViewItems
currentContainer.IsExpanded = true;
//if the TreeViewItem child containers have not been generated, we must listen to
//the StatusChanged event until they are
if (currentContainer.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
{
//store the event handler in a variable so we can remove it (in the handler itself)
EventHandler eh = null;
eh = new EventHandler(delegate
{
if (currentContainer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
if (ExpandAndSelectItem(currentContainer, itemToSelect) == false)
{
//The assumption is that code executing in this EventHandler is the result of the parent not
//being expanded since the containers were not generated.
//since the itemToSelect was not found in the children, collapse the parent since it was previously collapsed
currentContainer.IsExpanded = false;
}
//remove the StatusChanged event handler since we just handled it (we only needed it once)
currentContainer.ItemContainerGenerator.StatusChanged -= eh;
}
});
currentContainer.ItemContainerGenerator.StatusChanged += eh;
}
else //otherwise the containers have been generated, so look for item to select in the children
{
if (ExpandAndSelectItem(currentContainer, itemToSelect) == false)
{
//restore the current TreeViewItem's expanded state
currentContainer.IsExpanded = wasExpanded;
}
else //otherwise the node was found and selected, so return true
{
return true;
}
}
}
}
//no item was found
return false;
}
}
}
给TreeView设置属性 VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"
可以让其加载更快, 但对搜索定位的支持很差
另外 mark一下问题
treeview不支持多层更新的问题
public ObservableCollection<Node> Children { get; }
public void AddChildren(Node node)
{
if (Children == null) Children = new ObservableCollection<Node>();
Children.Add(node);
}
之前的代码是这样的, 理论上讲这样更省空间……然后在treeview里面连续新建两三层就不显示了……想了想, 自己用的一个小程序省什么内存
改成下面这样就正常了, 即在Node
对象初始化时就创建集合
public ObservableCollection<Node> Children { get; } = new ObservableCollection<Node>();
public void AddChildren(Node node)
{
Children.Add(node);
}
总结
C#类库\WPF架构本身有很多精妙的设计(相对中小规模应用来说), 但是……不自己写一遍真不知道在哪找啊……翻API文档都不知道怎么用的
上一篇: 在CMD下怎么运行PHP程序