概述
本篇是控件完全解析系列之一,主要总结一下Tree
控件与TreeItem
的使用。
Tree
控件是一个非常强大的控件,尤其是在编写一些相关的程序或编辑器插件时,非常适合展示树形组织的节点型数据。
本篇将从简单的添加根节点,根节点子节点,设置文本和图标等等基础开始学习,尽量涵盖和串联Tree
和TreeItem
所提供的API。
为Tree添加节点
这里的“节点”只是对TreeItem
的一个通俗(但有歧义的)称呼,Tree
控件的TreeItem
并不算是一个真正意义上的Godot节点,行为方法也很节点不一样。因为它是直接继承自Object
类型的,而不是Control
,所以无法直接在场景面板中查看到,也无法直接用检视器面板添加和编辑。删除的时候也要使用free()
方法。
为了尽量减少歧义,又符合通俗描述,我会在本文中尽量少的地方用“节点”的称呼,而是直接改用TreeItem
。
创建根节点
- 我们可以使用
Tree
控件的create_item()
方法创TreeItem
。 create_item()
有一个参数,用于传入作为父节点的TreeItem
,如果为null
,则创建的TreeItem
作为Tree
的根节点被添加。create_item()
返回对新建的TreeItem
的引用,我们可以用一个变量接收,并通过这个变量继续设定TreeItem
的属性或调用其方法。
# 创建根节点
var root:TreeItem = tree.create_item()
root.set_text(0,"root") # 设定根节点文本
- 这里我们使用
TreeItem
的set_text()
方法设定根节点root
的文本 Tree
控件可以显示多列信息,显示列数由其columns
属性设定,默认为1
,也就是只显示一列。set_text()
第一个参数是所在列的索引,默认为0
上面代码的效果:
创建根节点的子节点
- 在添加根节点后,可以继续使用
Tree
控件的create_item()
方法创建根节点的子节点,只是需要将根节点引用root
作为父节点传入。
# 创建根节点
var root:TreeItem = tree.create_item()
root.set_text(0,"root") # 设定根节点文本
# 创建根节点的子节点
var itm:TreeItem = tree.create_item(root)
itm.set_text(0,"子节点1")
上面代码的效果:
- 除了直接用
Tree
控件的create_item()
方法外,也可以直接利用已经创建的TreeItem
(比如根节点)的create_child()
方法来创建子节点。
# 创建根节点
var root:TreeItem = tree.create_item()
root.set_text(0,"root") # 设定根节点文本
# 创建根节点的子节点
var itm:TreeItem = root.create_child()
itm.set_text(0,"子节点1")
两种方式得到的效果是一致的。
设定图标
使用TreeItem
的set_icon()
方法,可以设定图标。同样第一个参数指定所在列。
var icon = preload("res://icon.svg")
# 创建根节点
var root:TreeItem = tree.create_item()
root.set_text(0,"root") # 设定根节点文本
root.set_icon(0,icon) # 设定根节点图标
上面代码效果:
设定图标最大宽度
- 可以看到,单纯用
set_icon()
方法,只能在TreeItem
上显示图标 - 但是很多情况下我们会遇到图标尺寸不合适的情况,这时就需要用
set_icon_max_width
方法设定图标最大宽度了。
var icon = preload("res://icon.svg")
# 创建根节点
var root:TreeItem = tree.create_item()
root.set_text(0,"root") # 设定根节点文本
root.set_icon(0,icon) # 设定根节点图标
root.set_icon_max_width(0,30) # 设定根节点图标宽度
- Godot项目默认图标
icon.svg
尺寸为128×128
像素,通过设定最大宽度为30
,则图标将以30×30
像素进行显示。
使用SpriteSheet或打包纹理作为图标来源
-
SpriteSheet和打包纹理都是一种常见的2D游戏或UI美术素材存储形式。SpriteSheet采用尺寸一致的行列划分,每一帧大小尺寸一致。而打包纹理,则是更自由的压缩存储多个大小不一的小图片。
-
这里我使用如下的一个SpriteSheet来作为一个图标库,用来给不同的
TreeItem
显示不同的图标。
- 原图尺寸是
900×150
像素,单个图标的尺寸就是150×150
像素。 - 而我们将最终显示在
TreeItem
上的图标宽度限定为18像素,这样最终呈现的大小就是18×18
像素。
var icon = preload("res://items.png") # 使用SpriteSheet或打包纹理
var cell_size = Vector2(150,150) # 单元格尺寸
# ------ 创建根节点 ------
var root:TreeItem = tree.create_item()
root.set_text(0,"root") # 设定根节点文本
root.set_icon(0,icon) # 设定根节点图标
root.set_icon_max_width(0,18) # 设定根节点图标宽度
var cell_pos = Vector2(0,0) # 帧的位置
# 显示图片局部区域作为图标
root.set_icon_region(0,Rect2(cell_pos * cell_size,cell_size))
# ------ 为根节点创建5个子节点 ------
for i in range(5):
var itm:TreeItem = root.create_child()
itm.set_text(0,"item%d" % i)
itm.set_icon(0,icon) # 设定根节点图标
itm.set_icon_max_width(0,18) # 设定根节点图标宽度
cell_pos = Vector2(i+1,0) # 帧的位置
itm.set_icon_region(0,Rect2(cell_pos * cell_size,cell_size))
效果:
对图标颜色进行调制
我们连接并处理Tree
控件的item_selected
信号。
信号处理函数代码如下:
# 处理Tree控件的item_selected信号
func _on_tree_item_selected():
# 获取选中节点
var selected_item:TreeItem = tree.get_selected()
# 恢复其他节点的图标颜色
var root:TreeItem = tree.get_root()
root.set_icon_modulate(0,Color.WHITE)
for itm in root.get_children():
itm.set_icon_modulate(0,Color.WHITE)
# 修改选中节点的图标颜色
selected_item.set_icon_modulate(0,Color.BLUE)
运行后可以看到,选中项的图标颜色用蓝色调制的效果,没有使用额外的图标,而是通过颜色调制,做出了选中状态与非选中状态的图标差异效果。
创建多列
启用和添加多列
Tree控件支持多列视图,其columns
属性控制列的数目。我们可以在检视器面板直接修改,或者使用代码形式设定。
以下是代码形式:
# 设定显示两列信息
tree.columns = 2
# 创建根节点
var root:TreeItem = tree.create_item()
root.set_text(0,"root") # 设定根节点第1列文本
root.set_text(1,"第2列") # 设定第2列文本
我们通过设定第2列文本,就可以看到如下的效果:
每一列都可以像第1列一样设置图标或其他。
const icon = preload("res://icon.svg")
# 设定显示两列信息
tree.columns = 2
# 创建根节点
var root:TreeItem = tree.create_item()
root.set_text(0,"root") # 设定根节点第1列文本
root.set_icon(0,icon) # 设定第1列图标
root.set_icon_max_width(0,16) # 设定第1列图标最大宽度
root.set_text(1,"第2列") # 设定第2列文本
root.set_icon(1,icon) # 设定第1列图标
root.set_icon_max_width(1,16) # 设定第1列图标最大宽度
上面代码,我们为两列都设定的图标和图标最大宽度。
显示列标题
和一些其他可视化程序开发语言或数据表格软件的表格控件一样,Tree控件也可以显示列标题(或者通俗叫“字段”)。
你需要用代码或在检视器面板手动开启column_titles_visible
。
启用后就会在每列上方显示两个按钮,目前因为还没有设置其文本,所以是不完全的形态:
通过Tree
的set_column_title()
方法,我们可以设定所有列标题按钮的文本:
# 设定和显示列标题
tree.column_titles_visible = true # 显示列标题
tree.set_column_title(0,"第1列") # 设定第1列列标题
tree.set_column_title(1,"第2列") # 设定第2列列标题
上面代码设置后的效果:
是树型列表也是二维表格
我们为根节点添加一个子节点:
# 添加子节点
var itm:TreeItem = tree.create_item(root)
itm.set_text(0,"itm1") # 设定根节点第1列文本
itm.set_icon(0,icon) # 设定第1列图标
itm.set_icon_max_width(0,16) # 设定第1列图标最大宽度
itm.set_text(1,"第2列") # 设定第2列文本
itm.set_icon(1,icon) # 设定第1列图标
itm.set_icon_max_width(1,16) # 设定第1列图标最大宽度
此时的效果如下:
可以看到Tree
的第一列保持了树形列表的特征,同时每一列的单元格都可以单独选中和交互。节点的折叠与展开也不受影响,表现出一种树形结构+二维表格的特性。
设定列标题文本对齐方式
-
对列标题的,也可以设定其文本对齐方式,宽度等细节。
-
set_column_title_alignment
设定列标题文本的对齐方式:
tree.set_column_title_alignment(0,HORIZONTAL_ALIGNMENT_LEFT) # 居左显示
上面的代码设定第一列的列标题居左显示:
设定最小列宽
tree.set_column_custom_minimum_width(0,100) # 设定第一列最小宽度
上面的代码设定第一列的最小宽度为100像素。
处理列标题点击
我们可以连接和处理Tree
控件的column_title_clicked
信号,来处理列标题的点击。
# 列标题被点击
func _on_tree_column_title_clicked(column, mouse_button_index):
tree.select_mode = Tree.SELECT_MULTI # 设置多选模式
# 获取Tree的所有节点
var root:TreeItem = tree.get_root()
var items = [root]
items.append_array(root.get_children())
# 选中指定列所有单元格
for itm in items:
for i in range(tree.columns):
if i == column:
itm.select(column)
else:
itm.deselect(i)
等价写法:
# 列标题被点击
func _on_tree_column_title_clicked(column, mouse_button_index):
tree.select_mode = Tree.SELECT_MULTI # 设置多选模式
# 获取Tree的所有节点
var root:TreeItem = tree.get_root()
var items = [root]
items.append_array(root.get_children())
tree.deselect_all() # 清除所有选择
# 选中指定列所有单元格
for itm in items:
itm.select(column)
上面的代码实现点击列标题时,选中当前列的所有单元格:
设定选择模式
Tree控件有三种选择模式,不同模式下选中TreeItem
的效果哈方式不同,并且可能会影响某些信号的触发和处理。
检视器面板设定:
代码方式设定:
# 设定选择模式
tree.select_mode = Tree.SELECT_SINGLE # 单个单元格选择
tree.select_mode = Tree.SELECT_ROW # 按行选择
tree.select_mode = Tree.SELECT_MULTI # 多选
效果:
select_mode属性值 | 效果 |
---|---|
SELECT_SINGLE | 如果有多列,则一次只能单独选中某项的某列,也就是某个单元格 |
SELECT_ROW | 每次只能选中一行,也就是选中某项的所有列 |
SELECT_MULTI | 可以使用Ctrl或Shift键进行多选,一次可以选中多个单元格 |
创建按钮
在每列的单元格中都可以创建1个或多个按钮。按钮显示在单元格的最右侧,并且只显示一个图标,不显示文本。
但是可以设定其鼠标提示文本等。
这种按钮可以作为树控件单元格或行数据交互的一种形式。
const DIR = preload("res://custom_nodes/icons/dir.png")
# 创建根节点
var root:TreeItem = tree.create_item()
root.set_text(0,"root") # 设定根节点第1列文本
root.set_icon(0,icon) # 设定第1列图标
root.set_icon_max_width(0,16) # 设定第1列图标最大宽度
root.add_button(0,DIR) # 为根节点0列的单元格创建按钮
tree.set_column_custom_minimum_width(0,200)
上面的代码为两项的两列都添加了一个按钮。
处理按钮的点击
我们可以连接Tree
的button_clicked
信号,来处理单元格中添加的按钮的信号。
其参数返回:
item
:按钮所在的TreeItemcolumn
:按钮所在的列id
:按钮在所在单元格按钮中的ID或索引mouse_button_index
:点击按钮的鼠标按键,鼠标左键、中键、右键还是其他额外按键。
func _on_tree_button_clicked(item, column, id, mouse_button_index):
if mouse_button_index == MOUSE_BUTTON_LEFT: # 鼠标左键
match column: # 某一列
0,1:
match id: # 某按钮
0:
item.set_text(column,"")
上面的信号处理代码,可以让我们清空所在单元格的文本:
快速编辑单元格文本
单元格可以进入文本编辑状态,显示一个可以编辑的文本框,从而可以快速修改单元格的文本内容,回车后完成编辑状态。
双击进入编辑状态
Tree
的item_activated()
信号在TreeItem
或其某列单元格双击时触发。
我们通过连接此信号,创建信号处理函数,并设定如下的处理代码:
# 双击单元格
func _on_tree_item_activated():
tree.edit_selected(true) # 选中单元格进入编辑状态
此时,我们就可以通过双击单元格,进入单元格文本的编辑状态,修改完成后,回车确认,退出编辑状态:
编辑后的处理
Tree
的item_edited()
信号,在编辑完某个单元格并按回车确认后触发Tree
的get_edited()
方法可以获取当前编辑的TreeItem
。- 通过
Tree
的get_edited_column()
方法可以获取刚刚被编辑的TreeItem
的列。
这里我只简单的打印输出单元格被修改后的文本:
# 单元格编辑完成后
func _on_tree_item_edited():
var edt:TreeItem = tree.get_edited() # 获取被编辑的TreeItem
var col:int = tree.get_edited_column() # 被编辑的列索引
print(edt.get_text(col)) # 打印修改后的单元格文本
处理TreeItem或单元格选中
item_selected()
信号,在TreeItem
选中时触发,但仅在select_mode
为SELECT_ROW
或SELECT_SINGLE
时有效。
# TreeItem被选中或单击
func _on_tree_item_selected():
print(tree.get_selected())
pass
select_mode
为SELECT_MULTI
或SELECT_SINGLE
时,则可以响应cell_selected()
信号。
# TreeItem的单元格被选中或点击
func _on_tree_cell_selected():
var sel:TreeItem = tree.get_selected() # 选中的单元格所在TreeItem
var col:int = tree.get_selected_column() # 选中的单元格列索引
print(sel.get_text(col))
总结一下就是:
select_mode属性值 | item_selected()信号 | cell_selected()信号 |
---|---|---|
SELECT_SINGLE | √ | √ |
SELECT_ROW | √ | × |
SELECT_MULTI | × | √ |
用按钮控制Tree控件项的增、删、移动
我们创建一个如下的用户界面场景,创建一个Tree
以及多个Button
。
通过这些按钮,我们实现为Tree控件添加、删除好移动项。
为场景根节点添加如下代码:
extends Control
@onready var tree = $HBoxContainer/Tree
@onready var add_btn = $HBoxContainer/VBoxContainer/addBtn
@onready var del_btn = $HBoxContainer/VBoxContainer/delBtn
@onready var moveup_btn = $HBoxContainer/VBoxContainer/moveupBtn
@onready var movedown_btn = $HBoxContainer/VBoxContainer/movedownBtn
@onready var clear_btn = $HBoxContainer/VBoxContainer/clearBtn
# 添加
func _on_add_btn_pressed():
var parent:TreeItem = null
if tree.get_root(): # 树不是空的
if tree.get_selected(): # 有选中项
parent = tree.get_selected()
else:
parent = tree.get_root()
var itm = tree.create_item(parent)
var idx:int = parent.get_child_count() if parent else 0
itm.set_text(0,"新节点%d" % idx)
# 删除
func _on_del_btn_pressed():
if tree.get_root(): # 树不是空的
if tree.get_selected(): # 有选中项
tree.get_selected().free()
pass
# 上移
func _on_moveup_btn_pressed():
var sel:TreeItem = tree.get_selected()
if sel and sel.get_prev(): # 有选中项
sel.move_before(sel.get_prev())
# 下移
func _on_movedown_btn_pressed():
var sel:TreeItem = tree.get_selected()
if sel and sel.get_next(): # 有选中项
sel.move_after(sel.get_next())
# 清空
func _on_clear_btn_pressed():
tree.clear()
pass
可以看到其中主要是对按钮信号的处理代码,实现增、删,上下移动以及清空,效果如下动图所示:
自由拖放
通过为Tree
控件添加基本的控件拖放逻辑,就可以实现用鼠标对TreeItem
的移动和放置。
extends Tree
func _get_drag_data(at_position):
if get_tree(): # 不为空
var sel:TreeItem = get_item_at_position(at_position)
if sel: # 有选中项
set_drag_preview(make_drag_preview(sel))
return [sel]
func _can_drop_data(at_position, data):
# 如果拖放数据是一个TreeItem就可以放置
# 拖放完毕恢复拖动标志设定
drop_mode_flags = DROP_MODE_ON_ITEM | DROP_MODE_INBETWEEN
return data.size() > 0 and (data[0] is TreeItem)
func _drop_data(at_position, data):
# 获取目标拖放位置,-1,0,1分别代表在某项之前、之上和之后
var target_pos = get_drop_section_at_position(at_position)
# 获取鼠标位置处的TreeItem
var target_itm:TreeItem = get_item_at_position(at_position)
# 如果目标位置处TreeItem是data[0]的子孙节点
if target_itm in get_items(data[0]):
return # 禁止移动
match target_pos:
-1: # 拖放到了某个TreeItem之前
# 根据是否同级进行区别处理
if data[0].get_parent() == target_itm.get_parent(): # 如果同级
data[0].move_before(target_itm)
else:
data[0].get_parent().remove_child(data[0]) # 先从原来的父节点删除
target_itm.add_child(data[0]) # 添加到目标位置的TreeItem
data[0].move_before(target_itm)
0: # 拖放到了某个TreeItem上
data[0].get_parent().remove_child(data[0]) # 先从原来的父节点删除
target_itm.add_child(data[0]) # 添加到目标位置的TreeItem
1: # 拖放到了某个TreeItem之后
# 根据是否同级进行区别处理
if data[0].get_parent() == target_itm.get_parent(): # 如果同级
data[0].move_after(target_itm)
else:
data[0].get_parent().remove_child(data[0]) # 先从原来的父节点删除
target_itm.add_child(data[0]) # 添加到目标位置的TreeItem
data[0].move_after(target_itm)
# 返回某个TreeItem下所有子孙节点的集合
func get_items(item:TreeItem) -> Array[TreeItem]:
var arr:Array[TreeItem]
if item.get_child_count()>0:
arr.append_array(item.get_children())
for chd in item.get_children():
arr.append_array(get_items(chd))
return arr
# 创建拖动预览
func make_drag_preview(itm:TreeItem) -> Button:
var btn = Button.new()
#btn.flat = true
btn.text = itm.get_text(0)
btn.icon = itm.get_icon(0)
return btn
实现效果: