PyQt5 QThread不起作用,GUI仍然冻结。

3

我有这段代码(如果你安装了pyqt5,你应该能够自己运行它):

import sys
import time

from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot


class Worker(QObject):
    def __init__(self):
        super().__init__()
        self.thread = None


class Tab(QObject):
    def __init__(self, _main):
        super().__init__()
        self._main = _main


class WorkerOne(Worker):
    finished = pyqtSignal()

    def __init__(self):
        super().__init__()

    @pyqtSlot(str)
    def print_name(self, name):
        for _ in range(100):
            print("Hello there, {0}!".format(name))
            time.sleep(1)

        self.finished.emit()
        self.thread.quit()


class SomeTabController(Tab):
    def __init__(self, _main):
        super().__init__(_main)
        self.threads = {}

        self._main.button_start_thread.clicked.connect(self.start_thread)

        # Workers
        self.worker1 = WorkerOne()
        #self.worker2 = WorkerTwo()
        #self.worker3 = WorkerThree()
        #self.worker4 = WorkerFour()

    def _threaded_call(self, worker, fn, *args, signals=None, slots=None):
        thread = QThread()
        thread.setObjectName('thread_' + worker.__class__.__name__)

        # store because garbage collection
        self.threads[worker] = thread

        # give worker thread so it can be quit()
        worker.thread = thread

        # objects stay on threads after thread.quit()
        # need to move back to main thread to recycle the same Worker.
        # Error is thrown about Worker having thread (0x0) if you don't do this
        worker.moveToThread(QThread.currentThread())

        # move to newly created thread
        worker.moveToThread(thread)

        # Can now apply cross-thread signals/slots

        #worker.signals.connect(self.slots)
        if signals:
            for signal, slot in signals.items():
                try:
                    signal.disconnect()
                except TypeError:  # Signal has no slots to disconnect
                    pass
                signal.connect(slot)

        #self.signals.connect(worker.slots)
        if slots:
            for slot, signal in slots.items():
                try:
                    signal.disconnect()
                except TypeError:  # Signal has no slots to disconnect
                    pass
                signal.connect(slot)

        thread.started.connect(lambda: fn(*args)) # fn needs to be slot
        thread.start()

    @pyqtSlot()
    def _receive_signal(self):
        print("Signal received.")

    @pyqtSlot(bool)
    def start_thread(self):
        name = "Bob"
        signals = {self.worker1.finished: self._receive_signal}
        self._threaded_call(self.worker1, self.worker1.print_name, name,
                            signals=signals)


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Thread Example")
        form_layout = QVBoxLayout()
        self.setLayout(form_layout)
        self.resize(400, 400)

        self.button_start_thread = QPushButton()
        self.button_start_thread.setText("Start thread.")
        form_layout.addWidget(self.button_start_thread)

        self.controller = SomeTabController(self)


if __name__ == '__main__':
    app = QApplication(sys.argv)

    _main = MainWindow()
    _main.show()

    sys.exit(app.exec_())

然而,WorkerOne 仍然阻塞了我的 GUI 线程,当 WorkerOne.print_name 正在运行时窗口无响应。

最近我一直在研究 QThreads,但根据我所做的研究,我不确定为什么这不起作用。

怎么回事?

2个回答

5

这个问题是由连接lambda方法引起的,因为这个lambda不属于Worker,所以它不会在新线程上运行。解决方法是使用functools.partial

from functools import partial
...
thread.started.connect(partial(fn, *args))

完整代码:

完整的代码:

import sys
import time

from functools import partial

from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot


class Worker(QObject):
    def __init__(self):
        super().__init__()
        self.thread = None


class Tab(QObject):
    def __init__(self, _main):
        super().__init__()
        self._main = _main


class WorkerOne(Worker):
    finished = pyqtSignal()

    def __init__(self):
        super().__init__()

    @pyqtSlot(str)
    def print_name(self, name):
        for _ in range(100):
            print("Hello there, {0}!".format(name))
            time.sleep(1)

        self.finished.emit()
        self.thread.quit()


class SomeTabController(Tab):
    def __init__(self, _main):
        super().__init__(_main)
        self.threads = {}

        self._main.button_start_thread.clicked.connect(self.start_thread)

        # Workers
        self.worker1 = WorkerOne()
        #self.worker2 = WorkerTwo()
        #self.worker3 = WorkerThree()
        #self.worker4 = WorkerFour()

    def _threaded_call(self, worker, fn, *args, signals=None, slots=None):
        thread = QThread()
        thread.setObjectName('thread_' + worker.__class__.__name__)

        # store because garbage collection
        self.threads[worker] = thread

        # give worker thread so it can be quit()
        worker.thread = thread

        # objects stay on threads after thread.quit()
        # need to move back to main thread to recycle the same Worker.
        # Error is thrown about Worker having thread (0x0) if you don't do this
        worker.moveToThread(QThread.currentThread())

        # move to newly created thread
        worker.moveToThread(thread)

        # Can now apply cross-thread signals/slots

        #worker.signals.connect(self.slots)
        if signals:
            for signal, slot in signals.items():
                try:
                    signal.disconnect()
                except TypeError:  # Signal has no slots to disconnect
                    pass
                signal.connect(slot)

        #self.signals.connect(worker.slots)
        if slots:
            for slot, signal in slots.items():
                try:
                    signal.disconnect()
                except TypeError:  # Signal has no slots to disconnect
                    pass
                signal.connect(slot)

        thread.started.connect(partial(fn, *args)) # fn needs to be slot
        thread.start()

    @pyqtSlot()
    def _receive_signal(self):
        print("Signal received.")

    @pyqtSlot(bool)
    def start_thread(self):
        name = "Bob"
        signals = {self.worker1.finished: self._receive_signal}
        self._threaded_call(self.worker1, self.worker1.print_name, name,
                            signals=signals)


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Thread Example")
        form_layout = QVBoxLayout()
        self.setLayout(form_layout)
        self.resize(400, 400)

        self.button_start_thread = QPushButton()
        self.button_start_thread.setText("Start thread.")
        form_layout.addWidget(self.button_start_thread)

        self.controller = SomeTabController(self)


if __name__ == '__main__':
    app = QApplication(sys.argv)

    _main = MainWindow()
    _main.show()

    sys.exit(app.exec_())

哇,那是我最后一个会去找的地方。非常感谢你! - James Lemieux
@eyllanesc 这似乎与我几年前在一个问题中提供的答案相矛盾。你知道我错过了什么吗(为什么在这里使用partial有效而在其他问题中无效)? - three_pineapples
@three_pineapples的partial并没有创建一个函数,它只是扩展了你可以在文档中看到的源代码中的参数。你从哪里得到的partial会创建一个新的方法? - eyllanesc
我不确定,因为我回答那个问题已经有一段时间了。我觉得我的答案是错误的,但它解决了问题。我想我会尝试重新审视它,并看看我哪里出错了。 - three_pineapples

1
为了避免幻灯片功能显示图像时阻塞 GUI,我运行了以下简单代码。
一个后台线程类等待一秒钟,然后向等待的 GUI 发出信号已完成。
class Waiter(QThread):
    result = pyqtSignal(object)

    def __init__(self):
        QtCore.QThread.__init__(self)

    def run(self):
        while self.isRunning:
            self.sleep(1)
            self.result.emit("waited for 1s")

然后在主窗口中,将幻灯片的开始和停止按钮连接到主应用程序的开始和停止方法,并将nextImage函数连接到Waiter线程发出的信号。

    self.actionstopSlideShow.triggered.connect(self.stopSlideShow)
    self.actionslideShowStart.triggered.connect(self.startSlideShow)
    self.waitthread = Waiter()
    self.waitthread.result.connect(self.nextImage)

然后主应用程序有两种方法可以启动和停止时间计时器。

def startSlideShow(self):
    """Start background thread that waits one second,
    on wait result trigger next image
    use thread otherwise gui freezes and stop button cannot be pressed
    """
    self.waitthread.start()

def stopSlideShow(self):
    self.waitthread.terminate()
    self.waitthread.wait()

到目前为止,我在PyQt5中从QThread进行子类化没有遇到任何问题,GUI更改都在主(GUI)线程内处理。


您能详细阐述这个答案与我的问题的相关性吗? - James Lemieux
我的建议是使用QThread作为基类,然后使用self.sleep()而不是time.sleep(),并使用主线程执行.terminate()和.wait()来停止工作线程。 - knobi

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