Houdini 20.5 Examples Python panel examples

Linked parameters

How to link PySide parameter widgets (i.e. text fields and sliders) to Houdini node parameters and vice versa.

Example
<?xml version="1.0" encoding="UTF-8"?>
<pythonPanelDocument>
  <!-- This file contains definitions of Python interfaces and the
 interfaces menu.  It should not be hand-edited when it is being
 used by the application.  Note, that two definitions of the
 same interface or of the interfaces menu are not allowed
 in a single file. -->
  <interface name="LinkedParmsExample" label="Linked Parameters Example" icon="MISC_python">
    <script><![CDATA[import math
from hutil.Qt import QtCore, QtWidgets

class LinkedParmsExample(QtWidgets.QFrame):
    def __init__(self):
        QtWidgets.QFrame.__init__(self)

        self.nodePathField = QtWidgets.QLineEdit()
        self.nodePathField.setDragEnabled(True)

        # Set focus to "ClickFocus" to preserve "Tab" key
        # functionality in network view panes.
        self.nodePathField.setFocusPolicy(QtCore.Qt.ClickFocus)
        self.nodePathField.editingFinished.connect(self._onFinishedEditingNodePath)

        self.tx = TextFieldAndSlider("tx")
        self.ty = TextFieldAndSlider("ty")
        self.tz = TextFieldAndSlider("tz")

        nodeLabel = QtWidgets.QLabel('Node')
        nodeLabel.setFocusPolicy(QtCore.Qt.NoFocus)

        nodeSelector = QtWidgets.QPushButton(self)
        nodeSelector.clicked.connect(self._onShowNodeChooser)
        nodeSelector.setText("Choose a node")
        nodeSelector.setFocusPolicy(QtCore.Qt.NoFocus)

        selectorLayout = QtWidgets.QHBoxLayout()
        selectorLayout.addWidget(nodeLabel)
        selectorLayout.addWidget(self.nodePathField)
        selectorLayout.addWidget(nodeSelector)
        selectorLayout.addStretch(3)

        MasterLayout = QtWidgets.QVBoxLayout()
        MasterLayout.addLayout(selectorLayout)
        MasterLayout.addWidget(self.tx)
        MasterLayout.addWidget(self.ty)
        MasterLayout.addWidget(self.tz)
        MasterLayout.addStretch(1)

        self.setLayout(MasterLayout)

    def __del__(self):
        # QtWidgets.QFrame does not have a __del__ method 
        # so we do not have to call it here.

        # Disconnect the text fields and sliders 
        # if they are currently attached to a node.
        self.tx.disconnectFromNode()
        self.ty.disconnectFromNode()
        self.tz.disconnectFromNode()

    def _onShowNodeChooser(self):
        path = hou.ui.selectNode(relative_to_node=None, initial_node=None, node_type_filter=hou.nodeTypeFilter.Obj)
        self._linkToNode(path)

    def _onFinishedEditingNodePath(self):
        path = self.nodePathField.text()
        self._linkToNode(path)

    def _linkToNode(self, path):
        self.nodePathField.setText(path)
        node = hou.node(path)
        self.tx.setNode(node)
        self.ty.setNode(node)
        self.tz.setNode(node)


class TextFieldAndSlider(QtWidgets.QWidget):
    def __init__(self, parameter):
        QtWidgets.QWidget.__init__(self)   

        self.node = None
        self.parameterString = parameter        

        self.parameterLabel = QtWidgets.QLabel(parameter)
        self.parameterLabel.setFocusPolicy(QtCore.Qt.NoFocus)

        self.textField = QtWidgets.QLineEdit(self)
        self.textField.editingFinished.connect(self._onTextFieldFinishedEditing)
        self.textField.textChanged.connect(self._onTextFieldChanged)
        self.textField.setFocusPolicy(QtCore.Qt.ClickFocus)
        self.textField.setEnabled(False)

        self.valueSlider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self)
        self.valueSlider.setTickPosition(QtWidgets.QSlider.TicksBothSides)
        self.valueSlider.setTickInterval(1)
        self.valueSlider.sliderMoved.connect(self._onSliderChanged)
        self.valueSlider.setFocusPolicy(QtCore.Qt.NoFocus)
        self.valueSlider.setEnabled(False)

        # Limit values between -10 and 10.
        self.valueSlider.setRange(-10, 10)        

        self.layout = QtWidgets.QHBoxLayout()
        self.layout.addWidget(self.parameterLabel)
        self.layout.addWidget(self.textField)
        self.layout.addWidget(self.valueSlider, 2)
        self.setLayout(self.layout)      

    def setNode(self, selectedNode):
        self.disconnectFromNode()

        if selectedNode is None or selectedNode.parm(self.parameterString) is None:
            self.textField.setEnabled(False)
            self.valueSlider.setEnabled(False)
            return

        self.textField.setEnabled(True)
        self.valueSlider.setEnabled(True)
        self.node = selectedNode
        self.textField.setText(
            str(self.node.parm(self.parameterString).eval()))

        #####################################################################
        # Register an event callback with the selected node.
        # The callback will listen for node parameter value changes.
        # This mimics a channel reference from the PySide text field
        # and slider to the node parameter.
        #####################################################################
        self.node.addEventCallback(
            (hou.nodeEventType.ParmTupleChanged,), self._onNodeChange)

    def disconnectFromNode(self):
        if self.node:
            self.node.removeEventCallback(
                (hou.nodeEventType.ParmTupleChanged,), self._onNodeChange)

        self.node = None

    def _onNodeChange(self, **kwargs):
        # Iterate through the kwargs["parm_tuple"] to find the matching
        # parameter and update the value field.
        if kwargs["event_type"] == hou.nodeEventType.ParmTupleChanged \
            and kwargs["parm_tuple"] is not None:
            for parms in kwargs["parm_tuple"]:
                if(self.parameterString == parms.name()):
                    self.textField.setText(str(parms.eval()))        

    def _onTextFieldChanged(self, text):
        try:
            value = float(self.textField.text())
            self.valueSlider.setValue(value)
        except ValueError:
            pass

    def _onTextFieldFinishedEditing(self):
        try:
            value = float(self.textField.text())
        except ValueError:
            return

        #####################################################################        
        # Update the node parameter that the PySide text field is linked to.
        # This mimics a channel reference from the node parameter to the 
        # PySide text field.
        #####################################################################        
        self.node.setParms({self.parameterString: value})

    def _onSliderChanged(self, value):
        try:
            text_field_value = float(self.textField.text())
            needs_update = (value != math.floor(text_field_value))
        except:
            needs_update = True

        #####################################################################
        # Update the node parameter that the PySide slider is linked to.
        # This mimics a channel reference from the node parameter to the 
        # PySide slider.
        #####################################################################        
        if needs_update:
            self.node.setParms({self.parameterString:value})


def onCreateInterface():
    interface = LinkedParmsExample()
    return interface
]]></script>
  </interface>
</pythonPanelDocument>

Python panel examples

  • Custom Graphics

    Custom OpenGL drawing in a Python Panel.

  • Drag and Drop

    How to implement drag and drop functionality in a Python Panel.

  • Linked parameters

    How to link PySide parameter widgets (i.e. text fields and sliders) to Houdini node parameters and vice versa.

  • Node path

    How to listen for changes to the current node path.

  • Qt designer

    How to load user interface layout from a Qt Designer file.

  • Qt events

    How Python Panels can listen to Qt events.

  • Viewport color editor

    A PySide interface for editing viewport colors.