前言:随着技术的不断完善,微软Windows操作系统已成为当前个人计算机应用领域的主流操作系统。Windows操作系统提供了颇受用户喜爱的图形用户界面(GUI),微软为Windows的这个户界面保留了可扩充性,它使得基于32位的Windows应用程序可以通过多种方式来增强系统所提供的操作环境(也称为外壳,英文名称:Shell)。 通过对外壳的扩展,开发人员可以为用户提供其他的文件对象操作方式,或者简化文件系统和网络的浏览,或者使用户能更方便地调用文件系统中对各种对象进行处理的工具。为了说明基于COM接口技术的外壳扩展技术,本文首先编写一个简单的音频播放器,然后编写几个外壳扩展处理程序方便用户对指定文件的操作。
一、外壳编程的方法
编写外壳扩展(以下简称Shell)的应用程序有两种方法,第一种外壳扩展是指无需编程即可实现的Shell扩展,只需要修改相应的注册表条目就可以让Shell按照我们的意愿行事。第二种Shell扩展需要编程来实现,它的功能比第一种Shell扩展要强大得多,在本文中提到的Shell扩展所指的就是这一种。
Windows中Shell扩展处理程序都是基于COM接口,它通过特定的COM接口与Windows Shell进行正常的交互。为了让Windows Shell能找到扩展处理程序并与之交互,Windows Shell扩展处理程序需要遵循一定的规则,这个规则也是编写Windows Shell扩展处理程序时所要遵循的设计思路。这个规则包括两个方面:(1)Windows Shell扩展处理程序应该在注册表中预先设定的位置登记自己,以便Windows Shell能找到它;
(2)Windows Shell扩展处理程序应该实现Windows Shell知道的几个特定的COM接口,以便与Windows Shell进行正常的交互。
编程实现Shell扩展一般按如下步骤:(1)创建一个服务器项目(通常为DLL);(2)为项目添加一个实现特定Shell接口的COM类;(3)为该COM类实现一个类工厂;(4)为服务器实现一些框架性的代码;(5)编译链接以生成COM服务器;(6)编辑需要的注册文件;(7)测试和调试Shell扩展程序。
二、外壳扩展编程实例
由于编写外壳扩展处理程序的基本步骤是相同的,所以本文仅给出了编写上下文相关菜单处理程序的详细编写过程。关于文件类的上下文相关菜单,当用户用鼠标右键单击Shell名字空间中某个元素时(如某文件、目录、服务器、工作组等,本文中特指AC-3音频文件),Shell将生成此类元素的缺省上下文相关菜单(注意不同的文件对象生成缺省菜单是不一样的),Shell会搜索注册表,装载此类文件对象所登记的上下文相关菜单扩展,以便让这些扩展程序向已生成的上下文相关菜单中加入另外一些定制的菜单项。如果要为某个文件类对象增加一个上下文相关菜单扩展程序,需要在Windows注册表的下列位置进行注册:
HKEY_CLASSES_ROOT
<.扩展名>=<文件类描述>
......
<文件类描述>
Shellex
ContextMenuHandlers
ContextMenuHandler_Name={该上下文相关菜单处理程序GUID }
需要说明的是,还需要为上下文相关菜单处理程序进行登记,以便Shell能够知道在哪里找到处理程序:
HKEY_CLASS_ROOT
CLSID
{该上下文相关菜单处理程序GUID}=<处理程序描述>
InProcServer32=<服务器所在完整路径>
"Threading Model"="Apartment"
解决这个问题后,剩下的问题就是Windows Shell与上下文相关菜单处理程序交互问题,这就需要上下文相关菜单处理程序在代码中实现两个Shell知道的接口,即IContextMenu接口和IShellExtInit接口。IShellExtInit接口只有一个成员函数,即Initialize,当Shell决定对选定调用上下文相关菜单处理程序时,它会首先调用IshellExtInit接口的Initialize方法,以要求处理程序对自己进行初始化。IContexMenu接口中定义了三个方法,它们分别是QueryContexMenu、InvokeCommand和GetCommandString()。当用户用鼠标右键单击文件对象时,Shell将要显示出其上下文相关菜单。
这时系统将此文件对象的上下文相关菜单的地址传给此上下文相关菜单处理程序。但在处理程序中只应用此地址来向上下文相关菜单中加入菜单项,而不应修改或删除已有的菜单项,因为可能还会有其他处理程序在此前或此后向菜单中加入菜单项。最后,当所有的上下文相关菜单处理程序都被调用后,Shell再向此菜单中加入它的菜单选项。菜单中的菜单项可以是与特定类相关的(即适用于某种类型的所有文件),也可以是与某特定事例相关的(即只适用于单个文件对象)。
当Windows Shell将要为某文件对象显示其上下文相关菜单(或显示菜单条上的File菜单)时,系统将调用上下文相关菜单处理程序中IContextMenu接口的QueryContexMenu成员函数。此时上下文相关菜单处理程序可通过调用InsertMenu函数按位置(MF_POSITION)直接将想要加入的菜单选项加入列上下文相关菜单中。加入的菜单项可以是通常的字符串(MF_STRING),也可以是位图(MF_BITMAP)。当用户选中某个由上下文相关菜单处理程序维护的菜单选项时,Shell将调用此处理程序IContextMenu接口的InvokeCommand成员函数以使处理程序有机会处理用户选择的命令。若对某类文件登记有多个上下文相关菜单处理程序,则各命令的顺序是由文件类下的ContextMenuHandlers关键字决定。
上下文相关菜单处理程序的具体的编写过程如下:
(1)创建一个空的DLL项目
上下文相关菜单处理程序仍将以进程内COM服务器的形态存在,因此需要用为上下文相关菜单处理程序创建应该空的DLL项目,本文将项目命名为ContextMenuExt;
(2)为项目添加一个类CContextMenuExt
首先还是来考虑DLL需要实现的功能,上下文相关菜单处理程序需要一个类来实现COM接口IContextMenu和IShellExtInit。为此,笔者给项目添加一个类CContextMenuExt。添加了CContextMenuExt类之后,从项目的文件视图可以看出项目中多了两个文件,它们分别是类CContextMenuExt的头文件(CContextMenuExt.h)和源代码文件(CcontextMenuExt.cpp)。接下来修改CContextMenuExt的类声明,使之继承IContextMenu和IShellExtInit接口,并添加3个成员变量m_cRef、m_pDataObj、m_szFileName和一个受保护的成员函数。在CContextMenuExt类的公有声明部分,定义了类的构造函数、析构函数以及为了实现IUnknown、IContextMenu和IShellExtInit接口而设的成员函数。之所以要实现IUnknown接口,是因为IUnknown接口是一切接口的祖先。除继承了IUnknown接口的3个基本方法之外,IContextMenu接口还定义了真正实现菜单扩展的QueryContextMenu、InvokeCommand和GetCommandString方法,关于这些方法的作用,前面已经做了简要的说明,这里不在赘述。
在CContextMenuEx类的受保护部分,声明了一个成员函数ClickSample,这个函数正是为了处理添加的菜单命令而设的。定义了一个数组m_szFileName用以保存选中文件的完整路径;
(3)为类CContextMenuExt添加实现代码
为了清楚起见,笔者将逐步给出其各个成员函数的说明。首先来看看CContextMenuExt类的构造函数。在构造函数中对两个成员变量进行了初始化,其中m_cRef表示对该类的引用计数,m_pDataObj则表示系统传送过来的IDataObject接口的指针,将在后面使用。g_nDllRefCount是DLL中需要定义的一个全局变量,用来记录整个DLL被引用的次数,并将以此决定是否运行Shell对DLL服务器进行卸载。当用户选中了某个AC3文件类的上下文菜单DLL服务器维护的菜单选项时,Shell将调用CContextMenuExt类中IContextMenu接口的InvokeCommand成员函数来处理用户的选择。此处CContextMenuExt的做法是通过参数lpcmi的lpVerb找到用户选择的菜单的标志符,并调用相应的成员函数进行处理。在本文中这个成员函数启动AC3播放器播放存放在m_szFileName数组中的文件;
(4)为项目添加另一个类CcontextMenuExtFactory。Dll中有了CContextMenuExt类之后,还需要一种手段来创建DLL中的这个功能类。于是便需要添加另一个类CContextMenuExtFactory来作为类CContextMenuExt类工厂。所谓"类工厂",是专门用来生产别的功能类的COM对象。它需要实现IClassfactory接口,当然也需要实现IUnknown接口。其中的CreateInstance和LockServer两个成员函数是IClassFactory接口要求其实现类必须实现的方法;
(5)为服务器实现一些框架性代码
需要为整个DLL定义一个引用计数变量g_nDllRefCount和一个用来记录DLL的模块句柄的变量g_hThisDll。作为Win32的动态链接库,需要为DLL实现几个标准函数,如DLLMain、DLLCanUnloadNow、DLLGet ClassObject等,首先为这些函数添加原型。函数DllMain是所有Win32动态链接库的入口函数。在DLL的全局变量g_hThisDll中保存进程的实例标识hInstance以备后用。当系统的进程或线程初始化或者被终止时,Dll中的DllMain函数即被系统以相应的参数调用。此外,当别的进程或者线程调用LoadLibrary函数装载Dll或者调用FreddLibray函数卸载Dll时,DllMain函数也会被系统调用。函数DllCanUnloadNow通过全局变量g_nDllRefCount来决定实现该函数的DLL是否正在使用之中,如果不是,调用者就可以将该DLL安全地从内层中卸载。DllGetClassObject函数被用来获取DLL中定义的类的对象实例。DllGetClassObject函数通常在CoGetClassObject函数中被调用。
(6)编辑上下文菜单处理程序的注册文件制作一个 .reg文件,按照一定的格式书写完后,在操作系统中双击这个文件,文件表的注册条目被合并到注册表中,这里需要强调的是该处理程序的类和接口的全局唯一标志符是使用Visual C++6.0提供的UUIDGEN实用工具生成的。还需要说明的,由于笔者使用的操作系统是Windows2000,所以编译连接好的ContextMenuExt.dll文件应放到C:WINNTSystem下.