After a lot (and even more) of reading, trying and testing I still could not find a way to use QgsTasks in a QGIS Plugin. But with the help of mainly these two documents:
I was finally able to build a simple and basic QGIS Plugin using PyQt's QThreads and keeping the GUI active while running.
Imagine you create a QGIS Plugin called "TaskTest" using the PluginBuilder-Plugin and create a simple GUI:
The task_test.py file (explanations in the code as comments):
# -*- coding: utf-8 -*-
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, QObject, QThread, pyqtSignal
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction
from qgis.core import *
# Initialize Qt resources from file resources.py
from .resources import *
# Import the code for the dialog
from .task_test_dialog import TaskTestDialog
from time import sleep
from datetime import datetime
import os.path
import random
MESSAGE_CATEGORY = 'QgsTaskTestPlugin'
class Worker(QThread):
finished = pyqtSignal() # create a pyqtSignal for when task is finished
progress = pyqtSignal(int) # create a pyqtSignal to report the progress to progressbar
def __init__(self):
super(QThread, self).__init__()
#print('workerinit')
self.stopworker = False # initialize the stop variable
def run(self):
self.progress.emit(0) # reset progressbar
self.time = 22 # just a variable to work with
self.total = 10 # just a variable to work with
wait_time = self.time / 100.0 # calculate the waste time
# print('run') # debug: we just entered the run method of Worker-Class
for i in range(self.total): # just do something
sleep(wait_time) # just waste some time
self.progress.emit(int((i+1)/self.total*100)) # report the current progress via pyqt signal to reportProgress method of TaskTest-Class
if self.stopworker == True: # if cancel button has been pressed the stop method is called and stopworker set to True. If so, break the loop so the thread can be stopped
print('break req')
break
self.finished.emit() # report via pyqt signal that run method of Worker-Class has been finished
def stop(self):
self.stopworker = True
class TaskTest:
"""QGIS Plugin Implementation."""
def __init__(self, iface): # just the default generated by PluginBuilder
# Save reference to the QGIS interface
self.iface = iface
# initialize plugin directory
self.plugin_dir = os.path.dirname(__file__)
# initialize locale
locale = QSettings().value('locale/userLocale')[0:2]
locale_path = os.path.join(
self.plugin_dir,
'i18n',
'TaskTest_{}.qm'.format(locale))
if os.path.exists(locale_path):
self.translator = QTranslator()
self.translator.load(locale_path)
QCoreApplication.installTranslator(self.translator)
# Declare instance attributes
self.actions = []
self.menu = self.tr(u'&TaskTest')
# Check if plugin was started the first time in current QGIS session
# Must be set in initGui() to survive plugin reloads
self.first_start = None
# noinspection PyMethodMayBeStatic
def tr(self, message): # just the default generated by PluginBuilder
# noinspection PyTypeChecker,PyArgumentList,PyCallByClass
return QCoreApplication.translate('TaskTest', message)
def add_action(
self,
icon_path,
text,
callback,
enabled_flag=True,
add_to_menu=True,
add_to_toolbar=True,
status_tip=None,
whats_this=None,
parent=None): # just the default generated by PluginBuilder
icon = QIcon(icon_path)
action = QAction(icon, text, parent)
action.triggered.connect(callback)
action.setEnabled(enabled_flag)
if status_tip is not None:
action.setStatusTip(status_tip)
if whats_this is not None:
action.setWhatsThis(whats_this)
if add_to_toolbar:
# Adds plugin icon to Plugins toolbar
self.iface.addToolBarIcon(action)
if add_to_menu:
self.iface.addPluginToMenu(
self.menu,
action)
self.actions.append(action)
return action
def initGui(self): # just the default generated by PluginBuilder
"""Create the menu entries and toolbar icons inside the QGIS GUI."""
icon_path = ':/plugins/task_test/icon.png'
self.add_action(
icon_path,
text=self.tr(u'TaskTestPlugin'),
callback=self.run,
parent=self.iface.mainWindow())
# will be set False in run()
self.first_start = True
def unload(self): # just the default generated by PluginBuilder
"""Removes the plugin menu item and icon from QGIS GUI."""
for action in self.actions:
self.iface.removePluginMenu(
self.tr(u'&TaskTest'),
action)
self.iface.removeToolBarIcon(action)
def startWorker(self): # method to start the worker thread
self.thread = QThread()
self.worker = Worker()
# see https://realpython.com/python-pyqt-qthread/#using-qthread-to-prevent-freezing-guis
# and https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html
self.worker.moveToThread(self.thread) # move Worker-Class to a thread
# Connect signals and slots:
self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.worker.progress.connect(self.reportProgress)
self.thread.start() # finally start the thread
self.dlg.Test_StartTest.setEnabled(False) # disable the start-thread button while thread is running
self.thread.finished.connect(lambda: self.dlg.Test_StartTest.setEnabled(True)) # enable the start-thread button when thread has been finished
def killWorker(self): # method to kill/cancel the worker thread
self.worker.stop() # call the stop method in worker class
# print('pushed cancel') # debugging
# see https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html
try: # to prevent a Python error when the cancel button has been clicked but no thread is running use try/except
if self.thread.isRunning(): # check if a thread is running
# print('pushed cancel, thread is running, trying to cancel') # debugging
#self.thread.requestInterruption() # not sure how to actually use it as there are no examples to find anywhere, one somehow would need to listen to isInterruptionRequested()
self.thread.exit() # Tells the thread’s event loop to exit with a return code.
self.thread.quit() # Tells the thread’s event loop to exit with return code 0 (success). Equivalent to calling exit (0).
self.thread.wait() # Blocks the thread until https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html#PySide6.QtCore.PySide6.QtCore.QThread.wait
except:
# print('kill worker - task does no longer exist') # debugging
pass
def reportProgress(self, n): # method to report the progress to gui
# print(f"Long-Running Step: {n}") # debugging purposes
self.dlg.Test_ProgressBar.setValue(n) # set the current progress in progress bar
def run(self):
# Create the dialog with elements (after translation) and keep reference
# Only create GUI ONCE in callback, so that it will only load when the plugin is started
if self.first_start == True:
self.first_start = False
self.dlg = TaskTestDialog()
self.dlg.Test_StartTest.clicked.connect(lambda: self.startWorker()) # call the startWorker method when button has been clicked
self.dlg.Test_Cancel.clicked.connect(lambda: self.killWorker()) # call the killWorker method when button has been clicked
# show the dialog
self.dlg.show()
# Run the dialog event loop
result = self.dlg.exec_()
# See if OK was pressed
if result:
# Do something useful here - delete the line containing pass and
# substitute with your code.
pass
The .ui file:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TaskTestDialogBase</class>
<widget class="QDialog" name="TaskTestDialogBase">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>427</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>TaskTest</string>
</property>
<widget class="QDialogButtonBox" name="button_box">
<property name="geometry">
<rect>
<x>30</x>
<y>240</y>
<width>341</width>
<height>32</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QProgressBar" name="Test_ProgressBar">
<property name="geometry">
<rect>
<x>40</x>
<y>150</y>
<width>161</width>
<height>31</height>
</rect>
</property>
<property name="toolTip">
<string><html><head/><body><p>Current Progress of Task-Test</p></body></html></string>
</property>
<property name="locale">
<locale language="English" country="UnitedStates"/>
</property>
<property name="value">
<number>0</number>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
</widget>
<widget class="QPushButton" name="Test_Cancel">
<property name="geometry">
<rect>
<x>220</x>
<y>150</y>
<width>121</width>
<height>31</height>
</rect>
</property>
<property name="toolTip">
<string><html><head/><body><p>Cancel the Task-Test</p></body></html></string>
</property>
<property name="text">
<string>Cancel Test</string>
</property>
</widget>
<widget class="QPushButton" name="Test_StartTest">
<property name="geometry">
<rect>
<x>40</x>
<y>100</y>
<width>251</width>
<height>41</height>
</rect>
</property>
<property name="toolTip">
<string><html><head/><body><p>Start the Task-Test</p></body></html></string>
</property>
<property name="text">
<string>Start Test</string>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>button_box</sender>
<signal>accepted()</signal>
<receiver>TaskTestDialogBase</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>button_box</sender>
<signal>rejected()</signal>
<receiver>TaskTestDialogBase</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
</connections>
</ui>
Unfortunately my knowledge in this kind of stuff is still very limited, so I can not explain in detail, but as there are so few examples to find, I hope this basic example helps someone having the same questions and issues as I had/have.
Please see yourself still beeing encuraged to add your own answer so I can eventually move my accept checkmark. Or feel free to edit my answer if you can improve it.