3

I'm currently coding my first tkinter GUI. I'm trying to make an interactive plot, using some scales so that the user can set the values of parameters affecting the plot. when I do this it starts lagging and my solution; working with threading is not working as intended.

In my real code there are multiple plots so the application started lagging as shown here:

from tkinter import *
from tkinter import ttk

import threading

import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

import time

import numpy as np

def create_window(root):
    frame1 = Frame(root)
    frame2 = Frame(root)
    frame1.grid(row=0,column=0)
    frame2.grid(row=0,column=1)

    figure1 = plt.Figure(figsize=(5,5))
    figure1.set_tight_layout(True)
    
    ax1 = figure1.add_subplot(111)
    canvas1 = FigureCanvasTkAgg(figure1, frame1)
    canvas1.get_tk_widget().grid(column=1,row=1, padx=10, pady=10)

    m.trace_add('write', lambda var=None, index=None, mode=None: update_plot_tracer(ax1, canvas1))

    m_scale = ttk.Scale(frame2, orient=VERTICAL, length=200, from_=100.0, to=0.0, variable=m)
    m_scale.grid(column=0, row=0)
    update_plot(ax1,canvas1)


def update_plot(ax, canvas):
    #to make it lag
    time.sleep(0.1)

    x = np.arange(0,10,1)
    y = m.get() * x
    ax.clear()
    ax.plot(x,y)
    ax.set_ylim([0, 100])
    canvas.draw()
def update_plot_tracer(ax, canvas, var=None, index=None, mode=None):
    update_plot(ax, canvas)


if __name__ == '__main__':

    root = Tk()
    m = DoubleVar(value = 10.0)
    create_window(root)

    root.mainloop()

My solution to that was to use threading so that the rest of the user interface is still usable while the plot is plotting. The problem I am running into now is that when the user drags the scale a lot there would be multiple threads called which interfere so I tried to stop the program from updating multiple times at the same time as shown below:

from tkinter import *
from tkinter import ttk

import threading

import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

import time

import numpy as np

def create_window(root):
    frame1 = Frame(root)
    frame2 = Frame(root)
    frame1.grid(row=0,column=0)
    frame2.grid(row=0,column=1)

    figure1 = plt.Figure(figsize=(5,5))
    figure1.set_tight_layout(True)
    
    ax1 = figure1.add_subplot(111)
    canvas1 = FigureCanvasTkAgg(figure1, frame1)
    canvas1.get_tk_widget().grid(column=1,row=1, padx=10, pady=10)

    m.trace_add('write', lambda var=None, index=None, mode=None: update_plot_tracer(ax1, canvas1))

    m_scale = ttk.Scale(frame2, orient=VERTICAL, length=200, from_=100.0, to=0.0, variable=m)
    m_scale.grid(column=0, row=0)
    update_plot(ax1,canvas1)

    m_entry = ttk.Entry(frame2, textvariable = m)
    m_entry.grid(column=0, row=1)

def update_plot(ax, canvas):
    
    x = np.arange(0,10,1)
    y = m.get() * x
    print('last used value of m', m.get())
    ax.clear()
    ax.plot(x,y)
    ax.set_ylim([0, 100])
    canvas.draw()
    #to make it lag
    time.sleep(0.2)

def update_plot_tracer(ax, canvas, var=None, index=None, mode=None):
    global thread1#just to make this example work, in my real code i have it as a class variable...
    if thread1 is None or not thread1.is_alive():
        thread1 = threading.Thread(target= lambda: update_plot(ax, canvas))
        thread1.start()
        
if __name__ == '__main__':
    thread1 = None
    root = Tk()
    
    m = DoubleVar(value = 10.0)
    create_window(root)

    root.mainloop()

Now I have the next problem: when the thread is started but the user keeps moving the mouse, the value he/she sees is not the one used in the plot, so I would like to do something like adding the threads to a queue, but then I will run into the problem that this queue could get very long and it would take some time to update to the newest state, even though all the threads between the currently running and the newest added are useless, but as far as I have understood it, once I have added a thread to the queue (from the queue bib) it cant be removed. I have never worked with most of this so any help is appreciated.

Also, I want to keep it as interactive as possible, so to only update once the user lets go of the scale is not my preferred way of doing this.

EDIT: The question is how do I create a queue of threads from which I can remove entries?

4

1 Answer 1

2

Even though Tkinter supports manipulating GUI in a thread different than the thread that created the Tcl interpreter(the root Tk instance), it's best to avoid that scenario because the implementation is not perfect and most GUI frameworks do not support that scenario. I recommend to do a data processing work in a background thread and a plotting work in the main thread.

Now, let's move to the main topic on the queue. You don't need to implement a queue of threads. You can just start a new thread when the existing thread is finished. Use the after_idle() to schedule a function like this. Also, don't forget to join the thread.

...
def update_plot_tracer(ax, canvas, var=None, index=None, mode=None):
    global thread1
    if thread1 is None:
        def entry():
            update_plot(ax, canvas)
            root.after_idle(on_end_thread)
        def on_end_thread(): # This will be run in the main thread.
            global thread1
            if thread1.invalidated:
                root.after_idle(lambda: update_plot_tracer(ax, canvas))
            thread1.join()
            thread1 = None
        thread1 = threading.Thread(target=entry)
        thread1.invalidated = False
        thread1.start()
    else:
        thread1.invalidated = True
...
5
  • Thanks a lot! I think this is exactly what I was looking for, some questions: first why do you do thread1.join()? doesnt that force the mainloop to wait for the thread making it lag again? or have I misunderstood thread1.join()?
    – David
    Commented May 11, 2023 at 9:23
  • and also you point out that I should do the plotting in the mainloop and the calculation in a thread, so lets say I have a function that calculates the new y value to be plottet, how would I make sure it is always calculated with the newest value of m and that afterwards it is plottet? also with after idle probably?
    – David
    Commented May 11, 2023 at 9:29
  • The thread1.join() will not block because the thread entry will exit soon. It's a convention to join worker threads. In most languages and frameworks, joining the thread will release the language/framework/system resources for a thread. It also helps to clearly define threading model of the application.
    – relent95
    Commented May 11, 2023 at 9:44
  • For the second question, yes, you can call the after_idle() in a worker thread to schedule a plotting work. I'll rename the join() function to the on_end_thread(). Then, you can plot in that on_end_thread().
    – relent95
    Commented May 11, 2023 at 9:51
  • ok so what I have done now is to put my calc_y in the entry() function and the update_plot(ax, canvas) in the on_end_thread() function, I hope this is what you meant, it seems to work perfectly now, im just still very new to threading
    – David
    Commented May 11, 2023 at 10:40

Not the answer you're looking for? Browse other questions tagged or ask your own question.