欢迎回到我们的 《零基础:100个小案例玩转Python软件开发!》 系列!在前面我们已经学了五节课,大家是否对PyMe开发软件有一定了解和掌握了呢?
在本节课,我将教大家如何开发一款英语教学软件。在这一节课中,涉及到导航控件、语音播放和打包MSI安装包的知识哦!

一、界面设计
1.1 启动界面
我们先在PyMe中创建一个空工程EnglishStudy,然后将当前窗口界面Form_1的属性布局方式设置为"自动适应"(旧版本称为"打包排布"),然后从左边的控件工具条拖动一个导航控件Navigation和一个容器控件Frame到Form_1中。

选中Navigation_1,设置其布局方式也为"自动适应",居上停靠,横向填充,高度60像素,这时导航条就会在60像素高度空间横向占据顶部空间。

导航条Navigation是一种用于页面导航的按钮栏,我们经常在各类网站上看到它。

在PyMe中,可以通过右边的属性栏设置导航条的方向、对齐方式和每个选项的背景色、文字颜色、背景图等,感兴趣的小伙伴可以尝试设置观察不同的效果。

用鼠标右键在导航条Navigation_1单击,从弹出菜单里选择"标题选项编辑",这时会弹出导航栏编辑区对话框,我们创建两个选项:
-
"听力模式":根据英语语音选择单词选项,选项值0。
-
"翻译模式":根据英语单词选择中文翻译,选项值1。

Frame控件是容器控件,也就是用来放置其它控件的控件,使用Frame可以方便的将属于同一部分功能或区域的控件放在一起,方便我们管控。选中Frame_1,同样设置使用"自动适应"布局方式,但设为居下停靠,四周填充,这样设置后Frame控件将填充Form_1中余下的整个空间。
这样界面就做好了,我们稍后再来处理逻辑部分。
1.2 开始学习界面
在下方"文件和资源栏"用鼠标右键单击,新建一个Home界面,在生成的新界面里摆放一些图文和按钮,作为学习英语的开始界面。右上角放置了一个用于设置任务量的Frame控件,并在其中加入了设置输入框SpinBox和完成设置的按钮,并设置Frame_1的显示属性为"隐藏",我们让它开始状态看不见,点击按钮"设置任务量"时再显示。

1.3 听音练习界面
在下方"文件和资源栏"用鼠标右键单击,新建一个ListenMode界面,放置一个标题Label、一个播放声音的按钮和一个跳过当前单词的按钮。并在中间再放置用于点选的A、B、C、D四个选项的按钮和下方的学习进度条。

二、功能逻辑实现
2.1 启动界面逻辑
双击启动界面文件,进入到界面设计器,这个界面需要做两件事:
-
为导航栏设置功能。
-
加载开始学习界面。
我们可以在导航栏上用鼠标右键单击,在弹出菜单中选择"事件响应"菜单项,然后在弹出对话框中左边选择ItemSelect事件,右边点击"编辑函数代码"来绑定选项变化时的回调函数。

在Navigation_1_onItemSelect函数中,我们可以通过参数itemValue来获取设置的值0或1,然后根据这个值来做判断以加载对应界面。但在当前项目中,我们并没有为每个选项创建一个单独的界面,而是统一使用Home界面,只是记录一下选项值到全局变量中。
python
#Form 'Form_1's Load Event :
def Form_1_onLoad(uiName):
#学习模式
Fun.G_UserVarDict['mode']="0"
#将学习单词的配置信息存入PyMe的Fun函数库中的用户字典中
#learn字段代表总的单词数量
#do代表学完的单词数量,也就是进度
Fun.G_UserVarDict['wordConfig']={"learn":3,"do":0}
Fun.LoadUIDialog(uiName,'Frame_1','Home',None,ignoreSameUI=False)
#Navigation 'Navigation_1's ItemSelect Event :
def Navigation_1_onItemSelect(uiName,widgetName,itemText,itemValue):
Fun.G_UserVarDict['mode']=itemValue
Fun.LoadUIDialog(uiName,'Frame_1','Home',None,ignoreSameUI=False)
2.2 开始学习界面
在开始界面用鼠标右键单击"学习"按钮,在弹出菜单里选择"事件响应"菜单项,在弹出的对话框中选择单击事件Command,并在右边选择"控件中加载界面",然后选择将ListenMode界面加载到Form_1上,这样可以实现换界面的效果。

然后就是为"设置任务量"按钮和"完成设置"按钮绑定点击事件函数,在"设置任务量"按钮的单击事件函数中,将Frame_1面板设置显示。在"完成设置"按钮的单击事件函数中,获取SpinBox_1的值并记录和刷新,具体代码如下:
python
#显示当前任务信息
def show_task():
#当前要学习的单词数量
Fun.SetText(uiName,'Label_6',str(Fun.G_UserVarDict['wordConfig']["learn"]))
#当前进度
Fun.SetText(uiName,'Label_7',str(Fun.G_UserVarDict['wordConfig']["do"]))
#Form 'Form_1's Event :Load
def Form_1_onLoad(uiName):
#如果有进度,按钮显示为"继续学习"
if Fun.G_UserVarDict['wordConfig']["do"]>0:
Fun.SetText(uiName,'Button_1',"继续学习")
else:
Fun.SetText(uiName,'Button_1',"学习")
show_task()
#Button 'Button_1' 'sCommand Event :
def Button_1_onCommand(uiName,widgetName):
#在Form_1上嵌入其它界面,等于将当前页面更换为ListenMode
Fun.LoadUIDialog(uiName,"Form_1","ListenMode",ignoreSameUI=False)
#Button 'Button_set' 's Command Event :
def Button_set_onCommand(uiName,widgetName):
#打开设置面板
Fun.SetElementVisible(uiName,'Frame_1',True)
#Button 'Button_3' 's Command Event :
def Button_3_onCommand(uiName,widgetName):
#获取任务量并记录到Fun函数库中的自定义变量单词配置信息中
Fun.G_UserVarDict['wordConfig']["learn"]= int(Fun.GetText(uiName,'SpinBox_1'))
#刷新任务
show_task()
#隐藏设置面板Frame_1
Fun.SetElementVisible(uiName,'Frame_1',False)
2.3单词处理与语音播放
在听音练习界面,要实现单词语音播放和界面显示与选中的逻辑。首先我们需要创建一个单词的EXCEL表,在A、B两列放置单词和对应的中文解释:

有了这个EXCEL表,我们封装一个用于处理单词读取与语音播放的模块文件vocabularyUtil.py来读取和播放语音以方便使用:
python
import configparser
from openpyxl import load_workbook
from datetime import datetime
import random
#单词工具类
class WordUtil:
def __init__(self):
# 配置文件
self.local_ini = configparser.ConfigParser()
self.local_ini.read('local.ini', encoding='utf-8')
# 把单词和翻译从EXCEL表V.xlsx中读取
workbook_V = load_workbook(filename='V.xlsx')
# 取得第一个Sheet
self.sheet_V = workbook_V.worksheets[0]
self.max_row = 893
self.V_position = self.local_ini['Last_read']['position']
def getWord(self,index):
#取得对应索引的单词
count_next = str(int(self.V_position) + index)
cur_v = self.sheet_V['A' + count_next].value
cur_n=self.sheet_V['B' + count_next].value
return cur_v,cur_n
def getWordSalt(self,index):
#取得三个错误的单词信息
cur_v,cur_n=self.getWord(index)
salt_indices = self.salt(index)
# TODO:返回json格式{"word":"","options":[{"word_zh":"","word_en":"",""},{}]}
# 获取随机单词及其翻译
options = []
for idx in salt_indices:
word_en, word_zh= self.getWord(idx)
options.append({"word_zh": word_zh, "word_en": word_en})
# 构造结果字典
result = {
"cur_n": cur_n,
"cur_v": cur_v,
"options": options
}
# 返回 JSON 格式的字符串
return json.dumps(result, ensure_ascii=False)
#保存记录
def saveRecord(self,count):
self.local_ini.set('Last_read', 'time', str(datetime.now()))
self.local_ini.set('Last_read', 'position', str(int(self.V_position) + count))
self.local_ini.write(open('local.ini', 'w'))
#清除记录
def clearRecord(self):
self.local_ini.set("Last_read", "time", str(datetime.now()))
self.local_ini.set("Last_read", "position", "1")
self.local_ini.write(open("local.ini", "w"))
print("clear record")
#从集合中随机选择 count 个不包含 exclude_index 的索引
def salt(self, exclude_index, count=3):
indices = list(range(1,exclude_index) )+ list(range(exclude_index+1,self.max_row-int(self.V_position)+1))
selected_indices = list(set(random.sample(indices, count)))
return selected_indices
#根据模式取得选项信息:当前单词、对应的四个选项值、正确选项
def getOption(self,mode, index):
word_en_list = []
word_zh_list = []
cur_v,cur_n=self.getWord(index)
salt_indices = self.salt(index)
for idx in salt_indices:
word_en, word_zh= self.getWord(idx)
word_en_list.append(word_en)
word_zh_list.append(word_zh)
# 在 word_en_list 的一个随机位置插入新元素
answer_index = random.randint(0, len(word_zh_list)) # 生成一个随机索引
word_en_list.insert(answer_index, cur_v)
word_zh_list.insert(answer_index, cur_n)
if int(mode)==0:
return cur_v, word_en_list, answer_index
else:
return cur_v, word_zh_list, answer_index
单词语音播放的功能,我们也可以写成一个函数放置在这个文件中。这里用到pyttsx3。pyttsx3 是一个 Python 文本转语音(TTS)库,支持离线运行和跨平台使用(Windows/macOS/Linux),通过调用系统原生语音引擎实现高效语音合成。可以通过pip install pyttsx3进行安装。
python
import pyttsx3
import threading
# 创建一个全局的语音引擎实例
engine = pyttsx3.init()
# 创建一个线程锁
engine_lock = threading.Lock()
def run_in_thread(word):
with engine_lock:
#语音引擎调用say将单词进入播放队列
engine.say(word)
#语音引擎调用runAndWait来启动播放
engine.runAndWait()
def listen(word):
# 创建并启动新线程播放声音
threading.Thread(target=run_in_thread, args=(word,)).start()
完成这个文件后,我们就可以完成听音练习了。
2.3 听音练习界面
返回听音练习界面,引入WordUtil和listen函数,在Form_1_onLoad函数中创建WordUtil对象,并显示选项,在显示选项函数show_option函数中,根据模式来获取选项信息并设置到按钮选项上。
python
#coding=utf-8
import sys
import os
from os.path import abspath, dirname
sys.path.insert(0,abspath(dirname(__file__)))
import tkinter
from tkinter import *
import Fun
uiName="ListenMode"
ElementBGArray={}
ElementBGArray_Resize={}
ElementBGArray_IM={}
from vocabularyUtil import WordUtil,listen
import threading
#显示选项
def show_options(index):
global cur_v,answer_index
#如果是听音模式
if Fun.G_UserVarDict['mode'] =='0':
Fun.SetElementVisible(uiName,'Label_v',False)
Fun.SetElementEnable(uiName,'Label_v',False)
Fun.SetElementVisible(uiName,'Button_play',False)
Fun.SetElementEnable(uiName,'Button_play',False)
else:
#翻译模式
Fun.SetElementVisible(uiName,'Label_v',True)
Fun.SetElementEnable(uiName,'Label_v',True)
Fun.SetElementVisible(uiName,'Button_play',True)
Fun.SetElementEnable(uiName,'Button_play',True)
#取得四个选项信息并设置显示
cur_v,word_list,answer_index=wordUtil.getOption(Fun.G_UserVarDict['mode'],index)
Fun.SetText(uiName,'Label_v',cur_v)
Fun.SetText(uiName,'Button_A',word_list[0])
Fun.SetText(uiName,'Button_B',word_list[1])
Fun.SetText(uiName,'Button_C',word_list[2])
Fun.SetText(uiName,'Button_D',word_list[3])
Fun.SetTKAttrib(uiName,'Button_A',"bg","#FFFFFF")
Fun.SetTKAttrib(uiName,'Button_B',"bg","#FFFFFF")
Fun.SetTKAttrib(uiName,'Button_C',"bg","#FFFFFF")
Fun.SetTKAttrib(uiName,'Button_D',"bg","#FFFFFF")
#显示当前选中按钮的结果
def show_result(index,widgetName):
if answer_index != index:
#如果选项不对
Fun.SetTKAttrib(uiName,widgetName,"bg","#F56C6C")
else:
#如果选项正确
Fun.SetTKAttrib(uiName,widgetName,"bg","#67C23A")
#更新进度条
updateProgressBar()
#显示下一个按钮
Fun.SetElementVisible(uiName,'Button_6',True)
Fun.SetElementEnable(uiName,'Button_6',True)
Fun.SetImage(uiName,'Button_6',r"Resources\右箭头.png",True)
#更新进度条
def updateProgressBar():
Fun.SetCurrentValue(uiName,'Progress_1', Fun.GetCurrentValue(uiName,'Progress_1')+1)
text=str(Fun.GetCurrentValue(uiName,'Progress_1'))+"/"+str(Fun.G_UserVarDict['wordConfig']["learn"])
Fun.SetText(uiName,'Label_2',text)
#跳转到下一个单词
def getNext():
#隐藏下一个按钮
Fun.SetElementVisible(uiName,'Button_6',False)
Fun.SetElementEnable(uiName,'Button_6',False)
global count
count=count+1
if count < Fun.G_UserVarDict['wordConfig']["learn"]:
show_options(count)
listen(cur_v)
else:
Fun.LoadUIDialog("EnglishStudyApp",'Frame_1','Success',None,ignoreSameUI=False)
Fun.G_UserVarDict['wordConfig']["do"] += count
wordUtil.saveRecord(count)
#Form 'Form_1's Event :Load
def Form_1_onLoad(uiName):
#单词处理对象
global wordUtil
#当前单词和正确选项索引
global cur_v,answer_index
#当前练习过的单词数
global count
Fun.SetProgress(uiName,'Progress_1',Fun.G_UserVarDict['wordConfig']["learn"],0)
text="0/"+str(Fun.G_UserVarDict['wordConfig']["learn"])
Fun.SetText(uiName,'Label_2',text)
count=0
wordUtil=WordUtil()
show_options(count)
listen(cur_v)
写完这个界面代码,项目就完成啦!

运行的效果还是挺不错的!

三、打包发布
练习软件开发完成后,我们如果想让它发布给朋友们使用,最好还是做成完装包显得更加专业和友好,在PyMe中新加入了cx_Freeze打包模式,选择cx_Freeze后,勾选"打包成MSI安装包",即可一键生成MSI安装程序。

打包成MSI文件后,就可以进行安装使用啦!~

结语
在这一节里,我们学习了导航栏工具条的使用,也掌握了如何使用pyttsx3进行语音播放和打包MSI。学会这些之后,你将可以制作一些语音类小工具。