在Lazarus下的Free Pascal编程教程——使用画刷TBrush改变组件表面颜色或绘制图片

0.前言

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

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

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

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

在前面写的文章中我们已经构建了pTetris项目的框架,并逐步添加了一些功能,作为示例的应用程序俄罗斯方块游戏已经可以作为一个完整的游戏程序使用了。在这篇文章中我们将通过改变方块表面颜色和图案来使俄罗斯方块游戏变得“多彩”。
俄罗斯方块游戏中操作方块是核心。在前面的示例中我们已经让作为示例的应用程序俄罗斯方块游戏已经具有了游戏的全部功能,并提供了超出传统游戏的玩法,在这篇文章中我们将通过示例讲述在程序界面中使用画刷TBrush显示颜色和图片的方法。
在这篇文章里,我主要讲述以下几个方面的内容:

  1. 程序设计的界面显示中画刷TBrush的使用概述
  2. 在程序中使用画刷TBrush的策划
  3. 在程序中使用画刷TBrush改变方块颜色
  4. 在程序中使用画刷TBrush在方块表面显示画刷图案
  5. 在程序中使用画刷TBrush在方块表面显示图片
  6. 在程序中使用画刷TBrush显示图片的另一个例子
  7. 结束语

1.程序设计的界面显示中画刷TBrush的使用概述

任何操作系统下的图形界面设计中都用到两个工具:画笔Pen和画刷Brush,画刷和画笔都是在一个代表设备的DC上操作的。画笔用来写字、划线,画刷用来填充背景颜色或图案。
在Lazarus中,画笔对应的组件是TPen,画刷对应的组件是TBrush。
在Lazarus中,为所有可视的组件提供了一个叫做画布的TCanvas类成员,从而显示了设备无关性的图形输出。TPen和TBrush都作为画布TCanvas类的成员。
在这一篇文章中,我们将要讲述的就是画刷TBrush的使用。
在Lazarus的文档中有TBrush的介绍和示例:

TBrush - Lazarus wiki

在这篇文章中,介绍了TBrush的属性和用法,给出了示例。由于这篇文章是英文的,所以在这里选取其中的部分内容做一个简单的介绍。
TBrush主要有三个属性是我们常用的,这三个属性是:

  • Color:指定用于填充形状的基本颜色(类型 TColor)
  • Style :TBrushStyle 类型的枚举,定义用于填充的模式
  • Bitmap:定义图像,如果 Style 为 bsImage,则通过平铺来填充形状。此处可以使用从 TCustomBitmap 派生的类的任何实例(TBitmap、TJpegImage、TPortableNetworkgraphic 等)。

 在Lazarus中TBrushStyle的定义是这样的:

  TFPBrushStyle = (bsSolid, bsClear, bsHorizontal, bsVertical, bsFDiagonal,
                   bsBDiagonal, bsCross, bsDiagCross, bsImage, bsPattern); 

其中,

  • bsClear:完全没有填充,Color 被忽略。
  • bsSolid:使用指定的 Color 均匀填充形状
  • 以下样式定义以指定颜色绘制的填充图案。默认情况下,剖面线之间的背景留空。
    • bsHorizontalbsVerticalbsFDiagonal、bsBDiagonal 定义单线填充图案。
    • bsCross 和 bsDiagCross 定义交叉模式。
  • bsImage 和 bsPattern 平铺形状,并将图像设置为 Brush 的 Bitmap 属性。

 以下插图是《TBrush - Lazarus wiki》一文中的示例绘制的对应于TBrushStyle各个取值的效果图。在绘制示例图样时,Canvas.Brush.Color := clRed,bsImage 和 bsPattern绘制的图像来自一个图片文件,你可以用其他图片文件替换它。

 在下面的讲述中,我们将使用上面提到的这些属性在应用程序界面上绘图。

2.在程序中使用画刷TBrush的策划

2.1 在配置表中添加选项

2.1.1 在界面中添加组件

我们要做的是在移动区域正在移动的方块的表面上绘制多种效果的图案,从而形成多彩的效果。我们将提供多种类型的效果,为此在配置表中添加选项,用户可以使用选项决定得到什么样的界面效果。
在界面编辑器中给配置表的图案选择页面添加以下组件:

  • TDividerBevel组件,设置属性Caption为“方块表面”
  • TRadioGroup组件,设置属性Name为grpBoxFace,Caption为“方块表面图案类型”,Items为“单一颜色,画刷图案,图片文件”(分三行)
    • TComboBox组件,放在TRadioGroup组件内,设置设置属性Name为cmbPicFiles,Style为csDropDownList
  • TLabel组件用于添加说明,设置属性WordWrap为true,Caption为“如果选择了图片文件,需要在pTetris应用程序所在的文件夹中有可以使用的图片文件。有效的图片文件格式包括:png、jpg、bmp”

界面效果图如下:

2.1.2 添加响应界面选项的代码

在pTetris项目中, 方块盒子出现两个地方:方块提示区和方块移动区。我们的计划在方块移动区给移动的方块表面添加图案。实现添加图案的目标需要编写的代码包括:

  • 针对三种绘制图案的方式编制三个函数:BoxsColor、BoxsBrush、BoxsImage
  • 定义一个函数BoxsFace在方块表面绘制图案,在BoxsFace中,根据TRadioGroup中选中的项目选择使用以上三个函数之一

BoxsFace函数的代码如下:

procedure cxBoxMove.BoxsFace(bxs: cxBoxs);
begin
  case FConfig.ConfigData.GetValue('grpBoxFace/ItemIndex', 0) of
    1: BoxsBrush(bxs);
    2: BoxsImage(bxs);
    else
      BoxsColor(bxs);
  end;
end;

BoxsFace函数在哪里调用合适呢?按着我们以前完成的代码,开始移动盒子前在函数cxBoxMove.BoxsReady中从cxBoxQueue获得要移动的方块盒子。那么我们就在获得要移动的方块盒子后给方块绘制表面图案,也就是说在cxBoxMove.BoxsReady中调用函数BoxsFace,代码如下:

function cxBoxMove.BoxsReady: boolean;
var
  bxs: cxBoxs;
begin
  //准备当前移动的方块,如果没有则创建从队列中获取一个,并放置在移动区之上
  ......
  if not HasBoxs then
  begin
    bxs := Queue.NextBoxs;
    if assigned(bxs) then
    begin
     ......
      BoxsFace(bxs);
    end;
  end;
  ......
end; 

下面我们分别介绍在BoxsColor、BoxsBrush、BoxsImage三个函数中实现三种绘制图案的方法。

3.在程序中使用画刷TBrush改变方块颜色

游戏中在TRadioGroup中选择“单一颜色”会调用BoxsColor函数。
BoxsColor用单一的颜色填充方块的表面,也是就Brush.Style := bsSolid的情况。
方块类cxBox是以TPanel为父类创建的,从TPanel继承了一个Color属性。我们可以给这个Color属性赋值来设置方块表面的颜色,而不需要直接使用Brush。BoxsColor函数代码如下:

procedure cxBoxMove.BoxsColor(bxs: cxBoxs);
var
  clr: TColor;
  bx: cxBox;
  cmp: TComponent;
begin
  clr := Random($FFFFFF);
  for cmp in bxs do
  begin
    bx:=cmp as cxBox;
    bx.Color := clr;
    //bx.Brush.Style := bsSolid; //由于bsSolid是默认值,所以不需要这个语句
  end;
end;  

在以上代码中,我们使用了Random($FFFFFF)获得一个随机颜色,这样可以在移动区域看到不同颜色的方块,体现“多彩”的特性。
在Lazarus提供的源码中追溯cxBox的祖先类,在TCustomControl.SetColor中找到给Color属性赋值的操作,从代码中能够看出给Color属性赋值的操作本质上是给Canvas.Brush.Color赋值:

procedure TCustomControl.SetColor(Value: TColor);
begin
  if Value = Color then Exit;
  inherited SetColor(Value);
  Canvas.Brush.Color := Color;
end; 

游戏中选择单一颜色使用BoxsColor的效果如下图所示:

 

4.在程序中使用画刷TBrush在方块表面显示画刷图案

游戏中在TRadioGroup中选择“画刷图案”会调用BoxsBrush函数。
BoxsBrush用画刷图案填充方块的表面,也是就Brush.Style取值为bsHorizontal, bsVertical, bsFDiagonal,bsBDiagonal, bsCross, bsDiagCross之一的情况。图案的颜色使用Brush.Color设置。BoxsBrush函数的代码如下:

procedure cxBoxMove.BoxsBrush(bxs: cxBoxs);
var
  clr: TColor;
  bst: TFPBrushStyle;
  bx: cxBox;    
  cmp: TComponent;
begin
  clr := Random($FFFFFF);
  bst := TFPBrushStyle(Random(Ord(bsImage) - 2) + 2);
  for cmp in bxs do
  begin      
    bx:=cmp as cxBox;
    bx.Color := clr;
    bx.Brush.Style := bst;
  end;
end; 

游戏中选择画刷图案使用BoxsBrush的效果如下图所示:

 

从图片中我们可以看出画面虽然是多彩的,但画面并不美观。我们设置这种方式的目的是为了演示画刷TBrush的用法,感觉不美观我们在游戏的时候不选他就是了,哈哈哈。

5.在程序中使用画刷TBrush在方块表面显示图片

游戏中在TRadioGroup中选择“图片文件”会调用BoxsImage函数。
BoxsImage用图片文件中的画面填充方块的表面,也是就Brush.Style取值为bsImage或bsPattern的情况。在这种方式中Brush.Color没有意义。
使用BoxsImage要比前两种情况复杂一些,需要做以下工作:

  • 在运行pTetris应用程序之前要确保pTetris应用程序所在的文件夹中有可以使用的图片文件
  • 运行pTetris应用程序后首先要把可以使用的图片文件的名称添加到cmbPicFiles组件的列表中。为此我们编写了函数TfrmMain.PicFilesList
  • 在开始游戏时将要使用的图片文件读入内存中备用。我们选择保存在FImage中
  • 在BoxsImage中为方块指定画刷类型、指定OnPaint事件处理函数BoxPaint
  • 在BoxPaint中选取FImage中的部分画面绘制在方块的表面

 下面我们分别实现以上工作要求的程序编码。

 5.1 获得可以使用的图片文件名列表

按着我们的策划成果,用于绘制方块表面图案的图片文件要在组件cmbPicFiles的下拉列表中选择,这就需要我们事先将可以使用的图片文件名添加到cmbPicFiles.Items中。
Lazarus提供的丰富的文件操作函数,在FileUtil的FindAllFiles大大简化的我们的操作。FindAllFiles定义了两种形式,第二种更符合我们的要求。

function FindAllFiles(const SearchPath: String; const SearchMask: String = '';
  SearchSubDirs: Boolean = True; DirAttr: Word = faDirectory;
  MaskSeparator: char = ';'; PathSeparator: char = ';'): TStringList; overload;
procedure FindAllFiles(AList: TStrings; const SearchPath: String;
  const SearchMask: String = ''; SearchSubDirs: Boolean = True; DirAttr: Word = faDirectory;
  MaskSeparator: char = ';'; PathSeparator: char = ';'); overload;  

第二种形式的参数说明如下:

  • AList:TStringList 用于存储与搜索条件匹配的文件名。AList 必须先实例化,然后才能将其作为参数传递给方法。TStringList 实例也必须由创建它的例程释放。
  • SearchPath:搜索文件的基本路径。
  • SearchMask:文件掩码列表,用分号 (;) 分隔,用于确定哪些文件在例程中是匹配项。掩码可以包含通配符,如 * 和 ?它还支持像 [a-d,x] 这样的集合。有关更多详细信息,请参阅 Masks 单元。默认值为空字符串 (''),并导致使用平台的所有文件掩码。
  • SearchSubDirs:如果为 True,则在子目录中搜索匹配的文件。
  • DirAttr:指定 file 属性,该属性指示是否将文件系统条目视为目录。它可以包含faDirectory 、 faSymLink、 (faDirectory+ faSymLink) 或可以使用其他位。默认值为 faDirectory。
  • MaskSeparator:在 SearchMask 参数中的文件掩码之间使用的分隔符。默认值为 ';'。
  • PathSeparator:SearchPath 参数中路径名之间使用的分隔符。默认值为 ';'。

把图片文件名添加到cmbPicFiles.Items中的操作在函数PicFilesList中完成,其代码如下:

procedure TfrmMain.PicFilesList;
begin
  FindAllFiles(cmbPicFiles.Items, '', '*.png;*.jpg;*.jpeg;*.bmp', True);
end; 

那么,我们在哪里调用函数PicFilesList呢?根据以前编写代码的惯例,我们还是在FormShow中:

procedure TfrmMain.FormShow(Sender: TObject);
begin
  ......
  PicFilesList;
end; 

这样我们就可以在cmbPicFiles组件的下拉列表中选择我们要使用的图片文件了。不过,我们还想提供另一种选择图片文件的方法:随机选择。这样我们在游戏中就有了两种使用图片的方法:

  • 从cmbPicFiles组件的下拉列表中选择我们要使用的图片文件,这样可以使用我们最喜欢的图片
  • 如果没有从cmbPicFiles组件中选择,则应用程序会自动从可以使用的文件中随机选择一个图片文件,这样可以让游戏的画面更加多彩

为了达到这个目的,我们编写一个函数PicFileName来完成选择文件名的操作:

function TfrmMain.PicFileName: string;
var
  indx: integer;
begin
  Result := '';
  if cmbPicFiles.Items.Count > 0 then
  begin
    if cmbPicFiles.ItemIndex < 0 then
      indx := Random(cmbPicFiles.Items.Count)
    else
      indx := cmbPicFiles.ItemIndex;
    Result := cmbPicFiles.Items[indx];
  end;
end;

5.2 读取图片文件到内存中备用

要把图片文件读入内存中,首先要为图片文件设置一个容器。为此,我们先定义一个属性Image。由于要在cxBoxMove中使用,我们就把定义放在cxBoxMove类中:

    property Image: TCustomBitmap read FImage write SetImage; 

在SetImage函数中,我们在保存Image的新值前释放了原有的对象,这样做也是为了简化使用Image的操作,我们可以使用Image:=nil来释放Image保存的对象。SetImage的实现代码如下:

procedure cxBoxMove.SetImage(AValue: TCustomBitmap);
begin
  if FImage=AValue then Exit;
  if Assigned(FImage) then
    FreeAndNil(FImage);
  FImage:=AValue;
end; 

我们定义函数LoadImage读入图片文件保存在Image中,代码如下:

procedure cxBoxMove.LoadImage(aFilename: string);
var
  ext: string;
begin
  ext := ExtractFileExt(aFilename);
  case IndexText(ext, ['.png', '.jpg', '.jpeg']) of
    0: Image := TPortableNetworkGraphic.Create;
    1, 2: Image := TJpegImage.Create;else
      Image := TBitmap.Create;
  end;
  try
    Image.LoadFromFile(aFilename);
  except
    Image := nil;
  end;
end; 

从代码中看出,不同类型的图片文件需要使用不同的对象来读取。如果你想使用更多类型的文件,如果TBitmap不能识别,就需要你在这个函数里添加相应的对象,当然,不是所有类型的图片文件都适合我们的应用程序。由于TBitmap作为最后的保障,所以,虽然IndexText的列表中没有包含'.bmp',但还是可以使用bmp文件的。
除了我们使用的LoadImage函数的方法,我们还可以参照TPicture.LoadFromFile的方法实现同样的功能,甚至可以直接使用TPicture来读取和管理图形文件的数据。
按着我们策划的结果,需要在游戏开始时将图形文件的数据读入Image中,这就需要在GameBegin中添加以下代码:

procedure TfrmMain.GameBegin;
begin
  ......
  if grpBoxFace.ItemIndex=2 then
  begin
    boxMove.LoadImage(PicFileName);
  end;
  ......
end;      

在游戏结束后不再使用这个图片,需要释放保存图片数据的对象,这就需要在GameEnd中添加以下代码:

procedure TfrmMain.GameEnd;
begin
  ......
  { #todo : 添加游戏停止后需要做的事情 }
  boxMove.Image := nil;
  ......
end; 

5.3 在移动的方块表面绘制图像

在前面的叙述中已经说明了在方块表面绘制图像是在BoxsImage中实现的,但仅有BoxsImage函数还不够,还需要方块组件的OnPaint事件处理函数。我们编写代码需要实现的功能包括以下内容:

  • 在BoxsImage函数中为方块的cxBox对象的Brush.Style 指定为 bsImage,OnPaint指向BoxPaint。如果要一组方块自始至终保持同样的图案,则需要在BoxsImage中指定要绘制图像的坐标
  • 在BoxPaint将图像画在方块的表面。如果要让方块在移动的过程中表面的图案不断变化,则需要在BoxPaint中指定要绘制图像的坐标

我们在BoxsImage函数中考虑了读取图形文件失败的情况。BoxsImage的代码如下:

procedure cxBoxMove.BoxsImage(bxs: cxBoxs); 
var
  bx: cxBox;
  cmp: TComponent;
begin
  if Assigned(FImage) then
  begin
    for cmp in bxs do
    begin
      bx := cmp as cxBox;
      bx.Brush.Style := bsImage;
      bx.OnPaint := @BoxPaint;
    end;
  end
  else
  begin
    BoxsColor(bxs);
  end;
end; 

我们选用的图片可能会比方块表面大,我们在图像中随机选择一部分用来绘制在方块表面。BoxPaint函数的代码如下:

procedure cxBoxMove.BoxPaint(Sender: TObject);
var
  bx: cxBox;
  mx, my, cx, cy,dx,dy: integer;
  rcb, rcs: TRect;
begin
  if not Assigned(FImage) then Exit;

  mx := FImage.Width div 16;
  my := FImage.Height div 16;
  cx := Random(mx);
  cy := Random(my);
  dx := (FImage.Width - mx * 16) div 2;
  dy := (FImage.Height - my * 16) div 2;

  bx := Sender as cxBox;
  rcb := bx.ClientRect;
  rcs := rcb;
  OffsetRect(rcs, dx + cx * 16, dy + cy * 16);
  bx.Canvas.CopyRect(rcb, FImage.Canvas, rcs);
end;  

完成以上代码之后,我们在pTetris中玩游戏时,可以使移动的方块变得丰富多彩,初步实现了“多彩俄罗斯方块”的设想。

       

6.在程序中使用画刷TBrush显示图片的另一个例子

由于方块面积很小,在方块表面绘图使用的是图片文件的局部。有时候我们想看到使用图像的更大的画面甚至全部,现在我们就实现这个愿望,把图像的更多画面显示在方块移动区域外围的面板上。
在这里我们要演示的是使用图像填充程序界面的背景。

6.1 在配置表中添加选项

把图像画在面板上并不一定是美观的,所以我们在配置表中添加一个开关选项,让用户来决定是否要把图像画出来。在这里只需要一个组件就可以实现我们的目标:

  • TCheckBox组件,设置属性Name为ckMiddlePicture,Caption为“在窗口背景上绘制用于方块表面的图片”

只有当这个组件选中时才会把图像画出来。

6.2 在面板表面绘制图像

在面板表面绘制图像跟在方块表面绘制图像需要做的工作一样,只是绘图时使用的函数不同。我们需要编写的代码要完成以下工作:

  • 在GameBegin中将面板pnMiddle的OnPaint指向MiddlePaint
  • 在MiddlePaint将图像绘制在面板pnMiddle上

在GameBegin中添加以下代码中的红色代码:

procedure TfrmMain.GameBegin;
begin
  ......
  if grpBoxFace.ItemIndex=2 then
  begin
    boxMove.LoadImage(PicFileName);
  end;
  //在主窗口背景上绘制用于方块的图片
  if ckMiddlePicture.Checked and (boxMove.Image <> nil) then
  begin
    pnMiddle.OnPaint := @MiddlePaint;
    pnMiddle.Invalidate;
  end
  else
  begin
    pnMiddle.OnPaint := nil;
    pnMiddle.Invalidate;
  end;
  ......
end;      

在MiddlePaint中我们使用了FillRect在pnMiddle表面绘制图像。当图像小于面板时,使用图像排列填充整个画面。MiddlePaint的代码如下:

procedure TfrmMain.MiddlePaint(Sender: TObject);
var
  rc: TRect;
  dr: integer;
begin
  rc := pnMiddle.ClientRect;
  dr := pnMiddle.BevelWidth*2;
  InflateRect(rc, -dr, -dr);
  Inc(rc.Top,pnMiddle.Height div 4);
  with pnMiddle do
  begin
    Canvas.Brush.Style := bsPattern;
    Canvas.Brush.Bitmap := boxMove.Image; 
    //Canvas.Pen.Style:=psClear;
    //Canvas.Rectangle(rc);
    Canvas.FillRect(rc);
  end;
end; 

使用FillRect和Rectangle都可以在矩形区域使用画刷TBrush填充画面,二者的区别是FillRect没有边线,而Rectangle有边线。

7.结束语

在应用程序中显示图像是很常见的事情。使用图像可以美化应用程序的界面,给用户带来不一样的美感。
具有图形界面的操作系统提供了丰富的绘制图像的方法,使用画刷只是这些方法中的一种。有兴趣的朋友可以进一步探讨使用图像的方法。
在应用程序中使用图像可以带来美感,但由于图像文件占用很大的内存空间,所以在使用的时候要谨慎。

posted @ 2025-04-15 23:59  lexyao  阅读(154)  评论(0)    收藏  举报