在现在以及未来的操作系统中COM(组件对象模板)技术占有的比例越来越大了!
COM是什么呢?你可以理解它为对象性质的API(区别于函数性质的API),因为一般函数性质的API调用是这样的:Sendmessage(handle,msgunit,wparam.lparam);而对于COM中的方法(这里改叫方法了)调用是这样的:ShellLink.SetPath(pchar(filepath));在这里可以讲ShellLink看作一个对象,而SetPath是这个对象众多方法中的一个,我们可以这样生成一个对象:
var
ShellLink:IShellLink;//声明一个接口类型的指针变量,其中IShellLink是已经定义好的接口类型(它是个系统提供的一个接口)
begin
CoCreateInstance(CLSID_ShellLink,nil,CLSID_INPROC_SERVER,IID_IshelllinkA,shelllink)
或者:
ShellLink:=CreateComObject(CLSID_ShellLink) AS IShellLink;
然后就可以调用其中的方法了。象这样的调用方法用过DirectX(区别于一般GUI库的用户图象处理库,在游戏开发中占有极其重要的位置)朋友一定非常熟悉了,因为DirectX本来就是微软推出的第一个COM应用嘛!那么它到底是怎么工作的呢?想必很多初学COM的朋友一定很想知道吧?我开始学的时候就想知道其中的机理,但很多书都讲得不是很清楚,后来买了本<COM/DCOM精通>才算是看明白了,不过它是C++语言的,所以这里我结合Delphi中的源代码来讲一下其中的工作机制!
不过对读者有个要求:
你必须知道COM是什么以及在使用是如何调用(也就是你使用过COM,但不是很懂)。
1.COM的概念及作用:
COM是一种服务器,它将自己的服务包装在一个库中,并且自己以一个对象形式出现,而其中的子函数叫做它的方法。它的全称是Component Object Module组件对象模板,它是提供一种二进制层面(最底层)的共享手段,由于其是在二进制层面上提供共享,所以它就提供了一个很有用的特性-独立于开发环境,使用各种语言的开发工具都可以使用它,这是它最大的特点!而且由于它是一种模板,那么我们可以将软件的各个部分分为多个模板,在以后管理和升级的时候只要更改其中的模板而无需更改其他模块中的任何代码,这是它最大的卖点!
2.二进制层面的共享是什么:
这里会多次使用这个概念,那么到底什么是二进制层面呢?我们现在使用的各种开发工具都代用自身的库文件,如windows.h或者windows.dcu,当程序在编译时会将这些库中的例程加入到程序相应的部分,由于他们是用各种语言写的,所以如果呢要改变其中的行为必须重新编译你的源程序代码。这叫做语言层面的共享(现在开发工具绝大多数的库都是这种)!而COM首先是一种规范:它是用微软规定的(但现在已经实现跨平台了,毕竟好的理念谁都可以用嘛)一种二进制的规范,也就是COM必须提供一块内存给调用者,这块内存中放置各个COM方法的地址,这块内存就是我们说的接口了。由于现在大多数的开发工具都使用各种方式(方式各有不同)提供了这样一块内存,那么所有的开发语言就可以互相使用对方组件提供的方法了,因为虽然语言不同,但他们提供的内存块的规范是一样的,进入各个方法后就可以运行了(各种语言提供的函数编译器都编译成二进制了),而且COM规范中对使用的参数和返回类型都做了定义,各种语言只要按照这个规定就可以了,所以说COM是一种规范更合适一些,虽然它也是一种高级技术!
3.这篇文章将讲什么:
由于COM多为进程内服务器,所以我们这里只是讲以DLL形式存在的COM的使用和原理,对于进程外的服务器,你可以参考相应的DCOM教材。这里仅仅就Delphi中如何实现相应的技术提供讲解,对于其他语言的大家参考相应资料!
4.作者忠告:
COM是未来操作系统讲广泛采用的各种技术的基础,所以大家必须要重视这块的开发和理解,如果你对于COM一无所知的话,建议你看看C++中关于它的介绍,因为我我觉得如果连COM的规范都不懂的话,就很难深入了,更别说理解Delphi中的COM了,因为在Delphi中使用COM虽然简单,但要理解它是如何包装的没有基础是不可能的!
这篇文章讲分几个部分分别讲解,如果大家有兴趣可以关注这个论坛,如果有什么不明白的地方也可以告诉我,留言或者给我mail:[email protected],我们互相讨论一下^_^
这一节只是大家热热身,大家对于读者要求做到了吗?准备开始咯!
好了,我们下节见!
---------------------------------------------------------------------------------
com在delphi中是实现:
com在delphi中实现起来是相当简单的,只要你使用相应的开发向导,就可以很快实现一个com服务器了,我们先来做个简单的com服务器,它的作用就是在桌面显示一个时间和版权的信息!
此主题相关图片如下:
我们采用delphi6.0(你也可以使用7.0,代码一样)
选择file->open->other打开向导面板,选择new items->activex->activex library
确定就可以生成一个空的库文件(一个dll的框架),它只是提供了dllgetclassobject, dllcanunloadnow,dllregisterserver,dllunregisterserver这四个导出函数,其他的什么也不做(但就是这四个函数提供了com运行的机会,后面会讲)。然后我们要加一个com单元进来,因为正是要它来提供给我们某个服务(如:这里的桌面时间显示),同样操作选择file->open->other打开向导面板,然后选择new items->activex->com object这时会跳出一个对话框,你只要输入你的类名,我们这里使用的是tmyclass,其他的可以不动,生成一个com单元了。并且要求你用type library来设置你的接口,你可以设置你要的方法,我们这里只提供两个方法:getvalue,setvalue。好了,回到开发界面,我们现在已经有了一个可以自己写代码的类了(其中方法也空好了等我们写了,开发工具想得真实周到啊!),现在我们可以写上代码了,怎么写呢?想想!我们提供的是一个在桌面上的时钟,它要不断的变化时间,而且要在最上层,不管你怎么开窗口,它都可以让我们看到,看来要求得不高!时间的变化我们可以通过一个timer组件来实现,最上层嘛,使用桌面的tcanvas对象不就可以咯。我想这里多说两句:我们的timer组件需要在这个类初始化时候建立,按一般的想法,我们可以重写构造器函数create,但如果这样的话就game over了,因为在delphi中com的初始化是由initialize函数提供,就是你在create函数中写上也没有用,因为delphi不会调用的!好了现在可以写代码了:
初始化部分:
procedure tmyclass.initialize;
begin
inherited initialize;
canvas:=tcanvas.create;
canvas.handle:=getdc(0);//得到桌面的dc;
time:=ttimer.create(nil);
time.interval:=1000;
time.enabled:=false;
time.ontimer:=ontime;//赋予time对象一个事件句柄;
end;
相对应就应该有销毁的部分来销毁timer产生的对象:
destructor tmyclass.destroy;
begin
canvas.free;
time.free;
inherited destroy;
end;
事件句柄:
procedure tmyclass.ontime(sender:tobject);
begin
getvalue;//内部调用其中的一个方法
end;
一个调用函数:
procedure tmyclass.setvalue(value:integer);
begin
time.enabled:=true;//激活时间组件
end;
方法的代码:
function tmyclass.getvalue:integer;
var
t:tdatetime;
s:string;
begin
try
t:=now();
s:=formatdatetime('yyyy"年"mm"月"dd"日"ampmhh"时"',t);
canvas.font.color:=clred;
canvas.font.size:=18;
canvas.brush.style:=bsclear;
canvas.textout(500,520,s);
canvas.font.color:=clblue;
canvas.font.size:=18;
canvas.brush.style:=bsclear;
canvas.textout(500,540,'涂非平制作');
except
raise exception.create('canvas is error');
end;
result:=0;//我们这里没用它,返回一个0就可以了!^_^
end;
好了,基本代码写完了,可以编译了,编译后生成的dll还不可以调用,要先注册到系统中(向注册表写入相应的信息),我们按下菜单run->register activex server弹出对话框表示我们正确注册,现在我们就有一个com服务器了!!!!!!!!但我们怎样调用它呢?打开刚刚开发的工程单元,找到相应的tlb.pas单元,记下一个值:clsid_myclass就可以了,我们要靠它来调用呢
一些这个单元中的信息(至于guid不要我说了吧)
------------------------------------
imyinterface=interface(iunknown)
['{a0c53f6f-f270-457b-8d42-89fb12737d6b}']
function getvalue:integer;stdcall;
procedure setvalue(value:integer);stdcall;
end;
const clsid_tmyclass:tguid='{c421dfce-1cfa-476e-b69b-d7f519ff87d7}';
------------------------------------
下面我们来写调用部分:
建立一个单窗口程序,在上面加一个按钮就可以了,现在就写按钮的事件句柄的代码,其他的可以不写(本来就没有代码,^_^)。
不过要先加入你刚才编写的com的相应信息进来才可以,找到相应的tlb.pas单元,讲接口部分复制过来(提供一个调用接口的规范,告诉程序怎样调用),就是上面画线的部分复制过来!
现在可以写按钮事件代码了:
procedure tform1.button1click(sender: tobject);
begin
map:=createcomobject(clsid_tmyclass) as imyinterface;
map.setvalue(1);//至于这里的参数,由于我们没有用它,所以你可以输入任何数,但不能大于整数的范围哟!
end;
注意这里的map是在类中声明的一个接口变量,你可以认为它是一个对象: map:imyinterface;
好了,试试吧!按一下按钮是不是出现图片上的情景了?
好了,这一节主要是复习一下制作com的步骤,虽然简单,但复杂的com也是这样做出来的,这里还演示了如何在类中包装对象,让对象为我们做事。以后几节我会以这个例子来讲解系统是怎样让它激活工作的,以及它是怎么输出它自己的方法的!
好了,下节见!
---------------------------------------------------------------------------------
简单的利用开发向导,写几行简单的代码就完成一个显示时间的com服务器,是不是很简单,如果你真的这么认为那你就错了。对!虽然你会用delphi开发com了,但如果你想理解它或者以后开发复杂的com和深入dcom机制不理解它的运行机制是不可能的!现在我们先就调用端来分析一下它是怎么工作的!
我们调用是用这样的形式:
map:=createcomobject(clsid_tmyclass) as imyinterface;
其实这里完成的是两个操作:
完整的应该是这样的:
map1:=createcomobject(clsid_tmyclass);//map1是一个iunknown类型的接口
map1.queryinteface(iid_shelllinka,map);//通过map1查找iid_shelllinka接口
仅靠系统提供一个createcomobject我们就可以使用一个com服务器了,这个函数肯定做了什么?
在delphi的源代码中createcomobject是这样定义的:
function createcomobject(const classid: tguid): iunknown;
begin
olecheck(cocreateinstance(classid, nil, clsctx_inproc_server or
clsctx_local_server, iunknown, result));
end;
呵呵,原来是函数的包装啊!
这里就要介绍一下cocreateinstance函数。
function cocreateinstance; external ole32 name 'cocreateinstance';
它是系统(ole32.dll)提供的一个api,那么它是做什么的呢,具体做了什么工作?
这个函数真正要我们提供的参数就是一个classid,其他的可以使用上面的默认值。
它会从注册表中找该com类的classid(在hkey_classes_root\clsid键中),找到相应的键后系统会读取其中inprocserver32的数据,找到服务器的地址后用loadlibrary函数将服务器dll载入宿主进程,再利用dll中的dllgetclassobject函数(这是每一个dll形式com服务器必须提供的函数),获取了iclassfactory接口指针后利用这个接口中createinstance方法创建我们需要的com对象的实例(这个时候对象才出现了)!好了,现在知道他们之间的调用过程吗。小节一下:
createcomobject-->cocreateinstance-->注册表clsid-->loadlibrary
-->dllgetclassobject(我们在后面分析中将从这里开始)
-->iclassfactory.createinstance
好了,前面的分析完成了,由于它大部分功能都是由系统完成的,没有多少研究的必要,你只要会用,知道怎么用和它是做什么的就可以了。(也没有条件分析下去,我可没有这个api的源码,你只有去问微软了,不知道它会不会给你),那么下面我们从dllgetclassobject函数开始分析,它是编程语言提供的(系统要求的,工具提供的),我们可以分析其中的代码,先看看dll工程的代码:
library project3;
uses
comserv,
unit3 in 'unit3.pas',
project3_tlb in 'project3_tlb.pas',
unit7 in 'unit7.pas' {form1};
exports
dllgetclassobject,
dllcanunloadnow,
dllregisterserver,
dllunregisterserver;
{$r *.tlb}
{$r *.res}
begin
end.
仅仅是导出comserv库中的四个函数,其中dllgetclassobject我们上面说过,也是最重要的,先分析它:
function dllgetclassobject(const clsid, iid: tguid; var obj): hresult;
var
factory: tcomobjectfactory;//注意:这里都是由类工厂提供的。
begin
factory := comclassmanager.getfactoryfromclassid(clsid);//1
if factory <> nil then
if factory.getinterface(iid, obj) then//2
{由类工厂去查找相应的iid,成功就返回s_ok,并且将接口指针返回给obj}
result := s_ok
else
result := e_nointerface{没有找到就返回e_nointerface}
else
begin//如果没有相应类工厂
pointer(obj) := nil;//将接口指针置nil
result := class_e_classnotavailable;
end;
end;
现在讲讲这个函数,这个函数通过clsid, iid参数来找他们在dll中的接口的位置,然后通过参数obj来返回指针,而整个函数的返回值(hresult类型)只是用来告诉系统接口是否存在。这里的obj是什么呢?就是我们前面定义的map啊!是不是对于这种返回形式还不适应,不要紧,看多了就知道了,这种返回形式很有效的!不过要注意到它是var声明形式的哟!!这个函数要往下分析的部分(或者要重点注意的部分)已经用数字标明了!那么我们下一节就先分析1吧。
好了,先讲到这里,下节见。
------------------------------------------------------------------------------------
通过上面几节的分析,不知道你对于这种学习方法是否满意,如果你满意那我会非常高兴的。我希望更多的朋友分享我学习上的心得,做出更好的软件,找到更好的工作,赚到更多的钱!好了闲话少叙,接着讲。
上面我们讲的第一个要点是这行代码:
factory := comclassmanager.getfactoryfromclassid(clsid);//1
comclassmanager是一种对象,它很简单,主要是用来管理类工厂的,它重要的方法就是这里的getfactoryfromclassid,根据clsid来查找类工厂并返回之。有兴趣的朋友可以看这个类的源代码(很简单的,估计是object pascal最简单的几个类之一了),这里我们只要注意getfactoryfromclassid函数就可以了。
function tcomclassmanager.getfactoryfromclassid(const classid: tguid): tcomobjectfactory;
begin
flock.beginread;//锁定并开始读取其中数据
try
result := ffactorylist;{得到comclassmanager对象的ffactorylist(其中放置的就是类工厂,它使用addobjectfactory来添加相应的类工厂)}
while result <> nil do
begin
if isequalguid(result.classid, classid) then exit;{找到了就退出,并返回这个result值}
result := result.fnext;//类工厂存放在fnext变量中
end;
finally
flock.endread;//解锁恢复
end;
end;
isequalguid是一个比较函数,它比较classid,classid相同返回true否则返回false;
从这里可以知道这个函数主要是返回一个类工厂的指针。那么到底什么是类工厂,为什么要使用类工厂呢?
com中有一个重要的思想就是封装。它将自己的实现封装起来,仅仅只是留下一个接口让用户来调用!至于里面是怎样实现的外面一概不知,而类工厂的存在将这种封装推向了及至,因为它本身也是一个com对象,我们叫它“一级对象”,然后再由它来生成我们的com对象,那我们的com对象就可以叫做“二级对象”,通过两级封装,别人还知道你是怎么实现的?在delphi中默认的com对象都是通过类工厂生成的二级对象(当然你也可以用一级来搞定,最后一节我们将实现这么一个com,因为这要求一定的基础)。那么类工厂到底是个什么东西,它有什么特别之处呢?
类工厂其实就是一个简单的com类,不希奇,但的确有特别之处,因为它提供了一个很有用的接口和方法:
iclassfactory = interface(iunknown)
['{00000001-0000-0000-c000-000000000046}']
function createinstance(const unkouter: iunknown; const iid: tiid;
out obj): hresult; stdcall;
function lockserver(flock: bool): hresult; stdcall;
end;
现在明白了吧,只要实现了iclassfactory接口的com类我们就可以叫它类工厂,没有什么其他的特点!至于createinstance函数后面将介绍,先将上面一节的第二个重点解释一下:
factory.getinterface(iid, obj)
如果找到一个类工厂的话就调用这个方法来找参数id标识的接口并依靠obj来返回指针给dllgetclassobject的obj参数!那么它是怎么查找的,随我来!
function tobject.getinterface(const iid: tguid; out obj): boolean;
var
interfaceentry: pinterfaceentry;//接口指针的入口
begin
pointer(obj) := nil;//先将obj置为nil
interfaceentry := getinterfaceentry(iid);{根据iid查找出接口的入口,这又是一个重要的函数哟,^_^}
if interfaceentry <> nil then//如果返回了接口入口
begin
if interfaceentry^.ioffset <> 0 then//确定其中的偏移量
begin
pointer(obj) := pointer(integer(self) + interfaceentry^.ioffset);{呵呵,找到这个入口了,它就是我们要返回的接口指针了}
if pointer(obj) <> nil then iinterface(obj)._addref;{如果调用成功,先调用接口的_addref方法将com对象使用次数加1,这是一个iunknown接口的标准方法}
end
else
iinterface(obj) := invokeimplgetter(self, interfaceentry^.implgetter);{这个我们可以先放下}
end;
result := pointer(obj) <> nil;//返回值是一个boolean值
end;
接下来我们先看看几个数据结构的定义:
tguid = packed record
d1: longword;
d2: word;
d3: word;
d4: array[0..7] of byte;
end;
pinterfaceentry = ^tinterfaceentry;
tinterfaceentry = packed record
iid: tguid;//接口的定义iid
vtable: pointer;{接口指针指向的内存结构指针,一个虚拟方法表,这里要区别delphi中的vmt哟}
ioffset: integer;//偏移量
implgetter: integer;
end;
pinterfacetable = ^tinterfacetable;
tinterfacetable = packed record
entrycount: integer;//接口个数
entries: array[0..9999] of tinterfaceentry;{接口入口数组,个数依据entrycount来定}
end;
这里就不说几个结构了,如果你连这个都不清楚那就不要看下面的内容,先充充电再来
接着说,上面那个函数最重要的就是getinterfaceentry函数的使用了,正是由它来完成查找接口的工作的!
它是一个类方法,至于什么是类方法和类引用变量你可以参看论坛中我以前发表的一篇文章《delphi中的oop思想-1.pascal部分》里面有详细的介绍!
class function tobject.getinterfaceentry(const iid: tguid): pinterfaceentry;
var
classptr: tclass;//一个类引用变量
intftable: pinterfacetable;//接口表的指针
i: integer;
begin
classptr := self;//将自己的指针赋给classptr
while classptr <> nil do
begin
intftable := classptr.getinterfacetable;{调用getinterfacetable方法来返回给接口表指针变量,咳!又是一个包装的函数,烦啊}
if intftable <> nil then
for i := 0 to intftable.entrycount-1 do{遍历所有的接口来对比iid查找相应的表如果找到就跳出循环}
begin
result := @intftable.entries[i];
if (int64(result^.iid.d1) = int64(iid.d1)) and
(int64(result^.iid.d4) = int64(iid.d4)) then exit;//找到跳出
end;
classptr := classptr.classparent;
end;
result := nil;{如果刚才的循环没有跳出,说明没有找到,返回为nil}
end;
好了,这么多函数,函数包函数,是不是有点头晕啊,先歇歇,理清思路,准备下节接着讲,好了下节见。
---------------------------------------------------------------------------------
好了,思路理得怎么样啊?现在接着开讲了,上一节讲到了getinterfaceentry函数中内包装了一个函数getinterfacetable,这个函数和getinterfaceentry都是tobject提供的函数,很重要(特别为支持com设计的)下面看看getinterfacetable是怎么得到虚拟表指针的:
class function tobject.getinterfacetable: pinterfacetable;
begin
result := ppointer(integer(self) + vmtintftable)^;{如果你了解delphi的对象模型的话,这个代码很容易理解了,它就是指向vmt表中vmtintftable的地址中的值,简单吧,这样就找到我们找了这么辛苦的接口指针,下面有个图来说明vmt的结构!}
end;
此主题相关图片如下:
这个是怎么回事啊?原来object pascal编译器一碰到了如下的声明形式
tmyclass=class(tcomobject,imyinterface)
它就将imyinterface定义的方法(对应于你的com对象中的方法,通过名字来确定)地址记住,然后再生成一个符合com接口规范的内存块并将imyinterface中方法的地址写入其中,以后就可以通过这块内存来调用方法了,那么怎么确定这块内存呢?就是靠vmt表中的vmtintftable,它指向的地址就是这块内存块的头地址!至于vmt中的vmtintftable我要简单说说。对于一般的虚拟函数地址,它会加到vmt的正数偏移的表格中,而负数偏移的是object pascal中非常重要的信息存在地,如动态方法表,接口指针表等等,vmtintftable的负偏移是-72,那么上面函数的integer(self) + vmtintftable就是将vmt的值+vmtintftable,其中integer(self)是vmt的地址,而常数vmtintftable=-72,integer(self) + vmtintftable不就是vmt中vmtintftable的值吗?而我们要用到这个指针指向的表的值并将其转换为指针,所以还要做个变换: ppointer(integer(self) + vmtintftable)^ 。ok了,现在知道是怎么回事吧?
现在你对于com调用和查找应该明白了吧,那么现在你是否非常想知道我们的com对象是怎么产生的?是通过类工厂产生的啊,废话,到底怎么产生的呢?我们下一节接着说!
---------------------------------------------------------------------------------
我们回到最前面的那个显示时间的例子,那么我们的com是怎么产生的呢?如果你试图查找的话,一定会失望的,因为代码很少,给人的感觉是无从下手。这里列出它的源代码,大家看找得到吗?
unit unit3;
interface
uses comserv,comobj,activex,windows,graphics,sysutils,extctrls;
const clsid_tmyclass:tguid='{c421dfce-1cfa-476e-b69b-d7f519ff87d7}';
type
imyinterface=interface(iunknown)
['{a0c53f6f-f270-457b-8d42-89fb12737d6b}']
function getvalue:integer;stdcall;
procedure setvalue(value:integer);stdcall;
end;
tmyclass=class(tcomobject,imyinterface)
private
canvas:tcanvas;
time:ttimer;
protected
function getvalue:integer;virtual;stdcall;
procedure setvalue(value:integer);virtual;stdcall;
public
procedure initialize;override;
destructor destroy;override;
procedure ontime(sender: tobject);
end;
implementation
procedure tmyclass.initialize;
begin
.......
end;
destructor tmyclass.destroy;
begin
.......
end;
function tmyclass.getvalue:integer;
begin
.......
end;
procedure tmyclass.setvalue(value:integer);
begin
.......
end;
procedure tmyclass.ontime(sender:tobject);
begin
getvalue;
end;
initialization
tcomobjectfactory.create(comserver,tmyclass,clsid_tmyclass,'mytestclass',
'this is my first comobject',cimultiinstance,tmapartment);
end.
好像没有看到我们的tmyclass使用create创建对象啊,这里我们到是看到一个熟悉的函数tcomobjectfactory.create,对了,这是一个类工厂的创建函数,它的参数就是我们开始使用开发向导中输入的参数而已!我们的com对象就是由它创建的咯。注意tcomobjectfactory.create函数是方法initialization(初始化)中的,这表明这个dll被调用时这个类工厂实例就会被创建!这个类工厂是delphi提供的一种基本的类工厂,它除了定义和实现了iclassfactory接口外还添加了很多对象管理的功能!那么我们现在来看看这个类工厂是怎么创建我们的com对象的?
constructor tcomobjectfactory.create(comserver: tcomserverobject;
comclass: tcomclass; const classid: tguid; const classname,
description: string; instancing: tclassinstancing;
threadingmodel: tthreadingmodel);
begin
ismultithread := ismultithread or (threadingmodel <> tmsingle);
if threadingmodel in [tmfree, tmboth] then
coinitflags := coinit_multithreaded else
if (threadingmodel = tmapartment) and (coinitflags <> coinit_multithreaded) then
......
comclassmanager.addobjectfactory(self);{前面讲过,这里是添加自己到工厂管理者中}
fcomserver := comserver;
fcomclass := comclass;//我们的类定义就放在这里,这是个类引用哟
fclassid := classid;
......
end;
这个函数将我赋给它我们的com类tmyclass存放在fcomclass内部变量中,由这个类工厂来产生我们的对象,那么这里还是没有见到我们的对象的产生啊!?!?不知道你是否记得前面我们讲类工厂时讲到的一个重要的接口iclassfactory中有个createinstance方法啊,系统会调用这个方法让类工厂产生我们的com对象,那么我们对象肯定是在这里产生的没哟错咯!看看它的方法代码:
function tcomobjectfactory.createinstance(const unkouter: iunknown;
const iid: tguid; out obj): hresult;
begin
result := createinstancelic(unkouter, nil, iid, ', obj);
end;
呵呵,又包装了一个函数createinstancelic,接着看看它的代码:
function tcomobjectfactory.createinstancelic(const unkouter: iunknown;
const unkreserved: iunknown; const iid: tiid; const bstrkey: widestring;
out vobject): hresult; stdcall;
var
comobject: tcomobject;{注意这里哟!这里声明了一个tcomobject类型的变量,它就是存放我们com对象的指针}
begin
if @vobject = nil then
begin
result := e_pointer;
exit;
end;
pointer(vobject) := nil;
if fsupportslicensing and
((bstrkey <> ') and (not validateuserlicense(bstrkey))) or
((bstrkey = ') and (not hasmachinelicense)) then
begin
result := class_e_notlicensed;
exit;
end;
if (unkouter <> nil) and not (isequaliid(iid, iunknown)) then
begin
result := class_e_noaggregation;
exit;
end;
try
comobject := createcomobject(unkouter);{创建我们的com对象,参数先我们可以不管,它是高级com开发中需要的一个参数}
except
if fshowerrors and (exceptobject is exception) then
with exception(exceptobject) do
begin
if (message <> ') and (ansilastchar(message) > '.') then
message := message + '.';
messagebox(0, pchar(message), pchar(sdaxerror), mb_ok or mb_iconstop or
mb_setforeground);
end;
result := e_unexpected;
exit;
end;
result := comobject.objqueryinterface(iid, vobject);{通过comobject.objqueryinterface方法(objqueryinterface与我们前面讲的getinterface是一样的,只是一个包装而已)查找我们要的iid并通过vobject返回接口指针}
if comobject.refcount = 0 then comobject.free;{如果使用计数为0就删除com对象}
end;
那么我们要看看createcomobject方法了:
function tcomobjectfactory.createcomobject(const controller: iunknown): tcomobject;
begin
result := tcomclass(fcomclass).createfromfactory(self, controller);
end;
这里我们看到我们刚才输入的fcomclass了,它是我们com类的一个引用,就是我们最前面输入给类工厂的参数,将它进行类型转换后调用createfromfactory,再来看看createfromfactory这个函数,真是麻烦哟!
constructor tcomobject.createfromfactory(factory: tcomobjectfactory;
const controller: iunknown);
begin
frefcount := 1;
ffactory := factory;
fcontroller := pointer(controller);
if not fnoncountedobject then ffactory.comserver.countobject(true);
initialize;//嘿嘿,就是它了,我们的初始化就在这里完成的哟!
dec(frefcount);
end;
这里看到我们最开始例子中初始化的函数initialize,就是由它来负责我们我的com初始化,现在知道为什么com的初始化不能写在create中吗?那也许你会问我们希望得到的tmyclass.create还是没有出现啊!因为这个工作在这一步已经由delphi帮我们生成了,只是这些代码在它的source部分忽略了,我们也可以不必管它,因为既然追到我们需要初始化部分的代码,说明我们的com对象肯定已经成功创建了,否则com类中的成员数据怎么可能完成初始化呢?!
这里得说说分析代码的经验了:对于一个代码,它可能是多个函数的组合或者包装,你需要分清楚重点,然后再深入到包装的函数里面,对于其他的一些对象管理维护的变量和函数我们可以不管!
我们已经从系统调用和com内部实现两个方向理解了整个com是怎么来的怎么工作的!下一节我们将创建一个com对象,它有一个特别的地方,我们前面的com对象是二级对象,现在我们利用类工厂直接创建一个一级com对象。
好了,我们下一节接着说!
---------------------------------------------------------------------------------
我们现在了解了com的生存和调用机制,大家是不是想试试身手啊,那么我提一个要求看
大家是否可以自己独立完成,如果不行的话再回来看我这个例子!
要求:
大家都知道delphi中的com都是通过相应的类工厂产生的一个二级封装对象,那么我们有
没有办法做一个一级的com对象呢,就是直接将自己的com对象暴露出来?
好了,现在来说说我的考虑:
前面讲过createcomobject来创建com对象后台其实是通过查找注册表载入相应的dll来完
成的,而dll中的dllgetclassobject又是通过查找相应的类工厂来找我们的com对象类,
那么如果我们重新更改所有的代码将是很困难的!!那换个思维方式考虑,我们可以自
己建立一个com对象,让它同时是自己的工厂不就ok了,问题就迎刃而解!由于这个对象
同时是自己的类工厂,那么就是一个一级的对象了。好了,有了一个好的想法,我们现
在就来实现它吧!
一个类工厂最大的特点就是具有iclassfactory接口,那我们就可以定义成一个这样的类
:
type
tmyclasses=class of tcomobject;
imyclass = interface(iunknown)//我们自己的接口
['{20073cda-c810-4e6e-9b00-49b9349b3c99}']
function hehe: hresult; stdcall;
end;
iclassfactory = interface(iunknown)//系统提供的类工厂接口
['{00000001-0000-0000-c000-000000000046}']
function createinstance(const unkouter: iunknown; const iid: tiid;
out obj): hresult; stdcall;
function lockserver(flock: bool): hresult; stdcall;
end;
tmyclass = class(tcomobjectfactory,iclassfactory,imyclass){看!我们这里导出
了两个接口,一个是类工厂需要的iclassfactory接口,一个是为我们服务的imyclass接
口!}
protected
function hehe: hresult; stdcall;
function iclassfactory.createinstance=mycreate;{注意这里,由于我们是直接
继承于tcomobjectfactory类,它里面有默认的iclassfactory.createinstance实现,我
们要重新写一个实现方式,所以要将iclassfactory接口的createinstance方法指向我们
给它定义的一个相同参数的方法,偷梁换柱,呵呵}
public
function mycreate(const unkouter: iunknown; const iid: tiid;
out obj): hresult;virtual;stdcall;{这就是我们给iclassfactory接口create
instance方法的一个替换方法,注意应该声明为virtual方法,这是编译器的要求哟,它
提供一种动态分配方法地址机制,如果是静态的就不会将方法放置在接口vtable表里面
了,不信你试试,呵呵}
end;
好了,一个框架搞好了,现在先写我们的接口(imyclass)的唯一一个方法:
function tmyclass.hehe: hresult;
begin
messagebox(0,pchar('hehe'),pchar('success'),mb_ok);
result:=s_ok;
end;
仅仅是显示一个对话框并返回s_ok就可以了,我们仅仅是试验我们的想法和锻炼我们的
能力嘛。
现在的问题是我们该如何写那个我们改了的接口中的建立com对象的方法呢?
我们可以先看看系统提供的实现代码,它做了什么?前面的文章已经说得很清楚,这里
重申一下:它所做的就是建立我们的com类的对象并根据我们的对象查找相应的接口返回
就可以了。我们这里由于对象是本身,那就更好办了!因为它不要再去建立对象了,直
接用自己就可以了,下面是它的代码:
function tmyclass.mycreate(const unkouter: iunknown; const iid: tiid;
out obj): hresult;
var mycom:tmyclass;//这里声明一个对象,就是我们对象的类型了
begin
if @obj = nil then
begin
result := e_pointer;
exit;
end;
pointer(obj) := nil;
if (unkouter <> nil) and not (isequaliid(iid, iunknown)) then
begin
result := class_e_noaggregation;
exit;
end;
try
mycom:=self;{这里是关键,将这个对象的指针变量就设置为自己。}
except
result := e_unexpected;
exit;
end;
result :=mycom.queryinterface(iid,obj);
if mycom.queryinterface(iid,obj)<>s_ok then mycom.free;{根据我们的对象(自
己嘛)来调用queryinterface函数查找iid并且由obj返回之}
end;
代码简单吧!?如果你真的理解前面讲的内容,那么这些代码你一看就懂,否则的话我
是怎么改的你肯定不知道的!
再在单元中生成一个guid来表示类:const class_myclass: tguid = '{021038df-233c
-43f1-83b5-bf8cacc293a0}'
在初始化的地方写上:tmyclass.create({注意:这里使用的不是别的类工厂,而是我们
定义的对象本身tmyclass哟,由它自己创建}
comserver,
tmyclasses(tmyclass),{由于自己改写的函数中并没有用到这个参数,这里我们随便写
一个,只要记住类型就可以了,为了便于直接使用原来的函数,不然我们还得再写一个
,麻烦!这里我们复习一下原来讲的类引用的用法,所以故意写成这样的!}
class_myclass,
'mytestclass',
'this is my first comobject'
cimultiinstance,
tmapartment);
好了,一切都做好了!
注册,准备使用它,验证我们的想法是否正确!
然后再创建一个窗口来测试一下吧!
建立一个窗口并在上面放置一个按钮,按下按钮后调用com对象的hehe方法,看看会不会
有对话框弹出。下面是按钮的代码:
在类型声明中增加
const class_myclass: tguid = '{021038df-233c-43f1-83b5-bf8cacc293a0}';
imyclass = interface(iunknown)
['{20073cda-c810-4e6e-9b00-49b9349b3c99}']
function hehe: hresult; stdcall;
end;
在窗口类中增加一个数据:p:imyclass;
按钮代码:
procedure tform1.button1click(sender: tobject);
var s:hresult;
begin
p:=createcomobject(class_myclass)as imyclass;
s:=p.hehe;//调用我们对象的方法;
end;
如果一切正常(我们的想法正确,我们的做法正确)的话,按下按钮后应该弹出一个对
话框的!我的程序截图如下:
此主题相关图片如下:
不知道你做得正确吗?如果不正确,那么肯定有什么地方没有理解或者疏忽了,再看看
我的代码吧!
好了,这个专题讲完了,本来还想讲一个com应用的例子,但现在这样的例子太多了,我
就懒得写那么多代码了,大家自己看吧!
如果你有什么问题可以找我,我们共同讨论:[email protected]
如果有读者需要引用这篇文章,我希望读者能够保持文章的整体性(这里我分了7篇分别
讲完),并保持作者的署名。
本专题的所有程序代码均为本人自己制作,试验并保证其有效性!
------全文完------