第十九章 Delphi自定义部件开发(二)
19.2.2 Delphi部件编程
19.2.2.1 创建属性
属性(Property)是部件中最特殊的部分,主要因为部件用户在设计时可以看见和操作它们,并且在交互过程中能立即得到返回结果。属性也很重要,因为如果将它们设计好后,将使用户更容易地使用,自己维护起来也很容易。
为了使你在部件中更好地使用属性,本部分将介绍下列内容:
为什么要创建属性
属性的种类
公布(publishing)继承的属性
定义部件属性
编写属性编辑器
1. 为什么要创建属性
属性提供非常重要的好处,最明显的好处是属性在设计时能出现在Object Inspector窗口中,这将简化编程工作,因为你只需读用户所赋的值,而不要处理构造对象的参数。
从部件使用者的观点看,属性象变量。用户可以给属性赋值或读值,就好象属性是对象的域。
从部件编写者的观点看属性比对象的域有更强的功能;
⑴ 用户可以在设计时设置属性
这是非常重要的,因为不象方法,只能在运行时访问。属性使用户在运行程序之前就能定制部件,通常你的部件不应包含很多的方法,它们的功能可以通过属性来实现。
⑵ 属性能隐藏详细的实现细节
⑶ 属性能引起简单地赋值之外的响应,如触发事件
⑷ 用于属性的实现方法可以是虚拟方法,这样看似简单的属性在不同的部件中,将实现不同的功能。
2. 属性的类型
属性可以是函数能返回的任何类型,因为属性的实现可以使用函数。所有的Pascal类型,兼容性规则都适用属性。为属性选择类型的最重要的方面是不同的类型出现在Object Inspector窗口中的方式不同。Object Inspector将按不同的类型决定其出现的方式。
你也能在注册部件时描述不同的属性编辑器。
下表列出属性出现在Object Inspector窗口中的方式
表19.3 属性出现在Object Inspector窗口中的方式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
属性类型 处 理 方 式
───────────────────────────────────────
简单类型 Numeric、Character和 String属性出现在Object Inspector中,用户可
以直接编辑
枚举类型 枚举类型的属性显示值的方式定义在代码中。选择时将出现下拉
式列表框,显示所有的可能取值。
集合类型 集合类型出现在Object Inspector窗口中时正如一个集合,展开后,用
户通过将集合元素设为True或False来选择。
对象类型 作为对象的属性本身有属性编辑器,如果对象有自己的published属
性,用户在Object Inspector中通过展开对象属性列,可以独立编辑它们,
对象类型的属性必须从TPersistent继承。
数组类型 数组属性必须有它们自己的属性编辑器,Object Inspector没有内嵌对数
组属性编辑的支持。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3. 公布继承的属性
所有部件都从祖先类型继承属性。当你从已有部件继承时,新部件将继承祖先类型的所有属性。如果你继承的是抽象类,则继承的属性是protected或public,但不是published。如想使用户访问protected或public属性,可以将该属性重定义为published。如果你使用TWinControl继承,它继承了Ctl3D属性,但是protected的,因此用户在设计和运行时不能访问Ctl3D,通过在新部件中将Ctl3D重声明为published,就改变了Ctl3D的访问级别。下面的代码演示如何将Ctl3D声明为published,使之在设计时可被访问。
type
TSampleComponent=class(TWinControl)
published
property Ctl3D;
end;
4. 定义部件属性
⑴ 属性的声明
声明部件的属性,你要描述:
属性名
属性的类型
读和设置属性值的方法
至少,部件属性应当定义在部件对象声明的public部分,这样可以在运行时很方便地从外部访问;为了能在设计时编辑属性,应当将属性在published部分声明,这样属性能自动显示在Object Inspector窗口中。下面是典型的属性声明:
type
TYourComponent=class(TComponent)
…
private
FCount: Integer { 内部存储域 }
function GetCount: Integer; { 读方法 }
procedure SetCount(ACount: Integer); { 写方法 }
pubilic
property Count: Integer read GetCount write SetCount;
end;
⑵ 内部数据存储
关于如何存储属性的数据值,Delphi没有特别的规定,通常Delphi部件遵循下列规定:
属性数据存储在对象的数据域处
属性对象域的标识符以F开头,例如定义在TControl中的属性FWidth
属性数据的对象域应声明在private部分
后代部件只应使用继承的属性自身,而不能直接访问内部的数据存储。
⑶ 直接访问
使属性数据可用的最简单的办法是直接访问。属性声明的read 和write部分描述了怎样不通过调用访问方法来给内部数据域赋值。但一般都用read进行直接访问,而用write进行方法访问,以改变部件的状态。
下面的部件声明演示了怎样在属性定义的read 和write部分都采用直接访问:
type
TYourComponent=class(TComponent)
…
private { 内部存储是私有 }
FReadOnly: Boolean; { 声明保存属性值的域 }
published { 使属性在设计时可用 }
property ReadOnly: Boolean read FReadOnly write FReadOnly;
end;
⑷ 访问方法
属性的声明语法允许属性声明的read和write部分用访问方法取代对象私有数据域。不管属性是如何实现它的read 和write部分,方法实现应当是private,后代部件只能使用继承的属性访问。
① 读方法
属性的读方法是不带参数的函数,并且返回同属性相同类型的值。通常读函数的名字是“Get”后加属性名,例如,属性Count的读方法是GetCount。不带参数的唯一例外是数组属性。如果你不定义read方法,则属性是只写的。
② 写方法
属性的写方法总是只带一个参数的过程。参数可以是引用或值。通常过程名是"Set"加属性名。例如,属性Count的写方法名是SetCount。参数的值采用设置属性的新值,因此,写方法需要执行在内部存储数据中写的操作。
如果没有声明写方法,那么属性是只读的。
通常在设置新值前要检测新值是否与当前值不同。
下面是一个简单的整数属性Count的写方法:
procedure TMyComponent.SetCount( value: Integer);
begin
if value <>FCount then
begin
FCount := Value;
update;
end;
end;
⑸ 缺省属性值
当声明一个属性,能有选择地声明属性的缺省值。部件属性的缺省值是部件构造方法中的属性值集。例如,当从Component Palette选择某部件置于窗体中时,Delphi通过调用部件构造方法创建部件,并决定部件属性初始值。
Delphi使用声明缺省值决定是否将属性值存在DFM文件中。如果不描述缺省值,Delphi将总是保存该属性值。声明缺省值的方法是在属性声明后加default指令,再跟缺省值。
当重声明一个属性时,能够描述没有缺省值的属性。如果继承的属性已有一个,则设立没有缺省值的属性的方法是在属性声明后加nodefault指令。如果是第一次声明属性,则没有必要加nodefault指令,因为没有default指令即表示如此。
下例是名为IsTrue的布尔类型属性设置缺省值True的过程:
type
TSampleComponent=class(TComponent)
private
FIsaTrue: Boolean;
pubilic
constructor Create (AOwner: TComponent); Overvide;
published
property Istrue: Boolean read FIsTrue write FIsTrue default True;
end;
constructor TSampleComponent.Create (AOwner: TComponent);
begin
inherited Create ( Aowner);
Fistvue := True; { 设置缺省值 }
end;
5. 编写属性编辑器
Object Inspector提供所有类型属性的缺省编辑器,Delphi也支持通过编写和注册属性编辑器的方法为属性设计自己的编辑器。可以注册专门为自定义部件的属性设计的编辑器,也可设计用于所有某类型的属性。编写属性编辑器需要下列五个步骤:
继承一个属性编辑器对象
将属性作为文本编辑
将属性作为整体编辑
描述编辑器属性
注册属性编辑器
⑴ 继承属性编辑器对象
DsgnIntf库单元中定义了几种属性编辑器。它们都是从TPropertyEditor继承而来。当创建属性编辑器时,可以直接从TPropertyEditor中继承或从表中的任一属性编辑器中继承。
表19.4 属性编辑器的类型
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
类型 编辑的属性
─────────────────────────────────────
TOrdinalProperty 所有有序的属性(整数、字符、枚举)
TIntegerProperty 所有整型,包括子界类型
TCharProperty 字符类型或字符子集
TEnumProperty 任何枚举类型
TFloatProperty 所有浮点数
TStringProperty 字符串,包括定长的字符串
TSetElementProperty 集合中的独立元素
TSetElementProperty 所有的集合,并不是直接编辑集合类型,而是展开成一列
集合元素属性
TClassProperty 对象,显示对象名,并允许对象属性的展开
TMethodPropevty 方法指针,主要指事件
TComponentProperty 相同窗体中的部件,用户不能编辑部件的属性,
但能指向兼容的部件
TColorProperty 部件颜色,显示颜色常量,否则显示十六进制数
TFontNameProperty 字体名称
TFontProperty 字体,允许展开字体的属性或弹出字体对话框
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
下面是TFloatPropertyEditor的定义:
type
TFloatProperty=Class(TPropertyEditor)
public
function AllEqual: Boolean; override;
function GetValue: String; override;
procedure SetValue ( Const Value: string ); override;
end;
⑵ 象文本一样编辑属性
所有的属性都需要将它们的值在Object Inspector窗口中以文本的方式显示。属性编辑器对象提供了文本表现和实际值之间转换的虚方法。这些虚方法是GetValue和SetValue,你的属性编辑器也能继承了一系列的方法用于读和写不同类型的值。见下表:
表19.5 读写属性值的方法
━━━━━━━━━━━━━━━━━━━━━━━━━━
属性类型 "Get"方法 "Set"方法
──────────────────────────
浮点数 GetFloatValue SetFloatVallue
方法指针 GetMethodValue SetMehodValue
有序类型 GetOrdValue SetOrdValue
字符串 GetStrValue SetStrValue
━━━━━━━━━━━━━━━━━━━━━━━━━━
当覆盖GetValue方法时,调用一个"Get"方法;当覆盖SetValue方法时调用一个"Set"方法。
属性编辑器的GetValue方法返回一个字符串以表现当前属性值。缺省情况下GetValue返回"unknown"。
属性编辑器的SetValue接收Object Inspector窗口String类型的参数,并将其转换成合适的类型,并设置属性值。
下面是TIntegerProperty的GetValue和SetValue的例子:
function TIntegerProperty GetValue: string;
begin
Result := IntToStr (GetOrdValue);
end;
proceduve TIntegerPropertySetValue (Const Value: string);
var
L: Longint;
begin
L := StrToInt(Value); { 将字符串转换为数学 }
with GetTypeData (GetPropType)^ do
if ( L < Minvalue ) or ( L > MaxValue ) then
Raise EPropertyError.Create (FmtloadStr(SOutOfRange,
[MinValue,MaxValue]));
SetOrdValue (L);
end;
⑶ 将属性作为一个整体来编辑
Delphi支持提供用户以对话框的方式可视化地编辑属性。这种情况常用于对对象类型属性的编辑。一个典型的例子是Font属性,用户可以找开Font对话框来选择字体的属性。
提供整体属性编辑对话框,要覆盖属性编辑对象的Edit方法。Edit方法也使用"Get"和"Set"方法。
在大多数部件中使用的Color属性将标准的Windows颜色对话框作为属性编辑器。下面是TColorProperty的Edit方法
procedure TColorProperty.Edit
var
ColorDialog: TColorDialog;
begin
ColorDialog := TColorDialog.Create(Application); { 创建编辑器 }
try
ColorDialog.Color := GetOrdValue; { 使用已有的值 }
if ColorDialog.Execute then
SetOrdValue (ColorDialog.Color);
finally
ColorDialog.Free;
end;
end;
⑷ 描述编辑器的属性
属性编辑必须告诉Object Inspector窗口如何采用合适的显示工具。例如Object Inspector窗口需要知道属性是否有子属性,或者是否能显示可能取值的列表。描述编辑器的属性通常覆盖属性编辑器的GetAttributes方法。
GetAttributes返回TPropertyAttributes类型的集合。集合中包括表中任何或所有的值:
表19.6 属性编辑器特征标志
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
标志 含 义 相关方法
──────────────────────────────
paValuelist 编辑器能给予一组枚举值 GetValues
paSubPropertie 属性有子属性 GetPropertises
paDialog 编辑器能显示编辑对话框 Edit
PaMultiSelect 当用户选择多于一个部件
时,属性应能显示 N/A
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Color属性是灵活的,它允许在Object Inspector窗口中以多种方式选择他们。或者键入,或者从列表中选择定编辑器。因此TColorProperty的GetAttributes方法在返回值中包含多种属性。
function TColorProperty.GetAttributes: TProrertyAttributes;
begin
Result := [PaMultiselect, paDialog, paValuelist];
end;
⑸ 注册属性编辑器
一旦创建属性编辑器,必须在Delphi中注册。注册属性编辑器时,要与某种属性相联。
调用RegisterPropertyEditor过程来注册属性编辑器。该过程接受四个参数:
要编辑的属性的类型信息的指针。这总是通过调用调用TypeInfo函数得到,如TypeInfo ( TMyComponent )
编辑器应用的部件类型,如果该参数为nil则编辑器应用于所给的类型的所有属性
属性名,该参数只有在前一参数描述了部件的情况下才可用
使用该属性编辑器的属性的类型
下面引用了注册标准部件的过程:
procedure Register;
begin
RegisterPropertyEditor (TypeInfo(TComponent), nil, TComponentProperty,
RegisterPropertyEditor(TypeInfo(TComponentName), TComponent,
'Name', (ComponentNamePropety);
RegisterPropertyEditor (TypeInfo(TMenuItem), TMenu, '', TMenuItemProperty);
end;
这三句表达式使用RegisterPropertyEditor三种不同的用法:
第一种最典型
它注册了用于所有TComponent类型属性的属性编辑器TComponentProperty。通常,当为某种类型属性注册属性编辑器时,它就能应用于所有这种类型的属性,因此,第二和第三个参数为nil。
第二个表达式注册特定类型的属性编辑器
它为特定部件的特定属性注册属性编辑器,在这种情况下,编辑器用于所有部件的Name属性。
第三个表达式介于第一个和第二个表达式之间
它为部件TMenu的TMenuItem类型的所有属性注册了属性编辑器。
19.2.2.2 创建事件
事件是部件的很重要的部分。事件是部件必须响应的系统事件与响应事件的一段代码的联接。响应代码被称为事件处理过程,它总是由部件用户来编写。通过使用事件,应用开发者不需要改变部件本身就能定制部件的行为。作为部件编写者,运用事件能使应用发者定制所有的标准Delphi部件。要创建事件,应当理解:
什么是事件
怎样实现标准事件
怎样定义自己的事件
1. 什么是事件
事件是联接发生的事情与某些代码的机制,或者说是方法指针,一个指向特定对象实例的特定方法的指针。从部件用户的角度,事件是与系统事件(如OnClick)有关的名称,用户能给该事件赋特定的方法供调用。例如,按钮Buttonl有OnClick方法,缺省情况下Delphi在包含该按钮的窗体中产生一个为ButtonlClick的方法,并将其赋给OnClick。当一个Click事件发生在按钮上时,按钮调用赋给OnClick的方法ButtonlClick:
部件用户将事件看作是由用户编写的代码,而事件发生时由系统调用的处理办法。
从部件编写者角度事件有更多的含义。最重要的是提供了一个让用户编写代码响应特定事情的场所。
要编写一个事件,应当理解:
事件和方法指针
事件是属性
事件处理过程类型
事件处理过程是可选的
⑴ 事件是方法指针
Delphi使用方法指针实现事件。一个方法指针是指向特定对象实例的特定方法的特定指针。作为部件编写者,能将方法指针作为一种容器。你的代码一发现事情发生,就调用由用户定义的方法。
方法指针的工作方式就象其它的过程类型,但它们保持一个隐含的指向对象实例的指针。所有的控制都继承了一个名为Click的方法,以处理Click事件。Click方法调用用户的Click事件处理过程。
procedure TControl.Click;
begin
if Assigned(OnClick ) then OnClick( Self );
end;
如果用户给Control的OnClick事件赋了处理过程(Handle),那鼠标点按Control时将导致方法被调用。
⑵ 事件是属性
部件采用属性的形式实现事件。不象大多数其它属性,事件不使用方法来使实现read和write部分。事件属性使用了相同类型的私有对象域作为属性。按约定域名在属性名前加“F”。例如OnClick方法的指针,存在TNotifyEvent类型FOnClick域中。OnClick事件属性的声明如下:
type
TControl=class ( TComponent )
private
FOnClick: TNofiFyEvent; { 声明保存方法指针的域 }
protected
property OnClick: TNotifyEvent read FOnClick write FOnClick;
end;
象其它类型的属性一样,你能在运行时设置和改变事件的值。将事件做成属性的主要好处是部件用户能在设计时使用Object Inspector设置事件处理过程。
⑶ 事件处理过程类型
因为一个事件是指向事件处理过程的指针,因此事件属性必须是方法指针类型,被用作事件处理过程的代码,必须是相应的对象的方法。
所有的事件方法都是过程。为了与所给类型的事件兼容,一个事件处理过程必须有相同数目和相同类型的相同顺序的参数。Delphi定义了所有标准事件处理过程的方法类型,当你创建自己的事件时,你能使用已有的事件类型,或创建新的。虽然不能用函数做事件处理过程,但可以用var参数得到返回信息。
在事件处理过程中传递var参数的典型例子是TKeyPressEvent类型的KeyPressed事件。TKeyPressEvent定义中含有两个参数。一个指示哪个对象产生该事件。另一个指示那个键按下:
type
TKeyPressEvent=procedure( Sender: TObject; var key: char) of Object;
通常key参数包含用户按下键的字符。在某些情况下,部件的用户可能想改变字符值。例如在编辑器中强制所有字符为大写,在这种情况下,用户能定义下列的事件处理过程:
procedure TForml.EditlKeyPressed( Sender: TObject; var key: char);
begin
key := Upcase( key );
end;
也可使用var参数让用户覆盖缺省的处理。
⑷ 事件处理过程是可选的
在为部件创建事件时要记住部件用户可能并不编写该事件的处理过程。这意味着你的部件不能因为部件用户没有编写处理代码而出错。这种事件处理过程的可选性有两个方面:
① 部件用户并非不得不处理事件
事件总是不断地发生在Windows应用程序中。例如,在部件上方移动鼠标就引起Windows发送大量的Mouse-Move消息给部件,部件将鼠标消息传给OnMouseMove事件。在大多数情况下,部件用户不需要关心MouseMove事件,这不会产生问题,因为部件不依赖鼠标事件的处理过程。同样,自定义部件也不能依赖用户的事件处理过程。
② 部件用户能在事件处理过程写任意的代码
一般说来,对用户在事件处理过程中的代码没有限制。Delphi部件库的部件都支持这种方式以使所写代码产生错误的可能性最小。显然,不能防止用户代码出现逻辑错误。
2. 怎样实现标准事件
Delphi带的所有控制继承了大多数Windows事件,这些就是标准事件。尽管所有这些事件都嵌在标准控制中,但它们缺省是protected,这意味着用户无法访问它们,当创建控制时,则可选择这些事件使用户可用。将这些标准事件嵌入自定义控制需要考虑如下:
什么是标准事件
怎样使事件可见
怎样修改标准事件处理过程
⑴ 什么是标准事件
有两种标准事件:用于所有控制和只用于标准Windows控制。
最基本的事件都定义在对象TControl中。窗口控制、图形控制和自定义控制都继承了这些事件,下面列出用于所有控制的事件:
OnClick OnDragDrop OnEndDrag OnMouseMove
OnDblClick OnDragOver OnMouseDown OnMouseUp
所有标准事件在TControl中都定义了相应的protected动态方法,只是没有加“On”例如OnClick事件调用名为Click的方法。
标准控制(从TWinControl继承)具有下列事件:
OnEnter OnKeyDown OnkeyPress OnKeyUp OnExit
正如TControl中的标准事件,窗口控制也有相应protected动态方法。
⑵ 怎样使事件可见
标准事件的声明是protected,如果想使用户在运行时或设计时能访问它们,就需要将它们重声明为public和 published。重声明属性而不描述它的实现将继承相同的实现方法,只是改变了访问级别。例如,创建一个部件并使它的OnClick事件出现在运行时,你可增加下面的部件声明:
type
TMyControl=class(TCustomControl)
published
property OnClick; { 使OnClick在objectinspector中可见 }
end;
⑶ 怎样修改标准事件处理过程
如果想修改自定义部件响应某种事件的方法,可以重写代码并将其赋给事件。将联接每个标准事件的方法声明的protected是出于慎密的考虑。通过,覆盖实现方法,能修改内部事件处理过程,通过调用继承的方法,能保持标准事件处理过程。
调用继承的方法的顺序是很重要的。一般首先调用继承的方法,允许用户的事件处理过程代码在你的定制代码前执行。然而也有在调用继承的方法之前执行自己的代码情况出现。
下面是一个覆盖Click事件的例子:
procedure TMyControl.Click;
begin
inherited Click; { 执行标准处理,包括调用事件处理过程你自己的定制代码 }
end;
3. 定义自己的事件
定义全新的事件的情况是很少见的。只有当部件的行为完全不同于任何其它事件才需要定义新事件。定义新事件一般包含三个步骤:
触发事件
定义处理过程类型
声明事件
调用事件
⑴ 触发事件
定义自己的事件要遇到的第一个关键是:当使用标准事件时你不需要考虑由什么触发事件。对某些事件,问题是显然的。例如:一个MouseDown事件是在用户按下鼠标的左键时发生,Windows给应用发送WM_LBUTTONDOWN消息。接到消息后,一个部件调用它的MouseDown方法,它依次调用用户的OnMouseDown事件处理过程代码。但是有些事件却不是那么可以描述清楚的。例如:滚行杠有一个OnChange事件,可被各种情况触发,包括按键、鼠标点按或其它按制中的改变。当定义事件时,你必须使各种情况的发生调用正确的事件。
这里有TControl处理WM_LBUTTONDOWN消息的方法,DoMouseDown是私有的实现方法,它提供了一般的处理左、右和中按钮的方法,并将Windows消息的参数转换为MouseDown方法的值。
type
TControl = class(TComponent)
private
FOnMouseDown: TMouseEvent;
procedure DoMouseDown(var Message: TWMMouse; Button: TMouseButton;
Shift: TShiftState);
procedure WMLButtonDown(var Message: TWMLButtonDown);
message M_LBUTTONDOWN;
protected
procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer); dynamic;
end;
procedure TControl.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
if Assigned(FOnMouseDown) then
FOnMouseDown(Self, Button, Shift, X, Y); { 调用事件处理过程 }
end;
procedure TControl.DoMouseDown(var Message: TWMMouse; Button: TMouseButton;
Shift: ShiftState);
begin
with Message do
MouseDown(Button, KeysToShiftState(Keys) + Shift, XPos, YPos); { 调用动态方法 }
end;
procedure TControl.WMLButtonDown(var Message: TWMLButtonDown);
begin
inherited; { perform default handling }
if csCaptureMouse in ControlStyle then
MouseCapture := True;
if csClickEvents in ControlStyle then
Include(FControlState, csClicked);
DoMouseDown(Message, mbLeft, []); { 调用常规的mouse-down 方法 }
end;
当两种事情-状态变化和用户交互—发生时,处理机制是相同的,但过程稍微不同。用户交互事件将总是由Windows消息触发。状态改变事件也与Windows消息有关,但它们也可由属性变化或其它代码产生。你拥有对自定义事件触发的完全控制。
⑵ 定义处理过程类型
一旦你决定产生事件,就要定义事件如何被处理,这就是要决定事件处理过程的类型。在大多数情况下,定义的事件处理过程的类型是简单的通知类型(TNotifyEvent)和已定义的事件类型。
通知事件只是告诉你特定的事件发生了,而没有描述什么时候和什么地方。通知事件使用时只带一个TObject类型的参数,该参数是Sender。然而所有通知事件的处理过程都知道是什么样的事件发生和发生在那个部件。例如:Click事件是通知类型。当编写Click事件的处理过程时,你知道的是Click事件发生和哪个部件被点按了。通知事件是单向过程。没有提供反馈机制。
在某些情况下,只知道什么事件发生和发生在那个部件是不够的。如果按键事件发生,事件处理过程往往要知道用户按了哪个键。在这种情况下,需要事件处理过程包含有关事件的必要信息的参数。如果事件产生是为了响应消息,那么传递给事件的参数最好是直接来自消息参数。
因为所有事件处理过程都是过程,所以从事件处理过程中返回信息的唯一方法是通过var参数。自定义部件可以用这些信息决定在用户事件处理过程执行后是否和怎样处理事件。
例如,所有的击键事件(OnKeyDown、OnKeyUp和OnKeyPressed)通过名为key的var参数传递键值。为了使应用程序看见包含在事件中的不同的键,事件处理过程可以改变key变量值。
⑶ 声明事件
一旦你决定了事件处理过程的类型,你就要准备声明事件的方法指针和属性。为了让用户易于理解事件的功能,应当给事件一个有意义的名字,而且还要与部件中相似的属性的名称保持一致。
Delphi中所有标准事件的名称都以“On”开头。这只是出于方便,编译器并不强制它。Object Inspector是看属性类型来决定属性是否是事件,所有的方法指针属性都被看作事件,并出现在事件页中。
⑷ 调用事件
一般说来,最好将调用集中在事件上。就是说在部件中创建一个虚方法来调用用户的事件处理过程和提供任何缺省处理。当调用事件时,应考虑以下两点:
必须允许空事件
用户能覆盖缺省处理
不能允许使空事件处理过程产生错误的情况出现。就是说,自定义部件的正常功能不能依赖来自用户事件处理过程的响应。实际上,空事件处理过程应当产生与无事件处理过程一样的结果。
部件不应当要求用户以特殊方式使用它们。既然一个空事件处理过程应当与无事件处理过程一样动作,那么调用用户事件处理过程的代码应当象这样:
if Assigned(OnClick) then OnClick(Self);
{ 执行缺省处理 }
而不应该有这样的代码:
if Assigned(OnClick) then
OnClick(Self)
else
…; { 执行缺省处理 }
对于某些种类的事件,用户可能想取代缺省处理甚至删除所有的响应。为支持用户实现这种功能,你需要传递var参数给事件处理过程,并在事件处理过程返回时检测某个值。空事件处理过程与无事件处理过程有相同作用。因为空事件处理过程不会改变任何var参数值。所以缺省处理总是在调用空事件处理过程后发生。
例如在处理Key-Press事件,用户可以通过将var参数key的值设置为空字符(#0)来压制部件的缺省处理,代码如下:
if Assigned(OnkeyPress) then OnkeyPress(Self key);
if key <> #0 then { 执行缺省处理 } ;
实际的代码将与这稍有不同,因为它只处理窗口消息,但处理逻辑是相同的。在缺省情况下,部件先调用任何用户赋予的事件处理过程,然后执行标准处理。如果用户的事件处理过程将key设为空,则部件跳过缺省处理。
19.2.2.3 处理消息
在传统Windows编程中,一个很关键的方面是处理Windows发送给应用程序的消息。Delphi已经帮你处理了大多数的普通消息,但是在创建部件的过程中有可能Delphi没有处理方法,得由自己处理消息,也可能创建了新的消息需要处理它们。
学习掌握Delphi的消息处理,要掌握以下三个方面:
理解消息处理系统
修改(改变)消息处理方法
建立新的消息处理方法
1. 理解消息处理系统
所有的Delphi对象内部具有处理消息的机制,如调用消息处理方法或消息处理过程。消息处理的基本思想是对象接收某种消息并派送它们,这是通过调用与接收的消息相应的方法来实现的,如果没有相应于消息的指定的方法,那就调用缺省处理。下面的图解表示消息派送系统:
Delphi部件库定义了将所有Windows消息(包括用户自定义消息)直接转换到对象方法调用的消息派送系统。一般没有必要改变这种消息派送系统,只要建立消息处理方法。
⑴Windows消息中有什么?
Windows消息是包含若干有用的域的数据记录。记录中最重要的是一个整型大小的值,该值标识消息。Windows定义了大量的消息。库单元Messages声明了所有消息的标识。消息中其它的有用信息包括两个域参数和结果域。两个参数分别是16位和32位的。Windows代码总是以wParam和lParam来引用它们。
最初,Windows程序员不得不记住包含的每一个参数。现在,微软公司已经命名了这参数。这样理解伴随这些消息的信息就更简单了。例如,WM_KEYDOWN消息的参数被称为vkey和keydata,这就比wParam和lParam给出了更多的描述信息。
Delphi为不同类型的消息定义了指定的记录类型。如鼠标消息在long参数中传递鼠标事件的x、y座标,一个在高字,一个在低字。使用鼠标消息记录,你不需要自己关心哪个字是哪个座标,因为引用这些参数时通过名子Xpos和Ypos取代了lParamLo和lParamHi。
⑵ 派送方法
当应用程序创建窗口时,在Windows Kernel中注册了一个窗口过程。窗口过程是处理窗口消息的函数。传统上,窗口过程包括了Case表达式,表达式的每个入口是窗口要处理的每一条消息。当你每次创建窗口时,必须建立完整的窗口过程。
Delphi在下列三方面简化了消息派送:
每个部件继承了完整的消息派送系统
派送系统具有缺省处理。用户只需定义想响应的消息的处理方法
可以修改消息处理的一部分,依靠继承的方法完成大多数处理
这种消息派送系统的最大优点是用户能在任何时候安全地发送任何消息给任何部件。如果部件没有为该消息定义处理方法,那缺省处理方法会解决这个问题,通常是忽略它。
Delphi为应用程序每种类型的部件注册了名为MainWndProc的方法作为窗口过程。MainWndProc包含了异常处理块,它完成从Windows到名为WndProc的虚方法传送消息记录,并且通过调用应用程序对象的HandleException方法处理异常。
MainWndProc是静态方法,没有包含任何消息的指定处理方法。定制过程发生在WndProc中,因为每个部件类型都能覆盖该方法以适合特定的需要。
WndProc方法为每个影响它们处理的任何条件进行检查,以捕捉不要的消息。例如,当被拖动时,部件忽略键盘事件,因此,TWinControl的WndProc只在没有拖动时传送键盘事件。最后WndProc调用Dispatch方法,该方法是从TObject继承来的静态方法,决定什么方法来处理消息。
Dispatch使用消息记录的Msg域来决定怎样派送特定消息。如果部件已经给该消息定义了处理方法,则Dispatch调用该方法,反之,Dispatch调用缺省处理方法。
2. 改变消息处理方法
在改变自定义部件的消息处理方法之前,先要弄清楚你真正想要做什么。Delphi将大多数的Windows消息转换成部件编写者和部件用户都能处理的事件。一般来说,你应当改变事件处理行为而不是改变消息处理行为。
为了改变消息处理行为,要覆盖消息处理方法。也能提供捕获消息防止部件处理该消息。
⑴ 覆盖处理方法
为了改变部件处理特定消息的方法,要覆盖那个消息的处理方法。如果部件不处理该消息,你就需要声明新的消息处理方法。
为了覆盖消息处理方法,要在部件中以相同的消息索引声明新的方法。不要使用override指令,你必须使用Message指令和相应的消息索引。
例如,为了覆盖一个处理WM_PAINT消息的方法,你要重声明WMPaint方法:
type
TMyComponent=class(…)
procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
end;
⑵ 使用消息参数
在消息处理方法内部,自定义部件访问消息记录的所有参数。因为消息总是var参数,如果需要的话,事件处理过程可以改变参数的值。Result域是经常改变的参数。Result是Windows文档中所指的消息的返回值:由SendMessage返回。
因为消息处理方法的消息参数的类型随着被处理的消息的变化而变化,所以应当参考Windows消息文档中的参数的名字和含义。如果出于某种原因要使用旧风格的消息参数(wParam、lParam),可以配合通用类型TMessage来决定Message。
⑶ 捕获消息
在某种情况下,你可能希望自定义部件能忽略某种消息。就是说,阻止部件将该消息派送给它的处理方法。为了那样来捕获消息,可以覆盖虚方法WndProc。
WndProc方法在将消息传给Dispatch方法前屏蔽该消息。它依次决定哪一个方法来处理消息。通过覆盖WndProc,部件得到了派送消息之前过滤它们的机会。
通常,象下面这样覆盖WndProc:
procedure TMyControl.WndProc(var Message: TMessage);
begin
{ 决定是否继续处理过程 }
inherited WndProc (Message);
end;
下面的代码是TControl的WndProc的一部分。TControl定义整个范围内的鼠标消息,当用户拖动和放置控制时,它们将被滤过。
procedure TControl WndProc(var Message:TMessage);
begin
if (Message.Msg >= WM_MOVSEFIRST) and
(Message.Msg <= WM_MOUSELAST) then
if Dragging then
DragMouseMsg(TWMMOUSE(Message)) { 处理拖动 }
else
… { 正常处理其它 }
… { 否则正常处理 }
end;