# -*- coding: utf-8 -*-
import sys
import importlib
try:
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor, PyQtImpl
# QVTKRenderWindowInteractor raises an error if this isn't loaded.
from vtkmodules.vtkRenderingOpenGL2 import vtkOpenGLRenderer
except:
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor, PyQtImpl
QtWidgets = importlib.import_module(PyQtImpl + ".QtWidgets")
QtGui = importlib.import_module(PyQtImpl + ".QtGui")
QtCore = importlib.import_module(PyQtImpl + ".QtCore")
from vtkplotlib.figures.BaseFigure import BaseFigure
from vtkplotlib import nuts_and_bolts
if __name__ == "__main__":
debug = print
else:
debug = lambda *x: None
[docs]class QtFigure(BaseFigure, QtWidgets.QWidget):
"""The VTK render window in a `PyQt6.QtWidgets.QWidget`.
This can be embedded into a GUI the same way all other
`PyQt6.QtWidgets.QWidget`\\ s are used.
:param name: The window title of the figure, only applicable is **parent** is None, defaults to 'qt vtk figure'.
:type name: str
:param parent: Parent window, defaults to None.
:type parent: :sip:class:`PyQt6.QtWidgets.QWidget`
.. note::
If you are new to Qt then this is a rather poor place to start. Whilst
many libraries in Python are intuitive enough to be able to just dive
straight in, Qt is not one of them. Preferably familiarise yourself
with some basic Qt before coming here.
This class inherits both from `PyQt6.QtWidgets.QWidget` and a
vtkplotlib base figure class. Therefore it can be used exactly the same as
you would normally use either a `PyQt6.QtWidgets.QWidget` or a
`vtkplotlib.figure`.
Care must be taken when using Qt to ensure you have **exactly one**
`PyQt6.QtWidgets.QApplication`. To make this class quicker to use
the qapp is created automatically but is wrapped in a
.. code-block:: python
if QApplication.instance() is None:
self.qapp = QApplication(sys.argv)
else:
self.qapp = QApplication.instance()
This prevents multiple `PyQt6.QtWidgets.QApplication` instances
from being created (which causes an instant crash) whilst also preventing a
`PyQt6.QtWidgets.QWidget` from being created without a qapp
(which also causes a crash).
On :func:`show()`, :sip:method:`qapp.exec()
<PyQt6.QtWidgets.QApplication.exec>` is called automatically if
``figure.parent() is None`` (unless specified otherwise).
If the figure is part of a larger window then ``larger_window.show()``
must also explicitly show the figure. It won't begin interactive mode until
`PyQt6.QtWidgets.QApplication.exec` is called.
If the figure is not to be part of a larger window then it behaves exactly
like a regular figure. You just need to explicitly create it first.
.. code-block:: python
import vtkplotlib as vpl
# Create the figure. This automatically sets itself as the current
# working figure. The qapp is created automatically if one doesn't
# already exist.
vpl.QtFigure("Exciting Window Title")
# Everything from here on should be exactly the same as normal.
vpl.quick_test_plot()
# Automatically calls ``qapp.exec()``. If you don't want it to then
# use ``vpl.show(False)``.
vpl.show()
However this isn't particularly helpful. A more realistic example would
require the figure be part of a larger window. In this case, treat the
figure as you would any other QWidget. You must explicitly call
``figure.show()`` however. (Not sure why.)
.. code-block:: python
import sys
import numpy as np
from PyQt6 import QtWidgets
import vtkplotlib as vpl
class FigureAndButton(QtWidgets.QWidget):
def __init__(self):
super().__init__()
# Go for a vertical stack layout.
vbox = QtWidgets.QVBoxLayout()
self.setLayout(vbox)
# Create the figure
self.figure = vpl.QtFigure()
# Create a button and attach a callback.
self.button = QtWidgets.QPushButton("Make a Ball")
self.button.released.connect(self.button_pressed_cb)
# QtFigures are QWidgets and are added to layouts with `addWidget`
vbox.addWidget(self.figure)
vbox.addWidget(self.button)
def button_pressed_cb(self):
\"""Plot commands can be called in callbacks. The current working
figure is still self.figure and will remain so until a new
figure is created explicitly. So the ``fig=self.figure``
arguments below aren't necessary but are recommended for
larger, more complex scenarios.
\"""
# Randomly place a ball.
vpl.scatter(np.random.uniform(-30, 30, 3),
color=np.random.rand(3),
fig=self.figure)
# Reposition the camera to better fit to the balls.
vpl.reset_camera(self.figure)
# Without this the figure will not redraw unless you click on it.
self.figure.update()
def show(self):
# The order of these two are interchangeable.
super().show()
self.figure.show()
def closeEvent(self, event):
\"""This isn't essential. VTK, OpenGL, Qt and Python's garbage
collect all get in the way of each other so that VTK can't
clean up properly which causes an annoying VTK error window to
pop up. Explicitly calling QtFigure's `closeEvent()` ensures
everything gets deleted in the right order.
\"""
self.figure.closeEvent(event)
qapp = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
window = FigureAndButton()
window.show()
qapp.exec()
.. note:: `QtFigure`\\ s are not reshow-able if the figure has a parent.
.. note::
VTK automatically selects the Qt variant (``PyQt6``, ``PyQt5``,
``PySide6`` or ``PySide2``) based on what has already been imported. To
use a different Qt variant, import it *before* importing `vtkplotlib`.
.. seealso:: `vtkplotlib.QtFigure2` is an extension of this to provide some standard GUI elements, ready-made.
"""
def __init__(self, name="qt vtk figure", parent=None):
self.qapp = QtWidgets.QApplication.instance() or QtWidgets.QApplication(
sys.argv)
QtWidgets.QWidget.__init__(self, parent)
BaseFigure.__init__(self, name)
self.vl = QtWidgets.QVBoxLayout()
self.setLayout(self.vl)
self._vtkWidget = QVTKRenderWindowInteractor(self)
self.vl.addWidget(self.vtkWidget)
self.renWin
self.iren
def _re_init(self):
debug("re init")
name = self.window_name
QtWidgets.QWidget.__init__(self, self.parent())
self.window_name = name
self.setLayout(self.vl)
self.setWindowTitle(self.window_name)
self.vtkWidget = QVTKRenderWindowInteractor(self)
self.vl.insertWidget(self._vtkWidget_replace_index, self.vtkWidget)
self.renWin, self.iren
@property
def vtkWidget(self):
if not hasattr(self, "_vtkWidget"):
self._re_init()
return self._vtkWidget
@vtkWidget.setter
def vtkWidget(self, widget):
self._vtkWidget = widget
@vtkWidget.deleter
def vtkWidget(self):
if hasattr(self, "_vtkWidget"):
del self._vtkWidget
def _base_show_wrap(QWidget_show_name):
"""Wrap all the ``QWidget.show()``, ``QWidget.showMaximized()`` etc
methods so they can all be used as expected. Just in case Qt has changed
and some ``show...()`` methods aren't present, this defaults to just
``show()``.
"""
QWidget_show = getattr(QtWidgets.QWidget, QWidget_show_name,
QtWidgets.QWidget.show)
def show(self, block=None):
if not hasattr(self, "vtkWidget"):
self._re_init()
self._connect_renderer()
QWidget_show(self)
self.iren.Initialize()
self.renWin.Render()
self.iren.Start()
if block is None:
block = self.parent() is None
if block:
self._flush_stdout()
self.qapp.exec()
BaseFigure.show(self, block)
show.__name__ = QWidget_show.__name__
try:
show.__qualname__ = QWidget_show.__qualname__
except (AttributeError, TypeError):
pass
return show
show = _base_show_wrap("show")
showMaximized = _base_show_wrap("showMaximized")
showMinimized = _base_show_wrap("showMinimized")
showFullScreen = _base_show_wrap("showFullScreen")
showNormal = _base_show_wrap("showNormal")
@nuts_and_bolts.init_when_called
def renWin(self):
if not hasattr(self, "vtkWidget"):
self._re_init()
renWin = self.vtkWidget.GetRenderWindow()
return renWin
@nuts_and_bolts.init_when_called
def iren(self):
iren = self.renWin.GetInteractor()
iren.SetInteractorStyle(self.style)
return iren
def update(self):
BaseFigure.update(self)
QtWidgets.QWidget.update(self)
self.qapp.processEvents()
# def close(self):
# BaseFigure.close(self)
# QWidget.close(self)
# self._clean_up()
def on_close(self):
debug("cleaning up")
if hasattr(self, "_renWin"):
# These prevent error dialogs popping up.
self._disconnect_renderer()
self.renWin.MakeCurrent()
self.renWin.Finalize()
if hasattr(self, "_vtkWidget"):
self._vtkWidget_replace_index = self.vl.indexOf(self.vtkWidget)
self.vl.removeWidget(self.vtkWidget)
# self.renderer.RemoveAllViewProps()
del self.vtkWidget, self.iren, self.renWin
def closeEvent(self, event):
self.on_close()
window_name = property(QtWidgets.QWidget.windowTitle,
QtWidgets.QWidget.setWindowTitle)
def __del__(self):
try:
self.renderer.RemoveAllViewProps()
except (AttributeError, TypeError):
# In Python2, RemoveAllViewProps is already None
pass
def _prep_for_screenshot(self, off_screen=False):
BaseFigure._prep_for_screenshot(self, off_screen)
if off_screen:
print("Off screen rendering can't be done using QtFigures.")
self.show(block=False)
def close(self):
QtWidgets.QWidget.close(self)
# closeEvent seems to be called anyway but call this just to be sure.
self.on_close()
BaseFigure.close(self)