简体   繁体   中英

Tkinter scale widget not updating in realtime

I'm trying to use Tkinter.Scale to produce a slider that changes the data within a matplotlib, however I am having issues in getting the plots to update in realtime without having to regenerate the plot window each time.

If I run my code as so, then it works well, but it creates a new window each time I move the slider which is hard to focus upon visually.

import matplotlib.pyplot as plt
import numpy as np
import tkinter as tk

master = tk.Tk()

def update(val):
    plt.close()

    global idx
    idx = np.array(w.get())

    t1 = np.arange(0.0, 5.0, 0.1)
    a1 = np.sin(idx*np.pi *t1)
    a2 = np.sin((idx/2)*np.pi*t1)
    a3 = np.sin((idx/4)*np.pi*t1)
    a4 = np.sin((idx/8)*np.pi*t1)

    """Plotting of data"""  
    fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, sharey = False) # create figure
    plt.tight_layout()
    ax1.plot(t1, a1) 
    ax2.plot(t1, a2) 
    ax3.plot(t1, a3) 
    ax4.plot(t1, a4) 

w = tk.Scale(master, from_=0, to=10, command = update)
w.pack()
tk.mainloop()

I would like the slider to simply re-plot the data each time, however when I move the commands to create the figure preceding the function, as shown below, it no longer updates the plots when I move the slider, but only when the slider is closed.

import matplotlib.pyplot as plt
import numpy as np
import tkinter as tk


fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, sharey = False) # create figure
plt.tight_layout()

master = tk.Tk()

def update(val):

    ax1.cla() # clears the entire current figure but leaves the window
    ax2.cla()
    ax3.cla()
    ax4.cla()

    global idx
    idx = np.array(w.get())

    t1 = np.arange(0.0, 5.0, 0.1)
    a1 = np.sin(idx*np.pi *t1)
    a2 = np.sin((idx/2)*np.pi*t1)
    a3 = np.sin((idx/4)*np.pi*t1)
    a4 = np.sin((idx/8)*np.pi*t1)

    """Plotting of data"""  
    ax1.plot(t1, a1) 
    ax2.plot(t1, a2) 
    ax3.plot(t1, a3) 
    ax4.plot(t1, a4) 

w = tk.Scale(master, from_=0, to=10, command = update)
w.pack()
tk.mainloop()

Has anyone got any ideas on how to get the data only, and not the whole window, to update as the slider is moved? Apologies if this has been asked already, but I'm failing to find it if so. I cannot use the matplotlib slider option as in the actual script I am sweeping an string variable extracted from a .txt file, not an integer.

From the description of your problem I am assuming you want to have some plots that update with each tick of the slider and you do not want the plot recreated each time. The code you have provided does not even create a plot as you don't do anything with fig . That said there are a few changed you need.

First off when creating a plot inside tkinter IMO it is best to use the FugureCanvasTkAgg method in matplotlib. This will allow us to draw everything onto a canvas. We also want to cla() each axis instead of close() the plot. If you close the plot you will need to rebuild it so the best thing to do is clear the axis each time you update.

Here I would first build the plot with the value of zero from the slider and then let the function update the subplots only. This allows us to keep the plot intact and only change the lines in the subplots so we do not need to recreate the plot each time.

Take a look at the below code.

Update: Added in the toolbar and "TkAgg" per ImportanceOfBeingErnest comments.

import numpy as np
import tkinter as tk
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg


master = tk.Tk()

def update(val):
    global idx, chart_frame, ax1, ax2, ax3, ax4
    ax1.cla()
    ax2.cla()
    ax3.cla()
    ax4.cla()
    idx = np.array(w.get())
    t1 = np.arange(0.0, 5.0, 0.1)
    a1 = np.sin(idx*np.pi *t1)
    a2 = np.sin((idx/2)*np.pi*t1)
    a3 = np.sin((idx/4)*np.pi*t1)
    a4 = np.sin((idx/8)*np.pi*t1)
    ax1.plot(t1, a1) 
    ax2.plot(t1, a2) 
    ax3.plot(t1, a3) 
    ax4.plot(t1, a4) 
    canvas.draw()

w = tk.Scale(master, from_=0, to=10, command = update)
w.grid(row=0, column=0)

idx = np.array(w.get())

fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, sharey = False)
plt.tight_layout()

canvas = FigureCanvasTkAgg(fig, master)
canvas.get_tk_widget().grid(row=0, column=1)
toolbar_frame = tk.Frame(master)
toolbar_frame.grid(row=1, column=1, sticky="w")
NavigationToolbar2TkAgg(canvas, toolbar_frame)
canvas.draw()

master.mainloop()

Personally I feel that writing code in a non OOP method is frustrating and harder to manage. I think this would be better written in OOP so here is my example for that.

import numpy as np
import tkinter as tk
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg


class MyPlot(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.w = tk.Scale(self, from_=0, to=10, command = self.update)
        self.w.grid(row=0, column=0)

        fig, (self.ax1, self.ax2, self.ax3, self.ax4) = plt.subplots(4, sharey = False)
        plt.tight_layout()

        self.canvas = FigureCanvasTkAgg(fig, self)
        self.canvas.get_tk_widget().grid(row=0, column=1)
        toolbar_frame = tk.Frame(self)
        toolbar_frame.grid(row=1, column=1, sticky="w")
        NavigationToolbar2TkAgg(self.canvas, toolbar_frame)
        self.canvas.draw()

    def update(self, val):
        self.ax1.cla()
        self.ax2.cla()
        self.ax3.cla()
        self.ax4.cla()
        idx = np.array(self.w.get())
        t1 = np.arange(0.0, 5.0, 0.1)
        a1 = np.sin(idx*np.pi *t1)
        a2 = np.sin((idx/2)*np.pi*t1)
        a3 = np.sin((idx/4)*np.pi*t1)
        a4 = np.sin((idx/8)*np.pi*t1)
        self.ax1.plot(t1, a1) 
        self.ax2.plot(t1, a2) 
        self.ax3.plot(t1, a3) 
        self.ax4.plot(t1, a4) 
        self.canvas.draw()

if __name__ == '__main__':
    app = MyPlot()
    app.mainloop()

I would use draw_idle() as ImportanceOfBeingErnest mentioned but I am currently seeing a bug that can cause the plot to not be draw with the correct value.

Take this screenshot for example. After sliding the Bar up fast and releasing my mouse button often the value of the plot is not updated properly. This only seams to happen if I let go of the mouse button while sliding the bar up.

This might be an issue only related to Tkinter or some combination of matplotlib and Tkinter. I imagine it is not drawing the last slider value because Tkinter's mainloop is not idle due to my mouse release as this is an event in Tkinter and maybe is interfering.

在此处输入图片说明

An easy fix for your existing code is to actually show the figure ( fig.show ) and make sure it is redrawn each time the slider is moved ( fig.canvas.draw_idle() )

import matplotlib.pyplot as plt
import numpy as np
import tkinter as tk


fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, sharey = False) # create figure
plt.tight_layout()

master = tk.Tk()

def update(val):

    ax1.cla() # clears the entire current figure but leaves the window
    ax2.cla()
    ax3.cla()
    ax4.cla()

    global idx
    idx = np.array(w.get())

    t1 = np.arange(0.0, 5.0, 0.1)
    a1 = np.sin(idx*np.pi *t1)
    a2 = np.sin((idx/2)*np.pi*t1)
    a3 = np.sin((idx/4)*np.pi*t1)
    a4 = np.sin((idx/8)*np.pi*t1)

    """Plotting of data"""  
    ax1.plot(t1, a1) 
    ax2.plot(t1, a2) 
    ax3.plot(t1, a3) 
    ax4.plot(t1, a4)
    fig.canvas.draw_idle()

fig.show()
w = tk.Scale(master, from_=0, to=10, command = update)
w.pack()
tk.mainloop()

In case the aim is not necessarily to provide a tk slider, an appropriate solution may be the following, using in inbuilt Slider . This has the advantage of being completely platform and backend independent.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider

fig, axes = plt.subplots(4, sharey = False) # create figure
plots = [ax.plot([])[0] for ax in axes]

fig.tight_layout()
fig.subplots_adjust(bottom=0.12)

t1 = np.arange(0.0, 5.0, 0.1)

def update(idx):

    a1 = np.sin(idx*np.pi *t1)
    a2 = np.sin((idx/2)*np.pi*t1)
    a3 = np.sin((idx/4)*np.pi*t1)
    a4 = np.sin((idx/8)*np.pi*t1)

    for plot, a in zip(plots, [a1,a2,a3,a4]):
        plot.set_data(t1, a)
        plot.axes.relim()
        plot.axes.autoscale()

    fig.canvas.draw_idle()

update(5)
slider = Slider(fig.add_axes([.1,.04,.6,.03]), "Label", 0,10,5)
slider.on_changed(update)
plt.show()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM