Home > Back-end >  How to trigger again a callback function before the first call for that function has ended, if that
How to trigger again a callback function before the first call for that function has ended, if that

Time:10-20

Good morning,

What I am aiming for:

  • I want to create a button that when clicked triggers a call back function (even if that function is still running). The call back function is a matplotlib animation

Module:

  • For the GUI, I am using tkinter
  • For the callback function, I am using a matplotlib animation

Attempt n1:

  • I created a button with tkinter that triggers the animation

Problem for attempt n1:

  • When I trigger the button, the function runs, but then I cannot push the button again before the function has finished its run. I want to be able to push the button while the function is still running and have the function stop and run again from start.

Code for attempt n1 is basically this one

Attempt n2:

  • I then tried to use multi-threading, so that the animation and the GUI run on different threads, but when I do this, I get "Starting a Matplotlib GUI outside of the main thread will likely fail.". This is the code for this second attempt:

Here it is:

import tkinter as tk # Import tkinter
from tkinter import ttk # Import ttk
import threading
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from random import randint



def button():
    thread = threading.Thread(target=func)
    thread.start()

def func():
    fig = plt.figure()
    ax = fig.add_subplot(xlim=(0, 1),  ylim=(0, 1))
    point, = ax.plot([], [],  '-')
    CumulativeX,   CumulativeY=[], []
    def animate(i):
        CumulativeX.append(randint(0, 10)/10),  CumulativeY.append(randint(0, 10)/10)  
        point.set_data(CumulativeX,CumulativeY)
        return point
    ani = animation.FuncAnimation(fig, animate, interval=1000)
    plt.show()


win = tk.Tk() # Create instance of the Tk class
aButton = ttk.Button(win, text="Click Me!", command=button)
aButton.grid(column=0, row=0) # Adding a Button
win.mainloop() #  Start GUI

CodePudding user response:

Combining the example from jfaccioni's comment and your question code, we get something like the below (which works for me on Python 3.10.7)...

import tkinter
from random import randint

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

root = tkinter.Tk()
root.wm_title("Embedding in Tk")

fig = Figure(figsize=(5, 4), dpi=100)
t = np.arange(0, 3, 0.01)
ax = fig.add_subplot()
(line,) = ax.plot(t, 2 * np.sin(2 * np.pi * t))
ax.set_xlabel("time [s]")
ax.set_ylabel("f(t)")

CumulativeX, CumulativeY = [], []

canvas = FigureCanvasTkAgg(fig, master=root)  # A tk.DrawingArea.
canvas.draw()

restart = [True]


def button():
    restart[0] = True


def update(frame, CumulativeX, CumulativeY, restart, *fargs):

    if restart[0] is True:
        CumulativeY.clear()
        CumulativeX.clear()
        restart[0] = False
    else:
        CumulativeX.append(randint(0, 10) / 10)
        CumulativeY.append(randint(0, 10) / 10)

    line.set_data(CumulativeX, CumulativeY)


_ = animation.FuncAnimation(
    fig, update, interval=1000, fargs=(CumulativeX, CumulativeY, restart)
)

# pack_toolbar=False will make it easier to use a layout manager later on.
toolbar = NavigationToolbar2Tk(canvas, root, pack_toolbar=False)
toolbar.update()

button_quit = tkinter.Button(master=root, text="Quit", command=root.destroy)
button_restart = tkinter.Button(master=root, text="Click Me!", command=button)

# Packing order is important. Widgets are processed sequentially and if there
# is no space left, because the window is too small, they are not displayed.
# The canvas is rather flexible in its size, so we pack it last which makes
# sure the UI controls are displayed as long as possible.
button_quit.pack(side=tkinter.BOTTOM)
button_restart.pack(side=tkinter.BOTTOM)
toolbar.pack(side=tkinter.BOTTOM, fill=tkinter.X)
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True)

tkinter.mainloop()

Though it's quite verbose, it's not very complicated:

  • we are updating the image once per second
  • the button() function sets restart[0] equal to True
  • the update() function with either add another random line segment (if restart[0] is False) or clear the figure and start again (if restart[0] is True)
  • Related