COM组件设计与应用(十二)
错误与异常处理
下载源代码
一、前言
程序设计中,错误处理必不可少,而且通常要占用很大的篇幅。本回书着落在 COM 中的错误(异常)的处理方法。
在组件程序中,如果遇到错误,一般有两个方式进行处理。
二、简单返回
对于比较简单的错误,直接返回表示错误原因的 HRESULT。比如下面几个就是常见的错误值:
| E_INVALIDARG |
0x80070057 |
参数错误 |
| E_OUTOFMEMORY |
0x8007000E |
内存错误 |
| E_NOTIMPL |
0x80004001 |
未实现 |
| E_POINTER |
0x80004003 |
无效指针 |
| E_HANDLE |
0x80070006 |
无效句柄 |
| E_ABORT |
0x80004004 |
终止操作 |
| E_ACCESSDENIED |
0x80070005 |
拒绝访问 |
| E_NOINTERFACE |
0x80004002 |
不支持接口 |
另外,你还可以返回自己构造 HRESULT 错误值。方法是使用宏 MAKE_HRESULT(sev,fac,code)
| 参数 |
含义 |
值(二进制) |
|
sev 严重程度 |
成功 |
00 |
| 成功,但有一些报告信息 |
01 |
| 警告 |
10 |
| 错误 |
11 |
|
fac 设备信息 |
FACILITY_AAF |
00000010010 |
| FACILITY_ACS |
00000010100 |
| FACILITY_BACKGROUNDCOPY |
00000100000 |
| FACILITY_CERT |
00000001011 |
| FACILITY_COMPLUS |
00000010001 |
| FACILITY_CONFIGURATION |
00000100001 |
| FACILITY_CONTROL |
00000001010 |
| FACILITY_DISPATCH |
00000000010 |
| FACILITY_DPLAY |
00000010101 |
| FACILITY_HTTP |
00000011001 |
| FACILITY_INTERNET |
00000001100 |
| FACILITY_ITF |
00000000100 |
| FACILITY_MEDIASERVER |
00000001101 |
| FACILITY_MSMQ |
00000001110 |
| FACILITY_NULL |
00000000000 |
| FACILITY_RPC |
00000000001 |
| FACILITY_SCARD |
00000010000 |
| FACILITY_SECURITY |
00000001001 |
| FACILITY_SETUPAPI |
00000001111 |
| FACILITY_SSPI |
00000001001 |
| FACILITY_STORAGE |
00000000011 |
| FACILITY_SXS |
00000010111 |
| FACILITY_UMI |
00000010110 |
| FACILITY_URT |
00000010011 |
| FACILITY_WIN32 |
00000000111 |
| FACILITY_WINDOWS |
00000001000 |
| FACILITY_WINDOWS_CE |
00000011000 |
|
code 唯一错误码 |
16位(bit) 你自己定义去吧 |
|
调用者得到返回的 HRESULT 值后,也可以使用宏 HRESULT_SEVERITY()、HRESULT_FACILITY()、HRESULT_CODE() 来取得sev错误程度、fac设备信息和 code 错误代码。
三、错误信息接口
既然 COM 是靠各种各样的接口来提供服务的,于是很自然地就会想到,是否有一个接口能够提供更丰富的错误信息报告那?答案是:ISupportErrorInfo。下面这段代码是使用 ISupportErrorInfo 的一般方法:
STDMETHODIMP Cxxx::fun()
{
... ... ... ...
CComQIPtr< ICreateErrorInfo> spCEI;
::CreateErrorInfo( &spCEI );
spCEI->SetGUID( IID_Ixxx ); // 发生错误的接口IID
spCEI->SetSource( L"xxx.xxx" ); // ProgID
// 如果你的组件同时提供了帮助文件,那么就可以:
spCEI->SetHelpContext( 0 ); // 设置帮助文件的主题号
spCEI->SetHelpFile( L"xxx.hlp" ); // 设置帮助文件的文件名
spCEI->SetDescription( L"错误描述信息" );
CComQIPtr < IErrorInfo > spErrInfo = spCEI;
if( spErrInfo )
::SetErrorInfo( 0, spErrInfo ); // 这时调用者就可以得到错误信息了
return E_FAIL;
} 上面是原理性代码,在我们写的程序中,不用这么麻烦。因为 ATL 已经把上述的代码给我们包装成 CComCoClass::Error() 的6个重载函数了。如此,我们可以非常简单的改写为:
STDMETHODIMP Cxxx::fun()
{
... ... ... ...
return Error( L"错误描述信息" );
}四、关于 try/catch 学习了 C++ 后,很多人都喜欢使用 try/catch 的异常处理结构。如果你使用 vc6.0 的ATL,编译器默认是不支持异常处理的,编译后会报告“warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify -GX”,解决方法是手工加上编译开关:

图一、加上编译开关,支持C++的异常处理结构
在vc.net 2003 中,编译器默认是支持异常处理结构的,所以不用特别进行设置。如果想减小目标文件的尺寸,你也可以决定不使用 C++ 异常处理,那么在项目属性中

图二、在vc.net中修改是否支持C++异常结构的编译开关
五、客户端接收组件的错误信息 1、如果使用 API 方式调用组件,接收错误的方法是:
HRESULT hr = spXXX->fun() // 调用组件功能
if( FAILED( hr ) ) // 如果发生了错误
{
CComQIPtr < ISupportErrorInfo > spSEI = spXXX; // 组件是否提供了 ISupportErrorInfo 接口?
if( spSEI ) // 如果支持,那么
{
hr = spSEI->InterfaceSupportsErrorInfo( IID_Ixxx ); // 是否支持 Ixxx 接口的错误处理?
if( SUCCEEDED( hr ) )
{ // 支持,太好了。取出错误信息
CComQIPtr < IErrorInfo > spErrInfo; // 声明 IErrorInfo 接口
hr = ::GetErrorInfo( 0, &spErrInfo ); // 取得接口
if( SUCCEEDED( hr ) )
{
CComBSTR bstrDes;
spErrInfo->GetDescription( &bstrDes ); // 取得错误描述
...... // 还可以取得其它的信息
}
}
}
} 2、如果使用 #import 等包装方式调用组件,接收错误的方法是:
try
{
...... // 调用组件功能
}
catch( _com_error &e )
{
e.Description(); // 取得错误描述信息
...... // 还可以调用 _com_error 函数取得其它信息
}六、编写支持错误处理的组件程序 非常简单,只要在增加 ATL 组件对象的时候选中 ISupportErrorInfo 即可。

图三、vc6.0 中,选中组件支持错误处理接口

图四、vc.net 2003 中,选中组件支持错误处理接口
七、小结
阅读文章后,请下载本回的示例程序。示例程序中演示了三种错误处理方法和三种接收错误的方法,同时程序中也有比较详细的注释。
COM 组件设计与应用(十一)
IDispatch 及双接口的调用
下载源代码
一、前言
前段时间,由于工作比较忙,没有能及时地写作。其间收到了很多网友的来信询问和鼓励,在此一并表示感谢。咳......我也需要工作来养家糊口呀......
上回书介绍了两种方法来写自动化(IDispatch)接口的组件程序,一是用 MFC 方式编写“纯粹”的 IDispatch 接口;二是用 ATL 方式编写“双接口”的组件。
二、IDispatch 接口和双接口
使用者要想调用普通的 COM 组件功能,必须要加载这个组件的类型库(Type library)文件 tlb(比如在 VC 中使用 #import)。然而,在脚本程序中,由于脚本是被解释执行的,所以无法使用加载类型库的方式进行预编译。那么脚本解释器如何使用 COM 组件那?这就是自动化(IDispatch)组件大显身手的地方了。IDispatch 接口需要实现4个函数,调用者只通过这4个函数,就能实现调用自动化组件中所有的函数。这4个函数功能如下:
HRESULT GetTypeInfoCount( [out] UINT * pctinfo) |
组件中提供几个类型库?当然一般都是一个啦。 但如果你在一个组件中实现了多个 IDispatch 接口,那就不一定啦(注1) |
HRESULT GetTypeInfo( [in] UINT iTInfo, [in] LCID lcid, [out] ITypeInfo ** ppTInfo) |
调用者通过该函数取得他想要的类型库。 幸好,在 99% 的情况下,我们都不用关心这两个函数的实现,因为 MFC/ATL 都帮我们完成了默认的一个实现,如果是自己完成函数代码,甚至可以直接返回 E_NOTIMPL 表示没有实现。(注2) |
HRESULT GetIDsOfNames( [in] REFIID riid, [in,size_is(cNames)] LPOLESTR * rgszNames, [in] UINT cNames, [in] LCID lcid, [out,size_is(cNames)] DISPID * rgDispId) |
根据函数名称取得函数序号,为调用 Invoke() 做准备。 所谓函数序号,大家去观察双接口 IDL 文件和 MFC 的 ODL 文件,每一个函数和属性都会有 [id(序号)....] 这样的描述。 |
HRESULT Invoke( [in] DISPID dispIdMember, [in] REFIID riid, [in] LCID lcid, [in] WORD wFlags, [in,out] DISPPARAMS * pDispParams, [out] VARIANT * pVarResult, [out] EXCEPINFO * pExcepInfo, [out] UINT * puArgErr) |
根据序号,执行函数。 使用 MFC/ATL 写的组件程序,我们也不必关心这个函数的实现。如果是自己写代码,则该函数类似如下实现: switch(dispIdMember) { case 1: .....; break; case 2: .....; break; .... } 其实,就是根据序号进行分支调用啦。(注3) |
从 Invoke() 函数的实现就可以看出,使用 IDispatch 接口的程序,其执行效率是比较低的。ATL 从效率出发,实现了一种叫“双接口(dual)”的接口模式。下面我们来看看,到底什么是双接口:

图一、双接口(dual) 结构示意图
从上图中可以看出,所谓双接口,其实是在一个 VTAB 的虚函数表中容纳了三个接口(因为任何接口都是从 IUnknown 派生的,所以就不强调 IUnknown 了,叫做双接口)。我们如果从任意一个接口中调用 QueryInterface()得到另外的接口指针的话,其实,得到的指针地址都是同一个。双接口有什么好处那?答:好呀,多好呀,特别好呀......
| 使用方式 |
因为 |
所以 |
| 脚本语言使用组件 |
解释器只认识 IDispatch 接口 |
可以调用,但执行效率最低 |
| 编译型语言使用组件 |
它认识 IDispatch 接口 |
可以调用,执行效率比较低 |
| 编译型语言使用组件 |
它装载类型库后,就认识了 Ixxx 接口 |
可以直接调用 Ixxx 函数,效率最高啦 |
|
结论 |
双接口,既满足脚本语言的使用方便性,又满足编译型语言的使用高效性。 于是,我们写的所有的 COM 组件接口,都用双接口实现吗? 错!否!NO! 如果不是明确非要支持脚本的调用,则最好不要使用双接口,因为: |
| 如果所有函数都放在一个双接口中,那么层次、结构、分类不清 |
| 如果使用多个双接口,则会产生其它问题(注4) |
| 双接口、IDispatch接口只支持自动化的参数类型,使用受到限制,某些情况下很不方便喽 |
| 还有很多弊病呦,不过现在我想不起来喽...... |
三、使用方法
如果你的开发环境是 vc6.0,那么我们使用第九回中的Simple6组件为例,快去下载呀......
如果你的开发环境是 vc.net 2003,那么用第十回中的Simple8组件为例,快去下载呀......
嘿嘿,其实不下载也没有关系,因为你只要下载本回的示例程序,里面已经包含了所需的组件。但使用前不要忘了去注册呀:regsvr32.exe simple6.dll 或 regsvr32.exe simple8.dll (注意别忘了输入组件的安装目录)。注册成功后,就可以使用了,使用方法有:
| 示例程序 |
自动化组件的使用方式 |
简要说明 |
| 示例0 |
在脚本中调用 |
在第九回/第十回中,已经做了介绍 |
| 示例1 |
使用 API 方式调用 |
揭示 IDispatch 的调用原理,但傻子才去这么使用那,会累死了 |
| 示例2 |
使用 CComDispatchDriver 的智能指针包装类 |
比直接使用 API 方式要简单多啦,这个不错! |
| 示例3 |
使用 MFC 装载类型库的包装方式 |
简单!好用!常用!但它本质上是使用 IDispatch 接口,所以执行效率稍差 |
| 示例4 |
使用 #import 方式加载类型库方式 |
#import 方式使用组件,咱们在第七回中讲过啦。常用!对双接口组件,直接调用自定义接口函数,不再经过 IDispatch,因此执行效率最高啦 |
| 示例x |
vb、java、c#、bcb、delphi....... |
反正我不会,自己去请教高人去吧 :-( |
示例一、IDispatch 调用原理篇
void demo()
{
::CoInitialize( NULL ); // COM 初始化
CLSID clsid; // 通过 ProgID 得到 CLSID
HRESULT hr = ::CLSIDFromProgID( L"Simple8.DispSimple.1", &clsid );
ASSERT( SUCCEEDED( hr ) ); // 如果失败,说明没有注册组件
IDispatch * pDisp = NULL; // 由 CLSID 启动组件,并得到 IDispatch 指针
hr = ::CoCreateInstance( clsid, NULL, CLSCTX_ALL, IID_IDispatch, (LPVOID *)&pDisp );
ASSERT( SUCCEEDED( hr ) ); // 如果失败,说明没有初始化 COM
LPOLESTR pwFunName = L"Add"; // 准备取得 Add 函数的序号 DispID
DISPID dispID; // 取得的序号,准备保存到这里
hr = pDisp->GetIDsOfNames( // 根据函数名,取得序号的函数
IID_NULL,
&pwFunName, // 函数名称的数组
1, // 函数名称数组中的元素个数
LOCALE_SYSTEM_DEFAULT, // 使用系统默认的语言环境
&dispID ); // 返回值
ASSERT( SUCCEEDED( hr ) ); // 如果失败,说明组件根本就没有 ADD 函数
VARIANTARG v[2]; // 调用 Add(1,2) 函数所需要的参数
v[0].vt = VT_I4; v[0].lVal = 2; // 第二个参数,整数2
v[1].vt = VT_I4; v[1].lVal = 1; // 第一个参数,整数1
DISPPARAMS dispParams = { v, NULL, 2, 0 }; // 把参数包装在这个结构中
VARIANT vResult; // 函数返回的计算结果
hr = pDisp->Invoke( // 调用函数
dispID, // 函数由 dispID 指定
IID_NULL,
LOCALE_SYSTEM_DEFAULT, // 使用系统默认的语言环境
DISPATCH_METHOD, // 调用的是方法,不是属性
&dispParams, // 参数
&vResult, // 返回值
NULL, // 不考虑异常处理
NULL); // 不考虑错误处理
ASSERT( SUCCEEDED( hr ) ); // 如果失败,说明参数传递错误
CString str; // 显示一下结果
str.Format("1 + 2 = %d", vResult.lVal );
AfxMessageBox( str );
pDisp->Release(); // 释放接口指针
::CoUninitialize(); // 释放 COM
}示例二、CComDispatchDriver 智能指针包装类的使用方法
void demo()
{
// 已经进行过了 COM 初始化
CLSID clsid; // 通过 ProgID 取得组件的 CLSID
HRESULT hr = ::CLSIDFromProgID( L"Simple8.DispSimple.1", &clsid );
ASSERT( SUCCEEDED( hr ) ); // 如果失败,说明没有注册组件
CComPtr < IUnknown > spUnk; // 由 CLSID 启动组件,并取得 IUnknown 指针
hr = ::CoCreateInstance( clsid, NULL, CLSCTX_ALL, IID_IUnknown, (LPVOID *)&spUnk );
ASSERT( SUCCEEDED( hr ) );
CComDispatchDriver spDisp( spUnk ); // 构造只能指针
CComVariant v1(1), v2(2), vResult; // 参数
hr = spDisp.Invoke2( // 调用2个参数的函数
L"Add", // 函数名是 Add
&v1, // 第一个参数,值为整数1
&v2, // 第二个参数,值为整数2
&vResult); // 返回值
ASSERT( SUCCEEDED( hr ) ); // 如果失败,说明或者没有 ADD 函数,或者参数错误
CString str; // 显示一下结果
str.Format("1 + 2 = %d", vResult.lVal );
AfxMessageBox( str );
}示例程序中使用了
Invoke2()函数,其实你根据不同的函数,还可以使用
Invoke0
()、
Invoke1
()、
InvokeN()、
PutProperty()、
GetProperty()......等等等,的确很方便。
示例三、加载类型库,产生包装类来使用
这个方法使用更简单一些,如果你观察 MFC 帮你产生的包装类的实现,你就会发现,其实它调用的是
IDispatch 接口函数。使用
vc6.0 的朋友,步骤如下:
1、建立一个 MFC 的应用程序
2、开启
ClassWizard,执行
Add Class,选择
From a type library

图二、加载类型库
3、然后找到你要使用的组件文件
simple6.dll(tlb 文件也可以
),选择接口后确认

图三、选择类型库中需要包装的接口
4、在适当的地方输入调用代码
#include "simple6.h" // 包装类的头文件
void demo()
{
// 已经进行过了 COM 初始化
IDispSimple spDisp; // 包装类的对象
spDisp.CreateDispatch( _T("Simple6.DispSimple.1") ) //启动组件
spDisp.xxx(...); // 调用函数
spDisp.ReleaseDispatch(); // 释放接口
} 使用
vc.net 的朋友,步骤如下:
1、建立一个 MFC 的应用程序
2、执行菜单“添加
\添加类”,选择 MFC 分类中的“类型库中的MFC类”

图四、添加类型库中的MFC类
3、选择组件文件
simple8.dll(或
tlb 文件
),并选择需要包装的接口

图五、选择文件和接口
4、在适当的位置输入调用代码
#include "CDispSimple.h" // 包装类的头文件
void demo()
{
// 已经进行过了 COM 初始化
CDispSimple spDisp; // 包装类的对象
spDisp.CreateDispatch( _T("Simple8.DispSimple.1") ) // 启动组件
spDisp.xxx(...); // 调用函数
spDisp.ReleaseDispatch(); // 释放接口
}示例四、使用
#import 方式调用组件
#import 方式在
第七回中已经作过介绍,这里就不多罗嗦了。大家下载本回的示例程序后,自己去看吧。并且一定要掌握这个方法,因为它的运行效率是最快的呀。
四、小结
留作业啦。在我们以前所实现的所有组件程序中,只添加了接口方法(函数),而没有添加接口属性(变量),你自己练习一下吧,很简单的,然后写个程序调用看看。其实对于 VC 来说,调用属性和调用方法没有太大的区别(vc 把属性包装为
GetXXX()/PutXXX()或
getXXX()/putXXX()的函数方式
),但在另外一些语言中(比如脚本语言)则更方便,设置属性值是:对象
.属性 = 变量或常量,获取属性值是:变量
= 对象
.属性。
本回书至此做一了断,更多组件设计和使用的知识,且听下回分解
......
注1:多个自动化接口的实现方法,我们以后再说。
注2:将来介绍 ITypeLib::GetTypeInfo()
的时候,大家再回味 IDispatch::GetTypeInfo()
吧。
注3:在后面介绍“事件”的时候,我们会自己真正去实现一个 IDispatch::Invoke()
函数。
注4:介绍多个双接口实现的时候,会谈到这个问题。
COM组件设计与应用(十)
IDispatch 接口 for vc.net
作者:杨老师
下载源代码
一、前言
终于写到了第十回,我也一直期盼着写这回的内容耶,为啥呢?因为自动化(automation)是非常常用、非常有用、非常精彩的一个 COM 功能。由于 WORD、EXCEL 等 OFFICE 软件提供了“宏”的功能,就连我们使用的VC开发环境也提供了“宏”功能,更由于 HTML、ASP、JSP 等都要依靠脚本(Script)的支持,更体现出了自动化接口的重要性。
如果你使用 vc6.0 的开发环境,请阅读前一回。
如果你使用 vc.net 2003,请继续......
二、IDispatch接口
如果是编译型语言,那么我们可以让编译器在编译的时候装载类型库,也就是装载接口的描述。在第七回文章当中,我们分别使用了 #include 方法和 #import 方法来实现的。装载了类型库后,编译器就知道应该如何编译接口函数的调用了---这叫“前绑定”。但是,如果想在脚本语言中使用组件,问题就大了,因为脚本语言是解释执行的,它执行的时候不会知道具体的函数地址,怎么办?自动化接口就为此诞生了---“后绑定”。
自动化组件,其实就是实现了 IDispatch 接口的组件。IDispatch 接口有4个函数,解释语言的执行器就通过这仅有的4个函数来执行组件所提供的功能。IDispatch 接口用 IDL 形式说明如下:(注1)
[
object,
uuid(00020400-0000-0000-C000-000000000046), // IDispatch 接口的 IID = IID_IDispatch
pointer_default(unique)
]
interface IDispatch : IUnknown
{
typedef [unique] IDispatch * LPDISPATCH; // 转定义 IDispatch * 为 LPDISPATCH
HRESULT GetTypeInfoCount([out] UINT * pctinfo); // 有关类型库的这两个函数,咱们以后再说
HRESULT GetTypeInfo([in] UINT iTInfo,[in] LCID lcid,[out] ITypeInfo ** ppTInfo);
HRESULT GetIDsOfNames( // 根据函数名字,取得函数序号(DISPID)
[in] REFIID riid,
[in, size_is(cNames)] LPOLESTR * rgszNames,
[in] UINT cNames,
[in] LCID lcid,
[out, size_is(cNames)] DISPID * rgDispId
);
[local] // 本地版函数
HRESULT Invoke( // 根据函数序号,解释执行函数功能
[in] DISPID dispIdMember,
[in] REFIID riid,
[in] LCID lcid,
[in] WORD wFlags,
[in, out] DISPPARAMS * pDispParams,
[out] VARIANT * pVarResult,
[out] EXCEPINFO * pExcepInfo,
[out] UINT * puArgErr
);
[call_as(Invoke)] // 远程版函数
HRESULT RemoteInvoke(
[in] DISPID dispIdMember,
[in] REFIID riid,
[in] LCID lcid,
[in] DWORD dwFlags,
[in] DISPPARAMS * pDispParams,
[out] VARIANT * pVarResult,
[out] EXCEPINFO * pExcepInfo,
[out] UINT * pArgErr,
[in] UINT cVarRef,
[in, size_is(cVarRef)] UINT * rgVarRefIdx,
[in, out, size_is(cVarRef)] VARIANTARG * rgVarRef
);
}
以上 IDispatch 接口函数的讲解,我们留到后回中进行介绍。如何在组件程序中实现这些函数那?还好,还好,就象 IUnknown 一样,MFC 和 ATL 都帮我们已经完成了。本回我们着重介绍组件的编写,下回则介绍组件的调用方法。
三、用 MFC 实现自动化组件 我写的这整个系列文章---《COM 组件设计与应用》,多是用 ATL 写组件程序,但由于自动化非常有用,在后续的文章中,还要给大家介绍组件的“事件”功能,还要介绍如何在 MFC 的程序中象 WORD 一样支持“宏”的功能。这些都要用到 MFC,所以就给读者唠一唠啦:-)
3-1:建立一个解决方案
3-2:建立一个 MFC DLL 项目,项目名称为“Simple7”

3-3:一定要选择附加功能中的“自动化”,切记!切记!

3-4:添加新类

3-5:在新建类中支持自动化
类名 你随便写个类名子啦
基类 一定要从 CComTarget 派生呀,只有它才提供了 IDispatch 的支持
自动化 - 无 表示不支持自动化,你要选择了它,那就白干啦
自动化 - 自动化 支持自动化,但不能被直接实例化。后面在讲解多个 IDispatch 的时候就用到它了,现在先不要着急。
自动化 - 可按类型ID创建 一定要选择这个项目,这样我们在后面的调用中,VB就能够CreateObject(),VC就能够CreateDispatch()对组件对象实例化了。注意一点,这个 ID 其实就是组件的 ProgID 啦。
3-6:选择接口,添加函数

3-7:添加函数。我们要写一个整数加法函数Add()。

3-8:再增加一个转换字符串大小写的函数 Upper()。

3-9:好了,下面开始输入程序代码:
LONG CDispSimple::Add(LONG n1, LONG n2)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
return n1 + n2;
}
BSTR CDispSimple::Upper(LPCTSTR str)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CString strResult(str);
strResult.MakeUpper();
return strResult.AllocSysString();
} 3-10:编译注册
如果上面的操作由于疏忽而发生了错误,那么你可以手工进行改正。
其一、你可以打开 IDL 文件进行修改,修改时要特别小心函数的声明中,有一个[id(n)] 的函数序号,可不要乱了;
其二、同步修改 H/CPP 中的函数声明和函数体;
其三、在CPP文件中,根据情况也要修改 BEGIN_DISPATCH_MAP/END_DISPATCH_MAP()函数影射宏。
正确编译后,vc.net 2003 比 vc6.0 要聪明多了,它会自动注册组件。如果复制到其它计算机上,你也需要手工执行 regsvr32.exe 进行注册。
四、用 ATL 实现双接口组件(操作方法和步骤,请参考
《COM 组件设计与应用(六)》)
4-1:建立一个 ATL 项目,项目名称为“Simple8”
4-2:选择 DLL 类型、非属性方式、不要选择任何附加选项
4-3:添加新类,选择ATL 的简单对象
4-4:输入简称和选项,选项按默认进行,也就是双重接口方式(注2)


4-5:增加函数。选择接口、鼠标右键菜单、添加方法...
Add([in] VARIANT v1, [in] VARIANT v2, [out, retval] VARIANT * pVal);
Upper([in] BSTR str, [out,retval] BSTR * pVal);
关于Add()函数,你依然可以使用 Add([in] long n1, [in] long n2, [out,retval] long * pVal) 方式。但这次我们没有使用 long ,而是使用了 VARIANT 做参数和返回值。这里我先卖个关子,往下看,就知道使用 VARIANT 的精彩之处了。
4-6:完成代码
STDMETHODIMP CDispSimple::Add(VARIANT v1, VARIANT v2, VARIANT *pVal)
{
::VariantInit( pVal ); // 永远初始化返回值是个好习惯
CComVariant v_1( v1 );
CComVariant v_2( v2 );
if((v1.vt & VT_I4) && (v2.vt & VT_I4) ) // 如果都是整数类型
{ // 这里比较没有使用 == ,而使用了运算符 & ,你知道这是为什么吗?
v_1.ChangeType( VT_I4 ); // 转换为整数
v_2.ChangeType( VT_I4 ); // 转换为整数
pVal->vt = VT_I4;
pVal->lVal = v_1.lVal + v_2.lVal; // 加法
}
else
{
v_1.ChangeType( VT_BSTR ); // 转换为字符串
v_2.ChangeType( VT_BSTR ); // 转换为字符串
CComBSTR bstr( v_1.bstrVal );
bstr.AppendBSTR( v_2.bstrVal ); // 字符串连接
pVal->vt = VT_BSTR;
pVal->bstrVal = bstr.Detach();
}
return S_OK;
}
STDMETHODIMP CDispSimple::Upper(BSTR str, BSTR *pVal)
{
*pVal = NULL; // 永远初始化返回值是个好习惯
CComBSTR s(str);
s.ToUpper(); // 转换为大写
*pVal = s.Copy();
return S_OK;
}
刚才卖的关子,现在开始揭密了......加法函数Add()不使用long类型,而使用VARIANT的好处是:函数内部动态判断参数类型,如果是整数则进行整数加法,如果是字符串,则进行字符串加法(字符串加法就是字符串连接哈)。也就是说,如果参数是VARIANT,那么我们就可以实现函数的可变参数类型呀。怪怪个咙,真爽!
五、脚本中调用举例 打开“记事本”程序,输入脚本程序,保存为 xxx.vbs 文件。然后在资源管理器里就可以双击运行啦。

如果你有能力,也可以用 JScript 书写上面的程序,然后保存为 xxx.js 文件,同样也可以在资源管理器里运行。另外需要说明的一点是,脚本程序文件的图标(win 2000下)是

,如果你不是这样的(有一个软件叫“XX 解霸”。写这款软件的人水平太低,它居然使用 .vbs 的扩展名文件作为它的数据流文件,破坏了系统默认的文件类型影射模式,咳......),那么需要重新设置,方法是:
六、WORD 中使用举例 6-1:录制一段宏程序


6-2:选择“键盘”,当然你也可以把这个“宏”程序放到“工具栏”上去。这里我们随便指定一个快捷键,比如Ctrl+Z

6-3:开始录制了,下面你随便输入点什么东东。然后点“停止”

6-4:接下来,我们执行菜单,选择这个刚刚录制的宏,然后编辑它

6-5:点“编辑”按钮,输入下面的程序:

不做解释了,你如果会一点点 VB ,就能看懂这个东东哈。然后保存关闭 VBA 的编辑器(注4)。
6-6:执行啦,执行啦,看看有什么效果呀......

然后按快捷键Ctrl+Z

你已经扩展了 MS WORD 的功能啦,嘿啦啦啦啦,嘿啦啦啦,天空出彩霞呀......我们只是举了一个简单的例子,其实这个例子并没有什么实际应用的意义,因为人家 WORD 本身就有大小写转换功能。但通过这个小例子,你可以体会出自动化组件的功能了,有够厉害吧?!
七、小结 没小结!嘿嘿......上当喽:-)
注1:以后我们描述接口函数,都采用 IDL 的形式了。
注2:双接口,是支持 IDispatch 接口的一种特殊接口方式,后面马上就要讲啦
注3:VBA 是专门开发 Office 的一种语言---Visual Basic for Application