一个全 VB.NET 资源管理器树形控件,带ImageList管理






4.87/5 (132投票s)
带有 Shell 文件夹访问类和图标管理的 Explorer TreeView 控件。
引言
ExpTree
控件是一个类似 Windows Explorer 的 TreeView 控件。它显示所有正确的图标,并带有适当的覆盖。所有 Windows 文件夹,包括桌面、我的电脑和历史记录等虚拟文件夹,都会被正确显示并可供包含的窗体使用。该控件包含并使用了一个优化的图像列表管理类,该类为应用程序提供了小图标和大图标的图像列表。该控件只是一个强大的类库 (ExpTreeLib
) 的可视化部分,该库提供了超出 DirectoryInfo
和 FileInfo
类组合的功能。如上图所示,ExpTreeLib
可以轻松用于创建类似 Windows Explorer 的 ListView,并与 ExpTree
配合使用。
尽管 .Net 的 FolderBrowserDialog
在许多情况下是一个有用的替代品,但 ExpTree
是一个真正的控件,可以像 Windows Form 上的任何其他控件一样进行操作。它拥有一个定义明确的接口,该接口向窗体提供选定的节点更改通知,并允许在设计时和运行时操作显示树的关键方面。
我将此代码作为 Visual Studio 2005 解决方案分发,可以通过 Visual Studio 升级向导无错误地升级到 VS2008 和/或 VS2010。它以 Framework 2.0 为目标。
版本概述
ExpTreeLib
类库有两个支持的版本。版本 2.12(在本文中描述并可下载)和版本 3.00。版本 2.12 提供了 Windows Shell 命名空间(包括文件系统)的**静态**视图。版本 2.12 是在论坛中讨论过的“Rollup”版本包的增强版本。
版本 3.00(将在即将撰写的文章中介绍)提供了该命名空间的**动态**视图并增加了一些功能。它是论坛中提到的“未发布”版本的增强版本。这两个版本多年来都发展了一批用户。版本 2.12 对于不需要动态视图或版本 3.00 附加功能的应用程序很有用,并且更容易理解和使用。**本文档提供了两个版本的基础文档**。
此版本(2.12)在树节点展开或选择时,或者通过控件之间的拖放操作更改时,提供节点的当前视图。控件外部对文件系统的更改,直到更改的节点被展开或选择时才会反映出来。此版本支持拖放操作的一个版本,该版本在本文的第 2 部分进行讨论:为 Explorer Tree 控件添加拖放功能。版本 2.12 和版本 3.00 之间的根本区别在于拖放操作的实现方式以及在 3.00 版演示窗体中的说明方式。
相对于版本 2.11,版本 2.12 包含了 Windows Vista/Window 7 所需的更改、其他 bug 修复以及附加优化。在 XP 系统上性能可接受的引用远程系统上大文件夹的应用程序,在版本 2.11 中,在 Vista/Windows7 上可能会变得非常慢。版本 2.12 为大多数常用应用程序修复了这个问题。在某些情况下,需要版本 3.00 才能恢复性能。
包含所有先前添加的功能和 bug 修复。有关此更新和先前更新的详细信息,请参阅历史。
目标受众
我编写本文档和代码时,面向的是开发者。我期望读者会查看代码并尝试使用。我试图让注释保持最新。我对任何可能导致库改进的建设性意见都感兴趣。
背景
我的设计目标是创建一个只需一个 .dll(无辅助包装 .dll)的控件,能在任何 Windows 系统上显示正确的图标,能同时处理虚拟文件夹和文件系统文件夹,速度快且资源占用少。由于我其余的代码都将用 VB.NET 编写,所以我希望该控件也用 VB.NET 编写。我在这个网站或其他任何网站上都找不到满足我要求的代码。几乎所有其他类似的控件都是用 C# 编写的,并且没有一个完全满足其他要求。
基于 DirectoryInfo
和 FileInfo
类的控件无法处理虚拟文件夹。通过添加对 Shell32.dll 的引用的控件需要额外的 .dll 来包装 COM 接口,并且无法报告隐藏文件和目录。使用任一方法的应用程序都需要附加类来处理图标,因为两者都不会获取图标信息。由于我曾经用 C 编写过 Shell 访问 .dll,所以我熟悉这些技术,因此我决定使用 IShell
文件夹接口,并使用 SHGetFileInfo
来提供图标信息来解决这个问题。
类概述
该控件 ExpTree
与几个支持类一起打包到一个库程序集和 .dll (ExpTreeLib
) 中。ExpTreeLib
包含以下类:
ShellDll |
API 声明、接口、结构、枚举和常量。 |
CShItem |
此库的主要类。 |
SystemImageListManager |
用于管理大、小系统图像列表的类。 |
ExpTree |
实际控件。 |
可以使用 SystemImageListManager
类,但 ExpTree
会初始化并使用它。如果应用程序需要显示文件系统图标,该类将提供这些图标。有关详细信息,请参阅下面的 SystemImageList Manager 类 部分。
CShItem
类的详细信息将在 下面 讨论。它封装了描述一个文件夹或文件的信息集合。在使用上,它类似于 DirectoryInfo
或 FileInfo
实例。但是,它是使用 Shell 的 IShellFolder
接口构建的,因此它可以表示系统上可用的所有文件夹和文件类型。
该库还包含其他仅用于拖放支持的类,此处不作讨论。
使用控件
要使用该控件,请将 .dll 添加到项目中,然后将控件添加到工具箱。要将控件添加到工具箱,请右键单击工具箱,选择“自定义工具箱”,然后浏览到 DLL。完成此操作后,您就可以像使用其他控件一样使用它了。除了标准的 UserControl
属性外,ExpTree
控件还公开了几个属性:
AllowDrop |
设计时和运行时 | 允许/禁止在树上拖放 |
ShowHiddenFolders |
设计时和运行时 | 显示/隐藏隐藏文件夹 |
ShowRootLines |
设计时和运行时 | 允许/禁止折叠 TreeRoot |
StartupDirectory |
设计时和运行时 | 选择树的根目录 |
RootItem |
仅运行时 | 将根设置为特定的 CShItem |
SelectedItem |
仅运行时 | 获取当前选定的 CShItem |
StartupDirectory
设置 TreeView 的根。它只接受 SystemFolder
作为启动目录。最有用的包括 Desktop
和 My Computer
。在设计时更改 StartUpDirectory
,以便在 IDE 中查看初始显示。
RootItem
是一个仅运行时属性,用于将树根重置为另一个文件夹,该文件夹可以是 TreeView 中可用的任何文件夹。
提示
要设置一个 ExpTree
,使其看起来像从某个非系统文件夹启动根目录
- 在 IDE 中,将
StartupDirectory
设置为 Desktop。 - 在窗体的
Load
事件中,将RootItem
设置为所需的文件夹,例如:
ExpTree1.RootItem = CShItem.GetCShItem("C:\MyAppData")
ExpTree 方法
方法 | 类型 | 备注 |
RefreshTree |
N/A | 通过 SelectedNode 重新构建树 |
ExpandANode |
布尔值 | 通过输入的 Path 或 CShItem 展开树 |
RefreshTree
和 ExpandANode
方法对于基本使用不是必需的,稍后将在 后面 讨论。
ExpTree 事件
StartUpDirectoryChanged |
用于设计时交互 |
ExpTreeNodeSelected |
当 TreeNode 被选中时引发 |
ExpTreeNodeSelected
的 EventArgs
是一个字符串,包含底层文件夹的完整路径和代表 SelectedNode
的 CShItem
。
假设您有一个窗体,其中包含一个名为 ExpTree
的 ExpTree
、一个名为 lv1
的 ListView
和一个名为 sbr1
的 StatusBar
。要使用该控件,您必须导入一些项:
Imports ExpTreeLib
Imports ExpTreeLib.CShItem
Imports ExpTreeLib.SystemImageListManager
Public Class frmExplorerLike
Inherits System.Windows.Forms.Form
在窗体的 New
例程中,设置使用图像列表
'Add any initialization after the InitializeComponent() call
SystemImageListManager.SetListViewImageList(lv1, True, False)
SystemImageListManager.SetListViewImageList(lv1, False, False)
SetListViewImageList
语句将 ListView
的 LargeImageList
和 SmallImageList
设置为相应的系统图像列表。
添加以下事件处理程序:
Private Sub lv1_VisibleChanged(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles lv1.VisibleChanged
If lv1.Visible Then
SystemImageListManager.SetListViewImageList(lv1, True, False)
SystemImageListManager.SetListViewImageList(lv1, False, False)
End If
End Sub
要更改 ExpTree1
中选择节点时 ListView
的内容,请声明如下事件处理程序:
Private Sub AfterNodeSelect(ByVal pathName As String,
_ ByVal CSI As CShItem) Handles
ExpTree1.ExpTreeNodeSelected Dim dirList As New
ArrayList() Dim fileList As New
ArrayList() Dim TotalItems As
Integer If CSI.DisplayName.Equals(CShItem.strMyComputer) Then
'avoid re-query since only has dirs
dirList = CSI.GetDirectories
Else
dirList = CSI.GetDirectories
fileList = CSI.GetFiles
End If
TotalItems = dirList.Count + fileList.Count
If TotalItems > 0 Then
Dim item As CShItem
dirList.Sort()
fileList.Sort()
Me.Text = pathName
sbr1.Text = pathName & " " & _
dirList.Count & " Directories " & _
fileList.Count & " Files"
Dim combList As New ArrayList(TotalItems)
combList.AddRange(dirList)
combList.AddRange(fileList)
'Build the ListViewItems & add to lv1
lv1.BeginUpdate()
lv1.Items.Clear()
For Each item In combList
Dim lvi As New ListViewItem(item.DisplayName)
With lvi
'
' SubItem formatting and adding to lvi omitted from
' article text
'
'Set ListViewItem's IconIndex
'(and add Icon to lists if necessary)
.ImageIndex = _
SystemImageListManager.GetIconIndex(item, False)
.Tag = item
End With
lv1.Items.Add(lvi)
Next
lv1.EndUpdate()
Else
sbr1.Text = pathName & "Has No Items"
End If
End Sub
开头的“strMyComputer
”测试使用本地系统字符串“My Computer”来避免每次选择“My Computer”时都重新查询 A: 驱动器。为了像 Windows Explorer 那样对文件夹和文件进行排序,需要一些特殊的处理。这种特殊的处理是在 CShItem
的 IComparable.CompareTo
例程中完成的,该例程在排序目录和文件 ArrayList
时被调用。
通过调用 GetIconIndex
来处理图标的获取和添加到两个图像列表。GetIconIndex
的第二个参数设置为 False
,表示不获取“打开”IconIndex
。
注意:下载的演示项目在一个单独的线程中获取和设置图标索引。上面显示的仅是简化的单线程方法。
SystemImageListManager 类
此类基于系统图像列表。它访问两个系统图像列表,一个包含小图标,一个包含大图标。这两个列表是同步的,因此相同的 IconIndex
在两个列表中都指向同一个图标。当查询 CShItem
的 IconIndex
时,它会确定图标是否需要覆盖,如果需要,则将带有覆盖的图标作为附加图标添加到系统图像列表中。由于 SHGetFileInfo
返回的 IconIndex
不一定是文件夹或文件的实际 IconIndex
(可能需要使用带覆盖的图标),因此我使用 HashTable
来存储实际的 IconIndex
。HashTable
的键基于报告的 IconIndex
,并进行修改以反映任何附加覆盖。实际上,系统图像列表中存储的图标数量以及 HashTable
的大小都相当小。
SystemImageListManager
类仅包含 Shared
属性和方法。由于它管理外部资源(两个系统图像列表),因此只需要或适合使用 Shared
属性和方法。*请勿*在 SystemImageListManager
之外以任何方式修改这两个系统图像列表。
SystemImageListManager
的任何属性或方法都会调用该类的 Initializer
例程。
SystemImageListManager Initializer
Private Shared Sub Initializer()
If m_Initialized Then
Exit Sub
End If
Dim dwFlag As Integer = SHGFI.USEFILEATTRIBUTES Or _
SHGFI.SYSICONINDEX Or _
SHGFI.SMALLICON
Dim shfi As New SHFILEINFO()
m_smImgList = SHGetFileInfo(".txt", _
FILE_ATTRIBUTE_NORMAL, _
shfi, _
cbFileInfo, _
dwFlag)
If m_smImgList.Equals(IntPtr.Zero) Then
Throw New Exception("Failed to create Small ImageList")
End If
'
' Identical code as above using SHGFI.LARGEICON
' Omitted from Article text ... see source code
'
m_Initialized = True
End Sub
Initializer
检查这是第一次调用。如果不是,则假定所有都已设置好。如果是第一次调用,它会获取小系统图像列表和大系统图像列表的 Handle
,每次都检查是否成功。
SystemImageListManager 属性
SmallList |
小 ImageList 的句柄 |
LargeList |
大 ImageList 的句柄 |
这些 ReadOnly
属性可能对应用程序有用。演示程序仅在类内部使用它们。
SystemImageListManager 方法
GetIconIndex
Public Shared Function GetIconIndex(ByRef item As CShItem, _
Optional ByVal GetOpenIcon As Boolean = False, _
Optional ByVal GetSelectedIcon As Boolean = False _
) As Integer
GetIconIndex
返回 CShItem
项目所需图标在两个系统图像列表中的 IconIndex
。Optional
参数 GetOpenIcon
指示 GetIconIndex
返回 CShItem
的“打开”图标,而不是“正常”图标。Optional
参数 GetSelectedIcon
请求“选定”图标。
在内部,SystemImageListManager
维护一个 HashTable
,其 Key
基于系统图像列表的 IconIndex
、引用的 CShItem
的链接和共享状态以及图标的“打开”或“选定”状态。存储在 HashTable
中的 Value
是图标在系统图像列表中的 IconIndex
。
如果 IconIndex
已知(在 HashTable
中),则函数仅返回 HashTable
的 Value
作为函数返回值。
如果所需的图标未知(不在 HashTable
中)且图标将包含覆盖,则函数使用 SHGetFileInfo
获取图标,将其存储在系统图像列表中,将 IconIndex
插入 HashTable
,然后返回 IconIndex
作为函数返回值。
如果所需的图标不包含覆盖,那么 CShItem
中已存储的 IconIndex
就是正确的 IconIndex
。在这种情况下,函数将 IconIndex
存储到 HashTable
中,并将其作为函数返回值。
GetIcon
Public Shared Function GetIcon(ByVal Index As Integer, _
Optional ByVal smallIcon As Boolean = False) _
As Icon
返回图像列表在指定 Index
处的图标(默认为大图标)的 GDI+ 副本。
SetListViewImageList
Public Shared Sub SetListViewImageList( _
ByVal listView As ListView, _
ByVal forLargeIcons As Boolean, _
ByVal forStateImages As Boolean)
此方法将相应的系统图像列表附加到 ListView
的 ImageList
。forLargeIcons
参数选择哪个列表附加到哪个(True
表示大,False
表示小)。我没有对 forStateImages
参数做任何处理,只是将其传递为 False
。
SetListTreeViewImageList
Public Shared Sub SetTreeViewImageList( _
ByVal treeView As TreeView, _
ByVal forStateImages As Boolean)
此方法将相应的系统图像列表附加到 TreeView
的 ImageList
。我从未测试或使用过 forStateImages
参数,只是将其设置为 False
。
这两种方法都使用 SendMessage
API 向控件发送消息,将系统图像列表附加为控件的 ImageList
。有关详细信息,请参阅源代码。请注意,.NET 不知道此附件。如果控件被隐藏然后显示,则必须在 VisibleChanged
事件处理程序中重新建立附件。
CShItem 类
版本 2 更改
CShItem
类是 ExpTree 库的主要类。库的任何部分的添加通常都需要对其进行更改。版本 2 与先前版本之间的主要更改实际上发生在 CShItem
中。每个代表文件夹的 CShItem
都维护一个 CShItem
的 ArrayList
,代表其包含的子文件夹。在版本 2 之前的版本中,该 ArrayList
从未更新过。如果以参数 Optional Refresh As Boolean = False
调用 GetDirectories
方法并设置为 True
,则整个 ArrayList
将被丢弃并重新创建。在版本 2 中,参数 Optional doRefresh As Boolean = True
指示 GetDirectories
调用一个新函数 RefreshDirectories
。RefreshDirectories
检查更改并为新添加的目录创建新的 CShItem
,并删除代表不再存在的目录的 CShItem
。此过程由其他新代码实现,以低成本方式完成,因此可以频繁进行此目录刷新。在 ExpTree 中,它在每次 TreeNode 展开、每次选择以及与拖放相关的其他几个情况下完成。
请注意,代表文件的 CShItem
不会被保留,而是在每次 GetFiles
或 GetItems
请求时重新生成。一个潜在的研究方向是考察将文件 CShItem
与目录项类似地对待所涉及的内存与处理器时间的权衡。
此类版本 3.00 的方法完全不同。
构造函数
版本 2 弃用使用 New
作为获取 CShItem
的方法.
在版本 2 中,仍然支持 Sub New(ID As CSIDL)
和 Sub New(path As String)
例程。但是,版本 3.00 不支持任何 Public Sub New。对于版本 2,首选的替换功能由此处描述的 GetCShItem
例程提供:
GetCShItem(ByVal ID As CSIDL) As CShItem
CSIDL
是在ShellDll
类中声明的表示系统特殊文件夹的Enum
。ExpTree
为其用途定义了一个子集。与等效的Sub New
不同,除了在特定操作系统上可用性之外,对可以使用哪个CSIDL
没有限制。用法由此代码片段说明,该片段获取 My Computer 的CShItem
。Dim special As CShItem special = GetCShItem(CSIDL.DRIVES)
GetCShItem(ByVal path As String) As CShItem
path
是一个有效的目录路径(例如,“C:\”)。路径可以是任何CShItem.Path
属性,包括 GUID。此代码片段说明了获取特定目录的CShItem
的简单用法:Dim special As CShItem special = GetCShItem("C:\Temp\Test")
属性
属性 | 类型 | 备注 |
DisplayName |
字符串 |
显示名称 |
Path |
字符串 |
完整路径(请参阅注释 1) |
TypeName |
字符串 |
项目类型(请参阅注释 2) |
FullName |
字符串 |
项目的完整名称(请参阅注释 3) |
IconIndexNormal |
整数 |
在 SystemImageList 中的索引 |
IconIndexOpen |
整数 |
在 SystemImageList 中的索引 |
HasSubFolders |
布尔值 |
可能包含子文件夹 |
IsBrowsable |
布尔值 |
可以就地浏览 |
IsDropTarget |
布尔值 |
项目可以被拖放到此处 |
IsFileSystem |
布尔值 |
属于文件系统 |
IsFolder |
布尔值 |
是文件夹 |
IsDisk |
布尔值 |
是磁盘 |
IsLink |
布尔值 |
是快捷方式 |
IsRemovable |
布尔值 |
是可移动设备 |
IsReadOnly |
布尔值 |
是 ReadOnly |
IsShared |
布尔值 |
是 Shared |
IsSystem |
布尔值 |
是系统文件 |
LastWriteTime |
日期时间 |
请参阅 FileInfo 文档 |
LastAccessTime |
日期时间 |
请参阅 FileInfo 文档 |
CreationTime |
日期时间 |
请参阅 FileInfo 文档 |
长度 |
长整型 |
文件大小(字节) |
CanCopy |
布尔值 |
项目可复制 |
CanDelete |
布尔值 |
项目可删除 |
CanLink |
布尔值 |
项目可创建链接 |
CanMove |
布尔值 |
项目可移动 |
PIDL |
IntPtr |
可用在 SHGetFileInfo 中 |
clsPidl |
cPidl |
用于将 PIDL 作为 Byte() 操作的类 |
strMyComputer |
字符串 |
此计算机上的“我的电脑” |
strSystemFolder |
字符串 |
此计算机上的“系统文件夹” |
DesktopDirectoryPath |
字符串 |
用户桌面目录的路径 |
所有属性均为 ReadOnly
。
注释 1:对于文件系统对象,FullPath
属性就是其完整路径。对于非文件系统对象,完整路径可能是 GUID。
注释 2:TypeName
是 SHGetFileInfo
报告的类型名称。
注释 3:FullName
通常与 DisplayName
相同。但是,对于 .lnk 文件,DisplayName
不包含 .lnk 扩展名。而 FullName
包含。给定一个路径为“C:\Temp\ABC.txt.lnk”的链接文件,Displayname
将返回“ABC.txt”,FullName
将返回“ABC.txt.lnk”。
IconIndex...
属性报告系统图像列表的基本 IconIndex
。这对于应用程序来说不是直接有用的,除非只想要非覆盖图标。
在几乎所有情况下,都不应使用 PIDL
。它之所以可见,仅仅是因为 SystemImageListManager
需要引用它。如果应用程序需要调用某些 Shell .dll,PIDL
可能有用。clsPidl
属性是 cPidl
类的实例。它公开了将 PIDL
作为 Byte()
检查的方法。有关更多信息,请参阅源代码。
...Time
属性和 Length
属性与 FileInfo
类返回的值完全相同。我在请求其中任何一个值时会“作弊”并创建一个 FileInfo
实例来检索这些值。
str...
属性提供了在运行应用程序的计算机上表示“我的电脑”和“系统文件夹”的字符串。这提供了一种不区分区域设置的方法来测试这些特殊名称。特殊名称“Desktop”由 GetDeskTop
返回的 CShItem
的 DisplayName
提供,并且也是区域设置无关的。
方法
方法 | 返回类型 | 返回值 |
共享方法 | ||
GetCShItem |
CShItem |
有关描述,请参阅上文 |
GetDeskTop |
CShItem |
桌面 |
实例方法 | ||
GetDirectories |
CShItem 对象的 ArrayList |
CShItem 中的所有文件夹 |
GetFiles |
CShItem 对象的 ArrayList |
CShItem 中的所有文件 |
GetItems |
CShItem 对象的 ArrayList |
CShItem 中的所有文件和文件夹 |
RefreshDirectories |
布尔值 |
如果发生任何更改,则为 True 。 |
ToString |
字符串 |
DisplayName |
DebugDump |
无 | 将信息写入 Debug 控制台 |
GetDeskTop
返回桌面唯一的一个 CShItem
。该类内部维护此 CShItem
,并在类首次以任何方式访问时构建它。GetDeskTop
返回实际的 CShItem
,而不是副本。
GetDirectories
、GetFiles
和 GetItems
按请求返回 CShItem
对象的 ArrayList
。如果文件夹中没有请求的类型,它们将返回一个空的 ArrayList
。如果 CShItem
代表一个文件,则返回一个空的 ArrayList
。在常见错误情况下(例如,驱动器未就绪(例如,空的 CD 驱动器)),也返回一个空的 ArrayList
。与 Windows Explorer 不同,类在这些情况下不会显示“中止-重试”消息框。版本 2 之前的先前版本会在内部调用此方法时抛出异常(仅在 Debug 模式下编译时)。版本 2 及更高版本将不再抛出异常,而是在任何错误情况下返回一个空的 ArrayList
。
RefreshDirectories
确保 GetDirectories
返回的 ArrayList
反映文件系统的当前状态。如果发生任何更改,它将返回 True
。RefreshDirectories
由 GetDirectories
调用(除非通过 Optional
参数明确禁止),因此很少需要直接调用它。
ExpTree 控件
最后,我们来看控件本身。有了 CShItem
和 SystemImageListManager
类,控件就相对简单了。
ExpTree 属性和事件
属性 | |
AllowDrop |
允许(True )或阻止(False )拖放到树上。 |
StartUpDirectory |
必须是特殊文件夹的 CSIDL 。 |
RootItem |
将树的根设置为 CShItem 。 |
SelectedItem |
返回当前 SelectedNode 的 CShItem 。 |
ShowHidden |
允许/禁止在 TreeView 中显示隐藏目录。 |
ShowRootLines |
允许/禁止在 TreeView 中显示线条和展开/压缩框。 |
事件 | |
ExpTreeNodeSelected |
在选中 TreeNode 时引发。 |
StartUpDirectoryChanged |
在设置 StartUpDirectory 属性时引发。 |
方法 | |
ExpandANode |
通过代表输入 CShItem 的节点展开树。展开失败时返回 False 。 |
RefreshTree |
重新初始化树并通过先前选定的节点展开它。 |
StartUpDirectory
是一个 CSIDL
,代表一个系统特殊文件夹。控件可以处理的文件夹列表可通过 ExpTree
的属性表在 IDE 中获得。
RootItem
是一个仅运行时属性。通过运行时调用设置此项会导致将整个树重置为以输入的 CShItem
为根。CShItem
必须是某种文件夹(文件文件夹或系统文件夹)的有效 CShItem
。尝试使用非文件夹 CShItem
设置它将被忽略。此属性的使用在演示程序中的 ListView
的 MouseUp
事件中以及“C:\ Test”按钮后面的代码中得到了说明。
ExpTreeNodeSelected
是在 TreeView
的 AfterSelect
事件发生时引发的事件。这会将事件通知给包含的 Form
。Event
签名是
Public Event ExpTreeNodeSelected(ByVal SelPath As String, _
ByVal Item As CShItem)
其中 Item
是代表选定节点的 CShItem
,SelPath
是该 CShItem
的路径。对于虚拟文件夹,路径是 GUID,SelPath
包含 CShItem
的 DisplayName
。
StartUpDirectoryChanged
是在设置初始目录时引发的事件。为了让包含的 Form
需要此事件的通知,它被设置为 Public
。通常,这不需要,因为根目录的更改总是会选择新根,从而引发 ExpTreeNodeSelected
事件。
ExpandANode
是对本文论坛中提供的原始 ExpandANode
的修订。内部而言,此方法与原始版本有很大不同,并且不限于文件系统目录。任何 CShItem
都可以用作输入路径。与原始版本不同,此版本不会强制将树根固定在 Desktop
或 My Computer
上。此版本将保留原始树根。该方法从树根展开树,通过输入的 CShItem
按需展开节点。其签名是
Public Function ExpandANode(ByVal newItem As CShItem) As Boolean
该方法返回 True
表示展开成功,否则返回 False
。该类提供了一个备用的 ExpandANode
,它接受 Path
作为其参数。备用签名方法调用 GetCShItem
,检查返回值,然后使用返回的 CShItem
调用其他 ExpandANode
。
RefreshTree
是一个方法,它导致整个树被重新创建,然后展开到原始(在调用 RefreshTree
之前)选定的节点。这使得树能够反映控件外部目录结构所做的更改。如果原始选定节点不再有效,例如,它和/或其路径中的某些早期部分已被删除或重命名,则树将通过其原始路径中的最低有效点进行展开。此方法代码几乎与 Calum McLellan 在论坛中提供的代码相同。一个区别是,它现在默认将树根固定在原始树根,而不是默认固定到 Desktop
。另一个区别是它抑制了 ExpTreeNodeSelected
事件的引发,直到刷新完成。此方法受益于新版本的 ExpandANode
,因为它不再局限于只处理文件系统目录。
此方法的签名是
Public Sub RefreshTree(Optional ByVal root As CShItem = Nothing)
可选参数 root
允许在刷新操作中动态重置树根。
ExpTree 代码
在 ExpTree
的初始化中,我们设置了 TreeView
的 ImageList
并添加了控件对 StartUpDirectory
更改的处理程序。
'Add any initialization after the InitializeComponent() call
SystemImageListManager.SetTreeViewImageList(tv1, False)
AddHandler StartUpDirectoryChanged, AddressOf OnStartUpDirectoryChanged
OnStartUpDirectoryChanged(m_StartUpDirectory)
Public Property
StartUpDirectory
在设置或更改 StartUpDirectory
时开始工作
Private m_StartUpDirectory As StartDir = StartDir.Desktop
<Category("Options"), _
Description("Sets the Initial Directory of the Tree"), _
DefaultValue(StartDir.Desktop), Bindable(True)> _
Public Property StartUpDirectory() As StartDir
Get
Return m_StartUpDirectory
End Get
Set(ByVal Value As StartDir)
If Array.IndexOf(Value.GetValues(Value.GetType), _
Value) >= 0 Then
m_StartUpDirectory = Value
RaiseEvent StartUpDirectoryChanged(Value)
Else
Throw New ApplicationException( _
"Invalid Initial StartUpDirectory")
End If
End Set
End Property
属性属性为设计器提供信息。If Array.IndexOf...
处的代码将输入的 Value
与 Enum
的允许值进行比较,如果无效则 Throw
异常。如果有效,则设置私有版本的属性并引发 StartUpDirectoryChanged
Event
。
实际工作在 OnStartUpDirectoryChanged
事件处理程序中完成
Private Sub OnStartUpDirectoryChanged(ByVal newVal As StartDir)
If Not IsNothing(Root) Then
ClearTree()
End If
Dim L1 As ArrayList
Dim special As CShItem
special = GetCShItem(CType(Val(m_StartUpDirectory), ShellDll.CSIDL))
Root = New TreeNode(special.DisplayName)
BuildTree(special.GetDirectories)
Root.ImageIndex = SystemImageListManager.GetIconIndex(special, _
False)
Root.SelectedImageIndex = Root.ImageIndex
Root.Tag = special
tv1.Nodes.Add(Root)
Root.Expand()
End Sub
Private Function BuildTree(ByVal L1 As ArrayList)
L1.Sort()
Dim CSI As CShItem
For Each CSI In L1
If Not (CSI.IsHidden And Not m_showHiddenFolders) Then
Root.Nodes.Add(MakeNode(CSI))
End If
Next
End Function
Private Function MakeNode(ByVal fi As CShItem) As TreeNode
Dim newNode As New TreeNode(item.DisplayName)
newNode.Tag = item
newNode.ImageIndex = SystemImageListManager.GetIconIndex(item, False)
newNode.SelectedImageIndex = SystemImageListManager.GetIconIndex(item, True)
If item.IsRemovable Then
newNode.Nodes.Add(New TreeNode(" : "))
ElseIf item.HasSubFolders Then
newNode.Nodes.Add(New TreeNode(" : "))
ElseIf item.GetDirectories.Count > 0 Then
newNode.Nodes.Add(New TreeNode(" : "))
End If
Return newNode
End Function
Private Sub ClearTree()
tv1.Nodes.Clear()
Root = Nothing
End Sub
首先,获取并排序基础文件夹。对于基础中的每个文件夹,我们创建一个新的 TreeNode
,使用正确的图标,并将其添加到根节点。请注意,每个 TreeNode
的 Tag
都设置为它所属的 CShItem
。如果子文件夹本身可能包含子文件夹,我们会创建一个虚拟节点并将其添加到子节点,这样 Treeview
就会显示一个“+”并允许展开。
BuildTree
中检查 .IsHidden
的代码,如果 ShowHiddenFolders
属性为 False
,则会阻止在 TreeView
中显示隐藏目录。MakeNode
中的 If ... ElseIf
序列避免检查软盘驱动器,以防止烦人的软盘访问。它还解决了包含所有隐藏成员的目录被 .HasSubFolders
报告为 False
的事实。最后,我们将根节点附加到 TreeView
并 Expand
根节点以获得最终显示。
Treeview
的 BeforeExpand
Event
与上述代码非常相似。有趣的部分是
Private Sub tv1_BeforeExpand(ByVal sender As Object, _
ByVal e As System.Windows.Forms.TreeViewCancelEventArgs) _
Handles tv1.BeforeExpand
Dim oldCursor As Cursor = Cursor
Cursor = Cursors.WaitCursor
If e.Node.Nodes.Count = 1 AndAlso _
e.Node.Nodes(0).Text.Equals(" : ") Then
e.Node.Nodes.Clear()
Dim CSI As CShItem = e.Node.Tag
Dim D As ArrayList = CSI.GetDirectories(m_refresh)
If D.Count > 0 Then
'.... processing steps omitted
End If
Else
RefreshNode(e.Node)
End If
Cursor = oldCursor
End Sub
如果节点是虚拟节点,则将其清除并以与上述代码类似的方式处理。
否则,假设子节点已设置好,并调用 RefreshNode
,确保内容与实际情况匹配。请注意,如果此节点没有子文件夹,则 TreeNode
将被清除,删除“+”并阻止将来的扩展。
最后,我们处理 AfterSelect
Event
,它将 SelectedNode
的 CShItem
传递给包含的 Form
。
Private Sub tv1_AfterSelect(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.TreeViewEventArgs) _
Handles tv1.AfterSelect
Dim node As TreeNode = e.Node
Dim CSI As CShItem = e.Node.Tag
If CSI Is Root.Tag AndAlso Not tv1.ShowRootLines Then
With tv1
.BeginUpdate()
.ShowRootLines = True
RefreshNode(node)
.ShowRootLines = False
.EndUpdate()
End With
Else
RefreshNode(node)
End If
If EnableEventPost Then 'turned off during RefreshTree
If CSI.Path.StartsWith(":") Then
RaiseEvent ExpTreeNodeSelected(CSI.DisplayName, CSI)
Else
RaiseEvent ExpTreeNodeSelected(CSI.Path, CSI)
End If
End If
End Sub
SelectedNode
由 RefreshNode
更新。ShowRootLines
的测试是为了解决当测试条件为 True
时出现的显示问题。如果 RefreshTree
的事件发布没有被抑制,则引发 ExpTreeNodeSelected
事件,传递 CShItem
和节点的路径。请注意,某些系统文件夹的路径是一个 GUID。在这种情况下,我们返回 SelectedNode
的 DisplayName
而不是路径。真实的路径仍然可以在 CShItem
中找到。
演示程序和其他思考
演示窗体不做任何有用的工作,只是说明此处提供的控件和类的用法。我真的没有试图复制 Windows Explorer。演示包中有两个窗体。frmExplorerLike
(如下所示并在此处描述)仅用于说明如何使用 ExpTreeLib 中可用的一些方法。frmDragDrop
更现实一些,并演示了从 ExpTree 拖放和拖放到 ExpTree,以及从 ListView 拖放。
在 ListView
中左键单击一个文件夹,会导致在树中展开相应的文件夹,并在 ListView 中显示该文件夹的内容。
frmExplorerLike
显示了三种运行时更改树根的方法。右键单击 ListView
中的文件夹将导致该文件夹成为新的树根。它还会用该文件夹父级的名称填充 ComboBox
。选择组合框中的一个条目会将树根设置为该文件夹。换句话说,它提供了一种返回原始树根的方法。
单击“C:\ Test”按钮会将树根设置为 C:\。我没有提供在此情况下返回到原始树根的方法。考虑到 ExpTree
的 RootItem
属性和 CShItem
的 GetCShItem(Path as String)
方法,在演示程序中完成此更改的代码非常简单。
Private Sub cmdCTest_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles cmdCTest.Click
Dim cDir As New CShItem = GetCShItem("C:\")
If cDir.IsFolder Then
ExpTree1.RootItem = cDir
End If
End Sub
在 frmExplorerLike
中单击“刷新”按钮将调用 RefreshTree
。您可以创建一个测试目录树,运行演示程序,导航到测试树的底部,通过 Windows Explorer 删除部分或全部测试目录树,然后单击“刷新”按钮来测试此功能。
两个演示窗体现在都在一个单独的线程中收集图标索引以显示在 ListView
中,这提高了初始启动速度和整体响应能力。这在本文提供的代码中并未显示。实际代码请参见演示。
来自本文读者的反馈在改进此控件方面非常有帮助。查看“历史”部分和论坛会发现,多个功能添加和 bug 修复直接源于这些反馈。感谢大家。
致谢
我最初的文章包含一个用于访问系统图像列表的类。该类的某些重要片段保留在 SystemImageListManager
中。最初的版本只是将 Steve McMahon 的一些系统图像列表类从 C# 翻译成 VB.NET,您可以在 这里 找到。Steve 的类具有大量额外的功能,用于绘制图标并将它们附加到其他类型的控件。
Calum McLellan 进行了重要贡献,改进了此控件。Calum 的文章 VB.NET 中的 Explorer ComboBox 和 ListView 使用 ComboBox
和 ListView
类扩展了此库。
历史
- 2012/04/20 -- 版本 2.12.1 更新下载,以包含 Windows Explorer 风格的排序到显示的文件/文件夹名称。此更改添加了一个新类(
StringLogicalComparer
)以及 CShItem 中的一行更改以使用该类。结果是:xx11xx 在 xx101xx 之前排序。 - 2012/04/20 -- 版本 2.12。更新下载和文章,以包含:
- ASUS 修复,这可能也适用于 Carbonite 和其他几个作为 Shell 扩展实现的云备份。
- 正确处理 Vista/Win7 上的 Zip 和其他压缩文件(确保它们被视为文件,而不是文件夹)。
- 正确处理
ExpTree
中的AllowDrop
属性。现在可以在 IDE 中对其进行有用的设置。 - 更改了
CShItem.Attributes
的定义和源,因此该属性现在从FileInfo
或DirectoryInfo
设置。定义现在是System.IO.FileAttributes
(它应该一直都是这样)。 - 将
CShItem.HasSubfolders
设为按需填充属性,避免在不需要时检索的成本。在某些情况下是巨大的胜利。 - 修改了
HasSubFolders
属性的处理,以补偿 XP 和 Vista/Win7 之间的差异。在 Vista/Win7 客户端系统上,这显着提高了响应速度。 - 对消除一些无害的编译器警告进行了少量更改。
- 移除了死代码和调试代码,并更正了一些注释。
- 2006/03/12 -- 版本 2.11。更新以支持 VS2005。删除了
CShItem.GetContents
中的Application.DoEvents
调用,如论坛中所讨论。 - 2005/09/16 -- 版本 2.1。更新源代码和演示,使其与本文第 2 部分中的文件相同。对本文代码进行少量修复,对第 2 部分涵盖的代码进行较大修复。
- 2005/08/23 -- 版本 2 发布 - 更新文章、源代码和演示。
- 将目录刷新策略更改为在
GetDirectories
中更新缓存的目录,除非通过Optional
参数明确阻止。 - 添加了
CShItem.GetCShItem
来替换Sub New(ID as CSLID)
和Sub New(Path As String)
的功能。 - 在
BeforeExpand
和AfterSelect
事件中添加了节点的刷新内容。 - 添加了拖放功能——本文未讨论。
- 向
ExpTree
添加了ShowHiddenFolders
和ShowRootLines
属性。 - 在
ShellDll
中,将POINT
的声明从Private
更改为Public
,这可能会破坏现有代码。根据需要完全指定System.Drawing.Point
或ShellDll.Point
。 - 添加到
SystemImageListManager
中以获取选定IconIndex
以及正常和打开的 ImageIndices 的能力。 - 添加了从
GetIcon
获取真正的小图标的功能 -- 来自 Calum McLellan。 - 向
CShItem
添加了许多其他属性和方法。 - 应广大读者要求,移除了
CShItem
在DEBUG
模式下编译时对某些条件的错误Throw
。 - 多次小改进、一些 bug 修复以及一些代码重组。
- 将目录刷新策略更改为在
- 2005/04/02 -- 更新源代码和演示。
- 修改了
ExpTree
控件,使其在首次拖放到 IDE 中的窗体上时绘制树,并从 IDE 中隐藏无法在那里更改的属性。 - 更改了
CShItem
的排序顺序,使得“我的文档”出现在树中的“我的电脑”之前。 - 向
CShItem
添加了Public Shared
字段strMyDocuments
,其中包含字符串“My Documents”的区域设置表示。 - 添加了
Public ReadOnly Property
IsHidden
到CShItem
。(感谢 Calum。) - 修改了
SystemImageListManager
以获取并使用实际的小图标作为小图像列表,而不是重复使用大图标(感谢 Calum)。 - 修复了演示程序,使其使用
SystemImageListManager
为 ListView 设置图标索引。这在演示中添加线程后就失效了。
- 修改了
- 2005/03/02 -- 更新源代码、演示和文章。
- 重写了
ExpandANode
以消除先前版本的限制。 - 添加了
RefreshTree
方法,允许应用程序强制重建树以显示目录结构的变化。 - 添加了
SelectedItem
属性,该属性返回当前SelectedNode
的CShItem
。 - 将
TreeView
的HideSelection
属性初始化为False
。 - 修改了
Sub New(path as String)
以接受任何CShItem.Path
,包括 GUID。 - 修复了 XP 相关问题,该问题抑制了 ZIP 文件的显示。
- 移除了(仅在 Release 编译中)对意外错误的异常抛出。
- 在演示程序中添加了线程以提高响应能力。
- 重写了
- 2005/01/11 -- 更新以纠正阻止在工作线程中创建
CShItem
的错误。 - 2005/01/09 -- 更新以纠正错误并整合一些附加功能。
- 修复了
ItemIDListSize
和PidlCount
例程在CShItem
中,它们在某些极少数情况下会失败。 - 修改了
CShItem
中的Sub New(path as String)
以接受文件路径和目录路径。 - 添加了必要的特殊处理,以便“我的文档”可以用作基本目录。
Sub New(StartDir as CSIDL)
中的代码更改以实现此目的。还取消了“我的文档”的StartDir
条目注释,以便可以在设计器中使用它。 - 改进了
GetItems()
属性,以避免对目录内容进行额外扫描。 - 向
ExpTree
添加了ExpandANode
方法。这是本文论坛中讨论的此方法的受限版本。 - 修改了演示,以便在
TreeView
中选择空目录时清除ListView
。演示还包含一些注释掉的代码,可用于测试一些附加功能。有关如何执行此操作的说明,请参阅演示源代码。
- 修复了
- 2004/11/29 -- 向
CShItem
、ExpTree
和演示程序添加了功能。对ShellDll
进行了少量添加。使CShItem
和演示程序更具区域设置中立性。修改了文章以反映更改。- 添加了一个
CShitem
的变体构造函数,允许基于有效目录路径(例如,“C:\”)创建CShItem
。 - 向
ExpTree
添加了一个仅运行时属性,以允许动态更改ExpTree
的根目录。 - 修改了演示以说明新属性。
- 删除了或修改了基于
CShItem
的TypeName
和DisplayName
字符串的测试,这些测试在非英语区域设置下会失败。还修改了演示中的一个测试,该测试在相同情况下也会失败。修改了桌面CShItem
的创建代码,将其路径设置为其 GUID,并从SHGetFileInfo
获取其DisplayName
,而不是随意将其设置为“Desktop”。 - 修改了
CShItem
,以便仅在实际请求提供它的属性值时才调用SHGetFileInfo
。这与先前更新中将IconIndex
的获取推迟到实际请求时进行的更改类似。
- 添加了一个
- 2004/11/05 -- 更新
CShItem
源代码。这修复了以下问题:GetContents
方法中的内存泄漏。- 更改了
IconIndex
的获取方式,使其在被调用之前不被获取。这对于需要所有文件图标的应用程序来说区别不大,但对于不使用或不需要大多数文件图标的应用程序来说,速度会显著提高。 - 获取打开文件夹的正确图标。它此前为这个目的找到了并使用了我的文档的图标,而不是正确的图标。
- 源文件下载包含与文章匹配的正确代码。在上次更新之前,此代码未正确发布。
- 2004/10/22 -- 发现
SystemImageListManager
作为具有潜在多个实例的类的不当设计,在某些情况下(尤其是在 IDE 中)会导致问题。将其重塑为一个仅具有Shared
属性和方法的类,正如它一开始应该设计的那样。通过此更改,以及在实际写入系统图像列表的代码周围添加互斥锁,该类应该是线程安全的,尽管尚未经过详尽测试。 - 2004/10/20 -- 第二个版本。在 XP 系统上正确显示带 alpha 通道的图标。一个简化的、已更正的用于管理图标的类。修订文章以反映代码更改。
- 2004/10/11 -- 文章和 ExpTreeLib 的初始版本(Ver. 1.0.1743.41270)。