在Lazarus下的Free Pascal编程教程——使用LCL布局组件构建应用程序主窗口布局
0.前言
我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。
了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:
学习编程从游戏开始——编程计划(目录) - lexyao - 博客园
我已经在下面这篇文章中介绍了使用LCL和FCL组件构建一个项目(pTetris)的过程,后续的使用Lazarus的文章中使用的例子都是以向这个项目添加新功能的方式表述的:
在Lazarus下的Free Pascal编程教程——用向导创建一个使用LCL和FCL组件的项目(pTetris) - lexyao - 博客园
应用程序的主窗口是应用程序与用户打交道的第一场所,主界面设计是否合理直接影响用户的第一感受,关系到一个应用程序的成败。在这篇文章里我将介绍构建在Lazarus中使用LCL的布局组件构建应用程序主窗口的界面的方法。
在这篇文章里,我主要讲述以下几个方面的内容:
- 应用程序用户界面布局概述
- 主窗口的总体布局思路
- 使用Lazarus布局组件完成pTetris项目主窗口的总体布局
- 通过编写代码调整窗口及其中的组件有合适的大小
- 结束语
了解不同方式实现同样效果的差异,请阅读以下文章:
在Lazarus下的Free Pascal编程教程——使用LCL布局组件构建应用程序主窗口布局 - lexyao - 博客园
1.应用程序用户界面布局概述
应用程序的用户界面是由若干组件组成的。用户界面的布局直接影响界面的美观,也影响着使用的方便程度。如何布置出既美观又使用方便的用户界面是程序设计的重要工作内容。
每个可视组件都是一个窗口。按是否能够容纳其他组件来给组件分类,可以分为容器类组件和非容器类组件。
容器类组件也是组件,具有所有组件都有的共同属性,同时又有作为容器能够容纳其他组件的特有属性。容器类组件除了本身显示信息的属性,还有适用于其容纳的组件布局有关的属性。
还有一类组件被称作布局组件,它们除了具有容器类组件的属性,还有专门为了布局而设置的属性。使用布局组件构建用户界面可以节省大量的时间,同时布局组件构建的用户界面具有很强的适应性。
灵活使用布局组件和与布局有关的组件属性,是布局的重要手段。
打开下面的网址可了解组件布局有关的属性:
在Lazarus下的Free Pascal编程教程——Lazarus的LCL布局组件简介 - lexyao - 博客园
Autosize / Layout/zh CN - Free Pascal wiki
Anchor Sides - Free Pascal wiki
2.主窗口的总体布局思路
关于pTetris项目的主界面的构建思路大致采用以下方式:
- 主窗口:从上到下依次是主菜单、中心区域、状态栏
- 主菜单:提供pTetris项目的所有操作的入口,包括显示控制、游戏设置、游戏操作等。由于有些操作使用菜单不方便,所以还会添加其他的操作方法。
- 中心区域:这个区域是项目的核心,从左到右依次是设置区、游戏区、榜单区
- 设置区:提供给用户定制游戏界面、游戏规则的场所。从上到下依次是标题、设置页面。
- 标题:显示设置区域的标题
- 设置页面:使用分页组件为每一类设置选项提供一个设置页面,包括图案选择、计分规则、加速规则、声音定制、起始难度
- 图案选择:提供定制游戏区背景图案、游戏方块图案、操作按钮图案的选项
- 计分规则:提供定制游戏计分规则的选项,包括放下一组方块的得分、消除一行的加成比例、一次多行的奖励倍数、放置高度加成比例、操作一次减分比例
- 放下一组方块的得分:放下方块得分为基础分数。一个方块1分,每组四块,可得4分
- 消除一行的加成比例:可选奖励。放下方块的同时消除整行可获得奖励,每消除1行得分加n倍,n=1表示没有加成,最大加成10倍
- 一次多行的奖励倍数:可选奖励。放下方块一次消除多行得分按消除行数翻倍。此项依赖消除一行的加成是否选中
- 放置高度加成比例:可选奖励。按放下方块所在的高度得分加倍,所得分数乘以高度。底部行高为1,无加成
- 操作一次减分比例:可选惩罚。从方块出现到放下,每操作一次得分减去n%,n取值范围0-10,默认为1。累计最高可减去100%
- 加速规则:为了游戏能够结束,随着时间的推移速度会逐渐加快,或者每放下一组速度加快。每次放下一组得分加成可获得速度奖励,降低加速的程度。两次移动之间的时间间隔是1-1000毫秒,初始为1000,加成计算可按实数,定时器设置时转换为整数
- 声音定制:提供背景音乐、操作声音的定制和开关。可以为每种操作定制一个声音,包括:出现、旋转、左移、右移、下落、到底、跌落
- 起始难度:可定制起始的移动时间间隔和残存方块高度
- 游戏区:游戏显示和操作的区域。从上到下依次是标题、显示屏、操作按钮。游戏区边框凸起呈现立体形态,背景可定制颜色或图片。背景使用图片时,立体效果可以由图片实现
- 标题:显示Super Tetris
- 显示屏:显示方块移动、提示、得分统计等信息。从左到右依次是难度标志、方块移动区、提示信息区
- 难度标志:通过一个滑块表示起始高度,通过一个彩色进度条指示当前速度
- 方块移动区:方块移动和堆积的显示区域
- 提示信息区:显示当前得分、耗时、方块组数、下次出现方块形状的提示等信息
- 操作按钮
- 榜单区:提供历史游戏战绩排行榜的显示。从上到下依次是标题、表格
- 标题:显示排行榜标题
- 表格:显示排行榜的战绩。可以选定前100名,当前正在进行的游戏进入榜单后动态显示,名次提升有声音提示
- 设置区:提供给用户定制游戏界面、游戏规则的场所。从上到下依次是标题、设置页面。
- 状态栏:用于显示游戏相关的信息。对于这个项目来说,状态栏的用途不大,只是作为应用程序常用的组件保留下来,为了编程提供一种示例。
3.使用Lazarus布局组件完成pTetris项目主窗口的总体布局
pTetris项目主窗口的布局构想在下面链接的文章中有描述。在这里要做的的主窗口布局就是要实现下面这篇文章的构想:
学习编程从游戏开始——多彩俄罗斯方块的设计构想 - lexyao - 博客园
向pTetris项目主窗口添加组件的描述将按着前面“主窗口布局的思路”划分的层次来描述,具体操作的时候当然是先放置底层的容器组件,然后在向容器内添加其他组件,而同一个容器内的组件有先后顺序。修改组件属性的操作只要不影响容器内组件的排列,可以在任何时候修改,不必在意先后顺序。
由于主窗口中已经有了向下停靠的状态栏,要想在中间区域按左右方向排列三个区域,需要一个容器占据整个剩余的客户区作为一个基础框架。调整主窗口大小也不能使用主窗口的AutoSize属性,而是使用OnSize事件中的代码来调整。有关代码的编写将在下一篇文章中讨论,在这一篇文章里只讨论添加组件、设置组件属性。
作为布局使用的容器组件有很多,不需要显示标题,仅仅是为了排列组件的容器只有TFlowPanel和TPanel,其中TFlowPanel值专门作为布局组件使用的,没有其他的附加功能。TFlowPanel的功能可以为布局节省大量的时间和工作量,但它的一些优点也限制了它的灵活性,它的自动化让它在有些时候存在不可控或者不确定性。要布置稳定可靠的界面,TPanel更合适一些,当然工作量可能也会大一些。
在这里,我们选择以TPanel为布局容器,以锚定为主要手段的布局方法。在开始布局之前,先通过截图看一下Lazarus中锚定的设置界面,在界面中的图标反应了锚定后组件的参照物和被锚定的组件的相对位置。
相信看到这里的人已经在前面的连接中看到了锚定各种铆钉选项的效果图,这里就不再赘述了。
以下是设置锚点和设置属性的操作步骤,在下面介绍布局的操作过程时不再赘述,都会使用这个操作步骤:
- 设置锚定的锚点
- 在Lazarus的属性列表中点击Anchors后面有三个点的按钮,打开锚点设置窗口
- 在对象查看器的组件列表或正在设计的窗体中选中要设置锚定的组件
- 在锚点设置窗口中勾选要设置的锚点的“启用”,选择锚定方式(对齐、对中或对接)
- 在“同属”窗口的下拉列表中选择作为参照的组件(这里面有可以作为锚定参照物的容器组件或者兄弟组件)。选择之后会看到吧被锚定的组件按照选择的方式移动到参照物旁边
- 如果要让被锚定的组件与参照物之间保持距离,可以在锚点设置窗口中设置“边界空格”
- 设置组件属性
- 从对象查看器的组件列表或者窗体设计器的窗体上选中要设置属性的组件
- 在属性列表中设置属性的值
以下是使用TPanel组件作为主要布局的容器布置pTetris项目主界面的操作步骤:
- 基础容器:向窗体frmMain添加一个TPanel组件作为中心区域的基础容器。中心区域的左右组件都包含在这个容器中。
选中窗体frmMain,在组件面板Standard页双击TPanel图标,在主窗口frmMain中添加一个TPanel组件,自动命名Name属性为Panel1,将Panel1的Name改为pnBase
选中pnBase,在属性列表中设置pnBase的Left、Top属性的值为0
调整pnBase的大小到足够大(为后续的操作提供作业面,也可以以后根据需要调整) - 向基础容器添加第二层容器
选中pnBase,双击组件面板中的TPanel图标,向pnBase添加一个TPanel组件,将Name属性改为pnLeft,作为设置区的容器
选中pnBase,双击组件面板中的TPanel图标,向pnBase添加一个TPanel组件,将Name属性改为pnMiddle,作为游戏区的容器
选中pnBase,双击组件面板中的TPanel图标,向pnBase添加一个TPanel组件,将Name属性改为pnRight,作为游戏区的容器。
设置pnLeft、pnMiddle、pnRight的顶部锚点对齐pnBase的上边
设置pnLeft的左侧锚点对齐pnBase的左边,设置pnMiddle的左侧锚点对接pnLeft的右边,设置pnRight的左侧锚点对接pnMiddle的右边
设置pnLeft、pnRight的底部锚点对齐pnMiddle的下边
设置pnLeft、pnRight的BevelOuter属性为bvNone,隐藏边缘的3D立体效果
设置pnMiddle的BevelInner属性为bvRaised,BevelWidth为3,凸显边缘的3D立体效果。这是作为游戏机外形的效果,可以采用立体方框,将来优化改进时可以考虑使用图片描绘出其他效果
调整pnMiddle的大小到足够大,这时看到随着pnMiddle的高度变化,pnLeft、pnRight始终保持与pnMiddle同样的高度 - 向第二层容器添加组件
- 向第二层容器中添加标题组件
向pnLeft、pnMiddle、pnRight中各添加一个TDividerBevel组件(在组件面板的LazControls中),Name属性自动命名为DividerBevel1、DividerBevel2、DividerBevel3
设置DividerBevel1、DividerBevel2、DividerBevel3的顶部锚点对齐各自所在容器的上边、左侧锚点对中各自所在容器的上边
设置DividerBevel1、DividerBevel2、DividerBevel3的Style属性为gsHorLines,Font.Size属性为20,Caption属性依次为“设置表”、“Super Tetris”、“排行榜” - 向pnLeft中添加TPageControl组件,Name属性改为pgOption
设置pgOption的顶部锚点对接DividerBevel1的下边,左侧、右侧、底部对齐所在容器的边,这时看到pgOption铺满了pnLeft中DividerBevel1以下的所有区域,并能够随着pnLeft大小的变化而变化
设置pgOption的TabPosition属性为tpLeft,这样设置后它的页面标签将会显示在它的左边- 向pgOption组件添加页面
右击pgOption组件,从菜单中选择“添加页”,连续操作5次,添加5个页面,Name属性依次改为tbsFace、tbsScore、tbsSpeed、tbsSound、tbsStart
5个页面的Caption属性依次改为:图案选择、计分规则、加速规则、声音定制、起始难度 - 向5个页面添加组件【向5个页面添加设置数据的组件】
- 向pgOption组件添加页面
- 向pnRight中添加TDrawGrid组件,Name属性改为grdRankings
设置grdRankings的顶部锚点对接DividerBevel3的下边,左侧对中所在容器的上边,底部对齐所在容器的下边,这时看到grdRankings高度铺满了pnRight中DividerBevel3以下的所有区域,并能够随着pnRight高度的变化而变化,grdRankings的宽度可能在pnRight两边留有空白,也可能因为超过pnRight的宽度而不能完全显示。要保持grdRankings宽度方向上的完全显示,将会通过编写代码来实现 - 向pnMiddle中添加组件
- 添加显示屏:这是作为游戏机动画显示和信息显示的区域
向pnMiddle中添加一个TPanel组件,Name属性改为pnScreen
设置pnScreen的BevelOuter、BevelInner属性为bvLowered,BevelWidth为3,使得显示屏边框呈现凹陷的3D立体效果
设置pnScreen的顶部锚点对接DividerBevel2的下边,左侧锚点对中DividerBevel2的下边- 向显示屏pnScreen添加组件
调整pnScreen的大小到足够大,为添加组件留出操作空间- 向pnScreen添加TTrackBar组件,Name属性设置为trcStart,作为标志起始难度中的起始高度,游戏过程中用来显示速度
设置trcStart的Orientation属性为trVertical(成为竖条放置),Height属性为350(参照中间方块移动区域的高度设置,由于两端有空白,所以高度略大一些)
设置trcStart的Max属性为1000(方块移动的最大时间间隔为1000毫秒) - 向pnScreen添加TPanel组件,Name属性改为pnBox。这是显示屏的中心,方块移动和堆积的场所
设置pnBox的左侧锚点对接trcStart右边
设置pnBox的BevelOuter属性为bvNone,BorderStyle属性为bsSingle(单线边框)
设置pnBox的Width、Height分别为171、341(计划使用16x16的图标显示方块,方块个数10x20,考虑了方块和分格线占用的尺寸)
向pnScreen添加TPanel组件,Name属性改为pnInfo。这是显示屏中信息显示的场所设置pnInfo的左侧锚点对接pnBox右边设置pnInfo的BevelOuter属性为bvNone设置pnInfo的左侧锚点对接pnBox右边,顶部、底部锚点对齐pnBox的上边、下边,信息显示区域的高度自动保持与中部的pnBox等高 - 向pnInfo添加组件【添加下一组方块的提示、得分统计等信息
- 向pnScreen添加TTrackBar组件,Name属性设置为trcStart,作为标志起始难度中的起始高度,游戏过程中用来显示速度
- 设置显示屏的最终尺寸
设置pnScreen的AutoSize属性为true,屏幕自动调整为合适的大小
- 向显示屏pnScreen添加组件
- 添加操作按钮:操作游戏的按钮,可以用鼠标点击按钮进行游戏的操作。
- 添加按钮容器
向pnMiddle中添加两个TPanel组件,Name属性改为pnButtonsLeft、pnButtonsRight
设置pnButtonsLeft、pnButtonsRight的BevelOuter属性为bvNone
设置pnButtonsLeft、pnButtonsRight的顶部描点对接pnScreen下边,上部边界空格为20
设置pnButtonsLeft的左侧锚点对齐pnScreen左边,设置pnButtonsRight的右侧锚点对齐pnScreen右边 - 添加左边按钮
向pnButtonsLeft添加两个TButton,Name属性改为btnStart、btnSound
设置btnStart、btnSound的Caption属性依次为:开始游戏、声音开关
设置btnSound左侧锚点对齐btnStart左边,顶部锚点对接btnStart下边
设置pnButtonsLeft的AutoSize属性为true,自动调整为合适的大小 - 添加右边按钮
向pnButtonsRight添加6个TButton,Name属性改为btnPause、btnRotate、btnDowned、btnLeft、btnDown、btnRight
设置btnPause、btnRotate、btnDowned、btnLeft、btnDown、btnRight的Caption属性依次为:暂停、旋转、跌落、左移、下落、右移
设置btnRotate、btnDowned的顶部锚点对齐btnPause上边,设置左侧锚点使btnRotate对接btnPause的右边、btnDowned对接btnRotate的右边
设置btnLeft、btnDown、btnRight的顶部锚点对齐btnPause的下边,设置左侧锚点使btnLeft对齐btnPause的左边、btnDown对接btnLeft的右边、btnRight对接btnDown的右边
设置pnButtonsRight的AutoSize属性为true,自动调整为合适的大小
- 添加按钮容器
- 调整pnMiddle的大小,使得两边和下部的空白为适宜的宽度(空白大约为25左右为宜)
- 添加显示屏:这是作为游戏机动画显示和信息显示的区域
- 向第二层容器中添加标题组件
- 设置pnBase的AutoSize属性为true,自动调整为合适的大小
- 关于窗体大小的设置:窗体frmMain的大小以客户区正好能够容纳pnBase和状态条为宜。
由于pnBase的大小可能变化,frmMain的大小要适应这种变化,所以可以在frmMain的OnResize事件中设置frmMain的大小
通过以上添加组件、设置属性等操作后,pTetris项目的主界面基本成型了。以下是编译运行后主界面的截图。
现在只是完成了主界面的框架,有些细节的内容还没有添加,比如:配置表每个页面中设置数据的组件、游戏区中显示提示信息的组件、游戏动作移动的方块、排行榜中的信息等。这些将在以后的文章里介绍。
4.通过编写代码调整窗口及其中的组件有合适的大小
在前面的布局中,已经通过使用布局组件和设置组件的布局属性形成了应用程序的主窗口界面,但还有单个问题没有解决:
- 排行榜表格的列宽还没有确定,部分列没有显示出来
- 排行榜表格宽度方向在容器内可能留有空白或者因为容器宽度不够而不能完全显示
- 中心区域的容器pnBase自动适应了内部组件的大小,但其外部与主窗口边缘之间可能有空白或者因为主窗口偏小而导致pnBase不能完全显示
这三个问题之所以没有使用设置组件的布局属性来解决,是因为设计时存在许多不确定因素,只有到了运行时才表现出来,甚至在运行时还会发生变化,比如:表格的列宽会根据显示内容调整宽度。要解决这样的问题,单靠组件的布局属性是不能解决的,这就需要通过编写代码来实现界面布局的调整。
针对以上三个问题,我们在TfrmMain类中定义三个函数:
procedure DoFormResize; procedure DoGridResize; procedure DoPanelResize;
将代码编辑区的光标移动到函数上,点击组合键“Ctrl+Shift+C”,Lazarus的补全代码功能自动在实现区添加了者三个函数的的框架:
procedure TfrmMain.DoFormResize; begin end; procedure TfrmMain.DoGridResize; begin end; procedure TfrmMain.DoPanelResize; begin end;
我们给这三个函数添加代码,调整界面高度和宽度,达到我们要求的“全部显示、不留空白”的目标。
procedure TfrmMain.DoFormResize; var dw, dh: integer; begin //调整主窗口高度和宽度,使其中 组件全部显示切不留空白 dw := frmMain.ClientWidth - pnBase.Width; if dw <> 0 then frmMain.Width := frmMain.Width - dw; dh := frmMain.ClientHeight - pnBase.Height - StatusBar1.Height; if dh <> 0 then frmMain.Height := frmMain.Height - dh; end; procedure TfrmMain.DoGridResize; begin //按列宽调整排行榜表格组件宽度,使得所有的列都能完整显示(考虑滚动条宽度) { #todo : 所需代码在以后设置实现表格内容显示时再编写 } end; procedure TfrmMain.DoPanelResize; var dw: integer; begin //调整排行榜表格的容器宽度,使得表格在容器中完全显示且不留空白 dw := pnRight.ClientWidth - grdRankings.Width; if dw <> 0 then pnRight.Width := pnRight.Width - dw; end;
有了以上三个函数,但这三个函数还没有被调用,无法发挥作用。我们需要再代码能够执行的合适位置添加这三个函数的引用。
第一个合适的位置就是主窗口的OnResize事件处理函数。添加的操作步骤如下:
第一步、在Lazarus左上角的对象查看器中选中frmMain,这时右下角的属性列表显示了frmMain的属性
第二步、在属性查看器中选择“事件”选项卡,显示出frmMain的事件列表
第三步、找到OnResize事件,用鼠标双击,Lazarus自动在代码编辑器中给TfrmMain添加了一个FormResize函数和它的实现
第四步、在FormResize的实现中添加以上三个函数的引用,代码如下:
procedure TfrmMain.FormResize(Sender: TObject); begin DoGridResize; DoPanelResize; DoFormResize; end;
编译运行pTetris项目,可以看到运行后的界面除了排行榜表格列以外,其他方面都达到了我们“全部显示、不留空白”的目标。
5.结束语
Lazarus的组件提供了丰富的用于布局的属性,为程序员布置应用程序界面提供了很大的方便。具体采用哪一种方式布置界面更好,没有一定的答案。每个人有自己的爱好和习惯,适合自己的就是好的,只要能达到目的就行。所谓的殊途同归大概就是这个意思吧。
布局组件和组件的布局属性不是万能的,总有满足不了要求或者我们还没有发现满足要求的方法的时候,这时我们就需要通过编写代码调整布局的方法来达到目的。编写代码时需要注意一点:布局属性的自动调整有较高的优先级,编写代码调整的布局不会改变自动布局的结果。只有没有使用自动布局的地方才能使用代码调整布局,否则有可能进入死循环。
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现