This tutorial shows how to use the basic functionality of PyVista, a Python library for 3D interactive plotting. PyVista uses the Visualization Toolkit (VTK) for 3D plotting, and optionally PyQt6 for the user interface.
Here are the main Python documentations useful for this tutorial
Required packages can be installed with
pip install pyvista pyvistaqt PyQt6
This tutorial is available as a Jupyter notebook in this GitHub repository and as an html page.
import pyvista as pv
import pyvistaqt as pvqt
import PyQt6 as Qt
The BackgroundPlotter is the main PyVista class that plots 3D objects using a Qt window. The window is interactive and continues to live even after the end of the Python script.
By default, a toolbar is included to manage the camera, and a menu is added to take screenshots, change view settings, and edit the scene.
Here are the main camera control using the mouse:
mesh_plane = pv.examples.load_airplane()
plotter = pvqt.BackgroundPlotter()
plotter.add_mesh(mesh_plane)
meshes = [
pv.Arrow(),
pv.Box(),
pv.Capsule(),
pv.Circle(),
pv.Cone(),
pv.Cube(),
pv.Cylinder(),
pv.CylinderStructured(),
pv.Disc(),
pv.Icosphere(),
pv.Plane(),
pv.Polygon(),
pv.Pyramid(),
pv.Rectangle(),
pv.SolidSphere(),
pv.SolidSphereGeneric(),
pv.Sphere(),
pv.Superquadric(),
pv.Triangle(),
pv.Tube(),
]
plotter = pvqt.BackgroundPlotter()
for i,mesh in enumerate(meshes):
x = i % 4
y = i // 4
mesh.translate((x*2.5,y*2.5,0), inplace=True)
plotter.add_mesh(mesh, show_edges=True)
PyVista plots objects of type PolyData that correspond to 3D meshes and lines.
A polygonal mesh can be defined by a list of 3D vertices (var_inp
below) and a list of faces specified as a continuous list of integers with the number of vertices in a polygon followed by the index of these vertices.
Lines can be defined from a list of 3D vertices using lines_from_points.
The parameters of add_mesh control the appearance of the objects like their color and opacity.
mesh_custom = pv.PolyData(
var_inp=[
(-1.0, -1.0, 0.0), # vertex 0
(-1.0, +1.0, 0.0), # vertex 1
(+1.0, +1.0, 0.0), # vertex 2
(+1.0, -1.0, 0.0), # vertex 3
( 0.0, 0.0, 2.0), # vertex 4 (apex)
],
faces=[
4, 0,1,2,3, # bottom quad
3, 0,1,4,
3, 1,2,4,
3, 2,3,4,
3, 3,0,4,
],
)
line_custom = pv.lines_from_points([
[ 0, 0,-0.5],
[ 0, 0, 0.0],
[-1,-1, 0.0],
[-1,+1, 0.0],
[+1,+1, 0.0],
[+1,-1, 0.0],
[ 0, 0, 2.0],
[ 0, 0, 2.5],
])
plotter = pvqt.BackgroundPlotter()
plotter.add_mesh(
mesh_custom,
opacity=0.5,
color='lightblue',
show_edges=True,
edge_color='black',
line_width=4)
plotter.add_mesh(
line_custom,
color='red',
line_width=4)
The BackgroundPlotter
of pyvistaqt
uses a
QMainWindow that can have a
QMenuBar, a
QToolBar, a
QStatusBar, and several
QDockWidget as shown below:
The status bar shows text message permanently or during a certain amount of time.
The tool bar includes buttons and checkboxes that call Python methods when they are clicked on.
The menu bar contains menus and sub-menus that also call Python methods.
The menu 'File/Open...' allows to select the OBJ file data/bunny.obj
to load a custom triangular mesh.
mesh_plane = pv.examples.load_airplane()
plotter = pvqt.BackgroundPlotter(
# disable default toolbar and menus
toolbar=False,
menu_bar=False,
)
# store the 'actor' associated to the mesh to change its color and visibility later
actor_plane = plotter.add_mesh(mesh_plane)
main_window = plotter.app_window # QMainWindow
main_window.showMaximized()
status_bar = Qt.QtWidgets.QStatusBar()
tool_bar = Qt.QtWidgets.QToolBar()
menu_bar = Qt.QtWidgets.QMenuBar()
menu_file = Qt.QtWidgets.QMenu("File")
def hello():
'''Prints a message in the console and in the status bar at the bottom of the window'''
print("Hello World!")
status_bar.showMessage("Hello World!", 2000) # message for 2 second
def set_red():
'''Change the plane color to red'''
actor_plane.GetProperty().SetColor(1,0.2,0)
plotter.render() # update the rendering
def set_blue():
'''Change the plane color to blue'''
actor_plane.GetProperty().SetColor(0,0.2,1)
plotter.render() # update the rendering
def open():
'''Open a mesh file and add it to the scene'''
filename, _ = Qt.QtWidgets.QFileDialog.getOpenFileName(
main_window,
'Open file',
'',
'Mesh files (*.stl *.obj *.ply *.vtp *.vtu *.vtk *.g *.3ds *.glb *.gltf);;All files (*)'
)
if filename:
mesh = pv.read(filename)
plotter.add_mesh(mesh)
status_bar.showMessage(f"Loaded file {filename}", 2000) # message for 2 seconds
checkbox = Qt.QtWidgets.QCheckBox("Show plane")
checkbox.setChecked(True)
def show_hide_plane(checked: bool):
'''Show/hide the plane'''
actor_plane.SetVisibility(checked)
plotter.render() # update the rendering
checkbox.stateChanged.connect(show_hide_plane)
tool_bar.addAction("Hello", hello)
tool_bar.addAction("Red", set_red)
tool_bar.addAction("Blue", set_blue)
tool_bar.addWidget(checkbox)
menu_file.addAction("Open...", open)
menu_file.addAction("Close", plotter.close)
menu_bar.addMenu(menu_file)
main_window.addToolBar(tool_bar)
main_window.setStatusBar(status_bar)
main_window.setMenuBar(menu_bar)
Many more Qt widgets are available such as spinbox, combobox, slider...
These Qt widgets can be added into 'layouts' (like QGridLayout
, QVBoxLayout
or QHboxLayout
) that organize top-level widgets (like a QDialog
).
Their 'signals' can be 'connected' to Python methods that are automatically called when some events occur like when a button is clicked, when a value changes...
mesh_plane = pv.examples.load_airplane()
plotter = pvqt.BackgroundPlotter(
toolbar=False,
menu_bar=False,
)
plotter.add_mesh(mesh_plane)
main_window = plotter.app_window
main_window.showMaximized()
status_bar = Qt.QtWidgets.QStatusBar()
tool_bar = Qt.QtWidgets.QToolBar()
def demo_widgets():
# QCheckBox _________________________________________________
check_box = Qt.QtWidgets.QCheckBox("QCheckBox")
check_box_label = Qt.QtWidgets.QLabel("checked")
check_box.setChecked(True)
def checkbox_changed(checked: bool):
status_bar.showMessage("QCheckBox clicked")
if checked:
check_box_label.setText("checked")
else:
check_box_label.setText("unchecked")
check_box.stateChanged.connect(checkbox_changed)
# QSpinBox _________________________________________________
spin_box = Qt.QtWidgets.QSpinBox()
spin_box_label = Qt.QtWidgets.QLabel("QSpinBox value: 50")
spin_box.setRange(0,100)
spin_box.setValue(50)
def spin_box_changed(value: int):
spin_box_label.setText(f"QSpinBox value: {value}")
status_bar.showMessage(f"QSpinBox value: {value}")
spin_box.valueChanged.connect(spin_box_changed)
# QDoubleSpinBox ___________________________________________
double_spin_box = Qt.QtWidgets.QDoubleSpinBox()
double_spin_box_label = Qt.QtWidgets.QLabel("QDoubleSpinBox value: 0.000")
double_spin_box.setRange(-10,10)
double_spin_box.setValue(0)
double_spin_box.setDecimals(3)
double_spin_box.setSingleStep(0.01)
def double_spin_box_changed(value: int):
double_spin_box_label.setText(f"QDoubleSpinBox value: {value:.3f}")
status_bar.showMessage(f"QDoubleSpinBox value: {value:.3f}")
double_spin_box.valueChanged.connect(double_spin_box_changed)
# QComboBox ________________________________________________
combo_box = Qt.QtWidgets.QComboBox()
combo_box_label = Qt.QtWidgets.QLabel("QComboBox index/value: 0/Item 1")
combo_box.addItems([f"Item {i+1}" for i in range(5)])
def combo_box_changed(index: int):
value = combo_box.currentText()
combo_box_label.setText(f"QComboBox index/value: {index}/{value}")
status_bar.showMessage(f"QComboBox index/value: {index}/{value}")
combo_box.currentIndexChanged.connect(combo_box_changed)
# QSlider __________________________________________________
slider = Qt.QtWidgets.QSlider(Qt.QtCore.Qt.Orientation.Horizontal)
slider_label = Qt.QtWidgets.QLabel("QSlider value: 50")
slider.setRange(0,100)
slider.setValue(50)
def slider_changed(value: int):
slider_label.setText(f"QSlider value: {value}")
status_bar.showMessage(f"QSlider value: {value}")
slider.valueChanged.connect(slider_changed)
# QLineEdit ________________________________________________
line_edit = Qt.QtWidgets.QLineEdit()
line_edit.setText("enter text...")
# QPushButton ______________________________________________
push_button = Qt.QtWidgets.QPushButton("Ok")
def push_button_clicked():
print(f'QCheckBox: {check_box.isChecked()}')
print(f'QSpinBox: {spin_box.value()}')
print(f'QDoubleSpinBox: {double_spin_box.value()}')
print(f'QComboBox: {combo_box.currentText()}')
print(f'QSlider: {slider.value()}')
print(f'QLineEdit: {line_edit.text()}')
dialog.close()
push_button.clicked.connect(push_button_clicked)
# QDialog
dialog = Qt.QtWidgets.QDialog(parent=main_window)
layout = Qt.QtWidgets.QGridLayout()
layout.addWidget(Qt.QtWidgets.QLabel("This is a dialog box to demonstrate more widgets"), 0,0,1,2) # fromRow, fromColumn, rowSpan, columnSpan
layout.addWidget(check_box,1,0) # row, column
layout.addWidget(check_box_label,1,1)
layout.addWidget(spin_box,2,0)
layout.addWidget(spin_box_label,2,1)
layout.addWidget(double_spin_box,3,0)
layout.addWidget(double_spin_box_label,3,1)
layout.addWidget(combo_box,4,0)
layout.addWidget(combo_box_label,4,1)
layout.addWidget(slider,5,0)
layout.addWidget(slider_label,5,1)
layout.addWidget(line_edit,6,0,1,2) # fromRow, fromColumn, rowSpan, columnSpan
layout.addWidget(push_button,8,0,1,2)
dialog.setLayout(layout)
dialog.show()
tool_bar.addAction("QtWidgets", demo_widgets)
main_window.addToolBar(tool_bar)
main_window.setStatusBar(status_bar)