IC Python API:Tree View

From Reallusion Wiki!
Revision as of 19:51, 8 March 2020 by Chuck (RL) (Talk | contribs) (Necessary Modules)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search
Main article: RL Python Samples.
Ic python api bone hierarchies 01.png

The Qt tree widget is a useful UI element 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.

In class-based programming, the factory method pattern is a creation pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created. This is done by creating objects by calling a factory method—either specified in an interface and implemented by child classes, or implemented in a base class and optionally overridden by derived classes—rather than by calling a constructor. 
    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

Finally, we need a way to refresh the tree list, by look for all of the existing avatars in the scene and running the hierarchy walk down method on each and every one.

    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)

Notice that we clear the tree list widget before populating it again, in order to make this method safe for repeated calls.

Creating the Window

Ic python api bone hierarchies 02.png

We'll need to create a window to house this custom tree list widget, initialize it, and tack on a refresh button below the list.

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

Notice that we call the refresh member function after the class has been initialized, to populate the list.

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.