zoukankan      html  css  js  c++  java
  • 常见COM问题解答

    如何初始化同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对象的。

  • 相关阅读:
    java的System.getProperty()方法可以获取的值
    python-namedtuple使用
    ML-KDTree思想、划分、实现
    基于栈的10亿数字快速排序
    python栈、队列的使用
    1.(字符串)-计算n个数count-and-say
    1.(字符串)-获取最后一个字符串及长度
    1.(字符串)-空格替换
    1.(字符串)-获取字符串的最长回文子串
    1.(字符串)-回文判断
  • 原文地址:https://www.cnblogs.com/MaxWoods/p/469970.html
Copyright © 2011-2022 走看看