Bootstrap Chameleon Logo

从0开始创建一个重命名工具

在下面的例子里,我们将从零开始创建一个重命名工具。

TIP
在开始之前,强烈推荐安装这里:Better autocomplete 中的介绍,设置好环境,这样所有的代码,不管是Python还是Json,都有自动提示,可以大大提高开发效率。

查看一个有界面的工具的最小例子是什么样子, 可见最小工具范例

目录结构

~~我们的工具目录结构如下图所示,在<Your_UE_Project>/TA/TAPython/Python中存放了我们开发的工具集。默认情况下这里有若干的预置工具,现在不用关心它们,后续可以在Built-in tools查看它们的具体介绍。~~

文件路径结构中,我们有介绍TAPython在Unreal工程中的路径及各文件的作用。现在我们先不关心Plugins目录中的内容,重点关注<Your_UE_Project>\TA\TAPython中的部分

    Your_UE_Project
    ├── Content                         
    ├── ...
    ├── Plugins                        
    └── TA
        └── TAPython       
            ├── Config                     
            ├── Lib
            ├── Python                           # 0) <--- Python根目录
            |    ├── ...                    
            |    └── DemoTool                    # 1) <--- 单个工具目录
            |        ├── init.py    
            |        ├── rename_tool.py          # 2)<--- Python逻辑
            |        └── rename_tool.json        # 3)<--- 界面文件
            └─ UI
                └── MenuConfig.json              # 4)<--- 在这里配置启动工具的菜单项

步骤

1. 创建目录和文件

按照上图的所示的目录结构,创建我们的新工具所在的目录:DemoTool (当然,你完全可以给他换个更合适的名字)。

DemoTool目录中依次创建以下3个文件

  • __init__.py
  • rename_tool.py
  • rename_tool.json

它们分别是Package定义文件,Python工具逻辑文件界面文件

2. 配置启动工具的菜单项

打开MenuConfig.json文件, 在其中的OnToolBarChameleon"items"项的内容中添加一项:

    {
        "name": "Demo Rename Tool",
        "ChameleonTools": "../Python/DemoTool/rename_tool.json"
    },

完成后的样子:

MenuConfig.json

{
    ...
    "OnToolBarChameleon": {
        "name": "Python Chameleon on toolbar",
        "items": [
            {
                "name": "Demo Rename Tool",
                "ChameleonTools": "../Python/DemoTool/rename_tool.json",
            },
            {
                "name": "Minimal Example",
                "ChameleonTools": "../Python/Example/MinimalExample.json",
                "icon": {
                    "style": "ChameleonStyle",
                    "name": "Flash"
                }
            },
            ...
        ]
    }
}        

这样,我们将菜单项和工具进行了绑定。在完成步骤3之后,点击菜单项就会打开我们创建的工具界面。

3. 编辑界面文件

打开新创建的rename_tool.json文件, 添加以下内容:

rename_tool.json

{
    "TabLabel": "Rename Tool Demo",
    "InitTabSize": [380, 200],
    "InitTabPosition": [200, 200],
    "InitPyCmd": "",
    "Root":
    {

    }
}

要点:

  • "TabLab"定义了工具Tab的标题名
  • "InitTabSize""InitTabPosition" 分别定义了工具的初始尺寸和位置
  • "Root" 则定义了面板中的控件内容,现阶段为空
  • "InitPyCmd" 用于该工具的Python代码的初始化,暂时留空,将在步骤

此时点击步骤2中创建的菜单, 我们就将打开如下的一个工具面板:

Empty rename tool, the starting point of the tool

完善中的界面内容

rename_tool.json(NOT COMPLETED)

{
    "TabLabel": "Rename Tool Demo",
    "InitTabSize": [380, 200],
    "InitTabPosition": [200, 200],
    "InitPyCmd": "",
    "Root":
    {
        "SVerticalBox":
        {
            "Slots": [
                {
                    "AutoHeight": true,
                    "SHorizontalBox":
                    {
                        "Slots": [
                            {
                                "SEditableTextBox":
                                {
                                    "HintText": "Type name for actor(s)",
                                    "Aka": "NameInput"
                                }
                            },
                            {
                                "AutoWidth": true,
                                "SButton": {
                                    "Text": "Rename",
                                    "OnClick": ""
                                }
                            }
                        ]
                    }
                },
                {
                    "VAlign": "Bottom",
                    "SEditableTextBox":
                    {
                        "IsReadOnly": true,
                        "Aka": "StatusBar"
                    }
                }
            ]
        }
    }
}

TIP
修改界面之后,可以通过Tab上的右键菜单,Reload界面

G24_rename_tool_ui

要点:

  • 我们使用"SVerticalBox"作为一个“容器”将其中("Slots")的子控件上下排列。
  • "SVerticalBox"其中的子控件,分别是一个"SHorizontalBox"水平Box容器(用于水平布置名字输入控件和按钮控件),和一个文本框 "SEditableTextBox"(用作状态栏)
  • "AutoHeight": true 用于设定子控件“高度自适应”,此时的控件不会占用超出它自身内容的空间
  • "VAlign": "Bottom" 设定下方的文本框向下对齐,永远位于窗口的最下方
  • "AutoHeight""VAlign" 实际控制的是"SVerticalBox"中的"Slot{}",而非"Slot{}"``其中的"SHorizontalBox"等。因此,注意它们是写在"SHorizontalBox"```的外侧,与它平级。

  • "SHorizontalBox" 中的"SEditableTextBox"作为新名字的输入框。

  • 末尾的"SEditableTextBox"为状态栏文本框,"IsReadOnly": true
  • "HintText" 是文本框在没有任何输入内容时的内容提示
  • "Aka" 是这个文本控件的变量名,后续我们将在python代码中通过它来获取用户的输入内容等
  • "SButton"中的"OnClick"中的内容是按钮被点击后执行的Python代码,我们这里先留空
  • JSON中的布尔值是truefalse,首字母小写;Python中的则是首字母大写

4. 添加工具逻辑

编辑 __init__.py,将rename_tool这个模块导入到我们的PackageDemoTool

from . import rename_tool
编辑rename_tool.py**, 内容如下

rename_tool.py

# -*- coding: utf-8 -*-
import unreal
from Utilities.Utils import Singleton

class RenameTool(metaclass=Singleton):
    def __init__(self, jsonPath:str):
        self.jsonPath = jsonPath
        self.data = unreal.PythonBPLib.get_chameleon_data(self.jsonPath)

        self.ui_name_input = "NameInput"
        self.ui_status_bar = "StatusBar"

    def on_rename_button_click(self):
        new_name = self.data.get_text(self.ui_name_input)

        selected_actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).get_selected_level_actors()
        # if your are using UE4, use below code instead.
        # selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors()
        for i, actor in enumerate(selected_actors):
            actor.set_actor_label(f"{new_name}_{i:03d}")

        self.data.set_text(self.ui_status_bar, f"{len(selected_actors)} actor(s) has been rename.")

要点:

  • 我们的工具类RenameTool继承单例类,在整个编辑器中只有一个实例

  • 下面初始化函数中的写法是模式化的。它将保存初始化时传入的界面JSON文件路径,并且通过该路径获取该工具的一个ChameleonData的实例并赋值给self.data。这样Python端就能通过self.data来获取和修改Slate界面中的内容

    def __init__(self, jsonPath:str):
        self.jsonPath = jsonPath
        self.data = unreal.PythonBPLib.get_chameleon_data(self.jsonPath)
  • self.ui_name_input 的值就是rename_tool.json中定义的"Aka"对应的值

CAUTION
"Aka"中的字符串是大小写敏感的

  • new_name = self.data.get_text(self.ui_name_input) 我们通过控件的"Aka"名,获取到了其中的内容
  • self.data.get_textself.data.set_text 等方法无需指定控件的类型,我们可以用同一个方法获取/设置其他控件(比如按钮)中的文本内容

将界面与Python连接起来

重新打开rename_tool.json,将其中的"InitPyCmd" 和按钮上的"OnClick"补充完整:

rename_tool.json

...
"InitPyCmd": "import DemoTool; chameleon_rename_tool = DemoTool.rename_tool.RenameTool(%JsonPath)",
...
  • 这里"InitPyCmd"会在界面创建的时候同时创建RenameTool工具的实例,并将界面文件的路径传给Python
  • %JsonPath 是一个辨识符,无需修改,在创建工具时会被自动替换成实际路径
  • chameleon_rename_tool是持有工具实例的变量名,并且它处于Python的全局命名空间中,所以你可以在其他任何地方访问到它。因此,我们也应该确保它是全局唯一的。添加一个独一无二的前缀是一个不错的方法,事实上,我自己的工具实例变量名都是由chameleon_开头的

Extra
可以用一个全局的工具实例管理器,管理这些ChameleonTool的实例,以便最大程度得较少对全局命名空间的占用

...
"SButton": {
    "Text": "Rename",
    "OnClick": "chameleon_rename_tool.on_rename_button_click()"
}   
...

最终完成的 rename_tool.json

rename_tool.json

{
    "TabLabel": "Rename Tool Demo",
    "InitTabSize": [380, 200],
    "InitTabPosition": [200, 200],
    "InitPyCmd": "import DemoTool; chameleon_rename_tool = DemoTool.rename_tool.RenameTool(%JsonPath)",
    "Root":
    {
        "SVerticalBox":
        {
            "Slots": [
                {
                    "AutoHeight": true,
                    "SHorizontalBox":
                    {
                        "Slots": [
                            {
                                "SEditableTextBox":
                                {
                                    "HintText": "Type name for actor(s)",
                                    "Aka": "NameInput"
                                }
                            },
                            {
                                "AutoWidth": true,
                                "SButton": {
                                    "Text": "Rename",
                                    "OnClick": "chameleon_rename_tool.on_rename_button_click()"
                                }
                            }
                        ]
                    }
                },
                {
                    "VAlign": "Bottom",
                    "SEditableTextBox":
                    {
                        "IsReadOnly": true,
                        "Aka": "StatusBar"
                    }
                }
            ]
        }
    }
}

小结

  1. MenuConfig.json 定义了工具的菜单入口位置,并指向了rename_tool.json这个界面文件
  2. 打开界面时 rename_tool.jsonInitPyCmd中的Python代码被执行:创建出Python工具实例,并将界面文件的路径传入
  3. Python工具通过self.data获取和修改Slate界面内容

参考链接

  • live-template
  • 最小范例
  • hot-reload
  • 执行顺序