首页 > 编程 > Visual Basic > 正文

TreeView控件的设计实例

2023-06-12 12:11:28
字体:
来源:转载
供稿:网友

本文重点讲解了TreeView控件的使用方法。TreeView控件具有丰富的功能,可以运用到很多场合。摘要:讲述了如何向 TreeView 控件添加数据绑定功能,它是一系列 Microsoft Windows 控件开发示例之一。您可以将本文与相关的概述文章结合起来阅读。

简介

在可能的情况下,您应该先使用些现成的控件;因为提供的 Microsoft® Windows® 窗体控件中包含大量编码和测试成果,如果您要放弃它们从头开始,无疑是一种巨大的浪费。基于此,在本例中,我将继承一个现有 Windows 窗体控件 TreeView ,然后对其进行自定义。在下载该 TreeView 控件的代码时,您还会得到附加的控件开发示例,以及一个演示如何与其他数据绑定控件一起使用该增强 TreeView 的示例应用程序。

设计数据绑定树视图

对于 Windows 开发人员来说,向 TreeView 控件添加数据绑定是经常会遇到的问题,但由于 TreeView 和其他控件(如 ListBoxDataGrid)存在一个主要差别(即 TreeView 显示分层数据),因而基本控件目前还不支持此功能(也就是说,我们还必须使用它)。给定一个数据表,您就会很清楚如何在 ListBoxDataGrid 中显示该信息,但利用 TreeView 的分层特点来显示同样的数据就不那么简单明了。就个人而言,我在使用 TreeView 显示数据时曾应用过许多不同的方法,但有一种方法最常用:按某些字段将表中的数据分组,如图 1 所示。

图 1:在 TreeView 中显示数据

  在本例中,我将创建一个 TreeView 控件,在该控件中可传递一个平面数据集(如图 2 所示),并可轻松地生成图 1 所示的结果。

图 2:平面结果集,包含创建图 1 所示的树所需的所有信息

  在开始编码之前,我为新控件想出了一个可以处理该特定数据集的设计,并希望它能够适用于许多其他类似的情形。添加一个足可以使用大多数平面数据创建分层结构的组集合,在该集合中为每一级分层均指定一个分组字段、显示字段和值字段(任一或所有字段均应相同)。为了将图 2 所示的数据转变成图 1 所示的 TreeView,我的新控件要求您定义两个分组级别 Publisher 和 Title,并将 pub_id 定义为 Publisher 组的分组字段,将 title_id 定义为 Title 组的分组字段。除分组字段以外,还需要为每个组指定显示和值字段,以确定在相应组节点上显示的文本以及用来唯一标识特定组的值。当遇到此类数据时,请使用 pub_name/pub_idtitle/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 属性后,我首先确定是否提供了有效的对象,即一个支持 IListIListSource 的对象。我真正想要的是 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 中,这些属性将仅表示叶节点的 DisplayValue 成员,每个分组级别的相应信息将在 AddGroup 方法中指定。

  使用 CurrencyManager 对象

  在前面探讨的 DataSource 属性中,创建了一个 CurrencyManager 类的实例,并存储在类级别变量中。通过该对象访问的 CurrencyManager 类是实现数据绑定的关键部分,因为它具有的属性、方法和事件可实现以下功能:

  • 访问数据源的基础 IList 对象
  • 在数据源中检索和设置对象字段或属性,以及
  • 使您的控件与同一窗体中的其他数据绑定控件同步。

  检索属性/字段值

  CurrencyManager 对象允许您通过它的 GetItemProperties 方法从数据源的单个项中检索属性或字段值,如 DisplayMemberValueMember 字段的值。然后使用 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 类(TreeLeafNodeTreeGroupNode),并将每个节点的列表索引存储到创建的 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 引发的事件共有三个:CurrentChangedItemChangedPositionChanged。最后一个事件相当简单;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 转变为树

  编写完数据绑定代码后,我可以继续添加管理分组级别的代码,相应地生成树,然后添加一些自定义事件、方法和属性。

  管理组

  程序员要配置组集合,就必须创建 AddGroupRemoveGroupClearGroups 函数。每当修改组集合时,都必须重新绘制树(以反映新配置),因此我创建了一个通用过程 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


  生成树

  树的实际重建由一对过程来完成:BuildTreeAddNodes。由于这两个过程的代码太长,本文并未全部列出,而是尽量概括它们的行为(当然,如果愿意您可以下载完整的代码)。如前所述,程序员可以通过设置一系列组与该控件进行交互,然后在 BuildTree 中使用这些组来确定如何设置树节点。BuildTree 清除当前节点集合,然后遍历整个数据源来处理第一级分组(本文前面的示例和图解中提到的 Publisher),为每个不同的分组值添加一个节点(使用示例中的数据,为每个 pub_id 值添加一个节点),然后调用 AddNodes 来填充第一级分组下的所有节点。AddNodes 递归调用自身以处理任意多的级数,必要时可添加组节点和叶节点。使用两个基于 TreeNode 的自定义类以区别组节点和叶节点,并为两类节点提供各自相应的属性。

  自定义 TreeView 事件

  每当选择一个节点时,TreeView 都会引发两个事件:BeforeSelectAfterSelect。但在我的控件中,我想使组节点和叶节点的事件不同,于是便添加了自己的事件 BeforeGroupSelect/AfterGroupSelectBeforeLeafSelect/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

  自定义节点类(TreeLeafNodeTreeGroupNode)和自定义事件参数类均包括在可下载代码中。

  示例应用程序

  要全面理解本示例控件中的所有代码,您应该了解它在应用程序中的运行情况。包含的示例应用程序使用 pubs.mdb Access 数据库,并说明 Tree 控件如何与其他数据绑定控件一起创建 Windows 应用程序。本例中,尤其值得注意的主要功能包括树与其他绑定控件的同步以及对数据源执行搜索时树节点的自动选择。

注意:本示例应用程序(名为“TheSample”)包含在本文的下载中。

图 4:数据绑定 TreeView 的演示应用程序

  小结

  本文介绍的数据绑定 Tree 控件并非适用于所有需要 Tree 控件来显示数据库信息的项目,但它确实介绍了一种可针对个人目的自定义该控件的方法。请记住,您要生成的任何复杂数据绑定控件与 Tree 控件的大部分代码基本相同,您可以通过修改现有代码来简化以后的控件开发过程。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表