COM自动化Word格式文档下载.docx
《COM自动化Word格式文档下载.docx》由会员分享,可在线阅读,更多相关《COM自动化Word格式文档下载.docx(14页珍藏版)》请在冰豆网上搜索。
(但是我们可以使用get/set方法对模拟属性。
)
自动化对象怎样被创建?
创建一个自动化对象是一个简单的操作。
这儿我将使用VisualBasic作为例子,但是在任何兼容自动化的语言中,方法基本上一样。
在VisualBasic,你应先创建一个对象变量:
DimBeeperasObject
……接着设置它指向一个特殊的对象:
SetBeeper=CreateObject("
BeepCntMod.BeepCnt"
在这个例子中我们创建了一个BeepCnt对象(见第一篇ATL文章,这篇文章将会在以后刊出-译者注)。
我们可以接着调用对象上的方法控制它的属性,就像我们不久将看到的。
但是首先,让我们讨论VisualBasic(或者任何自动化客户程序)在幕后真正做什么。
我们早已知道我们将通过IDispatchCOM接口访问自动化对象。
所以DIM语句只显示集合至少需要的内存,因此VisualBasic能为我们即将创建的对象访问IDispatch指针。
CreateObject调用需要有一点技巧。
首先,GUID在哪里?
对象的CLSID没有GUID我们怎样创建它?
你可以重新调用,这样我们可以通过对象的ProgID引用对象类型。
你也可以重新调用我们在注册表用ProgID作为键名注册的一个键。
该键用一个CLSID作为子键。
COM提供一个叫CLSIDFromProgID的函数,它根据给出的ProgID查找CLSID。
VisualBasic用我们传送到CreateObject的字符串调用这个函数。
在这个例子中,VisualBasic将传送"
。
CLSIDFromProgID查阅那个键和返回与它相关的CLSID。
(顺便说一句,ProgID的第一部分是模块或应用程序名,第二部分是模块或应用程序中的对象名。
在这一点上VisualBasic调用我们的老朋友CoCreateInstanceEx,传送CLSID和请求IDispatch接口。
如果CoCreateInstanceEx成功,VB创建一个包含由CoCreateInstanceEx收到的IDispatch指针的对象变量,并且把它分配给我们的对象变量。
如果因任何原因创建失败:
对象不存在,或者它没实现IDispatch,则对CreateObject的调用失败。
就像你所看到的,VisualBasic(或任何自动化客户)的开销是最小的,所有必须知道的是用两个简单的COM函数创建对象。
那么你怎样访问自动化属性和方法?
访问我们的对象的VisualBasic源代码可能像下面这样:
BC=Beeper.Count
Beeper.Count=5
Beeper.Beep
这三个语句分别访问一个属性、设置一个属性和调用一个方法,都只使用了两种IDispatch方法:
GetIDsOfNames和Invoke。
IDispatch:
:
GetIDsOfNames获得与方法或属性的文本名有关的整型ID。
VisualBasic调用它发现"
Beep(嘟嘟响)"
对应ID1和"
Count(计数)"
对应ID2。
当我们调用IDispatch:
Invoke时,我们需要这些叫做dispids的ID。
所有现行的自动化属性和方法访问都是通过调用IDispatch:
Invoke实现。
换句话说,你的自动化客户要访问自动化对象所必须知道是几个简单的COM调用。
如果你的执行语言不是C或C++,你可以为你的运行时间编写帮助者来做那些调用,所以从任何程序使用自动化是简单的。
也许很简单,但并不是不重要:
Invoke接收一批参数,所有的参数必须被正确设置。
最重要的是:
一个叫dispid的整型ID,它指定要被访问的属性和方法(我们通过调用包含属性或方法名的字符串的GetIDsOfNames获得它)。
一个包含一列参数指针的结构。
(每个参数被存储到包含一个典型标记和一个叫variant的共用体的结构中。
)
一个包含指向属性(设置它、获得它、用一个引用设置它)或者方法(调用它)数列的指针的结构。
一个作为属性获取的或者是方法调用返回的返回值参数,也是一个变量。
噢,万一你想本地化方法、属性、名字化参数名或参数值,Invoke和GetIDsOfNames都接受一个本地ID。
Invoke也有几个其他参数可以把错误信息传递给自动化客户。
在这里我们将假定我们处在一个完美的世界,暂时不考虑它们。
变量(Variant)以16个字节存储。
前两个字节是一个标记,包含一个代表变量类型的数,其次的六个字节填满,最后的八个字节是变量的值。
值的格式取决于标记的值。
在C/C++中,我们使用一个共用体表示变量的值。
变量可以拥有大多数C++数据类型,包括指针、数组、串、日期和当前对象。
下一次我们将对包括变量COM数据类型做完整的处理。
那么为什么它比常规接口容易?
注意不一定是必须通过IDispatch:
Invoke进行一次调用:
没有偏移量--我们使用一个dispid,我们通过请求对象本身获得它。
没有C/C++参数列表和调用约定--我们使用变量的数组。
不用C/C++头文件告诉你上面的东西--但是典型库是可选择的。
你不能完全使用C/C++来进行分发。
很显然,这四个调用需要使用C/C++调用约定完成。
但这是你作为一个自动化客户唯一需要担心的地方。
那么,所有需要为调用做的是一个对象的IDispatch指针,你想访问的属性、方法名和参数的列表。
出于同样的原因,如果你想用脚本语言中编写COM对象,只有用IDispatch(噢,一种创建对象的方式)代替试图处理常规接口的无数变量的无数细节,这会使你的语言运行时间实现更方便。
COM接口和自动化间的不同
从前面的描述,你可以直接的看到自动化与COM接口间不同的几个方式:
自动化接口不必是永恒的,虽然在运行的时候你不必改变它们,因为客户能缓存dispids。
但是自动化接口的改变是平常的,尤其是从对象的一个版本到另一个版本时添加方法。
(如果你删除方法或改变参数,你可能破坏现有的客户代码。
自动化方法(和属性)可以获取包括不同类型的可变长度参数列表。
在运行时解析参数和执行任何必需的类型转变是IDispatch:
Invoke的事。
(如果不能实现参数的转换,对象的IDispatch:
Invoke实现将返回一个错误,HRESULT。
自动化方法和属性的访问是最后限制的;
换句话说,确定要被访问的方法/属性被推迟到调用时才进行。
因为所有这个后期连接,自动化方法和属性是多形态的,在某种意义上更像Smalltalk而非C++;
你可以存取任何对象上任何方法或属性,拒绝有害的调用是对象的责任。
例如,你可以在一个任意对象上调用一个Print方法。
任何对象调用Print方法大概都会打印它的值;
没有任何对象会发生调用失败。
在C++中,你只能在那些定义了Print方法的类的对象上调用Print:
在编译时检查名字和参数,而不是运行时。
那么看一些例子怎么样?
作为一个快速例子,让我们着眼于刚展示的三个调用怎样使用。
调用一个方法
因为没有参数传递到Beep方法,也没有从Beep方法或返回值,让我们先调用它。
记住调用被写作:
首先你需要知道一件事:
参数指针传递到Invoke实际上是一个指向一个DISPPARAMS结构的指针,就像下面定义的:
typedefstructFARSTRUCTtagDISPPARAMS{
//Pointertoarrayofarguments,namedandunnamed
VARIANTARGFAR*rgvarg;
//ArrayofDispatchIDsofnamedarguments
DISPIDFAR*rgdispidNamedArgs;
//Totalnumberofarguments,namedandunnamed
unsignedintcArgs;
//Numberofnamedarguments.
unsignedintcNamedArgs;
}DISPPARAMS;
我们将不会使用名字化的参数,所以rgdispidNamedArgs将为空,而cNamedArgs将为零。
进行这个简单调用的代码为:
DISPIDdispid;
OLECHAR*szMember="
Beep"
;
//Noparameters,sonoarray
DISPPARAMSdispparamsNoArgs={NULL,NULL,0,0};
//pdispisanIDispatchpointer
//totheBeeperobject
hresult=pdisp->
GetIDsOfNames(IID_NULL,&
szMember,1,
LOCALE_USER_DEFAULT,&
dispid);
Invoke(
dispid,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_METHOD,
&
dispparamsNoArgs,NULL,NULL,NULL);
首先,我们通过调用GetIDsOfNames获得dispid。
在两次调用中,IID_NULL值是保留参数的值。
我们传递一个指向字符指针的数组的指针(在这个例子中,只有一个字符指针)和数组中指针的数目、地区(万一我们想要本地化名字)和dispid数组的一个指针(同样,这儿只有一个指针)。
当调用返回时,dispid将包含与"
对应的dispid。
如果我们提供一种方式记住"
的dispid,我们不用多次调用GetIDsOfNames。
一旦我们有dispid,我们可以调用Invoke。
你将注意即使参数列表是空的,我们仍不得不提供一个最小的DISPPARAMS结构。
也会注意我们不得不说明我们想通过DISPATCH_METHOD执行方法调用。
你也将注意到这比只调用Beep()更加复杂的多。
自动化对非C/C++客户来说容易使用,但是他总比直接调用来得慢。
然而,如果服务器在另一个进程中或另一个机器上,设置(和执行)自动化调用的额外时间变的可以忽略。
获取一个属性的值
获取一个属性的值和调用一个方法很相似,唯一的不同是当调用返回时我们将注意返回值。
VisualBasic中获得一个属性的值的代码写为:
调用Invoke的C++代码是:
VARIANTvarResult;
//dispidsetbycalltoGetIDsOfNames
//(omittedforbrevity)
DISPATCH_PROPERTYGET,
dispparamsNoArgs,&
varResult,NULL,NULL);
//Property'
svaluestoredinvarResult
通过传递参数获取参数化属性(或者"
属性数组"
)很容易,就象当获取(和设置)属性时用到的一个索引或查找关键字。
注意VisualBasic语法不能区分获取属性和调用没有参数但有返回值的方法之间的区别。
换句话说,不可能从语法上区别BC=Beeper.Count是存储一个叫Count的属性或是调用一个叫Count的方法。
作为一个结果,一些自动化客户将为一个属性访问传送两个标记,在一个属性获取的情况下,可能传送DISPATCH_PROPERTYGET|DISPATCH_METHOD因为它不能区别是否它在做一个属性存取或一个方法调用。
自动化对象不得不通过根据dispid是指向一个属性或是一个方法来正确运行。
设置一个属性的值
设置一个属性与获取一个属性有三种区别:
属性的新值有一个参数。
使用dispidDISPID_PROPERTYPUT为参数命名。
返回的值参可以忽略。
设置一个属性与调用一个方法的不同之处在于属性设置的参数使用特殊的dispid命名。
为什么我们不得不进行这么详细的讨论?
记住属性可以被参数化的。
属性的值有一个特殊的名字使自动化对象容易认出哪个参数是属性的值。
(好大夫真正想避免使用命名的参数,但是这是我们绝对需要的例子)
回想在Beeper对象上设置Count属性的VB代码是:
调用的C++代码是:
//parameterstructure
DISPPARAMSdispparams;
//one-elementarrayofparameternames
DISPIDmydispid[1]={DISP_PROPERTYPUT};
//one-elementarrayofparameters
VARIANTARGvararg[1];
dispparams.rgvarg=vararg;
//1-elementarray
VariantInit(&
rgvarg[0]);
dispparams.rgvarg[0].vt=VT_I4;
//32-bitinteger
dispparams.rgvarg[0].iVal=5;
//here'
sour5!
dispparams.rgdispidNamedArgs=mydispid;
//namearray
dispparams.cArgs=1;
//totalargs
dispparams.cNamedArgs=1;
//namedargs
DISPATCH_PROPERTYPUT,
dispparams,NULL,NULL,NULL);
所有这些代码是一个小调用!
对于实现自动化调用需要多少代码和为什么自动化可能很缓慢,你可能开始有一个概念了。
如果有更多的参数,这三行设置每个变量:
……每个参数会被重复--一个语句初始化变量、一个语句设置它的类型、一个语句设定它的值。
如果参数需被命名,在rgdispidNamedArgs中有一个附加的语句设置每个参数名。
并且参数的大小应被正确设置。
所以自动化是用代码的尺寸作为代价实现了灵活调用。
并且它还放弃一件事情:
你唯一能传递的参数是那些能在变量(variant)中被描述的参数。
而最大的损失是你不能使用变量描述结构。
我们不久将讨论变量。
如果你想知道更多关于自动化参数传递的信息,查找你最喜欢的COM参考书。
准备写你自己的IDispatch:
Invoke调用?
阅读本部分……
除非你正在编写你自己的脚本语言或直接调用IDispatch:
Invoke,否则你不会遇到我们将要讨论的问题,对于一个C++客户来讲是一个纯自动化对象。
在阅读关于可选择的自变量时,会发现一个令人迷惑的情形:
现在对IDispatch:
Invoke的COM文档中,你不得不为每个遗漏的没命名的变量传送带有VT_ERROR标记的变量。
如果你跳过一个没命名的自变量,就像在object.method(a,,c)中,是明显正确的。
但是如果可选择的自变量是在最后呢?
脚本语言能否根本不使用类型库就知道要传递多少哑元自变量?
答案:
不能。
如果你最后想省略没命名的可选择的自变量,你可以省略它们。
使用实现了IDispatch的COM对象将会通过向在IDL中指定的双重接口函数提供自变量来正确处理这种情况。
但是有一个gotcha:
如果你在调用的对象本身实现了IDispatch:
Invoke本身,而不依赖COM,它可以被写入,所以它假设所有可选择的和缺省值的自变量为哑元自变量,--所以如果你能获取类型库信息,你应该可以确定参数的数目,,因此你能提供哑元自变量。
自动化:
服务器端
关于怎样调用方法、设置和获取属性、存取返回值和传送参数,我们现在看到的比我们曾经想知道的还要多。
自动化对象怎样处理所有这些问题?
简而言之,对象通过实现IDispatch:
Invoke(当然还有IDispatch其他部分,包括GetIDsOfNames)处理所有这些问题。
但是让我们假定你正在实现这个对象。
你怎样实现这些方法?
实现IDispatch困难的方式
如果你的方法没有任何参数(或者可能只有一个参数)和没有参数化的属性,你自己实现IDispatch:
Invoke就非常简单。
全部需要做的是一个开关语句,它为每个dispid和类型的组合调用正确的函数。
(你也可以使用一个调用表。
)你将不得不把参数转换为你需要的类型,但是调用VariantChangeType可以很容易的从任何类型转换变量到你需要的任何类型。
(如果转换失败,就返回一个错误给调用者。
但是可以想象如果你有多个参数的混乱状况。
首先,未命名的参数在数组中是颠倒次序的。
如果他们被命名,顺序由数组的dispids决定,意味着你将不得不把它挑选出。
但是等一下--那不是全部--你可能需要支持可选择的参数和带有缺省值可选择的参数。
给出所有的这些的描绘是一巨大的混乱。
它不仅仅困难--你很可能在做它时产生错误。
如果你去做,在你能处理的调用上你将多半以对一个你本来能处理的调用给出一个错误而结束---这不是在你的用户当中鼓舞信心的一种伟大的方式。
有比较容易的方式吗?
你可以打赌一定有。
不要用困难方式做它。
让COM帮你做它!
如果你的服务器遇到两个相对简单的要求,你能利用COM的内建的IDispatch的实现。
要求是:
·
你的自动控制接口必须是一个双重接口(作为ATL产生的),不是一个纯粹的dispinterface。
你必须产生和利用一个类型库告诉COM是哪些方法和属性。
ATL甚至不支持引入的纯disp接口,所以你的选择是双重的(包括自动控制)和自定义接口。
一个双重接口和等价的常规接口之间主要的差别是双重接口派生自IDispatch而不是IUnknown。
这意味着除了QueryInterface、Release和AddRef外,双重接口也必须实现所有的IDispatch方法(包括GetIDsOfNames和Invoke)。
正如ATL为你提供了IUnknown方法的实现,它也提供了IDispatch方法的实现。
双重接口的IDL
双重接口的IDL看起来非常像常规接口的IDL。
对BeepCnt对象,IbeepCnt接口的IDL是:
[
object,
uuid(4F74530F-3943-11D2-A2B5-00C04F8EE2AF),
dual,
helpstring("
IBeepCountInterface"
),
pointer_default(unique)
]
interfaceIBeepCount:
IDispatch
{
[id
(1),helpstring("
methodBeep"
)]HRESULTBeep();
[propget,id
(2),helpstring("
propertyCount"
)]
HRESULTCount([out,retval]long*pVal);
[propput,id
(2),helpstring("
HRESULTCount([in]longnewVal);
};
注意双重接口的IID和接口是一个双重接口的事实在接口属性中被指定。
同时也应该注意到接口派生自IDispatch,而非IUnknown。
方法属性中的IDs是自动化接口的dispids。
注意实现属性的方法有一个特别的属性--在这个情况中,Count属性有两种方法,一个是获取值和另一个是设置它。
当MIDL产生C++头文件时,这两种方法将被命名为get_Cou