SmallBasic-编程学习指南-全-
SmallBasic 编程学习指南(全)
原文:
zh.annas-archive.org/md5/02cdc9a53a496eb20d21ac21f2b3d41d译者:飞龙
第一章:简介

你是否曾经想过人们是如何创建计算机程序的?你是否曾想过制作你自己的视频游戏?你是否翻开过编程书籍,却被其中枯燥的语言和无趣的示例所打击?如果是这样,也许你内心深处隐藏着一位程序员,正等待着被释放。欢迎来到与 Small Basic 一起学习编程!
Microsoft Small Basic 是一种免费的文本编程语言,专为初学者设计。它提供了一个完整的编程环境,帮助你编写、测试和优化你的作品。本书将向你展示如何安装 Small Basic,并教你如何用它做出令人惊叹的作品。我们会让你发现编程不仅可以有趣、有成就感,而且——最重要的是——简单!
谁应该阅读本书?
就是你!本书以一种有趣、互动的方式向你介绍 Small Basic。我们提供了大量的示例程序,你可以运行、探索并修改它们来制作自己的作品。尝试每一个练习,深入了解额外的在线资源、复习问题和实践练习。等到你完成本书,你将能够创建自己的游戏!
如果你感到灵感涌现,可以在 Small Basic 的 MSDN 论坛上分享你的 Small Basic 创作,Small Basic 社区委员会将等待回答你的任何问题并查看你所有精彩的作品。
本书内容
每一章都会在上一章的基础上帮助你提升编程技能。我们将从基础开始,到最后你将成为一个编程高手!
• 第一章:介绍 Small Basic 解释了 Small Basic 的功能,并帮助你完成设置。然后,你将创建你的第一个程序。
• 第二章:入门 带你通过使用 Small Basic 内置的文本窗口来创建简单的程序。
• 第三章:绘制基础 向你展示如何编写程序,在图形窗口中绘制图形。
• 第四章:使用变量 解释了变量如何跟踪信息。变量在编程中起着至关重要的作用,你将在整本书中使用它们。
• 第五章:使用海龟图形绘制图形 教你如何指挥你自己的艺术海龟。你将绘制出复杂的几何形状和图案,而这些手工绘制会非常繁琐。
• 第六章:获取用户输入 向你展示如何通过让程序具备互动性来让你的程序更有生命。你将编写一个可以按名字打招呼的程序。
• 在第七章:用数学赋能程序中,你将使用数学来制作游戏,比如一个使用随机数生成器的骰子游戏。
• 第八章:使用 If 语句做决策 将向你展示如何控制程序的逻辑和流程。掌握了If语句,你将能够创建更强大、更激动人心的程序。
• 在第九章:使用决策制作游戏中,你将基于对If语句的理解,使用它们制作复杂的游戏。
• 在第十章:使用子程序解决问题中,你将把代码分解成可以反复使用的简单模块。然后,你会将所有代码合并在一起,制作一个你与喷火龙战斗的游戏!
• 在第十一章:事件驱动编程中,你将制作响应用户输入的交互式程序,比如一个简单的绘图程序。
• 第十二章:构建图形用户界面介绍了如何创建一个完整的应用程序,包含按钮、标签和所有专业程序的“花里胡哨”功能。你将在简单绘图程序的基础上,创建让用户可以更改画笔颜色的按钮。
• 第十三章:重复的 For 循环将向你展示如何在程序中使用For循环,以避免重复代码。你将学习如何自动化枯燥的任务,并在几行代码中绘制出大量图片。
• 第十四章:创建条件式 while 循环讨论了更高级的条件编程,并以制作一个你可以与计算机对战的石头剪刀布游戏作为结尾。
• 第十五章:在一维数组中分组数据介绍了数组以及如何存储大量数据。存储和处理数据是编程中的另一个重要方面,你将利用这一点编写一个神奇的 8 号球程序。
• 第十六章:使用关联数组存储数据向你展示如何将字符串存储在描述性数组中。你将通过编写一个能够自动生成诗歌的程序,将计算机变成一个诗人。
• 一旦你掌握了数组的使用,在第十七章:扩展到高维数组中,你将学习如何将数组扩展到两维或更多维度,这样你就可以存储更多的数据。最后,你将制作一个自己的寻宝游戏。
• 第十八章:高级文本魔法教你如何在程序中处理和处理文本。然后,你将利用这些知识编写一个简单的拼写检查程序。
• 第十九章:接收文件输入与输出帮助你通过教你如何处理满载数据的文件来构建更大的程序。然后,你将运用这些知识编写一个包含数学魔法师的程序。
在线资源
访问www.nostarch.com/smallbasic/下载额外的书籍资源并查找更新。你将找到这些附加资源以及针对教师和学生的复习题:
书籍程序和解决方案 下载完成的程序、你需要的所有图像、编程挑战的一些骨架代码,以及编程挑战和“试试看”练习的解决方案。这将帮助你减少手指的疲劳!
附加资源 这些是与本书所涵盖的主题相关的在线文章。许多文章是专门为补充本书而写的!
复习题 测试你的知识(或你学生的知识)。
练习题 除了书中的“试试看”和“编程挑战”练习外,你还可以找到更多的练习来加以实践。这对于希望有更多作业选项的教师也非常有用。
致读者的说明
在学习新技能时,没有什么比练习更重要的了。阅读本书只是第一步。要成为一名出色的程序员,你必须编程!你使用本书资源越多,学到的东西就越多。不要害怕实验。无论你按了什么按钮或输入什么命令,都不会伤害到计算机。我们保证。
只要稍加耐心和投入,你很快就能用你所创造的奇妙事物让朋友们惊叹。我们希望能赋予你创造有趣游戏甚至改变世界的能力!
第二章:1
介绍小型 Basic

比尔·盖茨曾经有一个目标:让每个家庭都有一台计算机。现在,几乎每个桌子上都有个人计算机——所以几乎每个人也可以学习编程了。在本书中,你将学习使用一种名为 Microsoft Small Basic 的编程语言。
我们将从解释一些基本的计算机概念和 Small Basic 本身开始本章内容。接着,我们将向你展示如何设置使用 Small Basic 所需的一切,并最后写出你的第一个程序!
什么是计算机?
计算机是一种电子设备,它根据一组指令处理数据——它就是你口袋里或桌面上或膝上的那个神奇设备。计算机可以进行计算(就像你的数学老师一样),并比较数字(比如在幻想足球中),它们能够以高速和高精度存储、检索和处理数据(就像父母记住宵禁时间一样)。
计算机的硬件是指你能触摸到的所有部分——每台计算机内部都包含数百个相互连接的电子元件。如果你想象计算机中的数据,可以把它想象成一个庞大的商场,里面有成百上千的商店,成千上万的顾客像时钟一样在商店之间移动。
但没有一些额外的东西,这些硬件什么也做不了。每台计算机都需要程序来告诉它该做什么——我们称这些指令为软件。能够编写软件的人被称为程序员——今天你就要成为其中一员。
什么是计算机程序?
计算机程序是一组指令,给计算机执行某个任务(就像老师布置的作业清单)。你的网页浏览器、你最喜欢的视频游戏、文字处理器——这些都是计算机程序。
程序告诉计算机要读取哪些数据(比如数字或文本)、从哪里读取数据(比如从用户、文件或互联网)、如何处理这些数据(可能是搜索、排序或计算数据)、要生成什么样的信息(比如段落、报告或图表)、将生成的输出存储在哪里(比如磁盘、网络或数据库),以及如何显示结果(比如通过显示器、打印机或绘图仪)。哇,这真是很多内容!
计算机程序指定了执行过程中的每个细节。计算机使用机器语言进行通信,这种语言由一堆 1 和 0 组成。(你能想象和朋友们用 1 和 0 交流吗?)很久以前,第一批计算机程序实际上是通过在计算机前面板上切换一些开关来输入的(1 为开,0 为关)。你想整天切换开关吗?想象一下可能出现的错误!
幸运的是,计算机科学家发明了编程语言,这些语言比机器语言更容易使用。今天有成百上千种编程语言,但在本书中你将学习的编程语言就是 Small Basic!
什么是小型 Basic?
小型基础是一种微软为任何想学习编程的人创建的免费编程语言。你可以使用小型基础编写各种应用程序,包括游戏、模拟、动画等。
这个语言是如何诞生的呢?它源于微软的一位程序员 Vijaye Raji。Raji 刚刚读了 David Brin 的文章《为什么 Johnny 不会编程》,^(1) 文章描述了学习和教授 BASIC 编程语言的价值。在文章中,Brin 挑战微软创造一种新的 BASIC 语言,帮助孩子们学习编程,Raji 接受了这个挑战。尽管 BASIC 在 1970 年代、1980 年代和 1990 年代对微软的成功至关重要,但在 2007 年,实际上并没有适合初学者的优秀编程语言。
所以 Raji 想知道是否可以只使用原始语言中最简单的部分,创建一个简化版的 BASIC。2008 年 10 月 23 日,他发布了微软小型基础 v0.1,这是小型基础的第一个版本。
小型基础的愿景
小型基础的四个目标将帮助你获得尽可能棒的学习体验:
• 简单。小型基础是一种简单的编程语言,配有便捷的代码编辑器和帮助区域,使得编程变得轻松。
• 有趣。小型基础让你立即开始创建游戏和其他有趣的程序。它还允许你控制海龟绘制艺术,非常有趣!
• 社交性。使用小型基础,你可以将你的游戏发布到微软画廊网站上,展示给朋友们,并将它嵌入到你的博客或网站上。你的朋友们可以导入你的程序,与您合作改进它。
• 渐进性。一旦你掌握了小型基础的编程基础,就可以轻松地将代码导出到免费的 Visual Studio Community,并开始新的冒险,学习 Visual Basic .NET,这是一种被数百万专业程序员使用的编程语言,也是你学习旅程中的一个重要步骤。
在本书中,我们将涵盖你开始使用小型基础所需的所有内容!
小型基础的基础
小型基础的三个主要部分是语言、支持库和编程环境,后者是你用来编写自己程序的界面。现在,让我们来探索每个元素。
小型基础语言
为了在英语中形成一个有效的句子,你需要遵循其语法规则。同样,要编写一个有效的小型基础程序,你必须遵循小型基础的语法规则,这些规则被称为语法规则。语法包括标点、拼写、语句顺序等。当你违反这些规则时,小型基础会检测到程序中的所有语法错误并报告给你,以便你修复它们。
小型基础库
Small Basic 库 包含了数百种方法,你可以在程序中使用它们来执行不同的任务。例如,当你想让计算机在屏幕上显示图片、画圆形、从互联网上下载文件,甚至计算 275,625 的平方根时,你都可以使用这些方法。
Small Basic 开发环境
Small Basic 配备了一个集成开发环境(IDE),这是你用来编写程序的应用程序。IDE 包含一个文本编辑器(你将在其中输入程序)和一个工具栏。工具栏有一些按钮,可以让你保存和运行程序,打开程序以便修改,分享程序到网页,将程序转换为 Visual Basic 等等。
安装 Small Basic
你学习旅程的第一步是将 Small Basic 安装到你的计算机上。打开你的网页浏览器,访问微软的 Small Basic 网站 www.smallbasic.com/,并点击右上角的 下载 按钮。你将进入下载页面,选择操作系统和语言。开始下载时,会弹出一个对话框,询问是否允许打开 SmallBasic.msi 文件。点击 运行 或 打开 按钮开始安装向导。
启动安装向导后,在第一页点击 下一步,接受许可协议,再次点击 下一步,选择默认设置并点击 安装。(如果弹出用户访问控制对话框并请求安装许可,点击 是。)安装完成后,点击 完成。如果你需要查看这些步骤的详细信息,请访问 tiny.cc/installationguide/。
Small Basic IDE
现在你的安装完成了,让我们来看看 Small Basic IDE。打开 Windows 开始菜单,输入 Small Basic 来搜索并打开它,或者选择 所有程序
Small Basic
Microsoft Small Basic。当你第一次运行程序时,你会看到类似于 图 1-1 的界面(在编辑器中输入 Prog 来查看 IntelliSense 菜单)。
IDE 包含四个主要部分。编辑器 ➊ 是你输入 Small Basic 程序的地方。你可以同时打开并处理多个编辑器窗口,但每次只有一个编辑器窗口是活动的。右键点击编辑器可以查看包含剪切、复制、粘贴和查找等选项的弹出菜单。该菜单还包含一个“格式化程序”选项,可以对程序中的行进行缩进,使其更容易阅读。

图 1-1:Small Basic IDE
工具栏 ➋ 包含按钮,允许你编辑和运行程序,帮助区域 ➌ 提供关于你输入编辑器的代码的即时信息。工作区 ➍ 是一个开放区域,你可以在这里移动和整理每个 Small Basic 程序的编辑器窗口。
你将经常使用工具栏,所以让我们详细了解它。
打开和保存你的工作
在工具栏的文件组中,点击新建(CTRL-N)从头开始编程,或点击打开(CTRL-O)继续写程序。保存(CTRL-S)经常保存,以免丢失工作,并点击另存为将程序保存在新文件中。
分享你的作品并导入游戏
假设你的朋友刚刚将一个新游戏发布到 Small Basic 网站,而你想看看。点击网页组中的导入,输入导入 ID(你从朋友那里获得的),然后下载你朋友的代码。然后,你可以通过自己的修改使这个游戏更酷。
让我们尝试打开别人已经制作的游戏。点击导入,然后输入代码TETRIS。你将看到某人写的代码,重现了著名的游戏,并可以看到它是如何制作的。现在,点击运行来玩游戏。
稍后,当你准备好分享自己的程序时,可以点击发布,Small Basic 将把你的程序发布到网页上,供你的朋友在线玩你的游戏或应用,并查看你的代码。你还可以在 Small Basic 论坛分享你的程序,直接向社区寻求帮助。Small Basic 甚至允许你嵌入代码片段,这样你就可以将项目添加到你的网站上。你可以在发布的网页上找到嵌入代码。
当你点击发布时,你将看到一个对话框,类似于图 1-2。

图 1-2:发布到网页对话框
当你发布代码时,除了获得导入 ID 和网页 URL 外,你还可以点击添加更多详细信息,输入程序的标题、描述和类别(如游戏、示例练习、示例、数学、娱乐或杂项)。
复制和粘贴;撤销和重做
在 Small Basic 中,你可以像编辑任何文本一样编辑代码。在剪贴板组中,点击剪切(CTRL-X)将代码从编辑器中的一个位置移除并粘贴到另一个位置。为了避免重新输入代码,点击复制(CTRL-C)。要选择所有代码,按 CTRL-A,然后剪切或复制它。
在剪切或复制之后,点击粘贴(CTRL-V)将内容粘贴到编辑器中。如果你犯了错误,不用担心!只需点击撤销(CTRL-Z)。如果你点击了撤销太多次,点击重做(CTRL-Y)来恢复更改。你还可以在一个大型文件中查找某段代码。要打开查找窗口并搜索文本,只需按 F3,按 CTRL-F,或右击编辑器并在上下文菜单中点击查找。
运行你的程序并毕业
当你完成一个程序时,点击运行(F5)按钮进行编译;Small Basic 编译器,作为 IDE 的一部分,会检查是否有错误,如果没有,则构建你的程序。当你掌握了 Small Basic 后,只需点击毕业按钮,将代码导出到 Visual Studio Community 中的 Visual Basic,并开始学习下一种语言。
编写和运行你的第一个程序
现在你已经熟悉了 IDE,让我们写一个 Small Basic 程序。首先,在你的计算机上创建一个名为Small Basic的新文件夹;这里将是你保存本书中创建的所有程序的地方。
然后点击新建按钮打开一个新的编辑器窗口,并按照以下步骤操作:
-
在编辑器中输入清单 1-1 中的程序。你需要准确地按照所示输入。
1 ' Greetings.sb 2 TextWindow.WriteLine("Greetings, Planet!")清单 1-1:你的第一个程序
注意
当你输入清单并尝试时,不要包括左侧的行号!这些行号仅供参考;我们会用它们来解释代码。你在编辑器中也会看到这些行号,但它们不是代码的一部分。
-
点击工具栏上的保存按钮(或按 CTRL-S),浏览到你刚创建的Small Basic文件夹,并将程序保存为Greetings.sb。
-
点击工具栏上的运行按钮。如果没有输入错误,你将看到一个类似于图 1-3 的输出窗口。
![image]()
图 1-3: Greetings.sb 的输出
注意
当你运行这个程序时,窗口将有一个黑色的背景;这是文本窗口的默认背景色。本书中的图片使用白色背景,便于你阅读。
尽管这段程序很短,但它是一个完整的 Small Basic 程序!那么每一部分程序是做什么的呢?让我们来分析一下。
对象和方法
图 1-3 中的窗口是文本窗口,它只能显示文本。你通过TextWindow(Small Basic 库中的许多对象之一)告诉 Small Basic 弹出文本窗口。你可以将对象看作是为特定任务提供工具的小工具箱,例如解决数学问题、定义单词或绘制图片。
Small Basic 中的对象可以通过使用方法执行预定义的任务。方法就像是你对象工具箱中的工具。为了让大多数方法完成某个任务,你需要给它们一个或多个值(如文本或数字)来操作。每个值都称为参数。
WriteLine()是TextWindow对象的一个方法,消息"Greetings, Planet!"是传递给括号的一个参数。语句TextWindow.WriteLine()指示计算机在文本窗口中显示消息Greetings, Planet!。
在本书中,我们将包括方法名称的括号,例如WriteLine(),以便你能轻松辨认出它们是方法。
命名你的程序
项目名称可以帮助你轻松识别项目的内容;这些名称对 Small Basic 来说并不重要。虽然我们建议你将该程序保存为Greetings.sb,因为它代表了程序的内容,但你也可以将它保存为SecretGarden.sb、FuzzyKittens.sb,甚至是HungerBoardGames.sb,如果你真想这么做的话。只是不要更改文件名的* .sb 部分,这叫做扩展名*。Small Basic 程序默认使用这个扩展名,没有理由去更改它!
Small Basic 生成的文件
当你点击运行按钮时,Small Basic 会生成其他文件以便运行你的程序。打开你保存Greetings.sb程序的文件夹。表 1-1 列出了你在该文件夹中应该找到的文件,如果你之前点击了运行。
表 1-1: Small Basic 编译器生成的文件
| 文件 | 描述 |
|---|---|
| Greetings.sb | 这是你的源代码文件,包含你在 IDE 中输入的所有内容。如果你想编辑代码并使其更好,你就编辑这个文件。 |
| Greetings.exe | 这是 Small Basic 创建的可执行文件。这个文件是你电脑实际运行的文件。双击该文件,你的程序就会运行。 |
| SmallBasicLibrary.dll | 你现在可以忽略这个文件。动态链接库(.dll)文件包含补充Greetings.exe文件的可执行代码。没有这个文件,Greetings.exe 文件无法运行! |
| Greetings.pdb | 你目前可以忽略这个文件。这个程序数据库(* .pdb )文件包含高级工具用来调试*,或修复程序中的错误所需的信息。 |
现在你已经编译了源代码,你也可以在不使用 IDE 的情况下运行Greetings.sb程序。你只需双击Greetings.exe文件即可。
注意
当你编辑源文件后点击运行时,Small Basic 会覆盖 .exe、.dll 和 .pdb 文件。如果你想保留这些文件,你需要在点击运行之前手动将它们复制到其他位置。另外,别忘了点击保存,以便保存对 .sb 文件的更改。
帮助提示:智能感知和语法高亮
如果你正在跟着操作并在 Small Basic 中输入,你会看到它会在你完成输入单词之前就分析你输入的内容。Small Basic 会提供一个建议列表,帮助你完成输入的内容。你可以通过按键盘上的上下箭头来滚动这个列表。按回车键或双击选项来将高亮的文本插入到代码中。这项技术叫做智能感知,简称IntelliSense。利用它可以加速你的输入速度,并减少语法错误。
提示
你可以通过按住 CTRL 键来使 IntelliSense 变得透明,以便查看下面的代码。
你可能还注意到 Small Basic 编辑器使用不同的颜色显示了程序中的某些单词。这个功能叫做语法高亮。关键字是对 Small Basic 有特殊含义的保留字,显示为蓝紫色。字符串是用引号括起来的字符序列,显示为橙色,数字也是如此。方法名称是深红色的,对象名称是蓝绿色的,依此类推。语法高亮帮助你区分代码的不同部分,并使你的程序更易于阅读。你将在本书后面学到更多关于这些代码部分的内容。
使用 Small Basic 绘图
我们之前使用的TextWindow对象非常适合没有图形用户界面 (GUI) 的应用程序,GUI 是包含按钮、文本框和图像的用户界面,例如 Microsoft Word 或 Angry Birds(或 Angry Words)。例如,你可以使用TextWindow来编写执行数学题目或处理数据的应用程序,其中的输入和输出只使用字符(如文本)。这被称为基于文本的用户界面。如果你想创建一个具有 GUI(发音为gooey,像糖果条一样)的应用程序,包含按钮和图像,你可以使用 Small Basic 库中的GraphicsWindow对象。使用GraphicsWindow,你可以创建展示按钮、图像等内容供用户互动的应用程序。让我们试试吧!
将清单 1-2 中的程序输入到编辑器中,然后点击工具栏中的运行。
1 ' Graphic.sb
2 GraphicsWindow.DrawText(100, 50, "Greetings, Planet!")
清单 1-2:你的第一个图形程序
这个程序使用了GraphicsWindow对象的DrawText()方法来显示一条消息。DrawText()方法需要三个参数。前两个参数告诉 Small Basic 输出消息的水平(x)和垂直(y)位置,从窗口的左上角开始。第三个参数告诉 Small Basic 显示什么文本。这个程序的输出如图 1-4 所示。你可以看到,消息显示在位置 (100, 50)。

图 1-4:Graphic.sb 的输出
GraphicsWindow对象包含许多其他方法,允许你创建 GUI 应用程序;在试试看 1-1 中可以探索其中的一些方法。
试试看 1-1
将以下程序输入到编辑器中,然后运行它,看看会发生什么:
GraphicsWindow.DrawEllipse(20, 20, 100, 100)
GraphicsWindow.DrawRectangle(140, 20, 100, 100)
GraphicsWindow.FillEllipse(260, 20, 200, 100)
程序的输出如图 1-5 所示。数字显示了 Small Basic 如何跟随你的代码。

图 1-5:程序的输出
编程挑战
如果你遇到问题,可以查看 nostarch.com/smallbasic/ 获取解决方案以及更多的资源和教师、学生的复习问题。
-
在以下代码片段中识别对象、方法、参数和关键字。(提示:Small Basic 编辑器会将关键字显示为蓝紫色。)
If (today = "Friday") Then TextWindow.WriteLine("Today is Friday.") Else TextWindow.WriteLine("I lost track of what day it is.") EndIf -
编写一个程序,在文本窗口中显示你的名字。
-
编写一个程序,使用
GraphicsWindow对象在消息框中显示你的名字。(提示:使用ShowMessage()方法。)
第三章:2
入门

现在我们将逐步讲解一些代码,让你了解重要的组成部分。在阅读时,输入这些示例,我们将解释如何运行它们并进行修改。但不要止步于此:通过实验为你的程序增添个人特色。我们在每一节的末尾都附有练习,帮助你成为编程大师(像绝地武士一样,但没有危险的光剑)。尝试完成这些练习来磨练你的技能。
程序的组成部分
让我们通过查看一个简单的示例来探索程序的不同部分。清单 2-1 展示了一个与第一章中你编写的Greetings.sb程序类似的程序。将这个程序输入到 Small Basic 编辑器中,然后点击工具栏上的运行按钮(或者按键盘上的 F5 键)来运行它。
1 ' Welcome.sb
2 TextWindow.WriteLine("Welcome to Small Basic.")
清单 2-1:编写欢迎信息
这两行是你Welcome.sb程序的源代码。当你运行这段代码时,你将看到一个输出窗口,类似于图 2-1 所示。(注意,窗口的标题显示了我们保存文件的位置,因此你的可能会不同。)

图 2-1:Welcome.sb 的输出窗口
注意
你的控制台窗口看起来会与这个略有不同,因为窗口默认有黑色背景。在本书的其余部分,我们将以文本形式展示输出,除非必须查看窗口时。
Small Basic 会自动将文本 Press any key to continue... 添加到窗口中,让你有机会查看输出结果(键盘上并没有一个任意键,所以不要寻找它)。否则,屏幕会闪烁显示输出,然后消失。
注释和语句
以单引号(')开头的行被称为注释。你可以添加注释来解释程序的功能,而 Small Basic 会忽略它们。第 1 行的注释是包含你源代码的文件名。
注意
你应该养成注释代码的习惯,因为你会经常到论坛或向朋友寻求帮助,他们需要理解你的代码在做什么。
Small Basic 编辑器将所有注释显示为绿色,这样你可以轻松地区分它们与实际代码行,后者称为语句。注释使程序更容易阅读,你可以在任何地方添加它们!但要小心,不要使用过多的注释,否则可能会让代码更难以阅读!在代码开头写注释来描述程序或解释任何难懂的部分是一个好习惯。
如果你添加一个空行来分隔注释和代码,Small Basic 也会忽略它,所以你可以根据需要添加任意数量的空行,以便让程序更易读!Welcome.sb中的第 2 行是你程序的第一条语句,程序就是从这里开始执行的。(别担心:没有人会死!)
图 2-2 展示了我们语句的各个部分。让我们逐一分析,看看每部分的作用!

图 2-2:Welcome.sb 中的语句
TextWindow 是 Small Basic 内置的一个对象,用于接收文本输入并将文本输出到屏幕上。WriteLine() 是 TextWindow 对象的方法。该方法将传递给它的数据显示在输出窗口中。当你使用 TextWindow.WriteLine() 时,你是告诉 TextWindow 对象执行它的 WriteLine() 方法。这被称为点符号表示法,因为对象和方法之间有一个点。点符号表示法用于访问对象的方法,格式如下:ObjectName.MethodName(Arguments)。在这个示例中,"Welcome to Small Basic." 是 WriteLine() 方法的一个参数。它明确告诉方法你想要写的内容。
字符和字符串
字母、数字、标点符号(如句号、冒号、分号等)以及其他符号统称为字符。这些字符组成的序列,如果被双引号包围,就叫做字符串。引号显示了字符串的开始和结束位置。
在我们的Welcome.sb程序中,显示的文本 "Welcome to Small Basic." 是一个字符串。
参数和方法
你通过方法的圆括号内传递参数。参数可以是一个字符串、一个数字或其他值。WriteLine()方法只接受一个参数,在你的Welcome.sb程序中,你将字符串 "Welcome to Small Basic." 作为它的参数传递。
点击编辑器中的 WriteLine() 方法,然后查看 Small Basic 的帮助区域(图 2-3)。它会告诉你应该传递什么类型的数据给该方法。

图 2-3:WriteLine() 方法的帮助区域信息
帮助区域是你的好帮手!阅读它可以避免不必要的错误和困惑。
试试看 2-1
请指出以下调用中的对象、方法和参数:
-
Shapes.AddRectangle(100, 50) -
Math.Max(5, 10) -
Sound.PlayBellRing()
探索其他特性
在本节中,你将通过对Welcome.sb程序进行一些小改动,探索 Small Basic 的其他关键特性。每个示例都突出展示了一个不同的特性,快来一起试试看吧!Small Basic 是友好且易于上手的!
大小写敏感
你最初输入的是 TextWindow.WriteLine("Welcome to Small Basic."),但如果你更改了TextWindow或WriteLine中的字母大小写,Small Basic 也不会在意。例如,你可以写成:TextWindow.writeLINE("Welcome to Small Basic.")。这将得到与之前相同的输出,因为 Small Basic 是不区分大小写的,这意味着你的代码是写成大写字母还是小写字母都不重要。
像Writeline、writeline和WRiTeLiNe这样的标识符在编译器中会被相同地解释,编译器会读取每一行代码并构建应用程序。但是,你应该养成尊重标识符大小写的习惯,因为其他语言是区分大小写的。Small Basic 就像一个友好的教练,不会因为大小写不当而对你大喊大叫。得益于 IntelliSense 的自动纠正功能,它甚至会帮你修正打字错误。
如果你改变字符串会发生什么呢?试试将欢迎信息输入为全大写字母:
TextWindow.WriteLine("WELCOME TO SMALL BASIC.")
当你运行这个程序时,WELCOME TO SMALL BASIC.将在输出窗口以全大写字母显示。为什么?原因是 Small Basic 的WriteLine()方法会准确地显示双引号之间的内容,正如你输入的那样!
顺序执行
示例 2-1 只显示一行文本,但你可以显示任意多的行。让我们按照示例 2-2 扩展程序,显示三行内容!
1 ' ThreeLines.sb
2 TextWindow.WriteLine("Welcome to Small Basic.")
3 TextWindow.WriteLine("")
4 TextWindow.WriteLine("Anyone can code!")
示例 2-2:显示更多行
当你运行这个程序时,你将看到以下输出:
Welcome to Small Basic.
Anyone can code!
你的程序输出显示了每一行代码按程序中列出的顺序从上到下执行。你看到输出中的空行了吗?那是第 3 行的语句创建的,那里你给WriteLine()传递了一个没有字符的双引号。因为""不包含任何字符,它被称为空字符串。空字符串在你想显示空行来分隔程序输出并使其更易读时非常有用。
显示数字并进行数学运算
你也可以使用WriteLine()来显示数字。试试示例 2-3。
1 ' TextAndNum.sb
2 TextWindow.WriteLine("5 + 7")
3 TextWindow.WriteLine(5 + 7)
示例 2-3:显示字符串和数字的区别
这是该程序的输出:
5 + 7
12
当你将任何内容传递给WriteLine()并用双引号括起来时,输出窗口会显示双引号内的内容。因此,当你在第 2 行将"5 + 7"传递给WriteLine()时,Small Basic 会将字符串中的加号当作普通字符处理,而不会把它看作加法运算!
然而,第 3 行的WriteLine()命令不同。你传递给WriteLine()的是5 + 7,没有用双引号括起来。在这种情况下,Small Basic 明白这些是数字,而不是字符串的一部分。它在幕后将 5 和 7 相加得到 12,并将和传递给WriteLine()。
连接字符串
你还可以将字符串连接在一起,构建句子或添加短语,正如在示例 2-4 中所示。将字符串合并称为拼接。
1 ' JoinString.sb
2 TextWindow.WriteLine("Hello," + " oblate spheroid!")
示例 2-4:解释拼接
在 清单 2-4 的第 2 行中,WriteLine() 方法接收两个字符串,"Hello," 和 " oblate spheroid!",它们之间用加号(+)连接。在这种情况下,由于你没有执行加法操作,加号的意义不同:它被称为 连接运算符,它将两个字符串连接成一个字符串。注意 " oblate spheroid!" 中的额外空格。它使得你的消息在单词之间显示空格。
加号(+)将 "Hello," 拼接到 " oblate spheroid!" 上,生成新的字符串 "Hello, oblate spheroid!"。
你还可以将字符串和数字连接在一起。Small Basic 会自动将任何数字转换为字符串,以便进行连接操作!查看 清单 2-5 及其在 图 2-4 中的输出。
1 ' JoinNum.sb
2 TextWindow.WriteLine("Let's concatenate: 5 + 7 = " + 12)
清单 2-5:将数字添加到文本中
WriteLine() 方法需要一个字符串作为参数。为了创建该字符串,Small Basic 会将整个参数转换为字符串,如 图 2-4 所示。它将数字 12 转换为字符串("12"),然后将其与 "Let's concatenate: 5 + 7 = " 拼接在一起,生成一个新的字符串:"Let's concatenate: 5 + 7 = 12"。

图 2-4:使用加号连接字符串和数字
尝试 2-2
编写程序以显示 图 2-5。

图 2-5:制作一个面孔
对象属性
Small Basic 对象可以具有 属性(或特性),你可以更改这些属性。如果更改这些属性,调用对象方法时可能会得到不同的结果。
例如,假设我们有一个名为 Frog 的新对象,它包含两个方法,Jump() 和 Eat(),以及一个名为 EnergyLevel 的属性。当你调用 Jump() 方法时,Frog 会跳跃,但每次跳跃都会使其 EnergyLevel 降低。你可以调用 Eat() 方法来恢复它的能量。如果你不停地让 Frog 跳跃却不给它食物,Frog 就会耗尽能量,无法再跳跃。调用 Jump() 方法的结果取决于 EnergyLevel 属性的当前值。这个属性改变了 Frog 对象的 状态(它是否能跳跃)。在一个状态下(EnergyLevel 高时)调用 Jump() 与在另一个状态下(EnergyLevel 低时)调用 Jump() 会产生不同的结果。可怜的饥饿青蛙!
设置和更改属性值
下面是设置或更改对象属性的常规格式:
ObjectName.PropertyName = Value
例如,要让 TextWindow 对象输出黄色文本,你可以输入:
TextWindow.ForegroundColor = "Yellow"
该语句会改变 TextWindow 对象的状态:在此语句执行后,任何通过调用 WriteLine() 输出的文本都会以黄色显示。但已经显示在文本窗口中的文本不会受到影响。该语句告诉 TextWindow 对象:“从此开始,使用黄色显示文本。”
处理属性
示例 2-6 展示了一些使用TextWindow属性的方法。
1 ' Properties.sb
2 TextWindow.Title = "Discovering Properties..."
3 TextWindow.BackgroundColor = "Yellow"
4 TextWindow.Clear()
5
6 TextWindow.CursorLeft = 4
7 TextWindow.CursorTop = 1
8 TextWindow.ForegroundColor = "Blue"
9 TextWindow.Write("BLUE TEXT")
10
11 TextWindow.CursorTop = 3
12 TextWindow.ForegroundColor = "Red"
13 TextWindow.Write("RED TEXT")
14
15 TextWindow.CursorLeft = 1
16 TextWindow.CursorTop = 5
17 TextWindow.BackgroundColor = "Green"
示例 2-6:放置和着色文本
运行这段代码会在图 2-6 中显示输出结果。

图 2-6: Properties.sb 的输出
现在让我们逐步分析代码。图 2-7 将帮助你更好地理解发生了什么。它展示了文本窗口作为一个矩形的字符网格,并显示了在 Small Basic 执行每条语句后光标的位置。

图 2-7:展示 Properties.sb 的输出
第 2 行设置了Title属性,告诉 Small Basic 文本窗口的标题。第 3 行将BackgroundColor属性设置为“黄色”,用于接下来所有的文本输出。Clear()方法(第 4 行)告诉TextWindow使用其BackgroundColor属性重新绘制自己,这使得窗口的背景变成了黄色。尝试从程序中删除这一行,看看程序的输出会发生什么变化。
第 6 到第 8 行将光标位置设置为第 4 列,第 1 行,并将前景色(文本颜色)设置为蓝色,以便下一个输出。第 9 行的Write()方法写入字符串"BLUE TEXT",从当前光标位置开始。Write()方法与WriteLine()方法类似,不同之处在于它不会在显示字符串后将光标移动到下一行。调用此方法后,光标位于第 13 列,但仍在第 1 行。
第 11 行将光标移动到第 3 行。第 12 行将前景色设置为红色,第 13 行调用Write()显示字符串"RED TEXT"。
第 15 行和第 16 行将光标移动到第 1 列,第 5 行;第 17 行将背景色设置为绿色。这是最后一条语句,因此程序在此时终止(因为没有更多的代码可供执行)。由于文本窗口的前景色仍然设置为红色,Press any key to continue...消息将在绿色背景上以红色显示。
提示
要查看可以在文本窗口中使用的完整颜色列表,请访问 tiny.cc/twcolors/。
试一试 2-3
现在你有机会在下一个情人节展示高科技。编写一个程序,绘制一个类似于图 2-8 所示的卡片,并与喜欢的人分享。(提示:先用红色画心形图案。然后将前景色切换为绿色,并调用Write()三次来绘制文本。)发挥你的创造力,选择个人化的颜色。

图 2-8:情人节心形图案
算术运算符
计算机非常擅长处理数字(它们有兆字节!)并且作为高级计算器非常有效。Small Basic 包括了四种基本的算术运算:加法、减法、乘法和除法,分别由+、–、*和/表示。这些符号叫做运算符,因为它们对数值进行操作,而这些数值被称为操作数。让我们来看一些例子。这些数学运算你应该很熟悉。试着在编辑器中输入这些行:
TextWindow.Writeline(4 + 5)
TextWindow.Writeline(3 / 6)
TextWindow.Writeline(8.0 / 4)
TextWindow.Writeline(3 * 4)
TextWindow.Writeline(9 - 3)
当你运行这个程序时,每个答案都会出现在新的一行,像这样:
9
0.5
2
12
6
但是,Small Basic 如何计算像6 * 2 + 3这样的表达式结果呢?这是否意味着先乘以 6 和 2,然后加 3,结果是 15,还是先乘以 6 再加上 2 和 3 的和,结果是 30?当一个算术表达式包含不同的运算符时,Small Basic 会按照代数中相同的优先级来完成运算,正如在图 2-9 中所示。

图 2-9:Small Basic 中的运算顺序
所以,对于没有括号的6 * 2 + 3,Small Basic 会先乘以 6 和 2,然后加上 3,得到 15。
如同普通数学一样,在 Small Basic 程序中,每个左括号必须有一个匹配的右括号。例如,表达式(6 + 4)是有效的,但(6 + (8 – 2)))是无效的,因为它有多余的右括号。
为了确保你得到正确的结果,使用括号来明确运算顺序。这有助于避免错误,并使你的代码更容易理解。例如,输入以下内容:
TextWindow.WriteLine((3.5 + 6.5) - (5 - 2.5))
如果你正确放置了括号,应该得到 7.5。
在运算符的两边加一个空格也是个不错的主意。例如,表达式5 + 4 * 8比5+4*8更容易阅读。虽然 Small Basic 能够读取两个连续的算术运算符,例如3*–8,但最好将负数放在括号中,如3 * (–8),这样可以让你的代码更易读,避免任何混淆。
试试看 2-4
在刘易斯·卡罗尔的《镜中奇遇》中,红皇后和白皇后让爱丽丝在仙境中做一些加法和减法。使用WriteLine()方法,创建 Small Basic 程序帮助她解决这两个问题:
“你会做加法吗?”白皇后问。“一加一加一加一加一加一加一加一加一加一是多少?”
“我不知道,”爱丽丝说,“我数错了。”
“她不会做加法,”红皇后打断道,“你会做减法吗?从八中减去九。”
“九减八我做不到,你知道的,”爱丽丝很快回答:“但是——”
编程错误
仅仅因为程序运行了,并不意味着它是正确的。所有程序员都会犯错误,尤其是在编写长程序时。但不用担心!你练得越多,错误就越少。编程中的三种主要错误类型是语法错误、逻辑错误和运行时错误;我们将教你如何找到并修复它们。
语法错误
当程序违反语言的语法规则时,就会出现错误。语法错误的例子包括以下几种:
• 缺少标点符号,例如TextWindow.WriteLine("Hello),它包含一个没有结束引号的字符串
• 语句末尾的额外标点符号
• 拼写错误的关键字,例如Whle而不是While
• 错误使用算术运算符,例如5 ** 2
• 算术表达式中的括号不匹配,例如5 * (6 - (3 + 2)
注意
A关键字是一个特殊的词,告诉 Small Basic 做某事,比如重复执行一条语句。我们将在后续章节中解释每个关键字。
幸运的是,一旦你点击运行按钮,Small Basic 会发现所有语法错误,并在错误消息中描述它们。错误消息会列出在源代码中发现错误的行号(请参阅图 2-10)。如果程序包含语法错误,查看包含错误的行,并看看你能否修复它!

图 2-10:语法错误的示例
需要快速找到问题?只需双击错误消息,即可跳转到包含错误的行。(相当酷吧?)
逻辑错误
有时,你可能会在程序的逻辑中犯错。这些逻辑错误导致程序输出错误的结果。例如,如果你不小心用了减号代替加号,你就犯了一个逻辑错误。程序正常运行,但输出不正确!
逻辑错误被称为bug,而调试是我们用来查找和修复这些 bug 的过程。对于短小的程序,你可能能够通过手动追踪来定位 bug,这意味着你逐行阅读程序,并记录每一步你期望的输出。另一种常见的技术是插入额外的WriteLine()语句,以在程序的不同部分显示输出。这有助于你缩小可能出错的代码行。
运行时错误
运行时错误发生在你运行程序后,当程序遇到无法在代码中解决的问题时。例如,用户可能输入不合适的数字,导致程序停止工作或崩溃。当你开始使用 Small Basic 时,你会亲自发现这些错误。
编程挑战
如果遇到困难,可以访问* nostarch.com/smallbasic/ *以获取解决方案以及更多针对教师和学生的资源和复习问题。
-
编写一个显示你名字和年龄的程序,类似于以下输出。使用颜色使输出符合你自己的风格!
My name is Sandra Wilson I am 12 years old -
用字符串替换下面程序中的问号,使其向用户提供有关元素在周期表中顺序的信息。运行程序以检查其输出。
TextWindow.Write("?" + " is the " + "?") TextWindow.WriteLine(" element in the periodic table.") -
凯西编写了以下程序,以计算她通过做保姆赚了多少钱。但是有一个问题:她的程序无法正常工作。帮凯西找到程序中的错误并修复它。
' This program computes my earnings from babysitting. ' Hours worked: 20 ' Pay rate: $4 per hour TextWindow.WriteLine("I earned: $" (20 * 4)) -
编写一个程序,创建一张类似于这里展示的圣诞卡片。使用任意颜色装饰树木。
![image]()
第四章:3
绘图基础

使用 Small Basic,你可以成为一位技艺精湛的艺术家。你拥有的不是画笔,而是代码的力量。让我们开始吧,让你可以开始创作属于自己的杰作!我们已经向你展示了TextWindow对象,但在本章中,你将探索GraphicsWindow对象,它包含绘制线条、三角形、矩形、椭圆甚至花式文本的方法。
图形坐标系统
将图形窗口看作一个矩形网格。网格上的每个点都用两个数字来描述,这两个数字叫做坐标。x 坐标告诉你点的水平位置,y 坐标告诉你点的垂直位置。你可以使用括号指定点的位置,像这样:(x,y)。
在你在学校使用的坐标系统中,点(0,0)位于图表的中心,但在图形窗口中情况有所不同。图 3-1 向你展示了点(0,0)位于图形窗口的左上角,这意味着你只能看到具有正 x 和 y 值的点。

图 3-1:图形窗口的坐标系统
现在你已经了解了图形窗口的坐标系统是如何工作的,让我们来玩一玩它。接下来的部分将带你游览一些你可以用来绘制简单图形的方法。在本章中,我们将展示如何用 Small Basic 创建图形,并且我们会加入网格线帮助你可视化每个形状中涉及的坐标。
绘制线条
要绘制一条线,你可以使用DrawLine()方法:
GraphicsWindow.DrawLine(x1, y1, x2, y2)
参数x1、y1和x2、y2是表示线条两个端点的 x 和 y 坐标。要使这个方法发挥作用,运行示例 3-1 中的程序,它绘制了两条平行线。
1 ' ParallelLines.sb
2 GraphicsWindow.Title = "Parallel Lines"
3 GraphicsWindow.DrawLine(40, 50, 100, 50) ' Top line
4 GraphicsWindow.DrawLine(40, 70, 100, 70) ' Bottom line
示例 3-1:绘制平行线
在第 3 行,Small Basic 从左上角开始,然后向右移动 40,再向下 50。接着,它绘制一条到达(100,50)终点的线。然后,在第 4 行,它跳到(40,70),并绘制第二条线到达(100,70)终点。每对终点使用相同的 x 坐标;不同的 y 坐标则将第二条线绘制在第一条线的下方。
做得很好!但是单独的线条并不太引人注目(除非它们是在迪士尼乐园的短线)。让我们使用几条不同的线条,像图 3-2 中那样画一艘帆船。

图 3-2:完全由线段绘制的帆船
这个形状由七个线段组成,你的程序包括七个DrawLine()语句。该程序的代码见示例 3-2,你可以在图 3-3 中看到它的输出。
1 ' SailBoat.sb
2 GraphicsWindow.Title = "SailBoat"
3 GraphicsWindow.DrawLine(10, 70, 130, 70) ' Top of the boat
4 GraphicsWindow.DrawLine(130, 70, 110, 90) ' Right side
5 GraphicsWindow.DrawLine(110, 90, 30, 90) ' Bottom of the boat
6 GraphicsWindow.DrawLine(30, 90, 10, 70) ' Left edge
7 GraphicsWindow.DrawLine(100, 70, 100, 10) ' Mast
8 GraphicsWindow.DrawLine(100, 10, 40, 50) ' Slanted sail edge
9 GraphicsWindow.DrawLine(40, 50, 100, 50) ' Bottom edge of sail
示例 3-2:用七条线绘制一艘船
恭喜你,你刚刚在 Small Basic 中绘制了你的第一幅画。你已经在成为一位伟大艺术家的路上了。

图 3-3: SailBoat.sb 的输出
绘制形状
你可以使用线条绘制许多酷炫的图形,但那会非常慢。通过使用内置的方法绘制几何图形,你可以简化代码,这也能节省你大量的时间!
三角形
使用DrawTriangle()和FillTriangle()方法绘制三角形:
GraphicsWindow.DrawTriangle(x1, y1, x2, y2, x3, y3)
GraphicsWindow.FillTriangle(x1, y1, x2, y2, x3, y3)
这些方法接受三角形三个角的 x 和 y 坐标。
DrawTriangle()方法绘制三角形的轮廓,而FillTriangle()方法则使用你为BrushColor属性设置的颜色填充三角形的内部。
提示
有关你可以在图形窗口中使用的完整颜色列表,请参见 tiny.cc/hexcolors/。
例如,要用蓝色填充一个三角形,使用以下两个语句:
GraphicsWindow.BrushColor = "Blue"
GraphicsWindow.FillTriangle(100, 10, 40, 50, 100, 50)
如果你想看到边框,可以添加对DrawTriangle()的调用:
GraphicsWindow.BrushColor = "Blue"
GraphicsWindow.FillTriangle(100, 10, 40, 50, 100, 50)
GraphicsWindow.DrawTriangle(100, 10, 40, 50, 100, 50)
尝试这些方法来绘制各种三角形。通过实验 3-1 检查你的理解。
实验 3-1
编写一个程序,绘制图 3-4 中显示的形状。(提示:先绘制四个蓝色三角形,再绘制四个黄色三角形。)

图 3-4:一个花式风车
矩形和正方形
使用DrawRectangle()和FillRectangle()方法,你可以绘制不同大小的矩形或正方形:
GraphicsWindow.DrawRectangle(x, y, width, height)
GraphicsWindow.FillRectangle(x, y, width, height)
在这两种方法中,前两个参数(x和y)是矩形左上角的坐标。第三个参数设置宽度,第四个参数设置高度。使用相同的数字作为第三和第四个参数来绘制正方形。
为了试验这些方法,让我们编写一个程序,绘制图 3-5 中显示的房子。

图 3-5:绘制房子
完整的程序显示在列表 3-3 中。
1 ' House.sb
2 GraphicsWindow.Title = "House"
3 GraphicsWindow.DrawRectangle(30, 50, 80, 40) ' Front of the house
4
5 GraphicsWindow.BrushColor = "Orange" ' Door is light orange
6 GraphicsWindow.FillRectangle(40, 60, 20, 30) ' Door
7 GraphicsWindow.DrawRectangle(40, 60, 20, 30) ' Door border
8
9 GraphicsWindow.BrushColor = "Lightblue" ' Window is light blue
10 GraphicsWindow.FillRectangle(80, 60, 20, 20) ' Window
11 GraphicsWindow.DrawRectangle(80, 60, 20, 20) ' Window border
12
13 GraphicsWindow.DrawRectangle(100, 20, 10, 30) ' Chimney
14
15 GraphicsWindow.BrushColor = "Gray" ' Roof is gray
16 GraphicsWindow.FillTriangle(30, 50, 70, 10, 110, 50) ' Roof
17 GraphicsWindow.DrawTriangle(30, 50, 70, 10, 110, 50) ' Roof border
列表 3-3:建造你梦想中的房子
图 3-6 显示了输出的效果。前面是一个矩形,其左上角位于(30, 50),宽度为 80,高度为 40(第 3 行)。门是一个填充的矩形,其左上角位于(40, 60),宽度为 20,高度为 30(第 6 行)。
窗户是一个填充的正方形,其左上角位于(80, 60),边长为 20(第 10 行)。屋顶是一个填充的三角形,其三个角点分别位于(30, 50)、(70, 10)和(110, 50)。

图 3-6: House.sb 的输出
烟囱也是一个矩形,其左上角位于(100, 20)。宽度为 10,高度为 30(第 13 行)。然而,这个矩形的一部分被屋顶覆盖,因此你需要先绘制烟囱,然后再在其上方绘制屋顶,遮住烟囱的底部。
现在你拥有了你梦想中的房子!
实验 3-2
现在你可以绘制线条、三角形、矩形和正方形,写一个程序绘制出 图 3-7 中的狐狸。并为其添加一些颜色。

图 3-7:绘制并上色狐狸
椭圆和圆形
GraphicsWindow 还具有绘制椭圆(椭圆形)和圆形的方法。这里有两个使用四个参数的椭圆方法:
GraphicsWindow.DrawEllipse(x, y, width, height)
GraphicsWindow.FillEllipse(x, y, width, height)
图 3-8 解释了这四个参数。前两个参数 x 和 y 设置椭圆的左上坐标。第三个参数 width 设置椭圆的宽度,第四个参数 height 设置椭圆的高度。要绘制一个圆形,只需将椭圆的宽度和高度设置为相同的值。

图 3-8:椭圆绘制方法的四个参数
要使用这些绘图方法,让我们编写一个程序,绘制出 图 3-9 中的面孔。

图 3-9:绘制面孔
要绘制面孔,你只需要绘制一个圆形和一些不同的椭圆,使用正确的参数。完整的程序请参见 列表 3-4。
1 ' Face.sb
2 GraphicsWindow.Title = "Face"
3
4 GraphicsWindow.BrushColor = "Yellow" ' Color of the two ears
5 GraphicsWindow.FillEllipse(20, 30, 10, 40) ' Left ear
6 GraphicsWindow.DrawEllipse(20, 30, 10, 40) ' Left ear border
7
8 GraphicsWindow.FillEllipse(100, 30, 10, 40) ' Right ear
9 GraphicsWindow.DrawEllipse(100, 30, 10, 40) ' Right ear border
10
11 GraphicsWindow.BrushColor = "Lime" ' Color of the two eyes
12 GraphicsWindow.FillEllipse(40, 30, 10, 10) ' Left eye
13 GraphicsWindow.DrawEllipse(40, 30, 10, 10) ' Left eye border
14
15 GraphicsWindow.FillEllipse(80, 30, 10, 10) ' Right eye
16 GraphicsWindow.DrawEllipse(80, 30, 10, 10) ' Right eye border
17
18 GraphicsWindow.BrushColor = "SandyBrown" ' Color of the nose
19 GraphicsWindow.FillEllipse(60, 40, 10, 20) ' Nose
20 GraphicsWindow.DrawEllipse(60, 40, 10, 20) ' Nose border
21
22 GraphicsWindow.BrushColor = "LightCyan" ' Color of the mouth
23 GraphicsWindow.FillEllipse(50, 65, 30, 10) ' Mouth
24 GraphicsWindow.DrawEllipse(50, 65, 30, 10) ' Mouth border
25
26 GraphicsWindow.DrawEllipse(30, 10, 70, 70) ' Face border
列表 3-4:绘制史上最酷的椭圆面孔
这个程序的输出如 图 3-10 所示。图中的所有椭圆都使用相同的笔宽和颜色,但你可以通过改变这些属性,给你的 Small Basic 绘图添加更多细节。让我们看看怎么做。

图 3-10: Face.sb 的输出
笔的大小和颜色
要改变笔的大小和颜色,可以在绘制线条或形状之前设置以下属性:
GraphicsWindow.PenWidth = 20 ' Sets line width
GraphicsWindow.PenColor = "Green" ' Sets line color
如果你想让程序每次运行时都有不同的效果,可以通过使用 GraphicsWindow 对象的 GetRandomColor() 方法每次改变笔的颜色。看看这个:
GraphicsWindow.PenColor = GraphicsWindow.GetRandomColor()
这个方法没有任何参数,这就是为什么 GetRandomColor() 方法的括号里什么也没有;它返回一个随机选定的颜色。试试看吧!
笔宽和形状大小
在绘制三角形、矩形和椭圆时,你使用的笔宽会影响形状的大小。列表 3-5 显示了我们的意思。
1 ' PenWidthDemo.sb
2 GraphicsWindow.Title = "Pen Width Demo"
3
4 GraphicsWindow.PenWidth = 20 ' Width of circle
5 GraphicsWindow.PenColor = "Lime" ' Color of circle
6 GraphicsWindow.DrawEllipse(20, 20, 100, 100) ' Circle border
7
8 GraphicsWindow.PenWidth = 1 ' Width of square
9 GraphicsWindow.PenColor = "Black" ' Color of square
10 GraphicsWindow.DrawRectangle(20, 20, 100, 100) ' Square border
列表 3-5:使用笔宽改变形状的大小
这个程序使用宽度为 20 的笔绘制圆形的边框。图 3-11 显示了边框从正方形的周围延伸出 10 像素,尽管圆形和正方形具有相同的尺寸。当测量外部边缘时,输出圆形的直径为 120 像素,而不是指定的 100 像素。

图 3-11: PenWidthDemo.sb 的输出
试一试 3-3
编写一个程序,绘制如图 3-12 所示的自行车。(提示:使用给定的网格线来确定不同形状的坐标,以便更容易编写代码。)

图 3-12:绘制一辆自行车
绘制文本
一张图片可能胜过千言万语,但你也可以像这样在图形窗口中绘制文本:
GraphicsWindow.DrawText(x, y, "text")
DrawText()接受三个参数。前两个参数设置文本左上角的 x 和 y 坐标,第三个参数接受你要绘制的文本(或数字)字符串。记得将字符串放在引号内。
如果你想更改文本的显示方式,请使用表 3-1 中GraphicsWindow对象的属性。
表 3-1: 字体名称、大小、样式和颜色的GraphicsWindow属性
| 属性 | 默认值 | 描述 |
|---|---|---|
FontName |
"Tahoma" |
字体名称 |
FontSize |
12 |
字体大小 |
FontBold |
"True" |
字体是否加粗 |
FontItalic |
"False" |
字体是否斜体 |
BrushColor |
"SlateBlue" |
绘制文本的画笔颜色 |
如果你没有更改这些属性,Small Basic 将使用表 3-1 中列出的默认值。列表 3-6 中的程序更改了这些属性来绘制一些华丽的文本。
1 ' Fonts.sb
2 GraphicsWindow.Title = "Fonts"
3 GraphicsWindow.BackgroundColor = "LightYellow"
4 GraphicsWindow.FontName = "Times New Roman"
5 GraphicsWindow.FontSize = 120
6 GraphicsWindow.FontItalic = "True"
7
8 GraphicsWindow.BrushColor = "Silver" ' Text shadow color
9 GraphicsWindow.DrawText(5, 5, "Hello!") ' Shadow position/text
10
11 GraphicsWindow.BrushColor = "RosyBrown" ' Text color
12 GraphicsWindow.DrawText(0, 0, "Hello!") ' Position and text
列表 3-6:尝试一些字体
在第 3 行,BackgroundColor属性更改图形窗口的背景颜色。第 4 到 6 行设置在任何调用DrawText()时使用的字体的名称、大小和斜体属性。第 8 行使用BrushColor属性设置字体颜色,第 9 行从点(5, 5)开始绘制字符串"Hello!"。这一行绘制了你在图 3-13 中看到的背景阴影。在第 11 行,程序更改了BrushColor属性,然后第 12 行在略微不同的位置绘制了相同的字符串。这创建了带有背景阴影的文本效果,如图 3-13 所示。

图 3-13: Fonts.sb 的输出
通过将文本层叠在其他文本上方,你可以创造一些很酷的效果。试着玩玩这段代码,看看你能做出什么!
你还可以通过使用DrawBoundText()方法将文本绘制为适应特定宽度:
GraphicsWindow.DrawBoundText(x, y, width, "text")
参数 x、y 和 "text" 的含义与 DrawText() 方法中的相同:x 和 y 是你开始绘制的位置,而 "text" 是要绘制的文本或数字字符串。第三个参数 width 告诉 Small Basic 输出中文本的最大可用宽度。如果文本超出了给定的宽度,它将继续换行。尽管文本所显示的矩形区域宽度是固定的,但文本会继续显示,因此矩形文本区域会根据需要垂直扩展。但是,如果一个单词太长,无法适应你定义的边界矩形(尤其是在字体过大的情况下),那么它会被截断!列表 3-7 中的程序和 图 3-14 中的输出会向你展示我们所说的意思。
1 ' BoundTextDemo.sb
2 GraphicsWindow.Title = "DrawBoundText Demo"
3
4 ' No clipping
5 GraphicsWindow.FontSize = 15 ' Smaller font
6 GraphicsWindow.DrawBoundText(10, 10, 70, "Today is my birthday")
7
8 ' With clipping
9 GraphicsWindow.FontSize = 18 ' Larger font
10 GraphicsWindow.DrawBoundText(150, 10, 70, "Today is my birthday")
11 GraphicsWindow.DrawRectangle(150, 10, 70, 80)
列表 3-7:包含文本的边界
左侧不可见矩形中的文本会自动换行,以确保文本不会超出你指定的宽度。在右侧的边界矩形中,文本被截断,因为它太长无法适应。Small Basic 会显示三个点,即省略号,表示文本已经被截断。

图 3-14:BoundTextDemo.sb* 的输出*
插入图像
有些图像可能太复杂,无法用基本形状绘制,或者它们可能需要花费太长时间来编码。相反,你可以提前用画图程序绘制这些图像,然后在你的应用程序中使用它们。GraphicsWindow 提供了两种方法来插入图像。尽管这些方法的名称以Draw开头,但它们实际上是在图形窗口中插入一个已有的图像:
GraphicsWindow.DrawImage(imageName, x, y)
GraphicsWindow.DrawResizedImage(imageName, x, y, width, height)
这两种方法都需要图像路径名以及 x 和 y 坐标来确定图像在图形窗口中的位置。DrawResizedImage() 方法额外接受两个参数(width 和 height),让你能够调整输入图像的大小。
列表 3-8 展示了带有示例图像的 DrawImage() 方法。
1 ' ImageDemo.sb
2 GraphicsWindow.Title = "Image Demo"
3 GraphicsWindow.Width = 320 ' Same width as background image
4 GraphicsWindow.Height = 240 ' Same height as image
5 GraphicsWindow.DrawImage("C:\Small Basic\Ch03\BkGnd.bmp", 0, 0)
6
7 GraphicsWindow.BrushColor = "White" ' Text color
8 GraphicsWindow.FontSize = 50
9 GraphicsWindow.DrawText(10, 120, "Hello Moon!")
列表 3-8:插入你的第一张图像
程序开始时设置了 GraphicsWindow 的宽度和高度,分别为 320 和 240 像素,以匹配图像的大小。第 5 行调用了 DrawImage() 并传入了图像保存的路径名。在第 7 到第 9 行,程序在背景图像上绘制了白色文本 Hello Moon!。当你在电脑上运行这个程序时,确保将第 5 行中的路径设置为你电脑上 BkGnd.bmp 文件的正确位置。图 3-15 显示了输出结果。

图 3-15:ImageDemo.sb* 的输出*
注意
Small Basic 还可以从网络上绘制图像。以下是一个示例:
GraphicsWindow.DrawImage("http://smallbasic.com/bkgnd.jpg", 0, 0)
编程挑战
如果你遇到困难,可以访问 nostarch.com/smallbasic/ 查找解决方案,并获取更多的资源和针对教师和学生的复习题目。
-
编写一个程序,用线段连接以下六个点:(20, 110),(110, 50),(10, 50),(100, 110),(60, 20),(20, 110)。你得到的是什么形状?
-
以下程序的输出是什么?
GraphicsWindow.DrawLine(50, 18, 61, 37) GraphicsWindow.DrawLine(61, 37, 83, 43) GraphicsWindow.DrawLine(83, 43, 69, 60) GraphicsWindow.DrawLine(69, 60, 71, 82) GraphicsWindow.DrawLine(71, 82, 50, 73) GraphicsWindow.DrawLine(50, 73, 29, 82) GraphicsWindow.DrawLine(29, 82, 31, 60) GraphicsWindow.DrawLine(31, 60, 17, 43) GraphicsWindow.DrawLine(17, 43, 39, 37) GraphicsWindow.DrawLine(39, 37, 50, 18) -
以下程序的输出是什么?
GraphicsWindow.DrawRectangle(10, 10, 90, 50) GraphicsWindow.DrawRectangle(15, 60, 75, 4) GraphicsWindow.DrawRectangle(34, 64, 6, 6) GraphicsWindow.DrawRectangle(74, 64, 6, 6) GraphicsWindow.DrawRectangle(30, 70, 75, 10) GraphicsWindow.DrawRectangle(20, 80, 80, 2) -
你家里最奇怪的东西是什么?用
DrawLine()方法画出来。
以下问题显示了一个网格,以便你更容易绘制形状。你可以选择任何大小的网格。我们推荐使用 20 像素。
-
编写一个程序,绘制这颗星星。
![image]()
-
编写一个程序,绘制这座银行,使用你喜欢的任何颜色。
![image]()
-
编写一个程序,绘制这辆卡车。额外加分,添加前轮。
![image]()
-
编写一个程序,绘制像这样的交通信号灯。
![image]()
-
编写一个程序,绘制像这样的火车。
![image]()
-
编写一个程序,绘制以下形状。
![image]()
-
编写一个程序,绘制这个人。
![image]()
-
编写一个程序,绘制一个类似这样的足球场。
![image]()
第五章:4
使用变量

你曾经想过成为医生、宇航员、消防员、动画师、市长、植物学家,或者忍者吗?如果你能成为它们所有的,但只能一次做一件事呢?一个变量可以是你程序中的任何东西,比如字符串或数字,但它一次只能是其中的一样。例如,一个猜数字游戏可能会要求玩家输入他们的名字(字符串),然后用这个名字问候玩家。接下来,程序可能会要求玩家输入他们的第一次猜测(数字),然后检查是否猜对了。
为了使游戏正常运行,它必须记住,或者存储玩家输入的数据。它可能还需要跟踪玩家猜出秘密数字用了多少轮。你可以通过使用变量来实现这一切!
在这一章中,你将学会如何定义变量,并使用它们存储文本或数字。然后你将使用变量来编写解决实际问题的程序。
什么是变量?
变量用于存储值,比如数字或文本。你可以像用一个藏宝箱来存放宝贵物品一样使用它。如果你是海盗的话,可以把变量当作一个箱子。你可以把不同的东西放进你的箱子里,以后使用。你可以更改变量的值,就像把新东西放进箱子里一样,比如你收集的口香糖。变量之所以叫变量,是因为它们的内容可以变化。
在 Small Basic 中创建一个变量,可以使用像这样的赋值语句:
treasureChest = "My booty!"
这条语句创建了变量 treasureChest 并赋值为字符串 "My booty!" 等号是一个赋值运算符。第一次给变量赋值被称为初始化变量。
现在让我们探索如何在你的程序中使用变量。锚链起航!
使用变量的基础
Karen 有 12 个毛绒熊。她的姐姐 Linda 有一半数量。Linda 有多少只熊?你可以用脑袋算或者用计算器算,但我们来按照清单 4-1 以 Small Basic 的方式解决这个问题!
1 ' Variables.sb
2 karenBears = 12 ' Karen has 12 bears
3 lindaBears = karenBears / 2 ' Linda has half as many bears as Karen
4
5 TextWindow.Write("Karen's " + karenBears + " bears aren't as ")
6 TextWindow.WriteLine("fancy as Linda's " + lindabears + " bears!")
清单 4-1:演示变量
第 2 行的语句 karenBears = 12 创建了一个名为 karenBears 的变量,并为其赋值 12。第 3 行将 karenBears 除以 2,然后将结果存储到一个名为 lindaBears 的新变量中。当第 3 行运行时,lindaBears 保存的值是 12 ÷ 2,即 6。
第 5 行输出字符串 "Karen's ",然后加号将 karenBears 的值(即 12)连接到这个字符串后面。接着,它将 " bears aren't as " 连接到 12 后面。(关于字符串连接的复习,请参见第 18 页的“连接字符串”)同样,第 6 行输出字符串 "fancy as Linda's ",然后是存储在变量 lindaBears 中的值。最后,它添加了 " bears!"。
现在,让我们运行程序来查看结果。你应该看到这个:
Karen's 12 bears aren't as fancy as Linda's 6 bears!
尝试更改karenBears的值并重新运行程序,看看会发生什么。不错吧?更改karenBears的值也会改变lindaBears的值。变量可以让你的编程生活变得更加轻松!
让我们看看一些你在使用自己变量时需要了解的其他重要概念。
将表达式赋值给变量
算术表达式是变量、运算符和数字的组合。它们可以是常量数字(例如3、6.8或–10)、算术运算(例如3 + 6或10 / 3),或者代数表达式(例如karenBears / 2)。在 Small Basic 中计算算术表达式就像在数学中计算一个表达式。例如,表达式4 * 3 + 6 / 2的计算过程为(4 × 3 + 6 ÷ 2)= 12 + 3 = 15。你还可以在表达式中使用括号来决定运算的顺序。
你可以通过赋值语句将变量设置为算术表达式的结果。你的程序会抓取等号右边的值,并将这个值赋给等号左边的变量。你已经在示例 4-1 中做过了,让我们在这个基础上继续扩展,编写更多将算术表达式赋值给变量的代码!
这是一个示例:
barbies = 5 ' You have 5 Barbies
ponies = barbies + 7 ' You have 7 more My Little Ponies than Barbies
donate = (barbies * ponies)/ 10 ' Total toys you need to donate
当你运行这个程序时,变量barbies、ponies和donate的值分别是 5、12 和 6。是时候捐赠 6 个玩具了!
你需要设置赋值运算符左侧的变量,正如你在每个赋值示例中看到的那样。所以这个语句是错误的:
5 = barbies ' This is backwards, like the Twilight Zone
尝试自己运行一下,看看是否会出现错误!
传递变量给方法
一个方法的参数可以是常量、变量,甚至是表达式。例如,下面语句中WriteLine()的参数是一个算术表达式:
TextWindow.WriteLine((3 * x + y) / (x - y))
如果x = 7且y = 5,此语句将在屏幕上显示 13。看看你是否能弄明白如何编写并运行这段代码。记得先设置x和y!
更改变量的值
当你在程序中创建一个新变量时,你使用赋值运算符为其赋予初始值。你也可以使用赋值运算符来更改现有变量的值,这将清除旧的值,如下所示:
ferrets = 5
ferrets = 15
TextWindow.WriteLine("There are " + ferrets + " ferrets in my bed!")
这个示例中的第一行创建了一个名为ferrets的新变量,并将 5 赋值给它,但第二行将其值更改为 15。因此,WriteLine语句将输出There are 15 ferrets in my bed!
在幕后,变量ferrets指向计算机内存中的一个存储区域。当你写ferrets = 15时,你告诉 Small Basic 进入为ferrets预留的内存空间,并将 5 替换为 15。每当你显示ferrets的值时,你就会获取当前存储在那个空间里的值。
你也可以通过给变量增加值来改变它的值。假设你正在编写一个游戏,玩家需要射击来袭的飞机。当玩家击落一架飞机时,你想要把玩家的分数(存储在名为score的变量中)增加五分。你如何更新score变量?下面是一种方式:
score = 10 ' Assumes the player already has 10 points
temp = score + 5 ' temp = 10 + 5 (= 15)
score = temp ' Now the player has 15 points
第二行使用一个临时变量temp来存储将 5 加到当前score值的结果。然后将temp的值赋给score。
但你可以在一条语句中更快地完成这件事:
score = score + 5
你是否看到相同的变量score出现在赋值语句的两边?这条语句将 5 加到变量score的当前值,并将结果重新存储回同一个变量中。旧值 10 会被新值 15 替换。参见图 4-1。

图 4-1:说明语句 score = score + 5
更新score变量的这两种方式本质上是相同的,但第二种方法更常见,你将在其他人的程序中经常看到它。
使用空格提高可读性
添加制表符和空格使表达式更具可读性。例如,看看这两个表达式:
x=y+10*(x-3)-z
x = y + 10 * (x - 3) - z
它们对 Small Basic 来说是相同的,但第二行中的空格使得人类(和其他非机器人)更容易阅读。
尝试练习 4-1
以下程序计算你在两门课程中每周花费的作业时间的平均值。首先,识别出程序中的所有变量。当你运行它时,这个程序会显示什么?试试看!
mathHours = 8
scienceHours = 6
avgHours = (mathHours + scienceHours) / 2
TextWindow.Write("I spend " + mathHours)
TextWindow.Write(" hours a week on math homework and " + scienceHours)
TextWindow.WriteLine(" hours a week on science homework.")
TextWindow.Write("The average of " + mathHours + " and ")
TextWindow.WriteLine(scienceHours + " is " + avgHours + ".")
现在,替换掉你在两门课程中每周花费的实际时间。你每周花多少时间做作业?把结果展示给你的父母看看——在你讨论成绩单之前,这可能是一个很好的预习!
命名变量的规则
你为变量命名的名称称为标识符。变量名可以包含字母(小写和大写)、数字(0 到 9)以及下划线字符(_)。你可以为变量取任何名字,只要遵循几个规则:
-
变量名必须以字母或下划线开头,不能以数字开头。
-
不要使用 Small Basic 的任何关键字,例如
If、Else、Then、And或While。这些关键字有特殊的含义,不能用于其他用途。你将在本书后面学到更多关于它们的内容。 -
Small Basic 中的变量名不区分大小写;
side、SIDE和siDE是相同的变量。
根据这些规则,MyAddress、totalScore、player1、sum、room_temperature 和 _x123 都是有效的变量名。
除了我们提到的规则之外,程序员在命名变量时还会使用其他准则。这些约定是良好的编程实践,你也应该遵循它们。让我们来看看这些额外的约定。
说清楚你的意思
尽管你可以随意命名变量,但我们建议你选择一个能解释变量用途的名称。例如,使用名为address的变量来存储一个人的地址比使用xy123或TacoTruck更有意义。
找到合适的长度
避免使用单个字母的变量名,如m、j、x和w,除非它们的含义非常明确,否则你会让程序变得更难阅读和理解。但也不要使用过长的变量名,避免让你的朋友打瞌睡!
选择简短且有意义的名称。例如,使用name或presidentName,而不是the_name_of_the_president。
坚持你的风格
目前流行的做法是将变量名以小写字母开头,然后将每个单词的首字母大写,例如sideLength、firstName、roomTemp、variablesAreAwesome和scoobyDoo。这种命名风格叫做驼峰命名法,因为它中间有一个小“驼峰”。但不用担心——驼峰命名的变量不会“喷”出来!
本书使用的是驼峰命名法,但如果你更喜欢其他风格,没关系。只要你选择一个命名规则并坚持使用!尽管 Small Basic 在变量名方面不区分大小写,但命名时应该保持一致。如果你命名一个变量为firstName,那么在程序中其他地方也要保持相同的大小写。这会让你更容易找到变量,并且让其他人更容易理解你的代码。IntelliSense 自动完成功能可以帮助你。让我们看看如何做到。
让 IntelliSense 为你工作
当你在程序中创建一个变量时,该变量的名称会被添加到 IntelliSense 下拉菜单中。当你想要重用一个变量(或检查其大小写)时,只需输入前几个字母并在 IntelliSense 中查找它,如图 4-2 所示。Small Basic 会自动完成你的变量名,就像一个总是替你完成三明治的好朋友!

图 4-2:如何将一个变量添加到 IntelliSense 菜单中
请注意,第一行中创建的变量名(interestRate)如何出现在菜单中。当我在第二行开始输入in时,IDE 会高亮显示该变量名。按下 ENTER 键即可自动完成我开始输入的内容。谢谢你,IntelliSense!
避免将变量命名为方法和对象的名称
方法名不是保留关键字,因此你可以将它们用作变量名。例如,如果你写下以下代码,Small Basic 并不会报错:
writeline = 5
TextWindow.WriteLine(writeline)
虽然这是有效的,但我们强烈建议你不要将变量命名为现有方法的名称。世界已经足够混乱了。
动手试试 4-2
-
以下哪些变量名是无效的?如果名称无效,请解释原因。
_myBooK 1MoreRound $FinalScore Level2 -
对于以下每个值,你会如何命名表示它的变量,为什么?
• 游戏中玩家的得分
• 直角三角形的斜边
• 一栋楼的楼层数
• 一辆车每加仑油能行驶的英里数
• 得到 Tutsi Pop 中心的舔数
简化表达式
变量可以使算术表达式的计算更容易。假设你想编写一个程序来计算这个表达式:

你可以编写许多不同的程序,这些程序都会给你正确的答案。例如,你可以写一个语句一次性计算整个表达式:
TextWindow.WriteLine((1 / 5 + 5 / 7) / (7 / 8 - 2 / 3))
或者你可能单独计算分子和分母,然后显示它们除法的结果:
num = (1 / 5) + (5 / 7) ' Finds the numerator
den = (7 / 8) - (2 / 3) ' Finds the denominator
TextWindow.WriteLine(num / den) ' Does the division
你也可以单独计算每个分数,然后显示合并表达式的结果:
a = 1 / 5
b = 5 / 7
c = 7 / 8
d = 2 / 3
answer = (a + b) / (c - d)
TextWindow.WriteLine(answer)
尽管这三个程序给出的答案相同,但每个程序的风格都不同。第一个程序是编程中的“胖兔子”:它将所有内容塞进一个语句中。如果原始表达式更复杂,语句就会变得很难跟随。第三个程序则正好相反:它用变量表示表达式中的每个分数,这样也可能难以阅读。
如果你是金发姑娘,那么第二种解决方案就是你认为的“恰到好处”。它将表达式分解为足够的部分,使程序更容易理解。变量num和den清晰地表示了分子和分母。
正如这些例子所示,通常有不止一种方法可以解决问题。如果你卡住了,试着将问题分解成更容易处理的部分!
尝试一下 4-3
写一个程序来计算以下表达式的值。试试几种不同的方法。试着在跳舞的时候做!别害羞!

使用变量解决问题
人们常常在没有真正考虑每一步的情况下解决问题。但计算机无法这样做:它们需要你为它们考虑每一步(至少直到《终结者》或《黑客帝国》成为现实)。这就是为什么使用计算机解决问题需要一些规划的原因。开发程序解决方案时,你应该这样做:
-
明白问题是什么。
-
设计解决方案。
-
编写程序。
-
测试程序,确保它按预期工作。
假设你想创建一个程序来计算给定半径的圆的面积。要解决这个问题,你需要回答以下基本问题:
-
你需要程序输出什么?
-
你需要什么输入,程序将从哪里获得这些输入?
-
程序需要进行哪些处理才能将输入转换为输出?
对于这个问题,以下是你如何回答这些问题:
-
你的程序需要输出圆的面积。它将在文本窗口中向用户显示这个输出。
-
这个程序只需要一个用户输入:圆的半径。
-
你的程序需要使用这个公式来计算面积,这个公式在数学课上可能很熟悉:
area = π × (radius)²
注意
希腊字母 π,发音 pi,是一个特殊的数字,可以四舍五入为 3.1416。我们免费赠送了π给你!
现在你已经定义了问题,让我们设计一个逐步的解决方案,这叫做算法。首先,我们将把问题的每一部分(输入、处理和输出)分解为详细的步骤。
养成编写程序大纲的习惯,将每个步骤按顺序列出。这个大纲叫做伪代码,因为它不是一个真正的程序,但它解释了代码应该做什么。查看你的伪代码应该能清楚地告诉你每个步骤中需要写的真实代码。对于复杂的问题,伪代码帮助你以简单的方式表达你的思维过程,之后可以将其转化为真实代码。图 4-3 展示了我们的圆面积问题的算法和伪代码。

图 4-3:计算圆面积的算法和伪代码
最后的步骤是将你的伪代码转化为一个 Small Basic 程序。由于我们还没有向你展示如何从用户那里获取值,你将使用一个固定的半径值。请在清单 4-2 中输入程序。
1 ' CircleArea.sb
2 radius = 5
3 area = 3.1416 * radius * radius
4 TextWindow.WriteLine("Area = " + area)
清单 4-2:计算圆的面积
在第 2 行,你创建了一个名为radius的变量,并为其赋值 5。在第 3 行,你使用第 51 页的公式计算圆的面积,并将结果赋值给一个名为area的新变量。在第 4 行,你在文本"Area = "后显示变量area的值,以便明确显示的数字表示什么。
当你运行代码时,你会看到这个结果:
Area = 78.5400
试试看 4-4
编写一个程序,计算并显示半径为 5 单位的圆的周长。我们给你一个提示:圆的周长公式是 2 × π × 半径。
两种数据类型
程序使用各种不同类型的数据:做计算的应用程序使用数字,但其他应用程序可能使用文本。
Small Basic 使用非常简单的数据类型。它仅内置支持两种数据类型:数字和字符串。数字可以是整数,如-2 和 2365,或者是小数,如 0.25 和-123.78。正如你所知,字符串是由双引号括起来的一系列字符,如"独立宣言"或"她在海边卖海贝壳。"
你不需要告诉 Small Basic 你将存储什么类型的数据在一个变量中。这是面向初学者的编程方式。
全局变量
Small Basic 中的变量是全局作用域的。这意味着你可以在程序的任何地方定义和访问变量。这个特性很有帮助,因为它允许你在需要时定义变量(而不是必须把所有变量放在程序的顶部)。但你必须小心!因为 Small Basic 按顺序读取你的程序,可能会导致逻辑错误。试试清单 4-3 中的程序。
1 ' LogicError.sb
2 y = x / 10
3 x = 20
4 TextWindow.WriteLine("y = " + y)
清单 4-3:全局变量的逻辑错误
如果你仔细阅读这段代码,你已经注意到,在第 2 行中,我们在给变量x赋值之前就使用了它。当我们运行这段代码时,得到如下输出:
y = 0
这个答案让你感到惊讶吗?这里发生了什么?当 Small Basic 编译器读取这段代码时,它首先列出程序中定义的所有变量。这些变量的初始值保持为空,就像空框一样。只要程序中的表达式使用的是该列表中的变量名,编译器就会很高兴,程序会正常运行。
当程序运行到第 2 行的表达式x / 10时,它将x解释为一个数字,因为除法仅对数字有意义。这就是为什么它会用 0 填充x的空框。然后,它将除法的结果(即 0)赋值给y。变量x在第 3 行被更改为 20,但为时已晚!你得到的是错误的输出。
注意
大多数其他编程语言会报告语法错误,因为第 2 行中的表达式x / 10使用了一个尚未在程序中定义的变量x。Small Basic 允许你在程序中的任何位置定义变量;不过,别指望在其他语言中也能这样做。
在编写语句时要小心,因为 Small Basic 是从上到下执行的。确保在第一次使用变量之前先定义它们。
动手试试 4-5
这个程序的输出是什么?解释你看到的结果。
TextWindow.WriteLine("Before: x = " + x + " and y = " + y)
x = 10
y = 10
TextWindow.WriteLine("After: x = " + x + " and y = " + y)
编程挑战
如果你卡住了,访问 nostarch.com/smallbasic/ 查找解决方案以及更多的资源和复习问题,适用于教师和学生。
-
你喜欢敲敲门笑话吗?试试下面的程序,看看如何在 Small Basic 中讲这些笑话!你需要更改哪些程序行才能讲一个不同的笑话?做出更改并重新运行程序,看看会发生什么:
' KnockKnock.sb ' Small Basic can tell knock-knock jokes! name = "Orange" reply = "you going to answer the door?" TextWindow.WriteLine("Knock Knock") TextWindow.WriteLine("Who's there?") TextWindow.WriteLine(name) TextWindow.WriteLine(name + " who?") TextWindow.WriteLine(name + " " + reply) TextWindow.WriteLine("") -
将以下伪代码翻译成一个 Small Basic 程序:
a. 将数量设为 10。
b. 将商品价格设为 15 美元。
c. 通过将数量乘以商品价格来计算总价格。
d. 显示总价格。
第六章:5
用海龟图形绘制形状

在第三章中,你学会了如何使用代码绘制图形,但在 Small Basic 中,你可以编程让一只友好的海龟为你绘画!在本章中,你将探索 Turtle 对象。你还将学习如何使用 For 循环重复多次代码行来绘制美丽的设计。
认识海龟
在 Small Basic 编辑器中输入以下语句:
Turtle.Show()
然后点击 运行。瞧!海龟应该出现在图形窗口的中央(图 5-1),等待你的指令。

图 5-1:你好,海龟!
你可能会想,这个缓慢的生物能有多大用处,但不要低估它的能力。曾几何时,一只海龟赢得了与地球上最快的兔子的比赛!
Small Basic 的海龟使用 GraphicsWindow 对象的笔来绘制线条。它总是带着这支笔(也许它藏在它的壳里,旁边是海龟蜡),你可以决定笔是上还是下!当笔在下时,海龟在移动时会画出轨迹;当笔在上时,海龟移动时不会留下痕迹。你可以使用 PenDown() 和 PenUp() 方法命令海龟放下或抬起它的笔(参见图 5-2)。

图 5-2:演示 PenUp() 和 PenDown() 方法
默认的笔状态是下,海龟从出生那天起就准备好绘画了。现在让我们探索它能做什么。
移动海龟
你可以输入命令告诉海龟该做什么。就像小智指挥比卡丘一样,你也可以指挥你的海龟。首先,让我们使用 Turtle 对象来告诉海龟移动!
在编辑器中输入这些代码行来推动它。然后点击 运行。
Turtle.Show()
Turtle.Move(100)
去吧,海龟,去吧!这个例子中的 Move() 方法命令海龟向前移动 100 像素。
现在让我们看看移动海龟的两种不同方式:绝对运动和相对运动。
绝对运动
使用绝对运动,你告诉海龟去图形窗口中的某个点。无论海龟在哪里,它都会移动到你选择的准确位置。
将海龟移动到图形窗口中的某个特定位置的一种方法是改变它的 X 和 Y 属性。要查看如何实现,请运行清单 5-1 中的程序。
1 ' SetTurtle.sb
2 Turtle.Show()
3 Program.Delay(1000)
4 Turtle.X = 100
5 Turtle.Y = 140
清单 5-1:设置海龟的位置
Show()方法(第 2 行)使海龟出现在图形窗口的中心附近(320, 240)。第 3 行的Delay()方法使程序暂停 1,000 毫秒(即 1 秒),这样你可以看到海龟的初始位置。第 4 行将海龟的X位置设置为 100,第 5 行将海龟的Y位置设置为 140。运行第 4 和第 5 行后,海龟将出现在图形窗口的 (100, 140) 位置,如图 5-3 所示。注意,海龟在移动到新位置时没有留下任何痕迹;就像海龟被捡起并放置到 (100, 140) 一样。

图 5-3:通过设置海龟的X和Y属性来移动海龟
另一种将海龟移动到图形窗口中绝对位置的方法是使用MoveTo()方法。此方法将目标位置的 x 和 y 坐标作为参数。运行示例 5-2 中的程序,查看该方法的效果。
1 ' MoveTo.sb
2 Turtle.Show()
3 Program.Delay(1000)
4 Turtle.MoveTo(100, 140)
示例 5-2:使用绝对运动移动海龟
你可以在图 5-4 中看到此程序的输出。再次强调,海龟从 (320, 240) 开始,朝北(第 2 行),程序暂停 1 秒以便你观察海龟的动作(第 3 行)。1 秒后,海龟转向 (100, 140),然后开始缓慢地朝该点移动。这一次,海龟在移动时会留下轨迹(因为海龟的笔默认是放下的)。如果你在调用MoveTo()之前的任何地方添加Turtle.PenUp(),海龟将移动到 (100, 140) 而不留下任何痕迹。

图 5-4:使用MoveTo()设置海龟的绝对位置
请注意,当它停止移动时,海龟保持朝向它转向的方向。它不会重新设置为朝北。比较此图与图 5-3,在图 5-3 中海龟仍然朝北,就像它被捡起并移动到新位置一样。
假设你希望海龟在完成它的旅程后面朝北方。在示例 5-2 的末尾添加以下语句:
Turtle.Angle = 0
当海龟到达 (100, 140) 时,它将原地转向朝北。试试看!参见图 5-5,了解Angle属性与海龟朝向之间的关系。

图 5-5:对于海龟,0 表示北方,90 表示东方,180 表示南方,270 表示西方。
正如你在图 5-5 中看到的,当你将海龟的Angle设置为 0 或 360 时,它面朝北方。你可以将海龟的Angle设置为 45,使其面朝东北;90,面朝东;135(东南);180(南);225(西南);270(西);315(西北);以及 360(再次朝北)。当然,你也可以将海龟的Angle设置为任何你想要的数字。通过将Turtle对象的Angle属性设置为不同的数字,尝试看看海龟会朝向哪些方向。别忘了尝试负数。
相对运动
使用相对运动时,你告诉海龟从当前位置移动多远;也就是说,你告诉它相对于当前位置移动多远。
让我们练习一下,让海龟击中一个假想的目标。清单 5-3 展示了编程海龟击中目标的一种方法。
1 ' RelativeMotion.sb
2 Turtle.Show()
3 Turtle.Move(150)
4 Turtle.TurnRight()
5 Turtle.Move(100)
清单 5-3:使用相对运动移动海龟
输出结果如图 5-6 所示。第 3 行将海龟向上移动 150 像素,第 4 行将海龟右转,第 5 行让海龟前进 100 像素。

图 5-6:使用相对运动命令移动海龟
相对运动与绝对运动的区别在于,我们告诉海龟移动一定的距离,而不是告诉它去一个特定的坐标。
当你向Move()传递一个负数时,海龟会向后移动。你也可以使用Turn()方法命令海龟在原地按你想要的角度转动。输入清单 5-4 中的代码来尝试这些选项,然后运行程序看看实际效果。
1 ' Turn.sb
2 Turtle.Show()
3 Turtle.Turn(45)
4 Turtle.Move(100)
5 Turtle.Turn(-90)
6 Turtle.Move(-100)
清单 5-4:使用相对运动转动海龟
第 3 行将海龟右转 45 度。第 4 行让海龟前进 100 像素(见图 5-7 左图)。第 5 行的–90 将海龟左转 90 度。第 6 行让海龟后退 100 像素(见图 5-7 右图)。

图 5-7:说明海龟的 Move() 和 Turn() 方法
为你的步骤上色
你可以通过GraphicsWindow的PenWidth和PenColor属性来设置海龟的画笔大小和颜色。例如,以下代码让海龟用一个宽度为 5 像素的红色画笔绘制。
GraphicsWindow.PenColor = "Red"
GraphicsWindow.PenWidth = 5
在命令海龟移动之前,先添加这段代码,然后观察会发生什么。
控制你的速度
Turtle对象还有一个你需要知道的属性。Speed属性设置海龟的移动速度。Speed的值范围是 1 到 10。跟着清单 5-5 来看看海龟如何在屏幕上飞速移动。
1 ' TurtleSpeed.sb
2 Turtle.Show()
3 Turtle.Speed = 2 ' Sets the initial speed to 2
4 Turtle.Move(100) ' Moves the turtle forward 100 pixels
5 Turtle.Speed = 5 ' Changes the speed to 5
6 Turtle.TurnRight() ' Turns the turtle to its right
7 Turtle.Move(100)
8 Turtle.Speed = 9 ' Changes the speed to 9
9 Turtle.TurnRight()
10 Turtle.Move(100)
清单 5-5:设置海龟的速度
第 3 行将乌龟的速度设置为 2。乌龟缓慢地移动 100 像素(第 4 行),然后在第 5 行加速。你可以看到乌龟转向右侧(第 6 行)并快速前进 100 像素(第 7 行)。接下来,你将乌龟的速度设置为 9(第 8 行)。乌龟快速向右转(第 9 行),并冲刺前进另 100 像素(第 10 行)。如果你不想在绘制时看到乌龟慢慢移动,可以在程序开始时将 Speed 属性设置为 10。乌龟将以非常快的速度移动,你几乎看不见它。它是超级乌龟!
动手试一试 5-1
编写一个程序,让你的乌龟画出这个星形图案(图 5-8)。每个点的坐标已包含在内。

图 5-8:一个星形图案
介绍 For 循环
当你开始编写更长的程序时,你需要重复某些语句。例如,让我们让乌龟画一个正方形:输入列表 5-6 中显示的代码。
1 ' Square1.sb
2 Turtle.Move(60) ' Moves 60 pixels
3 Turtle.TurnRight() ' Turns right 90 degrees
4 Turtle.Move(60) ' Moves 60 pixels
5 Turtle.TurnRight() ' Turns right 90 degrees
6 Turtle.Move(60) ' Moves 60 pixels
7 Turtle.TurnRight() ' Turns right 90 degrees
8 Turtle.Move(60) ' Moves 60 pixels
9 Turtle.TurnRight() ' Turns right 90 degrees
列表 5-6:让乌龟画一个正方形
乌龟开始时面朝上方。此代码指示乌龟向上移动 60 像素,绘制正方形的一条边,然后向右转 90 度,移动 60 像素绘制另一条边,再向下转 90 度,移动 60 像素绘制第三条边,向左转 90 度,再移动 60 像素完成正方形。最后,乌龟再向右转 90 度,恢复最初的上方向。查看图 5-9 中的结果。你的屏幕看起来一样吗?

图 5-9:使用移动和转向命令绘制正方形
你将 Move(60) 和 TurnRight() 方法重复了四次。计算机不介意重复这些任务,但你输入这些代码时会觉得很无聊。如果你能用更简单的方法让乌龟绘制这个正方形,那该多好啊?
当然可以!你可以让乌龟画出与列表 5-6 中相同的正方形,只需使用几行代码。使用一个 For 循环,就像列表 5-7 中所示。
1 ' Square2.sb
2 For I = 1 To 4 ' Repeats 4 times
3 Turtle.Move(60) ' Draws one side
4 Turtle.TurnRight() ' Turns right 90 degrees
5 EndFor
列表 5-7:使用 For 循环让乌龟画正方形
For 循环运行 Turtle.Move(60) 和 Turtle.TurnRight() 四次。你会在知道重复执行某些代码的次数时使用 For 循环(有关 For 循环的更多内容,参见第十三章)。在这个例子中,程序启动循环,运行两行代码,然后返回到循环的起始位置,再次运行。这会运行四次,然后退出循环。试试看吧!
在这个简短的程序中,你将使用三个新的 Small Basic 关键字:For、To 和 EndFor。
注意
关键字(For、To和EndFor)不需要像在 Listing 5-7 中那样大写,For循环中的语句也不需要缩进,但这些是默认格式。编辑器会在你输入时自动缩进For循环中的语句,使代码更易读。*
Figure 5-10 展示了发生了什么。

Figure 5-10: For 循环的各部分
要重复代码行,只需将你想要重复的语句放在For和EndFor关键字之间。如果你想重复这些语句四次,可以这样写:
For I = 1 To 4
变量I是计数器。它跟踪循环执行了多少次,还剩多少次每次程序运行时,它会将计数器加一。
下次在学校遇到麻烦时记得使用For循环!如果老师抓到你在嚼口香糖,要求你写我以后不在课堂上嚼口香糖 100 次,Small Basic 可以来救你!这时你可以这样写:
For I = 1 To 100
TextWindow.WriteLine("I won't chew gum in class again.")
EndFor
试试看吧。不是在课堂上嚼口香糖;是试试这个程序!
注意
程序员通常使用一个字母的变量名来命名循环计数器(比如I、J或K),但其他任何名字也可以。如果你使用大写或小写字母也没关系——Small Basic 会将I和i视为同一个变量。
尝试一下 5-2
预测以下程序的输出。然后运行程序来检查你的答案。
GraphicsWindow.PenColor = "Red"
GraphicsWindow.PenWidth = 3
For I = 1 To 4
Turtle.Move(30)
Turtle.Turn(-60)
Turtle.Move(30)
Turtle.Turn(120)
Turtle.Move(30)
Turtle.Turn(-60)
Turtle.Move(30)
Turtle.TurnRight()
EndFor
绘制规则多边形
你可以轻松地修改绘制正方形的程序(Listing 5-7)来绘制其他多边形。(别那么“死板”!)多边形只是一个简单的闭合图形。关于一些例子,看看 Figure 5-11 中的三个多边形。

Figure 5-11: 三个多边形的外角
你使用一种通用的模式来绘制这些形状。要在 Figure 5-11 中绘制正方形,你需要画四条边,每画一条边后转 90 度(即 360 度除以 4)。对于五边形(中间的多边形),你需要画五条边,每画一条边后转 72 度(360 度除以 5)。对于六边形(右侧的多边形),你需要画六条边,每画一条边后转 60 度(360 度除以 6)。你看出规律了吗?角度是 360 度除以边的数量。有了这个规律,你可以在 Listing 5-8 中创建绘制多边形的程序。
1 ' Polygon.sb
2 numSides = 5 ' Set to 3 (triangle), 4 (square), 5 (pentagon)...
3
4 For I = 1 To numSides
5 Turtle.Move(60) ' Polygon's side length
6 Turtle.Turn(360 / numSides)
7 EndFor
Listing 5-8: 绘制规则多边形
要绘制不同的多边形,只需将第 2 行中numSides变量的整数值替换为其他数字。Figure 5-12 展示了你可以用这个程序绘制的八个多边形(它们的边长相同)。试试看吧!

Figure 5-12: 使用不同numSides值时 Polygon.sb 的输出
当你为numSides设置一个较大的数值时,会发生什么?多边形开始看起来像一个圆形!将numSides设置为 36,将第 5 行的Move(60)改为Move(20),然后看看会发生什么。
星星诞生了
根据你现在对不同形状角度的了解,当你将海龟按 72 度的倍数旋转时(这是你用来绘制五边形的角度),比如 2 × 72 = 144 度或 3 × 72 = 216 度,会发生什么?运行清单 5-9 中显示的程序,看看结果。
1 ' PentaStar.sb
2 For I = 1 To 5
3 Turtle.Move(150)
4 Turtle.Turn(144) ' The turn angle is 2 * 72
5 EndFor
清单 5-9:绘制五角星
如果转角是 144 而不是 72,输出的将是一个星形,而不是五边形。查看图 5-13 看看是如何实现的。

图 5-13:展示 PentaStar.sb 的输出结果
注意
如果你想在创建完作品后隐藏海龟,可以在程序的最后调用 Turtle.Hide() 。
尝试通过不同的多边形和转角进行实验,发现你可以创造出哪些不同的星形。图 5-14 展示了三个示例,帮助你开始实验。

图 5-14:通过使用清单 5-9 绘制不同的星形
动手试试 5-3
编写一个程序,让海龟绘制图 5-15 中的五边形。(提示:使用Angle属性来设置海龟的初始方向。)

图 5-15:五边形
使用嵌套循环创建多边形艺术
你可以使用多边形和星形创造出美丽的图案。在本节中,我们将通过将正方形旋转 12 次来绘制一个图案(见图 5-16)。

图 5-16:旋转多边形的输出 RotatedPolygon.sb
要制作这幅艺术作品,你将使用嵌套循环,即将一个循环放在另一个循环内部。每次外部循环运行时,它也会运行内部循环。清单 5-10 展示了如何使用嵌套循环创建图 5-16 中的漂亮图案。
1 ' RotatedPolygon.sb
2 numSides = 4 ' Set to 3 (triangle), 4 (square)...
3 repeatCount = 12 ' How many times to rotate the polygon
4
5 For I = 1 To repeatCount
6 ' 1) Draw the desired polygon
7 For J = 1 To numSides
8 Turtle.Move(60) ' The polygon's side length
9 Turtle.Turn(360 / numSides)
10 EndFor
11 ' 2) Turn the turtle a little
12 Turtle.Turn(360 / repeatCount)
13 EndFor
清单 5-10:绘制旋转多边形的图案
这个程序有两个循环,一个嵌套在另一个里面。外部循环(第 5 行)使用名为I的循环计数器,并重复 12 次绘制 12 个正方形。在每一轮外部循环中,程序执行两个任务。首先,它使用另一个名为J的循环计数器(第 7 行)绘制一个正方形。然后,在第 12 行,它会让海龟稍微转动一下(在这种情况下,360° ÷ 12 = 30°),然后再次执行第 5 行的循环,绘制下一个正方形。真是太炫了!
使用嵌套循环时,确保为循环计数器使用不同的名称。在清单 5-10 中,我们为外部循环使用了I变量,为内部循环使用了J变量。
修改numSides和repeatCount变量,尝试不同的多边形和旋转次数。图 5-17 展示了你可以通过旋转六边形创建的一些形状。尝试更改笔的颜色和宽度,为你的作品增添一些别致的元素。可能性是无穷无尽的!

图 5-17:通过旋转六边形创建的图案
尝试 5-4
预测以下程序的输出。在运行代码之前,尽量想象代码的结果。然后运行代码,检查你的答案。
repeatCount = 5
For I = 1 To repeatCount
For J = 1 To 4 ' Draws a square
Turtle.Move(60)
Turtle.Turn(90)
EndFor
For J = 1 To 3 ' Draws a triangle
Turtle.Move(60)
Turtle.Turn(120)
EndFor
Turtle.Turn(360 / repeatCount)
EndFor
无尽的图形
在清单 5-10 中,你通过旋转单个多边形创建了图案。你也可以使用两个或更多不同大小的多边形来创建图案。为了简化代码,我们将绘制两个不同大小的多边形并旋转它们。
运行清单 5-11 中显示的程序,看看你能创建什么图案。
1 ' PolygonArt.sb
2 Turtle.Speed = 10
3 numSides = 6 ' Set to 3 (triangle), 4 (square)...
4 repeatCount = 8 ' How many times to rotate
5 sideLen1 = 30 ' Side length of polygon 1
6 sideLen2 = 40 ' Side length of polygon 2
7
8 For I = 1 To repeatCount
9 For J = 1 To numSides ' Draws the first polygon
10 Turtle.Move(sideLen1)
11 Turtle.Turn(360 / numSides)
12 EndFor
13
14 For J = 1 To numSides ' Draws the second polygon
15 Turtle.Move(sideLen2)
16 Turtle.Turn(360 / numSides)
17 EndFor
18
19 ' Turns the turtle to prepare for the next round
20 Turtle.Turn(360 / repeatCount)
21 EndFor
清单 5-11:旋转两个相似的多边形
图 5-18 展示了这个程序的输出。该程序旋转了两个六边形(第一个边长为 30,第二个边长为 40)八次。第 8 行的外循环会根据repeatCount中的数字重复执行。每次程序循环时,代码会执行三个动作:
-
使用
sideLen1中的边长绘制第一个多边形(第 9-12 行)。 -
使用
sideLen2中的边长绘制第二个多边形(第 14-17 行)。 -
将海龟转向,准备下一轮循环(第 20 行)。

图 5-18: PolygonArt.sb 的输出
现在尝试使用图 5-19 中的repeatCount值,创建许多不同的图案。尝试将sideLen1 = 40和sideLen2 = 60!

图 5-19:你可以通过实验 PolygonArt.sb 创建的一些图案
尝试这个程序,看看你还能发现什么其他形状!
尝试 5-5
修改清单 5-11,绘制三个不同大小的多边形(而不是两个),然后旋转它们。将你的发现保存到下一个艺术画廊中。(或者,如果你不想成为百万富翁,可以访问tiny.cc/turtlepatterns/并与全世界分享!)
编程挑战
如果遇到问题,可以查看nostarch.com/smallbasic/,获取解决方案、更多资源以及教师和学生的复习问题。
-
这段代码绘制了一个圆:
For K = 1 To 36 Turtle.Move(6) Turtle.Turn(10) EndFor编写一个程序,让海龟重复这段代码 12 次,创建如下图案:
![image]()
-
编写一个程序,绘制像这样的花盆:
![image]()
-
重新创建以下代码并进行实验:
For I = 1 To 20 ' Repeats 20 times For K = 1 To 36 ' Draws a circle Turtle.Move(12) Turtle.Turn(10) EndFor Turtle.Turn(18) ' Gets ready for next circle Turtle.Move(12) ' Moves a little bit before drawing next circle EndFor修改
Move()的距离,在旋转每个圆之后发现新图案!![image]()
第七章:6
获取用户输入

为了执行有用的任务或提供有用的信息,一些程序需要来自您的输入。例如,在 Microsoft Word 中,您输入文本,点击按钮使其看起来更好,并输入文件名以保存它。在网页浏览器中,您点击链接或输入 URL 或搜索词来找到网页。在绘图程序中,您通过点击和拖动鼠标来绘制。当您使用程序并向其提供帮助其完成工作的信息时,这些信息被称为用户输入。
程序需要一种方式来向用户请求输入,处理这些输入,并做出正确的响应。能够做到这一点的程序是交互式的。在本章中,您将通过使程序能够接受并响应用户输入(该输入将是字符串和数字的形式)来让程序变得互动。
与计算机对话
到目前为止,您的程序所需的所有信息都在源代码中。例如,看看清单 6-1 中的程序。
1 ' CircleArea.sb
2 radius = 5
3 area = 3.1416 * radius * radius
4 TextWindow.WriteLine("Area = " + area)
清单 6-1:计算圆的面积
这个程序在第 2 行使用一个固定值 5 作为半径。这被称为硬编码数据。硬编码数据是程序的一部分。如果您想要更改这些数据,必须更改源代码。例如,要使用不同的半径来计算圆的面积,您需要打开文件,在代码中更改半径值,保存文件并运行新代码。每次想要更改变量的值时这样做非常麻烦。而且如果将这段代码交给一个不懂编程的人,它就不会很有用。
请输入您的号码?
很显然,如果您能计算任何圆的面积,而无需更改源代码,那将是最好的。所以让我们来探索如何直接读取用户输入的值。我们希望程序询问用户输入半径的值,然后在计算中使用该值。为此,您将使用 TextWindow 方法 ReadNumber()。将清单 6-1 中的第 2 行替换为以下语句:
radius = TextWindow.ReadNumber()
当此语句运行时,文本窗口中会出现一个闪烁的光标,如图 6-1 所示。光标是程序的方式告诉你:“轮到你输入了。我在等你。别让我过来找你!”

图 6-1:运行 ReadNumber() 方法
当用户输入一个数字并按下 ENTER 键时,输入会被存储,供程序的其余部分使用。用户输入的数字现在存储在 radius 变量中。
注意
运行程序并尝试输入一个非数字字符。会发生什么?程序不会接受任何不是数字的输入!这就是该方法被命名为 ReadNumber() 的原因。
向计算机自我介绍
程序还可以接受用户以文本或字符串的形式输入数据。假设我们想要用玩家的名字来问候他们。我们将把玩家的名字存储在一个名为userName的变量中,但使用一个新的TextWindow方法,叫做Read()。运行以下程序,并在看到光标时输入你的名字:
userName = TextWindow.Read()
TextWindow.Write("Hello " + userName + ". ")
TextWindow.WriteLine("It's really nice to meet you.")
第一条语句接受用户在文本窗口中输入的文本,并将其存储在userName中。程序然后用用户的名字来问候他们。
那么,Read()和ReadNumber()有什么区别呢?Read()的工作方式和ReadNumber()完全相同:它们都会显示一个闪烁的光标,并等待用户输入并按下 ENTER。事实上,你甚至可以使用Read()来获取数字。但ReadNumber()只接受用户输入的数字,因此我们建议在你需要用户输入数字时使用它。
为输入编写提示
一个闪烁的光标没有任何提示,无法告诉用户应输入何种数据(比如数字、姓名、地址或最喜欢的猴子类型)。除非你的程序是为魔术师或像教授 X 这样的读心术师编写的,否则你应该在允许程序从Read()或ReadNumber()接受输入之前,提供一些指引。为此,你将显示一个提示,这是告诉用户应输入哪种数据的消息。
从清单 6-1 中,将第 2 行替换为以下两行:
TextWindow.Write("Please enter a radius; then press <Enter>: ")
radius = TextWindow.ReadNumber()
首先,我们调用Write()方法并传递一条消息来显示。在这个程序中,你传递的消息是一个提示,告诉用户输入半径的值并按下 ENTER。你用冒号结束提示,以便告诉用户程序在等待键盘输入。(你可以不加冒号,但这样会让用户更清楚。)使用Write()而不是WriteLine(),这样光标就会停留在与提示相同的行上。调用Write()后,接着调用ReadNumber()来接受用户输入的数字并将其存储在radius变量中。
添加这两条语句后,运行你的程序,应该会看到类似于图 6-2 的内容。

图 6-2:文本窗口等待用户输入
当 Small Basic 执行语句radius = TextWindow.ReadNumber()时,它会等待用户输入一个数字并按下 ENTER。程序不会在用户按下 ENTER 之前读取他们输入的内容。当用户按下 ENTER 后,程序会获取用户的输入,并将其赋值给radius变量。然后,程序会继续执行ReadNumber()方法后的语句。
现在你已经接受了用户的半径,剩下的就是计算面积并显示结果。清单 6-2 展示了完整的程序。
1 ' CircleArea3.sb
2 TextWindow.Write("Please enter a radius; then press <Enter>: ")
3 radius = TextWindow.ReadNumber()
4
5 area = 3.1416 * radius * radius
6 TextWindow.WriteLine("Area = " + area)
清单 6-2:让用户输入半径
让我们来看一下如果输入半径为 8 时,输出会是什么样子:
Please enter a radius; then press <Enter>: 8
Area = 201.0624
自己试试看吧!
请稍等片刻(暂停)
有时你可能需要向用户展示一些说明(比如解释“捉迷藏”游戏的规则),然后等待他们阅读这些说明。例如,你可能会显示说明内容,并跟上“按任意键继续...”的提示,然后等待用户按键表示他们准备继续。你可以通过使用Pause()方法来实现这一点。
为了看到这个方法的实际效果,让我们写一个程序,把计算机变成一个智慧机器。每次用户按下一个键时,计算机会显示一条新的智慧名言。该程序如清单 6-3 所示。
1 ' WisdomMachine.sb
2 TextWindow.WriteLine("WISDOM OF THE DAY")
3
4 TextWindow.WriteLine("A friend in need is a friend indeed.")
5 TextWindow.Pause()
6
7 TextWindow.WriteLine("A hungry man is an angry man.")
8 TextWindow.Pause()
9
10 TextWindow.WriteLine("Love your enemies. They hate that.")
清单 6-3:演示 Pause() 方法
在显示第一句智慧名言(第 4 行)之后,程序调用Pause()给用户时间阅读(第 5 行)。这个调用会显示“按任意键继续...”并等待用户按键。当用户按下一个键时,程序会显示下一句智慧名言(第 7 行),然后再次暂停(第 8 行)。程序会继续这样做,直到执行最后一句话。向程序中添加更多智慧名言,然后与他人分享!
如果你想显示与“按任意键继续...”不同的语句,例如“按任意键查看下一条智慧名言...”,该怎么办呢?好消息是,Small Basic 为此提供了PauseWithoutMessage()方法。你可以像平常一样使用Write()或WriteLine()编写自定义提示,然后调用PauseWithoutMessage()等待用户。试试看吧。将清单 6-3 中第 5 行和第 8 行的Pause()调用替换为以下语句:
TextWindow.WriteLine("Press any key to see the next line of wisdom...")
TextWindow.PauseWithoutMessage()
你的程序运行方式与之前相同,但使用了更具描述性的提示。
处理用户输入
让我们通过编写几个程序来运用你学到的新知识,这些程序将从用户那里读取输入,处理输入,并将输出显示给用户。
将华氏度转换为摄氏度
接下来,你将创建一个程序,将温度从华氏度转换为摄氏度。程序会提示用户输入华氏温度,然后使用以下公式将其转换为摄氏度:
C = (5 ÷ 9) × (F – 32)
多次运行清单 6-4 中的程序,看看它是如何工作的。要使用度符号,请按住 ALT 键,输入数字小键盘上的248,然后释放 ALT 键。
1 ' Fahrenheit2Celsius.sb
2 TextWindow.Write("Enter a temperature in °F: ")
3 F = TextWindow.ReadNumber()
4 C = (5 / 9) * (F - 32)
5 C = Math.Round(C) ' Rounds to nearest integer
6 TextWindow.WriteLine(F + " °F = " + C + " °C")
清单 6-4:将华氏度转换为摄氏度
首先,程序提示用户输入温度。当用户按下 ENTER 键时,输入的值会被赋给变量 F。然后,程序将存储在 F 中的值转换为摄氏度,并将结果存储在变量 C 中(这一切都在第 4 行完成)。接下来,程序在第 5 行使用 Math.Round() 对 C 的当前值进行四舍五入,将四舍五入后的值存储回 C,替换掉原来的值。你将在第七章中学习更多关于 Round() 方法的内容,但我们在这里使用它是为了让程序的输出更易读。最后,程序在第 6 行显示输出结果。
动手实践 6-1
尝试猜测以下程序的功能。运行它来检查你的答案:
TextWindow.Write("How old are you? ")
age = TextWindow.ReadNumber()
TextWindow.WriteLine("In ten years, you'll be " + (age + 10))
TextWindow.WriteLine("Wow! You'll be so old!")
计算数字平均值
让我们编写一个程序,计算用户提供的四个数字的平均值。实现这个目标有几种方式;第一种是使用五个变量,如清单 6-5 所示。
1 ' Avg1.sb
2 TextWindow.Write("Enter 4 numbers. ")
3 TextWindow.WriteLine("Press <Enter> after each one:")
4 n1 = TextWindow.ReadNumber()
5 n2 = TextWindow.ReadNumber()
6 n3 = TextWindow.ReadNumber()
7 n4 = TextWindow.ReadNumber()
8 avg = (n1 + n2 + n3 + n4) / 4
9 TextWindow.WriteLine("Average = " + avg)
清单 6-5: 计算四个数字的平均值
当我们输入 10、20、15 和 25 时,查看输出:
Enter 4 numbers. Press <Enter> after each one:
10
20
15
25
Average = 17.5
程序提示用户输入四个数字,每输入一个数字后按下 ENTER 键。它逐个读取这些数字,并将它们保存在四个变量中:n1、n2、n3 和 n4(第 4 到 7 行)。然后,它计算这些数字的平均值,并将平均值存储在变量 avg 中(第 8 行),最后显示结果(第 9 行)。
清单 6-6 展示了另一种编写此程序的方式。输入这个程序后运行它。这次你将只使用一个名为 sum 的变量。
1 ' Avg2.sb
2 TextWindow.Write("Enter 4 numbers. ")
3 TextWindow.WriteLine("Press <Enter> after each one:")
4 sum = TextWindow.ReadNumber()
5 sum = sum + TextWindow.ReadNumber()
6 sum = sum + TextWindow.ReadNumber()
7 sum = sum + TextWindow.ReadNumber()
8 TextWindow.WriteLine("Average = " + (sum / 4))
清单 6-6: 使用累加器计算四个数字的平均值
为了理解程序是如何工作的,假设用户输入了数字 10、20、15 和 25 作为回应。所以,在第 4 行,sum 变成了 10。在第 5 行,第二个数字(20)加到第一个数字(10)上,结果保存在 sum 变量中(总共 30)。在第 6 到 7 行,第三个数字(15)和第四个数字(25)被加到一起并保存在 sum 中(总共 70)。程序然后显示平均值,即 sum / 4,给用户看(第 8 行)。
因为 sum 变量不断将输入值累加到自己上(或称为积累),它被称为累加器(也称为累计和)。(这可能类似于你积累发圈或宝可梦卡片的方式,但这些数字只占用计算机内存,不会让你的房间变得杂乱。)
读取文本
接下来,让我们编写一个简单的程序,使用莎士比亚名言中的单词来构建搞笑的句子:“To be or not to be: that is the question。”你将要求用户输入两个动词和一个名词,然后用这些输入替换莎士比亚名言中的 be、be 和 question。清单 6-7 展示了完整的程序。
1 ' Silly.sb
2 TextWindow.Write("Please enter a verb: ")
3 verb1 = TextWindow.Read()
4
5 TextWindow.Write("Please enter another verb: ")
6 verb2 = TextWindow.Read()
7
8 TextWindow.Write("Now, please enter a noun: ")
9 noun = TextWindow.Read()
10
11 TextWindow.Write("To " + verb1)
12 TextWindow.Write(" or not to " + verb2 + ":")
13 TextWindow.Write(" that is the " + noun + ".")
14 TextWindow.WriteLine("")
清单 6-7: 搞笑的莎士比亚句子
当我们运行这段代码时,我们输入了eat、swim 和 cow。这是输出结果:
Please enter a verb: eat
Please enter another verb: swim
Now, please enter a noun: cow
To eat or not to swim: that is the cow.
尝试一下,然后回来。我们等你。你回来了么?你的输出比我们的更好笑吗?那就去给别人看看吧!
试试看 6-2
编写一个互动的 Mad Libs 风格程序,要求用户输入他们最喜欢的公主的名字(比如白雪公主)、某个邪恶的东西、公主学校的名字、某个美味的东西、一个矮小巫师的名字、某个他们永远不会卖掉的珍贵物品、一个动词、小生物的名称和超级英雄的力量。
然后向用户展示以下故事,并用用户的输入替换方括号中的词语:
“公主[PrincessName]正在穿越森林,突然邪恶的[SomethingEvil]跳出来向她递了一只苹果。公主[PrincessName]拒绝了,因为她的母亲让她去[NameOfSchool],在那里她学到了不要从陌生人那里接受未包装的食物(因为那可能是被下了毒的)。于是公主[PrincessName]继续穿越森林,直到她遇到了一座由[SomethingYummy]做成的房子!她不想破坏私人财产,所以继续走。接下来,公主[PrincessName]遇到了一个纺车,纺车旁站着一个名叫[ShortWizard’sName]的小个子巫师,他诱使她使用一个魔法纺车来制造黄金(以交换她的[SomethingValuable])。但是公主[PrincessName]的母亲早就告诉过她,当她还是婴儿时,一位邪恶的仙女对她施下了诅咒,如果她在纺车上刺破手指,她将永远[Verb]。因此,公主[PrincessName]继续前行,最终安全回到家,家里有七个[SmallCreatures],她把自己锁在房间里过完了余生,因为她拥有[SuperHeroPower]的力量。”
然后制作一个自己的互动故事程序,创建一个新角色(比如英雄、忍者、海盗,或者小马宝莉),并分享它!
编程挑战
如果你遇到困难,可以查看 nostarch.com/smallbasic/,这里有解决方案以及更多资源和针对教师和学生的复习问题。
-
使用 Small Basic,你可以轻松将你的计算机变成一个数字魔法师!打开本章节文件夹中的 Magician.sb 文件并运行它。解释这个程序是如何工作的。
-
创建一个愚蠢的 Mad Libs 风格游戏,使用这个短语:“一个人的垃圾是另一个人的宝藏。”这个短语的另一个版本是:“一个人的失落是另一个人的收获。”在你的版本中,要求用户提供两个活物名称和两个不同的名词。然后让你的程序输出如下格式:“一个[Creature1]的[Noun1]是另一个[Creature2]的[Noun2]。”
-
伊芙的妈妈正在举办车库销售。因为伊芙想赚些钱,她在车道上摆了张桌子,卖柠檬水、饼干和她亲手做的贺卡(她是个天才销售员,所以卖得很好)。通过编写一个程序,帮助伊芙计算顾客给她的钱,程序会要求伊芙输入她赚到的美元、quarters(四分之一美元硬币)、dimes(十分之一美元硬币)、nickels(五分之一美元硬币)和 pennies(分币)的数量。然后,程序会将这些钱转换成总金额,以美元和分(比如 $23.34)的形式显示出来。尝试使用以下金额来测试你的程序,确保它能正常工作:
a. 35 美元,3 个 quarter(四分之一美元硬币),3 个 penny(分币)
b. 2 美元,1 个 quarter(四分之一美元硬币),2 个 penny(分币)
c. 10 美元,1 个 nickel(五分之一美元硬币),3 个 penny(分币)
d. 6 美元,1 个 quarter(四分之一美元硬币),3 个 penny(分币)
e. 3 美元,2 个 quarter(四分之一美元硬币),1 个 dime(十分之一美元硬币),1 个 nickel(五分之一美元硬币),3 个 penny(分币)
f. 1 美元,2 个 dime(十分之一美元硬币),1 个 nickel(五分之一美元硬币),4 个 penny(分币)
第八章:7
用数学赋能程序

如果数学让你感到无聊或害怕,那也没关系。你很快就会发现,Small Basic 让你编写执行数学运算的程序变得非常简单。许多程序仅使用加法、减法、乘法和除法等基本运算。对于这些类型的问题,你只需要四个基本的数学运算符(+、–、*和/)。星号(*)表示乘法,而斜杠(/)表示除法。
其他程序可能需要使用一些你在代数中学过的数学函数(例如平方根、绝对值和三角函数)。Small Basic 的Math对象提供了这些函数以及其他许多函数。
如果你不知道什么是平方根或三角函数,别担心;你依然可以使用这些函数编写程序。而且,你也可以跳过本章的一些示例。
要使用任何Math对象的方法,你将编写如下语句:
ans = Math.SquareRoot(16)
在这个示例中,你调用SquareRoot()方法并传入 16(来求 16 的平方根)。方法的输出结果称为返回值。在这个语句中,方法的返回值被赋值给ans变量(即答案的缩写)。在本章中,你将学习Math对象的方法,并了解如何将它们投入实际使用。
指数方法
Math对象有四个与指数相关的方法,但本书只会介绍SquareRoot()和Power()这两个。
SquareRoot() 和古老的毕达哥拉斯
在这个第一个示例中,我们将找到直角三角形的最长边,或称斜边的长度。如果你将其他两条边的长度称为s1和s2,那么勾股定理告诉你,斜边的长度是每条边的平方和的平方根。公式如下:

我们将这个公式放入了列表 7-1 的程序中,这样你就不必过多思考它了。给定直角三角形的两条边长,以下程序使用勾股定理计算斜边的长度。
1 ' SquareRootDemo.sb
2 TextWindow.Write("Enter the length of side 1: ")
3 s1 = TextWindow.ReadNumber()
4
5 TextWindow.Write("Enter the length of side 2: ")
6 s2 = TextWindow.ReadNumber()
7
8 hypot = Math.SquareRoot(s1 * s1 + s2 * s2)
9 TextWindow.WriteLine("Hypotenuse = " + hypot)
列表 7-1:求斜边长度
该程序提示用户输入第一条边的长度(第 2 行),然后将输入值保存在s1中(第 3 行)。接着它要求输入第二个值并保存在s2中(第 6 行)。然后它计算斜边的长度(第 8 行)并显示结果(第 9 行)。在第 8 行,注意到 s1(和s2)的平方是通过将s1(和s2)乘以它们自己来计算的。
这是我们程序的一个示范运行。记住,这个程序只适用于直角三角形:
Enter the length of side 1: 3
Enter the length of side 2: 4
Hypotenuse = 5
强大的指数运算
你可以使用 Power() 进行各种涉及指数的计算,比如计算 3 的 5 次方。你可能在数学课上看到它写作 3⁵,它等于 3 × 3 × 3 × 3 × 3。这里的 3 被称为 底数,5 被称为 指数。以下是如何在 Small Basic 中进行此计算:
answer = Math.Power(3, 5)
TextWindow.Write(answer)
请注意,Power() 接受两个参数:第一个是底数,第二个是指数。结果保存在 answer 变量中。第二条语句显示输出结果,以便你检查答案。
现在让我们来看一个稍微复杂的程序。我们将使用 Power() 方法来展示钱是如何增长的。如果你在一家银行存入 P 美元,利率为 r%,那么在 n 年后你将拥有 A 美元:
A = P × (1 + r)^(n)
不用担心这个公式的来源,让我们编写一个程序来计算给定 P、r 和 n(由用户输入)的 A 的值。将程序输入到示例 7-2 中。
1 ' PowerDemo.sb
2 TextWindow.Write("Principal (in dollars)........: ")
3 P = TextWindow.ReadNumber()
4
5 TextWindow.Write("Interest rate (decimal form)..: ")
6 r = TextWindow.ReadNumber()
7
8 TextWindow.Write("Number of years...............: ")
9 n = TextWindow.ReadNumber()
10
11 A = P * Math.Power(1 + r, n)
12
13 TextWindow.WriteLine("")
14 TextWindow.Write("After " + n + " years, ")
15 TextWindow.WriteLine("you will have $" + A)
16 TextWindow.WriteLine("That fortune is almost as big as Batman's!")
示例 7-2:计算你的钱如何增长
运行程序,看看如果你存入 1,000 美元,年利率为 6%,20 年后你将拥有多少钱:
Principal (in dollars)........: 1000
Interest rate (decimal form)..: 0.06
Number of years...............: 20
After 20 years, you will have $3207.1354722128500
That fortune is almost as big as Batman's!
我们承认看到美元和分数写得有那么多小数位数确实很奇怪。在这种情况下,你不需要这么多的小数位。接下来,你将学习如何将这个长的答案四舍五入到最接近的美元和分。
尝试一下 7-1
马戏团正在寻找人才,他们认为你就是那个人!他们愿意支付你 1 美元来让你在头上平衡一只猫,2 美元来让你平衡两只猫,4 美元来平衡第三只猫,以此类推,每增加一只猫,报酬翻倍!编写一个程序,计算当你在头上平衡 n 只猫时,你会得到多少钱,其中 n 是由用户输入的。这些钱够你退休并买个猫咪大宅吗?
四舍五入方法
有时你需要在程序中对数字进行四舍五入。例如,如果你的程序计算了你所在社区每户的平均子女数,你肯定不希望程序显示 2.25(每户两个零点二五个孩子)。这显然不合适!
Math 对象提供了三个方法来对数字进行四舍五入或截断:Round()、Floor() 和 Ceiling()。请参见图 7-1,快速了解每个方法对数字的作用。
Round(x) 返回最接近 x 的整数。Floor(x) 返回小于或等于 x 的整数,而 Ceiling(x) 返回大于或等于 x 的整数。试验这些不同的方法,看看你得到什么结果。
让我们利用这些四舍五入的知识来修正我们利息计算器的输出。在示例 7-2 的第 11 行后添加以下语句:
A = Math.Round(A)
计算完A后,你将其四舍五入并将四舍五入后的结果重新赋值给A。当你用相同的输入重新运行程序时,它将显示$3207。太好了!

图 7-1:带有示例参数和返回值的四舍五入方法
传统四舍五入
当你使用Round()方法时,如果数字的小数部分恰好是 0.5,请小心。在这种情况下,Round()方法会将数字四舍五入到最接近的偶数整数(这叫做银行家舍入)。例如,0.5 和-0.5 会四舍五入为 0,1.5 和 2.5 会四舍五入为 2.0,-1.5 和-2.5 会四舍五入为-2。这与你在代数中学到的不同,在代数中 0.5 的分数总是向上舍入到 1!尽管这不是你习惯的方式,但银行家舍入非常常见,并且被银行家广泛使用,因此得名。
但是我们如何让 Small Basic 按照你在学校学到的方式四舍五入数字(即 0.5 的分数总是向上舍入)呢?我们将使用Floor()方法而不是Round()方法来做一些巧妙的处理,如下所示:
Math.Floor(x + 0.5)
使用这个技巧,x表示你想要四舍五入的任何值。所以如果x是 0.6,那么x + 0.5 = 1.1,Floor(1.1) = 1。酷吧!这正是我们期望的效果。
假设x是 2.5。如果我们只使用Math.Round(2.5),我们会得到 2,这不是你希望的传统四舍五入的结果。我们想要四舍五入并得到 3。使用我们巧妙的技巧,你会得到x + 0.5 = 3.0,Floor(3.0) = 3。现在这才是我们想要的!这样就得到了你期望的结果,如果你想四舍五入一个带有.5 的小数。
四舍五入到最接近的百分位数
让我们再看看例子 7-2。使用Round()或Floor()处理结果会得到一个整数(只有美元)。但如果你想要显示到最接近的便士的金额怎么办?如何让 Small Basic 将答案四舍五入到最接近的百分位数呢?考虑这个语句:
Math.Floor(100 * x + 0.5) / 100
例如,如果x = 2.8735,那么100 * x + 0.5 = 287.85,Floor()方法返回 287。将 287 除以 100 得到 2.87,这是我们想要的结果。
你也可以使用以下语句四舍五入到最接近的百分位数:
Math.Round(x * 100) / 100
我们使用第二种技术将例子 7-2 中的答案四舍五入到最接近的便士。在例子 7-2 的第 11 行之后,添加以下语句:
A = Math.Round(A * 100) / 100
在第 11 行计算完A后,程序将其四舍五入到最接近的百分位数(最接近的便士),并将四舍五入后的答案保存回A。如果你现在使用原始输入重新运行程序,输出将是$3207.14。完美!现在我们在谈钱了!
尝试一下 7-2
海伦在商店遇到了一些麻烦。她用计算器把 6%的销售税加到购买价格上。例如,如果顾客的总额是 27.46 美元,她会将 27.46 乘以 1.06 得到 29.1076。但是,她应该向顾客收取 29.10 美元还是 29.11 美元?她没有时间自己做这些计算!她的店铺把她忙得不可开交!
海伦听说了你的编程技能,因此她来找你帮忙。她需要一个程序,让她输入总购买金额。然后,她希望程序能加上销售税,将结果四舍五入到最接近的分,并显示答案。为海伦创建这个程序。
Abs()、Min()和 Max()方法
Math对象为你提供了一些方法来计算一个数字的绝对值。当你计算一个数字的绝对值时,实际上是在找它与零的距离,这个值始终是一个正数。例如,–1 和 1 的绝对值都是 1。
这段代码示范了几个例子:
Math.Abs(-2) ' = 2
Math.Abs(-3.5) ' = 3.5
Math.Abs(4) ' = 4
Abs()方法接受一个数字(无论是正数还是负数),并返回该数字与 0 的距离,即其绝对值。这个返回值始终是一个正数。(换句话说,Abs()去掉了负号。)
例如,假设你的游戏玩家需要猜一个秘密数字(10),但猜测不必完全准确。相反,你的游戏接受 8 到 12 之间的任何猜测。为了检查玩家的猜测是否合理,你可以测试玩家的猜测(保存在guess变量中)和 10 之间的绝对差值;即Abs(guess - 10)。如果结果小于或等于 2,那么玩家的猜测就可以接受。你将在下一章学习如何使用If语句执行这样的检查。
现在让我们找出两个数字中的最小值或最大值。Min()方法返回两个数字中的较小值,而Max()方法返回较大的数字:
Math.Min(5, 10) ' = 5
Math.Min(-3.5, -5.5) ' = -5.5
Math.Max(3, 8) ' = 8
Math.Max(-2.5, -4.7) ' = -2.5
你可以使用这些方法来限制用户输入的数字。例如,如果你的程序期望一个小于 100 的数字,你可以写出以下代码:
ans = TextWindow.ReadNumber()
ans = Math.Min(ans, 100)
TextWindow.WriteLine(ans)
试试看!运行这段代码两次。第一次输入一个小于 100 的数字,第二次输入一个大于 100 的数字。会发生什么?你能修改代码,使输入的数字不能低于 0 吗?
如果你想找到三个数字中的最小值怎么办?例如,假设你想找出上周参加的三场数学测验中的最低分。一个方法是这样写:
minScore = Math.Min(Math.Min(score1, score2), score3)
内部的Min()方法找到score1和score2变量中的最小值。该结果和score3被传递到外部的Min()方法中,以确定哪个较小:第一个最小值(来自score1和score2)还是score3。最终结果保存在minScore变量中。
尝试一下 7-3
你最喜欢的薯片在三个本地商店出售;每一袋的价格都不一样。写一个程序,提示你输入每个商店的价格,然后显示最低价格。节省下来的钱就能买更多的薯片!
Remainder() 方法
你可以通过使用Remainder()方法从任何除法操作中得到余数。例如,Math.Remainder(10, 3)返回 1,因为 10 ÷ 3 = 3,余数为 1。
你可以使用Remainder()方法测试一个整数(整数)是否能被另一个更小的整数整除。余数为 0 表示较大的数字可以被较小的数字整除(比如 9 可以被 3 整除)。知道是否有余数有各种有趣的用途。例如,如果你想检查一个数字是偶数还是奇数,你可以检查这个数字除以 2 的余数:如果余数是 0,数字是偶数;否则,它是奇数。
为了演示Remainder()方法的作用,让我们编写一个程序,查找给定金额中的美元、四分之一美元、一角硬币、五分硬币和一美分硬币的数量。为了找到最有效的美元和硬币数量,你需要从最大面额(美元)开始,然后逐步处理到最小面额(美分)。清单 7-3 展示了完整的程序,并在注释中包含了示例输出。阅读程序,看看你是否能弄清楚输入为 25.36 时会发生什么。
1 ' Money.sb
2 TextWindow.Write("Enter an amount of money (such as 25.36): ")
3 total = TextWindow.ReadNumber() ' In dollars and cents = 25.36
4 cents = Math.Floor(total * 100) ' Total cents = 2536
5 dollars = Math.Floor(cents / 100) ' Number of dollars = 25
6 cents = Math.Remainder(cents, 100) ' Remaining cents = 36
7 quarters = Math.Floor(cents / 25) ' Number of quarters = 1
8 cents = Math.Remainder(cents, 25) ' Remaining cents = 11
9 dimes = Math.Floor(cents / 10) ' Number of dimes = 1
10 cents = Math.Remainder(cents, 10) ' Remaining cents = 1
11 nickels = Math.Floor(cents / 5) ' Number of nickels = 0
12 pennies = Math.Remainder(cents, 5) ' Number of pennies = 1
13 TextWindow.Write("$" + total + " = ")
14 TextWindow.Write("$" + dollars + ", ")
15 TextWindow.Write(quarters + "Q, ")
16 TextWindow.Write(dimes + "D, ")
17 TextWindow.Write(nickels + "N, ")
18 TextWindow.Write(pennies + "P.")
19 TextWindow.WriteLine("")
清单 7-3:查找美元和硬币面额
让我们逐行分析这个程序,了解它是如何工作的。用户在第 2 行输入 25.36(即 25 美元和 36 美分),因此total = 25.36。第 4 行计算总的cents为Floor(25.36 * 100) = 2536。这个数字然后被除以 100 得到 25 并保存到dollars(第 5 行),剩下的 36 被保存到cents(第 6 行)。接下来,36 美分除以 25 得到 1 个四分之一美元(第 7 行)和剩余的 11 美分(第 8 行)。剩余的 11 美分然后被除以 10 得到 1 个一角硬币(第 9 行),剩下 1 美分(第 10 行)。第 11 行和第 12 行以相同的方式计算可用的nickels和剩余的pennies。程序的其余部分(第 13–19 行)显示结果。图 7-2 说明了这个程序。

图 7-2:展示 Money.sb 的输出
让我们试试不同的金额,看看输出是什么:
Enter an amount of money (such as 25.36): 23.78
$23.78 = $23, 3Q, 0D, 0N, 3P.
如果你要找零钱,这个方法非常有用!
尝试 7-4
编写一个程序,读取一个三位数,并输出每个数字后面跟着它的位值。例如,如果输入是 368,程序应该显示如下内容:
3 Hundreds
6 Tens
8 Ones
(提示:如果你将 368 除以 100,你得到 3,余数为 68。如果你将 68 除以 10,你得到 6,余数为 8。)
随机数
随机数在许多应用中都有使用,比如仿真和游戏。它们还被用于软件测试(查看程序如何响应不同的输入值)或模拟随机事件(如彩票)。
GetRandomNumber()方法返回一个介于 1 和你传入的方法的上限之间的随机整数。通过这个方法,你的程序可以生成随机数,应用于各种有趣的场景,例如查看一个巨魔是否会敲打你英雄的头。让我们来看一些例子。
要模拟掷骰子,写下如下代码:
dice = Math.GetRandomNumber(6)
TextWindow.WriteLine("You rolled: " + dice)
变量dice包含一个 1 到 6 之间的随机数,类似于从帽子里随机抽取(但不是霍格沃茨的分院帽)。运行程序几次,亲自感受一下。
要模拟投掷硬币,你可以编写如下代码:
coinFlip = Math.GetRandomNumber(2)
TextWindow.WriteLine("Outcome: " + coinFlip)
变量coinFlip的值为 1 或 2。值 1 表示正面,值 2 表示反面(或者你也可以反过来,取决于你!)。
要模拟掷一对骰子并计算它们的和,你可以编写如下代码:
num1 = Math.GetRandomNumber(6)
num2 = Math.GetRandomNumber(6)
outcome = num1 + num2
TextWindow.Write("You got (" + num1 + "," + num2 + "). ")
TextWindow.WriteLine("The total is " + outcome)
虽然你的结果会在 2(掷出两个 1)和 12(掷出两个 6)之间,但不要犯这样的错误:
outcome = 1 + Math.GetRandomNumber(11)
尽管这个语句会给你一个 2 到 12 之间的数字,但从一个随机数得到的概率与将两个随机数相加得到的概率是不同的。
动手试试 7-5
一个袋子里有 20 个球,编号从 1 到 20。编写一个程序,模拟随机抽取一个球。
三角方法
三角函数是那些让高中生头疼的“恶魔”(正弦、余弦、正切等等)。我们不会解释这些是什么,但如果你对三角函数毫无概念,或者从未听说过三角学这个词,别担心。你可以跳过这部分,直接看第八章。否则,让我们通过一个例子来快速入门。
假设来自未来的机器人穿越时空来到我们这个时代,目的是摧毁人类,而你是唯一能够阻止他们攻击的人。你需要用你的大炮摧毁他们的武器仓库,如图 7-3 所示。

图 7-3:摧毁机器人仓库
你的大炮以初始速度v为 160 英尺每秒发射,仓库距离 500 英尺。你只需要计算发射角度θ(希腊字母 Theta)。清单 7-4 中的程序会提示你输入所需的角度,然后根据图中显示的公式计算导弹射程d(单位:英尺)。
你需要多次运行程序(使用不同的发射角度)来找到最适合的发射角度。
1 ' AndroidAttack.sb
2 v = 160 ' Initial speed = 160 feet/sec
3
4 TextWindow.WriteLine("Shoot the cannon to destroy the warehouse!")
5 TextWindow.Write("Enter launch angle in degrees: ")
6 angle = TextWindow.ReadNumber()
7 theta = Math.GetRadians(angle) ' Angle in radians
8
9 d = (v * v) * Math.Sin(2 * theta) / 32
10 d = Math.Round(d) ' Rounds to the nearest integer
11
12 TextWindow.WriteLine("Distance = " + d + " feet.")
清单 7-4:计算发射角度
在提示符后,程序读取你的输入并将其保存在变量angle中(第 6 行)。然后,第 7 行使用GetRadians()方法将角度从度数转换为弧度(Sin()方法要求其输入为弧度)。
接下来,程序使用给定的公式计算距离(第 9 行),四舍五入到最接近的整数(第 10 行),然后显示结果(第 12 行)。
这是一个示例运行:
Shoot the cannon to destroy the warehouse!
Enter launch angle in degrees: 45
Distance = 800 feet.
看来人类还没有完全安全。继续在程序中输入不同的角度,直到你得到正确的答案。
除了Sin()方法,Math对象还提供Cos()、Tan()、ArcSin()、ArcCos()和ArcTan()方法。你可以在本章的附加资源部分阅读更多关于这些方法的信息,地址是www.nostarch.com/smallbasic/。
尝试一下 7-6
你想从一片森林中选一棵 20 英尺高的圣诞树(用于你学校的节日庆典)。找对树的一种方法是把卷尺绑在猴子身上,让它爬上每棵树,但我们不如用一点三角学来代替。通过测量从树基到树顶的距离d和角度θ,如图 7-4 所示,你可以像下面这样计算树的高度h:
h = d tan(θ)
编写一个程序,允许你输入d和θ,并计算树的高度。

图 7-4:计算树的高度
编程挑战
如果你遇到困难,可以访问nostarch.com/smallbasic/查看解决方案、更多资源以及供教师和学生复习的问题。
-
为以下代数表达式编写一个 Small Basic 语句:
a.
![image]()
b. a = x((*yz*))
c.
![image]()
-
以下难题是公元前 1650 年埃及抄写员 Ahmes 编写的。
“七座房子,每座房子里有七只猫。每只猫抓到七只老鼠。每只老鼠,如果活着的话,能吃掉七穗粮食。每穗粮食能产出七蒲式耳的小麦。这些猫节省了多少蒲式耳的小麦?”
编写一个 Small Basic 程序来找到答案。(提示:使用
Power()方法。) -
创建一个程序,将用户输入的秒数转换为相应的小时、分钟和秒数。例如,如果用户输入 8110 秒,程序将显示 2 小时 15 分钟 10 秒。
第九章:8
使用 If 语句做决策

我该穿哪件衬衫?我该吃什么晚餐?我该去哪里?我应该把裤子穿得那么低,以至于我的内裤都露出来吗?你每天都会问自己这样的问题并给出答案。就像你做决策一样,你的程序也能做到!当然,它们不会自己做出决策。你的程序只会做你希望它们做的比较,然后执行一些语句或跳过它们。在本章中,你将编写能够做出决策的程序。
到目前为止,你编写的程序遵循了一条简单的路径,其中语句从上到下执行。但有时你可能需要在某个条件为真时执行一些语句,或者在某个条件为假时执行其他语句。这就像你在生活中做决策的方式。例如,你可能会说:“如果下雪,我就去滑雪”或者“如果我在下午 4 点前完成工作,我就去看电影;否则,我就去史蒂夫家。”在这两种情况下,你采取的行动取决于一个条件。
Small Basic 使用几种不同的方法来控制程序中哪些语句会被执行:选择语句(If,If/Else,If/ElseIf),跳转语句(Goto)以及迭代或循环语句(For和While)。在本章和下一章中,我们将解释选择语句和跳转语句,而循环语句将在第十三章和第十四章中讲解。本章中,你将了解关系运算符、布尔表达式,以及如何使用If/Else语句编写一些有趣的程序。
If 语句
假设你妈妈打电话告诉你:“回家的路上,停在 Captain Snarf 的披萨店。如果它开门,给我们买一个大披萨。”她的指示没有说如果披萨店关门该怎么办;你假设自己会空手回家。清单 8-1 将这个情景用代码表示出来。
1 ' SnarfPizza.sb
2 TextWindow.WriteLine("Is Snarf's Pizza open?")
3 TextWindow.Write("Enter 1 (for open) or 2 (for closed): ")
4 status = TextWindow.ReadNumber()
5
6 If (status = 1) Then
7 TextWindow.WriteLine("You bought a delicious pizza!")
8 EndIf
9 TextWindow.WriteLine("Time to go home!")
清单 8-1:使用 If 和 EndIf 关键字
运行这个程序并在提示时输入1(表示 Snarf’s 开门)。由于第 6 行的条件为真,程序会显示第 7 行的信息,内容是“你买了一个美味的披萨!”第 9 行的语句(即EndIf关键字之后的语句)无论你是否买了披萨都会执行。再次运行这段代码,这次在提示时输入2。会发生什么呢?
第 6 行的语句是一个If语句。语句中If关键字后的部分(status = 1)是条件。程序检查该条件是否为真。在这个例子中,它检查 Captain Snarf 的披萨店是否开门。Then和EndIf关键字之间的代码是动作——程序要执行的内容。只有在条件为真时,程序才会执行动作。程序员通常使用代码块这个术语,来指代If和EndIf关键字之间的语句(第 6 行到第 8 行之间)。
注意
Small Basic 不要求你在条件表达式周围加括号,这意味着你可以像这样写第 6 行的语句: If status = 1 Then。但括号使语句更易读,所以我们在本书中会使用它们。
Small Basic 在你输入代码时会自动缩进If语句,这使得程序更易读,并清楚地显示哪些语句是代码块的一部分。如果你的代码失去了缩进,可以右击编辑器并从弹出菜单中选择格式化程序来重新缩进所有代码。太棒了!
If语句是 Small Basic 中所有决策的基础。查看图 8-1 中的插图,了解它是如何工作的。
If语句的条件是一个逻辑表达式(也叫布尔表达式或条件表达式),其值要么为真,要么为假。如果条件为真,程序会执行If和EndIf之间的语句(这部分称为If语句的主体)。但如果条件为假,语句块中的语句将被跳过。无论条件是否为真,程序都会执行EndIf之后的语句。

图 8-1:If/Then/EndIf 块的流程图
提示
你可以把 If 语句看作是程序流程中的一个绕行,它就像是一个可选的过山车回路。
现实世界中的布尔值
布尔(Boolean)这个词是为了纪念乔治·布尔(George Boole),这位 19 世纪的英国数学家发明了一种基于两种值(1 和 0,或者真和假)的逻辑系统。布尔代数最终成为现代计算机科学的基础。
在现实生活中,我们常常使用布尔表达式来做决定。计算机也使用它们来确定程序的执行路径。比如,当你在商场刷卡时,远程服务器可能会根据你的信用卡是否有效(真)或者无效(假)来授予或拒绝访问权限。车辆中的计算机会在判断发生碰撞时自动弹出安全气囊(collision = true)。当手机电池电量低时,可能会显示警告图标(batteryLow = true),而当电池电量恢复正常时,图标会消失(batteryLow = false)。
这些只是计算机通过检查布尔条件结果来执行不同操作的几个例子。
你可以使用关系运算符测试各种条件,接下来我们将讨论这些运算符。
关系运算符
Listing 8-1 中的条件(status = 1)测试变量status是否等于 1。我们把这里的等号称为关系运算符(或比较运算符),因为它测试两个值(或表达式)之间的关系。Small Basic 支持另外五个关系运算符,你可以在条件中使用它们。表 8-1 展示了这些关系运算符。
表 8-1: 小基础中的关系运算符
| 运算符 | 含义 | 数学符号 |
|---|---|---|
= |
等于 | = |
< |
小于 | < |
<= |
小于或等于 | ≤ |
> |
大于 | > |
>= |
大于或等于 | ≥ |
<> |
不等于 | ≠ |
让我们看几个简短的示例,看看这些运算符是如何工作的。很多人都想参加《与星共舞》。你被雇佣编写一个申请表,潜在的舞者会填写。一个要求是申请人必须至少 18 岁。你如何在程序中检查这个条件?
好吧,这很简单。你可以写出类似这样的代码:
TextWindow.Write("How old are you? ")
age = TextWindow.ReadNumber()
If (age < 18) Then
TextWindow.WriteLine("Sorry! You're not old enough!")
EndIf
If 条件检查 age 是否小于 18。如果是,申请人不够年龄,他们与明星共舞的梦想就此结束。好样的,小舞者!
另一种检查申请人年龄的方法如下:
If (age >= 18) Then
TextWindow.WriteLine("So far so good. You may have a chance!")
EndIf
If 条件检查 age 是否大于或等于 18。如果条件为真,申请人通过了这一条件,仍然有机会与明星共舞。
但如果申请人还需要恰好有 9 年的舞蹈经验呢?(别问为什么!)你可以写出类似这样的代码:
TextWindow.Write("How many years of dancing experience do you have? ")
experience = TextWindow.ReadNumber()
If (experience <> 9) Then
TextWindow.WriteLine("Sorry! You don't have the required experience.")
EndIf
注意,If 条件使用了不等于(<>)运算符。如果申请人输入的不是 9,那么这个舞者的游戏就结束了!
尝试 8-1
圣诞老人希望更高效地送礼物。为了避免从烟囱爬下来,他决定从雪橇上将礼物掉入烟囱。他需要一个程序,输入雪橇当前的高度(以米为单位),然后计算礼物掉到烟囱的时间(以秒为单位)。公式如下:

程序必须在计算时间之前检查圣诞老人输入的高度是否为正数。运行以下程序两次。第一次输入一个正高度,第二次输入一个负高度。解释每种情况下发生了什么。
TextWindow.Write("Please enter the height (meters): ")
height = TextWindow.ReadNumber()
If (height > 0) Then
time = Math.SquareRoot(10 * height / 49)
time = Math.Round(time * 100) / 100 ' Rounds to 2 decimal places
TextWindow.WriteLine("Fall time = " + time + " sec. ")
EndIf
复杂的 If 条件
和算术运算符一样,关系运算符也需要两个操作数,分别位于两边。这些操作数可以是简单的,使用变量和常量,也可以是复杂的数学表达式。例如,如果你想检查是否有足够的钱来购买两张大披萨并支付 5 美元的小费,可以输入如下内容:
If (myMoney >= (2 * pizzaPrice + 5)) Then
小基础首先计算 2 * pizzaPrice + 5 的值(使用当前的 pizzaPrice 值)。然后将结果与当前的 myMoney 值进行比较,以查看 If 条件是否为真或假。
你还可以使用任何在 If 条件中返回值的方法。例如,如果你制作一个披萨外送视频游戏,并希望在玩家的得分达到 100、200、300 等时给予玩家一条额外生命,你可以输入如下内容:
If (Math.Remainder(score, 100) = 0) Then
该条件检查当前得分 score 除以 100 后的余数。如果余数为 0,If 条件为真,玩家获得了他们应得的额外生命。
实践练习 8-2
将以下每个语句转换为逻辑表达式,然后检查条件是否为真或假。假设 x = 4 且 y = 5。
-
x 和 3 的和小于 8。
-
x 除以 3 的余数是 2。
-
x² 和 y² 的和大于或等于 40。
-
x 可以被 2 整除。
-
x 和 y 的最小值小于或等于 10。
比较字符串
我们刚刚展示了如何使用关系运算符比较数字,但在某些应用中,你需要比较字符串。例如,你可能需要检查用户是否输入了正确的密码,或者他们是否在猜词游戏中猜对了单词。
你可以使用 =(等于)或 <>(不等于)运算符来测试两个字符串是否相同。示例 8-2 让用户猜测秘密密码。
1 ' SecretCode.sb
2 TextWindow.Write("Guess the secret code! ")
3 guess = TextWindow.Read()
4 If (guess = "Pizza rules!") Then
5 TextWindow.WriteLine("You're right!")
6 EndIf
7 TextWindow.WriteLine("Goodbye!")
示例 8-2:在 Small Basic 中比较字符串
多次运行此程序,并尝试几种不同的猜测。例如,尝试输入 pizza rules!(使用小写 p)。会发生什么?再运行一次程序,这次输入 Pizza rules!(使用大写 P)。这次成功了吗?没错!原因是,当你比较字符串时,它们必须完全匹配。所有的大小写、空格和标点符号必须完全一致。
请注意,其他关系运算符(<, <=, >, 和 >=)不能与字符串一起使用。如果你将这些运算符与非数字字符串一起使用,结果总是为假。
If/Else 语句
你妈妈再次打电话给你,并给你更多的指示:“还有一件事!如果 Captain Snarf’s 关门了,请顺便去 LongLine Grocery 买一个冷冻比萨。”现在你可以在 Small Basic 中使用 If/Else 语句来帮助你了!
If/Else 语句(也称为 二选一 If 语句)让你在条件为真时采取一种行动,在条件为假时采取另一种行动。图 8-2 演示了这个语句的工作原理。

图 8-2: If/Else 语句的流程图
如果条件为真,Small Basic 将执行 If 块中的语句(位于 If 和 Else 关键字之间)。如果条件为假,Small Basic 将执行 Else 块中的语句(位于 Else 和 EndIf 关键字之间)。因此,Small Basic 只会执行两个块中的一个(If 块或 Else 块)。
你可以按照 示例 8-3 中所示编写你妈妈的新指示。
1 ' SnarfPizza2.sb
2 TextWindow.WriteLine("Is Snarf's Pizza open?")
3 TextWindow.Write("Enter 1 (for open) or 2 (for closed): ")
4 status = TextWindow.ReadNumber()
5
6 If (status = 1) Then
7 TextWindow.WriteLine("You bought a delicious pizza!")
8 Else
9 TextWindow.WriteLine("You got a frozen pizza!")
10 EndIf
11 TextWindow.WriteLine("Time to go home!")
示例 8-3:演示 If/Else 语句
如果 status = 1,意味着 Captain Snarf’s 是开放的,你将买一个美味的比萨并回家。但如果 status 不是 1(Captain Snarf’s 没有开门),你将从 LongLine Grocery 买一个冷冻比萨并回家。
你妈妈的指令假设 LongLine 总是开着,并且你能找到你需要的东西。但如果杂货店没有冷冻披萨了呢?敬请关注;你可能会接到妈妈的另一个电话,给你新的指令!
尝试 8-3
完成以下程序,创建一个脑筋急转弯测验。这个程序会用它的答案让你吃惊。一定要在展示正确答案时发挥创意!
' Asks first question
TextWindow.Write("If you take 2 apples from 3 apples, how many apples
do you have? ")
ans = TextWindow.ReadNumber()
If (ans = 2) Then
TextWindow.Write("Correct. ")
Else
TextWindow.Write("Nope. ")
EndIf
TextWindow.WriteLine("If you take 2 apples, then you have 2 apples!")
TextWindow.WriteLine("")
' Ask more fun questions here
这里有一些问题建议,你可以添加:
-
一个深 1 英尺、宽 1 英尺的坑里有多少英寸的土壤?
(答案:0。显示:坑里没有土壤!)
-
一吨金子比一吨羽毛重吗?(是或否)
(答案:不。显示:任何东西一吨就是一吨!)
-
一打有多少个 4 分钱邮票?
(答案:12。显示:一打总是有 12 个!)
嵌套的 If 和 If/Else 语句
你在If(或Else)块中的语句可以是任何类型的小基本语句,包括另一个If或If/Else语句。在另一个语句中写If(或If/Else)语句会创建一个嵌套的 If 语句(见图 8-3)。内层的If语句也可以包含其他的If或If/Else语句,嵌套可以继续到你想要的任何层级。但要小心,不要嵌套太多层次,否则你会迷失在所有的层次中,可能会觉得自己像超级马里奥掉进了无尽的深坑!
当你需要对同一个变量进行多次检查或需要测试多个条件时,你可以使用嵌套的If语句。让我们来看一个示例,使用嵌套的If/Else块来测试多个条件。

图 8-3:演示嵌套的 If 和 If/Else 语句
挂掉电话后,你妈妈觉得 LongLine Grocery 可能没有冷冻披萨了。所以她再次打电话给你,说:“听着,如果 Captain Snarf’s 关门了,而且 LongLine 没有冷冻披萨,那么就买一袋冷冻鸡翅。”列表 8-4 展示了如何将这些指令转化为代码。
1 ' SnarfPizza3.sb
2 TextWindow.WriteLine("Is Snarf's Pizza Open?")
3 TextWindow.Write("Enter 1 (for open) or 2 (for closed): ")
4 status = TextWindow.ReadNumber()
5
6 If (status = 1) Then ' Snarf's is open
7 TextWindow.WriteLine("You bought a delicious pizza!")
8 Else ' Snarf's is closed, so you'll go to LongLine
9 TextWindow.WriteLine("Snarf's is closed. Try LongLine Grocery.")
10 hasPizza = Math.GetRandomNumber(2) ' Checks your luck
11 If (hasPizza = 1) Then
12 TextWindow.WriteLine("You got a frozen pizza!")
13 Else
14 TextWindow.WriteLine("You got a bag of frozen chicken wings!")
15 EndIf
16 EndIf
17 TextWindow.WriteLine("Time to go home!")
列表 8-4:演示嵌套的 If 条件
就是这个——一个嵌套的If/Else语句!如果 Captain Snarf’s 关门了,你就运行一个嵌套的If/Else语句来决定从杂货店买什么。第 10 行将变量hasPizza随机设置为 1 或 2。1 表示 LongLine 仍然有冷冻披萨,而 2 表示杂货店没有了。多次运行这个程序,看看今晚你会买什么晚餐。
等等,你妈妈刚刚意识到你可能没有钱,于是她打电话回来:“抱歉,我忘了告诉你。如果你没有足够的钱,就去史蒂夫家吃饭!”现在我们需要再添加一个嵌套层级。列表 8-5 展示了如何处理这种情况。
1 ' SnarfPizza4.sb
2 TextWindow.Write("How many dollars do you have? ")
3 myMoney = TextWindow.ReadNumber()
4
5 If (myMoney >= 25) Then ' I have enough money
6 TextWindow.WriteLine("Is Snarf's Pizza Open?")
7 TextWindow.Write("Enter 1 (for open) or 2 (for closed): ")
8 status = TextWindow.ReadNumber()
9
10 If (status = 1) Then ' Snarf's is open
11 TextWindow.WriteLine("You bought a delicious pizza!")
12 Else ' Snarf's is closed, so you'll go to LongLine
13 TextWindow.WriteLine("Snarf's is closed. Try LongLine Grocery.")
14 hasPizza = Math.GetRandomNumber(2) ' Checks your luck
15 If (hasPizza = 1) Then
16 TextWindow.WriteLine("You got a frozen pizza!")
17 Else
18 TextWindow.WriteLine("You got a bag of frozen chicken wings!")
19 EndIf
20 EndIf
21 Else ' I don't have enough money
22 TextWindow.Write("Go to Steve's house for dinner ")
23 TextWindow.WriteLine("(it's earthworm pizza night).")
24 EndIf
25 TextWindow.WriteLine("Time to go home!")
列表 8-5:更多的嵌套层级
如你所见,你在程序中做决策的方式与现实生活中做决策的方式是一样的!
动手试试 8-4
修改以下程序,使其开始时读取用户输入的 x 和 y 的值。修改输出消息,让用户发笑!
If (x > 5) Then
If (y > 5) Then
TextWindow.WriteLine("The skylight is falling!")
Else
TextWindow.WriteLine("Now it's time to play the piper!")
EndIf
Else
TextWindow.WriteLine("I'll huff, puff, and blow $5 on tacos!")
EndIf
Goto 语句
Goto 语句通过让你跳转到程序中较早或较晚的某个语句来改变程序的流程。请查看 Mark 和 Andy 在清单 8-6 中的烦人对话。
1 ' GotoDemo.sb
2 Again:
3 TextWindow.Write("Mark: Pete and Repeat were in a boat. ")
4 TextWindow.WriteLine("Pete fell out, who was left?")
5 TextWindow.WriteLine("Andy: Repeat.")
6 TextWindow.WriteLine("")
7 Program.Delay(1000) ' Waits 1 sec to slow the program down
8 Goto Again
清单 8-6:一个无尽的 Goto 循环
第 2 行的语句被称为标签;它用于标识程序中的特定行。标签以冒号结尾,你可以将它们放置在程序的任何地方。
程序随后运行第 3 行到第 7 行。当它到达第 8 行时,它会返回到第 2 行(跳转到 Again 标签),然后 Small Basic 会再次执行第 3 行到第 7 行。循环指的是运行相同代码块多次,这个循环会永无止境(就像永无止境的歌和《Barney 歌》)。运行此程序查看其输出(并尽量把那些歌曲从脑海中赶出去;哈哈)。
Goto 语句是一种无条件跳转(或无条件转移)语句,因为程序会无条件地跳转到 Goto 标签指定的位置(不问任何问题)。而 If/Then 语句则是一种条件转移语句,因为程序只有在满足某个条件时才会改变其正常流程。
大多数程序员建议你不要使用 Goto 语句,因为它们会把程序变成意大利面条代码——那种混乱复杂到没人能理解的代码!但有时候 Goto 语句会非常有用,了解它在什么时候可能派上用场是很有帮助的。
Goto 的一个常见用法是检查用户输入的数据,确保其正确,如清单 8-7 所示。
1 ' VaildateWithGoto.sb
2 TryAgain:
3 TextWindow.Write("Enter a positive number: ")
4 num = TextWindow.ReadNumber()
5 If (num <= 0) Then
6 Goto TryAgain
7 EndIf
8 TextWindow.Write("You entered: " + num)
清单 8-7:使用 Goto 来检查用户输入
这段代码要求用户输入一个正数(第 3 行),并将输入读取到 num 变量中(第 4 行)。如果用户输入的数字不是正数(第 5 行),Goto 语句会将程序跳转回 TryAgain 标签,要求用户重新输入。如果输入的是正数,程序继续执行第 8 行的语句。你将在第十四章中学习另一种使用 While 循环检查用户输入的方法。
动手试试 8-5
我们(本书的作者)计划使用以下程序来衡量读者的满意度。你认为这样公平吗?我们觉得是!重写它并使其更具个人化。然后让别人参与你的调查!
Again:
TextWindow.Write("How many stars do you give this book [1-5]? ")
ans = TextWindow.ReadNumber()
ans = Math.Floor(ans) ' In case the user typed a decimal
If (ans <> 5) Then
TextWindow.Write("Invalid number! Please enter an integer. ")
TextWindow.WriteLine("That's greater than 4 but less than 6.")
Goto Again
EndIf
TextWindow.WriteLine("Wow! Thank you. You made our day!")
编程挑战
如果你遇到困难,请查看 nostarch.com/smallbasic/ 获取解决方案以及更多的资源和教师与学生的复习问题。
-
以下程序创建了一个简单的掷硬币游戏,用户需要掷硬币并输入 h(代表正面)或 t(代表反面)。根据用户的输入,程序会显示不同的信息。你认为计算机在玩一个公平的游戏吗?看看你能否找个家人或朋友一起玩这个不公平的掷硬币游戏!
TextWindow.Write("Toss a coin. Heads(h) or Tails(t)? ") ans = TextWindow.Read() If (ans = "h") Then TextWindow.WriteLine("I won. I'm the champion!") Else TextWindow.WriteLine("You lost. Cry home to Momma.") EndIf -
詹姆斯·P·科克船长正在驾驶“世纪鹰”号企业级星际飞船。他截获了来自敌人克林戈夫(Clingoffs)的消息,并需要你的帮助破解密码!这条消息包含数百万组三位数字;每组数字都需要排序后重新输入才能理解消息。编写一个程序,要求用户输入三个数字,然后将这些数字按从小到大的顺序显示给科克船长。我们已经为你写好了排序逻辑,但你需要编写用户输入部分。打开本章文件夹中的 CaptainCork_Incomplete.sb 文件,按照注释完成这个应用程序,帮助科克船长阻止邪恶的克林戈夫!
-
你正在启动一个名为“罐装泥土”的新业务。你有泥土,而人们需要它,那为什么不把它装进罐子里呢?编写一个程序,让客户输入罐子的高度和半径。程序应计算罐子的体积(以确定该放入多少泥土)。如果用户输入了负值的高度或半径,程序应显示适当的错误信息。
-
如同童话故事所述,鲁姆普尔斯蒂尔茨金帮助一位妇女将稻草纺成金子。作为交换,她答应将第一个孩子交给他。当孩子出生时,妇女拒绝交出孩子。鲁姆普尔斯蒂尔茨金同意,如果妇女能在三天内猜出他的名字,他就放弃对孩子的要求。编写一个程序,提示妇女输入她的猜测,并检查她的猜测是否正确。以下是程序的示例运行:
What is my name? Paul No! Your child will be mine! Mwahaha! What is my name? Peter No! Your child will be mine! Mwahaha! What is my name? Rumpelstiltskin Correct. You can keep the child. She's a brat anyway!
第十章:9
使用决策来制作游戏

有时候决策是复杂的。假设一个男孩和一个女孩想去看电影。她想看一部动作片,但他想看一部喜剧片。如果这部喜剧片有动作元素,且评价很好,并且有她喜欢的女演员,她愿意去看。但这部电影必须在晚上 10 点之前开始,并且必须在距离他们正在用餐的餐厅 10 英里的范围内。想象一下,要做出这样的决策,代码会是什么样子的!
在本章中,我们将继续探讨决策相关的话题,并介绍一些新的语句。我们将首先介绍If/ElseIf语句,并展示它如何简化编写嵌套If语句的过程。接着,你将学习逻辑运算符And和Or,它们可以让你在If语句中做更多的操作。我们还将介绍Shapes对象,让你更加熟悉图形的操作。最后,你将通过构建一个叫做“猜我的坐标!”的游戏,将所有这些新知识付诸实践!
If/ElseIf 阶梯
这个消息已经在新闻中报道了!外星怪物弗兰科已经逃脱了看守。幸运的是,当你看到他在邻里攻击人们时,你正好带着激光枪。你瞄准并开火。运行清单 9-1 中的程序,看看接下来会发生什么!
1 ' AlienAttack.sb
2 TextWindow.Write("A salivating alien monster approaches. ")
3 TextWindow.WriteLine("Press any key to shoot...")
4 TextWindow.PauseWithoutMessage()
5
6 damage = Math.GetRandomNumber(5) ' Randomly picks an outcome
7 If (damage = 1) Then
8 TextWindow.Write("Wow! You got him. ")
9 TextWindow.WriteLine("Now you can watch SpongeBob!")
10 ElseIf (damage = 2) Then
11 TextWindow.Write("You injured him. ")
12 TextWindow.WriteLine("He wants a Band-aid.")
13 ElseIf (damage = 3) Then
14 TextWindow.Write("Weak shot. Run for your life! ")
15 TextWindow.WriteLine("Now dance! You'll confuse him.")
16 Else
17 TextWindow.Write("You missed! He got you. ")
18 TextWindow.WriteLine("You should stick to video games.")
19 EndIf
清单 9-1:攀登 If/ElseIf 阶梯
程序从 1 到 5 之间随机选择一个数字(第 6 行),然后检查该数字来决定外星人的命运。第 7 到第 19 行构成了If/ElseIf阶梯,这是用来构建一连串If语句的常见方法。它的一般形式在图 9-1 中有所展示。
从第一个语句开始,程序会依次检查每个测试条件。一旦找到一个条件为真的语句,它会执行该语句,并跳到EndIf之后的语句,跳过阶梯中的其他部分。如果没有任何条件为真,程序会执行Else子句中的语句,然后继续执行EndIf之后的语句。
这就是为什么最终的Else语句通常被称为默认情况。如果你在阶梯中不包括最后的Else语句,且所有测试条件都为假,If/ElseIf阶梯将不会执行任何操作,程序将继续执行EndIf关键字之后的语句。

图 9-1: If/ElseIf 阶梯的结构
让我们看看另一种使用If/ElseIf阶梯的方法。
字母成绩
在这个例子中,你将创建一个程序,读取一个 0 到 100 之间的测试分数,并从表 9-1 中显示相应的字母成绩。
表 9-1: 字母成绩评分
| 分数 | 字母成绩 |
|---|---|
| 分数 ≥ 90 | A |
| 80 ≤ 分数 < 90 | B |
| 70 ≤ 分数 < 80 | C |
| 60 ≤ 分数 < 70 | D |
| 分数 < 60 | F |
完整的程序可以在清单 9-2 中查看。
1 ' GradeLetter.sb
2 TextWindow.Write("Enter the score: ")
3 score = TextWindow.ReadNumber()
4 If (score >= 90) Then
5 grade = "A"
6 ElseIf (score >= 80) Then
7 grade = "B"
8 ElseIf (score >= 70) Then
9 grade = "C"
10 ElseIf (score >= 60) Then
11 grade = "D"
12 Else
13 grade = "F"
14 EndIf
15 TextWindow.WriteLine("The grade is " + grade)
清单 9-2:评分作业
尝试运行程序并输入一些数字查看结果。以下是一些输出示例:
Enter the score: 90
The grade is A
Enter the score: 72
The grade is C
这个程序使用If/ElseIf阶梯来测试输入的成绩。让我们一步步地走过这个程序的工作原理。
程序测试第一个条件score >= 90是否为真(第 4 行)。如果为真,grade设置为A,程序跳转到第 15 行。
如果不成立,score一定小于 90,程序会检查下一个条件score >= 80,即第 6 行的条件。如果这个条件为真(意味着score大于或等于 80 但小于 90),grade将被设置为B,程序跳转到第 15 行。
如果条件不成立,那么score一定小于 80,程序会检查第 8 行的条件score >= 70。如果这个条件成立(意味着score大于或等于 70 但小于 80),grade将被设置为C,程序跳转到第 15 行。
如果这个条件也不成立,那么score一定小于 70。此时,程序会检查第 10 行的条件score >= 60。如果此条件为真(意味着score大于或等于 60 但小于 70),grade会被设置为D,程序跳转到第 15 行。
最后,如果最后一个条件仍然不成立,score一定小于 60。在这种情况下,程序不检查任何条件,grade被设置为F,程序跳转到第 15 行。
阶梯上的错误
当你编写If/ElseIf阶梯时,条件语句的顺序非常重要。在测试条件时,务必小心顺序。例如,回到列表 9-2,并将第 4 到第 7 行替换为以下代码:
If (score >= 80) Then
grade = "B"
ElseIf (score >= 90) Then
grade = "A"
这个程序的更改意味着你首先检查条件score >= 80,而不是score >= 90。现在,如果用户输入 95,程序首先测试第一个条件,看到score >= 80为真,并将grade设置为B。在这段代码中,grade永远不会被设置为A,无论score的值有多高。没有人能得A!当程序在这个If/ElseIf阶梯中找到一个为真的条件时,它会跳过所有其他语句,直接跳到EndIf。
为了避免这个问题,确保If/ElseIf阶梯中的条件顺序正确。你可能永远不想先检查中间值。此外,在用户之前一定要多次运行程序,测试值并发现任何问题。
试试 9-1
在列表 9-2 中,你从检查条件score >= 90开始。你也可以从检查最后一个条件score < 60开始,然后是60 <= score < 70,接着是70 <= score < 80,依此类推。使用这种反向检查顺序重写程序。
让我们来看看逻辑
有时你可能需要检查多个条件来决定是否执行某个语句。例如,你可能只有在狗既大又经过厕所训练且有三只头时,才会收养它。测试多个条件的一种方法是像在前几章中那样嵌套If和If/Else语句。另一种方法是使用逻辑运算符(也叫布尔运算符)。使用逻辑运算符,你可以编写结合两个或更多逻辑表达式的测试条件。让我们看看怎么做。
你还记得在小学数学课上学过的像 5 < x < 10 这样的不等式吗?这个表达式描述了一个数字 x,它大于 5 且小于 10。图 9-2 向你展示了如何在 Small Basic 中写出这个表达式。

图 9-2:Small Basic 中的复合条件
这是一个由两个逻辑表达式 x > 5 和 x < 10 组成的复合条件,你使用逻辑运算符 And 将它们结合起来。为了使这个复合条件为真,两个表达式必须都为真。
Small Basic 支持两个逻辑运算符:And 和 Or。图 9-3 描述了它们的工作原理。

图 9-3:解释逻辑运算符 And 和 Or
接下来,我们将更详细地解释这些运算符。
动物园中的逻辑运算符
看一下图 9-4,并回答这个问题:猴子该如何得到香蕉?没错:门 1 和 门 2 和 门 3 必须都打开。如果三扇门中有任何一扇关着,可怜的猴子就无法得到香蕉了!

图 9-4:使用 And 运算符进行逻辑运算
现在看一下图 9-5。在这个例子中,猴子只需要一扇门打开:门 1 或 门 2 或 门 3。这个猴子对它的机会很有信心!

图 9-5:使用 Or 运算符进行逻辑运算
在图 9-6 中,猴子有两个选择。

图 9-6:使用 And 和 Or 运算符进行逻辑运算
如果它走上上面的小路,它需要两扇门(门 1 和 门 2)都打开。如果它走下下面的小路,只需要门 3 打开。如果你在编程这个条件,你会这样描述:
((Door1 = open) And (Door2 = open)) Or (Door3 = open)
你准备好练习使用 And 和 Or 运算符了吗?
And 运算符
And 运算符将两个逻辑表达式作为操作数。操作数是指运算符作用的对象。表 9-2(称为真值表)列出了 And 运算符对其两个操作数 X 和 Y 的所有可能组合的输出。
表 9-2: And 运算符的真值表
如果 X 是 |
如果 Y 是 |
那么 (X 和 Y) 是 |
|---|---|---|
"真" |
"真" |
"真" |
"真" |
"假" |
"假" |
"假" |
"真" |
"假" |
"假" |
"假" |
"假" |
如果 X 和 Y 都为真,则 X And Y 也为真。但如果其中一个操作数为假,则 X And Y 也为假。
列表 9-3 显示了使用 And 运算符结合的两个条件(gameLevel = 1 和 score > 100)。当这两个条件都为真时,会显示消息 You get 200 bonus points!。
1 ' AndDemo.sb
2 TextWindow.Write("Game level: ")
3 gameLevel = TextWindow.ReadNumber()
4
5 TextWindow.Write("Score.....: ")
6 score = TextWindow.ReadNumber()
7
8 If ((gameLevel = 1) And (score > 100)) Then
9 TextWindow.WriteLine("You get 200 bonus points!")
10 EndIf
列表 9-3: And 运算符
只有当 gameLevel 等于 1 且 score 大于 100 时,If 块中的语句(第 9 行)才会执行。如果这两个条件中有一个为假,那么整个条件就是假的,Small Basic 将不会在第 9 行执行 WriteLine() 方法。
你可以通过将第 8 到第 10 行替换为以下嵌套的 If 语句来执行相同的检查:
If (gameLevel = 1) Then
If (score > 100) Then
TextWindow.WriteLine("You get 200 bonus points!")
EndIf
EndIf
你看到 And 运算符是如何更简洁地测试多个条件的吗?嵌套的 If 语句需要五行代码,但使用 And,你只需三行代码就能做到同样的事情!
Or 运算符
你喜欢你的比萨吗?你可能只想吃有四种肉或者外皮是黏糊糊的比萨。当你有多个条件时,如果只有一个条件需要为真,Or 运算符就派上用场了。请查看 表 9-3 中 Or 运算符的真值表。
表 9-3: Or 运算符的真值表
如果 X 为 |
如果 Y 为 |
那么 (X Or Y) 为 |
|---|---|---|
"True" |
"True" |
"True" |
"True" |
"False" |
"True" |
"False" |
"True" |
"True" |
"False" |
"False" |
"False" |
如果两个操作数中有一个为真,或者它们都为真,那么组合的逻辑表达式为真。只有当两个操作数都为假时,逻辑表达式才为假。
列表 9-4 显示了使用 Or 运算符的一个例子。目标是在没有时间继续游戏时(timeLeft = 0)或者玩家失去所有能量时(energyLevel = 0),结束游戏。
1 ' OrDemo.sb
2 TextWindow.Write("Time left: ")
3 timeLeft = TextWindow.ReadNumber()
4
5 TextWindow.Write("Energy level: ")
6 energyLevel = TextWindow.ReadNumber()
7
8 If ((timeLeft = 0) Or (energyLevel = 0)) Then
9 TextWindow.WriteLine("Game Over!")
10 EndIf
列表 9-4: Or 运算符
如果 timeLeft 为 0 或 energyLevel 为 0,Small Basic 会执行 If 块中的命令(第 9 行)。多次运行此程序,使用不同的输入来确保你理解 Or 运算符的工作原理。
你可以使用嵌套的 If 语句来做同样的事情。例如,你可以将第 8 到第 10 行替换为以下代码:
If (timeLeft = 0) Then
TextWindow.WriteLine("Game Over!")
Else
If (energyLevel = 0) Then
TextWindow.WriteLine("Game Over!")
EndIf
EndIf
然而,正如你所看到的,使用嵌套的 If 语句需要七行代码,而使用 Or 只需要三行!使用 Or 运算符是一种更简洁的方式来测试多个条件。
评估的宇宙顺序
看一下下面的条件。Small Basic 如何评估这个表达式?
If (A = 1 Or B = 1 And C = 1) Then
事实证明,Small Basic 给 And 运算符的优先级高于 Or。这意味着它首先会找到 B = 1 And C = 1,然后将结果作为 Or 表达式的右操作数。要改变运算顺序,可以使用括号,例如:
If ((A = 1 Or B = 1) And C = 1) Then
这段代码首先计算 A = 1 Or B = 1,然后将结果作为 And 表达式的左操作数。我们建议你使用括号,以避免任何混淆!
注意
逻辑运算符,如 And 和 Or 会在 任何算术运算符(+、-、*、/)和关系运算符(=、<、<=、>、>=、<>)的组合表达式之后进行计算。在逻辑运算符中, And 优先于 Or ;使用括号可以改变顺序,使代码更易读。
是时候应用你所学的所有决策信息,并构建一些令人兴奋的应用程序了。但在此之前,我们需要介绍一个新的 Small Basic 对象——Shapes 对象,它允许你使用丰富的图形来构建应用程序。让我们制作一些漂亮的图形吧!
动手试一试 9-2
打开本章文件夹中的 DiceGame_Incomplete.sb 文件,并编写缺失的代码来完成这个游戏。玩家输入他们的赌注(从 $1 到 $10),然后掷一对骰子。如果骰子的点数和为 2 或 12,玩家赢得赌注的三倍。如果点数和为 4 或 10,玩家赢得赌注的两倍。如果点数和为 7 或 11,玩家输掉赌注。否则,玩家的余额不变,玩家继续掷骰子。
Shapes 对象
在 第三章 中,你学会了如何在图形窗口中绘制各种形状和图像。但那些形状是固定的:一旦你在某个位置绘制了形状,唯一移动它到另一个位置的方法就是清空整个窗口并重新绘制该形状。如果你需要在程序中移动一些形状(比如当玩家按下一个键时移动一个角色),最好使用 Shapes 对象。
Shapes 对象让你在图形窗口中添加、移动和旋转图形。运行这段代码来绘制一个矩形:
rectID = Shapes.AddRectangle(100, 50)
Program.Delay(1000)
Shapes.Move(rectID, 400, 200)
程序调用 AddRectangle() 来添加一个 100×50 的矩形,并将创建的图形标识符保存到 rectID 中。创建的矩形默认出现在图形窗口的左上角。第二条语句使程序暂停 1 秒,以便你查看矩形的初始位置。第三条语句调用 Move() 来移动该矩形,使其左上角位于 (400, 200) 处。请注意,rectID 被作为 Move() 的第一个参数传入,以便它知道要移动的是哪个图形。
把 Shapes 对象当作一个“形状工厂”——一个制造线条、三角形、矩形、椭圆以及其他形状的工厂。当你请求它创建一个新的形状时,它会制造该形状并返回一个标识符。每当你想对已创建的形状进行操作时,你都需要将这个标识符传递给 Shapes 对象(作为你调用的方法的参数)。
我们不会在这里涵盖 Shapes 对象的所有方法。相反,我们会讨论你在下一个程序中将使用到的方法。随着你阅读本书,你会学习到其他方法。
我们现在要使用的两个方法是AddImage()和Move()。要了解这些方法是如何工作的,请打开本章文件夹中的ImageDemo.sb文件。你将看到清单 9-5 中展示的代码,该代码用于移动图像。
1 ' ImageDemo.sb
2 path = Program.Directory + "\Flower.png"
3 imgID = Shapes.AddImage(path)
4 Shapes.Move(imgID, 60, 20)
清单 9-5:使用Shapes对象移动图像
点击运行按钮。该程序的输出如图 9-7 所示(我们添加了网格线和数字,以展示代码的工作原理)。

图 9-7:移动花朵图像
假设这个程序被保存到C:\Book\Ch09\ImageDemo。imageDemo文件夹中还包含Flower.png图像文件。Program.Directory属性(第 2 行)指向目录C:\Book\Ch09\ImageDemo,该目录包含可执行程序(.exe文件)。第 2 行使用+符号将两件事附加到目录中:一个斜杠(\)和图像文件名(Flower.png)。当程序运行第 2 行时,path变量会被赋予完整的文件路径(C:\Book\Ch09\ImageDemo\Flower.png)。
第 3 行调用了AddImage()方法,并将path变量作为参数传递。该方法从文件加载图像并返回加载图像的标识符;该标识符保存在名为imgID的变量中。标识符就像是Shapes对象用来跟踪它创建的形状的标签(例如,"Image1"、"Rectangle3"、"Line100"等)。加载的图像显示在图形窗口的左上角。
第 4 行调用了Move()方法来移动图像。第一个参数是形状的标识符,它是程序从AddImage()获取并保存在imgID(第 3 行)中的。其他两个参数是新位置的左上角坐标。图 9-7 显示了花朵图像,其左上角位于(60, 20)。
Flower.png图像的宽度为 100 像素,高度为 140 像素。如果你想将图像移动到其中心位于(100, 100),你可以这样写:
Shapes.Move(imgID, 100 - 50, 100 - 70)
因为你想让图像的中心位于(100, 100),你需要减去图像宽度的一半(50)来水平居中,减去图像高度的一半(70)来垂直居中。
这是你在下一节构建应用程序时需要学习的有关Shapes对象的所有信息。现在是时候制作一个猜谜游戏了!
动手练习 9-3
使用类似以下的代码,在你的计算机上指向一张小图像,并在图形窗口中显示它:
imgID = Shapes.AddImage("C:\Temp\icon.png")
Shapes.Move(imgID, 40, 60)
更新路径为你图像的正确路径。更改第二条语句,将图像移动到以下位置:(100, 40)、(10, 10)、(27, 78),然后将其居中于图形窗口。
创建一个游戏:猜我的坐标
游戏开始了!在本节中,你将开发一个互动游戏,叫做“猜我的坐标”,它测试玩家对笛卡尔坐标系统的知识,或者说是他们读取 x、y 坐标图的能力。游戏展示一个代表笛卡尔网格上某一点的星星;图 9-8 展示了界面的样子。在每一轮游戏中,星星会移动到一个随机位置,并要求玩家猜测它的 x 和 y 坐标。游戏会检查玩家的答案,并显示反馈信息。它像是战舰游戏,但对于数学迷来说更有趣!
该游戏同时使用图形窗口和文本窗口。图形窗口显示网格和星星,文本窗口读取玩家的答案并显示程序的反馈信息。现在,我们将一步步带你完成创建这个游戏的过程。

图 9-8:猜测我的坐标游戏的用户界面
步骤 1:打开启动文件
从本章文件夹中打开GuessMyCoordinate_Incomplete.sb文件开始。该文件仅包含注释,你将一步一步地添加代码。
本章文件夹中还包含你将使用的两张图片(Grid.png和Star.png)。Grid.png是一个 480×360 的笛卡尔网格图像,而Star.png是一个 24×24 的星星图像。
注意
如果你遇到任何问题,可以查看本章文件夹中包含的完成版程序(GuessMyCoordinates.sb),看看你做错了什么。
步骤 2:设置游戏
在清单 9-6 中输入代码以设置游戏的用户界面。该代码应放在文件的开头。
1 GraphicsWindow.Title = "Guess My Coordinates"
2 GraphicsWindow.CanResize = "False"
3 GraphicsWindow.Width = 480 ' Same as background image
4 GraphicsWindow.Height = 360 ' Same as background image
5 GraphicsWindow.Top = 200 ' Position on your desktop
6 GraphicsWindow.Left = 50 ' Position on your desktop
7 TextWindow.Title = "Guess My Coordinates"
8 TextWindow.Top = GraphicsWindow.Top
9 TextWindow.Left = GraphicsWindow.Left + GraphicsWindow.Width + 15
10
11 path = Program.Directory ' Program's directory
12 bkgnd = Shapes.AddImage(path + "\Grid.png") ' Bkgnd (480 x 360)
13 star = Shapes.AddImage(path + "\Star.png") ' Star image (24 x 24)
14
15 While ("True") ' Runs forever
16 ' You'll add code from Listings 9-7 and 9-8 here
17 EndWhile
清单 9-6:设置游戏
第 1 行到第 6 行设置图形窗口的标题、大小和位置。窗口的大小设置为与网格图像的大小相同(第 3–4 行)。第 7 行到第 9 行设置文本窗口的标题,并将其位置设置在图形窗口的右侧(见图 9-8)。程序接着将程序的目录保存到path中(第 11 行),你将使用它来为两张图片生成完整路径,以便在屏幕上绘制它们。接下来,程序加载这两张图片,并将它们的标识符(由Shapes对象返回)保存在这两个变量中:bkgnd和star(第 12–13 行)。
行 15 和行 17 中的While/EndWhile关键字将在第十四章中详细解释。现在,你只需要知道这些代码创建了一个无限循环(一个会永远重复的循环,像你在第八章中写的 Pete and Repeat 程序,GotoDemo.sb)。你将在这些While/EndWhile关键字之间添加应用程序的其余代码。
测试到目前为止写的代码。你应该能看到两个并排的窗口,就像在图 9-8 中一样。星星图像出现在图形窗口的左上角,但暂时没有任何动作,因为你还没有编写代码来移动它。
现在关闭图形窗口或文本窗口,以便你可以添加剩余的代码。
步骤 3:隐藏星星
在游戏的每一轮中,你将把星星移动到网格上的一个随机位置,然后让玩家猜测其坐标。现在我们来添加代码来移动星星。
将列表 9-7 中的代码添加到 While 循环中(位于列表 9-6 的第 16 行)。
1 ' Finds the star's random position (in grid units)
2 X0 = Math.GetRandomNumber(23) - 12 ' Ranges from -11 to 11
3 Y0 = Math.GetRandomNumber(17) - 9 ' Ranges from -8 to 8
4 pt = "(" + X0 + ", " + Y0 + ")" ' Example: (5, -3)
5
6 ' Sets to pixel units and moves the star to the random position
7 xPos = ((X0 + 12) * 20) - 12 ' Sets 12 pixels to the left
8 yPos = ((9 - Y0) * 20) - 12 ' And 12 pixels up
9 Shapes.Move(star, xPos, yPos) ' Moves the star
列表 9-7:放置星星
在图 9-8 中,你可以看到网格在 x 方向上从 –12 到 12,在 y 方向上从 –9 到 9。如果你将星星放置在网格边界的任何一点,玩家只能看到星星的一部分;网格外部的部分会被裁剪掉。这就是为什么你需要将星星的 x 坐标限制在 [–11, 11] 范围内,y 坐标限制在 [–8, 8] 范围内。
但如何生成一个在 –11 到 11 之间的随机数呢?这很简单!从 –11 到 11 共包含 23 个整数(–11, –10, ..., 10, 11)。如果你调用 GetRandomNumber(23),你会得到一个介于 1 和 23 之间的随机整数。如果你从这个整数中减去 12,结果将是一个介于 –11(1 – 12)和 11(23 – 12)之间的整数,这正是你需要的。接下来,我们将解释代码。
你使用两个变量,X0 和 Y0,来存储星星的随机坐标。在第 2 行,X0 变量被赋予一个在 –11 到 11 之间的随机值,如前所述。在第 3 行,Y0 变量被赋予一个在 –8 到 8 之间的随机数。这些 X0 和 Y0 的随机值告诉你星星落在哪个网格交点上。接下来,程序构建一个名为 pt(表示点的缩写)的字符串,格式为 (X0, Y0)。如果玩家输入错误的答案,这个字符串会显示正确的坐标给玩家。
现在,你需要将星星移动到你刚刚创建的这个新坐标 (X0, Y0)。图 9-9 展示了网格的一部分,以及星星可能被设置的位置示例。如图所示,网格上的每个单位映射到图形窗口中的 20 像素;你可以将其与图 9-8 进行对比,以了解网格的整体缩放比例。

图 9-9:图形窗口中的像素位置的网格坐标
为了将星星移动到一个随机位置,你首先需要将 (X0, Y0) 网格单位(用户在网格图像上看到的内容)转换为 (xPos, yPos) 像素单位(Small Basic 中的坐标)。我们现在就来做这个转换。
如果星星的 x 坐标是–11,你需要在图形窗口的水平位置 20 绘制星星。如果星星的 x 坐标是–10,你需要在水平位置 40 绘制它,依此类推。所以你需要一个公式将星星的 x 坐标X0 = {–11, –10, –9, ..., 0},映射到图形窗口中对应的水平位置xPos = {20, 40, 60, ..., 240}。为此,你需要将 12 加到X0上,得到{1, 2, 3, ..., 12},然后将结果乘以 20。测试一下!当X0 = –11 时,(–11 + 12)× 20 = 20。当X0 = –10 时,(–10 + 12)× 20 = 40,依此类推。这正是你想要的。
对于 y 坐标的映射方式是相同的。如果星星的 y 坐标是 8,你需要在图形窗口的垂直位置 20 绘制它。如果星星的 y 坐标是 7,你需要在垂直位置 40 绘制它,依此类推。所以你需要一个公式将星星的 y 坐标Y0 = {8, 7, 6, ..., 0},映射到图形窗口中对应的垂直位置yPos = {20, 40, 60, ..., 180}。你通过从 9 中减去Y0并将结果乘以 20 来实现这一点。来测试一下吧!当Y0 = 8 时,(9 – 8)× 20 = 20。当Y0 = 7 时,(9 – 7)× 20 = 40,依此类推,这正是你需要的。
你还有一个小细节需要考虑。假设星星的(X0, Y0)坐标是(–10, 2),如图 9-9 所示。你将这些坐标映射到像素,并发现需要在图形窗口中将星星显示在点(xPos, yPos) = (40, 140)的位置。但你需要使星星的中心位于(40, 140)。由于星星图像是 24×24 像素,星星的左边位置必须是 28(40 – 12),星星的顶部位置必须是 128(140 – 12)。这些数字是你需要传递给Move()方法的。换句话说,为了让星星的中心与网格线的交点对齐,你必须从xPos中减去星星的宽度(12 像素),从yPos中减去星星的高度(12 像素)。
在清单 9-7 中,第 7 行找到星星的xPos,第 8 行找到星星在图形窗口中的yPos。然后第 9 行调用Move()方法,将星星放置在网格上的目标位置。
步骤 4:让用户猜测
现在星星已经显示在网格上,你需要让玩家猜测它的坐标。在你从清单 9-7 中添加的代码之后,仍然在While循环内,加入清单 9-8 中的代码。
1 TextWindow.Write("What is the x-coordinate? ")
2 xAns = TextWindow.ReadNumber()
3 If (xAns = X0) Then ' Player guessed the correct x-coordinate
4 TextWindow.Write("What is the y-coordinate? ")
5 yAns = TextWindow.ReadNumber()
6 If (yAns = Y0) Then ' Player guessed the correct y-coordinate
7 TextWindow.WriteLine("Good job! You're a star!")
8 Else ' Player entered an incorrect y-coordinate
9 TextWindow.WriteLine("Sorry. The star is at " + pt)
10 EndIf
11 Else ' Player entered an incorrect x-coordinate
12 TextWindow.WriteLine("Sorry. The star is at " + pt)
13 EndIf
14
15 TextWindow.WriteLine("") ' Empties the line before a new round
清单 9-8:猜测坐标
这段代码要求玩家输入星星的 x 坐标,并等待答案(第 1–2 行)。然后它检查 x 坐标的猜测是否正确(第 3 行)。如果答案不正确,程序将跳转到第 12 行显示星星的正确坐标(见 Figure 9-8 中标记为 2nd Round 的框)。但如果 x 坐标猜测正确,代码会要求玩家输入星星的 y 坐标并等待答案(第 4–5 行)。如果玩家答对了(第 7 行),程序会显示 Good Job! You're a star!。如果没有,程序将跳转到第 9 行显示正确的坐标。
在所有这些情况下,程序都会最终到达第 15 行,显示一个空行,然后 While 循环会重复进行下一轮游戏。游戏永远不会结束!(这正是你父母在你玩电子游戏时的感受。)
游戏现在完成了。现在试着玩一玩吧!
尝试一下 9-4
修改 Listing 9-8,要求玩家输入 x 和 y 坐标,然后使用 And 运算符在单个 If 语句中检查 xAns 和 yAns。
编程挑战
如果你卡住了,可以查看 nostarch.com/smallbasic/ 获取解决方案以及更多教师和学生资源和复习问题。
-
爱情计量器给出一个从 1 到 5 的数字,表示你心中的温暖程度(数字越低表示越温暖)。编写一个程序,要求用户输入他们的爱情指数数字,然后显示以下消息之一:
1: Your heart is lava hot! 2: Your heart is warm. 3: Your heart is neutral. 4: Your heart is cold, like the North Pole! 5: If your heart was a movie, it would be Frozen! -
编写一个程序来模拟老鼠寻找食物的过程(见下图)。老鼠从房间 1 开始。从那里,随机让老鼠进入房间 2 或房间 4(随机决定)。暂停并向用户显示这一动作。如果老鼠进入房间 4,那么下一步它可以进入房间 1、房间 2 或房间 5(随机决定,然后显示移动)。当老鼠进入房间 3(并找到奶酪)或进入房间 5,猫正在那里耐心地等待它的零食时,结束模拟。打开本章文件夹中的 HungryMouse_Incomplete.sb 文件,按照指示完成模拟。
![image]()
-
欧比·旺·肯诺比需要知道星期几。但 R2-D2 只是对着他发出嗡嗡声。欧比·旺数了数嗡嗡声,但他需要你的帮助,将这个数字转换成星期几。编写一个
If/ElseIf梯形结构,比较变量dayNum的值与 1、2、3、...、7,然后将变量dayName设置为"Sunday"、"Monday"、"Tuesday"、...、"Saturday"(所以 1 是星期天,7 是星期六)。帮助欧比·旺·肯诺比。你是他的唯一希望!
第十一章:10
使用子程序解决问题

到目前为止,你写的程序都很简短,容易理解。但随着你开始处理更复杂的问题,你将需要编写更长的程序。理解长程序可能会是一个挑战,因为你需要跟踪程序中的许多不同部分。在本章中,你将学习如何将程序组织成更小的部分。
一种名为结构化编程的方法始于 1960 年代中期,目的是简化编写、理解和维护计算机程序的过程。与其编写一个单一的、大型程序,不如将程序分成更小的部分。每个部分解决整个任务的一部分,子程序实现这些较小的部分,作为长程序的一部分。
子程序是创建大型程序的基本构建块(见图 10-1)。在本章中,你将深入了解子程序的奇妙世界,学习如何在它们之间传递数据,并利用它们来构建大型程序和有趣的游戏!

图 10-1:子程序是大型程序的构建块
为什么使用子程序?
假设你经营一家建筑公司。你的工作是协调承包商的工作并建造房屋。作为经理,你不需要了解建房的所有细节:水管工负责水管工作,屋顶工铺设屋顶瓦片,电工负责布线。每个承包商都知道自己的工作,并且在接到你的电话时,随时准备工作。
这非常类似于子程序的工作方式!每个子程序都有自己的名字,就像水管工的名字是马里奥一样。每个子程序执行不同的任务,就像水管工和屋顶工有不同的工作,但所有人都在建造房屋时不可或缺。作为程序员,你是经理,你的工作是在构建程序时解决问题。你调用你的承包商(也就是你的子程序),并告诉他们你何时需要他们工作(见图 10-2)。你通过在编辑器中键入语句来开始编写程序。当你需要执行一个子程序处理的任务时,只需调用该子程序并等待。当子程序完成任务后,你就可以继续执行程序中的下一步。

图 10-2:老板(主程序)调用 Bob 子程序
这种“呼叫与等待”的策略没有什么新鲜的,你从第一章开始就已经在使用它了。当你调用一个对象的方法时,你实际上是在把工作交给那个在 Small Basic 库中的对象。子程序就像方法,但你必须编写子程序中的所有语句。子程序帮助你组织思维过程,并使修复错误变得更加容易。
编写子程序
让我们用一个有趣的例子来学习如何编写子程序:在他的旅行中,格列佛曾与小人国的国王和王后共进晚餐。在晚餐时,国王解释说自己有 8.5 个 glum-gluffs 高。格列佛后来得知,1 个 glum-gluff 大约等于 0.75 英寸。为了了解小人国的物品大小与我们这边的物品大小如何比较,编写清单 10-1 中的程序,将 glum-gluffs 转换为英寸。
1 ' GlumGluff.sb
2 TextWindow.Write("How many glum-gluffs? ")
3 glumGluffs = TextWindow.ReadNumber()
4
5 inches = 0.75 * glumGluffs ' Converts to inches
6 inches = Math.Round(inches * 100) / 100 ' Rounds to 2 decimal places
7 TextWindow.WriteLine("That's about " + inches + " inches.")
清单 10-1:转换度量单位
这个程序看起来和你已经习惯的那些程序一样!你提示用户输入 glum-gluff 的度量值(第 2 行),将输入值读入glumGluffs变量(第 3 行),将输入的数字转换为英寸(第 5 行),将结果四舍五入到小数点后两位(第 6 行),然后显示结果(第 7 行)。运行程序来计算国王的身高是多少英寸;记住他是 8.5 个 glum-gluffs 高。
接下来,让我们重写这个程序,并将转换语句(第 5–6 行)放入一个名为GlumGluffToInch()的子程序中。请在清单 10-2 中输入代码。
1 ' GlumGluff2.sb
2 TextWindow.Write("How many glum-gluffs? ")
3 glumGluffs = TextWindow.ReadNumber()
4
5 GlumGluffToInch() ' Calls the subroutine
6 TextWindow.WriteLine("That's about " + inches + " inches.")
7
8 ' This subroutine converts from glum-gluffs to inches
9 ' Input: glumGluff; the size in glum-gluff units
10 ' Output: inches; the size in inches rounded to 2 decimal places
11 Sub GlumGluffToInch
12 inches = 0.75 * glumGluffs
13 inches = Math.Round(inches * 100) / 100
14 EndSub
清单 10-2:调用子程序
这段代码的功能与清单 10-1 中的代码相同,但它使用了一个子程序。子程序是一组完成特定任务的语句(就像雇佣马里奥水管工来建造一个豪华厕所)。在这个例子中,你的子程序将 glum-gluffs 转换为英寸。构成子程序的语句被Sub和EndSub关键字包围(第 11–14 行)。子程序的名称跟在Sub关键字后面(第 11 行)。定义子程序时,不要在其名称后加上圆括号。
但仅仅定义了一个子程序,并不意味着你的程序会自动执行它。要运行一个子程序,你需要调用(或激活)它!要调用子程序,你需要输入子程序的名称,后面跟上圆括号(第 5 行)。第 5 行的语句意味着“运行名为GlumGluffToInch()的子程序,然后返回到子程序调用后的下一行”(在这个例子中是第 6 行)。这就像你在打扫房间时休息去看电视,然后回来继续原来的工作。图 10-3 展示了子程序在程序中的工作原理。

图 10-3:展示 GlumGuff2.sb 如何调用 GlumGluffToInch() 子程序
这是该程序的输出示例:
How many glum-gluffs? 8.5
That's about 6.38 inches.
子程序可以访问主程序中的所有变量,主程序也可以访问子程序中的所有变量。变量glumGluffs是在主程序中创建并赋值的(第 3 行),但是它被子程序用来知道需要转换多少个 glum-gluffs(第 12 行)。而变量inches是在子程序内部创建的(第 12 行),但主程序读取它并将其值显示给用户(第 6 行)。
下面是将单位转换代码放入子程序的一些好理由:
-
你将单位转换的细节从主程序中隔离(或分离)出来。现在主程序不必担心转换是如何进行的。这使得你的代码更易于阅读和维护。
-
如果出现错误,你知道在哪里查找,这使得调试变得更容易。
-
你不必一遍遍地写相同的代码!如果不使用子程序,当程序需要多次执行相同的语句时,你必须在代码中重复这些语句。但如果将这些语句放入子程序中,你可以在程序的任何位置调用它(代码复用)。你将在下一节中练习这个技巧。
注意
在本书中,我们会将子程序的名称以大写字母开头。我们还会将所有子程序写在每个主程序的底部。我们建议你在自己的程序中也采用这种做法:它会帮助你保持条理!
试试看 10-1
当格列佛问什么是 glum-gluff 时,他被告知它是 mumgluff 的 1/20。编写一个名为MumGluffToFoot()的子程序,将 mum-gluff 转换为英尺。编写一个程序,提示用户输入一个 mum-gluff 的测量值,调用子程序,然后显示结果。
子程序的输入和输出
你可以将子程序视为一个为主程序提供服务的小程序。当主程序需要该服务时,它准备好子程序所需的输入,然后调用子程序开始工作。子程序运行并将其输出保存在某些变量中,然后返回主程序。当主程序继续时,它查看来自子程序的任何新信息,并根据这些数据决定接下来该做什么。
Small Basic 不允许你像在对象的方法中那样通过括号向子程序传递参数(例如GraphicsWindow的DrawLine()方法)。它也没有定义直接返回值的子程序(就像Math.Round()方法一样)。所以你需要使用变量在主程序和子程序之间传递数据。让我们看看这是如何工作的。
好消息!你继承了一块土地(图 10-4)来自钱袋叔叔。但是在你能卖掉这块土地之前,你需要知道这块土地的面积。图中还显示了海伦公式,它可以根据三角形的三条边长计算面积。如果你不熟悉这个公式也不用担心;你不需要完全理解某个东西才能使用它(否则大多数人都不能上厕所了)。

图 10-4:计算你继承的土地面积
因为这块土地由两个三角形组成,你可以计算这两个三角形的面积,然后将它们相加。请参阅清单 10-3,注意我们如何将计算三角形面积的代码(海伦公式)放入子程序中。
1 ' LandArea.sb
2 ' Calculates the area of the first triangle
3 side1 = 7
4 side2 = 20.6
5 side3 = 25
6 TriangleArea()
7 totalArea = area ' Saves the result from the subroutine call
8
9 ' Calculates the area of the second triangle
10 side1 = 30
11 side2 = 14
12 side3 = 22.3
13 TriangleArea()
14 totalArea = totalArea + area ' Adds the new area
15
16 totalArea = Math.Round(totalArea * 100) / 100 ' Rounds the answer
17 TextWindow.WriteLine("Area = " + totalArea + " square meters")
18
19 ' Subroutine: computes the area of a triangle given its three sides
20 ' Inputs: side1, side2, and side3; the length of the three sides
21 ' Outputs: area; the area of the triangle
22 ' Temporary variables: s; the semiperimeter
23 Sub TriangleArea
24 s = 0.5 * (side1 + side2 + side3)
25 area = Math.SquareRoot(s * (s - side1) * (s - side2) * (s - side3))
26 EndSub
清单 10-3:多次调用子程序
这是此程序的输出:
Area = 208.63 square meters
主程序设置第一个三角形的三条边的长度(第 3-5 行),然后调用TriangleArea()子程序(第 6 行)。子程序(第 23-26 行)将计算出的面积保存在一个名为area的变量中。子程序调用之后,主程序将第一个面积存储在totalArea变量中(第 7 行)。如果没有这一步,下一次调用TriangleArea()子程序时,存储在area中的值会丢失。接着,主程序设置计算第二个三角形面积的值(第 10-12 行),并再次调用子程序(第 13 行)。当子程序结束时,主程序将新的面积加到totalArea中(第 14 行)。然后,主程序将结果四舍五入(第 16 行)并显示出来(第 17 行)。
TriangleArea()子程序使用一个名为s的临时变量来存储半周长,即当前形状周长的一半(第 24 行)。注意这个变量是如何在第 25 行用于计算面积的。这个变量并不是为了供主程序使用的,主程序只关心area变量。但是主程序知道它的存在(例如,它可以显示该变量)。由于你的子程序可能会改变属于主程序的变量,请确保你的变量命名清晰明确。例如,如果s变量容易混淆,可以将它重命名为semiperimeter,这样你就能记住它的作用。
试试这个 10-2
钱袋叔叔又给你留了一块地(图 10-5)!更新清单 10-3 中的程序来计算它的面积(所有尺寸单位为米)。

图 10-5:你的新土地
嵌套子程序
如果你的任务是打扫房子,你可能会通过和你妹妹商量让她清洁窗户,并让你的狗打扫桌子下的地板来得到帮助。同样,一个子程序可能会调用其他子程序来帮助它完成更大的任务。在图 10-6 中,主程序调用了一个子程序SubA(),然后SubA()又调用了另一个子程序SubC()。从其他子程序中调用的子程序被称为嵌套子程序。

图 10-6:展示嵌套子程序
注意
如果你的程序包含许多子程序,你可以将这些子程序放在程序的末尾,按照你喜欢的顺序排列。例如,SubA()的代码放在SubB()之前或之后都没关系。重要的是你调用这些子程序的顺序,而不是它们在代码中的位置!
为了尝试这个概念,你将与计算机一起玩“辣椒敢挑战”,一款刺激的机会游戏。游戏开始时,玩家会拿到 10 张虚拟卡片,背面朝下。其中一张卡片上有一个哈瓦那辣椒,其他的都是空白的。玩家抽一张卡片,希望是空白卡片。如果玩家抽到带有辣椒的卡片,玩家必须吃下辣椒,计算机获胜!如果玩家没有抽到辣椒卡片,计算机会轮到自己。游戏在玩家或计算机吃到辣椒并急忙去喝水时结束。输入清单 10-4 中的主程序到 Small Basic。稍后你将添加子程序。
1 ' PepperDare.sb
2 player = 1 ' 1 for player, 2 for computer
3 pepper = Math.GetRandomNumber(10) ' Which card has the pepper
4
5 Again:
6 Pick() ' Updates the two variables: card and name
7 If (card = pepper) Then
8 TextWindow.Write("Hot tamale, it's a pepper! ")
9 TextWindow.WriteLine(name + " wins!")
10 TextWindow.WriteLine("")
11 Else
12 TextWindow.Write("The card is blank. ")
13 TextWindow.WriteLine("You put it back in and shuffle the deck.")
14 TextWindow.WriteLine("")
15 player = 3 - player ' Switches the player
16 Goto Again
17 EndIf
清单 10-4:设置辣椒敢挑战
游戏通过将player变量设置为 1 来开始,赋予你第一回合(第 2 行)。然后它随机选取 10 张卡片中的 1 张作为辣椒卡片(第 3 行)。接着它开始一个循环(第 5–17 行)来轮流进行。在每一轮中,游戏通过调用Pick()子程序(第 6 行)随机为玩家(或计算机)选一张卡片。如果选中的卡片上有辣椒(第 7 行),游戏会显示获胜者的名字(第 9 行),并结束游戏,因为程序会跳出If循环,从第 10 行跳到第 17 行,跳过第 16 行的Goto循环。
否则,程序会显示卡片是空白的。你把它放回去并重新洗牌。(第 12–13 行),表示玩家(或计算机)抽到了一张空白卡片。然后游戏会切换到下一个玩家(第 15 行)并开始新一轮(第 16 行)。第 15 行的语句是这样工作的:如果player是 1(你,用户),则 3 - 1 等于 2(切换到计算机的回合);如果player是 2(计算机),则 3 - 2 等于 1(切换回用户的回合)。
接下来,你将把清单 10-5 中的Pick()子程序添加到程序的底部。
1 Sub Pick
2 If (player = 1) Then
3 name = "The computer"
4 TextWindow.WriteLine("Your turn. Pick a card.")
5 Else
6 name = "The player"
7 TextWindow.WriteLine("The computer picks a card.")
8 EndIf
9
10 TextWindow.Write("[Press any key...]")
11 TextWindow.PauseWithoutMessage()
12 TextWindow.WriteLine("")
13
14 card = Math.GetRandomNumber(10) ' Picks a random card
15 Animate() ' Animates the delay in picking a card
16 EndSub
清单 10-5:辣椒敢挑战的 Pick() 子程序
子程序首先检查当前的玩家(你或计算机),然后设置name变量(第 3 行和第 6 行)。接下来,它要求你按任意键让你或计算机抽一张卡片(第 10–12 行)。然后它随机抽取一张卡片(第 14 行),并调用嵌套的Animate()子程序在文本窗口中显示一个箭头的动画。
现在将清单 10-6 中的Animate()子程序添加到程序的底部。
1 Sub Animate
2 For N = 1 To card
3 TextWindow.Write("-")
4 Program.Delay(100)
5 EndFor
6 TextWindow.Write("-> ")
7 EndSub
清单 10-6:动画延迟的子程序
不用担心这里的For循环。你将在第十三章中深入学习它。目前,这段代码只是缓慢地显示一个可变长度的箭头。以下是完成的辣椒敢挑战程序的示例运行:
Your turn. Pick a card.
[Press any key...]
--> The card is blank. You put it back in and shuffle the deck.
The computer picks a card.
[Press any key...]
--------> The card is blank. You put it back in and shuffle the deck.
Your turn. Pick a card.
[Press any key...]
---------> Hot tamale, it's a pepper! The computer wins!
注意
子程序不仅可以调用其他子程序,还可以调用自身(这称为递归)!请查看在线资源了解更多内容。
尝试 10-3
多次玩《胡椒敢挑战》游戏,理解它是如何运作的。想出一些改进的想法,然后尝试实现这些想法。
创建龙游戏
上一个示例向你展示了子程序如何为你的程序增加结构性和清晰度。你将程序分解成更小的部分,逐一解决它们。尽管每个问题都不同,没有一种通用的解决方案,我们建议你考虑一些方法来思考任何问题。
首先,花些时间彻底理解问题。在跳入泳池之前,你不会先看看水的情况吧?(如果池水里是布丁怎么办?)当你清楚需要解决的问题时,规划一个总体解决方案。然后将其分解为主要任务。作为解决方案的规划者,你决定这些任务是什么。没有对错之分;通过练习,你会在做这些选择时变得更好。但如果你从整体解决方案开始并将其分解成较小的任务,程序的逻辑会更清晰。
为了展示这个问题解决策略,我们来制作一个如图 10-7 所示的龙游戏。

图 10-7:龙游戏的用户界面
在这个游戏中,你控制骑士,而你的任务是击杀龙。屏幕上你可以看到用于记录分数的变量,并且玩家可以选择三种行动方式来进行游戏。
游戏开始时,勇敢的骑士位于右侧,距离龙龙(Draggy)有一定的距离。勇敢的骑士有一把弓和一些箭矢,他的盾牌有一定的强度(程序会随机选择这些数值)。骑士先行动。他可以向前移动一步、射箭攻击龙,或者用剑刺击龙(但仅当他与龙相距 1 步时)。如果箭矢击中龙,它会立刻杀死龙!使用剑时,骑士有 50%的机会击杀龙(但仅在足够接近时)。如果勇敢的骑士击杀了龙龙,他将成为年度骑士,赢得一场属于自己的舞会,并把自己的画像挂在城堡墙上。
一旦勇敢的骑士行动,龙龙就会向骑士喷火。如果火焰击中骑士,它会削弱骑士的盾牌。当盾牌失去强度时,骑士变得毫无防备。从这一点开始,如果龙的火焰再次击中骑士,它将烧死骑士!整个城市将受到无情、凶猛的龙的攻击。游戏结束!
游戏使用了五个图像,你可以在本章的文件夹中找到它们:背景图像(你的战场)、两张龙的图像(一张图像显示龙的火焰)、骑士的图像,以及一张箭矢的图像。按照步骤 1-10 制作一个有趣的龙游戏!
步骤 1:打开启动文件
打开本章代码文件夹中的Dragon_Incomplete.sb文件。这个文件包含了示例 10-7 中的代码,并为你的子例程预留了空白位置。你将一步步添加这些子例程的代码。程序的文件夹中也包含了你所需要的所有图像,还提供了完整的游戏代码Dragon.sb,以防你遇到困难。
1 ' Dragon_Incomplete.sb
2 SetUp() ' Does one-time set up
3
4 NewGame() ' Sets the parameters for a new game
5
6 UpdateUserInterface() ' Shows values on background image
7
8 NextMove:
9 GetChoice() ' Displays options and gets the knight's choice
10
11 ProcessChoice() ' Processes the user's choice
12
13 DragonFire() ' Now it's the dragon's turn
14 Goto NextMove
示例 10-7:龙游戏的高级结构
首先,你调用SetUp()子例程(第 2 行),绘制背景图像,创建文本形状(用于显示距离、箭的数量等),并加载游戏的图像(龙、骑士和箭)。第 4 行调用NewGame()来设置新游戏的参数,包括骑士的箭数、盾牌强度和与龙的距离。在第 6 行,你调用UpdateUserInterface()来更新游戏的用户界面(UI)。然后,代码进入一个循环(第 8–14 行)来管理游戏。每轮,你询问骑士的下一步操作(第 9 行),通过调用ProcessChoice()来处理他的选择(第 11 行),然后轮到龙(第 13 行)。正如你将看到的,这些子例程会跟踪游戏状态,并在有赢家时结束游戏!
接下来,你将逐个处理这些子例程。
步骤 2:编写 SetUp() 子例程
你将从编写SetUp()子例程开始,这个子例程为你的游戏创建场景。将示例 10-8 中的代码添加到你的程序中。
1 Sub SetUp
2 GraphicsWindow.Title = "Slay the Dragon"
3 TextWindow.Title = GraphicsWindow.Title
4
5 GraphicsWindow.Width = 480
6 GraphicsWindow.Height = 380
7 GraphicsWindow.CanResize = 0
8 GraphicsWindow.FontSize = 14
9 GraphicsWindow.Left = 40
10 ' Positions the text window
11 TextWindow.Left = GraphicsWindow.Left + GraphicsWindow.Width + 20
12 TextWindow.Top = GraphicsWindow.Top
13
14 path = Program.Directory
15 GraphicsWindow.DrawImage(path + "\bkgnd.png", 0, 0)
16
17 ' Creates text objects to show distance, arrows,
18 ' shield strength, and message
19 distText = Shapes.AddText("")
20 arrowsText = Shapes.AddText("")
21 shieldText = Shapes.AddText("")
22 msgText = Shapes.AddText("Draggy VS Good Knight")
23 Shapes.Move(distText, 60, 30)
24 Shapes.Move(arrowsText, 200, 30)
25 Shapes.Move(shieldText, 370, 30)
26 Shapes.Move(msgText, 5, 362)
27
28 ' Loads the images for the knight, dragon, and arrow
29 knightImg = Shapes.AddImage(path + "\knight.png")
30 dragon1Img = Shapes.AddImage(path + "\dragon1.png")
31 dragon2Img = Shapes.AddImage(path + "\dragon2.png")
32 arrowImg = Shapes.AddImage(path + "\arrow.png")
33 Shapes.Move(dragon1Img, 0, 250)
34 Shapes.Move(dragon2Img, 0, 250)
35 Shapes.Move(knightImg, 380, 250)
36
37 Shapes.HideShape(dragon2Img)
38 Shapes.HideShape(arrowImg)
39 EndSub
示例 10-8:设置窗口和属性
这段代码包含了你游戏的一次性设置;虽然有点长,但我们会一步步讲解。你设置了图形窗口和文本窗口的标题(第 2–3 行)。这些标题将在游戏运行时显示在这些窗口的标题栏中(见图 10-7)。
然后你设置图形窗口的大小(第 5–7 行)、字体大小(第 8 行)和位置(第 9 行)。接着,你将文本窗口定位到图形窗口的右侧(第 11–12 行)。在绘制背景图像(第 14–15 行)之后,你创建并定位文本形状,用于显示游戏界面上的所有数字(第 19–26 行)。然后,你加载并定位骑士、龙和箭的图像(第 29–35 行)。最后,你隐藏龙喷火和箭的图像,因为此时并不需要(第 37–38 行):当龙喷火和骑士射箭时,你会显示这些图像。
当我们构建这个程序时,我们通过反复试探的方法确定了文本和图像(包括我们使用的数字)在背景图像上的位置(我们猜测并调整,直到找对位置)。你在设计自己未来的游戏 UI 时,可能也需要采用这种方法。
步骤 3:加入一点运气元素
接下来,你需要为游戏添加一些运气因素。每次运行游戏时,我们希望好骑士能获得不同数量的箭矢,距离龙有不同的远近,盾牌强度也有所不同。为此,在清单 10-9 中将 NewGame() 子程序添加到你的程序中。
1 Sub NewGame
2 dist = 9 + Math.GetRandomNumber(10) ' 10 to 19
3 arrows = Math.Floor(0.4 * dist) ' 4 to 8
4 shield = Math.Floor(0.4 * dist) ' 4 to 8
5 moveStep = 280 / dist ' Knight's move in pixels
6 EndSub
清单 10-9:设置新游戏
在第 2 行,你将 1 到 10 之间的随机数加上 9,这样就设置了距离 dist,它的值在 10 到 19 之间。这是好骑士必须走的步数,才能到达龙的地方。接下来,你将箭矢的数量设置为距离的 40%(第 3 行)。骑士离龙越远,他拥有的箭矢就越多。在第 4 行,你设置骑士盾牌的强度——同样是基于他与龙的距离来设置。
让我们稍微考虑一下 moveStep 这一行。背景图片的宽度是 480 像素。龙的宽度是 100 像素,骑士的宽度也是 100 像素。当我们将龙和骑士放置在背景上时,从龙的右边缘到骑士的左边缘的距离是 280 像素。所以每次好骑士向前移动时,我们将把他的图像向左移动 280 / dist 像素。
提示
你可以将第 3 行和第 4 行中的分数从 0.4 改为其他值,以使游戏变得更容易或更难。完成游戏后,尝试更改分数并玩几次游戏!
步骤 4:让玩家知道发生了什么
在设置好游戏参数后,你需要将这些信息展示给用户。在清单 10-10 中添加 UpdateUserInterface() 子程序。
1 Sub UpdateUserInterface
2 Shapes.SetText(distText, dist)
3 Shapes.SetText(arrowsText, arrows)
4 Shapes.SetText(shieldText, shield)
5 EndSub
清单 10-10:更新文本的子程序
这个子程序非常基础(而且很小!)。你只需要使用 Shapes 对象的 SetText() 方法,并传入文本形状的标识符以及你要显示的数字。回想一下,我们在 SetUp() 子程序中创建这些文本形状时保存了这些标识符(清单 10-8 中的第 19 到 21 行)。
步骤 5:通过 GetChoice() 获取玩家的选择
如果你现在运行游戏,你应该能看到所有的图片和数字都已经就位,但还什么都不会发生。你需要开始接受骑士的命令,因此是时候在清单 10-11 中添加 GetChoice() 子程序了。
1 Sub GetChoice
2 AskAgain:
3 TextWindow.WriteLine("Select:")
4 TextWindow.WriteLine(" [1] Move 1 step forward")
5 TextWindow.WriteLine(" [2] Shoot an arrow")
6 TextWindow.WriteLine(" [3] Stab the dragon (you have to be 1 step away)")
7 TextWindow.Write(" Your choice [1-3]: ")
8
9 choice = TextWindow.ReadNumber()
10 If((choice <> 1) And (choice <> 2) And (choice <> 3)) Then
11 Goto AskAgain
12 EndIf
13
14 If ((choice = 2) And (arrows = 0)) Then
15 Shapes.SetText(msgText, "You ran out of arrows! Borrow some from Link.")
16 Goto AskAgain
17 EndIf
18
19 If ((choice = 3) And (dist > 1)) Then
20 Shapes.SetText(msgText, "You're too far to use your sword. Too bad
you can't train dragons.")
21 Goto AskAgain
22 EndIf
23
24 Shapes.SetText(msgText, "")
25 TextWindow.WriteLine("")
26 EndSub
清单 10-11:获取用户选择并显示任何错误
你首先显示选项给用户(第 3-7 行)。你读取用户对“勇敢骑士”的选择(第 9 行),并确保它是有效的。如果用户输入的数字不是 1、2 或 3,你会要求他们重新输入(第 10-12 行)。如果用户选择射箭但没有箭,你会告诉他们箭已用完并再次询问(第 14-17 行)。如果他们想刺杀龙但距离太远,你会告诉他们距离太远并要求重新选择(第 19-22 行)。否则,用户做出的选择是可接受的。你清除第 24 行的消息文本,向文本窗口添加一行空白文本以准备下一个提示(第 25 行),然后返回主程序(第 26 行)。
第 6 步:处理玩家的选择
现在用户已做出选择,你需要检查choice变量来决定接下来要做什么。将清单 10-12 中的ProcessChoice()子程序添加到你的程序中。
1 Sub ProcessChoice
2 If (choice = 1) Then ' Move-forward subroutine
3 MoveKnight()
4 ElseIf (choice = 2) Then ' Shoot-arrow subroutine
5 ShootArrow()
6 Else ' Stab subroutine
7 StabDragon()
8 EndIf
9 EndSub
清单 10-12:跳转到选择的子程序
你在choice变量上使用If/Else条件结构,并为每个选择调用不同的子程序。接下来,你将编写这三个子程序!
第 7 步:通过 MoveKnight()添加运动效果
将清单 10-13 中的MoveKnight()子程序添加进来,让“勇敢骑士”有了生气,并开始移动。
1 Sub MoveKnight
2 dist = dist - 1
3 Shapes.SetText(distText, dist)
4
5 Shapes.Move(knightImg, 100 + dist * moveStep, 250)
6
7 If (dist = 0) Then ' Checks whether the knight touched the dragon
8 Shapes.SetText(msgText, "The dragon swallowed you! You taste like chicken.")
9 GameOver()
10 EndIf
11 EndSub
清单 10-13:移动“勇敢骑士”的子程序
你首先通过减少骑士与龙的距离 1 步(第 2 行),然后在游戏界面上显示新的距离(第 3 行)。接着,你将骑士的图像向左移动(第 5 行)。
为了理解这个过程,假设骑士与龙的初始距离为dist,为 10,这使得moveStep = 28,如图 10-7 所示。当骑士距离龙 10 步时,骑士图像的左上角位于(100 + (10 × 28), 250)。当骑士距离龙 9 步时,骑士图像的左上角位于(100 + (9 × 28), 250),而当他距离龙 8 步时,图像的左上角位于(100 + (8 × 28), 250),依此类推。为了移动骑士,你将图像的水平位置设置为 100 加上当前距离dist乘以moveStep,并将图像的垂直位置设置为 250(见图 10-8)。

图 10-8:展示骑士的运动
移动骑士后,你检查他是否碰到了龙(第 7 行)。如果碰到了,你告诉“勇敢骑士”龙觉得他非常美味,并调用GameOver()子程序。这个子程序在清单 10-14 中;现在将它添加到你的程序中。
1 Sub GameOver
2 TextWindow.Pause()
3 Program.End()
4 EndSub
清单 10-14:运行GameOver()子程序
这个子程序调用Pause()方法,给用户一个阅读消息的机会(第 2 行)。当用户按下任意键时,Pause()方法结束,你调用End()方法退出程序(第 3 行)。
步骤 8:使用 ShootArrow() 射箭
将清单 10-15 中的 ShootArrow() 子程序添加到程序中,让勇敢的骑士成为一个超越鹰眼的弓箭高手。
1 Sub ShootArrow
2 arrows = arrows - 1
3 Shapes.SetText(arrowsText, arrows)
4
5 range = Math.GetRandomNumber(dist)
6
7 ' Animates the arrow
8 pos1X = 100 + dist * moveStep
9 pos2X = 100 + (dist - range)* moveStep
10 Shapes.Move(arrowImg, pos1X, 280)
11 Shapes.ShowShape(arrowImg)
12 Shapes.Animate(arrowImg, pos2X, 280, 2000)
13 Program.Delay(2000)
14 Shapes.HideShape(arrowImg)
15
16 If (range = dist) Then ' You hit the dragon right on
17 Shapes.SetText(msgText, "Perfect shot. The dragon's dead! You kiss the
princess's frog.")
18 GameOver()
19 Else
20 Shapes.SetText(msgText, "Your arrow missed! Robin Hood is giving lessons.")
21 Program.Delay(2000) ' To read the message
22 EndIf
23 EndSub
清单 10-15:射箭
你开始时使用一支箭(第 2 行),并在界面上显示剩余的箭(第 3 行)。接着,随机设置箭的射程为 1 到龙的距离之间的某个数字(第 5 行)。骑士离龙越近,他击中目标的概率越高。接下来的代码块(第 8–14 行)展示箭的动画。水平起始位置 pos1X 和骑士的位置相同(第 8 行),终点位置 pos2X 基于选定的射程(第 9 行)。你将箭移到起始位置(第 10 行),显示它(第 11 行),将其动画化到最终位置(第 12 行),等待箭到达目标(第 13 行),然后将其隐藏(第 14 行)。你可以更改第 12 行和第 13 行中的值 2000 来调整动画的时长。
动画完成后,你检查箭是否击中了龙(第 16 行)。如果击中,游戏结束(第 17–18 行),舞会属于你!否则,你告诉勇敢的骑士他的箭错过了(第 20 行),延迟程序以便用户阅读信息(第 21 行),然后返回 ProcessChoice() 子程序,再次返回主程序,轮到龙行动。
步骤 9:使用 StabDragon() 挥剑
现在,添加清单 10-16 中的最后一个骑士子程序。
1 Sub StabDragon
2 If (Math.GetRandomNumber(2) = 1) Then
3 Shapes.SetText(msgText, "You killed the dragon! You marry the princess
and 7 dwarves.")
4 GameOver()
5 Else
6 Shapes.SetText(msgText, "Your sword missed! Good one, Lance-a-Little!")
7 Program.Delay(2000) ' To read the message
8 EndIf
9 EndSub
清单 10-16:刺击龙
你随机选择数字 1 或 2。如果数字是 1(第 2 行),骑士击中了龙,游戏结束(第 3–4 行)。如果骑士未击中,你告诉骑士他错过了(第 6 行),并延迟程序以便用户阅读信息(第 7 行),然后返回 ProcessChoice() 子程序。
步骤 10:喷火
如果骑士没有击杀龙并结束游戏,主程序调用 DragonFire() 让龙有一个公平的机会。将清单 10-17 添加到你的程序中。
1 Sub DragonFire
2 Shapes.SetText(msgText, "The dragon ignited his fire. The Pokemon run.")
3 Shapes.HideShape(dragon1Img)
4 Shapes.ShowShape(dragon2Img)
5 Program.Delay(1000)
6 Shapes.HideShape(dragon2Img)
7 Shapes.ShowShape(dragon1Img)
8
9 If (Math.GetRandomNumber(2) = 1) Then ' Knight is hit
10 If (shield = 0) Then ' Shield is damaged
11 Shapes.SetText(msgText, "The dragon's fire BURNINATED you!")
12 GameOver()
13 Else
14 shield = shield - 1
15 Shapes.SetText(shieldText, shield)
16 Shapes.SetText(msgText, "You're hit! Your shield became weaker. Use
the force!")
17 EndIf
18 Else
19 Shapes.SetText(msgText, "The fire missed you! Aunt Mildred could've used
your luck.")
20 EndIf
21 EndSub
清单 10-17:龙对勇敢的骑士喷火
第 3–7 行展示龙的火焰动画。你隐藏正常的龙图像(第 3 行),并显示喷火的龙图像(第 4 行)。等待 1 秒(第 5 行),然后切换回原图像(第 6–7 行)。之后,龙有 50% 的机会用火焰击中骑士。你随机选择一个数字,1 或 2。如果是 1,表示龙击中了骑士(第 9 行)。在这种情况下,你检查盾牌的耐久度(第 10 行);如果盾牌的耐久度为 0,游戏结束(第 11–12 行)。如果不是 0,你将盾牌的耐久度减 1(第 14 行),显示新的值(第 15 行),告诉骑士他被击中了(第 16 行),然后返回主程序。如果随机数是 2(第 18 行),你告诉骑士龙的火焰未击中他(第 19 行),然后返回主程序。
游戏完成了!多玩几次,享受你创造的游戏吧!
尝试一下 10-4
龙的游戏很有趣,但并不完美。当你多次玩这个游戏时,你会注意到一些你不喜欢的地方或可以改进的地方。现在这个游戏是你的,你可以做出任何你认为能让游戏更好的改动。你甚至可以改变消息和图形。前往 tiny.cc/dragongame/ 在画廊中分享你的游戏,并查看别人做了什么!
编程挑战
如果你卡住了,可以查看 nostarch.com/smallbasic/ 获取解决方案,以及更多的资源和供教师与学生使用的复习问题。
-
本挑战的文件夹包含了外星生物的头部、眼睛、嘴巴和身体的图像(见下图)。
![image]()
编写一个程序,提示用户输入外星生物的眼睛数量(2、4 或 6)和嘴巴数量(1 或 2)。然后让你的主程序调用
DrawHead()、DrawEyes()、DrawMouths()和DrawBody()来绘制外星生物!例如,下面是一个有六只眼睛和两个嘴巴的外星人:![image]()
-
在这个挑战中,你将开发一个游戏《鬼魂狩猎》(见下图)。打开本章文件夹中的文件 GhostHunt_Incomplete.sb(该文件夹中包含了你所需的所有图像)。一个鬼魂藏在 12 个房间中的 1 个房间里。为了找到鬼魂,用户选择一个房间。如果用户在那个房间找到鬼魂,他们就赢了!否则,鬼魂会试图找到用户(通过随机选择一个房间号)。如果鬼魂找到用户,游戏结束。否则,鬼魂会移动到另一个房间,用户可以再试一次。
![image]()
第十二章:11
事件驱动编程

到目前为止,你写的程序大多是 顺序执行 的,因为它们是按照代码行的顺序,从上到下执行的。某些语句可能会进行比较或调用子程序以绕行,但总体来说,语句的顺序大多是线性的。
在某种程度上,这类似于你日常的生活方式:你起床、整理床铺、洗澡、吃早餐、看电视、梳头发,依此类推。但如果在这个过程中电话响了呢?如果你在等朋友打电话确认今晚的聚会,你最好接一下!即使你此时正在做某件事,你也会注意听电话。一旦你听到铃声(事件),你会放下手头的一切去接电话(希望不是你阿姨打来问你是否已经读完《高速公路上的小屋》)。
类似地,许多计算机程序(特别是游戏)使用 事件驱动编程,这意味着它们会监听并响应操作系统触发的事件(见图 11-1)。可以把 事件 看作是对某个动作做出反应的信号,例如移动或点击鼠标、点击按钮、敲击键盘、计时器到期等。Small Basic 库中的一些对象可以看到这些事件并告诉你它们何时发生。程序员称某个对象 触发 了一个事件。你可以通过处理这些事件来编写一些有趣的应用程序和游戏(比如一个超级有趣的爆炸农场游戏)。这些游戏通常会耐心等待玩家移动鼠标或按下某些键,然后执行相应的动作。

图 11-1:基于事件的编程模型
在图 11-1 中,事件位于顶部。当用户触发某个动作(如按键)时,Small Basic 库就会知道。如果你想知道某个事件发生的时刻,可以让 Small Basic 在事件发生时通知你,这样你就可以编写响应某些事件的程序。
Small Basic 库有三个处理事件的对象(见图 11-2):GraphicsWindow、Timer 和 Controls。本章将研究 GraphicsWindow 和 Timer 对象的事件,而 Controls 对象的事件将在下一章讨论。

图 11-2:Small Basic 中可用的事件
GraphicsWindow 事件
让我们先来探索GraphicsWindow中的事件。当用户与应用程序互动时,GraphicsWindow知道何时按下键、按下了哪些键,以及用户是否点击或移动鼠标。尽管GraphicsWindow知道这些事件的发生情况,但它并不会在事件发生时自动做任何事情。你需要指示GraphicsWindow在这些事件发生时通知你,这样你就可以使用它们。接下来,你将学习如何利用 Small Basic 关于用户的信息来创建有趣的互动应用。
通过 MouseDown 事件创建图案
让我们制作一个简单的应用程序,每次用户点击图形窗口时,都画一个随机颜色的圆。输入清单 11-1 中的代码。
1 ' Circles.sb
2 GraphicsWindow.MouseDown = OnMouseDown
3
4 Sub OnMouseDown
5 GraphicsWindow.PenColor = GraphicsWindow.GetRandomColor()
6 X0 = GraphicsWindow.MouseX - 10
7 Y0 = GraphicsWindow.MouseY - 10
8 GraphicsWindow.DrawEllipse(X0, Y0, 20, 20)
9 EndSub
清单 11-1:点击鼠标画圆
运行程序。一个示例输出显示在图 11-3 中。当你点击图形窗口内时,会画出一个随机颜色的圆形。制作一个有趣的图案,展示给别人,并试图说服他们这幅画是巴勃罗·毕加索画的!

图 11-3:Circles.sb 的示例输出
让我们看看清单 11-1 中的代码,了解 Small Basic 是如何处理事件驱动编程的。图 11-4 显示了该程序中的一行重要代码:第 2 行。

图 11-4:事件处理器注册语句
第 2 行的语句告诉GraphicsWindow对象,当发生MouseDown事件时,应该运行OnMouseDown()子程序。这个子程序也被称为事件处理器,因为它的目的是处理或处理一个事件。虽然你可以为这个子程序命名任何你想要的名称,但通常使用OnEventName的格式,这就是我们将处理器命名为OnMouseDown的原因。第 2 行的语句被称为注册事件处理器。在这个例子中,每次用户点击图形窗口时,Small Basic 都会调用OnMouseDown()子程序。
当用户点击图形窗口时,鼠标的 x 和 y 位置(相对于窗口的左上角)会保存在GraphicsWindow的MouseX和MouseY属性中。由于程序画一个直径为 20 的圆,并以鼠标点击位置为圆心,它从MouseX和MouseY中减去 10(以标记圆的左上角位置),然后将结果保存在X0和Y0变量中(第 6–7 行)。然后,子程序会画一个以鼠标点击位置为圆心、直径为 20 的圆(第 8 行)。
试试 11-1
修改清单 11-1 中的代码,改为画三角形和正方形,而不是圆形。如果你需要帮助,可以参考第三章回顾GraphicsWindow的绘图方法。
通过 KeyDown 事件发射导弹
许多电脑游戏使用键盘进行操作。例如,玩家可能使用方向键来移动主角,空格键来发射导弹,F1 键来获取帮助,P 键来捏角色的鼻子,以及 ESC 键来退出游戏。如果你想制作一个使用键盘输入的游戏,你需要在程序中添加KeyDown事件,这样你就可以知道用户按下了哪个键以及何时按下。
为了理解KeyDown事件,让我们编写一个简单的应用程序,显示用户按下的每个键的名称。在列表 11-2 中输入程序。
1 ' KeyDown.sb
2 yPos = 10
3 GraphicsWindow.KeyDown = OnKeyDown
4
5 Sub OnKeyDown
6 GraphicsWindow.DrawText(10, yPos, GraphicsWindow.LastKey)
7 yPos = yPos + 15
8 EndSub
列表 11-2:显示用户按下的每个键
一个带有一些注释的示例运行结果见于图 11-5。

图 11-5:KeyDown.sb 的示例运行结果
yPos变量设置了显示用户按下的键名的垂直位置。它从 10 开始,在显示完上一个按下的键名后(第 7 行),会增加 15。
你在第 3 行注册了KeyDown事件处理程序。每当用户按下一个键时,程序都会运行OnKeyDown()子程序。该子程序会显示按下的键的名称(第 6 行),并将yPos增加 15(第 7 行),以准备在下一行显示下一个键的名称。第 6 行的GraphicsWindow.LastKey属性提供了按下键的名称(作为字符串)。这个只读属性告诉你用户最后按下的是哪个键。
这个示例的重要性在于,它展示了 Small Basic 为不同的键盘键分配的名称。如果你想创建一个响应这些键的应用程序,你需要知道 Small Basic 是如何命名它们的。以下是你应该了解的其他一些细节:
-
字母键(A–Z)的名称始终是大写的。例如,即使你输入字母
"a",LastKey也会将其注册为大写的"A",无论是否打开大写锁定键或按下了 SHIFT 键。 -
数字键(0-9)的名称采用
"Ddigit"的形式。例如,数字 5 键的名称是"D5"。 -
四个方向键分别命名为
"Up"、"Down"、"Right"和"Left"。 -
回车(或返回)键的名称是
"Return",而空格键的名称是"Space"。 -
只要按下一个键,
KeyDown事件会不断触发(大约每 35 毫秒一次)。这与MouseDown事件不同,后者仅在点击左键时触发一次。
如果你想在程序中测试某些键的按下,了解键名是很重要的。
尝试一下 11-2
输入并运行以下代码。按下键盘上的一些键,并观察它们在文本窗口中的名称。按住一个键一段时间,看看会发生什么。(确保在你输入时,图形窗口是活动窗口。)
TextWindow.Show()
GraphicsWindow.Show()
GraphicsWindow.KeyDown = OnKeyDown
Sub OnKeyDown
TextWindow.WriteLine(GraphicsWindow.LastKey)
EndSub
当你尝试这个示例时,你注意到了什么?
使用 TextInput 事件制作打字机
TextInput 事件与 KeyDown 事件非常相似,但它仅在用户按下与文本相关的键时触发。这包括字母(A–Z)、数字(0–9)、特殊字符(如 !@#$%^&)以及其他键,如 ENTER、空格键、TAB 和 BACKSPACE。当 TextInput 事件被触发时,键盘上最后按下的字符会保存在 GraphicsWindow.LastText 属性中。
让我们看看这个事件是如何工作的。输入代码 Listing 11-3 来模拟打字机。我们知道打字机是旧式的,但嘿,情况还不算最糟;我们本可以在模拟算盘!
1 ' Typewriter.sb
2 x = 0 ' x position for displaying the last character
3 y = 0 ' y position for displaying the last character
4 GraphicsWindow.Title = "Typewriter"
5 GraphicsWindow.FontName = "Courier New"
6 GraphicsWindow.TextInput = OnTextInput
7
8 Sub OnTextInput
9 Sound.PlayClick() ' Plays a typewriter sound effect
10 If (GraphicsWindow.LastKey = "Return") Then
11 x = 0 ' Moves to next line
12 y = y + 15
13 Else
14 GraphicsWindow.DrawText(x, y, GraphicsWindow.LastText)
15 x = x + 8 ' Advances x position for the next character
16 If (x > GraphicsWindow.Width) Then ' If more than right margin
17 x = 0 ' Moves to the next line
18 y = y + 15
19 EndIf
20 EndIf
21 EndSub
Listing 11-3:每次按键时发出打字机声音
查看 Figure 11-6 中的示例输出。

图 11-6:打字机的示例输出
第 2 和第 3 行将光标设置在图形窗口的角落。第 4 行为窗口设置标题,第 5 行设置字体样式,第 6 行注册事件处理程序。第 9 行播放点击声音,第 10 到第 12 行在用户按下 ENTER 时前进一行。第 14 行写入用户输入的字符,第 15 行将光标移动到下一个位置,第 16 到第 18 行当光标到达右边缘时,将光标移动到下一行。
注意
当你在此应用程序中进行实验时,你会注意到 TextInput 事件会在设置 LastText 属性的值之前,检查不同键的状态。例如,如果你在按住 SHIFT 键的同时按下 A 键,LastText 属性会报告大写字母 "A";如果不按住 SHIFT 键,它则报告小写字母 "a"。
试一试 11-3
更新 Listing 11-3,使每个字符显示为随机颜色。查看 Listing 11-1,获取如何随机更改颜色的灵感。
使用 MouseMove 事件绘制图片
为了理解如何使用 MouseMove 事件,你将编写一个应用程序,允许用户通过鼠标绘画。用户在图形窗口中点击左键,然后拖动鼠标进行绘画。完整程序见 Listing 11-4。
1 ' Scribble.sb
2 GraphicsWindow.MouseMove = OnMouseMove
3
4 Sub OnMouseMove
5 x = GraphicsWindow.MouseX ' Current x position of mouse
6 y = GraphicsWindow.MouseY ' Current y position of mouse
7
8 If (Mouse.IsLeftButtonDown) Then
9 GraphicsWindow.DrawLine(prevX, prevY, x, y)
10 EndIf
11
12 prevX = x ' Updates the last (previous) position
13 prevY = y
14 EndSub
Listing 11-4:用户移动鼠标时绘制线条
Scribble.sb 的示例输出见 Figure 11-7。

图 11-7:Scribble.sb 的示例输出
OnMouseMove()子程序绘制一条从上次鼠标位置(你在第 12 行和第 13 行的prevX和prevY变量中保存的位置)到当前鼠标位置(你通过GraphicsWindow的MouseX和MouseY属性获取的当前位置)的线段。因为你希望用户仅在按下左键时绘制,所以OnMouseMove()子程序通过Mouse.IsLeftButtonDown属性(第 8 行)检查左键的状态。这个属性表示左键是否被按下。如果该值为真,子程序会绘制一条线段(第 9 行);如果值不为真,则不会绘制线条。
试试看 11-4
更改清单 11-4,使用TextInput事件来设置画笔的颜色(R 代表红色,G 代表绿色,B 代表黑色,等等)。
实用提示
在继续之前,我们将给你一些处理事件和事件处理程序的提示。你可以使用相同的子程序处理多个事件。例如,看看这些语句:
GraphicsWindow.MouseDown = OnMouseEvent
GraphicsWindow.MouseMove = OnMouseEvent
这些语句使得MouseDown和MouseMove事件调用OnMouseEvent()子程序。这个功能对于使用许多事件的复杂游戏非常有用,所以请记住这一点。
你可以在注册事件处理程序子程序后修改它。例如,假设你使用以下语句注册了OnMouseDown()子程序来处理MouseDown事件:
GraphicsWindow.MouseDown = OnMouseDown
如果你后来决定停止响应MouseDown事件(例如,游戏结束时),你可以写出以下语句:
GraphicsWindow.MouseDown = DoNothing
现在,DoNothing是新的MouseDown事件处理程序。如果你在DoNothing()子程序中不写任何语句,程序将不会对MouseDown事件做出任何响应。
MouseDown事件通常会后跟一个MouseUp事件,但不要总是依赖MouseUp事件的发生。如果你在图形窗口内点击左键,然后在松开按钮之前将光标移到图形窗口外部,你的应用程序只会收到一个MouseDown事件通知。如果你编写的应用程序需要将这两个事件配对(例如,点击抓住一个球,松开来扔它),记住这一点非常重要。
在接下来的部分,你将通过创建一个完整的游戏来实践到目前为止所学的内容。你还将了解Timer对象及其Tick事件。准备好迎接一场激动人心的计算机游戏冒险吧!
创建一个金币狂潮游戏
让我们创建一个简单的游戏,玩家使用方向键控制乌龟移动,尽可能多地收集金币袋(见图 11-8)。金币袋会随机出现在网格上的某个位置。如果玩家在 2 秒内没有抓到金币袋,它会移动到其他地方。看看你能多快让乌龟移动吧!

图 11-8:帮助乌龟抓取尽可能多的金币袋。
注意
网格是背景图像的一部分,但我们在图 11-8 中添加了 x 和 y 坐标,帮助你理解代码中使用的数字。请参考此图,想象乌龟和金袋是如何移动的。
步骤 1:打开启动文件
打开本章文件夹中的 GoldRush_Incomplete.sb 文件。该文件夹中还包含了你需要的三张图像。按照接下来的四个步骤逐步了解应用程序的代码。启动文件包含程序的主要代码,如清单 11-5 所示。它准备了游戏的用户界面,注册了事件处理程序,并初始化了游戏的变量。该文件还包含了所有你将添加的子例程的空占位符(未在清单 11-5 中显示)。
1 ' GoldRush_Incomplete.sb
2 GraphicsWindow.Title = "GOLD RUSH"
3 GraphicsWindow.CanResize = "False"
4 GraphicsWindow.Width = 480
5 GraphicsWindow.Height = 360
6
7 path = Program.Directory
8 grid = Shapes.AddImage(path + "\Grid.png")
9 player = Shapes.AddImage(path + "\Turtle.png")
10 gold = Shapes.AddImage(path + "\Gold.png")
11
12 ' Places the player (turtle) near the middle
13 XP = 4 ' x position (from 0 to 7)
14 YP = 3 ' y position (from 0 to 5)
15 Shapes.Move(player, XP * 60, YP * 60)
16
17 ' Creates the score text shape (over a black rectangle)
18 GraphicsWindow.BrushColor = "Black"
19 Shapes.AddRectangle(90, 20)
20 GraphicsWindow.FontSize = 14
21 GraphicsWindow.BrushColor = "Red"
22 scoreID = Shapes.AddText("Score: 0") ' For now
23
24 ' Registers two event handlers
25 GraphicsWindow.KeyDown = OnKeyDown
26 Timer.Tick = OnTick
27
28 ' Initializes variables
29 Timer.Interval = 2000 ' Ticks every 2 sec
30 score = 0 ' Keeps track of player's score
31 bagCount = 0 ' Counts how many bags so far
清单 11-5:设置 Gold Rush 游戏
第 3 到 5 行设置图形窗口的大小,以匹配背景图像的大小(grid.png)。第 8 到 10 行使用 Shapes 对象加载三张图像(背景网格、乌龟和金袋),并保存返回的标识符。你稍后需要这些标识符来移动乌龟和金袋。第 13 到 15 行将乌龟放置在网格的中间附近。请注意,网格上的每个方格大小为 60×60 像素。
第 18 到 22 行创建了你将用于显示玩家得分的文本形状。得分会以红色显示在屏幕左上角的黑色背景上(见图 11-8)。第 25 到 26 行注册了两个事件处理程序。OnKeyDown 处理程序检查箭头键,然后根据玩家的控制移动乌龟。OnTick 处理程序处理 Timer 对象的 Tick 事件,限制玩家到达每个金袋的时间。第 29 行将计时器间隔设置为 2 秒(2,000 毫秒),告诉 Timer 对象每 2 秒触发一次 Tick 事件。然后,代码将两个变量 score 和 bagCount 初始化为 0:score 跟踪玩家的得分(第 30 行),而 bagCount 跟踪到目前为止出现了多少个金袋(第 31 行)。
运行代码,你应该能看到乌龟位于网格的中间,金袋位于网格的左上角,得分文本显示为 0。
步骤 2:移动乌龟
为了让乌龟在玩家按下箭头键时移动,将清单 11-6 中的代码添加到文件的底部。
1 Sub OnKeyDown
2 key = GraphicsWindow.LastKey
3 If ((key = "Up") And (YP > 0)) Then
4 YP = YP - 1
5 ElseIf ((key = "Down") And (YP < 5)) Then
6 YP = YP + 1
7 ElseIf ((key = "Left") And (XP > 0)) Then
8 XP = XP - 1
9 ElseIf ((key = "Right") And (XP < 7)) Then
10 XP = XP + 1
11 EndIf
12 Shapes.Move(player, XP * 60, YP * 60)
13 CheckTouch() ' Checks if the player touched the bag
14 EndSub
清单 11-6:随着玩家按下箭头键,移动乌龟
网格有 8 个水平方格和 6 个垂直方格。水平方格编号为 0 到 7,垂直方格编号为 0 到 5。也就是说,XP变量(玩家的 x 坐标)可以取 0 到 7 之间的任何值,YP变量(玩家的 y 坐标)可以取 0 到 5 之间的任何值。OnKeyDown()子例程使用If/ElseIf结构检查按下的键是否是四个方向键之一。如果在图形窗口内按下方向键之一,子例程会根据按下的方向键调整XP或YP。
例如,第 3 行和第 4 行检查玩家是否按下了向上箭头,如果海龟还没有到达顶部边缘,海龟将上移一个方格。你可以通过将方格编号乘以 60(因为每个方格是 60 像素)来找到网格上的确切位置(以像素为单位),第 12 行就是这么做的。然后,代码会调用CheckTouch()子例程来检查玩家是否碰到了金袋。
再次运行应用程序,检查你刚刚添加的代码。你应该能够通过键盘上的方向键控制海龟在方格网格上移动。它活了!
步骤 3:移动金袋
现在你将添加OnTick处理程序,以创建时间限制,并移动金袋到新位置的代码。将列表 11-7 中的子例程添加到程序的底部。
1 Sub OnTick ' Timer expires
2 NewRound()
3 EndSub
列表 11-7:OnTick()子例程
如前所述,金袋会出现在一个随机位置,并给玩家 2 秒钟的时间去抓取它。如果计时器到期,玩家就输了,因为他们没有及时抓住金袋。在这种情况下,OnTick处理程序会调用NewRound()子例程(第 2 行),以开始新一轮游戏。
NewRound()子例程在列表 11-8 中显示。将它添加到程序的底部。
1 Sub NewRound
2 bagCount = bagCount + 1
3 If (bagCount <= 20) Then
4 XG = Math.GetRandomNumber(8) - 1 ' From 0 to 7
5 YG = Math.GetRandomNumber(6) - 1 ' From 0 to 5
6 Shapes.Move(gold, XG * 60, YG * 60)
7 CheckTouch()
8 Else
9 Shapes.Remove(gold) ' Deletes the gold bag shape
10 GraphicsWindow.KeyDown = OnGameOver ' Do nothing
11 Timer.Tick = OnGameOver ' Do nothing
12 EndIf
13 EndSub
列表 11-8:当计时器到期时开始新一轮
NewRound()子例程首先将bagCount增加 1(第 2 行);bagCount仅用于统计目前已出现的金袋数量。计划是向玩家展示总共 20 个金袋。如果还没有展示 20 个金袋(第 3 行),子例程会为金袋选择一个随机位置(第 4–5 行),然后将金袋移动到图形窗口中的该位置(第 6 行)。我们在CheckTouch()子例程中使用变量XG和YG(分别代表金袋的 x 和 y 坐标)。移动金袋后,代码会调用CheckTouch()来检查金袋是否正好放置在玩家上方(第 7 行)——真幸运!
如果bagCount大于 20(第 8 行),我们会删除金币袋形状(第 9 行),并为KeyDown和Tick事件注册OnGameOver处理程序,它是一个没有语句的子程序,用来结束游戏。然后,当玩家按下箭头键,或者在第 20 个金币袋出现后计时器到期时,什么也不会发生。当然,这可能会让用户感到惊讶。还有其他方式来结束游戏,但如果你以后想修改,留给你发挥的空间。
你需要添加的下一个子程序是示例 11-9 中显示的OnGameOver()子程序。
1 Sub OnGameOver
2 EndSub
示例 11-9: OnGameOver() 子程序
如果此时运行游戏,金币袋应该会每 2 秒钟移动到网格上的随机位置。你仍然可以使用箭头键来移动海龟。出现 20 个金币袋后,金币袋会消失,箭头键将无法再移动海龟。
在测试这个游戏时,你可能会决定给用户更多时间来捡起金币袋,或者去掉幸运功能,让金币袋不能直接出现在玩家身上。尽情玩弄这个代码,直到你觉得游戏好玩为止。
步骤 4:更新用户得分
要完成游戏,在示例 11-10 中添加CheckTouch()子程序来检查玩家是否成功捡起了金币袋,如果是的话,就增加他们的得分。
1 Sub CheckTouch
2 If ((XP = XG) And (YP = YG)) Then
3 score = score + 1 ' Gives the player one point
4 Shapes.SetText(scoreID, "Score: " + score)
5 Sound.PlayClick() ' Adds sound effect
6 Timer.Pause() ' Resets the timer
7 Timer.Resume() ' Starts the timer
8 NewRound() ' Starts a new round
9 EndIf
10 EndSub
示例 11-10:检查海龟是否捡到钱
如果玩家的 x 和 y 位置与金币袋相同,海龟就会抓到金币袋(第 2 行)。快乐的海龟!如果幸运的海龟抓到金币袋,我们就增加分数(第 3 行),并显示出来(第 4 行),同时使用Sound对象播放短促的点击声(第 5 行),以带来良好的音效。
我们还需要将计时器重置为 2 秒,以开始新的一轮。我们通过暂停计时器(第 6 行)然后再恢复它(第 7 行)来实现这一点。接着,我们调用NewRound()来在这场历史性胜利之后,在随机位置放置另一个金币袋。你的海龟能再来一次吗?
这样就完成了游戏,在经历了这些努力之后,你应该可以享受自己的创作了。你的最高得分是多少?(提示:按住箭头键可以更快地横跨格子。)与朋友们分享(只需点击工具栏中的发布按钮),看看他们能否打破你的得分。玩得开心!
尝试一下 11-5
想一想有哪些方法可以增强这个游戏,尝试一下你的想法。以下是一些你可以尝试的点子:
• 以更盛大的方式结束游戏!显示一条消息或展示一些有趣的图形。
• 添加第二个金币袋。
• 每次用户抓到金币袋后,缩短时间限制。
前往tiny.cc/turtlegame/,分享你的海龟游戏更新。
编程挑战
如果遇到困难,可以访问nostarch.com/smallbasic/,查看解决方案以及更多的资源和供教师与学生使用的复习题。
-
希曼正在和他的朋友们一起玩 暮光 知识竞赛,需要一个按钮来在朋友答错问题时使用。当希曼点击鼠标左键时,编写一个程序在图形窗口中画出一个大大的 X,并播放一个声音。下次点击应删除这个 X。确保希曼可以随时重复这一操作(毕竟这是一个漫长的知识竞赛游戏)。
-
编写一个程序,每当用户点击鼠标时,在点击的位置印上一个乌龟脸图像。图像文件 turtleface.jpg 可以从本章文件夹中找到。(提示:可以从清单 11-1 中的代码开始,并使用
GraphicsWindow.DrawImage()方法绘制图像。) -
打开本章文件夹中的 Maze_Incomplete.sb 文件。目标是以尽可能短的时间退出迷宫,但这个迷宫目前没有出口。找出如何添加迷宫出口条件。当玩家退出迷宫时,显示解决迷宫所用的时间。
第十三章:12
构建图形用户界面

每个设备都有一套接口。例如,接口可以是微波炉或电梯中的按钮,洗碗机上的旋钮,或者甚至是你最喜欢的汉堡店的汽水分配器。计算机程序也有接口。早期的程序只有文本菜单,但现在我们使用不同的方式与计算机交互,比如桌面上的图标。
尽管你在本书中编写了一些非常有用的程序,但它们看起来并不像你习惯的程序,比如文字处理器、绘图程序、网页浏览器、视频游戏等。
如今,大多数程序都使用图形用户界面,或简称GUI(发音为“gooey”,但别担心,它不粘)。GUI 可以包含菜单、按钮、文本框等等。
一个例子是图 12-1 中展示的计算器程序。当用户点击程序中的一个数字按钮时,该数字会出现在窗口顶部的文本框中。当用户点击=按钮时,程序会计算数学运算的结果并显示出来。
在本章中,你将学习 Controls 对象,它让你为程序和游戏创建图形界面。

图 12-1:计算器程序的用户界面
使用 Controls 对象设计用户界面
我们从一个简单的程序开始,让用户输入他们的名字和姓氏,然后程序通过友好的信息用名字问候他们。图 12-2 展示了你将要创建的FirstGUIApp.sb,这是你要创建的图形用户界面(GUI)。图中的网格线和坐标点不是输出的一部分,它们是用来说明界面中不同组件的 x 和 y 坐标的。

图 12-2: FirstGUIApp.sb 用户界面
步骤 1:设计阶段
在这个程序中,用户在文本框中输入他们的名字和姓氏,然后点击显示信息按钮。如果他们的名字是 Alpaca,姓氏是 Bag,程序将在多行文本框中显示如下信息:
Hello there, Alpaca Bag!
启动 Small Basic,输入以下两行:
GraphicsWindow.DrawText(20, 20, "First name:") ' Label
fnText = Controls.AddTextBox(100, 20) ' First name text box
第一条语句在位置(20,20)绘制文本名字:。在第二条语句中,Controls 对象创建了一个文本框,左上角位于(100,20)。这个文本框的标识符被保存在变量 fnText 中(表示名字文本框)。当你想知道用户在这个文本框中输入了什么时,你将需要这个标识符。
点击运行,你将看到名字:标签和位于其右侧的文本框。文本框的大小大约是 160×20(默认大小)。
接下来,添加以下两行来创建姓氏:标签及其关联的文本框:
GraphicsWindow.DrawText(20, 60, "Last name:") ' Label
lnText = Controls.AddTextBox(100, 60) ' Last name text box
在这里,框的标识符保存在lnText(姓氏文本框)中。再次点击运行,你应该能在图形窗口中看到文本框及其标签完美对齐。
现在你将通过调用Controls对象的AddButton()方法来创建“Show Message”按钮:
showBtn = Controls.AddButton("Show Message", 280, 20) ' Button
AddButton()的第一个参数是按钮的标题,"Show Message"。第二个和第三个参数告诉Controls对象按钮的左上角应放置在哪里。按钮的标识符保存在showBtn(显示按钮)中。点击运行查看你刚刚创建的内容。默认情况下,按钮的宽度将与其标签一样宽。尝试拉长或缩短按钮的标签,然后再次运行程序,看看会发生什么。
接下来,你需要添加最后一个 GUI 元素——显示输出消息的框。因为你可以根据需要用一条长消息来问候用户,所以我们使用多行文本框。多行文本框有水平和垂直滚动条,如果需要的话会自动出现,就像哈利·波特的需求室一样。要创建一个多行文本框,调用AddMultiLineTextBox()方法:
msgText = Controls.AddMultiLineTextBox(100, 100) ' Message text box
再次,两个参数指定了框的左上角位置。框的标识符保存在msgText(消息文本框)中;稍后你需要它来设置框的文本。点击运行,你将看到一个位于(100, 100)的多行文本框。默认情况下,这个框的大小大约是 200×80。让我们通过调用SetSize()方法来使这个框更宽。就在创建多行文本框后添加这一行代码:
Controls.SetSize(msgText, 280, 80) ' Makes width = 280 and height = 80
第一个参数是你想要调整大小的控件标识符,在本例中是msgText。第二个参数(280)是宽度,第三个参数(80)是高度。如果你现在运行代码,你会看到一个与图 12-2 类似的界面。注意,调用SetSize()时,消息文本框的左上角并没有发生变化。
步骤 2:程序交互性
你已经创建了所有需要的控件,并将它们放置在你希望的位置。接下来,你需要使这些控件具有交互性。你需要编写一些代码来响应按钮的点击。当用户点击按钮时,程序需要读取姓氏和名字文本框的内容,并在多行文本框中显示问候语。添加第 13 到 21 行,如清单 12-1 所示,以完成程序(你已经写好了第 2 到 11 行来创建 GUI 元素)。
1 ' FirstGUIApp.sb
2 GraphicsWindow.DrawText(20, 20, "First name:") ' Label
3 fnText = Controls.AddTextBox(100, 20) ' First name text box
4
5 GraphicsWindow.DrawText(20, 60, "Last name:") ' Label
6 lnText = Controls.AddTextBox(100, 60) ' Last name text box
7
8 showBtn = Controls.AddButton("Show Message", 280, 20) ' Button
9
10 msgText = Controls.AddMultiLineTextBox(100, 100) ' Message text box
11 Controls.SetSize(msgText, 280, 80) ' Makes width = 280 and height = 80
12
13 Controls.ButtonClicked = OnButtonClicked ' Handler for button click
14
15 Sub OnButtonClicked
16 firstName = Controls.GetTextBoxText(fnText) ' First name text box
17 lastName = Controls.GetTextBoxText(lnText) ' Last name text box
18 fullName = firstName + " " + lastName ' Constructs full name
19 message = "Hello there, " + fullName + "!" ' Greeting message
20 Controls.SetTextBoxText(msgText, message)
21 EndSub
清单 12-1:创建一个简单的 GUI 程序
第 13 行注册了一个处理ButtonClicked事件的处理程序。这一行代码告诉Controls对象,每当用户点击“Show Message”按钮时,调用OnButtonClicked()子程序。
在OnButtonClicked()子例程中,首先调用GetTextBoxText()来获取输入在名字文本框中的文本,并将其保存到firstName变量中(第 16 行)。这个方法接受一个参数——需要获取文本的文本框的标识符。然后再次调用GetTextBoxText(),但传入不同的参数,以获取输入在姓氏文本框中的文本,并将其保存到lastName中(第 17 行)。接着,通过将firstName和lastName之间加入空格来设置fullName变量(第 18 行)。在第 19 行,你创建了问候语并将其保存在message变量中。最后,你调用SetTextBoxText()来设置消息文本框的文本,第一个参数是控制项的标识符,第二个参数是新文本(第 20 行)。运行程序,输入一些文本到文本框中,然后点击按钮查看程序是如何工作的。
在接下来的章节中,你将学习如何制作具有多个按钮的 GUI 程序。现在你可以按下 Small Basic 的按钮了!
动手试一试 12-1
使用清单 12-1 中的代码,获取用户的名字和姓氏,然后更新代码来显示一个包含用户姓名的搞笑短故事。
制作一个多彩的绘图程序
如果你创建一个包含多个按钮的程序,当用户点击这些按钮中的任何一个时,ButtonClicked事件处理程序会被调用。为了找出点击了哪个按钮,你可以使用Controls.LastClickedButton属性来获取被点击按钮的标识符;这就像是请你的朋友告诉你是谁注意到了你那双全新的鞋子。
为了向你展示如何在一个程序有多个按钮时使用ButtonClicked事件,我们将继续在第十一章中创建的Scribble.sb程序(请参见第 156 页的清单 11-4)。用户可以通过点击按钮来选择笔的颜色。查看程序的 GUI 界面,参考图 12-3。

图 12-3: Scribble2.sb 的示例输出
尝试运行更新后的程序Scrible2.sb,如清单 12-2 所示。你可能会注意到,这个程序使用了与清单 11-4 中的OnMouseMove事件处理程序相同的事件处理程序。
1 ' Scribble2.sb
2 btnR = Controls.AddButton("Red", 10, 30)
3 btnG = Controls.AddButton("Green", 10, 65)
4 btnB = Controls.AddButton("Blue", 10, 100)
5 Controls.SetSize(btnR, 60, 30)
6 Controls.SetSize(btnG, 60, 30)
7 Controls.SetSize(btnB, 60, 30)
8
9 GraphicsWindow.MouseMove = OnMouseMove
10 Controls.ButtonClicked = OnButtonClicked
11
12 Sub OnButtonClicked ' Changes the pen color
13 If (Controls.LastClickedButton = btnR) Then
14 GraphicsWindow.PenColor = "Red"
15 ElseIf (Controls.LastClickedButton = btnG) Then
16 GraphicsWindow.PenColor = "Green"
17 Else
18 GraphicsWindow.PenColor = "Blue"
19 EndIf
20 EndSub
21
22 Sub OnMouseMove
23 x = GraphicsWindow.MouseX ' Current x position of mouse
24 y = GraphicsWindow.MouseY ' Current y position of mouse
25
26 If (Mouse.IsLeftButtonDown) Then
27 GraphicsWindow.DrawLine(prevX, prevY, x, y)
28 EndIf
29
30 prevX = x ' Updates the last (previous) position
31 prevY = y
32 EndSub
清单 12-2:点击按钮来改变笔的颜色
第 2 至第 4 行创建了三个颜色选择按钮。这三个按钮的左上角坐标分别是(10, 30)、(10, 65)和(10, 100)。第 5 至第 7 行的语句设置每个按钮的大小为 60 × 30(宽度 = 60,高度 = 30)。第 9 至第 10 行注册了MouseMove和ButtonClicked事件的处理程序。
程序在用户点击任意一个按钮时调用 OnButtonClicked() 子程序(第 12 行)。为了知道哪个按钮被点击,子程序使用 If/ElseIf 语句比较 LastClickedButton 属性与三个按钮的标识符(第 13 至 19 行)。在识别出被点击的按钮后,子程序将 GraphicsWindow 的 PenColor 属性设置为选定的颜色。OnMouseMove() 子程序与程序的前一个版本相同,它定义在第 22 至 32 行。
提示
你也可以这样编写 OnButtonClicked() 子程序:
Sub OnButtonClicked
btnID = Controls.LastClickedButton
GraphicsWindow.PenColor = Controls.GetButtonCaption(btnID)
EndSub
你不再硬编码点击按钮的颜色,而是通过 GetButtonCaption() 方法从被点击按钮的标题中获取颜色。
试试看 12-2
你可以通过在创建按钮之前设置 GraphicsWindow 的 BrushColor 属性来更改按钮标题的颜色。修改 Listing 12-2,使每个按钮的文字颜色与其标题一致(将蓝色按钮的文字写成蓝色,依此类推)。
使用代码探索电路
在本节中,你将创建一个演示电气串联电路的程序。(你的技能真是太震撼了!)电路包括一个电池、三个电阻和一个串联的开关。用户可以通过在文本框中输入数值来改变电池电压和三个电阻的数值。当用户在任意文本框中输入新值时,Controls 对象会触发 TextTyped 事件。作为响应,程序会自动计算(并显示)电路中的电流以及每个电阻上的电压(参见 图 12-4)。

图 12-4:一个展示串联电路运行的程序
以下是描述该程序背后科学原理的方程式:
总电阻 R[tot] = R[1] + R[2] + R[3]
电路中的电流 I = V ÷ R[tot],其中 V 是电池电压
电压跨越 R[1] V[1] = I × R[1]
电压跨越 R[2] V[2] = I × R[2]
电压跨越 R[3] V[3] = I × R[3]
让我们来看一下计算过程。你通过将三个电阻的数值相加来计算总电阻 (R[tot])。接下来,你通过将电池电压 (V) 除以总电阻来计算电路中的电流 (I)。然后,你通过将电流乘以每个电阻的数值来计算每个电阻上的电压。(试着大声读这段话给你的朋友听,仿佛它超级简单。这会让他们震惊!)
以下步骤将引导你创建这个程序。所以系好安全带,紧紧抓住,准备好进入激动人心的计算机模拟世界。
步骤 1:打开启动文件
要开始创建这个电路模拟器,从本章文件夹中打开SeriesCircuit_Incomplete.sb。该文件包含注释,告诉你在哪里添加代码,以及你将编写的子程序的空占位符。
本章的文件夹中还包括了你需要的两个背景图像:bkgndOff.bmp和bkgndOn.bmp(见图 12-5;我们添加了图像名称以便于理解)。这两张图像除了开关状态不同外,其他完全相同:bkgndOff.bmp中的开关处于打开状态,而bkgndOn.bmp中的开关处于关闭状态。

图 12-5: SeriesCircuit.sb 的两个背景图像
当你开始编写这个程序的代码时,你会看到许多硬编码的数字。这些数字表示文本框和标签的坐标点,以及用于检查开关边界的坐标点。为了帮助你理解这些数字的来源,请参考图 12-6。在这张图中,我们在背景图像上添加了坐标轴和网格线,并标出了程序中将使用的所有点的坐标。

图 12-6:展示在 SeriesCircuit.sb 中使用的魔法数字
步骤 2:添加主要代码
如同之前的示例,你将从设计用户界面开始。你将编写代码来加载背景图像,创建并定位 GUI 元素(文本框),然后注册事件处理程序。接下来,添加程序的主要部分,详见列表 12-3。
1 ' SeriesCircuit_Incomplete.sb
2 offImg = ImageList.LoadImage(Program.Directory + "\bkgndOff.bmp")
3 onImg = ImageList.LoadImage(Program.Directory + "\bkgndOn.bmp")
4 bkgndImg = offImg ' Starts with the switch-off image
5
6 GraphicsWindow.Width = ImageList.GetWidthOfImage(offImg)
7 GraphicsWindow.Height = ImageList.GetHeightOfImage(offImg)
8 GraphicsWindow.DrawImage(bkgndImg, 0, 0)
9
10 r1Text = Controls.AddTextBox(130, 140) ' R1 text box
11 r2Text = Controls.AddTextBox(270, 140) ' R2 text box
12 r3Text = Controls.AddTextBox(308, 208) ' R3 text box
13 vText = Controls.AddTextBox(57, 218) ' Voltage text box
14 Controls.SetSize(r1Text, 42, 25) ' Resizes the text boxes
15 Controls.SetSize(r2Text, 42, 25)
16 Controls.SetSize(r3Text, 42, 25)
17 Controls.SetSize(vText, 48, 25)
18 Controls.SetTextBoxText(vText, 10) ' Sets the initial values
19 Controls.SetTextBoxText(r1Text, 4)
20 Controls.SetTextBoxText(r2Text, 4)
21 Controls.SetTextBoxText(r3Text, 2)
22
23 GraphicsWindow.MouseDown = OnMouseDown
24 Controls.TextTyped = OnTextTyped
列表 12-3:设置 GUI
你首先加载两个背景图像,并将它们的标识符保存在offImg和onImg变量中(第 2 至 3 行)。bkgndImg变量保存当前的背景图像,用户点击开关时该图像会发生变化。当程序启动时,开关是打开的,所以程序将bkgndImg = offImg(第 4 行)。第 6 至 7 行调整图形窗口的宽度和高度,使其与背景图像的大小匹配,第 8 行在图形窗口中绘制背景图像(此时为offImg)。
第 10 至 17 行创建了四个文本框(分别用于三个电阻和电池电压),并调整其大小,使它们精确地位于背景图像中的对应位置。在第 18 至 21 行,你为这些文本框设置了默认值。在第 23 行,你注册了一个MouseDown事件的处理程序,因为你需要知道用户何时点击开关。第 24 行注册了一个TextTyped事件的处理程序,因为当用户在任意四个文本框中输入新值时,你将自动计算并显示I、V1、V2和V3的值。
步骤 3:切换开关
当用户点击开关时,你需要更改背景图像来切换开关。在列表 12-4 中添加OnMouseDown()子程序。
1 Sub OnMouseDown ' Switches the background image
2 x = GraphicsWindow.MouseX
3 y = GraphicsWindow.MouseY
4 If ((x > 185) And (x < 245) And (y > 300) And (y < 340)) Then
5 If (bkgndImg = offImg) Then
6 bkgndImg = onImg
7 Else
8 bkgndImg = offImg
9 EndIf
10 UpdateUserInterface()
11 EndIf
12 EndSub
列表 12-4:更改背景图像
该子程序首先获取鼠标点击位置的 x 和 y 坐标,并将它们赋值给x和y变量(第 2 到第 3 行)。第 4 行检查该点是否位于开关的矩形区域内;如果鼠标位于开关的边界内,子程序将在第 5 到第 9 行切换bkgndImg变量的当前值(从开到关或从关到开),然后调用UpdateUserInterface()子程序来切换背景图像并更新计算值(第 10 行)。如你所见,如果用户打开开关,程序只会显示offImg背景图像;因为开关打开时电路中没有电流流动,所以I、V1、V2和V3的值不会显示。
步骤 4:响应变化
在清单 12-5 中添加OnTextTyped()子程序。此子程序在用户在任意四个文本框中输入新值时被调用。如你所见,该子程序只是调用UpdateUserInterface(),该函数会更新用户界面,显示当前的V、R1、R2和R3的值以及开关的状态。
1 Sub OnTextTyped
2 UpdateUserInterface()
3 EndSub
清单 12-5: OnTextTyped() 子程序
步骤 5:更新程序界面
现在在清单 12-6 中添加UpdateUserInterface()子程序。
1 Sub UpdateUserInterface ' Puts new values on the background
2 GraphicsWindow.DrawImage(bkgndImg, 0, 0)
3 If (bkgndImg = onImg) Then
4 R1 = Controls.GetTextBoxText(r1Text)
5 R2 = Controls.GetTextBoxText(r2Text)
6 R3 = Controls.GetTextBoxText(r3Text)
7 V = Controls.GetTextBoxText(vText)
8 Rtot = R1 + R2 + R3
9 If (Rtot > 0) Then
10 I = V / Rtot
11 V1 = Math.Round(I * R1 * 100) / 100
12 V2 = Math.Round(I * R2 * 100) / 100
13 V3 = Math.Round(I * R3 * 100) / 100
14 I = Math.Round(I * 100) / 100
15 GraphicsWindow.DrawText(130, 80, V1 + " V")
16 GraphicsWindow.DrawText(270, 80, V2 + " V")
17 GraphicsWindow.DrawText(415, 230, V3 + " V")
18 GraphicsWindow.DrawText(34, 100, I + " A")
19 EndIf
20 EndIf
21 EndSub
清单 12-6:更新文本框
UpdateUserInterface()子程序首先会重新绘制选定的背景图像。如果开关处于关闭位置,第 3 行的If语句为假,子程序结束;用户界面不会显示任何计算结果(因为电路中没有电流流动)。但是,如果开关是开启状态(这意味着当前背景图像设置为onImg),子程序将继续计算I、V1、V2和V3的值。它首先收集四个文本框的内容(第 4 到第 7 行)。然后,它通过将R1、R2和R3的值相加来计算总电阻(第 8 行)。如果总电阻大于 0(第 9 行),子程序将计算电路中流过的电流(I)(第 10 行)以及V1、V2和V3的值,并将每个值四舍五入到最接近的百分位(第 11 到第 14 行)。然后,子程序会将计算出的值显示在背景图像的正确位置(第 15 到第 18 行)。
本程序的大部分工作是设计 GUI(绘制背景图像并将文本框放置在背景图像之上)。然后,你需要编写处理事件的代码,执行计算并显示结果。恭喜你,你已经创建了一个虚拟电路!
在下一节中,你将编写一个 GUI 程序,解释 Small Basic 库中的另一个对象——Flickr对象。
动手试试 12-3
想想办法将这个模拟程序改造成其他形式。使用不同的背景图片,例如建筑蓝图、披萨,或是你所在社区的 Google Maps 照片。然后更新文本框的位置以及输入逻辑/数学,以匹配你新的主题。前往 tiny.cc/sharesimulation/ 展示你的程序,并看看其他人创建了什么。
编程自己的图像查看器
在本节中,你将创建一个名为 ImageViewer.sb 的图像查看器,根据用户输入的搜索内容显示来自 Flickr(一种照片分享网站)的图像。Small Basic 提供了一个名为 Flickr 的对象,它可以从 Flickr 网站获取图片:* www.flickr.com/*。 图 12-7 显示了该程序的 GUI。

图 12-7: ImageViewer.sb 程序的示例输出
注意
你需要使用 Small Basic 1.1 或更高版本才能使用 Flickr 对象。
ImageViewer.sb 程序包括一个文本框,用户可以在其中输入搜索标签,以及一个按钮(标记为“下一步”)。当用户点击按钮时,程序使用 Flickr 对象根据用户的搜索标签获取(并显示)一张图片。该程序在 Listing 12-7 中展示。
1 ' ImageViewer.sb
2 GraphicsWindow.DrawText(10, 14, "Search for an image:")
3 tagText = Controls.AddTextBox(140, 10)
4 Controls.SetSize(tagText, 160, 26)
5 Controls.AddButton("Next", 305, 10)
6
7 Controls.ButtonClicked = OnButtonClicked
8
9 Sub OnButtonClicked
10 tag = Controls.GetTextBoxText(tagText)
11 If (tag <> "") Then
12 img = ImageList.LoadImage(Flickr.GetRandomPicture(tag))
13 If (img = "") Then
14 GraphicsWindow.ShowMessage("No images found.", "Sorry.")
15 Else
16 GraphicsWindow.Width = ImageList.GetWidthOfImage(img)
17 GraphicsWindow.Height = ImageList.GetHeightOfImage(img) + 40
18 GraphicsWindow.DrawImage(img, 0, 40)
19 EndIf
20 EndIf
21 EndSub
Listing 12-7:从 Flickr 加载图片
程序从设计 GUI 开始(第 2–5 行),并注册 ButtonClicked 事件处理程序(第 7 行)。当按钮被点击时,OnButtonClicked() 子程序从文本框中获取搜索文本,并将其保存在 tag 变量中(第 10 行)。如果 tag 不为空(第 11 行),代码会使用给定的 tag 文本搜索 Flickr,寻找一张随机图片,然后通过 Flickr.GetRandomPicture() 获取该图片的 URL(第 12 行)。
该 URL 会传递给 ImageList.LoadImage(),它会从文件或互联网上加载图片,并将其保存到 img 变量中(第 12 行)。如果 img 为空,意味着 Flickr 没有找到符合用户标签的图片,你会通过消息框通知用户(第 14 行)。如果 Flickr 找到了图片,你会调整图形窗口的大小以适应加载的图片尺寸(第 16–17 行),并将图片绘制在文本框和按钮的正下方(第 18 行)。
尝试一下 12-4
编写一个程序,询问用户他们最喜欢的动物是什么。然后,使用 Flickr 对象搜索该动物并显示返回的图片。接下来,询问用户:“喜欢这个吗?”并显示两个按钮,分别标记为“是”和“否”。如果用户点击“是”,则在消息框中显示 太好了!。如果他们点击“否”,则显示该动物的另一张随机图片,并询问:“这个怎么样?”继续这些步骤,直到用户点击“是”。你刚刚将一个简单的程序变成了一个游戏!
编程挑战
如果遇到困难,请查看 nostarch.com/smallbasic/,获取解决方案以及更多的资源和供教师和学生使用的复习问题。
-
在这个程序中,你将创建一个隐藏宝藏游戏。打开本章文件夹中的HiddenTreasure_Incomplete.sb文件。当你运行程序时,你将看到以下界面。
![image]()
游戏的目的是猜测隐藏宝藏的位置。玩家通过按下四个按钮之一来进行猜测。如果猜对了,他们将获得 10 美元。否则,他们将失去 5 美元。游戏在 10 轮后结束。按照程序源代码中的注释编写缺失的代码,完成程序。
-
在这个练习中,你将创建一个程序来计算参加海洋世界特别表演的总费用。打开本章文件夹中的SeaWorld_Incomplete.sb文件。当你运行程序时,你将看到以下用户界面。用户输入他们想购买的成人票、老年票、学生票和 VIP 票的数量,然后点击“计算”按钮来计算总费用。完成程序,使其在用户点击“计算”时显示总费用。
![image]()
第十四章:13
重复的 For 循环

你有没有注意到,无论你多少次倒垃圾、洗脏碗和做洗衣,最后总是要再做一次?如果每个任务只需要做一次,然后你创建一个机器人版本的自己来每次代替做呢?那会很棒!
在 Small Basic 宇宙中,自动化重复性任务轻而易举。你只需要为一个重复性任务编写一次代码,然后就可以使用循环重复执行该任务,无论你需要多少次。
Small Basic 使用两种类型的循环语句:For 循环 和 While 循环。在本章中,你将学习For循环,深入探索嵌套For循环,并创建一些利用你计算机对重复任务的热情的程序。你将学会在各种实际应用中使用For循环。让我们开始循环吧!
For 循环
假设你想写一个程序,显示九的乘法表:1 × 9,2 × 9,3 × 9,一直到 10 × 9。你第一次尝试可能是这样的:
TextWindow.WriteLine(" 1 x 9 = " + (1 * 9))
TextWindow.WriteLine(" 2 x 9 = " + (2 * 9))
TextWindow.WriteLine(" 3 x 9 = " + (3 * 9))
TextWindow.WriteLine(" 4 x 9 = " + (3 * 9))
TextWindow.WriteLine(" 5 x 9 = " + (3 * 9))
TextWindow.WriteLine(" 6 x 9 = " + (3 * 9))
TextWindow.WriteLine(" 7 x 9 = " + (3 * 9))
TextWindow.WriteLine(" 8 x 9 = " + (3 * 9))
TextWindow.WriteLine(" 9 x 9 = " + (3 * 9))
TextWindow.WriteLine("10 x 9 = " + (10 * 9))
呼!看看这段代码墙!虽然 Small Basic 让你轻松地复制和粘贴选定的语句,但这个程序重复了很多代码。如果你想显示乘法表直到 100 或 1000 呢?显然,这不是编写程序的最佳方式。这里是一个使用For循环来实现相同结果的程序版本:
For N = 1 To 10
TextWindow.WriteLine(N + " x 9 = " + (N * 9))
EndFor
运行这个程序,看看会发生什么。难道这不是比写出每一行代码更简单吗?现在你已经看到了循环的威力!
循环每次运行相同的语句,但N的值不同。首先,代码将N的值设置为 1,这是我们希望从中开始创建乘法表的值。接下来,它运行For和EndFor关键字之间的所有语句。在这种情况下,它运行WriteLine()方法,将N替换为当前值。这称为循环的迭代。
然后它将N设置为 2。N的值与循环的结束值(或终止值)进行比较,在这个例子中为 10。如果N小于 10,For循环体内的语句将再次执行,完成下一次循环迭代。请注意,For循环会在每次迭代时自动将N增加 1。这个过程会继续,使用N = 3,然后是N = 4,直到N = 10。
在程序执行第十次迭代后,它会跳转到EndFor关键字后的语句(如果有的话),并且循环完成。
现在你已经看到了基本的For循环的实际操作,看看图 13-1 中的语法吧。

图 13-1:基本 For 循环的语法
每个For循环以关键字For开始。For和EndFor之间的语句被称为For循环的主体。变量N是循环控制变量(或循环计数器)。它控制循环执行的次数,并且像程序中的其他变量一样使用。1 To 10部分决定了循环运行的次数。
需要注意的是,检查循环是否执行的条件是在循环顶部进行的。例如,下面的代码将N设置为 1,然后将其与终止值-10 进行比较。因为 1 大于-10,所以代码根本不会执行:
For N = 1 To -10
TextWindow.WriteLine(N) ' This won't be executed
EndFor
我们来看一些有趣的例子,向你展示如何使用For循环。
动手试一试 13-1
思考一些你可以通过循环自动化的其他重复性任务。描述一个你会用For循环构建的程序。
神奇的移动文字
在这个例子中,你将创建一个程序,使一个单词或句子从文本窗口的左侧移动到右侧。图 13-2 显示了每次迭代中,上一轮显示的单词消失,因此文字看起来像是动画效果,正向屏幕右侧移动。

图 13-2:使用 Write() 方法将一个单词移动到文本窗口
回想一下在第二章中,你使用CursorLeft属性将文本显示在文本窗口的不同位置。在这个例子中,你将CursorLeft设置为 0,并使用Write()方法写出单词。稍作延迟后,你将CursorLeft改为 1,再次写出单词。接着,你将CursorLeft改为 2,再到 3,依此类推。通过For循环,你将自动化这一过程,使得单词看起来像是从左到右在文本窗口中移动。请输入程序代码,见列表 13-1。
1 ' MovingWord.sb
2 For N = 0 To 40
3 TextWindow.CursorLeft = N
4 TextWindow.Write(" Moving") ' Erases the previous line
5 Program.Delay(250) ' Delays so you can read it
6 EndFor
7 TextWindow.WriteLine("")
列表 13-1:将一个单词移动到文本窗口
该程序启动一个从N = 0 To 40的循环(第 2 行)。在每次迭代中,它将CursorLeft属性设置为循环计数器N(第 3 行),然后使用Write()方法写出单词(第 4 行)。Moving前面的空格有助于擦除之前的单词。第 5 行的Program.Delay(250)调用使程序在开始下一次迭代之前等待 250 毫秒。当循环结束时,程序会写出一个空行(第 7 行)。
让我们进入另一个例子。
提示
虽然不是必需的,但将For循环主体中的语句进行缩进能使代码更易读。
动手试一试 13-2
修改列表 13-1 来为你自己创建动画,发送给你的朋友或家人,并与他们分享。我的例子是“我喜欢塔可饼!”
加起来
在编程中,循环有不同的使用方式。循环的一个重要用途被称为累加器循环,它在每次迭代时累加(或加总)一个值。累加器循环常用于程序中记录数值。
假设你需要计算从 1 到 10 的所有整数的和:1 + 2 + 3 + ... + 10。这正是清单 13-2 中的程序所做的。
1 ' Sum.sb
2 sum = 0
3 For N = 1 To 10
4 sum = sum + N ' Adds the new value of N to the sum
5 EndFor
6 TextWindow.WriteLine("sum = " + sum)
清单 13-2:使用 For 循环添加数字
该程序使用一个名为sum的变量来保存累计值(这个变量通常被称为累加器)。程序首先将sum初始化为 0(第 2 行)。然后,一个名为N的循环计数器在For循环中从 1 运行到 10(第 3 行)。在每次迭代中,程序使用第 4 行的语句将N的值加到累加器中。该语句将N的当前值加到sum的当前值,并将结果重新存储回sum中。第一次迭代后,sum为 1(0 + 1);第二次迭代后,sum为 3(1 + 2);第三次迭代后,sum为 6(3 + 3);以此类推。当循环结束时,程序在第 6 行显示sum变量的值:sum = 55。
尝试 13-3
当伟大的数学家卡尔·高斯首次上学时,他的老师要求全班同学计算 1 到 100 之间所有数字的和,即 1 + 2 + 3 + 4 + ... + 100。高斯看了一眼题目,立即把答案放在了老师的桌子上。老师惊讶了——高斯是对的!写一个程序来找出高斯脑海中算出来的答案。当然,高斯当时并没有使用 Small Basic,但他确实找到了一个捷径。你能找出他的方法吗?
格式化输出
显示程序输出的方式通常与显示的信息本身同样重要。如果输出难以阅读,人们就无法理解信息的含义。良好的显示布局是程序设计的重要部分,但正确的格式化可能会很繁琐。为了简化这一过程,你可以使用For循环。例如,使用For循环编写一个程序,以表格格式输出 1 到 5 的平方(见图 13-3)。

图 13-3: SquareTable.sb 的输出
输入并运行清单 13-3 中的程序。
1 ' SquareTable.sb
2 TextWindow.Title = "Table of Squares"
3 TextWindow.WriteLine(" Number Square")
4 TextWindow.WriteLine("======== =========")
5
6 For N = 1 To 5
7 TextWindow.CursorLeft = 3 ' Moves to middle of col 1
8 TextWindow.Write(N) ' Writes the number
9 TextWindow.CursorLeft = 14 ' Moves to next column
10 TextWindow.WriteLine(N * N) ' Writes its square
11 EndFor
清单 13-3:使用 For 循环显示表格数据
第 3 至 4 行编写了两列表格的标题。第 6 行的循环写入五个数字及其平方。TextWindow.CursorLeft 属性设置了每列下方的期望位置(第 7 行和第 9 行)。每次代码循环时,它会将正确的值显示在适当的位置。
尝试 13-4
那首著名的歌曲《圣诞十二日》是这样的:“在圣诞节的第一天,我的真爱给了我一只停在梨树上的鹧鸪。在圣诞节的第二天,我的真爱给了我两只海鸽和一只停在梨树上的鹧鸪……”如此继续,直到第 12 天。到了第十二天,歌手收到了 12 + 11 + … + 2 + 1 份礼物。编写一个程序,显示每一天收到的总礼物数。输出结果应包括两列:天数和当天收到的总礼物数。
绘制各种线条
你可以使用 For 循环来改变各种值,包括视觉显示。示例 13-4 在图形窗口中绘制了 10 条逐渐加宽的线条。
1 ' Lines.sb
2 GraphicsWindow.Title = "Lines"
3 GraphicsWindow.PenColor = "Blue"
4 For N = 1 To 10
5 GraphicsWindow.PenWidth = N
6 y = N * 15 ' Vertical position of the line
7 GraphicsWindow.DrawLine(0, y, 200, y)
8 EndFor
示例 13-4:每次迭代增加线条宽度
在设置窗口标题和画笔颜色(第 2–3 行)之后,程序开始执行一个名为 N 的 For 循环,该循环的计数器从 1 运行到 10(第 4 行)。在每次迭代中,程序将画笔的宽度设置为当前的 N 值(第 5 行),设置线条的垂直位置(第 6 行),然后绘制一条 200 像素长的线(第 7 行)。输出结果如图 13-4 所示。

图 13-4: Lines.sb 的输出
尝试示例 13-5
以下程序做了什么?运行程序检查你的答案。
For N = 1 To 200
GraphicsWindow.PenColor = GraphicsWindow.GetRandomColor()
x2 = Math.GetRandomNumber(300)
y2 = Math.GetRandomNumber(300)
GraphicsWindow.DrawLine(0, 0, x2, y2)
EndFor
更改步长
上一部分向你展示了 For 循环的语法,该语法在每次迭代后自动将循环计数器增加 1。但 For 循环有一个通用形式,它允许你控制循环控制变量的 Step 大小,从而按照你想要的任何幅度增加或减少它。这里是 For 循环的一般形式:
For N = A To B Step C
Statement(s)
EndFor
它的工作方式类似于你之前看到的简化循环。但与每次递增循环计数器 N 1 的情况不同,你可以决定如何改变 N。你通过设置 Step 大小中的值 C 来实现这一点,C 可以是正数、负数或任何 Small Basic 表达式。让我们看看一些示例,展示如何使用这种通用的 For 循环形式。
按二递减计数
在这个例子中,程序从一个起始值(此处为 10)开始倒计时到 0,每次减去 2,因此程序在文本窗口中写出数字 10、8、6、4、2、0。输入并运行示例 13-5 中的程序。
1 ' CountDown.sb
2 For N = 10 To 0 Step -2 ' Uses a negative step size
3 TextWindow.WriteLine(N)
4 EndFor
示例 13-5:用Step 进行倒计数
在 Step 大小(第 2 行)使用了负值,以便在每次迭代后将循环计数器的值减少 2。
这是输出结果:
10
8
6
4
2
0
制作分数步长
Step 大小不一定非得是整数值。你也可以使用小数值,如在示例 13-6 中所示。
1 ' DecimalStep.sb
2 GraphicsWindow.FontSize = 10
3 GraphicsWindow.BrushColor = "Black"
4
5 yPos = 0
6 For angle = 0 To (2 * Math.PI) Step 0.3
7 xPos = 100 * (1 + Math.Sin(angle))
8 GraphicsWindow.DrawText(xPos, yPos, "Hello")
9 yPos = yPos + 8
10 EndFor
示例 13-6:用文本做设计
在这个例子中,循环计数器是一个角度(以弧度为单位),它使用从 0 到 2π的值,步进为 0.3(第 6 行)。在每次迭代中,计算该角度的正弦值,并使用该值设置光标的水平位置(第 7 行)。然后在该位置显示单词Hello(第 8 行),并调整变量yPos来设置下一个输出文本的垂直位置(第 9 行)。尝试不同的Step大小可以创造出一些非常酷的效果,比如图 13-5 中展示的波浪设计。

图 13-5: DecimalStep.sb 的输出
试一试 13-6
编写一个程序,计算从 5 到 25 所有奇数的和。
嵌套循环
For循环体中的语句可以是任何 Small Basic 语句,包括另一个For循环。嵌套是指将一个For循环放在另一个For循环内部(不涉及鸟类)。使用嵌套循环可以创建二维或更多维度的迭代。这种技巧很重要,可以用来解决广泛的编程问题。
要理解嵌套For循环的概念,你将查看一个程序,它使得计算机“跳”四次,每跳一次后“拍手”三次。因为程序需要计算两个动作(跳跃和拍手),所以需要使用两个循环,如示例 13-7 所示。外部循环的计数器j从 1 到 4,内部循环的计数器c从 1 到 3。
1 ' NestedLoops.sb
2 For j = 1 To 4 ' The jump counter
3 TextWindow.Write("Jump " + j + ": ")
4 For c = 1 To 3 ' The clap counter
5 TextWindow.Write("Clap " + c + " ")
6 EndFor
7 TextWindow.WriteLine("")
8 EndFor
示例 13-7: 嵌套For 循环
在外循环的第一次迭代中(即j = 1),内循环重复三次(对应c的三个值);每次迭代时,它都会写出单词Clap,后面跟着一个空格,当前c的值,再加上一个空格(第 5 行)。当你像这样嵌套For循环时,内循环会在外循环的每次迭代中执行所有迭代。因此,外循环的第一次迭代会使程序显示Jump 1: Clap 1 Clap 2 Clap 3。当内循环结束时,程序输出一个空行(第 7 行),将光标移到下一行的开头,然后第二次外循环以j = 2开始。内循环再次运行,c = 1、c = 2和c = 3,这会导致程序显示Jump 2: Clap 1 Clap 2 Clap 3。如此继续,程序依次显示Jump 3: Clap 1 Clap 2 Clap 3,然后是Jump 4: Clap 1 Clap 2 Clap 3,最后程序结束。也许你的电脑想成为啦啦队员!
图 13-6 有助于解释程序的工作原理。外圈表示外部循环每次运行的情况:例如,在外圈的顶部,当外循环中的j = 1时,内循环运行三次,c的值依次为 1、2 和 3。跟随外循环并思考每一次内循环。继续直到走完外圈一圈。

图 13-6: 嵌套循环.sb 的工作原理
输出应该如下所示:
Jump 1: Clap 1 Clap 2 Clap 3
Jump 2: Clap 1 Clap 2 Clap 3
Jump 3: Clap 1 Clap 2 Clap 3
Jump 4: Clap 1 Clap 2 Clap 3
现在让我们来看一些需要嵌套 For 循环的其他问题吧!
为了乐趣进行镶嵌
在这个例子中,应用程序通过在图形窗口上盖上一个小图像来覆盖窗口。完整的程序显示在 列表 13-8 中。
1 ' Stamp.sb
2 GraphicsWindow.Title = "Stamp"
3
4 path = Program.Directory
5 img = ImageList.LoadImage(path + "\Trophy.ico")
6
7 width = ImageList.GetWidthOfImage(img) ' Width of image
8 height = ImageList.GetHeightOfImage(img) ' Height of image
9
10 GraphicsWindow.Width = 8 * width ' 8 columns
11 GraphicsWindow.Height = 3 * height ' 3 rows
12
13 For row = 0 To 2 ' 3 rows
14 For col = 0 To 7 ' 8 columns
15 GraphicsWindow.DrawImage(img, col * width, row * height)
16 EndFor
17 EndFor
列表 13-8:在图形窗口中盖上图案
将本章文件夹中的 Trophy.ico 文件复制到你的应用程序文件夹中,然后运行这个程序查看结果。你的屏幕应该像 图 13-7 一样。干得好,冠军!

图 13-7: Stamp.sb 的输出
程序从你的应用程序文件夹加载一个图像文件 (Trophy.ico) 并将图像的标识符保存到名为 img 的变量中(第 5 行)。这是通过调用 ImageList 对象的 LoadImage() 方法完成的。程序然后使用 ImageList 对象的方法来告诉你加载图像的宽度和高度(以像素为单位)(第 7–8 行)。图像的标识符(即 img 变量)作为参数传递给调用的方法。在第 10–11 行,程序调整图形窗口的大小,使其可以容纳八列和三行图像的副本。接着,程序使用一个嵌套循环在不同的位置盖上图像。外层循环运行三行,内层循环运行八列,总共进行 24 次迭代(3 × 8)(第 13–14 行)。在每次迭代中,图像的 x 和 y 位置根据图像的尺寸进行计算,并在该位置绘制图像(第 15 行)。现在,你的奖杯收藏比迈克尔·乔丹的还要大!
尝试一下 13-7
更新 列表 13-8,用不同的图像替代奖杯图标。然后展示给你的朋友和家人看!
多重嵌套层级
你可以有超过两个嵌套层级。列表 13-9 显示所有可能的 25 分、10 分和 5 分硬币的组合,总和为 50 分。
1 ' CoinsAdder.sb
2 TextWindow.WriteLine("Quarters Dimes Nickels")
3 TextWindow.WriteLine("-------- ----- -------")
4
5 For Q = 0 To 2 ' Quarters
6 For D = 0 To 5 ' Dimes
7 For N = 0 To 10 ' Nickels
8 If (Q * 25 + D * 10 + N * 5 = 50) Then
9 TextWindow.Write(Q)
10 TextWindow.CursorLeft = 13
11 TextWindow.Write(D)
12 TextWindow.CursorLeft = 24
13 TextWindow.WriteLine(N)
14 EndIf
15 EndFor
16 EndFor
17 EndFor
列表 13-9:列出所有加起来为 50 分的硬币组合
第一个循环最初通过设置 Q = 0 来跟踪 25 分硬币。第二个循环运行六次,计数所有的 10 分硬币:For D = 0 To 5。对于第二个循环的每次执行,第三个循环运行 11 次,跟踪 5 分硬币:For N = 0 To 10。这意味着第 8 行的 If 条件会被检查 198 次(3 × 6 × 11)!如果硬币的总和为 50 分,就会显示该组合(第 9–13 行)。在循环过程中,代码使用 CursorLeft 属性来正确对齐列和行。以下是输出:
Quarters Dimes Nickels
-------- ----- -------
0 0 10
0 1 8
0 2 6
0 3 4
0 4 2
0 5 0
1 0 5
1 1 3
1 2 1
2 0 0
尝试一下 13-8
编写一个程序,找出所有小于 20 的三整数集合,这些整数可以作为直角三角形的三边。
编程挑战
如果你卡住了,可以访问 nostarch.com/smallbasic/ 查找解决方案和更多的资源、教师和学生的复习问题。
-
编写一个
For循环,显示以下输出:I had 1 slices of pizza. I had 2 slices of pizza. I had 3 slices of pizza. ... I had 10 slices of pizza. -
尽管在上一个练习中的比萨看起来很好吃,但它在语法上并不正确,因为程序输出的是
1 slices of pizza。修正程序,使其输出符合语法规则(这样你就不会让你的英语老师尴尬了)。(提示:在For循环中使用If语句。) -
我们为你设计了一个游戏,来考察 Alice 的乘法能力,这样她就能准备好应对女王的提问。程序会生成 10 道随机乘法题,并要求 Alice 输入每道题的答案。每答对一道题,Alice 得一分。如果她输入错误的答案,程序会显示正确答案。程序最后会显示她的总得分。重新创建这个程序,运行它,并解释它是如何工作的:
score = 0 For N = 1 To 10 ' Asks 10 questions n1 = Math.GetRandomNumber(10) n2 = Math.GetRandomNumber(10) TextWindow.Write(n1 + " x " + n2 + "? ") ans = TextWindow.ReadNumber() If (ans = n1 * n2) Then ' Increases the score score = score + 1 Else ' Shows the correct answer TextWindow.WriteLine("Incorrect --> " + (n1 * n2)) EndIf EndFor TextWindow.WriteLine("Your score is: " + score + "/10") -
编写一个程序,绘制以下图像。(提示:使用
For循环绘制每个角落的线条模式。)![image]()
第十五章:14
创建条件的 While 循环

在第十三章中,我们向你展示了如何使用For循环来重复执行代码一定次数。当你确切知道要重复执行多少次代码时,For循环是理想选择。While是另一个 Small Basic 的关键字,可以用来创建循环。当你事先不知道要重复多少次循环时,While循环非常有用,因为While循环会一直运行代码,直到条件为假。
While循环的条件就像父母一直告诉你要把房间打扫得一尘不染,或者你吃感恩节火鸡直到吃撑一样!当循环的条件变为假时,循环结束,程序继续执行。
在本章中,你将学习如何编写While循环,并使用它们来验证用户输入和制作游戏。While循环是一个强大的编程概念,一旦掌握了它们,你将能够制作各种酷炫的应用程序。
何时使用 While 循环
假设你想制作一个数字猜谜游戏,游戏会随机选择一个 1 到 100 之间的数字,并提示玩家猜测。如果玩家猜错了,游戏会告诉他们猜测的数字是比秘密数字高还是低,然后再次提示玩家猜测。游戏会一直提示玩家猜数字,直到他们猜对为止。
在这里,For循环不是最佳选择,因为你无法预测玩家猜测秘密数字所需的次数。也许玩家第一次就猜对了,或者可能需要猜 100 次!在这种情况下,While循环是完美的选择。
在下一节中,你将学习While循环的语法,并用它来创建自己的数字猜谜游戏。
编写 While 循环
请尝试清单 14-1 中的代码。
1 ' GuessMyNumber.sb
2 num = Math.GetRandomNumber(100) ' From 1 to 100
3 ans = 0 ' Any value that isn't equal to num
4 While (ans <> num) ' Repeats as long as the guess is wrong
5 TextWindow.Write("Enter your guess [1-100]: ")
6 ans = TextWindow.ReadNumber()
7 If (ans = num) Then ' Player guessed correctly
8 TextWindow.WriteLine("Good job! You get sprinkles!")
9 ElseIf (ans > num) Then
10 TextWindow.WriteLine("Too High. Lower your standards.")
11 Else
12 TextWindow.WriteLine("Too Low. Aim for the stars!")
13 EndIf
14 EndWhile
清单 14-1:数字猜谜游戏
程序随机选择一个 1 到 100 之间的数字,并将其赋值给num(第 2 行)。然后,创建一个名为ans的变量,用来保存玩家的猜测,并将其初始化为 0(第 3 行)。我们将初始值设置为 0,因为它需要与正确答案不同。让我们仔细看看While循环的第一行(第 4 行):
While (ans <> num)
这段代码的意思是,“只要ans不等于num,就执行While和EndWhile之间的语句。”
首先,测试条件(ans <> num)会被评估。如果为真,程序会执行循环体内的语句,并继续重复,直到条件变为假。当测试条件变为假时,循环结束,程序继续执行EndWhile关键字后的下一条语句。图 14-1 的流程图展示了While循环的工作原理。

图 14-1:While 循环的流程图
在数字猜谜游戏中,当程序第一次执行第 4 行时,条件(ans <> num)为真(因为我们知道num不可能是 0),因此循环执行其主体中的语句(第 5 到 13 行)。在循环的每次迭代中,玩家被提示输入一个猜测(第 5 行),该猜测保存在变量ans中(第 6 行)。然后,代码将玩家的猜测与秘密数字进行比较。如果玩家猜对了(第 7 行),代码会显示Good Job! You get sprinkles!并跳转到EndIf后的语句。在这个例子中,它找到EndWhile,将程序带回检查While循环的条件。由于ans现在等于num,测试条件为假,While循环终止,程序结束(因为EndWhile后没有语句)。
如果玩家的猜测不正确,代码会检查猜测是否高于秘密数字(第 9 行)。如果是高于的,程序会显示Too High. Lower your standards.然后进入下一轮。如果玩家的猜测低于秘密数字(第 11 行的Else语句),程序会显示Too Low. Aim for the stars!(第 12 行),并开始另一轮。
这里是一个运气相当好的用户玩游戏的例子:
Enter your guess [1-100]: 50
Too High. Lower your standards.
Enter your guess [1-100]: 25
Too Low. Aim for the stars!
Enter your guess [1-100]: 37
Good Job! You get sprinkles!
多玩几次这个游戏,看看它是如何工作的!
注意
尽管 Small Basic 并没有强制要求这样做,我们为了让程序更易读,通常会在While循环的条件周围加上括号,并缩进While循环的主体部分。
在接下来的部分,我们将展示如何使用While循环检查用户输入的数据。
试试看 14-1
如果一只土拨鼠能扔木头,那么它能扔多少木头呢?打开本章文件夹中的Woodchuck.sb文件,运行它来回答这个古老的问题。然后想出一些方法来改进程序。
验证您的输入
当你编写一个需要从用户处读取数据的程序时,应该始终在继续执行程序之前检查输入的数据。这被称为验证。在本节中,我们将向你展示如何使用While循环来确保用户输入正确的数据。
假设你需要用户输入一个 1 到 5 之间的数字(包括 1 和 5)。如果他们输入的数字小于 1 或大于 5,你需要提示他们重新输入一个数字。Listing 14-2 展示了如何使用While循环来实现这一点。
1 ' InputValidation.sb
2 num = -1 ' Invalid value (to force a pass through the loop)
3
4 While ((num < 1) Or (num > 5))
5 TextWindow.Write("Enter a number between 1 and 5: ")
6 num = TextWindow.ReadNumber()
7 EndWhile
8 TextWindow.WriteLine("You entered: " + num)
Listing 14-2:使用 While 循环检查输入的数字
第 2 行将变量num(用于存储用户输入的数字)设置为-1。这使得While循环的条件(第 4 行)为真,因此循环的主体至少执行一次。虽然在这个例子中,如果没有第 2 行的初始化语句(因为变量num会被认为是 0),循环依然能正常运行,但我们建议你始终初始化变量,而不要依赖它们的默认值。这将帮助你避免未来的错误。
该程序提示用户输入一个数字,并将他们的输入赋值给num变量(第 5–6 行)。然后循环再次执行。如果num小于 1 或大于 5(用户输入了无效数字),循环体会再次执行,提示用户重新输入数字。如果num在 1 到 5 之间(包括 1 和 5),循环结束,程序跳到第 8 行显示数字。
提示
确保在While循环的测试条件中使用任何变量之前对其进行初始化。如果不初始化,程序可能会跳过循环!
现在你知道如何使用While循环验证用户输入了。
尝试一下 14-2
编写一个程序,询问用户是否认为海绵宝宝能成为圣诞老人,然后提示他们输入Y(是)或N(否)。他们也可以输入y或n。编写一个While循环,仅接受Y、y、N或n作为有效输入。每次用户输入错误时,告诉他们哪里出了问题。
无限循环
如果While循环的条件始终不为假,循环将永远运行,形成无限循环。有时这会导致问题,但有时无限循环很有用,比如当你想让游戏永远运行时。
但是如何在 Small Basic 中创建无限循环呢?有几种方法可以做到这一点,但这是许多 Small Basic 程序员常用的快捷方式:
While ("True")
TextWindow.WriteLine("Loop forever!")
EndWhile
在这段代码中,循环的条件始终为真;循环永不停止,并且永远显示Loop forever!。要查看这一点,你将编写一个简单的游戏,测试孩子们的加法技能。完整代码见清单 14-3。运行这个程序,看看它是如何工作的。
1 ' AddTutor.sb
2 While ("True")
3 num1 = Math.GetRandomNumber(10) ' Sets num1 between 1 and 10
4 num2 = Math.GetRandomNumber(10) ' Sets num2 between 1 and 10
5 correctAns = num1 + num2 ' Adds both numbers
6 TextWindow.Write("What is " + num1 + " + " + num2 + "? ")
7 ans = TextWindow.ReadNumber() ' User enters an answer
8 If (ans = correctAns) Then ' Checks if the answer is correct
9 TextWindow.WriteLine("This is correct.")
10 Else ' Gives the correct answer
11 TextWindow.WriteLine("Sorry. The answer is " + correctAns)
12 EndIf
13 EndWhile
清单 14-3:一个向用户提问加法题目的程序
在第 3 和第 4 行,num1和num2被设置为 1 到 10 之间的随机数字。第 5 行将它们相加得到正确答案。第 6 行提示用户输入正确答案。第 7 行获取用户的答案。第 8 行检查答案是否正确,如果正确,第 9 行告诉他们答案正确。否则,第 11 行告诉他们正确答案是什么。游戏将一直运行。当用户想退出时,可以通过点击应用程序窗口右上角的 X 图标关闭应用程序。
提示
你可以在While循环内使用Goto语句跳转到循环外的标签,以便退出循环。
现在是时候将你在本章所学的内容付诸实践,设计一个完整的游戏了。在继续阅读之前,去冰箱拿些大脑食品吧!
尝试一下 14-3
更改AddTutor.sb程序,以便当玩家答错时不提供正确答案。相反,程序应告诉玩家他们的答案是错误的,并让他们再试一次。
创建一个石头剪刀布游戏
在这一节中,你将创建一个石头剪刀布游戏,玩家与计算机对战。图 14-2 显示了这个游戏的用户界面。三个按钮分别代表石头、布和剪刀。玩家通过点击其中一个按钮来做出选择。然后计算机随机选择一个动作。决定胜者的规则是布胜石头,石头胜剪刀,剪刀胜布。

图 14-2:石头剪刀布游戏的用户界面
图像P1、P2和P3显示玩家的选择,图像C1、C2和C3显示计算机的选择。图像W0、W1、W2和W3显示每轮游戏的结果。在图 14-2 中,除了背景图像外,你还可以看到代表三种按钮的石头、布和剪刀图像。
步骤 1:打开启动文件
打开本章文件夹中的RockPaper_Incomplete.sb文件并跟随操作。该文件夹包含了你玩这个游戏所需的所有图像。启动文件显示在清单 14-4 中,包含了游戏的主要部分。它还包含了你需要添加的所有子程序的空占位符。
1 ' RockPaper_Incomplete.sb
2 GraphicsWindow.Title = "Rock, Paper, Scissors"
3 GraphicsWindow.CanResize = "False"
4 GraphicsWindow.Width = 480
5 GraphicsWindow.Height = 360
6
7 path = Program.Directory
8 GraphicsWindow.DrawImage(path + "\Bkgnd.png", 0, 0)
9 choice1 = 0 ' 0 = Unknown; 1 = Rock; 2 = Paper; 3 = Scissors
10 GraphicsWindow.MouseDown = OnMouseDown
11
12 While ("True") ' Loops forever
13 If (choice1 <> 0) Then ' If player made a choice
14 blankImg = path + "\W3.png" ' Clears last result
15 GraphicsWindow.DrawImage(blankImg, 115, 310)
16 choice2 = Math.GetRandomNumber(3) ' 1 to 3
17 SwitchImages() ' Shows player and computer choices
18 ShowWinner() ' Shows image for the result
19 choice1 = 0 ' Ready for another round
20 EndIf
21 Program.Delay(10) ' Waits a little, then checks again
22 EndWhile
清单 14-4:设置窗口和选择
如果你现在运行程序,你看到的只有背景图像,因为你还没有创建任何子程序。你会做到的,但首先让我们来看看游戏的设置和主循环。首先,设置图形窗口的大小,然后绘制背景图像(第 2–8 行)。变量choice1保存玩家的选择:0表示未知,1表示石头,2表示布,3表示剪刀。为了开始,我们将choice1设置为0,因为玩家还没有做出选择(第 9 行)。接着,我们为MouseDown事件注册一个处理器,以便能够得知玩家何时点击三个按钮中的一个(第 10 行)。然后,游戏的主循环开始(第 12–22 行)。
循环持续检查choice1的值。正如你马上会看到的,当玩家做出选择时,OnMouseDown()子程序会改变这个变量。如果choice1是 0,循环会等待 10 毫秒(第 21 行),然后再次检查。使用循环可以让程序等待choice1变为非 0 值(这叫做轮询;类似于长途旅行中反复问“我们到了吗?”)。当choice1变为非 0 值时(第 13 行),If块的主体会被执行(第 14–19 行)。我们绘制图像W3来显示一个空的结果(第 14–15 行)。接下来,我们将计算机的选择choice2设置为 1 到 3 之间的随机值(第 16 行)。然后,我们调用SwitchImages()来显示与choice1和choice2相对应的图像(第 17 行)。接着,我们调用ShowWinner()来显示这一轮游戏的结果(第 18 行)。最后,我们将choice1重置为0,告诉OnMouseDown()子程序主循环已经准备好进行新的一轮游戏(第 19 行)。
接下来,你将逐个添加每个子程序。
步骤 2:添加 MouseDown 处理程序
现在我们来处理 MouseDown 事件,确定玩家的选择。在程序的底部添加 清单 14-5 中的 OnMouseDown() 子程序。
1 Sub OnMouseDown
2 If (choice1 = 0) Then ' Ready for another round
3 y = GraphicsWindow.MouseY ' Vertical click position
4 If ((y > 80) And (y < 120)) Then ' Within range
5 x = GraphicsWindow.MouseX ' Horizontal click
6 If ((x > 40) And (x < 80)) Then ' Rock
7 choice1 = 1
8 ElseIf ((x > 110) And (x < 150)) Then ' Paper
9 choice1 = 2
10 ElseIf ((x > 175) And (x < 215)) Then ' Scissors
11 choice1 = 3
12 EndIf
13 EndIf
14 EndIf
15 EndSub
清单 14-5:检查用户点击的选择
小基础语言会在玩家点击图形窗口的任何地方时调用这个子程序。首先,子程序会检查 choice1 的值(第 2 行)。如果 choice1 的值为 0,子程序会检查玩家点击的位置,看看他们是否点击了三个按钮之一。如果 choice1 不是 0,这意味着主循环仍在处理玩家的上一个选择,所以子程序会忽略这次鼠标点击。这样,即使玩家在窗口的各个位置乱点击,游戏也不会混乱。
要查看玩家是否点击了三个图片按钮之一,子程序会检查点击的垂直位置(第 4 行)。如果点击位置在图片的范围内,子程序会检查水平位置(第 6 行)。接着,If/ElseIf 结构会将水平位置与每个图片的左右边缘进行比较,并相应地设置 choice1(第 6 到 12 行)。
提示
如果你想知道三个图片按钮的准确位置,可以将这段代码添加到你的程序中:
GraphicsWindow.MouseMove = OnMouseMove
Sub OnMouseMove
mx = GraphicsWindow.MouseX
my = GraphicsWindow.MouseY
TextWindow.WriteLine(mx + ", " + my)
EndSub
将鼠标移到背景图片上,可以看到文本窗口中显示的坐标。别忘了在和朋友分享游戏之前删除这段代码!
步骤 3:切换图片
当玩家做出选择后,你需要显示电脑的选择,这样他们就知道电脑没有作弊。为了增加一些兴奋感,你将在显示最终选择前先播放图片动画。在清单 14-6 中添加 SwitchImages() 子程序。
1 Sub SwitchImages
2 For M = 1 To 10 ' Flips images 10 times
3 N = 1 + Math.Remainder(M, 3) ' N = 1,2,3,1,2,3...
4 img1 = path + "\P" + N + ".png" ' {\P1, \P2, or \P3}.png
5 img2 = path + "\C" + N + ".png" ' {\C1, \C2, or \C3}.png
6 GraphicsWindow.DrawImage(img1, 40, 150) ' Draws img1
7 GraphicsWindow.DrawImage(img2, 280, 150) ' Draws img2
8 Program.Delay(100) ' Waits a short time
9 EndFor
10
11 ' Shows the actual choices of the player and the computer
12 img1 = path + "\P" + choice1 + ".png"
13 img2 = path + "\C" + choice2 + ".png"
14 GraphicsWindow.DrawImage(img1, 40, 150)
15 GraphicsWindow.DrawImage(img2, 280, 150)
16 EndSub
清单 14-6:切换图片以实现视觉效果
SwitchImages() 子程序首先会快速切换玩家和电脑的图片 10 次,制造一个有趣的视觉效果(第 2 到 9 行)。然后,代码会通过在 P 和 C 字母后附加数字来显示与 choice1 和 choice2 对应的图片,这两个字母分别代表图片的名称。
运行代码进行测试。当你点击任何一个图片按钮时,玩家和电脑的选择会在最终显示实际选择的图片之前变化 10 次。(不要翻得太快,不然会头晕!)
步骤 4:宣布获胜者
游戏的最后部分,ShowWinner() 子程序会检查结果并显示获胜者。在清单 14-7 中添加 ShowWinner() 子程序。
1 Sub ShowWinner
2 ' W0: Tie; W1: Player1; W2: Computer
3 If ((choice1 = 1) And (choice2 = 2)) Then ' Paper (2) beats rock (1)
4 img = "\W2.png"
5 ElseIf ((choice1 = 1) And (choice2 = 3)) Then ' Rock (1) beats scissors (3)
6 img = "\W1.png"
7 ElseIf ((choice1 = 2) And (choice2 = 1)) Then ' Paper (2) beats rock (1)
8 img = "\W1.png"
9 ElseIf ((choice1 = 2) And (choice2 = 3)) Then ' Scissors (3) beats paper (2)
10 img = "\W2.png"
11 ElseIf ((choice1 = 3) And (choice2 = 1)) Then ' Rock (1) beats scissors (3)
12 img = "\W2.png"
13 ElseIf ((choice1 = 3) And (choice2 = 2)) Then ' Scissors (3) beats paper (2)
14 img = "\W1.png"
15 Else
16 img = "\W0.png"
17 EndIf
18
19 GraphicsWindow.DrawImage(path + img, 115, 310)
20 EndSub
清单 14-7:检查谁赢了并显示正确的图片
这个子程序使用 If/ElseIf 结构比较 choice1 和 choice2 的值,并决定显示哪张图片(img)(第 3 到 17 行)。记住,选择 1 代表石头,2 代表剪刀,3 代表布。然后,第 19 行会绘制选定的图片。
动手试试 14-4
看看你是否能够将剪刀石头布游戏改成两人游戏!
编程挑战
如果遇到困难,可以访问 nostarch.com/smallbasic/ 查找解决方案,并获取更多的资源以及适用于教师和学生的复习题目。
-
打开本章文件夹中的Race_Incomplete.sb文件。这个应用程序模拟了两名玩家之间的比赛。当你运行程序时,你将看到以下界面。请根据应用程序源代码中的注释,编写缺失的代码并完成应用程序。
![image]()
-
打开本章文件夹中的SimpleSlot.sb文件。这个程序模拟了一个简单的老丨虎丨机,如下图所示。当你点击鼠标时,游戏会随机显示三个物体。如果三个物体相同,你赢得$20。如果两个物体相同,你赢得$5;否则,你将输掉$1。玩完游戏后,研究代码并解释程序的工作原理。
![image]()
-
打开本章文件夹中的Space.sb文件。在这个游戏中,你需要射击飞过屏幕顶部的 UFO(见下图)。使用左右箭头键移动,按空格键射击。你只有 100 发子弹,游戏会记录你的得分。想一想可以改进游戏的方式并添加进去。
![image]()
第十六章:15
将数据分组在一维数组中

到目前为止,您已经使用变量存储单个信息片段,并创建了一些非常棒的程序。但是,通过将大量信息存储在单个变量中,您可以创建更令人惊叹的程序!在 Small Basic 中,您可以通过使用数组来实现这一点。
数组 是一种内置数据类型,让您可以处理数据组。例如,您不会为您拥有的每双鞋子建立单独的衣柜(除非您是一个喜欢购鞋的巨人);您会把它们都放在一个衣柜里。好吧,数组让您一起存储许多数据片段,以便更容易一次性处理它们。您可以将衣柜看作包含一排鞋盒的一维数组。
Small Basic 有两种类型的数组:索引数组和关联数组。索引数组 中的数据片段使用整数索引引用,例如 score[1],name[3] 等。这就像在衣柜里的每个鞋盒上贴上编号标签一样。但关联数组 的元素使用字符串索引引用,例如 price["apple"] 或 address["John"]。本章将探讨索引数组。我们将在下一章介绍关联数组,也称为哈希或映射。
开始使用索引数组
假设您想编写一个程序,从用户那里获取四个测试分数,然后显示这些分数以及它们的平均值。根据您目前所学的知识,您可能会编写类似于 清单 15-1 中的程序。
1 ' Average1.sb
2 TextWindow.Write("Enter 4 scores. ")
3 TextWindow.WriteLine("Press <Enter> after each score.")
4 s1 = TextWindow.ReadNumber() ' Reads the 1st score
5 s2 = TextWindow.ReadNumber() ' Reads the 2nd score
6 s3 = TextWindow.ReadNumber() ' Reads the 3rd score
7 s4 = TextWindow.ReadNumber() ' Reads the 4th score
8 avg = (s1 + s2 + s3 + s4) / 4 ' Calculates the average
9 TextWindow.Write("Numbers: " + s1 + ", " + s2 + ", ")
10 TextWindow.WriteLine(s3 + ", " + s4)
11 TextWindow.WriteLine("Average: " + avg)
清单 15-1:将分数存储在单独的变量中
此程序提示用户输入四个分数(第 2–3 行)。它读取这些分数并将它们保存在四个变量s1,s2,s3和s4中(第 4–7 行)。然后计算平均值(第 8 行),在一行上显示这四个数字(第 9–10 行),并显示计算出的平均值(第 11 行)。
现在想象一下,您希望用户输入 100 个分数而不是 4 个。定义 100 个变量并复制几乎相同的语句 100 次将花费很长时间。好吧,Small Basic 的数组存储值的集合。使用数组,您不必单独创建每个变量。您可以将所有值放入一个数组变量中。例如,您可以读取用户输入的 10 个分数并将它们存储在一个数组中,使用以下循环:
For N = 1 To 10
score[N] = TextWindow.ReadNumber()
EndFor
TextWindow.WriteLine(score)
您不需要创建 10 个变量,如s1,s2等到s10,而是创建一个名为score的数组变量。要引用score数组中的每个数据片段,您使用语法score[N],其中N是一个将取值 1 到 10 的变量。编写score[N]就像编写score[1],score[2],...,score[10],而For循环会为您递增N。
运行此代码。在输入 10 个不同的数字后,Small Basic 显示score数组,并且您可以看到其中存储的所有 10 个值(我们将在本章后面展示更好的显示数组的方法)。
将数组看作是共享相同名称的一组变量。例如,美国十大城市的年平均降水量可以保存在rainLevel[1]到rainLevel[10]中,而你所在地区 100 家麦当劳的日销售额可以保存在sales[1]到sales[100]中。想象一下所有的快乐儿童餐!
数组可以帮助你以一种方式组织数据,使得数据更容易修改和使用。数组的命名遵循与你为变量命名时相同的规则和指导原则。
数组基础
数组中的每个信息单元称为元素。要访问数组中的一个元素,你可以使用以下语法:
arrayName[index]
arrayName变量是数组的名称,index是一个标识符,可以是数字或字符串,用于标识数组中的元素(参见图 15-1)。这种语法被称为带索引的变量,或下标变量。索引位于方括号中,唯一标识数组中的一个元素。

图 15-1:一维数组的图形表示
你可以像对待常规变量一样,通过使用正确的语法来处理带有索引的变量。例如,下面的语句会初始化并显示score数组中前面三个元素,如图 15-1 所示:
score[1] = 80
score[2] = 85
score[3] = 90
TextWindow.WriteLine(score[1] + ", " + score[2] + ", " + score[3])
如果你运行这段代码,你将看到以下输出:
80, 85, 90
如果你想改变第一个成绩,你可以写下如下语句:
score[1] = score[1] + 5
这行代码将 5 加到索引为 1 的第一个score上。如果你现在显示score的值,你会看到score[1] = 85。你可以使用下一条语句来将索引 1 和 2 的两个元素相乘:
score[1] = score[1] * score[2]
如果score[1]是 80,score[2]是 85,它们相乘得到 6800,并将结果保存回score[1]。高分!
初始化数组
在程序中使用数组之前,你需要先为它填充(或初始化)一些数据。在 Small Basic 中,你可以通过两种方式来做到这一点:直接的(逐个元素)初始化或字符串初始化。
假设你想创建一个包含四个成绩的数组(不是阿布·林肯那种类型的)。这里是直接做法:
score[1] = 80
score[2] = 85
score[3] = 90
score[4] = 95
你也可以使用字符串初始化器,它允许你仅用一行代码设置四个值,如下所示:
score = "1=80;2=85;3=90;4=95;"
这个字符串初始化器有四个标记(或字段),它们以分号结束。每个标记的形式如下(不,你不能用这些标记换取奖品):
index=value;
在这个例子中,第一个标记是1=80,第二个是2=85,第三个是3=90,第四个是4=95。等号前面的数字是元素的索引,等号后面的数字是存储在该元素中的值。注意等号前后没有空格。图 15-2 展示了这个字符串初始化器的工作原理。

图 15-2:数组字符串初始化器的语法
字符串初始化器允许你在一条语句中填充数组,但其语法有些复杂,且可能不小心引入代码错误。在你更加熟悉数组之前,我们建议你在程序中坚持使用基本的逐个元素初始化方法。不过,如果你使用了字符串初始化器并遇到问题,可以尝试逐个元素地重新初始化数组。本书中将同时使用这两种初始化方法,以节省空间并帮助你更好地掌握它们。
注意
Small Basic 允许你为索引选择任何你想要的数字。它甚至允许使用负数和小数,并且不要求索引是连续的。但在本书中,我们将始终使用从 1 开始的整数索引来表示第一个数组元素。
数组和For循环经常一起使用。当数组的大小已知时,你可以使用For循环遍历数组并对每个元素执行操作。接下来的示例展示了如何使用For循环对数组执行操作。
尝试示例 15-1
假设数组S中的元素以及变量A和B的值如图 15-3 所示。那么S[A]、S[B]、S[A * B - 2]、S[A + B]和S[A] - 2 * S[B]的值分别是多少?

图 15-3: S数组及变量A和B中的值
使用 For 循环填充数组
许多时候,你需要使用常量值、随机值、通过公式计算的值或用户输入的值来填充数组的元素。让我们来看每种情况!
常量初始化
以下代码片段展示了如何使用常量值 0 初始化一个美味的数组(名为scoobySnack)的前 10 个元素。
For N = 1 To 10
scoobySnack[N] = 0
EndFor
For循环执行 10 次。在第一次迭代中,N的值为 1,因此循环将scoobySnack[1] = 0。在第二次迭代中,N的值为 2,因此循环将scoobySnack[2] = 0,依此类推。这样就创建了一个包含 10 个元素的数组,所有元素的值都为 0。
随机初始化
你也可以通过随机数填充scoobySnack数组的元素,如下所示:
For N = 1 To 10
scoobySnack[N] = Math.GetRandomNumber(5)
EndFor
For循环迭代 10 次。在第N次迭代中,索引为N的元素scoobySnack[N]将被赋值为 1 到 5 之间的随机数。试着显示scoobySnack的值,看看你得到的随机数!在For循环中设置scoobySnack[N]之后,添加以下语句:
TextWindow.WriteLine(scoobySnack[N])
公式初始化
你还可以使用公式初始化数组的元素。在此示例中,你将把scoobySnack数组的第N个元素设置为N * 8;这段代码将把 8 的乘法表存储在数组中:
For N = 1 To 10
scoobySnack[N] = N * 8
EndFor
添加代码以显示scoobySnack的值,查看结果!
用户初始化
如果你想使用用户输入的值来初始化数组的元素呢?以下程序提示用户输入五个数字,并在每个数字后按下 ENTER 键。然后程序开始一个For循环,读取这五个数字并将它们存储在thunderCat[1]、thunderCat[2]、...、thunderCat[5]中。
TextWindow.WriteLine("Enter 5 numbers. Press Enter after each one.")
For N = 1 To 5
thunderCat[N] = TextWindow.ReadNumber()
EndFor
这种技术对于存储用户输入的海量数据非常有用。你还会请求哪些其他的数据集合呢?比如breakfastMenu、favoriteGames、bestPasswords、funnyJokes或frozenNames?
动手实践 15-2
编写一个程序,将一个名为skeletor的数组填充从 20 到 40 之间的偶数(例如,skeletor[1] = 20, skeletor[2] = 22,...)。
显示数组
假设我们有一个名为age的数组,保存了三兄弟的年龄,像这样:
age[1] = 14
age[2] = 15
age[3] = 16
你可以通过两种方式显示这个数组的内容。第一种也是最简单的方式是将数组的名称传递给WriteLine()方法,像这样:
TextWindow.WriteLine(age)
这是这个语句的输出:
1=14;2=15;3=16;
这个语句将在一行内显示数组的元素,每个元素之间用分号隔开。这个字符串中的每个标记显示了数组元素的索引和值。你现在能看出数组的字符串初始化语法是从哪里来的了吗?
如果你想以更易读的格式显示数组,可以使用For循环将数组的每个元素显示在单独的一行中:
For N = 1 To 3
TextWindow.WriteLine("age[" + N + "] = " + age[N])
EndFor
这是这个循环的输出:
age[1] = 14
age[2] = 15
age[3] = 16
如果你处理的是一个较短的数组,显示在一行上没问题。但如果你处理的是大量数据,最好以易于阅读的格式显示数组。
动手实践 15-3
编写一个程序,将一个名为burps的数组填充五个介于 80 到 100 之间的随机数,并显示这个数组。尝试通过将数组的名称传递给TextWindow.WriteLine()来显示数组,然后再通过使用For循环来显示数组。哪种方式看起来更好?
处理数组
许多程序涉及处理数组的元素,例如对它们进行求和、找出它们的平均值、最小值、最大值等。你将在本节中学习如何执行这些任务。
查找总和
一位名叫 Super Here-O 的超级英雄想知道他从镇上的 10 个强盗手中救回了多少钱。以下程序让 Super Here-O 将他救回的金额输入到一个名为moneyReturned的数组中。程序会计算该数组中所有元素的总和:
sum = 0
TextWindow.WriteLine("Enter the 10 amounts that were returned:")
For N = 1 To 10
moneyReturned[N] = TextWindow.ReadNumber()
sum = sum + moneyReturned[N]
EndFor
For N = 1 To 10
TextWindow.Write("$" + moneyReturned[N])
TextWindow.WriteLine(" rescued from robber " + N)
EndFor
TextWindow.WriteLine("$" + sum + " was rescued by Super Here-O!")
要计算总和,首先将sum变量初始化为 0。然后,运行一个For循环,读取moneyReturned数组的每个元素并将其加到sum变量中。当循环结束时,开始另一个循环,显示每个强盗被救回的金额,然后显示总金额。运行程序,看看这些钱是否足够买一套新的超级英雄紧身衣!
查找最大元素
假设你正在和九个好朋友竞争,看看谁在 Facebook 上有最多的朋友。使用以下代码片段找到名为 friends 的数组中的最大值:
friends = "1=10;2=30;3=5;4=10;5=15;6=8;7=1;8=23;9=6;10=11"
max = friends[1]
For N = 2 To 10
If (friends[N] > max) Then ' Nth element is larger than max
max = friends[N] ' Update max to hold the new maximum
EndIf
EndFor
TextWindow.WriteLine("The most friends is " + max + ".")
首先,我们填充了 friends 数组中的 10 个元素,表示你和你九个最亲密的朋友在 Facebook 上的朋友数量。在这个示例中,你的第一个朋友有 10 个朋友,第二个朋友有 30 个,第三个朋友有 5 个,以此类推,你(数组中的第 10 个)有 11 个朋友。你可以随意更改这些数字。程序开始时假设第一个元素 friends[1] 是最大的。然后它进入一个循环,检查数组中剩下的元素,从第二个元素开始。每次找到一个大于当前最大值的数字时,它会更新最大值 max。当循环结束时,最大值会被显示出来。
在数组中使用字符串值
数组不仅仅局限于数字,你也可以使用数组来存储字符串。例如,假设你想创建一个数组,用来存储你收藏书籍的名称。你可以像这样初始化这个数组:
book[1] = "The Hobbit"
book[2] = "Little Women"
book[3] = "My Little Pony vs Hello Kitty"
注意
你也可以使用字符串初始化器来初始化 book 数组,像这样(确保整个语句在一行内):
book = "1=The Hobbit;2=Little Women;3=My Little Pony vs Hello Kitty"
试一试 15-4
编写一个程序,填充两个数组(noun 和 verb),存储你选择的复数名词和动词。让程序以这种形式显示随机句子:noun verb noun(例如,dogs delight cats)。
保存记录
你可以在一个数组中混合不同的数据类型。你可以存储数字(整数和小数)和字符串作为数组中的不同元素。例如,以下数组是有效的(你知道这座建筑吗?):
arr[1] = 1600
arr[2] = "Pennsylvania Avenue NW"
arr[3] = 20500
这个数组的第一个和第三个元素是数字,第二个元素是字符串。这三个元素可以分别代表一个家的号码、街道名称和邮政编码。这是创建一个记录的方式,记录是一个包含相关数据片段的集合,在 Small Basic 中就是这样。
哇!好吧,我们已经学习了足够的内容来解决大量的问题。现在让我们花些时间写些有趣的程序吧!
使用索引数组
本节的第一个示例展示了如何从数组中选择随机元素。第二个示例模拟了一个魔法 8 号球游戏,计算机会随机选择答案来回应玩家的问题。让我们来进行随机选择吧!
随机选择
假设我们有一个袋子,里面装有编号从 1 到 10 的 10 个球,我们要从中随机取出五个球(见 图 15-4)。我们将编写一个程序,随机选择五个球,然后显示它们的编号。

图 15-4:从袋子中随机选择五个球
为了创建这个程序,我们将使用一个名为ball的数组来存储 10 个球的号码(ball[1] = 1,ball[2] = 2,...,ball[10] = 10)。然后程序随机选择一个 1 到 10 之间的数字来挑选一个球。例如,如果它选中了 2,那么它会将ball[2] = 0,表示第二个球已经被选中并且不再可用。然后它会选择另一个随机数。假设第二个数字也是 2。首先,程序检查ball[2]。由于它是 0,程序知道ball[2]已经被选中了(你不能把同一个球从袋子里拿出来两次!),所以它会选择另一个随机数。它会继续这样,直到选出五个不同的随机数。完整的程序请参见清单 15-2。
1 ' RandomSelect.sb
2 For N = 1 To 10 ' Puts the 10 balls in an array
3 ball[N] = N
4 EndFor
5
6 For N = 1 To 5 ' Loops to select 5 balls
7 idx = Math.GetRandomNumber(10) ' Gets random ball number
8 While (ball[idx] = 0) ' Ball already selected
9 idx = Math.GetRandomNumber(10) ' Gets another number
10 EndWhile
11
12 TextWindow.Write(ball[idx] + ", ") ' Displays selected ball
13 ball[idx] = 0 ' Marks it out (taken)
14 EndFor
15 TextWindow.WriteLine("")
清单 15-2:随机选择五个不同的球
程序首先通过For循环设置ball[1] = 1,ball[2] = 2,...,ball[10] = 10(第 2–4 行)。然后它开始一个循环来选择五个球(第 6 行)。在每次循环迭代中,它选择一个 1 到 10 之间的随机数idx(第 7 行)。一个While循环不断设置idx,直到ball[idx]不为 0(第 8–10 行)。在选择一个唯一的球号后,程序会显示该号码(第 12 行),然后通过将该球的数组元素设置为 0 来标记它已被选中(第 13 行),以避免再次选择该号码。下面是该程序的一个示例运行:
5, 9, 10, 1, 2,
运行程序,看看你会得到哪些数字!
魔术 8 球
在这个例子中,我们将编写一个模拟魔术 8 球游戏的程序。用户提出一个是或否的问题,计算机会给出答案。当然,这只是为了好玩,所以不要用它来做重要决策,比如选择配偶或房子!完整的程序请参见清单 15-3。
1 ' Magic8Ball.sb
2 ans[1] = "It is certain. Like really, really certain."
3 ans[2] = "It is decidedly so. By me. I decided."
4 ans[3] = "Without a doubt. Maybe one doubt."
5 ans[4] = "Yes, definitely. Isn't it obvious?"
6 ans[5] = "Very doubtful. The doubt is very full."
7 ans[6] = "Maybe. Depends on the horse race."
8 ans[7] = "No. Wait, yes. Wait, no. Yes, it's no."
9 ans[8] = "Let me consult my Magic 8 Ball... It says yes."
10 ans[9] = "Outlook not so good. Restart Outlook."
11 ans[10] = "Try again. It's funny when you shake things."
12
13 While ("True")
14 TextWindow.WriteLine("Ask me a yes-no question. Do it!")
15 ques = TextWindow.Read()
16 num = Math.GetRandomNumber(10)
17 TextWindow.WriteLine(ans[num])
18 TextWindow.WriteLine("")
19 EndWhile
清单 15-3:魔术 8 球模拟
游戏有 10 个可能的答案保存在ans数组中。在初始化数组(第 2–11 行)之后,游戏启动一个无限循环与用户互动。在每次循环中,游戏都会询问用户输入一个是或否的问题。它读取用户的问题(第 15 行),生成一个 1 到 10 之间的随机数(第 16 行),并使用这个数字通过ans[num]显示一个答案(第 17 行)。显示完信息后,我们会显示一个空白行(第 18 行)。对于那些不了解窍门的人,计算机看起来可能很聪明!邀请你的朋友玩这个游戏,看看他们怎么说。
你感觉怎么样?像海绵一样敏锐,像钉子一样新鲜?太好了,因为现在是游戏创建时间!
尝试一下 15-5
修改魔术 8 球游戏,使其每个答案只显示一次。当所有答案都显示完时结束游戏。
创建抓苹果游戏
图 15-5 显示了一个游戏,在图形窗口的顶部随机位置和时间出现苹果,然后掉落到地面。玩家需要通过鼠标移动手推车,抓住掉落的苹果,避免它们掉到地面。每个苹果值 1 分。别担心苹果会被撞坏,它们是硬核的!

图 15-5:捉苹果游戏
按照以下步骤,一步一步将这个精彩的游戏拼凑在一起。
步骤 1:打开启动文件
打开本章文件夹中的文件CatchApples_Incomplete.sb。文件夹中还包含了你需要的所有图像。启动文件包含主代码(见示例 15-4)和四个子程序的空占位符,你将编写这些子程序。我们从主代码开始。
1 ' CatchApples_Incomplete.sb
2 GraphicsWindow.Title = "Catch Apples"
3 GraphicsWindow.CanResize = "False"
4 GraphicsWindow.Width = 480
5 GraphicsWindow.Height = 360
6 GraphicsWindow.FontSize = 14
7 GraphicsWindow.BrushColor = "Black"
8
9 path = Program.Directory
10 GraphicsWindow.DrawImage(path + "\Background.png", 0, 0)
11
12 msgID = Shapes.AddText("")
13 Shapes.Move(msgID, 240, 0)
14
15 MAX_APPLES = 5 ' Change this to have more apples
16 AddApples() ' Creates the apple array
17
18 cartImg = Shapes.AddImage(path + "\Cart.png") ' 100x80 pixels
19
20 numMissed = 0 ' Missed apples
21 numCaught = 0 ' Caught apples
22
23 While ("True")
24 Shapes.Move(cartImg, GraphicsWindow.MouseX - 50, 280)
25 MoveApples()
26 Program.Delay(5)
27 EndWhile
示例 15-4:捉苹果游戏的主代码
在第 2–7 行中,我们设置了图形窗口的标题;大小与背景图像的大小匹配;字体大小;以及字体颜色。然后,我们绘制背景图像(第 10 行),并创建显示捕捉和掉落苹果数量的文本形状(第 12–13 行)。第 15 行中的MAX_APPLES变量是将出现在图形窗口中的最大苹果数量。一旦游戏运行起来,你可以调整这个数字来让游戏更容易或更难。
第 16 行调用AddApples()子程序来创建将保存掉落苹果的数组。第 18 行添加手推车的图像,并将其标识符保存在cartImg中;我们需要这个标识符来移动手推车。
第 20–21 行初始化了numMissed(错过的苹果数量)和numCaught(捕获的苹果数量)变量为 0。然后,代码启动了游戏的主循环(第 23–27 行)。在每次迭代中,我们移动手推车,使其中心与鼠标的 x 位置对齐(第 24 行)。由于手推车的宽度是 100 像素,手推车的左侧位置设置为MouseX – 50。手推车的 y 位置是固定的。我们调用MoveApples()子程序让苹果掉落,并检查它们是否碰到手推车或地面(第 25 行);然后我们等待 5 毫秒,再重复这些步骤(第 26 行)。不过不要告诉你爸爸等 5 毫秒,否则他可能以为你在顶嘴!
现在运行游戏,并移动鼠标。手推车会跟随鼠标,但苹果还没有出现。接下来,你将添加缺失的子程序来完成游戏。
步骤 2:添加苹果
在示例 15-5 中添加AddApples()子程序。
1 Sub AddApples
2 For aplNum = 1 To MAX_APPLES
3 apple[aplNum] = Shapes.AddImage(path + "\Apple.png")
4 scale =(3 + Math.GetRandomNumber(5)) / 10
5 Shapes.Zoom(apple[aplNum], scale, scale)
6 SetApplePosition()
7 EndFor
8 EndSub
示例 15-5:AddApples()子程序
这个子程序使用For循环来创建五个苹果。在每次迭代中,我们调用AddImage()从游戏文件夹加载苹果的图像,并将返回的标识符保存在apple数组中(第 3 行)。第一个苹果保存在apple[1],第二个苹果保存在apple[2],以此类推。
为了增加游戏的多样性,我们将改变苹果的大小。在第 4 行,我们将 scale 变量设置为来自集合 {0.4, 0.5, 0.6, 0.7, 0.8} 的随机值,计算方式为 (3 + Math.GetRandomNumber(5)) / 10。在第 5 行,我们将这个值传递给 Zoom() 方法来改变苹果的大小。这样,苹果的大小将是其原始大小的一个比例(介于 40% 到 80% 之间)。
接下来,我们将调用 SetApplePosition() 子程序来定位新的苹果。让我们看看这个子程序是如何工作的。
步骤 3:定位苹果
在清单 15-6 中添加 SetApplePosition() 子程序。
1 Sub SetApplePosition
2 xPos = Math.GetRandomNumber(420)
3 yPos = -Math.GetRandomNumber(500)
4 Shapes.Move(apple[aplNum], xPos, yPos)
5 EndSub
清单 15-6: SetApplePosition() 子程序
我们将水平位置设置为 1 到 420 之间的随机整数(第 2 行),将垂直位置设置为 -1 到 -500 之间的负值(第 3 行)。在第 4 行调用 Move() 将苹果(在 apple 数组中索引为 aplNum 的元素)放置在图形窗口顶部的一个不可见的点,使用这两个数字 xPos 和 yPos。这样,当苹果开始掉落时,它们会在屏幕顶部随机出现;被放置在 yPos = -100 的苹果会比放置在 yPos = -500 的苹果更早出现,因为它需要掉落的距离更短。正如你稍后会看到的,我们还将在玩家抓住或错过苹果时调用这个子程序。
步骤 4:移动苹果
现在我们准备让苹果下雨了(让猫和狗休息一下)。将清单 15-7 中的代码添加到 MoveApples() 子程序的占位符中。
1 Sub MoveApples
2 For aplNum = 1 To MAX_APPLES
3 xPos = Shapes.GetLeft(apple[aplNum])
4 yPos = Shapes.GetTop (apple[aplNum])
5 Shapes.Move(apple[aplNum], xPos, yPos + 1)
6
7 CheckCatch() ' Checks if the apple landed in the cart
8 If (gotIt = 1) Then
9 Sound.PlayClick()
10 numCaught = numCaught + 1
11 SetApplePosition()
12 ElseIf (yPos > 320) Then
13 numMissed = numMissed + 1
14 SetApplePosition()
15 EndIf
16 EndFor
17
18 msg = "Caught: " + numCaught + " Missed: " + numMissed
19 Shapes.SetText(msgID, msg)
20 EndSub
清单 15-7: MoveApples() 子程序
在第 2 行,我们开始一个 For 循环来掉落五个苹果。我们获取每个苹果的左上角坐标(第 3–4 行),然后将其向下移动 1 像素(第 5 行)。然后我们调用 CheckCatch() 来检查这个苹果是否被玩家抓住(第 7 行)。正如你稍后会看到的,这个子程序会将 gotIt 标志设置为 1,如果玩家抓住了苹果;否则,它会将 gotIt 设置为 0。错过苹果是没关系的,不会伤害到苹果的皮。
当 CheckCatch() 返回时,我们检查 gotIt 标志。如果它是 1(第 8 行),意味着苹果被玩家抓住了。在这种情况下,我们播放点击音效,将 numCaught 增加 1,并调用 SetApplePosition() 重新定位苹果并让它再次掉落(第 9–11 行)。另一方面,如果 gotIt 不是 1,我们检查苹果的 y 坐标,看看它是否落到小车中心以下,意味着玩家没有接住它(第 12 行)。在这种情况下,我们将 numMissed 增加 1,并调用 SetApplePosition() 重新定位苹果并让它再次掉落(第 13–14 行)。如果苹果既没有被抓住也没有错过,那么它正在掉落,并将在下次调用 MoveApples() 时再次处理。
在移动并检查五个苹果的状态后,我们更新显示已抓住和错过苹果数量的消息(第 18–19 行)。
步骤 5:抓住或错过
最后一部分是添加CheckCatch()子程序,见 Listing 15-8。
1 Sub CheckCatch
2 xApple = Shapes.GetLeft(apple[aplNum]) + 32 ' Center point
3 yApple = Shapes.GetTop(apple[aplNum]) + 32 ' Bottom point
4 xCart = Shapes.GetLeft(cartImg) + 50 ' Center point
5 yCart = Shapes.GetTop(cartImg) + 40 ' Around the center
6 xdiff = Math.Abs(xApple - xCart)
7 ydiff = Math.Abs(yApple - yCart)
8 gotIt = 0 ' Assumes we didn't get the apple
9 If ((xdiff < 20) And (ydiff < 20)) Then
10 gotIt = 1 ' We got it
11 EndIf
12 EndSub
Listing 15-8: The CheckCatch() 子程序
这个子程序检查苹果中心(其索引由aplNum给出)和小车中心之间的距离。如果苹果距离小车中心 20 像素以内,子程序将gotIt设置为 1;否则,它将gotIt设置为 0。
游戏现在已经完成,你可以开始玩了!也许你会捉到足够多的苹果做一个苹果派。
试试看 15-6
当前,Catch Apples 游戏会一直运行。想想如何结束游戏,然后实现它。你能想到其他一些改进游戏的方法吗?也许如果玩家捉到一个大苹果,你可以给他们更多的分数!关于在While循环内移动小车的语句呢?你能把这个语句移到新的MouseMove事件处理程序中吗?
编程挑战
如果你卡住了,可以查看* nostarch.com/smallbasic/*获取解决方案、更多资源以及供教师和学生使用的复习问题。
-
编写一个模拟掷骰子的程序。让程序掷骰子 10,000 次,并跟踪每个数字出现的次数。以下是程序的一个示例运行。(提示:使用一个名为
dice的数组,它有六个元素。如果掷出 1,就增加dice[1];如果掷出 2,就增加dice[2],以此类推。)Num Count Probability 1 1640 0.164 2 1670 0.167 3 1638 0.1638 4 1684 0.1684 5 1680 0.168 6 1688 0.1688 -
打开本章文件夹中的PinBall.sb文件。这个程序模拟了一个弹珠机。如下图所示,球从机器顶部掉下。当它向下滚动时,它会撞击固定的销钉,并以随机的方式向左或向右弹跳。最后,球会落入七个口袋中的一个。程序会让球掉落 10,000 次,并统计每次球落入哪个口袋。研究程序并解释它是如何工作的。
![image]()
-
打开本章文件夹中的FlowerAnatomy.sb文件。这个程序是一个教育游戏,测试玩家对花朵部位的认识(如下图所示)。玩家输入字母以匹配花朵的标签部位,然后点击“检查”按钮来检查答案。程序将用户的答案与正确答案进行比较,然后通过在每个正确答案旁边放一个绿色的对号,在每个错误答案旁边放一个红色的叉号,来显示玩家的成绩。研究这个程序,并解释它是如何工作的。
![image]()
-
打开本章文件夹中的USMapQuiz_Incomplete.sb文件。文件夹中还包含此处显示的背景图像(以及前一个练习中的Yes和No图像)。完善程序,使这个测验能够运行。显示九个州的两字母缩写,并提供九个文本框,让玩家将每个州与其代码匹配。
![image]()
第十七章:16
使用关联数组存储数据

在像 Facebook 和 LinkedIn 这样的社交网站上,人们会在文本框中输入信息,比如他们的名字、关系状态,甚至是定期向朋友更新(比如,“哦不!!我刚踩到了一只虫子,我觉得我得了虫子中毒!”)。需要搜索或过滤这些数据的程序可能会使用关联数组来存储文本的各个部分。
除了在第十五章中使用的索引数组外,Small Basic 还支持其他类型的数组,这些数组可以简化许多编程任务。在本章中,你将从学习关联数组开始。接着,你将学习Array对象,使用它创建一些有趣的应用,甚至将你的计算机变成一位诗人!
关联数组
在前一章中,你学习了如何使用整数索引来访问数组的元素。但在 Small Basic 中,数组的索引也可以是字符串。由字符串索引的数组被称为关联数组、映射或字典。在本书中,我们称它们为关联数组。就像索引数组一样,关联数组可以存储任何类型的值。你可以使用关联数组在一组键(字符串索引)和一组值之间创建关联,这就是创建键值对映射。
以下代码展示了关联数组在实际应用中的一个简单例子。这是一个由两位字母缩写键控的州列表:
state["CA"] = "California"
state["MI"] = "Michigan"
state["OH"] = "Ohio"
' ... and so on
要显示一个州的名称,你只需要使用其对应的键和正确的语法。例如,要显示Michigan,你可以写下这个语句:
TextWindow.WriteLine(state["MI"])
通过写出数组的名称,后跟用方括号括起来的键,你可以访问对应的项。关联数组就像一个查找表,将键映射到值;如果你知道键,就可以非常快速地找到其对应的值。
要学习如何使用关联数组,让我们编写一个程序,通过名字追踪你朋友的年龄。在清单 16-1 中输入该程序。
1 ' AssociativeArray.sb
2 age["Bert"] = 17
3 age["Ernie"] = 16
4 age["Zoe"] = 16
5 age["Elmo"] = 17
6 TextWindow.Write("Enter the name of your friend: ")
7 name = TextWindow.Read()
8 TextWindow.Write(name + " is [")
9 TextWindow.WriteLine(age[name] + "] years old.")
清单 16-1:使用关联数组
第 2 行到第 5 行创建了一个名为age的关联数组,其中包含四个元素。如果你愿意,可以添加更多元素,或者你可以更改数组来存储你自己朋友的年龄。第 6 行让你输入一个朋友的名字,第 7 行将其读取到name变量中。在第 9 行,age[name]查找该朋友的年龄。
让我们看一下这个程序的一些示例运行:
Enter the name of your friend: Ernie
Ernie is [16] years old.
Enter the name of your friend: ernie
ernie is [16] years old.
请注意,键是大小写不敏感的:无论你输入age["Ernie"]、age["ernie"],还是age["ERNIE"],都没关系。如果数组包含名为Ernie的键,无论其大小写如何,Small Basic 都会返回该键的值。
假设你忘记了在数组中存储了哪些朋友的名字,并且你试图访问一个你忘记包括的朋友的年龄:
Enter the name of your friend: Grover
Grover is [] years old.
如果数组中不包含某个键,Small Basic 会返回一个空字符串,这就是为什么age["Grover"]是空的原因。
关联数组与 IF/ELSEIF 阶梯
在编程中,通常有多种不同的方式来解决特定的问题。这里有另一种写法,类似于 Listing 16-1 中的程序:
TextWindow.Write("Enter the name of your friend: ")
name = TextWindow.Read()
If (name = "Bert") Then
age = 17
ElseIf (name = "Ernie") Then
age = 16
ElseIf (name = "Zoe") Then
age = 16
ElseIf (name = "Elmo") Then
age = 17
Else
age = ""
EndIf
TextWindow.WriteLine(name + " is [" + age + "] years old.")
尽管这个程序看起来与 Listing 16-1 中的程序类似,但两者有一个重要区别:在这里,字符串比较是区分大小写的。如果你输入ernie(小写的e),程序将显示如下输出:
ernie is [] years old.
表达式If("ernie" = "Ernie")为假。这个版本的程序也更难以阅读和编写。当你需要在一组键和值之间进行映射时,最好使用关联数组,这样你就不必担心大小写问题。
使用关联数组
现在你已经理解了关联数组的基础知识,让我们看几个程序示例,展示如何使用它们。
*法语中的星期
第一个示例将星期几从英语翻译成法语。这个程序提示用户输入一个英文的星期几名称,并输出该名称的法语翻译。请在 Listing 16-2 中输入代码。
1 ' FrenchDays.sb
2 day["Sunday"] = "Dimanche"
3 day["Monday"] = "Lundi"
4 day["Tuesday"] = "Mardi"
5 day["Wednesday"] = "Mercredi"
6 day["Thursday"] = "Jeudi"
7 day["Friday"] = "Vendredi"
8 day["Saturday"] = "Samedi"
9
10 TextWindow.Write("Enter the name of a day: ")
11 name = TextWindow.Read()
12 TextWindow.WriteLine(name + " in French is " + day[name])
Listing 16-2: 一个英法翻译程序
day数组存储了星期几的法语名称(第 2-8 行)。数组中的每个键是该天的英文名称。程序提示用户输入一个英文的星期几名称(第 10 行),并将用户的输入存储在name变量中(第 11 行)。然后,程序使用用户的输入作为键,通过语法day[name]查找对应的法语名称,并显示它(第 12 行)。以下是一次示例运行的输出:
Enter the name of a day: Monday
Monday in French is Lundi
你会其他语言吗?修改程序,帮助你的朋友们学习如何用一种新语言说出星期几。想要调皮一下吗?你甚至可以编造一个自己的秘密语言!
尝试一下 16-1
如果用户输入一个无效的星期名称(比如Windsday),Listing 16-2 的输出会是什么?当发生这种情况时,更新程序以显示错误信息。使用如下的If语句:
If (day[name] = "") Then
' Tell the user they entered a wrong name
Else
' Show the French translation
EndIf
存储记录
生意兴隆,你所在城镇的本地草坪修剪服务公司 Moe Mows 雇佣你编写一个程序,用于显示其客户的联系信息。当公司输入客户的姓名时,程序需要显示客户的家庭地址、电话号码和电子邮件地址。请在 Listing 16-3 中输入该程序。
1 ' MoeMows.sb
2 address["Natasha"] = "3215 Romanoff Rd"
3 phone["Natasha"] = "(321) 555 8745"
4 email["Natasha"] = "blackwidow64@shield.com"
5
6 address["Tony"] = "8251 Stark St"
7 phone["Tony"] = "(321) 555 4362"
8 email["Tony"] = "ironman63@shield.com"
9
10 TextWindow.Write("Name of customer: ")
11 name = TextWindow.Read()
12 TextWindow.WriteLine("Address...: " + address[name])
13 TextWindow.WriteLine("Phone.....: " + phone[name])
14 TextWindow.WriteLine("Email.....: " + email[name])
Listing 16-3: 构建一个简单的数据库
该程序使用了三个关联数组:address、phone 和 email。这三个数组都以客户的名字作为键,数组共同用来存储客户的记录。记录 是一组相关的数据项。在这个例子中,每个客户的记录有三个字段:地址、电话和电子邮件。无论程序有两个记录还是 1,000 条记录,搜索的方式都是一样的。例如,第 12 行的语句 address[name] 返回与 address 数组中 name 键关联的值。我们不需要自己去搜索 address 数组;Small Basic 会为我们做这一切,完全免费!
这是这个程序示例运行的输出:
Name of customer: Tony
Address...: 8251 Stark St
Phone.....: (321) 555 4362
Email.....: ironman63@shield.com
动手实践 16-2
更新 列表 16-3 中的程序,将一些朋友的联系信息存储在其中(但不是你所有 500 个 Facebook 朋友的信息)。再添加一个数组,用来存储每个朋友的生日。你再也不会忘记生日了!
数组对象
Small Basic 库中的 Array 对象可以帮助你找到程序中数组的重要信息。在本节中,我们将详细探讨这个对象,并查看一些如何使用它的示例。要探索 Array 对象,让我们首先输入以下代码:
name = "Bart" ' An ordinary variable
age["Homer"] = 18 ' An associative array with two elements
age["Marge"] = 17
score[1] = 90 ' An indexed array with one element
这段代码定义了一个普通变量 name,一个名为 age 的关联数组,包含两个元素,还有一个名为 score 的索引数组,包含一个元素。你将在接下来的例子中使用这些数组。Array 对象能告诉你什么?让我们来看看!
它是一个数组吗?
你认为 Small Basic 知道 name 是一个普通变量,而 age 和 score 是数组吗?运行 列表 16-4 中的程序来找出答案。
1 ' IsArray.sb
2 name = "Bart"
3 age["Homer"] = 18
4 age["Marge"] = 17
5 score[1] = 90
6 ans1 = Array.IsArray(name) ' Returns "False"
7 ans2 = Array.IsArray(age) ' Returns "True"
8 ans3 = Array.IsArray(score) ' Returns "True"
9 TextWindow.WriteLine(ans1 + ", " + ans2 + ", " + ans3)
列表 16-4:演示 IsArray() 方法
这段代码使用了 Array 对象的 IsArray() 方法。如果变量是数组,该方法返回 "True";否则返回 "False"。这个方法表明变量 age 和 score 是数组,但变量 name 不是数组。IsArray() 方法可以帮助你确保程序中的变量是数组。
数组有多大?
Array 对象还可以告诉你数组中存储了多少元素。运行 列表 16-5 中的程序。
1 ' GetItemCount.sb
2 name = "Bart"
3 age["Homer"] = 18
4 age["Marge"] = 17
5 score[1] = 90
6 ans1 = Array.GetItemCount(name) ' Returns: 0
7 ans2 = Array.GetItemCount(age) ' Returns: 2
8 ans3 = Array.GetItemCount(score) ' Returns: 1
9 TextWindow.WriteLine(ans1 + ", " + ans2 + ", " + ans3)
列表 16-5:演示 GetItemCount() 方法
GetItemCount() 方法返回指定数组中的项目数量。注意 GetItemCount(name) 返回 0,因为 name 不是一个数组。其他两个调用返回每个数组中的元素数量。使用 GetItemCount() 来跟踪你在数组中存储了多少项。你可能会在一个允许玩家将物品存入背包的游戏中使用此方法,并且你希望检查他们捡到了多少物品。
它有特定的索引吗?
你还可以使用 Array 对象来检查你的数组是否包含某个特定的索引。要了解如何操作,请运行 清单 16-6 中的程序。
1 ' ContainsIndex.sb
2 age["Homer"] = 18
3 age["Marge"] = 17
4 score[1] = 90
5 ans1 = Array.ContainsIndex(age, 1) ' Returns "False"
6 ans2 = Array.ContainsIndex(age, "homer") ' Returns "True"
7 ans3 = Array.ContainsIndex(age, "Lisa") ' Returns "False"
8 TextWindow.WriteLine(ans1 + ", " + ans2 + ", " + ans3)
9
10 ans1 = Array.ContainsIndex(score, "1") ' Returns "True"
11 ans2 = Array.ContainsIndex(score, 1) ' Returns "True"
12 ans3 = Array.ContainsIndex(score, 2) ' Returns "False"
13 TextWindow.WriteLine(ans1 + ", " + ans2 + ", " + ans3)
清单 16-6:演示 ContainsIndex() 方法
ContainsIndex() 方法接受两个参数。第一个参数是数组的名称,第二个参数是你要检查的索引。该方法会根据索引是否存在于数组中返回 "True" 或 "False"。
第 6 行显示了搜索索引时是不区分大小写的,这就是为什么搜索索引 homer 返回 "True"。此外,搜索 score 数组中的索引 "1"(作为字符串)或索引 1(作为数字)都返回了 "True"。
如果你不确定某个数组是否包含特定的索引,可以使用 ContainsIndex() 方法来查找。这个方法对于处理非常长的数组特别有用。
它是否具有特定的值?
Array 对象还提供了一种方法,用于检查数组是否包含某个特定的值。运行 清单 16-7 中的程序,了解 ContainsValue() 方法是如何工作的。
1 ' ContainsValue.sb
2 age["Homer"] = 18
3 age["Marge"] = 17
4 score[1] = 90
5 ans1 = Array.ContainsValue(age, 18) ' Returns "True"
6 ans2 = Array.ContainsValue(age, 20) ' Returns "False"
7 ans3 = Array.ContainsValue(score, 90) ' Returns "True"
8 TextWindow.WriteLine(ans1 + ", " + ans2 + ", " + ans3)
清单 16-7:演示 ContainsValue() 方法
ContainsValue() 方法根据检查的值是否存在于数组中,返回 "True" 或 "False"。
注意
与 ContainsIndex() 方法不同, ContainsValue() 方法是区分大小写的。所以最好保持大小写一致!
给我所有的索引
Array 对象的另一个有用方法是 GetAllIndices()。该方法返回一个包含给定数组所有索引的数组。返回数组的第一个元素的索引为 1。要理解这个方法是如何工作的,请运行 清单 16-8 中的程序。
1 ' GetAllIndices.sb
2 age["Homer"] = 18
3 age["Marge"] = 17
4 names = Array.GetAllIndices(age)
5 TextWindow.WriteLine("Indices of the age array:")
6 For N = 1 To Array.GetItemCount(names)
7 TextWindow.WriteLine("Index" + N + " = " + names[N])
8 EndFor
清单 16-8:演示 GetAllIndices() 方法
第 4 行调用 GetAllIndices() 来查找 age 数组的所有索引。该方法返回一个数组,并将其保存在 names 标识符中。接着代码开始一个循环,从 names 中的第一个元素运行到最后一个元素。注意代码是如何使用 GetItemCount() 方法来计算这个值的。以下是这段代码的输出:
Indices of the age array:
Index1 = Homer
Index2 = Marge
现在让我们将你学到的方法好好利用一下。你觉得你的电脑足够聪明,能够写诗吗?好吧,我们来看看!
动手实践 16-3
打开本章文件夹中的 AnimalSpeed.sb 文件。这个游戏会考察玩家不同动物的最高速度(单位为英里每小时)。程序包含一个关联数组,类似于这样:
speed["cheetah"] = 70
speed["antelope"] = 60
speed["lion"] = 50
' ... and so on
运行这个游戏看看它是如何工作的。这个游戏使用了哪些Array对象方法?解释一下游戏的工作原理,然后想一些点子让游戏更有趣。确保完成所有任务。别像猎豹一样偷懒!
你的电脑是诗人
现在,让我们运用所学的关联数组知识,编写一个生成诗歌的程序。这个人工诗人从五个列表(article,adjective,noun,verb 和 preposition)中随机选择单词,并将它们组合成固定的模式。为了给诗歌赋予一个中心主题,这些列表中的所有单词都与爱与自然相关。当然,我们可能还是会得到一些傻乎乎的诗歌,但那也一样有趣!
注意
这个程序的灵感来自于丹尼尔·瓦特的《使用 Logo 学习》(McGraw-Hill, 1983)。
图 16-1 显示了该应用程序的用户界面。

图 16-1:Poet.sb 的用户界面
每次点击“New”按钮时,诗人都会朗诵一首新诗。每首诗包含三行,遵循以下模式:
• 第 1 行:冠词,形容词,名词
• 第 2 行:冠词,名词,动词,介词,冠词,形容词,名词
• 第 3 行:形容词,形容词,名词
接下来的部分将指导你创建这个程序。
步骤 1:打开启动文件
打开本章文件夹中的 Poet_Incomplete.sb 文件。该文件包含一个名为 CreateLists() 的子例程,用于创建程序所需的五个列表。添加这个子例程是为了让你不必输入一堆单词。它的内容如下:
Sub CreateLists
article = "1=a;2=the;...;5=every;"
adjective = "1=beautiful;2=blue;...;72=young;"
noun = "1=baby;2=bird;...;100=winter;"
verb = "1=admires;2=amuses;...;92=whispers;"
prepos = "1=about;2=above;...;37=without;"
EndSub
省略号(...)表示缺失的数组元素,但当你打开文件时,你可以看到所有这些元素。请注意,article 数组还包括其他限定词,如 one、each 和 every。
步骤 2:设置图形用户界面
将 清单 16-9 中的代码添加到程序文件的开头,以设置图形用户界面(GUI)并注册按钮的事件处理程序。
1 GraphicsWindow.Title = "The Poet"
2 GraphicsWindow.CanResize = "False"
3 GraphicsWindow.Width = 480
4 GraphicsWindow.Height = 360
5 GraphicsWindow.FontBold = "False"
6 GraphicsWindow.FontItalic = "True"
7 GraphicsWindow.FontSize = 16
8
9 path = Program.Directory
10 GraphicsWindow.DrawImage(path + "\Background.png", 0, 0)
11 Controls.AddButton("New", 10, 10)
12
13 CreateLists()
14
15 artCount = Array.GetItemCount(article)
16 adjCount = Array.GetItemCount(adjective)
17 nounCount = Array.GetItemCount(noun)
18 verbCount = Array.GetItemCount(verb)
19 prepCount = Array.GetItemCount(prepos)
20
21 Controls.ButtonClicked = OnButtonClicked
22 OnButtonClicked()
清单 16-9:设置图形用户界面
程序通过初始化图形窗口(第 1-7 行)、绘制背景图像(第 9-10 行)和创建“New”按钮(第 11 行)开始。接下来,它调用 CreateLists() 子例程来初始化五个索引数组(第 13 行)。然后,程序使用 Array 对象获取每个数组中的项数,并将这些值保存在第 15-19 行。这样,你就可以在不影响程序其余部分的情况下,向这些数组的末尾添加更多元素。例如,如果你想添加第 73 个形容词,可以在 CreateLists() 子例程中的 adjectives 数组行末尾加上 73=callipygous;。因为第 16 行在 清单 16-9 中获取该数组的元素数量,所以你添加的新元素会自动被计数并随机选入诗歌,就像其他元素一样。
最后,程序为 ButtonClicked 事件注册了一个处理程序(第 21 行),并调用该处理程序子例程来显示第一首诗(第 22 行)。
步骤 3:响应按钮点击
现在你需要添加 OnButtonClicked() 子程序,如 列表 16-10 所示。
1 Sub OnButtonClicked
2 GraphicsWindow.DrawImage(path + "\Background.png", 0, 0)
3
4 MakeLine1() ' Constructs poemLine1
5 MakeLine2() ' Constructs poemLine2
6 MakeLine3() ' Constructs poemLine3
7
8 GraphicsWindow.DrawText(180, 140, poemLine1)
9 GraphicsWindow.DrawText(100, 165, poemLine2)
10 GraphicsWindow.DrawText(180, 190, poemLine3)
11 EndSub
列表 16-10: OnButtonClicked() 子程序
这个子程序重新绘制背景图像以清除图形窗口(第 2 行)。接着,它调用三个子程序来生成诗歌的三行内容(第 4-6 行),并将这些行绘制到图形窗口中(第 8-10 行)。接下来,你将添加三个缺失的子程序。
第 4 步:编写诗歌的第一行
诗歌的第一行采用以下形式:冠词、形容词、名词。添加 列表 16-11 中的子程序,该子程序创建诗歌的第一行并将其赋值给 poemLine1 变量。
1 Sub MakeLine1
2 art1 = article[Math.GetRandomNumber(artCount)]
3 adj1 = adjective[Math.GetRandomNumber(adjCount)]
4 noun1 = noun[Math.GetRandomNumber(nounCount)]
5 poemLine1 = art1 + " " + adj1 + " " + noun1
6 EndSub
列表 16-11: MakeLine1() 子程序
MakeLine1() 子程序从 article、adjective 和 noun 数组中随机选择三个单词,并将其存储在 art1、adj1 和 noun1 中(第 2-4 行)。然后,它通过在这些变量之间添加空格来填充 poemLine1(第 5 行)。
第 5 步:编写诗歌的第二行和第三行
MakeLine2() 和 MakeLine3() 子程序与 MakeLine1() 子程序非常相似。第二行的形式是:冠词、名词、动词、介词、冠词、形容词、名词。第三行的形式是:形容词、形容词、名词。自己创建这些子程序。如果遇到困难,可以打开文件 Poet.sb 查看我们如何编写这些子程序。完成后,把你最喜欢的诗歌输出背诵给家人或朋友听,看看他们是否认为是你写的!
尝试 16-4
多次运行你的诗人程序,看看机器诗人能创作出什么样的作品。设计不同的诗歌模式,并教这个诗人如何创作它们。然后,将单词更改为你想要的任何单词(以及任意数量的单词)!前往 tiny.cc/sbpoet/ 与社区分享你的诗歌程序,并看看其他人创作了什么。
注意
Array 对象包括三个创建不同类型数组的方法: SetValue()、GetValue() 和 RemoveValue()。尽管这些方法效果很好,但数组的方括号形式在编程语言中更为通用,这也是本书专注于这种形式的原因。
编程挑战
如果遇到困难,请查看 nostarch.com/smallbasic/ 以获取解决方案、更多资源和教师及学生的复习问题。
-
编写一个程序,记录你朋友的电话号码。使用一个关联数组,将你朋友的名字作为键;例如,
phone["Yoda"] = "555-1138"。 -
编写一个程序,保存书籍信息。书籍的关键是 ISBN。对于每本书,你需要知道书名、作者和出版年份。使用三个关联数组:
title[ISBN]、author[ISBN]和year[ISBN]。 -
打开本章文件夹中的VirtualPiano.sb文件。该程序使用键盘实现了一个虚拟钢琴。解释一下该程序是如何工作的。
第十八章:17
扩展到高维数组

在前两章中,你学习了如何使用一维数组来存储项目集合。本章将这一概念扩展到二维及更高维度。处理多维数组也叫做处理高维数组。
在二维(2D)数组中,你可以在表格或网格中存储值。例如,想象一下棒球比赛中的记分牌(见 Figure 17-1)。左侧列列出了队名,右侧列列出了局数和其他统计数据。

Figure 17-1:棒球记分牌
本章中你将创建的数组类似于记分牌。它们允许你将数据按行和列组织起来。
完成本章后,你将理解二维及其他高维数组,并能够使用它们来构建新类型的应用程序,包括寻宝游戏!
二维数组
一个二维数组有两个维度:行和列。你可以将二维数组视为一个表格。例如,Figure 17-2 显示了一个名为score的二维数组,用来存储学生在三门科目中的考试成绩。

Figure 17-2:二维数组的图示视图
第一行包含数学考试成绩,第二行记录了科学考试成绩,接下来的行存储了英语成绩。这种元素的二维排列也称为矩阵(复数为矩阵)。但这个矩阵不会教你慢动作功夫!
要访问矩阵中的单个元素,你需要两个索引:一个表示行,另一个表示列。以下是一些示例:
score[1][1] = 95 ' Row 1, column 1
score[1][2] = 87 ' Row 1, column 2
score[2][1] = 80 ' Row 2, column 1
score[2][3] = 92 ' Row 2, column 3
变量score是一个双重脚本变量,因为它需要两个索引来访问其元素。第一个索引是行号,第二个索引是列号。
与一维数组一样,每个维度的索引可以是数字或字符串。此外,矩阵中存储的值可以是数字、字符串或 Small Basic 库中对象返回的其他标识符。接下来我们来看一些二维数组的简单示例。
随机矩阵
一个名为 MI6 的客户需要你的帮助来为安全锁生成密码。程序 Listing 17-1 创建了一个名为mat的矩阵,其中包含随机数。该矩阵包含三行四列,是一个 3×4(读作3 乘 4)矩阵,或者是一个 3×4 数组。
1 ' Random2DArray.sb
2 For r = 1 To 3 ' 3 rows
3 For c = 1 To 4 ' 4 columns
4 mat[r][c] = Math.GetRandomNumber(9)
5 EndFor
6 EndFor
7
8 ' Displays the matrix to see its contents
9 For r = 1 To 3 ' 3 rows
10 For c = 1 To 4 ' 4 columns
11 TextWindow.Write(mat[r][c] + " ")
12 EndFor
13 TextWindow.WriteLine("")
14 EndFor
Listing 17-1:用随机数填充一个 3×4 数组
程序使用嵌套的For循环填充矩阵的随机数(第 2–6 行)。嵌套的For循环在处理二维数组时非常有用,因为你可以使用一个循环遍历行,另一个循环遍历列。在这个例子中,外层循环使用控制变量r(表示行),从 1 运行到 3(第 2 行);内层循环使用控制变量c(表示列),从 1 运行到 4(第 3 行)。
外层循环的第一次执行(r = 1)会导致内层循环执行四次(c = 1、2、3、4),填充mat[1][1]、mat[1][2]、mat[1][3]、mat[1][4]。外层循环的第二次执行(r = 2)会导致内层循环再执行四次(c = 1、2、3、4),填充mat[2][1]、mat[2][2]、mat[2][3]、mat[2][4]。同样,外层循环的第三次执行(r = 3)会填充矩阵的第三行。图 17-3 展示了这一过程。

图 17-3:使用嵌套的 For 循环访问矩阵元素
根据这张图,当r = 1时,程序执行顶部的c分支,填充二维数组的四组元素。当r = 2时,它会遍历中间的分支四次。当r = 3时,它会遍历底部的分支。
在用随机数填充矩阵之后,程序使用另一个嵌套循环以类似的方式显示其内容(第 9–14 行)。外层循环从 1 到 3 运行,用于索引三行(第 9 行),内层循环从 1 到 4 运行,用于索引四列(第 10 行)。第 11 行显示索引为mat[r][c](第 r 行第 c 列)的元素,并跟着一个空格。当内层循环结束时,意味着一整行已经显示完毕,光标将移动到下一行,准备显示下一行(第 13 行)。
现在是时候将你的程序交给 MI6 客户了。下面是该程序的一个示例输出,但你的输出很可能会有所不同:
2 8 1 6
3 9 3 9
1 5 7 8
通过编程使矩阵能够接受用户输入,你可以使矩阵变得更加有用。接下来,我们将看看如何做到这一点。
尝试一下 17-1
在清单 17-1 中,矩阵中的数字按行存储。首先填充第 1 行,然后是第 2 行,最后是第 3 行。原因是我们将代表行的r循环设置为外层循环,将代表列的c循环设置为内层循环。修改程序,让它先按列填充矩阵,而不是按行填充。
带有用户输入的矩阵
你的 MI6 客户喜欢你编写的程序,但现在他们希望能够将某些数字输入到密码矩阵中。你可以轻松修改清单 17-1,让它从用户处获取输入,而不是使用随机数。只需将第 4 行替换为以下两行:
TextWindow.Write("mat[" + r + "][" + c + "]: ")
mat[r][c] = TextWindow.ReadNumber()
第一条语句提示用户输入矩阵中的一个元素,第二条语句读取并存储用户的输入。进行此更改,并尝试运行程序,看看它是如何工作的。
但矩阵不仅仅是数字。你也可以使用它们来制作一些有趣的彩色应用程序。在接下来的示例中,你将创建一个彩色网格并使其动画化。
动画方块
我们来编写一个程序,创建一个 4×8 的随机颜色方块网格,并将这些方块动画化,飞向图形窗口的左上角,如图 17-4 所示。

图 17-4:展示 AnimatedSquares.sb 输出的效果
完整的应用程序在清单 17-2 中展示。
1 ' AnimatedSquares.sb
2 ' Creates a 4x8 grid of randomly colored squares
3 For r = 1 To 4 ' 4 rows
4 For c = 1 To 8 ' 8 columns
5 clr = GraphicsWindow.GetRandomColor()
6 GraphicsWindow.BrushColor = clr
7 box[r][c] = Shapes.AddRectangle(20, 20) ' Adds a square
8 Shapes.Move(box[r][c], c * 20, r * 20) ' Positions it
9 EndFor
10 EndFor
11
12 ' Animates the squares to the upper-left corner of the window
13 For r = 1 To 4
14 For c = 1 To 8
15 Shapes.Animate(box[r][c], 0, 0, 1000)
16 Program.Delay(400) ' A small delay (in milliseconds)
17 EndFor
18 EndFor
清单 17-2:使用矩阵存储形状 ID
程序使用嵌套的For循环来创建方块(第 3–10 行)。外部循环(创建行)运行四次,内部循环(创建列)运行八次(第 3–4 行),共进行 32 次迭代(4×8)。在每次内部循环中,方块的颜色通过更改BrushColor属性来设置(第 5–6 行),然后通过调用AddRectangle()创建一个方块。我们将其标识符保存在box[r][c]中(第 7 行),然后将创建的方块移动到它在方块网格中的位置(见图 17-4)。让我们仔细看一下第 7–8 行。
在第 7 行,AddRectangle()方法接受期望的矩形的宽度和高度,并返回创建的形状的标识符。在这个例子中,我们传递 20 作为两个参数来创建一个方块,并将返回的标识符保存在box[r][c]中。
要移动方块,我们调用Shapes对象的Move()方法(第 8 行)。该方法接受三个参数:我们要移动的形状标识符,以及我们要将其移动到的位置的 x 和 y 坐标。每一行中的方块的 x 位置(左边缘)分别是 1 × 20 = 20, 2 × 20 = 40, 3 × 20 = 60,依此类推。每一列中的方块的 y 位置(上边缘)分别是 1 × 20 = 20, 2 × 20 = 40, 3 × 20 = 60,依此类推。这就是为什么我们在调用Move()时使用c * 20和r * 20的原因。
在这个For循环的末尾,box矩阵包含了由Shapes对象创建的 32 个方块的 32 个唯一标识符。
程序随后通过动画化方块(第 13–18 行),使用嵌套的For循环来访问box的行和列。在每次迭代中,我们请求Shapes对象动画化一个方块(第 15 行),然后暂停一段时间(第 16 行)。Animate()方法接受四个参数:我们要动画化的形状的标识符、目标的 x 和 y 坐标,以及动画持续时间(毫秒)。我们要求Shapes对象将每个方块在 1 秒(1000 毫秒)内移动到点(0, 0)。
尝试一下 17-2
修改清单 17-2 中的程序,通过列而不是行来动画化方块。如果你有艺术感觉,可以尝试移动方块,创建图形窗口中的一个图案。
使用字符串索引
之前的示例使用整数索引来访问矩阵的元素。我们的下一个示例将教你如何使用字符串作为索引。你将查看一个应用程序,它用于跟踪学生在不同科目中的成绩。
欢迎来到教授 Xavier 的天才少年学校!目前班上只有三名学生:Scott、Jean 和 Logan(其他人正在执行一个重要任务)。学校只教授三门课程:数学、科学和战斗。让我们编写一个程序,提示用户输入学生的姓名,然后显示该学生的平均成绩。完整程序见示例 17-3。
1 ' StudentAvg.sb
2 score["Scott"]["Math"] = 92
3 score["Scott"]["Science"] = 90
4 score["Scott"]["Combat"] = 87
5 score["Jean"]["Math"] = 85
6 score["Jean"]["Science"] = 82
7 score["Jean"]["Combat"] = 92
8 score["Logan"]["Math"] = 85
9 score["Logan"]["Science"] = 95
10 score["Logan"]["Combat"] = 99
11
12 TextWindow.Write("Enter student name: ")
13 name = TextWindow.Read()
14 sum = score[name]["Math"]
15 sum = sum + score[name]["Science"]
16 sum = sum + score[name]["Combat"]
17 avg = Math.Round(sum / 3)
18 TextWindow.WriteLine(name + " average score = " + avg)
示例 17-3:使用字符串作为索引
程序首先通过初始化score矩阵来设置三名学生的成绩(第 2–10 行)。行由学生的名字索引,列由科目索引。图 17-5 展示了score矩阵的可视化表示。

图 17-5:在示例 17-3 中的成绩矩阵
程序提示用户输入学生的姓名(第 12 行),并将输入的值赋给name变量(第 13 行)。然后,程序将该学生的数学成绩存入sum变量(第 14 行),将该学生的科学成绩加到sum中(第 15 行),并加上学生的战斗成绩(第 16 行)。最后,程序计算平均成绩(第 17 行)并显示出来(第 18 行)。
以下是一个示例运行的输出:
Enter student name: scott
scott average score = 90
字符串索引不区分大小写,这就是为什么当我们输入小写的scott时程序仍然能正常工作。你觉得如果输入无效的学生姓名,程序会怎样输出?运行程序来验证你的答案。
动手实践 17-3
更新示例 17-3 中的程序,以显示某学生在给定科目的成绩。让用户输入学生的姓名和科目。
互动环节
让我们探索一下如何从用户那里获取学生成绩,而不是像在示例 17-3 中那样将它们硬编码在程序中。我们将使用两个循环来遍历学生的姓名和科目,如下所示的伪代码(稍后你将学习如何将这个伪代码转化为真实的代码):
For each student in the array: [Scott, Jean, Logan]
For each subject in the array: [Math, Science, Combat]
score[student][subject] = read score from user
EndFor
EndFor
你可以将学生的名字保存在一个一维数组中,将科目的名字保存在另一个一维数组中,然后使用嵌套的For循环和整数索引来访问这两个数组的个别元素。接着,你可以使用字符串(学生名字和科目)作为score矩阵的索引。查看示例 17-4 了解代码如何运作。
1 ' StudentAvg2.sb
2 nameList = "1=Scott;2=Jean;3=Logan;"
3 subjList = "1=Math;2=Science;3=Combat;"
4
5 For I = 1 To 3 ' Three students
6 name = nameList[I] ' Name of the Ith student
7 For J = 1 To 3 ' Three subjects
8 subj = subjList[J] ' Name of Jth subject
9 TextWindow.Write(name + "'s " + subj + " score: ")
10 score[name][subj] = TextWindow.ReadNumber()
11 EndFor
12 EndFor
13 TextWindow.Write("Enter student name: ")
14 name = TextWindow.Read()
15 sum = score[name]["Math"]
16 sum = sum + score[name]["Science"]
17 sum = sum + score[name]["Combat"]
18 avg = Math.Round(sum / 3)
19 TextWindow.WriteLine(name + " average score = " + avg)
示例 17-4:从用户输入成绩
程序首先创建学生姓名和科目数组(第 2–3 行)。然后,开始一个嵌套循环来填充score矩阵。外层循环遍历学生,内层循环遍历科目。
外部循环从 I = 1 开始。在这里,name 被赋值为 nameList[1],即 "Scott"(第 6 行)。然后,内部循环执行三次,第一次 J = 1,subject 被赋值为 subjList[1],即 "Math"(第 8 行)。第 9 行显示 Scott's Math score:,第 10 行等待用户输入。用户输入的数字被保存到 score["Scott"]["Math"],然后内部循环在 J = 2 时重复。现在 subject 被赋值为 subjList[2],即 "Science"。程序显示 Scott's Science score:,等待用户输入,将输入的数字存储在 score["Scott"]["Science"],并在 J = 3 时继续重复内部循环。现在 subject 被赋值为 subjList[3],即 "Combat"。程序显示 Scott's Combat score:,等待用户输入,并将输入的数字存储在 score["Scott"]["Combat"] 中。这结束了内部循环。
外部循环在 I = 2 时重复。这将 name 设置为 nameList[2],即 "Jean",然后内部循环再次运行,以填写 score["Jean"]["Math"]、score["Jean"]["Science"] 和 score["Jean"]["Combat"]。
外部循环在 I = 3 时重复。这将 name 设置为 nameList[3],即 "Logan",然后内部循环再次运行,以填写 score["Logan"]["Math"]、score["Logan"]["Science"] 和 score["Logan"]["Combat"]。
跟踪这个程序的第二个版本,理解它是如何工作的。思考每一步发生了什么是学习矩阵工作原理的好方法!
尝试一下 17-4
将清单 17-4 中用于计算 sum 的语句(第 15-17 行)替换为一个 For 循环,如下所示的代码片段:
sum = 0
For J = 1 To 3
' Add each student's score in the Jth subject to sum
EndFor
数字二维数组的常见操作
在本节中,我们将开发一套有用的子程序,能够对由数字组成的二维数组执行常见操作。我们将使用一个虚构公司 Duckberg Industries 的销售数据,该公司 12 月的销售报告如图 17-6 所示。该公司有四家门店(Beddy Buyz、UBroke I.T. Emporium、LAN Lord’s Cyber Store 和 Mother Bored Electronics),销售五种类型的产品:爆炸鞋(eShoes)、iShirt 计算机(iShirt)、Shampoop、脱水水(dWater)和隐形帽子(iHat)。这些数字代表每种产品的销售额(单位:千元)。

图 17-6:Duckberg Industries 12 月销售报告
打开本章文件夹中的 Duckberg_Incomplete.sb 文件。该文件包含图 17-6 中的数据,形式如下:
sales[1][1] = 50 ' Beddy Buyz store; Exploding Shoes sales
sales[1][2] = 60 ' Beddy Buyz store; iShirt Computer sales
--snip--
sales[4][4] = 80 ' Mother Bored Electronics; Dehydrated Water sales
sales[4][5] = 90 ' Mother Bored Electronics; Invisible Hat sales
程序还定义了以下变量:
ROWS = 4 ' Number of rows
COLS = 5 ' Number of columns
product = "1=eShoes;2=iShirt;3=Shampoop;4=dWater;5=iHat"
按照接下来的两节中的指示完成程序。
步骤 1:添加所有元素
销售经理 Donald 想知道公司总销售额。你需要将 sales 矩阵中的所有数字相加。清单 17-5 中的 TotalSales() 子程序展示了如何完成此操作。
1 Sub TotalSales
2 sum = 0 ' Initializes the running sum
3 For r = 1 To ROWS ' For all rows
4 For c = 1 To COLS ' For all columns
5 sum = sum + sales[r][c] ' Adds number at row r, column c
6 EndFor
7 EndFor
8 TextWindow.WriteLine("Total Sales: $" + sum + " K")
9 EndSub
清单 17-5:对矩阵中的所有数字求和
你首先将 sum 变量(用于保存运行总和)初始化为 0(第 2 行)。然后你使用嵌套循环遍历所有行和列(第 3–4 行)。对于每次迭代,你将 sales[r][c] 中存储的数字加到 sum 上(第 5 行)。当外部循环结束时,你显示结果,并在后面加上 K 表示千位(第 8 行)。
将此子程序添加到程序中,然后添加语句以调用它。以下是当你调用 TotalSales() 子程序时应该看到的结果:
Total Sales: $1340 K
步骤 2:计算每列的总和
Donald 还想查看每个 Duckberg Industries 产品的总销售额。他需要将这些数字与竞争对手的销售额进行对比,以评估他公司在市场中的份额。
为了给 Donald 提供这些信息,你将使用清单 17-6 中的 ColumnSum() 子程序来计算 sales 矩阵中每一列的总和。
1 Sub ColumnSum
2 For c = 1 To COLS ' For each column
3 sum = 0 ' Initializes the sum for column c
4 For r = 1 To ROWS ' Iterates over the rows
5 sum = sum + sales[r][c] ' Adds number at row r, column c
6 EndFor
7 colName = product[c] + " Sales: $" ' Name to display
8 TextWindow.WriteLine(colName + sum + " K")
9 EndFor
10 EndSub
清单 17-6:ColumnSum() 子程序
你开始外部循环,遍历五列(第 2 行)。对于每一列(每个 c 的值),你将该列的 sum 初始化为 0(第 3 行),然后启动一个 For 循环,将该列所有行的数字累加到 sum(第 4–6 行)。当内部循环结束时,你获取当前产品的名称(从 product[c] 中),在它后面附加 "Sales: $",并将结果字符串保存在 colName 中(第 7 行)。在第 8 行,你显示该字符串,并显示你刚刚计算的总和。外部循环然后重新开始,计算并显示下一列的总和。
将此子程序添加到程序中,然后添加语句以调用它。以下是当你调用 ColumnSum() 子程序时应该看到的结果:
eShoes Sales: $190 K
iShirt Sales: $200 K
Shampoop Sales: $310 K
dWater Sales: $330 K
iHat Sales: $310 K
动手试一试 17-5
Donald 想通过比较每个商店的总销售额来回顾他四家店的业绩。编写一个子程序 RowSum(),计算并显示 sales 矩阵中每一行的总和。
三维或更高维度的数组
你已经了解到,使用 2D 数组是一种表示表格或矩阵的方便方式。Small Basic 还支持多于两维的数组。你可以扩展创建 2D 数组的语法,来创建更高维度的数组。接下来我们将探讨如何在 Small Basic 中创建三维 (3D) 数组。
让我们来操作一个有五个架子的货架。每个架子有三行四列,每个位置上都有一个盒子,盒子中包含不同大小的螺丝。看看图 17-7,并想象每一列和每一行中都有不同大小的螺丝盒子(总共是 12 个盒子)。接着想象所有五个架子上都有同样数量的盒子。这样总共有 60 个盒子!

图 17-7:可视化 3D 数组
我们将查看一个程序,该程序为每个盒子填充一个随机数,表示该盒子中螺丝的大小。程序展示在清单 17-7 中。
1 ' 3DArrayDemo.sb
2 For rack = 1 To 5 ' For each rack
3 For row = 1 To 3 ' For each row
4 For col = 1 To 4 ' For each column
5 box[rack][row][col] = Math.GetRandomNumber(9)
6 EndFor
7 EndFor
8 EndFor
清单 17-7:演示 3D 数组的语法
这个程序创建了一个名为box的三维数组。它的元素使用三个下标来索引:rack的范围是 1 到 5(第 2 行),row的范围是 1 到 3(第 3 行),col的范围是 1 到 4(第 4 行)。这个数组有 60 个元素(5×4×3),就像示例中的架子一样。第 5 行使用语法box[rack] [row][col]来访问编号为rack的架子、编号为row的行和编号为col的列中的盒子,并在该盒子中放入一个随机数。
请注意,使用了另一个嵌套的For循环,不过在这个例子中,我们嵌套了三个For循环,而不仅仅是两个(第 2–4 行)。通常,你需要为高维数组的每个维度使用一个For循环;这样,你就能够访问数组中的每一个元素!
在下一部分,你将运用迄今为止学到的知识,创建一个激动人心的寻宝游戏。准备好迎接又一次冒险吧!
试试 17-6
编写一个程序,显示清单 17-7 中box数组的输出。你的输出应该具有以下格式:
Rack 1:
2 7 3 2
4 3 1 3
1 2 6 4
Rack 2:
8 8 2 1
7 4 2 7
1 5 2 7
--snip--
创建一个寻宝游戏
你某天早晨醒来,发现自己孤身一人在一个岛屿上。旁边有一张藏宝图和一只旧指南针。你几乎抑制不住兴奋的心情!你决定去寻找宝藏。图 17-8 展示了岛屿的示例地图。

图 17-8:寻宝游戏的用户界面
你可以每次移动一格,向北、东、南或西。但由于指南针很旧,它可能会把你指向错误的方向。例如,如果你向北或向南走,有 20%的概率你还会向左或向右移动一格。如果你向东或向西走,也有 20%的概率你会向上或向下移动一格。每次移动时,你会收到关于当前位置的信息。如果你找到了宝藏,或者不小心掉进了有饥饿鲨鱼等待的水域,游戏就结束了!在玩这个游戏时不要想起大白鲨!(抱歉,这可能没什么帮助。)
由于你手中有藏宝图,你应该能够猜出自己的位置。例如,假设你在一片森林中,当你点击 S 按钮向南走时,游戏告诉你你现在站在一个火山旁边。看着地图,你可以推测出宝藏就在西边两格远的地方。
以下部分将一步步指导你如何将这个游戏拼凑起来。冒险在等待着你!
步骤 1:打开启动文件
打开本章文件夹中的TreasureMap_Incomplete.sb文件。该文件包含一些注释和占位符,供你填写所需的子程序。你将一步步添加所有的代码。
这个文件夹还包含了你将使用的八张图片。Background.png是游戏背景的 580×450 像素图像,另外七个 32×32 像素的图标代表藏宝图上的不同物体。
注意
如果你遇到问题,可以查看完成的程序 TreasureMap.sb,它也包含在本章的文件夹中。
步骤 2:创建 GUI 元素
添加 清单 17-8 中的代码来初始化 GraphicsWindow 并创建游戏的控件(按钮和文本形状)。
1 GraphicsWindow.Title = "Treasure Map"
2 GraphicsWindow.Width = 580
3 GraphicsWindow.Height = 450
4 GraphicsWindow.CanResize = "False"
5 GraphicsWindow.FontSize = 14
6 GraphicsWindow.FontName = "Courier New"
7
8 ' Creates a text shape for showing the player's location
9 GraphicsWindow.BrushColor = "Black"
10 txtID = Shapes.AddText("")
11 Shapes.Move(txtID, 60, 415)
12
13 ' Creates the 4 movement buttons and the new game button
14 GraphicsWindow.BrushColor = "Red"
15 btnN = Controls.AddButton("N", 507, 10)
16 btnS = Controls.AddButton("S", 507, 90)
17 btnW = Controls.AddButton("W", 467, 50)
18 btnE = Controls.AddButton("E", 541, 50)
19 btnNew = Controls.AddButton("New Game", 480, 370)
20
21 Controls.ButtonClicked = OnButtonClicked
22
23 NewGame()
清单 17-8:初始化 GraphicsWindow
第 1 行到第 6 行设置 GraphicsWindow 的属性,第 9 行到第 11 行创建并定位显示玩家当前在岛上位置的文本,第 14 行到第 19 行创建五个按钮(见 图 17-8)。第 21 行注册一个处理程序来处理按钮,第 23 行调用 NewGame() 来开始新游戏。
步骤 3:开始新游戏
现在你将添加 NewGame() 子程序。该子程序(见 清单 17-9)在玩家点击“新游戏”按钮时被调用。
1 Sub NewGame
2 gameOver = 0 ' Game isn't over yet
3 moveNumber = 0 ' How many moves the player makes
4 path = Program.Directory
5
6 GraphicsWindow.DrawImage(path + "\Background.png", 0, 0)
7 CreateNewMap() ' Creates and draws a new treasure map
8 ShowLocation() ' Gives feedback to the player
9 EndSub
清单 17-9: NewGame() 子程序
你将 gameOver 标志设置为 0,因为游戏还没有结束(第 2 行)。你还将 moveNumber 设置为 0,因为玩家还没有进行任何移动(第 3 行)。接下来,你会找到程序的路径并将其赋值给 path 变量。你将使用这个变量在藏宝图上绘制不同的图标。在第 6 行,你会绘制一个新的背景图像来擦除之前的地图。然后,你会调用 CreateNewMap() 来创建并绘制新的藏宝图(第 7 行),并调用 ShowLocation() 来反馈玩家在岛上的当前位置(第 8 行)。ShowLocation() 更新文本信息,以描述玩家移动后的新位置。你接下来将添加这些子程序。
步骤 4:创建新的藏宝图
CreateNewMap() 子程序构建了一个 10×10 的数组来表示藏宝图。数组中的每个元素存储一个 0 到 7 之间的数字。数字 0 表示空地,1 表示草地,2 表示森林,3 表示火山,4 表示洞穴,5 表示雨,6 表示花朵,7 表示宝藏。CreateNewMap() 子程序的代码见 清单 17-10。
1 Sub CreateNewMap
2 For row = 1 To 10
3 For col = 1 To 10
4 map[row][col] = 0 ' Clears all cells
5 EndFor
6 EndFor
7
8 objId = "1=1;2=1;3=1;4=1;5=1;6=1;7=1;8=1;9=2;10=2;11=2;12=2;13=2;14=2;
15=2;16=2;17=3;18=3;19=4;20=4;21=5;22=5;23=6;24=6;25=7;26=0"
9 count = 1 ' Points to first element in objId
10 While (count <= Array.GetItemCount(objId))
11 row = Math.GetRandomNumber(10)
12 col = Math.GetRandomNumber(10)
13 If (map[row][col] = 0) Then ' Cell is clear
14 map[row][col] = objId[count] ' Reserves the cell
15 DrawObject()
16 count = count + 1 ' Points to next element in objId
17 EndIf
18 EndWhile
19
20 rowP = row ' Player's current row
21 colP = col ' Player's current column
22 EndSub
清单 17-10: CreateNewMap() 子程序
首先,你将地图的所有元素设置为 0(第 2 行到第 6 行)。在第 8 行,你定义了一个数组 objId,该数组保存你将添加到地图中的对象标识符。这个数组要求八个草地、八个森林、两个火山、两个洞穴、两个雨区、两个花地和一个宝藏点。数组中的最后一个元素被故意设置为 0,以便第 10 行的 While 循环能够找到玩家的空白起始位置。当你想要更具冒险精神时,可以修改 objId 数组,使藏宝图包含更多或更少的对象。
接下来,你开始一个While循环,将物体添加到藏宝图中。首先,你选择地图上的一个随机单元格(第 11-12 行)。如果该单元格为空(第 13 行),你用非零的数字标记它,以便为下一个来自objId的物体保留该位置(第 14 行),然后调用DrawObject()在藏宝图上绘制该物体(第 15 行),并递增count变量,指向objId中的下一个元素(第 16 行)。当循环完成时,你将玩家的当前行rowP和列colP设置为While循环最后一次迭代中找到的空单元格(第 20-21 行)。这样可以确保玩家从地图上的空单元格开始。
步骤 5:在地图上绘制物体
在你添加ShowLocation()子例程之前,需要先在清单 17-11 中添加DrawObject()子例程。你调用这个子例程来在map[row][col]的位置绘制物体。
1 Sub DrawObject
2 imgName = "1=Grass.ico;2=Tree.ico;3=Volcano.ico;4=Cave.ico;5=Rain.ico;
6=Flower.ico;7=Treasure.ico"
3
4 imgID = map[row][col]
5 If ((imgID >= 1) And (imgID <= 7)) Then
6 imgPath = path + "\" + imgName[imgID]
7
8 xPos = 52 + (col - 1) * 38
9 yPos = 25 + (row - 1) * 38
10 GraphicsWindow.DrawImage(imgPath, xPos, yPos)
11 EndIf
12 EndSub
清单 17-11: DrawObject() 子例程
你定义了imgName数组,用于存储游戏中七个物体的图像文件名(第 2 行)。在第 4 行,你获取存储在地图上行号为row、列号为col的单元格中的数字,然后将该值赋给imgID。如果该数字介于 1 和 7 之间(第 5 行),你会构建对应数字的图像完整路径(第 6 行),并将该图像绘制在地图上的相应位置(第 8-10 行)。你在第 8-9 行看到的数字(52、38 和 25)来自背景图像。这些数字确保物体被绘制在图 17-8 中单元格的中心。
步骤 6:显示玩家位置
现在,你可以在清单 17-12 中添加ShowLocation()子例程,它会告诉玩家他们在岛上的当前位置。
1 Sub ShowLocation
2 locID = map[rowP][colP]
3 If (locID = 1) Then
4 msg = "You're in a grass field."
5 ElseIf (locID = 2) Then
6 msg = "You're in a forest."
7 ElseIf (locID = 3) Then
8 msg = "You're next to a volcano."
9 ElseIf (locID = 4) Then
10 msg = "You're in a cave."
11 ElseIf (locID = 5) Then
12 msg = "You're in the rain."
13 ElseIf (locID = 6) Then
14 msg = "You're in a flower field."
15 ElseIf (locID = 7) Then
16 gameOver = 1
17 msg = "Congratulations! You found the treasure!"
18 Else
19 msg = "You're in the clear!"
20 EndIf
21
22 Shapes.SetText(txtID, "[" + moveNumber + "]: " + msg)
23 EndSub
清单 17-12: ShowLocation() 子例程
该子例程使用If/ElseIf结构根据玩家当前的位置创建一条信息msg,该位置由rowP和colP标识(第 1-20 行)。然后,子例程调用SetText()通过由txtID标识的文本形状显示这条信息。注意信息中包含了玩家的移动次数moveNumber,这样玩家就能知道他们移动了多少次。
步骤 7:处理按钮点击事件
这是完成游戏的最后一步!你只需要处理按钮点击事件。添加在清单 17-13 中显示的OnButtonClicked()子例程。
1 Sub OnButtonClicked
2 btnID = Controls.LastClickedButton
3
4 If (btnID = btnNew) Then
5 NewGame()
6 ElseIf (gameOver = 0) Then
7 moveNumber = moveNumber + 1
8
9 MovePlayer() ' Finds the player's new row and column
10
11 If ((rowP < 1) Or (rowP > 10) Or (colP < 1) Or (colP > 10)) Then
12 gameOver = 1
13 Shapes.SetText(txtID, "Sorry! You were eaten by the shark!")
14 Else
15 ShowLocation() ' Tells the player their new position
16 EndIf
17 EndIf
18 EndSub
清单 17-13: OnButtonClicked() 子例程
由于你使用了五个按钮,首先你需要找到被点击按钮的标识符(第 2 行)。如果是“新游戏”按钮(第 4 行),你调用NewGame()来重新开始(第 5 行)。否则,玩家点击了四个移动按钮中的一个。只有在游戏尚未结束时,你才需要处理玩家的请求。如果游戏仍在进行中(第 6 行),你增加moveNumber(第 7 行),调用MovePlayer()来设置玩家的新位置(第 9 行),然后检查此移动后的状态(第 11 行至第 16 行)。如果玩家掉入了鲨鱼出没的水域(第 11 行),你将gameOver设为 1(第 12 行),并通知玩家他们的运气不好(第 13 行)。否则,如果玩家仍然在岛上,你调用ShowLocation()来提供他们新位置的信息(第 15 行)。
你需要在这个游戏中添加的最后一个子程序位于示例 17-14。MovePlayer()子程序根据玩家点击的按钮(N、E、S 或 W)设置玩家的下一个位置。
1 Sub MovePlayer
2 shift = 0 ' How much to shift direction
3 randNum = Math.GetRandomNumber(10)
4 If (randNum = 1) Then
5 shift = 1
6 ElseIf (randNum = 2) Then
7 shift = -1
8 EndIf
9
10 If (btnID = btnN) Then ' North
11 rowP = rowP - 1
12 colP = colP + shift
13 ElseIf (btnID = btnS) Then ' South
14 rowP = rowP + 1
15 colP = colP + shift
16 ElseIf (btnID = btnE) Then ' East
17 colP = colP + 1
18 rowP = rowP + shift
19 ElseIf (btnID = btnW) Then ' West
20 colP = colP - 1
21 rowP = rowP + shift
22 EndIf
23 EndSub
示例 17-14: MovePlayer() 子程序
我们提到过旧指南针有 20%的可能性会出错。为了模拟这一点,你创建了变量shift来改变玩家的方向。首先,你生成一个介于 1 到 10 之间的随机数(第 3 行)。如果这个数字是 1,你将shift设为 1。如果这个数字是 2,你将shift设为–1(第 4 行至第 8 行)。否则,你将保持shift为 0,这意味着你不会改变玩家的移动(第 2 行)。
你开始使用If/ElseIf阶梯来处理被点击的按钮(第 10 行至第 22 行)。如果玩家点击了北方按钮 N(第 10 行),你将他们向上移动一行(第 11 行),并使用shift变量改变他们当前的列(第 12 行)。如果shift为 0,玩家的当前列不会改变,他们将向北移动。阶梯的其余部分以相同方式工作。
现在游戏完成了,你可以尽情享受。看看你需要多长时间才能找到宝藏而不被鲨鱼吃掉!
尝试一下 17-7
《寻宝图》游戏还有很多可以改进的地方。例如,如果玩家掉进鲨鱼的陷阱,你可以给他们另一次机会。你还可以提供更多关于玩家当前位置的线索。想出一些点子来改进游戏,并尝试实施它们。打造一场值得杰克·斯派罗船长参与的冒险!
编程挑战
如果你遇到困难,可以查看 nostarch.com/smallbasic/,了解解决方案以及更多资源和针对教师与学生的复习问题。
-
Okla 是一位无畏的战士,以勇气和智慧著称。他现在正在一个闹鬼的城堡中执行一项高尚的任务,寻找四把钥匙,以解救被困在里面的小狗!但问题来了:这个闹鬼的城堡被邪恶的怪物守卫着,怪物会到处扔炸弹。每当这些炸弹击中 Okla 时,他会失去 10 点能量。你需要帮助 Okla 穿越城堡,在他失去所有能量之前找到四把钥匙。
![image]()
打开本章文件夹中的 Okla.sb 文件,并运行它来玩游戏。在玩完游戏并理解其运行方式后,想出一些改进的想法,并尝试实现它们。
-
打开本章文件夹中的 TicTacToe_Incomplete.sb 文件。这个游戏让你与计算机进行井字游戏对战。游戏的棋盘由一个名为
board的 3×3 矩阵表示。当玩家点击一个方格时,游戏会在该格子中绘制一个 X,并在其board元素中填入数字 1。接着,计算机会轮到自己并随机选择一个空格(计算机并不是那么聪明)。游戏会在计算机选定的格子中绘制一个 O,并在该board元素中填入数字 5。下图展示了游戏的运行方式。![image]()
你的任务是完成
CheckWinner()子程序,该子程序会在每次移动后被调用。你需要检查每一行、每一列和两个对角线的和。和为 3 表示玩家赢得了游戏。和为 15 表示计算机赢得了游戏。如果没有赢家并且已经进行了九次移动(棋盘已完全填满 X 和 O),则游戏为平局。
第十九章:18
高级文本魔法

虽然蓝天和绿地的图片比一屏文字更让人赏心悦目,但许多实用的程序,如 Facebook、Twitter 和 Words with Friends,都是处理文本的。这就是为什么 Small Basic 提供了 Text 对象来处理文本的原因。在本章中,你将学习如何使用 Text 对象来查找字符串的长度、提取字符串的一小部分,并执行许多其他高级的字符串处理任务。你还将编写自己的字符串处理子程序,并将所学应用于创建一些有趣的应用程序,比如猪拉丁语翻译器和文字拼图游戏!
文本对象
你在本书中一直在处理字符串。为了回顾一下,字符串 是由字符组成的序列,这些字符被双引号包围,例如 "stringY strinGy striNg strIng stRing"。这些字符可以包括字母(大写和小写)、数字(0 到 9)和其他键盘上的符号(如 +、–、&、@ 等)。你可以在程序中使用字符串来存储姓名、地址、电话号码、书名、星际迷航剧集的名称等。Text 对象包含许多有用的方法来处理字符串。
图 18-1 显示了 Text 对象方法的完整列表。我们将这些方法分成四组,在接下来的章节中进行讨论。

图 18-1: Text 对象的方法
追加字符串并获取它们的长度
结合字符串并查找它们的长度是编程中的常见任务。让我们来看一下 Text 对象如何帮助你完成这些任务。
追加字符串
Append() 方法可以将两个字符串连接(或 追加)在一起,如以下示例所示:
str = Text.Append("He-", "Man")
TextWindow.WriteLine(str) ' Displays: He-Man
在本书的早些时候,你学习了如何使用 + 符号连接字符串。但当你处理的文本被 + 符号当作数字来处理时,Append() 方法就显得尤为有用,如以下示例所示:
res = Text.Append("1", "5")
TextWindow.WriteLine(res) ' Output: 15 (1 followed by 5)
TextWindow.WriteLine("1" + "5") ' Output: 6
第一条语句将两个字符串("1" 和 "5")连接起来,并将结果赋值给变量 res(result 的缩写)。第二条语句的输出显示,字符串 "5" 被追加到了字符串 "1" 后面,得到一个新字符串 "15"。第三条语句显示,你不能使用 + 符号进行这种连接。+ 操作符将其两个操作数解释为数字(1 和 5),并将这两个数字相加,这就是为什么第三条语句显示 6。
在 Small Basic 中,使用 Append() 是连接数字的唯一方法。
获取字符串的长度
字符串中的字符数量就是它的长度。要查找字符串的长度,你可以使用 GetLength() 方法,如以下示例所示:
1 res = Text.GetLength("") ' res = 0 (empty string)
2 res = Text.GetLength("Careless Bears") ' res = 14 (the space counts!)
3 res = Text.GetLength(1023) ' res = 4
4 res = Text.GetLength(-101.5) ' res = 6
GetLength()将它的参数视为字符串,并返回该字符串中的字符数。第 1 行显示空字符串的长度为 0。第 2 行显示字符串"Careless Bears"的长度为 14,因为这个字符串包含 14 个字符(空格也算作字符)。第 3 行使用数字 1023 作为参数调用GetLength()。GetLength()将这个数字视为字符串("1023"),并返回4,表示该字符串的长度。第 4 行的类似过程适用于数字–101.5,GetLength()返回6(包括四个数字、负号和小数点)。
尝试一下 18-1
编写一个程序,提示用户输入一个形容词。让程序通过在输入后面附加ly来显示相应的副词。例如,如果用户输入mad,程序将显示madly。这个程序能适用于所有形容词吗?(提示:考虑以y结尾的形容词,如happy,或以ic结尾的形容词,如heroic。)
拆分字符串:子字符串
就像你可以将字符串连接起来创建更长的字符串一样,你也可以将字符串分割成更小的字符串,这些被称为子字符串。子字符串只是一个较大字符串的一部分。Text对象有六个方法可以让你处理子字符串。让我们来看一下这些方法。
IsSubText() 方法
你可以使用IsSubText()来判断一个字符串是否是另一个字符串的一部分。这个方法有两个参数:你想要搜索的字符串和你想要查找的子字符串。它会返回"True"或"False",取决于子字符串是否在源字符串中。以下是一些示例:
1 myString = "The quick brown fox"
2 res = Text.IsSubText(myString, "brown") ' res = "True"
3 res = Text.IsSubText(myString, "BROWN") ' res = "False"
4 res = Text.IsSubText(myString, "dog") ' res = "False"
正如这些示例所示,IsSubText()在搜索子字符串时是区分大小写的。这就是为什么在第 3 行搜索"BROWN"返回"False"。
EndsWith() 方法
使用EndsWith()来判断一个字符串是否以给定的子字符串结束。以下是一些示例:
1 myString = "The quick brown fox"
2 res = Text.EndsWith(myString, "fox") ' res = "True"
3 res = Text.EndsWith(myString, "x") ' res = "True"
4 res = Text.EndsWith(myString, "FOX") ' res = "False"
5 res = Text.EndsWith(myString, "dog") ' res = "False"
再次提醒,字符串的大小写很重要:在第 4 行搜索"FOX"返回"False"。
StartsWith() 方法
使用StartsWith()来判断一个字符串是否以给定的子字符串开始。以下是一些示例:
1 myString = "The quick brown fox"
2 res = Text.StartsWith(myString, "The") ' res = "True"
3 res = Text.StartsWith(myString, "T") ' res = "True"
4 res = Text.StartsWith(myString, "the") ' res = "False"
同样,在第 4 行中搜索"the"返回的是"False"。
GetSubText() 方法
要从字符串中的任何位置提取文本,你可以使用GetSubText()。这个方法有三个参数:你要从中获取子字符串的源字符串、子字符串的起始位置以及你想要的子字符串的长度。要理解这个方法如何工作,请参见图 18-2。

图 18-2:字符串中字符位置的示意图
第一个字符的位置是 1,第二个字符的位置是 2,以此类推。现在考虑以下示例:
1 myString = "The quick brown fox"
2 res = Text.GetSubText(myString, 1, 3) ' res = "The"
3 res = Text.GetSubText(myString, 0, 3) ' res = ""
4 res = Text.GetSubText(myString, 17, 3) ' res = "fox"
5 res = Text.GetSubText(myString, 17, 4) ' res = "fox"
第 2 行获取从位置 1 开始,长度为 3 的子字符串,返回字符串"The"。第 3 行未能获取从位置 0 开始的子字符串,因为第一个有效位置是 1,而它返回一个空字符串。第 4 行获取从位置 17 开始的三字母子字符串,返回"fox"。第 5 行请求从位置 17 开始的长度为 4 的子字符串。因为该子字符串超出了字符串的末尾,长度被截短,方法返回"fox",其长度为 3。
你可以在For循环中使用GetSubText()来访问字符串的每个字符。例如,以下代码将strIn的每个字符写在新的一行上。输入并运行这段代码,确保你理解它是如何工作的:
strIn = "Pirate squids hate hot dogs."
For N = 1 To Text.GetLength(strIn) ' For each character
ch = Text.GetSubText(strIn, N, 1) ' Gets the character at position N
TextWindow.WriteLine(ch) ' Displays it on a new line
EndFor
循环计数器N从 1 运行到字符串的末尾。每次迭代请求一个长度为 1(即一个字符)的子字符串,起始位置为N,并显示该字符。
GetSubTextToEnd() 方法
GetSubTextToEnd()方法与GetSubText()类似,不同之处在于它返回从某个位置到字符串结尾的子字符串。它接受两个参数:你希望从中提取子字符串的源字符串和子字符串的起始位置。这里有一些示例(请参考图 18-2 以获取上下文):
1 myString = "The quick brown fox"
2 res = Text.GetSubTextToEnd(myString, 13) ' res = "own fox"
3 res = Text.GetSubTextToEnd(myString, 19) ' res = "x"
4 res = Text.GetSubTextToEnd(myString, 20) ' res = ""
第 2 行获取从位置 17 开始的子字符串,返回"own fox"。第 3 行获取从位置 19 开始的子字符串,返回"x"。第 4 行请求从位置 20 开始的子字符串。由于源字符串只有 19 个字符,该方法返回空字符串。
GetIndexOf() 方法
你将要传递给GetIndexOf()方法的是你希望搜索的子字符串,它会返回该子字符串在源文本中的索引位置。以下是一些示例:
1 myString = "The quick brown fox"
2 res = Text.GetIndexOf(myString, "The") ' res = 1
3 res = Text.GetIndexOf(myString, "quick") ' res = 5
4 res = Text.GetIndexOf(myString, "QUICK") ' res = 0
5 res = Text.GetIndexOf(myString, "o") ' res = 13
6 res = Text.GetIndexOf(myString, "dog") ' res = 0
搜索是区分大小写的,因此第 4 行返回0,因为在源字符串中没有找到"QUICK"。第 5 行请求字母o的索引,但由于有两个o,它返回第一个找到的o的索引。最后一行返回0,因为它没有在源字符串中找到"dog"。
动手试试 18-2
一个名叫富兰克林·罗斯福的小男孩曾经把写给母亲的信签名倒着写:Tlevesoor Nilknarf。写一个程序,反向显示输入字符串的字符。(提示:从字符串的长度开始,循环计数直到 1,并使用GetSubText()提取每个字符。)
改变大小写
有时你可能想要将字符串显示为大写或小写字母。ConvertToLowerCase()和ConvertToUpperCase()方法可以帮助你做到这一点。运行清单 18-1 中的示例。
1 ' ChangeCase.sb
2 var1 = "Ewok"
3 lwrCase = Text.ConvertToLowerCase(var1) ' lwrCase = "ewok"
4 TextWindow.WriteLine(lwrCase) ' Displays: ewok
5 TextWindow.WriteLine(var1) ' Displays: Ewok
6 uprCase = Text.ConvertToUpperCase(var1) ' uprCase = "EWOK"
7 TextWindow.WriteLine(uprCase) ' Displays: EWOK
8 TextWindow.WriteLine(var1) ' Displays: Ewok
清单 18-1:改变字符串的大小写
第 3 行调用ConvertToLowerCase()方法返回小写字符串"ewok",并在第 4 行显示。第 5 行的语句显示原始字符串没有受到小写转换的影响;调用ConvertToLowerCase()返回一个全新的小写字符字符串。第 6 行的ConvertToUpperCase()方法返回大写版本的"EWOK",并在第 7 行显示。第 8 行也显示原始字符串未受转换的影响。
你可以使用这些方法来进行不区分大小写的字符串比较。例如,假设你的程序询问用户他们最喜欢的史瑞克角色。如果用户喜欢驴子,他们赢得 200 分;否则,他们赢得 100 分。用户可以输入donkey、DONKEY、Donkey、DOnkey,或任何其他大小写组合来回答问题。与其检查所有可能的组合,不如将用户的回答转换为大写(或小写),然后将结果与新的字符串DONKEY(或donkey,如果你使用小写的话)进行比较。在清单 18-2 中运行程序。
1 ' StringMatch.sb
2 While ("True")
3 TextWindow.Write("Who's your favorite Shrek character? ")
4 name = Text.ConvertToUpperCase(TextWindow.Read())
5 If (name = "DONKEY") Then
6 TextWindow.WriteLine("You won 200 ogre points!")
7 Else
8 TextWindow.WriteLine("You won 100 ogre points!")
9 EndIf
10 EndWhile
清单 18-2:不区分大小写的字符串匹配
第 4 行的Read()方法读取用户输入的文本。然后,用户的文本被转换为大写,并将结果存储在name变量中。请注意,我们直接将Read()方法作为ConvertToUpperCase()的参数;这相当于以下两行代码:
name = TextWindow.Read()
name = Text.ConvertToUpperCase(name)
第 5 行的If语句将用户输入的内容转换为大写,并与字面字符串"DONKEY"进行比较,并相应地奖励用户。
这是一个输出示例:
Who's your favorite Shrek character? dOnkey
You won 200 ogre points!
尝试一下 18-3
编写一个程序,向用户提出一个是/否问题,例如“你能用风的所有颜色来画画吗?”创建一个程序,接受 y、yes、n 或 no,无论大小写,作为有效答案。如果答案无效,要求用户重新输入。
Unicode 字符编码
所有计算机数据(包括文本)都以二进制序列的 0 和 1 存储。例如,字母A的二进制表示是 01000001。字符与其二进制表示之间的映射关系称为编码。
Unicode 是一种通用编码方案,允许你对来自多种语言的超过一百万个字符进行编码。每个字符都会分配一个唯一的数字(称为码点)。例如,字符A的码点是 65,美元符号($)的码点是 36。GetCharacterCode()方法返回字符的码点。但GetCharacter()方法正好相反;当你给它一个字符的码点时,它会返回相应的字符。
在清单 18-3 中运行该程序。
1 ' CharCode.sb
2 str = "ABab12"
3 For N = 1 To Text.GetLength(str)
4 ch = Text.GetSubText(str, N, 1) ' Gets the Nth character
5 code = Text.GetCharacterCode(ch) ' Gets its code point
6 TextWindow.WriteLine(ch + ": " + code) ' Displays ch and its code point
7 EndFor
清单 18-3:演示GetCharacterCode()方法
第 2 行定义了一个包含六个字符的字符串。第 3 行启动了一个 For 循环,访问这些字符中的每一个;GetLength() 设置了循环的上限。每次循环都会从字符串中读取一个字符并将其保存在名为 ch 的变量中(第 4 行)。接着,循环获取该字符的 Unicode 代码点并将其保存在 code 变量中(第 5 行)。第 6 行显示该字符及其代码点。当你运行此程序时,你将看到以下输出:
A: 65
B: 66
a: 97
b: 98
1: 49
2: 50
花式字符
让我们探索一些在英语中未使用的字符。清单 18-4 显示了一个简单的程序,它显示了 140 个 Unicode 字符的符号,从代码点为 9728 的字符开始。你可以更改这个数字以探索其他 Unicode 符号。
1 ' UnicodeDemo.sb
2 GraphicsWindow.BrushColor = "Black"
3 GraphicsWindow.FontSize = 30 ' Makes the font larger
4
5 code = 9728 ' Code point for the first symbol
6 xPos = 0 ' Horizontal position for drawing a symbol
7 yPos = 0 ' Vertical position for drawing a symbol
8 For row = 1 To 7 ' Draws 7 rows
9 xPos = 0 ' For each new row, start at the left edge
10 For col = 1 To 20 ' 20 columns for each row
11 ch = Text.GetCharacter(code) ' Gets a character
12 GraphicsWindow.DrawText(xPos, yPos, ch) ' Draws it
13 code = code + 1 ' Sets to next code point
14 xPos = xPos + 30 ' Leaves a horizontal space
15 EndFor
16 yPos = yPos + 30 ' Moves to the next row
17 EndFor
清单 18-4:演示 Unicode 字符
外部的 For 循环执行七次(第 8 行)。每次外部循环执行时,内部循环显示 20 个符号,这些符号之间相隔 30 像素(第 10–15 行)。绘制完整一行符号后,我们将垂直绘制位置下移 30 像素,以绘制下一行(第 16 行)。图 18-3 显示了该程序的输出。

图 18-3: UnicodeDemo.sb 的输出
更多关于代码点的内容
小写字母的 Unicode 代码点是从 97(a)到 122(z)的连续整数。同样,大写字母的代码点范围从 65(A)到 90(Z)。小写字母 a 的代码点大于大写字母 A 的代码点,且 a 和 A 之间的代码点差(97 - 65 = 32)与 b 和 B 之间的差(98 - 66 = 32)相同,依此类推。当给定一个小写字母的代码点,我们用 ch 来表示,它对应的大写字母的代码点为 65 + (ch – 97)。这是公式:
code for uppercase ch = code(A) + (code for lowercase ch – code(a))
现在你知道字符串中的每个字符都有一个代码点,你可以对字符串执行许多有用的操作。以下示例展示了你可以做的操作。
显示引号
假设你想在输出中显示字符串 "Bazinga",并包含双引号。如果你写 TextWindow.WriteLine("Bazinga"),Small Basic 会显示 Bazinga,但不包括引号,因为引号用来标识字符串的开始和结束。然而,如果你写 TextWindow.WriteLine(""Bazinga""),Small Basic 会返回语法错误。那么如何显示引号呢?通过使用引号的代码点,你可以将引号字符附加到字符串中,如下所示的代码片段所示:
QUO = Text.GetCharacter(34) ' Gets the double quotation mark
TextWindow.WriteLine(QUO + "Bazinga" + QUO) ' Output: "Bazinga"
第一个语句从其 Unicode 代码点(34)获取引号字符,并将其赋值给变量 QUO。第二个语句将字符串 "Bazinga" 插入到两个 QUO 字符之间,从而输出所需的结果。
创建多行字符串
你可以通过将 换行符 字符(代码点 10)嵌入字符串中来创建多行字符串。输入以下代码片段作为示例:
LF = Text.GetCharacter(10) ' Code for line feed
TextWindow.WriteLine("Line1" + LF + "Line2") ' Displays two lines
当你运行这段代码时,两个字符串 "Line1" 和 "Line2" 会显示在两行中。结果与使用以下两条语句时的输出完全相同:
TextWindow.WriteLine("Line1")
TextWindow.WriteLine("Line2")
通过你迄今为止获得的知识,你已经可以编写使用字符串进行各种复杂操作的完整程序了!
实践操作 18-4
以下程序显示英文字母。请解释该程序的工作原理。
For code = 65 To 90
ch = Text.GetCharacter(code)
TextWindow.WriteLine(ch)
EndFor
字符串的实际示例
早些时候,你学习了如何使用 GetLength() 获取字符串的长度,以及如何使用 GetSubText() 访问字符串中的单个字符。当你将这两个方法与 For 循环结合使用时,你可以计算特殊字符、检查多个字符,并进行其他几项有用的字符串操作。让我们来看一些示例!
计算特殊字符
清单 18-5 展示了一个计算字符串中元音字母数量的程序。它要求用户输入一个字符串,然后计算并显示该字符串中的元音字母数量。
1 ' VowelCount.sb
2 TextWindow.Write("Enter a sentence: ") ' Prompts the user for text
3 str = TextWindow.Read() ' Reads text entered by the user
4
5 count = 0 ' Sets vowel count to 0 (so far)
6 For N = 1 To Text.GetLength(str) ' Checks all characters
7 ch = Text.GetSubText(str, N, 1) ' Gets Nth character
8 ch = Text.ConvertToUpperCase(ch) ' Makes it uppercase
9 If ((ch = "A") Or (ch = "E") Or (ch = "I") Or (ch = "O") Or (ch = "U")) Then
10 count = count + 1 ' If it finds a vowel, increments count
11 EndIf
12 EndFor
13 TextWindow.Write("Your sentence contains [") ' Shows result
14 TextWindow.WriteLine(count + "] vowels.")
清单 18-5:计算字符串中元音字母的数量
在获取用户输入(第 2–3 行)后,程序将 count 变量初始化为 0,因为到目前为止还没有找到任何元音字母(第 5 行)。然后,程序启动一个循环,逐一检查输入字符串中的字符(第 6 行)。循环计数器 N 指向字符串中的第 N 个字符。
第 7 行使用 GetSubText() 获取输入字符串的第 N 个字符,并将其赋值给变量 ch(字符的简称)。接着,代码将字符转换为大写字母(第 8 行),并将这个大写字母与元音字母进行比较(第 9 行)。如果字符是元音字母,count 的值就增加 1(第 10 行)。当循环结束时,程序会显示计算出的元音字母数量(第 13–14 行)。以下是该程序的示例输出:
Enter a sentence: Small Basic is fun
Your sentence contains [5] vowels.
Enter a sentence: Giants leave nasty diapers.
Your sentence contains [9] vowels.
实践操作 18-5
将清单 18-5 中的代码转换成一个两人游戏。第一个玩家输入一个单词,第二个玩家需要猜出单词中元音字母的数量。然后,玩家轮流进行。每次猜对,玩家得一分。游戏进行 10 轮后结束,并显示获胜者。
回文数字检查器
在这一节中,我们将编写一个程序,检查用户输入的整数是否为回文。回文是指正着读和反着读都一样的数字、单词或短语。例如,1234321 和 1122332211 都是回文。同样,racecar、Hannah 和 Bob 也是回文。
让我们看看在图 18-4 中显示的输入数字 12344321。

图 18-4:使用两个变量检查一个数字是否是回文
要检查这个数字是否是回文数,你需要比较第一个和第八个数字,第二个和第七个数字,第三个和第六个数字,依此类推。如果在比较中有任何两个数字不相等,则该数字不是回文数。如图所示,你可以通过使用两个变量(pos1和pos2)来访问你想要比较的数字,这两个变量分别朝相反的方向移动。第一个变量(pos1)从第一个数字开始并向前移动,第二个变量(pos2)从最后一个数字开始并向后移动。所需比较的次数最多为输入数字中数字个数的一半。在这个例子中,你最多需要进行四次比较,因为输入的数字有八个数字。如果输入的整数数字位数为奇数,则中间的数字无需比较,因为它不影响回文数的判断。
清单 18-6 展示了完整的程序。注释应该能帮助你理解程序是如何工作的。
1 ' Palindrome.sb
2 Again:
3 TextWindow.WriteLine("")
4 TextWindow.Write("Enter a number: ")
5 ans = TextWindow.ReadNumber() ' Saves user's input in ans
6
7 length = Text.GetLength(ans) ' Number of digits of input number
8 pos1 = 1 ' Sets pos1 to read first digit
9 pos2 = length ' Sets pos2 to read last digit
10 For N = 1 To (length / 2) ' Performs (length/2) comparisons
11 ch1 = Text.GetSubText(ans, pos1, 1) ' Reads digit at position pos1
12 ch2 = Text.GetSubText(ans, pos2, 1) ' Reads digit at position pos2
13 If (ch1 <> ch2) Then ' If not equal, no need to continue
14 TextWindow.WriteLine(ans + " isn't a palindrome.") ' Shows result
15 Goto Again
16 EndIf
17 EndFor
18
19 TextWindow.WriteLine(ans + " is a palindrome.")
20 Goto Again
清单 18-6:测试用户输入的数字是否为回文数
这是程序的一次示例运行:
Enter a number: 1234321
1234321 is a palindrome.
Enter a number: 12345678
12345678 isn't a palindrome.
试试这个 18-6
另一种创建清单 18-6 中程序的方法是反转输入字符串,然后将反转后的字符串与原始字符串进行比较。使用这种方法创建一个新的回文检查程序。
猪拉丁语
让我们教计算机一种语言游戏,叫做 猪拉丁语。创建猪拉丁语单词的规则很简单。要将一个单词转换成猪拉丁语,将第一个字母移到单词的末尾,并在后面加上字母 ay。所以,单词 talk 变成 alktay,fun 变成 unfay,以此类推。你能解密这一节的原始标题吗?
图 18-5 展示了将一个单词转换为猪拉丁语时使用的策略,使用的是单词 basic。

图 18-5:将英文单词翻译成猪拉丁语
你首先从第二个字符到末尾提取子字符串,并将其赋值给输出字符串。然后,你将输入字符串的第一个字母加到输出字符串中,后跟 ay。在清单 18-7 中输入代码以实现这些步骤。
1 ' PigLatin.sb
2 TextWindow.Title = "Pig Latin"
3
4 While ("True")
5 TextWindow.Write("Enter a word: ")
6 word = TextWindow.Read()
7
8 pigLatin = Text.GetSubTextToEnd(word, 2) ' Gets characters 2 to end
9 pigLatin = pigLatin + Text.GetSubText(word, 1, 1) ' Appends first character
10 pigLatin = pigLatin + "ay" ' Appends "ay"
11 TextWindow.WriteLine(pigLatin) ' Displays the output
12 TextWindow.WriteLine("")
13 EndWhile
清单 18-7:将用户输入的单词转换为猪拉丁语
该程序运行一个无限循环,允许用户尝试不同的单词(第 4 行)。在读取用户输入的单词后(第 6 行),我们提取从位置 2 开始的子字符串(即从第二个字符到输入单词的末尾),并将其赋值给pigLatin。然后我们从word中提取第一个字母并将其附加到pigLatin(第 9 行),接着加上ay(第 10 行)。我们显示猪拉丁语单词(第 11 行),然后是一个空行(第 12 行),并进入下一轮。Ongratulationscay! Ouyay inishedfay ouryay rogrampay!
试试这个 18-7
编写一个程序,接受一个猪拉丁语单词作为输入并显示其原始的英文单词。
修正我的拼写
现在,我们将开发一个游戏,展示拼错的单词并要求玩家输入正确的拼写。该游戏通过在一个英文单词中的随机位置插入一个随机字母来生成拼错的单词。拼错的简单单词可能有多个正确拼写。例如,如果游戏显示mwall,那么mall或wall都可以是正确的。为了简化游戏,我们将忽略这种可能性,并坚持使用某种特定拼写作为正确答案。
首先,我们从一个预定义的单词数组中选择一个要拼错的单词,并将选中的单词保存在名为strIn的变量中。然后,我们随机挑选一个字符randChar插入到strIn中。插入的位置charPos是strIn长度之间的一个随机数,范围从 1 到strIn的长度。图 18-6 展示了生成拼错单词hewlp的过程。

图 18-6:展示生成拼错单词的过程
我们首先提取从第 1 个字母到charPos – 1位置的子字符串并将其赋值给strOut(因为charPos是3,所以strOut = "he")。然后我们将randChar追加到strOut(这使得strOut = "hew")。接着,我们提取从charPos到末尾的子字符串(在这个例子中是"lp"),并将其追加到strOut(这使得strOut = "hewlp")。清单 18-8 展示了完整的程序。确保从本章的文件夹中下载并打开FixMySpelling.sb,以查看我们为此程序编写的单词列表。
1 ' FixMySpelling.sb
2 words = "1=mountain;2=valley;...;22=animation;" ' See file for full list
3
4 While ("True") ' Runs forever
5 strIn = words[Math.GetRandomNumber(Array.GetItemCount(words))]
6 randChar = Text.GetCharacter(96 + Math.GetRandomNumber(26))
7 charPos = Math.GetRandomNumber(Text.GetLength(strIn))
8
9 strOut = Text.GetSubText(strIn, 1, charPos - 1)
10 strOut = strOut + randChar
11 strOut = strOut + Text.GetSubTextToEnd(strIn, charPos)
12
13 TextWindow.Write("Enter correct spelling for [" + strOut + "]: ")
14 ans = TextWindow.Read()
15 ans = Text.ConvertToLowerCase(ans)
16 If (ans = strIn) Then
17 TextWindow.WriteLine("Good Job!")
18 Else
19 TextWindow.WriteLine("Incorrect. It is " + strIn + ".")
20 EndIf
21 TextWindow.WriteLine("")
22 EndWhile
清单 18-8:创建拼错单词并要求玩家修正它们
words数组包含了这个游戏的单词(第 2 行)。程序从words数组中随机选取一个单词并将该单词保存为strIn(第 5 行)。注意我们如何使用数组项的数量来设置随机数的上限。接着,程序从字母表中随机选择一个字母randChar(第 6 行)。它通过获取 1 到 26 之间的随机数并加上 96 来实现;这样你会得到一个在 97(字母 a 的代码点)和 122(字母 z 的代码点)之间的随机数。然后,程序在strIn中选择一个随机位置charPos(第 7 行):这是插入随机字符的位置。接着,程序创建拼错的单词并将其存储在strOut中(第 9 到 11 行)。
在第 13 行,程序要求玩家输入正确的拼写。它读取用户的回答(第 14 行)并将其转换为小写(第 15 行)。然后,它将答案与正确的单词进行比较(第 16 行)。如果玩家的答案与原始单词匹配,游戏会显示Good Job!(第 17 行)。否则,游戏会显示错误信息并显示正确的拼写(第 19 行)。无论哪种情况,程序最终会通过显示空行(第 21 行)结束,并且循环会重复,以便给用户一个新的拼写错误的单词。
下面是该程序的一个示例运行:
Enter correct spelling for [mairror]: miror
Incorrect. It is mirror.
Enter correct spelling for [inteorface]: interface
Good Job!
动手试试 18-8
更新程序 清单 18-8,使拼写错误的单词包含两个额外的随机字母,而不仅仅是一个随机字母。同时,向列表中添加更多的单词,以增加多样性。
重新排列
现在我们将创建一个字母打乱游戏。程序从一个英文单词开始,打乱字母,展示打乱后的单词给玩家,并要求他们猜测原始单词。
清单 18-9 显示了程序的主要部分。打开本章文件夹中的 Unscramble.sb 查看完整的单词列表。
1 ' Unscramble.sb
2 words = "1=mountain;2=valley;...;22=animation;" ' See file for full list
3
4 While ("True")
5 strIn = words[Math.GetRandomNumber(Array.GetItemCount(words))]
6 Scramble() ' Returns strOut (a scrambled version of strIn)
7
8 TextWindow.Write("Unscramble [" + strOut + "]: ")
9 ans = TextWindow.Read()
10 ans = Text.ConvertToLowerCase(ans)
11
12 If (ans = strIn) Then
13 TextWindow.WriteLine("Good Job!")
14 Else
15 TextWindow.WriteLine("No. It is " + strIn + ".")
16 EndIf
17 TextWindow.WriteLine("")
18 EndWhile
清单 18-9:打乱单词并要求玩家重新排列
words 数组包含了这个游戏的单词(第 2 行)。程序随机从这个数组中选择一个单词,并将其保存为 strIn(第 5 行)。然后,它调用 Scramble() 来生成 strOut,这是 strIn 的一个打乱版本(第 6 行):稍后我们将添加 Scramble() 子程序。接下来,程序要求玩家将 strOut 重新排列(第 8 行)。它读取玩家的答案(第 9 行),并将其转换为小写(第 10 行)。然后,它将玩家的答案与正确的单词进行比较(第 12 行)。如果玩家的答案与原始单词匹配,游戏会显示 Good Job!(第 13 行)。否则,游戏会显示正确的单词(第 15 行)。无论哪种情况,程序最后通过显示一个空行来分隔回合(第 17 行),然后循环重复。
现在我们来看一下 Scramble() 子程序,它将字符串的字符打乱成一个随机顺序。调用者设置输入字符串(strIn),然后子程序返回一个新的字符串(strOut),它包含了被打乱的 strIn 字符。清单 18-10 显示了这个子程序。
1 Sub Scramble ' Scramble subroutine
2 len = Text.GetLength(strIn)
3 For N = 1 To len ' Loops up to length of word
4 char[N] = Text.GetSubText(strIn, N, 1) ' Saves each letter into an array
5 EndFor
6
7 strout = "" ' Empties the output string
8 While (Text.GetLength(strout)< len)
9 pos = Math.GetRandomNumber(len) ' Picks where to place the letter
10 If (char[pos] <> "") Then
11 strout = strout + char[pos] ' Adds in the extra letter
12 char[pos] = "" ' Empties the element
13 EndIf
14 EndWhile
15 EndSub
清单 18-10:字母打乱子程序
该子程序将输入字符串的长度保存到 len(第 2 行)。然后,它使用 For 循环将 strIn 的每个字母保存到一个名为 char 的数组中(第 3 至 5 行)。它清空输出字符串 strOut,并开始一个 While 循环按字母逐个构建 strOut(第 7 至 14 行)。While 循环一直运行,直到 strOut 的长度与 strIn 相同(这意味着我们已经添加了所有 strIn 的字母)。循环的每次迭代从 char 数组中随机选择一个元素(第 9 行)。如果该元素为空,我们会再次循环选择另一个。如果不是,我们将选择的字母附加到 strOut(第 11 行),并清空该元素以表示我们已经使用过它(防止再次使用),见第 12 行。Ouy fishendi eth egma!
这是该程序的一个示例运行:
Unscramble [lalvey]: lovely
No. It is valley.
尝试一下 18-9
尝试使用你在之前章节中学到的技能更新这个字母打乱游戏。让游戏持续进行 10 回合,然后显示用户的得分:在 10 个回合中,有多少个单词被正确地重新排列?接下来,添加 28 个新的单词进行打乱,这样你就有 50 个单词了。然后将游戏展示给你的朋友,看看谁能获得最高分!
押韵时间:杰克建造的房子
让我们以一个展示英国经典儿歌和累积故事的程序来结束这一章。在累积故事中,某个动作会随着故事的进展重复并逐步增加。图 18-7 展示了这个程序的进展;每当用户点击“下一页”按钮时,会出现更多的押韵行。

图 18-7:杰克建造的房子押韵故事
仔细检查这个押韵故事,你会注意到故事页面中有一些共同的字符串。研究图 18-8,了解如何通过在每个阶段附加短字符串来创建这个押韵故事。

图 18-8:构成押韵故事的字符串
例如,我们来跟踪这个图中的第三行。跟随第三个箭头,你将得到以下内容:
This is the Rat,
That ate
当你继续跟随第二个箭头时,你会得到以下内容:
This is the Rat,
That ate the Malt,
That lay in
而当你跟随第一个箭头时,你将看到完整的押韵故事,这将在第三页显示:
This is the Rat,
That ate the Malt,
That lay in the House that Jack built.
打开本章文件夹中的JackHouse_Incomplete.sb文件。该文件包含清单 18-11 中的主程序和一个OnButtonClicked()子程序的占位符,我们稍后会添加这个子程序。文件夹中还包含了 11 个背景图片(Page1.png、Page2.png、...、Page11.png),这些图片将展示在每一页的押韵故事中。
1 ' JackHouse.sb
2 GraphicsWindow.Title = "The House That Jack Built"
3 GraphicsWindow.CanResize = "False"
4 GraphicsWindow.Width = 480
5 GraphicsWindow.Height = 360
6 GraphicsWindow.FontBold = "False"
7 GraphicsWindow.FontSize = 20
8 GraphicsWindow.FontName = "Times New Roman"
9
10 LF = Text.GetCharacter(10) ' Code for line feed
11
12 rhyme[1] = "the Farmer who sowed the corn," + LF + "That fed "
13 rhyme[2] = "the Cock that crowed in the morn," + LF + "That waked "
14 rhyme[3] = "the Priest all shaven and shorn," + LF + "That married "
15 rhyme[4] = "the Man all tattered and torn," + LF + "That kissed "
16 rhyme[5] = "the Maiden all forlorn," + LF + "That milked "
17 rhyme[6] = "the Cow with the crumpled horn," + LF + "That tossed "
18 rhyme[7] = "the Dog," + LF + "That worried "
19 rhyme[8] = "the Cat," + LF + "That killed "
20 rhyme[9] = "the Rat," + LF + "That ate "
21 rhyme[10] = "the Malt," + LF + "That lay in "
22 rhyme[11] = "the House that Jack built."
23
24 Controls.AddButton("Next", 420, 320)
25 Controls.ButtonClicked = OnButtonClicked
26 nextLine = 11
27 OnButtonClicked()
清单 18-11:杰克建造的房子程序的主要部分
第 2 到第 8 行设置了GraphicsWindow对象。第 10 行定义了换行符(用于在字符串中附加新行)。第 12 到第 22 行定义了rhyme数组,该数组包含了这个押韵故事的字符串。注意这个数组的元素是如何与图 18-8 中的框框对应的。第 24 行创建了“下一页”按钮,第 25 行注册了ButtonClicked事件的处理程序。接着,nextLine变量被设置为 11,指向rhyme数组的第 11 个元素,这个元素是故事的第一页(第 26 行),然后调用OnButtonClicked()来展示押韵故事的第一页(第 27 行)。
现在我们将在清单 18-12 中添加OnButtonClicked()子程序。这个子程序会在用户点击“下一页”按钮时被调用。
1 Sub OnButtonClicked
2 img = Program.Directory + "\Page" + (12 - nextLine) + ".png"
3 GraphicsWindow.DrawImage(img, 0, 0)
4
5 strOut = "This is "
6 For N = nextLine To 11
7 strOut = Text.Append(strOut, rhyme[N])
8 EndFor
9 GraphicsWindow.DrawText(10, 10, strOut)
10
11 nextLine = nextLine - 1
12 If (nextLine = 0) Then
13 nextLine = 11
14 EndIf
15 EndSub
清单 18-12:OnButtonClicked()子程序
第 2 行将img填充为当前韵文页面的图像名称。当nextLine为 11 时,我们显示Page1.png(即 12 减去 11)。当nextLine为 10 时,我们显示Page2.png(12 减去 10),当nextLine为 9 时,我们显示Page3.png(12 减去 9),依此类推。第 3 行在图形窗口中绘制图像。然后我们构建输出字符串(第 5 到第 8 行)。我们将strOut设置为"This is "(第 5 行),然后开始一个从nextLine到 11 的循环(第 6 到第 8 行)。当nextLine为 11 时,循环运行一次,并将rhyme[11]附加到strOut。当nextLine为 10 时,循环从 10 到 11 运行,并将rhyme[10]和rhyme[11]附加到strOut。类似地,当nextLine为 9 时,循环从 9 到 11 运行,并将rhyme[9]、rhyme[10]和rhyme[11]附加到strOut。
当循环结束时,strOut包含此时故事韵文的整个字符串。我们在第 9 行使用DrawText()来显示这个字符串。
然后我们将nextLine减 1,指向rhyme数组中的前一个元素(第 11 行)。如果nextLine变为 0(第 12 行),说明故事结束,我们将其重置为 11,以便重新开始(第 13 行)。因此,当用户在故事的最后一页点击“下一页”按钮时,程序会返回显示第一页。我们在故事还不无聊之前就完成了它!
尝试一下 18-10
使用你在《Jack 所建的房子》示例中学到的技巧,编写一个程序讲述你最喜欢的故事。如果没有最喜欢的故事?那就编一个关于外星老鼠被困在一座塔中,身边只有明胶、弹弓和高级化学实验套件的故事。解释一下这只老鼠是如何被困的,以及它是如何逃脱的!
编程挑战
如果你卡住了,可以查看* nostarch.com/smallbasic/*,那里有解决方案以及更多的资源和针对教师和学生的复习题。
-
打开本章文件夹中的Shoot_Incomplete.sb文件。运行程序以查看以下界面。
![image]()
这个游戏的目标是估算乌龟与目标之间的转角和移动距离。当玩家输入他们的输入时,它会保存在一个名为
strIn的变量中。你的任务是将strIn分成两部分:将逗号前的子字符串赋值给angle,将逗号后的子字符串赋值给dist。文件中的注释告诉你在哪里添加代码。如果你卡住了,可以查看Shoot.sb文件,其中包含完成的程序。 -
打开本章文件夹中的BinaryToDecimal_Incomplete.sb文件。此程序将二进制数字转换为十进制数字,然后要求用户输入一个 8 位的二进制数。接着,它会在图形窗口中显示输入的数字,计算其十进制数,并显示转换结果,如下图所示。
![image]()
完成
GetInput()子程序,提示用户输入一个 8 位二进制数。你需要验证用户输入的内容不能为空,并且最多只能包含八个二进制数字(即只能包含 1 和 0)。当用户输入有效时,将其保存在strIn中,并从子程序返回。文件中的注释会告诉你该怎么做。如果遇到困难,可以参考文件BinaryToDecimal.sb,其中包含已完成的代码。
第二十章:19
接收文件输入与输出

你在本书中写的程序到目前为止都是从键盘获取输入,并将输出显示在屏幕上。但是,如果你想创建一个虚拟电话簿并在程序中使用成千上万行数据呢?处理这么多数据可能会使你很难编写和维护程序。每次运行程序时,你都得输入每个名字和电话号码!
幸运的是,程序不仅可以从文件接收输入,还可以将输出发送到文件,这些文件都可以保存在你的计算机上。所以,所有的电话簿信息都可以整齐地保存在一个文件中,你只需输入一次数据。如今,许多程序都处理保存在文件中的数据。
在大多数编程语言中,处理文件是一个高级话题,但 Small Basic 使得文件处理变得非常简单。在本章中,你将学习 File 对象以及它是如何让文件处理变得轻松的!
文件的重要性
在你的程序中,你已经使用了变量和数组来存储数据。但是,存储在变量和数组中的数据是临时的:当程序结束或你关闭计算机时,所有数据都会丢失。当你再次运行程序时,它不会记得你上次输入的数据。如果你想永久存储程序中创建的数据,你需要将这些数据保存在文件中。存储在文件中的数据被称为持久数据,因为即使在你关闭计算机后它仍然保留。这就像松鼠储存橡果一样持久。
文件提供了一种方便的方式来处理大量数据。如果你的程序需要大量数据(比如你朋友的名字),你不可能每次都要求用户手动输入这些数据。很可能,他们会因此感到烦躁并停止使用程序。如果程序可以从文件中读取输入数据,用户就不需要手动输入数据,并且可能会多次运行程序。当程序使用文件时,用户甚至可以通过更改数据文件来定制应用程序。例如,如果你写了一个拼写游戏,它从文件中读取输入,用户可以通过更改输入文件来设置游戏的难度。例如,他们可以使用短单词来进行简单的游戏,用长单词来进行更困难的游戏。
从文件中获取数据叫做读取文件,程序读取的文件通常称为输入文件。类似地,将数据写入文件叫做写入文件,程序写入(或创建)的文件称为输出文件。将数据存储到文件(并从文件中读取数据)叫做文件访问。处理文件被称为文件 I/O,即输入/输出的缩写。
在我们开始在程序中处理文件之前,先来看看文件名以及文件是如何保存在计算机上的。
文件命名
当你创建一个新文件时,你为它起一个名字。你可以随便命名,比如Fred或DontOpenMe,但通常最好更具体一些,比如myFriends或myLoveStory。
Windows 操作系统对文件名大小写不敏感,因此在文件名中大写字母和小写字母没有区别,所以myFile、Myfile和MYFILE都会指向同一个文件。Windows 还支持由句点分隔的两部分文件名,例如myFile.dat。句点后面的部分(在这个例子中是dat)叫做文件扩展名。文件扩展名通常指示文件类型(例如照片或文本文件)。表 19-1 列出了常见的文件扩展名及其含义。文件扩展名通常由你使用的程序自动添加。例如,Small Basic IDE 会为源代码文件添加.sb扩展名。
表 19-1: 常见文件扩展名
| 扩展名 | 文件类型 | 用途 |
|---|---|---|
| .dat | 一般数据文件 | 存储特定应用程序的信息 |
| .exe | 可执行文件 | 应用程序 |
| .gif | 图形交换格式 | 网站图片 |
| .html | 超文本标记语言网站文件 | 网页 |
| .jpg | 使用 JPEG 标准编码的图像 | 数码相机拍摄的照片 |
| .mp3 | 采用 MPEG 层 3 音频格式编码的音乐 | 音频文件 |
| 可携带文档格式文件用于阅读 | 电子书 | |
| .txt | 一般文本文件 | 你可能在记事本中写的笔记 |
本章将使用文本(.txt)文件。
文件组织
想象一下,数十本书被组织在一个有多个架子的柜子里。每个架子都有不同的标签(例如科学、数学、小说、苏斯博士等),并且架子上放满了该类别的书籍。每个架子就像一个容器,将相关的书籍组合在一起。类似地,计算机上的文件存储在称为目录(或文件夹)的容器中。一个目录可以包含文件以及其他目录。一个目录中的目录被称为子目录。
文件系统是操作系统的一部分,负责组织计算机上的文件和目录,并提供管理它们的方法。当你从 Small Basic 程序调用与文件相关的方法(如创建、删除、读取或写入文件)时,操作系统的文件系统会处理所有底层细节,因此你不需要担心文件是存储在硬盘、闪存、CD、DVD 等介质上的。Small Basic 库与操作系统交互,以访问存储在各种介质上的文件,正如图 19-1 所示。

图 19-1:文件系统如何让你访问不同介质上的文件
文件系统具有树形结构,如图 19-2 所示。树的顶部称为根目录(在此图中是驱动器字母D:)。根目录下有许多文件和其他目录。这些目录中可能包含其他文件和子目录。
你可以通过从根目录沿着树形结构查找路径,直到到达目标文件,从而找到任何文件。你遵循的目录序列构成了该文件的路径名。例如,要查找图 19-2 中的最后一个文件,你需要首先在根目录D:中查找,然后进入Book目录,再进入Chapter03目录以定位该文件。如果你使用反斜杠(\)将每个目录分隔开来,那么路径名就是D:\Book\Chapter03\Ch03.docx。你可以通过路径名定位系统上的每一个文件。

图 19-2:文件系统的树形结构
要从一个 Small Basic 程序访问文件,你需要指定文件的路径名。要了解如何做,查看图 19-2 中的可执行文件Test.exe。当你运行这个文件时,运行中的程序知道它的当前目录(在这个例子中是D:\Book\Chapter01\Examples)。如果你希望Test.exe访问一个数据文件(比如Test1.dat或Test2.dat),你需要指定路径名——从根目录开始,程序需要导航的文件夹序列,直到到达目标文件。这也叫做绝对路径。图 19-2 中,Test1.dat的绝对路径是D:\Book\Chapter01\Examples\Test1.dat,而Test2.dat的绝对路径是D:\Book\Chapter01\Examples\Data\Test2.dat。
如果你编写的程序只有你自己使用,你可以将该程序需要的所有数据文件保存在任何你喜欢的地方,并使用在程序中硬编码的绝对路径访问这些文件。例如,你可以这样写:
str = File.ReadContents("D:\Book\Chapter01\Examples\Test1.dat")
但是,如果你将这个程序交给朋友试用,除非你的朋友拥有与您相同的文件树,否则程序会失败。一个更好的解决方案是在程序运行时动态构建所需的路径,如下所示:
path = Program.Directory
str = File.ReadContents(path + "\Test1.dat")
现在程序将会将Test1.dat添加到当前目录的末尾,这意味着它会在程序所在的同一文件夹中查找Test1.dat。然后,你的朋友只需要将Test.exe和Test1.dat放在同一个文件夹中;绝对路径就不再重要。你可以将程序的文件夹压缩成 ZIP 文件(右键点击文件夹,选择“发送到”,然后点击“压缩(zipped)文件夹”),然后将 ZIP 文件发送给你的朋友。你的朋友可以将 ZIP 文件中的文件保存到C:*、D:*、C:\Temp或任何他们选择的文件夹中,你的程序依然能按设计正常运行。
通过理解文件和路径名,你已经准备好学习File对象及其方法,如何从文件中读取数据、向文件写入数据,并执行其他文件管理操作。让我们开始学习单文件操作吧!
文件对象
Small Basic 的File对象包括所有处理文件数据读取和写入、删除和复制文件以及列出目录内容的方法。由于此对象支持许多方法,因此本节分为两部分。首先,我们将探索与读取和写入文件相关的方法。其次,我们将看看与文件管理相关的方法。
文件输入输出方法
File对象中最常用的方法是那些用于将数据写入文件和从文件中读取数据的方法。让我们详细了解这些方法。
首先,打开记事本并在编辑器中输入一些单词,使其看起来像图 19-3。确保在最后一行之后不要按 ENTER 键。

图 19-3:一个示例文本文件
将文件保存为Test1.txt,并放在C:\Temp目录下,这样它的绝对路径就是C:\Temp\Test1.txt。如果不想创建文件,你可以在本章的文件夹中找到它,直接将其复制到C:\Temp。
从文件中读取
现在让我们尝试读取Test1.txt的内容。你可以使用File对象的ReadContents()方法一次性读取文件的全部内容。此方法打开文件,读取文件,并将其全部内容作为字符串返回。输入并运行 Listing 19-1 中的程序,查看该方法是如何工作的。
1 ' ReadContentsDemo.sb
2 path = "C:\Temp\Test1.txt"
3 str = File.ReadContents(path)
4 len = Text.GetLength(str)
5 TextWindow.WriteLine(str)
6 TextWindow.WriteLine("This file has " + len + " characters.")
Listing 19-1:演示 ReadContents() 方法
这是该程序的输出:
This
is a
Test.
This file has 19 characters.
第 2 行设置了文件的绝对路径。第 3 行读取文件的全部内容,并使用ReadContents()方法将返回的字符串保存到名为str的变量中。ReadContents()方法接受一个参数:要读取的文件的路径名。第 4 行获取字符串的长度,并将其保存到名为len的变量中。第 5 行和第 6 行显示了str和len变量的值。
但是,为什么GetLength()方法会输出字符串长度为 19,尽管字符串 "This is a Test." 只有 15 个字符呢?要理解发生了什么,你需要检查构成str变量的实际字符。记住,第十八章提到字符是以某种格式(如 ASCII 或 Unicode)编码的。将以下代码添加到 Listing 19-1 的末尾,再次运行程序:
For N = 1 To len
ch = Text.GetSubText(str, N, 1) ' Gets one character
code = Text.GetCharacterCode(ch) ' Gets the code for this character
TextWindow.WriteLine(code) ' Displays it
EndFor
这段代码显示了str变量包含 19 个字符。图 19-4 详细解释了程序的工作原理。

图 19-4:在 Listing 19-1 中的 str 变量的 19 个字符
记事本插入了两个特殊字符(称为回车符和换行符,它们的 ASCII 码分别是 13 和 10)来标记每行的结束。可以将换行符(或行尾标记)看作是按下键盘上的 ENTER 键时产生的字符对。没有这些字符,文件中的所有行会连在一起,成为一长行。换行符是控制字符;它们只控制光标在屏幕或打印机上的位置。
ReadContents() 方法将文件的所有内容作为一个字符串返回,包括文件中各行之间的换行符。
写入文件
WriteContents() 方法让你将程序中字符串的内容保存到你选择的文件中。如果你想创建多行文本,你需要手动插入换行符。例如,让我们编写一个程序,从键盘读取文本输入并将其写回到文件。该程序在 清单 19-2 中显示。
1 ' WriteContentsDemo.sb
2 CR = Text.GetCharacter(13) ' Code for carriage return
3 LF = Text.GetCharacter(10) ' Code for line feed
4 outFile = "C:\Temp\Out.txt" ' Absolute path of output file
5
6 strOut = "" ' Text to be written to file
7 strIn = "" ' One line (read from the user)
8 While(strIn <> "exit") ' Until user enters exit
9 TextWindow.Write("Data (exit to end): ") ' Prompts for text
10 strIn = TextWindow.Read() ' Reads line
11 If (strIn <> "exit") Then ' If user didn't enter exit
12 strOut = strOut + strIn + CR + LF ' Appends text to strOut
13 EndIf
14 EndWhile
15
16 File.WriteContents(outFile, strOut) ' Writes strOut to file
清单 19-2:演示 WriteContents() 方法
这是该程序的一个示例运行,显示了用户输入:
Data (exit to end): If Peter Piper picked a peck of pickled peppers,
Data (exit to end): Where's the peck of pickled peppers? I'm hungry.
Data (exit to end): exit
现在,在记事本中打开输出文件 C:\Temp\Out.txt 并检查其内容。该文件包含了用户在文本窗口中输入的内容。很酷吧?你在没有使用记事本的情况下就写下了这些文字!
程序的工作方式如下。我们在第 2 到第 3 行定义回车符和换行符,并在第 4 行定义输出文件的路径。接下来我们开始一个循环来获取用户的文本(第 8 到第 14 行)。在每次循环迭代中,我们提示用户输入他们想要的任何文本(第 9 行),并将输入的文本读取到名为 strIn 的变量中(第 10 行)。如果用户输入的文本不是 exit(第 11 行),我们将该文本以及回车符和换行符附加到 strOut 字符串中(第 12 行)。当用户输入 exit 时,循环结束,我们调用 WriteContents() 将 strOut 写入输出文件(第 16 行)。如果文件不存在,WriteContents() 会自动创建它。如果文件已存在,WriteContents() 会用 strOut 中的内容覆盖文件的原有内容。
尝试一下 19-1
编写一个程序,读取输入文本文件,将文本转换为小写字母,然后将结果保存到新的输出文件中。
检查错误
与处理用户输入时一样,你无法控制用户在你的程序读取的文件中保存的内容。由于人为错误,文件中的数据有时可能是错误的。许多事情可能出错(如你稍后将看到的),因此你的程序需要准备好处理这些错误。
幸运的是,Small Basic 总是做好了准备!调用 WriteContents() 会自动返回 "SUCCESS" 或 "FAILED",取决于操作是否成功。一个写得好的程序会检查返回的字符串,并在失败时采取相应的措施。让我们更新 清单 19-2,检查 WriteContents() 的返回值。将第 16 行的语句替换为 清单 19-3 中的代码。
1 result = File.WriteContents(outFile, strOut) ' Writes strOut to file
2 If (result = "SUCCESS") Then
3 TextWindow.WriteLine("Output saved to: " + outFile)
4 Else
5 TextWindow.WriteLine("Failed to write to: " + outFile)
6 TextWindow.WriteLine(File.LastError)
7 EndIf
清单 19-3:检查 WriteContents() 的返回值
首先,我们将 WriteContents() 的返回值保存在名为 result 的变量中(第 1 行),然后检查该方法的返回值。如果方法成功(第 2 行),我们会通知用户输出已成功保存(第 3 行)。如果操作失败(第 4 行),我们会告诉用户程序未能写入输出文件(第 5 行),并使用 File 对象的 LastError 属性显示失败的原因(第 6 行)。如果写入文件失败,WriteContents() 会自动更新该属性。
在编写代码处理失败情况后,我们需要通过故意使其失败来测试代码。以下是一些可能导致 WriteContents() 失败的情况:
-
输出文件的路径不存在。
-
输出文件已经在另一个程序中打开。
-
没有足够的空间保存文件。
让我们尝试第一个可能性,看看会发生什么。
路径不存在
运行 Listing 19-4 中的简短程序。
1 ' BadPath.sb
2 path = "C:\Temp\Folder1\Out.txt"
3 res = File.WriteContents(path, "Hello")
4 TextWindow.WriteLine(res + ": " + File.LastError)
Listing 19-4:路径不存在时写入文件
你应该看到以下输出:
FAILED: Could not find a part of the path 'C:\Temp\Folder1\Out.txt'.
程序尝试将字符串 "Hello" 写入输出文件(第 2–3 行)。目录 Temp 存在,但子目录 Folder1 不存在,因此 WriteContents() 失败。
向文件追加内容
AppendContents() 方法打开指定的文件,并将数据添加到文件末尾,而不覆盖原有内容。AppendContents() 接受两个参数:输出文件的路径名和你希望添加到文件末尾的字符串。如果操作成功,方法返回 "SUCCESS";否则,返回 "FAILED"。如果你传递给 AppendContents() 的文件不存在,它会为你创建该文件,并将字符串写入其中。如果文件已存在,字符串将被追加到文件末尾。
为了展示 AppendContents() 方法的使用,假设你需要维护一个记录程序中动作、错误和其他事件的日志文件。为了简化程序,我们仅记录程序执行的时间。每次程序运行时,你都会向日志文件中添加一条记录,包括日期和时间。完整的程序在 Listing 19-5 中展示。
1 ' AppendContentsDemo.sb
2 outFile = Program.Directory + "\Log.txt"
3
4 strLog = Clock.WeekDay + ", " + Clock.Date + ", " + Clock.Time
5 result = File.AppendContents(outFile, strLog)
6 If (result = "FAILED") Then
7 TextWindow.WriteLine("Failed to write to: " + outFile)
8 TextWindow.WriteLine(File.LastError)
9 EndIf
10
11 TextWindow.WriteLine("Thank you for using this program. And for using
deodorant.")
Listing 19-5:演示 AppendContents() 方法
当你运行这个程序时,它会创建一个包含当前星期几、日期和时间的日志字符串(第 4 行),并将这个字符串追加到名为 Log.txt 的日志文件末尾,该文件位于程序的目录中(第 5 行)。如果写入文件失败,程序会显示一条错误信息,解释失败的原因(第 7–8 行)。然后,程序会显示一条消息(第 11 行)并结束。
每次运行此程序时,都会向 Log.txt 文件末尾添加一行。以下是运行程序三次后的 Log.txt 输出:
Sunday, 7/19/2015, 12:40:39 PM
Sunday, 7/19/2015, 12:43:21 PM
Sunday, 7/19/2015, 12:47:25 PM
ReadLine(), WriteLine(), 和 InsertLine()
ReadContents() 和 WriteContents() 方法允许你一次性读取和写入文件的全部内容。有时这正是你需要的,但在其他情况下,一次读取或写入一行可能更合适。
File 对象提供了 ReadLine() 方法,用于从文件中读取一行文本。一行文本由一串字符组成,末尾带有回车符和换行符。ReadLine() 会读取该行所有文本,直到(但不包括)回车符。该方法接受两个参数:文件路径和要读取的行号。文件的第一行是第 1 行,第二行是第 2 行,以此类推。如果文件包含指定的行号,方法将返回该行的文本;否则,返回空字符串。
File 对象还提供了 WriteLine() 方法,用于将一行文本输出到文件。该方法接受三个参数:文件路径、要写入文本的行号以及要写入的文本。使用该方法时,需注意以下几点:
-
如果文件不存在,
WriteLine()会创建该文件。 -
如果文件包含指定的行号,
WriteLine()会覆盖该行。 -
如果指定的行号大于文件中的行数,指定的文本将被附加到文件末尾。例如,如果文件包含三行,而你要求
WriteLine()在第 100 行写入新文本,指定的文本将写入第 4 行。 -
WriteLine()会自动在传入的文本末尾写入回车符和换行符。这意味着你不需要手动在字符串中追加这些字符。 -
如果操作成功,
WriteLine()返回"SUCCESS";否则,返回"FAILED"。
除了 ReadLine() 和 WriteLine(),File 对象还提供了 InsertLine() 方法,允许你在指定的行号插入一行文本。与 WriteLine() 方法类似,InsertLine() 方法接受三个参数:文件路径、要插入新文本的行号以及要插入的文本。InsertLine() 不会覆盖指定行的现有内容。如果操作成功,InsertLine() 返回 "SUCCESS";否则,返回 "FAILED"。
举个例子,我们可以写一个简单的程序,用用户的名字和姓氏生成登录名。该程序会读取一个输入文件,文件中包含用户的名字和姓氏,并创建一个包含这些用户登录名的输出文件。用户的登录名由名字的首字母和姓氏中的最多五个字符组成。例如,如果用户的名字是 Jack Skellington,他的登录名是 jskell。如果用户的名字是 Stan Lee(姓氏为三个字母),他的登录名将是 slee。完整的程序请参考 Listing 19-6。
1 ' LoginName.sb
2 inFile = Program.Directory + "\Users.txt"
3 outFile = Program.Directory + "\LoginNames.txt"
4
5 N = 1 ' Tracks the line number
6 While (N > 0) ' We'll set N = 0 when we detect end of file
7 strLine = File.ReadLine(inFile, N) ' Reads the Nth line
8 If (strLine = "") Then ' If the string's empty
9 N = 0 ' Exits the While loop
10 Else ' We have an entry
11 idx = Text.GetIndexOf(strLine, " ") ' Finds space in strLine
12 firstChar = Text.GetSubText(strLine, 1, 1)
13 lastName = Text.GetSubText(strLine, idx + 1, 5)
14 loginName = firstChar + lastName
15 loginName = Text.ConvertToLowerCase(loginName)
16 File.WriteLine(outFile, N, loginName) ' Saves to a file
17 N = N + 1 ' Gets ready for the next line
18 EndIf
19 EndWhile
清单 19-6:从名字和姓氏创建登录名
我们首先给出输入文件和输出文件的路径(第 2-3 行)。然后开始一个循环,每次读取输入文件的一行(第 6-19 行)。读取一行后(第 7 行),我们检查该行是否为空,如果是(第 8 行),则将N设为 0 以结束循环(第 9 行)。否则,我们处理从输入文件读取的用户姓名,以创建小写的登录名(第 11-15 行)。首先,我们找到名字和姓氏之间的空格(第 11 行)。接着,我们获取名字的第一个字母(第 12 行)和姓氏的前五个字母(第 13 行),将它们结合起来创建登录名(第 14 行),并将登录名转换为小写(第 15 行)。然后我们将登录名写入输出文件(第 16 行),并将N加 1 以读取输入文件中的下一行(第 17 行)。
为了保持代码简洁,我们没有添加错误检查代码。我们还假设输入文件的格式正确:每行包含用户的名字和姓氏,二者之间由一个空格分隔。表 19-2 展示了该程序的一个输入文件示例及其输出文件。
表 19-2: 创建登录名
| 用户姓名 | 登录名 |
|---|---|
| Tina Fey | tfey |
| Jimmy Fallon | jfallo |
| David Letterman | dlette |
| Jay Leno | jleno |
| Amy Poehler | apoehl |
动手试一试 19-2
编写一个程序,读取一个输入文件并统计其中的行数、字符数和空格数。
文件管理
除了让你执行文件 I/O 的方法外,File对象还提供了几个与文件和目录管理相关的方法。使用这些方法,你可以复制和删除文件,创建和删除目录,并从程序中列出文件和目录。
复制和删除文件
你可以使用 CopyFile() 方法创建一个现有文件的副本。此方法将源文件和目标文件的路径作为参数传递。源文件不会受到此操作的影响。如果操作成功,方法将返回 "SUCCESS"。否则,它将返回 "FAILED"。
如果目标路径指向一个不存在的位置,该方法会尝试自动创建该位置。例如,查看以下代码:
srcPath = "C:\Temp\Test1.txt" ' Path of the source file
dstPath = "C:\Temp\Temp1\Temp2\Test1.txt" ' Path of the destination file
File.CopyFile(srcPath, dstPath)
如果子文件夹Temp、Temp1 和 Temp2 不存在,CopyFile()会尝试从根目录开始创建目标路径中的所有目录。当你运行这段代码时,你将有两个 Test1.txt 文件的副本:一个是原始源文件,另一个是位于 C:\Temp\Temp1\Temp2 下的副本。
警告
如果目标路径指向一个已存在的文件,该文件将被覆盖。所以使用 CopyFile() 方法时要小心,因为你可能会覆盖某些文件!
如果你想删除文件,请使用 DeleteFile() 方法。该方法接收一个参数:你想删除的文件路径。如果操作成功,方法返回 "SUCCESS"。否则,返回 "FAILED"。
警告
被删除的文件不会进入回收站;相反,它会被完全从系统中删除。所以在使用 DeleteFile() 方法时一定要格外小心!
使用 CopyFile() 和 DeleteFile() 方法,你可以创建自己的子程序来移动和重命名文件。要将文件移动到新位置,先将文件复制到新位置,然后删除原始文件。要重命名文件,先复制文件,给副本起个新名字,然后删除原始文件。
创建和删除目录
你可以轻松地创建或删除目录。CreateDirectory() 方法接收一个参数:你想创建的目录路径。如果目录不存在,方法会尝试创建路径中的所有目录,从根目录开始。如果操作成功,方法返回 "SUCCESS"。否则,返回 "FAILED"。以下是一个示例:
File.CreateDirectory("C:\Temp\Temp1\Temp2")
如果目录 C:\Temp、C:\Temp\Temp1 和 C:\Temp\Temp1\Temp2 不存在,CreateDirectory() 会创建它们。如果目录路径已经存在,函数不会做任何事情,直接返回 "SUCCESS"。
DeleteDirectory() 方法也接收一个参数:你想删除的目录路径。路径下的所有文件和文件夹都会被删除。如果操作成功,方法返回 "SUCCESS"。否则,返回 "FAILED"。图 19-5 显示了 DeleteDirectory() 的示例。

图 19-5:演示 DeleteDirectory() 方法
警告
当你调用 DeleteDirectory(),路径下的所有文件和文件夹都会被删除。所以请确保你没有不想删除的文件!
列出文件和目录
File 对象包括 GetFiles() 方法,允许你列出目录中的所有文件。该方法以目标目录的路径作为参数。示例代码在示例 19-7 中展示了如何使用此方法。
1 ' GetFilesDemo.sb
2 path = "D:\Temp"
3 fileArray = File.GetFiles(path)
4 count = Array.GetItemCount(fileArray)
5 TextWindow.WriteLine(path + " contains " + count + " files:")
6 For N = 1 To count
7 TextWindow.WriteLine(" " + fileArray[N])
8 EndFor
示例 19-7:演示 GetFiles() 方法
运行该程序后的输出如下(将第 2 行中的 path 变量更改为你计算机上的一个目录):
D:\Temp contains 3 files:
D:\Temp\Fig01.bmp
D:\Temp\keys.txt
D:\Temp\Test.sb
我们首先指定要列出的目录路径(第 2 行)。接下来,我们调用 GetFiles() 方法,并传入所需的路径(第 3 行)。该方法会创建一个包含目录中所有文件路径的数组;我们将返回数组的标识符保存在 fileArray 中。然后,我们调用 GetItemCount() 来查找返回数组中的元素数量(第 4 行),并使用 For 循环来显示它的元素(第 6–8 行)。
注意
如果 GetFiles() 失败,则 fileArray 存储字符串 "FAILED"。在这种情况下,调用 Array.GetItemCount(fileArray) 将返回 0。因此,你可能不需要对 GetFiles() *的返回值执行额外的检查。
GetDirectories() 方法允许你列出给定目录中的所有子目录。列表 19-8 展示了该方法的一个示例。
1 ' GetDirectoriesDemo.sb
2 path = "D:\Temp"
3 dirArray = File.GetDirectories(path)
4 count = Array.GetItemCount(dirArray)
5 TextWindow.WriteLine(path + " contains " + count + " directories:")
6 For N = 1 To count ' Displays the array's elements
7 TextWindow.WriteLine(" " + dirArray[N])
8 EndFor
列表 19-8:演示 GetDirectories() 方法
这是运行该程序后的输出:
D:\Temp contains 3 directories:
D:\Temp\Chapter01
D:\Temp\Chapter02
D:\Temp\Chapter03
但是你的输出可能看起来不同,这取决于你的Temp目录的内容。这个程序类似于列表 19-7。我们首先存储我们感兴趣的路径(第 2 行)。接下来,我们用该路径调用GetDirectories()(第 3 行)。这个方法创建一个包含指定路径下所有目录路径名的数组;我们将返回数组的标识符保存在dirArray中。然后,我们调用GetItemCount()来找出返回数组中的元素数量(第 4 行),并使用For循环来显示其元素(第 6 到第 8 行)。试着修改第 2 行,访问不同的目录。
到目前为止,我们已经涵盖了关于File对象的所有内容。让我们将这些新获得的知识付诸实践,创建一些有趣的应用程序!
实用程序
我们将展示两个程序,旨在突出文件 I/O 的不同方面,并为你提供一些可以应用于自己创作的想法和新技术。
诗人
在这个示例中,我们将修改我们在第十六章创建的诗人程序,使其从文件中读取输入,而不是将单词列表硬编码到程序中。通过这样做,你的程序将变得更加简洁和强大,而且添加单词也会变得更加容易!
该程序使用五个输入文件:article.txt、adjective.txt、noun.txt、verb.txt 和 preposition.txt。article.txt 文件包含冠词和限定词的列表;adjective.txt 文件包含形容词的列表,依此类推。为了利用 Small Basic 处理数组的方式,每个文件都经过格式化,便于在程序中读取到数组中。
看看这个语句:
art = File.ReadContents("article.txt")
我们将article.txt文件的内容自动加载到名为art的数组中,该数组包含图 19-6 中显示的五个元素。

图 19-6:将 article.txt 文件的内容读取到名为 art 的数组中
打开本章文件夹中的 Poet_Incomplete.sb 文件,该文件还包含背景图像和我们需要的五个输入文件。该文件中有一个空的占位符,用于存放CreateLists()子例程,现在你需要添加它。这个子例程显示在列表 19-9 中。
1 Sub CreateLists
2 article = File.ReadContents(path + "\article.txt")
3 adjective = File.ReadContents(path + "\adjective.txt")
4 noun = File.ReadContents(path + "\noun.txt")
5 verb = File.ReadContents(path + "\verb.txt")
6 prepos = File.ReadContents(path + "\preposition.txt")
7 EndSub
列表 19-9: CreateLists() 子例程
运行此程序,它应该与之前的程序相同,但有一个优势:用户现在可以更改输入文件,创建自己的自定义诗歌。
数学奇才
在这个例子中,我们将创建一个程序,主角是一个似乎对数学了解很多的巫师。这个巫师不是梅林、甘道夫或哈利·波特:欢迎来到数学巫师的世界!巫师首先要求用户想一个秘密数字。接着,他请求用户对这个数字进行一些数学操作(例如将数字翻倍、减去 2、将结果除以 10 等等)。最后,巫师利用他的魔法力量告诉用户经过这些操作后得到的数字(尽管他并不知道用户的秘密数字!)
程序的思路非常简单。我们将每个数学谜题保存为一个文本文件,文件格式如 图 19-7 所示。第一行包含谜题的答案,剩下的行包含巫师要求用户执行的指令。该程序包含 11 个谜题,分别保存在 Puzzle01.txt、Puzzle02.txt、... 和 Puzzle11.txt 中。你可以通过创建更多的谜题文件来添加新的谜题(遵循 图 19-7 所示的格式)。

图 19-7:谜题文件的格式
开发此程序的策略概述如下:
-
当程序启动时,我们将列出程序目录中的文件,以获取谜题文件的路径名。
-
对于每一轮程序,我们将选择一个可用的谜题。
-
我们读取选定谜题文件的第一行,并将其解释为谜题的答案。其余的行代表巫师展示的指令。
-
巫师一一展示谜题的说明,直到程序遇到空行。每个指令后,巫师要求用户按 ENTER 键。
-
巫师展示谜题的答案。
打开本章文件夹中的 Wizard_Incomplete.sb 文件。此文件包含程序的主要代码,如 清单 19-10 所示,并且为你将要添加的 DoPuzzle() 子程序预留了空白占位符。该文件夹还包含 11 个预制谜题的文本文件。
1 ' Wizard_Incomplete.sb
2 TextWindow.Title = "MATH WIZARD"
3 TextWindow.WriteLine("========== MATH WIZARD ==========")
4 TextWindow.WriteLine("Press Enter after each instruction")
5 TextWindow.WriteLine("==================================")
6 TextWindow.WriteLine("")
7
8 puzzle = File.GetFiles(Program.Directory) ' Stores filenames into an array
9
10 For P = 1 To Array.GetItemCount(puzzle)
11 path = puzzle[P] ' File in the app's directory
12 If (Text.EndsWith(path, ".txt")= "True") Then
13 DoPuzzle()
14 EndIf
15 EndFor
16 TextWindow.WriteLine("The game was won, the math was fun, and the magic is
done!")
17 TextWindow.WriteLine("There is one Math Wizard to rule them all! Bye!")
清单 19-10:数学巫师程序的主要代码
在显示程序的标题和说明(第 2–6 行)后,我们调用 GetFiles() 获取程序目录中所有文件的列表,并将返回的数组标识符保存在 puzzle 变量中(第 8 行)。接着,我们开始一个循环来处理找到的文件(第 10–15 行)。在每次迭代中,我们从 puzzle 数组中获取一个路径名(第 11 行),并检查它是否具有 .txt 扩展名(第 12 行)。如果文件具有 .txt 扩展名(即它包含一个谜题),我们调用 DoPuzzle() 将该谜题展示给用户(第 13 行)。程序以数学巫师的一条消息结束(第 16–17 行)。
将 清单 19-11 中所示的 DoPuzzle() 子程序添加到 Wizard_Incomplete.sb 程序的底部。
1 Sub DoPuzzle
2 puzzleAns = File.ReadLine(path, 1) ' Reads answer from first line
3 N = 2 ' Starts from second line
4 line = "?" ' To enter the loop
5 While (line <> "") ' Loops as long as we have instructions
6 line = File.ReadLine(path, N) ' Reads the Nth line
7 If (line <> "") Then ' If we have an instruction
8 TextWindow.Write(line + "... ") ' Writes instruction
9 TextWindow.PauseWithoutMessage() ' Waits for user to press a key
10 TextWindow.WriteLine("")
11 N = N + 1 ' Prepares to read next line
12 EndIf
13 EndWhile
14 TextWindow.WriteLine("You still have: " + puzzleAns)
15 TextWindow.WriteLine("")
16 EndSub
清单 19-11: DoPuzzle() 子程序
我们从文件中读取第一行并将其保存在puzzleAns中(第 2 行)。接下来,我们将N设置为 2,以读取文件的第二行,并将line字符串设置为"?"以进入While循环(第 3–4 行)。在循环的每次迭代中,我们从谜题的文件中读取一行(第 6 行),并检查程序是否已到达最后一条指令。如果line不为空(第 7 行),我们显示程序刚刚读取的指令(第 8 行),并等待用户按下任意键(第 9 行)。当玩家按下任意键时,我们将N递增,以读取文件中的下一条指令(第 11 行)。当程序读取到空行时,While循环结束,程序跳转到第 14 行,在那里我们显示谜题的答案,后跟一个空行(第 14–15 行)。
图 19-8 显示了程序的示例运行。

图 19-8:Math Wizard 程序的示例输出
尝试一下 19-3
想想如何改进 Math Wizard 程序,并尝试实现它们。例如,添加一些颜色,使输出看起来更花哨,或者在每个谜题后画一些东西。
编程挑战
如果你卡住了,可以查看 nostarch.com/smallbasic/ 获取解决方案以及更多资源和教师和学生的复习题。
-
让我们使用同音词编写一个拼写测验游戏。同音词是发音相同但意思不同的单词。使用记事本创建以下文本文件:
In your math class;ad/add;add Halloween queen;which/witch;witch Eyes do this;sea/see;see In the church;altar/alter;altar A rabbit;hair/hare;hare A good story;tail/tale;tale Animals have them;clause/claws;claws Pencils do this;right/write;write该文件中的每一行包含三个由分号分隔的字段。第一个字段是你将展示给玩家的提示,例如
In your math class。第二个字段是玩家将从中选择的两个可能答案,例如ad/add。第三个字段是正确答案,例如add。在每一轮中,让程序显示提示和两个可能的答案给玩家,然后等待他们输入答案。让程序将用户的答案与正确答案进行比较,然后告知他们答案是否正确。
-
编写一个测试学生动物王国知识的科学测验。首先,使用记事本创建以下文本文件:
1=Invertebrates;2=Fish;3=Amphibians;4=Reptiles;5=Birds;6=Mammals Bat;6 Clam;1 Dog;6 Frog;3 Lizard;4 Peacock;5 Salamander;3 Salmon;2 Spider;1 Turkey;5 Turtle;4第一行包含可能的分类。其余的每一行包含一个动物的名称及其正确的分类。将动物名称显示给玩家,然后要求他们通过输入正确分类的编号来分类该动物。然后处理玩家的答案,并告知他们答案是否正确;如果他们的答案不正确,显示正确的分类。
第二十一章:接下来该做什么

你已经掌握了 Small Basic 的编程基础,恭喜你!如果你渴望更多,网上还有额外的资源可以供你探索。
在线资源
访问 www.nostarch.com/smallbasic/,下载本书的额外资源。下载并解压文件后,你将看到以下材料:
书中程序和解决方案 下载完成的程序、所需的所有图片、一些编程挑战的框架代码以及编程挑战和“动手试一试”练习的解决方案。这将省去你大量的输入工作!
附加资源 这些是与本书内容相关的在线文章,其中许多是专门为补充本书而编写的!
复习题 测试你(或你学生)的知识。
练习题 除了书中的“动手试一试”练习和编程挑战外,你还可以找到更多的练习来进行实践。这对教师来说也非常有帮助,可以提供更多的作业选项。
Small Basic 网站
访问 www.smallbasic.com/,探索 Small Basic 和编程的世界。你会找到精选的游戏和程序、文档、面向教师的课程内容等。
玩游戏 访问程序画廊,查看其他程序员制作的作品。
加入社区 访问 aka.ms/SmallBasicForum/ 在论坛上提问,并与整个 Small Basic 社区分享你的游戏和程序。
了解最新动态 了解新版本、扩展功能、精选内容、游戏和应用程序,访问 blogs.msdn.com/b/smallbasic/。
获取教学支持 教师可以加入一个私人网络,获得来自微软的个人支持,涵盖所有微软产品,访问 aka.ms/MCSTN/。
与团队联系 通过电子邮件联系 Ed Price 和其他 Small Basic 团队成员,地址是 smallbasic@microsoft.com。告诉他们是这本书引导你来的!
毕业到 Visual Basic
你可以将任何 Small Basic 程序转换为等效的 Visual Basic 程序,这样你就可以过渡到一个专业编程语言的完整功能。首先,你需要免费下载 Visual Studio。访问 www.visualstudio.com/,点击右上角的 Free Visual Studio。
接下来,打开 Small Basic 工具栏,点击毕业,选择你的输出位置,然后点击继续。Visual Studio 会打开,并将代码转换为 Visual Basic!返回 Visual Studio 网站,查看关于 Visual Basic 和 Visual Studio 界面的入门文档。如果你对毕业体验有任何疑问,欢迎在 Small Basic 论坛向我们提问。


































浙公网安备 33010602011771号