起源及复合文件
复合文件概念:
文件的 COM 结构化存储的实现。复合文件将单独对象存储在单一的、结构化文件中,此文件由两个主要元素组成:存储对象和流对象。二者结合使用,可象文件内的文件系统一样起作用。
特点:
(1)复合文件的内部是使用指针构造的一棵树进行管理的。使用的是单向指针,因此当做定位操作的时候,向后定位比向前定位要快
(2)复合文件中的“流对象”,是真正保存数据的空间。它的存储单位为512字节。
(3)不同的进程,或同一个进程的不同线程可以同时访问一个复合文件的不同部分而互不干扰
(4)复合文件则提供了非常方便的“增量访问”能力
(5)当频繁地删除文件,复制文件后,磁盘空间会变的很零碎,需要使用磁盘整理工具进行重新整合。和磁盘管理非常相似,复合文件也会产生这个问题,在适当的时候也需要整理,但比较简单,只要调用一个函数就可以完成了
复合文件函数
复合文件的函数和磁盘目录文件的操作非常类似。所有这些函数,被分为3种类型:WIN API 全局函数,存储 IStorage 接口函数,流 IStream 接口函数(“接口”看成是完成一组相关操作功能的函数集合)
小结:
复合文件,结构化存储,是微软组件思想的起源,在此基础上继续发展出了持续性、命名、ActiveX、对象嵌入、现场激活......一系列的新技术、新概念。因此理解和掌握复合文件是非常重要的,即使在你的程序中并没有全面使用组件技术,复合文件技术也是可以单独被应用的。
GUID 和接口
CLSID(注1)的方式间接描述这些对象数据的处理程序路径。CLSID 其实就是一个号码,或者说是一个16字节的数。CLSID 的结构定义如下:
typedef struct _GUID {
DWORD Data1; // 随机数
WORD Data2; // 和时间相关
WORD Data3; // 和时间相关
BYTE Data4[8]; // 和网卡MAC相关
} GUID;
typedef GUID CLSID; // 组件ID
typedef GUID IID; // 接口ID
产生 CLSID
1. 如果使用开发环境编写组件程序,则IDE会自动帮你产生 CLSID;
2. 你可以手工写 CLSID,但千万不要和人家已经生成的 CLSID 重复呀,所以严重地不推荐
3. 程序中,可以用函数 CoCreateGuid() 产生 CLSID;
4. 使用工具产生 GUID(注2);
微软为了使用方便,也支持另一个字符串名称方式,叫 ProgID。由于 CLSID 和 ProgID 其实是一个概念的两个不同的表示形式,所以在程序中可以随便使用任何一种。疑问?!字符串名称容易重复啊!!
介绍一下 CLSID 和 ProgID 之间的转换方法和相关的函数:
函数
|
功能说明
|
CLSIDFromProgID()
CLSIDFromProgIDEx()
|
由 ProgID得到CLSID。没什么好说的,你自己都可以写,查注册表贝
|
ProgIDFromCLSID()
|
由 CLSID 得到 ProgID,调用者使用完成后要释放 ProgID 的内存(注5)
|
CoCreateGuid()
|
随机生成一个 GUID
|
IsEqualGUID()、IsEqualCLSID()、IsEqualIID()
|
比较2个ID是否相等
|
StringFromCLSID()、StringFromGUID2()、StringFromIID()
|
由 CLSID,IID 得到注册表中CLSID样式的字符串,注意释放内存
|
关于接口
COM是接口(组件)的集合,接口是方法和属性的集合。 要了解COM,就得先了解IUnknown接口,IUnknown接口的C++形式的定义如下:
interface IUnknown
{
virtual HRESULT _stdcall QueryInterface([in]REFIID iid,[out]void * * ppv)=0;
virtual ULONG _stdcall AddRef(void)=0;
virtual ULONG _stdcall Release(void)=0;
}
她实现了“接口查询”和“引用计数”,她是一个纯抽象基类。 所有COM 定义的接口都必须从她继承。
IUnknown是所有接口的基础,他负责两项工作:
IUnknown::QueryInterface负责得到该组件的其他接口的指针
IUnknown::AddRef/Release负责管理该组件的生存期,但有人使用该组件时,保证该组件不会被意外删除;再没人使用该组件时,保证该组件被自动删除
下面是容器和组件之间的一个模拟对话过程:
|
容器 协商部分
|
组件 应答部分
|
1
|
根据CLSID启动组件 。 CoCreateInstance()
|
生成对象,执行构造函数,执行初始化动作。
|
2
|
你有IUnknown接口吗?
|
有,给你!
|
3
|
恩,太好了,那么你有IPersistStorage接口吗?(注9) IUnknown::QueryInterface(IID_IPersistStorage...)
|
没有!
|
4
|
真差劲,连这个都没有。那你有IPersistStreamInit接口吗?(注10) IUnknown::QueryInterface(IID_IPersistStreamInit...)
|
哈,这个有,给!
|
5
|
好,好,这还差不多。你现在给我初始化吧。 IPersistStreamInit::InitNew()
|
OK,初始化完成了。
|
6
|
完成了?好!现在你读数据去吧。 IPersistStreamInit::Load()
|
读完啦。我根据数据,已经在窗口中显示出来了。
|
7
|
好,现在咱们各自处理用户的鼠标、键盘消息吧......
|
......
|
8
|
哎呀!用户要保存退出程序了。你的数据被用户修改了吗? IPersistStreamInit::IsDirty()
|
改了,用户已经修改啦。
|
9
|
那好,那么用户修改后,你的数据需要多大的存储空间呀? IPersistStreamInit::GetSizeMax()
|
恩,我算算呀......好了,总共需要500KB。
|
10
|
晕,你这么个小玩意居然占用这么大空间?!......好了,你可以存了。 IPersistStreamInit::Save()
|
谢谢,我已经存好了。
|
11
|
恩。拜拜了您那。(注11) IPersistStreamInit::Release();IUnknown::Release()
|
执行析构函数,删除对象。
|
12
|
我自己也该退出了...... PostQuitMessage()
|
|
数据类型
简单调用组件
1、组件的启动和释放
图一 组件调用机制
由上图可以看出,当调用组件的时候,其实是依靠代理(运行在本地)和存根(运行在远端)之间的通讯完成的。具体来说,当客户程序通过 CoCreateInstance() 函数启动组件,则代理接管该调用,它和存根通讯,存根则它所在的本地(相对于客户程序来说就是远程了)执行 new 操作加载对象。遵守几个原则:
(1)启动组件得到一个接口指针(Interface)后,不要调用AddRef()。因为系统知道你得到了一个指针,所以它已经帮你调用了AddRef()函数;
(2)通过QueryInterface()得到另一个接口指针后,不要调用AddRef()。因为......和上面的道理一样;
(3)当你把接口指针赋值给(保存到)另一个变量中的时候,请调用AddRef();
(4)当不需要再使用接口指针的时候,务必执行Release()释放;
(5)当使用智能指针的时候,可以省略指针的维护工作;
2、内存分配和释放
函数内部根据实际需要动态申请内存,而调用者负责释放。这虽然违背了上述原则,但 COM 从方便性和效率出发,确实是这么设计的。
|
C语言
|
C++语言
|
Windows 平台
|
COM
|
IMalloc 接口
|
BSTR
|
申请
|
malloc()
|
new
|
GlobalAlloc()
|
CoTaskMemAlloc()
|
Alloc()
|
SysAllocString()
|
重新申请
|
realloc()
|
|
GlobalReAlloc()
|
CoTaskRealloc()
|
Realloc()
|
SysReAllocString()
|
释放
|
free()
|
delete
|
GlobalFree()
|
CoTaskMemFree()
|
Free()
|
SysFreeString()
|
以上这些函数必须要按类型配合使用(比如:new 申请的内存,则必须用 delete 释放)。在 COM 内部,当然你可以随便使用任何类型的内存分配释放函数,但组件如果需要与客户进行内存的交互,则必须使用上表中的后三类函数族。
(1)BSTR 内存在上回书中,已经有比较丰富的介绍了,不再重复;
(2)CoTaskXXX()函数族,其本质上就是调用C语言的函数(malloc...);
(3)IMalloc 接口又是对 CoTaskXXX() 函数族的一个包装。包装后,同时增强了一些功能,比如:IMalloc::GetSize()可以取得尺寸,使用 IMallocSpy 可以监视内存的使用;
3、参数传递方向
参数是动态分配的内存指针,那么遵守如下的规定:
方向
|
申请人
|
释放人
|
提示
|
[in]
|
调用者
|
调用者
|
组件接收指针后,不能重新分配内存
|
[out]
|
组件
|
调用者
|
组件返回指针后,调用者“爱咋咋地”(注3)
|
[in,out]
|
调用者
|
调用者
|
组件可以重新分配内存
|
用 ATL 写第一个组件(关于流程多写代码就熟悉了,这里不重复!)
1、建立 ATL 工程方法
图一、建立 ATL DLL 工程
Dynamic Link Library(DLL) 表示建立一个 DLL 的组件程序。
Executable(EXE) 表示建立一个 EXE 的组件程序。
Service(EXE) 表示建立一个服务程序,系统启动后就会加载并执行的程序。
Allow merging of proxy/stub code 选择该项表示把“代理/存根”代码合并到组件程序中,否则需要单独编译,单独注册代理存根程序。代理/存根,这个是什么概念?还记得我们在上回书中介绍的吗?当调用者调用进程外或远程组件功能的时候,其实是代理/存根负责数据交换的。关于代理/存根的具体变成和操作,以后再说啦......
Support MFC 除非有特殊的原因,我们写 ATL 程序,最好不要选择该项。
2、增加 ATL 对象类方法
图二、选择建立简单COM对象
Category Object 普通组件。其中可以选择的组件对象类型很多,但本质上,就是让向导帮我们默认加上一些接口。比如我们选 "Simple Object",则向导给我们的组件加上 IUnknown 接口;我们选 "Internet Explorer Object",则向导除了加上 IUnknown 接口外,再增加一个给 IE 所使用的 IObjectWithSite 接口。当然了,我们完全可以手工增加任何接口。
Category Controls ActiveX 控件。其中可以选择的 ActiveX 类型也很多。我们在后续的专门介绍 ActiveX 编程中再讨论。
Category Miscellaneous 辅助杂类组件。
Categroy Data Access 数据库类组件(我最讨厌数据库编程了,所以我也不会)。
图四、接口属性
Threading Model 选择组件支持的线程模型。默认Apartment,它代表什么那?简单地说:当在线程中调用组件函数的时候,这些调用会排队进行。因此,这种模式下,我们可以暂时不用考虑同步的问题。
Interface 接口基本类型。Dual 表示支持双接口(注2),这个非常 非常重要,非常非常常用,但我们今天不讲。Custom 表示自定义借口。切记!切记!我们的这第一个 COM 程序中,一定要选择它!!!!(如果你选错了,请删除全部内容,重新来过。)
Aggregation 我们写的组件,将来是否允许被别人聚合(注3)使用。Only 表示必须被聚合才能使用,有点类似 C++ 中的纯虚类,你要是总工程师,只负责设计但不亲自写代码的话,才选择它。
Support ISupportErrorInfo 是否支持丰富信息的错误处理接口。以后就讲。
Support Connection Points 是否支持连接点接口(事件、回调)。以后就讲。
Free Threaded Marshaler 暂时我也不知道
3、添加接口函数方法
编译、注册、调用
关于编译
2-1 最小依赖
“最小依赖”,表示编译器会把 ATL 中必须使用的一些函数静态连接到目标程序中。这样目标文件尺寸会稍大,但独立性更强,安装方便;反之系统执行的时候需要有 ATL.DLL 文件的支持。如何选择设置为“最小依赖”呢?答案是:删除预定义宏“_ATL_DLL”,操作方法见图一、图二。
图一、在vc6.0中,设置方法
图二、在 vc.net 2003中,设置方法
2-2 CRT库
如果在 ATL 组件程序中调用了 CRT 的运行时刻库函数,比如开平方 sqrt() ,那么编译的时候可能会报错“error LNK2001: unresolved external symbol _main”。怎么办?删除预定义宏“_ATL_MIN_CRT”!操作方法也见图一、图二。(vc.net 2003 中的这个项目属性叫“在 ATL 中最小使用 CRT”)
2-3 MBCS/UNICODE
这个不多说了,在预定义宏中,分别使用 _MBCS 或 _UNICODE。
2-4 IDL 的编译
COM 在设计初期,就定了一个目标:要能实现跨语言的调用。既然是跨语言的,那么组件的接口描述就必须在任何语言环境中都要能够认识。怎么办?用 .h 文件描述?------ C语言程序员笑了,真方便!BASIC 程序员哭了:-( 因此,微软使用了一个新的文件格式---IDL文件(接口定义描述语言)。IDL 是一个文本文件,它的语言语法比较简单,很象C。具体 IDL 文件的讲解,见下一回《COM 组件设计与应用(八)之添加新接口》。IDL 经过编译,生成二进制的等价类型库文件 TLB 提供给其它语言来使用。图三示意了 ATL COM 程序编译的过程:
图三、ATL 组件程序编译过程
说明1:编译后,类型库以 TLB 文件形式单独存在,同时也保存在目标文件的资源中。因此,我们将来在 #import 引入类型库的时候,既可以指定 TLB 文件,也可以指定目标文件;
说明2:我们作为 C/C++ 的程序员,还算是比较幸福的。因为 IDL 编译后,特意为我们提供了 C 语言形式的接口文件。
说明3:IDL 编译后生成代理/存根源程序,有:dlldata.c、xxx_p.c、xxxps.def、xxxps.mak,我们可以用 NMAKE.EXE 再次编译来产生真正的代理/存根DLL目标文件(注1)。
关于注册
情况1:当我们使用 ATL 编写组件程序,注册不用我们来负责。编译成功后,IDE 会帮我们自动注册;
情况2:当我们使用 MFC 编写组件程序,由于编译器不知道你写的是否是 COM 组件,所以它不会帮我们自动注册。这个时候,我们可以执行菜单“Tools\Register Control”来注册。
情况3:当我们写一个具有 COM 功能的 EXE 程序时,注册的方法就是运行一次这个程序;
情况4:当我们需要使用第三方提供的组件程序时,可以命令行运行“regsvr32.exe 文件名”来注册。顺便说一句,反注册的方法是“regsvr32.exe /u 文件名”;
情况5:当我们需要在程序中(比如安装程序)需要执行注册,那么:
typedef HRESULT (WINAPI * FREG)();
TCHAR szWorkPath[ MAX_PATH ];
::GetCurrentDirectory( sizeof(szWorkPath), szWorkPath ); // 保存当前进程的工作目录
::SetCurrentDirectory( 组件目录 ); // 切换到组件的目录
HMODULE hDLL = ::LoadLibrary( 组件文件名 ); // 动态装载组件
if(hDLL)
{
FREG lpfunc = (FREG)::GetProcAddress( hDLL, _T("DllRegisterServer") ); // 取得注册函数指针
// 如果是反注册,可以取得"DllUnregisterServer"函数指针
if ( lpfunc ) lpfunc(); // 执行注册。这里为了简单,没有判断返回值
::FreeLibrary(hDLL);
}
::SetCurrentDirectory(szWorkPath); // 切换回原先的进程工作目录
上面的示例,在多数情况下可以简化掉切换工作目录的代码部分。但是,如果这个组件在装载的时候,它需要同时加载一些必须依赖的DLL时,有可能由于它自身程序的 BUG 导致无法正确定位。咳......还是让我们自己写的程序,来弥补它的错误吧......谁让咱们是好人呢 ,谁让咱们的水平比他高呢,谁让咱们在 vckbase 上是个“榜眼”呢......
关于组件调用
总的来说,调用组件程序大概有如下方法:
#include 方法
|
IDL编译后,为方便C/C++程序员的使用,会产生xxx.h和xxx_i.c文件。我们真幸福,直接#include后就可以使用了
|
#import 方法
|
比较通用的方法,vc 会帮我们产生包装类,让我们的调用更方便
|
加载类型库包装类 方法
|
如果组件提供了 IDispatch 接口,用这个方法调用组件是最简单的啦。不过还没讲IDispatch,只能看以后的文章啦
|
加载ActiveX包装类 方法
|
ActiveX 还没介绍呢,以后再说啦
|
实现多接口
按照函数的功能进行分类,把不同功能分类的函数用多个接口表现出来。这样可以有如下的一些好处:
1、一个接口中的函数个数有限、功能集中,使用者容易学习、记忆和调用。一个接口到底提供多少个函数合适那?答案是:如果你是黑猩猩,那么一个接口最多3个函数,如果你是人,那么一个接口最好不要超过7个函数。(注1)
2、容易维护。至少你肉眼搜索的时候也方便一些呀。
3、容易升级。当我们给组件增加函数的时候,不要修改已经发表的接口,而是提供一个新的接口来完成功能扩展
实现
1 import "oaidl.idl";
2 import "ocidl.idl";
3 [
4 object,
5 uuid(072EA6CA-7D08-4E7E-B2B7-B2FB0B875595),
6 helpstring("IMathe Interface"),
7 pointer_default(unique)
8 ]
9 interface IMathe : IUnknown
10 {
11 [helpstring("method Add")] HRESULT Add([in] long n1, [in] long n2, [out,retval] long *pnVal);
12 };
13 [
14 uuid(CD7672F7-C0B4-4090-A2F8-234C0062F42C),
15 version(1.0),
16 helpstring("Simple3 1.0 Type Library")
17 ]
18 library SIMPLE3Lib
19 {
20 importlib("stdole32.tlb");
21 importlib("stdole2.tlb");
22 [
23 uuid(C6F241E2-43F6-4449-A024-B7340553221E),
24 helpstring("Mathe Class")
25 ]
26 coclass Mathe
27 {
28 [default] interface IMathe;
29 };
30 };
1-2
|
引入 IUnknown 和ATL已经定义的其它接口描述文件。import 类似与 C 语言中的 #include
|
3-12
|
一个接口的完整描述
|
4
|
object 表示本块描述的是一个接口。IDL文件是借用了PRC远程数据交换格式的说明方法
|
5
|
uuid(......) 接口的 IID,这个值是 ATL 自动生成的,可以手工修改或用 guidgen.exe 产生(注3)
|
6
|
在某些软件或工具中,能看到这个提示
|
7
|
定义接口函数中参数所使用指针的默认属性(注4)
|
9
|
接口叫 IMathe 派生自 IUnknown,于是 IMathe 接口的头三个函数一定就是QueryInterface,AddRef和Release
|
10-12
|
接口函数列表
|
13-30
|
类型库的完整描述(类型库的概念以后再说),下面所说明的行,是需要先了解的
|
18
|
#import 时候的默认命名空间
|
23
|
组件的 CLSID,CoCreateInstance()的第一个参数就是它
|
27-29
|
接口列表
|
28
|
[default]表示谁提供了IUnknown接口
|
手工修改IDL文件,黑体字部分是手工输入的。完成后保存。
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(072EA6CA-7D08-4E7E-B2B7-B2FB0B875595),
helpstring("IMathe Interface"),
pointer_default(unique)
]
interface IMathe : IUnknown
{
[helpstring("method Add")] HRESULT Add([in] long n1, [in] long n2, [out,retval] long *pnVal);
};
[ // 所谓手工输入,其实也是有技巧的:把上面的接口描述(IMathe)复制、粘贴下来,然后再改更方便哈
object,
uuid(072EA6CB-7D08-4E7E-B2B7-B2FB0B875595), // 手工或用工具产生的 IID
helpstring("IStr Interface"),
pointer_default(unique)
]
interface IStr : IUnknown
{
// 目前还没有任何接口函数
};
[
uuid(CD7672F7-C0B4-4090-A2F8-234C0062F42C),
version(1.0),
helpstring("Simple3 1.0 Type Library")
]
library SIMPLE3Lib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(C6F241E2-43F6-4449-A024-B7340553221E),
helpstring("Mathe Class")
]
coclass Mathe
{
[default] interface IMathe;
interface IStr; // 别忘了呦,这里还有一个那
};
};
3-4、打开头文件(Mathe.h),手工增加类的派生关系和接口入口表 ,然后保存。
class ATL_NO_VTABLE CMathe :
public CComObjectRootEx <CComSingleThreadModel>,
public CComCoClass <CMathe, &CLSID_Mathe>,
public IMathe, // 别忘了,这里加一个逗号
public IStr // 增加一个基类
{
public:
CMathe()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_MATHE)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CMathe) // 接口入口表。这里填写的接口,才能被QueryInterface()找到
COM_INTERFACE_ENTRY(IMathe)
COM_INTERFACE_ENTRY(IStr)
END_COM_MAP()
3-5、好了,一切就绪。接下来,就可以在 IStr 接口中增加函数了。
IDispatch 接口
自动化组件,其实就是实现了 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
);
}
用 MFC 实现自动化组件
3-1:建立一个工作区(Workspace)
3-2:建立一个 MFC DLL 工程(Project),工程名称为“Simple5”
3-3:一定要选择 automation,切记!切记!
3-4:建立新类
3-5:在新建类中支持automation
Class information - Name 你随便写个类名子啦
Class information - Base class 一定要从 CComTarget 派生呀,只有它才提供了 IDispatch 的支持
Automation - None 表示不支持自动化,你要选择了它,那就白干啦
Automation - Automation 支持自动化,但不能被直接实例化。后面在讲解多个 IDispatch 的时候就用到它了,现在先不要着急。
Automation - Createable by type ID 一定要选择这个项目,这样我们在后面的调用中,VB就能够CreateObject(),VC就能够CreateDispatch()对组件对象实例化了。注意一点,这个 ID 其实就是组件的 ProgID 啦。
3-6:启动 ClassWizard,选择 Automation 卡片,准备建立函数
3-7:添加函数。我们要写一个整数加法函数Add()。
IDispatch 及双接口的调用 双接口表示在一个接口中,同时支持自定义接口和 IDispatch
、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接口只支持自动化的参数类型,使用受到限制,某些情况下很不方便喽
|
还有很多弊病呦,不过现在我想不起来喽......
|
使用方法
示例程序
|
自动化组件的使用方式
|
简要说明
|
示例0
|
在脚本中调用
|
在第九回/第十回中,已经做了介绍
|
示例1
|
使用 API 方式调用
|
揭示 IDispatch 的调用原理,但傻子才去这么使用那,会累死了
|
示例2
|
使用 CComDispatchDriver 的智能指针包装类
|
比直接使用 API 方式要简单多啦,这个不错!
|
示例3
|
使用 MFC 装载类型库的包装方式
|
简单!好用!常用!但它本质上是使用 IDispatch 接口,所以执行效率稍差
|
示例4
|
使用 #import 方式加载类型库方式
|
#import 方式使用组件,咱们在第七回中讲过啦。常用!对双接口组件,直接调用自定义接口函数,不再经过 IDispatch,因此执行效率最高啦
|
错误与异常处理
事件和通知
流程:
客户端启动组件(Simple11.IEvent1.1)并得到接口指针 IEvent1 *;
调用接口方法 IEvent1::Advise() 把客户端内部的一个接收器(sink)接口指针(ICallBack *)传递到组件服务器中;
调用 IEvent1::Add() 去计算两个整数的和;
但是计算结果并不通过该函数返回,而是通过 ICallBack::Fire_Result() 返回给客户端;
当客户端不再需要接受事件的时候,调用 IEvent1::Unadvise() 断开和组件的联系。
连接点
持续性
属性包