第十三章 剖析几个数据库应用程序 前面已经详细讲述了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;