IC Python API:Tree View

From Reallusion Wiki!
Revision as of 02:16, 16 December 2019 by Chuck (RL) (Talk | contribs) (Walking the Hierarchy)

Jump to: navigation, search
Main article: RL Python Samples.
Ic python api bone hierarchies 01.png

The QT tree widget is a useful UI widget for listing relational hierarchical entities that may or may not share common set of attributes. Imagine if you were to represent an actual plant in the form of a UI that makes it clear of its structure. You would most likely have the plant name as the base node with the "Trunk" as the direct child node. From the "Trunk" you would have the "Stem", "Branch" and "Leaves" fanning out in a form that resembles a clear hierarchy of related elements. This is the inspiration for the QT tree widget class. In this article, we will create a skeleton hierarchy tree view by cobbling together QT widgets like QGroupBox, QTreeWidget, and QTreeWidgetItem.

If you are listing a set of items that don't have a relational structure with each other then you are better off using a QT list widget.

Necessary Modules

Besides the fundamental Reallusion Python module, we'll also need Pyside2 to build the user interface. But we don't need the entire Pyside2 module for this simple example, so I'll just import QtWidgets for building the user interface and wrapInstance to bind the iClone dialog window to the Pyside2 interface.

import RLPy
from PySide2 import QtWidgets
from PySide2.shiboken2 import wrapInstance

Skeleton Tree View

Next, we'll need to create a custom widget class by inheriting from QtWidgets.QGroupBox in order to get a group box to wrap around the tree widget itself. For the sake of simplicity, we'll just print the item name to the console upon selection in the tree widget. The following three sub-sections are member methods that belong with this custom tree view class.

class SkeletonTreeView(QtWidgets.QGroupBox):

    def __init__(self, parent=None):
        super(SkeletonTreeView, self).__init__(parent)

        # Create and setup the tree widget
        self._treeWidget = QtWidgets.QTreeWidget()
        self._treeWidget.setHeaderHidden(True)
        self._treeWidget.setColumnCount(1)
        # The following is need to show a horizontal scrollbar
        self._treeWidget.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
        self._treeWidget.header().setStretchLastSection(False)

        # Signal callback methods
        self._treeWidget.itemClicked.connect(lambda x: print(x.text(0)))

        # Configure the Group Box parent wdiget
        self.setTitle("Avatars")
        self.setLayout(QtWidgets.QVBoxLayout())
        self.setStyleSheet("QGroupBox  {color: #a2ec13} QTreeView{border:none}")
        self.layout().addWidget(self._treeWidget)

Notice that we stuff a style-sheet onto the QGroupBox itself that has influence over all of it's child elements.

Tree Widget Item Factory Method

Next, we need to populate this tree list widget with consistent tree widget items. In order to do this effectively, we should rely on a factory method that will return a new QTreeWidgetItem every time it is called. This factory method makes it easy to create a parent and child relationship by supplying both at the time of creation.

    def __default_tree_widget_item(self, parent, obj):
        item = QtWidgets.QTreeWidgetItem(parent)
        item.setText(0, obj.GetName())
        item.setExpanded(True)
        return item

Notice that this method name is prefixed with double underscores ("__"); This is so that the method is not exposed beyond the scope of our custom tree widget class.

Walking the Hierarchy

We'll also need a member function for walking through the character's skeleton hierarchy to fish out all of the child nodes. We do this with a recursive loop that searches the current node for child nodes and calls itself for those child nodes.

Be careful when using recursive methods because they can lead to a dead lock (infinite loop).  To prevent this from happening you should wrap your recursive method with a try and catch during the initial test phases until the code is mature and stable.
    # Recursive loop through the hierarchy.
    def __hierarchy_walk_down(self, parent_widget, node, root_node):
        children = node.GetChildren()
        __parent_widget = self.__default_tree_widget_item(parent_widget, node)
        for child in children:
            self.__hierarchy_walk_down(__parent_widget, child, root_node)

Refreshing the Tree List

Creating the Window

Everything Put Together

You can copy and paste the following code into a PY file and load it into iClone via Script > Load Python.

import RLPy
from PySide2 import QtWidgets
from PySide2.shiboken2 import wrapInstance


class SkeletonTreeView(QtWidgets.QGroupBox):

    def __init__(self, parent=None):
        super(SkeletonTreeView, self).__init__(parent)

        # Create and setup the tree widget
        self._treeWidget = QtWidgets.QTreeWidget()
        self._treeWidget.setHeaderHidden(True)
        self._treeWidget.setColumnCount(1)
        # The following is need to show a horizontal scrollbar
        self._treeWidget.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
        self._treeWidget.header().setStretchLastSection(False)

        # Signal callback methods
        self._treeWidget.itemClicked.connect(lambda x: print(x.text(0)))

        # Configure the Group Box parent wdiget
        self.setTitle("Avatars")
        self.setLayout(QtWidgets.QVBoxLayout())
        self.setStyleSheet("QGroupBox  {color: #a2ec13} QTreeView{border:none}")
        self.layout().addWidget(self._treeWidget)

    def __default_tree_widget_item(self, parent, obj):
        item = QtWidgets.QTreeWidgetItem(parent)
        item.setText(0, obj.GetName())
        item.setExpanded(True)
        return item

    # Recursive loop through the hierarchy.
    def __hierarchy_walk_down(self, parent_widget, node, root_node):
        children = node.GetChildren()
        __parent_widget = self.__default_tree_widget_item(parent_widget, node)
        for child in children:
            self.__hierarchy_walk_down(__parent_widget, child, root_node)

    def refresh(self):
        self._treeWidget.clear()
        avatars = RLPy.RScene.FindObjects(RLPy.EObjectType_Avatar)

        for avatar in avatars:
            rootBone = avatar.GetSkeletonComponent().GetRootBone()
            if rootBone is not None:
                parent_item = self.__default_tree_widget_item(self._treeWidget, avatar)
                self.__hierarchy_walk_down(parent_item, rootBone, avatar)


# Create a dialog window
dialog = RLPy.RUi.CreateRDialog()
dialog.SetWindowTitle("Bone Hierarchies")

# Create Pyside layout for RDialog
qt_dialog = wrapInstance(int(dialog.GetWindow()), QtWidgets.QDialog)
main_widget = QtWidgets.QWidget()
qt_dialog.setFixedWidth(350)
qt_dialog.setFixedHeight(600)

# Create a skeleton tree
tree_widget = SkeletonTreeView()
qt_dialog.layout().addWidget(tree_widget)
tree_widget.refresh()

# Add a refresh button
refresh_button = QtWidgets.QPushButton()
refresh_button.setFixedHeight(24)
refresh_button.setText("Refresh Tree List")
refresh_button.clicked.connect(lambda: tree_widget.refresh())
qt_dialog.layout().addWidget(refresh_button)

dialog.Show()

APIs Used

You can research the following references for the APIs deployed in this code.