HOM PyQt Example: Writing Custom GUIs inside Houdini

Overview

This example illustrates how to use the PyQt user interface widget toolkit to create a custom user interface inside Houdini. If you don’t have PyQt4 installed, you will need to install it to your Python distribution. On windows, install to $HFS/python.

Note if you use PyQt for commercial purposes, you must buy a commercial license. See for more information.

Location

Supporting files for this example are in $HFS/mozilla/documents/hom/cookbook/pyqt, also found in the cookbook/pyqt directory of cookbook_files.tar.gz.

Running the Example

Start Houdini from the cookbook/pyqt directory so Houdini can import the pyqt_helper module. Then create a shelf tool containing the following, and launch it. It will pop up a window with a button and some text. Clicking on the button will display a font chooser dialog that controls the font for the text.

import pyqt_helper

def runFontDialogInThread():
    # This function is called on the thread dedicated to PyQt.  It's only
    # safe to import PyQt4 from this thread.
    from PyQt4 import QtCore
    from PyQt4 import QtGui

    class FontDialog(QtGui.QWidget):
        def __init__(self, parent=None):
            QtGui.QWidget.__init__(self, parent)

            hbox = QtGui.QHBoxLayout()

            self.setGeometry(500, 300, 250, 110)
            self.setWindowTitle('FontDialog')

            button = QtGui.QPushButton('Change Font...', self)
            button.setFocusPolicy(QtCore.Qt.NoFocus)
            button.move(20, 20)

            hbox.addWidget(button)

            self.connect(button, QtCore.SIGNAL('clicked()'), self.showDialog)

            self.label = QtGui.QLabel('This is some Sample Text', self)
            self.label.move(130, 20)

            hbox.addWidget(self.label, 1)
            self.setLayout(hbox)

        def showDialog(self):
            font, ok = QtGui.QFontDialog.getFont()
            if ok:
                self.label.setFont(font)

    app = pyqt_helper.getApplication()
    cd = FontDialog()
    cd.show()
    app.exec_()

pyqt_helper.queueCommand(runFontDialogInThread)

Notes about Using PyQt

This example uses a module named pyqt_helper to help launch PyQt in a separate thread. See the notes inside this module.

:

import threading

"""
This module helps you use PyQt in Houdini's GUI.

Rules when using multiple threads:
- Don't directly run code that accesses PyQt.  Instead, call queueCommand
  to queue a command to run on the thread dedicated to PyQt apps.
- Don't import PyQt4 from any code that isn't running on the PyQt thread.
- Don't create QtGui.QApplication's.  Instead, call getApplication.
- Don't write/print anything to stdout/stderr (at least not when there's a
  Python shell open in Houdini), or Houdini will hang.
"""

# Set this variable to False to run PyQt in the main thread.  If it runs in
# the main thread, the rest of Houdini will be blocked until the application
# ends.
use_separate_thread = True

__command_queue = []
__command_queue_lock = threading.Lock()
__command_queue_event = threading.Event()

__pyqt_thread = None
def queueCommand(callable, arguments=()):
    """Queue up a command to run on the PyQt thread."""
    if use_separate_thread == False:
        callable(*arguments)
        return

    # Start up the PyQt thread if it's not already running.
    global __pyqt_thread
    if __pyqt_thread is None:
        __pyqt_thread = threading.Thread(target=__pyQtThreadMain)
        __pyqt_thread.start()

    __command_queue_lock.acquire()
    __command_queue.append((callable, arguments))
    __command_queue_lock.release()

    # Signal the PyQt thread to run the task.
    __command_queue_event.set()

def __pyQtThreadMain():
    """This function is the starting point for the PyQt thread."""

    # It's important that we import PyQt4 only from the PyQt thread, and not
    # from another thread.
    from PyQt4 import QtCore
    from PyQt4 import QtGui

    while True:
        # Wait for the main thread to signal us.
        __command_queue_event.wait()

        # Remove the command from the stack and reset the event.
        __command_queue_lock.acquire()
        command = __command_queue.pop()
        __command_queue_event.clear()
        __command_queue_lock.release()

        # Run the command.
        command[0].__call__(*command[1])

__pyqt_app = None
def getApplication():
    """Return the QtGui.QApplication.  Use this function instead of creating
       PyQt threads manually.  Do not call this function from anything other
       than the PyQt thread."""
    # This function may only be called from the PyQt thread.
    from PyQt4 import QtCore
    from PyQt4 import QtGui

    # We're careful not to create more than one QApplication.
    global __pyqt_app
    if __pyqt_app is None:
        __pyqt_app = QtGui.QApplication(['houdini'])
    return __pyqt_app