4

I am trying to implement a method for performing a task at regular intervals in QGIS. This is to be triggered by a timer (currently 'threading.Timer', although it could be something else) and should utilise a thread separate from the GUI as well as maintain communication with the user to allow for feedback and for cancelling of the task.

I am basing my method on this QGIS multithreading tutorial. It seems an excellent source of information but it is slightly out of date (i.e. PyQt4, not PyQt5) and assumes a single action not a recurring one.

What I have got so far is below. The main thing that confuses me here is the double use of threading. I don't presumably want to create a thread which creates another thread. Since the Worker class handles the threading does that mean that the timer should operate on the original thread and therefore 'threading.Timer' is not the most appropriate option?

My objective is to get this working as 'template' code which I can then implement to do something useful.

(And if there is a totally different way of doing it, of course, I am open to suggestions. )

from qgis.core import *
from PyQt5 import QtCore, QtGui, QtWidgets
import traceback
import time
import threading
from PyQt5.QtCore import QTimer

class Worker():

    def __init__(self):
        self.killed = False

    def run(self):
        ret = False
        try:

            # Do something that takes ten seconds...
            for i in range(0,10): # loop 0 to 9
                time.sleep(1) # wait for a second
                self.progress.emit(100 * i/9) # update progress bar

            if self.killed is False:
                self.progress.emit(100)
                ret = True # successfully completed
        except Exception as e:
            # forward the exception upstream
            #self.error.emit(e, traceback.format_exc())
            pass
        self.finished.emit(ret)

    def kill(self):
        self.killed = True

    finished = QtCore.pyqtSignal(object)
    error = QtCore.pyqtSignal(Exception, basestring)
    progress = QtCore.pyqtSignal(float)

# end class

def workerError(self, e, exception_string):
    QgsMessageLog.logMessage('Worker thread raised an exception:\n'.format(exception_string), level=QgsMessageLog.CRITICAL)

def startWorker(self):

    #import QgsMessageLog
    QgsMessageLog.logMessage('About to create Worker')

    # create a new worker instance
    worker = Worker()

    # configure the QgsMessageBar
    messageBar = self.iface.messageBar().createMessage('Doing something...', )
    progressBar = QtWidgets.QProgressBar()
    progressBar.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
    cancelButton = QtWidgets.QPushButton()
    cancelButton.setText('Cancel')
    cancelButton.clicked.connect(worker.kill)
    messageBar.layout().addWidget(progressBar)
    messageBar.layout().addWidget(cancelButton)
    self.iface.messageBar().pushWidget(messageBar, Qgis.Info)
    self.messageBar = messageBar

    # start the worker in a new thread
    thread = QtCore.QThread()
    worker.moveToThread(thread)
    worker.finished.connect(workerFinished)
    worker.error.connect(workerError)
    worker.progress.connect(progressBar.setValue)
    thread.started.connect(worker.run)
    thread.start()
    self.thread = thread
    self.worker = worker

def workerFinished(self, ret):
    # clean up the worker and thread
    self.worker.deleteLater()
    self.thread.quit()
    self.thread.wait()
    self.thread.deleteLater()
    # remove widget from message bar
    self.iface.messageBar().popWidget(self.messageBar)
    if ret: # ret = True - completed successfully
        self.iface.messageBar().pushMessage('Task completed')
    else:
        # notify the user that something went wrong
        self.iface.messageBar().pushMessage('Something went wrong! See the message log for more information.', level=QgsMessageBar.CRITICAL, duration=3)

QTimer.singleShot(60, lambda: startWorker(qgis.utils)) # execute every 60 seconds
  • Try using QTimer singleshot - see this answer for an example dynamic update of QGIS – JimT Jan 2 at 12:51
  • OK, thanks. I have done that and edited code shown here accordingly. I am now getting an error at the line 'worker.moveToThread(thread)'. This doesn't surprise me since there is no 'worker.moveToThread()' defined. But it is in the original code... snorfalorpagus.net/blog/2013/12/07/… – wotnot Jan 2 at 13:07
1

You can use the excellent features QgsTask and QgsTaskManager for this.

There are a few options for running tasks in background threads in this way. These are detailed here in the documentation.

Below I provide an example which subclasses QgsTask and pushes messages and a progress bar to a Message Bar.

As for repeating task at regular intervals note that QTimer.singleShot is a one off timer which does something once only after the specified time-out period which is passed in milliseconds. If you want to schedule your task to repeat at regular intervals, don't call singleShot. Just do something like in my example below (maybe not the most the most elegant solution but it seems to work).

You can test this script in the QGIS Python console editor. The timer is set to repeat the task every 30 seconds. To stop this process enter timer.stop() at the >>> Python prompt in the console.

from qgis.gui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import time

class TestTask(QgsTask):
    """Here we subclass QgsTask"""
    def __init__(self, desc):
        QgsTask.__init__(self, desc)

    def run(self):
        """This function is where you do the 'heavy lifting' or implement
        the task which you want to run in a background thread. This function 
        must return True or False and should only interact with the main thread
        via signals"""
        for i in range (21):
            time.sleep(0.25)
            val = i * 5
            #report progress which can be received by the main thread
            self.setProgress(val)
            #check to see if the task has been cancelled
            if self.isCanceled():
                return False
        return True

    def finished(self, result):
        """This function is called automatically when the task is completed and is
        called from the main thread so it is safe to interact with the GUI etc here"""
        if result is False:
            iface.messageBar().pushMessage('Task was cancelled', duration=3)
        else:
            iface.messageBar().pushMessage('Task Complete', duration=3)

class Main(QObject):

    def newTask(self):
        self.msg = iface.messageBar().createMessage('Action', 'Doing Something')
        self.prog = QProgressBar(self.msg)
        self.btn_cancel = QPushButton(self.msg)
        self.btn_cancel.setText('Cancel Task')
        self.btn_cancel.clicked.connect(self.cancelTask)
        self.msg.layout().addWidget(self.prog)
        self.msg.layout().addWidget(self.btn_cancel)
        self.task1 = TestTask('Scheduled Task')
        #connect to signals from the background threads to perform gui operations
        #such as updating the progress bar
        self.task1.begun.connect(lambda: iface.messageBar().pushWidget(self.msg, Qgis.Info))
        self.task1.progressChanged.connect(lambda: self.prog.setValue(self.task1.progress()))
        self.task1.taskCompleted.connect(lambda: iface.messageBar().popWidget(self.msg))
        self.task1.taskTerminated.connect(lambda: iface.messageBar().popWidget(self.msg))
        QgsApplication.taskManager().addTask(self.task1)

    def cancelTask(self):
        self.task1.cancel()

m = Main()
timer = QTimer()
timer.timeout.connect(m.newTask)
timer.start(30000) #Set timer interval in milliseconds(Task repeats every 30 seconds)
  • I have a similar problem, but can I retrieve objects from the QgsTask? Lets say my task creates a memory layer that i want to add to the project, but i cant interact with the GUI. How to get the layer if the task can only return True/False? – Ah4b Jan 23 at 8:14
  • 1
    Add the memory layer to your project in the finished() method, which is called automatically when the task is completed. It is safe to interact with the GUI here. You can create your memory layer inside the run() method e.g. self.mem_layer = QgsVectorLayer( 'Point?crs=epsg:4326&field=id:integer' '&field=name:string(20)&index=yes', 'Temp_Layer', 'memory') Then load it in the finished() method: QgsProject().instance().addMapLayer(self.mem_layer) – Ben W Jan 23 at 10:40
  • Thanks Ben W! Another problem i had with QgsTask is that they do not start reliably. They might need multiple tries to start them. Is this a known issue maybe? – Ah4b Jan 23 at 13:56
  • No problem @Ah4b. I personally have not experienced a task not starting reliably. I’m not sure it’s necessarily an issue as such though- the documentation says this: “Once the task has been created it can be scheduled for running using the addTask function of the task manager…The scheduling of the tasks is influenced by the task priority, which is set in addTask.” And this from the API docs for addTask method: “… The priority argument can be used to control the run queue's order of execution, with larger numbers taking precedence over lower priority numbers.” – Ben W Jan 23 at 23:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.