在多线程 PyQT 中更新 GUI 元素

30

我已经研究了一段时间,寻找关于如何使用PyQT编写多线程程序并更新GUI以显示结果的信息。

我习惯于通过示例学习,但是我找不到(是的,我已经找了几周),任何一个使用多线程执行简单任务的简单程序示例,例如连接到www站点列表(5个线程),并只打印处理过的URL及其响应代码。

是否有人可以分享代码或将我引导到一个很好的教程,其中解释了这样的程序?


1
嘿,我还没有尝试过pyQt,但是我在pygtk中使用了多线程。在pygtk中,通常使用gobject来实现。你应该在pyQt中寻找类似的东西。 - Froyo
请参阅以下关于编程的内容:https://dev59.com/bGgu5IYBdhLWcg3wgnZh,https://dev59.com/7GQn5IYBdhLWcg3wY2PK,https://dev59.com/yGw15IYBdhLWcg3wLIo2或https://dev59.com/oWIi5IYBdhLWcg3w_QiG。 - Trilarion
2个回答

56

这里有一些非常基本的例子。

您可以将GUI元素的引用传递给线程,并在线程中更新它们。

import sys
import urllib2

from PyQt4 import QtCore, QtGui


class DownloadThread(QtCore.QThread):
    def __init__(self, url, list_widget):
        QtCore.QThread.__init__(self)
        self.url = url
        self.list_widget = list_widget

    def run(self):
        info = urllib2.urlopen(self.url).info()
        self.list_widget.addItem('%s\n%s' % (self.url, info))


class MainWindow(QtGui.QWidget):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.list_widget = QtGui.QListWidget()
        self.button = QtGui.QPushButton("Start")
        self.button.clicked.connect(self.start_download)
        layout = QtGui.QVBoxLayout()
        layout.addWidget(self.button)
        layout.addWidget(self.list_widget)
        self.setLayout(layout)

    def start_download(self):
        urls = ['http://google.com', 'http://twitter.com', 'http://yandex.ru',
                'http://stackoverflow.com/', 'http://www.youtube.com/']
        self.threads = []
        for url in urls:
            downloader = DownloadThread(url, self.list_widget)
            self.threads.append(downloader)
            downloader.start()

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = MainWindow()
    window.resize(640, 480)
    window.show()
    sys.exit(app.exec_())

编辑注:Qt小部件不是线程安全的,除了主线程外,不应从任何线程访问它们(有关更多详细信息,请参见Qt文档)。使用线程的正确方法是通过信号/槽,正如本答案的第二部分所示。


此外,您还可以使用信号和槽来分离GUI和网络逻辑。
import sys
import urllib2

from PyQt4 import QtCore, QtGui


class DownloadThread(QtCore.QThread):

    data_downloaded = QtCore.pyqtSignal(object)

    def __init__(self, url):
        QtCore.QThread.__init__(self)
        self.url = url

    def run(self):
        info = urllib2.urlopen(self.url).info()
        self.data_downloaded.emit('%s\n%s' % (self.url, info))


class MainWindow(QtGui.QWidget):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.list_widget = QtGui.QListWidget()
        self.button = QtGui.QPushButton("Start")
        self.button.clicked.connect(self.start_download)
        layout = QtGui.QVBoxLayout()
        layout.addWidget(self.button)
        layout.addWidget(self.list_widget)
        self.setLayout(layout)

    def start_download(self):
        urls = ['http://google.com', 'http://twitter.com', 'http://yandex.ru',
                'http://stackoverflow.com/', 'http://www.youtube.com/']
        self.threads = []
        for url in urls:
            downloader = DownloadThread(url)
            downloader.data_downloaded.connect(self.on_data_ready)
            self.threads.append(downloader)
            downloader.start()

    def on_data_ready(self, data):
        print data
        self.list_widget.addItem(unicode(data))


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = MainWindow()
    window.resize(640, 480)
    window.show()
    sys.exit(app.exec_())

8
建议使用信号来分离线程和显示逻辑,点赞。 - Whatang
1
但是从非GUI(主要)线程直接与GUI交互是被禁止的,不是吗? - Winand
3
注意:第一个示例是错误的。您不被允许从线程中调用 QListWidget.addItem()。您必须使用信号(signals)。 - Aaron Digulla
1
是的,这是一个糟糕的答案(抱歉)。如果你这样做,你的程序会随机崩溃。Qt小部件不是线程安全的... - three_pineapples
2
@three_pineapples,感谢您的编辑!我同意有关小部件和线程安全性的观点。我想那段代码之所以能够工作,是因为运行在特定版本上,可能只是侥幸。 - reclosedev
显示剩余11条评论

3

虽然reclosedev的答案对我来说完全有效,但我遇到了一个 QThread:在线程仍在运行时被销毁 的错误。为了纠正这个问题,我按照这个问题中所示,将父类引用传递给QThread构造函数。

import sys
import urllib2

from PyQt4 import QtCore, QtGui


class DownloadThread(QtCore.QThread):

    data_downloaded = QtCore.pyqtSignal(object)

    def __init__(self, parent, url):
        QtCore.QThread.__init__(self, parent)
        self.url = url

    def run(self):
        info = urllib2.urlopen(self.url).info()
        self.data_downloaded.emit('%s\n%s' % (self.url, info))


class MainWindow(QtGui.QWidget):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.list_widget = QtGui.QListWidget()
        self.button = QtGui.QPushButton("Start")
        self.button.clicked.connect(self.start_download)
        layout = QtGui.QVBoxLayout()
        layout.addWidget(self.button)
        layout.addWidget(self.list_widget)
        self.setLayout(layout)

    def start_download(self):
        urls = ['http://google.com', 'http://twitter.com', 'http://yandex.ru',
                'http://stackoverflow.com/', 'http://www.youtube.com/']
        self.threads = []
        for url in urls:
            downloader = DownloadThread(parent=self, url)
            downloader.data_downloaded.connect(self.on_data_ready)
            self.threads.append(downloader)
            downloader.start()

    def on_data_ready(self, data):
        print data
        self.list_widget.addItem(unicode(data))


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = MainWindow()
    window.resize(640, 480)
    window.show()
    sys.exit(app.exec_())

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接