程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Windows窗體控件開發示例:擴展TreeView

Windows窗體控件開發示例:擴展TreeView

編輯:關於C#

摘要:講述了如何向 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
  IList 接口

可用作復雜數據綁定數據源的對象通常都支持,該接口將數據公開為對象集合,並提供若干有用屬性,如 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 類是實現數據綁定的關鍵部分,因為它具有的屬性、方法和事件可實現以下功能:

  • 訪問數據源的基礎 IList 對象
  • 在數據源中檢索和設置對象字段或屬性,以及
  • 使您的控件與同一窗體中的其他數據綁定控件同步。

檢索屬性/字段值

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved