二、控件开发纵览
通过开发上边这个控件,我们已经对Delphi控件开发有了基本的认识。下面我们将系统的讲述一下控件开发的知识。
制作控件第一件事就是选择适当的 Delphi对象类型作为父对象,以派生新的对象。子对象可以继承父对象的全部非 private部件,但不能摆脱不需要的部件。因此,所选父对象应尽可能多地包含子对象所需的属性、事件和方法,但不应包含子对象不需要的东西。Delphi必须从Tcomponent或Tcomponent的子类派生。TComponent是所有 Delphi控件的基点,但若直接从 TComponent 派生新控件,很多东西就需要自己从头做起。一般只有非可视控件才直接从 TComponent派生。 Delphi提供了若干专门用于制作控件(可视控件)的对象类型,都是从 TControl和 TWinControl派生而来。
TControl的子类型用于非窗口式控件, TWinControl的子类型则用于窗口式控件。除非特殊需要,一般不直接从 TControl和TWinControl派生新控件,而是从其子类型派生。这样可以充分利用原有的属性、事件和方法,减少很多工作量。在这些控件类型中,非通用的属性、事件和方法都声明为 protected。这样可以禁止控件用户访问,又能被子类型继承和修改。在新控件中,可以简单地把继承来的属性和事件重新声明为 published,使控件用户能在设计期通过对象编辑窗口访问,也可以进而修改属性的默认值和读写方式,或是重载( override)事件处理子过程和其他控件方法,以修改其中的程序代码。重声明可以放宽访问权限,但不能相反,例如,不可能把 published属性重声明为 private或 protected。
Delphi控件也是Delphi的类,所有的控件都有特定的结构。一般控件包括三大组成部分:属性、方法和事件,下面先介绍初学控件开发的最难懂的属性部分,其他部分我们将在以后章节为大家介绍。
属性主要部分就是属性的读写方法(或读写字段)。前面的例子用的是读写字段,也就是对属性的读写都通过对字段的读写来完成。下面为大家讲解一下读写方法的使用方法:
TmyComponent = class(TComponent)
Private
Fcount: Integer;
Procedure SetCount(Avalue: Integer);
Pulbished
Property Count: Integer read Fcount write SetCount;
End;
这个例子中当执行MyComponent1.Count := 1;这样的代码时,将会导致SetCount方法执行,并且参数Avalue被指定为1;当执行 I := MyComponent1.Count;方法时,会将 Fcount的值返回给I。
属性的声明语法允许属性声明的Read和Write部分用访问方法取代对象私有数据域。属性的读方法是不带参数的函数,返回同属性相同类型的值。通常读方法以Get开头。属性的写方法总是带一个参数的过程。写方法常常以Set开头。
三、开关控件TlincoSwitch
用过Delphi1(好古老的东东呀!)的人相信都记得这个开关控件
,不知道当初Borland为什么把这么一个在开发普通应用程序中应用不到的工控控件放到Delphi中,而且在Delphi2及其以后的版本中再也没有见过它的身影。让我们怀着怀旧的心情把这位“开国元老”请出来吧!
1、建立位图资源文件:
用Image Editor建立一个Res文件,并在文件中分别建立下面两个位图
,并分别命名为SWITCHON、SWITCHOFF。保存此Res到控件单元所在目录下。
2、写控件代码。
unit LincoSwitch;
interface
uses
SysUtils, Classes, Controls, Graphics, Windows;
type
TLincoSwitch = class(TCustomControl)
private
FIsOn: Boolean;
FPicOn: Graphics.TBitmap;
FPicOff: Graphics.TBitmap;
procedure FSetIsOn(AValue: Boolean);
protected
procedure Click;override;
procedure Paint;override;
public
constructor Create(AOwner: TComponent);override;
destructor Destroy;override;
published
property IsOn: Boolean read FIsOn write FSetIsOn;
property OnClick;
property OnKeyDown;
property OnKeyPress;
property OnKeyUp;
property OnCanResize;
property OnDblClick;
property OnMouseDown;
property OnMouseMove;
property OnMouseUp;
property OnMouseWheel;
property OnResize;
end;
procedure Register;
implementation
{$R *.res}
procedure LoadBitmapFromRes(ABitmapId: string; ABitmap: Graphics.TBitmap);
begin
ABitmap.LoadFromResourceName(hInstance, ABitmapId);//从资源文件中读取位图
end;
constructor TLincoSwitch.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FPicOn := Graphics.TBitmap.Create;
FPicOff := Graphics.TBitmap.Create;
LoadBitmapFromRes('SWITCHON', FPicOn);
LoadBitmapFromRes('SWITCHOFF', FPicOff);
Invalidate;
end;
destructor TLincoSwitch.Destroy;
begin
FPicOn.Free;
FPicOff.Free;
inherited;
end;
procedure TLincoSwitch.Click;
begin
IsOn := not IsOn;//改变按钮的状态
Invalidate;
inherited;
end;
procedure TLincoSwitch.Paint;
begin
//画开关图案
if IsOn then
StretchBlt(Canvas.Handle, 0, 0, self.Width, self.Height, FPicOn.Canvas.Handle,
0, 0, FPicOn.Width, FPicOn.Height,SRCCOPY)
else
StretchBlt(Canvas.Handle, 0, 0, self.Width, self.Height, FPicOff.Canvas.Handle,
0, 0, FPicOff.Width, FPicOff.Height,SRCCOPY);
end;
procedure TLincoSwitch.FSetIsOn(AValue: Boolean);
begin
FIson := AValue;
Invalidate;
end;
procedure Register;
begin
RegisterComponents('Linco', [TLincoSwitch]);
end;
end.
3、代码分析
(1)、因为我们要在控件表面上将按钮的图案画出来,所以我们选择TcustomControl做为父类控件,因为它有个Canvas属性,我们可以利用Canvas在控件表面作图。不选用Tcontrol的原因是因为它有很多我们不需要的属性。
(2)、ABitmap.LoadFromResourceName(hInstance, ABitmapId);是从资源文件中读取Id为AbitmapId的位图,关于资源文件的使用请参考其他相关资料。注意代码中的“{$R *.res}”,它的作用是将资源文件编译到程序文件中,如果没有这个预编译条件,程序将会出现错误。
(3)、StretchBlt是将位图画到画板上,使用方法请参考MSDN。
(4)、我们为控件增加了IsOn属性。这个布尔属性用来表示开关的状态(开/关)。
从property IsOn: Boolean read FIsOn write FSetIsOn;我们可以看出这个属性是个可读可写的属性。当读这个属性时会将FisOn的值返回给调用者,而写属性时则会调用FsetIsOn方法,并将赋给属性的值做为参数传递给FsetIsOn。在FsetIsOn方法中,有如下实现代码:
FIson := AValue;
Invalidate;
首先将Fison设置为参数传递来的值,然后调用 Invalidate;要求重画控件,以告诉用户控件的状态已经改变,这一点是使用写字段无法做到的。
(5)
FPicOn: Graphics.TBitmap;
FPicOff: Graphics.TBitmap;
是声明两个.Tbitmap类型变量以保存控件的开关两种状态的图案。
(6)
procedure Click;override;
procedure Paint;override;
分别是覆盖父类中相应的调度方法。当控件被鼠标单击时,Click方法会被调用,我们将在Click中改变控件的开关状态;Paint方法则在用户调用 Invalidate方法或控件发生重画时调用,我们一般在这个方法绘制控件的图案。
(7)、TcustomControl中又很多事件处理句柄。比如OnClick、OnKeyDown等,但是它把他们声明成了Protected保护级别,所以我们在Object Inspector中看不到他们,如果我们要他们可以在Object Inspector中被用户编辑的话,只要在Published中重新声明他们即可,不用写他们的读写方法,只要使用:Property 属性名;
这样的方法就可以。比如这个例子中的:Property Onclick;
思考题:
1、 做一个有特效的按钮控件,当鼠标按下时按钮是一个红色边框的空心圆,当鼠标松开时按钮是一个淡绿色边框的空心圆。
2、 对于父类控件中为protected的属性,如果想将它在子类控件中公布,应该怎么做?请思考Delphi为什么要将一些属性设为protected级别?
Delphi控件开发浅入深出(三)
四、对特定字符串敏感的Edit控件
我们这个控件将演示控件的自定义事件的书写。这个控件有一个类型为string的SensitiveText属性,当用户在输入框中输入的文字为InvalidText时就会触发OnSensitiveText事件。按照惯例,我先把源码展示给大家:
unit TextSenseEdit;
interface
uses
SysUtils, Classes, Controls, StdCtrls;
type
TSensitiveTextEvent = procedure(AText: string) of object;//方法指针
TTextSenseEdit = class(TEdit)
private
FSensitiveText: string;
FOnSensitiveText: TSensitiveTextEvent;
procedure SetSensitiveText(AValue: string);
protected
procedure Change;override;
public
published
property SensitiveText: string read FSensitiveText write SetSensitiveText;
property OnSensitiveText: TSensitiveTextEvent read FOnSensitiveText write FOnSensitiveText;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Linco', [TTextSenseEdit]);
end;
procedure TTextSenseEdit.Change;
begin
inherited;
if Text = SensitiveText then
if Assigned(OnSensitiveText) then
OnSensitiveText(Text);
end;
procedure TTextSenseEdit.SetSensitiveText(AValue: string);
begin
FSensitiveText := AValue;
end;
end.
代码解释:
(1)、SensitiveText属性的添加方法大家已经熟悉了,这里不多解释。
(2)、正如大家猜测的,Change方法正是编辑框文字发生变化时的调度方法,它将引起OnChange事件。我们可以在这个方法中监控编辑框文字发生的变化,当文字等于SensitiveText就触发OnSensitiveText事件(具体的实现方法在后边解释)。
(3)、Delphi中的控件的事件机制是通过方法指针来实现的。声明方法指针的格式为:
方法指针名称 = procedure(参数列表) of object;
声明事件属性的方法与声明普通属性的方法相同。在我们这个例子中,我们首先声明一个FOnSensitiveText: TSensitiveTextEvent;私有变量,然后property OnSensitiveText: TSensitiveTextEvent read FOnSensitiveText write FOnSensitiveText; 声明事件属性。这样注册控件后,当用户把控件放到窗体中后,就会在Object Inspector中Evnets页中出现OnSensitiveText事件,我们就可以像使用其他事件一样使用这个事件了。
但是我们现在只是声明了一个事件属性,并没有书写任何代码来激发这个事件。我们应该在合适的时候激发此事件,显而易见我们应该在Change方法中激发此事件:
procedure TTextSenseEdit.Change;
begin
inherited;
if Text = SensitiveText then
if Assigned(OnSensitiveText) then
OnSensitiveText(Text);
end;
当if Text = SensitiveText时就判断控件使用者是否为OnSetSensitiveText写代码了(准确的说是是否为OnSetSensitiveText事件句柄赋值了),如果写代码了则调用OnSetSensitiveText(Text);来激发OnSetSensitiveText事件,并把控件的Text传递给方法的Avalue参数。正如“方法指针”这个名字一样,被声明为方法指针类型的变量可以当作方法使用,用来激发事件。VCL已经为我们预定义了一些常用的事件句柄,我们直接拿来使用:TnotifyEvent,TmouseEvent,TmouseMoveEvent,TkeyPressEvent等,具体可以参考VCL源码。
思考题:
1、做一个支持累加运算的文本编辑框控件,用户可以在编辑框中输入正整数。当用户按回车时,如果编辑框中输入的不是正整数(为负数、小数或一般字符串)则触发控件的OnError事件;如果输入的是正整数,则开始计算从1到用户输入的那个正整数中所有整数的和(用1+2+3+……这种累加的办法实现,不要用(1+n)*n/2这种直接计算的方法),并且在计算工程中如果发现计算的中间结果位数是5,则触发OnTailFive事件。