使用PyQt5开发桌面应用

PyQt5工具可以快速实现简单的界面开发,包括界面设计、布局管理以及业务逻辑实现(信号与槽)。 简单说就是使用PyQt5工具可以快速画一个控件摆放整齐、界面整洁有序、布局合理的界面。

安装和配置

首先是按照Python环境和配置pip。官网下载最新版本的Python3安装即可。

pip工具镜像源配置。配置方法如下:

  1. 在cmd窗口下执行echo %HOMEPATH%获取用户家目录,并在该目录下创建pip目录。
  2. 在pip目录下创建pip.ini文件。记住,后缀必须是.ini格式。并在该文件中写入如下内容。
1
2
3
4
[global]
index-url = http://pypi.douban.com/simple
[install]
trusted-host = pypi.douban.com

安装PyQt5:

1
2
pip install PyQt5 -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
pip install PyQt5-tools -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com

最新版designer.exe放在C:\Python\Lib\site-packages\qt5_applications\Qt\bin,把它放到桌面快捷方式上。

PyCharm中PyQt5工具配置

还有一种更推荐方式,就是在PyCharm中集成。打开PyCharm,选择Settings -> Tools -> External Tools,点击左上角的绿色加号。

1)配置Qt Designer,参数配置说明:

  • Name:Qt Designer。方便记忆。实际可以任意取值。
  • Program:designer.exe程序绝对路径。也就是上面安装PyQt5所说的路径,根据实际安装路径填写。
  • Working directory: $FileDir$,固定取值。

2)配置PyUIC。该工具是用于将Qt Designer工具开发完成的.ui文件转化为.py文件。配置打开路径同Qt Designer。参数配置说明:

  • Name:PyUIC。方便记忆。实际可以任意取值。
  • Program:$PyInterpreterDirectory$\python.exe。固定取值。
  • Arguments:-m PyQt5.uic.pyuic $FileName$ -o $FileNameWithoutExtension$_ui.py。固定取值。
  • Working directory: $FileDir$。固定取值。

3)配置Pyrcc,用于PyQt5的资源文件转码。参数配置说明:

  • Name:Pyrcc。方便记忆。实际可以任意取值。
  • Program:pyrcc5.exe的绝对路径比如C:\Python38\Scripts\pyrcc5.exe
  • Arguments:$FileName$ -o $FileNameWithoutExtension$_rc.py。固定取值。
  • Working directory: $FileDir$。固定取值。

退出之前,点击Apply保存配置。配置完成之后,PyCharm中会加入3个工具。 选中项目根目录,右键->External Tool -> QtDesigner,则打开QtDesigner的界面。 img.png

打开后是如下的欢迎界面 img.png

创建新的Form给出了5个模板,其中Widget与Main Window最为常用。这里我们选择创建一个Main Window。 img.png

上面界面的最左侧菜单为Widget Box,Widget Box中包含PyQt5中的所有Widget组件, 我们可以从左侧的Widget Box中拖拽出诸如Button、View和Input等组件到中间的窗口中。

点击Form -> Preview(快捷键为Ctrl+R)则可以预览我们设计好的界面, 也可以用Preview In来选择在相应的主题风格下预览。

我们拖拽一个Label与Button进入主窗口(Main Window)。 img.png

此时在右上角的Object Inspector(对象查看器)中可以看到主窗口中的已放置的对象(label与pushButton)以及其相应地Qt类。 img.png

以Label为例,此时我们点击Main Window中的label或是在Object Inspector中选取label后,查看右侧的一块区域——Property Editor(属性编辑器)。 img.png

其主要包含属性有如下:

名称 含义
objectName 控件对象名称
geometry 相应宽和高与坐标
sizePolicy 控件大小的策略
minimumSize 最小的宽和高
maximumSize 最大的宽和高
font 字体
cursor 光标

最右下角的部分则为Resource Browser(资源浏览器),资源浏览器中可以添加相应地如图片素材,作为Label或Button等控件的背景图片等。

Qt Designer的UI文件

使用Qt Designer设计保存的文件为.ui格式的文件。其实质是一个XML文件。ui文件中存放了在主窗口中的一切控件的相关属性。 使用XML文件来存储UI文件,具有高可读性和移植性,因此我们可以方便地将.ui文件转换到.py文件,从而使得我们可以使用Python语言在设计的GUI上面编程。

将.ui文件转换为.py文件

将.ui文件转换到.py文件很简单,在前面我们曾设置了pyuic5这个工具。如果你没有在PyCharm中设置这个工具, 或者根本没有使用PyCharm,则可以到命令行中使用如下命令实现.ui到.py的转换。

1
pyuic5 - o 目标文件名.py 源文件名.ui

或者直接在PyCharm中,找到.ui文件,右键 打开菜单找到External Tools->PyUIC。点击之后,我们在相应工程目录下会产生一个.py文件。 (注意,.ui文件必须存放在我们的External Tools中设置的相应项目目录下)

转换完成之后,打开.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'demo.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(821, 517)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setGeometry(QtCore.QRect(300, 240, 93, 28))
self.pushButton.setObjectName("pushButton")
self.label = QtWidgets.QLabel(self.centralwidget)
self.label.setGeometry(QtCore.QRect(300, 180, 72, 15))
self.label.setObjectName("label")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 821, 26))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)

self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)

def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.pushButton.setText(_translate("MainWindow", "PushButton"))
self.label.setText(_translate("MainWindow", "TextLabel"))

观察上述文件,可以看到如果不通过Qt Designer来制作界面的话,我们将会一次次地调试程序,来讲按钮和Label等放在合适的位置, 这将是极其痛苦的过程。而通过Qt Designer,我们可以快速地制作UI,并生成Python的代码,从而实现快速地UI的开发。

使用转换的.py文件

然而,此时之间运行这个转换好的Python文件是无法显示任何窗口的。因为这个Python文件只有定义主窗口以及其控件的代码, 并没有程序入口的代码。为了秉持视图与逻辑分离的原则,我们再编写一个新的脚本来调用这个文件,并且创建一个窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
# 下面的demo是转换后的模块名称
from demo import *


class MyWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.setupUi(self)


if __name__ == '__main__':
app = QApplication(sys.argv)
myWin = MyWindow()
myWin.show()
sys.exit(app.exec_())

通过上述代码,我们继承了Ui_MainWindow类,使用其构造方法构造主窗口,并定义了程序的入口,通过创建QApplication对象来创建Qt窗口。

布局管理

Qt Designer中,在工具箱中最上方可以看到有4种布局。分别是垂直布局、水平布局、栅格布局和表单布局。

布局名称 布局含义
垂直(Vertical)布局 布局内的控件按照从上到下的顺序纵向排列
水平(Horizontal) 布局 布局内的控件按照从左到右的顺序横向排列
栅格(Grid)布局 将控件放入栅格中,然后划分成若干行与若干列,并且将每个窗口控件放在合适的单元中
表单(Form)布局 控件以两列布局在表单中,左列包含标签,右列包含输入控件

在Qt Designer中实现布局有两种方式,通过布局管理器进行布局和通过容器控件进行布局。

布局管理器

让我们在左侧的工具箱中随意拖动一些诸如按钮、标签、输入框等控件到主窗口中。 img.png

由于刚才是随意拖拽至主窗口,因此所有控件的排放是乱七八糟的。此时,我们不选中任何控件,在空白处点击右键,找到弹出菜单最下方的Layout布局。 这里我们选择垂直布局,整个主窗口内的所有控件一瞬间都垂直着排列整齐了。 img.png

此时如果需要调整垂直布局的顺序,只需按住待调整的控件,上下拖动即可。但是这样布局是针对整个窗口的, 有时我们需要让不同的布局有父子关系的继承。那么这时就不能单纯地在空白的地方点击右键来布局了。

让我们再次点击右键 -> Lay Out -> Break Lay Out来打开(取消)布局。选中需要水平布局的2个控件, 选中后点击右键,水平布局。再选中另外两个控件,选择水平布局。此时的主窗口应该如图所示: img.png

最后,我们再将两个布局选中,点击右键垂直布局,来排列两个水平布局。 img.png

最后在空白区域再次使用垂直布局。这样即使我们缩放窗口,整个窗口内的控件也会跟着窗口的变化做出相应改变了。 img.png

在上述操作的过程中,我们的一系列操作有决定这些物体的父子关系(层级关系)。而其层级关系在对象查看器中可以直观地看出。 img.png

使用容器进行布局

容器(Container)指的就是能容纳其他子控件的一个控件。使用容器控件可以将容器控件中的所有控件归为一类,从而区别于其他的控件。 当然,正如上文提到过的,使用容器也可以对控件进行布局。 img.png

首先,从左侧的Container中拖出一个Frame控件到主窗口中,再拖出一个label、line input和button到Frame中。 此时Frame中的控件应该是如下图: img.png

但选中Frame控件,点击右键-> Lay Out -> Lay Out Horizontally 则会自动水平排列Frame中的三个控件。 img.png

当我们需要变更Frame的位置的时候,可以直接拖动Frame到相应地位置,这样管理更加方便。使用容器进行布局的实质也是使用容器管理器进行布局的。

绝对布局

我们前面的学习重点放在了布局管理器上面。但是最简单的布局则是之间输入控件的Geometry属性值。 img.png

[!NOTE] 如果之前对这个控件已经设置过了布局管理器,那么这里的绝对布局就不能再设置了。变成了灰色

在属性编辑器中,我们通过修改X Y值来将控件放置在相应地位置,通过修改Width和Height来更改其高度。 让我们通过如下表格来解读一下这个Button的Geometry属性。

名称 含义
X 290 控件的最左上角距离主窗口的左侧290px
Y 140 控件的最左上角距离主窗口的上方140px
Width 93 按钮的宽度为93px
Height 28 按钮的高度为28px

sizePolicy 尺寸策略

在Qt Designer中,控件的尺寸是可以变化的。每个控件都拥有sizeHint和minisizeHint两个尺寸。其一,sizeHint即尺寸提示; 其二,minisizeHint则是最小尺寸。尺寸提示也是控件的期望尺寸,最小尺寸即窗口可以被压缩到的最小的尺寸。 sizePolicy与sizeHint和minisizeHint息息相关。

对于布局管理器中的布局无法满足我们的要求的时候,sizePolicy属性便派上了用场。 sizePolicy可以实现控件的微调。sizePolicy中共有如下几种水平和垂直策略。

策略 中文 含义
Fixed 固定 窗口控件具有sizeHint提示的尺寸且尺寸不变
Minimum 最小 窗口控件的sizeHint提示的尺寸即最小尺寸,窗口控件不能比这个值小,但是可以变大
Maximum 最大 窗口控件的sizeHint提示的尺寸即最大尺寸,窗口控件不能比这个值大,但是可以压缩到minisizeHint的尺寸
Preferred 期望 窗口控件的sizeHint提示的尺寸是期望的尺寸,可以压缩到minisizeHint尺寸,也可以比sizeHint尺寸更大
MinimumExpanding 最小扩展 窗口控件的sizeHint提示的尺寸是最小尺寸,不能缩得比这个尺寸小,但是这个控件期望可以比这个尺寸大
Expanding 扩展 窗口控件可以缩小到minisizeHint的尺寸,也可以比sizeHint的的尺寸大,但期望是更大
Ignored 忽略 无视窗口控件的sizeHint与minisizeHint,按照默认来设置

在sizePolicy的Horizontal Policy和Vertical Policy下面还有Horizontal Stretch和Vertical Stretch两个属性。

信号与槽

信号和槽是PyQt编程对象之间进行通信的机制。每个继承自QWideget的控件都支持信号与槽机制。信号发射时(发送请求), 连接的槽函数就会自动执行(针对请求进行处理)。这一小节主要讲述信号和槽最基本、最经常使用方法。就是内置信号和槽的使用的使用方法。

所谓内置信号与槽的使用。是指在发射信号时,使用窗口控件的函数,而不是自定义的函数。 信号与槽的连接方法是通过QObject.signal.connect将一个QObject的信号连接到另一个QObject的槽函数。

在任何GUI设计中,按钮都是最重要的和常用的触发动作请求的方式,用来与用户进行交互操作。常见的按钮包括QPushButton、QRadioButton和QCheckBox。 这些按钮都继承自QAbstractButton类,QAbstractButton提供的信号包括:

  • Clicked:鼠标左键点击按钮并释放触发该信号。最常用。记住这个就差不多够了。
  • Pressed:鼠标左键按下时触发该信号
  • Released:鼠标左键释放时触发该信号
  • Toggled:控件标记状态发生改变时触发该信号。

这里实现一个点击按钮退出界面需求实现过程来介绍内置信号和内置槽。只需要在init初始化方法中加一句即可

1
self.pushButton.clicked.connect(self.close)

也可以将内置信号连接到自定义槽函数里。将上面的内置self.close修改成自定义方法即可,比如我想要弹出一个消息对话框。

1
2
3
4
5
6
7
8
9
10
from PyQt5.QtWidgets import QMessageBox

class MyMainForm(QMainWindow, Ui_Form):
def __init__(self, parent=None):
super(MyMainForm, self).__init__(parent)
self.setupUi(self)
self.pushButton.clicked.connect(self.showMsg) # 这里连接到自定义槽

def showMsg(self):
QMessageBox.information(self, "信息提示框", "OK,内置信号与自定义槽函数!")

上面是通过代码方式添加,其实也可以在Qt Designer中添加。

Step1:打开Qt Designer界面,找到信号槽编辑区。如下 img.png

发送者选择按钮名称、信号选择clicked,接受者选择MainWindow,槽选择close()。这里貌似只能选择内置槽,所以有局限性。

QThread使用方法

本节主要讲解使用多线程模块QThread解决PyQt界面程序唉执行耗时操作时,程序卡顿出现的无响应以及界面输出无法实时显示的问题。 用户使用工具过程中出现这些问题时会误以为程序出错,从而把程序关闭。这样,导致工具的用户使用体验不好。 下面我们通过模拟上述出现的问题并讲述使用多线程QThread模块解决此类问题的方法。

使用PyQt实现在文本框中每秒打印1个数字。核心代码逻辑如下:

1
2
3
4
5
6
7
8
9
10
class MyMainForm(QMainWindow, Ui_Form):
def __init__(self, parent=None):
super(MyMainForm, self).__init__(parent)
self.setupUi(self)
self.runButton.clicked.connect(self.display)

def display(self):
for i in range(20):
time.sleep(1)
self.listWidget.addItem(str(i))

程序运行过程结果如下(点击Run按钮后界面出现未响应字样,同时程序也没有出现每隔1秒打印1个数字,实际结果是循环结束后20个数字一同展示)。

上述实现的GUI程序都是单线程运行,对于需要执行一个特别耗时的操作时就会出现该问题现象。要解决这种问题可以考虑使用多线程模块QThread。

QThread是Qt的线程类中最核心的底层类。由于PyQt的的跨平台特性,QThread要隐藏所有与平台相关的代码 要使用的QThread开始一个线程, 可以创建它的一个子类,然后覆盖其它QThread.run()函数。

1
2
3
4
5
6
7
8
class Thread(QThread):
def __init__(self):
super(Thread,self).__init__()
def run(self):
pass

thread = Thread()
thread.start()

可以看出,PyQt的线程使用非常简单,建立一个自定义的类(如Thread),自我继承自QThread ,并实现其run()方法即可。 在使用线程时可以直接得到Thread实例,调用其start()函数即可启动线程,线程启动之后,会自动调用其实现的run()的函数,该方法就是线程的执行函数 。

业务的线程任务就写在run()函数中,当run()退出之后线程就基本结束了,QThread有started和finished信号,可以为这两个信号指定槽函数, 在线程启动和结束之时执行一段代码进行资源的初始化和释放操作,更灵活的使用方法是,在自定义的QThread实例中自定义信号, 并将信号连接到指定的槽函数,当满足一定的业务条件时发射此信号。

QThread类中的常用方法:

  • start():启动线程
  • wait():等待线程,直到满足如下条件之一

(1)与此QThread对象关联的线程已完成执行(即从run返回时),如果线程完成执行,此函数返回True,如果线程尚未启动,也返回True

(2)等待时间的单位是毫秒,如果时间是ULONG_MAX(默认值·),则等待,永远不会超时(线程必须从run返回),如果等待超时,此函数将会返回False

  • sleep():强制当前线程睡眠多少秒

QThread类中的常用信号:

  • started:在开始执行run函数之前,从相关线程发射此信号
  • finished:当程序完成业务逻辑时,从相关线程发射此信号

使用QThread重新实现刚刚程序卡顿的问题,先继承QThread类创建多线程,使用主线程更新界面, 使用子线程实时处理数据(重写run()函数,将耗时的操作放入run()函数中),最后将结果显示到界面上。代码如下:

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
# -*- coding: utf-8 -*-

import sys
import time
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QMainWindow
from QThread_Example_UI import Ui_Form

class MyMainForm(QMainWindow, Ui_Form):
def __init__(self, parent=None):
super(MyMainForm, self).__init__(parent)
self.setupUi(self)
# 实例化线程对象
self.work = WorkThread()
self.runButton.clicked.connect(self.execute)

def execute(self):
# 启动线程
self.work.start()
# 线程自定义信号连接的槽函数
self.work.trigger.connect(self.display)

def display(self,str):
# 由于自定义信号时自动传递一个字符串参数,所以在这个槽函数中要接受一个参数
self.listWidget.addItem(str)

class WorkThread(QThread):
# 自定义信号对象。参数str就代表这个信号可以传一个字符串
trigger = pyqtSignal(str)

def __int__(self):
# 初始化函数
super(WorkThread, self).__init__()

def run(self):
#重写线程执行的run函数
#触发自定义信号
for i in range(20):
time.sleep(1)
# 通过自定义信号把待显示的字符串传递给槽函数
self.trigger.emit(str(i))

if __name__ == "__main__":
app = QApplication(sys.argv)
myWin = MyMainForm()
myWin.show()
sys.exit(app.exec_())