Search code examples
multithreadingqtpyqtsignals-slotsworker

PyQt signals between threads not emitted


I am stuck. It should be easy and I have done it many times using the C++ API of Qt however for some reason several of my signals/slots are not working when I'm doing this in PyQt (I've recently started with the concept of a worker QObject in PyQt). I believe it has to do something with the separate thread I'm emitting my signals to/from.

from PyQt4.QtCore import QThread, QObject, pyqtSignal, pyqtSlot, QTimer
from PyQt4.QtGui import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel

class Slave(QObject):

  countSignal = pyqtSignal(int)

  def __init__(self, parent = None):
    super(Slave, self).__init__()

    self.toggleFlag = False
    self.counter = 0

  @pyqtSlot()
  def work(self):
    if not self.toggleFlag: return

    if self.counter > 10: self.counter = 0
    self.counter += self.counter
    self.countSignal.emit(self.counter)

  @pyqtSlot()
  def toggle(self):
    self.toggleFlag = not self.toggleFlag


class Master(QWidget):

  toggleSignal = pyqtSignal()

  def __init__(self, parent = None):
    super(Master, self).__init__()

    self.initUi()
    self.setupConn()

  def __del__(self):
    self.thread.quit()
    while not self.thread.isFinished(): pass

  def initUi(self):
    layout = QVBoxLayout()
    self.buttonToggleSlave = QPushButton('Start')
    self.labelCounterSlave = QLabel('0')
    layout.addWidget(self.buttonToggleSlave)
    layout.addWidget(self.labelCounterSlave)
    self.setLayout(layout)
    self.show()

  def setupConn(self):
    self.thread = QThread()
    slave = Slave()
    timer = QTimer()
    timer.setInterval(100)

    # Make sure that both objects are removed properly once the thread is terminated
    self.thread.finished.connect(timer.deleteLater)
    self.thread.finished.connect(slave.deleteLater)

    # Connect the button to the toggle slot of this widget
    self.buttonToggleSlave.clicked.connect(self.toggle)
    # Connect widget's toggle signal (emitted from inside widget's toggle slot) to slave's toggle slot
    self.toggleSignal.connect(slave.toggle)

    # Connect timer's timeout signal to slave's work slot
    timer.timeout.connect(slave.work)
    timer.timeout.connect(self.timeout)
    # Connect slave's countSignal signal to widget's viewCounter slot
    slave.countSignal.connect(self.viewCounter)

    # Start timer
    timer.start()
    # Move timer and slave to thread
    timer.moveToThread(self.thread)
    slave.moveToThread(self.thread)

    # Start thread
    self.thread.start()    

  @pyqtSlot(int)
  def viewCounter(self, value):
    print(value)
    self.labelCounterSlave.setText(str(value))

  @pyqtSlot()
  def toggle(self):
    print("Toggle called")
    self.buttonToggleSlave.setText("Halt" if (self.buttonToggleSlave.text() == "Start") else "Start")
    self.toggleSignal.emit()

  @pyqtSlot()
  def timeout(self):
    print("Tick")


if __name__ == "__main__":
    app = QApplication([])
    w = Master()
    w.setStyleSheet('cleanlooks')
    app.exec_()

Following things are not triggered/emitted:

  • timeout() slot of my widget - I added this to see why the timer is not triggering my worker's slot but all I found out is that it doesn't work here either...
  • work() and toggle() slots inside my Slave worker class
  • countSignal - it is never emitted since my widget's viewCounter() slot is never triggered

I have no idea what I'm doing wrong. I have connected the signals and slots, started my timer, moved it along with the worker to my separate thread and started the thread.

Am I missing something here?


Solution

  • There are several issues with the code that are preventing it from working correctly.

    1. As per the documentation, you must start (and stop) a timer from the thread it resides in. You cannot start it from another thread. If you want to have the timer reside in the thread, you should relocate the instantiation code to the Slave object and call timer.start() in a slot connected to the threads started signal. You do need to be careful here though as the Slave.__init__ method is going to still run in the main thread. Alternatively, you could just leave the timer in the main thread.

    2. slave and timer are being garbage collected when setupConn() has finished. Store them as self.slave and self.timer. (Alternatively you should be able to specify a parent for them, but this seems to result in app crashes on exit so it's probably best to stick to storing them as instance attributes).

    3. I assume the line self.counter += self.counter should really be self.counter += 1? Otherwise the counter is never incremented :)