VB.NET 中的 Explorer ComboBox 和 ListView






4.88/5 (49投票s)
一个 ComboBox(和 ListView)显示文件系统,使用系统图像列表。
引言
本文是 Jim Parsells 的文章 “一个全 VB.NET 的 Explorer Tree 控件,带 ImageList 管理” 的扩展。我在 Jim 的 ExpTreeLib
程序集中添加了两个主要类 - ExpCombo
和 ExpList
。ExpList
直接继承自 ListView
,部分基于 Jim 演示项目中的 ListView,并添加了一些功能,如列标题中的图标以及隐藏文件或文件夹的半透明图标。ExpCombo
使用 Jim 的 SystemImageListManager
和 CShItem
类来创建一个类似于 Open
/SaveFileDialog
中可用的 Explorer ComboBox。
我没有在文章中包含很多代码示例,因为我真的不知道从哪里开始。我已经尽力保持代码注释良好,但如果您对特定部分有任何疑问,请随时提出,我将尽快回答。
背景
这些控件的原始需求是在我开始为一款处理 SharePoint 2003 文件的程序编写插件后产生的。令我非常惊讶的是,没有一个默认的对话框(打开、保存、浏览等)适用于 SharePoint 文件。在看到 Jim 的文章并意识到这些控件的很大一部分已经为我写好了之后,我开始开发这些控件。没有 Jim 的类,我永远无法完成这项工作。
更新信息
在此次更新中,我对代码进行了大量更改。其中许多更改将破坏现有代码(我已更改或完全删除了许多方法和属性)。一个主要变化是,附加代码包含一些 .NET 2.0 依赖项,因此没有 VS2003 项目可用。这并非我的本意,但随着我深入研究,维护两个版本变得太困难了。因此,该项目已获得新的版本号。
我还添加了一些我用于 SharePoint 应用程序的复制的 Windows 对话框。我没有在文章中介绍它们,因为它们只是为了避免两个不同版本 - 一个给我自己,一个给 The Code Project(我之前就是这样做的,所以是 3.1 版本)。
鉴于自上一版本以来控件发生了如此大的变化,我可能忘记提及一些内容。如果您发现文章中有遗漏或对所做的更改有任何疑问,请在论坛中发布 - 如果没有人提出异议,那么就不会进行任何更改。
演示项目文件夹中的 ExplorerControls.dll 文件已用强名称签名。使用的密钥已故意从发布的代码中省略,这意味着如果您想重新编译项目,您必须在 AssemblyInfo.vb 文件中删除或更改 AssemblyKeyFile
条目。此文件位于 My Project 文件夹中(在解决方案资源管理器中选择项目,然后单击“显示所有文件”按钮可在 Visual Studio 中看到此文件)。
Bug
仍然存在几个我无法修复的错误。最糟糕的一个是 SharePoint 文件夹的刷新 - 它就是不起作用。我进行了大量测试,并发现 IShellFolder.EnumObjects
API 方法在我重新启动应用程序之前会返回旧状态,我找不到任何方法来使返回的对象集合无效。
另一个错误是,在我使用 Thumbnail
视图对基类属性进行遮蔽以添加 ExpList.View
属性后,该属性在设计器中不起作用。虽然这在 .NET 1.1 中工作正常,但我不得不创建 ViewStyle
属性,并带有 DisplayName("View")
和 EditorBrowsable(EditorBrowsableState.Never)
属性,并为 View
属性添加 Browsable(False)
属性。这些属性是同步的,这意味着在设计时,您操作 ViewStyle
属性(在属性窗口中显示为“View”),在运行时,您操作 View
属性。
我遇到的最后一个主要问题是列宽被重置为默认值。为了解决这个问题,我只是暂时存储了它们被重置之前的值,然后再恢复。我已将此问题用 TODO
关键字注释掉,以防有人感兴趣。
COM 互操作
在发布的代码中,没有考虑 COM 客户端。我曾考虑为所有类实现接口,但有几件事阻止了我这样做。首先,一些背景知识:COM 接口不支持重载方法,也不支持参数化构造函数。这意味着 ExpListItemCollection.Add
方法对于 COM 客户端来说将显示为 Add
、Add2
和 Add3
。这也意味着 COM 客户端无法实例化 CShItem
类 - 这在很大程度上阻碍了使用这些控件。
为了解决这个问题,我们需要一个具有默认构造函数和用于构造新对象并将其作为返回值的方法的辅助类。我们还需要在所有类中实现接口,并使用对 COM 友好的方法名称。最后一步是在所有类上使用 ClassInterface(ClassInterfaceType.None)
属性,以便只有显式实现的接口才能被 COM 可见。
所有这一切的问题在于,在 VB.NET 中,实现接口时,所有成员都必须显式实现(据我所知)。这使得将基类的继承方法包含在接口中变得困难,需要继承类覆盖或遮蔽基类方法,以便启用接口成员的显式实现(?!)。总而言之:这一切都太费力了,除非有几个人要求,否则我不会这么做 :)
不过,总会有后期绑定...
类概述
ExpTreeLib
CShItem
库的核心类。用于文件系统信息。
ShellDll
包含所需的 Shell32 API 接口、函数等的类。
SystemImageListManager
获取
SystemImageList
的句柄并将其附加到控件所需的类。ExpTree
代表文件系统的 TreeView。
(有关这些和其他类的更多信息,请参阅 Jim 的文章,位于 ExpTreeLib 文件夹中。)
ExpList
ExpList
代表文件系统的 ListView。
ExpListItem
ExpList
的项类。ExpListColumn
ExpList
的列类。ExpListItemCollection
、SelectedExpListItemCollection
、ExpListColumnCollection
ExpList
中项和列的集合类。ExpListComparer
ExpList
的默认比较器类,它以适当的方式对标准的ExpListColumnTypes
进行排序。
ExpCombo
ExpCombo
代表文件系统的 ComboBox。
ExpComboItem
ExpCombo
的项类。ExpComboItemCollection
ExpCombo
中项的集合类。
用户控件和对话框
浏览器
一个结合了
ExpList
和ExpCombo
控件的用户控件,具有内置导航等附加功能。BrowseForFolder
、OpenFileDialog
、SaveFileDialog
.NET Framework 提供的相同控件的复制(但支持 SharePoint 文件)。
FileSelector
一个用户控件,结合了浏览器和第二个
ExpList
用于文件选择(批量处理等)。
杂类
Thumbnail
一个用于从
CShItem
中提取 Windows 缩略图的类。音效
用于在导航控件时发出“点击”声音的类(将
Silent
属性设置为True
以关闭声音)。FlatButton
创建此类是因为
ToolBarButton
类在浏览器控件中的行为不完全符合我的预期。
ExpList
这本质上是一个类似 Explorer 中的 ListView。它具有允许它附加到 ExpCombo
或 ExpTree
的属性,以便自动添加附加控件中当前选中文件夹的文件和文件夹,并支持通过文件系统进行双击导航。默认情况下,当用户双击文件和文件夹时,它们将被打开。此操作可以通过设置 File/FolderOpenOnActivate
属性来打开/关闭。如果关闭,可以使用 File/FolderActivate
事件在调用类中执行所需操作。
在以前的版本中,我使用 User32 的 SendMessage
调用来启用分组,但现在由于 .NET 2.0 ListView
中增加了分组功能,我已经将其移除。我仍然使用 SendMessage
为列标题添加图标,并显示选中的列(这会使排序列的背景变为灰色)。
我使用了 Globalization 来处理错误消息和上下文菜单(源文件中包含默认语言 - 英语 - 和中性德语的资源文件)。还有一个语言属性,允许固定语言。这只是将控件的 System.Threading.Thread.CurrentThread.CurrentUICulture
属性设置为德语、英语或操作系统的区域性。如果我想让控件的语言在不同操作系统上保持不变,我就会使用它。
Filter
属性可用于限制列表中显示的文件类型。只有最后一个 '.' 之后的字符才会被考虑,这意味着 'test.jpg'、'*.jpg'、'.jpg' 和 'jpg' 都具有相同的效果。我在这里使用了一个 Public Boolean Function
,以防需要检查项是否符合过滤器,而无需将该项添加到列表中。
也可以使用拖放功能从 Explorer、ExpTree
或另一个 ExpList
添加文件。如果列表附加到 ExpTree
或 ExpCombo
,则此功能将不起作用,因为它仅用于批量处理等的文件选择。
在第二次更新中,我添加了 ExpListItem
类,我对该类进行了另一次破坏性更改(抱歉!),因为它现在有一个 CShItem
属性,而不再是存储相关 CShItem
的 ShItem
属性。该类还有一个 State
属性来显示 Cut
或 Normal
的图标 - 我用它来正确显示 ExpList
中的隐藏项。在此版本中,我还添加了 ColumnHeaderContextMenu
。为此,我利用了鼠标指针在 ColumnHeader
上时不触发 MouseDown
事件的事实。我只是覆盖了 ContextMenu
属性,以确保它始终设置为 ColumnHeaderContextMenu
,并在 MouseDown
事件中更改了它,确保仅在未触发此事件时才使用 ColumnHeaderContextMenu
。
在最新版本中,我对 ExpList
的结构进行了更改,以更准确地反映 .NET ListView
控件的行为。我为项集合添加了新类,从而可以使用 ExpList.Items.Add
等,而不是像上一个版本那样使用 ExpList.AddItem
。我这样做是为了减少因未使用正确方法而导致的错误几率,认为我越接近 ListView
,控件就越容易使用。我还添加了 ExpListColumnCollection
,并启用了 ExpListColumn
的设计时支持。要在设计器中添加 ExpListColumn
,必须设置 ColumnType
属性(默认值为 None
)。也可以将 Visible
属性设置为 False
,然后该列将在运行时隐藏(但仍可通过 ColumnHeaderContextMenu
访问)。ColumnType
枚举包含三个“自定义”类型(CustomText
、CustomeDate
和 CustomNumeric
)。这些可用于在列表中显示自定义数据,并根据类型进行适当排序。使用这些列类型之一时,应在三个位置向列表项添加 SubItem
文本:托管窗体的 Load
事件、列的 ColumnAdded
事件以及列表的 ItemsAdded
事件(在演示应用程序中,我向浏览器控件添加了一个自定义列)。ExpListColumn
还有一个 CustomGrouping
属性;当将其设置为 True
时,调用类可以手动设置 ExpList.Group
(或 Browser.Group
)EventHandler 中的组 - 请参阅演示应用程序中的示例。
我做的最后一个重大更改是为 ExpList
添加了缩略图视图。这使用了 ExpList
的 OwnerDraw
模式来绘制文件缩略图,或者在没有缩略图可用时绘制特大图标。这需要在 SystemImageListManager
中添加 hXLargeImageList
属性,并在 Thumbnail
类中添加其唯一的静态 ExtractThumbnail
方法。此公共方法将缩略图作为 Bitmap
返回。
ExpList 摘要
ExpCombo
和ExpTree
这两个互斥的属性将列表附加到一个“驱动”控件。
ShowHiddenFiles
是否显示隐藏文件或文件夹。
StandardContextMenu
控件的标准上下文菜单。
File/FolderOpenOnActivate
设置当列表中的文件或文件夹被激活时是否执行默认操作(打开)。如果设置为
False
,则会引发相应的File/FolderActivate
事件。Filter
为要显示的文件类型设置过滤器(用法请参见上文)。
ShowPaths
在“详细信息”视图中使用,用于在显示文件大小、类型等与显示路径之间切换。
Sortable
列表是否可排序。
AllowRename
文件和文件夹是否可以重命名。
AllowNewFolders
是否可以在控件中创建新文件夹。
语言
控件使用的语言。
Filter
在检查通过的
CShItem
是否符合过滤器字符串后,返回True
或False
。或者,您可以传递一个CShItem
的ArrayList
,它将过滤并返回此列表。DecodeURL
用于具有 URL 路径的文件。删除“%20”和其他 URL 编码,并将其替换为相应的字符。
RefreshList
刷新关联的
ExpTree
或ExpCombo
,或者刷新列表项后面的CShItem
,以最合适的方式进行。
File/FolderActivate
这些事件仅在
FileOpenOnActivate
或FolderOpenOnActivate
设置为False
时引发。
ExpCombo
ExpCombo
类直接继承自 ComboBox
,因此暴露了所有常规的属性和方法。它利用 ExpComboItem
类来创建显示文件系统所需的分层结构,并存储相关文件夹的 CShItem
属性。请确保只添加 ExpComboItem
到 ComboBox,因为在 OnDrawItem
方法中进行了直接转换。
在第一个版本中,我使用了一个额外的类来绘制选中的 ExpCombo
项。这是我的一个解决方法,用于避免在下拉列表中选择的任何项旁边出现空格。Scott 在论坛帖子中指出,ComboBox
的 OnDrawItem
的 EventArgs
有一个 State
属性。使用这个属性,您可以找出任何 Item
是否处于“编辑”区域,然后就可以不绘制空格(见下文)... 谢谢 Scott,这大大简化了这个控件。
'Set the offset if the item is not in the Edit area
If (e.State And DrawItemState.ComboBoxEdit) = _
DrawItemState.ComboBoxEdit Then
offset = 1
Else
offset = (item.Offset * 16) + 1
End If
iconBounds = New Rectangle(e.Bounds.Left + offset, e.Bounds.Top + _
CInt((Me.ItemHeight - item.ShIcon.Height) / 2), _
item.ShIcon.Width, item.ShIcon.Height)
由于这一重大更改,我决定在第二次更新之前再进行几项更改。我将 ExpCombo
的初始化从 Browser
移动到了 ExpCombo
本身。我添加了两个属性,StartUpDirectory
和 RootDirectory
,它们利用了 ExpTree.StartDir
枚举。我还添加了相对容易理解的 SelectFolder
和 BuildCombo
方法。这基本上使控件更加独立,意味着您可以将 ExpCombo
和 ExpList
添加到窗体,设置几个属性,然后就可以使用了(Robert's your father's brother...)。
重载方法 SelectFolder
允许选择 ExpCombo
中的一个项,可以传递 CShItem
或 ExpTree.StartDir
枚举中的特殊文件夹。也可以传递文件夹的路径。这将返回一个布尔值,表示操作的成功。表示该路径的文件夹不必在调用前存在于 ExpCombo
中。此函数基本上是 ExpTree.ExpandANode
函数的一个改编版本。
在最新的更新中,我对 ExpCombo
所做的操作与对 ExpList
所做的操作类似,添加了 ExpComboItemCollection
以实现 ExpComboItem
的强类型化,并使控件的方法与 .NET ComboBox
更一致。
ExpCombo 摘要
SelectedFolder
当前选中的
ExpComboItem
。ShowHiddenFolders
是否显示隐藏文件夹。
RootDirectory
控件根部的特殊文件夹。如果您传递桌面,
ExpCombo
将以默认样式重建,并添加“我的电脑”中的所有文件夹。StartUpDirectory
启动时要选择的文件夹。
SelectFolder
选择一个特定文件夹,接受路径或
CShItem
。BuildCombo
使用指定的
CShItem
作为根重建ExpCombo
。
RefreshCombo
以当前状态重建 ComboBox。
浏览器
这基本上是一个结合了 ExpCombo
和 ExpList
的控件,并带有此类控件的标准导航按钮。
我已尽力确保此控件公开了所有所需的方法/函数/事件(Browser
没有 Refresh
方法,但当按下 F5 且 ExpList
获得焦点时,控件会刷新)。所有相关的 ExpList
事件都已包含,并添加了 SelectedFolderChanged
(当在 ExpCombo
中选择新文件夹时触发)和 SelectedListIndexChanged
(当 ExpList
的 SelectedIndex
更改时触发)事件。
该控件还具有 SaveState
和 LoadState
方法,这些方法需要一个 XML 文件或注册表项来写入/读取(在最新版本中,有一个重载的 SaveState
方法,它返回一个包含状态设置的 DataSet
)。这些方法存储和恢复当前选中的文件夹、视图样式、大小、列宽度以及排序的列和顺序。
关注点
全球化
我在源文件和演示应用程序中添加了对两种语言(德语和英语)的支持。控件的 Language
属性(OperatingSystem
)的默认设置为使用操作系统的区域性,如果找不到匹配项,则默认为英语。所有 MessageBox
字符串、工具提示和菜单项都从程序集中的 .resx 文件中获取字符串,使用 System.Resources.ResourceManager
类。添加新语言只需将 ControlStrings.resx 复制并用正确的字符串值更新,然后保存为适当的名称 - ControlStrings.[culture code].resx。例如,法语为 ControlStrings.fr.resx,英语(英国)为 ControlStrings.en-GB.resx。
我在这里使用 resx 资源文件而不是 IDE 中的本地化控件,原因很简单:我无法在本地化控件中设置 MessageBox
字符串。使用 resx 资源文件的一个主要缺点是您无法使用 IDE 为不同区域性更改控件的大小,这必须在代码中完成;但这不会影响此库中的控件,因此 resx 资源文件是显而易见的最佳选择。
我还发现,在获取 Web 文件夹中文件的路径时,返回的 URL 是编码的(例如“%20”而不是空格)。我曾在这里进行 String.Replace
操作,直到我意识到 URL 中有很多字符会被编码(ä、ö、ü 和 ß 对我产生了影响)。我研究了编码,并想出了以下代码(它可能不完美,但比我之前做的 8 次 Replace
调用要好)。
Public Shared Function DecodeURL(ByVal url As String) As String
'Return if there are no special characters
If url.LastIndexOf("%"c) = -1 Then Return url
'Integer to hold character index
Dim index As Integer
'String to hold Hexidecimal character code
Dim hexCharacter As String
Do
index = url.IndexOf("%"c)
'Return if there are no more special characters
If index = -1 Then Return url
'The two characters after the '%' in a URL is the hexidecimal
'ISO 8859 character code
hexCharacter = "&" & url.Substring(index + 1, 2)
hexCharacter = Encoding.GetEncoding("ISO-8859-1").GetString(New _
Byte() {CByte(hexCharacter)})
url = url.Replace("%" & url.Substring(index + 1, 2), hexCharacter)
Loop
End Function
这基本上是通过获取 URL 中“%”后面的十六进制值对应的 ISO 8859 字符来替换字符。这应该适用于所有路径。
致谢
我有很多要感谢的人,才使我完成了这一切,但尤其要感谢 Jim Parsells 的上述文章和代码,以及 Daniel Presman 的 CodeProject 文章,关于 ComboBox 中的图标。我还要感谢 Scott 在论坛上关于使用 DrawItemEventArgs
中的 State
属性来调整 ExpCombo
项在选中时的显示情况,以及 Dominique 的高质量反馈和改进。
历史
- 2005 年 5 月 16 日 - 发布文章和代码的第一个版本。
- 2005 年 5 月 18 日 - 在
SystemImageListManager
中实现了 Jim Parsells 的GetIconIndex
Sub
的新版本,允许返回CShItem
的选定图标。这修复了ExpComboSelectedItem
中由我修改SystemImageListManager
类之外的 System 图像列表而导致的错误。谢谢 Jim。 - 2005 年 6 月 13 日 - 更新了文章和代码。
- 删除了
ExpComboSelectedItem
,并使用DrawItemEventArgs.State
属性来控制当前选中项的显示。 - 修复了
ExpList
中ShowHidden()
方法发生的重载错误。 - 向
ExpList
添加了File/FolderActivate
事件。 - 将默认的
ExpList
上下文菜单设置为Public
,并向Browser
添加了ListViewContextMenu
属性。 - 改进了
ExpCombo
的实现,使其可以独立于Browser
更好地使用。 - 更改了
ExpComboItem
的默认排序,以按CShItem
排序。
- 删除了
- 2005 年 9 月 19 日 - 更新了文章和代码。
- 添加了
ExpListItem
类 - 这可能包含破坏性更改(请参阅上面的ExpList
部分)。 - 使用
ExpListItem.State
属性修复了隐藏项的显示。 - 向
ExpList
添加了分组。 - 向
ExpList
添加了ColumnHeaderContextMenu
。 - 向
ExpList
添加了SelectedColumn
的显示。 - 扩展了
Browser
控件的LoadState
和SaveState
函数,以接受注册表项。
- 添加了
- 2006 年 8 月 2 日 - 更新了文章和代码。
- 完全重新设计了
ExpList
以使用新的ExpListItem
集合。 - 为
ExpListColumns
添加了设计时支持。 - 向
ExpList
添加了缩略图视图。 - 对所有类进行了通用改进,以更好地反映基类行为。
- 完全重新设计了
- 2006 年 8 月 9 日 - 更新了文章和代码。
- 向
ExpCombo
添加了Style
属性(有关详细信息,请参见对象浏览器)。 - 修复了
ExpCombo
中打开和选中图标的显示。 - 修复了
ExpList
缩略图视图中的 LabelEdit 错误。 - 修复了
ExpList
缩略图视图中的选中文本显示。
- 向