在Lazarus下的Free Pascal编程教程——程序设计中的修改与版本控制

0.前言

我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。
了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:

学习编程从游戏开始——编程计划(目录) - lexyao - 博客园

我已经在下面这篇文章中介绍了使用LCL和FCL组件构建一个项目(pTetris)的过程,后续的使用Lazarus的文章中使用的例子都是以向这个项目添加新功能的方式表述的:

在Lazarus下的Free Pascal编程教程——用向导创建一个使用LCL和FCL组件的项目(pTetris) - lexyao - 博客园

在前面写的文章中我们已经构建了pTetris项目的框架,并逐步添加了一些功能,作为示例的应用程序俄罗斯方块游戏已经完成了游戏的全过程,并提供了丰富的操作方法。在这篇文章中我们将通过示例讲述使用程序程序修改过程中需要注意的问题。

俄罗斯方块游戏中操作方块是核心。在前面的示例中我们已经让作为示例的应用程序俄罗斯方块游戏已经完成了游戏的全过程,并通过配置数据提供了丰富的玩法,在这篇文章中我们将对程序中的几个方面的问题进行改进,关注修改过程中需要注意的几个问题。

在这篇文章里,我主要讲述以下几个方面的内容:

  1. 程序设计过程中的修改概述
  2. 简单的程序修改示例——游戏开始与结束的控制操作
  3. 增加新功能时的修改与版本控制——增加更丰富的玩法
  4. 信息显示的调整
  5. 为了代码共享而进行的修改
  6. 其他修改
  7. 结束语

1.程序设计过程中的修改概述

程序设计过程中不可避免地会遇到修改,根据修改的复杂程度,需要考虑的问题也会有所区别。可能遇到的情况通常会包含以下几种(不是全部):

  • 简单的修改:仅仅限于局部的修改,可能是以下情况之一
    • 仅限于某一个函数内部的修改,包括代码优化、功能增加、缺陷修复等情况
    • 涉及多处的代码修改,对修改范围以外的代码没有影响
  • 复杂的修改:会影响到相关代码或数据修改,可能是以下情况之一或多种情况并存
    • 函数参数个数或形式的修改,需要调用处同步修改。如果是多人共同编写的程序,需要考虑与其他人员或模块协调的问题
    • 涉及存盘文件的读取的修改,硬考虑与以前版本的兼容性
      • 增加配置数据的修改,需要考虑修改后对已经存盘的配置文件的影响,特别是新增加的选项的默认值的设定问题
      • 存盘文件的格式或文件中个别元素的修改

下面我们介绍对pTetris项目的几个修改,希望能起到抛砖引玉的效果。

2.简单的程序修改示例——游戏开始与结束的控制操作

2.1 修改的目的

在之前的程序中,点击“开始游戏”按钮会开始新的游戏,再次点击会中断正在进行的游戏,开始新的游戏。如果想结束游戏,只能等方块堆积到顶部。
在一段时间的试玩后,特别是调试程序时,游戏开始后要想结束就要等着。
现在希望对程序进行修改,在想结束的时候就能结束。

2.2 修改的实现

先看一看“开始游戏”按钮的现有代码:

procedure TfrmMain.btnStartClick(Sender: TObject);
begin
  Randomize;
  Gaming := True;
end;  

程序中使用属性Gaming作为标志。设置Gaming := True则游戏开始。
现在我们想增加游戏结束的操作,从上述代码来看,只要使用代码Gaming := False就可以实现这个目标。
实现这一目标的方法有两个:

  • 方法一:增加一个结束游戏的按钮,执行代码Gaming := False
  • 方法二:使用开始游戏按钮实现开始和结束游戏的操作,就像一个开关,点击一次打开,再点击一次关闭

我们采用第二种方法,修改后的代码如下:

procedure TfrmMain.btnStartClick(Sender: TObject);
begin
  Randomize;
  Gaming := not Gaming; //Gaming := True;
end; 

在属性Gaming的SetGaming函数中,根据设置Gaming的值决定执行GameBegin或GameEnd,我们对这两个函数中设置按钮标题的语句进行修改,使得按钮标题与点击按钮发生的操作一致:

procedure TfrmMain.GameBegin;
begin
  btnStart.Caption := '中止游戏';
  ......
end;

procedure TfrmMain.GameEnd;
begin
  btnStart.Caption := '开始游戏';
  ......
end;   

经过以上修改以后,我们可以在任何时候通过点击按钮主动结束正在进行的游戏了。

3.增加新功能时的修改与版本控制——增加更丰富的玩法

3.1 修改的目的

pTetris项目经过一段时间的试玩之后,发现运行初始速度比较慢的时候,游戏可以玩很长时间。如果是真正的高手在玩,可能结束的时间更长。出现这种情况会降低挑战性,为了避免这种情况,就需要对游戏时间有所限制。
要实现这样的目标,可以有两种方法:

  • 修改加速算法,使得游戏可以更快地加速,从而缩短一局游戏的时间。对于超级高手来说,这种做法有可能是无效的
  • 增加一个时间限制,到了这个时间限制后游戏就会结束。有了这种限制后游戏的目标是在规定的时间内尽可能获得更高的分数。这种方法适合所有人,其挑战性甚至超过传统的游戏规则

作为一次修改的尝试,在这里增加两种让游戏提前结束的限制:

  • 时间限制:规定游戏的时间,到达时间后结束游戏。这种限制可以避免游戏时间过长,增加一种挑战目标
  • 剩余方块限制:消除整行方块后剩余的方块数量少于指定数量时游戏结束。这种方式更像是一个陷阱,正在愉快地得分时突然意外结束了,哈哈,这会是怎样的心情?

3.2 修改的实现

3.2.1 修改需求分析

根据要达到的目标,现在我们来分析修改代码的需求:

  • 增加时间限制的需求:
    • 在配置表中增加选项,用来决定是否使用时间限制、时间限制的时长确定
    • 游戏持续时间的计算
    • 检查是否达到时间限制及决定结束游戏
  • 增加方块数量限制需求:
    • 在配置表中增加选项,用来决定是否使用方块数量限制、方块数量限制的方块数指定
    • 剩余方块数量的计算
    • 检查是否达到方块数量限制及决定结束游戏
  • 两种方式的修改的影响:
    • 配置文件的影响
    • 排行榜的影响

基于以上分析,我们看出需要修改的内容包括以下几个方面:

  • 在配置表中添加选项
  • 在代码中添加使用选项的代码
  • 处理修改影响的相关内容

在下面的叙述中,我们将完成这几方面的工作。

3.2.2 配置表界面的修改

3.2.2.1 配置表中添加组件

为了添加游戏持续时间限制和剩余方块限制,我们首先需要在配置表界面中添加设置这些选项的组件。我们可以通过复制粘贴相似的组件来简化添加组件的操作过程。
第一步、在界面编辑器的配置表的计分规则页面点击“高度加分”选中组件GroupBox1,右击鼠标选择“复制”。
第二步、在起始难度页面下部的空白处右击鼠标选择粘贴,在页面中添加了一组组件,在属性列表中修改新增的这三个组件的属性值:

  • TGroupBox组件,Name保持自动添加的值GroupBox5,Caption由“高度加分”改为“按持续时间限制结束游戏”
  • TCheckBox组件,Name改为ckGameKeptTime,Caption改为“游戏持续时间(5分钟)”
  • TTrackBar组件,Name改为trcGameKeptTime,Min、Max保持原来的1、9

第三步、在起始难度页面下部的空白处右击鼠标选择粘贴,在页面中添加了一组组件,在属性列表中修改新增的这三个组件的属性值:

  • TGroupBox组件,Name保持自动添加的值GroupBox6,Caption由“高度加分”改为“按剩余方块个数结束游戏”
  • TCheckBox组件,Name改为ckBoxHeap,Caption改为“消除整行后剩余方块少于(5个)”
  • TTrackBar组件,Name改为trcBoxHeap,Min、Max保持原来的1、9

如果粘贴组件后新增的组件不是在起始难度页面的最下边,可以用鼠标拖动TGroupBox组件到最下边,这样新增加的TGroupBox组件及其中包含的两个组件都会一起移动。
为了避免因为组件太多显得界面混乱,可以从其他页面复制TDividerBevel组件将页面分割为几个区域。
经过以上修改后,配置表的起始难度页面如下图:

3.2.2.2 给新增加的组件添加事件处理函数

 参照“高度加分”中的组件的事件处理函数,给起始难度页面新增加的两组组件中的TCheckBox和TTrackBar组件添加事件处理函数。代码如下:

procedure TfrmMain.ckGameKeptTimeChange(Sender: TObject);
begin
  trcGameKeptTime.Enabled := ckGameKeptTime.Checked;
  FConfig.ValueFrom(Sender as TComponent);
end;  

procedure TfrmMain.trcGameKeptTimeChange(Sender: TObject);
begin
  ckGameKeptTime.Caption:=Format('游戏持续时间(%d分钟)',[trcGameKeptTime.Position*5]);
  FConfig.ValueFrom(Sender as TComponent);
end;  

procedure TfrmMain.ckBoxHeapChange(Sender: TObject);
begin
  trcBoxHeap.Enabled := ckBoxHeap.Checked;
  FConfig.ValueFrom(Sender as TComponent);
end;  

procedure TfrmMain.trcBoxHeapChange(Sender: TObject);
begin
  ckBoxHeap.Caption:=Format('消除整行后剩余方块少于(%d个)',[trcBoxHeap.Position*5]);
  FConfig.ValueFrom(Sender as TComponent);
end;   
3.2.2.3 在代码中使用新增加的选项

使用新增加的条件限制确定游戏是否符合结束的条件,使用这些条件的代码应该放在移动一组方块结束之后,也就是放在函数OpenBoxAndCalcScore中,满足条件则设置Gaming := False来结束当前游戏。添加的代码如下:

procedure TfrmMain.OpenBoxAndCalcScore(bxs: cxBoxs);
var
  ......
begin
  ......
  //判断是否有条件结束游戏
  if FConfig.ConfigData.GetValue('ckGameKeptTime/Checked', True) and
    (GameKeptTime > FConfig.GameKeptTime) then
  begin
    Gaming := False;
  endelse if (iFullRows > 0) and FConfig.ConfigData.GetValue('ckBoxHeap/Checked', True) and
    (boxHeap.BoxCount < FConfig.BoxHeapCount) then
  begin
    Gaming := False;
  end;
end;   
3.2.2.4 配置数据管理类的修改

上面的代码中有两个新的函数:FConfig.GameKeptTime和FConfig.BoxHeapCount。添加这两个函数的原因是因为用到的选项值需要换算。按着我们的设想,配置文件中记录的值需要乘以一个扩大系数才是我们希望得到的数值,之所以这么做时为了兼顾排行榜的KeyName。这两个函数的代码如下:

function cxConfig.GameKeptTime: TDateTime;
begin  
  //由于需要换算,添加单独使用的函数完成数据换算
  Result := (ConfigData.GetValue('trcGameKeptTime/Position', 1) * 5) / (24 * 60);
end;  

function cxConfig.BoxHeapCount: integer;
begin  
  //由于需要换算,添加单独使用的函数完成数据换算
  Result := ConfigData.GetValue('trcBoxHeap/Position', 2) * 5;
end;    

 

3.2.2.5 添加配置选项需要注意的问题

 

原来的配置选项在第一次运行pTetris应用程序的时候按设计时的选项值保存到了配置文件中,而新增加的选项在配置文件中不存在,这种情况下运行新版本的pTetris应用程序的时候从配置文件中恢复上次保存的配置数据时,新增加选项应该是以默认值的形式出现。
那么,如何设置默认值呢?或者说确定默认值的时候需要注意什么呢?我的观点是一致性。下面以我们新增加的两组选项为例来说明这个问题。
pTetris应用程序开始运行时恢复配置表组件选项值时使用cxConfig.ValueTo函数实现。在cxConfig.ValueTo中,

  • TCheckBox组件的默认值是true
  • TTrackBar组件的默认值有两种情况:
    • 添加到名称列表中的组件默认值是max
    • 没有在列表中的其他组件默认值是min

在OpenBoxAndCalcScore函数中使用这些选项时使用的默认值必须与cxConfig.ValueTo中得到的默认值一致,否则将会出现配置表中显示的选项值与OpenBoxAndCalcScore中得到的结果不一致的问题。
这个例子是一个简单的选项,如果是比较复杂的情况,无法用这种方式得到正确的结果,就需要用到版本控制的方法。
所谓的版本控制,就是在配置文件中记录保存数据时应用程序的版本。在读取配置数据时检查文件中记录的版本号与当前应用程序版本是否一致,如果不一致,需要通过代码做适当的处理,比如补充新增选项的值到配置文件中。

3.2.3 排行榜的修改

在排行榜文件中我们是使用KeyName识别不同配置对应的排行榜表的。新增了两组配置选项后,KeyName函数中需要增加这两个选项的的值。新增加的代码如下:

class function cxRankings.KeyName: string;
var
  stb: TStringBuilder;
  iii: integer;
  bbb, bbc: boolean;
begin
  Result := '';
  stb := TStringBuilder.Create;
  try
    ......
    //起始难度       
    ......
    bbb := FConfig.ConfigData.GetValue('ckGameKeptTime/Checked', True);
    iii := FConfig.ConfigData.GetValue('trcGameKeptTime/Position', 1);
    if bbb then
      stb.Append(iii)
    else
      stb.Append(0);
    bbb := FConfig.ConfigData.GetValue('ckBoxHeap/Checked', True);
    iii := FConfig.ConfigData.GetValue('trcBoxHeap/Position', 2);
    if bbb then
      stb.Append(iii)
    else
      stb.Append(0);
    Result := stb.toString;
  finally
    stb.Free;
  end;
end;     

新增了两组配置选项后,KeyName函数的返回值要添加这两组选项,得到的名称与原来版本中的名称不一致,这样在新版本的pTetris应用程序中将找不到旧版本的应用程序记录的排行榜数据。
对于重要的数据,需要通过版本控制识别数据文件的版本做出相应的修改。
比如识别到旧的文件时,使用旧版本的KeyName读取数据,并将文件中的旧的KeyName替换为新的KeyName。
对于不重要的数据,也可以直接舍弃原来的文件,只要不觉得有损失就可以。

4.游戏的当前统计信息显示的调整

4.1 ViewScores函数的在代码中的位置

当前游戏的信息应该有很多是用户想要看到的,而排行榜中只收录了很少的一部分。怎么才能看到更多的信息呢?
大家应该还记得我们在游戏界面中部区域有一个组件pnInfo,在《在Lazarus下的Free Pascal编程教程——应用程序配置数据的使用 - lexyao - 博客园》一文中我们使用函数ViewScores在pnInfo组件上显示当前游戏得分。
在以前的代码中,调用ViewScores函数的位置放在了需要现在ViewScores函数中的所有属性的设置函数中,比如SetBoxCount、SetBoxScores、SetBoxFullRows等。
这样做的优点是无论在何处设置这些属性的值,都会导致显示信息的及时刷新,缺点是刷新的次数会增加。
在编写的代码完善后,发现这些信息调用的位置集中在几个函数中,只要在这几个函数的末尾添加函数ViewScores的调用,信息显示都能够及时刷新。这样做的优点是可以减少函数ViewScores的执行次数,缺点是可能会遗漏个别数据的刷新。
下面我们重新布置ViewScores的调用位置。
首先,游戏开始时,在完成了计时、计数、计分等初始化工作后添加GameBegin函数的调用:

procedure TfrmMain.GameBegin;
begin
  ......
  { #todo : 计时计分归零 }
  ......
  ViewScores;
end;    

然后是游戏持续过程中在计时器事件处理函数中。可以放在DoTimerVx中,这样做需要在各个有效版本的DoTimer函数中都要添加。还可以放在计时器事件处理函数中的DoTimerVx之后,云运行效果是一样的。

procedure TfrmMain.DoTimerV5;
var
  bxs: cxBoxs;
begin
  ......
  ViewScores;
end; 

删除仅在GameBegin和DoTimerVx中更新数据的属性的函数中的ViewScores调用。
按键计数属性MoveKey不在DoTimerVx中使用,所以需要保留SetMoveKey中的ViewScores调用。 

4.2 ViewScores函数的显示的内容

现在我们可以把更多的信息添加到ViewScores函数中,只要你想看到的信息,都可以在这个函数中添加并显示出来。需要显示的信息可能涉及到但不限于以下几个方面:

  • 当前游戏的成绩,比如得分、方块数
  • 当前游戏结束条件有关的数据,比如持续时间、剩余方块数
  • 当前游戏的运行参数,比如速度、加速度
  • 有利于改进游戏体验的数据,比如当前一组方块的得分及与得分相关的数据
  • 可能用于改进游戏算法的数据,比如移动一组方块后加速算法计算的结果、计分算法计算的结果
  • 其他任何认为有必要显示的数据

要显示的数据可以是程序员设计程序时设定的,也可以在配置表中增加一页显示控制选项,让用户选择需要显示的信息。当然,这是一种思路,我在程序中添加了以下方面的代码:

procedure TfrmMain.ViewScores;
var
  sb: TStringBuilder;
begin
  sb := TStringBuilder.Create;
  sb.AppendLine(Format('移动方块%d组', [BoxCount]));
  sb.AppendLine(Format('消除满行%d', [BoxFullRows]));
  sb.AppendLine(Format('累计得分%d', [BoxScores]));
  sb.AppendLine;                                   
  sb.AppendLine(Format('本次得分%d', [BoxScore]));
  sb.AppendLine(Format('当前速度%f', [TimerInterval]));
  sb.AppendLine(Format('调整时间%f', [TimerAdjust]));
  sb.AppendLine(Format('跳动次数%d', [MoveTimer]));
  sb.AppendLine(Format('按键次数%d', [MoveKey]));
  sb.AppendLine;
  sb.AppendLine(Format('堆积方块%d', [boxHeap.BoxCount]));
  sb.AppendLine(Format('游戏时间%s', [TimeToStr(GameKeptTime)]));
  pnInfo.Caption := sb.toString;
  sb.Free;
end;   

5.代码共享而进行的修改

在程序设计中经常会遇到很多地方用到向同样的代码的情况,这时我们可以把这些相同的代码提取出来作为一个函数,在需要的地方调用这个函数就行了。通过这样的方式可以节省大量的代码。
在pTetris项目中我们就遇到了这样的情况:配置表的组件在改变选择的值后会将新的值保存到配置文件中,这项工作是在组件的事件处理函数中实现的。
这类事件处理函数分为两种情况:

  1. 仅需要保存组件新的选项值
  2. 除了保存组件新的选项值,还需要执行与选项值改变有关的操作

下面提供这两种情况代码共享的的解决方案。需要说明的是,在这里提供的方案仅仅是为了提供一种思路,并不是说必须要这样做,也不是说这样做是最好的解决方案。

5.1 第一种情况的事件处理函数

对于第一种情况,事件处理函数中只有一行代码:

  FConfig.ValueFrom(Sender as TComponent);    

在以前的代码代码中,我们为配置表的每一个组件都编写了一个事件处理函数,而属于第一种情况的组件的事件处理函数虽然名称不同,但内容都是一样的。对于这种情况,我们可以只编写一个这样的函数,所有属于第一种情况的组件都使用这个函数,这样我们可以节省代码量,在添加新的组件时只需要引用这个函数就行了,不需要再编写新的函数。
我们可以使用任何一个属于第一种情况的组件的事件处理函数作为我们要共享的函数,但是为了程序的可读性,避免产生误解,还是要给这个函数取一个新的名字:

procedure TfrmMain.funDefaultControlChange(Sender: TObject);
begin
  FConfig.ValueFrom(Sender as TComponent);
end; 

有了这个函数之后,我们就可以把所有属于第一种情况的组件响应数值改变的事件处理函数改为使用函数funDefaultControlChange,而把原来的重复的事件处理函数删除。
在我们已经完成的代码中可以使用函数funDefaultControlChange代替的组件的事件处理函数包括:PageControl1Change、grpBoxStyleSelectionChanged、grpTimeCalcSelectionChanged、ckPenetrateChange 

5.2 第二种情况的TCheckBox和TTrackBar组件事件处理函数

在第二种情况中,有在一个TGroupBox组件中包含一个TCheckBox和一个TTrackBar的组合,其中:

  • 在TCheckBox的OnChange事件中根据TCheckBox的Cheched属性决定TTrackBar组件是否可用
  • 在TTrackBar的OnChange事件中将TTrackBar的Position属性值显示在TCheckBox组件的Caption属性中

对于这种情况的事件处理函数,可以分别为TCheckBox和TTrackBar组件编写一个通用的事件处理函数。

5.2.1 第二种情况的TCheckBox组件事件处理函数

这种情况的TCheckBox组件的事件处理函数中包含两行代码,例如:

procedure TfrmMain.ckScoreDesRowsChange(Sender: TObject);
begin
  trcScoreDesRows.Enabled := ckScoreDesRows.Checked;
  FConfig.ValueFrom(Sender as TComponent);
end;  

在这个例子中,函数的参数Sender是TCheckBox组件ckScoreDesRows,trcScoreDesRows是与ckScoreDesRows在同一个TGroupBox组件内的TTrackBar组件。
在程序中有容器组件不是TGroupBox而是TPanel组件的也适用于这种情况。
在类似这样的函数中,第二行是相同的,第一行的两个组件的名称是不同的。要编制通用函数代替这个函数需要将第一行代码改成通用的代码,要达到这样的目标需要做以下两方面的工作:

  • 用(Sender as TCheckBox).Checked代替ckScoreDesRows.Checked
  • 根据Sender找到与Sender在同一个TGroupBox下的TTrackBar组件来代替trcScoreDesRows

根据这样的要求编写出的通用函数代码如下:

procedure TfrmMain.funDefaultCheckChange(Sender: TObject);
var
  grp:TWinControl;
  chk:TCheckBox;
  trc:TTrackBar;
  ctl:TControl;
  i: Integer;
begin
  //确定组件
  chk:=Sender as TCheckBox;
  grp:=chk.Parent as TWinControl;
  for i := 0 to grp.ControlCount-1 do
  begin
    ctl:=grp.Controls[i];
    if ctl is TTrackBar then
    begin
      trc:=ctl  as TTrackBar;
      Break;
    end;
  end;
  //设置、保存组件属性
  trc.Enabled := chk.Checked;
  FConfig.ValueFrom(Sender as TComponent);
end; 

有了这个函数之后,我们就可以把所有属于第二种情况的TCheckBox组件响应数值改变的事件处理函数改为使用函数funDefaultCheckChange,而把原来的重复的事件处理函数删除。
在我们已经完成的代码中可以使用函数funDefaultCheckChange代替的组件的事件处理函数包括:ckBoxHeapChange、ckGameKeptTimeChange、ckScoreDesHeightChange、ckScoreDesRowsChange、ckScoreHeightChange

5.2.2 第二种情况的TTrackBar组件事件处理函数

这种情况的TTrackBar组件的事件处理函数中包含两行代码,例如:

procedure TfrmMain.trcScoreHeightChange(Sender: TObject);
begin
  ckScoreHeight.Caption:=Format('启用行高加分(行高×%d倍)',[trcScoreHeight.Position]);
  FConfig.ValueFrom(Sender as TComponent);
end; 

这个例子TTrackBar的OnChange事件中将TTrackBar的Position属性值显示在TCheckBox组件的Caption属性中。在程序中也有将TTrackBar的Position属性值显示在TLabel组件的Caption属性中的,也适用于这种情况。

在这个例子中,函数的参数Sender是TTrackBar组件trcScoreHeight,ckScoreHeight是与trcScoreHeight在同一个TGroupBox组件内的TCheckBox组件。
在类似这样的函数中,第二行是相同的,第一行的两个组件的名称是不同的。要编制通用函数代替这个函数需要将第一行代码改成通用的代码,要达到这样的目标需要做以下三方面的工作:

  • 用(Sender as TTrackBar).Position代替trcScoreHeight.Position
  • 根据Sender找到与Sender在同一个TGroupBox下的TCheckBox或TLabel组件来代替ckScoreHeight
  • 第一个语句中有字符串“启用行高加分(行高×%d倍)”需要找一个适当的地方保存,我们可以保存在TCheckBox或TLabel组件的Hint属性中

这三方面的工作中,第三项工作需要在设计时在属性浏览器中将相应的字符串添加到组件的Hint属性中。前两项工作在编写的通用事件处理函数中实现。
根据这样的要求编写出的通用函数代码如下:

有了这个函数之后,我们就可以把所有属于第二种情况的TTrackBar组件响应数值改变的事件处理函数改为使用函数funDefaultTrackBarChange,而把原来的重复的事件处理函数删除。
在我们已经完成的代码中可以使用函数funDefaultTrackBarChange代替的组件的事件处理函数包括:trcNextNumberChange、trcScoreBaseChange、trcScoreDesBaseChange、trcScoreDesHeightChange、trcScoreDesRowsChange、trcScoreHeightChange、trcSpeedBaseChange、trcSpeedKeyChange、trcSpeedTimerChange

经过以上修改实现了代码共享之后,程序中使用语句FConfig.ValueFrom(Sender as TComponent)的事件处理函数从30个减少到11个。今后再添加类似的组件时,可以不用编写新的事件处理函数,直接引用这些共享代码的函数就行了。

6.其他修改

pTetris项目中还有其他修改,由于理由与前面讲相似或相同,所以不再作为例子列出。为了代码的一致性,还是要把这些修改列出来,便于今后编写其他相关的代码。

6.1 cxBoxs.NewBox函数及其相关的修改

在cxBoxs.NewBox中有一行奇怪的代码,为此在代码中做了说明。现在消除这个奇怪的现象。
cxBoxs.NewBox函数原来的代码如下:

function cxBoxs.NewBox: cxBox;
begin
  //关于cxBox.Create参数的选择:
  //由于cxBoxs释放时cxBox是要还要保留,所以不能使用self
  //由于cxBoxs.Owner是cxBoxQueue,而cxBoxQueue将用来作为保存cxBoxs的队列,也不能用
  //cxBoxs.Owner.Owner是其他组件,可以使用
  Result := cxBox.Create(Owner.Owner);
end;  

上述代码中cxBox.Create的参数没有使用Self而使用了Owner.Owner,这是基于当时的思路设计的。代码中的说明就是当时的思路。
在后来的代码设计中,增加了cxDustbin类,并在移动方块的操作完成后使用cxBoxHeap.BoxsOpen打开移动的方块盒子时,将盒子中的方块转移到cxDustbin中,方块的Owner转换为cxDustbin,这样在使用cxBoxs时不会对曾经在cxBoxs中的方块cxBox有影响,那么,cxBoxs.NewBox函数中cxBox.Create的参数不能使用Self的理由就不存在了。
现在我们就把cxBoxs.NewBox函数中cxBox.Create的参数改为Self,代码如下:

function cxBoxs.NewBox: cxBox;
begin
  Result := cxBox.Create(Self);
end;   

经过以上修改后,我们就获得了另外一种访问cxBoxs中包含的cxBox的途径:

  • 使用cxBoxs.ComponentCount获得cxBoxs中包含的cxBox数量
  • 使用cxBoxs.Components[Index] as cxBox获得cxBoxs中第Index个cxBox对象

这种新增加的途径是从组件TComponent继承来的,在只需要获得cxBox对象而不需要考虑它的坐标位置时,使用这个新增的途径要比遍历cxBoxs.mBoxs方便快捷。
在下一篇文章给方块添加颜色时,我们将用到这种新增加的途径。

7.结束语

任何程序都可能存在某些在调试时没有发现的缺陷,也可能存在设计时考虑不周全的代码,还可能存在最初考虑到的算法不够完善,还可能在经过一段时间的使用后有了新的想法……
总之,有许多因素让我们觉得已经完成的程序设计需要改进。为了程序改进,我们需要考虑以下因素:

  • 程序设计时尽可能采用结构化、模块化的代码,将实现某一功能的代码尽可能局限于较小的范围内,这样做可以在将来修改时不会因为小的修改而对代码做大的调整。
  • 对程序进行修改时,要考虑到对应用程序以前保存的数据兼容的问题,要保持正确读取早期版本的程序保存的数据的能力。
posted @ 2025-04-09 11:13  lexyao  阅读(130)  评论(0)    收藏  举报