Is there a way to customize the default toolbar of a mayavi scene? I would like to delete some buttons as I don't need them (e.g. the save button). Here you can see which toolbar I am talking about:
The code is just an example code:
import os
os.environ['ETS_TOOLKIT'] = 'qt4'
from pyface.qt import QtGui, QtCore
from traits.api import HasTraits, Instance, on_trait_change
from traitsui.api import View, Item
from mayavi.core.ui.api import MayaviScene, MlabSceneModel, SceneEditor
from tvtk.pyface.api import DecoratedScene
from pyface.api import ImageResource
from pyface.action.api import Action
class MyCustomScene(DecoratedScene):
def _actions_default(self):
actions = [
Action(
image = ImageResource("path to image",
search_path = [self._get_image_path()],
),
tooltip = "blabla",
on_perform = self._save_snapshot,
)
]
actions.extend(DecoratedScene._actions_default(self))
return actions
#The actual visualization
class Visualization(HasTraits):
scene = Instance(MlabSceneModel, ())
#on_trait_change('scene.activated')
def update_plot(self):
# We can do normal mlab calls on the embedded scene.
self.scene.mlab.test_points3d()
# the layout of the dialog screated
view = View(Item('scene', editor=SceneEditor(scene_class=MyCustomScene),
height=250, width=300, show_label=False),
resizable=True # We need this to resize with the parent widget
)
################################################################################
# The QWidget containing the visualization, this is pure PyQt4 code.
class MayaviQWidget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
layout = QtGui.QVBoxLayout(self)
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
self.visualization = Visualization()
# The edit_traits call will generate the widget to embed.
self.ui = self.visualization.edit_traits(parent=self,
kind='subpanel').control
layout.addWidget(self.ui)
self.ui.setParent(self)
if __name__ == "__main__":
# Don't create a new QApplication, it would unhook the Events
# set by Traits on the existing QApplication. Simply use the
# '.instance()' method to retrieve the existing one.
app = QtGui.QApplication.instance()
container = QtGui.QWidget()
container.setWindowTitle("Embedding Mayavi in a PyQt4 Application")
# define a "complex" layout to test the behaviour
layout = QtGui.QGridLayout(container)
# put some stuff around mayavi
label_list = []
for i in range(3):
for j in range(3):
if (i==1) and (j==1):continue
label = QtGui.QLabel(container)
label.setText("Your QWidget at (%d, %d)" % (i,j))
label.setAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter)
layout.addWidget(label, i, j)
label_list.append(label)
mayavi_widget = MayaviQWidget(container)
layout.addWidget(mayavi_widget, 1, 1)
container.show()
window = QtGui.QMainWindow()
window.setCentralWidget(container)
window.show()
# Start the main event loop.
app.exec_()
I think it is somewhere hidden in the MayaviScene. Maybe it is necessary to create a new class with a new scene or something like this?
You should check _actions_default for MayaviScene and for DecoratedScene to see how to create one of your own. The second one shows how to create a toolbar from scratch while the first one shows how your toolbar code interfaces with other components.
class MyCustomScene(DecoratedScene):
# …
def _actions_default(self):
actions = [
# add icons here
# …
]
return actions
Related
I have embedded the vtkOrientationMarkerWidget to the QTWidget as an axes indicator, but I met a problem there arises a bug:
ERROR: In C:\Dev\Soft\vtk\source\Rendering\OpenGL2\vtkWin32OpenGLRenderWindow.cxx, line 217
vtkWin32OpenGLRenderWindow (00000278F82D1AD0): wglMakeCurrent failed in MakeCurrent(), error: Handle
Invalid
when I multiply generate QTWidget instances by clicking the button.
Because I want to add some buttons in the 3D view, I use QTWidget as a container, which is the root cause of the problem. Are there some solutions for this?
My environment is Python 3.8+PyQT5.15.0+VTK9.0.1
The code is:
The main window:
from PyQt5.QtWidgets import QMainWindow, QDesktopWidget, QFrame, QVBoxLayout, QApplication, QPushButton
from axes_widget import VTKWidget2
from axes import Axes
from axes2 import Axes2
class Widget:
def __init__(self):
self.window = QMainWindow()
self.window.resize(500, 400)
self.window.move(300, 310)
self.button = QPushButton('3D', self.window)
self.button.clicked.connect(self.click_3d)
self.button.move(380, 80)
def click_3d(self):
self._win_vtk = VTKWidget2()
self._win_vtk.show()
if __name__ == '__main__':
app = QApplication([])
widget_ins=Widget()
widget_ins.window.show()
app.exec_()
The VTK window:
from PyQt5.QtWidgets import QMainWindow, QDesktopWidget, QFrame, QVBoxLayout, QApplication
import sys
import vtk
import vtkmodules.qt
vtkmodules.qt.QVTKRWIBase = "QGLWidget"
# QGLWidget as the base class of the QVTKRenderWindowInteractor, instead of QWidget.
# This change is because it is reported that sometimes QWidget can cause rendering problems.
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from serum import dependency, singleton, inject
# #singleton
class VTKWidget2(QMainWindow):
def __init__(self, parent=None):
super(VTKWidget2, self).__init__(parent)
self.camera_focal_point = [0, 0, 0]
self.camera_position = [-4500, 0, 0]
self.camera_view_up = [0, -1, 0] # default, [0,1,0]
self.camera_azimuth = 0 # -20
self.camera_zoom = 1.8 # 1.75
self.z_angle = 0
self.x_angle = 0
self.y_angle = 0
self.x_transform = 200
self.y_transform = -900 # -820
self.z_transform = 1000
# vtk.vtkOutputWindow.SetGlobalWarningDisplay(0)
self.initial()
def initial(self):
self.setWindowTitle("3D_Model")
self.resize(1000, 800)
screen = QDesktopWidget().geometry()
self.self_size = self.geometry()
self.move(int((screen.width() - self.self_size.width()) / 2),
int((screen.height() - self.self_size.height()) / 2)) #
self.colors = vtk.vtkNamedColors()
# Create an actor
# self.actor = vtk.vtkActor()
self.left_text_actor = vtk.vtkTextActor()
self.right_text_actor = vtk.vtkTextActor()
# A renderer and render window
self.renderer = vtk.vtkRenderer()
# renderWindow = vtk.vtkRenderWindow()
# renderWindow.SetWindowName("Display Coordinate Axes")
# renderWindow.AddRenderer(renderer)
# An interactor
# renderWindowInteractor = vtk.vtkRenderWindowInteractor()
# renderWindowInteractor.SetRenderWindow(renderWindow)
self.renderWindowInteractor = QVTKRenderWindowInteractor()
self.renderWindow = self.renderWindowInteractor.GetRenderWindow()
self.renderWindow.AddRenderer(self.renderer)
# Add the actors to the scene
# self.renderer.AddActor(self.actor)
self.renderer.SetBackground(self.colors.GetColor3d("SlateGray"))
# add mouse interaction mode
vtkStyle = vtk.vtkInteractorStyleTrackballCamera()
# vtkStyle = MyInteractorStyle(self.renderWindow)
# vtkStyle = vtk.vtkInteractorStyleSwitch()
# vtkStyle = vtk.vtkInteractorStyleTrackballActor()
self.renderWindowInteractor.SetInteractorStyle(vtkStyle)
# self.renderWindow.GetInteractor().SetInteractorStyle(vtkStyle)
self.transform = vtk.vtkTransform()
self.transform.Translate(self.x_transform, self.y_transform, self.z_transform)
# set axes
self.add_axes()
self.vtkCamera = vtk.vtkCamera()
self.update_camera()
frame = QFrame()
self.setCentralWidget(frame) # QMainWindow's property
vl = QVBoxLayout()
vl.addWidget(self.renderWindowInteractor)
# vl.addWidget(self.widget) # problem: wrong: no widget for QT
frame.setLayout(vl)
# Begin mouse interaction
self.renderWindowInteractor.Initialize()
self.renderWindowInteractor.Start()
def add_axes(self):
# set axes
### important: widget must be set as field, otherwise it doesn't show.
# problem: no widget for QT
self.widget = vtk.vtkOrientationMarkerWidget()
self.axis = vtk.vtkAxesActor()
rgba = [0] * 4
self.colors.GetColor("Carrot", rgba)
self.widget.SetOutlineColor(rgba[0], rgba[1], rgba[2])
self.widget.SetOrientationMarker(self.axis) ### important
self.widget.SetInteractor(self.renderWindowInteractor)
self.widget.SetViewport(0.0, 0.0, 0.4, 0.4)
self.widget.SetEnabled(1)
self.widget.InteractiveOn()
def update_camera(self):
self.renderer.ResetCamera()
self.renderer.SetActiveCamera(self.vtkCamera)
self.vtkCamera.SetFocalPoint(self.camera_focal_point)
self.vtkCamera.SetPosition(self.camera_position)
self.vtkCamera.SetViewUp(self.camera_view_up)
self.vtkCamera.Azimuth(self.camera_azimuth)
self.vtkCamera.Zoom(self.camera_zoom)
self.renderWindow.Render()
The problem lies in Widget.click_3d(self). Every time you click the button you reassign a new VTKWidget2 to self._win_vtk. Since self._win_vtk is the only reference in your program to any of the VTKWidget2 windows, as soon as you assign a new value to self._win_vtk, the previous VTKWidget2 window will be deleted by the garbage collector (which apparently causes problems with the underlying QGLWidget). One way around this is to make a persistent reference to all the windows, for example by putting them in a list, e.g.
class Widget:
def __init__(self):
....
self.vtk_windows = []
def click_3d(self):
win = VTKWidget2()
self.vtk_windows.append(win)
win.show()
This still causes a bunch of errors when the last window is closed an the program exits which seems to be cause by using a QGLWidget as the base of your QVTKRenderWindowInteractor.
Below is some minimal code that fully demonstrates what I call "pipeline pollution". Each time you press the 'Draw' button, the MayaviScene editor (accessed via the top-left button on the figure) will update the figure, but also create a new scene's "shell" that lingers in the pipeline (as seen in the attached image).
I'm worried that in my more complex project, this pileup will have adverse effects.
Can someone please guide me on how to best set up this Mayavi scene to simply be updated without excess accumulation? I've read through tons of online materials, but still don't understand the developer's logic.
import sys, os
import numpy as np
from pyface.qt import QtGui, QtCore
os.environ['ETS_TOOLKIT'] = 'qt4'
from traits.api import HasTraits,Instance,on_trait_change
from traitsui.api import View,Item
from mayavi import mlab
from mayavi.core.ui.api import MayaviScene, MlabSceneModel, SceneEditor
class Mayavi_Scene(HasTraits):
scene = Instance(MlabSceneModel, ())
def update_scene(self):
Mayavi_Scene.fig1 = mlab.figure(1, bgcolor=(.5,.5,.5))
self.scene.mlab.clf(figure=Mayavi_Scene.fig1)
splot = mlab.points3d(P1.x, P1.y, P1.z,
scale_factor=0.05, figure=Mayavi_Scene.fig1)
view = View(Item('scene', editor = SceneEditor(scene_class=MayaviScene),
height=300, width=300, show_label=False),
resizable=True,
)
class P1(QtGui.QWidget):
# data starts out empty, wait for user input (below, via 'draw()'):
x = []
y = []
z = []
def __init__(self, parent=None):
super(P1, self).__init__(parent)
layout = QtGui.QGridLayout(self)
layout.setContentsMargins(20,20,20,20)
layout.setSpacing(10)
self.viz1 = Mayavi_Scene()
self.ui1 = self.viz1.edit_traits(parent=self, kind='subpanel').control
layout.addWidget(self.ui1, 0, 0, 1, 1)
def draw(): #a sample user input, could have been a custom data file, etc.
P1.x = np.random.random((100,))
P1.y = np.random.random((100,))
P1.z = np.random.random((100,))
Mayavi_Scene().update_scene()
#repeated presses pollute MayaviScene pipeline
# button to draw data:
self.btn1 = QtGui.QPushButton('Draw',self)
self.connect(self.btn1, QtCore.SIGNAL('clicked()'), draw)
layout.addWidget(self.btn1, 1, 0, 1, 1)
self.btn1.show()
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.window = P1(self)
self.setCentralWidget(self.window)
self.show()
if __name__ == '__main__':
app = QtGui.QApplication.instance()
w = MainWindow()
sys.exit(app.exec_())
The cause is likely the line containing Mayavi_Scene().update_scene() in the draw internal function. Every time draw is called, it creates a new Mayavi_Scene. The following P1 class instead defines draw as a method that accesses self.viz1 directly. I've also replaced the reference to draw with a reference to self.draw
class P1(QtGui.QWidget):
# data starts out empty, wait for user input (below, via 'draw()'):
x = []
y = []
z = []
def __init__(self, parent=None):
super(P1, self).__init__(parent)
layout = QtGui.QGridLayout(self)
layout.setContentsMargins(20,20,20,20)
layout.setSpacing(10)
self.viz1 = Mayavi_Scene()
self.ui1 = self.viz1.edit_traits(parent=self, kind='subpanel').control
layout.addWidget(self.ui1, 0, 0, 1, 1)
# button to draw data:
self.btn1 = QtGui.QPushButton('Draw',self)
# Connect the widget's draw method and the button
self.connect(self.btn1, QtCore.SIGNAL('clicked()'), self.draw)
layout.addWidget(self.btn1, 1, 0, 1, 1)
self.btn1.show()
def draw(self): #a sample user input, could have been a custom data file, etc.
P1.x = np.random.random((100,))
P1.y = np.random.random((100,))
P1.z = np.random.random((100,))
# Update the current scene without creating a new one.
self.viz1.update_scene()
I have an existing application that I am polishing off and I want to add some animation to a few of the widgets. Animating widgets with QPropertyAnimation outside of layouts is easy and fun, however when they are in a layout I am having various difficulties. The current one giving me a headache is that when I animate the size of a widget, the layout does not adjust to it's new size.
So lets say I have a QVBoxLayout with three widgets: a label which should expand to all available space, a treeview, and a button. When I click the button I want the tree to collapse and the label to take over it's space. Below is this example in code, and as you can see while the tree animates it's size nothing happens, and then when I hide it at the end of the animation the label pops to fill the now vacant space. So it seems that during the animation the layout does not "know" the tree is resizing. What I would like to happen is that AS the tree shrinks, the label expands to fill it.
Could this could be done not by absolute sizing of the label, but by calling a resize on the layout or something like that? I ask because I want to animate several widgets across my application and I want to find the best way to do this without having to make too many widgets interdependent upon each other.
Example code:
import sys
from PyQt4 import QtGui, QtCore
class AnimatedWidgets(QtGui.QWidget):
def __init__(self):
super(AnimatedWidgets, self).__init__()
layout1 = QtGui.QVBoxLayout()
self.setLayout(layout1)
expanding_label = QtGui.QLabel("Expanding label!")
expanding_label.setStyleSheet("border: 1px solid red")
layout1.addWidget(expanding_label)
self.file_model = QtGui.QFileSystemModel(self)
sefl.file_model.setRootPath("C:/")
self.browse_tree = QtGui.QTreeView()
self.browse_tree.setModel(self.file_model)
layout1.addWidget(self.browse_tree)
shrink_tree_btn = QtGui.QPushButton("Shrink the tree")
shrink_tree_btn.clicked.connect(self.shrink_tree)
layout1.addWidget(shrink_tree_btn)
#--
self.tree_size_anim = QtCore.QPropertyAnimation(self.browse_tree, "size")
self.tree_size_anim.setDuration(1000)
self.tree_size_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuart)
self.tree_pos_anim = QtCore.QPropertyAnimation(self.browse_tree, "pos")
self.tree_pos_anim.setDuration(1000)
self.tree_pos_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuart)
self.tree_anim_out = QtCore.QParallelAnimationGroup()
self.tree_anim_out.addAnimation(self.tree_size_anim)
self.tree_anim_out.addAnimation(self.tree_pos_anim)
def shrink_tree(self):
self.tree_size_anim.setStartValue(self.browse_tree.size())
self.tree_size_anim.setEndValue(QtCore.QSize(self.browse_tree.width(), 0))
tree_rect = self.browse_tree.geometry()
self.tree_pos_anim.setStartValue(tree_rect.topLeft())
self.tree_pos_anim.setEndValue(QtCore.QPoint(tree_rect.left(), tree_rect.bottom()))
self.tree_anim_out.start()
self.tree_anim_out.finished.connect(self.browse_tree.hide)
def main():
app = QtGui.QApplication(sys.argv)
ex = AnimatedWidgets()
ex.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
The layouts handle the geometry() of the widgets so that when wanting to change the pos property these are interfacing with their handles so it is very common that you get that type of behavior, a better option is to use a QVariantAnimation to establish a fixed height:
import sys
from PyQt4 import QtGui, QtCore
class AnimatedWidgets(QtGui.QWidget):
def __init__(self):
super(AnimatedWidgets, self).__init__()
layout1 = QtGui.QVBoxLayout(self)
expanding_label = QtGui.QLabel("Expanding label!")
expanding_label.setStyleSheet("border: 1px solid red")
layout1.addWidget(expanding_label)
self.file_model = QtGui.QFileSystemModel(self)
self.file_model.setRootPath(QtCore.QDir.rootPath())
self.browse_tree = QtGui.QTreeView()
self.browse_tree.setModel(self.file_model)
layout1.addWidget(self.browse_tree)
shrink_tree_btn = QtGui.QPushButton("Shrink the tree")
shrink_tree_btn.clicked.connect(self.shrink_tree)
layout1.addWidget(shrink_tree_btn)
#--
self.tree_anim = QtCore.QVariantAnimation(self)
self.tree_anim.setDuration(1000)
self.tree_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuart)
def shrink_tree(self):
self.tree_anim.setStartValue(self.browse_tree.height())
self.tree_anim.setEndValue(0)
self.tree_anim.valueChanged.connect(self.on_valueChanged)
self.tree_anim.start()
def on_valueChanged(self, val):
h, isValid = val.toInt()
if isValid:
self.browse_tree.setFixedHeight(h)
def main():
app = QtGui.QApplication(sys.argv)
ex = AnimatedWidgets()
ex.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Is posible add icon mesh to textScrollList items in maya python ?
def allMeshes(*args):
listMesh = cmd.ls(type="mesh")
geometry = cmd.listRelatives(listMesh, p=True )
if len(geometry) > 0:
for g in geometry:
cmd.textScrollList('lstMesh', e=True, a=g) #here add icon
else:
return
Thank you
Like I said in my comment, I don't see a way to add an icon for that control, but here's how you can get it working using PySide Maya natively comes with PySide so there's no need to install anything. If you're using 2017/2018 then it uses PySide2 instead and requires very minor changes.
from PySide import QtGui, QtCore
import maya.cmds as cmds
class Window(QtGui.QDialog):
def __init__(self, parent=None):
# Inherit QDialog.
QtGui.QDialog.__init__(self, parent=parent)
# Create a list.
self.list = QtGui.QListWidget(parent=self)
# Create a layout so the list will stretch with the window.
self.main_layout = QtGui.QVBoxLayout()
self.main_layout.addWidget(self.list)
self.setLayout(self.main_layout)
# Set window properties.
self.setWindowTitle("My tool")
self.resize(300, 500)
# Populate list with all mesh objects.
self.populate_list()
def populate_list(self):
# Collect all meshes in the scene.
geometry = cmds.listRelatives(cmds.ls(type="mesh"), parent=True) or []
# This uses Maya's internal icons.
# You can just point it to whatever icon you want.
img_name = cmds.resourceManager(nameFilter="*mesh*")[0]
img_path = ":/{}".format(img)
# Create list items.
for obj in geometry:
item = QtGui.QListWidgetItem(obj)
item.setSizeHint(QtCore.QSize(0, 50)) # Increases item's height a bit.
item.setIcon(QtGui.QIcon(img_path))
self.list.addItem(item)
# Create an instance of the tool.
win = Window()
win.show()
Here's the result with 3 spheres in the scene:
EDIT: There are a number of similar posts on PyQt4 progress bars not updating. They all focus on the issue of threads & where the program actually updates the window. Although helpful, my code was so structured that the replies were not practical. The accepted answer given here is simple, to the point & works.
I am using Python 2.7 and PyQT 4 on a Win 7 x64 machine.
I am trying to clear my window of one widget, an 'Accept' button, see code, and replace it with a progress bar.
Even though I close the 'Accept' button & add the progress bar before the processing loop is entered into. The window is only updated after the loop has finished & the progress bar jumps straight to 100%.
My code,
from PyQt4 import QtCore, QtGui
import sys
import time
class CentralWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(CentralWidget, self).__init__(parent)
# set layouts
self.layout = QtGui.QVBoxLayout(self)
# Poly names
self.pNames = QtGui.QLabel("Import file name", self)
self.polyNameInput = QtGui.QLineEdit(self)
# Polytype selection
self.polyTypeName = QtGui.QLabel("Particle type", self)
polyType = QtGui.QComboBox(self)
polyType.addItem("")
polyType.addItem("Random polyhedra")
polyType.addItem("Spheres")
polyType.addItem("Waterman polyhedra")
polyType.activated[str].connect(self.onActivated)
# Place widgets in layout
self.layout.addWidget(self.pNames)
self.layout.addWidget(self.polyNameInput)
self.layout.addWidget(self.polyTypeName)
self.layout.addWidget(polyType)
self.layout.addStretch()
# Combobox choice
def onActivated(self, text):
if text=="Random polyhedra":
self.randomPolyhedra(text)
if text=="Spheres": # not implementaed yet
self.polyTypeName.setText("Not implemented yet.")
self.polyTypeName.adjustSize()
if text=="Waterman polyhedra": # not implementaed yet
self.polyTypeName.setText("Not implemented yet.")
self.polyTypeName.adjustSize()
# New options for random polyhedra choice
def randomPolyhedra(self, text):
self.polyNumberLbl = QtGui.QLabel("How many: ", self)
self.polyNumber = QtGui.QLineEdit(self)
self.acceptSeed = QtGui.QPushButton('Accept') # Accept button created
self.acceptSeed.clicked.connect(lambda: self.ranPolyGen())
self.layout.addWidget(self.polyNumberLbl)
self.layout.addWidget(self.polyNumber)
self.layout.addWidget(self.acceptSeed) # Accept button in layout
self.randFlag = True
self.polyTypeName.setText(text)
self.polyTypeName.adjustSize()
# Act on option choices for random polyhedra
def ranPolyGen(self):
polyCount = int(self.polyNumber.text())
self.progressBar = QtGui.QProgressBar() # Progress bar created
self.progressBar.setMinimum(1)
self.progressBar.setMaximum(polyCount)
self.acceptSeed.close() # Accept button closed
self.layout.addWidget(self.progressBar) # Add progressbar to layout
for poly in range(1, polyCount+1):
time.sleep(1) # Calls to main polyhedral generating code go here
print poly
self.progressBar.setValue(poly)
self.doneLbl = QtGui.QLabel("Done", self)
self.layout.addWidget(self.doneLbl)
# Creates GUI
class Polyhedra(QtGui.QMainWindow):
def __init__(self):
super(Polyhedra, self).__init__()
# Place central widget in layout
self.central_widget = CentralWidget(self)
self.setCentralWidget(self.central_widget)
# Set up window
self.setGeometry(500, 500, 300, 300)
self.setWindowTitle('Pyticle')
self.show()
# Combo box
def onActivated(self, text):
self.central_widget.onActivated(text)
def main():
app = QtGui.QApplication(sys.argv)
poly = Polyhedra()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Below is a picture of during loop execution & after completion.
I dont think I have got my head around the addWidget() method. I was under the impression that this would add another widget to the present layout (a vbox layout here) & that the .close() method removed a widget when directed to do so.
What am I missing?
You can add:
from PyQt4.QtGui import QApplication
Then in your for loop:
QApplication.processEvents()
Your app is actually becoming unresponsive, you need to call processEvents() to process the events and redraw the gui. I am not overly familiar with pyqt but I imagine another alternative is using a thread.