【WPF】树形结构TreeView的用法(MVVM)
TreeView控件的用法还是有蛮多坑点的,最好记录一下。
参考项目:
静态的树形结构
如果树形结构的所有子节点都已经确定且不会改动,可以直接在控制层用C#代码来生成这个TreeView。
var rootItem = new OutlineTreeData { outlineTypeName = "David Weatherbeam", Children= { new OutlineTreeData { outlineTypeName="Alberto Weatherbeam", Children= { new OutlineTreeData { outlineTypeName="Zena Hairmonger", Children= { new OutlineTreeData { outlineTypeName="Sarah Applifunk", } } },new OutlineTreeData { outlineTypeName="Jenny van Machoqueen", Children= { new OutlineTreeData { outlineTypeName="Nick van Machoqueen", }, new OutlineTreeData { outlineTypeName="Matilda Porcupinicus", }, new OutlineTreeData { outlineTypeName="Bronco van Machoqueen", } } } } }, new OutlineTreeData { outlineTypeName="Komrade Winkleford", Children= { new OutlineTreeData { outlineTypeName="Maurice Winkleford", Children= { new OutlineTreeData { outlineTypeName="Divinity W. Llamafoot", } } }, new OutlineTreeData { outlineTypeName="Komrade Winkleford, Jr.", Children= { new OutlineTreeData { outlineTypeName="Saratoga Z. Crankentoe", }, new OutlineTreeData { outlineTypeName="Excaliber Winkleford", } } } } } } };
运行后能看到树形结构是下面的样子。
获取TreeViewItem控件
前台页面xaml:
<!-- 树形结构 --> <TreeView x:Name="treeView" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="10,-10,0,0" ItemsSource="{Binding ItemTreeDataList}" BorderThickness="0" Width="215" Height="210"> <TreeView.ItemContainerStyle> <Style TargetType="{x:Type TreeViewItem}"> <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" /> <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" /> <Setter Property="FontWeight" Value="Normal" /> <Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="FontWeight" Value="Bold" /> </Trigger> </Style.Triggers> </Style> </TreeView.ItemContainerStyle> <TreeView.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Children}"> <TextBlock x:Name="treeViewItemTB" Text="{Binding itemName}" Tag="{Binding itemId}"/> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView>
尝试过在初始化时获取TreeViewItem,发现都是为Null。
- TreeViewItem item= (TreeViewItem)(myWindow.treeView.ItemContainerGenerator.ContainerFromIndex(0)); // 无法获取,为Null!
- VisualTreeHelper.GetChild(); // 无法获取,为Null!
谷歌一下,看到这篇解答,下面这位跟我遇到的情况一样,用以上方法都无法获取TreeViewItem。
不过他给出的答案是通过点击来获取到被选中的TreeViewItem。
给TreeView默认选中一个TreeViewItem
上面的办法通过点击TreeViewItem来从事件中获得这个控件,但是如果我们想生成TreeView后不靠手动点击,立马自动选中一个默认的TreeViewItem呢?注意此时控件还未渲染,通过ItemContainerGenerator无法获取到控件。
此时只能考虑使用MVVM的绑定机制来获取控件!因为如果修改数据后,UI的更新是延迟的,无法立刻获取到前台控件。
绑定:
xaml中TreeView的ItemsSource绑定到ViewModel中的ItemTreeDataList列表,该列表中的元素类型是自定义ItemTreeData实体类。在样式中绑定好IsExpanded和IsSelected属性。TreeViewItem模板是绑定到Children属性,该属性在ItemTreeData实体类中。
ViewModel:
// Item的树形结构 private ObservableCollection<ItemTreeData> itemTreeDataList; public ObservableCollection<ItemTreeData> ItemTreeDataList { get { return itemTreeDataList; } set { SetProperty(ref itemTreeDataList, value); } }
ItemTreeData实体类:
public class ItemTreeData // 自定义Item的树形结构 { public int itemId { get; set; } // ID public string itemName { get; set; } // 名称 public int itemStep { get; set; } // 所属的层级 public int itemParent { get; set; } // 父级的ID private ObservableCollection<ItemTreeData> _children = new ObservableCollection<ItemTreeData >(); public ObservableCollection<ItemTreeData> Children { // 树形结构的下一级列表 get { return _children; } } public bool IsExpanded { get; set; } // 节点是否展开 public bool IsSelected { get; set; } // 节点是否选中 }
关于上面的层级/父级/下一级的概念,假设现在树形结构为以下结构,从上往下依此定义为根节点、零级节点、一级节点、二级节点。
/* * rootItem * |----zeroTreeItem * |----firstTreeItem * |----secondTreeItem */
控制层在生成TreeView时通过绑定的属性来设置默认选中的Item,预先将三个层级的Item分别装在不同的列表中使用。
现在尝试把二级节点中的第一个Item作为默认选中的Item。关键代码如下:
// 构造轮廓选择的树形结构 private void InitTreeView() { // 添加树形结构 ItemTreeData item = GetTreeData(); myViewModel.ItemTreeDataList.Clear(); myViewModel.ItemTreeDataList.Add(item); } // 构造树形结构 private ItemTreeData GetTreeData() { /* * rootItem * |----zeroTreeItem * |----firstTreeItem * |----secondTreeItem */ // 根节点 ItemTreeData rootItem = new ItemTreeData(); rootItem.itemId = -1; rootItem.itemName = " -- 请选择轮廓 -- "; rootItem.itemStep = -1; rootItem.itemParent = -1; rootItem.IsExpanded = true; // 根节点默认展开 rootItem.IsSelected = false; for (int i = 0; i < itemViewModel.ZeroStepList.Count; i++) // 零级分类 { Items zeroStepItem = itemViewModel.ZeroStepList[i]; ItemTreeData zeroTreeItem = new ItemTreeData(); zeroTreeItem.itemId = zeroStepItem.itemId; zeroTreeItem.itemName = zeroStepItem.itemName; zeroTreeItem.itemStep = zeroStepItem.itemSteps; zeroTreeItem.itemParent = zeroStepItem.itemParent; if (i == 0) { zeroTreeItem.IsExpanded = true; // 只有需要默认选中的第一个零级分类是展开的 } zeroTreeItem.IsSelected = false; rootItem.Children.Add(zeroTreeItem); // 零级节点无条件加入根节点 for (int j = 0; j < itemViewModel.FirstStepList.Count; j++) // 一级分类 { Items firstStepItem = itemViewModel.FirstStepList[j]; if (firstStepItem.itemParent == zeroStepItem.itemId) //零级节点添加一级节点 { ItemTreeData firstTreeItem = new ItemTreeData(); firstTreeItem.itemId = firstStepItem.itemId; firstTreeItem.itemName = firstStepItem.itemName; firstTreeItem.itemStep = firstStepItem.itemSteps; firstTreeItem.itemParent = firstStepItem.itemParent; if (j == 0) { firstTreeItem.IsExpanded = true; // 只有需要默认选中的第一个一级分类是展开的 } firstTreeItem.IsSelected = false; zeroTreeItem.Children.Add(firstTreeItem); for (int k = 0; k < itemViewModel.SecondStepList.Count; k++) // 二级分类 { Items secondStepItem = itemViewModel.SecondStepList[k]; if (secondStepItem.itemParent == firstStepItem.itemId) // 一级节点添加二级节点 { ItemTreeData secondTreeItem = new ItemTreeData(); secondTreeItem.itemId = secondStepItem.itemId; secondTreeItem.itemName = secondStepItem.itemName; secondTreeItem.itemStep = secondStepItem.itemSteps; secondTreeItem.itemParent = secondStepItem.itemParent; if (k == 0) { // 默认选中第一个二级分类 secondTreeItem.IsExpanded = true; // 已经没有下一级了,这个属性无所谓 secondTreeItem.IsSelected = true; } firstTreeItem.Children.Add(secondTreeItem); } } } } } return rootItem; }
通过初始化时给被绑定的属性赋值,使得TreeView默认选中了二级节点中的第一个TreeViewItem,效果如下图:
注意点:
- 只是修改目标节点的IsSelected = true还不够,还要把它的所有父节点(祖父节点)都设置IsExpanded = true才行!!!
显示选中TreeViewItem的完整分类路径
需求是如上图,要得到字符串"I户型_1.5-1_a2",即通过下划线连接得到“零级节点_一级节点_二级节点”,然后用一个TextBlock控件显示出来。注意这里不包含root根节点。
该功能可以写在树形结构的选项改变事件中。因为初始化TreeView时就选中了其中一个Item,所以初始化时也会调用到选项改变事件。
// 树形结构的选项改变事件 private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) { ItemTreeData selectedItem = (ItemTreeData)(myWindow.treeView.SelectedItem); if (selectedItem != null) { // UI层找不到,只能在数据层找 List<string> nameList = new List<string>(); int step = selectedItem.itemStep; // 可能值为:-1,0,1,2 int parentId = selectedItem.itemParent; // 临时保存遍历中每次使用的Id for (int i = 0; i < step; i++) // 零级分类虽然有父节点root,但是数据层中没有相应的itemParent值(为-1) { ItemTreeData parent = this.GetTreeDataById(parentId); if (parent != null) { nameList.Add(parent.itemName); parentId = parent.itemParent; } } // 组拼字符串 = 零级名称 + 一级名称 + 二级名称 string text = ""; for (int i = nameList.Count; i > 0; i--) // 倒序遍历 { if (i == nameList.Count) { text = nameList[i - 1]; } else { text = text + "_" + nameList[i - 1]; } } if (string.IsNullOrEmpty(text)) { myWindow.itemSelectedTB.Text = selectedItem.itemName; } else { myWindow.itemSelectedTB.Text = text + "_" + selectedItem.itemName; } } } // 根据Id,获得树形结构中指定的Item private ItemTreeData GetTreeDataById(int targetId) { ItemTreeData data = null; // 是否为根节点 ItemTreeData root = myViewModel.ItemTreeDataList[0]; if (root.itemId == targetId) { data = root; return data; } // 搜索零级大类 foreach (ItemTreeData zeroStepData in root.Children) { if (zeroStepData.itemId == targetId) { data = zeroStepData; return data; } // 搜索一级分类 foreach (ItemTreeData firstStepData in zeroStepData.Children) { if (firstStepData.itemId == targetId) { data = firstStepData; return data; } // 搜索二级分类 foreach (ItemTreeData secondStepData in firstStepData.Children) { if (secondStepData.itemId == targetId) { data = secondStepData; return data; } } } } return data; }
注意点:
- 同样是找不到TreeViewItem控件!UI层找不到,所以只能在数据层找它关联的ItemTreeData对象,从数据层中去获取itemName属性值。
- 因为每一级ItemTreeData对象中记录了它的父级ID,所以往根节点方向遍历父节点、祖父节点时,先加入到List中的是上一级的节点名。而需求是“零级节点_一级节点_二级节点”的顺序,所以在使用List时需要倒序遍历!
最后显示的完整分类如下: