Search code examples
pythontkintertoplevel

How to Display and Update a Message or Label containg results using TopLevel Tkinter inside a for loop at every iteration in Python?


I am building a GUI Python code using tkinter. I have a CSTR model that is solved inside a for loop; the values are updated inside the for loop. I have four parameters that are being updated T,Ca,Tc,q. I am using the TopLevel() to display the results in EVERY iteration inside for loop. In other words, I want the results at each iteration to be shown once the results are out on a display, and once I am in another interation, I want to open a new window file again and update the values with the new calculated values in the for loop. I have used the destroy function using the same time of the iterations in the for loop.

Kindly find my code attached below ( I have attached part of the code only for simplicity):

 # Time Interval (min)
t = np.linspace(0,800,401) # every 2 seconds
duration=8000 # I also tried 2000,4000,6000,1000,etc.

    # Simulate CSTR
    for i in range(len(t)-1):
        # simulate one time period (0.02 sec each loop)
        ts = [t[i],t[i+1]]
        #read CSTR Process T and Ca (to be controlled)

        y = odeint(cstr,x0,ts,args=(u[i],u2[i],Tf,Caf))
        # retrieve measurements
        Ca[i+1] = y[-1][0]
        T[i+1] = y[-1][1]
        # insert measurement
        m.T.MEAS = T[i+1]
        m.Ca.MEAS=Ca[i+1]

        #run MPC controller

        # solve MPC
        m.solve(disp=True)

        # retrieve new Tc and q values

        u[i+1] = m.Tc.NEWVAL
        u2[i+1]=m.q.NEWVAL

        #run optimization

        x=optimizer(numa,numb,numc,numd,Tsp[i+1],Casp[i+1],u[i+1],u2[i+1],numi,numj,nume,numf,numg,numh,numk,numl) # or put T and Ca (not sp)


        #updating the setpoints of the controller from the optimizer

        #Set the setpoint temperature equal to the optimized Temperature

        Tsp[i+1]=x[1]

        m.T.SPHI = Tsp[i+1] + DT
        m.T.SPLO = Tsp[i+1] - DT

        #Set the setpoint conc. equal to the optimized conc.
        Casp[i+1]=x[4]

        m.Ca.SPHI = Casp[i+1] + DT
        m.Ca.SPLO = Casp[i+1] - DT

        # retrieve new Tc and q values

        #update Tc and q values from optimizer as targets


        f[i]=m.Tc.NEWVAL #MPC
        f2[i]=x[2] #Optimizer (target)

        z[i]=m.q.NEWVAL #MPC
        z2[i]=x[3] #Optimizer (target)



        x2=optimize(f[i],f2[i],nume,numf)# MINIMIZES THE ERROR DIFFERENCE BETWEEN MPC AND OPTIMIZER Tc value
        x3=optimize2(z[i],z2[i],numg,numh)# MINIMIZES THE ERROR DIFFERENCE BETWEEN MPC AND OPTIMIZER q value

        #updating the Tc and q MV values

        u[i+1] = x2
        u2[i+1]=x3

        # update initial conditions
        x0[0] = Ca[i+1]
        x0[1] = T[i+1]
        #to show the results on GUI

        top1=Toplevel()

        output1=" q optimum= %d m^3/s \n"%(z[i]) #takes the output q of MPC
        output1+="q target= %d m^3/s \n"%(z2[i])#takes the output of the optimizer
        output1+="Tc optimum= %d K \n"%(f[i])#takes the output Tc of MPC
        output1+="Tc target= %d K \n"%(f2[i])#takes the output of the optimizer
        top1.title('Manipulated Variables')

        Message(top1,text=output1,padx=30,pady=30).pack()
        top1.after(duration,top1.destroy)


What is actually happening is that the window is only opening once all the runs of the for loops are done (take a huge time) and not at every iteration/calculation in the for loop! How can I display the window inside the for loop at every iteration and NOT once all the loop is done? Is there some sort of function or something where I can manually force the window to be displayed? For instance, I was able to shows plots using plt module that are updated in every iteration inside the for loop, I want to do exactly the same using toplevel() .

Any suggestions?

Thank you.


Solution

  • In a GUI, your code is basically a guest in the GUI toolkit's event loop (mainloop in tkinter).

    So you cannot have a long-running loop in a GUI program; you have to structure your code differently. For example, say that you have a loop like this;

    # *Long* list of data
    data = [ ... ]
    # storage for results
    result = []
    for item in data:
        rv = process(item)
        result.append(rv)
    

    If you were to run such a loop in a GUI program, it would interrupt the flow of events. In effect it would freeze the GUI and render is unresponsive.

    So in a tkinter GUI, you would have to do the calculations in a timeout handler. Below a sketch of the principle of dividing up a long-running job into small pieces that can be handled inside the GUI's event-loop.

    import tkinter as tk
    from tkinter import messagebox
    
    # *Long* list of data
    data = [ ... ]
    # storage for results
    result = []
    index = 0
    
    def iteration():
        '''Handles one iteration in the event loop.'''
        rv = process(data[index])
        result.append(rv)
        index += 1
        if index < len(data):
            root.after(100, iteration)
        else:
            messagebox.showinfo('Calculations', 'Finished!')
    
    
    def create_UI():
        # Create and place the widgets here.
        pass
    
    
    if __name__ == '__main__':
        root = tk.Tk(None)
        widgets = create_UI(root)
        root.after(100, iteration)
        root.mainloop()
    

    As you can see, your code has to be structured differently in this case.

    Note that on CPython only one thread at a time can be executing Python bytecode, so using threads is not really a good solution for this. Especially since a lot of GUI toolkits are not thread-safe, so you should only call GUI functions from one thread.

    Another solution would be to run the calculation in a different program. That program could then communicate with the GUI using a Queue. You would have to use a a timeout function in the GUI to monitor the Queue at regular intervals and display the results. This is not a simpler solution, but might offer higher perfomance it you can divide the calculations up over several processes.

    Edit:

    Note that in the first solution, each iteration should only take a relateively small amount of time, say e.g. 100 ms. If it would take much longer, the GUI would feel unresponsive. If it is unfeasible to to that, then use the second solution (a separate program).

    I don't really have a handy reference for this problem. But a simple pattern goes like this;

    You write two programs.

    The first program is a simple Python script that does its calculations in a regular for-loop. The only special thing is that at the end of each iteration, it writes the values of T, Ca, Tc and q to a file.

    The second program is a Python tkinter program that uses the after method to inspect the modification time of the aforementioned file. If it detects that the file has changed, it reads the file and updates its display.

    This approach completely decouples the problems of doing the calculations and displaying the intermediate results, meaning you can write and test them separately.

    A general name for this solution is called a "file watcher", I think. This solution is so common I can't recall where I say it first.