diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0976bc1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2023 The PyVista Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 3840f46..b4be671 100644 --- a/README.md +++ b/README.md @@ -1 +1,18 @@ -# pyvistaqt-exe +# PyVistaQt Standalone Application + +Create a standalone 3D viewer application with [PyVistaQt](https://qtdocs.pyvista.org). + +This repository provides a template for creating a Windows executable (`.exe`) of a +standalone PyVistaQt application. + +![screenshot](./screenshot.png) + +## Features + +- Bundle assets into the executable +- Create custom menus and actions +- Automated building on CI + +## Automatied Building + +The GitHub Actions workflow here will build the Windows installable executable and upload as an artifact to the workflow run. diff --git a/main.py b/main.py index 8aa9365..5b89611 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,8 @@ import numpy as np import pyvista as pv from pyvistaqt import MainWindow, QtInteractor +from pyvistaqt.dialog import ScaleAxesDialog +from pyvistaqt.utils import _create_menu_bar from qtpy import QtGui, QtWidgets @@ -26,6 +28,7 @@ def resource_path(relative_path=""): class MyMainWindow(MainWindow): + """Extandable MainWindow for PyVistaQt standalone applications.""" def __init__(self, parent=None, show=True): QtWidgets.QMainWindow.__init__(self, parent) @@ -44,33 +47,69 @@ def __init__(self, parent=None, show=True): self.setCentralWidget(self.frame) # simple menu to demo functions - mainMenu = self.menuBar() - fileMenu = mainMenu.addMenu("File") - exitButton = QtWidgets.QAction("Exit", self) - exitButton.setShortcut("Ctrl+Q") - exitButton.triggered.connect(self.close) - fileMenu.addAction(exitButton) - - # allow adding a sphere - meshMenu = mainMenu.addMenu("Mesh") - self.add_sphere_action = QtWidgets.QAction("Add Sphere", self) - self.add_sphere_action.triggered.connect(self.add_sphere) - meshMenu.addAction(self.add_sphere_action) - - # demonstrate using a built-in asset - self.plotter.add_mesh(pv.read(DOGE_FILE), color="tan") + self.main_menu = _create_menu_bar(self) + self.add_menus() if show: self.show() - def add_sphere(self): - """add a sphere to the pyqt frame""" - sphere = pv.Sphere() - self.plotter.add_mesh(sphere, show_edges=True) - self.plotter.reset_camera() + def add_menus(self): + file_menu = self.main_menu.addMenu("File") + exitButton = QtWidgets.QAction("Exit", self) + exitButton.setShortcut("Ctrl+Q") + exitButton.triggered.connect(self.close) + file_menu.addAction(exitButton) + + view_menu = self.main_menu.addMenu("View") + view_menu.addAction("Clear", self.plotter.clear) + view_menu.addAction("Scale Axes", self.scale_axes_dialog) + + view_menu.addSeparator() + # Orientation marker + orien_menu = view_menu.addMenu("Orientation Marker") + orien_menu.addAction("Show All", self.plotter.show_axes_all) + orien_menu.addAction("Hide All", self.plotter.hide_axes_all) + # Bounds axes + axes_menu = view_menu.addMenu("Bounds Axes") + axes_menu.addAction("Add Bounds Axes (front)", self.plotter.show_bounds) + axes_menu.addAction("Add Bounds Grid (back)", self.plotter.show_grid) + axes_menu.addAction("Add Bounding Box", self.plotter.add_bounding_box) + axes_menu.addSeparator() + axes_menu.addAction("Remove Bounding Box", self.plotter.remove_bounding_box) + axes_menu.addAction("Remove Bounds", self.plotter.remove_bounds_axes) + + cam_menu = view_menu.addMenu("Camera") + self._parallel_projection_action = cam_menu.addAction( + "Toggle Parallel Projection", self._toggle_parallel_projection + ) + + def scale_axes_dialog(self, show: bool = True) -> ScaleAxesDialog: + """Open scale axes dialog.""" + self.plotter.app_window = self # HACK + return ScaleAxesDialog(self, self.plotter, show=show) + + def _toggle_parallel_projection(self) -> None: + if self.plotter.camera.GetParallelProjection(): + return self.plotter.disable_parallel_projection() + return self.plotter.enable_parallel_projection() if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MyMainWindow() + + # Custom application logic + def add_doge(): + window.plotter.add_mesh(pv.read(DOGE_FILE), color="tan", name="doge") + window.plotter.camera_position = [ + (45.3316, -15.7107, 95.7495), + (-0.2799, -0.1400, 0.6100), + (-0.1308, 0.9665, 0.2209), + ] + + mesh_menu = window.main_menu.addMenu("Mesh") + add_doge_action = QtWidgets.QAction("Add Doge", window) + add_doge_action.triggered.connect(add_doge) + mesh_menu.addAction(add_doge_action) + sys.exit(app.exec_()) diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..efb9601 Binary files /dev/null and b/screenshot.png differ