原文:http://vckbase.com/index.php/wv/1236.html

一、前言

前段时间,由于工作比较忙,没有能及时地写作。其间收到了很多网友的来信询问和鼓励,在此一并表示感谢。咳......我也需要工作来养家糊口呀......

上回书介绍了两种方法来写自动化(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 及双接口的调用的更多相关文章

  1. COM组件 IDispatch 及双接口的调用

    转自:http://blog.csdn.net/cnhk1225/article/details/50555647 一.前言 前段时间,由于工作比较忙,没有能及时地写作.其间收到了很多网友的来信询问和 ...

  2. 【转载】COM 组件设计与应用(十)——IDispatch 接口 for VC.NET

    原文:http://vckbase.com/index.php/wv/1225.html 一.前言 终于写到了第十回,我也一直期盼着写这回的内容耶,为啥呢?因为自动化(automation)是非常常用 ...

  3. 【转载】COM 组件设计与应用(九)——IDispatch 接口 for VC6.0

    原文: http://vckbase.com/index.php/wv/1224.html 一.前言 终于写到了第九回,我也一直期盼着写这回的内容耶,为啥呢?因为自动化(automation)是非常常 ...

  4. 【转载】COM 组件设计与应用(十四)——事件和通知(vc.net)

    原文:http://vckbase.com/index.php/wv/1244.html 一.前言 我的 COM 组件运行时产生一个窗口,当用户双击该窗口的时候,我需要通知调用者: 我的 COM 组件 ...

  5. 【转载】COM 组件设计与应用(十三)——事件和通知(VC6.0)

    原文:http://vckbase.com/index.php/wv/1243.html 一.前言 我的 COM 组件运行时产生一个窗口,当用户双击该窗口的时候,我需要通知调用者: 我的 COM 组件 ...

  6. 【转载】COM 组件设计与应用(八)——实现多接口

    原文:http://vckbase.com/index.php/wv/1219.html 一.前言 从第五回开始到第七回,咱们用 ATL 写了一个简单的 COM 组件,之所以说简单,是因为在组件中,只 ...

  7. 【转载】COM 组件设计与应用(七)——编译、注册、调用

    原文:http://vckbase.com/index.php/wv/1218.html 一.前言 上两回中,咱们用 ATL 写了第一个 COM 组件程序,这回中,主要介绍编译.册和调用方法.示例程序 ...

  8. 【转载】COM 组件设计与应用(六)——用 ATL 写第一个组件

    原文:http://vckbase.com/index.php/wv/1216.html 一.前言 1.与 <COM 组件设计与应用(五)>的内容基本一致.但本回讲解的是在 vc.net ...

  9. 【转载】COM 组件设计与应用(五)——用 ATL 写第一个组件

    原文:http://vckbase.com/index.php/wv/1215.html 一.前言 1.如果你在使用 vc5.0 及以前的版本,请你升级为 vc6.0 或 vc.net 2003: 2 ...

随机推荐

  1. springMVC入门-07

    删除功能实现,对应controller类中的代码如下所示: @RequestMapping(value="/{username}/delete",method=RequestMet ...

  2. ORACLE 参数设置绑定变量

    使用 CURSOR_SHARING 参数 EXACT  默认,不替换 SIMIAR 当替换不会影响到执行计划时,才会将字面量替换成绑定变量 FORCE 只要有可能,字面量会被替换为绑定变量

  3. 修改UIView的默认Layer后,修改View的值会动态修改Layer的值

    修改UIView的默认Layer后,修改View的值会动态修改Layer的值 效果图: 如上图所示,当我们修改了一个UIView的子类中的Layer内置类型时(如上图中我们将CALayer直接替换成了 ...

  4. 3.5星|《硅谷产品》:Facebook网红社区产品经理经验谈

    硅谷产品:36讲直通世界级产品经理 作者是Facebook产品经理,目前负责的具体业务书的扉页上有含糊的介绍,书中没明确说,根据书中内容推测,主要是网红社区. 比较遗憾的是书中作者亲历的案例只有3个. ...

  5. Git rebase日志

    Git日志重写 为了方便管理,最近公司git接了jira,然后开发任务需要在jira上面先建立task,然后再task上面建立分支,后面该分支就和这个task进行了绑定. 因为之前一直使用传统的svn ...

  6. 【转】 java中Class对象详解和类名.class, class.forName(), getClass()区别

    Class对象的生成方式如下: 1.类名.class           说明: JVM将使用类装载器, 将类装入内存(前提是:类还没有装入内存),不做类的初始化工作.返回Class的对象 2.Cla ...

  7. Hive学习之路 (四)Hive的连接3种连接方式

    一.CLI连接 进入到 bin 目录下,直接输入命令: [hadoop@hadoop3 ~]$ hive SLF4J: Class path contains multiple SLF4J bindi ...

  8. Jsp实现在线作业提交系统

    Jsp实现在线作业提交系统 作为 Computer Science 的学生,凌晨四点之前睡都应该感到羞耻. 项目托管地址:https://github.com/four-in-the-morning/ ...

  9. css自问自答(一)

    css自问自答(一) 1.块级元素和行内元素特性与区别? 块级:display:block <div>.<p>.<h1>...<h6>.<ol&g ...

  10. saltstack之编写自定义模块

    编写自己的模块 1 默认会放在/srv/salt/_modules vi hello.py """ CLI Example : salt '*' hello.world ...