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

问题描述 投票:0回答:2

TL;博士:

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

上下文

我正在编写一个 GUI Python 应用程序(PyQt5、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. 点击测试按钮:动画将开始,帧数将打印到控制台;
  2. 点击绘图停止动画:帧数将停止打印;
  3. 调整窗口大小:帧数应从上一个动画的最后一帧开始继续打印。

我的假设

我怀疑

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

我试过的

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

帮助

目前我不知道还能尝试什么,任何建议将不胜感激!我对 Qt、Python 和 Stackoverlow 还很陌生,所以如果我需要进一步澄清,请告诉我。如果需要,我可以提供应用程序的完整代码,也可以在

self._ani.event_source.stop()
.

之后提供动画对象的 objgraph 检查
python matplotlib pyqt garbage-collection matplotlib-animation
2个回答
1
投票

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

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

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


0
投票

我知道已经有一段时间了,但我遇到了完全相同的问题,这让我发疯。为了提供一些上下文,我有一个类读取一堆 csv 文件,每个文件都有一些有趣的信息来填充一系列扫描(如视频中的帧)。然后我有一个玩家在 this 答案中非常受启发。重点是player是继承自

FuncAnimation
的类(你用的一样)

现在,无论何时调整窗口大小时,matplotlib 后端都必须执行一组操作以确保一切都保持原样。更准确地说,

FuncAnimation
有(好吧,它的父级)一个特定的方法 (
_on_resize(self, event)
),每次您调整窗口大小时都会调用该方法。它基本上停止动画,清除缓存,用事件处理程序做一些事情并再次启动动画(从头开始)。该函数的实现如下(更多信息见源代码):

    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)

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

© www.soinside.com 2019 - 2024. All rights reserved.