编程先锋 http://wlbookwl.myrice.com

制作：小黑侠
E-mail:wl3000wl@163.com
OICQ:46393567

此电子书籍由编程先锋整理制作

声明：本站所制作的书籍仅供个人学习,交流,研究之用。
版权归原作者和原出版物所有。


目录

DELPHI基础教程

目录

    第一章　Delphi快速入门（一）
    第一章　Delphi快速入门（二）
    第一章　Delphi快速入门（三）
    第一章　Delphi快速入门（四）
    第一章　Delphi快速入门（五）
    第二章　Delphi面向对象的编程方法（一）
    第二章　Delphi面向对象的编程方法（二）
    第二章　Delphi面向对象的编程方法（三）
    第二章　Delphi面向对象的编程方法（四）
    第三章　字符串列表及应用（一）
    第三章　字符串列表及应用（二）
    第四章　文本编辑器的设计（一）
    第四章　文本编辑器的设计（二）
    第五章　Delphi图形图像编程（一）
    第五章　Delphi图形图像编程（二）
    第六章　文件管理（一）
    第六章　文件管理（二）
    第六章　文件管理（三）
    第七章　剪贴板和动态数据交换（一）
    第七章　剪贴板和动态数据交换（二）
    第八章　对象链接与嵌入（一）
    第八章　对象链接与嵌入（二）
    第九章　Delphi拖放编程
    第十章　动态链接库编程（一）
    第十章　动态链接库编程（二）
    第十一章　Delphi应用程序的应用（一）
    第十一章　Delphi应用程序的应用（二）
    第十二章　异常处理与程序调试（一）
    第十二章　异常处理与程序调试（二）
    第十二章　异常处理与程序调试（三）
    第十三章　Delphi开发数据库应用程序概述（一）
    第十三章　Delphi开发数据库应用程序概述（二）
    第十四章　简单数据库应用的创建及MASTAPP介绍（一）
    第十四章　简单数据库应用的创建及MASTAPP介绍（二）
    第十四章　简单数据库应用的创建及MASTAPP介绍（三）
    第十五章　数据访问部件的应用及编程（一）
    第十五章　数据访问部件的应用及编程（二）
    第十五章　数据访问部件的应用及编程（三）
    第十六章 数据浏览部件的应用及编程（一）
    第十六章 数据浏览部件的应用及编程（二）
    第十七章　SQL编程（一）
    第十七章　SQL编程（二）
    第十八章　Delphi客户服务器应用开发（一）
    第十八章　Delphi客户服务器应用开发（二）
    第十八章　Delphi客户服务器应用开发（三）
    第十八章　Delphi客户服务器应用开发（四）
    第十九章　Delphi自定义部件开发（一）
    第十九章　Delphi自定义部件开发（二）
    第十九章　Delphi自定义部件开发（三）
    第十九章　Delphi自定义部件开发（四）
    第二十章　开发Delphi对象式数据管理功能（一）
    第二十章　开发Delphi对象式数据管理功能（二）
    第二十章　开发Delphi对象式数据管理功能（三）
    第二十章　开发Delphi对象式数据管理功能（四）
    第二十章　开发Delphi对象式数据管理功能（五）

  [下一页]


第一章 Delphi快速入门（一）

DELPHI基础教程

第一章　Delphi快速入门（一）

Delphi是全新的可视化编程环境，为我们提供了一种方便、快捷的Windows应用程序开发工具。它使用了Microsoft Windows图形用户界面的许多先进特性和设计思想，采用了弹性可重复利用的完整的面向对象程序语言(Object-Oriented Language)、当今世界上最快的编辑器、最为领先的数据库技术。对于广大的程序开发人员来讲，使用Delphi开发应用软件，无疑会大大地提高编程效率，而且随着应用的深入，您将会发现编程不再是枯燥无味的工作——Delphi的每一个设计细节，都将带给您一份欣喜。

1.1 Delphi基本概念介绍

1.1.1 Delphi的基本形式

Delphi实际上是Pascal语言的一种版本，但它与传统的Pascal语言有天壤之别。一个Delphi程序首先是应用程序框架，而这一框架正是应用程序的“骨架”。在骨架上即使没有附着任何东西，仍可以严格地按照设计运行。您的工作只是在“骨架”中加入您的程序。缺省的应用程序是一个空白的窗体(Form)，您可以运行它，结果得到一个空白的窗口。这个窗口具有Windows窗口的全部性质：可以被放大缩小、移动、最大最小化等，但您却没有编写一行程序。因此，可以说应用程序框架通过提供所有应用程序共有的东西，为用户应用程序的开发打下了良好的基础。Delphi已经为您做好了一切基础工作——程序框架就是一个已经完成的可运行应用程序，只是不处理任何事情。您所需要做的，只是在程序中加入完成您所需功能的代码而已。

在空白窗口的背后，应用程序的框架正在等待用户的输入。由于您并未告诉它接收到用户输入后作何反应，窗口除了响应Windows的基本操作(移动、缩放等)外，它只是接受用户的输入，然后再忽略。Delphi把Windows编程的回调、句柄处理等繁复过程都放在一个不可见的Romulam覆盖物下面，这样您可以不为它们所困扰，轻松从容地对可视部件进行编程。

1.1.2 面向对象编程的概念

面向对象的程序设计(Object-Oriented Programming，简记为OOP)是Delphi诞生的基础。OOP立意于创建软件重用代码，具备更好地模拟现实世界环境的能力，这使它被公认为是自上而下编程的优胜者。它通过给程序中加入扩展语句，把函数“封装”进Windows编程所必需的“对象”中。面向对象的编程语言使得复杂的工作条理清晰、编写容易。说它是一场革命，不是对对象本身而言，而是对它们处理工作的能力而言。对象并不与传统程序设计和编程方法兼容，只是部分面向对象反而会使情形更糟。除非整个开发环境都是面向对象的，否则对象产生的好处还没有带来的麻烦多。而Delphi是完全面向对象的，这就使得Delphi成为一种触手可及的促进软件重用的开发工具，从而具有强大的吸引力。

一些早期的具有OOP性能的程序语言如C++,Pascal,Smalltalk等，虽然具有面向对象的特征，但不能轻松地画出可视化对象，与用户交互能力较差，程序员仍然要编写大量的代码。Delphi的推出，填补了这项空白。您不必自己建立对象，只要在提供的程序框架中加入完成功能的代码，其余的都交给Delphi去做。欲生成漂亮的界面和结构良好的程序丝毫不必绞尽脑汁，Delphi将帮助您轻松地完成。它允许在一个具有真正OOP扩展的可视化编程环境中，使用它的Object Pascal语言。这种革命性的组合，使得可视化编程与面向对象的开发框架紧密地结合起来。

1.2 Delphi 快速入门

在这一节中，我们来开发一个小程序。随着开发的过程，逐步介绍Delphi的主要部件及其操作方法。建议读者按照本书介绍的过程，在您的电脑上直接操作。您将对Delphi的可视化编程有一个直观、快捷的了解，必将起到事半功倍的效果。

1.2.1 进入Delphi的可视化编程环境

1.2.1.1 安装Delphi

Delphi的安装与其它应用软件并无不同。2.0版必须在Windows 95以上的操作系统中使用。启动Windows 95或Windows NT后，将Delphi的光盘放入光驱(CD-ROM)中，运行光盘上的\INSTALL\SETUP.EXE文件，它的安装程序会提示您正确地装入Delphi。如果您是在微软中文Windows环境中安装Delphi，请参照附录A来设置您的BDE环境，以便于处理中文数据。

1.2.1.2 进入Delphi 环境

为避免隐藏在Delphi后的Program Manager和曾经运行过的其它程序扰乱版面，分散您的注意力，不妨在启动Delphi前关掉其它应用程序；启动Delphi后，再最小化隐藏在后面的Delphi 2.0程序组。这样屏幕上就只留下Delphi窗口可见了。

首次加载Delphi，屏幕上会出现四个窗口：

● 标题为“Delphi-Project1”的Delphi主窗口

● Object Inspector窗口

● 标题为“Form1”的窗体(Form)窗口

● 标题为“Unit1.PAS”的代码编辑窗口。刚启动时这一窗口的大部分被“Form1”窗体所掩盖。将“Form1”窗体移开，或单击Form1窗体下方的状态行，可以使其全部可见。在“Form1”窗体的任意可见位置单击鼠标，可以恢复主窗体可见

以下我们将对这四个窗口分别进行介绍。

1.2.2 Delphi可视化编程环境介绍

1.2.2.1 主窗口(Main Form)

Delphi的主窗口位于屏幕的上端，包括Menu(菜单)、Speed Bar(加速条)和Component Panel(部件选项板)。Menu是下拉式主菜单。Speed Bar位于主窗口的左下端，由两排共14个加速按钮组成。这些按钮是菜单功能的快捷方式，各种图标直观地表示了它能执行的动作。Component Panel由一行、若干页对象按钮所组成，利用它来选择需要的部件并将它放到窗体中去。

1.2.2.2 Object Inspector(对象检视器)

Object Inspector窗口含有两页：Properties页显示窗体中当前被选择部件的属性信息，并允许改变对象的属性；Events页列出了当前部件可以响应的事件。按动Object Inspector下端的“Events”页标签，使得Events页可见，这一定的事件后边的空白处，可以定义对象接受到相应事件时执行的动作。首次启动时，Object Inspector窗口显示的是当前窗体Form1的属性。Object Inspector根据对象属性的多少，决定是否有滚行显示。移动滚行条，可以查看当前对象的全部属性。

此外，Object Inspector上还有Object Selector(对象选择器)，位于Object Inspector上方的下拉式菜单中。它显示了窗体上所有部件的名称和类型，也包含窗体本身。您可以用Object Selector很容易地在窗体的各个部件之间切换，也可以快速地回到窗体本身。当窗体中含有较多的对象时，您会发现这是切换对象尤其是回到窗体的最快捷途径。

想使Object Inspector一直可见，可将鼠标移到Object Inspector上，按动右键，以启动Object Inspector的弹出式菜单，将其设置为Stay On Top。这对初学者常是一个很重要的设置方式。

1.2.2.3 窗体窗口

Forms窗口是开展大部分设计的工作区域。首次启动Delphi 2.0时显示的是窗体Form1。可以把部件放在窗体中，通过移动位置、改变尺寸等操作随心所欲地安排它们，以此来开发应用程序的用户界面。您可以把窗体想象成一个可以放置其它部件的容器。窗体上有栅格(Grids)，供放置部件时对齐位置用，在程序运行时Grids是不可见的。

一个真正的应用程序可能有不止一个窗口，您可以选用不同的窗体进行设计。其它窗体可以是对话框(Dialog Box)、数据录入框等。

1.2.2.4 代码窗口

代码窗口一开始处于窗体窗口之下。因为在Delphi中，设计用户界面直接在窗体中进行，运行结果和设计样板完全一致。当部件被放到窗体上时，Delphi会自动生成大部分的用户界面代码。您所应做的只是在它为您生成的框架中加入完成所需功能的程序段而已。点动Form1的状态行使代码窗口可见。

这个窗口中是代码编辑器。可以在其中书写Delphi应用程序的源代码。当程序中含有不止一个窗口时，会有几个库单元的源程序出现在代码编辑器中。代码编辑器的标题条中显示了当前正在编辑的库单元文件名。要查看某一特定程序的源代码，只需用鼠标点动写有该库单元文件名的页标签，就可以对该库单元进行编辑了。


第一章 Delphi快速入门（二）

DELPHI基础教程

第一章　Delphi快速入门（二）

1.2.3 设计简单的用户界面

首先从空白窗体开始工作。我们将设计一个简单的程序:在屏幕上开一个窗口，窗口中有一个图框，用三个按钮来改变图框的形状；再用一个图标按钮来进行图框的颜色设置，通过颜色编辑对话框来选择变成哪一种颜色。

在菜单上选用File|New Application菜单项来生成新的工程文件。这时，屏幕上出现图1.2所示的窗体。

1.2.3.1 选取部件加入到窗体中

部件(Component)是建立Delphi应用程序的要素。Delphi为用户提供了丰富的部件库，既有可视的部件(如编辑框、按钮)等，也有不可视的部件(如系统定时器、数据表等)。它们按照功能分别排列在Component Panel的各页上。

移动鼠标到Component Panel上，在部件按钮上“犹豫”一、两秒钟，一个黄色小提示框就会弹出，写有该部件的名称，我们称之为提示(Hint)。在要选择的部件上单击左键，则该部件按钮被按下，表示部件已被当前选择。然后，将鼠标移动到窗体上，按下左键，该部件被放到窗体中。部件的轮廓线上会显现八个被称为尺寸调整器(Sizing Handles)的黑色小方块。它除了供用户调整尺寸使用之外，还可以表示该对象处于当前编辑状态。此时，按“Delete”键可以将该部件删除。

在Component Panel上，点动写有“Additional”的页标签，切换到Additional页，再移动鼠标，逐个用观看Hint的办法查看部件的名称，找到“Shape”部件(其图标为圆、方形、三角形三个几何体)，将其放到窗体的左方。

再按动“Standard”页标签，找到“Button”部件(图标上画有OK按钮)。Delphi允许在添加多个同类型的部件时，不必每次到部件选项板上选取。按住“Shift”键，同时在“Button”部件上单击鼠标左键，这时“Button”部件处于按下状态，并有蓝色边框，表示已经被选择固定。依次在窗体右方的三个位置单击左键，则会放置三个相同的Button部件。把鼠标光标移到部件选项板左侧的箭头图标处，这是“取消选择”按钮，它没有提示。按动它，会发现“Button”部件按钮恢复弹起状态。否则，每次“Form1”上的鼠标单击左键动作，都会导致增加一个按钮部件。

再次进入“Additional”部件页，选择一个“BitBtn”按钮(图标是有绿色对号的OK按钮),把它放到其它三个按钮部件的下面。

点动“Dialog”页标签，选择以16色网格做图标的ColorDialog部件并把它放到窗体的任意位置。因为这一部件是不可视部件，所以它的位置并不影响大局。

1.2.3.2 部件的调整与对齐

一般来讲，此时放置到窗体的部件是分散排列的，而且其大小不是真正需要的尺寸。除了不可视的ColorDialog部件外，其它的部件都需要进行位置、大小和显示字样的调整。Delphi提供的对齐工具和窗口栅格为这些调整提供了方便。

1、移动部件

只需把鼠标落到想移动的部件上，按住左键并移动光标，到合适的位置再释放左键，整个部件即被移到新位置。

2、调整尺寸

先把鼠标光标移动到要改变尺寸的部件上，单击左键，选中该部件，尺寸调整器会出现，把鼠标移动到其中的一个小黑色方块上，当鼠标变成拖动方向指示时，按下并拖动鼠标左键，可以放大或缩小部件。上下左右的小方块用来移动对应的各边，四个角的方块可以移动相连的两条边。如果要精确地表述部件的尺寸，可以在Object Inspector上，改变Left(表示部件左边缘到窗体左边框的象素点数)、Top(表示窗体上边框到部件上边缘的象素点数)、 Width(部件本身的宽度)、Height(部件本身的高度)等属性。关于改变部件的属性，下文还将仔细讲解。

3、使得一组部件对齐

用调整位置的方法可以对齐部件，但操作步骤复杂。Delphi提供的对齐工具可以使多个部件的对齐极为迅速方便。下面我们来使四个按钮对齐。先将四个按钮选为一组：按住并向右下方拖动鼠标左键，在窗体上画出围绕四个按钮的矩形，释放左键后，被选中的按钮周边会出现暗灰色的边框。选用Edit|Align命令，或使用弹出式菜单(在被固定的部件上单击鼠标右键激活)，可以显示图1.4所示的对话框。

要使按钮沿左边对齐并使它们在垂直方向上均匀分布，先在Alignment对话框的Horizontal栏内选择“Left sides”，在“Vertical”栏内选择“Space equally”，按动OK按钮，Delphi就会自动将它们对齐。然后，您可以将它们四个作为一组来移动。在四个按钮以外的窗体上按动鼠标左键，就释放了组中的部件，使它们成为分立的部件。

利用对齐模板来对齐部件也是很方便的。首先要将要对齐的部件选成一组，选择View|Alignment Palette显示对齐模板。按照所示的方式选择即可达到对齐的目的

4、锁定部件

如果部件已经对齐，为防止不小心移动部件，可以将部件位置锁定。选择主菜单上的Edit|Lock Controls选项，使得部件不能进行移动操作。解锁只需再次选择此项即可。

调整“Shape”部件的大小，使之与右边的按钮组相匹配。再改变窗体的大小，按住并拖动窗体右下方使之刚好包容窗体上的全部部件。这样，您的用户界面就会比较美观。

1.2.3.3 保存所做的工作

及时地保存所做的工作至关重要。对设计者来讲，有两个文件需要保存：库单元文件(以.PAS为后缀)和工程文件(以.DPR为后缀)。

从主菜单上选择File|Save Project As...项，Delphi会显示标题为“Save Unit1 As”的文件保存对话框，Delphi 2.0 允许用户更改存储路径，您可以在下拉式列表框中选择。最好将您的文件保存在自己的目录中。在编辑框中键入demoform.pas以保存库单元文件；然后显示标题为“Save Project As”的另一个文件保存对话框，键入sample.dpr。Delphi保存这两个文件并返回窗体窗口。不要把库单元和工程存成一样的文件名，Delphi要求两者不同。

第一次保存后，以后可以随时通过Speed Bar中的“Save All”和“Save file”来保存工程文件和库单元文件。一般来讲，当确认文件的改变后，要同时存储这两个文件。

1.2.3.4 运行工程

以上的操作使您有了一个自己的应用程序界面。在速度条中按动“Run”按钮(绘有绿色三角图标)，您可以看到，所生成的界面与您设计的界面是完全一致的。

1.2.4 改变对象的属性

上述的工程虽能够运行，但它对您的按动按钮操作是没有什么反应的，而且，所有部件上还写着我们不需要的字样。双击窗口的关闭按钮结束运行，回到设计界面。下面，我们将仔细讲述如何在Object Inspector中改变部件的各种属性。

1.2.4.1 用Properties页改变部件的属性值

首先要改变各种部件的标题。先给窗口命名为“Demo”。按动Object Inspector上端的Object Selector的题条或者其右端的下拉标志，找到Form1项，并点动左键，窗体被选中。在Object Inspector的Properties页中，找到Caption属性并用左键选中，将其右端的Form1改为Demo,同时，您会发现窗体的标题已经相应地做了改变。

用鼠标点中窗体中的Shape部件，Object Inspector列出了它的属性。选中Shape属性，您会发现右端出现了下拉标志。点动这一标志，可以查看对象的Shape属性可选值。它的形状可以是矩形、圆形、圆角矩形、方形等几种。这是我们设计后续功能的基础。

选中Button1按钮，此时Object Inspector已经显示出此按钮的一应属性。将它的Caption属性改为“&Rectangle”,“&”号使得Delphi特殊处理它后面的字符，在这里，按钮中的R字母被做了下划线处理，运行时，可以用“Alt-R”热键来按动这一按钮。同样，您可以将其它的两个按钮Button2和Button3的Caption属性改成需要的形状指示，譬如“&RoundRec”、“&Ellipse”。

1.2.4.2 设置窗体的缺省按钮

可以把某个按钮作为窗口上的缺省按钮，Delphi会为按钮加上有黑色的边缘。运行时，回车即相当于被按下。只需将此按钮的Default属性从False改成True，即将它设为窗体的缺省按钮。点动Default属性，在右端的值后面双击左键，或从下拉菜单中选取True，即可改变这一属性。Delphi中有许多只有True、False两个属性的部件，双击左键可以在这两个值之间切换。


第一章 Delphi快速入门（三）

DELPHI基础教程

第一章　Delphi快速入门（三）

1.2.4.3 汉化界面及字体选取

如果您的Windows95系统中安装了中文系统，对界面做汉化是极其方便的。例如，您可以将Button1的Caption属性改成“&R矩形”，同样地可改变其它部件的属性，将窗体做成中文的操作界面。

若对中、西文字体不满意，则可以调整Font属性以满足您的要求。Font属性的前面，有一个小小的“+”号，这说明它表征的是集属性，也即属性不再是一个单值，而是一个属性的集合。双击Font，Object Inspector将在下面扩充显示它的其它属性。Color用来表示文本的颜色，Name定义了字体名，如System,MsSerif,Arial,宋体，黑体等。Style下又拥有四个属性：fsBold,fsItalic,fsUnderline和fsTrikeOut。如果想让字体有其中的某种风格，可把相应的属性值设成True。

Font对话框可以更为直接地设定以上属性。Font属性右侧值段有带省略号的按钮，单击它可以激活Font对话框，如图1.7所示：

这一对话框中包含了上述的全部属性，使得调整字形更为直接方便。但要申明的是，对于窗体Form来讲，Font属性改变的是窗体中其它部件的显示字形，它本身的标题字体是缺省的System字体，不能通过属性来改变。

1.2.4.4使用图形编辑对话框

最后，我们用图标按钮BitBtn1来控制Shape部件的颜色。选中图标按钮，将它的Caption属性改为“&Color”(或“&C颜色”)。可以在按钮中加入一个图标来形象地表述它的功能。选用Object Inspector中的Glyph属性，点动值段的三点按钮，弹出图形编辑对话框。

您可以通过图形编辑对话框装入图标，在本例程中，按动Load按钮，选择\Delphi 2.0\Images\Buttons\Brush.bmp文件装入，Bitbtn1就成为一个图标按钮。另外，Delphi还为您提供了丰富的Windows标准图标按钮。想运用它们，改变BitBtn的Kind属性。用户不妨尝试一下，选择其它值可以生成漂亮的标准按钮，如OK,Cancel等等。这会使您在以后的程序开发中受益匪浅。

至此，界面的设计工作就已完成了。运行一下观察效果，别忘记保存您的库单元文件、工程文件。

1.2.5 编写事件处理过程

完成用户界面，只是建立了一个“骨架”，下面要做的便是给程序加入“灵魂”，也即，使它能够完成所要求的功能。

1.2.5.1 为用户操作建立“事件”

单击窗体上的“Rectangle”按钮，在Object Inspector中，点动“Events”页标签，出现事件窗口。在本例程中，我们只关心OnClick事件，即按钮接收到左键单击时应用程序所作出的反应。在Object Inspector窗口中双击OnClick事件右端的值段，会使得Delphi激活库代码编辑器，并将光标停在该按钮所对应的过程的begin...end之间。“Rectangle”按钮的功能是使Shape部件的形状为矩形。联系前文我们查看过的Shape部件的“Shape”属性，现在，我们要做的是在程序中控制部件的属性。

在光标处键入以下的程序段：

    Shape1.Shape := stRectangle;

用同样的方法，在Button2和Button3的OnClick事件响应内复制以上语句，并分别将赋值号后的属性值改为stRoundRect和stEllipse。为防止输入错误导致程序运行出错，建议您使Shape1部件的Object Inspector可见，检视属性Shape的各个值并对应输入。

    在代码编辑器中，Delphi自动建立的过程以关键字Procedure开头，用户可以在begin…end之间加入自己的程序，也可以定义变量。这个程序块对用户的外部事件(此例中是按动按钮)进行响应。我们称之为事件处理过程。

1.2.5.2 使用颜色编辑对话框

图标按钮“Color”要控制部件Shape1的颜色，我们可以调入颜色编辑对话框，用以选择要变成的颜色。这时就要用到运行时不可视的部件ColorDialog了。同样地使图标按钮BitBtn1响应OnClick事件，在它的过程中加入以下的语句：

    ColorDialog1.Execute;

    Shape1.Brush.Color := ColorDialog1.Color;

这样，图标按钮的功能也就完成了。

初学者可能对这样的程序段不能理解。其实也是十分简单的。查看ColorDialog的Brush属性，就会发现它是个集属性，在其下还有一个Color选项，正是我们想改变的属性。既然我们想将图形的颜色变得更加丰富多彩，它本身提供的几种颜色就已不能满足要求了。用颜色编辑器可以得到更多的色彩。程序的第一句用Execute方法，使得ColorDialog运行它本身。当用户在对话框中进行操作，选定了一种颜色时，即使得TColorDialog对象的一个属性Color置为选定颜色的值。将此值赋给Shape1的Brush属性的子属性Color，就将选定的颜色加到了图形上。

读者的要求可能会是，怎样尽快的了解这些这些方法、属性和域呢？下文对使用联机帮助的简单介绍可能会对您有所裨益。

1.2.6 使用联机帮助Help

事实上，使用联机帮助是您全面地了解可用的方法、对象的域、属性等信息的最快捷途径。您还可以参考Delphi给定的例程。较为简单有效的方法是使用Help中的搜索功能。如果您对一个对象知之甚少，选用主菜单中的Help|Help Topics项，在弹出的帮助对话框，选用Indexes(“索引”)页，Delphi 2.0会提示您输入要检索的主题。以您想用的部件的对象名作为检索主题词，可以参阅很多基本的信息。例如，上文的例程中，在编辑框中键入对象的名称TColorDialog，在词条中就会出现该主题，按动Display按钮显示出TColorDialog的帮助信息。查看method,可以找到能应用在CclorDialog1上的方法Execute;仔细阅读它的功能，上述的编程便不难理解了。Properties中提供了部件的所有属性，在关键属性Key Properties前加上了“金钥匙”，提供了详尽的说明。在Task中您可以查阅该对象的其它相关主题，还可以参阅Example中的应用例程。用帮助窗口的Edit各种功能进行复制、剪贴等操作。

如果您已经对对象有一定的了解，想查阅它的具体方法、域等，可以直接用主题词进行检索。

以上，我们已经完成了简单的例程。运行您的程序，选用各个按钮改变图形的颜色和形状。如图1.9所示。虽然它只是一个“小玩具”，却直观地给您一个用Delphi编程的印象。对比一下您亲手编制的程序量和程序的功能，您就会理解Delphi为程序员提供了怎样的方便。

1.3 Delphi 2.0的可视化部件用法简介

本节中，我们将较为深入地讲解Delphi的部件。这是编制大型复杂应用程序的必由之路。下面，我们将对各种部件分类进行简单的介绍，以便于您在编程时能够较快地了解有哪些部件可能会满足您的要求，并对其重要的方法、属性等有一个大致的了解。

    基本技巧、工具的介绍在本章的第4节中，交互阅读这两部分，多创制几个试验程序，有助于您迅速进入Delphi世界，尽早将这一精妙软件用于实际工作中。

1.3.1 常用的文本相关部件

以下的部件以各种格式显示文本，其中有些是文本和数据输入的途径。它们是Label(标签)、Edit(文本编辑)、MaskEdit(格式编辑)、Memo(备注)、List Box(列表框)、Combo Box(组合框)。除了MaskEdit在Additional页之外，其它的都在Standard页中。

1.3.1.1 Label部件

Label(标签)一般放在对象的旁边，用来标记这些对象，从而对用户的操作进行提示，也可以用来显示其它信息。您可以在标签中设置热键，也即在Caption属性值段中输入含有“&”的字串，当用户使用“Alt+关键字母”时，将自动选中它所指向的对象。方法是设置Label部件的FocusControl属性，在值段中，选用与它关联对象的对象名。

1.3.1.2 Edit、MaskEdit和Memo部件

Edit、MaskEdit、Memo部件都是用作接收、显示用户输入文本的。它们具有一些相同的属性。ReadOnly在运行时间内控制对象是否可以进行Windows的操作，当此值为False时，该框内的文本就不能被复制到剪贴板上。MaxLength可以设置输入文本的长度限制。用PasswordChar属性可以按照显示隐蔽密码的方法显示用户输入文本，例如，它的缺省值为“*”，运行时，您的输入将用“*”来显示，从而提供了一种安全措施。您可能也发现了，当一个字段被加上高亮度显示时，按键操作会将这一字段删除，替换成当前的键盘输入。这种设置为操作提供了方便，您不必每次先删除原来的文本；但也可能会导致误删文本。将AutoSelect属性设置成False，这种替代功能就被取消了。

MaskEdit是格式文本输入对象。它的EditMask属性为它提供了过滤文本的格式。点动这一属性的省略按钮，会弹出过滤编辑对话框，除了Delphi为您提供的几种屏蔽格式，您也可以自己编写，查阅“帮助”，会为您提供更详细的用法介绍。

Memo是备注框，与以上对象不同的是，它可以接收多行文本输入。将ScrollBars设置成ssVertical，可以为它加上一个垂直的滚行条。Align属性调整该对象在窗口中的对齐情况，有alNone(无对齐指定)、alBottom(底部对齐)、alClient(全窗口显示)等可以选择；而Alignment属性则决定了文本在框中的对齐显示格式。Lines属性访问的文本被存储在一个TStrings对象中，按动它的省略按钮，可以通过对话框向它增加文本，也可以用程序对这一属性进行操作，以达到修改或增加备注文本的目的。

1.3.1.3 List Box和Combo Box部件

List Box(列表框)和Combo Box(组合框)都显示列表项目，所不同的是组合框占用较少的空间。常见的Windows操作系统中，显示可用磁盘驱动器时，用的就是组合框。列表框则提供了一个项目列表以供选择，如Windows打开文件操作时显示文件列表的就是List Box。

    下列程序段将Edit1的文本加入到ListBox1中，并清空Edit1中的文本：

  procedure TForm1.AddButtonClick(Sender:TObject);

  begin

ListBox1.Items.Add(Edit1.text);

Edit1.Text := ‘’;

  end;

常见的组合框初始化用以下的语句可以实现，它用下拉式列表框的第一项来初始化组合框的缺省值：

procedure TForm1.ComboBox1Text(Sender:TObject);

begin

if ComboBox1.Text = ‘’then

ComboBox1.Text := ComboBox1.Items.String[1] ;

      end;

以上就是文本输入的基本部件。

1.3.2 按钮和检查框部件

1.3.2.1 Button和BitBtn部件

按钮是Windows常用的部件，前文的例程中我们已经基本掌握了Button和BitBtn的主要用法。它们一般用在对话框中，做为执行某种功能的指示。您也可以为它们设置内置返回功能，将ModelResult属性加以匹配，可以不用编程，直接实现系统内置的功能，这一点上和Kind属性颇类似。

1.3.2.2 Speed Button部件

Speed Button(加速按钮)部件在Additional页上，是进行工具条快速设计的理想部件。它只有一个位图，没有标题。需要提起读者注意的是，用多个Speed Button制作一个工具条时，必须先放置一个窗口类部件，如Panel(操作板)、Group Box(群组框)等。否则试图将加速按钮先制作好再移上去，您会发现它会总是处于窗口类部件下面不可见。当然，您可以对加速按钮进行复制、粘贴到窗口类部件上处理，那就另当别论了。

1.3.2.3 Check Box与Radio Button部件

Check Box(检查框)和Radio Button(无线按钮)部件通常是相提并论的，多用作接收用户“是”或“否”的输入判别。它们一般都是成组的放置在Radio Group或Group Box部件中。所不同的是，成组的Radio Button是“互锁”的，用户选择定一个后，其它的将自动设置为不选；而Check Box部件则是分立的，您可以同时选中其中的几个，也可以一个都不选。

1.3.3 分组、分界部件

分组部件把其它的部件组合成一组，例如上述的无线按钮部件Radio Button，就需要放入以下的成组部件中。分界部件用于在窗口中产生边界和区域，以区别于其它区域。

1.3.3.1 Group Box、Radio Group及Panel部件

Group Box(群组框)、Radio Group(选项按钮组)部件都可以将部件分类、成组。它们都有标题，可以用文字表征成组部件的标题或信息。Radio Group可以进行Column和Item属性的设定，以决定其上的无线按钮的提示文本及显示格式。Panel部件也可以达到将部件分组的目的。通过编程向窗口加一个操作板部件，以书写提示和帮助信息，也不失为一种好方法。以上三个部件都在Standard页。

1.3.3.2 NoteBook、TabSet及TabbedNoteBook部件

记事本部件NoteBook一般和标签集部件TabSet共同使用，用来创制含有标签的重叠多窗体窗口。Pages属性包含了一个页名列表，在编程时，将TabSet的Tabs属性设置成NoteBook的Pages属性，就可以使页标签和相应的窗体对应起来。

另外一个部件TabbedNoteBook是带有标签的多窗体窗口，不过，它的页标签设在窗口的上面。它们相当于多个分组部件的集合，每次查看其中的一页。以上三个部件在Win 3.1页。

在Win95页上还有TabControl、PageControl等部件。它们对于生成Windows 95风格的标签集是很重要的。

1.3.3.3 分界部件

在Win 3.1页上，还有Header(表头)部件，它在窗口中产生一个凸起的题条，提供了一个可视化的文本显示区域。Additional页的Bevel(立体框)部件提供了一个方框，它的单一线条或整个边框都可以通过Style属性设置为外凸或内凹，可以美化窗口。在Win95页上，HeaderControl、StatusBar等部件，为您使用Winows95风格的界面提供了重要元素。

1.3.4 图形、图象部件

1.3.4.1 Image部件

图象部件Image在Additional页上，用来在窗口中显示一幅图片，可以在picture属性中调入图象文件。Delphi支持位图(.BMP)、图标(.ICO)、图元(.WFM)三种文件格式。比较重要的属性是Autosize和Strech。它们决定了图象在窗口中的显示尺寸。Autosize属性为True表示按原尺寸显示，Strech属性为True表示图象按对象图框的大小显示，这时图象的大小可以人为改变。


第一章 Delphi快速入门（四）

DELPHI基础教程

第一章　Delphi快速入门（四）

1.3.4.2 Shape部件

图形部件Shape在前文中我们已有了解，它可以处理多种几何形状，通过设置Pen和Brush的嵌套属性，可以设置图形边框颜色、线型及图形的风格、填充方式、贴图方式等。

1.3.4.3 PaintBox部件

在System页上还有一个PaintBox(绘图框)部件，它在窗体上为您提供一块可供绘图的区域。这一部件需要编程实现它的功能，一个只有在运行时才有效的重要属性Canvas是完成绘图的关键。PaintBox部件不能单独存在于窗体中，必须把它放在固定的分组部件中。

1.3.5 关系图、文件列表部件

1.3.5.1 OutLine部件

OutLine(略图)部件在Win 3.1页上，它适用于显示分层的数据、文本。在Delphi中，略图部件具有很宽的设置范围。它的Lines属性可以设置每一词条的文本或数据。一般一个空格相当于一个层次，如果在项目前加一个空格则表示它处于树的下一层。所以在进行Lines属性编辑的时候不能用Tab键。在编程时用Lines和Items属性来访问略图的名称、索引及完整的路径名。OutLineStyle属性用来配置显示时关系图的风格，您可以选定用旁边有图标的缩进方式，也可以选择连线方式。

1.3.5.2 目录访问部件

Delphi为您提供了强大的文件目录访问部件。您可以针对具体的用途设计自定义的文件对话框。在部件选项板上选中System页，您将发现这些部件，它们是文件列表框FileListBox、目录列表框DirectoryListBox、驱动器下拉式列表框DriveComboBox和过滤式下拉列表框FilterComboBox部件。

文件列表框部件FileListBox显示当前目录中的文件，缺省的设置是显示所有文件。通过改变Mask属性来设置过滤器，可以适用DOS的标准通配方式，选择要显示的文件名。当指定多个过滤器时，之间用“;”隔开。

目录列表框部件DirectoryListBox显示当前驱动器内的目录，并且允许用户在程序执行时切换目录。Column属性决定目录在窗口中用多少列进行显示，当窗体空间不够时，用几列进行显示将有助于全面地显示目录信息。在Delphi中，当窗口空间显示不下全部信息时，将自动加上滚行条，以方便用户的操作。

驱动器下拉式列表框部件DriveComboBox显示当前所在的驱动器，并且在执行时允许用户在驱动器之间切换。作为程序员在应用这一部件时，应考虑当用户选择到不存在的驱动器时，进行容错、提示处理。

FilterComboBox(过滤式下拉文件列表框)部件显示当前文件过滤器类型，例如*.*，*.pas等。在执行时允许用户在下拉列表中选择要显示的文件类型。对Filter属性进行初始化，就会得到下拉式列表。

除了以上的四个部件外，在Sample页上，Delphi还提供了一个目录略图部件DirectoryOutline。它将当前磁盘中的目录结构显示成一个多层次的略图，也允许用户在运行时进行目录的层次显示切换。

1.3.6 滚动部件

1.3.6.1 ScrollBar部件

ScrollBar(滚行条)是在Windows应用程序中常见的结构，在Delphi中多数部件有自己的是否加入滚行条的属性ScrollBar，但一个独立的滚行条部件仍是很有意义的。它在部件选项板的Standard页，提供一种可以改变菜单或是画面中可见部分的工具，可以以一定的增量在一定范围内滚动。Position属性是个整形值，如果需要以动态的方式显示滚行条，可以在程序中通过操作这一属性来实现。

1.3.6.2 ScrollBox部件

ScrollBox(滚动框)部件是加上了水平、垂直滚行条的群组框部件，它在Additional页上，用作在窗体中提供一个可以多方向滚行的工具。如果您只想显示较大幅面的一个部分，并允许您的用户对其进行滚行操作，ScrollBar将是理想的选择。您可以先在全幅面的滚动框中进行全面设计，然后适当地缩小外框，两个方向的滚行条将自动显示。它的Position属性是集成在HorzScrollBar和VertScrollBar两个集属性下的，编程时，可以选定这两个属性进行操作。

1.3.6.3 几个进度显示部件

在System、Sample、Win 95、OCX等页上，Delphi还为用户提供了几个部件，完成显示进度，增量等操作。部件Gauge可以显示成长条状的或饼状的进度指示仪表。例如在安装软件时，可以编程让这一部件显示目前安装的百分比。

    微调按钮部件SpinButton含有指示向上、向下的两个按钮，它可以用来调节数值，使被控制的值按一定的增量单位，由用户操作递增或递减。

    微调编辑框SpinEdit是微调按钮和编辑框的结合，它在编辑区域显示了数值，又在右侧设了微调按钮，使得用户选择调节按钮或改变数值的方式，来调整数据的值，并返回Value属性。

其它的此类部件，因篇幅限制，此处不做赘述。具体部件的信息，按照名称在在线帮助中查询其方法、域及应用实例，可以得到详细应用资料。

1.3.7 网格、表格部件

在Additional页中，还有字符串表格部件StringGrid，绘图表格部件DrawGrid等。它们可以用来模拟按网格排列的事物，如成排的按键、操纵钮、字符串等。属性RowCount和ColCount设置了在网格中显示的行列的数目，设置FixedCols和FixedRows的值，可以固定一些行列避免运行时被用户改变。Options集属性含有字符串表格部件的显示方式、操作方式等，可以查阅帮助获得详细的信息，此处不再一一表述。

图形表格部件DrawGrid使得用户可以用表格的方式显示非文本的数据，它的应用面比字符串表格部件更为广泛，可以把图片和文本一起放在网格中。

颜色网格部件ColorGrid在Sample页上，它提供控制前景色和背景色的界面，通过属性ForeGroundColor和BackGroundColor可以访问到，也可以通过程序，把得到的颜色赋值给其它对象的颜色属性。

Sample页的日历部件Calendar也是用表格的方式表现的，它提供了一个简单的以月份为库单元的日历表格，通过设定Year和Month的值，可以得到相对应的月份的日历。

1.3.8 多媒体(MultiMedia)和OLE部件

媒体播放器部件MediaPlayer和OLE部件可以在System页上找到。媒体播放器部件在处理多媒体文件时很有用。它显示一个VCR风格的控制面板，让您记录或播放多媒体的图像、声音文件。您可以通过属性的设定来控制部件上的各个按钮，如改变显示颜色、增减按钮数目等。改变一下ColoredButtons、EnabledButtons、VisableButtons的各个子属性值，您就不难发现它们控制的显示效果。具体的应用已经超过本简介的目的，请参阅帮助以及后文的详细介绍。

OLE客户端部件OLEContainer在窗体中创建一个OLE用户区域，用于与Delphi外部的某个对象进行接口。若对外部的Paradox表格、Word文件、扩展页等进行操作，则对象本身的应用程序必须支持OLE操作。Delphi提供了支持OLE应用的对象清单，单击ObjClass属性的省略按钮，就会显示出来。具体的应用，请查阅帮助或阅读后文的详细阐述。

以上，我们介绍了Delphi的基本可视部件。读者可能会发现，在Stardrad页最前面的两个部件MainMenu和PopupMenu还没有提及。因为这是建立菜单的重要、基本操作，我们将在下一节中做详细的介绍。另外，在下文中，我们还将介绍比较重要的不可视部件。至于数据库部件、动态数据交换DDE部件等，因为在后面章节中有详尽的用法介绍，此处不赘述。

1.4 使用非可视部件

上一节介绍的各种部件，在设计和运行时的形式是一样的。本节所介绍的部件，在运行时被隐蔽地嵌入到窗口中，它们本身并不做任何事情，必须被用户的程序所支持才能发生作用。因为部件的设计形状和运行状态并不相关，所以，这种部件在设计窗体中的位置无关紧要，可以放在窗体上不影响其它可视部件的任何位置。这种部件，称之为非可视化部件。

首先，介绍在Standard页上的重要部件主菜单MainMenu和弹出式菜单PopupMenu的详细用法。

1.4.1 使用菜单部件

菜单部件含有内置的交互设计器。在设计菜单时，可以用菜单设计器Menu Designer进行工作。

应用程序的菜单有两种形式：MainMenu是一般的Windows用户界面必须的部件，它显示在固定的位置，一般用键盘和鼠标左键来激活菜单的功能项；PopupMenu是弹出式菜单，例如，我们前文的小例程在对齐部件时用到的Delphi菜单，就是弹出式的。一般以鼠标的右键或其它快捷键来激活菜单；菜单的位置是在屏幕上“浮动”的，根据鼠标的当前位置决定。

以下，我们将MainMenu和PopupMenu统称为菜单。

1.4.1.1 进入菜单设计器Menu Designer

创建菜单时，首先要在窗体中放置一个菜单对象。然后调入Menu Designer进行菜单的详细设定。用两种方式可以进入Menu Designer：选中菜单部件，点动属性中Items省略按钮；或在菜单部件上双击左键，就会出现Menu Designer。

1.4.1.2 创建菜单的各个项目

进入Menu Designer，会发现在菜单的第一项位置上有加亮显示。输入顶层菜单项(实际上是在设定顶层菜单项的Caption属性)并回车。一项菜单条设置完后，加亮显示的位置被移至下拉菜单的下一项,同时，主菜单顶层上会出现虚线框，指示出下一个顶层菜单项的位置。要包含一个隔离条，可以键入短划横“-”即减号并回车。逐步输入菜单的各项，用鼠标左键切换到下一个顶层菜单，直到菜单完成。

1.4.1.3 设定加速键和热键

您可以设定加速键，与前文的例程相同，只需在输入时，将“&”放到需要指定为加速键的字母前面，该字母将被用下划线显示，运行时，按“Alt+加速键字母”可以激活该菜单条。设定热键也是很方便的，只需在Object Inspector中该菜单条的ShortCut属性值段的下拉菜单中，为它选定一个热键组合即可。在运行时，通过“Ctrl+热键字母”来激活菜单条。加速键和热键并不矛盾，您可以同时指定它们。

1.4.1.4 使用Menu Designer的弹出式菜单

Menu Designer中还有一个弹出式菜单，它提供了高频菜单创建命令，并可以对模板选项进行快速访问。在菜单设计器窗口中单击右键，或将光标停在菜单设计器上，按Alt-F10，可以调出Menu Designer的弹出式菜单。使用Menu Designer的弹出式菜单可以向菜单增加菜单项、删除菜单项等操作，还可以对标准主菜单模板进行增加、删除、插入等操作。

为菜单编制功能，可以双击菜单条，或在特定菜单条的Events页上，双击它响应的事件的值段，Delphi将回到代码编辑器，光标停在事件对应的事件处理过程中，您可以为菜单编制实现任务的代码。

如果您已经为Speed Bar上的加速按钮编制了事件处理过程，而需要把相应的菜单项连接到事件处理过程上，则只需在该菜单项的Events页的OnClick事件后，输入事件处理过程的名称即可。这样单击菜单项便和按动加速按钮便实现同样的功能。

1.4.1.5 创建嵌套菜单

创建嵌套菜单也是重要的手段。Delphi的嵌套菜单风格是嵌套菜单列在它上层菜单的边上，如图1.11所示：

创建嵌套菜单，把加亮条移到它的上层菜单条上，按Ctrl+右行键，将弹出子菜单，就可以按照同上所述的方法进行创建工作了。

通过拖--放操作，可以实现菜单条的重新定位。用鼠标左键按住某一菜单条，移动到菜单的某一位置，释放左键，菜单条即得到重新定位。菜单设计器允许菜单条在顶层下移动位置，支持菜单条在嵌套菜单中移进、移出，甚至可以将菜单条在各个顶层菜单下属的菜单间移动与定位。

1.4.1.6 给菜单加提示(Hint)

在菜单中加提示是很好的习惯。它使得您的用户在运行菜单功能前，可以对菜单的任务有一个简略的了解。在菜单条任一项的值段按F1,可以查阅在线帮助，在帮助的正文中找到OnHint并查阅它的例程，将DisplayHint等方法的例程Copy到您的程序中。然后，在菜单条的Hint属性中加入提示正文，运行时当用户将鼠标移到菜单项时，在窗体的底部状态行中会出现Hint属性中的提示信息

1.4.1.7 菜单的其它属性简介

菜单还有其它的一些属性。Enabled属性决定菜单是否被禁用，这在编程中可以实现一定情况下不允许用户使用某项菜单。Checked属性表征了菜单项的开关情况，当Checked为True时，菜单条的前面会出现选中标志。Visible属性控制菜单隐藏与否。设置这一属性可以实现多版本的菜单。被隐藏的菜单项以及它的子菜单都是不可见的，或是不可访问的。

Popup Menu在用Menu Designer时是完全相同的。只不过它在运行时和主菜单的激活形式有所不同：它是用鼠标的右键激活的。弹出式菜单在一定的窗口范围内激活，所以必须将窗口和弹出式菜单联系起来。在设计完PopupMenu后，须在Name属性中为它命名，然后把这个名称赋给窗体的PopupMenu属性。这样，窗口会接收右键输入，并激活弹出式对话框。

1.4.2 使用计时器部件Timer

在System页上，还有一个(Timer计时器)部件，它能够有规律地触发OnTimer事件，发送信息给应用程序。它是编制应用程序时最为重要的部件之一。

1.4.2.1 Timer的重要属性

Enabled属性表示了计时器打开还是关闭；用Interval属性设置两个OnTimer事件间的间隔，单位是毫秒，也即千分之一秒。将间隔置为0相当于关闭计时器，Interval的最大允许值是32767，也即32.767秒。

1.4.2.2 使用Timer对程序进行控制

计时器是独立的对象，在启动与Windows无关的逻辑和应用事件时极其有用。如：用计时器可以模拟时钟或计时器，可视地显示经过的时间；可以用作系统延时，Delphi提示信息出现只需在该区域停顿几秒，就是Timer应用的一个例子；可以检查系统环境、事件，根据结果进行响应；也可以在窗口中闪烁一段正文或图像，提示某种操作或处理正在进行等等。

在窗体中放置一个计时器Timer部件，将它的Interval属性置为100(每隔10毫秒触发一次)，双击部件进入代码编辑器，在它的事件中加入这样一句程序：

MessageBeep(0);

运行它，会有规律地发出“叮”声。它给您一个应用Timer部件的直观印象。

尽管Delphi的计时器每秒可以产生1000次激发，在编程中还必须注意程序对计时器激发的响应。如果程序处理OnTimer事件的时间超过Interval的设定值，就可能错过事件，因为当下一次激发到来时，系统正忙于处理上一事件，则这次激发就会被忽略。同时要注意其它的Windows应用程序是否会影响计时器的触发。如果后台正运行着一个占用处理器的程序，就可能会导致Timer的触发不准确，从而使前台程序运行出现错误。

1.4.3 使用公用对话框部件

Delphi为您在Dialogs页上提供了几个标准对话框，它们是文件打开OpenDialog；文件保存SaveDialog；字体Fontdialog；颜色ColorDialog；打印PrintDialog；打印设置PrintsetupDialog;查找FindDialog；替换ReplaceDialog等部件。


第一章 Delphi快速入门（五）

DELPHI基础教程

第一章　Delphi快速入门（五）

1.4.3.1 调用标准对话框

您可以发现，除了Font,Find,Replace对话框之外，其余标准对话框的Events页都是空白的。因为它们本身是不可视的对话框形式，所以不能响应标准的鼠标键盘操作，只能在程序中调用它们。在本章第一节的Demo程序中，我们已经对用Color对话框有了了解，在程序的适当位置加入对话框所属的Execute方法，可以执行标准对话框。

在编制应用程序时，您可以先设计好主菜单。在需要使用标准对话框的菜单项的事件处理过程中，调用标准对话框的Execute等方法，以满足设计需求，即响应菜单命令弹出标准对话框。

1.4.3.2 OpenDialog和SaveDialog部件

OpenDialog(文件的打开)和SaveDialog(保存对话框)部件处理文件的打开与存储，它们拥有完全相同的属性域。DefaultExt属性表示缺省扩展名。当用户没有输入文件的后缀时，可以为用户文件自动加扩展名。FileName属性指定出现在FileName正文框中的缺省文件名。Filter属性提供了文件过滤器。FileEditStyle决定在输入文件名时用Edit还是Combo- Box；设置为fsComboBox时，允许使用历史列表HistoryList属性，这在应用程序需要经常选择文件时，可以节约大量的时间。HistoryList属性保存有在File Name下拉式编辑框中显示的文件名，使用[TStrings]的省略按钮可以编辑历史列表。您也可以编程实现将HistoryList属性设为以前用OpenDialog打开的文件名历史列表。下列程序段可以实现自动历史文件名加载：

if OpenDialog1.Execute then

OpenDialog1.HistoryList.Insert(0,OpenDialog1.FileName);

1.4.3.3 ColorDialog和FontDialog

颜色对话框ColorDialog我们在前文的例程中已经用过，其更详细的功能如Options的设定请查阅在线帮助。

字体对话框Font可以帮助用户获取各种方式的字体。使用OnApply事件，可以使得Font对话框中包含一个Apply按钮，按动它，对话框中设定的字体会立即应用到指定的对象中，而对话框仍然是打开的，这样更便于观察修改的结果。

以下的例程在窗体中先放置一个按钮Button1，调用字体对话框来改变按钮上的Caption属性显示的字体：

Procedure TForm1.Button1Click(Sender:TObject);

begin

FontDialog1.Execute;

end;

Procedure TForm1.FontDlgApply(Sender:Tobject);

begin

Button1.Font:= FontDialog1.Font;

end;

1.4.3.4 PrintDialog和PrintSetupDiaog

打印对话框PrintDialog和打印设置对话框PrintSetupDialog可以显示标准打印、打印设置对话框，支持打印文件和打印设置功能，设置它的Options属性可以规定对话框的表现形式

1.4.3.5 FindDialog和ReplaceDialog

FindDialog和ReplaceDialog提供了查找、替换两个对话框部件，对于寻找和替换文本是极其有用的。在FindDialog和ReplaceDialog中都有OnFind事件，当用户单击寻找对话框中的Find Next按钮时将触发这一事件。FindText属性中保存了用户在Find What编辑框中输入的文本。在ReplaceDialog中还有OnReplace事件，当用户单击替换对话框中的Replace和Replace All按钮时，将触发OnReplace事件。FindText和ReplaceText属性分别保存了用户在Find What和Replace With编辑框中输入的文本。

1.5 使用Delphi的工程管理、设计工具

Delphi 2.0是完备的工程设计系统，除提供了大量的可视化编程部件和简单方便的设计方法外，还备有功能强大的工程管理、窗口设计工具。

1.5.1 创建多窗体工程项目

绝大多数的应用程序都用到不只一个窗口，Delphi允许用户创建多文档界面(MDI)应用程序。应用自动创建窗体、窗体样板、对话框专家、应用专家等，可以使得创建应用程序更为方便快捷，而且可重复利用，大大地降低了工作量。

1.5.1.1 创建一个含有About框的例程

作为例子，我们先创建一个含主窗体和简单的About框的应用程序：

在Delphi中创建一个新工程，并为空窗口增加一个Button部件。我们把它作为主窗体，目的是当按动按钮部件时，调出About窗口。下面我们直接进行About窗口的创制。

选用主菜单的File|New项，在New页中，选用Form样板，Delphi会自动创建一个空窗体Form2。您可以在其上进行About框的设计，例如加入标签以显示各种信息、加入图片帮助说明等。将Form2的BorderStyle属性设置为bsDialog，则窗口成为运行时不能改变大小的对话框。

下面编程实现主窗体对About框的显示控制。很可能在设计时，两个窗体出现重叠，Form1被About遮住。用Shift+F12产生View Form对话框，可以选择窗口的名字，调出所需的窗体Form1，双击其上的按钮部件，在事件处理过程加入以下的程序：

procedure TForm1.Button1Click(Sender: TObject);

begin

Form2.Show;

end;

由于Form1窗体调用About窗体，所以必须在Form1的库单元程序中对引用About进行说明。在Unit1的Uses后面加入About的库单元名Unit2。运行程序，在主窗体上按动按钮，可以出现About框。在框外单击鼠标，会回到主窗体中。

在About窗体中加入一个按钮，并将按钮的ModalResult属性设置为mrOK，同时将按钮的标签的Caption改为OK。这样一个有模式的About框已经建成了。将窗体Form1中Button1的OnClick处理过程的代码改变如下：

Form2.ShowModal;

再运行程序，除了与上述功能相同显示About对话框外，该程序只有当用户按动About框的按钮或被About窗控制图标关闭窗口后，才会回到主窗体中，而不能与第一个窗体发生交互行为。这就是方法Show和ShowModal的主要不同之处。

1.5.1.2 指定自动创建窗体

上面我们简单地生成了一个含About框的双窗体应用程序，运行时，应用程序启动时将自动创建第二个窗体。窗体保持隐蔽状态，但仍然占用Windows的资源。在大型应用程序中，有时不希望在应用程序加载时自动创建所有窗体。Delphi可以指定哪些窗体被自动创建。

选用Project|Options，Delphi显示Project options对话框。如果Forms不是当前页，按动下标签使之可见。如图1.12所示：

图示为Delphi装载的TextEdit例程(...\Delphi 2.0\Demos\doc\TextEdit.dpr)。在MainForm正文框中输入主窗体的名字。当应用程序启动时，主窗体自动打开并获取输入焦点。对MDI应用，主窗体的FormStyle属性必须设置为fsMDIForm。在Auto-created Forms列表框中列出了在启动时自动创建的窗体，缺省时工程文件的所有窗体都在此列中。

如果不需要自动创建窗体，使用箭头按钮把窗体移动到Available Forms列表框中。自动创建的窗体可以用Show方法进行显示，而不自动创建时，必须编程实现窗体的显示。在TextEdit工程中，定义一个TEditForm类型的变量EditForm，使用了以下的代码显示第二个窗体：

Begin

EditForm := TeditForm.Create(Self);

EditForm.Open(OpenFileDialog.Filename);

EditForm.Visible := True;

End;

也即，必须使用Create方法创建窗体，同时将窗体的Visible属性设为真。

1.5.2 使用工程管理器Project Manager

当窗体的数目逐步增加时，跟踪窗体以及与之相连的库单元代码就变得比较烦琐。用Delphi提供的工程管理器Project Manager可以使得管理各种窗口更为方便。

1.5.2.1 工程文件的组成

工程(project)是整个应用程序的源文件集合，这样的文件一共有三种：

1.一个包含主程序部分的工程文件，用以驱动由Delphi创建的Windows程序，扩展名为.DPR。这是在用户的工程被装载时就运行的全局应用程序文件，它的名字出现在Project Manager的标题条上。工程文件一般是Delphi自动创建的，用View|Project Source可以调出该工程的源文件，如果需要，可以对工程源文件进行编辑。

2.一个或多个窗体文件，它们含有相应的库单元文件。窗体文件的扩展名是.DFM。这些文件含装在窗体上的控制部件。在Delphi中这样的文件是在用户设计界面的时候由Delphi自动生成的，而且不能编辑。在Delphi中，一个应用程序至少需要一个窗体。

3. Object Pascal库库单元文件。 它们的扩展名为.PAS。窗体的库单元文件包含了程序部分，用以控制窗体的行为。在一个工程中，还要包含附加库和被应用程序其它部分调用的支持子程序。uses子句中包含的标准库单元文件包含了支持可视窗体及其控制的对象声明。这些库单元也包含了用于激活窗体的事件处理过程程序。

1.5.2.2 使用Project Manager进行工程管理

选用View|Project Manager，会调出Project Manager窗口。

Project Manager主体部分列出了库单元以及相关联的窗体的路径，当库单元和工程在同一目录下时，路径不再显示。Project Manager包含了应用程序的所有源文件和窗口。并不是每一个库单元都拥有一个相关联的窗体。Project Manager可以作为工程的一个简捷目录。在列出的任何一个窗体或库单元上双击鼠标左键，Delphi就会产生此对象的代码编辑器。

Project Manager 窗口上有增加、删除对象以及查看对象的加速按钮。Options加速键能够打开前文所述的Project Options对话框。如果在Project Manager打开时编辑了工程各文件的源代码，可单击Update加速条按钮刷新对象列表。

1.5.3 使用窗体样板和对话框专家

在前文的例程中，我们创建了一个简单的About框。在很多应用程序中，都会用到这种进行信息提示或表示版本信息的对话框。Delphi的设计思想是软件可重用，所以系统phi已经载入了一些用户可重复应用的窗体样板。使用窗体样板，可以简单地生成各种对话框，节省了大量的重复工作。

1.5.3.1 使用窗体样板创建窗体

创建一个新工程时，Delphi为用户创建了一个新的空窗体。如果需要使用一个窗体样板，向工程中增加一个窗体。选用File|New，Delphi即会显示New Items对话框。在Forms页上，从样板库中选择中意的新窗体增加到工程中。例如，Delphi提供了About Box样板。如果选择它，About对话框就增加到工程中，用户只需加入自己的各种信息即可，这样就简化了工作。同样，要建立一个对话框窗口，在Dialogs页上，选用所需的对话框样板，可以简便地建立起所需的对话框窗口。

1.5.3.2 存储自己的窗体样板

您可以在样板库中存储自己的窗体样板。首先创制一个窗体的样本，在窗体区域中单击鼠标的右键，可以调出窗体的加速菜单。选用Add To Repository...，将弹出对话框。

在Forms列表框中，选择想要创建为样板的窗体。在Title输入框中输入想要在样本库内图标下出现的文本。在Description区，输入关于样板的详细说明。这样在浏览样本库时，在状态行上会显示这段文字以进行提示。可以选择一个图标作为新窗体样板的图标，按动Browse按钮，选择一个现有图标来代表新的窗体样板。否则将使用按钮左方的缺省窗体图标。按OK按钮，Delphi会要求您确认将窗体存储为样板。

再次打开New Items对话框，您会发现您的窗体已经被存储成一个窗体样板。

1.5.3.3 使用对话框专家Dialog Expert

对话框样板呈现了友好界面的被保存对话框，而对话框专家却能根据用户提供的信息，智能地创建复杂的对话框。

选用File|New...，在New|Items的Dialog页选用Dialogs Expert，Delphi将提示回答各种问题，以便于建立复杂的对话框。

1.5.4 使用工程样板和应用专家

Delphi的工程样板提供预先设计的工程来作为应用程序开发的起点。而应用专家则允许用户按照一定的模式来创建一个应用程序。

1.5.4.1 使用工程样板

当您通过一个工程样板来开始一个工程时，除了空白工程样板外，都会被要求指定一个唯一的子目录即工程目录Project Directory，用来存储新的工程。如果您指定一个不存在的目录，Delphi会帮助您生成它。您可以通过加入新的窗体和程序库单元来修改它，或不加修改地直接利用，而只是加入您的事件处理过程。无论您如何修改，您所作的修改只影响到打开的工程，原先的工程样板不会受到影响，可再次被利用。

选用File|New，在New Items对话框中选用Projects页，可以选择将MDI、SDI、Windows 95 Logo等应用程序作为缺省的工程类型，点动所需工程的图标，使之高亮度显示，然后按OK按钮。如图1.17所示。在接下来的Select Dictionary对话框中指定一个用来包含新的工程文件的目录，则一个工程样板的副本在指定的目录中打开。

1.5.4.2 使用应用专家Application Expert

一个应用专家就是一个应用程序，它会依照您在一系列对话框中所设置的选项值来产生一个工程。

如果您想在打开一个工程时，利用应用专家来取代工程样板而成为缺省值，则使用File|New，在New Items的Projects页上选用Applcation Expert。这样，双击它的图标，会弹出一系列对话框提问，要求您选择您的工程是否需要一个标准的Windows 95对话框、含有哪些菜单、加速条等。图1.18是使用应用专家后出现的第一个对话框。

Delphi 会要求您输入您的工程文件名及它存储的唯一目录，并提示您选择应用程序是否使用MDI窗口、是否含有状态行、提示等。选项设置完毕，按动Finish按钮，Application Expert即按照您的要求，建立起一个可以直接运行的工程文件。


第二章 Delphi面向对象的编程方法（一）

DELPHI基础教程

第二章　Delphi面向对象的编程方法（一）

Delphi的编程语言是以Pascal为基础的。Pascal语言具有可读性好、编写容易的特点，这使得它很适合作为基础的开发语言。同时，使用编译器创建的应用程序只生成单个可执行文件(.EXE)，正是这种结合，使得Pascal成为Delphi这种先进开发环境的编程语言。

本章中，我们将讨论Object Pascal的主要特点，并讲解如何在事件处理过程和其他应用程序中，使用它来编制程序代码。本章将讲解Delphi应用程序中最常用的Object Pascal语法，而不是Pascal语言的一切细节。如果您完全不熟悉Pascal编程，请参阅一些基础的Pascal教程。如果您具有编程经验，并能熟练地使用其他流行程序语言，您将在本章的Object Pascal中发现一些相同的概念。如果您已经熟悉了Borland Pascal，就可以快速浏览或跳过本章。

2.1 编写Object Pascal程序代码

在前边的章节中，我们通过例程，已经编写了几行简单的代码。在本章中，我们将从熟悉Pascal编程的角度，配合实例，讲解Object Pascal编程的基本方法。

在编写自己的Object Pascal程序时，要注意程序的可读性。Pascal语言是英式结构语言，在程序中选择合适的缩排、大小写风格，并在需要时将程序代码分行，会使得程序代码能够很容易地被自己和他人读懂。一般的程序员都有这样的体验：如果不给程序加上适当的注解，一段时间后，自己也难以理清程序的流程。给程序及时地加上注释是良好的编程习惯。Delphi的注释需要加注在{}之间，编辑器会把它们处理成为空白。Delphi保留了Borland Pascal编辑器的风格，关键字采用黑体字，被注释的部分会变暗，这使得编程风格良好，易读易写。

2.1.1 编写赋值语句

在事件处理过程中，最常用到的工作就是把一个新值赋给一个属性或变量。在设计用户界面时，可以使用Object Inspector(Object Inspector)来改变其属性；但有时需要在程序执行时改变属性的值，而且有些属性只能在执行时改变，这些属性在Delphi的在线帮助的“Proprety”主题中被标为执行期属性。进行这种改变，就必须使用赋值语句。

下文的赋值语句表征一个OnClick事件。当按钮按动后，将编辑框部件Edit1的Color属性置为clRed:

procedure TForm1.Button1Click(Sender: TObject);

begin

Edit1.Color := clRed;

end;

当按动按钮后赋值语句被执行，编辑框变成红色。

在语句中，部件的名称在属性前，中间用“.”表示属性的所属关系。这样就准确地指定了要将clRed值赋给哪一部件的哪一属性。赋值号为“:=”，不论给属性还是给变量赋值，都是将右边的值赋给左边的属性或变量。

当将一个属性值、变量、常量或文本数据赋给属性或变量时，所赋值的类型和接受此值的属性或变量的类型应相同或兼容。一个属性或变量的类型定义了此属性或变量的可能值集合，也定义了程序代码可以执行的运算。在前边的例程中，编辑框部件的Color属性和clRed的类型都是TColor。可以在在线帮助中找到一个属性的类型；另外一种方法是在Object Inspector中选定该属性值段，并按下F1键，则类型将在属性说明的结尾处列出，例如Color属性列出下边的语句：

Property Color : TColor;

有些属性是只读(Read Only)的，它们只能被读取，不能被改变。请查阅在线帮助，在Delphi中这些只读属性都有注解。

2.1.2 标识符的说明与使用

标识符是Delphi应用程序中一些量的名称，这些量包括变量(var)、常量(const)、类型(type)、过程(procedure)、方法(Method)及其他，Object Pascal 在应用标识符时，必须首先说明它们。Object Pascal是强类型语言，它的编译器可以检查确保赋给变量或属性的值是正确的类型，以便于您改正错误。因为Object Pascal是编译语言，所以Delphi的执行速度要比使用解释语言快得多。在使用标识符前说明它们，可以减少程序错误并增加代码的效率。

2.1.2.1 变量

变量是程序代码中代表一个内存地址的标识符，而此地址的内存内容在程序代码执行时可以被改变。在使用变量前必须对它进行说明，即对它进行命名，并说明它的类型。在所有变量说明以前加上保留字var。变量说明左边是变量的名称，右边则是该变量的类型，中间用(:)隔开。

var

Value ,Sum : Integer;

Line : String;

在窗体中加入一个名称为Edit1的编辑框，再加入一个名称(属性Name)为Add的按钮部件，并建立如下的事件处理过程：

procedure TForm1.addClick(Sender: TObject);

var

X , Y: Integer;

begin

X := 100;

Y := 20;

Edit1.Text := IntToStr(X + Y);

end;

在本例中，当按动ADD按钮时，编辑框中显示值120。在Object Pascal中，必须确保变量或属性被赋予类型相同或兼容的值。您可以尝试将赋给X的值改为100.0，或去掉IntToStr函数，在编译时会出现类型不匹配的错误，这也说明了Object Pascal强类型语言的特点。

2.1.2.2 预定义类型

Object Pascal有多个预定义的数据类型，您可以说明任何这些类型的变量：

整形：Integer的范围是-32768到32767,占2字节的内存；Shortint从-128到127，占1字节内存；Longint从-2147443648到2147483647 占4字节内存；Byte从0到255，占1字节；Word从0到65535，占2字节内存。它们都是没有小数部分的数字。

实型：Single可以包含7到8位有效小数部分，占用4字节的内存；Double类可以包含15到16位有效小数部分，占用8字节的内存；Extended类型包含19到20位有效小数部分，占用10字节内存；Comp可以包含19到20位有效小数部分，占用8字节内存。以上实数类型只有在8087/80287选项[N+]打开才可以使用。Real可以包含11到12位有效小数部分，占用6字节内存。它只有在和以前Borland Pascal兼容的情况下才使用，否则应使用Double或Extended。

布尔型：Boolean，只包含true或False两个值，占用1字节内存。

字符型：Char，一个ASCII字符；字符串类型String一串最长可达255个ASCII字符。

指针型：Pointer，可以指向任何特定类型。

字符串型：PChar，是一个指向以零结尾的字符串的指针。

除了预定义类型外，Delphi还有自行定义的类型。上述例程的TColor就是这种类型。此外，用户还可以定义自己的数据类型，这部分内容将在下文中详细讲述。

整型类别和实型类别都各有五种类型，同一类别中，所有的类型与其他同类别的都相容，您可以将一种类型的值赋给相同类别中不同类型的变量或属性，而只需要这个值的范围在被赋值的变量或属性的可能值范围内。例如，对于一个Shortint型的变量，可以接受在-128到127范围内的任意整数，例如Shortint类型的7；您不能将300赋给它，因为300已经超出了Shortint的范围了。将范围检查功能打开(选用Options|Project，并在Compiler Options Page中选择Range Checking)，将会检查出一个范围错误；如果Range Checking没有被打开，那么程序代码将可以执行，但被赋值的值将不是您期望的值。

在一些情况下，您可以进行不同类型的变量或属性的赋值。一般来说，可以将一个较小范围的值赋给一个较大范围的值。例如，您可以将整型值10赋给一个接受实型值的Double属性而使得值成为10.0，但如果将一个Double类型的值赋给整形变量，则会出现类型错误。如果您不清楚类型的兼容性，可以参阅Delphi的在线帮助中“Type Compatibility and Assignment Compatibility”主题。

2.1.2.3 常量

常量在说明时就被赋予了一个值，在程序执行过程中是不可改变的。下面的例子说明了三个常量：

const

Pi = 3.14159;

Answer = 342;

ProductName = "Delphi";

象变量一样，常量也有类型。不同的是，常量假设其类型就是常量说明中其所代表的值的类型。上文的三个常量的类型分别是real型、整形、字符串型。常量用“= " 表示两边的值是相等的。

2.1.3 过程与函数

过程与函数是程序中执行特定工作的模块化部分。Delphi的运行库包含许多过程与函数以供您的应用程序调用。您不必了解过程与函数的逻辑，但要知道过程与函数的用途。在对象中说明的过程和函数称为方法(Method)。所有的事件处理过程都是过程，以保留字procedure开头。每一个事件处理过程只包含了当这一事件发生时需要执行的程序代码。在事件处理过程中使用Delphi已经存在的过程与函数，只需在程序代码中调用它们即可。

2.1.3.1 一个调用Delphi方法的简单例程

下文将通过对一个Memo部件的文本进行剪切、拷贝、粘贴、清除等编辑的应用程序编制，介绍使用Delphi过程和函数的调用方法。

Memo(备注)部件有一个CutToClipboard方法，实现将用户在memo中选择的文本移到剪贴板上去。由于这个功能已经被建立在此方法中了，所以您只需知道这个方法做什么以及如何使用它即可。

下面的语句表明如何调用一个名为Memo1的memo部件的CutToClipboard方法：

Memo1.CutToClipboard;

通过指定Memo1的名称，说明调用哪一个部件的CutToClipboard方法。如果不指明对象名称，Delphi会显示Unknown identifier错误。当该事件处理过程被触发，程序会执行CutToclipboard中的语句，将Memo1中的文本剪贴到剪贴板上去。

下文的例程展示了如何调用Delphi的方法，实现将备注部件的文本信息剪切、拷贝到剪贴板上；将剪贴板上的标记文本粘贴到备注中，清除备注部件中的全部文本等四个功能。

打开一个新的空窗体，加入一个memo部件和四个按钮，并排列整齐。改变按钮部件的Name属性，分别命名为Cut,Copy,Paste,Clear。您会发现，当Name属性发生改变时，Caption属性将发生相应的变化。在Caption属性前加标“&”号设立加速键

将memo部件的ScrollBars属性设为ScVertical，以便加上滚行条。将WordWrap属性设置为True，这样当用户输入文本到达Memo部件的右边缘时会自动回行。将Line属性第一行的Memo1文本删除，使得memo部件在初始显示时为空的。

为每一个按钮建立如下的事件处理过程：

procedure TForm1.CutClick(Sender: TObject)；

begin

Memo1.CutToClipboard;

end;

procedure TForm1.CopyClick(Sender: TObject);

begin

Memo1.CopyToClipboard;

end;

procedure TForm1.PasteClick(Sender: TObject);

begin

Memo1.PasteFromClipboard;

end;

procedure TForm1.ClearClick(Sender: TObject);

begin

Memo1.clear;

end;

执行此程序。您可以在备注部件中输入文本，在进行了文本的标记后，可以任意地进行剪切、拷贝、粘贴和清除。当按钮被按动时，就调用相应的过程进行处理。用户可以通过查阅在线帮助进行Memo部件的Topic Search，在Memo Component项中查阅Method，会得到以上过程的详细说明。

2.1.3.2 调用Delphi的含参过程

有些过程要求用户指明参数。被调用的过程会在执行时使用传入的参数值，这些值在过程中被认为是已经被说明的变量。例如，LoadFromFile方法在TString对象中被说明为：

Procedure LoadFromFile(const FileName: String)；

在调用这一过程时，应指明FileName参数是要装入的文件名称。下面的程序将先打开Open对话框，当您选择了一个文件后，Delphi将把该文件读入一个Memo部件：

begin

OpenDialog.Execute;

Memo1.lines.LoadFromFile(OpenDialog.FileName);

end;

2.1.3.3 使用Delphi函数

与过程一样，函数的程序代码也执行特定的工作。它和过程的差别为：函数执行时会返回一个值，而过程则没有返回值。函数可以用来赋给一个属性或变量；也可以使用返回值来决定程序的流程。

前文中我们实际上已经接触过了函数。在讲述变量时，曾用到过下面的程序段： Edit1.Text := IntToStr(X + Y)；其中，IntToStr(Value)把一个LongInt类型的数值转化为字符串的值，Value是IntToStr唯一的参数，它可以是一个整形的值、变量、属性或产生整形值的表达式。调用函数，必须把返回值赋给和此返回值类型兼容的变量或属性。

有些函数返回一个True或False的布尔量，用户的程序可以根据返回值来决定跳转。下文的例程讲述了函数返回值为Boolean的判断用法：

在窗体中加入一个ColorDialog对象和一个Name属性为ChangeColor的按钮。为按钮的OnClick事件建立事件处理过程如下：

procedure TForm1.ChangeColorClick(Sender: TObject);

begin

if ColorDialog1.Execute then

Form1.Color := ColorDialog1.Color

else

Form1.Color := clRed;

end;

此事件处理过程使用一个返回Boolean值的Execute方法。按动按钮，并在颜色对话框中选择一个颜色。如果按动OK按钮，ColorDialog.Execute方法将返回True，则Form1.Color将被赋值为ColorDialog1.Color，窗体显现您选用的颜色；如果按动颜色对话框的Cancel按钮，方法将返回False值，窗体将变为红色。


第二章 Delphi面向对象的编程方法（二）

DELPHI基础教程

第二章　Delphi面向对象的编程方法（二）

2.1.4 跳转语句

Object Pascal的跳转语句有if和case两个。

2.1.4.1 if语句

if语句会计算一个表达式，并根据计算结果决定程序流程。在上文的例程中，根据ColorDialog.Execute的返回值，决定窗体的背景颜色。if保留字后跟随一个生成Boolean值True或False的表达式。一般用“=”作为关系运算符，比较产生一个布尔型值。当表达式为True时，执行then后的语句。否则执行else后的代码，if语句也可以不含else部分，表达式为False时自动跳到下一行程序。

if语句可以嵌套，当使用复合语句表达时，复合语句前后需加上begin…end。else保留字前不能加“；”，而且，编译器会将else语句视为属于最靠近的if语句。必要时，须使用begin…end保留字来强迫else部分属于某一级的if语句。

2.1.4.2 case语句

case语句适用于被判断的变量或属性是整形、字符型、枚举型或子界型时(LongInt除外)。用case语句进行逻辑跳转比编写复杂的if语句容易阅读，而且程序代码整形较快。

下面的例程显示一个使用case语句的窗体：

建立如下的事件处理过程：

procedure TForm1.Button1Click(Sender: TObject);

var

Number : Integer;

begin

Number := StrToInt(Edit1.Text);

case Number of

1,3,5,7,9: Label2.Caption := '奇数';

0,2,4,6,8: Label2.Caption := '偶数';

10..100:

begin

Label2.Caption := '在10到100之间';

Form1.Color := clBlue;

end;

else

Label2.Caption := '大于100或为负数';

end;

end;

执行程序，当Edit1部件接受到一个值，并按动“OK”按钮触发程序后，Number便被赋值为用户输入的数值。case语句根据Number的值判断该执行哪一条语句。象if语句一样。case语句也有可选择的else部分。case语句以end结尾。

2.1.5 循环语句

Object Pascal的循环语句有三种：repeat、while和for语句。

2.1.5.1 repeat语句

repeat语句会重复执行一行或一段语句直到某一状态为真。语句以repeat开始，以until结束，其后跟随被判断的布尔表达式。参阅以下的例程：

i := 0;

repeat

i := i+1;

Writen(i);

until i=10;

当此语句被执行时，窗体的下方会出现1到10的数字。布尔表达式 i=10 (注意，与其他语言不同的是，“=”是关系运算符，而不能进行赋值操作)直到repeat..until程序段的结尾才会被计算，这意味着repeat语句至少会被执行一次。

2.1.5.2 while语句

while语句和repeat语句的不同之处是，它的布尔表达式在循环的开头进行判断。while保留字后面必须跟一个布尔表达式。如果该表达式的结果为真，循环被执行，否则会退出循环，执行while语句后面的程序。

下面的例程达到和上面的repeat例程达到同样的效果：

i := 0;

while i<10 do

begin

i := i+1;

writeln(i);

end;

2.1.5.3 for语句

for语句的程序代码会执行一定的次数。它需要一个循环变量来控制循环次数。您需要说明一个变量，它的类型可以是整形、布尔型、字符型、枚举型或子界型。

下面的程序段会显示1到5的数字，i为控制变量：

var

i : integer;

for i := 1 to 5 do

writeln(i);

以上介绍了三种循环语句。如果您知道循环要执行多少次的话，可以使用for语句。for循环执行速度快，效率比较高。如果您不知道循环要执行多少次，但至少会执行一次的话，选用repeat..until语句比较合适；当您认为程序可能一次都不执行的话，最好选用while..do语句。

2.1.6 程序模块

程序模块在Object Pascal中是很重要的概念。它们提供了应用程序的结构，决定了变量、属性值的范围及程序执行的过程。它由两个部分组成：可选择的说明部分和语句部分。如果有说明部分，则必在语句部分之前。说明部分包括变量说明、常量说明、类型说明、标号说明、程序，函数，方法的说明等。语句部分叙述了可执行的逻辑行动。

在Delphi中，最常见的程序模块便是事件处理过程中的程序模块。下面的事件处理过程是含有变量说明部分的程序模块：

procedure TForm.Button1Click(Sender Tobject);

var {程序模块的说明部分}

Name : string;

begin {程序模块的语句部分}

Name := Edit1.Text;

Edit2.Text := 'Welcome to Delphi'+Name;

end; {程序模块结束}

库单元也是程序模块。库单元的interface部分含有库函数、类型、私有，公有域的说明，也可以含有常量、变量的说明。这一部分可以作为程序模块的说明部分。在库单元的implementation部分中通常含有各种事件处理过程，它们可以视为模块的语句部分，是事件处理模块。库单元模块结束于库单元结束的end.处。

程序模块中可以包含其他的程序模块。上文库单元模块中含有事件处理模块。而库单元模块实际是在工程程序模块中。

所有的Delphi应用程序都有相同的基本结构。当程序逐渐复杂时，在程序中加入模块即可。例如在库单元模块中加入事件处理模块，向工程中加入库单元模块等。模块化编程使得程序结构良好，并且对数据具有保护作用。

2.1.7 关于作用范围

2.1.7.1 标识符的作用范围

一个变量、常量、方法、类型或其他标识符的范围定义了这个标识符的活动区域。对于说明这个标识符的最小程序模块而言，此标识符是局部的。当您的应用程序在说明一个标识符的程序模块外执行时，该标识符就不在此范围内。这意味着此时执行的程序无法访问这个标识符，只有当程序再度进入说明这个标识符的程序模块时，才可以访问它。

下面的示意图表示一个含有两个库单元的工程，每个库单元中又各有三个过程或事件处理过程。

2.1.7.2 访问其他程序模块中的说明

您可以在当前的程序模块中访问其他程序模块中的说明。例如您在库单元中编写一个事件处理过程来计算利率，则其他的库单元可以访问这个事件处理过程。要访问不在当前库单元中的说明，应在这个说明之前加上其他应用程序的名称和一个点号(.)。例如，在库单元Unit1中有事件处理过程CalculateInterest过程，现在您想在库单元Unit2中调用这一过程，则可以在Unit2的uses子句中加入Unit1，并使用下面的说明：

Unit1.CalculateInterest(PrincipalInterestRate : Double)；

应用程序的代码不能在一个模块外访问它说明的变量。事实上，当程序执行跳出一个模块后，这些变量就不存在于内存中了。这一点对于任何标识符都是一样的，不管事件处理过程、过程、函数还是方法，都具有这一性质。这样的标识符称为局部变量。

2.1.7.3 按照作用范围说明标识符

您可以在应用程序的不同地方说明一个标识符，而只需保证它们的有效范围不同即可。编译器会自动访问最靠近当前范围的标识符。

库单元的全局变量一般可以说明在保留字implementation后面。例如，下面的例程实现将两个编辑框中的整数相加，显示在第三个编辑框中。用到了一个整形的全局变量Count：

…implememntation

var

Count : Integer;

procedure TForm1.AddClick(Sender:TObject);

var

FirstNumber,SecondNumber:Integer;

begin

Count := Count + 1;

Counter.Text := IntToStr(Count);

FirstNumber := StrToInt(Edit1.Text);

SecondNumber := StrToInt(Edit2.Text);

Edit3.Text := IntToStr(FirstNumber+SecondNumber);

end;

…

为了实现每按动一次按钮Count增加一次，必须对全程变量Count进行初始化处理。在程序库单元的结尾处，最后一个end.保留字之前，加入保留字initialization和初始化Count的代码：

…

initialization

Count := 0;

这样当事件处理过程AddClick被触发时，Count就会被增加一次，以表征计算次数。如果用面向对象编程，则Count可以说明成窗体的一个域，这在下一节中将有讲述。

2.1.8 编写一个过程或函数

在您开发Delphi应用程序时，所需的大部分代码都编写在事件处理过程中，但有时仍然需要编写不是事件处理过程的函数或过程。例如，您可以把在多个事件处理过程中用得到语句编写成过程，然后任何事件处理过程、过程、函数都可以象调用已经存在的过程或函数一样直接调用它。好处是您只需编写一次代码，而且程序代码会比较清楚。

2.1.8.1 一个自行编写的函数例程

在上文两个数相加的程序中，如果编辑框中无值，则会使得程序出错中断。为避免这种情况，编写下面的函数，检查编辑框中是否有值，如无值，则提醒用户输入：

function NoValue(AnEditBox:TEdit):Boolean;

begin

if AnEditBox.Text='' then

begin

AnEditBox.Color := clRed;

AnEditBox.Text := '请输入整数值';

Result := True;

end

else

begin

AnEditBox.Color := clWindow;

Result := False;

end;

end;

NoValue函数会检查编辑框是否为空，如果是，编辑框颜色变红，并提醒用户输入一个整数，然后函数返回真值；Result保留字在Delphi中用来专指函数返回值。在上文的例程中加入NoValue函数：

procedure TForm1.AddClick(Sender: TObject);

var

FirstNumber,SecondNumber : Integer;

begin

if NoValue(Edit1)or NoValue(Edit2) then

exit;

Count := Count + 1;

Counter.Text := IntToStr(Count);

FirstNumber := StrToInt(Edit1.Text);

SecondNumber := StrToInt(Edit2.Text);

Edit3.Text := IntToStr(FirstNumber+SecondNumber);

end;

如果其中的任何一个返回真值，则表示有编辑框空，会执行exit过程，使得当前的程序模块停止执行，并使得编辑框出现输值提示。当新值被输入后，再执行程序时，红色提示被隐去，恢复正常的计算状态。

2.1.8.2 过程和函数的标题

每一个过程或函数都以标题开始，其中包括过程或函数的名称和它使用的参数。过程以保留字procedure开始，函数以保留字function开始。参数位于括号里面，每一个参数以分号分隔。例如：

procedure validateDate(Day : Integer; month : Integer; Year : Integer);

您也可以将相同类型的参数组合在一起，则上述过程头写作：

procedure ValidateDate(Day, Month, Year : Integer);

函数在标题中还多了一项：返回值的类型。下面是一个返回值为Double型的函数标题：

function CalculateInterest(principal,InterestRate:Double):Double;

2.1.8.3 函数和过程中的类型说明

一个过程或函数程序模块也含有说明部分和语句部分。说明部分可以包括类型说明、变量说明、常量说明等。除了Object Pascal语言中已经定义的类型之外，Delphi的应用程序还可以建立新的数据类型。类型说明部分有保留字type开始。下面是一些类型的说明：

type

Tcount = Integer;

TPrimaryColor = (Red,Yellow,Blue);

TTestIndex = 1..100;

TTextValue = -99..99;

TTestList = array [TTestIndex] of TTestValue;

TCharVal = Ord('A')..Ord('Z') ;

Today = (Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,

Sunday) ;

在类型标识符后面，用“＝”号定义了新的类型。类型界定了变量的取值范围，例如，TCount类型的变量必须是整形值；一个TPrimaryColor类型的变量只能是red、yellow或blue等等。每一个类型的名称都是由字母T开始，这并非必须的，但它是Delphi的惯例，在区别类型名和标识符时非常有用。类型说明可以是局部的，也可以是全局的。如果您把它放在implementation后面，则表明对于库单元来讲，它是全局的，所有的事件处理过程和其他的过程、函数都可以调用它。如果类型是在过程中被说明的，则是局部的，离开这一过程，该类型将失效。

一般来讲，在过程和函数中，任何类型说明都在变量说明之前，而任何变量说明都在常量之前。但是，只要遵从说明必须在过程与函数的标题之后，而且在程序代码之前，即是有效的。


第二章 Delphi面向对象的编程方法（三）

DELPHI基础教程

第二章　Delphi面向对象的编程方法（三）

2.1.8.4 过程和函数的语句部分

过程或函数的语句部分由begin开始，end结束。函数需要一个返回值。可以将返回值赋给函数名称，也可以将返回值赋给Result变量。下面的例程将返回值赋给函数名称：

function CalculateInterest(Principal,InterestRate: Double):Double;

begin

CalculateInterest := Principal * InterestRate;

end;

将返回值赋给Result变量也是可以的，则上面的程序改为：

Result := Principal*InterestRate;

下面是这个函数的调用方法：

InterestEarned :=CalculateInterest(2000,0.012);

在Implementation后面的过程和函数，可以且只能被此库单元的事件处理过程使用。要让过程和函数可以被其他的程序库单元使用，则需要将过程或函数的标题部分放在库单元中的interface部分，而把含标题的整个过程或函数放在库单元的inplementation部分，并在要访问这个过程或函数的库单元的uses子句中加入说明这个过程或函数的库单元名称。

2.1.8.5 函数的递归调用

在Object Pascal中，过程或函数必须先说明再调用。上文的NoValue函数必须在使用它的事件处理过程之前说明和执行，否则程序会报告一个未知标识符的错误。

以上规则在递归调用时是例外情况。所谓递归调用，是指函数A调用函数B，而函数B又调用函数A的情况。在递归调用中，函数要进行前置，即在函数或过程的标题部分最后加上保留字forword。下文的例程是一个递归调用的典型例子：

…

implementation

var

alpha:Integer;

procedure Test2(var A:Integer):forword;

{Test2被说明为前置过程}

procedure Test1(var A:Integer);

begin

A :=A-1;

if A>0 then

test2(A); {经前置说明，调用未执行的过程Test2}

writeln(A);

end;

procedure Test2(var A:Integer);{经前置说明的Test2的执行部分}

begin

A :=A div 2;

if A>0 rhen

test1(A); {在Test2中调用已执行的过程Test1}

end;

procedure TForm1.Button1Click(Sender:TObject);

begin

Alpha := 15; {给Alpha赋初值}

Test1(Alpha); { 第一次调用Test1,递归开始}

end;

按钮的OnClick事件处理过程给Alpha赋初值，并实现先减1再除2的循环递归调用，直到Alpha小于0为止。

2.1.8.6 过程和函数的参数

当您的程序代码在调用一个过程或函数时，通常用参数传递数据到被调用的过程或函数中。最常用的参数有数值参数、变量参数和常量参数三种。

由被调用过程或函数定义的参数为形参，而由调用过程或函数指明的参数叫实参。在NoValue函数中，说明函数体中的AnEditBox是形参，而调用时在if NoValue(Edit1)…中，Edit1是实参。

数值参数在运行过程中只改变其形参的值，不改变其实参的值，即参数的值不能传递到过程的外面。试看下面的例程：

procedure Calculate(CalNo:Integer);

begin

CalNo := CalNo*10;

end;

用以下例程调用Calculate函数：

…

Number := StrToInt(Edit1.Text);

Calculate(Number);

Edit2.Text := IntToStr(Number);

…

Number接受由编辑框1输入的数值，经Calculate过程运算。它是一个数值型实参。在进入Calculate函数后，会把Number实参拷贝给形参CalNo，在过程中CalNo增大十倍，但并未传递出来，因此Number值并未改变，在编辑框2中显示仍然是编辑框1中的输入值。形参和实参占用不同的内存地址，在过程或函数被调用时，将实参的值复制到形参占用的内存中。因此出了过程或函数后，形参和实参的数值是不同的，但实参的值并不发生变化。

如果您想改变传入的参数值，就需要使用变量参数，即在被调用程序的参数表中的形参前加上保留字var。例如：

procedure Calculate(var CalNo : Integer);

则CalNo并不在内存中占据一个位置，而是指向实参Number。当一个变参被传递时，任何对形参所作的改变会反映到实参中。这是因为两个参数指向同一个地址。将上一个例程中过程头的形参CalNo前面加上var，再以同样的程序调用它，则在第二个编辑框中会显示计算的结果，把第一个编辑框中的数值放大十倍。这时形参CalNo和实参Number的值都是Nnmber初始值的10倍。

如果当过程或函数执行是要求不改变形参的值，最保险的办法是使用常量参数。在参数表的参数名称前加上保留字const可以使一个形参成为常量参数。使用常量参数代替数值参数可以保护您的参数，使您在不想改变参数值时不会意外地将新的值赋给这个参数。

2.1.9 定义新的数据类型

Object Pascal有一些系统预定义的数据类型，在2.1.2中已经对它们作了介绍。您可以利用这些数据类型以建立新的数据类型来满足程序的特定需要。下面简单地叙述了您能建立的主要数据类型，如枚举型、子界型、数组型、集合型、记录型、对象型等。

2.1.9.1 枚举类型

一个枚举型的说明列出了所有这种类型可以包括的值：

type

Tdays=( Sunday ,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday);

可以定义上述枚举类型的变量：

var

DayOfWeek:TDays;

在枚举型中，括号中的每一个值都有一个由说明它的位置决定的整形值。例如Sunday有整形值0，Monday有整形值1等。您可以把DayOfWeek说明为一个整形变量，并将一星期的每一天赋一个整形值以达到相同的效果，但用枚举型会使得程序可读性好，编写容易。当您在枚举型中列出值时，您同时说明了这个值是一个标识符。例如您的程序中如果已经含有TDays类型且说明了DayOfWeeks变量，则程序中便不能使用Monday变量，因为它已经被说明为标识符了。

2.1.9.2 子界类型

子界型是下列这些类型中某范围内的值：整形、布尔量、字符型或枚举型。在您想限制一个变量的取值范围时，子界型是非常有用的。

type

Thours = 0..23;

TValidLetter = 'A' .. 'F';

TDays = ( Sunday ,Monday,Tuesday,Wednesday,Thursday,

Friday,Saturday); {枚举型}

TWorkDay = Monday..Friday; {一个TDays型的子界}

子界型限定了变量的可能取值范围。当范围检查打开时，(在库单元的Implementation后面有{$R*.DFM}字样表示范围检查打开，否则您可以在Options|Project|Complier Options中选择Range Cheking来打开范围检查)，如果变量取到子界以外的值，会出现一个范围检查错误。

2.1.9.3 数组类型

数组是某种数据类型的有序组合，其中每一个元素的值由其相对位置来指定，您可以在数组的某个位置上放置数据，并在需要时使用这些数据。下面的类型说明了一个Double型的数组变量：

var

Check : array [1..10] of Double;

它表示Check指向一个含有10个Double型元素的数据串列，代表每一个元素的是1到10之间的数字，称为索引。数组的每一项由数组名称加上[]中的索引来表示。Check包含10个变量，Check[1]表示第一个变量。您也可以把数组定义成类型：

type

TCheck = array[1..10] of Double;

则变量说明改为：

var

Check :TCheck;

您可以通过给数组赋值等方法来使用数组。下面的语句将0.0赋给Check数组中的所有元素：

for J := 1 to 10 do

Check[J] := 0.0;

数组也可以是多维的，下面的类型定义了一个20行、20列的数组。

type

Ttable = array[1..20,1..20] of Double;

var

table1:TTable;

想将这一表格的所有数据初始化为0.0，您可以使用for循环：

var

Col,Row:Integer;

…

for Col :=1 to 20 do

for Row := 1 to 20 do

Table1[Col,Row] := 0.0;

2.1.9.4 字符串类型

字符串类型事实上是一个一维的字符数组。当您说明一个字符串型的变量时，您应当指明这个字符串的大小，下面是说明字符串类型的例子：

type

MyString: string[15];

var

MyName: MyString;

则变量MyName被说明成为最多可以包含15个字符。如果您没有说明字符串的大小，Delphi会认为字符串包含最大值255个字符。给字符串赋值可以直接使用单引号括起的字串赋值：

MyName := 'Frank.Smith';

或MyName := '张明'；

因为MyName是一个可以包含15个字符的MyString型变量，上文的两个的变量都是有效的，一个汉字可以视作两个字符。当您给字符串型变量赋的值多于定义数值时，例如将MyName赋为‘FrankSmith.Franklin’，则Delphi只会接受前15个字符‘FrankSmith.Fran’。在内存中，字符串通常占用比所说明的大小多一个字节的空间，因为第一个位置是一个包含这个数组大小的字节。您可以使用索引值来访问字符串的字符，MyName[1]可以得到MyName的第一个字符'F'。

您可以使用Delphi丰富的运算符、过程和函数来处理字符串型的变量和属性。下面介绍几个常用的运算符和Delphi过程或函数：

Concat和(+)功能相同，都可以将多个字符串组合在一起，建立一个较大的字符串；Copy会返回一个字符串中的子字符串；Delete在一个字符串中从一个指定位置起删除一定数目的字符；Insert在一个字符串中插入一个字符串；Length返回字符串的长度；Pos返回一个子字符串在一个字符串中的位置，即索引值。

2.1.9.5 集合类型

集合类型是一群相同类型元素的组合，这些类型必须是有限类型如整形、布尔型、字符型、枚举型和子界型。在检查一个值是否属于一个特定集合时，集合类型非常有用。下面的例程可以说明集合类型的用法：

在窗体上加入一个编辑框和一个按钮，清除编辑框中的文字，在其上加上Caption为“输入元音”的标签Label，并在编辑框的下方加入一个空的标签，将按钮的Default属性改为True，建立按钮的事件处理过程如下：

procedure TForm1.Button1Click(Sender:TObject);

type

Tvowels=set of Char;

var

Vowels:TVowels;

begin

Vowels := ['a','e','i','o','u'];

if Edit1.Text[1] in Vowels then

Lable2.Caption := '是元音';

else

Lable2.Caption := '请再试';

end;

执行这个程序，在编辑框中输入字母，表达式Edit1.Text[1] in Vowels的结果是布尔型的，in是运算符，用来判断字母是否存在于集合中。输入的判别结果会显示在编辑框的下方。以上就用到了集合类型TVowels。

2.1.9.6 记录类型

记录是您的程序可以成组访问的一群数据的集合。下面的例程说明了一个记录类型的用法：

type

TEmployee=record

Name : string[20];

YearHired:1990..2000;

Salsry: Double;

Position: string[20];

end;

记录包含可以保存数据的域，每一个域有一个数据类型。上文的记录TEmployee类型就含有四个域。您可以用以下的方式说明记录型的变量：

var

NewEmployee,PromotedEmployee:TEmployee;

用如下的方法可以访问记录的单域：

NewEmployee.Salary := 1000;

编写如下的语句可以给整个记录赋值：

with PromotedEmployee do

begin

Name :='';

YearHired := 1993;

Salary := 2000.00

Position := 'editor';

end;

您的程序可以将记录当成单一实体来操作：

PromptEmployee := NewEmployee;

以上介绍了用户常用的自定义类型。在Delphi的编程中，对象是非常重要的用户自定义数据类型。象记录一样，对象是结构化的数据类型，它包含数据的域(Field)，也包含作为方法的过程和函数。在Delphi中，当您向窗体中加入一个部件，也就是向窗体对象中加入了一个域；每一个部件也是对象，每当您建立一个事件处理过程使得部件可以响应一个事件时，您即自动地在窗体中加入了一个方法。在本章第2节中，将详细讲述Delphi面向对象编程的方法和技巧。

2.1.10 Object Pascal的库单元Unit

Units是常量、变量、数据类型、过程和函数的集合，而且能够被多个应用程序所共享。Delphi已经拥有许多预定义的程序库单元可供您建立您的程序库单元使用。Delphi的Visual Component Library由多个程序库单元组成，它们说明了对象、部件以供您的应用程序用来设计用户界面。例如，当您在窗体中加入一个Check Box时，Delphi自动在您的程序库单元中加入了Stdctrls库单元，因为TCheckBox部件是在StdCtrls库单元中说明的。

当您设计您的窗体时，Delphi自动建立一个和您的窗体有关的库单元。您的库单元不必都和窗体有关，也可以使用预定义的只包含数学运算函数的库单元，或是自行编写数学函数库单元。在一个库单元中所有的说明都相互有关系，例如，CDialogs程序库单元包含了在您的应用程序中使用的普通对话框的所有说明。

2.1.10.1 Object Pascal程序库单元的结构

不管一个库单元是否和一个窗体有关，库单元的结构都是相同的。其结构如下：

unit <库单元名称>

interface

uses <选择性的库单元列表>

{公有说明}

implementation

uses <选择性的库单元列表>

{私有说明}

{过程和函数的执行部分}

initialization {选择性的}

{选择性的初始化程序}

end.

2.1.10.2 程序库单元的接口部分

interface是库单元的接口部分，它决定了本库单元对其他任何库单元或程序的可见(可访问)部分。您可以在接口部分说明变量、常量、数据类型、过程和函数等等。Delphi在您设计窗体的库单元中，将窗体数据类型、窗体变量和事件处理过程都说明在这一部分。

interface标志库单元接口部分的开始。在interface中的说明对要使用这些说明的其他库单元或应用程序是可见的。一个库单元可以使用其他Unit的说明，只需要在uses子句中指明那些库单元即可。例如，您在库单元A中编写程序代码，且您想调用UnitB于interface部分说明的程序。您可以把库单元B的名称加入到A的interface部分的uses子句中，则任何A中的程序都可以调用B中说明的程序。而且，如果B中interface部分的uses子句中出现C库单元，尽管A中未曾出现C，A同样可以调用B、C库单元在interface中说明的程序。但如果B出现在A的interface部分的uses子句中，那么库单元A便不能出现在B的interface的uses子句中。因为这样会产生对库单元的循环访问。当试图编译时，会产生出现错误信息。

2.1.10.3 程序库单元的实现部分

实现部分implementation中包含interface中说明的过程、函数、事件处理过程的具体实现程序代码。这一部分可以有自己的额外说明，但这些说明是私有的，外部程序不能调用这些说明。在interface中说明的函数实体必须在implementation部分出现，可以使用标题简写：只输入procedure或function保留字，后面跟过程或函数的名称即可，其后则是程序的实现部分了。如果您在implementation部分说明任何常式，其标题并未出现在interface部分，则必须写全其标题部分。

在implementation部分的uses子句中指定的库单元，只供给本库单元的程序使用其interface中说明的程序。其他使用本库单元的库单元，不能访问这些在implementation的udes子句中库单元的说明，因为在implementation后进行的库单元包含是私有的。所以上例中，如果C出现在B的implementation部分，则A不能使用C的公有部分，除非C出现在A的uses子句中。在implementation中出现的循环访问是Delphi所允许的，如果A的implemetation的uses子句中出现B，则B的implementation部分也可以出现A。

2.1.10.4 程序库单元的初始化部分

初始化当前库单元所使用的数据，或是通过interface部分将数据提供给其他应用程序、库单元使用时，您可以在库单元中加入一个initialization部分，在库单元的end前加上您的初始化语句。当一个应用程序使用一个库单元时，在库单元中的initialization部分会先于其他的代码执行。如果一个应用程序使用了多个库单元，则每一个库单元的初始化部分都会在所有的程序代码前执行。

2.1.10.5 使用Delphi的可视化部件及其库单元

当您在窗体中加入可视化部件时，如果该部件在可视化部件库中，Delphi会在您的库单元的interface部分的uses子句中自动加上需要使用的库单元名称。但有些对象在Delphi的环境中并没有可视化部件存在，例如，您想在库单元中加入一个预定义的信息框，则您必须把MsgDlg库单元加入您的uses子句中。如果您要使用TPrinter对象的话，必须将Printer库单元加入uses子句中。在在线帮助中可以查到对象所属的预定义库单元。

要使用在其他库单元中说明的函数，应在函数的前面加上这一库单元的名称，并用‘.’号隔开。例如，要在Unit2中使用Unit1中说明的Calculate函数，应使用下面的方法：

Number := Unit1.Calculate(10);

您可以在任何标识符如属性、常量、变量、数据类型、函数等之前加上库单元的名称。您可以在自由地在任何Delphi库单元中加入程序代码，但不要改变由Delphi生成的程序。

2.1.10.6 建立与窗体无关的新库单元

如果您想在工程中建立一个和任何窗体无关的新库单元，可以现选用File|New Unit。这时一个新的库单元加入了工程，新库单元的代码如下：

unit Unit2;

interface

implementation

end.

Delphi将根据您的工程中的文件数目为您的库单元选择名称，您可以在程序骨架间加入您的程序代码。

当编译您的工程时，这个新加入的库单元会被编译为一个具有.DCU后缀的文件。这个新生成的文件是链接到工程的可执行文件上的机器代码。


第二章 Delphi面向对象的编程方法（四）

DELPHI基础教程

第二章　Delphi面向对象的编程方法（四）

2.1.10.7 将库单元加入工程

将库单元加入工程是比较简单的。无论是您自己建立的库单元还是Delphi建立的与窗体有关的库单元，如果已经完成，则先打开您想加入库单元的工程(可以用Open Project打开工程)；再选用File|Open File，然后选择您想加入的源程序(.PAS文件)，并选择OK即可。则库单元被加入到应用程序中。

2.2 用Delphi的对象进行编程

Delphi是基于面向对象编程的先进开发环境。面向对象的程序设计(OOP)是结构化语言的自然延伸。OOP的先进编程方法，会产生一个清晰而又容易扩展及维护的程序。一旦您为您的程序建立了一个对象，您和其他的程序员可以在其他的程序中使用这个对象，完全不必重新编制繁复的代码。对象的重复使用可以大大地节省开发时间，切实地提高您和其他人的工作效率。

2.2.1 什么是对象

一个对象是一个数据类型。对象就象记录一样，是一种数据结构。按最简单的理解，我们可以将对象理解成一个记录。但实际上，对象是一种定义不确切的术语，它常用来定义抽象的事务，是构成应用程序的项目，其内涵远比记录要丰富。在本书中，对象可被理解为可视化部件如按钮、标签、表等。

了解对象，最关键的是掌握对象的特性。一个对象，其最突出的特征有三个：封装性、继承性、多态性。

2.2.1.1 对象的封装性

对对象最基本的理解是把数据和代码组合在同一个结构中，这就是对象的封装特性。将对象的数据域封闭在对象的内部，使得外部程序必需而且只能使用正确的方法才能对要读写的数据域进行访问。封装性意味着数据和代码一起出现在同一结构中，如果需要的话，可以在数据周围砌上“围墙”，只有用对象类的方法才能在“围墙”上打开缺口。

2.2.1.2 对象的继承性

继承性的含义直接而且显然。它是指把一个新的对象定义成为已存在对象的后代；新对象继承了旧类的一切东西。在往新对象中添加任何新内容以前，父类的每一个字段和方法都已存在于子类中，父类是创建子类的基石。

2.2.1.3 对象的多态性

多态性是在对象体系中把设想和实现分开的手段。如果说继承性是系统的布局手段，多态性就是其功能实现的方法。多态性意味着某种概括的动作可以由特定的方式来实现，这取决于执行该动作的对象。多态性允许以类似的方式处理类体系中类似的对象。根据特定的任务，一个应用程序被分解成许多对象，多态性把高级设计处理的设想如新对象的创建、对象在屏幕上的重显、程序运行的其它抽象描述等，留给知道该如何完美的处理它们的对象去实现。

2.2.1.4 通过Delphi实例了解对象

让我们结合Delphi的实例讨论对象的概念:

当您要建立一个新工程时，Delphi 将显示一个窗体作为设计的基础。在程序编辑器中，Delphi将这个窗体说明为一个新的对象类型，并同时在与窗体相关联的库单元中生成了创建这个新窗体对象的程序代码。

unit Unit1;

interface

uses SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs;

type

TForm1 = class(TForm) {窗体的类型说明开始}

private

{ Private declarations }

public

{ Public declarations }

end; {窗体的类型说明结束}

var

Form1: TForm1; {说明一个窗体变量}

implementation

{$R *.DFM}

end.

新的窗体类型是TForm1，它是从TForm继承下来的一个对象。它具有对象的特征：含有域或方法。由于您未给窗体加入任何部件，所以它只有从TForm类中继承的域和方法，在窗体对象的类型说明中，您是看不到任何域、方法的说明的。Form1称为TForm1类型的实例(instance)。您可以说明多个对象类型的实例，例如在多文档界面(MDI)中管理多个子窗口时就要进行这样的说明。每一个实例都有自己的说明，但所有的实例却共用相同的代码。

假设您向窗体中加入了一个按钮部件，并对这个按钮建立了一个OnClick事件处理过程。再查看Unit1的源程序，会发现TForm1的类型说明部分如下：

type

TForm1 = class(TForm)

Button1: TButton;

procedure Button1Click(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

现在TForm1对象有了一个名为Button1的域：它是您在窗体中加入的按钮。TButton是一个对象类型，Button1是Tbutton的一个实例。它被TForm1对象所包含，作为它的数据域。每当您在窗体中加入一个部件时，部件的名称就会作为TFom1的域加入到类型说明中来。在Delphi中，您所编写的事件处理过程都是窗体对象的方法。每当您建立一个事件处理过程，就会在窗体的对象类型中说明一个方法。

当您使用Object Inspector来改变对象(部件)的名称时，这个名称的改变会反映到程序中。例如，在Object Inspector中将Form1的Name属性命名为ColorBox，您会发现在类型说明部分，会将前文的TForm1改为：

TColorBox=class(TForm);

并且在变量说明部分，会说明ColorBox为TColorBox类型的变量，由Delphi自动产生的事件处理过程名称会自动改为TColorBox.Button1Click；但您自行编写的实现部分的代码却不会被自动修改。因此，如果您在改变Name属性前编写了程序，则您必须将事件处理过程中的对象名称进行改变。所以，原先的Form1.Color要改为ColorBox.Color。

2.2.2 从一个对象中继承数据和方法

前面的TForm1类型是很简单的，因为它只含有域Button1和方法Button1Click。但是在这个窗体上，您可以改变窗体的大小、加入或删除窗体的最大最小化按钮，或设置这个窗体为MDI界面。对于一个只包含一个域和方法的对象来讲，您并没有看到显式的支持程序。在窗体上单击鼠标或用Object Inspector的上端的Object Selector选中Form1对象，按动F1查阅它的在线帮助，您会在Properties和Method中找到它的继承到的全部属性和方法。这些是在TForm类型中说明的，TForm1是TForm的子类，直接继承了它所有的域、方法、属性和事件。例如窗体的颜色属性Color就是在TForm中说明的。当您在工程中加入一个新窗体时，就等于加入了一个基本模型。通过不断地在窗体中加入部件，您就自行定义了一个新的窗体。要自定义任何对象，您都将从已经存在的对象中继承域和方法，建立一个该种对象的子类。例如对象TForm1就被说明为对象TForm的子类，拥有一个窗体部件的基本属性或方法。只有当您在窗体中加入了部件或编写了事件处理过程时，Form1才成为您自己的类型。

一个比较特殊的对象是从一个范围较广或较一般的对象中继承下来的，它是这个特别对象的祖先，这个对象则称为祖先的后代。一个对象只能有一个直接的祖先，但是它可以有许多后代。TForm是TForm1类型的祖先，所有的窗体对象都是TForm的后代。

用F1查阅窗体的在线帮助时，您会发现TForm被称为component(部件)。这是因为所有的部件都是对象。

在这个结构中所有的部件都是对象。部件类型TComponent从TObject类型中继承数据和程序代码，并具有额外的可以用作特殊用途的属性、方法、事件，所以部件可以直接和用户打交道，记录它的状态并存贮到文件中等等。控制类型TControl从TComponent中继承而来，又增加了新的功能，如它可以显示一个对象。在上图中，虽然TCheckBox不是直接由TObject继承来的，但是它仍然有任何对象所拥有的属性，因为在VCL结构中，TCheckBox终究还是从TObject 中继承了所有功能的特殊对象，但它还有些自行定义的独到的功能，如可以选择记录状态等。

2.2.3 对象的范围

2.2.3.1 关于对象的范围

一个对象的范围决定了它的数据域、属性值、方法的活动范围和访问范围。在一个对象的说明部分说明的数据域、属性值、方法都只是在这个对象的范围中，而且只有这个对象和它的后代才能拥有它们。虽然这些方法的实际程序代码可能是在这个对象之外的程序库单元中，但这些方法仍然在这个对象的范围内，因为它们是在这个对象的说明部分中说明的。

当您在一个对象的事件处理过程中编写程序代码来访问这个对象的属性值、方法或域时，您不需要在这些标识符之前加上这个对象变量的名称。例如，如果您在一个新窗体上加入一个按钮和一个编辑框，并为这个按钮编写OnClick事件处理过程：

procedure TForm1.Button1Click(Sender:Tobject);

begin

Color :=clFuchsia;

Edit1.Color :=clLime;

end;

其中的第一行语句是为整个窗体Form1着色。您也可以编写如下：

Form1.Color :=clFuchsia;

但您可以不必加上Form1.，因为Button1Click方法是在TForm1对象的范围里。当您在一个对象的范围中时，您可以省略所有这个对象中的属性值、方法、域之前的对象标识符。但是当您编写第二个语句改变编辑框的底色时，因为此时您想访问的是TEdit1对象的Color属性，而不是TForm1类型的，所以您需要通过在属性前面加上编辑框的名称来指明Color属性值的范围。如果不指明，Delphi会象第一个语句一样，将窗体的颜色变成绿色。因为Edit1部件是在窗体中的，它是窗体的一个数据域，所以您同样不必指明其从属关系。

如果Edit1是在其他窗体中，那么您需要在编辑框之前加上这个船体对象的名称了。例如，如果Edit1是在Form2之中，那它是Form2说明的一个数据域，并位于Form2的范围中，那么您需要将第二句改为：

Form2.Edit1.Color := clLime;

而且需要把Unit2加入Unit1的uses子句中。

一个对象的范围扩展到这个对象的所有后代。TForm的所有属性值、方法和事件都在TForm1的范围中，因为TForm1是TForm的后代。您的应用程序不能说明和祖先的数据域重名的类型、变量等。如果Delphi显示了一个标识符被重复定义的信息，就有可能是一个数据域和其祖先对象(例如TForm)的一个数据域有了相同的名称。可以尝试改变这个标识符的名称。

2.2.3.2 重载一个方法

您可以重载(Override)一个方法。通过在后代对象中说明一个与祖先对象重名的方法，就可以重载一个方法。如果想使这个方法在后代对象中作和祖先对象中一样的工作但是使用不同的方式时，您就可以重载这个方法。Delphi不推荐您经常重载方法，除非您想建立一个新的部件。重载一个方法，Delphi编译器不会给出错误或警告提示信息。

2.2.4 对象公有域和私有域的说明

当使用Delphi的环境来建立应用程序时，您可以在一个TForm的后代对象中加入数据域和方法，也可以通过直接修改对象类型说明的方法来为一个对象加上域和方法，而不是把一个部件加入窗体或事件处理过程中。

您可以在对象的Public或Private部分加入新的数据域和方法。Public和Private是Object Pascal的保留字。当您在工程中加入新的窗体时，Delphi开始建立这个新窗体对象。每一个新的对象都包含public和private指示，以便您在代码中加入数据域和方法。在public部分中说明其它库单元中对象的方法也可以访问的数据域或方法。在private部分的说明有访问的限制。如果您在private中说明域和方法，那么它在说明这个对象的库单元外是不透明的，而且不能被访问。private中可以说明只能被本库单元方法访问的数据域和本库单元对象访问的方法。过程或函数的程序代码可以放在库单元的implementation部分。

2.2.5 访问对象的域和方法

当您想要改变一个窗体对象的一个域的某个属性，或是调用它的一个方法时，您必须在这个属性名称或调用方法之前加上这个对象的名称。例如，如果您的窗体上有一个编辑框部件，而您需要在运行中改变它的Text属性，需要编写下列的代码：

Edit1.Text := 'Welcome to Delphi';

同样，清除编辑框部件中选中的文本，可以调用TEdit部件的相应方法：

Edit1.ClearSelection;

如果您想改变一个窗体对象中一个对象域的多个属性或调用多个方法时，使用with语句可以简化您的程序。with语句在对象中可以和在记录中一样方便地使用。下面的事件处理过程在响应OnClick事件时，会对一个列表框作多个调整：

procedure TForm1.Button1Click(Sender:TObject);

begin

ListBox1.Clear;

ListBox1.MultiSelect :=True;

ListBox1.Item.Add('One');

ListBox1.Item.Add('Two');

ListBox1.Item.Add('Three');

ListBox1.Sorted :=Ture;

ListBox1.FontStyle :=[fsBold];

ListBox1.Font.Color :=clPurple;

ListBox1.Font.Name :='Times New Roman';

ListBox1.ScaleBy(125,100);

end;

如果使用了With语句，则程序如下：

procedure TForm1.Button1Click(Sender:TObject);

begin

with (ListBox1) do

begin

Clear;

MultiSelect :=True;

Item.Add('One');

Item.Add('Two');

Item.Add('Three');

Sorted :=Ture;

FontStyle :=[fsBold];

Font.Color :=clPurple;

Font.Name :='Times New Roman';

ScaleBy(125,100);

end;

end;

使用with语句，您不必在每一个属性或方法前加上ListBox1标识符，在With语句之内，所有的属性或调用方法对于ListBox这个对象而言都是在它的范围内的。

2.2.6 对象变量的赋值

如果两个变量类型相同或兼容，您可以把其中一个对象变量赋给另一个对象变量。例如，对象TForm1和TForm2都是从TForm继承下来的类型，而且Form1和Form2已被说明过，那么您可以把Form1赋给Form2:

Form2 :=Form1;

只要赋值的对象变量是被赋值的对象变量的祖先类型，您就可以将一个对象变量赋给另一个对象变量。例如，下面是一个TDataForm的类型说明，在变量说明部分一共说明了两个变量：AForm和DataForm。

type

TDataForm = class(TForm)

Button1:TButton;

Edit1:TEdit;

DataGrid1:TDataGrid;

Database1:TDatabase;

TableSet1:TTableSet;

VisibleSession1:TVisibleSession;

private

{私有域说明}

public

{公有域说明}

end;

var

AForm:TForm;

DataForm:TDataForm;

因为TDataForm是TForm类型的后代，所以Dataform是AForm的后代，因此下面的赋值语句是合法的：

AForm :=DataForm;

这一点在Delphi中是极为重要的。让我们来看一下应用程序调用事件处理过程的过程，下面是一个按钮部件的OnClick事件处理过程：

procedure TForm1.Button1Click(Sender:TObject);

begin

end;

您可以看到TObject类在Delphi的Visual Component Library的顶部，这就意味着所有的Delphi对象都是TObject的后代。因为Sender是TObject类型，所以任何对象都可以赋值给它。虽然您没有看见赋值的程序代码，但事实上发生事件的部件或控制部件已经赋给Sender了，这就是说Sender的值是响应发生事件的部件或控制部件的。

您可以使用保留字is来测试Sender以便找到调用这个事件处理过程的部件或控制部件的类型。Delphi中的一个显示drag-and-drop的DRAGDROP.DPR工程。加载它，可以查阅到DROPFONT.PAS库单元的代码，在Memo1DragOver方法中检查了一个对象变量的类型。在这种情形下，参数是Source而不是Sender。

procrdure TForm1.Memo1DragOver(SenderSource:TObject;X,Y:integer;

State:TDragState;var Accept:Boolean);

begin

Accept :=Source is TLabel;

end;

Source参数也是TObject类型，Source被赋值为那个被拖曳的对象。用Memo1DragOver方法的目的是确保只有标签可以被拖曳。Accept是布尔型参数，如果Accept为True，那么用户选择的部件可以被拖曳；反之当Accept的值为False时，用户就不可以拖曳选择控制部件。is保留字检查Source是否TLabel的类型，所以Accept只有在用户拖曳一个标签时才为真，并作为变参输出到函数之外。

下面的drag-and-drop展示的Memo1DragDrop事件处理过程中也使用了Source参数。这个方法是为了把Memo部件的字型改变成和放入这个备注控制部件的标签一样的字型：

procedure TForm1.Memo1DragDrop(SenderSource:TObject;

X,Y:Integer);

begin

Memo1.Font := (Source as TLabel).Font;

end;

当您在这个事件处理过程中编写赋值语句时，开发人员并不知道用户会放入哪一个标签，只有通过参考这个标签的名称(Source as TLabel)用户才能知道，并把标签类型赋给Memo1.TFont。Source包含了用户拖放控制部件的名称，只有当Source是一个标签时，这个事件处理过程才允许这个赋值发生。

2.2.7 建立非可视化对象

您在Delphi中使用的大部分对象都是您在设计和运行期间可以看见的部件，例如编辑框、按钮等；一些部件，如通用对话框(Common dialog box)等，在设计时看不见，而在运行时可以看见；另外有些部件，例如计时器(Timer)、数据源(Data Source)部件等，在程序的运行期间没有任何可视化的显示，但您却可以在您的应用程序中使用它们。

2.2.7.1说明一个非可视化对象

下面，通过一个简单的例子讲述如何建立自己的非可视化对象：

您可以用如下的方法，建立一个自己的TEmployee非可视化对象：

type

Temployee = class(TObject);

Name := String[25];

Title := String[25];

HourlyPayRate : Double;

function CalculatePayAmount:Double;

end;

在这种情况下，TEmployee从TObject继承下来，且包含三个域和一个方法。把您建立的类型说明放在库单元中的说明部分，并和窗体说明放在一起。在这个程序库单元的变量说明部分，说明一个新类型的变量：

var

Employee : TEmployee;

2.2.7.2用Create方法建立对象实例

TEmployee只是一个对象类型。除非通过一个构造函数的调用从而被实例取代或创建，否则一个对象并不存储在内存中。构造函数是一个方法，它为新对象配置内存并且指向这个新的对象。这个新的对象也被称为这个对象类型的一个实例。

建立一个对象的实例，需要调用Create方法，然后构造函数把这个实例赋给一个变量。如果您想说明一个TEmployee类型的实例，在您访问这个对象的任何域之前，您的程序代码必须调用Create。

Employee := TEmployee.Create;

Create方法并没有在TEmployee类型中说明，它继承自TObject类型。因为TEmployee是TObject的子类，所以它可以调用Create方法而创建一个TEmployee实例。然后把它赋给Employee变量。在创建了一个这样的对象后，您就可以象使用其他的Delphi对象一样访问Employee对象了。

2.2.7.3 撤销对象

当您使用完对象后，您应该及时撤销它，以便把这个对象占用的内存释放出来。您可以通过调用一个注销方法来撤销您的对象，它会释放分配给这个对象的内存。

Delphi的注销方法有两个：Destroy和Free。Delphi建议使用Free，因为它比Destroy更为安全，同时调用Free会生成效率更高的代码。

您可以用下列的语句释放用完的Employee对象：

Employee.Free;

和Create方法一样，Free方法也是TEmployee从TObject中继承过来的。把您的注销放在try…finally程序模块的finally部分，而把对象的程序代码放在try部分是编程的好习惯。这样，即使您的程序代码在使用对象时发生了异常事件，也会确保您为这个对象分配的内存会被释放。关于异常处理和try…finally程序模块的信息以及建立非可视化对象的例子，在后文中还将仔细讲述。


第三章 字符串列表及应用（一）

DELPHI基础教程

第三章　字符串列表及应用（一）

Delphi应用程序经常要处理字符串列表，如组合框和列表框中的字符串，TMemo部件的文本行，屏幕支持的字体列表，TNotebook部件的tabs属性，字符串网格的行、列等等。

    虽然应用程序以不同的方法使用这些列表，但Delphi通过一个叫字符串列表(Tstrings)的对象提供统一的界面，并且在不同场合可相互转化。例如，可以在TMemo部件中编辑某一字符串，并把它当成列表框中列表项使用。

    在Delphi集成开发环境中也经常要使用字符串列表。如在Object Inspector窗体的取值栏中常列有Tstrings字符，双击该字符，将弹出字符列表编辑器，如图3.1，在编辑器中可进行编辑、加入、删除等操作 。

    在运行状态时也可以操作字符串列表，常见的字符串列表操作如下：

    ● 列表中操作字符串

    ● 装载、保存字符串列表

    ● 创建字符串列表

    ● 在字符串列表中加入对象

本章将介绍字符串列表的常用操作及简单应用。

3.1 字符串列表的常用操作

3.1.1 列表中操作字符串

    在Delphi应用程序中，经常要对列表中的字符串进行操作。例如，设计时修改字符串列表属性。

    常见的字符串操作如下：

    ● 计算列表中字符串数目

    ● 访问指定字符串

    ● 查找字符串的位置

● 往列表中加入字符串

    ● 删除列表中的字符串

    ● 在列表中移动字符串

    ● 复制一个完整的字符串列表

    ● 复制列表中的字符串

3.1.1.1 计算列表中的字符串数目

    使用Count属性可计算列表中的字符串数目。Count是只读属性，用以指示列表中字符串列表数目。因为字符串列表是以零开始索引，因而Count比列表的最大索引数大一。

    例如，应用程序想计算当前屏幕支持的字体数目，可查找屏幕对象的字体列表，该列表包含了屏幕支持的所有字体的名字。

            FontCount:=Screen.Fonts.Count;

3.1.1.2 访问指定字符串

    字符串列表有一个可索引的Strings属性，可象使用字符串数组一样使用Strings。例如，列表中第一个字符串为Strings[0]。因为Strings属性为字符串列表中最常用的属性，Strings属性可做为字符串列表的缺省属性，即使用时可省略Strings标识符。

    要访问字符串中的指定字符，可查找该字符的起始位置或索引。字符串数目是以零开始记数的。如果列表中有三个字符串，其索引范围为0..2。

    以下代码是等价的：

        Memol.Lines.Strings[0]:='This is the first line.';

Memol.Lines[0]:='This is the first line.';

3.1.1.3 查找字符串的位置

      Indexof方法可查找指定字符串的位置。Indexof有一个字符串类型的参数，方法返回列表中匹配字符串的位置。如果列表中无匹配字符串，将返回- 1。

Indexof方法只能查找完整字符串，即必须完全匹配整个字符串。如果只匹配部分字符串，必须编写相应代码。

      以下代码判定列表中是否有指定字符串：

if FileListBox1.Items.IndexOf('AUTOEXEC.BAT') > -1 then

begin

Color := clYellow;

Label1.Caption := 'You are in the root directory!';

end;

3.1.1.4 在列表中加入字符串

    有两种方式往列表中加入字符串：可把字符串加到列表的最后，也可插入列表之中。

    要把字符串加至列表尾部，使用Add方法，把字符串作为参数传递。

    要把字符串插入列表中，使用Insert方法，传递两个参数：插入的位置和字符串。

    例如，要把“Three”插入至列表中的第三个位置，使用代码Insert(2,'Three')。如果列表中的字符不到2个，Delphi将产生超出索引范围的异常(关于异常详见十二章)。

3.1.1.5 在列表中移动字符串

    应用程序可以在列表中把指定字符串移至另一个位置，如果字符串与某个对象相连，则该对象与字符串同步移动。

    Move方法可实现字符串的移动，它有两个参数：现行位置和要移动的位置。以下代码把第三个字符串移至第五的位置：

      Move(2,4);

3.1.1.6 删除列表中的字符串

    使用Delete方法可以删除指定的字符串。Delete的参数是指定字符串的位置，如果不知道字符串的位置，可使用Indexof方法。

    要删除字符串列表中所有的字符串，可使用Clear方法。

    以下代码删除列表框中的指定字符串：

        With ListBox1.Items do

begin

if Indexof('bureaucracy')>-1 then

Delete (Indexof('bureaucracy'));

end;

3.1.1.7 复制完整的字符串列表

    把一个列表复制到另一个列表相当于把源列表赋值给目标列表，即使列表从属于不同的部件，Delphi也可以进行这种复制。

    复制列表将覆盖掉目标列表，如果要把源列表加到目标列表的尾部，使用Addstrings方法。

    以下代码分别为复制列表和连接列表：

Outline1.Lines:=ComboBox1.Items;

Outline1.Addstrings(ComboBox1.Items);

3.1.1.8 重复操作列表中的字符串

    很多情况需要对表中的每一个字符串进行操作，如改变字符串的大小写。象这种重复操作可以用 for 循环来实现，同时使用列表的整数类型的索引。

    以下代码对列表框的字符串进行重复操作。当用户按下按钮时，对列表框中的字符串进行大小写转换。

procedure TForm1.Button1Click(Sender: TObject);

var

I: Integer;

begin

for I := 0 to ListBox1.Items.Count -1 do

ListBox1.Items[I] := UpperCase(ListBox1.Items[I]);

end;

3.1.2 装载、保存字符串列表

    应用程序可以非常方便的把Delphi字符串列表存入文本文件，或者从文本文件中重新装载(或装入另一个不同的列表)，字符串列表有专门的方法处理这类操作。

    使用LoadFromFile方法从文件中装载字符串列表，LoadFromFile从文本文件中把每一行字符串装入列表中。

    把列表保存在文件中使用SaveToFile方法，使用时传递文件名的参数。如果文件不存在，SaveToFile将创建它，否则将用列表覆盖现有文件内容。

    以下代码装入AUTOEXEC.BAT的文件，并以AUTOEXEC.BAK为文件名进行备份。

    procedure TForm1.FormCreat(sender:TObject);

var

FileName:String;

begin

FileName:='C:\AUTOEXEC.BAT';

With Memo1 do

begin

LoadFromFile(FileName)

SaveToFile(ChangeFileExt(FileName,'BAK'));

end;

end;

3.1.3 创建新的字符串列表

    大多数情况下，应用程序使用的字符串列表是做为部件的某一部分，因此不必创建列表，但Delphi允许创建不依赖部件的字符串列表。

    值得注意的是程序创建的字符串列表必须在使用完之后，释放列表所占用的内存空间。有两种不同的情况需要处理：一是程序以简单的方式创建、使用、释放字符串列表；二是由程序创建，在运行期间均可能使用，在程序终止前释放。这两种情况主要取决于是创建短期字符串列表还是长期字符串列表。

3.1.3.1短期字符串列表

    短期字符串列表用于处理简单事物。程序在同一处创建、使用、释放列表。这是最安全的使用字符串列表的方法。

    因为字符串列表要为自己和它的字符串分配内存，所以要用try..finally对列表进行保护，以确保发生异常后释放列表所占用的内存空间。

    创建短期字符串列表的基本步骤为：

    1. 构造字符串列表对象；

    2. 在try..finally块中使用列表；

    3. 在finally后释放列表空间。

    以下代码创建列表、使用列表、最后释放列表空间：

    procedure TForm1.Button1Click(Sender:Tobject);

var

TemList:TStrings;

begin

Templist:=TStringList.Create;

try

{ use the string list }

finally

Templist.Free;

end;

end;

3.1.3.2 长期字符串列表

    如果要在程序运行的任何时候使用字符串列表，则需在程序开始运行时就创建列表，并在程序终止前释放。

    运行时创建字符串列表的步骤为：

    1. 在程序主窗体对象的域中加入TStringsList类型的域；

    2. 在主窗体的OnCreate事件中创建句柄，该事件句柄在主窗体显示前运行；

    3. 在创建事件句柄后，创建字符串列表对象；

    4. 在主窗体的OnDestroy事件创建句柄，该事件句柄在主窗体消失之前运行。

    这样，在程序运行过程中，任何过程、事件均能访问该字符串列表。

    以下代码在程序中加入了一个Clicklist的字符串列表，用户每按一次鼠标键，程序往Clicklist中加入一字符串，程序结束前把该列表存入文件。

unit Unit1;

interface

uses WinTYpes, WinProcs, Classes, Graphics, Forms, Controls, Apps;

type

TForm1 = class(TForm)

procedure FormCreate(Sender: TObject);

procedure FormDestroy(Sender: TObject);

procedure FormMouseDown(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y: Integer);

private

{ Private declarations }

public

{ Public declarations }

ClickList: TStrings; {declare the field}

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.FormCreate(Sender: TObject);

begin

ClickList := TStringList.Create; {construct the list}

end;

procedure TForm1.FormDestroy(Sender: TObject);

begin

ClickList.SaveToFile(ChangeFileExt(Application.ExeName, '.LOG'));

{save the list}

ClickList.Free; {destroy the list object}

end;

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y: Integer);

begin

ClickList.Add(Format('Click at (%d, %d)', [X, Y])); {add a

string to the list}

end;

end.


第三章 字符串列表及应用（二）

DELPHI基础教程

第三章　字符串列表及应用（二）

3.1.4 往字符串列表中加入对象

    字符串列表除了能在Strings属性中贮存字符串外，还可以在Objects属性中贮存对象。与Stings一样，Objects也是可以索引的，它是对象的索引。

    在应用程序使用列表中的字符串与列表中是否有对象没有多大关系。除非程序特地访问对象，否则Objects中的内容不变，Delphi只是保存了这些信息，应用程序在必要时对其进行操作。

    有些字符串列表忽略加入的对象。如TMemo部件中代表行的列表对加入其中的对象不保存。还有一些字符串列表，把对象与字符串联系起来，如TNotebook部件的Pages属性，它同时保存着页的名字和代表页的对象。如果应用程序往Pages中加入或删除字符串，Delphi自动的加入或删除与之相应的对象。

    虽然程序可分配任何类型的对象到列表中，但最常用的是在自画式控制中把位图与字符串联系起来，注意位图与字符串成对使用。

    Delphi在释放对象的内存空间时并不破坏与之相应的字符串。

3.1.4.1 操作字符串列表中的对象

    对于字符串的每一种操作方法，列表中的对象均有相应的方法。例如，应用程序可利用对象的索引来访问对象。与字符串不同的是，不能省略Objects，因为Strings才是列表的缺省属性。

    表3.1中总结了字符串对字符串和对象操作的方法。

表3.1 TStrings的字符串属性和对象操作属性的方法

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

操 作 　　　字　符　串　　　　　　　　对　　　象

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

     访　　问　　　　　 Strings属性 Objects属性

     加入项目　　　　　 Add 方法　　　　　　 AddObjects方法

     插入项目　　　　　 Insert方法　　　　　　InsertObjects方法

项目定位　 　　　 Indexof方法 IndexofObject方法

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

    Delete,Clear,More操作整个项目，即删除字符串时把相应的对象也删除了。但LoadFromFile,SaveToFile方法只对字符串进行操作。

3.1.4.2 加入对象

    如果把对象与已存在的字符串联系起来，Delphi将分配给该对象同样的索引号。例如，一个叫Fruits的列表中有字符串('apple')，程序可将名为AppleBitmap的位图与apple字符相联系。

    With Fruits do Objects[Indexof('apple')]:=AppleBitmap;

另一种方法是调用列表的AddObject方法，AddObject有两个参数：字符串和对象，如下：

    Fruits AddObject('Apple',AppleBitmap);

3.2 字符串列表应用

Delphi应用程序经常要用到字符串列表，我们编写的strlist. dpr是应用字符串列表的简单程序。程序运行状态如图3.2所示。列表框列出了屏幕支持的各种字体名称，并且以名称所代表的字体显示在列表中；Tabs的标签不只以字符串来表示，而且附有位图。这就是所谓的自画式控制。下面介绍字符串列表在自画式控制中的应用。

列表框、组合框、Tabset部件中有一个叫“自画(Ownerdraw)”的风格，它能替代Windows的文本输出，部件的自画式控制在运行状态对每个项目进行重新绘制。最常用的是用图像代替文本输出。

    自画式控制有一个共同特点：它们都包含有项目列表，缺省情况下这些列表就是字符串列表，Windows把它们当成文本显示。Delphi可以把字符串列表与某一对象相联系，这就使得应用程序能用对象来绘制项目。

    通常，创建自画式控制有以下三个步骤：

    1. 设置自画风格；

    2. 把图像对象加入字符串列表中；

    3. 绘制自画项目。

3.2.1 设置自画风格

    每个能进行自画式控制的部件都有一个叫Style的属性，Style决定部件是以缺省方式还是以自画方式绘制项目。

    对于列表框和组合框，也有自画式风格选项，表3.2列出了Style的取值及含义。

表3.2 Style的取值及含义

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

  Style 含　　义　　　　　　　　 举　　　例

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

  Fixed 每个项目有相同的高度 　　 1bOwnerDrawFixed

               高度由ItenHeight属性决定　　 csOwnerDrawFixed

Varible 每个项目有不同的高度 　　　 1bOwnweDrawVarible

               由运行数据决定　　 csOwnerDrawVarible

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

    tab-set与字符串网格的Style属性通常是Varible.

在Strlist程序中，列表框与tab-set取值如表3.3：

表3.3 列表框与tab-set的取值

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

        名称　　　　　　　　　style

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

ListBox1 lbOwnerDrawVariable

Tabset1 tsOwnerDrawVariable

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

3.2.2 把图像加入字符串列表

    上节已介绍如何把对象加入字符串列表，例程把位图对象加入Tabset1的Tabs中：

procedure TForm1.FormCreate(Sender: TObject);

var

Bitmap: TBitMap;

begin

Listbox1.Items := Screen.Fonts;

Bitmap := TBitmap.Create;

Bitmap.LoadFromFile('PHONE.BMP');

Tabset1.Tabs.AddObject('phone',Bitmap);

Bitmap := TBitmap.Create;

Bitmap.LoadFromFile('PRINTER.BMP');

Tabset1.Tabs.AddObject('printer ',Bitmap);

end;

3.2.3 绘制自画项目

    当部件的Style属性是自画式时，Windows不再绘制部件，相反Windows为每个可视项目产生事件，而应用程序必须在事件中绘制项目。

    在应用程序绘制自画控制之前，Windows产生测量项目事件，这个事件告诉程序项目显示的位置 。

    通常由Windows决定项目显示的大小，但应用程序可以处理这个事件并自己选择显示区域。例如，程序要用位图代替文本显示，则需要把区域设置成位图的大小。测量项目事件的名称随部件的名称不同而不同，对于列表框和组合框，该事件叫OnMeasureItem。对于Tabset，该事件叫OnMeasureTab。

    测量项目事件有两个重要参数: 项目索引号与项目的大小。这个大小是变化的。后继项目的输出位置由前面项目的大小决定。例如，在自画式列表框中，如果应用程序把第一个项目的高度设置成5个象素点，则第二个项目在第六个象素点开始输出。列表框和组合框中，应用程序只能设置成项目的高度，而项目的宽度就是部件的高度。在Tabset中，tabs的宽度是可变的，而高度则是固定的。自画式网格允许应用程序改变网格单元的高度和宽度。

    OnMeasureItem的声明如下：

ListBox1 MeasureItem(Control: TwinControl;Index: Integer; var Height: Integer);

    例程中响应OnMeasureItem事件的代码如下:

procedure TForm1.ListBox1MeasureItem(Control: TWinControl; Index: Integer;

var Height: Integer);

begin

with ListBox1.Canvas do

begin

Font.Name := ListBox1.Items[Index];

Height := TextHeight('A');

end;

end;

procedure TForm1.TabSetMeasureTab(Sender: TObject; Index: Integer;

var TabWidth: Integer);

var

BitmapWidth: Integer;

begin

BitmapWidth := TBitmap( TabSet1.Tabs.Objects[Index]).Width;

Inc(TabWidth, 2 + BitmapWidth);

end;

  在OnMeasureItem事件发生后，Windows激发一个叫OnDrawItem的事件，这个事件也随部件名称不同而不同，常见的有OnDrawItem、OnDrawTab、OnDrawCell。

    OnMeasureItem的声明如下：

    DrawItem( Control: TWinControl; Index: integer; Rect: TRect; State: TOwnerDraw);

其中Control是包含项目的部件引用

        Index　是项目的索引号

        Rect　 是绘制的矩形

        State 是项目的状态，如选中，得到焦点等。

    在例程的列表框中，所列项目是屏幕支持的各种字体名称，当列表框发生OnDrawItem事件时，程序把输出字体设置成该项目所代表的字体，因而列表框的项目呈现出不同的字体，其代码如下：

procedure TForm1.DrawItem(Control: TWinControl; Index: Integer;

Rect: TRect; State: TOwnerDrawState);

begin

with ListBox1.Canvas do

begin

FillRect(Rect);

Font.Name := ListBox1.Items[Index];

TextOut(Rect.Left, Rect.Top, ListBox1.Items[Index]);

end;

end;

    在Tabset部件中，则把位图与文本同时输出，其代码如下：

procedure TForm1.TabSet1DrawTab(Sender: TObject; TabCanvas: TCanvas;

R: TRect; Index: Integer; Selected: Boolean);

var

Bitmap: TBitmap;

begin

Bitmap := TBitmap(TabSet1.Tabs.Objects[Index]);

with TabCanvas do

begin

Draw(R.Left, R.Top + 4, Bitmap);

TextOut(R.Left + 2 + Bitmap.Width,

R.Top + 2, TabSet1.Tabs[Index]);

end;

end;


第四章 文本编辑器的设计（一）

DELPHI基础教程

第四章　文本编辑器的设计（一）

    本章介绍多文本界面(MDI)、多页面界面(MPI)技术；VCL库中TMemo，TEdit 控件以及有关文本编辑的常用对话框的使用。我们开发的MPIEdit.dpr是一个文本编辑的实用程序，可实现如下功能：

    ● MDI的编辑环境

    ● MPI的编辑环境

    ● 创建打开、编辑、保存文件

    ● 查找、替换文件中指定的字符串

    ● 复制、粘贴、剪切字符串

    ● 设置文件字体大小

    ● 打印文件

    本章将通过MPIEdit实用程序逐一介绍在Delphi中如何实现上述功能。

    文本编辑器是一种常用的应用程序。用户在编辑器中编辑多种文件，在多个文件之间进行数据交换，对文件进行各种属性设置，并按自己要求打印文件。

4.1 多文本界面

    多文本界面是一种在一个应用程序中同时打开两个或更多文件的界面形式。例如在字处理程序可同时打开多个文件，用户可在多个文件中方便地进行切换.

MDI应用程序提供了一种方便的方式，使得用户在同一工作区域内对多个文档进行观察和交换数据。MDI工作区域可分为父窗体和子窗体，在Dephi的MDI应用程序中，父窗体通常是程序的主窗体。

    在MDI中，父窗体之外的窗体称为子窗体，文档或其它数据在子窗体打开。这些文档可以是相同的文件格式，或在应用程序支持下也可以是不同的文件格式。

    在设计阶段，可创建 MDI 父窗体作为应用程序主窗体， 亦可创建子窗体样板。Delphi允许创建多个子窗体类型，但MDI应用程序只支持其中的一种。

    本节讲述创建MDI应用程序的基本步骤：

    ● 创建主窗口

    ● 创建子窗口

    ● 创建主窗口菜单

    ● 融合菜单

    ● 运行时创建子窗口

4.1.1 创建父窗口

    在MDI应用程序中，主窗口为应用文档提供一个工作区域。这个区域可打开一个或多个子窗口，创建父窗口是建立MDI应用程序的第一步。

    创建父窗口与其它窗口类似，不同之处在于设置窗体的FormStyle属性。

    FormStyle属性可决定一个窗体是父窗口还是子窗口，或不是MDI类型。 只能在设计阶段确定FormStyle。在Object Inspector窗口中将FormStyle属性设置成fsMDIForm。值得注意的是应当把父窗口定义为应用程序的主窗体，否则程序编译会出错。

4.1.2 创建子窗口

    设计阶段可创建子窗口的样板，用户在运行进使用样板的实例。子窗口是缺省可见的，如果应用程序在运行进创建子窗口，不要让Delphi自动地创建。

    创建子窗口时将窗体的FormStyle属性设置为fsMDIChild。如果程序在运行时创建子窗口，则

    1. 选择OPtions|Project菜单，系统弹出自动创建列表对话框；

    2. 在自动创建列表中选中子窗口；

    3. 单击>按钮将子窗口移至可得到(Available)窗体列表；

    4. 并单击OK按钮退出。

4.1.3 创建应用程序菜单与菜单融合

    父窗口的菜单应作为应用程序主菜单。如果子窗口有菜单， 则当子窗口在运行获得焦点并最大化时，子窗口的菜单项将融合父窗口菜单。

    创建父窗口与子窗口菜单的方法与创建普通窗体菜单类似， 详细步骤见第一章。菜单融合是指程序运行过程中，子菜单与父窗口菜单的相互作用。 如当子窗口获得焦点时，子窗口的菜单或插入主窗口的菜单中，或将替换部分或全部的父窗口菜单。

    进行菜单融合需设置的两个属性：

    ● 窗体的Menu属性

    ● 菜单项的GroupIndex属性

Menu属性定义窗体的活动菜单，而菜单融合只对活动菜单进行。 如果窗体有多个菜单部件，运行时可通过以下代码进行改变：

    Form1.Menu := SecondMenu;

GroupIndex属性决定出现在菜单条中各菜单项的位置，在菜单融合中，GroupIndex 将

决定融合菜单是插入还是替换主窗体菜单条中的菜单。

    GroupIndex的缺省值是0，可以用下规则确定其值：

   1. 数值越小，菜单的位置越靠左。

    例如：GroupIndex为0的菜单将出现在菜单条中的最左端。随着GroupIndex数值的增大，菜单项依次向右排列。

    2. 若需替换主菜单中的某一菜单项，则将子菜单相应菜单项的GroupIndex设为与之相等的值。这条规则适合一个或多个菜单项。例如，主菜单中的"Edit"菜单项的GroupIndex 的值为1。将子菜单的一个或多个菜单项的GroupIndext的值设为1，则在运行时，这些菜单项替换主窗口的"Edit"菜单。

    将同一窗体的多个菜单项的GroupIndex设为相同值， 原有的排列顺序在菜单融合时将保持

不变。

    3. 若要在菜单融合时插入菜单项，需在主菜单中预留数值“位置”。例如，主菜单的两菜单项数值为0,5，则子菜单GroupIndex数值为1,2,3,4的菜单在融合时将插入其中。

    在使用MDI界面时，用户通常会打开多个窗体。为了使用户方便地进行窗体切换，常设有一个进行切换的菜单项.此菜单列出了打开窗体的名称，当用户选择其中的一个时，程序进行相应的窗体切换。在Delphi的MDI设计时，可非常方便地实现这一功能。方法是将父窗口的WindowMenu设置成该菜单项的名字即可。

4.2 多页面界面

    多页面界面是一种非常友好的界面形式。它由一个窗体和多个页面组成， 关于每个页面的信息列在窗体底部的标签(Tabs)上，用户可通过选择标签来进行页面切换。 每次只有一个页面显示在窗体中。MPI较MDI使用更为方便，且切换速度更快。本章例程就是多页面界面的例子。另外Delphi集成开发环境中的代码编辑(Code Editor)窗体是MPI应用在文本编辑中的实例。在MPI中，一个窗体内的多个文件可以方便地进行切换和交换数据。

多页面界面分为静态MPI和动态MPI两种形式。静态MPI的标签数量固定，用户在事先设计好的多个页面上进行切换。象选择对话框(Option Dialog)就属于静MPI。动态MPI的标签数量不固定，由程序根据需要动态的产生或消除，象代码编辑窗体就是动态MPI，程序可根据用户的需要产生多个文本页面，也可以动态地关闭页面。利用Delphi的TNotebook和Ttabset 可十分方便地设计静态MPI。设计动态MPI则需要编写专门的代码。

4.2.1 静态多页面界面

    TNotebook,TTabSet可用来开发静态多页面界面。TNotebook部件能显示多页， 每页都有相应的控制。通常TNotebook与TTabset配合进行控制。TTabset 有一组水平的标签,每个标签可通过创建字符串列表进行某种控制。

    MPIEDit例程中的主窗体中有一个TNotebook 部件和 TTabSet 部件。 把两个部件的Aglin属性设置成bsTop和bsBotton，使它们分别处在窗体的上下两部分。为了使TTabSet与TNotebook配合工作，使用下代码：

    TabSet1.Tabs := Notebook1.Page;

另外，在TabSet的OnClick事件中定义下如下代码，可使用户在选择标签时开打相应的页。

    procedure TEditForm.TabSetClick(Sender : TObject);…

    begin

Notebook1.PageIndex := TabSet1.TabIndex;

…

end;

设计静态MPI时，可在部件窗体(Component Palette)的WIN3。1页面中选中TNotebook 部件，然后在Object inspector窗体中双击TNotebook的Pages属性，Dephi 将弹出对话框，用户可以在此确定Notebook的页数和字符串列表，如图4.6。关闭对话框后， 可对每一页进行设计，使用鼠标右按钮弹出快速菜单进行页面切换。

4.2.2 动态多页面界面

    使用Delphi进行静态MPI设计非常简单，进行动态MPI设计则需编写专门的代码。 对

于一个多页面文本编辑器，应能实现以下功能：

    ● 动态生成页面，每个页面均能进行文本编辑

    ● 动态关闭页面，直到窗体中只有一个页面为止

    ● 页面切换不影响各种文本编辑操作

    为了实现以上功能，程序中使用了动态页面类(TDynaPage)，其定义如下：

    type TDynaPage = Class(TObject);

该类可根据需要动态的产生页面， 每个页面上创建了可进行文本编辑的TMeno部件。

    procedure...

   puclic

CurPage : integer;

FileList : TSringList;

end;

CurPage表示当前用户选择的页面数，用户切换、增加、删除页面均影响CurPage 的值，CurPage初如化为零页。FileList存放打开或创建文件的名字以及与这些文件相关的编辑部件TMemo，页面动态创建、删除将影响FilstList的值。

    TNotebook部件创建后至少有一个页面，因此Pages属性不是空值，只要往Pages中加入字符串，Delphi自动地把该字符串与TPage类对象相联系。TPage类是TCustomEdit派生出来的，在对象浏览器(Object Browse)中可观察到TPage的数据成员和方法。静态生成的页面也是 TPage类。

    要创建多页面编辑器，必须从TPage的父件(Parent属件)创建相应编辑部件。但在动态创建页面时，TPage只是一个与字符串相联系的TObject类，不能写成：

    MemoParent := Notebook1.Pages.Object[ ];

在Delphi中，宣称对象和创建对象都是用指针来标识， 因此可用无类型指针进行指针传递。

    var

Pi : Pointer;

begin

Pi := Notebook1.Pages.Object[];

Memo.Parent := Pi;

end;

这样就可在TPage上动态创建编辑部件了。

    往Notebook1中动态生成页面时，页面应所相应的切换，TDynaPage. Notebook1.Tabset1有关的属性要作相应的调整。

TDynaPage的DynaAdd方法定义如下：

procedure TDynaPage.DynaAdd(Sender:TNotebook;FileName:String);

var

Pi:Pointer;

Memo:TMemo;

begin

Sender.Pages.add(FileName);

Pi:= Sender.Pages.Objects[Sender.Pages.Count-1];

DynaMemo(pi);

DynaPage.FileList.addObject(FileName,Memo1);

EditForm.TabSet1.Tabs := Sender.Pages;

EditForm.Tabset1.TabIndex:=Sender.Pages.Count-1;

EditForm.Notebook1.PageIndex := EditForm.Tabset1.TabIndex;

DynaPage.CurPage:= Sender.Pages.Count-1;

end;

procedure DynaMemo(Pi:Pointer);

var

Memo:TMemo;

begin

Memo:=TMemo.Create(Pi);

Memo.Parent:=Pi;

Memo.Align:=alClient;

Memo.borderStyle:=bsNone;

Memo.HideSelection:=False;

Memo1:=Memo;

end;

procedure TDynaPage.Del(Sender:TNotebook;No:integer);

var

Pi:pointer;

begin

Sender.Pages.delete(No);

EditForm.TabSet1.Tabs.delete(No);

Filelist.Delete(No);

DynaPage.CurPage:=EditForm.TabSet1.TabIndex;

Sender.PageIndex := EditForm.Tabset1.TabIndex;

Pi:=FileList.Objects[DynaPage.CurPage];

Memo1:=Pi;

EditForm.Caption:=Sender.Pages.Strings[DynaPage.CurPage];

end;

    当用户在多个页面中进行切换时，程序应当保证对当前页面进行编辑。 例如在多页编辑器中，用户选中某一页面，即可对该页面中的文件进行编辑、寻找、设置、打印等。为了实现这一功能，定义了一个TMemo类型的变量：Memo1,该变量没有实例化，每次调用DynaAdd,DynaDel方法均定把TabIndex指定页面的Memo指针传给Memo1。这样在程序运行中，始终有一个实例化的Memo指针赋给Memo1，而菜单中的文本编辑功能均对Memo1进行操作。这种指针传递就能保证对当前页进行操作。

    定义了TDynaPage后，只需在Open,Close菜单项中加入如下代码，即可方便的在用户打开关闭文件时创建成删除页面。

  procedure TEditForm.Close1Click(Sender: TObject);

begin

if DynaPage.CurPage<>0 then

DynaPage.Del(Notebook1,DynaPage.CurPage);

if Notebook1.Pages.count = 1 then

Close1.Enabled:=False;

end;

procedure TEditForm.Open1Click(Sender: TObject);

begin

if OpenDialog1.Execute then

begin

if not(OpenFile or NewFile) then

begin

OpenFile:=true;

Open(OpenDialog1.FileName);

Notebook1.Pages.Strings[0]:=ExtractFileName( OpenDialog1.FileName);

TabSet1.Tabs:=Notebook1.Pages;

end

else

begin

DynaPage.DynaAdd( Notebook1, ExtractFileName(OpenDialog1.FileName));

Open(OpenDialog1.Filename);

if Notebook1.Pages.count > 1 then

Close1.Enabled:=True;

end;

end;

end;

4.3 文本编辑部件及应用

4.3.1 TEdit 部件

TEdit部件是一个标准的编辑框，用户可在编辑框中输入数据。编辑框也可向用户显示数据。编辑时只能读写一行信息。

     TEdit的Text属性存放着用户输入的数据或向用户显示的数据，Modified属性用以标识 Text的数据是否改变，可通过设置Maxlength属性值来限制用户输入字符的个数量，CharCase

属性可定义编辑框中字符的大小写。如果设计者想禁止用户输入，可将ReadOnly属性设置成真值。编辑框也能用做密码输入框。通过设置PassWordChar 属性的值，可将用户输入的字符在编辑框中显示成指定的字符，如"*"号等。编辑框还可以进行字符选择操作、粘贴、复制和剪切操作。

4.3.2 TMemo 部件

TMemo部件与TEdit部件类似，能向用户显示数据，用户也可输入数据。与TEdit 部件

不同的是，TMemo部件可以处理多行文本，因此主要用于编辑文件。

    TMemo的Text属性只能在运行时才能访问。Modified属性用以标识Text的数据是否改

变，通过设置MaxLength属性值来限制用户输入字符的数量。

    如果把文本当成一个整体进行访问，可使用Text属性；若想逐行访问，则要使用Lines属性。Lines属性能对文件更方便地进行访问。Lines是TStrings类型的，因此可使用Add 、Delete方法，例如在Memo1中加入一行字符串的代码如下：

Memo1.Lines.Add('Another line is added');

通过Lines属性可以方便地把文件读入部件中，例程中使用下面的代码将文件读入Memo1：

Memo1.Lines.loadFromFile(Filename).

从TMemo 部件中剪切、复制、粘贴文本非常方便，只需使用 CutToclipboard ，CopyToClipBroad,PasteFromClipBoard方法，其代码如下：

    Memo1.CopyToClipboard

    Memo1.CutToClipboard

    Memo1.PasteFromClipboard

TMemo有一些属性，用以控制文本的显示效果。ScrollBars属性可以定义部件的水平滚动条和垂直滚动条。当文件字体改变时，使用AutoSize属性可使部件大小做相应的调整。设置WordWrap属性可以实现自动换行。

    例程中Edit|WordWrap菜单项提供了设置WordWrap的功能，并可根据WordWrap的值决定滚动条的形式。当WordWrap为真时，不需要水平滚动条， 并在菜单中作出检查记号。

其代码如下：

  procedure TEditForm.SetWordWrap(Sender: TObject);

begin

with Memo1 do

begin

WordWrap := not WordWrap;

if WordWrap then

ScrollBars := ssVertical else

ScrollBars := ssBoth;

WordWrap1.Checked := WordWrap;

end;

SetEditRect;

end;

TMemo部件提供了一组关于选择文本的属性和方法。如果想在部件成为当前控件时自动选择文本，可设置 AutoSelect 属性。运行时可用SelectAll 方法选中部件的全部文本。 Selstart属性返回选中文本的开始位置，SelText 包含着被选中的文本。SelLength属性返回选中文本的长度，这两个属性可用于字符串的查找和替换。下一节将详细讨论。

    TMemo的Modified属性是一个运行时才能得到的属性，可判断部件被创建时或Modified属性最后一次设置成假值之后，部件上的文本是否修改。如果修改了，Modified 将设成真值，反之假值。

    例程中在关闭文件时将测试文件的modified属性，如果文件修改后尚未保存， 将出现对话框，询问用户是否保存文件，其代码如下：

   procedure TEditForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);

var

DialogValue: Integer;

FName: string;

begin

if Memo1.Modified then

begin

FName := Caption;

DialogValue := MessageDlg(Format(SWarningText, [FName]), mtConfirmation,

[mbYes, mbNo, mbCancel], 0);

case DialogValue of

id_Yes: Save1Click(Self);

id_Cancel: CanClose := False;

end;

end;

end;

4.4 常用对话框的使用

   Delphi的可视部件类库(Vistual Component Liberty)中，有一组对话框部件，在对象选择板的Dialog 页面中可以找到。 本节着重介绍与文件编辑有关的字体对话框(TFontDialog Componement)，查找对话框(TFindDialog Componement) ，替换对话框(TReplace Dialog Componement)，文件对开对话框(TOpenDIalog Componement).

     应用这几个对话框可对文件进行字体设置、查找、替换等操作，但需要编写相应的代码。

4.4.1字体对话框部件

    字体对话框部件在应用程序中产生字体对话框， 用户可在对话框中进行字体选择和属性设置。用户选择字体并按下OK按钮之后，有关信息便贮存在部件的Font属性中。

    应用程序可通过调用字体对话框的Execult方法来显示对话框，当用户选择OK按钮时，Execult返回True值，否则返回Flase值。

应用程序可以使用Options属性来定义字体对话框的显示和行为方式：例如可在对话框中定义一个帮助按钮或指定出现在字体列表框中的字体。有关Options的主要取值如下表4.1：

表4.1 字体对话框的Options取值及含义

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

取值　　　 　　　　　　　含义

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

AdAnsiOnly 如果是真值，只能使用Window字符集，

fdEffects 如果是真值，对话框中显示颜色列表和效果检查框；用户可使

用效果检查框定义Strikout下划线文本；使用颜色列表定义字体

颜色。

fdForceFontExise 　　如果是真值，用户在字体组合框中输入字体名后选择OK按钮，

将出现一个用户字体无效的消息框。

fdNoOEMFont　　　 如果是真值，字体组合框中将不显示向量字体。

fdShowHelp 如果是真值，对话框显示Help按按钮。

fdWysiwyg 如果是真值， 只有打印和屏幕均可得到的字体才会出现在字体

                       组合框中。

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

    例程中(Edit/Font)菜单具有设置文本字体的功能，其代码如下：

    procedure TEditForm.SetFont(Sender : TObject);

begin

FontDialog.Font := Memo1.Font;

if FontDialog1.Execult then

Memo1.Fout := FontDialog1.Font;

SetEdit Rect;

end;


第四章 文本编辑器的设计（二）

DELPHI基础教程

第四章　文本编辑器的设计（二）

4.4.2查找对话框部件

    查找对话框部件为应用程序提供查找对话框， 用户可使用查找对话框在文本文件中查找字符串。

    可用Execult方法显示查找对话框，如图4.8。应用程序要查找的字符放到FindText属性中。Options 属性可决定查找对话框中有哪些选项。例如， 用户可选择是否显示匹配检查框。Options的常用选项如表4.2所示。

如果用户在对话框中输入字符并选择FindNext按钮，对话框将发生OnFind事件。

表4.2 查找对话框的Options属性的取值及含义

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

取值　　　　　　　　　　 含义

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

frDown 如果是真值，对话框中出现Down按钮，查找方向向下。如果是假

值，Up按钮将被选中，查找方向向上，frDown 值可在设计或运行

时设置。

frDisableUpDown 如果是真值，Up和Down按钮将变灰，用户不能进行选取；如果是

假值，用户可以选择其中之一。

frFindNext 如果是真值，应用程序查找在FindNext属性中的字符串。

frMatchCase 如果是真值，匹配检查框被选中。设计、运行时均可设置。

frWholeWord 如果是真值，整字匹配检查框被选中，设计、运行时均可设置。

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

    在OnFind事件中可使用Options属性来决定以何种方式查找。Find方法响应查找对话框的OnFind事件。

   procedure TEditform.Find(Sender: TObject);

begin

with Sender as TFindDialog do

if not SearchMemo(Memo1, FindText, Options) then

ShowMessage('Cannot find "' + FindText + '".');

end;

其中SearchMemo函数是Search单元中定义的，SearchMemo可在TEdit,TMemo，以及其它TCustomEdit派生类中查找指定的字符串。查找从控件的脱字号(^)开始， 查找方式由Options决定。如果向后查找从控件的StlStart处开始，如果向前查找则从控件的SelEnd处查找。

    如果在控件中找到相匹配的字符串，则字符串被选中，函数返回真值。如无匹配的字符串，函数返回假值。

    特别注意的是TEdit,TMemo中有一个HideSeletion属性，它决定当焦点从该控制转移至其它控制时，被选中的字符是否保持被选中的状态。如果是真值，则只有获得焦点才能保持被选中状态。查找时，焦点在查找对话框上，因此要想了解查找情况，必须将HideSeletion设成假值。控制的缺省值为真值。

    SearchMemo代码如下：

unit Search;

interface

uses WinProcs, SysUtils, StdCtrls, Dialogs;

const

WordDelimiters: set of Char = [#0..#255] - ['a'..'z','A'..'Z','1'..'9','0'];

function SearchMemo(Memo: TCustomEdit;

const SearchString: String;

Options: TFindOptions): Boolean;

function SearchBuf(Buf: PChar; BufLen: Integer;

SelStart, SelLength: Integer;

SearchString: String;

Options: TFindOptions): PChar;

implementation

function SearchMemo(Memo: TCustomEdit;

const SearchString: String;

Options: TFindOptions): Boolean;

var

Buffer, P: PChar;

Size: Word;

begin

Result := False;

if (Length(SearchString) = 0) then Exit;

Size := Memo.GetTextLen;

if (Size = 0) then Exit;

Buffer := StrAlloc(Size + 1);

try

Memo.GetTextBuf(Buffer, Size + 1);

P := SearchBuf(Buffer, Size, Memo.SelStart,

Memo.SelLength,SearchString, Options);

if P <> nil then

begin

Memo.SelStart := P - Buffer;

Memo.SelLength := Length(SearchString);

Result := True;

end;

finally

StrDispose(Buffer);

end;

end;

function SearchBuf(Buf: PChar; BufLen: Integer;

SelStart, SelLength: Integer;

SearchString: String;

Options: TFindOptions): PChar;

var

SearchCount, I: Integer;

C: Char;

Direction: Shortint;

CharMap: array [Char] of Char;

function FindNextWordStart(var BufPtr: PChar): Boolean;

begin { (True XOR N) is equivalent to

(not N) }

Result := False; { (False XOR N) is equivalent

to (N) }

{ When Direction is forward (1), skip non

delimiters, then skip delimiters. }

{ When Direction is backward (-1), skip delims, then

skip non delims }

while (SearchCount > 0) and

((Direction = 1) xor (BufPtr^ in

WordDelimiters)) do

begin

Inc(BufPtr, Direction);

Dec(SearchCount);

end;

while (SearchCount > 0) and

((Direction = -1) xor (BufPtr^ in

WordDelimiters)) do

begin

Inc(BufPtr, Direction);

Dec(SearchCount);

end;

Result := SearchCount > 0;

if Direction = -1 then

begin { back up one char, to leave ptr on first non

delim }

Dec(BufPtr, Direction);

Inc(SearchCount);

end;

end;

begin

Result := nil;

if BufLen <= 0 then Exit;

if frDown in Options then

begin

Direction := 1;

Inc(SelStart, SelLength); { start search past end of

selection }

SearchCount := BufLen - SelStart - Length(SearchString);

if SearchCount < 0 then Exit;

if Longint(SelStart) + SearchCount > BufLen then

Exit;

end

else

begin

Direction := -1;

Dec(SelStart, Length(SearchString));

SearchCount := SelStart;

end;

if (SelStart < 0) or (SelStart > BufLen) then Exit;

Result := @Buf[SelStart];

{ Using a Char map array is faster than calling

AnsiUpper on every character }

for C := Low(CharMap) to High(CharMap) do

CharMap[C] := C;

if not (frMatchCase in Options) then

begin

AnsiUpperBuff(PChar(@CharMap), sizeof(CharMap));

AnsiUpperBuff(@SearchString[1],

Length(SearchString));

end;

while SearchCount > 0 do

begin

if frWholeWord in Options then

if not FindNextWordStart(Result) then Break;

I := 0;

while (CharMap[Result[I]] = SearchString[I+1]) do

begin

Inc(I);

if I >= Length(SearchString) then

begin

if (not (frWholeWord in Options)) or

(SearchCount = 0) or

(Result[I] in WordDelimiters) then

Exit;

Break;

end;

end;

Inc(Result, Direction);

Dec(SearchCount);

end;

Result := nil;

end;

end.

  4.4.3 替换对话框部件

    替换对话框部件为应用程序提供替换对话框。如图4.9。它包括查找对话框的所有功能，此外还允许使用者更换被选中的字符串。FindText 属性是应用程序需查找的字符串。ReplaceText属性是被选中字符的替换字符串。Options 属性决定对话框的显示方式。其值如表4.3所示。

与查找对话框一样，替换对话框亦有OnFind 事件。用户输入查找字符串并按FindNext按钮时，发生OnFind 事件。用户选择Replace 或ReplacAll 时， 对话框发生OnRelpace事件，要替换的字符串存入ReplaceText属性中，要编写相应的代码以支持替换功能。

  表4.3 替换对话框的Options属性的取值及含义

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

取值　　　　　　　　　　　　　　含义

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

frRelpace 如果是真值， 应用程序将ReplaceText 属性中的字符串替换

                         FindText属性中的字符串。

frReplacAll 如果是真值，应用程序将ReplaceText属性中的字符串替换，

                         查找到的所有FindText属性中的字符串。

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

    例程中TEditForm.Replace方法响应OnReplace事件，Replace方法首先判断控制中被

选中字符串是否与替换字符串相等，如果不等则进行替换。而后根据Options中的方式循

环进行查找替换。直至无匹配字符串为止。其代码如下：

   procedure TEditForm.Replace(Sender: TObject);

var

Found: Boolean;

begin

with ReplaceDialog1 do

begin

if AnsiCompareText(Memo1.SelText, FindText) = 0 then

Memo1.SelText := ReplaceText;

Found := SearchMemo(Memo1, FindText, Options);

while Found and (frReplaceAll in Options) do

begin

Memo1.SelText := ReplaceText;

Found := SearchMemo(Memo1, FindText, Options);

end;

if (not Found) and (frReplace in Options) then

ShowMessage('Cannot find "' + FindText + '".');

end;

end;

4.4.4 打开对话框部件

    打开对话框部件为应用程序显示打开对话框。使用Execute方法可显示打开对话框用户通过选择文件类型下拉框中的文件类型，可以确定显示在文件列表中的文件。 例如，如果用户选择*.txt文件类型，那么只有在当前目录下的文本文件才会显示在文件列表中。文件扩展名通常也称为过滤器。

    打开对话框包含一个Filters(过滤器)的属性，它可确定文件类型和在文件类型下拉框中的顺序。应用程序可以为打开对话框定义多个过滤器，对话框的FilterIndex 属性可以决定哪个过滤器是文件类型下拉框中的缺省过滤器。如FilterIndex等于2，表示程序运行时出现在文件类型下拉框的过滤器是第2个过滤器。

    例程中关于文件打开的代码如下：

   procedure TEditForm.Open/Click(Sender : TObject);

begin

if OpenDialog/.Execult then

begin

  …

        Open(Open Dialog/.FileName)

end

end;

    打开，保存对话框中的Options属性值见表4.4

表4.4 打开、保存对话框的Options属性取值及含义

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

值　　　　　　　　　　　　　　　含义

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

ofAllowMultiSelect 如果是真值，则允许在文件名列表中选择多个文件。

ofCreatePrompt 如果是真值，当用户在文件编辑框中输入一不存在的文件名，

                        并选择OK按钮，则会出现消息框， 提示用户此文件不存在并

                        询问是否以此文件名创建一新文件。

ofExiengronDifferent 如果是真值，从对话框中返回的文件扩展名将不同于缺省扩展名。

其值存入DefaultExt属性中。

ofFileMustExist　　 如果是真值， 当用户在文件编辑框中输入一个不存在的文件名时，

并选择OK按钮， 则会出现一消息框提示用户此文件不存，并询

问是否输入了正确的路径和文件名。

ofNoChangeDir 如果是真值，当前目录将设置成对话框第一次出现的目录，并忽

略任何目录改变。

ofOverWritePrompt 如果是真值，当用户试图保存一个已存在的文件时， 将出现一消息

框，提示用户此文件已存在，并询问是否覆盖。

ofPathMastExit 如果是真值，用户在文件名编辑框只能输入有效路径名， 否则出

现消息框，提示用户路径无效。

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

表4.4 打开、保存对话框中的Options属性取值及含义

文件保存对话框与打开对话框类似，如图4.11。它的Option属性见上表。例程在保存文件前先对文件进行读写判断，如果文件是只读文件或未指定文件名的新文件， 则程序对文件不保存，否则备份文件。代码如下:

    procedure TEditForm.Save1Click(Sender: TObject);

procedure CreateBackup(const Filename: string);

var

BackupFilename: string;

begin

BackupFilename := ChangeFileExt(Filename, BackupExt);

DeleteFile(BackupFilename);

RenameFile(Filename, BackupFilename);

end;

function IsReadOnly(const Filename: string): Boolean;

begin

Result := Boolean(FileGetAttr(Filename) and faReadOnly);

if Result then MessageDlg(Format('%s is read only.',

[ExtractFilename(Filename)]), mtWarning, [mbOK], 0);

end;

begin

if (Filename = '') or IsReadOnly(Filename) then

SaveAs1Click(Sender)

else

begin

CreateBackup(Filename);

Memo1.Lines.SaveToFile(Filename);

Memo1.Modified := False;

end;

end;

其中CreateBackup过程用以改变需备份文件的扩展名。IsReadOnly 用以判断文件属性。

4.5 文件打印

    在Delphi中，文件打印有两种方式：

    1. 将文件变量分配给打印机，用此变量名创建或打开文件后， 往此文件变量写入的任何文本都视为向打印机输出，以下过程可实现文件的打印。

    procedure TEditForm,Print1Click(Sender: TObject);

var

Line: Integer;

PrintText: System.Text;

begin

if PrintDialog1.Execute then

begin

AssignPrn(PrintText)

Rewrite(PrintText);

Print.CanvasFont := Memo1.Font;

For Line := 0 to Memo1.Lines.Count - 1 do

Writeln(PrintText,Memo1.Line[line];

System.Close(PrintText);

end;

end;

2. 利用Printers单元中定义的TPrinter对象进行文件打印，本章例程采用这种方法打印文件。

4.5.1 TPrinter对象

    TPrinter对象可调用Windows的打印机，在Printer 单元中定义了TPrinter 的实例Printer，用户可直接使用。

    调用TPrinter的BeginDoc方法可开始一项打印工作，调用EndDoc 方法可结束一项已成功发送给打印机的工作。如果在发送过程中出现问题或用户想中途终止打印工作，可调用Abort方法。

    通过检查Printing属性可测试当前是否有打印工作，如果打印工作被终止，Abort属性为真。

    Canvas属性代表打印表面，Brush,Font,Pen属性可决定打印字体或图像的特征。

    Printers属性中包含着已安装的打印机列表，PrinterIndex 属性是当前选择的打印

机，Fonts属性中有当前打印机支持的字体。Orientertion属性可决定打印方向。

    PageHeight,PageWith中包含着当前的高度和宽度。PageNanber为当前页的值。

    设置Title属性可决定在Windows打印管理器或网络中出现的文本。

4.5.2 TPrintDialog打印对话框

    TPrintDialog部件显示一打印对话框。用户在对话框中，可以选择打印机、打印页数、打印份数。当用户选择对话框中的Setup按钮，则出现打印设置对话框。

    调用Execute方法显示打印对话框。如图4.12。使用Option属性可设置打印对话框显示的形式。Options的设置如表4.5所示。

    PrintRange属性可定义打印的范围。如果PrintPage的值是prPageNums，则可以设置FromPage和ToPage属性来确定打印范围。设置MinPage,MaxPage属性可限制用户的打印范围。

表4.5 打印对话框的Option属性的取值及含义

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

取值　　　　　　　　　　　　　　含义

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

PoHelp 如果是真值，对话框出现帮助按钮。

PoPageNums 如果是真值，页数按钮有效，用户可以设置打印范围。

PoPrintToFile 如果是真值，文件打印检查框将出现在对话框中，用户可以选

择文件打印。

PoSelection 如果是真值，选择按钮有效， 用户可打印文件中所选择的文本。

PoWarning 如果是真值，在打印机尚未安装时，用户选择OK 按按钮将出

现警告信息。

PoDisablePrinttoToFile 如果是真值，而PoPrintToFile亦是真值时，当对话框出现时，文

件打印对话框将无效。

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

本章例程是利用Printer的画布进行文本打印的。用户选择打印菜单后，将弹出打印对话框，用户可设置各种参数。当用户选择打印按钮后，打印工作进行发送，此时将弹出打印取消对话框，见图4.13， 用户可中止打印工作。有关打印和打印取消的代码如下：

procedure TEditForm.Print1Click(Sender: TObject);

var

DistanceLine,Line: Integer;

PrintText: System.Text;

begin

if PrintDialog1.Execute then

begin

Printer.Canvas.font := Memo1.Font;

DistanceLine := Trunc(1.5*FontDialog1.font.size);

OpenPrintCancelDialog;

Printer.BeginDoc;

for line := 0 to Memo1.Lines.Count - 1 do

begin

Printer.canvas.textout(0,DistanceLine*Line,Memo1.lines[Line]);

end;

Printer.EndDoc;

BtnBottomDlg.free;

end;

end;

procedure TEditForm.OpenPrintCancelDialog;

begin

BtnBottomDlg := TBtnBottomDlg.Create(Application);

BtnBottomDlg.show;

BtnBottomDlg.canvas.Brush.Color := clActiveBorder;

BtnBottomDlg.canvas.TextOut(50,20,'Print'+FileName);

BtnBottomDlg.canvas.TextOut(30,40,'if you want to

stop, please choice Cancel Button.');

end;


第五章 Delphi图形图像编程（一）

DELPHI基础教程

第五章　Delphi图形图像编程（一）

在Delphi中，专门定义了一组对象和部件用以绘制图形，完成一些简单的图像功能。利用这些对象、部件的方法，可以方便地绘制各种常用图形；通过设置它们的属性，能得到不同风格的图形。另外，通过对鼠标事件的定义，可以方便的设计图形绘制程序。

    本章将介绍以下内容：

    1. TCanvas,TPen,TBrush,TColor对象的方法及属性；

    2. 绘图功能的实现；

    3. TImage,TPicture,TBitBtn,TBitmap部件的方法及属性；

    4. 图像观测及处理。

    Graphex.dpr是一个简单的图形图像应用程序，是对以上这些对象和组件的具体应用。本章将结合此程序进行讲述。

5.1 图形对象概述

5.1.1 TCanvas Object(画布对象)

    TCanvas对象是一个用于绘图的表面，在这个区域上，程序可实现各种绘图功能，很多部件(如TIMage,TMemo)的Canvas属性就是TCanvas对象。在部件上绘制图形就是在部件的画布上绘制。TCanvas的Brush,Pen,Font属性分别是TBrush,TPen,TFont对象，它们用于定义绘制图形的风格。关于TBrush,TPen对象，下节中将详细介绍。

    画布的笔的位置定义在PenPos属性中，可用MoveTo方法来移动笔。如果要在画布上输出文本，可用Textout方法。

    TCanvas有对象很多方法，可完成常用的绘图功能，现将方法及功能简介如表5.1：

表5.1 TCanvas对象的方法

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

方法名称　　　　　　　　　形式及说明

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

Arc Arc(x1,y1,x2,y2,x3,y3,x4,y4 : Integer);

Arc方法在椭圆上画一段弧，椭圆由(x1,y1),(x2,y2) 两点所确定的椭圆所决

定。弧的起点是椭圆圆周和椭圆中心与(x3,y3)连线的交点。弧矩形终点是椭

圆圆周和椭圆中心与(x4,y4)连线的交点，以逆时针方向画弧。

Chord Chord(x1,y1,x2,yx,x3,y3,x4,y4 : Integer);

Chord方法连接椭圆上的两点，椭圆由(x1,y1),(x2,y3) 两点所确定的矩形决

定，(x3,y3)是始点，(x4,y4)是终点。

Brushcopy Brushcopy(const Dest : TRect;Bitmap : TBitmap;const Source TRect;

Color : TColor);

Brushcopy方法把位图的一部分复制到画布的某个矩形区域，并用画笔的当前颜色替换位图的颜色。参数Dest定义画布的一个矩形区域，该矩形用以填充位图，Bitmap定义位图；Source定义位图中的矩形区域，该区域上的位图

            将被复制；Color定义画笔中，用以替换位图的颜色。

CopyRect CopyRect(Dest : TRect;Canvas : TCanvas; Source TRect);

此方法从另一个画布对象上复制部分图像到该画布。Canvas表示源画布，Source是源画布上要复制的图像区域。Dest表示目标画布上将接受复制

图像的矩形区域。

Draw Draw(x,y : Integer;Graphic : TGraphic)；

            此方法在画布给定的象素点坐标(x,y)处画Graphic所给的图像，该图像可以是位图,图标或元位图。

Ellips Ellips(x1,y1,x2,y2 : Integer);

            Ellips方法在画布指定的矩形边界上画一个椭圆，(x1,y1)是矩形左上角的象素坐标，x2,y2是矩形右下角的象素坐标。如果矩形形成一个区域，将出现一个椭圆。

LineTo LineTo(x,y : Integer);

LineTo从当前位置画一条线至(x,y)所指定的位置，并把笔的位置移至(x ,y)。

MoveTo MoveTo(x,y : Integer);

MoveTo 将笔的当前位置设置到点(x,y)处，笔的当前位置在PenPos属性中，

改变笔的当前位置使用MoveTo方法，不要设法改变PenPos的值。

Die Die(x1,y1,x2,y2,x3,y3,x4,y4 : Longint);

Die方法绘制椭圆的一部分，椭圆由点(x1,y1),(x2,y2)所指定的矩形所决定，制的那部分由椭圆中心到(x3,y3),(x4,y4)两点的两条辐射线所决定。

Polygon Polygon(Points : array of TPrint);

Polygon方法在画布上绘制一系列的点，各点依次连成线，最后将首尾两点相接形成一个区域，并用当前笔刷填充此区域。

Polyline Polyline(Ports : array of TPort);

Polyline方法在画布上用当前画笔绘制一系列的点，各点依次连成线。

StretchDraw StretchDraw(Const Rect : TRcct : Graphic : TGraphic);

此方法在Rect参数指定的矩形内画一图像。图像延伸改变大小以适应矩形。

Rectangle Rectangle(X1,y1,x2,y2 : Integer);

Rectangle方法在画布上用当前画刷绘制矩形，(x1,y1)是矩形的左上角，(x2,y2)是矩形的右下角。

RomlRect RomlRect((x1,y1,x2,y2,x3,y3, : Integer);

DrawFocuseRect

DrawFocusRect(Const Rect : TRect)

此方法绘制一矩形以指示此矩形获得焦点。此方法是异或(XOR)函数，第二次调用时原有矩形将消失。DrawFocuseRect绘制的矩形不能滚动。要实现滚动功能则先调用此方法使矩形消失，待滚动过后重新绘制。

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

5.1.2 Tpen Object(画笔对象)

应用程序常用TPen对象在画布上绘制各种线段，笔的颜色在Color属性中定义。线段宽度在Width属性中定义。

    Style属性定义了线段的各种类型，如表5.2：

表5.2 Styled的取值及含义

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

    Style　　　　　含义

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

    PSolod 画固定线段

    PSDash 画由下划线组成的线段

    PSDot 画由点组成的线段

    PsDashDot 画点划线

    PsClear 画双点划线

    PsClear 画看不见的线段

    PsInsideFrame 画边界的矩形线框

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

    Mode属性定义线段的颜色。可结合当前的颜色、屏幕颜色或它们反转值，对线段的颜色重新定义，但不改变Color属性。详见表5.3。

表5.3 Mode的取值及含义

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

  Mode 象素颜色

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

   PmBlack 黑色

    PmWhite 白色

    PmNop 不变

    PmCopy 使用Color属性中的颜色

    PmNotCopy 笔颜色的反转值

    PmMergePenNot 笔的颜色与屏幕颜色反转值的结合

    PmNaskNotPen 屏幕颜色与笔颜色

    PmMergeNotPen 屏幕颜色与笔颜色反转值的结合

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

5.1.3 TBrush OBject(画刷对象)

    画刷对象用以填充图形，如用画刷颜色或图案对矩形或椭圆进行填充。TBrush拥有一个画刷句柄(HBrush)。

    画刷的颜色定义在Color属性中。画刷还有一个Bitmap属性，该属性只能在运行时得到，画刷可使用位图填充图形以产生特殊效果。位图大小为8个象素点，高8个象素点宽。

    Style属性定义了画刷填充图形的风格。

5.1.4 TColor类型

    TColor类型用于定义一个对象的颜色。很多部件的颜色属性就是TColor 类型， 在Graphics单元中TColor定义如下：

    TColor = -(COLOR_ENDCOLORS + 1)..$02FFFFF;

这是一个32位二进制数据。Graphic单元中还定义了一些常用的颜色常量，这些常量或直接映射成系统调色板中最相近的颜色，或映射成Wondows 控制面板中颜色部分的系统视频颜色。

    直接映射成系统调色板中的颜色有：

    ClAqua,CLBlack,ClBlue,ClbkGrray,ClFuchsoa......ClYellow

    映射程序用4字节的二进制码来定义颜色，低3 位字节代表RGB 相应的颜色，如$00FF0000表示纯蓝，$0000FF00表示纯绿，$000000FF表示纯红，$00000000表示黑色，$00FFFFFF表示白色。如果最高位字节是$00,则表示用系统调色板中最相近的颜色；最高位字节是$01，则表示用当前调色板中最相近的颜色匹配；最高位字节是$02，则用当前设备描述表中逻辑调色板的次相近颜色匹配。

    用Windows API的SelectPalette函数可创建逻辑调色板，要实现逻辑调色板到硬件调色板的映射，需用函数RealizePalett

5.2 图形程序的开发

    Graphex.dpr是一个简单的图形图像应用程序，运行状态如图5.2。该程序可用鼠标绘制多种图形，可设置画笔颜色、画刷填充方式；另外还可以浏览位图、元文件、图标，改变它们的大小，并打印。本节结合例程讲述以下问题：

    ● 在工具条中添加加速按钮

    ● 响应鼠标事件

    ● 设置画笔和画刷

    ● 实现绘图的“橡皮擦”功能

    ● 加入状态条

5.2.1 在工具条中添加加速按钮

    加速按钮是应用程序常用的部件，它是替代菜单的快捷形式，通常把多个加速按钮集中在一个工具条中以方便使用。Graphex中有三个工具条，它们是TPancel部件，面板上有几组加速按钮，用以设置绘图方式、画笔、画刷。

    每个加速按钮的glyph属性是图像对象，位图对象用来指示该按钮是否被使用。glyph 通常需要四幅图像，分别表示按下、不按、选择、失效四种状态。程序员可根据个人喜好来选择图像。

    加速按钮使用图像来告诉用户其状态和目的，因为按钮上无标题， 因此应给用户正确的提示：

    ● 把Down属性设置成真值使加速按钮呈按下状态

    ● 把Enable属性设置成假值使加速按钮呈失效状态。

    例程中缺省的绘图工具是画线，因此应用程序开始时画线按钮呈按下状态。

    在应用程序中，常把一些功能相似的按钮放在一起，用户在同一时刻只能选择其中的一个按钮。当其它按钮按下时，原来被按下的按钮自动弹起，这些选择排它的按钮称为一组加速按钮。

    要使多个加速按钮成为一组，将这些按钮的GraphIndex属性设成相同的值。最简单的办法是在设计状态时，用鼠标选中各个加速按钮，然后设置GraphIndex值。

    有时用户按一个已经按下的按钮，希望该按钮能够弹起，这样没有任何按钮被按下，使用AllowAllup 属性可实现上述功能。对于一组加速按钮来讲，设置该组中任一按钮的AllowAllup可使这组的每一个按钮具有这种功能。

Graphex程序中设计了三组加速按钮和两个单独的加速按钮。 第一组加速按钮用来选择绘图工具，它与两个单独的按钮处在同一面板中，这个面板是缺省可见的。另外两个按钮分别代表画笔、画刷。第二组与第三组加速按钮处在两个缺省不可见的面板中，它们分别代表不同风格的画笔和画刷，只有按下第一个面板中的画笔(或画刷)按钮，第二(或第三) 个面板才会显示，这样用户就可以选择画笔、画刷了。

5.2.2 响应鼠标事件

    鼠标常被用作绘图的工具，应用程序利用鼠标位置的变化来绘制各种不同的图形。鼠标有三个动作：鼠标按钮按下、鼠标移动、鼠标按钮弹起。在Delphi中， 对应三个动作有三个不同的事件：OnMouseDown,OnMouseMove,OnMouseUp。

当Dlephi应用程序探测到一个鼠标动作时，它传递五个参数，并调用相应的事件响应。

程序员可利用这些参数来定义事件程序。五个参数如下表5.4：

表5.4 鼠标事件的五个参数

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

    参数　　　　　　含义

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

    Sender 探测鼠标动作的对象

    Button 涉及的鼠标按钮：左键，中键，右键

    Shift 鼠标动作时，Alt,Ctrl,Shift按钮的状态

    X,Y 事件发生时鼠标的坐标

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

    当鼠标按下时发生OnMouseDown事件。举一个简单例子来说明程序如何对该事件进

行响应。假如我们想在鼠标按下的地方出现"Here"。

响应鼠标的OnMouseDown事件

    可在该事件中调用TextOut方法：

    procedure TForm1.FormMouseDown(Sender: TObject,Button: TMouseButton;

Shift : TShifState; X,Y : Integer);

begin

Canvas.TextOut(X, Y, 'Here!');

end;

用户放松鼠标键时发生OnMouseUp事件。该事件发生时，鼠标到达的对象并不一定是鼠标键按下时鼠标所在的对象。例如，用户可在窗体之外画一条线段，(鼠标在窗体外，线段在窗体内)。下面的代码可用鼠标绘制直线：

procedure TForm1.FormMouseDown(Sender:TObject)

begin

Moveto(x,y);

end;

procedure TForm1.FormMouse Up(Sender:Tobject)

begin

Lineto(X, Y);

end;


第五章 Delphi图形图像编程（二）

DELPHI基础教程

第五章　Delphi图形图像编程（二）

画直线时，用户只有在松开鼠标才能看见直线，对直线的变化不能进行实时观测。这是因为鼠标移动时程序没有进行某种应。Delphi定义了OnMouseMove事件来响应鼠标移动。以下代码可使用户随时观测直线的变化：

   procedure TForm1.FormMouseMove(Sender:Tobject)

begin

Drowto(X,Y);

Moveto(origin);

end.

origin是起始点。

5.2.3 绘图功能的实现

    绘图软件常根据用户的要求改变绘图工具。Graphex.dpr例程中，当用户按下某个按钮时，可选择绘图工具中的画笔或画刷，在程序类型说明部分定义了五种绘图工具。

      type

TDrawingTool = (dtLine,dtRectangle,dtEllips,dtRoundRect,dtPolygon);

当选中某种按钮，则选中了相应的绘图工具，如：

procedure TForm1.LineButtonClick(Sender: TObject);

begin

DrawingTool := dtLine;

end;

procedure TForm1.RectangleButtonClick(Sender: TObject);

begin

DrawingTool := dtRectangle;

end;

procedure TForm1.EllipseButtonClick(Sender: TObject);

begin

DrawingTool := dtEllipse;

end;

procedure TForm1.RoundRectButtonClick(Sender: TObject);

begin

DrawingTool := dtRoundRect;

end;

procedure TForm1.PolygonButtonClick(Sender: TObject);

begin

DrawingTool :=dtPolygon;

end;

DrawShape过程定义了每种绘图工具的动作：

procedure TForm1.DrawShape(TopLeft, BottomRight: TPoint; AMode: TPenMode);

begin

with Image.Canvas do

begin

Pen.Mode := AMode;

case DrawingTool of

dtLine: begin

MoveTo(TopLeft.X, TopLeft.Y);

LineTo(BottomRight.X, BottomRight.Y);

end;

dtRectangle: Rectangle(TopLeft.X, TopLeft.Y, BottomRight.X, BottomRight.Y);

dtEllipse: Ellipse(TopLeft.X, TopLeft.Y, BottomRight.X, BottomRight.Y);

dtRoundRect: RoundRect(TopLeft.X, TopLeft.Y, BottomRight.X, BottomRight.Y,

(TopLeft.X - BottomRight.X) div 2, (TopLeft.Y - BottomRight.Y) div 2);

dtPolygon:Polygon([Point(0,0),TopLeft,BottomRight]); end;

end;

end;

  程序刚运行时，只有一个工具栏。当用户单击画笔和画刷时，则出现相应的工具栏，如图5.4。其代码如下：

procedure TForm1.PenButtonClick(Sender: TObject);

begin

PenBar.Visible := PenButton.Down;

end;

procedure TForm1.BrushButtonClick(Sender: TObject);

begin

BrushBar.Visible := BrushButton.Down;

end;

在设计绘图程序时，还要解决一些问题。如为了在鼠标移动时能观测图形的变化，我们定义了OnMouseMove事件。但会出现这样的现象，当鼠标进入绘图区时，用户未按下鼠标键，画布上却出现绘制的图形，这是我们不希望看到的。其原因是没有对鼠标按钮是否按下进行判断。因此在窗体对象中定义了drawing的域，当鼠标按钮按下时，drawing 设置成真值。只有drawing为真，鼠标移动才执行绘图功能；当鼠标键松开时，drawing设置成假，鼠标移动将不执行绘图动作。

另外一个问题是， 我们希望得到的是鼠标按钮按下和松开这两点所形成的图形，但OnMouseMove却把鼠标轨迹上各点与起始点所形成的所有图形画在屏幕上，这同样是我们不希望看到的，为了解决这些问题，程序定义了鼠标的三个事件：

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y: Integer);

begin

Drawing := True;

Image.Canvas.MoveTo(X, Y);

Origin := Point(X, Y);

MovePt := Origin;

OriginPanel.Caption := Format('Origin: (%d, %d)', [X, Y]);

end;

procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y: Integer);

begin

if Drawing then

DrawShape(Origin, Point(X, Y), pmCopy);

Drawing := False;

end;

procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,

Y: Integer);

begin

if Drawing then

begin

DrawShape(Origin, MovePt, pmNotXor);

MovePt := Point(X, Y);

DrawShape(Origin, MovePt, pmNotXor);

end;

MovePt用来记录鼠标当前位置。当下次鼠标移动时， 就能在上次鼠标绘制的图形上画一个形状、大小一样的图形，并把画笔颜色设置成PmNotXor，使上次绘制的图形颜色变成了屏幕颜色，从而达到“橡皮擦”的效果。

    将画笔、画刷的Style属性设置成用户希望的值，可实现对画笔和画刷风格的选择。

procedure TForm1.SetBrushStyle(Sender: TObject);

begin

with Image.Canvas.Brush do

begin

if Sender = SolidBrush then Style := bsSolid

else if Sender = ClearBrush then Style := bsClear

else if Sender = HorizontalBrush then Style := bsHorizontal

else if Sender = VerticalBrush then Style := bsVertical

else if Sender = FDiagonalBrush then Style := bsFDiagonal

else if Sender = BDiagonalBrush then Style := bsBDiagonal

else if Sender = CrossBrush then Style := bsCross

else if Sender = DiagCrossBrush then Style := bsDiagCross;

end;

procedure TForm1.SetPenStyle(Sender: TObject);

begin

with Image.Canvas.Pen do

begin

if Sender = SolidPen then Style := psSolid

else if Sender = DashPen then Style := psDash

else if Sender = DotPen then Style := psDot

else if Sender = DashDotPen then Style := psDashDot

else if Sender = DashDotDotPen then Style := psDashDotDot

else if Sender = ClearPen then Style := psClear;

end;

end;

5.3 图像对象概述

5.3.1 TGraphic对象

    TGraphic对象是TBitmap ,TIcon,Tmetafile对象的基类。如果知道图像的具体类型( 如位图， 图标元文件) ， 则应将图像贮存在相应类型的对象中( 如TBitmap,TIcon,Tmetafile)，否则应该使用可贮存任何图像类型的TPicture对象。

5.3.2 TPicture对象

    TPicture对象可以保存位图、图标或元文件。Graphic属性中包括图像的类型；图像的高度和宽度分别定义在Height,Width属性中；调用LoadFromFile方法，可以从文件中装载一幅图像：

procedure TForm1.FormCreate(Sender: TObject);

begin

BitBtn1.Glyph.LoadFromFile('TARTAN.BMP');

end;

要保存一个位图，则要用SaveToFile方法；要把图像复制到剪切板，可以调用TClipboard对象的Assign方法。

5.3.3 TImage部件

    TImage部件用以在窗体中显示图像，它的Picture 属性保存着要显示的图像， 这是一个TPicture对象。AutoSize,Stretch属性是用来调节部件与图像的大小的。当AutoSize 为真值时，TImage部件将根据它所包含的图像的大小来调整自身的大小；当AutoSize为假值时，不论图像有多大，部件将保持设计时的大小。如果部件比图像小， 那么只有一部分图像是可见的。当Stretch为真值时，位图像将根据部件的大小调整自身的大小，当部件大小改变时，元文件也做相应变化。Stretch属性对图标没有作用。

5.3.4 TBitmap Object(位图对象)

    位图对象包含一个位图图像，有HBITMAP,HPALETE句柄，可自动管理调色板。位图对象也有画布属性。位图的Palette属性用来控制位图的颜色映射，它包括256种可显示的颜色。 如果应用程序用前景色绘制位图，Palette 属性的颜色将被加入Windows系统调色板，其它颜色被映射到系统调色板已存在的颜色。如果应用程序用自己的颜色绘制位图，而其它程序已占有系统调色板，位图的颜色将被映射到系统调色板中。

    如果Monochrome属性设置成假，位图将显示成彩色，反之显示成黑白色。

    调用Draw和StretchDraw方法可在画布上绘制位图。

5.4 图像对象的应用

    本章例程中，单击（文件|浏览）菜单项，将弹出一个图像浏览窗体。如果用户在窗体中选择文件列表框的图形文件，窗体右上角的图像部件上将出现此文件所代表的图像；若选择“雕刻效果”按钮中检查框，窗体中的加速按钮和位图按钮上将出现位图。

    以下代码是将图像文件装载至图像部件上：

procedure TImageForm.FileListBox1Click(Sender: TObject);

var

FileExt: string[4];

begin

FileExt := UpperCase(ExtractFileExt(FileListBox1.Filename));

if (FileExt = '.BMP') or (FileExt = '.ICO') or (FileExt = '.WMF') then

begin

Image1.Picture.LoadFromFile(FileListBox1.Filename);

Label1.Caption := ExtractFilename(FileListBox1.Filename);

if (FileExt = '.BMP') then

begin

Label1.Caption := Label1.Caption +

Format(' (%d x %d)', [Image1.Picture.Height, Image1.Picture.Width]);

ViewForm.Image1.Picture.Bitmap := Image1.Picture.Bitmap;

ViewAsGlyph(FileExt);

end;

if FileExt = '.ICO' then Icon := Image1.Picture.Icon;

if FileExt = '.WMF' then

ViewForm.Image1.Picture.Metafile := Image1.Picture.Metafile;

end;

end;

这个过程首先判断文件类型，如果是图像文件，则将图像装载至图像部件上，并在标签上列出文件名称。如果是位图文件，还将显示其大小。

    在加速按钮和位图按钮中显示位图的代码如下：

   procedure TImageForm.CheckBox1Click(Sender: TObject);

begin

ViewAsGlyph(UpperCase(ExtractFileExt(FileListBox1.Filename)));

end;

procedure TImageForm.ViewAsGlyph(const FileExt: string);

begin

if CheckBox1.Checked and (FileExt = '.BMP') then

begin

SpeedButton1.Glyph := Image1.Picture.Bitmap;

SpeedButton2.Glyph := Image1.Picture.Bitmap;

SpinEdit1.Value := SpeedButton1.NumGlyphs;

BitBtn1.Glyph := Image1.Picture.Bitmap;

BitBtn2.Glyph := Image1.Picture.Bitmap;

end;

end;

窗体中有一个检查框用来检验图像部件的Strecth 属性的效果。当此检查框被选中时，Stretch设成真值，图像将根据部件大小调整自身大小。代码如下：

procedure TImageForm.StretchCheckClick(Sender: TObject);

begin

Image1.Stretch := StretchCheck.Checked;

end;

在这个窗体中，用户可以在屏幕和打印纸上调整图像部件的大小、位置。调整图像的代码如下：

procedure TViewForm.SpinEdit1Change(Sender: TObject);

begin

IMage1.Height:=105+SpinEdit1.Value*5;

IMage1.Width:=105+SpinEdit1.Value*5;

end;

procedure TViewForm.SpinEdit2Change(Sender: TObject);

begin

Image1.Left:=40+ SpinEdit2.Value*20;

end;

procedure TViewForm.SpinEdit3Change(Sender: TObject);

begin

Image1.Top:=96+SpinEdit3.Value*10;

当用户按下标有“全尺寸”字样的按钮时，另一个窗体将显示。

图像打印代码如下：

procedure TViewForm.Button1Click(Sender: TObject);

begin

Printer.BeginDoc;

Printer.Canvas.Draw(Trunc(1.5*Image1.Left),Trunc(1.5*Image1.Top), Image1.Picture.Graphic);

Printer.EndDoc;

end;


第六章 文件管理（一)

DELPHI基础教程

第六章　文件管理（一)

文件是同一类型元素的有序集合，是内存与外设间传输数据的渠道。一些外设如显示器、键盘、打印机等都可以看作文件，但最常用的还是磁盘文件，这也是本章我们主要讨论的对象。

Delphi继承了Object Pascal的文件管理功能，并有很大的发展，其中最主要的是提供了用于文件管理的标准控件，同时也提供了更多的文件管理函数。利用Delphi的强大功能，开发一个自己的文件管理系统就成为很容易的事。

本章首先介绍Delphi文件管理的基本概念和标准过程/函数，并提供了一个记录文件的应用实例，这是从我们实际课题开发中提取出来的。而后介绍Delphi提供的文件控件的使用方法。最后提供的一个综合例程MDI文件管理器则是对Delphi文件管理功能的综合应用。

6.1 文件类型和标准过程

Delphi同Object Pascal一样支持三种文件类型，即：文本文件、记录文件、无类型文件。

6.1.1文本文件

文本文件类型的变量用如下方法声明：

var

TextFileVar: Text ;

文本文件是以行为单位进行读、写操作的。由于每一行长度不一定相同，不能计算出给定行在文件中的确切位置，因而只能顺序地读写。而且文本文件只能单独为读或写而打开，在一个打开的文本文件上同时进行读、写操作是不允许的。

6.1.1.1 文本文件的打开、关闭

文本文件的打开需要两个步骤：(1). 文件变量与文件名关联；(2). 初始化读写。

联文件变量与文件名调用AssignFile标准过程：

AssignFile ( TextFileVar , FileName ) ;

FileName 既可以是全路径名，也可以仅是文件名。对于后者系统将在当前目录下查找。

AssignFile是Delphi新提供的一个函数，其功能等价于Object Pascal中的Assign。而Assign在Delphi中更多地被用作一个方法名。

初始化读写有三种方式：

1. Reset : 为读打开文件并把文件指针移动到文件首；

2. Rewrite : 为写创建一个新文件；

3. Append : 为写打开存在的文件并把文件指针定位在文件尾。

当使用Reset或Append过程而文件不存在时将会引发一个I/O异常。有关I/O异常的处理请参看本章例程和第十二章中的介绍。

文件的关闭很简单，只须调用CloseFile过程即可。

虽然Delphi应用程序在退出时会自动关闭所有打开的文件，但自己动手关闭文件可以确保释放文件句柄，并使程序的可移植性增强。

为保持兼容，Delphi也允许用户用Assign建立关联，Close关闭文件。

6.1.1.2 文本文件的读写

从文本文件中读取信息用Read、Readln两个标准过程。

当读入数值时，Read、Readln假定数值是用一个或多个空格分开的，而不是逗号、分号或其它字符。对如下一条语句：

Read ( TextFileVar , Num1 , Num2 , Num3 ) ;

如果文件中的数值是：

100 200 300

则能够成功读入，而若文件中的数值是

100 200， 300

则Read读入“200，”并试图把它转化成一个数值时会引发一个异常。

当读入字符是字符串时，Read、Readln过程总是读取尽可能多的字符填充到字符串变量中或一直读到行结束符为止。因此从文本文件中读取格式化的字符串数据，必须声明与其长度相匹配的字符串变量。如果要从文件中读取单词，必须先把文件中的每一行读入字符串，然后再从字符串中逐个分析出单词。或者一次只从文本文件中读入一个字符并测试每个字符后是否是单词断开处。

格式化字符串之间的分隔符应读入到一个临时变量中，而字符串与数值、数值与数值间的分隔符读入时会自动识别剔除。对如下一行数据：

Mon 12:10 40 50

定义

var

Day: string[3] ;

Time: string[5] ;

Num1, Num2: Integer ;

则须用如下的read 语句读入：

read ( TextFileVar , Day , c , Time , Num1 , Num2 ) ;

C为一个临时字符变量。

6.1.1.3 文本文件的编辑

在Delphi中实现对一个文本文件的编辑，只须让其与一个Tmemo控件建立关联即可：

Memo1.Lines.LoadFromFile ( TextFileName ) ;

这样在TMemo上所做的一切修改当调用Memo部件的SaveToFile方法后都会反映到文件中去。

6.1.2 记录文件

记录文件是一种操作更为灵活的文件类型。它允许同时为读和写打开，而且由于记录文件中每条记录的长度固定，所以可随机存取。

记录文件的类型变量可如下声明：

var

RecordFileVar: file of RecordType;

RecordType是一个自定义的记录类型。

有关记录文件的操作我们将在下一节中结合例程进行讨论。

6.1.3 无类型文件

无类型文件提供了底层的I/O通道，可用于存取可变长度记录的文件。经常用于文件的复制操作中。由于Delphi提供了更好的方法(见第四节)，所以无类型文件很少使用。有兴趣的读者可参看BlockRead、BlockWrite两个联机帮助主题。

6.1.4 Delphi的文件管理标准过程

根据功能我们把标准过程划分为十一类进行介绍。

6.1.4.1 文件的打开与关闭

AssignFile ： 把一个外部文件名和一个文件变量相关联

Reset ：打开一个存在的文件

Rewrite ：创建并打开一个新文件（或覆盖原有文件）

Append ： 以添加方式打开一个文件（只适用于文本文件）

CloseFile ： 关闭一个打开的文件

FileOpen ：打开一个特定的文件并返回文件句柄

FileCreate ：创建一个给定文件名的文件并返回文件句柄

FileClose ： 关闭一个特定句柄的文件

后边三个文件主要供系统内部使用，在文件复制的编程中也往往会用到。它们操作的对象是文件句柄而不是文件变量。

6.1.4.2 文件定位

Seek ： 把文件当前位置移到指定部分

FilePos ： 返回文件的当前位置

Eoln ： 返回行结束标志

EOF ： 返回文件结束标志

FileSeek ： 改变当前文件指针的位置

Seek与FileSeek的区别是：1. Seek仅用于记录文件；2. FileSeek的参数是文件句柄、偏移量、起始位置。其中起始位置有文件首、当前位置、文件尾三种选择。Seek的参数是文件变量、偏移量，偏移量是从文件首开始定位的。3. FileSeek的偏移量以字节数来计算，而Seek是根据记录号进行移动。

Seek、FilePos仅用于记录文件。但任何文件都可以看作是基于字节的记录文件。下面一段程序表示了它们的用法。

{ 该例子的设计界面为一个包含TOpenDialog部件的窗体。}

uses Dialogs;

var

f: file of Byte;

size: Longint;

S: String;

y: Integer;

begin

if OpenDialog1.Execute then

begin

AssignFile(f, OpenDialog1.FileName);

Reset(f);

size := FileSize(f);

S := 'File size in bytes: ' + IntToStr(size);

y := 10;

Canvas.TextOut(5, y, S);

y := y + Canvas.TextHeight(S) + 5;

S := 'Seeking halfway into file...';

Canvas.TextOut(5, y, S);

y := y + Canvas.TextHeight(S) + 5;

Seek(f,size div 2);

S := 'Position is now ' + IntToStr(FilePos(f));

Canvas.TextOut(5, y, S);

CloseFile(f);

end;

end.

6.1.4.3 文件删除与截断

Erase ： 删除一个存在的文件

DeleteFile ： 删除一个文件

Truncate ： 从文件当前位置将文件截断

Erase与DeleteFile的区别是：Erase以文件变量为参数，当文件不能删除时引起一个异常；DeleteFile以文件名为参数，当文件不存在或不能删除时返回False，而并不引起一个异常。

6.1.4.4 文件名操作

Rename ：文件更名，以文件变量为操作对象

RenameFile ：文件更名，参数为文件的原名和新名

ChangeFileExt ：改变文件扩展名

ExpandFileName ：返回文件全路径名

ExtractFileExt ：返回文件扩展名

ExtractFileName ：从全路径名中返回文件名

ExtractFilePath ：返回特定文件的路径

6.1.4.5 文件属性

FileGetAttr ：返回文件属性

FileSetAttr ：设置文件属性

6.1.4.6 文件状态

FileSize ：返回文件对象大小

IOResult ：返回上一次I/O操作的状态

FileExists ：检测文件是否存在

6.1.4.7 文件日期

DateTimeToFileDate ：把Delphi日期格式转换为DOS日期格式

FileDateToDateTime ：把DOS日期格式转换为Delphi日期格式

FileGetDate ：返回文件的DOS日期时间戳

FileSetDate ：设置文件的DOS日期时间戳

6.1.4.8 文件读写

Read，Readln ：从文本或记录文件中读取变量

Write ：将指定变量写入文本或记录文件

Writeln ：将指定变量写入文本文件并写入一个行结束标志

FileRead ：从一个指定文件中读取变量

FileWrite ：向指定文件写入数据

FileRead和FileWrite都是以文件句柄为操作对象，主要供系统内部使用。

6.1.4.9 目录操作

MkDir ：创建当前目录的子目录

ChDir ：改变当前目录

GetDir ：返回特定磁盘的当前目录

RmDir ：删除一个空子目录

6.1.4.10 磁盘操作

DiskFree ：返回磁盘自由空间

DiskSize ：返回特定磁盘的大小

6.1.4.11 文件查找

FileSearch ：查找目录中是否存在某一特定文件

FindFirst ：在目录中查找与给定文件名（可以包含匹配符）及属性集相匹配 的第一个文件

FindNext ：返回符合条件的下一个文件

FindClose ：中止一个FindFirst / FindNext序列

有关文件管理标准过程/函数的更详细资料，请查阅Delphi相关的Help主题。以上的大部分过程在后面都有应用实例，读者可以从中体会其用法。

在Delphi的联机帮助Help系统中把有关文件的过程/函数分为两个主题：I/O Routine和File_Management Routine。前者大部分以文件变量为操作对象，而后者大部分以文件名或文件句柄为操作对象。这里为了方便读者的使用，我们按功能重新进行了分类。在下一节中主要应用I/O Routine主题下的过程，而在第四节的综合举例中主要应用File_Management Routine主题下的过程。

另外，Windows提供了许多有关文件管理的API函数。虽然在一般情况下，利用Delphi提供的函数已足够解决问题，但有时候仍然需要使用Windows API。在(6.4.4.2)中我们就用到了Windows API函数GetDriveType。有关Windows API函数的情况，请读者参阅相关的资料，这里不再进行介绍。

6.2 记录文件的应用

6.2.1 任务介绍

    在这一节，我们开发一个系统安全性综合评估方法管理系统。系统安全性在复杂项目开发中十分重要，但由于牵涉面广因而很难获得客观、全面的评估值。鉴于此我们提出多角度、多侧面评估而后定量集成的思路，并在此基础上提出了多种安全性综合评估方法。每种方法由不同部门进行评估而后把结果汇总、综合。

    为此我们定义如下的记录类型：

type

TNature = (Micro,Macro);

{方法性质，分为微观和宏观两类}

      TMethod = Record

Name: string[20]; {方法名}

Condition: string[40]; {方法适用条件}

Nature: TNature; {方法性质}

Result: Real; {方法评估值}

end;

用来记录不同方法的信息。

    由于不同方法的条件、性质不同，因而对工程开发的不同阶段适用方法集也不同。因此需要根据实际情况对方法集进行管理。我们把每一方法作为一条记录，每一方法集作为一个记录文件。下面讨论系统的实现方法。

6.2.2 设计基本思路

    本系统要实现的基本功能是文件的打开、创建、关闭、显示，记录的增加、修改、删除以及结果的综合和显示。为此我们使用了两组按钮分别用于文件和记录的操作， 使用一个StringGrid控件来显示文件内容，使用一个只读编辑框显示结果的综合。

其中各部件的名称、功能如下表所示：

表6.1 主窗口部件的设计

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

部件名称 主要属性 备注

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

RecFileForm BorderStyle=bsDialog 文件打开后把文件名附到窗口标题后

Position=poScreenCenter

StringGrid1 大小行数动态确定

HazAttr(编辑框) ReadOnly=True 显示综合结果

OpenButton TabOrder=0 打开一个记录文件,若文件不存在则创建

NewButton Caption='打开' 创建一个记录文件,若文件存在则打开

CloseButton Caption='关闭' 关闭一个已打开的文件

AddButton Caption='增加' 增加一条记录

ModifyButton Caption='修改' 修改一条记录

DeleteButton Caption='删除' 删除一条记录

CalcuButton Caption='计算' 计算最终结果并显示

ExitButton Caption='退出' 系统终止。若当前有打开的文件则先关闭

OpenDialog1 Filter= 选择或输入欲打开的文件

'Record File(*.Rec)|.Rec

|Any File(*.*)|*.*'

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

    另外，StringGrid1、HazAttr的标题用两个标签框(Label)来显示。

    另外我们还需要一个编辑对话框。其中四个编辑框Name、Condition、Nature、 Result分别对应TMethod记录的四个域。

为协调程序运行，我们定义了一组全局变量。各变量的类型、作用如下表。

     表6.2　全局变量及其作用

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

     变量名 类型 作用

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

MethodFile MethodFileType 与当前打开文件相关联的文件变量

FileName string[70] 当前打开文件的文件名

Count Count 当前打开文件的记录总数

CurrentRec Integer 当前处理记录号

FileOpened Boolean 当前是否有文件打开

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

记录文件类型MethodFileType的定义为

    type

MethodFileType = file of TMethod;

布尔变量FileOpened用于控制文件按钮的使能、变灰，记录按钮的反应以及系统结束时是否需要首先关闭文件。

6.2.3 记录文件的打开和创建

    记录文件的打开和创建同文本文件一样也需要关联和初始化两个步骤。同文本文件唯一的不同是不能使用Append过程。

    记录文件缺省情况下以读写方式打开，如果想以只读或只写方式打开，则需要修改System单元中定义的变量FileMode的值。

    FileMode的取值和意义如下表。

     表6.3 FileMode的取值和意义

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

取值 意义

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

0 只读

1 只写

2 读写

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

    FileMode是一个全局变量，对它的每次修改都将影响所有Reset的操作，因此在打开自己的文件后应还原它的值。

    在本系统中，当用户按下“打开”按钮时，首先弹出一个标准文件打开对话框，要求用户输入或选择文件名。确认后如果该文件名的文件存在，则用Reset打开，若不存在则创建。程序清单如下。

procedure TRecFileForm.OpenButtonClick(Sender: TObject);

begin

if OpenDialog1.Execute then

FileName := OpenDialog1.FileName

else

exit;

AssignFile(MethodFile,Filename);

try

Reset(MethodFile);

FileOpened := True;

except

On EInOutError do

begin

try

if FileExists(FileName) = False then

begin

ReWrite(MethodFile);

FileOpened := True;

end

else

begin

FileOpened := False;

MessageDlg('文件不能打开',mtWarning,[mbOK],0);

end;

except

On EInOutError do

begin

FileOpened := False;

MessageDlg('文件不能创建',mtWarning,[mbOK],0);

end;

end;

end;

end;

if FileOpened = False then exit;

Count := FileSize(MethodFile);

if Count>0 then

ChangeGrid;

RecFileForm.Caption := FormCaption+' -- '+FileName;

NewButton.Enabled := False;

OpenButton.Enabled := False;

CloseButton.Enabled := True;

end;

    首先系统试图用Reset打开一个文件，并置FileOpened为True。如果文件不能打开，则引发一个I/O异常。在异常处理过程中，首先检测文件是否存在。若不存在则创建这个文件。否则是其它原因引发的异常，则把FileOpend重置为False， 并显示信息“文件不能打开”。在文件创建过程中仍可能引发异常，因而在一个嵌套的异常处理中把FileOpened重置为False，并提示信息“文件不能创建”。

    有关异常处理的内容请读者参看第十二章。这段程序说明：异常处理机制不仅能使我们的程序更健壮，而且为编程提供了灵活性。

    当用户按下“创建”按钮时，系统首先弹出一个标准输入框，要求用户输入文件名，确认后系统首先检测文件是否存在。若存在则直接打开，否则创建一个新文件。打开或创建过程导致异常，则重置FileName和FileOpened两个全局变量。

procedure TRecFileForm.NewButtonClick(Sender: TObject);

begin

FileName := InputBox('输入框','请输入文件名','');

if FileName = '' then Exit;

try

AssignFile(MethodFile,FileName);

if FileExists(FileName) then

begin

Reset(MethodFile);

Count := FileSize(MethodFile);

if Count>0 then

ChangeGrid;

end

else

begin

Rewrite(MethodFile);

count := 0;

end;

FileOpened := true;

Except

on EInOutError do

begin

FileName := '';

FileOpened := False;

end;

end;

if FileOpened then

begin

NewButton.Enabled := False;

OpenButton.Enabled := False;

CloseButton.Enabled := True;

RecFileForm.Caption := FormCaption+' -- '+FileName;

end;

end;

    当文件打开或创建后，所要做的工作有：

    ● 若文件非空，则计算文件长度，并用文件内容填充StringGrid1

    ● “创建”、“打开”按钮变灰，“关闭”按钮使能

    ● 把文件名附到窗口标题后

6.2.4 记录文件的读入和显示

    定义一个全局变量Count用来保存文件中的记录个数。当文件装入时：

    Count := FileSize(MethodFile)；

    如果Count > 0，则首先确定StringGrid1的高度、行数。为保证StringGrid1不会覆盖窗口下面的编辑框，定义一个常量MaxShow。当Count < MaxShow时，记录可全部显示；当Count >= MaxShow时，StringGrid1自动添加一个滚动棒。为保证滚动棒不覆盖掉显示内容，StringGrid1的宽度应留有余地。

    确定StringGrid1高度、行数的代码如下：

    With StringGrid do

if count < MaxShow then

Height := DefaultRowHeight * (Count+1)+10

else

Height := DefaultRowHeight * MaxShow+10;

RowCount := Count+1;

end;

而后从文件中逐个读入记录并显示在StringGrid1的相应位置：

    for i := 1 to Count do

begin

Read(MethodFile,MethodRec);

ShowMethod(MethodRec,i);

end;

ShowMehtod是一个过程，用来把一条记录填入StringGrid1的一行中。对于Name、Condition域而言，只须直接赋值即可；而对Nature 域需要把枚举类型值转化为对应意义的字符串(0：“微观”，1：“宏观”)；而对Result域则需要把数值转化为一定格式的字符串：

Str (MethodRec.Result:6:4,ResultStr)；

   StringGrid1.Cells[3,Pos] := ResultStr;

即Result显示域宽为6，其中小数点后位数为4。

6.2.5 增加一条记录

    当用户单击“增加”按钮时屏幕将会弹出一个记录编辑模式对话框EditForm。在编辑框中填入合适的内容并按OK键关闭后，相应值写入一个TMethod类型的变量MethodRec中。其中Nature和Result 域需要进行转换。之后增加的记录添加到StringGrid1的显示中。

    最后文件定位于尾部，写入当前记录，总记录数加1。

   Seek(MethodFile,Count);

Write(MethodFile,MethodRec);

Count := Count+1;

完整的程序清单如下：

procedure TRecFileForm.AddButtonClick(Sender: TObject);

var

MethodRec: TMethod;

Rl: Real;

k: Integer;

EditForm: TEditForm;

begin

if FileOpenEd = False then Exit;

EditForm := TEditForm.Create(self);

if EditForm.ShowModal <> idCancel then

begin

HazAttr.text := '';

MethodRec.Name := EditForm.MethodName.text;

MethodRec.Condition := EditForm.Condition.text;

case EditForm.NatureCombo.ItemIndex of

0:

MethodRec.Nature := Micro;

1:

MethodRec.Nature := Macro ;

end;

Val(EditForm.Result.text,Rl,k);

MethodRec.Result := Rl;

with StringGrid1 do

begin

if Count < MaxShow then

Height := Height+DefaultRowHeight;

RowCount := RowCount+1;

end;

ShowMethod(MethodRec,Count+1);

seek(MethodFile,Count);

write(MethodFile,MethodRec);

Count := Count+1;

end;

end;

6.2.6 修改记录

    首先获取当前记录位置：

    CurrentRec := StringGrid1.Row - 1;

而后打开编辑对话框并显示当前值。修改完毕后，修改结果保存在一个记录中并在StringGrid1中重新显示。

    最后修改结果写入文件：

Seek(MethodFile,CurrentRec);

Write(MethodFile,MethodRec);

完整程序如下：

procedure TRecFileForm.ModifyButtonClick(Sender: TObject);

var

MethodRec: TMethod;

Rl: Real;

k: Integer;

EditForm: TEditForm;

begin

if FileOpened = False then Exit;

EditForm := TEditForm.Create(self);

CurrentRec := StringGrid1.Row-1;

with EditForm do

begin

MethodName.text := StringGrid1.Cells[0,CurrentRec+1];

Condition.text := StringGrid1.Cells[1,CurrentRec+1];

if StringGrid1.Cells[2,CurrentRec+1] = '微 观' then

NatureCombo.ItemIndex := 0

else

NatureCombo.ItemIndex := 1;

Result.text := StringGrid1.Cells[3,CurrentRec+1];

if ShowModal <> idCancel then

begin

HazAttr.text := '';

MethodRec.Name := MethodName.text;

MethodRec.Condition := Condition.text;

case NatureCombo.ItemIndex of

0:

MethodRec.Nature := Micro;

1:

MethodRec.Nature := Macro ;

end;

Val(Result.text,Rl,k);

MethodRec.Result := Rl;

ShowMethod(MethodRec,CurrentRec+1);

seek(MethodFile,CurrentRec);

write(MethodFile,MethodRec);

end;

end;

end;


第六章 文件管理（二）

DELPHI基础教程

第六章　文件管理（二）

6.2.7 记录的删除、插入、排序

    删除一条记录的基本思路是：获取当前记录的位置并把该位置后的记录逐个向前移动。 文件在最后一条记录前截断。

    for i:=CurrentRec+1 to Count-1 do

begin

seek(MethodFile,i);

read(MethodFile,MethodRec);

seek(MethodFile,i-1);

Write(MethodFile,MethodRec);

end;

Truncate(MethodFile);

为避免误删除，在进行删除操作前弹出一个消息框进行确认。删除后要更新全局变量的值和显示内容：

Count := Count - 1;

ChangeGrid;

完整的程序如下：

procedure TRecFileForm.DeleteButtonClick(Sender: TObject);

var

NewFile: MethodFileType;

MethodRec: TMethod;

NewFileName: String;

i: Integer;

begin

if FileOpened = False then Exit;

CurrentRec := StringGrid1.Row-1;

if CurrentRec < 0 then Exit;

if MessageDlg('Delete Current Record ?', mtConfirmation,

[mbYes, mbNo], 0) = idYes then

begin

HazAttr.text := '';

for I := CurrentRec+1 to Count-1 do

begin

seek(MethodFile,i);

read(MethodFile,MethodRec);

seek(MethodFile,i-1);

Write(MethodFile,MethodRec);

end;

Truncate(MethodFile);

Count := Count-1;

ChangeGrid;

end;

end;

    这里所显示的删除操作简单明了。但在程序开始设计时我却走了一条弯路，后来发现虽然这种方法用于记录的删除操作显得笨拙、可笑，但却恰恰是记录插入、排序的思想。

    这种思想的核心是创建一个新文件保存更新后的内容。若新文件顺利创建，则删除原文件，否则恢复原来的文件。程序清单如下：

procedure TRecFileForm.DeleteButtonClick(Sender: TObject);

var

NewFile: MethodFileType;

MethodRec: TMethod;

NewFileName: String;

i: Integer;

begin

if FileOpened = False then Exit;

CurrentRec := StringGrid1.Row-1;

if CurrentRec < 0 then Exit;

if MessageDlg('Delete Current Record ?', mtConfirmation,

[mbYes, mbNo], 0) = idYes then

begin

HazAttr.text := '';

NewFileName := ChangeFileExt(FileName,'.sav');

try

AssignFile(NewFile,FileName);

ReWrite(NewFile);

Except

On EInOutError do

begin

Rename(MethodFile,FileName);

Exit;

end;

end;

for i := 1 to Count do

if I <> CurrentRec+1 then

begin

MethodRec := GridToRec(i);

Write(NewFile,MethodRec);

end;

closeFile(MethodFile);

try

AssignFile(MethodFile,Filename);

Reset(MethodFile);

except

on EInOutError do

begin

DeleteFile(FileName);

AssignFile(MethodFile,NewFileName);

Reset(MethodFile);

Rename(MethodFile,FileName);

Exit;

end;

DeleteFile(NewFileName);

Count:=Count-1;

ChangeGrid;

end;

end;

    对于记录插入，方法基本同上。对于排序，可先将关键域读入排序，而后再按排序结果对应的记录号顺序重写文件。

6.2.8 结果综合

    对不同方法的评估结果，可按一定的公式进行综合。当用户按下“计算”按钮时，系统进行计算并把综合结果写入HazAttr只读编辑框中。

    为保证结果显示的正确性，每次增加、修改、删除操作确认后HazAttr编辑框清空。

6.2.9 编辑对话框的输入检查

    当用户单击“增加”或“修改”按钮时系统将弹出一个编辑对话框，让用户输入或修改记录内容。其中的三个编辑框，一个组合列表框分别对应TMethod 的四个域。由于TMethod的Result域必须是[0,1]间的小数，因此当用户按OK键关闭对话框时应进行类型和范围检查。

    在VB中我做过同样的工作，那时需要对用户输入的键码逐个进行判断。但这种方法很繁琐、很难做圆满(如不能很好地支持编辑键)。而Object Pascal提供了更好的方法。这种方法的关键就在于它的类型转换函数Val：

procedure Val(Str: String;var V; var Code: Integer)；

    V是由Str转换成的整型或实型数。若字符串非法，则出错位置返至Code;否则置Code为0。字符串非法并不会引发一个转换异常。

    如果转换后的数超出了我们的范围，则显式把Code置为-1。最后统一通过检测Code是否为0来判断输入是否合法。

    我们把输入检查放在对话框的OnCloseQuery事件处理过程中。如输入非法，则禁止对话框关闭，并将输入焦点置于Result编辑框中。但假如用户按了Cancel按钮，则这种检查是多余的。为此定义一个布尔变量IsCancel，对话框生成时置为False。假如用户按下Cancel，则置为True，此时OnCloseQuery事件不进行输入检查。

    对话框的OnCloseQuery事件处理过程的程序清单如下：

procedure TEditForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);

var

Res: Real;

k: Integer;

begin

if IsCancel = False then

begin

val(Result.text,Res,k);

if (Res > 1) or (Res < 0) then k := -1;

if k <> 0 then

begin

MessageDlg('非法输入 ！',mtWarning,[mbOK],0);

Result.text := '';

CanClose := False;

Result.SetFocus;

end;

end;

end;

6.2.10 文件和系统的关闭

    文件关闭须调用CloseFile过程：

     CloseFile(MethodFile);

并对系统的状态重新进行设置。

系统关闭时首先检测当前是否有打开的文件。若有则先关闭文件。这在主窗口的OnCloseQuery事件中实现。

实现文件关闭的程序清单如下：

procedure TRecFileForm.CloseButtonClick(Sender: TObject);

begin

if FileOpened then

begin

CloseFile(MethodFile);

FileOpened := False;

ClearGrid;

OpenButton.Enabled := True;

NewButton.Enabled := True;

CloseButton.Enabled := False;

RecFileForm.Caption := FormCaption;

end;

end;

实现系统关闭前检查的程序清单如下：

procedure TRecFileForm.FormCloseQuery(Sender: TObject;

var CanClose: Boolean);

begin

if FileOpened then

closeFile(MethodFile);

end;

6.2.11 记录文件小结

    我们所举的例子虽然简单，但基本覆盖了记录文件操作的主要方面。这里关键问题在于灵活应用Delphi提供的文件管理函数。同时，为了保证程序的健壮性应对异常进行捕获并处理。在数据库应用技术发展的今天，记录文件的重要性也许有所下降，但对象我们这里所处理的简单问题它仍有用武之地。

    这里所举的例子一次只能处理一个文件。但读者可以很容易把它改为一个MDI程序。虽然对于这里的实际情况来说，似乎并无必要。

6.3 文件控件的应用

    Delphi文件管理的最大特色是提供了一组文件操作控件。利用这些控件我们可以快速开发一个文件名浏览系统。其功能强大与其所需书写代码之少所形成的强烈反差，正是Dephi生命力的体现。

6.3.1 文件控件及其相互关系

    Delphi提供的专用文件控件如下表所示。

     表6.4 Delphi专用文件控件━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

控件名 功 能

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

DriveComboBox 驱动器组合列表框。用于选择当前驱动器

FileListBox 文件列表框。用于显示当前目录中的文件和选中当前文件

FilterComboBox 文件类型组合列表框。用于选择显示文件的类型

DirectoryOutline 目录树(6.4节专门介绍)

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

    以上控件前四个在Component Palette(部件选择板)的System页中，DirectoryOutline在Component Palette的Samples页中。

    以上文件控件再加上文件编辑框、目录标签框(事实上是一般的编辑框、标签框)就可以构成一个完整的文件操作系统。它们之间的联系几乎不用代码支持，只要设置好相应的属性就可以了。

   FileEdit、DirLabel、FileListBox、FileFilterComloList、 DirectoryListBox、DriveComboList六个控件间的属性联系如下：

    DriveComboList .DirList := DirectoryListBox;

    DirectoryListBox.DirLabel := DirLabel;

DirectoryListBox.FileList := FileListBox;

FileFilterComboList.FileList := FileListBox;

FileListBox.FileEdit := FileEdit;

以上联系可以在设计时完成。只要打开相应属性的选择列表框进行选择即可。也可以在运行时利用如上的赋值语句建立联系。

    文件控件的关键属性基本上都在以上联系中反映出来了。除此之外，FileFilterComboList有一个Filter属性，用来设置组合列表框的选择项；FileListBox 有一个Mask属性，用于设置显示文件的类型，这就允许FileListBox在脱离FileFilterComboList单独应用时仍能根据需要显示特定的文件。在6.4节中我们将应用这一功能。

    文件控件的方法、事件基本是从ListBox和ComboBox中继承的。但FileListBox 中有一个ApplyFilePath方法很有用，我们将在后边给出其用法。

6.3.2 文件名浏览查找系统的设计思路

    作为文件控件的应用实例，我们开发了一个简单的文件名浏览查找系统。这个系统可用于文件名的显示，把选中的文件写入列表框，并能按文件编辑框中输入的通配符对文件进行查找。

表6.5 部件的设计

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

部件 属性 功能

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

FileCtrForm Position=poDefault 主窗口

DirLabel 显示当前目录

FileEdit TabOrder=0 显示当前文件/输入文件显示匹配符

FileListBox1 FileEdit=FileEdit 显示当前目录文件

DirectoryListBox1 DirLabel=DirLabel 显示当前驱动器目录

FileList= FileListBox1

DriveComboBox1 DirList= DirectoryListBox1 选择当前驱动器

FilterComboBox1 FileList=FileListBox1 选择文件显示类型

Filter='All Files(*.*)|*.*|

Source Files(*.pas)|*.pas|

Form Files(*.dfm)|*.dfm|

Project Files(*.dpr)|*.dpr'

ListBox1 显示选中或查找的文件

Button1 Caption='查找' 按 FileEdit 中的内容进行查找

Button2 Caption='退出' 退出系统

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

6.3.3 文件名浏览查找系统的功能和实现

6.3.3.1 按指定后缀名显示当前目录中的文件

    实现这一功能只需要在控件间建立正确的联系即可，不需要代码支持。建立联系的方法如(6.3.1)中的介绍。

6.3.3.2 把选中的文件添加到列表框中

    在FileListBox1的OnClick事件中：

procedure TFileCtrForm.FileListBox1Click(Sender: TObject);

begin

if Searched then

begin

Searched := False;

ListBox1.Items.Clear;

Label5.Caption := 'Selected Files';

end;

if NotInList(ExtractFileName(FileListBox1.FileName),ListBox1.Items) then

ListBox1.Items.Add(ExtractFileName(FileListBox1.FileName));

end;

    Searched是一个全局变量，用于标明ListBox1当前显示内容是查找的结果还是从FileListBox1中选定的文件。

函数NotInList用于判断待添加的字符串是否已存在于一个TStrings对象中。函数返回一个布尔型变量。

    NotInList的具体实现如下。

Function TFileCtrForm.NotInList(FileName: String;Items: TStrings): Boolean;

var

i: Integer;

begin

for I := 0 to Items.Count-1 do

if Items[i] = FileName then

begin

NotInList := False;

Exit;

end;

NotInList := True;

end;

6.3.3.3 按指定匹配字符串显示当前目录中的文件

    当在FileEdit中输入一个匹配字符串，并回车，文件列表框将显示匹配结果。这一功能在FileEdit的OnKeyPress事件中实现。

procedure TFileCtrForm.FileEditKeyPress(Sender: TObject; var Key: Char);

begin

if Key = #13 then

begin

FileListBox1.ApplyFilePath(FileEdit.Text);

Key := #0;

end;

end;

    文件列表框提供的ApplyFilePath方法是解决这一问题的关键所在。

6.3.3.4 按指定匹配字符串查找当前目录中的文件

    为了进行比较，我们用另一种方法来实现文件的查找功能，即利用标准过程FindFirst、FindNext。FileList1与ListBox1 中的内容完全一致。

当用户单击“查找”按钮时，与FileEdit 中字符串相匹配的文件将显示在ListBox1中。下面是实现代码。

procedure TFileCtrForm.Button1Click(Sender: TObject);

var

i: Integer;

SearchRec: TSearchRec;

begin

Searched := True;

Label5.Caption := 'Search Result';

ListBox1.Items.Clear;

FindFirst(FileEdit.text,faAnyFile,SearchRec);

ListBox1.Items.Add(SearchRec.Name);

Repeat

i := FindNext(SearchRec);

If i = 0 then

ListBox1.Items.Add(SearchRec.Name);

until i <> 0;

end;

    SearchRec是一个TSearchRec类型的记录。TSearchRec的定义如下：

TSearchRec = record

Fill: array[1..21] of Byte;

Attr: Byte;

Time: Longint;

Size: Longint;

Name: string[12];

end;

   在这一结构中提供了很多信息，灵活应用将给编程带来很大方便。下面我们举几个例子。

    1. 检测给定文件的大小。

function GetFileSize(const FileName: String): LongInt;

var

SearchRec: TSearchRec;

begin

if FindFirst(ExpandFileName(FileName), faAnyFile, SearchRec) = 0 then

Result := SearchRec.Size

else

Result := -1;

end;

这一程序将在下一节中应用。

    2. 获取给定文件的时间戳，事实上等价于FileAge函数。

    function GetFileTime(const FileName: String): Longint;

var

SearchRec: TSearchRec;

begin

if FindFirst(ExpandFileName(FileName),faAnyFile, SearchRec) = 0 then

Result := SearchRec.Time

else

Result := -1;

end;

3. 检测文件的属性。如果文件具有某种属性，则

SearchRec.Attr And GivenAttr > 0

属性常量对应的值与意义如下表：

     表6.6 属性常量对应的值与意义

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

常量 值 描述

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

faReadOnly $01 只读文件

faHidden $02 隐藏文件

faSysFile $04 系统文件

faVolumeID $08 卷标文件

faDirectory $10 目录文件

faArchive $20 档案文件

faAnyFile $3F 任何文件

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

6.4 文件管理综合举例：文件管理器的实现

    在本章的最后，我们利用Delphi提供的文件控件和文件管理函数开发一个简单的文件管理器。虽然这一文件管理器还无法和Windows提供的文件管理器相比拟，但它也为一般的文件操作提供了足够多的功能，而且如果读者感兴趣，还可以对它做进一步的扩充。在后边的拖放操作一章中，我们就为它提供了拖放支持，使它看起来更象一个“文件管理器”。

6.4.1 设计基本思路

6.4.1.1 窗口设计

    文件管理器的主窗口是一个多文档界面(MDI)。有关文件、目录的显示和文件管理功能的实现都放在子窗口中。在程序执行过程中将根据需要弹出一些完成不同操作的对话框。这些对话框都是在需要时动态生成的。表6.7给出了本程序所设计窗体的清单。

     表6.7 FileManger窗体清单

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

窗体类 功能 用于创建该类窗体的菜单项

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

TFileManager 主窗口

TFMForm 子窗口 Windows|New Window

TFileAttrForm 显示文件属性 File|Properties;Function|Search

TChangeForm 文件移动、拷贝、改名、改变 File|Move.Cope.Rename 当前目录等操作的输入对话框 Directory|change Directory

TSearchForm 输入待查找文件的名称和路径 Function|Search

TDiskViewForm 显示磁盘信息 Function|Disk View

TViewDir 输入待创建的子目录 Directory|CreateDirectory

TAboutBox 显示版权信息 Help|About

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

6.4.1.2 界面设计

    主窗口界面主要是主菜单和用于表示当前目录、当前文件的状态条。

     表6.8 主窗口界面设计

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

部件 属性 功能

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

FileManager Style=fsMDI 主窗口

WindowMenu=Windows

Position=poDefault

MainMenu1 主菜单

FilePanel Align=alBottom 显示当前选中文件

BevelInner=bvLowered

BevelWidth=2

DirectoryPanel Align=alBottom 显示当前选中目录

Alignment=taLeftJustify

BevelInner=bvLowered

BevelWidth=2

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

主窗口主菜单包括File、WIndows、Help三项。File菜单项在子窗口生成时被子窗口同名菜单项所取代。设置Windows、Help的GroupIndex = 9，可以使子窗口生成时这两个菜单项仍存在。

    子窗口界面包括主菜单、目录树(DirectoryOutline)、文件列表框、 用于显示驱动器的标签集(TabSet)以及三个用于显示驱动器类型的TImage部件。

   表6.9 子窗口界面设计

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

部件 属性 功能

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

FMForm ActiveControl=DirectoryOutline 子窗口

Position=poDefault

Style=fsMDIChild

MainMenu1 主菜单

DriveTabSet Align=alTop 显示驱动器

style=tsOwnerDraw

DirectoryOutline Align=alLeft 显示当前驱动器的目录树

options=[ooDrawTreeRoot,

ooDrawFocusRect,ooStretchBitmaps]

FileList Align=alClient 显示当前目录中的文件

FileType=[ftReadOnly,

ftHidden,ftSystem,ftArchive,ftNormal]

ShowGlyphs=True

Network(Image) Picture(Network.bmp) 标志网络驱动器

Vsible=False

Floppy(Image) Picture(Floppy.bmp) 标志软驱

Visible=False

Fixed(Image) Picture(Fixed.bmp) 标志硬驱

Visible=False

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

子窗口主菜单包括File、Function、Directory三个菜单项， 分别用于完成文件的基本管理功能、其它管理功能和目录管理功能。

    由于对话框界面设计很简单，这里不再进行赘述。 读者可直接参考后面将给出的对话框界面图(图6.8---6.13)进行设计。

6.4.2 子窗口的创建、布置和关闭

    子窗口的创建、布置由父窗口的Windows菜单控制，其菜单项如下：

    ● New Windows : 创建新的子窗口

● Tile : 平铺

    ● Cascade : 层叠

    ● ArrangeIcon : 排列图标

   ● Minimized All : 极小化所有子窗口

    子窗口的创建只需要简单调用窗体的Create方法：

    FileMan := TFMForm.Create(Application);

子窗口的标准排列方式直接调用MDI窗口的标准方法Tile、Cascade和ArrangeIcons。

    极小化所有子窗口的实现利用MDI窗口的两个属性：MDIChildCount和MDIChildren：

    for i := 0 to MDICount - 1 do

MDIChildren[i].Windowstate := wsMinimized;

    子窗口关闭时释放内存空间，为此在子窗口TFMForm的OnClose事件中令

Action := OnFree；

    为了保持和Windows的File Manager的一致性，我们也禁止关闭最后一个子窗口，这需要在子窗口的OnCloseQuery事件处理过程中实现：

If FileManager.MDIChildCount <= 1 then

CanClose := False;

CanClose是OnCloseQuery事件过程返回的一个参数，用于判定窗口是否可以关闭。

    由于这一过程归子窗口所有，因而MDIChildCount前必须加上其对象名FileManager。

    但不幸的是：这样一来我们的程序无法终止了！原来MDI窗口关闭前首先关闭其所有的子窗口。如果子窗口不能关闭，MDI窗口也不能关闭。

为此我们需要判断发出关闭消息的是子窗口的系统菜单还是菜单的Exit项。

    定义一个全局变量

    var

ExitClick: Boolean;

在子窗口的Exit1Click事件处理过程中：

ExitClick := True;

FileManager.Exit1Click(Sender);

    子窗口关闭前可以利用这一全局变量检测是否应关闭：

    If (FileManager.MDIChildCount <= 1) and (Not ExitClick) then

CanClose := False;

6.4.3 文件控件的联系

    在本例中我们使用了一组新的控件：TabSet、DirectoryOutline、FileListBox，用于显示和选择驱动器、目录和文件。与(6.3)中所用方法相比，使用这一组控件需要少量的代码支持。

    TabSet与DirectoryOutline的联系在TabSet的Click事件处理过程中建立：

    With DriveTabSet do

DirectoryOutline.Drive := Tabs[TabIndex][1];

DirectoryOutline与FileListBox的联系在DirectoryOutline的Change事件处理过程中建立：

FileList.Directory := DirectoryOutline.Directory;

FileList.Update;

6.4.4 DriveTabSet的自画风格显示

    Dephi为一些控件提供了自画风格的显示，如ListBox、ComboBox、TabSet等。 在缺省情况下，这些控件自动显示文本。而在自画风格下，拥有控件的窗体在运行时间内自己画出控件的每一项目。

自画风格显示通常的应用是为项目除文本外再添加图形显示。能以自画风格显示的控件有一个共同特点：都拥有一个TStrings类型的项目链。由于TStrings类的特点(参第三章)，它们都可以加入一个和对应文本相联系的对象。 而这正是自画风格显示的关键。

    通常情况下产生一个自画风格需要三个步骤：

    1.设置自画风格；

    2.向字符串链表添加图形对象；

    3.画出自画项目。

6.4.4.1 设置自画风格

    控件属性Style 用于设置自画风格。对于DriveTabSet，我们把Style 属性设置为tsOwnerDraw。

    对于ListBox、ComboBox等控件的设置与TabSet略有差异，读者可参阅联机帮助文档。

6.4.4.2 向字符串链表添加图形对象

    1.在应用程序中添加图片部件

    在本程序中我们设置了三个图片部件NetWork、Floppy、Fixed，并分别与三个位图文件NetWork.bmp、Floppy.bmp、Fixed.bmp相关联。

    2.把图片添加到字符串链表中

    根据字符串链表的性质，我们可以把对象与已存在的字符串建立联系，也可以同时添加字符串和对象。这里我们采用后一种方法。

    在子窗口的OnCreate事件处理过程中，我们利用一个循环依次检测从a到z的驱动器是否存在以及驱动器的类型。这利用了Windwos API函数GetDrivetype, 如果驱动器不存在则返回0，否则返回驱动器的类型(DRIVE_REMOVABLE、DRIVE_FIXED、DRIVE_REMOTE)。根据驱动器类型我们可以判断与文本(驱动器名)同时添加到Tabs中的不同图形对象。在添加过程中，DriveTabSet的TabIndex被设置为当前驱动器。

程序清单如下：

procedure TFMForm.FormCreate(Sender: TObject);

var

Drive, AddedIndex: Integer;

DriveLetter: Char;

begin

for Drive := 0 to 25 do

begin

DriveLetter := Chr(Drive + ord('a'));

case GetDrivetype(Drive) of

DRIVE_REMOVABLE:

AddedIndex := DriveTabSet.Tabs.AddObject(DriveLetter, Floppy.Picture.Graphic);

DRIVE_FIXED:

AddedIndex := DriveTabSet.Tabs.AddObject(DriveLetter, Fixed.Picture.Graphic);

DRIVE_REMOTE:

AddedIndex := DriveTabSet.Tabs.AddObject(DriveLetter, Network.Picture.Graphic);

end;

if UpCase(DriveLetter) = UpCase(FileList.Drive) then

DriveTabSet.TAbIndex := AddedIndex;

end;

end;

6.4.4.3 画出自画项目

    当把一个控件的风格设置为自画时，Windows不再负责往屏幕上画出控件的项目，而是为每个可见项目产生自画事件。应用程序可以通过处理自画事件画出控件的项目。

1.确定自画项目的大小

    对于TabSet而言，这在OnMeasureTab事件处理过程中完成。我们需要把DriveTabSet每个标签的宽度增大到足以同时放下文本和位图。

procedure TFMForm.DriveTabSetMeasureTab(Sender: TObject; Index: Integer;

var TabWidth: Integer);

var

BitmapWidth: Integer;

begin

BitmapWidth := TBitmap(DriveTabSet.Tabs.Objects[Index]).Width;

Inc(TabWidth, 2 + BitmapWidth);

end;

    由于TStrings的Objects属性中存放的对象都是TObject类型，并没有Width属性，因而需要再把它转化为TBitmap类型的对象：

   BitmapWidth := TBitmap(DriveTabSet.Tabs.Objects[Index]).Width;


第六章 文件管理（三）

DELPHI基础教程

第六章　文件管理（三）

2.画出每个自画项目

    这在TabSet的OnDrawTab事件处理过程中完成。这一事件处理过程的参数中包含了待画项目索引、画板、待画区域、是否被选中等。这里我们只利用了前三个参数。事实上利用最后一个参数，我们可以对被选中的标签进行一些特殊的视觉效果处理。这一工作就留给读者自己去完成。

procedure TFMForm.DriveTabSetDrawTab(Sender: TObject; TabCanvas: TCanvas;

R: TRect; Index: Integer; Selected: Boolean);

var

Bitmap: TBitmap;

begin

Bitmap := TBitmap(DriveTabSet.Tabs.Objects[Index]);

with TabCanvas do

begin

Draw(R.Left, R.Top + 4, Bitmap);

TextOut(R.Left + 2 + Bitmap.Width, R.Top + 2, DriveTabSet.Tabs[Index]);

end;

end;

6.4.5 文件管理基本功能的实现

    在子窗口的File菜单中，定义了文件管理的基本功能，它们是：

    ● Open ：打开或运行一个文件(从文件列表框双击该文件可实现同样效果)

● Move ：文件在不同目录间的移动

    ● Copy ：文件拷贝

    ● Delete ：文件删除

    ● Rename ：文件更名

    ● Properties ：显示文件属性

6.4.5.1 文件打开

    文件打开功能可以运行一个可执行文件，或把文件在与之相关联的应用程序中打开。文件总是与创建它的应用程序相关联，这种关联可以在Windows的文件管理器中修改。要注意的是：文件的关联是以后缀名为标志的，因而对一个文件关联方式的修改将影响所有相同后缀名的文件。

    文件打开功能实现的关键是利用了Windows API函数ShellExecute 。由于Windows API函数的参数要求字符串类型是PChar，而Delphi中一般用的是有结束标志的String类型，因此为调用方便我们把这一函数进行了重新定义如下。

function ExecuteFile(const FileName, Params, DefaultDir: String;

ShowCmd: Integer): THandle;

var

zFileName, zParams, zDir: array[0..79] of Char;

begin

Result := ShellExecute(Application.MainForm.Handle, nil,

StrPCopy(zFileName, FileName), StrPCopy(zParams, Params),

StrPCopy(zDir, DefaultDir), ShowCmd);

end;

    以上函数在fmxutils单元中定义。fmxutils是一个自定义代码单元。

    有关ShellExecute中各参数的具体含义读者可查阅联机Help文件。

    StrPCopy把一个Pascal类型的字符串拷贝到一个无结束符的PChar类型字符串中。

    在子窗口的Open1Click事件处理过程中：

procedure TFMForm.Open1Click(Sender: TObject);

begin

with FileList do

ExecuteFile(FileName, '', Directory, SW_SHOW) ;

end;

    如果FileList允许显示目录的话(即FileType属性再增加一项ftDirectory)，那么对于一个目录而言，打开的含义应该是显示它下边的子目录和文件。程序修改如下。

   procefure TFMForm.Open1Click(Sender: Tobject);

begin

With FileList do

begin

if HasAttr(FileName,faDirectory) then

DirectoryOutline.Directory := FileName

else

ExecuteFile(FileName,' ' ,Directory,SW_SHOW);

end;

end;

其中HasAttr是一个fmxutils单元中的自定义函数，用于检测指定文件是否具有某种属性。

function HasAttr(const FileName: String; Attr: Word): Boolean;

begin

Result := (FileGetAttr(FileName) and Attr) = Attr;

end;

6.4.5.2 文件拷贝、移动、删除、更名

    文件拷贝的关键是使用了以文件句柄为操作对象的文件管理函数，因而提供了一种底层的I/O通道。在Object Pascal中这一点是利用无类型文件实现的。

    在文件拷贝中首先检查目标文件名是否是一个目录。如是则把原文件的文件名添加到目标路径后，生成目标文件全路径名。而后提取源文件的时间戳，以备拷贝完成后设置目标文件。拷贝过程中使用了返回文件句柄或以文件句柄为参数的文件管理函数FileOpen、FileCreate、FileRead、FileWrite、FileClose。为保证文件的正常关闭和内存的释放，在拷贝过程中进行异常保护。

过程CopyFile实现上述功能，它定义在fmxutils单元中。

procedure CopyFile(const FileName, DestName: TFileName);

var

CopyBuffer: Pointer;

TimeStamp, BytesCopied: Longint;

Source, Dest: Integer;

Destination: TFileName;

const

ChunkSize: Longint = 8192;

begin

Destination := ExpandFileName(DestName);

if HasAttr(Destination, faDirectory) then

Destination := Destination + '\' + ExtractFileName(FileName);

TimeStamp := FileAge(FileName);

GetMem(CopyBuffer, ChunkSize);

try

Source := FileOpen(FileName, fmShareDenyWrite);

if Source < 0 then

raise EFOpenError.Create(FmtLoadStr(SFOpenError, [FileName]));

try

Dest := FileCreate(Destination);

if Dest < 0 then

raise EFCreateError.Create(FmtLoadStr(SFCreateError,[Destination]));

try

repeat

BytesCopied := FileRead(Source, CopyBuffer^, ChunkSize);

if BytesCopied > 0 then

FileWrite(Dest, CopyBuffer^, BytesCopied);

until BytesCopied < ChunkSize;

finally

FileSetDate(Dest,TimeStamp);

FileClose(Dest);

end;

finally

FileClose(Source);

end;

finally

FreeMem(CopyBuffer, ChunkSize);

end;

end;

    如果我们不使用FileSetDate过程，Windows自动把当前时间作为时间戳写入文件。

    文件移动事实上是文件拷贝与文件删除的结合。fmxutils单元中的MoveFile过程实现了这一功能。

procedure MoveFile(const FileName, DestName: TFileName);

var

Destination: TFileName;

begin

Destination := ExpandFileName(DestName);

if not RenameFile(FileName, Destination) then

begin

if HasAttr(FileName, faReadOnly) then

raise EFCantMove.Create(Format(SFCantMove, [FileName]));

CopyFile(FileName, Destination);

DeleteFile(FileName);

end;

end;

EFCanMove是一个自定义异常类：

    type

EFCanMove := Class(EStreamError);

    有关自定义异常类请参阅第十二章。

    文件删除、文件更名直接调用Delphi文件管理过程DeleteFile、RenameFile。它们都以文件名为参数。操作执行前应弹出一个对话框进行确认，执行完毕后应调用Update方法更新FileList的显示。

6.4.5.3 一致的界面

    文件拷贝、文件移动、 文件更名以及后边的改变当前目录在形式上都表现为从一个源文件到一个目标文件。因而可以采用统一的用户界面，即ChangeForm对话框

这四个菜单项共用一个Click事件处理过程，通过对Sender参数的检测，决定将要打开对话框的标题和显示内容。当用户按OK键关闭且目标文件(目录)非空时，程序弹出一个消息对话框要求用户进一步确认，而后执行相应的动作。

    共用的事件处理过程FileChange的程序清单如下：

procedure TFMForm.FileChange(Sender: TObject);

var

ChangeForm: TChangeForm;

IsFile: Boolean;

begin

ChangeForm := TchangeForm.Create(Self);

IsFile := True;

with ChangeForm do

begin

if Sender = Move1 then Caption := 'Move'

else if Sender = Copy1 then Caption := 'Copy'

else if Sender = Rename1 then Caption := 'Rename'

else if Sender = ChangeDirectory1 then

begin

Caption:='Change Directory';

IsFile:=False;

end

else Exit;

if IsFile then

begin

CurrentDir.Caption := FileList.Directory;

FromFileName.Text := FileList.FileName;

ToFileName.Text := '';

end

else

begin

CurrentDir.Caption := DriveTabSet.Tabs[DriveTabSet.TabIndex];

FromFileName.Text := DirectoryOutline.Directory;

ToFileName.Text := '';

end;

if (ShowModal <> idCancel) and (ToFileName.Text <> '') then

ConfirmChange(Caption, FromFileName.Text, ToFileName.Text);

end;

end;

其中用到的自定义私有过程ConfirmChange用于执行相应的动作：

procedure TFMForm.ConfirmChange(const ACaption, FromFile, ToFile: String);

begin

if MessageDlg(Format('%s %s to %s', [ACaption, FromFile, ToFile]),

mtConfirmation, [mbYes, mbNo], 0) = idYes then

begin

if ACaption = 'Move' then

MoveFile(FromFile, ToFile)

else if ACaption = 'Copy' then

CopyFile(FromFile, ToFile)

else if ACaption = 'Rename' then

RenameFile(FromFile, ToFile)

else if ACaption = 'Change Directory' then

changeDirectory(ToFile);

FileList.Update;

end;

end;

6.4.5.4 显示文件属性

    当程序执行Properties 菜单项的Click 事件处理过程时，首先弹出一个TFileAttrForm类型的对话框，显示文件的属性

当用户修改并确认后程序重新设置文件属性。

    Properties菜单项的Click事件处理过程如下：

procedure TFMForm.Properties1Click(Sender: TObject);

var

Attributes, NewAttributes: Word;

FileAttrForm: TFileAttrForm;

begin

FileAttrForm := TFileAttrForm.Create(self);

ShowFileAttr(FileAttrForm,FileList.FileName,FileList.Directory);

end;

    其中过程ShowFileAttr的实现如下：

procedure TFMForm.ShowFileAttr(FileAttrForm:TFileAttrForm;

AFileName,Directory:String);

var

Attributes,NewAttributes: Word;

begin

with FileAttrForm do

begin

FileName.Caption := AFileName;

FilePath.Caption := Directory;

ChangeDate.Caption := DateTimeToStr(FileDateTime(AFileName));

Attributes := FileGetAttr(AFileName);

ReadOnly.Checked := (Attributes and faReadOnly) = faReadOnly;

Archive.Checked := (Attributes and faArchive) = faArchive;

System.Checked := (Attributes and faSysFile) = faSysFile;

Hidden.Checked := (Attributes and faHidden) = faHidden;

if ShowModal <> idCancel then

begin

NewAttributes := Attributes;

if ReadOnly.Checked then NewAttributes := NewAttributes or faReadOnly

else NewAttributes := NewAttributes and not faReadOnly;

if Archive.Checked then NewAttributes := NewAttributes or faArchive

else NewAttributes := NewAttributes and not faArchive;

if System.Checked then NewAttributes := NewAttributes or faSysFile

else NewAttributes := NewAttributes and not faSysFile;

if Hidden.Checked then NewAttributes := NewAttributes or faHidden

else NewAttributes := NewAttributes and not faHidden;

if NewAttributes <> Attributes then

FileSetAttr(AFileName, NewAttributes);

end;

end;

end;

以上过程中用到的函数FileDataTime在fmxutils单元中定义，返回一个TDatatime类型的变量。

function FileDateTime(const FileName: String): System.TDateTime;

begin

Result := FileDateToDateTime(FileAge(FileName));

end;

6.4.6 其它文件管理功能的实现

    在子窗口的Function菜单中，定义了一些其它的文件管理功能：

    ● Search ：查找一个给定名字的文件，若存在则显示该文件属性

    ● Disk View ：显示当前驱动器的大小和剩余空间

    ● View type ：确定显示文件的类型

6.4.6.1 文件查找

    当用户单击Search菜单项时，程序弹出一个对话框(如图6.10)，要求输入待查找的文件名和查找路径。文件名可以是通配符。当用户确认后程序显示第一个匹配文件的属性(如图6.9)。查找不到匹配文件则给出相应的信息。

在实现这一功能的最初设计中，我试图使用FileSearch函数，这个函数允许在多个不同路径中查找。但可惜的是：也许由于系统设计者的失误，这个函数并没有返回它应该返回的东西(第一个匹配文件的全路径名)，而是仍把输入的匹配符返回。

    没有办法我只能再次使用FindFirst，这个函数的特性在6.3节中已进行了介绍。下面是这一功能的实现代码。

procedure TFMForm.search1Click(Sender: TObject);

var

SearchForm: TSearchForm;

FileAttrForm: TFileAttrForm;

FindIt,path: String;

SearchRec: TSearchRec;

Return: Integer;

begin

SearchForm := TSearchForm.Create(self);

with SearchForm do

begin

SearchFile.text := '';

SearchPath.text := DirectoryOutline.Directory;

if (ShowModal <> idCancel) and

(SearchFile.Text <> '') and (SearchPath.text <> '') then

begin

FindIt := SearchPath.text+'\'+SearchFile.text;

Return := FindFirst(FindIt,faAnyFile,SearchRec);

if Return <> 0 then

FindIt := ''

else

FindIt := ExpandFileName(SearchRec.Name);

end;

if FindIt = '' then

MessageDlg('Cannot find the file in current directory.',

mtWarning, [mbOk], 0)

else

begin

Path := ExtractFilePath(FindIt);

FindIt := ExtractFileName(FindIt);

FileAttrForm := TFileAttrForm.Create(self);

ShowFileAttr(FileAttrForm,FindIt,Path);

end;

end;

end;

6.4.6.2 显示磁盘信息

    当用户单击Disk View菜单项时，将弹出一个TDiskViewForm类型的对话框，用来显示当前磁盘的信息

磁盘信息的获取是在DiskViewForm中DriveEdit编辑框的OnChange事件处理过程中实现的。

procedure TDiskViewForm.driveEditChange(Sender: TObject);

var

dr: Byte;

Free,Total: LongInt;

begin

Free := DiskFree(0);

Total := DiskSize(0);

FreeSpace.text := IntToStr(Free)+ ' bytes.';

TotalSpace.text := IntToStr(Total) + ' bytes.';

end;

    DiskFree、DiskSize带参数为0表示当前驱动器。读者可以很容易把它改成按用户输入显示磁盘信息的情况。

    DiskViewForm中的三个编辑框设计时都令ReadOnly为True。

6.4.6.3 改变显示文件的类型

    改变显示文件的类型事实上是设置FileList的Mask属性。我们利用一个标准的InputBox输入文件的匹配字符串。而后利用Update方法更新FileList。

procedure TFMForm.Viewtype1Click(Sender: TObject);

var

FileMask: String;

begin

FileMask := InputBox('File type','Input File type For View :',FileList.Mask);

If FileMask = '' then FileMask := '*.*';

FileList.Mask := FileMask;

FileList.Update;

CreateCaption;

end;

    其中的CreateCaption私有过程将在(6.4.8)中进行介绍。

6.4.7 目录管理功能的实现

    在子窗口的Directory菜单中，提供了目录管理功能：

    ● Create Directory ：创建一个子目录

    ● Delete Directory ：删除一个空的子目录

    ● Change Directory ：改变当前目录

6.4.7.1 创建目录

    创建目录时首先弹出一个TNewDir类型的对话框

对话框中要求用户输入目录名。如果用户不输入路径，则缺省认定为当前目录的子目录：

    Dir := ExpandFileName(DirName.Text)；

    而后调用MkDir函数。在目录创建过程中关闭了I/O错误检测，出错不产生异常而是把IOResult设置为非零值。通过检查IOResult是否为0可以确定创建是否成功。

程序清单如下：

procedure TFMForm.CreateDirectory1Click(Sender: TObject);

var

NewDir: TNewDir;

Dir: String;

begin

{$I-}

NewDir := TNewDir.Create(self);

with NewDir do

begin

CurrentDir.Caption := DirectoryOutline.Directory;

if (ShowModal <> idCancel) and (DirName.Text <> '') then

Dir := ExpandFileName(DirName.text);

end;

MkDir(Dir);

if IOResult <> 0 then

MessageDlg('Cannot Create directory', mtWarning, [mbOk], 0);

end;

    但不幸的是目录创建后我们却无法从当前目录树中看到。必须移到另一个驱动器而后再返回，创建的目录才是可见的。在后边我们将提供一种解决方法。

6.4.7.2 删除目录

    在实现目录删除过程中，远不如创建目录那么顺利。碰到的问题是:

    1.RmDir不允许删除当前目录。但为了操作方便，我们要求删除的恰恰是当前目录；

    2.目录删除后调用Refresh方法或Update方法并不能使该目录从屏幕显示中去除。因而当用户试图进入该目录时会导致系统崩溃。

    对第一个问题，我们的解决办法是把当前目录转换到其父目录。假如读者记得目录也被操作系统作为一种特殊的文件对待的话，那么就不会对下面的语句感到奇怪了：

    path := DirectoryOutline.Directory;

    Directoryoutlin.Directory := ExpandFilePath(Path);

    而后调用RmDir过程：

RmDir(Path)；

    第二个问题的解决却颇为费神。因为DirectoryOutline是Delphi提供的示例部件，没有Help文件支持。通过试验发现：只有当DirectoryOutline的Drive属性改变时，才重新从相应驱动器读取目录。而且它基本上是只读的，除非清除( Clear) 它，象Add、Delete这些方法对它都是无效的。

    我曾经考虑过一个笨拙的方法，那就是先改变当前驱动器而后再改回来。但这种方法一方面速度无法忍受，另一方面当只存在一个驱动器可用时会导致系统崩溃。

    正当我一筹莫展时，突然想到：DirectoryOutline是一个Sample部件，Delphi 提供了它的源代码。而当我分析了它的源代码后，我知道应该做什么了，那就是为DirectoryOutline增添一个Reset方法!

6.7.3 为部件增添一个方法

    严格地说，我们所做的工作属于创建一个新部件。但因为我们有源代码，所以不必从DirectoryOutline继承而是直接修改它。这样我们可以省去与创建部件有关的许多繁琐工作。对创建新部件感兴趣的读者可阅读本书第三编的有关章节。

    在Delphi IDE中打开DirectoryOutline的源文件后：

1.把库单元名改为DirPlus，类名改为TDirectoryOutlinePlus，表明这是DirectoryOutline的增强版。而后存入另一个目录中；

   2.添加一个公有方法Reset。这一方法的作用是重新读取当前驱动器的目录。程序清单如下。

procedure TDirectoryOutlinePlus.Reset;

begin

ChDir(FDrive + ':');

GetDir(0, FDirectory);

FDirectory := ForceCase(FDirectory);

if not (csLoading in ComponentState) then BuildTree;

end;

    读者也许被这段代码弄糊涂了。由于篇幅所限，而且涉及到许多自定义部件开发的内容，我们也不准备去详细解释它。假如读者想彻底搞懂它，我建议先看一下本书第三编有关自定义部件开发的内容，而后再对照原DirectoryOutline的源代码进行分析。

    3.编译成一个库文件DirPlus.tpu;

4.把DirPlus加入部件的Samples页中。

    如何添加一个部件见第三编有关章节的介绍。

    当增强的目录树准备好以后，必须修改我们的子窗口设计，但却不必亲自修改源代码。

    1.删除子窗口中的TDirectoryOutline类部件DirectoryOutline。此时FileList占据了整个客户区；

    2.把FileList的Align属改为None，并留出左边的空白供放部件用；

    3.在窗口左部加入TDirectoryOutlinPlus类的部件DirectoryOutline；

4.把DirectoryOutline的Align属性改为Left，FileList的Align属性还原为Client；

5.在DirectoryOutline的事件OnChange列表中选取DirectoryOutlineChange，即原DirectoryOutline的处理过程。

    以上工作的最终目标是实现目录创建、删除后屏幕的正确显示。这只需要调用DirectoryOutline的Reset方法即可。

目录删除过程的实现代码如下。

procedure TFMForm.DeleteDirectory1Click(Sender: TObject);

var

path: String;

k: Integer;

begin

{$I-}

path := DirectoryOutline.Directory;

DirectoryOutline.Directory := ExtractFilePath(Path);

if MessageDlg('Delete ' + path + '?', mtConfirmation,[mbYes, mbNo], 0) = idYes then

RmDir(path);

if IOResult <> 0 then

MessageDlg(' Cannot remove directory! The path might not'+

'exist,non-empty or is the current logged directory.',mtWarning,[mbOk], 0)

else

DirectoryOutline.Reset;

end;

修改后的目录创建过程如下。

procedure TFMForm.CreateDirectory1Click(Sender: TObject);

var

NewDir: TNewDir;

Dir: String;

begin

{$I-}

NewDir := TNewDir.Create(self);

with NewDir do

begin

CurrentDir.Caption := DirectoryOutline.Directory;

if (ShowModal <> idCancel) and (DirName.Text <> '') then

Dir := ExpandFileName(DirName.text);

end;

MkDir(Dir);

if IOResult <> 0 then

MessageDlg('Cannot Create directory', mtWarning, [mbOk], 0)

else

DirectoryOutline.Reset;

end;

    当完成了这些工作，把程序重新编译、运行后，可以发现我们所希望实现的功能完全实现了！同时，我们有了一个更好的目录树部件。

6.4.7.4 改变当前目录

    改变当前目录的实现非常简单，只要修改DirectoryOutline的Directory属性。但需注意的是：当改变后目录所在驱动器也发生变化时应相应修改DriveTabSet的当前值。由于驱动器名与DriveTabSet的索引属性TabIndex之间并没有确定的对应关系，因而需要通过一个循环进行查找匹配。

Change Directory的菜单事件处理过程是FileChange，即与文件的移动、拷贝、更名共用一个事件处理过程。详细情况请读者参看(6.4.5.3)中的介绍。

改变当前目录的实现如下。

procedure TFMForm.ChangeDirectory(Todir: String);

var

i: Integer;

begin

{$I-}

ChDir(ToDir);

if IOResult <> 0 then

MessageDlg('Cannot find directory', mtWarning, [mbOk], 0)

else

begin

with DirectoryOutline do

begin

Directory := ToDir;

Refresh;

if DriveTabSet.Tabs[DriveTabSet.TabIndex][1]<>drive then

for I := 1 to 25 do

if DriveTabSet.Tabs[i][1] = drive then

begin

DriveTabSet.TabIndex := i;

Exit;

end;

end;

end;

end;

6.4.8 一些问题的处理

6.4.8.1 子窗口的标题

    Windows的文件管理器是我们设计的楷模，在子窗口显示标题上也不例外。我们把当前目录加上文件的类型作为子窗口的标题。

过程CreateCaption用于生成子窗口的标题。

procedure TFMForm.CreateCaption;

var

Cap: String;

begin

Cap := DirectoryOutline.Directory;

Cap := cap+'\'+FileList.mask;

Caption := Cap;

end;

当前目录或文件显示类型发生变化时改变子窗口的标题。如DirectoryOutline的Change事件处理过程和ViewType菜单项的Click事件处理过程就调用了该过程。

6.4.8.2 状态条的显示

    状态条用于显示当前目录和当前选中文件。它们的值在DirectoryOutline 和FileList的Change事件处理过程中修改。

    DirectoryOutline和FileList最终的Change事件处理过程如下：

procedure TFMForm.DirectoryOutlineChange(Sender: TObject);

begin

CreateCaption;

FileList.clear;

FileList.Directory := DirectoryOutline.Directory;

FileList.Update;

FileManager.DirectoryPanel.Caption := DirectoryOutline.Directory;

end;

procedure TFMForm.FileListChange(Sender: TObject);

begin

with FileList do

begin

if (ItemIndex >= 0) and (Not HasAttr(FileName,faDirectory)) then

begin

TheFileName := FileName;

FileManager.FilePanel.Caption :=

Format('%s, %d bytes', [TheFileName, GetFileSize(TheFileName)]);

end

else

FileManager.FilePanel.Caption := '';

end;

end;

6.4.8.3 版本信息

    当用户单击主窗口的Help|About菜单项时将弹出一个About对话框，用于显示版本信息（如图6.13）。

    这一对话框是用Delphi提供的模板做的。

6.4.8.4 菜单项的变灰与使能

    File菜单中定义的文件管理功能只有当活动焦点在FileList(即有当前选中文件)时才起作用。否则所有菜单项应变灰，以免导致系统崩溃。

    这一功能在File菜单的Click事件处理过程中实现。这一点并不很容易被人想到，希望读者能从中受到启发。

procedure TFMForm.File1Click(Sender: TObject);

var

FileSelected: Boolean;

begin

FileSelected := FileList.ItemIndex >= 0;

Open1.Enabled := FileSelected;

Delete1.Enabled := FileSelected;

Copy1.Enabled := FileSelected;

Move1.Enabled := FileSelected;

Rename1.Enabled := FileSelected;

Properties1.Enabled := FileSelected;

end;

    判断是否有文件被选中是通过检测ItemIndex属性是否大于等于0来实现的。

     FileSelected := FileList.ItemIndex >= 0；

6.4.8.5 可重用的文件处理模块

    库单元fmxutils是一个代码库，提供了若干文件处理模块。这些模块除在本程序中使用外，读者可以在其它应用程序中直接调用，而且不必重新编译，只要在Uses子句中包含即可。从中我们可以体会到，Delphi 以库单元为中心的程序组织方式提供了一种较完善的代码重用机制。

6.4.9 小结

    文件管理器是一个较为综合的例程，使用到了绝大部分以文件名、文件句柄以及其它参数(除文件变量)为操作对象的文件管理过程/函数，同时也提供了一些程序设计开发的思想。我们的介绍是以程序功能模块来组织的，我建议读者在学习并试图自己建立这一程序时采用同样的方法。(6.4.8)中的内容或许是一开始就应了解的，但其它完全可以按顺序逐步地扩充，最后得到一个完整的程序。这一例程在后边的拖放操作和异常处理等章节中还要用到。读者可以以此为基础进一步完善它，使它真正成为一个完全实用的程序。

    文件管理是在开发一个高级的Windows程序中不可避免的要涉及到的问题。本章介绍的思路和方法将为读者成为一个熟练的程序员奠定基础。


第七章 剪贴板和动态数据交换（一）

DELPHI基础教程

第七章　剪贴板和动态数据交换（一）

应用程序间的数据交换是象Windows 这样的多任务环境的重要特性。作为一种基于Windows的开发工具，Delphi支持如下四种数据交换方式：剪贴板、动态数据交换 ( DDE)、对象联接与嵌入(OLE)以及动态联接库(DLLs)。这中间前三种方式最为常用，OLE功能最为强大，DDE次之。而剪贴板使用最为方便。在本章，我们只讨论剪贴板和动态数据交换。利用OLE实现数据交换见下一章，利用动态联接库(DLLs)进行数据交换将在第十章中介绍。

7.1 剪贴板及其应用

本质上，剪贴板只是一个全局内存块。当一个应用程序将数据传送给剪贴板后，通过修改内存块分配标志，把相关内存块的所有权从应用程序移交给Windows自身。其它应用程序可以通过一个句柄找到这个内存块，从而能够从内存块中读取数据。这样就实现了数据在不同应用程序间的传输。

剪贴板虽然功能较为简单，且不能实现实时传输，但却是更为复杂的DDE和OLE的基础。对于一些只是偶尔需要使用其它应用程序数据的程序来说，使用剪贴板不失为一种方便、快捷的方式。

Delphi把剪贴板的大部分功能封装到一个TClipboard类中，同时把使用频度最高的文本传输功能(包括DBImage的图像传输功能)置入相应部件作为部件的方法，从而使用户可以十分方便地使用剪贴板进行编程。

7.1.1 使用剪贴板传输文本

剪贴板传输文本主要是应用如下的三个方法:CopyToClipboard、CutToClipboard 和PasteFromClipboard。包含这些方法的部件如下表所示。

     表7.1 包含剪贴板方法的部件

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

方 法 部 件

———————————————————————————

TDBEdit　TDBMemo

TDBImage

CopyToClipboard TEdit TMemo TMaskEdit

TOLEContainer

TDDEServerItem

———————————————————————————

TDBEdit TDBMemo

CutToClipboard TDBImage

TEdit TMemo TMaskEdit

———————————————————————————

TDBEdit TDBMemo

PasteFromClipboard TDBImage

TEdit TMemo TMaskEdit

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

除TDBImage外，其余全是有关文本的控件。

在把文本传输到剪贴板之前，文本必须被选中。

若选TMaskEdit的AutoSelect属性为True，则当MaskEdit获得输入焦点时文本自动被选中；若选TEdit、TMemo的HideSelection属性为True，则失去焦点时，文本选中状态自动隐藏，重新获得焦点时再显示。

下面的语句把MaskEdit中选中的文本剪切到剪贴板：

MaskEdit .CutToClipboard;

下面的语句把剪贴板中的文本粘贴到Memo的当前光标处：

Memo.PasteFromClipboard；

利用剪贴板类也可以实现文本的传输，见(7.1.2)中的介绍。

7.1.2 剪贴板类

为方便剪贴板的操作，Delphi在Clipbrd库单元中定义了一个TClipboard类，并且预定义了一个变量Clipboard作为类TClipboard的实例，从而使用户在绝大多数场合不必自己去定义一个TClipboard的实例。

利用剪贴板类可以进行文本、图像和部件的传输，剪贴板类为实现这些方法提供了相应的属性和方法。表7.2、表7.3列出了TClipboard属性和方法的意义。

表 7.2 TClipboard的属性

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

属 性 意 义

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

AsText 保存剪贴板的文本，只有运行时才可设置

FormatCount 可用剪贴板格式的数目

Formats 可用剪贴板格式链

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

     表 7.3 TClipboard的方法

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

方 法 参 数 意 义

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

Clear 无 清除剪贴板的内容

Assign Source:TPersistent 把Source参数指定的对象拷贝到剪贴板，常

用于图形、图像对象

Open 无 打开剪贴板，阻止其它应用程序改变它的内容

Close 无 关闭打开的剪贴板

SetComponent Source:TPersistent 把部件拷贝到剪贴板

GetComponent Owner 从剪贴板取回一个部件并放置

Parent :TPersistent

SetAsHandle Format:Word 把指定格式数据的句柄交给剪贴板

返回类型：THandle

GetAsHandle Format:Word 返回剪贴板指定格式数据的句柄

返回类型：THandle

HasFormat Format:Word 判断剪贴板是否拥有给定的格式

返回类型：Boolean

SetTextBuf Buffer:PChar 设置剪贴板的文本内容

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

    剪贴板中可能的数据格式如下表。

表 7.4　剪贴板数据格式及其意义

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

数据格式 意 义

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

CF_TEXT 文本。每行以CF_LF结束，nil标志文本结束

CF_BITMAP Windows位图

CF_METAFILE Windows元文件

CF_PICTURE TPicture类型的对象

CF_OBJECT 任何TPersistent类型的对象

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

利用TClipboard实现文本的传输使用AsText属性和SetTextBuf方法。

AsText属性为非控件部件的剪贴板操作提供了方便。如：

Clipboard. AsText := Form1.Caption ;

把Form1的标题拷贝到剪贴板。

Label1.Caption := Clipboard.AsText;

把剪贴板中的文本写入Label1。

SetTextBuf用于把超过255个字符的字符串拷入剪贴板。

7.1.3 利用剪贴板传输图像

7.1.3.1 拷贝

Image部件上的内容和窗体上的图形可以直接拷贝到剪贴板。图像拷贝利用Clipboard的Assign方法。

例如：

Clipboard.Assign(Image1.Picture);

把Image1上的图像拷贝到剪贴板。

7.1.3.2 剪切

图像的剪切是首先把图像拷贝到剪贴板，而后在原位置用空白图像进行覆盖。

下面一段程序表示了图像的剪切。

procedure TForm1.Cut1Click(Sender: TObject);

var

ARect: TRect;

begin

Clipboard.Assign(Image1.Picture);

with Image.Canvas do

begin

CopyMode := cmWhiteness;

ARect := Rect(0, 0, Image.Width, Image.Height);

CopyRect(ARect, Image.Canvas, ARect);

CopyMode := cmSrcCopy;

end;

end;

7.1.3.3 粘贴

从剪贴板上粘贴图像，首先检测剪贴板上的数据格式。如果格式为CF_BITMAP，则调用目标位图的Assign 方法粘贴图像。

程序清单如下。

procedure TForm1.PasteButtonClick(Sender: TObject);

var

Bitmap: TBitmap;

begin

if Clipboard.HasFormat(CF_BITMAP) then

begin

Bitmap := TBitmap.Create;

try

Bitmap.Assign(Clipboard);

Image.Canvas.Draw(0, 0, Bitmap);

finally

Bitmap.Free;

end;

end;

end;

try...finally为资源保护块，参第十二章。

7.1.4 建立自己的剪贴板观察程序

在这一节中我们要建立一个自己的剪贴板观察程序，用来保存截获到剪贴板中的位图。

Windows允许用户建立自己的剪贴板观察程序，并把该程序添加到一个剪贴板观察器链中。在链中，位置靠前的程序有义务把有关剪贴板的消息传递到紧随其后的观察程序。而处于链首的程序由Windows的消息循环机制直接把剪贴板消息发送过来。

建立一个剪贴板观察程序，首先该程序必须能响应相应的Windows消息。对于那些熟悉Microsoft公司Visual Basic的读者来说，这是令他们头疼而束手无策的地方。但Delphi在这方面却有良好的表现：利用关键字message，用户可以将一个过程定义为响应特定的Windows消息。如：

procedure WMDrawClipboard(var Msg:TWMDrawClipboard);

message WM_DRAWCLIPBOARD;

可以响应WM_DRAWCLIPBOARD消息。类TWMDrawClipboard是消息类Message 的子类。Delphi把所有的消息都重新进行了定义，使用户在使用时可以直接引用其便于记忆的数据成员，而不必再自己动手去分解消息。虽然这并不能算作是一个重大的改进，但却体现了Delphi处处为用户方便着想的特点。

我们将要建立的程序目的是把截获到剪贴板上的位图保存下来。在本书的写作过程中，这一工作是大量存在的。虽然利用Windows工具PaintBrush(画笔），通过粘贴、保存等操作可以实现这一功能，但却存在以下一些问题：

1.程序频繁切换影响效率，当有大量位图存在时更是如此；

2.画笔有一个很讨厌的缺陷：当剪贴板上的位图比画笔界面的客户区大时，客户区外的位图被截断。因而往往需要根据所截获位图的大小来调整画笔客户区的大小，并重新进行粘贴。而如果开始就把画笔客户区调整到足够大，又会覆盖掉屏幕上一些有用的信息。

为解决这些问题，我开发了下面的程序。程序启动时，以极小化方式运行。此时只要剪贴板中存入位图，则自动弹出一个对话框请求用户保存。如果用户希望查看确认，则可以双击运行程序图标，选择相应按钮，剪贴板中的位图就会显示在屏幕上。

部件关键属性设计如下：

ClipSaveForm：

Caption＝‘Save Bitmap in Clipboard '

Panel1:

Align = ' Top '

Image1:

Align = ' Client '

SaveDialog1:

FileEditStyle = fsEdit

FileName = '*.bmp'

Filter = 'Bitmap Files(*.bmp)|*.bmp|Any Files(*.*)|*.*'

InitialDir = 'c:\bmp'

Title = 'Save Bitmap'

程序主窗口是TForm派生类TClipSaveForm的实例。TClipSaveForm通过定义一些私有数据成员和过程，使响应和处理Windows的相应消息成为可能。下面是TClipSaveForm的类定义：

type

TClipSaveForm = class(TForm)

SaveDialog1: TSaveDialog;

Image1: TImage;

Panel1: TPanel;

Button1: TButton;

SpeedButton1: TSpeedButton;

SpeedButton2: TSpeedButton;

Button2: TButton;

procedure FormCreate(Sender: TObject);

procedure FormDestroy(Sender: TObject);

procedure Button1Click(Sender: TObject);

procedure Button2Click(Sender: TObject);

procedure SpeedButton1Click(Sender: TObject);

procedure SpeedButton2Click(Sender: TObject);

private

{ Private declarations }

MyBitmap: TBitmap; { 保存截获的位图 }

View: Boolean; { 判断是否显示 }

NextViewerHandle: HWND; { 下一剪贴板观察器的句柄 }

procedure WMDrawClipboard(var Msg:TWMDrawClipboard);

message WM_DRAWCLIPBOARD;

procedure WMChangeCBChain(var Msg:TWMChangeCBChain);

message WM_CHANGECBCHAIN;

{ 响应Windows的剪贴板消息 }

public

{ Public declarations }

end;

窗口创建时，把该窗口登录为剪贴板观察器，添加到剪贴板观察器链中，同时进行变量、部件和剪贴板的初始化。

procedure TClipSaveForm.FormCreate(Sender: TObject);

begin

View := False;

SpeedButton2.Down := True;

MyBitmap := TBitmap.create;

try

MyBitmap.Width := 0;

MyBitmap.Height := 0 ;

except

Application.terminate;

end;

Clipboard.Clear;

NextViewerHandle := SetClipboardViewer(Handle);

end;

窗口关闭时，退出剪贴板观察器链，并释放内存：

procedure TClipSaveForm.FormDestroy(Sender: TObject);

begin

ChangeClipboardChain(Handle,NextViewerHandle);

MyBitmap.Free;

end;

在以上两段程序中用到的两个Windows API函数SetClipboardViewer和ChangeClipboardChain分别用于登录和退出剪贴板观察器链。

程序保存位图的功能是在消息响应过程WMDrawClipboard中实现的。该过程在剪贴板内容有变化时被调用。

procedure TClipSaveForm.WMDrawClipboard(var Msg: TWMDrawClipboard);

var

FileName: String;

begin

If NextViewerHandle <> 0 then

SendMessage(NextViewerHandle,msg.Msg,0,0);

If ClipBoard.HasFormat(CF_BITMAP) then

begin

MyBitmap.Assign(Clipboard);

If SaveDialog1.Execute then

begin

FileName := SaveDialog1.FileName;

MyBitmap.SaveToFile(FileName);

end;

If View then

begin

WindowState := wsNormal;

Image1.Picture.Bitmap := MyBitmap;

end;

end;

Msg.Result := 0;

end;

程序首先判断在剪贴板观察器链中是否还存在下一个观察器。如果有，则把消息传递下去，这是剪贴板观察器程序的义务。而后判断剪贴板上内容的格式是否为位图。如是，则首先把剪贴板上内容保存到数据成员MyBitmap中，并激活一个文件保存对话框把位图保存到文件中。如果View=True，则把窗口状态(WindowState)设置为wsNormal，并把MyBitmap赋给Image部件的相应值，使用户可以对剪贴板上的位图进行观察。

消息响应过程WMChangeCBChain在剪贴板观察器链上其它观察器退出时被调用。根据被移出观察器的不同位置决定了不同的处理方法。

procedure TClipSaveForm.WMChangeCBChain(var Msg: TWMChangeCBChain);

begin

if Msg.Remove = NextViewerHandle then

NextViewerHandle := Msg.Next

else

if NextViewerHandle <> 0 then

SendMessage(NextViewerHandle,Msg.Msg,Msg.Remove,Msg.Next);

Msg.Result := 0;

end;

窗口上有两个加速按钮，两个按钮。它们击键(click)事件处理过程如下。每一程序段的意义是非常显然的。

procedure TClipSaveForm.Button1Click(Sender: TObject);

begin

Close;

end;

procedure TClipSaveForm.Button2Click(Sender: TObject);

begin

WindowState := wsMinimized;

end;

procedure TClipSaveForm.SpeedButton1Click(Sender: TObject);

begin

View := True;

Image1.Picture.Bitmap := MyBitmap;

end;

procedure TClipSaveForm.SpeedButton2Click(Sender: TObject);

begin

View := False;

Image1.Picture.Bitmap := nil;

end;

通过对这个程序的介绍，以下几点是应该注意的：

1.提供了一种自己截获和处理剪贴板上内容的方法。读者可以根据需要进一步扩充；

2.提供了响应Windows消息的方法。在第三篇有关自定义部件开发的内容中，这一问题还要详细论述；

3.最后的一点启示是：在Delphi程序开发中巧妙应用传统的Windows方法(如消息处理、 API函数等)仍是很有必要的。而在应用这些方法中所体现的方便之处，正是Delphi胜过其它可视化开发工具的一个重要方面。

7.2 Windows的DDE原理和 Dephi的DDE实现机制

7.2.1 Windows的DDE原理

Windows的DDE机制基于Windows的消息机制。两个Windows应用程序通过相互之间传递DDE消息进行DDE会话(Conversation)，从而完成数据的请求、应答、传输。这两个应用程序分别称为服务器(Server)和客户(Client)。服务器是数据的提供者，客户是数据的请求和接受者。

DDE会话由客户程序启动。客户程序把一条消息(WM_DDE_INITIATE)传播给当前运行的所有Windows程序。这条消息指明了客户程序所需要的一般数据(应用程序、主题)。拥有这些数据的DDE服务器可以响应这条被传播的消息。此时，DDE会话就开始了。

由于在每个主题中，DDE服务器可以支持一个或多个数据项，所以在客户请求数据时应同时指明应用程序名、主题名和项目名。应用程序、主题、项目是DDE中三个最基本的概念。

利用Windows本身提供的DDE消息和API进行DDE编程是一件相当棘手的问题。 虽然使用DDE管理库(ddeml.dll)可以一定程度上减轻开发者的工作负担，但开发DDE程序仍不是一件轻松的事情。

此时Delphi出现了！Delphi通过其自身巧妙的设计使开发一个DDE应用程序同开发一个普通程序一样地快捷、方便。

7.2.2 Delphi的DDE实现机制简介

Delphi把所有的DDE功能做到四个部件中，它们是:

● TDDEClientConv ： 用于客户程序建立和维护一个DDE会话

● TDDEClientItem ： 用于客户程序建立和维护数据交换通道

● TDDEServerConv ： 用于服务器程序响应DDE会话

● TDDEServerItem ： 用于服务器程序维护数据交换通道

   前两个部件用于生成一个DDE客户程序，后两个部件用于生成一个DDE服务器程序。如果一个应用程序同时拥有这些部件，则这一程序既可以充当DDE客户，也可以充当DDE服务器。

会话部件TDDEClientConv、TDDEServerConv用于建立和维护一个DDE会话。DDE会话包括DDE服务和DDE主题两部分。

DDE服务是DDE服务器的名称，即在一般的Windows DDE机制中所讲的应用程序名。一般说来这一名称是DDE服务器应用程序执行文件名去掉 .EXE后缀。比如你的应用程序要和Word 6.0建立会话，则DDE服务为WINWORD。但也不尽然。比如你的应用程序要和Borland ReportSmith ( RPTSMITH.EXE ) 建立会话，则DDE 服务为 Report Smith。DDE服务到底如何，读者可参看相关的DDE服务器应用程序文档。

DDE主题是一个包含了联接信息的数据单元。一般说来DDE 主题是一个包括扩展名的完整文件名。例如和Excel中的一个文件建立DDE会话，则主题可能是

Topic = 'c:\excel\Example\sale.xls'

如果服务器是一个Delphi应用程序，缺省情况下主题是包含欲联接数据窗体的标题。如果服务器使用了DDEServerConv部件，则要求使用部件DDEServerConv的名称作为DDE主题。

项目部件TDDEclientItem、TDDEServerItem用于建立和维护DDE数据的传输通道。 DDE项目中包含着实际欲传输的数据。DDE项目的格式取决于DDE服务器应用程序。一个可能的DDE项目例子是电子表格中的单元和数据库表中的域。如果服务器是Delphi应用程序，则项目是连接的 DDEServerItem部件的名称。

Delphi的DDE实现机制方便、实用，但也有一个令人遗憾的缺陷：只能传输文本数据以及命令、宏，而不能传输图像数据。在这一点上微软公司推出的Visual Basic 要略胜一筹。不过在目前文本数据的使用仍是最广泛的，而且图像传输可以利用剪贴板和OLE来实现，则这一缺陷也并无很大的影响

7.3 DDE客户程序的实现

DDE客户程序启动DDE会话，向服务器请求并从服务器接收数据。同时还可以向服务器发送数据、命令、宏，改变服务器的状态并控制服务器的运行。

7.3.1 联接模式(ConnectMode)

    Delphi的DDE提供了两种联接模式：自动和人工。这可以通过DDEClinetConv 部件的ConnectMode属性进行设置。如下表所示。

表 7.5 DDE的联接模式

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

          值　　　　　　　　　　　意　　　　　　义

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

      ddeAutomatic 在运行中当包含TDDEClientConv部件的窗口创建时

联接自动建立

     ddeManual 只有当调用OpenLink方法时联接才建立

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

不同联接模式，DDE客户程序的实现方式不同。

对于自动模式：

1. 向窗体中加入DDEClientConv和DDEClientItem部件并命名；

2. 把DDEClientItem部件的DdeConv属性设置为DDEClientConv部件的名称；

如果在设计时建立，则通过对象观察器进行选择；如果在运行时建立联系， 则通过如下的一条语句设置属性的值：

DDEClientItem1.DdeConv := 'DDEClientConv1' ;

3. 和服务器建立联系，实现数据共享。

对于人工模式：

1.向窗体中加入DDEClientConv部件；

2.和服务器建立联系；

3. 数据更新时调用RequestData方法申请并获得数据。

7.3.2 和DDE服务器建立联系

和DDE服务器建立联系，既可以在设计时进行，也可以在运行时进行。

在设计时，DDE联接可以通过剪贴板进行粘贴。具体步骤如下：

1. 激活服务器程序，并选中你的客户程序欲联接的数据；

2. 把数据和DDE联接信息拷贝到剪贴板上。一般说来这只需要选择服务器应用程序的 Edit|Copy 菜单；

3. 在Delphi IDE的设计窗体中选中DDEClientConv部件；

4. 在Object Inspector(对象观察器)中单击DDEService属性或DDETopic属性，然后再单击Ellipsis按钮，打开DDE Info对话框；

5.选择Paste Link按钮。此时App编辑框和Topic编辑框被自动填充。如果Paste Link按钮变灰，说明你准备用作服务器的应用程序不支持DDE或者DDE信息没有被成功地拷贝到剪贴板上；

6.选择OK 按钮。此时Object Inspector中的DDEService、DDETopic 属性包含了建立一个DDE联接的正确值。

对于人工模式以下步骤是不需要的。

7.选中DDEClientItem部件，并在Object Inspector中设置DdeConv属性为已完成联接的DDEClientConv部件名称；

8.假如剪贴板上的DDE 联接信息仍保留的话，从Object Inspector的下拉列表框中选择 DDEItem 属性的值。否则输入正确的值。

在运行时，调用 SetLink 方法来建立DDE联接。

SetLink有两个String类型的参数，分别用来接受DDEService和DDETopic的值。过程执行后DDEClientConv部件的DDEService 和DDeTopic属性被设置。要注意的是：在运行时直接设置DDEService和DDETopic的值并不能建立一个DDE联接，而必须调用SetLink 方法进行初始化。

比如，下面的语句和Excel的System主题建立联接：

DDEClietnConv. SetLink('Excel','System')；

调用SetLink方法后，还需要设置DDEClientItem部件的DDEItem属性。

比如，下面的语句联接Excel的Topics项目，用以获取当前活跃文件的文件名：

DDEClietnItem.DDEItem := 'Topics';

当DDE联接建立后，联接的数据保存在DDEClientItem部件的Text和Lines 属性中，Text用于保存一个字符串(String)，Lines用于保存一个字符串链表(TStrings)对象。

为了显示联接数据，可以在DDEClientItem的OnChange事件中把数值赋给一个可视部件。

下面的事件过程把联接数据实时地显示在一个编辑框中。

   procedure Form1.DDEClientItemChange(Sender: Tobject);

begin

Edit1.Text := DDEClientItem1.Text;

end;

运行状态下也可以从剪贴板上粘贴DDE联接信息，并调用SetLink建立DDE会话。下面的例子显示了当用户按下应用程序中的Paste Link按钮时，动态建立DDE会话的过程。

procedure Form1.OnPasteLink(Sender: Tobject)；

    var

Service, Topic, Item: String;

begin

if GetPasteLinkInfo (Service, Topic, Item) then

begin

AppName.Text := Service;

TopicName.Text := Topic;

ItemName.Text := Item;

DDEClient.SetLink (Service, Topic);

DDEClientItem.DdeConv := DDEClient;

DDEClientItem.DDEItem := ItemName.Text;

end;

end;

GetPasteLinkInfo是DDEMan 库单元中定义的一个过程。如果返回True，则DDE联接信息保存在三个参数中；如果返回False，说明剪贴板上没有正确格式的DDE联接信息。

7.3.3 数据申请

虽然自动模式快捷、方便，但仍有一些理由使用DDE的人工模式：

1.服务器程序可能不支持自动数据传输，客户必须显式申请服务器更新一个特定的项目；

2.节省通信费用。假如没有实时传输的要求，则人工模式可以大幅度降低通信的开销；

3.若客户程序只用于控制服务器的运行，则往往没有必要使用自动模式。

人工模式下客户程序的数据更新需要采用数据申请的方式。数据申请需要调用DDEClientConv部件的RequestData方法。RequestData有一个参数，指向要申请的DDE项目。RequestData返回一个Pchar类型的无结束符字符串，包含了申请到的文本。返回字符串占用的内存必须在程序终止前显式释放。

在人工模式下，即使存在一个DDEClientItem部件且与DDEClientConv相联接，数据更新后DDEClientItem部件的Text、Lines属性的值也不会改变。

7.3.4 数据发送

数据发送与一般的DDE数据流向正好相反，是把数据从DDE客户应用程序发送到DDE服务器应用程序。

数据发送使用DDEClientConv部件的两个方法PokeData 和 PokeDataLines， 它们的语法是：

    function PokeData (Item: String ; Data: PChar): Boolean;

    function PokeDataLines (Item: String ; Data: TStrings): Boolean;

参数Item是DDE服务器中被联接的项目，Data是要发送的数据。如果数据是一个字符串，则把它转化为PChar类型并调用PokeData方法；如果数据是一个字符串链表对象，可调用PokeDataLines方法。

方法的返回值标志数据传送是否成功。因为有一些DDE服务器应用程序并不接收发送的数据。

下面的语句把编辑框中的内容发送给服务器：

    StrPCopy(TheText , Edit1.text);

DDEClientConv1.PokeData(DDEClientItem1.DDEItem , TheText);

过程StrPCopy把一个Pascal类型的字符串拷贝到一个无结束符的PChar类型字符串中。


第七章 剪贴板和动态数据交换（二）

DELPHI基础教程

第七章　剪贴板和动态数据交换（二）

7.3.5 控制服务器应用程序的执行

客户程序控制服务器应用程序的一个方面是：必要的时候客户程序可以启动服务器程序，并装载会话主题。

而客户程序控制服务器应用程序更重要的一点是向服务器发送服务器承认的宏命令，来完成对服务器应用程序的各种操作。服务器到底支持哪些宏命令，可参阅服务器应用程序文档。

发送宏命令要使用DDEClientConv的两个方法 ExecuteMacro和ExecuteMacroLines ，它们的语法如下：

function ExecuteMacro(Cmd: PChar; WaitFlag: Boolean): Boolean;

function ExecuteMacroLines(Cmd: TStrings;WaitFlag: Boolean): Boolean;

Cmd是欲发送的宏命令字符串或宏命令字符串链表。WaitFlag决定了在DDE 服务器程序执行宏命令时客户程序的行为。如果WaitFlag设置为True，则在服务器宏命令执行完毕前，不允许对ExecuteMacro、ExecuteMacroLines、PokeData、PokeDataLines这些方法的成功调用，它们都不向服务器发送数据并返回False。如果WaitFlag设置为False，则调用的方法在第一个宏执行完毕前即试图向服务器发送数据。

WaitFalg的设置也取决于服务器应用程序。一些应用程序当在第一个宏执行完之前就试图向它发送数据或命令时，可能导致第一个宏执行失败或导致不可预料的后果。具体情况可查阅服务器应用程序文档。

函数返回值表示命令串是否被成功传输。而宏命令执行是否成功客户是无法检测到的。

7.3.6 格式化文本

DDEClientConv有一个布尔属性FormartChars，用于决定是否格式化文本。所谓格式化文本是指从传输来的文本数据中过滤掉BackSpace(8)、 Tab(7) 、Linefeed(10) 、Return(13)等字符。括号内是字符的ASCII码。许多时候这些字符将导致DDE客户数据显示的混乱。

    FormatChars的缺省值是False。

7.3.7 响应DDE事件

部件DDEClientConv有两个事件OnOpen和OnClose，分别在DDE 会话建立和中止时触发。部件DDEClientItem有一个OnChange事件。这一事件常用于DDE项目数据的转储和显示，如(7.3.1)节所示。

在自动模式下，OnOpen事件在包含DDEClientConv部件的窗口创建时触发，或在调用SetLink方法时触发，OnClose事件在客户程序或服务器程序关闭时触发。

在人工模式下，OnOpen事件在调用OpenLink 方法时触发，OnClose事件在调用ColseLink方法时触发。

7.3.8 利用客户程序和Excel交换数据

下面我们建立一个DDE客户程序，并利用这一程序与Excel中的一个工作表交换数据。程序设计界面

界面中包含一个DDE会话部件DDEClientConv1和DDE项目部件DDEClientItem1，用于建立和维护DDE联接；一个RadioGroup控件和其中的两个无线电按钮AutoRadio、ManualRadio，用于设置联接模式；一个GroupBox控件和其中的两个按钮RequestBtn和PokeBtn，用于控制数据的申请和发送，其中RequestBtn在自动模式下变灰；一个文本框Memo1用于保存DDE数据；一个按钮PasteBtn用于粘贴联接信息并建立DDE联接；另外一个按钮CloseBtn用于关闭系统。

设计时把DDEClientConv1的FormatChars属性置为True，这样可以保留服务器传来数据的显示格式；ConnectMode保留ddeAutomatic的缺省设置。

程序在类TForm1中定义了一个私有数据成员Automatic，用于标志联接模式；三个字符串数据成员DDEService、DDETopic、DDEItem用于记录联接信息。

窗口生成时进行变量和部件状态的初始化。

procedure TForm1.FormCreate(Sender: TObject);

begin

RequestBtn.Enabled := False;

AutoRadio.Checked := True;

Automatic := True;

end;

当联接模式改变时，程序进行相应的处理。

自动模式转换为人工模式：

procedure TForm1.ManualRadioClick(Sender: TObject);

begin

if Automatic then

begin

RequestBtn.Enabled := ManualRadio.Checked;

DDEClientConv1.ConnectMode := ddeManual;

Automatic := False;

end;

end;

人工模式转换为自动模式：

procedure TForm1.AutoRadioClick(Sender: TObject);

begin

if not Automatic then

begin

RequestBtn.Enabled := ManualRadio.Checked;

If (DDEService = '') or (DDETopic = '') then

begin

MessageDlg(' Can not Set Link.',mtWarning,[mbOK],0);

Exit;

end;

DDEClientConv1.SetLink (DDEService, DDETopic);

DDEClientItem1.DdeConv := DDEClientConv1;

DDEClientItem1.DDEItem := DDEItem;

DDEClientConv1.ConnectMode := ddeAutomatic;

Automatic := True;

end;

end;

当从自动模式转换到人工模式，只需要简单修改相应属性即可；而从人工模式转换到自动模式，则需要调用SetLink重新建立联接，否则往往会引发一个DDE异常。

联接的建立采用从剪贴板粘贴联接信息的方式，这是最具有灵活性的一种方法。

procedure TForm1.PasteBtnClick(Sender: TObject);

begin

if GetPasteLinkInfo (DDEService, DDETopic, DDEItem) then

begin

DDEClientConv1.SetLink (DDEService, DDETopic);

if Automatic then

begin

DDEClientItem1.DdeConv := DDEClientConv1;

DDEClientItem1.DDEItem := DDEItem;

end;

end;

end;

GetPasteInfo是 DDEMan库单元中定义的一个函数，用于检测剪贴板上是否有联接信息并返回相应的DDE服务、主题和项目。

对于人工模式，必须由客户显式向服务器申请数据。在这种模式下DDE项目部件是多余的，接收到的DDE联接信息用一个字符串来记录。下面是实现代码。

procedure TForm1.RequestBtnClick(Sender: TObject);

var

TheData: PChar;

begin

If DDEItem = '' then

begin

MessageDlg('Can not Request Data',mtWarning,[mbOK],0);

Exit;

end;

TheData := StrAlloc(79);

DDEClientConv1.OpenLink;

TheData := DDEClientConv1.RequestData(DDEItem);

DDEClientConv1.CloseLink;

if TheData <> nil then

Memo1.Text := StrPas(TheData);

StrDisPose(TheData);

end;

OpenLink、CloseLink方法用于打开和关闭联接。RequestData方法向服务器申请数据并返回到一个PChar字符串中。字符串必须显式分配内存并在退出时释放。

数据发送在不同联接模式下是不同的。对于人工模式，增加了联接的打开和关闭操作。程序清单如下。

procedure TForm1.PokeBtnClick(Sender: TObject);

begin

If DDEItem = '' then

begin

MessageDlg('Can not Poke Data.',mtWarning,[mbOK],0);

Exit;

end;

if Automatic then

DDEClientConv1.PokeDataLines(DDEItem,Memo1.Lines)

else

begin

DDEClientConv1.OpenLink;

DDEClientConv1.PokeDataLines(DDEItem,Memo1.Lines);

DDEClientConv1.CloseLink;

end;

end;

打开Microsoft Office中的Excel，装入一个文件，把相关的单元选中，拷贝到剪贴板上。而后运行程序，按下Paste Link按钮，DDE联接就建立起来，相关单元中的数据显示在Memo1中。之后可以进行模式转换、数据申请、申请发送等一系列工作。运行后的屏幕显示如下图所示。

7.3.9 用客户程序控制程序管理器

下面的例子用客户程序向程序管理器发送命令，用于创建程序组、程序项以及删除程序组。

程序管理器提供了应用程序的DDE接口命令字符串，应用程序利用这些命令字符串可以实现以下的功能：

1.创建程序组

命令格式为：

CreateGroup(程序组名[，程序组所在的路径])

程序组不存在时进行创建；如程序组存在则按照指定的路径激活。

2.删除程序组

命令格式为：

DeleteGroup(程序组名)

3.显示程序组

命令格式为；

ShowGroup(程序组名，显示标志)

显示标志用于控制程序组在程序管理器中以极大、极小或正常方式显示。

4.重新装入程序组

命令格式为：

ReLoadGroup(程序组名)

该命令使程序管理器先删除而后再重新装入一个已有的程序组。

5.向程序组中添加程序项

命令格式为：

AddItem(命令行[，描述[，图标路径[，图标序号[，图标横坐标，图标纵坐标[，工作区目录[，热键[，是否最小化显示标志]]]]]]])

命令行控制程序项的执行，可包括路径、参数等。其它参数分别对应在程序管理器中添加一个程序项时需要设置的参数和选项。它们都有缺省设置，因而是可选的。

6.替换程序组中的程序项

命令格式为：

ReplaceItem(程序项名)

该命令删除一个程序项，并将所删除程序项的位置记录下来，以后通过AddItem在这个所记录的位置增加新项目。

7.从程序组中删除程序项

命令格式为：

DeleteItem(程序项名)

从当前活动程序组中删除一个程序项。

8.关闭程序管理器

命令格式为：

ExitProgram(是否保存程序组信息标志)

从应用程序向程序管理器发送命令字符串的方法是基本一致的。为简便起见，在例程中只实现了其中仅包含一个字符串参数的情形，读者可以很容易作进一步的扩展。

程序设计界面如图所示，包含一个DDE客户会话(DDEClientConv)部件和四个完成不同功能的按钮。

DDEClientConv在设计时和程序管理器建立一个DDE会话，其中DDE服务器和DDE主题 都为PROGMAN。联接模式ConnectMode设置为ddeManual。

我们把只有一个字符串参数的命令发送情况抽象出来，形成下面的SendMacro函数。

function TForm1.SendMacro(Name: String;Command: String): Boolean;

var

Macro: String;

Cmd: array[0..255] of Char;

begin

Result := True;

if Name <> '' then

begin

Macro := Format('['+Command+'(%s)]', [Name]) + #13#10;

StrPCopy (Cmd, Macro);

DDEClient.OpenLink;

if not DDEClient.ExecuteMacro(Cmd, False) then

Result := False;

DDEClient.CloseLink;

end;

end;

过程首先利用Format函数形成宏字符串：

Macro := Format('['+Command+'(%s)]', [Name]) + #13#10;

而后把Pascal类型的字符串拷贝到一个程序管理器可接受的PChar类型字符串中。

DDE联接采用人工模式。首先调用OpenLink方法。而后调用ExecuteMacro方法发送命令，如失败则返回False。最后用CloseLink关闭联接。

三个按钮CreateButton、AddButton、DeleteButton分别用于创建程序组、添加程序项、删除程序组。它们的程序实现大同小异，如下所示。

创建程序组：

procedure TForm1.CreateButtonClick(Sender: TObject);

var

Name: String;

begin

Name := InputBox('Input Box','Input Group Name','');

if Name = '' then

MessageDlg('Group name can not be blank.', mtError, [mbOK], 0)

else

if SendMacro(Name,'CreateGroup') = False then

MessageDlg('Unable to create group.', mtInformation, [mbOK], 0);

end;

添加程序项：

procedure TForm1.AddButtonClick(Sender: TObject);

var

Name: String;

begin

Name := InputBox('Input Box','Input Application full_Path name','');

if Name = '' then

MessageDlg('Application name can not be blank.', mtError, [mbOK], 0)

else

if SendMacro(Name,'AddItem') = False then

MessageDlg('Unable to Add Item.', mtInformation, [mbOK], 0);

end;

删除程序组：

procedure TForm1.DeleteButtonClick(Sender: TObject);

var

Name: String;

begin

Name := InputBox('Input Box','Input Group Name to be Deleted','');

if Name = '' then

MessageDlg('Group name can not be blank.', mtError, [mbOK], 0)

else

if SendMacro(Name,'DeleteGroup') = False then

MessageDlg('Unable to create group.', mtInformation, [mbOK], 0);

end;

7.4 DDE服务器程序的实现

   DDE服务器程序响应DDE客户的请求，一般地它包含了客户程序希望获取的数据。

创建一个DDE服务器程序，必须要把一个DDEServerItem部件添加到窗体中。DDEServerItem的text或Lines属性包含了要联接的数据。一般地 DDEServerItem部件又和另一个文本控件相联系。当文本控件中的内容变化时则更新DDEServerItem 的text或Lines属性的值。下面的一段程序把DDEServerItem和一个列表框相联系。这一联系是在列表框的OnChange事件中实现。

procedure Form1.OnListBoxChange(Sender: TObject);

begin

DDEServerItem1.Lines := ListBox1.Items;

end;

创建DDE服务器程序时也可以再加入一个DDEServerConv部件，并把两个部件利用DDEServerItem的ServerConv属性联系起来。此时DDE主题成为部件DDEServerConv的名称，而不是拥有DDEServerItem部件窗体的标题。

在下列情况下使用DDEServerConv部件成为必要：

1.拥有DDEServerItem 部件窗体的标题可能在运行时改变或可能有其它窗体拥有同样的标题。在这种情况下DDE联接可能无法建立；

2.DDE客户程序可能会向你的服务器程序发送一条宏命令。在这种情况下只有拥有一个DDEServerConv部件才能响应OnMacroExecute事件并执行相应的动作。

7.4.1 和DDE客户程序建立联接

一般说来，建立DDE联接是客户程序的任务。但服务器程序可以把一个联接拷贝到剪贴板上供客户程序粘贴并建立DDE会话。步骤如下：

1.调用DDEServerItem部件的CopyToClipboard方法， 把Text(或Lines)属性的值和DDE联接信息拷贝到剪贴板上；

2.DDE客户程序插入联接的数据。一般地这是通过选择适当的命令(如Edit|Paste Special或Edit|Paste Link)来实现的。

7.4.2 响应DDE事件

部件DDEServerConv有三个事件：OnOpen、OnClose、OnExecuteMacro。前两个事件在DDE会话建立和终止时触发。同(7.3.7)中的介绍。

OnExecuteMacro事件用于响应客户程序发送过来的宏指令。OnExecuteMacro事件处理过程有一个Msg参数，保存发送过来的指令串。用户可以在该过程中决定如何响应这些宏指令。

DDEServerItem部件只有一个事件OnPokeData。这一事件用于响应客户程序发送来的数据。如果客户程序是Delphi程序，则客户程序调用了PokeData或PokeDataLines方法。在这一事件的处理过程中用户可以把发送来的数据保存到一个合适的地方。一般说来这应该就是DDEServerItem所联系的文本控件。

下面的程序把发送来的数据保存到ListBox中。

procedure Form1.OnDDEServerItemPokeData(Serder: TObject)；

begin

ListBox1.Items := DDEServerItem1.Lines;

end;

7.4.3 DDE服务器应用例程

下面我们创建一个DDE服务器例程和一个相应的DDE客户例程。

DDE服务器例程可以完成的工作有：

1.把DDE联接信息拷贝到剪贴板上供其它程序使用；

2.利用一个TMemo部件为其它程序提供数据源；

3.接收客户程序发送来的数据；

4.根据客户程序发送来的宏指令改变自身的运行状态。

其中各部件的关键属性如下：

DDESrvrForm.ActiveControl = Memo1

DDESrvrForm.Menu = MainMenu1

Bevel1.Align = alTop

Memo1.Align = alClient

DDETestItem.ServerConv = DDETestTopic

通过设置Bevel1、Memo1的Align属性，可以保证窗口大小变化时仍能有较为美观的屏幕显示。

Memo1是服务器的数据源，DDE项目部件DDETestItem通过Memo1的OnChange事件与Memo1 建立联系。

procedure TDdeSrvrForm.doOnChange(Sender: TObject);

begin

if not FInPoke then

DDETestItem.Lines := Memo1.Lines;

end;

其中FInPoke是一个布尔类型的私有数据成员，用于标志程序是否在处理客户程序的数据发送。当数据是由客户发送过来转存到数据源时，则没有必要再把数据传给DDE项目部件。

把联接信息拷贝到剪贴板，只需简单调用DDETestItem的CopyToClipboard方法。

procedure TDDESrvrForm.CopyClick(Sender: TObject);

begin

DDETestItem.CopyToClipboard;

end;

这是通过菜单项Edit|Copy来调用的。

接收客户程序发送来的数据，是在DDETestItem的OnPokeData事件处理过程中。在接收过程中改变FInPoke的值，以阻止数据的无效反送。

procedure TDDESrvrForm.doOnPoke(Sender: TObject);

begin

FInPoke := True;

Memo1.Lines := DDETestItem.Lines;

FInPoke := False;

end;

在DDE会话部件DDETestTopic的OnExecuteMacro事件处理过程中处理客户发送来的宏指令。我们共定义了五种可以响应的宏指令：CopyDDE、Clear、WS_Normal、WS_MINIMIZED、WS_MAXIMIZED，分别用于拷贝联接信息、清除Memo1中的内容以及改变窗口显示状态。

procedure TDdeSrvrForm.doMacro(Sender: TObject;Msg: TStrings);

var

Cmd: String;

i: Integer;

begin

Cmd := '';

if Msg.Count = 0 then Exit;

for I := 0 to Msg.Count-1 do

begin

Cmd := Msg.Strings[i];

if UpperCase(Cmd) = 'COPYDDE' then

DDETestItem.CopyToClipboard

else if UpperCase(Cmd) = 'CLEAR' then

Memo1.text: = ''

else if UpperCase(Cmd) = 'WS_NORMAL' then

WindowState := wsNormal

else if UpperCase(Cmd) = 'WS_MINIMIZED' then

WindowState := wsMinimized

else if UpperCase(Cmd) = 'WS_MAXIMIZED' then

WindowState := wsMaximized

else

MessageDlg('Invalid Command',mtWarning,[mbOK],0);

end;

end;

下面的DDE客户程序，主要是为了验证上面的DDE服务器程序而设计的，但同时也提供了一个DDE客户程序设计的完整实例。

程序把接收到的DDE数据保存在一个TMemo类部件DDEDat中，而欲发送给服务器的数据和宏指令在另一个TMemo类部件PokeDat中输入。两个按钮PokeBtn、ExecuteBtn用于发送数据和宏指令。两个菜单项File|New Link和Edit|Paste Link用于根据用户的输入建立新联接和从剪贴板上粘贴DDE联接。

DDE联接的建立通过调用SetLink方法实现。

建立新联接的实现代码如下。

procedure TFormD.doNewLink(Sender: TObject);

begin

DDEClient.SetLink (AppName.Text, TopicName.Text);

DDEClientItem.DdeConv := DDEClient;

DDEClientItem.DDEItem := ItemName.Text;

end;

通过从剪贴板粘贴联接信息来建立联接的实现代码如下。

procedure TFormD.Edit1Click(Sender: TObject);

var

Service, Topic, Item : String;

begin

PasteLink1.Enabled := GetPasteLinkInfo (Service, Topic, Item);

end;

procedure TFormD.doPasteLink(Sender: TObject);

var

Service, Topic, Item : String;

begin

if GetPasteLinkInfo (Service, Topic, Item) then

begin

AppName.Text := Service;

TopicName.Text := Topic;

ItemName.Text := Item;

DDEClient.SetLink (Service, Topic);

DDEClientItem.DdeConv := DDEClient;

DDEClientItem.DdeItem := ItemName.Text;

end;

end;

在DDEClientItem的OnChange事件处理过程中把接收到的事件保存在DDEDat中。

procedure TFormD.DDEClientItemChange(Sender: TObject);

begin

DDEDat.Lines := DDEClientItem.Lines;

end;

数据发送的实现代码如下。

procedure TFormD.doPoke (Sender: TObject);

var

DDECli : TDDEClientConv;

begin

DDECli := DDEClientItem.DdeConv;

if DdeCli <> nil then

DDECli.PokeDataLines (DDEClientItem.DDEItem, PokeDat.Lines);

end;

宏指令发送的实现代码如下。

procedure TFormD.doMacro (Sender: TObject);

var

DDECli: TDDEClientConv;

Cmd: String;

begin

DDECli := DDEClientItem.DdeConv;

if DDECli <> nil then

begin

Cmd := PokeDat.Text + #13#10;

DDECli.ExecuteMacroLines (PokeDat.Lines, True);

end;

end;

运行以上两个程序，建立DDE联接。经测试，数据传输、数据发送和宏指令的发送与执行都达到预期要求。

7.4.4 小结

剪贴板和DDE是Windows下数据通信的两种方法。Delphi以简便、友好的方式实现了相应的功能，为用户编程提供了方便。一般说来，剪贴板多用于静态数据传输，而DDE用于动态数据交换、控制其它程序运行等场合。这些内容对于多用户环境下的程序开发是很重要的


第八章 对象链接与嵌入（一）

DELPHI基础教程

第八章　对象链接与嵌入（一）

    对象链接和嵌入(Object Linking and Embeding)是一组服务功能，它提供了一种用源于不同应用程序的信息创建复合文档的强有力方法。 对象可以是几乎所有的信息类型，如文字、位图、矢量图形，甚至于声音注解和录像剪辑等。

    Windows附件组中的书写器是应用OLE的实例，使用单击“对象 | 插入”菜单项， 书写器弹出插入对话框，对话框中列出了多个OLE服务器程序，如公式编辑工具，绘图工具，报表生成工具。用户双击鼠标左键，可激活一个OLE服务器。在OLE服务器中可编辑OLE对象，当用户返回到书写器中时，在书写器文档中将出现OLE对象。

    Delphi支持OLE技术，Delphi1.0可以创建OLE应用程序，Delphi2.0可创建OLE自动化服务器和控制器程序。本章通过例程介绍对象链接与嵌入的基本概念，Delphi创建OLE对象的方法，OLE自动化的概念以及如何开发OLE自动化服务器和控制器。

8.1 OLE简介

8.1.1 OLE1.0和OLE2.0

迄今为止，有两种版本的OLE：OLE1.0和OLE2.0。当用户在OLE1.0 服务器中激活OLE对象，服务器程序在前台打开自己的窗体，并获得焦点。OLE窗体失去焦点，存在于单独的窗体之中。

    OLE2.0服务器采用“本地”(in place)激活方式。本地激活意味着服务器菜单与应用程序菜单要进行融合，服务器的状态条更换应用程序状态条，服务器的工具条更换应用程序工具条。OLE对象在应用程序窗体中进行编辑，但所有过程均由服务器处理。

    创建OLE对象的服务器决定了OLE的激活方式。如果一个OLE1.0的对象在OLE2.0 编译的应用程序中打开，它将采用OLE1.0的方式。

8.1.2 链接与嵌入

    链接对象的数据保存在OLE服务器创建的文件中，嵌入对象的数据保存在OLE应用程序中。

    链接对象必须以文件形式保存，只有对OLE服务器已经创建好的OLE对象， 才能进行OLE链接，链接的OLE对象文件可被OLE应用程序或其它程序进行修改，OLE 服务器和其它OLE应用程序也可以访问和修改OLE对象。对象数据保存在某一处，但可以被多个应用程序访问。

    Delphi应用程序可以得到OLE对象文件中的最新数据。当OLE 对象数据被应用程序修改时，这些变化将在所有包含该对象的其它应用程序中体现。

    嵌入对象保存在OLE应用程序中，其它应用程序不能访问该对象。只有在OLE应用程序中激活OLE对象才能对其进行编辑。嵌入的OLE对象不需要保存在文件中，所有数据都在应用程序中，这就确保了OLE数据不会被偶然地删除或修改。不足之处是应用程序的规模因为保存了OLE数据而增大了 。

    如果用户想保存对嵌入对象的修改，可以把OLE数据存入文件中，本章第3 节将详细讨论这个问题。

     表8.1 使用链接或嵌入的原则。

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

     何时使用链接　　　　　　　　　何时使用嵌入

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

    想要对源对象进行修改及将　　　对源对象进行修改，并将这

    这些修改反映到其他与源对　　　些修改反映在一个特定的应

    象链接的应用程序或文本中　　　用程序或文本中

    源对象可能被多个OLE应 　　 源对象不可能被一个OLE应

    用程序应用程序频繁修改　　　　用程序频繁修改

    源对象的文件不会被频繁移　　　源对象的文件可能被频繁移

    动，且不会被删除　　　　 动，且不会被删除

    对象很大，一般通过网络或　　　对象很小，或对象很大却无法

    电子邮件进行分配　　　　　　　通过网络或电子邮件进行分配

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

8.1.3 设计状态OLE对象的创建

    在Delphi中，可分别在设计状态或运行状态创建OLE对象，表8.2说明了两种状态创建对象的差别。

表8.2 设计、运行状态OLE对象的创建

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

    设计状态OLE对象的创建　　　　　　　　 运行状态OLE对象的创建

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

对象保存在运行文件中，增加了所需 对象保存在一个文件中或只在运行时

编译的程序的规模　　　　　　 才有，减小了编译程序的规模

开发者需在设计时访问OLE服务器　　　开发者不需要在设计时访问OLE服务器

运行时OLE对象已经创建，减小了 　 运行时OLE对象已经创建，增加了运行

运行时间　　　　　　　　　　　　　　 时间

OLE对象在设计运行时间可行性编辑　　 OLE对象只能在运行时编辑

应用程序的OLE对象数目在设计时已　　 应用程序可以在运行时创建新的OLE对

经确立　　　 　　　　　　　　　　　　象

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

    在设计状态，OLE服务器不能被本地激活，只能在自己的窗体内激活。但在运行状态，

只要OLE服务器支持本地激活，就可以使用这种方式。

8.1.4 OLE类、文件、项目

    OLE类决定创建OLE对象的服务器。有些应用程序需要创建多种类型的OLE对象，例如应用程序同时链接或嵌入公式、图片等。OLE类也决定OLE对象所包含的数据类型，链接或嵌入对象均要定义OLE类。

    OLE文件是包含OLE对象数据的源文件。链接对象必须使用对象文件，因为链接对象在文件中保存。如果应用程序从已存在的源文件中创建嵌入对象，也要使用OLE文件。例如，如果链接到QuattiPro笔记本的OLE对象TUTOR.WBI存储在D:\DFFICE\QPW目录下，则OLE文件就是D:\DFFICE\QPW\TUTOR.WBI。值得注意的是OLE文件只能为链接对象所定义，而对于嵌入对象，只需定义OLE类。

    OLE项目是代表链接或嵌入数据的OLE文件中的一部分。当应用程序希望OLE对象包含比OLE文件小的数据块时，则必须使用OLE项目。

    例如，在QuattiPro笔记本中，OLE对象链接了GasCosts的B4 到B5 范围的网格，OLE项目是$GasCosts;$B$4.$B$5。

8.2 设计状态OLE对象的创建

    Dephi可以在设计状态和运行状态中创建OLE对象。本节介绍设计状态OLE对象的创建。

8.2.1 TOLEContainer部件

    要创建OLE对象，需在窗体中加入OLE包容器部件。 应用程序部件包含链接或嵌入的对象。用该部件可显示在OLE服务器编辑的数据。部件的ObjClass,ObjDoc,ObjItem 属性分别定义OLE类、文件、项目。要定义OLE对象是否本地激活，使用InPlaceActive 属性。如果OLE对象可以本地激活，OLE服务器菜单将与OLE应用程序的菜单进行融合，GroupIndex属性的值将决定菜单融合情况。

8.2.2 OLE对象创建的步骤：

    1.在窗体中增加OLE包容器部件；

    2.在Object inspector中单击ObjClass或ObjDoc属性的省略按钮，将出现插入对象对话框；

3.如果要插入的OLE 对象已存储在文件中，选择“Creat From File”，而后定义该对象的文件名和路径名。如果是链接对象，则选择链接检查框。 如果是嵌入对象，选择“Creat new”，并在对象类型列表框中选择OLE对象；

    4.选择OK按钮；

    如果是创建新对象，OLE服务器将激活，则可对OLE对象进行编辑，完成编辑后关闭OLE服务器。典型的例子是单击服务器中的“File”或“File|Update”菜单。

    5.此时ObjClass属性中包含了相应的值，如果OLE对象从已存在的文件中创建或插入一

个链接对象，ObjDoc属性包含了OLE文件。

    在设计对象状态时也可以粘贴OLE对象，其步骤如下：

    1.激活服务器应用程序，选择OLE包容器部件；

    2.在服务器中，将数据或对象拷贝到剪切板；

    3.进入Delphi集成开发环境，选择OLE包容器部件；

    4.在 Object inspector窗体中选择ObjItem属性的省略(…)按钮；

    5.在列表中选择OLE对象；

    6.选择“Paste"创建一个嵌入对象或选择"Pastelink"创建链接对象；

    7.选择OK。

OLE包容器部件在此时初始化。如果粘贴一个嵌入对象，ObjClass属性将包含适当的值。如果粘贴一链接对象，ObjClass,ObjDoc,ObjItem属性将全部定义。OLE 应用程序部件包含代表OLE对象的图片。

    如果OLE服务器程序支持OLE对象的拖放功能，则在设计状态从服务器中拖动对象至应用程序，应用程序将创建链接对象，具体步骤：

    1.激活服务器，并Delphi集成开放环境中选择要链接的对象；

    2.按隹鼠标左键拖动OLE对象至设计状态的窗体；

    3.松开鼠键释放OLE对象。

    窗体将创建OLE应用程序并进行初始化。

8.3 OLE应用程序的开发

    Delphi可以在设计状态和运行状态创建OLE对象，上一节介绍的是在设计状态如何创建OLE对象，这一节将通过例程介绍如何在运行状态创建OLE对象、粘贴对象、拖动对象，以及OLE 对象的文件操作。我们开发的 OLE.dpr是一个OLE应用程序的实例

8.3.1 OLE应用程序界面开发

    OLE.dpr采用了多文档界面，父窗体有菜单，工具条，状态条，子窗体有一个OLE包容器部件，下面分别加以介绍。

8.3.1.1 OLE应用程序的菜单

    OLE应用程序的菜单与其它应用程序的主菜单大体一致，如果应用程序中有支持本地激活的OLE 2.0对象，则要进行菜单融合。查阅OLE 服务器的资料可知道服务器是否支持本地激活。

    OLE应用程序菜单的GroupIndex属性决定融合菜单的位置，即融合菜单是更换主菜单，还是插入至应用程序的主菜单中。

    OLE服务器，将融合三组菜单:Edit,View,Help，每组菜单分配了唯一的组索引值。在OLE应用程序中任何索引值为1,3,5的菜单组在菜单融合时被OLE服务器中具有相应索引值的菜 单更换。在本例程中，编辑菜单项在菜单融合时被服务器的"Edit"替换。如图8.3。 要想保存应用程序中的菜单，分配有异于1,3,5的索引值。

表8.3 融合后的菜单

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

菜单 　索引值　　　　功能描述　　　　　　　来源(OLE激活时)

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

文件　　 0 　　使用文件和退出程序　　　　　 OLE应用程序

Edit 1 编辑OLE对象　　　　　　　　 OLE服务器

对象　　 2 操作未激活的OLE对象　　　　 OLE应用程序

View 3 修改OLE对象的观测方式　　　 OLE服务器

窗体　 4 操纵窗体　　　　　　　　　　 OLE应用程序

Help 5 访问服务器在线帮助　　　　 　OLE服务器

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

8.3.1.2 OLE工具条和状态条

    当OLE对象被本地激活时，OLE服务器将试图用自己的工具条和状态条替换OLE应用程序的。如果应用程序想要本地激活， 就应该在应用程序中编写相应的代码让服务器使用工具条和状态条。要做到这点，必须：

    ● 设置工具条和状态条

    ● 在应用程序中加入状态条

      通过修改面板部件的属性创建工具条和状态条。

    当OLE对象被本地激活时，面板或其他对齐控制将与OLE服务器程序进行协调。 这意味

OLE服务器可以替换OLE应用程序窗体中任何对齐控制，但锁定的控制不能被替换。例如，

如果面板的align属性是alTop,alleft,alBottom,alIngh时，控制未锁定，OLE服务器可以替换。要使应用程序的工具条、状态条不被替换，可将locked属性设置成真值。

    当OLE 对象被激活，OLE 服务器在状态条中显示有关信息时，OLE 应用程序部件的OnStatusLineEvent事件发生，一个文本字符会将从OLE服务器传至该事件句柄。 OnStatusLineEvent事件句柄的MSG参数接受文本字符。

以下代码用以状态条接收OLE服务器的信息：

procedure TOLEObjectForm.OleContainerStatusLineEvent(Sender: TObject;

Msg: String);

begin

OLEFrameForm.StatusBarPanel.Caption := Msg

end;

8.3.2 插入OLE对象

运行状态时进行对象链接与插入也要用到插入对话框，Delphi中没有插入对话框部件，但可调用InsertOLEObjectDlg 函数来显示对话框。

8.3.2.1 InsertOLEObjectDlg函数声明如下：

function InsertOleObjectDlg(Form: TForm; HelpContext: THelpContext;

var PInitInfo: Pointer): Boolean;

    其中参数Form是拥有插入对话框的窗体，一般将拥有OLE包容器部件的窗体名字传给Form.

参数Helplontext为插入对象对话框定义在线帮助，如果应用程序没有在线帮助， HelpContext的值为零，对话框中将不出现帮助按钮。

    参数PInitInfo是一个无类型指针，该指针指向一个包含初始化OLE 部件信息的内部数据结构。InsertOLEObjectDlg修改这个指针以指向一个有效的数据结构，该结构包含了对话框列表中被选择的OLE 对象初始化信息。当该指针被使用后，应调用ReleaseOLEInitInfo过程释放初始化信息所占用的内存。

    当用户选择OK 按钮关闭插入对象对话框，InsertOLEObjectDlg 返回真值，并把 PInitInfo指向包含OLE对象的初始化信息的数据结构。

8.3.2.2 初始化OLE包容器部件

    为了使OLE包容器部件包含OLE对象，必须对部件进行初始化。 初始化主要是定义部件的OLE类。如果定义了OLE文件和OLE项目，初始化完成后，OLE 应用程序部件将包含OLE对象。

    调用InsertOLEObjetDlg函数可在其参数PInitInfo获得关于OLE对象初始化的信息时，把它传递给OLE包容器部件的PInitInfo属性，OLE包容部件的ObjClass,ObjDoc,ObjItem属性将被自动定义。

    初始化完成后，OLE对象被击活。OLE服务器将获得控制，用户可通过OLE服务器对OLE对象进行编辑。当程序冻结OLE对象，OLE包容器部件将包含一幅图像或位图代表OLE对象。定义OLE包容器部件的AutoActive属性可重新激活OLE对象，缺省情况下，双击OLE包容器部件可击活OLE对象。

    例程中初始OLE对象的代码如下：

procedure TOLEObjectForm.InitializeOLEObject(Info: Pointer);

begin

OLEContainer.PInitInfo := Info;

ReleaseOLEInitInfo(Info)

end;

    该过程先将初始化指针传给OLE包容器部件的PInitInfo属性，而后释放其内存空间。

    当用户单击例程中的“编辑 | 插入”菜单项，将弹出插入对象对话框，选择对象类型后， OLE对象被激活，该过程的代码如下：

   procedure TOLEObjectForm.InsertObject1Click(Sender: TObject);

var

Info: Pointer;

begin

if InsertOLEObjectDlg(OLEFrameForm, 0, Info) then

InitializeOLEObject(Info);

end;

8.3.3 冻结OLE对象

    如果OLE对象是OLE 1.0服务器创建，对象将在OLE服务器中被击活，焦点和控制移到OLE服务器中。要冻结一个由OLE 1.0创建的对象选择"File | Exit"菜单项。

    如果OLE 2.0服务器支持本地激活，激活OLE对象后OLE服务器将进行菜单融合，并转换工具条和状态条。要冻结对象，只需在应用程序窗体中异于OLE包容器部件的任何地方单击鼠

标键即可。

    另一种冻结对象的方法是把OLE包容器部件的Active属性设置成假值。在例程中，“对象|冻结”菜单项实现冻结功能。代码如下：

   procedure TOLEObjectForm.Deactivate1Click(Sender: TObject);

begin

OLEContainer.Active := False

end;

8.3.4 粘贴OLE对象

    一些OLE服务器允许用户把OLE对象复制到剪贴板，如果一个OLE对象复制到剪贴板上，OLE应用程序可通过初始化OLE包容器部件来粘贴OLE对象。

8.3.4.1 粘贴对话框

把OLE对象粘贴到OLE包容器部件，要使用粘贴对话框，Delphi 中没有粘贴对话框部件，但可用PasteSpecialDlg函数显示粘贴对话框。

    PasteSpecialDlg 函数声明如下：

     function PasteSpecialDlg(Form :TForm;Const First:arrang; HelpConcert: THelpCOntext;var Forrmat : Word; var Hardle : THanlle var PInitInfo ：Point ) : Boolean;

PasteSpecialDlg参数定义如下：

    参数Form是拥有粘贴对话框的窗体，应把包含OLE包容器部件的窗体名字传递给Form。

参数Format是注册对象格式的数组，每组格式是BOLEFormat类型的数组成员。例如应用程序可注册两种对象格式。为嵌入对象注册FEmbedClipFmt ，为链接对象注册FlinkClipFmt。

BOLEFormat 声明如下：

    BOLEFormat： Record

fmtID : Word;

fmtName : array[0..31] of char;

fmtResultName : array[0..31] of char;

fmtMediun : BOleMedium;

fmIsLInkble : Bool;

end;

fmtID是对象的剪贴板格式ID号，fmtID 可以是标准的剪贴板格式：CF_TEXT,CF_BIFMAP。使用OLE 对象时， 需注册新的剪贴板格式来处理OLE 对象。Windows的API中 的RegisterClipbordFormat函数注册格式。

fmtName表示是对象的名字，用以定义出现在粘贴对话框中列表框 内的对象名称。在例程中，把“％S”匹配给fmtName，OLE服务器自动地把格式化的名字代替“％S”参数。例如，如果OLE服务器是画笔，在程序运行时“Paintbrush Picture Object”将代替“％S”。

    fmtResultName，定义出现在粘贴对话框中结果检查框内的名字。在例程中， 把“％S”传给了fmtResultName。OLE服务器自动地把格式结果名称代替“％S”参数。例如，如果OLE服务器是画笔，程序运行时“Paintbrush Picture”将代替“％S”。

    fmtMedium是BOLEMedium类型，是Windows决定对象格式的数据类型。例如，OLE 联

接对象的格式是BOLE_MED_STREAM。OLE嵌入对象的格式是BOLE_MED_STORAGE。BOLEMedium函数可计算出需要的BOLEMedium类型。

    fmtIsLinkale决定对象格式是否可联连。联连对象的fmtIsLinkable为真值。嵌入对象的fmtIsLinkable为假值。

    参数HelpContext 为粘贴对话框定义在线帮助。如果应用程序没有在线帮助，HelpContext的值为零，对话框中将不出现帮助按钮。

    参数Form用以定义剪粘板上的格式，是由PasteSpecialDlg函数进行修改。因为使用粘贴对话框时，应用程序并不知道剪贴板的格式。因而用Format来处理剪贴板的数据。在本章例程中。 PasteSpecialDlg 函数把format 变量修改成FEmbedClipFmt 或FLinkClipFmt格式，这两种格式是在主窗体的OnCreate事件中定义的。如果剪贴板上的数据不是OLE对象，Format将被修改成其它类型的格式，如CF_TEXT等。

    参数Landle定义剪贴板上的数据句柄。由PasteSpecialDlg函数进行修改。 当剪贴板的数据类型不是OLE对象时，需用Handle参数访问剪贴板数据。Handle是句柄类型。

    参数PInitInfo是一个指向OLE对象初始化结构的指针。前面在讲述初始化OLE应用程序部件时也用到了这种指针。PasteSpecialDlg函数将修改PInitInfo指针以使其指向一个有效的数据结构。该结构包括了粘贴对话框中被选中的OLE对象的初始化信息。

    下面介绍粘贴对话框中的部件。

    ● 将剪贴板上的数据插入OLE应用程序，以实现对象嵌入，须选择"Paste"；

    ● 在OLE服务器资源文件与OLE应用程序之间建立联连，以实现对象联连，须选择: "Paste Line；

    ● 要将闻连与嵌入的对象显示成图标，选择"Display As Icon"。若这个检查框被选中，改变图标("Chang Icon")按钮将显示通过这个按钮可改变OLE对象的缺省图标或标签。

    ● 如果数据不是注册的格式，"Paste","Paste link"选择键将变灰。 用户无法从剪贴板上粘贴数据。在本章例程中，剪贴板上的数据只能是FEmbedClipFmt(嵌入对象) 和FlinkClipFmt(链接对象)。

    ● 用户在列表框中选择数据类型。有时数据被解释成多种类型。例如在包含OLE服务器功能的字处理器中把文本复制到剪贴板中。应用程序可以以文本和OLE对象两种方式粘贴对象。列表框中出现的选择项由OLE服务器决定。

    用户在粘贴对话框中选择OK按钮，PasteSpecialDlg返回真值，关于OLE 应用程序的初始化信息贮存在PInitInfo所指向的结构中。

8.3.4.2 在剪贴板中使用OLE对象

    要把OLE对象粘贴到OLE应用程序中，必须用Windows的 RegisterClipboardFormat函数为链连对象、嵌入对象注册两种新的剪贴板格式。这些格式将在BOLEFormat记录的fmtIdt域中被用到。

    本章例程中， 程序在OnCreate事件中注册OLE对象的剪贴板格式，以下代码是主窗体的OnCreate事件：

   procedure TOLEFrameForm.FormCreate(Sender: TObject);

begin

FEmbedClipFmt := RegisterClipboardFormat('Embedded Object');

FLinkClipFmt := RegisterClipboardFormat('Link Source');

Fmts[0].fmtId := FEmbedClipFmt;

Fmts[0].fmtMedium := BOLEMediumCalc(FEmbedClipFmt);

Fmts[0].fmtIsLinkable := False;

StrPCopy(Fmts[0].fmtName, '%s');

StrPCopy(Fmts[0].fmtResultName, '%s');

Fmts[1].fmtId := FLinkClipFmt;

Fmts[1].fmtMedium := BOLEMediumCalc(FLinkClipFmt);

Fmts[1].fmtIsLinkable := True;

StrPCopy(Fmts[1].fmtName, '%s');

StrPCopy(Fmts[1].fmtResultName, '%s');

RegisterFormAsOleDropTarget(Self, Fmts)

end;

程序传给RegistClipBroardFormat函数一个描述格式的参数，它返回一个Word类型的值。该值能唯一的辨识新注册的格式。FEmbdeClipFmt,FlinkClipFmt 是TOLEFormat类的私有数据成员。 声明如下：

    TYPE

TOLEForaneForm = Class(TForm)

…

private

FEmbedClipFmt: Word;

FLinkClipFmt: Word;

function CreateChild: TOLEObjectForm;

public

Fmts: array[0..1] of BOleFormat;

end;

在注册剪贴板格式后， 还必须定义OLE 格式才能进行对象粘贴。 每种格式定义在BOLEFormat记录中。 程序中可能注册标准剪贴板格式并用这种格式进行粘贴。例如：注册文本作为粘贴格式，将BOLEFormat记录为fmtId域定义为CF_TEXT，fmt Medium 域定义为BOLE_MED_HGLOBOL。 BOLEMediumCalc 函数可以根据定义的剪贴板格式计算出fmtMedium值。在本章例程中，程序注册了两种格式，一种是链接OLE对象的格式，另一种是嵌入OLE对象的格式。

    BOLEFormat类型定义在BOLEDefs单元中，BOLEMediumCalc函数定义在ToCtrl单元。因此主窗中的interface部分应加入这两个单元。

    interface

use…,BOLEDefs,ToCtrl,

    在粘贴OLE对象前，应用程序必须知道在剪贴板中是否有OLE对象。

    PasteSpecialEnabled函数可判断粘贴对话框是否有效。如果剪贴板上有Fmts定义的任何一种格式，PasteSpecialEnable将返回真值， 粘贴对话框才能成功地调用。反之调用粘贴对话框将不发生任何事件。

    以下代码实现“编辑|粘贴”菜单项的功能：

procedure TOLEObjectForm.PasteSpecial1Click(Sender: TObject);

var

ClipFmt: Word;

DataHand: THandle;

Info: Pointer;

begin

if PasteSpecialEnabled(Self, OLEFrameForm.Fmts) then

if PasteSpecialDlg(Self, OLEFrameForm.Fmts, 0,

ClipFmt, DataHand, Info) then

InitializeOLEObject(Info)

end;

只有在粘贴对话框有效时“编辑|粘贴”菜单才有效，以下代码实现此功能：

    procedure TOLEObjectForm.Edit1Click(Sender: TObject);

begin

PasteSpecial1.Enabled := PasteSpecialEnabled(Self, OLEFrameForm.Fmts)

end;

8.3.5 释放OLE对象

    从OLE服务器拖动OLE对象并将其放在OLE应用程序是一种方便的对象链接与嵌入的方法。通过拖放操作，用户不需要使用插入对话框或粘贴对话框来定义OLE对象。而只需用鼠标键从OLE服务器中“抓”住OLE对象，拖至OLE应用程序，松开鼠标键，从而实现OLE对象的插入。

8.3.5.1 注册OLE释放目标窗体

    为了接收一个释放的OLE对象，必须有一个窗体在Windows中注册成OLE释放目标，用RegisterFormASOLEDropTarget函数可实现此功能。

    RegisterFormASOLEDropTarger(Form : TFrom;Const Fmts: array of BOlefrom).

    其中Form是OLE对象的释放目标窗体，在本章例程中，将子窗体传递给Form参数。

    Fmts是对象格式的数组。它是BOLEFormat 类型的数组。 所有要释放的数据必须用Fmts数组中相应BOLEFormat元素注册。

    在本章例程中，注册的Fmts 数组与主窗体OnCreate事件 声明的数组相同， 即：联接对象格式和嵌入对象格式。如果想接收更多类型的释放数据，就必须在Fmts数组中加入其它元素。例如应用程序要接收释放的文本，Fmts需加第三个元素， 其fmtId 域为CF_TEXT,BOLEMedium域为BOLE_MED_HGLOBL.

拖放过程中不需要用BOLEFormat的fmtName,fmtResultName域，如果程序只进行拖放操作而不进行对象粘贴，可以不初始化两个域。

    在主窗体的OnCreate事件中可调用RegisterFormAsOLEDropTorget。

procedure TOLEFrameForm,FormCreate(Sender : TObject);

begin…

    Register FormASOleDropTarget(Self,Fmts)

end;


第八章 对象链接与嵌入（二）

DELPHI基础教程

第八章　对象链接与嵌入（二）

8.3.5.2 在应用程序中释放OLE对象

    当一个对象释放到一个窗体，该窗体发生OnDragDrop 事件。该对象定义为TDragDropEvent方法中的Source参数，而TDragDropEvent 方法是用来处理OnDragDrop事件”。 如果Source 是一个OLE 对象， 那么它是TOLEDropNotify 对象的派生类型。 TOLEDropNotify对象有一个与OLE包容器部件PInitInfo属性相对应的PIniInfo属性。 如果一个OLE对象被释放。PInitInfo指向OLE对象的初始化信息结构。要实现释放功能。只需将TOLEDropNotify的PInitInfo属性赋给OLE包容器部件的PInitInfo属性。

    以下为处理OnDragDrop事件的代码：

procedure TOLEFrameForm.FormDragDrop(Sender, Source: TObject; X,

Y: Integer);

var

NewChild: TOLEObjectForm;

begin

if Source is TOLEDropNotify then

begin

NewChild := CreateChild;

with Source as TOLEDropNotify do

NewChild.OLEContainer.PInitInfo := PInitInfo

end

end;

注意不要用ReleaseOLEInitInfo释放分配给PInitInfo属性的内存。Delphi自动释放这块内存。

8.3.6 文件中的OLE对象

    在OLE应用程序中，要保存对OLE对象的修改，需将对象数据保存在文件中。 如果对象是链接的数据，Delphi将自动的保存在源文件中。当对象被修改时，文件中的数据自动修改。 如果对象是嵌入的，数据贮存在应用程序程序的窗体。要保存对嵌入对象的修改， 应用程序应把数据保存在特殊的OLE文件中。如果要对已存文件的对象进行编辑，应用程序必须从文件中装入OLE对象。

    OLE包容器部件的SaveToFile方法可保存对象：

    OleCntainer1.SaveToFile('C: \SALEs.OLE')；

    OLE包容器部件的loadFromFile方法可把文件中的对象装入OLE包容器部件。

    OleContainer1.loadFromFile('C:\SALEs.OLE')

    本章例程使用了保存对话框和打开对话框来实现运行状态的对象保存和对象装入。

    在OLEObjectForm窗体加入保存对话框部件和打开对话框部件。其主要属性如表8.4：

    表8.4 保存对话框的属性及取值：

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

  属性　　　　　　　　值

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

    Name SaveAsDialog

DefaultExit ole

FileName .OLE

Filter OLE files (*.OLE)|*.OLE

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

表8.5 打开对话框的属性及取值

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

    属性　　　　　　　　取值

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

    Name OpenDialog

DefaultExit ole

FileName .OLE

Filter OLE files (*.OLE)|*.OLE

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

    用户单击“文件|保存”菜单项实现OLE对象的保存。代码如下：

procedure TOLEObjectForm.SaveAs1Click(Sender: TObject);

begin

if SaveAsDialog.Execute then

OLEContainer.SaveToFile(SaveAsDialog.Filename)

end;

用户单击“文件|打开”菜单项实现对象文件装入：

procedure TOLEFrameForm.Open1Click(Sender: TObject);

var

NewChild: TOLEObjectForm;

begin

f OpenDialog.Execute then

begin

NewChild := CreateChild;

NewChild.OLEContainer.LoadFromFile(OpenDialog.FileName)

end

end;

     8.4 OLE自动化

    OLE自动化是Windows应用程序操纵另一个程序的一种机制。OLE 2.0提供了一种方法来集成应用程序，这就是应用程序之间的命令操作。

    利用OLE 2.0，程序员可以定义一组命令，使它们进入到其它程序中。这些命令可带参数。看起来很象应用程序在调用函数或过程一样。采用上述办法， 可以在人不参与的情况下，就能使得两个应用程序的相互作用。

    被自动化的程序称作自动化对象或自动化服务器， 操作或自动化其他程序的应用程序称为自动化控制器或自动化客户器。

    Delphi2.0完全支持OLE2.0的应用程序自动化，可以用Delphi 2.0编写自动化控制器和服务

器。在应用程序之间可编程的潜能是巨大的。用户可以创建宏或者其它命令， 使得某个应用程序能透过其它应用程序进行工作。已经存在的应用程序的宏语言很容易被扩展，它可以包括一组别的应用程序能够执行的命令和函数调用。

     现在介绍两个应用程序，其中MemoEdit.dpr 是多文档界面的文本编辑器，作为OLE自动化服务器,AutoFrom.dpr是自动化控制器。运行AutoForm前，在Delphi集成开发环境中单击菜单(run | parameters),Delphi弹出运行参数对话框，如图8.5，输入参数后运行状态如图8.6。AutoForm窗体的多个按钮。可对MemoEdit进行操作；如按Creat按钮，MemoEdit产生三个子窗体，如图8.7，按"AddText"，子窗体将出现"This text was added through OLE Automation"的字符串“

MemoEdit包括三个单元：

    Mainfrom MDI主窗体

    EditFrom MDE子窗体和自动化类

    MemoAuto 应用程序自动化对象

    下面结合例程讲述OLE自动化的基本概念及开发。

8.4.1 TAutoObject对象

    TAutoObject 是Delphi自动化服务器中所有对象的基类，任何自动化对象都是从TAutoObject类派生出来的。

    OLE对象的定义与其它类的定义类似。它的automated部分象普通类的public部分，OLE控制器可引用在这部分声明的属性和方法。编译器把automated部分创建成OLE自动化对象的入口。但automated部分的代码有很多限制：

    ● 属性方法可以定义，但不能定义域；

    ● 所有属性、参数、函数类型必须是以下类型之一：

    SmallInt,Integer,Single,Double,Currency,TDateTime,String,WordBool, Varint

● 属性声明只能包括访问定义符(read and Write)，其它定义符如index,stored,

default,odefault均不能使用；

    ● 访问定义符必须列出相应的方法标识符，不能使用域标识符；

    ● 支持数组类型；

    ● 不允许属性重载；

    ● 方法是可以是虚拟的，但不能是动态的，允许方法重载。

    在EditFrom单元中定义了TMemoDoc类：

    type

TMemoDoc = Class(TAutoObject)

private

FEditForm : TEditForm;

funtion CretFileName : String;

funtion CretModiFied : WordBool;

procedure SetFileName(Const Value : String);

automated

procedure Clear;

procedure Ineart(Const Text : String);

procedure Save;

procedure Close;

procedure FileName : String read GretFileName write

SetFileName;

procedure Modified : WordBool read GretModified

end;

TMemeDoc类是MemoEdit程序的内部自动化类，因此不需要注册。外部OLE自动化控制器对它不能直接引用。如果要使外部控制器对自动化对象进行操作，则要在声明自动化对象的单元中调用Automation. RegisterClass 进行注册。例程MemoAuto 单元定义了TMemoApp对象并进行注册。

    unit MemoAuto

   …

type

TMemoApp = Class(TAutoObject)

implementation

…

    procedure RegisterMemoApp

Const

AutoClassInfo : TAutoClassInfo = (

AutoClass : TMemoApp;

ProgID : MemoEdit,Application

ClassIn : '{FIFF4880 - 200D - 11CF - BDCF - D020AFOE5B81}';

Description : 'Memo Editor Application';

Instancing : acSingle Instance );

begin

Automation,RegisterClass(AutoClassInfo)

end;

inibialization

RegisterMemoApp;

end;

自动化对象要在initialization部分中对自动化对象进行注册。 注册的信息用以唯一辨识服务器对象。把一个自动化对象加入到服务器中要用到这些信息。程序一旦注册了自动化对象，全局自动化对象将用OLE自动化API进行自动管理。

    注册后的OLE自动化对象是引用记数的，因为对象可能被多个控制器控制。当使用完一个OLE对象，调用Release方法，Release可减少引用数目，当引用数目为零时，调用Free方法释放对象。

    通常把OLE对象作为变体类型(variants)进行输出，任何OLE 对象的方法和属性必须返回一个包含OLE对象的变体类型，TAutoObject提供了一个变体类型的OLEObject属性。控制器不能直接得到服务器中的类或指针，而是引用OLE对象的OLEObject属性。

    例程MemoAuto单元的NewMemo函数就是通过引用OLEObject 属性而提供引用TMemoDoc对象的接口。

    function TMemoApp,NewMemo : Variant;

begin

Result := MainForm,CreateMemo(' '),OleObject;

end;

8.4.2 创建OLE自动化服务器

    OLE自动化服务器是应用程序或动态链接库(DLL)，它可向OLE 自动化控制器输出OLE对象。 MemoEditdpr 就是OLE 自动化服务器， 在MemoAuto 单元中注册了MemoEdit.Appdication自动化类，所有OLE控制器均可对MemoEdit.Application进行引用。

    在Windows环境下有两种OLE自动化服务器，进程内服务器和进程外服务器， Delphi可创建这两种服务器。

    进程内服务器是输出OLE自动化对象的动态链接库。因为OLE自动化对象来自于DLL，

对象是控制器程序的同一窗体进程，进程内服务器适合于创建共享的程序模块， 而这个模块可以被用不同语言编写的多个程序所共享。 进程内服务器被调用时在同一地址中运行，这样就不需要控制器进行调度，以避免处理大量的消息句柄。 进程外服务器是能输出OLE自动化对象的应用程序。

    有些OLE自动化服务器只能创建和输出一个OLE对象，有些服务器则可以处理多个OLE对象，另外一些服务器不能输出OLE对象，只能在程序内部使用OLE对象。 服务器与其能输出的对象数目的关系称为实例(instancing)。

    在创建OLE 自动化对象时必须定义实例， 这样， 在创建一个OLE 自动化对象时，Windows就能决定是否创建一个新的服务器实例。表8.5列出三种实例类型。

表8.6 实例的取值及含义

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

instancing类型　 　　　　　　　　含义

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

internal OLE对象是应用程序的内部对象，对象不需要注册，外部进程不能创

                   建此对象

Single 每个服务器实例只能输出一个OLE对象实例， 若控制器需要多个OLE

                   对象实例，WIndows为第一个OLE对象创建一个服务器实例

Multiple 一个服务器能创建和输出多个OLE 对象实例， 进程内服务器大多是

                  Multiple类型

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

    每个使用OLEAuto单元的工程文件自动地拥有一个叫Automation的对象，它是非可视对象。就象Application部件拥有Delphi应用程序的一些信息一样，Automation对象也拥有服务器的一些信息，其中最重要的是StartMode属性和OnLastRelease事件。

    StartMode指示OLE自动化服务器打开方式打开的目的。表8.7列出StartMode四种取值。

表8.7 StartMode 的取值及含义

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

    取值　　　　　　　 　　含义

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

    SmStandAlone 用户启动应用程序

    SmAutomation Windows为创建OLE对象而启动程序

    SmRegSever 应用程序仅为注册一个或多个OLE对象而启动

    SmUnregSever 应用程序仅为注销一个或多个OLE对象而启动

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

    当StartMode模式是SmAutomation，而用户不再需要服务器时发生OnLastRelease 事件。此时所有OLE控制器释放了由服务器创建的对象。缺省情况下，服务器关闭实例，但OnLastRelease 事件可根据实际情况是否关闭。OnLastRelease 事件可得到一个叫ShutDown的布尔型变量。把ShutDown设置成True，则在最后一个OLE对象释放时服务器不关闭。

    无论创建何种自动化服务器，必须定义对控制器的界面，包括定义和注册OLE对象，OLE自动化对象的属性和方法。定义界面主要是为了控制器能够引用它们。

    对已存在的自动化服务器界进行修改时，要确保向上兼容　，不要删去已有的属性、方法，这样会导致已存在的自动化控制器发生错误，修改服务器只能增加属性和方法。

    创建OLE自动化服务器第一步是创建服务器自身。即创建能输出OLE 对象的应用程序或动态链接库。这主要取决于是创建进程内服务器还是进程外服务器。

    创建进程内服务器，即动态链接库：

    1.创建动态链接库；

    2.在工程文件的uses条款中加入OLEAuto单元；

    3.在DLL中输出四个标准入口，即加入以下代码。

    exports

DLLGetClassObject,DLLCanUnloadNow;

DLLRegisterServer,DLLUnregisterServer;

以上代码必须准确拼写，包括大小写。与Object Pascal的其它项目不同，这些代码

对大小写敏感。

    创建进程外服务器：

    1.创建一个Delphi应用程序；

    2.在工程文件的begin之后加入以下代码；

    if Automation,Server Registration then Exit;

创建服务器之后，应该向服务器加入OLE自动化对象，这个过程大部分是自动完成的，但必须向Delphi的自动化对象专家提供必要的信息。

    把OLE自动化对象加入服务器：

    1.在Delphi集成开发环境中选择File| New 菜单项， 并在对象集中选择Automation

Object,Delphi打开自动化对象专家。

2.给自动化对象命名

      这是服务器内部标识OLE对象的名字，必须是个有效的面象对象Pascal标识符，习惯上以T字母开头；

    3.给OLE类命名

      该名用以外部控制器创建对象。当服务器在Windows中注册OLE对象， 就以这个名字在系统注册。控制器使用这个名字调用CreateOLEObject来创建对象。

    4.描述要输出的对象。

    5.定义对象的实例(instancing)，进程内服务器常定义为Multiple,进程外服务器常定义为Single；

    6.选择OK键完成该过程

      自动化对象专家将产生以下代码：

      ● 从TAutoObject派生下来的自动化对象定义，但没有定义任何属性方法；

      ● 调用DelphiOLE自动化管理器的注册代码，管理器负责Windows中注册服务器和对象。

    在注册代码中包括一个自动产生的ID号，这个ID号是全局唯一的，通常不要修改。每个ID号与一个OLE类名相对应，如果其中之一被改变，应用程序在使用时会发生错误。

    在创建了服务器并把OLE自动化对象加入服务器之后，控制器程序就可以对服务器进行操纵。

8.4.3 自动化另一程序

    每个服务器在系统注册中有一个叫ProgID的关键定，主要用以控制器辨识服务器。任何控制器可以用ProgID号来创建OLE对象实例。例程AutoForm是控制器程序，它在其主窗体创建了OLE对象实例。

    procedure TMainForm.FormCreate(Sender : TObject);

begin

try

MemoEdit := CreateOleObject('MemoEdit.Application');

except

MessageDlg(

'An instance of the "MemoEdit Application"OLE Automation Class could

not be created,Make sure that the MemoEdit application has been registered

using a "MemoEdit|regserver"command line',

mtError,[mbok],0)

Halt;

end;

end;

控制器创建了OLE自动化对象实例后，可对其进行操纵。OLE自动对象包括属性和方法，虽然OLE自动化对象与面向对象Pascal中的对象不是同一概念，但Delphi允许使用与类似的语法对OLE对象的方法进行调用。

    AutoForm的很多过程引用了OLE自动化对象的方法：

    procedure TMainForm,TileButtonClick(Sender : Tobject);

begin

MemoEdit,TileWindow;

end;

其中TileWindows是OLE对象TMemoApp中定义的方法。

    AutoForm还通过TMemoApp的NewMemo方法获得了对服务器内部OLE对象TMemoDoc 的引用。

  procedure TMainForm,CreateButtonClick(Sender : TObject);

var

I : Integer;

begin

CloseMemo

for I := 1 to 3 do Memos[2] := MemoEdit.NewMemo;

end;

其中NewMemo在MemoAuto单元中定义如下：

    function IMemoApp.NewMemo : Variant;

begin

Result := MainForm,CreateMemo(' '),OleObject;

end;

控制器在获得服务器的内部OLE对象后，可以引用其方法：

    procedure TMainForm.AddTextButtonClick(Sender,TObject);

var

I : Integer;

begin

for I := 1 to 3 do

if not var IsEmpty(Memo[I]) then

Memo[I],Insert{'This text was added through OLE Automation'#13#10);

end;

Insert是TMemoDoc中定义的方法，用以在子窗体中插入字符串。


第九章 Delphi拖放编程

DELPHI基础教程

第九章　Delphi拖放编程

    拖放(DragDrop)是Windows提供的一种快捷的操作方式。作为基于Windows的开发工具，Delphi同样支持拖放操作，而且开发应用系统的拖放功能十分方便，真正体现了Delphi的强大功能和方便性。

    Delphi提供的所有控件(Control，即能获得输入焦点的部件)都支持拖放操作，并有相应的拖放属性、拖放事件和拖放方法。下面我们先介绍控件的拖放支持，而后再给出开发拖放操作的一般步骤和应用实例。

9.1　控件的拖放支持

    拖放操作中控件可以分为源控件和目标控件两类。绝大部分控件既可以作为源控件也可以作为目标控件。但也有一部分控件只能支持其中的一种。

9.1.1 拖放属性

    拖放属性主要有两个：

    ● DragMode ： 拖动模式

    ● DragCursor ： 拖动光标

    它们都是在拖放的源控件中设置。DragMode控制用户在运行时间内当在控件上按下鼠标时控件如何反应。如果DragMode置为dmAutomatic，那么当用户在控件上按下鼠标时拖动自动开始；如果DragMode置为dmManual(这是缺省值)，则将通过处理鼠标事件来判断一个拖动是否可以开始。

    DragCursor用于选择拖动时显示的光标，缺省值是CrDrag，一般不要去修改它。在程序设计过程中通用的界面规范应该得到开发者的尊重。但有时候为了特定的目的，开发者也可以把自己设计的光标赋给DragCursor。

9.1.2 拖放事件

    拖放事件主要有三个：

    ●OnDragOver：拖动经过时激发

    ●OnDragDrop：拖动放下时激发

    ●OnEndDrop ：拖动结束时激发

    前两个事件由目标控件响应，后一个事件由源控件响应。

    OnDragOver事件最主要的功能是确定当用户就地放下拖动时控件是否可以接受。它的参数包括：

Source ： TObject；　 {源控件}

X，Y ： Integer; {光标位置}

State ： TDragState； {拖动状态}

var Accept ： Boolean {能否接受}

    TDragState是一个枚举类型，表示拖放项目与目标控件的关系。

     type

TDragState = (dsDragEnter, dsDragLeave, dsDragMove);

    不同取值的意义如下表：

表9.1 DragState 的取值与意义

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

    取 值 意 义

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

dsDragEnter 拖动对象进入一个允许拖动对象放下

的控件中。为缺省状态。

dsDragLeave 拖动对象离开一个允许拖动对象放下

的控件。

dsDragMove 拖动对象在一个允许拖动对象放下的

控件内移动。

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

    用户可以利用提供的参数来确定放下的拖动是否可被接受，如：

    ● 判断源控件类型：

     Accept := Source is TLabel;

    ● 判断源控件对象：

     Accept := (Source = TabSet1);

    ● 判断光标位置：

见(9.2),(9.3)中的例程。

● 判断拖动状态：

     If (Source is TLabel) and (State = dsDragMove) then

     begin

source.DragIcon := ' New.Ico ';

Accept := True;

     end

     else

     Accept := False;

    当Accept=True时，目标控件可以响应OnDragDrop事件，用于确定拖动被放下后程序如何进行处理。

    OnDragDrop事件处理过程的参数包括源控件和光标位置。这些信息可用于处理方式的确定。

    OnEndDrag事件是在拖动操作结束后由源控件来进行响应的，用于源控件进行相应的处理。拖动操作结束既包括拖动放下被接受，也包括用户在一个不能接受放下的控件上释放了鼠标。该事件处理过程的参数包括目标控件(Target)和放下位置的坐标。如果Target=nil, 表示拖动项目没有被任何控件接受。

    在第3节将介绍的文件拖放移动、拖放拷贝操作中，如果操作成功，则文件列表框应更新显示内容。下面这段程序用于实现这一功能。

procedure TFMForm.FileListEndDrag(Sender, Target: TObject; X, Y: Integer);

begin

if Target <> nil then FileList.Update;

end;

    除以上介绍的三个事件外，还有一个事件OnMouseDown 也常用于拖放操作的响应。OnMouseDown虽然不是一个专门的拖放事件，但在人工模式下拖动的开始是在这一事件的处理过程中实现的。

9.1.3 拖放方法

    拖放方法有三个：

    ●BeginDrag ： 人工方式下开始一个拖动

    ●EndDrag ：　结束一个拖动

    ●Dragging ： 判断一个控件是否正被拖动

    这三个方法都被源控件使用。

    当DragMode置为dmManual时，拖动必须调用控件的BeginDrag方法才能开始。BeginDrag有一个布尔参数Immediate。如果输入参数为True，拖动立即开始，光标改变到DragCursor的设置。如果输入参数为False，直到用户将光标移动了一定的距离(5个象素点)后才改变光标，开始拖动。这就允许控件接受一个OnClick事件而并不开始拖动操作。

    EndDrag方法中止一个对象的被拖动状态。它有一个布尔参数Drop。如果Drop设置为True，被拖动的对象在当前位置放下(能否被接受由目标控件决定)；如果Drop设置为False，则拖动就地被取消。

    下面一段程序表明当拖动进入一控制面板时拖动被取消。

procedure TForm1.Panel1DragOver(Sender, Source: TObject; X, Y: Integer;

State: TDragState; var Accept: Boolean);

begin

Accept := False;

if (Source is TLabel) and (State = dsDragEnter) then

(Source as TLabel).EndDrag(False);

end;

    Draging方法判断一个控件是否正被拖动。在下面的例子中当用户拖动不同的检查框时窗口改变为不同的颜色。

procedure TForm1.FormActivate(Sender: TObject);

begin

CheckBox1.DragMode := dmAutomatic;

CheckBox2.DragMode := dmAutomatic;

CheckBox3.DragMode := dmAutomatic;

end;

procedure TForm1.FormDragOver(Sender, Source: TObject; X, Y: Integer;

State: TDragState; var Accept: Boolean);

begin

if CheckBox1.Dragging then

Color := clAqua;

if CheckBox2.Dragging then

Color := clYellow;

if CheckBox3.Dragging then

Color := clLime;

end;

9.2 开发拖放功能的一般步骤

    拖放作为Windows提供的一种方便操作对象的功能，在Delphi中可以很容易地开发出来。根据拖放操作的过程可以把开发步骤划分为四个阶段，即：

    ● 开始拖动操作

    ● 接收拖动项目

    ● 放下拖动项目

    ● 终止拖动操作

    在介绍过程中我们将结合一个TabSet(标签集)的拖放操作实例。界面设计如图。在运行时当用户把一个标签拖动到另一个标签的位置时，该标签将移动到该位置并引起标签集的重新布置。

9.2.1 开始拖动操作

    当拖动模式(DragMode)设置为dmAutomatic时，用户在源控件上按下鼠标时拖动自动开始；当设置为dmManual时通过处理鼠标事件来决定拖动是否开始。如果想开始拖动调用BeginDrag方法。

    在TabSet拖放中，我们用下面的MouseDown事件处理过程来开始一个标签的拖动。首先判断按下的是否是左键，而后再判断项目是否合法。

procedure TForm1.TabSet1MouseDown(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y: Integer);

var

DragItem: Integer;

begin

if Button = mbLeft then

begin

DragItem := TabSet1.ItemAtPos(Point(X, Y));

if (DragItem > -1) and (DragItem < TabSet1.Tabs.Count) then

TabSet1.BeginDrag(False);

end;

end;

9.2.2 接收拖动项目

    一个控件能否接收拖动项目是由该控件的OnDragOver事件决定的。在TabSet拖动中，主要是利用鼠标的位置进行判断。

procedure TForm1.TabSet1DragOver(Sender, Source: TObject; X, Y: Integer;

State: TDragState; var Accept: Boolean);

var

DropPos: Integer;

begin

if Source = TabSet1 then

begin

DropPos := TabSet1.ItemAtPos(Point(X, Y));

Accept := (DropPos > -1) and (DropPos <> TabSet1.TabIndex) and

(DropPos < TabSet1.Tabs.Count);

end;

else

Accept := False;

end;

9.2.3 放下拖动项目

    当OnDragOver事件处理过程返回的Accept为True且项目被放下时，由OnDragDrop事件处理过程来完成拖动放下后的响应。在TabSet拖放实例中是改变标签的位置。

procedure TForm1.TabSet1DragDrop(Sender, Source: TObject; X, Y: Integer);

var

OldPos: Integer;

NewPos: Integer;

begin

if Source = TabSet1 then

begin

OldPos := TabSet1.TabIndex;

NewPos := TabSet1.ItemAtPos(Point(X, Y));

if (NewPos > -1) and (NewPos <> OldPos) then

TabSet1.Tabs.Move(OldPos, NewPos);

end;

end;

9.2.4 结束拖动操作

    结束拖动操作的方式有两种：或者是用户释放了鼠标键或者是程序用EndDrag方法强行中止拖动。结束拖动操作的后果有两种：放下被接受或放下被忽略。

    拖动操作结束后源控件都要收到一条消息响应拖动结束事件OnEndDrag。

9.3　　拖放应用实例：文件管理器的拖放支持

    在第六章最后开发的文件管理器应用实例，虽然功能上已初具规模，但在操作上与Windows的文件管理器相比还有很大不足。其中最大的缺陷是它不支持文件的拖放移动和拖放拷贝。在这一章结束的时候，我们可以来弥补这一缺陷了。

    文件拖放移动指的是当用户把一个文件拖动到目录树下的某一目录并放下时，文件将自动移动到该目录中；文件拖放拷贝指的是当用户把一个文件拖动到某个驱动器标签上并放下时，文件将自动拷贝到该驱动器的当前目录下。作为源控件的文件列表框和作为目标控件的目录树、驱动器标签可以位于不同的子窗口。驱动器的当前目录是任一子窗口的最新操作结果，而不论这一子窗口与拖动源、拖动目标是否有关系。

    为了实现上述功能，有两个问题必须首先解决：

    1.如何记录每一驱动器的当前目录？

    为此我们定义了一个全局变量：

    var

CurentDirList: Array[0...25] of string[70];

在DirectoryOutline的OnChange事件中：

procedure TFMForm.DirectoryOutlineChange(Sender: TObject);

begin

CreateCaption;

FileList.clear;

FileList.Directory := DirectoryOutline.Directory;

FileList.Update;

CurrentDirList[DriveTabSet.TabIndex] := DirectoryOutline.Directory;

FileManager.DirectoryPanel.Caption := DirectoryOutline.Directory;

end;

    由于DriveTabSet在响应OnDragDrop事件前先响应OnClick事件，并由该事件激发DirectoryOutline的Onchange事件，因而可保证在任何时候OnDragDrop事件中用到的CurrentDirList数组项不为空字符串。

  2.如何保证移动、拷贝与子窗口的无关性？

    在这里一个关键问题是我们判断源控件时是用is操作符进行类型检查:

If Source is TFileList then

…

    如果我们用下面的语句：

   If Source = FileList then

     …

    则移动、拷贝操作将限制在本子窗口范围内。

    当解决了上述问题后我们的工作就只是遵循拖放的一般开发步骤，按步就班来完成了。

    1.FileList开始拖动操作

procedure TFMForm.FileListMouseDown(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y: Integer);

begin

if Button = mbLeft then

with Sender as TFileListBox do

begin

if ItemAtPos(Point(X, Y), True) >= 0 then

BeginDrag(False);

end;

end;

    ItemAtPos用来检查当前是否有文件存在。而BeginDrag方法传递参数False, 允许FileList单独处理鼠标事件而并不开始拖动。事实上这种情况是大量存在的。

    2.DirectoryOutline、DriveTabSet决定是否能接受拖动的就地放下。

procedure TFMForm.DirectoryOutlineDragOver(Sender, Source: TObject; X,

Y: Integer; State: TDragState; var Accept: Boolean);

begin

if Source is TFileListBox then

Accept := True;

end;

procedure TFMForm.DriveTabSetDragOver(Sender, Source: TObject; X,

Y: Integer; State: TDragState; var Accept: Boolean);

var

PropPos: Integer;

begin

if Source is TFileListBox then

with DriveTabSet do

begin

PropPos := ItemAtPos(Point(X,Y));

Accept := (PropPos > -1) and (PropPos < Tabs.Count);

end;

end;

    DirectoryOutline是无条件的接受，而DriveTabSet需检查是否是合法的标签。

    3.拖动放下的响应

    DirectoryOutline的拖动放下用于实现文件移动功能。程序中调用ConfirmChange事件处理过程，目标路径由DirctoryOutline.Items[GetItem(X,Y)].FullPath来得到。

procedure TFMForm.DirectoryOutlineDragDrop(Sender, Source: TObject; X,

Y: Integer);

begin

if Source is TFileListBox then

with DirectoryOutline do

begin

ConfirmChange('Move',FileList.FileName, Items[GetItem(X, Y)].FullPath);

end;

end;

   DriveTabSet的拖动放下用于实现文件拷贝功能。程序中把当前位置转化为相应的驱动器号，目标路径由CurrentDirList[DriveTabSet.TabIndex]获得。

procedure TFMForm.DriveTabSetDragDrop(Sender, Source: TObject; X,Y: Integer);

var

APoint: TPoint;

begin

APoint.X := X; APoint.Y := Y;

DriveTabSet.TabIndex := DriveTabSet.ItemAtPos(APoint);

if Source is TFileListBox then

with DriveTabSet do

begin

if CurrentDirList[TabIndex] <> '' then

ConfirmChange('Copy',TheFilename,CurrentDirList[TabIndex]);

end;

end;

4.FileList响应拖动结束，更新文件列表

procedure TFMForm.FileListEndDrag(Sender, Target: TObject; X, Y: Integer);

begin

if Target <> nil then FileList.Update;

end;

到目前为止，我们的文件管理器功能已足够强大。 不过还有许多问题值得读者去进

一步探讨，如：

    1.文件与应用程序关联的建立；

    2.在文件列表框中显示更多的文件信息；

    3.文件列表框中的文件按后缀各排序等。

    文件管理器是一个真正的综合例程，对它的钻研会使您更进一步模到Delphi编程的精髓。


第十章 动态链接库编程（一）

DELPHI基础教程

第十章　动态链接库编程（一）

10.1 Windows的动态链接库原理

    动态链接库(DLLs)是从C语言函数库和Pascal库单元的概念发展而来的。所有的C语言标准库函数都存放在某一函数库中，同时用户也可以用LIB程序创建自己的函数库。在链接应用程序的过程中，链接器从库文件中拷贝程序调用的函数代码，并把这些函数代码添加到可执行文件中。这种方法同只把函数储存在已编译的.OBJ文件中相比更有利于代码的重用。

    但随着Windows这样的多任务环境的出现，函数库的方法显得过于累赘。如果为了完成屏幕输出、消息处理、内存管理、对话框等操作，每个程序都不得不拥有自己的函数，那么Windows程序将变得非常庞大。Windows的发展要求允许同时运行的几个程序共享一组函数的单一拷贝。动态链接库就是在这种情况下出现的。动态链接库不用重复编译或链接，一旦装入内存，Dlls函数可以被系统中的任何正在运行的应用程序软件所使用，而不必再将DLLs函数的另一拷贝装入内存。

10.1.1 动态链接库的工作原理

    “动态链接”这几字指明了DLLs是如何工作的。对于常规的函数库，链接器从中拷贝它需要的所有库函数，并把确切的函数地址传送给调用这些函数的程序。而对于DLLs，函数储存在一个独立的动态链接库文件中。在创建Windows程序时，链接过程并不把DLLs文件链接到程序上。直到程序运行并调用一个DLLs中的函数时，该程序才要求这个函数的地址。此时Windows才在DLLs中寻找被调用函数，并把它的地址传送给调用程序。采用这种方法，DLLs达到了复用代码的极限。

    动态链接库的另一个方便之处是对动态链接库中函数的修改可以自动传播到所有调用它的程序中，而不必对程序作任何改动或处理。

    DLLs不仅提供了函数重用的机制，而且提供了数据共享的机制。任何应用程序都可以共享由装入内存的DLLs管理的内存资源块。只包含共享数据的DLLs称为资源文件。如Windows的字体文件等。

10.1.2 Windows系统的动态链接库

    Windows本身就是由大量的动态链接库支持的。这包括Windows API函数 ( KRNLx86.EXE，USER.EXE，GDI.EXE，…)，各种驱动程序文件，各种带有.Fon和.Fot 扩展名的字体资源文件等。Windows还提供了针对某一功能的专用DLLs，如进行DDE编程的ddeml.dll，进行程序安装的ver.dll等。

    虽然在编写Windows程序时必然要涉及到DLLs，但利用Delphi ，用户在大部分时候并不会注意到这一点。这一方面是因为Delphi提供了丰富的函数使用户不必直接去使用Windows API;另一方面即使使用Windows API，由于Delphi把API函数和其它Windows DLLs函数重新组织到了几个库单元中，因而也不必使用特殊的调用格式。所以本章的重点放在编写和调用用户自定义的DLLs上。

    使用传统的Windows编程方法来创建和使用一个DLLs是一件很令人头痛的事，正如传统的Windows编程方法本身就令人生畏一样。用户需要对定义文件、工程文件进行一系列的修改以适应创建和使用DLLs的需要。Delphi的出现，在这一方面，正如在其它许多方面所做的那样，减轻了开发者的负担。更令人兴奋的是Delphi利用DLLs 实现了窗体的重用机制。用户可以将自己设计好的窗体储存在一个DLLs中，在需要的时候可随时调用它。

10.2 DLLs的编写和调用

10.2.1 DLLs的编写

    在Delphi环境中，编写一个DLLs同编写一个一般的应用程序并没有太大的区别。事实上作为DLLs 主体的DLL函数的编写，除了在内存、资源的管理上有所不同外，并不需要其它特别的手段。真正的区别在工程文件上。

    在绝大多数情况下，用户几乎意识不到工程文件的存在，因为它一般不显示在屏幕上。如果想查看工程文件，则可以打开View菜单选择Project Source项，此时工程文件的代码就会出现在屏幕的Code Editor(代码编辑器)中。

    一般工程文件的格式为：

    program　　 工程标题;

    uses　　　　 子句;

    程序体

    而DLLs工程文件的格式为：

    library 工程标题;

    uses 子句;

    exprots 子句;

    程序体

    它们主要的区别有两点：

    1.一般工程文件的头标用program关键字，而DLLs工程文件头标用library 关键字。不同的关键字通知编译器生成不同的可执行文件。用program关键字生成的是.exe文件，而用library关键字生成的是.dll文件；

    2.假如DLLs要输出供其它应用程序使用的函数或过程，则必须将这些函数或过程列在exports子句中。而这些函数或过程本身必须用export编译指令进行编译。

    根据DLLs完成的功能，我们把DLLs分为如下的三类：

1.完成一般功能的DLLs；

2.用于数据交换的DLLs；

3.用于窗体重用的DLLs。

    这一节我们只讨论完成一般功能的DLLs，其它内容将在后边的两节中讨论。

10.2.1.1 编写一般DLLs的步骤

    编写一般DLLs的步骤如下：

    1.利用Delphi的应用程序模板，建立一个DLLs程序框架。

    对于Delphi 1.0的用户，由于没有DLLs模板，因此：

    (1).建立一个一般的应用程序，并打开工程文件；

    (2).移去窗体和相应的代码单元；

    (3).在工程文件中，把program改成library，移去Uses子句中的Forms，并添加适当的库单元（一般SysUtils、Classes是需要的），删去begin...end之间的所有代码。

    2.以适当的文件名保持文件，此时library后跟的库名自动修改；

    3.输入过程、函数代码。如果过程、函数准备供其它应用程序调用，则在过程、函数头后加上export 编译指示；

    4.建立exports子句，包含供其它应用程序调用的函数和过程名。可以利用标准指示 name 、Index、resident以方便和加速过程/函数的调用；

    5.输入库初始化代码。这一步是可选的；

    6.编译程序，生成动态链接库文件。

10.2.1.2 动态链接库中的标准指示

    在动态链接库的输出部分，用到了三个标准指示：name、Index、resident。

    1.name

    name后面接一个字符串常量，作为该过程或函数的输出名。如：

exports

InStr name MyInstr;

    其它应用程序将用新名字(MyInstr)调用该过程或函数。如果仍利用原来的名字(InStr)，则在程序执行到引用点时会引发一个系统错误。

    2.Index

    Index指示为过程或函数分配一个顺序号。如果不使用Index指示，则由编译器按顺序进行分配。

    Index后所接数字的范围为1...32767。使用Index可以加速调用过程。

    3.resident

    使用resident，则当DLLs装入时特定的输出信息始终保持在内存中。这样当其它应用程序调用该过程时，可以比利用名字扫描DLL入口降低时间开销。

    对于那些其它应用程序常常要调用的过程或函数，使用resident指示是合适的。例如：

exports

InStr name MyInStr resident;

10.2.1.3 DLLs中的变量和段

一个DLLs拥有自己的数据段(DS)，因而它声明的任何变量都为自己所私有。调用它的模块不能直接使用它定义的变量。要使用必须通过过程或函数界面才能完成。而对DLLs来说，它永远都没有机会使用调用它的模块中声明的变量。

    一个DLLs没有自己的堆栈段(SS)，它使用调用它的应用程序的堆栈。因此在DLL中的过程、函数绝对不要假定DS = SS。一些语言在小模式编译下有这种假设，但使用Delphi可以避免这种情况。Delphi绝不会产生假定DS = SS的代码，Delphi的任何运行时间库过程/函数也都不作这种假定。需注意的是如果读者想嵌入汇编语言代码，绝不要使SS和DS登录同一个值。

10.2.1.4 DLLs中的运行时间错和处理

    由于DLLs无法控制应用程序的运行，导致很难进行异常处理，因此编写DLLs时要十分小心，以确保被调用时能正常执行 。当DLLs中发生一个运行时间错时，相应DLLs并不一定从内存中移去（因为此时其它应用程序可能正在用它），而调用DLLs的程序异常中止。这样造成的问题是当DLLs已被修改，重新进行调用时，内存中保留的仍然可能是以前的版本，修改后的程序并没有得到验证。对于这个问题，有以下两种解决方法：

    1.在程序的异常处理部分显式将DLL卸出内存；

    2.完全退出Windows，而后重新启动，运行相应的程序。

    同一般的应用程序相比，DLL中运行时间错的处理是很困难的，而造成的后果也更为严重。因此要求程序设计者在编写代码时要有充分、周到的考虑。

10.2.1.5 库初始化代码的编写

    传统Windows中动态链接库的编写，需要两个标准函数：LibMain和WEP，用于启动和关闭DLL。在LibMain中，可以执行开锁DLL数据段、分配内存、初始化变量等初始化工作；而WEP在从内存中移去DLLs前被调用，一般用于进行必要的清理工作，如释放内存等。Delphi用自己特有的方式实现了这两个标准函数的功能。这就是在工程文件中的begin...end部分添加初始化代码。和传统Windows编程方法相比，它的主要特色是：

    1.初始化代码是可选的。一些必要的工作（如开锁数据段）可以由系统自动完成。所以大部分情况下用户不会涉及到；

    2.可以设置多个退出过程，退出时按顺序依次被调用；

    3.LibMain和WEP对用户透明，由系统自动调用。

    初始化代码完成的主要工作是：

    1.初始化变量、分配全局内存块、登录窗口对象等初始化工作。在(10.3.2)节“利用DLLs实现应用程序间的数据传输”中，用于数据共享的全局内存块就是在初始化代码中分配的。

    2.设置DLLs退出时的执行过程。Delphi有一个预定义变量ExitProc用于指向退出过程的地址。用户可以把自己的过程名赋给ExitProc。系统自动调用WEP函数，把ExitProc指向的地址依次赋给WEP执行，直到ExitProc为nil。

    下边的一段程序包含一个退出过程和一段初始化代码，用来说明如何正确设置退出过程。

library Test;

{$S-}

uses WinTypes, WinProcs;

var

SaveExit: Pointer;

procedure LibExit; far;

begin

if ExitCode = wep_System_Exit then

begin

{ 系统关闭时的相应处理 }

end

else

begin

{ DLL卸出时的相应处理 }

end;

ExitProc := SaveExit; { 恢复原来的退出过程指针 }

end;

begin

{DLL的初始化工作 }

SaveExit := ExitProc; { 保存原来的退出过程指针 }

ExitProc := @LibExit; { 安装新的退出过程 }

end.

    在初始化代码中，首先把原来的退出过程指针保存到一个变量中，而后再把新的退出过程地址赋给ExitProc。而在自定义退出过程LibExit结束时再把ExitProc的值恢复。由于ExitProc是一个系统全局变量，所以在结束时恢复原来的退出过程是必要的。

    退出过程LibExit中使用了一个系统定义变量ExitCode，用于标志退出时的状态。 ExitCode的取值与意义如下：

表10.1 ExitCode的取值与意义

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

取 值 意 义

—————————————————————

    WEP_System_Exit Windows关闭

WEP_Free_DLLx DLLs被卸出

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

    退出过程编译时必须关闭stack_checking，因而需设置编译指示 {$S-} 。

10.2.1.6 编写一般DLLs的应用举例

    在下面的程序中我们把一个字符串操作的函数储存到一个DLLs中，以便需要的时候调用它。应该注意的一点是：为了保证这个函数可以被其它语言编写的程序所调用，作为参数传递的字符串应该是无结束符的字符数组类型(即PChar类型)，而不是Object Pascal的带结束符的Srting类型。程序清单如下：

library Example;

uses

SysUtils,

Classes;

{返回字符在字符串中的位置}

function InStr(SourceStr: PChar;Ch: Char): Integer; export;

var

Len,i: Integer;

begin

Len := strlen(SourceStr);

for i := 0 to Len-1 do

if SourceStr[i] = ch then

begin

Result := i;

Exit;

end;

Result := -1;

end;

exports

Instr Index 1 name 'MyInStr' resident;

begin

end.

10.2.2 调用DLLs

    有两种方法可用于调用一个储存在DLLs中的过程。

    1.静态调用或显示装载

    使用一个外部声明子句，使DLLs在应用程序开始执行前即被装入。例如：

    function Instr(SourceStr : PChar;Check : Char); Integer; far; external 'UseStr';

    使用这种方法，程序无法在运行时间里决定DLLs的调用。假如一个特定的DLLs在运行时无法使用，则应用程序将无法执行。

    2.动态调用或隐式装载

    使用Windows API函数LoadLibray和GetProcAddress可以实现在运行时间里动态装载DLLs并调用其中的过程。

    若程序只在其中的一部分调用DLLs的过程，或者程序使用哪个DLLs， 调用其中的哪个过程需要根据程序运行的实际状态来判断，那么使用动态调用就是一个很好的选择。

    使用动态调用，即使装载一个DLLs失败了，程序仍能继续运行。

10.2.3 静态调用

    在静态调用一个DLLs中的过程或函数时，external指示增加到过程或函数的声明语句中。被调用的过程或函数必须采用远调用模式。这可以使用far过程指示或一个{$F +}编译指示。

    Delphi全部支持传统Windows动态链接库编程中的三种调用方式，它们是：

    ● 通过过程/函数名

    ● 通过过程/函数的别名

    ● 通过过程/函数的顺序号

    通过过程或函数的别名调用，给用户编程提供了灵活性，而通过顺序号(Index)调用可以提高相应DLL的装载速度。

10.2.4 动态调用

10.2.4.1 动态调用中的API函数

    动态调用中使用的Windows API函数主要有三个,即：Loadlibrary，GetProcAddress和Freelibrary。

     1.Loadlibrary: 把指定库模块装入内存

    语法为：

    function Loadlibrary(LibFileName: PChar): THandle;

LibFileName指定了要装载DLLs的文件名，如果LibFileName没有包含一个路径，则Windows按下述顺序进行查找：

    (1)当前目录；

    (2)Windows目录(包含win.com的目录)。函数GetWindowDirectory返回这一目录的路径；

    (3)Windows系统目录(包含系统文件如gdi.exe的目录)。函数GetSystemDirectory返回这一目录的路径；

    (4)包含当前任务可执行文件的目录。利用函数GetModuleFileName可以返回这一目录的路径；

    (5)列在PATH环境变量中的目录；

    (6)网络的映象目录列表。

    如果函数执行成功，则返回装载库模块的实例句柄。否则，返回一个小于HINSTANCE_ERROR的错误代码。错误代码的意义如下表：

     表10.2 Loadlibrary返回错误代码的意义

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

错误代码　　　　　　　　　意　　　　　　　　义

——————————————————————————————————————

       0 系统内存不够，可执行文件被破坏或调用非法

       2 文件没有被发现

       3 路径没有被发现

       5 企图动态链接一个任务或者有一个共享或网络保护错

       6 库需要为每个任务建立分离的数据段

        8 没有足够的内存启动应用程序

     10 Windows版本不正确

       11 可执行文件非法。或者不是Windows应用程序，或者在.EXE映

          像中有错误

       12 应用程序为一个不同的操作系统设计(如OS/2程序)

13 应用程序为MS DOS4.0设计

       14 可执行文件的类型不知道

       15 试图装载一个实模式应用程序(为早期Windows版本设计)

16 试图装载包含可写的多个数据段的可执行文件的第二个实例

       19 试图装载一个压缩的可执行文件。文件必须被解压后才能被装裁

       20 动态链接库文件非法

       21 应用程序需要32位扩展

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

    假如在应用程序用Loadlibrary调用某一模块前，其它应用程序已把该模块装入内存，则Loadlibrary并不会装载该模块的另一实例，而是使该模块的“引用计数”加1。

    2.GetProcAddress：捡取给定模块中函数的地址

    语法为：

    function GetProcAddress(Module: THandle; ProcName: PChar): TFarProc;

Module包含被调用的函数库模块的句柄，这个值由Loadlibrary返回。如果把Module设置为nil，则表示要引用当前模块。

    ProcName是指向含有函数名的以nil结尾的字符串的指针，或者也可以是函数的次序值。如果ProcName参数是次序值，则如果该次序值的函数在模块中并不存在时，GetProcAddress仍返回一个非nil的值。这将引起混乱。因此大部分情况下用函数名是一种更好的选择。如果用函数名，则函数名的拼写必须与动态链接库文件EXPORTS节中的对应拼写相一致。

    如果GetProcAddress执行成功，则返回模块中函数入口处的地址，否则返回nil。

3.Freelibrary：从内存中移出库模块

    语法为：

    procedure Freelibrary(Module : THandle);

Module为库模块的句柄。这个值由Loadlibrary返回。

    由于库模块在内存中只装载一次，因而调用Freelibrary首先使库模块的引用计数减一。如果引用计数减为0，则卸出该模块。

    每调用一次Loadlibrary就应调用一次FreeLibray，以保证不会有多余的库模块在应用程序结束后仍留在内存中。

10.2.4.2 动态调用举例

    对于动态调用，我们举了如下的一个简单例子。系统一共包含两个编辑框。在第一个编辑框中输入一个字符串，而后在第二个编辑框中输入字符。如果该字符包含在第一个编辑框的字符串中，则标签框显示信息：“位于第n位。”，否则显示信息：“不包含这个字符。”。如图是程序的运行界面。

输入检查功能的实现在Edit2的OnKeyPress事件处理过程中，程序清单如下。

procedure TForm1.Edit2KeyPress(Sender: TObject; var Key: Char);

var

order: Integer;

txt: PChar;

PFunc: TFarProc;

Moudle: THandle;

begin

Moudle := Loadlibrary('c:\dlls\example.dll');

if Moudle > 32 then

begin

Edit2.text := '';

Pfunc := GetProcAddress(Moudle,'Instr');

txt := StrAlloc(80);

txt := StrPCopy(txt,Edit1.text);

Order := TInstr(PFunc)(txt,Key);

if Order = -1 then

Label1.Caption := '不包含这个字符 '

else

Label1.Caption := '位于第'+IntToStr(Order+1)+'位';

end;

Freelibrary(Moudle);

end;

    在利用GetProcAddess返回的函数指针时，必须进行强制类型转换：

Order := TInstr(PFunc)(text,Key);

    TInStr是一个定义好了的函数类型：

type

TInStr = function(Source: PChar;Check: Char): Integer;

10.3 利用DLLs实现数据传输

10.3.1 DLLs中的全局内存

    Windows规定：DLLs并不拥有它打开的任何文件或它分配的任何全局内存块。这些对象由直接或间接调用DLLs的应用程序拥有。这样，当应用程序中止时，它拥有的打开的文件自动关闭，它拥有的全局内存块自动释放。这就意味着保存在DLLs全局变量中的文件和全局内存块变量在DLLs没有被通知的情况下就变为非法。这将给其它使用该DLLs的应用程序造成困难。

    为了避免出现这种情况，文件和全局内存块句柄不应作为DLLs的全局变量，而是作为DLLs中过程或函数的参数传递给DLLs使用。调用DLLs的应用程序应该负责对它们的维护。

    但在特定情况下，DLLs也可以拥有自己的全局内存块。这些内存块必须用gmem_DDEShare属性进行分配。这样的内存块直到被DLLs显示释放或DLLs退出时都保持有效。

    由DLLs管理的全局内存块是应用程序间进行数据传输的又一途径，下面我们将专门讨论这一问题。

10.3.2 利用DLLs实现应用程序间的数据传输

    利用DLLs实现应用程序间的数据传输的步骤为：

    1. 编写一个DLLs程序，其中拥有一个用gmem_DDEShare属性分配的全局内存块；

    2. 服务器程序调用DLLs，向全局内存块写入数据；

    3. 客户程序调用DLLs，从全局内存块读取数据。

10.3.2.1 用于实现数据传输的DLLs的编写

    用于实现数据传输的DLLs与一般DLLs的编写基本相同，其中特别的地方是：

    1. 定义一个全局变量句柄：

var

hMem: THandle;

    2. 定义一个过程，返回该全局变量的句柄。该过程要包含在exports子句中。如：

function GetGlobalMem: THandle; export;

begin

Result := hMem;

end;

    3. 在初始化代码中分配全局内存块：

程序清单如下：

begin

hMem := GlobalAlloc(gmem_MOVEABLE and gmem_DDEShare,num);

if hMem = 0 then

MessageDlg('Could not allocate memory',mtWarning,[mbOK],0);

end.

    num是一个预定义的常数。

Windows API函数GlobalAlloc用于从全局内存堆中分配一块内存，并返回该内存块的句柄。该函数包括两个参数，第一个参数用于设置内存块的分配标志。可以使用的分配标志如下表所示。

表10.3 全局内存块的分配标志

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

标 志 意 义

—————————————————————————————————

gmem_DDEShare 分配可由应用程序共享的内存

gmem_Discardable 分配可抛弃的内存(只与gmem_Moveable连用)

gmem_Fixed 分配固定内存

gmem_Moveable 分配可移动的内存

gmem_Nocompact 该全局堆中的内存不能被压缩或抛弃

gmem_Nodiscard 该全局堆中的内存不能被抛弃

gmem_NOT_Banked 分配不能被分段的内存

gmem_Notify 通知功能。当该内存被抛弃时调用GlobalNotify函数

gmem_Zeroinit 将所分配内存块的内容初始化为零

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

    有两个预定义的常用组合是：

GHND = gmem_Moveable and gmem_Zeroinit

GPTK = gmem_Fixed and gmem_Zeroinit

    第二个参数用于设置欲分配的字节数。分配的字节数必须是32的倍数，因而实际分配的字节数可能比所设置的要大。

    由于用gmem_DDEShare分配的内存在分配内存的模块终止时自动抛弃，因而不必调用GlobalFree显式释放内存。


第十章 动态链接库编程（二）

DELPHI基础教程

第十章　动态链接库编程（二）

10.3.2.2 服务器程序的编写

    服务器程序必须包含对DLL的调用代码，如：

function GetGlobalMem: THandle; far; external 'c:\dlls\glbmem';

    通过调用该函数，服务器可以获得全局内存块的句柄。

    在写入数据前，服务器必须锁定全局内存，以避免在写入过程中Windows移动该内存块的位置。

    函数GlobalLock锁定全局内存并返回指向该内存块的指针：

pMem := GlobalLock(hMem);

    对pMem的任何修改都会反映到全局内存块中。

    对内存块进行操作后，调用GlobalUnLock进行解锁。内存块操作之后尽早解锁，有利于Windows充分利用内存资源。

    服务器写入数据的实现代码如下。

var

hMem: THandle;

pMem: PChar;

begin

hMem := GetGlobalMem; {获得全局内存块的句柄}

if hMem <> 0 then

begin

pMem := GlobalLock(hMem); {加锁全局内存块}

if pMem <> nil then

begin

StrPCopy(pMem,Memo1.text); {向全局内存块写入数据}

GlobalUnlock(hMem); {解锁全局内存块}

end

else

MessageDlg('Couldnot Lock memory block',mtWarning,[mbOK],0);

end;

10.3.2.3 客户程序的编写

    客户程序几乎是服务器程序的翻版。唯一的区别在于一个是写入数据，一个是下载数据。

下面是客户从全局内存块下载数据的程序清单。

var

hMem: THandle;

pMem: PChar;

begin

hMem := GetGlobalMem; {获得全局内存块的句柄}

if hMem <> 0 then

begin

pMem := GlobalLock(hMem); {加锁全局内存块}

if pMem <> nil then

begin

Memo1.text := StrPas(pMem); {从全局内存块读取数据}

GlobalUnlock(hMem); {解锁全局内存块}

end

else

MessageDlg('Couldnot Lock memory block',mtWarning,[mbOK],0);

end;

10.4 利用DLLs实现窗体重用

    实现窗体重用是Delphi DLLs功能中一个引人注目的特色。当你创建了一个令自己满意的通用窗体并希望能在不同应用程序中使用，特别是希望能在非Delphi 应用程序中使用时，把窗体做进一个动态链接库中是最适当的。这样即使用其它工具开发的应用程序，如C++、Visual Basic等，也都可以去调用它。

    包含窗体的DLLs有100K左右的部件库(Component Library)开销。可以通过把几个窗体编译成一个DLLs来最小化这笔开销。DLl中的不同窗体可以共享部件库。

10.4.1 利用DLLs实现窗体重用的一般步骤

    利用DLLs实现窗体重用的步骤是：

    1.在集成开发环境(IDE)中，按自己的需要设计一个窗体；

    2.编写一个用于输出的函数或过程。在该函数或过程中，设计的窗体被实例化；

    3.重复步骤1、2，直到完成所有重用窗体的设计；

    4.打开工程文件，进行修改，以适应生成 .dll文件的需要：

    (1).把保留字program设为library；

    (2).从uses子句中去掉Forms单元；

    (3).移去begin,end之间的所有代码；

    (4).在uses子句下，begin…end块之前，添加保留字exprots。exports 后是输出函数名或过程名。

    5.编译生成DLLs文件；

    6.在其它应用程序中调用重用窗体。

    重用窗体的调用同一般DLLs函数或过程的调用完全一致，不再赘述。读者可参看下面的例子。

10.4.2 窗体重用实例

    下面我们通过一个具体的实例来说明窗体重用的设计过程。我们在一个名为passform.dll 的文件中储存了一个口令设置窗口和一个口令检查窗口。而后在一个Delphi 编写的程序和一个VB编写的程序中进行调用。事实证明这种方法是完全可行的。

10.4.2.1 窗体重用DLLs的设计

    窗体重用DLLs的设计依照(10.4.1)中介绍的步骤进行。DLLs中的两个窗体 SetPassWordForm和GetPassWordForm分别用于设置和检查口令。它们的设计界面如图所示。

窗体类TSetPassWordForm定义了两个数据成员Verified和PassWord，用于记录口令确认状态和设置的口令。TSetPassWordForm的定义如下：

type

TSetPassWordForm = class(TForm)

Label1: TLabel;

Edit1: TEdit;

OKBtn: TBitBtn;

CancelBtn: TBitBtn;

procedure FormCreate(Sender: TObject);

procedure Edit1KeyPress(Sender: TObject; var Key: Char);

private

{ Private declarations }

Verified: Boolean;

public

{ Public declarations }

PassWord: PChar;

end;

    窗口生成时，对数据成员和部件状态进行初始化：

procedure TSetPassWordForm.FormCreate(Sender: TObject);

begin

Verified := False;

PassWord := StrAlloc(40);

OKBtn.Enabled := False;

Label1.Caption := 'Please Input PassWord:';

end;

    按钮OKBtn在程序启动时Enabled属性设置为False，直到口令被正确设置后Enabled属性才恢复为True。这样就保证了只有口令被正确设置后，口令设置窗口才能正常关闭。否则只能按Cancel按钮取消。

    在口令设置代码单元中定义了一个输出函数SetPassWord，用于生成口令设置窗口并返回设置的口令：

function SetPassWord(PWord: PChar): Boolean;

var

SetPassWordForm: TSetPassWordForm;

begin

Result := False;

SetPassWordForm := TSetPassWordForm.Create(Application);

try

with SetPasswordForm do

if ShowModal = mrOK then

begin

StrCopy(PWord,StrUpper(Password));

Result := True;

end;

finally

SetPasswordForm.Free;

end;

end;

    口令成功设置，把PassWord的值拷贝给PWord输出，并返回True。应该注意的是由于 PWord本身就是指针类型，指向一个字符串的地址，因而虽然PWord用于输出，但在参数表中仍为传值参数，而不是传址参数。另外调用函数StrCopy，要求PWord在传入前已分配内存，否则会导致一个一般保护错。try...finally用于保护窗口所占用内存资源在任何情况下都能正常释放，读者可参看第十二章。

    在口令设置窗口中，为了确保用户记住了设置的口令，在用户输入并按回车键后，要求用户再次输入进行确认。只有用户重新输入的字符串与原设置口令相同，口令设置窗口才能正常关闭 。否则将原设置口令清空，要求用户再次输入。以上功能的实现在编辑框的OnKeyPress事件处理过程中。

procedure TSetPassWordForm.Edit1KeyPress(Sender: TObject; var Key: Char);

begin

if Edit1.text = '' then Exit;

if Key = #13 then

begin

if Verified then

if StrPas(PassWord) = Edit1.text then

begin

OKBtn.Enabled := True;

Edit1.Enabled := False;

OKBtn.SetFocus;

end

else

begin

Verified := False;

MessageDlg('PassWord is InValid.',mtWarning,[mbOK],0);

Edit1.text := '';

PassWord := '';

Label1.Caption := 'Please Input PassWord:';

end

else

begin

Verified := True;

StrPCopy(PassWord,Edit1.text);

Edit1.text := '';

Label1.caption := 'Please Verify PassWord:';

end;

Key := #0;

end;

end;

    口令检查窗口的实现相对简单，只定义了一个输出函数GetPassWord，用于生成口令检查窗口并返回口令检查的结果。

function GetPassword(Password: PChar): Boolean;

var

GetPasswordForm: TGetPasswordForm;

begin

Result := False;

GetPasswordForm := TGetPasswordForm.Create(Application);

try

with GetPasswordForm do

if ShowModal = mrOK then

if UpperCase(Edit1.Text) <> StrPas(StrUpper(Password)) then

MessageDlg('Invalid Password', mtWarning, [mbOK], 0)

else

Result := True;

finally

PasswordForm.Free;

end;

end;

    PassWord为输入的参数，不能为空，由调用以上函数的程序负责维护。

    窗口中用户输入口令时回显在屏幕上的字符由编辑框的PassWordChar属性确定。

    在DLLs的工程文件中，把两个输出函数写到exports子句中。

library PassForm;

uses

GetPass in 'GETPASS.PAS' {PasswordForm},

Setpass in 'SETPASS.PAS' {SetPassWordForm};

exports

GetPassword,SetPassWord;

begin

end.

10.4.2.2 Delphi应用程序调用重用窗体

    在Delphi应用程序中调用重用窗体，首先必须包含passform.dll的两个输出函数：

function GetPassword(Password: PChar): Boolean;

far; external 'c:\dlls\PassForm';

function SetPassword(PassWord: PChar): Boolean;

far; external 'c:\dlls\PassForm';

    这位于程序单元的implementation部分。

口令设置部分的实现代码为：

procedure TForm1.SetButtonClick(Sender: TObject);

begin

PassWord := StrAlloc(40);

if SetPassWord(PassWord) = False then

MessageDlg('PassWord is not set',mtInformation,[mbOK],0);

end;

    首先为口令字符串分配内存。当口令设置窗体按Cancel按钮取消时，显示相应的信息。

    口令检查部分的实现代码为：

procedure TForm1.TestButtonClick(Sender: TObject);

begin

if PassWord = nil then

begin

MessageDlg('Set password first', mtInformation, [mbOK], 0);

SetButton.SetFocus;

Exit;

end;

if GetPassword(PassWord) then

Label1.Caption := 'You are Wellcome !'

else

Label1.Caption := 'Sorry,You are InValid User.';

end;

    根据口令检查的结果，在标签框中显示相应的信息。

10.4.2.3 VB应用程序调用重用窗体

    VB是微软公司极力推荐的一个可视化开发工具。它虽然并不支持动态链接库的创建，但可以调用标准的Windows API动态链接库和用其它语言编写的动态链接库。为了验证所生成DLLs的普适性，我们用VB开发了一个简单的程序来调用passform.dll中储存的窗体。

下面是VB程序的完整代码，和Delphi程序的对应部分基本一致。

Option Explicit

Declare Function GetPassWord Lib "c:\dlls\passform.dll" (ByVal PassWord As String) As Integer

Declare Function SetPassWord Lib "c:\dlls\passform.dll" (ByVal PassWord As String) As Integer

Dim PassWord As String * 40

Sub Check_Click ()

If PassWord = "" Then

MsgBox ("Enter sample password first")

SetPass.SetFocus

Else

If GetPassWord(PassWord) Then

StatusLbl.Caption = "You are Welcome!"

Else

StatusLbl.Caption = "Sorry,You are Invalid User."

End If

End If

End Sub

Sub SetPass_Click ()

If SetPassWord(PassWord) = 0 Then

MsgBox ("PassWord is not Set.")

End If

End Sub

    有关VB编程的一些具体问题，读者可参看有关的VB参考书。

10.4.3 小结

    本章我们讨论的是动态链接库编程。许多可视化开发工具（如Visual Basic）不支持 DLLs的创建，而Delphi在这里又有上乘的表现。特别是窗体重用机制是Delphi对Windows下DLLs编程的一个重大改进。在一般的DLLs编程中也体现了Delphi快捷、方便的特点。动态链接库是 Windows下程序组织的一种重要方式，使用动态链接库可以极大地保护用户在不同开发工具、不同时期所做的工作。利用动态链接库，用户可以逐步去构筑自己的程序模块库，为今后的工作积累素材。


第十一章 Delphi应用程序的应用（一）

DELPHI基础教程

第十一章　Delphi应用程序的应用（一）

11.1 Help文件的建立

    Help文件是Micosoft Windows3.0以上的版本提供的超文本帮助文件。利用这种超文本,用户可非常方便地使用帮助文件系统。帮助文件是以主题为主线进行编写的，一个主题可以跳转至相关的主题，也可按关键字进行主题查询。帮助文件与软件开发工具相结合，可实现应用程序的'上下文敏感'，而且帮助系统自动装入。“上下文敏感”是指根据程序当前执行代码来显示Help文件的相应部分。

    Windows提供的很多应用程序都有帮助系统，读者可以从这些系统中了解应用程序的许多信息。

11.1.1 建立Help文件所需的工具和文件

    程序员可为自己的应用程序建立帮助文件系统。但建立最基本的帮助系统, 必须有以下文件

    1. WinHelp 应用程序 ( WinHelp.exe) 。运行帮助系统实际上是运行用帮助源文件的

WindHelp程序。帮助文件只有通过WinHelp文件才能运行。

    2. 能创建主题的字处理器。这种处理器能以RTF格式保存文件, 能创建$,#,K,+脚标。RTF(Rich Text Format)格式是一个能记录各种文本特征的文件格式。这些特征包括字体大小、线型风格等。Microsoft Word 6.0处理器能满足以上要求。

    3. 一个能以ASCII格式保存文件的字处理器或编辑器，这是为了创建Help工程文件(.HPJ文件)。

    4. 帮助文件编译器(HCP.EXE或HC31.EXE)，两种编译器均能编译在Windows3.1 环境中使用的帮助文件，但不能编译Windows3.0环境下的帮助文件。HCP.EXE是保护模式的编译器，能更好地使用内存空间。要在Windows的Dos窗口中使用HCP.EXE编译器。

    5. 帮助编译器所需的错误信息源文件(HCP.ERR或HC31.ERR)。如果帮助文件在编译过程中出现错误，WinHelp运行时将提示有关的错误信息，而这些信息保存在HCP.ERR或HC31.ERR文件中。

    以下工具能实现帮助系统的高级特征：

    1. 热点(Hotspot)编辑器(SHED.EXE)；

    热点编辑器能创建分段超图像文件(.SHG)。这种文件包括一些分成多个热点的图像，当用户单击图像，将弹出一个窗口或跳转至另一主题。

    2. 多分辨率位图编译器(MRBE.EXE)；

    这种编译器能将具备多种分辨率的位图结合到一个文件中，以供WinHelp 使用。WinHelp检查显示器的分辨率, 然后以相应的分辨率加以显示。

    3. 图像编辑器，它能以位图形式保存图像文件。 使用图像编辑器创建说明和自定义按钮。

    4. 绘图软件。用以创建除了位图之外的元文件(WMF)；

    程序员可以直接把图像插入文本中，也可以用Windows剪贴板把图像粘贴至文本中。

11.1.2 Help文件的创建

    下面介绍最简单、最直接的创建Help文件的方法，假设在Word中创建主题。

    创建Help文件分以下4个步骤：

    1. 建立组成帮助文件的主题，并以RTF格式保存；

    2. 建立内容主题(Content Topic)，并以RTF格式保存；

    3. 建立帮助工程文件(.HPJ)以文本格式保存；

    4. 将工程文件编译成帮助源文件(.HLP)。

11.1.2.1 建立主题

    一个简单的帮助主题包括主题题目(Title)，主题文本(Text)，脚标，主题内容，全局查询、打印。主题最好是带有题目，题目写在主题的第一行。用不同的字体大小、颜色以示区别

写完题目后，可输入主题的文本。输入时不用担心每行的宽度。 编译好的帮助文件会根据窗口大小自动确定行宽。在主题的最后插入一个分页符，WinHelp把每页视为一个单独主题。

    书写主题文本时应注意尽可能地把文本写成小段落列表，这样能方便阅读； 同时要控制主题长度，这样用户不需要使用滚动条来阅读文本。

    在主题中应加入一些脚标， WinHelp 使用这些脚标辨识主题并提供一些导向控制 (Novigation Control)，四种典型脚标如表11.1所示。

表11.1 脚标以及用途

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

      符号　　适用于　　　　　　用　　　　途

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

     # 内容字符串　　　 唯一辨识主题

     $ 标题　　　　　　 在搜询对话框和搜询历史列表框中显示主题

     K 关键字(段)　　　 出在搜询对话框中

     + 浏览顺序　　　　 用户使用时的浏览顺序

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

    以下分别介绍四种脚标的插入方法：

    1. 插入#脚标。把光标移至主题的最前端插入#脚标。这时主题文本下端也会出现#，在此后键入内容字符串。WinHelp使用内容字符串作为唯一的辨识主题。用户永远也看不见这些字符串，但设计人员用它们定义跳转主题。

    2. 插入$脚标，把光标移至#脚标后，插入$脚标。在文本下端的$脚标处，输入主题的标题，该标题与第一行出现的标题一致，标题将会出现在搜询对话框和搜询历史对话框中

3. 插入K脚标。在主题第一行的脚标之后插入K脚标，在主题文本中的K 脚标后键入字段，这些字段将出现在搜询列表框中，见11.3图。

    4.插入+脚标。在主题第一行的K脚标之后插入+脚标。在主题文本以下的+ 脚标处键入浏览顺序标识符。标识符可以是一个数(如005)，或一组名字加上冒号和数(如 CAL C:005)一个主题只能有一个浏览顺序。

    热点是用户可以激发某种动作的文本或图像。一个热点可跳转至另一个主题。在其它窗口中显示主题或执行宏。多数情况下，重要字段被设计成热点以实现主题跳转。

    以下是实现主题跳转的步骤：

    1. 输入要跳转的字段或插入图像；

    2. 高亮度选择字段，用双下划线格式化。在MicroSoft Word中，按ALT +T 键弹出字符格式对话框，在列表中选择双下划线；

    3. 在紧挨在这些字段或图像之后，键入指定主题的字符串。 并对内容字符串进行隐藏格式化。这个内容字符串是跳转主题的内容字符串；

    根据以上步骤能实现主题之间的跳转。

    最后要把编辑的文件以RTF格式保存下来，WinHelp只能编译RTF文件。以下是典型的RTF文件：

#$+ Help Example Indexindex_info 1 of 2 index_2

Commands

Edit Menumenu_edit

File Menumenu_file

Glossary

Defined Termsglossary

Procedures

Copying Textproc_copying_text

Deleting Textproc_deleting_text

Exitingproc_exiting

Available From Your Application

Context Sensitive Topics

cs_topics

# main_index

$ Help Index

+ index:0005

11.1.2.2 建立内容主题

    内容主题列出了帮助系统的主要部分。用图标启动帮助系统或按Content按钮均出现内容主题。内容主题的每个项目都可跳转。

建立内容主题与建立一般主题类似，WinHelp默认第一个主题为内容主题。其建立步骤如下：

    1. 移至第一个源文件的开始处；

    2. 键入希望出现的主题标题，这些标题处于不同的行；

    3. 将每个主题设置成热点。

11.1.2.3 建立帮助工程文件

    帮助工程文件是一个文本文件。包含了有关帮助文件的许多信息。 编译器对工程文件进行编译。工程文件的扩展名必须是HPJ，编译后的扩展名是HLP:

下面是一个简单的帮助工程文件：

[OPTIONS]

CONTENTS=context_string

TITLE=title

COMPRESS=compress_level

ERRORLOG=log_filename

[CONFIG]

BrowseButtons()

[FILES]

RTF_filename_1

RTF_filename_2

RTF_filename_3

    [OPTIONS]

Context_String是内容主题的内容字符串。这一行并不是必须有的。 如果没有第一行，WinHelp把第一个帮助文件的第一个主题作为内容主题。

    TITLE = title

    title是帮助窗口的标题。不要将标题用引号括住。这一行也不是必需要有的。如果没有，缺省的标题是Windows Help.

CoMPRESS = Compress level

Compress_level决定工程文件在编译时是否被压缩， 压缩后的文件编译时要花较长的时间。

    表11.2 为Compress_level的取值：

表11.2 Compress_leve的取值及含义

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

    取值　 　　编译时间　　　　　　　文件大小

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

  FALSE 快　　　　　　　　 　大(无压缩)

MEDIUM 中等　　　　　　　　 中等(高度压缩)

HIGH 慢　　　　　 　　　　小(无压缩)

   0 　　　 　 快　　　　　 　　　 大(无压缩)

  1 　　　 　 慢　　　　　　 　　 小(高度压缩)

   No 快　　　　　　　 　 大(无压缩)

  TRUE　　 　 慢　　　　　　　　　 小(高度压缩)

   YES 慢　　　　　　　 　　 小(高度压缩)

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

    ERROR(LOG = log_filename)

log_filename是WinHelp运行时的错误输出文件。如果编译时工程文件出错，需要一个记录错误的文件。如果工程文件有这一行，WinHelp在运行时自动创建的文件，如果没有，错误将显示在屏幕上，但不存入任何文件中。

    BrowseButton()

如果有这一行，帮助按钮条中将出现>>和<<键，要实现顺序浏览， 还需在帮助文件中定义。详见11.1.2.1节中的插入+脚标。

    [FILEs]

RTF_filename是.RTF源文件名。所有的RTF文件构成整个帮助系统。每个RTF 应处在

不同的行。

    以下是工程文件的实例

; This help project requires hc 3.1

[OPTIONS]

errorlog = iconwrks.err

title = IconWorks Help

contents = CONTENTS

compress = false

oldkeyphrase = false

warning = 3

[FILES]

iconwrks.rtf

[MAP]

CONTENTS 1

EDITOR_KEYBOARD 2

EDITOR_COMMANDS 3

VIEWER_KEYBOARD 5

VIEWER_COMMANDS 6

DEFINING_COLORS 1000

EDITOR_FILE_MENU 1100

EDITOR_FILE_MENU 1101

EDITOR_FILE_MENU 1102

EDITOR_FILE_MENU 1103

EDITOR_FILE_MENU 1104

EDITOR_FILE_MENU 1105

EDITOR_EDIT_MENU 1200

EDITOR_EDIT_MENU 1201

EDITOR_EDIT_MENU 1202

EDITOR_EDIT_MENU 1203

EDITOR_EDIT_MENU 1210

EDITOR_EDIT_MENU 1211

EDITOR_EDIT_MENU 1212

EDITOR_VIEW_MENU 1108

EDITOR_VIEW_MENU 1109

EDITOR_VIEW_MENU 1110

EDITOR_VIEW_MENU 1111

EDITOR_VIEW_MENU 1112

EDITOR_VIEW_MENU 1111

EDITOR_TOOLS_MENU 1400

SELECT_TOOL 1401

PAINT_TOOL 1402

FILL_TOOL 1403

LINE_TOOL 1404

[WINDOWS]

main = "IconWorks Help", (0,0,1023,1023 ),,, (192,192,192 )

glossary = "IconWorks Help", (222,206,725,486 ),,, (192,192,192 ), 1

[CONFIG]

CB("glossary", "&Glossary", "JI(`iconwrks.hlp>glossary', `GLOSSARY')")

BrowseButtons()

11.1.2.4 编译帮助工程文件

    有两种编译器可以编译帮助工程文件：HCP.EXE ，H31.EXE。两种编译器编译的文件不能在Winddow3.0中使用，但能在Windows 3.1中使用。其中HCP.EXE是保护模式“编译器”，它能更好的使用内存。必须在Windows的Dos窗口中使用HCP.EXE。

    编译前要注意两个问题：

    1. 所有源文件必须以RTF格式保存；

    2. 下面的文件必须在同一个目录下

     ● 所有的.RTF文件

     ● 帮助编译器(HCP.EXE,HC31.EXE)

● 编译器错误信息源文件(HCD.ERR,HC1.ERR)

     ● 帮助工程文件(.HPF)

● 任何引用位图或SHED的文件(.BMP.SHG)

    如果以上文件不在同一目录中，必须在工程文件中定义相应的路径。

    编译要在Dos环境中进行，命令格式：

    Help_Compiler_rootname project_File_rootname

Help_Compiler_rootname是不带扩展名的编译器名字。project_file_rootname是不

带扩展名的帮助工程文件名，如：

    HCP MYHELP

11.2 Delphi应用程序的Help编程

    Delphi应用程序能够方便地应用帮助系统。程序可以动态地运行帮助系统。 对话框可以与帮助系统相联。

11.2.1 定义应用程序的帮助文件

    要在应用程序中使用帮助系统，必须有相应的帮助文件。程序可以编写自己的帮助文件， 也可以使用已有的帮助文件。 另外，要为应用程序定义帮助文件以便在用户需要帮助时应用程序能打开相应的帮助文件。

    在 Delphi 集成开发环境中选择“Options | Project” 菜单项， 系统弹出工程选择对话框，　再选择Application Options页面，在辅助文件中输入帮助文件名。


第十一章 Delphi应用程序的应用（二）

DELPHI基础教程

第十一章　Delphi应用程序的应用（二）

所有的应用程序都是TApplication的派生类。TApplication有三种方法调用在线帮助系统。

    HelpContext方法可调用WinHelp(关于Winhelp的内容见上节)。它把HelpFile 中的文件名和一个文本代码传递给WinHelp。HelpFile是TApplication的字符串类型的属性，专门用来存放Help文件的。如果HelpFile属性是空字符，HelpContext返回假值，其它情况均返回真值。

    下面的例子使用窗体上的一个按钮，当用户单击按钮，屏幕出现DATA.HLP文件中714号主题内容。

    procedure TForm2.Bin1Click(Snder : TObject)

begin

Application.HelpFile := DATAHLP;

Application.HelpContext(714)；

     end;

HelpJump方法可调用WinHelp。它传递HelpFile属性中的文件名和帮助文件的内容字符串(详见11.1节)。内容字符串是帮助文件中唯一辨识帮助主题的字符串。如果HelpFile 属性是空字符，HelpFJump返回假值，其它情况均返回真值。

    下面的例子使用了窗体上的一个按钮。当用户单击按钮， 帮助系统调出了 DELPH2.HLP文件中的Default属性。因为Default属性的内容字符串是VclDefaultProperty。

procedure TForm1.Tbn|Click(Sender : TObject)

begin

Application.HelpFile := 'DELPHI.HLP';

Application.HelpJump ('VclDefaultProperty');

end

HelpCommand方法能快速访问WinHelp函数中的各种命令。根据这些命令WinHelp执行不同的动作。表11.2是WinHelp函数的有关信息。

    BOOL WinHelp(hwd,LpszHelpFile,fuCommand,dwData)

表11.2 WinHelp的参数及含义

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

    参数　　　 　类型　　　　　描 述

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

    hwnd HWND 　　请求帮助的窗口

    LpszHelpFile LPSTK 待显示的帮助文件的文件名

    fuCommand UNIT 请求的帮助类型

    dwData DWORD 帮助所需的描述表或关键字

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

    HelpCOmmand向WinHelp传递fuCommand和dwData，fuCommand 是帮助类型可为表11.3中的列值之一。

表11.3 fuCommand的取值及含义

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

    值　　　　　 　　　 含 义

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

    HELP_LONTEXT 　显示dwData指定描述长的帮助信息

    HELP_CONTENTS 　显示帮助的内容主题

    HELP_SET_LONTENTS 如果dwData是Orol04则在一个弹出

                         式窗口中显示Help主题

    HELP_HELP PONHELP 显示Help应用程序的自身帮助，函

                         数忽略lpszHelpFile和dwData参数

    HELP_INDEX 显示帮助文件的索引

    HELP_KEY 显示dwData指定的关键字的帮助

    HELP_MULTIKEY 显示一个关键字的帮助，该关键字

                         在一个可变关键字表中

    HELP_QUIT 向Help应用程序报告文件不再使用

    HELP_SETNDEX 把dwData指定的描述符作为帮助文

                          件的当前索引

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

    dwData参数的含义依赖于fuCommand的设置，如果fuCommand为HELP_CONTEXT，dwData为一个32 位的无符号整数，它包含一个描述表标识号：如果 fuCommand 为 HELP_KEY，dwData 则为一个指定长指针，所指的字符串是帮助的关键字。 如果 fuCommand 为HELPMULLTIKEY，dwData则指向一个MULTIKEYHELP数据结构的长指针。

    下面的例子使用了窗体中的按钮。 当用户单击按钮帮助系统将显示指定文件的帮助内容主题。

    procedure TForm1.Bin1Click(Sender : TObject)

begin

Application.HelpFile := 'MyHlep.HLP'

Application.HelpCommand(HELP_CONTENTS,0);

end;

TApplication部件的OnHelp事件响应帮助事件。 当应用程序接收到一个所需的帮助

时，发生OnHelp事件。使用OnHelp事件可以在需要帮助时定义一些特殊过程。 以下的例子改变了应用程序的帮助文件，AppHelp函数用来处理OnCreate事件。

    function TForm1.AppHelp(Command.Word;Data : lontint) : Boolean

begin

if OpenDialog1.Exeeute then

Application HelpFile := OpenDialog1.FileName;

end;

11.2.2 通用对话框中使用帮助系统

    Delphi通用对话框中都能显示一个帮助按钮。如果程序显示了对话框中的帮助按钮，应该确保应用程序的帮助文件中有相应的主题。

    在通用对话框中使用帮助系统，要做到以下三点：

  1. 把对话框的Option|SHOWHelp属性设置成true，这样在程序运行时将出现帮助按钮。 ShowHelp 属性与其部件的名字相关， 例如字体对话框的 ShowHelp 属性称为fdShowHelp。

   2. 为对话框部件定义帮助文件。

3. 定义应用程序的文件名。

11.3 Delphi帮助提示(Hint)的应用

    使用Delphi集成开发环境时，用户常把鼠标置于程序部件上，如加速按钮，对齐按钮等。鼠标在部件上停留超过一定时间后，Delphi将会显示一个弹出窗口， 里面有部件名称和概述。

这就是Delphi的帮助提示。Delphi的应用程序可通过定义ShowHint 属性实现帮助提示。

11.3.1 帮助提示的显示

    ShowHint属性可应用于所有的控件和应用程序部件，控件的ShowHint 属性含义与程序的稍有不同。控件的ShowHint属性决定某一控件是否显示帮助提示，如果ShowHint 是真值，当用户把鼠标置于控件之上超过一定时间后，控件将出现帮助提示。如果是假值，则不出现提示。控件是否显示还决定于控件的ParentShowHind属性。如果 ParentShowHint是真值，控件的父类的ShowHint属性将决定控件是否显示帮助提示。 假如有一个分组框和一个检查框，分组框是检查框的父件。表11.3说明了子件与父件的 ShowHint，ParentShowHint属性设置对子件帮助提示的影响。

表11.3 Hint属性设置对帮助提示的影响

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

    分组框ShowHint 检查框ParentShowHint 检查框ShowHint 帮助提示

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

    T或F F T 显示

   T T F 显示

  F T T 不显示

    T或F 　　　　　　　 F F 不显示

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

                    T表示真值，F表示假值

    把控件的ShowHint属性设置成真值，系统自动将ParentShowHint设置成假值。应用程序部件的ShowHint属性可以决定整个程序的帮助提示是否有效。如果应用程序的ShowHint 属性为真，程序中各部件的帮助提示才有效， 但是否显示还要取决于部件的SHowHint, ParentShowHint及其父件的ShowHint属性，如果应用程序的ShowHint属性为假值，无论程序部件的属性如何设置，程序中所有帮助提示都无效。

    Hint属性是显示在帮助提示框中的文本字符。Hint属性应用于所有控件，包括应用程序部件菜单部件。因为应用程序部件不是可视部件，因此不能在Object Inspector 窗口中定义Hint属性。但可以在定义部件的Hint属性时同时定义应用程序部件的Hint 属性，定义时只需用“|"字符会分开，例如：

     Edit.Hint := 'Name |Enter Name in the edit box'

等价于

     Edit.Hint := Name;

Application.Hint := Enter Name in the edit box

应用程序的Hint属性可用在OnHint事件。

    如果只定义了一个值，Delphi把这个值同时赋给部件和应用程序部件的Hint属性。 如果应用程序的ShowHint属性为假值，所有的帮助提示将不显示， 但可以利用程序部件的Hint属性显示其它提示，如状态条等。

    当某一部件的SHowHint属性为真， 但又没有定义Hint 属性， 如果此时文件定义了Hint属性，则此部件将使用文件的Hint值。

11.3.2 OnHint事件

    当用户把鼠标放在某一部件，而该部件的Hint 值不为空值，此时发生OnHint事件。利用OnHint事件可以执行一些特殊的操作。

    最常用是利用OnHint事件显示状态条的标题，状态条是用面板来实现的。下面举例说明。

    这个例子使用了面板部件，菜单，一个编辑框。菜单可随意设计， 但需记住每个菜单项的Hint 值。 另外， 定义编辑框的Hint 值， 把面板置于窗体底部( 将Align 属性置于dBotton)把面板标题置于左端(将Alignment属性置于taleftJustify)。

    OnHint 事件是应用程序部件的事件，而应用程序部件是非可视部件，不能使用Object Inspector窗体定义事件，必须编写自己的OnHint事件。

    首先，在TForm1对象中宣称DisplayHint方法，并在单元的implementation部分编写实现代码。在DisplayHint方法中，把应用程序的Hint属性赋给面板的标题。另一个重要问题是必须把DisplayHint方法作为处理OnHint事件的方法。窗体的OnCreate事件的代码解决了这个问题。

    下面列出了程序的完整代码。当用户运行程序， 把鼠标置于菜单或部件之上，在窗体的状态条中将出现定义的提示。

Type

TForm1 = class(TForm)

Button1: TButton;

Panel1: TPanel;

Edit1: TEdit;

procedure FormCreate(Sender: TObject);

private

{ Private declarations }

public

procedure DisplayHint(Sender: TObject);

end;

var

Form1: TForm1;

implementation

{$R *.FRM}

procedure TForm1.DisplayHint(Sender: TObject);

begin

Panel1.Caption := Application.Hint;

end;

procedure TForm1.FormCreate(Sender: TObject);

begin

Application.OnHint := DisplayHint;

end;

11.4 自定义部件的帮助安装

    Delphi有一个功能强大的帮助搜询引擎，叫做多层帮助，能为自定义的部件提供“上下文敏感”帮助。多层帮助允许把自定义部件的多个帮助文件安装成Delphi 的帮助序列，以提供给用户一种内层访问帮助文件的方式，用户有三种方法访问帮助文件：

    1. 设计状态选中部件，然后按F1；

    2. 在自定义部件的Object Inspector窗口中按F1；

    3. 在Delphi帮助系统选择搜询主题。

    Delphi 在提供这种帮助机制时， 不需要编写额外的代码。 有些文件是自定义部件帮助系统所必须的， 以下介绍安装的具体步骤。

    安装所需的文件

    STEREO.PAS　　　　　自定义部件的源代码

STEREO.RES　　　　　自定义部件的资源文件

STEREO.DCR　　　　　工具调色板图标0

STEREO.HRJ　　　　　帮助工程文件

STEREO.RTF　　　　　帮助源文件

STEREO.HLP　　　　　自定义部件的帮助文件

STEREO.KUF　　　　　关键字文件

    安装步骤

11.4.1 安装关键字文件

1. 退出Delphi集成开发环境

2. 备份\delphi\bin\delphi.hdx

3. 运行HelpInst应用程序

4. 打开\delphi\bin\delphi.hdx

5. 选择keywords |Add菜单项并选择Sberee.buf

6. 选择File|Source菜单项

7. 退出HelpInst

8. 因为WinHelp需要知道STEREO.HLP的位置所以要做以下其中之一：

        a. 把STEREO.Hlp复制到\delphi:\bin\目录下；

        b. 在WinHELP.INI文件中加上stereo.hlp=\usehelp；

11.4.2 安装自定义部件

1. 进入Delphi集成开发环境

2. 选择Option|Install Components菜单项

3. 选择Add

4. 选择Browse

5. 输入\stereo

6. 选择OK

11.4.3 激活自定义部件帮助系统

   1. TstereoButton和TStereeSpeaker部件从部件调色板上的Sample页拖至窗口；

2. 选择TStereoButton部件并按F1,屏幕上出现关于TStereoButton的帮助信息；

3. 在Object Inspector窗体口选择IsOn属性并按F1，屏幕显示IsOn属性；

4. 在主菜单中选择Help|Topic菜单项，并搜询Stereo 主题， 屏幕将出现STEREO

.HLP的帮助内容。


第十二章 异常处理与程序调试（一）

DELPHI基础教程

第十二章　异常处理与程序调试（一）

    在应用程序开发中如何检测、处理程序的运行错误是一个很重要的问题。在 Delphi 的集成开发环境( IDE )中提供了一个完善的内置调试器，可以帮助你发现大部分程序错误。但并不是所有的错误都可以被发现，而且当程序涉及到与外设的数据交换或操作外设，如要求用户输入、读写磁盘等时，错误的发生是程序无法控制的，如输入非法字符、磁盘不能读写等。这些情况不仅会导致应用程序异常中止而且可能引起系统的崩溃。针对这些问题，Delphi同时提供了一套强大的异常处理机制。巧妙地利用它，可以使你的程序更为强健，使用更为友好。

    虽然Delphi为应用程序提供了一套缺省的自动异常处理机制，即当前模块发生错误后退出当前模块并给出错误信息，而并不立即引起应用程序的中止。但当应用程序执行的过程性很强时，仅仅利用这种方法是不够的，而且很容易导致程序执行的不可预测性。

12.1　Delphi异常处理机制与异常类

    Delphi异常处理机制建立在保护块(Protected Blocks)的概念上。所谓保护块是用保留字try和end封装的一段代码。保护块的作用是当应用程序发生错误时自动创建一个相应的异常类(Exception)。程序可以捕获并处理这个异常类，以确保程序的正常结束以及资源的释放和数据不受破坏。如果程序不进行处理，则系统会自动提供一个消息框。

    异常类是Delphi异常处理机制的核心，也是Delphi异常处理的主要特色。下面我们对异常类的概念和体系进行详细的介绍。

    Delphi提供的所有异常类都是类Exception的子类。用户也可以从Exception派生一个自定义的异常类。

    Exception类的定义如下，对于不常用的成员没有列出。

{SysUtils 单元中}

Exception = class(TObject)

private

FMessage: PString;

FHelpContext: Longint;

function GetMessage: String;

procedure SetMessage(const Value: String);

public

constructor Create(const Msg: String);

constructor CreateFmt(const Msg: String; const Args: array of const);. . .

destructor Destroy; override;

property HelpContext: Longint

property Message: String;

property MessagePtr: PString;

end;

Exception的一系列构造函数中最重要的参数是显示的错误信息。而数据成员中最重要的也是可被引用的消息字符串(message，messagePtr)。 这些信息分别对自定义一个异常类和处理一个异常类有重要作用。

    Delphi提供了一个很庞大的异常类体系，这些异常类几乎涉及到编程的各个方面。从大的方面我们可以把异常类分为运行时间库异常、对象异常、部件异常三类。下面我们分别进行介绍。

12.1.1 运行时间库异常类(RTL Exception)

    运行时间库异常可以分为七类，它们都定义在SysUtils库单元中。

12.1.1.1 I/O异常

    I/O异常类EInOutError是在程序运行中试图对文件或外设进行操作失败后产生的，它从Exception派生后增加了一个公有数据成员ErrorCode，用于保存所发生错误的代码。这一成员可用于在发生I/O异常后针对不同情况采取不同的对策。

    当设置编译指示{$I- } 时，不产生I/O异常类而是把错误代码返回到预定义变量IOResult中。

12.1.1.2 堆异常

    堆异常是在动态内存分配中产生的，包括两个类EOutOfMemory和EInvalidPointer。

表12.1　　堆异常类及其产生原因

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

异常类 引发原因

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

EOutOfMemory 没有足够的空间用于满足所要求的内存分配

EInvalidPointer 非法指针。一般是由于程序试图去释放一个业已释 放的指针而引起的

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

12.1.1.3　　整数异常

    整数异常都是从一个EIntError类派生的，但程序运行中引发的总是它的子类：EDivByZero，ERangeError，EIntOverFlow。

     表12.2　　整数异常及其产生原因

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

异常类 引发原因

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

EDivByZero 试图被零除

ERangeError 整数表达式越界

EIntOverFlow 整数操作溢出

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

    ERangeError当一个整数表达式的值超过为一个特定整数类型分配的范围时引发。比如下面一段代码将引发一个ERangeError异常。

var

SmallNumber: ShortInt;

X , Y: Integer;

begin

X := 100;

Y := 75;

SmallNumber := X * Y;

end;

    特定整数类型包括ShortInt、Byte以及与整数兼容的枚举类型、布尔类型等。例如：

type

THazard = ( Safety , Marginal , Critical , Catastrophic );

var

Haz: THazard;

Item: Integer;

begin

Item:= 4;

Haz:= THazard ( Item );

end;

由于枚举数越界而引发一个ERangeError异常。

    数组元素越界也会引发一个ERangeError异常，如：

var

Values: array[1..10] of Integer;

i: Integer;

begin

for i := 1 to 11 do

Values[i] := i;

end;

ERangeError异常只有当类型检查打开时才会引发。这可以在代码中包含{$R+} 编译指示或设置IDE Option|Project的Range_Checking Option选择框。

    EIntOverFlow异常类在Integer、Word、Longint三种整数类型越界时引发。如：

var

I : Integer;

a,b,c : Word;

begin

a := 10;

b := 20;

c := 1;

for I := 0 to 100 do

begin

c := a*b*c;

end;

end;

引发一个EIntOverFlow异常。

EIntOverFlow异常类只有在编译选择框Option|Project|Over_Flow_Check Option选中时才产生。当关闭溢出检查，则溢出后变量保留该类整数的最大范围值。

整数类型的范围如下表。

     表12.3 整数类型的范围

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

类型 范围 格式

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

Shortint -128 .. 127 有符号8位

Integer -32768 .. 32767 有符号16位

Longint -2147483648 .. 2147483647 有符号32位

Byte 0 .. 255 无符号8位

Word 0 .. 65535 无符号16位

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

12.1.1.4 浮点异常

    浮点异常是在进行实数操作时产生的，它们都从一个EMathError类派生，但与整数异常相同，程序运行中引发的总是它的子类EInvalidOp、EZeroDivide、EOverFlow、EUnderFlow。

     表12.4　浮点异常类及其引发原因

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

异常类 引发原因

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

EInvalidOp 处理器碰到一个未定义的指令

EZeroDivide 试图被零除

EOverFlow 浮点上溢

EUnderFlow 浮点下溢

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

    EInvalidOp最常见的引发原因是没有协处理器的机器遇到一个协处理器指令。由于在缺省情况下Delphi总是把浮点运算编译为协处理器指令，因而在386以下微机上常常会碰到这个错误。此时只需要在单元的接口部分设置全局编译指示{$N-}，选择利用运行时间库进行浮点运算，问题就可以解决了。

    各种类型的浮点数(Real、Single、Double、Extended)越界引起同样的溢出异常。这同整数异常类是不同的。

12.1.1.5 类型匹配异常

    类型匹配异常EInvalidCast当试图用As 操作符把一个对象与另一类对象匹配失败后引发。

12.1.1.6 类型转换异常

    类型转换异常EConvertError当试图用转换函数把数据从一种形式转换为另一种形式时引发，特别是当把一个字符串转换为数值时引发。下面程序中的两条执行语句都将引发一个EConvertError异常。

var

rl : Real;

int: Integer;

begin

rl := StrToFloat(' $140.48');

int := StrToInt(' 1,402 ');

end;

要注意并不是所有的类型转换函数都会引发EConvertError异常。比如函数Val当它无法完成字符串到数值的转换时只把错误代码返回。利用这一点我们在(6.2)节中实现了输入的类型和范围检查。

12.1.1.7 硬件异常

    硬件异常发生的情况有两种：或者是处理器检测到一个它不能处理的错误，或者是程序产生一个中断试图中止程序的执行。硬件异常不能编译进动态链接库(DLLs)中，而只能在标准的应用中使用。

    硬件异常都是EProcessor异常类的子类。但运行时间并不会引发一个EProcessor 异常。

     表12.5　　硬件异常类及其产生原因

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

异常类 引发原因

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

Efault 基本异常类。是其它异常类的父类

EGPFault 一般保护错。通常由一个未 初始化的指针或对象引起

EStackFault 非法访问处理器的栈段

EPageFault Windows内存管理器不能正确使用交换文件

EInvalidOpCode 处理器碰到一个未定义的指令。这通常意味着处理器

试图去操作非法数据或未初始化的内存

EBreakPoint 应用程序产生一个断点中断

ESingleStep 应用程序产生一个单步中断

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

    EFault、EGPFault 往往意味着致命的错误。而EBreakPoint、ESingleStep被Delphi IDE的内置调试器处理。事实上前边的五种硬件异常的响应和处理对开发者来说都是十分棘手的问题。

12.1.2 对象异常类

    所谓对象异常是指非部件的对象引发的异常。Delphi定义的对象异常包括流异常、打印异常、图形异常、字符串链表异常等。

12.1.2.1 流异常类

    流异常类包括EStreamError、EFCreateError、 EFOpenError、EFilerError、EReadError、EWriteError、EClassNotFound。它们的结构关系如下：

EStreamError

|---------- EFCreateError

|---------- EFOpenError

|---------- EFilerError

|--------- EReadError

|--------- EWriteError

|--------- EClassNotFound

      图12.1 流异常结构图

流异常在Classes库单元中定义。

    流异常引发的原因如表12.6。

表12.6　　流异常类及其产生原因

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

异常类 引发原因

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

EStreamError 利用LoadFromStream方法读一个流发生错误

EFCreateError 创建文件时发生错误

EFOpenError 打开文件时发生错误

EFilerError 试图再次登录一个存在的对象

EReadError ReadBuffer方法不能读取特定数目的字节

EWriteError WriteBuffer方法不能写特定数目的字节

EClassNotFound 窗口上的部件被从窗口的类型定义中删除

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

12.1.2.2 打印异常类

    打印异常类EPrinter当打印发生错误时引发。它在printers库单元中定义。例如你的应用程序试图向一个不存在的打印机打印或由于某种原因打印工作无法送到打印机时，就会产生一个打印异常。

12.1.2.3 图形异常类

    图形异常类定义在Graphic 库单元中，包括EInvalidGraphic和EInvalidGraphicOperation两类。

    EInvalidGraphic当应用程序试图从一个并不包含合法的位图、图标、元文件或用户自定义图形类型的文件中装入图形时引发。例如下面的代码：

    Image1.Picture.LoadFromFile('Readme.txt')；

    由于Readme.txt并不包含一个合法的图形，因而将引发一个EInvalidGraphic异常。

    EInvalidGraphicOperation当试图对一个图形进行非法操作时引发。例如试图改变一个图标的大小。

var

AnIcon: TIcon;

begin

AnIcon := TIcon.Create;

AnIcon.LoadFromFile('C:\WINDOWS\DIRECTRY.ICO');

AnIcon.Width := 100; { 引发一个图形异常 }

...

12.1.2.4 字符串链表异常

    字符串链表异常EStringListError、EListError在用户对字符串链表进行非法操作时引发。由于许多部件(如TListBox,TMemo,TTabSet,…)都有一个TStrings类的重要属性，因而字符串链表异常在部件操作编程中非常有用。

    EStringListError异常一般在字符串链表越界时产生。例如对如下初始化的列表框：

ListBox1.Items.Add('First item');

ListBox1.Items.Add('Second item');

ListBox1.Items.Add('Third item');

    则以下操作都会引起EStringListError异常：

ListBox1.Item[3] := ' Not Exist';

str := ListBox1.Item [3];

    EListError异常一般在如下两种情况下引发：

    1.当字符串链表的Duplicates属性设置为dupError时，应用程序试图加入一个重复的字符串；

    2.试图往一个排序的字符串链表中插入一个字符串。

12.1.3 部件异常类

12.1.3.1 通用部件异常类

    通用部件异常类常用的有三个：EInvalidOperation、EComponentError、EOutOfResource。其中EInvalidOperation、EOutOfResource在Controls单元中定义；EComponentError在Classes单元中定义。

    1.非法操作异常 EInvalidOperation

    EInvalidOperation 引发的原因可能有：

    ● 应用程序试图对一个Parent属性为nil的部件进行一些需要Windows句柄的操作

    ● 试图对一个窗口进行拖放操作

    ● 操作违反了部件属性间内置的相互关系等

    例如，ScrollBar、Gauge等部件要求Max属性大于等于Min属性，因而下面的语句：

    ScrollBar1.Max := ScrollBar1.Min-1;

   将引发一个EInvalidOperation异常。

    2.部件异常EComponentError

引发该异常的原因可能有：

    ● 在Register过程之外试图登录一个部件(常用于自定义部件开发中)

    ● 应用程序在运行中改变了一个部件的名称并使该部件与另一个部件重名

    ● 一个部件的名称改变为一个Object Pascal非法的标识符

    ● 动态生成一个部件与已存在的另一部件重名

3.资源耗尽异常EOutOfResource

当应用程序试图创建一个Windows句柄而Windows 却没有多余的句柄分配时引发该异常。

12.1.3.2 专用部件异常类

    许多部件都定义了相应的部件异常类。但并不是有关部件的任何错误都会引发相应的异常类。许多情况下它们将引发一个运行时间异常或对象异常。

    下面列出几个典型的部件异常类。

    1.EMenuError

非法的菜单操作，例如试图删除一个不存在的菜单项。这一异常类在Menus库单元中定义。

    2.EInvalidGridOpertion

    非法的网格操作，比如试图引用一个不存在的网格单元。这一异常类在Grids库单元中定义。

    3.EDDEError

    DDE异常。比如应用程序找不到特定的服务器或会话，或者一个联接意外中止。这一异常类在DDEMan库单元中定义。

    4.EDatabaseError，EReportError

    数据库异常(EDatabaseError)和报表异常(EReportError) 在进行数据库和报表操作出现错误时引发。有关数据库的问题请读者参阅本书第二编。

12.1.4 小结

    在这一节中重点介绍了Delphi提供的异常类体系。我们力求给读者一个清晰、全面的印象，使读者能在自己的程序开发中实际使用它们。为便于理解我们也提供了一些简单的说明性示例。虽然在具体的使用中读者还可能会碰到许多问题，但意识到应该用异常类来增强程序的健壮性却是程序设计水平走上新台阶的标志。

12.2 异常保护

    确保回收分配的资源是程序健壮性的一个关键。但缺省情况下异常发生时程序会在出错点自动退出当前模块，因此需要一种特殊的机制来确保即使在异常发生的情况下释放资源的语句仍能被执行。而Delphi的异常处理正提供了这种机制。

12.2.1 需要保护的资源

    一般说来需要保护的资源包括：

    ● 文件

    ● 内存

    ● Windows资源

    ● 对象

    比如下面一段程序就会造成1K内存资源的丢失。

var

APointer : Pointer ;

AInt , ADiv: Integer ;

begin

ADiv := 0;

GetMem ( APointer , 1024 );

AInt := 10 div ADiv ;

FreeMem ( Apointer , 1024 );

end;

由于程序从异常发生点退出从而FreeMem永远没有执行的机会。

12.2.2 产生一个资源保护块

    Delphi提供了一个保留字finally，用于实现资源的保护：

    {分配资源}

    try

{资源使用情况}

finally

{释放资源}

    end;

try…finally…end就形成了一个资源保护块。finally后面的语句是在任何情况下，不论程序是否发生异常，都会执行的。

    对于(12.2.1)中的例子如下代码即可确保所分配内存资源的释放：

var

APointer : Pointer ;

AInt , ADiv : Integer;

begin

ADiv := 0;

GetMem ( APointer , 1024 );

try

AInt := 10 div ADiv ;

finally

FreeMem ( Apointer , 1024 );

end;

end;

下面的例子摘自(6.4)节，是在文件拷贝中实现文件资源的保护：

procedure CopyFile(const FileName, DestName: TFileName);

var

CopyBuffer: Pointer;

TimeStamp, BytesCopied: Longint;

Source, Dest: Integer;

Destination: TFileName;

const

ChunkSize: Longint = 8192;

begin

Destination := ExpandFileName(DestName);

if HasAttr(Destination, faDirectory) then

Destination := Destination + '\' + ExtractFileName(FileName);

TimeStamp := FileAge(FileName);

GetMem(CopyBuffer, ChunkSize);

try

Source := FileOpen(FileName, fmShareDenyWrite);

if Source < 0 then

raise EFOpenError.Create(FmtLoadStr(SFOpenError, [FileName]));

try

Dest := FileCreate(Destination);

if Dest < 0 then

raise EFCreateError.Create(FmtLoadStr(SFCreateError, [Destination]));

try

repeat

BytesCopied := FileRead(Source, CopyBuffer^, ChunkSize);

if BytesCopied > 0 then

FileWrite(Dest, CopyBuffer^, BytesCopied);

until BytesCopied < ChunkSize;

finally

FileClose(Dest);

end;

finally

FileClose(Source);

end;

finally

FreeMem(CopyBuffer, ChunkSize);

end;

end;

程序的具体解释见 (6.4)节。

    在异常保护的情况下，当异常发生时，系统会自动弹出一个消息框用于显示异常的消息。退出当前模块后异常类自动清除。


第十二章 异常处理与程序调试（二）

DELPHI基础教程

第十二章　异常处理与程序调试（二）

12.3　异常响应

   异常响应为开发者提供了一个按自己的需要进行异常处理的机制。try …except …end形成了一个异常响应保护块。与finally不同的是：正常情况下except 后面的语句并不被执行，而当异常发生时程序自动跳到except，进入异常响应处理模块。当异常被响应后异常类自动清除。

    下面的例子表示了文件打开、删除过程中发生异常时的处理情况：

uses Dialogs;

var

F: Textfile;

begin

OpenDialog1.Title := 'Delete File';

if OpenDialog1.Execute then

begin

AssignFile(F, OpenDialog1.FileName);

try

Reset(F);

if MessageDlg('Erase ' +OpenDialog1.FileName + '?',

mtConfirmation, [mbYes, mbNo], 0) = mrYes then

begin

System.CloseFile(F);

Erase(F);

end;

except

on EInOutError do

MessageDlg('File I/O error.', mtError, [mbOk], 0);

on EAccessDenied do

MessageDlg('File access denied.', mtError, [mbOk], 0);

end;

end;

end.

    保留字on…do用于判断异常类型。必须注意的是：except后面的语句必须包含在某一个on…do模块中，而不能单独存在。这又是同finally不同的一个地方。

12.3.1 使用异常实例

    上面所使用的异常响应方法可总结为如下的形式：

    on ExceptionType do

{响应某一类的异常}

    这种方法唯一使用的信息是异常的类型。一般情况下这已能满足我们的需要。但我们却无法获取异常实例中包含的信息，比如异常消息、错误代码等。假设我们需要对它们进行处理，那么就必须使用异常实例。

    为了使用异常实例，需要为特定响应模块提供一个临时变量来保存它：

    on EInstance : ExceptionType do　　…

    在当前响应模块中我们可以象使用一个普通对象那样来引用它的数据成员。但在当前响应模块之外不被承认。

    下面的代码用于获取异常消息并按自己的方式显示它：

{窗口中包括一个ScrollBar部件，一个Button部件}

procedure TErrorForm.Button1Click(Sender: TObject);

begin

try

ScrollBar1.Max := ScrollBar1.Min-1;

except

on E: EInvalidOperation do

MessageDlg('Ignoring Exception:'+E.Message,

mtInformation,[mbOK],0);

end;

end;

12.3.2 提供缺省响应

    在异常响应模块中，一般我们只对希望响应的特定异常进行处理。如果一个异常发生而响应模块并没有包含对它的处理代码，则退出当前响应模块，异常类仍被保留。

    为了保证任何异常发生后都能在当前响应模块中被清除，可以定义缺省响应：

try

{程序正常功能}

except

on ESomething do

{响应特定异常}

else

{提供缺省响应}

end;

由于else可以响应任何异常，包括我们一无所知的异常，因此在缺省响应中最好只包括诸如显示一个消息框之类的处理，而不要改变程序的运行状态或数据。

12.3.3 响应一族异常

    诸如

        on ExceptionType do

的异常响应语句不仅可响应本类异常，而且可以响应子类异常。对于象EIntError、EMathError等系统不会引发的异常，它们将只响应其子类异常。而对于象

     on Exception do

这样的语句将会对任何异常进行响应。

    下面一段代码对整数越界异常进行单独处理，而对其它整数异常进行统一处理：

    try

{整数运算}

except

on ERangeError do

{越界处理}

on EIntError do

{其它整数异常处理}

end;

由于异常在处理后即被清除，因而上面的代码可保证不会使ERangeError异常被多次处理。假如颠倒两条响应语句的顺序，则ERangeError异常响应将永远没有被执行的机会。

    由于异常在处理后即被清除，因而当希望对异常进行多次处理时就需要使用保留字raise来重引发一个当前异常。

    下面的代码同时使用了异常响应和异常保护。异常响应用于设置变量的值，异常保护用于释放资源。当异常响应结束时利用raise重引发一个当前异常。

var

APointer: Pointer ;

AInt , ADiv: Integer;

begin

ADiv := 0;

GetMem ( APointer , 1024 );

try

try

AInt := 10 div ADiv ;

except

on EDivByZero do

begin

AInt := 0 ;

raise;

end;

end;

finally

FreeMem ( APointer , 1024 );

end;

end;

    上面一段代码体现了异常处理的嵌套。异常保护、异常响应可以单独嵌套也可以如上例所示的那样相互嵌套。

12.3.5 自定义异常类的应用

    利用Delphi的异常类机制我们可以定义自己的异常类来处理程序执行中的异常情况。同标准异常不同的是：这种异常情况并不是相对于系统的正常运行，而是应用程序的预设定状态。比如输入一个非法的口令、输入数据值超出设定范围、计算结果偏离预计值等等。

    使用自定义异常需要：

    1.自己定义一个异常对象类；

    2.自己引发一个异常。

12.3.5.1 定义异常对象类

    异常是对象，所以定义一类新的异常同定义一个新的对象类型并无太大区别。由于缺省异常处理只处理从Exception或Exception子类继承的对象，因而自定义异常类应该作为Exception或其它标准异常类的子类。这样，假如在一个模块中引发了一个新定义的异常，而这个模块并没有包含对应的异常响应，则缺省异常处理机制将响应该异常，显示一个包含异常类名称和错误信息的消息框。

    下面是一个异常类的定义：

    type

EMyException = Class(Exception) ;

12.3.5.2 自引发异常

    引发一个异常，调用保留字raise，后边跟一个异常类的实例。

    假如定义：

type

EPasswordInvalid = Class(Exception);

则在程序中如下的语句将引发一个EPasswordInvalid异常：

  If Password <> CorrectPassword then

raise EPasswordInvalid.Create('Incorrect Password entered');

    异常产生时把System库单元中定义的变量ErrorAddr的值置为应用程序产生异常处的地址。在你的异常处理过程中可以引用ErrorAddr的值。

    在自己引发一个异常时，同样可以为ErrorAddr分配一个值。

    为异常分配一个错误地址需要使用保留字at，使用格式如下：

    raise EInstance at Address_Expession;

12.3.5.3 自定义异常的应用举例

下面我们给出一个利用自定义异常编程的完整实例。

两个标签框(Label1、Label2)标示对应编辑框的功能。编辑框PassWord和InputEdit用于输入口令和数字。程序启动时Label2、InputEdit不可见。当在PassWord中输入正确的口令时，Label2、InputBox出现在屏幕上。此时Label1、PassWord隐藏。

设计时，令Label2、InputEdit的Visible属性为False。通过设置PassWord的PassWordChar可以确定输入口令时回显在屏幕上的字符。

自定义异常EInvalidPassWord和EInvalidInput分别用于表示输入的口令非法和数字非法。它们都是自定义异常EInValidation的子类。而EInValidation直接从Exception异常类派生。

下面是三个异常类的定义。

type

EInValidation = class(Exception)

public

ErrorCode: Integer;

constructor Create(Const Msg: String;ErrorNum: Integer);

end;

EInvalidPassWord = class(EInValidation)

public

constructor Create;

end;

EInvalidInput = class(EInValidation)

public

constructor Create(ErrorNum: Integer);

end;

EInValidation增加了一个公有成员ErrorCode来保存错误代码。错误代码的增加提供了很大的编程灵活性。对于异常类，可以根据错误代码提供不同的错误信息；对于使用者可以通过截取错误代码，在try...except模块之外来处理异常。

从以上定义可以发现：EInvalidPassWord和EInvalidInput的构造函数参数表中没有表示错误信息的参数。事实上，它们保存在构造函数内部。下面是三个自定义异常类构造函数的实现代码。

constructor EInValidation.Create(Const Msg: String; ErrorNum: Integer);

begin

inherited Create(Msg);

ErrorCode := ErrorNum;

end;

constructor EInValidPassWord.Create;

begin

inherited Create('Invalid Password Entered',0);

end;

constructor EInValidInput.Create(ErrorNum: Integer);

var

Msg: String;

begin

case ErrorNum of

1:

Msg := 'Can not convert String to Number';

2:

Msg := 'Number is out of Range';

else

Msg := 'Input is Invalid';

end;

inherited Create(Msg,ErrorNum);

end;

对于EInvalidInput，ErrorCode=1表示输入的不是纯数字序列，而ErrorCode=2表示输入数值越界。

口令检查是用户在PassWord中输入口令并按下回车键后开始的。实现代码在PassWord的OnKeyPress事件处理过程中：

procedure TForm1.PassWordKeyPress(Sender: TObject; var Key: Char);

const

CurrentPassWord = 'Delphi';

begin

if Key = #13 then

begin

try

if PassWord.text <> CurrentPassWord then

raise EInvalidPassWord.Create;

Label2.Visible := True;

InputEdit.Visible := True;

InputEdit.SetFocus;

PassWord.Visible := False;

Label1.Visible := False;

except

on EInvalidPassWord do

begin

PassWord.text := '';

raise;

end;

end;

Key:=#0;

end;

end;

同样，在InputEdit的OnKryPress事件处理过程中实现了输入数字的合法性检查：

procedure TForm1.InputEditKeyPress(Sender: TObject; var Key: Char);

var

Res: Real;

Code: Integer;

begin

if Key = #13 then

begin

try

val(InputEdit.text,Res,Code);

if Code <> 0 then

raise EInValidInput.create(1);

if (Res > 1) or (Res < 0) then

raise EInValidInput.create(2);

MessageDlg('Correct Input', mtInformation,[mbOk], 0);

Key := #0;

except

on E:EInValidInput do

begin

InputEdit.text := '';

MessageDlg(E.Message, mtWarning,[mbOk], 0);

end;

end;

end;

end;

由于异常响应后即被清除，所以要显示异常信息，需要另外的手段。在以上两段程序中我们采用了两种不同的方法：在口令合法性检查中，利用异常重引发由系统进行缺省响应；在输入数字合法性检查中，通过异常实例来获取异常信息并由自己来显示它。

以上所举的是一个非常简单的例子，但从中已可以发现：使用自定义异常编程，为程序设计带来了很大的灵活性。

12.3.6 利用异常响应编程

    利用异常处理机制不仅能使程序更加健壮，而且也提供了一种使程序更加简捷、明了的途径。事实上，使用自定义异常类就是一种利用异常响应编程的方式。这里我们再讨论几个利用标准异常类编程的例子。

    比如为了防止零作除数，可以在进行除法运算前使用if…then…else语句。但如果有一系列这样的语句则繁琐程度是令人难以忍受的。这时候我们可能倾向于使用EDivByZero异常。例如如下一段程序就远比用if…then…else实现简捷明了。

function Calcu(x,y,z,a,b,c:Integer):Real;

begin

try

Result := x/a+y/b+z/c ;

except

on EDivByZero do

Result := 0;

end;

end;

在(6.2.3)记录文件的打开与创建中就是利用异常响应来实现文件的打开或创建。

procedure TRecFileForm.OpenButtonClick(Sender: TObject);

begin

if OpenDialog1.Execute then

FileName := OpenDialog1.FileName

else

exit;

AssignFile(MethodFile,Filename);

try

Reset(MethodFile);

FileOpened := True;

except

on EInOutError do

begin

try

if FileExists(FileName) = False then

begin

ReWrite(MethodFile);

FileOpened := True;

end

else

begin

FileOpened := False;

MessageDlg('文件不能打开',mtWarning,[mbOK],0);

end;

except

on EInOutError do

begin

FileOpened := False;

MessageDlg('文件不能创建',mtWarning,[mbOK],0);

end;

end;

end;

end;

if FileOpened = False then exit;

Count := FileSize(MethodFile);

if Count > 0 then

ChangeGrid;

RecFileForm.Caption := FormCaption+' -- '+FileName;

NewButton.Enabled := False;

OpenButton.Enabled := False;

CloseButton.Enabled := True;

end;

总之，利用异常响应编程的中心思想是虽然存在预防异常发生的确定方法，但却对异常的产生并不进行事前预防，而是进行事后处理，并以此来简化程序的逻辑结构。

12.4　程序调试简介

    Delphi提供了一个功能强大的内置调试器(Integrated Debugger), 因而对程序的调试不用离开集成开发环境(IDE)就可以进行。

    程序错误基本可以分为两类，即运行时间错和逻辑错。所谓运行时间错是指程序能正常编译但在运行时出错。逻辑错是指程序设计和实现上的错误。程序语句是合法的，并顺利执行了，但执行结果却不是所希望的。

    对于这两类错误，调试器都可以帮助你快速定位错误，并通过对程序运行的跟踪和对变量值的监视帮助你寻找错误的真正原因和解决错误的途径。

    程序调试的主要内容可以概括为如下的几方面：

    1.调试的准备和开始；

    2.控制程序的执行；

    3.断点的使用；

    4.检查数据的值。

    程序调试只有用户实际上机操作才能真正掌握。在这一节中我们主要对调试中的主要问题和一些关键点进行介绍。至于一些很细小的问题相信读者可以在上机实际应用中掌握，因而没有列出。

12.4.1 调试的准备和开始

    在程序开发过程中程序编码和调试是一个持续的循环过程，只有在你对程序进行了彻底的测试后才能交付最终用户使用。为了保证调试的彻底性，在调试前应制定一个详细的调试计划。一般说来应该把程序划分为几个相对独立的部分，分别进行调试，以利于错误的迅速定位，确保每一部分程序都按设计的要求运行。

    调试计划准备好后就可以开始程序的调试。

    开始一个调试过程包括：

    1.编译时产生调试信息；

    2.从Delphi里运行你的程序。

    在程序调试过程中，程序的执行完全在你的控制之中。你可以在任何位置暂停程序的执行去检查变量和数据结构的值，去显示函数调用序列，去修改程序中变量的值以便观察不同值对程序行为的影响。

12.4.1.1 产生调试信息

    要使用内部调试器必须选中Option| Environment菜单References页的Integrated Debugging检查框。缺省情况下该框被选中。

    在开始调试前需要使用Symbols Debug Information(调试符号信息)编译工程文件。调试符号信息包含了一个符号表，能够使调试器在程序的源代码与编译器产生的机器代码间建立联系。这样在程序执行中可以同时查看对应的源代码。

    Delphi 在缺省情况下自动产生调试符号信息。在集成开发环境中的开关选项是Option|project菜单Compiler Options页的Debug Information and Local Symbols检查框。

    当产生的调试符号信息供内部调试器使用时，编译器把调试符号表储存在每个相应的.dcu文件中。

    如果希望在集成环境外使用Turbo Debugger，则需要把调试信息储存在最终的 .exe文件中。为此需要选定Option|Project菜单Linker页的Include TDW Debug Info检查框。

    由于储存调试信息大大增加了执行文件的大小，因而调试完成后应重新生成一个不包含调试信息的执行文件。

12.4.1.2 运行程序

    通过调试器(包括内置调试器)运行程序，当程序处于等待状态时，调试器可以获得控制，利用调试器的功能来检查当前程序的状态。通过合理布置屏幕显示，使应用程序运行窗口和Code Editor(代码编辑器)互不重叠，可以让用户在它们间方便地切换以观察代码执行的效果。

    如果希望使用命令行参数来调试程序，则可以通过Run|Parameters 菜单打开运行参数对话框进行设置。

12.4.2 程序运行的控制

    程序运行控制的方法和使用如下表。

     表12.7　　程序运行控制的方法和使用途径

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

方法 使用途径

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

运行到光标位置 ● Code Editor加速菜单的Run to Cursor项

(Run to Cursor) ● Run主菜单的Run to Cursor项

● F4

跟踪(Trace Into) ● Run主菜单的Trace Into项

● Trace Into加速按钮

● F7

步进(Step Over) ● Run主菜单的Step Over项

● Step Over加速按钮

● F8

运行到断点 设置断点并按正常方式运行

暂停程序执行 Run主菜单的Program Pause项

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

    跟踪和步进都是一种单步执行方式。但“步”的含义不同。对跟踪而言它一次执行一条简单程序语句。当碰到包含调试信息的函数或过程调用时则跳入该函数或过程，并执行其第一条可执行语句。对步进而言它一次执行一条当前模块的可执行语句，而不管该语句是否是函数或过程调用。

    运行到光标位置和运行到断点都是程序正常运行到某一确定的源代码位置，而后进入调试状态。但相对于运行到光标位置而言，运行到断点更为灵活。因为断点一次可设置多个，同时也可以对断点设置一定的条件。只有满足该条件程序运行才会中止。


第十二章 异常处理与程序调试（三）

DELPHI基础教程

第十二章　异常处理与程序调试（三）

12.4.3 断点的使用

12.4.3.1 设置断点

    设置断点首先在Code Editor中选定你想设置断点的代码行，而后进行如下的任一种操作：

    ● 单击选定代码行左边的空白

    ● 按F5

    ● 选择Code Editor加速菜单的Toggle BreakPoint项

    ● 选择Run|Add Breadpoint打开断点编辑对话框(Edit BreakPoint Dialog Box)，而后选择New去确认一个新的断点设置或选择Modify去对一个存在的断点进行修改

    ● 从BreakPoint List加速菜单中选择Add BreakPoint项

    断点必须位于可执行代码行上，凡设置在注释、空白行、变量说明上的都是无效的。另外，断点既可以在设计状态下设置也可以在运行调试状态下设置。

12.4.3.2 断点的操作

    断点列表窗口(BreakPoint List Window)列出了所有断点所在的源文件名、行号、条件以及已通过的次数。如果一个断点非法或失去功能，则在列表窗口中变灰。

    断点列表窗口可以通过选择View|BreakPoint菜单打开。

断点列表窗口是断点操作的基础。

    1.显示和编辑断点处的代码

    利用断点列表窗口可以快速找到断点在源代码中的位置。

    首先选定断点而后从加速菜单中选择View Source或Edit Source。此时Code Editor更新，显示该断点位置处的代码。如果选择的是View Source，则断点列表窗口仍保持活动；如果选择的是Edit Source，则Code Editor获得输入焦点，可以在断点位置修改源代码。

    2.断点功能的丧失和恢复

    使断点失去功能可以使断点从当前程序运行中隐藏起来。假如你定义了一个断点当前并不需要，但可能在以后使用，则这一功能是很有用的。

    断点列表窗口加速菜单的Disable BreakPoint和Disable All BreakPoints项可以使当前选中断点或所有断点失去功能。

    加速菜单中的Enable BreakPoint和Enable All BreakPoint 可以使相应断点恢复功能。

    3.断点的删除

    断点删除可以从Code Editor或断点列表窗口中进行。

    从Code Editor:

    ● 把光标停到包含断点的行并按F5(或选择加速菜单的Toggle BreakPoint)

    ● 单击包含断点行左边的终止符

    从断点列表窗口：

    ● 选中欲删除的断点并选择加速菜单的Delete BreakPoint项

    ● 删除当前所有断点，则选择加速菜单的Delete All BreakPoints项

12.4.3.3 修改断点属性

    断点列表窗口双击选定断点或从加速菜单中选择Edit BreakPoint项，可以打开断点编辑对话框，用于显示和修改断点的属性。

利用断点编辑对话框可以改变断点的位置，设置断点条件。

    断点条件包括两种：布尔表示式和通过次数。

    Condition编辑框用于设置布尔表达式条件。如果表达式值为真(或非零)则程序运行在断点处中止；否则调试器将忽略该断点。

    Pass Count编辑框用于设置通过次数条件，即只有当程序运行在该断点处通过设定次数时程序运行才在该断点处中止。这往往用于对循环体内语句的调试。

    有一点应引起注意的是：当Condition和Pass Count同时设置时，Pass Count是指满足条件的通过次数。

    对如下一段程序：

var

i,Re,s: Integer ;

begin

s := 1;

Re := 0;

for i:=1 to 100 do

Re:=Re+s*i ;

end;

在 Re := Re + s*i;　一行设置一断点。

若条件设置为：

   Condition ：　　i = 3

Pass Count：　　 4

    则当程序中止时检测i 的值为7。

12.4.3.4 断点和程序执行点颜色的设置

    选择Option|Environment进入环境设置对话框而后选择Editor Colors页标签。此时即可对有关项按自己的希望设置背景和前景色。

12.4.4 监视数据的值

    内置调试器提供了如下的工具用于监视程序中数据的值：

    ● 监视列表窗口

    ● 计算/修改对话框

    ● 调栈窗口

12.4.4.1 监视表达式

    监视列表窗口(Watch List Window)显示程序运行中当前监视表达式的值。

    选择View|Watches可以打开监视列表窗口。

从Code Editor中添加一个监视表达式最方便的方法是：

    1.选中要监视的表达式；

    2.从Code Editor加速菜单中选择Add Watch把表达式添加到监视列表窗口。

    也可以利用下面的方法产生一个监视表达式：

    1.用下列方法之一打开监视属性对话框(Watch Properties Dialog Box)：

●主菜单中选择Run|Add Watch

●在光标处从Code Editor加速菜单中选择Add Watch

    ●按Ctrl－F5

    ●双击监视列表窗口中的一个监视表达式

    ●从监视列表窗口选定一个表达式而后从加速菜单中选择Edit

    2.在监视属性对话框的Expression下拉列表框中输入或选择一个被监视的表达式；

    3.设定表达式的显示格式和使能状态。

    与断点类似，利用加速菜单也可以使监视表达式功能丧失、恢复或删除监视表达式。

12.4.4.2 计算/修改表达式

  选择Run|Evaluate /Modify可打开计算/修改对话框。当单击Evaluate按钮时，Expression编辑框中表达式的值显示在Result域中。

    Expression中可以输入或选择任何合法的表达式(包括对象的属性)，但不包括；

    1.包含有当前执行点不能引用的局部或静态变量的表达式；

    2.函数或过程调用。

    Expression中的表达式可以带特定的格式字符用于规定其显示格式。 格式字符及其功能如下表。

     表12.8　　格式字符及其功能

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

格式字符 功 能

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

$,H,X 以十六进制格式显示标量

D 以十进制格式显示标量

C 把ASCII码在0..31的特殊字等显示为ASCII码图形

Fn 用n个有效数字显示浮点数

M 以十六进制方式显示一变量的内存转储值

P 以段和偏移量格式显示指针。两部分皆为四位十六进制值

R 显示记录、对象的域名和值(例如 X：5，Y：2)

S 用ASCII码显示字符串(包括特殊字符)。用于修改内存转储值

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

    修改表达式的值常用于验证错误解决方案的正确性。在Expression编辑框中输入或选定欲修改的表达式，单击Evaluate按钮观察表达式的当前值。而后在New Value编辑框中输入或选中一个新值，并单击Modify按钮确认并更新数据项。这种修改只影响特定的程序运行。

    修改表达式的值(特别是指针变量和数组下标)可能会引起无法预计的后果。因而使用中要特别小心。

12.4.4.3 显示函数调用

    选择View|Call Stack可以显示调栈窗口(Call Stack Window)。调栈窗口的顶端列出了应用程序最近的函数调用。

利用调栈窗口可以退出当前跟踪的函数，可以利用加速菜单项显示或编辑位于特定函数调用处的源代码

12.5　其它调试工具

    Delphi的内置调试器虽然功能很强大，但并不能胜任所有的任务。同时由于内置调试器在执行中引起程序环境的细微变化，所以可能影响错误的发生方式。为此我们需要使用其它调试工具来完成我们的任务。这些调试工具包括Turbo Debugger、WinSight、WinSpector和Browser。Browser将在下一节中专门进行介绍。

12.5.1 Turbo Debugger

Turbo Debugger是Borland公司推出的第三代语言调试器，它虽然还没有推出完全支持Delphi的新版本，但也基本能胜任一般Delphi程序的调试。

    Turbo Debugger在字符模式下执行，但它是一个真正的Windows程序，它仅使用基于字符的界面。由于Turbo Debugger是一个准备控制其它程序的特殊程序，因此不可以使用通常Windows任务切换功能如Alt+Tab。

Turbo Debugger的操作大部分与内置调试器相同或类似。利用File|Open菜单装入要调试的文件就可以开始一个调试过程。

    利用Turbo Debugger必须把调试符号信息储存在可执行文件中。具体操作见(12. 4.1.1)中介绍。

    Turbo Debugger与内置调试器相比，有许多新的功能。

    首先它提供了许多在低级硬件信息方面的控制。可以完全访问CPU的寄存器、标志及系统内存。用户可以跟踪远指针到内存位置并直接检查它们的内容。Turbo Debugger可以跟踪进到代码中，即使得不到源代码也可以。

    Turbo Debugger支持许多Windows的特殊功能。它可以跟踪Windows消息，让用户查看程序的局部堆和全局堆，并可以浏览包括DLLs在内的组成程序的所有代码单元列表。

    另外Turbo Debugger支持远程执行能力。可以通过串口链接或通过支持NetBIOS的网络配置Turbo Debugger控制另外一台机器。在这种模式下，一台机器显示调试器屏幕，另一台机器显示被跟踪的程序。这允许在一个屏上单步执行程序并在另一屏上监视结果。

12.5.2 WinSight

    WinSight 是一个用于查看Windows 对象并跟踪消息的发送和接收的调试工具。WinSight的图标可以在Delphi程序组中找到。

WinSight界面分为两部分，上面为对象树窗口，下面为消息跟踪窗口。如图12.9所示。

    在实际应用中，用户可能只是对其中的一部分消息感兴趣，而又不希望它们淹灭在无用信息之中。为此，用户可以打开Message菜单并选择Selected Windows。按住Shift键，单击对象树窗口中感兴趣的对象，所选定对象的任何消息都显示在消息跟踪窗口中。

    如果用户只想跟踪某些消息类，则打开Message菜单并选择Options ，使用如图12.10所示的检查框过滤消息。

12.6.6 小结

    本章介绍的内容，核心是如何增强程序的健壮性并提高开发效率。为此我们首先考察了Delphi的异常处理机制，而后介绍了几种程序调试工具，在您即将结束基础篇的学习时，这些内容是您步入开发大型应用程序的高级程序员行列的必备武器。


第十三章 Delphi开发数据库应用程序概述（一）

DELPHI基础教程

第十三章　Delphi开发数据库应用程序概述（一）

13.1 数据库系统概述

数据库系统为我们提供了一种把与我们的工作和生活紧密相关的信息集合在一起的方法，它还提供了在某个集中的地方存储和维护这些信息的方法。数据库系统主要由三大部分组成：数据库管理系统（DBMS:它是专门负责组织和管理数据信息的程序）、 数据库应用程序（它使我们能够获取、显示和更新由DBMS存储的数据）、数据库（按一定结构组织在一起的相关数据的集合）。

一般来说，DBMS和数据库应用程序都驻留在同一台计算机上并在同一台计算机上运行，很多情况下两者甚至结合在同一个程序中，以前使用的大多数数据库系统都是用这种方法设计的。但是随着DBMS技术的发展，目前的数据库系统正向客户/服务器模式发展。客户/服务器数据库将DBMS和数据库应用程序分开，从而提高了数据库系统的处理能力。数据库应用程序运行在一个或多个用户工作站（客户机）上，并且通过网络与运行在其它计算机上（服务器）的一个或多个DBMS进行通信。

   下面是数据库系统中一些概念和述语。

13.1.1 数据库管理系统（DBMS）

数据库管理系统（DBMS）是用于描述、管理和维护数据库的程序系统，是数据库系统的核心组成部分。它建立在操作系统的基础上，对数据库进行统一的管理和控制。其主要功能有：

1. 描述数据库:描述数据库的逻辑结构、存储结构、语义信息和保密要求等。

2. 管理数据库：控制整个数据库系统的运行，控制用户的并发性访问，检验数据的安 全、保密与完整性，执行数据检索、插入、删除、修改等操作。

3.维护数据库:控制数据库初始数据的装入，记录工作日志，监视数据库性能，修改更新数据库，重新组织数据库，恢复出现故障的数据库。

4.数据通信 :组织数据的传输。

DBMS主要有四种类型:文件管理系统、层次数据库系统、 网状数据库系统和关系数据库系统。因为目前关系数据库系统应用最为广泛，所以我们重点对关系数据库系统中的几个概念进行介绍。

关系数据库（Relational Database）:一个关系数据库是由若干表组成。在Delphi中，数据库概念对应到物理文件上是有一些不同的。对于dBASE、FoxPro、Paradox这三种数据库系统，数据库对应于某一个子目录，而其它类型如MS Access、Btrieve则是指某个文件。这是因为前者的表为单独的文件，而后者的表是聚集在一个数据库文件中的。

表（Table）:一个表就是一组相关的数据按行排列，象一张表格一样。比如一个班所有学生的期末考试成绩，存在一个表中，每一行对应一名学生，在这一行中，包括学生的学号、姓名以及各门课程的成绩。

字段（Field）:在表中，每一列称为一个字段。每一个字段都有相应的描述信息，如数据类型、数据宽度等。

记录（Record）:在表中，每一行称为一条记录。

索引（Index）:为了加快访问数据库的速度，许多数据库都使用索引。

13.1.2 数据库应用程序

DBMS中存储了大量的数据信息，其目的是为用户提供数据信息服务，而数据库应用程序正是与DBMS进行通信，并访问DBMS中的数据，它是DBMS实现其对外提供数据信息服务这一目的的唯一途径。简单地说，数据库应用程序是一个允许用户插入、修改、删除并报告数据库中的数据的计算机程序。数据库应用程序在传统上是由程序员用一种或多种通用或专用的程序设计语言编写的，但是近年来出现了多种面向用户的数据库应用程序开发工具，这些工具可以简化使用DBMS的过程，并且不需要专门编程。Delphi就是一种强有力的数据库应用程序开发工具。

用来生成数据库应用程序的语言主要分为三大类型:

1.过程化语言

标准的计算机程序设计语言如Pascal、Basic和C都是过程化语言，这些语言可以通过某种“应用程序接口”（API）来创建数据库应用程序，这种API由一组标准的函数（或调用）组成，这些函数和调用则扩展了语言的功能，使之能访问数据库中的数据。当程序设计人员用过程化语言创建数据库应用时，必须把应用的代码编写成一系列的过程，每个过程执行应用的某一部分的工作，如一个过程查询数据库，而另一过程更新数据库中的数据，然后不同的过程通过其他的用户界面过程（例如菜单系统）联系在一起，并且在应用中的适当地方运行。

上述这些过程化语言一般用来创建非数据库应用程序，它们通常被称为“第三代语言”（3GL）。还有一些过程化程序设计语言是某种特定的DBMS专用的， 这些语言一般被称为“第四代语言”（4GL），即数据库专用语言。常见的数据库专用的过程化语言如dBASE语言，Paradox数据库的PAL语言等等。

2.结构化查询语言（SQL）

结构化查询语言（Structure Query Language）是基于关系模型的数据库查询语言，它是一种非过程化的程序语言，也就是说，没有必要写出将如何做某事情，只需写出做到什么就可以了。写出的语句可看作是一个问题，称为“查询”（Query），针对这个查询，得到所需的查询结果。下面是一个例子：

Select Name,Total from Class where Total>600

这个查询意为从数据库表Class中将总分（Total）大于600的所有人选出来， 并列出他们的姓名(Name)和总分(Total)。

把SQL描述为子语言更适当一些，因为它没有任何屏幕处理或用户输入/输出的能力。它的主要目的是为了提供访问数据库的标准方法，而不管数据库应用的其余部分是用什么语言编写的，它既是为数据库的交互式查询而设计的（因此被称为动态SQL）， 同时也可在过程化语言编写的数据库应用程序中使用（因此被称为嵌入式SQL）。

3.其他语言

用于开发数据库应用程序的语言中，还可以使用目前数常见的“面向对象程序设计”（OOP）语言，如C++、 Objact Pascal等，OOP代表了一种完全不同的程序设计方法， 在这种程序设计方法中，活动被定义为在“对象”上发生的操作，而不是作为一系列过程来定义的。在数据库应用程序中使用OOP语言的情况正在不断增加。

开发数据库应用程序使用的另一种语言是“宏”语言。宏语言不是一种完全的程序设计语言，它实际上是一个用户手工输入的表，这个表被输入到应用程序中，以便自动执行一定的任务。对于某个特定应用的高级语言，宏语言通常可以在低档DBMS软件中或数据库服务器的前端中找到。

最后，还有一种“Query-By-Example”（QBE，范例查询）语言。严格地讲QBE不是一种语言，它是面向用户提供了一个或多个空表的界面，这些空表对应于数据库中的表。用户可以通过键盘选择需要查询的列，并在适当的列中填入条件从而定义查询的检索条件，然后DBMS就把QBE转换成相应的动作，以完成用户要求的查询任务。

13.2 Delphi的数据库特性及功能简介

直到目前为止，计算机软件的开发分为两个不同的体系，其中一个体系是使用传统的程序设计语言（如Pascal、Basic和C等）开发数值控制、数值运算等软件，围绕它们的重点是算术、数据结构以及近年产生的面向对象技术。另一个体系则是通用的数据库管理软件领域（数据库应用程序的开发）。这两个体系的发展都极为迅猛，但是二者并没出现混合渗透迹象。如果使用数据库语言进行传统的算术编程，虽然也能完成相应的功能，但是其编程过程可能极为复杂。如果使用传统的编程语言进行数据库编程，通过调用专用的数据库应用程序接口函数和过程，利用这些函数和过程提供的功能，可能也能做得比较完善，但这做起来大多是极其困难的。而Delphi结合了两个体系的优点，它结合了传统的编程语言Object Pascal和数据库语言的强大功能， 它即可以用于传统的算术编程又可以用于数据库编程，特别是Delphi具有强大的数据库功能，利用Delphi的数据库工具，我们根本不需要编写任何Object Pascal代码便可以创建一个简单的数据库应用。

Delphi是Borland公司于1994年底发布的用于开发数据库应用程序的工具， 它是面向对象的，它是目前开发客户/服务器数据库应用程序的强有力的工具。Delphi在Window3.1以上版本的系统环境下运行，目前具有两个版本：Delphi的标准版本和客户/服务器版本。标准版本包含一个Borland Database Engine的局部拷贝，它允许用户创建能访问dBASE、Paradox和Local InterBase 服务器的数据库应用， 它还支持具有 ODBC 接口的数据库。Delphi的客户/服务器版本包括Borland SQL Link， 它能直接访问 ORACLE 、 SyBase 和Microsoft SQL Server,Informix以及InterBase数据库服务器。

Delphi可以访问多种数据库管理系统的数据库，凭借窗体(Forms)和报表(Reports)，BDE(Borland Database Engine)可以访问诸如Paradox、dBASE、本地InterBase 服务器的数据库，也可以访问远程数据库服务器上的数据库(如ORACLE、SyBase、Informix等客户/服务器数据库中的数据库)，或任何经ODBC(Open Database Connecticity) 可访问的数据库管理系统中的数据库。

13.2.1 Delphi的数据库特性

跟其他的应用程序一样，Delphi提供了许多部件以方便地创建数据库应用程序。数据库对象的数据成员既可在设计阶段设置，也可在运行阶段通过程序代码进行设置。Delphi的部件板上提供了两页数据库应用程序开发中所要使用的部件：

数据访问页（Data Access Page）上的部件用于直接访问数据库中的数据库表。

数据控制页（Data Control Page）上的部件用来与用户交互，显示、 修改数据库中的数据。

数据库应用程序首先是利用Delphi提供的数据库部件与BDE建立联系，然后再通过BDE与数据库联系。下图阐述了Delphi的数据库工具和部件、Delphi数据库应用程序与BDE 、数据源之间的关系。

下表概括了Delphi的数据库特性：

表13.1 Delphi的数据库特性

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

   工具和部件 　 主 要 用 途

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

  Data Access Components 　访问数据库、数据库表、存贮过程等

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

  Data Control Components 　与用户交互，提供显示、修改数据库中数据的界面

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

  Database Desktop(DBD) 　建立、索引、查询数据库表以及访问、编辑来自各数据

     中的数据

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

  ReportSmith 　建立、浏览和打印数据库表中的数据

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

  Borland Database Engine 　数据库应用通过BDE访问dBASE Paradox数据库中的数据

   (BDE) 　和本地InterBase数据库服务器中的数据

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

   BDE Configuration 　建立和管理BDE与数据库建立连接时所使用的数据库的

   Utility 　别名

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

     它是一个单用户、多例程的本地SQL数据库服务器，可

  Local InterBase Server 　在单机环境下用来开发或测试客户/服务器数据库应用

     程序，然后再将之扩展成一个访问远程数据库服务器如

     ORACLE、SyBase、Informix等

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

  InterBase SQL Link 　连接Delphi数据库应用程序一本地InterBase服务器的

     驱动程序

Delphi上述这些特性使得我们创建数据库应用程序通过BDE能够很灵活地与 dBASE 、Paradox、Local InterBase数据库服务器进行连接并可以方便地访问其中的数据。我们在创建一个简单的数据库应用时通过使用Delphi提供的上述工具和部件甚至可以不需编写任何程序。

BDE被自动地包含在Delphi中，因此，我们在创建数据库应用程序时，不必关心BDE的有关内容。Delphi的安装程序自动为Paradox、dBASE和本地InterBaseServer 安装相应的驱动程序，并建立了有关的配置，DBE Configuration Utility 可以建立应用程序与数据库的连接信息，还可以为数据库设置别名。

下表列出了Delphi开发Client/Server应用程序的有关特性，这些特性扩展了 Delphi访问远程数据库的功能，如SQL数据库服务器（ORACLE、SyBase、Informix、 Microsoft SQL Server、InterBase）。

表13.2 Delphi Client/server数据库特性

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

工 具 　 主 要 用 途

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

  SQL Drivers中的SQL link和ReportSmith为

SQL Drivers 　Delphi数据库应用程序提供了访问远程SQL

  服务器的驱动程序，如访问ORACLE、SyBase、

  Microsoft SQL server、Informix、Intermix

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

Visual Query Builder　以可视化的方式建立SQL语句对数据库表和表

  中的记录进行操作

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

SQL links使得Delphi数据库应用程序利用SQL语言访问驻留在远程服务器上的数据，这些服务器包括ORACLE、Sybase、Microsoft SQL Server、Informix、InterBase。 当安装SQL Link驱动程序之后，SQL语句便可以直接操作服务器上的数据。

13.2.2 Delphi可以访问的数据源（DataSource）

Delphi数据库应用程序是通过BDE获取它们所需的数据的，BDE与不同类型的数据源打交道，BDE可以使用的数据源有如表13.3所示

表13.3 Delphi可访问的数据源

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

数据源(DataSource) 　 特 性 描 述 　文件扩展名

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

  数据库表是通过dBASE数据库管理系统或

dBASE数据库 　DBD建立的，每个表是一个独立的文件 　.DBF

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

  数据库表是通过Paradox数据库管理系统 　.DB

Paradox数据库 　或DBD建立的，每个表是一个独立的文件

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

ASCII文件 　表是通过Database Desktop建立的,每个 　.TXT

  表是一个独立的文件

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

本地InterBase服务器 　数据库是通过InterBase数据库管理系统 　.GDB

  建立的,多个表包含在一个数据库文件中

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

SQL数据库服务器: 　数据库是通过相应的数据库服务器提供的　依赖不同的

ORACLE,Sybase,Informix　专用或通用工具建立的,也可以通过DBD来　数据库管理

Microsoft SQL Server 　创建数据库,并通过SQL Link访问数据库 　系统

InterBase

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

ODBC数据源 　主要是指那些具有ODBC接口的数据库系统　依赖于相应

  如MS Access,Btrieve等 　的数据库

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

13.3 Delphi数据库的体系结构

Delphi使用可视化的部件创建数据库应用，跟创建其它的非数据库应用程序一样，数据库部件都具备一定的属性，程序设计人员可以在设计过程中设置部件的多种属性，也可以在程序运行过程中通过程序来设置部件的各种属性。

在Delphi部件板上有两页数据库部件用于开发数据库应用程序：

数据访问部件页：该页上的部件主要用于说明有关的数据库的信息，如应用程序要访问(连接)的数据库，要访问数据库中的具体的数据库表，以及要访问表中哪些字段等，在实际的开发应用中常用的部件有TDataSource、TTable、TQuery等。

数据控制部件页：该页上的部件主要用于显示浏览数据库中的数据信息，为用户提供了一个可视化的界面，常用的部件有：TDBGrid、TDBEdit、TDBCheck等，可以让用户对数据库中的信息进行有效的浏览、编辑、插入、删除等操作。

TTable、TQuery、TStoredproc部件负责与实际的数据库表联系， 并从中获取数据信息，因而它们又常常被称为数据集部件，它们在程序设计过程中是可见的，但在程序运行时是不可见的， 它们通过 BDE 为应用程序提供与数据库的连接， 数据控制部件通过TDataSource部件与数据集部件相连，为用户提供一个可视化的界面， 并在其中显示数据库中的数据信息。

13.3.1 数据访问部件

数据访问部件页上提供了一组数据访问部件用来访问数据库中的数据。

图13.3 数据访问页上的数据访问部件

当要创建一个数据库应用时，首先在窗体中选择一个数据访问部件，然后为数据访问部件设置有关的属性，说明要访问的数据库、数据表以及表中的记录等，数据访问部件为数据控制部件与数据源建立一条通道。数据访问部件在程序运行时是不可见的。下表列出了数据访问页上的数据访问部件以及它们的主要用途：

表13.4 数据访问部件

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

部件名称 　 主 要 用 途

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

  作为数据集部件TTable、TQuery、StoredProc组

TDataSource　件与数据浏览件TDBGrid、TDBEdit之间传送数据

  的通道。

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

  它是存取磁盘上数据库表的媒介，它通过BDE存

TTable 　取数据库表中的数据，TTable再与TDataSource

  进行“对话”，使得数据浏览部件能够有效地从

  TTable中访问数据并能显示和编辑其中的数据。

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

  它利用SQL语言访问磁盘上数据库表中的数据，

TQuery 　并与TDataSource“对话”，实现数据浏览部件

  对数据库的访问。

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

TStoredProc　在应用程序中，它主要用来访问远程服务器中的

  存贮过程

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

  当应用程序要登录到一个远程服务器上的数据库

TDatabase 　时，可以用该部件来建立应用程序与数据库永久

  性的连接。

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

TBatchMove　用于复制数据库表的结构或表中的记录。

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

TReport 　用于创建数据库的输出报表。

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

值得指出的是在绝大多数数据库应用中，一般都是使用数据集部件TTable、TQuery或TStoredProc与磁盘上的数据库进行连接，用TDataSource部件连接数据控制部件和数据集部件，当然用户也可以自定义数据集部件，用于数据库应用当中， TTable 、 TQuery 和TStoredProc部件中都包含一个不可见的TField类型的对象Fields，Fields是一个串列表，它对应于数据库表或一个查询结果的列或字段。Fields对象是伴随着TTable、 TQuery 和TStoredproc部件的活动状态动态地建立的，当数据库表被关闭时，Fields 对象也随之消失，它在程序设计和程序运行过程中都是不可见的。

当然也可以利用Fields Editor建立永久性的Fields对象供Delphi应用程序使用， 我们将在后面的内容中详细阐述。

13.3.1.1 TTable部件

利用TTable部件程序设计人员甚至可以不需要编写任何程序便可对数据库进行访问，在一个应用程序窗体中放置一个TTable部件的过程如下：

１、在部件选择板上选择Data Access页；

２、单击Table图标；

３、在窗体内单击鼠标，获得一个TTable部件；

４、为TTable部件设置有关的属性：

DatabaseName属性指定要访问的数据库所在的路径名，路径名可以用别名来表示。

TableName属性指定要访问数据库中具体的数据库表。

Active属性设置为True时，表示打开要访问的数据库表；设置为False时，暂时 不打开要访问的数据库表。

缺省情况下，TTtable部件中包含了要访问的数据库表中所有的字段和记录， 用鼠标双击TTable图标时，会出现一个字段编辑器（Fields Editor），使用Fields Editor可以对TTable部件中包含的数据库表中的字段的显示格式等属性进行编辑，具体可以控制：

● 建立一个永久性的字段列表，包括字段的顺序，字段的类型等，即使磁盘上实际的数据库表的表结构发生了改变，我们建立的这个永久性的字段列表也不会发生改变

● 为每个字段指定一个便于阅读和使用的名字

● 指定字段显示的顺序

● 为每个字段指定一个用于显示的字符串

● 为字段增加合法性检验

● 为了显示的需要还可以建立新的字段（如可计算的字段）具体的使用方法见后面的内容

13.3.1.2 TQuery部件

TQuery部件是我们使用SQL语言开发数据库应用程序的有力工具，因为使用SQL语言，我们可以非常方便灵活地对一个或多个数据库表中的记录进行访问，所以利用TQuery我们可以查询本地的数据库如Pà?aradox和dBASE数据库系统中的数据，我们还可以使用TQuery部件对一个远地的数据库SQL服务器进行访问，建立Client/Server模式的应用程序。

在一个应用程序窗体中放置一个TQuery部件的过程如下：

１、在部件选择板上选择Data Access页；

２、单击Query图标；

３、在窗体内单击鼠标，获得一个TQuery部件；

４、为TQuery部件设置有关的属性：

DatabaseName属性指定将要访问的数据库的路径名。

SQL属性指定对数据库表进行访问SQL语句，它可以是一条查询语句也可以是一条 修改语句或插入语句等。在对象浏览器上，单击SQL属性时，会打开一个字符串编辑器供程序设计者输入SQL语句。

在这里要注意在TQuery部件中，不是用TableName 属性来指定要访问的数据库中的数据库表，而是在SQL属性中，通过SQL语句来指定将要访问的数据库表。

13.3.1.3 TDataSouece部件

TDataSource部件是连接数据集部件TTable、TQuery、 TStoredProc 和数据控制部件TDBGrid、TDBEdit等的桥梁，TTable、TQuery、TStoredProc部件通过BDE可以实现与磁盘上的数据库连接即访问， 但它们本身不能显示数据库中的数据信息， 而数据控制部件如TDBGrid、TDBEdit等能够提供可视化的界面，显示数据库中的数据信息，但它们不具备访问磁盘数据库的能力，正是TDataSource将这两者有机地结合起来， 使得用户才能交互地对数据库中的数据信息进行查询、修改、插入、删除等操作。

在应用程序窗体中放置TDataSource部件的过程如下：

１、在部件选择板上选择Data Access页；

２、单击DataSource图标；

３、在窗体内单击鼠标，获得一个TDataSource部件；

４、为TDataSource部件设置有关的属性：

Dataset属性指定一个数据集部件，可以是TTable、TQuery或TStoredProc部件的 名字。


第十三章 Delphi开发数据库应用程序概述（二）

DELPHI基础教程

第十三章　Delphi开发数据库应用程序概述（二）

13.3.2 数据控制部件

数据控制部件页上的部件，主要用于设计用户界面，对数据库中的数据进行浏览、编辑、插入、删除等操作。因而数据控制部件常常又被称为数据浏览部件，数据控制部件其实是在Standard页上的标准部件的基础上，相应地增加了数据浏览功能，使得它们能够显示和编辑数据库中数据信息。

数据控制部件既能够把数据库中的数据显示到窗体中，又可以将其自身的经过修改的数据写回到数据库中。下表列出了数据控制页上的数据控制部件及它们的主要用途。

表13.5 数据控制部件

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

部件名称 　 主 要 用 途

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

  使用该部件可以向前向后移动记录指针，可以使

TDBNavigator 　用该部件对单条记录进行编辑，还可以用它来插

  入、删除记录以及刷新显示和取消前一次的操作

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

  它是显示数据库中的数据的文本框，它只能显示

TDBText 　数据库表当前记录的字段值，用户不能对其中的

  数据进行修改。

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

  它是显示和编辑数据库表中的数据的编辑框，它

TDBEdit 　既可以显示又可以编辑数据库表中当前记录的字

  段值。

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

TDBCheckBox 　它是浏览数据库中的数据的检查框，它可以用来

  显示和编辑数据库中的布尔型字段的字段值。

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

TDBListBox 　它是浏览数据库中的数据的列表框，它可以用一

  个列表框来显示数据库表中一个字段的值。

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

TDBComboBox 　它是浏览数据库中的数据的组合框，它可以用一

  个组合框来显示数据库表中一个字段的值。

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

TDBRadioGroup 　它是浏览数据库表中的数据的单选钮，用一组单

  选钮可以确定显示数据库表中哪一个字段。

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

TDBGrid 　它是浏览数据库中的数据的网格，以网格的方式

  显示数据库中的数据，在网格中还可以对数据库

  中的数据进行编辑。利用Fields Editor可以对

  数据库表中字段的显示格式、显示顺序、是否显

  示等进行控制。

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

TDBMemo 　它主要用于浏览数据库中备注型的字段，它可以

  用来显示数据库表中当前记录中的BLOB型字段。

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

TDBImage 　它是浏览数据库中的数据的图像框，它可以用于

  显示、拷贝、粘贴据库表中图像类型的字段。

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

TDBLookUpList 　它是浏览数据库表中的数据的列表框，在基于一

  个数据库表的应用中，用它可以显示另一个数据

  库表中一个指定的字段值。

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

TDBLookUpCombo　它是浏览数据库表中的数据的组合框，在基于一

  个数据库表的应用中，用它可以显示另一个数据

  库表中一个指定的字段值。

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

数据控制部件为开发Delphi数据库应用程序提供可视化的用户界面，不管应用程序是访问本地数据库中的数据文件，还是访问远程数据库服务器中的数据文件，用户界面都是一致的，即数据库的物理位置对数据控制部件是透明的。

13.3.3 数据库窗体专家和数据库操作台（DBD）

Delphi为用户开发简单的数据库应用程序提供了一个开发工具叫做“数据库窗体专家”（Database Form Expert），在Delphi系统菜单Tool菜单下可以找到。

数据库窗体专家能够自动生成简单的数据库应用程序中所必须完成的许多任务，它还可以生成基于单个数据库表的应用程序窗体或基于主要──明细型多个数据库表的应用程序窗体，数据库窗体专家能够自动完成的任务如下：

● 放置数据库部件到窗体中（TDataSource部件）

● 为数据集部件（TTable、TQuery）和磁盘上的数据库建立连接

● 建立数据源（TDataSource）与数据控制部件的连接，数据源（TDataSource） 与 数据访问部件（TTable、TQuery）的连接

● 为TQuery部件编写SQL语句

● 为窗体中的部件定义Tab顺序

数据库操作台（DBD）是数据库维护和数据定义工具，程序设计人员利用它可以查询、连接、建立、重构、索引、修改和拷贝数据库表，包括Pà�aradox和dBASE文件和基于 SQL语言的数据库表。而且在使用DBD操作Paradox或dBASE的表时，用户不必拥有 Paradox 或dBASE数据库管理系统。DBD还可以把一种格式的数据和数据字典拷贝成另一种格式，例如，你可以将一个dBASE的表拷贝到远程的SQL服务器上的一个数据库中去。有关DBD详细描述，请参看“数据库操作台(DBD)的使用”。

13.4 Delphi数据库应用程序的开发方法和步骤

13.4.1 概述

用Derphi用开数据库应用程序，虽然与开发其他应用程序有相似之处，但是也有一些重要的区别，我们必须要加以注意。

利用Delphi的客户/服务器功能， 程序设计者可以在本地数据库上或远程数据库服务器上开发客户/服务器模式的应用程序。Delphi 的一个强有力的功能之一是可以将基于本地桌面数据库系统的应用程序很容易地修改成客户/服务器模式的应用。一个Delphi 数据库应用程序访问的是本地数据库还是远程SQL数据库服务器上的数据库， 这对于最终用户是完全透明的。即数据库的物理位置对最终用户是透明的，当数据库的物理位置发生变化时，用户界面不必随之变化。

因为数据库应用程序的是依赖于它所访问的数据库的，所以在开发数据库应用程序之前必须要建立一个完善的数据库即数据定义，数据定义应当是开发数据库应用程序的一部分，但它超出了书本的范围，有关数据定义的内容，请参看关系数据库设计方面的资料。在我们用Derphi开发一个数据库应用程序时，可能有下列四种情况。

１、数据库不存在或者必须要重新定义。使用DBD为本地数据库定义Paradox或dBASE 数据库表。使用Delphi提供的服务器开发工具如Windows ISQL或DBD定义本地或远程SQL数据库务器上的数据库。

２、数据库在桌面数据库系统中或局域网上（如Paradox或dBASE），而且BDE、数据 库和应用程序在同一台机器上，这是典型的独立应用。

３、数据库在桌面数据库系统中，但是用户要把它改变到SQL数据库服务器中去，这 种情况我们在附录Ｃ中详细讨论。

４、数据库在SQL数据库服务器中，而且应用程序将到SQL数据库服务器中去访问数据 库，这是一个标准的客户/服务器应用。

有关开发客户/服务器应用程序的详细内容，参看“客户/服务器应用”一章。

13.4.2 数据库应用程序的开发步骤

数据库应用程序的最终目标就是为用户提供一个满足其长期需要的软件产品，因此，成功地开发一个数据库应用程序的关键之一是要详细地定义用户的需求，然后再围绕用户的需求进行开发，数据库应用程序的一般开发的三个基本步骤如下。

● 系统设计

● 系统实现

● 系统运行和维护

在这个三个基本步骤中，都包含着数据库的开发和应用程序界面的开发两大类任务，对于一个客户/服务器模式的应用，数据库和应用界面的区别就更明显一些， 因为它们运行在不同的平台之上，而且使用的操作系统都常常不一样，如（一个Unix环境的服务器和Windows环境的客户机）。

１、系统设计

系统设计阶段应当根据用户的需求，明确地描述数据库（数据库服务器端）和应用界面（客户机端）实现的功能，即决定哪些功能由服务器端实现，哪些功能由客户机端实现，对于客户/服务器应用程序，许多功能既可以在服务器端实现又可以在客户端实现的， 例如一个复杂的数学变换功能既可以由客户端的应用程序实现，也可以由服务器端的存贮过程实现。服务器和客户端应用程序功能的划分往往取决于应用程序运行的硬件环境，例如：如果客户端是低档的PC机，服务器端是高档的工作站，那么大量的计算由服务器完成是比较理想的。

２、系统实现

系统实现阶段的主要任务是使用Delphi提供的工具和部件以及Pascal语言实现系统设计阶段的设想，并进行调试。

在系统实现阶段，最好使用数据库的一个备份数据库，这个备份的数据库与原数据库具有相同的结构，但其中的数据库只是原数据库中的一部分，至所以不在原数据库上开发应用程序，是因为考虑到没有调试好的应用程序可能会破坏数据库中的数据或者妨碍数据库的正常操作。

如果应用程序是基于远程SQL数据库服务器的，在系统实现阶段可以采用两种方法：

● 在本地InterBase服务器上，使用数据库的备份数据库进行开发和调试

● 在远程服务器上，使用数据库的备份数据库进行开发和调试

第一种方法优越性在于，它是独立于服务器的，而不至于影响服务器的其它的操作，而且不消耗服务器的资源不增加网络的负担。它的不利的方面是开发出来的应用程序只能在标准的SQL服务器上使用和调试。 第二种方法能够使程序设计人员直接感受到服务器的特性，但它在调试阶段要消耗网络和服务器的资源，这种方法具有一定的危险性，程序中的错误可能会导致服务器的瘫痪。

３、系统运行和维护

一个应用系统性能的优劣，效率的高低始终应当由用户来做出判决，应用程序在运行过程中，用户会提出一些新的需求和建议，根据用户需求的变化，应当对应用程序做一定的修改，使其进一步地得到完善和提高。

13.4.3 交付数据库应用程序

交付数据库应用程序意味着将它交付给最终用户，并且提供应用程序运行所需的软件，非数据库应用程序往往只需要一个EXE文件，而数据库应用程序要包括下列几类文件：

● 数据库应用程序生成的EXE文件和DLL文件（如果有的话）

● 必要的辅助文件（如Readme文件或HLP联机帮助文件）

● 支持访问数据库的BDE，有时也称为IDAPI

● 用于打印输出报表的ReportSmith报表工具

● 如果应用中使用了VBX控件，还要包括VBX和BIVBXII.DLL

13.3.4 安装BDE

在交付数据库应用程序时，必须同时在运行应用程序的机器上安装BDE，Delphi 本身包含可再安装的BDE,在准备数据库应用程序安装盘时，同时从Delphi中复制一份BDE 到最终用户的机器中，BDE中包含访问多种数据库系统的驱动程序，为了节省磁盘空间， 在安装BDE时，可以只安装应用程序必须的驱动程序，例如， 如果我们的应用程序只需要访问dBASE数据库文件，那么在安装BDE时，只需安装dBASE的驱动程序就行了。要访问Paradox数据库，BDE至少需要500KB的磁盘空间。BDE主要包括下列文件，请参看DEPLOY.TXT文件。

表13.5 BDE中主要包括的文件

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

文 件 名 　 说 明

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

IDAPI01.DLL 　BDE应用程序接口动态连接库

IDBAT01.DLL 　BDE Batch应用动态连接库

IDQRY01.DLL 　BDE查询动态连接库

IDASCI01.DLL 　BDE ASCII码驱动程序

IDPDX01.DLL 　BDE Paradox数据库驱动程序

IDDBAS01.DLL 　BDE dBASE数据库驱动程序

IDR10009.DLL 　BDE 资源库

ILD01.DLL 　语言驱动程序

IDODBC01.DLL 　BDE的ODBC驱动程序

ODBC.NEW 　Microsoft ODBC驱动程序管理器(Version2.0)

ODBCINST.NEW 　Microsoft ODBC驱动程序安装程序(Version2.0)

TUTILITY.DLL 　BDE应用工具库

BDECFG.EXE 　BDE配置工具

BDECFG.HLP 　BDE配置工具帮助文件

IDAPI.CFG 　BDE(IDAPI)配置文件

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

安装BDE可以采用与安装Delphi相同的方法，在Windows环境中运行Delphi的安装程序setup.EXE，此时，Delphi显示Delphi Installation对话框

为我们只须安装BDE，所以只要选择Borland Database Engine检查框，如果应用程序要处理SQL数据库服务器上的数据，还必须要选择SQL Links 检查框， 以便安装 SQL Links。单击Continue按纽，完成BDE的安装。

如果没有Delphi安装盘，我们也可以手工安装BDE ， 从现有机器上的 BDE 目录（或IDAPI目录）中拷贝所有的文件到用户机器上的BDE目录中，并在用户的Win.INI 文件的结尾处增加下列语句：

[IDAPI]

DLLPATH=C:/DELPHI/IDAPI

CONFIGFILE01=C:\DELPHI\IDAPI.CFG

然后在Windows的Program Manager中为IDAPICFG.EXE起动图标。

语言驱动程序的安装

BDE可以为应用程序提供不同的语言支持，语言驱动程序可以在配置Paradox,dBASE数据库时装入，对于服务器上的数据库可以在IDAPI.CFG 文件中说明相应的语言驱动程序，语言驱动程序在C:\DELPHI\IDAPI\LANGDRV子目录下(驱动器名与Delphi安装的磁盘有关)，是带有扩展名为.LD的文件。如果正确安装了语言驱动程序，在Windows的WIN.INI 文件中包含下列内容（缺省情况下）：

[Borland Language Drivers]

LDpath=C:/dELPHI\IDAPI\LANGDRV

13.4.5 安装SQL Link

安装SQL链接与安装BDE执行相同的步骤，在图13.7中只选择SQL Link检查框，然后执行安装，选择应用程序要访问的SQL数据库服务器，当选择安装SQL Link时， 安装程序会自动地安装BDE。根据用户选择的SQL数据库服务器，安装程序会自动地提示相关的特定信息，例如Informix数据库系统的链路(SQL Link)需要一个放置消息文件的地方。用户选择的每个SQL Link都使用一个或多个附加的动态链接库(DLL文件)，这些文件将要拷贝到BDE目录中，下面是建立SQL Link时常见的SQL数据库系统所需的DLL文件。

1.ORACLE数据库

在建立ORACLE的SQL Link时除了需要下列文件外，应用程序还需要ORACLE数据库管理系统的客户端的连接产品与网络协议接口。

表13.6 建立ORACLE数据库的SQL Link所需的文件

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

文件名 　 描 述

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

SQLD_ORA.DLL 　 Borland SQL Link for ORACLE驱动程序

SQLD_ORA.HLP 　 联机帮助文件

SQL_ORA.CNF 　 ORACLE驱动的BDE配置文件

ORA6Win.DLL 　 ORACLE 6.x版客户端的DLL文件

ORA7Win.DLL 　 ORACLE 7.x版客户端的DLL文件

SQL13Win.DLL 　 ORACLE 客户端的DLL文件

SQLWin.DLL 　 ORACLE 客户端的DLL文件

COREWin.DLL 　 ORACLE 客户端的DLL文件

ORAWE850.Ld 　 ORACLE 的语言驱动程序

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

2. Sybase和MicroSoft SQL Server数据库

在建立Sybase或MicroSorft SQL Server的SQL Link时除了需要下列文件外， 应用程序还需要Sybase数据库管理系统的客户端的连接产品与网络协议接口。

表13.7 建立Sybase数据库的SQL Link所需的文件

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

文件名 　描 述

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

SQLD_SS.DLL 　BDE SQL Link for Sybase的驱动程序

SQLD_SS.HLP 　联机帮助文件

SQL_SS.CNF 　Sybase驱动程序的BDE配置文件

W3DBLIB.DLL 　Sybase/MC SQL Server客户端的DLL文件

DBNMP3.DLL 　Sybase/MC SQL Server客户端的DLL文件

SYDC437.LD 　Sybase语言驱动程序

SYDC850.LD 　Sybase语言驱动程序

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

3. Informix数据库

在建立Informix的SQL Link时除了需要下列文件外，应用程序还需要Informix数据库管理系统的客户端的连接产品与网络协议接口。

表13.8 建立Informix 数据库的SQL Link所需的文件

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

文件名 　 描 述

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

SQLD_ss.DLL 　BDE SQL Link for Informix的驱动程序

SQLD_INF.HLP 　联机帮助文件

SQL_INF.CNF 　Informix驱动程序的BDE配置文件

LDLLSQLW.DLL 　Informix客户端的DLL文件

ISAM.IEM 　Informix错误信息文件

OS.IEM 　Informix错误信息文件

RDS.IEM 　Informix错误信息文件

SECURITY.IEM 　Informix错误信息文件

SQL.IEM 　Informix错误信息文件

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

4.InterBase 数据库

在建立InterBase的SQL Link时除了需要下列文件外，应用程序还需要InterBase数据库管理系统的客户端的连接产品与网络协议接口。

表13.9 建立InterBase数据库的SQL Link所需的文件

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

文件名 　 描 述

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

SQLD_IB.DLL 　BDE SQL Link for InterBase的驱动程序

SQLD_IB.HLP 　联机帮助文件

SQL_IB.CNF 　InterBase驱动程序的BDE配置文件

CONNECT.EXE 　InterBase连接测试诊断工具

CONNECT.HLP 　InterBase连接测试诊断工具的帮助文件

GDS.DLL 　InterBase API DLL

REMOTE.DLL 　InterBase与网络的接口的DLL文件

INTERBASE.MSG 　InterBase错误信息文件

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


第十四章 简单数据库应用的创建及MASTAPP介绍（一）

DELPHI基础教程

第十四章　简单数据库应用的创建及MASTAPP介绍（一）

Delphi中嵌入的数据库应用开发工具如Database Form Expert具有很强大的功能，我们不需要编写任何程序代码便可以快速地创建一个简单的数据库应用程序，甚至还能创建基于多个数据库表的主要──明细型数据库应用程序。

本章主要介绍用Delphi开发简单的数据库应用程序的一般方法和步骤，首先让读者对Delphi强劲的数据库应用开发工具有一个直观的印象，然后在此基础上进行复杂的数据库应用程序的设计，本章主要包括以下内容：

● 创建数据库应用窗体

包括用Database Form Expert 或手工方式创建简单的无需编写程序代码的应用程序或者利用多个部件并编写功能复杂的程序代码创建主要──明细型数据库应用程序。

● 在应用程序中控制字段有关的属性

描述怎样读写数据库表中字段的值和控制字段的显示格式等。

本章所介绍的例子中用到的窗体、数据库表以及相关的文件都是在安装Delphi时缺省安装在C:\DELPHI\DEMOS\DB\MASTAPP目录中，并且用别名DBDEMOS表示这一子目录。 在本章例子中，除特殊声明外，所有的TTable和 TQuery 部件的 DatabaseName 属性都设置为DBDEMOS。

14.1 简单的基于单表的据库应用

用Decphi创建显示一个数据库表中的内容的应用非常简单和方便，只需要三个部件，只要将这三个部件通过相关的属性相互联系起来，不需要编写任何程序代码便可以实现。例如，用户想查看数据库表Customer.DB中的内容时，可以按下面步骤来实现：

14.1.1 选择相关的部件：

选择菜单Project/New开始一个新工程，并修改Form1的Caption属性为CustomerFrom1并把Name属性设置为CustomerForm1，然后从部件选择板上的Data Access 页上选取一个Datasounce部件和一个Table部件放到窗体的左上角，它们是非可见的部件， 在窗体中我们看到的只是部件的图标；从Data Control页上选取DBGrid部件放到窗体中前两个部件的下面。完成这些工作之后，窗体如图14.1所示。

图在CustomerFrom1窗体中放置三个部件

14.1.2 设置部件的属性

为了使TDBGrid部件能够显示数据库表Customer.DB中的客户信息，我们必须修改窗体三个部件相关的属性，这些属性的设置如表14.1所示。

表14.1 CustomerFrom1窗体中三个部件的属性设置

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

属 性 属 性 值

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

DataSource1.AutoEdit False

DataSource1.DataSet Table1

Table1.DatabaseName DBDEMOS

Table1.TableName CUSTOMER.DB

Table1.Active True

DBGrid1.DataSource DataSource1

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

这里要注意的是：DBDEMOS是Delphi缺省安装时C:\Delphi\DEMO\DB\MASTAPP目录的别名，而且数据库表Customer.DB存在该目录下，用户在使用这一例子时， 请注意这两项设置都是正确的。另外 Datasource1.Dataset，Table1.TableName和DBGrid1.Datasource属性都有下拉式列表框允许用户从可能的值列表中选择它们的值，这样能方便我们进行属性的设置，而且不容易出错。

Datasouuce1.AutoEdit属性设置为False是为了防止用户修改数据库表中的数据， 在下面的讨论中我们将详细地进行说明。

Table1.Active设置为True时,Delphi会打开Table1.TableName所指定的数据库表。如果这个数据库表不存在（或表中什么也没有， 即空表）， Delphi 会弹出出错信息并且Table1.Active变成False。当Table1.Active被设置成True之后，Table1 部件的一些属性就不能再修改了，如Table1.DatabaseName和Table1.Tablename属性。若要修改它们， 必须首先要将Table1.Active属性设置为False，然后再进行修改，否则，Delphi会弹出错误信息“Cannot perform this operation on an open database”。当看到这个错误信息时，只需把Table1.Active置成False，完成相关的修改后，再把 Table1. Active 属性设置为True。

当我们把DBGrid1.DataSource的值设置成DataSource1时，Delphi会把Customer.DB中的数据填充到DBGrid1部件中，并且可以用DBGrid1中的滚动条来浏览数据库表中的所有记录。

14.1.3 运行程序

保存文件，命名代码单元为Cust.pas，命名工程名为CustPRJ.DPR,然后按F9编译并运行程序。程序执行之后，我们可以使用滚动条或键盘移动键在字段和记录间移动。但不能修改表中的数据，因为Datasouc1.AutoEdit1属性已被设置为False。

Cust程序中的三个部件都有各自的特殊用途，三个部件的相关属性在内部相互联系生成最终的应用程序。TTable部件连接磁盘上的实际数据库表和应用程序中其他部件的通道。TTable部件具有打开和关闭、读取、更新以及其他处理磁盘数据库文件的方法。

TDatasource部件是连接TTable部件和数据浏览部件如TDBGrid部件的桥梁。 TDBGrid部件用于显示数据库表中的数据信息，它为应用程序提供一个直观的界面。图14.2阐述了这三个部件之间的关系。

Cust程序中三个部件之间的内部关系

TDBGrid 部件的奇妙之处在于它知道如何去获取数据库表中的下一条或前一条记录，我们使用滚动条或箭头键便可以完成这项任务。TDBGrid部件不知道如何增加、 删除和修改记录。如果想让 Cust 程序能够修改数据库表中的记录， 只要把 Datasource1 部件的AutoEdit属性设置成True ， 并重新编译和运行程序就可以达到目的。 使用箭头键， 把DBGrid的高亮度条定位到某一个字段上，然后键入新值，该字段中的值将被键入的新值所取代，并且当移动到另一条记录时，健入的信息会自动写入数据库表中。如果想放弃所做的改动，只需在离开该字段前按一下Escape键。

如果想在表中增加新记录，可以把高亮度条移到网格底端的空白记录上并输入新记录的有关字段值。也可以在用户指定的某一条记录的后面插入一条新记录，只要把高亮度条定位到指定的记录上，按Ins键，使可以在该记录的后面插入新记录。

删除某一条记录时，把高亮度条定位在想删除的记录的任何字段上，按Ctrl+ del键,这时会出现保护信息，我们可以确认是否真的想删除该项记录。

TDBGrid为用户提供了较完备的功能，用于控制是否编辑、增加或删除记录。 若想禁止对数据库表作任何修改，设置TDBGrid部件的Readonly属性为 True ， 并设置 Option.dgEDiting为False(这将为我们提供一个只读的数据库表浏览器而不是数据库编辑器，但它隐含着增加、编辑和删除记录的能力)。TDBGrid部件的这些属性和Option属性其它选项的各种不同组合可以让我们很方便地对数据库表进行有效的浏览、编辑等操作。

如果我们经常使用像电子表格那样的界面来显示和编辑数据记录，TDBGrid 部件便是一个很方便的工具，但那并不是最友好的用户界面，如果想拥有更优美更直观的界面，我们还可以使用单独的数据浏览部件来显示数据库表中各个字段的值，并利用TDBNavigator部件控制对数据库表的存取。

14.2 利用TDBNavigator部件创建存取程序

我们可以改进一下Cust程序以便它一次只在对话框中显示一个客户的记录信息，并用一个TDBNavigator部件控制对记录存取──允许我们选择一个记录来显示或编辑以及增加和删除记录。完成的应用窗体。

增强的Cust程序

14.2.1 创建应用程序窗体

我们可以非常迅速地创建起来，因为到目前为止我们对创建窗体的方法已经比较熟悉，我们首先把所有的部件都放到窗体中，然后再设置它们的属性。

开始一个新工程，设置窗体Form1的Name 属性为 Customerform2 ， Caption 属性为 CustomerForm2。然后从部件选择板上的Data Access页上选取一个Datasource部件和一个Table部件放在窗体的右上角。再从Data Controls页上选取DBNatvigator部件放在窗体的左上角。

窗体中其余的部件如图14.3所示。它们是TDBEdit和TLabel部件，按图14.3 所示创建并布置部件，分别命名DBEdit部件为EditCustno、 Editcompany 、 EditAddr1 、EditAddr2、EditCity、EditState、EditZip、EditCountry、EditPhone 、EditFAX、EditTaxRate、EditContact。

现在我们来连接TTable部件和 TDataSource 部件， 然后连接所有的数据浏览部件和DataSource部件。设置TBNavigator部件和TDBEdit部件的属性，它们的DataSource属性都设置为DataSouce1。我们最后要做的事是连接窗体中各个TDBEdit 部件和它们在数据库表中对应的字段名，通过设置TDBEdit 部件的 DataField 属性来完成。 例如要连接命名为EditCustNo的TDBEdit部件和数据库表中的CustNo字段，具体步骤如下：

①选中窗体中的EditCustNo部件。

②在Object Inspector窗体中，单击DataField属性右边的箭头。

③从下拉列表中选中CustNo字段名。

对窗体中的其他TDBEdit部件执行以上操作连接到其对应的字段，然后保存文件。 命名代码单元名为Cust2.pas，命名工程名为Cusprj2.DPR。

14.2.2 使用TDBNavigator部件移动记录指针

上述程序运行之后，在数据浏览部件中会显示数据库表中的第一条记录。利用Tab 键可以在字段之间移动，但是不能编辑字段。因为我们为了防止意外修改，设置了Table1的AutoEdit属性值为False。如果想对数据库表中的记录进行编辑、 插入和删除操作或者想显示数据库表中另一条记录， 需要按 TDBNvigator 部件上这些功能所对应的功能按钮。TDBNavigator部件上的按钮和它们的功能如图14.4所示。

TDBNavigator中的按钮

TDBNavigator部件的绝大多数功能都可以根据其按钮的图标能够很容易地识别出来，而且TDBNavigator部件本身能感知到很多事情，如当前指针是否在数据库表的开头或尾部。如果用户正在查看数据库表中的最后一个记录，Next和Last按钮将会变灰成为非活动状态。同样， 如果用户当前正在浏览数据库表中的第一条记录， TDBNavigator 上的 First 和Previous按钮会变灰而成为非活动状态。有关各个按钮的作用的更详细说明请查看联机帮助。

如果用户想修改当前的记录，单击TDBNavigator部件的Edit按钮，然后完成需要做的修改，在做完修改之后，单击Post按钮以便将作的修改写入实际的数据库表中（更新实际的数据库表中的记录在数据库术语中叫作“投寄”记录即PostT）。 如果想取消所做的修改，单击Cancel按钮。Cancel按钮只取消自从上一次往数据库表中投寄记录以来对记录所做的修改。例如，如果用户曾修改了CustNo字段并单击了Post按钮投寄了修改，然后再修改Company字段并按Cancel，那么只有对Company所做的修改将会被取消。也就是说，一旦修改被写入了数据库表中，再按Cancle按钮是无法取消对记录的修改的，要想恢复到以前的状态，用户必须要重新编辑修改记录。值得注意的是，当用户修改了当前的记录，并移动到其他记录时，TDBNavigaator部件会自动地投寄用户对记录的修改。 例如：如果我们修改了记录的Company字段，并没有按Post按钮以更新表中的记录， 而是移动到下一条记录，这时用户对记录的修改也会自动地被写入数据库表中。

14.2.3 定制TDBNavigator部件

TDBNavigator部件中的按钮对我们开发人员来说是很方便的，但对于程序的最终用户来说不一定那么一目了然。为了帮助最终用户或初级用户更方便有效地使用TDBNavigator部件，我们可以设置TDBNavigator部件的ShowHint属性为True，这样当鼠标光标停留在TDBNavigator部件上的某一个按钮上超过大约１秒钟，在屏幕上便会出现该按钮的提示信息。如果我们不想使用TDBNavigator部件本身嵌入的提示信息，我们还可以设置TDBNavigtor部件的Hints属性，为每个按钮指定特定的提示信息，以帮助用户使用TDBNavigator部件。

TDBNavigator部件中有多个功能按钮，但并不是所有的按钮对每一个数据库应用程序都是需要的，特别是那些不允许修改表中的数据，或修改只是在很严格的控制下进行的数据库应用程序。我们可以通过设置TDBNavigator部件的 VisibleButtons 属性来确定要在TDBNavigator中显示哪些按钮步显示哪些按钮。例如，如果我们不允许用户修改表中的记录，我们就不需要Add、Delete、Post、Cancel 或 Refresh 按钮， 我们设置这些按钮的VisibleButtons属性为False，这样在TDBNavigator部件中将不会出现这些按钮。

TDBNavigator部件的ConfirmDelete属性和Delete 按钮配合使用对用户删除数据库表中的记录是非常有用的，当ConfirmDelete属性设置为 True （缺省设置）， 当用户单击Delete按钮试图删除当前记录时，Delphi会弹出一个确认框，要用户确认是否真的想删除当前记录。这样，在用户进行删除记录的操作时，会更安全一些。如果用户不希望在按下Delete按钮时出现确认框，只要把ConfirmDelete设置为False就可以了。

还有一些属性可以用来定制TDBNavigator部件的外观和性能，有关这方面的详细信息请参看联机帮助。

14.3 创建主要──明细数据库应用

我们前面在介绍的基于单个数据库表的数据库应用程序只能对数据库表进行简单的管理，大多数只用来浏览单个数据库表中的记录信息，如果我们想浏览多个相关的数据库表中的记录信息，就必须要创建主要──明细型数据库应用程序。

在主要──明细型数据库应用程序中，一个数据库表作为主要表，其中存放着综合信息，其他的数据库表和主要数据库表相关联，它们当中存放着更详细的信息。例如，当数据库表Customer.DB作为主表，它包含着客户的综合信息如编号、姓名、 所在公司的名称等等。而数据库表Orders.DB中包含着每个客户的订货单的详细信息，如订单号、 发货日期、起运日期、发货目的地等信息，这样当在Customer.DB表中查看某一位客户时， 利用其中的字段CustNo与Orders.DB表发生联系，自动地从Orders.DB表中检索出这位客户曾经发来的所有订货单的详细信息。主要──明细型数据库体现了关系数据库的特点，即独立的数据库表之间基于它们共同的字段而发生联系。在这里Customer.DB和Orders.DB拥有一个共同的字段CustNo。

14.3.1 一对多关系的主要──明细型数据库应用程序

主要和明细数据库表之间存在一对多的关系，意思是说对于主表中的一条记录，在明细表中有多条记录与之对应。例如，创建一个主要──明细型数据库应用程序，其包括两个表Customer.DB和Orders.DB，它们分别作为主表和明细表，创建好的应用如图14.5所示，窗体中各部件的属性设置

表14.2 主要──明细型数据库应用中各部件的属性

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

部 件 属 性 属 性 值 注 释

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

Table1 Active True

(主表) DatabaseName DBDEMOS

TableName CUSTOMER.DB

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

DataSource1 DataSet Table1

AutoEdit False

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

Table2 Active True

(明细表) DatabaseName DBDEMOS

TableName ORDERS.DB

IndexFieldNames CUSTNO 指定字段CUSTNO作为

Table2中的索引字段

MasterField CUSTNO 指定与主表发生联系

的字段

MasterSource DataSource1 说明与主表相连接的

数据源即DataSource

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

DataSource2 DataSet Table2

AutoEdit False

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

DBGrid1 DataSource DataSource1

(对应主表)

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

DBGrid2 DataSource DataSource2

(对应明细表)

TableName ORDERS.DB

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

DBNavigator1 DataSource DataSource1

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

一对多关系是非常普遍的关系。即便是简单的名字/ 地址数据库都有一对多的关系，因为一个人可能不止一个地址：家庭地址、工作地址、还可能有别墅地址。在本例中，公司的一个客户常常有多个订货单，当我们单击DBNavigator1中的向前、向后按钮时，会移动DBGrid1中的记录指针，而在DBGrid2中会自动显示与DBGridl 中当前记录相关的多条记录，即显示一个客户的信息时，同时会显示该客户的所有订货单的详细信息。

14.3.2 一对多──多关系的数据库应用

前面我们介绍了基于两个表的一对多关系的应用，下面我们介绍怎样创建一个从三个表中浏览数据记录的一对多关系的应用。

例如：一个客户也许有多张订货单，而每一张订货单中有多个订货项目，这样我们在Customer.DB表和Orders.DB表之间建立一个主要──明细型关系，同时在orders.DB 表和Items.DB表之间建立一个主要──明细型关系。

窗体中各部件的属性如表14.3所示

表14.3 一对多──多关系的应用中各部件的属性

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

部 件 属 性 属 性 值 注 释

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

Active True

Table1 DatabaseName DBDEMOS

TableName CUSTOMER.DB

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

DataSource1 DataSet Table1

AutoEdit False

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

Active True

DatabaseName DBDEMOS

Table2 TableName ORDERS.DB

IndexFieldNames CUSTNO

MasterField CUSTNO

MasterSource DataSource1

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

DataSource2 DataSet Table2

AutoEdit False

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

Active True

DatabaseName DBDEMOS

Table3 TableName ORDERS.DB

IndexFieldNames ORDERNO

MasterField ORDERNO

MasterSource DataSource2

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

DataSource3 DataSet Table3

AutoEdit False

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

DBGrid1 DataSource DataSource3

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

DBNavigator1 DataSource DataSource1

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

窗体中其余的部件都是TDBEdit和TLabel部件，它们用于显示Customer.DB中的字段值和Order.DB中的字段值。在该例子中，总共连接了三个表， Customer. DB 表是主要表，Orders.DB表在窗体中起到了双重作用，它既是Customer.Db表的明细表，同时又是Items.DB表的主要表，Items.DB表是Orders.DB表的明细表。

14.4 字段对象的使用

Ttable和TQuery部件中有一个TField类型的属性Fiedls，Fields是TField类型的对象的列表，TField对象列表是Delphi数据库中较难以理解的一个对象，它是 TTable 部件和TQuary部件的一部分，它们是不能够选择到窗体中的独立的部件，而且无论是在设计阶段还是在程序运行过程中，它们都没有可见的图像。即使到Object Inspector窗中察看它们也很困难。

Tfield对象是在打开磁盘上的数据库表时动态产生的，并在数据库表被关闭时自动消失，TField对象可以控制表中的每一列是否在数据浏览部件中显示以及以何种格式显示等等。通过字段编辑器(Fields Editor)我们可以建立永久性的TField 对象列表代替动态的Tfield对象列表供Delphi应用程序使用，通过Fields Editor建立的永久性的字段对象会自动地加入到程序库单元的TForm类型定义中 ，它们保存在应用程序中，即使数据库表的基本结构发生了改变，它也是一直保留着，当然如果修改后的表中使得原来所定义的字段对象不再存在，Delphi应用程序在运行过程中会给出现错误信息。

14.4.1 字段对象的类型

字段对象TField对应数据库记录中的各个字段，因为数据库记录中的字段有多种数据类型，因此对记录字段可能出现的每一种数据类型都有一个独立的TField类型与之对应。TField的类型如表14.4所示

表14.4 字段对象的类型

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

字段对象的类型 对应的数据类型

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

TBooleanField 布尔型数据

TCurrentyField 货币型数据

TStringField 字符串数据

TIntegerField 整数型数据

TBLOB 大二进制对象

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

在大多数情况下可能使用的是TStringField和TIntegerField类型的字段对象， 从编程的角度来看这些TField对象的不同类型是完全相同的，应用程序是根本不必关心TField对象的实际类型，它们之间的主要区别在于：它们内部保留的以及它们和数据库表之间传递的数据类型不一样。

14.4.2 创建永久性的字段对象

我们知道字段对象在设计和运行阶段都是不可见的，它既可以随着磁盘上的数据库文件被打开时动态地生成也可以通过字段编辑器Fields Editor来创建它。 在应用程序中使用Fields Editor可以为数据库表中的字段创建相应的永久性的TField对象，TField 部件是不可见的部件，但是通过它，我们可以定义数据库表中各字段的显示属性和显示顺序以及控制字段的取值范围等。下面的例子，告诉我们如何使用Fields Editor定义Customer.DB表中的四字段，并在网格中显示表中的记录信息。

操作步骤：

１、建立一个基于 customer. DB 表的数据库应用窗体， 并在窗体中用一个网格显示customer.DB中的全部字段，详细方法请参见14.1节，建好的窗体如图14.1所示。

２、设置窗体中Table1的Active属性为True，使网格显示表中的记录。

３、选中Table1并双击鼠标左键，打开字段编辑器Fields Editor， 缺省情况下字段列表为空。

４、单击鼠标右键弹出一个弹出式菜单，然后选择Add Fields菜单项，缺省情况下表Customer.DB中的全部字段被选进字段列表框。 从字段列表框中选择你要在网格中显示的字段，具体做法是：单击Custno字段，并按住CTR键，再单击Company、Phone、LastInviceDate字段，然后单击OK按钮，确认被选择的四个字段，时窗体中的DBGrid1中只显示刚才被选中的四个字段值，而不再显示表中其它的字段值。


第十四章 简单数据库应用的创建及MASTAPP介绍（二）

DELPHI基础教程

第十四章　简单数据库应用的创建及MASTAPP介绍（二）

5、改变字段的显示顺序。单击LastInvoiceDate 字段并将它拖放到字段列表框中的第三行，即处于Company和Phone字段之间。此时窗体中显示Customer.DB 表中记录的字段将按新的顺序显示。

６、选择Close按钮，关闭字段编辑器Fields Editor。

７、按F9，运行上述程序。

14.4.2 字段对象的属性设置

虽然字段对象是不可见的对象，但是它同样具有很多的属性。在程序设计阶段，我们通过一定的方式可以设置它的有关属性，下面是设置字段对象的属性的方法和步骤。

１、选择窗体中的table1。

２、双击table1，打开字段编辑器Fields Editor。

３、选择要设置属性的字段。

４、在Object Inspector中修改字段对象的属性。

我们可以按上述方法设置Table1中各字段对象的有关属性，当我们选择Custno字段并修改其属性，窗体内会出现对话

字段对象的属性

修改字段CustNo的Alignment属性为taCenter，此时网格中显示的CustNo 字段值由原来的右对齐变成了居中。

表14.5中列出了字段对象在设计阶段可以修改的属性以及属性说明

表14.5 字段对象的重要属性

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

属 性 注 释

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

Alignment 说明字段值的显示方式：左对齐、右对齐、居中

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

Calculated 当该属性值为True时，表明该字段的值是根据其它字

段的值计算得来的。否则该字段是数据库表中的字段

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

DisplayLabel 说明字段在网格部件中显示时的标题，缺省情况下字

段的标题就是字段名

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

DisplayWidth 说明字段在网格中显示时所点的列宽度，即字符数

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

DisplayFormat 说明字段在显示和编辑状态下的显示格式和输入的过

and EditMask 滤条件(限定用户输入字段值的范围)。

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

FieldName 在数据库表中对应于该字段对象的字段名称

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

Index 指定该字段对象在数据集部件中的逻辑位置，如Table1

中的第一个字段对象的Index值为０

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

Name 字段对象的名称，缺省情况下，它是TTable、TQuery

部件的名称加上字段的名称。如上例中的CUSTNO字段

对象的Name属性值为Table1CUSTNO，通过字段对象的

Name属性可以访问该字段的值，如Table1CUSTNO.Value

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

ReadOnly 说明该字段是否能被修改，当该属性值为True时，该

字段的不能被修改

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

Visible 当该属性值为True时，在与之相连的网格部件中将不

显示该字段

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

根据表14.5中的属性， 我们可以修改上例中一些字段的某些属性， 使网络中显示表Customer.DB中的记录更符合我们的工作习惯。修改的属性如表14.6所示， 经过修改后的程序运行结果如图14.10所示。

表14.6 修改后的字段对象的属性

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

字 段 属 性 属 性 值

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

CustNo DisplayLabel 客户编号

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

Company DisplayLabel 公司名称

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

Phone DisplayLabel 电话号码

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

LastInvoiceDate DisplayLabel 购买日期

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

修改字段对象的属性

14.4.4 字段对象的访问

字段对象在应用程序中有动态生成的，也有通过字段编辑器Fields Editor 创建的永久性的，它们虽然在设计和运行阶段都是不可见，但是它们跟其他的对象一样都拥有自己的属性、方法和事件，因此我们在应用程序中是可以对字段对象进行控制和访问的。

因为动态字段对象是没有自己的名字的，永久性的字段对象有自己的名字，所以对这两种字段对象的访问方法是不一样的。

14.4.4.1 动态字段对象的访问

动态字段对象存在于数据集部件TTable和TQuery部件中，它们是随着磁盘上的数据库文件的打开而动态生成的，并且每一个字段对象对应于数据库表中的一个字段（即记录的一列），TTable或TQrery部件中所有的字段对象存在属性Fields列表中，Fields列表中的字段对象就像数组元素一样拥有自己的索引号，我们可以用这个索引号来访问字段对象。索引号在程序运行时赋值，从0开始，表中最左边的一列（第一个字段）的索引号为0，紧接着右边一个为1，以此类推。访问这些属性的方法和处理其他对象一样。

Table1.Fields[0].DisplayLabel:='标识符'

上述代码让我们访问与Table1相连的数据库表中的第一个字段，并为该字段指定一个标题，这是通过设置它的DisplayLabel属性值为一个特定的标识符来实现的。

通过索引号来访问Fields属性中的字段在使用For循环对列号进行迭代时会非常有用。但是在大多数简单应用程序中，通过列名（字段名）来访问字段会更加明白而且易读。在TTable部件中，提供了一个名为FieldByName的方法以便让我们通过列名访问字段对象。

Table1.FieldByName('CustNo').DisplayLabel:='标识符'

通过这种途径同样可以访问CUSTOMER.DB表中的CustNo字段， 并为该字段指定一个标题信息。

现在我们可以建立一个允许用户通过字段名和索引号来访问Customer.DB 表中的字段对象的简单窗体。

字段对象的访问

在该应用窗体的运行过程中，我们通过程序来访问其中的字段对象并设置有关的属性，这一控制过程我们放在窗体的OnCreate事件处理过程中。

例14.1 在窗体的Oncreate事件处理过程中访问字段对象。

procedure TForm1.FormCreate(Sender:TObject);

Begin

with Table1 Do

begin

{通过索引号访问字段对象}

Field[0].DisplayLabel:='客户编号';

{通过字段名访问字段对象}

FieldByName('Company').DisplayLabel:='公司名称';

FieldByName('Phone').DisplayLabel:='电话号码';

FieldByName('LastInvoiceDate').DisplayLabel:='购买日期';

end;

end;

在程序运行过程中访问字段对象

14.4.4.2 永久性字段对象的访问

通过字段编辑器Fields Editor 建立的永久性字段对象的访问相对于动态字段对象的访问要简单得多，我们在程序中可以直接通过字段对象的名称（即Name属性）进行访问。

例如：

Table1CustNo.DisplayLabel:='客户编号';

Table1CustNo.DisplayWidth:=12;

14.4.4.3 字段对象的读取和赋值

通过字段对象的Value属性，我们可以读取字段对象的值，例如在如图14.13所示的窗体中，单击Read按钮便可以将Customer.DB表中当前记录的COMPANY字段的值读取到编辑框Edit1中。

读取字段对象的字段值

窗体中各部件的属性如表14.7所示

表14.7 各部件的属性

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

部件的属性 属 性 值

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

Button1.Caption &Read

Button1.Name Button1

Label1.Caption 字段值

Label1.Name Label1

Edit1.Text

Edit1.Name Edit1

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

其它部件的的属性跟前面的例子一样。

为Read按钮编辑的OnClick事件处理过程如下：

procedure Form1.TButton1Click(Sender:TObject);

begin

Edit1.Text:=Table1Company.Value;

end;

在这里要注意的是：从字段对象中读取字段值时必须要将它赋给与之数据类型相匹配的变量，否则会出错。在上面的程序代码中，Table1Company的类型是TStringField 即是字符串类型的字段，而编辑框Edit1的属性Text的类型也是字符串型的， 因而它们是匹配的。如果类型不匹配，则要经过一定的转换才能够相互赋值。如：

Edit1.Text:=Table1CustNo.Value

这条代码在运行过程中将会出错，因为TablelcustNo是TFloatField 类型即是数值型数据，要在编辑框Edit1中显示数值型数据要经过下列转换：

Edit1.text:=Table1CustNo.AsString;

AsString是字段对象的属性，通过字段对象的AsString属性可以读取字段值并且将它转换成字符串类型。字段对象的字段值可以转换成以下几种类型的数据:

AsString: 将字段值转换成字符串数据

AsBoolean: 将字段值转换成布尔型数据

AsDateTime: 将字段值转换成日期时间数据

AsFloat: 将字段值转换成数值型数据

AsInteger: 将字段值转换成整数型数据

下面的程序代码是从字段对象中读取字段值并将它显示在编辑框Edit1中， 或者将字段值赋给相匹配的变量。

CustNoDouble: Double;

CustNoInt: Integer;

CustNoString: String;

{在Edit1中显示字段值}

Edit1.Text:=Table1Company;{类型相匹配，不需要转换}

Edit1.Text:=Table1CustNo.AsString;{类型不匹配，需要转换}

{将字段值赋给变量}

CustNoDouble:=Table1CustNo.Value;{类型相匹配，不需要转换}

CustNoInt:=Table1CustNo.AsInteger;{类型不匹配，需要转换}

CustNoString:=Table1CustNo.AsString;{类型不匹配，需要转换}

14.4.5 设定字段对象的显示格式

我们即可以在设计阶段设定字段对象的显示格式，也可以在运行过程中通过程序代码来设定字段对象的显示格式。

例14.2 在如图14.10所示的窗体中，再增加一个TaxRate字段， 并在程序设计过程中设定它的显示格式为0.00%，即设置TaxRate字段对象的DisplayFormat属性为0.00% ， 若TaxRate的值为0.085那么在网格部件中其显示的格式为8.50%。

在运行过程中我们通过程序代码来设定字段Phone的显示格式， 美国的电话表示形式与中国的表示形式不一样（如美国808-555-0269,中国(808) 5550269 ）， 为此我们将phone 字段的值表示成中国式的形式。 具体方法是：在 Object Inspector 中选取Table1phone对象，并为此对象的OnGetText事件编写如下程序代码：

TForm1.Table1PhoneGetText(Sender:TField;

Text:OpenString;DisplayText:Boolean);

begin

If DisplayText then

begin

Text:=Table1Phone.Value;

Delete(Text,4,1);

Delete(Text,7,1);

Insert('(',Text,1);

Insert(')',Text,1);

end;

end;

图14.14 设定字段对象的显示格式

14.4.6 自定义字段以及计算字段对象的创建

有时候为了使应用程序完成所期望的工作，我们要在数据库表现有字段的基础上增加一些自定义的字段，这些字段并不是数据库表中实际存在的字段，它们常常是根据数据库表中的其它的字段动态地计算出来的，因而它们常常被称为计算字段。

例如我们创建一个浏览ORDERS.DB表中记录的应用如图14.15所示。

浏览ORDERS.DB表中的记录

首先，我们想在显示OREDRES.DB表的网格中增加一个自定义的字段对象，完成以下步骤：

１、双击窗体中的Table1，打开字段编辑器Fields Editor。

２、在Fields Editor窗口中，单击鼠标右键，选择New Fields菜单项。

３、Delphi显示New Fields对话框。选择Field Type列表框中的Currency 项， 并在Field Name文体框中输入Balance ， 这样我们自定义了一个 CurrencyField 类型的字段Balance。Delphi会自动地填入相应的字段对象名，其缺省值为Table1Balance。如图14.16所示。

图14.16 New Field 对话框

４、单击Ok按钮，关闭New Field对话框。当Fields Editor 窗口重新出现时， 注意Balance已经出现在Fields列表框中。

５、在Fields Editor 窗口中单击鼠标右键， 并选择 Add Fields 菜单项， 打开AddFields对话框。

６、从Available Fields 列表框中， 按住 Ctrl 键并单击鼠标左键， 选择字段：

OrderNo、CustNo、SaleDate、ShipData、ItemsTotal、Amountpaid以及Balance.

７、单击OK按钮，关闭Add Fields对话框，得到如图14.17所示的Fields Editor窗口。

图14.17 字段编辑器Fields Editor

８、双击Fields Editor的控制盒关闭字段编辑器Fields Editor。

至此我们已经为Table1创建了一个自定义的字段对象Balance，下面我们把Balance字段设置成计算字段对象，使其显示每一个客户的现金余额，即此字段的值是由ORDERS. DB表中ItemsTotal和Amountpaid字段的值计算而来的。为使应用程序实现这种计算功能，完成以下步骤：

１、在Object Inspector中选择自定义字段对象Table1Balance，修改其 Calculated属性值为True。即定义Balance字段为计算字段。

２、在Object Inspector窗口中，选择Table1部件的Event页。

３、双击OnCalcField事件，为Table1OnCalcField编写事件处理过程如下：

procedure TForm1.Table1OnCalcFields(DataSet:TDataSet);

begin

Table1Balance.Value:=Table1ItemsTotal.Value-Table1AmountPaid.Value;

end;

浏览ORDERS.DB 中的记录

14.5 查询数据库中的记录

   数据库中储存着大量的数据信息，如何充分有效地查询其中的数据，对用户而言是至关重要的。如果想查询数据库，首先要确定要查询的字段要么是数据库表中的关键字段，要么是辅助索引。如果我们查询的是Paradox或dBASE数据库系统中的表，这是唯一的选择。

一般而言，查询数据库中的记录的方法有两种：Gotokey方法和Findkey方法。两种方法十分相似，主要区别在于我们如何指定查找值。这两种方法的思想是在指定列（字段）中寻找指定的查找值，如果在数据库表中找到了这个值，表中的记录指针便指向该记录，这样我们便查询到了我们需要的记录，进而可以访问找到的记录中的各项数据。

14.5.1 使用GotoKey方法查找数据记录

使用Gotokey方法查询数据库中的记录的具体步骤如下：

１、确保要查找的字段是关键字或已经为它定义了辅助索引，并保证TTable部件的属性列表中有关键字段名或辅助索引名。

２、通过调用GotoKey方法，把要查找的TTable部件置成查找模式。

３、把查找值送进被查找的Field的查找缓冲区。

４、调用TTable部件的GotoKey方法，并测试它的返回值判断查找是否成功。

如果查找成功，GotoKey返回一个True值，并且表中的记录指针指向找到的记录。 如果查找失败，GotoKey返回False，表中的记录指针不发生变化。

在这里要注意的是如何给Field的查找缓冲区赋值， 我们知道字段对象是不可见的对象，它们没有自己的名字，在大多数情况下，要使用TTable部件的FieldByName 方法到字段列表中查找字段对象以便为它赋值。但字段缓冲区也是没有名字的，当TTable部件处于查找模式时，我们只要把查找值赋给字段对象的AsString属性就可以了。AsString的作用不只是它的表面意思。它是一个转换属性，任何赋给字段对象的AsString属性的字符串都将转换成该字段对象应于数据库表中的字段的数据类型。当然AsString不能将查找值转换成BLOB、Bytes、Memo和Graphic类型的数据，用户一般也不会查找这种数据类型的字段。

下面便是说明使用Gotokey方法查找数据记录的例子。

例14.3 当用户在Edit1部件中输入客户号码并单击查找按钮，程序便开始在Table1中查找这个客户号。如果查找成功，查找信息“查找成功”便会显示在标签Label1上，被查询到的客户的电话号码显示在标签Label2上。表中的记录指针将转移到该客户记录处。并且在网格DBGrid1中以高亮度显示这一条记录。

查询数据库中的记录

下面的程序清单是查询按钮上的OnClick事件的处理程序，它是使用Gotokey方法查找数据库中的记录的。

procedure TForm1.Button1OnClick(Sender:TObject);

begin

with Table1 do

begin

Label1.Caption:=' ';

Label1.Caption:=' ';

IndexFieldName:='CustNo';

setkey;

FieldByName('CustNo').AsString:=Edit1.Text;

If GotoKey then

begin

Label1.Caption:='查找成功';

Label1.Caption:=FieldByName('Phone').AsString;

end;

else

Label1.Caption:='查找失败'；

end;

查询数据库中的记录

14.5.2 使用FindKey方法查找数据库中的记录

虽然使用上面的Gotokey方法在数据库中查找记录效果不错，但是Delphi 还提供了一种更加容易的查找方法，这就是Findkey方法，两种方法虽然很相似，但是Findkey方法更简单明了一些。

例14.4 我们可以使Findkey方法代替上面例子中的处理程序，下面是程序代码：

procedure TForm1.Button1OnClick(Sender:TObject);

var

SeekValue:string;

begin

with Table1 do

begin

Label1.Caption:=' ';

Label1.Caption:=' ';

IndexFieldName:='CustNo';

SeekValue:=Edit1.Text;

If FindKey([SeekValue]) then

begin

Label1.Caption:='查找成功';

Label1.Caption:=FieldByName('Phone').AsString;

end;

else

Label1.Caption:='查找失败'；

end;

Findkey方法和Gotokey方法的根本区别在于查找值要作为参数传递给Findkey 函数。而GOtokey是不带参数的， 它假定用户已经把查找值赋给了代表着被查找到的字段的查找缓冲区。

Findkey接受的参数是放在方括号中的，是用逗号分开的查找值数组。 数组中的每一个值都对应于特定列的查找值，即参数中允许有多个查找值，Findkey 允许用户同时查找数据库表中的多个列。上面的程序清单中的Findkey函数只接受了变量Seekvalue这一个查找值，这个查找值对应表中的字段CustNo，CustNo是表中的关键字段。如果要同时查找表中的多个字段，必须把要查找的多个字段名赋给TTable部件的IndexFieldName属性，并用逗号分开各字段，然后把每个字段的查找值赋给Findkey的参数数组中。

14.5.3 利用GotoNearest和FindNearest执行不精确查找

在我们上面讨论的查找中，要么查找成功要么查找失败，因为我们查找的是特定查找值的一个精确匹配值。Delphi还提供了一种查找方法，即不精确查找，这样的查找绝对不会失败，它总是给用户查找出一个结果来，也许这结果并不是用户需要的，但这个查找出来的结果是最接近用户要求的。在Delphi中是利用GotoNearest和FineNearest两种方法来执行不准确查找的，它们总是从数据库中查找出与查找值最接近的匹配值。如果它们查找到与查找值精确匹配的值，那当然最好不过了，如果他们找不到精确匹配的值，它们就会把与用户指定的查找值最接近的记录提交给用户。

GotoNearest的使用方法和Gotokey一样，FindNearest的使用方法和Findkey一样。跟Gotokey一样，使用GotoNearest时必须要把查找值赋给字段的查找缓冲区，两者的不同之处在于查找值的说明方式不一样，使用GotoNearest时， 说明的查找值可以是完整的也可以是不完整的，如果要对'Dunteman'进行不精确查找，在给字段的查找缓冲区赋查找值时，可以使用'Dunteman'、'Dun'或者`Du'作为查找值， 这样查找出来的结果会尽可能地接近这个值的。

如果没有找到与用户指定的查找值精确匹配的记录，Delphi会调整记录指针并停留在与查找值最接近的第一个记录上。例如如果查找`Dunteman'时，没有找到精确匹配的值，记录指针可能会停留在`Dunwoody'上或者停留在更远一些的'Event'上；如果查找'Du' 没有找到精确匹配的值，记录指针可能停留在‘Duncan’上，甚至‘Dunteman'之前， 总之Delphi会自己地调整记录指针，使之指向最接近查找值的记录并将该记录作为查找的结果提交给用户。

GotoNearest和FindNearest都返回一个Boolean值以表明查找是否成功。 它们一般都是成功的，它们总是要把记录指针移到某处。

下面的例子是用GotoNearest方法进行不精确查找。

例14.5 创建好的窗体,在编辑框中输入一个不完整的客户所在的公司名称，并且按“不精确查找”按钮，然后观察一下查找的结果并注意记录指针指向那一条记录。反复试验几次便会理解GotoNearest是如何工作的。


第十四章 简单数据库应用的创建及MASTAPP介绍（三）

DELPHI基础教程

第十四章　简单数据库应用的创建及MASTAPP介绍（三）

利用GotoNearest方法执行不精确查找

窗体中的“不精确查找”按钮的事件处理过程代码如下：

procedure TForm1.Button1Click(Sender: TObject);

begin

with table1 do

begin

IndexFieldNames:='Company';

setkey;

FieldByName('Company').AsString:=Edit1.text;

GotoNearest;

label3.caption:=FieldByName('Company').AsString;

end;

end;

读者可以利用 FindNearest 方法执行上面的不精确查找， 具体使用方法可以参看Findkey方法的使用。

在上面的例子中要设置table1的IndexFieldNames属性为Company。

GotoNearest方法进行不精确查找

14.6 修改数据库中的记录

我们掌握了字段对象的概念和如何查找数据库中的记录之后，下面我便可以很方便地修改数据库中现存的记录了，一般来说，在程序中修改数据库中的记录包括下面这些步骤：

１、在数据库中找到要修改的记录，并将记录指针移至该记录。

２、调用Edit方法将与数据库表相连的TTable部件设置成编辑状态。

３、修改一个或多个字段。

４、调用post方法将修改后的记录写入数据库。

以上这几个步骤只是概述性的，具体实现时还有很多细节需要留心，我们通过一个例子来演示上面的全过程，以便让读者进一步地了解和掌握修改记录的方法。

例14.6 我们为四个按钮分别编写了事件处理过程，用来遍历数据库中的记录并对每个客户记录的Company字段进行修改， 在程序对记录进行更新操作时窗口中的控件都是无效的，在这个例子中我们还编写了一个简单的异常代码块用来确保在更新过程中出现异常时使控件恢复正常操作。

修改数据库记录

14.6.1 Edit方法Post方法

为了能让用户通过程序修改数据库表中的记录，TTable部件必须要处在编辑状态下。在大多数情况下，数据库表都是以浏览（只读方式）方式打开的，也就是说它的每一个字段可以被读取介不能被编辑修改。调用Edit 方法能够将 TTable 部件置成编辑状态， 当TTable部件处于编辑状态后，我们才可以通过程序修改当前记录指针所指向的记录，但这样修改后的记录不会立即被写入到磁盘上的实际数据库表中。要想保存对记录的修改，必须要调用Post方法，Post方法才真正将我们对记录的修改写入实际的数据库表中。

一般来说，用来扫描整个数据库表并修改每个记录的某一个字段的程序如下所示：

with Table Do

begin

DisableControls;{在修改记录的过程中，使其它部件无效}

First; {将记录指针指向第一条记录}

while not EOF do

begin

<读取记录的一个字段值到一个变量中>

<做适当的修改>

Edit; {将TTable部件置成编辑状态}

<将修改后的字段值写回到其对应的字段>

post; {将修改后的记录写回数据库}

next; {修改下一条记录}

end;

enablecontrols; {恢复其它部件的功能}

end;

程序都是对TTable部件进行操作，因此使用With语句来防止错误的扩散是很有意义的。在这里要注意Disablecontrols方法和EnableControls方法的使用。DisableControls方法是在程序修改TTable部件中的记录时，切断TTable部件与数据访问部件TDatasource 部件的联系。否则，在对TTable中的每一修改之后，TDataSource 部件都会更新窗体中所有数据浏览部件的显示内容，这样会急剧减慢处理过程而且浪费时间。EnableControls方法是与DisableControle方法执行相反的操作，它是用来恢复TTable部件与TDatasource部件的联系并促使所有的数据浏览部件更新显示。

调用First方法是将记录指针移到数据库表中的第一条记录， 确保程序从表中的第一条记录开始进行修改。调用Next方法是将记录指针从当前的记录移到下一条记录，这样保证了从表中的第一条记录开始逐条记录进行修改，直到修改完最后一条记录。如果不调用Next方法，程序将会陷入无穷的死循环。

14.6.2 实现异常保护的TRY...FINALLY语句

上面的程序存在着潜在的危险，在实际应用过程中，可能因为某些原因使得对数据库表的更新不能进行下去。如当程序试图执行Post方法将修改后的记录写回磁盘时，而又因为某种原因磁盘没有准备好，这时便出现了异常。当出现异常时，应用程序会暂停下来并且会弹出一对话框显示有关的错误信息，在用户单击错误信息对话框之后，程序将继续执行到某一个地方去，而这个地方常常不是用户所能预料到的。 在我们的程序中， 在执行Post方法之前，窗体中所有的部件与TTable部件都已失去联系。因此，这种异常将导致窗体中显示的数据和数据库无关。

Object Pascal中的Try...Finally语句为我们解决上述异常问题提供了一个解决方法。在Delphi中仍然采用了这一语句用来处理异常问题。实际上，Try...Finally 语句是把两组语句组合在一起。语句的Try部分包含了可能产生异常的程序代码，Finally部分包含了即使发生了异常也必须执行的一条或多条语句。 在本例中， Finally 部分只包含了EnableControls方法调用这一条语句，我们将前面的代码改写并组合进Try...Finally 语句：

with Table Do

begin

DisableControls;{在修改记录的过程中，使其它部件无效}

Try;

First; {将记录指针指向第一条记录}

while not EOF do

begin

<读取记录的一个字段值到一个变量中>

<做适当的修改>

Edit; {将TTable部件置成编辑状态}

<将修改后的字段值写回到其对应的字段>

post; {将修改后的记录写回数据库}

next; {修改下一条记录}

end;

enablecontrols;

Finally;{出现异常时,执行下面的程序}

enablecontrols; {恢复其它部件的功能}

end; {结束Try...Finally语句}

end;

在保留字Try和Finally之间的代码跟前面的代码是一样的，它们用于在记录之间移动记录指针并处理对记录的修改，这一段代码可能会出现异常，当异常发生时，我们想保证执行EnableControls， 以便窗体中各控件恢复与 TTable 部件的联系， 因此我们必须将EnableControls语句放在Finally和结束语句End之间。

在这里要特别注意，请读者们不要混淆了Try...Finally语句和Try...Except 语句。如果真正想在发生异常时采取相应的处理，就要使用Try...Except语句。Try... Finally语句只是用来处理当异常出现时，使应用程序执行Finally部分的语句， 使程序继续执行下去。Try...Except语句是实现异常处理，Try...Finally语句是实现异常保护。

有了上述这些概念，我们便可以提供这个例子的一些程序代码，它涉及了所有这些内容。

程序清单：修改数据库中的记录

unit Unit26;

interface

uses

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

Dialogs, StdCtrls, Grids, DBGrids, ExtCtrls, DB, DBTables, Buttons;

type

TForm1 = class(TForm)

DataSource1: TDataSource;

customerTable: TTable;

Panel1: TPanel;

DBGrid1: TDBGrid;

Panel2: TPanel;

UpperCaseFirstAddBtn: TButton;

UpperCaseSecondAddBtn: TButton;

MixedCaseFirstAddBtn: TButton;

MixedCaseSecondAddBtn: TButton;

BitBtn1: TBitBtn;

procedure ForceCase(TargetField:String;ToUpper:Boolean);

procedure UpperCaseFirstAddBtnClick(Sender: TObject);

procedure MixedCaseFirstAddBtnClick(Sender: TObject);

procedure UpperCaseSecondAddBtnClick(Sender: TObject);

procedure MixedCaseSecondAddBtnClick(Sender: TObject);

procedure FormCreate(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

const

upper=true;

Mixed=False;

{$R *.DFM}

Function IsUpper(ch:char):Boolean;

begin

If (ch>='A')and(ch<='Z')then

IsUpper:=true

else

IsUpper:=False;

end;

procedure TForm1.ForceCase(TargetField:String;ToUpper:Boolean);

var

WorkBuffer:string;

i:Integer;

begin

with customerTable do

begin

DisableControls;

TRY

First; {将记录指针移到第一条记录处 }

While not EOF do

begin

WorkBuffer:=FieldByName(TargetField).AsString;

If ToUpper then

for i:=1 to Length(WorkBuffer)do

WorkBuffer[i]:=UpCase(WorkBuffer[i])

else

begin

for i:=1 to Length(WorkBuffer) do

If IsUpper(WorkBuffer[i]) then

WorkBuffer[i]:=chr(ord(WorkBuffer[i])+32);

WorkBuffer[1]:=UpCase(WorkBuffer[1])

end;

Edit;

FieldByName(TargetField).AsString:=WorkBuffer;

post;

Next;

end;

Finally

enableControls;

end;

end;

end;

procedure TForm1.UpperCaseFirstAddBtnClick(Sender: TObject);

begin

ForceCase('Addr1',Upper);

end;

procedure TForm1.MixedCaseFirstAddBtnClick(Sender: TObject);

begin

ForceCase('Addr1',Mixed);

end;

procedure TForm1.UpperCaseSecondAddBtnClick(Sender: TObject);

begin

ForceCase('Addr2',Upper);

end;

procedure TForm1.MixedCaseSecondAddBtnClick(Sender: TObject);

begin

ForceCase('Addr2',Mixed);

end;

procedure TForm1.FormCreate(Sender: TObject);

begin

customerTable.open;

end;

end.

14.7 插入和删除记录

虽然我们使用DBD或者在应用程序窗体中用TDBNavigator可以插入、删除表中的记录，但是任何重要的数据库应用程序都是根据最终用户的命令完成此类操作的。同样，如果我们掌握了字段对象及其用法，修改数据库中的记录，插入和删除记录将变得非常容易。

要想删除表中的某一条记录，首先将记录指针移到该记录处，然后调用delete方法，这样，当前指针所在的记录就会被删除，而且我们在进行删除操作时，不必将TTable部件设置成编辑状态。当前指针所在的记录被删除之后，被删除记录下面的所有记录都向前移动，记录指针自动移到紧挨着被删除的记录的下一条记录。在删除记录的过程中没有提醒用户是否真的想删除当前记录的信息确认框，因此在进行此项操作时要倍加小心，如果是开发应用程序，最好的办法是提供一个确认信息框确保用户不会意外删除记录。

插入一条记录也很简单，Delphi为用户提供两种方法用来插入记录到现存数据库表中，一种方法是在当前记录指针所在的记录处插入记录；另一种方法是在数据库表的尾部插入记录。这两种方法是分别调用Insert方法和Append方法实现的。但是无论是调用Insert方法还是调用Append方法在具有索引的数据库表中插入记录，增加到索引表中的记录都将按照索引顺序写入到数据库表中，也就是说对于索引表，调用Insert和Append方法的效果是一样的。事实上，Append方法只适用于那些没有索引的表，这种没有索引的表并不十分有用因而通常不创建这种表。几乎任何情况下我们都是用Insert方法来插入记录。

用户在插入记录时一般可以采用两种方式插入：逐步插入即首先建立一条空记录，然后再填充记录的各个字段，最后再将记录写回到磁盘，共分三个独立的操作步骤；而使用InsertRecord方法便可以一次将插入记录的操作完成。

14.7.1 逐步插入方法

逐步插入方法分为三个明确的步骤：先调用TTable部件的Insert方法在TTable中创建一条新的空记录，然后填充该记录的各个字段，最后调用post方法把新记录写到磁盘上的实际数据库文件中，在填充并传送记录以前，考虑插入记录到表中的什么位置是毫无意义的，假设插入的表是有索引的，在调用post方法时，Delphi会自动地把插入的新记录按照索引顺序插入到表中的正确位置。如果插入的表中没有索引，那么新记录将插入到当前指针所在记录的后面。

因此，采用逐步插入方法插入记录的程序代码一般如下形式：

With Table do

begin

Insert; {插入一条空白记录}

<填充该记录的各个字段>

post; {将插入的记录写回到磁盘文件}

end;

对于没有索引的数据库表，可以用Append方法替代Insert方法把新记录插入到表的尾部。

14.7.2 调用InsertRecord插入记录

对于简单的应用程序，Delphi允许用户用一条语句插入一个新记录，而且这个新记录可以带有任意多个新字段值。InsertRecord方法把新记录中字段的赋值语句和psot方法调用组合进一条语句中。

InsertRecord方法把记录的各个字段值组合成一个字段值数组作为它的唯一参数。在字段值数组中，可以为插入的记录的每个字段提供一个字段值，或从最左一列开始依次为任意多个字段赋值。 也就是说用户可以从表的最左边一列起， 把多个列的值同时传递给InsertRecord，直到所有字段都被赋值。用户也可以省略后面的字段，InsertRecord会用空值填充这些没有赋值的字段。用户还可以对那些明确希望用空值填充的字段传递保留字NIL来标明该字段为空。

如我们希望在Customer.DB表中插入一条记录，可以用下面的代码来实现：

InsertRecord(['2000',NIL,NIL,NIL]);

在上面的程序代码中，我们只填充了四个字段：CustNo、Company、Add1 、 Add2 。InsertRecord会自动将其它字段赋以空值。

例14.7 在这个例子中，我们在CustNo.DB表中插入和删除记录，都是在程序中完成这类操作的，而不再是使用DBD或数据浏览部件完成。

插入/删除记录

程序清单：

unit tt;

interface

uses

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

StdCtrls, Forms, DBCtrls, DB, DBGrids, Buttons, DBTables, Grids,

ExtCtrls,Mask,Dialogs;

type

TForm1 = class(TForm)

DBGrid1: TDBGrid;

DBNavigator: TDBNavigator;

Panel1: TPanel;

DataSource1: TDataSource;

Panel2: TPanel;

customerTable: TTable;

BitBtn1: TBitBtn;

Label1: TLabel;

Label2: TLabel;

BitBtn2: TBitBtn;

BitBtn3: TBitBtn;

CustNoEdit: TEdit;

CompEdit: TEdit;

procedure FormCreate(Sender: TObject);

procedure BitBtn2Click(Sender: TObject);

procedure BitBtn3Click(Sender: TObject);

procedure FormActivate(Sender: TObject);

private

{ private declarations }

public

{ public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.FormCreate(Sender: TObject);

begin

customerTable.Open;

end;

procedure TForm1.BitBtn2Click(Sender: TObject);

begin

If (Length(CustNoEdit.text)=0)and

(Length(CompEdit.text)=0)

then

MessageDlg('没有输入新记录的字段值!',mtError,[mbCancel],0)

else

with customerTable do

begin

IndexFieldNames:='CustNo';

If FindKey([CustNoEdit.text]) then

MessageDlg('已经存在这条记录!',mtError,[mbCancel],0)

else

InsertRecord([StrToInt(CustNoEdit.text),CompEdit.text,nil]);

CustNoEdit.text:=' ';

CompEdit.text:=' ';

end;

end;

procedure TForm1.BitBtn3Click(Sender: TObject);

begin

If (Length(CustNoEdit.text)=0)and

(Length(CompEdit.text)=0)

then

MessageDlg('没有输入删除的记录的字段值!',mtError,[mbCancel],0)

else

with customerTable do

begin

IndexFieldNames:='CustNo';

If FindKey([CustNoEdit.text]) then

begin

If MessageDlg('你确定要删除这条记录吗？',mtConfirmation,

[mbYes,mbno],0)=mrYes then Delete;

end

else

MessageDlg('没有你要删除的记录!',mtError,[mbCancel],0);

CustNoEdit.text:=' ';

CompEdit.text:=' ';

end;

end;

procedure TForm1.FormActivate(Sender: TObject);

begin

CustNoEdit.setfocus;

end;

end.

14.8 输入数据的有效性验证

当用户向一个数据库表中插入新记录或修改原有记录时，我们必须确保用户输入的数据是有效的，为此Delphi通过三种不同的途径用来验证用户输入的数据是否有效。

这三种途径是：基于数据库表的有效性验证、基于字段的有效性验证、基于记录的有效性验证。

基于数据库表的有效性验证：

在用户创建数据库表时就建立有效性验证机制，如在使用DBD创建一个表时， 我们可以为创建的数据库表说明一些验证手段，包括字段的最大值，最小值，图形字段的显示格式等等。在设定这些有效性验证机制时，不需要编写任何程序代码。基于数据库表的有效性验证是当数据写到数据库之前，由数据库本身来执行。Delphi也执行一些有效性验证，如在数据写到数据库之前Delphi会验证每一个字段是否被填入相应的值，有关这种途径来验证数据的有效性的详细情况请参考DBD的使用。

基于字段的有效性验证：

一般有两种方法来进行这种方式的有效性验证。

①为记录中需要设置有效性验证的字段编写Onvalidate事件处理过程。这样每当该字段的值被修改时，该字段的OnValidate事件处理过程就会被调用，进而对被修改的字段值进行验证。

②对于记录中要求非空的字段（如口令或关键字等），我们必须首先设置这些字段的Required属性为True，然后为这些字段编写OnValidate事件处理过程，这样在修改现存记录或插入新记录时，在写入数据库之前，如果要求非空的字段中没有填入适当的字段值，那么会出现错误信息提示用户必须输入字段值。

基于记录的有效性验证：

这种验证方式一般在TTable部件的BeforePost事件处理过程中进行处理，即在记录写回到数据库之前对记录的每个字段值进行有效性验证。

例14.8 在程序中对字段值的有效性进行验证。

1. 创建一个用TEdit部件浏览ORDERS.DB表的应用，如图14.25所示。

2. 修改TDataSource部件的AutoEdit属性为True。

3. 双击TTable部件打开字段编辑器Fields Editor，并单击SaleDate字段。

4. 在Object Inspector中双击SaleDate字段对象的OnValidate事件， 为该字段对象编写事件处理过程如下：

TForm1.Table1SaleDateValidate(Sender:TField);

begin

If SaleDate.Value>Now then

raise Exception.Create('不能输入一个未来的日期');

end;

当这个应用程序运行时，用户修改或插入ORDERS.DB中的记录时， 该应用程序会对销售日期（SaleDate）字段的值进行验证，该字段值不能晚于系统的当前日期，程序中调用Now方法获得系统的当前日期。如果字段值大于系统的当前日期会出现一错误信息提示框，告知用户不能输入一个未来的日期。

使用TDBComBox部件和TDBLookupComBox部件来限制用户输入字段值的范围。

创建查看orders.db表的应用，创建好的窗体如图14.25所示。窗体中显示Terms 字段的是TDBComBox部件，显示EmpNo字段的是TDBLookupComBox部件。

图14.25 用数据浏览部件限制用户的输入

TDBComBox和TDBLookupComBox部件的属性值如表14.8所示：

表14.8 窗体中各部件的属性设置

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

部 件 属 性 属 性 值

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

DataField Terms

DBComBox1 DataSource DataSource1

Items Prepaid

Net 30

COD

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

DataField EmpNo

DataSource DataSource1

DBLookupComBox LookupSource DataSource2

KeyField EmpNo

LookupField EmpNo

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

DataSource1 DataSet Table1

AutoEdit True

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

DataSource2 DataSet Table1

AutoEdit True

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

Table1 DatabaseName DemosDB

TableName orders.db

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

Table2 DatabaseName DemosDB

TableName orders.db

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

该应用运行时，当用户修改和插入记录到ORDERS.DB表中时，Terms字段的值可以从组合框中的Prepaid、Net30、COD三个值中任选，EmpNo字段的值是从另一个表Employee中获得的雇员号码，用户可以从中选择。


第十五章 数据访问部件的应用及编程（一）

DELPHI基础教程

第十五章　数据访问部件的应用及编程（一）

在这一章里我们主要介绍Delphi的数据访问部件的层次结构、 多部件之间的关系、部件的属性、方法、事件以及各部件的应用。这些部件包括：

● TSession部件

● 数据集部件(TTable和TQuery)

● TDatasource部件

● 字段对象TField

● 字段编辑器的使用

● TReport部件和TBatchMove部件

我们对这些部件的属性、方法和事件进行一般性的描述，读者在实际使用Delphi开发应用程序时，还可以通过联机帮助获得有关部件更详细的信息。

15.1 Delphi数据访问部件的层次结构

Delphi提供了强大的开发数据库应用程序的能力，它给用户提供了大量的数据访问部件。以方便程序设计人员开发数据库应用程序。这些部件中，有些部件继承了另一些部件的属性、方法和事件，也就是说多部件之间存在着继承和被继承的关系，各部件的这种关联便构成了一个层次结构

图15.1 Delphi数据访问部件的层次结构

TSession是全局性的部件，在应用程序运行时，它自动地建立，在设计阶段和运行过程中它是一个不可见的部件。

TDatabase部件是为开发客户/服务器数据库应用程序时，设置登录的数据库的有关参数的，它在数据访问部件页上。

TDataset部件是不可见的，TTable和TQuery部件是由它派生而来的，这两个部件一般被称为数据集部件，它们在数据访问部件页上。

TDatasource部件是连接数据集部件和数据浏览部件的桥梁，它在数据访问部件页上。

TFields部件对应于数据库表中的实际字段，它既可以在应用程序的运行过程中动态地生成也可以在程序设计阶段用字段编辑器创建。它是不可见的部件，在程序中我们可以通过TField部件来访问数据库记录的各个字段值。

15.2 Tsession部件及其应用

TSession部件一般用得较少，但它对于一些特殊的应用是很有用的，在每一个数据库应用程序运行时Delphi自动地创建一个TSession部件。程序设计人既不能看见该部件也不能显示地创建一个TSession 部件，但是我们可以在应用程序中全局性地使用TSession部件的属性、方法。

15.2.1 TSession部件的重要属性及作用

TSession部件的许多重要属性是用于控制数据库应用程序与数据库的连接的，在一个应用程序中，可以全局性地设置TSession的有关属性值，对与之相连接的磁盘上的数据库进行控制。TSession部件主要有下列属性：

Database属性：是TSession中可以进行连接的所有数据库的数据库名字列表，这些数据库的名字常常是实际数据库的别名，包括数据库的路径、用户名、用户登录口令等参数。

DatabaseCount属性：是TSession中可以进行连接的所有数据库的数量，它是一个整数。

KeepCounnections属性：是一个布尔型属性，用它说明应用程序是否保持与一个非活动数据库的连接。因为对于一个数据库，当该数据库中没有相应的数据集部件(TTable或TQuery)被打开时，该数据库将自动地变成非活动的数据库。缺省情况下，KeePcounnections的值是True，就是说应用程序总是保持着与数据库的连接， 即使数据库变成了非活动的数据库时，也是如此。如果将KeepConnections属性设置成False，那么当数据库由活动状态变成非活动状态时，应用程序与该数据库的连接也随之中断。

NetFileDir属性：说明BDE网络控制文件的路径名。

PrivateDir属性：说明存取临时文件的路径名。

15.2.2 TSession部件的方法：

TSession部件中的大部分方法是用于向用户提供与应用程序相连接的数据库的信息，如数据库的名字及别名，数据库中的表名以及数据库引擎BDE的有关参数等，在设计数据库应用程序时，想要获取有关数据库的信息，调用TSession部件的下列方法， 将会大大简化程序的设计。

GetAliasNames方法：调用该方法，我们可以获得数据库引擎BDE中定义的数据库别名。

GetAliasParams方法：该方法主要用于获取我们在BDE中定义数据库别名时所说明的参数值，如BDE所在的目录路径以及实际名称等。

GetDatabaseNames 方法：调用该方法可以帮助我们获得当前应用程序可以进行连接的所有数据库的名字，数据库的名字是用户使用BDE工具定义的实际数据库的别名。

GetDriverNames方法：数据库引擎BDE可以与多种数据库管理系统相连接，如客户/服务器数据库管理系统Oracle、Sybase以及本地数据库管理系统dBASE,Paradox等，BDE与每一种数据库管理系统进行连接时，都有相应的驱动程序，而且这些驱动程序都可以选择地安装。通过调用GetDriverNames方法。我们可以获得当前BDE安装的数据库驱动程序的名字。

GetDriverParams方法：BDE的数据库驱动程序中包含着多个参数，如支持的民族语言、DBMS的版本号、文件块大小等，对于服务器上的DBMS，还有数据库服务器的名字等等。

GetTableNames方法：因为每一个数据库都是由多个数据库表组成的，我们通过说明数据库名，然后调用GetTableNames方法，便可以获得该数据库中全部的数据库表的名字。

上述这些方法在调用时都需要一个字符串列表作为参数， 而且都返回一个字符串列表的值。

TSession部件还有一个叫DropConnections的方法用于控制应用程序与数据库的连接，当调用DropConnections方法时，应用程序与所有的数据库的连接将会切断。

15.2.3 TSession部件应用举例

例15.1：我们创建一个应用程序，通过调用TSession有关的方法获取当前应用程序可以进行连接的数据库的名字以及获取其中任意一个数据库中的全部数据库表的名字。

通过TSession部件获取数据库的有关信息

窗体中主要使用了两个列表框，其中列表框DatabaselistBox用于显示数据库的名字，列表框TablelistBox用于显示数据库中的表名。程序运行完后数据库的名字显示在DatabaselistBox列表框中，当用户单击DatabaselistBox列表框中的数据库名时，该数据库全部的数据库表的名字将会显示在TablelistBox列表框中。有关的程序代码如下：

程序清单15.1

unit unit31;

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;

Label1: TLabel;

Label2: TLabel;

Label3: TLabel;

ListBox1: TListBox;

ListBox2: TListBox;

ListBox3: TListBox;

TabSheet2: TTabSheet;

Memo1: TMemo;

procedure FormCreate(Sender: TObject);

procedure ListBox1Click(Sender: TObject);

procedure ListBox2Click(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 );

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 }

15.3.4 数据集中的数据维护

数据集中的数据维护主要包括数据记录的修改，插入和删除。Delphi为数据集部件提供了相应的方法用于其中的数据维护。这些方法如表15.所示。

表15.3 Delphi用于数据维护的方法

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

方 法 名 功 能

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

Edit 将数据集置为编辑状态

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

Append 投寄所有被修改的记录，将记录指针移到表中的最后

一条记录，且将数据集置为插入状态

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

Insert 投寄所有被修改的记录将数据集置为插入状态

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

Post 将插入的新记录和修改的记录写回磁盘上的数据库表，

即投寄，当投寄成功时数据集回到浏览状态，若投寄

不成功数据集仍然保持原有状态

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

Cancel 取消当前的操作且将数据集置为浏览状态

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

Delete 删除当前记录指针所在的记录且将数据集置为浏览状态

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

AppendRecord 在表的最后插入一条新记录，记录的各个字段值作为

AppendRecord的参数传递给新记录

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

InsertRecord 在当前指针所在记录的后面插入一条新记录， 记录的

各个字段值作为InsertRecord的参数传递给新记录。

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

SetRecords 修改当前记录，字段名和相应的字段值作为SetRecords

的参数

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

Edt方法：如果应用程序想对数据集中的数据记录进行修改，我们必须要将数据集设置成编辑状态。调用数据集部件的Edit方法便可以将数据集置成编辑状态，当数据集已经处在编辑状态时，调用Edit方法不会产生作用。当数据集处于编辑状态时，移动记录指针或调用post方法都可以将当前记录的修改写回到磁盘数据库表中。在程序中， Edit方法和post方法常常配合在一起使用，用于修改表中的记录。如：

Table1.Edit;

Tabel1.FieldByName('CustNo').Asstring := '1234';

Table1.st;

在上述这一段程序代码中，第一行程序是将Table1置成编辑状态，第二行程序是对当前记录指针所在的记录的CustNo字段的值修改成'1234'，第二行程序是调用post方法将对当前记录的修改写回数据库表。

Append方法和Insert 方法：这两个方法都是将数据集部件置成插入状态，以在表中插入新记录，Insert方法是在当前指针位置的记录后面插入一打新记录，Append方法是在表的尾部插入一打新记录，不过这要注意，无论用户是调用Insert方法还是Append方法插入新记录，增加记录到一个具有索引的表中时，都是按照索引顺序写入其位置，也就是说对于索引表格Insert方法和Append方法的作用是一样的，Append仅适用于没有索引的表。Insert方法和Append方法实际上是将数据集置成插入状态，并且插入一条空白记录，要真正插入一条新记录，我们必须在调用Insert或Append方法之后，还要给新记录的各个字段赋值，最后调用post方法，将插入的记录写回数据库表。调用这两种方法插入新记录的一般步骤如下：

With tabe1 DO

Begin

Insert; {调用Insert方法，插入一条空记录}

＜为记录的各字段赋值＞

Post;

End;

Post方法：数据集中的记录被修改或插入新记录时调用post方法将数据集的修改写回到数据库表。根据数据集所处的状态不同，post方法所产生的作用和效果是不一样的：

● 当数据集处于编辑状态时，调用post方法，将当前记录的修改写回数据库表

● 当数据集处于插入状态时，调用post方法，将插入的新记录写回数据库表

● 当数据集处于SetKey状态时，调用post方法，将数据集置成浏览状态(Browse状态)

post方法的调用既可以显式地调用，也可以隐含地调用，当数据集处于编辑状态或插入状态时，当移动记录指针时，Delphi会隐含地调用post方法，将将当前记录的修改写回数据库表，在程序调用Insert方法或Append方法时，也会隐含地调用Post方法，将先前的数据集的修改写回数据库表。

Delete方法：Delete方法用于删除表中的记录，调用Delete方法时，将会删除表中当前的记录，并且自动地将记录指针移到被删记录的下一条记录，同时将数据集置成Browse状态。

Cancel方法:Cancel方法用于取消当前的操作，当程序还没有调用Post方法，将对记录的修改写回数据库表时，调用Cancel方法，可以将记录恢复到没有修改之前的状态。并且在调用Cancel方法时，它总是将数据集置成Browse状态。

AppendRecord方法和InsertRecord方法：这两个方法分别与Append方法和Insert方法相似。它们都是用于在表中插入一条新记录，但AppendRecord方法和InsertRecord方法比Append和Insert方法更简单更方便一些，它们直接在表中插入一条新记录，新记录的各个字段值作为AppendRecord或InsertRecord方法的参数传递给新记录并且不需显式地调用post方法，将插入的新记录写回数据库表。在给插入的新记录赋字段值时，将由多个字段值组成的数组作为AppendRecord或InsertRecord的参数，在字段值数组中可以为每一个字段提供一个值，或从左边一列开始依次为任意多个字段赋值。也就是说，用户可以从数据库表的最左一列起，把许多列的值同时传递给InsertRecord，直到所有的字段被赋值，用户也可以省略字段序列后面的的一些字段值，InsertRecord会用空值来填充这些字段：用户也可以对那些明确希望用空填充的字段传递保留字NIl。

例如：如果表Country有Name，Captial，Continent，Area和Population字段， 并且数据集部件Table1与它相连，下面的代码便可以在Country表中当前记录的后面插入一条新记录。

Table1.InsertRecord (["中国","北京","五洲"]);

在上述代码中没有为Area和population字段赋值，InsertRecord会用空值来填这两个字段。

SetRecords方法：调用该方法可以修改表中当前记录的多个字段的值，调用该方法之前必须将数据集部件置成编辑状态，调用该方法之后，还要调用post方法，才能真正将当前记录的修改写回数据库表。调用SetRecord方法时，被修改的字段值必须要与表中实际存在的字段名对应，并且数据类型要相匹配。例如，下面的代码是修改上面刚刚插入的那条记录。

Table1.Edit;

Tabel1.SetRecord(， ， ，9600000,1200000000);

Tabel1.post;

这一段代码是修改上面刚刚插入的那条记录的Area 和Population 字段的值，而对Name,Continent和Captial字段没有修改。

在数据集部件中，还有一个重要方法Abort方法，该方法是用于取消其他方法的调用的，如在插入记录、修改记录和删除记录之前，往往需要用户确认是否真的要执行这种操作，此时调用Abort方法便可取消各种方法的调用，下面的代码是在用户删除一条记之前，让用户确认是否真的要执行删除操作。

Tabel1.BeforeDelete(DataSet:TDataSet);

If MessageDlg('真的要删除记录吗?',

mtConfirmation,mbyesNoCanel,0 <> mryes then

Abort; {取消删除操作}

关于书签(BookMark)操作；

书签操作主要用于在表中快速地定位记录指针，在应用程序中常常要保存记录指针所在的位置，在进行其他处理之后，希望能快速地返回到先前指针所在的位置，此时，使用书签将显得特别有用。有关书签操作，Delphi提供了三个方法，它们是：

● GetBookMark

● GotoBookMark

● FreeBokMark

这三个方法一般都是在一起使用，GetBookMark方法返回一个TBookMark类型的变量，该变量包含着指向当前记录的指针，GotoMark方法用于快速地将记录指针定位到具有书签的记录处。FreeBookmark方法是与GetBookMark方法相反的操作，它释放书签标志。下面的程序代码阐述了书签操作的一般方法：

BookMark : TBookMark;

<Do something>

BookMark := Table1.GetBookMark; {对当前记录作书签标志}

Table1.DisalbeControls; {切断Table1与数据察觉部件的联系}

Table.First

While Not EOF Do {对表中全部记录进行其他处理}

begin

<Do something>

Tabel1.Next;

end;

Tabel1.GotoBookMark(BookMark)

Table1.enableControls; {重新定位记录指针回到原来的位置}

Tabel1.FreeBookMark(BookMark); {删除书签BookMark标志}

15.3.5 数据集部件与数据浏览部件的连接

数据集部件TTabel和TQuery具有三个方法，DisableControls 方法、EnableControls方法、Refresh方法用于控制数据集部件和与其相连的数据浏览部件之间的连接，以及控制数据浏览部件的显示。在用户修改和更新以及遍历数据库表中的记录时，调用DisableControls方法具有重要意义，调用DisbaleControls方法以切断TTable或TQuery部件与数据浏览部件的连接，使数据浏览部件暂时失效，否则，在对TTable或TQuery部件的每次修改之后，窗体中所有与它们相连的数据浏览部件都要更新其显示内容，这亲显然会减慢处理速度。当遍历表中的记录时记录指针每移动一下，窗体中的数据浏览部件也随之更新一下其中的显示内容，在屏幕上产生闪烁。

EnableControls方法的作用与DisbaleControls方法的作用是相反的，调用EnableControls方法，使TTable或TQuery部件恢复与数据浏览部件的连接，使暂时失效的数据浏览部件恢复到正常显示表中记录信息的状态。

Refresh方法用于刷新数据浏览部件中的显示。在调用Refresh方法时，必须要确保TTable或TQuery部件是打开的。当数据集中的记录被修改之后，调用Refresh方法，数据浏览部件中显示的信息也随之改变。


第十五章 数据访问部件的应用及编程（二）

DELPHI基础教程

第十五章　数据访问部件的应用及编程（二）

15.3.6 数据集部件的事件

数据集部件TTable或TQuery具有很多的事件。为这些事件编写相应的程序代码可以进行有效性验证、计算可计算字段的值、确认对数据库表的多种操作等等。这些事件及其描述如表15.4所示。

表15.4 数据集部件常用的事件

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

事 件 描 述

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

BeforeOpen,Afteropen 在数据集部件被打开之前/之后被触发

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

BeforeClose,Afterclose 在数据集部件被关闭之前/之后被触发

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

BeforeInsert,AfterInsert 在数据集部件进入插入状态之前/之后被触发

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

BeforeEdit,AfterEdit 在数据集部件被编辑之前/之后被触发

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

BeforePost,AfterPost 在数据集部件投寄被修改的记录之前/之后被触发

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

BeforeCancel,AfterCancel 在数据集部件取消前一步操作之前/之后被触发

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

BeforeDelete,AfterDelete 在数据集部件删除当前记录之前/之后被触发

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

OnNewRecord 当建立一条新记录时被触发

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

OnCalcFields 当为表中的计算字段计算字段值时被触发

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

15.4 TTable部件及应用

在前一节里我们介绍了数据集部件TTable 和TQuery 的共同的一些属性和方法。TTable部件是Delphi数据库编程中要经常使用的最重要的部件之一，它是数据库应用程序访问数据库时必须使用的数据集部件之一，在这一节里，我们重点介绍TTable部件特有的属性和方法，TTable部件所有的属性、方法和事件都可以在联机帮助中查到。

15.4.1 TTabel部件主要的属性

DatabaseName属性和TableName属性：

DatabaseName属性是说明数据库应用程序所操作的数据库的名字，它可以是由BDE定义的数据库的别名、显式说明的数据库文件所在的磁盘路径或者由TDatabase部件定义的一个数据库名。DatabaseName属性常常是一个由BDE定义的数据库的别名。使用由BDE定义的数据库的别名代替数据库实际所在的路径和名字，好处是当实际的数据库存放的位置发生变化时，只需利用BDE简单地设置一下该数据库的别名，而数据库应用程序无需修改。有关BDE的使用请参看BDE的设置应用。TabelName属性用以说明当前TTable部件所连接的实际的数据库表。这两个属性一般都在设计阶段指定，当然在程序运行过程中也可以设置，但是要修改这两个属性时， 必须要在TTabel的Active属性为False时进行，当TTable的Active属性为True时，这两个属性是不能被修改和设置的。

TableType属性：

该属性说明与TTable部件相连接的数据库表的类型。当TableType属性设置成Default时，该属性所说明的数据库表的类型由数据库文件的扩展名决定。

● 若数据库文件的扩展名为.DB或没有扩展名，表的类型是Paradox表

● 若数据库文件的扩展名为.DBF时，表的类型是dBASE表

● 若数据库文件的扩展名为.TXT时，表的类型是ASCII表

如果TableType属性不设定为Default，那么与TTable 部件相连的数据库表的类型由TableType中的设置的值决定，不用考虑数据库文件的扩展名。

KeyExclusive属性：

该属性的一个作用是说明在数据库表中查找记录时，将记录移到与查找值相匹配的记录处还是将记录指针移到与查找值相匹配的记录后面一条记录处。 该属性是布尔型变量，当它的值为False时(缺省情况下为False)， 将记录指针移到相匹配的记录处，为True时，将记录指针移到相匹配记录的后面一条记录处。该属性另一个作用是在表中指定检索范围时，用来说明是否包括满足过滤条件的边界记录。当KeyExclusive的值为False时，检索范围包括边界记录，否则不包括边界记录，有关详细的操作请参看“限定表中记录的检索范围”。

IndexFields属性和IndexFieldsCount属性：

IndexFields的属性值是数据库表中字段名列表，它包含与TTable部件相连的数据库表中的全部索引字希。IndexFieldsCount属性说明表中索引字段的个数。这两个属性值都是只读的，只有在程序运行过程中可用。

IndexName属性和IndexFieldNames属性：

IndexName属性中存放着在建立数据库表时为数据库表定义的所有辅助索引名，它是一个辅助索引名列表，是只读属性。IndexFieldNames属性指定用于数据库表索引排序的字段名，多个字段名之间用分号隔开。例如对Customer.DB表中的客户记录按邮政编码ZipCode和客户号码CustNo排序时可以设定IndexFieldNames的值为：

ZipCode ; CustNo

在IndexFieldNames属性中指定的字段必须存在于相应的数据库表中，否则会导致错误。IndexName和IndexFieldName是互斥的，每次只能指定其中一个属性的值，不能同时为两个属性都指定属性值。

Exclusive属性：

该属性是一个布尔型属性，它标明是否以共享方式打开数据库表，如果Exclusive的值为True，当打开一个数据库表时，其他用户就不能访问该表了，若Exclusive的值为False，将以共享方式打开一个数据库表。 显然不能将其他用户正在访问的表以互斥方式打开(设定Exclusive的值为True)。对于SQL数据库服务器上的数据库表，当以互斥方式被一个用户打开时，其他用户可以读取该表中的数据，但不能修改表中的数据，当然有些数据库服务器不支持这种方式，这要具体参看有关的数据库服务器的文档。

ReadOnly属性和CanModify属性：

这两个属性都是布尔型属性，ReadOnly属性决定用户是否能够对表中的数据进行读写。ReadOnly为True 时，用户只能读取表中的数据，ReadOnly为False时，用户可以读写表中的数据(假设数据库已授权用户能够读写其中的数据库表)。CanModify属性是一个只读属性，用户不能够修改其属性值，它反映了用户对数据库表拥有的实际特权，当ReadOnly为True时CanModify将自动地被置为False，当ReadOnly为False时，如果数据库允许用户对表进行读写时，CanModify为True，否则CanModify为False。当CanModify为False时，数据库表是只读的，但不能将其置成编辑状态或插入状态；当CanModify属性为True时，虽然数据库表对应的数据集部件可以置成编辑和插入状态，但是这并不意味着用户能够插入和修改表中的数据，因为这还要受到其他因素的限制，如用户对SQL数据库服务器的访问权限等的限制。

TTable部件还有其他一些属性请参看联机帮助

15.4.2 TTable部件的方法及应用

15.4.2.1 设定数据库表的使用范围

在我们实际应用中的数据库表中常常存放着大量的数据信息，其中包含着很多的记录，而我们的应用程序可能只需对其中一部分记录进行操作，因此，为应用程序指定一个使用范围就显得特别重要了，为方便有效地指定数据库表的使用范围Delphi为TTable部件提供了下列方法供用户使用：

● SetRangeStart和EditRangeStart方法

● SetRangeEnd和EditRangeEnd方法

● SetRange([Start Values],[End Values])方法

● ApplyRange方法

● CancelRange方法

1. SetRangeStart方法

用于指定检索范围的起始记录，调用SetRangeStart方法之后，可以为起始记录的一个或多个字段指定相应的字段值。SetRangeEnd方法用于指定检索范围的结束记录，调用SetRangeEnd方法之后，可以为结束记录的一个或多个字段指定相应的字段值。

2. SetRange方法

SetRange方法包含了SetRangeStart和SetRangeEnd方法的功能，它可以同时指定检索范围的起始和结束记录，起始记录和结束记录的字段值以数组形式送给SetRange，其基本形式是：

SetRange([起始值]，[结束值])

3. ApplyRange方法

根据SetRangeStart,SetRangeEnd或SetRange方法说明的检索范围的起始和结束记录，具体设定一个检索范围，调用ApplyRange方法之后， 应用程序只能对检索范围内的记录进行有关的操作。

4. CancelRange方法

CancelRange方法的作用与ApplyRange方法的作用是相反的，这是取消为表设定的检索范围，调用CancelRange方法之后应用程序可以对表中全部记录进行有关的操作。

在这里要注意的是：如果我们使用的是paradox表或dBASE表，在调用SetRangeStart，SetRangeEnd以及SetRange方法时，只能为表中的索引字段或定义的索引指定相应的字段值，以设定检索范围。如果使用SQL数据库服务器中的数据库表，可以为IndexFieldNames属性中指定的字段指定相应的字段值。

例如：假设Table1与Customer.DB表相连，Customer.DB中一个索引字段是CustNo，同时应用窗体中有两个编辑框StartVal和EndVal用于输入起始、结束记录的字段CustNo的值，下面的程序代码便可以为我们设定一个检索范围：

Tabel1.SetRangeStart; {指定检索范围的起始记录}

Tabel1CustNo.AsString:= StartVal.Text {为起始记录的CustNo字段指定字段值}

Tabel1.SetRangeEnd; {指定检索范围的结束记录}

if EndVal.Text <> ' ' then

Tabel1CustNo.AsString := EndVal.Text; {为结束记录的CustNo 字段指定字段值}

Tabel1.ApplyRange; {根据检索范围的起始、结束记录设定检索范围}

注意上面的程序代码，在为结束记录的CustNo字段指定字段值时， 首先检查EndVal的值是否为空，如果EndVal的值为空，那么设定的检索范围没有包含一条记录， 因为没有任何记录的字段值小于NIL；如果StartVal的值为空，那么检索范围将从表中的第一条记录开始，因为表中任何记录的字段值都大于空(NIL)。

上述代码可以用SetRange方法改写成：

If EndVal.Text <>' ' then

Tabel1.SetRane([StartVal.Text].[EndVal.Text]);

Table1.ApplyRange;

EditRangeStart和EditRangeEnd方法的使用完全类似于SetRangeStart和SetRangeEnd方法，只是调这两个方法是设定一个可编辑的范围。

又如：假设一个表中的一个索引包含两个字段LastName和FirstName，我们为索引中的一个字段或多个字段指定相应的字段值，设定数据库表的使用范围。

Table1.SetRangeStart;

Table1.FieldByName('LastName').Asstring := 'Smith';

Table1.SetRangeEnd;

Tabel1.ApplyRange;

上述代码设定的范围包括LastName字段的值大于或等于Smith的所有记录。而下面的代码设定的范围则包括LastName字段的值大于或等于Smith且FirstName字段的值大于或等于'J'的记录。

Table1.SetRangeStart;

Table1.FieldByName('LastName').Asstring := 'Smith';

Table1.FieldByName('FirstName').Asstring := 'J';

Table1.SetRangeEnd;

Tabel1.ApplyRange;

15.4.2.2 查找数据库表中的记录

如果想查找数据库表中的记录，必须想指定查找记录的一些字段的字段值，然后在表中进行检索，检索出与查找值相匹配的记录来。如果我们是在Paradox或dBASE数据库中的表中查找记录，那么查找值所对应的字段必须是表中的关键字段或辅助索引字段。如果查找SQL数据库服务器中的表，那么查找值必须是表的IndexFieldNames属性中指定的字段。

Delphi提供了两种方式在数据库表中查找记录：Goto方式和Find方式。这两种方式十分相似，它们的主要区别在于为查找指定查找值的方法不一样。

使用Goto方式进行数据查找使用的方法有SetKey方法、GotoKey方法和GotoNearest方法。其实际步骤如下：

①确保要查找的字段是关键字段或辅助索引字段。

②调用SetKey方法把与表对应的TTable部件置成查找状态。

③把查找值赋给相应的字段。

④调用GotoKey方法，并测试它的返回值检验查找是否成功。

假设Table1对应的表中第一个字段是关键字段，Edit1是应用窗体中的一个编辑框，用户可以通过Edit1输入查找值。下面的代码将通过Goto方式进行查找。

Table1.SetKey; {将Table1置成查找状态}

Table1.Field[0].AsString := Edit1.Text; {指定查找值}

Table1.GotoKey; {进行查找}

上面最后一行代码是根据用户指定的查找值，在表中执行查找。查找的结果有两种，也许成功也许失败，这是由调用GotoKey方法之后返回的布尔值来决定，如果返回True，那么查找成功，并且记录指针会指向与查找值匹配的记录，如果返回Fale，那么查找失败，记录指针的位置不发生变化。下面的代码可以测试调用GotoKey方法之后的返回值，告知用户查找是否成功。

Table1.SetKey;

Table1.Field[0].AsString:= 'Smith';

If not Table1.GotoKey then

ShowMessage('记录没找到')

在这一段代码中，如果在表中没有找到第一个字段值为Smith的记录，该应用程序会弹出一个对话框告知用户“记录没有找到”。

如果在表中存在多个关键字段或辅助索引中包含多个字段时，你在进行查找时，只想为第一个字段指定查找值，那么必须要设置TTable部件的KeyFieldCount的属性值为1。如果想为多个字段指定查找值，只能为相邻的字段指定查找值，例如辅助索引中共有三个字段，那么我们只能为第一个字段、第一和第二个字段、第一和第二以及第三个字段指定查找值，而不能为第一和第三个字段指定查找值。

GotoNearest方法的使用与GotoKey方法完全一样，只是它用于不精确查找，它不要求查找结果与查找值精确匹配，当表中有与查找值精确匹配的记录时，它将记录指针移到该记录处，当表中没有与查找值精确匹配的记录时，它会查找出与查找值最接近的记录，并将记录指针移到该记录处。

下面是应用GotoNearest方法的一段代码：

Table1.SetKey;

Table1.Fields[0].AsString:= 'Sm';

Table1.GotoNearest;

执行上述代码后，若表中存在第一个字段值等于Sm的记录时，记录指针将移到该记录处，若表中不存在第一个字段值等于Sm的记录，而存在第一个字段值等于Smith的记录，那么记录指针会移到该记录处。

如果我们不是以数据库表中的关键字段作为查找字段，我们也可以为TTable部件的IndexFieldName属性中的字段或IndexName属性中的字段指定查找值进行数据查找。例如，假设Customer表中有一个名叫CityIndex的辅助索引，我们为CityIndex中的字段指定查找值进行查找时，首先设置TTable部件的IndexName属性为CityIndex，然后再进行查找，下面是具体的程序代码：

Table1.IndexName := 'CityIndex';

Table1.Open;

Table1.SetKey;

Table1.FieldByName{'City').AsString := Edit1.Text;

Table1.GotoKey;

使用Find方式：使用Find方式在数据库中进行数据查找的方法有：FindNearest方法和FindKey方法。

FindKey方法和FindNearest方法为数据查找提供了一个简单的方法，它们将SetKey、指定查找值、执行查找三个步骤融合在一步里完成，它们在指定查找值时，是把各字段的查找值组成一个数组传给FindKey或FindNearest。下面是使FindKey方法的一个例子。

假设Tabel1对应的表中的第一个字段是关键字段。

Table.FindKey([Edit1.Text]);

如果用GotoKey方法完成这一功能则需要编写下面代码：

Table1.SetKey;

Table1.Fields[0].AsStrine := Edit.Text;

Table1.GotoKey;

FindKey方法和FindNearest方法的区别与GotoKey和GotoNearest方法的区别是一样的。

15.4.2.3 创建主要──明细数据库应用

TTable部件中MasterSource属性和MasterFields属性是用于定义两个数据库表的一对多的关系。MasterSource属性指定主表对应的TDataSource部件，MasterFields属性指定主表和明细表之间建立联系的字段，主表和明细表之间建立一对多关系时，可能不只是基于一个字段，可能有多个字段。如果有多个字段，那么在说明MasterFields属性时，多个字段之间要用分号隔开。如Table1.MasterFields := 'OrderNo;CustNo'。在设计阶段可以使用字段连接设计器(Field Link Designer)为两上表创建一对多的关系，在Object Inspector 中双击TTable部件的MasterFields便可以打开Field Link Designer，进行一对多关系的创建。 如创建Customer.DB表和Order.DB表之间的一对多关系时，使用Field Link Designer 如图15.5所示。

图15.5 使用Field Link Designer创建一对多关系

Field Link Designer提供了一种可视化的方法来创建主要──明细表之间的一对多关系。图中Available Indexes组合框中存放着明细表中的关键字段和索引字段，可以选择索引字段进行连接。在主表中选择一个用于连接的关键字段，然后将其与明细表中相应的关键字段连接，单击Add按钮，主要──明细表的连接字段将显示在Joined Fields列表框中，如：

CustNo->CustNo

15.5 TDataSource部件及其应用

TDataSource部件是开发数据库应用程序中用到的非常重要的部件，它是连接数据集部件TTable或TQuery和数据浏览部件的桥梁。TDataSource部件本身十分简单，它所拥有的属性、事件和方法都比较少，在使用该部件时无需作太多的工作，它主要是为数据浏览部件服务的，如果在应用程序中没有使用数据浏览部件，我们也没有必要为应用程序设置TDataSource部件。

15.5.1 TDataSource部件的属性

TDataSource部件除了其他部件都拥有的Name属性和Tag属性之外，主要有下面几个属性：

DataSet属性：该属性说明TDataSource部件从中获取数据的数据集的名字，它可以是TTable部件的名字，也可以是TQuery部件的名字，甚至还可以指定其他窗体内的数据集作为该属性的值，如在下面的程序中我们指定窗体Form2中的table1作为窗体Form1中的DataSource1的DataSet属性值：

TForm1.Formcreate(Sender : Tobject);

Begin

DataSource1.DataSet := Form2.Table1;

end;

Enable属性：Enable属性可以暂时性地切断TDataSource部件和与之相连的数据集部件的连接。这是一个布尔型变量。当它的值为False时，TDataSource部件和数据集部件的连接被切断，且所有与TDataSource部件相连的数据浏览部件中将变为一片空白，不显示任何数据信息。当Enabled的值变为True时，TDataSource部件和数据集部件的连接恢复，且与TDataSource部件相连的数据浏览部件恢复显示数据。不过要实现上述这些功能，一般不使用TDataSource部件的Enabled属性，而是调用数据集部件的DisableControls方法和EnableControls 方法，因为调用这两个方法可以方便地控制与数据集部件相连的所有TDataSource部件以及与TDataSource部件相连的数据浏览部件。

AutoEdit属性：这是一个布尔型变量，它用于说明是否将与TDataSource部件相连的数据集置于编辑状态。当AutoEdit的值为True时，应用程序运行时，与TDataSource相连的数据集部件自动地被设置成编辑状态，当用户在与TDataSource部件相连的数据浏览部件中输入新的值时，数据集部件中的记录也随之改变。如果AutoEdit的值为False，用户想通过数据浏览部件或程序修改数据集中的记录，必须要调用数据集部件的Edit方法，将其置为编辑状态之后才能够进行。

15.5.2 TDataSource部件的事件

TDataSource部件具有三个事件：

● OnDataChange事件

● OnStateChange

● OnUpdataData

OnDataChange事件：当与TDataSource相连的数据集中的记录指针的位置发生改变时，该事件就被触发，也就是说当程序调用数据集部件的Next、Previous、Insert、Append等方法导致记录指针的位置发生改变时，便会触发该事件。该事件一般用于保持应用中多个部件之间的同步。

OnUpdataData事件：当数据集部件中当前记录将要被修改时，触发该事件。例如在程序调用post方法之后但在修改后的数据记录真正被写回磁盘中的数据库文件之前触发该事件，在应用中使用非数据浏览部件时要它与数据集保持同步时常使用该事件进行相关的处理。

OnStateChange事件：当与TDataSource部件相连的数据集部件的状态发生改变时， 便触发该事件。因为数据集部件的State属性标明了数据集部件当前所处的状态，当数据集的状态发生变化时，使用该事件进行有关的处理是很有用的，在一个具体的应用中， 数据集部件的状态常常是频繁地变化的，为了跟踪数据集部件的状态变化， 可以用下面例子中的程序代码将数据集部件当前的状态显示在一个标签上：

TForm1.DataSource1OnStateChange(Sender : Tobject);

var

S : String;

begin

Case Table1,State of

dsInactive : S := 'Inactive';

dsBrowse : S := 'Browse';

dsEdit : S := 'Edit';

dsInsert : S := 'SetKey';

dsSetKey : S := 'SetKey';

end;

Label1.Caption := S;

end;

类似地我们也可以通过检测数据集部件的状态来控制有关的按钮和菜单项是否有效。 例如：在一个应用窗体中有一个InsertBtn按钮，用于控制向数据集部件table1对应的数据库表中插入记录；还有一个CancelBtn按钮用于控制是否取消用户对当前记录的修改或插入新记录。下面的程序代码根据Table1的状态来控制这两个按钮的功能(是否有效，在窗体是否变灰暗)。

Form1.DataSource1OnStateChange(Sender : Tobject);

begin

InsertBtn.Enabled := (Table1.State = dsBrowse);

CancelBtn.Enabled := Table1.State in [dsInsert,dsEdit,dsSetKey]

end;

上面的代码中，当Table1处于浏览状态(Browse状态时)， 用户是不能够向数据库表中插入新记录的，此时InsertBtn按钮将变灰暗即无效。当Table1不处于Browse状态时，InsertBtn按钮有效，用户是可以向表中插入新记录。同理，只有当Table1处于特入状态(Insert状态)或编辑状态(Edit状态)或查找状态(SetKey状态)时，CancelBtn按钮才有效，也即用户可以取消当前插入的记录、修改当前的记录以及查找到的结果等。

15.6 字段部件和字段编辑器的使用

字段部件有时又称字段对象它对应着数据库表中的列即字段，字段对象是不可见的部件，在Delphi中有两种方式创建字段部件：

①在应用程序运行过程中，随着数据集部件被激活，对应于数据库表中每一列的字段部件便动态地被创建。

②在设计过程中，程序设计人员利用字段编辑器(Fields Editor)可以创建永久性的字段部件，即使字段对象对应的数据库表的结构发生了变化时，这些字段部件也不会发生变化。

既然字段部件是对应于数据库表中的各个字段的，而数据库表中的字段有多种数据类型，所以字段部件相应也有多种类型，字段部件的类型与数据库表中的字段的数据类型的对应关系如表15.5所示。

表15.5 字段部件的类型

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

字段部件的类型 对应的数据类型

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

TStringField 字符串类型的字段

TSmallIntField 短整数类型的字段 -32768－32767

TIntegerField 整数类型的字段

TWordField 正整数类型的字段0－65535

TBooleanField 布尔型字段

TFloatField 浮点数类型的字段

TCurrenCyField 货币型字段

TDataField 日期型

TTimeField 时间型

TBCDField 小数位数固定的浮点数

TDataTimeField 日期时间型字段

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

我们在本书中只介绍一些常见类型的字段部件的使用，其他类型字段部件的使用可以参看联机帮助文件。


第十五章 数据访问部件的应用及编程（三）

DELPHI基础教程

第十五章　数据访问部件的应用及编程（三）

15.6.1 字段部件

字段部件在应用程序中始终是不可见的部件。在程序运行过程中是如此，在程序设计阶段也是如此，但是它在应用中起着非常重要的作用，可以说它是所有数据浏览部件从数据库表中显示、编辑数据的基础。这是因为字段部件直接对应着数据库表中的字段，浏览和修改表中的数据必须要通过字段部件，同时字段部件所拥有的属性可以用来说明数据库表中对应的字段的数据类型、当前的字段值、显示格式、编辑格式等，字段部件的事件如OnValidate可以用来设定输入字段值时进行有效性检验。

数据库表的每一列在应用程序中都有其对应的一个字段部件，在缺省情况下，当TTable或TQuery的Active属性被置为False或调用close方法时，与表中各列对应的字段部件也随即消失，要想为应用程序创建永久性的字段部件，我们必须要在程序设计阶段使用字段编辑器(Fields Editor)来创建。使用字段编辑器创建永久性字段的好处是：我们在程序代码中利用永久性字段部件可以更加有效、方便、可靠地访问数据库表中记录的各字段值， 在任何时候我们都可以以同样的字段顺序、固定的字段显示表中的记录，即使数据库表的结构已发生了变化。当然如果在数据库表中与字段部件对应的字段已经不存在时，应用程序就不能正常地执行下去了，Delphi会弹出一个错误信息框，告诉用户表中的字段已经不存在了。

15.6.1.1 字段部件的属性及应用

字段部件具有很多的属性，通过设置字段部件有关的属性，可以控制字段对象在数据浏览部件中的显示方式、字段值能否被修改等。特别是对于用字段编辑器创建的永久性的字段部件，我们在程序设计阶段便可以在Object Inspector中方便地选取字段部件， 进行有关属性的设置。

字段部件的主要属性如表15.6所示，该表中列出的属性只是字段部件的部分属性，它主要用来控制字段对象的显示方式。

表15.6 字段部件的主要属性

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

属性名 功 能

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

Alignment 说明字段值在数据浏览部件中显示时的对齐方式：

左对齐、右对齐、居中三种方式。

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

Calculated 说明字段是否是计算字段，属性值为True时，该

字段是计算字段、字段值可以根据表中其它字段

的值计算得出。

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

Currency 等于true时，以货币格式显示数值，等于False时，

不以货币格式显示数值型数据。

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

DisplayFormat 用于说明字段值在数据浏览部件中的显示格式

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

DisplayLabel 字段在网格(TDBGrid部件)中显示时，为字段指定

显示标题。

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

DisplayNidth 字段在网格(TDBGrid部件)中显示时，为字段指定

显示宽度，单位是字符数。

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

EditFormat 说明字段在数据浏览部件中的编辑输入格式

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

EditMask 在进行字段值的编辑输入时，限定输入字段值的

过滤条件(即字段值的范围)。

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

FieldName 该字段部件对应实际数据库表中的字段的名字

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

Index 该字段部件在数据集所有字段部件中的顺序号

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

MaxValue 说明可以为该字段输入最大的数值

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

MinValue 说明可以为该字段输入最小的数值

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

Name 字段部件的名字

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

ReadOnly 等于true时，只能读取该字段的字段值，不能修改；

等于False时，可以对该字段的字段值进行读写。

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

Size 说明字段的大小，单位是字符数

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

Visible 为True时，该字段可以在TBDBGrid部件中显示；

为False时，该字段不能在TDBGrid部件中显示

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

表15.6中的属性并不是所有类型的字段部件都拥有的，如一个TStringField类型的字段部件是没有Currency、MaxValue、MinValue和DisplayFormat属性的，一个TFloatField类型的字段部件是没有Size属性的。

对于布尔型属性，在设计过程中的Object Inspector中双击该属性，该属性的值将会在True和False之间来回切换，其他属性需要用户输入属性值或从下拉式列表框中选取属性值。所有的属性都可以通过程序代码进行设置。大多数属性可以独立地设置，只有DisplayFormat，EditFormat和EditMask是相互联系的。在设置它们的属性值时一定要确保相互协调。

利用EditMask属性为字段设定编辑模式：

为字段部件设置一定的EditMask属性值，当编辑输入该字段的字段值时，用户只能根据EditMask设定的编辑模式进行编辑或输入字段值。在为EditMask属性设置属性值时可以用手动方式也可以用输入模式编辑器来完成，当为某字段部件设置EditMask属性时，双鼠标双击EditMask属性便可以打开输入模式编辑器(Input Mask Editor) 。例如在为Customer.DB表的Phone字段设定编辑模式时，首先在Object Inspector中选取与Phone字段对应的Table1Phone字段对象，然后双击EditMask属性，打开输入模式编辑器。

字段输入模式编辑器

在字段输入模式编辑中可以选择一种输入模式，而且在TestInput编辑框中输入字段值进行检验。

因为TStringField类型的字段部件没有DisplayFormat属性，但是可以把EditMask属性当DisplayFormat属性使用。

设定字段的显示和编辑格式：

Delphi本身为某些类型的字段对象提供了设定其显示和编辑格式的例程，并且为字段部件的DisplayFormat和EditFormat属性指定了缺省值，例如对于与浮点型数值字段对应的TFloatField类型的字段部件，而且该字段部件的Currency属性设置为True 时，字段值1234.56的显示格式为$1234.56，编辑格式是1234.56。表15.7是Delphi提供了设置字段显示和编辑格式的例程。

表15.7 字段格式例程

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

例 程 名 运用的字段对象

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

FormatFloat TFloatField,TCurrencyField

FormatDateTime TDateField,TTimeField,TDateTimeField

FormatInteger TIntegerField,TSmallIntField,TWordField

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

上述这些用于设定日期时间类型、数值型以及货币型字段的显示和编辑格式的例程，都是按国际上通行格式来设定相应类型字段的格式的，用户可以自己设置字段部件的DisplayFormat和EditFormat属性，来设定适合自己使用的格式，还可以为有关字段对象的OnGetText和OnSetText事件编写代码来设定字段的显示和编辑格式。

15.6.1.2 字段部件的事件及应用

字段部件常需处理的事件如表15.8所示

表15.8 字段部件的事件

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

事件名 用 途

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

OnChange 当字段部件的字段值发生改变时，触发该事件

OnGetText 当字段部件获得字段值时，触发该事件

OnSetText 当字段部件被设置字段值时，触发该事件

OnValidata 当字值被修改或插入新的字段值时，对字段值

进行有效性检验时，触发该事件

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

用户想自己设定字段的显示和编辑格式时，可以编写OnGetText事件和OnSetText事件的处理过程，以达到设定字段的显示和编辑格式。

15.6.1.3 字段部件的类型转换函数及使用

字段部件具有一些内部函数用于转换字段值的类型，对于不同的字段类型，这些转换函数的作用是不一样的，表15.9概括了不同类型的字段及转换函数的作用。

表15.9 字段部件的转换函数

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

字段类型 AsString AsInteger AsFloat AsDatetime AsBoolean

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

TStringField 转换成 转换成整数 若能转换 日期 转换成布型

Stringg型 (若能转换) 则转换成 (若能转换)

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

TIntegerField

TSmallField 字符型 整数型 浮点型 不允许 不允许转换

TWordField

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

TFloatField

TCurrencyField 字符串型 舍入成整数 浮点型 不允许 不允许

TBCDField

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

TDateField

TDateTimeField 字符串 不允许 浮点数 日期型 不允许

TTimeField

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

TBooleanField 转换成Time 不允许 不允许 不允许 布尔型

或False

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

TBytesField

TVarBytesField 字符串 不允许 不允许 不允许 不允许

TBlobField

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

TMemoField 二进制 不允许 不允许 不允许 不允许

TGraphilField 字段

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

上述这些转换函数可以在任何与字段部件有关的表达式中使用，只要是表15.9中允许进行转换的数据类型，这些转换函数其实是当做字段部件的属性来使用的，它们可以出现在赋值语句的两边。例如下面的程序代码是将字段部件TableMyField的字段值转变成字符串类型的数据，并将它赋给编辑框Edit1的Text属性：

Edit1.Text := TableMyField.AsString;

而下面的代码是进行相反的操作，它将编辑框部件Edit1的Text属性值以字符串的形式赋给字段TableMyField，TableMyField通过AsString接受字符串并将其转变成自身的数据类型。

TableMyField.AsString :=Edit1.Text;

15.6.1.4 字段部件的访问

字段部件对应着数据库表中实际的字段，用户要读写数据库表中的字段值其实是通过访问相应的字段部件进行的。在前面的章节中我们介绍过在Delphi的数据库应用程序中有两类字段部件：一类是利用字段编辑器创建的永久性字段部件；另一类是随着数据集部件被激活(被打开)而动态生成的字段部件。对于永久性字段部件的访问可以直接调用使用字段部件的名字进行。假设我们在设计阶段利用字段编辑器创建了对应于Customer.DB表中Company字段的字段部件Table1Company，下面的代码访问Company字段的字段值，并将该字段值显示在编辑框部件Edit1中。

Edit1.Text := Table1Company.Value;

因为company字段是字符串类型的数据，它与Edit1中的数据类型相匹配的，因此可以直接使用字段部件的Value属性读取字段值。如果两个变量的类型不匹配，则要使用表15.9中的转换函数进行字段值的读取。例如：要读取Customer.DB表中的CustNo字段的值并将它显示在编辑模框Edit1中，假设我们已用字段编辑器(Fields Editor)创建了CustNo相应的字段部件，Table1CustNo，则程序代码如下：

Edit1.Text := Table1CustNo.AsString;

访问动态生成的字段部件相对要困难一些，因为动态生成的字段部件是没有自己的名字的，我们必须利用特殊的手段获得数据库表中各字段对应的字段部件，然后对字段进行访问。一般采用的方法有两种：

● 使用数据集部件的Fields属性

● 使用数据集部件的FieldByName方法

1. 使用数据集部件的Fields属性访问数据库表中各字段

数据集部件的Fields属性是与数据集部件相连的数据库表中各个字段对应的动态字段部件的名字列表，因此我们可以通过Fields属性的下标(即索引号)来访问各字段部件，从而达到访问数据库表中的各个字段，索引号从0开始，也就是说数据库表中第一个字段对应着Fields列表的第一行即0索引，第二个字段对应的Fields的索引号为1，以此类推。下面的例子是访问Customer.DB表中的第一个字段并在编辑框Edit1中显示其字段值。假设Table1与数据库表Customer.DB相连。

Edit1.Text := Table1.Fields[0].AsString;

下面的代码是将编辑框Edit1中的字符值赋给Customer.DB表中当前记录的第一个字段，以实现修改Customer.DB表中的字段值。

Table1.Fields[0].AsString := Edit1.Text;

2.使用数据集部件的FieldByName方法访问字段部件

在数据集部件所拥有的方法中，有一个FieldByName方法，它是专门用于访问数据集部件中动态生成的字段部件的，调用FieldByName方法时，必须要把数据库表中的字段名作为参数传给FieldByName，调用该方法后便可以得到该字段所对应的字段部件，这样通过字段部件我们便可以读写表中相应的字段值了，用这种方法访问字段部件时，必须要知道数据库表中各个字段的名字，否则是没有办法调用该方法的。还是基于上面的假设。下面是访问Customer.DB表中的CustNo字段的程序代码：

Edit1.Text := Table1.FieldByName('CustNo').AsString;

Table1.FieldByName('CustNo').AsString := Edit1.Text;

在使用这两种方法访问动态生成的字段部件时，可以使用表15.9中的转换函数，在变量和字段值之间进行数据类型的转换。

15.6.2 字段编辑器的使用

字段编辑器(Fields Editor)主要是用于创建永久性的字段部件。在前面的内容中我们知道，当TTable或TQuery部件与数据库表相连接时，且TTable或TQuery部件被激活时(Active属性被设置成True或调用Open方法)，Delphi便动态地为表中各字段创建相应的字段部件，字段部件中包含着相应字段的很多信息如字段值、字段值的显示、编辑格式等，有时我们在应用程序中为了更加方便、可靠地访问数据库表中各个字段，需要创建永久性的字段部件，这时我们必须要借助于字段编辑器来实现我们的设想。字段编辑器的主要功能如下：

● 创建永久性的字段部件

● 修改永久性字段的显示属性，如显示格式、显示宽度等

● 删除永久性的字段部件

● 增加新的永久性的字段部件

● 定义计算字段(不对应数据库表中实际的字段，字段值根据表中其他字段的值计算得出)

15.6.2.1 打开字段编辑器

为TTable和TQuery部件打开字段编辑有两种方法：

● 用鼠标左键双击TTable或TQuery部件

● 选择TTable部件或TQuery部件，然后单击鼠标右键，然后从弹出式菜单中选择 Fields Editor

字段编辑器Fields Editor被打开以后，窗体的名字和数据集部件的名字会显示在窗口的标题上。

字段编辑器Fields itor中的Fields列表框是用于显示已经创建的永久性字段部件的名字的。字段编辑器Fields Editor第一次被打开时， 该列表框是空的，因为在此之前的字段部件都是动态生成的，只要Fields列表框中有字段部件，那么与数据集部件相连的数据浏览部件中只显示Fields中列出的字段的字段值，在Fields列表框中，可以通过拖放字段部件的名字来改变相应的字段值在数据浏览部件中的显示顺序，如在TDBGrid部件中根据各字段在Fields列表框中的顺序显示各字段的值。

在字段编辑器Fields Editor窗体上面的导航按钮是用来移动TTable或TQuery部件中的记录指针的，使用导航按钮可以将记录指针向前、向后移动，也可以移到第一条记录处或最后一条记录处。

字段编辑器中的弹出式菜单

15.6.2.2 增加字段部件

字段编辑器Fields Editor中的Add Fields菜单项用于向数据集部件中增加字段部件的，单击Add Fields菜单项时便会打开增加字段部件对话框，如图15.9所示。Available Fields列表框中显示出数据集部件TTable或TQuery中当前可以用于创建永久字段部件的全部的字段，也就是说Available Fields列表框中显示字段是数据库表中实际存在的字段，而且还没有为这些字段创建相应的永久性的字段部件，在缺省状态下所有的字段都被选择用于创建相应的永久性的字段部件，用鼠标单击其中的字段名可以有选择地创建其相应的永久性的字段部件，选择好有关的字段名之后，单击OK按钮便可以创建永久性的字段部件。

字段编辑器的增加字段部件对话框

15.6.2.3 删除字段部件

用字段编辑器Fields Editor为数据集部件创建好的字段部件都会显示在字段编辑器的Fields列表框中，如果用户认为其中的一些字段部件不合适或不再需要时，可以单击这些不需要的字段部件，然后单击鼠标右键弹出一佣弹出式菜单，从弹出式菜单中选择Delete菜单项，便可删除相应的字段部件，如果在弹出式菜单中单击Select All菜单项，然后选择Delete菜单项，这样会删除已创建好的所有的字段部件。某一个字段部件被删除以后，通过单击Add Fields菜单项可以重新创建，只是先前为该字段部件设定的一些属性将不复存在。

  15.6.2.4 定义新的字段部件

字段编辑器Fields Editor中的弹出式菜单中New Fields菜单项是用来为数据集部件TTable或TQuery创建用于显示目的的新的字段部件，我们可以用它来为数据库表中实际存在的字段创建新的字段部件(如改变字段的数据类型，使它的字段值被显示时不再需有关的类型转换)，但是我们使用New Fields菜单项创建新的字段部件主要是创建计算字段。计算字段并不与数据库表中实际存在的字段对应，它的字段值是根据表中其它的字段值计算而来的，具体的计算表达式由用户为TTable部件或TQuery部件的OnCalCFields事件编写程序代码时决定。

定义(创建)计算字段的过程如下：

1.单击字段编辑器中的New Fields菜单项，定义字段对话框如图15.11所示。

2.在FieldName编辑框中输入新字段部件的名字，或者从下拉式列表框中选择一个已 存在的字段部件的名字。

3.在FieldType列表框中为新字段部件选择一个字段类型。

4.单击Calculated检查框，确认定义的新字段部件是计算字段。

5.单击ok按钮，创建上述定义的计算字段部件，此时该字段部件的名字会自动地加入 到字段编辑中的Fields列表框中。

创建新的计算字段

新的计算字段创建好了之后，它是没有任何字段值的，我们必须要编写相应的程序代码，根据数据库表中实际存在的字段的字段值为计算字段的宝定义字段值，我们为计算字段所在数据集部件的OnCalcFields事件编写代码来为计算字段赋值，其步骤如下：

1.选择数据集部件TTable或TQuery

2.单击数据集部件的事件页

3.双击OnCalcFields事件为TTable或TQuery部件编写事件处理过程

  15.7 TReport部件及其应用

在一般的数据库应用程序中都包含着为最终用户提供输出报表的功能，使用Delphi开发数据库应用程序时，可以使用一个叫TReport的部件来执行报表功能的，报表的具体格式和内容是由Delphi提供的一个专用报表生成工具ReprotSmith创建的，它报表的具体格式和内容生成一个报表文件，然后为TReport部件设置相应的属性参数，由TReport部件执行报表功能。

我们可以在设计阶段双击TReport部件，调用ReportSimith工具或者在Delphi程序组内双击ReportSmith图标来调用ReportSmith工具来创建一个报表文件，具体的操作步骤和设计方法请参看ReportSimth工具的使用说明。

我们在使用TReport部件执行报表功能时，要设置TReport部件的一些的一些属性，这些属性是：

ReportName属性：说明报表文件的名字，就是用ReportSmith创建的报表文件。

ReportDir属性：说明报表文件所在的途径名。

PreView属性：这是一个布尔型属性。若它的值为True，那么在执行报表功能时，只是在屏幕上显示报表；若它的值为False，则报表内容将在缺省的打印机打印出来。

AutoUnload属性：布尔型属性，它的值为True时，在执行完一个报表功能后，自动地从内存中卸出ReportSmith工具；它的值为False时，在运行完一个报表功能后，不从内存中卸出ReportSmith工具。一般情况下，如果应用程序只有一个报表或者只有较少的报表要输出时，应设置AutoUnload属性为True，如果应用程序一次要输出多个报表，那么要应设置AutoUnload属性为False。

InitialValues属性：这是一个字符串类型的属性，它是说明报表文件中使用的变量，每一条说明一个变量。如：

ReportVAR := Value;

要详细了解创建和使用报表变量的过程请参看创建报表一节。

TReport部件要真正执行报表功能以输出一个报表需要调用Run方法。如下所示：

Report1.Run;

TReport部件所具有的重要方法如表15.10所示。

表15.10 TReport部件的方法

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

方法 功 能

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

Run 执行报表功能，输出报表

RunMacro 发送一个宏命令给Reportimith工具

Connect 预先连接报表文件和数据库，在输出报表时不

需要登录到数据库

SetVariable 改变说明的报表变量

ReCalcReport 当报表变量改变以后，重新输出报表

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

还有一些其他的数据访问部件如TBatchMove部件， 它主要用在两个数据库表之间移动或拷贝帆数据记录，具体的使用请参看本地SQL服务器的使用。

15.8 应用举例：多个窗体显示同一个数据库表

在应用当中，我们常常需要以不同的视图显示同一个数据库表中的内容，例如要在两窗体中同时显示一个数据库表中一个记录的不同字段时，我们必须要想办法使两个窗体中的数据浏览部件同步地显示数据库表中的同一条记录的不同字段的值。要想做到以不同的视图显示同一个数据库表中的记录，下面两条规则是很重要的：

● 多个TDataSource部件能够同时访问同一个数据集部件

● 在多个窗体中显示同一个表时，必须为每个窗体设置一个TDataSource部件，只须 为其中的一个窗体设置一个TTable部件

例如，如果想在窗体Form1和Form2中同时显示一个数据库表的记录，最简单可行的办法是：为Form1和Form2各设置一个TDataSource部件叫DataSource1、DataSource2，并在Form1中设置一个TTable部件Table1，连接Form1中的Datasource1和Table1，在程序运行过程中设置Form2中的DataSource2的DataSet属性为Form1中的Table1，代码如下：

Format2.DataSource1.Dataset := Form1.Table1;

这样，当Table1被打开时，两个窗体中便可以同步地显示数据库表中的同一条记录了。

一个名叫TWOForms.DPR的例子在C:\Delphi\DEMos\DB\TwoForms中(如果Delphi安装在其它的磁盘驱动器中，从相应的磁盘驱动器中可以找到该例子)，它演示了在两个窗体中显示同一个数据库表的记录。应用程序在第一个窗体中打开Contry.DB表，并在窗体中显示Name、Captial和Continent字段，在第二个窗体中显示Area和Population字段，在第一个窗体中有一个按钮用于打开第二个窗体，两个窗体中都有TDBNavigator部件，用于记录的导航。


第十六章 数据浏览部件的应用及编程（一）

DELPHI基础教程

第十六章 数据浏览部件的应用及编程（一）

数据浏览部件主要用于显示和编辑数据库表中的数据，因而它们又常常被称为数据控制部件或数据明了部件，它们在部件选择板中的DataControls页上，图16.1显示的是DataControls页上的全部数据浏览部件，其中的TDBGrid部件用于全屏幕显示和编辑数据库表中的记录，TDBNavigator用于在数据记录之间导航、插入记录、删除记录、投寄被修改的记录。

图16.1 DataControls部件页上的部件

在表16.1中，我们对DataControls部件上的各个数据浏览部件的一些特性进行描述：

表16.1 各数据浏览部件概述

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

部 件 名 称 特 性 描 述

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

TDBGrid 用网格的形式显示数据库表中的记录信息，网格中的

各列可以在设计阶段使用字段编辑器创建也可以在运

行过程中用程序设定

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

TDBNavigator 它提供了一组按钮用于数据库表中的导航，编辑修改、

插入、删除记录以及刷新数据的显示，TDBNavigator中

包含的控制按钮在设计阶段可以进行选择

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

TDBText 用于显示数据库表中当前记录的字段值

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

TDBEdit 用于显示和编辑数据库表中当前记录指定的字段值

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

TDBMemo 用于显示数据库表中的备注型字段，备注型字段中可

以包含多行字符甚至可以是BLOB(大二进制对象)数据

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

TDBImage 用于显示数据库表中的图像字段和BLOB数据

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

TDBListBox 当用户编辑修改表中当前记录的某个字段时，该部件

是一个包含多个选择项的列表框，用户可以从中选择

一个项做为字段的值

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

TDBComboBOx 该部件是一个组合框，当用户编辑修改表中当前记录

的一个指定字段时，可以直接在该部件中输入字段值

也可以单击该部件从下拉式列表框中选择一个字段值

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

TDBCheck 用于显示数据库中的字段信息的检查框，当表中字段

的值与该检查框的ValueChecked属性值相匹配时，该

检查框被选中

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

TDBReadioGroup 使用该部件可以为用户提供一组选择项，但用户只能

从中选择一个可选项

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

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

使用该部件提供多个可选项，这多个可选项是从相关

的其他表中读取的，且以列表框的形式提供给用户

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

TDBLookupCombo 该部件结合了TDBEdit部件和TDBComboBox部件的功能，

用户可以直接向该部件中输入字段值，也可以从下拉

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

可选项是从相关的其他的数据库表中读取来的。

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

16.1 数据浏览部件的基本特性

大多数的数据浏览部件是从标准部件中演变过来的，它们具有一些相似的特性，如TDBGrid部件和TStringGrid部件，TDBEdit和TEdit部件，TDBListBox和TListBox等等，只是数据浏览部件是专门用于显示和编辑数据库中记录的字段信息而已。它们的使用方法以及属性有很大的不同。在程序设计阶段，当数据浏览部件通过TDatasource部件和TTable部件连接到一个物理数据库时，便可在其中观看到数据库中的数据信息，也就是说在程序设计阶段便可以看到应用程序运行之后的效果，这种特性给我们修改应用程序带来了方便。

数据浏览部件是通过TDatasource部件连接到TTable部件中具体的字段部件的，因而数据控制部件具有一些共同的属性，用于连接TDatasource部件和TField部件。

Datasource属性：说明数据浏览部件连接的数据源部件TDatasource，数据浏览部件是从TDatasource部件中获取数据的。

DataField属性：说明数据浏览部件对应数据库表中实际的字段名称。该属性的值其实是TDataSource连接的数据集部件TTable或TQuery部件中的字段部件的名字。

因此要创建一个应用程序显示和编辑数据库表中的记录，一般要在应用窗体中放置一个数据集部件(TTable部件或TQuery部件)和至少一个数据源部件TDataSource部件以及多个数据浏览部件。其创建的一般步骤如下：

1.在窗体中放置上述所说的部件并连接数据集部件、数据源部件。

2.为各数据浏览部件设置DataSource属性值为窗休中存在的TDataSource部件的名字。

3.设置各数据浏览部件的DataField属性为数据集部件TTable或TQuery部件中存在的 字段部件的名字。

在这里需要注意的是：TDBGrid部件和TDBNavigator部件是自动地访问数据集部件中所有可以访问的字段部件的，因此它们是没有DataField属性的，对于这些部件可以跳过第3步。

Enabled属性：当数据浏览部件连接到数据集部件时，它的Enabled属性决定了数据浏览部件能否接受来自鼠标、键盘和定时器事件的消息。当Enabled属性值为False时，数据浏览部件将变为无效而不能接受外界的信息。当与数据浏览部件相连的TDataSource部件的Enabled属性为False时或与数据源部件TDataSource部件相连的数据集部件TTable或TQuery部件的Active属性为False时，数据浏览部件也会随之而变为无效。

ReadOnly属性：大多数的数据浏览部件能够用来编辑修改与之对应的字段，因而有ReadOnly属性，该属性用来控制是否可以在数据浏览中编辑修改字段的值。缺省情况下，该属性的值为False，也就是说用户可以在其中编辑修改字段的值。

当然用户要想通过数据浏览部件编辑修改数据库表中的记录字段时，还要受到其它因素的制约。除了数据浏览部件本身的ReadOnly属性设置为False外，还要设置其相应的字段部件和数据集部件的CanModify属性True性；设置TDataSource部件的AutoEdit属性为True，如果数据库表是SQL数据库服务器中的数据库表，用户必须要具有读写数据库的权限等。当然在实际的程序设计过程中并没有这样繁琐，因为很多属性的缺省值都是允许用户修改表中的记录的。

除了TDBGrid部件之外，如果通过一个数据浏览部件修改字段值，那么当光标(或焦点)离开数据浏览部件时，数据浏览部件中被修改的值就会自动地被写回到磁盘数据库中。若在焦点没有离开数据浏览部件之前，按ESC键，那么Delphi会自动地放弃其对字段值的修改。在TDBGrid部件中修改表中的记录时，只有当焦点离开当前的记录时，即记录指针移到其他记录上时，用户对当前记录的修改会被写回磁盘上的数据库表，在焦点没有离开当前记录时，按ESC键，Delphi会自动放弃对当前记录的修改。

16.2 使用TDBText部件显示表中的数据

TDBText部件是一个只读的数据浏览部件，它类似于TLabel部件。只是TDBText 部件用于显示数据库表中记录的指定字段的值。因为TDBText部件显示的是表中当前记录的指定的字段的值，因而它显示的内容也是动态的，在其中显示的内容随着记录指针的移动而变化。用TDBText部件显示Customer.DB表中的Company字段信息时可用如图16.2所示的窗体来实现。

其中各部件的属性设置如表16.2所示

表16.2 表中各部件的属性设置

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

属 性 属 性 值

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

Table1.DatabaseName DEMOS

Table1.TableName Customer.DB

Datasource1.DataSet Table1

DBText.DataSource DataSource1

DBText.DBField Company

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

16.3 使用TDBEdit部件显示和编辑表中的数据

TDBEdit部件是专门用于显示编辑数据库表中当前记录的各个字段值的数据浏览部件，在应用程序中，我们常常用一个TDBEdit部件来对应表中一个字段，通过设置TDBEdit部件的DataSource、DataField属性便可以为TDBEdit部件指定表中相应的字段。如果用户希望能通过TDBEdit部件编辑修改数据库表中的字段值，还要设置TDBEdit部件的ReadOnly属性为False，设置与TDBEdit相连的数据源部件TdataSource部件的AutoEdit属性为True以及确保与TDataSource部件相连的数据集部件TTable或TQuery部件处于编辑状态，即设置它们的CanModify属性为True。

例如，在图16.3所示的窗体中，使用多个TDBEdit部件显示和编辑Customer.DB表中当前记录的各个字段。窗体中各部件的属性如表16.3所示。

图16.3 用TDBEdit部件显示和编辑表中的数据

表16.3 窗体中各部件的属性

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

属 性 属 性 值

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

Table1.DatabaseName DEMOS

Table1.TableName Customer.DB

Datasource.DataSet Table1

DataSource.AutoEdit True

DBNavigator.DataSource DataSource1

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

窗体中其它部件都是TLabel部件和TDBEdit部件，TLabel部件用于显示表中各字段的名字，TDBEdit部件对应表中各个字段。程序运行之后如图16.4所示。用户可以在其中任何一个TDBEdit部件中修改其中的字段值。

用TDBEdit部件显示和修改表中的数据

窗体中还使用了一个TDBNavigator部件，使用它的目的是在表中移动记录指针，还可以进行修改、插入、删除记录等操作，具体的使用和操作参看 16.5 TDBNaigator部件的使用一节。

16.4 用TDBGrid部件显示和编辑表中的数据

TDBGrid部件和TDBEdit部件一样，它们是专门用来显示和编辑数据库表中的数据的，但TDBGrid部件的功能更强大一些，它可以以网格的形式显示数据库表中全部记录的所有字段信息。

用TDBGrid显示数据库表中的记录信息


第十六章 数据浏览部件的应用及编程（二）

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属性，并且列表框底部可选项可能不能被全部地显示出来。


第十七章 SQL编程（一）

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 方法，以撤消查询。


第十七章 SQL编程（二）

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中显示出来。

程序清单２是用来清除查询的， 其前两行语句跟程序清单１中的代码是一样的。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.


第十八章 Delphi客户服务器应用开发（一）

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)


第十八章 Delphi客户服务器应用开发（二）

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)


第十八章 Delphi客户服务器应用开发（三）

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 从开发平台到服务器的向上适化


第十八章 Delphi客户服务器应用开发（四）

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;


第十九章 Delphi自定义部件开发（一）

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参数，理由是对象已经是指针引用了。
v


第十九章 Delphi自定义部件开发（二）

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 当用户选择多于一个部件

时，属性应能显示 Ｎ／Ａ

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

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;


第十九章 Delphi自定义部件开发（三）

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访问这一页。

    部件帮助页应当有一个用于关键词搜索的“Ｋ”脚注，脚注中包含部件名。例如，TMemo的关键词脚注读作"TMemo Component"

● 部件增加和修改的每一个属性，事件和方法应当有一页帮助

    属性、事件或方法的帮助页应当指出该项用于哪个部件，显示声明语法和描述它的使用方法。

    属性、事件或方法的帮助页应当有一个用于关键词搜索的“Ｋ”脚注，该脚注中包含该项的名字和种类。例如，属性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;

;


第十九章 Delphi自定义部件开发（四）

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方法所做的那样。


第二十章 开发Delphi对象式数据管理功能（一）

DELPHI基础教程

第二十章　开发Delphi对象式数据管理功能（一）

    面向对象技术是九十年代的主流技术，各类应用软件如果以面向对象的方法构造并且渗透面向对象的风格将使软件具有更高的品质。在面向对象程序设计中，对象式数据管理占有很重要的地位。在Delphi中，对对象式数据管理的支持方式是其一大特色。

Delphi是一个面向对象的可视化设计与面向对象的语言相结合的集成开发环境。Delphi的核心是部件。部件是对象的一种。Delphi应用程序完全是由部件来构造的，因此开发高性能的Delphi应用程序必然会涉及对象式数据管理技术。

对象式数据管理包括两方面的内容：

● 用对象来管理数据

● 对各类数据对象（包括对象和部件）的管理

Delphi在这两方面都做的相当出色。在Delphi的早期版本Turbo Pascal 中就曾有流(Stream)、群(Collection)和资源(Resource)等专门用于对象式数据管理的类。在Delphi中，这些功能得到了大大的加强。Delphi将对象式数据管理类归结为Stream对象(Stream)和Filer对象(Filer)，并将它们应用于可视部件类库（VCL）的方方面面。它们不仅提供了在内存、外存和Windows资源中管理对象的功能，还提供了在数据库BLOB字段中对象的功能。

    在本章中将介绍Stream对象和Filer对象的实现原理、应用方法以及在超媒体系统中的应用。这对于运用Delphi 开发高级应用是很重要的。

20.1 流式对象的实现原理和应用

    Stream对象，又称流式对象，是TStream、THandleStream、TFileStream、TMemoryStream、TResourceStream和TBlobStream等的统称。它们分别代表了在各种媒介上存储数据的能力，它们将各种数据类型(包括对象和部件) 在内存、外存和数据库字段中的管理操作抽象为对象方法，并且充分利用了面向对象技术的优点，应用程序可以相当容易地在各种Stream对象中拷贝数据。

    下面介绍各种对象的数据和方法及使用方法。

20.1.1 TStream对象

    TStream对象是能在各种媒介中存储二进制数据的对象的抽象对象。从TStream 对象继承的对象用于在内存、Windows资源文件、磁盘文件和数据库字段等媒介中存储数据。

    TStream中定义了两个属性：Size和Position。它们分别以字节为单位表示的流的大小和当前指针位置。TStream中定义的方法用于在各种流中读、写和相互拷贝二进制数据。因为所有的Stream对象都是从TStream中继承来的，所以在TStream中定义的域和方法都能被Stream对象调用和访问。此外，又由于面向对象技术的动态联编功能，TStream为各种流的应用提供了统一的接口，简化了流的使用；不同Stream对象是抽象了对不同存储媒介的数据上的操作，因此，TStream的需方法为在不同媒介间的数据拷贝提供了最简捷的手段。

20.1.1.1 TStream的属性和方法

    1. Position属性

声明：property Position: Longint;

    Position属性指明流中读写的当前偏移量。

    2. Size属性

    声明：property Size: Longint;

Size属性指明了以字节为单位的流的的大小，它是只读的。

    3. CopyFrom方法

    声明：function CopyFrom(Source: TStream; Count: Longint): Longint;

CopyFrom从Source所指定的流中拷贝Count个字节到当前流中， 并将指针从当前位置移动Count个字节数，函数返回值是实际拷贝的字节数。

    4. Read方法

    声明：function Read(var Buffer; Count: Longint): Longint; virtual; abstract;

Read方法从当前流中的当前位置起将Count个字节的内容复制到Buffer中，并把当前指针向后移动Count个字节数，函数返回值是实际读的字节数。如果返回值小于Count，这意味着读操作在读满所需字节数前指针已经到达了流的尾部。

    Read方法是抽象方法。每个后继Stream对象都要根据自己特有的有关特定存储媒介的读操作覆盖该方法。而且流的所有其它的读数据的方法（如：ReadBuffer，ReadComponent等）在完成实际的读操作时都调用了Read方法。面向对象的动态联编的优点就体现在这儿。因为后继Stream对象只需覆盖Read方法，而其它读操作(如ReadBuffer、ReadComponent等)都不需要重新定义，而且TStream还提供了统一的接口。

    5. ReadBuffer方法

    声明：procedure ReadBuffer(var Buffer; Count: Longint);

    ReadBuffer方法从流中将Count个字节复制到Buffer 中， 并将流的当前指针向后移动Count个字节。如读操作超过流的尾部，ReadBuffer方法引起EReadError异常事件。

    6. ReadComponent方法

   声明：function ReadComponent(Instance: TComponent): TComponent;

ReadComponent方法从当前流中读取由Instance所指定的部件，函数返回所读的部件。ReadComponent在读Instance及其拥有的所有对象时创建了一个Reader对象并调用它的ReadRootComponent方法。

    如果Instance为nil，ReadComponent的方法基于流中描述的部件类型信息创建部件，并返回新创建的部件。

    7. ReadComponentRes方法

    声明：function ReadComponentRes(Instance: TComponent): TComponent;

ReadComponentRes方法从流中读取Instance指定的部件，但是流的当前位置必须是由WriteComponentRes方法所写入的部件的位置。

    ReadComponentRes 首先调用ReadResHeader方法从流中读取资源头，然后调用ReadComponent方法读取Instance。如果流的当前位置不包含一个资源头。ReadResHeader将引发一个EInvalidImage异常事件。在Classes库单元中也包含一个名为ReadComponentRes的函数，该函数执行相同的操作，只不过它基于应用程序包含的资源建立自己的流。

    8. ReadResHeader方法

    声明：procedure ReadResHeader;

ReadResHeader方法从流的当前位置读取Windows资源文件头，并将流的当前位置指针移到该文件头的尾部。如果流不包含一个有效的资源文件头，ReadResHeader将引发一个EInvalidImage异常事件。

    流的ReadComponentRes方法在从资源文件中读取部件之前，会自动调用ReadResHeader方法，因此，通常程序员通常不需要自己调用它。

    9. Seek方法

    声明：function Seek(Offset: Longint; Origin: Word): Longint; virtual; abstract;

Seek方法将流的当前指针移动Offset个字节，字节移动的起点由Origin指定。如果Offset是负数，Seek方法将从所描述的起点往流的头部移动。下表中列出了Origin的不同取值和它们的含义：

表20.1 函数Seek的参数的取值

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

    常量　　　　　　　值　　 　　　Seek的起点 Offset的取值

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

  SoFromBeginning 0 　流的开头 正 数

  SoFromCurrent 1 流的当前位置 正数或负数

  SoFromEnd 2 流的结尾 负 数

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

   10. Write方法

    在Delphi对象式管理的对象中有两类对象的方法都有称为Write的：Stream对象和Filer对象。Stream对象的Write方法将数据写进流中。Filer对象通过相关的流传递数据，在后文中会介绍这类方法。

    Stream对象的Write方法声明如下：

function Write(const Buffer; Count: Longint): Longint; virtual; abstract;

Write方法将Buffer中的Count个字节写入流中，并将当前位置指针向流的尾部移动Count个字节，函数返回写入的字节数。

   TStream的Write方法是抽象的，每个继承的Stream对象都要通过覆盖该方法来提供向特定存储媒介(内存、磁盘文件等)写数据的特定方法。流的其它所有写数据的方法(如WriteBuffer、WriteComponent)都调用Write担当实际的写操作。

    11. WriteBuffer方法

    声明：procedure WriteBuffer(const Buffer; Count: Longint);

    WriteBuffer的功能与Write相似。WriteBuffer方法调用Write来执行实际的写操作，如果流没能写所有字节，WriteBuffer会触发一个EWriteError异常事件。

    12. WriteComponent方法

    在Stream对象和Filer对象都有被称为WriteComponent的方法。Stream对象的WriteComponent方法将Instance所指定的部件和它所包含的所有部件都写入流中；Writer对象的WriteComponent将指定部件的属性值写入Writer对象的流中。

    Stream对象的WriteComponent方法声明是这样的：

procedure WriteComponent(Instance: Tcomponent);

    WriteComponent创建一个Writer对象，并调用Writer的WriteRootComponent方法将Instance及其拥有的对象写入流。

    13. WriteComponentRes方法

    声明：WriteComponentRes(const ResName: String; Instance: TComponent);

    WriteComponentRes方法首先往流中写入标准Windows 资源文件头，然后将Instance指定的部件写入流中。要读由WriteComponentRes写入的部件，必须调用ReadComponentRes方法。

    WriteComponentRes使用ResName传入的字符串作为资源文件头的资源名，然后调用WriteComponent方法将Instance和它拥有的部件写入流。

    14. WriteDescendant方法

    声明：procedure WriteDescendant(Instance Ancestor: TComponent);

    Stream对象的WriteDescendant方法创建一个Writer对象，然后调入该对象的WriteDescendant方法将Instance部件写入流中。Instance可以是从Ancestor部件继承的窗体，也可以是在从祖先窗体中继承的窗体中相应于祖先窗体中Ancestor部件的部件。

    15. WriteDescendantRes方法

    声明：procedure WriteDescendantRes(const ResName: String;

Instance, Ancestor: TComponent);

    WriteDescendantRes方法将Windows资源文件头写入流，并使用ResName作用资源名，然后调用WriteDescendant方法，将Instance写入流。

20.1.1.2 TStream的实现原理

    TStream对象是Stream对象的基础类，这是Stream对象的基础。为了能在不同媒介上的存储数据对象，后继的Stream对象主要是在Read和Write方法上做了改进，。因此，了解TStream是掌握Stream对象管理的核心。Borland公司虽然提供了Stream对象的接口说明文档，但对于其实现和应用方法却没有提及，笔者是从Borland Delphi 2.0 Client/Server Suite 提供的源代码和部分例子程序中掌握了流式对象技术。

    下面就从TStream的属性和方法的实现开始。

    1. TStream属性的实现

    前面介绍过，TStream具有Position和Size两个属性，作为抽象数据类型，它抽象了在各种存储媒介中读写数据所需要经常访问的域。那么它们是怎样实现的呢？

    在自定义部件编写这一章中介绍过部件属性定义中的读写控制。Position和Size也作了读写控制。定义如下：

property Position: Longint read GetPosition write SetPosition;

property Size: Longint read GetSize;

    由上可知，Position是可读写属性，而Size是只读的。

    Position属性的实现就体现在GetPosition和SetPosition。当在程序运行过程中，任何读取Position的值和给Position赋值的操作都会自动触发私有方法GetPosition和SetPosition。两个方法的声明如下：

function TStream.GetPosition: Longint;

begin

Result := Seek(0, 1);

end;

procedure TStream.SetPosition(Pos: Longint);

begin

Seek(Pos, 0);

end;

在设置位置时，Delphi编译机制会自动将Position传为Pos。

    前面介绍过Seek的使用方法，第一参数是移动偏移量，第二个参数是移动的起点，返回值是移动后的指针位置。

    Size属性的实现只有读控制，完全屏蔽了写操作。读控制方法GetSize实现如下：

function TStream.GetSize: Longint;

var

Pos: Longint;

begin

Pos := Seek(0, 1);

Result := Seek(0, 2);

Seek(Pos, 0);

end;

2. TStream方法的实现

    ⑴ CopyFrom方法

    CopyFrom是Stream对象中很有用的方法，它用于在不同存储媒介中拷贝数据。例如，内存与外部文件之间、内存与数据库字段之间等。它简化了许多内存分配、文件打开和读写等的细节，将所有拷贝操作都统一到Stream对象上。

    前面曾介绍：CopyFrom方法带Source和Count两个参数并返回长整型。该方法将Count个字节的内容从Source拷贝到当前流中，如果Count值为0则拷贝所有数据。

function TStream.CopyFrom(Source: TStream; Count: Longint): Longint;

const

MaxBufSize = $F000;

var

BufSize, N: Integer;

Buffer: PChar;

begin

if Count = 0 then

begin

Source.Position := 0;

CouNG="ZH-CN">资源文件中的部件时调用，通常程序员不需自己调用。如果读取的不是资源文件ReadResHeader，将触发异常事件。

procedure TStream.ReadResHeader;

var

ReadCount: Longint;

Header: array[0..79] of Char;

begin

FillChar(Header, SizeOf(Header), 0);

ReadCount := Read(Header, SizeOf(Header) - 1);

if (Byte((@Header[0])^) = $FF) and (Word((@Header[1])^) = 10) then

Seek(StrLen(Header + 3) + 10 - ReadCount, 1)

else

raise EInvalidImage.CreateRes(SInvalidImage);

end;

    ReadComponentRes在Windows资源文件中读取部件，为了判断是否是资源文件，它首先调用ReadResHeader方法，然后调用ReadComponent方法读取Instance指定的部件。下面是它的实现：

function TStream.ReadComponentRes(Instance: TComponent): TComponent;

begin

ReadResHeader;

Result := ReadComponent(Instance);

end;

  与ReadComponentRes相应的写方法是WriteComponentRes，Delphi 调用这两个方法读写窗体文件(DFM文件)，在后面书中会举用这两个方法读取DFM文件的例子。

    ⑷ WriteComponent和WriteDescendant方法

    Stream对象的WriteDescendant方法在实现过程中，创建了TWriter对象，然后利用TWriter的WriteDescendant方法将Instance写入流。而WriteComponent方法只是简单地调用WriteDescendant方法将Instance写入流。它们的实现如下：

procedure TStream.WriteComponent(Instance: TComponent);

begin

WriteDescendent(Instance, nil);

end;

procedure TStream.WriteDescendent(Instance, Ancestor: TComponent);

var

Writer: TWriter;

begin

Writer := TWriter.Create(Self, 4096);

try

Writer.WriteDescendent(Instance, Ancestor);

finally

Writer.Free;

end;

end;

    ⑸ WriteDescendantRes和WriteComponentRes方法

    WriteDescendantRes方法用于将部件写入Windows资源文件；而WriteComponentRes 方法只是简单地调用WriteDescendantRes方法，它们的实现如下：

procedure TStream.WriteComponentRes(const ResName: string; Instance:

TComponent);

begin

WriteDescendentRes(ResName, Instance, nil);

end;

procedure TStream.WriteDescendentRes(const ResName: string; Instance,

Ancestor: TComponent);

var

HeaderSize: Integer;

Origin, ImageSize: Longint;

Header: array[0..79] of Char;

begin

Byte((@Header[0])^) := $FF;

Word((@Header[1])^) := 10;

HeaderSize := StrLen(StrUpper(StrPLCopy(@Header[3], ResName, 63))) + 10;

Word((@Header[HeaderSize - 6])^) := $1030;

Longint((@Header[HeaderSize - 4])^) := 0;

WriteBuffer(Header, HeaderSize);

Origin := Position;

WriteDescendent(Instance, Ancestor);

ImageSize := Position - Origin;

Position := Origin - 4;

WriteBuffer(ImageSize, SizeOf(Longint));

Position := Origin + ImageSize;

end;

    WriteCompnentRes是与ReadComponentRes相应的对象写方法，这两个方法相互配合可读取Delphi的DFM文件，从而利用Delphi系统的功能。

20.1.2 THandleStream对象

    THandleStream对象的行为特别象FileStream对象，所不同的是它通过已创建的文件句柄而不是文件名来存储流中的数据。

    THandleStream对象定义了Handle属性，该属性提供了对文件句柄的只读访问，并且Handle属性可以作为Delphi的RTL文件管理函数的参数，利用文件类函数来读写数据。THandleStream覆盖了构造函数Create，该函数带有Handle 参数，该参数指定与THandleStream对象相关的文件句柄。

20.1.2.1 THandleStream的属性的方法：

    1. Handle属性

    声明：property Handle: Integer;

Handle属性提供了对文件句柄的只读访问，该句柄由THandleStream的构造方法Create传入。因此除了用THandleStream提供的方法外，也可以用文件管理函数对句柄进行操作。实际上，THandleStream的方法在实现上也是运用文件管理函数进行实际的读写操作。

    2. Create方法

    声明：constructor Create(AHandle: Integer);

    Create方法使用传入的Handle参数创建一个与特定文件句柄相联的THandleStream对象，并且将AHandle赋给流的Handle属性。

    3. Read、Write和Seek方法

    这三个方法是TStream的虚方法，只是在THandleStream 中覆盖了这三个方法，以实现特定媒介──文件的数据存取。后面会详细介绍这三个方法的实现。

20.1.2.2 THandleStream的实现原理

    THandleStream是从TStream继承来的，因此可以共用TStream中的属性和大多数方法。THandleStream在实现上主要是增加了一个属性Handle和覆盖了Create、Read、Write和Seek四个方法。

    1. 属性的实现

    Handle属性的实现正如Delphi大多数属性的实现那样，先在对象定义的private部分声明一个存放数据的变量FHandle，然后在定义的public部分声明属性Handle，其中属性定义的读写控制部分加上只读控制，读控制只是直接读取FHandle变量的值，其实现如下：

THandleStream = class(TStream)

private

FHandle: Integer;

public

…

property Handle: Integer read FHandle;

end;

2. 方法的实现

    THandleStream的Create方法，以AHandle作为参数，在方法里面只是简单的将AHandle的值赋给FHandle，其实现如下：

constructor THandleStream.Create(AHandle: Integer);

begin

FHandle := AHandle;

end;

    为实现针对文件的数据对象存储，THandleStream的Read、Write和Seek方法覆盖了TStream中的相应方法。它们的实现都调用了Windows的文件管理函数。

    Read方法调用FileRead函数实现文件读操作，其实现如下：

function THandleStream.Read(var Buffer; Count: Longint): Longint;

begin

Result := FileRead(FHandle, Buffer, Count);

if Result = -1 then Result := 0;

end;

    Write方法调用FileWrite函数实现文件写操作，其实现如下：

function THandleStream.Write(const Buffer; Count: Longint): Longint;

begin

Result := FileWrite(FHandle, Buffer, Count);

if Result = -1 then Result := 0;

end;

    Seek方法调用FileSeek函数实现文件指针的移动，其实现如下：

function THandleStream.Seek(Offset: Longint; Origin: Word): Longint;

begin

Result := FileSeek(FHandle, Offset, Origin);

end;

20.1.3 TFileStream对象

    TFileStream对象是在磁盘文件上存储数据的Stream对象。TFileStream是从THandleStream继承下来的，它和THandleStream一样都是实现文件的存取操作。不同之处在于THandleStream用句柄访问文件，而TFileStream用文件名访问文件。实际上TFileStream是THandleStream上的一层包装，其内核是THandleStream的属性和方法。

    TFileStream中没有增加新的属性和方法。它只是覆盖了的构造方法Create和析构方法Destory。在Create方法中带两个参数FileName和Mode。FileName描述要创建或打开的文件名，而Mode描述文件模式如fmCreate、fmOpenRead和fmOpenWrite等。Create方法首先使用FileCreate或FileOpen函数创建或打开名为FileName的文件，再将得到的文件句柄赋给FHandle。TFileStream的文件读写操作都是由从THandleStream继承的Read< P>

var

Stream: TStream;

begin

Stream := TFileStream.Create(FileName, fmCreate);

try

SaveToStream(Stream);

finally

Stream.Free;

end;

end;

    在Delphi 的许多对象的SaveToStream 和SaveToFile、LoadFromStream和LoadFromFile方法的实现都有类似的嵌套结构。

20.1.5 TMemoryStream对象

   TMemoryStream对象是一个管理动态内存中的数据的Stream对象，它是从TCustomMemoryStream中继承下来的，除了从TCustomMemoryStream中继承的属性和方法外，它还增加和覆盖了一些用于从磁盘文件和其它注台读数据的方法。它还提供了写入、消除内存内容的动态内存管理方法。下面介绍它的这些属性和方法。

20.1.5.1 TMemoryStream的属性和方法

    1. Capacity属性

    声明：property Copacity: Longint;

Capacity属性决定了分配给内存流的内存池的大小。这与Size属性有些不同。Size属性是描述流中数据的大小。在程序中可以将Capacity 的值设置的比数据所需最大内存大一些，这样可以避免频繁地重新分配。

    2. Realloc方法

    声明：function Realloc(var NewCapacity: Longint): Pointer; virtual;

Realloc方法，以8K为单位分配动态内存，内存的大小由NewCapacity指定，函数返回指向所分配内存的指针。

    3. SetSize方法

    SetSize方法消除内存流中包含的数据，并将内存流中内存池的大小设为Size字节。如果Size为零，是SetSize方法将释放已有的内存池，并将Memory属性置为nil；否则，SetSize方法将内存池大小调整为Size。

4. Clear方法

    声明：procedure Clear;

Clear方法释放内存中的内存池，并将Memory属性置为nil。在调用Clear方法后，Size和Position属性都为0。

    5. LoadFromStream方法

    声明：procedure LoadFromStream(Stream: TStream);

LoadFromStream方法将Stream指定的流中的全部内容复制到MemoryStream中，复制过程将取代已有内容，使MemoryStream成为Stream的一份拷贝。

    6. LoadFromFile方法

    声明：procedure LoadFromFile(count FileName: String);

LoadFromFile方法将FileName指定文件的所有内容复制到MemoryStream中，并取代已有内容。调用LoadFromFile方法后，MemoryStream将成为文件内容在内存中的完整拷贝。

20.1.5.2 TMemoryStream对象的实现原理

    TMemoryStream从TCustomMemoryStream对象直接继承，因此可以享用TCustomMemoryStream的属性和方法。前面讲过，TCustomMemoryStream是用于内存中数据操作的抽象对象，它为MemoryStream对象的实现提供了框架，框架中的内容还要由具体MemoryStream对象去填充。TMemoryStream对象就是按动态内存管理的需要填充框架中的具体内容。下面介绍TMemoryStream对象的实现。

    1. TMemoryStream属性的实现

    TMemoryStream在其protected部分增加了一个Capacity属性，该属性决定了MemoryStream所占动态内存的大小。TMemoryStream首先在private部分声明了FCapacity变量作为存储Capacity属性值的数据域，然后在protected部分声明了该属性。在属性声明的读控制部分简单读取FCapacity的值，在写控制处调用了方法SetCapacity。该方法除了给FCapacity赋值外还执行了修改Capacity属性所必需操作如状态改变等。

    下面是属性的实现：

TMemoryStream = class(TCustomMemoryStream)

private

FCapacity: Longint;

procedure SetCapacity(NewCapacity: Longint);

protected

…

property Capacity: Longint read FCapacity write SetCapacity;

public

…

end;

    写控制方法SetCapacity的实现是这样的：

procedure TMemoryStream.SetCapacity(NewCapacity: Longint);

begin

SetPointer(Realloc(NewCapacity), FSize);

FCapacity := NewCapacity;

end;

    在SetCapacity 方法先是调用Realloc重新分配内存，然后用NewCapacity的值给FCapacity赋值。Realloc方法进行某些对象状态的改变。

    2. TMemoryStream对象方法的实现

    ⑴ Realloc方法

    Realloc方法是TMemoryStream动态内存分配的核心，它的SetSize、SetCapacity等方法最终都是调用Realloc进行内存的分配和初始化工作的。它的实现如下：

const

MemoryDelta = $2000;

function TMemoryStream.Realloc(var NewCapacity: Longint): Pointer;

begin

if NewCapacity > 0 then

NewCapacity := (NewCapacity + (MemoryDelta - 1)) and not (MemoryDelta - 1);

Result := Memory;

if NewCapacity <> FCapacity then

begin

if NewCapacity = 0 then

begin

GlobalFreePtr(Memory);

Result := nil;

end else

begin

if Capacity = 0 then

Result := GlobalAllocPtr(HeapAllocFlags, NewCapacity)

else

Result := GlobalReallocPtr(Memory, NewCapacity, HeapAllocFlags);

if Result = nil then raise EStreamError.CreateRes(SMemoryStreamError);

end;

end;

end;

    Realloc方法是以8K为单位分配动态内存的，方法中的第一句if语句就是执行该操作。如果传入的NewCapacity参数值为0，则释放流中的内存。Realloc方法用GLobal FreePtr函数释放内存，用GlobalAllocPtr分配内存，用GlobalReallocPtr进行内存的重分配。如果原来的Capacity属性值为0，则调用Globa|AllocPtr否则调用GlobalReallocPtr。最后如果Result为nil则触发内存流错的异常事件，否则返回指向分配的内存的指针。

    ⑵ Write方法

    Write方法从内存流内部缓冲池的当前位置开始写入二进制数据。其实现如下：

function TMemoryStream.Write(const Buffer; Count: Longint): Longint;

var

Pos: Longint;

begin

if (FPosition >= 0) and (Count >= 0) then

begin

Pos := FPosition + Count;

if Pos > 0 then

begin

if Pos > FSize then

begin

if Pos > FCapacity then

SetCapacity(Pos);

FSize := Pos;

end;

System.Move(Buffer, Pointer(Longint(FMemory) + FPosition)^, Count);

FPosition := Pos;

Result := Count;

Exit;

end;

end;

Result := 0;

end;

    Buffer中存储要写入流的二进制数据，如果要写入的数据的字节超出了流的内存池的大小，则调用SetCapacity方法再分配内存，然后用内存复制函数将Buffer中的数据复制到FMemory中。接着移动位置指针，并返回写入数据的字节数。分析这段程序可以知道，FCapacity的值和FSize的值是不同的。

    ⑶ Clear方法

    Clear方法消除内存流中的数据，将Memory属性置为nil，并将FSize和FPosition 的值设为0。其实现如下：

procedure TMemoryStream.Clear;

begin

SetCapacity(0);

FSize := 0;

FPosition := 0;

end;

    ⑷ LoadFromStream和LoadFromFile方法

    LoadFromStream方法首先根据传入的Stream的Size属性值重新分配动态内存，然后调用Stream的ReadBuffer方法往FMemory中复制数据，结果Stream的全部内容在内存中有了一份完整拷贝。其实现如下：

procedure TMemoryStream.LoadFromStream(Stream: TStream);

var

Count: Longint;

begin

Stream.Position := 0;

Count := Stream.Size;

SetSize(Count);

if Count <> 0 then Stream.ReadBuffer(FMemory^, Count);

end;

   LoadFromFile与LoadFromStream是一对方法。LoadFromFile首先创建了一个TFileStream对象，然后调用LoadFromStream方法，将FileStream文件流中的数据写入MemoryStream中。


第二十章 开发Delphi对象式数据管理功能（二）

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对象的属性和方法。


第二十章 开发Delphi对象式数据管理功能（三）

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;


第二十章 开发Delphi对象式数据管理功能（四）

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文件方法、步骤和注意事项。


第二十章 开发Delphi对象式数据管理功能（五）

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 TMDIChild.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 := TMDIChild.Create(Application);

ReadComponentResFile(OpenDialog.FileName, DesignWin);

DesignWin.Init;

FileName := OpenDialog.FileName;

DesignWin.Caption := FFileName;

end;

end;

    DesignWin是在TMainForm中定义的TMDIChild类型的窗体部件，是卡片设计平台；FFileName是私有变量，用来保存当前编辑的卡片文件名。DesignWin的Init方法实现如下：

procedure TMDIChild.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 TMDIChild.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 TMDIChild.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[i]);

WriteRootComponent(DesignWin.Controls[i]);

{ 写相应媒体扩展信息 }

    ……

     end;

WriteListEnd;

finally.

Free;

end;

FileStream.Free;

end;

WriteInteger(MMID[i])语句是写入媒体标识。

    下面是相应的读扩展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[i]);

WriteRootComponent(DesignWin.Controls[i]);

{ 写相应媒体扩展信息 }

    ……

     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）就不行。