Search code examples
pythonmultithreadingpyqt5signals-slotsqthread

Sending signals to Thread function in PyQt5 leads to TypeError of decorated slot


I am currently working on a Gui, which counts until a given input number is reached. It should be possible to stop the while-loop during counting and restart it again using buttons without ending the main Gui thread. This already works, but only if the target counting number is fixed in the thread work function, e.g. n = 10 -> counts to 10 and can't be changed to 20 or any other number without closing the Gui and change the number in the main code. I want to use a line edit for number input and send the desired target number to the thread, so that it counts up to that given number at the time I start the counting. However, when starting the Gui (after implementing the signals usually needed to send and get the input value) typing the number in the line edit and pressing the start button, I get a TypeError, stating that the 'decorated slot has no signature compatible with started()' ('started' should connect thread and worker function). Does anyone by chance know where I am doing a mistake or have to change something and/or if there is a workaround for this error?

I am quite desperate (I've been trying to fix this for quite a while and couldn't find any hints in the net...) and any help is greatly appreciated! Best regards and thanks!

This is the complete code with comments:

import sys
import time
from PyQt5.QtWidgets import QPushButton, QMainWindow, QApplication, QLineEdit
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot

class Worker(QObject):

    finished = pyqtSignal()  # signal out to main thread to alert it that work is completed

    def __init__(self):
        super(Worker, self).__init__()
        self.working = True  # flag to control our loop

    @pyqtSlot(int) # should take the sended integer n from signal...
def work(self, n):

        s = 0

        while self.working:

            if s != n: # count until input integer is reached (doesn't work so far)
                print(s)
                s += 1
                time.sleep(0.5)

        self.finished.emit() # alert gui that the loop stopped

class Window(QMainWindow):

    sendnumber = pyqtSignal(int) # send signal (this is how it is usually done...?)

    def __init__(self):

        super(Window, self).__init__()
        self.setGeometry(50, 50, 200, 250)
        self.setWindowTitle("Program")

        self.inputnumber=QLineEdit(self, placeholderText="number")
        self.inputnumber.resize(self.inputnumber.minimumSizeHint())
        self.inputnumber.move(50, 50)

        self.startbtn = QPushButton("Start", self)
        self.startbtn.resize(self.startbtn.minimumSizeHint())
        self.startbtn.move(50, 100)

        self.stopbtn = QPushButton("Stop", self)
        self.stopbtn.resize(self.stopbtn.minimumSizeHint())
        self.stopbtn.move(50, 150)

        self.thread = None
        self.worker = None

        self.startbtn.clicked.connect(self.start_loop)  

    def start_loop(self):

        self.thread = QThread()  # a new thread to run the background tasks in
        self.worker = Worker()  # a new worker to perform those tasks
        self.worker.moveToThread(self.thread)  # move the worker into the thread, do this first before connecting the signals

        self.thread.started.connect(self.worker.work)  # begin worker object loop when the thread starts running
        self.sendnumber.connect(self.worker.work) # connect input number to worker in thread
        # this doesn't work so far and gives a TypeError!

        self.stopbtn.clicked.connect(self.stop_loop)  # stop the loop on the stop button click
        self.worker.finished.connect(self.loop_finished)  # do something in the gui when the worker loop ends
        self.worker.finished.connect(self.thread.quit)  # tell the thread it's time to stop running
        self.worker.finished.connect(self.worker.deleteLater)  # have worker mark itself for deletion
        self.thread.finished.connect(self.thread.deleteLater)  # have thread mark itself for deletion
        # make sure those last two are connected to themselves or you will get random crashes

        self.thread.start()

    def stop_loop(self):

        self.worker.working = False
        # when ready to stop the loop, set the working flag to false

    @pyqtSlot() # as far as I know you need this Slot to send the input to Slot in thread?
    def getnumber(self):

        try:
            n = int(self.inputnumber.text()) # input for the work function in thread
            print('Trying number...')
        except:
            print('Use an integer!')
            return

        self.sendnumber.emit(n) # emit integer signal
        print('Emitting signal to worker...')

    def loop_finished(self):
        # received a callback from the thread that it's completed
        print('Loop finished.')

if __name__ == '__main__':
    def run():

        app = QApplication(sys.argv)
        gui = Window()
        gui.show()
        app.exec_()

    run() 

And here is the console output showing the error:

Traceback (most recent call last):

  File "C:/Users/***/WhileLoopInterruptTest2.py", line 63, in start_loop
    self.thread.started.connect(self.worker.work)

TypeError: decorated slot has no signature compatible with started()

This is all the error says and the Gui has to be closed. I am using Python 3.6.1, Spyder 3.3.1, PyQt5.9.


Solution

  • Replace this line:

    self.thread.started.connect(self.worker.work)
    

    with this:

    self.thread.started.connect(lambda: self.worker.work(10))
    

    You can replace 10 with the value from the text box. Just remember to convert it to an int first.

    Checkout this article for a more detailed explanation.