65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (132投票s)

2004年10月13日

CPOL

30分钟阅读

viewsIcon

2369290

downloadIcon

29808

带有 Shell 文件夹访问类和图标管理的 Explorer TreeView 控件。

VB.NET Explorer Tree Control

引言

ExpTree 控件是一个类似 Windows Explorer 的 TreeView 控件。它显示所有正确的图标,并带有适当的覆盖。所有 Windows 文件夹,包括桌面、我的电脑和历史记录等虚拟文件夹,都会被正确显示并可供包含的窗体使用。该控件包含并使用了一个优化的图像列表管理类,该类为应用程序提供了小图标和大图标的图像列表。该控件只是一个强大的类库 (ExpTreeLib) 的可视化部分,该库提供了超出 DirectoryInfoFileInfo 类组合的功能。如上图所示,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# 编写的,并且没有一个完全满足其他要求。

基于 DirectoryInfoFileInfo 类的控件无法处理虚拟文件夹。通过添加对 Shell32.dll 的引用的控件需要额外的 .dll 来包装 COM 接口,并且无法报告隐藏文件和目录。使用任一方法的应用程序都需要附加类来处理图标,因为两者都不会获取图标信息。由于我曾经用 C 编写过 Shell 访问 .dll,所以我熟悉这些技术,因此我决定使用 IShell 文件夹接口,并使用 SHGetFileInfo 来提供图标信息来解决这个问题。

类概述

该控件 ExpTree 与几个支持类一起打包到一个库程序集和 .dll (ExpTreeLib) 中。ExpTreeLib 包含以下类:

ShellDll API 声明、接口、结构、枚举和常量。
CShItem 此库的主要类。
SystemImageListManager 用于管理大、小系统图像列表的类。
ExpTree 实际控件。

可以使用 SystemImageListManager 类,但 ExpTree 会初始化并使用它。如果应用程序需要显示文件系统图标,该类将提供这些图标。有关详细信息,请参阅下面的 SystemImageList Manager 类 部分。

CShItem 类的详细信息将在 下面 讨论。它封装了描述一个文件夹或文件的信息集合。在使用上,它类似于 DirectoryInfoFileInfo 实例。但是,它是使用 Shell 的 IShellFolder 接口构建的,因此它可以表示系统上可用的所有文件夹和文件类型。

该库还包含其他仅用于拖放支持的类,此处不作讨论。

使用控件

要使用该控件,请将 .dll 添加到项目中,然后将控件添加到工具箱。要将控件添加到工具箱,请右键单击工具箱,选择“自定义工具箱”,然后浏览到 DLL。完成此操作后,您就可以像使用其他控件一样使用它了。除了标准的 UserControl 属性外,ExpTree 控件还公开了几个属性:

AllowDrop 设计时和运行时 允许/禁止在树上拖放
ShowHiddenFolders 设计时和运行时 显示/隐藏隐藏文件夹
ShowRootLines 设计时和运行时 允许/禁止折叠 TreeRoot
StartupDirectory 设计时和运行时 选择树的根目录
RootItem 仅运行时 将根设置为特定的 CShItem
SelectedItem 仅运行时 获取当前选定的 CShItem

StartupDirectory 设置 TreeView 的根。它只接受 SystemFolder 作为启动目录。最有用的包括 DesktopMy Computer。在设计时更改 StartUpDirectory,以便在 IDE 中查看初始显示。

RootItem 是一个仅运行时属性,用于将树根重置为另一个文件夹,该文件夹可以是 TreeView 中可用的任何文件夹。

提示

要设置一个 ExpTree,使其看起来像从某个非系统文件夹启动根目录

  1. 在 IDE 中,将 StartupDirectory 设置为 Desktop。
  2. 在窗体的 Load 事件中,将 RootItem 设置为所需的文件夹,例如:
  3. ExpTree1.RootItem = CShItem.GetCShItem("C:\MyAppData")

ExpTree 方法

方法 类型 备注
RefreshTree N/A 通过 SelectedNode 重新构建树
ExpandANode 布尔值 通过输入的 PathCShItem 展开树

RefreshTreeExpandANode 方法对于基本使用不是必需的,稍后将在 后面 讨论。

ExpTree 事件

StartUpDirectoryChanged 用于设计时交互
ExpTreeNodeSelected TreeNode 被选中时引发

ExpTreeNodeSelectedEventArgs 是一个字符串,包含底层文件夹的完整路径和代表 SelectedNodeCShItem

假设您有一个窗体,其中包含一个名为 ExpTreeExpTree、一个名为 lv1ListView 和一个名为 sbr1StatusBar。要使用该控件,您必须导入一些项:

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 语句将 ListViewLargeImageListSmallImageList 设置为相应的系统图像列表。

添加以下事件处理程序:

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 那样对文件夹和文件进行排序,需要一些特殊的处理。这种特殊的处理是在 CShItemIComparable.CompareTo 例程中完成的,该例程在排序目录和文件 ArrayList 时被调用。

通过调用 GetIconIndex 来处理图标的获取和添加到两个图像列表。GetIconIndex 的第二个参数设置为 False,表示不获取“打开”IconIndex

注意:下载的演示项目在一个单独的线程中获取和设置图标索引。上面显示的仅是简化的单线程方法。

SystemImageListManager 类

此类基于系统图像列表。它访问两个系统图像列表,一个包含小图标,一个包含大图标。这两个列表是同步的,因此相同的 IconIndex 在两个列表中都指向同一个图标。当查询 CShItemIconIndex 时,它会确定图标是否需要覆盖,如果需要,则将带有覆盖的图标作为附加图标添加到系统图像列表中。由于 SHGetFileInfo 返回的 IconIndex 不一定是文件夹或文件的实际 IconIndex(可能需要使用带覆盖的图标),因此我使用 HashTable 来存储实际的 IconIndexHashTable 的键基于报告的 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 项目所需图标在两个系统图像列表中的 IconIndexOptional 参数 GetOpenIcon 指示 GetIconIndex 返回 CShItem 的“打开”图标,而不是“正常”图标。Optional 参数 GetSelectedIcon 请求“选定”图标。

在内部,SystemImageListManager 维护一个 HashTable,其 Key 基于系统图像列表的 IconIndex、引用的 CShItem 的链接和共享状态以及图标的“打开”或“选定”状态。存储在 HashTable 中的 Value 是图标在系统图像列表中的 IconIndex

如果 IconIndex 已知(在 HashTable 中),则函数仅返回 HashTableValue 作为函数返回值。

如果所需的图标未知(不在 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)

此方法将相应的系统图像列表附加到 ListViewImageListforLargeIcons 参数选择哪个列表附加到哪个(True 表示大,False 表示小)。我没有对 forStateImages 参数做任何处理,只是将其传递为 False

SetListTreeViewImageList

Public Shared Sub SetTreeViewImageList( _
    ByVal treeView As TreeView, _
    ByVal forStateImages As Boolean)

此方法将相应的系统图像列表附加到 TreeViewImageList。我从未测试或使用过 forStateImages 参数,只是将其设置为 False

这两种方法都使用 SendMessage API 向控件发送消息,将系统图像列表附加为控件的 ImageList。有关详细信息,请参阅源代码。请注意,.NET 不知道此附件。如果控件被隐藏然后显示,则必须在 VisibleChanged 事件处理程序中重新建立附件。

CShItem 类

版本 2 更改

CShItem 类是 ExpTree 库的主要类。库的任何部分的添加通常都需要对其进行更改。版本 2 与先前版本之间的主要更改实际上发生在 CShItem 中。每个代表文件夹的 CShItem 都维护一个 CShItemArrayList,代表其包含的子文件夹。在版本 2 之前的版本中,该 ArrayList 从未更新过。如果以参数 Optional Refresh As Boolean = False 调用 GetDirectories 方法并设置为 True,则整个 ArrayList 将被丢弃并重新创建。在版本 2 中,参数 Optional doRefresh As Boolean = True 指示 GetDirectories 调用一个新函数 RefreshDirectoriesRefreshDirectories 检查更改并为新添加的目录创建新的 CShItem,并删除代表不再存在的目录的 CShItem。此过程由其他新代码实现,以低成本方式完成,因此可以频繁进行此目录刷新。在 ExpTree 中,它在每次 TreeNode 展开、每次选择以及与拖放相关的其他几个情况下完成。

请注意,代表文件的 CShItem 不会被保留,而是在每次 GetFilesGetItems 请求时重新生成。一个潜在的研究方向是考察将文件 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 类中声明的表示系统特殊文件夹的 EnumExpTree 为其用途定义了一个子集。与等效的 Sub New 不同,除了在特定操作系统上可用性之外,对可以使用哪个 CSIDL 没有限制。用法由此代码片段说明,该片段获取 My ComputerCShItem

    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:TypeNameSHGetFileInfo 报告的类型名称。

注释 3:FullName 通常与 DisplayName 相同。但是,对于 .lnk 文件,DisplayName 不包含 .lnk 扩展名。而 FullName 包含。给定一个路径为“C:\Temp\ABC.txt.lnk”的链接文件,Displayname 将返回“ABC.txt”,FullName 将返回“ABC.txt.lnk”。

IconIndex... 属性报告系统图像列表的基本 IconIndex。这对于应用程序来说不是直接有用的,除非只想要非覆盖图标。

在几乎所有情况下,都不应使用 PIDL。它之所以可见,仅仅是因为 SystemImageListManager 需要引用它。如果应用程序需要调用某些 Shell .dllPIDL 可能有用。clsPidl 属性是 cPidl 类的实例。它公开了将 PIDL 作为 Byte() 检查的方法。有关更多信息,请参阅源代码。

...Time 属性和 Length 属性与 FileInfo 类返回的值完全相同。我在请求其中任何一个值时会“作弊”并创建一个 FileInfo 实例来检索这些值。

str... 属性提供了在运行应用程序的计算机上表示“我的电脑”和“系统文件夹”的字符串。这提供了一种不区分区域设置的方法来测试这些特殊名称。特殊名称“Desktop”由 GetDeskTop 返回的 CShItemDisplayName 提供,并且也是区域设置无关的。

方法

方法 返回类型 返回值
共享方法    
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,而不是副本。

GetDirectoriesGetFilesGetItems 按请求返回 CShItem 对象的 ArrayList。如果文件夹中没有请求的类型,它们将返回一个空的 ArrayList。如果 CShItem 代表一个文件,则返回一个空的 ArrayList。在常见错误情况下(例如,驱动器未就绪(例如,空的 CD 驱动器)),也返回一个空的 ArrayList。与 Windows Explorer 不同,类在这些情况下不会显示“中止-重试”消息框。版本 2 之前的先前版本会在内部调用此方法时抛出异常(仅在 Debug 模式下编译时)。版本 2 及更高版本将不再抛出异常,而是在任何错误情况下返回一个空的 ArrayList

RefreshDirectories 确保 GetDirectories 返回的 ArrayList 反映文件系统的当前状态。如果发生任何更改,它将返回 TrueRefreshDirectoriesGetDirectories 调用(除非通过 Optional 参数明确禁止),因此很少需要直接调用它。

ExpTree 控件

最后,我们来看控件本身。有了 CShItemSystemImageListManager 类,控件就相对简单了。

ExpTree 属性和事件

属性  
AllowDrop 允许(True)或阻止(False)拖放到树上。
StartUpDirectory 必须是特殊文件夹的 CSIDL
RootItem 将树的根设置为 CShItem
SelectedItem 返回当前 SelectedNodeCShItem
ShowHidden 允许/禁止在 TreeView 中显示隐藏目录。
ShowRootLines 允许/禁止在 TreeView 中显示线条和展开/压缩框。
事件  
ExpTreeNodeSelected 在选中 TreeNode 时引发。
StartUpDirectoryChanged 在设置 StartUpDirectory 属性时引发。
方法
ExpandANode 通过代表输入 CShItem 的节点展开树。展开失败时返回 False
RefreshTree 重新初始化树并通过先前选定的节点展开它。

StartUpDirectory 是一个 CSIDL,代表一个系统特殊文件夹。控件可以处理的文件夹列表可通过 ExpTree 的属性表在 IDE 中获得。

RootItem 是一个仅运行时属性。通过运行时调用设置此项会导致将整个树重置为以输入的 CShItem 为根。CShItem 必须是某种文件夹(文件文件夹或系统文件夹)的有效 CShItem。尝试使用非文件夹 CShItem 设置它将被忽略。此属性的使用在演示程序中的 ListViewMouseUp 事件中以及“C:\ Test”按钮后面的代码中得到了说明。

ExpTreeNodeSelected 是在 TreeViewAfterSelect 事件发生时引发的事件。这会将事件通知给包含的 FormEvent 签名是

Public Event ExpTreeNodeSelected(ByVal SelPath As String, _
             ByVal Item As CShItem)

其中 Item 是代表选定节点的 CShItemSelPath 是该 CShItem 的路径。对于虚拟文件夹,路径是 GUID,SelPath 包含 CShItemDisplayName

StartUpDirectoryChanged 是在设置初始目录时引发的事件。为了让包含的 Form 需要此事件的通知,它被设置为 Public。通常,这不需要,因为根目录的更改总是会选择新根,从而引发 ExpTreeNodeSelected 事件。

ExpandANode 是对本文论坛中提供的原始 ExpandANode 的修订。内部而言,此方法与原始版本有很大不同,并且不限于文件系统目录。任何 CShItem 都可以用作输入路径。与原始版本不同,此版本不会强制将树根固定在 DesktopMy 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 的初始化中,我们设置了 TreeViewImageList 并添加了控件对 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... 处的代码将输入的 ValueEnum 的允许值进行比较,如果无效则 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,使用正确的图标,并将其添加到根节点。请注意,每个 TreeNodeTag 都设置为它所属的 CShItem。如果子文件夹本身可能包含子文件夹,我们会创建一个虚拟节点并将其添加到子节点,这样 Treeview 就会显示一个“+”并允许展开。

BuildTree 中检查 .IsHidden 的代码,如果 ShowHiddenFolders 属性为 False,则会阻止在 TreeView 中显示隐藏目录。MakeNode 中的 If ... ElseIf 序列避免检查软盘驱动器,以防止烦人的软盘访问。它还解决了包含所有隐藏成员的目录被 .HasSubFolders 报告为 False 的事实。最后,我们将根节点附加到 TreeViewExpand 根节点以获得最终显示。

TreeviewBeforeExpand 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,它将 SelectedNodeCShItem 传递给包含的 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

SelectedNodeRefreshNode 更新。ShowRootLines 的测试是为了解决当测试条件为 True 时出现的显示问题。如果 RefreshTree 的事件发布没有被抑制,则引发 ExpTreeNodeSelected 事件,传递 CShItem 和节点的路径。请注意,某些系统文件夹的路径是一个 GUID。在这种情况下,我们返回 SelectedNodeDisplayName 而不是路径。真实的路径仍然可以在 CShItem 中找到。

演示程序和其他思考

演示窗体不做任何有用的工作,只是说明此处提供的控件和类的用法。我真的没有试图复制 Windows Explorer。演示包中有两个窗体。frmExplorerLike(如下所示并在此处描述)仅用于说明如何使用 ExpTreeLib 中可用的一些方法。frmDragDrop 更现实一些,并演示了从 ExpTree 拖放和拖放到 ExpTree,以及从 ListView 拖放。

ListView 中左键单击一个文件夹,会导致在树中展开相应的文件夹,并在 ListView 中显示该文件夹的内容。

frmExplorerLike 显示了三种运行时更改树根的方法。右键单击 ListView 中的文件夹将导致该文件夹成为新的树根。它还会用该文件夹父级的名称填充 ComboBox。选择组合框中的一个条目会将树根设置为该文件夹。换句话说,它提供了一种返回原始树根的方法。

单击“C:\ Test”按钮会将树根设置为 C:\。我没有提供在此情况下返回到原始树根的方法。考虑到 ExpTreeRootItem 属性和 CShItemGetCShItem(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 使用 ComboBoxListView 类扩展了此库。

历史

  • 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 的定义和源,因此该属性现在从 FileInfoDirectoryInfo 设置。定义现在是 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) 的功能。
    • BeforeExpandAfterSelect 事件中添加了节点的刷新内容。
    • 添加了拖放功能——本文未讨论。
    • ExpTree 添加了 ShowHiddenFoldersShowRootLines 属性。
    • ShellDll 中,将 POINT 的声明从 Private 更改为 Public,这可能会破坏现有代码。根据需要完全指定 System.Drawing.PointShellDll.Point
    • 添加到 SystemImageListManager 中以获取选定 IconIndex 以及正常和打开的 ImageIndices 的能力。
    • 添加了从 GetIcon 获取真正的小图标的功能 -- 来自 Calum McLellan。
    • CShItem 添加了许多其他属性和方法。
    • 应广大读者要求,移除了 CShItemDEBUG 模式下编译时对某些条件的错误 Throw
    • 多次小改进、一些 bug 修复以及一些代码重组。
  • 2005/04/02 -- 更新源代码和演示。
    • 修改了 ExpTree 控件,使其在首次拖放到 IDE 中的窗体上时绘制树,并从 IDE 中隐藏无法在那里更改的属性。
    • 更改了 CShItem 的排序顺序,使得“我的文档”出现在树中的“我的电脑”之前。
    • CShItem 添加了 Public Shared 字段 strMyDocuments,其中包含字符串“My Documents”的区域设置表示。
    • 添加了 Public ReadOnly Property IsHiddenCShItem。(感谢 Calum。)
    • 修改了 SystemImageListManager 以获取并使用实际的小图标作为小图像列表,而不是重复使用大图标(感谢 Calum)。
    • 修复了演示程序,使其使用 SystemImageListManager 为 ListView 设置图标索引。这在演示中添加线程后就失效了。
  • 2005/03/02 -- 更新源代码、演示和文章。
    • 重写了 ExpandANode 以消除先前版本的限制。
    • 添加了 RefreshTree 方法,允许应用程序强制重建树以显示目录结构的变化。
    • 添加了 SelectedItem 属性,该属性返回当前 SelectedNodeCShItem
    • TreeViewHideSelection 属性初始化为 False
    • 修改了 Sub New(path as String) 以接受任何 CShItem.Path,包括 GUID。
    • 修复了 XP 相关问题,该问题抑制了 ZIP 文件的显示。
    • 移除了(仅在 Release 编译中)对意外错误的异常抛出。
    • 在演示程序中添加了线程以提高响应能力。
  • 2005/01/11 -- 更新以纠正阻止在工作线程中创建 CShItem 的错误。
  • 2005/01/09 -- 更新以纠正错误并整合一些附加功能。
    • 修复了 ItemIDListSizePidlCount 例程在 CShItem 中,它们在某些极少数情况下会失败。
    • 修改了 CShItem 中的 Sub New(path as String) 以接受文件路径和目录路径。
    • 添加了必要的特殊处理,以便“我的文档”可以用作基本目录。Sub New(StartDir as CSIDL) 中的代码更改以实现此目的。还取消了“我的文档”的 StartDir 条目注释,以便可以在设计器中使用它。
    • 改进了 GetItems() 属性,以避免对目录内容进行额外扫描。
    • ExpTree 添加了 ExpandANode 方法。这是本文论坛中讨论的此方法的受限版本。
    • 修改了演示,以便在 TreeView 中选择空目录时清除 ListView。演示还包含一些注释掉的代码,可用于测试一些附加功能。有关如何执行此操作的说明,请参阅演示源代码。
  • 2004/11/29 -- 向 CShItemExpTree 和演示程序添加了功能。对 ShellDll 进行了少量添加。使 CShItem 和演示程序更具区域设置中立性。修改了文章以反映更改。
    • 添加了一个 CShitem 的变体构造函数,允许基于有效目录路径(例如,“C:\”)创建 CShItem
    • ExpTree 添加了一个仅运行时属性,以允许动态更改 ExpTree 的根目录。
    • 修改了演示以说明新属性。
    • 删除了或修改了基于 CShItemTypeNameDisplayName 字符串的测试,这些测试在非英语区域设置下会失败。还修改了演示中的一个测试,该测试在相同情况下也会失败。修改了桌面 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)。
© . All rights reserved.