How to use lambda when connecting PyQt5 buttons to functions

Viewed 533

In my code, when clicking a button (self.longRunningBtn) it connects to a function that takes no parameters and it works fine. When that function is modified to take extra parameters a, b, c i get an error TypeError: argument 1 has unexpected type 'NoneType'. This is then fixed by adding lambda to the call but now the function doesn't seem to run at all and the entire GUI freezes even though it is on a different thread. I would like to be able to click self.longRunningBtn with parameters a, b, c without seeing that error and without the GUI freezing.

Original code without parameters, works perfectly:

from PyQt5.QtCore import QObject, QThread, pyqtSignal
import sys
from time import sleep

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

class Worker(QObject):
    finished = pyqtSignal()
    progress = pyqtSignal(str)

    def run(self):
        for i in range(5):
            sleep(1)
            self.progress.emit(f"{i+1}")
        self.finished.emit()


class Window(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.clicksCount = 0
        self.setupUi()

    def setupUi(self):
        self.setWindowTitle("Freezing GUI")
        self.resize(300, 150)
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        # Create and connect widgets
        self.clicksLabel = QLabel("Counting: 0 clicks", self)
        self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.stepLabel = QLabel("Long-Running Step: 0")
        self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.countBtn = QPushButton("Click me!", self)
        self.countBtn.clicked.connect(self.countClicks)
        self.longRunningBtn = QPushButton("Long-Running Task!", self)
        self.longRunningBtn.clicked.connect(self.runLongTask)
        # Set the layout
        layout = QVBoxLayout()
        layout.addWidget(self.clicksLabel)
        layout.addWidget(self.countBtn)
        layout.addStretch()
        layout.addWidget(self.stepLabel)
        layout.addWidget(self.longRunningBtn)
        self.centralWidget.setLayout(layout)

    def countClicks(self):
        self.clicksCount += 1
        self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks")

    def reportProgress(self, n):
        self.stepLabel.setText(f"Long-Running Step: {n}")

    def runLongTask(self):
        # Step 2: Create a QThread object
        self.thread = QThread()
        # Step 3: Create a worker object
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.worker.progress.connect(self.reportProgress)
        # Step 6: Start the thread
        self.thread.start()

        # Final resets
        self.longRunningBtn.setEnabled(False)
        self.thread.finished.connect(
            lambda: self.longRunningBtn.setEnabled(True)
        )
        self.thread.finished.connect(
            lambda: self.stepLabel.setText("Long-Running Step: 0")
        )

app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())

Code with parameters added:

    def run(self, a, b, c):
        for i in range(5):
            sleep(1)
            self.progress.emit(f"{i+1} {a} {b} {c}")
        self.finished.emit()

        self.thread.started.connect(self.worker.run("alpha", "beta", "charlie"))

returns TypeError: argument 1 has unexpected type 'NoneType'

Fix for TypeError:

        self.thread.started.connect(lambda: self.worker.run("alpha", "beta", "charlie"))

But now the GUI freezes and I can't see anything that the function is doing, I would like it to function exactly as in the original code but with a, b, c attached to the end of the label as well.

1 Answers

The problem is that in the end the lambda is equivalent to invoking the run() method directly in the main thread, unlike the one that signals it to be executed in the receiver thread.

One possible solution is to use functools.partial:

from functools import partial
self.thread.started.connect(partial(self.worker.run, "alpha", "beta", "charlie"))
Related