前言:
在个人项目中,实现小初高数学出题程序只需要命令行,但在结对编程中需要带图形用户界面,并且用户对象不再是出题老师,而是小初高学生。因为在个人项目中,两人分别使用C++和Python语言来编写的,而C++的图形用户界面感觉晦涩难懂,所有一致决定使用Python来开发结对编程项目,使用的工具为Python自带的Tkinter
结对编程项目要求:
用户:
小学、初中和高中学生。
功能:
1、用户注册功能。用户提供手机号码,点击注册将收到一个注册码,用户可使用该注册码完成注册;
2、用户完成注册后,界面提示设置密码,用户输入两次密码匹配后设置密码成功。密码6-10位,必须含大小写字母和数字。用户在登录状态下可修改密码,输入正确的原密码,再输入两次相同的新密码后修改密码成功;
3、密码设置成功后,跳转到选择界面,界面显示小学、初中和高中三个选项,用户点击其中之一后,提示用户输入需要生成的题目数量;
4、用户输入题目数量后,生成一张试卷(同一张卷子不能有相同题目,题目全部为选择题),界面显示第一题的题干和四个选项,用户选择四个选项中的一个后提交,界面显示第二题,...,直至最后一题;
5、最后一题提交后,界面显示分数,分数根据答对的百分比计算;
6、用户在分数界面可选择退出或继续做题;
7、小初高数学题目要求见个人项目。
项目原型:
在正式动工前,我们首先在纸上画了软件原型图,如下:
其中用红色线将该项目分为三个部分,两人分工编写
第一部分,登陆:
由于未能掌握数据库的处理,在保存账号密码的时候,使用Excel表格实现
在登陆界面只会验证Excel中是否有该手机号,以及手机号对应的密码是否正确,若没有找到手机号,或者找到手机号但密码对不上,则会弹出一个对话框提示手机号或密码输入错误
若没有注册手机号,可以点击“注册账号”按钮进入注册界面。在注册界面里,当点击“注册”按钮后会验证手机号、密码是否满足格式要求,这里使用正则表达式来实现验证功能,然后会验证两次输入的密码是否一致,以及验证码是否与发送到手机号的验证码一致。点击“获取验证码按钮”会发送验证码到对应手机号上,这里使用了腾讯云的免费试用发送短信功能,免费试用100条短信。注册完成后,注册界面会关闭,在登陆界面输入正确的手机号和密码即可登陆。
第二部分,选择:
登陆成功后会进入选择界面,在界面右上角可以修改密码,可以在“小学”,“初中”,“高中”三种难度中选择一种,选择后输入生成题目数量,会验证题目数量是否大于0。
第三部分,做题:
会按照选择的难度和输入题目的数量生成题目,每次生成一道题,生成一道题的代码复用的是C++的,代码如下:
1 //level为0代表难度为小学,1代表初中,2代表高中 2 string GetOneQuestion(int level) 3 { 4 stringstream ss; 5 int bracketNum = 0; //括号的数量,单位为对 6 int operateNum; //操作数 7 bool leftBracket; //判断该操作数前是否有左括号,若有,则在该操作数后不加右括号,因为要避免出现“(?)”的情况 8 bool need = false; //判断是否满足备注要求,初中题目至少一个根号或平方(概率皆为1/2),高中题目至少一个三角函数 9 if (level == 0) 10 { 11 operateNum = 2+rand()%4; //小学题目2~5个操作数 12 } 13 else 14 { 15 operateNum = 1+rand()%5; //初中和高中题目1~5个操作数 16 } 17 18 if (operateNum == 1) 19 { 20 if (level == 1) 21 { 22 if (rand()%2 == 0) 23 { 24 ss << "√" << 1+rand()%100; 25 } 26 else 27 { 28 ss << 1+rand()%100 << "^2"; 29 } 30 } 31 else if (level == 2) 32 { 33 string fun; 34 int num; 35 //tan值要存在 36 do 37 { 38 fun = g_triFun[rand()%3]; 39 num = g_triNum[rand()%46]; 40 } 41 while (fun == "tan(" && (num+90)%180 == 0); 42 ss << fun << num << "°" << ‘)‘; 43 } 44 } 45 else 46 { 47 //生成第一个操作数 48 if (rand()%4 == 0) 49 { 50 ss << ‘(‘; 51 bracketNum++; 52 } 53 54 if (level == 1 && rand()%4 == 0) //根号 55 { 56 need = true; 57 ss <<"√"; 58 } 59 60 if (level == 2 && rand()%4 == 0) //操作数,或者sin(?)cos(?)tan(?) 61 { 62 need = true; 63 string fun; 64 int num; 65 //tan值要存在 66 do 67 { 68 fun = g_triFun[rand()%3]; 69 num = g_triNum[rand()%46]; 70 } 71 while (fun == "tan(" && (num+90)%180 == 0); 72 ss << fun << num << "°" << ‘)‘; 73 } 74 else 75 { 76 ss << 1+rand()%100; 77 } 78 79 if (level == 1 && rand()%4 == 0) //平方 80 { 81 need = true; 82 ss << "^2"; 83 } 84 85 //生成后续运算符与操作数 86 for (int i = 0; i < operateNum; i++) 87 { 88 char c = g_operators[rand()%4]; //运算符 89 ss << c; 90 91 if (level == 1 && (rand()%4 == 0 || (!need && i == operateNum-1 && rand()%2 == 0))) //根号 92 { 93 need = true; 94 ss <<"√"; 95 } 96 97 leftBracket = false; 98 if (rand()%4 == 0 && i != operateNum-1) //左括号 99 { 100 ss << ‘(‘; 101 leftBracket = true; 102 bracketNum++; 103 } 104 105 if (level == 2 && (rand()%4 == 0 || (!need && i == operateNum-1))) //操作数,或者sin(?)cos(?)tan(?) 106 { 107 need = true; 108 string fun; 109 int num; 110 //不能发生除0的情况,并且tan值要存在 111 do 112 { 113 fun = g_triFun[rand()%3]; 114 num = g_triNum[rand()%46]; 115 } 116 while ((c == ‘/‘ && ((fun == "sin(" && num%180 == 0) || (fun == "cos(" && (num+90)%180 == 0))) 117 ||(fun == "tan(" && (num+90)%180 == 0)); 118 ss << fun << num << "°" << ‘)‘; 119 } 120 else 121 { 122 ss << 1+rand()%100; 123 } 124 125 if (level == 1 && rand()%8 == 0) //平方在操作数右边 126 { 127 need = true; 128 ss << "^2"; 129 } 130 if (rand()%4 == 0 && bracketNum > 0 && !leftBracket) //右括号 131 { 132 ss << ‘)‘; 133 bracketNum--; 134 } 135 if (level == 1 && (rand()%8 == 0 || (!need && i == operateNum-1))) //平方有可能在括号右边 136 { 137 need = true; 138 ss << "^2"; 139 } 140 } 141 while (bracketNum--) 142 { 143 ss << ‘)‘; 144 } 145 } 146 147 ss << ‘=‘; 148 if (g_questions.find(ss.str()) != g_questions.end()) 149 { 150 return GetOneQuestion(level); 151 } 152 else 153 { 154 g_questions.insert(ss.str()); 155 return ss.str(); 156 } 157 }
转换为Python代码时会有些不同,因为在个人项目中,只需要考虑出题,而在结对编程中还需要考虑出正确答案与干扰选项。生成题目的流程还是一样的,然后会用到Python的内置函数eval(),函数会把字符串当作正确的表达式来运行,可以借此来获得随机生成的表达式的值,当然前提是在调用eval()函数前要把表达式的根号换成sqrt函数,把圆周率π变成p,其中p=pi,把平方符号换成**2,这样才符合Python的正确表达式形式。通过修改该表达式中某一运算符,分别改成加减乘除,可以得到四个表达式,也就得到四个值,返回四个表达式与四个对应的值。这里没有决定哪个是正确选项,返回之后,程序再生成随机数来决定哪一个表达式是正确答案。
具体Python代码如下:
1 operators = [‘+‘, ‘-‘, ‘*‘, ‘/‘] 2 triFun = [‘sin(‘, ‘cos(‘, ‘tan(‘] 3 triNum = [‘0‘, ‘π/6‘, ‘π/4‘, ‘π/3‘, ‘π/2‘] 4 5 def createOneQuestion(level): 6 question = ‘‘ 7 questions = [] 8 answers = [] 9 p = pi 10 bracketNum = 0 11 need = False 12 if level==0: 13 operateNum = randint(2,5) 14 else: 15 operateNum = randint(1,5) 16 17 if operateNum==1: 18 if level==1: 19 s = set() 20 while (len(s)<4): 21 if randint(0,7)==0: 22 s.add(‘√(‘+str(randint(1,100))+‘)²‘) 23 elif randint(0,6)==0: 24 s.add(‘√(‘+str(randint(1,100))+‘²)‘) 25 elif randint(0,1)>0: 26 s.add(‘√(‘+str(randint(1,100))+‘)‘) 27 else: 28 s.add(str(randint(1,100))+‘²‘) 29 questions = list(s) 30 for i in range(4): 31 answers.append(eval(questions[i].replace(‘√‘,‘sqrt‘).replace(‘²‘,‘**2‘))) 32 else: 33 s = set() 34 while (len(s)<4): 35 fun = triFun[randint(0,2)] 36 num = triNum[randint(0,4)] 37 while (fun==‘tan(‘ and num==‘π/2‘): 38 fun = triFun[randint(0,2)] 39 num = triNum[randint(0,4)] 40 s.add(fun+num+‘)‘) 41 questions = list(s) 42 for i in range(4): 43 answers.append(eval(questions[i].replace(‘π‘,‘p‘))) 44 45 return questions, answers 46 else: 47 #第一个操作数 48 if randint(0,3)==0: 49 question += ‘(‘ 50 bracketNum += 1 51 if level==1 and randint(0,3)==0: 52 need = True 53 question += ‘√(‘ 54 bracketNum += 1 55 56 if level==2 and randint(0,3)==0: 57 need = True 58 fun = triFun[randint(0,2)] 59 num = triNum[randint(0,4)] 60 while (fun==‘tan(‘ and num==‘π/2‘): 61 fun = triFun[randint(0,2)] 62 num = triNum[randint(0,4)] 63 question += fun+num+‘)‘ 64 else: 65 question += str(randint(1,100)) 66 67 if level==1 and randint(0,3)==0: 68 question += ‘²‘ 69 70 #生成后续运算符与操作数 71 indexs = [] 72 for i in range(0,operateNum): 73 c = operators[randint(0,3)] 74 question += c 75 indexs.append(len(question)-1) 76 77 if level==1 and (randint(0,3)==0 or (not need and i==operateNum-1 and randint(0,1)==0)): 78 need = True 79 question += ‘√(‘ 80 bracketNum += 1 81 82 if randint(0,3)==0 and i!=operateNum-1: 83 question += ‘(‘ 84 bracketNum += 1 85 86 if level==2 and (randint(0,3)==0 or (not need and i==operateNum-1)): 87 need = True 88 fun = triFun[randint(0,2)] 89 num = triNum[randint(0,4)] 90 while ((fun==‘tan(‘ and num==‘π/2‘) or (c==‘/‘ and (((fun==‘sin(‘ or fun==‘tan(‘) and num==‘0‘) or (fun==‘cos(‘ and num==‘π/2‘)))): 91 fun = triFun[randint(0,2)] 92 num = triNum[randint(0,4)] 93 question += fun+num+‘)‘ 94 else: 95 question += str(randint(1,100)) 96 97 if level==1 and randint(0,7)==0: 98 need = True 99 question += ‘²‘ 100 if randint(0,3)==0 and bracketNum>0: 101 question += ‘)‘ 102 bracketNum -= 1 103 if level==1 and (randint(0,7)==0 or (not need and i==operateNum-1)): 104 need = True 105 question += ‘²‘ 106 107 question += ‘)‘*bracketNum 108 # print(indexs)#输出题目的运算符的位置 109 index = indexs[randint(0,operateNum-1)] 110 temp = operators.copy() 111 shuffle(temp) 112 for i in range(4): 113 question = question[:index]+temp[i]+question[index+1:] 114 try: 115 if level==0: 116 answers.append(eval(question)) 117 elif level==1: 118 answers.append(eval(question.replace(‘√‘,‘sqrt‘).replace(‘²‘,‘**2‘))) 119 elif level==2: 120 answers.append(eval(question.replace(‘π‘,‘p‘))) 121 except (ZeroDivisionError, ValueError): 122 answers.append(‘不存在‘) 123 questions.append(question) 124 if len(set(answers))<4: 125 # print(‘有相同答案‘) 126 # for i in range(4): 127 # print(questions[i], answers[i]) 128 return createOneQuestion(level) 129 return questions, answers
做题时会记录正确与否,做完题后会按照百分制输出分数。然后可以退出程序或者继续做题。
经验教训:
在开发图形用户界面的时候,先画出一个软件原型图会使得工作事半功倍,编程界面的时候一下子就完成了。但画完原型图还有一个工作要做,因为是分工合作,需要给出互相调用的接口,命名要一致。比如我做了登陆界面,登陆成功要跳转到选择界面,选择界面是队友在搞,这时候需要调用他写的类,类名要一致,这样当我们编好程后直接导入模块就可以进行测试了。
编写用户界面的时候,最好使用类来编写,这样类成员变量就相当于全局变量,类成员函数之间就可以直接使用类成员变量,而不需要global全局变量,也不需要考虑不同函数摆放的位置。一开始我们直接用函数来生成图形用户界面,但遇到了很多bug,换成类来编写逻辑就十分清晰明了。
原文地址:https://www.cnblogs.com/wwht233/p/11587547.html