1.3 第二个程序,来点数据保存的代码
在上一节中我们已完成了一个十分简单的MDI程序,但这个程序除了能打开和关闭外就什么也干不了,现在我们想象一下实际的情况,一个MIS的程序很多时候都需要保存用户录入的数据到数据库中去,而且为了保证这些数据能不被遗留的保存,程序一般会提示用户。现在我们在第二个程序中加入这些代码,当然为了程序的简单,我们暂时还是不讨论连接数据库的情况,我们用一个外置的文本文件(dataa.txt)代替数据库。
在FormA上添加一个memo和一个button控件,并添加以下的代码
……
private
FMemoChange : boolean;
FDataFile : string;
function LoadData : boolean;
function SaveData : boolean;
function NeedSave : boolean;
{ Private declarations }
public
{ Public declarations }
end;
var
frmChildA: TfrmChildA;
implementation
{$R *.dfm}
procedure TfrmChildA.btnSaveClick(Sender: TObject);
begin
SaveData;
end;
procedure TfrmChildA.FormClose(Sender: TObject; var Action: TCloseAction);
var
kk: Integer;
begin
Action := caFree;
if NeedSave then
begin
kk := MessageBox(WindowHandle, PChar(Caption + '需要保存吗?'),
PChar( 'MDI Tutorial'),
MB_YESNOCANCEL + MB_ICONQUESTION + MB_DEFBUTTON1);
if kk = IDCancel then
Action := caNone
else if kk = IDYes then
begin
if not self.SaveData then
begin
Action := caNone;
end
end;
end;
end;
procedure TfrmChildA.FormCreate(Sender: TObject);
begin
FDataFile := ExtractFilePath(Application.ExeName)+ '/dataa.txt';
self.LoadData;
FMemoChange := false;
end;
procedure TfrmChildA.FormDestroy(Sender: TObject);
begin
frmChildA := nil;
end;
function TfrmChildA.LoadData: boolean;
begin
result := true;
memo1.Lines.LoadFromFile(FDataFile);
end;
procedure TfrmChildA.Memo1Change(Sender: TObject);
begin
FMemoChange := true;
end;
function TfrmChildA.NeedSave: boolean;
begin
result := FMemoChange ;
end;
function TfrmChildA.SaveData: boolean;
begin
result := true;
memo1.Lines.SaveToFile(FDataFile);
FMemoChange := false;
end;
在MainForm上添加如下的代码
procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if frmChildA <> nil then
begin
frmChildA.Close;
if frmChildA <> nil then Action := caNone;
end;
end;
红色的为新加的代码,先说明一下这些代码的作用:
当用户创建FormA时,程序会读取目录下的DataA.txt文件,并将其内容加载到memo1中,用户可修改memo1中的内容,修改后可按save按钮保存。另外,如果用户修改了内容但忘记保存就退出的话,程序会弹出一个是否保存的提示。程序的代码比较简单,也十分易懂。如果是要保存在真正的数据库中的话,那么只要替换SaveData, LoadData及NeedSave的内容即可。在FormB中我们也可以增加类似的代码来完成相应的功能。
现在我们从重新来考虑一下整个程序的结构,我们如果新加入一个Form的话,要为每一个Form准备一个变量,及一段退出保存的询问代码,而在主窗体中要添加创建该窗体及为每一个创建的子窗体判断是否需要保存的代码。如果整个程序只是两三个子窗体的话,那么按上面的写法并无不可,但如果窗体太多的话(一般的MIS起码都有十到二十个不同的子窗体)。那么整个程序类同的代码就会十分多,但这些类同的代码却不能简单的通过一、两个公共函数来解决问题,那么我们应该怎样办好呢?
1.4 创建子窗体的父窗体
Delphi是一种面向对象的语言,像其它语言一样支持类的继承,窗体也是一个类,因此也支持这种机制,只是可能平时我们用得不多而已。
我们IDE中创建一个新的窗体,将其保存为ChildFormDefine.pas,并将该窗体命名为CustomChildForm,同时与FormA相似,我们要设置这个Form的一系列属性,如字体,FormStyle为fsMDIChild,WindowState值为wsMaximized等等MDI子窗应有的属性值。同时要将MDI_Tutorial中的Application.CreateForm(TCustomChildForm, CustomChildForm);删除。下面是ChildFormDefine.pas的代码:
……
type
TCustomChildForm = class(TForm)
procedure FormClose(Sender: TObject; var Action: TCloseAction);
public
function LoadData : boolean; virtual;
function SaveData : boolean; virtual;
function NeedSave : boolean; virtual;
end;
var
CustomChildForm: TCustomChildForm;
implementation
uses
MainForm;
{$R *.dfm}
procedure TCustomChildForm.FormClose(Sender: TObject; var Action: TCloseAction);
var
kk: Integer;
begin
Action := caFree;
if NeedSave then
begin
kk := MessageBox(WindowHandle, PChar(Caption + '需要保存吗?'),
PChar( 'MDI Tutorial'),
MB_YESNOCANCEL + MB_ICONQUESTION + MB_DEFBUTTON1);
if kk = IDCancel then
Action := caNone
else if kk = IDYes then
begin
if not self.SaveData then
begin
Action := caNone;
end
end;
end;
if Action = caFree then
frmMain.RemoveMDIChild(self);
end;
function TCustomChildForm.LoadData: boolean;
begin
result := true;
end;
function TCustomChildForm.NeedSave: boolean;
begin
result := true;
end;
function TCustomChildForm.SaveData: boolean;
begin
result := true;
end;
……
这个CustomChildForm中的代码与FormA的代码差不多,但因为这个是父类窗体,所以它不会保存任何的数据,所有与数据相关的函数都定义成virtual,以方便它的子类能override它们。另外,我们在FormClose的响应事件中多加了一个语句
if Action = caFree then
frmMain.RemoveMDIChild(self);
这句的作用是将窗体从窗体列表中删除。
以下列出MainForm.pas的代码:
unit MainForm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Menus, ChildFormDefine;
type
TMDIChildListItem = class
aForm : TCustomChildForm;
aKey : TObject;
end;
TfrmMain = class(TForm)
MainMenu1: TMainMenu;
MenuTest1: TMenuItem;
mnFormA: TMenuItem;
mnFormD: TMenuItem;
mnFormC: TMenuItem;
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure mnFormCClick(Sender: TObject);
procedure mnFormDClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FMDIChildList : TList;
function GetMDIChildItems(index : integer) : TMDIChildListItem;
procedure AddMDIChildList (aKey : TObject; aForm : TCustomChildForm);
function SearchMDIChild(aKey : TObject) : TCustomChildForm;
{ Private declarations }
public
procedure RemoveMDIChild(aForm : TCustomChildForm);
{ Public declarations }
end;
var
frmMain: TfrmMain;
implementation
uses
ChildFormC, ChildFormD;
{$R *.dfm}
procedure TfrmMain.mnFormCClick(Sender: TObject);
var
frm : TCustomChildForm; //注二
begin
frm := SearchMDIChild(Sender);
if frm = nil then
begin
frm := TfrmChildC.Create(Application);
AddMDIChildList(Sender, frm);
end;
frm.Show;
end;
procedure TfrmMain.mnFormDClick(Sender: TObject);
var
frm : TCustomChildForm;
begin
frm := SearchMDIChild(Sender);
if frm = nil then
begin
frm := TfrmChildD.Create(Application);
AddMDIChildList(Sender, frm);
end;
frm.Show;
end;
procedure TfrmMain.RemoveMDIChild(aForm: TCustomChildForm);
var
i : integer;
afi : TMDIChildListItem;
begin
for i := 0 to self.FMDIChildList.Count - 1 do
begin
afi := GetMDIChildItems(i);
if aForm = afi.aForm then
begin
self.FMDIChildList.Remove(afi);
afi.Free;
break;
end;
end;
end;
function TfrmMain.SearchMDIChild(aKey: TObject): TCustomChildForm;
var
i : integer;
afi : TMDIChildListItem;
begin
result := nil;
for i := 0 to self.FMDIChildList.Count - 1 do
begin
afi := GetMDIChildItems(i);
if aKey = afi.aKey then
begin
result := afi.aForm;
break;
end;
end;
end;
procedure TfrmMain.AddMDIChildList(aKey : TObject; aForm : TCustomChildForm);
var
afi : TMDIChildListItem;
begin
afi := TMDIChildListItem.Create;
afi.aKey := aKey;
afi.aForm := aForm;
self.FMDIChildList.Add(afi);
end;
procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
var
i : integer;
begin
while FMDIChildList.Count > 0 do //注1
begin
i := FMDIChildList.Count;
GetMDIChildItems(0).aForm.Close;
if i = FMDIChildList.Count then
begin
Action := caNone;
exit;
end;
end;
end;
procedure TfrmMain.FormCreate(Sender: TObject);
begin
FMDIChildList := TList.Create;
end;
procedure TfrmMain.FormDestroy(Sender: TObject);
var
i : integer;
begin
for i := 0 to FMDIChildList.Count - 1 do
GetMDIChildItems(i).Free;
FMDIChildList.Free;
end;
function TfrmMain.GetMDIChildItems(index: integer): TMDIChildListItem;
begin
result := FMDIChildList.Items[index];
end;
end.
这个MainForm.pas与原来的版本大不相同,最主要增加的是一个子窗体列表管理的功能。工作原理是这样的:当用户点击菜单时,系统会主动调用mnFormXClick(Sender: TObject)这个函数,其中Sender就是这个菜单项的地址(每一个菜单项的地址都是唯一的),我们利用这个地址作为键值,通过SearchMDIChild(aKey: TObject)函数去查找一张子窗体列表,如果在列表中有这个项目,那么意味着这个窗体已经被创建过,我们只需要调用Show方法即可。如果找不到相应的键值,那么就创建一个窗体再Show出来,并将该窗体和对应的菜单地址值加入到列表中去,以便下次查找。
当子窗体被用户关闭的时候,子窗体会先根据NeedSave的情况去判断是否需要保存,如需要保存,那么就会调用SaveData方法,接着在窗体被Free掉之前,调用主窗体的方法frmMain.RemoveMDIChild(self);将自己从窗体列表中删除。
如果用户直接关闭的是主窗体的话,那么会通过(注一)中的代码,通过一个循环,每次把列表中的第一个窗体取出来,并试图关闭,如果能关闭成功,那么就会继续往下取;如果某子窗体不能关闭,那么就会停下来。
现在,我们有了主体窗体和子窗体的父窗体,那么让我们再创建两个真正的子窗体。我们通过File->New->Other->Inheritable Items->CustomChildForm,就可以创建出窗体了,创建的这两个新窗体,我们也要像上一节讲的那样要在MDI_Tutorial.dpr中去除自动创建窗体的代码,同时在主窗体中新增相应的菜单项,并在每一个菜单响应函数中加入(注二)中的代码。
子窗体中就不用加任何控制代码了,当然,为了保证显示的效果,我们可以在子窗体中加入一个TMemo,然后像上节中的例子一样,加上读取和保存的代码:
unit ChildFormC;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ChildFormDefine, StdCtrls;
type
TfrmChildC = class(TCustomChildForm)
Memo1: TMemo;
btnSave: TButton;
procedure FormCreate(Sender: TObject);
procedure btnSaveClick(Sender: TObject);
procedure Memo1Change(Sender: TObject);
private
FMemoChange : boolean;
FDataFile : string;
public
function LoadData : boolean; override;
function SaveData : boolean; override;
function NeedSave : boolean; override;
end;
var
frmChildC: TfrmChildC;
implementation
{$R *.dfm}
procedure TfrmChildC.btnSaveClick(Sender: TObject);
begin
inherited;
SaveData;
end;
procedure TfrmChildC.FormCreate(Sender: TObject);
begin
inherited;
FDataFile := ExtractFilePath(Application.ExeName)+ '/dataa.txt';
self.LoadData;
FMemoChange := false;
end;
function TfrmChildC.LoadData: boolean;
begin
result := true;
memo1.Lines.LoadFromFile(FDataFile);
end;
procedure TfrmChildC.Memo1Change(Sender: TObject);
begin
inherited;
FMemoChange := true;
end;
function TfrmChildC.NeedSave: boolean;
begin
result := FMemoChange ;
end;
function TfrmChildC.SaveData: boolean;
begin
result := true;
memo1.Lines.SaveToFile(FDataFile);
FMemoChange := false;
end;
end.
至此,我们就完成了第二个程序了,在这个程序中,我们简化了子窗体的中的一些公共控制代码,保证子窗体中的代码能尽量保持纯洁(没有与业务逻辑无关的其它控制代码)。但我们也为了这个“纯洁性”付出了不少的代价,主窗体中的增加的代码十分多,而且每调用任一个子窗体都要加一大段代码,这段代码还能简化吗?我们将在下一节去解决。