一般介绍
很多人一定用过ZipMagic,对它能把一个压缩文件映射成文件夹感到很奇怪,不知道它使用了什么技术,实际上它用到的技术就是实现了一个外壳的命名空间扩展(Shell Namespace Extention)。
文件夹和视图:资源管理器的基本结构
资源管理器的界面显示为两部分:左边显示的是对象在外壳命名空间的位置,它们是以树结构显示的,通常认为左边显示的应该是文件目录树,但事实上,左边还显示了很多并不是文件目录的外壳对象,比如控制面板、打印机等,事实上在资源管理器中看到的文件夹、控制面板、网上邻居等广义上来说都是命名空间;管理器右边显示了当前被选对象的详细内容,当选择目录时,右边显示目录中的文件,当选择控制面板时,右边显示控制面板项。这就是文件夹和视图结构。
文件夹同管理器的交互
传统文件夹是由外壳实现的代表硬盘上的物理目录结构,我们不能重载它的实现。而虚拟文件夹是通过外壳扩展的COM对象来实现的,比如控制面板。COM对象至少必须是以动态连接库形式实现了IUnknown和IShellExtInit接口。命名空间的两个主要组成部分是文件夹对象和视图对象。它们分别实现了IShellFolder和IShellView接口。
项目标识符
管理器左边显示的每一个项目都有一个唯一的标识符,由于项目不一定是文件,所以外壳不能再用目录来标识它们了。windows用项目标识符来表示它们,标识符的结构如下:
PSHItemID = ^TSHItemID;
TSHItemID = packed record { mkid }
cb: Word; { 需要添入结构的大小 }
abID: array[0..0] of Byte;
end;
标识符很少单独使用,通常一个连一个地在标识符号链表里出现,当cb为0时表示到达链表的末尾了。当一个文件夹对象被创建了以后,外壳会调用它的IPersistFolder接口并传递给它一个标识符链表。
命名空间类型
系统创建的命名空间称为标准命名空间,用户创建的则称为用户定制的命名空间。注意用户定制的命名空间只有根节点对象才会自动出现在标准命名空间内。可以使用两种方法来创建命名空间:
在标准命名空间里创建一个目录并把类标识符附在文件夹对象的后面,作为对象的文件名扩展。例如:
Custom Namespace.{12345678-0000-0000-0000-C00000000000}.
在注册表中创建下列键值:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\
Explorer\Desktop\Namespace.
资源管理器会调用扩展的文件夹对象来枚举它的子对象,就好像子文件夹一样。
文件夹同子对象的交互
当用户点击文件夹的"+"号时,管理器会调用IShellFolder.EnumObjects函数来显示子文件夹列表。当用户点击最下层文件夹时,管理器会用视图来显示对象的内容。之前我们要做两件事:
创建文件夹对象。管理器先创建被选中的文件夹对象的父对象,然后调用IShellFolder.BindToObject函数。
管理器调用文件夹对象的IShellFolder.CreateViewObject函数来创建视图对象。
文件夹同视图的交互
视图有两种类型:一种是弹出式视图窗口,另一种是普通视图显示在资源管理器右边。文件夹对象创建这两种视图是通过调用视图对象的IShellView.CreateViewWindow函数实现的。必须注意的一点是一个文件夹对象可能会对应多个视图对象,因为用户可能会为一个文件夹开很多窗口。这意味着视图和文件夹对象必须为每个实现创立一个分离的COM对象,资源管理器会负责同步不同视图的内容。
使用Delphi创建命名空间扩展实现视图对象
对象必须做到:
(1)创建一个视图窗口的子窗口并使用它来显示文件夹的内容。
(2)同资源管理器通讯。
(3)向资源管理器的菜单条和工具条添加文件夹相关的命令,并处理这些命令。
(4)显示上下文相关的右键菜单和处理拖放操作。
资源管理器要请求一个视图对象可通过调用文件夹对象的 IShellFolder.CreateViewObject方法来实现,过程是:
(1)文件夹对象创建一个视图的新的实例,并返回一个
IShellView接口。
(2)资源管理器初始化视图对象通过调用IShellView::CreateViewWindow方法。创建一个子窗口,并把句柄返回给资源管理器。
(3)视图对象使用IShellBrowser接口来定制工具条、菜单条和状态条。
(4)视图在子窗口里显示文件夹的内容。
(5)视图处理用户的输入命令和工具条及菜单条命令。
初始化视图对象
IShellView.CreateViewWindow方法的参数提供必要的信息给视图来创建子窗口:
(1)前一个视图对象的
接口指针,可以是nil。
(2)一个TFOLDERSETTINGS结构包含先前视图的设置信息,settings。
(3) IShellBrowser接口指针。
(4)
TRECT结构表示视图窗口的显示范围。
IShellView.CreateViewWindow方法是在先前视图被销毁之前被调用的。因此,
接口指针可以让我们同先前视图通信。如果先前接口也属于我们的扩展,我们可以和它通信交换私有配置信息。
一个判断IShellView指针是否属于自己的扩展的简单方法是定义一个私有接口。然后调用 IShellView.QueryInterface 来请求这个私有接口,若能获得接口就说明是属于扩展的接口。TFOLDERSETTINGS结构包含了视图的显示设置,主要的显示模式是大图标、小图标、列表或是详细信息,同时还有一个标志表示一系列的显示选项,如是否左对齐等。我们可以修改它,资源管理器调用IShellView.GetCurrentInfo方法来获得这个结构的最新信息。
IShellBrowser接口允许视图同资源管理器通信,因为没有其他获得这个接口的途径,所以视图必须保存它以便再次使用。特别是我们需要调用IShellBrowser.GetWindow来获得父视图窗口用来创建子窗口。在保存了接口指针后,别忘了调用IShellBrowser.AddRef来增加接口引用记数。当不需要接口时,使用IShellBrowser.Release释放接口。
创建子窗口:
(1)检查 TFOLDERSETTINGS和
结构。
(2)调用IshellBrowser.GetWindow获得父视图窗口。创建子窗口并返回给资源管理器。
显示视图:视图窗口总是存在,即使没有获得焦点。因此,我们应该维护子窗口的如下三种状态:
① 激活并有焦点。设定对应焦点状态的菜单项和工具条项。
② 激活但没有焦点。设定对应无焦点状态的菜单项和工具条项。
③ 失活状态。视图将要被销毁,删除所有相关菜单项。
资源管理器通过调用IShellView.UIActivate方法来通知窗口状态的变化。反过来视图也应该用IShellBrowser.OnViewWindowActive方法通知管理器。当视图处于激活状态时,我们应该处理窗口消息,如WM_SIZE,它属于子窗口。同时还要处理与菜单和工具条对应的WM_COMMAND消息。当视图将要被销毁时,资源管理器会调用IShellView.DestroyViewWindow方法通知视图。
实现IShellView接口
1. AddPropertySheetPages方法
当用户选择资源管理器的工具菜单的文件夹选项时,会显示一个属性页允许用户修改文件夹选项。资源管理器调用
IShellView.AddPropertySheetPages方法允许添加属性页面到属性页上。
2. GetCurrentInfo方法
在切换视图前,资源管理器会调用
IShellView.GetCurrentInfo来请求当前TFOLDERSETTINGS值传递给下一个视图。
3. Refresh方法
资源管理器调用
IshellView.Refresh来刷新视图的显示。
4. SaveViewState方法
IShellView.SaveViewState方法提示视图保存它的外观状态,使视图在下次显示时可以恢复状态。通过调用IShellBrowser.GetViewStateStream方法返回一个IStream接口,视图可以利用这个接口保存状态。
5. TranslateAcelerator方法
当用户按下快捷键时,资源管理器会调用
IShellView.TranslateAccelerator 方法来传递消息给视图。如果视图返回 S_FALSE,资源管理器就处理这个消息。若视图处理了这个消息,视图就返回S_OK。当视图有焦点时,资源管理器调用 IShellView::TranslateAccelerator后,若视图没处理这个消息,资源管理器就处理它。如果视图没有焦点,资源管理器就先处理消息,若它没能处理的话,会调用IShellBrowser.TranslateAcceleratorSB。使用IshellBrowser接口同资源管理器通信。 IShellBrowser接口用于以下方面:
(1)修改资源管理器的菜单。
(2)修改资源管理器的工具条。
(3)修改资源管理器的状态条。
(4)储存外观信息,如当前设置或状态。
修改资源管理器的菜单
可以使用
接口来修改、添加或删除菜单及其相关联的命令。每次视图状态改变时,资源管理器会调用 IShellView.UIActivate,因此应该把修改操作放到这里来做,基本步骤是:
(1)创建菜单句柄。
表2.2
| 标 识 | 组 | |
文件 | FCIDM_MENU_FILE | File | |
编辑 | FCIDM_MENU_EDIT | ||
查看 | FCIDM_MENU_VIEW | Container | |
收藏 | FCIDM_MENU_FAVORITES | ||
工具 | FCIDM_MENU_TOOLS | ||
帮助 | FCIDM_MENU_HELP | Window |
(2)调用
IShellBrowser.InsertMenusSB方法,资源管理器会添加适当的菜单信息。
(3)修改返回的菜单信息。
(4)调用
IShellBrowser.Set- MenuSB方法让资源管理器显示修改后的菜单。
资源管理器有6个菜单。资源管理器菜单条被分成6组:File、Edit、Container、Object、Window和Help。表2.2列示了各菜单的标识和分组。
当调用
IShellBrowser. InsertMenusSB方法时,还必须传递一个指向
TOLEMENUGROUPWIDTHS的结构,成员都被初始化为0。调用完
方法后,就可以使用返回的菜单句柄进行普通菜单操作。
(1)添加菜单项。
(2)修改或删除已有的菜单项。
(3)添加新的菜单。
注意为了避免和资源管理器的命令冲突,被添加的命令标识必须处在 FCIDM_SHVIEWFIRST和FCIDM_SHVIEWLAST之间。当资源管理器调用IShellView.UIActivate方法来表明视图失活时,可调用IShellBrowser.Remove- MenusSB方法来恢复最初状态。
修改工具条
步骤是:
(1)添加按钮的位图到工具条的图像列表里。
(2)定义按钮的显示字符串。
(3)添加按钮到工具条。
调用
IShellBrowser.SendControlMsg方法给工具条发送
TB_ADDBITMAP消息。设定参数ID为FCW_TOOLBAR,设定wParam为位图中的按钮图像数,lParam为
TTBADDBITMAP结构的地址。图像索引在pret参数里返回。
有两种方法设定按钮的显示字符串:
(1)设定
TTBBUTTON结构的iString成员。
(2)调用IShellBrowser.SendControlMsg发送TB_ADDSTRING消息。wParam参数为0,lParam参数指向字符串。字符串索引由pret返回。
添加按钮,要先填写TTBBUTTON结构然后调用IShellBrowser. SetToolbarItems。
修改资源管理器状态条
两种使用方法是:
(1)用IShellBrowser.SetStatusTextSB方法来显示字符串。
(2)用IShellBrowser.SendControlMsg方法直接发消息。
实现文件夹接口
1. 注册扩展
下面几个键值只对包含子目录的命名空间扩展有意义,同时这些值并不能用于那些子目录是文件系统目录的扩展。要想改变有子目录的扩展的行为,应添加下列键值到扩展的CLSID子键下:
WantsFORPARSING:有子目录的扩展的解析名通常有如下形式::{GUID}。 这种扩展通常包括虚拟的子对象。然而,某些扩展,比如我的文档,是完全对应于文件系统目录的。如果我们的扩展只是对现有系统文件夹的重新定义和扩充,可以先设定WantsFORPARSING 值,资源管理器将会通过调用扩展根目录对象的IShellFolder.GetDisplayNameOf 方法来请求根目录对象解析名称,其中参数uFlags会被设定为SHGDN_FORPARSING,而pidl 参数被设定为一个只包含一个终结符的空的PIDL。
HideFolderVerbs:在HKEY_CLASSES_ROOT\Folder 子键下定义的verbs通常同所有的扩展关联。它们出现在扩展的上下文相关菜单中,并可以通过
ShellExecute调用,要想禁止这些Verbs同我们的扩展关联,需要设定HideFolderVerbs值。
HideAsDelete:如果一个用户试图删除我们的扩展,资源管理器将会隐藏这个扩展。
HideAsDeletePerUser:这个值同HideAsDelete有相同的效果,但它是基于单一用户的,扩展将会只对试图删除该扩展的用户隐藏,而对其他用户依然显示。
QueryForOverlay:设定这个值表示根目录的图标拥有掩码重叠图标。同时,这要求文件夹对象必须支持
IShellIconOverlay接口。在资源管理器显示根目录的图标前,它会通过调用IShellIconOverlay接口的两个方法来请求一个重叠图标。
下面这些键值适用于所有的命名空间扩展:
(1)要想指定扩展的节点文件夹的显示名称,设定扩展的CLSID子键缺省值为名称字符串。
(2)当光标在文件夹上停留时,应该显示一个飞跃信息提示来描述文件夹的内容。要想为扩展的根目录提供飞跃信息提示的话,在扩展的CLSID的子键下创建一个字符串类型的InfoTip值,并赋值给它想要显示的信息字符串。
(3)要想为扩展的根目录指定一个定制的图标的话,要在扩展的CLSID子键下创建DefaultIcon子键。设定DefaultIcon子键的缺省值为包含图标的文件名的字符串,同时字符串中还应该用逗号隔开要使用的图标在文件中的索引值(以0为底的)。
(4)缺省时,扩展根目录对应的上下文相关菜单将会包括定义在HKEY_CLASSES_ ROOT\Folder主键下的菜单项。同时外壳还会根据扩展的SFGAO_XXX标志决定是否添加删除、重命名、和属性菜单项。如果还想在菜单中添加或重载已有菜单项,就同扩展文件类的上下文相关菜单一样,需要在扩展的CLSID子键下建立一个Shell子键并定义相应的命令。(
Extending Context Menus)。
(5)如果需要一种更灵活的定制根目录右键菜单的方式,可以实现一个上下文相关菜单扩展。为了注册这个菜单扩展,需要在扩展的CLSID子键下创建ShellEx 子键,并像注册其他扩展一样注册菜单扩展(shell extension handler)。
(6)要想为扩展的根目录用右键菜单的属性命令调出的属性页上添加一个新的属性页面的话,需要在设定文件夹的SFGAO_HASPROPSHEET 属性的同时实现一个属性页扩展。并像上面的上下文相关菜单扩展一样注册属性页扩展。
(7)要想指定根目录的属性,需要添加在扩展的CLSID子键下添加ShellFolder 子键,并创建一个Attributes值,设定它为合适的SFGAO_XXX 标识的组合。
表2.3是一些常用的属性标识 :
表2.3
属性标识 | 值 | 描 述 |
SFGAO_FOLDER | 0x20000000 | 扩展根目录包括一个或多个项 |
SFGAO_HASSUBFOLDER | 0x80000000 | 扩展根目录包括一个或多个子目录 |
SFGAO_CANDELETE | 0x00000020 | 扩展目录可以被用户删除。目录的上下文相关菜单有一个删除菜单项 This flag should be set for junction points that are placed under one of the virtual folders |
SFGAO_CANRENAME | 0x00000010 | 扩展根目录可以被改名 |
SFGAO_HASPROPSHEET | 0x00000040 | 根目录有属性页,但必须实现一个属性页扩展 |
下面的例子显示了如何注册命名空间扩展:
HKEY_CLASSES_ROOT
CLSID
{Extension CLSID}=demo
InfoTip=演示
InProcServer32=c:\Namespace\demo.dll
ThreadingModel=Apartment
ShellFolder
Attributes=0xA00000020
//属性包括SFGAO_FOLDER, SFGAOHASSUBFOLDER,SFGAO_CANDELETE标志
DefaultIcon=c:\Namespace\demo.dll,1
2. 处理PIDL
每一个命名空间的项必须有一个唯一的标识符 PIDL。
注意:为PIDL设计一个数据结构的最重要的方面就是确保结构是可持续的和可传输的,意义在于:
(1)可持续性:系统经常会把PIDL进行长期储存,比如放在快捷方式文件中。储存一段时间后,自然需要从储存中恢复PIDL,从储存中恢复的PIDL对于我们的扩展必须还应该是有效的。这就意味着在PIDL结构中不能使用指针或句柄。这类数据通常总是发生变化的。
(2)可传输性:当一个PIDL从一台机器转移到另一台机器时,仍然要保证它有意义。比如一个PIDL可以被写到一个快捷方式文件中,复制到软盘中,然后再安装到其他机器上,如果这台机器上也安装了我们的扩展,那么被复制的快捷方式文件应该继续有效。为了使PIDL可传输,应该在PIDL中使用ANSI或Unicode字符串,同时要注意,在一台运行着Unicode版本的扩展所建立的PIDL将无法被Ansi版本的扩展所读取。
下面是一个简单的PIDL数据结构设计:
Type
TPIDLDemp= record
Cb:integer;
DwType:Dword;
WszDisplayName:array [0..39] of char;
End;
cb参数是用来指定数据结构大小的,这确保了TPIDLDemo结构成为一个有效的
SHITEMID结构。结构中剩下的部分等效于SHITEMID结构中的abID 参数,用于保存私有数据。DwType参数是一个扩展定义的变量用于表示外壳对象类型,比如,dwType如果为True,可以用来表示文件夹,而用False来表示其他的外壳对象。WszDisplayName用于保存外壳对象的显示名称。注意这里不能为目录中不同的对象赋予同样的名称,同时由于名称不同,wszDisplayName完全可以作为对象ID。这里wszDisplayName长度设定为40是为了确保
结构是双字对齐的。为了限制PIDL的尺寸,我们当然也可以使用变长的字符数组,要想双字对齐,只须通过在显示字符串后添加足够的'\0'字符就可以了。除此以外,还可以在结构中包括对象尺寸、属性等其他自定义的参数。
实现基本接口
1. IPersistFolder接口
IPersistFolder.Initialize方法需要给新的对象提供一个合格的PIDL,我们可能需要储存PIDL为了以后使用,文件夹对象必须使用这个PIDL来为它的子对象创建合格的PIDL。文件夹对象也可以调用
IPersistFolder.GetClassID来请求对象类标识符。通常,文件夹对象的创建和初始化是通过父目录的
IShellFolder.BindToObject方法实现的。当用户浏览进我们的扩展后,资源浏览器会创建并初始化扩展的根目录对象,根目录对象通过
方法获得的PIDL应该包括从桌面到扩展部分的路径,以便我们的扩展可以构造完全的PIDL。
2. IShellFolder接口
资源管理器可以通过多种途径获得我们扩展的CLSID。获得CLSID后,资源管理器会使用CLSID来创建和初始化根目录对象的一个实例,并查询
IShellFolder接口。我们的扩展这时需要创建一个根目录对象并返回对象的IShellFolder 接口。资源管理器同扩展的交互依赖于IShellFolder接口。 资源管理器使用IShellFolder用来:
(1) 请求一个对象来枚举根目录的内容 。
(2) 获得根目录内容的各种信息 。
(3) 请求其他可选接口,这些接口可以用来获得额外的信息,如图标或右键菜单。
(4) 请求一个目录对象代表根文件夹的一个子文件夹。
接口方法的实现
1. EnumObjects方法
资源管理器通过调用
IShellFolder.EnumObjects方法来确定文件夹包括的内容。这个方法创建了一个标准枚举对象提供了
IEnumIDList接口。IEnumIDList 接口使资源管理器获得文件夹包含的全部对象的PIDL,PIDL然后可以用来获得这些对象的信息。
注意
IEnumIDList.Next方法应该返回相对于父目录的PIDL。PIDL应该仅包含对象的
TSHITEMID结构,并有一个结束符。
2. CreateViewObject方法
资源管理器调用CreateViewObject方法来获得
接口,这个接口是用来管理视图的。
CreateViewObject还可以用来获得可选接口如
IContextMenu。如果资源管理器想获得目录下对象的可选接口,需要调用IShellFolder.GetUIObjectOf方法。
3. GetUIObjectOf方法
资源管理器GetUIObjectOf 方法来获得对象的额外信息,如图标和右键菜单。
4. BindToObject方法
BindToObject方法被调用,当用户打开扩展的子目录时。如果参数riid=IID_IShellFolder,应该创建和初始化一个子目录的文件夹对象并返回一个
5. GetDisplayNameOf方法
GetDisplayNameOf方法是用来转换PIDL成为可显示的名称字符串。PIDL必须是相对于对象的父目录的。换句话说,它必须包含一个非空的
结构。因为有多种命名对象的方式,资源管理器通过在uFlags参数中定义
SHGNO标识的组合来表示名称类型。SHGDN_NORMAL或SHGDN_INFOLDER将被用来指定名称是相对于文件夹的还是相对于桌面的。其他三个值SHGDN_FOREDITING、SHGDN_FORADDRESSBAR和SHGDN_FORPARSING可以用来指定名称的用途。
名称必须按
STRRET的结构形式返回,如果SHGDN_FOREDITING、SHGDN_FORADDRESSBAR和 SHGDN_FORPARSING没有设定,就返回外壳对象的显示名称。如果设定了SHGDN_FORPARSING 标识,资源管理器就会请求一个解析名称,解析名称可以被
IShellFolder.ParseDisplayName方法调用来获得对象的PIDL,即便对象在目录树中处于当前目录下一层或更多层。例如,对于文件对象来说,它的解析名就是它的路径,我们用文件系统对象的完全路径名来调用桌面的IshellFolder接口的ParseDisplayName 方法,它会返回这个对象的完全PIDL。
因为解析名都是文本字符串,所以没必要包括显示名。解析名的设计可以基于使
方法调用效率更高。比如很多外壳虚拟文件夹不是文件系统的一部分,并没有完全的路径名,每个文件夹的解析名通常都是采用一个GUID和解析名结合的方式,格式示意如下:
::{GUID}
6. GetAttributesOf方法
资源管理器调用IShellFolder.GetAttributesOf方法来确定文件夹下项目的属性,参数cidl给出被查询的项目数,参数apidl 指向对应的PIDL链表。
因为检验某些属性是非常耗时的,资源管理器通常通过设定rfgInOut参数来限制查询范围,应该只检验那些标识定义在rfgInOut参数中的属性。
注意:外壳对象的属性必须正确设置以便正确显示,比如如果一个文件夹包括子目录,就必须设定SFGAO_HASSUBFOLDERS 标识。这时,资源管理器就会在树视图中的文件夹图标前加上一个+号图标。
7. ParseDisplayName方法
方法就相当于
IShellFolder. GetDisplayNameOf方法的逆操作。它主要被用于转化外壳对象的解析名为相关联的PIDL。返回的PIDL是相对于暴露接口的文件夹的。要想获得完全的PIDL,调用者还需要把这个PIDL附在暴露接口的文件夹的PIDL后面。
方法还可以用来请求外壳对象的属性,因为确定所有的属性非常耗时,我们同样需要通过设定
SFGAO_XXX标识来限定感兴趣的信息。
IEnumIDList接口
当资源管理器需要枚举文件夹包含的外壳对象时,它会调用
IShellFolder. EnumObjects方法,文件夹对象必须创建一个枚举对象来暴露
接口并返回接口指针。
是一个标准的OLE枚举接口,很容易实现,但要注意的是返回的PIDL必须是相对于文件夹的,并且只包含一个
结构和终止符。
实现其他任选的接口
除了上面那些基本的接口外,还可以实现相当多的任选的外壳接口,比如such as
IExtractIcon接口可以用来定制视图的图标,还比如
IDataObject接口可以用来支持拖放特性。
上面这些接口不是由文件夹对象直接暴露出来的,而是资源管理器通过调用下面两个IShellFolder方法来请求的:
资源管理器调用文件夹对象的
IShellFolder.GetUIObjectOf方法来请求文件夹包含的对象的接口。
资源管理器通过调用文件夹对象的
IShellFolder.CreateViewObject方法来请求文件夹本身的接口。
下面将讨论最常用的任选接口:
1. IExtractIcon接口
资源管理器会在它显示文件夹内容之前请求一个
接口,这个接口允许扩展定制文件夹中包含的对象的图标,否则标准的文件和文件夹图标就会被使用。实现IExtractIcon 接口的具体细节参见MSDN。
2. IContextMenu接口
当用户在外壳对象上点击右键时,资源管理器会请求
接口,扩展实现
接口的细节参见MSDN。
3. IQueryInfo接口
IQueryInfo接口来获得信息飞跃提示字符串,实现细节参见MSDN。
4. IDataObject 和IDropTarget 接口
对于扩展来说没有直接的方法从资源管理器中获知用户是否执行了删除、复制或正在拖放一个对象。但每当有这类操作发生时,资源管理器会请求一个
接口,要想允许对象操作,就要创建一个数据对象并返回它的IDataObject接口指针。
当用户试图释放一个数据对象到扩展中的外壳对象上时,资源管理器会请求一个
IDropTarget接口,要想允许数据对象释放,需要创建一个对象暴露IDropTarget 接口并返回接口指针。具体实现可参考前面关于基于COM的拖放技术的讨论。
命名空间扩展两个主要的组成部分是文件夹对象和视图COM对象,其中文件夹对象至少要实现IUnknown、IShellExtInit、IShellFolder 及IPersistFolder 接口。而视图对象至少要实现IUnknown、IShellExtInit 和 IShellView接口。在文件夹对象创建后,外壳会通过IPersistFolder接口通知它在命名空间中的位置,也就是它的Item Identifier List。
文件夹对象同其下的子外壳对象交互
我们将命名空间扩展添加到系统中后,用户就可以控制扩展中显示的内容。或者展开左侧面板中的文件夹,或者点中文件夹,资源管理器会在右侧显示面板中显示其包含的子对象。
当用户点中文件夹对象的”+”字号后直接双击文件夹对象时,资源管理器标准的行为是显示被选定的文件夹的子目录。这是通过调用文件夹对象的IShellFolder接口的EnumObjects 方法来实现的。当用户单击文件夹时,资源管理器会显示文件夹包含的对象的视图,扩展在显示视图前必须做两件事情:
创建文件夹对象,并调用对象的IShellFolder接口的BindToObject方法进行绑定。
创建视图对象,一旦资源管理器创建完文件夹对象后,它就会调用对象的IShellFolder接口的CreateViewObject 方法。
文件夹对象同视图的交互
图2.5 |
一旦视图对象被创建,必须确定创建的视图类型。一种是显示文件夹中外壳对象项目的弹出式窗口。这类视图,可以通过文件夹右键菜单的打开命令调出,如图2.5所示。
另一种是当用户双击左侧面板的文件夹后,内容缺省时会显示在右侧面板中。文件夹对象创建视图窗口是通过调用视图对象的IshellView接口的CreateViewWindow 方法来实现的。视图对象必须实现CreateViewWindow方法来确定创建哪类视图。
还要注意的关键一点是对应于一个文件夹对象,系统中可以同时存在多个视图对象,可以打开任意多个资源管理器和视图窗口。因此,视图和文件夹对象必须实现为相互独立的COM对象。至于同步不同视图窗口的显示内容则由资源管理器负责处理。
接下来,就具体研究一下如何实现扩展,我们将建立一个非常简单的扩展,它的唯一功能就是在视图中显示各类文件的内容。下面是例子项目中各个单元的说明:
ShellFolder.pas: 实现了文件夹对象。
ShellView.pas: 实现了视图对象。
ViewForm.pas: 实现了显示在右侧面板中的窗体。
下面是文件夹COM对象的具体实现代码:
unit ShellFolder;
interface
uses Windows, ActiveX, ComObj, ComServ, ShlObj, ShellView;
const
CLSID_RADFindBrowser: TGUID = '{23CE4E06-73A7-11D0-BC62-00A0243ABE0B}';
type
TShellFolderImpl = class(TComObject, IShellFolder, IPersistFolder)
protected
function IPersistFolder.Initialize = IPersistFolder_Initialize;
public
// IShellFolder
function ParseDisplayName(hwndOwner: HWND;
pbcReserved: Pointer; lpszDisplayName: POLESTR; out pchEaten: ULONG;
out ppidl: PItemIDList; var dwAttributes: ULONG): HResult; stdcall;
function EnumObjects(hwndOwner: HWND; grfFlags: DWORD;
out EnumIDList: IEnumIDList): HResult; stdcall;
function BindToObject(pidl: PItemIDList; pbcReserved: Pointer;
const riid: TIID; out ppvOut): HResult; stdcall;
function BindToStorage(pidl: PItemIDList; pbcReserved: Pointer;
const riid: TIID; out ppvObj): HResult; stdcall;
function CompareIDs(lParam: LPARAM;
pidl1, pidl2: PItemIDList): HResult; stdcall;
function CreateViewObject(hwndOwner: HWND; const riid: TIID;
out ppvOut): HResult; stdcall;
function GetAttributesOf(cidl: UINT; var apidl: PItemIDList;
var rgfInOut: UINT): HResult; stdcall;
function GetUIObjectOf(hwndOwner: HWND; cidl: UINT; var apidl: PItemIDList;
const riid: TIID; prgfInOut: Pointer; out ppvOut): HResult; stdcall;
function GetDisplayNameOf(pidl: PItemIDList; uFlags: DWORD;
var lpName: TStrRet): HResult; stdcall;
function SetNameOf(hwndOwner: HWND; pidl: PItemIDList; lpszName: POLEStr;
uFlags: DWORD; var ppidlOut: PItemIDList): HResult; stdcall;
// IPersist
function GetClassID(out classID: TCLSID): HResult; stdcall;
// IPersistFolder
function IPersistFolder_Initialize(pidl: PItemIDList): HResult; virtual;
stdcall;
end;
implementation
uses
RegisterExtension;
function TShellFolderImpl.ParseDisplayName(hwndOwner: HWND;
pbcReserved: Pointer; lpszDisplayName: POLESTR; out pchEaten: ULONG;
out ppidl: PItemIDList; var dwAttributes: ULONG): HResult;
begin
MessageBox(0, 'TShellFolderImpl.ParseDisplayName', nil, 0);
Result := E_NOTIMPL;
function TShellFolderImpl.EnumObjects(hwndOwner: HWND; grfFlags: DWORD;
out EnumIDList: IEnumIDList): HResult;
MessageBox(0, 'TShellFolderImpl.EnumObjects', nil, 0);
function TShellFolderImpl.CompareIDs(lParam: LPARAM;
pidl1, pidl2: PItemIDList): HResult;
MessageBox(0, 'TShellFolderImpl.CompareIDs', nil, 0);
function TShellFolderImpl.GetAttributesOf(cidl: UINT; var apidl: PItemIDList;
var rgfInOut: UINT): HResult;
MessageBox(0, 'TShellFolderImpl.GetAttributesOf', nil, 0);
function TShellFolderImpl.GetDisplayNameOf(pidl: PItemIDList; uFlags: DWORD;
var lpName: TStrRet): HResult;
MessageBox(0, 'TShellFolderImpl.GetDisplayNameOf', nil, 0);
function TShellFolderImpl.SetNameOf(hwndOwner: HWND; pidl: PItemIDList;
lpszName: POLEStr;
uFlags: DWORD; var ppidlOut: PItemIDList): HResult;
MessageBox(0, 'TShellFolderImpl.SetNameOf', nil, 0);
{ IPersistFolder }
function TShellFolderImpl.GetClassID(out classID: TCLSID): HResult;
classID := CLSID_RADFindBrowser;
MessageBox(0, 'TShellFolderImpl.GetClassID', nil, 0);
Result := NOERROR;
function TShellFolderImpl.IPersistFolder_Initialize(pidl: PItemIDList): HResult;
function TShellFolderImpl.BindToObject(pidl: PItemIDList;
pbcReserved: Pointer; const riid: TIID; out ppvOut): HResult;
MessageBox(0, 'TShellFolderImpl.BindToObject', nil, 0);
function TShellFolderImpl.BindToStorage(pidl: PItemIDList;
pbcReserved: Pointer; const riid: TIID; out ppvObj): HResult;
MessageBox(0, 'TShellFolderImpl.BindToStorage', nil, 0);
// 当IpersistFolder.Initialize方法调用完后,外壳就会调用这个方法,我们必须构建一个新的窗口来显示视图对象
function TShellFolderImpl.CreateViewObject(hwndOwner: HWND;
const riid: TIID; out ppvOut): HResult;
var
shellView: IShellView;
try
if IsEqualGUID(riid, IShellView) then
begin
ShellView := TShellViewImpl.Create;
Result := (ShellView as IUnknown).QueryInterface(riid, ppvOut)
end
else
Result := E_NOINTERFACE;
except
on E: EOleSysError do
Result := E.ErrorCode;
else
Result := E_UNEXPECTED;
end;
function TShellFolderImpl.GetUIObjectOf(hwndOwner: HWND; cidl: UINT;
var apidl: PItemIDList; const riid: TIID; prgfInOut: Pointer;
out ppvOut): HResult;
MessageBox( 0, 'TShellFolderImpl.GetUIObjectOf', nil, 0 );
initialization
TNamespaceExtensionFactory.Create(ComServer, TShellFolderImpl,
CLSID_RADFindBrowser,
'', 'Delphi RADFind Explorer Extension', ciMultiInstance)
end.
在上面代码中,对于IShellFolder接口的大部分方法都没有实现,只是给出了像下面这样一个空的实现:
Result := E_NOTIMPL;//没有实现这个方法
但我们必须实现一个重要的方法,这就是CreateViewObject方法,在此方法中先要创建视图对象的一个实例,然后返回被资源管理器请求的接口。过程非常简单,只用下面5行代码就可以实现:
If IsEqualGUID(riid, IShellView) then
begin
ShellView := TShellViewImpl.Create;//创建视图对象实例返回被请求的接口
Result := (ShellView as IUnknown).QueryInterface(riid, ppvOut)、、
End
除此以外还可以通过使用类工厂来实现同上面代码完全一样的功能。代码实现如下:
// 获得类工厂
Factory := ComClassManager.GetFactoryFromClassID( CLSID_RADFindView );
if Factory <> nil then
FObject := Factory.CreateComObject( nil );
if FObject <> nil then
begin
// 请求接口
FObject.ObjAddRef;
Result := FObject.ObjQueryInterface( riid, ppvOut );
FObject.ObjRelease;
unit ShellView;
interface
uses Windows, ActiveX, CommCtrl, ShellAPI, ShlObj, ViewForm;
TShellViewImpl = class(TInterfacedObject, IShellView)
private
FFolderSettings: TFolderSettings;
FShellBrowser: IShellBrowser;
FHWndParent: HWND;
FForm: TView;
constructor Create;
// IOleWindow Methods
function GetWindow(out wnd: HWnd): HResult; stdcall;
function ContextSensitiveHelp(fEnterMode: BOOL): HResult; stdcall;
// IShellView Methods
function TranslateAccelerator(var Msg: TMsg): HResult; stdcall;
function EnableModeless(Enable: Boolean): HResult; stdcall;
function UIActivate(State: UINT): HResult; stdcall;
function Refresh: HResult; stdcall;
function CreateViewWindow(PrevView: IShellView;
var FolderSettings: TFolderSettings; ShellBrowser: IShellBrowser;
var Rect: TRect; out Wnd: HWND): HResult; stdcall;
function DestroyViewWindow: HResult; stdcall;
function GetCurrentInfo(out FolderSettings: TFolderSettings): HResult;
function AddPropertySheetPages(Reseved: DWORD;
var lpfnAddPage: TFNAddPropSheetPage; lParam: LPARAM): HResult; stdcall;
function SaveViewState: HResult; stdcall;
function SelectItem(pidl: PItemIDList; flags: UINT): HResult; stdcall;
function GetItemObject(Item: UINT; const iid: TIID; var IPtr: Pointer):
HResult; stdcall;
property ShellBrowser: IShellBrowser read FShellBrowser;
RegisterExtension, Classes;
constructor TShellViewImpl.Create;
inherited Create;
FForm := nil;
FShellBrowser := nil;
////////////////////////////////////////////////////////////////////////////////
// IOleWindow Implementation
function TShellViewImpl.GetWindow(out wnd: HWnd): HResult; stdcall;
FShellBrowser.SetStatusTextSB(StringToOleStr('IOleWindow.GetWindow'));
Wnd := FForm.Handle;
function TShellViewImpl.ContextSensitiveHelp(fEnterMode: BOOL): HResult;
stdcall;
FShellBrowser.SetStatusTextSB(StringToOleStr('IOleWindow.ContextSensitiveHelp'));
// IShellView Implementation
function TShellViewImpl.TranslateAccelerator(var Msg: TMsg): HResult; stdcall;
function TShellViewImpl.EnableModeless(Enable: Boolean): HResult; stdcall;
FShellBrowser.SetStatusTextSB(StringToOleStr('IShellView.EnableModeless'));
function TShellViewImpl.UIActivate(State: UINT): HResult; stdcall;
S: string;
case TSVUIAEnums(State) of
SVUIA_DEACTIVATE:
S := 'Deactivate view';
SVUIA_ACTIVATE_NOFOCUS:
S := 'Activate view without focus';
SVUIA_ACTIVATE_FOCUS:
S := 'Activate view with focus';
SVUIA_INPLACEACTIVATE:
S := 'Activate view for inplace-activation within ActiveX control';
FShellBrowser.SetStatusTextSB(StringToOleStr('IShellView.UIActivate: ' + S));
function TShellViewImpl.Refresh: HResult; stdcall;
FShellBrowser.SetStatusTextSB(StringToOleStr('IShellView.Refresh'));
function TShellViewImpl.CreateViewWindow(PrevView: IShellView;
var FolderSettings: TFolderSettings; ShellBrowser: IShellBrowser;
var Rect: TRect; out Wnd: HWND): HResult; stdcall;
FFolderSettings := FolderSettings;
FShellBrowser := ShellBrowser;
FShellBrowser.GetWindow(FHWndParent);
FForm := TView.CreateShView(nil, FShellBrowser, Self as IShellView);
Wnd := FForm.Handle;
SetParent(Wnd, FHWndParent);
with FForm do
SetWindowPos(Handle, HWND_TOP, Rect.Left, Rect.Top,
Rect.Right - Rect.Left, Rect.Bottom - Rect.Top, SWP_SHOWWINDOW);
Show;
end;
if Wnd <> 0 then
Result := NOERROR
Result := E_UNEXPECTED;
except
Result := E_UNEXPECTED;
function TShellViewImpl.DestroyViewWindow: HResult; stdcall;
FShellBrowser.SetStatusTextSB(StringToOleStr('IShellView.DestroyViewWin-dow'));
FForm.Free;
function TShellViewImpl.GetCurrentInfo(out FolderSettings: TFolderSettings):
HResult; stdcall;
FShellBrowser.SetStatusTextSB(StringToOleStr('IShellView.GetCurrent-Info'));
function TShellViewImpl.SaveViewState: HResult; stdcall;
FShellBrowser.SetStatusTextSB(StringToOleStr('IShellView.SaveViewState'));
function TShellViewImpl.SelectItem(pidl: PItemIDList; flags: UINT): HResult;
FShellBrowser.SetStatusTextSB(StringToOleStr('IShellView.SelectItem'));
function TShellViewImpl.AddPropertySheetPages(Reseved: DWORD;
var lpfnAddPage: TFNAddPropSheetPage; lParam: LPARAM): HResult;
FShellBrowser.SetStatusTextSB(StringToOleStr('IShellView.AddPropertySheetPages'));
function TShellViewImpl.GetItemObject(Item: UINT; const iid: TIID;
var IPtr: Pointer): HResult;
FShellBrowser.SetStatusTextSB(StringToOleStr('IShellView.GetItemObject'));
接下来是视图对象的实现代码:
constructor TShellViewImpl.Create;
S := '视图失焦';
S := '激活视图,没有焦点';
S := '激活视图有焦点';
S := '激活视图的原位ActiveX激活';
// 一旦TShellViewImpl被创建,这个函数就会被调用来创建真正的视图窗口
var FolderSettings: TFolderSettings; ShellBrowser: IShellBrowser;
var Rect: TRect; out Wnd: HWND): HResult; stdcall;
// 保存文件夹设置
// 获得资源管理器父窗口句柄
FShellBrowser.GetWindow(FHWndParent);
// 创建窗体,传递文件夹和视图对象接口给窗体
通知外壳窗体什么时候获得焦点
Rect.Right - Rect.Left, Rect.Bottom - Rect.Top, SWP_SHOWWINDOW);
Begin
//在状态栏中显示信息
FShellBrowser.SetStatusTextSB(StringToOleStr('IShellView.GetCurrentInfo'));
end;
同文件夹的实现类似,这里只实现了IShellView接口的CreateViewWindow 方法,这个方法很简单,唯一要说的是在方法中用下面的代码将窗体设定为资源管理器的子窗体,这样资源管理器可以刷新和重定义窗体的大小。
Wnd := FForm.Handle;
SetParent(Wnd, FHWndParent);
我们的窗体的功能主要是在刚开始启动时显示系统日志,并且可以在richEdit中显示任意其他文件的内容,实现代码如下,重要的部分都添加了注释:
constructor TView.Create(AOwner: TComponent);
ShowMessage('应该使用CreateSHView来创建视图对象');
Abort; // 安静的异常
(对于窗体来说它必须是一个子窗体)
constructor TView.CreateSHView(AOwner: TComponent; SHBrowser: IShellBrowser; SHView: IShellView);
FSHBrowser := nil;
FShellView := nil;
inherited Create(AOwner);
FSHBrowser := SHBrowser;
FShellView := SHView;
//增加引用记数
FSHBrowser._AddRef;
FShellView._AddRef;
SetWindowLong(Handle, GWL_STYLE, WS_CHILD);// 必须是子窗体
destructor TView.Destroy;
if Assigned(FSHBrowser) then
FSHBrowser._Release; // 减少引用记数
if Assigned(FShellView) then
FShellView._Release;
finally
inherited Destroy;
end
(打开其他文件)
procedure TView.N1Click(Sender: TObject);
if OpenDialog1.Execute then
RichEdit1.Lines.LoadFromFile( OpenDialog1.FileName );
(我们必须在窗体获得焦点时调用OnViewWindowActive 回调函数
以便外壳调用UIActivate接口回调)
procedure TView.FormActivate(Sender: TObject);
FSHBrowser.OnViewWindowActive(FShellView);
RichEdit1.SetFocus;
(加载系统日志文本)
procedure TView.FormShow(Sender: TObject);
if FileExists('c:\BootLog.txt') then
RichEdit1.Lines.LoadFromFile('c:\BootLog.txt')
if FileExists('c:\config.sys') then
RichEdit1.Lines.LoadFromFile('c:\config.sys')
RichEdit1.Lines.Add('BootLog.txt or Config.sys not found')
最后就是注册创建好的扩展了,下面是其实现代码,这里给出了代码的注释,具体过程的意义参见前面的叙述:
unit RegisterExtension;
ComObj;
TNamespaceExtensionFactory = class(TComObjectFactory)
function GetProgID: string; override;
procedure UpdateRegistry(Register: Boolean); override;
Windows, SysUtils, ShellView, ShlObj;
(获得短文件名)
function GetShortPath(LongPath: ansiString): ansistring;
szShortPath,
szLongPath: array[0..MAX_PATH] of char;
PLen: Longint;
Result := LongPath;
StrPCopy(szLongPath, LongPath);
PLen := GetShortPathName(szLongPath, szShortPath, MAX_PATH);
if not ((PLen = 0) or (Plen > MAX_PATH)) then
Result := StrPas(szShortPath);
procedure CreateRegKeyEx(const Key, ValueName: string; Value: PChar;
Kind, Size: DWORD; RootKey: HKEY);
Handle: HKey;
Status,
Disposition: Integer;
Status := RegCreateKeyEx(RootKey, PChar(Key), 0, '',
REG_OPTION_NON_VOLATILE, KEY_READ or KEY_WRITE or KEY_SET_VALUE, nil,
Handle, @Disposition);
if Status = 0 then
Status := RegSetValueEx(Handle, PChar(ValueName), 0, Kind, Value, Size);
RegCloseKey(Handle);
if Status <> 0 then
raise EWin32Error.Create(SysErrorMessage(Status));
procedure DeleteRegValue(const Key, ValueName: string; RootKey: HKEY);
Handle: HKEY;
Status: Integer;
Status := RegOpenKey(RootKey, PChar(Key), Handle);
Status := RegDeleteValue(Handle, PChar(ValueName));
{ TNamespaceExtensionFactory }
function TNamespaceExtensionFactory.GetProgID: string;
Result := ''; {对于命名空间扩展来说不需要Progid}
procedure TNamespaceExtensionFactory.UpdateRegistry(Register: Boolean);
// 设定我们的扩展位于我的电脑下面
NamespaceKey='SOFTWARE\Microsoft\Windows\CurrentVersion\
Explorer\MyComputer\Namespace\';
// 在NT上需要设定的注册表项
ApproveKey='SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved\';
Temp,
ClsID: string;
Value: DWORD;
ClsID := GUIDToString(ClassID);
inherited UpdateRegistry(Register);
if Register then
Temp := GetShortPath(ComServer.ServerFileName);
CreateRegKey('CLSID\' + ClsID + '\' + ComServer.ServerKey, '', Temp);
if Win32Platform = VER_PLATFORM_WIN32_NT then
CreateRegKeyEx(ApproveKey, ClsId, PChar(Description), REG_SZ,
Length(Description) + 1, HKEY_LOCAL_MACHINE);
CreateRegKeyEx('CLSID\' + ClsId + '\InProcServer32\', 'ThreadingModel',
'Apartment'#0, REG_SZ, 10, HKEY_CLASSES_ROOT);
// 本扩展所属节点为‘我的电脑'
CreateRegKeyEx(NameSpaceKey + ClsId, '', PChar(Description), REG_SZ,
Length(Description) + 1, HKEY_LOCAL_MACHINE);
Value := SFGAO_FOLDER;// or SFGAO_HASSUBFOLDER;
CreateRegKeyEx('CLSID\' + ClsId + '\ShellFolder\', 'Attributes',
@Value, REG_BINARY, SizeOf(DWORD), HKEY_CLASSES_ROOT);
// 使用DLL的缺省图标
CreateRegKey('CLSID\' + ClsId + '\DefaultIcon', '', Temp + ',0');
else begin
// 删除节点
RegDeleteKey(HKEY_LOCAL_MACHINE, PChar(NameSpaceKey + ClsId));
if Win32Platform = VER_PLATFORM_WIN32_NT then
DeleteRegValue(ApproveKey, ClsId, HKEY_LOCAL_MACHINE);
图2.6 |
扩展的运行如图2.6所示。
结论
命名空间扩展是所有外壳扩展中最复杂的一种,因为它的实现涉及的接口非常多,同时各个接口之间的交互关系也非常复杂,微软提供了两个C++版本的例子,RegView和CabView,可以供读者参考。