尽管Visual Basic标准控件DriveListBox、DirListBox和FileListBox在一些环境中能很好使用,但若想显示类似Windows Explorer的文件夹结构就显得力不从心了。这时就必须使用TreeView控件,因为它为文件、文件夹和驱动器显示提供了完善的解决方案,并且系统的FileSystemObject对象能有效地获得主机的目录信息。
本文将介绍如何使用FileSystemObject将指定的目录或全部的驱动器与TreeView控件相结合,以及如何消除当驱动器全部文件列表填充TreeView时所遇到的缺陷。作为技巧,文中还讨论如何用SendMessage()的API函数来对TreeView内容的快速清除。事实上,这是一种最快捷的方法,它也可用于其他控件中。
关于FileSystemObject对象库
众所周知,FileSystemObject对象库是在Visual Basic Scripting Edition 2.
中发布的,它包括一系列用于驱动器、文件夹和文件的操作。为了能在Visual Basic中使用FileSystemObject,必须引用SCRRUN.DLL文件以便能使用它的属性、方法和事件。在VB工程中添加SCRRUN.DLL的引用,是通过选择"Project"(工程)->"References"(引用)命令,然后选定"Microsoft Scripting Runtime"项来进行的。一旦引用后,就可用FileSystemObject方法获得文件夹名称以及每个文件夹下的文件名。
FileSystemObject允许用户在当前磁盘中进行文件操作,如复制、删除以及其他文件夹、文件操作等。本文着重讨论与文件夹结构相关的方法,即如何获得文件夹和文件名称。
创建示例
示例创建的过程是这样的,首先在默认的表单中添加必要的控件;然后添加代码,用来将目录内容添加在TreeView中;最后,利用SendMessageLong() API函数添加一个过程,用来清除TreeView中的内容。
创建用于文件和文件夹的表单
首先,创建一个新的Visual Basic工程;然后,将默认的表单标题设置为"Exploring the Windows' directory",再在表单上添置一个TreeView控件和一个命令按钮控件,保留默认名称。最后,将命令按钮的标题设置为"Fill TreeView"。
声明两个重要的变量
在这一步中,我们需要为表单添加一些代码。首先,需要安装SCRRUN.DLL的引用,从菜单栏上选择"Project"(工程)->"References"(引用)命令,在弹出的对话框中,选中"Microsoft Scripting Runtime"项。单击[OK]按钮关闭对话框。然后,在表单中右击鼠标,从弹出的快捷菜单中选择"View Code"命令,在声明部分添加下列语句:
Const DEFAULT_DRIVE = "C:\"
Dim FSO As New Scripting.FileSystemObject
这里为简单起见,我们用一个常量来指定建立的目录树的默认驱动器。当然,最好能让用户对这个值作出选择。
用户或许想知道为什么在表单层中声明一个FSO变量,而不是在实际读取文件信息的过程中声明。这完全是一次偶然机会的心得,因为在许多好的程序中,通常允许用户用多种方式处理选择的文件。在表单层范围内定义一个FSO变量,其目的是可以处理所有其他FileSystemObject成员,而不必每次都有定义一个对象变量。
基于这种考虑,在表单被调用时应对FSO变量进行初始化,而当表单关闭后,该变量应被清除。这项工作的最终完成是通过下面的代码:
Private Sub Form_Load()
Set FSO = New FileSystemObject
End Sub
Private Sub Form_Unload(Cancel As Integer)
Set FSO = Nothing
End Sub
添加命令按钮Click()的事件
下面来添加代码用于TreeView控件的填充,这个过程是通过命令按钮的Click()事件来处理的,具体的代码如Listing A所示。代码中,一开始是定义几个变量,然后判断TreeView控件是否已经包含默认驱动器下的节点内容。若是,则不需要重新填充TreeView控件。若不是,则根据已知的节点创建新的节点变量,并连同TreeView控件一起调用PopulateTreeView()过程。显然,这个定制的子程序需要两个参数,一个是要填充的TreeView对象,另一个是要创建的目录树的父节点。PopulateTreeView()过程将在后面进行详细讨论。
Listing A:
Private Sub Command1_Click()
Dim strRootDir As String
Dim ndRoot As Node
strRootDir = UCase(DEFAULT_DRIVE)
On Error Resume Next
Set ndRoot = TreeView1.Nodes(strRootDir)
On Error GoTo
If ndRoot Is Nothing Then
Set ndRoot = TreeView1.Nodes.Add(, , strRootDir, _
strRootDir)
ndRoot.Sorted = True
PopulateTreeview TreeView1, ndRoot
End If
Set ndRoot = Nothing
End Sub
消除长时间等待的缺陷
在这一步中,我们需要考虑到定制的过程必须能从默认的目录中读取完整的文件信息并能填充到TreeView控件中。在磁盘大小越来越大的今天,这种方式却是非常不明智的。为了能得到目录的文件列表,Visual Basic必须在给定的文件夹下不断循环搜索所有的文件和子文件夹,当发现一个另外的子文件夹时,又要开始同样的循环,直到最后一个文件被找到为止。用这种方式读取全部的目录信息必然使程序挂起许多分钟。如果假设读取的磁盘,不仅仅是主机上的,而且还有网络上的,那么Visual Basic可能要耗上几个小时的时间。
作为技巧,我们在获取文件信息采用"即需即用"的原则。也就是说,只将在用户选定的文件夹的目录树的信息填充到TreeView控件中。是啊,一个从来就没有被选定的内容又怎么能去填充呢?
也就是说,在TreeView控件中,只有当用户展开一个文件夹项时,代码才填充该文件夹下的目录信息。基于这种思想,在TreeView控件的Expand()事件中必须调用定制的PopulateTreeView()过程,如下面的代码:
Private Sub TreeView1_Expand(ByVal Node _As MSComctlLib.Node)
PopulateTreeview TreeView1, Node
End Sub
这样,当Visual Basic调用该过程时,它处理被选中的节点,理论上包含能建立其他文件结构的目录。现在我们在这里提供相应的代码,用来添加到PopulateTreeView()过程中。
增加PopulateTreeView()程序
这一步,我们准备添加定制的PopulateTreeView()过程。前面已提及,我们的这个子程序是通过其中一个父节点参数创建相应的目录结构。但是,我们每次向TreeView添加的目录应该包含多少层呢?如果代码在所有的嵌套子文件夹中不断循环,这必然有潜在的缺陷,因为获取所有的硬盘文件结构信息必然需要大量的时间。另一方面,我们的代码也不能简单地在一个过程查找当前子文件夹下的文件信息。
为了能让TreeView控件显示子目录节点旁的"+"、"-"号,我们的代码必须填充其子项内容。但实际上,尽管用户可能仅仅选择显示C驱动器的目录结构,但我们的子程序还必须能添加所有可见的子目录内容。幸运的是,这第二层的内容仍然可以用代码去填充到TreeView控件中去。Listing B 是定制的PopulateTreeView()过程的全部代码:
Listing B:
Sub PopulateTreeview(trvw As TreeView, ndParent As Node)
Dim fldrParent As Folder
Dim fldrChildren As Folders
Dim FoundFile As Files
Dim FoundDir As Folder
Dim FoundFiles As Files
Dim iDir As Integer, iFile As Integer
Dim ndChild As Node
Dim blnHasChildren As Boolean
Set fldrParent = FSO.GetFolder(ndParent.Key)
Set fldrChildren = fldrParent.SubFolders
Set FoundFiles = fldrParent.Files
blnHasChildren = CBool(ndParent.Children)
With trvw
For Each FoundDir In fldrChildren
If blnHasChildren Then
Set ndChild = .Nodes(FoundDir.Path)
Else
Set ndChild = .Nodes.Add(ndParent.Key, _ tvwChild, FoundDir.Path, FoundDir.Name)
ndChild.Sorted = True
End If
If ndParent.Expanded Then
PopulateTreeview trvw, ndChild
End If
Next FoundDir
For Each FoundFile In FoundFiles
On Error Resume Next
Set ndChild = .Nodes(FoundFile.Path)
On Error GoTo
If ndChild Is Nothing Then
.Nodes.Add FoundFile.ParentFolder.Path, _tvwChild, FoundFile.Path, FoundFile.Name
End If
Next FoundFile
End With
Set ndChild = Nothing
Set fldrParent = Nothing
Set fldrChildren = Nothing
Set FoundFiles = Nothing
End Sub
在不工作中填充TreeView的子项
要查看这个定制过程的结果,按[F5]来运行这个工程。当Visual Basic显示默认的表单时,单击[Fill TreeView]按钮。此时,产生Click()事件,并运行PopulateTreeView()过程,处理TreeView1和ndRoot节点(此时是C:)。然后,该过程得到一个基于ndParent参数的文件夹对象以及该文件夹下的子文件夹。这时,代码循环所有的子文件夹。如果代码查找到父节点存在相应的子项内容,则代码为该父节点添加子文件夹,然后该过程为TreeView项目添加一个子节点。如果当前父节点在TreeView中没有子项,但是与父节点相关的父文件夹包含子文件夹,则代码向TreeView添加新的文件夹名。
然后,该过程判断父节点是否展开。若是,则代码添加相应的子文件夹内容,这样在TreeView层次上出现"+"、"-"号。为了达到这一点,后面调用的PopulateTreeView()就是用子节点来处理的。在第二次调用这个过程时,由于子节点没有展开,因此PopulateTreeView()过程不会再被调用。最后,该过程在该文件夹下循环所有的文件,并将其填充到TreeView中。
快速清除TreeView控件中的内容
在这个示例中,用户可以通过单击命令按钮在TreeView控件中添加文件夹和文件列表。在实际的应用程序中,我们应该可以让用户通过相应的控件选择不同的开始目录。因此,在用新项目填充TreeView控件之前,还必须删除该控件中的原有内容。通常,我们可以使用递归的方式来遍历TreeView控件中的所有节点,然后删除相应的项目,但是却要花费不少时间,尤其是当TreeView控件含有更多的项目和嵌套时。一个最简单快捷的方法,是使用SendMessage() API函数,在一次循环后就可将TreeView控件的内容删除。
为了说明它的工作原理,我们返回到默认表单的设计视图中,再在表单中添加一个命令按钮,打开表单的代码窗口,在声明部分添加如Listing C所示的API声明。
Listing C:
Const TV_FIRST As Long = &H11
Const TVM_GETNEXTITEM As Long = (TV_FIRST + 1)
Const TVM_DELETEITEM As Long = (TV_FIRST + 1)
Const TVGN_ROOT As Long = &H
Const WM_SETREDRAW As Long = &HB
Private Declare Function SendMessageLong Lib "user32" _
Alias "SendMessageA" (ByVal hWnd As Long, ByVal msg _
As Long, ByVal wParam As Long, ByVal lParam As Long) _
As Long
然后,添加Command2的Click()事件,并增加下列代码:
Private Sub Command2_Click()
'Remove all items from the Treeview
ClearTreeView TreeView1.hWnd
End Sub
最后,建立一个ClearTreeView()子程序,如Listing D所示。
Listing D:
Private Sub ClearTreeView(ByVal tvHwnd As Long)
Dim lNodeHandle As Long
'Turn off redrawing first
SendMessageLong tvHwnd, WM_SETREDRAW, False,
'Remove each node in the treeview
Do
lNodeHandle = SendMessageLong(tvHwnd, _
TVM_GETNEXTITEM, TVGN_ROOT,
)
If lNodeHandle Then
SendMessageLong tvHwnd, TVM_DELETEITEM,, _lNodeHandle
Else
Exit Do
End If
Loop
SendMessageLong tvHwnd, WM_SETREDRAW, True,
End Sub