本文重點(diǎn)講解了TreeView控件的使用方法。TreeView控件具有豐富的功能,可以運(yùn)用到很多場(chǎng)合。摘要:講述了如何向 TreeView 控件添加數(shù)據(jù)綁定功能,它是一系列 Microsoft Windows 控件開(kāi)發(fā)示例之一。您可以將本文與相關(guān)的概述文章結(jié)合起來(lái)閱讀。
簡(jiǎn)介
在可能的情況下,您應(yīng)該先使用些現(xiàn)成的控件;因?yàn)樘峁┑?Microsoft® Windows® 窗體控件中包含大量編碼和測(cè)試成果,如果您要放棄它們從頭開(kāi)始,無(wú)疑是一種巨大的浪費(fèi)。基于此,在本例中,我將繼承一個(gè)現(xiàn)有 Windows 窗體控件 TreeView ,然后對(duì)其進(jìn)行自定義。在下載該 TreeView 控件的代碼時(shí),您還會(huì)得到附加的控件開(kāi)發(fā)示例,以及一個(gè)演示如何與其他數(shù)據(jù)綁定控件一起使用該增強(qiáng) TreeView 的示例應(yīng)用程序。
設(shè)計(jì)數(shù)據(jù)綁定樹(shù)視圖
對(duì)于 Windows 開(kāi)發(fā)人員來(lái)說(shuō),向 TreeView 控件添加數(shù)據(jù)綁定是經(jīng)常會(huì)遇到的問(wèn)題,但由于 TreeView 和其他控件(如 ListBox 或 DataGrid)存在一個(gè)主要差別(即 TreeView 顯示分層數(shù)據(jù)),因而基本控件目前還不支持此功能(也就是說(shuō),我們還必須使用它)。給定一個(gè)數(shù)據(jù)表,您就會(huì)很清楚如何在 ListBox 或 DataGrid 中顯示該信息,但利用 TreeView 的分層特點(diǎn)來(lái)顯示同樣的數(shù)據(jù)就不那么簡(jiǎn)單明了。就個(gè)人而言,我在使用 TreeView 顯示數(shù)據(jù)時(shí)曾應(yīng)用過(guò)許多不同的方法,但有一種方法最常用:按某些字段將表中的數(shù)據(jù)分組,如圖 1 所示。
圖 1:在 TreeView 中顯示數(shù)據(jù)
在本例中,我將創(chuàng)建一個(gè) TreeView 控件,在該控件中可傳遞一個(gè)平面數(shù)據(jù)集(如圖 2 所示),并可輕松地生成圖 1 所示的結(jié)果。
圖 2:平面結(jié)果集,包含創(chuàng)建圖 1 所示的樹(shù)所需的所有信息
在開(kāi)始編碼之前,我為新控件想出了一個(gè)可以處理該特定數(shù)據(jù)集的設(shè)計(jì),并希望它能夠適用于許多其他類(lèi)似的情形。添加一個(gè)足可以使用大多數(shù)平面數(shù)據(jù)創(chuàng)建分層結(jié)構(gòu)的組集合,在該集合中為每一級(jí)分層均指定一個(gè)分組字段、顯示字段和值字段(任一或所有字段均應(yīng)相同)。為了將圖 2 所示的數(shù)據(jù)轉(zhuǎn)變成圖 1 所示的 TreeView,我的新控件要求您定義兩個(gè)分組級(jí)別 Publisher 和 Title,并將 pub_id 定義為 Publisher 組的分組字段,將 title_id 定義為 Title 組的分組字段。除分組字段以外,還需要為每個(gè)組指定顯示和值字段,以確定在相應(yīng)組節(jié)點(diǎn)上顯示的文本以及用來(lái)唯一標(biāo)識(shí)特定組的值。當(dāng)遇到此類(lèi)數(shù)據(jù)時(shí),請(qǐng)使用 pub_name/pub_id 和 title/title_id 作為這兩個(gè)組的顯示/值字段。作者信息將變成樹(shù)的葉節(jié)點(diǎn)(分組分層結(jié)構(gòu)末端的節(jié)點(diǎn)),您還需要為這些節(jié)點(diǎn)指定 ID (au_id) 和顯示 (au_lname) 字段。
構(gòu)建自定義控件時(shí),在開(kāi)始編碼之前確定程序員對(duì)該控件的使用方法將有助于提高控件的使用效率。這種情況下,我希望程序員(在給定了前面所示的數(shù)據(jù)和所需結(jié)果的情況下)能夠使用如下幾行代碼完成分組:
With DbTreeControl .ValueMember = "au_id" .DisplayMember = "au_lname" .DataSource = myDataTable.DefaultView |
為了真正構(gòu)建該樹(shù),我將瀏覽數(shù)據(jù)并查找字段(指定為每個(gè)分組的分組值)的更改,同時(shí)在必要時(shí)創(chuàng)建新分組節(jié)點(diǎn),并針對(duì)每個(gè)數(shù)據(jù)項(xiàng)創(chuàng)建一個(gè)葉節(jié)點(diǎn)。由于存在分組節(jié)點(diǎn),因此總節(jié)點(diǎn)數(shù)將大于數(shù)據(jù)源中的項(xiàng)目數(shù),但基礎(chǔ)數(shù)據(jù)中的每個(gè)項(xiàng)有且僅有一個(gè)葉節(jié)點(diǎn)。
圖 3:分組節(jié)點(diǎn)與葉節(jié)點(diǎn)
葉節(jié)點(diǎn)和分組節(jié)點(diǎn)之間的區(qū)別(如圖 3 所示)對(duì)本文的余下部分具有重要意義。我決定將這兩類(lèi)節(jié)點(diǎn)區(qū)別對(duì)待,為每一類(lèi)節(jié)點(diǎn)分別創(chuàng)建自定義節(jié)點(diǎn),并根據(jù)所選的節(jié)點(diǎn)類(lèi)型引發(fā)不同的事件。
實(shí)現(xiàn)數(shù)據(jù)綁定
為該控件編寫(xiě)代碼的第一步是創(chuàng)建項(xiàng)目和相應(yīng)的起始類(lèi)。在本例中,我首先創(chuàng)建一個(gè)新 Windows 控件庫(kù),然后刪除默認(rèn)的 UserControl 類(lèi),并用一個(gè)從 TreeView 控件繼承的新類(lèi)來(lái)代替它:
Public Class dbTreeControl
Inherits System.Windows.Forms.TreeView
從這時(shí)起,我將設(shè)計(jì)一個(gè)可以放入到窗體中的控件,并使其具有常規(guī)的 TreeView 的外觀和功能。下一步是開(kāi)始添加旨在處理在 TreeView 中加入的新功能所需的代碼,即數(shù)據(jù)綁定和分組數(shù)據(jù)。
添加 DataSource 屬性
我的新控件的所有功能都很重要,但構(gòu)建復(fù)雜數(shù)據(jù)綁定控件的兩個(gè)關(guān)鍵問(wèn)題是處理 DataSource 屬性和從數(shù)據(jù)源的每個(gè)對(duì)象中檢索單個(gè)項(xiàng)目。
創(chuàng)建屬性例程
首先,任何用于實(shí)現(xiàn)復(fù)雜數(shù)據(jù)綁定的控件都需要實(shí)現(xiàn)一個(gè) DataSource 屬性例程,并保持適當(dāng)?shù)某蓡T變量:
Private m_DataSource As Object
_ |
可用作復(fù)雜數(shù)據(jù)綁定數(shù)據(jù)源的對(duì)象通常都支持,該接口將數(shù)據(jù)公開(kāi)為對(duì)象集合,并提供若干有用屬性,如 Count。我的新 TreeView 控件要求在其綁定中使用一個(gè)支持 IList 的對(duì)象,但使用另一個(gè)接口也可以,因?yàn)樗峁┝艘粋€(gè)獲取 IList 對(duì)象的簡(jiǎn)便方法 (GetList)。當(dāng)設(shè)置 DataSource 屬性后,我首先確定是否提供了有效的對(duì)象,即一個(gè)支持 IList 或 IListSource 的對(duì)象。我真正想要的是 IList,因此如果對(duì)象僅支持 IListSource(例如 DataTable),那么我將使用該接口的 GetList() 方法獲得正確的對(duì)象。
某些實(shí)現(xiàn) IListSource 的對(duì)象(如 DataSet)實(shí)際上包含多個(gè)由 ContainsListCollection 屬性表示的列表。如果該屬性為 True,則 GetList 將返回一個(gè)表示列表(包含多個(gè)列表)的 IList 對(duì)象。在我的示例中,我決定支持直接連接到 IList 對(duì)象或僅包含一個(gè) IList 對(duì)象的 IListSource 對(duì)象,并忽略需要附加工作來(lái)指定數(shù)據(jù)源的對(duì)象,如 DataSet。
注意:如果要支持此類(lèi)對(duì)象(DataSet 或與之類(lèi)似的對(duì)象),您可以再添加一個(gè)屬性(如 DataMember)來(lái)指定用于綁定的特定子列表。
如果提供的數(shù)據(jù)源有效,則最終結(jié)果是創(chuàng)建的實(shí)例 (cm = Me.BindingContext(Value))。由于該實(shí)例將用于訪問(wèn)基礎(chǔ)數(shù)據(jù)源、對(duì)象屬性和位置信息,因此被存儲(chǔ)在局部變量中。
添加顯示和值成員屬性
擁有 DataSource 是實(shí)現(xiàn)復(fù)雜數(shù)據(jù)綁定的第一步,但該控件需要了解數(shù)據(jù)的哪些特定字段或?qū)傩詫⒂米黠@示和值成員。Display 成員將用作樹(shù)節(jié)點(diǎn)的標(biāo)題,而 Value 成員可通過(guò)節(jié)點(diǎn)的 Value 屬性進(jìn)行訪問(wèn)。這些屬性都是字符串,表示字段或?qū)傩悦梢苑奖愕靥砑拥娇丶校?/p>
Private m_ValueMember As String Private m_DisplayMember As String _ _ |
在此 TreeView 中,這些屬性將僅表示葉節(jié)點(diǎn)的 Display 和 Value 成員,每個(gè)分組級(jí)別的相應(yīng)信息將在 AddGroup 方法中指定。
使用 CurrencyManager 對(duì)象
在前面探討的 DataSource 屬性中,創(chuàng)建了一個(gè) CurrencyManager 類(lèi)的實(shí)例,并存儲(chǔ)在類(lèi)級(jí)別變量中。通過(guò)該對(duì)象訪問(wèn)的 CurrencyManager 類(lèi)是實(shí)現(xiàn)數(shù)據(jù)綁定的關(guān)鍵部分,因?yàn)樗哂械膶傩浴⒎椒ê褪录蓪?shí)現(xiàn)以下功能:
檢索屬性/字段值
CurrencyManager 對(duì)象允許您通過(guò)它的 GetItemProperties 方法從數(shù)據(jù)源的單個(gè)項(xiàng)中檢索屬性或字段值,如 DisplayMember 或 ValueMember 字段的值。然后使用 PropertyDescriptor 對(duì)象獲取特定列表項(xiàng)上的特定字段或?qū)傩缘闹怠O旅娴拇a片斷顯示了這些 PropertyDescriptor 對(duì)象的創(chuàng)建方法以及如何使用 GetValue 函數(shù)獲取基礎(chǔ)數(shù)據(jù)源中某一項(xiàng)的屬性值。請(qǐng)注意 CurrencyManager 對(duì)象的 List 屬性:通過(guò)它可以訪問(wèn)該控件綁定到的 IList 實(shí)例:
Dim myNewLeafNode As TreeLeafNode Dim currObject As Object currObject = cm.List(currentListIndex) If Me.DisplayMember <> "" AndAlso Me.ValueMember <> "" Then ' 添加葉節(jié)點(diǎn)? 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 在返回對(duì)象時(shí)忽略屬性的基本數(shù)據(jù)類(lèi)型,因此在使用返回值前需要對(duì)其進(jìn)行轉(zhuǎn)換。
保持?jǐn)?shù)據(jù)綁定控件同步
CurrencyManager 還有一個(gè)主要功能:除了可以訪問(wèn)綁定數(shù)據(jù)源和項(xiàng)屬性外,它還允許使用相同的 DataSource 來(lái)協(xié)調(diào)該控件和任何其他控件之間的數(shù)據(jù)綁定。該支持可用于確保多個(gè)同時(shí)綁定到同一數(shù)據(jù)源的控件停留在數(shù)據(jù)源的同一項(xiàng)。對(duì)于我的控件而言,我想確保在樹(shù)中選擇項(xiàng)時(shí),其他所有綁定到同一數(shù)據(jù)源的控件均指向同一項(xiàng)(同一記錄、行、甚至數(shù)組,如果您愿意從數(shù)據(jù)庫(kù)的角度進(jìn)行思考)。為此,我覆蓋了基本 TreeView 中的 OnAfterSelect 方法。在該方法(在選擇樹(shù)節(jié)點(diǎn)后被調(diào)用)中,我將 CurrencyManager 對(duì)象的 Position 屬性設(shè)置為當(dāng)前選定項(xiàng)的索引。與該 TreeView 控件一起提供的示例應(yīng)用程序闡釋了同步控件如何使生成數(shù)據(jù)綁定用戶界面變得更為容易。為了使確定當(dāng)前選定項(xiàng)的列表位置更為容易,我使用了自定義 TreeNode 類(lèi)(TreeLeafNode 或 TreeGroupNode),并將每個(gè)節(jié)點(diǎn)的列表索引存儲(chǔ)到創(chuàng)建的 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 |
在前面的代碼片段中,您可能注意到了一個(gè)稱(chēng)為 FindFirstLeafNode 的函數(shù),在此我想對(duì)其加以簡(jiǎn)要介紹。在我的 TreeView 中,只有葉節(jié)點(diǎn)(分層結(jié)構(gòu)中的最終節(jié)點(diǎn))才與 DataSource 中的項(xiàng)相對(duì)應(yīng),其他所有節(jié)點(diǎn)只用于創(chuàng)建分組結(jié)構(gòu)。如果我要?jiǎng)?chuàng)建一個(gè)性能優(yōu)良的數(shù)據(jù)綁定控件,便始終需要選擇一個(gè)與 DataSource 相對(duì)應(yīng)的項(xiàng),因此每當(dāng)選擇組節(jié)點(diǎn)時(shí),我就會(huì)找到該組下的第一個(gè)葉節(jié)點(diǎn),就好象該節(jié)點(diǎn)是當(dāng)前的選定內(nèi)容。您可以檢查該示例的運(yùn)行情況,但現(xiàn)在您大可放心地使用它。
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 |
設(shè)置 CurrencyManager 對(duì)象的 Position 屬性可使其他控件與當(dāng)前選定項(xiàng)同步,但是當(dāng)其他控件的位置發(fā)生變化時(shí),CurrencyManager 也產(chǎn)生事件,以便相應(yīng)地更改選定項(xiàng)。要成為一個(gè)優(yōu)秀的數(shù)據(jù)綁定組件,所選內(nèi)容應(yīng)隨著數(shù)據(jù)源位置的更改而移動(dòng),修改某一項(xiàng)的數(shù)據(jù)時(shí),顯示應(yīng)隨之更新。CurrencyManager 引發(fā)的事件共有三個(gè):CurrentChanged、ItemChanged 和 PositionChanged。最后一個(gè)事件相當(dāng)簡(jiǎn)單;CurrencyManager 的用途之一是為數(shù)據(jù)源維護(hù)當(dāng)前位置指示器,以便多個(gè)綁定控件均可以顯示同一記錄或列表項(xiàng),只要該位置更改,此事件便會(huì)引發(fā)。其他兩個(gè)事件有時(shí)會(huì)相互重疊,因而區(qū)別不太明顯。以下分別介紹如何在自定義控件中使用這些事件:PositionChanged 是一個(gè)比較簡(jiǎn)單的事件,此處不再贅述;當(dāng)您要在復(fù)雜數(shù)據(jù)綁定控件(如 Tree)中調(diào)整當(dāng)前選定項(xiàng)時(shí),請(qǐng)使用該事件。只要修改數(shù)據(jù)源中的項(xiàng),ItemChanged 事件就會(huì)引發(fā),而 CurrentChanged 只有在當(dāng)前項(xiàng)被修改時(shí)才引發(fā)。
在我的 TreeView 中,我發(fā)現(xiàn)每當(dāng)我選擇一個(gè)新項(xiàng)時(shí),所有三個(gè)事件均會(huì)引發(fā),因此我決定通過(guò)更改當(dāng)前選定項(xiàng)來(lái)處理 PositionChanged 事件,而對(duì)另外兩項(xiàng)不進(jìn)行任何處理。建議將數(shù)據(jù)源強(qiáng)制轉(zhuǎn)換為 IBindingList(如果數(shù)據(jù)源支持 IBindingList 的話)并改用 ListChanged 事件,但我未實(shí)現(xiàn)此功能。
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 Private Overloads Function FindNodeByPosition(ByVal index As Integer) _ Private Overloads Function FindNodeByPosition(ByVal index As Integer, _ Do While i < NodesToSearch.Count |
將 DataSource 轉(zhuǎn)變?yōu)闃?shù)
編寫(xiě)完數(shù)據(jù)綁定代碼后,我可以繼續(xù)添加管理分組級(jí)別的代碼,相應(yīng)地生成樹(shù),然后添加一些自定義事件、方法和屬性。
管理組
程序員要配置組集合,就必須創(chuàng)建 AddGroup、RemoveGroup 和 ClearGroups 函數(shù)。每當(dāng)修改組集合時(shí),都必須重新繪制樹(shù)(以反映新配置),因此我創(chuàng)建了一個(gè)通用過(guò)程 GroupingChanged,當(dāng)情況發(fā)生變化,需要強(qiáng)制重建樹(shù)時(shí),它可以由控件中的各種代碼調(diào)用:
Private treeGroups As New ArrayList()
Public Sub RemoveGroup(ByVal group As Group) Public Overloads Sub AddGroup(ByVal group As Group) Public Overloads Sub AddGroup(ByVal name As String, _
|
樹(shù)的實(shí)際重建由一對(duì)過(guò)程來(lái)完成:BuildTree 和 AddNodes。由于這兩個(gè)過(guò)程的代碼太長(zhǎng),本文并未全部列出,而是盡量概括它們的行為(當(dāng)然,如果愿意您可以下載完整的代碼)。如前所述,程序員可以通過(guò)設(shè)置一系列組與該控件進(jìn)行交互,然后在 BuildTree 中使用這些組來(lái)確定如何設(shè)置樹(shù)節(jié)點(diǎn)。BuildTree 清除當(dāng)前節(jié)點(diǎn)集合,然后遍歷整個(gè)數(shù)據(jù)源來(lái)處理第一級(jí)分組(本文前面的示例和圖解中提到的 Publisher),為每個(gè)不同的分組值添加一個(gè)節(jié)點(diǎn)(使用示例中的數(shù)據(jù),為每個(gè) pub_id 值添加一個(gè)節(jié)點(diǎn)),然后調(diào)用 AddNodes 來(lái)填充第一級(jí)分組下的所有節(jié)點(diǎn)。AddNodes 遞歸調(diào)用自身以處理任意多的級(jí)數(shù),必要時(shí)可添加組節(jié)點(diǎn)和葉節(jié)點(diǎn)。使用兩個(gè)基于 TreeNode 的自定義類(lèi)以區(qū)別組節(jié)點(diǎn)和葉節(jié)點(diǎn),并為兩類(lèi)節(jié)點(diǎn)提供各自相應(yīng)的屬性。
自定義 TreeView 事件
每當(dāng)選擇一個(gè)節(jié)點(diǎn)時(shí),TreeView 都會(huì)引發(fā)兩個(gè)事件:BeforeSelect 和 AfterSelect。但在我的控件中,我想使組節(jié)點(diǎn)和葉節(jié)點(diǎn)的事件不同,于是便添加了自己的事件 BeforeGroupSelect/AfterGroupSelect 和 BeforeLeafSelect/AfterLeafSelect,除基本事件外,還引發(fā)了自定義事件參數(shù)類(lèi):
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 _ Protected Overrides Sub OnAfterSelect _ If Not tln Is Nothing Then |
自定義節(jié)點(diǎn)類(lèi)(TreeLeafNode 和 TreeGroupNode)和自定義事件參數(shù)類(lèi)均包括在可下載代碼中。
示例應(yīng)用程序
要全面理解本示例控件中的所有代碼,您應(yīng)該了解它在應(yīng)用程序中的運(yùn)行情況。包含的示例應(yīng)用程序使用 pubs.mdb Access 數(shù)據(jù)庫(kù),并說(shuō)明 Tree 控件如何與其他數(shù)據(jù)綁定控件一起創(chuàng)建 Windows 應(yīng)用程序。本例中,尤其值得注意的主要功能包括樹(shù)與其他綁定控件的同步以及對(duì)數(shù)據(jù)源執(zhí)行搜索時(shí)樹(shù)節(jié)點(diǎn)的自動(dòng)選擇。
注意:本示例應(yīng)用程序(名為“TheSample”)包含在本文的下載中。
圖 4:數(shù)據(jù)綁定 TreeView 的演示應(yīng)用程序
小結(jié)
本文介紹的數(shù)據(jù)綁定 Tree 控件并非適用于所有需要 Tree 控件來(lái)顯示數(shù)據(jù)庫(kù)信息的項(xiàng)目,但它確實(shí)介紹了一種可針對(duì)個(gè)人目的自定義該控件的方法。請(qǐng)記住,您要生成的任何復(fù)雜數(shù)據(jù)綁定控件與 Tree 控件的大部分代碼基本相同,您可以通過(guò)修改現(xiàn)有代碼來(lái)簡(jiǎn)化以后的控件開(kāi)發(fā)過(guò)程。
新聞熱點(diǎn)
疑難解答