2
votes

Tkinter plt.figure () ne trace pas, mais Figure () le fait

J'ai commencé à créer une application Tkinter et j'utilisais initialement les Figure et figure.add_subplot de matplotlib. Avec cela, tout fonctionne parfaitement. Pour plus de personnalisation, je souhaite maintenant passer à pyplot et subplot2grid , mais ce faisant, soudainement, toute ma variable tkinter cesse de fonctionner.

Dans mon MWE, la variable gArrChoice suit quel bouton radio est sélectionné et doit par défaut être la première option. Sur la base de cette option, le graphique doit tracer une ligne planant autour de 0,1. Si la deuxième option est sélectionnée, le graphique devrait changer pour passer autour de 5. Le graphique se met à jour automatiquement toutes les 2,5 secondes. Si vous commentez les 3 lignes sous «Working» et utilisez les 3 lignes «Not Working» à la place, les paramètres par défaut de la variable cessent de fonctionner et la commutation entre les boutons radio n'a plus d'effet. Déclarer une fonction à l'intérieur de la fonction animate ne change pas le problème.

Comment puis-je utiliser plt avec Tkinter et ne pas détruire mes variables?

MWE:

import tkinter as tk
import matplotlib
matplotlib.use("TkAgg") #make sure you use the tkinter backend
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.animation as animation
import numpy as np

gArrChoice = 0

#Working - using Figure and add_subplot
from matplotlib.figure import Figure
f = Figure()
a = f.add_subplot(121)

#Not Working - using plt and subplot2grid
# from matplotlib import pyplot as plt
# f = plt.figure()
# a = plt.subplot2grid((10, 7), (0, 0), rowspan=10, colspan=5)


class BatSimGUI(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        self.frames = {}
        frame = StartPage(container,self)
        self.frames[StartPage] = frame
        frame.grid(row=0, column=0, sticky="nsew")
        frame.tkraise()

class StartPage(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        #Set defaults for global variable
        global gArrChoice
        gArrChoice = tk.IntVar()
        gArrChoice.set(1)

        radioArr1 = tk.Radiobutton(self, variable=gArrChoice, text="Exponential", value=1, command= lambda: print(gArrChoice.get()))
        radioArr1.grid(row=2, column=0)
        radioArr2 = tk.Radiobutton(self, variable=gArrChoice, text="Normal", value=2, command= lambda: print(gArrChoice.get()))
        radioArr2.grid(row=3, column=0)

        #Add Canvas
        canvas = FigureCanvasTkAgg(f, self)
        canvas.draw()
        canvas.get_tk_widget().grid(row=1, column=1, columnspan=7, rowspan = 10)

def animate(i):
    global gArrChoice
    if gArrChoice.get() == 1:
        lam = np.random.exponential(scale=.1, size = 100).reshape(-1,1)
    else:
        lam = np.random.normal(loc=5, scale=1, size = 100).reshape(-1,1)

    a.clear()
    a.step(list(range(100)), list(lam))

#Actually run the interface
app = BatSimGUI()
app.geometry("800x600")
ani = animation.FuncAnimation(f, animate, interval = 2500)
app.mainloop()


4 commentaires

montrer d'abord un code de travail minimal - afin que nous puissions l'exécuter et voir le problème.


Puisque vous ne devriez pas utiliser pyplot ( plt ) dans les interfaces graphiques personnalisées, je soupçonne que c'est le problème. Je ne comprends pas pourquoi vous souhaitez soudainement utiliser pyplot . Ne l'utilisez pas et cela devrait continuer à fonctionner.


@ImportanceOfBeingErnest. C'est juste une réponse terrible à tous égards. Vous n'expliquez pas ou ne créez pas de lien vers une source qui explique pourquoi pyplot doit être évité dans les interfaces graphiques personnalisées, ce que je n'ai jamais entendu ailleurs. Les avantages de personnalisation de pyplot par rapport à Figure me donnent envie de l'utiliser, mais la question de la motivation à des fins de programmation est secondaire. Si le mieux que vous puissiez faire pour résoudre ce problème est de suggérer de ne pas utiliser le programme que j'ai essayé d'utiliser, alors vous n'avez rien à publier du tout, mais de cette façon, c'est perdant. Si vous avez des commentaires constructifs, je serai ravi de tenter votre chance.


Ce n'est pas une réponse, mais un commentaire. Je suis désolé si vous pensez que ce n'est pas constructif. Les raisons de ne pas utiliser pyplot en conjonction avec une interface graphique personnalisée peuvent être résumées comme suit: si vous avez votre interface graphique, ainsi que pyplot gère une figure, ils peuvent interférer et causer toutes sortes de problèmes, dont certains sont assez difficiles à suivre vers le bas. Peut-être est-ce suffisant pour vous signaler qu'aucun des exemples d'intégration dans les interfaces graphiques de la page matplotlib n'utilise pyplot?


3 Réponses :


1
votes

Il semble y avoir un bogue sur la mise à jour de IntVar () lorsque vous utilisez à la place pyplot . Mais vous pouvez contourner le problème si vous forcez un changement de valeur dans vos boutons radio:

import tkinter as tk
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.animation as animation
import numpy as np
from matplotlib import pyplot as plt

class BatSimGUI(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        self.frames = {}
        self.start_page = StartPage(container,self)
        self.frames[StartPage] = self.start_page
        self.start_page.grid(row=0, column=0, sticky="nsew")
        self.start_page.tkraise()

class StartPage(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        self.gArrChoice = tk.IntVar()
        self.gArrChoice.set(1)
        radioArr1 = tk.Radiobutton(self, variable=self.gArrChoice, text="Exponential", value=1)
        radioArr1.grid(row=2, column=0)
        radioArr2 = tk.Radiobutton(self, variable=self.gArrChoice, text="Normal", value=2)
        radioArr2.grid(row=3, column=0)

        self.f = plt.figure()
        self.a = plt.subplot2grid((10, 7), (0, 0), rowspan=10, colspan=5)
        canvas = FigureCanvasTkAgg(self.f, self)
        canvas.draw()
        canvas.get_tk_widget().grid(row=1, column=1, columnspan=7, rowspan = 10)

    def animate(self,i):
        if self.gArrChoice.get() == 1:
            lam = np.random.exponential(scale=.1, size = 100).reshape(-1,1)
        else:
            lam = np.random.normal(loc=5, scale=1, size = 100).reshape(-1,1)
        self.a.clear()
        self.a.step(list(range(100)), list(lam))

app = BatSimGUI()
app.geometry("800x600")
ani = animation.FuncAnimation(app.start_page.f, app.start_page.animate, interval=1000)

app.mainloop()

Ou vous pouvez créer votre IntVar comme attribut de StartPage à la place qui semble fonctionner très bien.

radioArr1 = tk.Radiobutton(self, variable=gArrChoice, text="Exponential", value=1, command= lambda: gArrChoice.set(1))
radioArr2 = tk.Radiobutton(self, variable=gArrChoice, text="Normal", value=2, command= lambda: gArrChoice.set(2))


2 commentaires

Oui, le problème semble provenir de la mise à jour de la variable. Ce problème se produit également avec les autres types de variables tkinter. Pour les boutons radio, vos suggestions fonctionnent, mais avez-vous une idée de comment faire quelque chose de similaire pour les champs d'entrée (en fin de compte, les paramètres saisis dans le champ Entrée doivent être utilisés pour exécuter la fonction d'animation. Étant donné que les champs d'entrée n'ont pas de fonction de commande qui est exécuté à l'entrée je ne vois pas comment prendre soin de cela.


Vous pouvez probablement lier un événement à votre entrée , mais il semble que si vous mettez votre IntVar comme attribut de votre classe à la place, cela fonctionnerait très bien. J'ai mis à jour ma réponse.



2
votes

Je pense qu'une approche OO serait meilleure.

Voir ci-dessous, j'ai utilisé le fil et la file d'attente pour gérer l'animation du tracé, vous pouvez même définir l'intervalle de temps et changer à la volée le type de graphique p>

Bon travail quand même, très intéressant

#!/usr/bin/python3
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox

import threading
import queue
import time

from matplotlib.figure import Figure
import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

try:
    from matplotlib.backends.backend_tkagg import  NavigationToolbar2Tk as nav_tool
except:
    from matplotlib.backends.backend_tkagg import NavigationToolbar2TkAgg as nav_tool

import numpy as np


class MyThread(threading.Thread):

    def __init__(self, queue, which, ops, interval):
        threading.Thread.__init__(self)

        self.queue = queue
        self.check = True
        self.which = which
        self.ops = ops
        self.interval = interval

    def stop(self):
        self.check = False

    def run(self):

        while self.check:

            if self.which.get() ==0:
                lam = np.random.exponential(scale=.1, size = 100).reshape(-1,1)
            else:
                lam = np.random.normal(loc=5, scale=1, size = 100).reshape(-1,1)

            time.sleep(self.interval.get())
            args = (lam, self.ops[self.which.get()])
            self.queue.put(args)
        else:
            args = (None, "I'm stopped")
            self.queue.put(args)

class Main(ttk.Frame):
    def __init__(self, parent):
        super().__init__()

        self.parent = parent

        self.which = tk.IntVar()
        self.interval = tk.DoubleVar()
        self.queue = queue.Queue()
        self.my_thread = None

        self.init_ui()

    def init_ui(self):

        f = ttk.Frame()
        #create graph!
        self.fig = Figure()
        self.fig.suptitle("Hello Matplotlib", fontsize=16)
        self.a = self.fig.add_subplot(111)
        self.canvas = FigureCanvasTkAgg(self.fig, f)
        toolbar = nav_tool(self.canvas, f)
        toolbar.update()
        self.canvas._tkcanvas.pack(fill=tk.BOTH, expand=1)

        w = ttk.Frame()

        ttk.Button(w, text="Animate", command=self.launch_thread).pack()
        ttk.Button(w, text="Stop", command=self.stop_thread).pack()
        ttk.Button(w, text="Close", command=self.on_close).pack()

        self.ops = ('Exponential','Normal',)            

        self.get_radio_buttons(w,'Choice', self.ops, self.which,self.on_choice_plot).pack(side=tk.TOP, fill=tk.Y, expand=0)

        ttk.Label(w, text = "Interval").pack()

        tk.Spinbox(w,
                    bg='white',
                    from_=1.0, to=5.0,increment=0.5,
                    justify=tk.CENTER,
                    width=8,
                    wrap=False,
                    insertwidth=1,
                    textvariable=self.interval).pack(anchor=tk.CENTER) 

        w.pack(side=tk.RIGHT, fill=tk.BOTH, expand=1)
        f.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)

    def launch_thread(self):

        self.on_choice_plot()

    def stop_thread(self):

        if self.my_thread is not None:
            if(threading.active_count()!=0):
                self.my_thread.stop()

    def on_choice_plot(self, evt=None):

        if self.my_thread is not None:

            if (threading.active_count()!=0):

                self.my_thread.stop()

        self.my_thread = MyThread(self.queue,self.which, self.ops, self.interval)
        self.my_thread.start()
        self.periodiccall()

    def periodiccall(self):

        self.checkqueue()
        if self.my_thread.is_alive():
            self.after(1, self.periodiccall)
        else:
            pass

    def checkqueue(self):
        while self.queue.qsize():
            try:

                args = self.queue.get()
                self.a.clear()
                self.a.grid(True)

                if args[0] is not None:
                    self.a.step(list(range(100)), list(args[0]))
                    self.a.set_title(args[1], weight='bold',loc='left')
                else:
                    self.a.set_title(args[1], weight='bold',loc='left')

                self.canvas.draw()

            except queue.Empty:
                pass        


    def get_radio_buttons(self, container, text, ops, v, callback=None):

        w = ttk.LabelFrame(container, text=text,)

        for index, text in enumerate(ops):
            ttk.Radiobutton(w,
                            text=text,
                            variable=v,
                            command=callback,
                            value=index,).pack(anchor=tk.W)     
        return w        


    def on_close(self):

        if self.my_thread is not None:

            if(threading.active_count()!=0):
                self.my_thread.stop()

        self.parent.on_exit()

class App(tk.Tk):
    """Start here"""

    def __init__(self):
        super().__init__()

        self.protocol("WM_DELETE_WINDOW", self.on_exit)

        self.set_title()
        self.set_style()

        Main(self)

    def set_style(self):
        self.style = ttk.Style()
        #('winnative', 'clam', 'alt', 'default', 'classic', 'vista', 'xpnative')
        self.style.theme_use("clam")

    def set_title(self):
        s = "{0}".format('Simple App')
        self.title(s)

    def on_exit(self):
        """Close all"""
        if messagebox.askokcancel("Simple App", "Do you want to quit?", parent=self):
            self.destroy()               

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

 entrez la description de l'image ici


1 commentaires

Je vais certainement réécrire le code à long terme. C'était la première fois que je travaillais sur quelque chose d'un peu plus grand avec Tkinter et je voulais commencer par créer le cadre de base. Mais la POO convenue est la voie à suivre :)



0
votes

Il semble que le problème soit de remplacer

from matplotlib.figure import Figure
f = Figure()
gs = f.add_gridspec(10,7)
a = f.add_subplot(gs[:, :5])

de manière indépendante de pyplot. Une option est l'utilisation de gridspec:

# Not Working - using plt and subplot2grid
from matplotlib import pyplot as plt
f = plt.figure()
a = plt.subplot2grid((10, 7), (0, 0), rowspan=10, colspan=5)


0 commentaires