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

VB.NET 中的 Explorer ComboBox 和 ListView

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (49投票s)

2005 年 5 月 15 日

CPOL

18分钟阅读

viewsIcon

1095125

downloadIcon

13279

一个 ComboBox(和 ListView)显示文件系统,使用系统图像列表。

Sample Image - ExpCombo/ExpCombo.jpg

引言

本文是 Jim Parsells 的文章 “一个全 VB.NET 的 Explorer Tree 控件,带 ImageList 管理” 的扩展。我在 Jim 的 ExpTreeLib 程序集中添加了两个主要类 - ExpComboExpListExpList 直接继承自 ListView,部分基于 Jim 演示项目中的 ListView,并添加了一些功能,如列标题中的图标以及隐藏文件或文件夹的半透明图标。ExpCombo 使用 Jim 的 SystemImageListManagerCShItem 类来创建一个类似于 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 客户端来说将显示为 AddAdd2Add3。这也意味着 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 的列类。

  • ExpListItemCollectionSelectedExpListItemCollectionExpListColumnCollection

    ExpList 中项和列的集合类。

  • ExpListComparer

    ExpList 的默认比较器类,它以适当的方式对标准的 ExpListColumnTypes 进行排序。

ExpCombo

  • ExpCombo

    代表文件系统的 ComboBox。

  • ExpComboItem

    ExpCombo 的项类。

  • ExpComboItemCollection

    ExpCombo 中项的集合类。

用户控件和对话框

  • 浏览器

    一个结合了 ExpListExpCombo 控件的用户控件,具有内置导航等附加功能。

  • BrowseForFolderOpenFileDialogSaveFileDialog

    .NET Framework 提供的相同控件的复制(但支持 SharePoint 文件)。

  • FileSelector

    一个用户控件,结合了浏览器和第二个 ExpList 用于文件选择(批量处理等)。

杂类

  • Thumbnail

    一个用于从 CShItem 中提取 Windows 缩略图的类。

  • 音效

    用于在导航控件时发出“点击”声音的类(将 Silent 属性设置为 True 以关闭声音)。

  • FlatButton

    创建此类是因为 ToolBarButton 类在浏览器控件中的行为不完全符合我的预期。

ExpList

这本质上是一个类似 Explorer 中的 ListView。它具有允许它附加到 ExpComboExpTree 的属性,以便自动添加附加控件中当前选中文件夹的文件和文件夹,并支持通过文件系统进行双击导航。默认情况下,当用户双击文件和文件夹时,它们将被打开。此操作可以通过设置 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 添加文件。如果列表附加到 ExpTreeExpCombo,则此功能将不起作用,因为它仅用于批量处理等的文件选择。

在第二次更新中,我添加了 ExpListItem 类,我对该类进行了另一次破坏性更改(抱歉!),因为它现在有一个 CShItem 属性,而不再是存储相关 CShItemShItem 属性。该类还有一个 State 属性来显示 CutNormal 的图标 - 我用它来正确显示 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 枚举包含三个“自定义”类型(CustomTextCustomeDateCustomNumeric)。这些可用于在列表中显示自定义数据,并根据类型进行适当排序。使用这些列类型之一时,应在三个位置向列表项添加 SubItem 文本:托管窗体的 Load 事件、列的 ColumnAdded 事件以及列表的 ItemsAdded 事件(在演示应用程序中,我向浏览器控件添加了一个自定义列)。ExpListColumn 还有一个 CustomGrouping 属性;当将其设置为 True 时,调用类可以手动设置 ExpList.Group(或 Browser.Group)EventHandler 中的组 - 请参阅演示应用程序中的示例。

我做的最后一个重大更改是为 ExpList 添加了缩略图视图。这使用了 ExpListOwnerDraw 模式来绘制文件缩略图,或者在没有缩略图可用时绘制特大图标。这需要在 SystemImageListManager 中添加 hXLargeImageList 属性,并在 Thumbnail 类中添加其唯一的静态 ExtractThumbnail 方法。此公共方法将缩略图作为 Bitmap 返回。

ExpList 摘要

  • ExpComboExpTree

    这两个互斥的属性将列表附加到一个“驱动”控件。

  • ShowHiddenFiles

    是否显示隐藏文件或文件夹。

  • StandardContextMenu

    控件的标准上下文菜单。

  • File/FolderOpenOnActivate

    设置当列表中的文件或文件夹被激活时是否执行默认操作(打开)。如果设置为 False,则会引发相应的 File/FolderActivate 事件。

  • Filter

    为要显示的文件类型设置过滤器(用法请参见上文)。

  • ShowPaths

    在“详细信息”视图中使用,用于在显示文件大小、类型等与显示路径之间切换。

  • Sortable

    列表是否可排序。

  • AllowRename

    文件和文件夹是否可以重命名。

  • AllowNewFolders

    是否可以在控件中创建新文件夹。

  • 语言

    控件使用的语言。

  • Filter

    在检查通过的 CShItem 是否符合过滤器字符串后,返回 TrueFalse。或者,您可以传递一个 CShItemArrayList,它将过滤并返回此列表。

  • DecodeURL

    用于具有 URL 路径的文件。删除“%20”和其他 URL 编码,并将其替换为相应的字符。

  • RefreshList

    刷新关联的 ExpTreeExpCombo,或者刷新列表项后面的 CShItem,以最合适的方式进行。

  • File/FolderActivate

    这些事件仅在 FileOpenOnActivateFolderOpenOnActivate 设置为 False 时引发。

ExpCombo

ExpCombo 类直接继承自 ComboBox,因此暴露了所有常规的属性和方法。它利用 ExpComboItem 类来创建显示文件系统所需的分层结构,并存储相关文件夹的 CShItem 属性。请确保只添加 ExpComboItem 到 ComboBox,因为在 OnDrawItem 方法中进行了直接转换。

在第一个版本中,我使用了一个额外的类来绘制选中的 ExpCombo 项。这是我的一个解决方法,用于避免在下拉列表中选择的任何项旁边出现空格。Scott 在论坛帖子中指出,ComboBoxOnDrawItemEventArgs 有一个 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 本身。我添加了两个属性,StartUpDirectoryRootDirectory,它们利用了 ExpTree.StartDir 枚举。我还添加了相对容易理解的 SelectFolderBuildCombo 方法。这基本上使控件更加独立,意味着您可以将 ExpComboExpList 添加到窗体,设置几个属性,然后就可以使用了(Robert's your father's brother...)。

重载方法 SelectFolder 允许选择 ExpCombo 中的一个项,可以传递 CShItemExpTree.StartDir 枚举中的特殊文件夹。也可以传递文件夹的路径。这将返回一个布尔值,表示操作的成功。表示该路径的文件夹不必在调用前存在于 ExpCombo 中。此函数基本上是 ExpTree.ExpandANode 函数的一个改编版本。

在最新的更新中,我对 ExpCombo 所做的操作与对 ExpList 所做的操作类似,添加了 ExpComboItemCollection 以实现 ExpComboItem 的强类型化,并使控件的方法与 .NET ComboBox 更一致。

ExpCombo 摘要

  • SelectedFolder

    当前选中的 ExpComboItem

  • ShowHiddenFolders

    是否显示隐藏文件夹。

  • RootDirectory

    控件根部的特殊文件夹。如果您传递桌面,ExpCombo 将以默认样式重建,并添加“我的电脑”中的所有文件夹。

  • StartUpDirectory

    启动时要选择的文件夹。

  • SelectFolder

    选择一个特定文件夹,接受路径或 CShItem

  • BuildCombo

    使用指定的 CShItem 作为根重建 ExpCombo

  • RefreshCombo

    以当前状态重建 ComboBox。

浏览器

这基本上是一个结合了 ExpComboExpList 的控件,并带有此类控件的标准导航按钮。

我已尽力确保此控件公开了所有所需的方法/函数/事件(Browser 没有 Refresh 方法,但当按下 F5 且 ExpList 获得焦点时,控件会刷新)。所有相关的 ExpList 事件都已包含,并添加了 SelectedFolderChanged(当在 ExpCombo 中选择新文件夹时触发)和 SelectedListIndexChanged(当 ExpListSelectedIndex 更改时触发)事件。

该控件还具有 SaveStateLoadState 方法,这些方法需要一个 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 属性来控制当前选中项的显示。
    • 修复了 ExpListShowHidden() 方法发生的重载错误。
    • ExpList 添加了 File/FolderActivate 事件。
    • 将默认的 ExpList 上下文菜单设置为 Public,并向 Browser 添加了 ListViewContextMenu 属性。
    • 改进了 ExpCombo 的实现,使其可以独立于 Browser 更好地使用。
    • 更改了 ExpComboItem 的默认排序,以按 CShItem 排序。
  • 2005 年 9 月 19 日 - 更新了文章和代码。
    • 添加了 ExpListItem 类 - 这可能包含破坏性更改(请参阅上面的 ExpList 部分)。
    • 使用 ExpListItem.State 属性修复了隐藏项的显示。
    • ExpList 添加了分组。
    • ExpList 添加了 ColumnHeaderContextMenu
    • ExpList 添加了 SelectedColumn 的显示。
    • 扩展了 Browser 控件的 LoadStateSaveState 函数,以接受注册表项。
  • 2006 年 8 月 2 日 - 更新了文章和代码。
    • 完全重新设计了 ExpList 以使用新的 ExpListItem 集合。
    • ExpListColumns 添加了设计时支持。
    • ExpList 添加了缩略图视图。
    • 对所有类进行了通用改进,以更好地反映基类行为。
  • 2006 年 8 月 9 日 - 更新了文章和代码。
    • ExpCombo 添加了 Style 属性(有关详细信息,请参见对象浏览器)。
    • 修复了 ExpCombo 中打开和选中图标的显示。
    • 修复了 ExpList 缩略图视图中的 LabelEdit 错误。
    • 修复了 ExpList 缩略图视图中的选中文本显示。
© . All rights reserved.