Dynamic UI in Maya using Python
In a recent job, I needed to create a dynamic user interface in Maya. I don't really like MEL, so I decided to go with Python. This was the first time I really used Python for anything.
One of the reasons I don't like MEL, is it kind of forces you to use global variables (using local ones becomes a pain if not impossible at some point.)
So, on I went and wrote something like this:
import maya.cmds as cmds
import maya.mel as mel
class dynamicUIElements:
"""A demo for creating dynamic UI elements in Maya using python."""
# On initialize, create the UI and show it.
def __init__(self):
"""Initialize."""
self.createWindow()
# I like to keep all the iportant UI elements in a dictionary.
UIElements = {}
def createWindow(self):
"""This function creates the window."""
# Create the window element
self.UIElements['window'] = cmds.window()
#Create a layout
self.UIElements['main_layout'] = cmds.columnLayout( adjustableColumn=True )
# Create the dynamic buttons
self.createButtons()
# Show the window.
cmds.showWindow( self.UIElements['window'] )
def createButtons(self):
"""Creates some buttons dynamically."""
for i in range(1,6):
self.UIElements['button'+str(i)] = cmds.button( label=('Button ' + str(i)), command=self.ehButtonPressed )
def ehButtonPressed(self, *args):
"""An event handler for the button pressed event."""
print args
# End of class
# Create an instance of the class
demo = dynamicUIElements()
As you can see creating the elements dynamically, is obviously not a problem. However, if you try this code, and click any of the buttons, you'll see that the there's no way to determine which one of the buttons raised the event. Now, this could have easily be solved if Maya would pass the sender as one of the arguments, however, I could not find a way to do this.
So I looked for a different solution, and this is what I came up with:
import maya.cmds as cmds
import maya.mel as mel
class dynamicUIElements:
"""A demo for creating dynamic UI elements in Maya using python."""
# On initialize, create the UI and show it.
def __init__(self):
"""Initialize."""
self.createWindow()
# I like to keep all the iportant UI elements in a dictionary.
UIElements = {}
# Add a variable to store the event handlers
UIEventHandlers = {
'ButtonEvents' : {},
}
def createWindow(self):
"""This function creates the window."""
# Create the window element
self.UIElements['window'] = cmds.window()
#Create a layout
self.UIElements['main_layout'] = cmds.columnLayout( adjustableColumn=True )
# Create the dynamic buttons
self.createButtons()
# Show the window.
cmds.showWindow( self.UIElements['window'] )
def createButton(self,i):
"""Creates a signle button."""
# defing a temporary function for the event handler of the specific button:
def func(*args): self.ehButtonPressed([i] + list(args))
# Save a reference of temporary function in a class variable, so it remain in memory as long as the class exists.
# Otherwise it will be garbage collected as soon as this function is done.
self.UIEventHandlers['ButtonEvents'][i] = func
self.UIElements['button'+str(i)] = cmds.button( label=('Button ' + str(i)), command=self.UIEventHandlers['ButtonEvents'][i] )
def createButtons(self):
"""Creates some buttons dynamically."""
for i in range(1,6):
# I moved the button creation into a separate function since I couldn'f
# find any way to unset the temporary function. del func didn't do the trick
# and the index stayed the same. When it's moved into a different function it works fine.
self.createButton(i)
def ehButtonPressed(self, *args):
"""An event handler for the button pressed event."""
print args
# End of class
# Create an instance of the class
demo = dynamicUIElements()
OK, so what's going on there. The idea is to have a different function for each button, each of these function are only wrappers for the real event handler function. These wrapper functions are executed when a button is pressed and call the real event handler with the addition of the button's index as an argument.
Now, there are a couple things to note:
First, the functions have to be stored in a class variable. The reason for this is that having them referenced on a Maya object is not enough and they get garbage collected. This is what the UIEventHandlers variable is for.
Second, I had an issue with redefining the temporary function. I tried to use del func in the loop when creating the buttons (the temp func was defined in the loop as well), but it didn't work, I got the last index used for all buttons. Since I'm new to Python I couldn't find any other solution than separating the func definition to a different function, and it was only logical for me to move the whole button creation logic into that function as well.
This method works even for UI elements that get some arguments from Maya, it just adds another argument (the element's index, or whatever you wish to add,) and passes it on to the real event handler.
That's it. A bit of a hack, but worked nicely for me.
5 comments
Comment from: Bård Visitor

This helped me a lot.
I think that you really don’t need to store the func in an array. It’s better to let the UIElements be a class variable instead of a static (add self. prefix and put on top in __init__). Then you can pass func directly to the command parameter when creating buttons.
Thanks for sharing!
Comment from: o.z Member

Thank you for your comment.
The UIElements variable is a class variable (I had some formatting issues in this post, hope now it’s clearer.) However, it is not necessary to use the UIElements var, I only like to use in case I need to manipulate those UI elements later.
As for storing the event functions in an array (UIEventHandlers), this is necessary, because, as I explain in the post, if you don’t those functions will be garbage collected as soon as the creation function is done, and the event commands won’t work.
o
Comment from: Kiaran Ritchie Visitor

Hey Ofer!!
I went Googling to see if anyone else had hit this little snag before. Low and behold my old friend has already solved it. Thanks a lot for putting this together!
Hope you’re still liking IM Digital. Jas and I are freezing up here in Canada-duh ;) Keep up the good work.
-Kiaran
Comment from: matt Visitor

the python-in-maya group has an elegant solution i stumbled across today; use partial. lets you skip all sorts of class trickery (insert indents where appropiate, can’t html format comments…):
import maya.cmds as cmds
from functools import partial
def myFunc(set,*args):
print ‘you chose ‘, set
occSets = [’A',’B',’C',’D']
for i in occSets():
cmds.menuItem(l=i, c=partial(myFunc,i))
Comment from: o.z Member

Thanks for sharing this Matt!
I’ll have to test it sometime.