文件是同一类型元素的有序集合,是内存与外设间传输数据的渠道。一些外设如显示器、键盘、打印机等都可以看作文件,但最常用的还是磁盘文件,这也是本章我们主要讨论的对象。
Delphi继承了Object Pascal的文件管理功能,并有很大的发展,其中最主要的是提供了用于文件管理的标准控件,同时也提供了更多的文件管理函数。利用Delphi的强大功能,开发一个自己的文件管理系统就成为很容易的事。
本章首先介绍Delphi文件管理的基本概念和标准过程/函数,并提供了一个记录文件的应用实例,这是从我们实际课题开发中提取出来的。而后介绍Delphi提供的文件控件的使用方法。最后提供的一个综合例程MDI文件管理器则是对Delphi文件管理功能的综合应用。
6.1 文件类型和标准过程
Delphi同Object Pascal一样支持三种文件类型,即:文本文件、记录文件、无类型文件。
6.1.1文本文件
文本文件类型的变量用如下方法声明:
var
TextFileVar: Text ;
文本文件是以行为单位进行读、写操作的。由于每一行长度不一定相同,不能计算出给定行在文件中的确切位置,因而只能顺序地读写。而且文本文件只能单独为读或写而打开,在一个打开的文本文件上同时进行读、写操作是不允许的。
6.1.1.1 文本文件的打开、关闭
文本文件的打开需要两个步骤:(1). 文件变量与文件名关联;(2). 初始化读写。
联文件变量与文件名调用AssignFile标准过程:
AssignFile ( TextFileVar , FileName ) ;
FileName 既可以是全路径名,也可以仅是文件名。对于后者系统将在当前目录下查找。
AssignFile是Delphi新提供的一个函数,其功能等价于Object Pascal中的Assign。而Assign在Delphi中更多地被用作一个方法名。
初始化读写有三种方式:
1. Reset : 为读打开文件并把文件指针移动到文件首;
2. Rewrite : 为写创建一个新文件;
3. Append : 为写打开存在的文件并把文件指针定位在文件尾。
当使用Reset或Append过程而文件不存在时将会引发一个I/O异常。有关I/O异常的处理请参看本章例程和第十二章中的介绍。
文件的关闭很简单,只须调用CloseFile过程即可。
虽然Delphi应用程序在退出时会自动关闭所有打开的文件,但自己动手关闭文件可以确保释放文件句柄,并使程序的可移植性增强。
为保持兼容,Delphi也允许用户用Assign建立关联,Close关闭文件。
6.1.1.2 文本文件的读写
从文本文件中读取信息用Read、Readln两个标准过程。
当读入数值时,Read、Readln假定数值是用一个或多个空格分开的,而不是逗号、分号或其它字符。对如下一条语句:
Read ( TextFileVar , Num1 , Num2 , Num3 ) ;
如果文件中的数值是:
100 200 300
则能够成功读入,而若文件中的数值是
100 200, 300
则Read读入“200,”并试图把它转化成一个数值时会引发一个异常。
当读入字符是字符串时,Read、Readln过程总是读取尽可能多的字符填充到字符串变量中或一直读到行结束符为止。因此从文本文件中读取格式化的字符串数据,必须声明与其长度相匹配的字符串变量。如果要从文件中读取单词,必须先把文件中的每一行读入字符串,然后再从字符串中逐个分析出单词。或者一次只从文本文件中读入一个字符并测试每个字符后是否是单词断开处。
格式化字符串之间的分隔符应读入到一个临时变量中,而字符串与数值、数值与数值间的分隔符读入时会自动识别剔除。对如下一行数据:
Mon 12:10 40 50
定义
var
Day: string[3] ;
Time: string[5] ;
Num1, Num2: Integer ;
则须用如下的read 语句读入:
read ( TextFileVar , Day , c , Time , Num1 , Num2 ) ;
C为一个临时字符变量。
6.1.1.3 文本文件的编辑
在Delphi中实现对一个文本文件的编辑,只须让其与一个Tmemo控件建立关联即可:
Memo1.Lines.LoadFromFile ( TextFileName ) ;
这样在TMemo上所做的一切修改当调用Memo部件的SaveToFile方法后都会反映到文件中去。
6.1.2 记录文件
记录文件是一种操作更为灵活的文件类型。它允许同时为读和写打开,而且由于记录文件中每条记录的长度固定,所以可随机存取。
记录文件的类型变量可如下声明:
var
RecordFileVar: file of RecordType;
RecordType是一个自定义的记录类型。
有关记录文件的操作我们将在下一节中结合例程进行讨论。
6.1.3 无类型文件
无类型文件提供了底层的I/O通道,可用于存取可变长度记录的文件。经常用于文件的复制操作中。由于Delphi提供了更好的方法(见第四节),所以无类型文件很少使用。有兴趣的读者可参看BlockRead、BlockWrite两个联机帮助主题。
6.1.4 Delphi的文件管理标准过程
根据功能我们把标准过程划分为十一类进行介绍。
6.1.4.1 文件的打开与关闭
AssignFile :把一个外部文件名和一个文件变量相关联
Reset :打开一个存在的文件
Rewrite :创建并打开一个新文件(或覆盖原有文件)
Append :以添加方式打开一个文件(只适用于文本文件)
CloseFile : 关闭一个打开的文件
FileOpen :打开一个特定的文件并返回文件句柄
FileCreate :创建一个给定文件名的文件并返回文件句柄
FileClose :关闭一个特定句柄的文件
后边三个文件主要供系统内部使用,在文件复制的编程中也往往会用到。它们操作的对象是文件句柄而不是文件变量。
6.1.4.2 文件定位
Seek :把文件当前位置移到指定部分
FilePos : 返回文件的当前位置
Eoln : 返回行结束标志
EOF : 返回文件结束标志
FileSeek : 改变当前文件指针的位置
Seek与FileSeek的区别是:1. Seek仅用于记录文件;2. FileSeek的参数是文件句柄、偏移量、起始位置。其中起始位置有文件首、当前位置、文件尾三种选择。Seek的参数是文件变量、偏移量,偏移量是从文件首开始定位的。3. FileSeek的偏移量以字节数来计算,而Seek是根据记录号进行移动。
Seek、FilePos仅用于记录文件。但任何文件都可以看作是基于字节的记录文件。下面一段程序表示了它们的用法。
{ 该例子的设计界面为一个包含TOpenDialog部件的窗体。}
uses Dialogs;
var
f: file of Byte;
size: Longint;
S: String;
y: Integer;
begin
if OpenDialog1.Execute then
begin
AssignFile(f, OpenDialog1.FileName);
Reset(f);
size := FileSize(f);
S := 'File size in bytes: ' + IntToStr(size);
y := 10;
Canvas.TextOut(5, y, S);
y := y + Canvas.TextHeight(S) + 5;
S := 'Seeking halfway into file...';
Canvas.TextOut(5, y, S);
y := y + Canvas.TextHeight(S) + 5;
Seek(f,size div 2);
S := 'Position is now ' + IntToStr(FilePos(f));
Canvas.TextOut(5, y, S);
CloseFile(f);
end;
end.
6.1.4.3 文件删除与截断
Erase : 删除一个存在的文件
DeleteFile : 删除一个文件
Truncate :从文件当前位置将文件截断
Erase与DeleteFile的区别是:Erase以文件变量为参数,当文件不能删除时引起一个异常;DeleteFile以文件名为参数,当文件不存在或不能删除时返回False,而并不引起一个异常。
6.1.4.4 文件名操作
Rename :文件更名,以文件变量为操作对象
RenameFile :文件更名,参数为文件的原名和新名
ChangeFileExt :改变文件扩展名
ExpandFileName :返回文件全路径名
ExtractFileExt :返回文件扩展名
ExtractFileName :从全路径名中返回文件名
ExtractFilePath :返回特定文件的路径
6.1.4.5 文件属性
FileGetAttr :返回文件属性
FileSetAttr :设置文件属性
6.1.4.6 文件状态
FileSize :返回文件对象大小
IOResult :返回上一次I/O操作的状态
FileExists :检测文件是否存在
6.1.4.7 文件日期
DateTimeToFileDate :把Delphi日期格式转换为DOS日期格式
FileDateToDateTime :把DOS日期格式转换为Delphi日期格式
FileGetDate :返回文件的DOS日期时间戳
FileSetDate :设置文件的DOS日期时间戳
6.1.4.8 文件读写
Read,Readln :从文本或记录文件中读取变量
Write :将指定变量写入文本或记录文件
Writeln :将指定变量写入文本文件并写入一个行结束标志
FileRead :从一个指定文件中读取变量
FileWrite :向指定文件写入数据
FileRead和FileWrite都是以文件句柄为操作对象,主要供系统内部使用。
6.1.4.9 目录操作
MkDir :创建当前目录的子目录
ChDir :改变当前目录
GetDir :返回特定磁盘的当前目录
RmDir :删除一个空子目录
6.1.4.10 磁盘操作
DiskFree :返回磁盘自由空间
DiskSize :返回特定磁盘的大小
6.1.4.11 文件查找
FileSearch :查找目录中是否存在某一特定文件
FindFirst :在目录中查找与给定文件名(可以包含匹配符)及属性集相匹配的第一个文件
FindNext :返回符合条件的下一个文件
FindClose :中止一个FindFirst / FindNext序列
有关文件管理标准过程/函数的更详细资料,请查阅Delphi相关的Help主题。以上的大部分过程在后面都有应用实例,读者可以从中体会其用法。
在Delphi的联机帮助Help系统中把有关文件的过程/函数分为两个主题:I/O Routine和File_Management Routine。前者大部分以文件变量为操作对象,而后者大部分以文件名或文件句柄为操作对象。这里为了方便读者的使用,我们按功能重新进行了分类。在下一节中主要应用I/O Routine主题下的过程,而在第四节的综合举例中主要应用File_Management Routine主题下的过程。
另外,Windows提供了许多有关文件管理的API函数。虽然在一般情况下,利用Delphi提供的函数已足够解决问题,但有时候仍然需要使用Windows API。在(6.4.4.2)中我们就用到了Windows API函数GetDriveType。有关Windows API函数的情况,请读者参阅相关的资料,这里不再进行介绍。
6.2 记录文件的应用
6.2.1 任务介绍
在这一节,我们开发一个系统安全性综合评估方法管理系统。系统安全性在复杂项目开发中十分重要,但由于牵涉面广因而很难获得客观、全面的评估值。鉴于此我们提出多角度、多侧面评估而后定量集成的思路,并在此基础上提出了多种安全性综合评估方法。每种方法由不同部门进行评估而后把结果汇总、综合。
为此我们定义如下的记录类型:
type
TNature = (Micro,Macro);
{方法性质,分为微观和宏观两类}
TMethod = Record
Name: string[20]; {方法名}
Condition: string[40]; {方法适用条件}
Nature: TNature; {方法性质}
Result: Real; {方法评估值}
end;
用来记录不同方法的信息。
由于不同方法的条件、性质不同,因而对工程开发的不同阶段适用方法集也不同。因此需要根据实际情况对方法集进行管理。我们把每一方法作为一条记录,每一方法集作为一个记录文件。下面讨论系统的实现方法。
6.2.2 设计基本思路
本系统要实现的基本功能是文件的打开、创建、关闭、显示,记录的增加、修改、删除以及结果的综合和显示。为此我们使用了两组按钮分别用于文件和记录的操作, 使用一个StringGrid控件来显示文件内容,使用一个只读编辑框显示结果的综合。
其中各部件的名称、功能如下表所示:
表6.1 主窗口部件的设计
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
部件名称 主要属性 备注
──────────────────────────────────────
RecFileForm BorderStyle=bsDialog 文件打开后把文件名附到窗口标题后
Position=poScreenCenter
StringGrid1 大小行数动态确定
HazAttr(编辑框) ReadOnly=True 显示综合结果
OpenButton TabOrder=0 打开一个记录文件,若文件不存在则创建
NewButton Caption='打开' 创建一个记录文件,若文件存在则打开
CloseButton Caption='关闭' 关闭一个已打开的文件
AddButton Caption='增加' 增加一条记录
ModifyButton Caption='修改' 修改一条记录
DeleteButton Caption='删除' 删除一条记录
CalcuButton Caption='计算' 计算最终结果并显示
ExitButton Caption='退出' 系统终止。若当前有打开的文件则先关闭
OpenDialog1 Filter= 选择或输入欲打开的文件
'Record File(*.Rec)|.Rec
|Any File(*.*)|*.*'
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
另外,StringGrid1、HazAttr的标题用两个标签框(Label)来显示。
另外我们还需要一个编辑对话框。其中四个编辑框Name、Condition、Nature、 Result分别对应TMethod记录的四个域。
为协调程序运行,我们定义了一组全局变量。各变量的类型、作用如下表。
表6.2 全局变量及其作用
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
变量名 类型 作用
─────────────────────────────────
MethodFile MethodFileType 与当前打开文件相关联的文件变量
FileName string[70] 当前打开文件的文件名
Count Count 当前打开文件的记录总数
CurrentRec Integer 当前处理记录号
FileOpened Boolean 当前是否有文件打开
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
记录文件类型MethodFileType的定义为
type
MethodFileType = file of TMethod;
布尔变量FileOpened用于控制文件按钮的使能、变灰,记录按钮的反应以及系统结束时是否需要首先关闭文件。
6.2.3 记录文件的打开和创建
记录文件的打开和创建同文本文件一样也需要关联和初始化两个步骤。同文本文件唯一的不同是不能使用Append过程。
记录文件缺省情况下以读写方式打开,如果想以只读或只写方式打开,则需要修改System单元中定义的变量FileMode的值。
FileMode的取值和意义如下表。
表6.3 FileMode的取值和意义
━━━━━━━━━━━━━━
取值 意义
──────────────
0 只读
1 只写
2 读写
━━━━━━━━━━━━━━
FileMode是一个全局变量,对它的每次修改都将影响所有Reset的操作,因此在打开自己的文件后应还原它的值。
在本系统中,当用户按下“打开”按钮时,首先弹出一个标准文件打开对话框,要求用户输入或选择文件名。确认后如果该文件名的文件存在,则用Reset打开,若不存在则创建。程序清单如下。
procedure TRecFileForm.OpenButtonClick(Sender: TObject);
begin
if OpenDialog1.Execute then
FileName := OpenDialog1.FileName
else
exit;
AssignFile(MethodFile,Filename);
try
Reset(MethodFile);
FileOpened := True;
except
On EInOutError do
begin
try
if FileExists(FileName) = False then
begin
ReWrite(MethodFile);
FileOpened := True;
end
else
begin
FileOpened := False;
MessageDlg('文件不能打开',mtWarning,[mbOK],0);
end;
except
On EInOutError do
begin
FileOpened := False;
MessageDlg('文件不能创建',mtWarning,[mbOK],0);
end;
end;
end;
end;
if FileOpened = False then exit;
Count := FileSize(MethodFile);
if Count>0 then
ChangeGrid;
RecFileForm.Caption := FormCaption+' -- '+FileName;
NewButton.Enabled := False;
OpenButton.Enabled := False;
CloseButton.Enabled := True;
end;
首先系统试图用Reset打开一个文件,并置FileOpened为True。如果文件不能打开,则引发一个I/O异常。在异常处理过程中,首先检测文件是否存在。若不存在则创建这个文件。否则是其它原因引发的异常,则把FileOpend重置为False, 并显示信息“文件不能打开”。在文件创建过程中仍可能引发异常,因而在一个嵌套的异常处理中把FileOpened重置为False,并提示信息“文件不能创建”。
有关异常处理的内容请读者参看第十二章。这段程序说明:异常处理机制不仅能使我们的程序更健壮,而且为编程提供了灵活性。
当用户按下“创建”按钮时,系统首先弹出一个标准输入框,要求用户输入文件名,确认后系统首先检测文件是否存在。若存在则直接打开,否则创建一个新文件。打开或创建过程导致异常,则重置FileName和FileOpened两个全局变量。
procedure TRecFileForm.NewButtonClick(Sender: TObject);
begin
FileName := InputBox('输入框','请输入文件名','');
if FileName = '' then Exit;
try
AssignFile(MethodFile,FileName);
if FileExists(FileName) then
begin
Reset(MethodFile);
Count := FileSize(MethodFile);
if Count>0 then
ChangeGrid;
end
else
begin
Rewrite(MethodFile);
count := 0;
end;
FileOpened := true;
Except
on EInOutError do
begin
FileName := '';
FileOpened := False;
end;
end;
if FileOpened then
begin
NewButton.Enabled := False;
OpenButton.Enabled := False;
CloseButton.Enabled := True;
RecFileForm.Caption := FormCaption+' -- '+FileName;
end;
end;
当文件打开或创建后,所要做的工作有:
● 若文件非空,则计算文件长度,并用文件内容填充StringGrid1
● “创建”、“打开”按钮变灰,“关闭”按钮使能
● 把文件名附到窗口标题后
6.2.4 记录文件的读入和显示
定义一个全局变量Count用来保存文件中的记录个数。当文件装入时:
Count := FileSize(MethodFile);
如果Count > 0,则首先确定StringGrid1的高度、行数。为保证StringGrid1不会覆盖窗口下面的编辑框,定义一个常量MaxShow。当Count < MaxShow时,记录可全部显示;当Count >= MaxShow时,StringGrid1自动添加一个滚动棒。为保证滚动棒不覆盖掉显示内容,StringGrid1的宽度应留有余地。
确定StringGrid1高度、行数的代码如下:
With StringGrid do
if count < MaxShow then
Height := DefaultRowHeight * (Count+1)+10
else
Height := DefaultRowHeight * MaxShow+10;
RowCount := Count+1;
end;
而后从文件中逐个读入记录并显示在StringGrid1的相应位置:
for i := 1 to Count do
begin
Read(MethodFile,MethodRec);
ShowMethod(MethodRec,i);
end;
ShowMehtod是一个过程,用来把一条记录填入StringGrid1的一行中。对于Name、Condition域而言,只须直接赋值即可;而对Nature 域需要把枚举类型值转化为对应意义的字符串(0:“微观”,1:“宏观”);而对Result域则需要把数值转化为一定格式的字符串:
Str (MethodRec.Result:6:4,ResultStr);
StringGrid1.Cells[3,Pos] := ResultStr;
即Result显示域宽为6,其中小数点后位数为4。
6.2.5 增加一条记录
当用户单击“增加”按钮时屏幕将会弹出一个记录编辑模式对话框EditForm。在编辑框中填入合适的内容并按OK键关闭后,相应值写入一个TMethod类型的变量MethodRec中。其中Nature和Result 域需要进行转换。之后增加的记录添加到StringGrid1的显示中。
最后文件定位于尾部,写入当前记录,总记录数加1。
Seek(MethodFile,Count);
Write(MethodFile,MethodRec);
Count := Count+1;
完整的程序清单如下:
procedure TRecFileForm.AddButtonClick(Sender: TObject);
var
MethodRec: TMethod;
Rl: Real;
k: Integer;
EditForm: TEditForm;
begin
if FileOpenEd = False then Exit;
EditForm := TEditForm.Create(self);
if EditForm.ShowModal <> idCancel then
begin
HazAttr.text := '';
MethodRec.Name := EditForm.MethodName.text;
MethodRec.Condition := EditForm.Condition.text;
case EditForm.NatureCombo.ItemIndex of
0:
MethodRec.Nature := Micro;
1:
MethodRec.Nature := Macro ;
end;
Val(EditForm.Result.text,Rl,k);
MethodRec.Result := Rl;
with StringGrid1 do
begin
if Count < MaxShow then
Height := Height+DefaultRowHeight;
RowCount := RowCount+1;
end;
ShowMethod(MethodRec,Count+1);
seek(MethodFile,Count);
write(MethodFile,MethodRec);
Count := Count+1;
end;
end;
6.2.6 修改记录
首先获取当前记录位置:
CurrentRec := StringGrid1.Row - 1;
而后打开编辑对话框并显示当前值。修改完毕后,修改结果保存在一个记录中并在StringGrid1中重新显示。
最后修改结果写入文件:
Seek(MethodFile,CurrentRec);
Write(MethodFile,MethodRec);
完整程序如下:
procedure TRecFileForm.ModifyButtonClick(Sender: TObject);
var
MethodRec: TMethod;
Rl: Real;
k: Integer;
EditForm: TEditForm;
begin
if FileOpened = False then Exit;
EditForm := TEditForm.Create(self);
CurrentRec := StringGrid1.Row-1;
with EditForm do
begin
MethodName.text := StringGrid1.Cells[0,CurrentRec+1];
Condition.text := StringGrid1.Cells[1,CurrentRec+1];
if StringGrid1.Cells[2,CurrentRec+1] = '微 观' then
NatureCombo.ItemIndex := 0
else
NatureCombo.ItemIndex := 1;
Result.text := StringGrid1.Cells[3,CurrentRec+1];
if ShowModal <> idCancel then
begin
HazAttr.text := '';
MethodRec.Name := MethodName.text;
MethodRec.Condition := Condition.text;
case NatureCombo.ItemIndex of
0:
MethodRec.Nature := Micro;
1:
MethodRec.Nature := Macro ;
end;
Val(Result.text,Rl,k);
MethodRec.Result := Rl;
ShowMethod(MethodRec,CurrentRec+1);
seek(MethodFile,CurrentRec);
write(MethodFile,MethodRec);
end;
end;
end;
6.2.7 记录的删除、插入、排序
删除一条记录的基本思路是:获取当前记录的位置并把该位置后的记录逐个向前移动。 文件在最后一条记录前截断。
for i:=CurrentRec+1 to Count-1 do
begin
seek(MethodFile,i);
read(MethodFile,MethodRec);
seek(MethodFile,i-1);
Write(MethodFile,MethodRec);
end;
Truncate(MethodFile);
为避免误删除,在进行删除操作前弹出一个消息框进行确认。删除后要更新全局变量的值和显示内容:
Count := Count - 1;
ChangeGrid;
完整的程序如下:
procedure TRecFileForm.DeleteButtonClick(Sender: TObject);
var
NewFile: MethodFileType;
MethodRec: TMethod;
NewFileName: String;
i: Integer;
begin
if FileOpened = False then Exit;
CurrentRec := StringGrid1.Row-1;
if CurrentRec < 0 then Exit;
if MessageDlg('Delete Current Record ?', mtConfirmation,
[mbYes, mbNo], 0) = idYes then
begin
HazAttr.text := '';
for I := CurrentRec+1 to Count-1 do
begin
seek(MethodFile,i);
read(MethodFile,MethodRec);
seek(MethodFile,i-1);
Write(MethodFile,MethodRec);
end;
Truncate(MethodFile);
Count := Count-1;
ChangeGrid;
end;
end;
这里所显示的删除操作简单明了。但在程序开始设计时我却走了一条弯路,后来发现虽然这种方法用于记录的删除操作显得笨拙、可笑,但却恰恰是记录插入、排序的思想。
这种思想的核心是创建一个新文件保存更新后的内容。若新文件顺利创建,则删除原文件,否则恢复原来的文件。程序清单如下:
procedure TRecFileForm.DeleteButtonClick(Sender: TObject);
var
NewFile: MethodFileType;
MethodRec: TMethod;
NewFileName: String;
i: Integer;
begin
if FileOpened = False then Exit;
CurrentRec := StringGrid1.Row-1;
if CurrentRec < 0 then Exit;
if MessageDlg('Delete Current Record ?', mtConfirmation,
[mbYes, mbNo], 0) = idYes then
begin
HazAttr.text := '';
NewFileName := ChangeFileExt(FileName,'.sav');
try
AssignFile(NewFile,FileName);
ReWrite(NewFile);
Except
On EInOutError do
begin
Rename(MethodFile,FileName);
Exit;
end;
end;
for i := 1 to Count do
if I <> CurrentRec+1 then
begin
MethodRec := GridToRec(i);
Write(NewFile,MethodRec);
end;
closeFile(MethodFile);
try
AssignFile(MethodFile,Filename);
Reset(MethodFile);
except
on EInOutError do
begin
DeleteFile(FileName);
AssignFile(MethodFile,NewFileName);
Reset(MethodFile);
Rename(MethodFile,FileName);
Exit;
end;
DeleteFile(NewFileName);
Count:=Count-1;
ChangeGrid;
end;
end;
对于记录插入,方法基本同上。对于排序,可先将关键域读入排序,而后再按排序结果对应的记录号顺序重写文件。
6.2.8 结果综合
对不同方法的评估结果,可按一定的公式进行综合。当用户按下“计算”按钮时,系统进行计算并把综合结果写入HazAttr只读编辑框中。
为保证结果显示的正确性,每次增加、修改、删除操作确认后HazAttr编辑框清空。
6.2.9 编辑对话框的输入检查
当用户单击“增加”或“修改”按钮时系统将弹出一个编辑对话框,让用户输入或修改记录内容。其中的三个编辑框,一个组合列表框分别对应TMethod 的四个域。由于TMethod的Result域必须是[0,1]间的小数,因此当用户按OK键关闭对话框时应进行类型和范围检查。
在VB中我做过同样的工作,那时需要对用户输入的键码逐个进行判断。但这种方法很繁琐、很难做圆满(如不能很好地支持编辑键)。而Object Pascal提供了更好的方法。这种方法的关键就在于它的类型转换函数Val:
procedure Val(Str: String;var V; var Code: Integer);
V是由Str转换成的整型或实型数。若字符串非法,则出错位置返至Code;否则置Code为0。字符串非法并不会引发一个转换异常。
如果转换后的数超出了我们的范围,则显式把Code置为-1。最后统一通过检测Code是否为0来判断输入是否合法。
我们把输入检查放在对话框的OnCloseQuery事件处理过程中。如输入非法,则禁止对话框关闭,并将输入焦点置于Result编辑框中。但假如用户按了Cancel按钮,则这种检查是多余的。为此定义一个布尔变量IsCancel,对话框生成时置为False。假如用户按下Cancel,则置为True,此时OnCloseQuery事件不进行输入检查。
对话框的OnCloseQuery事件处理过程的程序清单如下:
procedure TEditForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
var
Res: Real;
k: Integer;
begin
if IsCancel = False then
begin
val(Result.text,Res,k);
if (Res > 1) or (Res < 0) then k := -1;
if k <> 0 then
begin
MessageDlg('非法输入 !',mtWarning,[mbOK],0);
Result.text := '';
CanClose := False;
Result.SetFocus;
end;
end;
end;
6.2.10 文件和系统的关闭
文件关闭须调用CloseFile过程:
CloseFile(MethodFile);
并对系统的状态重新进行设置。
系统关闭时首先检测当前是否有打开的文件。若有则先关闭文件。这在主窗口的OnCloseQuery事件中实现。
实现文件关闭的程序清单如下:
procedure TRecFileForm.CloseButtonClick(Sender: TObject);
begin
if FileOpened then
begin
CloseFile(MethodFile);
FileOpened := False;
ClearGrid;
OpenButton.Enabled := True;
NewButton.Enabled := True;
CloseButton.Enabled := False;
RecFileForm.Caption := FormCaption;
end;
end;
实现系统关闭前检查的程序清单如下:
procedure TRecFileForm.FormCloseQuery(Sender: TObject;
var CanClose: Boolean);
begin
if FileOpened then
closeFile(MethodFile);
end;
6.2.11 记录文件小结
我们所举的例子虽然简单,但基本覆盖了记录文件操作的主要方面。这里关键问题在于灵活应用Delphi提供的文件管理函数。同时,为了保证程序的健壮性应对异常进行捕获并处理。在数据库应用技术发展的今天,记录文件的重要性也许有所下降,但对象我们这里所处理的简单问题它仍有用武之地。
这里所举的例子一次只能处理一个文件。但读者可以很容易把它改为一个MDI程序。虽然对于这里的实际情况来说,似乎并无必要。
6.3 文件控件的应用
Delphi文件管理的最大特色是提供了一组文件操作控件。利用这些控件我们可以快速开发一个文件名浏览系统。其功能强大与其所需书写代码之少所形成的强烈反差,正是Dephi生命力的体现。
6.3.1 文件控件及其相互关系
Delphi提供的专用文件控件如下表所示。
表6.4 Delphi专用文件控件━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
控件名 功能
─────────────────────────────────────
DriveComboBox 驱动器组合列表框。用于选择当前驱动器
FileListBox 文件列表框。用于显示当前目录中的文件和选中当前文件
FilterComboBox 文件类型组合列表框。用于选择显示文件的类型
DirectoryOutline 目录树(6.4节专门介绍)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
以上控件前四个在Component Palette(部件选择板)的System页中,DirectoryOutline在Component Palette的Samples页中。
以上文件控件再加上文件编辑框、目录标签框(事实上是一般的编辑框、标签框)就可以构成一个完整的文件操作系统。它们之间的联系几乎不用代码支持,只要设置好相应的属性就可以了。
FileEdit、DirLabel、FileListBox、FileFilterComloList、 DirectoryListBox、DriveComboList六个控件间的属性联系如下:
DriveComboList .DirList := DirectoryListBox;
DirectoryListBox.DirLabel := DirLabel;
DirectoryListBox.FileList := FileListBox;
FileFilterComboList.FileList := FileListBox;
FileListBox.FileEdit := FileEdit;
以上联系可以在设计时完成。只要打开相应属性的选择列表框进行选择即可。也可以在运行时利用如上的赋值语句建立联系。
文件控件的关键属性基本上都在以上联系中反映出来了。除此之外,FileFilterComboList有一个Filter属性,用来设置组合列表框的选择项;FileListBox 有一个Mask属性,用于设置显示文件的类型,这就允许FileListBox在脱离FileFilterComboList单独应用时仍能根据需要显示特定的文件。在6.4节中我们将应用这一功能。
文件控件的方法、事件基本是从ListBox和ComboBox中继承的。但FileListBox 中有一个ApplyFilePath方法很有用,我们将在后边给出其用法。
6.3.2 文件名浏览查找系统的设计思路
作为文件控件的应用实例,我们开发了一个简单的文件名浏览查找系统。这个系统可用于文件名的显示,把选中的文件写入列表框,并能按文件编辑框中输入的通配符对文件进行查找。
表6.5 部件的设计
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
部件 属性 功能
─────────────────────────────────────
FileCtrForm Position=poDefault 主窗口
DirLabel 显示当前目录
FileEdit TabOrder=0 显示当前文件/输入文件显示匹配符
FileListBox1 FileEdit=FileEdit 显示当前目录文件
DirectoryListBox1 DirLabel=DirLabel 显示当前驱动器目录
FileList= FileListBox1
DriveComboBox1 DirList= DirectoryListBox1 选择当前驱动器
FilterComboBox1 FileList=FileListBox1 选择文件显示类型
Filter='All Files(*.*)|*.*|
Source Files(*.pas)|*.pas|
Form Files(*.dfm)|*.dfm|
Project Files(*.dpr)|*.dpr'
ListBox1 显示选中或查找的文件
Button1 Caption='查找' 按 FileEdit 中的内容进行查找
Button2 Caption='退出' 退出系统
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6.3.3 文件名浏览查找系统的功能和实现
6.3.3.1 按指定后缀名显示当前目录中的文件
实现这一功能只需要在控件间建立正确的联系即可,不需要代码支持。建立联系的方法如(6.3.1)中的介绍。
6.3.3.2 把选中的文件添加到列表框中
在FileListBox1的OnClick事件中:
procedure TFileCtrForm.FileListBox1Click(Sender: TObject);
begin
if Searched then
begin
Searched := False;
ListBox1.Items.Clear;
Label5.Caption := 'Selected Files';
end;
if NotInList(ExtractFileName(FileListBox1.FileName),ListBox1.Items) then
ListBox1.Items.Add(ExtractFileName(FileListBox1.FileName));
end;
Searched是一个全局变量,用于标明ListBox1当前显示内容是查找的结果还是从FileListBox1中选定的文件。
函数NotInList用于判断待添加的字符串是否已存在于一个TStrings对象中。函数返回一个布尔型变量。
NotInList的具体实现如下。
Function TFileCtrForm.NotInList(FileName: String;Items: TStrings): Boolean;
var
i: Integer;
begin
for I := 0 to Items.Count-1 do
if Items[i] = FileName then
begin
NotInList := False;
Exit;
end;
NotInList := True;
end;
6.3.3.3 按指定匹配字符串显示当前目录中的文件
当在FileEdit中输入一个匹配字符串,并回车,文件列表框将显示匹配结果。这一功能在FileEdit的OnKeyPress事件中实现。
procedure TFileCtrForm.FileEditKeyPress(Sender: TObject; var Key: Char);
begin
if Key = #13 then
begin
FileListBox1.ApplyFilePath(FileEdit.Text);
Key := #0;
end;
end;
文件列表框提供的ApplyFilePath方法是解决这一问题的关键所在。
6.3.3.4 按指定匹配字符串查找当前目录中的文件
为了进行比较,我们用另一种方法来实现文件的查找功能,即利用标准过程FindFirst、FindNext。FileList1与ListBox1 中的内容完全一致。
当用户单击“查找”按钮时,与FileEdit 中字符串相匹配的文件将显示在ListBox1中。下面是实现代码。
procedure TFileCtrForm.Button1Click(Sender: TObject);
var
i: Integer;
SearchRec: TSearchRec;
begin
Searched := True;
Label5.Caption := 'Search Result';
ListBox1.Items.Clear;
FindFirst(FileEdit.text,faAnyFile,SearchRec);
ListBox1.Items.Add(SearchRec.Name);
Repeat
i := FindNext(SearchRec);
If i = 0 then
ListBox1.Items.Add(SearchRec.Name);
until i <> 0;
end;
SearchRec是一个TSearchRec类型的记录。TSearchRec的定义如下:
TSearchRec = record
Fill: array[1..21] of Byte;
Attr: Byte;
Time: Longint;
Size: Longint;
Name: string[12];
end;
在这一结构中提供了很多信息,灵活应用将给编程带来很大方便。下面我们举几个例子。
1. 检测给定文件的大小。
function GetFileSize(const FileName: String): LongInt;
var
SearchRec: TSearchRec;
begin
if FindFirst(ExpandFileName(FileName), faAnyFile, SearchRec) = 0 then
Result := SearchRec.Size
else
Result := -1;
end;
这一程序将在下一节中应用。
2. 获取给定文件的时间戳,事实上等价于FileAge函数。
function GetFileTime(const FileName: String): Longint;
var
SearchRec: TSearchRec;
begin
if FindFirst(ExpandFileName(FileName),faAnyFile, SearchRec) = 0 then
Result := SearchRec.Time
else
Result := -1;
end;
3. 检测文件的属性。如果文件具有某种属性,则
SearchRec.Attr And GivenAttr > 0
属性常量对应的值与意义如下表:
表6.6 属性常量对应的值与意义
━━━━━━━━━━━━━━━━━━━━
常量 值 描述
─────────────────────
faReadOnly $01 只读文件
faHidden $02 隐藏文件
faSysFile $04 系统文件
faVolumeID $08 卷标文件
faDirectory $10 目录文件
faArchive $20 档案文件
faAnyFile $3F 任何文件
━━━━━━━━━━━━━━━━━━━━
6.4 文件管理综合举例:文件管理器的实现
在本章的最后,我们利用Delphi提供的文件控件和文件管理函数开发一个简单的文件管理器。虽然这一文件管理器还无法和Windows提供的文件管理器相比拟,但它也为一般的文件操作提供了足够多的功能,而且如果读者感兴趣,还可以对它做进一步的扩充。在后边的拖放操作一章中,我们就为它提供了拖放支持,使它看起来更象一个“文件管理器”。
6.4.1 设计基本思路
6.4.1.1 窗口设计
文件管理器的主窗口是一个多文档界面(MDI)。有关文件、目录的显示和文件管理功能的实现都放在子窗口中。在程序执行过程中将根据需要弹出一些完成不同操作的对话框。这些对话框都是在需要时动态生成的。表6.7给出了本程序所设计窗体的清单。
表6.7 FileManger窗体清单
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
窗体类 功能 用于创建该类窗体的菜单项
──────────────────────────────────────
TFileManager 主窗口
TFMForm 子窗口 Windows|New Window
TFileAttrForm 显示文件属性 File|Properties;Function|Search
TChangeForm 文件移动、拷贝、改名、改变 File|Move.Cope.Rename 当前目录等操作的输入对话框 Directory|change Directory
TSearchForm 输入待查找文件的名称和路径 Function|Search
TDiskViewForm 显示磁盘信息 Function|Disk View
TViewDir 输入待创建的子目录 Directory|CreateDirectory
TAboutBox 显示版权信息 Help|About
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6.4.1.2 界面设计
主窗口界面主要是主菜单和用于表示当前目录、当前文件的状态条。
表6.8 主窗口界面设计
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
部件 属性 功能
─────────────────────────────
FileManager Style=fsMDI 主窗口
WindowMenu=Windows
Position=poDefault
MainMenu1 主菜单
FilePanel Align=alBottom 显示当前选中文件
BevelInner=bvLowered
BevelWidth=2
DirectoryPanel Align=alBottom 显示当前选中目录
Alignment=taLeftJustify
BevelInner=bvLowered
BevelWidth=2
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
主窗口主菜单包括File、WIndows、Help三项。File菜单项在子窗口生成时被子窗口同名菜单项所取代。设置Windows、Help的GroupIndex = 9,可以使子窗口生成时这两个菜单项仍存在。
子窗口界面包括主菜单、目录树(DirectoryOutline)、文件列表框、 用于显示驱动器的标签集(TabSet)以及三个用于显示驱动器类型的TImage部件。
表6.9 子窗口界面设计
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
部件 属性 功能
───────────────────────────────────────
FMForm ActiveControl=DirectoryOutline 子窗口
Position=poDefault
Style=fsMDIChild
MainMenu1 主菜单
DriveTabSet Align=alTop 显示驱动器
style=tsOwnerDraw
DirectoryOutline Align=alLeft 显示当前驱动器的目录树
options=[ooDrawTreeRoot,
ooDrawFocusRect,ooStretchBitmaps]
FileList Align=alClient 显示当前目录中的文件
FileType=[ftReadOnly,
ftHidden,ftSystem,ftArchive,ftNormal]
ShowGlyphs=True
Network(Image) Picture(Network.bmp) 标志网络驱动器
Vsible=False
Floppy(Image) Picture(Floppy.bmp) 标志软驱
Visible=False
Fixed(Image) Picture(Fixed.bmp) 标志硬驱
Visible=False
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
子窗口主菜单包括File、Function、Directory三个菜单项, 分别用于完成文件的基本管理功能、其它管理功能和目录管理功能。
由于对话框界面设计很简单,这里不再进行赘述。 读者可直接参考后面将给出的对话框界面图(图6.8---6.13)进行设计。
6.4.2 子窗口的创建、布置和关闭
子窗口的创建、布置由父窗口的Windows菜单控制,其菜单项如下:
● New Windows : 创建新的子窗口
● Tile : 平铺
● Cascade : 层叠
● ArrangeIcon : 排列图标
● Minimized All : 极小化所有子窗口
子窗口的创建只需要简单调用窗体的Create方法:
FileMan := TFMForm.Create(Application);
子窗口的标准排列方式直接调用MDI窗口的标准方法Tile、Cascade和ArrangeIcons。
极小化所有子窗口的实现利用MDI窗口的两个属性:MDIChildCount和MDIChildren:
for i := 0 to MDICount - 1 do
MDIChildren[i].Windowstate := wsMinimized;
子窗口关闭时释放内存空间,为此在子窗口TFMForm的OnClose事件中令
Action := OnFree;
为了保持和Windows的File Manager的一致性,我们也禁止关闭最后一个子窗口,这需要在子窗口的OnCloseQuery事件处理过程中实现:
If FileManager.MDIChildCount <= 1 then
CanClose := False;
CanClose是OnCloseQuery事件过程返回的一个参数,用于判定窗口是否可以关闭。
由于这一过程归子窗口所有,因而MDIChildCount前必须加上其对象名FileManager。
但不幸的是:这样一来我们的程序无法终止了!原来MDI窗口关闭前首先关闭其所有的子窗口。如果子窗口不能关闭,MDI窗口也不能关闭。
为此我们需要判断发出关闭消息的是子窗口的系统菜单还是菜单的Exit项。
定义一个全局变量
var
ExitClick: Boolean;
在子窗口的Exit1Click事件处理过程中:
ExitClick := True;
FileManager.Exit1Click(Sender);
子窗口关闭前可以利用这一全局变量检测是否应关闭:
If (FileManager.MDIChildCount <= 1) and (Not ExitClick) then
CanClose := False;
6.4.3 文件控件的联系
在本例中我们使用了一组新的控件:TabSet、DirectoryOutline、FileListBox,用于显示和选择驱动器、目录和文件。与(6.3)中所用方法相比,使用这一组控件需要少量的代码支持。
TabSet与DirectoryOutline的联系在TabSet的Click事件处理过程中建立:
With DriveTabSet do
DirectoryOutline.Drive := Tabs[TabIndex][1];
DirectoryOutline与FileListBox的联系在DirectoryOutline的Change事件处理过程中建立:
FileList.Directory := DirectoryOutline.Directory;
FileList.Update;
6.4.4 DriveTabSet的自画风格显示
Dephi为一些控件提供了自画风格的显示,如ListBox、ComboBox、TabSet等。 在缺省情况下,这些控件自动显示文本。而在自画风格下,拥有控件的窗体在运行时间内自己画出控件的每一项目。
自画风格显示通常的应用是为项目除文本外再添加图形显示。能以自画风格显示的控件有一个共同特点:都拥有一个TStrings类型的项目链。由于TStrings类的特点(参第三章),它们都可以加入一个和对应文本相联系的对象。 而这正是自画风格显示的关键。
通常情况下产生一个自画风格需要三个步骤:
1.设置自画风格;
2.向字符串链表添加图形对象;
3.画出自画项目。
6.4.4.1 设置自画风格
控件属性Style 用于设置自画风格。对于DriveTabSet,我们把Style 属性设置为tsOwnerDraw。
对于ListBox、ComboBox等控件的设置与TabSet略有差异,读者可参阅联机帮助文档。
6.4.4.2 向字符串链表添加图形对象
1.在应用程序中添加图片部件
在本程序中我们设置了三个图片部件NetWork、Floppy、Fixed,并分别与三个位图文件NetWork.bmp、Floppy.bmp、Fixed.bmp相关联。
2.把图片添加到字符串链表中
根据字符串链表的性质,我们可以把对象与已存在的字符串建立联系,也可以同时添加字符串和对象。这里我们采用后一种方法。
在子窗口的OnCreate事件处理过程中,我们利用一个循环依次检测从a到z的驱动器是否存在以及驱动器的类型。这利用了Windwos API函数GetDrivetype, 如果驱动器不存在则返回0,否则返回驱动器的类型(DRIVE_REMOVABLE、DRIVE_FIXED、DRIVE_REMOTE)。根据驱动器类型我们可以判断与文本(驱动器名)同时添加到Tabs中的不同图形对象。在添加过程中,DriveTabSet的TabIndex被设置为当前驱动器。
程序清单如下:
procedure TFMForm.FormCreate(Sender: TObject);
var
Drive, AddedIndex: Integer;
DriveLetter: Char;
begin
for Drive := 0 to 25 do
begin
DriveLetter := Chr(Drive + ord('a'));
case GetDrivetype(Drive) of
DRIVE_REMOVABLE:
AddedIndex := DriveTabSet.Tabs.AddObject(DriveLetter, Floppy.Picture.Graphic);
DRIVE_FIXED:
AddedIndex := DriveTabSet.Tabs.AddObject(DriveLetter, Fixed.Picture.Graphic);
DRIVE_REMOTE:
AddedIndex := DriveTabSet.Tabs.AddObject(DriveLetter, Network.Picture.Graphic);
end;
if UpCase(DriveLetter) = UpCase(FileList.Drive) then
DriveTabSet.TAbIndex := AddedIndex;
end;
end;
6.4.4.3 画出自画项目
当把一个控件的风格设置为自画时,Windows不再负责往屏幕上画出控件的项目,而是为每个可见项目产生自画事件。应用程序可以通过处理自画事件画出控件的项目。
1.确定自画项目的大小
对于TabSet而言,这在OnMeasureTab事件处理过程中完成。我们需要把DriveTabSet每个标签的宽度增大到足以同时放下文本和位图。
procedure TFMForm.DriveTabSetMeasureTab(Sender: TObject; Index: Integer;
var TabWidth: Integer);
var
BitmapWidth: Integer;
begin
BitmapWidth := TBitmap(DriveTabSet.Tabs.Objects[Index]).Width;
Inc(TabWidth, 2 + BitmapWidth);
end;
由于TStrings的Objects属性中存放的对象都是TObject类型,并没有Width属性,因而需要再把它转化为TBitmap类型的对象:
BitmapWidth := TBitmap(DriveTabSet.Tabs.Objects[Index]).Width;
2.画出每个自画项目
这在TabSet的OnDrawTab事件处理过程中完成。这一事件处理过程的参数中包含了待画项目索引、画板、待画区域、是否被选中等。这里我们只利用了前三个参数。事实上利用最后一个参数,我们可以对被选中的标签进行一些特殊的视觉效果处理。这一工作就留给读者自己去完成。
procedure TFMForm.DriveTabSetDrawTab(Sender: TObject; TabCanvas: TCanvas;
R: TRect; Index: Integer; Selected: Boolean);
var
Bitmap: TBitmap;
begin
Bitmap := TBitmap(DriveTabSet.Tabs.Objects[Index]);
with TabCanvas do
begin
Draw(R.Left, R.Top + 4, Bitmap);
TextOut(R.Left + 2 + Bitmap.Width, R.Top + 2, DriveTabSet.Tabs[Index]);
end;
end;
6.4.5 文件管理基本功能的实现
在子窗口的File菜单中,定义了文件管理的基本功能,它们是:
● Open :打开或运行一个文件(从文件列表框双击该文件可实现同样效果)
● Move :文件在不同目录间的移动
● Copy :文件拷贝
● Delete :文件删除
● Rename :文件更名
● Properties :显示文件属性
6.4.5.1 文件打开
文件打开功能可以运行一个可执行文件,或把文件在与之相关联的应用程序中打开。文件总是与创建它的应用程序相关联,这种关联可以在Windows的文件管理器中修改。要注意的是:文件的关联是以后缀名为标志的,因而对一个文件关联方式的修改将影响所有相同后缀名的文件。
文件打开功能实现的关键是利用了Windows API函数ShellExecute 。由于Windows API函数的参数要求字符串类型是PChar,而Delphi中一般用的是有结束标志的String类型,因此为调用方便我们把这一函数进行了重新定义如下。
function ExecuteFile(const FileName, Params, DefaultDir: String;
ShowCmd: Integer): THandle;
var
zFileName, zParams, zDir: array[0..79] of Char;
begin
Result := ShellExecute(Application.MainForm.Handle, nil,
StrPCopy(zFileName, FileName), StrPCopy(zParams, Params),
StrPCopy(zDir, DefaultDir), ShowCmd);
end;
以上函数在fmxutils单元中定义。fmxutils是一个自定义代码单元。
有关ShellExecute中各参数的具体含义读者可查阅联机Help文件。
StrPCopy把一个Pascal类型的字符串拷贝到一个无结束符的PChar类型字符串中。
在子窗口的Open1Click事件处理过程中:
procedure TFMForm.Open1Click(Sender: TObject);
begin
with FileList do
ExecuteFile(FileName, '', Directory, SW_SHOW) ;
end;
如果FileList允许显示目录的话(即FileType属性再增加一项ftDirectory),那么对于一个目录而言,打开的含义应该是显示它下边的子目录和文件。程序修改如下。
procefure TFMForm.Open1Click(Sender: Tobject);
begin
With FileList do
begin
if HasAttr(FileName,faDirectory) then
DirectoryOutline.Directory := FileName
else
ExecuteFile(FileName,' ' ,Directory,SW_SHOW);
end;
end;
其中HasAttr是一个fmxutils单元中的自定义函数,用于检测指定文件是否具有某种属性。
function HasAttr(const FileName: String; Attr: Word): Boolean;
begin
Result := (FileGetAttr(FileName) and Attr) = Attr;
end;
6.4.5.2 文件拷贝、移动、删除、更名
文件拷贝的关键是使用了以文件句柄为操作对象的文件管理函数,因而提供了一种底层的I/O通道。在Object Pascal中这一点是利用无类型文件实现的。
在文件拷贝中首先检查目标文件名是否是一个目录。如是则把原文件的文件名添加到目标路径后,生成目标文件全路径名。而后提取源文件的时间戳,以备拷贝完成后设置目标文件。拷贝过程中使用了返回文件句柄或以文件句柄为参数的文件管理函数FileOpen、FileCreate、FileRead、FileWrite、FileClose。为保证文件的正常关闭和内存的释放,在拷贝过程中进行异常保护。
过程CopyFile实现上述功能,它定义在fmxutils单元中。
procedure CopyFile(const FileName, DestName: TFileName);
var
CopyBuffer: Pointer;
TimeStamp, BytesCopied: Longint;
Source, Dest: Integer;
Destination: TFileName;
const
ChunkSize: Longint = 8192;
begin
Destination := ExpandFileName(DestName);
if HasAttr(Destination, faDirectory) then
Destination := Destination + '\' + ExtractFileName(FileName);
TimeStamp := FileAge(FileName);
GetMem(CopyBuffer, ChunkSize);
try
Source := FileOpen(FileName, fmShareDenyWrite);
if Source < 0 then
raise EFOpenError.Create(FmtLoadStr(SFOpenError, [FileName]));
try
Dest := FileCreate(Destination);
if Dest < 0 then
raise EFCreateError.Create(FmtLoadStr(SFCreateError,[Destination]));
try
repeat
BytesCopied := FileRead(Source, CopyBuffer^, ChunkSize);
if BytesCopied > 0 then
FileWrite(Dest, CopyBuffer^, BytesCopied);
until BytesCopied < ChunkSize;
finally
FileSetDate(Dest,TimeStamp);
FileClose(Dest);
end;
finally
FileClose(Source);
end;
finally
FreeMem(CopyBuffer, ChunkSize);
end;
end;
如果我们不使用FileSetDate过程,Windows自动把当前时间作为时间戳写入文件。
文件移动事实上是文件拷贝与文件删除的结合。fmxutils单元中的MoveFile过程实现了这一功能。
procedure MoveFile(const FileName, DestName: TFileName);
var
Destination: TFileName;
begin
Destination := ExpandFileName(DestName);
if not RenameFile(FileName, Destination) then
begin
if HasAttr(FileName, faReadOnly) then
raise EFCantMove.Create(Format(SFCantMove, [FileName]));
CopyFile(FileName, Destination);
DeleteFile(FileName);
end;
end;
EFCanMove是一个自定义异常类:
type
EFCanMove := Class(EStreamError);
有关自定义异常类请参阅第十二章。
文件删除、文件更名直接调用Delphi文件管理过程DeleteFile、RenameFile。它们都以文件名为参数。操作执行前应弹出一个对话框进行确认,执行完毕后应调用Update方法更新FileList的显示。
6.4.5.3 一致的界面
文件拷贝、文件移动、 文件更名以及后边的改变当前目录在形式上都表现为从一个源文件到一个目标文件。因而可以采用统一的用户界面,即ChangeForm对话框
这四个菜单项共用一个Click事件处理过程,通过对Sender参数的检测,决定将要打开对话框的标题和显示内容。当用户按OK键关闭且目标文件(目录)非空时,程序弹出一个消息对话框要求用户进一步确认,而后执行相应的动作。
共用的事件处理过程FileChange的程序清单如下:
procedure TFMForm.FileChange(Sender: TObject);
var
ChangeForm: TChangeForm;
IsFile: Boolean;
begin
ChangeForm := TchangeForm.Create(Self);
IsFile := True;
with ChangeForm do
begin
if Sender = Move1 then Caption := 'Move'
else if Sender = Copy1 then Caption := 'Copy'
else if Sender = Rename1 then Caption := 'Rename'
else if Sender = ChangeDirectory1 then
begin
Caption:='Change Directory';
IsFile:=False;
end
else Exit;
if IsFile then
begin
CurrentDir.Caption := FileList.Directory;
FromFileName.Text := FileList.FileName;
ToFileName.Text := '';
end
else
begin
CurrentDir.Caption := DriveTabSet.Tabs[DriveTabSet.TabIndex];
FromFileName.Text := DirectoryOutline.Directory;
ToFileName.Text := '';
end;
if (ShowModal <> idCancel) and (ToFileName.Text <> '') then
ConfirmChange(Caption, FromFileName.Text, ToFileName.Text);
end;
end;
其中用到的自定义私有过程ConfirmChange用于执行相应的动作:
procedure TFMForm.ConfirmChange(const ACaption, FromFile, ToFile: String);
begin
if MessageDlg(Format('%s %s to %s', [ACaption, FromFile, ToFile]),
mtConfirmation, [mbYes, mbNo], 0) = idYes then
begin
if ACaption = 'Move' then
MoveFile(FromFile, ToFile)
else if ACaption = 'Copy' then
CopyFile(FromFile, ToFile)
else if ACaption = 'Rename' then
RenameFile(FromFile, ToFile)
else if ACaption = 'Change Directory' then
changeDirectory(ToFile);
FileList.Update;
end;
end;
6.4.5.4 显示文件属性
当程序执行Properties 菜单项的Click 事件处理过程时,首先弹出一个TFileAttrForm类型的对话框,显示文件的属性
当用户修改并确认后程序重新设置文件属性。
Properties菜单项的Click事件处理过程如下:
procedure TFMForm.Properties1Click(Sender: TObject);
var
Attributes, NewAttributes: Word;
FileAttrForm: TFileAttrForm;
begin
FileAttrForm := TFileAttrForm.Create(self);
ShowFileAttr(FileAttrForm,FileList.FileName,FileList.Directory);
end;
其中过程ShowFileAttr的实现如下:
procedure TFMForm.ShowFileAttr(FileAttrForm:TFileAttrForm;
AFileName,Directory:String);
var
Attributes,NewAttributes: Word;
begin
with FileAttrForm do
begin
FileName.Caption := AFileName;
FilePath.Caption := Directory;
ChangeDate.Caption := DateTimeToStr(FileDateTime(AFileName));
Attributes := FileGetAttr(AFileName);
ReadOnly.Checked := (Attributes and faReadOnly) = faReadOnly;
Archive.Checked := (Attributes and faArchive) = faArchive;
System.Checked := (Attributes and faSysFile) = faSysFile;
Hidden.Checked := (Attributes and faHidden) = faHidden;
if ShowModal <> idCancel then
begin
NewAttributes := Attributes;
if ReadOnly.Checked then NewAttributes := NewAttributes or faReadOnly
else NewAttributes := NewAttributes and not faReadOnly;
if Archive.Checked then NewAttributes := NewAttributes or faArchive
else NewAttributes := NewAttributes and not faArchive;
if System.Checked then NewAttributes := NewAttributes or faSysFile
else NewAttributes := NewAttributes and not faSysFile;
if Hidden.Checked then NewAttributes := NewAttributes or faHidden
else NewAttributes := NewAttributes and not faHidden;
if NewAttributes <> Attributes then
FileSetAttr(AFileName, NewAttributes);
end;
end;
end;
以上过程中用到的函数FileDataTime在fmxutils单元中定义,返回一个TDatatime类型的变量。
function FileDateTime(const FileName: String): System.TDateTime;
begin
Result := FileDateToDateTime(FileAge(FileName));
end;
6.4.6 其它文件管理功能的实现
在子窗口的Function菜单中,定义了一些其它的文件管理功能:
● Search :查找一个给定名字的文件,若存在则显示该文件属性
● Disk View :显示当前驱动器的大小和剩余空间
● View type :确定显示文件的类型
6.4.6.1 文件查找
当用户单击Search菜单项时,程序弹出一个对话框(如图6.10),要求输入待查找的文件名和查找路径。文件名可以是通配符。当用户确认后程序显示第一个匹配文件的属性(如图6.9)。查找不到匹配文件则给出相应的信息。
在实现这一功能的最初设计中,我试图使用FileSearch函数,这个函数允许在多个不同路径中查找。但可惜的是:也许由于系统设计者的失误,这个函数并没有返回它应该返回的东西(第一个匹配文件的全路径名),而是仍把输入的匹配符返回。
没有办法我只能再次使用FindFirst,这个函数的特性在6.3节中已进行了介绍。下面是这一功能的实现代码。
procedure TFMForm.search1Click(Sender: TObject);
var
SearchForm: TSearchForm;
FileAttrForm: TFileAttrForm;
FindIt,path: String;
SearchRec: TSearchRec;
Return: Integer;
begin
SearchForm := TSearchForm.Create(self);
with SearchForm do
begin
SearchFile.text := '';
SearchPath.text := DirectoryOutline.Directory;
if (ShowModal <> idCancel) and
(SearchFile.Text <> '') and (SearchPath.text <> '') then
begin
FindIt := SearchPath.text+'\'+SearchFile.text;
Return := FindFirst(FindIt,faAnyFile,SearchRec);
if Return <> 0 then
FindIt := ''
else
FindIt := ExpandFileName(SearchRec.Name);
end;
if FindIt = '' then
MessageDlg('Cannot find the file in current directory.',
mtWarning, [mbOk], 0)
else
begin
Path := ExtractFilePath(FindIt);
FindIt := ExtractFileName(FindIt);
FileAttrForm := TFileAttrForm.Create(self);
ShowFileAttr(FileAttrForm,FindIt,Path);
end;
end;
end;
6.4.6.2 显示磁盘信息
当用户单击Disk View菜单项时,将弹出一个TDiskViewForm类型的对话框,用来显示当前磁盘的信息
磁盘信息的获取是在DiskViewForm中DriveEdit编辑框的OnChange事件处理过程中实现的。
procedure TDiskViewForm.driveEditChange(Sender: TObject);
var
dr: Byte;
Free,Total: LongInt;
begin
Free := DiskFree(0);
Total := DiskSize(0);
FreeSpace.text := IntToStr(Free)+ ' bytes.';
TotalSpace.text := IntToStr(Total) + ' bytes.';
end;
DiskFree、DiskSize带参数为0表示当前驱动器。读者可以很容易把它改成按用户输入显示磁盘信息的情况。
DiskViewForm中的三个编辑框设计时都令ReadOnly为True。
6.4.6.3 改变显示文件的类型
改变显示文件的类型事实上是设置FileList的Mask属性。我们利用一个标准的InputBox输入文件的匹配字符串。而后利用Update方法更新FileList。
procedure TFMForm.Viewtype1Click(Sender: TObject);
var
FileMask: String;
begin
FileMask := InputBox('File type','Input File type For View :',FileList.Mask);
If FileMask = '' then FileMask := '*.*';
FileList.Mask := FileMask;
FileList.Update;
CreateCaption;
end;
其中的CreateCaption私有过程将在(6.4.8)中进行介绍。
6.4.7 目录管理功能的实现
在子窗口的Directory菜单中,提供了目录管理功能:
● Create Directory :创建一个子目录
● Delete Directory :删除一个空的子目录
● Change Directory :改变当前目录
6.4.7.1 创建目录
创建目录时首先弹出一个TNewDir类型的对话框
对话框中要求用户输入目录名。如果用户不输入路径,则缺省认定为当前目录的子目录:
Dir := ExpandFileName(DirName.Text);
而后调用MkDir函数。在目录创建过程中关闭了I/O错误检测,出错不产生异常而是把IOResult设置为非零值。通过检查IOResult是否为0可以确定创建是否成功。
程序清单如下:
procedure TFMForm.CreateDirectory1Click(Sender: TObject);
var
NewDir: TNewDir;
Dir: String;
begin
{$I-}
NewDir := TNewDir.Create(self);
with NewDir do
begin
CurrentDir.Caption := DirectoryOutline.Directory;
if (ShowModal <> idCancel) and (DirName.Text <> '') then
Dir := ExpandFileName(DirName.text);
end;
MkDir(Dir);
if IOResult <> 0 then
MessageDlg('Cannot Create directory', mtWarning, [mbOk], 0);
end;
但不幸的是目录创建后我们却无法从当前目录树中看到。必须移到另一个驱动器而后再返回,创建的目录才是可见的。在后边我们将提供一种解决方法。
6.4.7.2 删除目录
在实现目录删除过程中,远不如创建目录那么顺利。碰到的问题是:
1.RmDir不允许删除当前目录。但为了操作方便,我们要求删除的恰恰是当前目录;
2.目录删除后调用Refresh方法或Update方法并不能使该目录从屏幕显示中去除。因而当用户试图进入该目录时会导致系统崩溃。
对第一个问题,我们的解决办法是把当前目录转换到其父目录。假如读者记得目录也被操作系统作为一种特殊的文件对待的话,那么就不会对下面的语句感到奇怪了:
path := DirectoryOutline.Directory;
Directoryoutlin.Directory := ExpandFilePath(Path);
而后调用RmDir过程:
RmDir(Path);
第二个问题的解决却颇为费神。因为DirectoryOutline是Delphi提供的示例部件,没有Help文件支持。通过试验发现:只有当DirectoryOutline的Drive属性改变时,才重新从相应驱动器读取目录。而且它基本上是只读的,除非清除( Clear) 它,象Add、Delete这些方法对它都是无效的。
我曾经考虑过一个笨拙的方法,那就是先改变当前驱动器而后再改回来。但这种方法一方面速度无法忍受,另一方面当只存在一个驱动器可用时会导致系统崩溃。
正当我一筹莫展时,突然想到:DirectoryOutline是一个Sample部件,Delphi 提供了它的源代码。而当我分析了它的源代码后,我知道应该做什么了,那就是为DirectoryOutline增添一个Reset方法!
6.7.3 为部件增添一个方法
严格地说,我们所做的工作属于创建一个新部件。但因为我们有源代码,所以不必从DirectoryOutline继承而是直接修改它。这样我们可以省去与创建部件有关的许多繁琐工作。对创建新部件感兴趣的读者可阅读本书第三编的有关章节。
在Delphi IDE中打开DirectoryOutline的源文件后:
1.把库单元名改为DirPlus,类名改为TDirectoryOutlinePlus,表明这是DirectoryOutline的增强版。而后存入另一个目录中;
2.添加一个公有方法Reset。这一方法的作用是重新读取当前驱动器的目录。程序清单如下。
procedure TDirectoryOutlinePlus.Reset;
begin
ChDir(FDrive + ':');
GetDir(0, FDirectory);
FDirectory := ForceCase(FDirectory);
if not (csLoading in ComponentState) then BuildTree;
end;
读者也许被这段代码弄糊涂了。由于篇幅所限,而且涉及到许多自定义部件开发的内容,我们也不准备去详细解释它。假如读者想彻底搞懂它,我建议先看一下本书第三编有关自定义部件开发的内容,而后再对照原DirectoryOutline的源代码进行分析。
3.编译成一个库文件DirPlus.tpu;
4.把DirPlus加入部件的Samples页中。
如何添加一个部件见第三编有关章节的介绍。
当增强的目录树准备好以后,必须修改我们的子窗口设计,但却不必亲自修改源代码。
1.删除子窗口中的TDirectoryOutline类部件DirectoryOutline。此时FileList占据了整个客户区;
2.把FileList的Align属改为None,并留出左边的空白供放部件用;
3.在窗口左部加入TDirectoryOutlinPlus类的部件DirectoryOutline;
4.把DirectoryOutline的Align属性改为Left,FileList的Align属性还原为Client;
5.在DirectoryOutline的事件OnChange列表中选取DirectoryOutlineChange,即原DirectoryOutline的处理过程。
以上工作的最终目标是实现目录创建、删除后屏幕的正确显示。这只需要调用DirectoryOutline的Reset方法即可。
目录删除过程的实现代码如下。
procedure TFMForm.DeleteDirectory1Click(Sender: TObject);
var
path: String;
k: Integer;
begin
{$I-}
path := DirectoryOutline.Directory;
DirectoryOutline.Directory := ExtractFilePath(Path);
if MessageDlg('Delete ' + path + '?', mtConfirmation,[mbYes, mbNo], 0) = idYes then
RmDir(path);
if IOResult <> 0 then
MessageDlg(' Cannot remove directory! The path might not'+
'exist,non-empty or is the current logged directory.',mtWarning,[mbOk], 0)
else
DirectoryOutline.Reset;
end;
修改后的目录创建过程如下。
procedure TFMForm.CreateDirectory1Click(Sender: TObject);
var
NewDir: TNewDir;
Dir: String;
begin
{$I-}
NewDir := TNewDir.Create(self);
with NewDir do
begin
CurrentDir.Caption := DirectoryOutline.Directory;
if (ShowModal <> idCancel) and (DirName.Text <> '') then
Dir := ExpandFileName(DirName.text);
end;
MkDir(Dir);
if IOResult <> 0 then
MessageDlg('Cannot Create directory', mtWarning, [mbOk], 0)
else
DirectoryOutline.Reset;
end;
当完成了这些工作,把程序重新编译、运行后,可以发现我们所希望实现的功能完全实现了!同时,我们有了一个更好的目录树部件。
6.4.7.4 改变当前目录
改变当前目录的实现非常简单,只要修改DirectoryOutline的Directory属性。但需注意的是:当改变后目录所在驱动器也发生变化时应相应修改DriveTabSet的当前值。由于驱动器名与DriveTabSet的索引属性TabIndex之间并没有确定的对应关系,因而需要通过一个循环进行查找匹配。
Change Directory的菜单事件处理过程是FileChange,即与文件的移动、拷贝、更名共用一个事件处理过程。详细情况请读者参看(6.4.5.3)中的介绍。
改变当前目录的实现如下。
procedure TFMForm.ChangeDirectory(Todir: String);
var
i: Integer;
begin
{$I-}
ChDir(ToDir);
if IOResult <> 0 then
MessageDlg('Cannot find directory', mtWarning, [mbOk], 0)
else
begin
with DirectoryOutline do
begin
Directory := ToDir;
Refresh;
if DriveTabSet.Tabs[DriveTabSet.TabIndex][1]<>drive then
for I := 1 to 25 do
if DriveTabSet.Tabs[i][1] = drive then
begin
DriveTabSet.TabIndex := i;
Exit;
end;
end;
end;
end;
6.4.8 一些问题的处理
6.4.8.1 子窗口的标题
Windows的文件管理器是我们设计的楷模,在子窗口显示标题上也不例外。我们把当前目录加上文件的类型作为子窗口的标题。
过程CreateCaption用于生成子窗口的标题。
procedure TFMForm.CreateCaption;
var
Cap: String;
begin
Cap := DirectoryOutline.Directory;
Cap := cap+'\'+FileList.mask;
Caption := Cap;
end;
当前目录或文件显示类型发生变化时改变子窗口的标题。如DirectoryOutline的Change事件处理过程和ViewType菜单项的Click事件处理过程就调用了该过程。
6.4.8.2 状态条的显示
状态条用于显示当前目录和当前选中文件。它们的值在DirectoryOutline 和FileList的Change事件处理过程中修改。
DirectoryOutline和FileList最终的Change事件处理过程如下:
procedure TFMForm.DirectoryOutlineChange(Sender: TObject);
begin
CreateCaption;
FileList.clear;
FileList.Directory := DirectoryOutline.Directory;
FileList.Update;
FileManager.DirectoryPanel.Caption := DirectoryOutline.Directory;
end;
procedure TFMForm.FileListChange(Sender: TObject);
begin
with FileList do
begin
if (ItemIndex >= 0) and (Not HasAttr(FileName,faDirectory)) then
begin
TheFileName := FileName;
FileManager.FilePanel.Caption :=
Format('%s, %d bytes', [TheFileName, GetFileSize(TheFileName)]);
end
else
FileManager.FilePanel.Caption := '';
end;
end;
6.4.8.3 版本信息
当用户单击主窗口的Help|About菜单项时将弹出一个About对话框,用于显示版本信息(如图6.13)。
这一对话框是用Delphi提供的模板做的。
6.4.8.4 菜单项的变灰与使能
File菜单中定义的文件管理功能只有当活动焦点在FileList(即有当前选中文件)时才起作用。否则所有菜单项应变灰,以免导致系统崩溃。
这一功能在File菜单的Click事件处理过程中实现。这一点并不很容易被人想到,希望读者能从中受到启发。
procedure TFMForm.File1Click(Sender: TObject);
var
FileSelected: Boolean;
begin
FileSelected := FileList.ItemIndex >= 0;
Open1.Enabled := FileSelected;
Delete1.Enabled := FileSelected;
Copy1.Enabled := FileSelected;
Move1.Enabled := FileSelected;
Rename1.Enabled := FileSelected;
Properties1.Enabled := FileSelected;
end;
判断是否有文件被选中是通过检测ItemIndex属性是否大于等于0来实现的。
FileSelected := FileList.ItemIndex >= 0;
6.4.8.5 可重用的文件处理模块
库单元fmxutils是一个代码库,提供了若干文件处理模块。这些模块除在本程序中使用外,读者可以在其它应用程序中直接调用,而且不必重新编译,只要在Uses子句中包含即可。从中我们可以体会到,Delphi 以库单元为中心的程序组织方式提供了一种较完善的代码重用机制。
6.4.9 小结
文件管理器是一个较为综合的例程,使用到了绝大部分以文件名、文件句柄以及其它参数(除文件变量)为操作对象的文件管理过程/函数,同时也提供了一些程序设计开发的思想。我们的介绍是以程序功能模块来组织的,我建议读者在学习并试图自己建立这一程序时采用同样的方法。(6.4.8)中的内容或许是一开始就应了解的,但其它完全可以按顺序逐步地扩充,最后得到一个完整的程序。这一例程在后边的拖放操作和异常处理等章节中还要用到。读者可以以此为基础进一步完善它,使它真正成为一个完全实用的程序。
文件管理是在开发一个高级的Windows程序中不可避免的要涉及到的问题。本章介绍的思路和方法将为读者成为一个熟练的程序员奠定基础。