Maya UI Classes
Python lets you abstract the existing maya UI script functions in ways that let you write UI that just isn’t possible to do using mel.
The following page goes over some powerful UI abstraction classes. These classes can be found here.
If you open up the script you’ll see a bunch of classes that should sound familiar. For the most part they’re wrappers around existing script commands. For example: MelFormLayout is a wrapper around the formLayout script command. But the wrappers provide you with a more convenient way of building UI that is a bit more like standard UI toolkits like QT or WX.
But its not just about more succinct and convenient syntax. Its also about functionality. By wrapping common pieces of UI in classes they can be more readily re-used. There is also primitive functionality in the framework for having UI emit signals to other pieces of UI that “care”. By using events, the pieces of re-usable UI you write can be pieced together without explicit knowledge of one another.
NOTE: all examples below you should be able to copy paste into the script editor in maya and run them to see a working example.
Normal maya UI is parented using a state based system. The current parent is the last layout or window control that you’ve created. You can change this by calling setParent and then build the widget. This is kind of awkward because its never clear what commands change parents.
One of the biggest differences with baseMelUI is that the classes it defines require you to explicitly specify a parent when you instantiate a widget.
from baseMelUI import * class TestWindow(BaseMelWindow): WINDOW_NAME = 'tmp' WINDOW_TITLE = 'amazing window' DEFAULT_MENU = None #if this is None, the window will have no menu, otherwise make it a string FORCE_DEFAULT_SIZE = True #always resets the size of the window when its re-created def __init__( self ): tabs = MelTabLayout( self ) tab1 = MelColumn( tabs ) tab2 = MelColumn( tabs ) MelButton( tab1, l='button1' ) MelButton( tab1, l='button2' ) MelButton( tab1, l='button3' ) MelLabel( tab2, l='lots o buttons here!' ) MelButton( tab2, l='another button' ) MelButton( tab2, l='YAHOOOO' ) self.show() TestWindow()
At the time of writing most of the mel layout controls have been wrapped. Wrapping the layout controls can be almost as simple as you want it to be. The functionality that the class includes of course is up to you. For example the MelPaneLayout class has methods to make it easier and more intuitive to change and query panel sizes.
There are also some variations on the MelFormLayout that make it easier to use. For example the MelHLayout implements a layout() method which will distribute children horizontally. For example:
from baseMelUI import * class TestWindow(BaseMelWindow): WINDOW_NAME = 'tmp' DEFAULT_SIZE = 300, 90 FORCE_DEFAULT_SIZE = True def __init__( self ): hForm = MelHLayout( self ) b1 = MelButton( hForm, l='button1' ) b2 = MelButton( hForm, l='button2' ) b3 = MelButton( hForm, l='button3' ) hForm.setWeight( b2, 2 ) hForm.setWeight( b3, 3 ) hForm.layout() self.show() TestWindow()
By wrapping the existing UI up into classes it becomes easy to modularize your own UI into more manageable, separable chunks. A good example of this is the mappingUI. This mapping UI is used by xferAnimUI and a variety of other pieces of UI. The class can be found here: mappingEditor.py. You can use this UI and add functionality on top of it for your own purposes. For example:
from baseMelUI import * from mappingEditor import MappingForm class TestWindow(BaseMelWindow): DEFAULT_SIZE = 400, 300 def __init__( self ): f = MelSingleLayout( self ) map = MappingForm( f ) f.layout() self.show() TestWindow()
Try running that code and you should see that a window with the mapping editor gets created. Leveraging the functionality provided by the mapping editor becomes a reasonably simple matter. As you can see in the script, the classes have a bunch of functionality to set and get mapping information from the UI.
Here is another simple example – the comments in the following code describe the interesting points.
from baseMelUI import * class ButtonsLayout(MelColumnLayout): #inherits from MelColumn - so all child UI will automatically be in a column style layout def __init__( self, parent, *a, **kw ): kw[ 'rowSpacing' ] = 5 MelColumnLayout.__init__( self, parent, *a, **kw ) MelButton( self, l='Do NOTHING' ) MelSeparator( self ) def doPrint( info ): print info self.count = 0 row1 = MelHLayout( self ) #create a stretchy horizontal layout for buttons MelButton( row1, l='Print Hello', c=lambda *a: doPrint( "hello") ) MelButton( row1, l='Goodbye', c=lambda *a: doPrint( "Goodbye") ) row1.layout() #tell the stretchy layout to figure things out MelSeparator( self ) labelWidth = 150 buttonWidth = 75 row2 = MelHSingleStretchLayout( self ) #layout makes only one of its items stretch row2.setStretchWidget( MelLabel( row2, l='Counter', w=labelWidth, align='right' ) ) #define the stretchy child widget MelButton( row2, l='Increment', w=buttonWidth, c=self.inc ) MelButton( row2, l='Decrement', w=buttonWidth, c=self.dec ) MelButton( row2, l='Print Count', w=buttonWidth, c=lambda *a: doPrint( self.count) ) row2.layout() #tell the layout to figure things out def inc( self, *a ): self.count += 1 def dec( self, *a ): self.count -= 1 class TestWindow(BaseMelWindow): DEFAULT_SIZE = 400, 170 def __init__( self ): f = MelSingleLayout( self ) map = ButtonsLayout( f ) f.layout() self.show() TestWindow()
Don’t Use Doc Tags!
The docTag feature of maya UI gets used by the system to encode into the widget the class it belongs to. This allows baseMelUI to cast widgets to the appropriate class from the widget’s name. Of course, this won’t properly setup all custom instance variables, but for the most part this isn’t a problem.
Its a pretty simple system. Basically baseMelUI knows all the classes that subclass BaseMelUI. When the widget is instantiated the name of the class that build it gets stored in that widget’s docTag. Then if you need to cast the widget back to its original type you can simply pass the widget’s name to BaseMelUI.FromStr(). The FromStr classmethod will find the BaseMelUI subclass with the name stored in the docTag and cast the widget as one of its instances.
Technically you CAN use docTags if you want, but there should be fairly few cases that you should need to use them as a solution to a UI problem given the ability to use instance variables. If the FromStr classmethod cannot find an appropriately named subclass it will fall back to the most appropriate class in baseMelUI based on the widget’s type.
So subclassing existing UI classes should work exactly as you expect, although there are a few small caveats.
Obviously you’ll need to make sure you call the __init__ method on the base class. This is easy to forget and with some classes things will mostly work without doing this. But it depends on the class you’re inheriting from. Most of the magic happens in the __new__ method because ultimately widget objects are unicode strings which are immutable.
If your derived class requires a different calling signature to the base class then you’ll probably have to re-define the __new__ method. This is kinda annoying but again, because the widgets derive from the immutable type unicode there’s not a lot that can be done. The MelProgressWindow is a good example of this.
If you’ve never defined a __new__ method before just remember you need to return the new instance you create. The __new__ method in python is actually the class constructor. The __init__ method is more of, as the name implies, an initializer. Its the __new__ method that actually constructs the new object and must return it.
When creating a new piece of UI, try to keep in mind what the best way to break it out into its component pieces might be. Usually its a good idea to have at least two classes – the class that defines the window for the tool, and the class that defines the inner layout. That way if down the road you want to embed the functionality of the tool inside another tool (perhaps in a different tab, or a frameLayout etc) its easy to do without refactoring code.
This can get tricky if there is deep interaction between the window’s menu and the UI, but if its an easy thing to do this is almost always a good place to start. It costs almost no extra time to do, and more often than not, useful tools find their way into either other tools, or meta-metamorphose into something else.
The BaseMelWidget provides a simple “event” mechanism which can make it easy to have pieces of UI interact without the two pieces of UI necessarily knowing about one another. Its a dumber version of events in more full featured UI toolkits like QT or WX.
Basically the way it works is a piece of UI makes a call to sendEvent(). The event is basically a string, and an arg/kwarg pair. BaseMelUI then takes this event, walks up the UI hierarchy and asks each widget along the way if they have a method of the name given. Here is a simple example:
from baseMelUI import * class EventTestLayout(MelColumnLayout): def __init__( self, parent, *a, **kw ): MelColumnLayout.__init__( self, parent, rowSpacing=5, *a, **kw ) MelButton( self, l='Button 1', c=lambda *a: self.sendEvent( 'do_pressed', 'this is the message' ) ) MelSeparator( self ) MelButton( self, l='Button 2', c=lambda *a: self.sendEvent( 'do_pressed', 'this is another message!' ) ) class TestWindow(BaseMelWindow): def __init__( self ): f = MelSingleLayout( self ) map = EventTestLayout( f ) f.layout() self.show() def do_pressed( self, message ): print message TestWindow()