Tkinter plt.figure() не рисует, а Figure() делает

Я начал создавать приложение 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()

сначала покажите минимальный рабочий код - чтобы мы могли запустить его и увидеть проблему.

furas 05.04.2019 23:00

Поскольку вы не должны использовать pyplot (plt) в пользовательских графических интерфейсах, я подозреваю, что это проблема. Я не понимаю, почему вы вдруг захотели использовать pyplot. Просто не используйте его, и он должен продолжать работать.

ImportanceOfBeingErnest 05.04.2019 23:47

@ImportanceOfBeingErnest. Это просто ужасный ответ по любым меркам. Вы не объясняете и не ссылаетесь на какой-либо источник, который объясняет, почему pyplot следует избегать в пользовательских графических интерфейсах, чего я никогда не слышал в другом месте. Преимущества настройки pyplot по сравнению с Figure заставляют меня хотеть его использовать, но вопрос мотивации для целей программирования является второстепенным. Если лучшее, что вы можете сделать для решения этой проблемы, это предложить не использовать программу, которую я пытался использовать, тогда вам вообще ничего не нужно публиковать, но в этом случае вы проиграете. Если у вас есть какие-либо конструктивные отзывы, я буду рад дать им шанс.

seulberg1 06.04.2019 13:24

Это не ответ, а комментарий. Мне жаль, если вы считаете, что это неконструктивно. Причины не использовать pyplot в сочетании с пользовательским графическим интерфейсом можно резюмировать следующим образом: если у вас есть свой графический интерфейс, а также pyplot управляет фигурой, они могут мешать и вызывать всевозможные проблемы, некоторые из которых довольно сложно отследить. вниз. Может быть, достаточно указать вам на тот факт, что ни один из примеров встраивания в графические интерфейсы на странице matplotlib не использует pyplot?

ImportanceOfBeingErnest 07.04.2019 14:29
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
4
967
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Кажется, есть ошибка при обновлении 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. Для радиокнопок ваши предложения работают, но знаете ли вы, как сделать что-то подобное для полей ввода (в конечном счете, любые параметры, введенные в поле ввода, должны использоваться для запуска функции анимации. Учитывая, что поля ввода не имеют командной функции который выполняется при входе, я не вижу, как об этом позаботиться.

seulberg1 06.04.2019 16:56

Вероятно, вы можете привязать событие к своему entry, однако кажется, что если вместо этого вы поместите свою IntVar в качестве атрибута своего класса, это будет работать нормально. Я обновил свой ответ.

Henry Yik 06.04.2019 17:19
Ответ принят как подходящий

Я думаю, что подход 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, и я хотел сначала начать создавать базовый фреймворк. Но согласен, ООП - это путь :)

seulberg1 06.04.2019 20:21

похоже проблема в замене

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

Другие вопросы по теме