Houdini 18.5 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 must interpret various low-level events like mouse moves, mouse clicks, parameter changes, keyboard events, and so on. See input events for more details on how to interpret mouse events.

Mouse handling

onMouseEvent

onMouseEvent does most of the work for implementing the Python handle interactivity.

  • Use onMouseEvent and hou.ViewerEvent to track down low-level mouse interactions, detect mouse downs, etc…

  • Mouse movements can be easily constrained to axes, planes and the construction grid with hou.ViewerHandleDragger.

  • Unlike Python state’s onMouseEvent, there is no need to perform a geometry intersection to find which gadget needs to be picked or located, Houdini takes care of it for you.

  • The results of picking and locating gadgets is stored in a hou.ViewerHandleContext object which is typically used to choose the action associated to a given gadget.

  • onMouseEvent is only called when the mouse is over a gadget with or down.

  • onMouseEvent is always processed before Python state’s onMouseEvent gets processed.

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

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):

        # 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.
    """

    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.

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()

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