There are a few parts to this problem, so let me summarize. I'm trying to be able to create an animation in a PySide window, and then later destroy and reanimate it based on new information. I've created a sample script to demonstrate this, but it's a bit long, so here's an outline:
- Create a QtGui main window using PySide
- Create a widget in the window for drawing the animation
- Animate the widget using FuncAnimation
- On a button press:
- Delete the widget and animation
- Recreate the widget using different parameters for plotting
- Reanimate
Everything works until the button press, and I get the following stack trace:
Traceback (most recent call last): File "/usr/lib/pymodules/python2.7/matplotlib/backends/backend_qt4.py", line 366, in idle_draw self.draw() File "/usr/lib/pymodules/python2.7/matplotlib/backends/backend_qt4agg.py", line 149, in draw self.update() RuntimeError: Internal C++ object (MplWidget) already deleted.
How can I get rid of the current animation so that I can redraw a separate one? Here's the code.
from matplotlib import pyplot as p
from mpl_toolkits.mplot3d import Axes3D # @UnusedImport
from PySide import QtGui, QtCore
import matplotlib
import matplotlib.animation as animation
import sys
# specify the use of PySide
matplotlib.rcParams['backend.qt4'] = "PySide"
# import the figure canvas for interfacing with the backend
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg \
as FigureCanvas
from matplotlib.figure import Figure
import numpy as np
from math import pi, cos, sin
class Ui_MainWindow(object):
def setupUi(self, MainWindow, direction, maxRadius):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 500)
self.centralwidget = QtGui.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.horizontalLayout = QtGui.QHBoxLayout(self.centralwidget)
self.horizontalLayout.setObjectName("horizontalLayout")
self.pushButton = QtGui.QPushButton(self.centralwidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Maximum,
QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.pushButton.sizePolicy().hasHeightForWidth())
self.pushButton.setSizePolicy(sizePolicy)
self.pushButton.setMaximumSize(QtCore.QSize(150, 16777215))
self.pushButton.setObjectName("pushButton")
self.pushButton.setText("Change Direction")
self.horizontalLayout.addWidget(self.pushButton)
self.frame = QtGui.QFrame(self.centralwidget)
self.frame.setFrameShape(QtGui.QFrame.StyledPanel)
self.frame.setFrameShadow(QtGui.QFrame.Raised)
self.frame.setObjectName("frame")
self.gridLayout = QtGui.QGridLayout(self.frame)
self.gridLayout.setObjectName("gridLayout")
self.horizontalLayout.addWidget(self.frame)
# ------
self.mplWidget = MplWidget(self.frame, direction, maxRadius)
# ------
MainWindow.setCentralWidget(self.centralwidget)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.direction = 'up'
self.maxRadius = 0.3
self.ui.setupUi(self, self.direction, self.maxRadius)
self.ui.pushButton.clicked.connect(self.changeStuff)
self.animation = self.ui.mplWidget.animate()
def changeStuff(self):
del self.animation
self.ui.mplWidget.deleteLater()
dirs = {'up' :'down', 'down':'up'}
rads = {0.3:1, 1:0.3}
self.direction = dirs[self.direction]
self.maxRadius = rads[self.maxRadius]
self.ui.mplWidget = MplWidget(self.ui.frame, self.direction,
self.maxRadius)
self.animation = self.ui.mplWidget.animate()
class MplWidget(FigureCanvas):
def __init__(self, parent=None, direction='up', maxRadius=0.3):
self.figure = Figure()
super(MplWidget, self).__init__(self.figure)
self.setParent(parent)
self.axes = self.figure.add_subplot(111, projection='3d')
self.axes.set_xlabel("x label")
self.axes.set_ylabel("y label")
self.axes.set_zlabel("z label")
self.axes.set_xlim3d([-1, 1])
self.axes.set_ylim3d([-1, 1])
self.axes.set_zlim3d([-1, 1])
self.axes.set_aspect('equal')
if direction == 'up':
self.c = 1
elif direction == 'down':
self.c = -1
else:
self.c = 1
self.maxRadius = maxRadius
self.frames = 50
self.plot_handle = self.func_plot(self.frames)
def func_plot(self, z):
z /= float(self.frames) * self.c
theta = np.arange(0, 2 * pi + pi / 50, pi / 50)
xdata = self.maxRadius * z * np.array([cos(q) for q in theta])
ydata = self.maxRadius * z * np.array([sin(q) for q in theta])
zdata = z * np.ones(np.shape(xdata))
if not hasattr(self, 'plot_handle'):
plot_handle = self.axes.plot(xdata, ydata, zdata)[0]
else:
plot_handle = self.plot_handle
plot_handle.set_data(xdata, ydata)
plot_handle.set_3d_properties(zdata)
return plot_handle
def animate(self):
return animation.FuncAnimation(
fig=self.figure, func=self.func_plot, frames=self.frames,
interval=1000.0 / self.frames, blit=False)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
Version information:
matplotlib - 1.2.1
PySide - 1.1.2
Python - 2.7.4
Edit:
I followed the matplotlib Installation Instructions to do a source build from git. I now have version 1.4.x installed. However, the original problem persists.
This is related to a bug in
matplotlib
qtbackend that tears things down in the wrong order (see here). Addingin
ChangeStuff
takes care of the exception you have, but the newly created canvas isn't shown.On the other hand, I don't understand why any canvas is shown at all as the widget is never added to a layout. If you tweak the set up a bit and explicitly add/remove the widget to the
gridLayout
it does what you want: