PDA

View Full Version : QDialog and Multithreading within PYQT5



xtlc
19th March 2020, 11:05
I am trying to update a QDialogs labels with a background process in two ways: Via a STATUS class and a Result function I post in the end. I think the QDialog prevents my functions in the back to be executed, as the console output is also only generated once I close the QDialog:


from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QDialog, QApplication, QMessageBox, QDialogButtonBox, QLabel, QInputDialog
from PyQt5.QtGui import QBrush, QColor
from PyQt5.QtCore import QTimer, QThreadPool, QRunnable, QObject, Qt, pyqtSlot, pyqtSignal
from datetime import datetime
import traceback, sys

class AppWindow(QDialog):
def __init__(self):
super().__init__()
self.ui = Ui_Dialog()
self.threadpool = QThreadPool()
self.ui.setupUi(self)
self.results = 0
self.STATI = [Status(ID=i, callback=self.postStatus) for i in range(2)]
self.show()
self.ui.Button.clicked.connect(self.Button_clkd)

def OkButton_clkd(self):
self.ui.resultBox.close()

def postStatus(self, STATUS):
lables_list_qdialog = self.ui.resultBox.findChildren(QtWidgets.QLabel)
lables_list_qdialog[STATUS.id].setText("Slot "+ str(STATUS.id) +" job: "+ STATUS.status)
self.ui.res_0_label.setText("postStatus working")

def postResult(self, job):
self.results += 1
lables_list_qdialog = self.ui.resultBox.findChildren(QtWidgets.QLabel)
lables_list_qdialog[job["id"]].setText(job["result"]+ "--" +job["arg"])
self.ui.res_1_label.setText("postResult working")

def workFunc(self, *argv):
self.ui.resultWindow(self)
self.total_jobs = 0

self.jobs = {0: {"id": 0, "time": datetime.now(), "result": None, "arg": "Hello", "func": argv[0].__name__},
1: {"id": 1, "time": datetime.now(), "result": None, "arg": "World", "func": argv[0].__name__}}

for job in self.jobs:
self.total_jobs += 1
self.STATI[job].reset()
worker = Worker(argv[0], self.jobs[job], argv[1], self.STATI[job])
worker.signals.result.connect(self.postResult)
self.threadpool.start(worker)

def Button_clkd(self):
self.workFunc(self.test, "testing parameter")

def test(self, job, arg, STATUS):
STATUS.update("Test executing")
secs = datetime.now() - job["time"]
job["time"] = secs.seconds
job["result"] = arg
return job


class Worker(QRunnable):
def __init__(self, func, *args):
super().__init__()
self.func = func
self.args = args
self.signals = WorkerSignals()

@pyqtSlot()
def run(self):
try:
result = self.func(*self.args)
print(result)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result)
finally:
self.signals.finished.emit()


class WorkerSignals(QObject):
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)


class Status():
def __init__(self, ID, callback=lambda callbackparameter: None):
self.status = ""
self.callback = callback
self.id = ID

def update(self, callbackparameter):
self.status = callbackparameter
self.callback(self)

def reset(self):
self.status = ""


class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(200, 200)
Dialog.setWindowTitle("test")

self.res_0_label = QtWidgets.QLabel(Dialog)
self.res_0_label.setGeometry(QtCore.QRect(10, 100, 120, 40))
self.res_0_label.setText("id 0 job: ...")

self.res_1_label = QtWidgets.QLabel(Dialog)
self.res_1_label.setGeometry(QtCore.QRect(10, 120, 120, 40))
self.res_1_label.setText("id 1 job: ...")

self.Button = QtWidgets.QPushButton(Dialog)
self.Button.setGeometry(QtCore.QRect(40, 40, 140, 40))
self.Button.setObjectName("Button")
self.Button.setText("WORK!")


def resultWindow(self, parent):
self.resultBox = QDialog(parent)
self.resultBox.installEventFilter(parent)
self.resultBox.setWindowTitle("Please Wait")
self.resultBox.setGeometry(300, 300, 360, 200)

self.OkButton = QtWidgets.QPushButton(self.resultBox)
self.OkButton.setGeometry(QtCore.QRect(130, 160, 100, 40))
self.OkButton.setText("Ok")
self.OkButton.clicked.connect(parent.OkButton_clkd )

self.job_0_label = QtWidgets.QLabel(self.resultBox)
self.job_0_label.setGeometry(QtCore.QRect(10, 10, 300, 40))
self.job_0_label.setText("Slot 0 job: IDLE ...")

self.job_1_label = QtWidgets.QLabel(self.resultBox)
self.job_1_label.setGeometry(QtCore.QRect(10, 30, 300, 40))
self.job_1_label.setText("Slot 1 job: IDLE ...")

self.resultBox.exec_()


if __name__ == "__main__":
app = QApplication(sys.argv)
w = AppWindow()
w.show()
sys.exit(app.exec_())

This snippet basically describes my pain: Once executed it should update the labels in the QDialog ResultBox. But it does not. Please enlighten me :) Also, how can I find all labels in my Ui_Dialog class that are inside the setupUi method? .findChildren does work for resultbox (see code) but I fail to apply the principle on the Ui_Dialog.

d_stranz
19th March 2020, 16:36
QDialog::exec() starts its own event loop, independent of the main application's event loop. The loop is modal, meaning that events from other event loops are not processed until the dialog's loop exits. So your worker thread might be running, but the events it sends back to the parent process aren't handled until the dialog is closed.

A call to the static method QCoreApplication::processEvents() is the usual remedy to avoid the app freezing during long compute processes. This might work here, but I haven't studied your code in enough detail to know where to place it. Possibly in the slot that handles the finished status from the worker thread.

xtlc
21st March 2020, 12:36
Thanks for you answer. So I can just add a line "QCoreApplication.processEvents()" after the QDialog is initiated (that would be line 34 here)?

My second question is still puzzeling me: How do I get a list of all labels, buttons and so on for my Ui_Dialog? It dos not have a children/findchildren method.

d_stranz
21st March 2020, 16:42
So I can just add a line "QCoreApplication.processEvents()" after the QDialog is initiated (that would be line 34 here)?

Probably would have no effect there, since nothing has happened yet in your worker thread. I would try it at the end of "postResult()".


How do I get a list of all labels, buttons and so on for my Ui_Dialog?

If you created the dialog's .ui file using Qt Designer, then you (or Qt Designer) would have defined a variable for every QWidget in your dialog (QLabel, QLineEdit, etc.). Open the .ui file in Qt Designer and select any widget. At the top of the Property Editor panel (Execute the View->Property Editor menu command if you don't see it), there will be a QObject entry. Under that is the "objectName" property. This is the name assigned to the widget variable. You can change this to anything you want that is more meaningful ("statusLabel" instead of "label_4" for instance). After all, will you remember next week which label "label_4" refers to?

Since you are adding the ui into your dialog class by composition (i.e. there is a "ui" member variable for the Ui_Dialog class), then you access the label as "self.ui.statusLabel".

xtlc
22nd March 2020, 01:28
First: Thanks for your time. And thanks for the hint with processEvents. That worked! Here I think I was not clear enough in what I wanted to know:

Since you are adding the ui into your dialog class by composition (i.e. there is a "ui" member variable for the Ui_Dialog class), then you access the label as "self.ui.statusLabel".
I want to get ALL labels of the Ui_Dialog class - something like
Ui_Dialog.findChildren(QWidgets.QLabel) if I wanted to find all QLabels ... but this only works for the resultWindow method. How does it work on the Ui_Dialog?
dir(Ui_Dialog) shows no method Children/findChildren.

d_stranz
22nd March 2020, 16:47
The documentation for QObject::findChildren() says that the first argument should be a QString that names the type (ie. class) of the widgets you want to retrieve.

In C++, this is implemented using templates, as the documentation shows. I do not know how this is done in python.

In addition, you are using the name of your class (Ui_Dialog), not the name of the variable that implements that class in your dialog. So at a minimum, your code should look something like this:



self.ui.findChildren( QWidgets.QLabel )


assuming QWidgets.QLabel is the correct syntax for the argument. Perhaps it should be "QWidgets.QLabel" (ie. a quoted string) instead.

Or maybe you don't need the "ui" variable at all:



self.findChildren( QWidgets.QLabel )


where "self" is your Dialog class instance.