本文重點在於講解了TreeView控制項的使用方法。 TreeView控制項具有豐富的功能,可以運用到許多場合。摘要:講述如何在TreeView 控制項中新增資料綁定功能,它是一系列Microsoft Windows 控制項開發範例之一。您可以將本文與相關的概述文章結合起來閱讀。
簡介
在可能的情況下,您應該先使用些現成的控制項;因為提供的Microsoft® Windows® 窗體控制項中包含大量編碼和測試成果,如果您要放棄它們從頭開始,無疑是一種巨大的浪費。基於此,在本例中,我將繼承一個現有Windows 窗體控制項TreeView ,然後對其進行自訂。在下載該TreeView控制項的程式碼時,您還會得到附加的控制項開發範例,以及一個示範如何與其他資料綁定控制項一起使用該增強TreeView的範例應用程式。
設計資料綁定樹視圖
對於Windows 開發人員來說,在TreeView控制項中新增資料綁定是經常會遇到的問題,但由於TreeView和其他控制項(如ListBox或DataGrid )有一個主要差異(即TreeView顯示分層資料),因而基本控件目前還不支援此功能(也就是說,我們還必須使用它)。給定一個資料表,您就會很清楚如何在ListBox或DataGrid中顯示該訊息,但利用TreeView的分層特徵來顯示相同的資料就不那麼簡單明了。就我個人而言,我在使用TreeView顯示資料時曾應用過許多不同的方法,但有一種方法最常用:按某些欄位將表格中的資料分組,如圖1 所示。
圖1:在TreeView 中顯示數據
在本例中,我將建立一個TreeView控件,在該控件中可傳遞一個平面資料集(如圖2 所示),並可輕鬆地產生圖1 所示的結果。
圖2:平面結果集,包含創建圖1 所示的樹所需的所有資訊
在開始編碼之前,我為新控制項想出了一個可以處理該特定資料集的設計,並希望它能夠適用於許多其他類似的情況。新增一個足可以使用大多數平面資料建立分層結構的群組集合,在該集合中為每一級分層指定一個分組欄位、顯示欄位和值欄位(任一或所有欄位應相同)。為了將圖2 所示的資料轉換成圖1 所示的TreeView ,我的新控制項要求您定義兩個分組層級Publisher 和Title,並將pub_id定義為Publisher 群組的分組字段,將title_id定義為Title 群組的分組欄位。除分組字段以外,還需要為每個組指定顯示和值字段,以確定在相應組節點上顯示的文本以及用來唯一標識特定組的值。當遇到此類資料時,請使用pub_name/pub_id和title/title_id作為這兩個群組的顯示/值欄位。作者資訊將變成樹的葉節點(分組分層結構末端的節點),您還需要為這些節點指定ID ( au_id ) 和顯示( au_lname ) 欄位。
建立自訂控制項時,在開始編碼之前確定程式設計師對該控制項的使用方法將有助於提高控制項的使用效率。這種情況下,我希望程式設計師(在給定了前面所示的資料和所需結果的情況下)能夠使用以下幾行程式碼完成分組:
| With DbTreeControl .ValueMember = au_id .DisplayMember = au_lname .DataSource = myDataTable.DefaultView .AddGroup(Publisher, pub_id, pub_name, pub_id) .AddGroup(Title, title_id, title, title_id) End With |
注意:這並不是我最終編寫的程式碼行,但兩者相差不多。在開發控制項的過程中,我意識到需要將與TreeView關聯的ImageList中的圖片索引與每個分組層級相關聯,因此必須在AddGroup方法中額外新增一個參數。
為了真正建立該樹,我將瀏覽資料並查找欄位(指定為每個分組的分組值)的更改,同時在必要時建立新分組節點,並針對每個資料項目建立一個葉節點。由於存在分組節點,因此總節點數將大於資料來源中的項目數,但基礎資料中的每個項有且僅有一個葉節點。
圖3:分組節點與葉節點
葉節點和分組節點之間的差異(如圖3 所示)對本文的餘下部分具有重要意義。我決定將這兩類節點區別對待,為每一類節點分別建立自訂節點,並根據所選的節點類型引發不同的事件。
實作資料綁定
為該控制項編寫程式碼的第一步是建立專案和對應的起始類別。在本例中,我先建立一個新Windows 控制項庫,然後刪除預設的UserControl類,並用一個從TreeView控制項繼承的新類別來取代它:
Public Class dbTreeControl
Inherits System.Windows.Forms.TreeView
從這時起,我將設計一個可以放入到窗體中的控件,並使其具有常規的TreeView的外觀和功能。下一步是開始新增旨在處理在TreeView中加入的新功能所需的程式碼,即資料綁定和分組資料。
新增DataSource 屬性
我的新控制項的所有功能都很重要,但建構複雜資料綁定控制項的兩個關鍵問題是處理DataSource屬性和從資料來源的每個物件中檢索單一項目。
建立屬性例程
首先,任何用於實作複雜資料綁定的控制項都需要實作一個DataSource屬性例程,並保持適當的成員變數:
| Private m_DataSource As Object _ Public Property DataSource() As Object Get Return m_DataSource End Get Set(ByVal Value As Object) If Value Is Nothing Then cm = Nothing GroupingChanged() Else If Not (TypeOf Value Is IList Or _ TypeOf Value Is IListSource) Then ' 不是針對該用途的有效資料來源 Throw New System.Exception(無效DataSource) Else If TypeOf Value Is IListSource Then Dim myListSource As IListSource myListSource = CType(Value, IListSource) If myListSource.ContainsListCollection = True Then Throw New System.Exception(無效DataSource) Else ' 對,對。它是有效的資料來源 m_DataSource = Value cm = CType(Me.BindingContext(Value), _ CurrencyManager) GroupingChanged() End If Else m_DataSource = Value cm = CType(Me.BindingContext(Value), _ CurrencyManager) GroupingChanged() End If End If End If End Set End Property |
可用作複雜資料綁定資料來源的物件通常都支持,該介面將資料公開為物件集合,並提供若干有用屬性,如Count 。我的新TreeView控制項要求在其綁定中使用一個支援IList的對象,但使用另一個介面也可以,因為它提供了一個獲取IList對象的簡單方法( GetList )。當設定DataSource屬性後,我首先確定是否提供了有效的對象,也就是一個支援IList或IListSource的對象。我真正想要的是IList ,因此如果物件僅支援IListSource (例如DataTable ),那麼我將使用該介面的GetList()方法來獲得正確的物件。
某些實作IListSource的物件(如DataSet )實際上包含多個由ContainsListCollection屬性表示的清單。如果屬性為True ,則GetList將傳回表示清單(包含多個清單)的IList物件。在我的範例中,我決定支援直接連接到IList對像或僅包含一個IList對象的IListSource對象,並忽略需要附加工作來指定資料來源的對象,如DataSet 。
注意:如果您想要支援此類物件( DataSet或類似的物件),您可以再新增一個屬性(如DataMember )來指定用於綁定的特定子清單。
如果提供的資料來源有效,則最終結果是建立的實例( cm = Me.BindingContext(Value) )。由於該實例將用於存取基礎資料來源、物件屬性和位置信息,因此儲存在局部變數中。
新增顯示和值成員屬性
擁有DataSource是實現複雜資料綁定的第一步,但該控制項需要了解資料的哪些特定欄位或屬性將用作顯示和值成員。 Display成員將用作樹節點的標題,而Value成員可透過節點的Value屬性進行存取。這些屬性都是字串,表示欄位或屬性名,可以方便地添加到控制項中:
| Private m_ValueMember As String Private m_DisplayMember As String _ Public Property ValueMember() As String Get Return m_ValueMember End Get Set(ByVal Value As String) m_ValueMember = Value End Set End Property _ Public Property DisplayMember() As String Get Return m_DisplayMember End Get Set(ByVal Value As String) m_DisplayMember = Value End Set End Property |
在此TreeView中,這些屬性將僅表示葉節點的Display和Value成員,每個分組層級的對應資訊將在AddGroup方法中指定。
使用CurrencyManager 對象
在前面探討的DataSource屬性中,建立了一個CurrencyManager類別的實例,並儲存在類別層級變數中。透過該物件存取的CurrencyManager類別是實現資料綁定的關鍵部分,因為它具有的屬性、方法和事件可實現以下功能:
檢索屬性/欄位值
CurrencyManager物件可讓您透過它的GetItemProperties方法從資料來源的單一項目中檢索屬性或欄位值,例如DisplayMember或ValueMember欄位的值。然後使用PropertyDescriptor物件取得特定清單項目上的特定欄位或屬性的值。下面的程式碼片段顯示了這些PropertyDescriptor物件的建立方法以及如何使用GetValue函數來取得基礎資料來源中某一項的屬性值。請注意CurrencyManager物件的List屬性:透過它可以存取該控制項綁定到的IList實例:
| Dim myNewLeafNode As TreeLeafNode Dim currObject As Object currObject = cm.List(currentListIndex) If Me.DisplayMember <> AndAlso Me.ValueMember <> Then ' 新增葉節點? Dim pdValue As System.ComponentModel.PropertyDescriptor Dim pdDisplay As System.ComponentModel.PropertyDescriptor pdValue = cm.GetItemProperties()(Me.ValueMember) pdDisplay = cm.GetItemProperties()(Me.DisplayMember) myNewLeafNode = _ New TreeLeafNode(CStr(pdDisplay.GetValue(currObject)), _ currObject, _ pdValue.GetValue(currObject), _ currentListIndex) |
GetValue在傳回物件時忽略屬性的基本資料類型,因此在使用傳回值之前需要轉換。
保持資料綁定控件同步
CurrencyManager還有一個主要功能:除了可以存取綁定資料來源和項目屬性外,它還允許使用相同的DataSource來協調該控制項和任何其他控制項之間的資料綁定。此支援可用於確保多個同時綁定到相同資料來源的控制項停留在資料來源的同一項。對於我的控制項而言,我想確保在樹中選擇項目時,其他所有綁定到相同資料來源的控制項均指向同一項(同一記錄、行、甚至數組,如果您願意從資料庫的角度進行思考) 。為此,我覆蓋了基本TreeView中的OnAfterSelect方法。在該方法(在選擇樹節點後被呼叫)中,我將CurrencyManager物件的Position屬性設定為目前選取項目的索引。與該TreeView控制項一起提供的範例應用程式闡述了同步控制項如何使產生資料綁定使用者介面變得更為容易。為了讓確定目前選定項目的清單位置更為容易,我使用了自訂TreeNode類別( TreeLeafNode或TreeGroupNode ),並將每個節點的清單索引儲存到建立的Position屬性中:
| Protected Overrides Sub OnAfterSelect _ (ByVal e As System.Windows.Forms.TreeViewEventArgs) Dim tln As TreeLeafNode If TypeOf e.Node Is TreeGroupNode Then tln = FindFirstLeafNode(e.Node) Dim groupArgs As New groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect(groupArgs) ElseIf TypeOf e.Node Is TreeLeafNode Then Dim leafArgs As New leafTreeViewEventArgs(e) RaiseEvent AfterLeafSelect(leafArgs) tln = CType(e.Node, TreeLeafNode) End If If Not tln Is Nothing Then If cm.Position <> tln.Position Then cm.Position = tln.Position End If End If MyBase.OnAfterSelect(e) End Sub |
在前面的程式碼片段中,您可能注意到了一個稱為FindFirstLeafNode的函數,在此我想對其加以簡要介紹。在我的TreeView中,只有葉節點(分層結構中的最終節點)才與DataSource中的項相對應,其他所有節點只用於建立分組結構。如果我要建立一個效能優良的資料綁定控件,便始終需要選擇一個與DataSource相對應的項,因此每當選擇群組節點時,我就會找到該群組下的第一個葉節點,就好像該節點是目前的選定內容。您可以檢查該範例的運行情況,但現在您大可放心地使用它。
| Private Function FindFirstLeafNode(ByVal currNode As TreeNode) _ As TreeLeafNode If TypeOf currNode Is TreeLeafNode Then Return CType(currNode, TreeLeafNode) Else If currNode.Nodes.Count > 0 Then Return FindFirstLeafNode(currNode.Nodes(0)) Else Return Nothing End If End If End Function |
設定CurrencyManager物件的Position屬性可使其他控制項與目前選取項目同步,但是當其他控制項的位置變更時, CurrencyManager也會產生事件,以便相應地變更選取項目。要成為優秀的資料綁定元件,所選內容應隨著資料來源位置的變更而移動,修改某一項的資料時,顯示應隨之更新。 CurrencyManager引發的事件共有三: CurrentChanged 、 ItemChanged和PositionChanged 。最後一個事件相當簡單; CurrencyManager的用途之一是為資料來源維護目前位置指示器,以便多個綁定控制項均可顯示相同記錄或清單項,只要該位置更改,此事件便會引發。其他兩個事件有時會相互重疊,因而差異不太明顯。以下分別介紹如何在自訂控制項中使用這些事件: PositionChanged是一個比較簡單的事件,這裡不再贅述;當您要在複雜資料綁定控制項(如Tree)中調整目前選定項時,請使用該事件。只要修改資料來源中的項, ItemChanged事件就會引發,而CurrentChanged只有在當前項被修改時才引發。
在我的TreeView中,我發現每當我選擇一個新項目時,所有三個事件都會引發,因此我決定透過更改目前選定項來處理PositionChanged事件,而對另外兩項不進行任何處理。建議將資料來源強制轉換為IBindingList (如果資料來源支援IBindingList的話)並改用ListChanged事件,但我未實作此功能。
| Private Sub cm_PositionChanged(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles cm.PositionChanged Dim tln As TreeLeafNode If TypeOf Me.SelectedNode Is TreeLeafNode Then tln = CType(Me.SelectedNode, TreeLeafNode) Else tln = FindFirstLeafNode(Me.SelectedNode) End If If tln.Position <> cm.Position Then Me.SelectedNode = FindNodeByPosition(cm.Position) End If End Sub Private Overloads Function FindNodeByPosition(ByVal index As Integer) _ As TreeNode Return FindNodeByPosition(index, Me.Nodes) End Function Private Overloads Function FindNodeByPosition(ByVal index As Integer, _ ByVal NodesToSearch As TreeNodeCollection) As TreeNode Dim i As Integer = 0 Dim currNode As TreeNode Dim tln As TreeLeafNode Do While i < NodesToSearch.Count currNode = NodesToSearch(i) i += 1 If TypeOf currNode Is TreeLeafNode Then tln = CType(currNode, TreeLeafNode) If tln.Position = index Then Return currNode End If Else currNode = FindNodeByPosition(index, currNode.Nodes) If Not currNode Is Nothing Then Return currNode End If End If Loop Return Nothing End Function |
將DataSource 轉變為樹
編寫完資料綁定程式碼後,我可以繼續新增管理分組層級的程式碼,相應地產生樹,然後新增一些自訂事件、方法和屬性。
管理群組
程式設計師要配置群組集合,就必須建立AddGroup 、 RemoveGroup和ClearGroups函數。每當修改群組集合時,都必須重新繪製樹(以反映新配置),因此我建立了一個通用過程GroupingChanged ,當情況發生變化,需要強制重建樹時,它可以由控制項中的各種程式碼呼叫:
| Private treeGroups As New ArrayList() Public Sub RemoveGroup(ByVal group As Group) If Not treeGroups.Contains(group) Then treeGroups.Remove(group) GroupingChanged() End If End Sub Public Overloads Sub AddGroup(ByVal group As Group) Try treeGroups.Add(group) GroupingChanged() Catch End Try End Sub Public Overloads Sub AddGroup(ByVal name As String, _ ByVal groupBy As String, _ ByVal displayMember As String, _ ByVal valueMember As String, _ ByVal imageIndex As Integer, _ ByVal selectedImageIndex As Integer) Dim myNewGroup As New Group(name, groupBy, _ displayMember, valueMember, _ imageIndex, selectedImageIndex) Me.AddGroup(myNewGroup) End Sub Public Function GetGroups() As Group() Return CType(treeGroups.ToArray(GetType(Group)), Group()) End Function |
生成樹
樹的實際重建由一對過程來完成: BuildTree和AddNodes 。由於這兩個過程的程式碼太長,本文並未全部列出,而是盡量概括它們的行為(當然,如果願意您可以下載完整的程式碼)。如前所述,程式設計師可以透過設定一系列群組與該控制項進行交互,然後在BuildTree中使用這些群組來確定如何設定樹節點。 BuildTree清除目前節點集合,然後遍歷整個資料來源來處理第一層分組(本文前面的範例和圖解中提到的Publisher),為每個不同的分組值新增一個節點(使用範例中的數據,為每個pub_id值新增一個節點),然後呼叫AddNodes來填入第一級分組下的所有節點。 AddNodes遞迴呼叫本身以處理任意多的級數,必要時可加入群組節點和葉節點。使用兩個基於TreeNode的自訂類別以區別群組節點和葉節點,並為兩類節點提供各自對應的屬性。
自訂TreeView 事件
每當選擇一個節點時, TreeView都會引發兩個事件: BeforeSelect和AfterSelect 。但在我的控制項中,我想讓群組節點和葉節點的事件不同,於是便加入了自己的事件BeforeGroupSelect/AfterGroupSelect和BeforeLeafSelect/AfterLeafSelect ,除基本事件外,也引發了自訂事件參數類別:
| Public Event BeforeGroupSelect _ (ByVal sender As Object, ByVal e As groupTreeViewCancelEventArgs) Public Event AfterGroupSelect _ (ByVal sender As Object, ByVal e As groupTreeViewEventArgs) Public Event BeforeLeafSelect _ (ByVal sender As Object, ByVal e As leafTreeViewCancelEventArgs) Public Event AfterLeafSelect _ (ByVal sender As Object, ByVal e As leafTreeViewEventArgs) Protected Overrides Sub OnBeforeSelect _ (ByVal e As System.Windows.Forms.TreeViewCancelEventArgs) If TypeOf e.Node Is TreeGroupNode Then Dim groupArgs As New groupTreeViewCancelEventArgs(e) RaiseEvent BeforeGroupSelect(CObj(Me), groupArgs) ElseIf TypeOf e.Node Is TreeLeafNode Then Dim leafArgs As New leafTreeViewCancelEventArgs(e) RaiseEvent BeforeLeafSelect(CObj(Me), leafArgs) End If MyBase.OnBeforeSelect(e) End Sub Protected Overrides Sub OnAfterSelect _ (ByVal e As System.Windows.Forms.TreeViewEventArgs) Dim tln As TreeLeafNode If TypeOf e.Node Is TreeGroupNode Then tln = FindFirstLeafNode(e.Node) Dim groupArgs As New groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect(CObj(Me), groupArgs) ElseIf TypeOf e.Node Is TreeLeafNode Then Dim leafArgs As New leafTreeViewEventArgs(e) RaiseEvent AfterLeafSelect(CObj(Me), leafArgs) tln = CType(e.Node, TreeLeafNode) End If If Not tln Is Nothing Then If cm.Position <> tln.Position Then cm.Position = tln.Position End If End If MyBase.OnAfterSelect(e) End Sub |
自訂節點類別( TreeLeafNode和TreeGroupNode )和自訂事件參數類別均包含在可下載程式碼中。
範例應用程式
要全面理解本範例控制項中的所有程式碼,您應該了解它在應用程式中的運作情況。包含的範例應用程式使用pubs.mdb Access 資料庫,並說明Tree控制項如何與其他資料綁定控制項一起建立Windows 應用程式。本例中,尤其值得注意的主要功能包括樹與其他綁定控制項的同步以及對資料來源執行搜尋時樹節點的自動選擇。
注意:本範例應用程式(名為TheSample)包含在本文的下載中。
圖4:資料綁定TreeView 的演示應用程式
小結
本文介紹的資料綁定Tree控制項並非適用於所有需要Tree控制項來顯示資料庫資訊的項目,但它確實介紹了一種可針對個人目的自訂該控制項的方法。請記住,您要產生的任何複雜資料綁定控制項與Tree 控制項的大部分程式碼基本上相同,您可以透過修改現有程式碼來簡化以後的控制項開發流程。