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


    第十三章 剖析几个数据库应用程序


      前面已经详细讲述了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.Array所示。
      这个窗口演示了怎样处理事务。首先,要调用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(1ArrayArray4, 0Array, 30)) + );
    ComboBox1.Items.Add(Country = US and LastInvoiceDate >  +DateToStr(EncodeDate(1ArrayArray4, 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.Array 一个复杂的数据库应用程序
      这一节介绍一个复杂的数据库应用程序,项目名称叫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(Array5, 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.1Array所示的对话框,让用户选择首尾日期。
      图13.1Array 选择首尾日期
      当用户选择了首尾日期并单击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;

  • 相关阅读:
    php中的闭包和匿名函数
    php魔术常量
    SQL查询:存在一个表而不在另一个表中的数据
    php发送http请求的几种方式
    php高级工程师面试题
    php中include、require、include_once、require_once的区别
    gitignore规则基础
    Google地图数据算法
    【营销】包子和星座
    【GPS】 数据围栏
  • 原文地址:https://www.cnblogs.com/m0488/p/3304608.html
Copyright © 2020-2023  润新知