在Lazarus下的Free Pascal编程教程——向窗体动态添加组件

0.前言

我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。

了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:

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

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

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

在前面写的文章中我们已经构建了pTetris项目的框架,并逐步添加了一些功能。在前面添加到应用程序窗口中的组件都是在Lazarus的窗体设计器中添加的,也就是在设计时添加的。在这一篇文章里,我们将介绍通过编写代码向窗体添加组件的方法,也就是在运行时添加组件。

俄罗斯方块游戏中显示方块的区域有两个:一个是方块移动和堆积的区域,一个是提示下一组方块样式的区域。我们将通过编写代码给这两个区域添加一个作为背景的网格组件。

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

  1. Lazarus运行时添加组件简介
  2. 定制自己的网格背景组件
  3. 向pTetris项目的方块移动区添加背景网格组件
  4. 通过菜单控制背景网格显示
  5. 向pTetris项目的方块提示区添加背景网格组件(调整已经构建的布局)
  6. 结束语

了解不同方式实现同样效果的差异,请阅读以下文章:

 在Lazarus下的Free Pascal编程教程——向窗体动态添加组件 - lexyao - 博客园

1.Lazarus运行时添加组件简介

Lazarus的用户界面窗口是放置在窗体组件TForm中的若干组件构成的,这些组件可以在设计时在窗体设计器中添加,这样添加的组件必须是在组件面板中存在的。
还有一种方法是动态添加组件,也就是代码在运行时添加组件。有两种理由让我们选择在运行时动态添加组件的方法:

  • 为了保持窗口的简洁,在窗口中只显示当前情景用到的组件,而跟在窗口中能够用到而与当前情景无关的组件则隐藏起来。因隐藏组件的方法有两种:
    • 通过设置组件的Visible:=false让组件不显示出来
    • 使用时动态添加组件,是需要时删除组件
  • 自己临时编写的组件,没有注册添加到组件面板中,只能选择用代码在运行时添加到窗口中

在这篇文章里我们将要添加到pTetris项目中的背景网格组件就是我们自己编写的、没有注册到组件面板中的组件。

 2.定制自己的网格背景组件

我们将要添加到pTetris项目中的背景网格组件需要具有的功能包括:

  • 能够在放置方块的区域在方块之间画出网格线
  • 网格线可以显示,也可以隐藏,还可以改变颜色
  • 可以使用网格坐标定位每个网格,而不是使用像素坐标
  • 可以使用网格坐标将一个方块显示在指定的网格内

符合这样的功能要求的组件在Lazarus中是找不到的,所以我们只能自己编写一个组件。为了叙述风格的一致性,我们给要编写的背景网格组件取一个名字叫做cxGrid。为了让文章的内容简洁,我们将编写网格组件的有关内容放到另一篇文章中,我们在这篇文章里只介绍使用cxGrid组件的方法。
以下是介绍编写cxGrid组件的文章的链接:

在Lazarus下的Free Pascal编程教程——定制自己的组件 - lexyao - 博客园

3.向pTetris项目的方块移动区添加背景网格组件

3.1 向主窗口中添加cxGrid组件

在Lazarus中动态添加组件可以在程序运行的任何时刻进行,但你要保证在你使用这个组件之前添加。通常情况下有两种地方可以添加组件:

  • 窗体的OnCreate事件处理函数中
  • 界面中的情景引发的事件或者鼠标点击事件中

在这里,我们希望程序运行后主窗口出现时我们能够看到,所以我们选择前一种。
在对象查看器中双击frmMain,在代码编辑器中便添加了TfrmMain.FormCreate函数。这个函数中的代码是在应用程序运行时已经构建了主窗口的实例但主窗口还没有显示出来的时候执行的。要在主窗口中添加背景网格组件cxGrid,需要在三个地方添加代码。

1.在uses中添加cxGrid所在的单元名pTetrisUint

2.在TfrmMain中添加一个cxGrid类型的变量以便使用添加的cxGrid对象。由于只在这个单元中使用,所以可以添加到private段中。

  private
    grdBox:cxGrid;  

3.在这个函数的实现中添加以下代码。由于我们添加的cxGrid组件是用作方块移动区的背景的,所以我们在Create的参数中使用的是pnBox,这样添加的cxGrid组件会显示在pnBox中并铺满pnBox的客户区。

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  grdBox:=cxGrid.Create(pnBox);
  //grdBox.AdjustBox(10,20,0);
end;  

编译运行pTetris项目,主窗口中“显示屏”中的pnBox中显示了一些有规则排列的黑点,这就是cxGrid组件默认LineStyle := lsDot的显示效果。

 从画面中我们看到存在两个问题:

  • cxGrid的右边列和下边行不完整,没有边线
  • 原来在pnBox中显示的在定时器控制下移动的红色小方块不见了。这是被cxGrid组件覆盖了

3.2 将红色方块在cxGrid中显示

在程序的适当位置添加以下代码,红色小方块就显示出来了,而且还是像往常一样跳动。

Panel1.Parent:=grdBox; 

这一行代码可以在grdBox:=cxGrid.Create(pnBox)语句执行后的任何一个能够执行的代码中添加,只有在Panel1.Parent:=grdBox执行后红色方块才能显示出来。
在这里涉及到组件的两个属性:

  • TComponent中定义的属性property Owner: TComponent。Owner是组件的所有者,若干一个组件实例没有使用Free释放,它的Owner所指的对象会在程序结束是释放它。
  • TControl中定义的属性property Parent: TWinControl。Parent是控件显示时的所有者,控件定位使用的坐标是Parent所指的控件客户区的坐标。

3.3 调整cxGrid组件使它的单元格完整显示

曾经专门为了调整网格完整显示添加了一个函数cxGrid.AdjustBox。这个函数可以在grdBox:=cxGrid.Create(pnBox)语句执行后的任何一个能够执行的代码中添加调用。
我想到的第一个地方是TfrmMain.FormCreate中,就是前面的代码中被注释的那一行。添加了之后编译运行,发现这一行代码没有达到预期的效果,跟没有添加是一样的。这是为什么呢?
后来我使用调试器查看了cxGrid高、宽属性的变化,终于想到了问题的原因:TForm创建后调用OnCreate,这个时候在窗体设计器中添加的组件已经创建,但还没有调整尺寸。也就是说在调用FormCreate的时候pnBox的尺寸还没有调整,这样在FormCreate调用cxGrid.AdjustBox更改的pnBox 的尺寸在后来调整尺寸时又被改变了。什么时候pnBox的尺寸确定了呢?一个是在调用FormSize之前。这样,我们就可以把调用cxGrid.AdjustBox的代码添加到FormSize或者之后执行的代码中。
我把它添加到了TfrmMain.FormShow中。其实,添加到FormShow中和添加到FormSize中执行的效果是一样的。

procedure TfrmMain.FormShow(Sender: TObject);
begin
  grdBox.AdjustBox(-0,-0,0);
  Panel1.Parent:=grdBox;
end;  

添加了上述代码后,这时再编译运行,看到网格的边框完整了,红色小方块也能够正常跳动了。

3.4 调整方块对齐网格线

从运行的画面来看,cxGrid的右边和下边的网格完整了,但还有一个问题存在,那就是红色小方块没有对齐cxGrid的网格线。
这个红色小方块是用来测试方块移动的,将来我们在游戏中移动和堆积方块的时候必须是与网格对齐的。怎么才能做到呢?这就要用到我们添加的另一个函数:cxGrid.MoveTo。
移动小方块的代码是在计时器组件的事件函数TfrmMain.Timer1Timer中的,我们将原来的代码提取出来,作为一个函数保存。
操作方法如下:

  1. 选中TfrmMain.Timer1Timer除了第一行和最后一行之外的所有代码
  2. 在选中的代码上右击鼠标,从弹出的菜单中选择[重构->提取过程...]弹出提取过程的窗口
  3. 选择方法的类型(新过程存放的位置)、输入新的过程名(我输入的是DoTimerV1)、点击[抽取]按钮

 完成以上操作后,TfrmMain.Timer1Timer的代码转移到了新构建的一个函数TfrmMain.DoTimerV1中,并在TfrmMain.Timer1Timer中调用了DoTimerV1。

复制代码
procedure TfrmMain.Timer1Timer(Sender: TObject);
begin
  DoTimerV1;
end;

procedure TfrmMain.DoTimerV1;
var
  yy, xx: integer;
  rc:TRect;
begin
  if dx = 0 then dx := 1;
  if dy = 0 then dy := 1;
  rc:=pnBox.ClientRect;
  if Panel1.Left <= rc.Left then
    dx := 1
  else if  Panel1.Left+Panel1.Width>= rc.Right  then
    dx := -1;

  if Panel1.Top <= rc.Top then
    dy := 1
  else if  Panel1.Top+Panel1.Height>= rc.Bottom  then
    dy := -1;

  xx := Panel1.Left + Panel1.Width * dx;
  yy := Panel1.Top + Panel1.Height * dy;
  Panel1.Left := xx;
  Panel1.Top := yy;
end;  
复制代码

编译运行pTetris项目,运行结果可以看出,提取代码形成DoTimerV1函数前后的效果是一样的。
我们参照DoTimerV1中的代码重新编写一个新的函数DoTimerV2,将其中定位Panel1的代码改为使用cxGrid的网格坐标,然后在TfrmMain.Timer1Timer用DoTimerV2代替DoTimerV1。
添加DoTimerV2后的代码如下:

复制代码
procedure TfrmMain.Timer1Timer(Sender: TObject);
begin
  //DoTimerV1;
  DoTimerV2;
end;

procedure TfrmMain.DoTimerV2;
var
  mc, mr: integer;
  cr, nr: TPoint;
begin
  if dx = 0 then dx := 1;
  if dy = 0 then dy := 1;
  cr := grdBox.ColRowFromXY(Panel1.Left, Panel1.Top);
  mc := grdBox.Cols;
  mr := grdBox.Rows;

  if cr.X <= 0 then
    dx := 1
  else if cr.X >= mc-1 then
    dx := -1;

  if cr.Y <= 0 then
    dy := 1
  else if cr.Y >= mr-1 then
    dy := -1;

  nr.X := cr.X + dx;
  nr.Y := cr.Y + dy;

  grdBox.MoveTo(Panel1, nr);
end;      
复制代码

编译运行pTetris项目,运行结果可以看出,红色小方块移动时总是对齐cxGrid的网格线,到了组件边缘的时候也不会超出边界了。这就是我们要达到的效果,我们成功了。

4.通过菜单控制背景网格显示

 在Lazarus中,任何组件的外观都有默认值。要想改变组件的外观有两种途径:

  • 设计时在设计器和属性列表中修改组件外观的属性值
  • 通过编写代码在运行时通过操作或者响应情景变化的事件改变组件外观的属性值

我们将通过改变背景网格显示的例子来介绍运行时改变组件外观的方法。
在应用程序设计中,通常在主菜单中有改变界面外观的操作:

  • 对于简单的操作,通过点击菜单项实现操作,比如控制组件显示与隐藏的开关、多种方案中选择其一的单选项
  • 对于复杂的操作,通过打开一个操作界面,在操作界面中实现需要的操作

以上两种操作我们都将在另一篇专题文章中描述,包括控制背景网格显示的操作。下面就是描述在运行时改变组件外观的文章的网址:

在Lazarus下的Free Pascal编程教程——在程序运行时改变组件的外观 - lexyao - 博客园

5.向pTetris项目的方块提示区添加背景网格组件(调整已经构建的布局)

布局的方法有多种,使用不同方法布局的难易程度不同,而对于已经完成的布局进行修改,其难易程度也与布局方法有关。容易布局的方法不一定容易修改。
我们向已经完成的pTetris项目添加一个用于方块提示区的网格背景组件就涉及到布局修改的问题。如果是按着原来的想法在方块提示区只提示一组方块,放置在pnInfo中,那就只是添加一个组件的事情,没有任何问题。现在想改变一种思路:将方块提示区放在方块移动区的左边,提供更多组方块提示。按着这样的思路修改布局,麻烦就要大很多。
关于方块提示区的布局问题,我们提出如下要求:

  • 方块提示区放置在pnScreen内部的最左边,高度与方块移动区pnBox相同
  • 可以提示多组即将进入方块移动区的方块
  • 实现方法是在pnScreen中添加一个跟pnBox一样的面板pnNext,在pnNext中放置一个cxGrid组件grdNext,用来显示即将进入移动区的方块队列

添加pnNext具体的实现方法将在下面链接的文章中讲述:

在Lazarus下的Free Pascal编程教程——布局方法对比、修改已经完成的布局 - lexyao - 博客园

在上面文章中已经在pTetris项目中添加了作为grdNext容器的组件pnNext,再添加grdNext组件就简单了。添加grdNext的方法跟添加grdBox的方法相同,使用的代码也是一样的,在添加和调整grdBox的地方添加用于grdNext的同样的代码就行了。具体添加的代码如下:

grdBox, grdNext: cxGrid;  
procedure TfrmMain.FormCreate(Sender: TObject);
begin
  grdBox:=cxGrid.Create(pnBox);
  grdNext:=cxGrid.Create(pnNext);
end;  
procedure TfrmMain.FormShow(Sender: TObject);
begin
  grdBox.AdjustBox(-0,-0,0);
  Panel1.Parent:=grdBox;
  grdNext.AdjustBox(5,20,0);
end;  

添加pnNext和grdNext后编译运行pTetris项目出现的界面中部区域如下图所示:

6.结束语

在这篇文章里主要介绍了使用代码在运行时向应用程序主窗口添加组件的方法。在没有所见即所得的程序开发环境中编写程序使用的都是这种方法,而在有窗体设计器的开发环境中编写程序,使用代码添加组件就变成了一种辅助手段。熟悉这种添加组件的方法让编写的程序更加灵活,界面更加清晰。
在这篇文章里我们还用到了Lazarus的一个非常有用的功能:提取过程(或函数)。这个功能在Delphi中也有,而且Delphi和Lazarus的这个功能的使用方法是一样的,而Lazarus提供了更多的选项让这个方法更加方便。

posted @   lexyao  阅读(83)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示