浅谈开发PyQt5应用的实践

前言

其实在此之前我早就有接触过PyQt4,但是无奈接触的教程实在不够完整,也没有一条线索能循序渐进地学习,因此才放弃了PyQt,转而使用Electron开发GUI应用(学习Electron的路径实在太好了,有个很详细的例子可以参考)。直至到2019年2月某一天偶然发现packt的一门PyQt5视频教程限免,我就领取了并尝试看,然后就再次正式学习PyQt5。课程地址

开发PyQt应用的核心

在接下来的内容里,我将会谈及几个在开发PyQt应用时重点关注的几个要点,部分内容引用自前面所述课程和我的github repo,一个很简陋的记事本。我所使用的Python版本是Python 3.6.6

核心流程

开发一个复杂的PyQt应用必然不应该完全手写GUI部分的代码,无论是调整还是其他的改动都不太方便(如果真的纯手写我直接用Tkinter不就行了吗)。因此用Qt Designer进行UI设计是最好的,Qt Designer需要手动安装:

1
pip install pyqt5-tools

安装完毕后在你的python安装目录里找到这个路径:~\Python36\Lib\site-packages\pyqt5_tools,并运行designer.exe即可打开。但关于如何使用Qt Designer不是本文的要点。

  1. 在启动Qt Designer后就可以进行UI设计,设计完毕保存为.ui文件,实际上这个是xml结构的文件,可以通过pyuic进行转换,转换为python代码。
  2. 把UI转换为python代码后就可以另开一个文件,请参考我前面提到的记事本demo中的main.py,这个是我编写的程序的入口,经过pyuic自动生成的python文件作为程序的UI渲染器,而我所写的逻辑代码则放在main.py中,实现解耦。如果我们把逻辑混在自动生成的代码里,那么一旦需要在UI的设计上做微调,经过pyuic再次转换的话就会丢失前面所写的内容,因此解耦是很重要的一环。
  3. 完成逻辑编写、测试,就可以使用auto-py-to-exe进行Windows平台可执行文件的打包。

关键知识点

Signal & Slot

这个可谓Qt的核心,如果有接触过jQuery,你就会发现这玩意其实和jQuery的事件处理机制差不多。这里贴一段jQuery的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script type="text/javascript" src="/jquery/jquery.js"></script>
<script type="text/javascript">
function onclicked() {
$("p").slideToggle();
}
$(document).ready(function () {
$("button").click(onclicked);
});
</script>
</head>
<body>
<p>这是一个段落。</p>
<button>切换</button>
</body>
</html>

我再贴一段PyQt自动生成的代码:

1
self.actionExit.triggered.connect(MainWindow.close)

可以看到两个代码具有相同的模式:选定对象,检测信号,根据提供的函数名调用函数。

在Qt里,actionExit是sender,被按下的时候就会emit一个triggered signal,使用connect到槽,PyQt这里的槽可以是python函数或者QObject的成员方法。同时你也应该注意到,两个代码都是提供了函数名作为click和connect的参数,若connect的参数是MainWindow.close(),那不用说都会报错。但笔者并不打算在此详细介绍signal & slot,请有需要深入了解的读者自行查阅相关资料。

QThread

如果程序需要执行长时间的任务并且任务与Widgets有交互的话,最好是使用QThread来实现,举个例子,执行一个爬虫并且需要在界面上展示爬取结果的话,就需要使用QThread创建新的线程,然后使用该线程执行相关代码,而如果不用多线程的话,就会阻塞GUI线程直到任务完成,期间用户无法操作GUI,而且Windows可能会提示程序无响应。

所以我们应该继承自QThread然后创建一个类,然后实现run方法,run方法的内容就是我们程序里的耗时任务。

这里引用一个网上的非常straight-forward的例子,情看下面的代码:

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
#!/usr/bin/python3
import sys
import tempfile
import subprocess
from PyQt5 import QtWidgets
from PyQt5.QtCore import QThread, pyqtSignal
from mainwindow import Ui_MainWindow
class CloneThread(QThread):
signal = pyqtSignal('PyQt_PyObject')
def __init__(self):
QThread.__init__(self)
self.git_url = ""
# run method gets called when we start the thread
def run(self):
tmpdir = tempfile.mkdtemp()
cmd = "git clone {0} {1}".format(self.git_url, tmpdir)
subprocess.check_output(cmd.split())
# git clone done, now inform the main thread with the output
self.signal.emit(tmpdir)
class ExampleApp(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(ExampleApp, self).__init__(parent)
self.setupUi(self)
self.pushButton.setText("Git clone with Thread")
# Here we are telling to call git_clone method when
# someone clicks on the pushButton.
self.pushButton.clicked.connect(self.git_clone)
self.git_thread = CloneThread() # This is the thread object
# Connect the signal from the thread to the finished method
self.git_thread.signal.connect(self.finished)
def git_clone(self):
self.git_thread.git_url = self.lineEdit.text() # Get the git URL
self.pushButton.setEnabled(False) # Disables the pushButton
self.textEdit.setText("Started git clone operation.") # Updates the UI
self.git_thread.start() # Finally starts the thread
def finished(self, result):
self.textEdit.setText("Cloned at {0}".format(result)) # Show the output to the user
self.pushButton.setEnabled(True) # Enable the pushButton
def main():
app = QtWidgets.QApplication(sys.argv)
form = ExampleApp()
form.show()
app.exec_()
if __name__ == '__main__':
main()

这个代码的耗时任务是git clone,因此作者继承自QThread并实现了run方法,这里引用Qt官方文档对run()的描述:

void QThread::run()

The starting point for the thread. After calling start(), the newly created thread calls this function. The default implementation simply calls exec().

You can reimplement this function to facilitate advanced thread management. Returning from this method will end the execution of the thread.

See also start() and wait().

同时run的结尾有个emit,当任务执行完毕后将会发送一个信号,并把结果发送出去。

然后看ExampleApp的代码,实例化CloneThread后,把self.git_clone()和clicked事件连接起来,当按钮被clicked之后,就会通过新的线程执行git clone(调用CloneThread实例的start()方法),然后任务完毕把CloneThread实例发出的信号connect到self.finished()上,并作为run()所emit的内容作为finished()的参数执行收尾工作。

最后我们总结一下使用QThread的流程:

1.继承自QThread创建一个类,实现run方法,根据情况选择emit。

2.在GUI上监听事件,实例化步骤一的类,调用start()方法。

3.根据情况选择connect。

GUI代码与逻辑代码的解耦

在“核心流程”一节已经简单介绍过解耦的重要性,这里简单谈谈如何实践和需要关注的问题。首要的步骤是通过Qt Designer设计出你想要的UI,并可以借助style sheet来美化UI,然后把设计导出为.ui文件。设计是非常重要的工作,因此为了减少以后因为UI更改导致已经写好的逻辑代码出问题,我非常建议在用Designer开发UI前就做好设计,禁止在写逻辑代码的阶段再对UI做大幅度修改。

这里补充个坑:而在开发UI阶段要注意objectName字段,比如Designer里创建的默认窗体的菜单栏,如果你删掉了之前写的menuFile子项,你再创建同名子项的话,objectName字段里得到是自动加了后缀(删掉xxx,再创建就会出现xxx_2)的名字,为了减少不必要的麻烦,应该重做UI。

而.ui文件的备份也很重要,本人试过误操作得到了个惨重教训:对.ui文件执行了PyCharm里配置的autopep8工具,导致格式变化无法再被读取和修改,因此UI设计制作完毕,导出后应该做备份。只要UI文件在,就可以对UI进行修改调整,但是一旦丢失就很麻烦了。

备份好.ui文件后,就可以使用pyuic工具把它们转换为python文件,这里贴我的PyCharm设置:

设置

实际上命令是这样的:pyuic5 xxx.ui -x -o xxx.py

-x选项会自动为生成的python代码添加执行入口,即

1
if __name__ == "__main__":

-o指定输出的文件名,不过要注意不要把External Tools用在错误的文件上搞到格式混乱无法挽回就好了。

用pyuic5转换为py代码之后就可以开一个新文件(比如main.py)来写逻辑,以我的代码为例(dialog.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'dialog.ui'
#
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
...

每个pyuic5转换后的代码都有注释警告不要在此文件中写别的内容,同时每个窗体都有一个以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
25
26
27
28
import sys
from PyQt5 import QtWidgets
# when you run you should delete the . before textb
from textb import Ui_MainWindow
from dialog import Ui_Dialog
class MyApp:
def __init__(self):
self.app = QtWidgets.QApplication(sys.argv)
self.main_window = QtWidgets.QMainWindow()
self.ui = Ui_MainWindow()
self.ui.setupUi(self.main_window)
self.dialog = QtWidgets.QDialog()
self.dialog_ui = Ui_Dialog()
self.dialog_ui.setupUi(self.dialog)
self.ui.textEdit.textChanged.connect(self.auto_save)
self.ui.actionOpen.triggered.connect(self.on_open_click)
self.ui.actionSave.triggered.connect(self.on_save_click)
self.ui.actionNew.triggered.connect(self.on_new_click)
self.main_window.show()
sys.exit(self.app.exec_())

这里就导入了Ui_MainWindow和Ui_Dialog并且进行实例化,但是要注意的是,就算mian.py里有多个Ui实例,像

1
self.app = QtWidgets.QApplication(sys.argv)

的代码也只需要写一行,上面的例子里,Dialog被执行setupUi之后并不会直接显示出来,而我们需要这个展示这个dialog的时候就需要执行show()和exec_():

1
2
3
4
5
6
def dialog_alert(self, content):
self.dialog_ui.label.setText(content)
self.dialog_ui.pushButton.clicked.connect(self.dialog.close)
self.dialog.show()
self.dialog.exec_()

这里我们总结一下解耦的原理:

1.设计转换UI为python代码。

2.在主程序入口里import UI转换后的类,实例化并添加逻辑代码,按需显示。

打包exe

虽然这个并不是重点,在Linux环境下直接安装依赖就可以跑起来,但对于我这种常年用Windows的人来说,打包exe肯定比直接在cmder里python xxx.py便捷。这里我觉得auto-py-to-exe这个工具比较好用。

用pip安装后就可以在console里启动这玩意:

转换工具

有趣的是,这玩意的GUI是用一个叫做Eel的库实现的,一个Python+Electron混合程序开发库,有兴趣的读者可以去了解下。

首先是Script Location,以我的代码为例,这里我们选择main.py,即记事本demo的程序入口。

Onefile选项的One Directory是表示打包为一个目录,目录的mian.exe是程序入口;第二个是打包为单个exe文件。就个人感觉,如果你的程序引用了以目录存放的资源(比如图标),或会在相对路径下的进行文件输出,最好是用One Directory,比如我的程序会引用源码包里icon\的文件,如果打包为单个exe,在exe的路径里若没有icon\目录,就无法显示图标。为了组织文件,你肯定需要一个目录把这些东西和其他文件隔离,所以为何不直接打包one directory?还可以做自安装包。

Console Window的第一个选项是程序运行时会带一个cmd黑框,后者则没有,为了美观,一般都是选择后者。

Icon

Icon展开后可以添加ico文件,这个主要是为了打包出来的exe文件的图标,而窗体的icon与此无关,窗体icon需要在Qt Designer里设置。

Addition Files是添加其他依赖资源,比如前面提到的icon\目录,可以用add folder添加,那么在打包出来的包里就会包含icon\目录,记事本demo的窗体icon也能正常显示。

Advance的内容我还没怎么试过,有兴趣的读者可以自行了解。

配置好之后直接按convert .py to .exe即可自动打包,途中应该会报类似的警告:

1
WARNING: lib not found: api-ms-win-crt-heap-l1-1-0.dll dependency of C:\Program Files (x86)\Python\python.exe

我的平台是Win10 1803,实际上打包出来的exe无论在Win7还是Win10上都可以运行,因此直接无视就好了。

总结

这个是我最终做出来的demo:

记事本

说实话,PyQt有Qt非常详细的文档可供参考,只要掌握开发PyQt应用的核心知识,开发GUI应用的难度一点都不比Electron高,甚至更加轻松。我也有意用PyQt重写一个之前用Electron+Vue实现的GUI应用。我还是非常推荐packt那门视频课程的,讲得非常有条理,例子也很好懂,当然Qt的官方文档(重点!是Qt的文档,而非PyQt的文档)也是非常有用,可以去这里看。希望读者看了这篇文章能有所帮助吧。

Qt牛逼!

####################################################################

再推荐一个YouTube大佬的PyQt5系列视频教程

Powered by Hexo and Hexo-theme-hiker

Copyright © 2017 - 2020 HOCHIKONG's WAPORIZer All Rights Reserved.

访客数 : | 访问量 :