第三十四章:UI布局管理

第三十四章:UI布局管理

良好的UI布局是创建专业游戏界面的关键。Godot 4提供了强大的布局系统,包括锚点、容器、响应式设计等功能。本章将深入讲解如何有效管理UI布局,创建适应不同屏幕尺寸的界面。

34.1 布局基础

34.1.1 锚点系统详解

# anchor_system.gd
# 锚点系统深入讲解

extends Control

## 锚点概念
## 锚点是相对于父容器的比例位置(0-1)
## 0 = 左/上边缘
## 0.5 = 中心
## 1 = 右/下边缘
##
## 偏移(Offset)是相对于锚点位置的像素偏移

func _ready():
    _demonstrate_anchor_presets()

## 锚点预设示例
func _demonstrate_anchor_presets():
    # 左上角固定
    var top_left = _create_panel("左上角")
    _anchor_top_left(top_left)
    
    # 右上角固定
    var top_right = _create_panel("右上角")
    _anchor_top_right(top_right)
    
    # 底部居中
    var bottom_center = _create_panel("底部居中")
    _anchor_bottom_center(bottom_center)
    
    # 全屏填充
    var full = _create_panel("全屏")
    _anchor_full_rect(full)

func _create_panel(title: String) -> Panel:
    var panel = Panel.new()
    panel.custom_minimum_size = Vector2(100, 50)
    
    var label = Label.new()
    label.text = title
    label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
    panel.add_child(label)
    
    add_child(panel)
    return panel

## 锚点设置函数
func _anchor_top_left(control: Control):
    control.anchor_left = 0
    control.anchor_top = 0
    control.anchor_right = 0
    control.anchor_bottom = 0
    control.offset_left = 10
    control.offset_top = 10
    control.offset_right = 110
    control.offset_bottom = 60

func _anchor_top_right(control: Control):
    control.anchor_left = 1
    control.anchor_top = 0
    control.anchor_right = 1
    control.anchor_bottom = 0
    control.offset_left = -110
    control.offset_top = 10
    control.offset_right = -10
    control.offset_bottom = 60

func _anchor_bottom_center(control: Control):
    control.anchor_left = 0.5
    control.anchor_top = 1
    control.anchor_right = 0.5
    control.anchor_bottom = 1
    control.offset_left = -50
    control.offset_top = -60
    control.offset_right = 50
    control.offset_bottom = -10

func _anchor_full_rect(control: Control):
    control.anchor_left = 0
    control.anchor_top = 0
    control.anchor_right = 1
    control.anchor_bottom = 1
    control.offset_left = 0
    control.offset_top = 0
    control.offset_right = 0
    control.offset_bottom = 0

## 边距保持布局
func _anchor_with_margins(control: Control, margin: float):
    control.anchor_left = 0
    control.anchor_top = 0
    control.anchor_right = 1
    control.anchor_bottom = 1
    control.offset_left = margin
    control.offset_top = margin
    control.offset_right = -margin
    control.offset_bottom = -margin

## 宽度填充,高度固定
func _anchor_horizontal_fill(control: Control, height: float, top_margin: float):
    control.anchor_left = 0
    control.anchor_top = 0
    control.anchor_right = 1
    control.anchor_bottom = 0
    control.offset_left = 10
    control.offset_top = top_margin
    control.offset_right = -10
    control.offset_bottom = top_margin + height

## 高度填充,宽度固定
func _anchor_vertical_fill(control: Control, width: float, left_margin: float):
    control.anchor_left = 0
    control.anchor_top = 0
    control.anchor_right = 0
    control.anchor_bottom = 1
    control.offset_left = left_margin
    control.offset_top = 10
    control.offset_right = left_margin + width
    control.offset_bottom = -10

34.1.2 增长方向

# grow_direction.gd
# 增长方向设置

extends Control

## 增长方向(Grow Direction)
## 控制Control节点在调整大小时如何增长
## GROW_DIRECTION_BEGIN: 向开始方向增长(左/上)
## GROW_DIRECTION_END: 向结束方向增长(右/下)
## GROW_DIRECTION_BOTH: 双向增长

func setup_grow_directions():
    # 向右下增长(默认)
    var panel1 = Panel.new()
    panel1.grow_horizontal = Control.GROW_DIRECTION_END
    panel1.grow_vertical = Control.GROW_DIRECTION_END
    
    # 向左上增长
    var panel2 = Panel.new()
    panel2.grow_horizontal = Control.GROW_DIRECTION_BEGIN
    panel2.grow_vertical = Control.GROW_DIRECTION_BEGIN
    
    # 双向增长(居中)
    var panel3 = Panel.new()
    panel3.grow_horizontal = Control.GROW_DIRECTION_BOTH
    panel3.grow_vertical = Control.GROW_DIRECTION_BOTH

## 实用示例:居中弹窗
func center_popup(control: Control, popup_size: Vector2):
    control.grow_horizontal = Control.GROW_DIRECTION_BOTH
    control.grow_vertical = Control.GROW_DIRECTION_BOTH
    
    control.anchor_left = 0.5
    control.anchor_top = 0.5
    control.anchor_right = 0.5
    control.anchor_bottom = 0.5
    
    control.offset_left = -popup_size.x / 2
    control.offset_top = -popup_size.y / 2
    control.offset_right = popup_size.x / 2
    control.offset_bottom = popup_size.y / 2

## 实用示例:右下角固定
func anchor_bottom_right_fixed(control: Control, size: Vector2):
    control.grow_horizontal = Control.GROW_DIRECTION_BEGIN
    control.grow_vertical = Control.GROW_DIRECTION_BEGIN
    
    control.anchor_left = 1
    control.anchor_top = 1
    control.anchor_right = 1
    control.anchor_bottom = 1
    
    control.offset_left = -size.x - 10
    control.offset_top = -size.y - 10
    control.offset_right = -10
    control.offset_bottom = -10

34.1.3 最小大小与大小标志

# size_management.gd
# 大小管理

extends Control

## 最小大小
## custom_minimum_size: 手动设置的最小大小
## get_minimum_size(): 返回控件需要的最小大小
## get_combined_minimum_size(): 返回考虑子节点后的最小大小

func setup_minimum_sizes():
    var button = Button.new()
    button.text = "按钮"
    
    # 设置自定义最小大小
    button.custom_minimum_size = Vector2(120, 40)
    
    # 获取实际最小大小
    var min_size = button.get_combined_minimum_size()
    print("最小大小: ", min_size)

## 大小标志详解
func explain_size_flags():
    var control = Control.new()
    
    # SIZE_SHRINK_BEGIN: 收缩到容器开始位置
    control.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
    
    # SIZE_SHRINK_CENTER: 收缩到容器中心
    control.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
    
    # SIZE_SHRINK_END: 收缩到容器结束位置
    control.size_flags_horizontal = Control.SIZE_SHRINK_END
    
    # SIZE_FILL: 填充分配的空间
    control.size_flags_horizontal = Control.SIZE_FILL
    
    # SIZE_EXPAND: 扩展以获取额外空间
    control.size_flags_horizontal = Control.SIZE_EXPAND
    
    # SIZE_EXPAND_FILL: 扩展并填充
    control.size_flags_horizontal = Control.SIZE_EXPAND_FILL

## 拉伸比例
func setup_stretch_ratios():
    var hbox = HBoxContainer.new()
    add_child(hbox)
    
    # 1:2:1 的比例分配
    var left = Panel.new()
    left.size_flags_horizontal = Control.SIZE_EXPAND_FILL
    left.size_flags_stretch_ratio = 1
    hbox.add_child(left)
    
    var center = Panel.new()
    center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
    center.size_flags_stretch_ratio = 2
    hbox.add_child(center)
    
    var right = Panel.new()
    right.size_flags_horizontal = Control.SIZE_EXPAND_FILL
    right.size_flags_stretch_ratio = 1
    hbox.add_child(right)

34.2 容器布局

34.2.1 BoxContainer

# box_container_layout.gd
# BoxContainer布局

extends Control

func _ready():
    _create_horizontal_layout()
    _create_vertical_layout()
    _create_nested_layout()

## 水平布局
func _create_horizontal_layout() -> HBoxContainer:
    var hbox = HBoxContainer.new()
    hbox.set_anchors_preset(Control.PRESET_TOP_WIDE)
    hbox.custom_minimum_size = Vector2(0, 50)
    
    # 子元素间距
    hbox.add_theme_constant_override("separation", 10)
    
    # 对齐方式
    hbox.alignment = BoxContainer.ALIGNMENT_CENTER
    
    # 添加按钮
    for i in range(5):
        var btn = Button.new()
        btn.text = "按钮%d" % (i + 1)
        
        if i == 2:
            # 中间按钮扩展
            btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
        else:
            btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
        
        hbox.add_child(btn)
    
    add_child(hbox)
    return hbox

## 垂直布局
func _create_vertical_layout() -> VBoxContainer:
    var vbox = VBoxContainer.new()
    vbox.set_anchors_preset(Control.PRESET_LEFT_WIDE)
    vbox.custom_minimum_size = Vector2(200, 0)
    
    vbox.add_theme_constant_override("separation", 5)
    vbox.alignment = BoxContainer.ALIGNMENT_BEGIN
    
    # 标题
    var title = Label.new()
    title.text = "菜单"
    title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
    title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
    vbox.add_child(title)
    
    # 分隔符
    vbox.add_child(HSeparator.new())
    
    # 菜单项
    for item in ["新建", "打开", "保存", "退出"]:
        var btn = Button.new()
        btn.text = item
        btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
        vbox.add_child(btn)
    
    # 弹性空间
    var spacer = Control.new()
    spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
    vbox.add_child(spacer)
    
    # 底部状态
    var status = Label.new()
    status.text = "就绪"
    vbox.add_child(status)
    
    add_child(vbox)
    return vbox

## 嵌套布局
func _create_nested_layout() -> VBoxContainer:
    var main = VBoxContainer.new()
    main.set_anchors_preset(Control.PRESET_FULL_RECT)
    
    # 顶部工具栏
    var toolbar = HBoxContainer.new()
    toolbar.custom_minimum_size = Vector2(0, 40)
    for tool_name in ["文件", "编辑", "视图", "帮助"]:
        var btn = Button.new()
        btn.text = tool_name
        btn.flat = true
        toolbar.add_child(btn)
    main.add_child(toolbar)
    
    # 中间区域(水平分割)
    var middle = HBoxContainer.new()
    middle.size_flags_vertical = Control.SIZE_EXPAND_FILL
    
    # 左侧面板
    var left_panel = Panel.new()
    left_panel.custom_minimum_size = Vector2(200, 0)
    middle.add_child(left_panel)
    
    # 主内容区
    var content = Panel.new()
    content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
    middle.add_child(content)
    
    # 右侧面板
    var right_panel = Panel.new()
    right_panel.custom_minimum_size = Vector2(200, 0)
    middle.add_child(right_panel)
    
    main.add_child(middle)
    
    # 底部状态栏
    var statusbar = Panel.new()
    statusbar.custom_minimum_size = Vector2(0, 30)
    main.add_child(statusbar)
    
    add_child(main)
    return main

34.2.2 GridContainer

# grid_container_layout.gd
# GridContainer网格布局

extends Control

func _ready():
    _create_inventory_grid()
    _create_settings_form()
    _create_dynamic_grid()

## 物品栏网格
func _create_inventory_grid() -> GridContainer:
    var grid = GridContainer.new()
    grid.columns = 6
    
    grid.add_theme_constant_override("h_separation", 5)
    grid.add_theme_constant_override("v_separation", 5)
    
    # 创建格子
    for i in range(24):
        var slot = _create_inventory_slot(i)
        grid.add_child(slot)
    
    add_child(grid)
    return grid

func _create_inventory_slot(index: int) -> Panel:
    var slot = Panel.new()
    slot.custom_minimum_size = Vector2(50, 50)
    
    var style = StyleBoxFlat.new()
    style.bg_color = Color(0.2, 0.2, 0.2)
    style.border_width_left = 1
    style.border_width_right = 1
    style.border_width_top = 1
    style.border_width_bottom = 1
    style.border_color = Color(0.4, 0.4, 0.4)
    slot.add_theme_stylebox_override("panel", style)
    
    return slot

## 设置表单
func _create_settings_form() -> GridContainer:
    var grid = GridContainer.new()
    grid.columns = 2
    
    grid.add_theme_constant_override("h_separation", 20)
    grid.add_theme_constant_override("v_separation", 10)
    
    var settings = [
        {"label": "玩家名称", "type": "text"},
        {"label": "音量", "type": "slider"},
        {"label": "难度", "type": "option"},
        {"label": "全屏", "type": "check"},
    ]
    
    for setting in settings:
        # 标签
        var label = Label.new()
        label.text = setting.label + ":"
        label.size_flags_horizontal = Control.SIZE_SHRINK_END
        grid.add_child(label)
        
        # 控件
        var control: Control
        match setting.type:
            "text":
                control = LineEdit.new()
                control.custom_minimum_size = Vector2(200, 0)
            "slider":
                control = HSlider.new()
                control.custom_minimum_size = Vector2(200, 0)
            "option":
                control = OptionButton.new()
                control.add_item("简单")
                control.add_item("普通")
                control.add_item("困难")
            "check":
                control = CheckButton.new()
        
        control.size_flags_horizontal = Control.SIZE_EXPAND_FILL
        grid.add_child(control)
    
    add_child(grid)
    return grid

## 动态网格
func _create_dynamic_grid() -> GridContainer:
    var grid = GridContainer.new()
    grid.columns = 4
    
    add_child(grid)
    return grid

## 根据容器大小调整列数
func adjust_grid_columns(grid: GridContainer, item_width: float):
    var available_width = grid.size.x
    var separation = grid.get_theme_constant("h_separation")
    var columns = int(available_width / (item_width + separation))
    grid.columns = max(1, columns)

34.2.3 FlowContainer

# flow_container_layout.gd
# FlowContainer流式布局

extends Control

func _ready():
    _create_tag_cloud()
    _create_button_bar()

## 标签云
func _create_tag_cloud() -> HFlowContainer:
    var flow = HFlowContainer.new()
    flow.set_anchors_preset(Control.PRESET_TOP_WIDE)
    flow.custom_minimum_size = Vector2(0, 100)
    
    flow.add_theme_constant_override("h_separation", 5)
    flow.add_theme_constant_override("v_separation", 5)
    
    var tags = ["动作", "冒险", "角色扮演", "策略", "模拟", "体育", "竞速", 
                "益智", "音乐", "恐怖", "生存", "沙盒"]
    
    for tag in tags:
        var btn = Button.new()
        btn.text = tag
        btn.toggle_mode = true
        flow.add_child(btn)
    
    add_child(flow)
    return flow

## 按钮栏(自动换行)
func _create_button_bar() -> HFlowContainer:
    var flow = HFlowContainer.new()
    flow.alignment = FlowContainer.ALIGNMENT_CENTER
    
    flow.add_theme_constant_override("h_separation", 10)
    flow.add_theme_constant_override("v_separation", 10)
    
    for i in range(10):
        var btn = Button.new()
        btn.text = "选项 %d" % (i + 1)
        btn.custom_minimum_size = Vector2(80, 40)
        flow.add_child(btn)
    
    add_child(flow)
    return flow

## 垂直流式布局
func _create_vertical_flow() -> VFlowContainer:
    var flow = VFlowContainer.new()
    flow.custom_minimum_size = Vector2(300, 200)
    
    for i in range(15):
        var label = Label.new()
        label.text = "项目 %d" % (i + 1)
        flow.add_child(label)
    
    add_child(flow)
    return flow

34.2.4 其他容器

# other_containers.gd
# 其他容器类型

extends Control

## CenterContainer居中容器
func create_center_container() -> CenterContainer:
    var center = CenterContainer.new()
    center.set_anchors_preset(Control.PRESET_FULL_RECT)
    
    # 使用大小标志控制居中行为
    center.use_top_left = false  # true时内容靠左上
    
    var content = Panel.new()
    content.custom_minimum_size = Vector2(200, 100)
    center.add_child(content)
    
    add_child(center)
    return center

## MarginContainer边距容器
func create_margin_container() -> MarginContainer:
    var margin = MarginContainer.new()
    margin.set_anchors_preset(Control.PRESET_FULL_RECT)
    
    margin.add_theme_constant_override("margin_left", 20)
    margin.add_theme_constant_override("margin_right", 20)
    margin.add_theme_constant_override("margin_top", 10)
    margin.add_theme_constant_override("margin_bottom", 10)
    
    var content = Panel.new()
    content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
    content.size_flags_vertical = Control.SIZE_EXPAND_FILL
    margin.add_child(content)
    
    add_child(margin)
    return margin

## AspectRatioContainer比例容器
func create_aspect_container() -> AspectRatioContainer:
    var aspect = AspectRatioContainer.new()
    aspect.set_anchors_preset(Control.PRESET_FULL_RECT)
    
    # 宽高比
    aspect.ratio = 16.0 / 9.0
    
    # 拉伸模式
    aspect.stretch_mode = AspectRatioContainer.STRETCH_FIT
    # STRETCH_WIDTH_CONTROLS_HEIGHT: 宽度控制高度
    # STRETCH_HEIGHT_CONTROLS_WIDTH: 高度控制宽度
    # STRETCH_FIT: 适应(保持比例,可能有空白)
    # STRETCH_COVER: 覆盖(保持比例,可能裁切)
    
    # 对齐
    aspect.alignment_horizontal = AspectRatioContainer.ALIGNMENT_CENTER
    aspect.alignment_vertical = AspectRatioContainer.ALIGNMENT_CENTER
    
    var video_area = ColorRect.new()
    video_area.color = Color.BLACK
    aspect.add_child(video_area)
    
    add_child(aspect)
    return aspect

## SubViewportContainer子视口容器
func create_subviewport_container() -> SubViewportContainer:
    var container = SubViewportContainer.new()
    container.custom_minimum_size = Vector2(400, 300)
    
    container.stretch = true
    
    var viewport = SubViewport.new()
    viewport.size = Vector2i(400, 300)
    viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
    container.add_child(viewport)
    
    # 在视口中添加3D场景或其他内容
    
    add_child(container)
    return container

## ScrollContainer滚动容器
func create_scroll_container() -> ScrollContainer:
    var scroll = ScrollContainer.new()
    scroll.custom_minimum_size = Vector2(300, 200)
    
    scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
    scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
    
    # 跟随焦点
    scroll.follow_focus = true
    
    var content = VBoxContainer.new()
    for i in range(50):
        var label = Label.new()
        label.text = "长列表项目 %d" % (i + 1)
        content.add_child(label)
    
    scroll.add_child(content)
    add_child(scroll)
    return scroll

34.3 响应式设计

34.3.1 屏幕适配

# responsive_design.gd
# 响应式设计

extends Control

## 屏幕断点
enum ScreenSize {
    MOBILE,    # < 600
    TABLET,    # 600 - 1024
    DESKTOP,   # 1024 - 1440
    LARGE      # > 1440
}

var current_screen_size: ScreenSize = ScreenSize.DESKTOP

func _ready():
    get_viewport().size_changed.connect(_on_viewport_resized)
    _on_viewport_resized()

func _on_viewport_resized():
    var width = get_viewport_rect().size.x
    var new_size = _get_screen_size(width)
    
    if new_size != current_screen_size:
        current_screen_size = new_size
        _apply_layout_for_screen_size(current_screen_size)

func _get_screen_size(width: float) -> ScreenSize:
    if width < 600:
        return ScreenSize.MOBILE
    elif width < 1024:
        return ScreenSize.TABLET
    elif width < 1440:
        return ScreenSize.DESKTOP
    else:
        return ScreenSize.LARGE

func _apply_layout_for_screen_size(size: ScreenSize):
    match size:
        ScreenSize.MOBILE:
            _apply_mobile_layout()
        ScreenSize.TABLET:
            _apply_tablet_layout()
        ScreenSize.DESKTOP:
            _apply_desktop_layout()
        ScreenSize.LARGE:
            _apply_large_layout()

func _apply_mobile_layout():
    print("应用移动端布局")
    # 单列布局
    # 隐藏侧边栏
    # 使用底部导航

func _apply_tablet_layout():
    print("应用平板布局")
    # 可折叠侧边栏
    # 两列布局

func _apply_desktop_layout():
    print("应用桌面布局")
    # 完整侧边栏
    # 三列布局

func _apply_large_layout():
    print("应用大屏布局")
    # 最大内容宽度
    # 更多侧边空间

## 响应式网格
func create_responsive_grid() -> GridContainer:
    var grid = GridContainer.new()
    
    var width = get_viewport_rect().size.x
    if width < 600:
        grid.columns = 2
    elif width < 1024:
        grid.columns = 3
    else:
        grid.columns = 4
    
    return grid

34.3.2 安全区域

# safe_area.gd
# 安全区域处理(移动端刘海屏等)

extends Control

@onready var content: Control = $Content

func _ready():
    _apply_safe_area()
    get_viewport().size_changed.connect(_apply_safe_area)

func _apply_safe_area():
    var safe_area = DisplayServer.get_display_safe_area()
    var screen_size = DisplayServer.screen_get_size()
    
    # 计算安全边距
    var margin_left = safe_area.position.x
    var margin_top = safe_area.position.y
    var margin_right = screen_size.x - (safe_area.position.x + safe_area.size.x)
    var margin_bottom = screen_size.y - (safe_area.position.y + safe_area.size.y)
    
    # 应用边距
    if content.get_parent() is MarginContainer:
        var margin_container = content.get_parent() as MarginContainer
        margin_container.add_theme_constant_override("margin_left", int(margin_left))
        margin_container.add_theme_constant_override("margin_top", int(margin_top))
        margin_container.add_theme_constant_override("margin_right", int(margin_right))
        margin_container.add_theme_constant_override("margin_bottom", int(margin_bottom))

## 创建安全区域包装器
func create_safe_area_wrapper() -> MarginContainer:
    var wrapper = MarginContainer.new()
    wrapper.set_anchors_preset(Control.PRESET_FULL_RECT)
    return wrapper

34.3.3 屏幕方向处理

# orientation_handler.gd
# 屏幕方向处理

extends Control

signal orientation_changed(is_landscape: bool)

var is_landscape: bool = true

func _ready():
    get_viewport().size_changed.connect(_check_orientation)
    _check_orientation()

func _check_orientation():
    var size = get_viewport_rect().size
    var new_landscape = size.x > size.y
    
    if new_landscape != is_landscape:
        is_landscape = new_landscape
        orientation_changed.emit(is_landscape)
        _apply_orientation_layout()

func _apply_orientation_layout():
    if is_landscape:
        _apply_landscape_layout()
    else:
        _apply_portrait_layout()

func _apply_landscape_layout():
    print("横屏布局")
    # 侧边导航
    # 水平排列面板

func _apply_portrait_layout():
    print("竖屏布局")
    # 底部导航
    # 垂直排列面板

## 锁定屏幕方向(移动端)
func lock_orientation(landscape: bool):
    if landscape:
        DisplayServer.screen_set_orientation(DisplayServer.SCREEN_LANDSCAPE)
    else:
        DisplayServer.screen_set_orientation(DisplayServer.SCREEN_PORTRAIT)

func unlock_orientation():
    DisplayServer.screen_set_orientation(DisplayServer.SCREEN_SENSOR)

34.4 复杂布局实现

34.4.1 仪表板布局

# dashboard_layout.gd
# 仪表板布局

extends Control

## 仪表板面板配置
var panel_configs: Array[Dictionary] = [
    {"id": "stats", "title": "统计", "size": Vector2(2, 1)},
    {"id": "chart", "title": "图表", "size": Vector2(2, 2)},
    {"id": "list", "title": "列表", "size": Vector2(1, 2)},
    {"id": "info", "title": "信息", "size": Vector2(1, 1)},
]

@onready var grid: GridContainer = $GridContainer

func _ready():
    _create_dashboard()

func _create_dashboard():
    grid.columns = 4
    grid.add_theme_constant_override("h_separation", 10)
    grid.add_theme_constant_override("v_separation", 10)
    
    for config in panel_configs:
        var panel = _create_dashboard_panel(config)
        grid.add_child(panel)

func _create_dashboard_panel(config: Dictionary) -> PanelContainer:
    var panel = PanelContainer.new()
    
    # 根据配置大小设置最小尺寸
    var base_size = Vector2(200, 150)
    panel.custom_minimum_size = base_size * config.size
    
    var style = StyleBoxFlat.new()
    style.bg_color = Color(0.15, 0.15, 0.15)
    style.corner_radius_top_left = 8
    style.corner_radius_top_right = 8
    style.corner_radius_bottom_left = 8
    style.corner_radius_bottom_right = 8
    panel.add_theme_stylebox_override("panel", style)
    
    var vbox = VBoxContainer.new()
    
    # 标题栏
    var title_bar = HBoxContainer.new()
    var title = Label.new()
    title.text = config.title
    title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
    title_bar.add_child(title)
    
    var options_btn = Button.new()
    options_btn.text = "⋮"
    options_btn.flat = true
    title_bar.add_child(options_btn)
    
    vbox.add_child(title_bar)
    vbox.add_child(HSeparator.new())
    
    # 内容区域
    var content = Control.new()
    content.size_flags_vertical = Control.SIZE_EXPAND_FILL
    vbox.add_child(content)
    
    panel.add_child(vbox)
    
    return panel

34.4.2 聊天界面布局

# chat_layout.gd
# 聊天界面布局

extends Control

@onready var messages_scroll: ScrollContainer = $VBox/MessagesScroll
@onready var messages_container: VBoxContainer = $VBox/MessagesScroll/Messages
@onready var input_container: HBoxContainer = $VBox/InputContainer
@onready var message_input: LineEdit = $VBox/InputContainer/MessageInput
@onready var send_button: Button = $VBox/InputContainer/SendButton

func _ready():
    _setup_layout()
    _connect_signals()

func _setup_layout():
    # 主布局
    var vbox = VBoxContainer.new()
    vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
    
    # 消息滚动区域
    messages_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
    messages_scroll.follow_focus = true
    
    # 输入区域
    input_container.custom_minimum_size = Vector2(0, 50)
    input_container.add_theme_constant_override("separation", 10)
    
    message_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
    message_input.placeholder_text = "输入消息..."
    
    send_button.text = "发送"
    send_button.custom_minimum_size = Vector2(80, 0)

func _connect_signals():
    send_button.pressed.connect(_send_message)
    message_input.text_submitted.connect(func(_t): _send_message())

func _send_message():
    var text = message_input.text.strip_edges()
    if text.is_empty():
        return
    
    add_message(text, true)
    message_input.clear()

func add_message(text: String, is_self: bool):
    var message = _create_message_bubble(text, is_self)
    messages_container.add_child(message)
    
    # 滚动到底部
    await get_tree().process_frame
    messages_scroll.scroll_vertical = messages_scroll.get_v_scroll_bar().max_value

func _create_message_bubble(text: String, is_self: bool) -> Control:
    var container = HBoxContainer.new()
    
    if is_self:
        var spacer = Control.new()
        spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
        container.add_child(spacer)
    
    var bubble = PanelContainer.new()
    bubble.custom_minimum_size = Vector2(0, 0)
    bubble.size_flags_horizontal = Control.SIZE_SHRINK_END if is_self else Control.SIZE_SHRINK_BEGIN
    
    var style = StyleBoxFlat.new()
    style.bg_color = Color(0.2, 0.5, 0.8) if is_self else Color(0.3, 0.3, 0.3)
    style.corner_radius_top_left = 10
    style.corner_radius_top_right = 10
    style.corner_radius_bottom_left = 10 if is_self else 0
    style.corner_radius_bottom_right = 0 if is_self else 10
    style.content_margin_left = 10
    style.content_margin_right = 10
    style.content_margin_top = 8
    style.content_margin_bottom = 8
    bubble.add_theme_stylebox_override("panel", style)
    
    var label = Label.new()
    label.text = text
    label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
    label.custom_minimum_size = Vector2(0, 0)
    # 限制最大宽度
    var max_width = get_viewport_rect().size.x * 0.7
    if label.get_theme_font("font").get_string_size(text).x > max_width:
        label.custom_minimum_size.x = max_width
    
    bubble.add_child(label)
    container.add_child(bubble)
    
    if not is_self:
        var spacer = Control.new()
        spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
        container.add_child(spacer)
    
    return container

34.4.3 卡片布局

# card_layout.gd
# 卡片布局系统

extends Control

@onready var cards_container: HFlowContainer = $ScrollContainer/CardsContainer

func _ready():
    _create_cards()
    get_viewport().size_changed.connect(_on_viewport_resized)

func _create_cards():
    var card_data = [
        {"title": "项目A", "description": "这是项目A的描述", "image": "res://card1.png"},
        {"title": "项目B", "description": "这是项目B的描述", "image": "res://card2.png"},
        {"title": "项目C", "description": "这是项目C的描述", "image": "res://card3.png"},
        {"title": "项目D", "description": "这是项目D的描述", "image": "res://card4.png"},
    ]
    
    for data in card_data:
        var card = _create_card(data)
        cards_container.add_child(card)

func _create_card(data: Dictionary) -> PanelContainer:
    var card = PanelContainer.new()
    card.custom_minimum_size = Vector2(280, 200)
    
    var style = StyleBoxFlat.new()
    style.bg_color = Color(0.18, 0.18, 0.18)
    style.corner_radius_top_left = 12
    style.corner_radius_top_right = 12
    style.corner_radius_bottom_left = 12
    style.corner_radius_bottom_right = 12
    style.shadow_color = Color(0, 0, 0, 0.3)
    style.shadow_size = 5
    style.shadow_offset = Vector2(2, 2)
    card.add_theme_stylebox_override("panel", style)
    
    var vbox = VBoxContainer.new()
    vbox.add_theme_constant_override("separation", 0)
    
    # 图片区域
    var image_rect = TextureRect.new()
    image_rect.custom_minimum_size = Vector2(0, 120)
    image_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
    image_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
    if ResourceLoader.exists(data.image):
        image_rect.texture = load(data.image)
    else:
        var placeholder = ColorRect.new()
        placeholder.color = Color(0.3, 0.3, 0.3)
        placeholder.custom_minimum_size = Vector2(0, 120)
        vbox.add_child(placeholder)
    vbox.add_child(image_rect)
    
    # 内容区域
    var content_margin = MarginContainer.new()
    content_margin.add_theme_constant_override("margin_left", 15)
    content_margin.add_theme_constant_override("margin_right", 15)
    content_margin.add_theme_constant_override("margin_top", 10)
    content_margin.add_theme_constant_override("margin_bottom", 15)
    
    var content_vbox = VBoxContainer.new()
    content_vbox.add_theme_constant_override("separation", 5)
    
    var title = Label.new()
    title.text = data.title
    title.add_theme_font_size_override("font_size", 18)
    content_vbox.add_child(title)
    
    var desc = Label.new()
    desc.text = data.description
    desc.autowrap_mode = TextServer.AUTOWRAP_WORD
    desc.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
    content_vbox.add_child(desc)
    
    content_margin.add_child(content_vbox)
    vbox.add_child(content_margin)
    
    card.add_child(vbox)
    
    # 悬停效果
    card.mouse_entered.connect(func(): _on_card_hover(card, true))
    card.mouse_exited.connect(func(): _on_card_hover(card, false))
    
    return card

func _on_card_hover(card: PanelContainer, hover: bool):
    var tween = create_tween()
    var target_scale = Vector2(1.02, 1.02) if hover else Vector2.ONE
    tween.tween_property(card, "scale", target_scale, 0.1)

func _on_viewport_resized():
    # 响应式调整卡片大小
    var width = get_viewport_rect().size.x
    var card_width: float
    
    if width < 600:
        card_width = width - 40  # 全宽
    elif width < 900:
        card_width = (width - 60) / 2  # 两列
    else:
        card_width = (width - 80) / 3  # 三列
    
    for card in cards_container.get_children():
        card.custom_minimum_size.x = card_width

34.5 布局工具类

# layout_utils.gd
# 布局工具类

class_name LayoutUtils
extends RefCounted

## 居中控件
static func center_control(control: Control):
    control.set_anchors_preset(Control.PRESET_CENTER)
    control.grow_horizontal = Control.GROW_DIRECTION_BOTH
    control.grow_vertical = Control.GROW_DIRECTION_BOTH

## 全屏填充
static func fill_parent(control: Control, margin: float = 0):
    control.set_anchors_preset(Control.PRESET_FULL_RECT)
    if margin > 0:
        control.offset_left = margin
        control.offset_top = margin
        control.offset_right = -margin
        control.offset_bottom = -margin

## 创建间隔
static func create_spacer(expand: bool = true) -> Control:
    var spacer = Control.new()
    if expand:
        spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
        spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
    return spacer

## 创建分隔线
static func create_separator(vertical: bool = false) -> Control:
    if vertical:
        return VSeparator.new()
    else:
        return HSeparator.new()

## 创建带标题的面板
static func create_titled_panel(title: String, content: Control) -> PanelContainer:
    var panel = PanelContainer.new()
    
    var vbox = VBoxContainer.new()
    
    var title_label = Label.new()
    title_label.text = title
    title_label.add_theme_font_size_override("font_size", 16)
    vbox.add_child(title_label)
    
    vbox.add_child(HSeparator.new())
    
    content.size_flags_vertical = Control.SIZE_EXPAND_FILL
    vbox.add_child(content)
    
    panel.add_child(vbox)
    return panel

## 创建工具栏
static func create_toolbar(buttons: Array[Dictionary]) -> HBoxContainer:
    var toolbar = HBoxContainer.new()
    toolbar.add_theme_constant_override("separation", 5)
    
    for btn_data in buttons:
        var button = Button.new()
        button.text = btn_data.get("text", "")
        if btn_data.has("icon"):
            button.icon = btn_data.icon
        if btn_data.has("callback"):
            button.pressed.connect(btn_data.callback)
        toolbar.add_child(button)
    
    return toolbar

## 应用统一边距
static func apply_margins(container: MarginContainer, margin: int):
    container.add_theme_constant_override("margin_left", margin)
    container.add_theme_constant_override("margin_right", margin)
    container.add_theme_constant_override("margin_top", margin)
    container.add_theme_constant_override("margin_bottom", margin)

## 计算适合的网格列数
static func calculate_grid_columns(available_width: float, item_width: float, separation: float = 0) -> int:
    var columns = int((available_width + separation) / (item_width + separation))
    return max(1, columns)

34.6 本章小结

本章深入讲解了Godot 4的UI布局管理:

  1. 布局基础:锚点系统、增长方向、大小管理
  2. 容器布局:BoxContainer、GridContainer、FlowContainer等
  3. 响应式设计:屏幕适配、安全区域、方向处理
  4. 复杂布局:仪表板、聊天界面、卡片布局
  5. 布局工具类:常用布局辅助函数

掌握布局管理是创建专业UI的关键,良好的布局能够适应不同的屏幕尺寸和设备。下一章我们将学习主题与样式系统,了解如何统一管理UI外观。

← 返回目录