Qt——線程類QThread


本文主要介紹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上獲得程序源碼,有任何問題歡迎指出。

點我進入github


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM