`
v5qqcom
  • 浏览: 1289314 次
文章分类
社区版块
存档分类
最新评论

多层数据库开发十三:剖析几个数据库应用程序

 
阅读更多
第十三章 剖析几个数据库应用程序
  前面已经详细讲述了Delphi 4的数据库编程技术。为了使读者能够透彻地理解有关编程技术并灵活运用,我们把Delphi 4的几个示范程序拿出来加以剖析,这些示范程序都编得非常有技巧。要说明的是,剖析程序时我们可能会忽略掉一些与主题无关的细节。
13.1 一个后台查询的示范程序
  这一节详细剖析一个后台查询的示范程序,项目名称叫Bkquery,它可以在C:/Program Files/Borland/Delphi4/Demos/Db/Bkquery目录中找到。它的主窗体如图13.1所示。
  图13.1 Bkquery的主窗体
  我们先从处理窗体的OnCreate事件的句柄开始,因为它是应用程序的起点。Procedure TAdhocForm. FormCreate(Sender: TObject);
Procedure CreateInitialIni;
  Const 
  VeryInefficientName = 'IB:
  Very Inefficient Query';
  VeryInefficientQuery ='select EMP_NO, Avg(Salary) as Salary/n'+' from employee, employee, employee/n' +'
 group by EMP_NO';
  AmountDueName = 'DB: Amount Due By Customer';
  AmountDueByCustomer ='select Company, Sum(ItemsTotal) - Sum(AmountPaid) as AmountDue/n' +'
  from customer, orders/n' +'
  where Customer.CustNo = Orders.CustNo/n' + ' group by Company';
  Begin
  With SavedQueries Do
   Begin
    WriteString(VeryInefficientName, 'Query', VeryInefficientQuery);
    WriteString(VeryInefficientName, 'Alias', 'IBLOCAL');
    WriteString(VeryInefficientName, 'Name', 'SYSDBA');
   SavedQueryCombo.Items.Add(VeryInefficientName);
   WriteString(AmountDueName, 'Query',  AmountDueByCustomer);
    WriteString(AmountDueName, 'Alias', 'DBDEMOS');
    WriteString(AmountDueName, 'Name', '');
    SavedQueryCombo.Items.Add(AmountDueName);
   End;
  End;
Begin
  Session.GetAliasNames(AliasCombo.Items);
  SavedQueries := TIniFile.Create('BKQUERY.INI');
  SavedQueries.ReadSections(SavedQueryCombo.Items);
  If SavedQueryCombo.Items.Count <= 0 then CreateInitialIni;
   SavedQueryCombo.ItemIndex := 0;
   QueryName := SavedQueryCombo.Items[0];
   Unmodify;ReadQuery;
End;
  FormCreate主要做了这么几件事情:首先,它调用TSession的GetAliasNames函数把所有已定义的BDE别名放到一个字符串列表中,实际上就是填充图13.1中的“Database Alias”框。接着,创建了一个TIniFile类型的对象实例,并指定文件名是BKQUERY.INI。如果这个文件现在还不存在的话,就需要调用CreateInitialIni去创建一个文件。至于怎样写.INI文件,这不是本章要讨论的主题。最后,调用ReadQuery把文件中保存的有关参数读出来。
  ReadQuery函数是这样定义的:
Procedure TAdhocForm.ReadQuery;
Begin
If not CheckModified then Exit;
With SavedQueries Do
Begin
QueryName := SavedQueryCombo.Items[SavedQueryCombo.ItemIndex];
QueryEdit.Text := IniStrToStr(ReadString(QueryName, 'Query', ''));
AliasCombo.Text := ReadString(QueryName, 'Alias', '');
NameEdit.Text := ReadString(QueryName, 'Name', '');
End;
Unmodify;
If Showing thenIf NameEdit.Text <> '' then PasswordEdit.SetFocus else
QueryEdit.SetFocus;
End;
  当用户单击“Execute”按钮,程序就调用BackgroundQuery在后台执行查询。Procedure TAdhocForm.ExecuteBtnClick(Sender: TObject);
Begin
BackgroundQuery(QueryName, AliasCombo.Text, NameEdit.Text, PasswordEdit.Text,QueryEdit.Text);
BringToFront;
End;
  BackgroundQuery是在另一个叫ResItFrm的单元中定义的,后面将重点介绍这个过程。当用户单击“New”按钮,程序就把窗体上的一些窗口重新初始化。
Procedure TAdhocForm.NewBtnClick(Sender: TObject);
Function UniqueName: string;
var
I: Integer;
Begin
I := 1;
Repeat
Result := Format('Query%d', [I]);
Until
SavedQueryCombo.Items.IndexOf(Result) < 0;
End;
Begin
AliasCombo.Text := 'DBDEMOS';
NameEdit.Text := '';
PasswordEdit.Text := '';
QueryEdit.Text := '';QueryEdit.SetFocus;
QueryName := UniqueName;
SavedQueryCombo.ItemIndex := -1;
Unnamed := True;
End;
  当用户单击“Save”按钮,程序就调用SaveQuery函数把当前有关参数保存到.INI文件中。
Procedure TAdhocForm.SaveBtnClick(Sender: TObject);
Begin
SaveQuery;
End;
  而SaveQuery是这样定义的:
Procedure TAdhocForm.SaveQuery;
Begin
If Unnamed then SaveQueryAs
Else
With SavedQueries Do
Begin
WriteString(QueryName, 'Query', StrToIniStr(QueryEdit.Text));
WriteString(QueryName, 'Alias', AliasCombo.Text);
WriteString(QueryName, 'Name', NameEdit.Text);Unmodify;
End;
End;
  当用户单击“Save As”按钮,程序调用SaveQueryAs函数以另一个名称保存有关参数。
Procedure TAdhocForm.SaveAsBtnClick(Sender: TObject);
Begin
SaveQueryAs;
End;
  而SaveQueryAs是这样定义的:
Procedure TAdhocForm.SaveQueryAs;
Begin
If GetNewName(QueryName) then
Begin
Unnamed := False;
SaveQuery;
With SavedQueryCombo, Items Do
Begin
If IndexOf(QueryName) < 0 then Add(QueryName);
ItemIndex := IndexOf(QueryName);
End;
End;
End;
  其中,GetNewName是在一个叫SaveQAs的单元中定义的,它将打开如图13.2所示的对话框,让用户输入一个文件名。图13.2 指定另一个文件名此外,程序还处理了SavedQueryCombo框的OnChange事件:
Procedure TAdhocForm.SavedQueryComboChange(Sender: TObject);
Begin
ReadQuery;
End;
  所谓后台查询,实际上是运用多线程的编程技术,使查询在一个专门的线程中进行。为此,首先要以TThread为基类声明一个线程对象:
TypeTQueryThread = Class(TThread)PrivateQueryForm: TQueryForm;
MessageText: string;
Procedure ConnectQuery;
Procedure DisplayMessage;
ProtectedProcedure Execute;
override;
PublicConstructor Create(AQueryForm: TQueryForm);
End;
  我们先看线程对象是怎样创建的:
Constructor TQueryThread.Create(AQueryForm: TQueryForm);
Begin
QueryForm := AQueryForm;
FreeOnTerminate := True;
Inherited Create(False);
End;
  当用户单击“Execute”按钮,程序就调用BackgroundQuery函数在后台执行查询。BackgroundQuery是这样定义的:
Procedure BackgroundQuery(const QueryName, Alias, User, Password, QueryText: string);
var
QueryForm: TQueryForm;
Begin
QueryForm := TQueryForm.Create(Application);
With QueryForm, Database Do
Begin
Caption := QueryName;
QueryLabel.Caption := QueryText;
Show;
AliasName := Alias;
Params.Values['USER'] := User;
Params.Values['PASSWORD'] := Password;
Query.Sql.Text := QueryText;
End;
TQueryThread.Create(QueryForm);
End;
  BackgroundQuery主要做了三件事情,一是动态创建和显示一个窗体(TQueryForm),因为要用这个窗体显示查询结果。二是把传递过来的参数分别赋给TDadabase构件的AliasName、Params以及TQuery构件的SQL属性。三是创建线程对象的实例。由于线程对象的FreeOnTerminate属性设为True,所以用不着专门去删除线程对象。
  好,现在让我们看看这个程序最关键的代码,即线程对象的Execute函数:
Procedure TQueryThread.Execute;
varUniqueNumber: Integer;
Begin
Try
With QueryForm Do
Begin
UniqueNumber := GetUniqueNumber;
Session.SessionName := Format('%s%x', [Session.Name, UniqueNumber]);
Database.SessionName := Session.SessionName;
Database.DatabaseName:=Format('%s%x',[Database.Name,UniqueNumber]);
Query.SessionName := Database.SessionName;
Query.DatabaseName := Database.DatabaseName;
Query.Open;
Synchronize(ConnectQuery);MessageText := 'Query openned';
Synchronize(DisplayMessage);
End;
Except
On E: Exception Do
Begin
MessageText := Format('%s: %s.', [E.ClassName, E.Message]);
Synchronize(DisplayMessage);
End;
End;
End;
  由于这是个多线程的数据库应用程序,因此,需要显式地使用TSession构件,而且要保证每个线程所使用的BDE会话期对象是唯一的。所以,程序首先调用GetUniqueNumber来获得一个唯一的序号。同样,对于TDatabase构件来说,也有类似的问题。
  Execute通过Synchronize让主线程去执行ConnectQuery、DisplayMessage等方法,这是因为ConnectQuery、DisplayMessage都需要与VCL打交道,必须用Synchronize作外套。
13.2 一个缓存更新的示范程序
  这一节详细剖析一个缓存更新的示范程序,项目名称叫Cache,它可以在C:/Program Files/Borland/Delphi4/Demos/Db/Cacheup目录中找到。它的主窗体如图13.3所示。
  图13.3 Cache的主窗体
  主窗体上有一个“Cached Updates”复选框,如果选中此复选框,表示使用缓存更新技术。否则,表示不使用缓存更新技术,当用户修改了数据后,数据被直接写到数据集中。
  主窗体上还有一个“Use Update SQL”复选框,如果选中这个复选框,表示使用TUpdateSQL构件来进行缓存更新。
  当用户单击“Apply Updates”按钮,就向数据库申请更新数据。
  当用户单击“Cancel Updates”按钮,所有未决的修改将被取消。
  当用户单击“Revert Record”按钮,对当前记录所作的修改将被取消。
  在“Show Records”分组框内有几个复选框,用于选择要在栅格中显示哪些记录,包括未修改的记录、修改的记录、插入的记录和删除的记录。
  当用户单击“Re-Execute Query”按钮,就重新执行查询。此外,这个示范程序还用一个计算字段来表达当前的更新状态。
  下面我们就来看看怎样实现上述功能。在介绍程序代码之前,我们先要介绍数据模块CacheData,因为几个关键的构件都是放在这个数据模块上,如图13.4所示。
  图13.4 数据模块
  数据模块上有四个构件,分别是:一个TDataSource构件,其名为CacheDS,一个TDatabase构件名为CacheDB,一个TQuery构件名为CacheQuery,一个TUpdateSQL构件名为UpdateSQL。
  TQuery构件的OnCalcFields事件是这样处理的:
Procedure TCacheData.CacheQueryCalcFields(DataSet: TDataSet);
ConstUpdateStatusStr: array[TUpdateStatus] of string = ('Unmodified', 'Modified','Inserted', 'Deleted');
Begin
If CacheQuery.CachedUpdates then
  CacheQueryUpdateStatus.Value := UpdateStatusStr[CacheQuery.UpdateStatus];
End;
  上述代码用于给计算字段CacheQueryUpdateStatus赋值,以显示当前的更新状态。TQuery构件的OnUpdateError事件是这样处理的:
Procedure TCacheData.UpdateErrorHandler(DataSet: TDataSet; E: EDatabaseError;
UpdateKind:TUpdateKind;
var UpdateAction: TUpdateAction);
Begin
UpdateAction := UpdateErrorForm.HandleError(DataSet, E, UpdateKind);
End;
  现在我们回到主窗体,从处理主窗体的OnCreate事件的句柄开始。
Procedure TCacheDemoForm. FormCreate(Sender: TObject);
Begin
FDataSet := CacheData.CacheDS.DataSet as TDBDataSet;
FDataSet.CachedUpdates := CachedUpdates.Checked;
SetControlStates(FDataSet.CachedUpdates);
FDataSet.Open;
End;
  第一行代码从TDataSource构件的DataSet属性取出当前的数据集,第二行代码是根据复选框CachedUpdates来决定数据集的CachedUpdates属性,进而再调用SetControlStates函数设置窗体上有关控件的状态,最后调用Open执行查询。SetControlStates是这样定义的:
Procedure TCacheDemoForm.SetControlStates(Enabled: Boolean);
Begin
ApplyUpdatesBtn.Enabled := True;
CancelUpdatesBtn.Enabled := True;
RevertRecordBtn.Enabled := True;
UnmodifiedCB.Enabled := True;
ModifiedCB.Enabled := True;
InsertedCB.Enabled := True;
DeletedCB.Enabled := True;
UseUpdateSQL.Enabled := True;
End;
  下面是处理一些控件的事件。首先是复选框CachedUpdates的OnClick事件:
Procedure TCacheDemoForm.ToggleUpdateMode(Sender: TObject);
Begin
FDataSet.CachedUpdates := not FDataSet.CachedUpdates;
SetControlStates(FDataSet.CachedUpdates);
End;
  复选框UseUpdateSQL的OnClick事件是这样处理的:
Procedure TCacheDemoForm.UseUpdateSQLClick(Sender: TObject);
Begin
FDataSet.Close;
If UseUpdateSQL.Checked then
  FDataSet.UpdateObject := CacheData.UpdateSQLElseFDataSet.UpdateObject := nil;
  FDataSet.Open;
End;
  当用户单击“Apply Updates”按钮,就向数据库申请更新数据。
Procedure TCacheDemoForm.ApplyUpdatesBtnClick(Sender: TObject);
Begin
FDataSet.Database.ApplyUpdates([FDataSet]);
End;
  当用户单击“Cancel Updates”按钮,所有未决的修改将被取消。
Procedure TCacheDemoForm.CancelUpdatesBtnClick(Sender: TObject);
Begin
FDataSet.CancelUpdates;
End;
  当用户单击“Revert Record”按钮,对当前记录所作的修改将被取消。
Procedure TCacheDemoForm.RevertRecordBtnClick(Sender: TObject);
Begin
FDataSet.RevertRecord;
End;
  在“Show Records”分组框内的几个复选框,它们的OnClick事件是这样处理的:
Procedure TCacheDemoForm.UpdateRecordsToShow(Sender: TObject);varUpdRecTypes : TUpdateRecordTypes;
Begin
UpdRecTypes := [];
If UnModifiedCB.Checked then
  Include(UpdRecTypes, rtUnModified);
If ModifiedCB.Checked then Include(UpdRecTypes, rtModified);
If InsertedCB.Checked then Include(UpdRecTypes, rtInserted);
If DeletedCB.Checked thenInclude(UpdRecTypes, rtDeleted);
FDataSet.UpdateRecordTypes := UpdRecTypes;
End;
  UpdateRecordsToShow 函数首先声明了一个TUpdateRecordTypes类型的变量UpdRecTypes,并把它初始化为空的集合。然后依次判断四个复选框是否选中,如选中的话,就把对应的元素包含到这个集合中,作为数据集的UpdateRecordTypes属性。
  当用户单击“Re-Execute Query”按钮,就重新执行查询。
Procedure TCacheDemoForm.ReExecuteButtonClick(Sender: TObject);
Begin
FDataSet.Close;
FDataSet.Open;
End;
  此外,在主窗体上,还有一个菜单命令叫About,此命令将调用ShowAboutDialog打开一个对话框。
  ShowAboutDialog是这样定义的:
Procedure ShowAboutDialog;
Begin
With TAboutDialog.Create(Application) Do
Try
AboutMemo.Lines.LoadFromFile(ExtractFilePath(ParamStr(0))+'ABOUT.TXT');
ShowModal;
FinallyFree;
End;
End;
13.3 一个Client/Server示范程序
   这一节详细剖析一个Client/Server示范程序,项目名称叫Csdemos,它可以在C:/Program Files/Borland/Delphi4/Demos/Db/Csdemos目录中找到。其主窗体如图13.5所示。
   图13.5 Csdemos的主窗体
  当用户单击“Show a View in action”按钮时,就打开FrmViewDemo窗口。
Procedure TFrmLauncher.BtnViewsClick(Sender: TObject);
Begin
FrmViewDemo.ShowModal;
End;
  当用户单击“Salary Change Trigger Demo”按钮时,就打开FrmTriggerDemo窗口。
Procedure TFrmLauncher.BtnTriggClick(Sender: TObject);
Begin
FrmTriggerDemo.ShowModal;
End;
  当用户单击“Query Stored Procedure Demo”按钮时,就打开FrmQueryProc窗口。
Procedure TFrmLauncher.BtnQrySPClick(Sender: TObject);
Begin
FrmQueryProc.ShowModal;
End;
  当用户单击“Executable Stored Procedure Demo”按钮时,就打开FrmExecProc窗口。
Procedure TFrmLauncher.BtnExecSPClick(Sender: TObject);
Begin
FrmExecProc.ShowModal;
End;
  当用户单击“Transaction Editing Demo”按钮时,就打开FrmTransDemo窗口。
Procedure TFrmLauncher.BtnTransClick(Sender: TObject);
Begin
FrmTransDemo.ShowModal;
End;
  下面我们详细介绍这些窗口。FrmViewDemo窗口如图13.6所示。
   图13.6 FrmViewDemo窗口
   当这个窗口弹出时,首先调用TTable构件的Open函数打开数据集。
Procedure TFrmViewDemo.FormShow(Sender: TObject);
Begin
VaryingTable.Open;
End;
  程序用两个快捷按钮来切换表格名称,其中,左边一个按钮对应于EMPLOYEE表。
Procedure TFrmViewDemo.BtnShowEmployeeClick(Sender: TObject);
Begin
ShowTable('EMPLOYEE');
End;
  右边一个按钮对应于PHONE_LIST表。
Procedure TFrmViewDemo.BtnShowPhoneListClick(Sender: TObject);
Begin
ShowTable('PHONE_LIST');
End;
  ShowTable是这样定义的:
Procedure TFrmViewDemo.ShowTable( ATable: string );
Begin
Screen.Cursor := crHourglass;
VaryingTable.DisableControls;
VaryingTable.Active := FALSE;
VaryingTable.TableName := ATable;
VaryingTable.Open;
VaryingTable.EnableControls;
Screen.Cursor := crDefault;
End;
  FrmTriggerDemo窗口如图13.7所示:
  图13.7 FrmTriggerDemo窗口
  当这个窗口弹出时,首先调用两个TTable构件的Open打开数据集。
Procedure TFrmTriggerDemo.FormShow(Sender: TObject);
Begin
DmEmployee.EmployeeTable.Open;
DmEmployee.SalaryHistoryTable.Open;
End;
  其中,DmEmployee是数据模块的名称。FrmQueryProc窗口如图13.7所示。
  图13.7 FrmQueryProc
  当这个窗口弹出时,将触发OnShow事件。这个事件是这样处理的:
Procedure TFrmQueryProc.FormShow(Sender: TObject);
Begin
DmEmployee.EmployeeTable.Open;
EmployeeSource.Enabled := True;
With EmployeeProjectsQuery Do
If not Active then Prepare;
End;
  首先调用EmployeeTable的Open打开数据集,然后把数据源EmployeeSource的Enabled属性设为True,接着调用Prepare准备查询。
  为了执行查询,程序处理了数据源EmployeeSource的OnDataChange事件:
Procedure TFrmQueryProc.EmployeeDataChange(Sender: TObject; Field: TField);
Begin
EmployeeProjectsQuery.Close;
EmployeeProjectsQuery.Params[0].AsInteger :=DmEmployee.EmployeeTableEmp_No.Value;
EmployeeProjectsQuery.Open;
WriteMsg('Employee ' + DmEmployee.EmployeeTableEmp_No.AsString +' is assigned to ' + IntToStr(EmployeeProjectsQuery.RecordCount) +' project(s).');
End;
  调用WriteMsg的目的是在状态栏上显示一个消息。WriteMsg是这样定义的:
Procedure TFrmQueryProc.WriteMsg(StrWrite: String);
Begin
StatusBar1.SimpleText := StrWrite;
End;
  最后,当这个窗口暂时隐去时,应当把数据源EmployeeSource的Enabled属性设为False:
Procedure TFrmQueryProc.FormHide(Sender: TObject);
Begin
EmployeeSource.Enabled := False;
End;
   FrmExecProc窗口如图13.8所示。
  图13.8 FrmExecProc
  当这个窗口弹出时,将触发OnShow事件。这个事件是这样处理的:
Procedure TFrmExecProc.FormShow(Sender: TObject);
Begin
DmEmployee.SalesTable.Open;
DmEmployee.CustomerTable.Open;
SalesSource.Enabled := True;
End;
  当用户在栅格中浏览记录时,将触发SalesSource的OnDataChange事件。在处理这个事件的句柄中,要判断ORDER_STATUS字段的值是否是SHIPPED,如果是,就使“Ship Order”按钮有效。
Procedure TFrmExecProc.SalesSourceDataChange(Sender: TObject; Field: TField);
Begin
If DmEmployee.SalesTable['ORDER_STATUS'] <> NULL then
  BtnShipOrder.Enabled :=AnsiCompareText(DmEmployee.SalesTable['ORDER_STATUS'],'SHIPPED')<>0;
End;
  当用户单击“Ship Order”按钮,就执行存储过程,存储过程的参数取自PO_NUMBER字段。
Procedure TFrmExecProc.BtnShipOrderClick(Sender: TObject);
Begin
With DmEmployee Do
Begin
ShipOrderProc.Params[0].AsString := SalesTable['PO_NUMBER'];
ShipOrderProc.ExecProc;
SalesTable.Refresh;
End;
End;
  FrmTransDemo窗口如图13.9所示。
  这个窗口演示了怎样处理事务。首先,要调用EmployeeDatabase(TDatabase构件)的StartTransaction开始一次新的事务。此后,对数据库的所有修改都暂时保留在缓存中,直到程序调用Commit或Rollback。
Procedure TFrmTransDemo.FormShow(Sender: TObject);
Begin
DmEmployee.EmployeeDatabase.StartTransaction;
DmEmployee.EmployeeTable.Open;
End;
  当用户单击“Commit Edits”按钮,就要向服务器提交数据。首先要访问TDatabase构件的InTransaction属性,看看当前是否正在处理事务。如果是的话,还要弹出一个对话框,让用户确认是否要提交数据。程序代码如下:
Procedure TFrmTransDemo.BtnCommitEditsClick(Sender: TObject);
Begin
If DmEmployee.EmployeeDatabase.InTransaction and(MessageDlg('Are you sure you want to commit your changes?',mtConfirmation, [mbYes, mbNo], 0) = mrYes) then
Begin
DmEmployee.EmployeeDatabase.Commit;
DmEmployee.EmployeeDatabase.StartTransaction;
DmEmployee.EmployeeTable.Refresh;
End
Else
MessageDlg('Can抰 Commit Changes:No Transaction Active',mtError, [mbOk], 0);
End;
  如果用户回答Yes的话,调用Commit向服务器提交数据。当用户单击“Undo Edits”按钮,调用Rollback取消所有的修改。
Procedure TFrmTransDemo.BtnUndoEditsClick(Sender: TObject);
Begin
If DmEmployee.EmployeeDatabase.InTransaction and(MessageDlg('Are you sure you want to undo all changes made during the ' +'current transaction?', mtConfirmation, [mbYes, mbNo], 0) = mrYes) then
Begin
DmEmployee.EmployeeDatabase.Rollback;
DmEmployee.EmployeeDatabase.StartTransaction;
DmEmployee.EmployeeTable.Refresh;
End
Else
MessageDlg('Can抰 Undo Edits: No Transaction Active', mtError, [mbOk], 0);
End;
  在窗口即将隐去的时候,也要调用Commit向服务器提交数据,因为用户可能没有单击“Commit Edits”按钮。
Procedure TFrmTransDemo.FormHide(Sender: TObject);
Begin
DmEmployee.EmployeeDatabase.Commit;
End;
13.4 一个TDBCtrlGrid构件的示范程序
  这一节详细剖析一个TDBCtrlGrid构件的示范程序,项目名称叫Ctrlgrid,它可以在C:/ Program Files/Borland/Delphi4/Demos/Db/Ctrlgrid目录中找到。它的主窗体如图13.10所示。
  我们先介绍数据模块,因为几个关键的构件在数据模块上,如图13.11所示
  可以看出,DM1上有三个TTable构件和三个TDataSource构件,这三个TTable构件分别访问Master表、Industry表和Holdings表。
  主窗体上有两个栅格,一个是用TDBGrid构件建立的栅格,另一个是用TDBCtrlGrid构件建立的栅格,这两个栅格都用同一个TDBNavigator构件来导航。
  这个程序运用了这样一个编程技巧,当用户把输入焦点移到TDBGrid构件建立的栅格中时,导航器就为TDBGrid构件建立的栅格导航;当用户把输入焦点移到TDBCtrlGrid构件建立的栅格中时,导航器就为TDBCtrlGrid构件建立的栅格导航。程序代码如下:
Procedure TFmCtrlGrid.DBGrid1Enter(Sender: TObject);
Begin
DBNavigator1.DataSource := DM1.DSMaster;
End;

Procedure TFmCtrlGrid.DBCtrlGrid1Enter(Sender: TObject);
Begin
DBNavigator1.DataSource := DM1.DSHoldings;
End;
  当主窗体弹出时,将触发OnShow事件。程序是这样处理OnShow事件的:
Procedure TFmCtrlGrid.FormShow(Sender: TObject);
Begin
DM1.CalculateTotals(Sender, nil);
End;
  其中,CalculateTotals用于计算几个数值,这些数值将显示在“InvestmentValue”框内。CalculateTotals是在数据模块DM1的单元中定义的:
Procedure TDM1.CalculateTotals(Sender: TObject; Field: TField);
var
flTotalCost, flTotalShares, flTotalValue, flDifference: Real;
strFormatSpec: string;
Begin{显示股票交易的次数}
FmCtrlGrid.lPurchase.Caption := IntToStr( tblHoldings.RecordCount );
{如果股票交易次数为0,就把“Investment Value”框内的数值清掉}
If tblHoldings.recordCount = 0 then
Begin
FmCtrlGrid.lTotalCost.Caption := '';
FmCtrlGrid.lTotalShares.Caption := '';
FmCtrlGrid.lDifference.Caption := '';
End
Else
Begin
{ 把光标设为沙漏状,因为计算数值的时间可能较长 }
Screen.Cursor := crHourglass;
{ 把数值初始化为0.0 }
flTotalCost := 0.0;
flTotalShares := 0.0;
{ 计算购买所持股票的金额 }
tblHoldings.DisableControls;
tblHoldings.First;
While not tblHoldings.eof Do
Begin
flTotalCost := flTotalCost + tblHoldingsPUR_COST.AsFloat;flTotalShares := flTotalShares + tblHoldingsSHARES.AsFloat;
tblHoldings.Next;
End;
tblHoldings.First;
tblHoldings.EnableControls;{ 计算股票的市值和赢亏 }
flTotalValue := flTotalShares * tblMasterCUR_PRICE.AsFloat;
flDifference := flTotalValue - flTotalCost;
strFormatSpec := tblMasterCUR_PRICE.DisplayFormat;
{ 显示上述数据 }
FmCtrlGrid.lTotalCost.Caption := FormatFloat( strFormatSpec, flTotalCost );
FmCtrlGrid.lTotalShares.Caption := FormatFloat( strFormatSpec, flTotalValue );
FmCtrlGrid.lDifference.Caption := FormatFloat( strFormatSpec, flDifference );
{ 如果是赚的,就以绿色显示。如果是亏的,就以红色显示 }
If flDifference > 0 then FmCtrlGrid.lDifference.Font.Color := clGreen
Else FmCtrlGrid.lDifference.Font.Color := clRed;
FmCtrlGrid.lDifference.Update;
{ 把光标恢复原状 }
Screen.Cursor := crDefault;
End;
End;
  此外,当用户选择“About”命令时,将打开About框。程序代码如下:
Procedure TFmCtrlGrid.About1Click(Sender: TObject);
Begin
With TFMAboutBox.Create(nil) Do
Try
ShowModal;
Finally
Free;
End;
End;
  当显示Holdings表的数据集打开后,就动态指定CalculateTotals作为处理dsMaster的OnDataChange事件的句柄。
Procedure TDM1.tblHoldingsAfterOpen(DataSet: TDataSet);
Begind
sMaster.OnDataChange := CalculateTotals;
End;
  此外,这个程序还演示了书签的用法。
Procedure TDM1.tblHoldingsAfterPost(DataSet: TDataSet);
var
bmCurrent : TBookmark;
Begin
With tblHoldings Do
Begin
bmCurrent := GetBookmark;
Try
CalculateTotals(nil, nil);
GotoBookmark(bmCurrent);
Finally;
FreeBookmark(bmCurrent);
End;
End;
End;
13.5 一个捕捉数据库错误的示范程序
  这一节剖析一个捕捉数据库错误的示范程序,项目名称叫Dberrors,它可以在C:/Program Files/Borland/Delphi4/Demos/Db/Dberrors目录中找到。它的主窗体如图13.11所示。
  这个程序演示了怎样捕捉数据库错误。Delphi 4用OnPostError、OnEditError和OnDeleteError事件来捕捉错误,这些错误产生于用户对数据库的操作,如修改、删除和插入记录。
  首先从它的数据模块开始。它的数据模块叫DM,如图13.12所示。
  图13.12 数据模块
  可以看出,数据模块上有三个TTable构件和三个TDataSorce构件,这三个TTable构件分别访问Customer表、Orders表和Items表。
  要说明的是,这三个表之间并不是并行的关系,而是一对多的Master/Detail关系。例如,Orders表的MasterSource属性指定必须指定为CustomerSource,而Items表的MasterSource属性必须指定为OrdersSource。因此,这些TTable构件和TDataSource构件的生成顺序(Creation Order)是很重要的,不能搞错。
  这个程序的主窗体很简单,有三个栅格(TDBGrid构件),分别显示Customer表、Orders表和Items表的数据。
  这个程序用同一个TDBNavigator构件为这三个栅格导航。因此,这个程序运用了一个小小的编程技巧,即动态地切换TDBNavigator构件的DataSource属性。程序代码如下:
Procedure TFmMain.GridOrdersEnter(Sender: TObject);
Begin
DBNavigator1.DataSource := Dm.OrdersSource;
End;
Procedure TFmMain.GridCustomersEnter(Sender: TObject);
Begin
DBNavigator1.DataSource := Dm.CustomerSource;
End;
Procedure TFmMain.GridItemsEnter(Sender: TObject);
Begin
DBNavigator1.DataSource := Dm.ItemsSource;
End;
  如果用户在Customer表中修改、插入或删除了记录,当用户要把输入焦点移到其他栅格中之前,应当调用Post把用户对数据的编辑写到数据库中。
Procedure TFmMain.GridCustomersExit(Sender: TObject);
Begin
If Dm.Customer.State in [dsEdit,dsInsert] then Dm.Customer.Post;
End;
  此外,当用户选择“About”命令时,将显示一个About框。代码如下:
Procedure TFmMain.About1Click(Sender: TObject);
var fmAboutBox : TFmAboutBox;
Begin
FmAboutBox := TFmAboutBox.Create(self);
Try
FmAboutBox.showModal;
Finally
FmAboutBox.free;
End;
End;
  下面重点分析怎样捕捉错误。凡是捕捉错误的代码都是在数据模块的单元中实现的,这也是使用数据模块的好处之一。当程序调用Post或用户单击导航器上的Post按钮,就会把用户对数据的修改写到数据库中,如果出错(可能是因为有重复的客户编号),就会触发OnPostError事件。让我们来看看Customer表是怎样处理OnPostError事件的:
Procedure TDM.CustomerPostError(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction);
Begin
If (E is EDBEngineError) then
  If (E as EDBEngineError).Errors[0].Errorcode = eKeyViol then
Begin
MessageDlg('Unable to post: Duplicate Customer ID.',mtWarning,[mbOK],0);
Abort;
End;
End;
  其中,EDBEngineError是一个处理BDE错误的异常类,可以访问它的Errors数组来获取当前的错误代码。如果错误代码是eKeyViol的话,就显示一个对话框,告诉用户不能把数据写到数据库中,因为有重复的客户编号。然后调用Abort放弃此次操作。
  在Customer表中删除记录时也有可能出错,因为被删除的客户在Orders表和Items表中还有记录,这种情况下,就会触发OnDeleteError事件。让我们来看看Customer表是怎样处理OnDeleteError事件的:
Procedure TDM.CustomerDeleteError(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction);
Begin
If (E is EDBEngineError) then
If (E as EDBEngineError).Errors[0].Errorcode = eDetailsExist then
Begin
MessageDlg('To delete this record, first delete related orders and items.',mtWarning, [mbOK], 0);
Abort;
End;
End;
  读者可能发现,处理OnDeleteError事件的方式与处理OnPostError事件的方式差不多,首先判断错误代码是否是eDetailsExist,如果是的话,表示被删除的客户在Orders表和Items表中还有记录,就显示一个对话框告诉用户:要删除这条记录,先要删除Orders表和Items表中的相关记录。然后调用Abort放弃此次操作。
  由于CustNo字段是Customer表的关键字段,当用户修改CustNo字段的值但还没有Post之前,为了防止显示Orders表和Items表的栅格出现混乱,最好调用DisableControls函数暂时禁止刷新数据,等程序调用Post或用户单击导航器上的Post按钮后,再调用EnableControls函数。
Procedure TDM.CustomerCustNoChange(Sender: TField);
Begin
Orders.DisableControls;
Items.DisableControls;
End;
  当程序调用Post或用户单击导航器上的Post按钮后,将触发AfterPost事件。程序是这样处理Customer表的AfterPost事件的:
Procedure TDM.CustomerAfterPost(DataSet: TDataSet);
Begin
Dm.Orders.Refresh;
Dm.Items.Refresh;
Dm.Orders.EnableControls;
Dm.Items.EnableControls;
End;
  对于Items表来说,处理OnPostError事件的方式与Customer表处理OnPostError事件的方式大致上是相同的:
Procedure TDM.ItemsPostError(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction);
Begin
If (E as EDBEngineError).Errors[0].Errorcode = eForeignKey then
Begin
MessageDlg('Part number is invalid', mtWarning,[mbOK],0);
Abort;
End;
End;
  Orders表是这样处理OnPostError事件的:
Procedure TDM.OrdersPostError(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction);
var iDBIError: Integer;
Begin
If (E is EDBEngineError) then
Begin
iDBIError := (E as EDBEngineError).Errors[0].Errorcode;
Case iDBIError of
eRequiredFieldMissing:
{EmpNo字段必须有值}
Begin
MessageDlg('Please provide an Employee ID', mtWarning, [mbOK], 0);
Abort;
End;
eKeyViol:
{对于Orders表来说,关键字段是OrderNo}
Begin
MessageDlg('Unable to post. Duplicate Order Number', mtWarning,[mbOK], 0);
Abort;
End;
End;
End;
End;
  由于Items表依赖于Orders表,因此,删除Orders表中的记录时也有可能出错。因此,程序处理了Orders表的OnDeleteError事件。不过,与处理Customer表的OnDeleteError事件不同的是,这里用一个对话框让用户选择是否要删除这条有“问题”的记录,如果用户回答Yes,就把Items表的记录全部删掉,然后把Action参数设为daRetry,表示等退出这个事件句柄后将重新尝试删除这条记录。如果用户回答No,就调用Abort放弃此次操作。
Procedure TDM.OrdersDeleteError(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction);
Begin
If E is EDBEngineError then
If (E as EDBEngineError).Errors[0].Errorcode = eDetailsExist then
Begin
If MessageDlg('Delete this order and related items?', mtConfirmation,
[mbYes, mbNo], 0) = mrYes then
Begin
While Items.RecordCount > 0 Do
Items.delete;Action := daRetry;
End
Else
Abort;
End;
End;
13.6 一个对数据集进行过滤的示范程序
  这一节剖析一个对数据集进行过滤的示范程序,项目名称叫Filter,它可以在C:/Program Files/Borland/Delphi4/Demos/Db/Filter目录中找到。它的主窗体如图13.13所示。
  这个示范程序演示了怎样通过修改Filter属性动态地设置过滤条件,怎样在处理OnFilterRecord事件的句柄中改变过滤条件,怎样通过TQuery构件的Datasource属性从另一个数据集中获取参数,一个栅格怎样动态地切换数据集。
  我们还是从数据模块开始,因为几个关键的构件放在数据模块上。这个程序的数据模块叫DM1,如图13.14所示。
  数据模块上有一个TTable构件叫Customer,用于访问Customer表。有一个TQuery构件叫SQLCustomer,通过SQL语句来访问Customer表,其SQL语句如下:
  SELECT * FROM "CUSTOMER.DB"
  数据模块上有一个TDataSource构件叫CustomerSource,它的DataSet属性既可以设为Customer,也可以设为SQLCustomer。
  数据模块上还有一个TQuery构件叫SQLOrders,用于查询Orders表,SQL语句如下:
  Select * From Orders Where CustNo = :CustNo
  SQLOrders的DataSource属性设为CustomerSource,表示:CustNo参数取自于Customer表的CustNo字段。主窗体上有两个栅格,上面这个栅格叫DBGrid1,下面这个栅格叫DBGrid2。
  DBGrid1的DataSource属性设为CustomerSource,而CustomerSource的DataSet属性既可以设为Customer,也可以设为SQLCustomer,这是通过“DataSet”框内的两个单选按钮来切换的。
Procedure TfmCustView.rgDataSetClick(Sender: TObject);
var
st: string;
Begin
With DM1, CustomerSource Do
Begin
If Dataset.Filtered then st := Dataset.Filter;
Case rgDataset.ItemIndex of
0: If Dataset <> SQLCustomer then Dataset := SQLCustomer;
1: If CustomerSource.Dataset <> Customer then Dataset := Customer;
End;
If st <> '' then BeginDataset.Filter := st;
Dataset.Filtered := True;
End;
End;
End;
  当用户单击“Filter Customers”按钮,就打开一个窗口让用户设置过滤条件。关于这个窗口后面再讲。
Procedure TfmCustView.SpeedButton1Click(Sender: TObject);
Begin
fmFilterFrm.Show;
End;
  DBGrid2显示Orders表的数据。用户可以通过一个复选框来选择是否要对数据集进行过滤,实际上就是修改SQLOrders的Filtered属性。
Procedure TfmCustView.cbFilterOrdersClick(Sender: TObject);
Begin
DM1.SQLOrders.Filtered := cbFilterOrders.Checked;
If cbFilterOrders.Checked then
Edit1Change(nil);
End;
  如果选中这个复选框的话,就调用Edit1Change把“Amount Paid”框内输入的数值赋值给DM1单元中的一个公共变量叫OrdersFilterAmount,至于这个变量有什么作用,后面在介绍DM1单元时会讲到的。调用Refresh将触发SQLOrders的OnFilterRecord事件。如果在调用Refresh之前用户在“AmountPaid”框内键入了非数字字符,调用Refresh会触发EConvertError异常,因此,程序用Try匛xcept结构对这段代码进行了保护。
Procedure TfmCustView.Edit1Change(Sender: TObject);
Begin
If (cbFilterOrders.checked) and (Edit1.Text <> '') then
Try
DM1.OrdersFilterAmount := StrToFloat(fmCustView.Edit1.Text);
DM1.SQLOrders.Refresh;
ExceptOn EConvertError DoRaise Exception.Create('Threshold Amount must be a number')
End
End;
  前面多次介绍了这样一个编程技巧,当一个导航器为几个数据集导航时,应当处理栅格的OnEnter事件,以便动态地切换TDBNavigator构件的DataSource属性。
Procedure TfmCustView.DBGrid1Enter(Sender: TObject);
Begin
DBNavigator1.DataSource := DBGrid1.DataSource;
End;
Procedure TfmCustView.DBGrid2Enter(Sender: TObject);
Begin
DBNavigator1.DataSource := DBGrid2.DataSource;
End;
  此外,当用户选择“About”命令时,将显示About框。代码如下:
Procedure TfmCustView.About1Click(Sender: TObject);
Begin
With TFMAboutBox.Create(nil) do
Try
ShowModal;
Finally
Free;
End;
End;
  这个程序还演示了怎样处理OnFilterRecord事件:
Procedure TDM1.SQLOrdersFilterRecord(DataSet: TDataSet; var Accept: Boolean);
Begin
Accept := SQLOrdersAmountPaid.Value >= OrdersFilterAmount;
End;
  请读者注意,由于OrdersFilterAmount是一个变量,这意味着用户只要修改这个变量的值,就能使过滤条件动态地变化。当用户单击“Filter Customers”按钮,就打开一个对话框让用户设置过滤条件。这个对话框如图13.15所示。
  最上面的“List”框是一个组合框,用于列出过去曾经输入过的过滤条件表达式。“ Condition”框是一个多行文本编辑器,用于输入过滤条件表达式。
  “Fields”框是一个列表框,用于列出Customer表中的所有字段,因为过滤条件表达式中需要用到字段。因此,程序在处理这个窗口的OnCreate事件的句柄中首先要填充这个列表框。此外,程序还在“List”框中加入了两个过滤条件。
Procedure TfmFilterFrm. FormCreate(Sender: TObject);
var
I: Integer;
Begin
For I := 0 to DM1.CustomerSource.Dataset.FieldCount - 1 do
ListBox1.Items.Add(DM1.Customer.Fields[I].FieldName);
ComboBox1.Items.Add('LastInvoiceDate >= ''' +DateToStr(EncodeDate(1994, 09, 30)) + '''');
ComboBox1.Items.Add('Country = ''US'' and LastInvoiceDate > ''' +DateToStr(EncodeDate(1994, 06, 30)) + '''');
End;
  当用户从“List”框中选择或输入一个过滤表达式,应当首先把下面的“Condition”框清空,然后把用户选择或输入的过滤表达式加到“Condition”框中。
Procedure TfmFilterFrm.ComboBox1Change(Sender: TObject);
Begin
Memo1.Lines.Clear;
Memo1.Lines.Add(ComboBox1.Text);
End;
  当用户在“Fields”框中双击一个字段,就把该字段加到“Condition”框中。
Procedure TfmFilterFrm.AddFieldName(Sender: TObject);
Begin
If Memo1.Text <> '' then
Memo1.Text := Memo1.Text + ' ';
Memo1.Text := Memo1.Text + ListBox1.Items[ListBox1.ItemIndex];
End;
  当用户在“Operators”框中双击一个运算符,就把该运算符加到“Condition”框中。
Procedure TfmFilterFrm.ListBox2DblClick(Sender: TObject);
Begin
If Memo1.Text <> '' thenMemo1.Text := Memo1.Text + ' '+ ListBox2.Items[ListBox2.ItemIndex];
End;
  由于用户有可能把过滤条件表达式分成几行写,因此,程序需要把以行为单位的字符串转换为一个字符串列表,因为Filter属性是一个TStrings对象。
Procedure TfmFilterFrm.Memo1Change(Sender: TObject);
var I: Integer;
Begin
ComboBox1.Text := Memo1.Lines[0];
For I := 1 to Memo1.Lines.Count - 1 do
ComboBox1.Text := ComboBox1.Text + ' ' + Memo1.Lines[I];
End;
  程序用两个复选框让用户设置过滤的选项。一个是“Case Sensitive”框,如果选中此框,FilterOptions属性中将包含foCaseInSensitive元素。另一个是“NoPartial Compare”框,如果选中此框,FilterOptions属性中将包含foNoPartialCompare元素。
Procedure TfmFilterFrm.cbCaseSensitiveClick(Sender: TObject);
Begin
With DM1.CustomerSource.Dataset Do
If cbCaseSensitive.checked then
FilterOptions := FilterOptions - [foCaseInSensitive]ElseFilterOptions := FilterOptions + [foCaseInsensitive];
End;
Procedure TfmFilterFrm.cbNoPartialCompareClick(Sender: TObject);
Begin
With DM1.CustomerSource.Dataset Do
If cbNoPartialCompare.checked then
FilterOptions := FilterOptions + [foNoPartialCompare]
Else
FilterOptions := FilterOptions - [foNoPartialCompare];
End;
  当用户输入了过滤条件表达式并且设置了过滤选项,就可以单击“Apply”按钮把过滤条件表达式赋给Filter属性:
Procedure TfmFilterFrm.ApplyFilter(Sender: TObject);
Begin
With DM1.CustomerSource.Dataset Do
Begin
If ComboBox1.Text <> '' then
Begin
Filter := ComboBox1.Text;
Filtered := True;
fmCustView.Caption := 'Customers - Filtered';
End
Else
Begin
Filter := '';
Filtered := False;
fmCustView.Caption := 'Customers - Unfiltered'
End;
End;
End;
  如果用户单击“Clear”按钮,就把“Condition”框清空,并把输入的过滤条件表达式加到“List”框中。
Procedure TfmFilterFrm.SBtnClearClick(Sender: TObject);
var st: string;
Begin
Memo1.Lines.Clear;
st := ComboBox1.Text;
ComboBox1.Text := '';
If ComboBox1.Items.IndexOf(st) = -1 then ComboBox1.Items.Add(st);
End;
  当用户单击“Close”按钮,就关闭这个窗口。
Procedure TfmFilterFrm.SBtnCloseClick(Sender: TObject);
Begin
Close;
End;
13.9 一个复杂的数据库应用程序
  这一节介绍一个复杂的数据库应用程序,项目名称叫Mastapp,它可以在C:/Program Files/Borland/Delphi4/Demos/Db/ Mastapp目录中找到。它的主窗体如图13.18所示。
  图13.18 Mastapp的主窗体
  这个程序比较复杂,读者一定要对它的程序结构搞清楚。我们先介绍主窗体。我们还是从处理OnCreate事件的句柄开始,因为这是应用程序的起点。
Procedure TMainForm.FormCreate(Sender: TObject);
Begin
ClientWidth := CloseBtn.Left + CloseBtn.Width + 1;
ClientHeight := CloseBtn.Top + CloseBtn.Height;
MainPanel.Align := alClient;
Left := 0;
Top := 0;
InitRSRUN;
End;
  前面两行代码用于设置主窗口的宽度和高度。把Left属性和Top属性都设为0将使主窗口显示在屏幕的左上角。
  注意:这个示范程序有一个错误是,从Delphi 3开始已经取消了ReportSmith,因此,这里调用InitRSRUN以及InitRSRUN中调用的UpdateRSConnect都是多余的。当用户使用“File”菜单上的“New Order”命令或单击工具栏上的“NewOrder”按钮,程序将打开“Order Form”窗口,代码如下:
Procedure TMainForm.NewOrder(Sender: TObject);
Begin
EdOrderForm.Enter;
End;
  当用户使用“File”菜单上的“Print Report”命令,再选择“Customer List”,将调用PrintCustomerReport函数打印客户报表。
Procedure TMainForm.CustomerReport(Sender: TObject);
Begin
PrintCustomerReport(False);
End;
  其中,PrintCustomerReport是这样定义的:
Procedure TMainForm.PrintCustomerReport(Preview: Boolean);
Begin
With MastData.CustByLastInvQuery Do
Begin
Open;
If Preview then CustomerByInvoiceReport.Preview
Else
CustomerByInvoiceReport.Print;
Close;
End;
End;
  由于传递给Preview参数的值是False,因此,这里将打印而不是预览报表。当用户使用“File”菜单上的“Print Report”命令,再选择“Order History”,将调用PrintOrderReport函数打印定单报表。
Procedure TMainForm.OrderReport(Sender: TObject);
Begin
PrintOrderReport(False);
End;
  其中,PrintOrderReport是这样定义的:
Procedure TMainForm.PrintOrderReport(Preview: Boolean);
Const FromToHeading = 'From ''%s'' To ''%s''';
Begin
With QueryCustDlg Do
Begin
MsgLab.Caption := 'Print all orders ranging:';
If FromDate = 0 then FromDate := EncodeDate(95, 01, 01);
If ToDate = 0 then ToDate := Now;
If ShowModal = mrOk then
With MastData.OrdersByDateQuery Do
Begin
Close;
Params.ParamByName('FromDate').AsDate := FromDate;
Params.ParamByName('ToDate').AsDate := ToDate;
Open;
OrdersByDateReport.FromToHeading.Caption :=Format(FromToHeading, [DateToStr(FromDate), DateToStr(ToDate)]);
If Preview then
OrdersByDateReport.Preview
Else OrdersByDateReport.Print;
Close;
End;
End;
End;
  PrintOrderReport函数首先弹出一个如图13.19所示的对话框,让用户选择首尾日期。
  图13.19 选择首尾日期
  当用户选择了首尾日期并单击OK按钮,就预览报表,因为Preview参数是False。当用户使用“File”菜单上的“Print Report”命令,再选择“Invoice”,将调用PrintInvoiceReport函数打印发货单报表。
Procedure TMainForm.InvoiceReport(Sender: TObject);
Begin
PrintInvoiceReport(False);
End;
  其中,PrintInvoiceReport是这样定义的:
Procedure TMainForm.PrintInvoiceReport(Preview: Boolean);
Begin
If PickOrderNoDlg.ShowModal = mrOk then
If Preview then
InvoiceByOrderNoReport.Preview
Else
InvoiceByOrderNoReport.Print;
End;
  PrintInvoiceReport函数首先将弹出如图13.20所示的对话框,让用户选择定单编号。
  图13.20 选择定单编号
  当用户使用“File”菜单上的“Printer Setup”命令,将打开“打印设置”对话框。
Procedure TMainForm.PrinterSetupClick(Sender: TObject);
Begin
PrinterSetup.Execute;
End;
  当用户使用“View”菜单上的“Orders”命令或者单击工具栏上的“Browse”按钮,程序将打开“Order By Customer”窗口,代码如下:
Procedure TMainForm.BrowseCustOrd(Sender: TObject);
Begin
Case GetDateOrder(ShortDateFormat) Of
doYMD: ShortDateFormat := 'yy/mm/dd';
doMDY: ShortDateFormat := 'mm/dd/yy';
doDMY: ShortDateFormat := 'dd/mm/yy';
End;
BrCustOrdForm.Show;
End;
  BrowseCustOrd首先调用GetDateOrder函数返回日期的格式,然后弹出“OrderBy Customer”窗口。GetDateOrder函数是这样定义的:
Function GetDateOrder(const DateFormat: string): TDateOrder;
var I: Integer;
Begin
Result := doMDY;
I := 1;
While I <= Length(DateFormat) Do
Begin
Case Chr(Ord(DateFormat[I]) and $DF) of
'Y': Result := doYMD;
'M': Result := doMDY;
'D': Result := doDMY;
Else Inc(I);
Continue;
End;
Exit;
End;
Result := doMDY;
End;
  当用户使用“View”菜单上的“Parts/Inventory”命令或单击工具栏上的“Parts”按钮,程序将打开“Browse Parts”窗口,代码如下:
Procedure TMainForm.BrowseParts(Sender: TObject);
Begin
BrPartsForm.Show;
End;
  当用户使用“View”菜单上的“Stay On Top”命令,就使主窗口总是在屏幕的前端。
Procedure TMainForm.ToggleStayonTop(Sender: TObject);
Begin
With Sender as TMenuItem Do
Begin
Checked := not Checked;
If Checked then MainForm.FormStyle := fsStayOnTop
Else MainForm.FormStyle := fsNormal;
End;
End;
  请读者注意一个编程技巧,即怎样使窗口总是在屏幕前端。
  这个程序可以让用户选择用本地数据库还是远程数据库。当用户选择“View”菜单上的“Local Data(Paradox Data)”命令时,就使用本地数据库。当用户选择“View”菜单上的“Remote Data(Local Interbase)”命令时,就使用Interbase数据库。注意:选择后者时,必须保证已安装Interbase服务器并且正在运行,否则会触发异常。
Procedure TMainForm.ViewLocalClick(Sender: TObject);
Begin
CloseAllWindows;
MastData.UseLocalData;
ViewLocal.Checked := True;
Caption := Application.Title + ' (Paradox Data)';
End;

Procedure TMainForm.ViewRemoteClick(Sender: TObject);
Begin
CloseAllWindows;
MastData.UseRemoteData;
ViewRemote.Checked := True;
Caption := Application.Title + ' (Local Interbase)';
End;
  其中,UseLocalData和UseRemoteData是在数据模块的单元中定义的。在切换数据库之前必须调用CloseAllWindows关闭所有打开的窗口。CloseAllWindows是这样定义的:
Procedure TMainForm.CloseAllWindows;
var I: Integer;
F: TForm;
Begin
For I := 0 to Application.ComponentCount - 1 Do
Begin
If Application.Components[I] is TForm then
Begin
F := TForm(Application.Components[I]);
If (F <> Self) and (F.Visible) then F.Close;
End;
End;
End;
  当用户单击工具栏上的“Reports”按钮,就打开“Report Select”窗口,让用户选择要打印或预览哪个报表,代码如下:
Procedure TMainForm.ReportBtnClick(Sender: TObject);
Begin
With PickRpt Do
If ShowModal = mrOK then
Case ReportType.ItemIndex of
0: PrintCustomerReport( Preview );
1: PrintOrderReport( Preview );
2: PrintInvoiceReport( Preview );
End;
End;
分享到:
评论

相关推荐

    分布式数据库系统-复习.doc

    组合法 重构法 集中式数据库设计一般包括:需求分析,概念设计,逻辑设计和物理设计四个阶段,分 布式数据库设计除了上述四个阶段外,还需增加一些个新的阶段 ,它位于 和 之间。 分布设计 逻辑设计 物理设计 水平...

    delphi数据库-课程设计.doc

    因此,可以说应 用程序框架通过提供所有应用程序共有的东西,为用户应用程序的开发打下良好的基础 。用户所需要做的,只是在应用程序中中假如完成所需功能的代码而已。 Delphi具有以下的特性:基于窗体和面向对象的...

    分布式数据库系统复习材料.doc

    10、一个分布式数据库系统应用应该具有以下几种特点: (1)物理分布性:分布式数据库系统的数据具有物理分布性,这是与集中式数据库系统 的最大差别之一 (2)逻辑整体性:区别一个数据库系统是分散式还是分布式,...

    简单超市数据库管理系统需求分析.doc

    二、系统性能分析 1)多层结构设计 严格意义上的三层结构设计,其程序逻辑结构分为用户界面层、业务逻辑处理层和数据 存储层。本系统采用的上有三层结构进一步扩展而成的多层结构。 2)面向对象设计 在系统中将商品...

    J2EE开发使用手册.part1

    全书共分六大部分30章,分别从企业面临的问题、企业数据的表示、企业通信、常见企业服务、企业Web支持和企业应用程序支持等几个方面阐述企业问题的J2EE解决方案。本书配套光盘中的附录还分别介绍了分布式应用程序...

    J2EE开发使用手册.part3

    全书共分六大部分30章,分别从企业面临的问题、企业数据的表示、企业通信、常见企业服务、企业Web支持和企业应用程序支持等几个方面阐述企业问题的J2EE解决方案。本书配套光盘中的附录还分别介绍了分布式应用程序...

    J2EE开发使用手册.part4

    全书共分六大部分30章,分别从企业面临的问题、企业数据的表示、企业通信、常见企业服务、企业Web支持和企业应用程序支持等几个方面阐述企业问题的J2EE解决方案。本书配套光盘中的附录还分别介绍了分布式应用程序...

    J2EE开发使用手册.part2

    全书共分六大部分30章,分别从企业面临的问题、企业数据的表示、企业通信、常见企业服务、企业Web支持和企业应用程序支持等几个方面阐述企业问题的J2EE解决方案。本书配套光盘中的附录还分别介绍了分布式应用程序...

    Delphi5开发人员指南

    1.5 创建一个简单的应用程序 11 1.6 事件机制的优势在哪里 12 1.7 加速原型化 13 1.8 可扩展的组件和环境 13 1.9 IDE最重要的十点功能 13 1.10 总结 15 第2章 Object Pascal语言 16 2.1 注解 16 2.2 新的过程和函数...

    基于Delphi的仓库管理系统开发论文

    Delphi提供了各种开发工具,包括集成环境、图像编辑(Image Editor),以及各种开发数据库的应用程序,如DesktopDataBase Expert等。除此之外,还允许用户挂接其它的应用程序开发工具,如Borland公司的资源编辑器...

    asp.net知识库

    深入剖析ASP.NET组件设计]一书第三章关于ASP.NET运行原理讲述的补白 asp.net 运行机制初探(httpModule加载) 利用反射来查看对象中的私有变量 关于反射中创建类型实例的两种方法 ASP.Net应用程序的多进程模型 NET委托...

    联网收费环境下的不停车收费系统软件设计

    本文主要从以下几个方面对网络环境下电子不停车收费系统展开研究: 1、分析和研究网络环境下电子不停车收费系统的框架结构,并设计了...环境下电子不停收费系统,并详细设计系统多层C/S结构的数据库应用程序体系 结构。

    软件工程知识点

    它包含:软件定义、软件开发、软件运行维护三个时期,并可以细分为可行性研究、项目计划、需求分析、概要设计、详细设计、编码实现与单元测试、系统集成测试、系统确认验证、系统运行与维护等几个阶段。 软件定义...

    本文对几种经典的软件体系结构风格进行了具体的阐述,分析了各种风格的特点、优缺点,最后重点介绍了三层C/S软件体系结构。

    20世纪60年代中期的软件危机使得人们开始重视软件工程的研究。起初,人们把软件设计的重点放在数据结构和算法的选择上。...随着Internet的发展,一个更灵活的体系结构“三层/多层计算”体系结构应运而生。

    超级有影响力霸气的Java面试题大全文档

    创建了几个String Object? 两个 31、EJB包括(SessionBean,EntityBean)说出他们的生命周期,及如何管理事务的?  SessionBean: Stateless Session Bean 的生命周期是由容器决定的,当客户机发出请求要建立一个...

    Toad 使用快速入门

    可以自定义存储过程得模板,在新建存储过程的时候,自动生成程序的框架可以方便的调用Schema Browser,把鼠标定位于某个对象上,F4,Schema Browser打开该对象的详细描述,帮助顺利快速开发程序。支持代码自动更正,...

    HUST(华中科技大学大作业),电影评分预测.zip

    爬虫的工作流程包括以下几个关键步骤: URL收集: 爬虫从一个或多个初始URL开始,递归或迭代地发现新的URL,构建一个URL队列。这些URL可以通过链接分析、站点地图、搜索引擎等方式获取。 请求网页: 爬虫使用HTTP...

    java-ee电子商城系统课程设计.doc

    Spring 由Rod Johnson 创建,它是为了解决企业应用开发的复杂性而创建的。Spring 使用基本的JavaBean 来完成以前只可能由EJB 完成的事情。然而,Spring 的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合...

    在线考试系统设计与开发(课程设计).doc

    前者主要的缺点是维护、升级较麻烦,后者是近几年伴随Internet迅速发展而应 运而生的一种技术,在这种模式下,客户端需要一个浏览器,服务器端是Web Server ,而Web Server是与数据库和应用服务器的紧密结合,可见,...

    java 面试题 总结

    创建了几个String Object? 两个 28、设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。 以下程序使用内部类实现线程,对j增减的时候没有考虑顺序问题。 public class ThreadTest1{ ...

Global site tag (gtag.js) - Google Analytics