插件结构的实现之原理篇

来源:互联网 发布:java程序设计 pdf 编辑:程序博客网 时间:2024/04/27 23:49
插件结构有助于编写有良好的扩充和定制功能的应用程序。例如,您可能想做一个有三个不同版本的软件(标准版、专业版和企业版),您不必写三套不同的代码,只需建立一个单独的主程序(host application),通过挂接插件实现三个不同的版本。这就是说,标准版=主程序+标准版插件;专业版=主程序+标准版插件+专业版插件;企业版=主程序+标准版插件+专业版插件+企业版插件。

另一个使用插件的好处是可以编制特定的功能模块挂接到您的主程序上面。这是近年来十分常见的一种技术,许多软件甚至操作系统或其外壳程序都有使用。例如,Win32的外壳程序资源管理器提供了大量的API和COM接口允许您编写自己的外壳扩展程序或者说外壳插件。

说到COM,它实际上是一个实现插件的极好的体系结构。您可以用COM建立一个包含有主程序和插件的框架,它们可能是用不同的编程语言写成(VB、Delphi、C++等等),但由于建立在COM之上,它们之间完全能够无缝地结合在一起。

下面,我们将会一步步地制作一个建立在COM之上的插件框架。

万能浏览器
言归正传,一个插件框架包括两个部分:主程序(host)和插件(plug-in)。主程序即是“包含”插件的程序。主程序公布一个标准接口,IHost,作为插件和主程序通信时用;同样,插件也公布一个标准接口 IPlugin,由主程序在与插件通信时调用。在这里,我们将要建立一个简单的程序叫做万能浏览器(universal Explorer)。这个浏览器可以用来查看任何有着层次结构的信息,如文件系统、数据库的主/从关系、组织结构图、家庭成员关系……等等。浏览器公布的宿主接口叫做IExplorer。

浏览器的插件模块知道如何在层次信息中浏览、导航。例如,我们可以建立一个文件系统插件来浏览我们电脑上的文件系统。每个万能浏览器的插件都要公布一个插件接口叫做IExplorable。

接口设计
我们希望每个插件都要能描述它自己并且还要有一个指向它的宿主程序的指针:

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_DEF



转自http://infos.edulife.com.cn/200412/2004125441.html