分享
 
 
 

多层数据库开发十四:剖析几个MIDAS示范程序

王朝delphi·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

第十四章 剖析几个MIDAS示范程序

MIDAS是Multi-Tier Distributed Application Services Suite的简称,为Delphi 4的一个关键技术。对于初学者来说,MIDAS具有相当的难度,因此,这一章详细剖析几个MIDAS示范程序,以帮助读者理解和掌握MIDAS技术。

与一般的数据库应用程序不同的是,只有当应用服务器正在运行的情况下,才能打开、编译和运行“瘦”客户程序的项目。

14.1 一个ActiveForm的例子

Delphi 4可以把分布式的数据库结构引申到Internet/Intranet上,把“瘦”客户程序作为ActiveForm嵌入到网页中让人们下载,然后在当地执行。

这一节剖析一个ActiveForm的示范程序,项目名称叫empeditx,它可以在C:\Program Files\Borland\Delphi4\Demos\Midas\Activefm目录中找到。它的主窗体如图14.1所示。

在打开这个项目之前,先要编译、运行位于C:\ProgramFiles\Borland\Delphi4\Demos\ Midas\Empedit目录中的Server项目,这是个应用服务器,如图14.2所示。

这里还要交代一下,在应用服务器上,用一个TQuery构件引入数据集,它的SQL语句如下:

Select * From Employee

在这个ActiveForm上,有一个TDCOMConnection构件,用于以DCOM方式连接应用服务器,它的ServerName属性设为Serv.EmpServer,它的ServerGUID设为{53BC6562-5B3E-11D0-9FFC-00A0248E4B9A}。

ActiveForm用TClientDataSet构件从应用服务器引入数据集,它的RemoteServer属性设为MidasConnection即TDCOMConnection构件的名称,它的ProviderName属性设为EmpQuery即应用服务器上的TQuery构件,由它来提供IProvider接口。

ActiveForm上有几个数据控件,用于显示数据,它们都通过一个TDataSource构件获得数据。此外,ActiveForm上还有一个TDBNavigator构件,用于浏览数据集。

由于本节要介绍的示范程序是一个ActiveForm,它的大部分代码与类型库有关,我们只把其中涉及到MIDAS技术的部分“拎”出来。

当用户单击ActiveForm上的“Get Employees”按钮时,就从应用服务器检索数据。每次检索到的记录数取决于TClientDataSet构件的PacketRecords属性。

Procedure TEmpEditForm.QueryButtonClick(Sender: TObject);

Begin

Employees.Close;

{ Employees是TClientDataSet构件的名称}E

mployees.Open;

End;

当用户单击ActiveForm上的“Update Employees”按钮时,把用户对数据的修改写到数据集中。

Procedure TEmpEditForm.UpdateButtonClick(Sender: TObject);

Begin

Employees.ApplyUpdates(-1);

End;

当用户单击ActiveForm上的“Undo Last Change”按钮时,取消用户对数据的修改。

Procedure TEmpEditForm.UndoButtonClick(Sender: TObject);

Begin

Employees.UndoLastChange(True);

End;

为了测试这个ActiveForm,首先需要把它发布到Web服务器上,供下载用。为此,要使用“Project”菜单上的“Web Deployment Options”命令设置有关Web发布的选项,主要是指定ActiveForm在Web服务器上的URL。然后使用“Project”菜单上的“WebDeploy”命令把ActiveForm发布到Web服务器上。

14.2 一个动态传递SQL语句的示范程序

这一节要剖析一个动态传递SQL语句的示范程序,可以在C:\Program Files\Borland\Delphi4\Demos\Midas\Adhoc目录中找到。

这个程序分为应用服务器和客户程序两个部分。当客户程序通过IProvider接口调用DataRequest请求数据时,把用户输入的SQL语句传递给应用服务器,这样,应用服务器上的TQuery构件就能够根据用户的要求来查询数据库,这就是本示范程序的基本思路。

先来剖析应用服务器,看它的数据模块,如图14.3所示。

图14.3 数据模块

数据模块上有这么几个构件:

一个TSession构件,它的SessionName属性设为Session1_2。

一个TDatabase构件,它的SessionName属性设为Session1_2,并且定义了一个专用的别名叫ADHOC。

一个TQuery构件,它的DatabaseName属性设为ADHOC,它的SessionName属性也设为Session1_2,而它的SQL属性为空,因为SQL语句由客户程序动态地传递过来。

一个TProvider构件,它的DataSet属性设为AdHocQuery即TQuery构件的名称。

现在我们暂时不管数据模块,再来看看应用服务器的主窗体,如图14.4所示。

图14.4 应用服务器的主窗体

主窗体上显示两个计数,一个是当前连接应用服务器的客户数(Clients),另一个是已经执行的查询次数(Queries)。

用什么来判断当前的客户数,这与数据模块的实例方式有关。我们可以回到数据模块的单元,看看它的初始化代码:

Initialization

TComponentFactory.Create(ComServer, TAdHocQueryDemo,

Class_AdHocQueryDemo, ciMultiInstance);

End.

可以看出,这个数据模块的实例方式设为ciMultiInstance,表示每当有一个客户连接应用服务器,就会创建数据模块的一个新的实例。因此,数据模块的实例数就是当前的客户数。怎样统计数据模块的实例数呢?很简单,只要处理数据模块的OnCreate事件。

Procedure TAdHocQueryDemo.AdHocQueryDemoCreate(Sender: TObject);

Begin

MainForm.UpdateClientCount(1);

End;

当一个客户退出连接时,将删除一个数据模块的实例,此时将触发数据模块的OnDestroy事件:

Procedure TAdHocQueryDemo.AdHocQueryDemoDestroy(Sender: TObject);

Begin

MainForm.UpdateClientCount(-1);

End;

其中,UpdateClientCount函数是在主窗体的单元中定义的:

Procedure TMainForm.UpdateClientCount(Incr: Integer);

Begin

FClientCount := FClientCount + Incr;

ClientCount.Caption := IntToStr(FClientCount);

End;

请读者注意Incr参数的作用。怎样统计已经执行过的查询数呢?很简单,只要统计TQuery构件被激活的次数就可以了。因此,程序处理了TQuery构件的AfterOpen事件。Procedure TAdHocQueryDemo.AdHocQueryAfterOpen(DataSet: TDataSet);

Begin

MainForm.IncQueryCount;

End;

IncQueryCount是在主窗体的单元中定义的:

Procedure TMainForm.IncQueryCount;

Begin

Inc(FQueryCount);

QueryCount.Caption := IntToStr(FQueryCount);

End;

在打开客户程序的项目之前,必须先编译和运行应用服务器的项目。好,现在我们打开客户程序的项目,它的主窗体如图14.5所示。

这个客户程序用一个TDCOMConnection构件连接应用服务器,它的ServerName属性设为Serv.AdHocQueryDemo。客户程序用TClientDataSet构件从应用服务器引入数据集,它的RemoteServer属性设为TDCOMConnection构件的名称,它的ProviderName属性设为AdHocQuery,这是应用服务器输出的 IProvider接口。

客户程序上有一个栅格,用于显示数据,栅格与数据集之间通过TDataSource构件连接。此外,客户程序上有一个多行文本编辑器,让用户输入SQL语句。有一个组合框用于选择要访问的数据库。我们还是先从处理OnCreate事件的句柄开始。

Procedure TForm1.FormCreate(Sender: TObject);

varI: Integer;DBNames: OleVariant;

Begin

RemoteServer.Connected := True;

DBNames := RemoteServer.AppServer.GetDatabaseNames;

If VarIsArray(DBNames) then

For I := 0 to VarArrayHighBound(DBNames, 1) Do

DatabaseName.Items.Add(DBNames[I]);

DatabaseNameClick(Self);

End;

首先,把TDCOMConnection构件的Connected属性设为True,将连接应用服务器。TDCOMConnection构件的AppServer属性将返回应用服务器上数据模块的接口,通过此接口就可以调用远程数据模块的方法,例如GetDatabaseNames。GetDatabaseNames是在应用服务器的数据模块单元中定义的:

Function TAdHocQueryDemo.GetDatabaseNames: OleVariant;

varI: Integer;

DBNames: TStrings;

Begin

DBNames := TStringList.Create;

Try

Session1.GetDatabaseNames(DBNames);

Result := VarArrayCreate([0, DBNames.Count - 1], varOleStr);

For I := 0 to DBNames.Count - 1 DoResult[I] := DBNames[I];

FinallyDBNames.Free;

End;

End;

GetDatabaseNames函数的作用是返回一个数组,该数组由所有已定义的别名和BDE会话期对象专用的别名组成。现在我们回到客户程序中,调用了数据模块的GetDatabaseNames函数后,就把检索到别名加到窗体右上角的组合框中,然后调用DatabaseNameClick函数。

Procedure TForm1.DatabaseNameClick(Sender: TObject);

varPassword: string;

Begin

If DatabaseName.Text <> '' then

Begin

ClientData.Close;

Try

RemoteServer.AppServer.SetDatabaseName(DatabaseName.Text, '');

Except

On E: Exception DoIf E.Message = 'Password Required' then

Begin

If InputQuery(E.Message, 'Enter password', Password) then

RemoteServer.AppServer.SetDatabaseName(DatabaseName.Text, Password);

End

Else

Raise;

End;

End;

End;

调用DatabaseNameClick的目的是使应用服务器与另一个数据库连接,这就需要通过AppServer属性获得数据模块的接口,然后调用数据模块单元的SetDatabaseName。SetDatabaseName是在应用服务器的数据模块单元中定义的:

Procedure TAdHocQueryDemo.SetDatabaseName(const DBName, Password: WideString);

Begin

Try

Database1.Close;

Database1.AliasName := DBName;

If Password <> '' then

Database1.Params.Values['PASSWORD'] := Password;

Database1.Open;

Except

{如果数据库打开失败,很可能是因为该数据库需要口令}

On E: EDBEngineError DoIf (Password = '') then Raise Exception.Create('Password Required')Else

Raise;

End;

End;

SetDatabaseName的作用是修改TDatabase构件的AliasName属性,然后连接新的数据库,如果失败,就触发一个异常。在客户程序的DatabaseNameClick过程中,如果出现异常,就弹出一个输入框,让用户输入口令,然后再次调用数据模块的SetDatabaseName。

当用户在“Query”框中输入了SQL语句,就可以单击“Run Query”按钮执行这个查询。问题是,只有应用服务器才可以执行查询,那么客户程序是怎样把SQL语句传递给应用服务器的呢?这就是本示范程序的关键之处。

Procedure TForm1.RunButtonClick(Sender: TObject);

Begin

ClientData.Close;

ClientData.Provider.DataRequest(SQL.Lines.Text);

ClientData.Open;

End;

原来,客户程序通过IProvider接口调用DataRequest把用户输入的SQL语句传递给应用服务器。客户程序通过IProvider接口调用DataRequest将在应用服务器端触发OnDataRequest事件,我们来看看应用服务器是怎样处理OnDataRequest事件的。

Function TAdHocQueryDemo.AdHocProviderDataRequest(Sender: TObject; Input: OleVariant): OleVariant;

Begin

AdHocQuery.SQL.Text := Input;

End;

至此,一个动态传递SQL语句的示范程序剖析完毕,请读者仔细琢磨其中的编程技巧。实际上,通过IProvider接口调用DataRequest可以传递任何信息。

14.4 一个全面演示TClientDataSet功能的示范程序

这一节介绍一个演示TClientDataSet功能的示范程序,项目名称叫Alchtest,它可以在C:\Program Files\Borland\Delphi4\Demos\Midas\Alchtest目录中找到,主窗体如图14.6所示。

这个程序的总体思路是,用一个多页控件让用户修改TClientDataSet的属性或者调用它的方法,然后在下面的TAB控件中演示修改后的效果。

程序首先在处理OnCreate事件的句柄中做了一些初始化的工作。

Procedure TDBClientTest.FormCreate(Sender: TObject);

varI: Integer;

Begin

Database1.Close;

FMaxErrors := -1;

FPacketRecs := -1;

SetCurrentDirectory(PChar(ExtractFilePath(ParamStr(0))));

For I := 0 to StatusBar.Panels.Count - 1 do

StatusBar.Panels[I].Text := '';

Application.OnIdle := ShowHeapStatus;

Application.OnHint := OnHint;

StreamSettings(False);

SetEventsVisible(ViewEvents.Checked);

End;

其中,指定ShowHeapStatus作为处理应用程序的OnIdle事件的句柄,指定OnHint作为处理应用程序的OnHint事件的句柄。ShowHeapStatus是这样定义的:

Procedure TDBClientTest.ShowHeapStatus(Sender: TObject; var Done: Boolean);

Begin

Caption := Format('Client DataSet Test Form - (Blocks=%d Bytes=%d)',[AllocMemCount, AllocMemSize]);

End;

ShowHeapStatus的作用是在应用程序空闲的时候,在主窗口的标题栏显示堆的状态,其中,AllocMemCount是当前分配的内存块数,AllocMemSize是当前分配的内存总长度。

OnHint是这样定义的:

Procedure TDBClientTest.OnHint(Sender: TObject);

Begin

StatusMsg := Application.Hint;

End;

StatusMsg是一个自定义的属性,用于表达要在状态栏上显示的提示信息。

在处理OnCreate事件的句柄中还调用了StreamSettings函数。StreamSettings是非常有用的,当窗体关闭时,就调用StreamSettings把窗体上一些控件的状态保存到一个配置文件中。当窗体弹出时,就调用StreamSettings读取配置文件以初始化窗体上的控件。

Procedure TDBClientTest.StreamSettings(Write: Boolean);

Procedure WriteStr(const OptName, Value: string);

Begin

FConfig.WriteString('Settings', OptName, Value);

End;

Procedure WriteBool(const OptName: string; Value: Boolean);

Begin

FConfig.WriteBool('Settings', OptName, Value);

End;

Function ReadStr(const OptName: string): string;

Begin

Result := FConfig.ReadString('Settings', OptName, '');

End;

Function ReadBool(const OptName: string): Boolean;

Begin

Result := FConfig.ReadBool('Settings', OptName, False);

End;

Function FindPage(const PageName: string): TTabSheet;

var I: Integer;

Begin

For I := AreaSelector.PageCount - 1 downto 0 do

Begin

Result := AreaSelector.Pages[I];

If Result.Caption = PageName then Exit;

End;

Result := ProviderPage;

End;

Procedure ProcessComponents(Components: array of TComponent);

varI: Integer;

Begin

If Write then

Begin

For I := Low(Components) to High(Components) Do

If Components[I] is TCustomEdit then

With TEdit(Components[I]) do WriteStr(Name, Text)

Else if Components[I] is TComboBox then

With TDBComboBox(Components[I]) do WriteStr(Name, Text)

Else if Components[I] is TCheckBox then

With TCheckBox(Components[I]) do WriteBool(Name, Checked)

Else if Components[I] is TAction then

With TAction(Components[I]) do WriteBool(Name, Checked)

Else if Components[I] is TPageControl then

With TPageControl(Components[I]) doWriteStr(Name,ActivePage.Caption);

End;

Else

Begin

For I := Low(Components) to High(Components) do

If Components[I] is TCustomEdit then

With TEdit(Components[I]) do Text := ReadStr(Name)

Else if Components[I] is TComboBox then

With TComboBox(Components[I]) do Text := ReadStr(Name)

Else if Components[I] is TCheckBox then

With TCheckBox(Components[I]) do Checked := ReadBool(Name)

Else if Components[I] is TAction then

With TAction(Components[I]) do Checked := ReadBool(Name)

Else if Components[I] is TPageControl then

With TPageControl(Components[I]) doActivePage := FindPage(ReadStr(Name));

End;

End;

Begin

GetConfigFile;

If not Write and (ReadStr('AreaSelector') = '') then Exit;

ProcessComponents([AreaSelector, DatabaseName, MasterTableName,DetailTableName, MasterSQL, DetailSQL, poCascadedDeletes, poCascadedUpdates,poDelayedDetails, poDelayedBlobs, poIncludeFieldProps, poReadOnly,DisableProvider, ObjectView, SparseArrays, MixedData, FetchOnDemand,DisableProvider, ResolveToDataSet, DataRows, CreateDataSetDesc,EnableBCD, RequestLiveQuery, ViewEvents, DisplayDetails, IncludeNestedObject]);

End;

StreamSettings用Write参数来区分现在是要读还是写。StreamSettings中又嵌套了几个过程和函数,其中,WriteStr、WriteBool、ReadStr、ReadBool分别用于在配置文件中存取字符串和布尔类型的信息,FindPage函数搜索并返回一个特定的对象,而ProcessComponents则用于存取与具体构件有关的信息。

GetConfigFile函数用于创建一个TIniFile对象的实例(如果还没有创建的话)。

Function TDBClientTest.GetConfigfile: TIniFile;

Begin

If FConfig = nil Then

FConfig := TIniFile.Create(ChangeFileExt(ParamStr(0), '.INI'));

Result := FConfig;

End;

请读者注意StreamSettings是怎样调用ProcessComponents函数的。ProcessComponents需要传递一个数组,数组中的元素就是窗体上的一些控件的名称。

我们先翻到“Provider”页,看看怎样指定数据库和建立Master/Detail关系,如图14.7所示。

图14.7 “Provider”页

“Database”框用于指定要访问的数据库。当用户下拉此框时,将触发OnDropDown事件。如果此时“Database”框还是空的话,就调用TSession的GetDatabaseNames函数把所有已定义的BDE别名和专用的别名填到“Database”框中。

Procedure TDBClientTest.DatabaseNameDropDown(Sender: TObject);

Begin

If DatabaseName.Items.Count = 0 then

Session.GetDatabaseNames(DatabaseName.Items);

End;

当用户在“Database”框中选择一个别名,将触发OnClick事件。此时,就调用CheckDatabase连接另一个数据库。由于数据库已改变,“Master/DetailTables”框内的内容应当清掉。

Procedure TDBClientTest.DatabaseNameClick(Sender: TObject);

Begin

If (DatabaseName.Text <> '') and not DatabaseName.DroppedDown then

Begin

CheckDatabase(True);

MasterTableName.Items.Clear;

MasterTableName.Text := '';

DetailTableName.Text := '';

ClientData.Close;

End;

End;

用户也可以直接在“Database”框键入一个数据库别名,然后按Enter键,此时将触发OnKeyPress事件。

Procedure TDBClientTest.DatabaseNameKeyPress(Sender: TObject; var Key: Char);

Begin

If Key = #13 then

Begin

If DatabaseName.DroppedDown then

DatabaseName.DroppedDown := False;

DatabaseNameClick(Sender);Key := #0;

End;

End;

好,现在让我们看看CheckDatabase是怎样定义的:

Procedure TDBClientTest.CheckDatabase(CloseFirst: Boolean);

varSPassword, SUserName: string;

Begin

If not CloseFirst and Database1.Connected and(Database1.AliasName = DatabaseName.Text) then Exit;

Database1.Close;

Database1.AliasName := DatabaseName.Text;

Session.GetAliasParams(Database1.AliasName, Database1.Params);

If Database1.Params.IndexOfName('PATH') = -1 then

Begin

SPassword := ConfigFile.ReadString('Passwords', Database1.AliasName, '');

If SPassword = '' then

Begin

SUserName := Database1.Params.Values['USER NAME'];

If not LoginDialog('DatabaseName.Text', SUserName, SPassword) then Exit;

Database1.Params.Values['USER NAME'] := SUserName;

End;

Database1.Params.Values['PASSWORD'] := SPassword;

End;

If EnableBCD.Checked thenDatabase1.Params.Add('ENABLE BCD=TRUE');

Database1.Open;

If Database1.IsSQLBased and (SPassword <> '') thenConfigFile.WriteString('Passwords', Database1.AliasName, SPassword);

End;

CheckDatabase用于连接一个用户指定的数据库。如果当前连接的就是用户指定的数据库,CheckDatabase就什么也不干。如果不是的话,首先要调用TDatabase构件的Close断开与数据库的连接,然后把TDatabase构件的AliasName 属性设为用户选择的别名,并调用BDE会话期对象的GetAliasParams取出这个别名的参数。

注意,对于本地数据库来说,只有一个PATH参数,而对于SQL数据库来说,参数就有好几个,因此,可以用有没有PATH参数来区分本地数据库和SQL数据库。如果是SQL数据库的话,就要设置USER NAME和PASSWORD参数给出用户名和口令。如果“Settings”菜单上的“EnableBCD”命令被选中的话,就增加一个ENABLE BCD参数,并把它的值设为TRUE。然后调用Open重新连接数据库。

这个程序还能够让客户选择“Master/Detail”关系中的Master表和Detail表,这是在“Master/Detail Tables”框中选择的,其中,上面一个组合框用于选择Master表,下面一个组合框用于选择Detail表。当用户在组合框中选择一个表,将触发OnClick事件。

Procedure TDBClientTest.MasterTableNameClick(Sender: TObject);

Begin

With Sender as TComboBox Do

If not DroppedDown and (MasterTable.TableName <> Text) thenOpenTable.Execute;

End;

当用户下拉“Master/Detail Tables”框中的一个组合框,将触发OnDropDown事件。此时就调用BDE会话期对象的GetTableNames把当前数据库中的所有表格的名称填到组合框中,供用户选择。

Procedure TDBClientTest.MasterTableNameDropDown(Sender: TObject);

Begin

CheckDatabase(False);

With Sender as TComboBox do

If (Items.Count < 1) and (Database1.AliasName <> '') then

Session.GetTableNames(Database1.DatabaseName, '', True, False, Items);

End;

用户也可以直接在“Master/Detail Tables”框中的一个组合框内键入一个表格的名称,然后按Enter键,此时将触发OnKeyPress事件。

Procedure TDBClientTest.MasterTableNameKeyPress(Sender: TObject; var Key: Char);

Begin

If Key = #13 then

Begin

With Sender as TComboBox Do

If DroppedDown then DroppedDown := False;

OpenTable.Execute;

Key := #0;

End;

End;

注意:上面都是以选择Master表的组合框为例的,实际上,选择Detail表的操作完全一样,代码如下。

Procedure TDBClientTest.DetailTableNameClick(Sender: TObject);

Begin

With Sender as TComboBox Do

If not DroppedDown and (DetailTable.TableName <> Text) then

OpenTable.Execute;

End;

在上面几个事件句柄中,OpenTable是一个动作列表,这是Delphi 4新增加的功能。在窗体上双击TActionList构件,将打开一个如图14.8所示的编辑器。

图14.8 动作列表编辑器

在这个编辑器中找出OpenTable这个动作,然后在对象观察器中可以发现, 执行这个动作的代码是OpenTableExecute函数。

Procedure TDBClientTest.OpenTableExecute(Sender: TObject);

Begin

ClearEventLog.Execute;

If MasterTableName.Text <> '' then OpenDataSet(MasterTable);

End;

而OpenDataSet是这样定义的:

Procedure TDBClientTest.OpenDataSet(Source: TDBDataSet);

Begin

Screen.Cursor := crHourGlass;

Try

ClientData.Data := Null;

Source.Close;

If not DisableProvider.Checked then

Begin

BDEProvider.DataSet := Source;

SetProviderOptions;

ClientData.ProviderName := BDEProvider.Name;

ActiveDataSet := ClientData;

End

Else

ActiveDataSet := Source;

MasterGrid.SetFocus;

StatusMsg := 'Dataset Opened';

FinallyScreen.Cursor := crDefault;

End;

StreamSettings(True);

End;

OpenDataSet通过一个叫DisableProvider的复选框来决定是否使用TProvider构件。如果没有选中“Disable Provider”这个复选框,表示使用TProvider构件,此时就把TProvider构件的DataSet属性设为MasterTable,然后调用SetProviderOptions来设置TProvider构件的选项,接着设置TClientDataSet构件的ProviderName属性指定这个TProvider构件,最后把ActiveDataSet变量设为此TClientDataSet构件。如果用户选中“Disable Provider”复选框,表示不使用TProvider构件,此时就直接把ActiveDataSet设为MasterTable。

SetProviderOptions是这样定义的:

Procedure TDBClientTest.SetProviderOptions;

varOpts: TProviderOptions;

Begin

Opts := [];If poDelayedDetails.Checked then

Include(Opts, poFetchDetailsOnDemand);

if poDelayedBlobs.Checked then Include(Opts, poFetchBlobsOnDemand);

if poCascadedDeletes.Checked then Include(Opts, poCascadeDeletes);

if poCascadedUpdates.Checked then Include(Opts, poCascadeUpdates);

if poReadOnly.Checked then Include(Opts, Provider.poReadOnly);

if poIncludeFieldProps.Checked then Include(Opts, poIncFieldProps);

BDEProvider.Options := Opts;

End;

SetProviderOptions实际上是根据“Settings”菜单上的“Provider Options”命令的一些子命令是否被选中来设置TProvider构件的Options属性。这个程序还可以让用户在“Master/Detail Queries”框中输入SQL语句。当用户输入了SQL语句并且按下Enter键,将触发OnKeyPress事件。

Procedure TDBClientTest.MasterSQLKeyPress(Sender: TObject; var Key: Char);

Begin

If Key = #13 then

Begin

OpenQuery.Execute;

Key := #0;

End;

End;

其中,OpenQuery也是一个动作,执行它的是OpenQueryExecute函数。OpenQueryExecute是这样定义的:

Procedure TDBClientTest.OpenQueryExecute(Sender: TObject);

Begin

If UpperCase(Copy(MasterSQL.Text, 1, 6)) = 'SELECT' then

OpenDataSet(MasterQuery)

Else

Begin

CheckDatabase(False);

MasterQuery.RequestLive := True;

MasterQuery.SQL.Text := MasterSQL.Text;

MasterQuery.ExecSQL;

StatusMsg := Format('%d rows were affected', [MasterQuery.RowsAffected]);

End;

Events.Items.

Begin

Update;

Try

Events.Clear;

Finally

Events.Items.EndUpdate;

End;

End;

OpenQueryExecute首先判断用户输入的SQL语句是否为SELECT。如果是的话,就调用OpenDataSet执行SELECT语句。如果不是的话,就调用ExecSQL执行SQL语句。

当用户翻到“Fields”页,将触发FieldsPage(TTabSheet对象)的OnShow事件,此时就把数据集中的字段和字段定义对象名称分别显示在两个多行文本编辑器中,如图14.9所示。

图14.9 “Fields”页

Procedure TDBClientTest.FieldsPageShow(Sender: TObject);

Procedure WriteFullNames(Fields: TFields);

varI: Integer;

Begin

For I := 0 to Fields.Count - 1 Do

With Fields[I] Do

Begin

FieldList.Lines.Add(Format('%d) %s', [FieldNo, FullName]));

If Fields[I].DataType in [ftADT, ftArray] then

WriteFullNames(TObjectField(Fields[I]).Fields);

End;

End;

Procedure WriteLists(DataSet: TDataSet);

varI: Integer;

Begin

FieldList.Clear;

For I := 0 to DataSet.FieldList.Count - 1 Do

With DataSet.FieldList Do

FieldList.Lines.Add(Format('%d) %s', [Fields[I].FieldNo, Strings[I]]));

FieldDefList.Clear;

DataSet.FieldDefs.Updated := False;

DataSet.FieldDefList.Update;

For I := 0 to DataSet.FieldDefList.Count - 1 Do

With DataSet.FieldDefList Do

FieldDefList.Lines.Add(Format('%d) %s', [FieldDefs[I].FieldNo, Strings[I]]));

End;

varDataSet: TDataSet;

Begin

DataSet := DBNavigator1.DataSource.DataSet;

If Assigned(DataSet) and DataSet.Active then

Begin

WriteLists(DataSet)

End

Else

Begin

CheckDatabase(False);

MasterTable.TableName := MasterTableName.Text;

WriteLists(MasterTable);

End;

End;

首先要说明的是,FieldsPageShow中嵌套了WriteFullNames,其实WriteFullNames完全是多余的。FieldsPageShow先获取当前的数据集。如果当前的数据集已打开的话,就调用WriteLists显示字段对象和字段定义对象的列表。如果当前数据集没有打开,就显示MasterTable中的字段对象和字段定义对象的列表。当用户翻到“Indexes”页,将触发IndexPage(TTabSheet对象)的OnShow事件,此时就把当前数据集中的索引列出来,用户也可以创建新的索引或者删除一个索引。“Indexes”页如图14.10所示。

图14.10 “Indexes”页

Procedure TDBClientTest.IndexPageShow(Sender: TObject);

Begin

If not Assigned(ActiveDataSet) or not ActiveDataSet.Active then

OpenTable.Execute;

RefreshIndexNames(0);

End;

IndexPageShow首先检查当前是否打开了一个数据集,如果没有,就执行OpenTable的代码即打开数据集,然后调用RefreshIndexNames函数列出所有的索引名称。

Procedure TDBClientTest.RefreshIndexNames(NewItemIndex: Integer);

varI: Integer;

IndexDefs: TIndexDefs;

Begin

IndexList.Clear;

If ActiveDataSet = MasterTable then

IndexDefs := MasterTable.IndexDefs

Else

IndexDefs := ClientData.IndexDefs;

IndexDefs.Update;

For I := 0 to IndexDefs.Count - 1 Do

If IndexDefs[I].Name = '' then IndexList.Items.Add('')

Else

IndexList.Items.Add(IndexDefs[I].Name);

If IndexList.Items.Count > 0 then

Begin

If NewItemIndex < IndexList.Items.Count then

IndexList.ItemIndex := NewItemIndex

ElseIndexList.ItemIndex := 0;

ShowIndexParams;

End;

End;

RefreshIndexNames又调用ShowIndexParams检索索引的选项,用这些选项来初始化“Indexes”页上的几个编辑框和复选框。

Procedure TDBClientTest.ShowIndexParams;varIndexDef: TIndexDef;

Begin

If ActiveDataSet = MasterTable then

IndexDef := MasterTable.IndexDefs[IndexList.ItemIndex]

Else

IndexDef := ClientData.IndexDefs[IndexList.ItemIndex];

idxCaseInsensitive.Checked := ixCaseInsensitive in IndexDef.Options;idxDescending.Checked := ixDescending in IndexDef.Options;idxUnique.Checked := ixUnique in IndexDef.Options;idxPrimary.Checked := ixPrimary in IndexDef.Options;IndexFields.Text := IndexDef.Fields;

DescFields.Text := IndexDef.DescFields;

CaseInsFields.Text := IndexDef.CaseInsFields;

End;

如果用户在列表框中选择了另一个索引,就应当相应地刷新这些选项。Procedure TDBClientTest.IndexListClick(Sender: TObject);

Begin

If ActiveDataSet = MasterTable then

MasterTable.IndexName := MasterTable.IndexDefs[IndexList.ItemIndex].Name

Else

ClientData.IndexName := ClientData.IndexDefs[IndexList.ItemIndex].Name;

ShowIndexParams;

End;

如果要创建一个新的索引,用户必须事先设置索引的选项,然后单击“CreateIndex”按钮。

Procedure TDBClientTest.CreateIndexClick(Sender: TObject);

varIndexName: string;Options: TIndexOptions;

Begin

IndexName := Format('Index%d', [IndexList.Items.Count+1]);

If InputQuery('Create Index', 'Enter IndexName:', IndexName) then

Begin

Options := [];

If idxCaseInsensitive.Checked then Include(Options, ixCaseInsensitive);

If idxDescending.Checked then Include(Options, ixDescending);

If idxUnique.Checked then Include(Options, ixUnique);

If idxPrimary.Checked then Include(Options, ixPrimary);

If ActiveDataSet = MasterTable then

Begin

MasterTable.Close;

MasterTable.AddIndex(IndexName,IndexFields.Text,Options,DescFields.Text);

MasterTable.Open;

End

Else

ClientData.AddIndex(IndexName, IndexFields.Text, Options,DescFields.Text, CaseInsFields.Text);

StatusMsg := 'Index Created';

RefreshIndexNames(IndexList.Items.Count);

End;

End;

CreateIndexClick首先弹出一个输入框,让用户输入索引名称,然后根据用户设置的选项来设置索引的Options属性。

在调用AddIndex之前,首先要区分当前的数据集是MasterTable还是ClientData,为什么要区分MasterTable和ClientData呢?因为对于一般的数据集构件来说,在创建索引之前必须先关闭数据集,而对于TClientDataSet构件来说,则不必先关闭数据集。

用户也可以先选择一个索引,然后单击“Delete Index”按钮删除这个索引。

Procedure TDBClientTest.DeleteIndexClick(Sender: TObject);

Begin

If IndexList.ItemIndex > -1 then

If ActiveDataSet = MasterTable then

Begin

MasterTable.Close;

MasterTable.DeleteIndex(MasterTable.IndexDefs[IndexList.ItemIndex].Name);

MasterTable.Open;

End

Else

ClientData.DeleteIndex(ClientData.IndexDefs[IndexList.ItemIndex].Name);

End;

与调用AddIndex一样,在调用DeleteIndex之前,首先要区分当前的数据集是MasterTable还是ClientData。当用户翻到“Filters”页,就可以设置过滤条件,如图14.11所示。

图14.11 “Filters”页

当“Filters”页刚刚打开的时候,将触发OnShow事件,这样就可以初始化“Filter”框。这里运用了一个编程技巧,先从下面的栅格中取出一个字段,然后判断这个字段的数据类型是不是ftString、ftMemo或ftFixedChar中的一种,如果是的话,过滤条件表达式的运算符后面的值要用引号括起来。

Procedure TDBClientTest.FilterPageShow(Sender: TObject);

varField: TField;LocValue,QuoteChar: String;

Begin

If (Filter.Text = '') and Assigned(ActiveDataSet) and ActiveDataSet.Active then

Begin

Field := MasterGrid.SelectedField;If Field = nil then Exit;

With ActiveDataSet DoTryDisableControls;

MoveBy(3);

LocValue := Field.Value;

First;

Finally

EnableControls;

End;

If Field.DataType in [ftString, ftMemo, ftFixedChar] then

QuoteChar := ''''

Else QuoteChar := '';

Filter.Text := Format('%s=%s%s%1:s', [Field.FullName, QuoteChar, LocValue]);

End;

End;

用户可以在“Filter”框内键入新的过滤条件,当用户按下Enter键或把输入焦点移走,就会把用户输入的过滤条件表达式赋给当前数据集的Filter属性。当用户翻到“FindKey”页,就可以输入一个键值,然后在数据集中搜索特定的记录,如图14.12所示。

图14.12 “FindKey”页

当用户单击“Find Key”或“Find Nearest”按钮,就开始搜索特定的记录。

Procedure TDBClientTest.FindKeyClick(Sender: TObject);

Begin

If ActiveDataSet = ClientData then

With ClientData Do

Begin

SetKey;IndexFields[0].AsString := FindValue.Text;

KeyExclusive := Self.KeyExclusive.Checked;If FindPartial.Checked then KeyFieldCount := 0;

If Sender = Self.FindNearest then GotoNearest else

If not GotoKey then StatusMsg := 'Not found';

End

Else

if ActiveDataSet = MasterTable then

With MasterTable Do

Begin

SetKey;

IndexFields[0].AsString := FindValue.Text;

KeyExclusive := Self.KeyExclusive.Checked;

If FindPartial.Checked then KeyFieldCount := 0;

If Sender = Self.FindNearest then GotoNearest

Else

if GotoKey thenStatusMsg := 'Record Found'

Else StatusMsg := 'Not found';

End;

End;

首先,要区分当前数据集是ClientData还是MasterTable,调用SetKey使数据集进入dsSetKey状态,把用户输入的键值赋给索引中的第一个字段。然后根据Sender参数判断用户按下的是“Find Key”按钮还是“Find Nearest”按钮,如果是后者,就调用GotoNearest,如果是前者,就调用GotoKey,最后根据GotoKey的返回值显示有关信息。

当用户翻到“Locate”页,将触发LocatePage(TTabSheet对象)的OnShow事件,程序就把下面的栅格中选择的字段作为关键字段。“Locate”页如图14.13所示。

图14.13 “Locate”页

Procedure TDBClientTest.LocatePageShow(Sender: TObject);

varField: TField;

Begin

If (ActiveDataSet <> nil) and ActiveDataSet.Active then

BeginField := MasterGrid.SelectedField;

If LocateField.Items.Count = 0 then

LocateFieldDropDown(LocateField);

If (LocateField.Text = '')or(LocateField.Items.IndexOf(Field.FieldName) < 1) then

LocateField.Text := Field.FieldName;

With ActiveDataSet Do

Try

DisableControls;

MoveBy(3);

LocateEdit.Text := Field.Value;

First;

Finally

EnableControls;

End;

End;

End;

用户也可以在“Field”框选择一个关键字段。当用户下拉“Field”框时,触发OnDropDown事件,这样就可以把当前数据集中的字段显示到“Field”框中。

Procedure TDBClientTest.LocateFieldDropDown(Sender: TObject);

Begin

ActiveDataSet.GetFieldNames(LocateField.Items);

End;

当用户选择了关键字段并且输入了键值,就可以单击“Locate”按钮开始定位记录。

Procedure TDBClientTest.LocateButtonClick(Sender: TObject);varOptions: TLocateOptions;LocateValue: Variant;

Begin

Options := [];

If locCaseInsensitive.Checked then Include(Options, loCaseInsensitive);

If locPartialKey.Checked then Include(Options, loPartialKey);

If LocateNull.Checked then LocateValue := Null

Else

LocateValue := LocateEdit.Text;

If ActiveDataSet.Locate(LocateField.Text, LocateValue, Options) then

StatusMsg := 'Record Found'

Else

StatusMsg := 'Not found';

End;

前面几行代码主要是设置有关选项,其中,如果用户选中“Null Value”复选框的话,就把键值设为Null。然后调用当前数据集的Locate函数定位记录,并根据Locate函数的返回值显示相应的信息。

14.6 一个登录的示范程序

这一节剖析一个登录示范程序,它可以在C:\Program Files\Borland\Delphi4\Demos\Midas\Login目录中找到。

这个程序分为应用服务器和客户程序两个部分。应用服务器的主窗体上有一个列表框,用于记载曾经登录到应用服务器上的用户名,如图14.16所示。

应用服务器上的数据模块如图14.17所示。

数据模块上只有一个TTable构件,它的DatabaseName属性设为DBDEMOS,TableName属性设为COUNTRY。数据模块上没有TProvider构件,由TTable构件提供IProvider接口。

这个数据模块的实例方式设为ciMultiInstance,这意味着每当一个客户连接应用服务器时,就会创建数据模块的一个新的实例,当客户不再连接应用服务器时,就删除数据模块的实例。因此,这个程序利用数据模块的OnCreate事件做了一些初始化的工作,利用数据模块的OnDestroy事件从列表框中删除一个用户名。

Procedure TLoginDemo.LoginDemoCreate(Sender: TObject);

Begin

FLoggedIn := False;

End;

为什么要把FLoggedIn变量设为False呢?其原因后面将解释。

Procedure TLoginDemo.LoginDemoDestroy(Sender: TObject);

Begin

With Form1.ListBox1.Items do Delete(IndexOf(FUserName));

End;

编译和运行这个应用服务器。打开客户程序的项目,它的主窗体如图14.18所示。

窗体上的TDCOMConnection构件用于连接应用服务器,它的ServerName属性设为Server.LoginDemo,它的LoginPrompt属性设为True。窗体上的TClientDataSet构件的RemoteServer属性指定了TDCOMConnection构件,它的ProviderName属性设为Country。

此外,窗体上有一个栅格用于显示数据集中的数据,还有一个“Open”按钮用于打开数据集。

由于TDCOMConnection构件的LoginPrompt属性设为True,当客户程序试图连接应用服务器时就会弹出一个“Remote Login”对话框,要求用户输入用户名和口令。登录以后,就触发OnLogin事件。在处理这个事件的句柄中,客户程序通过AppServer属性获得数据模块的接口,从而调用数据模块的Login。

Procedure TForm1.DCOMConnection1Login(Sender: TObject; Username,Password: String);

Begin

DCOMConnection1.AppServer.Login(UserName, Password);

End;

在应用服务器的数据模块单元中,Login是这样定义的。

Procedure TLoginDemo.Login(const UserName, Password: WideString);

Begin

Form1.ListBox1.Items.Add(UserName);

FLoggedIn := True;

FUserName := UserName;

End;

Login把用户名加到列表框中,然后把FLoggedIn变量设为True,表示用户已登录。当用户单击“Open”按钮,就调用TClientDataSet构件的Open打开数据集。

Procedure TForm1.Button1Click(Sender: TObject);

Begin

ClientDataSet1.Open;

End;

14.7 一个演示Master/Detail关系的示范程序

这一节剖析一个演示Master/Detail关系的示范程序,它可以在C:\ProgramFiles\Borland\Delphi4\ Demos\Midas\Mstrdtl目录中找到。

这个程序分为应用服务器和客户程序两个部分。应用服务器有一个窗体,不过,这个窗体其实是多余的,如果不想显示,可以打开应用服务器的项目文件,加入这么一行:

Application.ShowMainForm := False;

应用服务器的数据模块如图14.19所示。

应用服务器的数据模块上有这么几个构件:

名为Database的TDatabase构件,其AliasName属性设为IBLOCAL,并且定义了一个应用程序专用的别名叫ProjectDB。其Params属性提供了用户名和口令。

名为Project的TTable构件,其DatabaseName属性设为ProjectDB,它的TableName属性设为PROJECT(注意:必须已运行Interbase Server)。

名为Employee的TQuery构件,其DatabaseName属性设为ProjectDB,它的SQL语句如下:Select * From EMPLOYEE_PROJECT E Where E.PROJ_ID= :PROJ_ID

名为EmpProj的TQuery构件,其DatabaseName属性设为ProjectDB,它的SQL语句如下:Select EMP_NO,FULL_NAME From EMPLOYEE

名为UpdateQuery的TQuery构件,其DatabaseName属性设为ProjectDB,它的SQL语句目前是空的。

名为ProjectProvider的TProvider构件,其DataSet属性设为Project。

名为ProjectSource的TDataSource构件,其DataSet属性设为Project。编译并运行应用服务器。现在可以打开客户程序的项目,它的数据模块如图14.20所示。

图14.20 数据模块

客户程序的数据模块上有这么几个构件:

名为DCOMConnection的TDCOMConnection构件,其ServerName属性设为Serv.ProjectData。

名为Project的TClientDataSet构件,其RemoteServer属性设为DCOMConnection它的ProviderName属性设为ProjectProvider。并且建立了一个叫ProjectEmpProj的永久字段对象,它的类型是TDataSetField。与Project对应的TDataSource构件叫ProjectSource。

名为Emp_Proj的TClientDataSet构件,其RemoteServer属性和ProviderName属性都是空的,但它的DataSetField属性设为叫ProjectEmpProj的字段对象,这就构成了Master/Detail关系。与Emp_Proj对应的TDataSource构件叫EmpProjSource。

名为Employee的TClientDataSet构件,其RemoteServer属性指定了TDCOMConnection构件,但它的ProviderName属性设为Employee。与Employee对应的TDataSource构件叫EmployeeSource。

我们再来看客户程序的主窗体,如图14.21所示。

左边一个栅格只显示Project数据集中的PROJ_NAME字段即项目名称,“Product”框显示Project数据集中的PRODUCT字段,“Description”框显示Project数据集中的PROJ_DESC字段,并且用一个TDBNavigator构件为Project数据集导航。

右下角的栅格显示Emp_Proj数据集中一个叫EmployeeName的字段的值,这是个Lookup字段,它的LookupDataSet属性设为Employee,它的LookupKeyField属性设为EMP_NO,它的LookupResultField属性设为FULL_NAME。当用户用导航器浏览Project数据集的记录时,右下角的栅格就从Employee数据集中查找与EMP_NO字段匹配的记录,并且显示其中的FULL_NAME字段。

由于右下角的栅格只建立了一个永久的列对象,因此,可以把这一列的宽度设为与栅格本身同宽,它是在处理窗体的OnCreate事件的句柄中进行的。

Procedure TClientForm.FormCreate(Sender: TObject);

Begin

MemberGrid.Columns[0].Width :=MemberGrid.ClientWidth - GetSystemMetrics(SM_CXVSCROLL);

End;

由于一个项目中不止一个雇员,为了醒目起见,可以把其中的负责人加粗显示,这需要处理栅格的OnDrawColumnCell事件。

Procedure TClientForm.MemberGridDrawColumnCell(Sender: TObject; const Rect: TRect;DataCol: Integer;Column: TColumn;State: TGridDrawState);

Begin

If DM.ProjectTEAM_LEADER.Value = DM.Emp_ProjEMP_NO.Value thenMemberGrid.Canvas.Font.Style := [fsBold];

MemberGrid.DefaultDrawColumnCell(Rect, DataCol, Column, State);

End;

怎样来判断其中的负责人呢?在Project数据集中,有一个TEAM_LEADER 字段,它存储的是项目负责人的雇员编号。在Emp_Proj数据集中,有一个EMP_NO,它存储的也是雇员编号,如果这两者相等,即表示该雇员是项目负责人。当用户单击“Add”按钮,就可以在栅格中增加一条记录,即在项目中增加一个雇员。

Procedure TClientForm.AddBtnClick(Sender: TObject);

Begin

MemberGrid.SetFocus;

DM.Emp_Proj.Append;

MemberGrid.EditorMode := True;

End;

由于栅格事先建立了一个永久的列对象,而该列对象的FieldName属性指定了一个Lookup字段,所以,用户可以从一个组合框中选择一个值。

当用户单击“Delete”按钮,就删除当前记录,即一个雇员。

Procedure TClientForm.DeleteBtnClick(Sender: TObject);

Begin

DM.Emp_Proj.Delete;

End;

当用户先选择其中一个雇员,然后单击“Leader”按钮,就把该雇员设为项目负责人。

Procedure TClientForm.LeaderBtnClick(Sender: TObject);

varNewLeader: Integer;

Begin

NewLeader := DM.Emp_ProjEMP_NO.Value;

If not (DM.Project.State in dsEditModes) then DM.Project.Edit;

DM.ProjectTEAM_LEADER.Value := NewLeader;

MemberGrid.Refresh;

End;

增加、删除或修改了记录后,用户应当单击“Apply Update”按钮更新数据库。

Procedure TClientForm.ApplyUpdatesBtnClick(Sender: TObject);

Begin

DM.ApplyUpdates;

End;

在数据模块的单元中,ApplyUpdates是这样定义的:

Procedure TDM.ApplyUpdates;

Begin

If Project.ApplyUpdates(0) = 0 then Project.Refresh;

End;

可以看出,数据模块的ApplyUpdates又调用了TClientDataSet构件的ApplyUpdates,并且把MaxErrors参数设为0,这样,只要应用服务器发现有一个错误的记录,更新就停止。

当用户在左边的栅格中试图增加一个新的项目时,会触发TClientDataSet构件的OnNewRecord事件。由于这个栅格只显示了PROJ_NAME字段,用户不能直接输入PROJ_ID字段的值,因此,程序在处理OnNewRecord事件的句柄中推出一个输入框,让用户输入PROJ_ID字段的值。如果用户输入的字符超过了该字段允许的长度,就触发一个异常。

如果用户没有输入任何字符,也触发一个异常。

Procedure TDM.ProjectNewRecord(DataSet: TDataSet);

varValue: String;

Begin

If InputQuery('Project ID','Enter Project ID:',Value) then

Begin

If Length(Value) > ProjectPROJ_ID.Size then

Raise Exception.CreateFmt('Project ID can only be %d characters',[ProjectPROJ_ID.Size]);If Length(Value) = 0 then

Raise Exception.Create('Project ID is required');

End

Else

SysUtils.Abort;

ProjectPROJ_ID.Value := Value;

End;

由于Project数据集与Employee数据集之间存在着Master/Detail关系,当删除Project数据集的一条记录时,应当先删除Employee数据集中关联的记录。应用服务器利用TProvider构件的BeforeUpdateRecord事件实现了这一点。

Procedure TProjectData.ProjectProviderBeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet;DeltaDS: TClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean);

ConstDeleteQuery = 'Delete From EMPLOYEE_PROJECT where PROJ_ID = :ProjID';

Begin

If (UpdateKind = ukDelete) and (SourceDS = Project) then

Begin

UpdateQuery.SQL.Text := DeleteQuery;

UpdateQuery.Params[0].AsString := DeltaDS.FieldByName('PROJ_ID').AsString;

UpdateQuery.ExecSQL;

End;

End;

14.9 一个动态设置查询参数的示范程序

这一节剖析一个动态设置查询参数的示范程序,它可以在C:\ProgramFiles\Borland\Delphi4\ Demos\ Midas\Setparam目录中找到。

这个程序分为应用服务器和客户程序两个部分。当客户程序通过TClientDataSet构件的Params属性设置参数时,这些参数会自动地传递给应用服务器上的TQuery构件,这样就能够根据用户的要求来查询数据库,这就是本示范程序的基本思路。

我们来剖析应用服务器,先看它的数据模块,如图14.24所示。图14.24 数据模块数据模块上只有一个TQuery构件,它的DatabaseName属性设为DBDEMOS,它的SQL语句如下:

Select * From EventsWhere Event_Date >= :Start_Date and Event_Date <= :End_Date Order by Event_Date

可以看出,这个SQL语句中有两个参数,一个是:Start_Date,另一个是:End_Date。

现在我们暂时不管数据模块,再来看看应用服务器的主窗体,如图14.25所示。

图14.25 应用服务器的主窗体

主窗体上显示两个计数,一个是当前连接应用服务器的客户数(Clients),另一个是已经执行的查询次数(Queries)。用什么来判断当前的客户数,这与数据模块的实例方式有关。我们可以回到数据模块的单元,看看它的初始化代码:

Initialization

TComponentFactory.Create(ComServer, TSetParamDemo,

Class_SetParamDemo, ciMultiInstance);

End.

可以看出,这个数据模块的实例方式设为ciMultiInstance,表示每当有一个客户连接应用服务器,就会创建数据模块的一个新的实例。因此,数据模块的实例数就是当前的客户数。怎样统计数据模块的实例数呢?很简单,只要处理数据模块的OnCreate事件。

Procedure TSetParamDemo.SetParamDemoCreate(Sender: TObject);

Begin

MainForm.UpdateClientCount(1);

End;

当一个客户退出连接,将删除一个数据模块的实例,此时将触发数据模块的OnDestroy事件:

Procedure TSetParamDemo.SetParamDemoCreate(Sender: TObject);

Begin

MainForm.UpdateClientCount(1);

End;

其中,UpdateClientCount是在主窗体的单元中定义的:

Procedure TMainForm.UpdateClientCount(Incr: Integer);

Begin

FClientCount := FClientCount + Incr;

ClientCount.Caption := IntToStr(FClientCount);

End;

请注意Incr参数的作用。怎样统计已经执行过的查询数呢?也很简单,只要统计TQuery构件被激活的次数就可以了。因此,程序处理了TQuery构件的AfterOpen事件。

Procedure TSetParamDemo.EventsAfterOpen(DataSet: TDataSet);

Begin

MainForm.IncQueryCount;

End;

IncQueryCount是在主窗体的单元中定义的:

Procedure TMainForm.IncQueryCount;

Begin

Inc(FQueryCount);

QueryCount.Caption := IntToStr(FQueryCount);

End;

编译和运行这个应用服务器。打开客户程序的项目,它的主窗体如图14.26所示。

窗体上有一个TDCOMConnection构件用于连接应用服务器,有一个叫Events的TClientDataSet构件,用于引入数据集。

“Starting Date”框用于输入:Start_Date参数的值,

“Ending Date”框用于输入:End_Date参数的值。中间的栅格用于显示查询的结果。“Description”框用于显示Event_Description字段的值。“Photo”框用于显示Event_Photo字段的值。

客户程序在处理窗体的OnCreate事件的句柄中对“Starting Date”框和“EndingDate”框进行初始化。

Procedure TForm1.FormCreate(Sender: TObject);

Begin

StartDate.Text := DateToStr(EncodeDate(96, 6, 19));

EndDate.Text := DateToStr(EncodeDate(96, 6, 21));

End;

用户可以在这两个框中重新输入其他日期,然后单击“Show Events”按钮。

Procedure TForm1.ShowEventsClick(Sender: TObject);

Begin

Events.Close;

Events.Params.ParamByName('Start_Date').AsDateTime:=StrToDateTime(StartDate.Text);Events.Params.ParamByName('End_Date').AsDateTime :=StrToDateTime(EndDate.Text);

Events.Open;

End;

首先,要调用TClientDataset构件的Close关闭数据集,然后分别设置Start_Date参数和End_Date参数的值,最后,调用TClientDataset构件的Open打开数据集,此时,这两个参数就被自动传递给应用服务器上的TQuery构件。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有