第一章、基础知识

       1.1 基本数据类型

       1.2 变量和常量

              1.2.1 变量和常量的命名

              1.2.2 变量的作用范围

              1.2.3 静态变量

              1.2.4 变量的初始值

       1.3 运算符

              1.3.1运算符的优先级

       1.4流程控制

              1.4.1 如果、如果真、判断

              1.4.2 判断循环、循环判断

              1.4.3 计次循环、变量循环

              1.4.4 到循环尾、跳出循环

              1.4.5 返回、结束

       1.5 子程序（函数）

              1.5.1 子程序参数(参考、可空)

              1.5.2 子程序的递归

              1.5.3 子程序的静态局部变量

       1.6 自定义数据类型

              1.6.1[例]黑客帝国屏保

              1.6.2 自定义数据类型的内存存储

       1.7 数组

              1.7.1 数组的维数

              1.7.2 数组的排序

                     1.7.2.1冒泡排序

                     1.7.2.2 选择排序

                     1.7.2.3 插入排序

                     1.7.2.4 快速排序

                     1.7.2.5 自定义数据类型数组的多级排序

              1.7.3 [例]扫雷游戏

 

第二章、字节集

       《将字节集显示为十六进制》

       《文件分割机》

       《数据隐藏》

       《电子贺卡》

       《配置信息写入exe文件》

《exe文件捆绑》

       《exe文件的自校验》

 

Windows API和动态链接库

 

 

<枚举窗口，枚举进程>

       <使窗口可移动>

       <窗口子类化>

       <动态菜单>

       <动态组件>

       <文件拖放>

       <读取ｄｌｌ中的资源－扑克牌图片>

       <读DOS程序执行结果>

       <自制皮肤>

       <远程线程>

       。。。。。

 

文件系统

       <模拟资源管理器>

       <shell菜单>

       <文件格式>

       《编写自己的文件格式》

 

易语言模块编程

 

注册表

       文件关联、命令行处理、文件右键菜单

枚举注册表

注册表模拟器

 

图像处理

       取图像宽度、高度

       取像素字节集

       各种图像运算方法

面向对象编程

       类、封装、属性、行为、继承、多态性

 

矢量图形(面向对象程序设计)

       CAD 系统

       EMF 文件的读写

 

OpenGL 三维图形编程

 

DirectX 游戏编程

 

网络编程

       <Web服务器>

       <聊天程序>

       <远程控制>

       <邮箱登录器>

       <IP 追捕>

       <天气查询>

      

 

数据库编程

 

界面编程

       <使用《易容大师》进行界面编程>

 


第一章、基础知识

 

 

1.1基本数据类型

计算机程序是用来采集和处理现实世界的数据的，而现实世界的数据又是多样的，比如数量、名称、状态、温度、时间、图像等等，计算机程序要处理这些数据，那么其编程语言也必须规定相应的类型，不同的数据类型用来保存不同类型的数据。易语言中的基本数据类型和其存储的信息如下表所示：

表1.1 易语言中的数据类型

数据类型名

能存储的数据

初始值

字节型

数值型，表数范围：[0,255] ，占用1个字节的存储空间。

0

短整数型

数值型，表数范围：[-32768,32767] ，占用2个字节的存储空间。

0

整数型

数值型，表数范围：[-2147483648,2147483648] ，占用4个字节的存储空间。

0

长整数型

数值型，表数范围：[-9223372036854775808, 9223372036854775807] ，占用8个字节的存储空间。

0

小数型

数值型，表数范围：  ，占用4个字节的存储空间。

0

双精度小数型

数值型，表数范围：  ，占用8个字节的存储空间。

0

逻辑型

表示真假、男女等具有二值性的数据，占用4个字节的存储空间。

假

日期时间型

表示年月日时分秒的数据，占用8个字节的存储空间。

*

文本型

用来表示描述性的文字、符号等的数据。易语言的文本行变量可以大于64KB。

“”

字节集

可用来表示任何数据，比如图片、视频、声音等，其表示的数据的意义取决于数据的设计者。

{}

子程序指针

表示子程序在内存中的地址，这是专门为编程而设的数据类型。占用4个字节的存储空间。

0

*日期时间型的初始值是 1899 年 12 月 30 日 0 时 0 分 0 秒

 

在上表中我们主要看一下数值型的数据类型，因为这种类型的数据我们接触到最多的。我们看到，长整数型的数据表示的整数范围最大，而双精度型的数据表示的范围也大，精确度也最高，那为什么还需要有比他们范围小的数据类型呢？呵呵，这正如尺有所短，寸有所长。首先，它们在内存和磁盘中占用的空间不同，对于字节型的数据，只占用1个字节，而长整型的数据则占用8个字节之多，对于要表示人的年龄这样的数据，字节型经济又实惠——人的年龄不会是负数，也不可能超过255岁；其次，运算速度不一样。我们知道，当前主流的计算机都是32位的，在内存中数据是4字节对齐的，那么长度为4字节的数据，在运算时和在内存中移动时速度是最快的，如果你的程序不吝惜内存，而更在乎速度的话，就尽量采用长度为4字节

的数据类型吧。

字节集数据类型是其他的编程语言中所没有的，是易语言的一个很有特色的数据类型，在文件处理、类对象的持久化中有重要用途，以后会有专门章节讲述。

 

1.2 变量和常量

 

       所谓变量，就是其中保存的数据可以变化的一个数据容器（在易语言的早期版本中，变量都叫容器）。简单地说，变量就是在内存中保存数据的地方，而其中的数据是可以随时修改的。顾名思义，常量就是恒定不变的量，其中的数据不能被修改。如果在程序中有语句修改了常量的值，编译器会报错。在编辑源代码的任何时候，可以通过点选菜单〔插入>常量〕来插入常量。在易语言中，要使用某个常量，必须在前面加上“#”号。有同学可能会问：“既然常量的值是不可改变的，那么要常量有什么意义呢？在源代码中直接使用其值不就行了吗？要定义一个常量，还要想心思取个名字，不是多此一举吗？”其实不然，使用常量有很多好处：

1. 简化代码输入。比如你要写个与数学相关的程序，要大量使用π，我们知道π= 3.1415926535897932384626433832795，每次在使用π的时候，输入这么一长串数字是不是很麻烦？容易输错且不说，读代码的时候还不知其含义，所以我们不如定义一个常量“派”，这样写代码和读代码都方便多了，运行时速度也快，最后编译成exe文件占用的存储空间也少。

2. 使代码更易于阅读。对于键盘上的每一个按键，都有一个数字型的扫描码，比如空格键的代码是32，回车是13，如果不使用常量，你如何记得住它们？幸好易语言编程环境已经给我们定义好了这些常量，我们可以直接使用。

       随着编码的深入，你会更多地发现常量有时很好的——不要怕麻烦，巧妙地使用常量，有时会有意想不到的效果。

 

1.2.1 变量和常量的命名

       在易语言中，对常量名、变量名和函数名的等需要命名的地方有一定的命名规则，这些名称的命名规则为：名称的首字母必须为全半角字母或汉字，其它字符必须为全半角字母、全半角数字或者汉字。虽然名称中允许半角字符“_”存在，但它被保留为系统专用，因此建议不要使用。与其它的编程语言相比，易语言的命名规则有以下特点：

·无长度限制，用户可以尽情地使用长名称来进行名称描述；

·永远不会与易语言的关键字产生冲突。譬如：现已存在“如果”命令，但用户仍然可以定义一个名称为“如果”的变量，两者之间不会产生任何冲突。虽然如此，最好还是不要以关键字来命名，因为这样的代码难于阅读和理解。

 

       这里顺便说一下“关键字”的概念。在计算机语言中，有些词汇和符号是作为语言的某种特殊的用途而使用的，比如类型定义、流程控制、算术逻辑运算符、预处理指令、编译指令等，在其他的编程语言中，这些词汇和符号是不能作其他用的，比如不能作为变量名。这就好比我的名字是曾劲松，那我生的孩子我能给他取名“曾祖父”吗？不能，因为“曾祖父”就是一个关键字，已经有其特定的用途了。再说一遍，在易语言中虽然可以用关键字来命名变量、常量和函数名等，但建议不要这样——当然，中国的考试有时候会刁钻古怪，难免以后易语言进入课堂而有老师出此偏门的考试题目——嘿嘿，话题扯远了。

 

1.2.2 变量的作用范围

在易语言中，常量是全局范围的，也就是说，在程序的任何地方都可以使用该常量。而变量的使用就没有那么随意了，根据变量的作用范围不同，变量可以分为全局变量、程序集变量和和局部变量。

全局变量的值在程序的任何地方都可以访问和修改，这虽然方便了编程，但实际上在编程的过程中，如果使用了太多的全局变量，程序写复杂了之后，很容易导致思维的混乱，因为你往往不知道你在程序的哪里修改了该变量。最后往往程序虽然通过了编译，运行的结果却常常莫名其妙。所以，记住一条忠告：能够不使用全局变量，就不要使用，全局变量越少越好。要插入全局变量，请按键盘快捷键[Ctrl]+[G]，也可以点选菜单〔插入>全局变量〕。

程序集变量是在当前程序集的范围内都可以访问的变量，它的范围比全局变量要小些。易语言中的程序集，是指一系列相关子程序和变量的有机组合，说简单点，你可以把程序集想像成资源管理器中的文件夹。一般来说，一个窗口对应一个程序集，当然也有独立于窗口的程序集，关键在于你如何组织你的代码。对于一个有窗口的程序集，你可以把程序集变量看成该窗口的“额外”的属性，这个小技巧在编程时很有用。除去范围小些外，程序集变量的使用和全局变量差不多。所以，程序集变量也要谨慎使用。要插入程序集变量，请将光标放置在程序集名的后面，然后按回车键。

局部变量表示在当前子程序（也称函数）的范围内可以访问的变量，它的作用范围最小，也是使用得最多的变量。要在当前子程序内加入局部变量，请按[Ctrl]+[L]。

在本书中，对于变量和子程序参数的命名有一个约定，那就是：全局变量都以“全”字开头，程序集变量都以“集”字开头，函数参数都以“参”字开头，类的成员数据都以“私”字开头。有了这个约定之后，我们一看变量名就知道它是在何处定义的，方便编写和阅读源代码。

 

1.2.3 静态变量

       只有子程序中的变量可以指定为“静态”类型的变量，所以有关静态变量请参看“子程序”一节。

 

1.2.4 变量的初始值

变量的初始值是值变量在声明后未给其赋值的情况下变量里面存储的内容。具体的初始值请参见表1.1。

1.3 运算符

       运算符就是用来进行运算的符号。在计算机中，运算又分为算术运算、逻辑运算、位运算和赋值运算。算术运算用来计算加减乘除求余数等，这些我们在数学中已经学过了。逻辑运算是进行是非判断以及是非组合的运算，其基本运算规则为：非真为假，非假为真；真且真为真，真且假为假，假且假为假；真或真为真，真或假为真，假或假为假。位运算是对数据位进行操作。我们知道，在计算机内部，所有的数据都是以二进制表示的，比如字符“A”，其ASCII码为65，二进制为1000001，二进制中只有两个数学符号：0和1，数据中的每一个1或0，叫做一位，位运算正是针对这些数据位进行操作的。在易语言中，位运算符以函数的形式存在，其基本规则如下：

       位与(1,1)=1，位与(1,0)=0，位与(0,0)=0，规则为“有0则0”；

       位或(1,1)=1，位或(1,0)=1，位或(0,0)=0，规则为“有1则1”；

位异或(1,1)=0，位异或(1,0)=1，位异或(0,0)=0，位异或(0,1)=1，规则为“同0异1”；

位取反(1)=0，位取反(0)=1；

左移是将所有的位左移指定的位数，移出位的被丢掉，右边补0，比如 左移(10000012,3)=0001000 2，这里3是十进制数，其他的是二进制数。同理，右移是将所有的位右移，移出的位被丢掉，左边补0。

赋值运算是指将值赋给一个或多个变量。易语言中所有的运算符如下表所示：

 

表1.2　　易语言中的运算符

运算符类型

运算符名

中文操作符号

英文操作符号

位运算符

位与

位与

band

位或

位或

bor

位取反

位取反

bnot

位异或

位异或

bxor

左移

左移

shl

右移

右移

shr

算术运算符

负

－

-

相乘

×

*

相除

÷

/

整除

＼

\

求余数

％

% 或 Mod

相加

＋

+

相减

－

-

逻辑运算符

大于

＞

> 

小于

＜

< 

等于

＝

= 或　==

不等于

≠

<> 或 !=

大于或等于

≥

>=

小于或等于

≤

<=

约等于

≈

?=

且

且

and 或 &&

或

或

or 或　||

赋值运算符

赋值

＝

=

 

1.3.1运算符的优先级

       考虑这样一个数学算式：8+3*4^2，它的结果是如何计算的呢？从数学知识中，我们知道要先算４的平方，再与３相乘最后加上８，而不是简单地按从左至右的顺序来计算。这就是运算符的优先级。同样地，在编程环境中，对一个表达式进行求值时，也有个运算符优先级的问题。上表是按运算符的优先级进行排列的，我们可以看到，在易语言中，位运算的优先级最高(其实它们都是函数)，接下来依次是算术运算、逻辑运算、赋值运算。同一个运算符类别中不同的运算符也有优先级的差别，例如对于算术运算符来说，取反（－）运算符是最高的，其次是乘除，再次是整除、求余、加减等(易语言中没有求幂运算符，是函数)。

       在上表中，相邻的背景色相同的运算符有相同的优先级。比如相乘和相除有相同的优先级，易语言中的位运算全部是函数，所以也有相同的优先级。相同优先级的运算符按从左至右的顺序求值。对于如下的表达式，变量１的最终值是什么呢？

　　变量１＝位或 (左移 (3, 2), 3) ＞ －(5 － 3 × 8)

       求值过程如下：先计算左移(3,2)，3=(11)2，左移两位是(1100) 2=12，再与３位或，即　(1100) 2　与　(0011) 2 位或，结果为(1111)2=15；再算大于号右边，结果为19，显然　15>19 的结果是假，所以　变量１的值为假。在易语言中可以通过以下代码查看运行结果：

输出调试文本 (位或 (左移 (3, 2), 3) ＞ －(5 － 3 × 8))

 

       跟数学算式类似，程序中的表达式也可以通过添加括号来改变运算顺序。如果你不清楚究竟是那个运算符的优先级高，那么就加括号吧！这是确保正确的省事办法。

 

1.4流程控制

       如同现实世界的数据是多样性的，现实世界的条件也是多样性并且在时刻发生变化的。假如我们要设计计算机程序来处理类似这样的事务：

①              通过判断外界的温度来控制通过电炉丝的电流以保持大致的恒温——当温度高于４０℃的时候，减小通过的电流以降温；当温度低于３７℃的时候，增大通过的电流以升温；

②              根据考试得分评等级的程序，如果得分高于８５分为优秀；介于６０分到８５分之间的为优良；低于６０分的为不及格；

③              一个图像处理程序，要给一张图片加上１０００个随机的彩点；

如果计算机始终只能按照指令的顺序一条条地执行，那么很显然，要处理这样的事情就很棘手了。这样就引入了流程控制的概念，流程控制允许计算机根据不同的条件跳过一段代码继续执行后面的代码，或者跳转到任意指定的指令行去继续执行，或者重复地执行指定的程序段。易语言中设计到流程控制的关键字如下表。

 

关键字

说明

VB 或Ｃ中的类似关键字

如果

满足条件执行一个分支，不满足则执行另一个分支。

if… else… endif

如果真

满足条件则指令下面的分支。

if…endif

判断

满足条件则执行该分支，其他的分支不再进行条件判断。如果条件都不满足，则执行最后一条分支（缺省分支）。也就是说，始终只执行其中的一个分支。

select case number

case 1:

case n…..

case else

判断循环首

如果条件满足，则执行循环体。循环体有可能一次也不被执行。

while

wend

循环判断首

先执行循环体，再进行条件判断。循环体至少被执行一次。

do

loop while

计次循环首

执行指定次数的循环，循环的当前次数存入指定的变量中。

 

变量循环首

根据指定的取值范围进行循环，循环的当前值存入指定的变量中。

for i=5 to 8

next i

到循环尾

不执行此次循环后面的语句而直接进行下次循环。

continue

跳出循环

不再执行循环。

break

返回

返回到调用程序，用于子程序中。

return

结束

退出程序。

end

 

1.4.1 如果、如果真、判断

       在论坛上，经常看到有朋友问“如果”和“如果真”有什么区别、“如果真”和“判断”有什么区别的问题，这里我作一个简要的解答。

       “如果”语句带有两个分支，如果条件成立，执行第一个分支，否则执行第二个分支；“如果真”则仅有一个分支，只有条件满足才执行。如果把“如果”语句的第二个分支留空，那么它的效果和“如果真”是一样的，但在易语言的编程环境中会绘制一条空的流程线，不太美观。所以，如果你要根据条件是否成立来执行某些指令，而不管相反的条件，就要用“如果真”，如果正反两个条件都要兼顾，则需要使用“如果”。易语言的示例代码如图1.4.1-a和图1.4.1-b所示：

 

图1.4.1-a  “如果”和“如果真”的区别

 

图1.4.1-b “如果”和“如果真”的等效

 

       下面说说“判断”和“如果真”的区别。从表面上看，“判断”也是只有条件为真时执行，但要注意，判断是基于多分支的，只要发现其中一条分支的条件满足，后续分支的条件就不再进行判断，当然其中的代码也不会被执行，如果所有的条件都不满足，则执行默认的分支。用多个“如果真”语句可以写出与“判断”语句等效的效果，但程序执行的效率没有“判断”语句高，为何？因为使用“判断”语句只要发现一条分支满足就不再进行后续判断，而“如果真”语句对每个条件都要进行判断。“判断”语句通常用来对类似的多个条件且在同一个时刻下最多只有一个条件满足的事务进行处理，例如根据用户按下了键盘的某个键、选择了工具条上的哪个按钮来执行相应的功能；在消息循环中处理当前的消息等。图1.4.1-c的示例代码演示了在易语言中用“判断”语句来处理用户单击的工具条按钮。

 

图1.4.1-c 用“判断”语句来处理用户单击工具条按钮

1.4.2 判断循环、循环判断

 

       顾名思义，判断循环就是先判断条件是否成立，成立则执行循环体，不成立就结束循环；而循环判断是先执行循环体，再判断条件是否成立，成立则再次执行循环体，否则结束循环。

这两个语句很简单，就不再敖述。图1.4.2-a 和　图1.4.2-b 分别是使用“判断循环”和“循环判断”的例子。

图1.4.2-a使用“判断循环”将一个文本文件的内容读入列表框中

 

 

图1.4.2-b 使用循环判断随机画圆，直至用户按下ESC键

 

1.4.3 计次循环、变量循环

       记得我读小学的时候，老师常对我说“把每个写错了的字抄写１００遍！”，我想大家都有过类似的经历吧。对于计算机来说，这正是一个“计次循环”的问题。在你明确知道要循环的次数的时候，推荐使用“计次循环”。计次循环的计数器从１开始，每循环一次，计数器自动加1，如果需要的话，可以指定一个变量来保存计数器的值。“变量循环”是高级的“计次循环”，使用起来也稍微复杂一些，可以指定计数器的起始值、终止值和每次递增的数量，同样的，你也可以把计数器的值存入变量中使用。

 

图1.4.3-a  用“计次循环”求１０的阶乘

 

图1.4.3-b 用“变量循环”计算１００以内能被３整除的数的和

 

       从原理上来说，所有的循环都可以使用“判断循环”来实现，但不同的实现方法除代码量不同之外，执行的效率差距也很显著。推荐的原则如下：能够使用“计次循环”和“变量循环”实现的，就不要使用“判断循环”或“循环判断”，因为前者的执行效率要高得多，尤其是对于次数较多的循环。为什么呢？因为使用“判断循环”或“循环判断”，每循环一次都要执行一次条件判断，而进行条件判断的开销往往是比较大的（尤其是进行文本比较和字节集比较），而“计次循环”则是“明确目的，直奔主题”，效率当然就高多啦！

 

1.4.4 到循环尾、跳出循环

       OK，刚才老师罚你把“羸”字写100遍，也就是给你发了个“计次循环”的指令。当你写到第13个字中途的时候，却差点写成了“赢”字，下面的小“贝”都快写完了，此时你决定放弃继续写这个字，因为错字老师是不算数的，搞不好要再发写100遍呢。于是你把笔移到了下一格，开始写下一个“羸”字——暂停！你的这个过程就是“到循环尾”。“到循环尾”并不是放弃整个循环，而是仅仅放弃当前循环中尚未完成的步骤，直接进入下一循环；你继续写阿写，当你写到第90个字的时候，老师突然说“好了，今天时间比较晚了，大家都回家吃饭吧！”。“真郁闷，又让我功败垂成！真是行百里者半九十阿。”，当你心理这样想着的时候，却不知已经深入计算机语言的真谛，因为你已经“跳出循环”。“跳出循环”就是不再继续循环了。这句话对吗？错！如果你在一个多层的嵌套循环中，中层循环中的“跳出循环”指令仅仅跳出当前这一层循环，外层循环还得继续执行的阿。

 

1.4.5 返回、结束

       “返回”主要用在子程序中，返回到子程序的调用者，在返回时可以携带一个返回值。

       “结束”指令用来终止当前进程，退出程序。

       这两个很简单，不再敖述。

 

1.5 子程序（函数）

       在我们编程的时候，有一些经常使用的功能，比如求一段文本的长度、计算某个数的平方根、弹出一个消息框提示用户等，如果每次实现这些功能都写一大段代码，是不是很烦琐？结构化的程序设计提出了代码功能模块化的思想，这些实现特定功能的代码模块我们称之为“子程序”。子程序有系统内置的，更多的则需要我们自己编写。

       系统内置的子程序实现的都是一些通用的功能，是我们编写其他子程序的基石，所以要好好掌握。这些内置的子程序涉及文本处理、算术运算、数组操作、拼音处理、文件读写、系统处理等诸多方面。我的建议是把“工作夹”上“支持库”页中的核心支持库中的所有子程序及其在“提示”窗口中的简要说明都仔细看一遍，不需要全部记下来，只要大概知道有哪些东西、能实现什么样的功能就行了，用的时候再仔细查阅。

 

1.5-a易语言中系统内置函数及其简要说明

 

       很显然，仅仅使用易语言内置的子程序无法满足我们编写多种应用程序的要求，所以我们常常要编写自己的子程序。在编辑代码的时候，你可以随时按[Ctrl]+[N]或单击菜单项〔插入>子程序〕来添加子程序。

 

1.5.1 子程序参数(参考、可空)

       “吃的是草，挤出来的是奶”，这是鲁迅先生对“孺子牛”的亲切描述。我们写的子程序就跟鲁迅先生笔下孺子牛差不多，这里，“草”就是子程序的参数，而“奶”就是子程序返回的结果。当然，有的牛奉献精神更佳，不需要吃草也能挤奶，这就相当于不需要参数的子程序；当然，如果是公牛，吃了草也挤不出奶来，那就是无返回值的子程序——虽然它不能挤奶出来，那么肯定有别的用途，比如说跟能挤奶的牛待在一起，会使她们心情舒畅，产出优质量多的奶来；还有的牛，可能不光要吃草，还要吃树叶饲料之类的，那就是带有多个参数的子程序了。

       通常情况下，一个子程序最多有一个返回值，如果要同时返回多个值该怎么办呢？这里介绍几个技巧：

①：使用“参考”类型的参数。当你勾中了参数名后面的那个“参考”选项的时候，参数传递的就不是它的值，而是它在内存中的地址。你在子程序中对该参数的修改，实际上是对相应的内存中的数据的修改，所以在子程序返回的时候，调用程序中的变量已经被修改了。请看下面的代码片断。

1.5.1-a 使用“参考”参数类型返获得多个返回值：平均值、最小值和最大值

 

② 使用数组类型的参数。如果返回的参数个数不定，但类型是一致的，那最好用“数组”类型的参数了。不管勾不勾中“参考”，数组类型始终是传址的。

 

③ 使用自定义数据类型。自定义数据类型将在后面介绍。同样地，不管是否勾中“参考”，自定义数据类型也是传址的。

 

       子程序参数的“可空”选项允许用户在调用该子程序时该参数位置不输入参数，这样极大地方便了使用该子程序的用户（很多情况下就是你自己啦），但对于编写该类子程序的程序员来说，会稍微麻烦一些，因为你要考虑到用户是否传入了参数，一般来说，你应该使用如图1.4.1-b所示的形式来设定空参数的默认值。

图1.5.1-b 设置子程序“可空”参数的默认值

 

1.5.2 子程序的递归

       “从前有座山，山上有座庙，庙里有一个老和尚和一个小和尚。有一天，老和尚给小和尚讲故事，他说：‘从前有座山，山上有座庙，庙里有个老和尚和一个小和尚。有一天，老和尚给小和尚讲故事，他说：“从前有座山，山上有座庙……”’”我倒，这故事还有玩没玩？！也许你觉得这个故事纯粹是瞎胡闹，那么你错了，这里面可蕴含了巧妙的计算机编程思想——递归。

       “递归”并不是子程序的某个特点，而是一种编程思想。也许你问：递归到底有什么用阿？递归用处可大了，在计算机编程——不，在生活中都随处可见，也许只是你没有感觉到。当你在和别人下棋的时候，你是如何思索每一个要下的棋步的呢？或许你是这样思考的：如果我这么走，那么对手会怎样怎样，我再如何如何……；如果我那么走，又如何如何……等等，根据推理的几步棋，从中挑出一个最优的走法。计算机程序与人类对弈，它的算法也是与此类似的, 它会遍历棋盘上当前情况下每一个可能的走法，在内存中形成很多个新的棋局，再以这些棋局为基础，站在你的角度遍历你的所有可能的走法，同时再生成很多新的棋局……这样循环到指定的深度（通常就是指思考的“步数”）后，挑出棋局最优的那一盘，回到计算的起始点，，再举一个例子，伸出你的左手，看到你掌心的血管没有？从一根主血管分成很多子分支，这些子分支下再分子子分支，子子分支下再分……这就是计算机的分形图的基础，而分形图的核心算法必须依赖于递归。还有你每天使用的计算机的资源管理器的目录树，也是递归。这些都是看得见的，还有看不见的，比如编译器的表达式解析，也要用到递归。由此可见，递归是无处不在而且功能强大的，所以一定要好好理解。

       计算机编程中的递归，指的是子程序不断调用它自身，这想起来总觉得有点不可思议，但确实可以做到。当然计算机中的递归不可能象我前面讲的那个故事那样，否则执行起来没玩没了，任何一台电脑都会死机。所以在使用递归的时候，一定要注意设定终止条件，否则会死得很难看。

1.5.2-a 使用递归计算阶乘

 

       图1.5.2-a的子程序用递归来计算指定数的阶乘。我们知道，任何一个大于1的整数的阶乘实际上等于该数乘以比它小1的数的阶乘，而1的阶乘是1。用数学式子表述如下：


用易语言程序来表述正是如上的算法。

 

       图 1.5.2-b 则使用递归来遍历指定的目录，列出指定目录下的所有文件（包括子目录下的文件）。虽然我们不知道一个目录下面有多少层子目录，但子目录与目录间有共性：目录下可能有文件，也可能有目录。当我们发现文件的时候，就输出它；发现目录的时候，就用同样的方式来遍历它。具体代码实现的时候，我们是把当前目录下所有的文件输出，所有的子目录存入一个数组中，再逐个遍历。易语言的“寻找文件”子程序会将当前目录“.”和当前目录的父目录“..”也都列出来，所以在递归的时候要注意排除这两个目录，否则始终在当前目录循环，直至死机。这里如果对易语言的“寻找文件”函数不太理解，可以查看编程环境下的即时帮助。

图1.5.2-b 使用递归来列出指定目录及其子目录下的所有文件

 

 

1.5.3 子程序的静态局部变量

       前面提到过，子程序的局部变量可以设置为“静态”类型。所谓“静态”，就是指该变量具有“记忆”功能，对该子程序调用后，该局部变量的值不会被销毁，到下次进入该子程序，它仍然保留上次调用该子程序后的值。普通的子程序变量在每次进入子程序内后都要重新分配内存空间，在退出子程序时自动释放所占用的空间；而“静态”类型的子程序变量跟全局变量和程序集变量一样，在程序启动时就分配了内存空间并初始化（只初始化一次，而不是每次进入子程序都初始化），并且在整个程序运行期间都不释放，所以该类型的变量有“记忆”效应。 图1.5.3所示的代码使用静态局部变量和递归分解指定自然数的质因数，理解起来可能有些困难，请仔细思考体会。

 

图1.5.3  使用静态局部变量和递归来分解质因数

 

1.6 自定义数据类型

       易语言的基本数据类型可以满足我们普通的编程需求，而对于复杂的应用程序，往往需要我们自己定义专用的数据类型，以简化编程。比如说我们要编一个学生信息管理程序，我们知道，一个学生的信息通常包含以下方面：姓名（文本型）、学号（文本型）、性别（逻辑型）、出生日期（日期时间型）、年级和班级（文本型）、专业（文本型）等。而这些不同类型的信息其实是一个整体，如果能把这些不同的数据类型整合成一个新的数据类型，那么编程起来会方便很多。

       在易语言编程环境中，点选菜单〔插入>数据类型〕来加入新的数据类型，先更改数据类型名称，然后按回车键来添加数据成员。申明好新的数据类型之后，就可以象使用普通变量那样来申明变量了，差别就是在赋值和访问时要使用点语法。

图 1.6 定义和使用一个“学生信息”的数据类型

 

 

1.6.1[例]黑客帝国屏保

       如果你对自定义数据类型使用不习惯，那么下面这个小例子或许会让你熟悉自定义数据类型并增强对编程的兴趣。这里我们来做一个很酷的类似《黑客帝国》中片头的字符下落特效的屏保，让你的电脑更富有个性！请按下面的步骤操作。

 

1>     新建一个易程序。在上面加一个画板，改名为“画板缓冲”,设置画板的背景色为黑色、文本颜色为绿色，选择一个长型的字体，比如“Impact”，字号为四号，注意，还要将其“可视”属性设为“假”，“自动重画”设为“真”。这个画板我们将作为在后台绘制的一个缓冲画板，用户是看不见的。再加一个画板，改名为“画板可视”，其他的保持默认值。

2>     往窗体上添加一个时钟控件，将其“时钟周期”改为50，我们需要每秒钟刷新20张画面。

3>     点选菜单〔插入>数据类型〕，添加一个如图1.6.1-a的数据类型：

图 1.6.1-a 黑客帝国特效的数据类型

 

       4> 双击“_启动窗口”，如图1.5.1-b添加几个程序集变量。

图 1.5.1-b 黑客帝国特效的数据类型

 

       5> 在“__启动窗口_创建完毕”中添加图1.6.1-c所示代码。

图 1.6.1-c “__启动窗口_创建完毕”事件中的代码

       这段代码中，读者可能会对初始化下落字符数组下面的计次循环中的代码有点费解，这里解释一下。

       ① “字符 (取随机数 (33, 126))” 这一句用来随机取一个ASCII范围在33到126之间字符。从图1.6.1-d所示的ASCII表可以看到，这些字符包含所有的可见英文字符和标点符号。如果你只想要数字，那么把这句改成“字符 (取随机数 (48, 57))”。

图1.6.1-d ASCII表

       ② “取整 (取随机数 (0, _启动窗口.宽度) ÷ 20) × 20”这段代码先从0到窗口宽度中随机取出一个值，然后把该值除以20并去掉小数部分，然后再乘以20，就得到了一个能被20整除的随机数。这样设置以后，所有的字符都会在竖直方向形成列，每列个占用20个像素的宽度。

       ③ “集下落字符们 [i].y ＝ －取随机数 (, _启动窗口.高度)”，这句用来将字符的纵坐标设置为屏幕上方的某一个随机值，这样字符就会从屏幕外落到屏幕内。

       ④ “集下落字符们 [i].速度 ＝ 取随机数 (2, 30)”，这句很简单啦。用来设置每个字符下落的速度范围。当然它们下落的速度不应该一样，否则的话就像军队阵列，太呆滞了。

 

       6> 双击窗体上的计时器，添加以下代码。

图1.6.1-e 黑客帝国的时钟周期事件代码

       在这段代码中，程序将后台画板清空，重新设定每个字符（这样字符边下落边翻转。如果只需要0到9之间的数字，可以象前面说明的那样改），将每个字符下落一次，把它们绘制到缓冲画板；如果字符落到了屏幕下边，则再次把它移到屏幕的上边。全部绘制到缓冲画板后，再一起复制到前台的可视画板。为什么要使用两个画板呢？因为如果只使用一个画板，清除后立即重绘会导致画面闪动，在慢速的机器上甚至可以看到字符一个个被绘制出来，效果很差。而采用两画板，绘制完毕后一起复制到前台，用户看不到绘制的过程，所以就不会出现闪屏的情况——这就是游戏编程中常使用的“双缓冲”技术。

       OK，运行以下看看效果吧。是不是很酷？你还可以通过调整字符个数、下落速度、以及时钟周期来获得更满意的效果。

       7> 把它变成屏保吧。这个小程序做成屏幕保护最合适不过了，这里我们使用最简单的一种方法来把它变成屏保。首先，屏保在用户移动鼠标、点按鼠标或敲击键盘时要退出，所以我们按以下步骤添加代码：

       ① 加一个程序集变量“集鼠标上次横坐标”，类型为整数型，用来进行用户移动鼠标距离的判断，如果鼠标移动的距离很小（比如桌面被震动），则不退出屏保。

       ② 添加鼠标移动事件、单击事件和按键事件的代码，如图1.6.1-f所示。

图1.6.1-f 添加适合屏保的退出程序的代码

 

其次，需要将程序编译成“黑客帝国特效exe” 文件，再在资源管理器中将扩展名改为“scr”，然后把它拷贝到 windows XP 的 system32 目录下（如果是windows98/ME，则拷贝到 system 目录下），打开桌面的属性窗口，就可以把屏保设置为我们刚才写的“黑客帝国特效”了（图1.6.1-g）。

不过，真正的屏保程序可没这么简单，还需要涉及到预览窗口、参数配置等，后面章节会有专门介绍。

 

图 1.6.1-g 设置黑客帝国屏保

 

 

1.6.2 自定义数据类型的内存存储

　　自定义数据类型的使用很简单，我们只需用“.”操作符就可以读取或修改它的各个数据成员了。那么我现在要考考你：能不能不使用“.”操作符而直接修改某个自定义数据类型的成员数据呢？答案是肯定的，我们可以通过直接读取变量的内存地址来做到这一点。要通过修改内存来修改数据成员，我们必须要知道自定义数据类型的内存存储方式。也许你会问：既然可以直接通过“.”操作符来修改数据成员，为何还要舍近求远地直接修改内存呢？这样做，一方面可以加深我们对计算机数据存储器的理解，另一方面，也为后面章节中的向ｄｌｌ函数中传递自定义数据类型做铺垫。如果你对此不感兴趣，可以先跳过这一小节。

　　首先我们考虑一个简单的问题：文本型的数据的存储问题，一个文本型的变量，它里面究竟保存的是什么？是文本数据本身，抑或仅仅是文本在内存中的地址？我们来研究一下图1.6.2-a 所示的代码【注意：这段代码中使用的函数“取变量地址()”在“特殊功能支持库”中，所以在编写和运行代码前，需要配置支持库，方法如下：点击菜单 工具>支持库配置，在支持库列表中勾中“特殊功能支持库”】。这段代码的前三行先初始化一个文本变量的内容为“http://goomoo.cn”，然后取该变量的地址，再用“指针到文本”函数将地址中的内容读出来。从逻辑上来说，如果文本型变量保存的是文本数据本身，那么这里应该将字符串正确地输出，但实际上这里输出的却是乱码。由此可见，“文本”变量中保存的不是真正的字符串，那么是什么呢？只有一个可能，那就是指向文本数据的内存地址，也就是说，“文本”变量里面实际保存的是一个整数。

既然“文本”变量里面保存的是真实文本数据的地址，那么怎样才能把它取出来呢？“到数值”函数显然不行，易语言又没有提供一个“指针到整数”的函数，因此只能采用一个变通的方法：先用“指针到字节集”转换成字节集，再从字节集转换成整数，就得到了真实的文本地址了。代码片断中中间三行做的正是这个工作，因此得到了正确的地址，输出了正确的结果。

　　最后的两行代码就是直接修改内存中的数据了，也就是修改了“文本”变量中的值。

图1.6.2-a　文本变量的内存存储

为了理解更直观，这里画一个表（表1.6.2-a），从表中我们可以看到，文本变量的内存地址是1242512（在你的机器上也许会不一样），存储的内容是9896255(这个值也会不一样)，而内存起始地址为9896255的内存块中保存的是文本“http://goomoo.cn”。

 

内存地址

..........

1242512

..........

9896255

..........

内存内容

..........

9896255

 

http://goomoo.cn

 

变量名

 

文本

 

 

 

表1.6.2-a 文本变量的内存存储

 

       既然文本型变量的存储搞清楚了，那么自定义数据类型的内存存储方式也就不是难事。我猜想应该是这样的：对于一个自定义数据类型的数据来说，其各个数据成员的数据是存储在连续的内存单元中的，如果成员数据的数据类型占用的内存空间是固定的（比如整数型、逻辑型、日期时间型等），数据就直接存储在该内存单元中，如果数据的长度不定（如文本型、字节集等），那么该内存单元保存的是指向它们的内存地址，占用4个字节的存储空间，实际的数据则另外再开辟内存空间保存。

       我们用代码来证实我们的猜想，我们需要先定义一个这样的数据类型(图1.5.2-b)：

图1.6.2-b 用来测试的数据结构

       在窗体上添加一个按钮，添加以下代码。因为这段代码太长，所以以文本的方式列出，以后不作说明，均如此。

 

.版本 2

.支持库 spec

 

.子程序 _按钮自定义类型_被单击

.局部变量 某学生, 学生

.局部变量 结构地址, 整数型

.局部变量 临时地址, 整数型

.局部变量 临时字节集, 字节集

.局部变量 生日, 日期时间型

.局部变量 男女, 逻辑型

.局部变量 年龄, 整数型

 

某学生.姓名 ＝ “曾唯思”  ' 文本型，保存的实际是文本的地址，固定长度为4个字节

某学生.生日 ＝ [1968年8月8日]  ' 日期时间型，固定长度为8个字节

某学生.性别 ＝ 真  ' 逻辑型，固定长度为4个字节，易语言帮助文档有误

某学生.年龄 ＝ 37  ' 字节型，固定长度为1个字节

 

' 以下三行代码获取“某教师”变量中保存的结构的起始地址

临时地址 ＝ 取变量地址 (某学生)

临时字节集 ＝ 指针到字节集 (临时地址, 4)

结构地址 ＝ 取字节集数据 (临时字节集, #整数型, )

' 以下三行代码取出第一个成员数据“姓名”的地址

临时字节集 ＝ 指针到字节集 (结构地址, 4)

临时地址 ＝ 取字节集数据 (临时字节集, #整数型, )

输出调试文本 (指针到文本 (临时地址))

 

临时地址 ＝ 结构地址 ＋ 4  ' 跳过“姓名”成员的4个字节，到“生日”成员

临时字节集 ＝ 指针到字节集 (临时地址, 8)  ' 日期时间型长度固定为8个字节

生日 ＝ 取字节集数据 (临时字节集, #日期时间型, )

输出调试文本 (生日)

 

临时地址 ＝ 临时地址 ＋ 8  ' 跳过“生日”成员的8个字节，到“性别”成员

临时字节集 ＝ 指针到字节集 (临时地址, 4)  ' 逻辑型数据应占4个字节

男女 ＝ 取字节集数据 (临时字节集, #逻辑型, )

输出调试文本 (男女)

 

临时地址 ＝ 临时地址 ＋ 4  ' 跳过“性别”成员的4个字节，来到“年龄”成员

临时字节集 ＝ 指针到字节集 (临时地址, 1)

年龄 ＝ 取字节集数据 (临时字节集, #字节型, )

输出调试文本 (年龄)

 

' 既然已经找到了正确的内存地址，再往其中写数据就很简单了。

临时字节集 ＝ 指针到字节集 (结构地址, 4)

临时地址 ＝ 取字节集数据 (临时字节集, #整数型, )

写到内存 (“曾睿姝”, 临时地址, )  ' 写姓名

 

临时地址 ＝ 结构地址 ＋ 4

写到内存 ([2003年10月1日], 临时地址, )  ' 写生日

临时地址 ＝ 临时地址 ＋ 8

写到内存 (假, 临时地址, )  ' 写性别

临时地址 ＝ 临时地址 ＋ 4

写到内存 (2, 临时地址, )  ' 写年龄

 

' 再用结构成员的访问方式输出看看

输出调试文本 (某学生.姓名)

输出调试文本 (某学生.生日)

输出调试文本 (某学生.性别)

输出调试文本 (某学生.年龄)

 

       很幸运的是，代码的运行结果证实了我们的猜想。

       从代码的分析我们可以看到，自定义数据类型变量实际保存的也是个内存地址，我们只要找到这个基址，然后顺次根据成员变量的长度进行递增就可以得到这些成员变量的地址了，然后就可以直接进行读取和写入。对于数据长度不定的数据类型，比如文本型、字节集、数组等，保存的又是一个地址，我们再通过该地址进行访问。所以我们看到，对文本型的成员变量的处理复杂一些，转了一个弯。为什么要采取这样的一种存储方式呢？试想一下，如果文本型数据也直接保存在结构的地址中，那么在初始化该结构时就要对该成员变量分配足够的内存空间，因为系统并不知道后续的代码会将该成员赋一个多么长的文本。很显然，这会造成内存空间的极大浪费，也会极大影响程序的运行效率。而仅仅保存一个内存地址，在32位的操作系统上，内存地址的长度始终是4个字节，这样就固定了。如果后续代码修改了该成员变量，那么系统只用动态分配另一个内存块，并且把该内存块的起始地址赋值到该成员变量即可。

       这段代码的运行过程可以用下表来说明：

 

内存地址

......

1405472

(基址)

基址+4

基址+12

基址+16

...........

9896243

......

内容

......

9896243

[19680808]

真

37

...........

曾唯思

......

变量名

 

某学生.

姓名

某学生.

生日

某学生.

性别

某学生.

年龄

 

 

 

变量名

 

某学生.

姓名

某学生.

生日

某学生.

性别

某学生.

年龄

 

 

 

 

长度

 

4

8

4

1

 

7

 

 

表1.6.2-b 自定义数据类型的内存存储

 

       嗯，研究了半天自定义数据类型的内存存储结构，好象没什么用，是不是？前面已经说过，这是为后面章节中的在dll函数中使用自定义数据类型作的准备工作，权作铺垫。 :P

 

 

1.7 数组

       计算机处理的数据量往往是很大的，而且通常是同一种数据类型，比如上例中整屏的字符，或者图像中的一批像素，或者一批文本等等。易语言中提供的“数组”数据结构允许我们方便地处理这样的数据。简单地说，数组就是一批同种类型的数据的顺序集合，我们可以通过他们的序号来方便地访问其中的每一个元素。要定义一个指定数据类型的数组，只需要在声明它时输入数组中元素的个数就可以了，如图1.6。

图1.7 数组的定义方法

 

       在给数组变量取名的时候，我喜欢在后面加一个“们”或“数组”以表示它是一个数组。如果你不喜欢，你可以采用你自己的方法。一旦定义了数组，我们可以通过方括号和其顺序索引来访问其中的元素，如“整数们[4]”。 注意：数组的索引从1开始，到所定义的元素个数结束。如果你访问的数组索引不在此范围之内，那就要弹出著名的“数组索引越界”运行时错误而导致程序意外终止。所以，在使用数组时一定要非常小心，应该尽可能做多的判断以确保索引值在其范围之内！

“如果在定义数组时我不知道数组里面的确切元素个数该怎么办呢？”你也许会这么问，这时你就把它的元素个数定为“0”吧，就向上面的“文本们”变量一样，然后在程序代码中使用“重定义数组”函数来动态设置其长度和维数，就可以象普通数组一样使用了。

 

1.7.1 数组的维数

       前面我们说过，数组是同一类型数据的顺序集合，这种“顺序”，指的是数组数据是存储在连续的内存单元中的。然而我们可以从逻辑上把这些数据分割成“行”和“列”、或“长”、“宽”、“高”等从而形成多维数组。单维数组很容易理解，那么多维数组如何理解呢？请看下面的图示。

图1.7.1-a 一行队列是一个一维数组，可以按序号访问其中每个士兵

 

图1.7.1-b 行军方阵可以表示为二维数组，要访问其中的一个士兵，必须同时指定行和列

 

 

图1.7.1-c 三维数组，必须同时指定三个序号才能访问其中的一个元素

 

       至于三维以上的数组，则无法用图示表示出来了，有什么用处，也只有充分发挥你的想像力了。

       要改变数组的维数，请使用“重定义数组”函数。该函数可以对一个已存在数据的数组进行重新指定维数，而且能保证数组中已存的数据不丢失，这在实际编程中非常有用。要访问多维数组中的一个元素，需要使用多个方括号将其索引值分别括起来。图1.7.1-d的程序片断演示了多维数组的使用方法。

由程序的运行结果可以看出，如果指定新的维数后数组的元素个数比以前少，则多余的数据会删除；如果元素个数多余以前的，则多余的部分会根据数据类型以默认的数据进行填充。任何维度的数组都可以使用单维的访问方法访问其中的元素，但要注意索引值不能超过数组中元素的个数；“数组排序”函数依然是按照单维数组的方式进行排序。由此进一步说明，数组是在连续的内存空间中存储的，只是我们在“逻辑上”分成不同的维数，以方便编程。

图1.7.1-d 多维数组的使用

      

1.7.2 数组的排序

       易语言的数组操作类命令中提供了一个“数组排序”函数，此函数可以很方便有效地对数值型数组进行排序，使用起来也很简短，具体的用法请参见上例。在实际的编程中我们通常不仅要对数值型数组进行排序，还需要对文本型、时间日期型、自定义数据类型等的数组数据进行排序，因此我们经常需要编写自己的排序算法，这里我们就以对文本型数组进行排序为例，简述几种常用的排序算法。

       首先需要写几个辅助的函数：

1>随机生成文本的函数，这样方便我们初始化一个文本数组。具体代码如下：

图1.7.2-a 取随机文本的函数

 

2>用指定的文本数组来填充列表框的函数，以方便我们对比查看排序的结果。

图1.7.2-b 用文本数组来填充列表框的函数

 

       接下来就进入实质性的问题了。新建一个易语言程序，在窗口上放一个列表框和5个按钮，布局如图1.7.2-c所示。五个按钮的名字均为“按钮”加上其标题。添加一个程序集变量“集文本们”，类型为“文本型”，在“数组”一栏中输入“1000”，设定一个1000个元素的文本数组。

图1.7.2-c用于对文本数组排序的窗体界面

 

       为按钮“初始化数组”编写如下代码：

图1.7.2-d用于对文本数组排序的初始化代码

 

1.7.2.1 冒泡排序

       我们现来看看最简单、最容易实现的“冒泡排序”。冒泡排序的方法是这样的：从第一个元素开始，比较当前元素和下一个元素的值，如果当前元素大于下一个元素，就交换它们。显然，经过第一轮比较后，最大的元素被挪到了最后面。再对前面的元素重复这个过程，次大的元素移到倒数第二的位置，如此循环，直到最后两个元素，要么交换，要么不交换，排序结束。为什么把这个排序方法叫“冒泡排序”呢？因为在排序的过程中，数组中较大的元素象水中的气泡一样一个个浮到了上面，因此得名。为了直观理解，我们先以一串数字471253 进行排序观察：

       第一轮：427153→247153→247153→241753→241573→241537

       第二轮：241537→241537→214537→214537→214357

       第三轮：214357→124357→124357→123457

       ……

       具体的实现代码如图1.6.2.1-a。

图1.7.2.1-a对文本数组进行冒泡排序

       在这段代码中，每一次内循环就是一轮，每轮比较的次数逐步递减，因为每进行一轮比较，就有一个元素方到了后面正确的位置。因为一个元素是无法进行比较的，所以总共的轮数是元素的个数减1（从2到数组元素的个数）。从冒泡排序的源代码可以看到，对于一个有n个元素的数组，需要进行1+2+3+…+(n-1)=n·(n-1)/2 次比较，最坏的情况下每比较一次就要进行一次交换(刚好是逆序)，所以最多要交换n·(n-1)/2 次。

双击“冒泡排序”按钮，添加如下代码：

 

运行程序，点击按钮，观察排序的结果吧。

       从冒泡排序的算法来看，写成递归函数或许会更容易理解：排序前，是一个杂乱的数组。进行排序一轮后，最大的元素放到了最后，前面的仍是一个杂乱的数组，再对这个杂乱的数组进行排序，显然这是一个相似的过程。在递归调用的时候，只需指定最后一个要排的元素的位置（也就是要排序的元素的范围）就行了。递归冒泡排序的代码如图1.7.2.1-b所示。

图1.7.2.1-b 递归冒泡排序文本数组

 

       递归的算法代码稍微长一些，但更易于理解。该函数的使用方法和上面的一样，直接传递文本数组就可以了，不必传递“参最后位置”这个参数，它是递归自身使用的。

       冒泡排序的算法效率是比较低的，但对于少量的数据(1000个以下元素的文本数组)还是很有效的，在我的机器上，排序1000个长度均为8的文本的数组耗时约1400毫秒（可见我的机器快过时了）。递归的算法比较和交换的次数虽然一样，但因为递归的过程中要在栈上不断重新分配和释放空间，效率会比不用递归的低，占用的内存也大，这里权作一个演示。还有一个要注意的问题，文本比较本身比数值比较要慢得多，交换也慢得多，所以不能以数值排序的结果来判断文本排序的效率。

 

1.7.2.2 选择排序

       选择排序的思想是这样的：从数组中找出一个最小的，把它与第一个元素交换；从剩下的元素中再找出一个最小的，与第二个交换…… 如此循环，直到最后一个元素。很简单是不是？看看图1.7.2.2-a所示的代码。

       用选择排序来演示排序前面的那一串数字，是这样的：471253→174253→124753→123754→123457→123457，显然交换的步骤少了很多。

      

图1.7.2.2-a 选择排序文本数组

 

       从代码可以看出，对于有n个元素的数组，选择排序要进行(n-1)+(n-2)+…+1=n·(n-1)/2 次，这与冒泡排序一样，但最多交换n-1次(每一轮都交换一次)，所以效率要高很多。在我的机器上1000元素个长度均为8的文本数组大约耗时47毫秒，显然速度提高了很多。

 

1.7.2.3 插入排序

       你玩过扑克牌吗？你是否对一幅扑克牌进行排序过？如果要排序的话，我想是这样的：先抽出一张牌，放在桌面上，再抽出一张牌，看它比桌面上的牌大还是小，从而放在前面或后面，继续抽牌，与桌面上的牌进行比较并插在合适的位置……如此循环，直至手上的牌抽完。这个排序的思想就是插入排序。也很好理解，看看代码(图1.6.2.3)。

       在这段代码中，定义了一个局部临时文本数组，它就相当于放纸牌的桌面，我们要把元素逐一插入到该数组中。程序先把要排序的数组的第一个元素放入其中，然后把后续的元素也逐一插入其中，最终，“临时文本数组”就是一个排好序的数组了，在函数的最后将其复制给“参文本数组”变量以返回。

       从代码中可以看出，插入排序不用交换元素，只用进行比较即可，在最坏的情况下，需要比较1+2+3+…+（n-1）= n·(n-1)/2 次，但实际上远不会有这么多次。因此，插入排序效率比选择排序效率还要高，在我的机器上，排序1000个元素长度均为8个字符的文本数组平均耗时31毫秒。在这个子程序中，我仍然有疑惑的地方。按照我的理解，这里有两个地方会比较耗时（至少会比选择排序耗时），一是插入成员：在数组中插入成员会导致后面所有的元素向后移动，如果数据量大，显然很耗时；二是最后的将临时文本数组赋值给参文本数组，需要将数组数据全部复制过去，也应该是耗时的。但从实际的运行效果来看，速度却比选择排序快，如果不执行最后的复制语句，排序时间可以缩短为15毫秒，难道易语言的数组在内部实际是以链表的方式存储的？

图1.7.2.3 用插入排序来排序文本数组

 

 

1.7.2.4 快速排序

       快速排序是由C.R.A.Hoare于1962年提出的一种采取分治策略的交换排序算法。所谓“分治”，就是“分块治理”。这是一个经典的排序算法，在大部分的计算机上，快速排序算法通常可能比其他任何排序方法快很多（这也是快速排序名称的由来），因而快速排序广泛被用作编程语言或数据库的内置排序方法，例如易语言的“数组排序()”函数使用的就是快速排序算法。

       快速排序算法的基本过程是这样的：从一个无序数组中取出一个数据作为比较的基准，将小于或等于基准值的元素置于基准值的左侧，将大于或等于基准值的元素置于其右侧，这样操作之后，虽然左侧和右侧的数据仍是无序的，但基准值已放在了正确的位置上；再对左侧和右侧的数据分别执行同样的过程。这样经过多次递归操作之后，数据就排序好了。比如说要排序数据760923，我们先以7作为基准值，则第一轮操作后，数据变为602379，７被排在了正确的位置；右侧是9，显然不用再排了；左侧是6023，我们再以6作为基准值进行操作，则数据变为0236；返回，加上前面79，数据为023679，已经排序好了。

       具体到编程的实现上，可能没有上面说的这么简单，因为涉及到数组数据的比较和交换，你不能总是假定基准值的左侧元素个数和右侧元素个数一样多且刚好可以交换它们的位置。如果按照前面的说法，那么还需要一个临时数组(象插入排序一节讲的那样)，还要进行数组元素的插入操作，显然会影响效率。这里推荐一个很好的实现方法：①用i和j标记数组中要排序部分的左侧和右侧，将欲排序部分的第一个数据作为基准数据；②从数组的右侧(j)开始，向左搜寻第一个比基准值小的元素，找到后将其与基准值(i)交换，将ｊ递减１；将i递增１，再从左侧(i)向右搜寻比基准值大的元素，找到话将其与基准值(j)交换，将ｉ递增１……　如此循环，直至ｉ和j 碰头。这时候，基准值刚好放到了它该去的位置，而且比其小的元素在其左侧，比其大的元素在其右侧；③对其左侧和右侧分别进行类似处理。

       由此，可得快速排序算法的具体代如图1.6.2.4-a所示。用代码的执行过程我们对数据760923进行排序分析试试。

首先i=1,j=6，基准值=7；

进入循环，发现３比７小，于是“参文本们 [1] ＝ 参文本们 [6]”，得360923，咦？怎么把７搞丢了，７没有换过来？非也，这里是程序的一个小优化，本来这里应该交换元素１和６的值，但实际在排序的过程中，基准值始终左右跳动，而且其值已经保存在了变量“基准值”中，只要最后找准了基准值的位置，直接把值填进去即可，所以，这里忽略了将基准值赋给元素６的操作，减去不少运行开销。交换后i=2；

再从左侧搜寻，搜到９，发现比７大，此时i=4,j=6，于是执行“参文本们 [6] ＝ 参文本们 [4]”，得数据360929，执行后j自减1，所以此时i=4，j=5；

再从右搜寻，发现2比７小，于是又执行“参文本们 [4] ＝ 参文本们 [5]”得360299，然后i=５=j，i与j相等，循环后部分都不满足而跳出循环；

执行“参文本们 [5] ＝ 基准值”将基准值填入，得360279，７被补到了正确的位置上，数组被７分成了左右两部分；

对左部分3602按上面的步骤进行递归处理；

右部分只有一个元素９，直接退出。

 

       嗯，还挺复杂的，对不对？给按钮“快速排序”添加前面类似的代码，运行程序，哇！时间减少到惊人的15毫秒，有时甚至是０毫秒！简直就是飞快！对10000个数据的文本数组排序也仅需125毫秒!

       但是在运行的过程中我们也发现了一个问题，那就是对排好序的数据再进行排序，时间反而花得更多，这是为什么呢？这是因为排好序之后，每次选取的基准值恰好是最小的，于是每次分割的时候，基准值左右两侧的元素个数严重不平衡——一边为０，一边为剩余的元素，快速排序遇到了最糟糕的情形，虽然如此，其速度还是远远超过冒泡排序，也胜过选择排序。由此可见，基准值的选择是多么重要，在一些描述快速排序算法的专著中，有很大的篇幅是讨论关于基准值的选取的，通常有以下方法：

①取中间元素：使用数组中要排序部分中间的那个元素；

②三者取中：取数组左侧、右侧和中间元素中不大不小的那个元素；

③随机取一个元素。

具体的实现就留给有兴趣的你来完成了。

对于快速排序，如果你不能完全理解或不想深入理解也完全可以使用该算法：你只需把这个子程序作为模板，修改为满足你自己的数据类型就可以完成对其他数据类型和自定义数据类型数组的快速排序(在此我开始有点怨恨易语言不支持模板函数了)。

图 1.7.2.4-a 快速排序算法

 

1.7.2.5 自定义数据类型数组的多级排序

       在实际的编程过程中，我们通常不仅需要对基本数据类型进行排序，更多的是需要对自定义数据类型按照指定的成员进行排序；不仅如此，某些应用要求我们指定排序关键字的优先级次序。比如对一批商品，我们需要先按产地进行排序，产地相同的再按类别进行排序，类别相同的再按售价排序。我们称这样的排序为多级排序。

       为此，我们新建一个易程序，并定义如下的数据类型：

图1.7.2.5-a 用于多级排序的自定义数据类型

 

       在_启动窗口上创建一个超级列表框和一个按钮，并设置超级列表框的相关属性如图1.7.2.5-b所示，这里超级列表框中要显示排序前和排序后的数据。

图1.7.2.5-b 用于多级排序的窗体

 

       在启动窗口程序集中添加一个程序集变量“集商品们”，类型为“商品”，数组成员个数为1000。然后写一些初始化数组和超级列表框的代码，如下：

 

.版本 2

.支持库 iext

 

.程序集 窗口程序集1

.程序集变量 集商品们, 商品, , "1000"

 

.子程序 更新超级列表框

.局部变量 i, 整数型

.局部变量 表项索引, 整数型

 

超级列表框1.全部删除 ()

.计次循环首 (取数组成员数 (集商品们), i)

    表项索引＝ 超级列表框1.插入表项 (, 到文本 (集商品们 [i].编号), , , , i)

    超级列表框1.置标题 (表项索引, 1, 集商品们 [i].品名)

    超级列表框1.置标题 (表项索引, 2, 集商品们 [i].类别)

  超级列表框1.置标题 (表项索引, 3, 到文本 (集商品们 [i].售价))

    超级列表框1.置标题 (表项索引, 4, 集商品们 [i].产地)

.计次循环尾 ()

 

 

.子程序 __启动窗口_创建完毕

.局部变量 i, 整数型

 

' 初始化数组的成员

.计次循环首 (取数组成员数 (集商品们), i)

    集商品们 [i].编号 ＝ i

    集商品们 [i].品名 ＝ 取随机品名 ()

    集商品们 [i].类别 ＝ 多项选择 (取随机数 (1, 8), “家电”, “服饰”, “文具”, “餐具”, “洁具”, “工具”, “食品”, “饮料”)

    集商品们 [i].售价 ＝ 取随机数 (, ) ÷ 100

    集商品们 [i].产地 ＝ 多项选择 (取随机数 (1, 8), “湖南”, “湖北”, “河南”, “河北”, “广东”, “广西”, “山东”, “山西”)

.计次循环尾 ()

更新超级列表框 ()

 

 

.子程序 取随机品名, 文本型, , 获取一个随机的商品名称

.局部变量 长度, 整数型

.局部变量 i, 整数型

.局部变量 品名, 文本型

 

长度 ＝ 取随机数 (4, 12)

.计次循环首 (长度, i)

    品名＝ 品名 ＋ 字符 (取随机数 (65, 90))

.计次循环尾 ()

返回 (品名)

 

       写了这些代码后，运行程序，就会有随机数据填充超级列表框(见图1.7.2.5-c)。

 

图1.7.2.5-c 排序前随机填充的数据

 

       从前面的排序例子中我们知道，快速排序速度是最快的，这里我们就采用快速排序算法。我们只需要按照前面的快速排序的方法套几个出来就行了。先套按产地排序的，代码如图1.6.2.5-d。将该代码与前面的快速排序的代码对比一下，可以看到，改动的部分很少，所以说，即使不能透彻理解快速排序也没有关系，按这个模板套吧。

       这个函数写完了之后，我们双击窗体上的按钮，给它添加一点测试代码，看看排序是否正确。测试代码只有两句：

 

.子程序 _按钮排序_被单击

 

根据产地排序 (集商品们)

更新超级列表框 ()

 

       运行程序，单击按钮，可以看到数据正确地按产地进行排序了。

图1.7.2.5-d 根据产地进行排序

 

       测试成功之后，我们再把该函数复制粘贴两个，修改函数名和其中自定义数据类型的成员变量，得到如下两个函数（鉴于排版关系，这里使用文本方式）：

 

.版本 2

 

.子程序 根据类别排序, , , 用快速排序的方法根据类别排序

.参数 参商品们, 商品, 数组

.参数 参左边, 整数型

.参数 参右边, 整数型

.局部变量 基准点, 整数型

.局部变量 基准值, 商品

.局部变量 i

.局部变量 j

 

.如果真 (参左边 ≥ 参右边)

    返回 ()

.如果真结束

i ＝ 参左边

j ＝ 参右边

基准值 ＝ 参商品们 [参左边]

.判断循环首 (i ＜ j)

    .判断循环首 (i ＜ j 且 参商品们 [j].类别 ≥ 基准值.类别)

        j ＝ j － 1

    .判断循环尾 ()

    .如果真 (i ＜ j)

        参商品们 [i] ＝ 参商品们 [j]

        i ＝ i ＋ 1

    .如果真结束

    .判断循环首 (i ＜ j 且 参商品们 [i].类别 ≤ 基准值.类别)

        i ＝ i ＋ 1

    .判断循环尾 ()

    .如果真 (i ＜ j)

        参商品们 [j] ＝ 参商品们 [i]

        j ＝ j － 1

    .如果真结束

 

.判断循环尾 ()

参商品们 [i] ＝ 基准值

根据类别排序 (参商品们, 参左边, i － 1)

根据类别排序 (参商品们, i ＋ 1, 参右边)

 

 

.子程序 根据售价排序, , , 用快速排序的方法根据类别排序

.参数 参商品们, 商品, 数组

.参数 参左边, 整数型

.参数 参右边, 整数型

.局部变量 基准点, 整数型

.局部变量 基准值, 商品

.局部变量 i

.局部变量 j

.如果真 (参左边 ≥ 参右边)

    返回 ()

.如果真结束

i ＝ 参左边

j ＝ 参右边

基准值 ＝ 参商品们 [参左边]

.判断循环首 (i ＜ j)

    .判断循环首 (i ＜ j 且 参商品们 [j].售价 ≥ 基准值.售价)

        j ＝ j － 1

    .判断循环尾 ()

    .如果真 (i ＜ j)

        参商品们 [i] ＝ 参商品们 [j]

        i ＝ i ＋ 1

    .如果真结束

    .判断循环首 (i ＜ j 且 参商品们 [i].售价 ≤ 基准值.售价)

        i ＝ i ＋ 1

    .判断循环尾 ()

    .如果真 (i ＜ j)

        参商品们 [j] ＝ 参商品们 [i]

        j ＝ j － 1

    .如果真结束

 

.判断循环尾 ()

参商品们 [i] ＝ 基准值

根据售价排序 (参商品们, 参左边, i － 1)

根据售价排序 (参商品们, i ＋ 1, 参右边)

 

       这样复制粘贴子程序，然后修改每个子程序中对应的一小部分，是不是感觉到有点蠢？应该有更省事更科学的办法吧？是的，你的感觉是对的，这就是C++中模板函数的由来，可惜易语言目前不支持，只好先就这么办了。写好这些代码后对它们逐一测试，测试的代码如下：

 

.子程序 _按钮排序_被单击

 

' 根据产地排序 (集商品们)

' 根据类别排序 (集商品们, 1, 取数组成员数 (集商品们))

根据售价排序 (集商品们, 1, 取数组成员数 (集商品们))

更新超级列表框 ()

 

       根据需要注释掉其他两个排序函数的调用就行了。如果三个都不注释掉，那会是什么结果呢？最后的排序函数将前面两个排序函数排的结果冲掉了，结果仅仅是按售价进行排序了。

       为了实现多级排序，我们还要写一些代码。商品数据按产地进行排序了之后，相同产地的数据就摆放到一起了，我们再分别对这些数据块按类别进行排序；这样排序之后，相同产地、相同类别的产品又排放到了一起，我们再分别对这些更小的数据块按售价进行排序就完成了具体的排序代码如下：

 

.版本 2

 

.子程序 _按钮排序_被单击

.局部变量 i, 整数型

.局部变量 产地, 文本型

.局部变量 起始, 整数型

.局部变量 终止, 整数型

.局部变量 类别, 文本型

.局部变量 售价, 小数型

.局部变量 起始时间

 

起始时间 ＝ 取启动时间 ()

根据产地排序 (集商品们)  ' 先根据产地排序

 

' 以下代码按产地分块根据类型进行排序

产地 ＝ 集商品们 [1].产地

起始 ＝ 1

.计次循环首 (取数组成员数 (集商品们), i)

    .如果真 (集商品们 [i].产地 ≠ 产地)  ' 根据产地分块

        终止 ＝ i － 1

        根据类别排序 (集商品们, 起始, 终止)  ' 按类别排序

        产地＝ 集商品们 [i].产地

        起始＝ i

    .如果真结束

 

.计次循环尾 ()

根据类别排序 (集商品们, 起始, 1000)

 

' 以下代码按类别分块根据售价进行排序

类别 ＝ 集商品们 [1].类别

起始 ＝ 1

.计次循环首 (取数组成员数 (集商品们), i)

    .如果真 (集商品们 [i].类别 ≠ 类别)  ' 根据类别分块

        终止 ＝ i － 1

        根据售价排序 (集商品们, 起始, 终止)  ' 按售价排序

        类别＝ 集商品们 [i].类别

        起始＝ i

    .如果真结束

 

.计次循环尾 ()

根据售价排序 (集商品们, 起始, 1000)

信息框 (“排序所花时间：” ＋ 到文本 (取启动时间 () － 起始时间) ＋ “毫秒。”, 0, )

' 根据类别排序 (集商品们, 1, 取数组成员数 (集商品们))

' 根据售价排序 (集商品们, 1, 取数组成员数 (集商品们))

更新超级列表框 ()

 

       在我的机器上，排序这1000条数据耗时78毫秒。图1.7.2.5-e 是排序后的结果。

 

图1.7.2.5-e 自定义数据类型排序后的结果

 

1.7.3 [例]扫雷游戏

       这里我们综合前面所讲的知识——自定义数据类型、数组和递归——来开发一个小游戏——扫雷。Windows 操作系统从95时代就自带这样一个扫雷游戏了，是个很小的益智类游戏，如果你还没有玩过它，那么建议你先玩一玩，这样编程时就有一个感性的认识，读代码时也更容易理解。

       在编程之前，对我们要实现的功能仔细分析一下是很有好处的，这便于我们设计好的数据结构和算法，所以在此我对扫雷游戏作一个简要的介绍。

       在扫雷游戏中，默认情况下程序会提供一个9×9的方格雷区，并在其中的某些格子中随机分布10个雷。你需要通过单击鼠标左键来翻开雷块，如果该雷块下有雷，则你不幸“壮烈牺牲”，游戏结束；如果没有雷，则程序会自动显示该雷块周围8个雷块中雷数的总和，你要根据该数值来判断周围的布雷情况，进而找处所有有雷的雷块；如果你确定某个雷块下有雷，则在其上单击右键，插一面红旗，如果不能确定有没有雷，则再次单击鼠标右键，标记为“？”。在你扫雷的时候，程序还会提供一些便利：如果某个雷块周围雷的总数为0，那么显然周围都没有雷，程序会自动翻开周围的雷块而免去你用鼠标逐个点击。在游戏进行的时候，程序会自动计时，如果你在很短的时间内找出了所有的雷，那么你就上排行榜了！扫雷游戏的界面如图1.6.3-a 所示。

图1.7.3-a 在进行中的扫雷游戏

 

       游戏过程分析完毕，下面我们开始动手编程了。首先当然是设计界面啦。古人云：“三思而后行”，所以对于主界面上的九九八十一个按钮，这里我想了三个方案：

       1. 使用“按钮样式”为“真”的选择框，去掉标题，弄成正方形，然后复制粘贴排成矩阵。这样它们在鼠标按下时会自动呈现按下状态，好像很不错，是不是？但是很不幸，答案是不行，你想想吧，八十一个按钮，不光手动创建按钮麻烦，而且你还得为每个按钮写鼠标左键和鼠标右键事件，显然行不通。

       2. 在上面的想法的基础上，我们深入考虑一下：既然静态创建的控件不行，那么使用动态复制组件可以吗？我们知道，在易语言中可以通过“复制窗口组件（）”函数和“取事件组件（）”函数来简化大量的手动创建组件的工作量和代码的编写量，这个方案比上一个方案强些，可以行得通。但是还有一些问题：如何在按钮上画上旗帜？而且对于游戏来说，这样的灰按钮显得很呆板，不好看。比如说，我就不喜欢Windows自带的扫雷游戏的界面。

       3. 既然上面的方法有的行不通，有的不太理想，那么我们自己在画板来绘制这些按钮，然后根据鼠标在画板上的事件位置来判断单击的是哪个按钮如何？看上去很复杂，其实很简单的，我们只用写两个函数即可：一个画凸出的那种按钮，一个画被按下的那种按钮，根据其参数画在指定的地方。况且且我们自己画的按钮可以随心所欲地更改显示效果，因此会比系统默认的按钮要好看得多，虽然会多花点代码和时间，但绝对是值得的。

 

       既然决定自己画按钮，那么还得思考一些细节问题：对于按下的按钮，会存在显示雷数或画地雷两种状态；对于凸起的按钮，存在空白、有旗帜、有问号三种状态，而且这三种状态会在用户不断单击右键的时候循环显示，所以把这三种状态设计成连续的数字最好不过了，这样就可以通过递增取余数的方法来确定其状态。还有，跟Windows自带的扫雷游戏有点不同，我们决定做一个8×8的雷区（在计算机中，8这个数字好啊），每个雷块的宽度为20。既然如此，我们先定义了如图1.7.3-b所示的常量。

1.7.3-b 扫雷游戏中使用的常量

 

       雷块宽度确定了之后，窗口的宽度和高度也就定了。这里我将_启动窗口改动过的属性列在下表1.7.3-a中。

 

_启动窗口

属性

宽度

170

高度

208

标题

扫雷

边框

镜框式固定边框

控制按钮

真

最大化按钮

假

最小化按钮

真

表1.7.3-a 扫雷游戏中_启动窗口的属性

 

       为_启动窗口设计如图1.7.3-c所示的菜单。

图1.7.3-c 扫雷游戏的菜单

 

然后在_启动窗口上添加一个画板，设置属性如表1.7.3-b。

 

画板

属性

名称

画板

宽度

160

高度

160

自动重画

真

字体

Arial Black，12

可停留焦点

真

表1.7.3-c扫雷游戏中画板性

 

       接下来就要写两个画按钮的函数了，一个画凸起按钮，一个画按下的按钮。画凸起按钮函数的具体实现代码如下：

 

 

.子程序 画凸起按钮

.参数 参横坐标, 整数型, , 以数组索引为单位

.参数 参纵坐标, 整数型, , 以数组索引为单位

.参数 参按钮类型, 整数型, , 0:空白,1:旗帜,2:问号

.局部变量 x, 整数型

.局部变量 y, 整数型

 

x ＝ #雷块宽度 × (参横坐标 － 1)

y ＝ #雷块宽度 × (参纵坐标 － 1)

画板.画渐变矩形 (x, y, #雷块宽度, #雷块宽度, #从左上到右下, 取颜色值 (131, 192, 252), 取颜色值 (3, 92, 182))

画板.画笔颜色 ＝ 取颜色值 (1, 78, 155)

画板.刷子类型 ＝ 0

画板.画笔类型 ＝ 0

画板.画矩形 (x, y, x ＋ #雷块宽度, y ＋ #雷块宽度)

画板.画渐变矩形 (x ＋ 2, y ＋ 2, #雷块宽度 － 4, #雷块宽度 － 4, #从上到下, 取颜色值 (175, 216, 254), 取颜色值 (91, 173, 253))

画板.画笔类型 ＝ 1

.判断开始 (参按钮类型 ＝ #凸起按钮_旗帜)

    ' 以下代码绘制旗帜、旗杆和基座

    画板.画笔颜色 ＝ #红色

    画板.刷子颜色 ＝ #红色

    画板.刷子类型 ＝ 1

    画板.画矩形 (x ＋ 9, y ＋ 4, x ＋ 16, y ＋ 9)

    画板.画笔颜色 ＝ #黑色

    画板.画笔粗细 ＝ 1

    画板.画直线 (x ＋ 8, y ＋ 3, x ＋ 8, y ＋ 15)

    画板.画笔粗细 ＝ 2

    画板.画直线 (x ＋ 5, y ＋ 15, x ＋ 13, y ＋ 15)

.判断 (参按钮类型 ＝ #凸起按钮_问号)

    画板.定位写出 (x ＋ 4, y － 2, “?”)  ' 画问号

.默认

 

 

 

.子程序 画凸起按钮

.参数 参横坐标, 整数型, , 以数组索引为单位

.参数 参纵坐标, 整数型, , 以数组索引为单位

.参数 参按钮类型, 整数型, , 0:空白,1:旗帜,2:问号

.局部变量 x, 整数型

.局部变量 y, 整数型

 

x ＝ #雷块宽度 × (参横坐标 － 1)

y ＝ #雷块宽度 × (参纵坐标 － 1)

画板.画渐变矩形 (x, y, #雷块宽度, #雷块宽度, #从左上到右下, 取颜色值 (131, 192, 252), 取颜色值 (3, 92, 182))

画板.画笔颜色 ＝ 取颜色值 (1, 78, 155)

画板.刷子类型 ＝ 0

画板.画笔类型 ＝ 0

画板.画矩形 (x, y, x ＋ #雷块宽度, y ＋ #雷块宽度)

画板.画渐变矩形 (x ＋ 2, y ＋ 2, #雷块宽度 － 4, #雷块宽度 － 4, #从上到下, 取颜色值 (175, 216, 254), 取颜色值 (91, 173, 253))

画板.画笔类型 ＝ 1

.判断开始 (参按钮类型 ＝ #凸起按钮_旗帜)

    ' 以下代码绘制旗帜、旗杆和基座

    画板.画笔颜色 ＝ #红色

    画板.刷子颜色 ＝ #红色

    画板.刷子类型 ＝ 1

    画板.画矩形 (x ＋ 9, y ＋ 4, x ＋ 16, y ＋ 9)

    画板.画笔颜色 ＝ #黑色

    画板.画笔粗细 ＝ 1

    画板.画直线 (x ＋ 8, y ＋ 3, x ＋ 8, y ＋ 15)

    画板.画笔粗细 ＝ 2

    画板.画直线 (x ＋ 5, y ＋ 15, x ＋ 13, y ＋ 15)

.判断 (参按钮类型 ＝ #凸起按钮_问号)

    画板.定位写出 (x ＋ 4, y － 2, “?”)  ' 画问号

.默认

 

 

       这里说明一下，在后续的章节中，因为整幅的图片不便于排版，对于大段的源代码，将直接以文字的方式编排出来，这对源代码的理解并没有影响。

       在这段代码中，传递的“参横坐标”和“参纵坐标”参数是雷块的横纵编号，其范围为1到8之间，所以先要计算出实际的绘制坐标x和y，将编号值减一乘以每个雷块的宽度就是了。为了将雷块画得美观，这里我们使用了易语言提供的“画渐变矩形”方法：先从左上角往右下角画一个渐变矩形，然后画边框，再叠画一个小的渐变矩形，这样就形成了一个很漂亮的按钮。具体的图示如下：

图1.7.3-d 画按钮的过程

 

       该函数还根据传递的“参按钮类型”来判断是绘制旗帜、问号或留空白。这些代码很简单，就懒得敲键盘详说了。

 

       画按下按钮的代码是这个样子的：

 

 

.子程序 画凹下按钮

.参数 参横坐标, 整数型, , 以数组索引为单位

.参数 参纵坐标, 整数型, , 以数组索引为单位

.参数 参周围雷数, 整数型, , -1表示是雷，其他是雷的数目

.局部变量 x

.局部变量 y

 

x ＝ #雷块宽度 × (参横坐标 － 1)

y ＝ #雷块宽度 × (参纵坐标 － 1)

画板.画笔类型 ＝ 0  ' 空画笔

画板.画笔颜色 ＝ #深灰

画板.刷子类型 ＝ 1  ' 颜色刷子

画板.刷子颜色 ＝ 取颜色值 (224, 242, 254)

画板.画矩形 (x, y, x ＋ #雷块宽度, y ＋ #雷块宽度)

.判断开始 (参周围雷数 ＝ -1)  ' 是地雷

    ' 以下代码画地雷

    画板.画笔颜色 ＝ #黑色

    画板.画笔类型 ＝ 0

    画板.刷子颜色 ＝ #黑色

    画板.刷子类型 ＝ 1

    画板.画椭圆 (x ＋ 4, y ＋ 4, x ＋ #雷块宽度 － 4, y ＋ #雷块宽度 － 4)

    画板.画直线 (x ＋ 2, y ＋ #雷块宽度 ÷ 2, x ＋ #雷块宽度 － 2, y ＋ #雷块宽度 ÷ 2)

    画板.画直线 (x ＋ #雷块宽度 ÷ 2, y ＋ 2, x ＋ #雷块宽度 ÷ 2, y ＋ #雷块宽度 － 2)

    画板.画直线 (x ＋ 4, y ＋ 4, x ＋ #雷块宽度 － 4, y ＋ #雷块宽度 － 4)

    画板.画直线 (x ＋ #雷块宽度 － 4, y ＋ 4, x ＋ 4, y ＋ #雷块宽度 － 4)

    画板.刷子颜色 ＝ #白色

    画板.画笔颜色 ＝ #黑色

    画板.画椭圆 (x ＋ 6, y ＋ 6, x ＋ 10, y ＋ 10)

    返回 ()

.判断 (参周围雷数 ＝ 0)

    返回 ()

.判断 (参周围雷数 ＝ 1)

    画板.文本颜色 ＝ #蓝色

.判断 (参周围雷数 ＝ 2)

    画板.文本颜色 ＝ #深青

.默认

    画板.文本颜色 ＝ #红色

.判断结束

画板.定位写出 (x ＋ 4, y － 2, 参周围雷数)

 

 

 

.子程序 画凹下按钮

.参数 参横坐标, 整数型, , 以数组索引为单位

.参数 参纵坐标, 整数型, , 以数组索引为单位

.参数 参周围雷数, 整数型, , -1表示是雷，其他是雷的数目

.局部变量 x

.局部变量 y

 

x ＝ #雷块宽度 × (参横坐标 － 1)

y ＝ #雷块宽度 × (参纵坐标 － 1)

画板.画笔类型 ＝ 0  ' 空画笔

画板.画笔颜色 ＝ #深灰

画板.刷子类型 ＝ 1  ' 颜色刷子

画板.刷子颜色 ＝ 取颜色值 (224, 242, 254)

画板.画矩形 (x, y, x ＋ #雷块宽度, y ＋ #雷块宽度)

.判断开始 (参周围雷数 ＝ -1)  ' 是地雷

    ' 以下代码画地雷

    画板.画笔颜色 ＝ #黑色

    画板.画笔类型 ＝ 0

    画板.刷子颜色 ＝ #黑色

    画板.刷子类型 ＝ 1

    画板.画椭圆 (x ＋ 4, y ＋ 4, x ＋ #雷块宽度 － 4, y ＋ #雷块宽度 － 4)

    画板.画直线 (x ＋ 2, y ＋ #雷块宽度 ÷ 2, x ＋ #雷块宽度 － 2, y ＋ #雷块宽度 ÷ 2)

    画板.画直线 (x ＋ #雷块宽度 ÷ 2, y ＋ 2, x ＋ #雷块宽度 ÷ 2, y ＋ #雷块宽度 － 2)

    画板.画直线 (x ＋ 4, y ＋ 4, x ＋ #雷块宽度 － 4, y ＋ #雷块宽度 － 4)

    画板.画直线 (x ＋ #雷块宽度 － 4, y ＋ 4, x ＋ 4, y ＋ #雷块宽度 － 4)

    画板.刷子颜色 ＝ #白色

    画板.画笔颜色 ＝ #黑色

    画板.画椭圆 (x ＋ 6, y ＋ 6, x ＋ 10, y ＋ 10)

    返回 ()

.判断 (参周围雷数 ＝ 0)

    返回 ()

.判断 (参周围雷数 ＝ 1)

    画板.文本颜色 ＝ #蓝色

.判断 (参周围雷数 ＝ 2)

    画板.文本颜色 ＝ #深青

.默认

    画板.文本颜色 ＝ #红色

.判断结束

画板.定位写出 (x ＋ 4, y － 2, 参周围雷数)

 

 

       同样地，这个子程序也需要接受按钮的横纵坐标编号并转换成实际的绘制坐标，按钮被按下后，绘制一个有边界的浅蓝色填充的矩形就完事，除此之外，还需要该雷块周围的雷的个数，根据个数设置文本的颜色；如果个数>0，则绘制文本；如果个数是-1，则表示要画一个地雷，画雷的次序是这样的：

图1.7.3-e 画地雷的过程

 

 

       当然，按钮的绘制方式和配色并不是一成不变的的，你可以根据自己的喜好来绘制有你的特色的按钮——如果你愿意，还可以作成换肤型的扫雷程序。

 

       接下来，我们对这两个函数测试一下，在“__启动窗口_创建完毕”中添加如下代码：

 

 

.子程序 __启动窗口_创建完毕

 

画凸起按钮 (1, 1, #凸起按钮_空白)

画凸起按钮 (8, 8, #凸起按钮_旗帜)

画凸起按钮 (1, 8, #凸起按钮_问号)

画凹下按钮 (8, 1, -1)

画凹下按钮 (4, 4, 3)

 

 

运行程序，你应该得到如图1.7.3-f所示的情形。可以看到，按钮以正确的方式画到了正确的位置上。测试完毕后，请及时注释掉或删除掉以上代码。

图1.7.3-f 测试画按钮函数

 

       好了，绘制按钮已不成问题，但如何判断用户单击的按钮是否有雷？ 又如何判断用户在一个按钮上有鼠标右击了几次？显然，我们还需要在内存中保存当前雷的分布情况和按钮的当前状态，并把这些信息和按钮一一对应起来，这就需要一个自定义数据类型和一个保存该数据类型的8×8的数组。

       为此，我们定义如下的数据类型：

 

图1.7.3-g 定义雷块数据类型

 

       还需要在窗口程序集中添加图1.6.3-f所示的程序集变量。其中“集游戏已结束”用来判断游戏是否已结束，如果已结束，按钮就不能再点击了。“集雷块阵列”是一个雷块数组，初始长度设为0，我们需要重新定义其为一个8×8的数组。

图1.7.3-h 扫雷游戏的程序集变量

 

       为此，有一个子程序是必须要写的，那就是“新游戏”。程序每次起动时都要调用此子程序，用户单击“新游戏”菜单项的时候也需要调用它，在此子程序中，我们需要重设游戏结束标志、重画所有按钮、重新布雷，所以其代码如下：

 

 

.子程序 新游戏

.局部变量 i, 整数型

.局部变量 j, 整数型

 

集游戏已结束 ＝ 假

重定义数组 (集雷块阵列, 假, 8, 8)  ' 定义一个8×8的雷块数组

置随机数种子 ()

' 先重画所有按钮

.计次循环首 (8, i)

    .计次循环首 (8, j)

        画凸起按钮 (j, i, #凸起按钮_空白)

    .计次循环尾 ()

.计次循环尾 ()

' 再设置雷区数组

.计次循环首 (10, )  ' 随机放１０个雷

    .循环判断首 ()

        i ＝ 取随机数 (1, 8)  ' 随机取一个地方

        j ＝ 取随机数 (1, 8)

        ' 已放雷的地方就不再放，直到找到空地方

    .循环判断尾 (集雷块阵列 [i] [j].有雷)

    集雷块阵列 [i] [j].有雷 ＝ 真

.计次循环尾 ()

 

 

 

.子程序 新游戏

.局部变量 i, 整数型

.局部变量 j, 整数型

 

集游戏已结束 ＝ 假

重定义数组 (集雷块阵列, 假, 8, 8)  ' 定义一个8×8的雷块数组

置随机数种子 ()

' 先重画所有按钮

.计次循环首 (8, i)

    .计次循环首 (8, j)

        画凸起按钮 (j, i, #凸起按钮_空白)

    .计次循环尾 ()

.计次循环尾 ()

' 再设置雷区数组

.计次循环首 (10, )  ' 随机放１０个雷

    .循环判断首 ()

        i ＝ 取随机数 (1, 8)  ' 随机取一个地方

        j ＝ 取随机数 (1, 8)

        ' 已放雷的地方就不再放，直到找到空地方

    .循环判断尾 (集雷块阵列 [i] [j].有雷)

    集雷块阵列 [i] [j].有雷 ＝ 真

.计次循环尾 ()

 

 

       在这段代码中，程序先把数组重新定义成了一个8×8的数组，这样每个数组成员就可以很方便地与按钮对应起来了。我们还看到在布雷的时候，使用了一个循环判断。这个循环的目的是防止把两个或两个以上的雷放到了一个雷块中，否则雷的总数就不是10了。

       在“__启动窗口_创建完毕”事件中，添加一句代码“新游戏()”，再次运行程序，我们得到了如下的结果：

 

图1.6.3-i 新游戏后绘制的按钮

 

       这些按钮可比Windows自带的扫雷游戏的按钮漂亮多了，但悲惨的是，它们都还是花架子——不能响应鼠标点击。

       接下来，我们需要给画板添加响应鼠标事件的子程序。在这些子程序中，首要要把鼠标的位置转换成数组的索引，然后根据点击的鼠标键和周围的布雷情况重画该按钮。在这里，点击鼠标左键的情况要复杂些：1. 按下后要立即算出周围的雷数；2. 如果周围的雷数为0，则还要自动按下周围的按钮，如果周围的雷块中有的周围的雷数也是0，则还要自动按下该按钮周围的按钮……显然这里要用到递归。而要自动点按周围的按钮，则需要遍历周围的8个按钮，如果该按钮已被按下或以被右键作了标示，就跳过，否则就计算该按钮的中心坐标，用鼠标去点击画板的那个位置，如此循环往复；3. 如果有雷，则需要画出所有的雷，同时在用户标了旗帜但没有雷的按钮上画上红叉叉，同时宣布则游戏结束。如果是点击右键，则把该按钮对应的数组成员“右击索引”加1，并求与3的余数，这样其值刚好与凸起按钮的三种类型相对应，从而绘制出不同的状态。不论是鼠标左键按下和右键按下，都要判断用户是否已标出了所有的雷，这个代码由子程序“判断胜负”来完成 。具体的实现代码如下：

 

 

.子程序 _画板_鼠标左键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 x, 整数型

.局部变量 y, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

m ＝ 横向位置 ÷ #雷块宽度 ＋ 1

n ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [m] [n].已按下)

    返回 ()

.如果真结束

集雷块阵列 [m] [n].已按下 ＝ 真

集雷块阵列 [m] [n].右击索引 ＝ 0

.如果 (集雷块阵列 [m] [n].有雷)

    ' 有雷则显示所有的雷，同时弹出信息框

    .计次循环首 (8, j)

        .计次循环首 (8, i)

            .如果 (集雷块阵列 [i] [j].有雷)

                画凹下按钮 (i, j, -1)

            .否则

                ' 没有雷却标了旗帜的地方则画叉叉

                .如果真 (集雷块阵列 [i] [j].右击索引 ＝ #凸起按钮_旗帜)

                    画红叉叉 (i, j)

                .如果真结束

            .如果结束

        .计次循环尾 ()

    .计次循环尾 ()

    集游戏已结束 ＝ 真

    信息框 (“你踩雷了！”, #错误图标, )

    返回 ()

.否则

    ' 无雷则显示周围雷的数目，数目是０则不显示且递归按周围的按钮

    ' 遍历周围的雷块

    .变量循环首 (n － 1, n ＋ 1, 1, j)

        .变量循环首 (m － 1, m ＋ 1, 1, i)

            .如果真 (i ≥ 1 且 i ≤ 8 且 j ≥ 1 且 j ≤ 8)

                ' 确保在数组索引范围内，以免导致数组下标越界错误！

                .如果真 (集雷块阵列 [i] [j].有雷)

                    周围雷数 ＝ 周围雷数 ＋ 1

                .如果真结束

 

            .如果真结束

 

        .变量循环尾 ()

    .变量循环尾 ()

    画凹下按钮 (m, n, 周围雷数)

    .如果真 (周围雷数 ＝ 0)

        ' 如果周围雷数是０，则还需要自动向周围扩展。(递归)

        .变量循环首 (n － 1, n ＋ 1, 1, j)

            .变量循环首 (m － 1, m ＋ 1, 1, i)

                .如果真 (i ＜ 1 或 i ＞ 8 或 j ＜ 1 或 j ＞ 8)

                    ' 确保数组下标不会越界

                    到循环尾 ()

                .如果真结束

                .如果真 (集雷块阵列 [i] [j].已按下 或 集雷块阵列 [i] [j].右击索引 ≠ #凸起按钮_空白)

                    ' 已标为旗帜或问号的则不翻出来

                    到循环尾 ()  ' 此处退出递归，否则会因无限递归而死机。

                .如果真结束

                x ＝ i × #雷块宽度 － #雷块宽度 ÷ 2

                y ＝ j × #雷块宽度 － #雷块宽度 ÷ 2

                _画板_鼠标左键被按下 (x, y, 功能键状态)  ' 此处递归调用自身

            .变量循环尾 ()

        .变量循环尾 ()

    .如果真结束

 

.如果结束

判断胜负 ()

 

 

.子程序 _画板_鼠标右键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

i ＝ 横向位置 ÷ #雷块宽度 ＋ 1

j ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [i] [j].已按下)

    返回 ()

.如果真结束

集雷块阵列 [i] [j].右击索引 ＝ (集雷块阵列 [i] [j].右击索引 ＋ 1) ％ 3

画凸起按钮 (i, j, 集雷块阵列 [i] [j].右击索引)

判断胜负 ()

 

 

 

.子程序 新游戏

.局部变量 i, 整数型

.局部变量 j, 整数型

 

集游戏已结束 ＝ 假

重定义数组 (集雷块阵列, 假, 8, 8)  ' 定义一个8×8的雷块数组

置随机数种子 ()

' 先重画所有按钮

.计次循环首 (8, i)

    .计次循环首 (8, j)

        画凸起按钮 (j, i, #凸起按钮_空白)

    .计次循环尾 ()

.计次循环尾 ()

' 再设置雷区数组

.计次循环首 (10, )  ' 随机放１０个雷

    .循环判断首 ()

        i ＝ 取随机数 (1, 8)  ' 随机取一个地方

        j ＝ 取随机数 (1, 8)

        ' 已放雷的地方就不再放，直到找到空地方

    .循环判断尾 (集雷块阵列 [i] [j].有雷)

    集雷块阵列 [i] [j].有雷 ＝ 真

.计次循环尾 ()

 

 

       在这段代码中，程序先把数组重新定义成了一个8×8的数组，这样每个数组成员就可以很方便地与按钮对应起来了。我们还看到在布雷的时候，使用了一个循环判断。这个循环的目的是防止把两个或两个以上的雷放到了一个雷块中，否则雷的总数就不是10了。

       在“__启动窗口_创建完毕”事件中，添加一句代码“新游戏()”，再次运行程序，我们得到了如下的结果：

 

图1.6.3-i 新游戏后绘制的按钮

 

       这些按钮可比Windows自带的扫雷游戏的按钮漂亮多了，但悲惨的是，它们都还是花架子——不能响应鼠标点击。

       接下来，我们需要给画板添加响应鼠标事件的子程序。在这些子程序中，首要要把鼠标的位置转换成数组的索引，然后根据点击的鼠标键和周围的布雷情况重画该按钮。在这里，点击鼠标左键的情况要复杂些：1. 按下后要立即算出周围的雷数；2. 如果周围的雷数为0，则还要自动按下周围的按钮，如果周围的雷块中有的周围的雷数也是0，则还要自动按下该按钮周围的按钮……显然这里要用到递归。而要自动点按周围的按钮，则需要遍历周围的8个按钮，如果该按钮已被按下或以被右键作了标示，就跳过，否则就计算该按钮的中心坐标，用鼠标去点击画板的那个位置，如此循环往复；3. 如果有雷，则需要画出所有的雷，同时在用户标了旗帜但没有雷的按钮上画上红叉叉，同时宣布则游戏结束。如果是点击右键，则把该按钮对应的数组成员“右击索引”加1，并求与3的余数，这样其值刚好与凸起按钮的三种类型相对应，从而绘制出不同的状态。不论是鼠标左键按下和右键按下，都要判断用户是否已标出了所有的雷，这个代码由子程序“判断胜负”来完成 。具体的实现代码如下：

 

 

.子程序 _画板_鼠标左键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 x, 整数型

.局部变量 y, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

m ＝ 横向位置 ÷ #雷块宽度 ＋ 1

n ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [m] [n].已按下)

    返回 ()

.如果真结束

集雷块阵列 [m] [n].已按下 ＝ 真

集雷块阵列 [m] [n].右击索引 ＝ 0

.如果 (集雷块阵列 [m] [n].有雷)

    ' 有雷则显示所有的雷，同时弹出信息框

    .计次循环首 (8, j)

        .计次循环首 (8, i)

            .如果 (集雷块阵列 [i] [j].有雷)

                画凹下按钮 (i, j, -1)

            .否则

                ' 没有雷却标了旗帜的地方则画叉叉

                .如果真 (集雷块阵列 [i] [j].右击索引 ＝ #凸起按钮_旗帜)

                    画红叉叉 (i, j)

                .如果真结束

            .如果结束

        .计次循环尾 ()

    .计次循环尾 ()

    集游戏已结束 ＝ 真

    信息框 (“你踩雷了！”, #错误图标, )

    返回 ()

.否则

    ' 无雷则显示周围雷的数目，数目是０则不显示且递归按周围的按钮

    ' 遍历周围的雷块

    .变量循环首 (n － 1, n ＋ 1, 1, j)

        .变量循环首 (m － 1, m ＋ 1, 1, i)

            .如果真 (i ≥ 1 且 i ≤ 8 且 j ≥ 1 且 j ≤ 8)

                ' 确保在数组索引范围内，以免导致数组下标越界错误！

                .如果真 (集雷块阵列 [i] [j].有雷)

                    周围雷数 ＝ 周围雷数 ＋ 1

                .如果真结束

 

            .如果真结束

 

        .变量循环尾 ()

    .变量循环尾 ()

    画凹下按钮 (m, n, 周围雷数)

    .如果真 (周围雷数 ＝ 0)

        ' 如果周围雷数是０，则还需要自动向周围扩展。(递归)

        .变量循环首 (n － 1, n ＋ 1, 1, j)

            .变量循环首 (m － 1, m ＋ 1, 1, i)

                .如果真 (i ＜ 1 或 i ＞ 8 或 j ＜ 1 或 j ＞ 8)

                    ' 确保数组下标不会越界

                    到循环尾 ()

                .如果真结束

                .如果真 (集雷块阵列 [i] [j].已按下 或 集雷块阵列 [i] [j].右击索引 ≠ #凸起按钮_空白)

                    ' 已标为旗帜或问号的则不翻出来

                    到循环尾 ()  ' 此处退出递归，否则会因无限递归而死机。

                .如果真结束

                x ＝ i × #雷块宽度 － #雷块宽度 ÷ 2

                y ＝ j × #雷块宽度 － #雷块宽度 ÷ 2

                _画板_鼠标左键被按下 (x, y, 功能键状态)  ' 此处递归调用自身

            .变量循环尾 ()

        .变量循环尾 ()

    .如果真结束

 

.如果结束

判断胜负 ()

 

 

.子程序 _画板_鼠标右键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

i ＝ 横向位置 ÷ #雷块宽度 ＋ 1

j ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [i] [j].已按下)

    返回 ()

.如果真结束

集雷块阵列 [i] [j].右击索引 ＝ (集雷块阵列 [i] [j].右击索引 ＋ 1) ％ 3

画凸起按钮 (i, j, 集雷块阵列 [i] [j].右击索引)

判断胜负 ()

 

 

 

.子程序 _画板_鼠标左键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 x, 整数型

.局部变量 y, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

m ＝ 横向位置 ÷ #雷块宽度 ＋ 1

n ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [m] [n].已按下)

    返回 ()

.如果真结束

集雷块阵列 [m] [n].已按下 ＝ 真

集雷块阵列 [m] [n].右击索引 ＝ 0

.如果 (集雷块阵列 [m] [n].有雷)

    ' 有雷则显示所有的雷，同时弹出信息框

    .计次循环首 (8, j)

        .计次循环首 (8, i)

            .如果 (集雷块阵列 [i] [j].有雷)

                画凹下按钮 (i, j, -1)

            .否则

                ' 没有雷却标了旗帜的地方则画叉叉

                .如果真 (集雷块阵列 [i] [j].右击索引 ＝ #凸起按钮_旗帜)

                    画红叉叉 (i, j)

                .如果真结束

            .如果结束

        .计次循环尾 ()

    .计次循环尾 ()

    集游戏已结束 ＝ 真

    信息框 (“你踩雷了！”, #错误图标, )

    返回 ()

.否则

    ' 无雷则显示周围雷的数目，数目是０则不显示且递归按周围的按钮

    ' 遍历周围的雷块

    .变量循环首 (n － 1, n ＋ 1, 1, j)

        .变量循环首 (m － 1, m ＋ 1, 1, i)

            .如果真 (i ≥ 1 且 i ≤ 8 且 j ≥ 1 且 j ≤ 8)

                ' 确保在数组索引范围内，以免导致数组下标越界错误！

                .如果真 (集雷块阵列 [i] [j].有雷)

                    周围雷数 ＝ 周围雷数 ＋ 1

                .如果真结束

 

            .如果真结束

 

        .变量循环尾 ()

    .变量循环尾 ()

    画凹下按钮 (m, n, 周围雷数)

    .如果真 (周围雷数 ＝ 0)

        ' 如果周围雷数是０，则还需要自动向周围扩展。(递归)

        .变量循环首 (n － 1, n ＋ 1, 1, j)

            .变量循环首 (m － 1, m ＋ 1, 1, i)

                .如果真 (i ＜ 1 或 i ＞ 8 或 j ＜ 1 或 j ＞ 8)

                    ' 确保数组下标不会越界

                    到循环尾 ()

                .如果真结束

                .如果真 (集雷块阵列 [i] [j].已按下 或 集雷块阵列 [i] [j].右击索引 ≠ #凸起按钮_空白)

                    ' 已标为旗帜或问号的则不翻出来

                    到循环尾 ()  ' 此处退出递归，否则会因无限递归而死机。

                .如果真结束

                x ＝ i × #雷块宽度 － #雷块宽度 ÷ 2

                y ＝ j × #雷块宽度 － #雷块宽度 ÷ 2

                _画板_鼠标左键被按下 (x, y, 功能键状态)  ' 此处递归调用自身

            .变量循环尾 ()

        .变量循环尾 ()

    .如果真结束

 

.如果结束

判断胜负 ()

 

 

.子程序 _画板_鼠标右键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

i ＝ 横向位置 ÷ #雷块宽度 ＋ 1

j ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [i] [j].已按下)

    返回 ()

.如果真结束

集雷块阵列 [i] [j].右击索引 ＝ (集雷块阵列 [i] [j].右击索引 ＋ 1) ％ 3

画凸起按钮 (i, j, 集雷块阵列 [i] [j].右击索引)

判断胜负 ()

 

 

 

.子程序 _画板_鼠标左键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 x, 整数型

.局部变量 y, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

m ＝ 横向位置 ÷ #雷块宽度 ＋ 1

n ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [m] [n].已按下)

    返回 ()

.如果真结束

集雷块阵列 [m] [n].已按下 ＝ 真

集雷块阵列 [m] [n].右击索引 ＝ 0

.如果 (集雷块阵列 [m] [n].有雷)

    ' 有雷则显示所有的雷，同时弹出信息框

    .计次循环首 (8, j)

        .计次循环首 (8, i)

            .如果 (集雷块阵列 [i] [j].有雷)

                画凹下按钮 (i, j, -1)

            .否则

                ' 没有雷却标了旗帜的地方则画叉叉

                .如果真 (集雷块阵列 [i] [j].右击索引 ＝ #凸起按钮_旗帜)

                    画红叉叉 (i, j)

                .如果真结束

            .如果结束

        .计次循环尾 ()

    .计次循环尾 ()

    集游戏已结束 ＝ 真

    信息框 (“你踩雷了！”, #错误图标, )

    返回 ()

.否则

    ' 无雷则显示周围雷的数目，数目是０则不显示且递归按周围的按钮

    ' 遍历周围的雷块

    .变量循环首 (n － 1, n ＋ 1, 1, j)

        .变量循环首 (m － 1, m ＋ 1, 1, i)

            .如果真 (i ≥ 1 且 i ≤ 8 且 j ≥ 1 且 j ≤ 8)

                ' 确保在数组索引范围内，以免导致数组下标越界错误！

                .如果真 (集雷块阵列 [i] [j].有雷)

                    周围雷数 ＝ 周围雷数 ＋ 1

                .如果真结束

 

            .如果真结束

 

        .变量循环尾 ()

    .变量循环尾 ()

    画凹下按钮 (m, n, 周围雷数)

    .如果真 (周围雷数 ＝ 0)

        ' 如果周围雷数是０，则还需要自动向周围扩展。(递归)

        .变量循环首 (n － 1, n ＋ 1, 1, j)

            .变量循环首 (m － 1, m ＋ 1, 1, i)

                .如果真 (i ＜ 1 或 i ＞ 8 或 j ＜ 1 或 j ＞ 8)

                    ' 确保数组下标不会越界

                    到循环尾 ()

                .如果真结束

                .如果真 (集雷块阵列 [i] [j].已按下 或 集雷块阵列 [i] [j].右击索引 ≠ #凸起按钮_空白)

                    ' 已标为旗帜或问号的则不翻出来

                    到循环尾 ()  ' 此处退出递归，否则会因无限递归而死机。

                .如果真结束

                x ＝ i × #雷块宽度 － #雷块宽度 ÷ 2

                y ＝ j × #雷块宽度 － #雷块宽度 ÷ 2

                _画板_鼠标左键被按下 (x, y, 功能键状态)  ' 此处递归调用自身

            .变量循环尾 ()

        .变量循环尾 ()

    .如果真结束

 

.如果结束

判断胜负 ()

 

 

.子程序 _画板_鼠标右键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

i ＝ 横向位置 ÷ #雷块宽度 ＋ 1

j ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [i] [j].已按下)

    返回 ()

.如果真结束

集雷块阵列 [i] [j].右击索引 ＝ (集雷块阵列 [i] [j].右击索引 ＋ 1) ％ 3

画凸起按钮 (i, j, 集雷块阵列 [i] [j].右击索引)

判断胜负 ()

 

 

       在递归调用“_画板_鼠标左键被按下”的时候，程序需要计算周围的雷块的中心位置，用该按钮的坐标索引值乘以按钮的宽度，然后减去按钮宽度的一半，刚好就是该按钮的中心位置了。具体算法如图 所示。

 

图1.6.3-j 自动点击周围的按钮示意图

 

       程序的核心算法和代码就是这样的了，至于计算用户排雷所花的时间，以及排行榜的处理等，就留给有兴趣的读者自己来完成了。——嗯，还有一点，如果用现在这个扫雷程序和你的朋友比赛，你说谁会赢？这恐怕就难说了，也许他是位扫雷高手呢？既然游戏是我们自己写的，我何不在里面加个后门？这样谁都胜不了我！哈哈，说干就干，于是，我在游戏中加了一个“_画板_按下某键”事件，代码如下所示。

 

 

.子程序 _画板_按下某键, 逻辑型

.参数 键代码, 整数型

.参数 功能键状态, 整数型

.局部变量 j, 整数型

.局部变量 i, 整数型

.局部变量 x, 整数型

.局部变量 y, 整数型

 

' 此处是绝技，不可告人哦　^_^

.如果真 (键代码 ＝ #F12键 且 功能键状态 ＝ 位或 (#Ctrl键状态, #Alt键状态))

    画板.画笔颜色 ＝ #黑色

    .计次循环首 (8, j)

        .计次循环首 (8, i)

            .如果真 (集雷块阵列 [i] [j].有雷)

                x ＝ (i － 1) × #雷块宽度

                y ＝ (j － 1) × #雷块宽度

                ' 用黑点把有雷的地方标出来，哈哈！

                画板.画直线 (x ＋ 3, y ＋ 3, x ＋ 5, y ＋ 3)

            .如果真结束

 

        .计次循环尾 ()

    .计次循环尾 ()

.如果真结束

 

 

 

.子程序 _画板_按下某键, 逻辑型

.参数 键代码, 整数型

.参数 功能键状态, 整数型

.局部变量 j, 整数型

.局部变量 i, 整数型

.局部变量 x, 整数型

.局部变量 y, 整数型

 

' 此处是绝技，不可告人哦　^_^

.如果真 (键代码 ＝ #F12键 且 功能键状态 ＝ 位或 (#Ctrl键状态, #Alt键状态))

    画板.画笔颜色 ＝ #黑色

    .计次循环首 (8, j)

        .计次循环首 (8, i)

            .如果真 (集雷块阵列 [i] [j].有雷)

                x ＝ (i － 1) × #雷块宽度

                y ＝ (j － 1) × #雷块宽度

                ' 用黑点把有雷的地方标出来，哈哈！

                画板.画直线 (x ＋ 3, y ＋ 3, x ＋ 5, y ＋ 3)

            .如果真结束

 

        .计次循环尾 ()

    .计次循环尾 ()

.如果真结束

 

 

       现在运行程序，按下[Ctrl]+[Alt]+[F12]组合键，哪里有雷就一览无余了，如果动作块，你可以在3秒钟内就把所有的雷找出来，看谁还胜得了你！当然，你也可以把这个组合键更改成其他更隐秘的东西，比如连续按下字符串“zjs”、或鼠标横向快速晃动3次——一切全在于你的创意。

 

从这个扫雷游戏中，我们得到如下结论：

1> Windows 系统中的所有控件都是“画”出来的，所以我们可以通过自己绘制这些控件来更改它们的外观，这就是换肤的基本思想，在后续的章节中将有专门章节讲述。

2> 没有自己的核心技术是多么可怕，落后就要挨打啊！所以，我们要有自己的操作系统、自己的编译器！     支持易语言～！！

       图1.6.3-k是这个扫雷游戏在运行中的界面。

图1.6.3-k 扫雷游戏最终运行界面

 

       最后列出这个程序的所有代码，以方便读者在纸上阅读理解。

 

自定义数据类型

.数据类型 雷块

    .成员 有雷, 逻辑型

    .成员 右击索引, 整数型, , , 0:空 1:旗帜 2:问号

    .成员 已按下, 逻辑型, , , 该雷块对应的按钮是否已按下

程序集变量

 

.程序集 主窗口程序集

.程序集变量 集雷块阵列, 雷块, , "0"

.程序集变量 集游戏已结束, 逻辑型

 

 

.子程序 __启动窗口_创建完毕

新游戏 ()

 

画凸起按钮

 

.子程序 画凸起按钮

.参数 参横坐标, 整数型, , 以数组索引为单位

.参数 参纵坐标, 整数型, , 以数组索引为单位

.参数 参按钮类型, 整数型, , 0:空白,1:旗帜,2:问号

.局部变量 x, 整数型

.局部变量 y, 整数型

 

x ＝ #雷块宽度 × (参横坐标 － 1)

y ＝ #雷块宽度 × (参纵坐标 － 1)

画板.画渐变矩形 (x, y, #雷块宽度, #雷块宽度, #从左上到右下, 取颜色值 (131, 192, 252), 取颜色值 (3, 92, 182))

画板.画笔颜色 ＝ 取颜色值 (1, 78, 155)

画板.刷子类型 ＝ 0

画板.画笔类型 ＝ 0

画板.画矩形 (x, y, x ＋ #雷块宽度, y ＋ #雷块宽度)

画板.画渐变矩形 (x ＋ 2, y ＋ 2, #雷块宽度 － 4, #雷块宽度 － 4, #从上到下, 取颜色值 (175, 216, 254), 取颜色值 (91, 173, 253))

画板.画笔类型 ＝ 1

.判断开始 (参按钮类型 ＝ #凸起按钮_旗帜)

    ' 以下代码绘制旗帜、旗杆和基座

    画板.画笔颜色 ＝ #红色

    画板.刷子颜色 ＝ #红色

    画板.刷子类型 ＝ 1

    画板.画矩形 (x ＋ 9, y ＋ 4, x ＋ 16, y ＋ 9)

    画板.画笔颜色 ＝ #黑色

    画板.画笔粗细 ＝ 1

    画板.画直线 (x ＋ 8, y ＋ 3, x ＋ 8, y ＋ 15)

    画板.画笔粗细 ＝ 2

    画板.画直线 (x ＋ 5, y ＋ 15, x ＋ 13, y ＋ 15)

.判断 (参按钮类型 ＝ #凸起按钮_问号)

    画板.定位写出 (x ＋ 4, y － 2, “?”)  ' 画问号

.默认

 

画凸起按钮

 

.子程序 画凸起按钮

.参数 参横坐标, 整数型, , 以数组索引为单位

.参数 参纵坐标, 整数型, , 以数组索引为单位

.参数 参按钮类型, 整数型, , 0:空白,1:旗帜,2:问号

.局部变量 x, 整数型

.局部变量 y, 整数型

 

x ＝ #雷块宽度 × (参横坐标 － 1)

y ＝ #雷块宽度 × (参纵坐标 － 1)

画板.画渐变矩形 (x, y, #雷块宽度, #雷块宽度, #从左上到右下, 取颜色值 (131, 192, 252), 取颜色值 (3, 92, 182))

画板.画笔颜色 ＝ 取颜色值 (1, 78, 155)

画板.刷子类型 ＝ 0

画板.画笔类型 ＝ 0

画板.画矩形 (x, y, x ＋ #雷块宽度, y ＋ #雷块宽度)

画板.画渐变矩形 (x ＋ 2, y ＋ 2, #雷块宽度 － 4, #雷块宽度 － 4, #从上到下, 取颜色值 (175, 216, 254), 取颜色值 (91, 173, 253))

画板.画笔类型 ＝ 1

.判断开始 (参按钮类型 ＝ #凸起按钮_旗帜)

    ' 以下代码绘制旗帜、旗杆和基座

    画板.画笔颜色 ＝ #红色

    画板.刷子颜色 ＝ #红色

    画板.刷子类型 ＝ 1

    画板.画矩形 (x ＋ 9, y ＋ 4, x ＋ 16, y ＋ 9)

    画板.画笔颜色 ＝ #黑色

    画板.画笔粗细 ＝ 1

    画板.画直线 (x ＋ 8, y ＋ 3, x ＋ 8, y ＋ 15)

    画板.画笔粗细 ＝ 2

    画板.画直线 (x ＋ 5, y ＋ 15, x ＋ 13, y ＋ 15)

.判断 (参按钮类型 ＝ #凸起按钮_问号)

    画板.定位写出 (x ＋ 4, y － 2, “?”)  ' 画问号

.默认

 

画凹下按钮

 

.子程序 画凹下按钮

.参数 参横坐标, 整数型, , 以数组索引为单位

.参数 参纵坐标, 整数型, , 以数组索引为单位

.参数 参周围雷数, 整数型, , -1表示是雷，其他是雷的数目

.局部变量 x

.局部变量 y

 

x ＝ #雷块宽度 × (参横坐标 － 1)

y ＝ #雷块宽度 × (参纵坐标 － 1)

画板.画笔类型 ＝ 0  ' 空画笔

画板.画笔颜色 ＝ #深灰

画板.刷子类型 ＝ 1  ' 颜色刷子

画板.刷子颜色 ＝ 取颜色值 (224, 242, 254)

画板.画矩形 (x, y, x ＋ #雷块宽度, y ＋ #雷块宽度)

.判断开始 (参周围雷数 ＝ -1)  ' 是地雷

    ' 以下代码画地雷

    画板.画笔颜色 ＝ #黑色

    画板.画笔类型 ＝ 0

    画板.刷子颜色 ＝ #黑色

    画板.刷子类型 ＝ 1

    画板.画椭圆 (x ＋ 4, y ＋ 4, x ＋ #雷块宽度 － 4, y ＋ #雷块宽度 － 4)

    画板.画直线 (x ＋ 2, y ＋ #雷块宽度 ÷ 2, x ＋ #雷块宽度 － 2, y ＋ #雷块宽度 ÷ 2)

    画板.画直线 (x ＋ #雷块宽度 ÷ 2, y ＋ 2, x ＋ #雷块宽度 ÷ 2, y ＋ #雷块宽度 － 2)

    画板.画直线 (x ＋ 4, y ＋ 4, x ＋ #雷块宽度 － 4, y ＋ #雷块宽度 － 4)

    画板.画直线 (x ＋ #雷块宽度 － 4, y ＋ 4, x ＋ 4, y ＋ #雷块宽度 － 4)

    画板.刷子颜色 ＝ #白色

    画板.画笔颜色 ＝ #黑色

    画板.画椭圆 (x ＋ 6, y ＋ 6, x ＋ 10, y ＋ 10)

    返回 ()

.判断 (参周围雷数 ＝ 0)

    返回 ()

.判断 (参周围雷数 ＝ 1)

    画板.文本颜色 ＝ #蓝色

.判断 (参周围雷数 ＝ 2)

    画板.文本颜色 ＝ #深青

.默认

    画板.文本颜色 ＝ #红色

.判断结束

画板.定位写出 (x ＋ 4, y － 2, 参周围雷数)

 

画凹下按钮

 

.子程序 画凹下按钮

.参数 参横坐标, 整数型, , 以数组索引为单位

.参数 参纵坐标, 整数型, , 以数组索引为单位

.参数 参周围雷数, 整数型, , -1表示是雷，其他是雷的数目

.局部变量 x

.局部变量 y

 

x ＝ #雷块宽度 × (参横坐标 － 1)

y ＝ #雷块宽度 × (参纵坐标 － 1)

画板.画笔类型 ＝ 0  ' 空画笔

画板.画笔颜色 ＝ #深灰

画板.刷子类型 ＝ 1  ' 颜色刷子

画板.刷子颜色 ＝ 取颜色值 (224, 242, 254)

画板.画矩形 (x, y, x ＋ #雷块宽度, y ＋ #雷块宽度)

.判断开始 (参周围雷数 ＝ -1)  ' 是地雷

    ' 以下代码画地雷

    画板.画笔颜色 ＝ #黑色

    画板.画笔类型 ＝ 0

    画板.刷子颜色 ＝ #黑色

    画板.刷子类型 ＝ 1

    画板.画椭圆 (x ＋ 4, y ＋ 4, x ＋ #雷块宽度 － 4, y ＋ #雷块宽度 － 4)

    画板.画直线 (x ＋ 2, y ＋ #雷块宽度 ÷ 2, x ＋ #雷块宽度 － 2, y ＋ #雷块宽度 ÷ 2)

    画板.画直线 (x ＋ #雷块宽度 ÷ 2, y ＋ 2, x ＋ #雷块宽度 ÷ 2, y ＋ #雷块宽度 － 2)

    画板.画直线 (x ＋ 4, y ＋ 4, x ＋ #雷块宽度 － 4, y ＋ #雷块宽度 － 4)

    画板.画直线 (x ＋ #雷块宽度 － 4, y ＋ 4, x ＋ 4, y ＋ #雷块宽度 － 4)

    画板.刷子颜色 ＝ #白色

    画板.画笔颜色 ＝ #黑色

    画板.画椭圆 (x ＋ 6, y ＋ 6, x ＋ 10, y ＋ 10)

    返回 ()

.判断 (参周围雷数 ＝ 0)

    返回 ()

.判断 (参周围雷数 ＝ 1)

    画板.文本颜色 ＝ #蓝色

.判断 (参周围雷数 ＝ 2)

    画板.文本颜色 ＝ #深青

.默认

    画板.文本颜色 ＝ #红色

.判断结束

画板.定位写出 (x ＋ 4, y － 2, 参周围雷数)

 

新游戏

 

.子程序 新游戏

.局部变量 i, 整数型

.局部变量 j, 整数型

 

集游戏已结束 ＝ 假

重定义数组 (集雷块阵列, 假, 8, 8)  ' 定义一个8×8的雷块数组

置随机数种子 ()

' 先重画所有按钮

.计次循环首 (8, i)

    .计次循环首 (8, j)

        画凸起按钮 (j, i, #凸起按钮_空白)

    .计次循环尾 ()

.计次循环尾 ()

' 再设置雷区数组

.计次循环首 (10, )  ' 随机放１０个雷

    .循环判断首 ()

        i ＝ 取随机数 (1, 8)  ' 随机取一个地方

        j ＝ 取随机数 (1, 8)

        ' 已放雷的地方就不再放，直到找到空地方

    .循环判断尾 (集雷块阵列 [i] [j].有雷)

    集雷块阵列 [i] [j].有雷 ＝ 真

.计次循环尾 ()

画红叉叉

 

.子程序 画红叉叉, , , 在用户标错的地方画个叉叉

.参数 参横坐标, 整数型

.参数 参纵坐标, 整数型

.局部变量 x

.局部变量 y

 

x ＝ #雷块宽度 × (参横坐标 － 1)

y ＝ #雷块宽度 × (参纵坐标 － 1)

 

画板.画笔粗细 ＝ 1

画板.画笔类型 ＝ 1

画板.画笔颜色 ＝ #红色

画板.画直线 (x, y, x ＋ #雷块宽度, y ＋ #雷块宽度)

画板.画直线 (x ＋ #雷块宽度, y, x, y ＋ #雷块宽度)

 

画红叉叉

 

.子程序 画红叉叉, , , 在用户标错的地方画个叉叉

.参数 参横坐标, 整数型

.参数 参纵坐标, 整数型

.局部变量 x

.局部变量 y

 

x ＝ #雷块宽度 × (参横坐标 － 1)

y ＝ #雷块宽度 × (参纵坐标 － 1)

 

画板.画笔粗细 ＝ 1

画板.画笔类型 ＝ 1

画板.画笔颜色 ＝ #红色

画板.画直线 (x, y, x ＋ #雷块宽度, y ＋ #雷块宽度)

画板.画直线 (x ＋ #雷块宽度, y, x, y ＋ #雷块宽度)

 

判断胜负

 

.子程序 判断胜负

.局部变量 已找出的雷数, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

 

.计次循环首 (8, j)

    .计次循环首 (8, i)

        .如果真 (集雷块阵列 [i] [j].右击索引 ＝ #凸起按钮_旗帜)

            .如果 (集雷块阵列 [i] [j].有雷)

                已找出的雷数 ＝ 已找出的雷数 ＋ 1

            .否则

                返回 ()

            .如果结束

 

        .如果真结束

 

    .计次循环尾 ()

.计次循环尾 ()

.如果真 (已找出的雷数 ＝ 10)

    集游戏已结束 ＝ 真

    信息框 (“你成功了！祝贺你！”, #信息图标, )

 

_画板_鼠标左键被按下

 

.子程序 _画板_鼠标左键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 x, 整数型

.局部变量 y, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

m ＝ 横向位置 ÷ #雷块宽度 ＋ 1

n ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [m] [n].已按下)

    返回 ()

.如果真结束

集雷块阵列 [m] [n].已按下 ＝ 真

集雷块阵列 [m] [n].右击索引 ＝ 0

.如果 (集雷块阵列 [m] [n].有雷)

    ' 有雷则显示所有的雷，同时弹出信息框

    .计次循环首 (8, j)

        .计次循环首 (8, i)

            .如果 (集雷块阵列 [i] [j].有雷)

                画凹下按钮 (i, j, -1)

            .否则

                ' 没有雷却标了旗帜的地方则画叉叉

                .如果真 (集雷块阵列 [i] [j].右击索引 ＝ #凸起按钮_旗帜)

                    画红叉叉 (i, j)

                .如果真结束

 

            .如果结束

 

        .计次循环尾 ()

    .计次循环尾 ()

    集游戏已结束 ＝ 真

    信息框 (“你踩雷了！”, #错误图标, )

    返回 ()

.否则

    ' 无雷则显示周围雷的数目，数目是０则不显示且递归按周围的按钮

    ' 遍历周围的雷块

    .变量循环首 (n － 1, n ＋ 1, 1, j)

        .变量循环首 (m － 1, m ＋ 1, 1, i)

            .如果真 (i ≥ 1 且 i ≤ 8 且 j ≥ 1 且 j ≤ 8)

                ' 确保在数组索引范围内，以免导致数组下标越界错误！

                .如果真 (集雷块阵列 [i] [j].有雷)

                    周围雷数 ＝ 周围雷数 ＋ 1

                .如果真结束

 

            .如果真结束

 

        .变量循环尾 ()

    .变量循环尾 ()

    画凹下按钮 (m, n, 周围雷数)

    .如果真 (周围雷数 ＝ 0)

        ' 如果周围雷数是０，则还需要自动向周围扩展。(递归)

        .变量循环首 (n － 1, n ＋ 1, 1, j)

            .变量循环首 (m － 1, m ＋ 1, 1, i)

                .如果真 (i ＜ 1 或 i ＞ 8 或 j ＜ 1 或 j ＞ 8)

                    ' 确保数组下标不会越界

                    到循环尾 ()

                .如果真结束

                .如果真 (集雷块阵列 [i] [j].已按下 或 集雷块阵列 [i] [j].右击索引 ≠ #凸起按钮_空白)

                    ' 已标为旗帜或问号的则不翻出来

                    到循环尾 ()  ' 此处退出递归，否则会因无限递归而死机。

                .如果真结束

                x ＝ i × #雷块宽度 － #雷块宽度 ÷ 2

                y ＝ j × #雷块宽度 － #雷块宽度 ÷ 2

                _画板_鼠标左键被按下 (x, y, 功能键状态)

            .变量循环尾 ()

        .变量循环尾 ()

    .如果真结束

 

.如果结束

判断胜负 ()

 

_画板_鼠标左键被按下

 

.子程序 _画板_鼠标左键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 x, 整数型

.局部变量 y, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

m ＝ 横向位置 ÷ #雷块宽度 ＋ 1

n ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [m] [n].已按下)

    返回 ()

.如果真结束

集雷块阵列 [m] [n].已按下 ＝ 真

集雷块阵列 [m] [n].右击索引 ＝ 0

.如果 (集雷块阵列 [m] [n].有雷)

    ' 有雷则显示所有的雷，同时弹出信息框

    .计次循环首 (8, j)

        .计次循环首 (8, i)

            .如果 (集雷块阵列 [i] [j].有雷)

                画凹下按钮 (i, j, -1)

            .否则

                ' 没有雷却标了旗帜的地方则画叉叉

                .如果真 (集雷块阵列 [i] [j].右击索引 ＝ #凸起按钮_旗帜)

                    画红叉叉 (i, j)

                .如果真结束

 

            .如果结束

 

        .计次循环尾 ()

    .计次循环尾 ()

    集游戏已结束 ＝ 真

    信息框 (“你踩雷了！”, #错误图标, )

    返回 ()

.否则

    ' 无雷则显示周围雷的数目，数目是０则不显示且递归按周围的按钮

    ' 遍历周围的雷块

    .变量循环首 (n － 1, n ＋ 1, 1, j)

        .变量循环首 (m － 1, m ＋ 1, 1, i)

            .如果真 (i ≥ 1 且 i ≤ 8 且 j ≥ 1 且 j ≤ 8)

                ' 确保在数组索引范围内，以免导致数组下标越界错误！

                .如果真 (集雷块阵列 [i] [j].有雷)

                    周围雷数 ＝ 周围雷数 ＋ 1

                .如果真结束

 

            .如果真结束

 

        .变量循环尾 ()

    .变量循环尾 ()

    画凹下按钮 (m, n, 周围雷数)

    .如果真 (周围雷数 ＝ 0)

        ' 如果周围雷数是０，则还需要自动向周围扩展。(递归)

        .变量循环首 (n － 1, n ＋ 1, 1, j)

            .变量循环首 (m － 1, m ＋ 1, 1, i)

                .如果真 (i ＜ 1 或 i ＞ 8 或 j ＜ 1 或 j ＞ 8)

                    ' 确保数组下标不会越界

                    到循环尾 ()

                .如果真结束

                .如果真 (集雷块阵列 [i] [j].已按下 或 集雷块阵列 [i] [j].右击索引 ≠ #凸起按钮_空白)

                    ' 已标为旗帜或问号的则不翻出来

                    到循环尾 ()  ' 此处退出递归，否则会因无限递归而死机。

                .如果真结束

                x ＝ i × #雷块宽度 － #雷块宽度 ÷ 2

                y ＝ j × #雷块宽度 － #雷块宽度 ÷ 2

                _画板_鼠标左键被按下 (x, y, 功能键状态)

            .变量循环尾 ()

        .变量循环尾 ()

    .如果真结束

 

.如果结束

判断胜负 ()

 

_画板_鼠标右键被按下

 

.子程序 _画板_鼠标右键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

i ＝ 横向位置 ÷ #雷块宽度 ＋ 1

j ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [i] [j].已按下)

    返回 ()

.如果真结束

集雷块阵列 [i] [j].右击索引 ＝ (集雷块阵列 [i] [j].右击索引 ＋ 1) ％ 3

画凸起按钮 (i, j, 集雷块阵列 [i] [j].右击索引)

判断胜负 ()

 

_画板_鼠标右键被按下

 

.子程序 _画板_鼠标右键被按下, 逻辑型

.参数 横向位置, 整数型

.参数 纵向位置, 整数型

.参数 功能键状态, 整数型

.局部变量 m, 整数型

.局部变量 n, 整数型

.局部变量 i, 整数型

.局部变量 j, 整数型

.局部变量 周围雷数, 整数型

 

.如果真 (集游戏已结束)

    返回 ()

.如果真结束

' 将横纵坐标映射为数组的索引值

i ＝ 横向位置 ÷ #雷块宽度 ＋ 1

j ＝ 纵向位置 ÷ #雷块宽度 ＋ 1

.如果真 (集雷块阵列 [i] [j].已按下)

    返回 ()

.如果真结束

集雷块阵列 [i] [j].右击索引 ＝ (集雷块阵列 [i] [j].右击索引 ＋ 1) ％ 3

画凸起按钮 (i, j, 集雷块阵列 [i] [j].右击索引)

判断胜负 ()

 

_画板_按下某键

 

.子程序 _画板_按下某键, 逻辑型

.参数 键代码, 整数型

.参数 功能键状态, 整数型

.局部变量 j, 整数型

.局部变量 i, 整数型

.局部变量 x, 整数型

.局部变量 y, 整数型

 

' 此处是绝技，不可告人哦　^_^

.如果真 (键代码 ＝ #F12键 且 功能键状态 ＝ 位或 (#Ctrl键状态, #Alt键状态))

    画板.画笔颜色 ＝ #黑色

    .计次循环首 (8, j)

        .计次循环首 (8, i)

            .如果真 (集雷块阵列 [i] [j].有雷)

                x ＝ (i － 1) × #雷块宽度

                y ＝ (j － 1) × #雷块宽度

                ' 用黑点把有雷的地方标出来，哈哈！

                画板.画直线 (x ＋ 3, y ＋ 3, x ＋ 5, y ＋ 3)

            .如果真结束

 

        .计次循环尾 ()

    .计次循环尾 ()