赞
踩
/结构化存储和OLE对象/
1 引言
目前,传统的二层C/S(Client/Server)结构应用软件已发展为多层结构的分布式应用
系统[2]。为了改善系统的工作效率以及提高系统的伸缩性,很多软件开发人员都把服务器
上的一些基本数据分发到各个客户机上。这种工作模式的优点是显著的,因为它减少了那
些非实时数据(如员工表、产品数据表等)在网络上的流动,并且在网络瘫痪时,各个客
户机仍然可以维持部分的工作。同时,这种模式需要定时在客户机与服务器之间进行数据
更新,为了更有效地进行数据更新,我们设想在客户机上创建类似公文包的数据库,即与
服务器上的SQL Server一样,把所有的数据表保存在几个文件中。围绕这一问题,本文详
细分析了OLE 结构化文件的存储原理,并在Delphi 环境下深入讨论了与其相关的一系列
操作。
2 OLE复合文档的存储原理与操作
2.1 OLE复合文档的存储原理
OLE(Object linking and Embedding),即对象连接与嵌入的简称,是在Windows环境
下实现不同Windows 应用程序之间共享数据的一种方法。OLE 结构化文件,也称为OLE
复合文档,简单来说,OLE复合文档的结构化,实际上是指它的内容按照流(stream)和存
储(storage)的方式进行组织。MicrosoftWord和Excel的文件就是典型的OLE复合文档,
如图1所示,这种文档的内容类似操作系统中的文件系统,即文档内包含“文件夹“和“文
件”。其中,“文件夹”被称为存储,每个存储中所包含的连续数据即“文件”被称为流。
由于OLE复合文档中每一个“文件”都是彼此独立的,所以引言中提出的问题便得到很好
的解决,我们首先建立一个OLE复合文档,然后把众多的数据表以流的方式保存到该复合
文档中,这样,客户机与服务器在进行数据更新时仅是传递几个文件,非常有效。而且,
我们在把Paradox 7格式的数据表写入到OLE复合文档的过程中发现:OLE复合文档的容
量大小约是原来众多数据表容量的总和的50%。
OLE复合文档-----MyOleDoc.ole
DataBase -----存储
Employee -----流
Customer OLE Data
Excel 或AutoCad的数据
2.2 OLE复合文档的建立
可以使用Windows SDK 函数StgCreateDocFile 来建立OLE 复合文档,它的声明在
ActiveX单元中。函数的原形是:
function StgCreateDocfile(pwcsName:PoleStr;grfMode:Longint;reserved:Longint;out stgOpen:IStorage):Hresult;stdcall;
,函数返回的存储是复合文档的根目录存储。具体参数如下:
(1) wcsName:被创建的文件名称;
(2) grfMode:复合文档的操作方式,各个选值的含义如表1所示:
(3) reserved:必须设置为0;
(4) stgOpen:返回一个存储;
//grfMode参数意义如下:
STGM_READ 只读模式
STGM_WRITE 只写模式
STGM_READWRITE 读写模式
STGM_SHARE_DENY_NONE 共享存取模式
STGM_SHARE_DENY_READ 禁止共享的读模式
STGM_SHARE_DENY_WRITE 禁止共享的写模式
STGM_SHARE_EXCLUSIVE 独占的存取模式
STGM_DIRECT 对复合文档的所有修改立即生效
STGM_TRANSACTED 提交时所有修改才被保存到复合文档中
STGM_FAILIFTHERE 若已存在一个流或存储,则创建复合文档失败
STGM_CREATE 若已存在一个流或存储,则它将被覆盖,否则将创建一个新的流或存储
STGM_DELETEONRELEASE 当这个复合文档中的流或存储被释放时,它也会自动被释放
2.3 OLE复合文档的打开
可以使用Windows SDK 函数StgOpenStorage 来打开一个OLE 复合文档,它的声明在
ActiveX单元中。函数的原形是:
function StgOpenStorage(pwcsName: PoleStr;stgPriority:Istorage;
grfMode:Longint;snbExclude:TSNB;
reserved:Longint;out stgOpen:IStorage):Hresult;stdcall;
,这里的snbExclude选取nil,其它参数参见StgCreateDocFile()。
2.4 流的建立及数据的写入
打开一个OLE 复合文档后,可用IStorage 接口的CreateStream 函数在该文档中创建一
个流,然后充分利用Delphi强大的流机制与基于OLE的各种应用程序的数据进行信息交换。
例如,用户可以使用Delphi下的OleContainer 控件中加载一个支持OLE 应用程序的数据,
然后调用该控件下的SaveToStream()方法把信息以流的形式写进复合文档。CreateStream 函
数的原形是:
function CreateStream(pwcsName:PoleStr;grfMode:Longint;
reserved1:Longint;reserved2:Longint;
out stm:IStream):Hresult;stdcal;
,其中,pwcsName是指新建流的名称,reserved1、reserved2两参数的值均置为0,其它参数参见StgCreateDocFile()。
2.5 OLE复合文档的存储
如上所述,OLE 复合文档的存储与文件系统的“文件夹”在概念上相似的,它也有着
建立、打开和删除等操作。其中使用IStorage 接口的CreateStorage()、OpenStorage()函数可
以分别建立或打开一个子存储。它们的原形分别是:
function CreateStorage(pwcsName:PoleStr; grfMode: Longint;
dwStgFmt: Longint; reserved2: Longint; out stg: IStorage): Hresult;stdcall;
function OpenStorage(pwcsName: PoleStr; const stgPriority: Istorage;
grfMode:Longint; snbExclude: TSNB; reserved: Longint;
out stg: IStorage): Hresult; stdcall;,它们的参数与上述类同,具体用法
见源代码部分。
2.6 存储和流的删除
使用IStorage接口的DestroyElement()函数可以删除OLE复合文档的存储或流,它的函
数原形是:
function DestroyElement(pwcsName:POleStr):Hresult;stdcall;
,其中,pwcsName参数是指被删除的存储或流的名称。应该指出的是,在删除复合文档的存储或流时,调用
DestroyElement()函数的接口应是被删除的存储或流的上一层存储。如图1 中,若想删除
“Customer”流,则正确的语句是:DataBase·DestroyElement('Customer');而要删
除“DataBase” 存储时,则应使用RootStorage·DestroyElement('DataBase'),其中
RootStorage是根目录存储。此外,当一个流或存储被删除时,它的数据并没有被物理删除,
3 主要源代码
本程序将创建一个名为MyOleDoc.ole 的复合文档,在该复合文档中,有一个名为
Database的存储,该存储中保存着两个流employee、customer,分别对应着两个数据表,
如图1所示。
3.1 相关控件及属性:
(1) 在Unit1单元的接口引用中添加ActiveX,AxCtrls两个单元;声明一个全局变量
Duqu:Boolean=True;,该变量的作用是记录customer流是否被删除。
(2) 程序中所涉及的相关控件及属性见表2。
控件名称类 属性名称值
Table1 TTable DataBaseName C:\OLE(数据表存放路径) TableName employee.db
Table2 TTable DataBaseName C:\OLE(数据表存放路径)TableName customer.db
DataSetProvider1 TDataSetProvider DataSet Table1
DataSetProvider2 TDataSetProvider DataSet Table2
ClientDataSet1 TClientDataSet ProviderName DataSetProvider1
ClientDataSet2 TClientDataSet ProviderName DataSetProvider2
ClientDataSet3 TClientDataSet 依靠ClientDataSet3、ClientDataSet4 的LoadFromStream()
ClientDataSet4 TClientDataSet 方法从复合文档读取两个数据表;这两个控件全部选用默认值。
表1:程序中主要控件及属性
3.2主要代码
(1)对BitBtn1的OnClick事件编程;新建复合文档、存储和流;并把两个数据表保存
到该复合文档中。
procedure TForm1.BitBtn1Click(Sender: TObject);
var//声明一些相关的变量
Hre:HResult;
RootStorage,SubStorage:IStorage ;
Istr1,Istr2:IStream;
OleStream1:TOleStream;
begin
Hre:=StgCreateDocfile('MyOleDoc.ole',STGM_CREATE or STGM_READWRITE or
STGM_DIRECT or STGM_SHARE_EXCLUSIVE,0,RootStorage);//建立一个名为
//MyOleDoc.ole复合文档
if not SUCCEEDED(Hre) then Application.Terminate; //SUCCEEDED()函数的功能是判
//断复合文档的建立是否成功
Hre:=RootStorage.CreateStorage('Database',STGM_CREATE or STGM_READWRITE or
STGM_DIRECT or STGM_SHARE_EXCLUSIVE,0,0,SubStorage); //建立一个名为
// Database的存储
if not SUCCEEDED(Hre) then Application.Terminate;//判断存储的建立是否成功
Hre:=SubStorage.CreateStream('employee',STGM_CREATE or STGM_READWRITE or
STGM_DIRECT or STGM_SHARE_EXCLUSIVE,0,0,Istr1); //建立一个名为employee
//的流
if not SUCCEEDED(Hre) then Application.Terminate; //判断流的建立是否成功
ClientDataSet1.Active:=True;
OleStream1:=TOleStream.Create(Istr1);
ClientDataSet1.SaveToStream(OleStream1); //把employee数据表的数据写入到
// MyOleDoc.ole 复合文档的Database 存储下,并以名为employee的流进行保存
OleStream1.Free;
Hre:=SubStorage.CreateStream('customer',STGM_CREATE or STGM_READWRITE or
STGM_DIRECT or STGM_SHARE_EXCLUSIVE,0,0,Istr2);
if not SUCCEEDED(Hre) then Application.Terminate;
ClientDataSet2.Active:=True;
OleStream1:=TOleStream.Create(Istr2);
ClientDataSet2.SaveToStream(OleStream1);// 把customer 数据表的数据写入到
//MyOleDoc.ole 复合文档的Database 存储下,并以名为customer的流进行保存
OleStream1.Free;
DuQu:=True;
end;
(2)对BitBtn2的OnClick事件编程;打开复合文档、存储和流;并从该复合文档读取两个数据表。
procedure TForm1.BitBtn2Click(Sender: TObject);
var
Hre:HResult;
RootStorage,SubStorage:IStorage ;
Istr1,Istr2:IStream;
OleStream1:TOleStream;
begin
ClientDataSet4.Active:=False;
ClientDataSet4.Active:=False;
Hre:=StgOpenStorage('MyOleDoc.ole',nil, STGM_READWRITE or STGM_DIRECT or
STGM_SHARE_EXCLUSIVE,nil,0,RootStorage);//打开一个名为MyOleDoc.ole复合文档
if not SUCCEEDED(Hre) then Application.Terminate;; //判断复合文档的打开是否成功
Hre:=RootStorage.OpenStorage('Database',nil,STGM_READWRITE or STGM_DIRECT or
STGM_SHARE_EXCLUSIVE,nil,0,SubStorage);//打开一个名为Database的存储
if not SUCCEEDED(Hre) then Application.Terminate;; //判断存储的打开是否成功
Hre:=SubStorage.OpenStream('employee',nil,STGM_READWRITE or STGM_DIRECT or
STGM_SHARE_EXCLUSIVE,0,Istr1);//打开一个名为employee的流
if not SUCCEEDED(Hre) then Application.Terminate;; //判断流的打开是否成功
OleStream1:=TOleStream.Create(Istr1);
ClientDataSet3.LoadFromStream(OleStream1); //从MyOleDoc.ole 复合文档的Database
//存储中,读取employee流,并把数据传送给ClientDataSet3组件;
OleStream1.Free;
ClientDataSet3.Active:=True;
if Duqu then
begin
Hre:=SubStorage.OpenStream('customer',nil,STGM_READWRITE or STGM_DIRECT or
STGM_SHARE_EXCLUSIVE,0,Istr2);
if not SUCCEEDED(Hre) then Application.Terminate;;
OleStream1:=TOleStream.Create(Istr2);
ClientDataSet4.LoadFromStream(OleStream1); //从MyOleDoc.ole 复合文档的Database
//存储中,读取customer流,并把数据传送给ClientDataSet4组件;
OleStream1.Free;
ClientDataSet4.Active:=True;
end;
end;
(3)对BitBtn4的OnClick事件编程;/删除复合文档中的一个流
procedure TForm1.BitBtn4Click(Sender: TObject);
var
Hre:HResult;
RootStorage,TempStorage,SubStorage:IStorage ;
CLS:TCLSID; //是一个16字节的唯一数字
Sta:TStatStg; //保存IStorage .Stat()返回的信息
Istr1:IStream;
begin
Hre:=StgOpenStorage('MyOleDoc.ole',nil, STGM_READWRITE or STGM_DIRECT or
STGM_SHARE_EXCLUSIVE,nil,0,RootStorage);
if not SUCCEEDED(Hre) then Application.Terminate;
RootStorage.Stat(Sta,0); //IStorage .Stat()返回很多根目录存储信息
CLS:=Sta.clsid; //获取根目录存储唯一标识符
Hre:=RootStorage.OpenStorage('Database',nil,STGM_READWRITE or STGM_DIRECT or
STGM_SHARE_EXCLUSIVE,nil,0,SubStorage);//打开一个名为Database的存储
SubStorage.DestroyElement('Customer'); //删除MyOleDoc.ole 复合文档Database 存
//储的Customer流
Hre:=StgCreateDocfile('MyOleDocTemp.ole',STGM_CREATE or STGM_READWRITE or
STGM_DIRECT or STGM_SHARE_EXCLUSIVE,0,TempStorage);//建立
//MyOleDocTemp.ole临时复合文档
if not SUCCEEDED(Hre) then Application.Terminate;
RootStorage.CopyTo(0,nil,nil,TempStorage);//把MyOleDoc.ole 复合文档的内容复制到临
//时复合文档中
RootStorage:=nil; //把MyOleDoc.ole 复合文档的根目录存储置空,以便重新建立该复
//合文档
Hre:=StgCreateDocfile('MyOleDoc.ole',STGM_CREATE or STGM_READWRITE or
STGM_DIRECT or STGM_SHARE_EXCLUSIVE,0,RootStorage);//建立一个名为
// MyOleDoc.ole复合文档
if not SUCCEEDED(Hre) then Application.Terminate;
RootStorage.SetClass(CLS); //设置新文档的CLSID为原来文档的CLSID
TempStorage.CopyTo(0,nil,nil,RootStorage); // 把临时复合文档的内容复制到新的复合文
//档中
TempStorage:=nil;//把临时复合文档的根目录存储置空,以便可以删除它
deletefile('MyOleDocTemp.ole');
DuQu:=False;
end;
/
//范例2,结构化存储读写复合文档的范例
/
Uses Activex;
type
TRec = record
Name: string[8];
Age: Word;
end;
const FileName = 'C:\Temp\Test.dat';
procedure TForm1.FormCreate(Sender: TObject);
begin
Button1.Caption := '写复合文件';
Button2.Caption := '读复合文件';
Position := poDesktopCenter;
end;
procedure TForm1.Button1Click(Sender: TObject);
const
Mode = STGM_CREATE or STGM_READWRITE or STGM_SHARE_EXCLUSIVE;
var
StgRoot, StgSub: IStorage;
Stm: IStream;
Rec1: TRec;
begin
{建立根 IStorage: StgRoot}
StgCreateDocfile(FileName, Mode, 0, StgRoot);
{建立子 IStorage: StgSub}
StgRoot.CreateStorage('StgSub', Mode, 0, 0, StgSub);
{在子 IStorage: StgSub 中建立 IStream: Stm}
StgSub.CreateStream('Stm', Mode, 0, 0, Stm);
{写入数据}
Rec1.Name := '张三';
Rec1.Age := 99;
Stm.Write(@Rec1, SizeOf(TRec), nil);
end;
procedure TForm1.Button2Click(Sender: TObject);
const
Mode = STGM_READ or STGM_SHARE_EXCLUSIVE;
Var
StgRoot, StgSub :IStorage;
Stm: IStream;
Rec1: TRec;
Begin
{如果不是结构化存储文件则退出}
if StgIsStorageFile(FileName) <> S_OK then Exit;
{获取根 IStorage: StgRoot}
StgOpenStorage(FileName, nil, Mode, nil, 0, StgRoot);
{获取子 IStorage: StgSub; 注意: 第一个参数的名称必须和保存时一致}
StgRoot.OpenStorage('StgSub', nil, Mode, nil, 0, StgSub);
{获取 IStream: Stm; 注意: 第一个参数的名称必须和保存时一致}
StgSub.OpenStream('Stm', nil, Mode, 0, Stm);
{读出数据}
Stm.Read(@Rec1, SizeOf(TRec), nil);
ShowMessageFmt('%s, %d', [Rec1.Name, Rec1.Age]);
end;
end.
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
第二部分
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.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接口,该接口可以处理与链接相关的信息
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接口,该接口可以处理与链接相关的信息
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。