在 PySimpleGUI 窗口中自动更新 Matplotlib 图

Posted

技术标签:

【中文标题】在 PySimpleGUI 窗口中自动更新 Matplotlib 图【英文标题】:Automated Updating Matplotlib Plot in PySimpleGUI Window 【发布时间】:2020-11-19 04:25:52 【问题描述】:

我正在创建一个 GUI,以允许用户查看光谱仪的“实时视图”,其中数据从光谱仪获取并绘制在 Matplotlib 中以显示在 GUI 窗口中。 GUI 还有一些其他按钮,允许用户使用其他功能(不相关但只是背景)。

我已经让实时视图在 matplotlib 中使用 while 循环并清除数据以重新绘制:

while True:
    data = ccs.take_data(num_avg=3) # spectrometer function
    norm = (data[0]-dark[0])/(light[0]-dark[0]) # some calcs.
    plt.plot(data[1],norm)
    plt.axis([400,740,0,1.1])  
    plt.grid(color='w', linestyle='--')       
    plt.xlabel('Wavelength [nm]')
    plt.ylabel('Normalized Intesity')          
    plt.pause(0.1)
    plt.cla()

下一步是在 PySimpleGUI 中显示此图。比expexted更难...如果用户按下“更新”按钮,我可以使用 PySimpleGUI 中的一些演示代码来显示和更新单个图形:

from instrumental.drivers.spectrometers import thorlabs_ccs
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import PySimpleGUI as sg
import matplotlib, time, threading
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt


def fig_maker(ccs, dark, sub):
    plt.clf()
    plt.close()
    data = ccs.take_data(num_avg=3)
    norm = (data[0]-dark[0])/(sub[0]-dark[0])
    plt.plot(data[1],norm,c='r')
    plt.axis([400,750,0,1.1])  
    plt.grid(color='w', linestyle='--')       
    plt.xlabel('Wavelength [nm]')
    plt.ylabel('Normalized Intesity')  

return plt.gcf() 


def draw_figure(canvas, figure, loc=(0, 0)):
    figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
    figure_canvas_agg.draw()
    figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
    return figure_canvas_agg


def delete_fig_agg(fig_agg):
    fig_agg.get_tk_widget().forget()
    plt.close('all')


if __name__ == '__main__':
    
    ... some code ...

    # define the window layout
    layout = [[sg.Button('update')],
              [sg.Text('Plot test', font='Any 18')],             
              [sg.Canvas(size=(500,500), key='canvas')] ]

    # create the form and show it without the plot
    window = sg.Window('Demo Application - Embedding Matplotlib In PySimpleGUI',
                       layout, finalize=True)

    fig_agg = None
    while True:
        event, values = window.read()
        if event is None:  # if user closes window
            break         
        if event == "update":
            if fig_agg is not None:
                delete_fig_agg(fig_agg)
            fig = fig_maker(ccs,dark,sub)
            fig_agg = draw_figure(window['canvas'].TKCanvas, fig) 
    window.close()            

现在是有趣的部分(我似乎无法让它工作)。我希望情节总是像我使用 matplotlib 那样更新,这样用户就不必按“更新”。使用PySimpleGUI long_task threaded example 是我的程序开始失败的地方。除了在 Python 关闭脚本之前打印到 Debug I/O 声明 *** Faking Timeout *** 之外,我实际上没有抛出任何错误。

我什至只是尝试做一个 10 次迭代的 for 循环,而不是连续的 while 循环:

from instrumental.drivers.spectrometers import thorlabs_ccs
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import PySimpleGUI as sg
import matplotlib, time, threading
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt


def long_function_thread(window, ccs, dark, sub):
    for i in range(10):
        fig = fig_maker(ccs, dark, sub)
        fig_agg = draw_figure(window['canvas'].TKCanvas, fig) 
        window.write_event_value('-THREAD PROGRESS-', i)
        time.sleep(1)
        delete_fig_agg(fig_agg)
        time.sleep(0.1)

    window.write_event_value('-THREAD DONE-', '')


def long_function(window, ccs, dark, sub):
    print('In long_function')
    threading.Thread(target=long_function_thread, args=(window, ccs, dark, sub), daemon=True).start()


def fig_maker(ccs, dark, sub):
    plt.clf()
    plt.close()
    data = ccs.take_data(num_avg=3)
    norm = (data[0]-dark[0])/(sub[0]-dark[0])
    plt.plot(data[1],norm,c='r')
    plt.axis([400,750,0,1.1])  
    plt.grid(color='w', linestyle='--')       
    plt.xlabel('Wavelength [nm]')
    plt.ylabel('Normalized Intesity')  
    
    return plt.gcf() 


def draw_figure(canvas, figure, loc=(0, 0)):
    figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
    figure_canvas_agg.draw()
    figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
    return figure_canvas_agg


def delete_fig_agg(fig_agg):
    fig_agg.get_tk_widget().forget()
    plt.close('all')


if __name__ == '__main__':
    
     ... some code ...

    # define the window layout
    layout = [[sg.Button('Go')],
              [sg.Text('Plot test', font='Any 18')],            
              [sg.Canvas(size=(500,500), key='canvas')] ]

    # create the form and show it without the plot
    window = sg.Window('Demo Application - Embedding Matplotlib In PySimpleGUI',
                       layout, finalize=True)

    fig_agg = None
    while True:
        event, values = window.read()
        if event is None or event == 'Exit':
            break
        if event == 'Go':
            print('Calling plotter')
            long_function(window, ccs, dark, sub)
            print('Long function has returned from starting')            
        elif event == '-THREAD DONE-':
            print('Your long operation completed')

window.close()

关于长描述和代码转储的应用,但我认为这是最简单的解释方式。任何有关此问题的帮助或链接将不胜感激。

如果有人想尝试运行我的脚本,这应该只是生成一个随机图

def random_fig_maker():
   plt.scatter(np.random.rand(1,10),np.random.rand(1,10))
   return plt.gcf()

【问题讨论】:

【参考方案1】:

您需要使用两个额外的 PySimpleGUI 功能:window.Refresh()window.write_event_value()。当您删除 figg_agg 并且新绘图准备就绪时,请致电 window.Refresh()。这将重绘窗口,但也引入了一个问题:主事件 (while) 循环将永远运行。为了解决这个问题,您还需要将window.write_event_value('-THREAD-', 'some message.') 添加到从事件循环中调用的函数之一。这将作为事件循环继续运行的外部触发器,但这也会使窗口保持响应,因此您可以更改一些其他窗口元素(这里我使用了单选开关)来停止循环。

对于奖励积分,您还可以将“触发功能”作为单独的线程运行。然后,该函数中的time.sleep() 不会影响 GUI 响应能力。因此,我会使用一些只返回一些列表或元组的数据收集函数作为重新启动循环的触发器。在这种情况下,matplotlib 对从外部线程调用不满意,所以我只是在事件循环中添加了延迟以保持绘图可见。

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import PySimpleGUI as sg
import matplotlib, time, threading
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import numpy as np


def fig_maker(window): # this should be called as a thread, then time.sleep() here would not freeze the GUI
    plt.scatter(np.random.rand(1,10),np.random.rand(1,10))
    window.write_event_value('-THREAD-', 'done.')
    time.sleep(1)
    return plt.gcf()


def draw_figure(canvas, figure, loc=(0, 0)):
    figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
    figure_canvas_agg.draw()
    figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
    return figure_canvas_agg


def delete_fig_agg(fig_agg):
    fig_agg.get_tk_widget().forget()
    plt.close('all')


if __name__ == '__main__':
    # define the window layout
    layout = [[sg.Button('update'), sg.Button('Stop', key="-STOP-"), sg.Button('Exit', key="-EXIT-")],
              [sg.Radio('Keep looping', "RADIO1", default=True, size=(12,3),key="-LOOP-"),sg.Radio('Stop looping', "RADIO1", size=(12,3), key='-NOLOOP-')],
              [sg.Text('Plot test', font='Any 18')],             
              [sg.Canvas(size=(500,500), key='canvas')]]

    # create the form and show it without the plot
    window = sg.Window('Demo Application - Embedding Matplotlib In PySimpleGUI',
                       layout, finalize=True)

    fig_agg = None
    while True:
        event, values = window.read()
        if event is None:  # if user closes window
            break
        
        if event == "update":
            if fig_agg is not None:
                    delete_fig_agg(fig_agg)
            fig = fig_maker(window)
            fig_agg = draw_figure(window['canvas'].TKCanvas, fig)

        if event == "-THREAD-":
            print('Acquisition: ', values[event])
            time.sleep(1)
            if values['-LOOP-'] == True:
                if fig_agg is not None:
                    delete_fig_agg(fig_agg)
                fig = fig_maker(window)
                fig_agg = draw_figure(window['canvas'].TKCanvas, fig)
                window.Refresh()
        
        if event == "-STOP-":
            window['-NOLOOP-'].update(True)
        
        if event == "-EXIT-":
            break
            
    
    window.close()            

【讨论】:

【参考方案2】:

它没有完全连接,但我遇到了类似的问题。这有帮助吗....

import PySimpleGUI as sg
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np

class updateable_matplotlib_plot():
    def __init__(self, canvas) -> None:
        self.fig_agg = None
        self.figure = None
        self.canvas = canvas

    def plot(self, data):
        self.data = data
        self.figure_controller()
        self.figure_drawer()

    #put all of your normal matplotlib stuff in here
    def figure_controller(self):
        #first run....
        if self.figure is None:
            self.figure = plt.figure()
            self.axes = self.figure.add_subplot(111)
            self.line, = self.axes.plot(self.data)
            self.axes.set_title("Example of a Matplotlib plot updating in PySimpleGUI")
        #all other runs
        else:            
            self.line.set_ydata(self.data)#update data            
            self.axes.relim() #scale the y scale
            self.axes.autoscale_view() #scale the y scale

    #finally draw the figure on a canvas
    def figure_drawer(self):
        if self.fig_agg is not None: self.fig_agg.get_tk_widget().forget()
        self.fig_agg = FigureCanvasTkAgg(self.figure, self.canvas.TKCanvas)
        self.fig_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
        self.fig_agg.draw()

def getGUI():
    # All the stuff inside your window.
    layout = [  [sg.Canvas(size=(500,500), key='canvas')],
                [sg.Button('Update', key='update'), sg.Button('Close')] ]

    # Create the Window
    window = sg.Window('Updating a plot example....', layout)
    return window


if __name__ == '__main__':
    window = getGUI()
    spectraPlot = updateable_matplotlib_plot(window['canvas']) #what canvas are you plotting it on
    window.finalize() #show the window
    spectraPlot.plot(np.zeros(1024)) # plot an empty plot    
    while True:
        event, values = window.read()
        if event == "update":
             some_spectrum = np.random.random(1024) # data to be plotted
             spectraPlot.plot(some_spectrum) #plot the data           
        if event == sg.WIN_CLOSED or event == 'Close': break # if user closes window or clicks cancel

    window.close()

【讨论】:

以上是关于在 PySimpleGUI 窗口中自动更新 Matplotlib 图的主要内容,如果未能解决你的问题,请参考以下文章

用 Python 库 PySimpleGUI 制作自动化办公小软件

为啥我在使用 PySimpleGUI 的 while 循环中对列表框的更新最终导致我的程序挂起?

如何在Python中更新GUI窗口?

Pysimplegui 和 Pygame 合并

pysimplegui-第四课:如何设定主题

Angular:mat-sidenav 在调整大小时忽略自动调整大小或模式