分类:编程手记 吴下阿蒙 (2001-11-30 21:57:00)
http://www.ccidnet.com/html/tech/guide/2001/09/29/58_3373.html
(文:Binh Ly 安富国编译 2001年09月29日 15:29)
经过前文所述的注册过程后,下面让我们以Delphi为例,开始具体的编程工作。
文件系统插件的实现
在Delphi里,为了创建一个in-process (DLL) COM服务器工程,我们从菜单File|New的对话框的ActiveX页选择ActiveX Library,取名“FileSystemPlugin”,Delphi将会生成一个FileSystemPlugin.dll文件做为我们的插件服务器。
然后要建立我们的插件coclass。选择File|New ,从对话框ActiveX页上选COM Object,在新对话框里,为它取名“FileSystemExplorable”,新的模块以FileSystemExplorable.pas的名字保存。
用Delphi 4/5生成,要确保取消“Include Type Library复选框的选中状态。这是因为我们只是简单地建立一个轻量级的COM对象,而不需要任何其它东西进入类型库。
经过前面的步骤,Delphi产生了一个轻量级的COM对象:
type
TFileSystemExplorable = class (TComObject)
end;
const
Class_FileSystemExplorable: TGUID = '{8B9A0689-7434-11D3-A802-0000B4552A26}';
因为我们正在建立一个插件,我们希望FileSystemExplorable实现IExplorable插件接口。为此,我们为TFileSystemExplorable 手工加上IExplorable:
type
TFileSystemExplorable = class(TComObject, IExplorable)
protected
//IExplorable 方法
function SetExplorer(const Explorer: IExplorer): HResult; stdcall;
function GetDescription(out Description: WideString): HResult; stdcall;
function GetSubItems(const Path: WideString; out SubItems: ISubItems): HResult; stdcall;
function GetMenuActions(const Path: WideString; out Actions: OleVariant): HResult; stdcall;
function DoMenuAction(const Path: WideString; ActionId: Integer): HResult; stdcall;
function GetProperties(const Path: WideString; out Properties: OleVariant): HResult; stdcall;
protected
FExplorer : IExplorer;
end;
接下来是实现它们。以SetExplorer and GetDescription为例:
function TFileSystemExplorable.SetExplorer (const Explorer: IExplorer): HResult;
begin
FExplorer := Explorer; //FExplorer是TFileSystemExplorable的成员
Result := S_OK;
end;
function TFileSystemExplorable.GetDescription (out Description: WideString): HResult;
begin
Description := 'File System (Delphi version)';
Result := S_OK;
end;
一点也不复杂,是不是?下面看看GetSubItems。
记住,GetSubItems将由万能浏览器调用以取得任意结点的子项(由ISubItems接口枚举)。这意味着我们要用函数FindFirst和FindNext扫描文件夹(由node/path指定)。我们把这些操作封装进TSubItems类:
type
TSubItems = class (TInterfacedObject)
protected
FSubItems : TStringList;
procedure LoadSubItems (const Path : string);
procedure LoadDrives;
procedure LoadFiles (Path : string);
public
constructor Create (const Path : string);
end;
constructor TSubItems.Create(const Path: string);
begin
inherited Create;
FSubItems := TStringList.Create;
LoadSubItems (Path); // 把所有Path下的文件和文件夹放入FSubItems
end;
... 以下部分略 ...
//根据给定的路径Path装入文件系统的一个分枝
procedure TSubItems.LoadSubItems(const Path: string);
begin
//reset list
FSubItems.Clear;
//如果Path是根,那么装入所有驱动器否则把Path做为文件夹装入
if (Path = '') then
LoadDrives
else
LoadFiles (Path);
end;
//把系统驱动器装入FSubItems列表
procedure TSubItems.LoadDrives;
begin
//以下是伪码实现
Find all drives;
For each drive found
Add drive name into FSubItems list;
end;
//把文件夹中的文件及子文件夹装入FSubItems列表
procedure TSubItems.LoadFiles(Path: string);
begin
//伪码实现
Find all files (and folders) under Path (using FindFirst/FindNext)
For each file (and folder) found
Add file name into FSubItems list;
end;
这里最重要的方法是LoadSubItems,如果Path为空,那么我们装入您系统中的所有驱动器,否则只需使用FindFirst与FindNext找齐指定路径下的所有文件。
由于FileSystemExplorable必须把ISubItems 交给万能浏览器,我们简单地TSubItems里实现ISubItems:
type
TSubItems = class (TInterfacedObject, ISubItems)
protected
//ISubItems方法
function GetCount(out Count: Integer): HResult; stdcall;
function GetItem(Index: Integer; out Item: WideString): HResult; stdcall;
protected
FSubItems : TStringList;
...
end;
//返回子项个数
function TSubItems.GetCount(out Count: Integer): HResult;
begin
Count := FSubItems.Count;
Result := S_OK;
end;
//返回对应Index的一个子项
function TSubItems.GetItem(Index: Integer; out Item: WideString): HResult;
begin
Item := FSubItems [Index];
Result := S_OK;
end;
最后,我们回到TFileSystemExplorable并实现GetSubItems:
function TFileSystemExplorable.GetSubItems(const Path: WideString;
out SubItems: ISubItems): HResult;
begin
//要求TSubItems返回一个指定path的子项列表
SubItems := TSubItems.Create (Path);
Result := S_OK;
end;
下面实现GetProperties。如果您还记得,万能浏览器调用GetProperties来得到任意结点的以名/值对表达的结点信息。在COM中,我们只需使用可变数组即可实现:
function TFileSystemExplorable.GetProperties(const Path: WideString;
out Properties: OleVariant): HResult;
begin
Result := S_OK;
//Properties 是一个二维数组:
//
// | Property Name 1 | Property Value 1 |
// | Property Name 2 | Property Value 2 |
//
Properties := VarArrayCreate ([
0, 4, //行范围
0, 1 //列范围
],
varOleStr //数组成员是字符串
);
//类型(Type): 文件还是文件夹?
Properties [0, 0] := 'Type';
if IsFolder (Path) then
Properties [0, 1] := 'Folder' // 文件夹
else
Properties [0, 1] := 'File'; // 文件
//文件名
Properties [1, 0] := 'Name';
Properties [1, 1] := NameOfFile (Path);
//文件大小
Properties [2, 0] := 'Size';
Properties [2, 1] := IntToStr (SizeOfFile (Path));
//文件日期
Properties [3, 0] := 'Date/Time';
Properties [3, 1] := DateTimeToStr (DateTimeOfFile (Path));
//文件属性
Properties [4, 0] := 'Attributes';
Properties [4, 1] := AttributesOfFile (Path);
end;
我们做的只是简单地返回一个二维可变数组(从0到4共有5行)包含下面的文件属性:文件类型、文件名、文件大小、文件日期及文件属性。具体细节请见源代码。
最后还应实现的是GetMenuActions和DoMenuAction。(由于篇幅所限,这里不再介绍,详见源码。)
这两个方法可以在您的层次结构中的任意结点上实现上下文相关的自定义的操作。GetMenuActions返回给万能浏览器一个包含“动作-ID”值的数组,DoMenuAction执行用户选定的动作(Action)。
我们这里仅仅实现一个动作-文件改名,为安全起见当文件除“存档位”之外的其它属性位有值时不允许文件改名操作:
const ACTION_RENAME = 1;
function TFileSystemExplorable.GetMenuActions(const Path: WideString;
out Actions: OleVariant): HResult;
var
ActionCount : integer;
begin
Result := S_OK;
//ActionCount展示了怎样根据上下文动态生成菜单项
ActionCount := 0;
//菜单动作(Actions)是一个二维数组
//
// | 动作名1 | 动作ID1 |
// | 动作名2 | 动作ID2 |
//
Actions := VarArrayCreate ([
0, 0, //行范围
0, 1 //列范围
],
varVariant //可变的元素
);
//改名
//文件只有存档位被置位时才允许改名
if (FileOnlyHasArchiveAttributeSet (Path)) then
begin
//增加改名动作(Rename Action)
Actions [ActionCount, 0] := 'Rename';
Actions [ActionCount, 1] := ACTION_RENAME; //这是一个整型常量,值为1
//one action in!
inc (ActionCount);
end;
//这里您可以自行定义更多内容...
//如果Actions中没有内容,清除它
if (ActionCount <= 0) then Actions := Unassigned;
end;
我们简单地建立了一个二维数组保存“动作-ID”值。由于我们只实现了一个改名动作,所以数组中只包含了一行内容(行范围是0到0)。
每当万能浏览器得到它的动作列表,它会产生相应的上下文相关的弹出菜单。随后,如果用户选择了“改名”动作,万能浏览器会通过给插件传递一个ACTION_RENAME常量来调用插件中的DoMenuAction动作。下面即是插件中实现DoMenuAction动作完成改名的部分:
function TFileSystemExplorable.DoMenuAction(const Path: WideString; ActionId: Integer): HResult;
var
NewName : string;
begin
Result := S_OK;
case ActionID of
ACTION_RENAME :
//文件改名
if (TfrmRename.Rename (Path, NewName)) then
//如果改名成功,给万能浏览器主程序发送一个通知
FExplorer.RenamePath (Path, NewName);
//如果还有其它动作,在这里实现...
end;
end;
这里,我们首先检查ActionID参数并对ACTION——RENAME动作进行响应。对于ACTION_RENAME,我们只是简单地打开一个窗体(TfrmRename),允许用户为文件输入新名字。 如果改名成功的话,就调用FExplorer.RenamePath告诉万能浏览器主程序有一个结点已经改名,主程序会及时地更新它的用户界面以反映这种改变。
这是一个插件如何和它的主程序进行回调的示例。通常,这么做是必需的,因为主程序和插件需要同步更新它们共同拥有的数据。
最后一件事就是以某种方式为我们的插件服务器注册及注销组件目录。最容易的办法是每当注册了我们的插件时,我们也一同注册目录信息,注销插件的同时注销目录信息。对于DLL服务器来说,相应的注册和注销入口是下面的两个公布的函数:DLLRegisterServer(注册)与DLLUnregisterServer(注销)。
先看一下注册过程:
In FileSystemPlugin.dpr
library FileSystemPlugin;
//overidden to include categories registration
function DllRegisterServer: HResult; stdcall;
begin
//调用缺省例程
Result := ComServ.DllRegisterServer;
//注册为浏览插件
RegisterAsExplorableClass (Class_FileSystemExplorable, True); //True即注册
end;
exports
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;
...
我们对DLLRegisterServer使用了“overriding”技术保证了同时执行目录注册过程RegisterAsExplorableClass。(RegisterAsExplorableClass的具体实现略)
//注册一个指定的类作为一个explorable服务器
procedure RegisterAsExplorableClass (const CLSID : TCLSID; Register : boolean);
var
CatReg : ICatRegister;
begin
CatReg := StdComponentCategoryMgr as ICatRegister;
if (Register) then
begin
//先注册CATID_Explorable目录
//ExplorableCategoryInfo中放的内容在前面已经讨论过
OleCheck (CatReg.RegisterCategories (1, @ExplorableCategoryInfo));
//然后注册支持这个目录的CLSID
OleCheck (CatReg.RegisterClassImplCategories (CLSID, 1, @ExplorableCategoryInfo));
end
else begin
//这里是注销部分
end;
end;
//返回标准组件目录管理器
function StdComponentCategoryMgr : IUnknown;
begin
Result := CreateComObject (CLSID_StdComponentCategoryMgr);
end;
我们在这里做的只不过是注册“Explorable Plugins”目录,及注册实现这一目录的FileSystemExplorable的coclass。该讨论的前面已经讲过了。
注册过程同样照此办理:
In FileSystemPlugin.dpr
library FileSystemPlugin;
//重载以实现目录的注销
function DllUnregisterServer: HResult; stdcall;
begin
//注销万能浏览器的插件
RegisterAsExplorableClass (Class_FileSystemExplorable, False); //False表示注销
//返回缺省的注销句柄
Result := ComServ.DllUnregisterServer;
end;
exports
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;
...
相应的,RegisterAsExplorableClass的注销部分定义如下:
//注册一个指定的类作为一个explorable服务器
procedure RegisterAsExplorableClass (const CLSID : TCLSID; Register : boolean);
var
CatReg : ICatRegister;
begin
CatReg := StdComponentCategoryMgr as ICatRegister;
if (Register) then
begin
//这里是注册部分
end
else begin
//注意:我们不能注销CATID_Explorable,因为其它服务器可能仍在用它!
//注销支持这个目录的CLSID
OleCheck (CatReg.UnregisterClassImplCategories (CLSID, 1, @ExplorableCategoryInfo));
DeleteRegKey ('CLSID/' + GuidToString (CLSID) + '/' + 'Implemented Categories');
end;
end;
//返回标准组件目录管理器
function StdComponentCategoryMgr : IUnknown;
begin
Result := CreateComObject (CLSID_StdComponentCategoryMgr);
end;
注意我们额外地调用了DeleteRegKey删除“Implemented Categories”子键,从而完全地从注册表中删除我们的coclass。
另一个注册目录信息的方法是建立自己的定制的类代理(class factory),它继承自TComObjectFactory 。TComObjectFactory有一个虚拟方法UpdateRegistry,可以通过重载实现自定义的注册和注销动作。
万能浏览器主程序的实现
随着插件的完工(一定不要忘记注册您的插件),现在到了主程序部分。如前所述,我们的万能浏览器主程序是一个MDI程序,每个MDI子窗口掌管着一个浏览插件。作为实现,当万能浏览器加载时,它寻找所有注册过的浏览插件并把它们放到一个菜单列表里。用户从菜单中选择任何一个插件项目,我们将打开一个MDI子窗体(TfrmExplorerHost)并把选中的插件纳入窗体。
图:“Explorable Plugins”菜单包括了已注册的插件的列表
我们简单地使用前面提到的COM组件目录设备来取得可用的插件,具体说来,是用到了ICatInformation。实现方法略。
procedure TfrmMain.LoadExplorableClasses;
var
Count, i : integer;
Explorable : IExplorable;
Description : widestring;
MenuItem : TMenuItem;
begin
//取得explorable服务器列表
//FExplorableClasses是一个CLSID数组
Count := GetExplorableClasses (FExplorableClasses);
if (Count > 0) then
begin
//取得每一个公布的插件描述并放入子菜单
for i := 1 to Count do
begin
//创建Explorable插件
Explorable := CreateComObject (FExplorableClasses
) as IExplorable;
//取得插件描述
OleCheck (Explorable.GetDescription (Description));
//创建一个新菜单项
MenuItem := TMenuItem.Create (Self);
MenuItem.Caption := Description;
//把FExplorableClasses数组的索引放入Tag属性
MenuItem.Tag := i;
//放入菜单中
miExplore.Add (MenuItem);
end;
end;
end;
这段代码取得所有可用coclass的CLSID放入FExplorableClasses数组,然后取得数组中每个插件的描述放入一个菜单列表。
以下是一个真正的GetExplorableClasses:
type
TExplorableClasses = array [1..50] of TCLSID; //50足够大了
//返回Explorable 服务器的CLSIDs
function GetExplorableClasses (var ExplorableClasses : TExplorableClasses) : integer;
var
CatInfo : ICatInformation;
Enum : IEnumGuid;
Fetched : UINT;
begin
Result := 0;
//得到标准目录信息管理器
CatInfo := StdComponentCategoryMgr as ICatInformation;
//取得所有已注册的Explorable类
//ExplorableCategoryInfo中的内容前面已经讨论过
OleCheck (CatInfo.EnumClassesOfCategories (1, @ExplorableCategoryInfo, 0, NIL, Enum));
//把Explorable类放入ExplorableClasses数组
if (Enum <> NIL) then
begin
OleCheck (Enum.Reset);
//填充ExplorableClasses数组
//注意如果Fetched >= High (ExplorableClasses),那么可能还有!
//但对于我们的目的来说,这么简单的代码就够了
OleCheck (Enum.Next (High (ExplorableClasses), ExplorableClasses [1], Fetched));
Result := Fetched;
end;
end;
前面讲过,ICatInformation.EnumClassesOfCategories用来取得所有实现一个指定目录的coclass,然后重复产生与所有匹配CLSID相应的数组。
到此为止,我们已经取得了所有可用的插件并把它们放进菜单。当用户选择了其中的一个时,我们取得选中的插件的coclass,然后把插件装入万能浏览器的主窗体。实现部分略。
//用户从主菜单中选择了一个插件时调用
procedure TfrmMain.miExplorableItemClick(Sender: TObject);
var
ExplorableClass : TCLSID;
Explorable : IExplorable;
begin
//取得选中的Explorable类
//Tag属性包含着FExplorableClasses数组的序号
ExplorableClass := FExplorableClasses [(Sender as TMenuItem).Tag];
//创建Explorable插件
Explorable := CreateComObject (ExplorableClass) as IExplorable;
//往一个新的explorer主窗体中调入Explorable服务器
TfrmExplorerHost.Load (Explorable);
end;
这段程序取得选取的菜单项对应的插件coclass。然后我们把新插件加载到万能浏览器的主窗体。
万能浏览器的主窗体分为两个部分:左边是TreeView目录树(tvwExplorer),右边是ListView列表项(lvwProperties)。目录树显示插件的层次结构,当选中一个树结点时列表项显示它的属性。对FileSystemPlugin来说,目录树显示了您的文件系统的结构图而列表项显示文件属性(文件名、文件大小等)。如图:
小虫~ (2001-11-13 0:22:00)
好文
吴下阿蒙 (2001-11-13 0:30:00)
插件结构的实现之原理篇
http://www.ccidnet.com/tech/guide/2001/09/29/58_3374.html
(文:Binh Ly 安富国编译 2001年09月29日 15:41)
插件结构有助于编写有良好的扩充和定制功能的应用程序。例如,您可能想做一个有三个不同版本的软件(标准版、专业版和企业版),您不必写三套不同的代码,只需建立一个单独的主程序(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_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为例,开始具体编程的工作。