第三十三章:控件详解

第三十三章:控件详解

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控件的使用:

  1. 列表与选择控件:ItemList、OptionButton、Tree
  2. 进度与范围控件:ProgressBar、Slider、ScrollBar
  3. 图形与纹理控件:TextureRect、NinePatchRect、ColorRect
  4. 特殊控件:TabBar、SplitContainer、LinkButton
  5. 自定义控件:创建自定义滑块和健康条

掌握这些控件的使用,能够构建出功能丰富、交互友好的游戏界面。下一章我们将学习UI布局管理,了解如何组织和排列这些控件。

← 返回目录