2

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)
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Peter
  • 23
  • 1
  • 4
  • What have you been trying with threads? A QThread should be doing what you want. – Maxime B Apr 05 '18 at 22:18
  • well as I read in other answers with the threads I can do that the update of the text is done in the Qlabel when it is processed, but I do not understand much, you are a beginner in this part. I tried to implement this example in my code https://gist.github.com/jazzycamel/8abd37bf2d60cce6e01d, but it did not work, I made mistake after mistake and it did not work out – Peter Apr 05 '18 at 22:42
  • First of all it is terrible performance to call 'self.estado(starttime,ult_print,contador,n,y)' at each montecarlo iteration, try to call it every 10000 runs or so with a 'if contador % 10000 == 0' – Maxime B Apr 05 '18 at 23:19
  • So that is the reason why the interface freezes ?. The code that I give for example is simple since the one I work with uses other functions within the while, and if I put the number of iterations 1000 it takes a minute. – Peter Apr 06 '18 at 06:18
  • The self.state is printed every 2 seconds but you could increase the time to 5 seconds, but the function is simple since it only calculates one area so I put a number of iterations high so that the iterations that it does are seen. But in what way can I adapt the thread so that the message is displayed while doing the calculation, as I configure the signal, sincerely in it I do not understand – Peter Apr 06 '18 at 06:19
  • Could you link your simulacion.ui so I can test on my side? – Maxime B Apr 06 '18 at 17:23
  • @Peter Combining the logic part and the GUI part is not correct, you could separate your algorithm from your simulation and would be happy to provide an optimal solution. – eyllanesc Apr 06 '18 at 17:25
  • @eyllanesc all the algorithm is developed in montecarlo, hence I call the functions that are previously defined. How could the code pass you? How could I get in touch with you since I would like very much to be able to learn this from the threads within PyQt. – Peter Apr 09 '18 at 06:28
  • @Peter separate the algorithm, do not join it with the GUI and I will show you the optimal way to implement the solution, the one you have accepted is not the correct way, you are forcing things, that's why you have those downvotes. – eyllanesc Apr 09 '18 at 06:33
  • @eyllanesc I just edited the entry and in the final part I separated the GUI and the algorithm, that way is it ok? – Peter Apr 09 '18 at 07:14

1 Answers1

1

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_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • It worked very well, thanks for the solution. I have a doubt in the calibration function within the GUI, specifically in the second line, it is necessary to put the "Qt.QueuedConnection" ?? – Peter Apr 09 '18 at 21:03
  • @Peter one of the python's philosophies is: better explicit than implicit, by default the connection is Qt.AutoConnection, with that flag you indicate to PyQt to decide, and it will see which one is better, but this can be dangerous. the flag Qt.QueuedConnection is oriented for connections between elements that live in different threads, so it is better to make explicit the type of connection, if you want to know more read: http://doc.qt.io/qt-5/qt.html#ConnectionType-enum – eyllanesc Apr 09 '18 at 21:10
  • And when establishing the thread, in what way can I change the color of a specific text, in a qtextedit? Can you put it in bold? – Peter Jun 13 '18 at 13:50
  • @Peter that change must be done from the GUI thread, what you should do is send the necessary parameters such as the position and in the main thread make that change. – eyllanesc Jun 13 '18 at 13:52
  • Could you help me with an example? – Peter Jun 14 '18 at 21:11
  • @Peter First try to solve it yourself, when you have a problem with a real code and with a specific question I will be happy to help you. Your current problem is very open, for example it does not indicate what your entry is – eyllanesc Jun 14 '18 at 21:16