在Lazarus下的Free Pascal编程教程——使用画笔TPen在组件表面绘制图形
0.前言
我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。
了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:
学习编程从游戏开始——编程计划(目录) - lexyao - 博客园
我已经在下面这篇文章中介绍了使用LCL和FCL组件构建一个项目(pTetris)的过程,后续的使用Lazarus的文章中使用的例子都是以向这个项目添加新功能的方式表述的:
在Lazarus下的Free Pascal编程教程——用向导创建一个使用LCL和FCL组件的项目(pTetris) - lexyao - 博客园
在前面写的文章中我们已经构建了pTetris项目的框架,并逐步添加了一些功能,作为示例的应用程序俄罗斯方块游戏已经可以作为一个完整的游戏程序使用了。在这篇文章中我们将通过改变方块表面颜色和图案来使俄罗斯方块游戏变得“多彩”。
俄罗斯方块游戏中操作方块是核心。在前面的示例中我们已经让作为示例的应用程序俄罗斯方块游戏已经具有了游戏的全部功能,并提供了超出传统游戏的玩法,在这篇文章中我们将通过示例讲述在程序界面中使用画笔TPen绘制图形的方法。
在这篇文章里,我主要讲述以下几个方面的内容:
- 程序设计的界面显示中画笔TPen的使用概述
- 在pTetris项目中实现要删除的方块闪烁的策划
- 在pTetris项目中使用画笔TPen绘制图形的方法实现方块闪烁效果
- 结束语
1.程序设计的界面显示中画笔TPen的使用概述
任何操作系统下的图形界面设计中都用到两个工具:画笔Pen和画刷Brush,画刷和画笔都是在一个代表设备的DC上操作的。画笔用来写字、划线,画刷用来填充背景颜色或图案。
在Lazarus中,画笔对应的组件是TPen,画刷对应的组件是TBrush。
在Lazarus中,为所有可视的组件提供了一个叫做画布的TCanvas类成员,从而显示了设备无关性的图形输出。TPen和TBrush都作为画布TCanvas类的成员。
在这一篇文章中,我们将要讲述的就是画刷TPen的使用。
在Lazarus的帮助文档中有TPen的介绍:
在网上搜索了很久,没有找到与Lazarus的TPen类有关的感兴趣的文章,如果你想了解关于TPen类的信息,可以搜索Delphi的TPen有关的介绍,这一方面的示例很多。
外面在使用TPen时常用的属性包括以下几个:
- Color:指定用于绘制图形的颜色(类型 TColor)
- Style :TFPPenStyle类型的枚举,定义用于绘制图形的线形,包括实现、虚线、点画线等。TFPPenStyle = (psSolid, psDash, psDot, psDashDot, psDashDotDot, psinsideFrame, psPattern,psClear)
- Mode : TFPPenMode类型的枚举,定义用于绘制图形的模式。TFPPenMode = (pmBlack, pmWhite, pmNop, pmNot, pmCopy, pmNotCopy,pmMergePenNot, pmMaskPenNot, pmMergeNotPen, pmMaskNotPen, pmMerge,pmNotMerge, pmMask, pmNotMask, pmXor, pmNotXor);
- EndCap : TFPPenEndCap类型的枚举,定义用于绘制线段两端的样式。TFPPenEndCap = (pecRound,pecSquare,pecFlat);
- JoinStyle : TFPPenJoinStyle,定义用于绘制两条相交的线的连接样式。TFPPenJoinStyle = (pjsRound,pjsBevel,pjsMiter);
- Width : 使用 Pen 绘制的线条的宽度。
在Lazarus中编写程序代码时,我们需要设置TPen的属性,然后使用TCanvas提供的函数绘制线条或文字。
2.在pTetris项目中实现要删除的方块闪烁的策划
2.1 方块消除前闪烁效果的实现方法
再使用传统的应用程序、网页或者游戏机玩俄罗斯方块游戏的时候,当堆积区域出现满行时,满行的方块会闪烁,然后消失。
在使用pTetris玩俄罗斯方块游戏的时候我们会发现,当堆积区域出现满行时,满行的方块直接消失了。现在我们要做的是给pTetris项目添加方块消失前闪烁的效果。
要实现方块的显示效果,只要让方块的外观在短时间内发生多次变化就行了。要实现这种外观变化效果,我们可以有两种思路:
- 方块表面的颜色变化成与以前不同的颜色
- 方块表面的图形变化成与以前不同的图形
要实现这个目标,我们有两种途径:
- 使用画刷TBrush改变画布TCanvas的背景颜色或图案
- 使用画笔TPen改变画布TCanvas的前景颜色或图案
在前一篇文章中已经演示了画刷TBrush的使用方法,在这篇文章里,我们将使用画笔TPen实现同样的效果。
2.2 在配置表中添加选项
2.1.1 在界面中添加组件
我们要达到的目标是用户可以通过选项决定满行方块消除前闪烁的时间、次数和闪烁方式。为此我们需要在程序界面的配置表中添加相应的组件。
在界面编辑器中给配置表的图案选择页面添加以下组件:
- TDividerBevel组件,设置属性Caption为“整行消除前方块闪烁”
- TGroupBox组件,Name属性保持默认值GroupBox7,设置属性Caption为“闪烁频次”,Align为alTop,AtuoSize为true
在GroupBox7添加以下四个组件,并通过设置它们的Anchors属性或坐标值实现从左到右依次排列- TSpinEdit组件,设置属性Name为seFlashTime,MinValue为0,MaxValue为900,Increment为20,Value为200,Tag为200
- TLabel组件,Name属性保持默认值Label5,设置属性Caption为“毫秒变色”
- TSpinEdit组件,设置属性Name为seFlashTimes,MinValue为1,MaxValue为9,Increment为1,Value为3,Tag为3
- TLabel组件,Name属性保持默认值Label6,设置属性Caption为“次”
- TRadioGroup组件,设置属性Name为grpFlashMode,Caption为“闪烁方式”,Columns属性为3,Items为“同时变色,堆积顺序,从左到右”(分三行)
界面效果图如下:
2.1.2 添加响应界面选项的代码
按着以前的惯例,在添加了新的组件后要添加组件的事件处理函数,使得程序能够在运行时用户改变选项值后能够将新的选项值保存到配置文件,再次运行时能够恢复以前保存的选项值。
由于选项改变后的操作只是将新的选项值保存到配置文件中,我们可以使用我们在《在Lazarus下的Free Pascal编程教程——程序设计中的修改与版本控制 - lexyao - 博客园》一文中讲述的funDefaultControlChange函数作为新增加组件的事件处理函数。具体的操作步骤是:
- 在界面设计器的窗体frmMain中同时选中seFlashTime、seFlashTimes组件,在属性浏览器的事件页面设置OnChange事件处理函数为funDefaultControlChange
- 在界面设计器的窗体frmMain中选中grpFlashMode组件,在属性浏览器的事件页面设置OnSelectionChanged事件处理函数为funDefaultControlChange
为了阅读方便,在这里展示函数funDefaultControlChange的代码如下:
procedure TfrmMain.funDefaultControlChange(Sender: TObject); begin FConfig.ValueFrom(Sender as TComponent); end;
由于seFlashTime、seFlashTimes是在pTetris项目中初次使用的组件,需要在ValueFrom、ValueTo函数中添加识别这两个组件的代码。
procedure cxConfig.ValueFrom(ACtrl: TComponent); ...... begin ...... //为了简化代码,按组件类型保存组件属性值作为配置数据 ...... else if ACtrl is TSpinEdit then ConfigData.SetValue(pth + '/Value', (ACtrl as TSpinEdit).Value); end; procedure cxConfig.ValueTo(ACtrl: TComponent); ...... begin ...... //为了简化代码,按组件类型获取组件属性值作为配置数据 ...... else if ACtrl is TSpinEdit then (ACtrl as TSpinEdit).Value := ConfigData.GetValue(pth + '/Value', (ACtrl as TSpinEdit).Tag); end;
由于seFlashTime、seFlashTimes组件的Value属性的默认值是介于MinValue和MaxValue之间的一个数字,且二者数值不同,为了简化代码,在ValueFrom函数中使用了一个小的技巧,借用设计时在组件的Tag属性中设置的数值来作为默认值,从而避免为两个组件分别编写代码。
3.在pTetris项目中使用画笔TPen绘制图形的方法实现方块闪烁效果
3.1 实现方块闪烁效果的代码框架构建
在我们已经完成的代码中,检查满行、释放满行的cxDustbin类示例及其中的cxBox类实例是在cxBoxHeap.ClearFullRows中完成的。
procedure cxBoxHeap.ClearFullRows(var iRows, iRowHeight: Integer); var i: integer; rw: cxDustbin; begin //销毁满行 iRows := 0; iRowHeight:= 0; for i := RowCount - 1 downto 0 do begin if RowFull(i) then begin rw := Rows(i); rw.Free; Inc(iRows); Inc(iRowHeight, i); end; end; //如果有销毁的行,则需要重新显示堆积的方块(向下移动) if iRows > 0 then begin MoveBox; end; end;
从这个函数代码中可以看出,如果只有一行需要删除,只需要在直接添加rw.Free语句之前添加闪烁的代码就行了,但这样有一个明显的缺点:当有多行需要删除时,无法同时闪烁,闪烁的时间会随着消除行的行数增加而成倍增加,这显然是不合适的。
我们应该怎么做呢?
要消除的方块是保存在cxDustbin类中的。要实现方块销毁前闪烁,需要做以下几项工作:
- 检查所有堆积的方块,将满行的cxDustbin加入一个满行列表
- 让满行的方块闪烁
- 释放满行的cxDustbin类示例及其中的cxBox类实例
我们可以在cxDustbin类中添加成员函数实现方块消除前的闪烁效果,但一次消除多个行时需要协调多个cxDustbin中方块的闪烁,这样一来cxDustbin就不合适了。
cxDustbin是由cxBoxHeap管理的,在cxBoxHeap中管理方块的闪烁比cxDustbin更合适。除此之外,我们还可以单独建立一个类,将管理要消除的行的所有工作都封装在这个类中,这样做更符合面向对象的程序设计的思路,这也是最初我们策划的cxBoxDestroy类要做的工作。
我们最初策划的类只有cxBoxDestroy类没有实现了,现在我们就来完成cxBoxDestroy类的代码。
给cxBoxDestroy类添加成员函数,cxBoxDestroy类定义如下:
cxBoxDestroy = class(TLCLComponent) private protected public procedure Add(ACom: TComponent); procedure Flashing; procedure FlashSync; procedure FlashOrder; procedure FlashMarguee; function Count: integer; end;
让cxBoxDestroy从TLCLComponent类继承是为了借用已经存在的成熟代码,这个没有需要解释的。为了可读性,定义了一个计数函数,当然如果不喜欢这样做也可以直接使用原有的计数函数。
function cxBoxDestroy.Count: integer; begin Result := ComponentCount; end;
定义Add函数将满行cxDustbin对象添加到cxBoxDestroy类中,同时将cxDustbin从cxBoxHeap中剔除,这样执行cxBoxDestroy.Free时就释放其中的cxDustbin及cxBox对象了,不需要再逐个执行Free。
procedure cxBoxDestroy.Add(ACom: TComponent); begin ACom.Owner.RemoveComponent(ACom); Self.InsertComponent(ACom); end;
定义函数Flashing来实现要销毁的行的闪烁:
procedure cxBoxDestroy.Flashing; begin case FConfig.ConfigData.GetValue('grpFlashMode/ItemIndex', 0) of 0: FlashSync; 1: FlashOrder; 2: FlashMarguee; end; end;
从代码中可以看出,Flashing使用配置数据选择了三个函数,这三个函数对应于我们再配置表中闪烁方式的三个选项。
3.2 使用画笔TPen绘制图形的方法实现方块闪烁效果的实现
我们使用画笔实现方块闪烁的思路很简单:就是用画笔画一条宽度足够覆盖方块表面的线,从而改变方块表面的颜色。短时间内多次变换颜色,从而产生闪烁的效果。为了控制方块闪烁的速度,我们使用Sleep来控制两次闪烁之间的时间间隔。
3.2.1 同时闪烁
函数FlashSync实现同步闪烁。也就是先将所有的方块表面颜色改变,然后使用Sleep保持一段时间的停顿之后再执行下一次改变颜色的操作。
procedure cxBoxDestroy.FlashSync; var cmp, box: TComponent; clr: TColor; i, tms, tm, slp: integer; begin tms := FConfig.ConfigData.GetValue('seFlashTimes/Value', 3); tm := FConfig.ConfigData.GetValue('seFlashTime/Value', 200); slp := tm div tms; for i := 1 to tms do begin clr := Random($FFFFFF); for cmp in Self do begin for box in cmp do begin with box as cxBox do begin Canvas.Pen.Color := clr; Canvas.Pen.Width := Width; Canvas.Line(0, Height div 2, Width, Height div 2); end; end; end; Sleep(slp); end; end;
3.2.2 堆积顺序
FlashOrder函数按方块进入cxDustbin的先后顺序逐个改变颜色,每改变一个方块的颜色后使用Sleep保持一段时间的停顿,然后再执行改变下一个方块颜色的操作。这样能看到方块变色的过程,但是看上去有点乱。
procedure cxBoxDestroy.FlashOrder; var cmp,box: TComponent; clr: TColor; i, tms, tm, slp, cls: integer; begin tms := FConfig.ConfigData.GetValue('seFlashTimes/Value', 3); tm := FConfig.ConfigData.GetValue('seFlashTime/Value', 200); slp := tm div tms div Count; for i := 1 to tms do begin clr := Random($FFFFFF); for cmp in Self do begin for box in cmp do begin if box <> nil then begin with box as cxBox do begin cls:=(Parent as cxGrid).Cols; Canvas.Pen.Color := clr; Canvas.Pen.Width := Width; Canvas.Line(0, Height div 2, Width, Height div 2); end; end; Sleep(slp div cls); end; end; end; end;
3.2.3 从左到右
FlashMarguee函数按方块在堆积区域从左到右的排列顺序逐个改变颜色,每改变一个方块的颜色后使用Sleep保持一段时间的停顿,然后再执行改变下一个方块颜色的操作。这样能看到方块变色的过程,从左到右逐个变色,体现为走马灯的效果。
procedure cxBoxDestroy.FlashMarguee; var cmp: TComponent; clr: TColor; box: cxBox; dst: cxDustbin; i, j, tms, tm, slp, cls: integer; begin tms := FConfig.ConfigData.GetValue('seFlashTimes/Value', 3); tm := FConfig.ConfigData.GetValue('seFlashTime/Value', 200); slp := tm div (tms * Count); for i := 1 to tms do begin clr := Random($FFFFFF); for cmp in Self do begin dst := cmp as cxDustbin; for j := 0 to dst.BoxCount - 1 do begin box := dst.Boxs[j]; if box <> nil then begin with box as cxBox do begin cls:=(Parent as cxGrid).Cols; Canvas.Pen.Color := clr; Canvas.Pen.Width := Width; Canvas.Line(0, Height div 2, Width, Height div 2); end; end; Sleep(slp div cls); end; end; end; end;
3.2.4 三种闪烁方式的对比
FlashSync使用Sleep的次数少,时间计算相对准确,闪烁效果整齐。
FlashOrder使用Sleep的次数多,受操作系统线程管理的影响,实际使用的时间要比设置的时间长一些,闪烁有些杂乱。
FlashMarguee使用Sleep的次数多,受操作系统线程管理的影响,实际使用的时间要比设置的时间长一些,闪烁为走马灯式。
3.3 实现方块闪烁效果的代码引用
前面提到消除满行的操作是在cxBoxHeap.ClearFullRows函数中实现的。现在我们已经完成了cxBoxDestroy的全部代码,下一步的工作就是改写cxBoxHeap.ClearFullRows函数,在其中引用cxBoxDestroy类实现满行方块消除前的闪烁效果。
下面就是改写后的cxBoxHeap.ClearFullRows函数的代码,行号标记为红色的行为新增加或改动的代码,其中的改动可以与3.1中cxBoxHeap.ClearFullRows函数原来的代码比较后找到其中的不同。
1 procedure cxBoxHeap.ClearFullRows(var iRows, iRowHeight: Integer); 2 var 3 i: integer; 4 rw: cxDustbin; 5 bd:cxBoxDestroy; 6 begin 7 iRows := 0; 8 iRowHeight := 0; 9 bd := cxBoxDestroy.Create(nil); 10 try 11 //统计满行(将满行加入bd中) 12 for i := RowCount - 1 downto 0 do 13 begin 14 if RowFull(i) then 15 begin 16 rw := Rows(i); 17 bd.Add(rw); 18 Inc(iRowHeight, i); 19 end; 20 end; 21 iRows := bd.Count; 22 //闪烁满行 23 if iRows > 0 then 24 begin 25 bd.Flashing; 26 end; 27 finally 28 //销毁满行(销毁bd时会销毁其中包含的组件) 29 bd.Free; 30 end; 31 //如果有销毁的行,则需要重新显示堆积的方块(向下移动) 32 if iRows > 0 then 33 begin 34 MoveBox; 35 end; 36 end;
4.结束语
在这篇文章中通过示例讲述了在Lazarus中使用画笔TPen的划线方法。从TPen的属性的定义可以看出它具有非常丰富的功能,灵活运用可以获得多种不同的效果。
关于俄罗斯方块中满行方块闪烁的实现方法,使用TPen画线是一种实现方法,如果有兴趣你也可以使用绘制图像实现,也是使用TPen的特性使用画面反转实现。

浙公网安备 33010602011771号