概述
本篇是控件完全解析系列之一,主要總結一下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() # 獲取被編輯的TreeItemvar 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() # 選中的單元格所在TreeItemvar 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 = nullif 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 0itm.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 Treefunc _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_INBETWEENreturn 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)# 獲取鼠標位置處的TreeItemvar 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]) # 添加到目標位置的TreeItemdata[0].move_before(target_itm)0: # 拖放到了某個TreeItem上data[0].get_parent().remove_child(data[0]) # 先從原來的父節點刪除target_itm.add_child(data[0]) # 添加到目標位置的TreeItem1: # 拖放到了某個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]) # 添加到目標位置的TreeItemdata[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 = truebtn.text = itm.get_text(0)btn.icon = itm.get_icon(0)return btn
實現效果: