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

COM 组件设计与应用 系列文章:http://vckbase.com/index.php/piwz?&p=1

一、前言

书接上回,话说在 doc(Word) 复合文件中,已经解决了保存 xls(Excel) 数据的问题了。那么,接下来又要解决另一个问题:当 WORD 程序读取复合文件,遇到了 xls 数据的时候,它该如何启动 Excel 呢?启动后,又如何让 Excel 自己去读入、解析、显示 xls 数据呢?

二、CLSID 概念

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

图一、CLSID 的概念

这的确是一个简单的方法,但同时问题也很严重。在“张三”的计算机上,Excel 的路径是:"c:\office\Excel.exe",如果把这个 doc 文件复制到“李四”的计算机上使用,而“李四”的 Excel 的路径是:
"d:\Program files\Microsoft Office\Office\Excel.exe",完蛋了:-(

于是,微软想出了一个解决方案,那就是不使用直接的路径表示方法,而使用一个叫 CLSID(注1)的方式间接描述这些对象数据的处理程序路径。CLSID 其实就是一个号码,或者说是一个16字节的数。观察注册表(上图),在HKCR\CLSID\{......}主键下,LocalServer32(DLL组件使用InprocServer32) 中保存着程序路径名称。CLSID 的结构定义如下:

01.typedef struct _GUID {
02.DWORD Data1;    // 随机数
03.WORD Data2; // 和时间相关
04.WORD Data3; // 和时间相关
05.BYTE Data4[8];  // 和网卡MAC相关
06.} GUID;
07. 
08.typedef GUID CLSID;  // 组件ID
09.typedef GUID IID;    // 接口ID
10.#define REFCLSID const CLSID &
11. 
12.// 常见的声明和赋值方法
13.CLSID CLSID_Excel = {0x00024500,0x0000,0x0000,{0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}};
14.struct __declspec(uuid("00024500-0000-0000-C000-000000000046")) CLSID_Excel;
15.class DECLSPEC_UUID("00024500-0000-0000-C000-000000000046") CLSID_Excel;
16.// 注册表中的表示方法
17.{00024500-0000-0000-C000-000000000046}

用一个号码间接表示程序名,的确是个 Good idea,实现了组件位置的透明性,并方便地扩展出 DCOM(远程组件)。但,但,但,但.....CLSID 有16个字节共128位二进制数,干吗用这么长的数字呀?遥想当年......我还在上幼儿园的时候,人们设计了 socket,用 TCP/IP 协议进行网络通讯。每个参与通讯的计算机都有一个4字节的 IP 表示编号地址,范围是 0,0,0,0 ~ 255,255,255,255 共42亿个地址。可是没想到啊,没想到,自从 Internet 选择了TCP/IP 协议后,42亿个地址就不够全世界的劳动人民分配啦。除了劳动人民,还有冰箱、彩电、电饭锅、手机、手提电脑......这些都需要连网呀。在办公室通过网络开启电饭锅给我焖饭,下班回家后就能吃现成的啦,多幸福呀?!(注:在我们家老婆是领导,所以是我做饭。咳......)

由于前车之鉴,微软这次设计 CLSID/IID 就使用了GUID概念的16个字节,这下好啦,全世界60亿人口,每个人每秒钟分配10亿个号码,那么需要分配1800亿年。反正等到地球没有了都不会使用完的:-)

三、产生 CLSID

1.如果使用开发环境编写组件程序,则IDE会自动帮你产生 CLSID;

2.你可以手工写 CLSID,但千万不要和人家已经生成的 CLSID 重复呀,所以严重地不推荐;(可是微软的CLSID都是手工写的,这叫“只许州官放火,不许百姓点灯”) ;

3.程序中,可以用函数 CoCreateGuid() 产生 CLSID;

4.使用工具产生 GUID(注2);

vc6.0版本运行:"vc目录\Common\Tools\GuidGen.exe"程序(你可以参照上回文章中介绍的方法,把这个工具程序加到开发环境中,方便调用)。vc.net版本,在菜单“工具\创建GUID”中,就可以执行了。

四、ProgID 概念

每一个COM组件都需要指定一个 CLSID,并且不能重名。它之所以使用16个字节,就是要从概率上保证重复是“不可能”的。但是,(世界上就怕“但是”二字)微软为了使用方便,也支持另一个字符串名称方式,叫 ProgID(注3)。见上图注册表的ProgID 子键内容(注4)。由于 CLSID 和 ProgID 其实是一个概念的两个不同的表示形式,所以我们在程序中可以随便使用任何一种。(有些人就是讨厌,说话不算数。明明 GUID 的目的就是禁止重复,但居然又允许使用 ProgID?!ProgID 是一个字符串的名字,重复的可能性就太大了呀。赶明儿我也写个程序,我打算这个程序的 ProgID 叫“Excel.Application”,嘿嘿)下面介绍一下 CLSID 和 ProgID 之间的转换方法和相关的函数:

函数 功能说明
CLSIDFromProgID()、CLSIDFromProgIDEx() 由 ProgID 得到 CLSID。没什么好说的,你自己都可以写,查注册表贝
ProgIDFromCLSID() 由 CLSID 得到 ProgID,调用者使用完成后要释放 ProgID 的内存(注5)
CoCreateGuid() 随机生成一个 GUID
IsEqualGUID()、IsEqualCLSID()、IsEqualIID() 比较2个ID是否相等
StringFromCLSID()、StringFromGUID2()、StringFromIID() 由 CLSID,IID 得到注册表中CLSID样式的字符串,注意释放内存

五、接口(Interface)的来历

到此,我们已经知道了 CLSID 或 ProgID 唯一地表示一个组件服务程序,那么根据这些ID,就可以加载运行组件,并为客户端程序提供服务了。(启动组件程序的方法,会陆续介绍)。接下来先讨论如何调用组件提供的函数?-----接口。

作为客户端程序员,它希望或者说他要求:我的程序只写一次,然后不做任何修改就可以调用任意一个组件。举例来说:

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:哦......这个比较高级,但我现在不能告诉你,因为我怕你印象不深。

老O:~!・#¥%……―*......

小B:是这样的,在VB中,我们制定了一个标准,这个标准允许任何一个VB开发者,把他自己写的某个功能的小程序放在VB的工具栏上,这样就好象他扩展了 VB 的功能一样。

老O:哦?就是那个叫什么 VBX 的滥玩意儿?

小B:我呸......别看 VBX 这个东西不起眼儿,的确我也没看上它。但你猜怎么着?现在有成千上万的 VB 程序爱好者把他们写的各式各样功能的 VBX 小程序,放到网上,让大家共享那。

老O:哦~~~,那你们的这个 VBX 标准是什么?

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

老O:哦~~~,这样呀......对了,我现有个急事,我先走了。88,你付帐吧......

小B:喂!喂喂...... 走这么急干什么,钱包都掉了:-)

老O虽然丢了钱包,仍然兴奋地冲回办公室,他开始了思考......

1、我的程序C,要能调用任何人写的程序B。那么B必须要按照我事先的要求,提供我需要的函数F1(),F2(),F3(),K1(),K2()。

2、BASIC 是解释执行,因此它的函数不用考虑书写顺序,只要给出函数名,解释器就能找到。但我使用的是 C++呀......

3、C++编译后的代码中没有函数名,只有函数地址,因此我必须改进为用VTAB(虚函数表)表示函数入口:

图二、VTAB 的结构

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

图三、多个 VTAB 的结构

5、问题又来了,现在有2个 VTAB 虚函数表,那么怎么能够从一个表找到另一个表那?恩又有办法了,我要求你必须要实现一个函数,并且这个函数地址必须放在所有表的开头(表中的第一个函数指针),这个函数就叫 QueryInterface()吧,完成从一个表查找到另一个表的功能:(除了QueryInterface()函数,顺便也完成另外两个函数,叫 AddRef() 和 Release()。这两个函数的功能以后再说)

图四、COM 接口结构

6、为了以后描述方便,不再使用上图(图四)的方法了,而使用图五这样简洁的样式:

图五、COM 接口结构的简洁图示

六、接口(Interface)概念

1、函数是通过 VTAB 虚函数表提供其地址, 从另一个角度来看,不管用什么语言开发,编译器产生的代码都能生成这个表。这样就实现了组件的“二进制特性”轻松实现了组件的跨语言要求。

2、假设有一个指针型变量保存着 VTAB 的首地址,则这个变量就叫“接口指针”(注6), 变量命名的时候,习惯上加上"I"开头。另外为了区分不同的接口,每个接口 也都要有一个名字,该名字就和 CLSID 一样,使用 GUID 方式,叫 IID。

3、接口一经发表,就不能再修改了。不然就会出现向前兼容的问题。这个性质叫“接口不变性”。

4、组件中必须有3个函数,QueryInterface、AddRef、Release,它们3个函数也组成一个接口,叫"IUnknown"。(注7)

5、任何接口,其实都包含了 IUnknown 接口。随着你接触到更多的接口就会了更体会解到接口的另一个性质“继承性”。

6、在任何接口上,调用表中的第一个函数,其实就是调用 QueryInterface()函数,就得到你想要的另外一个接口指针。这个性质叫“接口的传递性”

7、C/C++语言中需要事先对函数声明,那么就 会要求组件也必须提供C语言的头文件。不行!为了能使COM具有跨语言的能力,决定不再为任何语言提供对应的函数接口声明,而是独立地提供一个叫类型库(TLB)的声明。每个语言的IDE环境自己去根据TLB生成自己语言需要的包装。这个性质叫“接口声明的独立性”(注8)

七、客户程序与组件之间的协商调用

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

  容器 协商部分 组件 应答部分
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()
 

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

八、小结

第二回中,介绍了两个非常重要的概念:CLSID 和 Interface。由于全篇都是概念描述而没有示例程序相配合,可能读者的理解还不太深入、不彻底。别着急,我们马上就要进入到组件程序设计阶段了,到那个时候,你根据具体的程序代码,再回过头来再次阅读本回文章,没读懂?哦......再读!慢慢地您老人家就懂了:-)

留作业啦......

1、IDispatch 接口的 IID 是多少?(哎~~~ 笨笨,在源程序中,用鼠标右键执行Go to definition 呀)

2、IPicture 接口有几个函数?功能是什么?(别玩了!你多大了?想不想在程序中显示 JPG 图像呀,看 MSDN 去)

想知道为什么COM函数总是返回 HRESULT 吗?想知道如何使用 BSTR、VARIANT 吗?想知道 COM 中应该如何使用内存吗?想知道如何使用 UNICODE 吗?......恩~~~,我现在不能告诉你,我现在告诉你,怕你印象不深!且听下回分解......

注1:CLSID = Class ID 上回书已经介绍了把CLSID写入复合文件的函数:WriteClassStg()、IStorage::SetClass()。

注2:GUID 全局唯一标示符,CLSID/IID 其实是借用了GUID的概念。

注3:ProgID = Program ID,等价于 CLSID, 是用字符串表示的。

注4:注册表子键 ProgID 和 VersionIndependentProgID 分别表示真正的 ProgID 和版本无关的 ProgID。比如在我计算机上安装的 Excel,它的 ProgID = "Excel.Application.9",而 VersionIndependentProgID = "Excel.Application"。

注5:COM 组件的内存管理,见后续的文章。

注6:Interface = 接口,以前微软不叫它接口,而叫协议Protocol。其实我 到认为这个词更贴切一些。

注7:IUnknown 这个名字起的好,居然叫“我不知道”:-),它的 IID 叫 IID_IUnknown,如果用注册表样式表示,那么它的值是{00000000-0000-0000-C000-000000000046}。

注8:TLB是由一个描述接口的文件 IDL 经过编译产生的。IDL 的说明,见后续的文章吧。

注9:IPersistStorage 是用复合文件的存储(Storage)功能来保存/读取数据用的一个接口。

注10:IPersistStreamInit 是用复合文件的流(Stream)功能来保存/读取数据用的一个接口。

注11:拜拜了您那 = 英语北京话,再见。

【转载】COM 组件设计与应用(二)——GUID 和 接口的更多相关文章

  1. xmlplus 组件设计系列之二 - 按钮

    除了图标以外,按钮也许是最简单的组件了,现在来看看如何定义按钮组件. 使用原生按钮组件 在 xmlplus 中,HTML 元素也以组件的方式存在.所以,你可以直接通过使用 button 标签或者 in ...

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

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

  3. xmlplus 组件设计系列之零 - xmlplus 简介

    xmlplus 是什么 xmlplus 是博主写的一个 JavaScript 框架,用于快速开发前后端项目. xmlplus 基于组件设计,组件是基本的构造块.评价组件设计好坏的一个重要标准是封装度. ...

  4. 【转载】COM 组件设计与应用(三)——数据类型

    原文:http://vckbase.com/index.php/wv/1206.html COM 组件设计与应用 系列文章:http://vckbase.com/index.php/piwz?& ...

  5. 【转载】COM 组件设计与应用(十七)——持续性

    原文:http://vckbase.com/index.php/wv/1264.html 一.前言 我们写程序,经常需要实现这样的需求: 例一.程序运行产生一个窗口,用户关闭的时候需要记录窗口的位置, ...

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

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

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

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

  8. 【转载】COM 组件设计与应用(十一)—— IDispatch 及双接口的调用

    原文:http://vckbase.com/index.php/wv/1236.html 一.前言 前段时间,由于工作比较忙,没有能及时地写作.其间收到了很多网友的来信询问和鼓励,在此一并表示感谢.咳 ...

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

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

随机推荐

  1. 解决win 和 ubuntu 虚拟机之间 无法 复制粘贴的问题,以及重装vmtool后,还是无法解决的办法

    第一步:重新安装vmware-tool 我这里已经装过了,所以显示这个,不然应该是显示:安装 VMware Tools 它会打开一个文件夹,把压缩包复制到任一个地方解压,执行.pl的那个文件:sudo ...

  2. Linux系统锁定关键文件

    锁定系统关键文件 1.密码.账号文件 chattr +i /etc/passwd /etc/group /etc/shadow /etc/gshadow /etc/inittab 加锁:chattr ...

  3. JAVA入门之程序设计环境搭建

    这篇文章写给刚接触或者想学JAVA的新朋友.学习JAVA,需要找一本好的入门书籍,推荐<Java从入门到精通>,然后就是JAVA程序设计开发环境的搭建. 首先,我们需要安装JAVA开发工具 ...

  4. AltiumDesigner17学习指南

    AltiumDesigner工程模板 工程文件管理 视图->桌面布局->默认 恢复界面 AltiumDesigner17功能 修改元件标号 双击元件标号,在Designetor的Value ...

  5. 内置函数 sorted

    内置函数 sorted 语法: sorted(iterable,key = None,reverse= false)iterable: 可迭代的对象key:排序规则(排序函数),在sorted内部将& ...

  6. Mina使用总结(一)MinaServer

    我们先看一个最简单的Mina Server服务端代码,该段代码实现了服务端Server启动并监听客户端请求 package com.bypay.mina.server; import java.io. ...

  7. Uva1395 POJ3522 Slim Span (最小生成树)

    Description Given an undirected weighted graph G, you should find one of spanning trees specified as ...

  8. 1.4 Installation and Setup(安装和设置)

    1.4 Installation and Setup(安装和设置) 这里我们用Anaconda发行版作为Python的使用环境,推荐安装Python3.6,本书就是用Python3.6代码写成的.(译 ...

  9. 超链接<a>标签用法

    1.a标签点击事件 1>1a href="javascript:js_method();" 这是我们平台上常用的方法,但是这种方法在传递this等参数的时候很容易出问题,而且 ...

  10. HBase学习之路 (二)HBase集群安装

    前提 1.HBase 依赖于 HDFS 做底层的数据存储 2.HBase 依赖于 MapReduce 做数据计算 3.HBase 依赖于 ZooKeeper 做服务协调 4.HBase源码是java编 ...