WORD 复合文件中,已经解决了保存 xls(Excel) 数据的问题了。

 

那么,接下来又要解决另2个问题:

1.当 WORD 程序读取复合文件,遇到了 xls 数据的时候,它该如何去读入、解析、显示 xls 的数据呢?如何启动EXCEL程序的呢?

 

2.作为容器的WORD程序是怎样与作为提供OLE服务的Excel 程序进行交互的呢?

一、        CLSID引入

 

解决启动EXCEL程序问题:

   有一个非常简单的解决方案,那就是在对象数据的前面,保存有处理这个数据的程序名。(见下图左上)

 

CLSID=ClassID:用于标识一个组件

         ProgID:是一个字符串的名字,CLSID ProgID 是一个概念的两个不同的表示形式。

IID = InterfaceID:用于标识一个接口

 

      以上三个概念详细说明和用法,请参见相关书籍。

 

      到此,我们已经知道了 CLSID ProgID 唯一地表示一个组件服务程序,那么根据这些ID,就可以加载运行组件,并为客户端程序提供服务了

 

二、虚表(virtual table)引入

作为客户端的WORD程序,它希望或者说它要求:WORD程序一但开发完成,然后不做任何修改就可以调用任意一个组件。举例来说:

1.    你可以在 Word 中嵌入 Excel,也可以嵌入 Picture,也可以嵌入任何第三方发表的 ActiveX 文档......也就是说,连 Word 自己都不知道使用它的人将会在 doc 里面插入什么东东;

2.    你可以在 HTML 文件中插入一个 ActiveX,也可以插入一个程序脚本Script......你自己写的插件也可以插入到 IE 环境中。为了完成你的功能, 你绝对也不会去让微软修改IE吧?!

插曲:

这个要求实在有点难度,Office 开发停滞了。说来话巧,一天老O(Office 项目的总工程师)和小B(VB 项目的总工程师)一起喝酒,老O向小B倾诉了他的烦恼:


O:怎么能让我写的程序C,可以调用其它人写的程序S中的函数?(C表示客户程序,S表示提供服务的程序)
B:你是不是喝糊涂了?让S作成 DLL,你去 LoadLibrary()

GetProcAddress()...FreeLibrary()?!
O:废话!要是这么简单就好了。问题是,连我都不知道这个S程序是干什么的?能干什么?我怎么调用呀?

B:嘿嘿......其实特简单,就是在 VBX 中必须实现7个函数,这7个函数名称和功能必须是:初始化、释放、显示、消息处理......,而至于它内部想干什么,我也管不着。我只是在需要的时候调用我需要的这7个函数。

 

 

关键一句:

“。。。至于它内部想干什么,我也管不着。我只是在需要的时候调用我需要的这7个函数。。。”

 

函数的调用,说白了,就是地址的转移,只要知道函数地址就够了。

 

1、我的程序C,要能调用任何人写的程序B。那么B必须要按照我事先的要求,提供我需要的函数F1(),F2(),F3(),K1(),K2()地址。
    2
、将提供出去的函数地址,放在一个函数指针数组中,此数组在C++中名为:VTAB(虚函数表);此虚表为程序的全局数据。如下图:

3.还不够好,需要改进一下,因为所有的函数地址都放在一个表中会不灵活、不好修改、不易扩展。恩,有了!按照函数功能的类型进行分类:

 

 

4.暂且只能如下描述:

C++中,一个虚表对应一个接口。虚表名对应接口的标识:IID

问题又来了,现在有2 VTAB 虚函数表,要是多个虚表呢,怎样对这些虚表进行管理呢?那必须要实现一个函数,并且这个函数地址必须放在所有表的开头(表中的第一个函数指针),这个函数就叫 QueryInterface()吧,通过表名(接口的IID)完成从一个表查找到另一个表的功能:(除了QueryInterface()函数,顺便也完成另外两个函数,叫 AddRef() Release()。这两个函数的功能以后再说)。

 

5。为了以后描述方便,不再使用上图(图四)的方法了,而使用图五

这样简洁的样式:



 

三、     接口(Interface)引入

1、函数是通过 VTAB 虚函数表向外提供其地址,而实际上,函数地址就是“二进制的虚拟地址”, 从另一个角度来看,不管用什么开发工具,只要支持多态特性的语言,编译器产生的代码都能生成这个表。这样就实现了组件的“二进制特性”轻松实现了组件的跨语言要求。
2
、假设有一个指针型变量保存着 VTAB 的首地址,则这个变量就叫“接口指针”(6) 变量命名的时候,习惯上加上"I"开头。另外为了区分不同的接口,每个接口 也都要有一个名字,该名字就和 CLSID 一样,使用 GUID 方式,叫 IIDInteraceID)。
3
、组件中必须有3个函数,QueryInterfaceAddRefRelease,它们3个函数也组成一个接口,叫"IUnknown"(7)

    4C/C++语言中需要事先对函数声明,那么就 会要求组件也必

   须提供C语言的头文件。不行!为了能使COM具有跨语言的能

   力,决定不再为任何语言提供对应的函数接口声明,而是独

   立地提供一个叫类型库(TLB)的声明(TLB文件实际上都放在COM组件的DLL内,VC提供了相应的编译指令IMPORT,将TLB文件从DLL中导出,在VC中,该文件,实际上相当与头文件)。每个语言的IDE环境自己去根据TLB生成自己语言需要的包装。这个性质叫“接口声明的独立性”(注8

5.其它方面就不多说,有关COM的详细技术请查看《COM技术内幕》。

 

四、 容器(WORD)与OLE服务器(EXCEL)的交互

回到我们的上一个话题,Word中嵌入一个组件,那么Word是如何协商使用这个组件的那?下面是容器和组件之间的一个模拟对话过程:
 

 

容器 协商部分

组件 应答部分

1

根据CLSID启动组件
CoCreateInstance()

生成对象,执行构造函数,执行初始化动作。

2

你有IUnknown接口吗?

有,给你!

3

恩,太好了,那么你有IPersistStorage接口吗?(9)


IUnknown::QueryInterface(IID_IPersistStorage...)


IUnknown::QueryInterface(IID_IPersistStorage...)

没有!

4

真差劲,连这个都没有。那你有IPersistStreamInit接口吗?(10)


IUnknown::QueryInterface(IID_IPersistStreamInit...)


IUnknown::QueryInterface(IID_IPersistStreamInit...)

哈,这个有,给!

5

好,好,这还差不多。你现在给我初始化吧。


IPersistStreamInit::InitNew()


IPersistStreamInit::InitNew()

OK,初始化完成了。

6

完成了?好!现在你读数据去吧。


IPersistStreamInit::Load()


IPersistStreamInit::Load()

读完啦。我根据数据,已经在窗口中显示出来了。

7

好,现在咱们各自处理用户的鼠标、键盘消息吧......

......

8

哎呀!用户要保存退出程序了。你的数据被用户修改了吗?
IPersistStreamInit::IsDirty()

改了,用户已经修改啦。

9

那好,那么用户修改后,你的数据需要多大的存储空间呀?
IPersistStreamInit::GetSizeMax()

恩,我算算呀......好了,总共需要500KB

10

晕,你这么个小玩意居然占用这么大空间?!......好了,你可以存了。
IPersistStreamInit::Save()

谢谢,我已经存好了。

11

恩。拜拜了您那。(11)
IPersistStreamInit::Release()
IUnknown::Release()

执行析构函数,删除对象。

12

我自己也该退出了......
PostQuitMessage()

 

以上的IpersistStorageIpersistStreamInitIunknown等都是标准接口,要实现OLE服务,必须实现这些接口。

容器WORD通过这些标准接口与EXCEL交互。

容器(或者说客户端)就是这样和组件进行对话,协商调用的。如果组件甲实现了 IA 接口,那么容器就会使用它,如果组件乙没有提供 IA 接口,但是它提供了 IB 接口,那么容器就会调用 IB 接口的函数 ...... 如此,容器程序根本就不需要知道组件到底是干什么的,组件到底是用什么语言开发的,组件的磁盘位置到底在哪里,它都可以正常运行。太奇妙了!太精彩了!怎一个“爽”字了得!
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐