第三十四章: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布局管理:
- 布局基础:锚点系统、增长方向、大小管理
- 容器布局:BoxContainer、GridContainer、FlowContainer等
- 响应式设计:屏幕适配、安全区域、方向处理
- 复杂布局:仪表板、聊天界面、卡片布局
- 布局工具类:常用布局辅助函数
掌握布局管理是创建专业UI的关键,良好的布局能够适应不同的屏幕尺寸和设备。下一章我们将学习主题与样式系统,了解如何统一管理UI外观。