1. 程式人生 > >PyQt 分離UI主執行緒與工作執行緒

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類效果圖

用另一個檔案新建一個類,繼承上面的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執行緒分離