Bootstrap Chameleon Logo

下一步,动态创建

在之前的很长一段时间,一只有小伙伴和开发者问我,“TAPython是否能够动态添加控件”。在之前的TAPython中,我们可以在打开工具时重新创建界面,或者使用ExternalJSON功能实现“一定程度”的动态创建。因此我对“为TAPython通过代码添加控件”这件事,并没有那么积极,其实在内心我对这个功能是有一些担忧的。

担忧

首先,TAPython的目标用户,不仅有资深程序员,也有美术和策划。

对于程序员来说,通过Python代码实现界面和逻辑是非常自然的事情,单独分离的JSON文件对于他们来说,甚至可以是多余的。但对于非程序用户来说,用分离的JSON文件来定义节目会极大程度的简化工具的复杂程度,界面JSON中的每一行和界面一一对应,界面外观和点击逻辑在JSON文件中就如同直接写在纸面上一样直接;而“通过代码来创建界面”就又给他们添加了一个“界面创建”的技术负担。 在我看来,如果TAPython的目标是将做工具这件事变得尽可能的简单,那么将“走出第一步”所需要知道的上下文降低到最小程度,无疑是最重要的。

现在,TAPython已经相对比较成熟,使用JSON来描述界面已经成为一个底层习惯。那么是时候为TAPython添加动态添加,移除控件的功能了。

Examples

下图的Gif示例和下面的API,都可以在 https://github.com/cgerchenhp/TAPython_ButtonDivision_Example 中找到。

Gif showing a demo splinter buttons of dynamic widget creation

Dynamic Slate Creation

下面API中使用的JSON内容设置控件或Slot中的内容。

TIP
JSON中的内容为以'{''}'开始和结束,内容为一个完整的Slate控件组件的内容

Gif showing dynamic creation of SWidget through python

下面提到的API和例子,在DynamicSlateExamples中都有对应的控件内容。

设置单个控件的内容

Slate中有一些控件有且只能有一个子控件,在这些控件的JSON文件中,我们会通过Content字段来设置其中的子空间内容。

同样,通过chameleon_data_instance.set_content_from_json, 我们可以动态的为这些控件添加子控件。

支持的控件有:

  • SBox
  • SBorder
  • SCheckBox
  • SBox
  • SButton

例如,我们有一个SBorder控件:

DynamicSlateExamples.json

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

通过self.data.set_content_from_json(aka_name='a_named_sborder', json_content='{ "SButton": { "Text": "A New Button" } }') 可以为这个SBorder添加一个按钮作为子控件。其中变量json_content中的内容为描述子控件内容的字符串内容。其格式与界面JSON文件中的相同。

除了之间编写字符串内容以外,我们也可以json.dumps将对象dump为字符串格式,通过这种方法,设置控件的属性会更为方便。下面的json_content

import json

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

设置Slots中的内容

和上面说的set_content_from_json类似,我们可以通过append_slot_from_jsoninsert_slot_from_json来动态的添加Slot中的控件。

append_slot_from_json 支持的控件有:

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

TIP
SUniformGridPanelSUniformGridPanel添加控件时,需要指定该函数中第三和第四个参数ColumnRow,(默认均为0)。多次添加会使得控件重叠。

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

#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 支持的控件有:

  • SHorizontalBox
  • SVerticalBox

其中最常用的是SHorizontalBoxSVerticalBox,我们可以通过insert_slot_from_json在指定的Slot位置添加子控件。例如:

# 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
上述例子中的self.ui_widget_name,均为控件的Aka名称

移除指定的控件

我们可以通过remove_widget_at来移除指定的控件。上述两个API中提到的所有控件:

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

例如我们可以通过下面的代码来移除SVerticalBox控件中的第一个子控件:

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

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

获取当前控件中所有已用Aka

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

我们可以通过chameleon_data_instance.get_all_akas来获取当前控件中所有已用的Aka名称。例如:

all_akas = self.data.get_all_akas()

获取指定Aka名的控件所在路径

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

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}")

本文提到的内容的实例代码可以在下面的链接中找到:https://github.com/cgerchenhp/TAPython_ButtonDivision_Example

下面是我对动态创建控件的一些理解和建议:

建议

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

    例如,我们的控件数量需要按照某个网络服务的返回值的数量动态修改,那么这些控件的动态创建是非常合理和自然的

  • 将需要动态创建的组件合并在一起,并发起一次调用

    当我们要创建一个包含了100个子控件的SHorizontalBox时,应该把包含所有子控件的JOSN代码先在Python端拼接成一段完整的JSON,然后发起1次调用。合并后的创建速度是单独创建的几十倍以上。我们甚至可以简单的认为两者的速度是:“C++的速度和Python的速度之比”。 - 注意控件中的“Aka”必须在每个工具中唯一

    由于我们的控件是通过代码创建的,这个时候很容易出现多个控件适用同一个“Aka”的情况。这对于工具逻辑来说是致命的。

TIP
注意Output中的黄色警告,特别是“Aka”相关的

不要

  • 不要将整个工具的界面代码都用Python来创建,除非你是唯一的工具维护者
    • 当由于“没有正确导入某个package”,使得工具的代码无法执行,进而连界面都无法显示的时候,无疑是非常沮丧的
    • 当其他开发者需要先理解Python中的界面创建逻辑,才知道界面长什么样的时候,无疑更加沮丧
    • 当其他开发者修复了你的工具之后,你发现居然连节目也和以前完全不同了,你会非常沮丧