8.4.3 编程案例:汇率换算器
本节通过一个应用实例来介绍 MV 方法的具体应用。我们希望设计一个汇率换算器程序, 其功能是将外币换算成人民币,或者相反。最终的版本是图形用户界面的,但在设计过程中, 我们还会设计一个文本界面的版本用来测试程序功能的正确性。
我们首先设计程序模型,这是由 CCApp 类实现的。设计 CCApp 时并不限定将使用的界 面,但随着 CCApp 的细化设计,我们会得到有关界面的功能需求,从而有助于程序用户界 面的设计和实现。
程序规格
汇率换算器程序用于在外币与人民币之间进行换算。 输入:外币币种,换算方向,金额 输出:等值的目标币种金额
明确候选对象
根据程序需求来确定对解题有用的对象。汇率换算器处理的是货币,货币用一个简单的 符号或数值就能表示,没有必要封装成对象。我们只将应用程序模型部分封装成一个类 CCApp,该类的实例需要记录当前的汇率信息,并至少需要实现构造器方法 init__和程序启 动方法 run。也许还需要若干辅助方法,这只有到设计主算法时才会明确。
除了核心部分,程序的另一个组成部分是用户界面。我们将界面也封装成对象,这样做 的好处是:在不改变模型的情况下,通过换用不同界面对象即可改变程序的外观。假设程序 将使用的界面对象是 xInterface,目前还不清楚这个类应有的行为,但随着我们精化 CCApp 的设计,就会明确需要从用户输入什么信息和向用户显示什么信息,所有输入和输出正对应 着 xInterface 类要实现的方法。
实现模型
由于本章重点是用户界面,所以作为例子的汇率换算器程序的模型很简单:按照存储的 当前汇率信息,将给定数额的外币换算成人民币,或将人民币换算成外币。对如此简单的问 题,只需一个 CCApp 类即可实现。当然,对于复杂程序,模型会涉及多种对象,那样就需 要实现多个类。模型的设计可采用自顶向下逐步求精方法,在此逐步细化的过程中,即可逐 步明确用户界面应该提供哪些方法。下面是 CCApp 类的定义:
【程序 8.12 之一】ccapp.py
class CCApp:
def init (self, inter):
self.xRate = {'USD':6.306,
'Euro':8.2735,
'Yen':0.0775,
'Pound':10.0486}
self.interface = inter
def run(self):
while True:
to,fc,amount,bye = self.interface.getInfo()
if bye:
break
elif to == 'RMB':
result = amount * self.xRate[fc]
else:
result = amount / self.xRate[fc]
self.interface.showInfo(result)
首先看构造器 __init__(),它负责汇率换算器的初始化,具体就是用一个字典 self.xRate 来存储若干外币名称及其对人民币的汇率①。注意, __init__方法还有个参数 inter,它代表程 序的用户界面,后面我们会分别用两种用户界面对象传递给此参数,从而得到两种版本的换 算器程序。
将来创建汇率换算器实例之后,通过调用实例的 run 方法来启动换算功能。换算器的核 心算法是个循环,每次循环完成一次换算,换算所需的各种信息都来自用户界面。可以看出 总体上换算过程仍然是简单的 IPO(即输入——处理——输出)算法模式,涉及的输入信息 包括换算方向、换算的外币、换算金额和退出标志(通过界面提供的 getInfo 方法获得),输 出信息就是换算结果(通过界面提供的 showInfo 方法显示)。当用户在用户界面选择退出时, 则不再循环,程序结束。
至此,我们实现了汇率换算器的核心功能,实现了程序的模型部分。但现在还无法测试 程序,因为还没有建立用户界面。但模型对用户界面的基本要求已经确定了,就是要提供 getInfo 和 showInfo 方法,参见图 8.26。
图 8.26 模型-视图方法例 基于文本的用户界面
CCApp 类的定义中用到了很多用户界面的功能,可见模型的设计实现过程也揭示了用户 界面应当如何设计。和模型一样,我们将视图(用户界面)的功能也封装成一个类,这个界 面类必须包括 CCApp 类中所用到的所有方法:quit、close、getCurr、getDirection、getAmount 和 display。如果以不同方式来实现界面类 CCInterface,就能产生具有不同外观的换算器程序, 注意作为基础的模型 CCApp 类是不变的。可见视图与模型可以独立地设计,为同一模型可 以设计多种视图。
① 程序中的汇率数据是 2012 年 4 月 18 日的汇率。
由于一般来说 GUI 比较复杂,为了尽快测试模型的正确性,可以先设计一个简单的文本界面。这个界面纯粹用于测试,无需过多考虑用户友好性,因此我们以最简单最直接的方式 来实现 CCApp 所需的各种界面功能。
【程序 8.12 之二】ti.py
class TextInterface:
def __init__ (self):
print "Welcome to Currency Converter!"
self.qFlag = False # Quit flag
self.fc = 'USD' # foreign currency selected
self.to = 'RMB' # convert to?
self.amt = 0 # amount to be converted
def getInfo(self):
self.qFlag = self.getQFlag()
if self.qFlag:
self.close()
else:
self.fc = self.getFC()
self.to = self.getTo()
self.amt = self.getAmount()
return self.qFlag,self.fc,self.to,self.amt
def getQFlag(self):
ans = raw_input("Want to quit? (y/n) ")
if ans[0] in 'yY':
return True
else:
return False
def getFC(self):
return raw_input("Choose among {USD,Euro,Yen,Pound}: ")
def getTo(self):
ans = raw_input("Convert to RMB? (y/n) ")
if ans[0] in 'yY':
return 'RMB'
else:
return self.fc
def getAmount(self):
if self.to == 'RMB':
return input("How much " + self.fc + "? ")
else:
return input("How much RMB? ")
def showInfo(self,r):
if self.to == 'RMB':
print "%.2f %s ==> %.2f RMB" % (self.amt,self.fc,r)
else:
print "%.2f RMB ==> %.2f %s" % (self.amt,r,self.fc)
def close(self):
print "Goodbye!"
下面我们利用此用户界面来测试 CCApp 的正确性。为此目的,只需创建一个文本界面 对象,再创建 CCApp 对象,然后启动换算。测试程序如下:
【程序 8.12 之三】testti.py
from ccapp import CCApp
from ti import TextInterface
inter = TextInterface()
cc = CCApp(inter)
cc.run()
以下是测试运行示例,黑体部分是用户输入。结果表明程序的模型部分实现了预期的功 能。
Welcome to Currency Converter! Want to quit? (y/n) n
Choose among {USD,Euro,Yen,Pound}: USD
Convert to RMB? (y/n) y
How much USD? 100
## 100.00 USD ==> 630.60 RMB
Want to quit? (y/n) n
Choose among {USD,Euro,Yen,Pound}: Euro
Convert to RMB? (y/n) n
How much RMB? 10000
## 10000.00 RMB ==> 1208.68 Euro
Want to quit? (y/n) y
Goodbye!
实现 GUI
经过文本界面的测试,如果确信核心部分没有问题,即可转向设计更复杂但更加用户友 好的图形界面。我们要做的是确定图形界面的各种构件及布局,然后编写构件的处理代码。 与文本界面类似,图形界面需要提供的功能在模型设计过程已经确定了,图形界面必须支持 与文本界面相同的方法,另外也许还需要一些辅助方法。
根据模型部分所要求的界面功能来设计图形界面:选择要换算的外币种类,由于每次只 处理一种外币,故可用单选钮实现;输入和显示外币及等价人民币的金额,可用两个录入框 实现;双向换算和退出用三个命令按钮实现。至此即大致确定了图形界面的外观,接下来即 可为构件(主要是命令按钮)实现处理代码。最终得到如下 GUInterface 类定义:
【程序 8.12 之四】gui.py
from Tkinter import * class GUInterface:
def init (self): self.root = Tk()
self.root.title("Currency Converter")
self.qFlag = False # Quit flag
self.fc = StringVar() # foreign currency selected self.fc.set('USD')
self.to = 'RMB' # convert to?
self.amt = 0 # amount to be converted self.aRMB = StringVar() # amount of RMB self.aRMB.set('0.00')
self.aFC = StringVar() # amount of foreign currency self.aFC.set('0.00')
Label(self.root,textvariable=self.fc).grid( row=0,column=0,sticky=W)
Label(self.root,text='RMB').grid( row=0,column=2,sticky=W)
self.e1 = Entry(self.root,textvariable=self.aFC) self.e1.grid(row=1,column=0,rowspan=2)
self.e2 = Entry(self.root,textvariable=self.aRMB) self.e2.grid(row=1,column=2,rowspan=2)
self.b1 = Button(self.root,
text='---->',command=self.toRMB) self.b1.grid(row=1,column=1)
self.b2 = Button(self.root,
text='<----',command=self.toFC) self.b2.grid(row=2,column=1)
self.f = Frame(self.root) self.f.grid(row=3,column=0,columnspan=3) self.r1 = Radiobutton(self.f,text='USD',
variable=self.fc,value='USD') self.r1.grid(row=0,column=0)
self.r2 = Radiobutton(self.f,text='Euro',
variable=self.fc,value='Euro') self.r2.grid(row=0,column=1)
self.r3 = Radiobutton(self.f,text='Yen',
variable=self.fc,value='Yen') self.r3.grid(row=0,column=2)
self.r4 = Radiobutton(self.f,text='Pound',
variable=self.fc,value='Pound') self.r4.grid(row=0,column=3)
self.rate = Button(self.root,text='Update Rates') self.rate.grid(row=4,column=1)
self.qb = Button(self.root,text='Quit',command=self.close) self.qb.grid(row=5,column=1)
def getInfo(self): self.root.mainloop()
return self.qFlag,self.fc.get(),self.to,self.amt
def showInfo(self,r): rStr = "%.2f" % r if self.to == 'RMB':
self.aRMB.set(rStr) else:
self.aFC.set(rStr)
def toRMB(self): self.to = 'RMB'
self.amt = eval(self.aFC.get()) self.root.quit()
def toFC(self):
self.to = self.fc.get()
self.amt = eval(self.aRMB.get()) self.root.quit()
def close(self): self.qFlag = True self.root.quit() self.root.destroy()
这个类中的 getInfo 和 showInfo 是被模型部分调用的方法,用于输入和输出;其他几个 方法都是辅助方法,用来设置输入输出的信息。在此需要解释一下用到的技术性手段:当核 心程序调用界面的 getInfo 方法时,self.root.mainloop 方法使图形界面进入事件循环,从而能 够处理用户在界面上的各种交互事件(如在录入框中输入数据、点击单选钮选择货币、点击 换算按钮等)。当用户点击换算按钮,相应的处理程序 toRMB 和 toFC 在设置有关信息后必 须用 self.root.quit 方法来退出事件循环,从而使 getInfo 方法结束并将控制返回核心部分。
下面我们利用此图形用户界面来实现图形版的汇率换算器。和前面测试文本界面一样, 在主程序中先创建一个图形界面对象,再创建 CCApp 对象,然后启动换算器。程序如下:
【程序 8.12 之五】testgui.py
from ccapp import CCApp
from gui import GUInterface
inter = GUInterface()
cc = CCApp(inter)
cc.run()
执行此程序,在图形界面中选择 Euro,并在 RMB 录入框中输入 10000,最后点击“<----” 按钮,得到的结果如图 8.27 所示:
图 8.27 图形版汇率换算器
从图 8.27 还可看到,我们的图形界面中还有一个前面未提到的按钮 Update Rates,这是 用来更新汇率数据的,但本程序中没有为此按钮编写处理程序,作为练习,读者可以自己试 着完善这个功能①。另外,支持换算的外币种类也很容易扩充。
至此我们完成了一个汇率换算器程序。通过这个程序的设计,我们看到,即使是如此简 单的程序,它的 GUI 设计也相当复杂。一般而言,图形界面由很多构件组成,创建构件并进 行布局是枯燥而繁琐的工作;而为构件编写相应的处理程序通常都比较简单直接。