本文主要介紹Qt中線程類QThread的用法,參考(翻譯+修改)了一篇文章:PyQt: Threading Basics Tutorial,雖然使用的是PyQt,但與C++中Qt的用法大同小異,不必太在意語言的差異。
在這篇文章中,我將寫一個獲取熱點新聞的程序(使用新聞網站reddit.com的api),每隔2秒發送一個關鍵字,從服務器獲得與該關鍵字相關的一條熱點新聞。
我們的目標是實現以下幾個功能:
- 用戶在輸入框中輸入n個關鍵字,以英文的逗號,隔開
- 用一個搜索結果列表來呈現所獲得的新聞標題
- 使用進度條更新已獲得的新聞數目
- 用戶隨時可以停止獲取數據
界面設計如下圖:
上面是一個關鍵字輸入框QLineEdit,中間使用QListWidget呈現獲得的數據,下面是QProgressBar更新進度,最下面有一個停止按鈕和一個開始按鈕。
一、代碼片段
1.新聞獲取部分
我們使用接口https://www.reddit.com/r/keyword.json?limit=1從服務器獲取數據。
import json import time import requests agent = 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.8 Safari/537.36' headers = { 'User-Agent': agent } def get_top_post(subreddit): #從服務器獲取數據 url = "https://www.reddit.com/r/{}.json?limit=1".format(subreddit) try: restext = requests.get(url, headers=headers) data = json.loads(restext.text) top_post = data['data']['children'][0]['data'] except Exception as e: print(e) return '錯誤數據' return "'{title}' by {author} in {subreddit}".format(**top_post) def get_top_from_subreddits(subreddits): for subreddit in subreddits: yield get_top_post(subreddit) time.sleep(2) if __name__ == '__main__': for post in get_top_from_subreddits(['python', 'php', 'learnpython']): print(post)#輸出結果
上面是獲取並處理新聞數據的程序。需要注意的是其中 time.sleep(2) ,之所以每次發送請求要隔兩秒,是因為服務器出於性能考慮,只允許每2秒發送一次請求,否則可能會得到錯誤的數據。在這里有3個關鍵字,python、php、learnpython,所以整個過程持續了大約6秒。
不必在意其中實現的細節,因為本文的重點是線程,而不是獲取數據。
2.基本界面
我們可以在代碼中實現所有控件和布局;也可以用Qt Designer設計好,然后使用命令 pyuic5 -o yourui.py yourui.ui 生成界面代碼(具體可以參考該文:點我)。
在這里,我用的是第一個方法:
def initUI(self): self.setWindowTitle('QThread Study') keywordLbl = QLabel('關鍵字(以逗號,隔開):') self.keywordEdit = QLineEdit() hrLayout = QHBoxLayout() hrLayout.addWidget(keywordLbl) hrLayout.addWidget(self.keywordEdit) resultLbl = QLabel('搜索結果:') self.resultList = QListWidget() vrLayout = QVBoxLayout() vrLayout.addWidget(resultLbl) vrLayout.addWidget(self.resultList) self.searchProgBar = QProgressBar() self.searchProgBar.setValue(0) self.stopBtn = QPushButton('停止') self.stopBtn.setEnabled(False) self.startBtn = QPushButton('開始') hrLayout1 = QHBoxLayout() hrLayout1.addWidget(self.stopBtn) hrLayout1.addWidget(self.startBtn) vrLayout1 = QVBoxLayout(self) vrLayout1.addLayout(hrLayout) vrLayout1.addLayout(vrLayout) vrLayout1.addWidget(self.searchProgBar) vrLayout1.addLayout(hrLayout1)
二、未使用多線程
如果沒有使用多線程,你可能會這么做:寫好新聞獲取的代碼、寫好界面代碼,接下來簡單地調用函數處理數據。這么做可以,但所有工作都在單獨的GUI線程中完成,所以執行函數獲取新聞時,你的程序將會被“凍結”住。
就像這樣:
- 主線程被鎖住
- 直到程序執行結束,搜索結果列表才會更新
- 輸入框以及其它界面中的元素都無法使用
- 一旦函數開始執行,就沒法停止獲取數據
下面是主要代碼(點擊開始按鈕 - 進入槽函數 - 獲取新聞數據):
class ThreadTestUI(QWidget): def __init__(self, parent = None): super().__init__(parent) self.initUI() #建立信號槽連接 self.startBtn.clicked.connect(self.startBtnClicked) def startBtnClicked(self): subreddit_list = str(self.keywordEdit.text()).split(',') if subreddit_list == ['']: print('沒有搜索內容') return self.resultList.clear() for post in self.get_top_from_subreddits(subreddit_list): self.resultList.addItem(post)
三、使用多線程
沒有使用多線程將導致程序卡住,體驗很差,下面將使用QThread類重寫我們的代碼。
首先要做的就是寫一個線程,這個線程與之前新聞獲取部分 get_top_post 和 get_top_from_subreddits 做相同的事,每當獲得新數據就立即更新界面,而且允許用戶點擊“停止”按鈕停止獲取數據。
1.QThread的基本結構
QThread類很簡單,它的整體結構如下:
from PyQt4.QtCore import QThread class YourThreadName(QThread): def __init__(self): QThread.__init__(self) def __del__(self): self.wait() def run(self): # your logic here
你可以通過給構造方法 __init__ 添加參數,將數據傳給線程。
在 run 方法中處理你的數據。
注意不能直接調用run方法,而是通過 start 方法間接調用它,否則界面仍有可能被“凍結”住。
接下來是使用上面你定義的線程:
self.myThread = YourThreadName() self.myThread.start()
如此,在run方法中寫的代碼得以執行,可以使用像isRunning這樣的方法檢測線程是否正在運行。
你可能會經常用到這些QThread的方法: quit 、 start 、 terminate 、 isFinished 、 isRunning 。
還有QThread的這些信號: finished 、 started 、 terminated 。
2.我們的程序
介紹完QThread類,下面回到我們的新聞獲取程序。
我們可以很容易地將獲取新聞的代碼移到QThread類,除了修改run方法,其它地方基本保持原樣。
另一個小的變化是,需要將新聞關鍵字的列表傳到線程類中,從而在run方法中使用這些關鍵字。
def setSubReddit(self, subReddit): self.subreddits = subReddit def run(self): for subreddit in self.subreddits: top_post = self._get_top_post(subreddit) self.sleep(2)
_get_top_post 方法是從之前的新聞獲取代碼直接復制過來的,在run方法中遍歷之前設置的關鍵字subreddits。
主界面類:
self.testThread.setSubReddit(subreddit_list) self.testThread.start()
OK,程序將在單獨的線程中運行,然后根據關鍵字獲取所有熱點新聞。
但是,界面中的元素還沒有得到更新,沒有反饋給用戶,所以我們還需做些什么。
當然,不能簡單地在線程類中這么寫: self.searchProgBar.setValue(int) ,因為它指向QThread對象,而不是UI對象。
在數據處理線程和UI線程之間溝通的正確方法是使用“信號”。
四、信號
數據獲取線程在背后運行,主界面線程需要獲得數據(比如新聞標題),從而更新界面元素(比如進度條和新聞列表)
下面先講一下Pyqt的信號,它與C++中信號槽連接有所不同。
1.內建信號
獲取數據結束之后需要通知用戶,我們將使用一個所有QThread實例都有的信號。
首先寫一個線程結束后我們想要執行的代碼,比如打印一條信息,我們在主界面類中這么寫:
def threadFinished(self): print('獲取結束')
接下來是信號的連接,將QThread實例發出的信號與我們線程結束后打印信息的函數連接起來:
self.testThread = GetPostThread() self.testThread.finished.connect(self.threadFinished)
內建信號與槽函數的連接很直接,自定義信號與之唯一的不同就是,我們首先需要在QThread類中定義一個信號,在主線程中的寫法是一樣的。
所以接下來——
2.自定義信號
想要像內建信號一樣使用自定義信號,首先需要定義它們,在QThread類中定義信號:
postSignal = pyqtSignal(str)
注意:定義的信號有一個參數,類型是字符串str。
run方法中處理並獲得數據,然后通過信號將其發出:
def run(self):
for subreddit in self.subreddits:
top_post = self._get_top_post(subreddit)
self.postSignal.emit(top_post)
self.sleep(2)
主線程獲得信號,並將它與信號處理函數(槽函數)相連接:
self.testThread.postSignal.connect(self.getPostSlot)
信號發出時帶有一個字符串參數(在這里是新聞的標題),定義信號處理函數時也設置一個額外的參數,獲得傳來的字符串:
def getPostSlot(self, top_post): self.resultList.addItem(top_post) self.searchProgBar.setValue(self.searchProgBar.value() + 1)
將獲得的新聞標題呈現在列表中,並調整進度條的數值。
五、總結
到此為止,我們已經完成所有工作:
- 從新聞網站獲取新聞的線程
- 線程與主線程的連接
- 如何實現自定義信號
- 如何使用內建信號
注意:在QThread線程類中處理數據,通過信號將數據發送到主界面線程,進而更新界面元素
看一下現在界面是怎么樣的吧:
你將看到:
- 每獲得一條新數據,界面立即更新
- 界面仍然可響應,比如拖動、改變輸入框內容
- 主線程沒有被鎖住
- 隨時可以點擊停止按鈕,停止獲取數據
That's all.您可以在github上獲得程序源碼,有任何問題歡迎指出。