level 11
天道玄虚
楼主
【Delphi 奥法】
天道玄虚出品
其余相关内容见该帖子的五楼楼中楼:https://tieba.baidu.com/p/4934621524
注意:该教程适用于Delphi 10.1.2。
第六章 流,保存,读取以及粘贴板
不得不说,流(TStream)是一个很实用的东西,不管是缓存、读文件、写文件或者其余类似的事情,都离不开流的应用。比如说,要读取一个文本文件,应该使用TStringlist下的方法LoadFromFile,如果深入其中,将会发现其根本还是来自于LoadFromStream。
并不是所有的文件结构都有已经写好的LoadFromStream和SaveToStream,因此很多时候就需要我们自己来提供这样的方法,从而实现保存一种自己独有的文件或者缓存。
那么首先说一说最常用的内存流。
6.0、内存流(TMemoryStream)
顾名思义,内存流在程序运行期间,创建出来后以及被释放之前,是一直待在内存中的,也就意味着,如果程序内部使用内存流传递大量的数据或者缓存能够有比较高的效率。
假定我们声明的内存流为——Stream: TMemoryStream;
内存流的创建于一般类的创建一致,当然是:
Stream := TMemoryStream.Create;
为了防止内存泄露,一般需要在内存流创建后到释放前使用一个try结构,如下:
Stream := TMemoryStream.Create;
try
你需要做的事情
finally
FreeAndNil(Stream)
end;
其中FreeAndNil当然可以替换为Stream.Free,前一个函数的好处是Stream将会被置空,可以在一定程度上便于排错。即便使用Free,区别并不太大。另外,如果是一个其余函数创建的流,那么最好不要随便释放,也就是说,创建和释放的动作最好在一个函数中体现。
如果你有一些内存流一定要在不同函数间使用,而且其创建和释放无法再一个函数中完成,那么尽可能使用全局变量,以免忘记或者无法释放。虽然一个内存流占据的内存并不一定很多,但是毕竟内存是有限的。
那么,我们需要内存流做什么事情呢?
内存流最主要能够做的动作当然是读和写。读就是从流中取出信息,赋予外界变量;写就是把外界变量写入流,或者说赋予流信息。
流的读取和写入操作参数一致,都是:变量、长度。其中,流的读取的方法是:
Stream.Read(变量, 长度);
其中长度即是变量占有内存的长度,除非特殊需要,否则此处请使用:
SizeOf(变量类型)
而非常数。
流的写入方法则是:
Stream.Write(变量, 长度);
需要注意的是,流的读和写的操作最好对变量完成,而不要选择类的属性或者常数。
也就是说,假定整数变量b = 2,那么写入2的时候应该使用:
Stream.Write(b,SizeOf(Integer));
因为变量的内存地址是相对固定的,而常数的地址则通常是难以预料的,因而不应该使用常数。(好像使用常数会报错)
当然,内存流不止能够读写单变量,内存流同样能够读写字符串、数组、流。对于这些长度并非固定的读写动作,有一个必须要注意的技巧。
首先先要提到信息的完备性,也就是说,对于一个流,只有已经取得了所有必备的信息(这个信息对于流而言,就是起始位置和长度,起始位置一般顺次改变,不需要人为调整)的时候才可以完整地读取。对于单变量,只要类型确定,我们即可以确定其长度,而对于字符串、数组、流,我们则不能够这样确定其长度。
比如说,我的一个内存流中储存了另外两个内存流的信息,那么这两个内存流的长度分别是多少?
显而易见,除了事先约定的情况外,没有办法准确地取得这两个内存流的长度,也就是说,我们的信息是不完备的。
那么,很自然地,我们必须要把这种长度不固定的变量的长度也标识出来。
标识长度的问题,从来都有两种风格,一种是C风格,另一种是Pascal风格,就像字符串一样。
何为C风格?即结尾标识。通俗地说,就是规定一个序列,只要读取到这个序列,那么这一段就此结束。讲道理,细胞内部的种种操作:转录、翻译、DNA复制等都是这个风格,也就是说,这种风格其实是经历了无数年实践检验的,是一种经典而且可行度高、简单易行的方法。
比如说,C语言中,字符串就是以#0(C语言里面叫’\0’)作为结尾标识的。
那么Pascal风格呢?就是把长度写在第一个位置。这也就是为什么Pascal的字符串第一个字符不是0索引。
就我个人而言,Pascal风格无疑更为好一些,因为Stream有一个与此适配的方法,叫做ReadBuffer、WriteBuffer。
ReadBuffer和WriteBuffer就是专为读写这一类长度不固定,在内存中连续的变量或者类的。从机理上讲,这种方法就是复制一段连续的内存。不要怀疑这种方法的效率,这种方法的效率是很高的,高到每秒钟上百兆。
也就是说,我们如果使用结尾标识,那么必须每次读取一个字节,然后还要判断是否是结尾标识,如果使用长度标识,那么直接就可以确定需要复制的内存的长度了。
不过,这个方法并不是可以直接套用的,经过实践表明,如果只是:
Stream.WriteBuffer(s,Length(s) * SizeOf(Char));
那么读取的时候,总是会莫名其妙地出错。
为此,对于字符串的操作,我们需要使用TBytes。系统提供了方法转换字符串和TBytes。
6.0.0、字符串的读写
如果我们需要储存的字符串是s,那么需要的变量如下:
var
s: string;
c: Integer;
bs: TBytes;
操作思路是:首先将字符串转换为TBytes,然后获取其长度,然后写入即可。也就是:
bs := WideBytesOf(s);
c := Length(bs);
Stream.Write(c,SizeOf(Integer));
Stream.WriteBuffer(bs, c);
如果是按照这个方法写入的,那么读取的时候思路则是:首先获取长度,然后读取相应长度的流即可。也就是:
Stream.Read(c,SizeOf(Integer));
SetLength(bs);
Stream.ReadBuffer(bs, c);
s := WideStringOf(bs);
这里使用了WideBytes和WideString(不需要额外声明),也就是说字符串的编码使用的是Unicode而不是ANSI。
6.0.1、列表的读写
由于数组是不好用的,因此一般也就不使用了……
一般情况下,列表中储存的大都是自定义的类,因此其内存未必是连续的。目前还没有找到适配WriteBuffer的方法。
如果要储存列表的内容,那么最好的套路依旧是第一步储存列表中项目的个数,也就是TList.Count,存下这个之后,按照项目依次处理就好。
6.0.2、流的读写
流的读写是一个很重要的部分,通常我们使用的类,有很多都是提供SaveToStream方法的,这时候如果再自己写方法,那既浪费时间,又不必要。这种时候,就需要研究流的读写。
假定我们需要处理一个图片,也就是TBitmap,TBitmap提供SaveToStream方法。为此,通常不选择直接将图片存入目标流中,因为这样我们必然又和之前一样,面临缺少长度信息的问题。
为了解决这个问题,我们需要另外一个临时内存流,不妨称作temp。
也就是:
var
temp: TMemoryStream;
如果以存储图片为例,我们需要做以下操作:
var
bmp: TBitmap;
temp: TMemoryStream;
c: Integer;
省略中间的部分。
bmp.SaveToStream(temp);
temp.Position := 0;
c := temp.Size;
Stream.Write(c,SizeOf(Integer));
Stream.WriteBuffer(temp.Memory^,c);
一定不要少了temp.Position := 0这一步,如果缺少这一步,那么Stream中写入的内容将会是乱码。
要读取时,只需要进行逆向操作:
Stream.Read(c,SizeOf(Integer));
temp.SetSize := c;
temp.Position := 0;
Stream.ReadBuffer(temp.Memory^,c);
temp.Position := 0;
bmp.LoadFromStream(temp);
同样地,不要少了设置Position的两步,否则很容易什么也没有。任何和流有关的操作中,时刻想清楚你目前的流的Position属性是不是你需要的值,记得任何Read、Write的方法都会改变其Position属性。
6.1、文件流(TFileStream)
我们的数据并不会是一直在内存中的,经常需要写入硬盘的文件中。相应地,我们也经常需要从硬盘的文件中读取一些信息。这个时候,我们就需要文件流。
文件流在创建时关联文件,释放时完成诸如写入文件的操作,在之间进行流操作。创建时需要指定关联方式,如下:
fmOpenRead:只读
fmOpenWrite:只写
fmOpenReadWrite:读写
mShareDenyWrite:防止写入
fmCreate:创建
其余还有一些参数,大都不太常用。
如果需要指定多个参数,那么使用or连接,比如:
Stream :=TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
通常,类似SaveToFile、LoadFromFile之类的方法都是通过和SaveToSream,LoadFromStream配合使用来实现的,比如:
procedureTUIPassage.LoadFromFile(FileName: string);
var
Stream: TStream;
begin
Stream := TFileStream.Create(FileName,fmOpenRead or fmShareDenyWrite);
try
LoadFromStream(Stream);
finally
Stream.Free;
end;
end;
procedure TUIPassage.SaveToFile(FileName:string);
var
Stream: TStream;
begin
Stream := TFileStream.Create(FileName,fmCreate);
try
SaveToStream(Stream);
finally
Stream.Free;
end;
end;
6.2、粘贴板
流的应用还可以在复制和粘贴自定义格式的类中。为此我们需要使用到IFMXExtendedClipboardService这个接口类。这个类声明在单元FMX.Clipboard中。
var
ClipService: IFMXExtendedClipboardService;
当我们已经将一段数据储存为流之后,可以使用以下方法来实现将其写入粘贴板(Clipboard)中:
ifTPlatformServices.Current.SupportsPlatformService(IFMXExtendedClipboardService,ClipService) then
begin
if not ClipService.IsCustomFormatRegistered('格式名称') then
ClipService.RegisterCustomFormat('格式名称');
save.Position := 0;
ClipService.SetCustomFormat('格式名称', save);
end;
如果需要读取,那么类似地,使用以下方法将粘贴板中的数据写入一个流中:
if not ClipService.HasCustomFormat('格式名称') then
Exit;
if not ClipService.GetCustomFormat('格式名称', Stream)then
Exit;
这个函数ClipService.GetCustomFormat('格式名称', Stream)在无法获取正确的格式时会返回False。
2017年03月01日 05点03分
1
天道玄虚出品
其余相关内容见该帖子的五楼楼中楼:https://tieba.baidu.com/p/4934621524
注意:该教程适用于Delphi 10.1.2。
第六章 流,保存,读取以及粘贴板
不得不说,流(TStream)是一个很实用的东西,不管是缓存、读文件、写文件或者其余类似的事情,都离不开流的应用。比如说,要读取一个文本文件,应该使用TStringlist下的方法LoadFromFile,如果深入其中,将会发现其根本还是来自于LoadFromStream。
并不是所有的文件结构都有已经写好的LoadFromStream和SaveToStream,因此很多时候就需要我们自己来提供这样的方法,从而实现保存一种自己独有的文件或者缓存。
那么首先说一说最常用的内存流。
6.0、内存流(TMemoryStream)
顾名思义,内存流在程序运行期间,创建出来后以及被释放之前,是一直待在内存中的,也就意味着,如果程序内部使用内存流传递大量的数据或者缓存能够有比较高的效率。
假定我们声明的内存流为——Stream: TMemoryStream;
内存流的创建于一般类的创建一致,当然是:
Stream := TMemoryStream.Create;
为了防止内存泄露,一般需要在内存流创建后到释放前使用一个try结构,如下:
Stream := TMemoryStream.Create;
try
你需要做的事情
finally
FreeAndNil(Stream)
end;
其中FreeAndNil当然可以替换为Stream.Free,前一个函数的好处是Stream将会被置空,可以在一定程度上便于排错。即便使用Free,区别并不太大。另外,如果是一个其余函数创建的流,那么最好不要随便释放,也就是说,创建和释放的动作最好在一个函数中体现。
如果你有一些内存流一定要在不同函数间使用,而且其创建和释放无法再一个函数中完成,那么尽可能使用全局变量,以免忘记或者无法释放。虽然一个内存流占据的内存并不一定很多,但是毕竟内存是有限的。
那么,我们需要内存流做什么事情呢?
内存流最主要能够做的动作当然是读和写。读就是从流中取出信息,赋予外界变量;写就是把外界变量写入流,或者说赋予流信息。
流的读取和写入操作参数一致,都是:变量、长度。其中,流的读取的方法是:
Stream.Read(变量, 长度);
其中长度即是变量占有内存的长度,除非特殊需要,否则此处请使用:
SizeOf(变量类型)
而非常数。
流的写入方法则是:
Stream.Write(变量, 长度);
需要注意的是,流的读和写的操作最好对变量完成,而不要选择类的属性或者常数。
也就是说,假定整数变量b = 2,那么写入2的时候应该使用:
Stream.Write(b,SizeOf(Integer));
因为变量的内存地址是相对固定的,而常数的地址则通常是难以预料的,因而不应该使用常数。(好像使用常数会报错)
当然,内存流不止能够读写单变量,内存流同样能够读写字符串、数组、流。对于这些长度并非固定的读写动作,有一个必须要注意的技巧。
首先先要提到信息的完备性,也就是说,对于一个流,只有已经取得了所有必备的信息(这个信息对于流而言,就是起始位置和长度,起始位置一般顺次改变,不需要人为调整)的时候才可以完整地读取。对于单变量,只要类型确定,我们即可以确定其长度,而对于字符串、数组、流,我们则不能够这样确定其长度。
比如说,我的一个内存流中储存了另外两个内存流的信息,那么这两个内存流的长度分别是多少?
显而易见,除了事先约定的情况外,没有办法准确地取得这两个内存流的长度,也就是说,我们的信息是不完备的。
那么,很自然地,我们必须要把这种长度不固定的变量的长度也标识出来。
标识长度的问题,从来都有两种风格,一种是C风格,另一种是Pascal风格,就像字符串一样。
何为C风格?即结尾标识。通俗地说,就是规定一个序列,只要读取到这个序列,那么这一段就此结束。讲道理,细胞内部的种种操作:转录、翻译、DNA复制等都是这个风格,也就是说,这种风格其实是经历了无数年实践检验的,是一种经典而且可行度高、简单易行的方法。
比如说,C语言中,字符串就是以#0(C语言里面叫’\0’)作为结尾标识的。
那么Pascal风格呢?就是把长度写在第一个位置。这也就是为什么Pascal的字符串第一个字符不是0索引。
就我个人而言,Pascal风格无疑更为好一些,因为Stream有一个与此适配的方法,叫做ReadBuffer、WriteBuffer。
ReadBuffer和WriteBuffer就是专为读写这一类长度不固定,在内存中连续的变量或者类的。从机理上讲,这种方法就是复制一段连续的内存。不要怀疑这种方法的效率,这种方法的效率是很高的,高到每秒钟上百兆。
也就是说,我们如果使用结尾标识,那么必须每次读取一个字节,然后还要判断是否是结尾标识,如果使用长度标识,那么直接就可以确定需要复制的内存的长度了。
不过,这个方法并不是可以直接套用的,经过实践表明,如果只是:
Stream.WriteBuffer(s,Length(s) * SizeOf(Char));
那么读取的时候,总是会莫名其妙地出错。
为此,对于字符串的操作,我们需要使用TBytes。系统提供了方法转换字符串和TBytes。
6.0.0、字符串的读写
如果我们需要储存的字符串是s,那么需要的变量如下:
var
s: string;
c: Integer;
bs: TBytes;
操作思路是:首先将字符串转换为TBytes,然后获取其长度,然后写入即可。也就是:
bs := WideBytesOf(s);
c := Length(bs);
Stream.Write(c,SizeOf(Integer));
Stream.WriteBuffer(bs, c);
如果是按照这个方法写入的,那么读取的时候思路则是:首先获取长度,然后读取相应长度的流即可。也就是:
Stream.Read(c,SizeOf(Integer));
SetLength(bs);
Stream.ReadBuffer(bs, c);
s := WideStringOf(bs);
这里使用了WideBytes和WideString(不需要额外声明),也就是说字符串的编码使用的是Unicode而不是ANSI。
6.0.1、列表的读写
由于数组是不好用的,因此一般也就不使用了……
一般情况下,列表中储存的大都是自定义的类,因此其内存未必是连续的。目前还没有找到适配WriteBuffer的方法。
如果要储存列表的内容,那么最好的套路依旧是第一步储存列表中项目的个数,也就是TList.Count,存下这个之后,按照项目依次处理就好。
6.0.2、流的读写
流的读写是一个很重要的部分,通常我们使用的类,有很多都是提供SaveToStream方法的,这时候如果再自己写方法,那既浪费时间,又不必要。这种时候,就需要研究流的读写。
假定我们需要处理一个图片,也就是TBitmap,TBitmap提供SaveToStream方法。为此,通常不选择直接将图片存入目标流中,因为这样我们必然又和之前一样,面临缺少长度信息的问题。
为了解决这个问题,我们需要另外一个临时内存流,不妨称作temp。
也就是:
var
temp: TMemoryStream;
如果以存储图片为例,我们需要做以下操作:
var
bmp: TBitmap;
temp: TMemoryStream;
c: Integer;
省略中间的部分。
bmp.SaveToStream(temp);
temp.Position := 0;
c := temp.Size;
Stream.Write(c,SizeOf(Integer));
Stream.WriteBuffer(temp.Memory^,c);
一定不要少了temp.Position := 0这一步,如果缺少这一步,那么Stream中写入的内容将会是乱码。
要读取时,只需要进行逆向操作:
Stream.Read(c,SizeOf(Integer));
temp.SetSize := c;
temp.Position := 0;
Stream.ReadBuffer(temp.Memory^,c);
temp.Position := 0;
bmp.LoadFromStream(temp);
同样地,不要少了设置Position的两步,否则很容易什么也没有。任何和流有关的操作中,时刻想清楚你目前的流的Position属性是不是你需要的值,记得任何Read、Write的方法都会改变其Position属性。
6.1、文件流(TFileStream)
我们的数据并不会是一直在内存中的,经常需要写入硬盘的文件中。相应地,我们也经常需要从硬盘的文件中读取一些信息。这个时候,我们就需要文件流。
文件流在创建时关联文件,释放时完成诸如写入文件的操作,在之间进行流操作。创建时需要指定关联方式,如下:
fmOpenRead:只读
fmOpenWrite:只写
fmOpenReadWrite:读写
mShareDenyWrite:防止写入
fmCreate:创建
其余还有一些参数,大都不太常用。
如果需要指定多个参数,那么使用or连接,比如:
Stream :=TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
通常,类似SaveToFile、LoadFromFile之类的方法都是通过和SaveToSream,LoadFromStream配合使用来实现的,比如:
procedureTUIPassage.LoadFromFile(FileName: string);
var
Stream: TStream;
begin
Stream := TFileStream.Create(FileName,fmOpenRead or fmShareDenyWrite);
try
LoadFromStream(Stream);
finally
Stream.Free;
end;
end;
procedure TUIPassage.SaveToFile(FileName:string);
var
Stream: TStream;
begin
Stream := TFileStream.Create(FileName,fmCreate);
try
SaveToStream(Stream);
finally
Stream.Free;
end;
end;
6.2、粘贴板
流的应用还可以在复制和粘贴自定义格式的类中。为此我们需要使用到IFMXExtendedClipboardService这个接口类。这个类声明在单元FMX.Clipboard中。
var
ClipService: IFMXExtendedClipboardService;
当我们已经将一段数据储存为流之后,可以使用以下方法来实现将其写入粘贴板(Clipboard)中:
ifTPlatformServices.Current.SupportsPlatformService(IFMXExtendedClipboardService,ClipService) then
begin
if not ClipService.IsCustomFormatRegistered('格式名称') then
ClipService.RegisterCustomFormat('格式名称');
save.Position := 0;
ClipService.SetCustomFormat('格式名称', save);
end;
如果需要读取,那么类似地,使用以下方法将粘贴板中的数据写入一个流中:
if not ClipService.HasCustomFormat('格式名称') then
Exit;
if not ClipService.GetCustomFormat('格式名称', Stream)then
Exit;
这个函数ClipService.GetCustomFormat('格式名称', Stream)在无法获取正确的格式时会返回False。