Files
chouka/第05课-卡片名字与布局.md
2026-05-16 12:05:04 +08:00

294 lines
9.2 KiB
Markdown
Raw Permalink 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.
# 第 5 课:卡片名字与布局
> **本课目标**:给每种稀有度配上具体的卡片名字(小火龙、冰霜之狼、雷霆神龙……)。再把界面分区,让窗口整齐漂亮。
>
> **预计时间**35~45 分钟
>
> **学完之后**:你能用数组管理一组"内容",并用 `box()`/`color()` 让窗口有层次感。
---
## 0. 上课前先理理
### 从 "★ 稀有卡 ★" 到 "★ 稀有卡 ★ 冰霜之狼"
设计思路两步走:
```
1. 随机决定稀有度(这一步和第 3、4 课一样)
2. 从这一档的卡片数组里再随机选一张
```
### 卡牌表设计
| 稀有度 | 卡片名 | 张数 |
|--------|--------|------|
| 普通 | 小火龙、水跃鱼、绿草龟、小电鼠、土拨仔 | 5 |
| 稀有 | 烈焰飞龙、冰霜之狼、暗影豹、风暴鹰、水晶鹿 | 5 |
| 超稀有 | 雷霆神龙、凤凰王、深海巨兽 | 3 |
### 界面分区目标
```
┌────────────────────────────┐
│ [单抽] [十连抽] │ ← 顶部:操作区
├────────────────────────────┤
│ │
│ ★ 稀有卡 ★ │ ← 中部:结果区(凹陷边框)
│ 冰霜之狼 │
│ │
├────────────────────────────┤
│ 单抽:普通 0 稀有 1 超 0 │ ← 底部:统计区(灰色背景)
└────────────────────────────┘
```
### 这一课认识 2 个新朋友
| 名字 | 干什么 |
|---------------|---------------------------------------------|
| 多组数组 | 每档稀有度一个数组,下标随机选 |
| `box()` | 控件的边框样式(凹陷、凸起、扁平……) |
| `color()` | 控件的**背景色**(注意和 `labelcolor` 文字色 区分) |
---
## 1. 起步:先看现状
打开 `lesson_gui_5.cpp`。骨架是第 4 课结束的样子——但是变量 `info_box` 之后要改名 `stats_box`。先编译看看:
```bash
cmake -B build -DLESSON=5
cmake --build build && ./build/gacha5
```
### ✅ 检查点 1
- [ ] 和第 4 课一样:单抽、十连抽都能用,结果只有"普通/稀有/超稀有"
---
## 2. 步骤 1:定义三组卡片数组
### 🤔 先想一下
- 每档数组长度可以不一样吗?(可以!)
- 怎么从数组里"随机取一张"?
### ✏️ 在所有函数之外(include 下面)加(替换对应 TODO):
```cpp
std::string normal_cards[5] = {
"小火龙", "水跃鱼", "绿草龟", "小电鼠", "土拨仔"
};
std::string rare_cards[5] = {
"烈焰飞龙", "冰霜之狼", "暗影豹", "风暴鹰", "水晶鹿"
};
std::string super_cards[3] = {
"雷霆神龙", "凤凰王", "深海巨兽"
};
```
### ▶️ 编译运行
### ✅ 检查点 2
- [ ] 编译没报错(即使还没用到这些数组)
- [ ] 程序行为同前
---
## 3. 步骤 2:让单抽显示具体卡名
### 🤔 先想一下
- "普通卡数组有 5 张,要随机选一张"——表达式是?(答案:`normal_cards[std::rand() % 5]`
- 超稀有数组只有 3 张,对应的取模数是?
### ✏️ 把 `draw_one_callback` 函数体**整体替换**成:
```cpp
void draw_one_callback(Fl_Widget *w, void *data) {
int r = std::rand() % 100;
std::string result = "";
std::string card_name = "";
if (r < 70) {
result = "☆ 普通卡 ☆";
card_name = normal_cards[std::rand() % 5];
result_box->labelcolor(fl_rgb_color(128, 128, 128));
} else if (r < 95) {
result = "★ 稀有卡 ★";
card_name = rare_cards[std::rand() % 5];
result_box->labelcolor(FL_BLUE);
} else {
result = "★★★ 超稀有卡!! ★★★";
card_name = super_cards[std::rand() % 3];
result_box->labelcolor(FL_RED);
}
// 拼成两行:稀有度 + 换行 + 卡片名
std::string display = result + "\n " + card_name + "\n";
result_box->labelsize(24);
result_box->label(display.c_str());
}
```
> 注意:暂时把 `info_box->label(...)` 拿掉了,下一步再统一处理统计行。
### ▶️ 编译运行
### ✅ 检查点 3
- [ ] 点单抽 → 看到两行:稀有度 + 卡片名
- [ ] 多点几次:同一档里的卡片名会随机变化
---
## 4. 步骤 3:让十连抽也显示卡名
### ✏️ 把 `draw_ten_callback` 里的 `for` 循环**整体替换**成:
```cpp
for (int i = 1; i <= 10; i++) {
int r = std::rand() % 100;
all += "第" + std::to_string(i) + "抽:";
if (r < 70) {
all += "普通 " + normal_cards[std::rand() % 5];
} else if (r < 95) {
all += "★稀有 " + rare_cards[std::rand() % 5];
rare_count++;
} else {
all += "★★★超稀有 " + super_cards[std::rand() % 3];
super_count++;
}
all += "\n";
}
```
只是在每个分支末尾把卡名拼了上去。
### ▶️ 编译运行
### ✅ 检查点 4
- [ ] 十连抽 10 行结果,每行都有卡名
- [ ] 同一档里卡名随机变化
---
## 5. 步骤 4:分区布局,让界面变好看 ⭐
现在界面看着像草稿。我们用 `box()``color()` 给它分区。
顺手做一件清理:把 `info_box` 改名 `stats_box`(语义更准——它显示的是统计而不是单次信息)。
### ✏️ 改 4 处
**(1) 全局变量改名**(替换对应 TODO):
```cpp
Fl_Box *stats_box;
```
(删掉原来的 `Fl_Box *info_box;`
**(2) 用编辑器**把整个文件里**所有 `info_box`** 替换成 `stats_box`(两个回调函数里都有用)。
**(3) 改窗口高度**
```cpp
Fl_Window *window = new Fl_Window(420, 520, "抽卡世界 - 第5课");
```
**(4) 把 `result_box` 和原来的 `info_box` 创建代码换成新版(替换 main 里相关 3 段)**:
```cpp
result_box = new Fl_Box(20, 80, 380, 350, "点击按钮开始抽卡!");
result_box->labelsize(22);
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
result_box->box(FL_DOWN_BOX); // ← 凹陷边框,像卡片
stats_box = new Fl_Box(20, 445, 380, 40, "等待抽卡...");
stats_box->labelsize(14);
stats_box->box(FL_FLAT_BOX);
stats_box->color(fl_rgb_color(230, 230, 230)); // ← 浅灰背景
```
也别忘了改按钮位置(让它们和新的窗口宽度匹配):
```cpp
Fl_Button *btn1 = new Fl_Button(40, 15, 150, 45, "抽一张");
Fl_Button *btn10 = new Fl_Button(230, 15, 150, 45, "十连抽");
```
### 💡 新工具:`box()` 和 `color()`
```cpp
result_box->box(FL_DOWN_BOX); // 边框样式:凹陷
stats_box->color(fl_rgb_color(230, 230, 230)); // 背景色
```
常用边框样式:
| 样式 | 效果 |
|------|------|
| `FL_NO_BOX` | 没有边框(默认) |
| `FL_FLAT_BOX` | 扁平、有背景 |
| `FL_UP_BOX` | 凸起 |
| `FL_DOWN_BOX` | 凹陷 |
| `FL_EMBOSSED_BOX` | 浮雕 |
**别混淆**`color()` 是**背景色**`labelcolor()` 是**文字色**。
### ▶️ 编译运行
### ✅ 检查点 5
- [ ] 中间是个**凹陷框**,结果显示在里面
- [ ] 底部是**灰底**的统计区
- [ ] 单抽 / 十连抽都还能用
- [ ] 整体看起来"像个游戏"了
🎉 **第 5 课通关!**
---
## 6. 玩一玩
1. 把三组卡片名全换成你自己的主题(宝可梦、数码宝贝、三国武将、星座……)。
2. 超稀有再加 2 张,**别忘了改取模数** `% 3``% 5`
3.`FL_DOWN_BOX` 改成 `FL_UP_BOX``FL_EMBOSSED_BOX` 试试。
4. 改窗口背景色:在 `Fl_Window` 创建后加 `window->color(fl_rgb_color(245, 245, 255));`
5. **思考题**:如果数组长度从 5 改成 8`rand() % 5` 不改会怎样?
---
## 7. 课后挑战(选做)
- **A. 图鉴按钮**:再加一个按钮 "图鉴",点了把三组数组里所有卡名拼成长字符串显示出来。
- **B. 突出超稀有**:在单抽回调里,如果抽到超稀有就把 `result_box->box(FL_UP_BOX);`(凸起感);其他档恢复 `FL_DOWN_BOX`
- **C. 思考题**:现在两个回调里都有"判断 `r` → 选卡名"的相似代码。能写一个函数 `std::string get_card(int &rarity)`,让两个回调都调用它吗?(这是第 6 课会做的事,可以提前试)
---
## 8. 卡住了?常见小问题
| 现象 | 多半是 |
|------|-------|
| 数组越界崩溃 | `% N` 没和数组长度对齐 |
| 同档卡名总是同一个 | `rand() % N` 放错位置,或一次抽卡里调多次但漏写 |
| 编译报 `stats_box` 未定义 | 改名时漏了某处的 `info_box` |
| 文字看不见但背景能看到 | `labelcolor``color` 设成同色了 |
| 凹陷框看不见 | `result_box->box(FL_DOWN_BOX);` 没加 |
---
## 9. 偷看老师答案
```bash
cmake -B build -DLESSON=5 -DUSE_SOLUTION=ON
cmake --build build && ./build/gacha5
```
---
## 10. 下一课预告
第 6 课是**大作业**:把前面所有功能组合起来——加**金币系统**(每抽消耗金币)、**累计统计**、**重置/退出按钮**,做一个真正像样的抽卡游戏。
> 📝 学完记得在 `学习进度与状态-GUI.md` 里把第 5 课打勾。
**下一课**`第06课-完整抽卡游戏.md`