概述

本篇是控件完全解析系列之一,主要总结一下Tree控件与TreeItem的使用。

Tree控件是一个非常强大的控件,尤其是在编写一些相关的程序或编辑器插件时,非常适合展示树形组织的节点型数据。

本篇将从简单的添加根节点,根节点子节点,设置文本和图标等等基础开始学习,尽量涵盖和串联TreeTreeItem所提供的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")   # 设定根节点文本
  • 这里我们使用TreeItemset_text()方法设定根节点root的文本
  • Tree控件可以显示多列信息,显示列数由其columns属性设定,默认为1,也就是只显示一列。
  • set_text()第一个参数是所在列的索引,默认为0

上面代码的效果:

image.png

创建根节点的子节点

  • 在添加根节点后,可以继续使用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")

上面代码的效果:

image.png

  • 除了直接用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")

两种方式得到的效果是一致的。

设定图标

使用TreeItemset_icon()方法,可以设定图标。同样第一个参数指定所在列。

var icon = preload("res://icon.svg")
# 创建根节点
var root:TreeItem = tree.create_item()
root.set_text(0,"root")   # 设定根节点文本
root.set_icon(0,icon)     # 设定根节点图标

上面代码效果:

image.png

设定图标最大宽度

  • 可以看到,单纯用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像素进行显示。
    image.png

使用SpriteSheet或打包纹理作为图标来源

  • SpriteSheet打包纹理都是一种常见的2D游戏或UI美术素材存储形式。SpriteSheet采用尺寸一致的行列划分,每一帧大小尺寸一致。而打包纹理,则是更自由的压缩存储多个大小不一的小图片。

  • 这里我使用如下的一个SpriteSheet来作为一个图标库,用来给不同的TreeItem显示不同的图标。

roller_sprites.png

  • 原图尺寸是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))

效果:

image.png

对图标颜色进行调制

我们连接并处理Tree控件的item_selected信号。

image.png
信号处理函数代码如下:

# 处理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)

运行后可以看到,选中项的图标颜色用蓝色调制的效果,没有使用额外的图标,而是通过颜色调制,做出了选中状态与非选中状态的图标差异效果。

调制图标颜色.gif

创建多列

启用和添加多列

Tree控件支持多列视图,其columns属性控制列的数目。我们可以在检视器面板直接修改,或者使用代码形式设定。

image.png

以下是代码形式:

# 设定显示两列信息
tree.columns = 2
# 创建根节点
var root:TreeItem = tree.create_item()
root.set_text(0,"root")             # 设定根节点第1列文本
root.set_text(1,"第2列")             # 设定第2列文本

我们通过设定第2列文本,就可以看到如下的效果:

image.png

每一列都可以像第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列图标最大宽度

上面代码,我们为两列都设定的图标和图标最大宽度。

image.png

显示列标题

和一些其他可视化程序开发语言或数据表格软件的表格控件一样,Tree控件也可以显示列标题(或者通俗叫“字段”)。

你需要用代码或在检视器面板手动开启column_titles_visible

image.png

启用后就会在每列上方显示两个按钮,目前因为还没有设置其文本,所以是不完全的形态:

image.png

通过Treeset_column_title()方法,我们可以设定所有列标题按钮的文本:

# 设定和显示列标题
tree.column_titles_visible = true    # 显示列标题
tree.set_column_title(0,"第1列")     # 设定第1列列标题
tree.set_column_title(1,"第2列")     # 设定第2列列标题

上面代码设置后的效果:

image.png

是树型列表也是二维表格

我们为根节点添加一个子节点:

# 添加子节点
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列图标最大宽度

此时的效果如下:

image.png

可以看到Tree的第一列保持了树形列表的特征,同时每一列的单元格都可以单独选中和交互。节点的折叠与展开也不受影响,表现出一种树形结构+二维表格的特性。

设定列标题文本对齐方式

  • 对列标题的,也可以设定其文本对齐方式,宽度等细节。

  • set_column_title_alignment设定列标题文本的对齐方式:

tree.set_column_title_alignment(0,HORIZONTAL_ALIGNMENT_LEFT)   # 居左显示

上面的代码设定第一列的列标题居左显示:

image.png

设定最小列宽

tree.set_column_custom_minimum_width(0,100)  # 设定第一列最小宽度

上面的代码设定第一列的最小宽度为100像素。
image.png

处理列标题点击

我们可以连接和处理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)

上面的代码实现点击列标题时,选中当前列的所有单元格:

处理列标题按钮点击.gif

设定选择模式

Tree控件有三种选择模式,不同模式下选中TreeItem的效果哈方式不同,并且可能会影响某些信号的触发和处理。

检视器面板设定:

image.png

代码方式设定:

# 设定选择模式
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)

上面的代码为两项的两列都添加了一个按钮。

image.png

处理按钮的点击

我们可以连接Treebutton_clicked信号,来处理单元格中添加的按钮的信号。
其参数返回:

  • item:按钮所在的TreeItem
  • column:按钮所在的列
  • 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,"")

上面的信号处理代码,可以让我们清空所在单元格的文本:

处理按钮点击.gif

快速编辑单元格文本

单元格可以进入文本编辑状态,显示一个可以编辑的文本框,从而可以快速修改单元格的文本内容,回车后完成编辑状态。

双击进入编辑状态

Treeitem_activated()信号在TreeItem或其某列单元格双击时触发。

我们通过连接此信号,创建信号处理函数,并设定如下的处理代码:

# 双击单元格
func _on_tree_item_activated():
	tree.edit_selected(true)  # 选中单元格进入编辑状态

此时,我们就可以通过双击单元格,进入单元格文本的编辑状态,修改完成后,回车确认,退出编辑状态:

双击编辑单元格文本.gif

编辑后的处理

  • Treeitem_edited()信号,在编辑完某个单元格并按回车确认后触发
  • Treeget_edited()方法可以获取当前编辑的TreeItem
  • 通过Treeget_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_modeSELECT_ROWSELECT_SINGLE 时有效。
# TreeItem被选中或单击
func _on_tree_item_selected():
	print(tree.get_selected())
	pass
  • select_modeSELECT_MULTISELECT_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控件添加、删除好移动项。

image.png

为场景根节点添加如下代码:

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

可以看到其中主要是对按钮信号的处理代码,实现增、删,上下移动以及清空,效果如下动图所示:

按钮操控-添加.gif按钮操控-删除.gif
按钮操控-上移和下移.gif按钮操控-清空.gif

自由拖放

通过为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

实现效果:

自由拖放.gif

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐