python Matplotlib Funcanimation在Qt5Agg后端调整窗口大小时恢复循环

mefy6pfw  于 2023-05-21  发布在  Python
关注(0)|答案(2)|浏览(218)

TL;DR:

matplotlib.backends.backend_qt5.TimerQT似乎包含了对以前运行过的动画对象的引用,即使在使用animation.event_source.stop()之后也是如此。在调整应用程序窗口的大小时,动画循环从其离开的地方继续。del self.animation没有帮助。我如何才能避免这种情况?

上下文

我正在编写一个GUI Python应用程序(PyQt 5,Python 3.8,Matplotlib 3.3.4),它允许用户绘制和分析一些数据。分析的一部分要求用户选择标绘数据的范围。我使用matplotlib动画来实时显示选定的数据点和其他相关信息:

一切如预期:一旦用户结束了与绘图的交互,动画就停止。不幸的是,如果用户调整窗口的大小,动画循环将从原来的位置恢复(通过打印i第帧编号进行检查)。这里有一个简短的gif来说明有问题的行为。

如果在调整大小之前执行了多个动画,则在调整窗口大小时,所有这些动画将同时开始运行。

样本编码

下面是一个代码片段,可以运行它来重现所描述的行为:

import sys
import matplotlib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
import matplotlib.animation as animation

from PyQt5 import QtCore, QtWidgets

matplotlib.use("Qt5Agg")

class MplCanvas(FigureCanvasQTAgg):
    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)

        super().__init__(fig)

        self._ani = None
        self.plots = None
        self.loop = None

    def animation(self):
        self.plot = [self.axes.plot([], [])[0]]

        self._ani = animation.FuncAnimation(
            self.figure,
            self._animate_test,
            init_func=self._init_test,
            interval=40,
            blit=True,
        )
        self.figure.canvas.mpl_connect("button_press_event", self._on_mouse_click)

        self.loop = QtCore.QEventLoop()
        self.loop.exec()

        self._ani.event_source.stop()

    def _init_test(self):
        return self.plot

    def _animate_test(self, i):
        print(f"Running animation with frame {i}")

        return self.plot

    def _on_mouse_click(self, event):
        self.loop.quit()

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setObjectName("MainWindow")
        self.resize(1000, 600)

        self.centralwidget = QtWidgets.QWidget()
        self.horizontal_layout = QtWidgets.QHBoxLayout()

        self.centralwidget.setLayout(self.horizontal_layout)

        self.mpl_canvas = MplCanvas(self.centralwidget)
        self.button = QtWidgets.QToolButton(self.centralwidget)
        self.button.setText("Test button")

        self.horizontal_layout.addWidget(self.mpl_canvas)
        self.horizontal_layout.addWidget(self.button)

        self.button.clicked.connect(self._button_clicked)

        self.setCentralWidget(self.centralwidget)

    def _button_clicked(self):
        self.mpl_canvas.animation()

app = QtWidgets.QApplication(sys.argv)

window = MainWindow()
window.show()
app.exec()

sys.exit()

运行此代码片段后,可以按如下方式重现该行为:
1.点击测试按钮:动画将开始,帧编号将打印到控制台;
1.单击绘图停止动画:帧号将停止打印;
1.调整窗口大小:帧编号应该从前一动画的最后一帧开始恢复打印。

假设

我怀疑matplotlib.backends.backend_qt5.TimerQT持有一个引用(weakref?)到先前已启动的动画。当发生窗口调整事件时,可能会请求重画,并重新启动所有以前的动画。根据我对previous question的理解,self._ani.event_source.stop()应该从计时器回调中注销动画对象,但由于某种原因,它在这里不起作用。

我所尝试的

1.我试图删除QtCore.QEventLoop(),但行为仍然存在;
1.我在self._ani.event_source.stop()之后尝试了del self._ani,但行为仍然存在。即使self._ani被删除(用hasattr检查),在窗口调整大小时,动画循环在它被留下的帧处重新开始;
1.我尝试保存mpl_connectcid,然后在QEventLoop结束后保存mpl_disconnect,但行为仍然存在;
1.搜索Stackoverflow以查找类似问题,但除了上面引用的案例外,找不到任何类似案例。

帮助

我不知道还有什么可以尝试的时刻,任何建议将不胜感激!我对Qt、Python和Stackoverlow都很陌生,所以如果我需要进一步澄清的话,请告诉我。我可以提供应用程序的完整代码,如果需要,也objgraph检查后的动画对象self._ani.event_source.stop()

yzckvree

yzckvree1#

遇到同样的问题,我的解决方法是创建一个全局标志pausePlot,每当用户在我的GUI中按下暂停按钮时,我都会设置它,然后在我的updatePlot函数中读取:

def _animate_test(self, i):
        global pausePlot
        if not pausePlot:
            print(f"Running animation with frame {i}")
        return self.plot

这样,即使动画一直在运行,当暂停标志被Assert时,我的更新函数也不会更新任何情节线,并且我可以在没有情节动画自动恢复的情况下调整窗口大小。

5lhxktic

5lhxktic2#

我知道这是一段时间,但我遇到了完全相同的问题,它是把我疯了。为了给予一些上下文,我有一个类,它读取一堆csv文件,每个文件都有一些有趣的信息来填充一系列扫描(就像视频中的帧)。然后我有一个球员非常启发this的答案。关键点是播放器是一个从FuncAnimation(与您使用的相同)继承的类。
现在,每当你调整窗口大小时,matplotlib后端必须执行一组操作来保证一切都保持原样。更准确地说,FuncAnimation有一个特定的方法(_on_resize(self, event))(好吧,是它的父方法),每次调整窗口大小时都会调用该方法。它基本上停止动画,清除该高速缓存,用事件处理程序做一些事情,然后重新开始动画(从头开始)。该函数的实现如下所示(请参阅莫尔info的源代码):

def _on_resize(self, event):
        # On resize, we need to disable the resize event handling so we don't
        # get too many events. Also stop the animation events, so that
        # we're paused. Reset the cache and re-init. Set up an event handler
        # to catch once the draw has actually taken place.
        self._fig.canvas.mpl_disconnect(self._resize_id)
        self.event_source.stop()
        self._blit_cache.clear()
        self._init_draw()
        self._resize_id = self._fig.canvas.mpl_connect('draw_event',
                                                       self._end_redraw)
    
    def _end_redraw(self, event):
        # Now that the redraw has happened, do the post draw flushing and
        # blit handling. Then re-enable all of the original events.
        self._post_draw(None, False)
        self.event_source.start()
        self._fig.canvas.mpl_disconnect(self._resize_id)
        self._resize_id = self._fig.canvas.mpl_connect('resize_event',
                                                       self._on_resize)

因此,我解决在调整大小后保持播放器状态的问题的方法是在我自己的播放器类中重新定义这些方法。这对你的情况会有所不同,但应该不难。你必须以某种方式能够存储动画的状态。例如,在我的实现中,我在类中有一个属性,用于存储我所在的帧(self.i),还有一个方法用于对该帧进行新的绘图(self.set_pos(i),它还在内部设置self.i = i)。考虑到这一点,这里是我如何改变方法来恢复每次调整大小后的状态:

def _on_resize(self, event):
    i = self.i
    self._fig.canvas.mpl_disconnect(self._resize_id)
    self.event_source.stop()
    self._blit_cache.clear()
    self._init_draw()
    if self.paused:
        self.pause()
    self.set_pos(i)
    self._resize_id = self._fig.canvas.mpl_connect('draw_event',
                                                   self._end_redraw)

def _end_redraw(self, event):
    i = self.i
    self._post_draw(None, False)
    self.event_source.start()
    if self.paused:
        self.pause()
    self.set_pos(i)
    self._fig.canvas.mpl_disconnect(self._resize_id)
    self._resize_id = self._fig.canvas.mpl_connect('resize_event',
                                                   self._on_resize)

我希望这可以帮助其他人在未来找到同样的问题!

相关问题