消息处理程序、过程类型以及事件处理程序把Delphi程序与Windows操作系统联系在一起。这就是说程序是嵌入到Windows中的。Windows不能也无法预知运行在其中的程序,因为当Windows出现时,大部分应用软件还没有编写出来。
当Windows出现时,它必须提供一种途径,使得应用程序可以响应操作系统。最后的结果是:Windows成为了一个基于消息的操作系统,而Windows程序必须响应这些消息。我将其称之为邮政服务式体系结构。最重要的是:通过响应消息,Windows应用程序只需与Windows系统松散耦合。
所有的Windows程序都必须响应消息,而Delphi程序对此尤为出色。Windows程序必须可以与任何程序通讯,而无须预先知道该特定程序所响应的消息和响应的方式。仿照Delphi中使用过程类型、事件特性和事件处理程序的方式,就可以隐藏Windows笨重的消息和事件驱动体系结构,并屏蔽不同的Windows消息和消息记录。实际上,也就是用通常的Pascal过程来屏蔽Windows消息处理程序和消息记录。
本章讨论了消息处理程序、过程类型和事件处理程序,它们在用Delphi编写的Windows程序中随处可见。由于这些技术有助于使您的程序成为整洁、健壮、出色工作的独立子系统,本章中完整地涵盖了有关的内容。
6.1 赢得对意大利细面条的战争
所谓的意大利细面条式代码,指的是耦合代码。在避免耦合代码这一点上,基本上每个人都是口惠而实不至,因此代码很容易出现耦合。赢得战争的关键策略是,采用所有可能避免耦合代码的技术,并使这些技术成为根深蒂固的习惯。要做到这一点,您有必要了解一些术语,它们有助于维护模块的分离、独立,从而不至于成为相互依赖的大块耦合代码。这里有一个代码有害的例子,您可能以前见到过。
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
Canceled : Boolean;
Procedure Process;
end;
var
Form1: TForm1;
implementation
uses Unit2;
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
begin
Process;
end;
procedure TForm1.Process;
var
I : Integer;
begin
Canceled := False;
Form2 := TForm2.Create(Self);
try
Form2.Show;
for I := 1 to 10 do
begin
if( Canceled ) then break;
Sleep( 1000 ); // simulates some processing
Form2.ProgressBar1.Position := Trunc(I *
Form2.ProgressBar1.Max / 10);
Application.ProcessMessages;
end;
finally
Form2.Free;
end;
end;
end.
unit Unit2;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs,
StdCtrls, ComCtrls;
type
TForm2 = class(TForm)
Button1: TButton;
ProgressBar1: TProgressBar;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form2: TForm2;
implementation
uses Unit1;
{$R *.DFM}
procedure TForm2.Button1Click(Sender: TObject);
begin
Form1.Canceled := True;
end;
end.
在列出的代码中,Form1模拟了主窗体,其中包含了过程Process的代码。该类定义了名为Canceled的公有布尔类型成员。在Form1中对Form2进行了实例化。对Sleep的调用模拟了Form1中可能进行的处理过程。Form1直接修改了Form2中的进度条(见图6.1)。处理将一直进行,直至所有的项都处理完毕或者Canceled被设置为True。在Form2的Button1Click事件处理程序中修改了Canceled特性。结果是,Form1必须很清楚地知道Form2的存在,反之亦然,二者之间的联系非常紧密(很清楚,二者是互相“了解”的,因为两个单元的实现部分的uses子句进行了相互引用)。
图6.1 负责进度条更新的窗体
当程序的规模十分有限时,这种类型的代码很容易被忽视。不幸的是,有用的软件很少是简单的。如果两个窗体中有一个变为对话框组件,问题就更糟糕了。考虑Form2变成对话框组件的情况(对话框组件的更多信息请参见第10章)。进一步可以认为Form1是某个复杂应用程序的主窗体,因此它拥有该应用程序中大多数的其他窗体。把Form2添加到VCL中(它是个组件,我们可以这样做),您的整个应用程序就都添加到了VCL中(不要笑,确实如此)。最后的结果形成了一个臃肿的VCL库,它依赖于非VCL的代码。当您编译应用程序时,VCL代码也会重新进行编译。当您建立VCL库时,您的应用程序也将被编译。如果出了错,组件就无法建立因而也无法装载。真是一团糟。如果还要试着给其他开发者讲述这乱成一团的代码、其工作原理、以及如何对其进行修改等,那简直是代价高昂而且灭绝人性的行为。
注意:公平地讲,必须提到定义接口会引入另一种复杂性。这与一次性付款和延期付款孰优孰劣的问题颇为相似。通过定义接口,可以使您更快地写出更多的代码。而当代码模块之间相互关系的数目和复杂程度已经难于控制时,试图解决问题可能为时已晚,因此,通过精确的接口提高代码的质量是更为可取的做法。即使对于非常简单的应用程序,定义接口的方法也可以得到非常好的效果。不幸的是,这种方法需要进行训练。程序员和审阅者需要注意并经常修改代码之间的关系,并对其提出一些简化方案。从长远看来,将解决问题的时机拖后代价要更高,等问题拖到了不能不解决的时候,可能就太晚了。
窗体或数据模块之外的紧耦合代码,同样会导致问题。两个并非窗体的类也会产生前面的代码中的问题,如果在项目中很晚才发现这种行为,将使产品的完成期限和交付产生严重的问题。如果Form1使用并显示Form2,那么很清楚,Form1与Form2之间的关系一种是拥有性质:Form1拥有Form2。当打破类的边界时,程序的复杂性就会增加。上面的例子中,出现了两处这样的实例:Form1引用了Form2.ProgressBar1.Position,而Form2引用了Form1.Canceled.Form2.ProgressBar1.Position,这就是代码质量很差的原因所在,因为它打破了两个对象Form1和ProgressBar1的边界。如果改变状态的实现方式,则Form1和Form2都需要进行改变。改动是代价昂贵的。本章的其余部分将提出一些策略,在更高的层次上向您提供与Delphi的高级术语密切相关的知识,这将有助于您写出更为独立、修改更少的代码,并且减少了代码演化时的麻烦。
6.2 类定义实用指南
在学校里,主要的课程可能是有关语法与数据结构的。很少有大学和学院会讲授一个好的程序的组成要素,因为其答案过于主观。如果您确实对此有所了解,那可能是投入大量时间的和几经碰壁之后得到的。事实上,无须碰壁就可以学到一些好习惯。程序设计已经出现很多年了,许多程序员也编了很多年的程序,有一些好的惯例已经牢固的确立。遵循这些惯例,即可编出一些出色的程序。Delphi就是这些程序之一。Delphi的源代码是软件业的迈克尔•乔丹。
当然,向Delphi的源代码学习需要阅读很多代码。Delphi的源代码并非有关知识的惟一来源。可以找到大量相关的例子和指南,但并不存在惟一的知识来源,即所谓“最好的惯例手册”。一些思想仍然被认为令人讨厌、过于主观。但确实有一些惯例被无争议的采用,它们将有助于您编写出更好的代码。
6.2.1 类中有什么
有大约半打的最好的惯例,可有助于定义好的类。
1.由Booch的书(Booch,第137页)可知,类应该是简单的。这意味着基本上没有公有权限的属性。在Delphi的类中,大约会有半打左右的方法和特性。
2.要使公有权限的接口尽早稳定,并使其数量尽可能少,这样程序员可以从类的简单行为建立较为高级的行为。
3.某个类都有一个可识别的主要函数。
4.将数据封装在私有权限的接口中,通过公有特性和支持特性的方法进行访问。
5.通过子类化来扩展类的行为,减少对已存在代码的影响,最小化重新测试的可能性。
6.要明白,第一次就得到最好的抽象模型可能很困难。当理解了有关问题域的更多信息后,要准备好对抽象模型进行修改。
一个程序员曾表示,在单一的应用程序中出现几个类表明对程序设计缺乏基本的理解。很显然,这是错误的观点。从拉丁名言“分而治之”可知,反过来才是对的。通常,错误的抽象模型或缺乏抽象模型证明对面向对象程序设计缺乏基础知识。
注意:断言代码质量的陈述是基于所谓的专家意见。诸如“这就是我们在XYZ公司做事的方法,因此它是最好的”之类的陈述。这是孤立工作的程序员的通病。关于什么是最好的惯例有许多断言,但其中大部分都指出对大量经验证据进行仔细思考后才能作出精确而科学的发现。例如在Booch的书中提到,对质量的定性度量是基于耦合、内聚、充足性、完全性以及原子性等(Booch,1994)。而缺乏经验或支持信息的纯粹主观论断,是非常值得怀疑的。
应用“分而治之”的告诫,我们应把复杂的问题分解为一系列简单而基本的问题,并分别解决某个问题。本节开头的指南较为通用,可成为很好的起点。如果在类的公有接口部分定义了很多属性,代码会变得更复杂,反过来会影响您或其他程序员对代码的控制能力。
6.2.2 没有数据的类
许多规则都有例外。一般的,如果类定义中没有数据,按照通常的规则应把该类合并到其他类中,因为这种没有数据的类形式并未捕捉到问题域的完全的抽象。有一种类是该规则的例外,可称之为工具类。如果类的属性都是方法,则通常将其定义为类方法,这样无论有无对象实例均可使用方法。
提示:按照通常的规则,类应有数据和方法。数据记录状态而方法定义行为。只有在确实需要时,才可以背离该规则。
在Delphi中,TObject类是所有类的基类。它包含了几个类方法,通常只在其子类实例化时才间接地创建该类的实例。对于所有类都应有数据的通常规则,TObject就是个例外,定义该类是为了使所有的Delphi类都具有某些基本能力,有助于它们在Delphi程序中发挥作用。
6.2.3 命名惯例
Delphi并未强加任何令人难以忍受的命名惯例。Delphi开发者所使用的一些惯例是基于规则的,而不是基于需要记忆的前缀,您可以自由选择是否遵从该惯例。但如果您试过,可能会认为它们是易于使用的,而且简化了编程。
方法的命名惯例
方法是动词与名词的组合。动词描述了动作,并且在名词之前,而名词则描述动作所施行的对象。我们还知道名词与动词合起来,足以明确表达一个完整的口语或书面的句子。因此名词与动词联合的名字具有很高的可读性。
按照规则,要把方法的作用域限制到方法名中的动作和主题范围之内。如果在方法名中只有一个名词,那么您可能是在处理特性。按照惯例,特性方法中读方法的前缀为动词Get,而写方法的前缀为动词Set,其后紧接着特性名(高级特性编程的更多信息请参见第8章)。
事件处理程序的命名惯例
Delphi使用介词On作为事件处理程序的前缀。On描述了动作或运动,如OnClick或OnDragDrop。通过遵循一些惯例,几乎不需要花费时间即可找到方法、事件或特性的名字。术语的类型、动作和动作的主题可以帮助您为代码命名。
数据的命名惯例
Delphi中的数据属性称之为字段。按照惯例,私有字段的前缀为F。去掉F,即可得到表示实际字段的特性的很方便的名字。请记住过程类型,即事件和数据也可以是字段,因此前缀为F。将字段与特性匹配很简单,将字段名去掉F前缀即可。
前面提到过,基于规则的惯例可以使得代码在外观上一致而可靠,并可以减轻命名时想方设法的烦恼。遵守Delphi的命名惯例与否,是您个人的选择。推荐您使用一种可辨别的风格,并一直坚持使用。
消息处理程序的命名惯例
消息处理程序是一些特别的方法,用于响应Delphi所实现的消息分发模型。按照规则,消息处理程序与其所响应的消息名字大致相同。许多Windows消息的前缀为WM_,而Delphi对消息方法名的前两个字符使用了WM。与特定的控件相关的消息的前缀也具有特别的前缀,例如前缀为CB_的组合框。在messages.pas中可以找到这些消息的名字,它们被定义为常数。
6.2.4 存取限定符的使用
Delphi帮助文档中称存取限定符定义了成员的可见性。这有点用词不当,因为存取限定符并非限制代码的可见性,而是限制代码的可访问性。存取限定符将代码划分为四种不同的可访问层次和大体上三个可访问区域。公有和公开区域指明了类的用户可以访问而且应该关心的那部分代码。保护权限指明了扩展一个类时,除了公有权限的代码之外,还需要注意的代码,最后,私有权限表示只有作者自己才会看到的代码。
仔细而适中地将代码分布在不同的访问区域中,对于预期的用户可提高类所发挥的效用。对于公有访问权限来说,越少越好。要维持对公有属性的紧密控制,以确保您的类可以通过代码质量的原子性度量。
6.2.5 默认的公开或公有权限
默认情况下,所有的属性都具有公有权限,这与C++并不相同,在C++中属性在默认情况下是私有的。如果类编译时添加了运行时类型信息(在$M+状态下编译),如TPersistent类及其所有的子类,则所有属性默认情况下具有公开权限。当在工程中创建一个新的窗体时,所有的组件都出现在窗体定义的上部。
警告:最好由Delphi来管理位于窗体类上部和.DFM文件中的那些属性。如果您希望自己来做,也可以手工进行管理,但需要非常小心。
Delphi的行为是一致的,它并未把在窗体上绘制的控件与其他类区分开来,即使对于可以从.DFM文件中读写窗体定义并使用脚本消息来自动实例化组件的流类也是如此。
考虑本章开头例子中的进度条窗体。该窗体中有一个TProgressBar和一个TButton对象。Delphi对这些组件流化了足够的消息,以便在创建Form2的实例时自动创建这些组件。
object Form2: TForm2
Left = 441
Top = 222
Width = 263
Height = 163
Caption = 'Progress'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -13
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 120
TextHeight = 16
object Button1: TButton
Left = 88
Top = 88
Width = 75
Height = 25
Caption = 'Cancel'
TabOrder = 0
OnClick = Button1Click
end
object ProgressBar1: TProgressBar
Left = 24
Top = 24
Width = 217
Height = 25
Min = 0
Max = 100
TabOrder = 1
end
end
当读到object语句时(上面列出的.DFM文件中有三个),Delphi将从object语句一行判断对象的类,创建该对象的实例,并读入其余的属性,这就是DefineProperties方法的任务。由于组件有构造函数和析构函数,因此您可以选择在设计时将其添加到应用程序中或在运行时动态地创建它们。
在任何TWinControl控件上,都可以动态地创建并初始化控件。TWinControl控件可以拥有控件。要在窗体上动态地创建TButton控件,可使用下面的代码,其中Self参数代表该窗体。
with TButton.Create(Self) do
begin
Parent := Self;
Name := 'ButtonProcess';
OnClick := Button1Click;
SetBounds( 10, 10, Width, Height );
Caption := 'Process';
end;
注意:TWinControl类维护了一个TControl的列表,由TWinControl所拥有的子控件组成。因此,虽然并未显式保存对控件列表中动态创建对象的引用,通过搜索控件列表来查找名为ButtonProcess的TButton控件即可得到该引用。
上面的代码模拟了Delphi在读入资源文件并创建窗体时的行为。上面列出的代码与设计时添加按钮的区别在于,上面的代码添加了一个按钮到窗体,但并未在DFM文件中维护对该按钮的引用,而设计时也无法操纵动态生成的按钮。
6.2.6 公开接口
当使用published存取限定符时,表示该属性将出现在Object Inspector中。除此之外它与公有访问权限是相同的。把方法放在公开部分是没有意义的,这与将其放在公有部分效果相同。
当定义既可在设计时又可在运行时修改的组件特性时,可将其访问权限定义为公开权限。如果您不需要在设计时修改特性,则无须将其定义为公开权限。事件特性也可以是公开的,组件公开的事件特性会在Object Inspector的Events属性页中列出。Delphi 6中新增了公开对象特性的概念。在Delphi的早期版本中,如果创建包含其他组件的组件,只能使用属性提升的手段来访问内部对象的特性。例如,如果要在设计时修改对话框控件上的图像,则只能向该组件添加一个图像特性,然后利用图像特性的方法来访问实际的TImage控件的Picture特性。现在您可以把TImage控件作为公开特性,并直接访问其picture特性。这是对公开访问区域的一个很好的改进(对象特性的更多知识及相应实例,请参见第10章)。
6.2.7 公有接口
公有接口是类怎样使用的决定性因素。如果无法利用公有接口达到目的,那么也就没有使用该类的必要。按照Booch对代码质量的测量标准,公有接口中的属性应该足够、完全、并具有原子性。足够指的是该类足以解决问题。即公有接口应是自给自足的。例如,一个文件流类中有写功能,要达到足够并完全,需要在该类中添加兼容的读功能。原子性指的是类的功能应是基本的,如果一个类方法建立在另一个公有方法基础之上,则第二个方法不是原子性的,应从类中去掉。
应使公有接口保持简明,并尽早地定义类的公有接口并使之稳定。如果在一个类的公有接口的基础上建立了其他类,则修改该类也会导致对依赖于它的类的修改。类中非公有的属性都应该是保护或私有的。
6.2.8 保护接口
在面向对象中,保护接口供扩展该类的程序员使用。扩展意味着从已有的类派生子类,扩展其行为和状态。把属性放在保护接口中时,对类进行扩展的程序员就可以修改这些属性。像古老的格言所说“可以做的事都会有人做”,如果您不希望在子类中改变方法或直接操纵属性,那就使用私有接口吧。
6.2.9 私有接口
私有接口与公有接口是并列的。从外部看来,只有构成类的基本行为和状态的属性才可以放在公有接口中,而私有接口好比是清洁工具柜。除非有意地使扩展该类的程序员可以访问它们,否则一切用于实现类的公有行为的属性都会放在私有接口中。
注意:在私有与保护权限之间进行选择是困难的,因为这几乎与预先判断其他开发者然后使用您的代码一样,都是不可能的。如果您的用户是一些特定的开发者,例如组件的作者,可以考虑违反规则,通过将更多的数据和方法设置为保护权限,从而使代码的扩展性更好。这也会使扩展您的组件的开发者有更多的灵活性。
请把类的所有实现细节放在私有访问区域中。这样做可以清楚地向用户表明,他们无须关心这些项,因此可以使得您的类更易于使用。在定义类时,可以最后解决私有实现细节,因为在类改动时,它们对用户的影响最小。
6.3 创建自定义过程类型
Delphi和C++支持函数指针。而Visual Basic和Java则不支持。函数指针是个强有力的概念,它在过程一级提供了一个额外的动态层次。我们提到过,可以把回调过程传递给Windows,这样就能让操作系统来调用回调过程。C++中的函数指针例子如下:
#include "stdafx.h"
#include <iostream.h>
void (*fp)();
void Function()
{
cout << "Hello World!" << endl;
}
int main(int argc, char* argv[])
{
fp = Function;
fp();
return 0;
}
void (*fp)();一行定义了一个名为fp的函数指针变量。通过把过程Function的名字赋值给fp,就可以通过fp调用Function。函数指针是很高级的概念,它向程序员提供了额外的实现选项(例子请参见6.3.2节“回调过程”)。
Delphi对函数指针的支持不那么难懂。函数指针的概念在Delphi中称之为过程类型。过程类型与其他类型的声明在外观上是一致的,无须像C++中那样进行间接引用。使用过程类型,可以编写支持Windows回调过程、动态过程参数和事件处理程序的代码。它们的共同特征就是可以编写出动态和富于表达力的代码,而且难于在任何其他的语言中复制。
6.3.1 定义过程类型
当定义过程类型时,可以将该类型作为别名引入,也可以将其作为新的类型引入,对于后者,编译器将进行强类型检查;该类型可以指定为方法或非方法的过程类型。
过程类型
简单的过程类型定义在类型声明部分。与前面的C++代码一致的过程类型可如下声明。
type TProcedure = Procedure;
上面的声明意味着,任何没有参数的过程都可以赋值给TProcedure类型的变量。如果要声明有参数的过程类型,可以像声明过程那样包括参数列表,只需去掉过程名既可。
type
TIntegerProcedure = Procedure( I : Integer);
TObjectProcedure = Procedure( Sender : TObject );
TManyParams = Procedure( S : String; I : Integer );
过程类型定义的参数列表可以与过程声明一样多变。可以包括数组参数、变量列表、常数和输出参数。决定性因素是您的需求。所需引用的过程的类型将决定您所需要的过程类型。
函数类型
在对应于过程和函数的过程类型之间,不存在实际的区别。定义语句也是相同的,除了用关键字Function代替Procedure外,在定义语句的末尾添加返回类型即可。
type
TFunction = Function : Integer;
TStringFunction = Function( S: String ) : Boolean;
TVarFunction = Function( var D : Double ): String;
如同过程类型一样,函数类型的参数列表是由该类型所引用的函数的参数所决定的。返回类型也必须匹配。
用于方法的过程类型
过程类型也能引用方法,可以是类的成员函数或成员过程。当定义方法指针,即用于方法的过程类型时,需要表明该过程类型指向类的成员。在类型定义的结尾用of Object标记即可。
type
TFunctionMethod = Function : Integer of Object;
TProcedureMethod = Procedure( var S : String ) of Object;
除了定义结尾的of Object限定符以外,该类型定义与非方法的类型定义是相同的。
在Delphi中遇到的过程类型通常是事件特性的类型。最常用的方法指针类型是在classes.pas中定义的TNotifyEvent。
type TNotifyEvent = procedure (Sender: TObject) of object;
该类型用于许多事件特性,包括您会经常用到的OnClick事件。
6.3.2 回调过程
回调过程是这样一种过程,其地址被赋值给变量或作为参数传递,用于在指定的时间进行调用。Windows和Delphi都支持回调。回调过程的最常见的用途是在对象和响应事件的动作之间提供一个松散的耦合点。例如,鼠标单击行为就是这样引入到TControl控件中的。
procedure WMLButtonUp(var Message: TWMLButtonUp); message WM_LBUTTONUP;
procedure TControl.WMLButtonUp(var Message: TWMLButtonUp);
begin
inherited;
if csCaptureMouse in ControlStyle then MouseCapture := False;
if csClicked in ControlState then
begin
Exclude(FControlState, csClicked);
if PtInRect(ClientRect, SmallPointToPoint(Message.Pos)) then
Click;
end;
DoMouseUp(Message, mbLeft);
end;
procedure TControl.Click;
begin
{ Call OnClick if assigned and not equal to associated action's
OnExecute. If associated action's OnExecute assigned then
call it, otherwise, call OnClick. }
if Assigned(FOnClick) and (Action <> nil)
and (@FOnClick <> @ Action.OnExecute)
then FOnClick(Self)
else if not (csDesigning in ComponentState)
and (ActionLink <> nil) then
ActionLink.Execute
else if Assigned(FOnClick) then
FOnClick(Self);
end;
TControl类建立了自己的WndProc过程,作为对Windows消息的回调过程。当WndProc从Windows收到WM_LBUTTONUP消息时,它通过在TObject中引入的Dispatch方法将消息分发到控件。Dispatch调用上面列出的WMLButtonUp消息方法。该消息方法确保了控件可以像设计的那样接收鼠标单击。
if( csClicked in ControlState ) then
如果控件接收到单击,则调用动态过程Click。动态方法表会调用正确的Click方法。处理单击的方法还会检查FOnClick事件处理程序是否指向有效的过程。
if( Assigned(FOnClick)) then FOnClick(Self);
如果Assigned(FOnClick)结果为True,就会调用该过程。
一个通情达理的人可能问的第一个问题会是,为什么需要绕这么多圈子才能知道发生了鼠标单击?只有从某一角度看来,答案才是显然的。您确实不必这样做,因为所有的这些信息在日常的开发活动中是不可见的。Windows程序设计在十年前被认为很困难。而现在所有的Delphi程序员都只需双击Object Inspector中的OnClick事件特性,然后填写方法体中的空白即可。
Windows消息机制的复杂性在封装后对于日常的程序员是不可见的,复杂性的隐藏使事情变得很容易。当编写响应事件的代码时,无须关心Windows如何工作。反过来,如果编写代码来改进与Windows的交互,您必须确切地知道Windows是如何工作的。回调是Windows中很重要的一部分。在Delphi尚未命名为Delphi之前,它就已经支持过程类型了。大约七到八年前,Object Pascal(Delphi)还被称为Turbo Pascal;因此它支持动态过程类型至少有十年之久了。
6.4 过程类型中的默认参数值
在过程类型中,可以定义参数的默认值。事实上,过程类型的定义并未改变默认参数值的语法,仍然与所有的过程定义都相同。如果察看一下帮助中的Object Pascal Grammar,很显然过程类型定义的标准规则中包括了与过程定义相同的子规则FunctionHeading和ProcedureHeading(依赖于过程类型的种类)。要包括参数的默认值,只需在参数类型之后添加常数表达式即可。
type TDefaultParams = Procedure( const S : String = 'Default' ) of object;
注意:如果为某个过程提供了一个默认参数,并将该过程赋值给定义了默认参数值的过程类型变量,则参数将使用定义在过程类型中的默认值而不是定义在实际过程中的默认值。
警告:可以将有默认参数的过程赋值给没有定义默认值的过程类型变量,但对该变量将无法使用默认参数。否则编译将出错。
上面定义了一个以TDefaultParams为别名的方法指针类型,参数为常量字符串类型,默认值为‘Default’。可以声明TDefaultParams类型的变量,并将方法赋值给这些变量。对应的方法的原型必须与TDefaultParams相同,但参数不需要默认值。
6.5 传递过程类型的参数
使用过程类型的参数,需要定义相应的过程类型。编译器不能分析内嵌的过程类型定义,例如Procedure P(P : Procedure (I : Integer));。必须先定义过程类型,然后将该别名或新类型作为参数的类型。
type TProcedure = Procedure( I : Integer);
// ...
Procedure P( Proc : TProcedure );
上面的例子对Procedure(I : Integer);使用了别名TProcedure。如果要定义新类型,在等号右侧使用关键字type即可,但实际传递的参数必须与该类型精确匹配。假定有过程Procedure Foo( I : Integer );,就可以用参数Foo来调用过程P,如下所示。
P( Foo );
但如果要把TProcedure作为新类型定义,则需要按两步进行。
type TProcedure = Procedure( I : Integer );
type TNewProcedure = type TProcedure;
提示:如果试图在一个语句中定义过程类型和新类型,编译器将给出错误提示“identifier expected but ’PROCEDURE’ found”(在beta版的编译器中可能出错,但Delphi 6发布时该问题将得到解决)。
然后需要把Foo赋值给TProcedure类型的变量,并将该变量传递给P。第5章中提到过,如果类型为新类型(即在类型定义的右侧使用type关键字),编译器将对参数进行严格的类型检查。
type TDummyProcedure = Procedure(I : Integer);
type TProcedure = type TDummyProcedure;
Procedure P( Proc : TProcedure );
// ...
Procedure Foo( I : Integer );
begin
// some code
end;
// ...
var
AProc : TProcedure;
begin
AProc := Foo;
P( AProc );
end;
列出的代码中结尾处的块语句示范了如何将过程赋值给类型为TProcedure类型的变量。当过程类型定义为新类型时,这一点是必须的。
6.6 过程类型常量
当定义类型后,就可以像其他类型一样使用。可以声明该类型的变量、常数或创建新的子类型。
const MyNowProc : Function : TDateTime = SysUtils.Now;
上面列出的代码声明了过程类型常数MyNowProc,它指向返回TDateTime的函数,其值为SysUtils.pas中定义的Now函数。在可以使用其他类型常量的地方,也可以使用过程类型常量,如:创建静态本地变量的等价物,或定义可修改的过程常数等。
6.7 事件处理程序
事件处理程序是设计用来响应Windows消息的过程。在Delphi中,事件处理程序大多数是在双击事件特性时创建的。事件特性是过程类型的特性,它的值是Delphi所创建的事件处理程序。在分配代码来响应Windows和Delphi内部所产生的消息的途径中,这种定义事件处理程序的方式是最直接的。
事件并不限于在Object Inspector中所列出的那些,也不必以指定的方式来使用事件。动态实例化组件或创建自定义组件,是另一个需要定义自己的事件特性并编写处理程序的常见场合。在编写事件处理程序的代码时,有几个重要的准则需要记住。
1.一个事件处理程序可能会分配给多于一个的事件特性。在控件上拖动鼠标指针来选定控件(或按下Shift键并分别单击每个控件),双击事件特性编辑域来创建事件处理程序(如图6.2所示,可以看到当有多于一个的组件被选定时,Object Inspector的反应)。
图6.2 当多个组件被选定时,在Object Inspector 顶部的对象选
择器中会显示选定对象的数目(图中有两个对象被选定)
2.避免在事件处理程序中直接编写代码,而是调用一个方法来完成实际的工作。这样在其他环境中需要该行为时就不必像OnClick(Nil)一样直接调用事件处理程序,因而使得代码更加可读。
3.当多于一个组件共享同一事件特性时,可使用Sender参数来判断哪个组件实际触发了事件处理程序。
遵循这些步骤,可以提高代码的可读性和可扩展性。Button1Click中包含了较多的代码,这就不如调用一个命名得很好的过程那样有意义。
6.7.1 定义事件处理程序
事件处理程序是一个方法。为把事件处理程序分配给特定的事件特性,该方法与所处理的事件的参数数目、顺序和类型必须是相同的。例如,OnClick事件类型为TNotifyEvent。在Delphi帮助中查找TNotifyEvent,它定义为一个过程,有一个名为Sender的TObject类型的参数。
提示:对于预定义的事件,查看事件特性的过程类型定义并将其复制即可得到其参数列表。
type TNotifyEvent = Procedure(Sender : TObject ) of Object;
of Object限定符意味着该过程是类的成员。假定有类TForm1,它的OnClick事件的可能的事件处理程序如下。
TForm1 = class(TForm)
protected
Procedure OnClick( Sender : TObject );
public
// …
end;
把OnClick过程赋值给任何定义为TNotifyEvent的事件特性都可以编译通过,而且在技术上这也是正确的。
Button1.OnClick := OnClick;
假定Button1是TForm的成员,则前面的代码是正确的。
在技术上,把OnClick赋值给任何TNotifyEvent类型的事件特性都是正确的,但如果该事件并非Click事件,将导致语义上的错误。惯例是使用On作为事件处理程序的前缀,并用表示动作的词来描述该事件。
6.7.2 调用事件方法
调用事件方法的惟一禁令是出于风格方面的考虑。假定一些代码要使用事件处理程序OnClick( Sender : TObject ),可以调用OnClick(Nil)或OnClick(Self)来触发该行为。在风格上,定义一个提供OnClick行为的方法则较为可取。考虑对于菜单的exit命令的OnClick方法,其中exit将结束应用程序。
Procedure TFormMain.Exit1Click( Sender : TObject );
begin
// cleanup
Application.Terminate;
end;
上面的代码执行了应用程序的清除工作并结束程序。在风格上,下面列出的代码更为可取。
Procedure TFormMain.TerminateApplication;
begin
// cleanup
Application.Terminate;
end;
Procedure TFormMain.Exit1Click( Sender : TObject );
begin
TerminateApplication;
end;
注意:当使用UML建模,特别是使用序列图时,如果事件处理程序出现在序列中,则显然事件处理程序中包含了该对象的可定义的行为;例如,当演示应用程序结束的序列到达需要结束行为的点时,Exit1Click就会出现。试着运转序列,如果存在某种行为但没有定义方法时,通常表示该类是不完全或不足够的。这只是个例子,利用正向和逆向工程以确保定义的类是完全并足够的。
现在类的行为是自明的,在其他的上下文中无须通过事件处理程序即可调用TerminateApplication行为。
6.7.3 触发事件
当需要产生事件时,可以调用分配给事件特性的过程。在6.1节“赢得对意大利细面条的战争”中,可利用事件处理程序简化主窗体与显示进度条的窗体之间的关系。
unit UFormMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
FCanceled : Boolean;
Procedure OnCancel( Sender : TObject );
Procedure Cancel;
Procedure Process;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses UFormProgress;
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
begin
Process;
end;
procedure TForm1.Cancel;
begin
FCanceled := True;
end;
procedure TForm1.OnCancel(Sender: TObject);
begin
Cancel;
end;
procedure TForm1.Process;
type
TRange = 1..10;
Function PercentComplete( I : TRange ) : Double;
begin
result := I / High(TRange);
end;
var
I : Integer;
begin
FormProgress := TFormProgress.Create(Self);
FormProgress.Show;
FormProgress.OnCancel := OnCancel;
try
for I := Low(TRange) to High(TRange) do
begin
if( FCanceled ) then break;
FormProgress.UpdateProgress( PercentComplete(I) );
Application.ProcessMessages;
Sleep( 300 );
end;
finally
FormProgress.Free;
end;
end;
end.
unit UFormProgress;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs,
ComCtrls, StdCtrls;
type
TFormProgress = class(TForm)
Button1: TButton;
ProgressBar1: TProgressBar;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
FOnCancel : TNotifyEvent;
function GetMax: Integer;
public
{ Public declarations }
procedure UpdateProgress( PercentComplete : Double );
property OnCancel : TNotifyEvent read FOnCancel write
FOnCancel;
property Max : Integer read GetMax;
end;
var
FormProgress: TFormProgress;
implementation
{$R *.DFM}
{ TFormProgress }
function TFormProgress.GetMax: Integer;
begin
result := ProgressBar1.Max;
end;
procedure TFormProgress.UpdateProgress(PercentComplete: Double);
begin
ProgressBar1.Position := Trunc(PercentComplete * ProgressBar1.Max);
end;
procedure TFormProgress.Button1Click(Sender: TObject);
begin
if( Assigned(FOnCancel)) then
FOnCancel( Self );
end;
end.
如前所述,FormMain类的Process方法创建了显示进度条的窗体,但本例把事件处理程序OnCancel链接到FormProgress的一个事件特性。在FormProgress的代码中可以看出,它在设计时完全不知道FormMain的存在。这意味着FormProgress是完全独立于FormMain的。当单击FormProgress窗体中的按钮时,它将调用与FormProgress.OnCancel事件特性相关联的事件处理程序。FormMain也并不在意进度显示是如何实现的,它调用FormProgress类的方法UpdateProgress,并把它所知道的进度情况作为参数传递,而无须考虑FormProgress如何实现该行为。通过该方法,FormMain无须知道使用进度条来表示完成情况,也不用在意进度条的最大值是多少。
Assigned过程定义在system.pas单元中,它用来检查是否存在OnCancel事件处理程序;如果有,将调用该处理程序。从而只有FormMain知道对FormProgress的引用,而所有的交互都维持在两个窗体的接口一级。
这里所示范的代码风格还需要加工一下,因此要增加一些代码。收获看来不错,因为各个方面都具有了更高的可重用度,代码是自文档化的,简化的相互关系意味着FormProgress可以在其他的环境中重用。FormProgress也可用其他的进度指示器来实现,而对于FormMain没有任何负面影响。本章中的术语促进了这种代码风格,使用它可以得到更为健壮的应用程序。
6.7.4 定义事件特性
事件特性就是过程类型的特性。当看到与下面的定义相似的特性:
property OnCancel : TNotifyEvent read FOnCancel write FOnCancel;
它等价于:
property OnCancel : Procedure( Sender : TObject ) of Object read
FOnCancel
write FOnCancel;
但后者的语法是不正确的。而且读起来也令人迷惑。聚合应该以逐渐而自然的步骤进行,将复杂性层次化而后形成较简单的表现形式;这在大脑中是很容易的。
事件特性的类型由所响应的事件的类型决定。事件特性的命名可按照惯例进行:以On为前缀,后跟去掉T的事件类型或者处理程序所响应的消息类型。这样,响应事件MouseDown的事件特性命名为OnMouseDown就足够了(特性定义的更多信息请阅读第8章)。
6.7.5 事件处理程序将消息转发到应用程序
在海军陆战队的广告中,有一个铁匠通过轧制金属来制作剑,一遍又一遍地敲打边缘。其思想在于,通过对钢进行多次层叠来提高金属的强度。具有工业水准的代码同样需要千锤百炼。通过隐藏复杂性层次,应用程序可以更为健壮。事件与消息之间的关系就是这样的一个层次。事件处理程序是Windows和Delphi的消息与应用代码之间的桥梁。由于层次的存在,消息驱动的Windows操作系统的复杂性被隐藏起来,但是却可以访问。这就是Delphi看起来与Visual Basic一样易于编程但却更为强大的原因之一。Visual Basic中由于不存在独立的层次,因此VB就没有Delphi那么健壮。
6.8 消 息 方 法
相比事件处理程序,消息处理程序是更靠近Delphi和Windows的一个层次。在消息一级捕获事件,可以获得更多的控制权限和选择自由。许多控件都有相当数目的消息处理程序来捕获许多通常类型的消息以及发送到方法以调用指定的事件处理程序的消息。在日常的编程中就可以发现,存在着特定消息的事件处理程序。
如果要为某个消息定义新的行为,只需对该消息所产生的事件编写一个处理程序即可。例如,要在窗口接收绘制消息时加入新的行为,只需向该窗口的OnPaint事件处理程序添加代码即可。也可在更低点捕获该消息,即接收消息的时候。这提高了更多的控制权限,但也增加了责任。
重载消息与重载虚函数相似。不同之处在于其声明。虚函数在子类中使用同样的方法名重载,并在声明的结尾处添加override指令。消息处理程序不需要override指令,它使用message指令(在6.8.2节“定义消息处理程序”中描述)。不必对消息处理程序使用同样的过程名,尽管这样做可以使您的代码更加容易阅读。
6.8.1 查找预定义消息常数
消息常数定义在Windows API帮助以及Delphi单元messages.pas和windows.pas中。API中的Windows消息在索引中按字母顺序列出,以WM_为前缀。大多数Windows消息定义为messages.pas中的命名常数,无须再定义消息常量。包括messages.pas单元,即可在代码中使用命名消息常数。
在Windows.pas中定义了两个消息过程(由user32.dll引入)SendMessage和PostMessage,可进行原始的消息发送。也可以使用每个对象都包含的Dispatch方法直接向对象传递消息。下面列出的代码示范了如何在Delphi应用程序的菜单中实现Windows的Undo、Cut、Copy和Paste行为。
Function TForm1.ActiveHandle : Integer;
begin
result := 0;
if( Assigned(ActiveControl)) then
try
result := ActiveControl.Handle
except
end;
end;
Procedure TForm1.SetMenuStates( const Enabled : Boolean );
begin
CanUndo;
Cut1.Enabled := Enabled;
Copy1.Enabled := Enabled;
Paste1.Enabled := Enabled;
end;
procedure TForm1.Edit1Click(Sender: TObject);
begin
SetMenuStates( ActiveHandle <> 0 );
end;
procedure TForm1.Undo1Click(Sender: TObject);
begin
SendMessage( ActiveHandle, WM_UNDO, 0, 0 );
CanUndo;
end;
procedure TForm1.Cut1Click(Sender: TObject);
begin
SendMessage( ActiveHandle, WM_CUT, 0, 0 );
CanUndo;
end;
procedure TForm1.Copy1Click(Sender: TObject);
begin
SendMessage( ActiveHandle, WM_COPY, 0, 0 );
CanUndo;
end;
procedure TForm1.Paste1Click(Sender: TObject);
begin
SendMessage( ActiveHandle, WM_PASTE, 0, 0 );
CanUndo;
end;
procedure TForm1.CanUndo;
begin
Undo1.Enabled := Boolean(SendMessage( ActiveHandle, EM_CANUNDO, 0, 0 ));
end;
按下列步骤可实现该例程:
1.创建一个新工程。
2.从组件面板的Standard属性页,向主窗体添加TMainMenu控件。
3.双击窗体上的菜单组件,显示TMainMenu的特性编辑器(见图6.3)。
图6.3 TMainMenu组件在设计时的特性编辑器
4.对每个菜单项(Undo、Cut、Copy、Paste)双击OnClick事件特性,向对应的事件处理程序添加代码,如上面列出的代码所示。
5.向窗体类添加私有方法声明:Function ActiveHandle : Integer ;、Procedure SetMenuStates( const Enabled : Boolean );以及Procedure CanUndo;。
6.向每个方法和事件处理程序添加代码。
更好的办法是把消息的实际值赋值给对应菜单项的Tag特性,这样可以对菜单项的单击事件实现单一的处理程序,把菜单项的Tag特性传递给SendMessage作为Msg参数,如下所示:SendMessage( ActiveHandle, TMenuItem(Sender).Tag, 0, 0);。虽然该代码较为模糊,而且并未达到自文档化的理想目标,但如果加入一段注释来阐明代码的意义,那么同样可以有效使用。由于没有使用常数而是在Tag特性中编码了常数的字面值,该代码可能不像直接定义四个单独的事件处理程序那样具有良好的可移植性。
现在已经编写出了一个较为实际的例子,示范了如何在应用程序中发送消息。我们接着看一下怎样定义消息处理程序。
6.8.2 定义消息处理程序
与所有其他术语相同,消息处理程序也有基本的语法。该语法由下列规则定义:
· 在类的私有访问区域定义消息处理程序。
· 将消息处理程序定义为过程。
· 消息处理程序总是只有一个记录参数,类型为TMessage或具有相似定义的类型(关于自定义消息记录的限制,请参考帮助文档)。
· 消息处理程序无须override指令。
· 消息处理程序无须与父类中对应的处理程序同名,但这样做是个好主意,可以使代码更为清晰。
· 调用inherited进行父类的消息处理,这与重载方法相同。如果不存在父类的处理程序,会调用DefaultHandler方法。
· 把消息处理程序的代码写在另一个方法中,在处理程序中调用该方法。
· 通常应避免直接调用消息处理程序,应该调用SendMessage、SendNotifyMessage、PostMessage或Dispatch方法,并将消息作为参数传递。
TMessage是Windows消息处理程序通用的记录类型。TMessage定义在messages.pas中,它是紧缩记录类型。
TMessage = packed record
Msg: Cardinal;
case Integer of
0: (
WParam: Longint;
LParam: Longint;
Result: Longint);
1: (
WParamLo: Word;
WParamHi: Word;
LParamLo: Word;
LParamHi: Word;
ResultLo: Word;
ResultHi: Word);
end;
记录中的case语句(如上所示)与C和C++中的联合较为相似,但不那么直观。Case语句中的每一项都代表着该字段在记录中可能的表示方式。可以访问记录值的任意字段;一种表示方式是WParam、LParam和results都是长整数,另一种表示方式是同样的,但值划分为高位和低位的字。Windows内部存储信息使用反向字节存储顺序,因此值的低位部分存储在较低的地址然后是值的高位部分。这就是WParamLo列在WParamHi之前的原因。在WParam中,WParamLo代表低位字,而WParamHi代表高位字(常整数是32位的,而字是16位的)。
您可以使用TMessage,也可以定义与TMessage变量大小相同的紧缩记录类型,定制其成员以满足您的需求。Messages.pas中定义了许多消息记录,您可以直接使用,也可作为定义自己的消息类型的参考。遵循定义消息处理程序的准则,下面列出的代码示范了编辑控件的消息处理程序。
type
TMessageEvent = Procedure( const Strings : TStrings ) of
Object;
TMyEdit = class(TEdit)
private
FStrings : TStrings;
FOnSetSel : TMessageEvent;
procedure EMSetSel( var Msg : TMessage ); message EM_SETSEL;
procedure MessageEvent( Msg : TMessage );
Procedure SetSel( Msg : TMessage );
public
constructor Create( AOwner : TComponent ); override;
destructor Destroy; override;
property OnSetSel : TMessageEvent read FOnSetSel write
FOnSetSel;
end;
implementation
constructor TMyEdit.Create(AOwner: TComponent);
begin
inherited;
FStrings := TStringList.Create;
end;
destructor TMyEdit.Destroy;
begin
FStrings.Free;
inherited;
end;
procedure TMyEdit.EMSetSel(var Msg: TMessage);
begin
SetSel( Msg );
inherited;
end;
procedure TMyEdit.MessageEvent(Msg: TMessage);
begin
with FStrings do
begin
Clear;
Add( 'Msg=' + IntToStr( Msg.Msg ));
Add( 'WParam=' + IntToStr( Msg.WParam ));
Add( 'LParam=' + IntToStr( Msg.LParam ));
Add( 'Result=' + IntToStr( Msg.Result ));
Add( 'WParamLo=' + IntToStr( Msg.WParamLo ));
Add( 'WParamHi=' + IntToStr( Msg.WParamHi ));
Add( 'LParamHi=' + IntToStr( Msg.LParamLo ));
Add( 'LParamLo=' + IntToStr( Msg.LParamHi ));
Add( 'ResultHi=' + IntToStr( Msg.ResultLo ));
Add( 'ResultLo=' + IntToStr( Msg.ResultHi ));
end;
if( Assigned( FOnSetSel)) then
FOnSetSel( FStrings );
end;
procedure TMyEdit.SetSel(Msg: TMessage);
begin
MessageEvent( Msg );
end;
TMyEdit类示范了如何重载EM_SETSEL消息的处理程序,当选定文本时会收到该编辑消息。TMessageEvent定义为过程类型,接受一个TStrings对象作为参数,其中包含了EM_SETSEL消息经过格式化的值。格式化过程在方法MessageEvent中发生,如果已经通过OnSetSel特性向FOnSetSel字段分配了事件处理程序,还将调用该处理程序。请注意,EmSetSel消息处理程序委托一个过程来完成工作并调用继承的消息处理程序。
消息处理程序的用途很广泛。WinSight32应用程序使用它们在调试期间跟踪Windows消息。也可编写消息处理程序,基于一些条件性代码来冻结一个消息,而如果条件测试成功,则调用继承的消息处理程序。还可以使用消息处理程序来实现通用的事件处理。尽管并非所有的对象对所有的消息都会响应,但可以假定已知某个类的对象会响应消息。我们不在分发消息的对象中试图判断当前的接收者,而是对当前接收者或某个列表中的接收者维护一个通用的TObject引用,然后发送消息。如果接收者对象需要该消息,就会有已定义的消息处理程序。如果它不需要该消息,在没有消息处理程序的情况下将忽略该消息。当对象和消息的数目都很多时,请避免把很多的事件处理程序分配给同样多的事件特性,然后再试图找出哪个对象与哪个消息相关联,只需把消息分发给所有可能的接收者,让接收者自行决定即可。这种广播过程与电视和无线电广播的工作方式非常相似。
6.8.3 理解Delphi的消息发送体系
所有的Windows编程语言都必须在某种层次上支持Windows消息发送机制。对Windows消息机制的支持仔细而精巧,它隐藏了许多Windows编程的复杂性,同时在需要的情况下仍然可以访问低层消息机制(参见图6.4中Delphi消息机制的图示)。
图6.4 对Delphi所实现的消息机制的可视化描述
您可以在合适之处插入自己的代码。大部分通常的程序只需编写事件处理程序。但如果在Visual Basic中试图重载标签的绘制行为,您就可以知道无法访问Windows的消息机制会带来多少局限。如果您必须使用Visual C++中的预编译器来编写消息拆析器,您也会知道使用一种实现得半生不熟的事件处理机制来实现绘制究竟是什么样子。
因为Delphi中所有的类都是TObject类的子类,因此Delphi可以把消息传播到通常不接收消息的控件。通过对特定的消息调用Dispatch方法,Delphi中的VCL控件能够响应范围广泛的消息,通常这些消息是只发送给具有Windows句柄的控件的。
6.9 小 结
Windows是事件驱动、基于消息的操作系统。Windows编程如果缺乏消息发送和事件处理程序的支持,就像在陆地上驾驶飞机只能用脚来掌舵一样,既不自然也不舒服。Delphi具有仔细而精巧的体系结构,从而完善地支持了比其他Windows编程语言更为自然的开发环境。
您现在已经理解了基于消息的机制的一些深入的细节。这种知识使得您可以定义松散耦合的类体系结构,编写更富于表达力和更健壮的代码,创建更高级的定制控件。本章中示范的技术对于讲述如何建立组件的章节将会特别有用。