Bootstrap Chameleon Logo

Next Step, Dynamic Creation

In the past, I have been asked by several developers, "Can TAPython dynamically add widgets?" In the previous TAPython, we can recreate the interface when opening the tool, or use the ExternalJSON function to achieve a "certain degree" of dynamic creation. So I am not so active about "adding widgets to TAPython through code". In fact, I have some concerns about this feature in my heart.

Concerns

First of all, TAPython's target users include not only senior programmers, but also artists and designers.

For programmers, it is very natural to implement UI and logic through Python code, and separate JSON files may even be redundant for them. But for non-program users, using separate JSON files to define programs will greatly simplify the complexity of the tool. Each line in the Slate JSON file corresponds to the UI one by one, and the UI appearance and click logic are as direct as writing directly on paper in the JSON file; and "Create UI through code" adds them again. A "technical burden of UI creation".

In my opinion, if the goal of TAPython is to make the tool as simple as possible, then reducing the context required to "take the first step" to a minimum is undoubtedly the most important.

Now, TAPython is relatively mature, and using JSON to describe the interface has become a low-level habit. Then it's time to add the ability to dynamically add and remove widgets to TAPython. And we can create something cool with this feature.

Examples

The Gif examples in the figure below and the API below can be found at https://github.com/cgerchenhp/TAPython_ButtonDivision_Example.

Gif showing a demo splinter buttons of dynamic widget creation

Dynamic Slate Creation

The JSON content used in the API below sets the content of the widget or slot.

TIP
The content in JSON starts and ends with '{' and '}', and the content is the content of a complete Slate widget component.

Gif showing dynamic creation of SWidget through python

The APIs and examples mentioned below can be found in DynamicSlateExamples

Set the content of a single widget

There are some widgets in Slate that have only one child widget. In the JSON file of these widgets, we use the Content field to set the content of the child space.

Similarly, through chameleon_data_instance.set_content_from_json, we can dynamically add a new child widget to these widgets.

Supported Widgets:

  • SBox
  • SBorder
  • SCheckBox
  • SBox
  • SButton

For example, we have an SBorder widget:

DynamicSlateExamples.json

"SBorder": {
    "Aka": "a_named_sborder",
    "BorderImage": {
        "Style": "FEditorStyle",
        "Brush": "ToolPanel.DarkGroupBorder"
    }
}

Through self.data.set_content_from_json(aka_name='a_named_sborder', json_content='{ "SButton": { "Text": "A New Button" } }'), you can add a button as a child widget to this SBorder. The content of the variable json_content is a string which describing the content of the widget. Its format is the same as that in the UI JSON file.

In addition to writing string content directly, we can also json.dumps the object into a string format. In this way, it will be more convenient to set the properties of the widget.

In the following json_content

import json

new_widget = {"SButton": {}}
new_widget["SButton"]["Text"] = "A New Button"
json_content = json.dumps(new_widget)

Set the content of Slots

Similar to the set_content_from_json mentioned above, we can dynamically add controls in the Slot through append_slot_from_json and insert_slot_from_json.

append_slot_from_json Supported widgets:

  • SHorizontalBox
  • SVerticalBox
  • SScrollBox
  • SGridPanel
  • SUniformGridPanel
  • SUniformWrapPanel
  • SOverlay
  • SCanvas
  • SSplitter

TIP
When adding widgets to SUniformGridPanel and SUniformGridPanel, you need to specify the third and fourth parameters Column and Row in this function (both default to 0). Multiple additions will cause the widgets to overlap.

例如,我们通常会通过类似下面的代码在控件的Slot中添加子控件:

For example, we usually add child widgets to the Slot of the widget through code similar to the following:

#append a new slot to the vertical box
self.data.append_slot_from_json(self.ui_verticalbox, json_content)

# add a new slot to the grid at column 1, row 2
self.data.append_slot_from_json(self.ui_grid, json_content, column=1, row=2)

insert_slot_from_json Supported Widgets:

  • SHorizontalBox
  • SVerticalBox

With the most commonly used are SHorizontalBox and SVerticalBox, we can add child widgets at the specified Slot position through insert_slot_from_json. For example:

# insert a new widget to the vertical box with specified index
self.data.insert_slot_from_json(self.ui_verticalbox, json.dumps(new_widget), index)

TIP
In the above example, self.ui_widget_name is the Aka name of the widget

Remove the specified widget

We can remove the specified widget through remove_widget_at. All widgets mentioned in the above two APIs:

  • SHorizontalBox
  • SVerticalBox
  • SScrollBox
  • SGridPanel
  • SUniformGridPanel
  • SUniformWrapPanel
  • SOverlay
  • SCanvas
  • SSplitter
  • SBox
  • SBorder
  • SCheckBox
  • SBox
  • SButton'

For example, we can remove the first child widget in the SVerticalBox widget through the following code:

self.data.remove_widget_at(self.ui_verticalbox, 0)

当我们的控件时只能有一个子控件的控件时(SBox, SBorder,SCheckBox,SBox,SButton),参数index应为0,其余值无效。

When our widget can only have one child widget (SBox, SBorder, SCheckBox, SBox, SButton), the parameter index should be 0, and the rest are invalid.

Get all used Aka in the current widget

由于我们可以动态创建和添加控件了,因此我们控件的Aka名的来源也不仅限于界面JSON文件中的内容了,也包括python代码添加的控件的Aka名称。

Since we can dynamically create and add widgets, the source of our widget's Aka name is not limited to the content in the UI JSON file, but also includes the Aka name of the widget added by python code.

We can use chameleon_data_instance.get_all_akas to get all the used Aka names in the current chameleon tool. For example:

all_akas = self.data.get_all_akas()

Get the path where the widget with the specified Aka name is located

在获取了控件的Aka名之后,我们可以通过chameleon_data_instance.get_widget_path来获取该控件所在的实际Slate路径。例如下面的函数中,获取和打印了所有控件的Aka名和其所在的界面路径。

After obtaining the Aka name of the widget, we can use chameleon_data_instance.get_widget_path to get the actual Slate path where the widget is located. For example, in the following function, the Aka name of all widgets and their Slate paths are obtained and printed.

def on_button_log_akas(self):
    all_akas = self.data.get_all_akas()
    widget_paths = [self.data.get_widget_path(aka) for aka in all_akas]

    for aka, widget_path in zip(all_akas, widget_paths):
        print(f"Aka: {aka: <30} :{widget_path}")

The example code of the content mentioned in this article can be found in the following link: https://github.com/cgerchenhp/TAPython_ButtonDivision_Example

Here are some of my understanding and suggestions for dynamically creating widgets:

Suggestion

Do

  • 创建数量不可预知的控件

  • Using it for unknown number of widgets

    For example, the number of our widgets needs to be dynamically modified according to the number of return values of a network service, then the dynamic creation of these widgets is very reasonable and natural

  • Merge the components that need to be dynamically created together and call it once

    When we want to create a SHorizontalBox with 100 child widgets, we should first splice the JSON code containing all child widgets on the Python side into a complete JSON, and then initiate a call. The speed of the merged json is more than ten times that of the separate creation. We can even simply think that the speed of the two is: "the ratio of the speed of C++ and the speed of Python".

  • Note that the "Aka" in the widget must be unique in each tool

    Since our controls are created through code, it is easy to have multiple controls that use the same "Aka" at this time by a mistake. But, this is fatal to the tool logic.

TIP
Caution the yellow warning in Output, especially the "Aka" related

Don't

  • Don't use Python to create the entire tool interface code unless you are the only tool maintainer

Here are some of the reasons:

  • When the code of the tool cannot be executed due to "no correct import of a package", and then the UI cannot be displayed, it is undoubtedly very frustrating.

  • When other developers need to understand the interface creation logic in Python before they know what the interface looks like, it is undoubtedly more frustrating.

  • When some developer fix our tool which coding the full UI with python, we find that the program and the UI are completely different from before, we will be very frustrated.