ShaderOp.com

ShaderOp.com

Hi there. I have moved to a new website at Mhmmd.org. I'm no longer updating this one, but I'm keeping it around so the Internet wouldn't break. See you there.

Using wxPython with Autodesk Softimage

Every now and then someone on the Softimage mailing list would ask a question about using wxPython or PyQT with Autodesk Softimage, and such questions usually go unaddressed or receive a vague answer at best. So I’ll try to answer that question once and for all in this post.

Softimage already has a robust set of built-in user interface controls that should be adequate for most scenarios, but sometimes it would make sense to build a richer UI, like in the case of integrating external pipeline tools. And since Python is something of a de facto standard in the VFX industry, wxPython and PyQT are often the libraries of choice for such tasks.

The only problem is… Well, you better see for yourself. This is what happened when I tried to import the wxPython module in the script editor inside Softimage:

An instant, albeit orderly crash to desktop.

Here’s the issue as far as I understand it: On Windows operating systems, every process that has a window (i.e. any none-console application) also has a message loop, and each process can only have one message loop. The problem with PyQT and wxPython is that they will try to create their own message loops inside the Softimage process, which somehow leads to the whole process shutting down. I don’t know Linux that well, but the same issue should arise there.

It turns out that the solution to this is to run wxPython or PyQT on their own separate threads, so that both message loops will run in isolation from each other.

Apparently Houdini 9.5 had the same thing happening with PyQT, and its documentation contains a code sample that addresses this issue. All I had to do was repackage it inside a Softimage plug-in, although I used wxPython instead of PyQT because I’m more familiar with it. But it should be trivial to change the code to use PyQT instead.

So, without further ado, here’s the code:

# By Mohammad "ShaderOp" Abdulfatah
#
# This little sample demonstrates how to use wxPython to display
# a dialog box inside Autodesk Softimage. It attached an item
# to the "Window" menu named "WxSceneInfo", which executes a
# a command that shows a wxPython dialog box with a labed and
# two buttons. One button adpats the label with the number
# of top-level objects in the current scne, and the other creates
# a polygonal cube.
#
# Modifying this code to work wiht PyQT should be trivial.
#
# For questions, comments, and feedback, you can reach me
# through my website at http://shaderop.com/contact/
 
import win32com.client
from win32com.client import constants
import threading
 
# This will be used to ensure that only one copy of the dialog
# is running.
is_dialog_running_event = threading.Event()
 
def XSILoadPlugin(in_reg):
  in_reg.Author = "Mohammad Abdulfatah"
  in_reg.Name = "WxSceneInfoPlugin"
  in_reg.Major = 1
  in_reg.Minor = 0
 
  in_reg.RegisterCommand("WxSceneInfo","WxSceneInfo")
  in_reg.RegisterMenu(constants.siMenuMainWindowID,
    "WxSceneInfo_Menu",
    False,
    False)
 
  #RegistrationInsertionPoint - do not remove this line
 
  return True
 
def XSIUnloadPlugin(in_reg):
  strPluginName = in_reg.Name
  return True
 
def WxSceneInfo_Init(in_ctxt):
  oCmd = in_ctxt.Source
  oCmd.Description = "Wx Scene Info"
  oCmd.ReturnValue = True
 
  return True
 
def WxSceneInfo_Execute():
  # only launch the dialog if it's not already running.
  if is_dialog_running_event.is_set() == False:
    __queueCommand(runDialog)
  else:
    # It's probably a good idea to somehow notify the user that
    # the dialog is already running.
    pass
 
  return True
 
def WxSceneInfo_Menu_Init( in_ctxt ):
  oMenu = in_ctxt.Source
  oMenu.AddCommandItem("WxSceneInfo","WxSceneInfo")
  return True
 
def runDialog():
  # Any attempt to import the wx module on the main application thread
  # will cause Softiamge to crash and burn, but since this method runs
  # on a separate thread and has its own message loop, importing wx
  # will work here.
  import wx	
 
  # Now go to town
  class MyFrame(wx.Frame):
    def __init__(self, title):
      wx.Frame.__init__(self,
        None,
        size=(400,300),
        style=wx.DEFAULT_FRAME_STYLE,
        title=title)
 
      # set background color to SI's signature desert gray.
      self.SetBackgroundColour(wx.Color(171, 168, 166))
      self.Bind(wx.EVT_CLOSE, self.OnClose)
 
      self.objectCountLabel = wx.StaticText(self,
        label="Number of objects in scene:",
        pos = (10, 10),
        size = (300, -1))
      self.refreshButton = wx.Button(self, label = "Refresh",
                                     pos = (310, 10))
      self.Bind(wx.EVT_BUTTON, self.OnRefresh, self.refreshButton)
 
      self.createCubeButton = wx.Button(self,
                                      label="Create cube",
                                      pos=(310, 50))
      self.Bind(wx.EVT_BUTTON, self.OnCreateCube, self.createCubeButton)
 
    def OnRefresh(self, event):
      count = Application.ActiveSceneRoot.Children.Count
      label = "Number of objects in scene: {0}".format(count)
      self.objectCountLabel.SetLabel(label)
 
    def OnCreateCube(self, event):
      Application.ActiveSceneRoot.AddGeometry('Cube', 'MeshSurface')
 
    def OnClose(self, event):
      self.Destroy()
      # IMPORTANT: Must release the event once the windows is closed,
      # else the dialog won't launch again until after SI is restarted
      global is_dialog_running_event
      is_dialog_running_event.clear()
 
  app = __getApplication()
  frame = MyFrame("Hello World")
  frame.Show(True)
  is_dialog_running_event.set()
  app.MainLoop()
 
# Following code is copied almost verbatim from Houdini 9.5 sample
# code at http://www.sidefx.com/docs/houdini9.5/hom/cookbook/pyqt/
 
__command_queue = []
__command_queue_lock = threading.RLock()
__command_queue_event = threading.Event()
 
__wx_thread = None
 
def __queueCommand(callable, arguments=()):
  global __wx_thread
 
  if __wx_thread is None:
    __wx_thread = threading.Thread(target=__wxThreadMain,
                                   name="wxThread")
    __wx_thread.start()
 
  __command_queue_lock.acquire()
  __command_queue.append((callable, arguments))
  __command_queue_lock.release()
 
  __command_queue_event.set()
 
def __wxThreadMain():
  while True:
    __command_queue_event.wait()
    __command_queue_lock.acquire()
    command = __command_queue.pop()
    __command_queue_event.clear()
    __command_queue_lock.release()
 
    command[0].__call__(*command[1])
 
__wx_app = None
 
def __getApplication():
  import wx
  global __wx_app
  if __wx_app is None:
    __wx_app = wx.App(False)
  return __wx_app

Here’s what it looks like inside Softimage:

And here’s the link to the Github gist:

https://gist.github.com/911265

Hope this helps.

No Comments

Powered by: