Home » Featured, Python, XSI

Solutions for Python in XSI (Part 1)

8 July 2010 13,218 views 2 Comments

This article is the first of two that will show you solutions to a couple of common problems you’ll face when using Python in XSI.

How to pass functions to PPG logic in Python

When making user interfaces in XSI, I find that I often need to create custom properties on the fly, rather that fully self-installed custom property plugins. When doing this, I usually need to pass callback functions to the PPGLogic attribute so that XSI knows what to do when a button is clicked or a value changes. In JScript, this is simply done using the toString() method of the JScript function object, like this:


function MyJScriptCommand_Execute(  )
{
    prop = XSIFactory.CreateObject("CustomProperty")
    prop.AddParameter2("myValue",siInt4,0,0,100,0,100,
                            siClassifUnknown,
                            siPersistable | siKeyable);

    layout = prop.PPGLayout;
    layout.AddItem("myValue");
    layout.AddButton("myButton");

    layout.language="JScript"
    layout.logic=myValue_OnChanged.toString()+
                     myButton_OnClicked.toString();
    InspectObj(prop);
    return true;
}

function myValue_OnChanged()
{
    LogMessage("myValue was changed");
}

function myButton_OnClicked()
{
    LogMessage("myButton was clicked");
}

Despite the flexibility that Python offers as a language, it doesn’t have a mechanism able to do this. So we need to find another method.

Triple Quoted Strings

One common way to do it is to write your functions inside a triple quoted string. Here’s the Python equivalent of the above code:


def MyPythonCommand_Execute(  ):

    prop = XSIFactory.CreateObject("CustomProperty")
    prop.AddParameter2("myValue",constants.siInt4,0,0,100,0,100,
                            constants.siClassifUnknown,
                            constants.siPersistable | constants.siKeyable)

    layout = prop.PPGLayout
    layout.AddItem("myValue")
    layout.AddButton("myButton")

    layout.Language="Python"
    layout.logic='''
def myValue_OnChanged():
    LogMessage("myValue was changed")

def myButton_OnClicked():
    LogMessage("myButton was clicked")
'''
    Application.InspectObj(prop)
    return true;

This is perfectly acceptable and works fine, but it does have a few drawbacks:

  • It makes the code look ugly and less readable. Python is formatted in a specific way due to the indentation being linked to the syntactical interpretation. Putting code inside a triple quoted string goes somewhat against this ideal.
  • Most text editors will not show the syntax highlighting for the callback code, and will highlight it as a string. This is a minor problem but can be annoying, particularly if you’re building a complex string with escaped characters.
  • All the code inside the triple quotes is executed in a new scope. This means that any global constants, helper functions, or module imports will have to be repeated inside the triple quoted section. This can lead to a lot of code duplication, which is obviously a bad thing.

Using the Logic File attribute

It would be fair to say that the triple quoted string method isn’t particularly convenient. Are there any other ways that we can do this?

Yes. If you look at the PPGLayout attributes, you’ll see that we have the ability to specify a PPG logic file. This is a file in which we can put all our callback functions, and as long as we tell XSI where to find it, everything should work fine. For example:


def MyPythonCommand_Execute(  ):

    prop = XSIFactory.CreateObject("CustomProperty")
    prop.AddParameter2("myValue",constants.siInt4,0,0,100,0,100,
                            constants.siClassifUnknown,
                            constants.siPersistable | constants.siKeyable)

    layout = prop.PPGLayout
    layout.AddItem("myValue")
    layout.AddButton("myButton")

    layout.Language="Python"
    layout.SetAttribute(constants.siUILogicFile,
                        "C:\\Workgroup\\Application\\Plugins\\PPGCallbacks.py")
    Application.InspectObj(prop)

…and in a separate file called PPGCallbacks.py


def myValue_OnChanged():
    LogMessage("myValue was changed")

def myButton_OnClicked():
    LogMessage("myButton was clicked")

This certainly gets around the problem of syntax highlighting, but it still means we have to redeclare global constants, helper functions, and reimport modules. It also gives us two extra problems of having to hardcode the location of the logic file, and having to make sure that the plugin file and the callback file are always installed together. If anything goes wrong with either of those, you’re not going to know about it until you open the PPG and press a button or change a value.

Okay, why not eliminate the extra file and just point it at the original script?


def MyPythonCommand_Execute(  ):

    prop = XSIFactory.CreateObject("CustomProperty")
    prop.AddParameter2("myValue",constants.siInt4,0,0,100,0,100,
                            constants.siClassifUnknown,
                            constants.siPersistable | constants.siKeyable)

    layout = prop.PPGLayout
    layout.AddItem("myValue")
    layout.AddButton("myButton")

    layout.Language="Python"
    layout.SetAttribute(constants.siUILogicFile,
                 "C:\\Workgroup\\Application\\Plugins\\MyPythonPlugin.py")
    Application.InspectObj(prop)

def myValue_OnChanged():
    LogMessage("myValue was changed")

def myButton_OnClicked():
    LogMessage("myButton was clicked")

That’s much better. We don’t need to manage two files and we can put all our callback code in the same file as the PPG definition. It starts behaving a lot like a self-installed custom property, although we still have the problem of hard coding the path.

Using XSI’s plugin registry

The solution is to look up the path to the current plugin file using Application.Plugins.

Let’s look at the details of how this works.

Application.Plugins is a collection of XSI plugin objects. We can retrieve any particular one by asking for it by name. This is the one we declared it with in the registration function.


def XSILoadPlugin( in_reg ):
    in_reg.Author = "Andy"
    in_reg.Name = "My Python Plugin"
    in_reg.Major = 1
    in_reg.Minor = 0

    in_reg.RegisterCommand("MyPythonCommand","MyPythonCommand")
    #RegistrationInsertionPoint - do not remove this line

    return true

By using this name to index into the plugin collection, XSI will return us a Plugin object:


pluginObject = Application.Plugins("My Python Plugin")

Once we have the plugin object we can ask it for its filename:


LogMessage(Application.Plugins("My Python Plugin").FileName)

# INFO : C:\Workgroup\Application\Plugins\MyPythonPlugin.py

So to set the logic file to point to the current plugin, we can just do this:


layout.SetAttribute(constants.siUILogicFile, 
                    Application.Plugins("My Python Plugin").FileName)

To obey the rule I mentioned earlier about not defining the same thing twice, we can define the plugin name as a global variable right at the top of the script. The final script looks like this:


import win32com.client
from win32com.client import constants

g_pluginName = "My Python Plugin"

def XSILoadPlugin( in_reg ):
    in_reg.Author = "Andy"
    in_reg.Name = g_pluginName
    in_reg.Major = 1
    in_reg.Minor = 0

    in_reg.RegisterCommand("MyPythonCommand","MyPythonCommand")
    #RegistrationInsertionPoint - do not remove this line

    return true

def MyPythonCommand_Execute(  ):

    prop = XSIFactory.CreateObject("CustomProperty")
    prop.AddParameter2("myValue",constants.siInt4,0,0,100,0,100,
                            constants.siClassifUnknown,
                            constants.siPersistable | constants.siKeyable)

    layout = prop.PPGLayout
    layout.AddItem("myValue")
    layout.AddButton("myButton")

    layout.Language="Python"
    layout.SetAttribute(constants.siUILogicFile, 
                        Application.Plugins(g_pluginName).FileName)
    Application.InspectObj(prop)

def myValue_OnChanged():
    LogMessage("myValue was changed")

def myButton_OnClicked():
    LogMessage("myButton was clicked")

That’s all there is to it. Now we have a workable strategy that has the following advantages:

  • Tidy code
  • Full syntax highlighting
  • All the code is in a single file
  • All the constant definitions, helper functions, and imports only need to be written once

Note that the callback code is still being executed in a different context. There’s no way around that. This means that changes to the values of global variables will not get passed through to the callback code. Global constants work fine because they are declared in global scope and don’t change.

In the second installment of this article, I’ll be showing you how to load python modules that are specific to your plugin using a similar system.

1 Star2 Stars3 Stars4 Stars5 Stars (2 votes, average: 5.00 out of 5)
Loading...

2 Comments »

  • Hiro Goto said:

    Hi, Andy

    I\’m a TD in Japanese CG production @ Tokyo.

    I read your article.

    May I ask you a question about Python Custom module in SelfInstall Property by Jscript?

    I want to hide codes and limit using time about custom selfinstall property by python\’s wrapper custom module(.pyc).

    I tried to use a zip with passward and zipfile module in python.

    I zipped some call back function codes to zip file(.zip).
    and, my custom wrapper python module read it and run by ExecuteScriptcode().

    I have a problem about args in it.
    it has some special args(in_ctxt, in_reg).
    but, It seems that these args can\’t be use in wrapper python module.

    Could you tell me some advice about this issue?

    Best Regards,

    Hiro

  • AndyN (author) said:

    Hi Hiro,
    Sorry, I don’t understand what you’re trying to do. Maybe if you could copy and paste some example code it might make it clearer.

    Since it sounds quite involved could you send more information via the contact form (http://www.andynicholas.com/?page_id=224) instead of here and we can discuss it over email.

    Thanks,

    Andy

Leave your response!

Add your comment below, or trackback from your own site. You can also subscribe to these comments via RSS.

You can use these tags:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This is a Gravatar-enabled weblog. To get your own globally-recognized-avatar, please register at Gravatar.