
PySide: More ScriptJobs
Previously I showed how to add a ScriptJob to a PySide QDialog. This time I’m going to extend that concept. We’re going to make a window that has a combo menu which dynamically loads QWidgets, one of which creates a ScriptJob. That’s the basic functionality, but there is one added twist. We need to kill the job when the combo menu changes the QWidget or when the window is closed.
Let’s start with the MainWindow class. In the __init__ we add the variable self.SCRIPT_JOB_NUMBER. This is used to store any ScriptJob created by the dynamic widgets.
1 2 3 4 5 6 7 8 9 |
class MainWindow( QtGui.QDialog ): def __init__( self, parent=maya_main_window() ): super( MainWindow, self ).__init__( parent ) self.winName = 'My Main Window' self.userInput = None self.SCRIPT_JOB_NUMBER = None self.create() |
Now the window’s UI.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
def create( self ): # Check to see if this UI is already open. If it is then delete it before # creating it anew. if cmds.window( self.winName, exists=True ): cmds.deleteUI( self.winName ) # Set the window object name and window title. self.setObjectName( self.winName ) self.setWindowTitle( self.winName ) # Mode combo menu. self.comboMenu = QtGui.QComboBox() self.comboItems = [ 'Widget One', 'Widget Two' ] self.comboMenu.insertItems( 0, self.comboItems ) self.comboMenu.setFixedWidth( 100 ) self.comboMenu.activated.connect( self.updateGui ) # Combo box layout comboLayout = QtGui.QHBoxLayout() comboLayout.setContentsMargins( 2, 2, 2, 2 ) comboLayout.setAlignment( QtCore.Qt.AlignLeft ) comboLayout.addWidget( self.comboMenu ) # Dynamic layout. This is where the different widgets are loaded by the combo menu. self.dynamicLayout = QtGui.QVBoxLayout() self.dynamicLayout.setAlignment( QtCore.Qt.AlignTop ) self.updateGui() # Main vertical layout. This is the parent of everything in the GUI. self.mainColumn = QtGui.QVBoxLayout() self.mainColumn.addLayout( comboLayout ) self.mainColumn.addLayout( self.dynamicLayout ) self.setLayout( self.mainColumn ) |
Most of this is straight forward. The two items of note are self.comboMenu and self.dynamicLayout. The combo menu triggers the updateGui() method. This method is what sets the widgets for self.dynamicLayout.
Let’s break down updateGui().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def updateGui( self ): #Updates the widget in the main layout based on the combo box selection. currentComboIndex = self.comboMenu.currentIndex() if self.dynamicLayout.count() is not 0: clearLayout( self.dynamicLayout ) buildClass = {0:WidgetOne, 1:WidgetTwo} widgetClass = buildClass[ currentComboIndex ] self.dynamicLayout.addWidget( widgetClass() ) if currentComboIndex == 0: # Set the script job number so we can delete it outside of the # component GUI. The script job can hang around if the main GUI # is closed using the X while the component section is active. widgetWithScriptJob = self.dynamicLayout.itemAt( 0 ).widget() self.SCRIPT_JOB_NUMBER = widgetWithScriptJob.SCRIPT_JOB_NUMBER |
The first thing we do is get the active item in self.comboMenu.
1 |
currentComboIndex = self.comboMenu.currentIndex() |
Next we want to clear out self.dynamicLayout. We do this with the method clearLayout() (more on this later).
1 |
if self.dynamicLayout.count() is not 0: clearLayout( self.dynamicLayout ) |
Here I build a dictionary. The dicts keys correspond to the indexes of the combo menu, while the values are custom QWidget classes. I then use currentComboIndex to get the correct widget class and then add it to self.dynamicLayout.
1 2 3 4 |
buildClass = {0:WidgetOne, 1:WidgetTwo} widgetClass = buildClass[ currentComboIndex ] self.dynamicLayout.addWidget( widgetClass() ) |
The QWidget class at index 0 of the combo menu is the only one with a ScriptJob. If that is the active widget then we grab it’s ScriptJob number and assign it to self.SCRIPT_JOB_NUMBER in MainWindow().
1 2 3 4 5 6 |
if currentComboIndex == 0: # Set the script job number so we can delete it outside of the # component GUI. The script job can hang around if the main GUI # is closed using the X while the component section is active. widgetWithScriptJob = self.dynamicLayout.itemAt( 0 ).widget() self.SCRIPT_JOB_NUMBER = widgetWithScriptJob.SCRIPT_JOB_NUMBER |
We’re almost done with MainWindow(). All that’s left is to add the method closeEvent(), which I cover in the previous example. Basically this guy is needed to kill the ScriptJob when the user closes the window. Otherwise it’ll hang around till Maya is shutdown and to many of these things active can cause problems.
1 2 3 4 |
def closeEvent( self, event ): # Kill the ScriptJob prior to closing the dialog. cmds.scriptJob( kill=self.SCRIPT_JOB_NUMBER, force=True ) super( MainWindow, self ).closeEvent( event ) |
Before we put it all together I need to go back and address clearLayout(). This is used to remove all the items from a PySide layout. Without this we would end up with an ever growing window. Each change of the combo menu would add a new QWidget instead of replacing the existing one. So clearLayout() loops through all the items in a layout and deletes them.
Okay, so that’s the entire MainWindow class. And here it is put together.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
import maya.OpenMayaUI as omui from PySide import QtCore from PySide import QtGui from shiboken import wrapInstance import maya.cmds as cmds def maya_main_window(): ''' Return the Maya main window as a Python object ''' main_window_ptr = omui.MQtUtil.mainWindow() return wrapInstance( long( main_window_ptr ), QtGui.QWidget ) def clearLayout( layout ): ''' Clears out a PySide layout. ''' if layout is not None: while layout.count(): item = layout.takeAt( 0 ) widget = item.widget() if widget is not None: widget.deleteLater() else: clearLayout( item.layout() ) class MainWindow( QtGui.QDialog ): def __init__( self, parent=maya_main_window() ): super( MainWindow, self ).__init__( parent ) self.winName = 'My Main Window' self.userInput = None self.SCRIPT_JOB_NUMBER = None self.create() def create( self ): # Check to see if this UI is already open. If it is then delete it before # creating it anew. if cmds.window( self.winName, exists=True ): cmds.deleteUI( self.winName ) # Set the window object name and window title. self.setObjectName( self.winName ) self.setWindowTitle( self.winName ) # Mode combo menu. self.comboMenu = QtGui.QComboBox() self.comboItems = [ 'Widget One', 'Widget Two' ] self.comboMenu.insertItems( 0, self.comboItems ) self.comboMenu.setFixedWidth( 100 ) self.comboMenu.activated.connect( self.updateGui ) # Combo box layout comboLayout = QtGui.QHBoxLayout() comboLayout.setContentsMargins( 2, 2, 2, 2 ) comboLayout.setAlignment( QtCore.Qt.AlignLeft ) comboLayout.addWidget( self.comboMenu ) # Dynamic layout. This is where the different widgets are loaded by the combo menu. self.dynamicLayout = QtGui.QVBoxLayout() self.dynamicLayout.setAlignment( QtCore.Qt.AlignTop ) self.updateGui() # Main vertical layout. This is the parent of everything in the GUI. self.mainColumn = QtGui.QVBoxLayout() self.mainColumn.addLayout( comboLayout ) self.mainColumn.addLayout( self.dynamicLayout ) self.setLayout( self.mainColumn ) def updateGui( self ): ''' Updates the widget in the main layout based on the combo box selection. ''' currentComboIndex = self.comboMenu.currentIndex() if self.dynamicLayout.count() is not 0: clearLayout( self.dynamicLayout ) buildClass = {0:WidgetOne, 1:WidgetTwo} widgetClass = buildClass[ currentComboIndex ] self.dynamicLayout.addWidget( widgetClass() ) if currentComboIndex == 0: # Set the script job number so we can delete it outside of the # component GUI. The script job can hang around if the main GUI # is closed using the X while the component section is active. widgetWithScriptJob = self.dynamicLayout.itemAt( 0 ).widget() self.SCRIPT_JOB_NUMBER = widgetWithScriptJob.SCRIPT_JOB_NUMBER def closeEvent( self, event ): # Kill the ScriptJob prior to closing the dialog. cmds.scriptJob( kill=self.SCRIPT_JOB_NUMBER, force=True ) super( MainWindow, self ).closeEvent( event ) |
Now we need to make WidgetOne and WidgetTwo. These are the custom QWidget classes used in updateGui().
To keep things simple I’m using the same ScriptJob from my previous example – the user selects an item and a label on the window updates to show the item’s name.
We’ll start with WidgetOne. This time we are extending QWidget instead of QDialog since these guys will be widgets added to a layout and not windows in their own right.
1 2 3 4 5 |
class WidgetOne( QtGui.QWidget ): def __init__( self ): super( WidgetOne, self ).__init__() self.create() |
For the layout we have two QLabels. The first, self.widgetLabel, displays the name of the active widget. This way you can actually see the widgets changing when the combo menu changes. The second, self.selectedLabel, is the one changed by the ScriptJob.
The ScriptJob is then created, but we need to handle this in a special way. A ScriptJob can have a parent assigned at creation. This allows the job to be automatically killed if the parent is destroyed. We want this to happen because of the dynamic nature of the widgets and because we don’t need the job when it’s widget is inactive. The catch her is that a ScriptJob can’t have a PySide object for a parent. So in order to make this work we create a Maya UI object, assign it as the parent for the ScriptJob, and then convert the Maya UI object into a PySide object. Now we can take this newly created PySide object and add it to the widget’s layout.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def create( self ): layout = QtGui.QVBoxLayout( self ) self.widgetLabel = QtGui.QLabel() self.widgetLabel.setText( 'Widget One' ) self.widgetLabel.setStyleSheet( 'font:{0}; font-size:{1}px; color:{2}; background-color:#{3}'.format( 'bold', 14, 'white', '2B2B30' ) ) layout.addWidget( self.widgetLabel ) self.selectedLabel = QtGui.QLabel() self.selectedLabel.setText( 'Nothing Selected' ) layout.addWidget( self.selectedLabel ) # In order to delete the ScriptJob when this QWidget is destroyed, we need to # parent the ScriptJob to a Maya UI item. In this case I'm using a text object # with visibility turned off. scriptJobHolder = cmds.text( visible=False ) self.SCRIPT_JOB_NUMBER = cmds.scriptJob( event=[ 'SelectionChanged', self.onSelectionChange ], protected=True, parent=scriptJobHolder ) scriptJobHolderQT = mayaToQtObject( scriptJobHolder ) layout.addWidget( scriptJobHolderQT ) |
The last thing to add to WidgetOne is the class method that the ScriptJob triggers.
1 2 3 4 5 6 7 8 9 10 11 |
def onSelectionChange( self ): # Change the text for the label. selList = cmds.ls( selection=1, long=True ) if len( selList ) == 1: shortName = selList[0].split( '|' ) selName = shortName[ len(shortName)-1 ] self.selectedLabel.setText( selName ) elif len( selList ) > 1: self.selectedLabel.setText( 'Select One Object' ) else: self.selectedLabel.setText( 'Nothing Selected' ) |
Here is the full version of WidgetOne.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class WidgetOne( QtGui.QWidget ): def __init__( self ): super( WidgetOne, self ).__init__() self.create() def create( self ): layout = QtGui.QVBoxLayout( self ) self.widgetLabel = QtGui.QLabel() self.widgetLabel.setText( 'Widget One' ) self.widgetLabel.setStyleSheet( 'font:{0}; font-size:{1}px; color:{2}; background-color:#{3}'.format( 'bold', 14, 'white', '2B2B30' ) ) layout.addWidget( self.widgetLabel ) self.selectedLabel = QtGui.QLabel() self.selectedLabel.setText( 'Nothing Selected' ) layout.addWidget( self.selectedLabel ) # In order to delete the ScriptJob when this QWidget is destroyed, we need to # parent the ScriptJob to a Maya UI item. In this case I'm using a text object # with visibility turned off. scriptJobHolder = cmds.text( visible=False ) self.SCRIPT_JOB_NUMBER = cmds.scriptJob( event=[ 'SelectionChanged', self.onSelectionChange ], protected=True, parent=scriptJobHolder ) scriptJobHolderQT = mayaToQtObject( scriptJobHolder ) layout.addWidget( scriptJobHolderQT ) def onSelectionChange( self ): # Change the text for the label. selList = cmds.ls( selection=1, long=True ) if len( selList ) == 1: shortName = selList[0].split( '|' ) selName = shortName[ len(shortName)-1 ] self.selectedLabel.setText( selName ) elif len( selList ) > 1: self.selectedLabel.setText( 'Select One Object' ) else: self.selectedLabel.setText( 'Nothing Selected' ) |
WidgetTwo is just a deeply watered down version of WidgetOne. All it has is a single QLabel with it’s name.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class WidgetTwo( QtGui.QWidget ): def __init__( self ): super( WidgetTwo, self ).__init__() self.create() def create( self ): layout = QtGui.QVBoxLayout( self ) self.widgetLabel = QtGui.QLabel() self.widgetLabel.setText( 'Widget Two' ) self.widgetLabel.setStyleSheet( 'font:{0}; font-size:{1}px; color:{2}; background-color:#{3}'.format( 'bold', 14, 'white', '2B2B30' ) ) layout.addWidget( self.widgetLabel ) |
And finally here is everything put together.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
import maya.OpenMayaUI as omui from PySide import QtCore from PySide import QtGui from shiboken import wrapInstance import maya.cmds as cmds def maya_main_window(): ''' Return the Maya main window as a Python object ''' main_window_ptr = omui.MQtUtil.mainWindow() return wrapInstance( long( main_window_ptr ), QtGui.QWidget ) def mayaToQtObject( inMayaUI ): ptr = omui.MQtUtil.findControl( inMayaUI ) if ptr is None: ptr = omui.MQtUtil.findLayout( inMayaUI ) if ptr is None: ptr= omui.MQtUtil.findMenuItem( inMayaUI ) if ptr is not None: return wrapInstance( long( ptr ), QtGui.QWidget ) def clearLayout( layout ): ''' Clears out a PySide layout. ''' if layout is not None: while layout.count(): item = layout.takeAt( 0 ) widget = item.widget() if widget is not None: widget.deleteLater() else: clearLayout( item.layout() ) class MainWindow( QtGui.QDialog ): def __init__( self, parent=maya_main_window() ): super( MainWindow, self ).__init__( parent ) self.winName = 'My Main Window' self.userInput = None self.SCRIPT_JOB_NUMBER = None self.create() def create( self ): # Check to see if this UI is already open. If it is then delete it before # creating it anew. if cmds.window( self.winName, exists=True ): cmds.deleteUI( self.winName ) # Set the window object name and window title. self.setObjectName( self.winName ) self.setWindowTitle( self.winName ) # Mode combo menu. self.comboMenu = QtGui.QComboBox() self.comboItems = [ 'Widget One', 'Widget Two' ] self.comboMenu.insertItems( 0, self.comboItems ) self.comboMenu.setFixedWidth( 100 ) self.comboMenu.activated.connect( self.updateGui ) # Combo box layout comboLayout = QtGui.QHBoxLayout() comboLayout.setContentsMargins( 2, 2, 2, 2 ) comboLayout.setAlignment( QtCore.Qt.AlignLeft ) comboLayout.addWidget( self.comboMenu ) # Dynamic layout. This is where the different widgets are loaded by the combo menu. self.dynamicLayout = QtGui.QVBoxLayout() self.dynamicLayout.setAlignment( QtCore.Qt.AlignTop ) self.updateGui() # Main vertical layout. This is the parent of everything in the GUI. self.mainColumn = QtGui.QVBoxLayout() self.mainColumn.addLayout( comboLayout ) self.mainColumn.addLayout( self.dynamicLayout ) self.setLayout( self.mainColumn ) def updateGui( self ): ''' Updates the widget in the main layout based on the combo box selection. ''' currentComboIndex = self.comboMenu.currentIndex() if self.dynamicLayout.count() is not 0: clearLayout( self.dynamicLayout ) buildClass = {0:WidgetOne, 1:WidgetTwo} widgetClass = buildClass[ currentComboIndex ] self.dynamicLayout.addWidget( widgetClass() ) if currentComboIndex == 0: # Set the script job number so we can delete it outside of the # component GUI. The script job can hang around if the main GUI # is closed using the X while the component section is active. widgetWithScriptJob = self.dynamicLayout.itemAt( 0 ).widget() self.SCRIPT_JOB_NUMBER = widgetWithScriptJob.SCRIPT_JOB_NUMBER def closeEvent( self, event ): # Kill the ScriptJob prior to closing the dialog. cmds.scriptJob( kill=self.SCRIPT_JOB_NUMBER, force=True ) super( MainWindow, self ).closeEvent( event ) class WidgetOne( QtGui.QWidget ): def __init__( self ): super( WidgetOne, self ).__init__() self.create() def create( self ): layout = QtGui.QVBoxLayout( self ) self.widgetLabel = QtGui.QLabel() self.widgetLabel.setText( 'Widget One' ) self.widgetLabel.setStyleSheet( 'font:{0}; font-size:{1}px; color:{2}; background-color:#{3}'.format( 'bold', 14, 'white', '2B2B30' ) ) layout.addWidget( self.widgetLabel ) self.selectedLabel = QtGui.QLabel() self.selectedLabel.setText( 'Nothing Selected' ) layout.addWidget( self.selectedLabel ) # In order to delete the ScriptJob when this QWidget is destroyed, we need to # parent the ScriptJob to a Maya UI item. In this case I'm using a text object # with visibility turned off. scriptJobHolder = cmds.text( visible=False ) self.SCRIPT_JOB_NUMBER = cmds.scriptJob( event=[ 'SelectionChanged', self.onSelectionChange ], protected=True, parent=scriptJobHolder ) scriptJobHolderQT = mayaToQtObject( scriptJobHolder ) layout.addWidget( scriptJobHolderQT ) def onSelectionChange( self ): # Change the text for the label. selList = cmds.ls( selection=1, long=True ) if len( selList ) == 1: shortName = selList[0].split( '|' ) selName = shortName[ len(shortName)-1 ] self.selectedLabel.setText( selName ) elif len( selList ) > 1: self.selectedLabel.setText( 'Select One Object' ) else: self.selectedLabel.setText( 'Nothing Selected' ) class WidgetTwo( QtGui.QWidget ): def __init__( self ): super( WidgetTwo, self ).__init__() self.create() def create( self ): layout = QtGui.QVBoxLayout( self ) self.widgetLabel = QtGui.QLabel() self.widgetLabel.setText( 'Widget Two' ) self.widgetLabel.setStyleSheet( 'font:{0}; font-size:{1}px; color:{2}; background-color:#{3}'.format( 'bold', 14, 'white', '2B2B30' ) ) layout.addWidget( self.widgetLabel ) ''' RUN THE WINDOW ''' testUI = MainWindow() testUI.show() |