【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實體類。在樣式中綁定好IsExpandedIsSelected屬性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時需要倒序遍歷!

最后顯示的完整分類如下:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM