第三十三章:控件详解
Godot 4提供了丰富的UI控件,从简单的按钮到复杂的树形视图,能够满足各种游戏和应用的界面需求。本章将详细介绍各类控件的特性、属性和使用方法。
33.1 列表与选择控件
33.1.1 ItemList
# item_list_usage.gd
# ItemList控件详解
extends Control
@onready var item_list: ItemList = $ItemList
func _ready():
_setup_item_list()
_connect_signals()
func _setup_item_list():
# 基础设置
item_list.max_columns = 1 # 列数,0为自动
item_list.same_column_width = true
item_list.fixed_column_width = 0 # 0为自动
# 图标设置
item_list.icon_mode = ItemList.ICON_MODE_LEFT
item_list.fixed_icon_size = Vector2i(32, 32)
item_list.icon_scale = 1.0
# 选择设置
item_list.select_mode = ItemList.SELECT_SINGLE
item_list.allow_reselect = true
item_list.allow_search = true
# 添加项目
_populate_items()
func _populate_items():
item_list.clear()
var items = [
{"name": "剑", "icon": "res://icons/sword.png", "tooltip": "一把锋利的剑"},
{"name": "盾", "icon": "res://icons/shield.png", "tooltip": "坚固的防御盾"},
{"name": "药水", "icon": "res://icons/potion.png", "tooltip": "恢复生命值"},
{"name": "卷轴", "icon": "res://icons/scroll.png", "tooltip": "魔法卷轴"},
]
for item in items:
var idx = item_list.add_item(item.name)
# 设置图标
if ResourceLoader.exists(item.icon):
item_list.set_item_icon(idx, load(item.icon))
# 设置提示
item_list.set_item_tooltip(idx, item.tooltip)
# 设置元数据
item_list.set_item_metadata(idx, item)
# 设置是否可选
item_list.set_item_selectable(idx, true)
# 设置是否禁用
item_list.set_item_disabled(idx, false)
func _connect_signals():
item_list.item_selected.connect(_on_item_selected)
item_list.item_activated.connect(_on_item_activated)
item_list.multi_selected.connect(_on_multi_selected)
item_list.empty_clicked.connect(_on_empty_clicked)
func _on_item_selected(index: int):
var item_text = item_list.get_item_text(index)
var metadata = item_list.get_item_metadata(index)
print("选中: ", item_text)
print("元数据: ", metadata)
func _on_item_activated(index: int):
# 双击或回车激活
print("激活: ", item_list.get_item_text(index))
func _on_multi_selected(index: int, selected: bool):
print("多选: 索引 %d, 选中: %s" % [index, selected])
func _on_empty_clicked(at_position: Vector2, mouse_button: int):
print("点击空白区域: ", at_position)
item_list.deselect_all()
## 操作方法
func select_item_by_name(name: String):
for i in range(item_list.item_count):
if item_list.get_item_text(i) == name:
item_list.select(i)
item_list.ensure_current_is_visible()
return true
return false
func remove_selected():
var selected = item_list.get_selected_items()
# 从后往前删除,避免索引变化
for i in range(selected.size() - 1, -1, -1):
item_list.remove_item(selected[i])
func move_item(from: int, to: int):
if from < 0 or from >= item_list.item_count:
return
if to < 0 or to >= item_list.item_count:
return
item_list.move_item(from, to)
func sort_items_alphabetically():
item_list.sort_items_by_text()
33.1.2 OptionButton
# option_button_usage.gd
# OptionButton下拉选择控件
extends Control
@onready var difficulty_option: OptionButton = $DifficultyOption
@onready var language_option: OptionButton = $LanguageOption
func _ready():
_setup_difficulty_option()
_setup_language_option()
func _setup_difficulty_option():
difficulty_option.clear()
# 添加选项
difficulty_option.add_item("简单", 0)
difficulty_option.add_item("普通", 1)
difficulty_option.add_item("困难", 2)
difficulty_option.add_item("噩梦", 3)
# 设置图标
difficulty_option.set_item_icon(0, preload("res://icons/easy.png"))
difficulty_option.set_item_icon(3, preload("res://icons/hard.png"))
# 禁用某个选项
difficulty_option.set_item_disabled(3, true)
# 设置默认选中
difficulty_option.select(1)
# 连接信号
difficulty_option.item_selected.connect(_on_difficulty_selected)
func _setup_language_option():
language_option.clear()
var languages = [
{"name": "简体中文", "code": "zh_CN"},
{"name": "English", "code": "en"},
{"name": "日本語", "code": "ja"},
{"name": "한국어", "code": "ko"},
]
for i in range(languages.size()):
var lang = languages[i]
language_option.add_item(lang.name, i)
# 使用元数据存储语言代码
language_option.set_item_metadata(i, lang.code)
# 添加分隔符
language_option.add_separator()
language_option.add_item("更多语言...", -1)
language_option.item_selected.connect(_on_language_selected)
func _on_difficulty_selected(index: int):
var id = difficulty_option.get_item_id(index)
var text = difficulty_option.get_item_text(index)
print("难度: ", text, " ID: ", id)
func _on_language_selected(index: int):
var id = language_option.get_item_id(index)
if id == -1:
# 打开更多语言对话框
_show_more_languages()
return
var code = language_option.get_item_metadata(index)
print("语言: ", code)
TranslationServer.set_locale(code)
func _show_more_languages():
print("显示更多语言选项")
## 编程控制
func select_by_id(option_button: OptionButton, id: int):
for i in range(option_button.item_count):
if option_button.get_item_id(i) == id:
option_button.select(i)
return
func get_selected_metadata(option_button: OptionButton):
var idx = option_button.selected
if idx >= 0:
return option_button.get_item_metadata(idx)
return null
33.1.3 Tree控件
# tree_usage.gd
# Tree树形控件详解
extends Control
@onready var tree: Tree = $Tree
var root: TreeItem
func _ready():
_setup_tree()
_create_tree_structure()
_connect_signals()
func _setup_tree():
# 列设置
tree.columns = 3
tree.set_column_title(0, "名称")
tree.set_column_title(1, "类型")
tree.set_column_title(2, "大小")
tree.column_titles_visible = true
# 列宽
tree.set_column_expand(0, true)
tree.set_column_expand(1, false)
tree.set_column_expand(2, false)
tree.set_column_custom_minimum_width(1, 80)
tree.set_column_custom_minimum_width(2, 60)
# 选择模式
tree.select_mode = Tree.SELECT_SINGLE
# 隐藏根节点
tree.hide_root = false
# 允许拖放
tree.drop_mode_flags = Tree.DROP_MODE_ON_ITEM | Tree.DROP_MODE_INBETWEEN
func _create_tree_structure():
# 创建根节点
root = tree.create_item()
root.set_text(0, "项目")
root.set_icon(0, preload("res://icon.png"))
# 场景文件夹
var scenes = tree.create_item(root)
scenes.set_text(0, "Scenes")
scenes.set_text(1, "文件夹")
scenes.set_icon(0, preload("res://icons/folder.png"))
_add_file(scenes, "Main.tscn", "场景", "12KB")
_add_file(scenes, "Player.tscn", "场景", "8KB")
_add_file(scenes, "Enemy.tscn", "场景", "6KB")
# 脚本文件夹
var scripts = tree.create_item(root)
scripts.set_text(0, "Scripts")
scripts.set_text(1, "文件夹")
scripts.set_icon(0, preload("res://icons/folder.png"))
_add_file(scripts, "Player.gd", "脚本", "4KB")
_add_file(scripts, "Enemy.gd", "脚本", "3KB")
_add_file(scripts, "GameManager.gd", "脚本", "5KB")
# 资源文件夹
var resources = tree.create_item(root)
resources.set_text(0, "Resources")
resources.set_text(1, "文件夹")
resources.set_collapsed(true) # 默认折叠
_add_file(resources, "player.png", "图片", "24KB")
_add_file(resources, "enemy.png", "图片", "18KB")
func _add_file(parent: TreeItem, name: String, type: String, size: String):
var item = tree.create_item(parent)
item.set_text(0, name)
item.set_text(1, type)
item.set_text(2, size)
# 根据类型设置图标
match type:
"场景":
item.set_icon(0, preload("res://icons/scene.png"))
"脚本":
item.set_icon(0, preload("res://icons/script.png"))
"图片":
item.set_icon(0, preload("res://icons/image.png"))
# 设置元数据
item.set_metadata(0, {"name": name, "type": type, "size": size})
return item
func _connect_signals():
tree.item_selected.connect(_on_item_selected)
tree.item_activated.connect(_on_item_activated)
tree.item_collapsed.connect(_on_item_collapsed)
tree.button_clicked.connect(_on_button_clicked)
tree.item_edited.connect(_on_item_edited)
func _on_item_selected():
var selected = tree.get_selected()
if selected:
var name = selected.get_text(0)
var metadata = selected.get_metadata(0)
print("选中: ", name)
func _on_item_activated():
var selected = tree.get_selected()
if selected:
print("激活: ", selected.get_text(0))
# 可以打开文件或展开文件夹
func _on_item_collapsed(item: TreeItem):
print("折叠/展开: ", item.get_text(0), " -> ", item.collapsed)
func _on_button_clicked(item: TreeItem, column: int, id: int, mouse_button: int):
print("按钮点击: ", item.get_text(0), " 列: ", column, " ID: ", id)
func _on_item_edited():
var item = tree.get_edited()
var column = tree.get_edited_column()
print("编辑: ", item.get_text(column))
## 高级操作
func add_item_with_button(parent: TreeItem, text: String):
var item = tree.create_item(parent)
item.set_text(0, text)
# 添加按钮
item.add_button(0, preload("res://icons/delete.png"), 0, false, "删除")
item.add_button(0, preload("res://icons/edit.png"), 1, false, "编辑")
return item
func add_editable_item(parent: TreeItem, text: String):
var item = tree.create_item(parent)
item.set_text(0, text)
item.set_editable(0, true) # 可编辑
return item
func add_checkbox_item(parent: TreeItem, text: String, checked: bool = false):
var item = tree.create_item(parent)
item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
item.set_text(0, text)
item.set_checked(0, checked)
item.set_editable(0, true) # 允许切换
return item
func add_range_item(parent: TreeItem, text: String, value: float):
var item = tree.create_item(parent)
item.set_cell_mode(0, TreeItem.CELL_MODE_RANGE)
item.set_text(0, text)
item.set_range(0, value)
item.set_range_config(0, 0, 100, 1) # min, max, step
item.set_editable(0, true)
return item
func find_item_by_text(text: String, column: int = 0) -> TreeItem:
return _find_item_recursive(root, text, column)
func _find_item_recursive(item: TreeItem, text: String, column: int) -> TreeItem:
if item.get_text(column) == text:
return item
var child = item.get_first_child()
while child:
var found = _find_item_recursive(child, text, column)
if found:
return found
child = child.get_next()
return null
func remove_item(item: TreeItem):
item.free()
func clear_tree():
tree.clear()
root = null
33.2 进度与范围控件
33.2.1 ProgressBar
# progress_controls.gd
# 进度控件
extends Control
@onready var progress_bar: ProgressBar = $ProgressBar
@onready var texture_progress: TextureProgressBar = $TextureProgressBar
func _ready():
_setup_progress_bar()
_setup_texture_progress()
func _setup_progress_bar():
progress_bar.min_value = 0
progress_bar.max_value = 100
progress_bar.value = 0
progress_bar.step = 1
# 是否显示百分比
progress_bar.show_percentage = true
# 填充方向
progress_bar.fill_mode = ProgressBar.FILL_BEGIN_TO_END
# 自定义样式
var style_bg = StyleBoxFlat.new()
style_bg.bg_color = Color(0.2, 0.2, 0.2)
style_bg.corner_radius_top_left = 5
style_bg.corner_radius_top_right = 5
style_bg.corner_radius_bottom_left = 5
style_bg.corner_radius_bottom_right = 5
var style_fill = StyleBoxFlat.new()
style_fill.bg_color = Color(0.2, 0.8, 0.3)
style_fill.corner_radius_top_left = 5
style_fill.corner_radius_top_right = 5
style_fill.corner_radius_bottom_left = 5
style_fill.corner_radius_bottom_right = 5
progress_bar.add_theme_stylebox_override("background", style_bg)
progress_bar.add_theme_stylebox_override("fill", style_fill)
func _setup_texture_progress():
# 纹理设置
texture_progress.texture_under = preload("res://progress_bg.png")
texture_progress.texture_over = preload("res://progress_fg.png")
texture_progress.texture_progress = preload("res://progress_fill.png")
# 填充模式
texture_progress.fill_mode = TextureProgressBar.FILL_LEFT_TO_RIGHT
# 九宫格拉伸
texture_progress.nine_patch_stretch = true
texture_progress.stretch_margin_left = 10
texture_progress.stretch_margin_right = 10
# 值设置
texture_progress.min_value = 0
texture_progress.max_value = 100
texture_progress.value = 50
## 动画进度
func animate_progress(target_value: float, duration: float = 0.5):
var tween = create_tween()
tween.tween_property(progress_bar, "value", target_value, duration)
tween.set_trans(Tween.TRANS_QUAD)
tween.set_ease(Tween.EASE_OUT)
## 带颜色变化的进度条
func update_progress_with_color(value: float):
progress_bar.value = value
var ratio = value / progress_bar.max_value
var color: Color
if ratio > 0.6:
color = Color.GREEN
elif ratio > 0.3:
color = Color.YELLOW
else:
color = Color.RED
var style = progress_bar.get_theme_stylebox("fill").duplicate()
if style is StyleBoxFlat:
style.bg_color = color
progress_bar.add_theme_stylebox_override("fill", style)
## 环形进度条(TextureProgressBar)
func setup_radial_progress():
texture_progress.fill_mode = TextureProgressBar.FILL_CLOCKWISE
texture_progress.radial_center_offset = Vector2.ZERO
texture_progress.radial_initial_angle = 270 # 从顶部开始
texture_progress.radial_fill_degrees = 360
33.2.2 Slider滑块
# slider_controls.gd
# 滑块控件
extends Control
@onready var h_slider: HSlider = $HSlider
@onready var v_slider: VSlider = $VSlider
func _ready():
_setup_h_slider()
_setup_v_slider()
func _setup_h_slider():
h_slider.min_value = 0
h_slider.max_value = 100
h_slider.step = 1
h_slider.value = 50
# 刻度
h_slider.tick_count = 11
h_slider.ticks_on_borders = true
# 可滚动
h_slider.scrollable = true
# 信号
h_slider.value_changed.connect(_on_h_slider_changed)
h_slider.drag_started.connect(_on_drag_started)
h_slider.drag_ended.connect(_on_drag_ended)
func _setup_v_slider():
v_slider.min_value = 0
v_slider.max_value = 100
v_slider.step = 5
v_slider.value = 50
v_slider.value_changed.connect(_on_v_slider_changed)
func _on_h_slider_changed(value: float):
print("水平滑块: ", value)
func _on_v_slider_changed(value: float):
print("垂直滑块: ", value)
func _on_drag_started():
print("开始拖动")
func _on_drag_ended(value_changed: bool):
print("结束拖动,值改变: ", value_changed)
## 自定义滑块样式
func customize_slider(slider: Slider):
# 滑轨样式
var grabber_area = StyleBoxFlat.new()
grabber_area.bg_color = Color(0.3, 0.3, 0.3)
grabber_area.corner_radius_top_left = 3
grabber_area.corner_radius_top_right = 3
grabber_area.corner_radius_bottom_left = 3
grabber_area.corner_radius_bottom_right = 3
# 滑块样式
var grabber = StyleBoxFlat.new()
grabber.bg_color = Color(0.8, 0.8, 0.8)
grabber.corner_radius_top_left = 10
grabber.corner_radius_top_right = 10
grabber.corner_radius_bottom_left = 10
grabber.corner_radius_bottom_right = 10
slider.add_theme_stylebox_override("grabber_area", grabber_area)
slider.add_theme_stylebox_override("grabber_area_highlight", grabber_area)
# 注意:grabber使用纹理而非样式盒
33.2.3 ScrollBar滚动条
# scrollbar_controls.gd
# 滚动条控件
extends Control
@onready var h_scroll: HScrollBar = $HScrollBar
@onready var v_scroll: VScrollBar = $VScrollBar
@onready var content: Control = $Content
func _ready():
_setup_scrollbars()
func _setup_scrollbars():
# 水平滚动条
h_scroll.min_value = 0
h_scroll.max_value = 1000
h_scroll.page = 200 # 可见区域大小
h_scroll.step = 1
# 垂直滚动条
v_scroll.min_value = 0
v_scroll.max_value = 2000
v_scroll.page = 400
# 连接信号
h_scroll.value_changed.connect(_on_h_scroll_changed)
v_scroll.value_changed.connect(_on_v_scroll_changed)
# 同步内容位置
_update_content_position()
func _on_h_scroll_changed(value: float):
content.position.x = -value
func _on_v_scroll_changed(value: float):
content.position.y = -value
func _update_content_position():
content.position = Vector2(-h_scroll.value, -v_scroll.value)
## 鼠标滚轮支持
func _gui_input(event):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
v_scroll.value -= v_scroll.page * 0.1
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
v_scroll.value += v_scroll.page * 0.1
elif event.button_index == MOUSE_BUTTON_WHEEL_LEFT:
h_scroll.value -= h_scroll.page * 0.1
elif event.button_index == MOUSE_BUTTON_WHEEL_RIGHT:
h_scroll.value += h_scroll.page * 0.1
## 平滑滚动
func smooth_scroll_to(target: float, vertical: bool = true):
var scroll = v_scroll if vertical else h_scroll
var tween = create_tween()
tween.tween_property(scroll, "value", target, 0.3)
tween.set_trans(Tween.TRANS_QUAD)
tween.set_ease(Tween.EASE_OUT)
33.3 图形与纹理控件
33.3.1 TextureRect
# texture_rect_usage.gd
# TextureRect控件
extends Control
@onready var texture_rect: TextureRect = $TextureRect
func _ready():
_setup_texture_rect()
func _setup_texture_rect():
# 加载纹理
texture_rect.texture = preload("res://image.png")
# 拉伸模式
texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
# 可选拉伸模式:
# STRETCH_SCALE: 缩放填充
# STRETCH_TILE: 平铺
# STRETCH_KEEP: 保持原始大小
# STRETCH_KEEP_CENTERED: 居中保持原始大小
# STRETCH_KEEP_ASPECT: 保持比例
# STRETCH_KEEP_ASPECT_CENTERED: 居中保持比例
# STRETCH_KEEP_ASPECT_COVERED: 保持比例覆盖
# 翻转
texture_rect.flip_h = false
texture_rect.flip_v = false
# 扩展模式
texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
## 动态加载纹理
func load_texture_async(path: String):
var tex = load(path)
if tex:
texture_rect.texture = tex
## 从网络加载图片
func load_texture_from_url(url: String):
var http = HTTPRequest.new()
add_child(http)
http.request_completed.connect(_on_http_completed.bind(http))
http.request(url)
func _on_http_completed(result: int, code: int, headers: PackedStringArray, body: PackedByteArray, http: HTTPRequest):
http.queue_free()
if result != HTTPRequest.RESULT_SUCCESS:
return
var image = Image.new()
var error = image.load_png_from_buffer(body)
if error != OK:
error = image.load_jpg_from_buffer(body)
if error == OK:
var tex = ImageTexture.create_from_image(image)
texture_rect.texture = tex
33.3.2 NinePatchRect
# nine_patch_usage.gd
# NinePatchRect九宫格控件
extends Control
@onready var nine_patch: NinePatchRect = $NinePatchRect
func _ready():
_setup_nine_patch()
func _setup_nine_patch():
# 设置纹理
nine_patch.texture = preload("res://panel.png")
# 设置边距(不拉伸的区域)
nine_patch.patch_margin_left = 20
nine_patch.patch_margin_right = 20
nine_patch.patch_margin_top = 20
nine_patch.patch_margin_bottom = 20
# 拉伸模式
nine_patch.axis_stretch_horizontal = NinePatchRect.AXIS_STRETCH_MODE_STRETCH
nine_patch.axis_stretch_vertical = NinePatchRect.AXIS_STRETCH_MODE_STRETCH
# 可选模式:
# AXIS_STRETCH_MODE_STRETCH: 拉伸
# AXIS_STRETCH_MODE_TILE: 平铺
# AXIS_STRETCH_MODE_TILE_FIT: 适应平铺
# 是否绘制中心区域
nine_patch.draw_center = true
# 区域选择(纹理图集)
nine_patch.region_rect = Rect2(0, 0, 64, 64)
## 创建自适应面板
func create_adaptive_panel(min_size: Vector2) -> NinePatchRect:
var panel = NinePatchRect.new()
panel.texture = preload("res://panel.png")
panel.patch_margin_left = 15
panel.patch_margin_right = 15
panel.patch_margin_top = 15
panel.patch_margin_bottom = 15
panel.custom_minimum_size = min_size
return panel
33.3.3 ColorRect与其他
# color_rect_usage.gd
# ColorRect和其他图形控件
extends Control
func _ready():
_demo_color_rect()
_demo_video_player()
_demo_graph_edit()
func _demo_color_rect():
var color_rect = ColorRect.new()
color_rect.color = Color(0.2, 0.4, 0.8, 0.5)
color_rect.size = Vector2(100, 100)
add_child(color_rect)
func _demo_video_player():
var video = VideoStreamPlayer.new()
video.stream = preload("res://video.ogv") if ResourceLoader.exists("res://video.ogv") else null
video.autoplay = false
video.loop = true
video.volume_db = 0
# 控制
# video.play()
# video.pause()
# video.stop()
# video.stream_position = 10.0 # 跳转到10秒
video.finished.connect(func(): print("视频播放完成"))
func _demo_graph_edit():
var graph = GraphEdit.new()
graph.custom_minimum_size = Vector2(400, 300)
# 设置
graph.right_disconnects = true
graph.scroll_offset = Vector2.ZERO
graph.zoom = 1.0
graph.zoom_min = 0.5
graph.zoom_max = 2.0
graph.show_zoom_label = true
graph.show_grid = true
graph.snapping_enabled = true
graph.snapping_distance = 20
graph.minimap_enabled = true
# 创建图节点
var node1 = _create_graph_node("节点1", Vector2(50, 50))
var node2 = _create_graph_node("节点2", Vector2(250, 100))
graph.add_child(node1)
graph.add_child(node2)
# 连接节点
graph.connect_node("节点1", 0, "节点2", 0)
# 信号
graph.connection_request.connect(_on_connection_request.bind(graph))
graph.disconnection_request.connect(_on_disconnection_request.bind(graph))
add_child(graph)
func _create_graph_node(title: String, pos: Vector2) -> GraphNode:
var node = GraphNode.new()
node.title = title
node.name = title
node.position_offset = pos
node.resizable = true
# 添加槽位
var input = Label.new()
input.text = "输入"
node.add_child(input)
node.set_slot(0, true, 0, Color.RED, true, 0, Color.GREEN)
return node
func _on_connection_request(from: StringName, from_port: int, to: StringName, to_port: int, graph: GraphEdit):
graph.connect_node(from, from_port, to, to_port)
func _on_disconnection_request(from: StringName, from_port: int, to: StringName, to_port: int, graph: GraphEdit):
graph.disconnect_node(from, from_port, to, to_port)
33.4 特殊控件
33.4.1 TabBar标签栏
# tab_bar_usage.gd
# TabBar标签栏控件
extends Control
@onready var tab_bar: TabBar = $TabBar
@onready var content_container: Control = $ContentContainer
var tab_contents: Array[Control] = []
func _ready():
_setup_tab_bar()
_create_tabs()
func _setup_tab_bar():
# 对齐方式
tab_bar.tab_alignment = TabBar.ALIGNMENT_LEFT
# 允许关闭
tab_bar.tab_close_display_policy = TabBar.CLOSE_BUTTON_SHOW_ACTIVE_ONLY
# 可拖拽
tab_bar.drag_to_rearrange_enabled = true
# 滚动
tab_bar.scrolling_enabled = true
# 选择新标签
tab_bar.select_with_rmb = false
# 信号
tab_bar.tab_changed.connect(_on_tab_changed)
tab_bar.tab_close_pressed.connect(_on_tab_close_pressed)
tab_bar.tab_rmb_clicked.connect(_on_tab_rmb_clicked)
tab_bar.active_tab_rearranged.connect(_on_tab_rearranged)
func _create_tabs():
var tabs_data = [
{"title": "文件", "icon": "res://icons/file.png"},
{"title": "编辑", "icon": "res://icons/edit.png"},
{"title": "视图", "icon": null},
]
for data in tabs_data:
var idx = tab_bar.add_tab(data.title)
if data.icon and ResourceLoader.exists(data.icon):
tab_bar.set_tab_icon(idx, load(data.icon))
# 创建对应内容
var content = Control.new()
content.name = data.title
content.visible = idx == 0
content_container.add_child(content)
tab_contents.append(content)
# 设置禁用标签
# tab_bar.set_tab_disabled(2, true)
func _on_tab_changed(tab_idx: int):
for i in range(tab_contents.size()):
tab_contents[i].visible = i == tab_idx
func _on_tab_close_pressed(tab_idx: int):
# 确认关闭
var title = tab_bar.get_tab_title(tab_idx)
print("关闭标签: ", title)
tab_bar.remove_tab(tab_idx)
var content = tab_contents[tab_idx]
tab_contents.remove_at(tab_idx)
content.queue_free()
func _on_tab_rmb_clicked(tab_idx: int):
# 显示右键菜单
var menu = PopupMenu.new()
menu.add_item("关闭", 0)
menu.add_item("关闭其他", 1)
menu.add_item("关闭所有", 2)
menu.id_pressed.connect(_on_tab_menu_pressed.bind(tab_idx))
add_child(menu)
menu.popup(Rect2(get_global_mouse_position(), Vector2.ZERO))
func _on_tab_menu_pressed(id: int, tab_idx: int):
match id:
0: _on_tab_close_pressed(tab_idx)
1: _close_other_tabs(tab_idx)
2: _close_all_tabs()
func _on_tab_rearranged(idx_to: int):
print("标签移动到: ", idx_to)
func _close_other_tabs(keep_idx: int):
for i in range(tab_bar.tab_count - 1, -1, -1):
if i != keep_idx:
_on_tab_close_pressed(i)
func _close_all_tabs():
while tab_bar.tab_count > 0:
_on_tab_close_pressed(0)
## 添加新标签
func add_new_tab(title: String) -> int:
var idx = tab_bar.add_tab(title)
var content = Control.new()
content.name = title
content.visible = false
content_container.add_child(content)
tab_contents.append(content)
return idx
33.4.2 SplitContainer分割容器
# split_container_usage.gd
# 分割容器详解
extends Control
@onready var h_split: HSplitContainer = $HSplitContainer
@onready var v_split: VSplitContainer = $HSplitContainer/VSplitContainer
func _ready():
_setup_split_containers()
func _setup_split_containers():
# 水平分割
h_split.split_offset = 200
h_split.collapsed = false
h_split.dragger_visibility = SplitContainer.DRAGGER_VISIBLE
# 垂直分割
v_split.split_offset = 150
# 信号
h_split.dragged.connect(_on_h_dragged)
v_split.dragged.connect(_on_v_dragged)
func _on_h_dragged(offset: int):
print("水平分割偏移: ", offset)
func _on_v_dragged(offset: int):
print("垂直分割偏移: ", offset)
## 折叠面板
func collapse_left_panel():
h_split.collapsed = true
func expand_left_panel():
h_split.collapsed = false
h_split.split_offset = 200
## 创建可调整的多面板布局
func create_multi_panel_layout() -> HSplitContainer:
var main_split = HSplitContainer.new()
# 左侧面板
var left_panel = PanelContainer.new()
left_panel.custom_minimum_size = Vector2(150, 0)
main_split.add_child(left_panel)
# 右侧(再次分割)
var right_split = VSplitContainer.new()
# 主内容区
var main_content = PanelContainer.new()
right_split.add_child(main_content)
# 底部面板
var bottom_panel = PanelContainer.new()
bottom_panel.custom_minimum_size = Vector2(0, 100)
right_split.add_child(bottom_panel)
main_split.add_child(right_split)
return main_split
33.4.3 LinkButton链接按钮
# link_button_usage.gd
# LinkButton和其他特殊按钮
extends Control
func _ready():
_setup_link_button()
_setup_menu_button()
_setup_option_button()
func _setup_link_button():
var link = LinkButton.new()
link.text = "访问官网"
link.uri = "https://godotengine.org"
link.underline = LinkButton.UNDERLINE_MODE_ALWAYS
# 手动处理点击
link.pressed.connect(func():
OS.shell_open(link.uri)
)
add_child(link)
func _setup_menu_button():
var menu_btn = MenuButton.new()
menu_btn.text = "菜单"
var popup = menu_btn.get_popup()
popup.add_item("选项1", 0)
popup.add_item("选项2", 1)
popup.add_separator()
popup.add_item("选项3", 2)
popup.id_pressed.connect(func(id):
print("菜单选择: ", id)
)
add_child(menu_btn)
func _setup_option_button():
var opt = OptionButton.new()
opt.add_item("低", 0)
opt.add_item("中", 1)
opt.add_item("高", 2)
opt.select(1)
opt.item_selected.connect(func(idx):
print("选择: ", opt.get_item_text(idx))
)
add_child(opt)
33.5 自定义控件
33.5.1 创建自定义控件
# custom_control.gd
# 自定义控件基类
@tool
class_name CustomSlider
extends Control
## 导出属性
@export var min_value: float = 0.0:
set(value):
min_value = value
_update_appearance()
@export var max_value: float = 100.0:
set(value):
max_value = value
_update_appearance()
@export var value: float = 0.0:
set(v):
value = clamp(v, min_value, max_value)
value_changed.emit(value)
_update_appearance()
@export var step: float = 1.0
@export var track_color: Color = Color(0.3, 0.3, 0.3)
@export var fill_color: Color = Color(0.2, 0.6, 0.9)
@export var handle_color: Color = Color.WHITE
@export var handle_radius: float = 8.0
@export var track_height: float = 4.0
## 信号
signal value_changed(new_value: float)
signal drag_started
signal drag_ended
## 状态
var _dragging: bool = false
var _hover: bool = false
func _ready():
mouse_filter = Control.MOUSE_FILTER_STOP
custom_minimum_size = Vector2(100, handle_radius * 2 + 4)
func _draw():
var center_y = size.y / 2
var track_start = handle_radius
var track_end = size.x - handle_radius
var track_width = track_end - track_start
# 绘制轨道背景
var track_rect = Rect2(
track_start,
center_y - track_height / 2,
track_width,
track_height
)
draw_rect(track_rect, track_color)
# 绘制填充
var fill_ratio = (value - min_value) / (max_value - min_value) if max_value > min_value else 0
var fill_rect = Rect2(
track_start,
center_y - track_height / 2,
track_width * fill_ratio,
track_height
)
draw_rect(fill_rect, fill_color)
# 绘制手柄
var handle_x = track_start + track_width * fill_ratio
var handle_pos = Vector2(handle_x, center_y)
var current_handle_color = handle_color
if _hover or _dragging:
current_handle_color = handle_color.lightened(0.2)
draw_circle(handle_pos, handle_radius, current_handle_color)
# 手柄边框
draw_arc(handle_pos, handle_radius, 0, TAU, 32, Color.BLACK.lightened(0.3), 2)
func _gui_input(event):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
_dragging = true
drag_started.emit()
_update_value_from_mouse(event.position)
accept_event()
else:
if _dragging:
_dragging = false
drag_ended.emit()
elif event is InputEventMouseMotion:
# 更新悬停状态
var handle_pos = _get_handle_position()
_hover = handle_pos.distance_to(event.position) < handle_radius * 1.5
queue_redraw()
if _dragging:
_update_value_from_mouse(event.position)
accept_event()
func _update_value_from_mouse(mouse_pos: Vector2):
var track_start = handle_radius
var track_end = size.x - handle_radius
var ratio = clamp((mouse_pos.x - track_start) / (track_end - track_start), 0, 1)
var new_value = min_value + ratio * (max_value - min_value)
if step > 0:
new_value = round(new_value / step) * step
value = new_value
func _get_handle_position() -> Vector2:
var track_start = handle_radius
var track_end = size.x - handle_radius
var fill_ratio = (value - min_value) / (max_value - min_value) if max_value > min_value else 0
return Vector2(track_start + (track_end - track_start) * fill_ratio, size.y / 2)
func _update_appearance():
queue_redraw()
func _notification(what):
if what == NOTIFICATION_MOUSE_EXIT:
_hover = false
queue_redraw()
33.5.2 自定义健康条
# custom_health_bar.gd
# 自定义健康条控件
@tool
class_name HealthBar
extends Control
@export var max_health: float = 100.0:
set(value):
max_health = max(1, value)
_update_display()
@export var current_health: float = 100.0:
set(value):
var old_health = current_health
current_health = clamp(value, 0, max_health)
if old_health != current_health:
health_changed.emit(current_health, max_health)
if current_health <= 0:
health_depleted.emit()
_update_display()
@export var show_text: bool = true
@export var animate_changes: bool = true
@export var animation_duration: float = 0.3
@export_group("Colors")
@export var background_color: Color = Color(0.2, 0.2, 0.2)
@export var health_color: Color = Color(0.2, 0.8, 0.2)
@export var damage_color: Color = Color(0.8, 0.2, 0.2)
@export var low_health_color: Color = Color(0.8, 0.8, 0.2)
@export var low_health_threshold: float = 0.3
@export_group("Appearance")
@export var corner_radius: float = 5.0
@export var border_width: float = 2.0
@export var border_color: Color = Color(0.4, 0.4, 0.4)
signal health_changed(current: float, maximum: float)
signal health_depleted
var _display_health: float = 100.0
var _damage_display: float = 0.0
var _tween: Tween
func _ready():
custom_minimum_size = Vector2(100, 20)
_display_health = current_health
func _draw():
var rect = Rect2(Vector2.ZERO, size)
# 绘制背景
_draw_rounded_rect(rect, background_color)
# 绘制伤害显示(延迟减少的部分)
if _damage_display > 0:
var damage_ratio = (_display_health + _damage_display) / max_health
var damage_rect = Rect2(0, 0, size.x * damage_ratio, size.y)
_draw_rounded_rect(damage_rect, damage_color)
# 绘制当前血量
var health_ratio = _display_health / max_health
if health_ratio > 0:
var health_rect = Rect2(0, 0, size.x * health_ratio, size.y)
var color = health_color if health_ratio > low_health_threshold else low_health_color
_draw_rounded_rect(health_rect, color)
# 绘制边框
_draw_rounded_rect_outline(rect, border_color, border_width)
# 绘制文字
if show_text:
var text = "%d / %d" % [int(current_health), int(max_health)]
var font = get_theme_font("font", "Label")
var font_size = get_theme_font_size("font_size", "Label")
var text_size = font.get_string_size(text, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size)
var text_pos = (size - text_size) / 2 + Vector2(0, text_size.y * 0.75)
draw_string(font, text_pos, text, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size, Color.WHITE)
func _draw_rounded_rect(rect: Rect2, color: Color):
draw_rect(rect, color) # 简化版,完整版使用draw_primitive绘制圆角
func _draw_rounded_rect_outline(rect: Rect2, color: Color, width: float):
draw_rect(rect, color, false, width)
func _update_display():
if not animate_changes or not is_inside_tree():
_display_health = current_health
_damage_display = 0
queue_redraw()
return
if _tween:
_tween.kill()
var old_display = _display_health
var damage_taken = old_display - current_health
if damage_taken > 0:
_damage_display = damage_taken
_tween = create_tween()
_tween.tween_property(self, "_display_health", current_health, animation_duration)
_tween.parallel().tween_property(self, "_damage_display", 0.0, animation_duration * 1.5)
_tween.tween_callback(queue_redraw)
func _process(_delta):
if animate_changes:
queue_redraw()
## 公共方法
func take_damage(amount: float):
current_health -= amount
func heal(amount: float):
current_health += amount
func set_health(value: float):
current_health = value
func get_health_ratio() -> float:
return current_health / max_health if max_health > 0 else 0
func is_dead() -> bool:
return current_health <= 0
33.6 本章小结
本章详细介绍了Godot 4中各类UI控件的使用:
- 列表与选择控件:ItemList、OptionButton、Tree
- 进度与范围控件:ProgressBar、Slider、ScrollBar
- 图形与纹理控件:TextureRect、NinePatchRect、ColorRect
- 特殊控件:TabBar、SplitContainer、LinkButton
- 自定义控件:创建自定义滑块和健康条
掌握这些控件的使用,能够构建出功能丰富、交互友好的游戏界面。下一章我们将学习UI布局管理,了解如何组织和排列这些控件。