ost := 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”按钮,就打开一个对话框让用户设置过滤条件。这个对