潘爱民 提供 2000/06/12 未经作者允许任何机构或个人不得以任何方式转载或复制本文
ATL之深入浅出
介绍一本关于ATL的书《ATL Internals》
潘爱民,5月7日,2000年
北京大学计算机研究所,100871
引言
面对计算机图书市场的繁荣景象,我经常感叹今天学习计算机开发技术的同道们是多么幸运。十年前,我们学习计算机语言非常不容易,要掌握各种开发工具只有靠自己的摸索和极少量的参考手册。我记得,94年我学习Visual C++和MFC的时候,基本上只有靠软件自带的联机帮助;现在情形大不同了,书店中的计算机图书琳琅满目,关于Visual C++和MFC的书籍尤其多。有几位编辑朋友劝我写一点这方面的书,我觉得不大有必要了,因为Visual C++的好书已经不少了,适合各种读者层次的书籍几乎都可以买到,而且有一些书还相当不错。不过,在98年的时候,我感觉关于COM书籍实在太少,几乎没有,于是我下定决心,自己写一本关于COM的书籍,在99年底的时候由清华出版社出版。很快地,关于COM的书籍已经很多了,有些国外的名著也相继引入国内。从这十多年的计算机开发技术历史来看,一门技术只要有很多的书籍来介绍,那么这项技术很快就会普及,否则就难以推广。
说到COM,相信在Windows平台上有过开发经验的朋友一定接触过,它是Windows操作系统的基本软件模型,从93年建立以来,为Windows平台的推广和发展做出了不可磨灭的贡献,而且其自身还在不断发展。但是要真正开发COM组件并不是很轻松,在Visual C++中,我们既可以使用MFC也可以使用ATL。MFC完全面向Windows应用,它用C++的封装技术建立了一套适合于开发Windows应用的C++类库,虽然在后期的版本中MFC提供了大量的COM支持,但是从基本的设计结构上讲,MFC不适合于开发专业的COM组件,它适合于在Windows应用的基础上提供相应的COM支持。
ATL则不同于MFC,它完全面向COM组件,其技术路线也不同于MFC,MFC使用的是C++中的继承、封装、嵌套等常规技术,而ATL使用了C++中模板、多继承等高级技术,甚至还用到了STL。所以学习和使用ATL要求我们必须熟悉这些C++高级特性。另一方面,ATL结构完全针对COM中的诸多规范,这就要求使用人员必须非常了解COM规范,才有可能真正把ATL用好。
虽然目前关于MFC的书籍很多,但是完全介绍ATL的书籍非常少,甚至根本没有,这不能不说是一个遗憾。我有幸在今年2月份看到一本ATL的英文原版书《ATL Internals》,本文将为大家介绍这本书。
在看这本书之前,我对ATL已经有了基本的了解,98年底由于写作的需要,我曾经读过ATL的部分源码,对于ATL的基本结构还算清楚。我刚开始看到这本书的时候,快速读了一章,并没有感觉这本书有多好,后来由于工作忙碌的原因,一直没有得闲,直到最近,我才仔细把这本书读了一遍,感觉这是一本不可多得的好书。以前我很少仔细阅读开发技术类的书籍,一则是由于自己读书太慢,二则是往往开发技术类的书籍不大值得精读。但是这本书我读得很仔细,因为这本书把ATL的精华几乎全表述出来了,ATL中的许多内容都能让你为之心动,作为一个程序员,这也是一个学习和提高的机会。
我写这篇文章的意图不仅仅是向读者介绍这本书,我也希望能够把我在阅读过程中的心得与大家分享。同时我还希望能够按照这本书的路线,向大家介绍一下ATL的结构和机理。
准备阅读
《ATL Internals》由Addison-Wesley出版社出版,作者为Brent Rector和Chris Sells,出版时间为1999年2月,全书600多页。关于这本书的背景知识可能对于许多COM迷和ATL迷来说很有意思。首先,这本书的序言是由ATL的发明人Jim Springfield所撰写,在序言中,Jim介绍了ATL的历史,对于书中所介绍的内容大加赞赏,称赞“阅读此书可以学到许多阅读源码所不能掌握的内容”,在序言的最后,Jim还谈到了ATL的将来。
其次,在作者写的自序中,他们提到了ATL离不开COM,要想掌握ATL,就一定要先掌握COM。ATL是一个产生C++/COM代码的框架,就如同C语言是一个产生汇编代码的框架,这个观点颇为新颖,仔细想来,确是如此。作者特别推荐了Don Box的书《Essential COM》,其实在英文书中,COM书籍不少,这显示出两位作者与Don Box的关系非同一般。我记得Don Box也曾经推荐过《ATL Internals》这本书,如果读者有机会到亚马逊网上书店(www.amazon.com)看看本书的书评就可以知道他们之间有很亲密的关系,其中作者之一Chris Sells与Don Box以及另外两人合作写了一本COM的书《Effective COM》(本书文中有多处推荐了这本书)。我相信他们的互荐是基于相互之间绝对了解的基础之上的,他们都是COM顶尖高手,也是ATL顶尖高手,读者经常可以在MSJ(Microsoft System Journal)杂志上看到他们的文章。
诚如作者所言,阅读此书需要极强的预备知识,按照我阅读此书之后的理解,读者在阅读此书之前应该有以下几方面的准备知识:
一定要懂COM。ATL完全针对COM,许多细节都是为了更好地实现COM而设计。如果读者仅仅看过《Inside COM》(清华大学出版社出版的《COM技术内幕》),那么要想通篇阅读《ATL Internals》还不够,建议读者再找其他的资料看一看。
一定要懂C++的模板技术。ATL充分发挥模板的优势,其整个体系结构完全建立在模板的基础之上,如果读者不熟悉模板,那么几乎无法阅读ATL。
基本了解STL(可选)。ATL的集合类和枚举类用到了STL,当然,如果读者不懂STL,基本不影响全局的理解,但是STL中容器的思想和COM集合对象的思想是相通的,ATL把两者有机地统一起来了。
所以说,《ATL Internals》是一本起点很高的书,原因在于ATL是一门起点很高的技术。据我所知,现在有许多程序员已经在使用ATL了,这是好现象,说明我们国内的程序员水平相当高,虽然我们的资料信息还不够丰富(至少对于ATL而言是这样),但是我们仍然紧跟这些新的技术。尽管如此,要想真正用好ATL,一定要了解ATL的机理,这不同于MFC的情形。假如我们不懂MFC的机理,一样可以做出很好的程序,利用MFC,在不懂OLE细节的情况下,一样可以提供OLE的支持。ATL要求我们很细致地调节它的类,根据需要选择合适的模板类,必要时还要修正它的行为。ATL尽管已经到了3.0版本,但是仍然有不少的错误,这本书已经指出了一些错误,但我相信肯定还会有更多的错误,这对程序员提出了更高的要求,确实是这样,因此结论是:使用ATL一定要懂ATL!
尽管我这样说,但我还是认为ATL是一项好技术、是一个好的COM模板类库。而且我也深感好书对于ATL程序员的重要性,有些东西是不能从源码和参考手册获得的,既然我看到了这本好书,那我应该把这本书介绍出来,让大家知道这本书。也让大家分享我的体会。如果有那家出版社能够引进这本书的话,则是我们广大ATL程序员的福音了。
内容介绍
下面我按照《ATL Internals》的叙述顺序,逐项介绍ATL的内容,希望读者不仅能够了解本书的内容,也能够借此了解ATL的机理。原书共包括十一章,我按照每一章所介绍的内容把这十一章分成四个部分。
第一部分 ATL的使用和功能展示
这一部分篇幅很小,只有短短的一章内容,读起来非常轻松。初时我以为整本书都是这样的,所以感觉这也是一本指南性质的书,当时就没有太重视。
在这一章里,作者简单介绍了ATL的概念,然后通过AppWizard和ATL Object Wizard创建COM服务程序和COM对象类的过程逐项介绍了Wizard中各个选项的含义。有了基本的工程框架和对象类框架之后,作者开始展示ATL的一些其他特性:
添加属性和方法。
增加另外的接口。
提供脚本支持。
提供永久特性,增加永久接口。
加入事件支持,利用COM的可连接对象特性。
对窗口的支持。
实现组件类别(component category)支持。
增加用户界面特性,用ATL开发ActiveX控制。
怎样包容ActiveX控制。
作者介绍这些内容非常简捷,但是能够让读者有一些基本的印象,在阅读后续内容之前对ATL有一个清晰的思路。如果读者对COM比较熟悉而且曾经有过ATL开发经验的话,读这部分内容会非常轻松。
第二部分 ATL实现COM:基础部分
这部分介绍了ATL实现COM的基本方法,包括四章内容,分别如下:ATL Smart Types、Objects in ATL、COM Servers、Interface Maps。这四章内容是ATL的精华所在,也是这本书的精华所在,如果说全书其他的内容都是可读可不读的话,那么这部分的内容则是不可不读的。
第二章介绍了ATL的智能类型(smart type),包括字符串类型、VARIANT类型以及接口指针类型。COM规范要求所有的字符必须用双字节字符,不是我们常用的ANSI字符,所以对于字符串的处理往往是我们编程工作中所必须面对的任务,虽然谈不上有多困难,但是往往要花掉我们不少时间来处理这些琐事。作者在这一章首先介绍了与字符表达有关的许多概念以及多种转换途径,然后介绍了ATL的基本字符类型封装类CComBSTR以及CComVariant。作者对于这两个类的介绍非常细致,指出了每个成员函数的一些细节,甚至个别缺陷。我在阅读时,对这一部分很感兴趣,虽然这些内容我基本上都知道,每个成员函数也可以通过ATL的源码得到其细节知识,但是读下来仍然感觉受益匪浅。反过来,这一章最后部分介绍的智能指针类,我的兴趣并不大,大概是我对智能指针一直存有偏见的原因吧,不过智能指针在ATL中用得很广泛,书中后面部分到处可见智能指针的应用。ATL的智能指针封装类是一个比较典型的、功能全面的智能指针模板类,有兴趣的读者可以读一读这两个类(CComPtr和CComQIPtr)的源码。
第三章的开篇就是COM的套间(apartment)和COM的线程模型,虽然篇幅很短,但是这些内容很重要。因为ATL是一个支持多线程模型的COM类库,为了支持多线程,代价是非常昂贵的,要求COM对象的每一个细节都要涉及到并发的可能性,ATL既要考虑到代码的大小,又要考虑到代码的运行效率,所以ATL用了许多技巧来保证其方案的有效性。介绍了线程模型之后,作者又讲述了实现COM基本接口IUnknown的一些考虑要点。之后再给出ATL对象的层次图,如果读者对于ATL中类的结构还不了解的话,那么这个层次图可以让你知道Wizard生成的类与其他的类有什么样的关系,这个图可以指导你阅读完后续的两章。有了这些准备知识之后,作者详述了ATL为对象提供的线程模型支持,限于本文的篇幅,我不能详细讲述这些内容。
讲述了线程模型之后,作者进一步介绍COM对象的基类CComObjectRootEx实现IUnknown相关的方法,有了线程模型的基础后实现引用计数非常简单,但是QueryInterface成员函数不是基类就能够完成的,它需要用到CComObjectRootEx派生类也就是Wizard生成的类所提供的接口信息。ATL通过接口映射表的形式提供对象的接口信息,通过多继承的方式实现多接口的支持。
COM对象的实例化非常与众不同,因为Wizard生成的类还只是一个抽象类,所以它不能够直接被实例化,即我们不能用new操作符生成一个对象。真正的对象类应该是CComObject,它实现了IUnknown的所有方法,并提供了一个用于创建对象的函数。如果对象支持聚合模型的话,那么最终的对象类应该是CComAggObject。为了统一两种情况以便减小最终的代码量,ATL提供了CComPolyObject类作为最终的对象类。
ATL的创建过程并不复杂,但是它提供了多阶段构造(multiphase construction),允许我们在创建过程中加入更多的控制代码,获得更大的灵活性。作者对这一部分的介绍甚为细致,还解释了ATL_NO_VTBL宏即novtbl编译指示符的含义,如果一个类声明了novtbl指示符,那么编译器在派生类的构造过程中不为基类产生虚表(vtable)。
这一章的内容是ATL的基础,读起来并不难,但是一定要清楚ATL类的层次关系,否则很容易陷入ATL的复杂语法之中。
第四章介绍COM服务程序的ATL支持,作为一个COM服务程序,它的主要任务是管理对象的注册、为每个对象提供一个类厂、以及自身的生存期管理。对于进程内组件和进程外组件还需要区别对待。ATL实现的对象分为可通过类厂创建的对象和不可创建的对象,不可创建的对象不需要类厂的支持,往往为服务程序中其他的对象所用。ATL实现这些功能主要通过对象映射表和CComModule类。
对象映射表是一个全局表,其中包括当前服务程序所实现的所有对象类,表中的每一项包括对象的CLSID、注册该对象信息的函数、创建类厂的函数、创建对象的函数等等。有了这些信息,服务程序就可以管理它所支持的每一个对象类。回过头来,为了让对象映射表管理好这些工作,每一个对象类也需要提供相应的函数或者信息,比如对象的注册函数、创建函数等。ATL的注册功能很强,除了标准对象的注册支持之外,它可以提取出内嵌在资源中的注册脚本文件(Registry Script File),实现更为灵活、功能强大的注册操作。对于使用者来说,只要编写资源脚本再加上一个宏声明即可。ATL对于类厂的支持在CComClassFactory类中实现,对象类从CComCoClass继承一个类厂创建类的定义_ClassFactoryCreatorClass。CComClassFactory类的实现没有用到模板参数,而是内嵌一个创建函数,由该函数完成实际的创建工作。这个过程并不复杂,书中讲得很清楚,而且书中还介绍了ATL实现IClassFactory2的方法。
CComModule是COM服务程序的主线,当我们创建一个ATL工程的时候,Visual C++都会为我们生成一个CComModule派生类,并且定义一个全局变量_Module,这就如同我们在MFC工程中使用CWinApp应用类一样。CComModule类的许多成员函数都对应了它所应该完成的任务,比如更新注册表的操作、获取类厂对象、注册类厂对象(对于进程外服务程序)等。
第四章介绍的内容对于我们理解ATL工程的总体思路非常有用,结合COM规范对COM实现中的所有细节要求,ATL给出了一种高效、针对小尺寸组件的实现方案。结合第三章的内容,就构成了ATL实现COM的基本技术框架。
第五章讲述ATL的接口映射表,实际上这是对第三章内容的补充,但是因为ATL的接口映射表比较灵活,而且多接口支持对于COM对象非常重要。所以作者单独用一章的篇幅来讲述接口映射表。对于多接口的对象,COM有很严格的规范来指示客户程序调用这些接口成员函数,特别是IUnknown::QueryInterface。为了遵循这些规范,并保持一定的灵活性,ATL使用了接口映射表的技术。接口映射表的原理非常简单,它以表的形式记录了每个接口的IID以及接口的vtable与对象类的this指针之间的偏移,但是ATL的接口映射表并没有这么简单,它以函数的形式把这样的逻辑封装起来,从而允许用户使用更为灵活的接口查询策略。
ATL用多继承的方法实现多接口的支持,如果两个接口的方法名和参数重合的话,这时就会产生问题,书中介绍了一种避免名字冲突的方法,方法并不复杂,但很有效。这一章还介绍了一种被称为“接口着色”的技术,其实很简单,只不过是按照COM所要求的虚表结构另行构造而已,其好处是可以实现两个接口语义完全相同但是IID却不相同的接口。这也体现了COM接口实现的灵活性。
除了支持多继承方式的接口之外,ATL有一个很强大的接口支持就是对于动态接口的支持,书中称为“tear-off interface”。每一个动态接口类都应该从CComTearOffObjectBase派生,以后当客户向对象请求该接口的时候,对象类会调用接口映射表中指定的创建函数创建该接口对象。
除了动态接口技术应用了接口映射表这种结构之外,还有对象类对聚合接口的支持,实现形式与动态接口非常类似,ATL对聚合的支持分有计划聚合(planned aggregation)和盲聚合(blind aggregation),可以说,ATL对聚合的支持比较全面,但是我们在使用的时候一定要谨慎,COM中的有些特性往往隐含着潜在的出错可能性,比如说盲聚合就是这样的一种特性。
这一章最后介绍了接口映射表的一些诀窍,包括接口映射表的链结构、拒绝支持某个接口、利用接口请求进行调试、对接口映射表的扩展(比如,利用接口映射表设置后门,通过后门得到对象类的this指针;以及基于对象实例的接口请求)。这一章所讲述的内容非常细节,涉及到COM规范中的许多细微的地方,读懂这一部分并不难,但是要求读者具有有关的COM背景知识。
以上四章内容是ATL的基石,即使把这一部分独立出来也可以构成一本书“ATL深入浅出”,如果读者要依靠ATL来编写COM组件的话,那么认真读懂这一部分就可以奠定工作的基础。如果有人说ATL使用C++语法非常花哨的话,那么他们一定是指这一部分所讲述的内容。由于C++模板语法本身的复杂性,加上ATL在许多模板类的定义中使用了“typedef”,再加上ATL也使用了类似MFC的宏结构,所以读起源码来非常晦涩。尽管作者讲述这一部分内容非常有条理,但我看这几章的时候不免要前后翻动,偶尔还要查看一下ATL的源码。但是一旦明白了ATL的思路,又不免为它的设计所折服。
第三部分 ATL实现COM:扩展部分
ATL实现COM的扩展部分包括三章内容,分别为:Persistence in ATL(ATL的永久特性支持)、Collections and Enumerators(集合对象和枚举器对象)、Connection Points(连接点对象)。这三章是面向COM应用层面的三个大方向,也是我们比较常用的一些COM特征。如果读者要全面掌握ATL的话,那么应该读一读这部分。
第六章介绍ATL对COM永久机制的支持,相对来说,这一章内容的介绍读起来要轻松得多,只要读者对COM永久模型比较熟悉即可。由于COM永久模型的复杂性主要位于客户程序一方,在对象一方只需要实现有关的几个永久接口,当然这些永久接口与对象本身的逻辑是密切相关的。这一章前面部分回顾了IPersistPropertyBag、IPersistStream[Init]、IPersistStorage永久接口的定义和实现,然后介绍ATL对这些永久接口的实现,重点介绍了属性映射表(Property Map)。ATL提供的永久接口的实现能够自动对属性映射表中的属性进行永久处理,即提供Load和Save支持。对于属性映射表不支持的永久内容(比如说书中所举的索引属性的例子),我们可以在适当的地方进行重载处理,ATL允许我们在多个地方重载这套机制。
在介绍了这几个常用的永久接口之后,作者还介绍了IPersistMemory接口,并细致说明了几个永久接口公共的成员函数GetSizeMax的重要性以及作者补充的实现方法。在这一章的最后,作者还给出了一个用永久特性实现自定义列集(marshaling)的一种方法,如果读者对自定义列集有兴趣的话,可以看一看这一章最后几页的介绍。
第七章介绍了COM集合对象和枚举器对象(enumerator)的ATL实现。在讲述集合和枚举器对象之前,作者先介绍了STL中的容器和迭代器(iterator),这是STL中数据组织和数据访问的基本形式,然后作者以一个类比,指出虽然STL不能直接用于COM,但是COM提供了类似的对象组织和访问机制,这就是COM集合对象和枚举器对象。
COM的集合对象是构成COM对象模型的基础,为了在客户程序一方特别是VB(Visual Basic)或者VBA作为客户程序时,它能够方便有效地访问集合对象,COM制定了集合对象的接口规范以及枚举对象的接口规范。ATL实现了这些规范,并且在ATL内部,还提供了多种途径来管理这些成员数据或者成员对象。
ATL的集合对象实现起来比较简单,只要按照COM规范,增加集合对象所特有的属性:Count、Item、_NewEnum即可。_NewEnum属性把集合对象和枚举对象联系起来了。在ATL中,枚举数组类为CComEnum,它以数组的形式管理其成员数据,值得一提的是,ATL在实现枚举接口的时候,为了方便对于数据的拷贝操作,专门抽象出一个被称为“拷贝策略”的类,由该类的静态成员函数实施成员拷贝操作。ATL真正实现枚举接口的类为CComEnumImpl,它是CComEnum的基类,CComEnumImpl的实现并不复杂,唯一值得注意的是CComEnumImpl内部保存数据的方式,既可以是快照方式,也可以引用集合对象中的数据。有了这些基础,加上上一部分介绍的ATL对象类,实现枚举对象就非常容易了,作者在书中用一个素数集合对象的例子讲述了整个过程,最终通过素数集合对象的_NewEnum属性把它与素数枚举对象联系起来。
第七章的后半部分讲述了以STL作为数据组织方式来实现COM集合对象和枚举对象,如果读者对于STL不是很熟悉的话,可以把这部分内容跳过去而无关大局。用STL实现集合对象和枚举对象与前面方法的主要不同在于对象的内部实现细节,基本的思路和模型仍然一致。对应于CComEnum的枚举器类为CComEnumOnSTL,对应于CComEnumImpl的枚举接口实现类为IEnumOnSTLImpl,这两个类的用法可以非常灵活,书中举例说明了这两个类的用法。如果用STL实现枚举对象,则集合类也需要提供相应的支持,比较好的做法是用STL来实现集合类。紧接着作者就介绍了ICollectionOnSTLImpl,它与CComEnumOnSTL配合起来实现集合对象和枚举对象。
作者在这一章还介绍了如何把ATL的数据类型(也就是在第二章讲述的一些类)封装到STL的容器中,这部分内容对于那些STL迷来说可能非常有用,也非常有意思。在这一章的最后,作者用前面讲到的内容构造了一个简单的对象模型例子,让读者知道如何把集合对象和枚举对象应用到对象模型中,起到一个全局指导的作用。
第八章讲述可连接对象的ATL实现,作者首先回顾了COM的可连接对象机制,指出ATL利用两个全局函数AtlAdvise和AtlUnadvise建立源对象和接收器对象之间的连接或者撤销两者之间的连接。可连接对象是COM的双向通信机制,它并没有应用复杂的技术,实际上就是COM的一个反向应用。
然后作者从一个例子开始讲述ATL实现可连接对象的全过程(分为七个步骤),包括如何实现IConnectionPointContainer接口、如何实现每个连接点对象、如何增加连接点映射表以及如何编写事件激发函数或者让Visual C++的集成环境产生事件激发函数。在介绍了源对象的实现过程之后,作者又介绍了客户程序一方实现事件接收器对象的过程,相对而言,这个过程涉及到的细节要多一些,因为ATL的模板类IDispatchImpl只支持双接口,不支持dispinterface,源对象的出接口(outgoing interface)往往是dispinterface,所以接收器对象要通过其他的途径来实现事件接口。ATL提供了两个模板类IDispEventImpl和IDispEventSimpleImpl用于接收器对象的实现,IDispEventImpl要借助于类型库所提供的出接口类型信息,这是最简单的实现方法,而IDispEventSimpleImpl不需要类型库的支持,这是效率最高的方法。这两种方法都需要用到事件接收器映射表(event sink map),程序员可以把具体的事件函数以及对应的dispid等信息通过ATL提供的一组宏提交给客户类。配合前面给出的源对象例子,作者在讲解过程中也提供了客户端接收器对象的例子程序。
在介绍了ATL中可连接对象的用法之后,作者继续讲解这套机制的实现细节,包括以下一些要点:
源对象实现IConnectionPointContainer接口的原理,包括如何操纵连接点对象枚举器、如何使用连接点映射表。
连接点对象实现类IConnectionPointImpl。包括如何管理多个连接、如何操纵连接枚举器对象、如何在一个源对象上实现多个连接点对象等细节。
事件接收器对象所涉及到的多个类。包括_IDispEvent、_IDispEventLocator、IDispEventSimpleImpl,同时也讨论了事件接收器映射表的基本原理。
这一章介绍的内容相对要简单一些,如果上一部分的基础打得比较好的话,这些内容可以轻松地读下来。尽管内容比较简单,但是对于我们熟练应用ATL的可连接对象机制非常有帮助,因为在实际工作中,完全手工实现可连接对象机制非常繁琐,即使ATL的Wizard中已经提供了连接点对象的支持,要让可连接对象和接收器对象真正工作起来还需要许多手工工作。所以这部分内容很有实际意义。
以上三章内容是COM扩展的内容,但是我们在实际工作中经常会碰到这些内容,尤其是COM的永久特性和可连接对象特性,具有非常广泛的应用背景,比如下一部分要讲到的ActiveX控制就同时需要这两种技术的支持,而集合对象和枚举器对象则是VBA程序所非常依赖的对象组织手段。
第四部分 ATL对窗口和ActiveX控制的支持
我们知道,COM是一个平台无关的组件规范,但是COM的应用几乎都是与Windows平台相关的,这是由COM的历史背景所决定的。本书第四部分讨论的内容是ATL如何实现与用户界面有关的功能,特别是如何封装窗口、如何支持ActiveX控制。
在Visual C++提供的两套类库中,MFC侧重于对Windows平台上界面特性的封装,包括各种风格的窗口程序、对话框、大量的控制类等,而ATL则侧重于对COM的封装。但是,如同MFC也提供了COM支持一样,ATL也提供了对用户界面的支持,这就是第九章所要讨论的ATL窗口封装,当然封装的基础仍然是Win32 API。
第九章讨论的内容不涉及到COM,完全是Windows平台上的与窗口有关的许多细节,作者从Windows窗口应用的基本模式讲起,讲到了窗口的三大要素:窗口类(WNDCLASSEX结构)、窗口句柄(HWND)和窗口过程(WndProc),这也是封装窗口类的几个要点。ATL的窗口类层次结构比MFC要简单得多,其中主要的类是CWindow、CWindowImpl、CDialogImpl和CContainedWindow,然后作者逐一介绍这些类。
CWindow类非常简单,它只是对窗口句柄HWND的封装,几乎所有与窗口有关的API函数都有对应的CWindow成员函数。这些成员函数仅仅是个简单封装而已。
CWindowImpl是ATL窗口类中的关键类,它一方面继承自CWindow,同时它解决了窗口的两个关键问题:窗口类的注册和窗口消息处理。窗口类的注册是创建该类窗口的必要条件,CWindowImpl类把这个过程作了封装,程序员只需使用简单的宏就可以完成这些必要的任务。CWindowImpl类实现窗口过程则使用了一点技巧,因为窗口过程是以HWND作为窗口对象标识的,而CWindowImpl是以this指针作为对象标识的,所以如何在HWND和this指针之间建立对应关系是关键所在。CWindowImpl的基类CWindowImplBaseT以StartWindowProc作为窗口过程,在第一次被调用的过程中建立两者的映射关系,它通过一个被称为“thunk”的对象在运行过程中建立起来的一组机器指令。每个窗口对象都有一个thunk对象。thunk的任务是在调用CWindowImpl类的静态成员函数WindowProc处理消息之前先用this指针代替栈中的HWND。书中对这个过程作了详细的说明,对于ATL的窗口底层封装有兴趣的读者可以看一看这部分内容介绍,很有意思的。
完成了HWND到this指针的映射还只是一小步的工作,窗口过程的根本目标是处理窗口的消息,ATL的消息处理机制非常强大,首先它利用一组宏构造出ProcessWindowMessage成员函数,一旦把这些宏展开,其实就是一个规范的switch语句,以及每个case下的一大堆if语句。这种程序结构明显不同于MFC的消息映射表,MFC消息映射表通过查表来处理每一个消息。ATL的消息支持非常灵活,我们可以按消息码指定消息处理函数,也可以按消息码的范围值指定处理函数,对于命令消息和通知消息有专门的宏提供支持。更为强大的是,ATL的消息表可以构成链,也就是把派生类和基类的消息表链接起来,如果有多个基类的话,可以把所有这些消息表都链起来。而且在每个类中,消息表中的消息处理项又可以分组,有的用于派生类,有的用于自身,为派生类提供了许多机会。
理解了CWindowImpl之后再来理解CDialogImpl则容易多了,同样的thunk技术,同样的消息链技术,所不同的是底层Win32 API有不同的处理,而且对话框分为有模式对话框和无模式对话框。如果读者用MFC编写过对话框类的话,一定对其中的DDX/DDV函数有印象,Class Wizard产生的这些函数调用完成了对话框成员变量与对话框上控制之间的数据交换和数据有效性判断,不幸的是,ATL没有提供这样的功能,我们只好自己解决,其实真要做起来并不难,而且自己实现可以更灵活。
我们知道MFC封装了大量的控制类,而ATL的窗口类树中并没有这么多的控制类,实际上,在ATL的atlcon例子中同样给出了所有这些控制的封装类,只是没有正式的文档而已,这些类用起来很简单,源码本身就是最好的文档,我们可以充分利用这些资源。
这一章最后还介绍了CContainedWindow,它的特殊之处在于,它接收到消息之后交给父窗口处理这些消息,父窗口既可以直接创建这样的子窗口,也可以对一个已经被创建的窗口对象利用“子类化”的技术截取其窗口过程。
第九章的内容与COM完全无关,但它是ATL库中不同缺少的一部分,特别对于有用户界面的ATL工程来说,更是非常重要。第十章则讲述如何用ATL来建立ActiveX控制(大多数中文资料把“ActiveX control”称为ActiveX控件)。ActiveX控制是COM技术的大集成,也是OLE技术的大集成,如果纯粹从技术角度来讲的话,几乎还没有一本书能够全面讲述ActiveX控制的各项技术。ATL对ActiveX控制的支持比较全面,而且它的应用非常灵活,程序员可以有所选择地选取某一部分。
第十章作者首先回顾了ActiveX控制的各项功能,然后从一个例子BullsEye的功能需求分析出发,利用ATL Wizard创建一个初始的ActiveX控制框架,对创建过程中涉及到的选项逐一作了说明。有了初始的控制代码之后,接下去的任务是逐项完善BullsEye控制的功能,分别如下:
首先是增加BullsEye控制的属性和方法,因为ActiveX控制是自动化技术的超集,包容器(客户)需要通过IDispatch接口或者双接口与控制进行通信,所以控制的属性和方法是基础,属性和方法分两种:库存的(stock)和自定义的(custom)。ATL支持库存属性的类为CStockPropImpl,它继承自IDispatchImpl类。而自定义的属性和方法则可以通过VC集成环境提供的“Add Method”和“Add Property”加入,它会自动更新IDL接口定义文件和相应的源代码.h文件。
加入事件支持。首先在IDL文件中加入出接口的事件定义,然后加入连接点支持以及IConnectionPointContainer接口的支持。ActiveX控制除了支持自定义的出接口之外,它还支持IPropertyNotifySink接口作为出接口,专门用于向包容器程序提供属性变化通知。ATL的类IPropertyNotifySinkCP实现了相应的连接点对象。实现连接点支持的基本机制如上一部分所介绍,只是ATL为ActiveX控制提供了更多的便利。作为与连接点相关的内容,一个ActiveX控制也应该支持IProvideClassInfo2接口,相应的ATL类为IProvideClassInfo2Impl。
作为ActiveX控制,在窗口中绘制必要的图形信息是它的任务之一,ATL只提供了绘制的框架,具体的绘制任务由派生类完成,对于我们编写ATL控制而言,就是一个OnDraw函数。
属性的永久性。这是第六章内容的综合应用,直接利用ATL提供的永久接口实现类,以及控制的属性映射表即可。
一个客户友好的ActiveX控制应该实现IQuickActivate接口,ATL想得很周到,它实现了这个接口,把有关的逻辑交给控制类的IQuickActivate_QuickActivate成员。
组件类别。利用ATL的类别映射表很容易实现控制的类别功能。
属性分类的功能。ATL没有提供支持,但是我们可以很方便地实现ICategorizeProperties接口。
针对属性的浏览功能。这是接口IPerPropertyBrowsing的任务,ATL提供了接口实现IPerPropertyBrowsingImpl,我们只要重载有关的成员函数即可。
ActiveX控制的键盘处理。这是ActiveX控制与包容器之间的协作基础,我们只要根据ActiveX控制的规范重载必要的函数即可。
这一章的内容覆盖面比较广,作者通过BullsEye例子程序展示了ATL实现全功能的ActiveX控制的诸多细节。有许多地方用到了前面讲述的ATL类或者相应的各种支持,这个例子对于我们自己实现ActiveX控制很有启发性。
最后第十一章介绍ActiveX被包容器程序使用的情况。作者首先介绍了ActiveX控制与包容器之间的协作概况,然后叙述ATL实现包容的基本技术。CAxHostWindow类是ATL实现包容控制的内部基本类。作者从ActiveX控制的创建过程解释了CAxHostWindow类如何参与包容器窗口与控制之间的协作,我们知道,包容器程序通过控制站点对象来管理ActiveX控制,这里的CAxHostWindow对象相当于控制的站点对象,它不是由客户程序显式创建,而是在包容器窗口创建控制的过程中被隐式创建,这个过程涉及到很多协作细节。作者花了很大的篇幅来讲述这个过程,一旦这个过程讲清楚了,那么其他的细节就显得非常简明。
加入事件控制的过程很简单,如第八章所述,利用IDispEventImpl类,加入接收器事件映射表,然后在适当的时候建立接收器与控制之间的连接即可。同样地,我们可以在包容器上实现IPropertyNotifySink接口,并建立它与控制之间的连接,以便处理控制的属性变化通知。对于属性页的处理也是包容器程序的一项任务,但处理起来比较简单,只需调用OLE函数OleCreatePropertyFrame即可。控制的永久处理也不复杂,在包容器的保存和恢复操作中分别调用控制永久接口的Save和Load成员即可。
除了在一般的窗口中包容ActiveX控制之外,对话框作为包容器窗口也是一种典型情况,对于客户程序而言,在对话框中使用ActiveX控制更为简便。在程序设计阶段,集成环境往往能够为我们做更多的事情,比如控制的初始状态处理、控制的事件处理等。但是仍然有些工作需要我们在后期手工进行处理,如建立接收器对象与控制之间的连接、编写事件函数等。
在实际应用中,用ActiveX控制或者Windows的标准控制构造新的控制是一项很有用的技术,这就是复合控制,ATL也支持复合控制,它把对话框的功能和ActiveX控制的功能结合起来。在构造复合控制的时候,我们可以指定一个对话框模板,把设计阶段完成的界面模板引入到复合控制中,这是一个非常简便的构造界面单元的方法。
另一个功能更为强大的构造界面单元的方法是HTML控制,它利用Web浏览器控制直接把HTML页面封装成一个新的ActiveX控制。由于它把HTML页面作为界面内容,所以使用的时候非常灵活,我们可以在HTML页面中嵌入脚本,可以使用多种字体,可以访问HTML文档的对象模型。
这一章的内容也比较广泛,但是它把上一章介绍的ActiveX控制与实际的应用结合起来了。而且通过这些内容的介绍,我们可以拓宽视野把ActiveX控制应用得更加灵活,把Web引入到我们的桌面程序上来,或者把桌面程序的功能引入到Web当中去。这两章的内容相对比较独立,它们构成了用ATL开发和使用ActiveX控制的主体。如果读者关注MSJ杂志的话,可以在1999年的2、3、4月期上找到有关这些内容的一个连载,文章的名字为:“Write ActiveX Controls Using Custom Interfaces Provided by ATL 3.0”。
第十一章是全书的最后一章,我们跟随作者从基本的向导开发学起,经过深入浅出的分析,终于达到了一个歇脚点,可以轻松一下了。但这不等于我们学习ATL的任务已经完成了,ATL还有很多内容有待于我们去挖掘。
结束语
对于COM应用的开发,ATL无疑是首选的工具,与MFC相比,ATL的规模还不算大,但是从上述的介绍我们可以看出,ATL涉及到了COM的方方面面。实际上,ATL的内容还要多得多,比如OLE DB的支持、MTS的支持等,尽管如此,如果我们有了这本书中的内容为基础,那么再去学习这些扩展的内容就会容易得多,结合ATL中实现COM的基本手段加上这些应用技术的背景知识,我们可以很容易地掌握这些开发技术。
但是如果我们要想熟练掌握甚至精通ATL的话,那么这只是一个开头,前面还有漫长的路要走。原因有多方面,一则COM本身异常复杂,不下苦功难窥全貌;二则ATL确实奥妙很多,它体现了C++语法的博大精深;三则ATL还存在很多错误,虽然本书作者指出了一些错误,但实际的错误肯定更多,这就对ATL使用者提出了更高的要求,如果使用过程中不能发现这些错误或者避开这些错误,那么用ATL反而会阻碍我们的工作。
虽然ATL比较精深,但是这本书的讲解非常通俗易懂,语言比较简练,条理非常清楚。即使在读完这本书之后,它仍然可以作为参考书指导我们的开发和学习工作。我想,这就是好书的价值所在吧。