分享到新浪微博 分享到QQ空间 打印

DELPHI基础教程

DELPHI基础教程

第十六章 数据浏览部件的应用及编程(二)
在TDBGrid部件中显示数据库表中的记录信息时,如果TDBGrid使用数据集部件在运行过程中动态生成的字段部件时,TDBGrid显示数据库表中的记录是按表中记录的缺省顺序和字段的缺省顺序显示表中的记录信息,而且要显示表中各个记录的全部字段的值。而在大多数情况下,用户可能希望按自己喜欢的字段顺序显示记录的各个字段,有时还希望只显示记录的部分字段值,要达到这一目的,必须在设计阶段使用字段编辑器来创建永久性的字段部件,并且还要设置各个字段部件有关的属性。

当使用字段编辑器(Fields Editor)创建永久性的字段部件提供给TDBGrid部件来使用时,我们可以在TDBGrid部件中更灵活地显示数据库表中的记录信息。例如在字段编辑器中的Fields列表框中我们可以设定字段部件的显示顺序,在设定好字段的显示顺序之后,TDBGrid部件便按这个顺序显示记录的各个字段值,当我们设置字段部件的DisplayFormat和EditFormat属性之后,在TDBGrid部件中便相应地以设定的显式和编辑格式显示字段值和编辑字段值;当设置某一个字段部件的Required属性为True时,当插入一条新记录时,必须要为该字段输入相应的字段值,否则会出错;通过设置字段部件的Visible属性,可以确定相应的字段值是否在TDBGrid组件中显示。有关使用字段编辑器来创建字段部件,设置字段部件的属性请参看3.6.2节。 

16.4.1 TDBGrid部件的主要属性及应用 

TDBGrid部件是用于显示和编辑数据库表中的记录信息的重要部件,它是我们在程序设计过程当中要经常使用的、灵活地用于显示和编辑数据库表中的记录信息的一个强有力的工具。TDBGrid具有很多重要的属性,我们可以在程序设计阶段和程序运行过程中进行设置。TDBGrid部件的一些重要属性及其设置方法请参看联机帮助文件。TDBGrid部件中一些重要的属性是Option属性、DrawMode属性和DefaultDrawing属性,我们重点对两个属性进行阐述。

Options属性:它是TDBGrid部件的一个扩展属性,在程序设计阶段设置Options属性可以控制TDBGrid部件的显示特性和对事件的响应特性。Options属性在TDBGrid部件的属性栏中显示时,它的前面带有一个“+”标志,双击“+”标志,便可以展开一个布尔型属性列表,用户可以逐个地修改其中的各个属性值,修改完毕后可以双击Options属性前的“-”标志,使TDBGrid部件的属性列表恢复到原来的显示状态。

表16.5列出了Options属性中包含的所有的扩展属性项以及它们对TDBGrid部件的影响。 

表16.5 TDBGrid部件的Options属性中的扩展属性项

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

属 性 名 取 值 及 影 响

─────────────────────────────────

dbEditing True: 缺省情况下为此值,确保用户能够在网格中编辑插

入和删除数据库表中的记录

False:在网格中不能编辑、插入和删除表中的记录

─────────────────────────────────

dbAlwaysShow True: 当用户选中记录中的一个字段时,自动地使该字段

Editor 处于编辑状态

False:缺省情况下为此值。当一个字段被选中,它不能

自动地变成编辑状态

─────────────────────────────────

dgTitles True: 缺省情况下为此值。在网格的第一行中显示字段名

或字段标题

False:在网格中不显示字段名或字段对应的标题

─────────────────────────────────

dgIndicator True: 缺省情况下为此值。在网格的最左边用一个黑箭头

标注当前记录指针所在的位置,在插入状态时,箭

头变成星状,在编辑状时,箭头变成"I"头。

False:在网格中不标识当前记录指针的位置

─────────────────────────────────

dgColumnResize True: 缺省情况下为此值。通过拖拉网格的垂直分隔线可

以改变网格中各列的宽度,在具体操作时要拖拉各

列中显示字段标题区域中的垂直分隔线。

False:网格中各列的宽度不能改变

─────────────────────────────────

dgCloLines True: 缺省情况下为此值。在网格中显示各列之间的垂直

分隔线。

False:在网格中不显示垂直分隔线

─────────────────────────────────

dgRowLines True: 缺省情况下为此值。在网格中显示各行之间的水平

分隔线。

False:在网格中不显示水平分隔线。

─────────────────────────────────

dgTabs True: 缺省情况下为此值。可以在记录的各字段之间移动

输入焦点(也即选择提示棒)

False:不能在记录的名字段之间移动输入焦点,在网格中

按Tab键时,直接跳出网格

─────────────────────────────────

dgRowSelect True: 选择提示棒覆盖整条记录中的全部字段

False:缺省情况下为此值。选择提示棒一次只覆盖记录中

的一个字段

─────────────────────────────────

dgAlwaysShow True: 缺省情况下为此值。在网格始终显示选择提示棒,即

-Selection 使其控件获得焦点时,也是如此。

False:只在当网格获得焦点时,才显示选择提示棒。

─────────────────────────────────

dbConfirmDelete True: 缺省情况下为此值。当在网格中删除记录时,弹出确

认信息。

False:在网格中删除记录时不弹出确认信息。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

用户想了解这些可选属性项的作用和影响,还可以参看联机帮助信息。

DragMode属性:该属性有两个可选的属性值。当它的值被设置为dmManual时,在应用程序运行过程中,用户可以用鼠标拖放网格中的各列,改变各列在网格中的显示顺序和位置。当用鼠标拖放网格中的一列、改变它在网格中的位置时,只是改变了该列在数据集中的位置,并没有改变它对应的数据库表中的位置。当该属性的值被设置成dmAutomatic时,用户不能用鼠标拖放网格中的各列而改变它在网格中的位置。

DefalultDrawing属性:该属性是布尔型属性,它用于控制网格中各网格单元的绘制方式。在缺省情况下,该属性的值为True,也就是说Delphi使用网格本身缺省的方法绘制网格中各网格单元,并填充各网格单元中的内容,各网格单元中的数据根据其对应的字段部件的DisplayFormat属性和EidtFormat属性进行显示和绘制。如果DefaulDrawing属性被设置为False时,Delphi不会自动地绘制网格中各网格单元和网格单元中的数据,用户必须自己为TDBGrid部件的OnDrawDataCell事件编写相应的程序用于绘制各网格单元和其中的数据。

在了解了TDBGrid部件的各个属性之后,我们便可以使用TDBGrid部件来显示和编辑数据库表中的数据了。图16.5所示的应用窗体中各部件的属性设置如表16.6所示。

 

表16.6 各部件的属性设置

━━━━━━━━━━━━━━━━━━━━

属 性 名 属 性 值

────────────────────

Table1.DatabaseName DEMOS

Table1.TableName Customer.DB

Table1.CanModify True

DataSource1.DataSet Table1

DataSource1.AutoEdit True

DBGrid1.Datasource DataSource1

DBGrid1.ReadOnly False

━━━━━━━━━━━━━━━━━━━━

 

在其他数据浏览部件(如TDBEdit)中编辑修改其中的内容时,只要用户用Tab键或鼠标将焦点移到其他部件时,用户对该字段的修改会自动地写回到数据库表中,使用TDBGrid部件编辑修改数据库表时,Delphi是以记录为基本单位将修改写回磁盘上的数据库表的。用户在编辑和修改表中的当前记录时,只有用户将记录指针移到其他的记录时,Delphi才将用户对当前记录的修改写回到磁盘上的数据库表,否则,用户即使改变焦点到窗体中的其他部件,Dephi也不会投寄用户对当前记录的修改。Delphi在向数据库表投寄TDBGrid部件中的被修改的记录时,它会自动地检查所有与当前数据库相连的数据浏览部件的状态,只要其中有任何一数据浏览部件正在修改数据,这时会弹出出错信息,并且当前记录的修改不会被投寄(即被写回磁盘上的数据库表)。

 

16.4.2 TDBGrid部件的事件及应用

 

TDBGrid部件在具有很多重要属性的同时,Delphi也为它赋予了一些事件,以用于控制用户在TDBGrid部件中的操作,我们通过为其中的一些事件编写处理程序,可以有效地控制TDBGrid部件的行为。因为在TDBGrid部件中一次是显示多条记录和记录中的多个字段,也许在实际应用中,不同的用户各有自己特殊的需要,如只想改变其中某一列的值或者控制用户每次只能进出网格中指定的列等等。表16.7列出了TDBGrid部件的主要事件及目的用途。

 

表16.7 TDBGrid部件中的主要事件

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

事 件 名 目 的 用 途

────────────────────────────────

OnColEntor 当用户进入网格各列时,触发该事件

OnColExit 当用户离开网格各列时,触发该事件

OnDblClick 当用户在网格中双击鼠标左键时,触发该事件

OnDragDrop 当用户在网格中用鼠标进行拖放操作时,触发该事件

OnDragOver 当用户在网格中用鼠标拖动网格时,触发该事件

OnDrawDataCell 用于定制绘制网格中各网格单元,当向网格中填充数

据时触发该事件

OnEndDrag 当用户停止拖动网格时,触发该事件

OnEnter 当网格获得焦点时,触发该事件

OnExit 当网格失去焦点时,触发该事件

OnKeyDown 当用户在网格中按下任何键或组合键时,触发该事件

OnKeyPress 当用户在网格中按了任何一个数字键或字母键时,触

发该事件

OnKeyUp 当用户在网格中释放任何被按下的键时,触发该事件

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

上述这些事件在我们开发实际的应用程序时,是很有用处的,读者们可以仔细地研究这些事件的用途,并参看联机帮助信息。

例如,我们可以为TDBGrid部件的OnDblClick事件编写处理程序,当用户在网格中双击鼠标左键时,弹出一个列表框供用户为网格中某一列选择一个字段值。在例16.1中我们创建如图16.5所示的应用,在Object Inspector中设置各字段部件的Visible属性,在网格中只显示CustNo、Company、Country和City字段。我们为DBGrid编写OnDblclick事件处理过程,当用户编辑修改Country字段时,双击鼠标左键便弹出一个列表框ListBox1,其中显示“中国”、“美国”、“日本”、“英国”、“法国”、“俄罗斯”供用户选择,用户单击其中的国家名称后,将相应的洲名选入网格中。 

其中ListBox1的Items属性写入上述国家的名称,并且设置其Visible属性为False。

6.5 TDBNavigator部件及其应用 

TDBNavigator 部件主要用于在数据集中进行记录导航和为用户操纵数据集中的记录提供了一组简单明了的控制按钮。TDBNavigator部件中包含一组控制按钮,用户单击其中的按钮可以向前向后移动记录指针、插入记录、修改现存记录、投寄对记录的修改、取消修改、删除记录;以及刷新记录的显示等。

图16.7为TDBNavigator部件中的控制按钮。 

下表描述了TDBNavigator部件中的各个控制按钮。 

表16.8 TDBNavigator部件中的控制按钮

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

按钮名称 主 要 功 能

─────────────────────────────────

First 将当前记录指针移到数据库表中第一条记录处

Prior 将记录指针移到当前记录的前一条记录处

Next 将记录指针移到当前记录的后一条记录处

Last 将当前记录指针移到数据库表中最后一条记录处

Insert 调用数据集部件的Insert方法,在当前记录的前面

插入一条新记录,并将数据集部件置为插入状态

Delete 删除当前记录,如果TDBNavigator部件的ConfirmDelete

属性设置为true时,会弹出删除确认对话框

Edit 将数据集部件置为编辑状态,以便用户修改当前的记录

Post 投寄对当前记录的修改

Cancel 取消对当前记录的修改,并将数据集部件置为浏览状态

Refresh 清除数据浏览部件的显示缓冲区,并用与其相连的数据

集部件(TTable或TQuery)中的记录刷新显示缓冲区。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

TDBNavigator部件的属性较少。下面我们作一简单的介绍:TDBNavigator部件的一个重要属性是VisibleButtons属性,该属性中包含着多个可选的扩展子属性,主要用于选择TDBNavigator部件中各个控制按钮的显示与否的。因为在实际的应用当中,我们并不需要那么多的控制按钮。如在一个浏览数据库表的应用中,我们一般只需要First、Prior、Next、Last四个按钮就行了,那么我们便在VisibleButtons属性中设置其它的按钮为False即可。

ShowHint属性:该属性是布尔型属性,它用于控制是否显示TDBNavigator部件中各按钮的动态提示信息。缺省情况下该属性的值为False,当设置它的值为True时,当用户将鼠标光标停留在TDBNavigator部件中某一个控制按钮上,超过1秒钟时间后,Delphi便会自动显示有关该控制按钮的提示信息。

Hints属性:在缺省情况下,TDBNavigator部件中的各控制按钮都有相应的动态提示信息,如First、Prior、Next、Last等,用户可以根据自己的需要,通过设置Hints属性可以为各控制按钮设置其他的动态提示信息,用户自己设置的动态提示信息会覆盖原来的提示信息。 

16.6 TDBMemo部件及其应用

 TDBMemo部件主要用于显示和编辑数据库表中的大二进制(BLOB)类型的字段值。TDBMemo部件能够显示多行文本,也允许用户在其中输入和修改多行文本信息,它是Delphi中用来显示和编辑数据库表中的大二进制类型的文本字段的唯一的数据浏览部件。

TDBMemo部件的主要属性和作用如下:

ReadOnly属性:这是布尔型属性,为True时,用户只能在TDBMemo部件中显示BLOB型文本信息,如Paradox和dBASE数据库表中的备注型字段。当为False时,用户在TDBMemo 部件中不仅可以显示BLOB文本信息而且还允许用户编辑修改其中的文本信息。

MaxLength属性:该属性是整数型属性,设置该属性的值用于限制用户向TDBMemo 部件中输入字符的个数。若设置该值为0时,表示输入字符的个数没有限制。

SCrollBar属性:说明TDBMemo部件是否显示滚动条。

WordWrap属性:说明在TDBMemo部件中输入文本信息时,输入到右边界时,是否自动换行。

Alignment属性:说明文本信息在TDBMemo部件中的对齐方式,有三种可选值:taLeftJustify、taCenter和taRightJustify。其含义分别是左对齐,居中和右对齐。

在运行过程中,用户对TDBMemo部件中显示的文本信息是不能够进行剪切、拷贝和粘贴操作的,要想具备这些功能,用户必须编程调用CutToClipboard、CopyToClipboard 和PasteFromClipboard方法分别来实现剪切,拷贝和粘贴操作。

AutoDisplay属性:因为TDBMemo部件中包含着大量的文本信息。应用程序在运行过程中要显示其中的信息需要花费很多的时间,特别是当用户移动记录指针时,都要更新TDBMemo部件中显示的信息,这样会大大减慢程序的运行速度。为此Delphi为TDBMemo部件设定了AutoDisplay属性,用来控制是否自动显示表中的备注型字段。当AutoDisplay设置为False时,在TDBMemo部件中只显示其对应表中的字段名而不显示字段中的文本信息,用户如果想浏览字段中的文本信息,用鼠标左键双击TDBMemo部件的内部即可;当设置AutoDisplay属性为True时,在TDBMemo部件中会自动地显示其对应数据库表中的字段值。

这里要注意的是,TDBMemo部件中显示和编辑文本信息的最大字节数为32K,在使用过程中不要超过这一限制。

图16.8是TDBMemo显示数据库表中备注型字段的情形。该例子在C:\Delphi\DEMOS\DA子目录中,项目名称为FashFact.dpr。 

用TDBMemo部件显示备注型字段 

16.7 TDBImage部件及其应用

 TDBImage部件与TDBMemo部件具有很多相似的属性,它是用来显示和编辑数据库表中的BLOB类型的位图图像字段的。

图16.8中同时也使用了一个TDBImage部件来显示数据库表中的位图图像。

缺省情况下,在TDBImage部件中是允许用户对位图图像进行编辑的,如将图像剪切或拷贝到剪帖板上或从剪帖板上粘帖到TDBImage部件中等操作,同时也可以在程序中调用CutToClipboard、CopyToClipboard和PasteFromClipboard方法来实现剪切、拷贝、粘帖操作,当然要进行上述操作必须确保TDBImage的ReadOnly属性值为False。

TDBImage部件也具有一个AutoDisplay属性,该属性的控制和作用与TDBMemo 部件的AutoDisplay属性是一样的。 

16.8 数据浏览部件中的列表框和组合框 

在数据浏览部件中有四个部件类似于标准部件中的列表框和组合框,这些列表框和组合框主要是在数据库应用程序中为用户提供一系列的可选择的字段值。注意这些部件只能与TTable部件配合使用,而不能与TQuery部件配合使用。 

表16.9 数据浏览部件中的列表框和组合框

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

部 件 名 用 途

──────────────────────────────────

TDBlistBox 在用户修改当前记录中指定的字段值时,可用该部件显示

一个字段值列表供用户选择

TDBComboBox 把一个TDBEdit部件与一个可选的字段值列表结合在一起,

当用户修改当前记录中的字段时,可以直接从部件中输入

新的字段,也可以打开下拉式列表框选择其中的一个可选项。

TDBLookapList 当用户要编辑修改数据库表当前记录的指定字段时,使用

该部件提供多个可选项,这多个可选项是从相关的其它数

据库表中读取的,且以列表框的形式提供给用户

TDBLookupCombo 该部件结合了TDBEdit部件和TDBComboBOx部件的功能,用

户可以直接向该部件中输入字段值,也可以从下拉式列表

框中选择一个可选项,只是下拉式列表框中的可选项是从

相关的其他数据库表中读取来的。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 

16.9 TDBComboBox部件

TDBComboBox部件中包含了TDBEdit部件的全部功能,它们具有相似性,不同的是在运行过程中TDBComboBox部件同时有一个下拉式列表框,在下拉式列表框中有一组可供选择的项供用户选择,这些可选项是在设计阶段由程序设计人员提供给TDBComboBox部件的Items属性的。图16.9是TDBComboBox部件的下拉式列表框中的内容,注意TDBComboBox部件一定要对应数据库表中的一个字段。 

图16.9 TDBComboBox部件在运行过程中 

TDBComboBox部件的一个重要的属性是Items属性,该属性中包含着TDBComboBox部件在运行过程中下拉式列表框中的可选项,Items中的内容可以在设计阶段指定。

如果一个TDBComboBox部件对应着数据库表中一个字段,那么当用户要编辑修改该字段中的值时,可以打开下拉式列表框,从中选择一个可选项作为字段值,也可以自己在TDBComboBox部件中输入一个其他的字段值。

TDBComboBox部件还有下列一些重要的属性,主要用来控制TDBComboBox部件的显示模式和风格的。

Style属性:控制TDBComboBox部件列表框的显示格式的,当它的取值为:

● CSDropDown

缺省情况下为此值,显示一个下拉式列表框和一个编辑框,下拉式列表框中的可选项都是字符串且各选择项占居的高度一样。

● CsSimple

只显示一个列表框,列表框中的可选项都是字符串,且各选项占居一样的高度。

● CSDropDownList

显示一个下拉式列表框和一个编辑框,但用户不能向编辑框中输入一个在列表框中 没有的值。

● CSOwnerDrawFixed和CSOWnerDrawVariable

在列表框中不仅有字符串选项而且还允许有其他类型的选项,如位图图像等,这方 面的详细信息请参看联机帮助。 

DropDownCount属性:允许列表框中显示可选项的最大数目,当可选项数目大于该属性值时,用户可以用滚动条察看全部的可选项,当可选项数小于该属性值时,列表框会自动调整其大小以足够显示全部可选项。

ItemHeight属性:当TDBComboBox部件的Style属性被设置为CSOwnerDrawFixed时,用此属性来设置列表框中每个可选项占居的高度。

Sorted属性:布尔型属性,它决定列表框中的可选项是否按字母的排列顺序排序。 

16.10 TDBListBox部件 

TDBListBox部件的基本功能与TDBComboBox部件基本上是一样的,它们的不同之处在于TDBListBox部件没有下拉式列表框而是一个列表框,在列表框中显示一组供用户选择的可选项,在运行过程中,用户单击其中的可选项可以为TDBListBox 部件对应的字段赋一个字段值,但用户不能自己从键盘上输入一个列表框中不存在的字段值。

如果在应用程序中,TDBListBox对应数据库表中一个具体的字段,那么当在数据集中移动记录指针时,当前记录中对应TDBListBox部件的字段的值在TDBListBox部件的列表框中将以高亮度显示,如果当前记录的该字段值不在列表框中,那么列表框中的可选项没有一项是高亮度地显示的。

TDBListBox的几个属性:

IntegralHeight属性:该属性是用来控制TDBListBox部件中的列表框的显示格式的。当该属性值为True(缺省情况下为此值)时,列表框底部的可选项自动地移到上一次被选用过的可选项的上面。当该属性的值为False时,列表框底部的可选项的显示方式取决于TDBListBox部件的ItemHeight属性,并且列表框底部可选项可能不能被全部地显示出来。


想死你们了!

TOP

DELPHI基础教程

第十七章 SQL编程(一)
SQL语言作为关系数据库管理系统中的一种通用的结构查询语言, 已经被众多的数据库管理系统所采用,如ORACLE、Sybase、Informix等数据库管理系统,它们都支持SQL 语言。Delphi与使用SQL语言的数据库管理系统兼容,在使用Delphi开发数据库应用程序时,我们可以使用SQL语言编程,支持SQL编程是Delphi的一个重要特征,这也是体现Delphi作为一个强大的数据库应用开发工具的一个重要标志。 

17.1 SQL语言简介 

17.1.1 SQL的历史 

在70年代初,E.E.Codd首先提出了关系模型。70年代中期,IBM公司在研制 SYSTEM R关系数据库管理系统中研制了SQL语言,最早的SQL语言(叫SEQUEL2)是在1976 年 11 月的IBM Journal of R&D上公布的。1979年ORACLE公司首先提供商用的SQL,IBM公司在DB2 和SQL/DS数据库系统中也实现了SQL。

1986年10月,美国ANSI采用SQL作为关系数据库管理系统的标准语言(ANSI X3. 135-1986),后为国际标准化组织(ISO)采纳为国际标准。1989年,美国ANSI采纳在ANSI X3.135-1989报告中定义的关系数据库管理系统的SQL标准语言,称为ANSI SQL 89, 该标准替代ANSI X3.135-1986版本。该标准为下列组织所采纳:

● 国际标准化组织(ISO),为ISO 9075-1989报告“Database Language SQL With Integrity Enhancement”

● 美国联邦政府,发布在The Federal Information Processing Standard Publication(FIPS PUB)127

目前,所有主要的关系数据库管理系统支持某些形式的SQL语言, 大部分数据库打算遵守ANSI SQL89标准。 

17.1.2 SQL的优点

SQL广泛地被采用正说明了它的优点。它使全部用户,包括应用程序员、DBA管理员和终端用户受益非浅。

(1) 非过程化语言

SQL是一个非过程化的语言,因为它一次处理一个记录,对数据提供自动导航。SQL允许用户在高层的数据结构上工作,而不对单个记录进行操作,可操作记录集。所有SQL 语句接受集合作为输入,返回集合作为输出。SQL的集合特性允许一条SQL语句的结果作为另一条SQL语句的输入。

SQL不要求用户指定对数据的存放方法。 这种特性使用户更易集中精力于要得到的结果。所有SQL语句使用查询优化器,它是RDBMS的一部分,由它决定对指定数据存取的最快速度的手段。查询优化器知道存在什么索引,哪儿使用合适,而用户从不需要知道表是否有索引,表有什么类型的索引。

(2) 统一的语言

SQL可用于所有用户的DB活动模型,包括系统管理员、数据库管理员、 应用程序员、决策支持系统人员及许多其它类型的终端用户。基本的SQL 命令只需很少时间就能学会,最高级的命令在几天内便可掌握。

SQL为许多任务提供了命令,包括:

● 查询数据

● 在表中插入、修改和删除记录

● 建立、修改和删除数据对象

● 控制对数据和数据对象的存取

● 保证数据库一致性和完整性

 

以前的数据库管理系统为上述各类操作提供单独的语言,而SQL 将全部任务统一在一种语言中。

(3) 是所有关系数据库的公共语言

由于所有主要的关系数据库管理系统都支持SQL语言,用户可将使用SQL的技能从一个RDBMS转到另一个。所有用SQL编写的程序都是可以移植的。

 

17.2 TQuery部件在SQL编程中的运用

 

在Delphi中是通过TQuery部件来实现对SQL语言支持的,也就是说用Delphi 开发数据库应用程序时,使用SQL语言操作数据库中的数据的唯一途径是经过TQuery部件。 TQuery部件在Delphi中使用SQL语言编程时占居着绝对重要的地位。在使用Delphi 开发的数据库应用中,可以使用SQL语言访问下列三个方面的数据库:

● Paradox或dBASE数据库中的表

在访问这些桌面数据库系统中的数据时,只能使用ANSI标准的SQL语言中的部分SQL 语句,它们主要包括:Select、Insert、Update和Delete语句;即本地SQL语句。有关详细情况请参见附录“局部SQL语句的使用”。

● 本地InterBase数据库服务器中的数据库

在InterBase数据库中支持的SQL语句, 在Delphi中都可以使用。有关InterBase中SQL语句的语法和限制,请参看“InterBase的语言参考”。

● 远程数据库服务器中的数据库

当然这要求在Delphi中必须安装相应的SQL Link。只要是数据库服务器上的DBMS支持的SQL语句,在Delphi中都可以使用。有关语法 及限制请参看相关的数据库管理 系统的文档。

 

值得一提的是,Delphi还支持异构查询,即可以同时查询多个数据库服务器中相同的或不同类型的数据库表,例如查询的数据可以是来自ORACLE数据库中的表和Sybase数据库中的表或者其它多个数据库中的表。

 

17.2.1 TQuery部件的使用

 

TQuery部件是一个数据集部件,它在Delphi部件选择板上的数据访问页(Data Access)上,它与TTable部件具有很多共同的特性,我们在第十五章“数据访问部件的应用及编程”中较详细地进行了介绍。 TQuery 部件在 SQL 编程中占居了十分重要的地位。 它实现了Delphi对SQL语言的支持,在Delphi开发的数据库应用中,SQL语句是通过TQuery部件传递到要访问的数据库系统的数据库引擎中,由数据库引擎具体执行SQL语句, 以实现对数据的操作,而不是传递给Delphi中的BDE,由BDE实施具体的SQL动作。

 

我们已经知道了TTable部件在访问数据库时已经具备很强大的功能。TTable部件通过Delphi内置的BDE可以实现对各种数据库系统的访问,然而TQuery部件提供了一些 TTable部件不具备的功能,它们是:

●多表联接查询

●复杂的嵌套查询(Select中包含着Select子查询)

●明确需要按SQL语言进行的操作

 

因为TTabel部件不能使用SQL语言,而在TQuery部件可以使用SQL语言,因而TQuery部件也就具备了强大的关系查询能力。当然这也使数据库应用程序本身变得更复杂了。

在Delphi应用程序中编写和使用的SQL语句有两种:即静态SQL语句、动态SQL 语句。静态SQL语句是在程序设计阶段,将SQL命令文本作为TQuery部件的SQL属性值设置。 而动态SQL语句编程是SQL语句中包含一系列的参数,在程序运行过程中各参数值是可变的,即可以动态地给SQL语句中的参数赋值。

静态方式是把SQL命令文作为TQuery部件的SQL属性值进行设置,这样,当执行应用程序时,Delphi便执行TQuery部件SQL属性中设置的SQL命令。如果是SQL中的查询命令, 把TQuery部件通过TDataSource部件与数据控制部件相连,查询的结果将会显示在与 TQuery部件相连接的数据浏览部件中。动态SQL语句是指SQL语句中包含一些参数变量,在程序中可以为这些参数赋值,在程序运行过程中,各个参数值是变化的。TQuery部件的SQL 属性中的SQL语句的编写也有两种方法,一种方法是在程序设置阶段便将相应的SQL语句写入到TQuery的SQL属性中,另一种方法是在Delphi开发的应用程序将SQL语句,包含在Pascal代码单元中。

在SQL编程中使用TQuery部件的具体方法步骤如下:

①为TQuery部件设置DatabaseName属性,它可以是用BDE建立的数据库的别名, 或桌面数据库系统中的目录名或数据库服务器中的文件名,如果在应用中使用了TDatabase 部件,那么TQuery部件的DatabaseName属性可以是TDatabase部件中定义的一个数据库别名。详细情况请参看“TDatabase部件的使用”;

②为TQuery部件设置SQL属性,TQuery部件的SQL属性值就是应用程序要执行的SQL 命令文本,设置SQL属性有两种方法:

● 在程序设计过程中,我们可以通过对象浏览器(Object Inspector)编辑SQL属性 在Object Inspector中选择SQL属性,这样会打开String List Editor窗口,在其 中我们便可以编写SQL命令,我还可以打开Visual Query Builder来编写SQL命令 (只有Delphi的客户/服务器版本才具有这一工具)。

● 将SQL命令包含在Pascal代码单元中

在程序运行过程中,首先调用TQuery部件的Close方法关闭当前的TQuery部件,然 后调用Clear方法清除SQL属性,并说明新的SQL命令文本,然后调用Add方法,将新的SQL命令文本加入到SQL属性中。

 

③通过调用TQuery部件的Open方法或ExecSQl方法执行 SQL 命令。 Open 方法只执行Select命令,ExecSQL方法还可以执行其它的SQL命令。Open方法和ExecSQL 方法的区别我们在后面的章节里会进一步地加以讨论的。

如果使用动态SQL语句,首先调用prepare方法,给动态SQL语句中的参数赋值, 然后再调用Open方法或ExecSQL方法。调用propare 方法并不是必须的, 但是对于要多次执行TQuery部件中SQL属性中的动态SQL语句,调用Prepare可以大大提高TQuery部件执行SQL语句的性能。 

17.2.2 在TQuery部件中编写简单的SQL查询命令 

在这一节里我们将学习如何使用TQuery部件编写简单的SQL查询命令,并在Delphi 应用程序中实现SQL查询。

例如,如果我们想查询出表Customer.DB中客户的编号和公司名称, 我们按下列步骤来实现:

①在应用窗体中放置一个TQuery部件、一个TDataSource部件一个TDataGrid部件,并将它们连接起来 

②设置窗体TQuery 部件Query1的DatabaseName属性值为DBDEMOS

③双击Object Inspector窗口中Query1的SQL 属性, Delphi 将显示 String List Editor窗口。

④在图17.3中的窗口中输入SQL语句:

Select CustNo,Company From Custormer;

⑤单击OK按钮,关闭String List Editor窗口。

⑥设置Query的Open属性为True。

17.3 SQL语言编程概述 

在Delphi应用程序中的SQL命令语句是包含在TQuery部件的SQL属性中,TQuery部件的SQL属性是TString类型的,也就是说SQL属性值是一个字符串列表, 这个字符串列表非常类似于一个字符串类型的数组,有关TString类型的信息请参看联机帮助。 在前一节里我们介绍了TQuery部件可以执行两种SQL语句:

● 静态SQL语句

● 动态SQL语句 

静态SQL语句在程序设计时便已固定下来,它不包含任何参数和变量, 例如下面的语句便是一条静态SQL语句: 

Select * From Cusromer Where CustNo = 1234; 

而动态SQL语句,也被称作参数化的语句,在其中间包含着表示字段名或表名的参数,例如下面的语句是一条动态SQL语句: 

Select * From Customer Where CustNo =: Number;

 

其中的变量Number便是一个参数变量,它由一个冒号引导,在程序运行过程中,必须要为该参数赋值,该条SQL语句才能正确执行, 每次运行应用程序时可以为该参数变量赋予不同的值。

 

17.3.1 SQL命令文本的编写

 

1. 使用String List Editor编写

我们要为TQuery部件的SQL属性设置SQL命令文本时,可以在应用窗体中选择TQuery部件且双击Object Inspector窗口中的SQL属性,这样便打开了String List Editor 窗口,在该窗口中我们便可以编写各种SQL命令,如图17.3所示。

在编写完适当的SQL语句之后,选择 OK 按钮便可以将编辑器中的 SQL 命令文装入到TQuery部件的SQL属性中,选择SAVE按钮可以将编写好的SQL命令保存到一个文件中供以后编程时使用。我们在编写SQL命令文本时还可以选择Load按钮从一个 SQL 命令文件中调入SQL命令。在程序运行过程中,要想设置TQuery部件的SQL属性,必须首先调用Close方法,关闭TQuery部件,然后再调用Clear方法清除SQL属性中现存的SQL命令语句, 最后再调用Add方法为SQL属性设置新的SQL命令语句。例如:

 

Query1.Close {关闭Query1)

Query1.SQL.Clear {清除SQL属性中的SQL命令语句}

Query1.SQL.Add('Select * From Country');

Query1.SQL.Add('Where Name ="ARGENTINA" ');

 

在为TQuery部件设置SQL属性时调用Close方法总是很安全的,如果TQuery部件已经被关闭了,调用Close方法时不会产生任何影响。在应用程序中为SQL属性设置新的SQL 命令语句时,必须要调用Clear方法以清除SQL属性中现存的SQL命令语句,如果不调用Clear方法,便调用Add方法向SQL属性中设置SQL命令语句,那么新设置的SQL命令语句会追加在现存SQL命令语句后面, 在程序运行时常常会出现出乎意料的查询结果甚至程序无法运行下去。

在这里要特别注意的,一般情况下TQuery部件的SQL属性只能包含一条完整的SQL语句,它不允许被设置成多条SQL语句。当然有些数据库服务器也支持在TQuery部件的SQL属性中设置多条SQL语句,只要数据库服务器允许这样,我们在编程时可以为 SQL 属性设置多条SQL语句。

2. 使用Visual Query Builder编写

客户/服务器版本的Delphi还包含一个可视化的查询构造器Visual Query Builder ,用这个可视化的工具我们只能编写Select语句。在应用程序窗体中选择TQuery部件后,单击鼠标右键,弹出一个弹出式菜单,从中选择Run Visual Query Builder后便会弹出一对话框提示你选择要访问的数据库,选择想要访问的数据库之后选择OK按钮,紧接着会出现一个弹出式对话框提示你选择要查询的数据库表,一次可以选择多个数据库表,若要选择多个数据库表,每选择一个表之后单击Add按钮,接着选择另一个表, 选择完要查询的表之后单击Close按钮,这样,可视化的查询构造器中将会显示出用户选择的数据库表。  

有关如何使用可视化的查询构造器Visual Query Builder 请参看联机帮助信息, 在Visual Query Builder中构造完一个查询并退出Visual Query Builder时,其中的SQL 命令语句会自动地写入相应的TQuery部件的SQL属性。 

17.3.2 SQL程序的执行 

在为TQuery部件设置完SQL属性的属性值之后,也即编写好适当的SQL程序之后,可以有多种方式来执行SQL程序。

在设计过程中,设置完TQuery部件的SQL属性之后将其Active属性的值置为True, 这样便可以执行SQL属性中的SQL程序,如果应用中有与TQuery部件相连的数据浏览部件( 如TDDGrid TDBEdit等)那么在这些数据浏览部件中会显示SQL程序的执行结果。

在应用程序运行过程中,通过程序调用TQuery部件的Open方法或ExecSQL 方法可以执行其SQL属性中的SQL程序。Open方法和ExecSQL方法是不一样的。 大家在程序设计过程中一定要注意。Open方法只能用来执行SQL语言的查询语句(Select命令), 并返回一个查询结果集,而ExecSQL方法还可以用来执行其它常用的SQL语句(如Insert、UPDATE、 DELETE等命令)例如:

 

Query1.Open (这样会返回一个查询结果集)

 

如果调用Open方法,而没有查询结果时,会出错。此时应该调用ExecSQL 方法来代替Open方法。如:

 

Query1.ExecSQL (没有返回结果)

 

当然在设计应用程序时,程序设计人员是无法确定TQuery部件中的SQL 语句是否会返回一个查询结果的。对于这种情况应当用Try…Except模块来设计程序。在 Try 部分调用Open方法,而在Except部分调用ExceSQL方法,这样才能保证程序的正确运行。

例如:

 

Try

Query1.Open

Except

Query1.ExecSQL

End

 

在应用程序中使用TQuery部件时,还可以设置它的UniDirectional属性为True,这样会加快检索数据库表的速度, 但是这样只能往一个方向移动记录指针, 在缺省情况下,UniDirectional属性的值为False。

 

17.3.3 通过TQuery部件如何获得活动的数据

 

我们在前面的章节里介绍TTable部件时,我们知道通过TTable部件从数据库中获得的数据都是活动的,也就是说用户可以直接通过数据浏览部件对这些数据进行编辑修改。而通过TQuery部件可以获得两种类型的数据:

● “活动”的数据

这种数据就跟通过TTable部件获得的数据一样,用户可以通过数据浏览部件来编 辑修改这些数据,并且当调用Post方法或当焦点离开当前的数据浏览部件时,用户对数据的修改自动地被写回到数据库中,详细情况请参看第四章“数据浏览部件的应用及编程”。

● 非活动的数据(只读数据)

用户通过数据浏览部件是不能修改其中的数据。在缺省情况下,通过TQuery部件 获得的查询结果数据是只读数据,要想获得“活动”的数据,在应用程序中必须要设置TQuery部件的RequestLive属性值为True,然而并不是在任何情况下(通过设置RequestLive的属值True)都可以获得“活动”的数据的,要想获得“活动”的数据,除了将TQuery部件的RequestLive属性为True外,BDE要能够返回“活动”的数据,相应的SQL命令语句还要满足附录C中的语法规则和下列的约束条件:

TQuery部件获得“活动”的查询结果数据的约束条件:

 

当查询Paradox或dBASE数据库中的表:

● 查询只能涉及到一个单独的表

● SQL语句中不能包含ORDER BY命令

● SQL语句中不能含聚集运算符SUM或AVG

● 在Select后的字段列表中不能有计算字段

● 在Select语句WHERE部分只能包含字段值与常量的比较运算,这些比较运算符是: Like,>,<,>=,<=,各比较运算之间可以有并和交运算:AND和OR。

 

当通过SQL语句查询数据库服务器中的数据库表:

● 查询只能涉及到一个单独的表

● SQL语句中不能包含ORDER BY命令

● SQL语句中不能含聚集运算符SUM或AVG运算

 

另外,如果是查询Sybase数据库中的表,那么被查询的表中只能有一个索引。

如果在应用程序中要求TQuery部件返回一个“活动”的查询结果数据集,但是SQL 命令语句不满足上述约束条件时,对于本地数据库的SQL查询,BDE只能返回只读的数据集。对于数据库服务器中的SQL查询,只能返回错误的代码。当TQuery 部件返回一个“活动”的查询结果数据集时,它的CanModify属性的值会被设置成True。

 

表17.1 TQuery部件返回查询结果数据的类型

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

RequestLive属性值 CanModify属性值 查询结果的类型

────────────────────────────────

False False 只读数据

True(SQL语句满足约束条件) True “活动”数据

True(SQL语句不满足约束条件) False 只读数据

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

当TQuery部件返回只读的查询结果数据集,而用户又希望修改这只读的数据集时,一般这样来处理, 在应用程序中另外增加一个 TQuery 部件 Query2( 假设获得只读结果的TQuery部件的名字是Query1),在Query2中设置修改语句UpDATE对Query1 中的数据进行修改操作,这样会实现对只读数据的修改。

 

17.4 动态SQL语句的编程

 

在17.3节中,我们已经介绍了动态SQL语句(又被称为参数化的SQL语句),在其中包含在程序过程中可以变化的参数,在实际的程序设计中使用得更多的是动态SQL语句, 因而在这一节里我们重点介绍如何给动态SQL语句的参数赋值, 以在应用程序中灵活地使用SQL语句。动态SQL语句的编写、执行等等与17.3节中介绍的SQL语句的编写、 执行是一样的。

动态SQL语句中的参数,我们可以通过两种途径来为它赋值:

1. 利用参数编辑器(Parameter Editor)来为参数赋值

具体方法是:选中TQuery部件,单击鼠标右键,然后从中选择Define Parameters 便可以打开参数编辑器。 

例如,在TQuery部件的SQL属性中我们设置如下的SQL语句: 

Setect * From Customer Where CustNO=:Number;

 

TQuery的DatabaseName属性为DBDEMOS,其中Number为参数变量。我们便可以为参数Number赋值,在Datetype组合框中选择该参数的数据类型为整数Integer,在Value编辑框中可以为参数Number赋一个值,也可以单击Null Value检查框为参数Number赋一个空值Null。给参数赋值之后,单击OK按钮,这样TQuery部件中的SQL 查询便准备好了,而且参数值也被赋给了动态SQL语句中相应的参数,此时当把TQuery 部件的Active属性设置成True时,在与TQuery部件相连的数据浏览部件中会显示出查询结果,通过参数编辑器为参数赋值,这种方式缺乏应有的灵活性,在实际应用中用得较少,在实际应用中程序设计人员希望用更灵活方便的方式为参数赋值,那就是我们接下来要介绍的另一种途径:

2. 在运行过程中,通过程序为参数赋值

用这种方式为参数赋值有三种方法:

①根据参数在SQL语句中出现的顺序,设置TQuery部件的Params属性值为参数赋值。

②直接根据SQL语句中各参数的名字,调用ParamByName方法来为各参数赋值。

③将TQuery部件的DataSource属性设置为另一个数据源,这样将另一个数据源中与当前TQuery部件的SQL语句中的参数名相匹配的字段值赋给其对应的参数。

这三种方法我们将在下面的三小节中具体地介绍

 

17.4.1 使用Params属性为参数赋值

 

TQuery部件具有一个Params属性,它们在设计时不可用,在程序运行过程中可用,并且是动态建立的,当为TQuery部件编写动态SQL 语句时, Delphi 会自动地建立一个数组Params,数组Params是以0下标开始的,依次对应动态SQL 语句中的参数, 也就是说动态SQL语句中第一个参数对应Params[0],第二个参数对应params[1],依此类推。

例如:一个TQuery部件Query1,我们为它编写的动态SQL语句是:

 

Insert Into Customer(CustNo,Name,Country)

Values(:CustNo,:Name, : Country)

 

对于上述这条动态SQL语句中的参数,我们可以利用TQuery部件的params 属性为参数赋值:

 

Query1.params[0].AsString := "1988";

Query1.params[1].AsString := "Lichtenstein";

Query1.params[2].AsString := "USA";

 

上述语句将把"1988"赋给参数:Cuse_No,"Lichtenstein"赋给参数:Name,"USA"赋给参数:Country。

 

17.4.2 使用ParamByName方法为参数赋值

 

ParamByName是一个函数,用动态SQL语句中的参数作为调用ParamByName函数的参数,这样便可以为它们赋值,使用这种赋值方法,必须要知道动态SQL语句参数的名字。

例如在17.4.1节中的例子中,也可以用下述方法给参数赋值:

 

Query1.ParamByName('CustNo').AsString := "1988";

Query1.ParamByName('Name').AsString := "Lichtenstein";

Query1.ParamByName('Country').AsString := "USA";

 

使用这种方法同样可以为各参数赋值,而且更加直观一些。

 

17.4.3 使用Datasource属性为参数赋值

 

上述两种方法的共同特点是:我们在为各参数赋值时,我们是知道各参数对应的具体参数值的。而在具体的应用程序中,有些参数值常常是无法确定的,例如参数值来自于另一个查询结果,对于这种情况,Delphi提供了使用Datasource属性为动态SQL 语句中尚存在没有赋值的参数时, Delphi 会自动检查 TQuery 部件的 Datasource 属性, 如果为Datasource属性设置了属性值(该属性的值是另一个TDatasource部件的名字),Delphi 会把没有赋值的参数与TDatasource部件中的各字段比较,Delphi 会将相应的字段值赋给与其相匹配的参数,利用这种方法也能实现所谓的连接查询,我们在学习使用TTable部件时,便会创建主要--明细型数据库应用,用TQuery部件创建的连接查询与主要- -明细型应用是相似的。

例如:在如图17.7所示的应用中,设置了下列部件:

● 一个TTable部件

名字为Cust,它的DatabaseName属性为DEMOS,TableName属性为Customer。

● 一个TDatasource部件

名字为Custsource,其Dataset属性被设置为Cust。

● 一个TQuery部件

名字为ORDERS,其DatabaseName被设置为DEMOS,SQL属性值为:

 

Select Orders.CustNo,Orders.OrderNo,Orders.SaleDate FROM Orders

WHERE Orders.CustNo =: CustNo

 

ORDERS的DataSouce属性被设置为CustSource

● 一个TDatasource部件

名字为OrderSource,其DataSet属性被设置为Orders。

● 两个TDBGrid部件

它们分别连接CustSource和OrderSource。

TQuery部件Orders中的动态SQL语句中的参数:CustNo在程序设计过程中没有给它赋值,当该应用程序运行时Delphi会自动地到其Datasource属性中说明的数据源CustSource中查找与参数:CustNo匹配的字段,而CustSource中正好有一个名字为 CustNo 的字段与参数:CustNo匹配,这样Customer表中的CustNo字段值被赋给了参数 : CustNo , 而当每移动Customer表中的记录指针,参数:CustNo的值会随之改变,而参数:CustNo的值发生改变时,Orders中的动态SQL语句会根据新的参数值重新查询,从数据库表中获取相应的订单数据,这样也变实现了类似于主要--明细型应用。即连接查询。 

17.4.4 Prepare方法的使用 

在使用动态SQL语句编程时,常常用到一个很重要的方法prepare,调用prepare 方法之后,Delphi会将带参数的SQL语句传送给与其对应的数据库引擎,对动态SQL语句进行语法分析和优化。虽然在用动态SQL语句编程时,调用prepare方法并不是必须的,但是这里我们要极力推荐调用prepare方法,因为调用prepare方法后,会大大提高动态SQL 语句的执行性能,特别是当要反复多次执行同一条动态SQL语句时,其优越性会更加明显。 如果在应用程序中执行一条SQL语句之前并没有显式地调用prepare方法,每次在执行SQL 语句时,Delphi会隐含地调用propare方法以准备这个查询。

TQuery部件还有一个prepare属性,这是一个布尔型属性,当其属性值为True时, 表明该查询已被准备好了( SQL 语句已被传送到数据库引擎中 ) , 当我们使用参数编辑器Parameters Editor来为动态SQL语句中的参数赋值时,当设置完相应的参数值并退出参数编辑器时,Delphi会隐含地调用prepare方法以准备好查询。

当SQL语句执行完之后,要想准备下一个查询,首先必须调用close方法,然后才能调用prepare方法准备下一个查询。一般来说,在一个应用程序中应该调用一次prepare方法,常常在窗体的OnCreate事件处理过程中调用prepare方法, 然后用上述介绍的方法为参数赋值,最后调用Open方法或ExecSQL方法执行SQL语句,以完成查询。

当然在调用prepare方法准备好一个查询时,会消耗一些数据库资源, 因而每当一个查询执行完毕之后,要养成调用Unprepare方法以撤消查询的好习惯。在运行程序过程中,通过程序改变TQuery部件的SQL属性值时,Delphi会自动地调用Close方法和Unprepare 方法,以撤消查询。


想死你们了!

TOP

DELPHI基础教程

第十七章 SQL编程(二)
17.5 SQL编程实例 

我们在学习了SQL程序的编写方法之后,我们便可以着手创建自己的应用程序了, 通过创建应用程序我们对Delphi的强大功能就会有更深刻的印象,同时会进一步全面掌握有关SQL编程的知识,在本节中我们主要介绍两个例子,前一个例子主要是用静态的SQL语句编程,后一个例子是用动态SQL语句编程。 

17.5.1 设计简单的SQL程序编辑器 

例17.1:在这个例子中,我们设计一个交互式的SQL程序编辑器, 在这个编辑器中,我们可以根据SQL语言的语法规则,编写常用的SQL命令,并通过单击编辑器中的有关的按钮,直接执行编写好的SQL命令,SQL命令的执行结果也会及时地通过一个TDBGrid 部件显示出来。 

表17.3 SQL编辑器中个主要部件的属性

━━━━━━━━━━━━━━━━━━━━

部 件 属 性 值

────────────────────

Form1 Caption=SQL程序编辑器

DBGrid1 DataSource=DataSource1

Button1 Caption=执行(&E)

Button2 Caption=清除(&C)

Button3 Caption=退出(&X)

Button3 kind=bkClose

Memo1

DataSource1 DataSet=Query1

Query1 DatabaseName=DEMOS

━━━━━━━━━━━━━━━━━━━━

 

因为我们在设置Query1的DatabaseName属性时将其设置为DEMOS, 所以我们设计的这个SQL程序编辑器只能对DEOMS中的数据库表进行操作。

单击按钮Button1的事件处理过程代码为:

 

程序清单17.1

 

procedure TForm1.Button1Click(Sender:TObject);

begin

Query1.close;

Query1.SQL.clear;

Query1.SQL.Add(Memo1.text);

Query1.Open;

end;

 

单击按钮Button2的事件处理过程为:

 

程序清单17.2

 

procedure TForm1.Button2Click(Sender:TObject);

begin

Query1.close;

Query1.SQL.clear;

Query1.ExceSQL;

end;

 

下面我们对程序清单17.1和程序清单17.2中的程序代码进行简要的分析:

程序清单17.1中的程序代码是用来执行查询的。

 

Query1.close;

 

这一行程序是用来关闭Query1的,我们在前面的章节中介绍过,只有在调用close 方法将TQuery部件关闭之后,才能修改其SQL属性值,执行close命令关闭查询是很安全的,如果查询已经被关闭了,调用该方法不会产生任何影响。

 

Query1.SQL.clear;

 

因为TQuery部件的SQL属性只能包含一条SQL语句,调用Clear 方法的目的是为了清除SQL属性原来的属性值即原来的SQL命令语句,如果不调用clear方法清除原来的SQL命令语句,当在后面的程序中调用Add方法为SQL属性设置新的SQL命令语句时,Delphi 会将新的SQL命令语句加在原来的SQL命令语句,这样使得SQL属性中包含两条独立的SQL语句,这是不允许的。

 

Query1.SQL.Add(Memo.text);

 

该条命令是将SQL编辑器的编辑区内的内容(TMemo部件Memo1)设置成Query1的SQL属性值。

 

Query1.open;

 

该语句用来执行Query1中的SQL命令语句, 如果执行查询从数据库中获得查询结果,查询结果会在数据网格DBGrid1中显示出来。

程序清单2是用来清除查询的, 其前两行语句跟程序清单1中的代码是一样的。Query1.ExecSQL有一些特别,调用ExecSQL方法也是打开Query1,ExecSQL方法与open方法不一样的,请参看前面的章节,当Query1中SQL属性值为空时,即没有SQL语句时,只能调用ExecSQL方法来打开Query1,如果调用 open 方法会返回一个错误。 在执行完 Query1.ExecSQL语句之后,应用程序将会清除数据网格DBGrid1中的所有内容。 

17.5.2 设计一个数据库查询器 

例17.2:在数据库查询器中,用户可以选择要查询的数据库,查询数据库中的那一个表、根据数据库表中那一个字段进行查询,并且可以方便地指定查询条件,指定查询条件主要包括指定逻辑运算符(=、>、<、<=、>=、like、in、NOT like、NOT in)和字段值。

例子全部的程序清单如下:

unit main;

 

interface

 

uses

SysUtils, Windows, Messages, Classes, Graphics, Controls,

Forms, Dialogs, StdCtrls, DB, DBTables, Buttons, ComCtrls, Tabnotbk;

 

type

TQueryForm = class(TForm)

BitBtn1: TBitBtn;

DataSource1: TDataSource;

Table1: TTable;

GroupBox1: TGroupBox;

CheckBox1: TCheckBox;

CheckBox2: TCheckBox;

PageControl1: TPageControl;

TabSheet1: TTabSheet;

Label5: TLabel;

Label1: TLabel;

Label2: TLabel;

Label3: TLabel;

Label4: TLabel;

ListBox1: TListBox;

ListBox2: TListBox;

ListBox3: TListBox;

Edit1: TEdit;

ComboBox1: TComboBox;

BitBtn2: TBitBtn;

TabSheet2: TTabSheet;

Memo1: TMemo;

procedure FormCreate(Sender: TObject);

procedure ListBox1Click(Sender: TObject);

procedure ListBox2Click(Sender: TObject);

procedure BitBtn2Click(Sender: TObject);

end;

 

var

QueryForm: TQueryForm;

 

implementation

 

{$R *.DFM}

 

uses RSLTFORM;

 

procedure TQueryForm.FormCreate(Sender: TObject);

begin

Screen.Cursor := crHourglass;

 

{ Populate the alias list }

 

with ListBox1 do

begin

Items.Clear;

Session.GetAliasNames(Items);

end;

 

{ Make sure there are aliases defined }

 

Screen.Cursor := crDefault;

if ListBox1.Items.Count < 1 then

MessageDlg( 'There are no database aliases currently defined. You

need at least one alias to use this demonstration.',

mtError, [mbOK], 0 );

 

{ Default the drop-down list to the first value in the list }

ComboBox1.ItemIndex := 0;

end;

 

procedure TQueryForm.ListBox1Click(Sender: TObject);

var

strValue: string; { Holds the alias selected by the user }

bIsLocal: Boolean; { Indicates whether or not an alias is local }

slParams: TStringList; { Holds the parameters of the selected alias }

iCounter: Integer; { An integer counter variable for loops}

begin

 

{ Determine the alias name selected by the user }

 

with ListBox1 do

strValue := Items.Strings[ItemIndex];

 

{ Get the names of the tables in the alias and put them in the

appropriate list box, making sure the user's choices are reflected

in the list. }

 

ListBox2.Items.Clear;

Session.GetTableNames(strValue, { alias to enumerate }

'', { pattern to match }

CheckBox1.Checked, { show extensions flag }

CheckBox2.Checked, { show system tables flag }

ListBox2.Items); { target for table list }

 

{ Make sure there are tables defined in the alias. If not, show an

error; otherwise, clear the list box. }

 

Screen.Cursor := crDefault;

if ListBox2.Items.Count < 1 then

MessageDlg('There are no tables in the alias you selected. Please

choose another', mtError, [mbOK], 0 );

 

ListBox3.Items.Clear;

end;

 

procedure TQueryForm.ListBox2Click(Sender: TObject);

begin

Screen.Cursor := crHourglass;

try

{ First, disable the TTable object. }

if Table1.Active then

Table1.Close;

 

{ Open the selected table }

 

with ListBox1 do

Table1.DatabaseName := Items.Strings[ItemIndex];

 

with ListBox2 do

Table1.TableName := Items.Strings[ItemIndex];

 

{ Open the table and put a list of the field names in the Fields

list box. }

 

Table1.Open;

if Table1.Active then

Table1.GetFieldNames(ListBox3.Items);

finally

Screen.Cursor := crDefault;

end;

end;

 

procedure TQueryForm.BitBtn2Click(Sender: TObject);

var

strAlias, { Alias name selected by the user }

strTable, { Table name selected by the user }

strField, { Field name selected by the user }

strValue, { Field Value entered by the user }

strWhere, { WHERE clause for the user's query }

strQuote, { Holds quotes is the query field is text }

strQuery: string; { String used to construct the query }

frmQuery: TResultForm; { The Results form }

type

 

{ The following type is used with the Type drop-down

list. The text values corresponding with each item is

described in comments, along with the relevant SQL operators. }

 

etSQLOps = (soNoCondition, { not field conditions: no WHERE clause }

soEqual, { equals: = }

soNotEqual, { is not equal to: <> }

soLessThan, { is less than: < }

soLessEqual, { is less than or equal to: <= }

soMoreThan, { is greater than: > }

soMoreEqual, { is greater than or equal to: >= }

soStartsWith, { starts with: LIKE xx% }

soNoStartsWith, { doesn't start with: NOT LIKE xx% }

soEndsWith, { ends with: LIKE %xx }

soNoEndsWith, { doesn't end with: NOT LIKE %xx }

soContains, { contains: LIKE %xx% }

soNoContains, { doesn't contain: NOT LIKE %xx% }

soBlank, { is blank: }

soNotBlank, { is not blank: }

soInside, { contains only: IN ( xx, yy, zz ) }

soOutside); { doesn't contain: NOT IN (xx, yy, zz) }

begin

 

{ Initialize the variables needed to run the query }

 

with ListBox1 do

if ItemIndex = -1 then

raise Exception.Create('Can''t Run Query: No Alias Selected')

else

strAlias := Items.Strings[ItemIndex];

 

with ListBox2 do

if ItemIndex = -1 then

raise Exception.Create('Can''t Run Query: No Table Selected')

else

strTable := Items.Strings[ItemIndex];

 

with ListBox3 do

if ItemIndex = -1 then

begin

if ComboBox1.ItemIndex > Ord(soNocondition) then

raise Exception.Create('Can''t Run Query: No Field Selected')

else

strField := '';

end

else

strField := Items.Strings[ItemIndex];

 

if (Edit1.Text = '') and

(ComboBox1.ItemIndex > Ord(soNoCondition)) and

(ComboBox1.ItemIndex < Ord(soBlank)) then

raise Exception.create('Can''t Run Query: No Search Value Entered')

else

strValue := Edit1.Text;

 

{ See if the field being search is a string field. If so, then pad the

quote string with quotation marks; otherwise, set it to a null value. }

 

if strField <> '' then

with Table1.FieldByName(strField) do

if (DataType = ftString) or (DataType = ftMemo) then

strQuote := '"' else

strQuote := '';

 

{ Construct the WHERE clause of the query based on the user's choice

in Type. }

 

case etSQLOps(ComboBox1.ItemIndex) of

soNoCondition: strWhere := '';

soEqual: strWhere := strField + ' = ' + strQuote + strValue+ strQuote;

soNotEqual: strWhere := strField + ' <> ' + strQuote + strValue +

strQuote;

soLessThan: strWhere := strField + ' < ' + strQuote + strValue +

strQuote;

soLessEqual: strWhere := strField + ' <= ' + strQuote + strValue +

strQuote;

soMoreThan: strWhere := strField + ' > ' + strQuote + strValue +

strQuote;

soMoreEqual: strWhere := strField + ' >= ' + strQuote + strValue +

strQuote;

soStartsWith: strWhere := strField + ' LIKE ' + strQuote +

strValue + '%' + strQuote;

soNoStartsWith: strWhere := strField + ' NOT LIKE ' + strQuote +

strValue + '%' + strQuote;

soEndsWith: strWhere := strField + ' LIKE ' + strQuote +

'%' + strValue + strQuote;

soNoEndsWith: strWhere := strField + ' NOT LIKE ' +

strQuote + '%' + strValue + strQuote;

soContains: strWhere := strField + ' LIKE '+ strQuote+'%'+ strValue

+ '%' + strQuote;

soNoContains: strWhere := strField + ' NOT LIKE ' + strQuote + '%'

+ strValue + '%' + strQuote;

soBlank: strWhere := strField + ' IS NULL';

soNotBlank: strWhere := strField + ' IS NOT NULL';

end;

 

if ComboBox1.ItemIndex = Ord(soNoCondition) then

strQuery := 'SELECT * FROM "' + strTable + '"'

else if Table1.FieldByName(strField).DataType = ftString then

strQuery := 'SELECT * FROM "' + strTable + '" t WHERE t.' + strWhere

else

strQuery := 'SELECT * FROM "' + strTable + '" t WHERE t.' + strWhere;

 

{ Create an instance of the browser form. }

frmQuery := TResultForm.Create(Application);

 

{ Use a resource protection block in case an exception is raised. This

ensures that the memory allocated for the Results form is released. }

try

with frmQuery do

begin

Screen.Cursor := crHourglass;

if Query1.Active then Query1.Close;

Query1.DatabaseName := strAlias; {set the alias the query poitns to}

Query1.SQL.clear; { empty existing SQL in the query }

Query1.SQL.Add(strQuery); { add query string to query object }

Query1.Active := True; { try to run the query }

Screen.Cursor := crDefault;

 

if Query1.Active then

begin

{ If the query didn't return any records, there's no point in

displaying the form. In that event, raise an exception. }

if Query1.RecordCount < 1 then

raise Exception.create('No records matched your criteria.

Please try again.' );

 

{ write a message to the browse form's status line }

if strField = '' then

Panel3.Caption := 'Now showing all records from ' + strTable

+ '...'

else

Panel3.Caption := 'Now showing '+ strTable +' where '+ strField

+' contains values equal to '+ strValue + '...';

 

{ show the form }

ShowModal;

end;

end;

finally

frmQuery.Free;

end;

end;

 

end.

 

 

unit RSLTFORM;

 

interface

 

uses

SysUtils, Windows, Messages, Classes, Graphics, Controls, StdCtrls, DB,

Forms, DBCtrls, DBGrids, DBTables, Buttons, Grids, ExtCtrls, Dialogs;

 

type

TResultForm = class(TForm)

DBGrid1: TDBGrid;

DBNavigator: TDBNavigator;

Panel1: TPanel;

DataSource1: TDataSource;

Panel2: TPanel;

Panel3: TPanel;

Query1: TQuery;

SpeedButton2: TSpeedButton;

Panel4: TPanel;

SpeedButton1: TSpeedButton;

procedure SpeedButton1Click(Sender: TObject);

procedure SpeedButton2Click(Sender: TObject);

end;

 

var

ResultForm: TResultForm;

 

implementation

 

{$R *.DFM}

 

procedure TResultForm.SpeedButton1Click(Sender: TObject);

begin

Close;

end;

 

procedure TResultForm.SpeedButton2Click(Sender: TObject);

var

strText: string; { Variable to hold display text }

iCounter: Integer; { Loop counter variable }

begin

 

{ Build a string containing the query }

 

strText := '';

for iCounter := 0 to Query1.SQL.Count - 1 do

strText := strText + Query1.SQL[iCounter];

 

{ Display the query text }

 

MessageDlg('The underlying query is: ' + #10 + #10 + strText,

mtInformation, [mbOK], 0 );

end;

 

end.


想死你们了!

TOP

DELPHI基础教程

第十八章 Delphi客户服务器应用开发(一)

  客户/服务器的开发工作涉及定义客户/服务器的体系结构, 然后再将该结构与其它一些对于客户/服务器的实现至关重要的系统结构和技术集成起来。Delphi 2.0的Client/Sever版支持用户开发客户/服务器结构的应用程序。本章中我们将阐述客户服务器体系结构原理、如何用Delphi构建客户/服务器的环境和Delphi存取远程SQL服务器的编程和注意事项。

 

18.1 Delphi客户/服务器应用开发原理

 

18.1.1 客户/服务器体系结构

 

18.1.1.1 体系结构概述

 

  客户/服务器系统的体系结构有以下两个特点:

● 是集合智能用户工作站作为有效平台使用

● 平台和软件之间的互操作性

 

客户/服务器结构包括连接在一个网络中的多台计算机。那些处理应用程序,请求另一计算机的服务的计算机称为客户机(Client)。而处理数据库的计算机称为服务器(Server)。所有用户都拥有他们自己的计算机来处理应用程序。

客户机计算机可以是大型机、小型机或微机。但是由于微机具有成本的优势,因而通常选择它们作为客户机。同样地,服务器通常是一台微机但在需要较大能力时,也可以使用一台大型机或小型机。在数据库环境下,通过若干称作中间件(Middleware)的程序设计接口,客户机可以与服务器通信。这些接口提供应用程序和数据库之间的连通性。

 

 

 

图18.1 客户/服务器体系结构

 

  虽然图18.1只有一个服务器,但客户/服务器结构也可以包括多个服务器。然而在这种情况下,每个服务器必定只处理一个不同的数据库或提供一个唯一的服务。(注意:使用两上或多个服务器来处理同一个数据库的结构不认为是客户/服务器系统,相反它是一个分布式数据库系统〕

  表18.1归纳了计算机在客户/服务器系统中的既定作用。

 

  表18.1 客户机和服务器计算机的作用

    ━━━━━━━━━━━━━━━━━━━━━━━━━━━

  客户机功能         服务器功能

   ───────────────────────────

  管理用户接口        从客户机接受数据库请求

  从用户接受数据       处理数据库请求

  处理应用逻辑        格式化结果并传送给客户机

  产生数据库请求       执行完整性检查

   向服务器发送数据库请求   提供并行访问控制

  从服务器接收结果      执行恢复

   格式化结果         优化查询和更新处理

    ━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

18.1.1.2 客户机概述

 

  如上所述,客户机运行那些使用户能阐明其服务请求的程序,并将这些请求传送到服务器。由客户机执行的计算称为前端处理(front-end processing)。前端处理具有所有与提供、操作和显示数据相关的功能。

  客户机软件由网络接口软件、支持用户需求的应用程序以及实现网络能力的实用程序【例如电子邮件(E-Mail)和群件(Groupware)】组成。网络接口软件提供各种数据传输服务。应用程序软件执行具体的任务,如字处理、电子表格和数据库查询生成。实用程序软件通常执行几乎所有网络用户都要求的标准任务。

 

18.1.1.3 服务器概述

 

  在服务器上执行的计算称为后端处理(back-end processing)。后端硬件(back- end hardware)是一台管理数据资源并执行数据库引擎功能(如存储、操作和保护数据)的计算机。在大型机环境下,后端网络(back-end network)提供大型计算机至大容量存储设备、控制器以及文件服务器的连接。在识别、评价和选择适当的服务器平台时,必须考虑将由该平台提供的服务。例如,一个数据库服务器可能需要快速处理能力。其他可能需要执行的网络服务有通信、应用程序、文件访问以及只读存储器(CD-ROM)服务。随着新的应用技术的广泛使用,可被提供的潜在服务还将继续增加。

  服务器软件既包括遵循于OSI或其它网络结构的网络软件,又包括由该服务器提供给网络上客户机的应用程序或服务软件。

 

 

 

18.1.1.4 中间件概述

 

  中间件是一个软件层,它保护应用程序开发人员避免受到各种通信协议、操作系统以及数据库管理系统的影响。它为建立可与以前沿袭下来的应用程序并存的新应用程序打下了基础。

  中间件有好几种类型。它们包括应用程序设计接口(API),远程过程调用(RPC),网络通信、数据库访问以及计算机辅助软件工程(CASE)工具。

  由于客户/服务器系统需要集成各种不同结构的机器和技术,因而应用程序设计相当复杂。选择适当的中间件可以消除程序设计人员为每个单独协议和操作系统编写代码的麻烦。

 

18.1.2 关系数据库体系结构与客户/服务器模式

 

18.1.2.1 关系数据库概述

 

  关系数据库被定义为一种特殊的数据库,其中各个文件(称作关系)以平面文件(FlatFiles)或表的形式保持数据。表必须只含有一种记录类型。每个记录具有固定数目的字段,所有字段皆显示命名。表内的字段内容是各不相同的,不允许重复组(repeating groups)。不含有复制记录和预定的记录序列。

  在构造关系数据库时,必须特别注意关系的内容以及记录的各属性(字段)之间的内在联系。

  关系数据库上的基本操作有选择、投影、连接和除法,选择建立一个含有与原始关系相同列数的新表,但是行只包括那些满足某些特写标准的原始关系行。投影操作指定将被选择的列,因而形成的表只含有原始表列的一个子集。如果在投影操作删除的列中有两个行不同,那么将只有一个记录被转入新的关系。连接操作从两个或多个表中组合信息。两个表中的公用字段用作组合记录的基础字段。在公用字段中具有相等值的记录被连接在结果关系内。

 

18.1.2.2 关系数据库实现的任务

 

  实现关系数据库所涉及的任务分为三组:

● 为DBMS定义数据库结构的任务

● 将数据库分配给物理存储介质的任务

● 建立数据库数据的任务

 

执行这些任务的方法取决于所采用的DBMS产品。

  各种不同的DBMS产品提供定义数据库结构的实用程序。这些实用程序使用一种专用的数据定义语言(DDL)。某些DBMS产品含有一些规定,一旦数据库已被定义到DBMS,即将该数据库分配到物理介质。根据应用程序处理的特点,数据可以定位在指定表上或定位在同一磁盘上。它有些DBMS产品偏重于数据库数据的建立。

  如上所述,数据库定义、存储分配以及数据建立过程都将取决于应用需求和所选择的特定DBMS产品的特征。

 

 

 

18.1.2.3 关系数据操作

 

  为了开发数据库应用,需要使用一种语言来表达处理逻辑。关系数据操作语言共有四类:

  ● 关系代数,它是一种语言,提供一组远算符处理关系数据库中的关系

  ● 关系演算,它是一种语言,在该语言中用户指定一组来自关系数据库内数据操作的结果

  ● 面向变换的语言,它们构成一类非过程语言,这类语言将表示为关系的输入数据变换成表示为单个关系的结果。SQL就是一种面向变换的语言

  ● 面向图形的系统,它们为用户提供一个关系结构的图形,如Borland的Paradox 和IBM公司的QBE(Query By Example)

 

  用户可以采用多种方法与关系数据库进行联系:

● 某些DBMS产品,包括有生成表格的工具并提供表格和报告的处理

● 通过查询语言提供一个接口,它们执行查询和更新功能;最重要的查询语言是SQL

● 与关系数据库联系的第三种方法是通过应用程序

 

18.1.2.4 扩展关系系统

 

  众多的销售商都在积极扩展关系模型。这些扩展包括在关系表中存储复杂数据类型、存储过程、触发器以及二进制大对象(BLOB)。目前正在SQL 3标准中考虑的SQL扩展将包括对对象的进一步支持,还有可能包括对用户定义数据类型及嵌套表的支持。向关系模型提供对象扩展的产品包括Sybase、Informix、Oracle和Borland。

 

18.1.2.5 SQL:集成客户/服务器体系结构的基本链路

 

  SQL为前面讨论的集成客户/服务器体系结构提供一条基本链路。目前美国国家标准局(ANSI)已认可SQL作为操作数据库的正式工业标准。它是许多数据库管理系统(DBMS)产品都采用的数据存取语言。

  SQL允许用户在关系表数据上进行查询、建立新表、存取现有的远程表、操作数据、建立应用程序存取SQL数据,运行SQL语句,处理错误以及访问多个服务器。SQL数据库服务器是多用户关系数据库管理系统(DBMS)。

  SQL可以作为一个查询语言用于交互式使用或嵌入在应用程序中。在执行查询时,SQL接受一个或多个关系作为输入并产生一个关系作为输出,结果是一个表或平面文件,例如,一批不含有重复组的同一类型记录。在查询多个表时,SQL将这些表连接起来。SQL内还含有一些规定,用来向表中插入新数据、从表中删除数据或修改表中的数据。

 

18.1.3 各种数据库服务器功能介绍

 

  服务器数据管理包括若干软件,它们使用户可以访问网络中的任何节点以及确保多用户环境下的保密性、可恢复性和完整性。如前面所提到的,客户/服务器计算中的基本存取链路是SQL,它是一种高级非过程数据库语言, 现在已开发出很多支持SQL 的后端服务器及DBMS。下面将描述这些产品。

 

18.1.3.1 DB2

 

  DB2是一种由IBM公司开发的RDBMS。它使用SQL执行所有的数据库操作。数据定义、数据存取、数据操作以及授权功能。SQL语句由用户在一个客户机节点从键盘输入或嵌套在应用程序中。

  DB2的结构包括表、视图、表空间、索引、索引空间、数据库和存储组。 这种RDBMS提供有允许用户动态建立和修改这些结构的工具。DB2还包括一些并行处理软件,以控制和限制干预、后备和恢复功能以及安全性保证等。

  并行处理通过锁来完成,当应用程序读数据库数据时,DB2在该数据上获取一个共享锁,允许其他应用程序读这个相同的数据。如果一个应用程序需要修改数据,那么DB2将一个互斥型锁放在该数据上,以阻止其它应用程序访问这个数据。DB2还提供一些关于锁的级别或锁的大小的任选项。

  DB2周期性地存储并检查所有数据库变化。所有驻留在系统缓冲区中的变化被写到数据库,并将一个变化的记录载入日志。以最近一次写到日志的变化起所建立的全部映像可用于完成系统故障的恢复。DB2包括一些用来从备份拷贝重新建立数据库的实用程序。这种实用程序含有一个选择项,允许用户只拷贝表空间中那些自最后一个备份后新被修改的页面。

  DB2还含有一些用来保护数据库的安全性规定。

 

18.1.3.2 Borland对象成分体系结构(BOCA)

 

  BOCA建立了一个既考虑开发工具又考虑数据库管理工具的客户/服务器体系结构。它将一级面向对象的工具、中间件和数据库服务器技术集中在一起提供客户/服务器的解决方案。该体系结构的组成部分有:

  1. 先进的面向对象工具

Borland建立有广泛基础和紧密集成的面向对象的工具,这些工具充分利用了当前客户/ 服务器变革的优点。使用面向对象的方法学,建立了如下产品:Borland C++、Borland Delphi、Paradox、QuattroPro、Visual dBase以及ObjectVision等。

  2. IDAPI

IDAPI(集成数据库应用程序设计接口)是Borland公司的SQL连通性解决方法。 IDAPI使得开发人员能够以更高的效率建立数据库应用,允许用户在多种硬件和操作系统平台以及网络环境下访问,以多种数据库格式存储的数据。

  3. InterBase

InterBase是一种分布式SQL数据库服务器。它支持每个数据库系统查询数据并将信息返回到其它任何一个InterBase服务器。InterBase 的可变体系结构代表了关系系统技术的第三次浪潮,可变引擎使得InterBase可以以最少的锁支持高效事务处理和决策支持事务处理。

  Borland公司的面向对象技术使得开发人员可以通过构造模块化的应用成分来建立复杂客户/服务器系统。这些模块化应用成分可以很容易地开发、测试、维护和增强,并可方便地装配到复杂的应用程序包中,此外Borland公司的可视化技术极大地提高了软件生产率。

 

18.1.3.3 Informix SQL服务器系列

 

  Informix公司推出了多种产品来满足特定的客户/服务器需求。 它们包括Informix-On-Line,Informix TP/XA,Informix Star Informix On-Line/Optical和Informix On-Line工作站版。

  Informix-On-Line是一个联机事务处理(OLTP)数据库服务器,具有可用性、数据完整性以及多媒体数据管理能力。它建立有效的数据存储方法进行快速数据存取;缓冲数据于内存最低限度地使用磁盘存取;利用多处理器特征,允许不同处理器同时存取;以及自动确定是有效的搜索策略等,从而获得极高的性能。

  Informix-TP/XA将On-Line连接到事务处理管理程序;支持那些涉及多个数据库以及多个DBMS(由不同的销售商提供)的事务处理。在众多RDBMS中,Informix 第一个向依从于X/Open XA的事务处理管理程序提供了这种基于标准的接口。

  Informix-STAR是一种用于On-Line的分布式客户/服务器数据库产品,它提供最佳的性能,并且具有最小的网络通信量、站点透明性以及在不同站点操作数据库的高度可靠性。

  Informix-On-Line/Optical是一种针对On-Line用户的附加产品。这些用户想在他们的数据库系统上使用具有大容量存储能力的光学设备。On_Line/Optical允许用户在“写一次读多次”(WORM)的光学子系统上存储BLOB。用户必须拥有On-Line/Optical On-Line和一个光学子系统。那当然,如果没有这个On-Line/Optical产品用户仍可以使用On- LIne在磁存储设备上操作BLOB。

  Informix-On-Line工作站版是On-Line管理员手册的图形化版本。该工作站版具有与硬件版本相同的技术内容,但它是构造在一个窗口化、点一揿式(Point-and-Click)图形接口,采用关键字交叉查阅。这使得用户可以在某一窗口中存取所需信息的同时,在另一窗口中配置监视或调节On-Line。

 

18.1.3.4 Microsoft SQL服务器系列

 

  作为Microsoft SQL服务器系列的一部分,有以下产品:Microsoft SQL Server for Window NT,Microsoft SQL Administrator for Windows,Microsoft SQL Bridge 和Microsoft SQL Server程序员工具包。

  Microsoft SQL Server for Windows NT旨在为有关键任务的应用系统管理大型数据库和满足网络化客户/服务器应用的需要。

  Microsoft SQL Bridge在Microsoft SQL Server环境和Sybase SQL Server环境之间提供一个协议网点。

  Microsoft SQL程序员工具包提供开发客户/服务器应用程序的灵活性,这些应用程序将关键的组合信息传送给基于Windows、MS-DOS和OS/I的生成系统。

 

18.1.3.5 Oracle RDBMS

 

  Oracle RDBMS为以任务为中心的企业范围的应用提供所需要的操作、监督和管理软件。使用Oracle RDBMS的分布式数据库和网点能力,用户可以透明地集成该企业的新旧数据、系统和应用程序。Oracle被分组形成几个软件包,使得客户可选地获取他们自己的应用所需要的功能。这些软件包是:

● 标准Oracle软件包

● 过程化选件软件包

● 分式式对象软件包

● 并行服务器选件软件包

● 开放网关软件包

 

  标准Oracle软件包提供解决大量关键任务的联机数据处理(OLDP)和决策支持应用所需要的功能及性能。这个标准软件除具有其它几个软件包的全部特征外,还有以下标准Oracle软件包所专有的特征:

  1. Oracle的过程化选件(procedual option)

提供多种能力可使数据库服务器成为应用环境的一个有效组成部分,过程化选件非常适用于具有高级需求和复杂商业实施规则的应用。它可选择地包括在Oracle服务器内使用程序设计语言PL/SQL过程的能力。具有存储过程和函数、过程软件包、数据库触发器、锁管理程序软件包以及数据库报警等特点。它还允许交互式提交或从3GL(第三代语言)程序提交“匿名”PL/SQL过程。

2. 分布式选件(distributed Option)允许把一个物理分布式数据库当作一个逻辑数据库来看待。那些需要在多个站点更新数据的应用可以从分布式选件获益。该选件的特征包括:

● 分布式更新

● 事务处理(TP)

● 监控器(XA)接口

● 透明的二阶段提交

● 远程过程调用(RPC)

● 表复制以及Oracle邮件接口

 

对于标准Oracle服务器,还包含查询能力和全局数据库名。

  3. 并行服务器选件(Parallel | Server Option)

提供对松耦合系统多个节点的支持,从而并行存取OLTP和决策支持的一个数据库。并行服务器在高性能、可扩充性、可用性以及数据库连接等领域具有极大的优势。并行服务器选件包括高速缓存(Cache)管理以及松耦合和大规模并行平台所需要的其它所有特征。

  4. Oracle开放网关软件包

提供对非Oracle数据管理程序、文件系统、应用程序和其它各种系统中的数据可编程且透明的存取。Oracle公司采用开放网关技术来向众多流行的数据系统和文件系统提供SQL连接网关,以进行透明的SQL存取。对于那些没有SQL连接产品的目标数据管理程序,Oracle开放网关开发人员工具包可以简化基于开放系统的应用手册的系统、数据及应用的集成工作。

  如果有些部门希望使用基于数据灵敏性或分类的存取控制,那么Trusted Oracle 会包含有Oracle T的所有特征,并具有多级安全性。

 

18.1.3.6 Sybase SQL服务器系列

 

  Sybase SQL客户/服务器体系结构由三个产品系列组成Sybase SQL服务器、Sybase生命周期开发工具和Sybase开放式互操作性产品。

  Sybase SQL服务器是一个针对联机应用的RDBMS。它提供亚秒级响应时间,每用户低成本操作和可用性,SQL服务器智能数据词典收集了众多数据定义、商业规则、报告以及配置信息。

  Sybase SQL生命周期工具提供一种快速原型设计、建立和维护联机应用的手段,使事务处理模型化,实施商业规则保护数据完整性,并将现有应用与新的数据源结合起来。

它们允许开发人员在SQL卡上用生命周期的各个阶段工作。利用Sybase工具,开发人员可以任意组合使用SQL、3GL、4GL多媒体和面向对象的工具来建立联机系统。

  Sybase的互操作性策略提供用于开发客户机和服务器应用的两种扩展工具包,并面向最通用的RDBMS提供拨动网关(turnkey gateways)。该策略使得复杂计算环境下的多机种硬件,操作系统、网络、数据库和应用程序协同作成为可能。

 

18.1.4 IDAPI结构原理

 

  IDAPI(集成数据库应用程序设计接口)是Borland公司解决客户/服务器连通性的方案。也是Delphi 客户/服务器开发的重要组成部分。为了说清楚什么是IDAPI,让我们先来讨论一下普通数据库的接口问题。

每个数据库管理系统和数据库应用都需一定的方式来访问内容所采用的数据格式,连接二者的部分称为接口,其最简单的情况就是对文件的直接访问,也可复杂到由几个层次组成。

  当今,面向用户的数据库通常都直接与其数据打交道,对那些有一定的用户和使用了一定时间的产品尤其如此。制造商总是认为自己的产品就是标准,而不关心对其它数据格式的访问。同样,许多纵向数据库应用用类似C的语言开发,多是直接访问其数据库。对开发者,这样做会变得容易些,但用户的情况往往是多变的。他对底层数据格式的选择并不一定与产品一致。其实在ODBC和IDAPI等尚未推出的前20年里,工业界已经认识到,一个DBMS不只是要访问其自身的数据格式。

  广义地讲,数据库接口可分为以下两类:

  ● 本地型(Local)

  ● 客户/服务器型(Client/Server)


想死你们了!

TOP

DELPHI基础教程

第十八章 Delphi客户服务器应用开发(二)
18.1.4.1 本地型数据库接口 

  本地型数据库是伴随微机的产生而产生的。dBASEII作为最早的并仍在使用的系统之一就是典型的本地型数据库。 

  本地型数据库管理系统的数据存放在一个本地硬盘上。DBMS接受来自用户或用户程序的命令。这些命令通常是系统特有的数据库管理语言。命令被转换为简单的磁盘访问命令,并交付文件系统来处理。然后DBMS接收来自磁盘上的数据,并加以处理。

  在本地型DBMS应用中,数据库引擎(DBE)运行于工作站上。图18.2暗示数据是存放在工作站的局部驱动器上,其实在网络中,数据还可存放在文件服务器上。这里数据库引擎使用典型的文件I/O调用和记录封锁技术来直接读写数据。 

  存放数据的文件位于网络中的某个服务器上时,DBMS的行力与单机情况无异。 网络操作系统负责对服务器的管理,因此对DBMS而言,对服务器的使用就象使用局部的驱动器一样。

  当用户发出命令,请求DBMS读取数据库中的数据时,该请求首先由工作站(客户)的网络驱动程序处理,它负责把请求从网络上传到所需的服务器网络文件系统。服务器操作系统从适当的磁盘卷上找到数据,并发回等待中的工作站驱动程序。最后,数据回传给DBMS,这样DBMS使用这些数据就象使用本地存储的数据一样。网络情况下的接口比单机情况下接口的处理增加了通信开销,正常情况下这种额外开销不会影响用户的响应时间,除非在网络通信的高峰期间或DBMS要求大数据量传递。

  对于本地型DBMS其大部分工作都在工作站一侧完成,即使数据存储在文件服务器上,其对数据的处理仍然要在工作站上进行。这种方式的最主要的一个缺点是无论查询需要多么少的数据,都需要首先将查询中的所有数据通过网络传到工作站,然后由工作站负责选出满足查询条件的数据,不难想象,当几个用户同时操作数据库时,数据库网络的带宽会很快阻塞。

  在这一方式下,工作站不仅要负责所有用户界面管理,还要负责所有数据处理的工作。在当今的数据库应用中,尤其是那些功能强且使用简便的系统,用户界面的处理开销是相当大的,象Windows这样的图形环境,处理上的额处开销会更大。为此而升级工作站是很得不偿失的。

 

18.1.4.2 客户/服务器型数据接口

 

  由于服务器硬件技术逐年迅速地提高,数据库在处理模式上在近五年内发生了改变。本地型DBMS逐渐让位给客户/服务器型DBMS,尤其是在大中型企业中更是这样。

  正如名字所暗示的,客户/服务器是将处理工作分散到工作站和服务器上去处理,服务器不仅负责存取数据,还要对数据作一定的处理工作,这样在数据发送给工作站之前即求得查询结果集,从而在大部分情况下可大大减少网络传输的开销,因此,也减轻了工作处理负担,从而只需关心用户界面的处理工作即可。

  服务器处理数据带来的另一个好处是,当服务器中数据库引擎使用了缓冲机制时,多个工作站可以从中受益。例如,一用户查询了某数据,当另一用户要查询同样的数据时,即可从服务器缓冲中直接得到结果,从而免去很多开销。

  客户/服务器系统的成功与否在很大程度上依赖于服务器硬件质量和容量。用户越多,服务器的处理负担越重,相应服务器硬件性能也要跟得上,否则就会导致响应时间比本地型数据库还要差的结果。  

  处理工作,而工作站负责用户界面处理工作 

  客户和服务器间的数据库接口要比本地型系统复杂得多。它有几个转换级负责命令和结果集在工作站和服务器间的传送。图18.5给出了客户/服务器数据库接口的详细情况。 

  客户/服务器的前端应用程序实际上不直接与数据库引擎打交道。每个客户服务器提供一个数据库通信接口,该数据库通信接口运行于前端。这些接口也称为数据库通信API等。数据库通信接口的工作流程如下:

  ① 前端应用程序发送命令给数据库通信接口。

② 接口通过网络把命令传给数据库引擎。

 ③ 数据库引擎在服务时上做查询或更新操作之类的工作,通过网络文件系统访问物理数据。

  ④ 数据库引擎将结果返送给工作站上的通信接口。

  ⑤ 前端从接口上接到结果后,显示或按用户要求做其它处理。 

  客户/服务器型比本地型DBMS更接近ODBC的原理。因为由前端向数据库的命令发送和由数据库向前端结果的返回都是透明的,并不需知道具体传送方式如何,各系统存在差别地方是:客户/服务器系统在管理工作站和服务器间通信的方式不同,彼此会不兼容。此外,对于本地型DBMS缺乏读取不同类型数据源的能力的问题。这些问题在IDAPI中得到了有效解决。 

18.1.4.3 Borland Delphi 的解决方案 

  IDAPI是通过BDE(Borland Database Engine)和SQL Links,来解决本地型数据库接口和客户/服务器型数据库接口的兼容问题的,见图13.1。

  Delphi 的数据库特性使你能很容易构造数据库应用程序。这些应用程序能访问Visual dBASE、Paradox、Local InterBase Server for Windows等本地数据库和Oracle、Sybase、Informix、SQL Server和Remote InterBase Server等客户/服务器数据库。

BDE是Borland公司支持Delphi 2.0 Client/Server Suite、Paradox for Windows、Visual dBASE for Windows等产品的核心数据库引擎和互连软件。BDE 提供了丰富和强壮的特性支持客户/服务器应用的开发。

  提供支持多种数据库如dBASE、Paradox、Text、InterBase、Oracle、Sybase和Microsoft SQL Server以及任何ODBC数据源的统一和一致的应用程序编程接口(API)。开发者能不用修改数据库应用就能访问不同的数据库站点和数据库格式:

  ● BDE是用于开发客户/服务器数据库应用的理想工具,数据库应用程序既可访问本地数据库又可访问远程数据库

● 允许数据库用直接和灵活地访问数据源

● BDE对于Paradox和dBASE文件格式来是高性能的数据库引擎

  ● 支持使用ISAM(Indextd Sequential Access Method)SQL和QBE访问数据

  ● BDE是数据集成化引擎,提高跨不同数据库的共享服务。支持不同数据库格式的相互转化,如dBASE和Oracle表、从InterBase到Paradox拷贝数据甚至建立InterBase 和Oracle表之间的一对多关系

  ● BDE查询引擎为SQL,QBE和面向集合访问提供一致性的查询语言。支持用户定义和访问基本SQL的服务器和基于文件数据库的能力

  ● BDE支持全32位功能,如多线程,抢占式多进程,长文件名和UNC,用户可在后台执行多个查询,多个数据库应用可访问同一个数据库文件

 

  BDE的体系结构是基于数据库驱动程序的,它提供了各种共享服务:

  ● 缓冲区管理(Buffer Manager)

  ● 排序引擎

  ● OS服务

  ● 内存管理

  ● BLOB快速存取

  ● SQL查询引擎

  ● SQL产生器

  ● 数据库重构

  ● 表的批处理

  ● 数据转换服务

  ● 连接服务

  ● 内存数据库服务

  ● SQL驱动程序服务

  ● 系统管理

  ● 语言管理

 

  Paradox、dBASE和文本数据库BDE包含的数据库驱动程序支持对标准数据源的一致性访问。用户可以增加ODBC驱动和Borland SQL Links产品以支持对SQL 服务器的数据访问,如InterBase Oracle和Sybase等。此外BDE给予Windows 95和Windows NT应用开发者以直接、独立共享的对多种数据源的高级访问。

  BDE在设计上是面向对象的。在运行时,数据库应用通过建立各种类型的BDE 对象与BDE交互,这些运行的对象用于操作数据库实体如数据库表、查询。BDE的扩展的API支持C、C++、Delphi等对数据库引擎的访问。

  在Delphi应用程序中访问数据库是通过调BDE的API函数。Delphi在库单元BDE中提供了大约三十多个API函数和各种BDE消息和结构。由于Delphi应用程序的开发是基于部件的,有关BDE API的调用都嵌入了Delphi可视部件类库,因此,建立数据库应用时可以不必管BDE API的细节。只要正确安装IDAPI的Drivers,并进行正确的配置,就能使你的数据库应用程序与服务器连接并访问数据库。当然为了提高应用程序的数据库访问性能, 可以在程序中直接调用BDE API函数。

  Borland的IDAPI包含对ODBC的支持,因此通过BDE,你的应用程序能访问一切与ODBC兼容的数据库如Access和Btrieve。

  Local InterBase Server提供了一个单用户多实例的SQL服务器平台,特别是在将数据库应用程序转移到对Oracle、Sybase和Informix等远程数据库的访问之前,可以在Local InterBase Server平台建立和测试数据库应用程序。

  对客户/服务器型的数据库应用程序来说,SQL Links在Delphi数据库体系结构中起着至关重要的作用。

  Borland SQL Links支持访问局部(Paradox dBASE)和远程SQL数据库的BDE应用程序。为了访问特定的远程SQL服务器必须在客户端安装相应的SQL Links驱动程序。安装了SQL Links驱动程序后,SQL表达式才能被传送给相应的服务器执行。

  一旦你安装了SQL Links的驱动程序并建立SQL驱动程序的Alias,你就能采用下列方法使数据库应用程序象访问本地Paradox和dBASE数据库一样访问远程数据库:

  ● 通过应用程序用户接口(界面)

● 通过包含嵌入SQL表达式的应用程序

● 直接将SQL表达式传送给服务器

 

  BDE也支持应用程序使用SQL访问局部数源,

  安装了SQL Links驱动程序,你能用SQL访问数据。驱动程序负责SQL服务器的链接,将查询语句转换成兼容的SQL语句并将它们传送给SQL数据库。等处理完成后,SQL数据库把结果以应 

   ⑴ BDE客户查询SQL数据库;

   ⑵ SQL Links驱动程序建立客户工作站与SQL Server的链接,并将查询发送给SQL

服务器;

   ⑶ SQL服务器进行SQL表达式的错误和语法检查,处理查询并将结果返回给BDE

客户;

   ⑷ SQL Links驱动程序将结果通过SQL转换成客户端能识别的形式,客户端将其进

行格式化并将数据显示给用户。

 

  在应用程序中使用SQL Links驱动程序对使用SQL的数据库具有如下好处:

  ● 直接查询SQL服务器的能力

  ● 支持SQL网络的传输

  ● 增强了记录的快速存取

  ● 数据锁定

  ● 在SQL结果集和数据库表的双向变换

  ● 使用索引给数据排序

  ● 为数据库当前Session建立书签,并可在以后重用书签

  ● 通过动态访问数据源来动态处理SQL数据

 

18.2 Delphi客户/服务器应用开发环境的构造

 

18.2.1 Borland SQL Links 的安装

 

SQL Links的安装过程如下:

 ⑴ 在客户端工作站上将SQL Links1号磁盘插入软盘驱动器.A:或B。

⑵ 在Windows 95的资源管理器中,选择运行A:\INSTALL( 或B:\INSTALL),并显示打开对话框。

  ⑶ 选择Continue 或按Enter。INSTALL 检测工作站上是否已安装了BDE 的动态链接库IDAPI01.DLL 。如果需要它将显示一个对话框,以让你描述该文件所在位置。当定位IDAPI01.DLL后,INSTALL显示Borland SQL Links目录对话框,描述你想安装的SQL Links驱动程序。

  当你完成这些工作,选择Continue或按Enter.INSTALL显示IDAPI配置定位框。

  ⑷ 描述你想安装的IDAPI配置文件的升级版的位置。当你完成这些工作后,选择Continue或按Enter。在继续安装之前按照你所选择的驱动程序。安装程序将需要更多的信息,后面的章节中将叙述这些信息。

  ⑸ 一旦你提供了所有的必要信息,安装开始当安装结束后,你能选择察看SQL Links自述文件——READLINK.TXT。阅读这个文件将找到最新的信息。

 

18.2.2 配置SQL环境

 

  与你的BDE应用程序一起安装的有BDE配置工具(BDECFG32.EXE),该工具帮助用户修改他们的应用程序配置。配置参数被存于命名为IDAPI32.CFG的二进制文件中。当应用程序启动时,将读这个文件。通常该文件在应用安装过程中被置于BDE文件目录中(C:\Program Files\Common\BDE)。

  本节描述怎样使用BDE配置工具设置应用程序的SQL环境。一旦配置好BDE应用环境,就可开始联接网络,并访问SQL服务器。

  关于怎样使用BDE配置工具的详细介绍,可参见BDE用户指南或按运行BDE配置工具选择Help按钮显示在线帮助。

  在开始配置SQL环境前,必须已完成以下工作:

  ● 已安装SQL Links软件

  ● 退出所有其它Borland应用程序

  ● 在Windows 95中打开应用程序组Borland Delphi 2.0

  ● 选择IDAPI配置工具图标,出现配置工作窗口后进入驱动程序管理页

 

18.2.2.1 配置SQL Link驱动程序缺省设置的方法

 

  SQL Link驱动程序缺省设置是指在BDE配置工具的Drivers页中的参数设置,这些参数在建立新的Alias中使用。Alias是描述网络资源的一组参数的集合。BDE应用使用Alias 联接共享数据库。Alias对于访问局部数据库并不是必要的,但要访问SQL数据库,却是必不可少。

  SQL Links驱动程序的缺省设置,是你建立新的Alias 的原型。尽管你在建立Alias后能定制它,但在建立新的Alias前设置相应的缺省设置要来的容易。因为这样建立的每一个Alias将继承这些设置。

  要描述驱动程序的缺省设置,要完成以下几项:

 ● 将亮条移到驱动程序的入口,驱动程序管理程序显示所有的该驱动程序的配置参数,在参数列表表端可用滚行杠检察各配置参数

  ● 如果需要编辑驱动程序的缺省配置参数,如果光标停在这格, 配置工作将自动套用缺省参数

  ● 当完成这些工作,选择File|Save,修改将在应用程序下一次启动时生效

 

18.2.2.2 SQL Links驱动程序的缺省设置项目的含义

 

  1. VERSION

SQL Links驱动程序的版本号。

  2. TYPE

描述当前驱动程序类型。SERVER就表示该驱动程序用于连接一个SQL服务器,FILE就表示驱动程序用于连接一个标准的基于文件服务器。

  3. DLL选项

所选SQL Links的16位驱动程序的动态链接库名。

4. DLL32

所选SQL Links的32位驱动程序的动态链接库名。

5. DRIVER FIAGS

内部的产品描述标志。

6. TRACE MODE

描述记录跟踪信息的类型

  7. SERVER NAME

指定目标SQL服务器名。如果指定为InterBase服务器,将包含数据库文件的全部路径,Servername:/Usr/gds/directoryname/databasenam.gdb

  8. USER NAME

访问SQL服务器的缺省用户名。

  9. OPENMODE

OPENMODE是SQL Links打开SQL数据库时的读写模式。取值可以是READ/WRITE或READ ONLY,缺省值是READ/WRITE。把OPEN MODE设为READ ONLY,将影响用户端的操作,但对SQL服务器没有影响。

  10. SCHEMA CACHE SIZE

描述被贮存视图信息的SQL表个数。取值范围是0-32, 缺省值为8。

  11. LANGDRIVER

用来操作来自SQL 服务器的数据的语言驱动程序。当光标停止LANGDRIVER域时,一个滚行框出现在正文域的左侧,用滚行杠可以用于你的驱动程序的可选的语言列表。如使用美国英语,该缺省值是空格。

  当描述的语言驱动程序与一个服务器别名相适应,那么你的应用程序将使用该驱动程序处理从服务器发来的数据。这包括你察看的所有表和所有查询返回的结果表。运行在不同系统上的服务器利用字符集的转换来决定怎样对数据编码。如果你操作在非英语环境,你的BDE应用程序可以使用不同于SQL服务器的字符集。如果你的平台上的字符集同SQL服务器上的不匹配,那么在两种不同平台间传递数据将引起下列问题:

 ● 数据在你的平台上不正确的显示

  ● SQL数据库上将记录错误的字符

 

  为防止这种情况的出现,SQL Links提供语言驱动程序,实现你的应用程序的字符集与SQL数据库的字符集的数据转换。这将使从SQL服务器传来的数据在你的平台上正确显示,或将你输入的数据可靠地传送到服务器上。

  语言驱动程序包含有关排序和大小写转换的信息。无论何时,对SQL数据库的查询按本地数据库的规则处理应用程序的语言驱动程序用于评测排序的字符范围。 如果平台上的排序和大小写转换与SQL服务器上的不同,你的应用程序就会显示不一致的结果。

  如果SQL数据库使用扩展字符集,请确信用于访问SQL 服务的别名中描述正确的,SQL Links语言版本选择的驱动程序的字符集应当与SQL服务器的相同。如果你没有找到合适的SQL Links语言驱动,你可修改别名中的SQLQRYMODE入口,防止按局部数据库规则处理查询。

12. SQLPASSTHRU MODE

描述应用程序访问SQL服务器时是否借助平台命令和传递式SQL。取值范围和它们的含义列于下表:

 

   表18.3 SQLPASSTHRU MODE设置

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

设置            含 义

  ──────────────────────────────────────

  NOT SHARED    传递SQL和非传递性SQL不共享相同的连接

  SHARED AUTOCOMMIT  缺省值。传递SQL和非传递SQL将共享相同连接,传

递式SQL将以与非传递SQL相似的方式动作。 也就说

用户的传递式SQL表达式将被自动提交。

  SHARED NOAUTOCOMMIT 传递式SQL和非传递式SQL将共享同一个连接,但SQL

驱动程序并不自动提交SQL表达式。在这种模式下,传

递行为是服务器独立的。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

 SHARED AUTOCOMMIT和SHARED NOAUTOCOMMIT 模式并不支持所有的传递式表达式。当SHARED AUTOCOMMIT或SHARED NOAUTOCOMMIT模式被设置,在传递式SQL中不需执行事务控制语言。使用你的BDE应用编程语言来开始,提交回送事务。当传递式SQL和非传递式SQL共享一个连接,记录快存并不立即反映传递SQL操作的更新。

  13. SQLQRYMODE

描述处理查询SQL数据的方法。取值范围含义列于下表,缺省值NULL:

 

表18.4 SQLQRYMODE设置

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  设置      模 式      含 义

  ──────────────────────────────────────

  NULL    Server-Local  在Server-local中,查询模式查询首先传递给SQL服

务器,如果服务器不能执行查询,查询就在本地执

行。

  SERVER Server-Only 在Server-Only查询模式,查询被发送给SQL 服务

                 器。如果服务器不能执行查询,不执行本地查询。

  LOCAL Local-Only 在local-only模式,查询总是本地执行。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

正常情况下,查询SQL数据库将在数据库服务器端被处理。然而,在某些情况下,完全在SQL服务器端执行的查询的结果不同于在本地执行的查询的结果。例如,你的BDE 应用程序查询函数执行在字符域的大小写敏感搜索。如果,服务器不支持大小写敏感搜索,那么Select语句的条件如“>A”在不同地方查询将产生不同的结果。 如果你想确信所有来自BDE应用的查询都按照SQL服务器上的规则来执行,那么你就应合理配置SQL Links驱动程序,阻止查询的本地处理。使用BDE配置工具修改SQL数据库别名,将SQL QRYMODE设置成SERVER就可做到这一点。新的SQLQRYMODE值将在应用程序下一次启动时生效。

  14. SCHEMA CACHE TIME 

描述表信息将被贮存多长时间,取值范围和它们的含义列于下表,缺省值为-1。

 

表18.2 SCHEMA CACHE TIME设置表

  ━━━━━━━━━━━━━━━━━━━━━━━━━

设置 含 义

─────────────────────────

-1 表被贮存至你关闭数据库

   0        不贮存表

  1…2147483647  贮存表的时间数(秒为单位)

  ━━━━━━━━━━━━━━━━━━━━━━━━━

 

  设置该值能提高数据库表的访问性能。

15. MAX ROWS

描述SQL Links驱动程序能给服务器传递的SQL语句的最大行数。如果值为-1,则没有限制。

16. BATCH COUNT

描述在自动提交前包含在BATCH中的修改记录个数。

 

18.2.2.3 建立和管理SQL别名

 

  设置标准别名的过程包括给Alias参数赋值,描述包含Paradox或dBASE文件的路径名和目录名。设置用于SQL数据库的别名包括给alias赋名,定制用于SQL服务器和数据库的访问参数。SQL别名包括目标SQL服务器的用户名和口令。这些参数对于访问任何SQL数据都是必须的。常用的SQL别名是当安装时,第一次修改缺省的SQL Link驱动程序的参数时自动建立。

  关于如何使用配置工具增加、修改和删除别名,请参阅附录。

 

18.2.2.4 联接SQL服务器

 

  当应用程序第一次访问SQL数据库,会触发一个自动联接过程。联接过程需要确认访问数据库的权限。

  如果配置文件和SQL Link驱动正确配置,应能用应用程序的File菜单通过相应的数据库别名在你的SQL数据库中选取一张表。下面介绍如何解决联接InterBase服务器过程中出现现的一般问题,并讨论有关使用Borland SQL Links的各种主题。

  1. InterBase服务器需求

  下表所列软件应当已经安装在运行InterBase的服务器

 

表18.5 InterBase服务器软件要求

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  项 目       描 述

  ────────────────────────────────────

  数据库服务器软件   InterBaseV4.0或更高版本

  网络协议软件     与数据库服务器和客户工作端网络协议兼容的网络协议

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

2. 客户工作站需求

  下表所列软件应当已经安装和运行于客户工作站,表中还列出相关文件和参数。

 

表18.6 客户工作站软件要求

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  项目          描述

  ──────────────────────────────────────

  BDE应用程序    被支持BDE应用程序,并已按产品文档要求安装

  硬件和操作系统   1.5MB剩余磁盘空间,适合Borland Delphi需要的硬件和操作

            系统

  访问网络服务器的  如果你的Borland Delphi被安装在网络文件服务器上,请确

  权力        认对BDE文件安装目录有读写的权力

  网络协议软件    网络协议软件应当与服务器网络协议和客户工作端的客户数

            库通信驱动程序兼容。InterBase支持Windows 95 WinSock API

  HOSTS文件    HOSTS文件包含你要连接的服务器的名称和地址:

例如:128.127.50.12 MIS_Server

SERVIDES文件   一个SERVICES文件包含访问InterBase服务器的协议。 在SQL

Links安装过程中,“gds_db 3050/tcp”被加入这个文件

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

erver)


想死你们了!

TOP

DELPHI基础教程

第十八章 Delphi客户服务器应用开发(三)
3. 安装的软件项目

  当你安装InterBase SQL Link驱动程序,下列项目将被安装于你的工作站上。 

表18.7 安装的文件名

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

   项 目        描 述

──────────────────────────────────────

  SQLD-IB.DLL 包含InterBase驱动程序和支持文件的动态链接库

  SQLINT32.DLL

  INTRBASE驱动类型  加在配置工具的驱动管理程序中以配置基本的Borland

  InterBase SQL Link驱动程序

  INTRBASE别名类型  加入配置文件的别名以使建立联接SQL服务器数据库的别名

  SQLD_IB.HLP 配置InterBase驱动程序的帮助文件

  READLINK.TXT Borland SQL Links for Windows自述文件

  INTERBAS.MSG InterBase消息文件,通常安装上C:\INTERBAS

  CONNECT.EXE 测试工作站和InterBase服务器连接情况的工具

  REMOTE.DLL、 InterBase的支持动态链接库

  GDS.DLL、GDS32.DLL

  将InterBase服务器描述 安装过程修改工作站的SERVICES文件以增加用于

  加入SERVICES文件  InterBase 服务器访问所需的协议描述 ,

如:gds- db 3050/tcp

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

  软件项目中还应包括TCP/IP接口软件

  下表列出的文件给InterBase客户端应用提供访问Winsock 1.1的接口

 

表18.8 TCP/IP接口软件

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

   文 件 名       描 述

────────────────────────────────

  MVWASYNC.EXE 异步通信模块

   VSL.INT TCP/1P传输初始化文件

  WINSOCK.DLL Windows Socket动态链接库

   MSOCKLIB.DLL 将Windows Socket调用映射到VSL驱动程序

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

  如果TCP/IP产品不是Winsock1.1兼容,InterBase客户端应用将也可采用其它TCP/1P 驱动程序。InterBase服务器还可支持其它通信协议,如SPX/1PX,NetBeIU等。

4. 解决一般的联接问题

如果用SQL Links建立与InterBase服务器的连接有问题可采用下列步骤来分离问题原因:

⑴ 通过Windows ISQL工具测试能否与InterBase服务器联接

如果成功,状态信息会出现,并继续步骤⑵。

如果不成功,询问数据库管理员。

⑵ 检验InterBase SQL Links 驱动程序是否正确安装。

⑶ 重新安装SQL Links。

⑷ 检查SERVICES,文件中应有行:

 

gds_db 3050/tcp

 

如果不能正确安装,就请询问数据库管理员,否则继续步骤⑸。

⑸ 测试底层协议

① 输入TELNET命令,确认TCP库是否正确安装。

如果TCP库正确安装,注册提示符会出现。注册入网检查数据库是否存在。

如果消息是“can't resolve hostname” 出现,检查工作站的HOSTS文件是否有你 的主机名和IP地址的人口。如:

 

128.127.50.12 mis_server

 

如果用TELNET是成功的,但仍然无法正确联接,则没有正确安装InterBase。请寻

求数据库管理员的帮助。

② PING到服务器服务器上,测试InterBase服务器是否正常运行并且为桌面应用可见

(如果PING是成功的,消息“servername is alive”被显示)。

PING成功但TELNET不成功,则inet daemon可能有问题。

如果PING到服务器上不成功,则有网络路径问题,将问题报给网络管理员。

 

如果底层协议不正常,请询问数据库管理员,否则继续帮助⑹。

⑹ 确认是否有InterBase服务器的访问权,如果有请继续步骤⑺。

⑺ 检查BDE应用程序的InterBase别名是否正确安装。

如果能够直接从工作站上联接,但不能从BDE应用程序中,那么很有可能你的

IDAPI32.CFG别名设置有问题。运行BDE配置工具检查InterBase别名。

 

 

18.3 Delphi Client/Server编程

 

  本节介绍如何运用Delphi可视化开发工具和ObjectPascal语言开发Client\Server的数据库应用程序,采用的例子是CSDEMOS。这是Delphi2.0自带的演示Client\Server开发的例子,它安装在C:\Program Files\Borland\Delphi 2.0\Demos\DB\CSDemos中(缺省安装)。

  本节将包含以下内容:

● 使用TDatabase部件连接SQL服务器

  ● 用DataSet部件(又称数据集部件),如TTable和TQuery,联接TDatabase部件并访问数据库以及各种表之间如何切换

  ● 使用数据库连接

  ● 触发器的使用方法

  ● TStoredProc部件的使用方法

  ● 客户和服务器之间的事务控制

  ● TStoredProc部件的使用方法

 

18.3.1 使用TDatabase部件联接SQL服务器

 

18.3.1.1 TDatabase部件概述

 

  TDatabase部件处理应用程序与单个数据库的联接。如果不需要控制数据库联接,可以不用创建TDatabase部件。当应用程序试图打开数据库表(Table)时,会自动创建一个临时的TDatabase部件。但如果你想控制数据库的持续联接、进入数据库服务器的注册和数据库别名的值或事务控制,那么你就必须为每个所需的联接创建一个TDatabase部件。

  1. 创建TDatabase部件

  TDatabase 部件在Component Palette中的Data Access页上,你能将其拖放在数据模块(Data module)或窗体中。在设计时创建TDatabase 部件,用户可以设置初始值和编写OnLogin事件处理过程(Event Handle)。OnLogin事件给用户提供了第一次注册数据库服务器时定制服务器安全参数,如口令,的能力。

  2. TDatabase的关键属性

⑴ DatabaseName属性

DatabaseName是所要联接的数据库名,并且用于DataSet软件,它将出现在DataSet部件的DatabaseName属性的下拉式列表框中。设置DataBaseName属性是定义数据库应用的特定别名。DataSet部件能引用该名字以取代直接使用BDE别名。当TDatabase部件的Connected属性为True时不能修改该属性。

  ⑵ AliaName属性

AliasName是BDE配置工具定义的BDE别名的名字。TDatabase 从中获取其缺省的设置。如果设置DriveName属性,则该属性将被清除,如果当Connected为True 时强行设置DriveName属性将引发异常。

  ⑶ DriveName属性

DriveName是BDE驱动程序,如STANDARD 、ORACLE、SYBASE、INFORMIX或INTERBASE的名字。如果设置AliasName,则该属性值将被清除。

  ⑷ Params属性

Params属性包含了打开SQL服务器上数据库时所需的参数。在缺省情况下,这些参数由BDE配置工具设置;用户也可以用数据库参数编辑器(Database Parameters Editor)修改这些参数。对于数据库服务器而言,Params将描述一系列的参数,如服务器名、 数据库名、用户名和口令。

  ⑸ Connected属性

  Connected属性指明是否建立数据库的联接,当应用程序打开数据库中的一个表时Connected将被置为True;反之,关闭数据库表,Connected将被置为False,除非KeepConnection为True。而将Connected置为True则可不需打开数据库表即可建立数据库联接。TDatabase的KeepConnection属性描述当数据库中没有表打开时是否维持数据库联连。

  ⑹ KeepConnection属性

  KeepConnection属性描述当数据库中没有打开表时是否要保持与服务器的联连, 如果数据库应用需要打开和关闭单个数据库中的多个表时,将KeepConnection 设置为True是很有用的,那样,即使没有打开任何表,应用仍能保持与数据库的联接,它能够重复地打开和关闭数据库表,而不需要重复执行联接过程。如果KeepConnection置为False,当每次将Connected置为True,数据库都必须执行注册过程。

  ⑺ LoginPrompt属性

  LoginPrompt属性用于控制如何处理SQL数据库的安全性问题。如置为True,当应用程序试图建立数据库联接时屏幕上将出现标准Delphi注册对话框。用户必须输入正确的用户名和口令。如果置为False,则应用程序将寻找TDatabase部件的Params 属性中的注册参数。下面是USERNAME和PASSWORD参数的例子:

 

  USERNAME = SYSDBA

PASSWORD = masterkey

 

  ⑻ TransIsolation属性

  TransIsolation属性描述SQL服务器所有的事务控制独立级别。 tiDirtyRead使所有修改都被返回,而不管记录是否已被提交。tiReadCommitted将只返回提交的记录,而提交的修改将不会在结果中反映出来。tiRepeatableRead 将只返回事务过程中最初的记录,即使另一个应用程序将所作的修改提交。

  各种数据库服务器可能不同程度地支持这些独立级别,或者根本不支持。 如果需要的独立级别不被服务器支持,那么Dephi将使用下一个更高的独立级别,如下表所示:

 

表18.10 各类服务器TransIsolation设置

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  独立级别     Oracle Sybase和     InterBase

Microsoft SQL

──────────────────────────────────────

Dirty Read Read Committed Read Committed Read Committed

Read Committed Read Committed Read Committed Read Committed

Repeatable read Repeatable read Not Supported Repeatable Read

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

各个独立级别的含义请见表18.12。

 

3. TDatabase的关键方法

  ⑴ StartTransaction方法

  StartTransartion方法在由TaransIsolation属性指定的独立级别下开始事务控制。如果在一个事务已被激活的情况下调用该方法,Delphi将引发异常。

调用了该方法后,对数据库所做的修改一直由数据库服务器维持到调用Commmit方法提交数据或调用Rollback方法取消修改为止。只有当联接数据库服务器时,才能调用该方法。

  ⑵ Rollback方法

  Rollback方法返转当前事务控制,并且取消自最近一次调用StartTransaction以来对数据库所做的所有修改。

  ⑶ Commit方法

  Commit方法提交当前事务控制,并且将自最近一次调用StartTransaction以来所有数据修改存入数据库。

4. TDatabase的OnLogin事件的处理

  OnLogin事件的触发条件是当联接SQL数据库的TDatabase部件被打开并且LoginPrompt属性为True。使用OnLogin事件处理过程可以在运行时设置注册参数。OnLogin 事件处理过程得到TDatabase的注册参数数组Params,并且使用Values属性改变这些参数。

  例如:

 

 LoginParams.Vaiues['SERVER NAME'] := 'MYSERVERNAME';

LoginParams.Values['USER NAME'] := 'MYUSERNAME';

LoginParams.Values[PASSWORD'] := 'MYAPSSWORD';

 

  当控制从OnLogin事件处理过程中返回时,应用程序用这些参数来建立联接。

  OnLogin事件处理过程的声明是这样的:

 

  TLoginEvent = procedure(Database: TDatabase; LoginParam: TStrings) of Object;

property OnLogin: TLoginEvent;

 

TLoginEvent类型是处理OnLogin事件的方法头。Database参数是要联接的数据库。LoginParams是TStrings类型的对象,包含用户名和口令,以及打开数据库时所用的其它参数。用户名是形如USER NAME = John.Doe的字符串,口令是形如PASSWORD = is_Password的字符串。当OnLogin事件处理过程被调用时应当在LoginParams中加入用户名和口令。

 

18.3.1.2 定制数据库服务器的注册参数

 

  大多数数据库服务器都包含限制数据库访问的安全特征。通常,在用户能访问数据库之前,服务器都要求注册的用户名和口令。

  如果服务器需要注册,在设计阶段,Delphi 会在你试图联接时提示你,诸如在会TTable部件描述数据库表名时。

  在缺省情况下,Delphi应用在打开数据库服务器的联接时,显示标准注册对话框。如果联接已建立,则注册对话框不会出现。

  可以用下列方法处理服务器注册:

1. 将TDatabase部件的LoginPrompt属性置为True。这样,当应用程序试图建立数据库联接时,标准注册对话框会打开。

   2. 将LoginPrompt属性置为False,在TDatabase部件的Params属性中包含用户名和口令参数。例如:

 

   USERNAME = SYSDBA

PASSWORD = mosterkey

 

但不推荐使用该方法,因为这会危害数据库安全

  3. 使用TDatabase部件的OnLogin事件设置注册参数。OnLogin事件得到TDatabase 注册参数数组的拷贝,并利用Values属性改变这些参数。如:

 

   LoginParams.Values['SERVER NAME'] := 'MYSERVERNAME';

   LoginParams.Values['USER NAME'] := 'MYUERNAME';

   LoginParams.Values['PASSWORD'] := 'MYPASSWORD';

 

  当控制从数据库注册事件处理过程中返回时,这些参数被用来建立联接。

 

18.3.1.3 建立应用程序特定的别名

 

  TDatabase的Aliases描述了数据库表的位置和数据库服务器的联接参数。通常都是在Delphi之外,运用BDE配置工具(BDECFG32.EXE)创建别名,并且别名被存在BDE 配置文件IDAPI32.CFG中。

  用户也可以用TDatabase创建只在应用程序中可用的别名,用TDatabase 创建的别名不会加进BDE配置文件中。任何DataSet部件可通过描述DatabaseName 属性来使用这些别名。为了定制这些局部别名的参数,用鼠标左键双击TDatabase部件或从TDatabase部件中选择Database Editor,Delphi就会打开数据库属性编辑器(Database Properties Editor)。

 

18.3.1.4 控制数据库的联接

 

  TDatabase部件的Connected属性,指示TDatabase部件是否建立与数据库服务器的联接。当应用程序打开数据库中的表时,Connected被设置为True。将Connected 设为True就建立了数据库的联接。

  1. 保持数据库联接

  TDatabase的KeepConnection属性描述当没有数据库表打开时是否要与保持数据库的联接。

  如果应用程序需要在单个数据库中多次打开关闭多个表时,将KeepConnection 置为True能使应用程序具备更好的性能。

  当KeepConnection为True时,即使没有表打开,应用程序也能保持数据库的联接。那么就能重复打开和关闭数据库表而不需每次进行联接注册。

  2. 使用TSesstion控制联接

 TSesstion部件有一个面向整个应用程序的KeepConnections属性。如果Session.KeepConnections为True,那么用于所有TDatabase部件的数据库联接都是持久的。

  TSession为应用程序提供数据库联接的全局控制。TSession中的Databases 属性是Session中所有活跃数据库组成的数组,DatabasesCount属性描述活跃数据库的数目。

  3. 描述Net和Private目录

  TSession的NetFileDir属性描述BDE网络控制目录的路径。TSession的PrivateDir属性描述存储诸述处理局部SQL表达式的临时文件的目录的路径。

 

18.3.1.5 获取数据库信息

 

  TSession拥有许多让用户获取数据库有关的信息,每个方法都以TStringList 部件作为传入参数,并将信息返回TStringList中。

  1. GetAliasNames方法

  声明:procedure GetAliasNames(List: TStringList);

  GetAliasNames方法消除List中的参数,并将所有已定义的BDE别名的名字写入List。应用程序生成的别名不包括在内。

  2. GetAliasParams方法

  声明:procedure GetAliasParams(const AliasName: String; List: TStringList);

GetAliasParams方法清除List的内容,并将BDE别名为AliaName的参数写入List。

  3. GetDatabaseNames方法

  声明:procedure GetDatabaseNames(List: TStrings);

GetDatabaseNames方法清除List的内容并将所有BDE别名和应用程序定义的别名的名字写入List。

4. GetDriverNames方法

  声明:procedure GetDriverNames(List: TStrings);

  GetDriverNames方法清除List中的内容,并将BDE当前安装的驱动程序名写入List。

  5. GetDriverParams方法

  声明:procedure GetDriverParams(const DriverName: String; List: TStrings);

  GetDriverParams方法消除List中的内容,并将名为DriveName驱动程序缺省参数写入List。

  6. GetTableNames方法

  声明:procedure GetTableNames(const DatabaseName, Pattern: Strings;

Extensions, SystemTable: Boolean; List: TStrings);

  GetTableNames方法消除List中的内容,并将名为DatabaseName的数据库中的所有表的名字写入List。Pattern参数将限制表名。对于SQL服务器,将SystemeTables设为True将获取系统表和用户表。对非SQL数据库,将Extensions设为True将在表名中包含扩展名。

 

18.3.2 处理Client/Server事务控制

 

  使用隐式控制和显示控制的数据库应用中有两种方法管理事务控制:

 ● 运用TDatabase部件的属性和方法进行显式控制

  ● 运用TQuery部件的传递式SQL控制事务

 

  Delphi还支持Paradox和dBASE表的局部事务处理

 

18.3.2.1 事务控制概述

 

  当用Delphi创建数据库应用时,Delphi提供了用为所有数据库访问的事务控制。

  事务是这样一组操作,在被提交前,它们对一个或多个数据库的操作,必须全部执行成功。如果其中一个操作失败,则所有操作失败,即事务具有原子性。

  即使发生硬件失败,事务也要保证数据库一致性。当允许多用户并发访问时,事务还要维持数据完整性。

  例如:一个应用程序可能更新ORDERS表以指明接受购买某一项目的定单,那么也要更新INNENTORY表以反映库存的减少。如果在第一个更新之后,第二个更新之前发生硬件错误,数据库就会处于不一致状态,因为库存情况没有反映定单情况。在事务控制下,两个表达式将在同一时间提交,如果其中一个表达式失败,则被返转(Rolled Back)。

 

18.3.2.2 使用隐式控制

 

  在缺省情况下,Delphi通过BDE为应用程序提供隐式事务控制。当应用程序处于隐式事务控制时Delphi为DataSet中的写每个记录进行隐式事务控制。它提交每一个独立的写操作,如Post和Append Record。

  使用隐式事务控制是容易的,它保证最小的记录更新冲突和数据库的一致性视图。另一方面,因为写入数据库的数据的每一行都要进行事务控制, 所以隐式事务控制将导致网络过忙和应用程序性能下降。

  如果采用显式事务控制,就能选择最有效的时机来开始、提交和终止事务,特别是在开发多用户环境下的客户应用程序运行访问远程SQL服务器,就更应该采用显式控制。

 

18.3.2.3 使用显式事务控制

 

  有两种协作又独立的方式可运用于Delphi数据库应用的事务控制:

  ● 使用TDatabase部件的方法和属性

  ● 使用TQuery部件中的传递式SQL。这种方式只有在Delphi Client/Server Suite版中才有效,SQL Links将SQL表达式直接传给过程SQL或ODBC服务器

 

  使用TDatabase部件的方法和属性的好处是提供了清晰的、轻便的、与特定数据库或服务器无关的应用能力。

  使用传递式SQL的主要好处在于可以运用特定服务器的先进事务管理能力。

  1. 使用TDatabase的方法和属性

  下表中列出了TDatabase部件中用于事务管理的方法和属性以及它们的使用方法:

 

表18.11 TDatabase用于事务显式控制的方法表

  ━━━━━━━━━━━━━━━━━━━━━━━━

   方法或属性      作 用

  ────────────────────────

  Commit 提交数据的修改并终止事务

  Rollback 取消数据的修改并终止事务

  StartTransaction 开始一个事务

  TransIsolation 表述事务的独立性级别

  ━━━━━━━━━━━━━━━━━━━━━━━━

 

  StartTransaction,Commit和Rollback是供应用程序在运行时调用开始事务,控制事务并且保存或放弃所做数据修改的方法。

  TransIsolation是TDatabase部件的用于控制作用于相同表的不同事务之间如何交互的属性。

  ⑴ 开始一个事务

  当你开始一个事务时,后来的所有读写数据库的表达式都发生在那次事务的环境中。每个表达式都是其中一部分。任何表达式所做的修改,要么成功地提交给数据库,要么每一个修改都被取消。考虑一个在ATM上的银行传输问题。当顾客决定将钱从存款帐户转到支付帐户时,在银行数据库记录上必须发生两个修改:

  ● 存款帐户必须记入借方

  ● 支付帐户必须记入贷方

 

  如果出于某种原因,其中的一个操作不能被完成,那么任何一个操作都不应该发生。因为这些操作是相关的,它们应该发生在同一个事务中。

  为了开始Delphi应用程序中的一个事务,需要调用TDatabase部件中的StartTransaction方法:

 

  DatabaseInterBase.StartTransaction;

 

此后的所有数据操作都发生在最近一个事务的环境中,直到该次事务通过调用Commit或Rollback显式地终止为止。

  那么,应当将事务保持多久呢?理想状态下,只要需要,多久都行。事务的活跃状态越长,同时访问数据库的用户越多,在你的事务的生命其中,更多的并发、同时的事务开始和终止,于是当试图提交修改时,与其它事务冲突的可能性更大。

  ⑵ 提交一个事务

  为了做永久性修改,事务必须使用TDatabase部件的Commit方法提交。执行提交表达式将保存数据库的修改并终止事务。例如,下列表达式将终止在上例中开始的事务:

 

  DatabaseInterBase.Commit

 

  Commit的调用应当置于try…except表达式中。如果一个事务不能成功提交,你就能处理错误,并重试操作。

  ⑶ 返转一个事务

  为了取消数据库修改,必须用Rollback方法返转一个事务。Rollback 复原一个事务的修改,并终止事务,例如:下列表达式将返转一个事务:

 DatabaseInterBase.Rollback;

 

Rollback通常发生在:

  ● 异常处理代码

 ● 按钮或菜单事件代码,如用户点按了Cancel按钮

 

  ⑷ 使用TransIsolation属性

  TransIsolation属性描述TDatabase部件事务的独立级别,事务的独立级别决定了事务与其它作用于相同表的事务是如何相互作用的。在改变或设置TransIsolation 的值之前,应当相当熟悉Delphi中的事务和事务管理。

  TransIsolation的缺省值是tiReadCommitted。下表中总结了TransIsolation的可能值并描述了它们的含义:

 

表18.12 TransIsolation属性值的含义

 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

   独立级别         含 义

──────────────────────────────────────

  tiDirtyRead 允许读由其它同时事务写入数据库的未提交的修改。未提交的

             修改不是永久性的,可能在任何时候被复原。 在这个级别你

             的事务与其它事务所做的修改具有最低独立度。

  tiReadCommitted 只允许读由其它同时事务提交的数据库修改。这是缺省的独

             立级别。

  tiRepeatableRead 允许单个的数据库读事务无法看见其它同时事务对相同数据做

的修改。这个独立级别保证了你的事务一次读一个记录,记录

的视图不会改变, 在这个级别你的事务与其它事务做的修改完

全独立。

 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

  各种数据库服务器不同程度地支持这些独立级别,有的根本不支持。 如果请求的隔离级别不被服务器支持,Delphi将采用更高的独立级别。各种服务器支持的独立级别请参见表18.10。

如果应用程序使用ODBC与服务器交互,ODBC驱动程序必须支持独立级别。

  2. 使用传递式SQL

  为了能使用传递式SQL控制事物,必须:

  ● 使用Delphi Client/Server Suite

● 安装正确的SQL Links驱动程序

  ● 正确配置网络协议

  ● 访问远程服务器上数据库的能力

  ● 用BDE配置工具将SQLPASSTHROUGH MODE设置为NOT SHARED

 

通过传递式SQL,你可以使用TQuery、TStoredProc、或TUpdateSQL部件直接发送一个SQL事务控制表达式给远程数据库服务器;BDE本身并不处理SQL表达式,采用传递式SQL可使用户直接获得SQL服务器提供了事务控制优点,尤其是当那些控制是非标准的时。

  SQL PASS THROUGHMODE 描述BDE和传递式SQL是否共享相同的数据库联接。在大多数情况下,SQLPASSTHROUGHMODE被设置SHARED AUTOCOMMIT。然而,如果你想将SQL事务控制传递给服务器,你就必须用BDE配置工具,将BDE的SQLPASSTHROUGHMODE 设置为NOT SHARED。此外,还必须为传递SQL事务控制表达式的TQuery部件建立独立的TDatabase 部件。

  3. 使用本地事务

  BDE还支持Paradox和dBASE上的本地事务。从代码角度而言,在本地事务和远程数据库服务器的事务之间没有什么差别。

  当作用于本地数据库表的事务开始时,更新操作被记录在日志中,每个日志记录包含旧的记录缓冲区。当事务处于活跃状态,更新的记录被锁定,直到事务被提交或返转,在返转过程中,旧的记录被应用于将更新的记录恢复到原先的状态。

 

18.3.3 使用存储过程

 

18.3.3.1 TStoredProc部件概述

 

  存储过程是以数据库服务器为基础的接受输入参数,并将结果返回给应用程序的一段程序。TStoredProc部件操作远程服务器上的数据库中的存储过程。存储过程是一连串表达式的集合,作为服务器的一部分存储。存储过程在服务器上执行一系列重复性的与数据库相关的任务,并将结果传给客户应用程序,如Delphi数据库应用程序。

TStaredProc部件使Delphi数据库应用程序能执行服务器上的存储过程。

  通常,作用于数据库表中大量记录并且使用统计或数学函数的操作都是存储过程的首选对象。通过将这些重复计算任务转移到服务器,可以提高数据库应用程序的性能。

  ● 充分利用服务器的处理能力和速度

 ● 减少网络传输的数量

 

  例如,考虑一个需要计算单个值的应用程序,在大批记录中的标准差值。如果在Delphi应用程序中执行这项功能就必须从服务器中得到所有在计算中用到的记录,这必将导致网络拥塞。因为应用程序所需的只是代表标准差的最终返回值。因此,由服务器上的存储过程来读数据,执行计算和将值传给应用程序将更有效。

  1. TSoredProc的关键属性

  ⑴ DatabaseName属性

  DatabaseName属性描述要访问的数据库的名字。该属性可以为:

  ● 已定义的BDE别名

  ● 本地型数据库的目录

  ● Local InterBase服务器的目录路径和文件名

  ● TDatabase定义的应用程序别名

 

  在改变DatabaseName之前要使用Close方法将Dataset部件置为非活跃状态。

  ⑵ StoredProcName属性

  StoredProcName属性表示服务器上的存储过程名。Oracle服务器允许多个具有相同名字的存储过程。因此要设置Overload属性来描述执行在Oracle服务器上的存储过程名。

  ⑶ Overload属性

  Oracle服务器允许Oracle软件包中存储过程的重载。就是说具有相同名字的不同过程,设置Overload属性用来描述执行在Oracle服务器上的存储过程。如果Overload值为零,则假定没有重载,如果Overload为1,则Delphi执行具有同名的第一个存储过程;如果值为2,则执行第二个存储过程。

  ⑷ Params属性

  Params属性包含传给存储过程的参数。

  2. 关键方法

  ⑴ ParamByName方法

  声明:function ParamByName(const Value: String): TParam;

ParamByName方法返回Params属性中具有名为Value的元素值。一般用该方法在动态查询中给参数赋值。

  ⑵ Prepare方法

  PrePare方法准备要执行的存储过程,这允许服务器载入存储过程,否则准备处理异常。

  ⑶ ExecProc方法

  ExecProc方法执行服务器上的存储过程。

  ⑷ Open方法

  Open方法打开DataSet部件,并将其置于浏览状态。这相当于将Active属性置为True。对于TStoredProc如果存储过程返回一个结果集,则使用Open执行存储过程。如果存储过程返回单行,早使用ExecProc执行存储过程。

 

18.3.3.2 TStoreProc使用方法

 

  1. 建立一个StoredProc部件

  为数据库服务器上的存储过程建立一个TStoredProc部件的步骤如下:

  ⑴ 从Component Palette的Data Access页选择TStoredProc部件放在数据模块上。

  ⑵ 将TStoredProc部件的DatabaseName属性设置为存储过程所在的数据库名。

DatabaseName必须是BDE别名。

  ⑶ 将TStoredProc部件的StoredProcName属性设为所用的存储过程名,或者从下拉式

列表框中选择。

  ⑷ 在TStoredProc部件的Params属性中描述输入参数。可以使用参数编辑器来设置输

入参数。参数编辑器也可让用户察看存储过程返回给应用程序的值。

 

  2. 设置存储过程的输入参数,察看输出结果参数

  许多存储过程需要给它们传入一系列的输入参数,以确定处理什么和怎样处理,在Params属性中描述这些参数。所描述的输入参数的顺序是很重要的,它由服务器上的存储过程来确定。在设计时,最容易和最安全的方法是激活TStoredProc参数编辑器,编辑输入参数。参数编辑器以正确的次序列出输入参数,让你给它们赋值。

  要激活TStoredProc的参数编辑器:

① 选择TStoredProc部件。

  ② 按鼠标右键激活加速菜单(Speed Menu)。

③ 选择Define Parameters。

 

参数名列表框显示过程的所有输入,输出和结果参数。有关输入、输出参数的信息从服务器中获得。对于某些服务器而方,参数信息是不可访问的,如Sybase,在这种情况下,列表框是空的,因此必须自己按过程要求的顺序增加输入输出参数。

  参数类型复选框中描述所选的参数是输入、输出,还是结果参数。如果服务器支持参数可以既是输入又是输出。如果在列表框中增添参数,就必须设置参数类型。

  数据类型复选框,列出列表框中所选参数的数据类型。如果给列表框真善美参数,必须设置数据类型。

  在值编辑框中给输入参数赋值。

  如果服务器不传递存储过程信息给Delphi,则可以用Add按钮给存储过程增添参数。Delete按钮则是将增添的参数删除,Clear按钮将清除列表框中所有参数。

  设置完参数后,选择OK按钮。

 

  3. 在运行时建立参数和参数值

  在运行时建立参数,可直接访问Params属性。Params属性是参数字符串的数组。例如,下列代码将编辑框的文本赋给数组的第一个字串:

 

  StaredProc1.Params[0].Asstring := Edit1.Text

 

  也能够用ParamsByName方法通过名字访问参数:

 

  StoredProc1.ParamsByName('Company') Asstring := Edit1.Text;

 

  4. 准备和执行存储过程

  要使用存储过程还必须准备并执行它。可以有两种方式准备一个存储过程:

  ● 在设计时,通过选择参数编辑器的OK按钮

  ● 在运行时,通过调用TStoredProc的Prepare方法

 

  例如,下面的代码准备存储过程的执行:

 

  StoredProc1.Prepare;

 

  要执行准备好的存储过程,调用TStroedProc部件的ExecProc方法。下列代码演示了准备和执行存储过程:

 

   StoredProc1.Params[0] Asstring := Edit1.Text;

StoredProc1.prepare;

StoredProc1.ExecProc;

 

  当你执行一个存储过程,它返回输出参数或结果集,有两种可能的返回类型:单个返回,如单值或值集,和一群结果集,返回很多值。

  5. 访问输出参数和结果集

  存储过程在输出参数数组中返回值。如果服务器支持返回值可以是单个结果或者结果集。

  在运行时访问存储过程的输出参数,可以索引Params属性或者用ParamByName方法访问这些值。下列表达式都用输出参数设置了编辑框的值:

 

  Edit1.Text := StoredProc1.Params[6].AsString;

   Edit1.Text := StoredProc1.ParamsByName('Contact').AsString;

 

  如果存储过程返回结果集,则用标准数据相关控制访问和显示值会更有用。

  在某些服务器上如Sybase,存储过程能象查询语句那样返回结果集,应用程序可以使用数据相关控制一显示这些存储过程的输出。

  用数据相关控制显示存储过程返回结果的方法如下:

  ① 将DataSource部件放在数据模块上。

 ② 将DataSource部件的DataSet属性设置为接收数据的TStoredProc部件的名字。

  ③ 将数据相关控制的DataSource属性设为DataSource部件的名字。 

  这样,当用于TStoredProc部件和Active属性为True时,数据相关控制就能显示从存储过程返回的结果。 

18.3.4 从开发平台到服务器的向上适化


想死你们了!

TOP

DELPHI基础教程

第十八章 Delphi客户服务器应用开发(四)
18.3.4.1 适化概述 

  所谓适化就是将桌面应用转化为Client/Server应用。

  适化是一个很复杂的主题,这里不详细讲述。本节将介绍适化Delphi 应用程序中最重要的方面。

  适化的主要方面有:

  ● 将数据库从桌面平台到服务器的适化

  ● 将应用程序转化为Client/Server的适化

 

  适化还需要实现从桌面环境到Client/Server环境的转化。

  桌面数据库和SQL服务器数据库在许多方面有不同之处。例如:

 ● 桌面数据库用于同一时刻单用户的访问,而服务器用于多用户访问

 ● 桌面数据库是面向记录的,而服务器是面向集合的

  ● 桌面数据库将每个表存储在独立的文件中, 而服务器将所有的表存储在数据库中Client/Server应用必须解决更新的问题,最复杂的是联接、网络和事务控制

 

18.3.4.2 适化数据库

 

  适化数据库包含下列步骤:

  ● 在桌面数据库结构的基础上,定义服务器上的元数据

  ● 将数据从桌面转化到服务器中

 ● 解决下列问题:

  ● 数据类型差异

   ● 数据安全性和完整性

  ● 事务控制

   ● 数据访问权

   ● 数据合法性

   ● 锁定

 

 Delphi提供了两种方法适化一个数据库。

  ● 使用Database Desktop工具,选择菜单Tools/Utilities/Copy to命令将数据库表从桌面方式拷贝到SQL格式

● 建立应用TBatchMove部件的应用程序

 

  这两种方法都可以将表结构和数据从桌面数据源转化到服务器上。依靠这些数据库,可能需要改变结果表。例如,可能想进行不同数据类型的映射。

  也可以将下列特征加入数据库:

● 完整性约束

● 索引

  ● 检测约束

● 存储过程和触发器

  ● 其它服务器特征

 

  如果用SQL脚本和服务器数据定义工具定义元数据会更有效。然后用前面介绍的两种方法转移数据。因为如果是手工定义数据库表,Database Desktop和TBatchMove 部件将只拷贝数据。

 

18.3.4.3 适化应用程序

 

  在理论上,设计用来访问局部数据的Delphi应用程序做很少的修改就可以访问远程服务器上的数据。如果在服务器上定义适合的数据源,你就能将应用程序指向访问它,这只需简单地改变应用程序中TTable或TQuery部件的DatabaseName属性。

  实际上,在访问局部和过程数据源之间有许多重要的不同之处。Client/Server应用程序必须解决大量的在桌面应用中所没有的问题。

  任何Delphi应用程序都能用TTable或TQuery部件访问数据。桌面应用程序通常都是使用TTable部件。当适化到SQL服务器上时,用TQuery会更有效,如果应用程序要检索大量记录,则TQuery部件要略胜一筹。

  如果应用程序使用统计或数学函数,那么在服务器上通过存储过程执行这些函数会更有效。因为存储过程执行更快,使用存储过程还可以减少网络负载,特别是大量行数据的函数。

  例如,计算大量记录的标准差:

  ● 如果该函数在客户端执行,所有的值从服务器上检索出来并送到客户端,导致网络拥塞

  ● 如果该函数在服务器端执行,则应用程序只需要服务器上的答案

 

 

 

 

 

 

18.4 Delphi客户/服务器应用实例分析

 

  本节中采用的实例是Delphi2.0数据库的例子CSDEMO。CSDEMO是Delphi客户/服务器编程的示例程序,它采用的数据库服务器是Local InterBase Server。

CSDEMO较好地示范了BDE环境的配置,InterBASE Server高级功能应用,SQL服务器联接,触发器应用、存储过程编程和事务控制技术等,具有较高的参考价值。本节讲述下列内容:

  ● 数据库环境介绍

  ● TDatabase的应用

 ● 不同数据库表的切换

  ● 触发器编程

  ● 存储过程编程

 ● 事务控制应用

 

18.4.1 数据库环境介绍

 

  本例中采用的数据库服务器是Local InterBase Server。Local InterBase是InterBase Server的单用户版32位、兼容ANSI SQL。Local InterBase支持客户/服务器应用在单机上的开发和测试,并且可以很容易地适化到InterBase Server上。因此,开发客户/服务器应用采用Local InterBase作为原型开发环境是很方便的。

 

18.4.1.1 IBLOCAL的BDE参数

 

  本例中的SQL数据库是IBLOCAL。它是由BDE配置工具(BDECFG32.EXE)设置参数值。它的各项参数值列于下表:

 

表18.13 IBLOCAL的各项参数值

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

参 数 名 参 数 值

────────────────────────────────────

TYPE INTRBASE

PATH

SERVER NAME C:\INTRBASE\EXAMPLES\EMPLOYEE.GDB

USER NAME SYSDBA

OPEN MODE READ/WRITE

SCHEMA CACHE SIZE 8

LANGDRIVER

SQLQRYMODE

SQLPASSTHRU MODE SHARED AUTOCOMMIT

SCHEMA CHCHE TIME -1

MAX ROWS -1

BATCH COUNT 200

ENABLE SCHEMA CACHE FALSE

SCHEMA CACHE DIR

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

18.4.1.2 数据库结构介绍

 

 IBLOCAL数据库的结构都是由InterBase服务器工具交互式SQL工具(ISQL)定义的。

  用ISQL定义数据库,首先要用Create Database命令建立数据库,建立的新数据库一般是以GDB为扩展名。建立好后,就可以用SQL语言定义数据库表,例如建立EMPLOYEE表的SQL语句如下:

 

定义域名数据类型:

 

CREATE DOMAIN FIRSTNAME AS VARCHAR(15);

CREATE DOMAIN LASTNAME AS VARCHAR(20);

CREATE DOMAIN COUNTRYNAME AS VARCHAR(15);

CREATE DOMAIN EMPNO AS SMALLINT;

CREATE DOMAIN DEPTNO AS CHAR(3)

CHECK (VALUE = '000' OR (VALUE > '0' AND VALUE <= '999') OR VALUE IS NULL);

CREATE DOMAIN JOBCODE AS VARCHAR(5)

CHECK (VALUE > '99999');

CREATE DOMAIN JOBGRADE AS SMALLINT

CHECK (VALUE BETWEEN 0 AND 6);

CREATE DOMAIN SALARY AS NUMERIC(15, 2)

DEFAULT 0

CHECK (VALUE > 0);

 

建立EMPLOYEE表:

 

CREATE TABLE EMPLOYEE (EMP_NO EMPNO NOT NULL,

FIRST_NAME FIRSTNAME NOT NULL,

LAST_NAME LASTNAME NOT NULL,

PHONE_EXT VARCHAR(4),

HIRE_DATE DATE DEFAULT 'NOW' NOT NULL,

DEPT_NO DEPTNO NOT NULL,

JOB_CODE JOBCODE NOT NULL,

JOB_GRADE JOBGRADE NOT NULL,

JOB_COUNTRY COUNTRYNAME NOT NULL,

SALARY SALARY NOT NULL,

FULL_NAME COMPUTED BY (last_name || ', ' || first_name),

PRIMARY KEY (EMP_NO));

 

  CHECK语句是给数据库字段取值范围加约束条件。PRIMARY_KEY语句是给表建立关键字索引。

  如法炮制,就可以定义IBLOCAL中的所有表。

  IBLOCAL中的表包括:

 

EMPLOYEE CUSTOMER DEPARTMENT EMPLOYEE_PROJECT

PROJECT SALES SALARY_HISCORY

 

各数据库表中的内容如下:

 

表18.14 EmployeeDemoDB中各数据库表的内容

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━

    数据库表名        表中内容

   ───────────────────────────

     EMPLOYEE 雇员信息

   CUSTOMER 客户信息

DEPARTMENT 部门信息

EMPLOYEE_PROJECT 雇员负责的工程

PROJECT 工程信息

SALES 销售信息

SALARY_HISTORY 雇员薪水调整的历史信息

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

每个数据库表中都定义了关键字段。关于数据库表中的字段名、类型、大小,这里不再赘述。

 

18.4.2 应用程序分析

 

18.4.2.1 TDatabase部件的使用

 

  CSDEMO程序中定义了一个数据库模块部件——TDmEmployee,它是继承于TDataModule。TDataModule是在Delphi2.0中才出现的专门放置数据访问部件(如TDatabase、TTable和TQuery等)的框架。其它涉及数据库访问的窗体,只要在uses语句中插入数据库模块所在的库单元,该窗体上的数据库部件就可引用相应的数据库访问部件。

  在TDmEmployee中定义了一个TDatabase类型的部件──EmployeeDatabase。EmployeeDatagase的主要属性及属性值如下:

 

表18.15 EmployeeDatabase部件主要属性的取值

   ━━━━━━━━━━━━━━━━━━━━━━━

    属性        属性值

   ───────────────────────

    AliasName IBLOCAL

DatabaseName EmployeeDemoDB

KeepConnection True

LoginPrompt False

TransIsolation tiReadCommitted

Params USERNAME = SYSDBA

PASSWORD = masterkey

Connected True

  ━━━━━━━━━━━━━━━━━━━━━━━

 

AliasName属性所指定的IBLOCAL,必须已经在BDE中配置好,DatabaseName属性指定要使用的数据库名,该数据库名是由应用程序自己定义的,因此不反应到BDE中,该属性值被TTable、TQuery等DataSet部件引用,并且出现在DataSet部件的DatabaseName 下拉式列表框中。本例中的“EmployeeDemoDB”,被EmployeeTable,SalesTable等所有DataSet部件引用。

Connected为True表明,应用程序与数据库将保持联接。

KeepConnection属性为True,表明多次打开和关闭EmployeeDemoDB数据库中的任意表,应用程序将始终与数据库保持联接,这省却了重复注册的开销。

LoginPrompt 属性为False,表明应用程序自动处理与数据库的联接注册,因此,Params属性中定义了注册的用户名和口令:

 

  USERNAME = SYSDBA

PASSWORD = masterkey

 

TransIsolation属性为tiReadCommitted表明,如果存在多个同时事务,则某一事务只允许读由其它事务提交了的数据。

  程序中EmployeeDatabase的应用还与事务控制等有关。下文中会介绍这方面的内容。

 

18.4.2.2 不同数据库表的切换

 

  在许多数据库应用中都要在不同数据库表之间相互切换,以响应用户输入条件或系统状态的变化。这时,往往需要特别的处理,例如改变光标形状或隐藏数据改变等,尤其是在客户/服务器应用程序中。因为是用SQL语句访问远程数据库,有时还要在服务器端执行计算任务,所以客户端的数据变化会有一定的间隔,因此应该让用户明白发生了什么。下面是CSDEMO在数据库表切换时的处理办法:

 

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;

 

  crHourglass型光标表明正在执行SQL查询。DisableControls和EnableControls的作用是隐藏和显示数据变化。

 

18.4.2.3 InterBase触发器(Trigger)的应用

 

  在CSDEMO应用程序中,演示触发器应用的窗体是TFromTriggerDemo;

 

在该窗体中包含两个TDBGrid对象。DBGrid1显示EmployeeTable中的数据,DBGrid2显示SalaryHistoryTable中的数据。它们的主要属性及属性值如下:

 

表18.16 EmlpoyeeTable部件主要属性的取值

   ━━━━━━━━━━━━━━━━━━━━━

    属 性       属 性 值

   ─────────────────────

     DatabaseName EmployeeDemoDB

IndexFieldName Emp_No

TableName EMPLOYEE

━━━━━━━━━━━━━━━━━━━━━

 

表18.17 SalaryHistoryTable部件主要属性的取值

   ━━━━━━━━━━━━━━━━━━━━━

   属 性       属 性 表

    ─────────────────────

    DatabaseName EmployeeDemoDB

IndexFieldName Emp_No

     MasterFields Emp_No

MasterSource EmployeeSource

TableName SALARY_HISTORY

   ━━━━━━━━━━━━━━━━━━━━━

 

  这两个表之间存在两种关系:

  ● 连接关系

EmployeeTable的记录变化时,SalaryHistoryTable的数据要作相应的变化。这种连接关系是通过索引来实现的。

  ● 数据一致性

对EmployeeTable中的Salary字段的值作修改必须反映到SalaryHistoryTable中,SalaryHistoryTable维护的是Salary变化的历史信息。这种数据一致性要求在本程序中是通过触发器实现的。

  触发器是在SQL服务器端执行的一段程序,它在服务器端被触发执行完成一定的数据计算任务。

  下面是InterBase服务器上与Employee表相关的触发器程序:

 

Triggers on Table EMPLOYEE:

SAVE_SALARY_CHANGE, Sequence: 0, Type: AFTER UPDATE, Active AS

BEGIN

IF (old.salary <> new.salary) THEN

INSERT INTO salary_history

(emp_no, change_date, updater_id, old_salary, percent_change)

VALUES (

old.emp_no,

'now',

user,

old.salary,

(new.salary - old.salary) * 100 / old.salary);

END

 

  因为触发器是相应于EMPLOYEE表上的数据修改由服务器自动触发执行的,所以在客户应用程序上没有显式的调用。在客户端有打开并显示数据库表内容的程序和当SALARY_HISTORY表中数据变化时的更新显示的操作。

 

procedure TFrmTriggerDemo.FormShow(Sender: TObject);

begin

DmEmployee.EmployeeTable.Open;

DmEmployee.SalaryHistoryTable.Open;

end;

 

procedure TDmEmployee.EmployeeTableAfterPost(DataSet: TDataSet);

begin

{ 一个雇员的薪水变化将触发薪水调整历史记录的变化,

因此,如果SalaryHistory打开的话,就需要更新显示 }

with SalaryHistoryTable do if Active then Refresh;

end;

 

18.4.2.4 存储过程编程

 

  存储过程也是SQL服务器上的一段程序,它接收输入参数,在服务器端执行,并将结果返回客户端,存储过程是必须在客户应用程序中显式调用的。

  对于数据库表中大量记录的统计和函数计算,存储过程是很有用,这样可以将重复性计算任务转换到服务器,提高数据库应用的性能。

  Delphi中有两个部件能操作远程数据库服务器上的存储过程:TQuery和TStoredProc。

1. TQuery的存储过程编程

CSDEMO中演示用TQuery调用存储过程的窗体是TFrmQueryProc。 

  TFrmQueryProc中有两个TDBGrid 部件。DBGrid1显示EmployeeTable中的数据。DBGrid2显示Project表中的数据。使用存储过程的TQuery部件名为EmployeeProjectsQuery,它的作用是建立Employee 表和Project 表的连接,以实现当DBGrid1中记录改变时,DBGrid2中的数据作相应的改变。具体的连接任务是由服务器上的存储过程Get_Emp_Proj完成。下面是Get_Emp_Proj的程序:

 

PROCEDURE Get_Emp_Proj

BEGIN

FOR SELECT proj_id

FROM employee_project

WHERE emp_no = :emp_no

INTO :proj_id

DO

SUSPEND;

END

 

EMP_NO INPUT SMALLINT

PROJ_ID OUTPUT CHAR(5)

 

  该过程带两个参数:

  EMP_NO是输入参数,类型是SMALLINT.

PROJ_ID是输出参数,类型是CHAR(5)

 

  相应地,EmployeeProjectsQuery的主要属性如下:

 

表18. 18 EmployeeProjectsQuery部件主要属性的取值

━━━━━━━━━━━━━━━━━━━━━━━━━━

   属 性       属 性 值

   ──────────────────────────

    DatabaseName EmployeeDemoDB

Params EMP_No(输入参数,Smallint类型)

SQL Select * from

Get_Emp_Proj(:EMP_NO)

━━━━━━━━━━━━━━━━━━━━━━━━━━

 

TQuery部件是在SQL语句中直接调用存储过程。

  下面是客户端的程序:

 

procedure TFrmQueryProc.FormShow(Sender: TObject);

begin

DmEmployee.EmployeeTable.Open;

EmployeeSource.Enabled := True;

with EmployeeProjectsQuery do if not Active then Prepare;

end;

 

  用Prepare显式地准备SQL语句,虽非必须,但可以优化SQL的执行。

 

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;

 

  该事件处理过程与EmployeeSource的OnDataChange属性相联。用于当EmployeeTable数据记录变化时,修正存储过程的输入参数,并执行SQL语句。

  2. TStoredProc部件的存储过程编程

  TStoredProc Delphi 专门用来使用服务器存储过程的部件。CSDEMO 中演示用TStoredProc调用存储过程的窗体是TFrmExecPr

  在程序运行中,当按下ShipOrder按钮,要求对ORED_STA_TUS等字段的内容作修改以维护数据库的一致性。字段内容的修改任务由服务器上的存储过程SHIP_ORDER完成。SHIP_ORDE的程序如下:

 

PROCEDURE SHIP_ORDER

DECLARE VARIABLE ord_stat CHAR(7);

DECLARE VARIABLE hold_stat CHAR(1);

DECLARE VARIABLE cust_no INTEGER;

DECLARE VARIABLE any_po CHAR(8);

BEGIN

SELECT s.order_status, c.on_hold, c.cust_no

FROM sales s, customer c

WHERE po_number = :po_num

AND s.cust_no = c.cust_no

INTO :ord_stat, :hold_stat, :cust_no;

IF (ord_stat = "shipped") THEN

BEGIN

EXCEPTION order_already_shipped;

SUSPEND;

END

ELSE IF (hold_stat = "*") THEN

BEGIN

EXCEPTION customer_on_hold;

SUSPEND;

END

 

FOR SELECT po_number

FROM sales

WHERE cust_no = :cust_no

AND order_status = "shipped"

AND paid = "n"

AND ship_date < 'NOW' - 60

INTO :any_po

DO

BEGIN

EXCEPTION customer_check;

 

UPDATE customer

SET on_hold = "*"

WHERE cust_no = :cust_no;

 

SUSPEND;

END

 

UPDATE sales

SET order_status = "shipped", ship_date = 'NOW'

WHERE po_number = :po_num;

SUSPEND;

END

 

Parameters:

PO_NUM INPUT CHAR(8)

 

  该过程只带有一个输入参数:PO_NUM,类型是CHAR(8)。

  在客户端使用该过程的TStoreProc部件是ShipOrderProc,其主要属性如下表:

 

表18.19 ShipOrderProc部件主要属性的取值

━━━━━━━━━━━━━━━━━━━━━━━━━━━━

   属性名          属 性 值

    ────────────────────────────

    DatabaseName EmployeeDemoDB

ParamBindMode pbByName

Params PO_NUM(输入参数,String类型)

StoredProcName SHIP_ORDER

━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

  客户端执行SHIP_ORDER的程序如下:

 

procedure TFrmExecProc.BtnShipOrderClick(Sender: TObject);

begin

with DmEmployee do

begin

ShipOrderProc.Params[0].AsString := SalesTable['PO_NUMBER'];

ShipOrderProc.ExecProc;

SalesTable.Refresh;

end;

end;

 

  当用户按ShipOrder按钮时,执行这段程序。程序中先准备输入参数,用ExecProc方

法执行存储过程。调用SalesTable.Refresh方法刷新数据显示。

  在CSDEMO应用程序中另一个使用存储过程的TStoredProc部件是DeleteEmployeeProc。它完成的任务是删除Employee表中的记录,并修改所有相关的表, 以维护数据的一致性。其属性如下:

 

表18.20 DeleteEmployeeProc部件主要属性的取值

  ━━━━━━━━━━━━━━━━━━━━━━━━━━

   属性名          属 性 值

  ──────────────────────────

   DataBaseName EmployeeDemoDB

ParamBindMode PbByName

Params EMP_NUM(输入参数,整型)

StoredProcName DELETE_EMPLOYEE

━━━━━━━━━━━━━━━━━━━━━━━━━━

 

  存储过程DELETE_EMPLOYEE的程序如下:

 

PROCEDURE DELETE_EMPLOYEE

DECLARE VARIABLE any_sales INTEGER;

BEGIN

any_sales = 0;

SELECT count(po_number)

FROM sales

WHERE sales_rep = :emp_num

INTO :any_sales;

IF (any_sales > 0) THEN

BEGIN

EXCEPTION reassign_sales;

SUSPEND;

END

UPDATE department

SET mngr_no = NULL

WHERE mngr_no = :emp_num;

UPDATE project

SET team_leader = NULL

WHERE team_leader = :emp_num;

DELETE FROM employee_project

WHERE emp_no = :emp_num;

DELETE FROM salary_history

WHERE emp_no = :emp_num;

DELETE FROM employee

WHERE emp_no = :emp_num;

SUSPEND;

END

 

Parameters:

EMP_NUM INPUT INTEGER

 

  从上述存储过程的例子中,我们看到存储过程在维护服务器上的数据一致性方面有很强的能力,它节省了系统开销,提高了客户端的性能。

 

18.4.2.5 事务控制编程

 

  在客户/服务器应用程序中,事务控制是一项很重要的技术。它对于提高系统的可靠性,维护数据一致性有着重要的意义。

  Delphi中提供了事务的隐式和显式两种控制方法。其中显式控制的性能较高,下面介绍Delphi事务显式控制的编程方法。

  Delphi担当事务控制任务的部件是TDatabase 。TDatabase 用于事务控制的属性是TransIsolation,方法有StartTranstion、Commit和Rollback。关于这些属性和方法作用和使用方法请参阅客户/服务器事务管理。

  在CSDEMO中TDatabase 部件为EMployeeDatabase,其TransIsolation属性值为tiReadCommitted,意为如果存在多个同时事务访问数据库,则其中任一事务只能读其它事务提交的了数据。

  CSDEMO中演示事务控制的窗体是TFrmTransDemo。

  DBGrid1中显示EmployeeTable中的内容。当窗口显示时,EmployeeDatabase开始一次事务控制并激活EmployeeTable:

 

procedure TFrmTransDemo.FormShow(Sender: TObject);

begin

DmEmployee.EmployeeDatabase.StartTransaction;

DmEmployee.EmployeeTable.Open;

end;

 

当窗口被关闭或隐藏时,EmployeeDatabase提交事务:

 

procedure TFrmTransDemo.FormHide(Sender: TObject);

begin

DmEmployee.EmployeeDatabase.Commit;

end;

 

  窗口中有两个按钮BtnCommitEdits和BtnUndoEdits。按下BtnCommitEdits按钮将提交当前事务,并开始新的事务控制并刷新数据。

 

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''t Commit Changes: No Transaction Active', mtError, [mbOk], 0);

end;

 

  按下BtnUndoEdits按钮将返转当前事物,恢复原来的数据,开始新的事务控制,并刷新数据的显示。

 

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''t Undo Edits: No Transaction Active', mtError, [mbOk], 0);

end;


想死你们了!

TOP

DELPHI基础教程

第十九章 Delphi自定义部件开发(一)
   Delphi除了支持使用可视化部件所见即所得地建立应用程序外,还支持为开发应用而设计自己的部件。

  在本章中将阐述如何为Delphi应用程序编写部件。这一章将达到两个目的:

  ● 教你如何自定义部件

  ● 使你的部件成为Delphi环境的有机组合部分

 

19.1 Delphi部件原理

 

19.1.1 什么是部件

 

  部件是Delphi应用程序的程序构件。尽管大多数部件代表用户界面的可见元素,但部件也可以是程序中的不可见元素,如数据库部件。为弄清什么是部件可以从三个方面来考察它:功能定义、技术定义和经验定义。

  1. 部件的功能定义

  从最终用户角度,部件是在Component Palette上选择的,并在窗体设计窗口和代码窗口中操作的元素。从部件编写者角度,部件是代码中的对象。在编写部件之前,你应用相当熟悉已有的Delphi部件,这样才能使你的部件适合用户的需要。编写部件的目标之一是使部件尽可能的类似其它部件。

  2. 部件的技术定义

  从最简单的角度看,部件是任何从TComponent继承的对象。TComponent定义了所有部件必须要的、最基本的行为。例如,出现在Component Palette上和在窗体设计窗口中编辑的功能。但是TComponent并不知如何处理你的部件的具体功能,因此,你必须自己描述它。

  3. 部件编写者自己的定义。

  在实际编程中,部件是能插入Delphi开发环境的任何元素。它可能具有程序的各种复杂性。简而言之,只要能融入部件框架,部件就是你用代码编写的一切。部件定义只是接口描述,本章将详细阐述部件框架,说明部件的有限性,正如说明编程的有限性。本章不准备教你用所给语言编写每一种部件,只能告诉编定代码的方法和怎样使部件融入Delphi环境。

  

19.1.2 编写部件的不同之处

 

  在Delphi环境中建立部件和在应用程序中使用部件有三个重要差别:

  ● 编写部件的过程是非可视化的

  ● 编写部件需要更深入的关于对象的知识

  ● 编写部件需要遵循更多的规则

 

  1. 编写部件是非可视化的

  编写部件与建立Delphi应用最明显的区别是部件编写完全以代码的形式进行,即非可视化的 。因为Delphi应用的可视化设计需要已完成的部件,而建立这些部件就需要用Object Pascal 代码编写。

  虽然你无法使用可视化工具来建立部件,但你能运用 Delphi开发环境的所有编程特性如代码编辑器、集成化调试和对象浏览。

  2. 编写部件需要更深的有关对象的知识

  除了非可视化编程之外,建立部件和使用它们的最大区别是:当建立新部件时,需要从已存部件中继承产生一个新对象类型,并增加新的属性和方法。另一方面,部件使用者,在建立Delphi应用时,只是使用已有部件。在设计阶段通过改变部件属性和描述响应事件的方法来定制它们的行为。

  当继承产生一个新对象时,你有权访问祖先对象中对最终用户不可见的部分。这些部分被称为protected界面的。在很大部分的实现上,后代对象也需要调用他们的祖先对象的方法,因此,编写部件者应相当熟悉面向对象编程特性。

  3. 编写部件要遵循更多的规则

  编写部件过程比可视化应用生成采用更传统的编程方法,与使用已有部件相比,有更多的规则要遵循。在开始编写自己的部件之前,最重要的事莫过于熟练应用Delphi自带的部件,以得到对命名规则以及部件用户所期望功能等的直观认识。部件用户期望部件做到的最重要的事情莫过于他们在任何时候能对部件做任何事。编写满足这些期望的部件并不难,只要预先想到和遵循规则。

 

19.1.3 建立部件过程概略

 

  简而言之,建立自定义部件的过程包含下列几步: 

● 建立包含新部件的库单元

  ● 从已有部件类型中继承得到新的部件类型

  ● 增加属性、方法和事件

  ● 用Delphi注册部件

  ● 为部件的属性方法和事件建立Help文件

 

  如果完成这些工作,完整的部件包含下列4个文件

  ● 编译的库单元   ( .DCU文件)

● 选择板位图    (.DCR文件)

● Help文件     (.HLP文件)

● Help-keyword文件 (.KWF文件)

 

19.2 Delphi部件编程方法

 

19.2.1 Delphi部件编程概述

 

19.2.1.1 Delphi可视部件类库

 

   Delphi的部件都是可视部件类库(VCL)的对象继承树的一部分,下面列出组成VCL的对象的关系。TComponent是VCL中每一个部件的共同祖先。TComponent提供了Delphi部件正常工作的最基本的属性和事件。库中的各条分支提供了其它的更专一的功能。 

当建立部件时,通过从对象树中已有的对象继承获得新对象,并将其加入VCL中。 

19.2.1.2 建立部件的起点 

  部件是你在设计时想操作的任意程序元素。建立新部件意味着从已有类型中继承得到新的部件对象类。

建立新部件的主要途径如下:

  ● 修改已有的控制

  ● 建立原始控制

 ● 建立图形控制

  ● 建立Windows控制的子类

  ● 建立非可视部件

 

  下表列出了不同建立途径的起始类

 

表19.1 定义部件的起始点

 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 途 径      起 始 类

  ─────────────────────────────

修改已有部件   任何已有部件,如TButton、TListBox

或抽象部件对象如TCustomListBox

建立原始控制    TCustomControl

建立图形控制 TGraphicControl

建立窗口控制的子类 TWinControl

建立非可视部件   TComponent

 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

  也可以继承非部件的其它对象,但无法在窗体设计窗口中操作它们。Delphi包括许多这种对象,如TINIFile、TFont等。

  1. 修改已有控制

  建立部件的最简单的方法是继承一个已有的、可用的部件并定制它。可以从Delphi提供的任何部件中继承。例如,可以改变标准控制的缺省属性值,如TButton。

  有些控制,如Listbox和Grid等有许多相同变量,在这种情况下,Delphi提供了抽象控制类型,从该类型出发可定制出许多的类型。例如,你也许想建立TListBox的特殊类型,这种部件没有标准TListBox的某些属性,你不能将属性从一个祖先类型中移去,因此你需要从比TListBox更高层次的部件继承。例如TCustomListBox,该部件实现了TCustomListBox的所有属性但没有公布(Publishing)它们。当从一个诸如TCustomListBox的抽象类中继承时,你公布那些你想使之可获得的属性而让其它的保护起来(protected)。

  2. 建立原始控制

  标准控制是在运行时可见的。这些标准控制都从TWinControl,继承来的,当你建立原始控制时,你使用TCustomControl作为起始点。标准控制的关键特征是它具有窗口句柄,句柄保存在属性Handle中,这种控制:

  ● 能接受输入焦点

  ● 能将句柄传送给Windows API函数

 

  如果控制不需要接受输入焦点,你可把它做成图形控制,这可能节省系统资源。

  3. 建立图形控制

  图形控制非常类似定制的控制,但它们没有窗口句柄,因此不占有系统资源。对图形控制最大的限制是它们不能接收输入焦点。你需要从TGraphicControl继承,它提供了作图的Canvas和能处理WM_PAINT消息,你需要覆盖Paint方法。

  4. 继承窗口控制

Windows中有一种称之为窗口类的概念,类似于面向对象的对象和类的概念。窗口类是Windows中相同窗口或控制的不同实例之间共享的信息集合。当你用传统的Windows编程方法创建一种新的控制,你要定义一个新的窗口类,并在Windows中注册。你也能基于已有的窗口类创建新的窗口类。这就称为从窗口类继承。在传统的Windows编程中,如果你想建立客户化的控制,你就必须将其做在动态链接库里,就象标准Windows控制,并且提供一个访问界面。使用Delphi,你能创建一个部件包装在已有窗口类之上。如果你已有客户化控制的库,并想使其运行在你的Delphi应用中,那你就能创建一个使你能使用已有控制和获得新的控制的部件。在库单元StdCtrls中有许多这样的例子。

  5. 建立非可视化的部件

  抽象对象类型TComponent是所有部件的基础类型。从TComponent直接继承所创建的部件就是非可视化部件。你编写的大多数部件都是可视控制。TComponent定义了部件在FormDesigner中所需的基本的属性和方法。因此,从TComponent继承来的任何部件都具备设计能力。

  非可视部件相当少,主要用它们作为非可视程序单元(如数据库单元)和对话框的界面。

 

19.2.1.3 建立新部件的方法

 

  建立新部件的方法有两种:

  ● 手工建立部件

 ● 使用Component Expert

 

 一旦完成建立后,就得到所需的最小功能单位的部件,并可以安装在Component Palette上。安装完后,你就能将新部件放置在窗体窗口,并可在设计阶段和运行阶段进行测试。你还能为部件增加新的特征、更新选择板、重新测试。

  1. 手工创建部件

显然创建部件最容易的方法是使用Component Expert。然而,你也能通过手工来完成相同步骤。

  手工创建部件需要下列三步:

  ● 创建新的库单元

  ● 继承一个部件对象

 ● 注册部件

 

  ⑴ 创建新的库单元

  库单元是Object Pascal代码的独立编译单位。每一个窗体有自己的库单元。大多数部件(在逻辑上是一组)也有自己的库单元。

  当你建立部件时,你可以为部件创建一个库单元,也可将新的部件加在已有的库单元中。

 ① 为部件创建库单元,可选择File/New... ,在New Items对话框中选择Unit,Delphi将创建一个新文件,并在代码编辑器中打开它

 ② 在已有库单元中增加部件,只须选择File/OPen为已有库单元选择源代码。在该库单元中只能包含部件代码,如果该库单元中有一个窗体,将产生错误

 

  ⑵ 继承一个部件对象

  每个部件都是TComponent的后代对象。也可从TControl、TGraphicControl等继承。

  为继承一个部件对象,要将对象类型声明加在库单元的interface部分。

  例如,建立一个最简单的从TComponent直接继承非可视的部件,将下列的类型定义加在部件单元的interface部分。

 

  type

TNewComponent=class(TComponent)

……

end;

 

 现在你能注册TNewComponent。但是新部件与TComponent没什么不同,你只创建了自己部件的框架。

  ⑶ 注册部件

  注册部件是为了告诉Delphi什么部件被加入部件库和加入Component Palette的哪一页。

  为了注册一个部件:

  ① 在部件单元的interface部分增加一个Register过程。Register不带任何参数,因此声明很简单:

 

procedure Register;

 

如果你在已有部件的库单元中增加部件,因为已有Register 过程,因此不须要修改声明。

  ② 在库单位的implementation部件编写Register过程为每一个你想注册的部件调用过程RegisterComponents,过程RegisterComponents带两个参数:Component Palette的页名和部件类型集。例如,注册名为TNewComponent的部件,并将其置于Component Palette的Samples页,在程序中使用下列过程:

 

procedure Register;

begin

RegisterComponents('Samples', [TNewComponent]);

end;

 

 一旦注册完毕,Delphi自动将部件图标显示在Component Palette上。

  2. 使用Component Expert(部件专家)

  你能使用Component Expert创建新部件。使用Component Expert简化了创建新部件最初阶段的工作,因为你只需描述三件事:

  ● 新部件的名字

  ● 祖先类型

  ● 新部件要加入的Component Palette页名

 

  Component Expert执行了手工方式的相同工作:

  ● 建立新的库单元

 ● 继承得到新部件对象

  ● 注册部件

 

  但Component Expert不能在已有单元中增加部件。

可选择File/New... ,在New Items对话框中选择Component,就打开Component Expert对话框。 

  填完Component Expert对话框的每一个域后,选择OK。Delphi建立包括新部件和Register过程的库单元,并自动增加uses语句。

  你应该立刻保存库单元,并给予其有意义的名字。

 

19.2.1.4. 测试未安装的部件

 

  在将新部件安装在Component Palette之前就能测试部件运行时的动作。这对于调试新部件特别有用,而且还能用同样的技术测试任意部件,无论该部件是否出现在Component Palette上。

  从本质上说,你通过模仿用户将部件放置在窗体中的Delphi的动作来测试一个未安装的部件。

  可按下列步骤来测试未安装的部件

  1. 在窗体单元的uses语句中加入部件所在单元的名字

2. 在窗体中增加一个对象域来表示部件

  这是自己增加部件和Delphi增加部件的方法的主要不同点。

  你将对象域加在窗体类型声明底部的public部分。Delphi则会将对象域加在底部声明的上面。

  你不能将域加在Delphi管理的窗体类型的声明的上部。在这一部分声明的对象域将相应在存储在DFM文件中。增加不在窗体中存在的部件名将产生DFM文件无效的错误。

 3. 附上窗体的OnCreate事件处理过程

  4. 在窗体的OnCreate处理过程中构造该部件

  当调用部件的构造过程时,必须传递Owner参数(由Owner负责析构该部件)一般说来总是将Self作为Owner的传入参数。在OnCreate中,Self是指窗体。

  5. 给Component的Parent属性赋值

  设置Parent属性往往是构造部件后要做的第一件事时。Parent在形式上包含部件,一般来说Parent是窗体或者GoupBox、Panel。通常给Parent赋与Self,即窗体。在设置部件的其它属性之前最好先给Parent赋值。

  6. 按需要给部件的其它属性赋值

  假设你想测试名为TNewComponent类型的新部件,库单元名为NewTest。窗体库单元应该是这样的;

 

unit Unitl;

 

interface

 

uses SysUtils, Windows, Messages, Classes, Grophics, Controls, Forms, Dialogs,

Newtest;

type

Tforml = class(TForm)

procedure FormCreate(Sender: TObject);

private

{ private申 明 }

public

{ public申 明 }

NewComponent: TNewComponent;

end;

 

var

Forml: TForml;

 

implementation

 

{$R *.DFM }

 

procedure TForml.FormCreate ( Sender: TObject ) ;

begin

NewComponent := TNewComponent.Create ( Self );

NewCompanent.Parent := Self;

NewCompanent.Left := 12;

end;

 

end.

 

19.2.1.5 编写部件的面向对象技术

 

  部件使用者在Delphi环境中开发,将遇到在包含数据和方法的对象。他们将在设计阶段和运行阶段操作对象,而编写部件将比他们需要更多的关于对象的知识,因此,你应当熟悉Delphi的面向对象的程序设计。

  1. 建立部件

  部件用户和部件编写者最基本的区别是用户处理对象的实例,而编写者创建新的对象类型。这个概念是面向对象程序设计的基础。例如,用户创建了一个包含两个按钮的窗体,一个标为OK,另一个标为Cancel,每个都是TButton的实例,通过给Text、default和Cancel等属性赋不同的值,给OnClick事件赋予不同的处理过程,用户产生了两个不同的实例。

建立新部件一般有两个理由

  ● 改变类型的缺省情况,避免反复

  ● 为部件增加新的功能

 

  目的都是为了建立可重用对象。如果从将来重用的角度预先计划和设计,能节省一大堆将来的工作。

  在程序设计中,避免不必要的重复是很重要的。如果发现在代码中一遍又一遍重写相同的行,就应当考虑将代码放在子过程或函数中,或干脆建立一个函数库。

  设计部件也是这个道理,如果总是改变相同的属性或相同的方法调用,那应创建新部件。

  创建新部件的另一个原因是想给已有的部件增加新的功能。你可以从已有部件直接继承(如ListBox)或从抽象对象类型继承(如TComponent,TControl)。你虽然能为部件增加新功能,但不能将原有部件的属性移走,如果要这样做的话,就从该父对象的祖先对象继承。

  2. 控制部件的访向

  Object Pascal语言为对象的各部分提供了四个级别的访问控制。访问控制让你定义什么代码能访问对象的哪一部分。通过描述访问级别,定义了部件的接口。如果合理安排接口,将提高部件的可用性和重用性。

  除非特地描述,否则加在对象里的域、方法和属性的控制级别是published,这意味着任何代码可以访问整个对象。

  下表列出各保护级别:

 

 

表19.2 对象定义中的保护级别

  ━━━━━━━━━━━━━━━━━━━

 保护级       用处

   ───────────────────

private 隐藏实现细节

protected     定义开发者接口

public 定义运行时接口

published 定义设计时接口

  ━━━━━━━━━━━━━━━━━━━

 

所有的保护级都在单元级起作用。如果对象的某一部分在库单元中的一处可访向,则在该库单元任意处都可访向。

  ⑴ 隐藏实现细节

  如果对象的某部分被声明为private,将使其它库单元的代码无法访问该部分,但包含声明的库单元中的代码可以访问,就好象访问public一样,这是和C++不同的。

  对象类型的private部分对于隐藏详细实现是很重要的。既然对象的用户不能访问,private部分,你就能改变对象的实现而不影响用户代码。

  下面是一个演示防止用户访问private域的例子:

 

unit HideInfo;

 

interface

 

uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms,

Dialogs;

 

type

TSecretForm = class(TForm) { 声明新的窗体窗口 }

procedure FormCreate(Sender: TObject);

private { declare private part }

FSecretCode: Integer; { 声明private域 }

end;

 

var

SecretForm: TSecretForm;

 

implementation

 

procedure TSecretForm.FormCreate(Sender: TObject);

begin

FSecretCode := 42;

end;

 

end.

 

unit TestHide; { 这是主窗体库单元 }

 

interface

 

uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms,

Dialogs, HideInfo; { 使用带TSecretForm声明的库单元 }

type

TTestForm = class(TForm)

procedure FormCreate(Sender: TObject);

end;

 

var

TestForm: TTestForm;

 

implementation

 

procedure TTestForm.FormCreate(Sender: TObject);

begin

SecretForm.FSecretCode := 13; {编译过程将以"Field identifier expected"错误停止}

end;

 

end.

 

  ⑵ 定义开发者接口

  将对象某部分声明为protected,可使在包含该部件声明的库单元之外的代码无法访问,就象private部分。protected部分的不同之处是,某对象继承该对象,则包含新对象的库单元可以访问protected部分,你能使用protected声明定义开发者的接口。也就是说。对象的用户不能访向protected部分,但开发者通过继承就可能做到,这意味着你能通过protected部分的可访问性使部件编写者改变对象工作方式,而又不使用户见到这些细节。

  ⑶ 定义运行时接口

  将对象的某一部分定义为public可使任何代码访问该部分。如果你没有对域方法或属性加以private、protected、public的访问控制描述。那么该部分就是published。

  因为对象的public部分可在运行时为任何代码访问,因此对象的public部分被称为运行接口。运行时接口对那些在设计时没有意义的项目,如依靠运行时信息的和只读的属性,是很有用的。那些设计用来供用户调用的方法也应放在运行时接口中。

  下例是一个显示两个定义在运行时接口的只读属性的例子:

 

type

TSampleComponent = class(TComponent)

private

FTempCelsius: Integer; { 具体实现是private }

function GetTempFahrenheit: Integer;

public

property TempCelsius: Integer read FTempCelsius; { 属性是public }

property TempFahrenheit: Integer read GetTempFahrenheit;

end;

 

function GetTempFahrenheit: Integer;

begin

Result := FTempCelsius * 9 div 5 + 32;

end;

 

  既然用户在设计时不能改变public部分的属性的值,那么该类属性就不能出现在Object Inspector窗口中。

  ⑷ 定义设计时接口

  将对象的某部分声明为published,该部分也即为public且产生运行时类型信息。但只有published部分定义的属性可显示在Object Inspector窗口中。对象的published部分定义了对象的设计时接口。设计时接口包含了用户想在设计时定制的一切特征。

  下面是一个published属性的例子,因为它是published,因此可以出现在Object Inspector窗口:

 

TSampleComponent = class(TComponent)

private

FTemperature: Integer; { 具体实现是 private }

published

property Temperature: Integer read FTemperature write FTemperature; { 可写的 }

end;

 

  3. 派送方法

  派送(Dispatch)这个概念是用来描述当调用方法时,你的应用程序怎样决定执行什么样的代码,当你编写调用对象的代码时,看上去与任何其它过程或函数调用没什么不同,但对象有三种不同的派送方法的方式。

  这三种派送方法的类型是:

  ● 静态的

  ● 虚拟的

  ● 动态的

 

  虚方法和动态方法的工作方式相同,但实现不同。两者都与静态方法相当不同。理解各种不同的派送方法对创建部件是很有用的。

 ⑴ 静态方法:

  如果没有特殊声明,所有的对象方法都是静态的.。静态方法的工作方式正如一般的过程和函数调用。在编译时,编译器决定方法地址,并与方法联接。

  静态方法的基本好处是派送相当快。因为由编译器决定方法的临时地址,并直接与方法相联。虚方法和动态方法则相反,用间接的方法在运行时查找方法的地址,这将花较长的时间。

  静态方法的另一个不同之处是当被另一类型继承时不做任何改变,这就是说如果你声明了一个包含静态方法的对象,然后从该对象继承新的对象,则该后代对象享有与祖先对象相同的方法地址,因此,不管实际对象是谁,静态方法都完成相同的工作。

  你不能覆盖静态方法,在后代对象中声明相同名称的静态方法都将取代祖先对象方法。

  在下列代码中,第一个部件声明了两静态方法,第二个部件,声明了相同名字的方法取代第一个部件的方法。

 

type

TFirstComponent = class(TComponent)

procedure Move;

procedure Flash;

end;

 

TSecondComponent = class(TFirstComponent)

procedure Move; { 尽管有相同的声明,但与继承的方法不同 }

function Flash(HowOften: Integer): Integer; { 同Move方法一样 }

end;

 

  ⑵ 虚方法

  调用虚方法与调用任何其它方法一样,但派送机制有所不同。虚方法支持在后代对象中重定义方法,但调用方法完全相同,虚方法的地址不是在编译时决定,而是在运行时才查找方法的地址。

  为声明一个新的方法,在方法声明后增加virtual指令。方法声明中的virtual指令在对象虚拟方法表(VMT)中创建一个入口,该虚拟方法表保存对象类所有虚有拟方法的地址。

  当你从已有对象获得新的对象,新对象得到自己的VMT,它包含所有的祖先对象的VMT入口,再增加在新对象中声明的虚拟方法。后代对象能覆盖任何继承的虚拟方法。

  覆盖一个方法是扩展它,而不是取代它。后代对象可以重定义和重实现在祖先对象中声明的任何方法。但无法覆盖一个静态方法。覆盖一个方法,要在方法声明的结尾增加override指令,在下列情况,使用override将产生编译错误:

  ● 祖先对象中不存在该方法

  ● 祖先对象中相同方法是静态的

  ● 声明与祖先对象的(如名字、参数)不匹配

 

  下列代码演示两个简单的部件。第一个部件声明了三个方法,每一个使用不同的派送方式,第二个部件继承第一个部件,取代了静态方法,覆盖了虚拟方法和动态方法。

 

type

TFirstComponent = class(TCustomControl)

procedure Move; { 静态方法 }

procedure Flash; virtual; { 虚 方 法 }

procedure Beep; dynamic; { 动态虚拟方法 }

end;

 

TSecondComponent = class(TFirstComponent)

procedure Move; { 声明了新的方法 }

procedure Flash; override; { 覆盖继承的方法 }

procedure Beep; override; { 覆盖继承的方法 }

end;

 

  ⑶ 动态方法

  动态方法是稍微不同于虚拟方法的派送机制。因为动态方法没有对象VMT的入口,它们减少了对象消耗的内存数量。派送动态方法比派送一般的虚拟方法慢。因此,如果方法调用很频繁,你最好将其定义为虚方法。

  定义动态方法时,在方法声明后面增加dynamic指令。

  与对象虚拟方法创建入口不同的是dynamic给方法赋了一数字,并存储相应代码的地址,动态方法列表只包含新加的和覆盖的方法入口,继承的动态方法的派送是通过查找每一个祖先的动态方法列表(按与继承“反转的顺序”),因此动态方法用于处理消息(包括Windows消息)。实际上,消息处理过程的派送方式与动态方法相同,只是定义方法不同

  ⑷ 对象与指针

  在Object Pascal中,对象实际上是指针。编译器自动地为程序创建对象指针,因此在大多数情况下,你不需要考虑对象是指针。但当你将对象作为参数传递时,这就很重要了。通常,传递对象是按值而非按引用,也就是说,将对象声明为过程的参数时,你不能用var参数,理由是对象已经是指针引用了。


想死你们了!

TOP

DELPHI基础教程

第十九章 Delphi自定义部件开发(二)
19.2.2 Delphi部件编程 

19.2.2.1 创建属性 

 属性(Property)是部件中最特殊的部分,主要因为部件用户在设计时可以看见和操作它们,并且在交互过程中能立即得到返回结果。属性也很重要,因为如果将它们设计好后,将使用户更容易地使用,自己维护起来也很容易。

  为了使你在部件中更好地使用属性,本部分将介绍下列内容:

 ● 为什么要创建属性

  ● 属性的种类

 ● 公布(publishing)继承的属性

  ● 定义部件属性

  ● 编写属性编辑器

 

  1. 为什么要创建属性

  属性提供非常重要的好处,最明显的好处是属性在设计时能出现在Object Inspector窗口中,这将简化编程工作,因为你只需读用户所赋的值,而不要处理构造对象的参数。

  从部件使用者的观点看,属性象变量。用户可以给属性赋值或读值,就好象属性是对象的域。

  从部件编写者的观点看属性比对象的域有更强的功能;

  ⑴ 用户可以在设计时设置属性

  这是非常重要的,因为不象方法,只能在运行时访问。属性使用户在运行程序之前就能定制部件,通常你的部件不应包含很多的方法,它们的功能可以通过属性来实现。

  ⑵ 属性能隐藏详细的实现细节

  ⑶ 属性能引起简单地赋值之外的响应,如触发事件

  ⑷ 用于属性的实现方法可以是虚拟方法,这样看似简单的属性在不同的部件中,将实现不同的功能。

 

 2. 属性的类型

  属性可以是函数能返回的任何类型,因为属性的实现可以使用函数。所有的Pascal类型,兼容性规则都适用属性。为属性选择类型的最重要的方面是不同的类型出现在Object Inspector窗口中的方式不同。Object Inspector将按不同的类型决定其出现的方式。

你也能在注册部件时描述不同的属性编辑器。

  下表列出属性出现在Object Inspector窗口中的方式

 

表19.3 属性出现在Object Inspector窗口中的方式

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

属性类型       处 理 方 式

───────────────────────────────────────

简单类型   Numeric、Character和 String属性出现在Object Inspector中,用户可

以直接编辑

枚举类型 枚举类型的属性显示值的方式定义在代码中。选择时将出现下拉  

式列表框,显示所有的可能取值。

集合类型 集合类型出现在Object Inspector窗口中时正如一个集合,展开后,用         

户通过将集合元素设为True或False来选择。

对象类型 作为对象的属性本身有属性编辑器,如果对象有自己的published属

性,用户在Object Inspector中通过展开对象属性列,可以独立编辑它们,

对象类型的属性必须从TPersistent继承。

数组类型 数组属性必须有它们自己的属性编辑器,Object Inspector没有内嵌对数

组属性编辑的支持。  

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

3. 公布继承的属性

  所有部件都从祖先类型继承属性。当你从已有部件继承时,新部件将继承祖先类型的所有属性。如果你继承的是抽象类,则继承的属性是protected或public,但不是published。如想使用户访问protected或public属性,可以将该属性重定义为published。如果你使用TWinControl继承,它继承了Ctl3D属性,但是protected的,因此用户在设计和运行时不能访问Ctl3D,通过在新部件中将Ctl3D重声明为published,就改变了Ctl3D的访问级别。下面的代码演示如何将Ctl3D声明为published,使之在设计时可被访问。

 

  type

TSampleComponent=class(TWinControl)

published

property Ctl3D;

end;

 

4. 定义部件属性

  ⑴ 属性的声明

  声明部件的属性,你要描述:

 ● 属性名

  ● 属性的类型

  ● 读和设置属性值的方法

 

  至少,部件属性应当定义在部件对象声明的public部分,这样可以在运行时很方便地从外部访问;为了能在设计时编辑属性,应当将属性在published部分声明,这样属性能自动显示在Object Inspector窗口中。下面是典型的属性声明:

 

  type

TYourComponent=class(TComponent)



private

FCount: Integer { 内部存储域 }

function GetCount: Integer; { 读方法 }

procedure SetCount(ACount: Integer); { 写方法 }

pubilic

property Count: Integer read GetCount write SetCount;

end;

 

  ⑵ 内部数据存储

  关于如何存储属性的数据值,Delphi没有特别的规定,通常Delphi部件遵循下列规定:

 ● 属性数据存储在对象的数据域处

  ● 属性对象域的标识符以F开头,例如定义在TControl中的属性FWidth

  ● 属性数据的对象域应声明在private部分

 

  后代部件只应使用继承的属性自身,而不能直接访问内部的数据存储。

  ⑶ 直接访问

  使属性数据可用的最简单的办法是直接访问。属性声明的read 和write部分描述了怎样不通过调用访问方法来给内部数据域赋值。但一般都用read进行直接访问,而用write进行方法访问,以改变部件的状态。

  下面的部件声明演示了怎样在属性定义的read 和write部分都采用直接访问:

 

  type

TYourComponent=class(TComponent)



private { 内部存储是私有 }

FReadOnly: Boolean; { 声明保存属性值的域 }

published { 使属性在设计时可用 }

property ReadOnly: Boolean read FReadOnly write FReadOnly;

end;

 

  ⑷ 访问方法

  属性的声明语法允许属性声明的read和write部分用访问方法取代对象私有数据域。不管属性是如何实现它的read 和write部分,方法实现应当是private,后代部件只能使用继承的属性访问。

  ① 读方法

属性的读方法是不带参数的函数,并且返回同属性相同类型的值。通常读函数的名字是“Get”后加属性名,例如,属性Count的读方法是GetCount。不带参数的唯一例外是数组属性。如果你不定义read方法,则属性是只写的。

  ② 写方法

  属性的写方法总是只带一个参数的过程。参数可以是引用或值。通常过程名是"Set"加属性名。例如,属性Count的写方法名是SetCount。参数的值采用设置属性的新值,因此,写方法需要执行在内部存储数据中写的操作。

  如果没有声明写方法,那么属性是只读的。

  通常在设置新值前要检测新值是否与当前值不同。

  下面是一个简单的整数属性Count的写方法:

 

  procedure TMyComponent.SetCount( value: Integer);

begin

if value <>FCount then

begin

FCount := Value;

update;

end;

end;

 

⑸ 缺省属性值

  当声明一个属性,能有选择地声明属性的缺省值。部件属性的缺省值是部件构造方法中的属性值集。例如,当从Component Palette选择某部件置于窗体中时,Delphi通过调用部件构造方法创建部件,并决定部件属性初始值。

  Delphi使用声明缺省值决定是否将属性值存在DFM文件中。如果不描述缺省值,Delphi将总是保存该属性值。声明缺省值的方法是在属性声明后加default指令,再跟缺省值。

  当重声明一个属性时,能够描述没有缺省值的属性。如果继承的属性已有一个,则设立没有缺省值的属性的方法是在属性声明后加nodefault指令。如果是第一次声明属性,则没有必要加nodefault指令,因为没有default指令即表示如此。

  下例是名为IsTrue的布尔类型属性设置缺省值True的过程:

 

  type

TSampleComponent=class(TComponent)

private

FIsaTrue: Boolean;

pubilic

constructor Create (AOwner: TComponent); Overvide;

published

property Istrue: Boolean read FIsTrue write FIsTrue default True;

end;

 

constructor TSampleComponent.Create (AOwner: TComponent);

begin

inherited Create ( Aowner);

Fistvue := True; { 设置缺省值 }

end;

 

5. 编写属性编辑器

Object Inspector提供所有类型属性的缺省编辑器,Delphi也支持通过编写和注册属性编辑器的方法为属性设计自己的编辑器。可以注册专门为自定义部件的属性设计的编辑器,也可设计用于所有某类型的属性。编写属性编辑器需要下列五个步骤:

● 继承一个属性编辑器对象

 ● 将属性作为文本编辑

  ● 将属性作为整体编辑

  ● 描述编辑器属性

 ● 注册属性编辑器

 

  ⑴ 继承属性编辑器对象

  DsgnIntf库单元中定义了几种属性编辑器。它们都是从TPropertyEditor继承而来。当创建属性编辑器时,可以直接从TPropertyEditor中继承或从表中的任一属性编辑器中继承。

 

 

表19.4 属性编辑器的类型

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  类型 编辑的属性

─────────────────────────────────────

TOrdinalProperty    所有有序的属性(整数、字符、枚举)

TIntegerProperty    所有整型,包括子界类型

TCharProperty     字符类型或字符子集

TEnumProperty   任何枚举类型

TFloatProperty   所有浮点数

TStringProperty   字符串,包括定长的字符串

TSetElementProperty 集合中的独立元素

TSetElementProperty 所有的集合,并不是直接编辑集合类型,而是展开成一列

集合元素属性

TClassProperty 对象,显示对象名,并允许对象属性的展开

TMethodPropevty 方法指针,主要指事件

TComponentProperty 相同窗体中的部件,用户不能编辑部件的属性,

但能指向兼容的部件

TColorProperty    部件颜色,显示颜色常量,否则显示十六进制数

TFontNameProperty 字体名称

TFontProperty 字体,允许展开字体的属性或弹出字体对话框

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

 

 

下面是TFloatPropertyEditor的定义:

 

type

TFloatProperty=Class(TPropertyEditor)

public

function AllEqual: Boolean; override;

function GetValue: String; override;

procedure SetValue ( Const Value: string ); override;

end;

 

⑵ 象文本一样编辑属性

  所有的属性都需要将它们的值在Object Inspector窗口中以文本的方式显示。属性编辑器对象提供了文本表现和实际值之间转换的虚方法。这些虚方法是GetValue和SetValue,你的属性编辑器也能继承了一系列的方法用于读和写不同类型的值。见下表:

 

表19.5 读写属性值的方法

━━━━━━━━━━━━━━━━━━━━━━━━━━

属性类型   "Get"方法 "Set"方法

──────────────────────────

浮点数 GetFloatValue SetFloatVallue

方法指针 GetMethodValue SetMehodValue

有序类型 GetOrdValue SetOrdValue

字符串 GetStrValue SetStrValue

━━━━━━━━━━━━━━━━━━━━━━━━━━

 

当覆盖GetValue方法时,调用一个"Get"方法;当覆盖SetValue方法时调用一个"Set"方法。

  属性编辑器的GetValue方法返回一个字符串以表现当前属性值。缺省情况下GetValue返回"unknown"。

属性编辑器的SetValue接收Object Inspector窗口String类型的参数,并将其转换成合适的类型,并设置属性值。

  下面是TIntegerProperty的GetValue和SetValue的例子:

 

function TIntegerProperty GetValue: string;

begin

Result := IntToStr (GetOrdValue);

end;

 

proceduve TIntegerPropertySetValue (Const Value: string);

var

L: Longint;

begin

L := StrToInt(Value); { 将字符串转换为数学 }

with GetTypeData (GetPropType)^ do

if ( L < Minvalue ) or ( L > MaxValue ) then

Raise EPropertyError.Create (FmtloadStr(SOutOfRange,

[MinValue,MaxValue]));

SetOrdValue (L);

end;

 

⑶ 将属性作为一个整体来编辑

  Delphi支持提供用户以对话框的方式可视化地编辑属性。这种情况常用于对对象类型属性的编辑。一个典型的例子是Font属性,用户可以找开Font对话框来选择字体的属性。

  提供整体属性编辑对话框,要覆盖属性编辑对象的Edit方法。Edit方法也使用"Get"和"Set"方法。

 在大多数部件中使用的Color属性将标准的Windows颜色对话框作为属性编辑器。下面是TColorProperty的Edit方法

 

procedure TColorProperty.Edit

var

ColorDialog: TColorDialog;

begin

ColorDialog := TColorDialog.Create(Application); { 创建编辑器 }

try

ColorDialog.Color := GetOrdValue; { 使用已有的值 }

if ColorDialog.Execute then

  SetOrdValue (ColorDialog.Color);

finally

ColorDialog.Free;

end;

 end;

 

⑷ 描述编辑器的属性

  属性编辑必须告诉Object Inspector窗口如何采用合适的显示工具。例如Object Inspector窗口需要知道属性是否有子属性,或者是否能显示可能取值的列表。描述编辑器的属性通常覆盖属性编辑器的GetAttributes方法。

GetAttributes返回TPropertyAttributes类型的集合。集合中包括表中任何或所有的值:

 

表19.6 属性编辑器特征标志

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  标志 含 义 相关方法

   ──────────────────────────────

paValuelist 编辑器能给予一组枚举值 GetValues

paSubPropertie 属性有子属性 GetPropertises

paDialog 编辑器能显示编辑对话框 Edit

PaMultiSelect 当用户选择多于一个部件

时,属性应能显示 N/A

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

Color属性是灵活的,它允许在Object Inspector窗口中以多种方式选择他们。或者键入,或者从列表中选择定编辑器。因此TColorProperty的GetAttributes方法在返回值中包含多种属性。

 

  function TColorProperty.GetAttributes: TProrertyAttributes;

begin

Result := [PaMultiselect, paDialog, paValuelist];

end;

 

 ⑸ 注册属性编辑器

  一旦创建属性编辑器,必须在Delphi中注册。注册属性编辑器时,要与某种属性相联。

  调用RegisterPropertyEditor过程来注册属性编辑器。该过程接受四个参数:

  ● 要编辑的属性的类型信息的指针。这总是通过调用调用TypeInfo函数得到,如TypeInfo ( TMyComponent )

  ● 编辑器应用的部件类型,如果该参数为nil则编辑器应用于所给的类型的所有属性

  ● 属性名,该参数只有在前一参数描述了部件的情况下才可用

  ● 使用该属性编辑器的属性的类型

 

  下面引用了注册标准部件的过程:

 

procedure Register;

begin

RegisterPropertyEditor (TypeInfo(TComponent), nil, TComponentProperty,

RegisterPropertyEditor(TypeInfo(TComponentName), TComponent,

'Name', (ComponentNamePropety);

RegisterPropertyEditor (TypeInfo(TMenuItem), TMenu, '', TMenuItemProperty);

end;

 

  这三句表达式使用RegisterPropertyEditor三种不同的用法:

  ● 第一种最典型

它注册了用于所有TComponent类型属性的属性编辑器TComponentProperty。通常,当为某种类型属性注册属性编辑器时,它就能应用于所有这种类型的属性,因此,第二和第三个参数为nil。

  ● 第二个表达式注册特定类型的属性编辑器

它为特定部件的特定属性注册属性编辑器,在这种情况下,编辑器用于所有部件的Name属性。

 ● 第三个表达式介于第一个和第二个表达式之间

它为部件TMenu的TMenuItem类型的所有属性注册了属性编辑器。

 

19.2.2.2 创建事件

 

  事件是部件的很重要的部分。事件是部件必须响应的系统事件与响应事件的一段代码的联接。响应代码被称为事件处理过程,它总是由部件用户来编写。通过使用事件,应用开发者不需要改变部件本身就能定制部件的行为。作为部件编写者,运用事件能使应用发者定制所有的标准Delphi部件。要创建事件,应当理解:

  ● 什么是事件

  ● 怎样实现标准事件

  ● 怎样定义自己的事件

 

1. 什么是事件

事件是联接发生的事情与某些代码的机制,或者说是方法指针,一个指向特定对象实例的特定方法的指针。从部件用户的角度,事件是与系统事件(如OnClick)有关的名称,用户能给该事件赋特定的方法供调用。例如,按钮Buttonl有OnClick方法,缺省情况下Delphi在包含该按钮的窗体中产生一个为ButtonlClick的方法,并将其赋给OnClick。当一个Click事件发生在按钮上时,按钮调用赋给OnClick的方法ButtonlClick:

部件用户将事件看作是由用户编写的代码,而事件发生时由系统调用的处理办法。

  从部件编写者角度事件有更多的含义。最重要的是提供了一个让用户编写代码响应特定事情的场所。

  要编写一个事件,应当理解:

 ● 事件和方法指针

  ● 事件是属性

  ● 事件处理过程类型

 ● 事件处理过程是可选的

 

  ⑴ 事件是方法指针

  Delphi使用方法指针实现事件。一个方法指针是指向特定对象实例的特定方法的特定指针。作为部件编写者,能将方法指针作为一种容器。你的代码一发现事情发生,就调用由用户定义的方法。  

  方法指针的工作方式就象其它的过程类型,但它们保持一个隐含的指向对象实例的指针。所有的控制都继承了一个名为Click的方法,以处理Click事件。Click方法调用用户的Click事件处理过程。

 

  procedure TControl.Click;

begin

if Assigned(OnClick ) then OnClick( Self );

end;

 

如果用户给Control的OnClick事件赋了处理过程(Handle),那鼠标点按Control时将导致方法被调用。

  ⑵ 事件是属性

  部件采用属性的形式实现事件。不象大多数其它属性,事件不使用方法来使实现read和write部分。事件属性使用了相同类型的私有对象域作为属性。按约定域名在属性名前加“F”。例如OnClick方法的指针,存在TNotifyEvent类型FOnClick域中。OnClick事件属性的声明如下:

 

type

TControl=class ( TComponent )

private

FOnClick: TNofiFyEvent; { 声明保存方法指针的域 }

protected

property OnClick: TNotifyEvent read FOnClick write FOnClick;

end;

 

 象其它类型的属性一样,你能在运行时设置和改变事件的值。将事件做成属性的主要好处是部件用户能在设计时使用Object Inspector设置事件处理过程。

  ⑶ 事件处理过程类型

  因为一个事件是指向事件处理过程的指针,因此事件属性必须是方法指针类型,被用作事件处理过程的代码,必须是相应的对象的方法。

  所有的事件方法都是过程。为了与所给类型的事件兼容,一个事件处理过程必须有相同数目和相同类型的相同顺序的参数。Delphi定义了所有标准事件处理过程的方法类型,当你创建自己的事件时,你能使用已有的事件类型,或创建新的。虽然不能用函数做事件处理过程,但可以用var参数得到返回信息。

  在事件处理过程中传递var参数的典型例子是TKeyPressEvent类型的KeyPressed事件。TKeyPressEvent定义中含有两个参数。一个指示哪个对象产生该事件。另一个指示那个键按下:

 

  type

TKeyPressEvent=procedure( Sender: TObject; var key: char) of Object;

 

通常key参数包含用户按下键的字符。在某些情况下,部件的用户可能想改变字符值。例如在编辑器中强制所有字符为大写,在这种情况下,用户能定义下列的事件处理过程:

 

 procedure TForml.EditlKeyPressed( Sender: TObject; var key: char);

begin

key := Upcase( key );

end;

 

也可使用var参数让用户覆盖缺省的处理。

  ⑷ 事件处理过程是可选的

  在为部件创建事件时要记住部件用户可能并不编写该事件的处理过程。这意味着你的部件不能因为部件用户没有编写处理代码而出错。这种事件处理过程的可选性有两个方面:

  ① 部件用户并非不得不处理事件

事件总是不断地发生在Windows应用程序中。例如,在部件上方移动鼠标就引起Windows发送大量的Mouse-Move消息给部件,部件将鼠标消息传给OnMouseMove事件。在大多数情况下,部件用户不需要关心MouseMove事件,这不会产生问题,因为部件不依赖鼠标事件的处理过程。同样,自定义部件也不能依赖用户的事件处理过程。

  ② 部件用户能在事件处理过程写任意的代码

  一般说来,对用户在事件处理过程中的代码没有限制。Delphi部件库的部件都支持这种方式以使所写代码产生错误的可能性最小。显然,不能防止用户代码出现逻辑错误。

  2. 怎样实现标准事件

  Delphi带的所有控制继承了大多数Windows事件,这些就是标准事件。尽管所有这些事件都嵌在标准控制中,但它们缺省是protected,这意味着用户无法访问它们,当创建控制时,则可选择这些事件使用户可用。将这些标准事件嵌入自定义控制需要考虑如下:

  ● 什么是标准事件

  ● 怎样使事件可见

  ● 怎样修改标准事件处理过程

 

  ⑴ 什么是标准事件

  有两种标准事件:用于所有控制和只用于标准Windows控制。

  最基本的事件都定义在对象TControl中。窗口控制、图形控制和自定义控制都继承了这些事件,下面列出用于所有控制的事件:

  OnClick OnDragDrop OnEndDrag OnMouseMove

 OnDblClick OnDragOver OnMouseDown OnMouseUp

 

  所有标准事件在TControl中都定义了相应的protected动态方法,只是没有加“On”例如OnClick事件调用名为Click的方法。

  标准控制(从TWinControl继承)具有下列事件:

 OnEnter OnKeyDown OnkeyPress OnKeyUp OnExit

 

正如TControl中的标准事件,窗口控制也有相应protected动态方法。

  ⑵ 怎样使事件可见

  标准事件的声明是protected,如果想使用户在运行时或设计时能访问它们,就需要将它们重声明为public和 published。重声明属性而不描述它的实现将继承相同的实现方法,只是改变了访问级别。例如,创建一个部件并使它的OnClick事件出现在运行时,你可增加下面的部件声明:

 

  type

TMyControl=class(TCustomControl)

published

property OnClick; { 使OnClick在objectinspector中可见 }

end;

 

⑶ 怎样修改标准事件处理过程

  如果想修改自定义部件响应某种事件的方法,可以重写代码并将其赋给事件。将联接每个标准事件的方法声明的protected是出于慎密的考虑。通过,覆盖实现方法,能修改内部事件处理过程,通过调用继承的方法,能保持标准事件处理过程。

  调用继承的方法的顺序是很重要的。一般首先调用继承的方法,允许用户的事件处理过程代码在你的定制代码前执行。然而也有在调用继承的方法之前执行自己的代码情况出现。

  下面是一个覆盖Click事件的例子:

 

procedure TMyControl.Click;

begin

inherited Click; { 执行标准处理,包括调用事件处理过程你自己的定制代码 }

end;

 

3. 定义自己的事件

  定义全新的事件的情况是很少见的。只有当部件的行为完全不同于任何其它事件才需要定义新事件。定义新事件一般包含三个步骤:

  ● 触发事件

   ● 定义处理过程类型

   ● 声明事件

  ● 调用事件

 

⑴ 触发事件

  定义自己的事件要遇到的第一个关键是:当使用标准事件时你不需要考虑由什么触发事件。对某些事件,问题是显然的。例如:一个MouseDown事件是在用户按下鼠标的左键时发生,Windows给应用发送WM_LBUTTONDOWN消息。接到消息后,一个部件调用它的MouseDown方法,它依次调用用户的OnMouseDown事件处理过程代码。但是有些事件却不是那么可以描述清楚的。例如:滚行杠有一个OnChange事件,可被各种情况触发,包括按键、鼠标点按或其它按制中的改变。当定义事件时,你必须使各种情况的发生调用正确的事件。

  这里有TControl处理WM_LBUTTONDOWN消息的方法,DoMouseDown是私有的实现方法,它提供了一般的处理左、右和中按钮的方法,并将Windows消息的参数转换为MouseDown方法的值。

 

type

TControl = class(TComponent)

private

FOnMouseDown: TMouseEvent;

procedure DoMouseDown(var Message: TWMMouse; Button: TMouseButton;

Shift: TShiftState);

procedure WMLButtonDown(var Message: TWMLButtonDown);

message M_LBUTTONDOWN;

protected

procedure MouseDown(Button: TMouseButton; Shift: TShiftState;

X, Y: Integer); dynamic;

end;

 

procedure TControl.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);

begin

if Assigned(FOnMouseDown) then

FOnMouseDown(Self, Button, Shift, X, Y); { 调用事件处理过程 }

end;

 

procedure TControl.DoMouseDown(var Message: TWMMouse; Button: TMouseButton;

Shift: ShiftState);

begin

with Message do

MouseDown(Button, KeysToShiftState(Keys) + Shift, XPos, YPos); { 调用动态方法 }

end;

 

procedure TControl.WMLButtonDown(var Message: TWMLButtonDown);

begin

inherited; { perform default handling }

if csCaptureMouse in ControlStyle then

MouseCapture := True;

if csClickEvents in ControlStyle then

Include(FControlState, csClicked);

DoMouseDown(Message, mbLeft, []); { 调用常规的mouse-down 方法 }

end;

 

  当两种事情-状态变化和用户交互—发生时,处理机制是相同的,但过程稍微不同。用户交互事件将总是由Windows消息触发。状态改变事件也与Windows消息有关,但它们也可由属性变化或其它代码产生。你拥有对自定义事件触发的完全控制。

⑵ 定义处理过程类型

  一旦你决定产生事件,就要定义事件如何被处理,这就是要决定事件处理过程的类型。在大多数情况下,定义的事件处理过程的类型是简单的通知类型(TNotifyEvent)和已定义的事件类型。

  通知事件只是告诉你特定的事件发生了,而没有描述什么时候和什么地方。通知事件使用时只带一个TObject类型的参数,该参数是Sender。然而所有通知事件的处理过程都知道是什么样的事件发生和发生在那个部件。例如:Click事件是通知类型。当编写Click事件的处理过程时,你知道的是Click事件发生和哪个部件被点按了。通知事件是单向过程。没有提供反馈机制。

  在某些情况下,只知道什么事件发生和发生在那个部件是不够的。如果按键事件发生,事件处理过程往往要知道用户按了哪个键。在这种情况下,需要事件处理过程包含有关事件的必要信息的参数。如果事件产生是为了响应消息,那么传递给事件的参数最好是直接来自消息参数。

  因为所有事件处理过程都是过程,所以从事件处理过程中返回信息的唯一方法是通过var参数。自定义部件可以用这些信息决定在用户事件处理过程执行后是否和怎样处理事件。

  例如,所有的击键事件(OnKeyDown、OnKeyUp和OnKeyPressed)通过名为key的var参数传递键值。为了使应用程序看见包含在事件中的不同的键,事件处理过程可以改变key变量值。

 

⑶ 声明事件

  一旦你决定了事件处理过程的类型,你就要准备声明事件的方法指针和属性。为了让用户易于理解事件的功能,应当给事件一个有意义的名字,而且还要与部件中相似的属性的名称保持一致。

  Delphi中所有标准事件的名称都以“On”开头。这只是出于方便,编译器并不强制它。Object Inspector是看属性类型来决定属性是否是事件,所有的方法指针属性都被看作事件,并出现在事件页中。

⑷ 调用事件

  一般说来,最好将调用集中在事件上。就是说在部件中创建一个虚方法来调用用户的事件处理过程和提供任何缺省处理。当调用事件时,应考虑以下两点:

  ● 必须允许空事件

● 用户能覆盖缺省处理

 

不能允许使空事件处理过程产生错误的情况出现。就是说,自定义部件的正常功能不能依赖来自用户事件处理过程的响应。实际上,空事件处理过程应当产生与无事件处理过程一样的结果。

  部件不应当要求用户以特殊方式使用它们。既然一个空事件处理过程应当与无事件处理过程一样动作,那么调用用户事件处理过程的代码应当象这样:

 

  if Assigned(OnClick) then OnClick(Self);

{ 执行缺省处理 }

 

而不应该有这样的代码:

 

if Assigned(OnClick) then

OnClick(Self)

else

…; { 执行缺省处理 }

 

  对于某些种类的事件,用户可能想取代缺省处理甚至删除所有的响应。为支持用户实现这种功能,你需要传递var参数给事件处理过程,并在事件处理过程返回时检测某个值。空事件处理过程与无事件处理过程有相同作用。因为空事件处理过程不会改变任何var参数值。所以缺省处理总是在调用空事件处理过程后发生。

  例如在处理Key-Press事件,用户可以通过将var参数key的值设置为空字符(#0)来压制部件的缺省处理,代码如下:

 

  if Assigned(OnkeyPress) then OnkeyPress(Self key);

if key <> #0 then { 执行缺省处理 } ;

 

实际的代码将与这稍有不同,因为它只处理窗口消息,但处理逻辑是相同的。在缺省情况下,部件先调用任何用户赋予的事件处理过程,然后执行标准处理。如果用户的事件处理过程将key设为空,则部件跳过缺省处理。

 

19.2.2.3 处理消息

 

  在传统Windows编程中,一个很关键的方面是处理Windows发送给应用程序的消息。Delphi已经帮你处理了大多数的普通消息,但是在创建部件的过程中有可能Delphi没有处理方法,得由自己处理消息,也可能创建了新的消息需要处理它们。

  学习掌握Delphi的消息处理,要掌握以下三个方面:

 ● 理解消息处理系统

● 修改(改变)消息处理方法

● 建立新的消息处理方法

 

1. 理解消息处理系统

  所有的Delphi对象内部具有处理消息的机制,如调用消息处理方法或消息处理过程。消息处理的基本思想是对象接收某种消息并派送它们,这是通过调用与接收的消息相应的方法来实现的,如果没有相应于消息的指定的方法,那就调用缺省处理。下面的图解表示消息派送系统:

Delphi部件库定义了将所有Windows消息(包括用户自定义消息)直接转换到对象方法调用的消息派送系统。一般没有必要改变这种消息派送系统,只要建立消息处理方法。

  ⑴Windows消息中有什么?

Windows消息是包含若干有用的域的数据记录。记录中最重要的是一个整型大小的值,该值标识消息。Windows定义了大量的消息。库单元Messages声明了所有消息的标识。消息中其它的有用信息包括两个域参数和结果域。两个参数分别是16位和32位的。Windows代码总是以wParam和lParam来引用它们。

  最初,Windows程序员不得不记住包含的每一个参数。现在,微软公司已经命名了这参数。这样理解伴随这些消息的信息就更简单了。例如,WM_KEYDOWN消息的参数被称为vkey和keydata,这就比wParam和lParam给出了更多的描述信息。

  Delphi为不同类型的消息定义了指定的记录类型。如鼠标消息在long参数中传递鼠标事件的x、y座标,一个在高字,一个在低字。使用鼠标消息记录,你不需要自己关心哪个字是哪个座标,因为引用这些参数时通过名子Xpos和Ypos取代了lParamLo和lParamHi。

  ⑵ 派送方法

  当应用程序创建窗口时,在Windows Kernel中注册了一个窗口过程。窗口过程是处理窗口消息的函数。传统上,窗口过程包括了Case表达式,表达式的每个入口是窗口要处理的每一条消息。当你每次创建窗口时,必须建立完整的窗口过程。

  Delphi在下列三方面简化了消息派送:

  ● 每个部件继承了完整的消息派送系统

● 派送系统具有缺省处理。用户只需定义想响应的消息的处理方法

● 可以修改消息处理的一部分,依靠继承的方法完成大多数处理

 

这种消息派送系统的最大优点是用户能在任何时候安全地发送任何消息给任何部件。如果部件没有为该消息定义处理方法,那缺省处理方法会解决这个问题,通常是忽略它。

  Delphi为应用程序每种类型的部件注册了名为MainWndProc的方法作为窗口过程。MainWndProc包含了异常处理块,它完成从Windows到名为WndProc的虚方法传送消息记录,并且通过调用应用程序对象的HandleException方法处理异常。

  MainWndProc是静态方法,没有包含任何消息的指定处理方法。定制过程发生在WndProc中,因为每个部件类型都能覆盖该方法以适合特定的需要。

  WndProc方法为每个影响它们处理的任何条件进行检查,以捕捉不要的消息。例如,当被拖动时,部件忽略键盘事件,因此,TWinControl的WndProc只在没有拖动时传送键盘事件。最后WndProc调用Dispatch方法,该方法是从TObject继承来的静态方法,决定什么方法来处理消息。

  Dispatch使用消息记录的Msg域来决定怎样派送特定消息。如果部件已经给该消息定义了处理方法,则Dispatch调用该方法,反之,Dispatch调用缺省处理方法。

  2. 改变消息处理方法

  在改变自定义部件的消息处理方法之前,先要弄清楚你真正想要做什么。Delphi将大多数的Windows消息转换成部件编写者和部件用户都能处理的事件。一般来说,你应当改变事件处理行为而不是改变消息处理行为。

为了改变消息处理行为,要覆盖消息处理方法。也能提供捕获消息防止部件处理该消息。

⑴ 覆盖处理方法

为了改变部件处理特定消息的方法,要覆盖那个消息的处理方法。如果部件不处理该消息,你就需要声明新的消息处理方法。

为了覆盖消息处理方法,要在部件中以相同的消息索引声明新的方法。不要使用override指令,你必须使用Message指令和相应的消息索引。

  例如,为了覆盖一个处理WM_PAINT消息的方法,你要重声明WMPaint方法:

 

type

TMyComponent=class(…)

procedure WMPaint(var Message: TWMPaint); message WM_PAINT;

end;

 

⑵ 使用消息参数

  在消息处理方法内部,自定义部件访问消息记录的所有参数。因为消息总是var参数,如果需要的话,事件处理过程可以改变参数的值。Result域是经常改变的参数。Result是Windows文档中所指的消息的返回值:由SendMessage返回。

  因为消息处理方法的消息参数的类型随着被处理的消息的变化而变化,所以应当参考Windows消息文档中的参数的名字和含义。如果出于某种原因要使用旧风格的消息参数(wParam、lParam),可以配合通用类型TMessage来决定Message。

  ⑶ 捕获消息

  在某种情况下,你可能希望自定义部件能忽略某种消息。就是说,阻止部件将该消息派送给它的处理方法。为了那样来捕获消息,可以覆盖虚方法WndProc。

  WndProc方法在将消息传给Dispatch方法前屏蔽该消息。它依次决定哪一个方法来处理消息。通过覆盖WndProc,部件得到了派送消息之前过滤它们的机会。

  通常,象下面这样覆盖WndProc:

 

procedure TMyControl.WndProc(var Message: TMessage);

begin

{ 决定是否继续处理过程 }

inherited WndProc (Message);

end;

 

下面的代码是TControl的WndProc的一部分。TControl定义整个范围内的鼠标消息,当用户拖动和放置控制时,它们将被滤过。

 

  procedure TControl WndProc(var Message:TMessage);

begin

if (Message.Msg >= WM_MOVSEFIRST) and

(Message.Msg <= WM_MOUSELAST) then

if Dragging then

DragMouseMsg(TWMMOUSE(Message)) { 处理拖动 }

else

…   { 正常处理其它 }

…   { 否则正常处理 }

end;


想死你们了!

TOP

DELPHI基础教程

第十九章 Delphi自定义部件开发(三)
3. 创建新的消息处理方法

因为Delphi只为大多数普通Windows消息提供了处理方法,所以当你定义自己的消息时,就要创建新的消息处理方法。

 用户自定义消息的过程包括两个方面:

  ● 定义自己的消息

● 声明新的消息处理方法

 

⑴ 定义自己的消息

许多标准部件为了内部使用定义了消息。定义消息的最一般的动因是广播信息和状态改变的通知。

  定义消息过程分两步:

  ● 声明消息标识符

● 声明消息记录类型

 

① 声明消息标识

消息标识是整型大小的常量。Windows保存了小于1024的消息用于自己使用,因此当声明自己的消息时,你应当大于1024。

  常量WM_USER代表用于自定义消息的开始数字。当定义消息标准时,你应当基于WM_USER。

  某些标准Windows控制使用用户自定义范围的消息,包括ListBox、ComboBox、EditBox和Button。如果从上述部件中继承了一个部件,在定义新的消息时,应当检查一下Message单元是否有消息用于该控制。

  定义消息的方法如下:

 

  Const

WM_MYFIRSTMESSAGE=WM_USER+0;

WM_MYSECONDMESSAGE=WM_USER+1;

 

② 声明消息记录类型

  如果你想给予自定义消息的参数有含义的名字,就要为该消息声明消息记录类型。消息记录是传给消息处理方法的参数的类型。如果不使用消息参数或者想使用旧风格参数,可以使用缺省的消息记录。

  声明消息记录类型要遵循下列规则

● 以消息名命名消息记录类型,以T打头

● 将记录中第一个域命名为Msg,类型为TMsgPraram

● 将接着的两个字节定义为word 以响应word大小的参数

● 将接着的四个字节与long参数匹配

● 将最后的域命名为Result,类型为Longint

 

下面是TWMMouse的定义

 

type

TWMMouse=record

Msg: TMsgParam; { 第一个是消息ID }

Keys: Word; { wParam }

case Integer of { 定义lParam的两种方式 }

o: (

Xpos: Integer; { 或者以x,y座标 }

Ypos: Integer);

1: (

Pos : TPoint; { 或者作为单个点 }

Result: Longint; ) { 最后是Result域 }

end;

 

TWMMouse使用变长记录定义了相同参数的不同名字集。

  ⑵ 声明新的消息处理方法

  有两类环境需要你定义新的消息处理方法:

  ● 自定义新部件需要处理没有被标准部件处理的Windows消息

● 已定义了自定义部件使用的新消息

 

声明消息处理方法的办法如下:

● 在部件声明中的protected部分声明方法

● 将方法做成过程

● 以要处理的消息名命名方法 但不带下划线

● 传递一个命名为Message的var参数,类型为消息记录类型

● 编写用于该部件的特别处理代码

● 调用继承的消息方法

 

下面是用于用户自定义消息CM_CHANGECOLOR的消息处理代码:

 

type

TMyComponent=class(TControl)



protected

procedure CMChangeColor(var Message:TMessage);

message CM_CHANGECOLOR;

end:

 

procedure TMyComponent.CMChangeColor(var Message: TMessage);

begin

color := Message lParam;

inherited;

end;

 

19.2.2.4 注册部件

 

  编写部件及其属性、方法和事件只是部件创建过程的一部分。尽管部件具有这些特征就可用,但部件真正功能强大的是在设计时操作它们的能力。

  使部件在设计时可用需要经过如下几步:

  ● 用Delphi注册部件

● 增加选择板位图

● 提供有关属性和事件的帮助

● 存贮和读取属性

 

1. 用Delphi注册部件

为了让Delphi识别自定义部件,并将它们放置于Component Palette上,你必须注册每一个部件。

注册一个部件要在部件所在单元里加入Register方法,这包括两个方面的内容:

● 声明注册过程

● 实现注册过程

 

一旦安装了注册过程,就可以将部件安装在选择板上。

  注册过程要在部件所在单元中写一个过程,该过程必须以Register命名。Register必须出现在库单元的interface部分,这样Delphi就能定位它。在Register过程中,可以为每个部件调用过程RegisterComponents。

下面的代码演示了建立和注册部件的概略方法:

 

unit MyBtns;

 

interface

 

type

… { 声明自定义部件 }

procedure Register;

 

Implementation

 

procedure Register;

begin

… { 注册部件 }

end;

 

end.

 

在Register过程中,必须注册每一个要加入Component Palette的部件,如果库单元包含若干部件,就要将它们一次性注册。

  注册一个部件时,为部件调用RegisterComponents过程。RegisterComponents告诉Delphi两件有关所注册的部件的事::

● 要注册部件所在的Component Palette的页名

● 要安装的部件的名字

 

选择板的页名是个字符串。如果你所给名字的页不存在,Delphi就用该名字创建新的页。

下面的Register过程注册了一个名为TMyComponent的部件,并将其放在名为“Miscellaneous”的Component Palette页上。

 

procedure Register;

begin

RegisterComponents('Miscellaneous', [TFirst, TSecond]);

end;

 

也可以在相同的页上,或者在不同的页上,一次注册多个部件:

 

procedure Register;

begin

RegisterComponents('Miscellaneous', [TFirst, TSecond]);

RegisterComponents('Assorted', [TThird]);

end;

 

2. 增加Component Palette上的位图

每个部件都需要一个位图来在Component Palette上代表它。如果安装时没有描述自己的位图,则Delphi会自动套用缺省位图。

  因为选择板位图只有在设计时需要,所以没有必要将它们编译进库单元。而是将它们提供在与库单名相同的Windows资源文件中,扩展名为.DCR。用Delphi的位图编辑器来生成资源文件,每个位图边长24个象素。

  为每个要安装的库单元提供一个选择板位图文件,在每个文件中为每个要注册的部件提供一个位图。位图图象名与部件名相同,将文件放在与库单元相同的目录中,这样在安装部件时Dephi就能发现位图。

  例如,如果你在ToolBox单元中创建一个名为TMyControl的部件,就需要建立名为TOOLBOX.DCR的资源文件,文件中包含名为TMyControl的位图。

  3. 提供有关属性和事件的帮助

当在窗体中选择一个部件或在Object Inspector中选择事件或属性时,能够按F1得到有关这一项的帮助。如果创建了相应的Help文件的话,自定义部件的用户能得到有关你的部件的相应的文档。

  因为Delph使用了特殊的Help引擎支持跨多个Help文件处理主题搜索,所以你能提供关于自定义部件的小的Help文件,用户不需要额外的步骤就能找到你的文档。你的Help成了Delphi Help系统的一部分。

  要给用户提供帮助,要理解下列两方面:

● Delphi怎样处理HELP请求

● 将HELP插入Delphi

 

⑴ Delphi怎样处理HELP请求

Delphi基于关键词查询HELP请求。就是说,当用户在窗体设计窗口的已选部件上按F1键时,Delpdi将部件的名字转换成一个关键词,然后调用Windows Help引擎查找那个关键词的帮助主题。关键词是Windows Help系统的标准部分。实际上 ,WinHelp使用Help中的关键词产生Search对话框中的列表。因为用于上下文敏感搜索中的关键词不是实际供用户读的,所以要输入关键词的替代词。

例如,一个查找名为TSomething的部件的详细信息的用户可能打开WinHelp的Search对话框并输入TSomething。但不会使用用于窗体设计窗口的上下文查找的替代形式class-TSomething。因此,这个特殊的关键词Class-TSomething对用户是不可见的,以免弄乱了搜索列表。

  ⑵ 将Help插入Delphi

Delphi提供了创建和插入Windows Help文件的工具,包括Windows Help编译器HC.EXE。为自定义部件建立Help文件的机制与建立任何Help文件没什么不同,但需要遵循一些约定以与库中其它Help兼容。

  保持兼容性的方法如下:

  ● 建立Help文件

● 增加特殊的注脚

● 建立关键词文件

● 插入Help索引

 

当你为自定义部件建立完Help,有下列几个文件:

● 编译过的Help(.HLP)文件

● Help关键词(.KWF)文件

● 一个或多个Help源文件(.RTF)

● Help工程文件(.HLJ)

 

编译过的Help文件和关键词文件应当与库单元在同一目录。

  ① 建立Help文件

你可以使用任何的工具创建Windows Help文件。Delphi的多文件搜索引擎,可以包含任何数目的Help文件的要素。在编译的Help文件之外,你应当拥有RTF源文件,这样才能生成关键词文件。

  为使自定义部件的Help同库中其它部件一起工作,要遵循下列约定:

  ● 每个部件有占一页的帮助

部件帮助页应当给出部件目的的简单描述,然后列出最终用户可用的属性、事件和方法的描述。应用开发者通过在窗体上选择部件并按F1访问这一页。

  部件帮助页应当有一个用于关键词搜索的“K”脚注,脚注中包含部件名。例如,TMemo的关键词脚注读作"TMemo Component"

● 部件增加和修改的每一个属性,事件和方法应当有一页帮助

  属性、事件或方法的帮助页应当指出该项用于哪个部件,显示声明语法和描述它的使用方法。

  属性、事件或方法的帮助页应当有一个用于关键词搜索的“K”脚注,该脚注中包含该项的名字和种类。例如,属性Top的关键词脚注为“Top property”。

  Help文件的每一页也需要用于多文件索引搜索的特殊脚注。

  ② 增加特殊脚注

Delphi需要特殊的搜索关键词以区别用于部件的帮助页和其它项目。你应当为每一项提供标准的关键词搜索项。但你也需要用于Delphi的特殊脚注。

  要为来自Object Inspector窗口或代码编辑器F1的搜索增加关键词,就得为Help文件帮助页增加"B"脚注。

  “B”脚注与用于标准WinHelp关键词搜索的“K”脚注很相象,但它们只用于Delphi搜索引擎。下表列出怎样为每种部件帮助页建立“B”脚注:

 

表19.7 部件帮助页搜索注脚

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

帮助页类型 "B"脚注内容 示 例

   ──────────────────────────────────

主部件页 'class_'+部件类型名 class_TMemd

一般属性或事件页 'prop_'+属性名 prop_WordWrap

'event_'+事件名 event_OnChange

部件特有的属性 'prop_'+部件类型名 prop_TMemoWordWrap

或事件页 +属性名

'event_'+部件类型名 event_TMemoOnChange

+事件名

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

  区别一般帮助页和部件特有的帮助页是很重要的。一般帮助页应用于所有部件上的特定属性和事件。例如Left属性是所有部件中的标识。因此,它用字符串Prop-Left进行搜索。而Borde-style依赖于所属的部件,因此,BorderStyle属性拥有自己的帮助页。例如,TEdit有BorderStyle属性的帮助页,搜索字符串为Prop_TEditBorderStyle。

  ③ 建立关键词文件

  建立和编译了Help文件,并且增加了脚注之后,还要生成独立的关键词文件,这样Delphi才能将它们插入主题搜索的索引。

  从Help资源文件RTF创建关键词文件的方法如下:

● 在DOS提示行下,进入包含RTF文件的目录

● 运行关键词文件产生程序——KWGEN.EXE,后跟Help工程文件,如KWGEN SPECIAL.HPJ。当KWGEN运行完毕后,就有了与Help工程文件相同的关键词文件,但以.KWF为扩展名

● 将关键词文件放在编译完的库单元和Help文件相同的目录

当你在Component Palette上安装部件时,希望关键词插入Delphi Help系统的搜索索引。

 

④ 插入Help索引

以自定义部件建立关键词文件后,要将关键词插入Delphi的Help索引。

  将关键词文件插入Detphi Help索引的方法如下:

● 将关键词文件放在与编译完的库单元和Heph文件相同的目录中

● 运行HELPINST程序

 

HELPINST运行完后,Delphi的Help索引文件(.HDX)包含自定义部件帮助页的关键词。

⑶ 存储和装入属性

Delphi将窗体及其拥有的部件存储在窗体文件(.DFM)中,DFM文件用二进制表示窗体的属性和它的部件。当Delphi用户将自定义部件加入窗体中时,自定义部件应当具有存储它们的属性的能力。同样,当被调入Delphi或应用程序时,部件必须能从DFM文件中恢复它们。

  在大多数时候,不需要做任何使部件读写DFM文件的事。存储和装入都是继承的祖先部件的行为的一部分。然而在某些情况下,你可能想改变部件存储和装入时初始化的方法。因此,应当理解下述的机制:

● 存储和装入机制

● 描述缺省值

● 决定存储什么

● 装入后的初始化

 

① 存储和装入机制

当应用开发者设计窗体时,Delphi将窗体的描述存储在DFM文件中。当用户运行程序时,它读取这些描述。

  窗体的描述包含了一系列的窗体属性和窗体中部件的相似描述。每一个部件,包括窗体本身,负责存储和装入自身的描述。

  在缺省情况下,当存储时,部件将所有public和published属性的不同于缺省值的值以声明的顺序写入。当装入时,部件首先构造自己,并将所有属性设为缺省值;然后,读存储的、非缺省的属性值。

  这种缺省机制,满足了大多数部件的需要,而又不需部件编写者的任何工作。然而自己定义存储和装入过程以适合自定义部件需要的方法也有几种。

  ② 描述缺省值。

  Delphi部件只存储那些属性值不同于缺省值的属性。如果你不描述,Delphi假设属性没有缺省值,这意味着部件总是存储属性。

  一个属性的值没被构造函数设置,则被假设为零值。为了描述一个缺省值,在属性声明后面加default指令和新的缺省值。

  你也能在重声明属性时描述缺省值。实际上,重声明属性的一个原因是指定不同的缺省值。只描述缺省值,那么在对象创建时并不会自动地给属性赋值,还需要在部件的Create方法中赋所需的值。

  下面的代码用Align属性演示了描述缺省值的过程.

 

type

TStatusBar=class(TPanel)

public

constructor Create(Aowner: TComponent); override; { 覆盖以设置新值 }

published

property Align default alBottom; { 重新声明缺省值 }

end;

 

constructor TStatusBar.Create(Aowner: TComponent);

begin

inherited Create(Aowner); { 执行继承的初始化过程 }

Align := alBottom; { 为Align赋新的缺省值 }

end;

 

③ 决定存储什么

用户也可以控制Delphi是否存储部件的每一个属性。缺省情况下,在对象的published部分声明的所有属性都被存储。然而,可以选择不存储所给的属性,或者设计一个函数在运行时决定是否存储属性。

  控制Delphi是否存储属性的方法是在属性声明后面加stored指令,后跟True或False,或者是布尔方法名。你可以给任何属性的声明或重声明加stored表达式。下面的代码显示了部件声明三种新属性。一个属性是总是要存储,一个是不存,第三个则决定于布尔方法的值:

 

type

TSampleCompiment = class(TComponent)

protected

function storeIt: Boolean;

public { 正常情况下在不存 }

property Important: Integer stored True; { 总是存储 }

published { 正常情况下保存 }

property UnImportant: Integer stored False; { 不存 }

property Sometimes: Integer stored StoreIt; { 存储依赖于函数值 }

end;

 

④ 载入后的初始化

在部件从存储的描述中读取所有的属性后,它调用名为Loaded的虚方法,这提供了按需要执行任何初始化的机会。调用Loaded是在窗体和它的控制显示之前,因此,不需要担心初始化会带来屏幕闪烁。

  在部件载入属性时初始化它,要覆盖Loaded方法。

  在Loaded方法中,要做的第一件事是调用继承的Loaded方法。这使得在你的部件执行初始化之前,任何继承的属性都已初始化。

  下面的代码来自于TDatabase部件。在装入后,TDatabase试图重建在它存储时已打开的连接,并描述在连接发生异常时如何处理。

 

  procedure TDatabase.Loaded

begin

inherited Loaded; { 总是先调用继承的方法 }

Modified; { 设置内部标志 }

try

if FStreamedConnected then Open; { 重建联接 }

except

if csDesigning in ComponentState then { 在设计时 }

Application.HandleException(self) { 让Delphi处理异常 }

else raise; { 否 则 }

end;

end;

 

 

19.3 Delphi部件编程实例

 

19.3.1 创建数据库相关的日历控制-TDBCalendar

 

  当处理数据库联接时,将控制和数据直接相联是很重要的。就是说,应用程序可以建立控制与数据库之间的链。Delphi包括了数据相关的标签、编辑框、列表框和栅格。用户可以使自己的控制与数据相关。

  数据相关有若干等级。最简单的是只读数据相关或数据浏览,以及反映数据库当前状态的能力。比较复杂的是数据相关的编辑,也即用户可以在控制上操作数据库中的数据。

  在本部分中将示例最简单的情况,即创建联接数据库的单个字段的只读控制。本例中将使用Component Palette的Samples页中的TCalendar部件。

创建数据相关的日历控制包括下列几步:

● 创建和注册部件

● 使控制只读

● 增加数据联接(Data Link)

● 响应数据改变

 

19.3.1. 1创建和注册部件

 

每个部件的创建都从相同的方式开始,在本例中将遵循下列过程:

● 将部件库单元命名为DBCal

● 从TCalendar继承一个新部件,名为TDBCalendar

● 在Component Palette的Samples页中注册TDBCalendar

 

下面就是创建的代码:

 

unit DBCal;

 

interface

 

uses SysUtils, WinTypes, WinProc, Messages, Classes, Graphics, Controls,

Forms, Grids, Calendar;

type

TDBCalendar=class(TCalendar)

end;

 

procedure Register;

 

implementation

 

procedure Register;

begin

RegisterComponents(Samples,[TDBabendar]);

end;

 

end.

 

19.3.1.2 使控制只读

 

因为这个数据日历以只读方式响应数据,所以用户不能在控制中改变数据并指望它们反映到数据库中。

使日历只读包含下列两步:

● 增加只读属性

● 允许所需的更新

 

1. 增加只读属性

给日历控制增加只读选项是直接过程。通过增加属性,可以提供在设计时使控制只读的方法,当属性值被设为True,将使控制中所有元素不可被选。

⑴ 增加属性声明和保存值的private域:

 

type

TDBCalendar=class(TClendar)

private

FReadOnly: Boolean;

public

constructor Create (Aowner: TComponent); override;

published

property ReadOnly: Boolean read FReadOnly write FReadOnly default True;

end;

 

constructor TDBCalendar.Create(Aowner: TComponent);

begin

inherited Create(AOwner);

FReadOnly := True;

end;

 

⑵ 覆盖SelectCell方法,使得当控制是只读时,不允许选择:

 

function TDBCalendar.SelectCell(ACol, Arow: Longint): Boolean;

begin

if FReadOnly then

Result := False

else

Result := inherited SelectCell(Acol,ARow);

end;

 

还要在TDBcalendar的声明中声明SelectCell。

如果现在将Calendar加入窗体,会发现部件完全忽略鼠标和击键事件,而且当改变日期时,也不能改变选择的位置。下面将使控制响应更新。

2. 允许所需的更新

只读日历使用SelectCell方法实现各种改变,包括设置Row和Col的值。当日期改变时,UpdateCalendar方法设置Row和Col的值,但因为SelectCell不允许你改变,即使日期改变了,选择仍留在原处。

可以给日历增加一个Boolean标志,当标志为True时允许改变:

 

type

TDBCalendar=class(TCalendar)

private

Fupdating: Boolean;

protected

function SelectCell(Acol, Arow: Longint); Boolean; override;

public

procedure UpdateCalendar; override;

end;

 

function TDBCalendar.SelectCell(ACol, ARow: Longint): Boolean;

begin

if (not FUpdating) and FReadOnly then

Result := False { 如果更新则允许选择 }

else

Result := inherited SelectCell(ACol, ARow); { 否则调用继承的方法 }

end;

 

procedure UpdateCalendar;

begin

FUpdating := True; { 将标志设为允许更新 }

try

inherited UpdateCalendar; { 象通常一样更新 }

finally

FUpdating := False; { 总是清除标志 }

end;

end;

 

  现在日历仍旧不允许用户修改,但当改变日期属性时能正确反映改变;目前已有了一个真正只读控制,下一步是增加数据浏览能力。

 

  3. 增加数据联接

  控制和数据库的联接是由一个名为DataLink的对象处理。Delphi提供了几种类型的Datalink。将控制与数据库单个域相联的DataLink对象是TFieldDatalink。Delphi也提供了与整个表相联的DataLink。

  一个数据相关控制拥有DataLink对象,就是说,控制负责创建和析构DataLink。

  要建立作为拥有对象的Datalink,要执行下列三步:

  ● 声明对象域

  ● 声明访问属性

  ● 初始化DataLink

 

  ⑴ 声明对象域

  每个部件要为其拥有对象声明一个对象域。因此,日历对象DataLink 声明TFieldDataLink类型的域。

  日历部件中DataLink的声明如下:

 

type

TDBCalendar = class(TSampleCalendar)

private

FDataLink: TFieldDataLink;



end;

 

  ⑵ 声明访问属性

  每一个数据相关控制有一个DataSource属性,该属性描述应用程序给控制提供数据的数据源。而且,访问单个域的数据库还需要一个DataField 属性描述数据源中的域。

  下面是DataSource和DataField的声明和它们的实现方法:

 

type

TDBCalendar = class(TSampleCalendar)

private { 属性的实现方法是 }

function GetDataField: string; { 返回数据库字段的名字 }

function GetDataSource: TDataSource; { 返回数据源(Data source)的引用 }

procedure SetDataField(const Value: string); { 给数据库字段名赋值 }

procedure SetDataSource(Value: TDataSource); { 给数据源赋值 }

published { 使属性在设计时可用 }

property DataField: string read GetDataField write SetDataField;

property DataSource: TDataSource read GetDataSource write SetDataSource;

end;

 

……

 

function TDBCalendar.GetDataField: string;

begin

Result := FDataLink.FieldName;

end;

 

function TDBCalendar.GetDataSource: TDataSource;

begin

Result := FDataLink.DataSource;

end;

 

procedure TDBCalendar.SetDataField(const Value: string);

begin

FDataLink.FieldName := Value;

end;

 

procedure TDBCalendar.SetDataSource(Value: TDataSource);

begin

FDataLink.DataSource := Value;

end;

 

  现在,就建立了日历和DataLink的链,此外还有一个更重要的步骤。你必须在日历构建时创建DataLink对象,在日历析构时,撤消DataLink对象。

  ⑶ 初始化DataLink

  在数据相关控制在其存在的期间要不停地访问DataLink对象,因此,必须在其构建函数中创建DataLink创建并且在析构时,撤消DataLink对象,因此要覆盖日历的Create和Destroy方法。

 

type

TDBCalendar=class(TCalendar)

public

constructor Create(Aowna: TComponent); override;

destructor Destroy; override;

end;

 

constructor TDBCalendar Create (Aowner: TComponent);

begin

inherited Create(AOwner);

FReadOnly := True;

FDataLink := TFieldDataLink.Create;

end;

 

destructor TDBCalendar Destroy;

begin

FDataLink.Free;

inherited Destroy;

end;

 

现在,部件已拥有完整的DataLink,但部件还不知从相联的域中读取什么数据。

 

19.3.1.4 响应数据变化

 

  一旦控制拥有了数据联接(DataLink)和描述数据源和数据域的属性。就需在数据记录改变时响应域中数据的变化。

  DataLink对象都有个名为OnDataChange的事件。当数据源指示数据发生变化时,DataLink对象调用任何OnDataChange所联接的事件处理过程。

  要在数据改变时更新数据,就需要给DataLink对象的OnDataChange事件增加事件处理过程。

  下面声明了DataChange方法,并将其赋给DataLink对象的OnDataChange事件:

 

type

TDBCalendar=class(TCalendar)

private

procedure Datachange(Sender: TObject);

end;

 

constructor TDBCalendar Create(AOwner:TComponent);

begin

inherited Create(AOwner);

FReadOnly := True;

FDataLink := TFieldDataLink.Create;

FDataLink.OnDataChange := DataChange;

end;

 

destructor TDBcalendar.Destroy;

begin

FDataLink.OnDataChange := nil;

FDataLink.Free;

inherited Destroy

end;

 

procedure TDBCalendar.DataChange(Sender: TObject);

begin

if FDataLink.Filed=nil then

CalendarDate := 0;

else

CalendarDate := FDataLink.Field.AsDate;

end;

;


想死你们了!

TOP

DELPHI基础教程

第十九章 Delphi自定义部件开发(四)
19.3.2 创建图形部件 

图形控制是一类简单的部件。因为纯图形部件从不需要得到键盘焦点,所以它没有也不要窗口句柄。包含图形控制的应用程序用户仍然可以用鼠标操作控制,但没有键盘界面。

  在本例中提供的图形部件是TShape。Shape部件位于Component Palette的Additional页。本例中的Shape部件有所不同,因此称其为TSampleShape。

  创建图形部件需要下列三个步骤:

  ● 创建和注册部件

  ● 公布(publishing)继承的属性

● 增加图形功能

 

19.3.2.1 创建和注册部件

 

每个部件的创建都从相同的方式开始,在本例中如下:

● 建立名为Shapes的部件单元

● 从TGraphicControl 继承,将新部件称为TSampleShape

● 在Component Palette的Samples页上注册TSampleShape

 

unit Shapes

 

intertace

 

use SysUtils, WinTypes, WinProcs, Messages, Classes,

Graphics,Controls,Forms;

 

type

TSampleShape=class(TGraphicControl)

end;

 

implementation

 

procedure Register;

begin

RegisterComponents('Samples',[TSampleShape]);

end;

 

end.

 

19.3.2.2 公布继承的属性

 

一旦决定了部件类型,就能决定在父类的protected部分声明哪些属性和事件能为用户可见。TGraphicControl已经公布了所有作为图形控制的属性,因此,只需公布响应鼠标和拖放事件的属性。

 

  type

TSampleShape=class(TGraphicControl)

published

property DragCursor;

property DragMode;

property OnDragDrop;

property OnDragOver;

property ONEndDrag;

property OnMouseDown;

property OnMouseMove;

property OnMouseup;

end;

 

这样,该Shape控制具有通过鼠标和拖放与用户交互的能力。

 

19.3.2.3 .增加图形能力

 

一旦你声明了图形部件并公布了继承的属性,就可以给部件增加图形功能。这时需要知道两点:

  ● 决定画什么

  ● 怎样画部件图形

 

  在Shape控制的例子中,需要增加一些能使用户在设计时改变形状的属性。

 

1. 决定画什么

  图形部件通常都具有改变外观的能力,图形控制的外观取决于其某些属性的结合,例如Gauge控制具有决定其形状、方向和是否图形化地显示其过程的能力。同样,Shape控制也应有决定显示各种形状的能力.

给予Shape控制这种能力,增加名为Shape的属性。这需要下列三步:

● 声明属性类型

● 声明属性

● 编写实现方法

 

  ⑴ 声明属性类型

  当声明一个用户自定义类型的属性时,必须首先声明属性类型。最普通地用于属性的自定义类型是枚举类型。

  对Shape控制来说,需要声明一个该控制能画形状的枚举,下面是枚举类型的声明:

 

type

TSampleShapeType=(sstRectangle, sstSquare, sstRoundRect,

sstRoundSquare, sstEllipse, sstCircle);

TSampleShape = class(TGraphicControl)

end;

 

这样,就可以用该类型来声明属性。

⑵ 声明属性

当声明一个属性时,通常需要声明私有域来保存属性值,然后描述读写属性值的方法。

对于Shape控制,将声明一个域保存当前形状,然后声明一个属性通过方法调用来读写域值。

 

type

TSampleShape=class(TGrahpicControl)

private

FShape: TSampleShapeType;

procedure SetShape(value: TSampleShapeType);

published

property Shape: TSampleShapeType read FShape write SetShape;

end;

 

现在,只剩下SetShape的实现部分了。

⑶ 编写实现方法

下面是SetShape的实现:

 

procedure TSampleShape.SetShape(value: TSampleShapeType);

begin

if FShape<>value then

begin

FShape := value;

Invalidate(True); { 强制新形状的重画 }

end;

end;

 

2. 覆盖constructor和destructor

为了改变缺省属性值和初始化部件拥有的对象,需要覆盖继承的constructor和destructor方法。

图形控制的缺省大小是相同的,因此需要改变Width和Height属性。

本例中Shape控制的大小的初始设置为边长65个象素点。

⑴ 在部件声明中增加覆盖constructor

 

type

TSampleShape=class(TGraphicControl)

public

constructor Create(Aowner: TComponent); override;

end;

 

⑵ 用新的缺省值重新声明属性Height和width

 

type

TSampleShape=class(TGrahicControl)

published

property Height default 65;

property Width default 65;

end;

 

⑶ 在库单元的实现部分编写新的constructor

 

constructor TSampleShape.Create(Aowner: TComponent);

begin

inherited Create(AOwner);

width := 65;

Height := 65;

end;

 

3. 公布Pen和Brush

在缺省情况下,一个Canvas具有一个细的、黑笔和实心的白刷,为了使用户在使用Shape控制时能改变Canvas的这些性质,必须能在设计时提供这些对象;然后在画时使用这些对象,这样附属的Pen或Brush被称为Owned对象。

管理Owned对象需要下列三步:

● 声明对象域

● 声明访问属性

● 初始化Owned对象

 

⑴ 声明Owned对象域

拥有的每一个对象必须有对象域的声明,该域在部件存在时总指向Owned对象。通常,部件在constructor中创建它,在destructor中撤消它。

Owned对象的域总是定义为私有的,如果要使用户或其它部件访问该域,通常要提供访问属性。

下面的代码声明了Pen和Brush的对象域:

 

type

TSampleShape=class(TGraphicControl)

private

FPen: TPen;

FBrush: TBrush;

end;

 

⑵ 声明访问属性

可以通过声明与Owned对象相同类型的属性来提供对Owned对象的访问能力。这给使用部件的开发者提供在设计时或运行时访问对象的途径。

下面给Shape控制提供了访问Pen和Brush的方法

 

type

TSampleShape=class(TGraphicControl)

private

procedure SetBrush(Value: TBrush);

procedure SetPen(Value: TPen);

published

property Brush: TBrush read FBrush write SetBrush;

property Pen: TPen read FPen write SetPen;

end;

 

然后在库单元的implementation部分写SetBrush和SetPen方法:

 

procedure TSampleShape.SetBrush(Value: TBrush);

begin

FBrush.Assign(Value);

end;

 

procedure TSampleShape.SetPen(Value: TPen);

begin

FPen.Assign(Value);

end;

 

⑶ 初始化Owned对象

部件中增加了的新对象,必须在部件constructor中建立,这样用户才能在运行时与对象交互。相应地,部件的destructor必须在撤消自身之前撤消Owned对象。

因为Shape控制中加入了Pen和Brush对象,因此,要在constructor中初始化它们,在destructor中撤消它们。

① 在Shape控制的constructor中创建Pen和Brush

 

constructor TSampleShape.Create(Aowner: TComponent);

begin

inherited Create(AOwner);

Width := 65;

Height := 65;

FPen := TPen.Create;

FBrush := TBrush.Create;

end;

 

② 在部件对象的声明中覆盖destructor

 

type

TSampleShape=class(TGraphicControl)

public

construstor.Create(Aowner: TComponent); override;

destructor.destroy; override;

end;

 

③ 在库单元中的实现部分编写新的destructor

 

destructor TSampleShape.destroy;

begin

FPen.Free;

FBrush.Free;

inherited destroy;

end;

 

④ 设置Owned对象的属性

处理Pen和Brush对象的最后一步是处理Pen和Brush发生改变时对Shape控制的重画问题。Pen和Brush对象都有OnChange事件,因此能够在Shape控制中声明OnChange事件指向的事件处理过程。

下面给Shape控制增加了该方法并更新了部件的constructor以使Pen和Brush事件指向新方法:

 

type

TSampleShape = class(TGraphicControl)

published

procdeure StyleChanged(Sender: TObject);

end;

 

implemintation

 

constructor TSampleShape.Create(AOwner:TComponent);

begin

inherited Create(AOwner);

Width := 65;

Height := 65;

Fpen := TPen.Create;

FPen.OnChange := StyleChanged;

Fbrush := TBrush.Create;

FBrush.OnChange := StyleChanged;

end;

 

procedure TSampleShape.StyleChanged(Sender: TObject);

begin

Invalidate(true);

end;

 

当变化发生时,部件重画以响应Pen或Brush的改变。

 

4. 怎样画部件图形

图形控制基本要素是在屏幕上画图形的方法。抽象类TGraphicControl定义了名为Paint的虚方法,可以覆盖该方法来画所要的图形。

Shape控制的paint方法需要做:

● 使用用户选择的Pen和Brush

● 使用所选的形状

● 调整座标。这样,方形和圆可以使用相同的Width和Height

 

覆盖paint方法需要两步:

● 在部件声明中增加Paint方法的声明

● 在implementation部分写Paint方法的实现

 

下面是Paint方法的声明:

 

type

TSampleShape = class(TGraphicControl)

protected

procedure Paint; override;

end;

 

然后,编写Paint的实现:

 

procedure TSampleShape.Paint;

begin

with Canvas do

begin

Pen := FPen;

Brush := FBrush;

case FShape of

sstRectangle, sstSquare :

Rectangle(0, 0, Width, Height);

sstRoundRect, sstRoundSquare:

RoundRect(0, 0, Width, Height, Width div 4, Height div 4);

sstCircle, sstEllipse :

Ellipse(0, 0, Width, Height);

end;

end;

end; 

无论任何控制需要更新图形时,Paint就被调用。当控制第一次出现,或者当控制前面的窗口消失时,Windows会通知控制画自己。也可以通过调用Invalidate方法强制重画,就象StyleChanged方法所做的那样。


想死你们了!

TOP

DELPHI基础教程

第十九章 Delphi自定义部件开发(四)
19.3.2 创建图形部件 

图形控制是一类简单的部件。因为纯图形部件从不需要得到键盘焦点,所以它没有也不要窗口句柄。包含图形控制的应用程序用户仍然可以用鼠标操作控制,但没有键盘界面。

  在本例中提供的图形部件是TShape。Shape部件位于Component Palette的Additional页。本例中的Shape部件有所不同,因此称其为TSampleShape。

  创建图形部件需要下列三个步骤:

  ● 创建和注册部件

  ● 公布(publishing)继承的属性

● 增加图形功能

 

19.3.2.1 创建和注册部件

 

每个部件的创建都从相同的方式开始,在本例中如下:

● 建立名为Shapes的部件单元

● 从TGraphicControl 继承,将新部件称为TSampleShape

● 在Component Palette的Samples页上注册TSampleShape

 

unit Shapes

 

intertace

 

use SysUtils, WinTypes, WinProcs, Messages, Classes,

Graphics,Controls,Forms;

 

type

TSampleShape=class(TGraphicControl)

end;

 

implementation

 

procedure Register;

begin

RegisterComponents('Samples',[TSampleShape]);

end;

 

end.

 

19.3.2.2 公布继承的属性

 

一旦决定了部件类型,就能决定在父类的protected部分声明哪些属性和事件能为用户可见。TGraphicControl已经公布了所有作为图形控制的属性,因此,只需公布响应鼠标和拖放事件的属性。

 

  type

TSampleShape=class(TGraphicControl)

published

property DragCursor;

property DragMode;

property OnDragDrop;

property OnDragOver;

property ONEndDrag;

property OnMouseDown;

property OnMouseMove;

property OnMouseup;

end;

 

这样,该Shape控制具有通过鼠标和拖放与用户交互的能力。

 

19.3.2.3 .增加图形能力

 

一旦你声明了图形部件并公布了继承的属性,就可以给部件增加图形功能。这时需要知道两点:

  ● 决定画什么

  ● 怎样画部件图形

 

  在Shape控制的例子中,需要增加一些能使用户在设计时改变形状的属性。

 

1. 决定画什么

  图形部件通常都具有改变外观的能力,图形控制的外观取决于其某些属性的结合,例如Gauge控制具有决定其形状、方向和是否图形化地显示其过程的能力。同样,Shape控制也应有决定显示各种形状的能力.

给予Shape控制这种能力,增加名为Shape的属性。这需要下列三步:

● 声明属性类型

● 声明属性

● 编写实现方法

 

  ⑴ 声明属性类型

  当声明一个用户自定义类型的属性时,必须首先声明属性类型。最普通地用于属性的自定义类型是枚举类型。

  对Shape控制来说,需要声明一个该控制能画形状的枚举,下面是枚举类型的声明:

 

type

TSampleShapeType=(sstRectangle, sstSquare, sstRoundRect,

sstRoundSquare, sstEllipse, sstCircle);

TSampleShape = class(TGraphicControl)

end;

 

这样,就可以用该类型来声明属性。

⑵ 声明属性

当声明一个属性时,通常需要声明私有域来保存属性值,然后描述读写属性值的方法。

对于Shape控制,将声明一个域保存当前形状,然后声明一个属性通过方法调用来读写域值。

 

type

TSampleShape=class(TGrahpicControl)

private

FShape: TSampleShapeType;

procedure SetShape(value: TSampleShapeType);

published

property Shape: TSampleShapeType read FShape write SetShape;

end;

 

现在,只剩下SetShape的实现部分了。

⑶ 编写实现方法

下面是SetShape的实现:

 

procedure TSampleShape.SetShape(value: TSampleShapeType);

begin

if FShape<>value then

begin

FShape := value;

Invalidate(True); { 强制新形状的重画 }

end;

end;

 

2. 覆盖constructor和destructor

为了改变缺省属性值和初始化部件拥有的对象,需要覆盖继承的constructor和destructor方法。

图形控制的缺省大小是相同的,因此需要改变Width和Height属性。

本例中Shape控制的大小的初始设置为边长65个象素点。

⑴ 在部件声明中增加覆盖constructor

 

type

TSampleShape=class(TGraphicControl)

public

constructor Create(Aowner: TComponent); override;

end;

 

⑵ 用新的缺省值重新声明属性Height和width

 

type

TSampleShape=class(TGrahicControl)

published

property Height default 65;

property Width default 65;

end;

 

⑶ 在库单元的实现部分编写新的constructor

 

constructor TSampleShape.Create(Aowner: TComponent);

begin

inherited Create(AOwner);

width := 65;

Height := 65;

end;

 

3. 公布Pen和Brush

在缺省情况下,一个Canvas具有一个细的、黑笔和实心的白刷,为了使用户在使用Shape控制时能改变Canvas的这些性质,必须能在设计时提供这些对象;然后在画时使用这些对象,这样附属的Pen或Brush被称为Owned对象。

管理Owned对象需要下列三步:

● 声明对象域

● 声明访问属性

● 初始化Owned对象

 

⑴ 声明Owned对象域

拥有的每一个对象必须有对象域的声明,该域在部件存在时总指向Owned对象。通常,部件在constructor中创建它,在destructor中撤消它。

Owned对象的域总是定义为私有的,如果要使用户或其它部件访问该域,通常要提供访问属性。

下面的代码声明了Pen和Brush的对象域:

 

type

TSampleShape=class(TGraphicControl)

private

FPen: TPen;

FBrush: TBrush;

end;

 

⑵ 声明访问属性

可以通过声明与Owned对象相同类型的属性来提供对Owned对象的访问能力。这给使用部件的开发者提供在设计时或运行时访问对象的途径。

下面给Shape控制提供了访问Pen和Brush的方法

 

type

TSampleShape=class(TGraphicControl)

private

procedure SetBrush(Value: TBrush);

procedure SetPen(Value: TPen);

published

property Brush: TBrush read FBrush write SetBrush;

property Pen: TPen read FPen write SetPen;

end;

 

然后在库单元的implementation部分写SetBrush和SetPen方法:

 

procedure TSampleShape.SetBrush(Value: TBrush);

begin

FBrush.Assign(Value);

end;

 

procedure TSampleShape.SetPen(Value: TPen);

begin

FPen.Assign(Value);

end;

 

⑶ 初始化Owned对象

部件中增加了的新对象,必须在部件constructor中建立,这样用户才能在运行时与对象交互。相应地,部件的destructor必须在撤消自身之前撤消Owned对象。

因为Shape控制中加入了Pen和Brush对象,因此,要在constructor中初始化它们,在destructor中撤消它们。

① 在Shape控制的constructor中创建Pen和Brush

 

constructor TSampleShape.Create(Aowner: TComponent);

begin

inherited Create(AOwner);

Width := 65;

Height := 65;

FPen := TPen.Create;

FBrush := TBrush.Create;

end;

 

② 在部件对象的声明中覆盖destructor

 

type

TSampleShape=class(TGraphicControl)

public

construstor.Create(Aowner: TComponent); override;

destructor.destroy; override;

end;

 

③ 在库单元中的实现部分编写新的destructor

 

destructor TSampleShape.destroy;

begin

FPen.Free;

FBrush.Free;

inherited destroy;

end;

 

④ 设置Owned对象的属性

处理Pen和Brush对象的最后一步是处理Pen和Brush发生改变时对Shape控制的重画问题。Pen和Brush对象都有OnChange事件,因此能够在Shape控制中声明OnChange事件指向的事件处理过程。

下面给Shape控制增加了该方法并更新了部件的constructor以使Pen和Brush事件指向新方法:

 

type

TSampleShape = class(TGraphicControl)

published

procdeure StyleChanged(Sender: TObject);

end;

 

implemintation

 

constructor TSampleShape.Create(AOwner:TComponent);

begin

inherited Create(AOwner);

Width := 65;

Height := 65;

Fpen := TPen.Create;

FPen.OnChange := StyleChanged;

Fbrush := TBrush.Create;

FBrush.OnChange := StyleChanged;

end;

 

procedure TSampleShape.StyleChanged(Sender: TObject);

begin

Invalidate(true);

end;

 

当变化发生时,部件重画以响应Pen或Brush的改变。

 

4. 怎样画部件图形

图形控制基本要素是在屏幕上画图形的方法。抽象类TGraphicControl定义了名为Paint的虚方法,可以覆盖该方法来画所要的图形。

Shape控制的paint方法需要做:

● 使用用户选择的Pen和Brush

● 使用所选的形状

● 调整座标。这样,方形和圆可以使用相同的Width和Height

 

覆盖paint方法需要两步:

● 在部件声明中增加Paint方法的声明

● 在implementation部分写Paint方法的实现

 

下面是Paint方法的声明:

 

type

TSampleShape = class(TGraphicControl)

protected

procedure Paint; override;

end;

 

然后,编写Paint的实现:

 

procedure TSampleShape.Paint;

begin

with Canvas do

begin

Pen := FPen;

Brush := FBrush;

case FShape of

sstRectangle, sstSquare :

Rectangle(0, 0, Width, Height);

sstRoundRect, sstRoundSquare:

RoundRect(0, 0, Width, Height, Width div 4, Height div 4);

sstCircle, sstEllipse :

Ellipse(0, 0, Width, Height);

end;

end;

end; 

无论任何控制需要更新图形时,Paint就被调用。当控制第一次出现,或者当控制前面的窗口消失时,Windows会通知控制画自己。也可以通过调用Invalidate方法强制重画,就象StyleChanged方法所做的那样。


想死你们了!

TOP

DELPHI基础教程

第二十章 开发Delphi对象式数据管理功能(二)
20.1.6 TResourceStream对象 

  TResourceStream对象是另一类MemoryStream对象,它提供对Windows 应用程序资源的访问,因此称它为资源流。TResourceSream也是从TCustomMemoryStream 继承的。因此在TCustomMemoryStream对象的基础上,定义了与指定资源模块或资源文件建立连接的构造方法,并且还覆盖了Write,以实现向资源文件中写数据。

下面介绍TResourceStream的实现

  1. 私有域

  TResourceStream没有定义新的属性,但它在private部分定义了两个数据域HResInfo和HGlobol和一个私有方法Initialize,它们的定义如下:

 

TResourceStream = class(TCustomMemoryStream)

private

HResInfo: HRSRC;

HGlobal: THandle;

procedure Initialize(Instance: THandle; Name, ResType: PChar);



end;

 

  HRSRC是描述Windows资源信息的结构句柄。HGlobal变量代表资源所在模块的句柄。如果操作的是应用程序资源,HGlohal就代表EXE程序的句柄;如果是动态链接库(DLL),则HGlobal 代表动态链接库的句柄。TResourceStream对象使用这两上变量访问应用程序或动态链接库中的资源。

  Initialize方法是TResourceStream对象内部使用的。它的构造方法Create和CreateFromID都是调用Initialize方法完成对TResourceStream的初始化。它的实现如下:

 

procedure TResourceStream.Initialize(Instance: THandle; Name, ResType: PChar);

 

procedure Error;

begin

raise EResNotFound.Create(FmtLoadStr(SResNotFound, [Name]));

end;

 

begin

HResInfo := FindResource(Instance, Name, ResType);

if HResInfo = 0 then Error;

HGlobal := LoadResource(Instance, HResInfo);

if HGlobal = 0 then Error;

SetPointer(LockResource(HGlobal), SizeOfResource(Instance, HResInfo));

end;

 

 该方法实现中,首先调用Windows函数FoundResource得到由参数Instance指定的模块中的名为Name和类型为ResType的资源,然后调用LoadResource将资源调用内存,并返回该资源在内存中的句柄,最后,将该资源复制到ResourceStream中。方法的Instance参数代表要调用的资源所在的模块句柄。模块可以是可执行文件,也可以是动态链接库。如果在读取资源时没在模块中发现要找的资源则产生异常事件。

  2. 构造方法Create和CreateFromID

  这两个方法在实现上没有大的不同。顾名思义,第一个方法是通过资源名构造TResourceStream; 第二个方法通过资源ID构造TResourceStream,而且在实现过程中,它们都调用了Initialize方法。下面是它们的实现:

 

constructor TResourceStream.Create(Instance: THandle; const ResName: string;

ResType: PChar);

begin

inherited Create;

Initialize(Instance, PChar(ResName), ResType);

end;

 

constructor TResourceStream.CreateFromID(Instance: THandle; ResID: Integer;

ResType: PChar);

begin

inherited Create;

Initialize(Instance, PChar(ResID), ResType);

end;

 

  这两个方法中都有Instance参数,该参数值的含义在Insitialize中介绍过。

  3. Write方法

  TResourceStream的Write方法只完成一件事,就产生这个异常事件,其实现如下:

 

function TResourceStream.Write(const Buffer; Count: Longint): Longint;

begin

raise EStreamError.CreateRes(SCantWriteResourceStreamError);

end;

 

  从方法实现中可以看到,TSourceStream对象是不允许写数据的。一旦往资源流中写数据将产生异常事件。

  4. 析构方法Destroy

  该方法产生给资源解锁,然后释放该资源,最后调用继承的Destroy方法释放ResourceStream。其实现如下:

 

destructor TResourceStream.Destroy;

begin

UnlockResource(HGlobal);

FreeResource(HResInfo);

inherited Destroy;

end;

 

  回顾Initialize方法,我们不难发现:

  ● ResourceStream没有额外地给资源重新分配内存,而是直接使用HGlobal句柄所指 的内存域

  ● ResourceStream中的资源在流的生存期,始终是Lock状态,因此要根据Windows 的内存使用规则合理安排ResourceStream的使用

  ● ResourceStream只是用于访问应用程序和动态链接库中的资源的

 

在Classes在单元中提供了InternalReadComponentRes函数,该函数使用了TResourceStream对象从Delphi应用程序中读取部件。Delphi是将窗体和部件信息放在模块资源的RCDATA段的。

 

20.1.7 TBlobStream对象

 

  从Delphi 数据库开发平台这个意义上说,TBlobStream 对象是个很重要的对象。TBlobStream对象提供了修改TBlobField、TBytesField或TVarBytesField中数据的技术。开发者可以象对待文件或流那样在数据库域中读写数据。

  传统数据库发展的一个重要趋向是往多媒体数据库发展。目前比较著名和流行的数据库都支持多媒体功能,多媒体数据存储中的一大难点是数据结构不规则,数据量大。各大数据库产品是采用BLOB技术解决多媒体数据存储中的问题。Delphi的TBlobStream对象的意义就在于:一方面可以使Delphi应用程序充分利用多媒体数据库的数据管理能力;另一方面又能利用Object Pascal的强大程序设计能力给多媒体数据库提供全方向的功能扩展余地。

  使用TBlobStream对象可以在多媒体数据库的BLOB字段存储任意格式的数据。一般说来,许多多媒体数据库只能支持图像、语音或者OLE服务器支持的数据。利用TBlobStream则不同,只要是程序能够定义的数据格式,它都能在BLOB字段中读写,而不需要其它辅助工具。

  TBlobStream用构造方法Create建立数据库域和BLOB流的联接。用Read或Write 方法访问和改变域中的内容;用Seek方法,在域中定位;用Truncate方法删除域中当前位置起所有的数据。

 

20.1.7.1 TBlobStream的属性和方法

 

  TBlobStream对象从TStream直接继承,没有增添新的属性。它覆盖了Read、Write 和Seek方法,提供了对BLOB字段的访问操作;它增添了Truncate方法以实现BLOB字段中的删除操作。

  1. Read方法

  声明:function Read(var Buffer; Count: Longint): Longint;

Read方法从数据库域的当前位置起复制Count个字节的内容到Buffer中。Buffer也必须至少分配Count个字节。Read方法返回实际传输的字节数,因为传输的字节数可能小于Count,所以需要选择符的边界判断。

  2. Write方法

  声明:function Write(const Buffer; Count: Longint); override; Longint;

Write方法从Buffer中向数据库域的当前位置复制Count个字节的内容。Buffer必须分配有Count个字节的内存空间,函数返回实际传输的字节数,传输过程也要进行选择符边界判断。

  3. Seek方法

  声明:function Seek(Offset: Longint; Origin: Word): Longint;

  Seek方法重新设置BLOB流中的指针位置。如果Origin的值是soFromBeginning,则新的指针位置是Offset; 如Origin的值是soFromCurrent,则新的指针位置是Position+Offset;如果Origin的值是SoFromCurrent,则新的指针位置是Size+Offset。函数返回新的指针位置值。当Origin为0(SoFromBegin)时,Offset的值必须大于等于零; 当Origin的值为2(SoFromEnd),Offset的值必须小于等于零。

  4. Truncate方法

  声明:procedure Truncate;

Truncate方法撤消TBlobField、TBytesField或TVarBytesField中从当前位置起的数据。

  5. Create方法

  声明:constructor Create(Field: TBlobField; Mode: TBlobStreamMode);

  Create方法使用Field参数建立BLOB流与BLOB字段的联接。Mode 的值可为bmRead、bmWrite和bmReadWrite。

 

20.1.7.2 TBlobStream的实现原理

 

  说明TBlobStream对象的实现原理,不可避免地要涉及它的私有域,下面是私有域的定义:

 

TBlobStream = class(TStream)

private

FField: TBlobField;

FDataSet: TDataSet;

FRecord: PChar;

FBuffer: PChar;

FFieldNo: Integer;

FOpened: Boolean;

FModified: Boolean;

FPosition: Longint;



public



end;

 

  FField是与BLOB流相联的数据库BLOB域,该域用于BLOB流的内部访问。FDataSet是代表FField所在的数据库,它可以是TTable部件,也可以是TQuery 部件。FRecord和FBuffer都是BLOB流内部使用的缓冲区,用于存储FField所在记录的数据,该数据记录中不包含BLOB数据,TBlobStream使用FRecord作为调用BDE API函数的参数值。FFieldNo代表BLOB字段的字段号,也用于BDE API的参数传递,FOpened和FMocified都是状态信息,FPosition表示BLOB流的当前位置,下面介绍TBlobStream方法实现。

  1. Create方法和Destroy方法的实现

  Create方法的功能主要是建立BlobStream流与BLOB字段的联系并初始化某些私有变量。其实现如下:

  

constructor TBlobStream.Create(Field: TBlobField; Mode: TBlobStreamMode);

var

OpenMode: DbiOpenMode;

begin

FField := Field;

FDataSet := Field.DataSet;

FRecord := FDataSet.ActiveBuffer;

FFieldNo := Field.FieldNo;

if FDataSet.State = dsFilter then

DBErrorFmt(SNoFieldAccess, [FField.DisplayName]);

if not FField.FModified then

begin

if Mode = bmRead then

begin

FBuffer := AllocMem(FDataSet.RecordSize);

FRecord := FBuffer;

if not FDataSet.GetCurrentRecord(FBuffer) then Exit;

OpenMode := dbiReadOnly;

end else

begin

if not (FDataSet.State in [dsEdit, dsInsert]) then DBError(SNotEditing);

OpenMode := dbiReadWrite;

end;

Check(DbiOpenBlob(FDataSet.Handle, FRecord, FFieldNo, OpenMode));

end;

FOpened := True;

if Mode = bmWrite then Truncate;

end;

 

 该方法首先是用传入的Field参数给FField,FDataSet,FRecord和FFieldNo赋值。方法中用AllocMem按当前记录大小分配内存,并将指针赋给FBuffer,用DataSet部件的GetCurrentRecord方法,将记录的值赋给FBuffer,但不包括BLOB数据。

  方法中用到的DbiOpenBlob函数是BDE的API函数,该函数用于打开数据库中的BLOB字段。

  最后如果方法传入的Mode参数值为bmWrite,就调用Truncate将当前位置指针以后的

数据删除。

  分析这段源程序不难知道:

  ● 读写BLOB字段,不允许BLOB字段所在DataSet部件有Filter,否则产生异常事件

  ● 要读写BLOB字段,必须将DataSet设为编辑或插入状态

  ● 如果BLOB字段中的数据作了修改,则在创建BLOB 流时,不再重新调用DBiOpenBlob函数,而只是简单地将FOpened置为True,这样可以用多个BLOB 流对同一个BLOB字段读写

 

  Destroy方法释放BLOB字段和为FBuffer分配的缓冲区,其实现如下:

 

destructor TBlobStream.Destroy;

begin

if FOpened then

begin

if FModified then FField.FModified := True;

if not FField.FModified then

DbiFreeBlob(FDataSet.Handle, FRecord, FFieldNo);

end;

if FBuffer <> nil then FreeMem(FBuffer, FDataSet.RecordSize);

if FModified then

try

FField.DataChanged;

except

Application.HandleException(Self);

end;

end;

 

  如果BLOB流中的数据作了修改,就将FField的FModified置为True;如果FField的Modified为False就释放BLOB字段,如果FBuffer不为空,则释放临时内存。最后根据FModified的值来决定是否启动FField的事件处理过程DataChanged。

  不难看出,如果BLOB字段作了修改就不释放BLOB字段,并且对BLOB 字段的修改只有到Destroy时才提交,这是因为读写BLOB字段时都避开了FField,而直接调用BDE API函数。这一点是在应用BDE API编程中很重要,即一定要修改相应数据库部件的状态。

  2. Read和Write方法的实现

  Read和Write方法都调用BDE API函数完成数据库BLOB字段的读写,其实现如下:

  

function TBlobStream.Read(var Buffer; Count: Longint): Longint;

var

Status: DBIResult;

begin

Result := 0;

if FOpened then

begin

Status := DbiGetBlob(FDataSet.Handle, FRecord, FFieldNo, FPosition,

Count, @Buffer, Result);

case Status of

DBIERR_NONE, DBIERR_ENDOFBLOB:

begin

if FField.FTransliterate then

NativeToAnsiBuf(FDataSet.Locale, @Buffer, @Buffer, Result);

Inc(FPosition, Result);

end;

DBIERR_INVALIDBLOBOFFSET:

{Nothing};

else

DbiError(Status);

end;

end;

end;

 

  Read方法使用了BDE API的DbiGetBlob函数从FDataSet中读取数据,在本函数中,各参数的含义是这样的:FDataSet.Handle代表DataSet的BDE句柄,FReacord表示BLOB字段所在记录,FFieldNo表示BLOB字段号,FPosition表示要读的的数据的起始位置,Count表示要读的字节数,Buffer是读出数据所占的内存,Result是实际读出的字节数。该BDE函数返回函数调用的错误状态信息。

  Read方法还调用了NativeToAnsiBuf进行字符集的转换。

 

function TBlobStream.Write(const Buffer; Count: Longint): Longint;

var

Temp: Pointer;

begin

Result := 0;

if FOpened then

begin

if FField.FTransliterate then

begin

GetMem(Temp, Count);

try

AnsiToNativeBuf(FDataSet.Locale, @Buffer, Temp, Count);

Check(DbiPutBlob(FDataSet.Handle, FRecord, FFieldNo, FPosition,

Count, Temp));

finally

FreeMem(Temp, Count);

end;

end else

Check(DbiPutBlob(FDataSet.Handle, FRecord, FFieldNo, FPosition,

Count, @Buffer));

Inc(FPosition, Count);

Result := Count;

FModified := True;

end;

end;

 

Write方法调用了BDE API的DbiPutBlob函数实现往数据库BLOB字段存储数据。

该函数的各参数含义如下:

 

表20.2 调用函数DbiPutBlob的各传入参数的含义

 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

   参数名           含义

──────────────────────────────

  FDataSetHandle 写入的数据库的BDE句柄

  FRecord 写入数据的BLOB字段所在的记录

FFieldNo BLOB字段号

  FPosition 写入的起始位置

  Count 写入的数据的字节数

  Buffer 所写入的数据占有的内存地址

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

  方法中还根据FField和FTransliterate的值判断是否进行相应的字符集转换,最后移动BLOB流的位置指针,并将修改标志FModified置为True。

3. Seek和GetBlobSize方法的实现

  Seek方法的功能主要是移动BLOB流的位置指针。GetBlobSize方法是私有的,在Seek方法中被调用,其功能是得到BLOB数据的大小。它们的实现如下:

 

function TBlobStream.GetBlobSize: Longint;

begin

Result := 0;

if FOpened then

Check(DbiGetBlobSize(FDataSet.Handle, FRecord, FFieldNo, Result));

end;

 

function TBlobStream.Seek(Offset: Longint; Origin: Word): Longint;

begin

case Origin of

0: FPosition := Offset;

1: Inc(FPosition, Offset);

2: FPosition := GetBlobSize + Offset;

end;

Result := FPosition;

end;

 

GetBlobSize调用了BDE API的DbiGetBlobSize函数,该函数的参数的含义同前面的API函数相同。

  4. Truncate方法

该方法是通过调用BDE API函数实现的。其实现如下:

 

procedure TBlobStream.Truncate;

begin

if FOpened then

begin

Check(DbiTruncateBlob(FDataSet.Handle, FRecord, FFieldNo, FPosition));

FModified := True;

end;

end;

 

  该方法从BLOB流的当前位置起删除所有数据,并设置修改标志FModified为True。在Delphi VCL中许多部件特别是数据库应用方面的部件都用BDE API函数完成对数据库的访问,如Data Access和Data Control部件。各种数据库部件都是BDE API函数外层的包装简化了对数据库的访问操作。BDE API中还提供了避开BDE配置工具在程序中直接处理Alias(建立、修改、删除等)的函数支持,这也是部件所没有提供的。在Delphi数据库应用安装程序中,这些Alias操作函数无疑是相当重要的。有关BDE API函数的详细介绍,可阅读Delphi2.0 Client/Server Suite所带的BDE API 帮助文件。

 

 

20.2 读写对象的实现原理和应用

 

  读写对象(Filer)包括TFiler对象、TReader对象和TWriter对象。TFiler对象是文件读写的基础对象,在应用程序中使用的主要是TReader和TWriter。TReader和TWriter对象都直接从TFiler对象继承。TFiler对象定义了Filer对象的基本属性和方法。

  Filer对象主要完成两大功能:

  ● 存取窗体文件和窗体文件中的部件

  ● 提供数据缓冲,加快数据读写操作

 

20.2.1 TFiler对象

 

 TFiler对象是TReader和TWriter的抽象类,定义了用于部件存储的基本属性和方法。它定义了Root属性,Root指明了所读或写的部件的根对象,它的Create方法将Stream对象作为传入参数以建立与Stream对象的联系, Filer对象的具体读写操作都是由Stream对象完成。因此,只要是Stream对象所能访问的媒介都能由Filer对象存取部件。TFiler 对象还提供了两个定义属性的方法:DefineProperty和DefineBinaryProperty,这两个方法使对象能读写不在部件published部分定义的属性。

  因为Filer对象主要用于存取Delphi的窗体文件和窗体文件中的部件,所以要清楚地理解Filer对象就要清楚Delphi 窗体文件(DFM文件)的结构。

  DFM文件是用于Delphi存储窗体的。窗体是Delphi可视化程序设计的核心。窗体对应Delphi应用程序中的窗口,窗体中的可视部件对应窗口中的界面元素,非可视部件如TTable和TOpenDialog,对应Delphi应用程序的某项功能。Delphi应用程序的设计实际上是以窗体的设计为中心。因此,DFM文件在Delphi应用设计中也占很重要的位置。窗体中的所有元素包括窗体自身的属性都包含在DFM文件中。

  在Delphi应用程序窗口,界面元素是按拥有关系相互联系的,因此树状结构是最自然的表达形式;相应地,窗体中的部件也是按树状结构组织;对应在DFM文件中,也要表达这种关系。DFM文件在物理上,是以二进制方式存储的,在逻辑上则是以树状结构安排各部件的关系。Delphi编辑窗口支持以文本方式显示DFM文件。从该文本中可以看清窗体的树状结构。下面是DFM文件的文本显示:

 

  Object Form1: TForm1

Left = 72

Top = 77

ActiveControl = DBIMage1



Object Panell: TPanel

Left = 6



Object DBLabel1: TDBText



end

Object DBImage1: TDBImage



end

end

Object Panel2: TPanel

Left = 6



Object Label1: TLable



end

end

Object Panel3: TPanel

Left = 6



Object DBLabel2: TDBText



end

end

end

  关于DFM文件中存储属性值的规则,请参见自定义部件开发这一章。

  对照TFiler对象的属性。Root属性就表示部件树的根──窗体。Filer对象的许多方法都是读从根起始的树中所有的部件。Ancestor属性表示根的祖先对象,IgnoreChildren属性则是读部件时忽略根的子结点。

  下面介绍Filer对象的属性和方法。


想死你们了!

TOP

DELPHI基础教程

第二十章 开发Delphi对象式数据管理功能(三)
20.2.1.1 TFiler对象的属性和方法 

  1. Root属性

  声明:property Root: TComponent;

Root 属性给Filer对象指出被读写的对象中哪一个对象是根或主要拥有者。RootComponent和WriteRootComponent方法在读和写部件及其拥有的部件前先设置Root的值。

  2. Ancestor属性

  声明:property Ancestor: TPersistent;

Ancestor属性用于往继承下来的窗体中写部件,因为当写部件时,Write对象只需要写入与所继承的部件不同的属性,所以在写之前要跟踪每个继承的部件,并且比较它们的属性。

  如果Ancestor为nil,就表示没有相应的继承的部件,Writer对象应当将部件完全写入流。Ancestor一般为nil,只有当调用WriteDescendant和WriteDescendantRes时,才给赋值。当编写和覆盖DefineProperties时,必须设置Ancestor的值。

  3. IgnoreChildren属性

  声明:property Ignorechildren: Boolean;

IgnoreChildren属性使一个Writer对象存储部件时可以不存储该部件拥有的部件。如果IgnoreChildren属性为True,则Writer对象存储部件不存它拥有的子部件。否则,Writer对象将所有其拥有的对象写入流。

  4. Create方法

  声明:constructor Create(Stream: TStream; BufSize: Cardinal);

 Create方法创建一个新的Filer对象,建立它和流Stream的联系;并且给它分配一个缓冲区Buffer。Buffer的大小由BufSize指定。

  5. Defineproperty方法

  声明:procedure Defineproperty(const Name: String; ReadData: TReaderProc;

WriteData: TWriterProc; HasData: Boolean); virtual; abstract;

Defineproperty方法定义Filer对象将作为属性存储的数据。Name参数描述接受的属性名,该属性不在published部分定义。ReadData和WriteData参数指定在存取对象时读和写所需数据的方法。HasData参数在运行时决定了属性是否有数据要存储。

  只有当对象有数据要存储时,才在该对象的DefineProperties中调用DefineProperty。DefineProperties有一个Filer对象作为它的参数,调用的就是该Filer对象的DefineProperty和DefineBinaryProperty方法。当定义属性时,Writer对象应当引用Ancestor属性,如果该属性非空,Writer对象应当只写入与从Ancestor继承的不同的属性的值。

  一个最简单的例子是TComponent的DefineProperties方法。尽管TComponent 没有在published中定义Left、Top属性,但该方法存储了部件的位置信息。

 

procedure TComponent.DefineProperties(Filer: TFiler);

begin

Filer.DefineProperty('Left', ReadLeft, WriteLeft, LongRec(FDesignInfo).Lo <> 0);

Filer.DefineProperty('Top', ReadTop, WriteTop, LongRec(FDesignInfo).Hi <> 0);

end;

 

6. DefineBinaryproperty方法

  声明:procedure DefineBinaryproperty(const Name: String;

ReadData, WriteData: TStreamProc;

HisData: Boolean); virtual; abstract;

DefineBinaryProperty方法定义Filer对象作为属性存储的二进制数据。Name参数描述属性名。ReadData和WriteData参数描述所存储的对象中读写所需数据的方法。HasData参数在运行时决定属性是否有数据要存。

  DefineBinaryProperty和DefineProperty方法的不同之处在于,二进制型的属性直接用Stream对象读写,而不是通过Filer对象。通过ReadData和WriteData传入的方法,直接将对象数据写入流或从流读出。

  DefineBinaryProperty属性用得较少。只有标准的VCL对象定义了象图形、图像之类的二进制属性的部件中才用它。

  7. FlushBuffer方法

  声明:procedure FlushBuffer; virtual: abstract;

FlushBuffer方法用于使Filer对象的缓冲区与相联的Stream对象同步。对Reader对象来说,是通过重新分配缓冲区;对于Writer对象是通过写入当前缓冲区。

  FlushBuffer是一个抽象方法,TReader和TWriter都覆盖了它,提供了具体实现。

 

20.2.1.2 TFiler对象的实现原理

 

  TFiler对象是Filer对象的基础类,它定义的大多数方法都是抽象类型的,没有具体实现它,这些方法要在TReader和TWrite中覆盖。但它们提供了Filer对象的框架,了解它无疑是很重要的。

  1. TFiler对象属性的实现

  TFiler对象定义了三个属性:Root、Ancestor和IgnoreChildren。正如定义对象属性通常所采用的方法那样,要在private部分定义存储属性值的数据域,然后在public或Published部分定义该属性,并按需要增加读写控制。它们的定义如下:

  

TFiler = class(TObject)

private



FRoot: TComponent;

FAncestor: TPersistent;

FIgnoreChildren: Boolean;

public



property Root: TComponent read FRoot write FRoot;

property Ancestor: TPersistent read FAncestor write FAncestor;

property IgnoreChildren: Boolean read FIgnoreChildren write FIgnoreChildren;

end;

 

  它们在读写控制上都是直接读写私有的数据域。

  在介绍TReader和TWriter的实现,我们还会看到这几个属性的原理介绍。

  2. TFiler对象方法的实现

  在TFiler对象定义的众多方法中很多都是抽象类方法,没有具体实现。在TFiler 的后继对象TReader中覆盖了这些方法。在后面章节,会介绍这些方法的实现。

  在TFiler对象中有具体实现的有两个方法Create和Destroy。

  ⑴ Create方法的实现

  Create方法是TFiler的构造方法,它有两个参数Stream和BufSize。Stream是指定与TFiler对象相联系的Stream对象,Filer对象都是用Stream对象完成具体的读写。BufSize是TFiler对象内部开设的缓冲区的大小。Filer对象内部开设缓冲区是为了加快数据的读写,它的实现如下:

 

constructor TFiler.Create(Stream: TStream; BufSize: Integer);

begin

FStream := Stream;

GetMem(FBuffer, BufSize);

FBufSize := BufSize;

end;

 

  FStream、FBuffer和FBufSize都是TFiler在private部分定义的数据域。FStream表示与Filer对象相联的Stream对象,FBuffer指向Filer对象内部开设的缓冲区,FBufSize是内部缓冲区的大小。Create方法用Stream参数值给FStream赋值,然后用GetMem分配BufSize大小的动态内存作为内部缓冲区。

  ⑵ Destroy方法的实现

  Destroy方法是TFiler对象的析构函数,它的作用就是释放动态内存。

 

destructor TFiler.Destroy;

begin

if FBuffer <> nil then FreeMem(FBuffer, FBufSize);

end;

 

20.2.2 TWriter对象

 

  TWriter 对象是可实例化的,往流中写数据的Filer对象。TWriter对象直接从TFiler继承而来,除了覆盖从TFiler继承的方法外,还增加了大量的关于写各种数据类型(如Integer、String和Component等)的方法。TWriter对象和TReader 对象配合使用将使对象读写发挥巨大作用。

 

20.2.2.1 TWriter对象的属性和方法

 

  1. Position属性

  声明:property Position: Longint;

TWriter对象的Position属性表示相关联的流中的当前要写的位置,TReader 对象也有这个属性,但与TReader对象不同的是TWriter对象的Position的值比流的Position值小,这一点一看属性实现就清楚了。

  2. RootAncesstor属性

  声明:property RootAncestor: TComponent;

RootAncestor属性表示的是Root属性所指的部件的祖先。如果Root 是继承的窗体,Writer对象将窗体拥有部件与祖先窗体中的相应部件依次比较,然后只写入那些与祖先中的不同的部件。

  3. Write方法

  声明:procedure Write(const Buf; Count: Longint);

Write方法从Buf中往与Writer相关联的流中写入Count个字节。

  4. WriteListBegin方法

  声明:procedure WriteListBegin;

WriteListBegin方法往Write对象的流中写入项目列表开始标志,该标志意味着后面存储有一连串的项目。Reader对象,在读这一连串项目时先调用ReadListBegin方法读取该标志位,然后用EndOfList判断是否列表结束,并用循环语句读取项目。在调用WriteListBegin方法的后面必须调用WriteListEnd方法写列表结束标志,相应的在Reader对象中有ReadListEnd方法读取该结束标志。

  5. WriteListEnd方法

  声明:procedure WriteListEnd;

WriteListEnd方法在流中,写入项目列表结束标志,它是与WriteListBegin相匹配的方法。

  6. WriteBoolean方法

  声明:procedure WriteBoolean(Value: Boolean);

WriteBoolean方法将Value传入的布尔值写入流中。

  7. WriteChar方法

  声明:procedure WriteChar(Value: char);

WriteChar方法将Value中的字符写入流中。

  8. WriteFloat方法

  声明:procedure WriteFloat(Value: Extended);

WriteFloat方法将Value传入的浮点数写入流中。

  9. WriteInteger方法

  声明:procedure WriteInteger(Value: Longint);

WriteInteger方法将Value中的整数写入流中。

  10. WriteString方法

  声明:procedure WriteString(const Value: string);

WriteString方法将Value中的字符串写入流中。

  11. WriteIdent方法

  声明:procedure WriteIdent(const Ident: string);

WriteIdent方法将Ident传入的标识符写入流中。

  12. WriteSignature方法

  声明:procedure WriteSignature;

WriteSignature方法将Delphi Filer对象标签写入流中。WriteRootComponent方法在将部件写入流之前先调用WriteSignature方法写入Filer标签。Reader对象在读部件之前调用ReadSignature方法读取该标签以指导读操作。

  13. WritComponent方法

  声明:procedure WriteComponent(Component: TComponent);

WriteComponent方法调用参数Component的WriteState方法将部件写入流中。在调用WriteState之前,WriteComponent还将Component的ComponetnState属性置为csWriting。当WriteState返回时再清除csWriting.

14. WriteRootComponent方法

  声明:procedure WriteRootComponent(Root: TComponent);

WriteRootComponent方法将Writer对象Root属性设为参数Root带的值,然后调用WriteSignature方法往流中写入Filer对象标签,最后调用WriteComponent方法在流中存储Root部件。

 

20.2.2.2 TWriter对象的实现

 

  TWriter对象提供了许多往流中写各种类型数据的方法,这对于程序员来说是很重要的功能。TWrite对象往流中写数据是依据不同的数据采取不同的格式的。 因此要掌握TWriter对象的实现和应用方法,必须了解Writer对象存储数据的格式。

  首先要说明的是,每个Filer对象的流中都包含有Filer对象标签。该标签占四个字节其值为“TPF0”。Filer对象为WriteSignature和ReadSignature方法存取该标签。该标签主要用于Reader对象读数据(部件等)时,指导读操作。

  其次,Writer对象在存储数据前都要留一个字节的标志位,以指出后面存放的是什么类型的数据。该字节为TValueType类型的值。TValueType是枚举类型,占一个字节空间,其定义如下:

 

  TValueType = (VaNull, VaList, VaInt8, VaInt16, VaInt32, VaEntended, VaString, VaIdent,

VaFalse, VaTrue, VaBinary, VaSet, VaLString, VaNil, VaCollection);

 

因此,对Writer对象的每一个写数据方法,在实现上,都要先写标志位再写相应的数据;而Reader对象的每一个读数据方法都要先读标志位进行判断,如果符合就读数据,否则产生一个读数据无效的异常事件。VaList标志有着特殊的用途,它是用来标识后面将有一连串类型相同的项目,而标识连续项目结束的标志是VaNull。因此,在Writer对象写连续若干个相同项目时,先用WriteListBegin写入VaList标志,写完数据项目后,再写出VaNull标志;而读这些数据时,以ReadListBegin开始,ReadListEnd结束,中间用EndofList函数判断是否有VaNull标志。

  下面就介绍它们的实现。

  1. TWriter对象属性的实现

  TWriter对象直接从TFiler对象继承,它只增加了Position和RootAncestor属性。

RootAncestor属性在private部分有数据域FRootAncestor存入其值。在属性定义的读与控制上都是直接读取该值。

  Position属性的定义中包含了两个读写控制方法:GetPosition和SetPosition:

 

TWriter = class(TFiler)

private

FRootAncestor: TComponent;



function GetPosition: Longint;

procedure SetPosition(Value: Longint);

public



property Position: Longint read GetPosition write SetPosition;

property RootAncestor: TComponent read FRootAncestor write FRootAncestor;

end;

 

GetPosition和SetPosition方法实现如下:

 

function TWriter.GetPosition: Longint;

begin

Result := FStream.Position + FBufPos;

end;

 

procedure TWriter.SetPosition(Value: Longint);

var

StreamPosition: Longint;

begin

StreamPosition := FStream.Position;

{ 只清除越界的缓冲区 }

if (Value < StreamPosition) or (Value > StreamPosition + FBufPos) then

begin

WriteBuffer;

FStream.Position := Value;

end

else FBufPos := Value - StreamPosition;

end;

 

  WriteBuffer是TWriter对象定义的私有方法,它的作用是将Writer 对象内部缓冲区中的有效数据写入流中,并将FBufPos置为0。Writer对象的FlushBuffer对象就是用WriteBuffer方法刷新缓冲区。

  在SetPosition方法中,如果Value值超出了边界(FStream.Position,FStream.Position + FBufPos),就将缓冲区中的内容写入流,重新设置缓冲区在流中的相对位置;否则,就只是移动FBufPos指针。

  2. TWriter方法的实现

  ⑴ WriteListBegin和WriteListEnd的实现

  这两个方法都是用于写连续若干个相同类型的值。WriteListBegin写入VaList标志,WriteListEnd写入VaNull标志。

 

procedure TWriter.WriteListBegin;

begin

WriteValue(vaList);

end;

 

procedure TWriter.WriteListEnd;

begin

WriteValue(vaNull);

end;

 

  这两个方法都调用TWriter对象的WriteValue方法,该方法主要用于写入TValueType类型的值。

 

procedure TWriter.WriteValue(Value: TValueType);

begin

Write(Value, SizeOf(Value));

end;

 

  ⑵ 简单数据类型的写入

  简单数据类型指的是整型、字符型、字符串型、浮点型、布尔型等。TWriter对象都定义了相应的写入方法。

  WriteInteger方法用于写入整型数据。

 

procedure TWriter.WriteInteger(Value: Longint);

begin

if (Value >= -128) and (Value <= 127) then

begin

WriteValue(vaInt8);

Write(Value, SizeOf(Shortint));

end else

if (Value >= -32768) and (Value <= 32767) then

begin

WriteValue(vaInt16);

Write(Value, SizeOf(Smallint));

end else

begin

WriteValue(vaInt32);

Write(Value, SizeOf(Longint));

end;

end;

 

  WriteInteger方法将整型数据分为8位、16位和32位三种,并分别用vaInt8、vaInt16和VaInt32。

  WriteBoolean用于写入布尔型数据:

 

procedure TWriter.WriteBoolean(Value: Boolean);

begin

if Value then

WriteValue(vaTrue) else

WriteValue(vaFalse);

end;

 

  与其它数据类型不同的是布尔型数据只使用了标志位是存储布尔值,在标志位后没有数据。

  WriteFloat方法用于写入浮点型数据。

 

procedure TWriter.WriteFloat(Value: Extended);

begin

WriteValue(vaExtended);

Write(Value, SizeOf(Extended));

end;

 

  字符串“True”、“False”和“nil”作为标识符传入是由于Delphi的特殊需要。如果是“True”、“False”和“nil”则写入VaTrue、VaFalse和VaNil,否则写入VaIdent标志,接着以字符串形式写入标识符。

  WriteString方法用于写入字符串

  

procedure TWriter.WriteString(const Value: string);

var

L: Integer;

begin

L := Length(Value);

if L <= 255 then

begin

WriteValue(vaString);

Write(L, SizeOf(Byte));

end else

begin

WriteValue(vaLString);

Write(L, SizeOf(Integer));

end;

Write(Pointer(Value)^, L);

end;

 

  Delphi的字符串类型有两种。一种长度小于256个字节,另一种长度小于65536 个字节。WriteString方法区分这两类情况存储字符串,一种设置VaStirng标志,另一种设置VaLString。然后存储字符串的长度值,最后存储字符串数据。

  WriteChar方法用于写入字符。

  

procedure TWriter.WriteChar(Value: Char);

begin

WriteString(Value);

end;

 

  字符类型的读写是用读写字符串的方法,在读的时候,判断字节数为1时,则为字符型。

  ⑶ 部件的写入

  TWriter对象中与写入部件有关的方法有WriteSignature、WritePrefix、WriteComponent、WriteDescendant和WriteRootComponent。

 WriteSignature方法用于往流中写入Filer对象标签。

 

procedure TWriter.WriteSignature;

begin

Write(FilerSignature, SizeOf(FilerSignature));

end;

 

  FilerStgnature是字符串常量,其值为“TPF0”,代表对象标签。

  WritePrefix方法用于在写入部件前写入ffInherited和ffChildPos标志,这些标志表示部件的继承特征和创建序值特征。

 

procedure TWriter.WritePrefix(Flags: TFilerFlags; AChildPos: Integer);

var

Prefix: Byte;

begin

if Flags <> [] then

begin

Prefix := $F0 or Byte(Flags);

Write(Prefix, SizeOf(Prefix));

if ffChildPos in Flags then WriteInteger(AChildPos);

end;

end;

 

  如果ffChildPos置位,则存入部件在Owner中的创建序值。更详细的信息请参阅TReader的ReadPrefix方法。

  WriteComponent方法往流中写入部件。

 

procedure TWriter.WriteComponent(Component: TComponent);

 

function FindAncestor(const Name: string): TComponent;

begin



end;

 

begin

Include(Component.FComponentState, csWriting);

if Assigned(FAncestorList) then

Ancestor := FindAncestor(Component.Name);

Component.WriteState(Self);

Exclude(Component.FComponentState, csWriting);

end;

 

  方法中用Component的WritState方法写入部件的属性。在写入之前将Component.FComponentState置为csWriting写入完后再将csWriting复位。

  WriteDescendant是根据祖先AAncestor的情况写入部件Root。

 

procedure TWriter.WriteDescendent(Root: TComponent; AAncestor: TComponent);

begin

FRootAncestor := AAncestor;

FAncestor := AAncestor;

FRoot := Root;

WriteSignature;

WriteComponent(Root);

end;

 

方法先调用WriteSignature方法写入Filer对象标签。然后调用WriteComponent将部件Root写入流。

  WriteRootComponent方法则是调用WriteDescendant方法写入部件,只是将后者的Ancestor参数以nil值传入。 

procedure TWriter.WriteRootComponent(Root: TComponent);

begin

WriteDescendent(Root, nil);

end;


想死你们了!

TOP

DELPHI基础教程

第二十章 开发Delphi对象式数据管理功能(四)
20.2.3 TReader对象 

  TReader对象是可实例化的用于从相联系的流中读取数据的Filer对象。TReader对象从TFiler继承下来,除了从TFiler继承的属性和方法外,TReader声明了不少属性、方法和事件。

  Owner和Parent属性用于表示从Reader对象的流中读取的部件的拥有者和双亲结点。OnError,OnFindMethod和OnSetName事件使应用程序在运行中读数据时能定制响应方式。除了覆盖了一些从TFiler对象中继承的方法外,TReader对象还定义大量的读不同类型的数据和触发事件的方法。

 

20.2.3.1 TReader对象的属性和方法

 

  1. Owner属性

  声明:property Owner: TComponent;

Reader对象的Owner属性存储了将用来给从Reader的流中读出的部件的Owner属性赋值的部件。

  2. Parent属性

  声明:property Parent: TComponent;

Parent属性存储将用来给从Reader的流中读出所有控制的Parent属性赋值的部件。

  3. Position属性

  声明:propertion: Longint;

Reader对象的Position属性表示相联的流中读的当前位置。Position的值还应包括读缓冲区的大小。对于Reader 对象,Position的值大于流的Position 的值。如果将Position的值设得超过当前缓冲区,将引起调用FlushBuffer。

  4. BeginReferences方法

  声明:procedure BeginReferences;

BeginReferences方法启动一连串关于读部件的命令,这些部件包含相互间的交叉引用。在使用上通常和FixupReferences和EndReferences一起放在Try…finally程序块中。

  在调用了BeginReferences后,Reader对象创建读取所有对象和名字的列表。所有的独立对象被读出后,调用FixupReferences方法将名字的相互从流中转移到对象实例中。最后调用EndReferences方法释放列表。

  处理部件相互引用的程序块形式如下:

 

BeginReferences; { 创建临时列表 }

try

{ 读出所有部件并将它们的名字放在一临时列表中 }



FixupReferences; { 分 解 }

finally

EndReferences; { 释放临时列表 }

end;

 

  5. FixUpReferences方法

  声明:procedure FixupReferences;

FixupReferences方法分解从流中读出的存在各种相互依赖部件的引用关系。FixupReferences总在try…finally块中并配合BeginReferences和EndReferences一起使用。

  6. EndReferences方法

  声明:procedure EndReferences;

EndReferences方法终止处理相互引用的块操作,释放对象列表。它总配合BeginReferences和FixupReferences一起使用。

  7. ReadListBegin方法

  声明:procedure ReadListBegin;

ReadListBegin方法从Reader对象相联的流中读取列表开始标志。如果流中紧接着要读取的项目不是一个由WritelistBegin方法写入的列表起始标志,ReadListBegin将引起一个读异常事件。

  通常在调用ReadlistBegin方法之后,紧跟着一个读项目的循环,循环以EndfList方法返回True 终止条件。这时,预示流中的下一个项目是列表结束标志,需要调用ReadListEnd方法。

  8. ReadListEnd方法

  声明:procedure ReadListEnd;

ReadListEnd 方法从流中读取列表结束标志。如果所读的项目不是一个列表结束标志,ReadListEnd方法引发一个EReadError异常事件。

  9. EndOfList方法

  声明:function EndOfList: Boolean;

如果Reader对象读到项目列表结果标志,EndOfList方法返回True。

  TStrings对象在从Reader对象读取项目列表时使用了ReadListBegin和ReadListEnd方法。下面的ReadData是TStrings的方法,用于在DefineProperties方面中读string数据。

 

procedure TStrings.ReadData(Reader: TReader);

begin

Reader.ReadListBegin; { 读列表开始标志 }

Clear; { 清除已有的字符串 }

while not Reader.EndOfList do { 只要还有数据 … }

Add(Reader.ReadString); { …读一个字符串并将其加在列表中 }

Reader.ReadListEnd; { 越过列表结束标志 }

end;

 

10. ReadSignature方法

  声明:procedure ReadSignature;

ReadSignature方法从流中读取部件之前首先调用ReadSignature方法。在载入对象之前检测标签。Reader对象就能防止疏忽大意,导致读取无效或过时的数据。Filer标签是四个字符,对于Delphi 2.0,该标签是“TPF0”。

  11. ReadPrefix方法

  声明:procedure ReadPrefix(var Plags: TFilerFlags; var AChild, Pos: Integer);

ReadPrefix方法的功能与ReadSignature的很相象,只不过它是读取流中部件前面的标志(PreFix)。当一个Write对象将部件写入流中时,它在部件前面预写了两个值,第一个值是指明部件是否是从祖先窗体中继承的窗体和它在窗体中的位置是否重要的标志;第二个值指明它在祖先窗体创建次序。ReadComponent方法自动调用ReadPrefix。但如果需要独立读取部件的预读标志,也可直接调用该方向。

  12. OnFindMethod事件

  声明:property OnFindMethod: TFindMethodEvent;

OnFindMethod事件,发生在Reader对象读取对象的方法指针时,属性为方法指针的通常都是事件。

  响应OnFindMethod事件的理由,通常是处理过程找不到方法的情况。在FindMethod方法没有找到由Name指定的方法的情况下,如果它将OnFindMethod方法的Error 参数设为True,将引起ReadError异常事件;反之,将Error参数置为False,将防止FindMethod方法引发异常事件。

  13. Error方法

  声明:function Error(const Message: String): Boolean; virtual;

Error方法定义在Reader对象的protected部分,它是用于Reader对象的OnError事件。其返回值决定是否继续错误处理过程。如果返回值为True,则表示用程序应当继续错误处理;如果返回值为False,则表示错误情况被忽略。

  如果读部件或属性出错。Reader对象调用Error方法。缺省情况下,Error将返回值设为False,然后调用OnError事件处理过程。

  TReader对象总是在try…except程序块的except部分,并提供用户忽略错误的机会。Error的使用方法如下:

 

  try

… { 读部件 }

except

on E: Exception do

begin

…{ 执行一些清除操作 }

if Error(E.Message) then raise;

end;

end;

 

  14. OnError事件

  声明:property OnError: TReaderError;

当Reader对象读取数据出错时将引发OnError事件。通过处理OnError事件,可以有选择地处理或忽略错误。

  传给OnError事件处理过程的最后一个参数是名为Handled的var参数。在缺省情况下,Error方法将Handled置为True。这将阻止错误更进一步处理。如果事件处理过程仍旧将Handled置为False,Reader对象将引发一个EReadError异常事件。

 

15. SetName方法

  声明:procedure SetName(Component: TComponent; var Name: String virtual);

SetName方法允许Reader对象在将从流中读取的部件的Name值赋给部件的Name属性前修改Name值。ReadComponent方法在读取部件的属性值和其它数据前先读部件的类型和名字在读完名字后,ReadComponent将所读的名字作为Name参数传给SetName,Name 是个var参数,因此SetName能在返回前修改字符串值。SetName还调用了OnSetName事件处理过程,将名字字符串作为var参数传入事件处理过程中,因此,事件处理过程也可修改字符串的值。

  16. OnSetName事件

  声明:property OnSetName: TSetNameEvent;

OnSetName事件发生在Read对象设置部件的Name属性前,OnSetName事件处理过程的var参数Name参数是一个var参数,因此,事件处理过程再将Name赋给部件前,可以修改Name的值。这对于想过滤窗体中部件的名字是很有帮助的。

  下面的OnSetName事件处理过程,命名了名字中包含“Button”的部件,并用“PushButton”替代。

 

procedure TForm1.ReaderSetName(Reader: TReader; Component: TComponent;

var Name: string);

var

ButtonPos: Integer;

begin

ButtonPos := Pos('Button', Name);

if ButtonPos <> 0 then

Name := Copy(Name, 1, ButtonPos - 1) + 'PushButton' +

Copy(Name, ButtonPos + 6, Length(Name));

end;

 

17. ReadValue方法

  声明:function ReadValue: TValueType;

ReadValue方法读取流中紧着的项目的类型,函数返回后,流的指针移到值类型指示符之后。

  TValueType是枚举类型。存储在Filer对象的流中的每个项目之前都有一个字节标识该项目的类型,在读每个项目之前都要读取该字节,以指导调用哪个方法来闱取项目。该字节的值就TValuetype定义的值类型之一。

  18. NextValue方法

  声明:function Nextvalue: TValuetype;

Nextvalue方法的作用也是返回Reader对象流中紧接着的项目的类型,它与ReadValue的区别在于并不移动指针位置。

  19. ReadBoolean方法

  声明:function ReadBoolean: Boolean;

ReadBoolean方法从Reader对象的流中读取一个布尔值,并相应地移动流位置指针。

  20、ReadChar方法

  声明:function ReadChar: char;

ReadChar方法从Reader对象的流中读取一个字符。

  21. ReadFloat方法

  声明:function ReadFloat: Extended;

  ReadFloat方法从流中读取浮点数。

  20. ReadIdent方法

  声明:function ReadIdent: string;

ReadIdent方法从流中读取标识符。

  23. ReadInteger方法

  声明:function ReadInteger: Longin

ReadInteger方法从流中读取整型数字。

24.ReadString方法

  声明:function Read String: string;

  ReadString方法从Reader对象的流中读取一个字符串,并返回字符串中的内容。该字符串是由Writer对象的WriteString方法写入。

 

20.2.3.2 TReader对象的实现

 

  Filer对象的作用主要是Delphi用来在DFM文件中读写各种类型的数据(包括部件对象)。这些数据的一个本质特征是变长,而且Filer对象将读写数据操作抽象化,包装成对象提供了大量的读写方法,方便了程序的调用。因此在应用程序中可以广泛使Filer对象,充分利用Delphi的面向对象技术。而且Filer对象与Stream对象捆绑在一起,一方面可以在各种存储媒介中存取任意格式的数据;另一方面,由于充分利用面向对象的动态联编,各种读写方法的使用方法是一致的,因此,方法调用很简单。下面我们着重介绍Reader 对象中与读写数据操作有关的属性和方法的实现。

  1. TReader属性的实现

  在TReader对象的属性实现中我们重点介绍Position的实现。

  Position属性的定义了使用了读写控制,它们分别是GetPosition和SetPosition方法。

 

TReader = class(TFiler)

private



function GetPosition: Longint;

procedure SetPosition(Value: Longint);

public



property Position: Longint read GetPosition write SetPosition;

end;

 

Postition的读写控制方法如下:

 

function TReader.GetPosition: Longint;

begin

Result := FStream.Position + FBufPos;

end;

 

procedure TReader.SetPosition(Value: Longint);

begin

FStream.Position := Value;

FBufPos := 0;

FBufEnd := 0;

end;

 

在TReader的父对象TFiler对象中介绍过FBufPos和FBufEnd变量。Filer对象内部分配了一个BufSize大小的缓冲区FBufPos就是指在缓冲区中的相对位置,FBufEnd是指在缓冲区中数据结束处的位置(缓冲区中的数据不一定会充满整个缓冲区)。

 在GetPosition方法中可以看到Reader对象的Position值和Stream对象的Position值是不同的。Reader对象多了一个FButPos的编移量。

  2. Defineproperty和DefineBinaryproperty方法的实现

这两个方法是虚方法,在TFiler中是抽象方法,在TReader和TWriter对象中才有具体的实现。

  它们在TReader中的实现如下:

  

procedure TReader.DefineProperty(const Name: string; ReadData: TReaderProc;

WriteData: TWriterProc; HasData: Boolean);

begin

if CompareText(Name, FPropName) = 0 then

begin

ReadData(Self);

FPropName := '';

end;

end;

 

procedure TReader.DefineBinaryProperty(const Name: string;

ReadData, WriteData: TStreamProc; HasData: Boolean);

var

Stream: TMemoryStream;

Count: Longint;

begin

if CompareText(Name, FPropName) = 0 then

begin

if ReadValue <> vaBinary then

begin

Dec(FBufPos);

SkipValue;

FCanHandleExcepts := True;

PropValueError;

end;

Stream := TMemoryStream.Create;

try

Read(Count, SizeOf(Count));

Stream.SetSize(Count);

Read(Stream.Memory^, Count);

FCanHandleExcepts := True;

ReadData(Stream);

finally

Stream.Free;

end;

FPropName := '';

end;

end;

 

在两个方法都将Name参数值与当前的属性名比较,如果相同则进行读操作。在DefineBinaryproperty中,创建了一个内存流。先将数据读到内存流中然后调用ReadData读取数据。

  3. FlushBuffer的实现

  FlushBuffer方法用于清除Reader对象的内部缓冲区中的内容,保持Reader对象和流在位置(Position)上的同步,其实现如下:

 

procedure TReader.FlushBuffer;

begin

FStream.Position := FStream.Position - (FBufEnd - FBufPos);

FBufPos := 0;

FBufEnd := 0;

end;

 

  4. ReadListBegin、ReadListEnd和EndOfList方法

  这三个方法都是用于从Reader对象的流中读取一连串的项目,并且这些项目都由WriteListBegin写入的标志标定开始和WriteListEnd写入标志,标定结束,在读循环中用EndOfList进行判断。它们是在Reader对象读取流中数据时经常用于的。它们的实现如下:

 

procedure TReader.ReadListBegin;

begin

CheckValue(vaList);

end;

 

procedure TReader.ReadListEnd;

begin

CheckValue(vaNull);

end;

 

function TReader.EndOfList: Boolean;

begin

Result := ReadValue = vaNull;

Dec(FBufPos);

end;

 

  项目表开始标志是VaList,项目表结束标志是VaNull,VaList和VaNull都是枚举类型TValueType定义的常量。

  它们实现中调用的CheckValue是TReader的私有方法,其实现如下:

 

procedure TReader.CheckValue(Value: TValueType);

begin

if ReadValue <> Value then

begin

Dec(FBufPos);

SkipValue;

PropValueError;

end;

end;

 

  CheckValue方法的功能是检测紧接着要读的值是否是Value指定的类型。如果不是则跳过该项目并触发一个SInvalidPropertyValue错误。

  EndOfList函数只是简单地判断下一字节是否是VaNull将判断结果返回,并将字节移回原来位置。

  5. 简单数据类型读方法的实现

  简单数据类型指的是布尔型、字符型、整型、字符串型、浮点型、集合类型和标识符。将它们放在一起介绍是因为它们的实现方法类似。

  因为它们的实现都用到了ReadValue方法,因此先来介绍ReadValue方法的实现:

 

function TReader.ReadValue: TValueType;

begin

Read(Result, SizeOf(Result));

end;

 

  该方法调用私有方法Read,从Reader对象流中读一个字节,并移动位置指针。

  ReadValue方法专门从流中读取值的类型的,所有的数据读写方法中在读取数据前都要调用ReadValue方法判断是否是所要读的数据。如果是,则调用Read方法读取数据;否则触发一个异常事件,下面看Integer类型的读方法:

 

function TReader.ReadInteger: Longint;

var

S: Shortint;

I: Smallint;

begin

case ReadValue of

vaInt8:

begin

Read(S, SizeOf(Shortint));

Result := S;

end;

vaInt16:

begin

Read(I, SizeOf(I));

Result := I;

end;

vaInt32:

Read(Result, SizeOf(Result));

else

PropValueError;

end;

end;

 

因为Delphi 2.0中,整型可分8位、16位和32位,因此读取整型数据时分别作了判断。

  布尔类型的数据是直接放在值类型标志上,如果类型为VaTrue,则值为True;如果类型为VaFalse,则值为False。

 

function TReader.ReadBoolean: Boolean;

begin

Result := ReadValue = vaTrue;

end;

 

ReadString方法也利用ReadValue方法判断是字符串还是长字符串。

 

function TReader.ReadString: string;

var

L: Integer;

begin

L := 0;

case ReadValue of

vaString:

Read(L, SizeOf(Byte));

vaLString:

Read(L, SizeOf(Integer));

else

PropValueError;

end;

SetString(Result, PChar(nil), L);

Read(Pointer(Result)^, L);

end;

 

如果VaString类型紧接着一个字节存有字符串的长度;如果是VaLString类,则紧接着两个字节存放字符串长度,然后根据字符串长度用SetString过程给分配空间,用Read方法读出数据。

  ReadFloat方法允许将整型值转换为浮点型。

 

function TReader.ReadFloat: Extended;

begin

if ReadValue = vaExtended then Read(Result, SizeOf(Result)) else

begin

Dec(FBufPos);

Result := ReadInteger;

end;

end;

 

字符类型数据设有直接的标志,它是根据VaString后面放一个序值为1的字节来判断的。

 

function TReader.ReadChar: Char;

begin

CheckValue(vaString);

Read(Result, 1);

if Ord(Result) <> 1 then

begin

Dec(FBufPos);

ReadStr;

PropValueError;

end;

Read(Result, 1);

end;

 

出于读取DFM文件需要,Filer对象支持读取标识符。

 

function TReader.ReadIdent: string;

var

L: Byte;

begin

case ReadValue of

vaIdent:

begin

Read(L, SizeOf(Byte));

SetString(Result, PChar(nil), L);

Read(Result[1], L);

end;

vaFalse:

Result := 'False';

vaTrue:

Result := 'True';

vaNil:

Result := 'nil';

else

PropValueError;

end;

end;

 

一般说来,各种复杂的数据结构都是由这些简单数据组成;定义了这些方法等于给读各种类型的数据提供了元操作,使用很方便。例如,读取字符串类型的数据时,如果采用传流方法还要判断字符串的长度,使用ReadString方法就不同了。但应该特别注意的是这些类型数据的存储格式是由Delphi设计的与简单数据类型有明显的不同。因此,存入数据时应当使用Writer对象相应的方法,而且在读数据前要用NextValue方法进行判断,否则会触发异常事件。

  6. 读取部件的方法的实现

  Reader对象中用于读取部件的方法有ReadSignature、ReadPrefix、ReadComponent、ReadRootComponent和ReadComponents。

ReadSignature方法主要用于读取Delphi Filer对象标签一般在读取部件前,都要用调用ReadSignature方法以指导部件读写过程。

 

procedure TReader.ReadSignature;

var

Signature: Longint;

begin

Read(Signature, SizeOf(Signature));

if Signature <> Longint(FilerSignature) then ReadError(SInvalidImage);

end;

 

FilerSignature就是Filer对象标签其值为“TPF0” ,如果读的不是“TPF0” ,则会触发SInValidImage异常事件。

  ReadPrefix方法是用于读取流中部件前的标志位,该标志表示该部件是否处于从祖先窗体中继承的窗体中和它在窗体中的位置是否很重要。

 

procedure TReader.ReadPrefix(var Flags: TFilerFlags; var AChildPos: Integer);

var

Prefix: Byte;

begin

Flags := [];

if Byte(NextValue) and $F0 = $F0 then

begin

Prefix := Byte(ReadValue);

Byte(Flags) := Prefix and $0F;

if ffChildPos in Flags then AChildPos := ReadInteger;

end;

end;

 

  TFilerFlags的定义是这样的:

 

   TFilerFlag = (ffInherited, ffChildPos);

TFilerFlags = Set of TFilerFlag;

 

充当标志的字节的高四位是$F,低四位是集合的值,也是标志位的真正含义。如果ffChildPos置位,则紧接着的整型数字中放着部件在窗体中的位置序值。

  ReadComponent方法用于从Reader对象的流中读取部件。Component 参数指定了要从流中读取的对象。函数返回所读的部件。

 

function TReader.ReadComponent(Component: TComponent): TComponent;

var

CompClass, CompName: string;

Flags: TFilerFlags;

Position: Integer;



 

begin

ReadPrefix(Flags, Position);

CompClass := ReadStr;

CompName := ReadStr;

Result := Component;

if Result = nil then

if ffInherited in Flags then

FindExistingComponent else

CreateComponent;

if Result <> nil then

try

Include(Result.FComponentState, csLoading);

if not (ffInherited in Flags) then SetCompName;

if Result = nil then Exit;

Include(Result.FComponentState, csReading);

Result.ReadState(Self);

Exclude(Result.FComponentState, csReading);

if ffChildPos in Flags then Parent.SetChildOrder(Result, Position);

FLoaded.Add(Result);

except

if ComponentCreated then Result.Free;

raise;

end;

end;

 

ReadCompontent方法首先调用ReadPrefix方法,读出部件标志位和它的创建次序值(Create Order)。然后用ReadStr方法分别读出部件类名和部件名。如果Component参数为nil,则执行两个任务:

● 如果ffInberited 置位则从Root 找已有部件,否则,就从系统的Class表中找到该部件类型的定义并创建

● 如果结果不为空,将用部件的ReadState方法读入各种属性值,并设置部件的Parent 属性,并恢复它在Parent部件的创建次序。

 

  ReadComponent方法主要是调用ReadComponent方法从Reader对象的流中读取一连串相关联的部件,并分解相互引用关系。

 

procedure TReader.ReadComponents(AOwner, AParent: TComponent;

Proc: TReadComponentsProc);

var

Component: TComponent;

begin

Root := AOwner;

Owner := AOwner;

Parent := AParent;

BeginReferences;

try

while not EndOfList do

begin

ReadSignature;

Component := ReadComponent(nil);

Proc(Component);

end;

FixupReferences;

finally

EndReferences;

end;

end;

 

  ReadComponents首先用AOwner和AParent参数给Root,Owner和Parent赋值,用于重建各部件的相互引用。然后用一个While循环读取部件并用由Proc传入的方法进行处理。在重建引用关系时,用了BeginReferences、FixUpReferences和EndReferences嵌套模式。

  ReadRootComponent方法从Reader对象的流中将部件及其拥有的部件全部读出。如果Component参数为nil,则创建一个相同类型的部件,最后返回该部件:

 

function TReader.ReadRootComponent(Root: TComponent): TComponent;

 

function FindUniqueName(const Name: string): string;

begin



end;

 

var

I: Integer;

Flags: TFilerFlags;

begin

ReadSignature;

Result := nil;

try

ReadPrefix(Flags, I);

if Root = nil then

begin

Result := TComponentClass(FindClass(ReadStr)).Create(nil);

Result.Name := ReadStr;

end else

begin

Result := Root;

ReadStr; { Ignore class name }

if csDesigning in Result.ComponentState then

ReadStr else

Result.Name := FindUniqueName(ReadStr);

end;

FRoot := Result;

if GlobalLoaded <> nil then

FLoaded := GlobalLoaded else

FLoaded := TList.Create;

try

FLoaded.Add(FRoot);

FOwner := FRoot;

Include(FRoot.FComponentState, csLoading);

Include(FRoot.FComponentState, csReading);

FRoot.ReadState(Self);

Exclude(FRoot.FComponentState, csReading);

if GlobalLoaded = nil then

for I := 0 to FLoaded.Count - 1 do TComponent(FLoaded[I]).Loaded;

finally

if GlobalLoaded = nil then FLoaded.Free;

FLoaded := nil;

end;

GlobalFixupReferences;

except

RemoveFixupReferences(Root, '');

if Root = nil then Result.Free;

raise;

end;

end;

 

  ReadRootComponent首先调用ReadSignature读取Filer对象标签。然后在try…except循环中执行读取任务。如果Root参数为nil,则用ReadStr读出的类名创建新部件,并以流中读出部件的Name属性;否则,忽略类名,并判断Name属性的唯一性。最后用Root的ReadState方法读取属性和其拥有的拥有并处理引用关系。

  7. SetName方法和OnSetName事件

  因为在OnSetName事件中,Name参数是var型的,所以可以用OnSetName事件处理过程修改所读部件的名字。而OnSetName事件处理过程是在SetName方法中实现的。

 

procedure TReader.SetName(Component: TComponent; var Name: string);

begin

if Assigned(FOnSetName) then FOnSetName(Self, Component, Name);

Component.Name := Name;

end;

 

SetName方法和OnSetName事件在动态DFM文件的编程中有很重要的作用。

  8. TReader的错误处理

  TReader的错误处理是由Error方法和OnError事件的配合使用完成的。OnError 事件处理过程的Handled参数是var型的布尔变量,通过将Handled设为True或False可影响TReader 的错误处理。OnError事件处理过程是在Error方法中调用的。

 

function TReader.Error(const Message: string): Boolean;

begin

Result := False;

if Assigned(FOnError) then FOnError(Self, Message, Result);

end;

 

  9. FindMethod和OnFindMethod事件

  有时,在程序运行期间,给部件的方法指针(主要是事件处理过程)动态赋值是很有用的,这样就能动态地改变部件响应事件的方式。在流中读取部件捍做到一点就要利用OnFindMehtod事件。OnFIndMethod事件是在FindMethod方法中被调用的。

 

function TReader.FindMethod(Root: TComponent;

const MethodName: string): Pointer;

var

Error: Boolean;

begin

Result := Root.MethodAddress(MethodName);

Error := Result = nil;

if Assigned(FOnFindMethod) then FOnFindMethod(Self, MethodName, Result,

Error);

if Error then PropValueError;

end;

 

  OnFindMethod 方法除了可以给部件的MethodName所指定的方法指针动态赋值外,还可修改Error参数来决定是否处理Missing Method错误。方法中调用的MehtodAddress 方法定义在TObject中,它是个很有用的方法,它可以得到对象中定义的public方法的地址。FindMethod方法和OnFindMethod事件在动态DFM的编程中有很重要的作用。

 

 

20.3 Delphi对象式数据管理应用实例

 

  Delphi 2.0无论是其可视化设计工具,还是可视化部件类库(VCL),都处处渗透了对象存储技术,本节将从Delphi可视化设计内部机制、VCL中的数据存储、BLOB数据操作和动态生成部件的存储几方面介绍对象存储功能的实例应用。

 

20.3.1 Delphi 动态DFM文件及部件的存取在超媒体系统中的应用

 

  Delphi的可视化设计工具是同其部件类库紧密结合在一起的。

  每个部件只有通过一段注册程序并通过Delphi的Install Component功能,才能出现在Component Palette上;部件的属性才有可能出现在Object Inspector窗口中;部件的属性编辑器才能被Delphi环境使用。因为这种浑然天成的关系,DFM文件存取必然得到VCL在程序上的支持。

  DFM文件的部件存取是Delphi可视化设计环境中文件存取的中心问题。因为Delphi可视化设计的核心是窗体的设计。每个窗体对应一个库单元,是应用程序的模块,窗体在磁盘上的存储就是DFM文件。

  DFM文件结构我们前面介绍过了。它实际上是存储窗体及其拥有的所有部件的属性。这种拥有关系是递归的。问题在于如何将这些属性数据与程序中的变量(属性)代码联系起来。

  在Delphi中处理这种联系的过程分为两种情况:设计时和运行时。

在设计时,建立联系表现为读取DFM 文件,建立DFM文件中的部件及其属性与可视化设计工具(Object Inspector、窗体设计窗口和代码编辑器)的联系,也就是说让这些部件及其属性能出现在这些窗口中,并与代码中的属性定义联系起来;分解联系表现为存储DFM文件,将窗体窗口中的部件及其属性写入DFM文件。

在运行时,主要是建立联系的过程,即读取DFM文件。这时,DFM文件不是作为独立的磁盘文件,而是以应用程序资源中的RCDATA类型的二进制数据存在。建立联系的过程表现为将资源中的部件及其属性与应用程序中的对象及其数据域联系起来。其过程为:根据DFM中的部件类名创建对象,再将用DFM中的部件属性值给程序中的部件属性赋值。当然要完成这一过程,还必须在代码中有相应的窗体定义,因为方法等代码是不存入部件的。

  VCL对读取DFM文件在代码上的支持是通过Stream对象和Filer对象达到的。在20. 1和20.1节中,我们可以看到Stream对象和Filer对象中有大量的用于存取部件及其属性的方法,尤其在TReader对象中,还有关于错误处理和动态的方法赋值的方法。下面我们就通过程序实例介绍存取DFM文件方法、步骤和注意事项。


想死你们了!

TOP

DELPHI基础教程

第二十章 开发Delphi对象式数据管理功能(五)
20.3.1.1写DFM文件的过程:WriteComponentResFie

   该过程带有两个参数FileName和Instance。FileName参数指定要写入的DFM文件名,Instance参数是TComponent类型的,它指定要写入的部件名,一般是TForm对象的子类。该过程将Instance部件和其拥有的所有部件写入DFM文件。

  这个过程的意义在于,可以在程序运行过程中产生Delphi的窗体部件和在窗体中插入部件,并由该函数将窗体写入DFM文件,支持了动态DFM文件的重用性。

  该过程的程序是这样的:

 

procedure WriteComponentResFile(const FileName: string; Instance: TComponent);

var

Stream: TStream;

begin

Stream := TFileStream.Create(FileName, fmCreate);

try

Stream.WriteComponentRes(Instance.ClassName, Instance);

finally

Stream.Free;

end;

end;

 

  函数中,用FileStream创建文件,用Stream对象的WriteComponetRes方法将Instance写入流中。

 

20.3.1.2 读DFM文件的函数:ReadComponentResFile

 

ReadComponentResFile函数带有两个参数FileName和Instance。FileName参数指定要读DFM文件名,Instance参数指定从DFM文件中要读的部件。该函数从DFM文件中将Instance和它拥有的所有部件,并返回该部件。

  这个函数的意义在于,配合WriteComponentResFile过程的使用支持DFM文件的重用性。

  该函数的程序是这样的:

 

function ReadComponentResFile(const FileName: string; Instance: TComponent):

TComponent;

var

Stream: TStream;

begin

Stream := TFileStream.Create(FileName, fmOpenRead);

try

Result := Stream.ReadComponentRes(Instance);

finally

Stream.Free;

end;

end;

 

  程序中使用FileStream对象打开由FileName指定的DFM文件,然后用Stream对象的ReadComponentRes方法读出Instance,并将读的结果作为函数的返回值。

 

20.3.1.3 读取Delphi应用程序资源中的部件

 

  函数InternalReadComponentRes可以读取Delphi应用程序资源中的部件。Delphi 的DFM文件在程序经过编译链接后被嵌入应用程序的资源中,而且格式发生了改变,即少了资源文件头。

在第一节中曾经介绍过TResourceStream对象,该对象是操作资源媒介上的数据的。函数InternalReadComponentRes用了TResourceStream。程序是这样的:

 

function InternalReadComponentRes(const ResName: string;

var Instance: TComponent): Boolean;

var

HRsrc: THandle;

begin { 避免“EResNotFound”异常事件的出现 }

HRsrc := FindResource(HInstance, PChar(ResName), RT_RCDATA);

Result := HRsrc <> 0;

if not Result then Exit;

FreeResource(HRsrc);

with TResourceStream.Create(HInstance, ResName, RT_RCDATA) do

try

Instance := ReadComponent(Instance);

finally

Free;

end;

Result := True;

end;

 

  HInstance是一个Delphi VCL定义的全局变量,代表当前应用程序的句柄。函数用了资源访问API函数FindResource来测定是否存在ResName所描述资源。因为在TResourceStream的创建过程还有FindResource等操作,所以函数中调用了FreeResource。最后函数调用了Stream对象的ReadComponent方法读出部件。因为函数的Instance是var类型的参数,所以可以访问Instance,得到读出的部件。

 

20.3.1.4 DFM文件与标准文本文件(TXT文件)的相互转换

 

  在Delphi可视化设计环境中,允许程序员在代码编辑器中以文本的方式浏览和修改DFM文件内容。当用File/Open命令直接打开DFM文件或者选择窗体设计窗口的弹出式菜单上的View as Text命令时,就会在编辑器中出现文本形式的信息。我们姑且将这种文本形式称之为窗体设计脚本。Delphi提供的这种脚本编辑功能是对Delphi可视化设计的一大补充。当然这个脚本编辑能力是有限制的,比方说不能在脚本任意地添加和删除部件,因为代码和DFM脚本是紧密相连的,任意添加和修改会导致不一致性。然而在动态生成的DFM文件中,就不存在这一限制,后面会介绍DFM动态生成技术的应用。

  实际上,DFM文件内容是二进制数据,它的脚本是经过Delphi开发环境自动转化的,而且Delphi VCL中的Classes库单元中提供了在二进制流中的文件DFM和它的脚本之相互转化的过程。它们是ObjectBinaryToText和ObjectTextBinary、ObjectResourceToText和ObjectTextToResource。

ObjectBinaryToText过程将二进制流中存储的部件转化为基于文本的表现形式,这样就可以用文本处理函数进行处理,还可以用文本编辑器进行查找和替代操作,最后可以将文本再转化成二进制流中的部件。

  ObjectBinaryToText过程的主程序是这样的:

 

procedure ObjectBinaryToText(Input, Output: TStream);

var

NestingLevel: Integer;

SaveSeparator: Char;

Reader: TReader;

Writer: TWriter;

 

procedure WriteIndent;

const

Blanks: array[0..1] of Char = ' ';

var

I: Integer;

begin

for I := 1 to NestingLevel do Writer.Write(Blanks, SizeOf(Blanks));

end;

 

procedure WriteStr(const S: string);

begin

Writer.Write(S[1], Length(S));

end;

 

procedure NewLine;

begin

WriteStr(#13#10);

WriteIndent;

end;

 

procedure ConvertHeader;

begin



end;

 

procedure ConvertBinary;

begin



end;

 

procedure ConvertValue;

begin



end;

 

procedure ConvertProperty;

begin



end;

 

procedure ConvertObject;

begin



end;

 

begin

NestingLevel := 0;

Reader := TReader.Create(Input, 4096);

SaveSeparator := DecimalSeparator;

DecimalSeparator := '.';

try

Writer := TWriter.Create(Output, 4096);

try

Reader.ReadSignature;

ConvertObject;

finally

Writer.Free;

end;

finally

DecimalSeparator := SaveSeparator;

Reader.Free;

end;

end;

 

  过程中调用的ConvertObject过程是个递归过程,用于将DFM文件中的每一个部件转化为文本形式。因为由于部件的拥有关系,所以部件成嵌套结构,采用递归是最好的方式:

 

procedure ConvertObject;

begin

ConvertHeader;

Inc(NestingLevel);

while not Reader.EndOfList do ConvertProperty;

Reader.ReadListEnd;

while not Reader.EndOfList do ConvertObject;

Reader.ReadListEnd;

Dec(NestingLevel);

WriteIndent;

WriteStr('end'#13#10);

end;

 

  NestStingLevel变量表示部件的嵌套层次。WriteIndent是写入每一行起始字符前的空格,ConvertHeader过程是处理部件的继承标志信息。转换成的头信息文本有两种形式。

  Inherited TestForm1: TTestForm[2]

  或者:

Object TestForm1: TTestForm

 

前者是ffInherited和ffChildPos置位,后面是都没置位。

  ConvertProperty过程用于转化属性。

 

procedure ConvertProperty;

begin

WriteIndent;

WriteStr(Reader.ReadStr);

WriteStr(' = ');

ConvertValue;

WriteStr(#13#10);

end;

 

  WriteIndent语句写入属性名前的空格,WriteStr(Reader.ReadStr)语句写入属性名ConvertValue过程根据属性的类型将属性值转化为字符串,然后写入流中。

  ObjectTextToBinary过程执行的功能与ObjectBinaryToText相反,将TXT文件转换为二进制流中的部件,而且只要TXT文件内容的书写符合DFM脚本语法,ObjectTextToBinary可将任何程序生成的TXT文件转换为部件,这一功能也为DFM 文件的动态生成和编辑奠定了基础。ObjectTextToBinary过程的主程序如下:

 

procedure ObjectTextToBinary(Input, Output: TStream);

var

SaveSeparator: Char;

Parser: TParser;

Writer: TWriter;

 

  …

  

begin

Parser := TParser.Create(Input);

SaveSeparator := DecimalSeparator;

DecimalSeparator := '.';

try

Writer := TWriter.Create(Output, 4096);

try

Writer.WriteSignature;

ConvertObject;

finally

Writer.Free;

end;

finally

DecimalSeparator := SaveSeparator;

Parser.Free;

end;

end;

 

  在程序流程和结构上与ObjectBinaryToText差不多。ConvertObject也是个递归过程:

 

procedure ConvertObject;

var

InheritedObject: Boolean;

begin

InheritedObject := False;

if Parser.TokenSymbolIs('INHERITED') then

InheritedObject := True

else

Parser.CheckTokenSymbol('OBJECT');

Parser.NextToken;

ConvertHeader(InheritedObject);

while not Parser.TokenSymbolIs('END') and

not Parser.TokenSymbolIs('OBJECT') and

not Parser.TokenSymbolIs('INHERITED') do ConvertProperty;

Writer.WriteListEnd;

while not Parser.TokenSymbolIs('END') do ConvertObject;

Writer.WriteListEnd;

Parser.NextToken;

end;

 

  DFM文件与DFM脚本语言之间相互转换的任务由ObjectResourceToText和ObjextTextToResource两个过程完成。

 

procedure ObjectResourceToText(Input, Output: TStream);

begin

Input.ReadResHeader;

ObjectBinaryToText(Input, Output);

end;

 

ObjectTextToResource过程就比较复杂,因为DFM文件资源头中要包含继承标志信息,因此在调用ObjectTextToBinary后,就读取标志信息,然后写入资源头。

 

procedure ObjectTextToResource(Input, Output: TStream);

var

Len: Byte;

Tmp: Longint;

MemoryStream: TMemoryStream;

MemorySize: Longint;

Header: array[0..79] of Char;

begin

MemoryStream := TMemoryStream.Create;

try

ObjectTextToBinary(Input, MemoryStream);

MemorySize := MemoryStream.Size;

FillChar(Header, SizeOf(Header), 0);

MemoryStream.Position := SizeOf(Longint); { Skip header }

MemoryStream.Read(Len, 1);

if Len and $F0 = $F0 then

begin

if ffChildPos in TFilerFlags((Len and $F0)) then

begin

MemoryStream.Read(Len, 1);

case TValueType(Len) of

vaInt8: Len := 1;

vaInt16: Len := 2;

vaInt32: Len := 4;

end;

MemoryStream.Read(Tmp, Len);

end;

MemoryStream.Read(Len, 1);

end;

MemoryStream.Read(Header[3], Len);

StrUpper(@Header[3]);

Byte((@Header[0])^) := $FF;

Word((@Header[1])^) := 10;

Word((@Header[Len + 4])^) := $1030;

Longint((@Header[Len + 6])^) := MemorySize;

Output.Write(Header, Len + 10);

Output.Write(MemoryStream.Memory^, MemorySize);

finally

MemoryStream.Free;

end;

end;

 

20.3.1.5 动态DFM文件应用揭秘

 

  1. 动态DFM文件概述

动态DFM文件是相对于静态DFM文件而言。所谓静态DFM文件是指在Delphi开发环境中设计的窗体文件。窗体的设计过程就是程序的编制过程。因此,动态DFM文件就是指在程序运行过程生成或存取的DFM文件。

  动态DFM文件的创建和使用分别如下两种情况:

  ● 在程序运行过程中,由Create方法动态生成窗体或部件,然后动态生成其它部件插入其中生成DFM文件

  ● 在Delphi开发环境中,设计生成DFM文件,然后用DFM 文件存取函数,或者用Stream对象和Filer对象的方法,将DFM文件读入内存,进行处理,最后又存入磁盘中

 

  由Delphi的窗体设计的常规方法生成的DFM文件在程序运行一开始就规定了部件的结构。因为在窗体设计过程中,窗体中的每个部件都在程序的对象声明中定义了部件变量。这种固定的结构虽然能方便应用,但以牺牲灵活性为代价。

  在Delphi应用程序中有时需要在运行过程中创建控制,然后将该控制插入另一个部件中。例如:

 

procedure TForm1.Button1Click(Sender: Tobject);

var

Ctrl: TControl

begin

Ctrl := TEdit.Create(Self);

Ctrl.Top := 100;

Ctrl.Left := 100;

Ctrl.Width := 150;

Ctrl.Height := 20;

InsertControl(Ctrl);

end;

 

  动态插入控制的优点是可以在任何时刻、任意位置插入任意数量的任何类型的控制。因为应用程序需求在很多情况下是在程序运行中才知道的,所以动态插入控制就显得很重要。而且在很多情况下,需要保存这些界面元素,留待程序再次调用。例如应用程序界面的定制、系统状态的保存、对话框的保存等。这时生成动态DFM文件是最佳选择。

  动态插入控制的不足之处是在插入控制前,无法直观地看到控制的大小、风格、位置等,也就是动态插入控制的过程是非可视化的。但可以借助于静态DFM文件的可视化设计。这就是生成和使用动态DFM文件的第二种方法。也就是在应用程序运行前,在Delphi开发环境中,使用可视化开发工具设计所需窗口或部件的样式,以DFM文件保存。然后在应用程序运行过程中,将DFM文件读入内存。Delphi的Stream对象和Filer对象在读取DFM文件时,会根据DFM文件的内容自动创建部件及其拥有的所有部件。

  在使用动态DFM文件时有两点需要注意。

 ● 每一个动态插入的控制或部件必须在程序中调用RegisterClass进行注册

  ● 读入DFM文件自动创建部件后,如果调用了InsertControl方法, 则在关闭窗口时要调用RemoveControl方法移去该控制,否则会产生异常事件

 

  2. 动态DFM文件应用之一:超媒体系统的卡片设计

  Delphi多种类型的可视部件,如文本部件、编辑部件、图形图像部件、数据库部件、媒体媒放部件和OLE部件等,每一种部件在屏幕中占据一定的区域,具有相当丰富的表现能力,可以作为卡片中的一种媒体,因此可以利用这些可视部件进行超媒体系统的卡片设计。

  超媒体卡片设计要求卡片中的媒体数目和媒体种类是不受限制的,而且必须能够修改和存取卡片,因此,采用动态DFM文件是比较合适的。而且如果利用Stream对象,将卡片存储在数据库BLOB字段中,就为把超文本与关系数据库技术结合起来创造了契机。

  下面是超媒体卡片设计子系统中的部分源程序,它演示了如何创建对象、插入对象和存取动态DFM文件。

  ⑴ 在应用程序中注册对象

 

procedure TMainForm.FormCreate(Sender: TObject);

begin

RegisterClass(TLabel);

RegisterClass(TEdit);

RegisterClass(TMemo);

RegisterClass(TButton);

RegisterClass(TPanel);

RegisterClass(TPanelP);

RegisterClass(TBitBtn);



end;

 

⑵ 创建和插入对象

 

procedure T M DIChild.FormClick(Sender: TObject);

var

Ctrl : TControl;

Point: TPoint;

begin

GetCursorPos(Point);

Point := BackGround.ScreenToClient(Point);

case CurToolIndex of

1 : begin

Ctrl := TLabel.Create(self);

TLabel(Ctrl).AutoSize := False;

TLabel(ctrl).Caption := 'Label'+S;

TLabel(ctrl).Name := 'Label 1';

TLabel(ctrl).Top := Point.Y;

TLabel(ctrl).Left := Point.X;

TLabel(Ctrl).Height := Round(100*Res/1000/Ratio);

TLabel(Ctrl).Width := Round(600*Res/1000/Ratio);

TLabel(Ctrl).Color := clWhite;

TLabel(Ctrl).Font.Color := clBlack;

TLabel(Ctrl).Font.Name := 'Roman';

TLabel(Ctrl).Font.Height := -TLabel(Ctrl).Height;

TLabel(Ctrl).Font.Pitch := fpFixed;

TLabel(Ctrl).Enabled := False;

TLabel(Ctrl).OnClick := LabelClick;

TLabel(Ctrl).OnMouseMove := ReportPos;

BackGround.InsertControl(Ctrl);

CurTool.Down := False;

CurTool := nil;



end;

2: begin

Ctrl := TEdit.Create(self);

TEdit(ctrl).AutoSize := True;

TEdit(ctrl).Top := Point.Y;

TEdit(ctrl).Left := Point.X;

TEdit(Ctrl).Height := 20;

BackGround.InsertControl(Ctrl);



end;

3:



end;

end;

  

  ⑵ 存取动态DFM文件

 

procedure TMainForm.FileOpen(Sender: TObject);

begin

if OpenDialog.Execute then

begin

DesignWin := T M DIChild.Create(Application);

ReadComponentResFile(OpenDialog.FileName, DesignWin);

DesignWin.Init;

FileName := OpenDialog.FileName;

DesignWin.Caption := FFileName;

end;

end;

 

  DesignWin是在TMainForm中定义的T M DIChild类型的窗体部件,是卡片设计平台;FFileName是私有变量,用来保存当前编辑的卡片文件名。DesignWin的Init方法实现如下:

 

procedure T M DIChild.Init;

var

I: Integer;

Ctrl: TControl;

begin

BackGround.BringToFront;

with BackGround do

for I:= 0 to ControlCount - 1 do

if Controls[I].Name <> ''then

ObjectIns.ObjectList.Items.AddObject(Controls[I].Name, Controls[I]);

end;

 

  BackGround是TPanel类型的部件,所有的动态创建对象都插入到BackGround中,所以,后面调用BackGround.InsertControl(Ctrl);ObjectIns是个仿Delphi 的媒体属性编辑器。

  动态DFM文件的存储过程是这样的:

 

procedure TMainForm.FileSave(Sender: TObject);

begin

if DesignWin.CurControl <> nil then

DesignWin.CurControl.Enabled := True;

WriteComponentResFile(FFilename, DesignWin);

DesignWin.Caption := FileName;

end;

end;

 

  因为在DesignWin的Init方法中调用了InsertControl方法,所以在关闭DesignWin窗口时要相应地调用RemoveControl,否则在关闭DesignWin窗口时会产生内存错误。

 

procedure T M DIChild.FormCloseQuery(Sender: TObject; var CanClose: Boolean);

var

I: Integer;

Ctrl: TControl;

Removed: Boolean;

begin

if Modified = True then

if MessageDlg('Close the form?', mtConfirmation,

[mbOk, mbCancel], 0) = mrCancel then

CanClose := False;

if CanClose = True then

begin

repeat

removed := False;

I := 0;

repeat

if BackGround.Controls[I].Name <> '' then

begin

BackGround.RemoveControl(BackGround.Controls[I]);

Removed := True;

end;

I := I + 1

until (I >= BackGround.ControlCount) or (Removed = True);

until (Removed = False);

SendMessage(ObjectIns.Handle, WM_MDICHILDCLOSED, 0, 0);

end;

end;

 

  3. 动态DFM文件应用之二:超媒体系统脚本语言设计

  超媒体脚本语言设计是超媒体系统设计的重要内容。脚本语言必须能够表达卡片中的多种媒体对象,必须是可编程,可理解的,必须是可执行的,应该可以由脚本语言生成超媒体系统中的卡片和链。

  DFM文件可以看作是超媒体系统的卡片,DFM脚本能够表达DFM文件中的多种控制,也就是说能够表达卡片中的多种媒体对象,再加上DFM脚本的对象式表达,可编辑性,可转换为DFM文件,因此用作超媒体系统脚本语言较好的形式。

  ObjectBinaryToText和ObjectTextToBinary过程提供了在部件和DFM脚本之间相互转化的功能,ObjectResourceToText和ObjectTextToResoure过程提供了DFM文件和DFM脚本之间相互转化的功能。这样就可以在应用程序中自如实现超媒体卡片和超媒体脚本语言相互转化。

 

  下面是卡片和脚本语言相互转化的程序:

 

procedure T M DIChild.CardToScript;

var

In, Out: TStream;

begin

In := TMemoryStream.Create;

Out := TMemoryStream.Create;

try

In.WriteComponentRes(Self.ClassName, Self);

ObjectResourceToText(In, out);

ScriptForm.ScriptEdit.Lines.LoadFromStream(Out);

finally

In.Free;

Out.Free;

end;

end;

 

  ScriptEdit是个文本编辑器,它的Lines属性是TStrings类型的对象。

 

procedure TScriptForm.ScriptToCard;

var

In, Out: TStream;

begin

In := TMemoryStream.Create;

Out := TMemoryStream.Create;

try

ScriptForm.ScriptEdit.Lines.SaveToFromStream(In);

ObjectTextToResource(In, out);

In.ReadComponentRes(DesignWin);

finally

In.Free;

Out.Free;

end;

end;

 

  这两段程序是对整个卡片,即窗体级,进行转换的。ObjectBinaryToText和ObjectTextToBinary过程可以细化到部件级的转换。因此超媒体脚本语言的编辑可以细化到媒体对象级。

  4. 超媒体编辑和表现系统与动态DFM文件的扩展

  超媒体系统的媒体编辑与卡片管理有其特殊的需求,比如链接需求。这时采用已有的窗体部件和媒体部件并按常规的DFM文件处理就显得力不从心了。解决这个矛盾有两套方案:

  ● 利用Delphi部件开发技术,继承和开发新的部件增加新的超媒体特有的属性和处理方法

  ● 扩展DFM文件结构,使之能按自己的需要任意地存取和转换部件和DFM文件

 

  前者是充分利用Delphi的面向对象部件开发技术,在存取和转换等处理上仍旧与常规DFM文件相同。而后者需要DFM的存取和转换上作比较大的改动。下文介绍扩展DFM文件的思路。

  扩展动态DFM文件的总体思路是降低处理操作的数据的颗粒度,即从原先窗体级降低到部件级。

  下面是存取操作的扩展示范:

 

  var

FileStream: TStream;

I: Integer;

begin

FileStream := TFileStream.Create('OverView.Crd', fmOpenWrite);

With TWriter.Create(FileStream, 4096) do

try

for I := 0 to DesignWin.ControlCount - 1 do

begin

WriteInteger(MMID);

WriteRootComponent(DesignWin.Controls);

{ 写相应媒体扩展信息 }

  ……

   end;

WriteListEnd;

finally.

Free;

end;

FileStream.Free;

end;

 

WriteInteger(MMID)语句是写入媒体标识。

  下面是相应的读扩展DFM的程序:

 

  var

PropInfo: PPropInfo;

Method : TMethod;

FileStream: TStream;

I: Integer;

begin

FileStream := TFileStream.Create('OverView.Crd', fmOpenRead);

With TReader.Create(FileStream, 4096) do

try

while not EndOfList do

begin

case ReadInteger of

IDText: begin

Ctrl := TControl(ReadRootComponent(nil));

PropInfo := GetPropInfo(Ctrl.ClassInfo, 'OnClick');

Method.Code:= Self.MethodAddress(MethodName);

Method.Data := Self;

if Method.Code <> nil then

SetMethodProp(Ctrl, PropInfo, Method);

DesignWin.InsertControl(Ctrl);

end;

IDImage:

  ……

  end;

   ……

WriteListEnd;

end;

finally.

Free;

end;

FileStream.Free;

end;

 

  SetMethodProp过程是用于重新联接控制和它的事件处理过程。类似的功能还可以用TReader对象的OnFindMethod事件的处理过程来实现。

  实现脚本语言扩展的基本方法与存取扩展类似,但它还要加扩展媒体信息转换为文本,并插入到部件的脚本描述中。

 

20.3.2 数据库BLOB字段应用

 

  Delphi VCL提供了TBlobStream对象支持对数据库BLOB字段的存取。Delphi 的TBlobStream对象的作用在于一方面可以使Delphi应用程序充分利用多媒体数据库的数据管理能力。另一方面又能利用Delphi Object Pascal的程序设计能力给关系型多媒体数据库提供底层控制能力和全方位的功能扩展余地。

 

20.3.2.1 TBlobStream的使用

 

  TBlobStream对象用一个TBlobField类型的对象作为参数来创建与BLOB字段相联的BLOB流,接着就可用流的存取方法在BLOB字段中存取数据。

 

  var

BlobStream: TBlobStream;

I: Integer;

begin

BlobStream := TBlobStream.Create(TBlobField(CardTable.Fields[10], bmWrite);

With TWriter.Create(BlobStream, 4096) do

try

for I := 0 to DesignWin.ControlCount - 1 do

begin

WriteInteger(MMID);

WriteRootComponent(DesignWin.Controls);

{ 写相应媒体扩展信息 }

  ……

   end;

WriteListEnd;

finally.

Free;

end;

BlobStream.Free;

CardTable.Post;

end;

 

  Fields变量是表示数据库记录的字段数组,Fields[10]正是数据库的BLOB 字段。CardTable的Post方法将数据库的修改反馈到数据库的物理存储上。

  上面这段程序是超媒体卡片存储的部分源程序,我们就是将卡片保存在数据库BLOB字段中,实现将超文本和关系数据库两种数据管理方式结合起来。读卡片的程序如下:

 

  var

PropInfo: PPropInfo;

Method: TMethod;

Blobtream: TStream;

I: Integer;

begin

BlobStream := TBlobStream.Create(TBlobField(CardTable.Fields[10]), bmRead);

With TReader.Create(BlobStream, 4096) do

try

while not EndOfList do

begin

case ReadInteger of

IDText: begin

Ctrl := TControl(ReadRootComponent(nil));

PropInfo := GetPropInfo(Ctrl.ClassInfo, 'OnClick');

Method.Code:= Self.MethodAddress(MethodName);

Method.Data := Self;

if Method.Code <> nil then

SetMethodProp(Ctrl, PropInfo, Method);

DesignWin.InsertControl(Ctrl);

end;

IDImage:

  ……

  end;

   ……

WriteListEnd;

end;

finally.

Free;

end;

FileStream.Free;

end;

 

20.3.2.2 BLOB字段与图形图像

 

  在多媒体数据库中处理得比较多的是图形图像,因此早期的多媒体数据库在扩展关系数据库时往往是增加一个图像字段。BLOB字段是以二进制数据存储方式,因此它完全可以表达图形图像数据。

  在TBlobField对象中提供了LoadFromBitMap和SaveToBitMap方法存取位图数据。它们在实现上都是使用BlobStream对象。

 

procedure TBlobField.LoadFromBitmap(Bitmap: TBitmap);

var

BlobStream: TBlobStream;

Header: TGraphicHeader;

begin

BlobStream := TBlobStream.Create(Self, bmWrite);

try

if (DataType = ftGraphic) or (DataType = ftTypedBinary) then

begin

Header.Count := 1;

Header.HType := $0100;

Header.Size := 0;

BlobStream.Write(Header, SizeOf(Header));

Bitmap.SaveToStream(BlobStream);

Header.Size := BlobStream.Position - SizeOf(Header);

BlobStream.Position := 0;

BlobStream.Write(Header, SizeOf(Header));

end else

Bitmap.SaveToStream(BlobStream);

finally

BlobStream.Free;

end;

end;

 

procedure TBlobField.SaveToBitmap(Bitmap: TBitmap);

var

BlobStream: TBlobStream;

Size: Longint;

Header: TGraphicHeader;

begin

BlobStream := TBlobStream.Create(Self, bmRead);

try

Size := BlobStream.Size;

if Size >= SizeOf(TGraphicHeader) then

begin

BlobStream.Read(Header, SizeOf(Header));

if (Header.Count <> 1) or (Header.HType <> $0100) or

(Header.Size <> Size - SizeOf(Header)) then

BlobStream.Position := 0;

end;

Bitmap.LoadFromStream(BlobStream);

finally

BlobStream.Free;

end;

end;

 

程序中按两种方式存取数据,对于位图数据,数据的起点是流的Potition为0处,对于图形或其它类型的Blob数据,则以流的Position为SizeOf(Header) + 1处开始, 即多了个头信息。

 

20.3.2.3 BLOB字段与文本

 

  Delphi BLOB字段中增加了大型文本的处理能力。可以在TBlobField和Strings中自由地交换数据。

 

procedure TBlobField.LoadFromStrings(Strings: TStrings);

var

BlobStream: TBlobStream;

begin

BlobStream := TBlobStream.Create(Self, bmWrite);

try

Strings.SaveToStream(BlobStream);

finally

BlobStream.Free;

end;

end;

 

procedure TBlobField.SaveToStrings(Strings: TStrings);

var

BlobStream: TBlobStream;

begin

BlobStream := TBlobStream.Create(Self, bmRead);

try

Strings.LoadFromStream(BlobStream);

finally

BlobStream.Free;

end;

end;

 

20.3.2.4 BLOB字段与Stream对象

 

  因为Delphi中,BLOB字段是通过BLOB流来访问的,所以可以很容易地在BLOB字段和Stream对象之间传递数据。为此,TBlobField对象提供了LoadFromStream和SaveToStream方法。

 

procedure TBlobField.LoadFromStream(Stream: TStream);

var

BlobStream: TBlobStream;

begin

BlobStream := TBlobStream.Create(Self, bmWrite);

try

BlobStream.CopyFrom(Stream, 0);

finally

BlobStream.Free;

end;

end;

 

procedure TBlobField.SaveToStream(Stream: TStream);

var

BlobStream: TBlobStream;

begin

BlobStream := TBlobStream.Create(Self, bmRead);

try

Stream.CopyFrom(BlobStream, 0);

finally

BlobStream.Free;

end;

end;

 

20.3.3 存取嵌入在OleContainer对象中的OLE服务器的数据

 

  对象链接和嵌入(Object Linking and Embedding,简称OLE),是一组服务功能,它提供了一种用来源于不同应用程序的信息创建复合文档的强有力方法。

  通过把图像、图形、表格、声音、注解、文件和其它表示手段描述成对象,用它能在不同软件厂家提供的应用程序中更为容易地交换合成和处理数据它是应用程序的集成更为容易。OLE2.0支持直观编辑。用户不需切换到不同窗口就能在文档中直接对对象进行操作,改进了操作环境。用户不用再关注应用程序和操作环境,只需关注于使用对象技术的数据和文件,便能完成全部工作。

  OLE已成为操作系统功能上的一大标准,各大软商纷纷在开发工具中支持OLE 2.0规范。Delphi 2.0提供了OleContainer对象支持OLE窗户应用程序的开发。

  尽管通过OLE可以用来源于不同应用程序的信息创建复合文档,充分体现以任务、以文档为中心的思想,但是很难分解来自其它应用程序中的嵌入数据,以进行特殊的处理。

  例如,一套多媒体电子文档管理系统,系统需要数据库管理功能文档编辑功能,全文检索功能等。在文档编辑功能的实现上,如果能利用中文Word 或写字板之类的强大的编辑排版功能,就可以省却重新开发一个文档编辑的费用,使用具有直观编辑的OLE复合文档嵌入Word的DOC数据或RTF数据当然是最佳的选择。 但问题在于全文检索系统要求能直接在文档中搜索关键字,因此要求将文档数据从OLE嵌入数据或文档中的本地数据中分离出来。

  Delphi 2.0的OleContainer部件支持存储OLE对象数据。OLE对象数据包括两部分:OLE类描述信息和OLE服务器嵌入数据。一般说来,OLE服务器嵌入数据是以服务器支持的数据格式存储的; 比方说,中文Word 6.0的嵌入数据的格式就是Word 6.0文档的格式。因此,要将文档数据从OLE 嵌入式文档中分离出来就是要访问第二部分数据。

我们分析了Delphi 2.0的OleContainer对象存取复合文档的程序,得到分离数据的方法。

  让我们来看一段OleContainer对象存储数据的程序:

 

procedure TOleContainer.SaveToStream(Stream: TStream);

var

DataHandle: HGlobal;

Buffer: Pointer;

Header: TStreamHeader;

R: TRect;

 ……

begin

   ……

try

   ……

if FOldStreamFormat then

begin

R := BoundsRect;

Header.PartRect.Left := R.Left;

Header.PartRect.Top := R.Top;

Header.PartRect.Right := R.Right;

Header.PartRect.Bottom := R.Bottom;

end else

begin

Header.Signature := StreamSignature;

Header.DrawAspect := FDrawAspect;

end;

Header.DataSize := GlobalSize(DataHandle);

Stream.WriteBuffer(Header, SizeOf(Header));

Buffer := GlobalLock(DataHandle);

try

Stream.WriteBuffer(Buffer^, Header.DataSize);

finally

GlobalUnlock(DataHandle);

end;

finally

ReleaseObject(TempStorage);

ReleaseObject(TempLockBytes);

end;

end;

 

程序中,OleContainer对象执行了两次往流中写数据的操作。

   Stream.WriteBuffer(Header, Size(Header));

Stream.WriteBuffer(Buffer^, Header.DataSize);

 

前一语句是写入OLE类描述信息,后一句语句是写入OLE服务器的嵌入数据。Header是TStreamHeader记录类型的变量。TStreamHeader记录的定义如下:

 

TStreamHeader = record

case Integer of

0: ( { 新版OLE对象 }

Signature: Integer;

DrawAspect: Integer;

DataSize: Integer);

1: ( { 旧版OLE对象 }

PartRect: TSmallRect);

end;

 

  因此读OLE服务器嵌入数据时,要跳过文件头的TStreamHeader记录。下面就是如何分离OLE服务器嵌入数据的程序:

 

var

Stream : TMemoryStream;

FileStream : TFileStream;

begin

Stream := TMemoryStream.Create;

FileStream := TFileStream.Create('TEST.DOC', fmCreate) ;

with OleContainer1 do

if (State <> osEmpty) then

SaveToStream(Stream);

Stream.Seek(Sizeof(TStreamHeader), 0);

FileStream.CopyFrom(Stream, Stream.Size - SizeOf(TStreamHeader));

Stream.Free;

FileStream.Free;

end;

 

OleContainer1包含的服务器对象是中文Word 6.0,程序中将分离出的数据存储在磁盘文件“TEST.DOC”上。如果希望存储在不同的媒介上,可以使用相应的Stream对象,分离的方法类似。但是,这种方法并非对所有的OLE服务器数据都适用,如Windows 95 附件中的写字板(WordPad)就不行。


想死你们了!

TOP