Tim Carnahan
Tim Carnahan

Reputation: 371

PyQt thread communication help? QThread and QObject

After read and searching I am trying to use the generate a QObject then use the movetoThread method to run an independent process and allow the QMainWindow to continue to respond. This has not worked when I have tried to implement the operation in a QThread.run() method. The following code is my attempt to make a simple example. While the code works in running thread independent of the MainWindow, it does not abort. The only way I can get a thread to stop is to set worker.end = True. Which I think should not be the way to do it.

"""
This is a program to test Threading with Objects in PyQt4.
"""

from time import sleep
import sys

from PyQt4.QtCore import QObject, pyqtSlot, pyqtSignal, QThread
from PyQt4.QtGui import QMainWindow, QApplication, QProgressBar
from PyQt4.QtGui import QPushButton, QVBoxLayout, QWidget

class workerObject(QObject):
    bar_signal = pyqtSignal(int)
    res_signal = pyqtSignal(str)
    term_signal = pyqtSignal()

    def __init__(self, maxIters):
        super(workerObject, self).__init__()
        self.maxIters = maxIters

    def run(self):
        self.bar_signal.emit(self.maxIters)        
        sleep(1)
        self.end = False

        for step in range(self.maxIters):
            if self.end:
                self.maxIters = step
                break
            self.bar_signal.emit(step)
            sleep(2)

        self.res_signal.emit("Got to {}".format(self.maxIters)) 
        self.term_signal.emit()

    @pyqtSlot()
    def mystop(self):
        print "stop signalled?"
        self.end = True

class MCwindow(QMainWindow):
    abort_signal = pyqtSignal(name='abort_signal')

    def __init__(self):
        super(MCwindow,self).__init__()        
        self.maxIters = 50

        widget = QWidget()
        layout = QVBoxLayout(widget)
        self.go_btn = QPushButton()
        self.go_btn.setText('Go')
        layout.addWidget(self.go_btn)
        self.abort_btn = QPushButton()
        self.abort_btn.setText('Stop')
        layout.addWidget(self.abort_btn)
        self.simulation_bar = QProgressBar()
        self.simulation_bar.setRange(0, self.maxIters)
        self.simulation_bar.setFormat("%v")
        layout.addWidget(self.simulation_bar)
        self.setCentralWidget(widget)

        self.go_btn.clicked.connect(self.run_mc)
        # The button calls the windows method to stop --- it could 
        # be that is 'clicked' calls the worker.mystop
#        self.abort_btn.clicked.connect(self.stop_mc)
        # This allows for the abort button to do somethign in the MainWindow
        # before the abort_signal is sent, this works
        self.abort_btn.clicked.connect(self.stop_mc)

    def run_mc(self):        
        self.thread = QThread()                
        self.worker = workerObject(self.maxIters)
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        # This is the simple stop method, but does not work
#        self.abort_btn.clicked.connect(self.worker.mystop)
        # This uses the signal in the MCwindow - this connection does NOT works
        self.abort_signal.connect(self.worker.mystop)
        # This does NOT stop the thread
        # and would not allow for any clean up in the worker.
#        self.abort_signal.connect(self.thread.terminate)
        # This is a 'bad' way to stop the woker ... It does, however, work
#        self.abort_signal.connect(self.stopper)
        self.worker.bar_signal.connect(self.setBar)
        self.worker.res_signal.connect(self.setData)
        self.worker.term_signal.connect(self.thread.terminate)
        self.thread.start()

    def stop_mc(self):
        print "Stopping?!"
        # This signal is NEVER seen by the Worker.
        self.abort_signal.emit()

    def stopper(self):
        print "I should stop?!"
        # Should use signals to tell the worker to stop - and not setting a attribute
        self.worker.end=True

    @pyqtSlot(int)
    def setBar(self, val):
        self.simulation_bar.setValue(val)

    @pyqtSlot(str)    
    def setData(self, txt):
        print "Got done Sig!", txt

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MCwindow()
    window.show()
    sys.exit(app.exec_())

Upvotes: 4

Views: 1897

Answers (2)

ekhumoro
ekhumoro

Reputation: 120578

The reason why the slot connected to abort_signal doesn't seem to get called, is because cross-thread signals are queued by default. This means the signal will be wrapped as an event and posted to the event queue of whichever thread the receiver is living in.

In your particular example, the receiver is a worker object which has been moved to a worker thread. Calling start() on the worker thread will start its event-loop, and that is where abort_signal will be queued. However, the run() method of the worker object starts a for loop, which will block the thread's event processing in exactly the same way it would if it was executed in the main gui thread!

You can more clearly see what's happening if you make a few adjustments to your example:

class MCwindow(QMainWindow):
    abort_signal = pyqtSignal(name='abort_signal')

    def __init__(self):
        super(MCwindow,self).__init__()
        # use a sane default
        self.maxIters = 5
        ...
        # DO NOT use QThread.terminate
        self.worker.term_signal.connect(self.thread.quit)

Now run the example, and then click the Go button, click the Stop button, and wait for the worker to complete normally. This should produce output like this:

Stopping?!
Got done Sig! Got to 5
stop signalled?

Note that "stop signalled" is output last - i.e. after run() exits and control has returned to the thread's event-loop. In order to process in-coming signals while the worker is running, you will need to force immediate processing of the thread's pending events. This can be done like this:

     for step in range(self.maxIters):
        QApplication.processEvents()
        ...

With that in place, you should then see output like this:

Stopping?!
stop signalled?
Got done Sig! Got to 2

Which is presumably what you intended.

Upvotes: 4

justengel
justengel

Reputation: 6320

Typically a thread will close when it exits the run method. The other way to get a regular python thread to close is by calling it's join method.

For PyQt the join method should either be the quit or terminate method. You should probably still set your end variable to True.

Upvotes: 0

Related Questions