Extra2D/docs/API_Tutorial/07_UI_System.md

757 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 07. UI 系统
Extra2D 提供了一套完整的 UI 系统,支持按钮、文本、标签、复选框、单选按钮、滑块、进度条等常用控件。
## UI 控件类型
```
Widget (UI控件基类)
├── Button (按钮)
├── Text (文本)
├── Label (标签)
├── CheckBox (复选框)
├── RadioButton (单选按钮)
├── Slider (滑块)
└── ProgressBar (进度条)
```
## 坐标空间
UI 控件支持三种坐标空间:
```cpp
enum class CoordinateSpace {
World, // 世界空间 - 随相机移动(默认)
Screen, // 屏幕空间 - 固定位置,不随相机移动
Camera, // 相机空间 - 相对于相机位置的偏移
};
```
设置坐标空间:
```cpp
// 屏幕空间UI 常用)
button->setCoordinateSpace(CoordinateSpace::Screen);
button->setScreenPosition(100.0f, 50.0f);
// 或使用链式调用
button->withCoordinateSpace(CoordinateSpace::Screen)
->withScreenPosition(100.0f, 50.0f);
```
## 通用链式调用方法
所有 UI 组件都支持以下链式调用方法:
```cpp
widget->withPosition(x, y) // 设置位置
->withAnchor(x, y) // 设置锚点 (0-1)
->withCoordinateSpace(space) // 设置坐标空间
->withScreenPosition(x, y) // 设置屏幕位置
->withCameraOffset(x, y); // 设置相机偏移
```
## 按钮Button
### 创建按钮
```cpp
auto& resources = Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", 24);
// 方式1简单创建
auto button = Button::create();
button->setText("点击我");
button->setFont(font);
button->setPosition(Vec2(400, 300));
// 方式2链式调用创建
auto button = Button::create()
->withText("点击我")
->withFont(font)
->withPosition(400, 300)
->withSize(200, 60)
->withTextColor(Colors::White)
->withBackgroundColor(Colors::Blue, Colors::Green, Colors::Red)
->withBorder(Colors::White, 2.0f);
// 设置点击回调
button->setOnClick([]() {
E2D_LOG_INFO("按钮被点击!");
});
addChild(button);
```
### 按钮属性设置
```cpp
// 文本和字体
button->setText("新文本");
button->setFont(font);
button->setTextColor(Colors::White);
// 尺寸和内边距
button->setCustomSize(200.0f, 60.0f);
button->setPadding(Vec2(10.0f, 5.0f));
// 背景颜色(正常、悬停、按下三种状态)
button->setBackgroundColor(
Colors::Blue, // 正常状态
Colors::Green, // 悬停状态
Colors::Red // 按下状态
);
// 边框
button->setBorder(Colors::White, 2.0f);
// 圆角
button->setRoundedCornersEnabled(true);
button->setCornerRadius(8.0f);
// 图片背景(普通按钮)
button->setBackgroundImage(normalTex, hoverTex, pressedTex);
button->setBackgroundImageScaleMode(ImageScaleMode::ScaleFit);
// 悬停光标
button->setHoverCursor(CursorShape::Hand);
```
### 切换按钮模式Toggle Button
按钮支持切换模式,可以在 on/off 两种状态间切换:
```cpp
// 创建切换按钮
auto toggleBtn = Button::create();
toggleBtn->setToggleMode(true); // 启用切换模式
// 设置状态图片off状态图片, on状态图片
toggleBtn->setStateBackgroundImage(soundOffTex, soundOnTex);
// 或设置带悬停/按下效果的状态图片
toggleBtn->setStateBackgroundImage(
soundOffTex, soundOnTex, // 普通状态
soundOffHoverTex, soundOnHoverTex, // 悬停状态(可选)
soundOffPressedTex, soundOnPressedTex // 按下状态(可选)
);
// 设置状态文字
toggleBtn->setStateText("关闭", "开启");
// 设置状态文字颜色
toggleBtn->setStateTextColor(Colors::Red, Colors::Green);
// 设置初始状态
toggleBtn->setOn(true);
// 获取当前状态
bool isOn = toggleBtn->isOn();
// 手动切换状态
toggleBtn->toggle();
// 状态改变回调
toggleBtn->setOnStateChange([](bool isOn) {
E2D_LOG_INFO("切换按钮状态: {}", isOn ? "开启" : "关闭");
});
// 点击回调(切换模式也会触发此回调)
toggleBtn->setOnClick([]() {
E2D_LOG_INFO("按钮被点击");
});
```
### 图片缩放模式
```cpp
enum class ImageScaleMode {
Original, // 使用原图大小
Stretch, // 拉伸填充
ScaleFit, // 等比缩放,保持完整显示
ScaleFill // 等比缩放,填充整个区域(可能裁剪)
};
```
### 透明按钮(菜单项)
```cpp
// 创建纯文本按钮(透明背景,用于菜单)
auto menuBtn = Button::create();
menuBtn->setFont(font);
menuBtn->setText("新游戏");
menuBtn->setTextColor(Colors::Black);
menuBtn->setBackgroundColor(
Colors::Transparent, // 正常
Colors::Transparent, // 悬停
Colors::Transparent // 按下
);
menuBtn->setBorder(Colors::Transparent, 0.0f);
menuBtn->setPadding(Vec2(0.0f, 0.0f));
menuBtn->setCustomSize(200.0f, 40.0f);
menuBtn->setAnchor(0.5f, 0.5f); // 中心锚点
menuBtn->setPosition(centerX, centerY);
addChild(menuBtn);
```
### 按钮启用/禁用
Widget 基类提供了 `setEnabled()` 方法控制按钮的交互状态:
```cpp
// 禁用按钮
button->setEnabled(false);
// 启用按钮
button->setEnabled(true);
// 检查按钮状态
bool isEnabled = button->isEnabled();
```
### 完整示例:动画完成后启用按钮
参考 `examples/flappy_bird/GameOverLayer.cpp`
```cpp
// GameOverLayer.h
class GameOverLayer : public extra2d::Node {
public:
GameOverLayer(int score);
void onEnter() override;
void onUpdate(float dt) override;
private:
void initButtons();
int score_ = 0;
bool animationDone_ = false;
extra2d::Ptr<extra2d::Button> restartBtn_;
extra2d::Ptr<extra2d::Button> menuBtn_;
};
// GameOverLayer.cpp
void GameOverLayer::onEnter() {
Node::onEnter();
// 初始化按钮(初始禁用)
initButtons();
// 创建动画
auto moveAction = extra2d::makePtr<extra2d::MoveBy>(
1.0f, extra2d::Vec2(0.0f, -screenHeight));
// 动画完成后启用按钮
moveAction->setCompletionCallback([this]() {
animationDone_ = true;
if (restartBtn_)
restartBtn_->setEnabled(true);
if (menuBtn_)
menuBtn_->setEnabled(true);
});
runAction(moveAction);
}
void GameOverLayer::initButtons() {
auto restartFrame = ResLoader::getKeyFrame("button_restart");
if (restartFrame) {
restartBtn_ = extra2d::Button::create();
restartBtn_->setBackgroundImage(restartFrame->getTexture(),
restartFrame->getRect());
restartBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
restartBtn_->setPosition(extra2d::Vec2(0.0f, 360.0f));
restartBtn_->setEnabled(false); // 初始禁用
restartBtn_->setOnClick([]() {
// 处理点击
});
addChild(restartBtn_);
}
// 菜单按钮类似...
}
void GameOverLayer::onUpdate(float dt) {
Node::onUpdate(dt);
// 动画完成后才响应手柄输入
if (!animationDone_)
return;
auto &input = extra2d::Application::instance().input();
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
// 重新开始
}
}
```
### 使用场景
- **动画播放期间**:禁用按钮,防止用户过早操作
- **加载过程中**:禁用按钮,显示加载状态
- **条件限制**:当条件不满足时禁用按钮(如未选择关卡)
- **切换按钮**:音效开关、设置开关等需要显示两种状态的按钮
## 文本Text
### 创建文本
```cpp
// 方式1简单创建
auto text = Text::create("Hello World", font);
text->setPosition(Vec2(100, 50));
// 方式2链式调用
auto text = Text::create("Hello World")
->withFont(font)
->withPosition(100, 50)
->withTextColor(Colors::White)
->withFontSize(24)
->withAlignment(Alignment::Center);
addChild(text);
```
### 文本属性设置
```cpp
// 设置文本
text->setText("新文本");
text->setFormat("Score: %d", score); // 格式化文本
// 字体和颜色
text->setFont(font);
text->setTextColor(Colors::White);
text->setFontSize(24);
// 对齐方式
text->setAlignment(Alignment::Left); // 水平:左/中/右
text->setVerticalAlignment(VerticalAlignment::Middle); // 垂直:上/中/下
// 获取文本尺寸
Size size = text->getTextSize();
float lineHeight = text->getLineHeight();
```
### 对齐方式枚举
```cpp
enum class Alignment { Left, Center, Right };
enum class VerticalAlignment { Top, Middle, Bottom };
```
## 标签Label
Label 是功能更丰富的文本组件,支持阴影、描边、多行文本。
### 创建标签
```cpp
// 创建标签
auto label = Label::create("玩家名称", font);
label->setPosition(Vec2(100, 50));
label->setTextColor(Colors::White);
// 链式调用
auto label = Label::create("玩家名称")
->withFont(font)
->withPosition(100, 50)
->withTextColor(Colors::White)
->withFontSize(24);
addChild(label);
```
### 标签特效
```cpp
// 阴影
label->setShadowEnabled(true);
label->setShadowColor(Colors::Black);
label->setShadowOffset(Vec2(2.0f, 2.0f));
// 描边
label->setOutlineEnabled(true);
label->setOutlineColor(Colors::Black);
label->setOutlineWidth(1.0f);
// 多行文本
label->setMultiLine(true);
label->setLineSpacing(1.2f);
label->setMaxWidth(300.0f); // 自动换行宽度
// 对齐方式
label->setHorizontalAlign(HorizontalAlign::Center);
label->setVerticalAlign(VerticalAlign::Middle);
```
## 复选框CheckBox
### 创建复选框
```cpp
// 方式1简单创建
auto checkBox = CheckBox::create();
checkBox->setPosition(Vec2(100, 200));
checkBox->setChecked(true);
// 方式2带标签
checkBox = CheckBox::create("启用音效");
checkBox->setPosition(Vec2(100, 200));
// 方式3链式调用
checkBox = CheckBox::create("启用音效")
->withPosition(100, 200)
->withFont(font)
->withTextColor(Colors::White);
// 状态改变回调
checkBox->setOnStateChange([](bool checked) {
E2D_LOG_INFO("复选框状态: {}", checked);
});
addChild(checkBox);
```
### 复选框属性
```cpp
// 状态
checkBox->setChecked(true);
checkBox->toggle();
bool isChecked = checkBox->isChecked();
// 标签
checkBox->setLabel("新标签");
checkBox->setFont(font);
checkBox->setTextColor(Colors::White);
// 外观
checkBox->setBoxSize(20.0f); // 复选框大小
checkBox->setSpacing(10.0f); // 复选框与标签间距
checkBox->setCheckedColor(Colors::Green);
checkBox->setUncheckedColor(Colors::Gray);
checkBox->setCheckMarkColor(Colors::White);
```
## 单选按钮RadioButton
### 创建单选按钮
```cpp
// 创建单选按钮
auto radio1 = RadioButton::create("选项 A");
radio1->setPosition(Vec2(100, 300));
radio1->setSelected(true);
auto radio2 = RadioButton::create("选项 B");
radio2->setPosition(Vec2(100, 340));
auto radio3 = RadioButton::create("选项 C");
radio3->setPosition(Vec2(100, 380));
// 添加到组(互斥选择)
radio1->setGroupId(1);
radio2->setGroupId(1);
radio3->setGroupId(1);
// 或使用 RadioButtonGroup
auto group = std::make_shared<RadioButtonGroup>();
group->addButton(radio1.get());
group->addButton(radio2.get());
group->addButton(radio3.get());
// 选择改变回调
group->setOnSelectionChange([](RadioButton* selected) {
if (selected) {
E2D_LOG_INFO("选中: {}", selected->getLabel());
}
});
addChild(radio1);
addChild(radio2);
addChild(radio3);
```
### 单选按钮属性
```cpp
// 状态
radio->setSelected(true);
bool isSelected = radio->isSelected();
// 标签
radio->setLabel("新标签");
radio->setFont(font);
radio->setTextColor(Colors::White);
// 外观
radio->setCircleSize(16.0f); // 圆形大小
radio->setSpacing(10.0f); // 圆形与标签间距
radio->setSelectedColor(Colors::Green);
radio->setUnselectedColor(Colors::Gray);
radio->setDotColor(Colors::White);
// 分组
radio->setGroupId(1); // 相同 groupId 的按钮互斥
```
## 滑块Slider
### 创建滑块
```cpp
// 方式1简单创建
auto slider = Slider::create();
slider->setPosition(Vec2(200, 400));
slider->setRange(0.0f, 100.0f);
slider->setValue(50.0f);
// 方式2带初始值创建
auto slider = Slider::create(0.0f, 100.0f, 50.0f);
// 方式3链式调用
auto slider = Slider::create()
->withPosition(200, 400)
->withSize(200, 20)
->withMinValue(0.0f)
->withMaxValue(100.0f)
->withValue(50.0f);
// 值改变回调
slider->setOnValueChange([](float value) {
E2D_LOG_INFO("滑块值: {}", value);
});
// 拖动开始/结束回调
slider->setOnDragStart([]() {
E2D_LOG_INFO("开始拖动");
});
slider->setOnDragEnd([]() {
E2D_LOG_INFO("结束拖动");
});
addChild(slider);
```
### 滑块属性
```cpp
// 值和范围
slider->setRange(0.0f, 100.0f);
slider->setValue(50.0f);
slider->setStep(5.0f); // 步进值0表示无步进
float value = slider->getValue();
float min = slider->getMin();
float max = slider->getMax();
// 方向
slider->setVertical(false); // false=水平, true=垂直
// 外观
slider->setTrackSize(4.0f); // 轨道粗细
slider->setThumbSize(16.0f); // 滑块大小
// 颜色
slider->setTrackColor(Colors::Gray);
slider->setFillColor(Colors::Green);
slider->setThumbColor(Colors::White);
slider->setThumbHoverColor(Colors::Yellow);
slider->setThumbPressedColor(Colors::Orange);
// 显示选项
slider->setShowThumb(true); // 显示滑块
slider->setShowFill(true); // 显示填充
slider->setTextEnabled(true); // 显示数值文本
slider->setTextFormat("{:.0f}%"); // 数值格式
slider->setFont(font);
slider->setTextColor(Colors::White);
```
## 进度条ProgressBar
### 创建进度条
```cpp
// 方式1简单创建
auto progressBar = ProgressBar::create();
progressBar->setPosition(Vec2(200, 500));
progressBar->setSize(300.0f, 30.0f);
progressBar->setValue(75.0f); // 75%
// 方式2带范围创建
auto progressBar = ProgressBar::create(0.0f, 100.0f, 75.0f);
// 方式3链式调用
auto progressBar = ProgressBar::create()
->withPosition(200, 500)
->withSize(300, 30)
->withProgress(0.75f); // 0-1 的进度值
addChild(progressBar);
```
### 进度条属性
```cpp
// 值和范围
progressBar->setRange(0.0f, 100.0f);
progressBar->setValue(75.0f);
float value = progressBar->getValue();
float percent = progressBar->getPercent(); // 0.0-1.0
// 方向
progressBar->setDirection(Direction::LeftToRight);
// Direction::LeftToRight, RightToLeft, BottomToTop, TopToBottom
// 颜色
progressBar->setBackgroundColor(Colors::DarkGray);
progressBar->setFillColor(Colors::Green);
// 渐变填充
progressBar->setGradientFillEnabled(true);
progressBar->setFillColorEnd(Colors::LightGreen);
// 分段颜色(根据进度显示不同颜色)
progressBar->setSegmentedColorsEnabled(true);
progressBar->addColorSegment(0.3f, Colors::Red); // <30% 红色
progressBar->addColorSegment(0.7f, Colors::Yellow); // 30-70% 黄色
// >70% 使用默认填充色(绿色)
// 圆角
progressBar->setRoundedCornersEnabled(true);
progressBar->setCornerRadius(8.0f);
// 边框
progressBar->setBorderEnabled(true);
progressBar->setBorderColor(Colors::White);
progressBar->setBorderWidth(2.0f);
progressBar->setPadding(2.0f);
// 文本
progressBar->setTextEnabled(true);
progressBar->setTextFormat("{:.0f}%");
progressBar->setFont(font);
progressBar->setTextColor(Colors::White);
// 动画效果
progressBar->setAnimatedChangeEnabled(true);
progressBar->setAnimationSpeed(5.0f);
// 延迟显示效果
progressBar->setDelayedDisplayEnabled(true);
progressBar->setDelayTime(0.5f);
progressBar->setDelayedFillColor(Colors::Yellow);
// 条纹效果
progressBar->setStripedEnabled(true);
progressBar->setStripeColor(Colors::White);
progressBar->setStripeSpeed(1.0f);
```
## 完整示例:设置场景
```cpp
class SettingsScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
auto& resources = Application::instance().resources();
font_ = resources.loadFont("assets/font.ttf", 24);
// 标题
auto title = Text::create("设置", font_);
title->setPosition(Vec2(400, 100));
title->setAlignment(Alignment::Center);
addChild(title);
// 音效开关(使用切换按钮)
auto soundLabel = Label::create("音效", font_);
soundLabel->setPosition(Vec2(200, 200));
addChild(soundLabel);
auto soundOn = resources.loadTexture("assets/sound_on.png");
auto soundOff = resources.loadTexture("assets/sound_off.png");
soundToggle_ = Button::create();
soundToggle_->setPosition(Vec2(350, 200));
soundToggle_->setToggleMode(true);
soundToggle_->setStateBackgroundImage(soundOff, soundOn);
soundToggle_->setOn(true);
soundToggle_->setOnStateChange([](bool isOn) {
E2D_LOG_INFO("音效: {}", isOn ? "开启" : "关闭");
AudioManager::instance().setEnabled(isOn);
});
addChild(soundToggle_);
// 音量滑块
auto volumeLabel = Label::create("音量", font_);
volumeLabel->setPosition(Vec2(200, 280));
addChild(volumeLabel);
volumeSlider_ = Slider::create(0.0f, 1.0f, 0.8f);
volumeSlider_->setPosition(Vec2(350, 280));
volumeSlider_->setSize(200, 20);
volumeSlider_->setOnValueChange([](float value) {
E2D_LOG_INFO("音量: {:.0f}%", value * 100);
});
addChild(volumeSlider_);
// 难度选择
auto difficultyLabel = Label::create("难度", font_);
difficultyLabel->setPosition(Vec2(200, 360));
addChild(difficultyLabel);
auto easyRadio = RadioButton::create("简单");
easyRadio->setPosition(Vec2(350, 360));
easyRadio->setSelected(true);
easyRadio->setGroupId(1);
auto normalRadio = RadioButton::create("普通");
normalRadio->setPosition(Vec2(450, 360));
normalRadio->setGroupId(1);
auto hardRadio = RadioButton::create("困难");
hardRadio->setPosition(Vec2(550, 360));
hardRadio->setGroupId(1);
addChild(easyRadio);
addChild(normalRadio);
addChild(hardRadio);
// 返回按钮
auto backBtn = Button::create("返回", font_);
backBtn->setPosition(Vec2(400, 500));
backBtn->setCustomSize(150.0f, 50.0f);
backBtn->setBackgroundColor(
Colors::Blue,
Colors::LightBlue,
Colors::DarkBlue
);
backBtn->setOnClick([]() {
Application::instance().scenes().popScene();
});
addChild(backBtn);
}
private:
Ptr<FontAtlas> font_;
Ptr<Button> soundToggle_;
Ptr<Slider> volumeSlider_;
};
```
## 最佳实践
1. **使用屏幕空间** - UI 控件通常使用 `CoordinateSpace::Screen` 固定在屏幕上
2. **使用链式调用** - 创建控件时优先使用链式调用,代码更简洁
3. **设置合适的锚点** - 使用锚点0.5, 0.5)让控件中心对齐,方便布局
4. **复用字体资源** - 避免重复加载相同字体
5. **使用回调函数** - 使用 `setOnClick`、`setOnValueChange` 等回调响应用户操作
6. **使用切换按钮** - 对于需要显示两种状态的按钮(如开关),使用 `setToggleMode(true)`
## 下一步
- [08. 音频系统](./08_Audio_System.md) - 学习音频播放