4

I'm working on a GUI for calling a function 'my_function' when the button 'my_button' is pushed.

This function processes data iteratively. It contains a 'for' loop, and at each iteration it prints out a message that shows the progress of my function. I would like this prints to be displayed in my GUI (in this example in a textEdit widget), in real time. How could I do that?

I would like to make it clear that I need real time display. I found some solutions online, but all prints only appear when the function finishes execution. I need the prints to display in real time in order to appreciate the function progress. Thanks in advance for your tips!

Here's a minimal reproductible example (I obtained the template via qt designer):

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(163, 225)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.formLayout = QtWidgets.QFormLayout(self.centralwidget)
        self.formLayout.setObjectName("formLayout")
        self.my_button = QtWidgets.QPushButton(self.centralwidget)
        self.my_button.setObjectName("my_button")
        self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.my_button)
        self.textEdit = QtWidgets.QTextEdit(self.centralwidget)
        self.textEdit.setObjectName("textEdit")
        self.formLayout.setWidget(1, QtWidgets.QFormLayout.SpanningRole, self.textEdit)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 163, 22))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

        self.my_button.clicked.connect(self.my_function)

    def my_function(self):
        for idx in range(10):
            print('Executing iteration '+str(idx)+' ...')
            time.sleep(1) # replaces time-consuming task
        print('Finished!')

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.my_button.setText(_translate("MainWindow", "Execute"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())
Manu
  • 186
  • 3
  • 11
  • What do you use the `time.sleep(1)` for? Do you use it for a delay or is it to replace a time-consuming task? – eyllanesc Jan 14 '20 at 13:39
  • He put it in as a minimal example, so I'd imagine it's replacing a heavy task. Shouldn't matter either way for the problem though. – Peter Jan 14 '20 at 13:49
  • @eyllanesc yep, its for replacing a time-consuming task – Manu Jan 14 '20 at 14:18

1 Answers1

3

Golden rule of Qt: You should not execute tasks that consume a lot of time in the main thread of the GUI since they block the event loop of Qt.

Considering the above, you must execute the time-consuming task in another thread and send the information to the GUI thread through signals.

On the other hand you should not modify the class generated by Qt Designer, instead create another class that inherits from the appropriate widget and is filled in with the initial class.

import threading
import time

from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(163, 225)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.formLayout = QtWidgets.QFormLayout(self.centralwidget)
        self.formLayout.setObjectName("formLayout")
        self.my_button = QtWidgets.QPushButton(self.centralwidget)
        self.my_button.setObjectName("my_button")
        self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.my_button)
        self.textEdit = QtWidgets.QTextEdit(self.centralwidget)
        self.textEdit.setObjectName("textEdit")
        self.formLayout.setWidget(1, QtWidgets.QFormLayout.SpanningRole, self.textEdit)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 163, 22))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.my_button.setText(_translate("MainWindow", "Execute"))


class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    valueChanged = QtCore.pyqtSignal(int)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.valueChanged.connect(self.on_value_changed)
        self.my_button.clicked.connect(self.on_clicked)

    @QtCore.pyqtSlot()
    def on_clicked(self):
        threading.Thread(target=self.my_function, daemon=True).start()

    @QtCore.pyqtSlot(int)
    def on_value_changed(self, value):
        self.textEdit.append("Value: {}".format(value))

    def my_function(self):
        for idx in range(10):
            self.valueChanged.emit(idx)
            print("Executing iteration " + str(idx) + " ...")
            time.sleep(1)  # replaces time-consuming task
        print("Finished!")


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Many thanks for your answer and advices. In my case, my_function calls a function defined in another package. The 'for' loop is actually happening somewhere else. If possible I would like to avoid editing this loop. In other words, is it possible to "listen" to the prints of the function, without having to add 'self.valueChanged.emit(idx)' in the loop? – Manu Jan 14 '20 at 17:16
  • @Manu My answer based on your MRE so I will not be able to tell you if it is possible or not in other cases that it does not cover. On the other hand if you cannot modify the for-loop then it is very likely that it cannot be done. In conclusion, if you have a different characteristic, then you must provide an appropriate MRE so that I can objectively indicate whether or not it can be done. – eyllanesc Jan 14 '20 at 17:26
  • Thanks for your quick answer. Here below the new MRE – Manu Jan 14 '20 at 17:57
  • ok I'll create the followup as soon as my question limit lifts up – Manu Jan 14 '20 at 21:02