Stream 对象,又称流式对象,是 TStream 、 THandleStream 、 TFileStream 、 TMemoryStream 、 TResourceStream 和 TBlobStream 等的统称。它们分别代表了在各种媒介上存储数据的能力,它们将各种数据类型 ( 包括对象和部件 )
在内存、外存和数据库字段中的管理操作抽象为对象方法,并且充分利用了面向对象技术的优点,应用程序可以相当容易地在各种 Stream 对象中拷贝数据。 下面介绍各种对象的数据和方法及使用方法。
TStream 对象
TStream 对象是能在各种媒介中存储二进制数据的对象的抽象对象。从 TStream 对象继承的对象用于在内存、 Windows 资源文件、磁盘文件和数据库字段等媒介中存储数据。 Stream 中定义了两个属性: Size 和 Position 。它们分别以字节为单位表示的流的大小和当前指针位置。 TStream 中定义的方法用于在各种流中读、写和相互拷贝二进制数据。因为所有的 Stream 对象都是从 TStream 中继承来的,所以在 TStream 中定义的域和方法都能被 Stream 对象调用和访 问。此外,又由于面向对象技术的动态联编功能, TStream 为各种流的应用提供了统一的接口,简化了流的使用;不同 Stream 对象是抽象了对不同存储媒介的数据上的操作,因此, TStream 的需方法为在不同媒介间的数据拷贝提供了最简捷的手段。
TStream 的属性和方法
1. Position 属性 声明: property Position: Longint; Position 属性指明流中读写的当前偏移量。 2. Size 属性 声明: property Size: Longint; Size 属性指明了以字节为单位的流的的大小,它是只读的。 3. CopyFrom 方法 声明: function CopyFrom(Source: TStream; Count: Longint): Longint; CopyFrom 从 Source 所指定的流中拷贝 Count 个字节到当前流中, 并将指针从当前位置移动 Count 个字节数,函数返回值是实际拷贝的字节数。 4. Read 方法 声明: function Read(var Buffer; Count: Longint): Longint; virtual; abstract; Read 方法从当前流中的当前位置起将 Count 个字节的内容复制到 Buffer 中,并把当前指针向后移动 Count 个字节数,函数返回值是实际读的字节数。如果返回值小于 Count ,这意味着读操作在读满所需字节数前指针已经到达了流的尾部。 Read 方法是抽象方法。每个后继 Stream 对象都要根据自己特有的有关特定存储媒介的读操作覆盖该方法。而且流的所有其它的读数据的方法(如: ReadBuffer , ReadComponent 等)在完成实际的读操作时都调用了 Read 方法。面向对象的动态联编的优点就体现在这儿。因为后继 Stream 对 象只需覆盖 Read 方法,而其它读操作 ( 如 ReadBuffer 、 ReadComponent 等 ) 都不需要重新定义,而且 TStream 还提供了统一的接口。 5. ReadBuffer 方法 声明: procedure ReadBuffer(var Buffer; Count: Longint); ReadBuffer 方法从流中将 Count 个字节复制到 Buffer 中, 并将流的当前指针向后移动 Count 个字节。如读操作超过流的尾部, ReadBuffer 方法引起 EReadError 异常事件。 6. ReadComponent 方法 声明: function ReadComponent(Instance: TComponent): TComponent; ReadComponent 方法从当前流中读取由 Instance 所指定的部件,函数返回所读的部件。 ReadComponent 在读 Instance 及其拥有的所有对象时创建了一个 Reader 对象并调用它的 ReadRootComponent 方法。 如果 Instance 为 nil , ReadComponent 的方法基于流中描述的部件类型信息创建部件,并返回新创建的部件。 7. ReadComponentRes 方法 声明: function ReadComponentRes(Instance: TComponent): TComponent; ReadComponentRes 方法从流中读取 Instance 指定的部件,但是流的当前位置必须是由 WriteComponentRes 方法所写入的部件的位置。 ReadComponentRes 首先调用 ReadResHeader 方法从流中读取资源头,然后调用 ReadComponent 方法读取 Instance 。如果流的当前位置不包含一个资源头。 ReadResHeader 将引发一个 EInvalidImage 异常事件。在 Classes 库单元中也包含一个名为 ReadComponentRes 的函数,该函数执行相同的操作,只不过它基于应 用程序包含的资源建立自己的流。 8. ReadResHeader 方法 声明: procedure ReadResHeader; ReadResHeader 方法从流的当前位置读取 Windows 资源文件头,并将流的当前位置指针移到该文件头的尾部。如果流不包含一个有效的资源文件头, ReadResHeader 将引发一个 EInvalidImage 异常事件。 流的 ReadComponentRes 方法在从资源文件中读取部件之前,会自动调用 ReadResHeader 方法,因此,通常程序员通常不需要自己调用它。 9. Seek 方法 声明: function Seek(Offset: Longint; Origin: Word): Longint; virtual; abstract; Seek 方法将流的当前指针移动 Offset 个字节,字节移动的起点由 Origin 指定。如果 Offset 是负数, Seek 方法将从所描述的起点往流的头部移动。下表中列出了 Origin 的不同取值和它们的含义:
函数 Seek 的参数的取值 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 常量 值 Seek 的起点 Offset 的取值 ───────────────────────────────── SoFromBeginning 0 流的开头 正 数 SoFromCurrent 1 流的当前位置 正数或负数 SoFromEnd 2 流的结尾 负 数 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10. Write 方法 在 Delphi 对象式管理的对象中有两类对象的方法都有称为 Write 的: Stream 对象和 Filer 对象。 Stream 对象的 Write 方法将数据写进流中。 Filer 对象通过相关的流传递数据,在后文中会介绍这类方法。 Stream 对象的 Write 方法声明如下:
function Write(const Buffer; Count: Longint): Longint; virtual; abstract;
Write 方法将 Buffer 中的 Count 个字节写入流中,并将当前位置指针向流的尾部移动 Count 个字节,函数返回写入的字节数。 TStream 的 Write 方法是抽象的,每个继承的 Stream 对象都要通过覆盖该方法来提供向特定存储媒介 ( 内存、磁盘文件等 ) 写数据的特定方法。流的其它所有写数据的方法 ( 如 WriteBuffer 、 WriteComponent) 都调用 Write 担当实际的写操作。 11. WriteBuffer 方法 声明: procedure WriteBuffer(const Buffer; Count: Longint); WriteBuffer 的功能与 Write 相似。 WriteBuffer 方法调用 Write 来执行实际的写操作,如果流没能写所有字节, WriteBuffer 会触发一个 EWriteError 异常事件。 12. WriteComponent 方法 在 Stream 对象和 Filer 对象都有被称为 WriteComponent 的方法。 Stream 对象的 WriteComponent 方法将 Instance 所指定的部件和它所包含的所有部件都写入流中; Writer 对象的 WriteComponent 将指定部件的属性值写入 Writer 对象的流中。 Stream 对象的 WriteComponent 方法声明是这样的: procedure WriteComponent(Instance: Tcomponent);
WriteComponent 创建一个 Writer 对象,并调用 Writer 的 WriteRootComponent 方法将 Instance 及其拥有的对象写入流。 13. WriteComponentRes 方法 声明: WriteComponentRes(const ResName: String; Instance: TComponent); WriteComponentRes 方法首先往流中写入标准 Windows 资源文件头,然后将 Instance 指定的部件写入流中。要读由 WriteComponentRes 写入的部件,必须调用 ReadComponentRes 方法。 WriteComponentRes 使用 ResName 传入的字符串作为资源文件头的资源名,然后调用 WriteComponent 方法将 Instance 和它拥有的部件写入流。 14. WriteDescendant 方法 声明: procedure WriteDescendant(Instance Ancestor: TComponent); Stream 对象的 WriteDescendant 方法创建一个 Writer 对象,然后调入该对象的 WriteDescendant 方法将 Instance 部件写入流中。 Instance 可以是从 Ancestor 部件继承的窗体,也可以是在从祖先窗体中继承的窗体中相应于祖先窗体中 Ancestor 部件的部件。 15. WriteDescendantRes 方法 声明: procedure WriteDescendantRes(const ResName: String; Instance, Ancestor: TComponent); WriteDescendantRes 方法将 Windows 资源文件头写入流,并使用 ResName 作用资源名,然后调用 WriteDescendant 方法,将 Instance 写入流。
TStream 的实现原理
TStream 对象是 Stream 对象的基础类,这是 Stream 对象的基础。为了能在不同媒介上的存储数据对象,后继的 Stream 对象主要是在 Read 和 Write 方法上做了改进,。因此,了解 TStream 是掌握 Stream 对象管理的核心。 Borland 公司虽然提供了 Stream 对象的接口说明文档,但对于其实现和应 用方法却没有提及,笔者是从 Borland Delphi 2.0 Client/Server Suite 提供的源代码和部分例子程序中掌握了流式对象技术。 下面就从 TStream 的属性和方法的实现开始。 1. TStream 属性的实现 前面介绍过, TStream 具有 Position 和 Size 两个属性,作为抽象数据类型,它抽象了在各种存储媒介中读写数据所需要经常访问的域。那么它们是怎样实现的呢? 在自定义部件编写这一章中介绍过部件属性定义中的读写控制。 Position 和 Size 也作了读写控制。定义如下:
property Position: Longint read GetPosition write SetPosition; property Size: Longint read GetSize;
由上可知, Position 是可读写属性,而 Size 是只读的。 Position 属性的实现就体现在 GetPosition 和 SetPosition 。当在程序运行过程中,任何读取 Position 的值和给 Position 赋值的操作都会自动触发私有方法 GetPosition 和 SetPosition 。两个方法的声明如下:
function TStream.GetPosition: Longint; begin Result := Seek(0, 1); end;
procedure TStream.SetPosition(Pos: Longint); begin Seek(Pos, 0); end;
在设置位置时, Delphi 编译机制会自动将 Position 传为 Pos 。 前面介绍过 Seek 的使用方法,第一参数是移动偏移量,第二个参数是移动的起点,返回值是移动后的指针位置。 Size 属性的实现只有读控制,完全屏蔽了写操作。读控制方法 GetSize 实现如下:
function TStream.GetSize: Longint; var Pos: Longint; begin Pos := Seek(0, 1); Result := Seek(0, 2); Seek(Pos, 0); end;
2. TStream 方法的实现 ⑴ CopyFrom 方法 CopyFrom 是 Stream 对象中很有用的方法,它用于在不同存储媒介中拷贝数据。例如,内存与外部文件之间、内存与数据库字段之间等。它简化了许多内存分配、文件打开和读写等的细节,将所有拷贝操作都统一到 Stream 对象上。 前面曾介绍: CopyFrom 方法带 Source 和 Count 两个参数并返回长整型。该方法将 Count 个字节的内容从 Source 拷贝到当前流中,如果 Count 值为 0 则拷贝所有数据。
function TStream.CopyFrom(Source: TStream; Count: Longint): Longint; const MaxBufSize = $F000; var BufSize, N: Integer; Buffer: PChar; begin if Count = 0 then begin Source.Position := 0; Count := Source.Size; end; Result := Count; if Count > MaxBufSize then BufSize := MaxBufSize else BufSize := Count; GetMem(Buffer, BufSize); try while Count <> 0 do begin if Count > BufSize then N := BufSize else N := Count; Source.ReadBuffer(Buffer^, N); WriteBuffer(Buffer^, N); Dec(Count, N); end; finally FreeMem(Buffer, BufSize); end; end;
⑵ ReadBuffer 方法和 WriteBuffer 方法 ReadBuffer 方法和 WriteBuffer 方法简单地调用虚拟函数 Read 、 Write 来读写流中数据,它比 Read 和 Write 增加了读写数据出错时的异常处理。
procedure TStream.ReadBuffer(var Buffer; Count: Longint); begin if (Count <> 0) and (Read(Buffer, Count) <> Count) then raise EReadError.CreateRes(SReadError); end;
procedure TStream.WriteBuffer(const Buffer; Count: Longint); begin if (Count <> 0) and (Write(Buffer, Count) <> Count) then raise EWriteError.CreateRes(SWriteError); end;
⑶ ReadComponent 、 ReadResHeader 和 ReadComponentRes 方法 ReadComponent 方法从当前流中读取部件。在实现上 ReadComponent 方法创建了一个 TStream 对象,并用 TReader 的 ReadRootComponent 方法读部件。在 Delphi 对象式管理中, Stream 对象和 Filer 对象结合很紧密。 Stream 对象的许多方法的实现需要 Filer 对象的支持,而 Filer 对象的构造函数 直接就以 Stream 对象为参数。在 ReadComponent 方法的实现中就可清楚地看到这一点:
function TStream.ReadComponent(Instance: TComponent): TComponent; var Reader: TReader; begin Reader := TReader.Create(Self, 4096); try Result := Reader.ReadRootComponent(Instance); finally Reader.Free; end; end;
ReadResHeader 方法用于读取 Windows 资源文件的文件头,由 ReadComponentRes 方法在读取 Windows 资源文件中的部件时调用,通常程序员不需自己调用。如果读取的不是资源文件 ReadResH := FSize + Offset; end; Result := FPosition; end;
Offse 代表移动的偏移量。 Origin 代表移动的起点,值为 0 表示从文件头开始,值为 1 表示从当前位置开始,值为 2 表示从文件尾往前,这时 OffSet 一般为负数。 Seek 的实现没有越界的判断。 3. SaveToStream 和 SaveToFile 方法 SaveToStream 方法是将 MemoryStream 对象中的内容写入 Stream 所指定的流。其实现如下:
procedure TCustomMemoryStream.SaveToStream(Stream: TStream); begin if FSize <> 0 then Stream.WriteBuffer(FMemory^, FSize); end;
SaveToStream 方法调用了 Stream 的 WriteBuffer 方法,直接将 FMemory 中的内容按 FSize 字节长度写入流中。 SaveToFile 方法是与 SaveToStream 方法相关的。 SaveToFile 方法首先创建了一个 FileStream 对象,然后把该文件 Stream 对象作为 SaveToStream 的参数,由 SaveToStream 方法执行写操作,其实现如下:
procedure TCustomMemoryStream.SaveToFile(const FileName: string); var Stream: TStream; begin Stream := TFileStream.Create(FileName, fmCreate); try SaveToStream(Stream); finally Stream.Free; end; end;
在 Delphi 的许多对象的 SaveToStream 和 SaveToFile 、 LoadFromStream 和 LoadFromFile 方法的实现都有类似的嵌套结构。
TMemoryStream 对象
TMemoryStream 对象是一个管理动态内存中的数据的 Stream 对象,它是从 TCustomMemoryStream 中继承下来的,除了从 TCustomMemoryStream 中继承的属性和方法外,它还增加和覆盖了一些用于从磁盘文件和其它注台读数据的方法。它还提供了写入、消除内存内容的动态内存管理方法。下面 介绍它的这些属性和方法。
TMemoryStream 的属性和方法
1. Capacity 属性 声明: property Copacity: Longint; Capacity 属性决定了分配给内存流的内存池的大小。这与 Size 属性有些不同。 Size 属性是描述流中数据的大小。在程序中可以将 Capacity 的值设置的比数据所需最大内存大一些,这样可以避免频繁地重新分配。 2. Realloc 方法 声明: function Realloc(var NewCapacity: Longint): Pointer; virtual; Realloc 方法,以 8K 为单位分配动态内存,内存的大小由 NewCapacity 指定,函数返回指向所分配内存的指针。 3. SetSize 方法 SetSize 方法消除内存流中包含的数据,并将内存流中内存池的大小设为 Size 字节。如果 Size 为零,是 SetSize 方法将释放已有的内存池,并将 Memory 属性置为 nil ;否则, SetSize 方法将内存池大小调整为 Size 。 4. Clear 方法 声明: procedure Clear; Clear 方法释放内存中的内存池,并将 Memory 属性置为 nil 。在调用 Clear 方法后, Size 和 Position 属性都为 0 。 5. LoadFromStream 方法 声明: procedure LoadFromStream(Stream: TStream); LoadFromStream 方法将 Stream 指定的流中的全部内容复制到 MemoryStream 中,复制过程将取代已有内容,使 MemoryStream 成为 Stream 的一份拷贝。 6. LoadFromFile 方法 声明: procedure LoadFromFile(count FileName: String); LoadFromFile 方法将 FileName 指定文件的所有内容复制到 MemoryStream 中,并取代已有内容。调用 LoadFromFile 方法后, MemoryStream 将成为文件内容在内存中的完整拷贝。
TMemoryStream 对象的实现原理
TMemoryStream 从 TCustomMemoryStream 对象直接继承,因此可以享用 TCustomMemoryStream 的属性和方法。前面讲过, TCustomMemoryStream 是用于内存中数据操作的抽象对象,它为 MemoryStream 对象的实现提供了框架,框架中的内容还要由具体 MemoryStream 对象去填充。 TMemoryStrea m 对象就是按动态内存管理的需要填充框架中的具体内容。下面介绍 TMemoryStream 对象的实 ? 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 字段存储数据。 该函数的各参数含义如下:
调用函数 DbiPutBlob 的各传入参数的含义 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 参数名 含义 ────────────────────────────── FDataSetHandle 写入的数据库的 BDE 句柄 FRecord 写入数据的 BLOB 字段所在的记录 FFieldNo BLOB 字段号 FPosition 写入的起始位置 Count 写入的数据的字节数 Buffer 所写入的数据占有的内存地址 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
标志,该标志意味着后面存储有一连串的项目。 Reader 对象,在读这一连串项目时先调用 ReadListBegin 方法读取该标志位,然后用 EndOfList 判断是否列表结束,并用循环语句读取项目。在调用 WriteListBegin 方法的后面必须调用 WriteListEnd 方法写列表结束标志,相应的在 Reader 对象中 有 ReadListEnd 方法读取该结束标志。 5. WriteListEnd 方法 声明: procedure WriteListEnd; WriteListEnd 方法在流中,写入项目列表结束标志,它是与 WriteListBegin 相匹配的方法。 6. WriteBoolean 方法 声明: procedure WriteBoolean(Value: Boolean); WriteBoolean 方法将 Value 传入的布尔值写入流中。 7. WriteChar 方法 声明: procedure WriteChar(Value: char); WriteChar 方法将 Value 中的字符写入流中。 8. WriteFloat 方法 声明: procedure WriteFloat(Value: Extended); WriteFloat 方法将 Value 传入的浮点数写入流中。 9. WriteInteger 方法 声明: procedure WriteInteger(Value: Longint); WriteInteger 方法将 Value 中的整数写入流中。 10. WriteString 方法 声明: procedure WriteString(const Value: string); WriteString 方法将 Value 中的字符串写入流中。 11. WriteIdent 方法 声明: procedure WriteIdent(const Ident: string); WriteIdent 方法将 Ident 传入的标识符写入流中。 12. WriteSignature 方法 声明: procedure WriteSignature; WriteSignature 方法将 Delphi Filer 对象标签写入流中。 WriteRootComponent 方法在将部件写入流之前先调用 WriteSignature 方法写入 Filer 标签。 Reader 对象在读部件之前调用 ReadSignature 方法读取该标签以指导读操作。 13. WritComponent 方法 声明: procedure WriteComponent(Component: TComponent); WriteComponent 方法调用参数 Component 的 WriteState 方法将部件写入流中。在调用 WriteState 之前, WriteComponent 还将 Component 的 ComponetnState 属性置为 csWriting 。当 WriteState 返回时再清除 csWriting. 14. WriteRootComponent 方法 声明: procedure WriteRootComponent(Root: TComponent); WriteRootComponent 方法将 Writer 对象 Root 属性设为参数 Root 带的值,然后调用 WriteSignature 方法往流中写入 Filer 对象标签,最后调用 WriteComponent 方法在流中存储 Root 部件。 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论