Search code examples
pythonpyqtpyqt5qthread

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?


Solution

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