Object Linking and Embedding,对象嵌入与链接,简称Ole。Ole从当初的Ole1发展到现在的Ole2,发生了非常大的变化。在Ole1中应用程序之间的数据传输是通过DDE进行的,我们知道DDE的效率非常的低下,使用起来也是非常的繁琐,给开发者们带来了很大的麻烦。而Ole2是基于Com技术在应用程序之间传递数据的,由于Com的高效性,这种方法很好的解决了Ole1中的问题,因此Ole2很快的取代了Ole1成为了应用程序之间集成和交互的主要手段。
Ole文档也被人们称为复合文档,它可以无缝隙的组合各种数据成分,如声音片段、表格、图片等。刚开始学习Com的时候初次看到复合文档,因为知道复合文档是结构化存储的一个实现,所以就认为符合文档的作用仅仅是为Com对象提供持久化。直到现在学习了Ole之后才发现之前的理解是错的。真正的复合文档应该是和Ole紧密融合在一起的,支持结构化存储只是它的所有功能中的一小部分而已,对于复合文档的介绍MSDN上有很详细的描述(Compound Documents),在这里我就不多说了。
Ole2是一个非常庞大的体系结构,它涵盖了复合文档、名字对象、嵌入链接、拖放、定位激活等多个功能部件,这些功能部件之间的关系如图1所示,图中所描述的每个功能部件都是建立再低层功能部件之上的。在图中,我们会发现Linking是在Embedding之上的,而从本小节的标题来看嵌入和链接应该是并列的关系啊!难道这个图有问题吗?不是的,这个图本身是没有任何问题的,在实现链接对象的时候有一种链接到嵌入的情况,这种链接的链接源是一个嵌入到载体中的嵌入对象,如:在Word文档中嵌入的Excel表格单元格,在这种情况下,可以认为链接是建立在嵌入的基础上的。
图 1
2.1 载体的构成
载体也叫Ole容器,是对支持对象嵌入和链接的应用程序或组件的统称。作为一个最简单的载体必须具有一下的功能:
1) 必须支持IOleClientSite和IAdviseSink接口;
2) 必须维护一份嵌入对象或链接对象的数据;
3) 提供激活嵌入对象或链接对象的入口,对这些对象进行编辑;
根据上面的三点我们可以用图2来描述一个载体应用程序。其中Site对应1),复合文档对应2),3)属于界面操作逻辑或者菜单、工具条项目。
图2 载体应用程序的基本结构
2.2 载体中的复合文档
载体中的复合文档维护了一份嵌入Ole对象数据(在这里,我把载体中的嵌入对象和链接对象统称为Ole对象。实际上他们都是Com对象,而且他们都存在于载体当中,由Ole库函数来创建。他们之间的区别是:嵌入对象中包含可被服务器应用程序打开、编辑的数据;而链接对象仅仅维护一个链接信息,其数据可以在一个文件中也可以是其他载体应用程序当中的嵌入对象)。
Ole库提供了一系列API函数用来创建和打开载体复合文档中的Ole对象:
表1 用来创建Ole对象的API函数及其作用
函数 |
描述 |
OleLoad |
用来加载保存再复合文档中Ole对象 |
OleCreate |
根据CLSID创建一个新的嵌入对象 |
OleCreateFromFile |
根据文件名创建一个嵌入对象 |
OleCreateFromData |
根据LPDATAOBJECT创建一个嵌入对象) |
OleCreateLink |
根据名字对象创建一个链接对象 |
OleCreateLinkFromData |
根据一个LPDATAOBJECT创建一个链接对象 |
OleCreateLinkToFile |
根据文件名创建一个链接对象 |
这组API函数所带的参数大体上是一致的(详细情况查阅MSDN),有一个函数比较特别 OleLoad,这个函数是用来加载复合文档中的Ole对象的 。在复合文档中的Ole对象数据存在着三种状态:被动态、加载态、运行态(见表2)。调用OleLoad将使Ole对象从被动态进入加载态。
表2 复合文档中的Ole对象数据的三种状态
状态 |
描述 |
被动态 |
数据存储在符合文档中(在本地) |
加载态 |
数据加载到内存并创建一个Ole对象 |
运行态 |
启动一个服务器进程对Ole对象数据进行编辑 |
我们可以调用API函数让Ole对象数据在这三个状态之间进行转换:
图 3 复合文档中的Ole对象数据三种状态之间的转换
2.3 Ole对象的绘制
进入加载态后Ole对象有义务承担绘制的责任,如果当前Ole对象没有绘制能力(就是说Ole对象数据中没有缓存,通过ICache接口可以获得缓存)将启动Ole服务器进入运行态,让Ole服务器提供位图或者元文件,绘制流程如下图所示:
图4 Ole对象绘制的流程
在每次创建一个新的Ole对象的时都不会有缓存,因此将要启动服务器进入运行态以获得缓存,当Ole对象拿到缓存后Ole库会关闭对象服务器,Ole对象数据重新回到加载态。
2.4 载体接口之IOleClientSite
一个载体之所以能够成为一个载体不仅仅是因为它能创建和绘制Ole对象,更重要的是因为它实现了载体接口,通过这些接口和服务器进行通信协同服务器对Ole对象数据进行编辑。其中IOleClientSite在这些载体接口中占有非常重要的地位,它处理对象服务器发送过来的保存、服务器进入隐藏运行态以及显示运行态等事件。
表 3 IOleClientSite接口函数及说明
函数名称 |
说明 |
SaveObject |
通知载体保存Ole对象数据 |
GetMoniker |
请求Ole对象的名字对象(链接对象) |
GetContainer |
请求对象的IOleContainer接口,在嵌入到链接时使用 |
ShowObject |
告诉载体显示Ole对象,要使得Ole对象在客户区显示。 |
OnShowWindow |
通知载体,对象服务器窗体是隐藏的还是可见的 |
RequestNewObjectLayout |
告诉载体重新排列Ole对象的位置 |
ShowObject与OnShowWindow的区别:ShowObject是要求载体在客户区显示Ole对象,如:用户使用滚动条将Ole对象移到非客户区,在接受到ShowObject调用后应该回滚滚动条,使Ole对象可见; OnShowWindow则表示对象服务器窗体是隐藏的还是可见的,一般的处理该调用的方法是:参数为True的时候为Ole对象上绘制阴影线,参数为False的时候正常绘制。
2.5 载体接口之IAdviseSink
IAdviseSink的作用在于:Ole对象数据在服务器进程中进行编辑的时候发生了变化要通知载体的时候就应该使用IAdviseSink接口。IAdviseSink定义如下:
表 4 IAdviseSink接口函数及说明
函数名称 |
说明 |
OnDataChange |
Ole对象数据发生了改变 |
OnViewChange |
对象视图发生了变化 |
OnRename |
(链接)对象重命名 |
OnSave |
Ole对象数据已经被保存 |
OnClose |
对象服务器被关闭 |
OnViewChange:对象视图发生了变化,所说的视图应该是指IViewObject,在Ole对象的IViewObject接口被调用SetAdvise方法的时候会给载体发送OnViewChange。
3. 对象服务器
3.1 对象服务器简述
对象服务器本身是一个Com对象,它有自己的类厂对象,我们需要在注册表中注册对象服务器。对象服务器的注册表要添加的条目参见NOleServerDemo中的NOleServerDemo.reg文件。
按照对象服务的的功能大小可以分为最大服务器和最小服务器两大类。最大服务器是指可以独立运行的,支持嵌入和链接的应用程序;最小服务器则不能独立运行,只能再嵌入对象激活后才能运行,不支持链接。
本文仅介绍最大服务器的原理与实现,编写一个功能最简单的最大对象服务器必须满足下面的条件:
1) 是一个Com对象,有自己的类厂,要再注册表中建立自己的CLSID;
2) 支持IOleObject、IDateObject、IPerisistorage、IPerisistFile接口;
一个最大对象服务器具有图5所示的结构:
图5 对象服务器的基本结构
3.2 关于类厂
最大化对象服务器往往是以一个应用程序的方式来实现的,因此在程序启动的时候如果带有-Embedding或者/Embedding参数的情况下需要调用 CoRegisterClassObject 函数向Com库注册类厂。
3.3 对象服务器接口之IOleObject
IOleObject是对象服务器中最重要的接口,它主要负责接受IOleClientSite和IAdviseSink接口、执行iVerb动作、关闭服务器等重要功能。IOleObject非常的庞大,总共有23个成员函数,但并不是每个函数都要自己去实现,Ole库为我们做了很多的事情,一些比较通用的函数只要委托给Ole库就行了。一个对象服务器中必须实现的方法有三个:
表 5 IOleObject中必须实现的函数
函数名 |
描述 |
SetHostNames |
向对象提供载体应用程序名及其所在的文档名。在这个调用上,对象改变其用户接口以反映其嵌套状态。这个函数旨在嵌入对象上被调用(链接不调用)。 |
Close |
指示对象关闭其自身,如果正在编辑的是嵌入对象数据将执行默认的保存操作,而如果是链接对象数据将提示用户是否保存数据。最终,调用Close之后将清除对象,服务器进程退出。 |
DoVerb |
在对象上执行一个隐藏、显示对象编辑窗口动作。 |
此外,为了达到更好的用户体验效果或者对对象进行一些特定的操作,可以选择一些接口来实现:
表 6 IOleObject中可选的函数
函数名 |
描述 |
SetExtent |
指示对象窗口改变其尺寸,以与其载体视图中的该对象尺寸相匹配。 |
InitFromData |
给对象提供一个IDataObject指针,从中对象可初始化自身,实际上相当于执行了一次Paste操作。 |
GetClipboardData |
向对象申请一个IDataObject指针,该指针包含对象的信息并将被放到剪切板上去。 |
SetColorScheme |
给对象提供一个优先考虑的彩色调色板,就是说只要可能的话,对象就应使用该调色板。 |
还有两个函数仅与链接有关:
函数名 |
描述 |
SetMoniker |
给对象提供一个在一个标记中的名字; |
GetMoniker |
想对象申请一个描述其自身的携有或没有携有有关载体的信息的标记。 |
此外,一些函数可以返回OLE_S_USEREG,Ole库将注册表中的缺省设置来实现,如:EnumVerb,GetUserType、GetMiscStatus。
其他函数或者不实现,或者委托给Ole库中默认的实现来完成,可参见例子NOleServerDemo。
3.4 对象服务器接口之IDataObject
IDataObject是统一数据传输机制里面的标准数据传送接口,它的主要作用在于为载体中的Ole对象生成一个位图或元文件等图片格式的高速缓存,因此,我们需要在GetData方法的实现中为载体绘制一张图片。
3.5 对象服务器接口之IPersistFile、IPersistStorage
这两个接口的主要作用为:打开文件或者存储对象,并初始化服务器对象;将服务器对象数据保存到文件或存储对象中去。
之前对这两个接口的理解存在一个误区,认为在打开嵌入对象数据的时候使用IPersistStorage来完成,而打开链接对象的时候使用IPersistFile对象来完成。其实则不然,载体中调用OleLoad创建的Ole对象在激活时,服务器是按照上面的说法来进行加载的,而调用OleCreate等函数创建的Ole对象在激活的时候,将会根据不统的传入参数来选择IPersistFile或者IPersistStorage,一般带有文件名参数的API将会使用IPersistFile,而带有IDataObject参数的API将会使用IPersistStorage。
4.1 Ole对象与服务器对象
Ole对象和服务器对象的区别不是很明显,因为往往在Ole对象中的接口与服务器对象中的接口是一致的,刚接触Ole的时候很容易认为他们是同一个对象。实际上,在我们创建一个Ole对象之后,使用任务管理器把对象服务器进程杀掉,然后再对我们的Ole对象进行操作会发生什么情况?如果他们是同一个对象,那么服务器进行退出后Ole对象也跟着销毁吗?试试就会知道,即使对象服务器进程被杀掉,我们仍可以随心所欲的操作Ole对象,请求它的接口、调用它的方法丝毫不会有任何的问题。
在我们的注册表文件当中会有这样的键:CLSID/InProcServer、CLSID/InProcHandler等。这两个键值代表这两个处理Ole对象数据的服务器和处理器,CLSID/InProcServer对应的值就是我们的对象服务器,而CLSID/InProcHandler是一个对象处理器,它可以由用户来实现,也可以采用默认的Ole2.DLL来帮你完成。在工作的时候,载体中创建的Ole对象一般是CLSID/InProcHandler下面的处理器,该处理器能够Ole对象的绘制(绘制情况比较特殊,如果Ole对象数据中存在着一个缓存,那么就不必启动对象服务器了,省去了服务器进程的开销),不过一般来说,载体对Ole对象的大部分调用都将启动对象服务器(关于这些服务器与处理器在《Ole2高级编程技术》第九章中有比较详细的介绍)。
4.2 嵌入与链接对象
我们在载体中创建一个嵌入或链接对象,并不仅仅是为了它的高速缓存,我们还需要能够对这些数据进行编辑。由于只有对象服务器能够解析和编辑这些数据,所以我们必须启动对象服务器。我们将在载体中启动对象服务器来编辑Ole对象数据的过程称之为:激活。
现在我们已经了解了什么Ole对象和服务器对象,也知道对象处理器和服务器的概念,下面将给出两个图示来清楚的为大家展示他们之间的相互关系:
图6-a 嵌入对象与服务器
图6-b 链接对象与服务器
从图中可以看出无论是在执行嵌入或链接的时候对象服务器暴露的接口是一样的;载体中的Ole对象存在着差异,链接对象多了一个IOleLink接口,该接口可以处理与链接相关的信息