本文主要介紹Qt中線程類QThread的用法
在這篇文章中,將寫一個獲取熱點新聞的程序,每隔2秒發(fā)送一個關(guān)鍵字,從服務(wù)器獲得與該關(guān)鍵字相關(guān)的一條熱點新聞。
我們的目標是實現(xiàn)以下幾個功能:
- 用戶在輸入框中輸入n個關(guān)鍵字,以英文的逗號,隔開
- 用一個搜索結(jié)果列表來呈現(xiàn)所獲得的新聞標題
- 使用進度條更新已獲得的新聞數(shù)目
- 用戶隨時可以停止獲取數(shù)據(jù)
界面設(shè)計如下圖:
上面是一個關(guān)鍵字輸入框QLineEdit,中間使用QListWidget呈現(xiàn)獲得的數(shù)據(jù),下面是QProgressBar更新進度,最下面有一個停止按鈕和一個開始按鈕。
一、代碼片段
1.新聞獲取部分
我們使用接口,從服務(wù)器獲取數(shù)據(jù)。
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):
#從服務(wù)器獲取數(shù)據(jù)
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 '錯誤數(shù)據(jù)'
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/ target=_blank class=infotextkey>Python', 'php', 'learnpython']):
print(post)#輸出結(jié)果
上面是獲取并處理新聞數(shù)據(jù)的程序。需要注意的是其中 time.sleep(2) ,之所以每次發(fā)送請求要隔兩秒,是因為服務(wù)器出于性能考慮,只允許每2秒發(fā)送一次請求,否則可能會得到錯誤的數(shù)據(jù)。在這里有3個關(guān)鍵字,python、php、learnpython,所以整個過程持續(xù)了大約6秒。
不必在意其中實現(xiàn)的細節(jié),因為本文的重點是線程,而不是獲取數(shù)據(jù)。
【領(lǐng)更多QT學習資料,點擊下方鏈接免費領(lǐng)取↓↓,先碼住不迷路~】
點擊領(lǐng)取→Qt學習資料~
2.基本界面
我們可以在代碼中實現(xiàn)所有控件和布局;也可以用Qt Designer設(shè)計好,然后使用命令 pyuic5 -o yourui.py yourui.ui 生成界面代碼。
在這里,我用的是第一個方法:
def initUI(self):
self.setWindowTitle('QThread Study')
keywordLbl = QLabel('關(guān)鍵字(以逗號,隔開):')
self.keywordEdit = QLineEdit()
hrLayout = QHBoxLayout()
hrLayout.addWidget(keywordLbl)
hrLayout.addWidget(self.keywordEdit)
resultLbl = QLabel('搜索結(jié)果:')
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)
二、未使用多線程
如果沒有使用多線程,你可能會這么做:寫好新聞獲取的代碼、寫好界面代碼,接下來簡單地調(diào)用函數(shù)處理數(shù)據(jù)。這么做可以,但所有工作都在單獨的GUI線程中完成,所以執(zhí)行函數(shù)獲取新聞時,你的程序?qū)?ldquo;凍結(jié)”住。
就像這樣:
- 主線程被鎖住
- 直到程序執(zhí)行結(jié)束,搜索結(jié)果列表才會更新
- 輸入框以及其它界面中的元素都無法使用
- 一旦函數(shù)開始執(zhí)行,就沒法停止獲取數(shù)據(jù)
下面是主要代碼(點擊開始按鈕 - 進入槽函數(shù) - 獲取新聞數(shù)據(jù)):
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('沒有搜索內(nèi)容')
return
self.resultList.clear()
for post in self.get_top_from_subreddits(subreddit_list):
self.resultList.addItem(post)
三、使用多線程
沒有使用多線程將導(dǎo)致程序卡住,體驗很差,下面將使用QThread類重寫我們的代碼。
首先要做的就是寫一個線程,這個線程與之前新聞獲取部分 get_top_post 和 get_top_from_subreddits 做相同的事,每當獲得新數(shù)據(jù)就立即更新界面,而且允許用戶點擊“停止”按鈕停止獲取數(shù)據(jù)。
1.QThread的基本結(jié)構(gòu)
QThread類很簡單,它的整體結(jié)構(gòu)如下:
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
你可以通過給構(gòu)造方法 __init__ 添加參數(shù),將數(shù)據(jù)傳給線程。
在 run 方法中處理你的數(shù)據(jù)。
注意不能直接調(diào)用run方法,而是通過 start 方法間接調(diào)用它,否則界面仍有可能被“凍結(jié)”住。
接下來是使用上面你定義的線程:
self.myThread = YourThreadName()
self.myThread.start()
如此,在run方法中寫的代碼得以執(zhí)行,可以使用像isRunning這樣的方法檢測線程是否正在運行。
你可能會經(jīng)常用到這些QThread的方法: quit 、 start 、 terminate 、 isFinished 、 isRunning 。
還有QThread的這些信號: finished 、 started 、 terminated 。
2.我們的程序
介紹完QThread類,下面回到我們的新聞獲取程序。
我們可以很容易地將獲取新聞的代碼移到QThread類,除了修改run方法,其它地方基本保持原樣。
另一個小的變化是,需要將新聞關(guān)鍵字的列表傳到線程類中,從而在run方法中使用這些關(guān)鍵字。
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 方法是從之前的新聞獲取代碼直接復(fù)制過來的,在run方法中遍歷之前設(shè)置的關(guān)鍵字subreddits。
主界面類:
self.testThread.setSubReddit(subreddit_list)
self.testThread.start()
OK,程序?qū)⒃趩为毜木€程中運行,然后根據(jù)關(guān)鍵字獲取所有熱點新聞。
但是,界面中的元素還沒有得到更新,沒有反饋給用戶,所以我們還需做些什么。
當然,不能簡單地在線程類中這么寫:
self.searchProgBar.setValue(int) ,因為它指向QThread對象,而不是UI對象。
在數(shù)據(jù)處理線程和UI線程之間溝通的正確方法是使用“信號”。
四、信號
數(shù)據(jù)獲取線程在背后運行,主界面線程需要獲得數(shù)據(jù)(比如新聞標題),從而更新界面元素(比如進度條和新聞列表)
下面先講一下Pyqt的信號,它與C++中信號槽連接有所不同。
1.內(nèi)建信號
獲取數(shù)據(jù)結(jié)束之后需要通知用戶,我們將使用一個所有QThread實例都有的信號。
首先寫一個線程結(jié)束后我們想要執(zhí)行的代碼,比如打印一條信息,我們在主界面類中這么寫:
def threadFinished(self):
print('獲取結(jié)束')
接下來是信號的連接,將QThread實例發(fā)出的信號與我們線程結(jié)束后打印信息的函數(shù)連接起來:
self.testThread = GetPostThread()
self.testThread.finished.connect(self.threadFinished)
內(nèi)建信號與槽函數(shù)的連接很直接,自定義信號與之唯一的不同就是,我們首先需要在QThread類中定義一個信號,在主線程中的寫法是一樣的。
所以接下來——
2.自定義信號
想要像內(nèi)建信號一樣使用自定義信號,首先需要定義它們,在QThread類中定義信號:
postSignal = pyqtSignal(str)
注意:定義的信號有一個參數(shù),類型是字符串str。
run方法中處理并獲得數(shù)據(jù),然后通過信號將其發(fā)出:
def run(self):
for subreddit in self.subreddits:
top_post = self._get_top_post(subreddit)
self.postSignal.emit(top_post)
self.sleep(2)
主線程獲得信號,并將它與信號處理函數(shù)(槽函數(shù))相連接:
self.testThread.postSignal.connect(self.getPostSlot)
信號發(fā)出時帶有一個字符串參數(shù)(在這里是新聞的標題),定義信號處理函數(shù)時也設(shè)置一個額外的參數(shù),獲得傳來的字符串:
def getPostSlot(self, top_post):
self.resultList.addItem(top_post)
self.searchProgBar.setValue(self.searchProgBar.value() + 1)
將獲得的新聞標題呈現(xiàn)在列表中,并調(diào)整進度條的數(shù)值。
五、總結(jié)
到此為止,我們已經(jīng)完成所有工作:
- 從新聞網(wǎng)站獲取新聞的線程
- 線程與主線程的連接
- 如何實現(xiàn)自定義信號
- 如何使用內(nèi)建信號
注意:在QThread線程類中處理數(shù)據(jù),通過信號將數(shù)據(jù)發(fā)送到主界面線程,進而更新界面元素
看一下現(xiàn)在界面是怎么樣的吧:
你將看到:
- 每獲得一條新數(shù)據(jù),界面立即更新
- 界面仍然可響應(yīng),比如拖動、改變輸入框內(nèi)容
- 主線程沒有被鎖住
- 隨時可以點擊停止按鈕,停止獲取數(shù)據(jù)