user2949762
user2949762

Reputation: 11

Simple application of QThread

I am getting a grip on QThread in order to not lock my GUI, while performing possibly longer threads in the background. I am trying for practice to write a simple application: A countdown timer which I can start by clicking on a button "Start", thereby initiating a countdown loop, which I can pause by clicking on a button "Pause".

I want to do it in the "proper way" of using QThread (explained over here), i.e. subclassing QObject and then attach an instance of this subclass to a QThread via moveToThread. I did the GUI with QTDesigner and this is what I modified so far (from other examples on the Net) in Python:

from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import QThread, SIGNAL

import time, sys, mydesign

class SomeObject(QtCore.QObject):

    def __init__(self, lcd):
        super(self.__class__, self).__init__()
        self.lcd = lcd
        self.looping = True
    finished = QtCore.pyqtSignal()
    def pauseLoop(self):
        print "Hello?"
        self.looping = False

    def longRunning(self):
        count = 10
        self.lcd.display(count)
        while count > 0 and self.looping:
            time.sleep(1)
            count -= 1
            self.lcd.display(count)
        self.finished.emit()

class ThreadingTutorial(QtGui.QMainWindow, mydesign.Ui_MainWindow):
    def __init__(self):
        super(self.__class__, self).__init__()
        self.setupUi(self)

        #an instance of SomeObject gets attached to
        #an instance of wrapper class QThread()

        #objThread is a wrapper object for an instance of
        # self-defined class SomeObject
        self.objThread = QtCore.QThread()
        self.obj = SomeObject(self.lcdNumber)
        self.obj.moveToThread(self.objThread)
        #connect all the signals
        self.obj.finished.connect(self.objThread.quit)
        self.objThread.started.connect(self.obj.longRunning)
        self.startCountdown.clicked.connect(self.objThread.start)
        self.pauseButton.clicked.connect(self.obj.pauseLoop)


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    form = ThreadingTutorial()
    form.show()
    app.exec_()

The GUI is not locked but the function pauseLoop() only gets executed after the loop is finished. How do I have to do it in order to accomplish my goal to be able to pause the loop in longRunning()?

Thx in advance!

update:

With the help of Steve's and three_pineapples's remarks, I come up with following solution using the inner event loop of objThread:

from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import QThread, SIGNAL, QTimer

import time, sys, mydesign

class SomeObject(QtCore.QObject):

    def __init__(self):
        super(self.__class__, self).__init__()
        self.looping = True
        self.count = 10
        self.timer = QTimer(self)
        self.timer.start(1000)
        self.connect(self.timer, SIGNAL("timeout()"), self.longRunning)

    finished = QtCore.pyqtSignal()
    iterated = QtCore.pyqtSignal()
    def pauseLoop(self):
        self.looping = False

    def longRunning(self):
        if self.count > 0 and self.looping:
            self.count -= 1
            self.iterated.emit()
        else:
            self.finished.emit()# sends signal for stopping event loop

class ThreadingTutorial(QtGui.QMainWindow, mydesign.Ui_MainWindow):
    def __init__(self):
        super(self.__class__, self).__init__()
        self.setupUi(self)


        #an instance of SomeObject gets attached to
        #an instance of wrapper class QThread()

        #objThread is a wrapper object for an instance of
        # self-defined class SomeObject
        self.objThread = QtCore.QThread()
        self.obj = SomeObject()
        self.lcdNumber.display(self.obj.count)
        self.obj.moveToThread(self.objThread)
        #connect all the signals
        self.obj.finished.connect(self.objThread.quit)
        self.obj.iterated.connect(lambda: self.lcdNumber.display(self.obj.count))
        self.objThread.started.connect(self.obj.longRunning)
        self.startCountdown.clicked.connect(self.objThread.start)        
        self.pauseButton.clicked.connect(self.obj.pauseLoop)


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    form = ThreadingTutorial()
    form.show()
    app.exec_()

Upvotes: 0

Views: 351

Answers (1)

Steve
Steve

Reputation: 552

This is happening because you tell Qt to run the code for your SomeObject object in a single thread. In your code, you have two threads, your main GUI thread, and your self.objThread. The pauseLoop() is called in the same thread as longRunning(), which means longRunning() must complete before pauseLoop() can run. Instead, you need to call pauseLoop() from a different thread, not self.objThread.

Note that when you start changing data from two different threads (your main thread and your new thread), you could start to run into race conditions. Since you're only setting one boolean variable, you'll be fine, but it's something to keep in mind.

Edit: As pointed out in the comment by three_pineapples, accessing GUI objects (ex: QWidget) should only be done in the main GUI thread. To show how this would work, I have updated this answer to use signals to update the LCD in the GUI thread instead of just printing the value to stdout. I also added code to print out the current thread at different sections.

from PySide import QtGui, QtCore
from PySide.QtCore import QThread, SIGNAL

import time, sys

class SomeObject(QtCore.QObject):
    updateCounter = QtCore.Signal(int)
    finished = QtCore.Signal()
    def __init__(self):
        super(self.__class__, self).__init__()
        self.looping = True
    def pauseLoop(self):
        self.looping = False
        print 'Pause Loop: '+str(QThread.currentThreadId())
    def longRunning(self):
        print 'Long Running: '+str(QThread.currentThreadId())
        count = 10
        while count > 0 and self.looping:
            count -= 1
            self.updateCounter.emit(count)
            time.sleep(1)
        self.finished.emit()

class ThreadingTutorial(QtGui.QWidget):
    def __init__(self):
        super(self.__class__, self).__init__()
        #
        self.thisLayout = QtGui.QVBoxLayout(self)
        self.startCountdown = QtGui.QPushButton('Start', self)
        self.pauseButton = QtGui.QPushButton('Stop', self)
        self.lcd = QtGui.QLabel('', self)
        self.thisLayout.addWidget(self.startCountdown)
        self.thisLayout.addWidget(self.pauseButton)
        self.thisLayout.addWidget(self.lcd)
        #
        print 'Main GUI Thread: '+str(QThread.currentThreadId())
        self.objThread = QtCore.QThread()
        self.obj = SomeObject()
        self.obj.moveToThread(self.objThread)
        self.obj.updateCounter.connect(self._updateTimer)
        #
        self.obj.finished.connect(self.objThread.quit)
        self.objThread.started.connect(self.obj.longRunning)
        self.startCountdown.clicked.connect(self.objThread.start)
        self.pauseButton.clicked.connect(self._stopTimer)
    def _stopTimer(self):
        self.obj.pauseLoop()
    def _updateTimer(self, num):
        self.lcd.setText(str(num))
        print 'Update Timer: '+str(QThread.currentThreadId())


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    form = ThreadingTutorial()
    form.show()
    app.exec_()

Example output:

Main GUI Thread: 3074717376
Long Running: 3034471232
Update Timer: 3074717376
Update Timer: 3074717376
Update Timer: 3074717376
Update Timer: 3074717376
Update Timer: 3074717376
Update Timer: 3074717376
Pause Loop: 3074717376

Upvotes: 1

Related Questions