您的位置:首页 > 产品设计 > UI/UE

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

2015-07-18 19:49 483 查看

前言

前几天刚学 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_())


执行效果如图:



用另一个文件新建一个类,继承上面的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_())


运行效果如图:



额…和上面并没有什么不同,我们点击一下试试。



不出几秒,窗口就成了这样子。鼠标转几下之后,它又恢复了原样。

产生这个未响应的原因,是我的工作函数
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线程分离
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: