save code
This commit is contained in:
+162
-148
@@ -1,222 +1,236 @@
|
||||
# 第 2 课:按钮与回调
|
||||
|
||||
**本课目标**:在窗口上放一个按钮,点击按钮后文字发生变化——学会"用户点击 → 程序响应"。
|
||||
|
||||
**复习**:第 1 课你做出了一个静态窗口。本课让它"能动"。
|
||||
> **本课目标**:在窗口里放一个按钮,**点一下文字会变**。理解 GUI 程序最核心的套路:**用户操作 → 程序响应**。
|
||||
>
|
||||
> **预计时间**:35~45 分钟
|
||||
>
|
||||
> **学完之后**:你能解释什么是回调函数,并能让按钮触发任意行为。
|
||||
|
||||
---
|
||||
|
||||
## 1. 这一课要搞懂什么
|
||||
## 0. 上课前先理理
|
||||
|
||||
### 最重要的新概念:回调(callback)
|
||||
第 1 课你做出了一个**静态**窗口——打开后就不变了。本课让它"活"起来。
|
||||
|
||||
### 这是 GUI 最大的脑回路转弯
|
||||
|
||||
以前命令行程序是这样跑的:
|
||||
```
|
||||
一行接一行执行 → 程序结束
|
||||
命令行: 从上到下跑完每一行 → 程序结束
|
||||
GUI: 创建好窗口和按钮 → 进入"等待模式" → 用户点 → 跑回调函数 → 继续等 ...
|
||||
```
|
||||
|
||||
GUI 程序是这样跑的:
|
||||
```
|
||||
创建窗口和控件 → 进入"等待模式" → 用户点按钮 → 执行回调函数 → 继续等待...
|
||||
```
|
||||
GUI 程序大部分时间在**发呆等**,只有用户点鼠标时才动一下。"动一下做什么"由你写的**回调函数**决定。
|
||||
|
||||
**回调函数**就是"当按钮被按下时,要执行的那段代码"。就像你告诉机器:"如果有人按这个按钮,就做这件事。"
|
||||
### 这一课认识 3 个新朋友
|
||||
|
||||
### 本课新朋友
|
||||
|
||||
| FLTK 类/概念 | 比喻 | 作用 |
|
||||
|-------------|------|------|
|
||||
| `Fl_Button` | 按钮 | 可以点击的控件 |
|
||||
| `callback()` | 说明书 | 告诉按钮"被按时做什么" |
|
||||
| 全局指针 | 留言板 | 让回调函数能找到要改的控件 |
|
||||
| 名字 | 比喻 | 干什么用 |
|
||||
|---------------|------------------|-----------------------------------------|
|
||||
| `Fl_Button` | **按钮** | 可以被点击的控件 |
|
||||
| `callback` | **说明书** | "按这个按钮要做什么"的那段代码 |
|
||||
| **全局指针** | **留言板** | 让回调函数能找到要修改的控件 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么需要"全局指针"?
|
||||
## 1. 起步:先编译空骨架
|
||||
|
||||
看下面的问题:
|
||||
打开 `lesson_gui_2.cpp`(已经把第 1 课的窗口写好了),切到第 2 课编译:
|
||||
|
||||
```cpp
|
||||
// ❌ 这样不行!
|
||||
void button_clicked() {
|
||||
box->label("抽卡了!"); // 这个函数里不认识 box!
|
||||
}
|
||||
|
||||
int main() {
|
||||
Fl_Box *box = new Fl_Box(50, 100, 300, 100, "还没抽");
|
||||
// box 只在 main 里有效,button_clicked 函数看不到它
|
||||
}
|
||||
```bash
|
||||
cmake -B build -DLESSON=2 # 切到第 2 课
|
||||
cmake --build build && ./build/gacha2
|
||||
```
|
||||
|
||||
**解决方案**:把 `box` 声明在 `main` 外面——成为"全局变量",到处都能看到。
|
||||
|
||||
```cpp
|
||||
// ✅ 这样就行!
|
||||
Fl_Box *box; // 全局声明
|
||||
|
||||
void button_clicked(Fl_Widget *, void *) {
|
||||
box->label("抽卡了!"); // 全局 box,函数里也能用
|
||||
}
|
||||
```
|
||||
|
||||
> 小提示:`(Fl_Widget *, void *)` 是回调要的固定格式,本课先记住写法,不用深究。
|
||||
### ✅ 检查点 1
|
||||
- [ ] 看到一个 400×300 的空窗口,标题 "抽卡世界 - 第2课"
|
||||
- [ ] 窗口里什么都没有(按钮和文字都还没加)
|
||||
|
||||
---
|
||||
|
||||
## 3. 完整示例程序
|
||||
## 2. 步骤 1:先放一个按钮(点了没反应也没关系)
|
||||
|
||||
保存为 `lesson_gui_2.cpp`:
|
||||
### 🤔 先想一下
|
||||
- 按钮和窗口、便利贴一样都是控件——它要不要 `new`?
|
||||
- 按钮和便利贴比,参数列表多了什么?少了什么?
|
||||
|
||||
### ✏️ 改两个地方
|
||||
|
||||
**(1) 顶上加 include**(替换对应 TODO):
|
||||
|
||||
```cpp
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Window.H>
|
||||
#include <FL/Fl_Button.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
|
||||
// 全局变量——让回调函数也能访问
|
||||
Fl_Box *result_box;
|
||||
|
||||
// 回调函数——按钮被按时执行
|
||||
void draw_callback(Fl_Widget *w, void *data) {
|
||||
result_box->label("你点击了抽卡按钮!");
|
||||
result_box->labelcolor(FL_RED); // 文字变红
|
||||
}
|
||||
|
||||
int main() {
|
||||
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界 - 第2课");
|
||||
|
||||
// 在上方放一个按钮
|
||||
Fl_Button *btn = new Fl_Button(130, 40, 140, 50, "🎴 抽一张!");
|
||||
btn->labelsize(18);
|
||||
btn->callback(draw_callback); // 绑定回调函数
|
||||
|
||||
// 在下方放一个显示结果的标签
|
||||
result_box = new Fl_Box(50, 130, 300, 100, "请点击按钮抽卡~");
|
||||
result_box->labelsize(20);
|
||||
|
||||
window->end();
|
||||
window->show();
|
||||
return Fl::run();
|
||||
}
|
||||
```
|
||||
|
||||
配套 `CMakeLists.txt`:
|
||||
**(2) 在 `main` 里、`window->end()` 之前加按钮**:
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(gacha2 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
if(MSVC)
|
||||
add_compile_options("/utf-8")
|
||||
endif()
|
||||
|
||||
find_package(FLTK CONFIG REQUIRED)
|
||||
|
||||
add_executable(gacha2 lesson_gui_2.cpp)
|
||||
target_link_libraries(gacha2 PRIVATE fltk)
|
||||
```cpp
|
||||
Fl_Button *btn = new Fl_Button(130, 40, 140, 50, "抽一张!");
|
||||
btn->labelsize(18);
|
||||
```
|
||||
|
||||
Mac 编译:`cmake -B build && cmake --build build && ./build/gacha2`
|
||||
读法:`new` 出一个按钮,位置 (130, 40),宽 140、高 50,文字 "抽一张!"。
|
||||
|
||||
### ▶️ 编译运行
|
||||
|
||||
```bash
|
||||
cmake --build build && ./build/gacha2
|
||||
```
|
||||
|
||||
### ✅ 检查点 2
|
||||
- [ ] 窗口里出现了一个按钮,写着 "抽一张!"
|
||||
- [ ] 鼠标移上去会变色(按钮的反馈)
|
||||
- [ ] 点一下……什么也没发生 ← 正确!因为我们还没告诉它做什么
|
||||
|
||||
---
|
||||
|
||||
## 4. 代码逐行解释
|
||||
## 3. 步骤 2:放一个文字框,先静态显示
|
||||
|
||||
按钮按了要让什么变?得先有个"被改的人"。
|
||||
|
||||
### ✏️ 在按钮代码下面加文字框
|
||||
|
||||
```cpp
|
||||
Fl_Box *result_box = new Fl_Box(50, 130, 300, 100, "请点击按钮抽卡~");
|
||||
result_box->labelsize(20);
|
||||
```
|
||||
|
||||
(注意:现在 `result_box` 还只是 `main` 里的**局部变量**——下一步会把它升级成全局)
|
||||
|
||||
### ▶️ 编译运行
|
||||
|
||||
### ✅ 检查点 3
|
||||
- [ ] 看到按钮和"请点击按钮抽卡~"两段文字
|
||||
- [ ] 点按钮还是没反应(正常)
|
||||
|
||||
---
|
||||
|
||||
## 4. 步骤 3:写回调,让点击真的改文字 ⭐核心⭐
|
||||
|
||||
这是本课最关键的一步。
|
||||
|
||||
### 🤔 先想一下这个看似简单的问题
|
||||
|
||||
我们想要:**点按钮 → 改 `result_box` 的文字**。
|
||||
|
||||
很自然的写法是:
|
||||
|
||||
```cpp
|
||||
void on_click() {
|
||||
result_box->label("你点击了抽卡按钮!"); // ← 编译器在这里报错!
|
||||
}
|
||||
```
|
||||
|
||||
为什么?因为 `result_box` 是 `main` 函数里的局部变量,`on_click` 这个外部函数**看不到它**。
|
||||
|
||||
### 💡 解决方案:把 `result_box` 变成"全局留言板"
|
||||
|
||||
把 `result_box` 的声明拿出 `main`,放到所有函数外面 → 任何函数都能用到它。
|
||||
|
||||
### ✏️ 改 3 个地方
|
||||
|
||||
**(1) 在所有函数之外(include 下面),声明全局变量**(替换对应 TODO):
|
||||
|
||||
### 全局变量
|
||||
```cpp
|
||||
Fl_Box *result_box;
|
||||
```
|
||||
放在所有函数外面 → 全局 → 回调函数和 main 都能用。
|
||||
|
||||
### 回调函数
|
||||
**(2) 在 `main` 之外也写一个回调函数**(替换对应 TODO):
|
||||
|
||||
```cpp
|
||||
void draw_callback(Fl_Widget *w, void *data) {
|
||||
result_box->label("你点击了抽卡按钮!");
|
||||
result_box->labelcolor(FL_RED);
|
||||
result_box->labelcolor(FL_RED); // 顺便把字变红
|
||||
}
|
||||
```
|
||||
- `Fl_Widget *w` —— 被点击的那个控件(本课先不关心)
|
||||
- `void *data` —— 可以传额外数据(本课先用不上)
|
||||
- 这两个参数是 FLTK 要求的固定格式
|
||||
|
||||
### 按钮
|
||||
> 📌 `(Fl_Widget *w, void *data)` 是 FLTK 规定的回调函数固定写法,**不能改**。即使你不用 `w` 和 `data`,也得写上。
|
||||
|
||||
**(3) 把 `main` 里原来的局部声明**:
|
||||
|
||||
```cpp
|
||||
Fl_Box *result_box = new Fl_Box(50, 130, 300, 100, "请点击按钮抽卡~");
|
||||
```
|
||||
|
||||
去掉前面的 `Fl_Box *`(因为已经在全局声明过类型),改成:
|
||||
|
||||
```cpp
|
||||
result_box = new Fl_Box(50, 130, 300, 100, "请点击按钮抽卡~");
|
||||
```
|
||||
|
||||
**(4) 在 `btn->labelsize(18);` 后面加一行,把按钮和回调连起来**:
|
||||
|
||||
```cpp
|
||||
Fl_Button *btn = new Fl_Button(130, 40, 140, 50, "🎴 抽一张!");
|
||||
btn->callback(draw_callback);
|
||||
```
|
||||
- 位置 (130, 40),宽 140,高 50
|
||||
- `callback(draw_callback)` —— 告诉按钮:被按时执行 `draw_callback`
|
||||
|
||||
### labelcolor
|
||||
```cpp
|
||||
result_box->labelcolor(FL_RED);
|
||||
读法:把 `draw_callback` 这本"说明书"交给按钮,按它的时候就照着做。
|
||||
|
||||
### ▶️ 编译运行
|
||||
|
||||
```bash
|
||||
cmake --build build && ./build/gacha2
|
||||
```
|
||||
- `FL_RED` 是 FLTK 内置的颜色名
|
||||
- 其他常用:`FL_BLUE`、`FL_GREEN`、`FL_BLACK`、`FL_WHITE`
|
||||
|
||||
### ✅ 检查点 4
|
||||
- [ ] 启动后看到 "请点击按钮抽卡~"
|
||||
- [ ] 点一下按钮 → 文字变成 "你点击了抽卡按钮!",**而且是红色的**
|
||||
- [ ] 多点几次……文字一直是红色,没变化(因为我们写的是固定文字)
|
||||
|
||||
🎉 **第 2 课通关!** 你完成了 GUI 编程最重要的一步:把用户操作和代码反应连起来。
|
||||
|
||||
---
|
||||
|
||||
## 5. 运行效果
|
||||
## 5. 玩一玩:改一改看会发生什么
|
||||
|
||||
启动后看到:
|
||||
```
|
||||
┌─────────────── 抽卡世界 ───────────────┐
|
||||
│ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 🎴 抽一张! │ │
|
||||
│ └──────────────┘ │
|
||||
│ │
|
||||
│ 请点击按钮抽卡~ │
|
||||
│ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
每改一个,**保存 → 编译 → 运行**:
|
||||
|
||||
点击按钮后:
|
||||
```
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 🎴 抽一张! │ │
|
||||
│ └──────────────┘ │
|
||||
│ │
|
||||
│ 你点击了抽卡按钮! ← 变红了 │
|
||||
│ │
|
||||
```
|
||||
1. 把回调里的文字改成 "你又抽了一次!"。
|
||||
2. 把 `FL_RED` 换成 `FL_BLUE`、`FL_GREEN`、`FL_MAGENTA`。
|
||||
3. 试试自定义颜色:`result_box->labelcolor(fl_rgb_color(255, 128, 0));` ← 橙色。
|
||||
4. 把按钮文字改成你自己起的名字。
|
||||
5. 在回调里**再加一行** `result_box->labelsize(30);`,点按钮后字会变更大吗?
|
||||
6. **思考题**:如果把 `btn->callback(draw_callback);` 这行注释掉,会怎样?
|
||||
|
||||
---
|
||||
|
||||
## 6. 本课课堂小练习
|
||||
## 6. 课后挑战(选做)
|
||||
|
||||
1. 修改回调函数,让点击后显示"你又抽了一次!"。
|
||||
2. 把按钮文字改成"抽一抽"或你自己想的名字。
|
||||
3. **放两个按钮**:一个"抽卡",一个"重置"。点"重置"让文字变回初始状态。
|
||||
- 提示:需要两个回调函数(或一个回调 + `data` 参数区分)
|
||||
挑一个:
|
||||
|
||||
- **A. 双按钮**:再加一个按钮 "重置",点了让 `result_box` 恢复成 "请点击按钮抽卡~",颜色变回黑色 (`FL_BLACK`)。
|
||||
- 提示:需要再写一个回调函数,比如 `reset_callback`,然后给第二个按钮 `callback(reset_callback)`。
|
||||
- **B. 计次按钮**:点 5 次之后文字自动变成 "次数用完啦!"。
|
||||
- 提示:在所有函数外加 `int count = 0;`,回调里 `count++;` 并判断。
|
||||
- **C. 思考题**:GUI 程序为什么不按"先执行第一行,再执行第二行"的固定顺序跑?回调函数是被**谁**叫起来的?
|
||||
|
||||
---
|
||||
|
||||
## 7. 本课课后作业(选做)
|
||||
## 7. 卡住了?常见小问题
|
||||
|
||||
- 点击按钮 5 次后,文字自动变成"次数用完啦!"(提示:加一个全局 `int count` 变量,在回调里累加并判断)
|
||||
- 试试 `FL_BLUE`、`FL_GREEN`、`fl_rgb_color(255, 128, 0)`(自定义橙色)
|
||||
- 思考题:为什么 GUI 程序不按"先执行第一行,再执行第二行"的固定顺序跑?
|
||||
| 现象 | 多半是 |
|
||||
|------|-------|
|
||||
| 点按钮没反应 | 忘了 `btn->callback(draw_callback);` |
|
||||
| 编译报 "`result_box` 未声明" | 全局声明那行漏了,或者写到 `main` 里去了 |
|
||||
| 编译报 "回调函数参数太少" | `(Fl_Widget *w, void *data)` 这两个参数不能省 |
|
||||
| 文字变了但颜色没变 | `labelcolor` 拼错了,或者用了不存在的颜色名 |
|
||||
| 红字看起来糊糊的 | 正常的,FLTK 默认抗锯齿如此 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见小问题
|
||||
## 8. 偷看老师答案
|
||||
|
||||
- **点按钮没反应**:检查 `callback()` 那一行有没有写,函数名有没有拼错。
|
||||
- **label 变了但颜色没变**:检查 `labelcolor()` 的拼写。
|
||||
- **`result_box` 未声明**:检查是否在 `main` 外面写了 `Fl_Box *result_box;`。
|
||||
- **回调函数的参数**:`(Fl_Widget *w, void *data)` 这个写法不能省略参数;即使不用,也要写上。
|
||||
```bash
|
||||
cmake -B build -DLESSON=2 -DUSE_SOLUTION=ON
|
||||
cmake --build build && ./build/gacha2
|
||||
```
|
||||
|
||||
源文件路径:`solutions/lesson_gui_2.cpp`。
|
||||
|
||||
---
|
||||
|
||||
## 9. 下一课预告
|
||||
|
||||
第 3 课把**随机数**加进来:点击按钮 → 随机出普通 / 稀有 / 超稀有——这才是真正的抽卡!
|
||||
第 3 课把**随机数**拉进来:点按钮 → 随机出"普通卡 / 稀有卡 / 超稀有卡",**不同稀有度配不同颜色**。这才是真正的抽卡。
|
||||
|
||||
---
|
||||
> 📝 学完记得在 `学习进度与状态-GUI.md` 里把第 2 课打勾。
|
||||
|
||||
*学完请在 `学习进度与状态-GUI.md` 里更新第 2 课的状态。*
|
||||
|
||||
**下一课文档**:`第03课-随机抽卡.md`
|
||||
**下一课**:`第03课-随机抽卡.md`
|
||||
|
||||
Reference in New Issue
Block a user