[翻译整理] Guide to Shell / Namespace Extension Part I
Wednesday, November 28th, 2007最近由于某种不可抗拒力的需要, 开始啃Windows Shell/Namespace Extension Programming. 因为没有COM 和ATL 基础, 很累人. 在网上找来找去, 关于Shell/Namespace Extension 入门最多的也比较好的就是Code Project 上的几篇软文, Shell Extension 的部分国内有人做了翻译, 做成了CHM, 很不错, 不过原文的教程也已经很明白了, 要写一个Shell 扩展已经成为了可能, 在Visual Studio 的帮助下, 即使没有很深的COM/ATL 知识.
但是我要的是Namespace Extension, 一个比Shell Extension 麻烦多的东西 T_T … Shell Extension 的作者也写了Namespace Extension 的篇文章, 但是明显已经开始跳跃了… 虽然还是Complete Idiot’s Guide to Namespace Extension … 哭…
不过在怎么难受也要继续啃… 加上MSDN 的帮助 — MSDN 上有个Namespace Extension 的例子, RegView, 也挺完整的. 希望可以度过难关啦^^ 这次就做个翻译整理, 权当学习笔记HOHO~~ 边学习边写~~
基础/参考文章地址:
The Complete Idiot’s Guide to Writing Shell Extensions – Index by Michael Dunn [共9个部分]
翻译原文地址:
The Complete Idiot’s Guide to Writing Namespace Extensions – Part I by Michael Dunn [只找到了Part I
] 是2001年的文章, 似乎作者已经不再更新了… 但是它的价值依然存在! 另外, 在CodeProject 可以搜索到另外几篇关于Namespace Extension 的很好的文章来填补这篇文章的空白和缺憾. 以下, 褐色文字及代码为原文/翻译文.
1. Introduction [介绍]
从Shell 的角度来看, 计算机的内部组成 — 包括硬盘, 光驱, 网络映射驱动器, 桌面等等 — 被组织在一个树形结构里面, 并且以桌面(Desktop) 为根节点, 我们称之为Shell Namespace. Explorer 通过Namespace Extension 提供了一些方法来插入自定义的对像. 在这篇文章里, 我们会涉及到创建一个基础的, 简单的Namespace Extension 的所有步骤. 我们的扩展(Extension) 会创建一个虚拟文件夹(Virtual Folder) 来列出计算机上的所有驱动器(Drives) , 类似于下面图片中的我的电脑(My Computer).
这篇文章假设读者有C++, ATL 和COM 的有关知识(Know). 熟悉Shell Extension 会有很大帮助.
对于不熟悉ATL, COM, Shell Extension 的可以先看一下作者的上一系列, 关于创建Shell Extension 的文章. 同样文章有点老. 这里提供一个简单的, 在Visual Studio 2005 下创建一个Shell Extension 项目的步骤. 这个步骤同样适用于Namespace Extension.
1.1 新建一个ATL Project. ATL 可以极大地简化我们编写COM 组件的工作… 如图, 选择ATL 项目类型.
1.2 创建中选项保持默认就可以了. 结束后出现如下窗口, 无视PS 后缀的项目和Generated Files 系列文件夹.
1.3 右键SimpleShellExt 添加一个新的类, 选择ATL Simple Object (有多种ATL 对象, 以后可以择优选择), 示例一个ContextMenu 的扩展, 选择Simple 就可以了. 如下:
1.4 下一步, 出现ATL Simple Object 向导, 选择一个名字(Short Name), 其余的部分会自动填充, 如下:
1.5 这时候, 可以直接接受默认选项直接Finish, 也可以Next 到Options 窗口选择一些额外的属性. 结束后项目中会多出三个文件, SimpleContextMenuExt.h 和SimpleContextMenuExt.cpp, 还有一个注册表自动注册文件SimpleContextMenuExt.rgs ; 这几个文件都是比较重要的, 在原作者的Shell Extension 教程中指示了如何在这些文件内修改/添加代码来实现一个简单的ContextMenu 扩展.
至此, 项目准备工作结束, 可以开始/继续我们的Shell/Namespace Extension Programming 之路鸟~~
我意识到这会是一篇很长的文章, 因为Namespace Extension 及其复杂, 而且我能找到的最好的文档就是MSDN 内的RegView 示例. 这个实例具有完整的功能, 但是并没有解释Namespace 内事件的内部顺序(Sequence). Dino Esposito 的Visual C++ Windows Shell Programming 分享了另外一些经验, 并且包含了一个基于RegView 的示例WinView. 我从这两个示例中获得灵感, 并且追踪了其中的逻辑流程, 并将在本文中解释.
这篇文章中包含的示例是一个基本的扩展; 它完成了一个很小但是完整的功能. (甚至是一个”简单”的扩展都会需要我们这篇文章中所提到的知识.) 我刻意避开了一些话题 — 比如Namespace 下的子文件夹, 以及和Namespace 的其它部分的交互 — 因为这些会使本文更长, 代码更加庞杂. 我会在今后的文章中介绍这些话题. [很遗憾, 没有了... 要自己另外找...]
2. Structure of Explorer [Explorer 的结构]
这样一个熟悉的结构实际上由几个部分组成, 它们对于一个Namespace Extension 来说很重要. 如下图:

在上图中, 像控制面板(Control Panel), 注册表试图(Registry View) 是虚拟文件夹(Virtual Folder). 他们并不是文件系统(File System) 的一部分, 而是像文件夹一样的UI 元素, 他们由Namespace Extension 提供了一些额外的功能. 一个在右边区域显示自己UI 的扩展(Extension) 被称为 Shell View. 一个扩展也可以填充Explorer 的菜单, 工具栏, 和状态栏, 通过Explorer 提供的一个COM 接口. Explorer 管理着这个树形结构, 用来显示Namespace, 而一个扩展对这个树形结构的控制被限制在现实子文件夹(Subfolder)内.
3. Structure of Namespace Extensions [Namespace 扩展的结构]
当然, 一个Namespace 扩展的内部结构会依赖于你所用的编程语言以及编译器. 但是, 这里有一个重要的通用元素(Common Element) — PIDL. PIDL 全称为 Pointer to an ID List (指向ID 列表的指针), 它是Explorer 用来组织在树型结构中显示的子文件夹和项目的数据结构. 虽然数据的实际结构格式由扩展来定义, 但是有一些规则规定了这些数据在内存中是如何组织的. 这些规则为PIDL 定义了一个通用的/一般性地(Generic) 格式, 因此Explorer 可以从各种扩展中来处理PIDL 而不用担心它们的内部结构.
我知道这很含糊, 但是, 我们需要知道的就是: PIDL 是一个扩展用来存贮对于它有意义的数据的机制. 我会在下文中详细介绍 PIDL, 以及如何创建它们.
扩展的另外一个主要的部分是我们必须要实现的一些COM 接口. 这些必须的接口是:
- IShellFolder: 为Explorer 和虚拟文件夹的实现代码提供了一个通信的通道.
- IEnumIDList: 一个COM 迭代器, 可以让Explorer 或者Shell View 遍历一个虚拟文件夹的内容.
- IShellView: 管理出现在Explorer 右部区域的窗口.
更加复杂的扩展会实现自定义Explorer 的树型结构的结构. 但是我在本文中不会介绍它, 因为文中介绍的这个扩展的目的是尽可能的简单.
4. PIDLs
4.1 What PIDLs are [PIDL 是什么]
Explorer 的Namespace 中每一个项目, 无论是文件, 目录, 控制面板, 或者由一个扩展展示的一个对象, 都可以被它的PIDL 所唯一表示. 一个对象的绝对PIDL 和一个文件的绝对路径相似; 它由该对象自己的PIDL 和它所有的父文件夹的PIDL 连接而成. 举例来说, 系统控制面板元素的绝对PIDL 可以被认为是: [Desktop]\[My Computer]\[Control Panel\[System Applet].
一个相对PIDL 则是该对象自己的PIDL, 相对于它的父文件夹. 这样一个PIDL 只对包含该对象的虚拟文件夹有意义, 因为这个文件夹是唯一能够理解该PIDL 中数据的对象.
本文中的扩展处理相对PIDL, 因为它没有和Namespace 的其他部分发生交互. (这样做需要构造一个绝对PIDL.)
4.2 Structure of a PIDL [PIDL 的结构]
一个PIDL 的结构类似于单链表, 除了没有指针之外. 一个PIDL 包含有一系列的 ITEMIDLIST 结构, 一个挨一个地安置在内存一块连续的区域中. 一个ITEMIDLIST 只有一个成员(Member), 一个SHITEMID 结构:
typedef struct _ITEMIDLIST
{
SHITEMID mkid;
} ITEMIDLIST;
SHITEMID 的定义为:
typedef struct _SHITEMID
{
USHORT cb; // Size of the ID (including cb itself)
BYTE abID[1]; // The item ID (variable length)
} SHITEMID;
成员 cb 记录了整个结构(Struct) 的大小, 功能和单链表中的next 指针相类似. 成员 abID 则是Namespace Extension 存储它自己的数据的地方. 这个成员允许有任意长度; cb 的值指定了它的确切大小. 举例来说, 如果一个扩展存储量12 个字节(byte) 的数据, 则cb 为14 (12+ sizeof(USHOR)). abID 可以存储对于Namespace 来说有意义的任何数据. 但是, 无论如何,一个文件夹下的两个对象都不能有相同的数据, 就好象在同一个目录下不能有两个文件同时有同一个文件名一样.
在PIDL 的结尾由一个cb 字段被设置为0 的SHITEMID 结构指定, 就好象一个链表的最后一个next 指针被设置为NULL 一样.
这里有一个PIDL 的例子, 它只包含了一块数据, 由一个变量pPidl 指向列表的开头.

注意我们可以通过将每个结构的cb 值加到指针上来从一个SHITEMID 结构移动到下一个.
现在, 你可能会问, 如果Explorer 不知道数据的格式, 一个SHITEMID 或者一个PIDL 有什么用. 事实上, Explorer 把PIDL 视作不透明数据类型, 它只负责将它们传送给Namespace. 这很像句柄. 当你有一个, 比如HWND 的时候, 你不用知道在这个窗口内部的数据结构是如何的, 但是你知道你可以通过把它传回给操作系统来对这个窗口做任何事情. PIDL 则相反, Explorer 不知道一个PIDL 内的数据结构, 但是可以通过将其传送给Namespaces 来和他们交互.
4.3 Our Namespace’s PIDL Data
正如上面所提到的, 用来表示Namespace 的文件夹下的项目的数据必须在该文件夹下是唯一的. 幸运的是, 所有的驱动器已经有一个唯一的标志 — 驱动器盘符, 所以我们把该字符存储在abID 内. 我们的PIDL 数据定义为如下的一个PIDLDATA 结构:
struct PIDLDATA
{
TCHAR chDriveLtr;
};
5. Namespace Extension Interfaces [Namespace 扩展的接口]
5.1 IEnumIDList
IEnumIDList 是一个COM 迭代器(Enumerator) 的实现, 它可以遍历一个PIDL 的集合. 一个COM 迭代器实现了对一个集合的顺序读取, 就像STL 集合的迭代器(Iterator) 一样. ATL 为我们提供了实现了该迭代器的类, 所以我们所需要做的只是提供数据的集合以及高速ATL 如何去存取PIDL.
IEnumIDList 在以下两种情况下被使用:
- Shell View 需要遍历一个文件夹的内容以便显示它们.
- Explorer 需要遍历一个文件夹的子文件夹以便填充树形结构.
因为我们的扩展不包含子文件夹, 因此我们只需要考虑第一种情况.
5.2 IShellFolder, IPersistFolder
IShellFolder 是Explorer 用来初始化一个扩展以及和它交流的接口. Explorer 会在创建View 窗口的时候调用IShellFolder 的方法. IShellFolder 也有遍历一个扩展的虚拟文件夹的内容的方法, 以及为了排序而比较两个项目的方法.
IPersistFolder 有一个方法, Initialize(), 一个扩展可以调用它来完成任何初始化任务.
5.3 IShellView, IOleCommandTarget
IShellView 是Explorer 用来通知一个扩展一些UI 有关的事件的接口. IShellView 有一些方法来告诉扩展去创建/销毁一个View 窗口, 刷新显示的内容, 等等. IOleCommandTarget 被Explorer 用来向View 发送命令(Commands), 比如用户按下F5 时的刷新命令.
5.4 IShellBrowser
IShellBrowser 是Explorer 暴露的一个接口, 用来让扩展填充Explorer 窗口. IShellBrowser 有一些方法来修改菜单, 工具栏, 状态栏等等, 也可以发送一个通用的消息给Explorer 的控件.
理论完毕, 第二部分将会是具体实现部分.
[... To Be Continued]




