1,前端代碼,在前端主要是為了將數據源綁定到控件上,主要用的就是HierarchicalDataTemplate類
<TreeView Name="TreeView_NodeList">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Node}" ItemsSource="{Binding Nodes,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
<StackPanel Orientation="Horizontal" Margin="0,2,0,2">
<TextBlock Text="{Binding NodeName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" ToolTip="{Binding NodeName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
其中還得定義Node類,Node對象使用Nodeld作為主鍵進行區分,Nodeid為32位的GUID。Node的屬性Nodes保存節點的子節點集合。屬性IsDeleted表及節點是否被刪除。
public class Node
{
public Node()
{
this.NodeId = Guid.NewGuid().ToString();
this.IsDeleted = false;
this.Nodes = new List<Node>();
}
/// <summary>
/// 節點ID
/// </summary>
public string NodeId { get; set; }
/// <summary>
/// 節點名稱
/// </summary>
public string NodeName { get; set; }
/// <summary>
/// 節點攜帶的內容
/// </summary>
public string NodeContent { get; set; }
/// <summary>
/// 被刪除
/// </summary>
public bool IsDeleted { get; set; }
/// <summary>
/// 節點類型
/// </summary>
public NodeType NodeType { get; set; }
/// <summary>
/// 子節點集合
/// </summary>
public List<Node> Nodes { get; set; }
}
然后我們的用枚舉類型的NodeType屬性表示節點的類型,節點具有根節點、葉子結點和結構節點三種類別。葉子結點不支持增加節點操作、飛葉子結點不支持刪除節點的操作。
public enum NodeType
{
RootNode,//根節點
LeafNode,//葉子節點
StructureNode//結構節點,僅起到組織配置文件結構的作用,不參與修改
}
下面就是我們從.cs里面想xaml里面添加數據。做項目的時候可能會通過解析xml或者ini等文件或者訪問數據庫等方式獲取數據源。
private List<Node> GetNodeList()
{
Node leafOneNode = new Node();
leafOneNode.NodeName = "葉子節點一";
leafOneNode.NodeContent = "我是葉子節點一";
leafOneNode.NodeType = NodeType.LeafNode;
leafOneNode.Nodes = new List<Node>();
Node leafTwoNode = new Node();
leafTwoNode.NodeName = "葉子節點二";
leafTwoNode.NodeContent = "我是葉子節點二";
leafTwoNode.NodeType = NodeType.LeafNode;
leafTwoNode.Nodes = new List<Node>();
Node leafThreeNode = new Node();
leafThreeNode.NodeName = "葉子節點三";
leafThreeNode.NodeContent = "我是葉子節點三";
leafThreeNode.NodeType = NodeType.LeafNode;
leafThreeNode.Nodes = new List<Node>();
Node secondLevelNode = new Node();
secondLevelNode.NodeName = "二級節點";
secondLevelNode.NodeContent = "我是二級節點";
secondLevelNode.NodeType = NodeType.StructureNode;
secondLevelNode.Nodes = new List<Node>() { leafOneNode, leafTwoNode, leafThreeNode };
Node firstLevelNode = new Node();
firstLevelNode.NodeName = "一級節點";
firstLevelNode.NodeContent = "我是一級節點";
firstLevelNode.NodeType = NodeType.StructureNode;
firstLevelNode.Nodes = new List<Node>() { secondLevelNode };
return new List<Node>()
{
new Node(){NodeName="根節點",NodeContent="我是根節點",NodeType=NodeType.RootNode,Nodes=new List<Node>(){firstLevelNode}}
};
}
程序使用nodeList接收返回的測試數據,並在頁面的Loaded事件中進行數據綁定。
public List<Node> nodeList { get; set; }
private void Window_Loaded(object sender, RoutedEventArgs e)
{
nodeList = GetNodeList();
this.TreeView_NodeList.ItemsSource = nodeList;
ExpandTree();
}
2、展開所有節點
其中使用到的ExpandTree()方法實現展開TreeView的所有節點。
private void ExpandTree()
{
if (this.TreeView_NodeList.Items != null && this.TreeView_NodeList.Items.Count > 0)
{
foreach (var item in this.TreeView_NodeList.Items)
{
DependencyObject dependencyObject = this.TreeView_NodeList.ItemContainerGenerator.ContainerFromItem(item);
if (dependencyObject != null)//第一次打開程序,dependencyObject為null,會出錯
{
((TreeViewItem)dependencyObject).ExpandSubtree();
}
}
}
}
執行程序,可以得到下面的執行結果。
3、增加和刪除節點
下面給TreeView增加右鍵彈出菜單,實現“增加節點”和“刪除節點”的操作。
畫面代碼中設置TreeView的ContextMenu屬性,增加彈出菜單項。
<TreeView.ContextMenu>
<ContextMenu x:Name="ContextMenu_EditNode">
<MenuItem Header="新增節點" Name="MenuItem_AddNode" Click="MenuItem_AddNode_Click"/>
<MenuItem Header="刪除節點" Name="MenuItem_DeleteNode" Click="MenuItem_DeleteNode_Click"/>
</ContextMenu>
</TreeView.ContextMenu>
下面是增加節點和刪除節點的具體邏輯代碼。
/// <summary>
/// 新增節點
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MenuItem_AddNode_Click(object sender, RoutedEventArgs e)
{
var currentNode = FindTheNode(nodeList, selectedNode.NodeId);
if (currentNode != null)
{
if (currentNode.NodeType == NodeType.LeafNode)
{
MessageBox.Show("葉子節點不支持新增節點操作!");
}
else
{
MessageBox.Show("開始新增節點操作!");
}
}
}
/// <summary>
/// 刪除節點
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MenuItem_DeleteNode_Click(object sender, RoutedEventArgs e)
{
var currentNode = FindTheNode(nodeList, selectedNode.NodeId);
if (currentNode != null)
{
if (currentNode.NodeType != NodeType.LeafNode)
{
MessageBox.Show("非葉子節點不支持刪除操作!");
}
else
{
MessageBoxResult dr = MessageBox.Show("確定要刪除這個節點嗎?", "提示", MessageBoxButton.OKCancel);
if (dr == MessageBoxResult.OK)
{
DeleteTheNode(nodeList, currentNode);
MessageBox.Show("成功刪除節點!");
}
else
{
return;
}
}
}
}
其中刪除節點需要使用下面的DeleteTheNode()方法,該方法使用遞歸思想實現刪除指定節點的邏輯。
private void DeleteTheNode(List<Node> nodeList, Node deleteNode)
{
foreach (Node node in nodeList)
{
if (node.Nodes != null && node.Nodes.Count > 0)
{
DeleteTheNode(node.Nodes, deleteNode);
}
if (node == deleteNode)
{
node.IsDeleted = true;
}
}
}
不管是刪除節點還是新增節點,都需要獲取當前選中的節點,使用FindTheNode()方法實現。
private Node FindTheNode(List<Node> nodeList, string nodeId)
{
Node findedNode = null;
foreach (Node node in nodeList)
{
if (node.Nodes != null && node.Nodes.Count > 0)
{
if ((findedNode = FindTheNode(node.Nodes, nodeId)) != null)
{
return findedNode;
}
}
if (node.NodeId == nodeId)
{
return node;
}
}
return findedNode;
}
4、處理PreviewMouseRightButtonDown事件
因為葉子節點不支持增加節點操作,非葉子結點不支持刪除節點操作,所以在點擊鼠標右鍵時,需要檢測選中節點的類型並進行控制。
為了實現這個功能,可以為TreeView綁定事件PreviewMouseRightButtonDown,在事件處理器中進行相應的控制即可。
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="TreeViewItem.PreviewMouseRightButtonDown" Handler="TreeViewItem_PreviewMouseRightButtonDown"/>
</Style>
</TreeView.ItemContainerStyle>
下面是PreviewMouseRightButtonDown事件的事件處理器代碼。
private void TreeViewItem_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
var treeViewItem = VisualUpwardSearch<TreeViewItem>(e.OriginalSource as DependencyObject) as TreeViewItem;
if (treeViewItem != null)
{
Node currentNode = treeViewItem.Header as Node;
if (currentNode.NodeType != NodeType.LeafNode)
{
MenuItem_AddNode.IsEnabled = true;
MenuItem_DeleteNode.IsEnabled = false;
}
else
{
MenuItem_AddNode.IsEnabled = false;
MenuItem_DeleteNode.IsEnabled = true;
}
treeViewItem.Focus();
e.Handled = true;
}
}
代碼中使用到VisualUpwardSearch()方法,該方法用於尋找控件的最外層父控件。
private DependencyObject VisualUpwardSearch<T>(DependencyObject source)
{
while (source != null && source.GetType() != typeof(T))
{
source = VisualTreeHelper.GetParent(source);
}
return source;
}
5、查找下一個節點
有時候,還需要實現循環“查找下一個”的功能,在畫面中增加輸入框和按鈕。
<TextBox x:Name="TextBox_FindNextNode" Grid.Column="1" HorizontalAlignment="Left" Height="23" Margin="49,20,-160,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" LostFocus="TextBox_FindNextNode_LostFocus"/>
<Button x:Name="Button_FindNextNode" Content="查找下一個" Grid.Column="1" HorizontalAlignment="Left" Margin="188,20,-254,0" VerticalAlignment="Top" Width="75" Click="Button_FindNextNode_Click"/>
為了實現循環“查找下一個”,定義了下面兩個對象。
private int nextSearchedNodeIndex = -1;
public List<Node> searchedNodeList { get; set; }
searchedNodeList 記錄根據關鍵字查找到的所有匹配的節點,nextSearchedNodeIndex 記錄當前查看節點的位置。
搜索框失去焦點時,查找匹配的節點並保存至searchedNodeList ,同時將索引置為-1。
private void TextBox_FindNextNode_LostFocus(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrEmpty(this.TextBox_FindNextNode.Text.Trim()))
{
nextSearchedNodeIndex = -1;
searchedNodeList = FindMatchedNodes(nodeList, this.TextBox_FindNextNode.Text.Trim());
}
}
點擊“查找下一個”按鈕,新增nextSearchedNodeIndex ,再使用nextSearchedNodeIndex 從searchedNodeList拿到當前展示的節點。這里需要注意的是一輪循環結束后,nextSearchedNodeIndex 再次置為-1,循環重新開始。
private void Button_FindNextNode_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(this.TextBox_FindNextNode.Text.Trim()))
{
MessageBox.Show("請輸入查找條件!");
return;
}
if (searchedNodeList != null && searchedNodeList.Count > 0)
{
//循環查找下一個
if (nextSearchedNodeIndex >= searchedNodeList.Count - 1)
{
nextSearchedNodeIndex = -1;
}
nextSearchedNodeIndex += 1;
selectedNode = searchedNodeList[nextSearchedNodeIndex];
SelectTheCurrentNode();
}
else
{
MessageBox.Show("沒有找到滿足條件的節點");
}
}
其中,查找匹配節點的邏輯由FindMatchedNodes()方法完成,該方法同樣使用遞歸思想來實現。
private List<Node> FindMatchedNodes(List<Node> srcNodeList, string filterString)
{
List<Node> destNodeList = new List<Node>();
foreach (Node node in srcNodeList)
{
if (node.NodeName.Contains(filterString))
{
destNodeList.Add(node);
}
if (node.Nodes != null && node.Nodes.Count > 0)
{
destNodeList.AddRange(FindMatchedNodes(node.Nodes, filterString));
}
}
return destNodeList;
}
其中,SelectTheCurrentNode()方法實現選中當前節點,並將焦點置於當前選中節點。這里同樣離不開遞歸的功勞。
private void SelectTheCurrentNode()
{
if (this.TreeView_NodeList.Items != null && this.TreeView_NodeList.Items.Count > 0)
{
foreach (var item in this.TreeView_NodeList.Items)
{
DependencyObject dependencyObject = this.TreeView_NodeList.ItemContainerGenerator.ContainerFromItem(item);
if (dependencyObject != null)//第一次打開程序,dependencyObject為null,會出錯
{
TreeViewItem tvi = (TreeViewItem)dependencyObject;
if ((tvi.Header as Node).NodeId == selectedNode.NodeId)
{
tvi.IsSelected = true;
tvi.Focus();
}
else
{
SetNodeSelected(tvi);
}
}
}
}
}
private void SetNodeSelected(TreeViewItem Item)
{
foreach (var item in Item.Items)
{
DependencyObject dependencyObject = Item.ItemContainerGenerator.ContainerFromItem(item);
if (dependencyObject != null)
{
TreeViewItem treeViewItem = (TreeViewItem)dependencyObject;
if ((treeViewItem.Header as Node).NodeId == selectedNode.NodeId)
{
treeViewItem.IsSelected = true;
treeViewItem.Focus();
}
else
{
treeViewItem.IsSelected = false;
if (treeViewItem.HasItems)
{
SetNodeSelected(treeViewItem);
}
}
}
}
}
6、處理SelectedItemChanged事件
操作TreeView,不然會選中節點,觸發SelectedItemChanged事件,下面是SelectedItemChanged事件的處理代碼。
private void TreeView_NodeList_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
Node node = this.TreeView_NodeList.SelectedItem as Node;
if (node != null)
{
selectedNode = node;
MessageBox.Show("當前選中的節點是:" + selectedNode.NodeName);
}
}
本文結合shil介紹了TreeView層級數據源的綁定,選中事件的處理,展開節點、新增節點、刪除節點、查找節點等常見操作。
普遍使用到遞歸思想,是學習遞歸的完美練兵場。