新书推介:《语义网技术体系》
作者:瞿裕忠,胡伟,程龚
   XML论坛     W3CHINA.ORG讨论区     计算机科学论坛     SOAChina论坛     Blog     开放翻译计划     新浪微博  
 
  • 首页
  • 登录
  • 注册
  • 软件下载
  • 资料下载
  • 核心成员
  • 帮助
  •   Add to Google

    >> 本版讨论高级C/C++编程、代码重构(Refactoring)、极限编程(XP)、泛型编程等话题
    [返回] 计算机科学论坛计算机技术与应用『 C/C++编程思想 』 → COM 组件设计与应用(一、二)起源及复合文件 查看新帖用户列表

      发表一个新主题  发表一个新投票  回复主题  (订阅本版) 您是本帖的第 12355 个阅读者浏览上一篇主题  刷新本主题   树形显示贴子 浏览下一篇主题
     * 贴子主题: COM 组件设计与应用(一、二)起源及复合文件 举报  打印  推荐  IE收藏夹 
       本主题类别:     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客楼主
    发贴心情 COM 组件设计与应用(一、二)起源及复合文件

    一、前言
      某个夜黑风高的晚上,我的一位老师跟我说:“小杨呀,以后写程序就和搭积木一样啦。你赶快学习一些OLE的技术吧......”,当时我心里就寻思 :“开什么玩笑?搭积木方式写程序?再过100年吧......”,但作为一名听话的好学生,我开始在书店里“踅摸”(注1)有关OLE的书籍(注2)。功夫不负有心人,终于买到了我的第一本COM书《OLE2 高级编程技术》,这本800多页的大布头花费了我1/5的月工资呀......于是开始日夜耕读.....

      功夫不负有心人,我坚持读完了全部著作,感想是:这本书,在说什么呐?

      功夫不负有心人,我又读完了一遍大布头,感想是:咳~~~,没懂!

      功夫不负有心人,我再,我再,我再读 ... 感想是:哦~~~,读懂了一点点啦,哈哈哈。

      ...... ......

      功夫不负有心人,我终于,我终于懂了。

      800页的书对现在的我来说,其实也就10几页有用。到这时候才体会出什么叫“书越读越薄”的道理了。到后来,能买到的书也多了,上网也更方便更便宜了......

      为了让VCKBASE上的朋友,不再经历我曾经的痛苦、不再重蹈我“无头苍蝇”般探索的艰辛、为了VCKBASE的蓬勃发展、为了中国软件事业的腾飞(糟糕,吹的太也高了)......我打算节约一些在 BBS 上赚分的时间,写个系列论文,就叫“COM组件设计与应用”吧。今天是第一部分——起源。

      二、文件的存储

      传说350年前,牛顿被苹果砸到了头,于是发现了万有引力。但到了二十一世纪的现在,任何一个技术的发明和发展,已经不再依靠圣人灵光的一闪。技术的进步转而是被社会的需求、商业的利益、竞争的压力、行业的渗透等推动的。微软在Windows平台上的组件技术也不例外,它的发明,有其必然因素。什么是这个因素那?答案是——文件的存储。

      打开记事本程序,输入了一篇文章后,保存。——这样的文件叫“非结构化文件”;

      打开电子表格程序,输入一个班的学生姓名和考试成绩,保存。——这样的文件叫“标准结构化文件”;

      在我们写的程序中,需要把特定的数据按照一定的结构和顺序写到文件中保存。——这样的文件叫“自定义结构化文件”;(比如 *.bmp 文件)

      以上三种类型的文件,大家都见的多了。那么文件存储就依靠上述的方式能满足所有的应用需求吗?恩~~~,至少从计算机发明后的50多年来,一直是够用的了。嘿嘿,下面看看商业利益的推动作用,对文件 的存储形式产生了什么变化吧。30岁以上的朋友,我估计以前都使用过以下几个著名的软件:WordStar(独霸DOS下的英文编辑软件),WPS(裘伯君写的中文编辑软件,据说当年的市场占有率高达90%,各种计算机培训班的必修课程),LOTUS-123(莲花公司出品的电子表格软件)......

      微软在成功地推出 Windows 3.1 后,开始垂涎桌面办公自动化软件领域。微软的 OFFICE 开发部门,各小组分别独立地开发了 WORD 和 EXCEL 等软件,并采用“自定义结构”方式,对文件进行存储。在激烈的市场竞争下,为了打败竞争对手,微软自然地产生了一个念头------如果我能在 WORD 程序中嵌入 EXCEL,那么用户在购买了我 WORD 软件的情况下,不就没有必要再买 LOTUS-123 了吗?!“恶毒”(中国微软的同志们看到了这个词,不要激动,我是加了引号的呀)的计划产生后,他们开始了实施工作,这就是 COM 的前身 OLE 的起源(注3)。但立刻就遇到了一个严重的技术问题:需要把 WORD 产生的 DOC 文件和 EXCEL 产生的 XLS 文件保存在一起。

         方案
      优点
      缺点

    建立一个子目录,把 DOC、XLS 存储在这同一个子目录中。 数据隔离性好,WORD 不用了解 EXCEL 的存储结构;容易扩展。 结构太松散,容易造成数据的损坏或丢失。

      不易携带。

    修改文件存储结构,在DOC结构基础上扩展出包容 XLS 的结构。 结构紧密,容易携带和统一管理。 WORD 的开发人员需要通晓 EXCEL 的存储格式;缺少扩展性,总不能新加一个类型就扩展一下结构吧?!


      以上两个方案,都有严重的缺陷,怎么解决那?如果能有一个新方案,能够合并前两个方案的优点,消灭缺点,该多好呀......微软是作磁盘操作系统起家的,于是很自然地他们提出了一个非常完美的设计方案,那就是把磁盘文件的管理方式移植到文件中了------复合文件,俗称“文件中的文件系统”。连微软当年都没有想到,就这么一个简单的想法,居然最后就演变出了 COM 组件程序设计的方法。可以说,复合文件是 COM 的基石。下图是磁盘文件组织方式与复合文件组织方式的类比图:

    按此在新窗口浏览图片
      图一、左侧表示一个磁盘下的文件组织方式,右侧表示一个复合文件内部的数据组织方式。

      三、复合文件的特点

    复合文件的内部是使用指针构造的一棵树进行管理的。编写程序的时候要注意,由于使用的是单向指针,因此当做定位操作的时候,向后定位比向前定位要快;
    复合文件中的“流对象”,是真正保存数据的空间。它的存储单位为512字节。也就是说,即使你在流中只保存了一个字节的数据,它也要占据512字节的文件空间。啊~~~,这也太浪费了呀?不浪费!因为文件保存在磁盘上,即使一个字节也还要占用一个“簇”的空间那;
    不同的进程,或同一个进程的不同线程可以同时访问一个复合文件的不同部分而互不干扰;
    大家都有这样的体会,当需要往一个文件中插入一个字节的话,需要对整个文件进行操作,非常烦琐并且效率低下。而复合文件则提供了非常方便的“增量访问”能力;
    当频繁地删除文件,复制文件后,磁盘空间会变的很零碎,需要使用磁盘整理工具进行重新整合。和磁盘管理非常相似,复合文件也会产生这个问题,在适当的时候也需要整理,但比较简单,只要调用一个函数就可以完成了。
    四、浏览复合文件

      VC6.0 附带了一个工具软件“复合文件浏览器”,文件名是“vc目录\Common\Tools\DFView.exe”。为了方便使用该程序,可以把它加到工具(tools)菜单中。方法是:Tools\Customize...\Tools卡片中增加新的项目。运行 DFView.exe,就可以打开一个复合文件进行观察了(注4)。但奇怪的是,在 Microsoft Visual Studio .NET 2003 中,我反而找不到这个工具程序了,汗!不过这恰好提供给大家一个练习的机会,在你阅读完本篇文章并掌握了编程方法后,自己写一个“复合文件浏览编辑器”程序,又练手了,还有实用的价值。

      五、复合文件函数

      复合文件的函数和磁盘目录文件的操作非常类似。所有这些函数,被分为3种类型:WIN API 全局函数,存储 IStorage 接口函数,流 IStream 接口函数。什么是接口?什么是接口函数?以后的文章中再陆续介绍,这里大家只要把“接口”看成是完成一组相关操作功能的函数集合就可以了。

         WIN API 函数
      功能说明

    StgCreateDocfile() 建立一个复合文件,得到根存储对象
    StgOpenStorage() 打开一个复合文件,得到根存储对象
    StgIsStorageFile() 判断一个文件是否是复合文件

      IStorage 函数
      功能说明

    CreateStorage() 在当前存储中建立新存储,得到子存储对象
    CreateStream() 在当前存储中建立新流,得到流对象
    OpenStorage() 打开子存储,得到子存储对象
    OpenStream() 打开流,得到流对象
    CopyTo() 复制存储下的所有对象到目标存储中,该函数可以实现“整理文件,释放碎片空间”的功能
    MoveElementTo() 移动对象到目标存储中
    DestoryElement() 删除对象
    RenameElement() 重命名对象
    EnumElements() 枚举当前存储中所有的对象
    SetElementTimes() 修改对象的时间
    SetClass() 在当前存储中建立一个特殊的流对象,用来保存CLSID(注5)
    Stat() 取得当前存储中的系统信息
    Release() 关闭存储对象
     
      IStream 函数
      功能说明

    Read() 从流中读取数据
    Write() 向流中写入数据
    Seek() 定位读写位置
    SetSize() 设置流尺寸。如果预先知道大小,那么先调用这个函数,可以提高性能
    CopyTo() 复制流数据到另一个流对象中
    Stat() 取得当前流中的系统信息
    Clone() 克隆一个流对象,方便程序中的不同模块操作同一个流对象
    Release() 关闭流对象
     
    WIN API 补充函数 功能说明


    WriteClassStg() 写CLSID到存储中,同IStorage::SetClass()
    ReadClassStg() 读出WriteClassStg()写入的CLSID,相当于简化调用IStorage::Stat()
    WriteClassStm() 写CLSID到流的开始位置
    ReadClassStm() 读出WriteClassStm()写入的CLSID
    WriteFmtUserTypeStg() 写入用户指定的剪贴板格式和名称到存储中
    ReadFmtUserTypeStg() 读出WriteFmtUserTypeStg()写入的信息。方便应用程序快速判断是否是它需要的格式数据。
    CreateStreamOnHGlobal() 内存句柄 HGLOBAL 转换为流对象
    GetHGlobalFromStream() 取得CreateStreamOnHGlobal()调用中使用的内存句柄

    为了让大家快速地浏览和掌握基本方法,上面所列表的函数并不是全部,我省略了“事务”函数和未实现函数部分。更全面的介绍,请阅读 MSDN。
    下面程序片段,演示了一些基本函数功能和调用方法。
    示例一:建立一个复合文件,并在其下建立一个子存储,在该子存储中再建立一个流,写入数据。

    void SampleCreateDoc()
    {
     ::CoInitialize(NULL); // COM 初始化
        // 如果是MFC程序,可以使用AfxOleInit()替代

     HRESULT hr;  // 函数执行返回值
     IStorage *pStg = NULL; // 根存储接口指针
     IStorage *pSub = NULL; // 子存储接口指针
     IStream *pStm = NULL; // 流接口指针

     hr = ::StgCreateDocfile( // 建立复合文件
      L"c:\\a.stg", // 文件名称
      STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE, // 打开方式
      0,  // 保留参数
      &pStg);  // 取得根存储接口指针
     ASSERT( SUCCEEDED(hr) ); // 为了突出重点,简化程序结构,所以使用了断言。
        // 在实际的程序中则要使用条件判断和异常处理

     hr = pStg->CreateStorage( // 建立子存储
      L"SubStg", // 子存储名称
      STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,
      0,0,
      &pSub);  // 取得子存储接口指针
     ASSERT( SUCCEEDED(hr) );

     hr = pSub->CreateStream( // 建立流
      L"Stm",  // 流名称
      STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,
      0,0,
      &pStm);  // 取得流接口指针
     ASSERT( SUCCEEDED(hr) );

     hr = pStm->Write(  // 向流中写入数据
      "Hello",  // 数据地址
      5,  // 字节长度(注意,没有写入字符串结尾的\0)
      NULL);  // 不需要得到实际写入的字节长度
     ASSERT( SUCCEEDED(hr) );

     if( pStm ) pStm->Release();// 释放流指针
     if( pSub ) pSub->Release();// 释放子存储指针
     if( pStg ) pStg->Release();// 释放根存储指针

     ::CoUninitialize()  // COM 释放
        // 如果使用 AfxOleInit(),则不调用该函数
    }
    按此在新窗口浏览图片
    图二、运行示例程序一后,使用 DFView.exe 打开观察复合文件的效果图

    示例二:打开一个复合文件,枚举其根存储下的所有对象。
    #include <atlconv.h> // ANSI、MBCS、UNICODE 转换

    void SampleEnum()
    { // 假设你已经做过 COM 初始化了

     LPCTSTR lpFileName = _T( "c:\\a.stg" );
     HRESULT hr;
     IStorage *pStg = NULL;
     
     USES_CONVERSION;    // (注6)
     LPCOLESTR lpwFileName = T2COLE( lpFileName ); // 转换T类型为宽字符
     hr = ::StgIsStorageFile( lpwFileName ); // 是复合文件吗?
     if( FAILED(hr) ) return;

     hr = ::StgOpenStorage(   // 打开复合文件
      lpwFileName,   // 文件名称
      NULL,
      STGM_READ | STGM_SHARE_DENY_WRITE,
      0,
      0,
      &pStg);    // 得到根存储接口指针

     IEnumSTATSTG *pEnum=NULL; // 枚举器
     hr = pStg->EnumElements( 0, NULL, 0, &pEnum );
     ASSERT( SUCCEEDED(hr) );

     STATSTG statstg;
     while( NOERROR == pEnum->Next( 1, &statstg, NULL) )
     {
      // statstg.type 保存着对象类型 STGTY_STREAM 或 STGTY_STORAGE
      // statstg.pwcsName 保存着对象名称
      // ...... 还有时间,长度等很多信息。请查看 MSDN

      ::CoTaskMemFree( statstg.pwcsName ); // 释放名称所使用的内存(注6)
     }
     
     if( pEnum ) pEnum->Release();
     if( pStg ) pStg->Release();
    }
    六、小结

      复合文件,结构化存储,是微软组件思想的起源,在此基础上继续发展出了持续性、命名、ActiveX、对象嵌入、现场激活......一系列的新技术、新概念。因此理解和掌握 复合文件是非常重要的,即使在你的程序中并没有全面使用组件技术,复合文件技术也是可以单独被应用的。祝大家学习快乐,为社会主义软件事业而奋斗:-)

    留作业啦......
    作业1:写个小应用程序,从 MSWORD 的 doc 文件中,提取出附加信息(作者、公司......)。
    作业2:写个全功能的“复合文件浏览编辑器”。

    注1:踅摸(xuemo),动词,北方方言,寻找搜索的意思。
    注2:问:为什么不上网查资料学习?
    答:开什么国际玩笑!在那遥远的1995年代,我的500块工资,不吃不喝正好够上100小时的Internet网。
    注3:OLE,对象的连接与嵌入。
    注4:可以用 DFView.exe 打开 MSWORD 的 DOC 文件进行复合文件的浏览。但是该程序并没有实现国际化,不能打开中文文件名的复合文件,因此需要改名后才能浏览。
    注5:CLSID,在后续的文章中介绍。
    注6:关于 COM 中内存使用的问题,在后续的文章中介绍。

    [此贴子已经被作者于2007-10-22 9:31:56编辑过]

       收藏   分享  
    顶(0)
      




    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/9/17 11:37:00
     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客2
    发贴心情 
    COM 组件设计与应用(二)——GUID 和 接口

    一、前言
      书接上回,话说在 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 的结构定义如下:

    typedef struct _GUID {
      DWORD Data1;  // 随机数
      WORD Data2;  // 和时间相关
      WORD Data3;  // 和时间相关
      BYTE Data4[8];  // 和网卡MAC相关
    } GUID;
    typedef GUID CLSID; // 组件ID
    typedef GUID IID;  // 接口ID
    #define REFCLSID const CLSID &
    // 常见的声明和赋值方法
    CLSID CLSID_Excel = {0x00024500,0x0000,0x0000,{0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}};
    struct __declspec(uuid("00024500-0000-0000-C000-000000000046")) CLSID_Excel;
    class DECLSPEC_UUID("00024500-0000-0000-C000-000000000046") CLSID_Excel;
    // 注册表中的表示方法
    {00024500-0000-0000-C000-000000000046}  用一个号码间接表示程序名,的确是个 Good idea,实现了组件位置的透明性,并方便地扩展出 DCOM(远程组件)。但,但,但,但.....CLSID 有16个字节共128位二进制数,干吗用这么长的数字呀?遥想当年......我还在上幼儿园的时候,人们设计了 socket,用 TCP/IP 协议进行[URL=http://www.newasp.cn/]网络[/URL]通讯。每个参与通讯的计算机都有一个4字节的 IP 表示编号地址,范围是 0,0,0,0 ~ 255,255,255,255 共42亿个地址。可是没想到啊,没想到,自从 Internet 选择了TCP/IP 协议后,42亿个地址就不够全世界的劳动人民分配啦。除了劳动人民,还有冰箱、彩电、电饭锅、手机、手提[URL=http://www.newasp.cn/]电脑[/URL]......这些都需要连网呀。在办公室通过[URL=http://www.newasp.cn/]网络[/URL]开启电饭锅给我焖饭,下班回家后就能吃现成的啦,多幸福呀?!(注:在我们家老婆是领导,所以是我做饭。咳......)

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

      三、产生 CLSID

      

    如果使用开发环境编写组件程序,则IDE会自动帮你产生 CLSID;
    你可以手工写 CLSID,但千万不要和人家已经生成的 CLSID 重复呀,所以严重地不推荐;(可是微软的CLSID都是手工写的,这叫“只许州官放火,不许百姓点灯”) ;
    程序中,可以用函数 CoCreateGuid() 产生 CLSID;
    使用工具产生 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,就可以加载运行组件,并为客户端程序提供服务了。(启动组件程序的方法,会陆续介绍)。接下来先讨论如何调用组件提供的函数?-----接口。

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

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

      你可以在 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 吗?......恩~~~,我现在不能告诉你,我现在告诉你,怕你印象不深!且听下回分解......

    一、前言

      书接上回,话说在 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) 中保存着程序路径名称。

    [此贴子已经被作者于2007-10-22 9:33:36编辑过]

    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/9/17 11:39:00
     
     zhoumi_1979 帅哥哟,离线,有人找我吗?
      
      
      等级:大一新生
      文章:1
      积分:65
      门派:XML.ORG.CN
      注册:2007/10/7

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给zhoumi_1979发送一个短消息 把zhoumi_1979加入好友 查看zhoumi_1979的个人资料 搜索zhoumi_1979在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看zhoumi_1979的博客3
    发贴心情 不得不佩服你的水平
    自己有水平,关键是还能够将技术描述得这样浅显易懂,人世间最难的事情莫过于将复杂问题简单化!
    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/17 23:02:00
     
     zhoumi_1979 帅哥哟,离线,有人找我吗?
      
      
      等级:大一新生
      文章:1
      积分:65
      门派:XML.ORG.CN
      注册:2007/10/7

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给zhoumi_1979发送一个短消息 把zhoumi_1979加入好友 查看zhoumi_1979的个人资料 搜索zhoumi_1979在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看zhoumi_1979的博客4
    发贴心情 天天来看你的续帖
    如题
    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/17 23:04:00
     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客5
    发贴心情 
    好了,续上了,前段时间比较忙没顾上,不好意思。

    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/19 8:36:00
     
     yqdrrjhan 帅哥哟,离线,有人找我吗?
      
      
      等级:大一新生
      文章:3
      积分:70
      门派:XML.ORG.CN
      注册:2006/3/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给yqdrrjhan发送一个短消息 把yqdrrjhan加入好友 查看yqdrrjhan的个人资料 搜索yqdrrjhan在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看yqdrrjhan的博客6
    发贴心情 
    这帖子是lz写的?
    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/23 21:20:00
     
     GoogleAdSense
      
      
      等级:大一新生
      文章:1
      积分:50
      门派:无门无派
      院校:未填写
      注册:2007-01-01
    给Google AdSense发送一个短消息 把Google AdSense加入好友 查看Google AdSense的个人资料 搜索Google AdSense在『 C/C++编程思想 』的所有贴子 访问Google AdSense的主页 引用回复这个贴子 回复这个贴子 查看Google AdSense的博客广告
    2024/4/30 21:51:49

    本主题贴数6,分页: [1]

    管理选项修改tag | 锁定 | 解锁 | 提升 | 删除 | 移动 | 固顶 | 总固顶 | 奖励 | 惩罚 | 发布公告
    W3C Contributing Supporter! W 3 C h i n a ( since 2003 ) 旗 下 站 点
    苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
    125.000ms