第三十五章:主题与样式

第三十五章:主题与样式

Godot 4的主题系统允许开发者统一管理UI控件的外观,包括颜色、字体、图标和样式盒等。本章将详细介绍如何创建和使用主题,实现一致的视觉风格。

35.1 主题系统概述

35.1.1 主题基础

# theme_basics.gd
# 主题系统基础

extends Control

## 主题组成部分
## Theme包含以下类型的资源:
## - 颜色(Color)
## - 常量(int)
## - 字体(Font)
## - 字体大小(int)
## - 图标(Texture2D)
## - 样式盒(StyleBox)

func _ready():
    _explore_theme_system()

func _explore_theme_system():
    # 获取当前主题
    var current_theme = theme
    if current_theme == null:
        current_theme = get_theme()
    
    print("=== 主题信息 ===")
    
    # 获取主题中的类型列表
    var types = current_theme.get_type_list() if current_theme else []
    print("控件类型数量: ", types.size())

## 主题继承
## 控件的主题查找顺序:
## 1. 控件自身的theme_override_*
## 2. 控件的theme属性
## 3. 父节点的theme
## 4. 项目默认主题

func demonstrate_theme_inheritance():
    var button = Button.new()
    button.text = "测试按钮"
    
    # 1. 直接覆盖(最高优先级)
    button.add_theme_color_override("font_color", Color.RED)
    
    # 2. 设置控件主题
    var custom_theme = Theme.new()
    custom_theme.set_color("font_color", "Button", Color.BLUE)
    button.theme = custom_theme
    
    # 注意:override优先于theme
    add_child(button)

## 获取主题值
func get_theme_values(control: Control):
    # 获取颜色
    var font_color = control.get_theme_color("font_color", "Button")
    
    # 获取常量
    var margin = control.get_theme_constant("margin", "Panel")
    
    # 获取字体
    var font = control.get_theme_font("font", "Label")
    
    # 获取字体大小
    var font_size = control.get_theme_font_size("font_size", "Label")
    
    # 获取图标
    var icon = control.get_theme_icon("icon", "Button")
    
    # 获取样式盒
    var stylebox = control.get_theme_stylebox("panel", "Panel")
    
    print("字体颜色: ", font_color)
    print("边距: ", margin)

35.1.2 创建主题资源

# create_theme.gd
# 创建主题资源

extends Node

func create_custom_theme() -> Theme:
    var theme = Theme.new()
    
    # 设置默认字体
    var default_font = load("res://fonts/main_font.ttf")
    theme.default_font = default_font
    theme.default_font_size = 16
    
    # 设置Button样式
    _setup_button_theme(theme)
    
    # 设置Label样式
    _setup_label_theme(theme)
    
    # 设置Panel样式
    _setup_panel_theme(theme)
    
    # 设置LineEdit样式
    _setup_line_edit_theme(theme)
    
    return theme

func _setup_button_theme(theme: Theme):
    # 颜色
    theme.set_color("font_color", "Button", Color(0.9, 0.9, 0.9))
    theme.set_color("font_hover_color", "Button", Color.WHITE)
    theme.set_color("font_pressed_color", "Button", Color(0.8, 0.8, 0.8))
    theme.set_color("font_disabled_color", "Button", Color(0.5, 0.5, 0.5))
    theme.set_color("font_focus_color", "Button", Color.WHITE)
    
    # 样式盒
    var normal = _create_button_stylebox(Color(0.2, 0.2, 0.2))
    var hover = _create_button_stylebox(Color(0.3, 0.3, 0.3))
    var pressed = _create_button_stylebox(Color(0.15, 0.15, 0.15))
    var disabled = _create_button_stylebox(Color(0.1, 0.1, 0.1))
    var focus = _create_button_stylebox(Color(0.2, 0.2, 0.2))
    focus.border_color = Color(0.4, 0.6, 1.0)
    focus.border_width_left = 2
    focus.border_width_right = 2
    focus.border_width_top = 2
    focus.border_width_bottom = 2
    
    theme.set_stylebox("normal", "Button", normal)
    theme.set_stylebox("hover", "Button", hover)
    theme.set_stylebox("pressed", "Button", pressed)
    theme.set_stylebox("disabled", "Button", disabled)
    theme.set_stylebox("focus", "Button", focus)
    
    # 常量
    theme.set_constant("h_separation", "Button", 5)

func _create_button_stylebox(bg_color: Color) -> StyleBoxFlat:
    var style = StyleBoxFlat.new()
    style.bg_color = bg_color
    style.corner_radius_top_left = 6
    style.corner_radius_top_right = 6
    style.corner_radius_bottom_left = 6
    style.corner_radius_bottom_right = 6
    style.content_margin_left = 15
    style.content_margin_right = 15
    style.content_margin_top = 8
    style.content_margin_bottom = 8
    return style

func _setup_label_theme(theme: Theme):
    theme.set_color("font_color", "Label", Color(0.85, 0.85, 0.85))
    theme.set_color("font_shadow_color", "Label", Color(0, 0, 0, 0))
    
    theme.set_constant("shadow_offset_x", "Label", 1)
    theme.set_constant("shadow_offset_y", "Label", 1)
    theme.set_constant("line_spacing", "Label", 3)

func _setup_panel_theme(theme: Theme):
    var panel_style = StyleBoxFlat.new()
    panel_style.bg_color = Color(0.15, 0.15, 0.15)
    panel_style.corner_radius_top_left = 8
    panel_style.corner_radius_top_right = 8
    panel_style.corner_radius_bottom_left = 8
    panel_style.corner_radius_bottom_right = 8
    
    theme.set_stylebox("panel", "Panel", panel_style)
    theme.set_stylebox("panel", "PanelContainer", panel_style)

func _setup_line_edit_theme(theme: Theme):
    theme.set_color("font_color", "LineEdit", Color(0.9, 0.9, 0.9))
    theme.set_color("font_placeholder_color", "LineEdit", Color(0.5, 0.5, 0.5))
    theme.set_color("caret_color", "LineEdit", Color.WHITE)
    theme.set_color("selection_color", "LineEdit", Color(0.3, 0.5, 0.8, 0.5))
    
    var normal = StyleBoxFlat.new()
    normal.bg_color = Color(0.1, 0.1, 0.1)
    normal.border_color = Color(0.3, 0.3, 0.3)
    normal.border_width_left = 1
    normal.border_width_right = 1
    normal.border_width_top = 1
    normal.border_width_bottom = 1
    normal.corner_radius_top_left = 4
    normal.corner_radius_top_right = 4
    normal.corner_radius_bottom_left = 4
    normal.corner_radius_bottom_right = 4
    normal.content_margin_left = 8
    normal.content_margin_right = 8
    normal.content_margin_top = 6
    normal.content_margin_bottom = 6
    
    var focus = normal.duplicate()
    focus.border_color = Color(0.4, 0.6, 1.0)
    
    theme.set_stylebox("normal", "LineEdit", normal)
    theme.set_stylebox("focus", "LineEdit", focus)

## 保存主题到文件
func save_theme(theme: Theme, path: String):
    ResourceSaver.save(theme, path)

## 加载主题
func load_theme(path: String) -> Theme:
    if ResourceLoader.exists(path):
        return load(path) as Theme
    return null

35.2 样式盒详解

35.2.1 StyleBoxFlat

# stylebox_flat.gd
# StyleBoxFlat详解

extends Control

func _ready():
    _demonstrate_stylebox_flat()

func _demonstrate_stylebox_flat():
    # StyleBoxFlat是最常用的样式盒
    var style = StyleBoxFlat.new()
    
    # === 背景 ===
    style.bg_color = Color(0.2, 0.4, 0.6)
    
    # === 边框 ===
    style.border_color = Color(0.4, 0.6, 0.8)
    style.border_width_left = 2
    style.border_width_right = 2
    style.border_width_top = 2
    style.border_width_bottom = 2
    style.border_blend = false  # 边框颜色是否与背景混合
    
    # === 圆角 ===
    style.corner_radius_top_left = 10
    style.corner_radius_top_right = 10
    style.corner_radius_bottom_left = 10
    style.corner_radius_bottom_right = 10
    style.corner_detail = 8  # 圆角的平滑度
    
    # === 内容边距 ===
    style.content_margin_left = 10
    style.content_margin_right = 10
    style.content_margin_top = 8
    style.content_margin_bottom = 8
    
    # === 阴影 ===
    style.shadow_color = Color(0, 0, 0, 0.3)
    style.shadow_size = 5
    style.shadow_offset = Vector2(2, 2)
    
    # === 抗锯齿 ===
    style.anti_aliasing = true
    style.anti_aliasing_size = 1.0
    
    # === 扩展边距 ===
    style.expand_margin_left = 0
    style.expand_margin_right = 0
    style.expand_margin_top = 0
    style.expand_margin_bottom = 0
    
    # === 绘制中心 ===
    style.draw_center = true  # 是否绘制中心区域
    
    # 应用到Panel
    var panel = Panel.new()
    panel.add_theme_stylebox_override("panel", style)
    panel.custom_minimum_size = Vector2(200, 100)
    add_child(panel)

## 创建渐变背景
func create_gradient_style() -> StyleBoxFlat:
    # StyleBoxFlat不直接支持渐变
    # 但可以通过着色器实现
    var style = StyleBoxFlat.new()
    style.bg_color = Color(0.2, 0.3, 0.5)
    return style

## 创建玻璃效果
func create_glass_style() -> StyleBoxFlat:
    var style = StyleBoxFlat.new()
    style.bg_color = Color(1, 1, 1, 0.1)
    style.border_color = Color(1, 1, 1, 0.3)
    style.border_width_left = 1
    style.border_width_right = 1
    style.border_width_top = 1
    style.border_width_bottom = 1
    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.2)
    style.shadow_size = 10
    return style

## 创建凹陷效果
func create_inset_style() -> StyleBoxFlat:
    var style = StyleBoxFlat.new()
    style.bg_color = Color(0.1, 0.1, 0.1)
    style.border_color = Color(0.05, 0.05, 0.05)
    style.border_width_top = 2
    style.border_width_left = 2
    style.corner_radius_top_left = 4
    style.corner_radius_top_right = 4
    style.corner_radius_bottom_left = 4
    style.corner_radius_bottom_right = 4
    return style

35.2.2 StyleBoxTexture

# stylebox_texture.gd
# StyleBoxTexture纹理样式盒

extends Control

func create_texture_stylebox() -> StyleBoxTexture:
    var style = StyleBoxTexture.new()
    
    # 设置纹理
    style.texture = preload("res://ui/panel_bg.png")
    
    # 九宫格边距(不拉伸的区域)
    style.texture_margin_left = 20
    style.texture_margin_right = 20
    style.texture_margin_top = 20
    style.texture_margin_bottom = 20
    
    # 拉伸模式
    style.axis_stretch_horizontal = StyleBoxTexture.AXIS_STRETCH_MODE_STRETCH
    style.axis_stretch_vertical = StyleBoxTexture.AXIS_STRETCH_MODE_STRETCH
    # 可选模式:
    # AXIS_STRETCH_MODE_STRETCH: 拉伸
    # AXIS_STRETCH_MODE_TILE: 平铺
    # AXIS_STRETCH_MODE_TILE_FIT: 适应平铺
    
    # 绘制中心
    style.draw_center = true
    
    # 调制颜色
    style.modulate_color = Color.WHITE
    
    # 区域选择(纹理图集)
    style.region_rect = Rect2(0, 0, 64, 64)
    
    # 内容边距
    style.content_margin_left = 15
    style.content_margin_right = 15
    style.content_margin_top = 10
    style.content_margin_bottom = 10
    
    return style

## 从图集创建多个样式
func create_styles_from_atlas(atlas: Texture2D, regions: Dictionary) -> Dictionary:
    var styles = {}
    
    for name in regions:
        var style = StyleBoxTexture.new()
        style.texture = atlas
        style.region_rect = regions[name]
        # 设置九宫格边距...
        styles[name] = style
    
    return styles

35.2.3 其他样式盒类型

# other_styleboxes.gd
# 其他样式盒类型

extends Control

## StyleBoxEmpty - 空样式盒
func create_empty_stylebox() -> StyleBoxEmpty:
    var style = StyleBoxEmpty.new()
    # 仅设置内容边距
    style.content_margin_left = 10
    style.content_margin_right = 10
    style.content_margin_top = 5
    style.content_margin_bottom = 5
    return style

## StyleBoxLine - 线条样式盒
func create_line_stylebox() -> StyleBoxLine:
    var style = StyleBoxLine.new()
    style.color = Color(0.5, 0.5, 0.5)
    style.thickness = 1
    style.grow_begin = 0
    style.grow_end = 0
    style.vertical = false  # 水平线
    return style

## 综合示例:创建按钮样式集
func create_button_style_set() -> Dictionary:
    return {
        "normal": _create_button_normal(),
        "hover": _create_button_hover(),
        "pressed": _create_button_pressed(),
        "disabled": _create_button_disabled(),
        "focus": _create_button_focus()
    }

func _create_button_normal() -> StyleBoxFlat:
    var style = StyleBoxFlat.new()
    style.bg_color = Color(0.25, 0.25, 0.25)
    style.corner_radius_top_left = 5
    style.corner_radius_top_right = 5
    style.corner_radius_bottom_left = 5
    style.corner_radius_bottom_right = 5
    style.content_margin_left = 12
    style.content_margin_right = 12
    style.content_margin_top = 6
    style.content_margin_bottom = 6
    return style

func _create_button_hover() -> StyleBoxFlat:
    var style = _create_button_normal()
    style.bg_color = Color(0.35, 0.35, 0.35)
    return style

func _create_button_pressed() -> StyleBoxFlat:
    var style = _create_button_normal()
    style.bg_color = Color(0.2, 0.2, 0.2)
    return style

func _create_button_disabled() -> StyleBoxFlat:
    var style = _create_button_normal()
    style.bg_color = Color(0.15, 0.15, 0.15)
    return style

func _create_button_focus() -> StyleBoxFlat:
    var style = _create_button_normal()
    style.border_color = Color(0.4, 0.6, 1.0)
    style.border_width_left = 2
    style.border_width_right = 2
    style.border_width_top = 2
    style.border_width_bottom = 2
    return style

35.3 主题变体与类型

35.3.1 自定义控件类型

# custom_control_type.gd
# 自定义控件类型

extends Control

## 创建自定义控件的主题类型
func setup_custom_type(theme: Theme):
    # 定义新的类型 "PrimaryButton"
    _setup_primary_button(theme)
    
    # 定义新的类型 "SecondaryButton"
    _setup_secondary_button(theme)
    
    # 定义新的类型 "DangerButton"
    _setup_danger_button(theme)

func _setup_primary_button(theme: Theme):
    var type_name = "PrimaryButton"
    
    # 颜色
    theme.set_color("font_color", type_name, Color.WHITE)
    theme.set_color("font_hover_color", type_name, Color.WHITE)
    theme.set_color("font_pressed_color", type_name, Color(0.9, 0.9, 0.9))
    
    # 样式盒
    var normal = StyleBoxFlat.new()
    normal.bg_color = Color(0.2, 0.5, 0.8)
    normal.corner_radius_top_left = 6
    normal.corner_radius_top_right = 6
    normal.corner_radius_bottom_left = 6
    normal.corner_radius_bottom_right = 6
    normal.content_margin_left = 20
    normal.content_margin_right = 20
    normal.content_margin_top = 10
    normal.content_margin_bottom = 10
    
    var hover = normal.duplicate()
    hover.bg_color = Color(0.3, 0.6, 0.9)
    
    var pressed = normal.duplicate()
    pressed.bg_color = Color(0.15, 0.4, 0.7)
    
    theme.set_stylebox("normal", type_name, normal)
    theme.set_stylebox("hover", type_name, hover)
    theme.set_stylebox("pressed", type_name, pressed)

func _setup_secondary_button(theme: Theme):
    var type_name = "SecondaryButton"
    
    theme.set_color("font_color", type_name, Color(0.9, 0.9, 0.9))
    
    var normal = StyleBoxFlat.new()
    normal.bg_color = Color(0.3, 0.3, 0.3)
    normal.border_color = Color(0.5, 0.5, 0.5)
    normal.border_width_left = 1
    normal.border_width_right = 1
    normal.border_width_top = 1
    normal.border_width_bottom = 1
    normal.corner_radius_top_left = 6
    normal.corner_radius_top_right = 6
    normal.corner_radius_bottom_left = 6
    normal.corner_radius_bottom_right = 6
    normal.content_margin_left = 20
    normal.content_margin_right = 20
    normal.content_margin_top = 10
    normal.content_margin_bottom = 10
    
    theme.set_stylebox("normal", type_name, normal)

func _setup_danger_button(theme: Theme):
    var type_name = "DangerButton"
    
    theme.set_color("font_color", type_name, Color.WHITE)
    
    var normal = StyleBoxFlat.new()
    normal.bg_color = Color(0.8, 0.2, 0.2)
    normal.corner_radius_top_left = 6
    normal.corner_radius_top_right = 6
    normal.corner_radius_bottom_left = 6
    normal.corner_radius_bottom_right = 6
    normal.content_margin_left = 20
    normal.content_margin_right = 20
    normal.content_margin_top = 10
    normal.content_margin_bottom = 10
    
    var hover = normal.duplicate()
    hover.bg_color = Color(0.9, 0.3, 0.3)
    
    theme.set_stylebox("normal", type_name, normal)
    theme.set_stylebox("hover", type_name, hover)

## 应用自定义类型
func apply_custom_type(button: Button, type_name: String):
    button.theme_type_variation = type_name

35.3.2 主题变体

# theme_variations.gd
# 主题变体

extends Control

## 设置主题变体
## theme_type_variation允许控件使用不同的主题类型

func setup_button_variations():
    var primary_btn = Button.new()
    primary_btn.text = "主要按钮"
    primary_btn.theme_type_variation = "PrimaryButton"
    add_child(primary_btn)
    
    var secondary_btn = Button.new()
    secondary_btn.text = "次要按钮"
    secondary_btn.theme_type_variation = "SecondaryButton"
    add_child(secondary_btn)
    
    var danger_btn = Button.new()
    danger_btn.text = "危险按钮"
    danger_btn.theme_type_variation = "DangerButton"
    add_child(danger_btn)

## 动态切换变体
func switch_button_variation(button: Button, variation: String):
    button.theme_type_variation = variation

## 创建变体切换器
func create_variation_demo() -> VBoxContainer:
    var vbox = VBoxContainer.new()
    
    var button = Button.new()
    button.text = "可变按钮"
    vbox.add_child(button)
    
    var options = OptionButton.new()
    options.add_item("默认", 0)
    options.add_item("Primary", 1)
    options.add_item("Secondary", 2)
    options.add_item("Danger", 3)
    
    options.item_selected.connect(func(idx):
        match idx:
            0: button.theme_type_variation = ""
            1: button.theme_type_variation = "PrimaryButton"
            2: button.theme_type_variation = "SecondaryButton"
            3: button.theme_type_variation = "DangerButton"
    )
    
    vbox.add_child(options)
    return vbox

35.4 主题管理系统

35.4.1 主题管理器

# theme_manager.gd
# 主题管理器单例

extends Node

## 可用主题
enum ThemeType {
    LIGHT,
    DARK,
    HIGH_CONTRAST
}

var current_theme_type: ThemeType = ThemeType.DARK
var themes: Dictionary = {}
var root_control: Control

signal theme_changed(theme_type: ThemeType)

func _ready():
    _initialize_themes()

func _initialize_themes():
    themes[ThemeType.DARK] = _create_dark_theme()
    themes[ThemeType.LIGHT] = _create_light_theme()
    themes[ThemeType.HIGH_CONTRAST] = _create_high_contrast_theme()

func set_root_control(control: Control):
    root_control = control
    apply_theme(current_theme_type)

func apply_theme(theme_type: ThemeType):
    if not themes.has(theme_type):
        return
    
    current_theme_type = theme_type
    
    if root_control:
        root_control.theme = themes[theme_type]
    
    theme_changed.emit(theme_type)

func get_current_theme() -> Theme:
    return themes.get(current_theme_type)

func toggle_theme():
    var next_type: ThemeType
    match current_theme_type:
        ThemeType.DARK: next_type = ThemeType.LIGHT
        ThemeType.LIGHT: next_type = ThemeType.HIGH_CONTRAST
        ThemeType.HIGH_CONTRAST: next_type = ThemeType.DARK
    
    apply_theme(next_type)

## 创建深色主题
func _create_dark_theme() -> Theme:
    var theme = Theme.new()
    
    # 背景色
    var bg_primary = Color(0.12, 0.12, 0.14)
    var bg_secondary = Color(0.18, 0.18, 0.2)
    var bg_tertiary = Color(0.24, 0.24, 0.26)
    
    # 前景色
    var fg_primary = Color(0.95, 0.95, 0.95)
    var fg_secondary = Color(0.7, 0.7, 0.7)
    
    # 强调色
    var accent = Color(0.3, 0.5, 0.9)
    
    _apply_colors_to_theme(theme, bg_primary, bg_secondary, bg_tertiary, fg_primary, fg_secondary, accent)
    
    return theme

## 创建浅色主题
func _create_light_theme() -> Theme:
    var theme = Theme.new()
    
    var bg_primary = Color(0.95, 0.95, 0.97)
    var bg_secondary = Color(0.9, 0.9, 0.92)
    var bg_tertiary = Color(0.85, 0.85, 0.87)
    
    var fg_primary = Color(0.1, 0.1, 0.1)
    var fg_secondary = Color(0.4, 0.4, 0.4)
    
    var accent = Color(0.2, 0.4, 0.8)
    
    _apply_colors_to_theme(theme, bg_primary, bg_secondary, bg_tertiary, fg_primary, fg_secondary, accent)
    
    return theme

## 创建高对比度主题
func _create_high_contrast_theme() -> Theme:
    var theme = Theme.new()
    
    var bg_primary = Color.BLACK
    var bg_secondary = Color(0.1, 0.1, 0.1)
    var bg_tertiary = Color(0.2, 0.2, 0.2)
    
    var fg_primary = Color.WHITE
    var fg_secondary = Color(0.9, 0.9, 0.9)
    
    var accent = Color.YELLOW
    
    _apply_colors_to_theme(theme, bg_primary, bg_secondary, bg_tertiary, fg_primary, fg_secondary, accent)
    
    return theme

func _apply_colors_to_theme(theme: Theme, bg1: Color, bg2: Color, bg3: Color, fg1: Color, fg2: Color, accent: Color):
    # Panel
    var panel_style = StyleBoxFlat.new()
    panel_style.bg_color = bg1
    panel_style.corner_radius_top_left = 8
    panel_style.corner_radius_top_right = 8
    panel_style.corner_radius_bottom_left = 8
    panel_style.corner_radius_bottom_right = 8
    theme.set_stylebox("panel", "Panel", panel_style)
    theme.set_stylebox("panel", "PanelContainer", panel_style)
    
    # Button
    var btn_normal = StyleBoxFlat.new()
    btn_normal.bg_color = bg3
    btn_normal.corner_radius_top_left = 6
    btn_normal.corner_radius_top_right = 6
    btn_normal.corner_radius_bottom_left = 6
    btn_normal.corner_radius_bottom_right = 6
    btn_normal.content_margin_left = 15
    btn_normal.content_margin_right = 15
    btn_normal.content_margin_top = 8
    btn_normal.content_margin_bottom = 8
    
    var btn_hover = btn_normal.duplicate()
    btn_hover.bg_color = bg3.lightened(0.1)
    
    var btn_pressed = btn_normal.duplicate()
    btn_pressed.bg_color = bg3.darkened(0.1)
    
    theme.set_stylebox("normal", "Button", btn_normal)
    theme.set_stylebox("hover", "Button", btn_hover)
    theme.set_stylebox("pressed", "Button", btn_pressed)
    theme.set_color("font_color", "Button", fg1)
    theme.set_color("font_hover_color", "Button", fg1)
    theme.set_color("font_pressed_color", "Button", fg2)
    
    # Label
    theme.set_color("font_color", "Label", fg1)
    
    # LineEdit
    var edit_style = StyleBoxFlat.new()
    edit_style.bg_color = bg2
    edit_style.border_color = bg3
    edit_style.border_width_bottom = 2
    edit_style.corner_radius_top_left = 4
    edit_style.corner_radius_top_right = 4
    edit_style.content_margin_left = 10
    edit_style.content_margin_right = 10
    edit_style.content_margin_top = 8
    edit_style.content_margin_bottom = 8
    
    var edit_focus = edit_style.duplicate()
    edit_focus.border_color = accent
    
    theme.set_stylebox("normal", "LineEdit", edit_style)
    theme.set_stylebox("focus", "LineEdit", edit_focus)
    theme.set_color("font_color", "LineEdit", fg1)
    theme.set_color("font_placeholder_color", "LineEdit", fg2)
    theme.set_color("caret_color", "LineEdit", fg1)

## 保存主题偏好
func save_preference():
    var config = ConfigFile.new()
    config.set_value("theme", "type", current_theme_type)
    config.save("user://theme_preference.cfg")

## 加载主题偏好
func load_preference():
    var config = ConfigFile.new()
    if config.load("user://theme_preference.cfg") == OK:
        var saved_type = config.get_value("theme", "type", ThemeType.DARK)
        apply_theme(saved_type)

35.4.2 主题切换动画

# theme_transition.gd
# 主题切换动画

extends CanvasLayer

@onready var overlay: ColorRect = $Overlay
var tween: Tween

func _ready():
    overlay.color = Color(0, 0, 0, 0)
    overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE

## 带动画的主题切换
func switch_theme_animated(new_theme_type: int, duration: float = 0.3):
    overlay.mouse_filter = Control.MOUSE_FILTER_STOP
    
    if tween:
        tween.kill()
    tween = create_tween()
    
    # 淡入
    tween.tween_property(overlay, "color:a", 1.0, duration / 2)
    
    # 切换主题
    tween.tween_callback(func():
        ThemeManager.apply_theme(new_theme_type)
    )
    
    # 淡出
    tween.tween_property(overlay, "color:a", 0.0, duration / 2)
    
    # 恢复交互
    tween.tween_callback(func():
        overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
    )

## 滑动切换效果
func switch_theme_slide(new_theme_type: int, direction: Vector2 = Vector2.RIGHT):
    var screenshot = _capture_current_ui()
    
    # 显示截图
    var sprite = TextureRect.new()
    sprite.texture = screenshot
    sprite.set_anchors_preset(Control.PRESET_FULL_RECT)
    add_child(sprite)
    
    # 切换主题
    ThemeManager.apply_theme(new_theme_type)
    
    # 滑出动画
    var tween = create_tween()
    tween.tween_property(sprite, "position", direction * get_viewport().size, 0.4)
    tween.set_trans(Tween.TRANS_QUAD)
    tween.set_ease(Tween.EASE_IN)
    tween.tween_callback(sprite.queue_free)

func _capture_current_ui() -> ImageTexture:
    var image = get_viewport().get_texture().get_image()
    return ImageTexture.create_from_image(image)

35.5 字体管理

35.5.1 字体配置

# font_manager.gd
# 字体管理

extends Node

var fonts: Dictionary = {}

func _ready():
    _load_fonts()

func _load_fonts():
    # 加载主字体
    var main_font = load("res://fonts/NotoSansSC-Regular.ttf")
    fonts["main"] = main_font
    
    # 加载标题字体
    var title_font = load("res://fonts/NotoSansSC-Bold.ttf")
    fonts["title"] = title_font
    
    # 加载代码字体
    var code_font = load("res://fonts/JetBrainsMono-Regular.ttf")
    fonts["code"] = code_font

func get_font(name: String) -> Font:
    return fonts.get(name)

## 创建带有字体变体的FontVariation
func create_font_variation(base_font: Font, bold: bool = false, italic: bool = false) -> Font:
    var variation = FontVariation.new()
    variation.base_font = base_font
    
    # 设置OpenType特性
    if bold:
        variation.variation_opentype = {"wght": 700}
    if italic:
        variation.variation_opentype["slnt"] = -12
    
    return variation

## 设置主题字体
func apply_fonts_to_theme(theme: Theme):
    theme.default_font = fonts["main"]
    theme.default_font_size = 16
    
    # 标题字体
    theme.set_font("font", "HeaderLabel", fonts["title"])
    theme.set_font_size("font_size", "HeaderLabel", 24)
    
    # 代码字体
    theme.set_font("font", "CodeEdit", fonts["code"])
    theme.set_font_size("font_size", "CodeEdit", 14)

35.5.2 动态字体大小

# dynamic_font_size.gd
# 动态字体大小

extends Control

@export_range(0.5, 2.0) var font_scale: float = 1.0:
    set(value):
        font_scale = value
        _apply_font_scale()

var base_font_sizes: Dictionary = {}

func _ready():
    _store_base_sizes()

func _store_base_sizes():
    base_font_sizes = {
        "small": 12,
        "normal": 16,
        "large": 20,
        "header": 24,
        "title": 32
    }

func _apply_font_scale():
    if not theme:
        return
    
    for size_name in base_font_sizes:
        var base_size = base_font_sizes[size_name]
        var scaled_size = int(base_size * font_scale)
        
        # 应用到主题
        match size_name:
            "normal":
                theme.default_font_size = scaled_size
                theme.set_font_size("font_size", "Label", scaled_size)
                theme.set_font_size("font_size", "Button", scaled_size)
            "small":
                theme.set_font_size("font_size", "SmallLabel", scaled_size)
            "large":
                theme.set_font_size("font_size", "LargeLabel", scaled_size)
            "header":
                theme.set_font_size("font_size", "HeaderLabel", scaled_size)
            "title":
                theme.set_font_size("font_size", "TitleLabel", scaled_size)

## 根据屏幕大小自动调整
func auto_adjust_font_scale():
    var viewport_size = get_viewport_rect().size
    var base_width = 1920.0
    
    font_scale = clamp(viewport_size.x / base_width, 0.7, 1.3)

35.6 实际案例:完整UI主题

# complete_theme.gd
# 完整UI主题示例

extends Node

func create_game_theme() -> Theme:
    var theme = Theme.new()
    
    # 字体
    _setup_fonts(theme)
    
    # 颜色方案
    var colors = _define_color_scheme()
    
    # 应用到所有控件
    _setup_common_controls(theme, colors)
    _setup_button_styles(theme, colors)
    _setup_input_styles(theme, colors)
    _setup_container_styles(theme, colors)
    _setup_popup_styles(theme, colors)
    _setup_progress_styles(theme, colors)
    
    return theme

func _define_color_scheme() -> Dictionary:
    return {
        "background": Color(0.1, 0.1, 0.12),
        "surface": Color(0.15, 0.15, 0.18),
        "surface_light": Color(0.2, 0.2, 0.24),
        "primary": Color(0.3, 0.5, 0.9),
        "primary_dark": Color(0.2, 0.4, 0.8),
        "secondary": Color(0.4, 0.4, 0.45),
        "success": Color(0.2, 0.7, 0.3),
        "warning": Color(0.9, 0.7, 0.2),
        "error": Color(0.8, 0.2, 0.2),
        "text_primary": Color(0.95, 0.95, 0.95),
        "text_secondary": Color(0.7, 0.7, 0.7),
        "text_disabled": Color(0.4, 0.4, 0.4),
        "border": Color(0.3, 0.3, 0.35),
        "shadow": Color(0, 0, 0, 0.3),
    }

func _setup_fonts(theme: Theme):
    var main_font = load("res://fonts/main.ttf")
    theme.default_font = main_font
    theme.default_font_size = 16

func _setup_common_controls(theme: Theme, colors: Dictionary):
    # Label
    theme.set_color("font_color", "Label", colors.text_primary)
    theme.set_color("font_shadow_color", "Label", Color.TRANSPARENT)
    
    # RichTextLabel
    theme.set_color("default_color", "RichTextLabel", colors.text_primary)

func _setup_button_styles(theme: Theme, colors: Dictionary):
    var base_style = StyleBoxFlat.new()
    base_style.corner_radius_top_left = 6
    base_style.corner_radius_top_right = 6
    base_style.corner_radius_bottom_left = 6
    base_style.corner_radius_bottom_right = 6
    base_style.content_margin_left = 16
    base_style.content_margin_right = 16
    base_style.content_margin_top = 10
    base_style.content_margin_bottom = 10
    
    # Normal Button
    var btn_normal = base_style.duplicate()
    btn_normal.bg_color = colors.surface_light
    
    var btn_hover = base_style.duplicate()
    btn_hover.bg_color = colors.surface_light.lightened(0.1)
    
    var btn_pressed = base_style.duplicate()
    btn_pressed.bg_color = colors.surface_light.darkened(0.1)
    
    var btn_disabled = base_style.duplicate()
    btn_disabled.bg_color = colors.surface
    
    var btn_focus = base_style.duplicate()
    btn_focus.bg_color = colors.surface_light
    btn_focus.border_color = colors.primary
    btn_focus.border_width_left = 2
    btn_focus.border_width_right = 2
    btn_focus.border_width_top = 2
    btn_focus.border_width_bottom = 2
    
    theme.set_stylebox("normal", "Button", btn_normal)
    theme.set_stylebox("hover", "Button", btn_hover)
    theme.set_stylebox("pressed", "Button", btn_pressed)
    theme.set_stylebox("disabled", "Button", btn_disabled)
    theme.set_stylebox("focus", "Button", btn_focus)
    
    theme.set_color("font_color", "Button", colors.text_primary)
    theme.set_color("font_hover_color", "Button", colors.text_primary)
    theme.set_color("font_pressed_color", "Button", colors.text_secondary)
    theme.set_color("font_disabled_color", "Button", colors.text_disabled)
    
    # Primary Button
    var primary_normal = base_style.duplicate()
    primary_normal.bg_color = colors.primary
    
    var primary_hover = base_style.duplicate()
    primary_hover.bg_color = colors.primary.lightened(0.1)
    
    var primary_pressed = base_style.duplicate()
    primary_pressed.bg_color = colors.primary_dark
    
    theme.set_stylebox("normal", "PrimaryButton", primary_normal)
    theme.set_stylebox("hover", "PrimaryButton", primary_hover)
    theme.set_stylebox("pressed", "PrimaryButton", primary_pressed)
    theme.set_color("font_color", "PrimaryButton", Color.WHITE)

func _setup_input_styles(theme: Theme, colors: Dictionary):
    # LineEdit
    var edit_normal = StyleBoxFlat.new()
    edit_normal.bg_color = colors.surface
    edit_normal.border_color = colors.border
    edit_normal.border_width_left = 1
    edit_normal.border_width_right = 1
    edit_normal.border_width_top = 1
    edit_normal.border_width_bottom = 1
    edit_normal.corner_radius_top_left = 4
    edit_normal.corner_radius_top_right = 4
    edit_normal.corner_radius_bottom_left = 4
    edit_normal.corner_radius_bottom_right = 4
    edit_normal.content_margin_left = 10
    edit_normal.content_margin_right = 10
    edit_normal.content_margin_top = 8
    edit_normal.content_margin_bottom = 8
    
    var edit_focus = edit_normal.duplicate()
    edit_focus.border_color = colors.primary
    edit_focus.border_width_left = 2
    edit_focus.border_width_right = 2
    edit_focus.border_width_top = 2
    edit_focus.border_width_bottom = 2
    
    theme.set_stylebox("normal", "LineEdit", edit_normal)
    theme.set_stylebox("focus", "LineEdit", edit_focus)
    theme.set_color("font_color", "LineEdit", colors.text_primary)
    theme.set_color("font_placeholder_color", "LineEdit", colors.text_secondary)
    theme.set_color("caret_color", "LineEdit", colors.text_primary)
    theme.set_color("selection_color", "LineEdit", Color(colors.primary.r, colors.primary.g, colors.primary.b, 0.3))

func _setup_container_styles(theme: Theme, colors: Dictionary):
    # Panel
    var panel_style = StyleBoxFlat.new()
    panel_style.bg_color = colors.surface
    panel_style.corner_radius_top_left = 8
    panel_style.corner_radius_top_right = 8
    panel_style.corner_radius_bottom_left = 8
    panel_style.corner_radius_bottom_right = 8
    panel_style.shadow_color = colors.shadow
    panel_style.shadow_size = 4
    panel_style.shadow_offset = Vector2(0, 2)
    
    theme.set_stylebox("panel", "Panel", panel_style)
    theme.set_stylebox("panel", "PanelContainer", panel_style)
    
    # TabContainer
    var tab_panel = panel_style.duplicate()
    theme.set_stylebox("panel", "TabContainer", tab_panel)
    
    # Separator
    theme.set_color("color", "HSeparator", colors.border)
    theme.set_color("color", "VSeparator", colors.border)

func _setup_popup_styles(theme: Theme, colors: Dictionary):
    var popup_style = StyleBoxFlat.new()
    popup_style.bg_color = colors.surface
    popup_style.border_color = colors.border
    popup_style.border_width_left = 1
    popup_style.border_width_right = 1
    popup_style.border_width_top = 1
    popup_style.border_width_bottom = 1
    popup_style.corner_radius_top_left = 8
    popup_style.corner_radius_top_right = 8
    popup_style.corner_radius_bottom_left = 8
    popup_style.corner_radius_bottom_right = 8
    popup_style.shadow_color = colors.shadow
    popup_style.shadow_size = 8
    popup_style.shadow_offset = Vector2(0, 4)
    
    theme.set_stylebox("panel", "PopupPanel", popup_style)
    theme.set_stylebox("panel", "PopupMenu", popup_style)

func _setup_progress_styles(theme: Theme, colors: Dictionary):
    # ProgressBar
    var progress_bg = StyleBoxFlat.new()
    progress_bg.bg_color = colors.surface
    progress_bg.corner_radius_top_left = 4
    progress_bg.corner_radius_top_right = 4
    progress_bg.corner_radius_bottom_left = 4
    progress_bg.corner_radius_bottom_right = 4
    
    var progress_fill = StyleBoxFlat.new()
    progress_fill.bg_color = colors.primary
    progress_fill.corner_radius_top_left = 4
    progress_fill.corner_radius_top_right = 4
    progress_fill.corner_radius_bottom_left = 4
    progress_fill.corner_radius_bottom_right = 4
    
    theme.set_stylebox("background", "ProgressBar", progress_bg)
    theme.set_stylebox("fill", "ProgressBar", progress_fill)
    theme.set_color("font_color", "ProgressBar", colors.text_primary)

35.7 本章小结

本章详细介绍了Godot 4的主题与样式系统:

  1. 主题系统概述:主题组成、继承机制、值获取
  2. 样式盒详解:StyleBoxFlat、StyleBoxTexture及其属性
  3. 主题变体与类型:自定义控件类型、主题变体
  4. 主题管理系统:主题管理器、切换动画
  5. 字体管理:字体配置、动态字体大小
  6. 完整主题示例:游戏UI主题实现

掌握主题系统能够创建统一、专业的UI视觉风格,并支持主题切换功能。下一章我们将学习UI动画,为界面添加生动的交互效果。

← 返回目录