2023年11月30日发(作者:)

软件重用是业界追求的目标人们一直希望能够像搭积木一样随意装配应用程

序,组件对象就充当了积木的角色。所谓组件对象,实际上就是预定义好的、能

完成一定功能的服务或接口。问题是,这些组件对象如何与应用程序、如何与其

他组件对象共存并相互通信和交互?这就需要制定?个规范,让这些组件对象按

统一的标准方式工作。

COM是个二进制规范,它与源代码无关。这样,即使COM对象由不同的

编程语言创建,运行在不同的进程空间和不同的操作系统平台,这些对象也能相

互通信COM既是规范也是实现它以COM(和贴)

的形式提供了访问COM对象核心功能的标准接口以及一组API函数,这些API

函数用于创建和管理COM对象。COM本质上仍然是客户服务器模式。客户(

常是应用程序)请求创建COM对象并通过COM对象的接口操纵COM对象。服

务器根据客户的请求创建并管理COM对象。客户和服务器这两种角色并不是绝

对的。

组件对象与一般意义上的对象既相似也有区别一般意义上的对象是一种把

数据和操纵数据的方法封装在一起的数据类型的实例,而组件对象则使用接口

(Interface)而不是方法来描述自己并提供服务。所谓接口,其精确定义是基于对

象的一组语义上相关的功能,实际上是一个纯虚类,真正实现接口的是接口对

)(Interface Object)。一个COM对象可以只有一个接口,例如Wndows 95

98外壳扩展;也可以有许多接口,例如ActiveX控件一般就有多个接口,客户

可以从很多方面来操纵ActiveX控件。接口是客户与服务器通信的唯一途径。如

果一个组件对象有多个接口,则通过一个接口不能直接访问其他接口。但是,

COM允许客户调用COM库中的QueryInterface()去查询组件对象所支持的其他

接口。从这个意义上讲,组件对象有点像接口对象的经纪人。

在调用QueryInterface()后,如果组件对象正好支持要查询的接口,则

QueryInterface()

QueryInterface()将返回一个出错信息。

所以,QueryInterface()是很有用的,它可以动态了解组件对象所支持的接

接口是团向对象编程思想的一种体现它隐藏了COM对象实现服务的细节

COM对象可以完全独立于访问它的客户,只要接口本身保持不变即可。如果需

要更新接口,则可以重新定义一个新的接口,对于使用老接口的客户来说,代码

得到了最大程度的保护。

Delphi通过向导可以非常迅速和方便的直接建立实现COM对象的代码但是整

COM实现的过程被完全的封装,甚至没有VCL那么结构清晰可见。

一个没有C++COM开发经验甚至没有接触过COM开发的Delphi程序员,

也能够很容易的按照教程设计一个接口,

但是,恐怕深入一想,连生成的代码代表何种意义,哪些能够定制都不清楚。前

几期 ―DELPHI下的COM编程技术一文已经初步介绍了COM的一些基本概

念,我则想谈一些个人的理解,希望能给对DelphiCOM编程有疑惑的朋友

带来帮助。

COM (组件对象模型 Component Object Model)是一个很庞大的体系。简单来

说,COM定义了一组API与一个二进制的标准,让来自不同平台、不同开发语

言的独立对象之间进行通信。COM对象只有方法和属性,并包含一个或多个接

口。这些接口实现了COM对象的功能,通过调用注册的COM对象的接口,能

够在不同平台间传递数据。

COM光标准和细节就可以出几本大书。这里避重就轻,仅仅初步的解释Delphi

如何进行COM的封装及实现对于上述COM技术经验不足的Delphi程序开发

者来说,

Delphi通过模版生成的代码就像是给你一幅抽象画照着画一样画出来了却不一

定知道画的究竟是什么,也不知该如何下手画自己的东西。本文能够帮助你解决

这类疑惑。

再次讲解一些概念

―DELPHI下的COM编程技术一文已经介绍了不少COM的概念,比如GUID

CLSIDIID,引用计数,IUnKnown接口等,下面再补充一些相关内容:

COMDCOMCOM+OLEActiveX的关系

DCOM(分布式COM)提供一种网络上访问其他机器的手段,是COM的网络

化扩展,可以远程创建及调用。COM+MicrosoftCOM进行了重要的更新

后推出的技术,但它不简单等于COM的升级,COM+是向后兼容的,但在某些

程度上具有和COM不同的特性,比如无状态的、事务控制、安全控制等等。

以前的OLE是用来描述建立在COM体系结构基础上的一整套技术,现在OLE

仅仅是指与对象连接及嵌入有关的技术;ActiveX则用来描述建立在COM基础

上的非COM技术,它的重要内容是自动化(Automation),自动化允许一个应

用程序(称为自动化控制器)操纵另一个应用程序或库(称为

自动化服务器)的对象,或者把应用程序元素暴露出来。

由此可见COM与以上的几种技术的关系,并且它们都是为了让对象能够跨开发

工具跨平台甚至跨网络的被使用

Delphi下的接口

Delphi中的接口概念类似C++中的纯虚类,又由于Delphi的类是单继承模式

C++是多继承的),即一个类只能有一个父类。接口在某种程度上

可以实现多继承。接口类的声明与一般类声明的不同是,它可以象多重继承那

样,类名 = class (接口类1,接口类2… ),然后被声明的接口类则重载继承类

的虚方法,来实现接口的功能。

以下是IInterfaceIUnknownIDispatch的声明,大家看出这几个重要接口之

间是什么样的联系了吗?任何一个COM对象的接口,最终都是从IUnknown

承的,而Automation对象,则还要包含IDispatch,后面DCOM部分我们会看

到它的作用。

IInterface = interface

[''''{00000000-0000-0000-C0046}'''']

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

end;

IUnknown = IInterface;

IDispatch = interface(IUnknown)

[''''{00020400-0000-0000-C0046}'''']

function GetTypeInfoCount(out Count: Integer): HResult; stdcall;

function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult;

stdcall;

function GetIDsOfNames(const IID: TGUID; Names: Pointer;NameCount,

LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;

function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;Flags:

Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;

end;

对照―DELPHI下的COM编程技术一文,可以明白IInterface中的定义,即接口

查询及引用记数,这也是访问和调用一个接口所必须的。

QueryInterface可以得到接口句柄AddRefRelease则负责登记调用次数

COM和接口的关系又是什么呢?COM通过接口进行组件、应用程序、客户和

服务器之间的通信。COM对象需要注册,而一个GUID则是作为识别接口的唯

一名字。

假如你创建了一个COM对象,它的声明类似 Txxxx= class(TComObject,

Ixxxx),前面是COM对象的基类,后面这个接口的声明则是: Ixxxx =

interface(IUnknown) 。所以说IUnknownDelphiCOM对象接

口类的祖先。到这一步,我想大家对接口类的来历已经有初步了解了。

聚合

接口是COM实现的基础,接口也是可继承的,但是接口并没有实现自己,仅仅

只有声明。那么怎么使COM对象对接口的实现得到重用呢?答案就是聚合。聚

合就是一个包含对象(外部对象)创建一个被包含对象(内部对象),这样内部

对象的接口就暴露给外部对象。

简单来说,COM对象被注册后,可以找到并调用接口。但接口不是仅仅有个定

义吗,它必然通过某种方式找到这个定义的实现,即接口的实现类的方法,这

样才最终通过外部的接口转入进行具体的操作,并通过接口返回执行结果。

进程内与进程外(In-Process, Out-Process

进程内的接口的实现基础是一个DLL进程外的接口则是建立在应用程序EXE

上的。通常我们建立进程外接口的目的主要是为了方便调试(跟踪DLL是件很

麻烦的事),然后在将代码改为进程内发布。因为进程内比进程外的执行效率会

高一些。(也就是先建立进程内的接口,再将其改为进程内发布。)

COM对象创建在服务器的进程空间。如果是EXE型服务器,那么服务器和客户

端不在同一进程;如果是DLL型服务器,则服务器和客户端就是一个进程。所

以进程内还能节省内存空间,并且减少创建实例的时间。

StdCallSafeCall

Delphi生成的COM接口默认的方法函数调用方式是stdcall而不是缺省的

Register。这是为了保证不同语言编译器的接口兼容。

双重接口(在后面讲解自动化时会提到双重接口)中则默认的是SafeCall。它的

意义除了按SafeCall约定方式调用外还将封装方法以便向调用者返回HResult

值。SafeCall的好处是能够捕获所有异常,即使是方法中未被代码处理的异常,

也可以被外套处理并通过HResult返回给调用者。

WideString等一些有差异的类型

接口定义中缺省的字符参数或返回值将不再是String而是WideString

WideString Delphi中符合OLE 32-bit版本的Unicode类型,当是字符

时,WideStringString几乎等同,当处理Unicode字符时,则会有很大差别。

联想到COM本身是为了跨平台使用,可以很容易的理解为什么数据通信时需要

使用WideString类型。

同样的道理,integer类型将变成SYSINT或者Int64SmallInt或者Shortint

这些细微的变化都是为了符合规范。

通过向导生成基础代码

打开创建新工程向导(菜单―File-New-Other‖―New Items按钮选择ActiveX

页。先建立一个ActiveX Library。编译后即是个DLL文件(进程内)。然后在

同样的页面再建立一个COM Object

接着你将看到如下向导,除了填写类名外(接口名会自动根据类名填充),创建

实例模式(Instancing)和线程模式(Threading Model的选项。

实例模式->>决定客户端请求后,COM对象如何创建实例:

Internal:供COM对象内部使用,不会响应客户端请求,只能通过COM对象内

部的其他方法来建立;

Single Instance:不论当前系统内部是否存在相同COM对象,都会建立一个新

的程序及独立的对象实例;

Mulitple Instance如果有多个相同的COM对象只会建立一个程序多个COM

对象的实例共享公共代码,并拥有自己的数据空间。

Single/ Mulitple Instance有各自的优点,Mulitple虽然节省了内存但更加费时。

Single模式需要更多的内存资源,而Mulitple模式需要更多的CPU资源,且

Single的实例响应请求的负荷较为平均。该参数应根据服务器的实际需求来考

虑。

线程模式有五种:

Single:仅单线程,处理简单,吞吐量最低;

ApartmentCOM程序多线程,COM对象处理请求单线程;

Free:一个COM对象的多个实例可以同时运行。吞吐量提高的同时,也要求对

COM对象进行必要的保护,以避免多个实例冲突;

Both:同时支持AartmentFree两种线程模式。

Neutral:只能在COM+下使用。

虽然FreeBoth的效率得到提高,但是要求较高的技巧以避免冲突(这是很

不容易调试的),所以一般建议使用Delphi的缺省方式

类型库编辑器(Type Library

假设我们建立一个叫做TSample的类和ISample的接口(如图),然后使用类

型库编辑器创建一个方法GetCOMInfo(在右边树部分点击右键弹出

菜单选择New-Method或者点击上方按钮),并于左边Parameters页面建立两

个参数(ValInt : Integer , ValStr : String),返回值为BSTR。如图:

可以看到,除了常用类型外,参数和返回值还可以支持很多指针、OLE对象、

接口类型。建立普通的COM对象,其Returen Type是可以任意的,

这是和DCOM的一个区别。

双击Modifier列弹出窗口,可以选择参数的方式:inout分别对应constout

定义,选择Has Default Value可设置参数缺省值。

Delphi生成代码详解

点击刷新按钮刷新后,上面类型库编辑器对应的Delphi自动生成的代码如下:

unit uCOM;

{$WARN SYMBOL_PLATFORM OFF}

interface

uses

Windows, ActiveX, Classes, ComObj, pCOM_TLB, StdVcl;

type

TSample = class(TTypedComObject, ISample)

protected

function GetCOMInfo(ValInt: SYSINT; const ValStr:

WideString): WideString;

stdcall;

end;

implementation

uses ComServ; //竟然会自动加上这个东西

function Info(ValInt: SYSINT;const ValStr: WideString):

WideString;

begin

end;

initialization

TTypedComObjectFactory.Create(ComServer, TSample, Class_Sample,

ciMultiInstance, tmApartment);

end.

引用单元

有三个特殊的单元被引用:ComObjComServpCOM_TLBComObj里定

义了COM接口类的父类TTypedComObject

TTypedComObjectFactoryTComObject

TComObjectFactory继承[加了Typed],早期版本如Delphi4建立的COM,就

直接从TcomObject继承和使用TComObjectFactory了); ComServ单元里

ComServer: TComServer[]

TComServerObject继承的,关于这个变量的作用,后面将会提到。

这几个类都是delphi实现COM对象的比较基础的类,TComObjectCOM

象类)和TComObjectFactoryCOM对象类工厂类)本身就是IUnknown

两个实现类,包含了一个COM对象的建立、查询、登记、注册等方面的代码。

TComServerObject则用来注册一个COM对象的服务信息。

接口定义说明

再看接口类定义TSample = class(TTypedComObject, ISample)。到这里,已经

可以通过涉及的父类的作用大致猜测到TSample是如何创建并注

册为一个标准的COM对象的了。那么接口ISample又是怎么来的呢?

pCOM_TLB单元是系统自动建立的其名称加上了_TLB它里面包含了ISample

= interface(IUnknown)的接口定义。前面提到过,所有COM接口都是从

IUnknown继承的。

在这个单元里我们还可以看到三种ID(类型库IDIIDCOM注册所必须的

CLSID的定义:LIBID_pCOMIID_ISampleCLASS_Sample。关键是

这时接口本身仅仅只有定义代码而没有任何的实现代码那接口创建又是在何处

执行的?_TLB单元里还有这样的代码:

CoSample = class

class function Create: ISample;

class function CreateRemote(const MachineName: string): ISample;

end;

class function : ISample;

begin

Result := CreateComObject(CLASS_Sample) as ISample;

end;

class function Remote(const MachineName: string):

ISample;

begin

Result := CreateRemoteComObject(MachineName, CLASS_Sample)

as ISample;

end;

Delphi的向导和类型编辑器帮助生成的接口定义代码,都会绑定一个Co+

的类,它实现了创建接口实例的代码。

CreateComObjectCreateRemoteComObject

函数在ComObj单元定义它们就是使用CLSID创建COM/DCOM对象的函数!

初始化:注册COM对象的类工厂

类工厂负责接口类的统一管理——实际上是由支持IClassFactory接口的对象来

管理的。类工厂类的继承关系如下:

IClassFactory = interface(IUnknown)

TComObjectFactory=class(TObject,IUnknown,IClassFactory,IClassFactory2)

TTypedComObjectFactory = class(TComObjectFactory)

我们知道了接口ISample是怎样被创建的,接口实现类TSample又是如何被定

义为COM对象的实现类。现在解释它是怎么被注册,以及何时创建的。

这一切的小把戏都在最后Delphi单元里的initialization的部分这里有一条类工

厂建立的语句。

InitializationDelphi用于初始化的特殊部分,此部分的代码将在整个程序启动

的时候首先执行。回顾前面的内容并观察一下TTypedComObjectFactory的参

数:

ComServer是用于注册/撤消注册COM服务的对象,TSample是接口实现

类,Class_Sample是接口唯一对应的GUIDciMultiInstance是实例模式,

tmApartment是线程模式。一个COM对象应该具备的特征和要素都包含在了里

面!】

那么COM对象的管理又是怎么实现的呢?在ComObj单元里面可以见到一条定

function ComClassManager: TComClassManager;

这里TComClassManager顾名思义就是COM对象的管理类。任何一个祖先类

TComObjectFactory的对象被建立时,其Create里面会执行这样一句:

ectFactory(Self);

AddObjectFactoryprocedure

ectFactory(Factory: TComObjectFactory);相对应

的还有

RemoveObjectFactory方法。具体的代码我就不贴出来了,相信大家已经猜测

到了它的作用——将当前对象(self)加入到ComClassManager管理的对象链

FFactoryList)中。

封装的秘密

读者应该还有最后一个疑问假如服务器通过类工厂的注册以及GUID确定一个

COM对象,那当客户端调用的时候,服务器是如何启动包含COM对象的程序

的呢?

当你建立ActiveX Library的工程的时候,将发现一个和普通DLL模版不同的地

——它定义了四个输出例程:

exports

DllGetClassObject,

DllCanUnloadNow,

DllRegisterServer,

DllUnregisterServer;

这四个例程并不是我们编写的,它们都在ComServ单元例实现。单元还定义了

TComServer,并且在初始化部分创建了类的实例,即前面提到过的全局变量

ComServer

例程DllGetClassObject通过CLSID得到支持IClassFactory接口的对象;例程

DllCanUnloadNow判断DLL是否可从内存卸载;DllRegisterServer

DllUnregisterServerDLL

ComServer实现。

接口类的具体实现

好了,现在自动生成代码的来龙去脉已经解释清楚了,下一步就是由我们来添加

接口方法的实现代码。在function Info的部分

添加如下代码。我写的例子很简单,仅仅是根据传递的参数组织一条字符串并返

回。以此证明接口正确调用并执行了该代码:

function Info(ValInt: SYSINT;const ValStr: WideString):

WideString;

const

Server1 = 1; Server2 = 2; Server3 = 3;

var

s : string;

begin

s := ''''This is COM server : '''';

case ValInt of

Server1: s := s + ''''Server1'''';

Server2: s := s + ''''Server2'''';

Server3: s := s + ''''Server3'''';

end;

s := s + #13 + #10 + ''''Execute client is '''' + ValStr;

Result := s;

end;

注册、创建COM对象及调用接口

随便建立一个Application用于测试上面的COM。必要的代码很少,创建一个接

口的实例然后执行它的方法。当然我们得先行注册COM,否则调用

根据CLSID找不接口的话,将报告无法向注册表写入项。如果接口定义不一

致,则会报告―Interface not supported‖

编译上面的这个COM工程,然后选择菜单Run Register ActiveX Server,或

者通过Windowssystem/system32目录中的程序注册编译好的

DLL文件regsvr32的具体参数可以通过regsvr32/?来获得对于进程外EXE

型)的COM对象,执行一次应用程序就注册了。

提示DLL注册成功后,就应该可以正确执行下列客户端程序了:

uses ComObj, pCOM_TLB;

procedure 1Click(Sender: TObject);

var

COMSvr : ISample;

retStr : string;

begin

COMSvr := CreateComObject(CLASS_Sample) as ISample;

if COMSvr <> nil then begin

retStr := Info(2,''''client 2'''');

showmessage(retStr);

COMSvr := nil;

end

else showmessage(''''接口创建不成功'''');

end;

最终值是从当前程序外的一个接口返回的,我们甚至可以不知道这个接口的实

现!第一次接触COM的人,成功执行此程序并弹出对话框后,

也许会体会到一种技术如斯奇妙的感觉,因为你仅仅调用了接口,就可以完成

你猜测中的东西。

创建一个分布式DCOM(自动化接口)

IDispatch

delphi6之前的版本中,所有接口的祖先都是IUnknown,后来为了避免跨平

台操作中接口概念的模糊,又引入了IInterface接口。

使用向导生成DCOM的步骤和COM几乎一致。而生成的代码仅将接口类的父

类换为TAutoObject,类工厂类换为TAutoObjectFactory。这其实没有

太大的不同,因为TAutoObject等于是一个标准COM外加IDispatch接口,而

TAutoObjectFactory是从TTypedComObjectFactory直接继承的:

TAutoObject = class(TTypedComObject, IDispatch)

TAutoObjectFactory = class(TTypedComObjectFactory)

自动化服务器支持双重接口,而且必须实现IDispatch。因讨论范畴限制,本文

只能简单提出,IDispatchDCOMCOM技术实现上的一个重要区

别。打开_单元,可以找到Ixxx = interface(IDispatch)Ixxx =

dispinterface的定义,这在前面COM的例子里面是没有的。

创建过程中的差异

使用类型库编辑器的时候,有两处和COM不同的地方。首先Return Type必须

选择HRESULT,否则会提示错误,这是为了满足双重接口的需要。当

Return Type选择HRESULT后,你会发现方法定义将变成procedure(过程)

而不是预想中的function(函数)。

怎么才能让方法有返回值呢?还需要在Parameters最后多添加一个参数,然后

将该参数改名与方法名一致,设置参数类型为指针(如果找不到

某种类型的指针类型,可以直接在类型后面加*,如图,BSTR*BSTR的指针

类型)。最后在Modifier列设置Parameter FlagsRetVal,同时

Out将被自动选中,而In将被取消。

刷新后,得到下列代码。添加方法的具体实现,大功告成:

TSampleAuto = class(TAutoObject, ISampleAuto)

protected

function GetAutoSerInfo(ValInt: SYSINT;const ValStr: WideString):

WideString; safecall;

end;

远程接口调用

远程接口的调用需要使用CreateRemoteComObject函数其它如接口的声明等

等与COM接口调用相同。CreateRemoteComObject函数比

CreateComObject 多了一个参数,即服务器的计算机名称,这样就比COM

出了远程调用的查询能力。前面接口定义说明一节的代码可以对

CreateComObjectCreateRemoteComObject的区别。

自定义COM的对象

接口一个重要的好处是:发布一个接口,可以不断更新其功能而不用升级客户

端。因为不论应用升级还是业务改变,客户端的调用方式都是一

致的。

既然我们已经弄清楚Delphi是怎样实现一个接口的,那能否不使用向导,自己

定义接口呢?这样做可以用一个接口继承出不同的接口实现类,

来完成不同的功能。同时也方便了小组开发、客户端开发、进程内/外同步编译

以及调试。

接口单元:xxx_

前面略讲了接口的定义需要注意的方面。接口除了没有实例化外,它与普通类还

有以下区别:接口中不能定义字段,所有属性的读写必须由方

法实现;接口没有构造和析构函数,所有成员都是public;接口内的方法不能定

义为virtualdynamicabstractoverride

首先我们要建立一接口。面讲过口的定义只存在于一个地方,即

xxx_单元里面。使用类型库编辑器可以产生这样一个单元。还是

在新建项目的ActiveX页,选择最后一个图标(Type Library)打开类型库编辑

器,按F12键就可以看到TLB文件(保存为.tlb)了。没有定义任

何接口的时候,TLB文件里除了一大段注释外只定义了LIBID(类型库的

GUID)。假如关闭了类型库编辑器也没有关系,可以随时通过菜单View

Type Library打开它。

先建立一个新接口(使用向导的话这步已经自动完成了),然后如前面操作一样

建立方法、属性生成的TLB文件内容与向导生成_TLB单元大致

相同,但仅有定义,缺乏―co+类名之类的接口创建代码。

再观察代码,将发现接口是从IDispatch继承的,必须将这里的IDispatch改为

IUnknown。保存将会得到.tlb文件,而我们想要的是一个单元

.pas文件仅仅为了声明接口所以把代码拷贝复制并保存到一个新的Unit

自定义CLSID

从注册和调用部分可以看出CLSID的重要作用。CLSID是一个GUID(全局唯

一接口表示符),用来标识对象。GUID是一个16个字节长的128位二进

制数据。Delphi声明一个GUID常量的语法是:

Class_XXXXX : TGUID = ''''{xxxxxxxx-xxxxx-xxxxx-xxxxx-xxxxxxxx}'''';

Delphi的编辑界面按Ctrl+Shift+G键可以自动生成等号后的数据串GUID

声明并不一定在_TLB单元里面,任何地方都可以声明并引用它。

接口类声明与实现

新建一个ActiveX Library工程,加入刚才定义的TLB单元,再新建一个Unit

我的TLB单元取名为MyDef_,定义了一个接口

IMyInterface = interface(IUnknown)以及一个方法function SampleMethod(val:

Smallint): SYSINT; safecall;现在让我们看看全部接口

类声明及实现的代码:

unit uMyDefCOM;

interface

uses ComObj, Comserv, ActiveX, MyDef_TLB;

const

Class_MySvr : TGUID =

''''{1C0E5D5A-B824-44A4-AF6C-478363581D43}'''';

type

TMyIClass = class(TComObject, IMyInterface)

procedure Initialize; override;

destructor Destroy; override;

private

FInitVal : word;

inherited;

end;

function Method(val: Smallint): SYSINT;

begin

Result := val + FInitVal;

end;

{ TMySvrFactory }

procedure Registry(Register: Boolean);

begin

inherited;

if Register then begin

CreateRegKey(''''MyApp''''+ClassName, ''''GUID'''',

GUIDToString(Class_MySvr));

end else begin

DeleteRegKey(''''MyApp''''+ClassName);

procedure Initialize是接口的初始化过程,而不是常见的Create方法。当客户

端创建接口后,将首先执行里面的代码,与Create的作用一样

。一个COM对象的生存周期内,难免需要初始化类成员或者设置变量的初值,

所以经常需要重载这个过程。

相对应的,destructor Destroy则和类的标准析构过程一样,作用也相同。

类工厂注册

在代码的最后部分,假如使用TComObjectFactory来注册,就和前面所讲的完

全一样了。我在这里刻意用类TMySvrFactory继承了一次,并且重

载了UpdateRegistry 方法,以便向注册表中写入额外的内容。这是种小技巧,

希望大家根据本文的思路,摸清COM/DCOM对象的Delphi实现结构

后,可以举一反三。毕竟随心所欲的控制COM对象,能提供的功能远不如此。

Delphi编写COM+简介(转)

学习 2008-03-28 16:47:49 阅读923 评论0 字号:大中小

(1) idispatchCOM object的接口, Delphi中通常指一个OleObject.

(2) OleVariant是一种COM object兼容的Variant类型, 可以通用任何

Ole Automation 类型, 他与CreateOleObject创建的idispatch兼容

1:Com+的编写:

1:File---->---->ActiveX Library

Transactional Object

2: : CoClss Name :,:ComPlus Threading

Modal :线程模式:Apartment 选项: Supports transactions

3:然后在View--->Library的对话框中增加方法 注意:如果参数为输出的

,则类型要是指针型,比如:Long * ,然后修改后面的参数in :out,ret

4:最后完善增加的方法就ok

2:客户端调用的编写:

1:先倒入Com+的接口类型. Project --->import Type Library---->选中你

编写的Com+的类型,然后选择:Create Unit

3、安装COM+组件有两种方式,

第一种(推荐)如果是在IDE环境里,点击―Run->Install COM+ Objects‖

即可把打开的Active Library项目安装到COM+环境中,注意:如果打开的项目

一个普通的Application项目,是不能被安装到COM+环境中的。 将要安装

com+打上勾,然后在application中有两个选项:install to existing application :

表示你的com+安装在com服务器的哪个组件包中, install to New application:

表示将当前com+组件安装到一个新的组件包中.

第二种办法:打开控制面板-> 管理工具->组件服务->计算机->我的电脑

->COM+应用程序,在COM+应用程序的树项上点击 鼠标右键,选择新建->

用程序->创建一个空的应用程序,并为此应用程序命名,接下 来点击下一步

直到结束即可。建立了空的COM+应用程序后,接下来就是把COM DLL安装

COM+应用程序中了。在刚建立的空应用程序的树项中新建一个组件,选择

装新组件 在打开文件对话框中选择要安装到COM+环境中的DLL文件,之

后跟着向导做都可以了,要把 多个COM DLL安装到同一个COM+应用程序包

中,只需重复以上步骤即可。

4导出客户端组件包指的是把已经注册的组件导出为.msi格式的文件,

这些文件在客 户端安装后,只会在客户端注册组件,而不会安装多余的文件。

如果不在客户端注册组件, 是不不能调用位于服务器上的组件的(此指服务器

和客户端分布在不同的机器上时)。

5:调试Com+程序 ---ok

1.打开Windows中的组件管理,找到要调试的组件包,点右键,选择属性,

;

/processID:{xxxxxxxxxxxxxxxxxxxxxxxxxxxxx} 复制出来

2.dephiRun | Parameters… HOST APPLICATION 填入 {系统路

} PARAMETERS

/processID:{xxxxxxxxxxxxxxxxxxxxxxxxxxxxx}

3。很关键的一点:组件程序:project|option|linker|Include TD32 debug

info Include remote debug symbols打勾

4.启动delphi,运行要调试的Com+程序,设置断点,然后运行客户端程序

即可进入到Com+断点.

5.调试完后记得要在 Windows中的组件管理中的高级这页里调试选项

勾去掉哟.

6:Com+需要注意的地方:

1:客户机运行就会报 interface not supported 错误 大致原因:轻舞肥羊

(2004-05-09 11:00:01) COM+的权限依赖于Windows的权限配置,在服务器需

要有客户机的用户名和密码。 如果还不行,就在服务器上重新安装com+,重新导

.

2:建立工程时,com+不能包含在工程组中.(我的实践)

3:COM+不支持Oracle吗?在用事务的时候出错:Using Oracle with

Microsoft Transaction Server and COM+

7:Com+中添加远程数据模块

1:File---->---->Multitier 标签 下的 Transactional Data

Module

2:然后在View--->Library的对话框中增加方法.

8:Com+中传递数组

先定义数组:

type

TDataRecord = record

A: Byte;

B: LongWord;

C: Word;

D: LongWord;

end;

1:server: function GetData: OleVariant;

var

P: Pointer;

begin

VarClear(Result);//不知D5有没。

try

Result := VarArrayCreate([0, SizeOf(TDataType)], varByte);

P := VarArrayLock(Result); //Data: TDataType为你要传的记录类型

Move(Data, P^, SizeOf(TDataType));

finally

VarArrayUnLock(Result);

end;

end

2:Client

procedure GetFile(const FileName: string);

var

P: Pointer;

V: OleVariant;

Data: TDataType;

begin

FillChar(Data, SizeOf(Data), 0);

V := a;

try

P := VarArrayLock(V);

Move(P^, Data, SizeOf(Data));

finally

VarArrayUnLock(V);

end;

end;

9:Com+中传递记录集 下面xeen 实验成功

uses

ADOInt*****************************************************************************

可以;你可以将ADO的数据作为一个Variant类型的变量进行传送:

Set这是原生的ado数据 这是服务端的一个方法的代码:

CodeSetVariant*[in,out] function a:

OleVariant; begin ; result := Set;

end;

**************************************************************************************

客户端

uses adoint;

var

MyRecordset :_recordset;

begin

MyRecordset := IUnknown(CodeSet) as _recordset

**************************************************************************************

************************************ Com基本概念:

1:COM是一个基于二进制的标准。打个比方,我们用Delphi实现了一个

对象,一般情况下,我们只能在Delphi来生成这个对象的实例并调用,而如果

我们用Delphi实现了一个COM对象的话,我们可以用VCVB或者其他任何

一种支持COM对象的语言来生成实例和调用。反过来也一样,我们可以在Delphi

中使用各种COM对象,而不用介意它是用什么语言编写的。COM提供了分布

COM对象的机制,形象地说你可以调用另一台机器中的COM对象。COM+

则是MTS的一个升级,COM的基础上进一步提供了事务处理和其他很多Pool

技术。

2:线程模式:Apartment:多个线程服务.

3:当建立Com+时选择的事务模式为Requires a Transaction,Com+会根

据客户的的请求建立相应的事务,不仅仅时数据库,还会有系统资源等事务.

SetComplete.回滚SetAbort. 选择Requires a Transaction表示当用户调用这

COM+组件时,COM+环境会为这个组件建立一个新的事务上下文,这和数据

库的事务不是一回事。当你的COM+组件提交数据时如果出错,应该告诉事务

上下文,只要调用COM+组件的SetAbort方法就可以。这样一来,处于同一个

事务上下文的所有COM+组件都会Rollback。如果数据提交成功,应该调用

SetComplete,不调用这个方法也可以,因为在默认情况下,COM+组件的事务

状态设置为EnableCommite。当处于同一事务上下文的所有COM+组件对象都

调用了SetComplete时,该事务上下文才会真正的向数据库提交数据。

4:SetAbortSetComplete是否正确调用

5:(阿朱) 建议:多个DLL在一个包,一个DLL中的COM公用一个

ADOCONNECTION

6:问题:我已经在Transactional Data ModulePooled属性里面设置了

True了,但是在Win2000的组件服务管理的组件属性激活一栏里面,仍然

无法打开启用对象共用的选项 --->您可以将线程模式设置为tmNeutral或者

tmBoth都可以。

Com有需要研究和有疑问的地方:

1:Com+的模式:

2:Com+的资源Pooling机制

3:delphi6 开发Com+,Neutral模式,尽量用Object Pooling,当然

就是要无状态了,事务要尽量短,避免死锁,用ADO不要用ess

.

4:我的做法是,按功能划分组件,把查询和更新分为两个功能组件,因

为查询不需要事务,所以只要支持事务就行了。更新一般需要事务

5:COM+的事务属性 COM+的事务默认级别是序列 Read,而不是我们

认为的commit Read,而且我们设置AdoConnection的隔离级是不 管用的,只

能强制用显式的SQL才能起效果。 例如: 一个中间层COM select * from a

where py_code like 'S%' 继续下面有很多代码 另一个中间层COM update a

set py_code=py_code where py_code like 'B%' 这时这个COM将会死锁等待,

虽然改动的不是一个数据也会死锁 Commit Read UnCommit Read不会

建议: 所以被多个子系统更新或读的表要在最后打开和更新,不要早。另外可

以强制改变ADOCONNECTION的隔离级

6:Com+中如何进行事务处理??? COM+,如何使用SetComplete

SetAbort进行事务管理?? 老兄,SetCompleteSetAbort怎么能在客户端

调用呢?这样肯定是不行的。要保证事务,就在服务器端声明一个专门保存的接

口方法,例如: procedure UpdateMyData(var AData1, AData2: OleVariant; var

AMaxError, AErrorCount: Integer); begin try :=

AData1; pdate(AMaxError, AErrorCount);

:= AData2;

pdate(AMaxError, AErrorCount); SetComplete;

except SetAbort; end; end;

7:在组件管理器中的事务列表里也看不到事务(应该有事务的时候),

怎么看?

8:Com+的两大研究:事务 pooling

9:做一个提交用的COM+对象和一个协调用的COM+对象 ???

Delphi通过向导可以非常迅速和方便的直接建立实现COM对象的代码,但是整

COM实现的过程被完全的封装,甚至没有VCL那么结构清晰可见。

一个没有C++COM开发经验甚至没有接触过COM开发的Delphi程序员,也能

够很容易的按照教程设计一个接口,但是,恐怕深入一想,连生成的

代码代表何种意义,哪些能够定制都不清楚。前几期 “DELPHI下的COM编程技

术”一文已经初步介绍了COM的一些基本概念,我则想谈一些个人

的理解,希望能给对DelphiCOM编程有疑惑的朋友带来帮助。

COM (组件对象模型 Component Object Model)是一个很庞大的体系。简单来说,

COM定义了一组API与一个二进制的标准,让来自不同平台、不

同开发语言的独立对象之间进行通信。COM对象只有方法和属性,并包含一个或

多个接口。这些接口实现了COM对象的功能,通过调用注册的COM

对象的接口,能够在不同平台间传递数据。

COM光标准和细节就可以出几本大书。这里避重就轻,仅仅初步的解释Delphi

如何进行COM的封装及实现。对于上述COM技术经验不足的Delphi

序开发者来说,Delphi通过模版生成的代码就像是给你一幅抽象画照着画一样,

画出来了却不一定知道画的究竟是什么,也不知该如何下手画

自己的东西。本文能够帮助你解决这类疑惑。

再次讲解一些概念

“DELPHI下的COM编程技术”一文已经介绍了不少COM的概念,比如GUID

CLSIDIID,引用计数,IUnKnown接口等,下面再补充一些相关内容

COMDCOMCOM+OLEActiveX的关系

DCOM(分布式COM提供一种网络上访问其他机器的手段,COM的网络化扩展,

可以远程创建及调用。COM+MicrosoftCOM进行了重要的更

新后推出的技术,但它不简单等于COM的升级,COM+是向后兼容的,但在某些程

度上具有和COM不同的特性,比如无状态的、事务控制、安全控

制等等。

以前的OLE是用来描述建立在COM体系结构基础上的一整套技术,现在OLE仅仅

是指与对象连接及嵌入有关的技术;ActiveX则用来描述建立在COM

基础上的非COM技术,它的重要内容是自动化(Automation),自动化允许一个

应用程序(称为自动化控制器)操纵另一个应用程序或库(称为

自动化服务器)的对象,或者把应用程序元素暴露出来。

由此可见COM与以上的几种技术的关系,并且它们都是为了让对象能够跨开发工

具跨平台甚至跨网络的被使用。

Delphi下的接口

Delphi中的接口概念类似C++中的纯虚类,又由于Delphi的类是单继承模式C++

是多继承的),即一个类只能有一个父类。接口在某种程度上

可以实现多继承。接口类的声明与一般类声明的不同是,它可以象多重继承那样,

类名 = class (接口类1,接口类2„ ),然后被声明的接口

类则重载继承类的虚方法,来实现接口的功能。

以下是IInterfaceIUnknownIDispatch的声明,大家看出这几个重要接口之

间是什么样的联系了吗?任何一个COM对象的接口,最终都是从

IUnknown继承的,Automation对象,则还要包含IDispatch后面DCOM部分

我们会看到它的作用。

IInterface = interface

[''''{00000000-0000-0000-C0046}'''']

function QueryInterface(const IID: TGUID; out Obj): HResult;

stdcall;

function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

end;

IUnknown = IInterface;

IDispatch = interface(IUnknown)

[''''{00020400-0000-0000-C0046}'''']

function GetTypeInfoCount(out Count: Integer): HResult; stdcall;

function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo):

HResult; stdcall;

function GetIDsOfNames(const IID: TGUID; Names: Pointer;

NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;

function Invoke(DispID: Integer; const IID: TGUID; LocaleID:

Integer;

Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer):

HResult; stdcall;

end;

对照“DELPHI下的COM编程技术”一文,可以明白IInterface中的定义,即接

口查询及引用记数,这也是访问和调用一个接口所必须的。

QueryInterface可以得到接口句柄,AddRefRelease则负责登记调用次数。

COM和接口的关系又是什么呢?COM通过接口进行组件、应用程序、客户和服务

器之间的通信。COM对象需要注册,而一个GUID则是作为识别接口

的唯一名字。

假如你创建了一个COM对象,它的声明类似 Txxxx= class(TComObject, Ixxxx)

前面是COM对象的基类,后面这个接口的声明则是:Ixxxx =

interface(IUnknown)。所以说IUnknownDelphiCOM对象接口类的祖先。

到这一步,我想大家对接口类的来历已经有初步了解了。

聚合

接口是COM实现的基础,接口也是可继承的,但是接口并没有实现自己,仅仅只

有声明。那么怎么使COM对象对接口的实现得到重用呢?答案就

是聚合。聚合就是一个包含对象(外部对象)创建一个被包含对象(内部对象)

这样内部对象的接口就暴露给外部对象。

简单来说,COM对象被注册后,可以找到并调用接口。但接口不是仅仅有个定义

吗,它必然通过某种方式找到这个定义的实现,即接口的“实现

类”的方法,这样才最终通过外部的接口转入进行具体的操作,并通过接口返回

执行结果。

进程内与进程外(In-Process, Out-Process

进程内的接口的实现基础是一个DLL,进程外的接口则是建立在应用程序(EXE

上的。通常我们建立进程外接口的目的主要是为了方便调试(

跟踪DLL是件很麻烦的事)然后在将代码改为进程内发布。因为进程内比进程

外的执行效率会高一些。

COM对象创建在服务器的进程空间。如果是EXE型服务器,那么服务器和客户端

不在同一进程;如果是DLL型服务器,则服务器和客户端就是一个

进程。所以进程内还能节省内存空间,并且减少创建实例的时间。

StdCallSafeCall

Delphi生成的COM接口默认的方法函数调用方式是stdcall而不是缺省的

Register。这是为了保证不同语言编译器的接口兼容。

双重接口(在后面讲解自动化时会提到双重接口)中则默认的是SafeCall。它

的意义除了按SafeCall约定方式调用外,还将封装方法以便向调

用者返回HResult值。SafeCall的好处是能够捕获所有异常,即使是方法中未

被代码处理的异常,也可以被外套处理并通过HResult返回给调用

者。

WideString等一些有差异的类型

接口定义中缺省的字符参数或返回值将不再是String而是WideString

WideString Delphi中符合OLE 32-bit版本的Unicode类型,当是字符

时,WideStringString几乎等同,当处理Unicode字符时,则会有很大差别。

联想到COM本身是为了跨平台使用,可以很容易的理解为什么数

据通信时需要使用WideString类型。

同样的道理,integer类型将变成SYSINT或者Int64SmallInt或者Shortint

这些细微的变化都是为了符合规范。

通过向导生成基础代码

打开创建新工程向导(菜单“File-New-Other”或“New Items按钮”),选择

ActiveX页。先建立一个ActiveX Library。编译后即是个DLL

件(进程内)。然后在同样的页面再建立一个COM Object

实例模式与线程模式

接着你将看到如下向导,除了填写类名外(接口名会自动根据类名填充),还有

实例创建方式(Instancing)和线程模式(Threading Model

的选项。

实例模式决定客户端请求后,COM对象如何创建实例:

InternalCOM对象内部使用,不会响应客户端请求,只能通过COM对象内部

的其他

方法来建立;

Single Instance:不论当前系统内部是否存在相同COM对象,都会建立一个新

的程序

及独立的对象实例;

Mulitple Instance如果有多个相同的COM对象,只会建立一个程序,多个COM

对象

的实例共享公共代码,并拥有自己的数据空间。

Single/ Mulitple Instance有各自的优点,Mulitple虽然节省了内存但更加费

时。即Single模式需要更多的内存资源,而Mulitple模式需要更

多的CPU资源,且Single的实例响应请求的负荷较为平均。该参数应根据服务

器的实际需求来考虑。

线程模式有五种:

Single:仅单线程,处理简单,吞吐量最低;

ApartmentCOM程序多线程,COM对象处理请求单线程;

Free:一个COM对象的多个实例可以同时运行。吞吐量提高的同时,也要求对

COM对象

进行必要的保护,以避免多个实例冲突;

Both:同时支持AartmentFree两种线程模式。

Neutral:只能在COM+下使用。

虽然FreeBoth的效率得到提高,但是要求较高的技巧以避免冲突(这是很不

容易调试的),所以一般建议使用Delphi的缺省方式。

类型库编辑器(Type Library

假设我们建立一个叫做TSample的类和ISample的接口(如图)然后使用类型

库编辑器创建一个方法GetCOMInfo(在右边树部分点击右键弹出

菜单选择New-Method或者点击上方按钮)并于左边Parameters页面建立两个

参数(ValInt : Integer , ValStr : String),返回值为BSTR

。如图:

可以看到,除了常用类型外,参数和返回值还可以支持很多指针、OLE对象、接

口类型。建立普通的COM对象,其Returen Type是可以任意的,

这是和DCOM的一个区别。

双击Modifier列弹出窗口,可以选择参数的方式:inout分别对应constout

定义,选择Has Default Value可设置参数缺省值。

Delphi生成代码详解

点击刷新按钮刷新后,上面类型库编辑器对应的Delphi自动生成的代码如下:

unit uCOM;

{$WARN SYMBOL_PLATFORM OFF}

interface

uses

Windows, ActiveX, Classes, ComObj, pCOM_TLB, StdVcl;

type

TSample = class(TTypedComObject, ISample)

protected

function GetCOMInfo(ValInt: SYSINT; const ValStr: WideString):

WideString;

stdcall;

end;

implementation

uses ComServ;

function Info(ValInt: SYSINT;const ValStr: WideString):

WideString;

begin

end;

initialization

(ComServer, TSample, Class_Sample,

ciMultiInstance, tmApartment);

end.

引用单元

有三个特殊的单元被引用:ComObjComServpCOM_TLBComObj里定义了COM

接口类的父类TTypedComObject和类工厂类

TTypedComObjectFactory(分别从TComObjectTComObjectFactory继承,早

期版本如Delphi4建立的COM,就直接从TcomObject继承和使用

TComObjectFactory了); ComServ单元里面定义了全局变量ComServer:

TComServer,它是从TComServerObject继承的,关于这个变量的作用

,后面将会提到。

这几个类都是delphi实现COM对象的比较基础的类,TComObjectCOM对象类)

TComObjectFactoryCOM对象类工厂类)本身就是IUnknown

两个实现类,包含了一个COM对象的建立、查询、登记、注册等方面的代码。

TComServerObject则用来注册一个COM对象的服务信息。

接口定义说明

再看接口类定义TSample = class(TTypedComObject, ISample)。到这里,已经

可以通过涉及的父类的作用大致猜测到TSample是如何创建并注

册为一个标准的COM对象的了。那么接口ISample又是怎么来的呢?pCOM_TLB

单元是系统自动建立的,其名称加上了_TLB,它里面包含了ISample

= interface(IUnknown)的接口定义。前面提到过,所有COM接口都是从IUnknown

继承的。

在这个单元里我们还可以看到三种ID(类型库IDIIDCOM注册所必须的

CLSID)的定义:LIBID_pCOMIID_ISampleCLASS_Sample。关键是

这时接口本身仅仅只有定义代码而没有任何的实现代码,那接口创建又是在何处

执行的?_TLB单元里还有这样的代码:

CoSample = class

class function Create: ISample;

class function CreateRemote(const MachineName: string): ISample;

end;

class function : ISample;

begin

Result := CreateComObject(CLASS_Sample) as ISample;

end;

class function Remote(const MachineName: string):

ISample;

begin

Result := CreateRemoteComObject(MachineName, CLASS_Sample) as ISample;

end;

Delphi的向导和类型编辑器帮助生成的接口定义代码,都会绑定一个“Co+

类名”的类,它实现了创建接口实例的代码。CreateComObject

CreateRemoteComObject函数在ComObj单元定义,它们就是使用CLSID创建

COM/DCOM对象的函数!

初始化:注册COM对象的类工厂

类工厂负责接口类的统一管理——实际上是由支持IClassFactory接口的对象

来管理的。类工厂类的继承关系如下:

IClassFactory = interface(IUnknown)

TComObjectFactory=class(TObject,IUnknown,IClassFactory,IClassFactory2)

TTypedComObjectFactory = class(TComObjectFactory)

我们知道了接口ISample是怎样被创建的,接口实现类TSample又是如何被定义

COM对象的实现类。现在解释它是怎么被注册,以及何时创建的

。这一切的小把戏都在最后initialization的部分,这里有一条类工厂建立的

语句。

InitializationDelphi用于初始化的特殊部分,此部分的代码将在整个程序

启动的时候首先执行。回顾前面的内容并观察一下

TTypedComObjectFactory的参数:ComServer是用于注册/撤消注册COM服务的

对象,TSample是接口实现类,Class_Sample是接口唯一对应的

GUIDciMultiInstance是实例模式,tmApartment是线程模式。一个COM对象

应该具备的特征和要素都包含在了里面!

那么COM对象的管理又是怎么实现的呢?在ComObj单元里面可以见到一条定义

function ComClassManager: TComClassManager;

这里TComClassManager顾名思义就是COM对象的管理类。任何一个祖先类为

TComObjectFactory的对象被建立时,其Create里面会执行这样一句

ectFactory(Self);

AddObjectFactoryprocedure

ectFactory(Factory: TComObjectFactory);

的还有

RemoveObjectFactory方法。具体的代码我就不贴出来了,相信大家已经猜测到

了它的作用——将当前对象(self)加入到ComClassManager

理的对象链(FFactoryList)中。

封装的秘密

读者应该还有最后一个疑问:假如服务器通过类工厂的注册以及GUID确定一个

COM对象,那当客户端调用的时候,服务器是如何启动包含COM

象的程序的呢?

当你建立ActiveX Library的工程的时候,将发现一个和普通DLL模版不同的地

方——它定义了四个输出例程:

exports

DllGetClassObject,

DllCanUnloadNow,

DllRegisterServer,

DllUnregisterServer;

这四个例程并不是我们编写的,它们都在ComServ单元例实现。单元还定义了类

TComServer,并且在初始化部分创建了类的实例,即前面提到过

的全局变量ComServer

例程DllGetClassObject通过CLSID得到支持IClassFactory接口的对象;例程

DllCanUnloadNow判断DLL是否可从内存卸载;DllRegisterServer

DllUnregisterServerDLL

ComServer实现。

接口类的具体实现

好了,现在自动生成代码的来龙去脉已经解释清楚了,下一步就是由我们来添加

接口方法的实现代码。在function Info的部分

添加如下代码。我写的例子很简单,仅仅是根据传递的参数组织一条字符串并返

回。以此证明接口正确调用并执行了该代码:

function Info(ValInt: SYSINT;const ValStr: WideString):

WideString;

const

Server1 = 1; Server2 = 2; Server3 = 3;

var

s : string;

begin

s := ''''This is COM server : '''';

case ValInt of

Server1: s := s + ''''Server1'''';

Server2: s := s + ''''Server2'''';

Server3: s := s + ''''Server3'''';

end;

s := s + #13 + #10 + ''''Execute client is '''' + ValStr;

Result := s;

end;

注册、创建COM对象及调用接口

随便建立一个Application用于测试上面的COM。必要的代码很少,创建一个接

口的实例然后执行它的方法。当然我们得先行注册COM,否则调用

根据CLSID找不接口的话,将报告“无法向注册表写入项”。如果接口定义不一

致,则会报告“Interface not supported”。

编译上面的这个COM工程,然后选择菜单“Run Register ActiveX Server”,

或者通过Windowssystem/system32目录中的

序注册编译好的DLL文件。regsvr32的具体参数可以通过regsvr32/?来获得。

对于进程外(EXE型)的COM对象,执行一次应用程序就注册了。

提示DLL注册成功后,就应该可以正确执行下列客户端程序了:

uses ComObj, pCOM_TLB;

procedure 1Click(Sender: TObject);

var

COMSvr : ISample;

retStr : string;

begin

COMSvr := CreateComObject(CLASS_Sample) as ISample;

if COMSvr <> nil then begin

retStr := Info(2,''''client 2'''');

showmessage(retStr);

COMSvr := nil;

end

else showmessage(''''接口创建不成功'''');

end;

最终值是从当前程序外的一个“接口”返回的,我们甚至可以不知道这个接口的

实现!第一次接触COM的人,成功执行此程序并弹出对话框后,

也许会体会到一种技术如斯奇妙的感觉,因为你仅仅调用了“接口”,就可以完

成你猜测中的东西。

创建一个分布式DCOM(自动化接口)

IDispatch

delphi6之前的版本中,所有接口的祖先都是IUnknown,后来为了避免跨平

台操作中接口概念的模糊,又引入了IInterface接口。

使用向导生成DCOM的步骤和COM几乎一致。而生成的代码仅将接口类的父类换

TAutoObject,类工厂类换为TAutoObjectFactory。这其实没有

太大的不同,因为TAutoObject等于是一个标准COM外加IDispatch接口,而

TAutoObjectFactory是从TTypedComObjectFactory直接继承的:

TAutoObject = class(TTypedComObject, IDispatch)

TAutoObjectFactory = class(TTypedComObjectFactory)

自动化服务器支持双重接口,而且必须实现IDispatch。因讨论范畴限制,本文

只能简单提出,IDispatchDCOMCOM技术实现上的一个重要区

别。打开_单元,可以找到Ixxx = interface(IDispatch)Ixxx =

dispinterface的定义,这在前面COM的例子里面是没有的。

创建过程中的差异

使用类型库编辑器的时候,有两处和COM不同的地方。首先Return Type必须选

HRESULT,否则会提示错误,这是为了满足双重接口的需要。当

Return Type选择HRESULT后,你会发现方法定义将变成procedure(过程)而

不是预想中的function(函数)。

怎么才能让方法有返回值呢?还需要在Parameters最后多添加一个参数,然后

将该参数改名与方法名一致,设置参数类型为指针(如果找不到

某种类型的指针类型,可以直接在类型后面加*,如图,BSTR*BSTR的指针类

型)。最后在Modifier列设置Parameter FlagsRetVal,同时

Out将被自动选中,而In将被取消。

刷新后,得到下列代码。添加方法的具体实现,大功告成:

TSampleAuto = class(TAutoObject, ISampleAuto)

protected

function GetAutoSerInfo(ValInt: SYSINT;const ValStr: WideString):

WideString; safecall;

end;

远程接口调用

远程接口的调用需要使用CreateRemoteComObject函数,其它如接口的声明等等

COM接口调用相同。CreateRemoteComObject函数比

CreateComObject 多了一个参数,即服务器的计算机名称,这样就比COM多出了

远程调用的查询能力。前面“接口定义说明”一节的代码可以对

CreateComObjectCreateRemoteComObject的区别。

自定义COM的对象

接口一个重要的好处是:发布一个接口,可以不断更新其功能而不用升级客户端。

因为不论应用升级还是业务改变,客户端的调用方式都是一

致的。

既然我们已经弄清楚Delphi是怎样实现一个接口的,那能否不使用向导,自己

定义接口呢?这样做可以用一个接口继承出不同的接口实现类,

来完成不同的功能。同时也方便了小组开发、客户端开发、进程内/外同步编译

以及调试。

接口单元:xxx_

前面略讲了接口的定义需要注意的方面。接口除了没有实例化外,它与普通类还

有以下区别:接口中不能定义字段,所有属性的读写必须由方

法实现;接口没有构造和析构函数,所有成员都是public;接口内的方法不能

定义为virtualdynamicabstractoverride

首先我们要建个接口前面讲接口的定义只存在于一个地方,即

xxx_单元里面。使用类型库编辑器可以产生这样一个单元。还是

在新建项目的ActiveX页,选择最后一个图标(Type Library)打开类型库编辑

器,按F12键就可以看到TLB文件(保存为.tlb)了。没有定义任

何接口的时候,TLB文件里除了一大段注释外只定义了LIBID(类型库的GUID

假如关闭了类型库编辑器也没有关系,可以随时通过菜单View

Type Library打开它。

先建立一个新接口(使用向导的话这步已经自动完成了)然后如前面操作一样

建立方法、属性„生成的TLB文件内容与向导生成_TLB单元大致

相同,但仅有定义,缺乏“co+类名”之类的接口创建代码。

再观察代码,将发现接口是从IDispatch继承的,必须将这里的IDispatch改为

IUnknown。保存将会得到.tlb文件,而我们想要的是一个单元

.pas文件,仅仅为了声明接口,所以把代码拷贝复制并保存到一个新的Unit

自定义CLSID

从注册和调用部分可以看出CLSID的重要作用。CLSID是一个GUID(全局唯一接

口表示符),用来标识对象。GUID是一个16个字节长的128位二进

制数据。Delphi声明一个GUID常量的语法是:

Class_XXXXX : TGUID = ''''{xxxxxxxx-xxxxx-xxxxx-xxxxx-xxxxxxxx}'''';

Delphi的编辑界面按Ctrl+Shift+G键可以自动生成等号后的数据串。GUID

的声明并不一定在_TLB单元里面,任何地方都可以声明并引用它。

接口类声明与实现

新建一个ActiveX Library工程,加入刚才定义的TLB单元,再新建一个Unit

我的TLB单元取名为MyDef_,定义了一个接口

IMyInterface = interface(IUnknown)function

SampleMethod(val: Smallint): SYSINT; safecall;现在让我们看看全部接口

类声明及实现的代码:

unit uMyDefCOM;

interface

uses

ComObj, Comserv, ActiveX, MyDef_TLB;

const

Class_MySvr : TGUID = ''''{1C0E5D5A-B824-44A4-AF6C-478363581D43}'''';

type

TMyIClass = class(TComObject, IMyInterface)

procedure Initialize; override;

destructor Destroy; override;

private

procedure Registry(Register: Boolean);

begin

inherited;

if Register then begin

CreateRegKey(''''MyApp''''+ClassName, ''''GUID'''',

GUIDToString(Class_MySvr));

end else begin

DeleteRegKey(''''MyApp''''+ClassName);

end;

end;

initialization

(ComServer, TMyIClass, Class_MySvr,

''''MySvr'''', '''''''', ciMultiInstance, tmApartment);

end.

Class_MySvr是自定义的CLSIDTMyIClass是接口实现类,TMySvrFactory是类

工厂类。

COM对象的初始化

procedure Initialize是接口的初始化过程,而不是常见的Create方法。当客

户端创建接口后,将首先执行里面的代码,与Create的作用一样

一个COM对象的生存周期内,难免需要初始化类成员或者设置变量的初值,

以经常需要重载这个过程。

相对应的,destructor Destroy则和类的标准析构过程一样,作用也相同。

类工厂注册

在代码的最后部分,假如使用TComObjectFactory来注册,就和前面所讲的完全

一样了。我在这里刻意用类TMySvrFactory继承了一次,并且重

载了UpdateRegistry 方法,以便向注册表中写入额外的内容。这是种小技巧,

希望大家根据本文的思路,摸清COM/DCOM对象的Delphi实现结构

后,可以举一反三。毕竟随心所欲的控制COM对象,能提供的功能远不如此。