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

Windows Explorer 通配符选择 Shell 扩展

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2002年8月4日

13分钟阅读

viewsIcon

199230

downloadIcon

3201

一个 Shell 扩展,允许您根据通配符搜索来选择文件。

引言

如果您用过 MS-DOS,那么您一定对通配符模式很熟悉。例如,要获取特定目录中所有文本文件的列表,您可以输入 dir *.txt。而 Windows 资源管理器不允许您这样做。我编写 Wildcard Select 的原因是我偶尔需要从一个文件夹中选择所有的 HTML 文件来对它们执行某些操作。在 MS-DOS 中,我可以使用通配符模式 *.html 来选择这些文件,但在 Windows 中,我只能手动挑选它们。当然,您可以切换到详细信息视图,按类型列排序,然后通过拖动一个框来选择所有 HTML 文件,但这并不特别方便。另外,如果您需要根据扩展名以外的其他标准来选择文件,例如选择所有以前缀 holiday 开头的 GIF 文件,这种方法就行不通了。使用 Wildcard Select,您只需输入 holiday*.gif 即可完成。

Wildcard Select 是一个资源管理器外壳扩展,这意味着它存在于资源管理器的右键菜单中。如果您在文件、文件夹或资源管理器窗口的背景上右键单击,并从菜单中选择 Select...(选择...)项,就会弹出以下对话框:

Wildcard Select screenshot

您可以在编辑框中输入搜索模式。在您输入时,Wildcard Select 会告诉资源管理器选择当前窗口中所有与该模式匹配的文件。如果您按下“取消”或清除该模式,Wildcard Select 将恢复原来的选择(如果有的话)。如果在调用 Wildcard Select 时已有一个或多个文件被选中,它将只尝试匹配该选择中的项目。

在本文中,我想解释一下 Wildcard Select 的工作原理。它使用了一些 COM 和 ATL,一点 MFC,并与 Windows 资源管理器进行了一些交互。我不会展示任何代码片段(这篇文章已经足够长了),但我会详细解释程序的内部工作原理。

如果您查看源代码,您会发现我在某些地方使用了 STL 的 string 类,而在另一些地方我使用了 C 的 char 数组和诸如 strrchr() 之类的函数。有时,我也会使用 MFC 的 CString。我更喜欢使用 STL 类,但有时使用其他类更有意义。例如,如果所有使用特定字符串的函数都期望一个 char 数组,那么仅仅为了保持一致性而将该字符串存储为 CString 或 STL string 就有点小题大做了。我希望这不会让源代码显得过于混乱……

枯燥的部分

您可以在 Michael Dunn 的《The Complete Idiot's Guide to Writing Shell Extensions》(外壳扩展编写完全傻瓜指南)系列文章中读到所有您想了解的关于外壳扩展的内容,所以我不会详细介绍编写实际外壳扩展代码的细节。我在这里只给您一个简短的回顾。外壳扩展是一个 COM DLL。Wildcard Select 并没有自己实现所有的 COM 逻辑,而是只使用了足够多的 ATL 来完成任务。负责处理这些工作的文件是 Select.idlCtxMenu.rgsSelect.cpp

Select.idl 文件描述了 CtxMenu(我们的 COM 类)和 ICtxMenu(它的接口)。CtxMenu 实际上并不需要自己的接口,但 IDL 要求我们指定一个。同样,我们指定了一个类型库,但我们也没有使用它。

CtxMenu.rgs 文件包含了我们外壳扩展的注册表项。这些项告诉资源管理器,我们的外壳扩展必须出现在所有文件、文件夹和资源管理器窗口背景的上下文菜单中。如果您右键单击驱动器名称或特殊文件夹(如控制面板),它将不会出现。

Select.cpp 文件包含几个枯燥的 COM 内务处理函数。DllRegisterServer()DllUnregisterServer() 函数使用 RGS 文件中的信息来注册和注销 DLL。与典型的 ATL 应用程序不同,我们向 _Module.RegisterServer() 传递 FALSE 作为参数。这只会将我们 COM 类的 GUID 放入注册表,而不会放入 ICtxMenu 接口或类型库的 GUID。我们不使用这些,所以没有必要让注册表变得杂乱。

请注意,DllRegisterServer()DllUnregisterServer() 不是由资源管理器调用的,而是由安装程序调用的。如果您下载的是源文件而不是安装程序,您将需要使用命令行工具 regsvr32 自己注册 DLL。如果您在 Visual C++ 中编译源代码,那么 regsvr32 会作为构建过程的一部分运行。

因为 Wildcard Select 使用 MFC 来构建其 GUI,所以 Select.cpp 也包含 CSelectApp 类,它继承自 MFC 的 CWinApp

用户点击了什么?

有趣的事情发生在 CtxMenu.hCtxMenu.cpp 中。这些文件包含了我们在 IDL 文件中看到的 CtxMenu 类的声明和实现。CtxMenu 是 Wildcard Select 的核心。像所有优秀的上下文菜单外壳扩展一样,它实现了 IShellExtInitIContextMenu 接口。

简而言之,过程是这样的:当用户在资源管理器窗口内右键单击时,资源管理器调用 CtxMenuInitialize() 函数。此时,上下文菜单还不可见。然后资源管理器调用 QueryContextMenu() 函数将 Select... 项添加到菜单中,并显示它。当用户将鼠标移到这个菜单项上时,资源管理器从 GetCommandString() 获取描述。最后,当用户选择 Select... 项时,资源管理器调用 InvokeCommand()

我们唯一能确定用户是点击了文件还是窗口背景的地方是在 Initialize() 中。这个区别很重要:如果用户点击了背景,那么没有文件被选中,Wildcard Select 必须尝试将所有文件与通配符模式进行匹配。另一方面,如果用户点击了文件,那么有一个选择集,Wildcard Select 将只尝试匹配该选择集中的文件。

处理这个问题的两个函数分别是 ClickedOnBackground()ClickedOnFileOrFolder()。在前一种情况下,我们将 allFiles 标志设置为 true。在后一种情况下,我们将其设置为 false,并将所选文件的名称复制到 selected 列表中。在这两种情况下,folder 字段都会接收当前文件夹的名称。

资源管理器的内部

当用户从上下文菜单中选择 Select... 项时,资源管理器会调用 InvokeCommand() 函数。在这里,我们弹出对话框并做一些有趣的事情。当然,有趣的事情就是在资源管理器窗口中选择文件。但我们如何做到这一点呢?嗯,用 Spy++ 花十五秒钟就会发现,资源管理器只是用一个 ListView 控件来显示文件名。而在 ListView 中选择项目,我们只需向它发送一个消息即可。

方便的是,InvokeCommand() 接收到资源管理器窗口的 HWND。在我的 Windows 95、98、NT4 和 XP 系统中,这个窗口包含一个类名为 SHELLDLL_DefView 的子窗口,而这个子窗口又包含一个类名为 SysListView32 的子窗口。这就是我们想要找的控件。然而,在 Windows Me 和 2000 上,SHELLDLL_DefView 包含的不是 SysListView32,而是 Internet Explorer_Server,它又包含 ATL Shell Embedding。这最后一个窗口最终包含了我们需要的 SysListView32。我们的 FindListView() 方法接收资源管理器窗口的句柄,寻找 SHELLDLL_DefView,然后简单地递归遍历 SHELLDLL_DefView 的所有子窗口,直到找到我们想要的 SysListView32

现在我们已经成功获取了资源管理器 ListView 控件的句柄,我们显示对话框。对话框由我们的 PatternDlg 类处理,这是一个相当简单的东西,千篇一律。我们向它的构造函数传递一个 this 指针,因为 PatternDlg 偶尔需要调用 CtxMenu 中的函数。具体来说,它在用户输入字符时调用 SelectFiles(),在用户清除搜索模式时调用 RestoreOriginalSelection()

选择文件

接下来是 SelectFiles() 函数,它完成了所有的实际工作。我没有自己编写通配符匹配函数,而是决定使用 Win32 的“查找文件”API 函数,因为它们已经知道如何处理通配符了。无论如何,我们都需要这些函数来读取当前文件夹的内容。所以我们只需将用户的搜索模式输入到 FindFirstFile() 中即可。然后我们将遍历它找到的所有文件,并告诉资源管理器的 ListView 选择相应的项目。

但什么是相应的项目呢?事实证明,我们不能简单地将从 FindFirstFile()FindNextFile() 获取的文件名与列表项中的文本进行比较,因为 ListView 中显示的内容可能不是实际的文件名。

如果 Windows 的“允许所有文件名大写”选项被禁用,那么文件名的字母大小写可能会有所不同。例如,名为 AUTOEXEC.BAT 的文件对应的列表项是 Autoexec.bat。如果这只是唯一的问题,我们可以进行不区分大小写的比较,但可惜的是,Windows 还有一个“隐藏已知文件类型的扩展名”选项。启用该选项后,像 cool.exe 这样的文件名将只显示为 cool,没有扩展名。最后,查找函数会很乐意返回隐藏文件,但您的资源管理器窗口可能被设置为不显示它们。

当然,一定有办法让 Windows 自己知道哪个文件名属于某个特定的列表项吧?嗯,我在这里猜了一下,并假设列表项的 lParam 成员包含了指向该文件的 PIDL。(关于 PIDL 等更多信息,请参阅 Mike Dunn 关于命名空间扩展的文章。)我猜对了——那天我一定很幸运 ;-) 我们实际上不需要对那个 PIDL 做任何事情,只需将它提供给 SHGetPathFromIDList(),它就会反过来给我们提供这个列表项所代表的文件的路径。不错吧。现在我们就可以将它与实际的文件名进行比较了。所有这些神奇的操作都发生在我们的 GetListViewIndex() 函数中。它返回文件在 ListView 中的索引。

实际上,SHGetPathFromIDList() 给我们返回了正确的文件名,但路径是错误的,总是从桌面开始(至少在我的测试中是这样)。这可能与相对和绝对 PIDL 有关。(再次,请参阅 Mike 的文章。)然而,我一点也不在乎。我们可以简单地忽略文件名之前的所有内容;毕竟,查找函数也不包含整个路径,所以我们无论如何都必须剥离它来进行比较。

最后,SelectListViewItem() 函数接收我们从 GetListViewIndex() 获得的索引,并使用 ListView_SetItemState 宏向 ListView 控件发送消息。请注意,我们不仅设置了 LVIS_SELECTED 标志来选择项目,还设置了 LVIS_FOCUSED 来使其获得焦点。如果我们不这样做,那么焦点可能会落在一个未被选中的文件上,如果您使用键盘与资源管理器交互,这会相当尴尬。

其他事项

为了提供尽可能多的反馈,选择操作是在用户输入时进行的。不幸的是,ListView 只有在拥有焦点时才会显示选中的项目。但由于用户是在对话框中输入,ListView 没有焦点,因此在用户关闭对话框之前不会显示选择。至少,在我的 Windows 98 机器上是这样的。在 XP 上,它确实会显示选择,但是是浅灰色,而不是深蓝色。

快速浏览一下 MSDN 就会发现,ListView 有一个名为 LVS_SHOWSELALWAYS 的特殊样式,它负责这个功能。现在即使控件没有焦点,它也总是显示选择。在旧版本的 Windows 上,资源管理器没有设置这个样式,尽管它的 ListView 支持它。因此,在显示对话框之前,我们在 ListView 中设置 LVS_SHOWSELALWAYS 标志。你敢相信吗,现在它在用户输入时*确实*显示了选择。当然,我们在对话框关闭后恢复了 ListView 的原始样式。

能够从文件夹背景的右键菜单中调用 Wildcard Select 很方便,但正如 Mike Dunn 所评论的,这里存在一个可用性问题。在 XP 之前的所有 Windows 版本中,传递给 IContextMenuQueryContextMenu() 函数的索引是 -1,这意味着 Select... 项被添加在菜单的底部,位于“属性”项之下。出于习惯,大多数用户会期望“属性”是菜单上的最后一项,如果我们把 Select... 放在底部,他们会感到非常困惑(和恼火)。所以我们把它放在顶部。如果 index 等于 -1,那么我们就假装它是 0。这样做不好吗?我不知道。在我的测试中,它似乎工作得很好……(顺便说一下,在窗口背景中右键单击只适用于 shell 版本 4.71 或更高版本。)

因为我们将 ListView 的内容与实际文件名进行比较,所以快捷方式有点问题。资源管理器没有显示,但快捷方式的文件名实际上是以 .lnk 扩展名结尾的。如果你想匹配一个快捷方式,那么你的模式也应该以 .lnk 结尾……(幸运的是,SHGetPathFromIDList() 不会尝试解析快捷方式,否则我们就真的有麻烦了。)

SelectFiles() 函数并不是特别智能。它包含几个嵌套循环,如果当前文件夹包含大量文件(比如超过 2000 个),Wildcard Select 可能会非常慢。我计划在未来对其进行一些提速。欢迎任何建议!

致谢

Wildcard Select 的对话框使用了两个子类化控件:一个自动完成的组合框和一个超链接。两者都是基于 Chris Maunder 编写的代码:实现一个自动完成的组合框超链接控件。我为了适应这个项目对代码做了一些修改;如果你想重用这些类,最好还是使用 Chris 的原始代码。外壳扩展代码是基于 Michael Dunn 的《外壳扩展编写完全傻瓜指南》系列文章中的示例。

有什么新内容?

版本 1.0 在从非资源管理器列表视图的地方调用时会给出一个错误消息(“无法向资源管理器发送消息”)。例如,当你在资源管理器窗口的树状视图中右键单击时就会发生这种情况。现在 Wildcard Select 无法找到列表视图,因为这次 InvokeCommand() 函数没有被赋予资源管理器窗口的 HWND,而是其他某个窗口的 HWND

我花了一些时间编写代码,首先找到主资源管理器窗口的句柄,而不是假设我们已经有了它。但后来我意识到,从树状视图的右键菜单调用 Wildcard Select 没有多大意义。尽管一个文件夹可能在树状视图中被选中,但这并不一定意味着它的内容在右侧可见。而且如果你看不到文件,选择它们也没有多大意义。

解决这个问题的最简单方法原来是:除非是从资源管理器的列表视图中调用,否则不要将 Select... 项添加到上下文菜单中。我们在 QueryContextMenu 中这样做。首先,我们用 GetFocus 获取活动窗口的 HWND。事实证明,这就是列表视图的 HWND,所以这很简单。然后我们确保窗口的类名是 SysListView32。如果不是,我们就直接退出,不添加我们的菜单项。

© . All rights reserved.