使用线程在tkinter中从用户输入更新绘图而无延迟。

huangapple go评论56阅读模式
英文:

Update plot in tkinter from user input without lag using threading

问题

Sure, here's the translated part of your text:

我目前正在编写我的第一个tkinter GUI。我试图制作一个交互式图,使用一些标度,以便用户可以设置影响图的参数值。当我这样做时,它开始出现滞后,我的解决方法是使用线程,但效果不如预期。

在我的实际代码中有多个图,因此应用程序开始出现滞后,如下所示:

# 你的Python代码

我的解决方案是使用线程,以便在绘制图表时仍然可以使用其他用户界面。现在我遇到的问题是,当用户大幅拖动标度时,会调用多个线程,它们会相互干扰,所以我尝试阻止程序同时多次更新,如下所示:

# 你的Python代码

现在我面临的问题是,当启动线程但用户继续移动鼠标时,用户看到的值与绘图中使用的值不同。因此,我想做一些类似于将线程添加到队列中的事情,但随后我将遇到问题,即这个队列可能会变得非常长,需要一些时间才能更新到最新状态,尽管当前正在运行和最新添加的所有线程都是无用的。但据我所了解,一旦将线程添加到队列中,就无法删除。我从未使用过大部分这些内容,因此任何帮助都将不胜感激。

此外,我希望保持尽可能的互动性,因此只有在用户释放标度时才进行更新不是我的首选方式。

编辑:问题是如何创建一个线程队列,从中可以删除条目?

英文:

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?

答案1

得分: 1

即使Tkinter支持在与创建Tcl解释器(根Tk实例)不同的线程中操作GUI,最好避免这种情况,因为实现不完美,大多数GUI框架不支持这种情况。我建议在后台线程中进行数据处理工作,并在主线程中进行绘图工作。

现在,让我们转到队列的主题。您不需要实现线程队列,只需在现有线程完成时启动新线程。使用after_idle()调度一个像这样的函数。还别忘了加入线程。

...
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(): # 这将在主线程中运行。
            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
...
英文:

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
...

huangapple
  • 本文由 发表于 2023年5月10日 18:15:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/76217229.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定