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_())
執行效果如圖:
用另一個檔案新建一個類,繼承上面的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執行緒分離