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
Workeris derived fromQtCore.QObjectwhich implementsmoveToThread(). – gumo Jan 28 '20 at 08:05