7

I'm using QThread to run processingBar and heavy job so the user will know it's loading. Everything works fine until the job is done then the map is stuck and not reloading. It looks like I need to reload Qgis somehow so the map that i use will work well after the thread is done

This is what I tried:

def onStart(self):
    self.dlg.progressBar.setRange(0, 0)
    self.myLongTask.start()

def onFinished(self): self.dlg.progressBar.setRange(0, 1)

def run(self): self.dlg.progressBar.setRange(0, 1) self.dlg.pushButton.clicked.connect(self.onStart) self.myLongTask = workThread() self.myLongTask.taskFinished.connect(self.onFinished)

class workThread(QtCore.QThread): taskFinished = QtCore.pyqtSignal() def run(self): time.sleep(3) self.taskFinished.emit()

Update: I try using QObject as showed here:https://realpython.com/python-pyqt-qthread/

This is how my run look like:

        self.dlg.progressBar.setRange(0, 1)
    self.thread=QThread()
    self.indexThread=IndexThread()
    self.indexThread.moveToThread(self.thread)
    self.thread.started.connect(self.indexThread.run)
    self.indexThread.taskFinished.connect(self.thread.quit)
    self.indexThread.taskFinished.connect(self.indexThread.deleteLater)
    self.indexThread.taskFinished.connect(self.onFinished)

    self.thread.finished.connect(self.thread.deleteLater)

    self.dlg.indexButton.clicked.connect(self.onStart)

On start:

def onStart(self):
    print("test")
    self.thread.start()
    self.dlg.progressBar.setRange(0, 0)

On finished:

def onFinished(self):
    print("finishing")
    self.dlg.progressBar.setRange(0, 1)

and I get the same result. Everything works but the used map on QGIS won't reload and get stuck. Any idea?

Kadir Şahbaz
  • 76,800
  • 56
  • 247
  • 389
Micha
  • 163
  • 6
  • I'm voting to close this question because it has no GIS component and, in it's current form, would be better asked at stackoverflow.com. – Ben W Jan 03 '22 at 09:53
  • 1
    You haven't called quit() or deleteLater() on your thread object but... please read: https://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/ which explains why you shouldn't subclass QThread. And: https://realpython.com/python-pyqt-qthread/#using-qthread-to-prevent-freezing-guis for a Python example. – Ben W Jan 03 '22 at 09:57
  • 1
    So question on developing plugins for QGIS has no GIS components? – Micha Jan 03 '22 at 09:58
  • Yes! In it's essence your question is entirely about QThread. Please look at the links I provided! – Ben W Jan 03 '22 at 10:01
  • @BenW updated my question. Please let me know if you got any idea.. – Micha Jan 03 '22 at 12:01
  • 1
    Maybe this example can give you a guide: https://gis.stackexchange.com/a/411792/107424 – MrXsquared Jan 03 '22 at 12:06
  • I have posted an answer with a working, trivial example of using QThread in a minimal Python plugin, which you can install and test. I honestly can't say why your plugin is not working because, thus far you have only shown us snippets of your code. In your updated answer you haven't shown us the contents of your IndexThread worker class. Maybe you are trying to do something in a background thread which is not thread safe like loading layers or interacting directly (not via signals) with the project or main gui??! – Ben W Jan 04 '22 at 10:40
  • 1
    Incidentally, IMHO QgsTask is a better option for threading in a QGIS plugin. I have used it sucessfully in a (not-published) plugin (but you can inspect the source code on my github account) https://github.com/benwirf/basemap_2_geopackage/blob/main/basemap_2_geopackage.py – Ben W Jan 04 '22 at 10:42

2 Answers2

5

Here is a working example of using Qthread inside a QGIS plugin. This is a minimal plugin based on the example from Martin Dobias here, and implements a trivial example of QThread based on the examples here and here. I believe that the more recent improved examples of QThread implementation are thanks to the work of Maya Posch. You can save the two files below (__init__.py and metadata.txt) into a folder and copy to your QGIS plugins folder to install and test.

from PyQt5.QtCore import QThread, QObject, pyqtSignal

from PyQt5.QtWidgets import (QAction, QMessageBox, QDialog, QVBoxLayout, QLabel, QLineEdit, QProgressBar, QDialogButtonBox, )

import time

def classFactory(iface): return QThreadExample(iface)

class QThreadExample: def init(self, iface): self.iface = iface self.dlg = testDialog()

    self.thread = None
    self.worker = None

def initGui(self):
    self.action = QAction('QTE!', self.iface.mainWindow())
    self.action.triggered.connect(self.run)
    self.iface.addToolBarIcon(self.action)

    self.dlg.btns.accepted.connect(self.create_thread)
    self.dlg.btns.rejected.connect(self.kill_thread)

def unload(self):
    self.iface.removeToolBarIcon(self.action)
    del self.action

def run(self):
    self.dlg.show()
    self.dlg.prog.setValue(0)
    self.dlg.info.clear()

def create_thread(self):
    self.thread = QThread()
    self.worker = Worker()
    self.worker.moveToThread(self.thread)
    self.thread.started.connect(self.worker.process)
    self.worker.progressChanged.connect(lambda: self.dlg.prog.setValue(self.worker.progress))
    self.worker.finished.connect(self.worker_finished)
    self.thread.finished.connect(self.thread.deleteLater)
    self.thread.start()
    self.dlg.info.setText('Running')

def kill_thread(self):
    self.worker.cancel()

def worker_finished(self, result):
    # catch an emitted object (other than bool) if needed        
    self.dlg.prog.setValue(0)
    if result:
        self.dlg.info.setText(result)
    elif not result:
        self.dlg.info.setText('Task was cancelled!')
    self.thread.quit()#IMPORTANT! emits thread.finished signal        


###---WORKER CLASS---### class Worker(QObject): progressChanged = pyqtSignal() finished = pyqtSignal(object) cancelled = pyqtSignal()

def __init__(self): # define additional constructor parameters if required
    QObject.__init__(self)
    self.progress = 0
    self.isCancelled = False

def process(self):
    for i in range(21):
        time.sleep(0.5)
        val = i * 5
        self.setProgress(val)
        if self.isCancelled:
            self.finished.emit(False)
            return

    self.finished.emit('Task finished') # emit an object if required

def setProgress(self, progressValue):
    self.progress = progressValue
    self.progressChanged.emit()

def cancel(self):
    self.isCancelled = True
    self.cancelled.emit()


###---PLUGIN DIALOG CLASS---### class testDialog(QDialog):

def __init__(self):
    QDialog.__init__(self)
    self.setGeometry(200, 200, 500, 350)
    layout = QVBoxLayout()
    self.lbl_1 = QLabel('Info: ', self)
    self.info = QLineEdit(self)
    self.lbl_2 = QLabel('Progress: ', self)
    self.prog = QProgressBar(self)
    self.btns = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self)
    for c in self.children():
        layout.addWidget(c)
    self.setLayout(layout)

metadata.txt

[general]
name=QThreadExample
description=QThread example plugin
about=A trivial example of using QThread in a plugin
version=1.0
qgisMinimumVersion=3.0
author=Your Name
email=your.name@gmail.com
repository=URL to the code repository

You can see from the screencasts below that the implementation of Qthread inside the plugin works fine and that the map canvas remains responsive after the completion of the background thread.

enter image description here

enter image description here

Ben W
  • 21,426
  • 3
  • 15
  • 39
0

For anyone else who struggling with it. After looking at @Ben W answer make sure that everything that is UI is separated from the Worker thread. Not a single iface should be on the worker thread.

Micha
  • 163
  • 6