什麼是流?流,簡單來說就是建立在物件導向基礎上的一種抽象的處理數據
的工具。在流程中,定義了一些處理資料的基本操作,如讀取數據,寫入資料等,
程式設計師是對流進行所有操作的,而不用關心流的另一頭資料的真正流向。流不
但可以處理文件,還可以處理動態記憶體、網路資料等多種資料形式。如果你對
流的操作非常熟練,在程序中利用流的方便性,寫起程序會大大提高效率的。
下面,筆者透過四個實例: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運行後應該等它運行完畢後刪除等等,讀者可以自行修改。