• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

谈Delphi编程中“流”的应用

原作者: [db:作者] 来自: [db:来源] 收藏 邀请
谈Delphi编程中“流”的应用 

    陈经韬  

    什么是流?流,简单来说就是建立在面向对象基础上的一种抽象的处理数据的工具。在流中,定 
义了一些处理数据的基本操作,如读取数据,写入数据等,程序员是对流进行所有操作的,而不用 
关心流的另一头数据的真正流向。流不但可以处理文件,还可以处理动态内存、网络数据等多种数 
据形式。如果你对流的操作非常熟练,在程序中利用流的方便性,写起程序会大大提高效率的。  

    下面,笔者通过四个实例:EXE 文件加密器、电子贺卡、自制 OICQ 和网络屏幕传输来说明 Delphi  
编程中“流”的利用。这些例子中的一些技巧曾经是很多软件的秘密而不公开的,现在大家可以无 
偿的直接引用其中的代码了。  

    “万丈高楼平地起”,在分析实例之前,我们先来了解一下流的基本概念和函数,只有在理解了 
这些基本的东西后我们才能进行下一步。请务必认真领会这些基本方法。当然,如果你对它们已经 
很熟悉了,则可以跳过这一步。  

    一、Delphi 中流的基本概念及函数声明  

    在 Delphi 中,所有流对象的基类为 TStream 类,其中定义了所有流的共同属性和方法。  

    TStream 类中定义的属性介绍如下:  

    1、Size: 此属性以字节返回流中数据大小。  

    2、Position: 此属性控制流中存取指针的位置。  

    Tstream 中定义的虚方法有四个:  

    1、Read: 此方法实现将数据从流中读出。函数原形为:  

    Function Read(var Buffer;Count:Longint):Longint;virtual;abstract;   

    参数 Buffer 为数据读出时放置的缓冲区,Count 为需要读出的数据的字节数,该方法返回值 
为实际读出的字节数,它可以小于或等于 Count 中指定的值。  

    2、Write: 此方法实现将数据写入流中。函数原形为:  

    Function Write(var Buffer;Count:Longint):Longint;virtual;abstract;   

    参数 Buffer 为将要写入流中的数据的缓冲区,Count 为数据的长度字节数,该方法返回值为 
实际写入流中的字节数。  

    3、Seek: 此方法实现流中读取指针的移动。函数原形为:  

    Function Seek(Offset:Longint;Origint:Word):Longint;virtual;abstract;  

    参数 Offset 为偏移字节数,参数 Origint 指出 Offset 的实际意义,其可能的取值如下:  

    soFromBeginning:Offset 为移动后指针距离数据开始的位置。此时 Offset 必须大于或者等于 
零。  

    soFromCurrent:Offset 为移动后指针与当前指针的相对位置。  

    soFromEnd:Offset 为移动后指针距离数据结束的位置。此时 Offset 必须小于或者等于零。该 
方法返回值为移动后指针的位置。  

    4、Setsize: 此方法实现改变数据的大小。函数原形为:  


    Function Setsize(NewSize:Longint);virtual;   

    另外,TStream 类中还定义了几个静态方法:  

    1、ReadBuffer: 此方法的作用是从流中当前位置读取数据。函数原形为:  

    Procedure ReadBuffer(var Buffer;Count:Longint); 参数的定义跟上面的 Read 相同。  

    注意:当读取的数据字节数与需要读取的字节数不相同时,将产生 EReadError 异常。  

    2、WriteBuffer: 此方法的作用是在当前位置向流写入数据。函数原形为:Procedure  
WriteBuffer(var Buffer;Count:Longint);   

    参数的定义跟上面的 Write 相同。  

    注意:当写入的数据字节数与需要写入的字节数不相同时,将产生 EWriteError 异常。  

    3、CopyFrom: 此方法的作用是从其它流中拷贝数据流。函数原形为:  

    Function CopyFrom(Source:TStream;Count:Longint):Longint;  

    参数 Source 为提供数据的流,Count 为拷贝的数据字节数。当 Count 大于 0 时,CopyFrom 从 
Source 参数的当前位置拷贝 Count 个字节的数据;当 Count 等于 0 时,CopyFrom 设置 Source  
参数的 Position 属性为 0,然后拷贝 Source 的所有数据;  

    TStream 还有其它派生类,其中最常用的是 TFileStream 类。使用 TFileStream 类来存取文件, 
首先要建立一个实例。声明如下:  

    constructor Create(const Filename:string;Mode:Word);  

    Filename 为文件名(包括路径),参数 Mode 为打开文件的方式,它包括文件的打开模式和共 
享模式,其可能的取值和意义如下:  

    打开模式:  

       fmCreate : 用指定的文件名建立文件,如果文件已经存在则打开它。  

       fmOpenRead : 以只读方式打开指定文件  

       fmOpenWrite : 以只写方式打开指定文件  

       fmOpenReadWrite: 以写写方式打开指定文件  

    共享模式:  

       fmShareCompat : 共享模式与 FCBs 兼容  

       fmShareExclusive: 不允许别的程序以任何方式打开该文件  

       fmShareDenyWrite: 不允许别的程序以写方式打开该文件  

       fmShareDenyRead : 不允许别的程序以读方式打开该文件  

       fmShareDenyNone : 别的程序可以以任何方式打开该文件  

    TStream 还有一个派生类 TMemoryStream ,实际应用中用的次数也非常频繁。它叫内存流,就 
是说在内存中建立一个流对象。它的基本方法和函数跟上面是一样的。好了,有了上面的基础后, 
我们就可以开始我们的编程之行了。  

    二、实际应用之一:利用流制作 EXE 文件加密器、捆绑、自解压文件及安装程序  

    我们先来说一下如何制作一个 EXE 文件加密器吧。  

    EXE 文件加密器的原理:建立两个文件,一个用来添加资源到另外一个 EXE 文件里面,称为添 



加程序。另外一个被添加的 EXE 文件称为头文件。该程序的功能是把添加到自己里面的文件读出来。 
Windows 下的 EXE 文件结构比较复杂,有的程序还有校验和,当发现自己被改变后会认为自己被病 
毒感染而拒绝执行。所以我们把文件添加到自己的程序里面,这样就不会改变原来的文件结构了。 
我们先写一个添加函数,该函数的功能是把一个文件当作一个流添加到另外一个文件的尾部。函数 
如下:  

    Function Cjt_AddtoFile(SourceFile,TargetFile:string):Boolean;  

    var Target,Source:TFileStream;  

        MyFileSize:integer;  

    begin  

        try  

            Source:=TFileStream.Create(SourceFile,fmOpenRead or fmShareExclusive);  

            Target:=TFileStream.Create(TargetFile,fmOpenWrite or fmShareExclusive);  

            Try  

                Target.Seek(0,soFromEnd);//往尾部添加资源  

                Target.CopyFrom(Source,0);  

                MyFileSize:=Source.Size+Sizeof(MyFileSize);  

                //计算资源大小,并写入辅程尾部  

                Target.WriteBuffer(MyFileSize,sizeof(MyFileSize));  

            finally  

                Target.Free;  

                Source.Free;  

            end;  

        except  

            Result:=False;  

            Exit;  

        end;  

        Result:=True;  

     end;  

    有了上面的基础,我们应该很容易看得懂这个函数。其中参数 SourceFile 是要添加的文件,参 
数 TargetFile 是被添加到的目标文件。 比如说把 a.exe 添加到 b.exe 里面可以: 
Cjt_AddtoFile('a.exe',b.exe');如果添加成功就返回 True 否则返回假。根据上面的函数我们可 
以写出相反的读出函数:  

    Function Cjt_LoadFromFile(SourceFile,TargetFile :string):Boolean;  

    var Source:TFileStream;  

        Target:TMemoryStream;  

        MyFileSize:integer;  


     begin  

         try  

             Target:=TMemoryStream.Create;  

             Source:=TFileStream.Create(SourceFile,fmOpenRead or fmShareDenyNone);  

             try   

                  Source.Seek(sizeof(MyFileSize),soFromEnd);  

                 Source.ReadBuffer(MyFileSize,sizeof(MyFileSize));  //读出资源大小  

                 Source.Seek(MyFileSize,soFromEnd);//定位到资源位置  

                 Target.CopyFrom(Source,MyFileSize-sizeof(MyFileSize));//取出资源  

                 Target.SaveToFile(TargetFile);//存放到文件  

             finally  

                 Target.Free;  

                 Source.Free;  

             end;  

         except  

             Result:=false;  

             Exit;  

         end;  

         Result:=true;  

     end;   

     其中参数 SourceFile 是已经添加了文件的文件名称,参数 TargetFile 是取出文件后保存的目 
标文件名。比如说 Cjt_LoadFromFile('b.exe','a.txt');在 b.exe 中取出文件保存为 a.txt 。如 
果取出成功就返回 True 否则返回假。打开 Delphi ,新建一个工程,在窗口上放上一个 Edit 控件 
Edit1 和两个 Button:Button1 和 Button2 。Button 的 Caption 属性分别设置为“确定”和“取 
消”。  

     在 Button1 的 Click 事件中写代码:   

     var S:string;  

     begin  

         S:=ChangeFileExt(Application.ExeName,'.Cjt');  

         if Edit1.Text='790617' then  

             begin  

                 Cjt_LoadFromFile(Application.ExeName,S);  

                  {取出文件保存在当前路径下并命名"原文件.Cjt"}  

                 Winexec(pchar(S),SW_Show);{ 运行"原文件.Cjt"}  

                 Application.Terminate;{ 退出程序}  



            end  

        else  

            Application.MessageBox(' 密码不对,请重新输入!',  

                            ' 密码错误',MB_ICONERROR+MB_OK);  

     end;  

     编译这个程序,并把 EXE 文件改名为 head.exe 。新建一个文本文件 head.rc, 内容为:head  
exefile head.exe, 然后把它们拷贝到 Delphi 的 BIN 目录下,执行 Dos 命令 Brcc32.exe head.rc,  
将产生一个 head.res 的文件,这个文件就是我们要的资源文件,先留着。  

     我们的头文件已经建立了,下面我们来建立添加程序。  

     新建一个工程,放上以下控件:一个 Edit, 一个 Opendialog, 两个 Button1 的 Caption 属性 
分别设置为"选择文件"和"加密"。在源程序中添加一句:{$R head.res} 并把 head.res 文件拷贝到 
程序当前目录下。这样一来就把刚才的 head.exe 跟程序一起编译了。  

     在   Button1  的     Cilck  事 件 里 面 写 下 代 码 : if  OpenDialog1.Execute  then  
Edit1.Text:=OpenDialog1.FileName;   

     在 Button2 的 Cilck 事件里面写下代码:  

    Function ExtractRes(ResType, ResName, ResNewName : String):boolean;  

    var  

        Res : TResourceStream;  

    begin  

        try  

            Res :=TResourceStream.Create(Hinstance, Resname, Pchar(ResType));  

            try  

                Res.SavetoFile(ResNewName);  

                Result:=true;  

            finally  

                Res.Free;  

            end;  

        except  

            Result:=false;  

        end;  

     end;  

    //其中 ExtractRes 为自定义函数,它的作用是把 head.exe 从资源文件中取出来。  

    procedure TForm1.Button2Click(Sender: TObject);  

    var  

        S:String;  

    begin  


        S:=ExtractFilePath(Edit1.Text);  

        if ExtractRes('exefile','head',S+'head.exe')then  

            if Cjt_AddtoFile(Edit1.Text,S+'head.exe')then  

                if DeleteFile(Edit1.Text)then  

                    if RenameFile(S+'head.exe',Edit1.Text)then  

                        Application.MessageBox(' 文件加密成功!'  

                                ,' 信息',MB_ICONINFORMATION+MB_OK)  

                    else  

                    begin  

                        if FileExists(S+'head.exe')then  

                            DeleteFile(S+'head.exe');  

                        Application.MessageBox(' 文件加密失败!'  

                                ,' 信息',MB_ICONINFORMATION+MB_OK);  

                    end;  

    end;  

    注意:我们上面的函数只不过是简单的把一个文件添加到另一个文件的尾部。实际应用中可以 
改成可以添加多个文件,只要根据实际大小和个数定义好偏移地址就可以了。比如说文件捆绑机就 
是把两个或者多个程序添加到一个头文件里面。那些自解压程序和安装程序的原理也是一样的,不 
过多了压缩而已。比如说我们可以引用一个 LAH 单元,把流压缩后再添加,这样文件就会变的很小。 
读出来时先解压就可以了。另外,文中 EXE 加密器的例子还有很多不完善的地方,比如说密码固定 
为"790617" ,取出 EXE 运行后应该等它运行完毕后删除等等,读者可以自行修改。  

    三、实际应用之二:利用流制作可执行电子贺卡  

    我们经常看到一些电子贺卡之类的制作软件,可以让你自己选择图片,然后它会生成一个 EXE 可 
执行文件给你。打开贺卡时就会一边放音乐一边显示出图片来。现在学了流操作之后, 我们也可以 
做一个了。  

    添加图片过程我们可以直接用前面的 Cjt_AddtoFile ,而现在要做的是如何把图像读出并显示。 
我们用前面的 Cjt_LoadFromFile 先把图片读出来保存为文件再调入也是可以的,但是还有更简单 
的方法,就是直接把文件流读出来显示,有了流这个利器,一切都变的简单了。  

    现在的图片比较流行的是 BMP 格式和 JPG 格式。我们现在就针对这两种图片写出读取并显示函 
数。  

    Function Cjt_BmpLoad(ImgBmp:TImage;SourceFile:String):Boolean;  

    var  

        Source:TFileStream;  

        MyFileSize:integer;  

    begin  

        Source:=TFileStream.Create(SourceFile,fmOpenRead or fmShareDenyNone);  

        try  



             try  

                 Source.Seek(sizeof(MyFileSize),soFromEnd);  

                 Source.ReadBuffer(MyFileSize,sizeof(MyFileSize));  

                 //读出资源  

                 Source.Seek(MyFileSize,soFromEnd);  

                 //定位到资源开始位置  

                 ImgBmp.Picture.Bitmap.LoadFromStream(Source);  

             finally  

                 Source.Free;  

             end;  

         except  

             Result:=False;  

             Exit;  

         end;  

         Result:=True;  

     end;  

     上面是读出 BMP 图片的,下面的是读出 JPG 图片的函数,因为要用到 JPG 单元,所以要在程 
序中添加一句:usesjpeg 。  

     Function Cjt_JpgLoad(JpgImg:Timage;SourceFile:String):Boolean;  

     var  

         Source:TFileStream;  

         MyFileSize:integer;  

         Myjpg: TJpegImage;  

     begin  

         try  

             Myjpg:=TJpegImage.Create;  

             Source:=TFileStream.Create(SourceFile,fmOpenRead or fmShareDenyNone);  

             try  

                 Source.Seek(sizeof(MyFileSize),soFromEnd);  

                 Source.ReadBuffer(MyFileSize,sizeof(MyFileSize));  

                 Source.Seek(MyFileSize,soFromEnd);  

                 Myjpg.LoadFromStream(Source);  

                 JpgImg.Picture.Bitmap.Assign(Myjpg);  

             finally  

                 Source.Free;  



                 Myjpg.free;  

             end;  

         except  

             Result:=false;  

             Exit;  

         end;  

         Result:=true;  

     end;  

     有了这两个函数,我们就可以制作读出程序了。下面我们以 BMP 图片为例:运行 Delphi ,新 
建一个工程,放上一个显示图像控件 Image1 。在窗口的 Create 事件中写上一句就可以了:  

     Cjt_BmpLoad(Image1,Application.ExeName); 这个就是头文件了,然后我们用前面的方法生成 
一个 head.res 资源文件。下面就可以开始制作我们的添加程序了。全部代码如下:  

     unit Unit1;  
  

     interface  
  

     uses  
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,  
ExtCtrls, StdCtrls, ExtDlgs;  
  

     type  
TForm1=class(TForm)  
Edit1: TEdit;  
Button1: TButton;  
Button2: TButton;  
OpenPictureDialog1: TOpenPictureDialog;  
procedure FormCreate(Sender: TObject);  
procedure Button1Click(Sender: TObject);  
procedure Button2Click(Sender: TObject);  
private  
Function ExtractRes(ResType, ResName, ResNewName : String):boolean;  
Function Cjt_AddtoFile(SourceFile,TargetFile:string):Boolean;  
{ Private declarations }  
public  
{ Public declarations }  
end;  
  

     var  
Form1: TForm1;  
  

     implementation  


 {$R *.DFM}  
  

Function TForm1.ExtractRes(ResType, ResName, ResNewName : String):boolean;  

var  

    Res : TResourceStream;  

begin  

    try  

        Res :=TResourceStream.Create(Hinstance, Resname, Pchar(ResType));  

        try  

            Res.SavetoFile(ResNewName);  

            Result:=true;  

        finally  

            Res.Free;  

        end;  

    except  

        Result:=false;  

    end;  

end;  

Function TForm1.Cjt_AddtoFile(SourceFile,TargetFile:string):Boolean;  

var  

    Target,Source:TFileStream;  

    MyFileSize:integer;  

begin  

    try  

        Source:=TFileStream.Create(SourceFile,fmOpenRead or fmShareExclusive);  

        Target:=TFileStream.Create(TargetFile,fmOpenWrite or fmShareExclusive);  

        try  

            Target.Seek(0,soFromEnd);//往尾部添加资源  

            Target.CopyFrom(Source,0);  

            MyFileSize:=Source.Size+Sizeof(MyFileSize);  

                                              //计算资源大小,并写入辅程尾部  

            Target.WriteBuffer(MyFileSize,sizeof(MyFileSize));  

        finally  

            Target.Free;  

            Source.Free;  

        end;  

    except  

        Result:=False;  

        Exit;  

    end;  

    Result:=True;  

end;  

  

procedure TForm1.FormCreate(Sender: TObject);  

begin  

    Caption:='Bmp2Exe 演示程序.作者:陈经韬';  

    Edit1.Text:='';  

    OpenPictureDialog1.DefaultExt :=GraphicExtension(TBitmap);  

    OpenPictureDialog  

    Button1.Caption:=' 选择 BMP 图片';  

    Button2.Caption:=' 生成 EXE';  

end;  

  

procedure TForm1.Button1Click(Sender: TObject);  

begin  

    if OpenPictureDialog1.Execute then  

        Edit1.Text:=OpenPictureDialog1.FileName;  

end;  

  

procedure TForm1.Button2Click(Sender: TObject);  

var  

    HeadTemp:String;  

begin  

    if Not FileExists(Edit1.Text)then  

    begin  

        Application.MessageBox('BMP 图片文件不存在,请重新选择!',  

                    ' 信息',MB_ICONINFORMATION+MB_OK)  

        Exit;  

    end;  


        HeadTemp:=ChangeFileExt(Edit1.Text,'.exe');  

        if ExtractRes('exefile','head',HeadTemp)then  

            if Cjt_AddtoFile(Edit1.Text,HeadTemp)then  

                Application.MessageBox('EXE 文件生成成功!',  

                           ' 信息',MB_ICONINFORMATION+MB_OK)  

            else  

                begin  

                    if FileExists(HeadTemp)then  

                        DeleteFile(HeadTemp);  

                    Application.MessageBox('EXE 文件生成失败!',  

                           ' 信息',MB_ICONINFORMATION+MB_OK)  

                end;  

            end;  

    end.  

    怎么样?很神奇吧:)把程序界面弄的漂亮点,再添加一些功能,你会发现比起那些要注册的软 
件来也不会逊多少吧。  

    实际应用之三:利用流制作自己的 OICQ   

    OICQ 是深圳腾讯公司的一个网络实时通讯软件,在国内拥有大量的用户群。但 OICQ 必须连接 
上互联网登陆到腾讯的服务器才能使用。所以我们可以自己写一个在局部网里面使用。  

    OICQ 使用的是 UDP 协议,这是一种无连接协议,即通信双方不用建立连接就可以发送信息, 
所以效率比较高。Delphi 本身自带的 FastNEt 公司的 NMUDP 控件就是一个 UDP 协议的用户数据报 
控件。不过要注意的是如果你使用了这个控件必须退出程序才能关闭计算机,因为 TNMXXX 控件有 
BUG 。所有 nm 控件的基础 PowerSocket 用到的 ThreadTimer ,用到一个隐藏的窗口(类为 
TmrWindowClass )处理有硬伤。出问题的地方:  

    Psock::TThreadTimer::WndProc(var msg:TMessage)  

    if msg.message=WM_TIMER then   

    他自己处理  

    msg.result:=0 else msg.result:=DefWindowProc(0,....) end   

    问题就出在调用 DefWindowProc 时,传输的 HWND 参数居然是常数 0,这样实际上 DefWindowProc  
是不能工作的,对任何输入的消息的调用均返回 0,包括 WM_QUERYENDSESSION ,所以不能退出 
windows 。由于 DefWindowProc 的不正常调用,实际上除 WM_TIMER ,其他消息由 DefWindowProc 处 
理都是无效的。解决的办法是在PSock.pas 在TThreadTimer.Wndproc 内Result :=DefWindowProc(0,  
Msg, WPARAM, LPARAM); 改为:  

    Result :=DefWindowProc(FWindowHandle, Msg, WPARAM, LPARAM); 早期低版本的 OICQ 也有 
这个问题,如果不关闭 OICQ 的话,关闭计算机时屏幕闪了一下又  

    返回了。好了,废话少说,让我们编写我们的 OICQ 吧,这个实际上是 Delphi 自带的例子而已:)  
新建一个工程,在 FASTNET 面版拖一个 NMUDP 控件到窗口,然后依次放上三个 EDIT, 名字  

    分别为 EditIP 、EditPort 、EditMyTxt ,三个按钮 BtSend 、BtClear 、BtSave ,一个 



MEMOMemoReceive ,一个 SaveDialog 和一个状态条 StatusBar1 。当用户点击 BtSend 时,建立一 
个内存流对象,把要发送的文字信息写进内存流,然后 NMUDP 把流发送出去。当 NMUDP 有数据接收 
时,触发它的 DataReceived 事件,我们在这里再把接收到的流转换为字符信息,然后显示出来。  

    注意:所有的流对象建立后使用完毕后要记得释放(Free),其实它的释构函数应该为 Destroy , 
但如果建立流失败的话,用 Destroy 会产生异常,而用 Free 的话程序会先检查有没有成功建立了 
流,如果建立了才释放,所以用 Free 比较安全。  

    在这个程序中我们用到了 NMUDP 控件,它有几个重要的属性。RemoteHost 表示远程电脑的 IP 或 
者计算机名,LocalPort 是本地端口,主要监听有没有数据传入。而 RemotePort 是远程端口,发 
送数据时通过这个端口把数据发送出去。理解这些已经可以看懂我们的程序了。  

    全部代码如下:  

    unit Unit1;  
  

     interface  
  

    uses  
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,StdCtrls,  
ComCtrls,NMUDP;  
  

     type  
TForm1=class(TForm)  
NMUDP1: TNMUDP;  
EditIP: TEdit;  
EditPort: TEdit;  
EditMyTxt: TEdit;  
MemoReceive: TMemo;  
BtSend: TButton;  
BtClear: TButton;  
BtSave: TButton;  
StatusBar1: TStatusBar;  
SaveDialog1: TSaveDialog;  
procedure BtSendClick(Sender: TObject);  
procedure NMUDP1DataReceived(Sender: TComponent; NumberBytes: Integer;  
FromIP: String; Port: Integer);  
procedure NMUDP1InvalidHost(var handled: Boolean);  
procedure NMUDP1DataSend(Sender: TObject);  
procedure FormCreate(Sender: TObject);  
procedure BtClearClick(Sender: TObject);  
procedure BtSaveClick(Sender: TObject);  
procedure EditMyTxtKeyPress(Sender: TObject; var Key: Char);  
private  
{ Private declarations }  

public  

     { Public declarations } end;   

    var Form1: TForm1;   


     implementation   

     {$R *.DFM}   

     procedure TForm1.BtSendClick(Sender: TObject)  
     var  
         MyStream: TMemoryStream;  
         MySendTxt: String;  
         Iport,icode:integer;  
     Begin  
         Val(EditPort.Text,Iport,icode);  
         if icode<>0 then  
         begin  
             Application.MessageBox(' 端口必须为数字,请重新输入!',  
                                               ' 信息',MB_ICONINFORMATION+MB_OK);  
             Exit;  
         end;  
         NMUDP1.RemoteHost :=EditIP.Text; { 远程主机}  
         NMUDP1.LocalPort:=Iport; { 本地端口}  
         NMUDP1.RemotePort :=Iport; { 远程端口}  
         MySendTxt :=EditMyTxt.Text;  
         MyStream :=TMemoryStream.Create; { 建立流}  
         try  
             MyStream.Write(MySendTxt[1], Length(EditMyTxt.Text)); { 写数据}  
             NMUDP1.SendStream(MyStream); { 发送流}  
         finally  
             MyStream.Free; { 释放流}  
         end;  
     end;  
       
     procedure TForm1.NMUDP1DataReceived(Sender: TComponent; NumberBytes: Integer; FromIP:  
String; Port: Integer);  
     var  
         MyStream: TMemoryStream;  
         MyReciveTxt: String;  
     begin  
         MyStream :=TMemoryStream.Create; { 建立流}  
         try  
             NMUDP1.ReadStream(MyStream);{ 接收流}  
             SetLength(MyReciveTxt,NumberBytes);{NumberBytes 为接收到的字节数}  
             MyStream.Read(MyReciveTxt[1],NumberBytes);{ 读数据}  
             MemoReceive.Lines.Add(' 接收到来自主机'+FromIP+' 的信息:'+MyReciveTxt);  
         finally  
             MyStream.Free; { 释放流}  
         end;  
     end;  
       
     procedure TForm1.NMUDP1InvalidHost(var handled: Boolean);  
     begin  
         Application.MessageBox(' 对方 IP 地址不正确,请重新输入!',  



                                              ' 信息',MB_ICONINFORMATION+MB_OK);  
     end;  
      
    procedure TForm1.NMUDP1DataSend(Sender: TObject);  
    begin  
        StatusBar1.SimpleText:=' 信息成功发出!';  
     end;  
      
    procedure TForm1.FormCreate(Sender: TObject);  
    begin  
        EditIP.Text:='127.0.0.1';  
        EditPort.Text:='8868';  
        BtSend.Caption:=' 发送';  
        BtClear.Caption:=' 清除聊天记录';  
        BtSave.Caption:=' 保存聊天记录';  
        MemoReceive.ScrollBars:=ssBoth;  
        MemoReceive.Clear;  
        EditMyTxt.Text:=' 在这里输入信息,然后点击发送.';  
        StatusBar1.SimplePanel:=true;  
     end;  
      
    procedure TForm1.BtClearClick(Sender: TObject);  
    begin  
        MemoReceive.Clear;  
     end;  
      
    procedure TForm1.BtSaveClick(Sender: TObject);  
    begin  
        if SaveDialog1.Execute  then  
            MemoReceive.Lines.SaveToFile(SaveDialog1.FileName);  
     end;  
      
    procedure TForm1.EditMyTxtKeyPress(Sender: TObject; var Key: Char);  
    begin  
        if Key=#13 then  
            BtSend.Click;  
     end;   
    上面的程序跟 OICQ 相比当然差之甚远,因为 OICQ 利用的是 Socket5 通信方式。它上线时先 
从服务器取回好友信息和在线状态,发送超时还会将信息先保存在服务器,等对方下次上线后再发 
送然后把服务器的备份删除。你可以根据前面学的概念来完善这个程序,比如说再添加一个 NMUDP 控 
件来管理在线状态,发送的信息先转换成 ASCII 码进行与或运行并加上一个头信息,接收方接收信 
息后先判断信息头正确与否,如果正确才把信息解密显示出来,这样就提高了安全保密性。  

     另外,UDP 协议还有一个很大的好处就是可以广播,就是说处于一个网段的都可以接收到信息 
而不必指定具体的 IP 地址。网段一般分 A、B、C 三类,   

     1~126.XXX.XXX.XXX(A 类网):广播地址为 XXX.255.255.255   

     128~191.XXX.XXX.XXX(B 类网):广播地址为 XXX.XXX.255.255   

     192~254.XXX.XXX.XXX(C 类网):广播地址为 XXX.XXX.XXX.255   


     比如说三台计算机 192.168.0.1、192.168.0.10、192.168.0.18,发送信息时只要指定 IP 地址 
为 192.168.0.255 就可以实现广播了。下面给出一个转换 IP 为广播 IP 的函数,快拿去完善自己 
的 OICQ 吧^^.   

    Function Trun_ip(S:string):string;  

    var  

        s1,s2,s3,ss,sss,Head:string;  

        n,m:integer;  

    begin  

        sss:=S;  

        n:=pos('.',s);  

        s1:=copy(s,1,n);  

        m:=length(s1);  

        delete(s,1,m);  

        Head:=copy(s1,1,(length(s1)1));  

        n:=pos('.',s);  

        s2:=copy(s,1,n);  

        m:=length(s2);  

        delete(s,1,m);  

        n:=pos('.',s);  

        s3:=copy(s,1,n);  

        m:=length(s3);  

        delete(s,1,m);  

        ss:=sss;  

        if strtoint(Head) in[1..126] then  

            ss:=s1+'255.255.255';//1~126.255.255.255(A 类网)  

        if strtoint(Head) in[128..191] then  

            ss:=s1+s2+'255.255';//128~191.XXX.255.255(B 类网)  

        if strtoint(Head) in[192..254] then  

            ss:=s1+s2+s3+'255';//192~254.XXX.XXX.255(C 类网)  

        Result:=ss;  

     end;  

      

     五、实际应用之四:利用流实现网络传输屏幕图像  

     大家应该见过很多网管程序,这类程序其中有一个功能就是监控远程电脑的屏幕。实际上,这 
也是利用流操作来实现的。下面我们给出一个例子,这个例子分两个程序,一个服务端,一个是客 


户端。程序编译后可以直接在单机、局部网或者互联网上使用。程序中已经给出相应注释。后面我 
们再来作具体分析。  

    新建一个工程,在 Internet 面版上拖一个 ServerSocket 控件到窗口,该控件主要用于监听客 
户端,用来与客户端建立连接和通讯。设置好监听端口后调用方法 Open 或者 Active:=True 即开始 
工作。注意:跟前面的 NMUDP 不同,当 Socket 开始监听后就不能再改变它的端口,要改变的话必 
须先调用 Close 或设置 Active 为 False ,否则将会产生异常。另外,如果该端口已经打开的话, 
就不能再用这个端口了。所以程序运行尚未退出就不能再运行这个程序,否则也会产生异常,即弹 
出出错窗口。实际应用中可以通过判断程序是否已经运行,如果已经运行就退出的方法来避免出错。  

    当客户端有数据传入,将触发 ServerSocket1ClientRead 事件,我们可以在这里对接收的数据 
进行处理。在本程序中,主要是接收客户端发送过来的字符信息并根据事先的约定来进行相应操作。 
程序全部代码如下:  

    unit Unit1;{ 服务端程序}   

    interface   

    uses  Windows,  Messages,  SysUtils,  Classes,  Graphics,  Controls,  Forms,  Dialogs,  
JPEG,ExtCtrls, ScktComp;   

    type   

    TForm1=class(TForm)   

      ServerSocket1: TServerSocket;   

      procedure ServerSocket1ClientRead(Sender: TObject;Socket: TCustomWinSocket);  

      procedure FormCreate(Sender: TObject);   

      procedure FormClose(Sender: TObject; var Action: TCloseAction);   

    private   

    procedure Cjt_GetScreen(var Mybmp: TBitmap; DrawCur: Boolean);  

     {自定义抓屏函数,DrawCur 表示抓鼠标图像与否}   

     { Private declarations }  

     public { Public declarations }   

    end;   

      

    var Form1: TForm1;   

       MyStream: TMemorystream;{ 内存流对象}   

       implementation {$R *.DFM}   

    procedure TForm1.Cjt_GetScreen(var Mybmp: TBitmap; DrawCur: Boolean);  

    var  

        Cursorx, Cursory: integer;  

        dc: hdc;  

        Mycan: Tcanvas;  

        R: TRect;  



    DrawPos: TPoint;  

    MyCursor: TIcon;  

    hld: hwnd;  

    Threadld: dword;  

    mp: tpoint;  

    pIconInfo: TIconInfo;  

begin  

    Mybmp :=Tbitmap.Create; { 建立 BMPMAP }  

    Mycan :=TCanvas.Create; { 屏幕截取}  

    dc :=GetWindowDC(0);  

    try  

        Mycan.Handle :=dc;  

        R :=Rect(0, 0, screen.Width, screen.Height);  

        Mybmp.Width :=R.Right;  

        Mybmp.Height :=R.Bottom;  

        Mybmp.Canvas.CopyRect(R, Mycan, R);  

    finally  

        releaseDC(0, DC);  

    end;  

    Mycan.Handle :=0; Mycan.Free;  

    if DrawCur then { 画上鼠标图象}  

    begin  

        GetCursorPos(DrawPos);  

        MyCursor :=TIcon.Create; getcursorpos(mp);  

        hld :=WindowFromPoint(mp);  

        Threadld :=GetWindowThreadProcessId(hld, nil);  

        AttachThreadInput(GetCurrentThreadId, Threadld, True);  

        MyCursor.Handle :=Getcursor();  

        AttachThreadInput(GetCurrentThreadId, threadld, False);  

        GetIconInfo(Mycursor.Handle, pIconInfo);  

        cursorx :=DrawPos.xround(pIconInfo.xHotspot);  

        cursory :=DrawPos.yround(pIconInfo.yHotspot);  

        Mybmp.Canvas.Draw(cursorx, cursory, MyCursor); { 画上鼠标}  

        DeleteObject(pIconInfo.hbmColor);  


            {GetIconInfo 使用时创建了两个 bitmap 对象. 需要手工释放这两个对象}  

        DeleteObject(pIconInfo.hbmMask);  

            { 否则,调用他后,他会创建一个 bitmap, 多次调用会产生多个,直至资源耗尽}  

        Mycursor.ReleaseHandle; { 释放数组内存}  

        MyCursor.Free; { 释放鼠标指针}  

    end;  

end;  

procedure TForm1.FormCreate(Sender: TObject);  

begin  

    ServerSocket1.Port :=3000; { 端口}  

    ServerSocket1.Open; {Socket 开始侦听}  

end;  

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);  

begin  

    if ServerSocket1.Active then  

        ServerSocket1.Close; { 关闭 Socket}  

end;  

procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket);  

var  

    S, S1: string;  

    MyBmp: TBitmap;  

    Myjpg: TJpegimage;  

begin  

    S :=Socket.ReceiveText;  

    if S='cap' then { 客户端发出抓屏幕指令}  

    begin  

        try  

            MyStream :=TMemorystream.Create;{ 建立内存流}  

            MyBmp :=TBitmap.Create;  

            Myjpg :=TJpegimage.Create;  

            Cjt_GetScreen(MyBmp, True); {True 表示抓鼠标图像}  

            Myjpg.Assign(MyBmp); { 将 BMP 图象转成 JPG 格式,便于在互联网上传输}  

            Myjpg.CompressionQuality :=10;  

                    {JPG 文件压缩百分比设置,数字越大图像越清晰,但数据也越大}  


                Myjpg.SaveToStream(MyStream); { 将 JPG 图象写入流中}  

                Myjpg.free; MyStream.Position :=0;{ 注意:必须添加此句}  

                s1 :=inttostr(MyStream.size);{ 流的大小}  

                Socket.sendtext(s1); { 发送流大小}  

            finally  

                MyBmp.free;  

            end;  

        end;  

        if s='ready' then { 客户端已准备好接收图象}  

        begin  

            MyStream.Position :=0;  

            Socket.SendStream(MyStream); { 将流发送出去}  

        end;  

     end;  

     end.   

     上面是服务端,下面我们来写客户端程序。新建一个工程,添加 Socket 控件 ClientSocket 、 
图像显示控件 Image 、一个 Panel 、一个 Edit 、两个 Button 和一个状态栏控件 StatusBar1 。 
注意:把 Edit1 和两个 Button 放在 Panel1 上面。ClientSocket 的属性跟 ServerSocket 差不多, 
不过多了一个 Address 属性,表示要连接的服务端 IP 地址。填上 IP 地址后点“连接”将与服务 
端程序建立连接,如果成功就可以进行通讯了。点击“抓屏”将发送字符给服务端。因为程序用到 
了 JPEG 图像单元,所以要在 Uses 中添加 Jpeg. 全部代码如下:unit Unit2{ 客户端};   

interface   

uses Windows,Messages,SysUtils,Classes,Graphics,Controls,Forms,  

    Dialogs,StdCtrls,ScktComp,ExtCtrls,J peg, ComCtrls;   

type   

TForm1=class(TForm)   

     ClientSocket1: TClientSocket;   

     Image1: TImage;   

     StatusBar1: TStatusBar;   

    Panel1: TPanel;  

    Edit1: TEdit;   

    Button1: TButton;   

    Button2: TButton;   

    procedure Button1Click(Sender: TObject);   

    procedure ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket);  

    procedure Button2Click(Sender: TObject);   



     procedure ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent:  
TErrorEvent; var ErrorCode: Integer);   

     procedure ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket); procedure  
FormCreate(Sender: TObject);   

     procedure FormClose(Sender: TObject; var Action: TCloseAction);   

     procedure  ClientSocket1Disconnect(Sender:  TObject;  Socket:  TCustomWinSocket);  
private   

     { Private declarations }   

public { Public declarations }   

end;   

var Form1: TForm1;   

MySize: Longint;   

MyStream: TMemorystream;{ 内存流对象}   

implementation {$R *.DFM}   

     procedure TForm1.FormCreate(Sender: TObject);  

     begin  

         {下面为设置窗口控件的外观属性}  

         {注意:把 Button1 、Button2 和 Edit1 放在 Panel1 上面}  

         Edit1.Text :='127.0.0.1';  

         Button1.Caption :='连接主机';  

         Button2.Caption :='抓屏幕';  

         Button2.Enabled :=false;  

         Panel1.Align :=alTop;  

         Image1.Align :=alClient;  

         Image1.Stretch :=True;  

         StatusBar1.Align:=alBottom;  

         StatusBar1.SimplePanel :=True; {}  

         MyStream :=TMemorystream.Create; { 建立内存流对象}  

         MySize :=0; { 初始化}  

     end;  

       

     procedure TForm1.Button1Click(Sender: TObject);  

     begin  

         if not ClientSocket1.Active then  

         begin  


        ClientSocket1.Address :=Edit1.Text; { 远程 IP 地址}  

        ClientSocket1.Port :=3000; {Socket 端口}  

        ClientSocket1.Open; { 建立连接}  

    end;  

end;  

  

procedure TForm1.Button2Click(Sender: TObject);  

begin  

    Clientsocket1.Socket.SendText('cap');  

    { 发送指令通知服务端抓取屏幕图象}  

    Button2.Enabled :=False;  

end;  

  

procedure TForm1.ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket);  

begin  

    StatusBar1.SimpleText :='与主机'+ ClientSocket1.Address +'成功建立连接!';  

    Button2.Enabled :=True;  

end;  

  

procedure TForm1.ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket;  

    ErrorEvent: TErrorEvent; var ErrorCode: Integer);  

begin  

    Errorcode :=0; { 不弹出出错窗口}  

    StatusBar1.SimpleText :='无法与主机'+ ClientSocket1.Address +'建立连接!';  

end;  

  

procedure TForm1.ClientSocket1Disconnect(Sender: TObject; Socket: TCustomWinSocket);  

begin  

    StatusBar1.SimpleText :='与主机'+ ClientSocket1.Address +'断开连接!';  

    Button2.Enabled :=False;  

end;  

  

procedure TForm1.ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket);  

var  



    MyBuffer: array[0..10000] of byte; { 设置接收缓冲区}  

    MyReceviceLength: integer;  

    S: string;  

    MyBmp: TBitmap;  

    MyJpg: TJpegimage;  

begin  

    StatusBar1.SimpleText :='正在接收数据......';  

           {MySize 为服务端发送的字节数,如果为 0 表示为尚未开始图象接收}   

     if MySize=0 then  

    begin  

        S :=Socket.ReceiveText;  

        MySize :=Strtoint(S); { 设置需接收的字节数}  

        Clientsocket1.Socket.SendText('ready'); { 发指令通知服务端开始发送图象}  

    end else begin { 以下为图象数据接收部分}  

        MyReceviceLength :=socket.ReceiveLength; { 读出包长度}  

        StatusBar1.SimpleText :='正在接收数据,数据大小为:'+ inttostr(MySize);  

        Socket.ReceiveBuf(MyBuffer, MyReceviceLength);{ 接收数据包并读入缓冲区内}  

        MyStream.Write(MyBuffer, MyReceviceLength); { 将数据写入流中}  

        if MyStream.Size >=MySize then { 如果流长度大于需接收的字节数,则接收完毕}  

        begin  

            MyStream.Position :=0;  

            MyBmp :=tbitmap.Create;  

            MyJpg :=tjpegimage.Create;  

            try  

                MyJpg.LoadFromStream(MyStream); { 将流中的数据读至 JPG 图像对象中}  

                MyBmp.Assign(MyJpg); { 将 JPG 转为 BMP}  

                StatusBar1.SimpleText :='正在显示图像';  

                Image1.Picture.Bitmap.Assign(MyBmp); { 分配给 image1 元件}  

            finally { 以下为清除工作}  

                MyBmp.free;  

                MyJpg.free;  

                Button2.Enabled :=true;   

                                  { Socket.SendText('cap');添加此句即可连续抓屏}  

                MyStream.Clear; MySize :=0;  


                end;  

            end;  

        end;  

    end;  

      

    procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);  

    begin  

        MyStream.Free; { 释放内存流对象}  

        if ClientSocket1.Active then  

            ClientSocket1.Close; { 关闭 Socket 连接}  

    end;  

    程序原理:运行服务端开始侦听,再运行客户端,输入服务端 IP 地址建立连接,然后发一个 
字符通知服务端抓屏幕。服务端调用自定义函数 Cjt_GetScreen 抓取屏幕存为 BMP ,把 BMP 转换 
成 JPG ,把 JPG 写入内存流中,然后把流发送给客户端。客户端接收到流后做相反操作,将流转换 
为 JPG 再转换为 BMP 然后显示出来。  

    注意:因为 Socket 的限制,不能一次发送过大的数据,只能分几次发。所以程序中服务端抓 
屏转换为流后先发送流的大小,通知客户端这个流共有多大,客户端根据这个数字大小来判断是否 
已经接收完流,如果接收完才转换并显示。  

    这个程序跟前面的自制 OICQ 都是利用了内存流对象 TMemoryStream 。其实,这个流对象是程 
序设计中用得最普遍的,它可以提高 I/O 的读写能力,而且如果你要同时操作几个不同类型的流, 
互相交换数据的话,用它作“中间人”是最好不过的了。比如说你把一个流压缩或者解压缩,就先 
建立一个 TMemoryStream 对象,然后把别的数据拷贝进去,再执行相应操作就可以了。因为它是直 
接在内存中工作,所以效率是非常高的。有时侯甚至你感觉不到有任何的延迟。  

    程序有待改进的地方:  

    当然可以加一个压缩单元,发送前先压缩再发送。注意:这里也是有技巧的,就是直接把 BMP 压 
缩而不要转换成 JPG 再压。实验证明:上面程序一幅图像大小大概为 4050KB ,如果用 LAH 压缩算 
法处理一下便只有 812KB ,这样传输起来就比较快。如果想更快的话,可以采用这样的方法:先抓 
第一幅图像发送,然后从第二幅开始只发跟前一幅不同区域的图像。外国有一个程序叫 Remote  
Administrator ,就是采用这样的方法。他们测试的数据如下:局部网一秒钟 100500 幅,互联网 
上,在网速极低的情况下,一秒钟传输 510 幅。说这些题外话只想说明一个道理:想问题,特别是 
写程序,特别是看起来很复杂的程序,千万不要钻牛角尖,有时侯不妨换个角度来想。程序是死的, 
人才是活的。当然,这些只能靠经验的积累。但是一开始就养成好习惯是终身受益的!  

Delphi_    流操作的语法 

    Delphi 在这两方面都做的相当出色。在 Delphi 的早期版本 Turbo Pascal 中就曾有流(Stream)、 
群(Collection) 和资源(Resource)等专门用于对象式数据管理的类。在 Delphi 中,这些功能得到 
了大大的加强。Delphi 将对象式数据管理类归结为 Stream 对象(Stream)和 Filer 对象(Filer), 并 
将它们应用于可视部件类库(VCL) 的方方面面。它们不仅提供了在内存、外存和 Windows 资源中 
管理对象的功能,还提供了在数据库 BLOB 字段中对象的功能。  



    在本章中将介绍 Stream 对象和 Filer 对象的实现原理、应用方法以及在超媒体系统中的应用。 
这对于运用 Delphi 开发高级应用是很重要的。  

    20.1 流式对象的实现原理和应用  

    Stream 对象, 又称流式对象, 是 TStream 、THandleStream 、TFileStream 、TMemoryStream 、 
TResourceStream 和 TBlobStream 等的统称。它们分别代表了在各种媒介上存储数据的能力, 它 
们将各种数据类型(包括对象和部件) 在内存、外存和数据库字段中的管理操作抽象为对象方法,并 
且充分利用了面向对象技术的优点,应用程序可以相当容易地在各种 Stream 对象中拷贝数据。  

    下面介绍各种对象的数据和方法及使用方法。  

    20.1.1 TStream 对象  

    TStream 对象是能在各种媒介中存储二进制数据的对象的抽象对象。从 TStream 对象继承的对 
象用于在内存、Windows 资源文件、磁盘文件和数据库字段等媒介中存储数据。  

    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 方法从流的当前位置读取 Windows 资源文件头,并将流的当前位置指针移到该 
文件头的尾部。如果流不包含一个有效的资源文件头, 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 方法首先往流中写入标准 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 写入流。  

    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: Int64): Int64;  

     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;  

     ReadResHeader 在资源文件中的读取部件时调用, 通常程序员不需自己调用。如果读取的不是 
资源文件,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) + 10ReadCount, 1)   

        else   

               raise EInvalidImage.CreateRes(SInvalidImage);  

     end;  

     ReadComponentRes 在 Windows 资源文件中读取部件, 为了判断是否是资源文件, 它首先调 
用 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 方法用于将部件写入 Windows 资源文件;而 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[HeaderSize6])^):=$1030;   

       Longint((@Header[HeaderSize4])^):=0;   

       WriteBuffer(Header, HeaderSize);   

       Origin :=Position;   

       WriteDescendent(Instance, Ancestor);   

       ImageSize :=PositionOrigin;   

       Position :=Origin4;   

       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 大多数属性的实现那样,先在对象定义的 private 部分声明一 
个存放数据的变量 FHandle ,然后在定义的 public 部分声明属性 Handle ,其中属性定义的读写 
控制部分加上只读控制,读控制只是直接读取 FHandle 变量的值,其实现如下:  

    THandleStream=class(TStream)   

    private   

       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  
中的相应方法。它们的实现都调用了 Windows 的文件管理函数。  

    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 、Lo 

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
Delphi实现检测线程类TThread是否结束发布时间:2022-07-18
下一篇:
TXMLDocumentusecase(Delphi)发布时间:2022-07-18
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap