2

I started building a Tkinter application and was initially using matplotlib's Figure and figure.add_subplot. With that everything works perfectly. For more customization, I now want to move to pyplot and subplot2grid, but in doing so, suddenly all of my tkinter variable stop working.

In my MWE, the variable gArrChoice tracks which radio button is selected and should default to the first option. Based on this option, the graph should plot a line hovering around 0.1. If the second option gets selected, the graph should change to hover around 5. The graph auto-updates ever 2.5 seconds. If you comment out the 3 lines below "Working" and use the 3 "Not Working" lines instead, the default settings of the variable stops working and switching between radio buttons has no effect anymore. Declaring a inside the animate function does not change the problem.

How can I use plt with Tkinter and not destroy my 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
  • first show minimal, working code - so we could run it and see problem. Commented Apr 5, 2019 at 21:00
  • Since you should not use pyplot (plt) in custom GUIs I suspect this to be the problem. I don't understand why you suddenly want to use pyplot though. Just don't use it and it should continue to work. Commented Apr 5, 2019 at 21:47
  • @ImportanceOfBeingErnest. This is just an awful answer by any measure. You don't explain or link to some source that explains, why pyplot is to be avoided in custom GUIs, which I have never heard elsewhere. The customization advantages of pyplot over Figure make me want to use it, but the question of motivation for programming purposes is secondary. If the best you can do for solving this problem is suggesting to not use the program I tried to use, then you don't have to post anything at all, but this way its a lose-lose. If you have any constructive feedback, I am happy to give it a shot. Commented Apr 6, 2019 at 11:24
  • It's not an answer, but a comment. I'm sorry if you feel that this is unconstructive. The reasons not to use pyplot in conjunction with a custom GUI may be summarized like this: If you have your GUI, as well as pyplot manage a figure, they might interfere and cause all sorts of problems, some of which are pretty hard to track down. Maybe it's enough to point you to the fact that none of the examples of embedding in GUIs on the matplotlib page use pyplot? Commented Apr 7, 2019 at 12:29

3 Answers 3

2

I'think that an OO approach it'would be better.

See below, I've use thread and queue to manage the plot animation, you can even set time interval and change on fly the graph type

Good job anyway, very interesting

#!/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()

enter image description here

Sign up to request clarification or add additional context in comments.

1 Comment

I will definitely re-write the code in the long-term. This was my first time working on something slightly larger with Tkinter and I wanted to start building the basic framework first. But agreed OOP is the way to go:)
1

There seems to be a bug on updating the IntVar() when you use pyplot instead. But you can workaround it if you force a change in value in your radio buttons:

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

Or you can make your IntVar as an attribute of StartPage instead which seems to work just fine.

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()

2 Comments

Yes, the problem indead seems to be in the updating of the variable. This problem also occurs with the other tkinter variable types. For the radiobuttons your suggestions works, but do you have any idea how to do something similar for entry fields (ultimately, whatever parameters are entered in the Entry field should be used to run the animate function. Given that the entry fields have no command function that is executed upon entry I dont see how to take care of that.
You can probably bind an event to your entry, however it seems that if you put your IntVar as an attribute of your class instead it would work just fine. I have updated my answer.
0

It seems the problem is to replace

# 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)

in a pyplot- independent fashion. One option is the use of gridspec:

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.