0.前言
我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。
了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:
学习编程从游戏开始——编程计划(目录) - lexyao - 博客园
我已经在下面这篇文章中介绍了使用LCL和FCL组件构建一个项目(pTetris)的过程,后续的使用Lazarus的文章中使用的例子都是以向这个项目添加新功能的方式表述的:
在Lazarus下的Free Pascal编程教程——用向导创建一个使用使用LCL和FCL组件的项目(pTetris) - lexyao - 博客园
这是一篇专题文章,我们将通过一个简单的例子,讲述如何在程序运行中修改程序界面的外观。
俄罗斯方块游戏的核心是方块移动和堆积的区域,我们通过修改这个区域背景网格的外观来讲述在程序运行中修改组件外观的方法,同时也探讨一些与程序功能设计有关的知识。
在这篇文章里,我主要讲述以下几个方面的内容:
- 通过修改组件外观的方法来了解应用程序功能设计的思路
- 在菜单中直接实现修改应用程序界面外观的功能
- 在菜单中打开修改应用程序界面外观的功能所在的界面
- 在配置表中修改方块移动区外观的方法
- 结束语
1.通过修改组件外观的方法来了解应用程序功能设计的思路
在Lazarus中,任何组件的外观都有默认值。要想改变组件的外观有两种途径:
- 设计时在设计器和属性列表中修改组件外观的属性值
- 通过编写代码在运行时通过操作或者响应情景变化的事件改变组件外观的属性值
我们将通过改变背景网格显示的例子来介绍运行时改变组件外观的方法。
在应用程序设计中,通常在主菜单中有改变界面外观的操作:
- 对于简单的操作,通过点击菜单项实现操作,比如控制组件显示与隐藏的开关、多种方案中选择其一的单选项
- 对于复杂的操作,通过打开一个操作界面,在操作界面中实现需要的操作
既然我们给自己出的题目是在运行时改变组件外观,这个组件是方块移动区的背景网格,那么我们就来分析一下需要做哪些事情。
背景网格是外观可以改变的内容包括以下几个方面:
- 网格的间距:这个是在设计之初确定的,将来可能会根据方块使用图标的大小而自动适应。这是将来的事情,现在先不考虑
- 网格线型:LineStyle定义了三种情况TcxLineStyle = (lsNone, lsDot, lsUsePen),可以在这三种情况中选择其一
- 选择lsUsePen是对应着Canvas.Pen.Style,它的可选项为TFPPenStyle = (psSolid, psDash, psDot, psDashDot, psDashDotDot, psinsideFrame, psPattern,psClear);
- 网格线颜色:默认使用Canvas.Pen.Color,可以改变这个颜色。颜色的确定有两种情况:
- 操作系统定义的颜色
- 一个$000000-$FFFFFF之间的数字代表的颜色
从以上分析来看,我们需要做的有两项工作:设置网格线型、设置网格线颜色。下面我们就来探讨问题的解决方法。
2.在菜单中直接实现修改应用程序界面外观的功能
既然我们决定使用菜单来操作,那就先考虑以下使用菜单需要做那些工作。通常使用菜单需要做以下工作:
- 菜单项的设置
- 菜单项的添加
- 菜单操作的代码
选择我们就着三方面的操作分开来讨论。
2.1 菜单项的设置
菜单项的划分关系到用户使用应用程序时的感受。设计菜单应该考虑以下原则:
- 我们应该让用户看到菜单名就知道是做什么用的
- 菜单项有很多,怎样划分才让用户便于找到需要的操作
- 菜单项的使用方法有多种,我们选择哪一种方式:
- 点击菜单直接执行命令。比如:点击菜单的[文件->退出]就直接关闭了应用程序
- 点击菜单打开一个窗口,用户在这个窗口中操作。这样的菜单项通常在名字后面跟着三个点,让用户看到后找到还有后续的操作。比如:点击[文件->新建...]会打开一个窗口让用户选择新建文件的类型
- 点击菜单可以打开或关闭某一项功能。通常可以在这种菜单前有一个对号表示选中。比如:确定是否显示状态栏的选项
- 点击菜单从多个项目中选择其中之一。通常在这种菜单前有一个对号或者圆点表示选中的项目,这样的菜单项一个是一组中有多个,只能选中其一
我们要添加的菜单操作是改变组件外观的,所以我们可以在主菜单中添加一个“视图”项,其他的操作都在这个菜单下添加:
- 显示状态栏:作为一项测试,先添加这个菜单项,可以显示/隐藏主窗口的状态栏
- 背景网格:添加三个菜单项对应LineStyle的三个值,只能三选一
- 无网格线:对应lsNone,不显示背景网格线
- 标记网格线交点:对应lsDot
- 使用画笔线型:对应lsUsePen,
- 画笔线型选择:当选中lsUsePen时才可以使用这个菜单项,也可以选中这个菜单项的子项后导致lsUsePen被选中,可以在这个项目下添加子菜单项,每一项对应TFPPenStyle的一个值
- 网格线颜色:打开颜色选择窗口,从中选择一种颜色作为网格线的颜色
2.2 菜单项的添加
添加菜单项的操作在前面的文章中我们已经使用过了。下面链接是Lazarus的关于菜单编辑器的介绍:
IDE Window: Menu Editor - Lazarus wiki
在对象查看器或者窗体设计器中双击主菜单组件,打开菜单编辑器。我们将在菜单编辑器中添加我们的菜单项。
这个菜单编辑器就是一个很好的根据情景改变组件外观的例子:当我们选中正在编辑的菜单的一个菜单项的时候,左边“菜单项动作”中的图标就会发生改变,只显示当前能够进行的操作,不能操作的变成灰色。
添加菜单项的操作步骤如下:
- 选中[文件],点击右侧添加菜单项图标,在“文件”和“帮助”之间插入一个菜单项,Name属性改为mnView,Caption属性改为“视图”
- 选中[视图],点击添加子菜单,在视图下添加一个子菜单项,Name属性改为mnStatusBar,Caption属性改为“显示状态栏”,Checked属性改为true,AutoCheck改为true
- 选中[显示状态栏],点击下边添加分隔符图标,添加一个Separator2
- 再点击添加菜单项,在分隔符下添加一个菜单项,Name属性改为mnBoxLine,Caption改为“选择背景网格显示方式”
- 点击添加子菜单,在mnBoxLine右侧添加一个子菜单MenuItem1,
- 在MenuItem1下添加两个菜单项MenuItem2、MenuItem3
- MenuItem1、MenuItem2、MenuItem3的Name属性依次改为mnLineNone、mnLineDot、mnLinePen,Caption属性依次改为“无网格线”、“标记网格线交点”、“使用画笔线型”,Tag属性依次为0、1、2
- mnLineNone、mnLineDot、mnLinePen的RadioItem属性改为true,AutoCheck改为true,GroupIndex属性改为1
- mnLineDot的Checked属性改为true
- 选中[使用画笔线型],点击下边添加分隔符图标,添加一个Separator3
- 再点击添加菜单项,在分隔符下添加一个菜单项,Name属性改为mnPenStyles,Caption改为“选择画笔线型”
- 在[选择画笔线型]下添加8个子菜单项
- 8个子菜单项的Name属性依次修改为mnPenSolid、mnPenDash、mnPenDot、mnPenDashDot、mnPenDashDotDot、mnPenInsideFrame、 mnPenPattern、mnPenClear
- 8个子菜单项的Caption属性依次修改为“Solid”、“Dash”、“Dot”、“DashDot”、“DashDotDot”、“InsideFrame”、“Pattern”、“Clear”
- 8个子菜单项的Tag属性依次修改为0、1、2、3、4、5、6、7
- 8个子菜单项的RadioItem属性改为true,AutoCheck改为true,GroupIndex属性改为2
- mnPenSolid的Checked属性改为true
- 在MenuItem1下添加两个菜单项MenuItem2、MenuItem3
- 选中[选择背景网格显示方式],点击添加菜单项,在“选择背景网格显示方式”下添加一个菜单项,Name属性改为mnBoxColor,Caption改为“选择背景网格颜色...”
编译运行pTetris项目,观察主菜单中“视图”下的菜单项,测试点击后菜单的变化:
- 开关项:前面有对号的,显示状态栏
- 多选一:前面有圆点的,选择背景网格显示方式、选择画笔线型下的子菜单
2.3 菜单操作的代码
菜单项已经添加到了主菜单的“视图”下,下一步就是让这些菜单在点击后有效果。
2.3.1 显示/隐藏状态栏
这是一个开关项,点击后隐藏状态栏,再点击状态栏又显示出来。添加代码的操作步骤如下:
第一步、在对象查看器中双击菜单项mnStatusBar,编辑区添加mnStatusBarClick的定义和实现代码。
第二步、在编辑区mnStatusBarClick的实现代码中添加显示/隐藏状态栏的代码。添加后代码如下:
procedure TfrmMain.mnStatusBarClick(Sender: TObject); varmni: TMenuItem; beginmni := Sender as TMenuItem;StatusBar1.Visible := mni.Checked; end;
编译运行pTetris项目,测试点击“显示状态栏”后的变化:点击后状态栏隐藏,再点击后状态栏又显示出来。
操作状态栏的目的达到了,但是有一个问题表现出来:状态栏隐藏后留下了一个空白。
怎么让这个空白去掉呢?唯一的办法是更改主窗口的高度。这时应该想起了以前我们曾经使用过的一个函数:
procedure TfrmMain.DoFormResize; vardw, dh: integer; begin//调整主窗口高度和宽度,使其中 组件全部显示切不留空白dw := frmMain.ClientWidth - pnBase.Width;if dw <> 0 thenfrmMain.Width := frmMain.Width - dw;dh := frmMain.ClientHeight - pnBase.Height - StatusBar1.Height;if dh <> 0 thenfrmMain.Height := frmMain.Height - dh; end;
应该在显示/隐藏状态栏后调用DoFormResize修改窗口高度,但从上面的代码看,DoFormResize中没有考虑状态栏隐藏后的情况。我们修改DoFormResize的代码,然后在mnStatusBarClick添加调用。
修改后DoFormResize的代码如下:
procedure TfrmMain.DoFormResize; vardw, dh, sh: integer; begin//调整主窗口高度和宽度,使其中 组件全部显示切不留空白dw := frmMain.ClientWidth - pnBase.Width;if dw <> 0 thenfrmMain.Width := frmMain.Width - dw;if StatusBar1.Visible thensh := StatusBar1.Heightelsesh := 0;dh := frmMain.ClientHeight - pnBase.Height - sh;if dh <> 0 thenfrmMain.Height := frmMain.Height - dh; end;
修改后mnStatusBarClick的代码如下:
procedure TfrmMain.mnStatusBarClick(Sender: TObject); varmni: TMenuItem; beginmni := Sender as TMenuItem;StatusBar1.Visible := mni.Checked;DoFormResize; end;
编译运行pTetris项目,测试点击“显示状态栏”后的变化:随着状态栏的显示、隐藏,窗口高度发生相应的变化,不会留下空白了。
2.3.2 选择背景网格显示方式
有三种方式可以选择,我们将使用一个数据按处理函数响应三个菜单项的操作。添加代码的操作步骤如下:
第一步、在对象查看器中选中菜单项mnLineNone,再按下Ctrl键不放,连续菜单项mnLineDot、mnLinePen,同时选中了这三个菜单项
第二步、再属性查看器中选择“事件”选项卡,双击OnClick事件,再编辑区添加了mnLineNoneClick的定义和实现代码,而且三个菜单项同时使用这个事件处理函数。
第三步、在编辑区mnLineNoneClick的实现代码中添加设置网格背景显示方式的代码。添加后代码如下:
procedure TfrmMain.mnLineNoneClick(Sender: TObject); varmni: TMenuItem; beginmni := Sender as TMenuItem;grdBox.LineStyle := TcxLineStyle(mni.Tag);grdBox.Repaint; end;
以上代码中grdBox.Repaint是再设置了网格显示方式后让组件使用新的设置样式重新绘制。
编译运行pTetris项目,测试点击切换显示方式后的变化:网格线可以隐藏、画点、画实线。我们又成功了。
使用上述方法,我们同时给mnPenStyles下的8个子菜单项添加一个共同使用的事件处理函数mnPenSolidClick,添加代码如下:
procedure TfrmMain.mnPenSolidClick(Sender: TObject); varmni: TMenuItem; beginmni := Sender as TMenuItem;grdBox.PenStyle := TFPPenStyle(mni.Tag);mnLinePen.Checked := True;grdBox.LineStyle := lsUsePen;grdBox.Repaint; end;
在上面的代码中添加了显示方式同步的代码:无论当前三种显示方式的哪一种被选中,在选择了画笔的线型后,总是使得“使用画笔线型”被选中,这样才能使得刚刚选择的画笔线型立即被使用。
编译运行pTetris项目,测试点击切换画笔线型后的变化:尽管又8中线型可以选择,并不是每一种都觉得好看。把他们都添加上是为了演示一种编程方法,可以选择并不代表它就是我们需要的线型。
2.3.3 选择背景网格颜色
选择颜色涉及到一个新的话题——使用公共对话框。
在组件面板的Dialog页面中有一个TColorDialog组件,这个组件就是用来选择颜色的。下面链接是Lazarus的关于TColorDialog的介绍:
- TColorDialog - Free Pascal wiki
- TColorDialog
我们有两种方式使用TColorDialog:
- 设计时从组件面板把TColorDialog添加到窗口中,然后再添加使用这个组件的代码。这种方式可以让用户在第二次打开这个对话框的时候保持第一次选择的颜色。
- 编写代码,在运行时创建TColorDialog的实例,使用完成后释放TColorDialog的实例。这种方式不能保持上一次选择的结果,但可以通过在打开窗口之前设置Color的初始值恢复上次选择的颜色。
我们选择后一种方式。在对象查看器中双击菜单项mnBoxColor,编辑区添加mnBoxColorClick的定义和实现代码。然后添加代码如下:
procedure TfrmMain.mnBoxColorClick(Sender: TObject); vardlg: TColorDialog; begindlg := TColorDialog.Create(Self);trydlg.Color := grdBox.LineColor;if dlg.Execute thenbegingrdBox.LineColor := dlg.Color;grdBox.Repaint;end;finallydlg.Free;end; end;
编译运行pTetris项目,测试选择颜色后的效果。以下是设置颜色的界面截图。
3.在菜单中打开修改应用程序界面外观的功能所在的界面
应用程序主菜单中可以不包含程序的所有操作,但必须包含所有操作的入口。如果在菜单中找不到入口,那么用户可能很难找到怎么去启动这项操作。当然,由于菜单的局限性,有些工作是不能在菜单中实现的,那么,可以在菜单中添加一个指引,点击菜单项的时候跳转到这个操作的入口或操作界面,也可以给出一个提示性的信息。总之,只要你打开菜单,能够知道这个应用程序能够做什么。
前面演示了可以在菜单中直接实现的操作,对于复杂的操作,无法在菜单中完成,通常采取的方法是点击菜单项打开一个操作窗口。比如:设置。通常是在主菜单的工具或者选项菜单下有一个叫做设置或者选项的菜单项,点击这个菜单项就会打开设置窗口,在设置窗口中有很多选项,几乎包含了应用程序可以定制的所有项目。
应用程序的设置一般是在一个独立的窗口中的,我们在pTetris项目中采用了一个而另类的做法。
我们的pTetris项目的中心界面占用的空间很小,为桌面留出了很大的空间,我们为了演示布局,尽可能在主界面中添加更多的组件,所以我们不但把设置放在了中心游戏区的左边,还在右边添加了排行榜。
在主窗口左侧的配置表分成了多个页面,这样可以放置更多的组件。在主菜单中点击菜单项,应该可以直接让配置表显示出需要的页面。
在主菜单中打开设置窗口是很直观的,但只是切换了主界面中的一个页面很容易让人忽略,不知道发生了什么。为了引起用户的注意,在点击了主菜单后应该给出一个提示,可以是弹出一个窗口,提供你下一步可以怎么做的信息。有了这些信息,可能你下一次要打开这个设置的时候不会再去点击菜单,而是直接点击配置表中的选项卡了。
作为一项演示,我们添加一个菜单项,引导用户到配置表中去定制背景网格的外观。操作步骤如下:
第一步、在主菜单“视图”中添加一个菜单项,Name属性改为mnToBoxLineOption,Caption改为“在配置表中修改背景网格外观...”
第二步、添加菜单项mnToBoxLineOption的事件处理处理程序mnToBoxLineOptionClick
第三步、在编辑区给TfrmMain.mnToBoxLineOptionClick添加以下代码:
procedure TfrmMain.mnToBoxLineOptionClick(Sender: TObject); beginPageControl1.ActivePage:= tbsFace;MessageDlg(Caption, '请到窗口左侧的[图案选择]页面的[背景网格]栏目修改背景网格外观。', mtConfirmation,[mbOk],0); end;
编译运行pTetris项目,点击菜单[视图->在配置表中修改背景网格外观...],会弹出一个提示窗口,配置区也打开了“图案选择”页面。关闭这个提示窗口,我们就可以在配置表中修改背景网格外观了。
不过,现在看到的“图案选择”页面是一个空白,什么也没有,什么也不能做。下一步我们就添加能做的事情。
4.在配置表中修改方块移动区外观的方法
前面已经描述了在菜单中修改背景网格外观的方法,在以前的文章里也描述了向主界面添加组件布局的方法。没有记清楚的可以先看一看前面的内容和下面链接中的文章,了解相关的做法。
在Lazarus下的Free Pascal编程教程——使用LCL布局组件构建应用程序主窗口布局 - lexyao - 博客园
下面我们直接开始在配置表的“图案选择”页面添加可以在运行期修改方块移动区背景网格外观的组件和代码的操作步骤:
第一步、添加组件
- 在对象查看器或者窗体设计器中选中PageControl1的tbsFace(图案选择)页面,添加一个TPanel组件,Name属性改为pnBoxLineOption
- 设置pnBoxLineOption的Align属性为alTop向上边停靠
调整pnBoxLineOption的高度足够大,为下一步添加组件留出空间- 向pnBoxLineOption中添加一个TDividerBevel组件,自动命名Name属性为DividerBevel4,Caption属性改为“背景网格”,Align属性为alTop向上边停靠
- 向pnBoxLineOption中添加一个TRadioGroup组件,Name顺序ing改为grpBoxLine,Caption属性改为“网格线型”,Align属性为alTop向上边停靠
点击Items属性后边的三点按钮,在打开的窗口中输入“无网格线,网格交点,画笔线型”(分成三行),点击[确定]后在grpBoxLine中出现输入的三个选项
设置grpBoxLine的AutoFill属性为true,三个选项在组件客户区均匀排列
设置grpBoxLine的Columns属性为2,组件中的项目分成两行两列
设置grpBoxLine的ItemIndex属性为1,组件中的“网格交点”被选中
在grpBoxLine中添加一个TComboBox组件,Name属性改为cbBoxLine,cbBoxLine自动占据grpBoxLine一个空白的项目位置,大小自动调整为与其他选项相同- 点击cbBoxLine的Items属性后边的三点按钮,在打开的窗口中输入“Solid,Dash,Dot,DashDot,DashDotDot,InsideFrame,Pattern,Clear”(分成8行)
设置cbBoxLine的ItemIndex属性为0,组件中显示出被选中的“Solid”
设置cbBoxLine的Style属性为csDropDownList,使得它的项目只能从下拉列表中选择
- 点击cbBoxLine的Items属性后边的三点按钮,在打开的窗口中输入“Solid,Dash,Dot,DashDot,DashDotDot,InsideFrame,Pattern,Clear”(分成8行)
- 向pnBoxLineOption中添加一个TPanel组件,自动命名Name属性为Panel3,Align属性为alTop向上边停靠
- 向Panel3张添加一个TLabel组件,自动命名Name属性为Label1,Caption属性改为“背景颜色”
调整Label1的位置,与TRadioGroup中左边一列项目的文字对齐(为了整齐美观)
设置Label1的顶部锚点对中Panel3的左边 - 向Panel3张添加一个TPanel组件,Name属性改为pnBoxColor,BevelOuter属性改为bvSpace,BorderSytle属性改为bsSingle
调整pnBoxColor的高宽与TRadioGroup中的cbBoxLine相同,左边与cbBoxLine对齐(为了整齐美观)
设置pnBoxColor的顶部锚点对中Panel3的左边 - 调整Panel3为合适的高度(觉得美观就可以了)
- 向Panel3张添加一个TLabel组件,自动命名Name属性为Label1,Caption属性改为“背景颜色”
- 设置pnBoxLineOption的AutoSize属性为true,释放多余的空间
- 设置pnBoxLineOption的Align属性为alTop向上边停靠
需要说明的是:在Lazarus中没有适合我们的用来显示所选颜色的组件,所以我们使用了一个TPanel组件pnBoxColor代替。这也算是灵活使用组件的一个例子吧。
以下是添加组件后pnBoxLineOption设计时的画面:
第二步、添加代码
参照菜单操作的代码,给配置表中的组件编写代码,实现与菜单同样的功能。
双击cbBoxLine组件,参照mnPenSolidClick添加以下代码:
procedure TfrmMain.cbBoxLineChange(Sender: TObject); begingrdBox.PenStyle := TFPPenStyle(cbBoxLine.ItemIndex);grdBox.LineStyle := lsUsePen;grpBoxLine.ItemIndex := 2;grdBox.Repaint; end;
双击pnBoxColor组件,参照mnBoxColorClick添加以下代码:
procedure TfrmMain.pnBoxColorClick(Sender: TObject); vardlg: TColorDialog; begindlg := TColorDialog.Create(Self);trydlg.Color := grdBox.LineColor;if dlg.Execute thenbeginpnBoxColor.Color := dlg.Color;pnBoxColor.Caption := IntToHex(dlg.Color);grdBox.LineColor := dlg.Color;grdBox.Repaint;end;finallydlg.Free;end; end;
编译运行pTetris项目,可以体验到在配置表的图案选择页面的背景网格栏目中操作跟在主菜单视图下的操作有一样的效果,而在配置表中的操作更加直观、简洁。
5.结束语
在这篇文章中,我们通过修改方块移动区背景网格外观的操作演示了是使用菜单和设置界面实现同样目标的方法,初步了解了应用程序设计中功能分配方面的知识。在这篇文章中讲到的只是冰山一角,有一个大概的了解,希望能起到抛砖引玉的效果。