I’ll be honest. Building an AETemplate using MEL is an activity that I enjoy about as much as going to the DMV. It’s very tedious and can take forever to get anything done. The more complexity you add, the more daunting the logistics become.
At this point, you’re probably thinking “Oh, he’s about to talk about how much better it must be to use PyQt!”
No, he isn’t.
While making AETemplates with PyQt has substantial advantages, which is why I do it, the overall process can be equally painful (just in different ways).
So why use PyQt? I was going to make a list but it really comes down to one thing: you get absolute control over all aspects of the interface including:
- Layout (esp, the ability to stretch multiple widgets in a row or column)
- Widget appearance (length, height, spacing)
- The ability to create all new widgets to better visualize certain attributes. For instance, if your node had an attribute that required a user to enter a time in hours and minutes (for some odd reason), you could create a clock widget with hour and minute hands.
Look at that…made a list anyway.
The Disclaimer
Unfortunately, with great power comes a great repercussion:
See everything in that context menu? When you created that control with editorTemplate in MEL, you got all that for free. Want the background to turn red when you set a key? editorTemplate did that for you too. With PyQt, on the other hand, you’re going to need to build in that functionality yourself!
But really
Actually, the situation isn’t as dire as all that. Yes, there can be a high initial cost for building an AETemplate in PyQt but, if you’re smart and abstract as much as possible, you’ll end up with a library of standard base classes, widgets, and menus that you can go back to whenever a new AETemplate is needed.
However, it’s a lot of material to cover so we will save all the context menu related stuff for a later tutorial.
Tutorial outline
We will be working with the same uselessTransform node as in the previous post. The source code for that plus the MEL and Python code discussed in this tutorial can be downloaded here: uselessTransform_PyQt_AETemplate.
In my opinion, the conceptual side of making AETemplates with PyQt is not too complex. However, the process is also not as linear as the material we discussed in previous posts. We can’t simply start building and hope for the best…rather, some planning is required. With that in mind, this tutorial can be broken into two parts:
- The boilerplate code for building and updating an AETemplate with PyQt. This code is generally consistent across all AETemplates built in this manner. I usually just copy/paste from older projects.
- The attribute and widget specific code. This refers to any UI elements that we must develop for displaying and editing attributes of the specific node type this AETemplate is being written for.
Unlike earlier tutorials, I am mostly unable to show snapshots of the intermediate stages. Rather, it’s one of those all or nothing cases where everything comes together at the end.
Boilerplate
Just as before, we must start with the AEuselessTransformTemplate.mel file. The big difference is that we will not be using it for any significant work. Rather, we will hook into our Python functions from there. To that end, we must also create an AEuselessTransformTemplate.py file.
Note: You can actually call the .py file whatever you want…I just name it that way for consistency and clarity.
MEL
Let’s implement the main AEuselessTransformTemplate procedure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
global proc AEuselessTransformTemplate( string $nodeName )
{
editorTemplate -beginScrollLayout;
//Call custom scripts to manage the PyQt UI elements
editorTemplate -callCustom "AEuselessTransform_buildQT" "AEuselessTransform_updateQT" "";
//Since this is derived from a transform node, we should add the template
//from a transform node
AEtransformMain $nodeName;
AEtransformNoScroll $nodeName;
//This is boilerplate stuff that should generally be included at the end.
editorTemplate -addExtraControls;
editorTemplate -endScrollLayout;
} |
Notice:
- The significant line here is:
1editorTemplate -callCustom "AEuselessTransform_buildQT" "AEuselessTransform_updateQT" ""; - The final argument of that call is an empty string. We don’t actually want to pass any attributes but we still need that argument so that we can get the name of the node.
- We still include the calls to AEtransformMain and AEtransformNoScroll, which will ensure that we still have the standard transform node template after our PyQt.
Now, let’s take a look at those buildQT and updateQT procedures:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
global proc AEuselessTransform_buildQT( string $nodeName )
{
//make sure the Python QT module is loaded
python("import AEuselessTransformTemplate");
// When the $nodeName is passed in, there will be a trailing dot (.)...rm it
$nodeName = python("'" + $nodeName + "'.rstrip('.')");
// Get the current layout
string $par = `setParent -q`;
// Pass the layout and node name to the QT build/create cmd
python("AEuselessTransformTemplate.buildQT('" + $par + "', '" + $nodeName + "')");
} |
Notice:
- The procedure accepts a single argument, which is the name of the node. That name will have a trailing dot (.), which we must remove before proceeding.
- We import the python AETemplate module here.
- We get the name of the parent layout and pass it to the python buildQT method, along with the name of the node
1 2 3 4 5 6 7 8 9 10 11 12 13 |
global proc AEuselessTransform_updateQT( string $nodeName )
{
// When the $nodeName is passed in, there will be a trailing dot (.)...rm it
$nodeName = python("'" + $nodeName + "'.rstrip('.')");
// Get the current layout
string $par = `setParent -q`;
// Pass the layout and node name to the QT update cmd
python("AEuselessTransformTemplate.updateQT('" + $par + "', '" + $nodeName + "')");
} |
The only differences are:
- We do not import the python module.
- We call updateQT instead of buildQT.
Python
We’ll need to import the following modules:
1 2 3 4 5 |
import maya.cmds as cmds
from PyQt4 import QtCore, QtGui
import maya.OpenMayaUI as mui
import sip |
The Python boilerplate requires three functions and one class:
- getLayout (function) – Convert a layout name (as provided by MEL’s setParent -q command) to a QtGui.QLayout object that we can insert our PyQt work into.
- buildQT (function) – Install the PyQt AETemplate into Maya’s AETemplate layout. This is responsible for initializing all the widgets.
- updateQT (function) – Update the AETemplate with new node information.
- AEuselessTransformTemplate (class) – The primary widget that will contain all the AETemplate widgets and data.
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 |
################################################################################
#Main AETemplate Widget
################################################################################
class AEuselessTransformTemplate(QtGui.QWidget):
'''
The main class that holds all the controls for the uselessTransform node
'''
def __init__(self, node, parent=None):
'''
Initialize
'''
super(AEuselessTransformTemplate, self).__init__(parent)
self.node = node
def setNode(self, node):
'''
Set the current node
'''
self.node = node
################################################################################
#Initialize/Update methods:
# These are the methods that get called to Initialize & install the QT GUI
# and to update/repoint it to a different node
################################################################################
def getLayout(lay):
'''
Get the layout object
@type lay: str
@param lay: The (full) name of the layout that we need to get
@rtype: QtGui.QLayout (or child)
@returns: The QLayout object
'''
ptr = mui.MQtUtil.findLayout(lay)
layObj = sip.wrapinstance(long(ptr), QtCore.QObject).layout()
return layObj
def buildQT(lay, node):
'''
Build/Initialize/Install the QT GUI into the layout.
@type lay: str
@param lay: Name of the Maya layout to add the QT GUI to
@type node: str
@param node: Name of the node to (initially) connect to the QT GUI
'''
#get the layout object
mLayout = getLayout(lay)
#create the GUI/widget
widg = AEuselessTransformTemplate(node)
#add the widget to the layout
mLayout.insertWidget(0, widg)
def updateQT(lay, node):
'''
Update the QT GUI to point to a different node
@type lay: str
@param lay: Name of the Maya layout to where the QT GUI lives
@type node: str
@param node: Name of the new node to connect to the QT GUI
'''
#get the layout
mLayout = getLayout(lay)
#find the widget
for c in range(mLayout.count()):
widg = mLayout.itemAt(c).widget()
if isinstance(widg, AEuselessTransformTemplate):
#found the widget, update the node it's pointing to
widg.setNode(node)
break |
If we run the template right now, we’ll get:
Have we actually accomplished anything? This looks the same as having no AETemplate at all. Actually, we’ve made it possible to add any PyQt widgets we want above that Transform Attributes section. Let’s add a simple QLabel to our AETemplate to validate our efforts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class AEuselessTransformTemplate(QtGui.QWidget):
'''
The main class that holds all the controls for the uselessTransform node
'''
def __init__(self, node, parent=None):
'''
Initialize
'''
super(AEuselessTransformTemplate, self).__init__(parent)
self.node = node
#: Temporary label
self.tmpLabel = QtGui.QLabel("This is a PyQt AETemplate!", parent=self)
########################################################################
#Layout
########################################################################
self.layout = QtGui.QBoxLayout(QtGui.QBoxLayout.TopToBottom, self)
self.layout.setContentsMargins(5,3,11,3)
self.layout.setSpacing(5)
self.layout.addWidget(self.tmpLabel) |
Specialty Widgets
Now that we’ve got the standard stuff out of the way, it’s time to discuss the specialized widgets that will display the info for particular attributes. Each widget needs to be able to do the following:
- Display the current value of an attribute.
- Allow the user to set a new value and have it be applied to the attribute.
- Update itself when the attribute gets changed externally (e.g., via the setAttr command).
- Change the node that it’s connected to.
Base
Although we have three different types of attributes to create widgets for, a clever developer will notice that most of the functionality is consistent across all of them. Therefore, we can create a base class to derive our widgets from.
All attribute widgets will need to:
- Store the current node and attribute names.
- Have a text label with the attribute’s display name.
- Have a basic left to right layout.
- Maintain a scriptJob that calls an appropriate update method when the attribute is modified by some external action.
- Track whether the UI is in the process of updating. This is necessary so that the scriptJob doesn’t trigger a UI update when the UI, itself, is the cause of that update.
With that in mind, let’s initialize our attribute base class:
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 |
################################################################################
#Base Attribute Widget
################################################################################
class BaseAttrWidget(QtGui.QWidget):
'''
This is the base attribute widget from which all other attribute widgets
will inherit. Sets up all the relevant methods + common widgets and initial
layout.
'''
def __init__(self, node, attr, label='', parent=None):
'''
Initialize
@type node: str
@param node: The name of the node that this widget should start with.
@type attr: str
@param attr: The name of the attribute this widget is responsible for.
@type label: str
@param label: The text that should be displayed in the descriptive label.
'''
super(BaseAttrWidget, self).__init__(parent)
self.node = node #: Store the node name
self.attr = attr #: Store the attribute name
#: This will store information about the scriptJob that we will create
#: so that we can update this widget whenever its attribute is updated
#: separately.
self.sj = None
#: Use this variable to track whether the gui is currently being updated
#: or not.
self.updatingGUI = False
########################################################################
#Widgets
########################################################################
#: The QLabel widget with the name of the attribute
self.label = QtGui.QLabel(label if label else attr, parent=self)
########################################################################
#Layout
########################################################################
self.layout = QtGui.QBoxLayout(QtGui.QBoxLayout.LeftToRight, self)
self.layout.setContentsMargins(0,0,0,0)
self.layout.setSpacing(5)
self.layout.addWidget(self.label) |
Next, we need to define two virtual methods. One for updating the UI when the attribute has been modified and one for updating the attribute when the UI has been modified.
1 2 3 4 5 6 |
def updateGUI(self):
'''
VIRTUAL method. Called whenever the widget needs to update its displayed
value to match the value of the attribute on the node.
'''
raise NotImplementedError |
1 2 3 4 5 6 7 |
def updateAttr(self):
'''
VIRTUAL method. Should be called whenever the user makes a change to this
widget via the UI. This method is then responsible for applying the same
change to the actual attribute on the node.
'''
raise NotImplementedError |
We actually want the updatingGUI variable to be True while the updateGUI method is running. However, as we don’t want to implement that logic in each child class, we’ll wrap updateGUI in another method that will be responsible for calling it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def callUpdateGUI(self):
'''
Calls the updateGUI method but makes sure to set the updatingGUI variable
while doing so.
This is necessary so that we do not get caught in a loop where updating
the UI will trigger a signal that updates the attr on the node, which
in turn triggers the scriptJob to run updateGUI again.
'''
self.updatingGUI = True
self.updateGUI()
self.updatingGUI = False |
We also don’t want updateAttr to run if updateGUI is running so we’ll add another wrapper for that:
1 2 3 4 5 6 |
def callUpdateAttr(self):
'''
Calls the updateAttr method but only if not currently updatingGUI
'''
if not self.updatingGUI:
self.updateAttr() |
setNode and scriptJob
There’s only one more thing we need to implement for our base class: the setNode functionality that will:
- Update self.node.
- Update the UI.
- Initialize or update a scriptJob that will call the callUpdateGUI method whenever the attribute is changed externally.
- Kill any pre-existing scriptJobs.
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 |
def setNode(self, node):
'''
This widget should now represent the same attr on a different node.
'''
oldNode = self.node
self.node = node
self.callUpdateGUI()
if not self.sj or not cmds.scriptJob(exists=self.sj) or not oldNode == self.node:
#script job
ct = 0
while self.sj:
#Kill the old script job.
try:
if cmds.scriptJob(exists=self.sj):
cmds.scriptJob(kill=self.sj, force=True)
self.sj = None
except RuntimeError:
#Could not kill the old script job for some reason.
#This happens, albeit very rarely, when that scriptJob is
#being executed at the same time we try to kill it. Pause
#for a second and then retry.
ct += 1
if ct < 10:
cmds.warning("Got RuntimeError trying to kill scriptjob...trying again")
time.sleep(1)
else:
#We've failed to kill the scriptJob 10 consecutive times.
#Time to give up and move on.
cmds.warning("Killing scriptjob is taking too long...skipping")
break
#Set the new scriptJob to call the callUpdateGUI method everytime the
#node.attr is changed.
self.sj = cmds.scriptJob(ac=['%s.%s' % (self.node, self.attr), self.callUpdateGUI], killWithScene=1) |
Notice that we only make changes to the scriptJob if:
- self.sj is empty.
- The current scriptJob ID assigned to self.sj is no longer valid.
- The new node being set is different from the previously assigned node. As explained in the last post, the Update functionality of an AETemplate can be called for various reasons including a simple refresh of the UI. Therefore, it is inefficient to update the scriptJob unless the node has actually changed.
The Numerical Attribute (numAttr)
Now that we’ve completed our Base class, let’s start deriving from it to define the widget that will track the numAttr on our uselessTransform.
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 NumWidget(BaseAttrWidget):
'''
This widget can be used with numerical attributes.
'''
def __init__(self, node, attr, label='', parent=None):
'''
Initialize
'''
super(NumWidget, self).__init__(node, attr, label, parent)
########################################################################
#Widgets
########################################################################
#: The QLineEdit storing the value of the attribute
self.valLE = QtGui.QLineEdit(parent=self)
#Set a validator for the valLE that will ensure that the user may only
#enter numerical values
self.valLE.setValidator(QtGui.QDoubleValidator(self.valLE))
########################################################################
#Layout
########################################################################
self.layout.addWidget(self.valLE)
self.layout.addStretch(1)
########################################################################
#Connections
########################################################################
#We need to call the callUpdateAttr method whenever the user modifies the
#value in valLE
self.connect(self.valLE, QtCore.SIGNAL("editingFinished()"), self.callUpdateAttr)
########################################################################
#Set the initial node
########################################################################
self.setNode(node) |
Notice:
- Our base class took care of most of the work. All we had to do was create the QLineEdit widget responsible for actually displaying information for a numerical attribute and add it to the layout.
- We connected the editingFinished() signal from self.valLE to callUpdateAttr.
- Our final step is to call setNode. This is necessary to initialize the scriptJob. However, as it also calls updateGUI, we had to have all our UI elements in place first.
All we need now is to re-implement updateGUI and updateAttr:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def updateGUI(self):
'''
Implement this virtual method to update the value in valLE based on the
current node.attr
'''
self.valLE.setText('%.03f' % round(cmds.getAttr("%s.%s" % (self.node, self.attr)), 3))
def updateAttr(self):
'''
Implement this virtual method to update the actual node.attr value to
reflect what is currently in the UI.
'''
cmds.setAttr("%s.%s" % (self.node, self.attr), float(self.valLE.text())) |
Easy peasy!
The String Attribute (stringAttr)
This widget is pretty similar to the previous one. Just minor tweaks:
- Modified the update methods to accommodate getting and setting a string instead of a number.
- No QDoubleValidator assigned to the QLineEdit.
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 |
class StrWidget(BaseAttrWidget):
'''
This widget can be used with string attributes.
'''
def __init__(self, node, attr, label='', parent=None):
'''
Initialize
'''
super(StrWidget, self).__init__(node, attr, label, parent)
########################################################################
#Widgets
########################################################################
#: The QLineEdit storing the value of the attribute
self.valLE = QtGui.QLineEdit(parent=self)
########################################################################
#Layout
########################################################################
self.layout.addWidget(self.valLE, 1)
########################################################################
#Connections
########################################################################
#We need to call the callUpdateAttr method whenever the user modifies the
#value in valLE
self.connect(self.valLE, QtCore.SIGNAL("editingFinished()"), self.callUpdateAttr)
########################################################################
#Set the initial node
########################################################################
self.setNode(node)
def updateGUI(self):
'''
Implement this virtual method to update the value in valLE based on the
current node.attr
'''
self.valLE.setText(str(cmds.getAttr("%s.%s" % (self.node, self.attr))))
def updateAttr(self):
'''
Implement this virtual method to update the actual node.attr value to
reflect what is currently in the UI.
'''
cmds.setAttr("%s.%s" % (self.node, self.attr), str(self.valLE.text()), type='string') |
The Enum Attribute (enumAttr)
Last, but not least, we must implement a widget to support choosing a value from an enumerated list of options. Again, the only changes from the previous two widgets is the stuff specific to an enumerated list:
- Using a extra argument in the __init___ to get the list of enumerated values to display.
- Using a QComboBox instead of a QLineEdit.
- Connecting a different signal to callUpdateAttr.
- Minor changes to the implementations of the update methods.
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 |
class EnumWidget(BaseAttrWidget):
'''
This widget can be used with enumerated attributes.
'''
def __init__(self, node, attr, label='', enums=None, parent=None):
'''
Initialize
@type enum: list
@param enum: An ordered list of the values to be show in the enumeration list
'''
super(EnumWidget, self).__init__(node, attr, label, parent)
#Make sure the provided enums are not None
enums = enums if enums else []
########################################################################
#Widgets
########################################################################
#: The QComboBox storing the enums
self.valCB = QtGui.QComboBox(parent=self)
#Populate valCB with the enums
self.valCB.addItems(enums)
########################################################################
#Layout
########################################################################
self.layout.addWidget(self.valCB)
self.layout.addStretch(1)
########################################################################
#Connections
########################################################################
#We need to call the callUpdateAttr method whenever the user modifies the
#value in valCB
self.connect(self.valCB, QtCore.SIGNAL("currentIndexChanged(int)"), self.callUpdateAttr)
########################################################################
#Set the initial node
########################################################################
self.setNode(node)
def updateGUI(self):
'''
Implement this virtual method to update the value in valCB based on the
current node.attr
'''
self.valCB.setCurrentIndex(cmds.getAttr('%s.%s' % (self.node, self.attr)))
def updateAttr(self):
'''
Implement this virtual method to update the actual node.attr value to
reflect what is currently in the UI.
'''
cmds.setAttr("%s.%s" % (self.node, self.attr), self.valCB.currentIndex()) |
Putting It Together!
We’re almost done! All that remains is to add our new widgets into the main AEuselessTransformTemplate class. We do this in the usual way:
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 |
class AEuselessTransformTemplate(QtGui.QWidget):
'''
The main class that holds all the controls for the uselessTransform node
'''
def __init__(self, node, parent=None):
''' Initialize '''
super(AEuselessTransformTemplate, self).__init__(parent)
self.node = node
########################################################################
#Widgets
########################################################################
#: Store the widget that will display/update the stringAttr attribute
self.strAttr = StrWidget(node, 'stringAttr', label='TXT:', parent=self)
#: Store the widget that will display/update the enumAttr attribute
self.enumAttr = EnumWidget(node,
'enumAttr',
label='Enumerated:',
enums=['zero', 'one', 'two', 'three', 'four',
'five', 'six', 'seven', 'eight', 'nine'],
parent=self)
#: Store the widget that will display/update the numAttr attribute
self.numAttr = NumWidget(node, 'numAttr', label='NUM:', parent=self)
########################################################################
#Layout
########################################################################
self.layout = QtGui.QBoxLayout(QtGui.QBoxLayout.TopToBottom, self)
self.layout.setContentsMargins(5,3,11,3)
self.layout.setSpacing(5)
self.layout.addWidget(self.strAttr)
self.layout.addWidget(self.enumAttr)
self.layout.addWidget(self.numAttr) |
setNode
One more thing. We must make sure that the setNode method of each widget in the AETemplate gets called during update. We do this by simply adding those widgets to the setNode implementation within AEuselessTransformTemplate:
1 2 3 4 5 6 7 8 9 |
def setNode(self, node):
'''
Set the current node
'''
self.node = node
self.strAttr.setNode(node)
self.enumAttr.setNode(node)
self.numAttr.setNode(node) |
Result!

And that’s how you build an AETemplate with PyQt!
Thanks for the information. But I know an easier way. Need to use pymel for templates. Accessing and updating attributes layoutu more convenient and easier. Try to do so. I can give a sample.
Привет Paul!
Pymel is a great way of creating AETemplates and a major step up from using MEL or Python MEL. However, it’s still wrapping MEL commands, which means you’re still limited to the GUI building blocks that Maya gives you.
My point in providing this PyQt AETemplate tutorial was to show that you don’t have to be. The full range of Qt’s capabilities can be available to you, if you should need them. You can paint your own widgets, re-implement events, etc.
The key is understanding that it’s just another tool in the toolbox and one that should be used sparingly and only when the desired effect can’t be accomplished with MEL (or pymel).
Just open my example and see what I mean. I use PyQt too (PySide. Example for Maya 2014 only). And if the code is not contains MEL and use python class, it is much more convenient and more understandable.
https://www.dropbox.com/s/an5gspiqae0kc5i/sineNode.py
I tested it with a OGLWidget and QGraphicsView, It works.
Ah, I see what you mean now. Similar logic but no need for MEL. Very cool! Definitely cleaner.
I’ll remember this for the next AETemplate (or tutorial) I write. The only mitigating factor is that you need to have pymel imported first, which I don’t think my facility does right now (would have to check).
Anyway, you should definitely put up this info as a tutorial on your site if you haven’t already!
Yes, certainly will do.
By the way, I did not find in MEL template flag “changeCommand”. Its very useful feature.
Oh sory, new link
https://dl.dropboxusercontent.com/u/22108850/downloads/sineNode.py
it’s weird. Both links are working now
you can delete these posts