📄 插件实现.txt
字号:
IExplorable = interface
procedure SetExplorer(Explorer); // 由宿主程序调用设置IExplorable的指向宿主程序的指针
function GetDescription : string; // 对自身的描述
end;
对于层次结构上任何一个指定的结点(node),万能浏览器都需要能够显示它的下级结点。要有一种机制,可以得到一个结点的已结点的信息。举例来说,在一个文件系统里,如果C:\下面有5个文件,万能浏览器就要问插件是哪5个文件。IExplorable提供了第二个接口,ISubItems:
ISubItems = interface
function GetCount : integer; //返回一个指定结点的子结点数目
function GetItem (Index) : string; //返回每一个下级结点(索引从0开始)
end;
有了ISubItems,万能浏览器可以得到指定结点的下级结点个数和内容。这里,我们需要为IExplorable增加一个方法返回给定结点的ISubItems:
IExplorable = interface
procedure SetExplorer(Explorer);
function GetDescription : string;
function GetSubItems (Path) : ISubItems; //返回给定结点的ISubItems
end;
Path参数是指定结点的绝对路径。例如,GetSubItems ("c:\")将返回C:\的下级文件及文件夹;而GetSubItems ("c:\Level1\Level2\Level3") 返回c:\Level1\Level2\Level3的下级文件及文件夹。
同样,我们还想得到任一给定结点的结点属性。简单起见,我们只对能表示为“名/值”对的信息感兴趣。比如,对于文件系统中的一个结点,会有如下的属性:
属性名
属性值
文件类型
文件(或文件夹)
文件名
Filename.txt
文件大小
1234
文件日期
1/1/2000 12:00 AM
文件属性
存档、只读
为了实现上面的名/值列表,我们再加上一个GetProperties方法:
IExplorable = interface
procedure SetExplorer(Explorer);
function GetDescription : string;
function GetSubItems (Path) : ISubItems;
function GetProperties (Path) : Array; //返回给定结点的名/值对
end;
稍后您就会看到,我们将会以一个二维数组的形式实现前面看到的文件属性列表。
最后,我要为我们的万能浏览器增加一个小小的花样:一个上下文件相关的弹出式菜单。这样一来,万能浏览器就能在我们的插件支持的任何一个结点上实现任意自定义的操作。在本例中,我们允许用户在任一结点(文件或文件夹)上右击鼠标并通过弹出菜单为文件改名(Rename):
插图:一个包含Rename操作的弹出式菜单
执行一个自定义的上下文相关菜单命令包括两个步骤:1.在弹出菜单中显示待执行命令;2.实际执行用户从菜单中选中的命令。换句话说,用户右击鼠标之后,万能浏览器先从插件中取得对应于这一结点的弹出菜单命令列表就象下面的样子:
命令名
命令ID
Rename(改名)
1
Delete(删除)
2
View(查看)
3
...
...
根据上面的列表,万能浏览器在弹出菜单中显示这些命令。用户单击一个菜单项后,万能浏览器将告诉插件对选中的结点执行相应的命令(通过传递命令ID):
IExplorable = interface
procedure SetExplorer(Explorer);
function GetDescription : string;
function GetSubItems (Path) : ISubItems;
function GetProperties (Path) : Array;
function GetMenuActions (Path) : Array; //返回给定结点相关的弹出式菜单命令
function DoMenuAction (Path; ActionId); //在给定结点执行命令(ActionID)
end;
再次说明:万能浏览器调用GetMenuActions取得弹出式菜单命令列表,一旦用户选择了一个菜单项,它再调用DoMenuAction激活同菜单项对应的命令。
这就是IExplorable。让我们再来看一下如何实现主程序的接口,IExplorer。
为了简化问题,我为IExplorer增加了一个方法,用于在文件改名之后插件能够调用这个方法。
IExplorer = interface(IUnknown)
procedure RenamePath (OldPath; NewPath); //结点改名后由插件调用
end;
插件使用RenamePath通知万能浏览器它的一个结点的名字变了。这样,主程序有机会执行相应的操作,具体说来,万能浏览器可以具体实现RenamePath从而在用户界面上直观地反映出结点名字的变化。
组件目录(Component Categories)
设计插件结构时的一个最为常见的问题是“有没有一个标准机制能让主程序知道哪些插件可用?”每当主程序运行的时候,我们都面临这个难题。插件是否应该把自己的特性写进注册表里的某个分枝?或是一个INI文件?要么是一个普通的数据库文件?还是其它地方?
幸运的是,COM提供了一个标准机制,就是插件能够声明它的能力(即:它是哪种插件)而主程序能够声明它的需求(即:它想要什么类型的插件)。这种机制叫做组件目录。进一步举例说明:我们的万能浏览器只要支持IExplorable接口的插件。我们可把所有符合条件的插件放到一起并命名为“Explorable Plugins”。在COM里,当我们把一些对象按照某种共性编组,这个组就叫做组件目录。所以我们称所有上述插件的集合叫做 组件目录。
COM组件目录在注册表中放在(HCKR\Component Categories)。和接口、coClasses以及其它的COM机制一样,每个目录都有一个唯一GUID标识,又叫目录ID或CATID。讲到这里,您应该清楚我们需要一个"Explorable Plugins"的CATID。我已经定义了这个ID:
//Explorable Plugin的CATID
CATID_Explorable = "{5111C0AC-7397-11D3-A801-0000B4552A26}";
下一步是注册这个CATID。COM再一次帮了我们的忙。COM为管理组件目录提供了标准接口(及其实现)。ICatRegister,能够注册(及注销)组件目录;ICatInformation则能够取得已注册的组件目录的信息。
ICatInformation及ICatRegister都由一个COM的coclass实现,后者由CLSID_StdComponentCategoryMgr定义。如果想用ICatInformation,只需简单地请求ICatInformation创建这个coclass,同样,想用ICatRegister,就请求ICatRegister。
当往HKCR\Component Categories注册一个CATID时,有必要看一下ICatRegister.RegisterCategories:
ICatRegister = interface (IUnknown)
function RegisterCategories (
cCategories: UINT; //待注册的CATID个数
rgCategoryInfo: PCATEGORYINFO //待注册的目录信息数组
): HResult; stdcall;
... 其它方法略 ...
end;
rgCategoryInfo部分简单地指向目录信息(CATEGORYINFO)记录的数组:
TCATEGORYINFO = record
catid: TGUID; //目录ID
lcid: UINT; //多语言支持时用
szDescription: array[0..127] of WideChar; //目录描述
end;
在这里,我们只需填充一个目录信息到数组中:
//declare variable ExplorableCategoryInfo of type TCATEGORYINFO record
var ExplorableCategoryInfo : TCATEGORYINFO;
//初始化ExplorableCategoryInfo record
ExplorableCategoryInfo.catid = CATID_Explorable;
ExplorableCategoryInfo.lcid = LOCALE_SYSTEM_DEFAULT; //dummy
ExplorableCategoryInfo.szDescription = "Explorable Plugins";
这样,注册目录的步骤即简化为:
var CatReg : ICatRegister;
//创建标准组件目录管理器并请求ICatRegister
CatReg = GetStdComponentCategoryMgr (ICatRegister);
//注册CATID_Explorable目录
CatReg.RegisterCategories (1, ExplorableCategoryInfo); //仅注册一个CatInfo
现在,我们刚刚HKCR\Component Categories下注册了新的组件目录。下一步就是标记我们的插件实现(coclass)。要知道,主程序需要确定哪一个插件实现了一个具体的组件目录。假设我们的文件系统浏览插件由下面的CLSID定义的coclass实现:
CLSID_FileSystemExplorable = "{8B9A0689-7434-11D3-A802-0000B4552A26}";
为了宣布就是这个coclass实现了我们的“Explorable Plugins”组件目录,应在注册表中这样登记这一coclass:
HKCR\{8B9A0689-7434-11D3-A802-0000B4552A26} //CLSID_FileSystemExplorable
Implemented Categories //表明我们实现了某个组件目录
{5111C0AC-7397-11D3-A801-0000B4552A26} //表明我们实现了CATID_Explorable
... 其它的已实现的CATID ...
InprocServer32
...
您可能已经猜到:把这些项目放入注册表可以使用ICatRegister。具体说来,即是使用ICatRegister.RegisterClassImplCategories注册coclass的实现的目录信息:
ICatRegister = interface (IUnknown)
function RegisterClassImplCategories (
const rclsid: TGUID; //coclass的实现的目录的CLSID
cCategories: UINT; //这一coclass对应的待注册的目录的个数
rgcatid: Pointer //指向TCATEGORYINFO类型的包含待注册目录信息
): HResult; stdcall;
... 其它属性略 ...
end;
以下,可注册FileSystemExplorable coclass,目录名为“Explorable Plugins”:
//定义一个TCATEGORYINFO类型的记录变量
var ExplorableCategoryInfo : TCATEGORYINFO;
//初始化ExplorableCategoryInfo记录
ExplorableCategoryInfo.catid = CATID_Explorable;
ExplorableCategoryInfo.lcid = LOCALE_SYSTEM_DEFAULT; //没用
ExplorableCategoryInfo.szDescription = "Explorable Plugins";
var CatReg : ICatRegister;
//建立标准组件目录管理器
CatReg = GetStdComponentCategoryMgr (ICatRegister);
//为FileSystemExplorable coclass注册实现了的目录
CatReg.RegisterClassImplCategories (
CLSID_FileSystemExplorable, //FileSystemExplorable coclass
1, //实现目录的个数
ExplorableCategoryInfo //一个categoryinfo记录
);
同样,我们也能注销(从注册表中去除)一个coclass的实现的目录。这在我们想卸载插件的时候是很有用处的。我们用的是ICatRegister.UnRegisterClassImplCategories方法,它的用法与RegisterClassImplCategories一样,唯一的不同是它将会从注册表中的同样删除CLSID。
ICatRegister = interface (IUnknown)
function UnRegisterClassImplCategories (
const rclsid: TGUID; //实现目录的coclass的CLSID
cCategories: UINT; //实现目录的个数
rgcatid: Pointer //指向TCATEGORYINFO类型的包含待注册目录信息
): HResult; stdcall;
... 其它属性略 ...
end;
现在我们能够在注册表中声明每个coclass 并支持任何组件目录,我们的主程序只需简单地扫描注册表中的CLSID项目并检查每个已实现的目录的子键以知道在一个给定的目录中哪个coclass是符合条件的插件。您同样不必手工地检查注册表,找我们的老朋友ICatInformation。特别是ICatInformation能够给我们一个清单,列出所有支持一个给定CATID的coclass。使用ICatInformation.EnumClassessOfCategories方法:
ICatInformation = interface (IUnknown)
function EnumClassesOfCategories (
cImplemented: UINT; //我们感兴趣的已实现的目录的数目
rgcatidImpl: Pointer; //我们感兴趣的已实现的目录的数组
cRequired: UINT; //我们感兴趣的要求的目录的数目
rgcatidReq: Pointer; //我们感兴趣的要求的目录的数组
out ppenumClsid: IEnumGUID //返回符合条件的coclass的CLSID/GUID的枚举
): HResult; stdcall;
... 其它方法略 ...
end;
我们最为关注的参数是cImplemented、rgcatidImpl、和ppenumClsid。在本例中,我们只对一个目录有兴趣,所以cImplemented=1,而rgcatidImpl将会指向我们单个的TCATEGORYINFO记录。扫描成功后,EnumClassesOfCategories返回一个枚举在ppenumClsid中。我们接下来使用ppenumClsid取得每一个符合条件的插件coclass的列表。这个枚举器(enumerator )与其它的标准COM枚举器(IEnumXXX)有着相同的用法,所以您不必深究它的细节,只需看一下源码它是如何实现的。
除了前面讲的“已实现的目录”(“implemented categories”),一个插件同样可能将自己注册为要求主程序提供特定的接口(或目录),这样,一个主程序同样也能告知它能否主持一个对主程序有某种要求的插件。相关信息保存在每个插件的CLSID主键之下的另一个主键“Required Categories”下面。
虽然在本文中我不会演示如何实现"required categories",但知道在COM技术中还存在关这样一种机制仍然是很有利的。您可以到MSDN上找到更详细的文档。
迄今为此,我们可以注册组件目录,我们还可以注册我们的coclasss成为一个特殊的目录的实现,我们还能够枚举所有支持或实现某一给定目录的coclass。现在我们开始着手完成我们的万能浏览器以及文件系统管理插件(FileSystemExplorable plug-in)。
在开始之前,我还想说明几点:
1、我们的万能浏览器将是一个MDI程序,每一个MDI子窗口装载一个单独的浏览插件。每个浏览插件被提交到一个极其单纯的TreeView用户界面。
2、我们的文件系统浏览插件被做成了一个DLL服务器。插件一般就是这样实现的。另外,文件系统插件coclass成为一个轻量极的COM对象,而不是一个自动化对象。这只不过是因为在我们的演示里不需要自动化方面的功能。
3、插件接口IExplorable是基于IUnknown的(而不是我们更加熟悉的IDispatch)。在类库里,我用了[oleautomation]标记给IExplorable以成为标准的类库结构。之所以这么做是因为您将来遇到的大多数的插件结构都将由象上面这样的基于IUnknown的接口的插件构成,今后,当您为其它开发商制作的程序框架制作插件时您就会有所体会。
由于IExplorable是基于IUnknown的,每个方法的返回值都是一个HRESULT (这是一种良好的程序设计习惯)。所以在真正实现它的时候,方法的返回值实际上是由外部[out]参数返回的。
定义插件框架的接口
我们要做的第一件事就是在一个类库型(type library)中定义我们的插件框架的接口。我们在实现万能浏览器和浏览插件时将使用类型库的定义。为了做到这一点,我们选择菜单File|New然后从对话框的ActiveX页上选择Type Library,创建一个类型库并以Explorer.tlb的名字存盘。接着我们在Explorer.tlb中定义IExplorer(主程序接口),IExplorable (插件接口)以及ISubItems(IExplorable的辅助接口),这些,都同前面所讲的一样。当然,我是不会一步步地繁琐地给你讲操作步骤的。您可以从下载到的源代码中好好看一下最终完成的Explorer.tlb文件。接口完成后,我们单击Register Type Library图标注册做了的类型库准备用于COM。
注册完成。在下一篇中我们将以Delphi为例,开始具体编程的工作。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -