Houdini 20.0 Python scripting

Python handle events

How to listen for and react to Viewer Handle events.

On this page

Python viewer handles

Overview

(See Viewer handles for the basics of how to implement a Python handle.)

To properly support node parameter changes interactively, Python handles should interpret various low-level events like mouse moves, mouse clicks and parameter changes. Other UI events support like keyboard and menu events is also possible, see the input events documentation for more details.

Mouse handling

Houdini lets you handle mouse events with three different handlers: onMouseEvent,onMouseIndirectEvent and onMouseWheelEvent.

onMouseEvent

The handler is used for implementing most of your Python handle interactive workflow.

  • onMouseEvent lets you handle LMB and MMB events when the mouse is over a handle gadget.

  • onMouseEvent is always processed before the active state’s onMouseEvent.

  • Use hou.ViewerEvent (kwargs["ui_event"]) to track down low-level mouse interactions, detect mouse downs, etc…

  • Use hou.ViewerHandleContext (self.handle_context) to access the state of a gadget.

  • Before calling onMouseEvent, Houdini does a pass to find out which geometry is under the mouse and updates self.handle_context with the (active) gadget associated to the geometry. There is no need to implement a geometry intersection to find the gadget by yourself.

  • The active gadget can then be accessed from self.handle_context in onMouseEvent to choose which action to execute.

  • Python handle actions can be implemented with hou.ViewerHandleDragger to constrain mouse movements along axes, planes or across the construction grid.

Here’s an example to demonstrate how to translate parameters with a handle dragger and gadget.

import viewerhandle.utils as hu

def createViewerHandleTemplate():
    handle_type = 'handle_example'
    handle_label = 'Handle example'
    handle_cat = [hou.sopNodeTypeCategory()]

    template = hou.ViewerHandleTemplate(handle_type, handle_label, handle_cat)
    template.bindFactory(Handle)

    # Define the handle parameters
    template.bindParameter( hou.parmTemplateType.Float, name="tx", label="Tx", 
        min_limit=-10.0, max_limit=10.0, default_value=0.0 )
    template.bindParameter( hou.parmTemplateType.Float, name="ty", label="Ty", 
        min_limit=-10.0, max_limit=10.0, default_value=0.0 )
    template.bindParameter( hou.parmTemplateType.Float, name="tz", label="Tz", 
        min_limit=-10.0, max_limit=10.0, default_value=0.0 )

    # Export all parameters
    template.exportParameters(["tx", "ty", "tz"])

    # Bind the pivot gadget
    template.bindGadget( hou.drawableGeometryType.Face, "pivot" )

    return template    

class Handle(base_class.Handle):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

        # Creates a dragger for translating the handle
        self.translate_dragger = hou.ViewerHandleDragger("translate_dragger")

        # Utility class to support the handle transform operations
        self.xform_aid = hu.TransformAid(self, parm_names={"translate":["tx","ty","tz"]})

def onMouseEvent( self, kwargs ):
    """ Called when a gadget is picked and dragged.
    """

    # Get the active gadget being located or picked.
    if self.handle_context.gadget() == "pivot":

        ui_event = kwargs["ui_event"]
        reason = ui_event.reason()

        # The pivot gadget is what the user drags to change the translate parameters.
        if reason == hou.uiEventReason.Start:
            # Get the the handle tx,ty,tz parameters as a hou.Vector3 object
            handle_pos = self.xform_aid.parm3("translate")

            # Start the handle translate with a hou.ViewerHandleDragger object. 
            # The dragger has various methods for specialized operations such as 
            # dragging along a line or along a plane. Here we just move the pivot 
            # in world space.
            self.translate_dragger.startDrag(ui_event, handle_pos)

        elif reason in [hou.uiEventReason.Changed, hou.uiEventReason.Active]:
            # drag the pivot continously
            drag_values = self.translate_dragger.drag(ui_event)

            # Update the handle tx, ty, tz parameters with the position returned by the dragger.
            self.xform_aid.addToParm3("translate", drag_values["delta_position"])

            # Note: The transform will be computed in onDrawSetup.

            if reason == hou.uiEventReason.Changed:
                # We are done, exit the drag.
                self.translate_dragger.endDrag()                   

        # Consume the event
        return True

    return False

This implementation pattern is pretty generic and can be reused for most of your Python handles. Since onMouseEvent is always called when a gadget is picked, there is no need to check if the mouse is down first, instead you check which gadget is active and acts upon it.

hou.uiEventReason.Start tells us the mouse was pressed down, you need to setup the dragger at this point by calling startDrag with the handle translate position (xyz parameters).

We should call the drag method when the UI event is set to hou.uiEventReason.Changed or hou.uiEventReason.Active, and update the handle’s translate parameters with the delta value returned by the dragger. Once the mouse has been released (hou.uiEventReason.Changed), we terminate the dragger with a call to endDrag.

You probably notice that self.handle_context is not explicitly created in the __init__ function. This is a class attribute created by Houdini, you don’t need to create one yourself. self.handle_context is set with a hou.ViewerHandleContext object which holds the state of gadgets with regards to picking and locating. This object is managed by Houdini to make sure it is constantly updated with the latest contextual information.

onMouseIndirectEvent

This handler is used for handling MMB events if the mouse is not located over one of the python handle gadgets.

  • onMouseIndirectEvent is called for the handle closest to the mouse.

  • Use hou.ViewerEvent (kwargs["ui_event"]) to track down low-level mouse interactions.

  • Houdini will only call onMouseIndirectEvent for the following events: hou.uiEventReason.Start, hou.uiEventReason.Active, hou.uiEventReason.Changed.

  • Dragging the mouse with LMB or RMB will not trigger the handler.

  • onMouseIndirectEvent must return True when the event has been consumed.

This example demonstrates how to translate the handle by dragging the mouse with MMB.

import viewerhandle.utils as hu

def createViewerHandleTemplate():
    handle_type = 'handle_example'
    handle_label = 'Handle example'
    handle_cat = [hou.sopNodeTypeCategory()]

    template = hou.ViewerHandleTemplate(handle_type, handle_label, handle_cat)
    template.bindFactory(Handle)

    # Define the handle parameters
    template.bindParameter( hou.parmTemplateType.Float, name="tx", label="Tx", 
        min_limit=-10.0, max_limit=10.0, default_value=0.0 )
    template.bindParameter( hou.parmTemplateType.Float, name="ty", label="Ty", 
        min_limit=-10.0, max_limit=10.0, default_value=0.0 )
    template.bindParameter( hou.parmTemplateType.Float, name="tz", label="Tz", 
        min_limit=-10.0, max_limit=10.0, default_value=0.0 )

    # Export all parameters
    template.exportParameters(["tx", "ty", "tz"])

    # Bind the pivot gadget
    template.bindGadget( hou.drawableGeometryType.Face, "pivot" )

    return template    

class Handle(base_class.Handle):
    PIVOT_SIZE = 0.3

    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

        # Creates a dragger for translating the handle
        self.translate_dragger = hou.ViewerHandleDragger("translate_dragger")

        # Utility class to support the handle transform operations
        self.xform_aid = hu.TransformAid(self, parm_names={"translate":["tx","ty","tz"]})

        # Pivot gadget
        sops = hou.sopNodeTypeCategory()
        verb = sops.nodeVerb("box")
        psize = Handle.PIVOT_SIZE
        verb.setParms(
            {   "type" : 1,
                "size" : (psize,psize,psize),
                "divrate": (2,2,2)
            })
        pivot = hou.Geometry()
        verb.execute(pivot, [])
        self.pivot = self.handle_gadgets["pivot"]
        self.pivot.setGeometry(pivot)
        self.pivot.show(True)

def onMouseInteractEvent(self, kwargs):
    """ Called when the mouse is dragged with MMB.
    """

    ui_event = kwargs["ui_event"]
    reason = ui_event.reason()

    consumed = False        

    # The example moves the pivot when MMB is down. 
    #
    # The implementation is not that different from `onMouseEvent`. 
    #
    # We don't need to check for the active gadget as the mouse cannot 
    # be over a gadget when onMouseInteractEvent is called.
    #
    # Using a "constraint free" dragger will compute the handle position 
    # relative to the world space position under the mouse.

    if reason == hou.uiEventReason.Start:
        # Get the handle tx,ty,tz parameters as a hou.Vector3 object
        handle_pos = self.xform_aid.parm3("translate")

        # init the dragger
        self.translate_dragger.startDrag(ui_event, handle_pos)

        consumed = True

    elif reason in [hou.uiEventReason.Changed, hou.uiEventReason.Active]:
        # drag the pivot continously
        drag_values = self.translate_dragger.drag(ui_event)

        # Update the handle tx, ty, tz parameters with the position returned by the dragger.
        self.xform_aid.addToParm3("translate", drag_values["delta_position"])

        # Note: The transform will be computed in onDrawSetup.

        if reason == hou.uiEventReason.Changed:
            # We are done, exit the drag.
            self.translate_dragger.endDrag()

        consumed = True

    return consumed

def onMouseEvent( self, kwargs ):
    """ Called when a gadget is picked and dragged.
    """

    consumed = False        

    # Get the active gadget being located or picked.
    if self.handle_context.gadget() == "pivot":

        ui_event = kwargs["ui_event"]
        reason = ui_event.reason()

        # The pivot gadget is what the user drags to change the translate parameters.
        if reason == hou.uiEventReason.Start:
            # Get the the handle tx,ty,tz parameters as a hou.Vector3 object
            handle_pos = self.xform_aid.parm3("translate")

            # Start the handle translate with a hou.ViewerHandleDragger object. 
            # The dragger has various methods for specialized operations such as 
            # dragging along a line or along a plane. Here we just move the pivot 
            # in world space.
            self.translate_dragger.startDrag(ui_event, handle_pos)

            consumed = True

        elif reason in [hou.uiEventReason.Changed, hou.uiEventReason.Active]:
            # drag the pivot continously
            drag_values = self.translate_dragger.drag(ui_event)

            # Update the handle tx, ty, tz parameters with the position returned by the dragger.
            self.xform_aid.addToParm3("translate", drag_values["delta_position"])

            # Note: The transform will be computed in onDrawSetup.

            if reason == hou.uiEventReason.Changed:
                # We are done, exit the drag.
                self.translate_dragger.endDrag()                   

            consumed = True

    return consumed

def onDraw( self, kwargs ):
    self.pivot.draw(kwargs["draw_handle"])

onMouseWheelEvent

Responding to the mouse wheel is achieved with onMouseWheelEvent. This handler works pretty much the same as the Python state onMouseWheelEvent handler.

There is an important difference between the two however. The Python handle’s onMouseWheelEvent handler is only called on located gadgets, hence the mouse cursor should always be over a gadget for a mouse wheel event to be fired. Sometimes, keeping the mouse over a located gadget can be finicky, so you might want to design your Python handle accordingly, with either a dedicated or a stationary gadget to enable a mouse wheel event.

Also, Houdini always call the Python handle’s onMouseWheelEvent before the Python state’s onMouseWheelEvent handler. Make sure the Python handle’s onMouseWheelEvent consumes the event to avoid processing the Python state’s handler by mistake.

Parameters

Parameters are an important part of a Python handle. Without parameters, a Python handle cannot be used for changing node parameters in the viewport.

Defining the handle parameters is part of the registration process, use hou.ViewerHandleTemplate.bindParameter to add parameters to a Python handle template. All defined parameters can then be manipulated in the viewport interactively only if they have been properly exported. Parameters are exported with hou.ViewerHandleTemplate.exportParameters, this step allows Python states to bind a Python handle parameters to a node parameters. If you choose not to export a specific parameter, it will not be available to the outside.

In addition to Python handle parameters, you can also add setting parameters to a Python handle with hou.ViewerHandleTemplate.bindSetting. These setting parameters are typically used as configuration settings for a Python handle. For instance, you might want to create a setting parameter to hold different color values for a gadget or a scaling value, etc… Python handle parameters and settings can be modified from the Handle Parameter Dialog and from the Python handle’s handle_parms class attribute. However, setting parameters are not exportable and therefore cannot be changed interactively from the viewport.

Note

Parameter and setting values are saved with the scene and restored to the last saved value when the scene is loaded back.

Responding to parameter changes

Use the onParmChangeEvent handler to respond to parameter changes. Houdini calls the handler after a Python handle’s parameter or setting parameter has been changed. For more details see onParmChangeEvent.

This sample demonstrates how to define the parameters of a Python handle and respond to parameter changes.

import viewerhandle.utils as hu

def createViewerHandleTemplate():
    handle_type = 'handle_example'
    handle_label = 'Handle example'
    handle_cat = [hou.sopNodeTypeCategory()]

    template = hou.ViewerHandleTemplate(handle_type, handle_label, handle_cat)
    template.bindFactory(Handle)

    # Handle parameters are typically used for controlling gadgets and binding node parms.
    template.bindParameter( hou.parmTemplateType.Float, name="tx", label="Tx", 
        min_limit=-10.0, max_limit=10.0, default_value=0.0 )
    template.bindParameter( hou.parmTemplateType.Float, name="ty", label="Ty", 
        min_limit=-10.0, max_limit=10.0, default_value=0.0 )
    template.bindParameter( hou.parmTemplateType.Float, name="tz", label="Tz", 
        min_limit=-10.0, max_limit=10.0, default_value=0.0 )

    # ... they also need to be exported, export only the ones you want Houdini to use. 
    # The exported parameters can be used in a python state for binding node parameters 
    # e.g. hou.ViewerStateTemplate.bindHandleStatic.
    template.exportParameters(["tx", "ty", "tz"])

    # Settings are parameters used for controlling the handle behavior.
    template.bindSetting( hou.parmTemplateType.Toggle, name="face", label="Face", default_value=True, align=True )
    template.bindSetting( hou.parmTemplateType.Toggle, name="wire", label="Wire", default_value=True, align=True )
    template.bindSetting( hou.parmTemplateType.Toggle, name="pivot", label="Pivot", default_value=True, align=True )
    template.bindSetting( hou.parmTemplateType.Toggle, name="knob", label="Knob", default_value=True, align=False )

    # Bind gadgets 
    template.bindGadget( hou.drawableGeometryType.Face, "pivot" )
    template.bindGadget( hou.drawableGeometryType.Face, "face" )
    template.bindGadget( hou.drawableGeometryType.Line, "wire" )
    template.bindGadget( hou.drawableGeometryType.Point, "knob" )

    return template    

class Handle(base_class.Handle):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
        self.xform_aid = hu.TransformAid(self, parm_names={"translate":["tx","ty","tz"]})

        # skip gadget initialization for brevity

    def onParmChangeEvent(self, kwargs):
        """
        Called when a handle parameter or setting has changed.
        """
        parm_name = kwargs['parm_name']
        parm_value = kwargs['parm_value']

        if parm_name in ["tx","ty","tz"]:
            # update the handle transform with the new values
            self.xform_aid.updateTransform()
        elif parm_name == "face":
            kwargs["handle_gadgets"]["face"].show(parm_value)
        elif parm_name == "wire":
            kwargs["handle_gadgets"]["wire"].show(parm_value)
        elif parm_name == "pivot":
            kwargs["handle_gadgets"]["pivot"].show(parm_value)
        elif parm_name == "knob":
            kwargs["handle_gadgets"]["knob"].show(parm_value)

        # update the viewport
        self.scene_viewer.curViewport().draw()

Accessing parameter values

Python handle’s parameters and settings can be accessed from the handle_parms dictionary. The dictionary is accessible from any Python handle handlers.

def createViewerHandleTemplate():
    handle_type = 'handle_example'
    handle_label = 'Handle example'
    handle_cat = [hou.sopNodeTypeCategory()]

    template = hou.ViewerHandleTemplate(handle_type, handle_label, handle_cat)
    template.bindFactory(Handle)

    # Handle parameters.
    template.bindParameter( hou.parmTemplateType.Float, name="tx", label="Tx", 
        min_limit=-10.0, max_limit=10.0, default_value=0.0 )
    template.bindParameter( hou.parmTemplateType.Float, name="ty", label="Ty", 
        min_limit=-10.0, max_limit=10.0, default_value=1.0 )
    template.bindParameter( hou.parmTemplateType.Float, name="tz", label="Tz", 
        min_limit=-10.0, max_limit=10.0, default_value=0.0 )
    template.bindParameter( hou.parmTemplateType.Float, name="float3"", label="Float 3", 
        min_limit=-10.0, max_limit=10.0, default_value=[1.0,1.0,1.0], num_components=3 )

    # Handle settings.
    template.bindSetting( hou.parmTemplateType.Toggle, name="ui_guides", label="Draw UI guides", default_value=True)

    return template    

class Handle(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

    def onActivate(self, kwargs):
        self.log("Handle tx parm value", kwargs["handle_parms"]["tx"]["value"])
        self.log("Handle ty parm value", kwargs["handle_parms"]["ty"]["value"])
        self.log("Handle tz parm value", kwargs["handle_parms"]["tz"]["value"])
        self.log("Handle float3 parm value", kwargs["handle_parms"]["float3"]["value"])
        self.log("UI guides value", kwargs["handle_parms"]["ui_guides"]["value"])

# output
'Handle tx parm value' 0.0 
'Handle ty parm value' 1.0 
'Handle tz parm value' 0.0 
'Handle float3 parm value' [1.0, 1.0, 1.0] 
'UI guides value' 1 

Drawing

A Python handle uses up to 2 drawing handlers:

  • onDraw:

    Used for drawing the Python handle gadgets and other drawables. It is pretty straight forward to implement and should basically delegate to the handle gadgets and drawables draw method. The handler is similar to the Python states onDraw handler.

  • onDrawSetup:

    Python handles need to be scaled dynamically in order to maintain a fixed size independent of the current camera position. onDrawSetup is typically used for computing the handle scale with the help of hou.ViewerHandleContext.scaleFactor. With a newly computed scale value, Python handles should also update their transformation matrices along with their drawable guides transformation matrices.

def onDrawSetup(self, kwargs):
    # Use the current handle position to compute the scale value
    hpos = self.xform_aid.parm3("translate")

    # Compute the scale value 
    fixed_scale_value = 100.0
    scale = self.handle_context.scaleFactor(hpos)*fixed_scale_value

    # Rebuild the handle transform matrix with the new scale
    xform = self.xform_aid.updateTransform(s=[scale,scale,scale])

    # Update the gadgets transform
    kwargs["handle_gadgets"]["face"].setTransform(xform)
    kwargs["handle_gadgets"]["wire"].setTransform(xform)
    kwargs["handle_gadgets"]["pivot"].setTransform(xform)
    kwargs["handle_gadgets"]["knob"].setTransform(xform)

Python viewer handles

Python scripting

Getting started

Next steps

Reference

  • hou

    Module containing all the sub-modules, classes, and functions to access Houdini.

Guru level

Python viewer states

Python viewer handles

Plugin types