Я начал создавать приложение Tkinter и изначально использовал Figure
и figure.add_subplot
matplotlib. При этом все работает идеально. Для большей настройки я теперь хочу перейти к pyplot
и subplot2grid
, но при этом внезапно все мои переменные tkinter перестают работать.
В моем MWE переменная gArrChoice
отслеживает, какой переключатель выбран, и по умолчанию должен использовать первый вариант. На основе этой опции график должен отображать линию, колеблющуюся около 0,1. Если выбран второй вариант, график должен измениться на 5. График автоматически обновляется каждые 2,5 секунды. Если вы закомментируете 3 строки ниже «Работает» и вместо этого используете 3 строки «Не работает», настройки переменной по умолчанию перестанут работать, и переключение между переключателями больше не будет работать. Объявление внутри функции анимации не решает проблему.
Как я могу использовать plt с Tkinter и не уничтожить свои переменные?
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()
Поскольку вы не должны использовать pyplot (plt
) в пользовательских графических интерфейсах, я подозреваю, что это проблема. Я не понимаю, почему вы вдруг захотели использовать pyplot
. Просто не используйте его, и он должен продолжать работать.
@ImportanceOfBeingErnest. Это просто ужасный ответ по любым меркам. Вы не объясняете и не ссылаетесь на какой-либо источник, который объясняет, почему pyplot следует избегать в пользовательских графических интерфейсах, чего я никогда не слышал в другом месте. Преимущества настройки pyplot по сравнению с Figure заставляют меня хотеть его использовать, но вопрос мотивации для целей программирования является второстепенным. Если лучшее, что вы можете сделать для решения этой проблемы, это предложить не использовать программу, которую я пытался использовать, тогда вам вообще ничего не нужно публиковать, но в этом случае вы проиграете. Если у вас есть какие-либо конструктивные отзывы, я буду рад дать им шанс.
Это не ответ, а комментарий. Мне жаль, если вы считаете, что это неконструктивно. Причины не использовать pyplot в сочетании с пользовательским графическим интерфейсом можно резюмировать следующим образом: если у вас есть свой графический интерфейс, а также pyplot управляет фигурой, они могут мешать и вызывать всевозможные проблемы, некоторые из которых довольно сложно отследить. вниз. Может быть, достаточно указать вам на тот факт, что ни один из примеров встраивания в графические интерфейсы на странице matplotlib не использует pyplot?
Кажется, есть ошибка при обновлении IntVar()
, когда вместо этого вы используете pyplot
. Но вы можете обойти это, если принудительно измените значение в своих переключателях:
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))
Или вы можете сделать свой IntVar
атрибутом StartPage
вместо этого, что, кажется, работает очень хорошо.
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()
Да, проблема действительно в обновлении переменной. Эта проблема также возникает с другими типами переменных tkinter. Для радиокнопок ваши предложения работают, но знаете ли вы, как сделать что-то подобное для полей ввода (в конечном счете, любые параметры, введенные в поле ввода, должны использоваться для запуска функции анимации. Учитывая, что поля ввода не имеют командной функции который выполняется при входе, я не вижу, как об этом позаботиться.
Вероятно, вы можете привязать событие к своему entry
, однако кажется, что если вместо этого вы поместите свою IntVar в качестве атрибута своего класса, это будет работать нормально. Я обновил свой ответ.
Я думаю, что подход OO был бы лучше.
См. ниже, я использую поток и очередь для управления анимацией сюжета, вы даже можете установить временной интервал и изменить тип графика на лету.
В любом случае хорошая работа, очень интересно
#!/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()
Я обязательно перепишу код в долгосрочной перспективе. Это был мой первый раз, когда я работал над чем-то более крупным с помощью Tkinter, и я хотел сначала начать создавать базовый фреймворк. Но согласен, ООП - это путь :)
похоже проблема в замене
# 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)
в независимой от сюжета манере. Одним из вариантов является использование gridspec:
from matplotlib.figure import Figure
f = Figure()
gs = f.add_gridspec(10,7)
a = f.add_subplot(gs[:, :5])
сначала покажите минимальный рабочий код - чтобы мы могли запустить его и увидеть проблему.