Monday, October 10, 2011

MotionBuilder Python & Window Focusing

I recently had to do some window handle trickery with Python and MotionBuilder. I have an external python tool that is communicating with MotionBuilder, 3dsmax, and in the near future Maya. For this post I will only be touching on the MotionBuilder implementation of the tool.   When I run the tool it will return a list of scene objects within a list view. If I select on a item in the list view it should select the object within MotionBuilder. While the selection in the scene navigator is updated, since the tool is not a child of the MotionBuilder application, MotionBuilder will not redraw the viewport. I'm quite new to MoBu but the only code I saw to potentially force redraw the scene was.

FBSystem().Scene.Evaluate()

It is likely that the Mobu viewport/rendering is on a separate thread from normal scene processing. Which would explain why the scene navigator updates and the viewport does not. The viewport only seems to update if the application is the focused application. With this in mind I decided to force change the application focus to get the results I wanted.

When the python tool makes a connection to the specific dcc app I will call the following:
import win32gui, win32con
 
def enum_callback(self, hwnd, results): 
 self.winlist.append((hwnd, win32gui.GetWindowText(hwnd)))
 
def get_dcc_hwnd(self, app_name): 
 
 self.toplist = [] 
 self.winlist = [] 
 
 win32gui.EnumWindows(self.enum_callback, self.toplist) 
 
 # look for the specific application window name 
 dcc = [(hwnd, title) for hwnd, title in self.winlist if app_name in title.lower()] 
 if dcc != None and dcc[0] != None: 
  dcc = dcc[0] 
  self.dcc_hwnd = dcc[0]
 
get_dcc_hwnd( 'motionbuilder')               
This will return self.dcc_hwnd and all that really is is a window process handle or long integer. In this example it is. DCC Handle: 3214530

Now that I have stored the window handle when I select an object in my listview it will call the following:
def set_dcc_focus(self):
 if self.dcc_hwnd != None:
 
  # store the current python tool window handle
  current_hwnd = win32gui.GetForegroundWindow()
 
  # use the window handle to set focus
  win32gui.SetForegroundWindow(self.dcc_hwnd)
 
  # important to sleep here as to allow Mobu time to draw
  time.sleep(0.03)

  try:
  # set the focus back to the python tool
   win32gui.SetForegroundWindow(current_hwnd)
  except:
   return
With this I can click on all the objects in my external tool and see the select update 'realtime' within the MotionBuilder viewport. The code will change the window focus momentarily and just long enough to redraw the MotionBuilder viewport then immediately return focus back to the python tool. With the sleep time I have set here it is fast enough that I can be pressing down on the keyboard to step between items in the listview without noticing the app focus switching. However, I have noticed that if Motionbuilder doesn't have enough time to redraw, some things have the possibility of not rendering when the viewport is redrawn. So the sleep here could become variable and dependent on the amount of objects within the viewport.

UPDATE:
In the comments below, Kevin pointed out that you can skip all of this and modify the application.txt by adding the following line under [Timing]
NoFastIdleOnDeactivate = No

Thanks Kevin!