Files
chouka/第04课-十连抽.md
2026-05-16 12:05:04 +08:00

310 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.
# 第 4 课:十连抽
> **本课目标**:再加一个 "十连抽" 按钮,用 `for` 循环一口气抽 10 次,全部结果一起显示。学会在 GUI 里用循环 + 字符串拼接。
>
> **预计时间**40 分钟
>
> **学完之后**:你能写出 "一个动作做很多次" 的 GUI 回调,并把多行结果拼成一个字符串显示。
---
## 0. 上课前先理理
### 命令行 vs GUI 的多抽
**命令行**(你以前的代码):
```cpp
for (int i = 1; i <= 10; i++) {
int r = rand() % 100;
cout << "第" << i << "抽:" << ... << "\n";
}
```
每轮 `cout` 一行,10 行自动排队。
**GUI**:你不能在循环里反复 `result_box->label(...)`——每次 `label` 都会把上一次的内容**完全覆盖**,最后只会显示最后一抽。
### 💡 GUI 的套路:先攒后贴
```
循环 10 次:
把这一抽的结果用 += 拼到一个大字符串后面
循环结束后:
把大字符串一次性贴到 label
```
像写信:先在纸上写完 10 行再寄出,而不是每写一行就寄一封。
### 这一课认识 2 个新朋友
| 名字 | 干什么 |
|-------------------|---------------------------------------------|
| `std::string` 拼接 | 用 `+``+=` 把多段字符串粘起来 |
| `std::to_string` | 把数字转成字符串(拼接时用) |
| `align()` | 让多行文字左上对齐(看起来像列表) |
---
## 1. 起步:先编译看上一课的状态
骨架已经是第 3 课的样子。先编译一次:
```bash
cmake -B build -DLESSON=4
cmake --build build && ./build/gacha4
```
### ✅ 检查点 1
- [ ] 程序行为和第 3 课一模一样:单抽按钮工作正常
- [ ] 标题是 "抽卡世界 - 第3课"(一会儿要改)
> 💡 接下来 4 步:先**重新布局**腾出地方,再加按钮,再写循环回调。
---
## 2. 步骤 1:重新布局——为十连抽腾出空间
十连抽的结果有 10 行,原来的 80 像素高的 `result_box` 装不下。先把控件挪一挪。
### 🤔 先想一下
- 窗口要加多高才能装得下 10 行字 + 两个按钮 + 一行信息?
- 把两个按钮一左一右排,单抽按钮的 X 应该是多少?
### ✏️ 改 5 处坐标
**(1) 窗口**
```cpp
Fl_Window *window = new Fl_Window(420, 480, "抽卡世界 - 第4课");
```
(宽 420,高 480,标题改了)
**(2) 单抽按钮挪到左边**
```cpp
Fl_Button *btn = new Fl_Button(50, 20, 140, 50, "抽一张");
```
**(3) `result_box` 改成大区域**(替换原来那行):
```cpp
result_box = new Fl_Box(30, 120, 360, 330, "点击按钮开始抽卡!");
result_box->labelsize(20);
```
(去掉 `labelfont(FL_BOLD)` 那行)
**(4) `info_box` 挪到 result_box 上方**
```cpp
info_box = new Fl_Box(30, 80, 360, 30, "");
info_box->labelsize(14);
```
### ▶️ 编译运行
### ✅ 检查点 2
- [ ] 窗口明显变高变宽
- [ ] 单抽按钮在左侧
- [ ] 中间一大块空白等着放结果
- [ ] 点单抽按钮还能用
---
## 3. 步骤 2:加十连抽按钮(先连个空回调)
### ✏️ 改 2 处
**(1) 在所有函数之外(紧挨 `draw_one_callback` 下面)加一个空回调**(替换对应 TODO):
```cpp
void draw_ten_callback(Fl_Widget *w, void *data) {
result_box->label("十连抽按钮通了!");
result_box->labelcolor(FL_BLACK);
}
```
**(2) 在 `main` 里、单抽按钮下面加十连抽按钮**(替换对应 TODO):
```cpp
Fl_Button *btn10 = new Fl_Button(230, 20, 140, 50, "十连抽");
btn10->labelsize(18);
btn10->callback(draw_ten_callback);
```
### ▶️ 编译运行
### ✅ 检查点 3
- [ ] 窗口顶部有两个按钮
- [ ] 点十连抽 → 文字变 "十连抽按钮通了!"(虽然还没真的抽 10 次)
- [ ] 点单抽 → 跟以前一样正常工作
🎉 这是 **GUI 开发的好习惯**:先把按钮接通,再往回调里塞真正的逻辑。
---
## 4. 步骤 3:用 `for` 循环 + 字符串拼接 ⭐核心⭐
现在把 `draw_ten_callback` 改成真正的十连抽。
### 🤔 先想一下
- 你要把 10 行结果拼成一个长字符串,怎么开头?怎么换行?
-`i` 抽的开头要拼 "第3抽:" 这种,`i``int`,怎么变成字符串?
### ✏️ 改 2 处
**(1) 顶上加 include**(替换对应 TODO):
```cpp
#include <string>
```
**(2) 把 `draw_ten_callback` 函数体整体替换成**
```cpp
void draw_ten_callback(Fl_Widget *w, void *data) {
std::string all = ""; // 攒结果的大字符串
for (int i = 1; i <= 10; i++) {
int r = std::rand() % 100;
all += "第" + std::to_string(i) + "抽:";
if (r < 70) {
all += "普通";
} else if (r < 95) {
all += "★稀有★";
} else {
all += "★★★超稀有★★★";
}
all += "\n"; // 这一抽的尾巴换一行
}
result_box->labelsize(16);
result_box->label(all.c_str()); // .c_str() 把 std::string 转成 const char*
result_box->labelcolor(FL_BLACK);
}
```
### 💡 新工具速记
| 写法 | 干什么 |
|------|--------|
| `std::to_string(i)` | 把整数 `i` 变字符串 |
| `s += "..."` | 在字符串末尾拼一段(像粘胶水) |
| `"\n"` | 字符串里的换行符 |
| `s.c_str()` | `std::string` → C 字符串(FLTK 需要这种) |
### ▶️ 编译运行
### ✅ 检查点 4
- [ ] 点十连抽 → 看到 10 行 "第N抽:xxx"
- [ ] 但是文字**居中显示**,结果看起来挤一起或者只显示中间几行 —— 别急,下一步修
---
## 5. 步骤 4:让十连结果左上对齐 + 加统计
### ✏️ 改 2 处
**(1) 在 `main` 里、`result_box` 创建之后**加一行(替换对应 TODO):
```cpp
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
```
`|` 表示"同时用这两个"。
**(2) 在 `draw_ten_callback` 里加稀有度计数**。改进版函数体(替换上一步的 `draw_ten_callback`):
```cpp
void draw_ten_callback(Fl_Widget *w, void *data) {
std::string all = "";
int rare_count = 0;
int super_count = 0;
for (int i = 1; i <= 10; i++) {
int r = std::rand() % 100;
all += "第" + std::to_string(i) + "抽:";
if (r < 70) {
all += "普通";
} else if (r < 95) {
all += "★稀有★";
rare_count++;
} else {
all += "★★★超稀有★★★";
super_count++;
}
all += "\n";
}
// 在 info_box 上显示统计
char stats[100];
std::sprintf(stats, "十连抽结果 | 稀有:%d 超稀有:%d",
rare_count, super_count);
info_box->label(stats);
if (super_count > 0) {
info_box->labelcolor(FL_RED);
} else if (rare_count > 0) {
info_box->labelcolor(FL_BLUE);
} else {
info_box->labelcolor(fl_rgb_color(128, 128, 128));
}
result_box->labelsize(16);
result_box->label(all.c_str());
result_box->labelcolor(FL_BLACK);
}
```
### ▶️ 编译运行
### ✅ 检查点 5
- [ ] 点十连抽 → 10 行从左上开始整齐排列
- [ ] 顶上信息栏出现统计 "十连抽结果 | 稀有:X 超稀有:Y"
- [ ] 出超稀有时统计变红色,只出稀有变蓝色,全普通变灰色
🎉 **第 4 课通关!**
---
## 6. 玩一玩
1. 把循环改成 `i <= 5`,变成 "五连抽",再把按钮文字改成 "五连抽"。
2. 在超稀有那行结果前加 `>>> `,让它一眼显眼:`all += ">>> ★★★超稀有★★★ <<<";`
3. 把单抽点完后清空 `info_box` 的统计——在 `draw_one_callback` 末尾加 `info_box->labelcolor(FL_BLACK);`
4. **思考题**:如果不写 `result_box->labelsize(16);`,第 4 步十连结果会怎样?为什么单抽要在自己回调里也 `labelsize(28)`
---
## 7. 课后挑战(选做)
- **A. 拆出辅助函数**:单抽和十连抽都在写"判断 `r` 落在哪档",**封装一个函数** `std::string get_rarity_name(int r)`,让两个回调都调用它。这是旧课程"函数"知识的 GUI 应用!
- **B. 自适应窗口**:在 `window->end();` 前加 `window->resizable(window);`,运行后拖窗口边角试试。
- **C. 重置按钮**:再加一个按钮 "清空",点了让 `result_box``info_box` 都变回初始文字。
---
## 8. 卡住了?常见小问题
| 现象 | 多半是 |
|------|-------|
| 十连只显示最后一抽 | 在循环里直接 `result_box->label(...)`,应该先拼字符串、循环结束后再 `label` |
| 编译报 `to_string` 未声明 | 忘了 `#include <string>` |
| 编译报 `c_str()` 未声明 | 那个变量不是 `std::string`(可能写成了 `char[]` |
| 多行文字挤一起看不清 | `result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT)` 没加 |
| 结果显示一半被切掉 | `result_box` 不够高,或 `labelsize` 太大 |
---
## 9. 偷看老师答案
```bash
cmake -B build -DLESSON=4 -DUSE_SOLUTION=ON
cmake --build build && ./build/gacha4
```
---
## 10. 下一课预告
第 5 课给每档稀有度配上**具体的卡片名字**(小火龙、冰霜之狼、雷霆神龙……),用**数组**存。还会把界面分区,整齐好看。
> 📝 学完记得在 `学习进度与状态-GUI.md` 里把第 4 课打勾。
**下一课**`第05课-卡片名字与布局.md`