Delphi在这两方面都做的相当出色。在Delphi的早期版本Turbo Pascal 中就曾有流(Stream)、群(Collection)和资源(Resource)等专门用于对象式数据管理的类。在Delphi中,这些功能得到了大大的加强。Delphi将对象式数据管理类归结为Stream对象(Stream)和Filer对象(Filer),并将他们应用于可视部件类库(VCL)的方方面面。他们不仅提供了在内存、外存和视窗系统资源中管理对象的功能,还提供了在数据库BLOB字段中对象的功能。
在本章中将介绍Stream对象和Filer对象的实现原理、应用方法及在超媒体系统中的应用。这对于运用Delphi 研发高级应用是非常重要的。
20.1 流式对象的实现原理和应用
Stream对象,又称流式对象,是TStream、THandleStream、TFileStream、TMemoryStream、TResourceStream和TBlobStream等的统称。他们分别代表了在各种媒介上存储数据的能力,他们将各种数据类型(包括对象和部件) 在内存、外存和数据库字段中的管理操作抽象为对象方法,并且充分利用了面向对象技术的好处,应用程式能相当容易地在各种Stream对象中拷贝数据。
下面介绍各种对象的数据和方法及使用方法。
20.1.1 TStream对象
TStream对象是能在各种媒介中存储二进制数据的对象的抽象对象。从TStream 对象继承的对象用于在内存、视窗系统资源文件、磁盘文件和数据库字段等媒介中存储数据。
TStream中定义了两个属性:Size和Position。他们分别以字节为单位表示的流的大小和当前指针位置。TStream中定义的方法用于在各种流中读、写和相互拷贝二进制数据。因为所有的Stream对象都是从TStream中继承来的,所以在TStream中定义的域和方法都能被Stream对象调用和访问。此外,又由于面向对象技术的动态联编功能,TStream为各种流的应用提供了统一的接口,简化了流的使用;不同Stream对象是抽象了对不同存储媒介的数据上的操作,因此,TStream的需方法为在不同媒介间的数据拷贝提供了最简捷的手段。
20.1.1.1 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方法从流的当前位置读取视窗系统资源文件头,并将流的当前位置指针移到该文件头的尾部。如果流不包含一个有效的资源文件头,ReadResHeader将引发一个EInvalidImage异常事件。
流的ReadComponentRes方法在从资源文件中读取部件之前,会自动调用ReadResHeader方法,因此,通常程式员通常不必自己调用他。
9. Seek方法
声明:function Seek(Offset: Longint; Origin: Word): Longint; virtual; abstract;
Seek方法将流的当前指针移动Offset个字节,字节移动的起点由Origin指定。如果Offset是负数,Seek方法将从所描述的起点往流的头部移动。下表中列出了Origin的不同取值和他们的含义:
表20.1 函数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方法首先往流中写入标准视窗系统 资源文件头,然后将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方法将视窗系统资源文件头写入流,并使用ResName作用资源名,然后调用WriteDescendant方法,将Instance写入流。
20.1.1.2 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; CouNG="ZH-CN">资源文件中的部件时调用,通常程式员不需自己调用。如果读取的不是资源文件ReadResHeader,将触发异常事件。
procedure TStream.ReadResHeader; var ReadCount: Longint; Header: array[0..79] of Char; begin FillChar(Header, SizeOf(Header), 0); ReadCount := Read(Header, SizeOf(Header) - 1); if (Byte((@Header[0])^) = $FF) and (Word((@Header[1])^) = 10) then Seek(StrLen(Header + 3) + 10 - ReadCount, 1) else raise EInvalidImage.CreateRes(SInvalidImage); end;
ReadComponentRes在视窗系统资源文件中读取部件,为了判断是否是资源文件,他首先调用ReadResHeader方法,然后调用ReadComponent方法读取Instance指定的部件。下面是他的实现:
function TStream.ReadComponentRes(Instance: TComponent): TComponent;
begin ReadResHeader; Result := ReadComponent(Instance); end;
和ReadComponentRes相应的写方法是WriteComponentRes,Delphi 调用这两个方法读写窗体文件(DFM文件),在后面书中会举用这两个方法读取DFM文件的例子。
⑷ WriteComponent和WriteDescendant方法
Stream对象的WriteDescendant方法在实现过程中,创建了TWriter对象,然后利用TWriter的WriteDescendant方法将Instance写入流。而WriteComponent方法只是简单地调用WriteDescendant方法将Instance写入流。他们的实现如下:
procedure TStream.WriteComponent(Instance: TComponent); begin WriteDescendent(Instance, nil); end;
procedure TStream.WriteDescendent(Instance, Ancestor: TComponent); var Writer: TWriter; begin Writer := TWriter.Create(Self, 4096); try Writer.WriteDescendent(Instance, Ancestor); finally
Writer.Free; end; end;
⑸ WriteDescendantRes和WriteComponentRes方法
WriteDescendantRes方法用于将部件写入视窗系统资源文件;而WriteComponentRes 方法只是简单地调用WriteDescendantRes方法,他们的实现如下:
procedure TStream.WriteComponentRes(const ResName: string; Instance: TComponent); begin WriteDescendentRes(ResName, Instance, nil); end;
procedure TStream.WriteDescendentRes(const ResName: string; Instance, Ancestor: TComponent); var HeaderSize: Integer; Origin, ImageSize: Longint; Header: array[0..79] of Char; begin Byte((@Header[0])^) := $FF;
Word((@Header[1])^) := 10;
HeaderSize := StrLen(StrUpper(StrPLCopy(@Header[3], ResName, 63))) + 10;
Word((@Header[HeaderSize - 6])^) := $1030;
Longint((@Header[HeaderSize - 4])^) := 0;
WriteBuffer(Header, HeaderSize);
Origin := Position;
WriteDescendent(Instance, Ancestor);
ImageSize := Position - Origin;
Position := Origin - 4;
WriteBuffer(ImageSize, SizeOf(Longint));
Position := Origin + ImageSize;
end;
WriteCompnentRes是和ReadComponentRes相应的对象写方法,这两个方法相互配合可读取Delphi的DFM文件,从而利用Delphi系统的功能。
20.1.2 THandleStream对象
THandleStream对象的行为特别象FileStream对象,所不同的是他通过已创建的文件句柄而不是文件名来存储流中的数据。
THandleStream对象定义了Handle属性,该属性提供了对文件句柄的只读访问,并且Handle属性能作为Delphi的RTL文件管理函数的参数,利用文件类函数来读写数据。THandleStream覆盖了构造函数Create,该函数带有Handle 参数,该参数指定和THandleStream对象相关的文件句柄。
20.1.2.1 THandleStream的属性的方法:
1. Handle属性
声明:property Handle: Integer;
Handle属性提供了对文件句柄的只读访问,该句柄由THandleStream的构造方法Create传入。因此除了用THandleStream提供的方法外,也能用文件管理函数对句柄进行操作。实际上,THandleStream的方法在实现上也是运用文件管理函数进行实际的读写操作。
2. Create方法
声明:constructor Create(AHandle: Integer);
Create方法使用传入的Handle参数创建一个和特定文件句柄相联的THandleStream对象,并且将AHandle赋给流的Handle属性。
3. Read、Write和Seek方法
这三个方法是TStream的虚方法,只是在THandleStream 中覆盖了这三个方法,以实现特定媒介──文件的数据存取。后面会周详介绍这三个方法的实现。
20.1.2.2 THandleStream的实现原理
THandleStream是从TStream继承来的,因此能共用TStream中的属性和大多数方法。THandleStream在实现上主要是增加了一个属性Handle和覆盖了Create、Read、Write和Seek四个方法。
1. 属性的实现
Handle属性的实现正如Delphi大多数属性的实现那样,先在对象定义的*******部分声明一个存放数据的变量FHandle,然后在定义的public部分声明属性Handle,其中属性定义的读写控制部分加上只读控制,读控制只是直接读取FHandle变量的值,其实现如下:
THandleStream = class(TStream)
*******
FHandle: Integer;
public
…
property Handle: Integer read FHandle;
end;
2. 方法的实现
THandleStream的Create方法,以AHandle作为参数,在方法里面只是简单的将AHandle的值赋给FHandle,其实现如下:
constructor THandleStream.Create(AHandle: Integer);
begin
FHandle := AHandle;
end;
为实现针对文件的数据对象存储,THandleStream的Read、Write和Seek方法覆盖了TStream中的相应方法。他们的实现都调用了视窗系统的文件管理函数。
Read方法调用FileRead函数实现文件读操作,其实现如下:
function THandleStream.Read(var Buffer; Count: Longint): Longint;
begin
Result := FileRead(FHandle, Buffer, Count);
if Result = -1 then Result := 0;
end;
Write方法调用FileWrite函数实现文件写操作,其实现如下:
function THandleStream.Write(const Buffer; Count: Longint): Longint;
begin
Result := FileWrite(FHandle, Buffer, Count);
if Result = -1 then Result := 0;
end;
Seek方法调用FileSeek函数实现文件指针的移动,其实现如下:
function THandleStream.Seek(Offset: Longint; Origin: Word): Longint;
begin
Result := FileSeek(FHandle, Offset, Origin);
end;
20.1.3 TFileStream对象
TFileStream对象是在磁盘文件上存储数据的Stream对象。TFileStream是从THandleStream继承下来的,他和THandleStream相同都是实现文件的存取操作。不同之处在于THandleStream用句柄访问文件,而TFileStream用文件名访问文件。实际上TFileStream是THandleStream上的一层包装,其内核是THandleStream的属性和方法。
TFileStream中没有增加新的属性和方法。他只是覆盖了的构造方法Create和析构方法Destory。在Create方法中带两个参数FileName和Mode。FileName描述要创建或打开的文件名,而Mode描述文件模式如fmCreate、fmOpenRead和fmOpenWrite等。Create方法首先使用FileCreate或FileOpen函数创建或打开名为FileName的文件,再将得到的文件句柄赋给FHandle。TFileStream的文件读写操作都是由从THandleStream继承的Read
var
Stream: TStream;
begin
Stream := TFileStream.Create(FileName, fmCreate);
try
SaveToStream(Stream);
finally
Stream.Free;
end;
end;
在Delphi 的许多对象的SaveToStream 和SaveToFile、LoadFromStream和LoadFromFile方法的实现都有类似的嵌套结构。
20.1.5 TMemoryStream对象
TMemoryStream对象是个管理动态内存中的数据的Stream对象,他是从TCustomMemoryStream中继承下来的,除了从TCustomMemoryStream中继承的属性和方法外,他还增加和覆盖了一些用于从磁盘文件和其他注台读数据的方法。他还提供了写入、消除内存内容的动态内存管理方法。下面介绍他的这些属性和方法。
20.1.5.1 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将成为文件内容在内存中的完整拷贝。
20.1.5.2 TMemoryStream对象的实现原理
TMemoryStream从TCustomMemoryStream对象直接继承,因此能享用TCustomMemoryStream的属性和方法。前面讲过,TCustomMemoryStream是用于内存中数据操作的抽象对象,他为MemoryStream对象的实现提供了框架,框架中的内容还要由具体MemoryStream对象去填充。TMemoryStream对象就是按动态内存管理的需要填充框架中的具体内容。下面介绍TMemoryStream对象的实现。
1. TMemoryStream属性的实现
TMemoryStream在其protected部分增加了一个Capacity属性,该属性决定了MemoryStream所占动态内存的大小。TMemoryStream首先在*******部分声明了FCapacity变量作为存储Capacity属性值的数据域,然后在protected部分声明了该属性。在属性声明的读控制部分简单读取FCapacity的值,在写控制处调用了方法SetCapacity。该方法除了给FCapacity赋值外还执行了修改Capacity属性所必需操作如状态改动等。
下面是属性的实现:
TMemoryStream = class(TCustomMemoryStream)
*******
FCapacity: Longint;
procedure SetCapacity(NewCapacity: Longint);
protected
…
property Capacity: Longint read FCapacity write SetCapacity;
public
…
end;
写控制方法SetCapacity的实现是这样的:
procedure TMemoryStream.SetCapacity(NewCapacity: Longint);
begin
SetPointer(Realloc(NewCapacity), FSize);
FCapacity := NewCapacity;
end;
在SetCapacity 方法先是调用Realloc重新分配内存,然后用NewCapacity的值给FCapacity赋值。Realloc方法进行某些对象状态的改动。
2. TMemoryStream对象方法的实现
⑴ Realloc方法
Realloc方法是TMemoryStream动态内存分配的核心,他的SetSize、SetCapacity等方法最终都是调用Realloc进行内存的分配和初始化工作的。他的实现如下:
const
MemoryDelta = $2000;
function TMemoryStream.Realloc(var NewCapacity: Longint): Pointer;
begin
if NewCapacity > 0 then
NewCapacity := (NewCapacity + (MemoryDelta - 1)) and not (MemoryDelta - 1);
Result := Memory;
if NewCapacity <> FCapacity then
begin
if NewCapacity = 0 then
begin
GlobalFreePtr(Memory);
Result := nil;
end else
begin
if Capacity = 0 then
Result := GlobalAllocPtr(HeapAllocFlags, NewCapacity)
else
Result := GlobalReallocPtr(Memory, NewCapacity, HeapAllocFlags);
if Result = nil then raise EStreamError.CreateRes(SMemoryStreamError);
end;
end;
end;
Realloc方法是以8K为单位分配动态内存的,方法中的第一句if语句就是执行该操作。如果传入的NewCapacity参数值为0,则释放流中的内存。Realloc方法用GLobal FreePtr函数释放内存,用GlobalAllocPtr分配内存,用GlobalReallocPtr进行内存的重分配。如果原来的Capacity属性值为0,则调用Globa|AllocPtr否则调用GlobalReallocPtr。最后如果Result为nil则触发内存流错的异常事件,否则返回指向分配的内存的指针。
⑵ Write方法
Write方法从内存流内部缓冲池的当前位置开始写入二进制数据。其实现如下:
function TMemoryStream.Write(const Buffer; Count: Longint): Longint;
var
Pos: Longint;
begin
if (FPosition >= 0) and (Count >= 0) then
begin
Pos := FPosition + Count;
if Pos > 0 then
begin
if Pos > FSize then
begin
if Pos > FCapacity then
SetCapacity(Pos);
FSize := Pos;
end;
System.Move(Buffer, Pointer(Longint(FMemory) + FPosition)^, Count);
FPosition := Pos;
Result := Count;
Exit;
end;
end;
Result := 0;
end;
Buffer中存储要写入流的二进制数据,如果要写入的数据的字节超出了流的内存池的大小,则调用SetCapacity方法再分配内存,然后用内存复制函数将Buffer中的数据复制到FMemory中。接着移动位置指针,并返回写入数据的字节数。分析这段程式能知道,FCapacity的值和FSize的值是不同的。
⑶ Clear方法
Clear方法消除内存流中的数据,将Memory属性置为nil,并将FSize和FPosition 的值设为0。其实现如下:
procedure TMemoryStream.Clear;
begin
SetCapacity(0);
FSize := 0;
FPosition := 0;
end;
⑷ LoadFromStream和LoadFromFile方法
LoadFromStream方法首先根据传入的Stream的Size属性值重新分配动态内存,然后调用Stream的ReadBuffer方法往FMemory中复制数据,结果Stream的全部内容在内存中有了一份完整拷贝。其实现如下:
procedure TMemoryStream.LoadFromStream(Stream: TStream);
var Count: Longint;
begin Stream.Position := 0; Count := Stream.Size; SetSize(Count); if Count <> 0 then Stream.ReadBuffer(FMemory^, Count); end;
|
请发表评论