如何初始化同COM交互的线程?
通常如果没有初始化线程会显示如下的错误信号:"CoInitialize has not been called" (800401F0 ) 。
问题在于每个同COM交互的线程必须使自身初始化并进入一个Apartment。可以通过加入一个单线程的 Apartment (STA)获得,也可以进入一个多线程的Apartment (MTA)。
STA是基于Windows的消息队列实现系统同步的。当COM对象或线程是依赖于线程相关的对象时,比如界面元素,就应该使用STA,下面演示如何初始化一个线程进入STA:
procedure FooThreadFunc;
Begin
CoInitializeEx (NIL, COINIT_APARTMENTTHREADED);
... ...
CoUninitialize;
end;
处于MTA的对象则可以随时随地收到用户的调用,对象同界面元素无关时应该使用MTA模式,但一定要小心地控制同步,下面是演示如何初始化一个进入MTA的线程:
procedure FooThreadFunc;
begin
CoInitializeEx (NIL, COINIT_MULTITHREADED);
... ...
CoUninitialize;
end;
实现、跨越Apartment列集接口指针
在运行COM Server时经常会遇到"The application called an interface that was marshaled for a different thread" (8001010E)这类错误,它是如何产生的呢?
在Apartment之间传递接口指针的时候,如果没有执行Marshal(列集),就会破坏COM的线程规则,引起这个错误。列集接口指针需要使用CoMarshalInterface 和CoUnmarshalInterface函数。但实际使用时,我们更多的是用更简单的CoMarshalInterThreadInterfaceInStream 和 CoGetInterfaceAndReleaseStream API。
下面的代码演示了如何在基于不同Aparment的Foo1和Foo2线程之间列集一个接口指针:
var MarshalStream : pointer;
//源线程
procedure Foo1ThreadFunc; //或者TFoo1.Execute
var Foo : IFoo;
begin
//假设Foo2Thread正处于暂停状态
CoInitializeEx (...);
Foo := CoFoo.Create;
//列集
CoMarshalInterThreadInterfaceInStream (IFoo, Foo, IStream (MarshalStream));
//告诉Foo2Thread 列集完毕
Foo2Thread.Resume;
CoUninitialize;
end;
//用户线程
procedure Foo2ThreadFunc; //或TFoo2.Execute
var Foo : IFoo;
begin
CoInitializeEx (...);
//逆列集
CoGetInterfaceAndReleaseStream (IStream (MarshalStream), IFoo, Foo);
MarshalStream := NIL;
//使用Foo
Foo.Bar;
CoUninitialize;
end;
上面的列集技术是列集一次然后逆列集一次。如果我们想列集一次然后多次逆列集的话,可以使用(NT 4 SP3) COM提供的全局接口表(Global Interface Table,GIT)。GIT允许列集一个接口指针到一个cookie,然后使用这个Cookie来多次逆列集。使用GIT的话,上面的例子要修改为:
const
CLSID_StdGlobalInterfaceTable : TGUID =
'{00000323-0000-0000-C000-000000000046}';
type
IGlobalInterfaceTable = interface(IUnknown)
['{00000146-0000-0000-C000-000000000046}']
function RegisterInterfaceInGlobal (pUnk : IUnknown; const riid: TIID;
out dwCookie : DWORD): HResult; stdcall;
function RevokeInterfaceFromGlobal (dwCookie: DWORD): HResult; stdcall;
function GetInterfaceFromGlobal (dwCookie: DWORD; const riid: TIID; out ppv): HResult; stdcall;
end;
function GIT : IGlobalInterfaceTable;
const
cGIT : IGlobalInterfaceTable = NIL;
begin
if (cGIT = NIL) then
OleCheck (CoCreateInstance (CLSID_StdGlobalInterfaceTable, NIL, CLSCTX_ALL,
IGlobalInterfaceTable, cGIT));
Result := cGIT;
end;
var MarshalCookie : dword;
//源线程
procedure Foo1ThreadFunc;
var Foo : IFoo;
begin
CoInitializeEx (...);
Foo := CoFoo.Create;
//列集
GIT.RegisterInterfaceInGlobal (Foo, IFoo, MarshalCookie)
//告诉Foo2Thread MarshalCookie已经准备好了
Foo2Thread.Resume;
CoUninitialize;
end;
//用户线程
procedure Foo2ThreadFunc;
var Foo : IFoo;
begin
CoInitializeEx (...);
//逆列集
GIT.GetInterfaceFromGlobal (MarshalCookie, IFoo, Foo)
//调用Foo
Foo.Bar;
CoUninitialize;
end;
另外当不需要列集的时候,不要忘了从GIT中删除指针:
GIT.RevokeInterfaceFromGlobal (MarshalCookie);
MarshalCookie := 0;
下面实现了一个TGIP类可以简化调用:
{ TGlobalInterfacePointer
用法:假定有一个接口指针pObject1,想使接口Iobject1全局化可以使用下面的代码
var
GIP1: TGIP;
begin
GIP1 := TGIP.Create (pObject1, IObject1);
end;
如果想使pObject1本地化,需要直接存取GIP1 对象变量:
var
pObject1: IObject1;
begin
GIP1.GetIntf (pObject1);
pObject1.DoSomething;
end;
}
下面是TGIP类的实现:
TGIP = class
protected
FCookie: DWORD;
FIID: TIID;
function IsValid: boolean;
public
constructor Create (const pUnk: IUnknown; const riid: TIID);
destructor Destroy; override;
procedure GetIntf (out pIntf);
procedure RevokeIntf;
procedure SetIntf (const pUnk: IUnknown; const riid: TIID);
property Cookie: dword read FCookie;
property IID: TGUID read FIID;
end;
{ TGIP }
function TGIP.IsValid: boolean;
begin
Result := (FCookie <> 0);
end;
constructor TGIP.Create (const pUnk: IUnknown; const riid: TIID);
begin
inherited Create;
SetIntf (pUnk, riid);
end;
destructor TGIP.Destroy;
begin
RevokeIntf;
inherited;
end;
procedure TGIP.GetIntf (out pIntf);
begin
Assert (IsValid);
OleCheck (GIT.GetInterfaceFromGlobal (FCookie, FIID, pIntf));
end;
procedure TGIP.RevokeIntf;
begin
if not (IsValid) then Exit;
OleCheck (GIT.RevokeInterfaceFromGlobal (FCookie));
FCookie := 0;
FIID := GUID_NULL;
end;
procedure TGIP.SetIntf (const pUnk: IUnknown; const riid: TIID);
begin
Assert ((pUnk <> NIL) and not (IsEqualGuid (riid, GUID_NULL)));
RevokeIntf;
OleCheck (GIT.RegisterInterfaceInGlobal (pUnk, riid, FCookie));
FIID := riid;
end;
实现正确的错误处理
在COM中,每个接口方法必须返回一个错误代码给客户端,错误代码是标准的32位数值,也就是我们所熟悉的HRESULT。HRESULT数值可以分为几部分:一位用于表示成功或失败,几位用于表示错误分类,剩下几位用于表示错误代号(COM推荐错误代码应该在0200到FFFF 范围内。
虽然HRESULT可以用来指示错误,但是它也有很大的局限性,因为除了错误代码,我们可能还想让COM服务器告诉客户端错误的详细描述、发生位置以及客户在哪儿可以得到更多的相关帮助(通过指定帮助上下文来调用帮助文件)。因此,COM引入了IErrorInfo接口,客户端可以通过这个接口来获得额外的错误信息。同时如果COM服务器支持IErrorInfo,COM同时建议服务器实现ISupportErrorInfo接口,虽然这个接口不是必须实现的,但一些客户端,比如Visual Basic将会向服务器请求这个接口。
Delphi本身已经为我们提供了安全调用处理。当在对象内部产生一个异常时,Delphi会自动俘获异常并把它转化为一个COM HRESULT,同时提供一个IErrorInfo 接口用于传递给客户端。这些是通过ComObj单元中的HandleSafeCallException函数实现的。此外,VCL 类也为我们实现了ISupportErrorInfo 接口。
下面举例来说,当在服务器内部产生一个Ewhatever的异常时,它总会被客户端认为是EOleException异常,EOleException异常包括HRESULT 和IErrorInfo 所包含的所有信息,比如错误代号、描述、发生位置以及上下文相关帮助。而为了提供客户端所需要信息,服务器必须把EWhatever转化为EoleSysError异常,同时要确保错误代码为格式化好的HRESULT。比如,假设有一个TFoo对象,它有一个Bar方法。在Bar方法中我们想产生一个异常,异常的错误代号为5,描述="错误消息",帮助文件="HelpFile.hlp",帮助上下文= 1,代码示意如下:
uses ComServ;
const
CODE_BASE = $200; //推荐代码在0200 – FFFF之间
procedure TFoo.Bar;
begin
//帮助文件
ComServer.HelpFileName := 'HelpFile.hlp';
//引发异常
raise EOleSysError.Create (
'错误消息', ErrorNumberToHResult (5 + CODE_BASE), //格式化HRESULT
1 //帮助上下文
);
end;
//格式化Hresult
function ErrorNumberToHResult (ErrorNumber : integer) : HResult;
const
SEVERITY_ERROR = 1;
FACILITY_ITF = 4;
Begin
Result := (SEVERITY_ERROR shl 31) or (FACILITY_ITF shl 16) or word (ErrorNumber);
end;
上面的ErrorNumberToHResult函数就是简单的把错误代号转化为标准的HRESULT。同时给错误代号加上了CODE_BASE (0x200),以便遵循COM的建议,就是使错误代码位于0200到 FFFF之间。
下面是客户端利用EOleException俘获错误的代码:
const
CODE_BASE = $200;
procedure CallFooBar;
var
Foo : IFoo;
Begin
Foo := CoFoo.Create;
Try
Foo.Bar;
Except
on E : EOleException do
ShowMessage ('错误信息: ' + E.Message + #13 +
'错误代号: ' + IntToStr (HResultToErrorNumber (E.ErrorCode) - CODE_BASE) + #13 +
'发生位置: ' + E.Source + #13 +
'帮助文件: ' + E.HelpFile + #13 +
'帮助上下文: ' + IntToStr (E.HelpContext)
);
end;
end;
function HResultToErrorNumber (hr : HResult) : integer;
begin
Result := (hr and $FFFF);
end;
上述过程其实就是服务器的逆过程,就是从HRESULT中提取错误代码,并显示额外错误信息的过程。
如何实现多重接口
其实非常非常简单,比如想建立一个COM对象,它已经支持IFooBar接口了,我们还想实现两个外部接口IFoo和IBar。IFoo和IBar 接口定义如下:
IFoo = interface
procedure Foo; //隐含返回HRESULT
end;
IBar = interface
procedure Bar;
end;
实现部分:
type
TFooBar = class (TAutoObject, IFooBar, IFoo, IBar)
Protected
//IfooBar
... IFooBar methods here ...
//IFoo methods
procedure Foo;
//IBar methods
procedure Bar;
...
end;
procedure TFooBar.Foo;
begin
end;
procedure TFooBar.Bar;
begin
end;
是不是很简单啊,要注意的是如果IfooBar、IFoo和IBar都是基于IDispatch接口的,TAutoObject 将只会为IFooBar实现IDispatch,基于脚本的客户端只能看到IFooBar接口方法。
Delphi中定义的COM基类的用途
Delphi提供了很多基类用于COM开发:TInterfacedObject、TComObject、TTypedComObject、TAutoObject、TAutoIntfObject、TComObjectFactory、TTypedComObjectFactory、TAutoObjectFactory等。那么这些类适用于哪些条件下呢?
(1)TInterfacedObject
TInterfacedObject 只提供对IUnknown接口的实现,如果想创建一个内部对象来实现内部接口的话,TInterfacedObject 就是一个最好的基类。
(2)TComObject
TComObject实现了IUnknown、ISupportErrorInfo、标准的COM聚集支持和一个对应的类工厂支持。如果我们想创建一个轻量级的可连接客户端的基于IUnknown接口的COM对象的话,COM对象就应该从TComObject 类继承。
(3)TComObjectFactory
TComObjectFactory 是同TComObject对象配合工作的。它把对应的TComObject 公开为coclass。TComObjectFactory 提供了coclass 的注册功能(根据CLSIDs、ThreadingModel、ProgID等)。还实现了IClassFactory 和 IClassFactory2 接口以及标准的COM 对象许可证支持。简单地说如果要想创建TComObject对象,就会同时需要TComObjectFactory对象。
(4)TTypedComObject
TTypedComObject等于TComObject + 对IProvideClassInfo接口的支持。IProvideClassInfo 是自动化的标准接口用来公开一个对象的类型信息的(比如可获得的名字、方法、支持的接口等,类型信息储存在相关的类型库中)。TTypedComObject 可以用来支持那些在运行时能够浏览类型信息的客户端,比如Visual Basic的TypeName 函数期望一个对象能够实现IProvideClassInfo 接口,以便通过类型信息确定对象的文档名称(documented name)。
(5)TTypedComObjectFactory
TTypedComObjectFactory 是和TTypedComObject配合工作的。就等于TComObjectFactory + 提供缓存了的TTypedComObject类型信息(ITypeInfo)引用。一句话,创建TTypedComObject必然会同时创建TypedComObjectFactory 类工厂。
(6)TAutoObject
TAutoObject 等于TTypedComObject + 实现IDispatch接口。TAutoObject适用于实现支持自动化控制的COM对象。
(7)TAutoObjectFactory
TAutoObjectFactory显然是同TAutoObject密不可分的。它等于TTypedComObjectFactory + 提供了TAutoObject的接口和连接点事件接口的缓存类型信息 (ITypeInfo)。
(8)TAutoIntfObject
TAutoIntfObject等于TInterfacedObject +实现了IDispatch接口。同TAutoObject相比, TAutoIntfObject 没有对应的类工厂支持,这意味着外部客户端无法直接实例化一个TAutoIntfObject的衍生类。然而,TAutoIntfObject 非常适合作为基于IDispatch接口的下层对象或属性对象,客户端可以通过最上层的自动化对象得到对它们的引用。
理解列集的概念
在进行COM调用的时候,最经常碰到的错误恐怕就是"Interface not supported/registered" (80004002)错误了。这通常是由于没有在客户端机器上注册类型库导致的。
图1.115 |
COM的位置透明性是通过代理和存根对象来实现的。当一个客户端调用一个远程机器上的COM对象(或是另一个Apartment中的COM对象)时,客户端的请求首先通过代理,然后代理再通过COM,然后再通过存根才到达真正的对象,其关系如图1.115所示。
每当客户端调用COM对象的方法时,代理都会把方法参数整理为一个平直数组然后再传递给COM,而COM再把数组传递给存根,由存根负责解包数组还原参数,最后服务器对象才会按参数调用方法,整个过程就成为列集。
注意代理和存根同样是COM对象,系统提供了一个缺省的存根和代理,它们实现在 oleaut32.dll 中,对于大多数的列集处理来说,缺省的存根和代理已经足够用了,但它只能列集那些自动化兼容的数据类型的参数。
在类型库中,必须注释接口定义的[oleautomation]标识,表明我们希望使用类型库列集器来列集我们的接口。[oleautomation]标识适用于任意接口(只要方法参数全是自动化兼容的),认为它只使用于IDispatch类型接口的想法是不正确的。
由于不能像Visual C++那样简单地创建用户定制的代理-存根DLL,所以Delphi严重依赖于类型库列集器实现列集。同时由于类型库列集器的列集依赖于类型库中的信息,所以必须在服务器和客户端的机器上同时注册类型库,否则调用时就会碰到"Interface not supported/registered" 错误。
另外,要注意只有当我们使用前期绑定时才需要注册类型库。如果使用后期绑定(比如variant或双接口绑定),COM会调用IDispatch 接口早已注册在系统中的代理-存根DLL,因此后期绑定时不需要注册类型库文件。
如何实现一个支持Visual Basic的For Each调用的COM对象
熟悉Visual Basic和ASP开发的人一定会很熟悉用Visual Basic的For Each语法调用COM集合对象。
For Each允许一个VB的客户端很方便地遍历一个集合中的元素:
Dim Items as Server.IItems //声明集合变量
Dim Item as Server.IItem //声明集合元素变量
Set Items = ServerObject.GetItems //获得服务器的集合对象
//用 For Each循环遍历集合元素
For Each Item in Items
Call DoSomething (Item)
Next
那么什么样的COM对象支持For Each语法呢?答案就是实现IEnumVARIANT COM接口,它的定义如下:
IEnumVARIANT = interface (IUnknown)
function Next (celt; var rgvar; pceltFetched): HResult;
function Skip (celt): HResult;
function Reset: HResult;
function Clone(out Enum): HResult;
end;
For Each语法知道如何调用IEnumVARIANT 接口的方法(特别是Next方法)来遍历集合中的全部元素。那么如何才能向客户端公开IEnumVARIANT 接口呢,下面是一个集合接口:
//集合元素
IFooItem = interface (IDispatch);
//元素集合
IFooItems = interface (IDispatch)
property Count : integer;
property Item [Index : integer] : IFoo;
end;
要想使用IEnumVARIANT接口,我们的集合接口首先必须支持自动化(也就是基于IDispatch接口),同时集合元素也必须是自动化兼容的(比如byte、BSTR、long、IUnknown、IDispatch等)。
然后,我们利用类型库编辑器添加一个名为_NewEnum的只读属性到集合接口中,_NewEnum 属性必须返回IUnknown 接口,同时dispid = -4 (DISPID_NEWENUM)。修改的IFooItems定义如下:
IFooItems = interface (IDispatch)
property Count : integer;
property Item [Index : integer] : IFoo;
property _NewEnum : IUnknown; dispid -4;
end;
接下来我们要实现_NewEnum属性来返回IEnumVARIANT 接口指针:
下面是一个完整的例子,它创建了一个ASP组件,有一个集合对象用来维护一个email地址列表:
unit uenumdem;
interface
uses
Windows, Classes, ComObj, ActiveX, AspTlb, enumdem_TLB, StdVcl;
type
IEnumVariant = interface(IUnknown)
['{00020404-0000-0000-C000-000000000046}']
function Next(celt: LongWord; var rgvar : OleVariant;
pceltFetched: PLongWord): HResult; stdcall;
function Skip(celt: LongWord): HResult; stdcall;
function Reset: HResult; stdcall;
function Clone(out Enum: IEnumVariant): HResult; stdcall;
end;
TRecipients = class (TAutoIntfObject, IRecipients, IEnumVariant)
protected
PRecipients : TStringList;
Findex : Integer;
Function Get_Count: Integer; safecall;
Function Get_Items(Index: Integer): OleVariant; safecall;
procedure Set_Items(Index: Integer; Value: OleVariant); safecall;
function Get__NewEnum: IUnknown; safecall;
procedure AddRecipient(Recipient: OleVariant); safecall;
function Next(celt: LongWord; var rgvar : OleVariant;
pceltFetched: PLongWord): HResult; stdcall;
function Skip(celt: LongWord): HResult; stdcall;
function Reset : HResult; stdcall;
function Clone (out Enum: IEnumVariant): HResult; stdcall;
public
constructor Create;
constructor Copy(slRecipients : TStringList);
destructor Destroy; override;
end;
TEnumDemo = class(TASPObject, IEnumDemo)
protected
FRecipients : IRecipients;
procedure OnEndPage; safecall;
procedure OnStartPage(const AScriptingContext: IUnknown); safecall;
function Get_Recipients: IRecipients; safecall;
end;
implementation
uses ComServ,
SysUtils;
constructor TRecipients.Create;
begin
inherited Create (ComServer.TypeLib, IRecipients);
PRecipients := TStringList.Create;
FIndex := 0;
end;
constructor TRecipients.Copy(slRecipients : TStringList);
begin
inherited Create (ComServer.TypeLib, IRecipients);
PRecipients := TStringList.Create;
FIndex := 0;
PRecipients.Assign(slRecipients);
end;
destructor TRecipients.Destroy;
begin
PRecipients.Free;
inherited;
end;
function TRecipients.Get_Count: Integer;
begin
Result := PRecipients.Count;
end;
function TRecipients.Get_Items(Index: Integer): OleVariant;
begin
if (Index >= 0) and (Index < PRecipients.Count) then
Result := PRecipients[Index]
else
Result := '';
end;
procedure TRecipients.Set_Items(Index: Integer; Value: OleVariant);
begin
if (Index >= 0) and (Index < PRecipients.Count) then
PRecipients[Index] := Value;
end;
function TRecipients.Get__NewEnum: IUnknown;
begin
Result := Self;
end;
procedure TRecipients.AddRecipient(Recipient: OleVariant);
var
sTemp : String;
begin
PRecipients.Add(Recipient);
sTemp := Recipient;
end;
function TRecipients.Next(celt: LongWord; var rgvar : OleVariant;
pceltFetched: PLongWord): HResult;
type
TVariantList = array [0..0] of olevariant;
var
i : longword;
begin
i := 0;
while (i < celt) and (FIndex < PRecipients.Count) do
begin
TVariantList (rgvar) [i] := PRecipients[FIndex];
inc (i);
inc (FIndex);
end; { while }
if (pceltFetched <> nil) then
pceltFetched^ := i;
if (i = celt) then
Result := S_OK
else
Result := S_FALSE;
end;
function TRecipients.Skip(celt: LongWord): HResult;
begin
if ((FIndex + integer (celt)) <= PRecipients.Count) then
begin
inc (FIndex, celt);
Result := S_OK;
end
else
begin
FIndex := PRecipients.Count;
Result := S_FALSE;
end; { else }
end;
function TRecipients.Reset : HResult;
begin
FIndex := 0;
Result := S_OK;
end;
function TRecipients.Clone (out Enum: IEnumVariant): HResult;
begin
Enum := TRecipients.Copy(PRecipients);
Result := S_OK;
end;
procedure TEnumDemo.OnEndPage;
begin
inherited OnEndPage;
end;
procedure TEnumDemo.OnStartPage(const AScriptingContext: IUnknown);
begin
inherited OnStartPage(AScriptingContext);
end;
function TEnumDemo.Get_Recipients: IRecipients;
begin
if FRecipients = nil then
FRecipients := TRecipients.Create;
Result := FRecipients;
end;
initialization
TAutoObjectFactory.Create(ComServer, TEnumDemo, Class_EnumDemo,
ciMultiInstance, tmApartment);
end.
下面是用来测试ASP组件的ASP脚本:
Set DelphiASPObj = Server.CreateObject("enumdem.EnumDemo")
DelphiASPObj.Recipients.AddRecipient "windows@ms.ccom"
DelphiASPObj.Recipients.AddRecipient "borland@hotmail.com"
DelphiASPObj.Recipients.AddRecipient "delphi@hotmail.com"
Response.Write "使用For Next 结构"
for i = 0 to DelphiASPObj.Recipients.Count-1
Response.Write "DelphiASPObj.Recipients.Items[" & i & "] = " & _
DelphiASPObj.Recipients.Items(i) & ""
next
Response.Write "使用 For Each 结构"
for each sRecipient in DelphiASPObj.Recipients
Response.Write "收信人 : " & sRecipient & ""
next
Set DelphiASPObj = Nothing
上面这个例子中,集合对象储存的是字符串数据,其实它可以储存任意的COM对象,对于COM对象可以用Delphi定义的TInterfaceList 类来管理集合中的COM对象元素。
下面是一个可重用的类TEnumVariantCollection,它隐藏了IEnumVARIANT接口的实现细节。为了插入TEnumVariantCollection 类到集合对象中去,我们需要实现一个有下列三个方法的接口:
IVariantCollection = interface
//使用枚举器来锁定列表拥有者
function GetController : IUnknown; stdcall;
//使用枚举器来确定元素数
function GetCount : integer; stdcall;
//使用枚举器来返回集合元素
function GetItems (Index : olevariant) : olevariant; stdcall;
end;
修改后的TFooItem的定义如下:
type
//Foo items collection
TFooItems = class (TSomeBaseClass, IFooItems, IVariantCollection)
Protected
{ IVariantCollection }
function GetController : IUnknown; stdcall;
function GetCount : integer; stdcall;
function GetItems (Index : olevariant) : olevariant; stdcall;
protected
FItems : TInterfaceList; //内部集合元素列表;
...
end;
function TFooItems.GetController: IUnknown;
begin
//always return Self/collection owner here
Result := Self;
end;
function TFooItems.GetCount: integer;
begin
//always return collection count here
Result := FItems.Count;
end;
function TFooItems.GetItems(Index: olevariant): olevariant;
begin
//获取IDispatch 接口
Result := FItems.Items [Index] as IDispatch;
end;
最后,我们来实现_NewEnum 属性:
function TFooItems.Get__NewEnum: IUnknown;
begin
Result := TEnumVariantCollection.Create (Self);
end;
这就是全部要做的工作。
客户端如何实现对基于IEnumVARIANT-接口的集合对象的枚举?
前面提到了在Visual Basic中,我们可以用For Each结构很简单地实现对基于IEnumVARIANT-接口的集合对象的枚举。那么在Delphi中有没有办法实现类似的操作呢?
答案是有两种方法可以做到,第一种比较困难,它需要我们非常熟悉IEnumVARIANT接口方法的调用,特别是reset和next方法。第二种简单的则是使用TEnumVariant类,它使用起来非常简单,代码示意如下:
uses ComLib;
var
Foo : IFoo;
Item : olevariant;
Enum : TEnumVariant;
Begin
Foo := CreateOleObject ('FooServer.Foo') as IFoo; //or CoFoo.Create
Enum := TEnumVariant.Create (Foo.Items);
while (Enum.ForEach (Item)) do
DoSomething (Item);
Enum.Free;
end;
看起来确实和For Each区别不大了。
如何使用聚集和包含
COM聚集和包含是两种重用COM对象的技术。为了弄清为什么需要使用聚集或包含技术,考虑一下下面的情况:假设现在有两个COM对象Foo (IFoo)和Bar (IBar)。我们想创建一个新的对象FooBar,它提供Foo和Bar两者的功能。那么我们可以这样定义新类:
IFoo = interface
procedure Foo;
end;
IBar = interface
procedure Bar;
end;
type
FooBar = class (BaseClass, IFoo, IBar)
end;
然后就是当实现IFoo接口的方法时重用Foo,当实现Ibar接口的时候重用Ibar。这时就需要聚集和包含了。
1. 包含
包含实际上就是初始化一个内部对象,然后把对接口方法的调用请求都传递给内部对象,如下为实现对IFoo的包含:
type
TFooBar = class (TComObject, IFoo)
Protected
//IFoo methods
procedure Foo;
protected
FInnerFoo : IFoo;
function GetInnerFoo : IFoo;
end;
procedure TFooBar.Foo;
var
Foo : IFoo;
Begin
//获得内部Foo对象
Foo := GetInnerFoo;
//传递方法请求给内部的Foo对象
Foo.Foo;
end;
function TFooBar.GetInnerFoo : IFoo;
begin
//创建内部的Foo对象
if (FInnerFoo = NIL) then
FInnerFoo := CreateComObject (Class_Foo) as IFoo;
Result := FInnerFoo;
end;
如果我们按下面定义实现类的话,由于没有代理接口请求,所以不能认为是包含:
type
TFooBar = class (TComObject, IFoo)
Protected
function GetInnerFoo : IFoo;
property InnerFoo : IFoo read GetInnerFoo implements IFoo;
end;
先前的实现和现在的不同在于代理的问题,前者必须公开了IFoo接口,然后通过Foo方法代理对接口的请求给内部对象,而后者是客户端直接请求InnerFoo提供的IFoo接口方法,没有代理请求的发生,所以不是包含。
2. 聚集
实现包含有时会变得非常烦琐,因为如果内部对象的接口支持大量的方法时,我们必须重复大量的编码工作来实现代理请求。还有很多其他原因使得我们需要聚集,简单地说聚集就是一种直接公开内部对象的机制。
聚集的首要规则是只能聚集那些支持聚集的内部对象,也就是说内部对象知道如何实现代理和非代理的接口请求。
要想了解更多关于代理和非代理的接口请求,参见Dale Rogerson写的《COM奥秘》一书。
第二条规则是当外部对象构建内部对象时,我们需要:
(1)把外部对象的IUnknown 接口作为CoCreateInstance调用的参数传递给内部对象。
(2)请求内部对象的IUnknown接口,而且是要IUnknown接口。
假设Foo对象是支持聚集的,下面让我们把Foo集成到TFooBar对象中。对IFoo的接口请求是通过Delphi的 implements 关键字实现的。代码示意如下:
Type
TFooBar = class (TComObject, IFoo)
Protected
function GetControllingUnknown : IUnknown;
function GetInnerFoo : IFoo;
property InnerFoo : IFoo read GetInnerFoo implements IFoo; //exposes IFoo directly from InnerFoo
protected
FInnerFoo : IUnknown;
end;
function TFoo.GetControllingUnknown : IUnknown;
begin
//返回正确的IUnknown接口
Result := Controller
Else
Result := Self as IUnknown;
end;
function TFooBar.GetInnerFoo : IFoo;
begin
//创建内部Foo对象 object if not yet initialized
if (FInnerFoo = NIL) then
CoCreateInstance (
CLASS_Foo, //Foo的CLSID
GetControllingUnknown, //传递Iunknown接口给内部对象
CLSCTX_INPROC, //假设Foo是进程内的
IUnknown, //请求Foo的Iunknown接口
FInnerFoo //输出内部Foo对象
);
//返回内部Foo对象
Result := FInnerFoo as IFoo;
end;
Delphi的TComObject 已经实现了内建的聚集特性,同时任何从TComObject继承的COM对象也支持聚集。同时不要忘记如果内部对象不支持聚集,那么这时我们只能使用包含。
理解类工厂的实例属性(SingleInstance, MultiInstance)
(1)类工厂的实例属性只对EXE类型的Server有作用。
(2)实例属性并不是EXE Server的属性也不是COM对象的属性而是类工厂的属性。它决定的是类工厂如何响应客户端的请求来创建对象的方式。所以所谓“一个Server生成一个对象和一个Server创建多个对象”的说法是完全错误的。
实例属性的真正意义其实是:
每一个COM服务器中的对象都会有一个相应的类工厂,每当客户端请求创建服务器中的对象时,COM将会要求对象的类工厂来创建这个对象。当EXE型的Server运行时会注册类工厂(当Server结束时又会被注销),类工厂的注册有三种实例模式:SingleUse、MultiUse和MultiSeparateUse。这里我们只讨论SingleUse和MultiUse这两种最常用的模式。
SingleUse意味着类工厂只创建最多一个相应对象的实例。在一个SingleUse的类工厂创建完它的一个实例后,COM将会注销它。因此,当下一个客户端请求创立一个对象时,COM 无法找到已注册的类工厂,它就会启动另一个EXE Server来获得新的类工厂,这就意味着如果前一个EXE Server运行没有结束,这时系统中会有两个EXE Server在同时运行。
MultiUse则意味着可以创建任意多个类工厂的实例。这意味着只要EXE Server不终止运行,则COM就不会注销类工厂,也就是说同时只可能有一个EXE Server运行并响应客户端创建相应对象的请求。
对于Delphi来说,实例模式相当于:
ciSingleInstance = SingleUse
ciMultiInstance = MultiUse
如何实现支持GetActiveObject函数的COM服务器
对于Microsoft Office来说,可以通过GetActiveObject函数获得系统中激活的Office程序:
var
Word : variant;
Begin
//连接到正在运行的Word实例,
//如果没有运行的实例,会产生异常
Word := GetActiveOleObject ('Word.Application');
end;
那么GetActiveOleObject函数是如何知道word是否正在运行的呢?又该如何实现支持GetActiveOleObject函数的COM Server呢?
需要把我们的COM Server注册到COM的运行对象表中去(Running Object Table,ROT),这可以通过调用RegisterActiveObject API实现:
function RegisterActiveObject (
unk: IUnknown; //要注册的对象
const clsid: TCLSID; //对象的CLSID
dwFlags: Longint; //注册标志通常使用ACTIVEOBJECT_STRONG
out dwRegister: Longint //成功注册后返回的句柄
): HResult; stdcall;
有注册自然就应该有撤消注册,撤消注册可以使用RevokeActiveObject API:
function RevokeActiveObject (
dwRegister: Longint; //先前调用RegisterActiveObject时返回的句柄
pvReserved: Pointer //保留参数,须设为nil
): HResult; stdcall;
要注意的是把一个COM对象注册到ROT中去,意味着只有当服务器从ROT撤消注册后,服务器才能终止运行,显然当不需要Server时,应该从ROT中把COM对象撤消,那么谁以及什么时候应该从ROT中撤消COM对象呢?
比较合适的办法是当客户端发出Quit或Exit命令时由服务器自己进行撤销。
详细的解决方案可参见Microsoft的自动化程序员参考。
另外下面要谈到的ROT的内容主要针对EXE类型的Server,对于进程内的DLL型Server来说,决定何时注册/撤消ROT比较复杂,因为DLL Server的生命期是依赖于客户端的。
假设我们想让一个全局的Foo对象注册到ROT中,代码如下:(在DPR文件中)
begin
Application.Initialize;
RegisterGlobalFoo;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
Var
GlobalFooHandle : longint = 0;
procedure RegisterGlobalFoo;
var
GlobalFoo : IFoo;
Begin
//创建Foo的实例
GlobalFoo := CoFoo.Create;
//注册到ROT
OleCheck (RegisterActiveObject (
GlobalFoo, //Foo的实例
Class_Foo, //Foo的CLSID
ACTIVEOBJECT_STRONG,
GlobalFooHandle //注册后返回句柄
));
end;
然后我们为Foo (IFoo) 添加一个Quit方法:
procedure TFoo.Quit;
begin
RevokeGlobalFoo;
end;
procedure RevokeGlobalFoo;
begin
if (GlobalFooHandle <> 0) then
begin
//撤销
OleCheck (RevokeActiveObject (
GlobalFooHandle, NIL
));
GlobalFooHandle := 0;
end;
end;
下面是一个客户端使用GetActiveOleObject API调用服务器的例子:
var
FooUnk : IUnknown;
Foo : IFoo;
Begin
if (Succeeded (GetActiveObject (
Class_Foo, //Foo的CLSID
NIL, //保留参数,这里用NIL
FooUnk //从ROT返回Foo )))
then begin
//请求IFoo接口
Foo := FooUnk as IFoo;
//......
//终止全局的Foo,从ROT撤销
Foo.Quit;
end;
end;
Delphi本身还有一个GetActiveOleObject函数使用对象的PROGID作为参数而不是对象的CLSID。GetActiveOleObject内部叫GetActiveObject,只工作于自动化对象。
如何实现支持自动化缺省属性语法的属性
假设我们要创建下面这样一个自动化接口:
ICollection = interface (IDispatch)
property Item [Index : variant] : variant;
end;
那么客户端则可以通过ICollection 接口指针像下面这样获得集合中的项目:
Collection.Item [Index]
但我们有时会很懒,希望能按下面的方式调用:
Collection [Index]
允许客户端使用这种简化的语法会带来很大的方便,特别是要调用很深层次的子对象的方法时,比较一下下面两种调用方法的方便程度:
Collection.Item [Index].SubCollection.Item [Index].SubsubCollection.Item [Index]
Collection [Index].SubCollection [Index].SubsubCollection [Index]
显然是后者要方便得多,实现缺省的属性语法支持同样非常方便,在类型库编辑器中,只要简单地标记Item [] 属性的dispid值为0 (DISPID_VALUE)就可以了。
因为缺省属性支持是基于dispids的,它只能在自动化接口中有作用。对于纯的虚方法表接口,不提供这方面的支持。
COM 组件分类
很多时候我们需要枚举一些功能类似的COM对象,例如假设想利用COM来提供插件的功能,那么宿主程序如何才能知道哪个COM对象可以作为插件呢?有没有什么标准的方法来实现COM识别呢?
在Windows 98/2000下可以通过组件分类来解决这个问题。简单地说,组件分类就是把实现一些通用功能的COM对象分为一组。客户端程序可以方便地确定要使用的COM对象。同其他COM对象类似,每个分类也要用一个唯一的标识符GUID来表示,这就是CATID (类别ID)。
Windows定义了ICatRegister和ICatInformation这两个接口来提供组件分类服务。实现了ICatRegister和ICatInformation接口组件的类GUID是CLSID_StdComponentCategoryMgr。我们可以使用ICatRegister接口的RegisterCategories方法来注册一个或多个类别。RegisterCategories方法需要两个参数,第一个参数确定有多少个类别将被注册,第二个参数是一个TCategoryInfo 类型的指针数组。TCategoryInfo声明如下:
TCATEGORYINFO = record
catid: TGUID; //类别 ID
lcid: UINT; //本地化 ID, 用于多语言支持
szDescription: array[0..127] of WideChar; //类别描述
end;
要想注册一个COM对象的类别,可以使用ICatRegister接口的RegisterClassImplCategories方法。RegisterClassImplCategories方法使用两个参数,一个是要注册的COM对象的CLSID,一个是要注册的类别数及类别记录(TcategoryInfo)的数组。对于客户端来说,为了扫描所有某一类别的COM对象,可以使用ICatInformation 接口的EnumClassesOfCategories方法。EnumClassesOfCategories方法需要五个参数,但通常只需要提供其中的三个参数就可以了,一个参数用来表明我们感兴趣的类别数,第二个参数是类别数组,最后一个参数是用来匹配COM对象的CLSID/GUID的枚举器。示意代码如下:
unit uhdshake;
interface
uses
Windows,
ActiveX,
ComObj;
type
TImplementedClasses = array [0..255] of TCLSID;
function GetImplementedClasses (var ImplementedClasses : TImplementedClasses) : integer;
procedure RegisterClassImplementation (const CATID, CLSID : TCLSID; const sDescription : String; bRegister : boolean);
implementation
function GetImplementedClasses (CategoryInfo : TCategoryInfo; var ImplementedClasses : TImplementedClasses) : integer;
var
CatInfo : ICatInformation;
Enum : IEnumGuid;
Fetched : UINT;
begin
Result := 0;
CatInfo := CreateComObject (CLSID_StdComponentCategoryMgr) as ICatInformation;
OleCheck (CatInfo.EnumClassesOfCategories (1, @CategoryInfo,0,nil,Enum));
if (Enum <> nil) then
begin
OleCheck (Enum.Reset);
OleCheck (Enum.Next (High (ImplementedClasses), ImplementedClasses [1], Fetched));
Result := Fetched;
end;
end;
procedure RegisterClassImplementation (const CATID, CLSID : TCLSID; const sDescription : String; bRegister : boolean);
var
CatReg : ICatRegister;
CategoryInfo : TCategoryInfo;
begin
CoInitialize (nil);
CategoryInfo.CATID := CATID;
CategoryInfo.LCID := LOCALE_SYSTEM_DEFAULT; //dummy
StringToWideChar(sDescription, CategoryInfo.szDescription, Length(sDescription) + 1);
CatReg := CreateComObject (CLSID_StdComponentCategoryMgr) as ICatRegister;
if (bRegister) then
begin
OleCheck (CatReg.RegisterCategories (1, @CategoryInfo));
OleCheck (CatReg.RegisterClassImplCategories (CLSID, 1, @CategoryInfo));
end
else
begin
OleCheck(CatReg.UnregisterClassImplCategories(CLSID,1,@CategoryInfo));
DeleteRegKey ('CLSID\' + GuidToString (CLSID) + '\' + 'Implemented Categories');
end;
CatReg := nil;
CoUninitialize;
end;
end.
客户端可以使用GetImplementedClasses方法来获得所有符合某一类别的COM对象的CLSID。注意这里使用TImplementedClasses 类型作为所有获得的CLSID的容器。TImplementedClasses 类型简单的定义为256个CLSID的数组,对于大多数情况来说已经足够了。封装的RegisterClassImplementation方法是用来按类别注册或撤消COM对象的。