Difference between revisions of "IC Python API:Linked Controls"

From Reallusion Wiki!
Jump to: navigation, search
(Created page with "{{TOC}} {{Parent|IC_Python_API:RL_Python_Samples|RL Python Samples}} Making a custom interface to drive common object properties such as transform values is a commonplace ope...")
 
m (Update Custom User Controls)
(16 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
{{TOC}}
 
{{TOC}}
 
{{Parent|IC_Python_API:RL_Python_Samples|RL Python Samples}}
 
{{Parent|IC_Python_API:RL_Python_Samples|RL Python Samples}}
 +
 +
[[File: Ic_python_api_linked_controls_01.png| frame]]
  
 
Making a custom interface to drive common object properties such as transform values is a commonplace operation.  Problems arise however, when custom interfaces come in conflict with iClone's native controls implementation.  To resolve this conflict of multiple operators, the custom controls much be in sync with the native controls and vice versa via a scripted link.
 
Making a custom interface to drive common object properties such as transform values is a commonplace operation.  Problems arise however, when custom interfaces come in conflict with iClone's native controls implementation.  To resolve this conflict of multiple operators, the custom controls much be in sync with the native controls and vice versa via a scripted link.
 +
 +
Linked interactions happen in 3 ways:
 +
 +
# Commands issued from the script in the form of custom user controls first updates the data model in the scene (such as prop transformations) and iClone handles the rest by updating the native UI parameters with no knowledge of the script's existence.  This form of interaction is the simplest and most straightforward of the three.
 +
# Changes to the data model are reflected back onto native UI parameters as well as any receiving linked custom user controls instantiated by scripts.  Since data updates are written to the custom user controls which are, in turn, linked to the data model; the signals back to the data model must be blocked to prevent a double update to the data model and the native controls.
 +
# Changes to the native controls drive changes to the data model which, in turn, drive the linked custom user controls instantiated by scripts. Similar to the mechanism mentioned above, the returning signal must be blocked to prevent a double update to the data model and native controls.
 +
 +
One can derive a unified premise for the aforementioned scenarios into one simple rule: Returning signals on all data model updates to the script must be blocked to prevent double updates to the data model.
 +
 +
== Required Modules and Global Variables ==
 +
 +
Besides the fundamental Reallusion Python module, we'll also need '''Pyside2''' and '''os''' to read the QT UI file and build the user interface.  We'll also need some global variables to house our UI and callback events we'll need to link the custom user controls with those of iClone.
 +
 +
<syntaxhighlight lang="python">
 +
import RLPy
 +
import os
 +
from PySide2 import *
 +
from PySide2.shiboken2 import wrapInstance
 +
 +
ui = {}  # User interface globals
 +
events = {}  # Globals for events and callbacks
 +
</syntaxhighlight>
 +
 +
== Event Callback ==
 +
 +
In order to sync the custom user controls with the data model, we'll need to receive event triggers on object data and selection change and tie it to an update function.  We do this by inheriting and configuring the RLPy.REventCallback class.
 +
 +
<syntaxhighlight lang="python">
 +
class EventCallback(RLPy.REventCallback):
 +
    def __init__(self):
 +
        RLPy.REventCallback.__init__(self)
 +
 +
    def OnObjectDataChanged(self):
 +
        update_interface()
 +
 +
    def OnObjectSelectionChanged(self):
 +
        update_interface()
 +
</syntaxhighlight>
 +
 +
== Dialog Event Callback ==
 +
 +
Event callbacks are global values, therefore they tend to persist even when the user is not longer using the script. This can quickly pollute the environment with wasteful scripted calculations that can chew up memory and CPU cycles.  Therefore, we'll need a dialog event callback to discard current event callbacks created by this script and clean up the global variables.
 +
 +
<syntaxhighlight lang="python">
 +
class DialogEventCallback(RLPy.RDialogCallback):
 +
    def __init__(self):
 +
        RLPy.RDialogCallback.__init__(self)
 +
 +
    def OnDialogClose(self):
 +
        global events
 +
 +
        RLPy.REventHandler.UnregisterCallback(events["event_callback_id"])
 +
 +
        # Clear all global variables.
 +
        ui.clear()
 +
        events.clear()
 +
        return True
 +
</syntaxhighlight>
 +
 +
== Update Prop (Data Model) ==
 +
 +
Now, we'll need a method to write to the data model from our custom user controls.  We can configure this method with an attribute parameter and a given value at the time of the control's creation.
 +
 +
<syntaxhighlight lang="python">
 +
def update_prop(parameter, value):
 +
    items = RLPy.RScene.GetSelectedObjects()
 +
 +
    if len(items) > 0:
 +
        object_type = items[0].GetType()
 +
 +
        if object_type == RLPy.EObjectType_Prop:
 +
            control = items[0].GetControl("Transform")
 +
            db = control.GetDataBlock()
 +
            db.SetData(parameter, RLPy.RGlobal.GetTime(), RLPy.RVariant(value))
 +
 +
    # Force update on the Modify panel
 +
    RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() + RLPy.RTime(1))
 +
    RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() - RLPy.RTime(1))
 +
</syntaxhighlight>
 +
 +
== Update Custom User Controls ==
 +
 +
Another method is needed to update our custom user controls based on the changes in the data model of interest. We can then tie this method as the event callback triggered by object data and selection change.
 +
 +
<syntaxhighlight lang="python">
 +
def update_interface():
 +
    global ui
 +
 +
    items = RLPy.RScene.GetSelectedObjects()
 +
 +
    if len(items) > 0:
 +
        object_type = items[0].GetType()
 +
 +
        if object_type == RLPy.EObjectType_Prop:
 +
            ui["dialog_window"].SetWindowTitle(items[0].GetName())
 +
            ct = RLPy.RGlobal.GetTime()
 +
            control = items[0].GetControl("Transform")
 +
            db = control.GetDataBlock()
 +
            ui["widget"].move_x.setValue(db.GetData("Position/PositionX", ct).ToFloat())
 +
            ui["widget"].move_y.setValue(db.GetData("Position/PositionY", ct).ToFloat())
 +
            ui["widget"].move_z.setValue(db.GetData("Position/PositionZ", ct).ToFloat())
 +
            ui["widget"].rotate_x.setValue(db.GetData("Rotation/RotationX", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
 +
            ui["widget"].rotate_y.setValue(db.GetData("Rotation/RotationY", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
 +
            ui["widget"].rotate_z.setValue(db.GetData("Rotation/RotationZ", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
 +
            ui["widget"].scale_x.setValue(db.GetData("Scale/ScaleX", ct).ToFloat() * 100)
 +
            ui["widget"].scale_y.setValue(db.GetData("Scale/ScaleY", ct).ToFloat() * 100)
 +
            ui["widget"].scale_z.setValue(db.GetData("Scale/ScaleZ", ct).ToFloat() * 100)
 +
            ui["widget"].widget.setEnabled(True)
 +
            return
 +
 +
    ui["dialog_window"].SetWindowTitle("No Prop Selected")
 +
    for w in [ui["widget"].move_x, ui["widget"].move_y, ui["widget"].move_z,
 +
              ui["widget"].rotate_x, ui["widget"].rotate_y, ui["widget"].rotate_z,
 +
              ui["widget"].scale_x, ui["widget"].scale_y, ui["widget"].scale_z]:
 +
        w.setValue(0)
 +
    ui["widget"].widget.setEnabled(False)
 +
</syntaxhighlight>
 +
 +
Notice that the window title is cleared of the prop name and the user controls reset to zero when the current selection is cleared.
 +
 +
== Creating the UI ==
 +
 +
<syntaxhighlight lang="python">
 +
def run_script():
 +
    global ui, events
 +
 +
    ui["dialog_window"] = RLPy.RUi.CreateRDialog()
 +
    ui["dialog_window"].SetWindowTitle("No Prop Selected")
 +
 +
    events["dialog_event_callback"] = DialogEventCallback()
 +
    ui["dialog_window"].RegisterEventCallback(events["dialog_event_callback"])
 +
 +
    dialog = wrapInstance(int(ui["dialog_window"].GetWindow()), QtWidgets.QDialog)
 +
    dialog.setFixedWidth(350)
 +
 +
    qt_ui_file = QtCore.QFile(os.path.dirname(__file__) + "/Linked_Controls.ui")
 +
    qt_ui_file.open(QtCore.QFile.ReadOnly)
 +
    ui["widget"] = QtUiTools.QUiLoader().load(qt_ui_file)
 +
    qt_ui_file.close()
 +
 +
    dialog.layout().addWidget(ui["widget"])
 +
 +
    ui["dialog_window"].Show()
 +
 +
    events["event_callback"] = EventCallback()
 +
    events["event_callback_id"] = RLPy.REventHandler.RegisterCallback(events["event_callback"])
 +
 +
    ui["widget"].move_x.valueChanged.connect(lambda x: update_prop("Position/PositionX", x))
 +
    ui["widget"].move_y.valueChanged.connect(lambda x: update_prop("Position/PositionY", x))
 +
    ui["widget"].move_z.valueChanged.connect(lambda x: update_prop("Position/PositionZ", x))
 +
    ui["widget"].rotate_x.valueChanged.connect(lambda x: update_prop("Rotation/RotationX", x * RLPy.RMath.CONST_DEG_TO_RAD))
 +
    ui["widget"].rotate_y.valueChanged.connect(lambda x: update_prop("Rotation/RotationY", x * RLPy.RMath.CONST_DEG_TO_RAD))
 +
    ui["widget"].rotate_z.valueChanged.connect(lambda x: update_prop("Rotation/RotationZ", x * RLPy.RMath.CONST_DEG_TO_RAD))
 +
    ui["widget"].scale_x.valueChanged.connect(lambda x: update_prop("Scale/ScaleX", x * 0.01))
 +
    ui["widget"].scale_y.valueChanged.connect(lambda x: update_prop("Scale/ScaleY", x * 0.01))
 +
    ui["widget"].scale_z.valueChanged.connect(lambda x: update_prop("Scale/ScaleZ", x * 0.01))
 +
 +
    update_interface()
 +
</syntaxhighlight>
 +
 +
== Everything Put Together ==
 +
 +
You can copy and paste the following code into a PY file and load it into iClone via Script > Load Python.
 +
 +
<syntaxhighlight lang="python">
 +
import RLPy
 +
import os
 +
from PySide2 import *
 +
from PySide2.shiboken2 import wrapInstance
 +
 +
ui = {}  # User interface globals
 +
events = {}  # Globals for events and callbacks
 +
 +
 +
class EventCallback(RLPy.REventCallback):
 +
    def __init__(self):
 +
        RLPy.REventCallback.__init__(self)
 +
 +
    def OnObjectDataChanged(self):
 +
        update_interface()
 +
 +
    def OnObjectSelectionChanged(self):
 +
        update_interface()
 +
 +
 +
class DialogEventCallback(RLPy.RDialogCallback):
 +
    def __init__(self):
 +
        RLPy.RDialogCallback.__init__(self)
 +
 +
    def OnDialogClose(self):
 +
        global events
 +
 +
        RLPy.REventHandler.UnregisterCallback(events["event_callback_id"])
 +
 +
        # Clear all global variables.
 +
        ui.clear()
 +
        events.clear()
 +
        return True
 +
 +
 +
def update_prop(parameter, value):
 +
    items = RLPy.RScene.GetSelectedObjects()
 +
 +
    if len(items) > 0:
 +
        object_type = items[0].GetType()
 +
 +
        if object_type == RLPy.EObjectType_Prop:
 +
            control = items[0].GetControl("Transform")
 +
            db = control.GetDataBlock()
 +
            db.SetData(parameter, RLPy.RGlobal.GetTime(), RLPy.RVariant(value))
 +
 +
    # Force update on the Modify panel
 +
    RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() + RLPy.RTime(1))
 +
    RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() - RLPy.RTime(1))
 +
 +
 +
def update_interface():
 +
    global ui
 +
 +
    items = RLPy.RScene.GetSelectedObjects()
 +
 +
    if len(items) > 0:
 +
        object_type = items[0].GetType()
 +
 +
        if object_type == RLPy.EObjectType_Prop:
 +
            ui["dialog_window"].SetWindowTitle(items[0].GetName())
 +
            ct = RLPy.RGlobal.GetTime()
 +
            control = items[0].GetControl("Transform")
 +
            db = control.GetDataBlock()
 +
            ui["widget"].move_x.setValue(db.GetData("Position/PositionX", ct).ToFloat())
 +
            ui["widget"].move_y.setValue(db.GetData("Position/PositionY", ct).ToFloat())
 +
            ui["widget"].move_z.setValue(db.GetData("Position/PositionZ", ct).ToFloat())
 +
            ui["widget"].rotate_x.setValue(db.GetData("Rotation/RotationX", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
 +
            ui["widget"].rotate_y.setValue(db.GetData("Rotation/RotationY", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
 +
            ui["widget"].rotate_z.setValue(db.GetData("Rotation/RotationZ", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
 +
            ui["widget"].scale_x.setValue(db.GetData("Scale/ScaleX", ct).ToFloat() * 100)
 +
            ui["widget"].scale_y.setValue(db.GetData("Scale/ScaleY", ct).ToFloat() * 100)
 +
            ui["widget"].scale_z.setValue(db.GetData("Scale/ScaleZ", ct).ToFloat() * 100)
 +
            ui["widget"].widget.setEnabled(True)
 +
            return
 +
 +
    ui["dialog_window"].SetWindowTitle("No Prop Selected")
 +
    for w in [ui["widget"].move_x, ui["widget"].move_y, ui["widget"].move_z,
 +
              ui["widget"].rotate_x, ui["widget"].rotate_y, ui["widget"].rotate_z,
 +
              ui["widget"].scale_x, ui["widget"].scale_y, ui["widget"].scale_z]:
 +
        w.setValue(0)
 +
    ui["widget"].widget.setEnabled(False)
 +
 +
 +
def run_script():
 +
    global ui, events
 +
 +
    ui["dialog_window"] = RLPy.RUi.CreateRDialog()
 +
    ui["dialog_window"].SetWindowTitle("No Prop Selected")
 +
 +
    events["dialog_event_callback"] = DialogEventCallback()
 +
    ui["dialog_window"].RegisterEventCallback(events["dialog_event_callback"])
 +
 +
    dialog = wrapInstance(int(ui["dialog_window"].GetWindow()), QtWidgets.QDialog)
 +
    dialog.setFixedWidth(350)
 +
 +
    qt_ui_file = QtCore.QFile(os.path.dirname(__file__) + "/Linked_Controls.ui")
 +
    qt_ui_file.open(QtCore.QFile.ReadOnly)
 +
    ui["widget"] = QtUiTools.QUiLoader().load(qt_ui_file)
 +
    qt_ui_file.close()
 +
 +
    dialog.layout().addWidget(ui["widget"])
 +
 +
    ui["dialog_window"].Show()
 +
 +
    events["event_callback"] = EventCallback()
 +
    events["event_callback_id"] = RLPy.REventHandler.RegisterCallback(events["event_callback"])
 +
 +
    ui["widget"].move_x.valueChanged.connect(lambda x: update_prop("Position/PositionX", x))
 +
    ui["widget"].move_y.valueChanged.connect(lambda x: update_prop("Position/PositionY", x))
 +
    ui["widget"].move_z.valueChanged.connect(lambda x: update_prop("Position/PositionZ", x))
 +
    ui["widget"].rotate_x.valueChanged.connect(lambda x: update_prop("Rotation/RotationX", x * RLPy.RMath.CONST_DEG_TO_RAD))
 +
    ui["widget"].rotate_y.valueChanged.connect(lambda x: update_prop("Rotation/RotationY", x * RLPy.RMath.CONST_DEG_TO_RAD))
 +
    ui["widget"].rotate_z.valueChanged.connect(lambda x: update_prop("Rotation/RotationZ", x * RLPy.RMath.CONST_DEG_TO_RAD))
 +
    ui["widget"].scale_x.valueChanged.connect(lambda x: update_prop("Scale/ScaleX", x * 0.01))
 +
    ui["widget"].scale_y.valueChanged.connect(lambda x: update_prop("Scale/ScaleY", x * 0.01))
 +
    ui["widget"].scale_z.valueChanged.connect(lambda x: update_prop("Scale/ScaleZ", x * 0.01))
 +
 +
    update_interface()
 +
</syntaxhighlight>
 +
 +
== APIs Used ==
 +
 +
You can research the following references for the APIs deployed in this code.
 +
 +
=== Linked_Control.py ===
 +
<div style="column-count:4; -moz-column-count:4; -webkit-column-count:4">
 +
* [[ IC_Python_API:RLPy_REventCallback#__init__ | RLPy.REventCallback.__init__() ]]
 +
* [[ IC_Python_API:RLPy_RDialogCallback#__init__ | RLPy.RDialogCallback.__init__() ]]
 +
* [[ IC_Python_API:RLPy_REventHandler#UnregisterCallback | RLPy.REventHandler.UnregisterCallback() ]]
 +
* [[ IC_Python_API:RLPy_RScene#GetSelectedObjects | RLPy.RScene.GetSelectedObjects() ]]
 +
* [[ IC_Python_API:RLPy_RGlobal#GetTime | RLPy.RGlobal.GetTime() ]]
 +
* [[ IC_Python_API:RLPy_RVariant | RLPy.RVariant() ]]
 +
* [[ IC_Python_API:RLPy_RGlobal#SetTime | RLPy.RGlobal.SetTime() ]]
 +
* [[ IC_Python_API:RLPy_RTime | RLPy.RTime() ]]
 +
* [[ IC_Python_API:RLPy_RUi#CreateRDialog | RLPy.RUi.CreateRDialog() ]]
 +
* [[ IC_Python_API:RLPy_REventHandler#RegisterCallback | RLPy.REventHandler.RegisterCallback() ]]
 +
</div>

Revision as of 22:34, 15 August 2019

Main article: RL Python Samples.
Ic python api linked controls 01.png

Making a custom interface to drive common object properties such as transform values is a commonplace operation. Problems arise however, when custom interfaces come in conflict with iClone's native controls implementation. To resolve this conflict of multiple operators, the custom controls much be in sync with the native controls and vice versa via a scripted link.

Linked interactions happen in 3 ways:

  1. Commands issued from the script in the form of custom user controls first updates the data model in the scene (such as prop transformations) and iClone handles the rest by updating the native UI parameters with no knowledge of the script's existence. This form of interaction is the simplest and most straightforward of the three.
  2. Changes to the data model are reflected back onto native UI parameters as well as any receiving linked custom user controls instantiated by scripts. Since data updates are written to the custom user controls which are, in turn, linked to the data model; the signals back to the data model must be blocked to prevent a double update to the data model and the native controls.
  3. Changes to the native controls drive changes to the data model which, in turn, drive the linked custom user controls instantiated by scripts. Similar to the mechanism mentioned above, the returning signal must be blocked to prevent a double update to the data model and native controls.

One can derive a unified premise for the aforementioned scenarios into one simple rule: Returning signals on all data model updates to the script must be blocked to prevent double updates to the data model.

Required Modules and Global Variables

Besides the fundamental Reallusion Python module, we'll also need Pyside2 and os to read the QT UI file and build the user interface. We'll also need some global variables to house our UI and callback events we'll need to link the custom user controls with those of iClone.

import RLPy
import os
from PySide2 import *
from PySide2.shiboken2 import wrapInstance

ui = {}  # User interface globals
events = {}  # Globals for events and callbacks

Event Callback

In order to sync the custom user controls with the data model, we'll need to receive event triggers on object data and selection change and tie it to an update function. We do this by inheriting and configuring the RLPy.REventCallback class.

class EventCallback(RLPy.REventCallback):
    def __init__(self):
        RLPy.REventCallback.__init__(self)

    def OnObjectDataChanged(self):
        update_interface()

    def OnObjectSelectionChanged(self):
        update_interface()

Dialog Event Callback

Event callbacks are global values, therefore they tend to persist even when the user is not longer using the script. This can quickly pollute the environment with wasteful scripted calculations that can chew up memory and CPU cycles. Therefore, we'll need a dialog event callback to discard current event callbacks created by this script and clean up the global variables.

class DialogEventCallback(RLPy.RDialogCallback):
    def __init__(self):
        RLPy.RDialogCallback.__init__(self)

    def OnDialogClose(self):
        global events

        RLPy.REventHandler.UnregisterCallback(events["event_callback_id"])

        # Clear all global variables.
        ui.clear()
        events.clear()
        return True

Update Prop (Data Model)

Now, we'll need a method to write to the data model from our custom user controls. We can configure this method with an attribute parameter and a given value at the time of the control's creation.

def update_prop(parameter, value):
    items = RLPy.RScene.GetSelectedObjects()

    if len(items) > 0:
        object_type = items[0].GetType()

        if object_type == RLPy.EObjectType_Prop:
            control = items[0].GetControl("Transform")
            db = control.GetDataBlock()
            db.SetData(parameter, RLPy.RGlobal.GetTime(), RLPy.RVariant(value))

    # Force update on the Modify panel
    RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() + RLPy.RTime(1))
    RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() - RLPy.RTime(1))

Update Custom User Controls

Another method is needed to update our custom user controls based on the changes in the data model of interest. We can then tie this method as the event callback triggered by object data and selection change.

def update_interface():
    global ui

    items = RLPy.RScene.GetSelectedObjects()

    if len(items) > 0:
        object_type = items[0].GetType()

        if object_type == RLPy.EObjectType_Prop:
            ui["dialog_window"].SetWindowTitle(items[0].GetName())
            ct = RLPy.RGlobal.GetTime()
            control = items[0].GetControl("Transform")
            db = control.GetDataBlock()
            ui["widget"].move_x.setValue(db.GetData("Position/PositionX", ct).ToFloat())
            ui["widget"].move_y.setValue(db.GetData("Position/PositionY", ct).ToFloat())
            ui["widget"].move_z.setValue(db.GetData("Position/PositionZ", ct).ToFloat())
            ui["widget"].rotate_x.setValue(db.GetData("Rotation/RotationX", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
            ui["widget"].rotate_y.setValue(db.GetData("Rotation/RotationY", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
            ui["widget"].rotate_z.setValue(db.GetData("Rotation/RotationZ", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
            ui["widget"].scale_x.setValue(db.GetData("Scale/ScaleX", ct).ToFloat() * 100)
            ui["widget"].scale_y.setValue(db.GetData("Scale/ScaleY", ct).ToFloat() * 100)
            ui["widget"].scale_z.setValue(db.GetData("Scale/ScaleZ", ct).ToFloat() * 100)
            ui["widget"].widget.setEnabled(True)
            return

    ui["dialog_window"].SetWindowTitle("No Prop Selected")
    for w in [ui["widget"].move_x, ui["widget"].move_y, ui["widget"].move_z,
              ui["widget"].rotate_x, ui["widget"].rotate_y, ui["widget"].rotate_z,
              ui["widget"].scale_x, ui["widget"].scale_y, ui["widget"].scale_z]:
        w.setValue(0)
    ui["widget"].widget.setEnabled(False)

Notice that the window title is cleared of the prop name and the user controls reset to zero when the current selection is cleared.

Creating the UI

def run_script():
    global ui, events

    ui["dialog_window"] = RLPy.RUi.CreateRDialog()
    ui["dialog_window"].SetWindowTitle("No Prop Selected")

    events["dialog_event_callback"] = DialogEventCallback()
    ui["dialog_window"].RegisterEventCallback(events["dialog_event_callback"])

    dialog = wrapInstance(int(ui["dialog_window"].GetWindow()), QtWidgets.QDialog)
    dialog.setFixedWidth(350)

    qt_ui_file = QtCore.QFile(os.path.dirname(__file__) + "/Linked_Controls.ui")
    qt_ui_file.open(QtCore.QFile.ReadOnly)
    ui["widget"] = QtUiTools.QUiLoader().load(qt_ui_file)
    qt_ui_file.close()

    dialog.layout().addWidget(ui["widget"])

    ui["dialog_window"].Show()

    events["event_callback"] = EventCallback()
    events["event_callback_id"] = RLPy.REventHandler.RegisterCallback(events["event_callback"])

    ui["widget"].move_x.valueChanged.connect(lambda x: update_prop("Position/PositionX", x))
    ui["widget"].move_y.valueChanged.connect(lambda x: update_prop("Position/PositionY", x))
    ui["widget"].move_z.valueChanged.connect(lambda x: update_prop("Position/PositionZ", x))
    ui["widget"].rotate_x.valueChanged.connect(lambda x: update_prop("Rotation/RotationX", x * RLPy.RMath.CONST_DEG_TO_RAD))
    ui["widget"].rotate_y.valueChanged.connect(lambda x: update_prop("Rotation/RotationY", x * RLPy.RMath.CONST_DEG_TO_RAD))
    ui["widget"].rotate_z.valueChanged.connect(lambda x: update_prop("Rotation/RotationZ", x * RLPy.RMath.CONST_DEG_TO_RAD))
    ui["widget"].scale_x.valueChanged.connect(lambda x: update_prop("Scale/ScaleX", x * 0.01))
    ui["widget"].scale_y.valueChanged.connect(lambda x: update_prop("Scale/ScaleY", x * 0.01))
    ui["widget"].scale_z.valueChanged.connect(lambda x: update_prop("Scale/ScaleZ", x * 0.01))

    update_interface()

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
import os
from PySide2 import *
from PySide2.shiboken2 import wrapInstance

ui = {}  # User interface globals
events = {}  # Globals for events and callbacks


class EventCallback(RLPy.REventCallback):
    def __init__(self):
        RLPy.REventCallback.__init__(self)

    def OnObjectDataChanged(self):
        update_interface()

    def OnObjectSelectionChanged(self):
        update_interface()


class DialogEventCallback(RLPy.RDialogCallback):
    def __init__(self):
        RLPy.RDialogCallback.__init__(self)

    def OnDialogClose(self):
        global events

        RLPy.REventHandler.UnregisterCallback(events["event_callback_id"])

        # Clear all global variables.
        ui.clear()
        events.clear()
        return True


def update_prop(parameter, value):
    items = RLPy.RScene.GetSelectedObjects()

    if len(items) > 0:
        object_type = items[0].GetType()

        if object_type == RLPy.EObjectType_Prop:
            control = items[0].GetControl("Transform")
            db = control.GetDataBlock()
            db.SetData(parameter, RLPy.RGlobal.GetTime(), RLPy.RVariant(value))

    # Force update on the Modify panel
    RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() + RLPy.RTime(1))
    RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() - RLPy.RTime(1))


def update_interface():
    global ui

    items = RLPy.RScene.GetSelectedObjects()

    if len(items) > 0:
        object_type = items[0].GetType()

        if object_type == RLPy.EObjectType_Prop:
            ui["dialog_window"].SetWindowTitle(items[0].GetName())
            ct = RLPy.RGlobal.GetTime()
            control = items[0].GetControl("Transform")
            db = control.GetDataBlock()
            ui["widget"].move_x.setValue(db.GetData("Position/PositionX", ct).ToFloat())
            ui["widget"].move_y.setValue(db.GetData("Position/PositionY", ct).ToFloat())
            ui["widget"].move_z.setValue(db.GetData("Position/PositionZ", ct).ToFloat())
            ui["widget"].rotate_x.setValue(db.GetData("Rotation/RotationX", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
            ui["widget"].rotate_y.setValue(db.GetData("Rotation/RotationY", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
            ui["widget"].rotate_z.setValue(db.GetData("Rotation/RotationZ", ct).ToFloat() * RLPy.RMath.CONST_RAD_TO_DEG)
            ui["widget"].scale_x.setValue(db.GetData("Scale/ScaleX", ct).ToFloat() * 100)
            ui["widget"].scale_y.setValue(db.GetData("Scale/ScaleY", ct).ToFloat() * 100)
            ui["widget"].scale_z.setValue(db.GetData("Scale/ScaleZ", ct).ToFloat() * 100)
            ui["widget"].widget.setEnabled(True)
            return

    ui["dialog_window"].SetWindowTitle("No Prop Selected")
    for w in [ui["widget"].move_x, ui["widget"].move_y, ui["widget"].move_z,
              ui["widget"].rotate_x, ui["widget"].rotate_y, ui["widget"].rotate_z,
              ui["widget"].scale_x, ui["widget"].scale_y, ui["widget"].scale_z]:
        w.setValue(0)
    ui["widget"].widget.setEnabled(False)


def run_script():
    global ui, events

    ui["dialog_window"] = RLPy.RUi.CreateRDialog()
    ui["dialog_window"].SetWindowTitle("No Prop Selected")

    events["dialog_event_callback"] = DialogEventCallback()
    ui["dialog_window"].RegisterEventCallback(events["dialog_event_callback"])

    dialog = wrapInstance(int(ui["dialog_window"].GetWindow()), QtWidgets.QDialog)
    dialog.setFixedWidth(350)

    qt_ui_file = QtCore.QFile(os.path.dirname(__file__) + "/Linked_Controls.ui")
    qt_ui_file.open(QtCore.QFile.ReadOnly)
    ui["widget"] = QtUiTools.QUiLoader().load(qt_ui_file)
    qt_ui_file.close()

    dialog.layout().addWidget(ui["widget"])

    ui["dialog_window"].Show()

    events["event_callback"] = EventCallback()
    events["event_callback_id"] = RLPy.REventHandler.RegisterCallback(events["event_callback"])

    ui["widget"].move_x.valueChanged.connect(lambda x: update_prop("Position/PositionX", x))
    ui["widget"].move_y.valueChanged.connect(lambda x: update_prop("Position/PositionY", x))
    ui["widget"].move_z.valueChanged.connect(lambda x: update_prop("Position/PositionZ", x))
    ui["widget"].rotate_x.valueChanged.connect(lambda x: update_prop("Rotation/RotationX", x * RLPy.RMath.CONST_DEG_TO_RAD))
    ui["widget"].rotate_y.valueChanged.connect(lambda x: update_prop("Rotation/RotationY", x * RLPy.RMath.CONST_DEG_TO_RAD))
    ui["widget"].rotate_z.valueChanged.connect(lambda x: update_prop("Rotation/RotationZ", x * RLPy.RMath.CONST_DEG_TO_RAD))
    ui["widget"].scale_x.valueChanged.connect(lambda x: update_prop("Scale/ScaleX", x * 0.01))
    ui["widget"].scale_y.valueChanged.connect(lambda x: update_prop("Scale/ScaleY", x * 0.01))
    ui["widget"].scale_z.valueChanged.connect(lambda x: update_prop("Scale/ScaleZ", x * 0.01))

    update_interface()

APIs Used

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

Linked_Control.py