软件重用是业界追求的目标,人们一直希望能够像搭积木一样随意“装配”应用程序,组件对象就充当了积木的角色。所谓组件对象,实际上就是预定义好的、能完成一定功能的服务或接口。问题是,这些组件对象如何与应用程序、如何与其他组件对象共存并相互通信和交互?这就需要制定?个规范,让这些组件对象按统一的标准方式工作。
COM是个二进制规范,它与源代码无关。这样,即使COM对象由不同的编程语言创建,运行在不同的进程空间和不同的操作系统平台,这些对象也能相互通信。COM既是规范,也是实现,它以COM库(OLE32.dll和贴OLEAut32.dll)的形式提供了访问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的一些基本概念,我则想谈一些个人的理解,希望能给对Delphi下COM编程有疑惑的朋友带来帮助。
1) idispatch是COM object的接口, 在Delphi中通常指一个OleObject.
(2) OleVariant是一种COM object兼容的Variant类型, 可以通用任何Ole Automation 类型, 他与CreateOleObject创建的idispatch兼容
1:Com+的编写:
1:File---->New---->Other....---->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.在dephi中Run | Parameters… HOST APPLICATION 填入 {系统路径}\system32\dllhost.exe 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---->New---->Other....---->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 := SocketConnection1.AppServer.GetData; try P := VarArrayLock(V); Move(P^, Data, SizeOf(Data)); finally VarArrayUnLock(V); end; end; 9:Com+中传递记录集 下面xeen 实验成功 uses ADOInt******************************************************************************************* 可以;你可以将ADO的数据作为一个Variant类型的变量进行传送:adodataset1.RecordSet这是原生的ado数据 这是服务端的一个方法的代码:把CodeSet的类型改为Variant*[in,out] function TADORec.getData: OleVariant; begin AdoDataSet1.Open; result := adodataset1.RecordSet; end; ************************************************************************************************************************ 客户端 uses adoint; var MyRecordset :_recordset; begin MyRecordset := IUnknown(CodeSet) as _recordset ************************************************************************************************************************** Com基本概念:
1:COM是一个基于二进制的标准。打个比方,我们用Delphi实现了一个对象,一般情况下,我们只能在Delphi来生成这个对象的实例并调用,而如果我们用Delphi实现了一个COM对象的话,我们可以用VC、VB或者其他任何一种支持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:SetAbort合SetComplete是否正确调用
5:(阿朱) 建议:多个DLL在一个包,一个DLL中的COM公用一个ADOCONNECTION 6:问题:我已经在Transactional Data Module的Pooled属性里面设置了True了,但是在Win2000的组件服务管理的“组件属性”的“激活”一栏里面,仍然无法打开“启用对象共用”的选项 --->您可以将线程模式设置为tmNeutral或者tmBoth都可以。
Com有需要研究和有疑问的地方:
1:Com+的模式:
2:到Com+的资源Pooling机制
3:用delphi6 开发Com+,用Neutral模式,尽量用Object Pooling,当然就是要无状态了,事务要尽量短,避免死锁,用ADO不要用BDe.Dbexpress等.
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进行事务管理?? 老兄,SetComplete和SetAbort怎么能在客户端调用呢?这样肯定是不行的。要保证事务,就在服务器端声明一个专门保存的接口方法,例如: procedure UpdateMyData(var AData1, AData2: OleVariant; var AMaxError, AErrorCount: Integer); begin try DataSetProvider1.Data := AData1; DataSetProvider1.ApplyUpdate(AMaxError, AErrorCount); DataSetProvider2.Data := AData2; DataSetProvider2.ApplyUpdate(AMaxError, AErrorCount); SetComplete; except SetAbort; end; end;
7:,在组件管理器中的“事务列表”里也看不到事务(应该有事务的时候),怎么看?
8:Com+的两大研究:事务 和 pooling
9:做一个提交用的COM+对象和一个协调用的COM+对象 ???
COM (组件对象模型 Component Object Model)是一个很庞大的体系。简单来说,COM定义了一组API与一个二进制的标准,让来自不同平台、不同开发语言的独立对象之间进行通信。COM对象只有方法和属性,并包含一个或多个接口。这些接口实现了COM对象的功能,通过调用注册的COM对象的接口,能够在不同平台间传递数据。
COM光标准和细节就可以出几本大书。这里避重就轻,仅仅初步的解释Delphi如何进行COM的封装及实现。对于上述COM技术经验不足的Delphi程序开发者来说,
Delphi通过模版生成的代码就像是给你一幅抽象画照着画一样,画出来了却不一定知道画的究竟是什么,也不知该如何下手画自己的东西。本文能够帮助你解决这类疑惑。再次讲解一些概念
“DELPHI下的COM编程技术”一文已经介绍了不少COM的概念,比如GUID、CLSID、IID,引用计数,IUnKnown接口等,下面再补充一些相关内容:COM与DCOM、COM+、OLE、ActiveX的关系
DCOM(分布式COM)提供一种网络上访问其他机器的手段,是COM的网络化扩展,可以远程创建及调用。COM+是Microsoft对COM进行了重要的更新后推出的技术,但它不简单等于COM的升级,COM+是向后兼容的,但在某些程度上具有和COM不同的特性,比如无状态的、事务控制、安全控制等等。
以前的OLE是用来描述建立在COM体系结构基础上的一整套技术,现在OLE仅仅是指与对象连接及嵌入有关的技术;ActiveX则用来描述建立在COM基础上的非COM技术,它的重要内容是自动化(Automation),自动化允许一个应用程序(称为自动化控制器)操纵另一个应用程序或库(称为
自动化服务器)的对象,或者把应用程序元素暴露出来。
由此可见COM与以上的几种技术的关系,并且它们都是为了让对象能够跨开发工具跨平台甚至跨网络的被使用。
Delphi下的接口
Delphi中的接口概念类似C++中的纯虚类,又由于Delphi的类是单继承模式(C++是多继承的),即一个类只能有一个父类。接口在某种程度上
可以实现多继承。接口类的声明与一般类声明的不同是,它可以象多重继承那样,类名 = class (接口类1,接口类2… ),然后被声明的接口类则重载继承类的虚方法,来实现接口的功能。
以下是IInterface、IUnknown、IDispatch的声明,大家看出这几个重要接口之间是什么样的联系了吗?任何一个COM对象的接口,最终都是从IUnknown继承的,而Automation对象,则还要包含IDispatch,后面DCOM部分我们会看到它的作用。
IInterface = interface ['{00000000-0000-0000-C000-000000000046}'] 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-C000-000000000046}'] 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可以得到接口句柄,而AddRef与Release则负责登记调用次数。
COM和接口的关系又是什么呢?COM通过接口进行组件、应用程序、客户和服务器之间的通信。COM对象需要注册,而一个GUID则是作为识别接口的唯一名字。
假如你创建了一个COM对象,它的声明类似 Txxxx= class(TComObject, Ixxxx),前面是COM对象的基类,后面这个接口的声明则是: Ixxxx = interface(IUnknown) 。所以说IUnknown是Delphi中COM对象接口类的祖先。到这一步,我想大家对接口类的来历已经有初步了解了。
聚合
接口是COM实现的基础,接口也是可继承的,但是接口并没有实现自己,仅仅只有声明。那么怎么使COM对象对接口的实现得到重用呢?答案就是聚合。聚合就是一个包含对象(外部对象)创建一个被包含对象(内部对象),这样内部对象的接口就暴露给外部对象。
简单来说,COM对象被注册后,可以找到并调用接口。但接口不是仅仅有个定义吗,它必然通过某种方式找到这个定义的实现,即接口的“实现类”的方法,这样才最终通过外部的接口转入进行具体的操作,并通过接口返回执行结果。
进程内与进程外(In-Process, Out-Process)
进程内的接口的实现基础是一个DLL,进程外的接口则是建立在应用程序(EXE)上的。通常我们建立进程外接口的目的主要是为了方便调试(跟踪DLL是件很麻烦的事),然后在将代码改为进程内发布。因为进程内比进程外的执行效率会高一些。(也就是先建立进程内的接口,再将其改为进程内发布。)
COM对象创建在服务器的进程空间。如果是EXE型服务器,那么服务器和客户端不在同一进程;如果是DLL型服务器,则服务器和客户端就是一个进程。所以进程内还能节省内存空间,并且减少创建实例的时间。
StdCall与SafeCall
Delphi生成的COM接口默认的方法函数调用方式是stdcall而不是缺省的Register。这是为了保证不同语言编译器的接口兼容。
双重接口(在后面讲解自动化时会提到双重接口)中则默认的是SafeCall。它的意义除了按SafeCall约定方式调用外,还将封装方法以便向调用者返回HResult值。SafeCall的好处是能够捕获所有异常,即使是方法中未被代码处理的异常,也可以被外套处理并通过HResult返回给调用者。
WideString等一些有差异的类型
接口定义中缺省的字符参数或返回值将不再是String而是WideString。WideString 是Delphi中符合OLE 32-bit版本的Unicode类型,当是字符
时,WideString与String几乎等同,当处理Unicode字符时,则会有很大差别。联想到COM本身是为了跨平台使用,可以很容易的理解为什么数据通信时需要使用WideString类型。
同样的道理,integer类型将变成SYSINT或者Int64、SmallInt或者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:仅单线程,处理简单,吞吐量最低;
Apartment:COM程序多线程,COM对象处理请求单线程;
Free:一个COM对象的多个实例可以同时运行。吞吐量提高的同时,也要求对COM对象进行必要的保护,以避免多个实例冲突;
Both:同时支持Aartment和Free两种线程模式。
Neutral:只能在COM+下使用。
虽然Free和Both的效率得到提高,但是要求较高的技巧以避免冲突(这是很不容易调试的),所以一般建议使用Delphi的缺省方式。
类型库编辑器(Type Library)
假设我们建立一个叫做TSample的类和ISample的接口(如图),然后使用类型库编辑器创建一个方法GetCOMInfo(在右边树部分点击右键弹出
菜单选择New-Method或者点击上方按钮),并于左边Parameters页面建立两个参数(ValInt : Integer , ValStr : String),返回值为BSTR。如图:
可以看到,除了常用类型外,参数和返回值还可以支持很多指针、OLE对象、接口类型。建立普通的COM对象,其Returen Type是可以任意的,
这是和DCOM的一个区别。
双击Modifier列弹出窗口,可以选择参数的方式:in、out分别对应const、out定义,选择Has Default Value可设置参数缺省值。
Delphi生成代码详解
点击刷新按钮刷新后,上面类型库编辑器对应的Delphi自动生成的代码如下:
unit uCOM;{$WARN SYMBOL_PLATFORM OFF}interfaceuses Windows, ActiveX, Classes, ComObj, pCOM_TLB, StdVcl;type TSample = class(TTypedComObject, ISample) protected function GetCOMInfo(ValInt: SYSINT; const ValStr: WideString): WideString; stdcall; end;implementationuses ComServ;function TSample.GetCOMInfo(ValInt: SYSINT; const ValStr: WideString): WideString;beginend;initialization TTypedComObjectFactory.Create(ComServer, TSample, Class_Sample, ciMultiInstance, tmApartment);end.
引用单元
有三个特殊的单元被引用:ComObj,ComServ和pCOM_TLB。ComObj里定义了COM接口类的父类TTypedComObject和
类工厂类 TTypedComObjectFactory(分别从TComObject和TComObjectFactory继承[加了Typed],早期版本如Delphi4建立的COM,就直接从TcomObject继承和使用TComObjectFactory了); ComServ单元里面定义了全局变量ComServer: TComServer[真的有这个],它是从TComServerObject继承的,关于这个变量的作用,后面将会提到。
这几个类都是delphi实现COM对象的比较基础的类,TComObject(COM对象类)和TComObjectFactory(COM对象类工厂类)本身就是IUnknown的
两个实现类,包含了一个COM对象的建立、查询、登记、注册等方面的代码。TComServerObject则用来注册一个COM对象的服务信息。
接口定义说明
再看接口类定义TSample = class(TTypedComObject, ISample)。到这里,已经可以通过涉及的父类的作用大致猜测到TSample是如何创建并注
册为一个标准的COM对象的了。那么接口ISample又是怎么来的呢?pCOM_TLB单元是系统自动建立的,其名称加上了_TLB,它里面包含了ISample
= interface(IUnknown)的接口定义。前面提到过,所有COM接口都是从IUnknown继承的。
在这个单元里我们还可以看到三种ID(类型库ID、IID及COM注册所必须的CLSID)的定义:LIBID_pCOM,IID_ISample和CLASS_Sample。关键是
这时接口本身仅仅只有定义代码而没有任何的实现代码,那接口创建又是在何处执行的?_TLB单元里还有这样的代码:
CoSample = class class function Create: ISample; class function CreateRemote(const MachineName: string): ISample;end;class function CoSample.Create: ISample;begin Result := CreateComObject(CLASS_Sample) as ISample;end;class function CoSample.CreateRemote(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对象的实现类。现在解释它是怎么被注册,以及何时创建的。
这一切的小把戏都在最后Delphi单元里的initialization的部分,这里有一条类工厂建立的语句。
Initialization是Delphi用于初始化的特殊部分,此部分的代码将在整个程序启动的时候首先执行。回顾前面的内容并观察一下TTypedComObjectFactory的参数:
【ComServer是用于注册/撤消注册COM服务的对象,TSample是接口实现类,Class_Sample是接口唯一对应的GUID,ciMultiInstance是实例模式,tmApartment是线程模式。一个COM对象应该具备的特征和要素都包含在了里面!】
那么COM对象的管理又是怎么实现的呢?在ComObj单元里面可以见到一条定义function ComClassManager: TComClassManager;
这里TComClassManager顾名思义就是COM对象的管理类。任何一个祖先类为TComObjectFactory的对象被建立时,其Create里面会执行这样一句:
ComClassManager.AddObjectFactory(Self);
AddObjectFactory方法的原形为procedure TComClassManager.AddObjectFactory(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
和DllUnregisterServer负责DLL的注册和解除注册,其具体的功能由ComServer实现。
接口类的具体实现
好了,现在自动生成代码的来龙去脉已经解释清楚了,下一步就是由我们来添加接口方法的实现代码。在function TSample.GetCOMInfo的部分
添加如下代码。我写的例子很简单,仅仅是根据传递的参数组织一条字符串并返回。以此证明接口正确调用并执行了该代码:
function TSample.GetCOMInfo(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”,或者通过Windows下system/system32目录中的regsvr32.exe程序注册编译好的DLL文件。regsvr32的具体参数可以通过regsvr32/?来获得。对于进程外(EXE型)的COM对象,执行一次应用程序就注册了。
提示DLL注册成功后,就应该可以正确执行下列客户端程序了:
uses ComObj, pCOM_TLB;procedure Ttest.Button1Click(Sender: TObject);var COMSvr: ISample; retStr: string;begin COMSvr := CreateComObject(CLASS_Sample) as ISample; if COMSvr <> nil then begin retStr := COMSvr.GetCOMInfo(2, ' client 2 '); showmessage(retStr); COMSvr := nil; end else showmessage(' ?¡§??¡§2??????¡ì2?3¡§|1| ');end;
最终值是从当前程序外的一个“接口”返回的,我们甚至可以不知道这个接口的实现!第一次接触COM的人,成功执行此程序并弹出对话框后,
也许会体会到一种技术如斯奇妙的感觉,因为你仅仅调用了“接口”,就可以完成你猜测中的东西。
创建一个分布式DCOM(自动化接口)
IDispatch
在delphi6之前的版本中,所有接口的祖先都是IUnknown,后来为了避免跨平台操作中接口概念的模糊,又引入了IInterface接口。
使用向导生成DCOM的步骤和COM几乎一致。而生成的代码仅将接口类的父类换为TAutoObject,类工厂类换为TAutoObjectFactory。这其实没有
太大的不同,因为TAutoObject等于是一个标准COM外加IDispatch接口,而TAutoObjectFactory是从TTypedComObjectFactory直接继承的:
TAutoObject = class(TTypedComObject, IDispatch)
TAutoObjectFactory = class(TTypedComObjectFactory)
自动化服务器支持双重接口,而且必须实现IDispatch。因讨论范畴限制,本文只能简单提出,IDispatch是DCOM和COM技术实现上的一个重要区
别。打开_TLB.pas单元,可以找到Ixxx = interface(IDispatch)和Ixxx = dispinterface的定义,这在前面COM的例子里面是没有的。
创建过程中的差异
使用类型库编辑器的时候,有两处和COM不同的地方。首先Return Type必须选择HRESULT,否则会提示错误,这是为了满足双重接口的需要。当
Return Type选择HRESULT后,你会发现方法定义将变成procedure(过程)而不是预想中的function(函数)。
怎么才能让方法有返回值呢?还需要在Parameters最后多添加一个参数,然后将该参数改名与方法名一致,设置参数类型为指针(如果找不到
某种类型的指针类型,可以直接在类型后面加*,如图,BSTR*是BSTR的指针类型)。最后在Modifier列设置Parameter Flags为RetVal,同时
Out将被自动选中,而In将被取消。
刷新后,得到下列代码。添加方法的具体实现,大功告成:
TSampleAuto = class(TAutoObject, ISampleAuto)
protected
function GetAutoSerInfo(ValInt: SYSINT;const ValStr: WideString): WideString; safecall;
end;
远程接口调用
远程接口的调用需要使用CreateRemoteComObject函数,其它如接口的声明等等与COM接口调用相同。CreateRemoteComObject函数比
CreateComObject 多了一个参数,即服务器的计算机名称,这样就比COM多出了远程调用的查询能力。前面“接口定义说明”一节的代码可以对
照CreateComObject、CreateRemoteComObject的区别。
自定义COM的对象
接口一个重要的好处是:发布一个接口,可以不断更新其功能而不用升级客户端。因为不论应用升级还是业务改变,客户端的调用方式都是一
致的。
既然我们已经弄清楚Delphi是怎样实现一个接口的,那能否不使用向导,自己定义接口呢?这样做可以用一个接口继承出不同的接口实现类,
来完成不同的功能。同时也方便了小组开发、客户端开发、进程内/外同步编译以及调试。
接口单元:xxx_TLB.pas
前面略讲了接口的定义需要注意的方面。接口除了没有实例化外,它与普通类还有以下区别:接口中不能定义字段,所有属性的读写必须由方
法实现;接口没有构造和析构函数,所有成员都是public;接口内的方法不能定义为virtual,dynamic,abstract,override。
首先我们要建立一个接口。前面讲过接口的定义只存在于一个地方,即xxx_TLB.pas单元里面。使用类型库编辑器可以产生这样一个单元。还是
在新建项目的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_TLB.pas,定义了一个接口
IMyInterface = interface(IUnknown),以及一个方法function SampleMethod(val: Smallint): SYSINT; safecall;现在让我们看看全部接口
类声明及实现的代码:
unit uMyDefCOM;interfaceuses 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; public function SampleMethod(val: Smallint): SYSINT; safecall; end; TMySvrFactory = class(TComObjectFactory) procedure UpdateRegistry(Register: Boolean); override; end;implementationprocedure TMyIClass.Initialize;begin inherited; FInitVal := 100;end;destructor TMyIClass.Destroy;begin inherited;end;function TMyIClass.SampleMethod(val: Smallint): SYSINT;begin Result := val + FInitVal;end;procedure TMySvrFactory.UpdateRegistry(Register: Boolean);begin inherited; if Register then begin CreateRegKey('MyApp\' + ClassName, 'GUID', GUIDToString(Class_MySvr)); end else begin DeleteRegKey('MyApp\' + ClassName); end;end;initialization TMySvrFactory.Create(ComServer, TMyIClass, Class_MySvr, 'MySvr', '', ciMultiInstance, tmApartment);end.
Class_MySvr是自定义的CLSID,TMyIClass是接口实现类,TMySvrFactory是类工厂类。
COM对象的初始化
procedure Initialize是接口的初始化过程,而不是常见的Create方法。当客户端创建接口后,将首先执行里面的代码,与Create的作用一样
。一个COM对象的生存周期内,难免需要初始化类成员或者设置变量的初值,所以经常需要重载这个过程。
相对应的,destructor Destroy则和类的标准析构过程一样,作用也相同。
类工厂注册
在代码的最后部分,假如使用TComObjectFactory来注册,就和前面所讲的完全一样了。我在这里刻意用类TMySvrFactory继承了一次,并且重
载了UpdateRegistry 方法,以便向注册表中写入额外的内容。这是种小技巧,希望大家根据本文的思路,摸清COM/DCOM对象的Delphi实现结构
后,可以举一反三。毕竟随心所欲的控制COM对象,能提供的功能远不如此。