结构化存储和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.