第3章计 算 器 本章的任务是开发一个带有常用功能的计算器。 Windows操作系统附件中的计算器大家一定不陌生,别看这个小小的计算器,却有着许多复杂的功能。将程序切换到科学型计算器界面,便可以看到那些功能。在那些功能中,有些需要有数理统计学的知识,而另一些功能实现起来逻辑非常复杂,例如括弧的运算需要有数据结构的知识和高级程序设计技巧,这些都超出了VB程序设计的教学大纲的要求,因此不在本书中介绍。 虽然小小的计算器名字听起来很简单,实际却蕴含了大量的知识点和编程技巧。 3.1 开 发 任 务 在本任务中,要分别实现基本算术运算、累加和计算、阶乘计算、三角函数计算、排列组合计算和对数计算等多个子任务。 3.1.1 计算器的初级版本 在本节中,先利用第2章学过的知识和方法,试着开发一个计算器的初级版本,看看所学的知识是否充分,如果不够,还要补充哪些知识。 第2章所设计的计算器看上去根本不像一个计算器,而且也只能做单一的加法运算。本节要开发一个稍微复杂一些的计算器,不但能做基本的四则运算,而且外观和各种操作方式也更像一个计算器。 1. 程序界面设计 1) 新建工程 打开VB开发环境,VB已经自动创建好一个新的工程和一个窗体。在“工程浏览器”窗口中单击工程文件夹(见图3-1(a)) ,在下方的“工程属性”窗口中将名称的属性值由“工程1”改为“计算器”(如图3-1(b))所示;再单击“工程浏览器”中的窗体文件名“Form1" ,并在下方属性窗口中将窗体名称改为“frmCalculator" ,如图3-1(c))所示。 图3-1 改变工程和窗体名称 然后先保存一次,单击VB工具栏中的“保存”按钮,在“保存”对话框中选择D盘或其他位置,并新建一个文件夹,命名为“计算器”,然后将窗体文件和工程文件保存在新建的文件夹中。窗体文件命名为frmCalculator.frm,工程文件命名为prjCalculator.vbp. 2) 添加控件 在本工程中,需要用到下列控件: 1个文本框用于输入运算数和输出结果;16个按钮构成计算器键盘,图3-2 添加文本框控件 其中10个用于输入10个数字字符,1个用于输入小数点,一个用于触发计算的等号,另外4个用于选择加、减、乘、除运算符。 首先在窗体上部添加一个文本框,默认名称是Text1,调整好大小和位置(如图3-2所示),并将属性Text的值清空,再将对齐方式属性Alignment的选项改成“1-Right Justify”右对齐(默认选项是“0-Left Justify”左对齐). 技巧: 调整控件的大小和位置,也可以用键盘组合键操作,Ctrl+箭头键: 移动控件位置;Shift+箭头键: 改变控件尺寸。调整前必须首先选中控件。 现在来制作键盘,第1步,添加第1个按钮。在文本框下方添加一个按钮Command1,将它调整为一个计算器按键般大小,并把Caption属性改成“1" . 第2步,添加第2个按钮。如果要添加很多外形类似的按钮,不需要逐个添加,用复制的方法可以更快捷地完成: 先在窗体选中Command1对象,并按下Ctrl+C组合键进行复制,再按下Ctrl+V组合键进行粘贴,这时VB会弹出一个对话框,询问“已经有一个控件为‘Command1’。创建一个控件数组吗?" (见图3-3) ,此时一定要回答“否”,这样在窗体左上角就会出现Command1对象的副本,两者的外观完全一样,Caption也相同,都是1,不过看看属性窗口,它俩的名称却不同,后者的名称是Command2。我们将它的Caption改成2,并移动到Command1的右边。 第3步,重复进行粘贴操作,依次制作其他按钮,按图3-4所示的布局排列。前9个按钮的Caption改成与它们的顺序号相同,Command10的Caption改为“0" , Command11的Caption改为“." , Command12的Caption改为“=" , Command13~Command16的Caption依次改为“+" 、 "-" 、 "*" 、 "/" . 图3-3 创建控件数组询问对话框 图3-4 键盘制作 由于键盘上没有数学运算符中的乘号“×”和除号“÷" ,因此编程语言通常都用“*”代替“×" ,用“/”代替“÷" . 技巧: 要把多个控件画得一样大小并且排列整齐,需要花费较多时间,好在VB为我们提供了很好的界面格式化效率工具,它们位于VB的“格式”菜单中。先拖动鼠标来框住需要统一格式的控件(也可以按下Ctrl键后再逐个单击控件),并且单击选择其中一个控件作为参照的基准,然后选用“格式”菜单中的各种对齐或者尺寸工具,即可又快又准地按基准控件的格式来批量地设置其他控件的格式。 2. 程序代码编写 添加了这么多控件,每一个都有自己的功能,要实现它们的功能,就需要为它们的事件编写过程代码。以下就对它们逐个编程来实现它们的功能。 1) 数字按钮的处理 数字按钮的功能是,在单击按钮后,将对应的数字加入到文本框Text1中。 双击Command1,进入代码编辑窗口,在Command1的单击事件过程中加入如下语句: Private Sub Command1_Click() Text1.Text="1" End Sub 现在测试一下,启动程序,单击按钮1(为了简化叙述,以下将Caption为n的按钮简称为按钮n) ,不错,文本框中的内容变成了1。不过,再单击一下按钮1,文本框中还是1,不对呀,正常的计算器这时应该出现11才对。 2) 错误的发现与修正 问题出在哪里呢?原来是这样的: 由于我们写的是赋值语句,每单击一次按钮1,便会执行一次它的单击事件过程,也就是执行一次赋值运算。而赋值过程是将等号右边的数据写入到等号左边的变量中,不论变量中原来有什么内容,新的数据总会替换掉原有的内容,因此赋值语句不论执行多少次,Text1.Text的内容始终是1. 问题找到了,那怎么解决呢?怎样把新写入的字符加到原有内容后面呢?还记得拼接运算吧?可以把文本框中原有的字符串数据取出,将它与新输入的字符相拼接,然后将拼接的结果作为新数据,再写回到文本框,不就可以了吗?好,现在来改写一下赋值语句: Private Sub Command1_Click() Text1.Text=Text1.Text & "1" End Sub 再试运行一下,多按几次按钮1, OK,这回可以了。 下面,再来接着写Command2的事件过程代码: Private Sub Command2_Click() Text1.Text=Text1.Text & "2" End Sub 测试一下,随机单击按钮1和按钮2,输入效果与我们期望的完全一致。 现在有了两个事件过程,它们相互独立,互不干涉,单击按钮1的时候就执行第1个过程,单击按钮2 的时候才会执行第2个过程,就像台风登陆的时候我们不会去执行地震预案一样。 仿照上述做法,再写出其他8个数字按钮和小数点按钮的单击事件过程代码。 3) 运算符按钮的处理 在第2章中所做的简易加法计算器使用了3个文本框,两个作输入,1个作输出。而本节的计算器只有1个文本框,当它被第1个运算数占领后,第2个运算数往哪里放呢? 可以这样考虑,在输入第2个数之前,先将第1个数暂时找个地方保存起来,这样就可以把文本框腾出来接收第2个数了。 (1) 运算数的暂存 先来推导一下用户操作计算器的步骤: 先输入第1个运算数,然后单击某一个运算符按钮,再输入第2个运算数,最后单击等号按钮实施计算并得到输出结果。 根据以上步骤可知,暂存第1个运算数的时机应该是在单击运算符之后。在这个点上我们需要做2件事: 暂存文本框中的数据和清空文本框中的数据。 要暂存数据就需要使用变量,我们先来声明一个变量,再将文本框中的数据保存下来: Dim sglNumber As Single '用于暂存第1个运算数,前缀sgl是Single的缩写 sglNumber=Text1.Text'保存第1个运算数,字符串自动转换成为单精度型 如果单击的是加法运算符,那么加法运算符按钮的单击事件过程应该是这样的: Private Sub Command13_Click() '单击加法运算符按钮 Dim sglNumber As Single '用于暂存第1个运算数的变量 sglNumber=Text1.Text '保存第1个运算数 Text1.Text="" '用空串赋值,即可清空文本框 End Sub (2) 加法运算代码 在输入第2个运算数之后,需要单击等号按钮来完成运算。如果之前单击的是加法运算符,那么现在就要进行加法运算,并且将运算结果输出到文本框中: Private Sub Command12_Click() '单击等号按钮 Text1.Text=sglNumber+Val(Text1.Text)'取出第2个数与第1个数做加法并输出结果 End Sub 试运行一下程序,单击按钮5,再单击加号按钮,然后再单击按钮6,最后单击等号按钮,结果出来了吗?怎么5+6=6?5到哪里去了?sglNumber中明明保存的是5嘛。 来找一下原因吧。我们可以在运算结果输出语句前调用一下MsgBox,让它把sglNumber弹出来看看它的值到底是不是5: Private Sub Command12_Click() '单击等号按钮 MsgBox sglNumber '弹出变量的值 Text1.Text=sglNumber+Val(Text1.Text)'做加法运算并输出 End Sub 再照刚才的步骤操作一次,对话框中弹出了什么?居然什么都没有!难道赋值语句没有作用?那么流程再往后退,去检查一下加法运算符按钮的单击事件过程,在其中插入一个对话框到赋值语句之后,看看赋值后变量的情形。 Private Sub Command13_Click() '单击加法运算符按钮 Dim sglNumber As Single sglNumber=Text1.Text MsgBox sglNumber '弹出变量的值 Text1.Text="" End Sub 还按刚才的次序操作,单击加号按钮后,对话框弹出了5,到目前为止运行正确。然后单击按钮6,再单击等号按钮,结果弹出的对话框是空的!明明赋值是成功的呀,怎么单击等号按钮,sglNumber中的值就没有了呢? 测试到了这里,只好暂时打住,因为我们的知识储备还差得远呢。下一小节我们来插入一个非常重要的概念,明白了这个概念,也就解答了刚刚遇到的难题。 在进入下一小节之前,先介绍几种程序调试的方法,以利后续的开发。 4) 程序调试方法 程序调试的方法有多种,这里介绍最基本的3种。 (1) 第1种调试方法,在适当的地方加上MsgBox调用,通过弹出变量的当前值来进行观察判断,以便确定错误发生在这之前还是之后,来帮助我们对程序的错误进行定位。 (2) 第2种调试方法,在适当位置加上中断标记,让程序运行到指定的位置时暂停,然后利用调试的环境来检查变量当前的值。 添加中断的方法是这样的: 在需要添加中断的语句左边灰色边框中单击鼠标,此时会出现一个棕红色圆点,同时将当前行高亮显示,这样就添加了一个断点(如图3-5所示). 图3-5 添加调试断点 添加断点后,再启动程序,当流程到达断点语句行的时候,会暂时中断,并切换到代码窗口,此时设有断点的代码行会以黄色背景高亮显示。如果要查看变量的当前值,可以将鼠标箭头悬停在变量名上,下方就会出现一个小矩形框,显示“变量名=变量值”的提示(见图3-6) . 图3-6 程序中断状态下查看变量值 注意: 当流程暂停在中断语句处时,该语句尚未被执行,看到的是本行执行之前的变量值。如果按下功能键F8,程序可以单步执行,即往下走1行,可以继续跟踪观察变量值的变化和后续语句的执行状态。要让程序离开调试环境恢复自动运行,可以按下F5键。若要取消断点,只需在断点的小圆点上再次单击即可。 (3) 第3种调试方法,借助于VB的调试专用对象Debug,在适当的位置加上Debug对象的打印方法(语法与窗体的Print方法完全一致): Debug.Print <变量名列表>\\ 当程序遇到此语句后,会在代码窗体下方的立即窗体中打印出变量的值,供观察分析。这是一种非中断的调试方法,不会影响程序的正常运行。调试完成后,如果忘记删除这些Debug.Print语句也没有关系,编译程序会忽略这些语句,程序照样能够通过编译。 3. 变量的作用范围 程序中的变量是有作用范围的,它们的作用范围与其生命周期有着密切的关系。 1) 变量的生命周期 如果变量声明语句写在一个过程之内,那么,只有当程序流程进入这个过程并执行到该声明语句时,变量才会被分配内存单元(这是变量声明语句的功能),而当程序执行到End Sub语句,也就是过程结束的同时,变量就被系统从内存中清除。换句话,过程中声明的变量是有生命周期的: 从被声明之时出生,在过程结束时消亡。在过程中声明的变量,称为过程级变量。 一个生活中的例子可以帮助我们通俗地理解这个概念: 我来到宾馆(进入过程),开了一个房间(声明了一个变量)后,房间就归我使用了,等到我离开宾馆(过程结束)时,就退房了,所以房间就不能归我使用了。对于我来说,这个房间就等于是没有了(消亡了). 变量声明语句还可以写在过程之外,通常是写在模块中所有过程的前面。这种变量称为模块级变量。模块级变量从文件被加载时出生,当文件被卸载或程序被结束时消亡。对于窗体文件中的模块级变量,它们的生命与窗体的寿命一样长,只要窗体对象未被卸载,模块级变量就始终存活。 2) 变量的作用范围 因为过程级变量只能在自身所在的过程之中存活,又因为在没有被事件触发或被调用之前,程序流程是不会进入任何过程的,因此根本就不会遇到过程级变量声明语句,也就是说,平时过程级变量是不可能存在的,所以它们的作用范围就仅限于自身所在的过程,因此过程级变量也称局部变量。 又由于两个事件过程不可能在同一时刻执行,所以一个正在执行过程中的语句就不可能引用另一个过程中尚未被声明的变量。在我没有开房的期间,别人当然也不能使用我不存在的房间了。 这就回答了为什么在等号按钮单击事件过程中不能使用运算符按钮单击事件过程中所声明的变量sglNumber了,因为在前者引用后者的变量sglNumber时,sglNumber已经消亡了。 而对于窗体模块级的变量来说,在模块中任何一个过程被执行时,窗体一定是保持在存活(未被卸载)状态的,所以只要被声明过的模块级变量也是处于存活状态的,就可以被本模块中的任何过程所访问,换句话说,它们的作用范围是整个模块。对于一个模块中的每个过程而言,模块级变量就是一种全局变量。 如果模块级变量被声明为Public(公共变量),那么只要模块没有被卸载,在整个工程范围之内,变量都是可访问的,只是访问方法有所不同,具体将在后续章节介绍。 明白这些道理之后,只要将过程级变量sglNumber改成模块级变量,那么前面遇到的问题就迎刃而解了。 本小节中对过程级变量的叙述,也都适用于函数中所声明的变量,函数有着与过程类似的结构。有关自定义函数的声明,将在后续章节引进。 4. 程序代码的改进 1) 加法运算代码 根据变量的作用范围,将第1个运算数的暂存变量sglNumber的声明语句放在了所有过程之外,让它成为模块级变量,这样所有过程中的语句就都可以访问这个变量了。 Option Explicit Dim sglNumber As Single '模块级变量,用于暂存第1个运算数 Private Sub Command13_Click() '单击加法按钮 sglNumber=Text1.Text '保存第1个运算数 Text1.Text="" '用空串赋值,即可清空文本框 End Sub Private Sub Command12_Click() '单击等号按钮 Text1.Text=sglNumber+Val(Text1.Text)'做加法运算并输出 End Sub Rem以下省略了所有数字按钮和小数点按钮的单击事件过程代码 现在再运行一下程序,加法运算已经基本符合要求了。 不过,到目前为止,程序还只能做加法运算。对于减法、乘法和除法运算的处理,我们该怎样考虑呢?是不是在等号按钮单击事件过程中相应地加上其他运算公式就可以了呢? 2) 通用运算代码 要能够进行每一种运算,在单击等号按钮时,程序必须知道用户单击的是哪一个运算符,这样才能选用相应的计算公式。因此必须在用户单击运算符按钮时记下按钮的标记,到后续计算时才能够做出正确的选择。不过保存按钮标记的变量也必须是模块级变量。 程序可以这样写: Option Explicit Dim sglNumber As Single '模块级变量,用于暂存第1个运算数 Dim strOperator As String '模块级变量,用于保存运算符标记 Private Sub Command13_Click()'单击加法按钮 sglNumber=Text1.Text '保存第1个运算数 strOperator=Command13.Caption'保存运算符标记 Text1.Text="" End Sub Private Sub Command14_Click() '单击减法按钮 sglNumber=Text1.Text '保存第1个运算数 strOperator=Command14.Caption'保存运算符标记 Text1.Text="" '清空文本框 End Sub Private Sub Command15_Click() '单击乘法按钮 sglNumber=Text1.Text '保存第1个运算数 strOperator=Command15.Caption'保存运算符标记 Text1.Text="" '清空文本框 End Sub Private Sub Command16_Click() '单击除法按钮 sglNumber=Text1.Text '保存第1个运算数 strOperator=Command16.Caption'保存运算符标记 Text1.Text="" '清空文本框 End Sub Private Sub Command12_Click() '单击等号按钮 If strOperator="+" Then Text1.Text=sglNumber+Val(Text1.Text) '加法 If strOperator="-" Then Text1.Text=sglNumber-Val(Text1.Text)'减法 If strOperator="" Then Text1.Text=sglNumberVal(Text1.Text)'乘法 If strOperator="/" Then Text1.Text=sglNumber/Val(Text1.Text)'除法 Text1.SetFocus Text1.SelStart=0 Text1.SelLength=Len(Text1.Text) End Sub Rem以下省略了所有数字按钮和小数点按钮的单击事件过程代码 现在程序已经可以进行基本的四则运算了。 最后一个过程中的4行选择语句如果用嵌套结构来处理,可以节省很多判断次数,不过四重嵌套结构太复杂,书写时不小心很容易出错。下面来介绍另一种选择结构的语句: 多路选择语句,可以很好地解决这个问题。 5. 多路选择语句 为了解决这个问题,程序设计语言都提供了一种多路选择处理结构,或者称多路分支结构语句,在VB中多路选择语句叫Select Case语句,它的语法格式如下: Select Case <匹配条件表达式> Case <情形1> <语句块1> \情形2> <语句块2> … \情形n> <语句块n>\] \语句块n+1>\]\] End Select 这是一种1个入口多个通道的结构(见图3-7) ,哪个条件匹配,就从哪个通道通行。 图3-7 多路选择语句流程 这种语句的执行逻辑是这样的: 当情形1与匹配条件表达式相匹配时,就执行语句块1中的所有语句,执行完后,直接跳到出口End Select,忽略所有后续的Case块;如果不匹配,则继续向下检查情形2,情形3, ……;当所有情形都不匹配时,就执行Case Else下的语句块,如果没有Case Else部分,则什么都不做。 Case的数量是可选的,根据程序需要而定。若只有1个Case,就相当于If-Then-End If结构;如果只有1个Case和Case Else,就等价于If-Then-Else-End If结构。 匹配条件表达式可以是一个数值型、字符串型、日期型或逻辑型表达式。语句执行时,先将表达式的值计算出来,再将计算结果与下面各Case关键字后的数据进行比较,来检查是否匹配。 每个Case后列出满足该种情形的条件,它可以是1个单一的数据,也可以是多个连续或不连续的数据,或者这些数据的组合表达式,例如: Case 2 Case 1, 3, 5 Case 1 To 10, 12 '1到12,不包含11 Case Is>1 And Is< 5 '1到5区间的数,不包含1和5 Case "Saturday" Or "Sunday" 多路选择程序结构对处理多选一的情况非常有用,它可以大大简化和优化If嵌套结构,提高代码的可读性和程序运行速度。 6. 代码的优化 利用Select Case语句,对运算符选择部分进行优化,代码如下: Private Sub Command12_Click() '单击等号按钮 Select Case strOperator Case "+" Text1.Text=sglNumber+Val(Text1.Text)'加法 Case "-" Text1.Text=sglNumber-Val(Text1.Text)'减法 Case "" Text1.Text=sglNumberVal(Text1.Text)'乘法 Case "/" Text1.Text=sglNumber/Val(Text1.Text)'除法 End Select Text1.SetFocus