James Lemieux
James Lemieux

Reputation: 770

PyQt5 QThread not working, gui still freezing

I have this code (if you have pyqt5, you should be able to run it yourself):

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_())

However WorkerOne still blocks my GUI thread and the window is non-responsive when WorkerOne.print_name is running.

I have been researching a lot about QThreads recently and I am not sure why this isn't working based on the research I've done.

What gives?

Upvotes: 3

Views: 4666

Answers (2)

knobi
knobi

Reputation: 922

To avoid blocking the gui for a slideshow function showing images i run the following simple code.

A background Thread Class to wait one second, then signal to the gui waiting is finished.

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")

Then in main window connect the slideshow start and stop button to start and stop methods of main app and connect the nextImage function to the signal the Waiter Thread emits.

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

Then two methods of main app allow to start and stop the time ticker

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()

Up to now i have no problems subclassing from QThread in pyqt5, gui changes are all handled inside the main (gui) thread.

Upvotes: 1

eyllanesc
eyllanesc

Reputation: 244282

The problem is caused by the connection with the lambda method since this lambda is not part of the Worker so it does not run on the new thread. The solution is to use functools.partial:

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

Complete Code:

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_())

Upvotes: 5

Related Questions