save code

This commit is contained in:
Feast
2026-05-16 12:05:04 +08:00
parent a169ceb263
commit d092dfb4a6
23 changed files with 2291 additions and 1343 deletions
+208 -145
View File
@@ -1,107 +1,147 @@
# 第 3 课:随机抽卡——核心来了
**本课目标**:点击按钮 → 随机出"普通卡 / 稀有卡 / 超稀有卡",结果显示在窗口里。把命令行抽卡逻辑搬进 GUI
**复习**:第 2 课学会了按钮和回调;命令行时期你已掌握 `rand()``srand()``if/else`
> **本课目标**:把命令行时学过的 `rand()` + `if/else` **搬进 GUI**。点按钮 → 随机出"普通卡 / 稀有卡 / 超稀有卡",不同稀有度配不同颜色
>
> **预计时间**40 分钟
>
> **学完之后**:你能在回调函数里写随机和判断,让 GUI 程序"每次都不一样"。
---
## 1. 这一课要搞懂什么
## 0. 上课前先理理
把以前学过的三样东西组合进一个 GUI 程序:
| 老朋友 | 在 GUI 里怎么用 |
|--------|---------------|
| `srand(time(...))` | 在 `main` 开头调一次(和以前一样) |
| `rand() % 100` | 放在回调函数里,每次点击执行 |
| `if / else if / else` | 放在回调函数里,根据 `r` 决定显示什么 |
**关键区别**:以前判断完用 `cout` 打印,现在用 `label()` 更新窗口上的文字。
---
## 2. 稀有度与颜色搭配
本课不仅显示文字,还**用不同颜色**区分稀有度:
| 随机数 r | 稀有度 | 文字颜色 | 概率 |
|----------|--------|---------|------|
| 0 ~ 69 | 普通卡 | 灰色 | ~70% |
| 70 ~ 94 | 稀有卡 | 蓝色 | ~25% |
| 95 ~ 99 | 超稀有卡! | 红色 | ~5% |
颜色让稀有卡一眼就能看出来——比命令行效果震撼多了。
---
## 3. 完整示例程序
保存为 `lesson_gui_3.cpp`
### 一句话回顾旧知识
```cpp
#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Box.H>
#include <cstdlib> // rand, srand
#include <ctime> // time
std::srand(std::time(nullptr)); // 设一次种子
int r = std::rand() % 100; // 每次生成 0~99 的随机数
// 全局变量
Fl_Box *result_box;
Fl_Box *info_box; // 显示本次点数的辅助信息
// 回调函数——每次点击按钮执行
void draw_callback(Fl_Widget *w, void *data) {
int r = std::rand() % 100;
// 显示点数(调试用,熟悉后可以删掉这行)
char info[50];
std::sprintf(info, "本次点数:%d", r);
info_box->label(info);
// 判断稀有度
if (r < 70) {
result_box->label("☆ 普通卡 ☆");
result_box->labelcolor(fl_rgb_color(128, 128, 128)); // 灰色
} else if (r < 95) {
result_box->label("★ 稀有卡 ★");
result_box->labelcolor(FL_BLUE); // 蓝色
} else {
result_box->label("★★★ 超稀有卡!! ★★★");
result_box->labelcolor(FL_RED); // 红色
}
}
int main() {
std::srand(std::time(nullptr)); // 种子设一次
Fl_Window *window = new Fl_Window(400, 350, "抽卡世界 - 第3课");
// 抽卡按钮
Fl_Button *btn = new Fl_Button(130, 30, 140, 50, "🎴 抽一张!");
btn->labelsize(18);
btn->callback(draw_callback);
// 结果标签(大字号)
result_box = new Fl_Box(50, 110, 300, 80, "点击按钮抽卡吧!");
result_box->labelsize(28);
result_box->labelfont(FL_BOLD);
// 信息标签(小字号,显示点数)
info_box = new Fl_Box(50, 200, 300, 50, "");
info_box->labelsize(14);
window->end();
window->show();
return Fl::run();
}
if (r < 70) { ... ... }
else if (r < 95) { ... ... }
else { ... ... }
```
这些你以前用 `std::cout` 打印结果。现在改用 `Fl_Box` 显示。
### 概率设计
| 随机数 r | 稀有度 | 颜色 | 大约概率 |
|---------|--------|------|---------|
| 0 ~ 69 | 普通卡 | 灰色 | 70% |
| 70 ~ 94 | 稀有卡 | 蓝色 | 25% |
| 95 ~ 99 | 超稀有卡 | 红色 | 5% |
### 这一课认识 2 个新朋友 + 1 个老技巧的新用法
| 名字 | 干什么 |
|-----------------|---------------------------------------------|
| `sprintf` | 把数字"塞"进字符串里(GUI 不能直接 `cout` |
| `fl_rgb_color` | 用 R/G/B 三个数自定义任意颜色 |
| `labelcolor` | (老朋友)配合上面两个,让稀有度看一眼区分 |
---
## 4. 新知识点
## 1. 起步:先编译空骨架
### sprintf —— 把数字变成字符串
打开 `lesson_gui_3.cpp`。骨架里已经准备好了第 2 课学过的东西(窗口 + 按钮 + 回调 + 全局 `result_box`),但回调里还没写"抽卡"。
```bash
cmake -B build -DLESSON=3
cmake --build build && ./build/gacha3
```
### ✅ 检查点 1
- [ ] 窗口出来了,按钮 "抽一张!" 在上面
- [ ] 下面写着 "点击按钮抽卡吧!"
- [ ] 点按钮文字会变成 "(写好步骤 4 后这里会变)"(提示当前回调没真正工作)
---
## 2. 步骤 1:让随机数能用起来
### 🤔 先想一下
- 设随机种子 `srand()` 要放哪儿?回调里行不行?
- 为什么不能每次点按钮都 `srand` 一下?
> 💡 答案:种子只能设一次,否则每次都"重置",结果会重复甚至不变。所以 `srand` 放 `main` 里、`Fl::run()` 之前。
### ✏️ 改两个地方
**(1) 加 include**(替换对应 TODO):
```cpp
#include <cstdlib> // rand, srand
#include <ctime> // time
```
**(2) 在 `main` 的第一行加**(替换对应 TODO):
```cpp
std::srand(std::time(nullptr));
```
### ▶️ 编译运行
```bash
cmake --build build && ./build/gacha3
```
### ✅ 检查点 2
- [ ] 编译没报错
- [ ] 程序行为和之前一样(因为我们还没真正用 `rand`
---
## 3. 步骤 2:在回调里掷骰子
### ✏️ 在 `draw_callback` 函数里**第一行**加(替换对应 TODO):
```cpp
int r = std::rand() % 100;
```
> 这一行还看不到效果。下一步把 `r` 显示在窗口上。
---
## 4. 步骤 3:用 `sprintf` 把数字显示出来
### 🤔 先想一下
- `result_box->label(...)` 只接受字符串。你怎么把整数 `r` 变成字符串?
- 以前命令行用 `std::cout << "r=" << r`,但 `label` 没有 `<<`。怎么办?
### 💡 新工具:`sprintf`
```cpp
char info[50]; // 准备一个 50 字节的盒子
std::sprintf(info, "本次点数:%d", r); // 把 "本次点数:" + r 的值 塞进盒子
info_box->label(info); // 把盒子的内容贴到 label 上
```
`%d` 是占位符,运行时被 `r` 的值替换。
### ✏️ 改 3 个地方
**(1) 顶上加**(替换对应 TODO):
```cpp
#include <cstdio> // sprintf
```
**(2) 在全局区,`result_box` 下面加**(替换对应 TODO):
```cpp
Fl_Box *info_box;
```
**(3) 在 `main` 里,紧挨 `result_box` 的创建代码下面加**(替换对应 TODO):
```cpp
info_box = new Fl_Box(50, 200, 300, 50, "");
info_box->labelsize(14);
```
**(4) 在 `draw_callback` 里、`int r = ...` 那一行下面加**(替换对应 TODO):
```cpp
char info[50];
@@ -109,75 +149,98 @@ std::sprintf(info, "本次点数:%d", r);
info_box->label(info);
```
以前你可能会写:
```cpp
std::cout << "本次点数:" << r << "\n";
```
### ▶️ 编译运行
但 FLTK 的 `label()` 只能接受一个字符串。`sprintf` 把数字塞进字符串里——`%d` 的位置被 `r` 的值替换。
### ✅ 检查点 3
- [ ] 点按钮 → 下方多出一行 "本次点数:XX",每次 XX 都不同
- [ ] 多点几次,能看到 0~99 各种数值
### fl_rgb_color —— 自定义颜色
---
## 5. 步骤 4:根据 `r` 决定稀有度和颜色 ⭐核心⭐
终于到抽卡的灵魂部分了。
### ✏️ 把 `draw_callback` 里那行假提示 `result_box->label("(写好步骤 4 后这里会变)");` **整段替换**成:
```cpp
fl_rgb_color(128, 128, 128) // R=128, G=128, B=128 → 灰色
fl_rgb_color(255, 200, 0) // → 金色
if (r < 70) {
result_box->label("☆ 普通卡 ☆");
result_box->labelcolor(fl_rgb_color(128, 128, 128)); // 灰色
} else if (r < 95) {
result_box->label("★ 稀有卡 ★");
result_box->labelcolor(FL_BLUE);
} else {
result_box->label("★★★ 超稀有卡!! ★★★");
result_box->labelcolor(FL_RED);
}
```
- R(红)、G(绿)、B(蓝),每个 0~255
- 试试不同的数值创造你自己的颜色!
### 💡 新工具:`fl_rgb_color(R, G, B)`
每个数 0~255,组合任意颜色:
| 颜色 | RGB |
|------|------|
| 纯白 | `(255, 255, 255)` |
| 灰 | `(128, 128, 128)` |
| 金 | `(255, 200, 0)` |
| 橙 | `(255, 128, 0)` |
| 紫 | `(180, 100, 200)` |
### ▶️ 编译运行
### ✅ 检查点 4
- [ ] 点按钮 → 看到三种结果之一,且颜色不一样
- [ ] 多点 20 次,能至少出现 1 次蓝色"稀有"
- [ ] 多点 50 次以上,能出现红色"超稀有"
🎉 **第 3 课通关!** 你的程序"每次都不一样"了。
---
## 5. 运行效果
## 6. 玩一玩
```
┌───────── 抽卡世界 - 第3课 ────────┐
│ │
│ ┌──────────────┐ │
│ │ 🎴 抽一张! │ │
│ └──────────────┘ │
│ │
│ ★ 稀有卡 ★ │ ← 蓝色大字
│ │
│ 本次点数:78 │ ← 小字
│ │
└─────────────────────────────────────┘
1. 把概率改成"超稀有更多":`r < 50` 普通,`r < 80` 稀有,其他超稀有。点 20 次试试感受。
2. 把超稀有改成金色:`fl_rgb_color(255, 200, 0)`
3. 给"超稀有"再加一行祝贺:在那个 `else` 分支里加 `info_box->label("🎉 恭喜抽到超稀有!🎉");`
4. **思考题**:如果把 `srand` 那一行从 `main` 删掉,多次运行程序会发生什么?
---
## 7. 课后挑战(选做)
- **A. 加第四档"传说卡"**`r >= 99`(约 1%),金色,更大字号。注意调整原来 95 的判断。
- **B. 抽卡计数**:用全局 `int total = 0;` 统计点击次数,每次抽完显示在 `info_box` 上(`"第 N 次:r=XX"`)。
- **C. 思考题**`if (r < 70) ... else if (r < 95) ...` —— 为什么 95 那行不需要写 `r >= 70 && r < 95`
---
## 8. 卡住了?常见小问题
| 现象 | 多半是 |
|------|-------|
| 每次结果都一样 | `srand` 没写、或者写在了回调里 |
| 编译报 `rand` / `srand` / `time` 未定义 | 忘了 `#include <cstdlib>``<ctime>` |
| 编译报 `sprintf` 未定义 | 忘了 `#include <cstdio>` |
| 信息行显示乱码 | `char info[50]` 太小?改成 `info[100]` 试试 |
| 颜色没变 | `labelcolor` 拼错;或 `fl_rgb_color` 写成了 `FL_rgb_color` |
---
## 9. 偷看老师答案
```bash
cmake -B build -DLESSON=3 -DUSE_SOLUTION=ON
cmake --build build && ./build/gacha3
```
连续点击多次,观察普通(灰)、稀有(蓝)、超稀有(红)的分布。超稀有大约 20 次出 1 次。
---
## 6. 本课课堂小练习
## 10. 下一课预告
1. 调概率:把 `r < 70` 改成 `r < 50`,把 `r < 95` 改成 `r < 85`。看看稀有和超稀有是不是变多了
2. 把"超稀有卡"的颜色改成金色 `fl_rgb_color(255, 200, 0)`
3. 给超稀有卡多加一行"🎉恭喜!"——提示:可以改 `info_box` 的内容。
第 4 课加一个 "十连抽" 按钮:一次抽 10 张全部显示。你会用上**老朋友 `for` 循环**和**字符串拼接**
---
> 📝 学完记得在 `学习进度与状态-GUI.md` 里把第 3 课打勾。
## 7. 本课课后作业(选做)
- 增加第四个稀有度:"传说卡"(`r >= 99`),颜色用金色,概率约 1%。和旧课程第 5 课的练习类似,但现在是 GUI 版本。
- 把按钮文字改成"再抽一次"。
- 试着把窗口上所有文字都换成你自己喜欢的卡牌主题(比如"SSR""SR""R"等)。
---
## 8. 常见小问题
- **每次结果都一样**:检查 `srand(time(nullptr))` 是否在 `main` 里、是否只写了一次。写在回调里会导致每次点按钮重置种子。
- **缺少 `#include <cstdlib>``<ctime>`**:报 `rand`/`srand`/`time` 未定义。
- **`sprintf` 缓冲区太小**:如果信息很长,`char info[50]` 可以改成 `char info[100]`
---
## 9. 下一课预告
第 4 课做**十连抽**:加一个"十连抽"按钮,用 `for` 循环一口气生成 10 个结果,全部显示在窗口里。
---
*学完请在 `学习进度与状态-GUI.md` 里更新第 3 课的状态。*
**下一课文档**`第04课-十连抽.md`
**下一课**`第04课-十连抽.md`