DELPHI基础教程
第二十章 开发Delphi对象式数据管理功能(二)
20.1.6 TResourceStream 对象 TResourceStream 对象是另一类 MemoryStream 对象,它提供对 Windows 应用程序资源的访问,因此称它为资源流。 TResourceSream 也是从 TCustomMemoryStream 继承的。因此在 TCustomMemoryStream 对象的基础上,定义了与指定资源模块或资源文件建立连接的构造方法,并且还覆盖了 Write ,以实现向资源文件中写数据。 下面介绍 TResourceStream 的实现 1. 私有域 TResourceStream 没有定义新的属性,但它在 private 部分定义了两个数据域 HResInfo 和 HGlobol 和一个私有方法 Initialize ,它们的定义如下:
TResourceStream = class(TCustomMemoryStream) private HResInfo: HRSRC; HGlobal: THandle; procedure Initialize(Instance: THandle; Name, ResType: PChar); … end;
HRSRC 是描述 Windows 资源信息的结构句柄。 HGlobal 变量代表资源所在模块的句柄。如果操作的是应用程序资源, HGlohal 就代表 EXE 程序的句柄;如果是动态链接库( DLL ),则 HGlobal 代表动态链接库的句柄。 TResourceStream 对象使用这两上变量访问应用程序或动态链接库中的资源。 Initialize 方法是 TResourceStream 对象内部使用的。它的构造方法 Create 和 CreateFromID 都是调用 Initialize 方法完成对 TResourceStream 的初始化。它的实现如下:
procedure TResourceStream.Initialize(Instance: THandle; Name, ResType: PChar);
procedure Error; begin raise EResNotFound.Create(FmtLoadStr(SResNotFound, [Name])); end;
begin HResInfo := FindResource(Instance, Name, ResType); if HResInfo = 0 then Error; HGlobal := LoadResource(Instance, HResInfo); if HGlobal = 0 then Error; SetPointer(LockResource(HGlobal), SizeOfResource(Instance, HResInfo)); end;
该方法实现中,首先调用 Windows 函数 FoundResource 得到由参数 Instance 指定的模块中的名为 Name 和类型为 ResType 的资源,然后调用 LoadResource 将资源调用内存,并返回该资源在内存中的句柄,最后,将该资源复制到 ResourceStream 中。方法的 Instance 参数代表要调用的资源所在的模块句柄。模块可以是可执行文件,也可以是动态链接库。如果在读取资源时没在模块中发现要找的资源则产生异常事件。 2. 构造方法 Create 和 CreateFromID 这两个方法在实现上没有大的不同。顾名思义,第一个方法是通过资源名构造 TResourceStream; 第二个方法通过资源 ID 构造 TResourceStream ,而且在实现过程中,它们都调用了 Initialize 方法。下面是它们的实现:
constructor TResourceStream.Create(Instance: THandle; const ResName: string; ResType: PChar); begin inherited Create; Initialize(Instance, PChar(ResName), ResType); end;
constructor TResourceStream.CreateFromID(Instance: THandle; ResID: Integer; ResType: PChar); begin inherited Create; Initialize(Instance, PChar(ResID), ResType); end;
这两个方法中都有 Instance 参数,该参数值的含义在 Insitialize 中介绍过。 3. Write 方法 TResourceStream 的 Write 方法只完成一件事,就产生这个异常事件,其实现如下:
function TResourceStream.Write(const Buffer; Count: Longint): Longint; begin raise EStreamError.CreateRes(SCantWriteResourceStreamError); end;
从方法实现中可以看到, TSourceStream 对象是不允许写数据的。一旦往资源流中写数据将产生异常事件。 4. 析构方法 Destroy 该方法产生给资源解锁,然后释放该资源,最后调用继承的 Destroy 方法释放 ResourceStream 。其实现如下:
destructor TResourceStream.Destroy; begin UnlockResource(HGlobal); FreeResource(HResInfo); inherited Destroy; end;
回顾 Initialize 方法,我们不难发现: ● ResourceStream 没有额外地给资源重新分配内存,而是直接使用 HGlobal 句柄所指 的内存域 ● ResourceStream 中的资源在流的生存期,始终是 Lock 状态,因此要根据 Windows 的内存使用规则合理安排 ResourceStream 的使用 ● ResourceStream 只是用于访问应用程序和动态链接库中的资源的
在 Classes 在单元中提供了 InternalReadComponentRes 函数,该函数使用了 TResourceStream 对象从 Delphi 应用程序中读取部件。 Delphi 是将窗体和部件信息放在模块资源的 RCDATA 段的。
20.1.7 TBlobStream 对象
从 Delphi 数据库开发平台这个意义上说, TBlobStream 对象是个很重要的对象。 TBlobStream 对象提供了修改 TBlobField 、 TBytesField 或 TVarBytesField 中数据的技术。开发者可以象对待文件或流那样在数据库域中读写数据。 传统数据库发展的一个重要趋向是往多媒体数据库发展。目前比较著名和流行的数据库都支持多媒体功能,多媒体数据存储中的一大难点是数据结构不规则,数据量大。各大数据库产品是采用 BLOB 技术解决多媒体数据存储中的问题。 Delphi 的 TBlobStream 对象的意义就在于:一方面可以使 Delphi 应用程序充分利用多媒体数据库的数据管理能力;另一方面又能利用 Object Pascal 的强大程序设计能力给多媒体数据库提供全方向的功能扩展余地。 使用 TBlobStream 对象可以在多媒体数据库的 BLOB 字段存储任意格式的数据。一般说来,许多多媒体数据库只能支持图像、语音或者 OLE 服务器支持的数据。利用 TBlobStream 则不同,只要是程序能够定义的数据格式,它都能在 BLOB 字段中读写,而不需要其它辅助工具。 TBlobStream 用构造方法 Create 建立数据库域和 BLOB 流的联接。用 Read 或 Write 方法访问和改变域中的内容;用 Seek 方法,在域中定位;用 Truncate 方法删除域中当前位置起所有的数据。
20.1.7.1 TBlobStream 的属性和方法
TBlobStream 对象从 TStream 直接继承,没有增添新的属性。它覆盖了 Read 、 Write 和 Seek 方法,提供了对 BLOB 字段的访问操作;它增添了 Truncate 方法以实现 BLOB 字段中的删除操作。 1. Read 方法 声明: function Read(var Buffer; Count: Longint): Longint; Read 方法从数据库域的当前位置起复制 Count 个字节的内容到 Buffer 中。 Buffer 也必须至少分配 Count 个字节。 Read 方法返回实际传输的字节数,因为传输的字节数可能小于 Count ,所以需要选择符的边界判断。 2. Write 方法 声明: function Write(const Buffer; Count: Longint); override; Longint; Write 方法从 Buffer 中向数据库域的当前位置复制 Count 个字节的内容。 Buffer 必须分配有 Count 个字节的内存空间,函数返回实际传输的字节数,传输过程也要进行选择符边界判断。 3. Seek 方法 声明: function Seek(Offset: Longint; Origin: Word): Longint; Seek 方法重新设置 BLOB 流中的指针位置。如果 Origin 的值是 soFromBeginning ,则新的指针位置是 Offset; 如 Origin 的值是 soFromCurrent ,则新的指针位置是 Position+Offset ;如果 Origin 的值是 SoFromCurrent ,则新的指针位置是 Size+Offset 。函数返回新的指针位置值。当 Origin 为 0(SoFromBegin) 时, Offset 的值必须大于等于零 ; 当 Origin 的值为 2(SoFromEnd) , Offset 的值必须小于等于零。 4. Truncate 方法 声明: procedure Truncate; Truncate 方法撤消 TBlobField 、 TBytesField 或 TVarBytesField 中从当前位置起的数据。 5. Create 方法 声明: constructor Create(Field: TBlobField; Mode: TBlobStreamMode); Create 方法使用 Field 参数建立 BLOB 流与 BLOB 字段的联接。 Mode 的值可为 bmRead 、 bmWrite 和 bmReadWrite 。
20.1.7.2 TBlobStream 的实现原理
说明 TBlobStream 对象的实现原理,不可避免地要涉及它的私有域,下面是私有域的定义:
TBlobStream = class(TStream) private FField: TBlobField; FDataSet: TDataSet; FRecord: PChar; FBuffer: PChar; FFieldNo: Integer; FOpened: Boolean; FModified: Boolean; FPosition: Longint; … public … end;
FField 是与 BLOB 流相联的数据库 BLOB 域,该域用于 BLOB 流的内部访问。 FDataSet 是代表 FField 所在的数据库,它可以是 TTable 部件,也可以是 TQuery 部件。 FRecord 和 FBuffer 都是 BLOB 流内部使用的缓冲区,用于存储 FField 所在记录的数据,该数据记录中不包含 BLOB 数据, TBlobStream 使用 FRecord 作为调用 BDE API 函数的参数值。 FFieldNo 代表 BLOB 字段的字段号,也用于 BDE API 的参数传递, FOpened 和 FMocified 都是状态信息, FPosition 表示 BLOB 流的当前位置,下面介绍 TBlobStream 方法实现。 1. Create 方法和 Destroy 方法的实现 Create 方法的功能主要是建立 BlobStream 流与 BLOB 字段的联系并初始化某些私有变量。其实现如下:
constructor TBlobStream.Create(Field: TBlobField; Mode: TBlobStreamMode); var OpenMode: DbiOpenMode; begin FField := Field; FDataSet := Field.DataSet; FRecord := FDataSet.ActiveBuffer; FFieldNo := Field.FieldNo; if FDataSet.State = dsFilter then DBErrorFmt(SNoFieldAccess, [FField.DisplayName]); if not FField.FModified then begin if Mode = bmRead then begin FBuffer := AllocMem(FDataSet.RecordSize); FRecord := FBuffer; if not FDataSet.GetCurrentRecord(FBuffer) then Exit; OpenMode := dbiReadOnly; end else begin if not (FDataSet.State in [dsEdit, dsInsert]) then DBError(SNotEditing); OpenMode := dbiReadWrite; end; Check(DbiOpenBlob(FDataSet.Handle, FRecord, FFieldNo, OpenMode)); end; FOpened := True; if Mode = bmWrite then Truncate; end;
该方法首先是用传入的 Field 参数给 FField , FDataSet , FRecord 和 FFieldNo 赋值。方法中用 AllocMem 按当前记录大小分配内存,并将指针赋给 FBuffer ,用 DataSet 部件的 GetCurrentRecord 方法,将记录的值赋给 FBuffer ,但不包括 BLOB 数据。 方法中用到的 DbiOpenBlob 函数是 BDE 的 API 函数,该函数用于打开数据库中的 BLOB 字段。 最后如果方法传入的 Mode 参数值为 bmWrite ,就调用 Truncate 将当前位置指针以后的 数据删除。 分析这段源程序不难知道: ● 读写 BLOB 字段,不允许 BLOB 字段所在 DataSet 部件有 Filter ,否则产生异常事件 ● 要读写 BLOB 字段,必须将 DataSet 设为编辑或插入状态 ● 如果 BLOB 字段中的数据作了修改,则在创建 BLOB 流时,不再重新调用 DBiOpenBlob 函数,而只是简单地将 FOpened 置为 True ,这样可以用多个 BLOB 流对同一个 BLOB 字段读写
Destroy 方法释放 BLOB 字段和为 FBuffer 分配的缓冲区,其实现如下:
destructor TBlobStream.Destroy; begin if FOpened then begin if FModified then FField.FModified := True; if not FField.FModified then DbiFreeBlob(FDataSet.Handle, FRecord, FFieldNo); end; if FBuffer <> nil then FreeMem(FBuffer, FDataSet.RecordSize); if FModified then try FField.DataChanged; except Application.HandleException(Self); end; end;
如果 BLOB 流中的数据作了修改,就将 FField 的 FModified 置为 True ;如果 FField 的 Modified 为 False 就释放 BLOB 字段,如果 FBuffer 不为空,则释放临时内存。最后根据 FModified 的值来决定是否启动 FField 的事件处理过程 DataChanged 。 不难看出,如果 BLOB 字段作了修改就不释放 BLOB 字段,并且对 BLOB 字段的修改只有到 Destroy 时才提交,这是因为读写 BLOB 字段时都避开了 FField ,而直接调用 BDE API 函数。这一点是在应用 BDE API 编程中很重要,即一定要修改相应数据库部件的状态。 2. Read 和 Write 方法的实现 Read 和 Write 方法都调用 BDE API 函数完成数据库 BLOB 字段的读写,其实现如下:
function TBlobStream.Read(var Buffer; Count: Longint): Longint; var Status: DBIResult; begin Result := 0; if FOpened then begin Status := DbiGetBlob(FDataSet.Handle, FRecord, FFieldNo, FPosition, Count, @Buffer, Result); case Status of DBIERR_NONE, DBIERR_ENDOFBLOB: begin if FField.FTransliterate then NativeToAnsiBuf(FDataSet.Locale, @Buffer, @Buffer, Result); Inc(FPosition, Result); end; DBIERR_INVALIDBLOBOFFSET: {Nothing}; else DbiError(Status); end; end; end;
Read 方法使用了 BDE API 的 DbiGetBlob 函数从 FDataSet 中读取数据,在本函数中,各参数的含义是这样的: FDataSet.Handle 代表 DataSet 的 BDE 句柄, FReacord 表示 BLOB 字段所在记录, FFieldNo 表示 BLOB 字段号, FPosition 表示要读的的数据的起始位置, Count 表示要读的字节数, Buffer 是读出数据所占的内存, Result 是实际读出的字节数。该 BDE 函数返回函数调用的错误状态信息。 Read 方法还调用了 NativeToAnsiBuf 进行字符集的转换。
function TBlobStream.Write(const Buffer; Count: Longint): Longint; var Temp: Pointer; begin Result := 0; if FOpened then begin if FField.FTransliterate then begin GetMem(Temp, Count); try AnsiToNativeBuf(FDataSet.Locale, @Buffer, Temp, Count); Check(DbiPutBlob(FDataSet.Handle, FRecord, FFieldNo, FPosition, Count, Temp)); finally FreeMem(Temp, Count); end; end else Check(DbiPutBlob(FDataSet.Handle, FRecord, FFieldNo, FPosition, Count, @Buffer)); Inc(FPosition, Count); Result := Count; FModified := True; end; end;
Write 方法调用了 BDE API 的 DbiPutBlob 函数实现往数据库 BLOB 字段存储数据。 该函数的各参数含义如下:
表 20.2 调用函数 DbiPutBlob 的各传入参数的含义 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 参数名 含义 ────────────────────────────── FDataSetHandle 写入的数据库的 BDE 句柄 FRecord 写入数据的 BLOB 字段所在的记录 FFieldNo BLOB 字段号 FPosition 写入的起始位置 Count 写入的数据的字节数 Buffer 所写入的数据占有的内存地址 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
方法中还根据 FField 和 FTransliterate 的值判断是否进行相应的字符集转换,最后移动 BLOB 流的位置指针,并将修改标志 FModified 置为 True 。 3. Seek 和 GetBlobSize 方法的实现 Seek 方法的功能主要是移动 BLOB 流的位置指针。 GetBlobSize 方法是私有的,在 Seek 方法中被调用,其功能是得到 BLOB 数据的大小。它们的实现如下:
function TBlobStream.GetBlobSize: Longint; begin Result := 0; if FOpened then Check(DbiGetBlobSize(FDataSet.Handle, FRecord, FFieldNo, Result)); end;
function TBlobStream.Seek(Offset: Longint; Origin: Word): Longint; begin case Origin of 0: FPosition := Offset; 1: Inc(FPosition, Offset); 2: FPosition := GetBlobSize + Offset; end; Result := FPosition; end;
GetBlobSize 调用了 BDE API 的 DbiGetBlobSize 函数,该函数的参数的含义同前面的 API 函数相同。 4. Truncate 方法 该方法是通过调用 BDE API 函数实现的。其实现如下:
procedure TBlobStream.Truncate; begin if FOpened then begin Check(DbiTruncateBlob(FDataSet.Handle, FRecord, FFieldNo, FPosition)); FModified := True; end; end;
该方法从 BLOB 流的当前位置起删除所有数据,并设置修改标志 FModified 为 True 。在 Delphi VCL 中许多部件特别是数据库应用方面的部件都用 BDE API 函数完成对数据库的访问,如 Data Access 和 Data Control 部件。各种数据库部件都是 BDE API 函数外层的包装简化了对数据库的访问操作。 BDE API 中还提供了避开 BDE 配置工具在程序中直接处理 Alias( 建立、修改、删除等 ) 的函数支持,这也是部件所没有提供的。在 Delphi 数据库应用安装程序中,这些 Alias 操作函数无疑是相当重要的。有关 BDE API 函数的详细介绍,可阅读 Delphi2.0 Client/Server Suite 所带的 BDE API 帮助文件。
20.2 读写对象的实现原理和应用
读写对象( Filer )包括 TFiler 对象、 TReader 对象和 TWriter 对象。 TFiler 对象是文件读写的基础对象,在应用程序中使用的主要是 TReader 和 TWriter 。 TReader 和 TWriter 对象都直接从 TFiler 对象继承。 TFiler 对象定义了 Filer 对象的基本属性和方法。 Filer 对象主要完成两大功能: ● 存取窗体文件和窗体文件中的部件 ● 提供数据缓冲,加快数据读写操作
20.2.1 TFiler 对象
TFiler 对象是 TReader 和 TWriter 的抽象类,定义了用于部件存储的基本属性和方法。它定义了 Root 属性, Root 指明了所读或写的部件的根对象,它的 Create 方法将 Stream 对象作为传入参数以建立与 Stream 对象的联系, Filer 对象的具体读写操作都是由 Stream 对象完成。因此,只要是 Stream 对象所能访问的媒介都能由 Filer 对象存取部件。 TFiler 对象还提供了两个定义属性的方法: DefineProperty 和 DefineBinaryProperty ,这两个方法使对象能读写不在部件 published 部分定义的属性。 因为 Filer 对象主要用于存取 Delphi 的窗体文件和窗体文件中的部件,所以要清楚地理解 Filer 对象就要清楚 Delphi 窗体文件 (DFM 文件 ) 的结构。 DFM 文件是用于 Delphi 存储窗体的。窗体是 Delphi 可视化程序设计的核心。窗体对应 Delphi 应用程序中的窗口,窗体中的可视部件对应窗口中的界面元素,非可视部件如 TTable 和 TOpenDialog ,对应 Delphi 应用程序的某项功能。 Delphi 应用程序的设计实际上是以窗体的设计为中心。因此, DFM 文件在 Delphi 应用设计中也占很重要的位置。窗体中的所有元素包括窗体自身的属性都包含在 DFM 文件中。 在 Delphi 应用程序窗口,界面元素是按拥有关系相互联系的,因此树状结构是最自然的表达形式;相应地,窗体中的部件也是按树状结构组织;对应在 DFM 文件中,也要表达这种关系。 DFM 文件在物理上,是以二进制方式存储的,在逻辑上则是以树状结构安排各部件的关系。 Delphi 编辑窗口支持以文本方式显示 DFM 文件。从该文本中可以看清窗体的树状结构。下面是 DFM 文件的文本显示:
Object Form1: TForm1 Left = 72 Top = 77 ActiveControl = DBIMage1 … Object Panell: TPanel Left = 6 … Object DBLabel1: TDBText … end Object DBImage1: TDBImage … end end Object Panel2: TPanel Left = 6 … Object Label1: TLable … end end Object Panel3: TPanel Left = 6 … Object DBLabel2: TDBText … end end end 关于 DFM 文件中存储属性值的规则,请参见自定义部件开发这一章。 对照 TFiler 对象的属性。 Root 属性就表示部件树的根──窗体。 Filer 对象的许多方法都是读从根起始的树中所有的部件。 Ancestor 属性表示根的祖先对象, IgnoreChildren 属性则是读部件时忽略根的子结点。 下面介绍 Filer 对象的属性和方法。 |