Windows Explorer 通配符选择 Shell 扩展





5.00/5 (10投票s)
2002年8月4日
13分钟阅读

199230

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 会告诉资源管理器选择当前窗口中所有与该模式匹配的文件。如果您按下“取消”或清除该模式,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.idl
、CtxMenu.rgs
和 Select.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.h
和 CtxMenu.cpp
中。这些文件包含了我们在 IDL 文件中看到的 CtxMenu
类的声明和实现。CtxMenu
是 Wildcard Select 的核心。像所有优秀的上下文菜单外壳扩展一样,它实现了 IShellExtInit
和 IContextMenu
接口。
简而言之,过程是这样的:当用户在资源管理器窗口内右键单击时,资源管理器调用 CtxMenu
的 Initialize()
函数。此时,上下文菜单还不可见。然后资源管理器调用 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 版本中,传递给 IContextMenu
的 QueryContextMenu()
函数的索引是 -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
。如果不是,我们就直接退出,不添加我们的菜单项。