Files
chouka/第02课-按钮与回调.md
T
2026-05-16 12:05:04 +08:00

7.4 KiB
Raw Blame History

第 2 课:按钮与回调

本课目标:在窗口里放一个按钮,点一下文字会变。理解 GUI 程序最核心的套路:用户操作 → 程序响应

预计时间35~45 分钟

学完之后:你能解释什么是回调函数,并能让按钮触发任意行为。


0. 上课前先理理

第 1 课你做出了一个静态窗口——打开后就不变了。本课让它"活"起来。

这是 GUI 最大的脑回路转弯

命令行:  从上到下跑完每一行 → 程序结束
GUI:    创建好窗口和按钮 → 进入"等待模式" → 用户点 → 跑回调函数 → 继续等 ...

GUI 程序大部分时间在发呆等,只有用户点鼠标时才动一下。"动一下做什么"由你写的回调函数决定。

这一课认识 3 个新朋友

名字 比喻 干什么用
Fl_Button 按钮 可以被点击的控件
callback 说明书 "按这个按钮要做什么"的那段代码
全局指针 留言板 让回调函数能找到要修改的控件

1. 起步:先编译空骨架

打开 lesson_gui_2.cpp(已经把第 1 课的窗口写好了),切到第 2 课编译:

cmake -B build -DLESSON=2     # 切到第 2 课
cmake --build build && ./build/gacha2

检查点 1

  • 看到一个 400×300 的空窗口,标题 "抽卡世界 - 第2课"
  • 窗口里什么都没有(按钮和文字都还没加)

2. 步骤 1:先放一个按钮(点了没反应也没关系)

🤔 先想一下

  • 按钮和窗口、便利贴一样都是控件——它要不要 new
  • 按钮和便利贴比,参数列表多了什么?少了什么?

✏️ 改两个地方

(1) 顶上加 include(替换对应 TODO):

#include <FL/Fl_Button.H>

(2) 在 main 里、window->end() 之前加按钮

Fl_Button *btn = new Fl_Button(130, 40, 140, 50, "抽一张!");
btn->labelsize(18);

读法:new 出一个按钮,位置 (130, 40),宽 140、高 50,文字 "抽一张!"。

▶️ 编译运行

cmake --build build && ./build/gacha2

检查点 2

  • 窗口里出现了一个按钮,写着 "抽一张!"
  • 鼠标移上去会变色(按钮的反馈)
  • 点一下……什么也没发生 ← 正确!因为我们还没告诉它做什么

3. 步骤 2:放一个文字框,先静态显示

按钮按了要让什么变?得先有个"被改的人"。

✏️ 在按钮代码下面加文字框

Fl_Box *result_box = new Fl_Box(50, 130, 300, 100, "请点击按钮抽卡~");
result_box->labelsize(20);

(注意:现在 result_box 还只是 main 里的局部变量——下一步会把它升级成全局)

▶️ 编译运行

检查点 3

  • 看到按钮和"请点击按钮抽卡~"两段文字
  • 点按钮还是没反应(正常)

4. 步骤 3:写回调,让点击真的改文字 核心

这是本课最关键的一步。

🤔 先想一下这个看似简单的问题

我们想要:点按钮 → 改 result_box 的文字

很自然的写法是:

void on_click() {
    result_box->label("你点击了抽卡按钮!");  // ← 编译器在这里报错!
}

为什么?因为 result_boxmain 函数里的局部变量,on_click 这个外部函数看不到它

💡 解决方案:把 result_box 变成"全局留言板"

result_box 的声明拿出 main,放到所有函数外面 → 任何函数都能用到它。

✏️ 改 3 个地方

(1) 在所有函数之外(include 下面),声明全局变量(替换对应 TODO):

Fl_Box *result_box;

(2) 在 main 之外也写一个回调函数(替换对应 TODO):

void draw_callback(Fl_Widget *w, void *data) {
    result_box->label("你点击了抽卡按钮!");
    result_box->labelcolor(FL_RED);   // 顺便把字变红
}

📌 (Fl_Widget *w, void *data) 是 FLTK 规定的回调函数固定写法,不能改。即使你不用 wdata,也得写上。

(3) 把 main 里原来的局部声明

Fl_Box *result_box = new Fl_Box(50, 130, 300, 100, "请点击按钮抽卡~");

去掉前面的 Fl_Box *(因为已经在全局声明过类型),改成:

result_box = new Fl_Box(50, 130, 300, 100, "请点击按钮抽卡~");

(4) 在 btn->labelsize(18); 后面加一行,把按钮和回调连起来

btn->callback(draw_callback);

读法:把 draw_callback 这本"说明书"交给按钮,按它的时候就照着做。

▶️ 编译运行

cmake --build build && ./build/gacha2

检查点 4

  • 启动后看到 "请点击按钮抽卡~"
  • 点一下按钮 → 文字变成 "你点击了抽卡按钮!",而且是红色的
  • 多点几次……文字一直是红色,没变化(因为我们写的是固定文字)

🎉 第 2 课通关! 你完成了 GUI 编程最重要的一步:把用户操作和代码反应连起来。


5. 玩一玩:改一改看会发生什么

每改一个,保存 → 编译 → 运行

  1. 把回调里的文字改成 "你又抽了一次!"。
  2. FL_RED 换成 FL_BLUEFL_GREENFL_MAGENTA
  3. 试试自定义颜色:result_box->labelcolor(fl_rgb_color(255, 128, 0)); ← 橙色。
  4. 把按钮文字改成你自己起的名字。
  5. 在回调里再加一行 result_box->labelsize(30);,点按钮后字会变更大吗?
  6. 思考题:如果把 btn->callback(draw_callback); 这行注释掉,会怎样?

6. 课后挑战(选做)

挑一个:

  • A. 双按钮:再加一个按钮 "重置",点了让 result_box 恢复成 "请点击按钮抽卡~",颜色变回黑色 (FL_BLACK)。
    • 提示:需要再写一个回调函数,比如 reset_callback,然后给第二个按钮 callback(reset_callback)
  • B. 计次按钮:点 5 次之后文字自动变成 "次数用完啦!"。
    • 提示:在所有函数外加 int count = 0;,回调里 count++; 并判断。
  • C. 思考题:GUI 程序为什么不按"先执行第一行,再执行第二行"的固定顺序跑?回调函数是被叫起来的?

7. 卡住了?常见小问题

现象 多半是
点按钮没反应 忘了 btn->callback(draw_callback);
编译报 "result_box 未声明" 全局声明那行漏了,或者写到 main 里去了
编译报 "回调函数参数太少" (Fl_Widget *w, void *data) 这两个参数不能省
文字变了但颜色没变 labelcolor 拼错了,或者用了不存在的颜色名
红字看起来糊糊的 正常的,FLTK 默认抗锯齿如此

8. 偷看老师答案

cmake -B build -DLESSON=2 -DUSE_SOLUTION=ON
cmake --build build && ./build/gacha2

源文件路径:solutions/lesson_gui_2.cpp


9. 下一课预告

第 3 课把随机数拉进来:点按钮 → 随机出"普通卡 / 稀有卡 / 超稀有卡",不同稀有度配不同颜色。这才是真正的抽卡。

📝 学完记得在 学习进度与状态-GUI.md 里把第 2 课打勾。

下一课第03课-随机抽卡.md