MPxCommand

 
 
 

The new hello command introduced earlier uses a proxy object to add new functionality to Maya (see MFnPlugin). This proxy object is derived from MPxCommand which provides all the functionality necessary for Maya to use the command as if it were built in.

A minimum of two methods must be defined. These are the doIt() method and the creator.

class hello : public MPxCommand
{
public:
 	Status doIt( const MArgList& args );
	static void* creator();
};

doIt() and redoIt() methods

The doIt() method is a pure virtual method, and since there is no creator defined in the base class, you must define both doIt() and creator.

For simple commands, the doIt() method performs the actions of the command. In more complex commands, the doIt() method parses the argument list, the selection list, and whatever else may be necessary. It then uses this information to set data internal to the command before calling the redoIt() method, which does the bulk of the work. This avoids code duplication between the doIt() and redoIt() methods.

Verifying when methods are called

The following is a simple plug-in that outputs a string when any of its methods are called by Maya. You can use it to see when the methods are called.

#include <stdio.h>
#include <maya/MString.h>
#include <maya/MArgList.h>
#include <maya/MFnPlugin.h>
#include <maya/MPxCommand.h>
#include <maya/MIOStream.h>
class commandExample : public MPxCommand
{
    public:
        commandExample();
        virtual ~commandExample();
        MStatus doIt( const MArgList& );
        MStatus redoIt();
        MStatus undoIt();
        bool isUndoable() const;
        static void* creator();
};
commandExample::commandExample() {
    cout << "In commandExample::commandExample()\n";
}
commandExample::~commandExample() {
    cout << "In commandExample::~commandExample()\n";
}
MStatus commandExample::doIt( const MArgList& ) {
    cout << "In commandExample::doIt()\n";
    return MS::kSuccess;
}
MStatus commandExample::redoIt() {
    cout << "In commandExample::redoIt()\n";
    return MS::kSuccess;
}
MStatus commandExample::undoIt() {
    cout << "In commandExample::undoIt()\n";
    return MS::kSuccess;
}
bool commandExample::isUndoable() const {
    cout << "In commandExample::isUndoable()\n";
    return true;
}
void* commandExample::creator() {
    cout << "In commandExample::creator()\n";
    return new commandExample();
}
MStatus initializePlugin( MObject obj )
{
    MFnPlugin plugin( obj, "My plug-in", "1.0", "Any" );
    plugin.registerCommand( "commandExample", commandExample::creator );
    cout << "In initializePlugin()\n";
    return MS::kSuccess;
}
MStatus uninitializePlugin( MObject obj )
{
    MFnPlugin plugin( obj );
    plugin.deregisterCommand( "commandExample" );
    cout << "In uninitializePlugin()\n";
    return MS::kSuccess;
}

When you first load this plug-in, notice that "In initializePlugin()" is printed immediately. If you then type "commandExample" in the command window you will see:

In commandExample::creator()
In commandExample::commandExample()
In commandExample::doIt()
In commandExample::isUndoable()

Note that the destructor is not called. This is because the command object remains indefinitely so that it can be undone, or redone (after being undone).

This is how Maya’s undo mechanism works. Command objects maintain information which allows them to undo themselves when necessary. The destructor is called when the command falls off the end of the undo queue, it is undone and not redone, or the plug-in is unloaded.

If you now use Edit > Undo (or you use the MEL undo command) and Edit > Redo (or you use the MEL redo command), the undoIt() and redoIt() methods of the command get called when these menu items are invoked.

If you modify this example so that the isUndoable() method returns false rather than true (remember to unload the plug-in before recompiling) when you run it, the output becomes:

In commandExample::creator()
In commandExample::commandExample()
In commandExample::doIt()
In commandExample::isUndoable()
In commandExample::~commandExample()

In this case the destructor is called immediately since the command cannot be undone. Maya treats a non-undoable command as an action that does not affect the scene in any way. This means that no information needs to be saved after the command executes, and when undoing and redoing commands, it does not need to be executed since it does not change anything.

Helix example with undo and redo

The following example is another implementation of the helix plug-in. This version is implemented as a full command with undo and redo. It works by taking a selected curve and turning it into a helix.

#include <stdio.h>
#include <math.h>
#include <maya/MFnPlugin.h>
#include <maya/MFnNurbsCurve.h>
#include <maya/MPointArray.h>
#include <maya/MDoubleArray.h>
#include <maya/MPoint.h>
#include <maya/MSelectionList.h>
#include <maya/MItSelectionList.h>
#include <maya/MItCurveCV.h>
#include <maya/MGlobal.h>
#include <maya/MDagPath.h>
#include <maya/MString.h>
#include <maya/MPxCommand.h>
#include <maya/MArgList.h>
class helix2 : public MPxCommand {
public:
    helix2();
    virtual ~helix2();
    MStatus doIt( const MArgList& );
    MStatus redoIt();
    MStatus undoIt();
    bool isUndoable() const;
    static void* creator();

The command starts out as the previous example, declaring the methods it will be defining.

    private:
    MDagPath fDagPath;
    MPointArray fCVs;
    double radius;
    double pitch;
};

This command will be modifying the model. So that it will be able to undo the changes it makes, it allocates space to store the original definition of the curve. It also stores the description of the helix so that it can reproduce it if the redoIt method is called.

It is important to notice that the command does not store a pointer to an MObject, but rather uses an MDagPath to reference the curve for undo and redo. An MObject is not guaranteed to be valid the next time your command is executed. As a result, if you had used an MObject, Maya would likely core dump when performing your undoIt() or redoIt(). An MDagPath however, being simply a description of the path to the curve, is guaranteed to be correct whenever your command is executed.

void* helix2::creator() {
    return new helix2;
}

The creator simply returns an instance of the object.

helix2::helix2() : radius( 4.0 ), pitch( 0.5 ) {}

The constructor initializes the radius and pitch.

helix2::~helix2() {}

The destructor does not need to do anything since the private data will be cleaned up automatically.

Note

Data owned by Maya should not be deleted.

MStatus helix2::doIt( const MArgList& args ) {
    MStatus status;
    // Parse the arguments.
    for ( int i = 0; i < args.length(); i++ )
        if ( MString( "-p" ) == args.asString( i, &status )
            && MS::kSuccess == status )
        {
            double tmp = args.asDouble( ++i, &status );
            if ( MS::kSuccess == status )
                pitch = tmp;
        }
        else if ( MString( "-r" ) == args.asString( i, &status )
            && MS::kSuccess == status )
        {
            double tmp = args.asDouble( ++i, &status );
            if ( MS::kSuccess == status )
                radius = tmp;
        }
        else
        {
            MString msg = "Invalid flag: ";
            msg += args.asString( i );
            displayError( msg );
            return MS::kFailure;
        }

As before, this simply parses the arguments passed into the doIt() method and uses them to set the internal radius and pitch fields which will be used by the redoIt() method. The doIt() method is the only one that receives arguments. The undoIt() and redoIt() methods each take their data from internal data of the command itself.

In the final else-clause, the displayError() method, inherited from MPxCommand, outputs the message in the command window and in the command output window. Messages output with displayError() are prefixed with "Error:". Another option is displayWarning() which would prefix the message with "Warning:".

    // Get the first selected curve from the selection list.
    MSelectionList slist;
    MGlobal::getActiveSelectionList( slist );
    MItSelectionList list( slist, MFn::kNurbsCurve, &status );
    if (MS::kSuccess != status) {
        displayError( "Could not create selection list iterator" );
        return status;
    }
    if (list.isDone()) {
        displayError( "No curve selected" );
        return MS::kFailure;
    }
    MObject component;
    list.getDagPath( fDagPath, component );

This code gets the first curve object off the selection list. The fDagPath field of the command is set to the selected object selection is covered in detail in Chapter 2, "Selecting with the API").

    return redoIt();
}

Once the internal data of the command is set, the redoIt() method is called. The doIt() method could perform the necessary actions itself, but these actions are always identical to those performed by redoIt() so, having doIt() call redoIt() reduces code duplication.

You might wonder why doIt() calls redoIt() and not the other way around. Although this is possible—the redoIt() method could take the cached data and turn it into an MArgList which it could then pass to doIt()—it would be far less efficient.

MStatus helix2::redoIt()
{
    unsigned i, numCVs;
    MStatus status;
    MFnNurbsCurve curveFn( fDagPath );
    numCVs = curveFn.numCVs();
    status = curveFn.getCVs( fCVs );
    if ( MS::kSuccess != status )
    {
        displayError( "Could not get curve’s CVs" );
        return MS::kFailure;
    }

This code gets the CVs from the selected curve and stores them in the command’s internal MPointArray. These stored CV positions could then be used if the undoIt() method is called to return the curve to its original shape.

    MPointArray points(fCVs);
    for (i = 0; i < numCVs; i++)
    points[i] = MPoint( radius * cos( (double)i ),
        pitch * (double)i, radius * sin( (double)i ) );
    status = curveFn.setCVs( points );
    if ( MS::kSuccess != status )
    {
        displayError( "Could not set new CV information" );
        fCVs.clear();
        return status;
    }

As with the earlier helix examples, this code sets the position of the curve’s CVs so that the curve forms a helix.

    status = curveFn.updateCurve();
    if ( MS::kSuccess != status )
    {
        displayError( "Could not update curve" );
        return status;
    }

The updateCurve() method is used to inform Maya that the geometry of the curve has changed. Failing to call this method after modifying geometry causes the display of the object to remain unchanged.

    return MS::kSuccess;
}

Returning MS::kSuccess at the completion of a function indicates to Maya that the operation completed successfully.

MStatus helix2::undoIt()
{
    MStatus status;
    MFnNurbsCurve curveFn( fDagPath );
    status = curveFn.setCVs( fCVs );
    if ( MS::kSuccess != status)
    {
        displayError( "Could not set old CV information" );
        return status;
    }

These few lines take the stored CV positions (the original positions of the curve’s CVs) and resets them.

Note

You needn’t worry about the number of CVs changing, or the curve being deleted in an undo function. You assume that anything done after your command has been undone before your undoIt() is called. As a result the model is in the same state as it was immediately after your command finished.

    status = curveFn.updateCurve();
    if ( MS::kSuccess != status )
    {
        displayError( "Could not update curve" );
        return status;
    }
    fCVs.clear();
    return MS::kSuccess;
}

The MPointArray is cleared here just as a precaution.

bool helix2::isUndoable() const
{
    return true;
}

This command is undoable. It modified the model, but an undoIt() method has been provided which returns the model to the state it was in before the command was run.

MStatus initializePlugin( MObject obj )
{ 
    MFnPlugin plugin( obj, "Autodesk", "1.0", "Any");
    plugin.registerCommand( "helix2", helix2::creator );
    return MS::kSuccess;
}
MStatus uninitializePlugin( MObject obj )
{
    MFnPlugin plugin( obj );
    plugin.deregisterCommand( "helix2" );
    return MS::kSuccess;
}

The plug-in is completed with the usual initialize and uninitialize functions.