在Lazarus下的Free Pascal编程教程——在应用程序中使用鼠标和键盘输入

0.前言

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

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

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

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

在前面写的文章中我们已经构建了pTetris项目的框架,并逐步添加了一些功能,作为示例的应用程序俄罗斯方块游戏已经达到了可玩的程度。在这篇文章中我们将通过示例讲述在应用程序中使用鼠标和键盘输入的知识。

俄罗斯方块游戏中操作方块是核心。在前面的示例中我们已经让作为示例的应用程序俄罗斯方块游戏已经达到了可玩的程度,但只是使用鼠标操作,在这篇文章中我们将增加使用键盘操作的功能。

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

  1. 计算机输入设备概述
  2. Lazarus中使用键盘输入的方法
  3. 给pTetris项目添加键盘输入的操作方法
  4. 给pTetris项目添加定制键盘输入的途径
  5. 给pTetris增加一种使用鼠标的玩法
  6. 结束语

1.计算机输入设备概述

计算机输入设备是指向计算机输入数据和信息的设备,是计算机与用户或其他设备通信的桥梁。输入设备是用户和计算机系统之间进行信息交换的主要装置之一。键盘,鼠标,摄像头,扫描仪,光笔,手写输入板,游戏杆,语音输入装置等都属于输入设备。输入设备是人或外部与计算机进行交互的一种装置,用于把原始数据和处理这些数的程序输入到计算机中。计算机能够接收各种各样的数据,既可以是数值型的数据,也可以是各种非数值型的数据,如图形、图像、声音等都可以通过不同类型的输入设备输入到计算机中,进行存储、处理和输出。
计算机的输入设备按功能可分为下列几类:

  • 字符输入设备:键盘;
  • 光学阅读设备:光学标记阅读机,光学字符阅读机;
  • 图形输入设备:鼠标器、操纵杆、光笔;
  • 图像输入设备:摄像机、扫描仪、传真机;
  • 模拟输入设备:语言模数转换识别系统

还有一些设备是兼具输入设备和输出设备两方面的功能的,比如:磁带机、磁盘等存储设备,网络通讯设备等,由于这些设备更偏重于其他的功能,通常在称呼上更偏重于他们的主要功能。
在早期的时候,计算机的数据输入使用的是一种纸带,在纸带上用打孔机打孔,然后用纸带输入机将纸带上的数据传输给计算机。后来有了更先进的磁带机,纸带机才逐渐被取代。我使用的第一台计算机就是用磁带机存储数据的,我编写的应用程序也是保存在磁带上的。再后来有了容量更太大、速度更快、存取更方便的磁盘,磁带机也就完成了它的使命光荣退休了。

从出生一直在计算机输入设备中占据重要地位且长盛不衰的恐怕只有键盘和鼠标了。
我在开始使用计算机的很多年里只是在一些英文资料里看到使用计算机时用到一只耗子,是什么样的耗子可以使用计算机呢?那个时候没有互联网,去过的书店中没有介绍耗子的内容。在一篇文章中看到“新加坡的中学生都是使用鼠标的高手(Secondary school students in Singapore are masters of using mouse)”,为什么在国内的资料中看不到介绍呢?不过,随着计算机操作系统的飞速更新换代,Windows成了计算机主要的操作系统,鼠标也就成了计算机的标配,再也不会有人不认识鼠标了。
鼠标的使用是有局限性的,不管它多么重要,到目前为止,还没有一种设备能够完全代替键盘。在多数的应用程序中,键盘仍然保持着它的霸主地位。
作为一个程序员,如何在应用程序中使用键盘输入是一个很重要的能力,有可能成为影响应用程序成败的关键因素。
使用键盘的方法可以归纳为两大类:

  • 输入一段文字:可以是一篇文章、一个用户名、一个密码、一个文字型的命令(命令行)等
  • 键盘指令:按下单个键或者组合键发出一个指令,使用效果与用鼠标点击菜单或者按钮的效果相同。由于使用键盘发出同样的指令比使用鼠标点击菜单、按钮更加方便快捷,所以把键盘的这种使用方式叫做加速键或者快捷键

2.Lazarus中使用键盘输入的方法

在Lazarus中,输入一段文字的方法是系统内定的,已经有许多组件能够直接使用,可以输入文字或者数字,甚至是一篇文章、一张表格。在这里我们想讨论的是键盘指令的用法,也就是快捷键。
在Lazarus中使用快捷键的方法有以下几种:

  • 操作系统设定的快捷键:这一类快捷键对于任何应用程序都适用,与编程语言无关。比如按下Alt+F4可以关闭当前窗口
  • Lazarus系统内定的快捷键:这是Lazarus核心代码或者组件代码中内定的快捷键,有些是Lazarus/Delphi自己添加的,有些是为了与其他应用程序兼容模仿设置的,比如使用Tab键、箭头键在组件之间切换焦点
  • 菜单默认的快捷键:在定义菜单项时,只要在一个字母前加&符号,应用程序运行时就会在这个字母下添加一个下划线,这个字母就成了这个菜单项的快捷键。不过,在Windows11中做了一些更改,只有按下Alt键时这些快捷键的下划线才会显示出来
  • 设计时为菜单项添加的快捷键:很多应用程序的菜单项的右边标有一个组合键,这就是菜单项的快捷键。在Lazarus中,设计时选中这个菜单项,在属性列表中给ShortCut选择一个组合键,这个组合键就是这个菜单项的快捷键
  • 键盘事件:很多组件都有按键事件,编写按键事件的代码就可以接收键盘输入,在识别到特定的按键时执行某一个操作,这个按键就相当于这个操作的快捷键

为菜单项设置快捷键的做法有一定的局限性,只能使用系统预置的组合键。记得以前我曾经在网上得到一段小程序,在Delphi中使用这段小程序可以在运行时为应用程序设置任意按键为快捷键。不过我用过一次之后没有再使用,不知道把它放到哪里了,想在网上找,不记得名字也没办法搜索了,有点小遗憾。
键盘事件是使用键盘输入的常规手段,无论哪一钟语言编写的程序都是使用键盘事件的。要使用键盘事件,就要了解键盘是怎样工作的,在这一方面可以百度一下会找到很多答案和示例。
作为Lazarus的用户,我们还是要看一看Lazarus文档是怎么介绍的。以下关于按键事件的链接:

LCL Key Handling - Lazarus wiki:Lazarus的wiki中关于键盘事件的介绍

Understanding and Processing Keyboard Events in Delphi:Delphi 中的键盘事件描述,也适用于 Lazarus

可惜是英文版的,没有中文版本。由于一些关键词的特殊性,这篇文章用电脑自动翻译会让人看到啼笑皆非的文字。为了弥补这个遗憾,在这里我对Lazarus的按键事件做简单的介绍。
Lazarus和Delphi在TWinControl类中定义了三个属性作为按键事件处理函数的接口:

  • property OnKeyDown: TKeyEven;用户在键盘上按下键时发生的事件,可以得到键盘的编码,在Lazarus中为每个按键定义了VK_XXX形式的常量编码。在按键事件中使用最多的是OnKeyDown。Lazarus的wiki中的示例:key down - Lazarus wiki
  • property OnKeyPress: TKeyPressEvent;用户按下字符或数字键后发生的事件,可以得到一个字符。Lazarus的wiki中的示例:OnKeyPress - Lazarus wiki
  • property OnKeyUp: TKeyEvent;用户释放按键时发生的事件,得到的同样是VK_XXX形式的常量编码。与OnKeyDown的使用方法是一样的,只是事件发生的事件晚了一点

Lazarus在TCustomForm和TApplication类中还定义了一个属性作为按键事件处理函数的接口:

  • property OnShortcut: TShortcutEvent;这是一个具有很高优先级的按键事件,在所有组件的按键事件之前发生。以下链接是Lazarus帮助文档关于这个事件的介绍:

VK_XXX形式的常量编码可以从下面链接中看到:

Reference for unit 'LCLType': Constants

virtual keyboard strokes/de - Lazarus wiki(德语)

生成 KeyDown/Char/KeyUp 的键

  • “简单”字母 (a..z)
  • 数字
  • Esc
  • 退格键Backspace
  • 空格键
  • 回车/换行键Return(Enter)
  • 数字键盘运算符(加、减、乘、除)
  • 数字键盘上的数字
  • 数字键盘上的小数分隔符

仅生成 KeyDown/KeyUp 的键

  • 功能键 (F1-F12)
  • 打印屏幕Print Screen
  • Scroll Lock
  • 暂停
  • Shift
  • 大写锁定Caps Lock
  • Tab
  • Ctrl或Control
  • Windows 徽标键
  • Alt
  • 插入Insert
  • 删除Delete或Del
  • Home
  • End
  • Page Up
  • Page Down
  • 箭头键 (向上、向下、向左、向右)
  • Num Lock

以下是两段示例代码:

复制代码
uses
  ...LCLType, Dialogs, ...;
  ...  
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  // 示例:检查简单键:
  if (Key = VK_DOWN) or
     (Key = VK_UP) then
    ShowMessage('按下箭头键 up 或 down');
  // Check for Alt-F2
  if (Key = VK_F2) and (ssAlt in Shift) then
    ShowMessage('Alt F2 组合键按下')
  Key := 0; // 对于某些 widgetset(例如 Cocoa)是必需的,以便在后续元素中禁用处理。
end;

procedure TMainForm.FormKeyPress(Sender: TObject; var Key: char);
begin
  case key of
    '0': ShowMessage('"0" 键按下');
    '1': ShowMessage('"1" 键按下');
    '2': ShowMessage('"2" 键按下');
  end;
  Key := #0; //  对于某些 widgetset(例如 Cocoa)是必需的,以便在后续元素中禁用处理。
end;
复制代码

3.给pTetris项目添加键盘输入的操作方法

按着前面的介绍,我们可以使用TfrmMain的FormKeyDown或FormKeyPress。
我们从事件列表中添加事件处理函数TfrmMain.FormKeyDown,并添加以下代码:

复制代码
procedure TfrmMain.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  case Key of
    VK_UP: btnRotate.Click;
    VK_DOWN: btnDown.Click;
    VK_LEFT: btnLeft.Click;
    VK_RIGHT: btnRight.Click;   
  end;
end; 
复制代码

编译运行pTetris项目后按箭头键却没有让方块按着我们的意图移动。为什么会这样呢?开始的时候我也很纳闷,测试了很久才发现了问题的所在:当我在按箭头键的时候方块盒子的运动没有受到影响,而“开始游戏”和“声音开关”两个按钮随着我的按键在切换输入焦点。这说明按钮切换焦点的优先级在FormKeyDown之前。

 我又从事件列表中添加事件处理函数TfrmMain.FormKeyPress,并添加以下代码:

复制代码
procedure TfrmMain.FormKeyPress(Sender: TObject; var Key: char);
begin
  case Key of
    '5': btnRotate.Click;
    '2': btnDown.Click;
    '1': btnLeft.Click;
    '3': btnRight.Click;
    '6': btnDowned.Click;
  end;
end; 
复制代码

编译运行pTetris项目后按小键盘上的数字键同样没有让方块按着我们的意图移动。为什么会这样呢?我想:应该是点击“开始游戏”按钮后这个按钮获得了输入焦点,而我写的代码是窗体的按键事件。是不是给“开始游戏”按钮添加按键事件处理程序就可以了呢?
选中btnStart按钮,在OnKeyDown事件的下拉列表中选择FormKeyDown,由于所有组件的按键事件处理函数是通用的,所以开始游戏按钮可以直接使用窗体的按键函数。
编译运行pTetris项目后按箭头键,发现两个按钮还在切换焦点,不过游戏中的方块盒子也在按着按键的指示移动了。这说明前面的猜想是对的。
用同样的方法给btnStart按钮添加FormKeyPress函数,编译运行pTetris项目后按小键盘上的数字键,方块盒子按着我们的意图移动了。不仅如此,使用大键盘上的数字也有同样的效果,这说明两个地方输入的数字是一样的字符。
为了测试,我去掉了btnStart按钮的FormKeyPress函数,在FormKeyDown中添加了以下代码:

    VK_NUMPAD5: btnRotate.Click;
    VK_NUMPAD2: btnDown.Click;
    VK_NUMPAD1: btnLeft.Click;
    VK_NUMPAD3: btnRight.Click;
    VK_NUMPAD6: btnDowned.Click;  

编译运行pTetris项目后按小键盘上的数字键,方块盒子按着我们的意图移动了,按大键盘上的数字键却没有效果。这是为什么呢?原因是这样的:

  • 使用FormKeyPress事件,大键盘和小键盘的数字键得到的都是数字字符,两个没有差别
  • 使用FormKeyDown事件,按下大键盘的数字键1,得到的是VK_1(数值是49),而按下小键盘的数字键1,得到的是VK_NUMPAD1(数值是96),二者是截然不同的两个数值

问题的原因找到了,怎么解决这个问题呢?总不能把界面中所有可以得到输入焦点的组件都加上FormKeyPress或者FormKeyDown吧?这样做虽然能够解决问题,但这样做是不合理的。我们应该选择合理的解决方法。
什么是合理的方法呢?
我们应该还记得前面提到的OnShortCut,这个事件具有最高的优先级,是不是在按钮切换焦点之前呢?是不是与输入焦点的组件有关呢?
我们先把窗体和开始游戏按钮的按键事件处理函数都去掉:不用删除代码,只需要在事件列表中点击事件后面的下拉列表中选择(none)就行了。
然后我们在事件列表中给TfrmMain添加OnShortCut函数,添加如下代码:

复制代码
procedure TfrmMain.FormShortCut(var Msg: TLMKey; var Handled: Boolean);
begin
  case Msg.CharCode of
    VK_NUMPAD5: btnRotate.Click;
    VK_NUMPAD2: btnDown.Click;
    VK_NUMPAD1: btnLeft.Click;
    VK_NUMPAD3: btnRight.Click;
    VK_NUMPAD6: btnDowned.Click;
    VK_UP: btnRotate.Click;
    VK_DOWN: btnDown.Click;
    VK_LEFT: btnLeft.Click;
    VK_RIGHT: btnRight.Click;
  end;
end;  
复制代码

在代码中,我们把箭头键和数字键盘中的数字键都添加上了。编译运行pTetris项目后进行测试:

  • 按小键盘上的数字键,方块盒子按着指令移动,但按键一次盒子移动两格
  • 按箭头键,方块盒子按着指令移动,按键一次盒子移动一格,同时开始按钮和声音开关按钮也在切换输入焦点

为什么会这样呢?在上面代码的函数参数中有一个Handled,我们没有处理这个参数。这个参数有什么作用呢?字面意思。Handled是Handle的完成时,意思是已经处理过了。也就是说我们需要设置Handled的值为true,表示按键事件已经处理过了,避免重复处理。
由于OnShortCut有很高的优先级,如果简单地设置Handled的值为true将会屏蔽所有的按键事件,所以需要选择我们使用的按键,而不会影响其他的按键。
作为测试,我们在OnShortCut中添加以下代码,仅表示处理的小数字键盘的按键事件:

  if (Msg.CharCode >= VK_NUMPAD1) and (Msg.CharCode <= VK_NUMPAD6) then
    Handled := True; 

编译运行pTetris项目后进行测试:

  • 按小键盘上的数字键,方块盒子按着指令移动,但按键一次盒子移动一格。正常了
  • 按箭头键,方块盒子按着指令移动,按键一次盒子移动一格,同时开始按钮和声音开关按钮也在切换输入焦点。跟以前一样

这说明我们的分析是对的。确定了之后,我们也编写出TfrmMain.FormShortCut最后使用的代码:

复制代码
procedure TfrmMain.FormShortCut(var Msg: TLMKey; var Handled: Boolean);
begin
  case Msg.CharCode of
    VK_NUMPAD1, VK_LEFT:  //小键盘数字1,箭头←
    begin
      btnLeft.Click;
      Handled := True;
    end;
    VK_NUMPAD3, VK_RIGHT: //小键盘数字2,箭头→
    begin
      btnRight.Click;
      Handled := True;
    end;
    VK_NUMPAD2, VK_DOWN: //小键盘数字2,箭头↓
    begin
      btnDown.Click;
      Handled := True;
    end;
    VK_NUMPAD5, VK_UP, VK_CONTROL: //小键盘数字5,箭头↑,Ctrl键
    begin
      btnRotate.Click;
      Handled := True;
    end;
    VK_NUMPAD6, VK_NEXT, VK_SPACE: //小键盘数字6,Page Down键,空格键
    begin
      btnDowned.Click;
      Handled := True;
    end;
  end;
end;   
复制代码

在以上代码中,我们为每一种操作添加了两到三个按键,使用的时候可以按着自己的习惯选择使用。

4.给pTetris项目添加定制键盘输入的途径

在前面我们已经在TfrmMain.FormShortCut中添加了两种以上的按键,用户可以根据自己的习惯选择使用。如果想要更多的选择怎么办呢?
既然我们的目标是可定制的游戏,当然也应该包括按键的定制。
要想定制操作游戏的按键,需要有以下的操作步骤:

  • 在窗体设计器中为PageControl1再添加一个页面,命名为tbsKey,标题改为“按键设置”
  • 在tbsKey页面中添加设置按键的选项,让用户可以在这里为每一个操作定制自己喜欢的按键(将来在设置表中设置的项目都会保存在配置文件中,每次运行都会自动调入,所以设置是长期有效的)
  • 添加一个函数,可以获得设置的按键值
  • 修改TfrmMain.FormShortCut中的代码,添加能够识别通过函数识别设置的按键,从而执行用户按着定制的按键操作

在这里我不想现在编写这些代码,有兴趣的朋友可以自己尝试一下。

5.给pTetris增加一种使用鼠标的玩法

5.1 使用鼠标的方案策划

鼠标跟键盘同样是重要的输入设备,有些人喜欢用鼠标而不习惯使用键盘。特别是那些不经常使用键盘,练就了一指禅功夫的人,更是希望自己使用的是鼠标而不是键盘。
在这里,我想让我们的pTetris项目可以不使用键盘就可以玩。或许你会说,在开始的时候不就是使用鼠标的吗?是的。不过,那时使用鼠标是点击按钮,需要来回移动鼠标。在这个以毫秒为计时单位的游戏中,移动鼠标意味着什么是不言而喻的,所以我想给pTetris项目增加一种使用鼠标的玩法——我们要做的是不移动鼠标就可以玩游戏。
常规的鼠标有左右两个按键和一个滚轮,而滚轮可以向两个方向转动,还可以按下,这样鼠标就有了五个动作。我们计划这样分配这个五动作的功能:

  • 按下鼠标左/右键可以让方块左移/右移
  • 按下鼠标滚轮可以让方块跌落
  • 鼠标滚轮向上转动可以让方块旋转
  • 鼠标滚轮向下转动可以让方块向下移动

这样的分配涵盖了移动方块盒子需要的全部动作,这样用户就可以使用鼠标操作游戏,再也不用在键盘上练习一指禅了。

5.2 Lazarus鼠标事件的支持

有了这样的设想,怎么实现呢?
先不着急,我们做一些准备工作,了解一下Lazarus鼠标事件的支持,然后从中选择合适的事件添加事件处理函数就可以了。
以下是Lazarus在TControl类中为鼠标事件定义的属性,从名字上就可以看出,都是鼠标事件,这些事件中肯定有满足我们需要的。

复制代码
    property OnMouseDown: TMouseEvent read FOnMouseDown write FOnMouseDown;
    property OnMouseMove: TMouseMoveEvent read FOnMouseMove write FOnMouseMove;
    property OnMouseUp: TMouseEvent read FOnMouseUp write FOnMouseUp;
    property OnMouseEnter: TNotifyEvent read FOnMouseEnter write FOnMouseEnter;
    property OnMouseLeave: TNotifyEvent read FOnMouseLeave write FOnMouseLeave;
    property OnMouseWheel: TMouseWheelEvent read FOnMouseWheel write FOnMouseWheel;
    property OnMouseWheelDown: TMouseWheelUpDownEvent read FOnMouseWheelDown write FOnMouseWheelDown;
    property OnMouseWheelUp: TMouseWheelUpDownEvent read FOnMouseWheelUp write FOnMouseWheelUp;
    property OnMouseWheelHorz: TMouseWheelEvent read FOnMouseWheelHorz write FOnMouseWheelHorz;
    property OnMouseWheelLeft: TMouseWheelUpDownEvent read FOnMouseWheelLeft write FOnMouseWheelLeft;
    property OnMouseWheelRight: TMouseWheelUpDownEvent read FOnMouseWheelRight write FOnMouseWheelRight;
复制代码

以下是Lazarus中鼠标事件的函数类型定义:

  TMouseEvent = procedure(Sender: TObject; Button: TMouseButton;
                          Shift: TShiftState; X, Y: Integer) of Object;
  TMouseMoveEvent = procedure(Sender: TObject; Shift: TShiftState;
                              X, Y: Integer) of Object;
  TMouseWheelEvent = procedure(Sender: TObject; Shift: TShiftState;
         WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean) of Object;
  TMouseWheelUpDownEvent = procedure(Sender: TObject;
          Shift: TShiftState; MousePos: TPoint; var Handled: Boolean) of Object; 

这些函数的参数意义如下:

  • TMouseEvent = procedure(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer) of Object;
    • Sender: TObject 获取事件通知。
    • Button: 用于事件通知的鼠标按钮。
      • TMouseButton = (mbLeft, mbRight, mbMiddle, mbExtra1, mbExtra2); 
    • Shift:  应用于鼠标按钮的修饰符。
      • TShiftStateEnum = (ssShift, ssAlt, ssCtrl,ssLeft, ssRight, ssMiddle, ssDouble,ssMeta, ssSuper, ssHyper, ssAltGr, ssCaps, ssNum,ssScroll,ssTriple,ssQuad,ssExtra1,ssExtra2); 
      • TShiftState = set of TShiftStateEnum; 
    • X:      鼠标光标在 button 事件中的水平位置。
    • Y:      鼠标光标在 button 事件中的垂直位置。
  • TMouseMoveEvent = procedure(Sender: TObject; Shift: TShiftState; X, Y: Integer) of Object;
    • 同名参数意义同上
  • TMouseWheelEvent = procedure(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean) of Object;
    • 同名参数意义同上
    • WheelDelta: 滚轮已经转动了多少个槽口。向前推动滚轮为正数,反之为负数
    • MousePos:   鼠标位置,以客户端坐标为单位 
    • Handled:    在处理事件时,将 Handled 设置为 True
  • TMouseWheelUpDownEvent = procedure(Sender: TObject; Shift: TShiftState; MousePos: TPoint; var Handled: Boolean) of Object;
    • 同名参数意义同上

我们现在从Lazarus文档中看一看这些事件属性定义是怎么解释的,这样使用的时候就知道哪一个更适合我们了:

  • OnMouseDown: TMouseEvent; // 事件处理程序 在处理控件的鼠标按下事件时发出信号。
  • OnMouseEnter: TNotifyEvent; // 当鼠标指针进入控件时发出信号的事件处理程序。
  • OnMouseLeave: TNotifyEvent; // 事件处理程序在鼠标指针离开控件时发出信号。
  • OnMouseMove: TMouseMoveEvent; // 事件处理程序在控件中移动鼠标指针时发出信号。
  • OnMouseUp: TMouseEvent; // 事件处理程序 在处理控件的鼠标松开事件时发出信号。
  • OnMouseWheel: TMouseWheelEvent; // 鼠标滚轮的事件处理程序已转动。
  • OnMouseWheelDown: TMouseWheelUpDownEvent; // 事件处理程序,指示鼠标滚轮向下移动。
  • OnMouseWheelUp: TMouseWheelUpDownEvent; // 事件处理程序发出鼠标滚轮向上移动的信号。

与鼠标事件有关的还有一个数据类型。

TMouseEvent:这是鼠标单元的中心类型,用于描述所有鼠标事件。该字段描述事件发生时哪些按钮已关闭。这些字段描述事件在屏幕上发生的位置。这描述了事件发生时正在进行的操作。

  TMouseEvent = packed record
    Buttons: word;         //事件发生时按下的按钮
    x: word;               //鼠标光标的水平位置
    y: word;               //鼠标光标的垂直位置。
    Action: word;          //事件类型。
  end;  

5.3 用鼠标移动方块的实现

 了解了Lazarus对鼠标事件的支持,我们在就开始利用这些事件来达到我们的目标。
这里还有一个问题:Lazarus的鼠标事件是在TControl中定义的,这意味着所有的TControl后代组件都支持鼠标事件,我们的代码添加在哪一个组件中呢?
我想,我们还是先从基层做起,先添加到处于基层的主窗体,如果有问题再想办法解决。

5.3.1 用鼠标滚动事件实现方块盒子的旋转和下落

从鼠标事件的解释中看到鼠标滚轮的上下转动是最简单的,我们就先从他们开始。
在OnMouseWheelUp中让方块旋转,在OnMouseWheelDown中让方块下落,代码如下:

复制代码
procedure TfrmMain.FormMouseWheelDown(Sender: TObject; Shift: TShiftState;
  MousePos: TPoint; var Handled: Boolean);
begin
  btnDown.Click;
  Handled := True;
end;

procedure TfrmMain.FormMouseWheelUp(Sender: TObject; Shift: TShiftState;
  MousePos: TPoint; var Handled: boolean);
begin
  btnRotate.Click;
  Handled := True;
end; 
复制代码

我们没有编写新的移动盒子的代码,直接调用btnDown和btnRotate的Click函数,这是使用其他组件的事件的一种方法。向按钮发送消息也是一种可用的方法,不过没有直接调用Click函数简单。
编译运行pTetris项目,开始运行后转动鼠标滚轮,看正在移动到方块盒子的效果。正如我们所想的,转动滚轮可以让方块盒子转动或下落。

5.3.2 用鼠标按下事件实现方块盒子的左右移动和跌落

有了上一个经验,我们可以直接做下一个动作了。不过这一个要复杂一些。
适合的事件有OnMouseDown和OnMouseUp,我们习惯在按下的时候发生动作就使用OnMouseDown,如果想在鼠标按键抬起的时候发生动作就使用OnMouseUp。这里我们选用OnMouseDown。
OnMouseDown的参数中Button参数的三个取值符合我们的目标:mbLeft, mbRight, mbMiddle。左右中三个按键动作对应我们需要的左移、右移、跌落。代码如下:

复制代码
procedure TfrmMain.FormMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  case Button of
    mbLeft: btnLeft.Click;
    mbRight: btnRight.Click;
    mbMiddle: btnDowned.Click;
  end;
end; 
复制代码

编译运行pTetris项目,开始运行后分别按动鼠标的左右按键和滚轮,看正在移动到方块盒子的效果。这次让我们失望了,方块盒子没有受到鼠标按键的影响。
这个问题不难理解:窗体处于最底层,如果窗体拦截了鼠标按键事件,那么在窗体之上的其他组件就无法接收到鼠标按键信号,组件依赖的鼠标点击事件就无法实现了。
明白了这个道理,我们就把鼠标按下事件添加到表层组件中。设计时可以使用的最合适的应该是作为游戏区域载体的pnMiddle了。
选中pnMiddle,在事件列表中设置pnMiddle的OnMouseDown事件指向FormMouseDown函数。完成设置后编译运行pTetris项目,开始运行后分别按动鼠标的左右按键和滚轮,看正在移动到方块盒子的效果。这时我们发现,鼠标指针在pnMiddle的外露区域按下鼠标按键的时候方块盒子会按着我们预想的方式左右移动或跌落,而在被其他组件遮挡的区域或者pnMiddle范围之外时就没有效果了。由此我们也就明白了OnMouseDown的适用场所:必须是在组件本身不被遮挡的区域按动鼠标按键才会收到按键信号。

5.3.3 给鼠标移动区添加OnMouseDown事件实现方块盒子的左右移动和跌落

pnMiddle可以使用鼠标按键了,而作为方块移动区域的grdBox不能使用这是不和情理的,所以我们要给grdBox添加OnMouseDown事件,让鼠标在方块移动区时可以通过鼠标按键移动方块盒子。
grdBox是在运行时创建的,我们需要在代码中设置grdBox的OnMouseDown指向FormMouseDown函数。现在就来完成这项工作。
在TfrmMain.FormCreate中添加以下代码:

1
grdBox.OnMouseDown := FormMouseDown;

编译运行pTetris项目,没有出现我们需要的结果,而是编译失败,消息栏内有以下两条错误信息:

ptetrismain.pas(671,25) Error: Wrong number of parameters specified for call to "FormMouseDown"
ptetrismain.pas(95,15) Error: Found declaration: FormMouseDown(TObject;TMouseButton;TShiftState;LongInt;LongInt);

这是在Delphi中常用的方法,怎么会出现编译错误呢?难道是我记错了?
为了验证这个问题,我在Delphi中编写了一个小程序。
在创建一个新的应用程序,添加三个按钮和主菜单,在主菜单中添加一个菜单项,在按钮事件中添加代码。以下是主单元的全部代码:

复制代码
unit Unit4;

{$MODE Delphi}

interface

uses
  LCLIntf, LCLType, LMessages, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Menus;

type

  { TForm4 }

  TForm4 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    MainMenu1: TMainMenu;
    Close1: TMenuItem;
    procedure Button1Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form4: TForm4;

implementation

{$R *.lfm}

procedure TForm4.Button1Click(Sender: TObject);
begin
  Close;
end;

procedure TForm4.Button3Click(Sender: TObject);
begin
  Button2.OnClick := Button1Click;
  Self.OnClick := Button1Click;
  Close1.OnClick := Button1Click;
end;

end.
复制代码

上边的红色代码就是我们刚才使用的方法:点击按钮Button3时将Button1的事件处理函数设置到窗体、菜单项、Button2的OnClick事件,这样三者就有了与Button1一样的功能——点击后关闭主窗口。
在Delphi中编译运行,结果如我们所想,能够正常编译运行。
使用Lazarus的[工具->Delphi转换器->转换Delphi项目为Lazarus项目]将这个Delphi项目转换后在Lazarus中编译运行,结果还是正确的,跟在Delphi中没有任何差别。
检查Lazarus转换对Delphi源程序的改动,发现有两处:一个是单元头部添加了{$MODE Delphi} ,另一个是在实现区将{$R *.dfm}改成了{$R *.lfm}。第二个不会有问题,这是链接窗体资源的,那么问题就应该是在第一个改动上了。
我们来做一个测试:
去掉Lazarus添加的{$MODE Delphi} ,编译运行项目,结果是运行正常。
将{$MODE Delphi} 改为我们在Lazarus中创建的项目使用的{$mode objfpc}{$H+} ,编译运行项目,结果在消息栏出现以下错误信息:

Unit4.pas(44,22) Error: Wrong number of parameters specified for call to "Button1Click"
Unit4.pas(37,18) Error: Found declaration: Button1Click(TObject);
Unit4.pas(45,19) Error: Wrong number of parameters specified for call to "Button1Click"
Unit4.pas(37,18) Error: Found declaration: Button1Click(TObject);
Unit4.pas(46,21) Error: Wrong number of parameters specified for call to "Button1Click"
Unit4.pas(37,18) Error: Found declaration: Button1Click(TObject);

这些消息跟我们前面遇到的情况是一样的。问题的原因找到了。
现在回到我们的pTetris项目中,

  • 将pTetrisMain单元头部的{$mode objfpc}{$H+} 注释掉,编译运行,错误跟以前一样,编译失败
  • 将pTetrisMain单元头部的{$mode objfpc}{$H+} 改为{$MODE Delphi} ,编译运行项目,结果是运行正常。

由此看出,这是Delphi模式跟objfpc模式不兼容所造成的,只要我们使用Delphi模式就不会有问题。这个问题就算是解决了。
这个问题真的解决了吗?没有!我们现在使用的是Lazarus,objfpc模式是Lazarus使用的模式,按着程序员追根究底的习惯,我们必须要找到在objfpc模式下的解决方案这个问题才算是解决了。
在网上按着错误信息百度一下“Wrong number of parameters specified for call to”,发现这样的错误有很多,我这这些信息中找到了这样一篇文章:

Lazarus中指定将控件的事件与delphi的区别_lazarus和delphi的差距-CSDN博客

文章内容不多,我就全文复制过来了:

复制代码
在Delphi中,我们可以将一个函数赋值给一个控件的事件,比如:

procedure TForm1.btnClick(Sender: TObject);
begin
  //..........
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Button1.OnClick := btnClick;
end;

但是,在Lazarus中,我们如果用同样的方法,在编译时就会出现一个错误提示:

Error: Wrong number of parameters specified for call to "btnClick"

造成这个现象的原因是FPC编译器和Delphi的编译器之间的差异。解决的方法也很简单,使用"@"这个操作符,即在Lazarus中,需要使用下面的表达方法:

Button1.OnClick := @btnClick;
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/arbin_he/article/details/6536781
复制代码

这才是我们要找的真正答案。
现在回到我们的pTetris项目,给在TfrmMain.FormCreate中添加的代码加上一个@符号:

1
grdBox.OnMouseDown := @FormMouseDown;  

完成修改后编译运行pTetris项目,开始运行后分别按动鼠标的左右按键和滚轮,看正在移动到方块盒子的效果。这一次编译顺利完成,项目运行后可以正常游戏。
不过还是有一个问题:鼠标在方块移动区可以正常操作方块左右移动、跌落,但当鼠标指针在任何一个方块上的时候,鼠标的按键又失效了。
有了前面的经验,这个问题就好解释了:我们的FormMouseDown是添加给grdBox的,方块在grdBox之上把grdBox遮挡了,grdBox也就接收不到鼠标按键事件了。
怎么解决呢?给每一个方块都添加上FormMouseDown?这当然可以解决问题,不过,我觉得还是有一个更好的办法:玩游戏的时候让鼠标躲开方块。哈哈哈!

6.结束语

键盘和鼠标都是计算机重要的输入设备,也是最常用的输入设备。在程序设计中如何发挥键盘和鼠标的作用是每一个程序员都需要重视的问题。
使用键盘和鼠标都可以向应用程序发出指令,归根结底是响应键盘和鼠标的事件的问题。
事件驱动是图形界面操作系统通用的方法,学会了键盘和鼠标事件的应用,其他事件也就触类旁通了。

posted @   lexyao  阅读(88)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示