level 11
天道玄虚
楼主
【Delphi 奥法】
天道玄虚出品
注意:该教程适用于Delphi 10.1.2。
相关内容见该贴五楼的楼中楼
https://tieba.baidu.com/p/4934621524
第八章 类的研究
上一章,我们一起初步地看了看类的结构,这一章我们来稍稍深入一些地研究类。
8.0、还是从计时器开始
由于计时器实在是一个结构简单的东西,因此我们还是从计时器入手研究。
计时器位于FMX.Types单元下。
系统原本的计时器的功能是很单调的,不过是到了时间执行一个函数,而且这个函数能够传递的参数只是计时器本身。
先来大致看看我们希望计时器还有些什么功能。
首先应该是计次功能,也就是循环了一定次数之后暂停的功能。如果加入这个功能,那么应该在计时器计次结束的时候给一个事件。
其次应该是传递参数的功能,我们很多时候都会面临需要计时器传递其余参数的情形,以往这个时候常用的手段大概是进程休眠,且不说进程休眠的精度通常比不上计时器,再就是这进程休眠有比较多的缺陷,尤其是在单进程下的时候。
好了,先考虑这两个功能,随我一起来看看吧。不妨首先约定我们的计时器叫做TNewTimer。
8.1、计时器计次功能
我们希望达到的效果当然是每次计时器计次的时候都会改变计数的值。我们首先不妨约定计数的值代表还需要进行计时的次数,这个值应该设置为只读。
那么,很自然地,我们需要在private下写一个用于内部储存的变量,public下写一个只读属性。不妨命名为Conut,那么private下的变量为FCount,public下的属性就直接命名为Count。
好了,接下来要做的就是覆盖事件了。需要覆盖的事件当然是计时器到期事件,查看计时器源代码可以看到,计时器到期事件是DoOnTimer。
如何分辨出这个事件就是计时器到期事件呢?首先应该在protected下,其次,事件的名称应该与OnTimer有关,最后,事件通常都会声明为virtual,也就是虚方法。所谓虚方法,就是可以被子类方法覆盖的方法,需要覆盖的时候声明为override。通常事件的命名习惯都是事件变量名(例如OnTimer)去掉On,但是当这个名称已存在的时候,就在前面加上Do。
不信的话,你可以多看几个类的事件,就会发现我所言不差。
如何覆盖事件?首先,我们需要在子类下声明一个同名函数,之后再后面加上override。需要注意的是,只有虚方法才能够被参数声明完全相同的子类的方法覆盖。不论是声明为虚函数,还是参数声明完全相同都是不可少的。
那么,我们的声明应该写成:procedure DoOnTimer;override;
如果直接Ctrl+Shift+C,你应该会看到这样的声明:
procedureTNewTimer.DoOnTimer;
begin
inherited;
end;
你会看到一个没有讲过的保留字:inherited。从字面上理解,这个保留字意思是继承的,也就是继承父类方法。当子类方法与父类方法同名,而且参数声明相同的时候,直接使用inherited表示调用使用同一参数的父类方法,如果参数声明不同,那么需要在后面写上具体的函数调用,比如:
inherited Create(AOwner);
如果父类没有声明这个方法,或者声明在private下,那么就不可以调用。当然,子类可以降低父类方法的访问权限,要做到这一点,只需要改变同名方法的声明位置即可。不过需要注意的是,子类不可以提高父类方法的访问权限。
那么我们需要做的是,在触发计时的时候进行计数,如果计数的变量为零,那么不触发计时器,并且禁用计时器,之后应该响应计时器结束的事件,暂且将这个事件命名为TimerFinish。如果计数的变量不为零,那么进行计数并触发计时器。大致如下:
procedureTNewTimer.DoOnTimer;
begin
if FCount = 0 then
begin
Enabled := False;
TimerFinish;
end
else
begin
Dec(FCount);
inherited;
end;
end;
很简单,对吧。
那么,问题来了,如何自定义一个事件呢?
首先,我们需要想清楚这个事件的类型,也就是这个事件需要传递什么参数。计时器计时结束事件的参数只需要传递计时器本身就行,这个事件类型是TNotifyEvent。
那么,仿照原本的OnTimer事件的做法,我们需要一个FOnTimerFinish来储存这个事件,类型为TNotifyEvent,并设置相应的属性OnTimerFinish。然后,就是设置这个事件触发之后的具体操作,也就是TimerFinish的具体代码。
这个事件并不需要做什么其他的处理,唯一需要的只是向外提供一个事件的接口。那么,仿照DoOnTimer,代码应该是这样的:
procedureTNewTimer.TimerFinish;
begin
if Assigned(FOnTimerFinish) then
FOnTimerFinish(Self);
end;
这样,计时器的计次功能算是完成了一半了。
为什么说完成了一半呢?因为我们还没有提供设置计次的接口。因为我们不希望这个次数能够被随意更改,尤其是在计时器运行期间是不宜修改的。所以我们应该提供一个专有的方法,不妨设置为:
procedureTNewTimer.StartTimer(TimerEvent: TNotifyEvent; AInterval: Cardinal; LoopCount:Integer);
begin
if LoopCount < 1 then
begin
Enabled := False;
Exit;
end
else
FCount := LoopCount;
if (TimerEvent = nil) and (OnTimer = nil)then
Exit
else if TimerEvent <> nil then
OnTimer := TimerEvent;
Interval := AInterval;
Enabled := True;
end;
这个方法的功能是开始计时器。类似地,也可以提供一个停止计时器或者暂停计时器的方法,由于这些并不难,所以略去不表。
8.2、计时器传递变量
说完了计次功能,那么计时器如何传递变量呢?还记得之前讲过的流吗?你必须要承认,不论要传递什么东西,流都是一个可以考虑的选择。尤其是在不知道要传递什么的时候。
那么,我们增加一个流,不妨命名为FStream,类型是TStream或者TMemoryStream。由于我们在这个类里面增加了另一个类,所以我们需要覆盖创建和释放方法,与之前的覆盖类似,这一次我们需要的是在创建类的同时创建一个流,释放类之前释放这个流。
如下:
constructor Create(AOwner:TComponent); override;
destructor Destroy;override;
constructorTNewTimer.Create(AOwner: TComponent);
begin
inherited;
FStream := TMemoryStream.Create;
end;
destructorTNewTimer.Destroy;
begin
FStream.Free;
inherited;
end;
是不是同样很简单?
至于传递变量的具体过程,相信会用流的话也就不复杂了。不过需要注意的是,建议在流的开头先写入一段用于辨别的数据。比如说写入整数长度的123代表这个流储存的变量一共是三个整数。
另外,使用流的时候一定不要忘记在适当的时候设置TStream.Position为0。
2017年03月18日 08点03分
1
天道玄虚出品
注意:该教程适用于Delphi 10.1.2。
相关内容见该贴五楼的楼中楼
https://tieba.baidu.com/p/4934621524
第八章 类的研究
上一章,我们一起初步地看了看类的结构,这一章我们来稍稍深入一些地研究类。
8.0、还是从计时器开始
由于计时器实在是一个结构简单的东西,因此我们还是从计时器入手研究。
计时器位于FMX.Types单元下。
系统原本的计时器的功能是很单调的,不过是到了时间执行一个函数,而且这个函数能够传递的参数只是计时器本身。
先来大致看看我们希望计时器还有些什么功能。
首先应该是计次功能,也就是循环了一定次数之后暂停的功能。如果加入这个功能,那么应该在计时器计次结束的时候给一个事件。
其次应该是传递参数的功能,我们很多时候都会面临需要计时器传递其余参数的情形,以往这个时候常用的手段大概是进程休眠,且不说进程休眠的精度通常比不上计时器,再就是这进程休眠有比较多的缺陷,尤其是在单进程下的时候。
好了,先考虑这两个功能,随我一起来看看吧。不妨首先约定我们的计时器叫做TNewTimer。
8.1、计时器计次功能
我们希望达到的效果当然是每次计时器计次的时候都会改变计数的值。我们首先不妨约定计数的值代表还需要进行计时的次数,这个值应该设置为只读。
那么,很自然地,我们需要在private下写一个用于内部储存的变量,public下写一个只读属性。不妨命名为Conut,那么private下的变量为FCount,public下的属性就直接命名为Count。
好了,接下来要做的就是覆盖事件了。需要覆盖的事件当然是计时器到期事件,查看计时器源代码可以看到,计时器到期事件是DoOnTimer。
如何分辨出这个事件就是计时器到期事件呢?首先应该在protected下,其次,事件的名称应该与OnTimer有关,最后,事件通常都会声明为virtual,也就是虚方法。所谓虚方法,就是可以被子类方法覆盖的方法,需要覆盖的时候声明为override。通常事件的命名习惯都是事件变量名(例如OnTimer)去掉On,但是当这个名称已存在的时候,就在前面加上Do。
不信的话,你可以多看几个类的事件,就会发现我所言不差。
如何覆盖事件?首先,我们需要在子类下声明一个同名函数,之后再后面加上override。需要注意的是,只有虚方法才能够被参数声明完全相同的子类的方法覆盖。不论是声明为虚函数,还是参数声明完全相同都是不可少的。
那么,我们的声明应该写成:procedure DoOnTimer;override;
如果直接Ctrl+Shift+C,你应该会看到这样的声明:
procedureTNewTimer.DoOnTimer;
begin
inherited;
end;
你会看到一个没有讲过的保留字:inherited。从字面上理解,这个保留字意思是继承的,也就是继承父类方法。当子类方法与父类方法同名,而且参数声明相同的时候,直接使用inherited表示调用使用同一参数的父类方法,如果参数声明不同,那么需要在后面写上具体的函数调用,比如:
inherited Create(AOwner);
如果父类没有声明这个方法,或者声明在private下,那么就不可以调用。当然,子类可以降低父类方法的访问权限,要做到这一点,只需要改变同名方法的声明位置即可。不过需要注意的是,子类不可以提高父类方法的访问权限。
那么我们需要做的是,在触发计时的时候进行计数,如果计数的变量为零,那么不触发计时器,并且禁用计时器,之后应该响应计时器结束的事件,暂且将这个事件命名为TimerFinish。如果计数的变量不为零,那么进行计数并触发计时器。大致如下:
procedureTNewTimer.DoOnTimer;
begin
if FCount = 0 then
begin
Enabled := False;
TimerFinish;
end
else
begin
Dec(FCount);
inherited;
end;
end;
很简单,对吧。
那么,问题来了,如何自定义一个事件呢?
首先,我们需要想清楚这个事件的类型,也就是这个事件需要传递什么参数。计时器计时结束事件的参数只需要传递计时器本身就行,这个事件类型是TNotifyEvent。
那么,仿照原本的OnTimer事件的做法,我们需要一个FOnTimerFinish来储存这个事件,类型为TNotifyEvent,并设置相应的属性OnTimerFinish。然后,就是设置这个事件触发之后的具体操作,也就是TimerFinish的具体代码。
这个事件并不需要做什么其他的处理,唯一需要的只是向外提供一个事件的接口。那么,仿照DoOnTimer,代码应该是这样的:
procedureTNewTimer.TimerFinish;
begin
if Assigned(FOnTimerFinish) then
FOnTimerFinish(Self);
end;
这样,计时器的计次功能算是完成了一半了。
为什么说完成了一半呢?因为我们还没有提供设置计次的接口。因为我们不希望这个次数能够被随意更改,尤其是在计时器运行期间是不宜修改的。所以我们应该提供一个专有的方法,不妨设置为:
procedureTNewTimer.StartTimer(TimerEvent: TNotifyEvent; AInterval: Cardinal; LoopCount:Integer);
begin
if LoopCount < 1 then
begin
Enabled := False;
Exit;
end
else
FCount := LoopCount;
if (TimerEvent = nil) and (OnTimer = nil)then
Exit
else if TimerEvent <> nil then
OnTimer := TimerEvent;
Interval := AInterval;
Enabled := True;
end;
这个方法的功能是开始计时器。类似地,也可以提供一个停止计时器或者暂停计时器的方法,由于这些并不难,所以略去不表。
8.2、计时器传递变量
说完了计次功能,那么计时器如何传递变量呢?还记得之前讲过的流吗?你必须要承认,不论要传递什么东西,流都是一个可以考虑的选择。尤其是在不知道要传递什么的时候。
那么,我们增加一个流,不妨命名为FStream,类型是TStream或者TMemoryStream。由于我们在这个类里面增加了另一个类,所以我们需要覆盖创建和释放方法,与之前的覆盖类似,这一次我们需要的是在创建类的同时创建一个流,释放类之前释放这个流。
如下:
constructor Create(AOwner:TComponent); override;
destructor Destroy;override;
constructorTNewTimer.Create(AOwner: TComponent);
begin
inherited;
FStream := TMemoryStream.Create;
end;
destructorTNewTimer.Destroy;
begin
FStream.Free;
inherited;
end;
是不是同样很简单?
至于传递变量的具体过程,相信会用流的话也就不复杂了。不过需要注意的是,建议在流的开头先写入一段用于辨别的数据。比如说写入整数长度的123代表这个流储存的变量一共是三个整数。
另外,使用流的时候一定不要忘记在适当的时候设置TStream.Position为0。