Delphi编程中“流”的应用

来源:互联网 发布:古柯淘宝 编辑:程序博客网 时间:2024/06/05 09:54

======================================================
注:本文源代码点此下载
======================================================

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

下面,笔者通过四个实例: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);

编译这个程序,并把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事件里面写下代码:

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;

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

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;

注意:我们上面的函数只不过是简单的把一个文件添加到另一个文件的尾部。实际应用中可以改成可以添加多个文件,只要根据实际大小和个数定义好偏移地址就可以了。比如说文件捆绑机就是把两个或者多个程序添加到一个头文件里面。那些自解压程序和安装程序的原理也是一样的,不过多了压缩而已。比如说我们可以引用一个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单元,所以要在程序中添加一句:uses jpeg。

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

openpicturedialog1.filter := graphicfilter(tbitmap);

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= 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;

end.

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

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

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

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


======================================================
在最后,我邀请大家参加新浪APP,就是新浪免费送大家的一个空间,支持PHP+MySql,免费二级域名,免费域名绑定 这个是我邀请的地址,您通过这个链接注册即为我的好友,并获赠云豆500个,价值5元哦!短网址是http://t.cn/SXOiLh我创建的小站每天访客已经达到2000+了,每天挂广告赚50+元哦,呵呵,饭钱不愁了,\(^o^)/