Search code examples
pythonmultithreadingqtpyqtqthread

How to call widget's method from QThread


The code runs but prints out the error: QObject::setParent: Cannot set parent, new parent is in a different thread.

What could be a reason?

enter image description here

import Queue, threading
from PyQt4 import QtGui, QtCore
app = QtGui.QApplication([])    

class MessageBox(QtGui.QMessageBox):
    def __init__(self, parent=None):
        QtGui.QMessageBox.__init__(self, parent)     

    def showMessage(self):
        self.setText('Completed')
        self.show()

class Thread(QtCore.QThread):
    def __init__(self, queue, parent=None):
        QtCore.QThread.__init__(self, parent)     
        self.queue=queue        

    def run(self):    
        while True:
            number=self.queue.get()
            result = self.process(number)
            messagebox.showMessage()
            self.queue.task_done()

    def process(self, number):
        timer = QtCore.QTimer()
        for i in range(number):
            print 'processing: %s'%i
            QtCore.QThread.sleep(1)        
        return True    

messagebox = MessageBox()

queue = Queue.Queue()
thread = Thread(queue)
thread.start()

lock=threading.Lock()
lock.acquire()
queue.put(3)
lock.release()    
app.exec_()

In an example posted below we are reaching the widget's method using signal and slot mechanism (instead of calling it directly from the thread). The code execution works as expected. Even while I "know" the solution I would like to know why it is happening.

class Emitter(QtCore.QObject):
    signal = QtCore.pyqtSignal()

class MessageBox(QtGui.QMessageBox):
    def __init__(self, parent=None):
        QtGui.QMessageBox.__init__(self, parent)     

    def showMessage(self):
        self.setText('Completed')
        self.show()

class Thread(QtCore.QThread):
    def __init__(self, queue, parent=None):
        QtCore.QThread.__init__(self, parent)     
        self.queue=queue        

    def run(self):
        emitter = Emitter()
        emitter.signal.connect(messagebox.showMessage)  

        while True:
            number=self.queue.get()
            result = self.process(number)
            emitter.signal.emit()
            self.queue.task_done()

    def process(self, number):
        timer = QtCore.QTimer()
        for i in range(number):
            print 'processing: %s'%i
            QtCore.QThread.sleep(1)        
        return True

messagebox = MessageBox()

queue = Queue.Queue()
thread = Thread(queue)
thread.start()

lock=threading.Lock()
lock.acquire()
queue.put(3)
lock.release()

app.exec_()

Solution

  • You'll need to do two things. You need to move the emitter object to the second thread, and you need to declare showMessage as a slot.

    emitter = Emitter()
    emitter.moveToThread(self)
    
    @QtCore.pyqtSlot()
    def showMessage(self):
        ...
    

    However, it's probably better to create the emitter and connect the signals and slots in the main thread and then move it to the second thread

    emitter = Emitter()
    emitter.signal.connect(messagebox.showMessage)
    emitter.moveToThread(thread)
    

    Also, QThreads inherit from QObject, so you don't absolutely need the emitter object, you can put the signals directly on the QThread. Just be aware that the QThread actually lives in the main thread, and any slots you have on it (except for run) are going to be executed in the main thread.

    That being said, if you want to send data back and forth between the main and second thread, you may want to look into the Worker Pattern of using QThread's in Qt.