欢迎访问我的网站,希望内容对您有用,感兴趣的可以加入免费知识星球。

PyQt5系列教程(八)分离UI主线程与耗时线程

PyQt5开发 迷途小书童 4年前 (2019-12-04) 6760次浏览 0个评论

软硬件环境

  • windows 10 64bit
  • PyQt5
  • Anaconda3 with python 3.6.5

前言

在做界面开发时,无论是移动端的Android,还是我们这里讲的PyQt5,经常会有一个界面开发准则,那就是UI主线程与耗时子线程一定要分开,主线程负责刷新界面,耗时操作,如网络交互、磁盘IO等,都应该放在子线程里执行,它们各司其职,保证系统正常运行,提升整体用户体验。

实例代码

首先看下工程目录结构

pyqt5

main.py,这是工程入口文件,它负责创建app

# -*- coding: utf-8 -*-

"""
@author: Xu Gaoxiang
@license: Apache V2
@email: xugx.ai@gmail.com
@site: https://www.xugaoxiang.com
@software: PyCharm
@file: main.py
@time: 2019/1/10 10:50
"""

import sys

from PyQt5.QtWidgets import QApplication

from gui.mainwindow import MainWindow

if __name__ == '__main__':

    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec_())

ui_mainwindow.py,负责界面的绘制,这个文件通过designer图形化工具作图然后使用pyuic工具生成对应的python代码,可以参考之前的文章 https://xugaoxiang.com/2019/12/04/pyqt5-3-qtdesigner/

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file '.\mainwindow.ui'
#
# Created by: PyQt5 UI code generator 5.6
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        MainWindow.setMinimumSize(QtCore.QSize(800, 600))
        MainWindow.setMaximumSize(QtCore.QSize(800, 600))
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.button_ok = QtWidgets.QPushButton(self.centralwidget)
        self.button_ok.setGeometry(QtCore.QRect(260, 220, 230, 140))
        self.button_ok.setMinimumSize(QtCore.QSize(230, 140))
        self.button_ok.setMaximumSize(QtCore.QSize(230, 140))
        font = QtGui.QFont()
        font.setPointSize(50)
        self.button_ok.setFont(font)
        self.button_ok.setFocusPolicy(QtCore.Qt.TabFocus)
        self.button_ok.setObjectName("button_ok")
        MainWindow.setCentralWidget(self.centralwidget)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 23))
        self.menubar.setObjectName("menubar")
        self.menuFile = QtWidgets.QMenu(self.menubar)
        self.menuFile.setObjectName("menuFile")
        self.menuHelp = QtWidgets.QMenu(self.menubar)
        self.menuHelp.setObjectName("menuHelp")
        MainWindow.setMenuBar(self.menubar)
        self.actionExit = QtWidgets.QAction(MainWindow)
        self.actionExit.setObjectName("actionExit")
        self.actionCopy = QtWidgets.QAction(MainWindow)
        self.actionCopy.setObjectName("actionCopy")
        self.actionPaste = QtWidgets.QAction(MainWindow)
        self.actionPaste.setObjectName("actionPaste")
        self.actionCut = QtWidgets.QAction(MainWindow)
        self.actionCut.setObjectName("actionCut")
        self.actionHelp = QtWidgets.QAction(MainWindow)
        self.actionHelp.setObjectName("actionHelp")
        self.actionAbout = QtWidgets.QAction(MainWindow)
        self.actionAbout.setObjectName("actionAbout")
        self.action_query = QtWidgets.QAction(MainWindow)
        self.action_query.setObjectName("action_query")
        self.action_backupDB = QtWidgets.QAction(MainWindow)
        self.action_backupDB.setObjectName("action_backupDB")
        self.action_reset_mac = QtWidgets.QAction(MainWindow)
        self.action_reset_mac.setObjectName("action_reset_mac")
        self.menuFile.addSeparator()
        self.menuFile.addAction(self.actionExit)
        self.menuFile.addSeparator()
        self.menuHelp.addSeparator()
        self.menuHelp.addSeparator()
        self.menuHelp.addAction(self.actionAbout)
        self.menuHelp.addSeparator()
        self.menubar.addAction(self.menuFile.menuAction())
        self.menubar.addAction(self.menuHelp.menuAction())

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "分离UI主线程和工作线程"))
        self.button_ok.setText(_translate("MainWindow", "确定"))
        self.menuFile.setTitle(_translate("MainWindow", "File"))
        self.menuHelp.setTitle(_translate("MainWindow", "Help"))
        self.actionExit.setText(_translate("MainWindow", "退出"))
        self.actionHelp.setText(_translate("MainWindow", "软件使用说明"))
        self.actionAbout.setText(_translate("MainWindow", "关于"))

mainwindow.py,主要负责界面上控件的事件处理

"""
@author: Xu Gaoxiang
@license: Apache V2
@email: xugx.ai@gmail.com
@site: https://www.xugaoxiang.com
@software: PyCharm
@file: mainwindow.py
@time: 2019/1/10 10:49
"""

import time

from PyQt5.QtWidgets import QMainWindow

from gui.ui_mainwindow import *

class MainWindow(QMainWindow, Ui_MainWindow):

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setupUi(self)

        # 绑定点击事件
        self.button_ok.clicked.connect(self.button_start)

    def button_start(self):

        self.button_ok.setChecked(True)
        self.button_ok.setDisabled(True)

        time.sleep(20)

这里我们使用time.sleep(20)来模拟耗时任务,执行python main.py后一会,界面就会出现无响应,假死的现象,等到20秒过后,界面又恢复了正常,用户体验非常差。

pyqt5

其实要解决这个问题,也非常简单。我们将UI主线程中的time.sleep(20)移动到子线程中就可以了。PyQt5中提供了线程类QThread,我们继承它并重写它的run方法,新建一个新的文件threads.py

# -*- coding: utf-8 -*-

"""
@author: Xu Gaoxiang
@license: Apache V2
@email: xugx.ai@gmail.com
@site: https://www.xugaoxiang.com
@software: PyCharm
@file: threads.py
@time: 2019/1/10 11:03
"""

import time

from PyQt5.QtCore import QThread, pyqtSignal

class WorkThread(QThread):

    # 使用信号和UI主线程通讯,参数是发送信号时附带参数的数据类型,可以是str、int、list等
    finishSignal = pyqtSignal(str)

    # 带参数示例
    def __init__(self,  ip, port, parent=None):
        super(WorkThread, self).__init__(parent)

        self.ip = ip
        self.port = port

    def run(self):
        '''
        重写
        '''

        print('=============sleep======ip: {}, port: {}'.format(self.ip, self.port))
        time.sleep(20)

        self.finishSignal.emit('This is a test.')
        return

注意到这里我们使用了pyqtSignal,我们使用它来跟UI主线程通讯,一般用于界面元素的刷新,在子线程的最后,我们发送这个信号。

对应的mainwindow.py,需要进行如下修改


from gui.threads import WorkThread

# 其它部分省略
def button_start(self):

    print('button_start clicked.')

    # 设置按钮不可用
    self.button_ok.setChecked(True)
    self.button_ok.setDisabled(True)

    self.th = WorkThread(ip='192.168.1.1', port=4000)

    # 将线程th的信号finishSignal和UI主线程中的槽函数button_finish进行连接
    self.th.finishSignal.connect(self.button_finish)

    # 启动线程
    self.th.start()

def button_finish(self, msg):

    print('msg: {}'.format(msg))

    # 设置按钮可用
    self.button_ok.setChecked(False)
    self.button_ok.setDisabled(False)

一顿操作之后,再次执行python main.py,界面就再也不会出现No Resonding的提示了,可以在子线程执行过程中可以随意操作界面上的其它控件。

备注

为了便于保存记录,在Github创建了一个工程,地址是: https://github.com/xugaoxiang/learningPyQt5,后续会把所有的代码、文档以及博文链接都放在上面。

喜欢 (0)

您必须 登录 才能发表评论!

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

请关闭 Adblock 等类似浏览器插件,然后刷新页面访问,感谢您的支持!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.