概述
在Godot中利用MenuBar
+PopupMenu
设计复杂菜单,以及利用Tree
控件创建复杂的树形导航,都是一件繁复的工作。
通过某种数据形式,以及一个解析函数,自动加载生成菜单或Tree
的树形列表,便成为了自然而然的选择。
我首先想到的是字典和JSON,两者其实类似,都是键值对形式,存在很多重复冗余的键定义。
所以我想自创一种树形数据表示形式。我对它的要求是:
- 可以很好的表示树形结构
- 基于纯文本,依靠字符串解析可以轻松获取数据
- 语法简洁,便于手动书写和更改,更易于阅读
尝试了好几种形式后,两种思路被验证为可行,其中一种非常适用于动态解析生成菜单栏(本篇将不做介绍)。
而另一种适合用于Tree控件的树形结构定义,也就是本篇下面要讲的自定义数据形式。
简易树形数据(Easy Tree Data,ETD)
没错,我甚至给这种我自创的纯文本数据描述形式,起了个名字,叫“简易树形数据”,英文名叫Easy Tree Data,简称ETD。如果我选择保存文件的话,可能会采用.etd
作为后缀名。
下面是最基本的ETD示例:
条目1
条目1.1
条目1.2
条目1.2.1
条目1.2.2
条目1.3
没错,就是这么简洁,就是一个多行字符串,但是仍然有一些细节:
- 只能有一个一级节点:或者叫根节点,与
Tree
控件特性对应 - 用缩进代表层级关系:而缩进必须使用
Tab
键,也就是\t
字符
解析原理
- 将ETD字符串用
\n
划分为单行字符串组成的数组,每行代表一个树控件子节点(TreeItem
)的定义数据,比如上文的ETD字符串用\n
划分后,变为:
["条目1", "\t条目1.1", "\t条目1.2", "\t\t条目1.2.1", "\t\t条目1.2.2", "\t条目1.3"]
- 然后我们遍历这个数组(相当于遍历每行数据),并比较当前项与前一项的缩进关系(
\t
字符的数目),从而决定将当前项添加为前一项的子节点,还是前一项的父节点的子节点,还是前一项父节点的父节点的子节点等等…,其判断规则如下:- 如果是第1项直接添加为根节点
- 否则判断和比较当前项与上一项的缩进深度:
- 如果缩进深度一样,将当前项添加到前一项的父节点上
- 如果比前一项深,则添加为前一项的子节点
- 如果比前一项浅,则添加为前一项爷爷或更爷爷节点的子节点
基础实现代码如下:
var tree = $Tree # 指定Tree控件
var items = data.split("\n",false) # 将ETD字符串按行切分为字符串数组
var pre_itm:TreeItem # 记录前一项对应的TreeItem
var p_itm = null # 记录父节点
# 遍历每行数据
for i in range(items.size()):
# 第1行直接添加为Tree控件的根节点(跳过下面if部分)
# 从第2行开始比较当前行与前一行的缩进深度(也就是\t的数目)
if i > 0:
var d_deep = deep(items[i-1]) - deep(items[i]) # 与前一行数据的缩进差值
match d_deep:
-1: # 缩进比前一项深:
p_itm = pre_itm # 将前一项作为父节点
0: # 缩进深度与前一项一样:
p_itm = pre_itm.get_parent() # 父节点与前一项父节点一样
_:
if d_deep>0: # 缩进比前一项浅
# 通过缩进差值计算获得合适的父节点
p_itm = pre_itm
for j in range(d_deep+1):
p_itm = p_itm.get_parent()
# 实际创建和添加TreeItem到Tree控件
var itm:TreeItem = tree.create_item(p_itm)
itm.set_text(0,items[i])
pre_itm = itm # 将当前项记录为前一项
实际解析和添加后的效果:
为项数据添加额外设定
上面的ETD只是最简单的形式,只解决了Tree控件的纯文本项的加载,实际使用中我们通常还需要设定每个TreeItem
的图标,或者鼠标提示文本、文本颜色,高亮颜色、选中颜色,甚至元数据。
因此需要每行数据都可以划分为几个子数据,设计思路也很简单,同样使用字符串分割为字符串数组的方式,只需要设定好特殊的分隔字符串就行。我的选择是使用|
作为分隔符,也就是一个竖线两边各带一个空格。
这里我只拓展定义了图标索引的ETD形式:
条目1 | 0
条目1.1 | 2
条目1.2 | 2
条目1.2.1 | 3
条目1.2.2 | 3
条目1.3 | 2
也就是直接在每行所代表的TreeItem
数据中,添加一个代表图标索引的数字。
这里需要定义一个数组来存储图标:
@export var icons:Array[Texture2D] # 图标集
@export var icon_width = 16 # 图标最大宽度
用导出变量形式我们可以更方便的设定图标列表:
这里我添加了4个图标。
修改ETD的解析代码,加入对图标索引数据的解析和处理:
var tree = $Tree # 指定Tree控件
var items = data.split("\n",false) # 将ETD字符串按行切分为字符串数组
var pre_itm:TreeItem # 记录前一项对应的TreeItem
var p_itm = null # 记录父节点
# 遍历每行数据
for i in range(items.size()):
# 第1行直接添加为Tree控件的根节点(跳过下面if部分)
# 从第2行开始比较当前行与前一行的缩进深度(也就是\t的数目)
if i > 0:
var d_deep = deep(items[i-1]) - deep(items[i]) # 与前一行数据的缩进差值
match d_deep:
-1: # 缩进比前一项深:
p_itm = pre_itm # 将前一项作为父节点
0: # 缩进深度与前一项一样:
p_itm = pre_itm.get_parent() # 父节点与前一项父节点一样
_:
if d_deep>0: # 缩进比前一项浅
# 通过缩进差值计算获得合适的父节点
p_itm = pre_itm
for j in range(d_deep+1):
p_itm = p_itm.get_parent()
# 实际创建和添加TreeItem到Tree控件
var itm:TreeItem = tree.create_item(p_itm)
# 解析单项的具体信息
if items[i].find(" | ")>0:
var itm_data = items[i].split(" | ")
itm.set_text(0,itm_data[0])
itm.set_icon(0,icons[int(itm_data[1])])
itm.set_icon_max_width(0,icon_width)
else:
itm.set_text(0,items[i])
pre_itm = itm # 将当前项记录为前一项
解析后的效果:
可以看到图标被正确的显示。
鼠标提示文本、文本颜色,高亮颜色、选中颜色以及元数据等,实现思路都是一样的。
ETD只是一个框架,基于它你可以具体的定义和存储不同的树形数据,并解析为Tree
控件显示的内容。
自定义控件和data参数
我们可以创建继承自Tree
的自定义控件,然后将ETD数据作为一个“检视器”面板参数(导出变量),我们可以直接在检视器面板修改ETD数据,并实时查看生成的树形结构。
创建静态函数
除了创建特化的Tree控件,解析特化的ETD数据外。也可以将不同的ETD数据定义和解析写成静态函数,统一的存入一个静态函数库中,方便任何地方调用。
这样,具体的ETD格式定义和解析就可以被保留,并且不断扩展。