有没有办法延迟对 matplotlib 滑块的 on_changed() 调用?

Is there a way to delay the on_changed() call for matplotlib sliders?

我目前正在创建一个 tkinter GUI,它显示一个带有两个滑块的 matplotlib 图。滑块用于选择数据范围(它们连接到图上的垂直线)。

我 运行 遇到的问题是,当我快速更改滑块几次时,GUI 会冻结。我相信这是由于我使用 on_changed() 调用来更新滑块。当您快速移动滑块时,我相信 on_changed() 被多次调用的速度快于程序可以跟上的速度,从而导致冻结。

有什么方法可以为 on_changed() 调用添加延迟,以便它只查看滑块是否在每个给定时间间隔(例如 100 毫秒)发生变化?

下面是我发现的示例代码,它以我描述的方式冻结。

import tkinter as tk
from tkinter.ttk import Notebook
from tkinter import Canvas
from tkinter import messagebox as msg

import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

from matplotlib.widgets import Slider, Button, RadioButtons



class LukeOutline(tk.Tk):

    def __init__(self):
        # Inherit from tk.Tk
        super().__init__()

        # Title and size of the window
        self.title('Luke Outline')
        self.geometry('600x400')

        # Create the drop down menus
        self.menu = tk.Menu(self,bg='lightgrey',fg='black')

        self.file_menu = tk.Menu(self.menu,tearoff=0,bg='lightgrey',fg='black')
        self.file_menu.add_command(label='Add Project',command=self.unfinished)

        self.menu.add_cascade(label='File',menu=self.file_menu)

        self.config(menu=self.menu)

        # Create the tabs (Graph, File Explorer, etc.)
        self.notebook = Notebook(self)

        graph_tab = tk.Frame(self.notebook)
        file_explorer_tab = tk.Frame(self.notebook)

        # Sets the Graph Tab as a Canvas where figures, images, etc. can be added
        self.graph_tab = tk.Canvas(graph_tab)
        self.graph_tab.pack(side=tk.TOP, expand=1)

        # Sets the file explorer tab as a text box (change later)
        self.file_explorer_tab = tk.Text(file_explorer_tab,bg='white',fg='black')
        self.file_explorer_tab.pack(side=tk.TOP, expand=1)

        # Add the tabs to the GUI
        self.notebook.add(graph_tab, text='Graph')
        self.notebook.add(file_explorer_tab, text='Files')

        self.notebook.pack(fill=tk.BOTH, expand=1)

        # Add the graph to the graph tab
        self.fig = Figure()
        graph = FigureCanvasTkAgg(self.fig,self.graph_tab)
        graph.get_tk_widget().pack(side='top',fill='both',expand=True)
        EllipseSlider(self.fig)

    #------------------------------------------------------
    def quit(self):
        '''
        Quit the program
        '''
        self.destroy()

    #------------------------------------------------------
    def unfinished(self):
        '''
        Messagebox for unfinished items
        '''
        msg.showinfo('Unfinished','This feature has not been finished')

    #------------------------------------------------------
    def random_graph(self):
        x = list(range(0,10))
        y = [i**3 for i in x]

        fig = Figure()
        axes = fig.add_subplot(111)
        axes.plot(x,y,label=r'$x^3$')
        axes.legend()

        return fig

#----------------------------------------------------------

class EllipseSlider():

    #------------------------------------------------------
    def __init__(self,fig):
        self.fig = fig

        # Initial values
        self.u = 0.     #x-position of the center
        self.v = 0.     #y-position of the center
        self.a = 2.     #radius on the x-axis
        self.b = 1.5    #radius on the y-axis

        # Points to plot against
        self.t = np.linspace(0, 2*np.pi, 100)

        # Set up figure with centered axes and grid
        self.ax = self.fig.add_subplot(111)
        self.ax.set_aspect(aspect='equal')
        self.ax.spines['left'].set_position('center')
        self.ax.spines['bottom'].set_position('center')
        self.ax.spines['right'].set_color('none')
        self.ax.spines['top'].set_color('none')
        self.ax.xaxis.set_ticks_position('bottom')
        self.ax.yaxis.set_ticks_position('left')
        self.ax.set_xlim(-2,2)
        self.ax.set_ylim(-2,2)
        self.ax.grid(color='lightgray',linestyle='--')

        # Initial plot
        self.l, = self.ax.plot(self.u+self.a*np.cos(self.t),
            self.v+self.b*np.sin(self.t),'k')

        # Slider setup
        self.axcolor = 'lightgoldenrodyellow'
        self.axb = self.fig.add_axes([0.25, 0.1, 0.65, 0.03], facecolor=self.axcolor)
        self.axa = self.fig.add_axes([0.25, 0.15, 0.65, 0.03], facecolor=self.axcolor)

        self.sb = Slider(self.axb, 'Y Radius', 0.1, 2.0, valinit=self.b)
        self.sa = Slider(self.axa, 'X Radius', 0.1, 2.0, valinit=self.a)

        # Call update as slider is changed
        self.sb.on_changed(self.update)
        self.sa.on_changed(self.update)

        # Reset if reset button is pushed
        self.resetax = self.fig.add_axes([0.8,0.025,0.1,0.04])
        self.button = Button(self.resetax, 'Reset', color=self.axcolor, hovercolor='0.975')

        self.button.on_clicked(self.reset)

        # Color button setup
        self.rax = self.fig.add_axes([0.025, 0.5, 0.15, 0.15], facecolor=self.axcolor)
        self.radio = RadioButtons(self.rax, ('red', 'blue', 'green'), active=0)

        self.radio.on_clicked(self.colorfunc)

    #------------------------------------------------------

    def update(self, val):
        '''
        Updates the plot as sliders are moved
        '''
        self.a = self.sa.val
        self.b = self.sb.val
        self.l.set_xdata(self.u+self.a*np.cos(self.t))
        self.l.set_ydata(self.u+self.b*np.sin(self.t))

    #------------------------------------------------------

    def reset(self, event):
        '''
        Resets everything if reset button clicked
        '''
        self.sb.reset()
        self.sa.reset()

    #------------------------------------------------------

    def colorfunc(self, label):
        '''
        Changes color of the plot when button clicked
        '''
        self.l.set_color(label)
        self.fig.canvas.draw_idle()

#----------------------------------------------------------

if __name__ == '__main__':
    luke_gui = LukeOutline()
    luke_gui.mainloop()
    es = EllipseSlider()

(更新):

我已经实施了您显示的更正并且代码看起来按预期工作,除了我现在使用的垂直滑块在我第二次移动滑块之前不会更新它们的位置。请参阅下面我的代码部分:

        # Call update if slider is changed
        self.sa.on_changed(self.schedule_update)
        self.sb.on_changed(self.schedule_update)

    def update(self, value):
        print(value)
        self.vert_a.set_xdata(value)
        self.vert_b.set_xdata(self.sb.val)
        root.update()

    def schedule_update(self, new_value):
        if self.after_id:
            root.after_cancel(self.after_id)
        self.after_id = root.after(1000, self.update, new_value)


root = tk.Tk()
mainapp = MainApp(root)
root.mainloop()

Tkinter 有一个名为 after 的方法用于延迟命令的执行。您的滑块可以使用它来延迟滑块的效果一两秒钟。如果滑块移动,它可以取消之前预定的命令并提交新命令。

为了简单起见,这里有一个使用普通 tkinter 的示例。 运行 代码,然后快速或缓慢地移动滑块。您会看到标签只会在您没有移动滑块一整秒后更新。

import tkinter as tk


after_id = None
def schedule_update(new_value):
    global after_id
    if after_id:
        root.after_cancel(after_id)
    after_id = root.after(1000, update_label, new_value)

def update_label(new_value):
    label.configure(text=f"New value: {new_value}")

root = tk.Tk()
label = tk.Label(root, text="", width=20)
scale = tk.Scale(
    root, from_=1, to=100,
    orient="horizontal", command=schedule_update
)

label.pack(side="top", fill="x", padx=20, pady=(10,0))
scale.pack(side="bottom", fill="x", padx=20, pady=10)

root.mainloop()