PyQt 分离UI主线程与工作线程

前言

前几天刚学 PyQt 的图像界面,制作一个小窗口的时候,需要拉取网络验证码,当用户点击已有的验证码的时候,就开始获取下载新的验证码,然后刷新QLabel显示新的验证码。
做出来之后,发现如果网络不通畅,特别是用户密码输入出错时,下载新的验证码图片特别慢,这时的登陆窗口就卡住了,不一会就变成了“未响应”,等了好一会下载完了,程序才恢复响应。
网上找了一下问题的原因,说是UI主线程和工作线程没有分开,使用urllib等库的时候堵塞主线程,系统就将程序判断为未响应了。做法说是耗时的工作要分开线程,要继承QThread类,要重写run函数等等等等,可惜都没有一个具体的例子说明,也就探索了许久。这里给出我的做法,也作为一个自己的笔记。

准备

Python 2.7
PyQt4
sublime text 3

开始

刚开始用PyQt designer做出ui类,然后自己的窗口要么继承要么里面声明ui对象去使用里面的setupUi()函数。我用的是继承,然后调用类内部函数。

ui类代码如下:

# -*- coding: utf-8 -*-
from PyQt4 import QtCore, QtGui

try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    def _fromUtf8(s):
        return s

try:
    _encoding = QtGui.QApplication.UnicodeUTF8
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig)

class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName(_fromUtf8("Dialog"))
        Dialog.resize(400, 300)
        self.pushButton = QtGui.QPushButton(Dialog)
        self.pushButton.setGeometry(QtCore.QRect(150, 160, 112, 34))
        self.pushButton.setObjectName(_fromUtf8("pushButton"))

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

    def retranslateUi(self, Dialog):
        Dialog.setWindowTitle(_translate("Dialog", "Dialog", None))
        self.pushButton.setText(_translate("Dialog", "干大事!", None))


if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    Dialog = QtGui.QDialog()
    ui = Ui_Dialog()
    ui.setupUi(Dialog)
    Dialog.show()
    sys.exit(app.exec_())

执行效果如图:
PyQt 分离UI主线程与工作线程_第1张图片

用另一个文件新建一个类,继承上面的ui类:

# -*- coding: utf-8 -*-
from PyQt4 import QtCore, QtGui
#从 ui.py 文件里 import ui类
from example_ui import Ui_Dialog
import sys
import time

#新建自己的窗口类,继承 QDialog 和 ui类
class MyDialog(QtGui.QDialog,Ui_Dialog):
    def __init__(self, parent=None):
        super(MyDialog, self).__init__(parent)
        #调用内部的 setupUi() ,本身对象作为参数
        self.setupUi(self)
        #连接 QPushButton 的点击信号到槽 BigWork()
        self.pushButton.clicked.connect(self.BigWork)

    def BigWork(self):
        # 干一件大事... 耗时 10s
        time.sleep(10)

if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)
    #新建类对象
    Dialog = MyDialog()
    #显示类对象
    Dialog.show()
    sys.exit(app.exec_())

运行效果如图:
PyQt 分离UI主线程与工作线程_第2张图片
额…和上面并没有什么不同,我们点击一下试试。
PyQt 分离UI主线程与工作线程_第3张图片
不出几秒,窗口就成了这样子。鼠标转几下之后,它又恢复了原样。

产生这个未响应的原因,是我的工作函数BigWork()和ui主线程是同个线程,它干着大事的时候,ui主线程就没有办法刷新自己,因为路被大事堵住了,要等大事做完之后才能刷新,系统就认为这个窗口这么长时间没有刷新肯定挂了,就变成了未响应。而且,这让用户体验也变得非常低,窗口在等待的时候,不仅仅不能点击,连移动窗口都不行,如果等的久了,还可能被用户kill掉,所以,分离工作线程是非常必要的。

PyQt也给我们提供了这么一个类:QThread
通过继承它然后重写里面的 run()函数,就可以很容易的新建一个线程,达到多线程的任务。
我们新建一个py文件,就起名叫做threads.py,代码如下:

# -*- coding: utf-8 -*-
from PyQt4 import QtCore, QtGui
import time

#继承 QThread 类
class BigWorkThread(QtCore.QThread):
    """docstring for BigWorkThread"""
    def __init__(self, parent=None):
        super(BigWorkThread, self).__init__(parent)

    #重写 run() 函数,在里面干大事。
    def run(self):
        #大事
        time.sleep(10)

相当于把BigWork()函数里的任务移到这个run()函数里来做。

要创建新进程也很简单,把原窗口类BigWork()函数改一下就可以了,代码如下:

    def BigWork(self):
        #import 自己的进程类
        from threads import BigWorkThread
        #新建对象
        self.bwThread = BigWorkThread()
        #开始执行run()函数里的内容
        self.bwThread.start()

注意
为什么要将新进程对象声明为私有成员嘞?原因是,如果声明为局部变量,那么BigWork()函数执行完bwThread.start()这一句,也就是最后一句的时候,局部变量将会被销毁,子进程也就被kill了,这时候会报错:“QThread: Destroyed while thread is still running”。
网上有种说法,说可以调用wait()函数等它执行完,但我测试了一下,wait()函数的调用就不能退出主线程函数了…结果还是成了单线程。

高级用法

假如,我现在点一次按钮就干一次大事,但干着一次大事的时候,我不想同时开始干第二次大事,我就要把“干大事”这个按钮变成无效,等干完了第一次再恢复有效。
这时就可以用到信号和槽,子进程有一个信号,连接着主窗口的一个函数,这个函数复制处理“子进程干完活了之后要干什么”这个问题。
(感觉还是单线程呀!然而,这么做就不会出现“未响应”的情况了)

再比如,原来的BigWork()函数需要接受一个参数t,来决定这个大事要干多久,我们就可以把这个参数放到子线程类的构造函数中。

再再比如,我要子线程执行完之后有返回值,就可以把这个返回值放到子进程的信号里,随着信号一起发回。当然,接受这个信号的槽的形参也要做相应的变化。

下面给出一个完整的例子,但不包括ui类的定义。

子进程定义:

# -*- coding: utf-8 -*-
from PyQt4 import QtCore, QtGui
import time

#继承 QThread 类
class BigWorkThread(QtCore.QThread):
    """docstring for BigWorkThread"""
    #声明一个信号,同时返回一个list,同理什么都能返回啦
    finishSignal = QtCore.pyqtSignal(list)
    #构造函数里增加形参
    def __init__(self, t,parent=None):
        super(BigWorkThread, self).__init__(parent)
        #储存参数
        self.t = t

    #重写 run() 函数,在里面干大事。
    def run(self):
        #大事
        time.sleep(self.t)
        #大事干完了,发送一个信号告诉主线程窗口
        self.finishSignal.emit(['hello,','world','!'])

信号声明不能在__init__()函数里,不然会报错:AttributeError: 'PyQt4.QtCore.pyqtSignal' object has no attribute 'emit',具体原因还没想通…

主进程窗口的定义:

# -*- coding: utf-8 -*-
from PyQt4 import QtCore, QtGui
#从 ui.py 文件里 import ui类
from example_ui import Ui_Dialog
import sys
import time

class MyDialog(QtGui.QDialog,Ui_Dialog):
    def __init__(self, parent=None):
        super(MyDialog, self).__init__(parent)
        #调用内部的 setupUi() ,本身对象作为参数
        self.setupUi(self)
        #连接 QPushButton 的点击信号到槽 BigWork()
        self.pushButton.clicked.connect(self.BigWork)

    def BigWork(self):
        #把按钮禁用掉
        self.pushButton.setDisabled(True)
        #import 自己的进程类
        from threads import BigWorkThread
        #新建对象,传入参数
        self.bwThread = BigWorkThread(int(1))
        #连接子进程的信号和槽函数
        self.bwThread.finishSignal.connect(self.BigWorkEnd)
        #开始执行 run() 函数里的内容
        self.bwThread.start()

    #增加形参准备接受返回值 ls
    def BigWorkEnd(self,ls):
        print 'get!'
        #使用传回的返回值
        for word in ls:
            print word,
        #恢复按钮
        self.pushButton.setDisabled(False)

if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)
    #新建类对象
    Dialog = MyDialog()
    #显示类对象
    Dialog.show()
    sys.exit(app.exec_())

嗯就是酱紫了,PyQt的线程分离就是这么简单。

关键词:PyQt4,Python ,多线程,信号槽,ui线程分离

你可能感兴趣的:(python)