0.小组成员
赵坤2017282110261
黄亦薇201728210260
1.项目Github地址
https://github.com/zkself/homework3
PS:建议使用chrome浏览器
2.预估耗时
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | |
· Estimate | · 估计这个任务需要多少时间 | 10 | |
Development | 开发 | 500 | |
· Analysis | · 需求分析 (包括学习新技术) | 30 | |
· Design Spec | · 生成设计文档 | 30 | |
· Design Review | · 设计复审 (和同事审核设计文档) | 0 | |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | |
· Design | · 具体设计 | 30 | |
· Coding | · 具体编码 | 400 | |
· Code Review | · 代码复审 | 60 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | |
Reporting | 报告 | 120 | |
· Test Report | · 测试报告 | 150 | |
· Size Measurement | · 计算工作量 | 10 | |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | |
合计 | 640 |
3.解题思路
拿到这个问题的时候,我们俩面临的 第一个问题 就是,她没接触过GUI编程,对于GUI编程思想缺乏认识,所以我们决定页面布局让她来写。选择GUI库的时候我们考虑过tkinter、pyqt、wxpython。最后选择了wxpython因为它文档全面并且demo较多。然后我们考虑的 第二个问题 就是,页面上会有很多重复的控件,作用是显示题目、以及让用户输入答案、判定结果。相当于每道题都有3个控件,所有题目都是这样的通过3个控件组成的。所以我就在考虑能不能动态生成控件呢?比如说5道题那么页面上就有1至少5个控件这样会造成代码不简洁,然后就找到了python提供的locals函数,这种动态生成控件(变量)的思想是我在js中学到的,python中正好也提供这种方法。我们面临的 第三个问题 就是计时器,计时器我发现wxpython没有现成的控件,所以必须得自己写,而我们用了一个笨办法就是用while循环的方式计时,时间间隔靠sleep函数,虽然比较笨但是实际上来说,pyhon执行每条代码的时间是很快很快的,基本上是us级别的所以循环一次的时间可以忽略不计。第四个问题 就是如何存储用户的记录,考虑到存储的信息很少且不是那么重要,如果用数据库来存储会增加代码的复杂性,我们认为没有这个必要,最后采取的方式是直接以txt方式存储在用户当前打开的目录下即可。第五个问题 多语言,这也是中期汇报把很多同学卡住的地方。说实话以前确实没接触过,最开始我们也是想实在不行就是用if-else来弄吧,但是我的伙伴说,肯定有聪明的办法,不然那么多大公司在开发多语言软件的时候都是这样if-else的?然后我们就搜了一下资料,发现python有一个模块是专门解决多语言(i18n)问题的,然后这个问题才解决掉。总的来说,这次开发我们遇到的主要是这五个问题。
4.设计实现过程
针对上面遇到的五个重要问题来详细说明一下实现过程。
4.1 页面布局
首先我们知道,一个GUI程序用户体验取决于布局是否合理、美观。而GUI编程本身不是python的强项,并且由于python跨平台的特性,wxpython这个库主要的优点就是能解决跨平台GUI编程问题,其本身无论是开发工具还是API设计都无法和windows平台下的GUI编程语言所媲美。换句话说就是原生界面不好看。它的编程逻辑是,首先在窗体上添加一个frame空间,然后再选择layout布局。且定位问题只能通过开发者自己去尝试,并不能做到c#那样在任何位置拖放控件。说实话这个布局问题确实让我们头疼了一段时间,因为layout的选择直接决定了布局的方式、最后我们选择了最灵活的layout————GirdBagSizer,这种布局可以随意在一行、一列放置任意控件。wxpython的布局逻辑是把frame分成行和列来看的,最后我们调出了比较简明的布局。
4.2 重复控件
这个问题就是解决代码的简洁性。因为,试想一下我们的页面最重要的部件就是一个题目(Label)、提供用户输入(Text)、显示判断结果(Label)由这三种控件组成。如果页面上一次显示5个题目就是15个控件,如果10题目就是30个控件,且如果手写这么多不利于后期的版本升级,意思就是我要从显示5个题改成显示10个题。所以我们没有选择最笨的办法而是借鉴了Js里面的动态生成变量的方式,用for循环去动态生成变量(控件),这里就不得不说脚本语言有脚本语言的好处,如果换成java/c++这种语言这个问题无解。还有这种方式,解决了变量名可以用变量拼接,比如题目一的label就是lanbel1,题目二的label就是label2 。python提供了一个内置的方法locals来以以字典的方式存储这种动态生成的变量,然后我们访问这些变量的时候直接个根据键值对的形式就可以取到了。还有就是事件绑定的问题,由于我们设定的逻辑是用户输入完之后按回车直接出发textbox的回车事件,所以这些变量都是绑定的同一个事件,就是说多个事件源一个事件处理函数。
4.3 计时器
这里又要感谢wxpython了,它里面没有计时的控件,所以没办法只能自己来写了。我们的逻辑很简单,一个while循环里面有个变量每次循环自加1,然后判断是否达到60,如果达到60,分钟变量加1,此变量归0,继续判断,如果分钟变量达到60则小时变量加1,分钟、秒变量归0。然后sleep1秒。虽然这个办法不好,但是我测了一下,每条语句的执行时间是us级别的,所以精度基本不怎么受影响。别的方法肯定有,但是我们没想到。只能用这个折中的办法来代替了。
4.4 用户记录存取
前面分析了我们为什么用存取txt这种方式来解决这个问题。这里我讲一下我们的逻辑,首先进入程序后判断本目录是否有record.txt这个文件,如果有则读取里面的数据显示在页面上,如果没有则新建一个record.txt。当用户选择关闭程序的时候把本次的统计结果保存到文件中。这样如果用户不小心删掉了文件也不会出现bug问题,无非就是记录丢失了。
4.5 多语言
利用Python提供的gettext模块可以解决这个问题。首先我们把需要显示不同语言的文字用_()这种形式做上记号,然后在python目录下的TOOls文件夹下找到i18n文件夹,cmd运行里面的pygettext.py文件会生成一个message.pot文件,用 非记事本 编辑器打开,使用非记事本的原因是win中文版系统默认编码是gb2312,记事本也是,并且改不了。如果你用记事本打开那么不管你最后保存成什么编码都是gb2312。我用的sublime text打开并编辑的。编辑的内容就是以msgid 和msgstr的这种形式来把你需要翻译的字符串,手动翻译成对应的语言,例如 msgid "hello world" ,msgstr "你好世界"。然后保存成.po文件,接着运行此目录下另外一个msgfmt.py文件用刚才保存的文件再生成一个.mo文件。然后把这两个文件移动到项目目录下即可。这样程序运行的时候就能按照你手动翻译的结果把_()标记的字符串转换成对应语言的翻译了。
4.6 程序操作流程图
5.代码说明
页面布局以及控件循环生成
wx.Frame.__init__(self, parent, id=wx.ID_ANY, title=wx.EmptyString, pos=wx.DefaultPosition, size=wx.Size(
600, 500), style=wx.DEFAULT_FRAME_STYLE | wx.TAB_TRAVERSAL)
self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize)
gbSizer1 = wx.GridBagSizer(10, 10)
gbSizer1.SetFlexibleDirection(wx.BOTH)
gbSizer1.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED)
# 生成控件
self.btnStart = wx.Button(self, wx.ID_ANY, _(u"开始答题"))
gbSizer1.Add(self.btnStart, span=(1, 1), pos=(1, 6))
self.labTime = wx.StaticText(self, wx.ID_ANY, _(u"时间:"))
gbSizer1.Add(self.labTime, span=(1, 1), pos=(
2, 0), flag=wx.EXPAND, border=3)
self.labTime1 = wx.StaticText(self, wx.ID_ANY, u"0:0:0")
gbSizer1.Add(self.labTime1, span=(1, 1),
pos=(2, 1), flag=wx.EXPAND, border=3)
self.labRatio = wx.StaticText(self, wx.ID_ANY, u"")
gbSizer1.Add(self.labRatio, span=(1, 1),
pos=(2, 3), flag=wx.EXPAND, border=3)
for i in range(1, 6):#循环生成控件
self.creatvar['self.labQues' +
str(i)] = wx.StaticText(self, wx.ID_ANY, u"")
gbSizer1.Add(self.creatvar['self.labQues' + str(i)], span=(1, 5),
pos=(i + 2, 0), flag=wx.ALIGN_CENTER_VERTICAL, border=3)
self.creatvar['self.texAns' +
str(i)] = wx.TextCtrl(self, i - 1, wx.EmptyString)
gbSizer1.Add(self.creatvar['self.texAns' + str(i)],
pos=(i + 2, 6), flag=wx.ALIGN_CENTER_VERTICAL)
self.creatvar['self.texAns' + str(i)].Disable()
self.creatvar['self.labCor' +
str(i)] = wx.StaticText(self, i + 4, u"")
gbSizer1.Add(self.creatvar['self.labCor' + str(i)],
pos=(i + 2, 7), flag=wx.ALIGN_CENTER_VERTICAL)
self.btnNext = wx.Button(self, wx.ID_ANY, _(u"再来五题"))
gbSizer1.Add(self.btnNext, span=(1, 1), pos=(9, 3))
self.btnPause = wx.Button(self, wx.ID_ANY, _(u"暂停"))
gbSizer1.Add(self.btnPause, span=(1, 1), pos=(9, 6))
self.btnEnd = wx.Button(self, wx.ID_ANY, _(u"结束答题"))
gbSizer1.Add(self.btnEnd, span=(1, 1), pos=(9, 8))
#################################################################
self.SetSizer(gbSizer1)
self.Layout()
事件绑定
self.btnStart.Bind(wx.EVT_BUTTON, self.btnStartOnButtonClick)
for i in range(1, 6):#动态控件绑定事件
self.creatvar['self.texAns' +
str(i)].Bind(wx.EVT_TEXT_ENTER, self.texAnsOnTextEnter)
self.btnNext.Bind(wx.EVT_BUTTON, self.btnNextOnButtonClick)
self.btnPause.Bind(wx.EVT_BUTTON, self.btnPauseOnButtonClick)
self.btnEnd.Bind(wx.EVT_BUTTON, self.btnEndOnButtonClick)
事件处理函数
def btnStartOnButtonClick(self, event):
for i in range(1, 6):
self.creatvar['self.texAns' + str(i)].Enable()
tmp = initFix()
self.creatvar['self.labQues' +
str(i)].SetLabel("".join(printFix(tmp)))
PostfixExp = changeToPostfix(tmp)
self.res.append(PostfixExp)
self.eve.set()
t = threading.Thread(target=self.stopwatch, name='timer')
t.start()
self.btnStart.Disable()
def texAnsOnTextEnter(self, event):
tex = event.GetEventObject()
value = tex.GetValue()
index = tex.GetId()
cor = CalculatePostfix(self.res[index])
if value == str(cor):
self.creatvar['self.labCor' + str(index + 1)].SetLabel(_(u'正确'))
self.ratio[0] += 1
self.ratio[1] += 1
self.labRatio.SetLabel(
_(u'正确|总数') + ':' + str(self.ratio[0]) + '|' + str(self.ratio[1]))
else:
self.creatvar['self.labCor' +
str(index + 1)].SetLabel(_(u'错误,正确答案是') + str(cor))
self.ratio[1] += 1
self.labRatio.SetLabel(
_(u'正确|总数') + ':' + str(self.ratio[0]) + '|' + str(self.ratio[1]))
tex.Disable()
def btnNextOnButtonClick(self, event):
self.res = []
for i in range(1, 6):
tmp = initFix()
self.creatvar['self.labQues' +
str(i)].SetLabel("".join(printFix(tmp)))
PostfixExp = changeToPostfix(tmp)
self.res.append(PostfixExp)
self.creatvar['self.texAns' + str(i)].Enable()
self.creatvar['self.labCor' + str(i)].SetLabel('')
self.creatvar['self.texAns' + str(i)].SetValue('')
def btnPauseOnButtonClick(self, event):
if self.btnPause.GetLabel() == _(u'暂停'):
self.eve.clear()
self.btnPause.SetLabel(_(u'开始'))
else:
self.eve.set()
self.btnPause.SetLabel(_(u'暂停'))
def btnEndOnButtonClick(self, event):
self.eve.clear()
writeFile(regularizeData(self.ratio))
dlg = wx.MessageDialog(None, _(u"您的记录已保存"), _(u"感谢使用"),
wx.OK | wx.ICON_QUESTION)
if dlg.ShowModal() == wx.ID_OK:
self.Close(True)
dlg.Destroy()
计时器函数
def stopwatch(self): # 计时器
second = 0
minute = 0
hour = 0
while 1:
if not self.eve.isSet():#判断eve变量是否为假
continue
second += 1
time.sleep(1)
if second == 60:
minute += 1
second = 0
if minute == 60:
hour += 1
minute = 0
second = 0
ntime = "%d:%d:%d" % (hour, minute, second)
self.labTime1.SetLabel(ntime)
用户记录读写
# 初始化记录
content = readFile()
if content:
self.labRatio.SetLabel(_(u'正确|总数') + ': ' + content)
else:
self.labRatio.SetLabel(_(u'当前无记录'))
#写用户记录
def btnEndOnButtonClick(self, event):
self.eve.clear()
writeFile(regularizeData(self.ratio))
dlg = wx.MessageDialog(None, _(u"您的记录已保存"), _(u"感谢使用"),
wx.OK | wx.ICON_QUESTION)
if dlg.ShowModal() == wx.ID_OK:
self.Close(True)
dlg.Destroy()
def regularizeData(ratio):#把数据变成想要的格式
recoard = str(ratio[0]) + '|' + str(ratio[1])
return recoard
def readFile():#读文件,如果没有文件则生成新的
cupath = os.getcwd()
fpath = cupath + '/recoard.txt'
if os.path.exists(fpath):
f = open(fpath)
content = f.read()
f.close()
return content
else:
f = open(fpath, 'w')
f.close()
return 0
def writeFile(content):#写文件
cupath = os.getcwd()
fpath = cupath + '/recoard.txt'
f = open(fpath, 'w')
f.write(content)
f.close()
多语言
gettext.install('lang', './locale/', unicode=False)
choices = ['简体中文', '繁體中文', 'English']
dialog = wx.SingleChoiceDialog(None, "语言选择", "请选择语言", choices)
if dialog.ShowModal() == wx.ID_OK:
langu = dialog.GetStringSelection()
if langu == u'English':
gettext.translation('lang', './locale/',
languages=['en_US']).install(True)
if langu == u'繁體中文':
gettext.translation('lang', './locale/',
languages=['zh_HK']).install(True)
6.运行结果
6.1 系统界面
首先选择语言
然后进入系统界面(简体中文)
繁体中文
英文
6.2 做题、计时、保存记录
做题
暂定做题
更换题目
结束答题,存储记录
7.合作情况
7.1赵坤
完善了上次作业的随机运算符个数问题、解决了动态生成重复控件功能、编写了基础的文件读写模块、解决了多语言问题、编写了测试用例
我在任务过程中比较急躁多亏了黄亦薇能帮我及时整理思路分析问题。女生做事往往比较仔细,有时候代码里的问题她能看出来而我却需要多次测试才能找到Bug在哪。
7.2黄亦薇
构建了基本的界面布局、编写了计时器功能、编写了显示题目处理用户输入并判断结果功能、使用coverage功能测试代码分支覆盖率
我的代码能力不强,赵坤也交给我了很多编码技巧,多语言问题本来他都要放弃了,然后我鼓励他一起继续找找资料然后才解决的了这个问题。
8.项目小结
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 10 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 500 | 600 |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 30 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 0 | 0 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 30 | 40 |
· Coding | · 具体编码 | 400 | 500 |
· Code Review | · 代码复审 | 60 | 70 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | 120 | 150 |
· Test Report | · 测试报告 | 150 | 180 |
· Size Measurement | · 计算工作量 | 10 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 30 |
合计 | 640 | 770 |
8.1赵坤
我觉得这次我主要掌握了多语言问题的处理方法,以及测试用例到底怎么编写。两个人结对编程最大的好处就是可以交换思路,有时候一个人容易钻牛角尖,并且找bug的时候别人看你的代码和自己看是不一样的,旁观者清当局者迷。但是我们还是没解决括号的问题……因为我们当时不想大改程序结构,因为后面的GUI也是挺需要时间来搞定的。
8.2黄亦薇
这次是赵坤一直在督促我快点写,现在看来要不是他的督促时间根本不够。他对于时间把控方面还是需要我学习的。这次我学到了好多,GUI编程思想、多线程问题等。可以说大的方向都是他把控的我的工作主要是给他提出建议以及帮他分担任务。感谢赵坤对我的帮助,我会继续努力的。