Search code examples
pythonpython-3.xpyqtpyqt5qthread

How can I update the text in real time while doing the calculations in a QLabel, without the interface freezing?


I have a problem regarding the real-time update in a Q Label, because when I click the "start" button, the screen stays static for a few seconds while it performs the calculations, but what I need is that in the QLabel the status messages are displayed while advancing with the simulation. I have tried with the use of threads, but I do not understand it very well since it does not work out, if someone could help me with this I would appreciate it, since I do not handle much the Thread issue in Pyqt5. (I do not understand) Thank you very much

I attach both the code and the images of the interface:


INTERFACE

Before execution:

enter image description here

During execution:

enter image description here

After execution:

enter image description here


Codigo

import sys
from PyQt5.QtWidgets import QStyleFactory,QApplication, QMainWindow,QFileDialog
from PyQt5.uic import loadUi
from PyQt5.QtGui import QTextCursor
from PyQt5.QtCore import *
import random
import time
import pandas as pd


parametros = [['Area', 0, 5]]

class simulacion(QMainWindow):

    def __init__(self, parent=None):

        QMainWindow.__init__(self, parent)
        loadUi('simulacion.ui', self)
        self.setStyle(QStyleFactory.create('Fusion'))
        self.numsim = 10000000

        self.pushButton.clicked.connect(self.calibracion)
        self.pushButton_2.clicked.connect(self.save)

    def cerrar(self):
        self.close()

    def calibracion(self):
        self.montecarlo(self.numsim)

    def generar_aleatorio(self):
        aleatorio = []
        for i in range(len(parametros)):
            aleatorio.append(random.uniform(parametros[i][1],parametros[i][2]))
        return aleatorio

    def area(self,x1):
        area = 3.1416 * x1**2
        return area

    def estado(self,starttime,last_print,contador, n, area):
        global ult_print
        acttime = time.time()
        if acttime - last_print >= 2:
            avg_time_per_run = (acttime - starttime) / (contador + 1)
            timestr = time.strftime("%H:%M:%S", time.gmtime(round(avg_time_per_run * (n - (contador + 1)))))

            text = ('Simulacion %i de %i - Tiempo estimado restante: %s\n' % (contador, n,timestr)+'Area = %5.3f' % (area)+'\n\n')

            self.textEdit.moveCursor(QTextCursor.End)
            self.textEdit.insertPlainText(text)
            self.textEdit.moveCursor(QTextCursor.End)

            ult_print = time.time()
            return text

    def montecarlo(self,n):
        QApplication.processEvents()
        global ult_print
        text='Iniciando iteraciones con {} repeticiones...\n\n'.format(n)
        self.textEdit.setText(text)
        self.textEdit.moveCursor(QTextCursor.End)
        ult_print = time.time()
        starttime = time.time()
        contador = 0
        self.data=[]
        self.num_sim=[]

        QApplication.setOverrideCursor(Qt.WaitCursor)

        while contador < n:
            contador +=1
            #Generar el numero aleatorio
            z = self.generar_aleatorio()
            #Simulacion del modelo con el numero aleatorio
            y = self.area(z[0])
            #Calculo de la funcion objetivo
            self.estado(starttime,ult_print,contador,n,y)

        QApplication.setOverrideCursor(Qt.CustomCursor)


    def save(self):
        file,_=QFileDialog.getSaveFileName(self,'Guardar Archivo de Simulación','','(*.csv)')

        if file:
            columns= []
            for valor in self.num_sim:
                columns.append('Simulación '+str(valor))
            #print(columns)

            df = pd.DataFrame(self.data,index=columns)
            a = df.transpose()
            a.to_csv(file,sep=';',index=False,encoding='utf-8')

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

Here I show the separate code of the call of simulacion.ui

Codigo

import sys
from PyQt5.QtWidgets import QStyleFactory,QApplication, 
QMainWindow,QFileDialog
from PyQt5.uic import loadUi
from PyQt5.QtGui import QTextCursor
from PyQt5.QtCore import *
import random
import time
import pandas as pd

'''This part of the code calls the window designed in QTDesigner'''

class simulacion(QMainWindow):

    def __init__(self, parent=None):

        QMainWindow.__init__(self, parent)
        loadUi('simulacion.ui', self)
        self.setStyle(QStyleFactory.create('Fusion'))
        self.numsim = 10000000

        self.pushButton.clicked.connect(self.calibracion)
        self.pushButton_2.clicked.connect(self.save)

    def cerrar(self):
        self.close()

    def calibracion(self):
        self.montecarlo(self.numsim)

    def save(self, data):
        file,_=QFileDialog.getSaveFileName(self,'Guardar Archivo de Simulación','','(*.csv)')
        if file:
            df = pd.DataFrame(data)
            df.to_csv(file,sep=';',index=False,encoding='utf-8')
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = simulacion()
widget.show()
sys.exit(app.exec_())

This part of the code is where I describe the functions that will be used within the Monte Carlo algorithm. where the texts that are specified should be shown in the QTextEdit It is important to mention that in the Montecarlo function, the data list is generated, which is what keeps all the simulations performed. This variable is necessary so that the save function in the simulation class can be executed

parametros = [['Area', 0, 5]]

def generar_aleatorio():
    aleatorio = []
    for i in range(len(parametros)):
        aleatorio.append(random.uniform(parametros[i][1],parametros[i][2]))
        return aleatorio

def area(x1):
    area = 3.1416 * x1**2
    return area

def estado(starttime,last_print,contador, n, area):
    global ult_print
    acttime = time.time()
    if acttime - last_print >= 2:
        avg_time_per_run = (acttime - starttime) / (contador + 1)
        timestr = time.strftime("%H:%M:%S", time.gmtime(round(avg_time_per_run * (n - (contador + 1)))))

        text = ('Simulacion %i de %i - Tiempo estimado restante: %s\n' % (contador, n,timestr)+'Area = %5.3f' % (area)+'\n\n')

        self.textEdit.moveCursor(QTextCursor.End)
        self.textEdit.insertPlainText(text)
        self.textEdit.moveCursor(QTextCursor.End)

        ult_print = time.time()
        return text

def montecarlo(n):
        global ult_print
        text='Iniciando iteraciones con {} repeticiones...\n\n'.format(n)
        #self.textEdit.setText(text)
        #self.textEdit.moveCursor(QTextCursor.End)
        ult_print = time.time()
        starttime = time.time()
        contador = 0
        data=[]
        num_sim=[]
        #QApplication.setOverrideCursor(Qt.WaitCursor)

        while contador < n:
            contador +=1
            #Generar el numero aleatorio
            z = generar_aleatorio()
            #Simulacion del modelo con el numero aleatorio
            y = area(z[0])
            #Calculo de la funcion objetivo
            estado(starttime,ult_print,contador,n,y)
            data.append(list(z+y))

        #QApplication.setOverrideCursor(Qt.CustomCursor)

Solution

  • The appropriate solution is to execute the blocking task in another thread, and send the data to the GUI that is in the main thread through signals, Qt prohibits updating the GUI from another than the main one, using processEvents is forcing the GUI to update some parameters which does not guarantee a correct operation, you can read more about this topic in the following link: Should I use QCoreApplication::processEvents() or QApplication::processEvents()?.

    In the following example I will use the native python threads next to 2 signals, one will send the text and the other the data.

    import random
    import sys
    import time
    from threading import Thread
    import pandas as pd
    
    from PyQt5.QtCore import QObject, pyqtSignal, Qt
    from PyQt5.QtWidgets import QApplication, QFileDialog, QStyleFactory, QMainWindow
    from PyQt5.uic import loadUi
    
    parametros = [['Area', 0, 5]]
    
    
    def generar_aleatorio():
        return random.uniform(*parametros[0][1:])
    
    
    def area(x1):
        area = 3.1416 * x1 ** 2
        return area
    
    
    class Helper(QObject):
        send_signal = pyqtSignal(str)
        data_signal = pyqtSignal(list)
    
    
    helper = Helper()
    
    
    def estado(starttime, last_print, contador, n, area):
        acttime = time.time()
        if acttime - last_print <= 2:
            avg_time_per_run = (acttime - starttime) / (contador + 1)
            timestr = time.strftime("%H:%M:%S", time.gmtime(round(avg_time_per_run * (n - (contador + 1)))))
    
            text = 'Simulacion %i de %i - Tiempo estimado restante: %s\n' % (contador, n, timestr) \
                   + 'Area = %5.3f\n\n' % area
            helper.send_signal.emit(text)
    
    
    def montecarlo(n):
        data = []
        text = 'Iniciando iteraciones con {} repeticiones...\n\n'.format(n)
        helper.send_signal.emit(text)
        ult_print = time.time()
        starttime = time.time()
    
        for contador in range(n):
            z = generar_aleatorio()
            y = area(z)
            estado(starttime, ult_print, contador + 1, n, y)
            ult_print = time.time()
            time.sleep(0.001)
            data.append([z, y])
        helper.data_signal.emit(data)
    
    
    class simulacion(QMainWindow):
        def __init__(self, parent=None):
            QMainWindow.__init__(self, parent)
            loadUi('simulacion.ui', self)
            self.setStyle(QStyleFactory.create('Fusion'))
            self.numsim = 10000000
    
            self.pushButton.clicked.connect(self.calibracion)
            self.pushButton_2.clicked.connect(self.save)
    
        def calibracion(self):
            thread = Thread(target=montecarlo, args=(self.numsim,))
            helper.send_signal.connect(self.textEdit.append, Qt.QueuedConnection)
            helper.data_signal.connect(self.obtener_resultados)
            thread.start()
    
        def obtener_resultados(self, data):
            self.data = data
    
        def save(self, data):
            file, _ = QFileDialog.getSaveFileName(self, 'Guardar Archivo de Simulación', '', '(*.csv)')
            if file:
                df = pd.DataFrame(self.data)
                df.to_csv(file, sep=';', index=False, encoding='utf-8')
    
    
    app = QApplication(sys.argv)
    w = simulacion()
    w.show()
    sys.exit(app.exec_())