前言

  其实为啥要突然要做个古诗默写软件的桌面软件,而不是做 Maya 相关的插件开发呢?
  我觉得 Maya 相关的插件开发我之前也有一些文章积累在博客当中了。
  当然那些都不是教程,只是个人的开发记录而已,

  其实重点是我上上个月做安卓开发的时候做了一个古诗词Demo
  不过最后没有用上,有些遗憾。
  既然代码都准比好了,将 Java 转换成 Python 应该难度不大的。
  所以就有了用 Python Qt 来重建这个 APP 上实现的功能。

安卓demo演示

  毕竟是桌面级开发,完成代码之后会介绍 pyinstaller 库实现将 py 文件封装成 exe 文件的方法。
  最终的成品实现效果。

PySide 重写效果演示

源码路径: https://github.com/FXTD-ODYSSEY/PoemMaster

开发前的准备

  开发一款应用之前应该要理清楚开发的需求,弄好数据结构和代码存放目录。
  做好这些,开发的时候头脑无杂念,条理更清晰。
  话虽如此,不过我自己还没能完全驾驭到么高的层次,一般做这一步的人都是传说中的架构师。
  我目前还是开发到半路,发现这里需要单独分出一个文件夹做管理,然后统一将代码的路径安排好。

文件目录

  这个目录就是我写完Qt效果之后,整理好的代码层级。
  整理代码层级方便后续的代码维护,提高代码可读性。

Qt Designer 制作界面

  Qt Designer是非常强大的 ui 制作工具。
  活用 Qt Designer 可以让开发效率极大提升。


title.ui


game.ui


dialog.ui

  这些ui文件都在 源码的 ui 文件夹下,大家可以拿下来放到 Qt Designer 下面看是如何实现的。

搭建程序框架

  要运行 Qt 必须创建出 QApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
# -*- coding:utf-8 -*-
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *

def main():
app = QApplication([])
window = QWidget()
window.show()
app.exec_()

if __name__ == '__main__':
main()

代码运行效果

  这样就可以运行一个简单的窗口。
  下面需要将我们的 ui 文件添加到窗口上。
  从一开始的准备的目录可以知道ui文件的处理应该放到 view 目录下生成
  另外为了避免uic编译成py的麻烦,可以写一个 loadUiType 的函数放在 util.py 下

util.py

  这样可以就不用手动编译 ui 文件。
  各个view下可以先将 ui 编译出来。

动态编译文件

  这样我们就可以生成出相关界面。

嵌入界面 & 添加跳转

  下面可以将编译的ui添加到主窗口上。
   main.py 可以适当改写扩展一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding:utf-8 -*-
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *

from view import TitleView

class PoemMaster(QWidget):
def __init__(self):
super(PoemMaster,self).__init__()
self.setWindowTitle(u"古诗词背诵软件")
self.setLayout(QVBoxLayout())
self.layout().setContentsMargins(0,0,0,0)
self.title_view = TitleView()
self.layout().addWidget(self.title_view)

def main():
app = QApplication([])
window = PoemMaster()
window.show()
app.exec_()

if __name__ == '__main__':
main()

代码运行效果

  下面要如何实现界面跳转呢?
  其实思路有很多,我这里的思路是将所有相关的界面添加到主界面的layout中。
  隐藏其他的界面,只保留当前前面,当需要切换的时候,再做相关的可视化处理。
  Stackoverflow 上面有推荐用 stackWidget 来实现切换的,应该也是可行的方案。
  我之前也试过将所有的界面存放到数组中,在需要的时候清空当前页面然后从数组中获取页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# -*- coding:utf-8 -*-
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *

from view import TitleView
from view import GameView

class PoemMaster(QWidget):
def __init__(self):
super(PoemMaster,self).__init__()
self.setWindowTitle(u"古诗词背诵软件")
self.setLayout(QVBoxLayout())
self.layout().setContentsMargins(0,0,0,0)
self.initUI()

def initUI(self):
# Note 添加界面切换的点击事件
self.title_view = TitleView()
self.title_view.Study_BTN.clicked.connect(self.viewNavigator)
self.game_view = GameView()
self.game_view.Back_Button.clicked.connect(self.viewNavigator)
self.game_view.setVisible(False)

self.layout().addWidget(self.title_view)
self.layout().addWidget(self.game_view)

def viewNavigator(self):
self.title_view.setVisible(not self.title_view.isVisible())
self.game_view.setVisible(not self.game_view.isVisible())
# Note 调整窗口大小 (可视化切换会让窗口大小变大)
self.adjustSize()

def main():
app = QApplication([])
window = PoemMaster()
window.show()
app.exec_()

if __name__ == '__main__':
main()

代码运行效果

GameVeiw 主程序编写

  其实其他界面的编写都很简单的,毕竟也么有太复杂的功能。
  下面来重点讲解如何实现 GameView

古诗数据显示

  既然要背古诗,当然需要有古诗的数据。
  一般来说数据应该使用数据库来管理的,这里我稍微偷懒,将数据以文本的形式存储起来了。
  搭建可以在 data 目录下的 struct.py 获取到所有的古诗词数据还有常用汉字列表。
  为什么需要常用汉字列表,我本来也想通过 unicode 的字符偏移获取任意的字符的。
  奈何这个方案出来的大都是生僻字,选择上一点难度都没有,所以我还把常用汉字列表加进去。

  古诗数据并没有按照字典的形式罗列,因此我在 struct.py 中加入了代码处理,让它变成一个带字典的数组数据。
  通过数组可以获取到每一首诗对应的字典,每个字典有 title author body 这三个关键字。

  下面就是通过随机数的方法在数组中随机抽取古诗数据了。
  当然由于这里使用的是 Html 富文本,所以数据还需要处理才可以正常显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# -*- coding:utf-8 -*-
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *


from data import common_chinese_list
from data import poem_data

import os
import random
from util import loadUiType
from util import DIR

UI_PATH = os.path.join(DIR,"ui","game.ui")

form_class , base_class = loadUiType(UI_PATH)

class GameView(base_class,form_class):
def __init__(self):
super(GameView,self).__init__()
self.setupUi(self)

self.question_char = ''

# Note 将按钮添加到数组当中
self.answer_list = []
self.answer_list.append(self.Answer_1)
self.answer_list.append(self.Answer_2)
self.answer_list.append(self.Answer_3)
self.answer_list.append(self.Answer_4)
self.answer_list.append(self.Answer_5)
self.answer_list.append(self.Answer_6)
self.answer_list.append(self.Answer_7)
self.answer_list.append(self.Answer_8)

self.poemDataHandler()

def poemDataHandler(self):
rand = random.randint(0,len(poem_data)-1)

poem_info = poem_data[rand]

body = poem_info['body']
name = poem_info['name']
author = poem_info['author']

# Note 模仿 do while 语句截取字符
while True:
num = random.randint(0,len(body)-1)
self.question_char = body[num]
if self.question_char != '\n':
break

# Note 添加下划线标注
body = body[0:num] + '__' + body[num+1:]
poem = ""
poem += "<p align=\"center\">%s</p>" % name
poem += "<p align=\"center\">%s</p>" % author
for line in body.split("\n"):
poem += "<p align=\"center\">%s</p>" % line

self.Poem_Label.setText(poem)

# Note 循环数组添加点击事件
for answer in self.answer_list:
rand = random.randint(0,len(common_chinese_list)-1)
answer.setText(common_chinese_list[rand])

rand = random.randint(0,len(self.answer_list)-1)
self.answer_list[rand].setText(self.question_char)

代码运行效果

  通过 while 循环 在正确的位置上挖空。
  有了挖空的数据就可以在随机按钮上添加答案了。
  而其他按钮就可以使用常用汉字列表中的数据代替。

添加点击事件判断

  下面添加点击按钮检查答案是否正确的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# -*- coding:utf-8 -*-
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *


from data import common_chinese_list
from data import poem_data

import os
import random
from functools import partial
from util import loadUiType
from util import DIR

UI_PATH = os.path.join(DIR,"ui","game.ui")

form_class , base_class = loadUiType(UI_PATH)

class GameView(base_class,form_class):
def __init__(self):
super(GameView,self).__init__()
self.setupUi(self)

self.score = 0
self.question_char = ''

# Note 将按钮添加到数组当中
self.answer_list = []
self.answer_list.append(self.Answer_1)
self.answer_list.append(self.Answer_2)
self.answer_list.append(self.Answer_3)
self.answer_list.append(self.Answer_4)
self.answer_list.append(self.Answer_5)
self.answer_list.append(self.Answer_6)
self.answer_list.append(self.Answer_7)
self.answer_list.append(self.Answer_8)

# Note 循环数组添加点击事件
for answer in self.answer_list:
answer.clicked.connect(partial(self.checkAnswer,answer))

self.poemDataHandler()

def checkAnswer(self,button):
if button.text() == self.question_char:
QMessageBox.information(self,u"回答正确", u"恭喜你回答正确")
self.score += 1
self.Score_Label.setText(u"学习积分: %s" % self.score)
self.poemDataHandler()
else:
QMessageBox.warning(self,u"回答错误", u"请重新作答")

def poemDataHandler(self):
rand = random.randint(0,len(poem_data)-1)

poem_info = poem_data[rand]

body = poem_info['body']
name = poem_info['name']
author = poem_info['author']

# Note 模仿 do while 语句截取字符
while True:
num = random.randint(0,len(body)-1)
self.question_char = body[num]
if self.question_char != '\n':
break

# Note 添加下划线标注
body = body[0:num] + '__' + body[num+1:]
poem = ""
poem += "<p align=\"center\">%s</p>" % name
poem += "<p align=\"center\">%s</p>" % author
for line in body.split("\n"):
poem += "<p align=\"center\">%s</p>" % line

self.Poem_Label.setText(poem)

# Note 循环数组添加点击事件
for answer in self.answer_list:
rand = random.randint(0,len(common_chinese_list)-1)
answer.setText(common_chinese_list[rand])

rand = random.randint(0,len(self.answer_list)-1)
self.answer_list[rand].setText(self.question_char)

代码运行效果

  其实到这一步就已经完成了古诗的代码逻辑。

创建仿 安卓 弹窗的效果

  上面的弹窗效果比较丑。
  因此我不用 QMessageBox ,可以自己实现一个类似安卓的弹窗。
  如何才能实现弹窗之后,无法影响其他窗口呢?
  我发现 QDialog 有 setModal 方法可以实现这个效果。
  QtDesigner 也有相关的设置选项。
  而 self.setWindowFlags(Qt.FramelessWindowHint) 则可以实现不带边框的窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# -*- coding:utf-8 -*-
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *


import os
from util import loadUiType
from util import DIR

UI_PATH = os.path.join(DIR,"ui","dialog.ui")

form_class , base_class = loadUiType(UI_PATH)

class NormalDialog(base_class,form_class):
def __init__(self,parent):
super(NormalDialog,self).__init__()
self.parent_window = parent
self.setupUi(self)
# Note 不带窗口边框
self.setWindowFlags(Qt.FramelessWindowHint)
self.OK_BTN.clicked.connect(self.close)

def display(self,title,msg):
# Note 设置标题和输出信息
self.Title_Label.setText(title)
self.Message_Label.setText(msg)

self.show()

代码运行效果

  注:代码还要修改为调用 NormalDialog
  效果虽然实现,但是因为背景没变暗,因此看起来很不舒服。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# -*- coding:utf-8 -*-
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *


import os
from util import loadUiType
from util import DIR

UI_PATH = os.path.join(DIR,"ui","dialog.ui")

form_class , base_class = loadUiType(UI_PATH)

class NormalDialog(base_class,form_class):
def __init__(self,parent):
super(NormalDialog,self).__init__()
self.parent_window = parent
self.setupUi(self)
# Note 不带窗口边框
self.setWindowFlags(Qt.FramelessWindowHint)
self.OK_BTN.clicked.connect(self.close)

def display(self,title,msg):
# Note 设置标题和输出信息
self.Title_Label.setText(title)
self.Message_Label.setText(msg)

width = self.parent_window.size().width()
height = self.parent_window.size().height()

# Note 居中显示
centerX = width/2 - self.geometry().width()/2
centerY = height/2 - self.geometry().height()/2
position = self.parent_window.mapToGlobal(QPoint(centerX,centerY))
self.move(position)

# Note 添加灰色遮盖
self.label = QLabel(self.parent_window)
self.label.resize(width, height)
self.label.setStyleSheet('background:rbga(0, 0, 0, 125)')

# Note 显示效果
self.label.show()
self.show()

def close(self):
self.label.close()
return super(NormalDialog,self).close()

  可以添加一个和窗口大小一致的 QLabel 然后用样式添加半透明的黑色,从而实现灰色覆盖在背景的效果。
  当然我的源码是使用 pixmap 来实现的,更加底层一点。

代码运行效果

  以上就是模仿 Android UI 制作的故事背诵软件。

pyinstaller 打包

  如果将源码拿出来运行时很不方便的,不是每一条电脑都有安装python编译器以及相关的运行库
  我们可以借助 pyinstaller 库将 Qt 代码封装成 exe 文件,这样只需要执行 exe 文件就可以打开我们的应用了。

  关于如何打包的方法,网上也有很多教程。
  必须得安装 pyinstaller 库。
  然后就可以在 源代码目录下 使用 pyinstaller main.py 打包写好的脚本了

代码运行效果

  当然我这里打包的 exe 还有坑。
  打开之后界面一闪而过就关闭了

代码运行效果

  这种问题可以用命令行来运行程序,这样程序的问题就会打印在命令行上。

代码运行效果

  报错提示是缺少 ui 文件,因为我们的ui是动态编译的,我们需要将ui文件夹拷贝到程序目录下。
  如果我们的ui文件通过 uic 编译出来的就不存在上述问题。

  再次运行程序还是出现问题了
  提示显示说 PySide2 目录下缺少 plugins/platforms 的文件

代码运行效果

  此处的坑是 PySide2 的问题,不知道为何打包的时候没有将相关的文件夹打包到对应的目录下,需要手动修复这个问题。
  我们可以去到 PySide2 的包路径

代码运行效果

  可以找到 plugins 目录了
  打开目录就有 platforms 文件夹。
  将相关的目录拷贝到程序的启动目录的 PySide2 目录下即可

代码运行效果

  我用 PyQt4 打包就没有这么多问题。
  另外 pyinstaller 打包也有很多属性,可以自定义打包软件的图标,也可以隐藏命令行窗口,甚至可以打包成单个文件。
  大家可以自行百度研究。

总结

  筹备了一个月的 Python Qt 系列教程,到这里就算是完结了。
  自己算是重新复习了一遍已经掌握的Qt知识。
  如果大家觉得有用,不妨在点一下下面 按钮(后面的操作你懂的),你的支持将是我前进的动力↖(^ω^)↗