在下面的例子里,我们将从零开始创建一个重命名工具。
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中创建的菜单, 我们就将打开如下的一个工具面板:
完善中的界面内容¶
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界面
要点:
- 我们使用
"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中的布尔值是
true
和false
,首字母小写;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_text
、self.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"
}
}
]
}
}
}
小结¶
MenuConfig.json
定义了工具的菜单入口位置,并指向了rename_tool.json
这个界面文件- 打开界面时
rename_tool.json
的InitPyCmd
中的Python代码被执行:创建出Python工具实例,并将界面文件的路径传入 - Python工具通过
self.data
获取和修改Slate界面内容
参考链接¶
- live-template
- 最小范例
- hot-reload
- 执行顺序