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