在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 方式,叫 IID(InteraceID)。
3、组件中必须有3个函数,QueryInterface、AddRef、Release,它们3个函数也组成一个接口,叫"IUnknown"。(注7)
4.C/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启动组件 。 | 生成对象,执行构造函数,执行初始化动作。 |
2 | 你有IUnknown接口吗? | 有,给你! |
3 | 恩,太好了,那么你有IPersistStorage接口吗?(注9)
| 没有! |
4 | 真差劲,连这个都没有。那你有IPersistStreamInit接口吗?(注10)
| 哈,这个有,给! |
5 | 好,好,这还差不多。你现在给我初始化吧。
| OK,初始化完成了。 |
6 | 完成了?好!现在你读数据去吧。
| 读完啦。我根据数据,已经在窗口中显示出来了。 |
7 | 好,现在咱们各自处理用户的鼠标、键盘消息吧...... | ...... |
8 | 哎呀!用户要保存退出程序了。你的数据被用户修改了吗? | 改了,用户已经修改啦。 |
9 | 那好,那么用户修改后,你的数据需要多大的存储空间呀? | 恩,我算算呀......好了,总共需要500KB。 |
10 | 晕,你这么个小玩意居然占用这么大空间?!......好了,你可以存了。 | 谢谢,我已经存好了。 |
11 | 恩。拜拜了您那。(注11) | 执行析构函数,删除对象。 |
12 | 我自己也该退出了...... |
|
以上的IpersistStorage、IpersistStreamInit、Iunknown等都是标准接口,要实现OLE服务,必须实现这些接口。
容器WORD通过这些标准接口与EXCEL交互。
容器(或者说客户端)就是这样和组件进行对话,协商调用的。如果组件甲实现了 IA 接口,那么容器就会使用它,如果组件乙没有提供 IA 接口,但是它提供了 IB 接口,那么容器就会调用 IB 接口的函数 ...... 如此,容器程序根本就不需要知道组件到底是干什么的,组件到底是用什么语言开发的,组件的磁盘位置到底在哪里,它都可以正常运行。太奇妙了!太精彩了!怎一个“爽”字了得!