save code
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
build/
|
||||||
|
build-*/
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.exe
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 抽卡世界 GUI 教程 —— 总 CMake
|
||||||
|
# 用法:cmake -B build -DLESSON=1 ← 切到第 1 课(默认 1)
|
||||||
|
# cmake --build build
|
||||||
|
# ./build/gacha1 ← Mac/Linux
|
||||||
|
# build\Release\gacha1.exe ← Windows
|
||||||
|
#
|
||||||
|
# 想看「老师写好的完整版」:cmake -B build -DLESSON=1 -DUSE_SOLUTION=ON
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(gacha LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
# Windows MSVC:让源码里的中文(UTF-8)正确编译
|
||||||
|
if(MSVC)
|
||||||
|
add_compile_options("/utf-8")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ===== 找 FLTK =====
|
||||||
|
# 先试 CONFIG(vcpkg / 自己编 FLTK 1.4 时会有 FLTKConfig.cmake)
|
||||||
|
# 找不到再回退到 CMake 自带的 FindFLTK 模块(Homebrew 走这条)
|
||||||
|
find_package(FLTK CONFIG QUIET)
|
||||||
|
if(NOT FLTK_FOUND)
|
||||||
|
find_package(FLTK REQUIRED)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ===== 选课 =====
|
||||||
|
set(LESSON 1 CACHE STRING "Which lesson to build: 1, 2, 3, 4, 5, 6")
|
||||||
|
option(USE_SOLUTION "Build from solutions/ instead of student starter file" OFF)
|
||||||
|
|
||||||
|
set(TARGET_NAME gacha${LESSON})
|
||||||
|
|
||||||
|
if(USE_SOLUTION)
|
||||||
|
set(SRC_FILE solutions/lesson_gui_${LESSON}.cpp)
|
||||||
|
else()
|
||||||
|
set(SRC_FILE lesson_gui_${LESSON}.cpp)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/${SRC_FILE}")
|
||||||
|
message(FATAL_ERROR "找不到源文件:${SRC_FILE}\n请确认 LESSON=${LESSON} 是否正确,或文件是否存在。")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message(STATUS "Building lesson ${LESSON}: ${SRC_FILE}")
|
||||||
|
|
||||||
|
add_executable(${TARGET_NAME} ${SRC_FILE})
|
||||||
|
|
||||||
|
if(TARGET fltk)
|
||||||
|
# CONFIG 模式:imported target 自带头文件路径
|
||||||
|
target_link_libraries(${TARGET_NAME} PRIVATE fltk)
|
||||||
|
else()
|
||||||
|
# 模块模式:手动加头文件路径和库
|
||||||
|
target_include_directories(${TARGET_NAME} PRIVATE ${FLTK_INCLUDE_DIR})
|
||||||
|
target_link_libraries(${TARGET_NAME} PRIVATE ${FLTK_LIBRARIES})
|
||||||
|
# Homebrew 的 FLTK 头文件是 *.H,教程里写 #include <FL/Fl.h> 会触发可移植性警告
|
||||||
|
# 在 AppleClang 下静音这个无害告警
|
||||||
|
if(APPLE)
|
||||||
|
target_compile_options(${TARGET_NAME} PRIVATE
|
||||||
|
$<$<CXX_COMPILER_ID:AppleClang>:-Wno-nonportable-include-path>)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# macOS SDK 把 sprintf 标为 deprecated(让用 snprintf)
|
||||||
|
# 对 11 岁初学者来说 sprintf 已经够用且写法简单,统一屏蔽这个告警
|
||||||
|
if(APPLE)
|
||||||
|
target_compile_options(${TARGET_NAME} PRIVATE
|
||||||
|
$<$<CXX_COMPILER_ID:AppleClang>:-Wno-deprecated-declarations>)
|
||||||
|
endif()
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.14)
|
|
||||||
project(gacha1 LANGUAGES CXX)
|
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
add_compile_options("/utf-8")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
find_package(FLTK CONFIG REQUIRED)
|
|
||||||
|
|
||||||
add_executable(gacha1 lesson_gui_1.cpp)
|
|
||||||
target_link_libraries(gacha1 PRIVATE fltk)
|
|
||||||
+21
-12
@@ -1,19 +1,28 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 1 课:GUI 你好窗口 —— 起步骨架
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// 不要一次把所有代码抄上来!跟着教程一步步往这里加。
|
||||||
|
// 教程文件:第01课-GUI你好窗口.md
|
||||||
|
//
|
||||||
|
// 想看完整答案:solutions/lesson_gui_1.cpp
|
||||||
|
// 编译命令:cmake -B build -DLESSON=1 -DUSE_SOLUTION=ON && cmake --build build
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
#include <FL/Fl.H>
|
#include <FL/Fl.H>
|
||||||
#include <FL/Fl_Window.H>
|
#include <FL/Fl_Window.H>
|
||||||
#include <FL/Fl_Box.H>
|
// TODO(步骤 2):还需要再 #include 一个头文件,才能用文字标签
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
// 创建窗口:宽 400,高 300,标题"抽卡世界"
|
// TODO(步骤 1):在这里创建一个窗口对象 window
|
||||||
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界");
|
// 窗口宽 400、高 300,标题写 "抽卡世界"
|
||||||
|
|
||||||
// 在窗口上放一个文字标签:位置(50,100),宽300,高100
|
// TODO(步骤 2):在窗口里放一个文字标签 box
|
||||||
Fl_Box *box = new Fl_Box(50, 100, 300, 100, "欢迎来到抽卡世界!");
|
// 位置 (50, 100),宽 300,高 100,文字 "欢迎来到抽卡世界!"
|
||||||
box->labelsize(24); // 字号 24
|
|
||||||
|
|
||||||
// 结束添加控件
|
// TODO(步骤 3):让文字大一点(字号 24)
|
||||||
window->end();
|
|
||||||
// 显示窗口
|
// TODO(步骤 1):告诉窗口"控件加完了",然后把窗口显示出来
|
||||||
window->show();
|
|
||||||
// 进入事件循环(让窗口一直显示)
|
// TODO(步骤 1):进入事件循环(让窗口一直显示),return Fl::run();
|
||||||
return Fl::run();
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-15
@@ -1,28 +1,32 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 2 课:按钮与回调 —— 起步骨架
|
||||||
|
// 教程文件:第02课-按钮与回调.md
|
||||||
|
// 完整答案:solutions/lesson_gui_2.cpp
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
#include <FL/Fl.H>
|
#include <FL/Fl.H>
|
||||||
#include <FL/Fl_Window.H>
|
#include <FL/Fl_Window.H>
|
||||||
#include <FL/Fl_Button.H>
|
|
||||||
#include <FL/Fl_Box.H>
|
#include <FL/Fl_Box.H>
|
||||||
|
// TODO(步骤 1):再 #include 一个头文件,才能用按钮
|
||||||
|
|
||||||
// 全局变量——让回调函数也能访问
|
// TODO(步骤 3):在这里声明一个全局变量 result_box(Fl_Box 类型的指针)
|
||||||
Fl_Box *result_box;
|
|
||||||
|
|
||||||
// 回调函数——按钮被按时执行
|
// TODO(步骤 3):在这里写一个回调函数 draw_callback
|
||||||
void draw_callback(Fl_Widget *w, void *data) {
|
// 函数签名固定写:void draw_callback(Fl_Widget *w, void *data)
|
||||||
result_box->label("你点击了抽卡按钮!");
|
// 函数体里:让 result_box 的文字变成 "你点击了抽卡按钮!"
|
||||||
result_box->labelcolor(FL_RED); // 文字变红
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界 - 第2课");
|
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界 - 第2课");
|
||||||
|
|
||||||
// 在上方放一个按钮
|
// TODO(步骤 1):放一个按钮 btn,位置 (130, 40)、宽 140、高 50、文字 "抽一张!"
|
||||||
Fl_Button *btn = new Fl_Button(130, 40, 140, 50, "抽一张!");
|
// 再用 btn->labelsize(18); 把字调大
|
||||||
btn->labelsize(18);
|
|
||||||
btn->callback(draw_callback); // 绑定回调函数
|
|
||||||
|
|
||||||
// 在下方放一个显示结果的标签
|
// TODO(步骤 3):给按钮绑定回调:btn->callback(draw_callback);
|
||||||
result_box = new Fl_Box(50, 130, 300, 100, "请点击按钮抽卡~");
|
|
||||||
result_box->labelsize(20);
|
// TODO(步骤 2):放一个结果文字框
|
||||||
|
// 先用局部变量练习,下一步再改成全局
|
||||||
|
// 位置 (50, 130)、宽 300、高 100、文字 "请点击按钮抽卡~"
|
||||||
|
// 字号 20
|
||||||
|
|
||||||
window->end();
|
window->end();
|
||||||
window->show();
|
window->show();
|
||||||
|
|||||||
+25
-27
@@ -1,54 +1,52 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 3 课:随机抽卡 —— 起步骨架
|
||||||
|
// 教程文件:第03课-随机抽卡.md
|
||||||
|
// 完整答案:solutions/lesson_gui_3.cpp
|
||||||
|
// 起步状态:复用第 2 课的"点按钮显示固定文字",逐步加随机和稀有度判断
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
#include <FL/Fl.H>
|
#include <FL/Fl.H>
|
||||||
#include <FL/Fl_Window.H>
|
#include <FL/Fl_Window.H>
|
||||||
#include <FL/Fl_Button.H>
|
#include <FL/Fl_Button.H>
|
||||||
#include <FL/Fl_Box.H>
|
#include <FL/Fl_Box.H>
|
||||||
#include <cstdlib> // rand, srand
|
// TODO(步骤 1):加上随机数和时间需要的头文件
|
||||||
#include <ctime> // time
|
// 提示:#include <cstdlib> 和 <ctime>
|
||||||
|
// TODO(步骤 3):加 #include <cstdio> 才能用 sprintf
|
||||||
|
|
||||||
// 全局变量
|
|
||||||
Fl_Box *result_box;
|
Fl_Box *result_box;
|
||||||
Fl_Box *info_box; // 显示本次点数的辅助信息
|
// TODO(步骤 3):再声明一个全局 Fl_Box 指针 info_box,用来显示本次点数
|
||||||
|
|
||||||
// 回调函数——每次点击按钮执行
|
|
||||||
void draw_callback(Fl_Widget *w, void *data) {
|
void draw_callback(Fl_Widget *w, void *data) {
|
||||||
int r = std::rand() % 100;
|
// TODO(步骤 2):在这里生成一个 0~99 的随机数 r
|
||||||
|
// 提示:int r = std::rand() % 100;
|
||||||
|
|
||||||
// 显示点数(调试用,熟悉后可以删掉这行)
|
// TODO(步骤 3):用 sprintf 把点数塞进字符串,显示在 info_box 上
|
||||||
char info[50];
|
// char info[50];
|
||||||
std::sprintf(info, "本次点数:%d", r);
|
// std::sprintf(info, "本次点数:%d", r);
|
||||||
info_box->label(info);
|
// info_box->label(info);
|
||||||
|
|
||||||
// 判断稀有度
|
// TODO(步骤 4):根据 r 的范围设置稀有度文字和颜色
|
||||||
if (r < 70) {
|
// < 70 普通卡,灰色 fl_rgb_color(128, 128, 128)
|
||||||
result_box->label("☆ 普通卡 ☆");
|
// < 95 稀有卡,FL_BLUE
|
||||||
result_box->labelcolor(fl_rgb_color(128, 128, 128)); // 灰色
|
// 其他 超稀有卡,FL_RED
|
||||||
} else if (r < 95) {
|
result_box->label("(写好步骤 4 后这里会变)");
|
||||||
result_box->label("★ 稀有卡 ★");
|
|
||||||
result_box->labelcolor(FL_BLUE); // 蓝色
|
|
||||||
} else {
|
|
||||||
result_box->label("★★★ 超稀有卡!! ★★★");
|
|
||||||
result_box->labelcolor(FL_RED); // 红色
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
std::srand(std::time(nullptr)); // 种子设一次
|
// TODO(步骤 1):在这里设一次随机种子
|
||||||
|
// 提示:std::srand(std::time(nullptr));
|
||||||
|
|
||||||
Fl_Window *window = new Fl_Window(400, 350, "抽卡世界 - 第3课");
|
Fl_Window *window = new Fl_Window(400, 350, "抽卡世界 - 第3课");
|
||||||
|
|
||||||
// 抽卡按钮
|
|
||||||
Fl_Button *btn = new Fl_Button(130, 30, 140, 50, "抽一张!");
|
Fl_Button *btn = new Fl_Button(130, 30, 140, 50, "抽一张!");
|
||||||
btn->labelsize(18);
|
btn->labelsize(18);
|
||||||
btn->callback(draw_callback);
|
btn->callback(draw_callback);
|
||||||
|
|
||||||
// 结果标签(大字号)
|
|
||||||
result_box = new Fl_Box(50, 110, 300, 80, "点击按钮抽卡吧!");
|
result_box = new Fl_Box(50, 110, 300, 80, "点击按钮抽卡吧!");
|
||||||
result_box->labelsize(28);
|
result_box->labelsize(28);
|
||||||
result_box->labelfont(FL_BOLD);
|
result_box->labelfont(FL_BOLD);
|
||||||
|
|
||||||
// 信息标签(小字号,显示点数)
|
// TODO(步骤 3):在这里创建 info_box(位置 (50,200)、宽 300、高 50、字号 14)
|
||||||
info_box = new Fl_Box(50, 200, 300, 50, "");
|
|
||||||
info_box->labelsize(14);
|
|
||||||
|
|
||||||
window->end();
|
window->end();
|
||||||
window->show();
|
window->show();
|
||||||
|
|||||||
+32
-61
@@ -1,16 +1,23 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 4 课:十连抽 —— 起步骨架
|
||||||
|
// 教程文件:第04课-十连抽.md
|
||||||
|
// 完整答案:solutions/lesson_gui_4.cpp
|
||||||
|
// 起步状态:已经是第 3 课结束时的样子(单抽 + 稀有度判断 + 颜色 + 点数显示)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
#include <FL/Fl.H>
|
#include <FL/Fl.H>
|
||||||
#include <FL/Fl_Window.H>
|
#include <FL/Fl_Window.H>
|
||||||
#include <FL/Fl_Button.H>
|
#include <FL/Fl_Button.H>
|
||||||
#include <FL/Fl_Box.H>
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <string> // for std::string
|
// TODO(步骤 3):加 #include <string> 才能用 std::string 拼接
|
||||||
|
|
||||||
// 全局变量
|
|
||||||
Fl_Box *result_box;
|
Fl_Box *result_box;
|
||||||
Fl_Box *info_box;
|
Fl_Box *info_box;
|
||||||
|
|
||||||
// 单抽回调
|
// 单抽回调:和第 3 课一样
|
||||||
void draw_one_callback(Fl_Widget *w, void *data) {
|
void draw_one_callback(Fl_Widget *w, void *data) {
|
||||||
int r = std::rand() % 100;
|
int r = std::rand() % 100;
|
||||||
|
|
||||||
@@ -28,75 +35,39 @@ void draw_one_callback(Fl_Widget *w, void *data) {
|
|||||||
result_box->label("★★★ 超稀有卡!! ★★★");
|
result_box->label("★★★ 超稀有卡!! ★★★");
|
||||||
result_box->labelcolor(FL_RED);
|
result_box->labelcolor(FL_RED);
|
||||||
}
|
}
|
||||||
|
result_box->labelsize(28);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 十连抽回调
|
// TODO(步骤 2):在这里写十连抽的回调函数 draw_ten_callback
|
||||||
void draw_ten_callback(Fl_Widget *w, void *data) {
|
// 先只让它在 result_box 上显示一行 "十连抽测试" 验证按钮通了
|
||||||
std::string all = ""; // 攒结果的字符串
|
// 步骤 3 才把循环加进去
|
||||||
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"; // 换行
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用 sprintf 做统计信息
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
std::srand(std::time(nullptr));
|
std::srand(std::time(nullptr));
|
||||||
|
|
||||||
// 窗口加高,容纳十连抽的内容
|
// TODO(步骤 1):窗口高度改大,从 350 改成 480,标题改成 "抽卡世界 - 第4课"
|
||||||
Fl_Window *window = new Fl_Window(420, 480, "抽卡世界 - 第4课");
|
Fl_Window *window = new Fl_Window(400, 350, "抽卡世界 - 第3课");
|
||||||
|
|
||||||
// 单抽按钮
|
// TODO(步骤 1):把原来居中的单抽按钮挪到左半边,位置 (50, 20),宽 140
|
||||||
Fl_Button *btn1 = new Fl_Button(50, 20, 140, 50, "抽一张");
|
Fl_Button *btn = new Fl_Button(130, 30, 140, 50, "抽一张!");
|
||||||
btn1->labelsize(18);
|
btn->labelsize(18);
|
||||||
btn1->callback(draw_one_callback);
|
btn->callback(draw_one_callback);
|
||||||
|
|
||||||
// 十连抽按钮
|
// TODO(步骤 2):在右半边添加十连抽按钮 btn10,位置 (230, 20)、宽 140、高 50
|
||||||
Fl_Button *btn10 = new Fl_Button(230, 20, 140, 50, "十连抽");
|
// 文字 "十连抽"、字号 18、callback 绑定 draw_ten_callback
|
||||||
btn10->labelsize(18);
|
|
||||||
btn10->callback(draw_ten_callback);
|
|
||||||
|
|
||||||
// 信息行
|
// TODO(步骤 1):info_box 也要挪到上面,位置 (30, 80)、宽 360、高 30
|
||||||
info_box = new Fl_Box(30, 80, 360, 30, "");
|
// 把 result_box 挪到 (30, 120)、宽 360、高 330
|
||||||
|
// 原来的 info_box 在 200、result_box 在 110,要换位置
|
||||||
|
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);
|
info_box->labelsize(14);
|
||||||
|
|
||||||
// 结果区域(加大高度)
|
// TODO(步骤 4):让 result_box 文字左上对齐(多行结果好看一些)
|
||||||
result_box = new Fl_Box(30, 120, 360, 330, "点击按钮开始抽卡!");
|
// result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
|
||||||
result_box->labelsize(20);
|
|
||||||
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT); // 左上对齐
|
|
||||||
|
|
||||||
window->end();
|
window->end();
|
||||||
window->show();
|
window->show();
|
||||||
|
|||||||
+55
-56
@@ -1,57 +1,49 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 5 课:卡片名字与布局 —— 起步骨架
|
||||||
|
// 教程文件:第05课-卡片名字与布局.md
|
||||||
|
// 完整答案:solutions/lesson_gui_5.cpp
|
||||||
|
// 起步状态:第 4 课结束的样子(单抽 + 十连抽 + 稀有度判断,但只有稀有度文字没有卡名)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
#include <FL/Fl.H>
|
#include <FL/Fl.H>
|
||||||
#include <FL/Fl_Window.H>
|
#include <FL/Fl_Window.H>
|
||||||
#include <FL/Fl_Button.H>
|
#include <FL/Fl_Button.H>
|
||||||
#include <FL/Fl_Box.H>
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
// ===== 卡片数据 =====
|
// TODO(步骤 1):在这里声明 3 个卡片名字数组
|
||||||
// 普通卡名字
|
// std::string normal_cards[5] = { "小火龙", "水跃鱼", "绿草龟", "小电鼠", "土拨仔" };
|
||||||
std::string normal_cards[5] = {
|
// std::string rare_cards[5] = { "烈焰飞龙", "冰霜之狼", "暗影豹", "风暴鹰", "水晶鹿" };
|
||||||
"小火龙", "水跃鱼", "绿草龟", "小电鼠", "土拨仔"
|
// std::string super_cards[3] = { "雷霆神龙", "凤凰王", "深海巨兽" };
|
||||||
};
|
|
||||||
// 稀有卡名字
|
|
||||||
std::string rare_cards[5] = {
|
|
||||||
"烈焰飞龙", "冰霜之狼", "暗影豹", "风暴鹰", "水晶鹿"
|
|
||||||
};
|
|
||||||
// 超稀有卡名字
|
|
||||||
std::string super_cards[3] = {
|
|
||||||
"雷霆神龙", "凤凰王", "深海巨兽"
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== 全局控件 =====
|
|
||||||
Fl_Box *result_box;
|
Fl_Box *result_box;
|
||||||
Fl_Box *stats_box;
|
// TODO(步骤 4):把 info_box 改名成 stats_box(贯穿这一课的命名风格)
|
||||||
|
Fl_Box *info_box;
|
||||||
|
|
||||||
// ===== 单抽回调 =====
|
|
||||||
void draw_one_callback(Fl_Widget *w, void *data) {
|
void draw_one_callback(Fl_Widget *w, void *data) {
|
||||||
int r = std::rand() % 100;
|
int r = std::rand() % 100;
|
||||||
std::string result = "";
|
|
||||||
std::string card_name = "";
|
char info[50];
|
||||||
|
std::sprintf(info, "单抽 | 点数:%d", r);
|
||||||
|
info_box->label(info);
|
||||||
|
|
||||||
if (r < 70) {
|
if (r < 70) {
|
||||||
result = "☆ 普通卡 ☆";
|
result_box->label("☆ 普通卡 ☆");
|
||||||
card_name = normal_cards[std::rand() % 5];
|
|
||||||
result_box->labelcolor(fl_rgb_color(128, 128, 128));
|
result_box->labelcolor(fl_rgb_color(128, 128, 128));
|
||||||
} else if (r < 95) {
|
} else if (r < 95) {
|
||||||
result = "★ 稀有卡 ★";
|
result_box->label("★ 稀有卡 ★");
|
||||||
card_name = rare_cards[std::rand() % 5];
|
|
||||||
result_box->labelcolor(FL_BLUE);
|
result_box->labelcolor(FL_BLUE);
|
||||||
} else {
|
} else {
|
||||||
result = "★★★ 超稀有卡!! ★★★";
|
result_box->label("★★★ 超稀有卡!! ★★★");
|
||||||
card_name = super_cards[std::rand() % 3];
|
|
||||||
result_box->labelcolor(FL_RED);
|
result_box->labelcolor(FL_RED);
|
||||||
}
|
}
|
||||||
|
result_box->labelsize(28);
|
||||||
// 拼接:稀有度 + 换行 + 卡片名
|
// TODO(步骤 2):把上面的 if/else 改造成"先选稀有度文字 → 再随机选一张卡名 → 拼成两行显示"
|
||||||
std::string display = result + "\n " + card_name + "\n";
|
|
||||||
result_box->labelsize(24);
|
|
||||||
result_box->label(display.c_str());
|
|
||||||
stats_box->label("单抽模式");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 十连抽回调 =====
|
|
||||||
void draw_ten_callback(Fl_Widget *w, void *data) {
|
void draw_ten_callback(Fl_Widget *w, void *data) {
|
||||||
std::string all = "";
|
std::string all = "";
|
||||||
int rare_count = 0;
|
int rare_count = 0;
|
||||||
@@ -62,54 +54,61 @@ void draw_ten_callback(Fl_Widget *w, void *data) {
|
|||||||
all += "第" + std::to_string(i) + "抽:";
|
all += "第" + std::to_string(i) + "抽:";
|
||||||
|
|
||||||
if (r < 70) {
|
if (r < 70) {
|
||||||
all += "普通 " + normal_cards[std::rand() % 5];
|
all += "普通";
|
||||||
} else if (r < 95) {
|
} else if (r < 95) {
|
||||||
all += "★稀有 " + rare_cards[std::rand() % 5];
|
all += "★稀有★";
|
||||||
rare_count++;
|
rare_count++;
|
||||||
} else {
|
} else {
|
||||||
all += "★★★超稀有 " + super_cards[std::rand() % 3];
|
all += "★★★超稀有★★★";
|
||||||
super_count++;
|
super_count++;
|
||||||
}
|
}
|
||||||
|
// TODO(步骤 3):在每个分支末尾把卡片名字也拼进去
|
||||||
|
// 比如:all += "普通 " + normal_cards[std::rand() % 5];
|
||||||
all += "\n";
|
all += "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
result_box->labelsize(15);
|
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->label(all.c_str());
|
||||||
result_box->labelcolor(FL_BLACK);
|
result_box->labelcolor(FL_BLACK);
|
||||||
|
|
||||||
char stats[100];
|
|
||||||
std::sprintf(stats, "十连:普通 %d | 稀有 %d | 超稀有 %d",
|
|
||||||
10 - rare_count - super_count, rare_count, super_count);
|
|
||||||
stats_box->label(stats);
|
|
||||||
stats_box->labelcolor(super_count > 0 ? FL_RED : FL_BLUE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
std::srand(std::time(nullptr));
|
std::srand(std::time(nullptr));
|
||||||
|
|
||||||
Fl_Window *window = new Fl_Window(420, 520, "抽卡世界 - 第5课");
|
// TODO(步骤 4):窗口高度从 480 改成 520
|
||||||
|
Fl_Window *window = new Fl_Window(420, 480, "抽卡世界 - 第4课");
|
||||||
|
|
||||||
// ===== 操作区 =====
|
Fl_Button *btn1 = new Fl_Button(50, 20, 140, 50, "抽一张");
|
||||||
Fl_Button *btn1 = new Fl_Button(40, 15, 150, 45, "抽一张");
|
|
||||||
btn1->labelsize(18);
|
btn1->labelsize(18);
|
||||||
btn1->callback(draw_one_callback);
|
btn1->callback(draw_one_callback);
|
||||||
|
|
||||||
Fl_Button *btn10 = new Fl_Button(230, 15, 150, 45, "十连抽");
|
Fl_Button *btn10 = new Fl_Button(230, 20, 140, 50, "十连抽");
|
||||||
btn10->labelsize(18);
|
btn10->labelsize(18);
|
||||||
btn10->callback(draw_ten_callback);
|
btn10->callback(draw_ten_callback);
|
||||||
|
|
||||||
// ===== 结果显示区 =====
|
info_box = new Fl_Box(30, 80, 360, 30, "");
|
||||||
result_box = new Fl_Box(20, 80, 380, 350, "点击按钮开始抽卡!");
|
info_box->labelsize(14);
|
||||||
result_box->labelsize(22);
|
|
||||||
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
|
|
||||||
result_box->box(FL_DOWN_BOX); // 凹陷边框,像卡片
|
|
||||||
|
|
||||||
// ===== 统计区 =====
|
result_box = new Fl_Box(30, 120, 360, 330, "点击按钮开始抽卡!");
|
||||||
stats_box = new Fl_Box(20, 445, 380, 40, "等待抽卡...");
|
result_box->labelsize(20);
|
||||||
stats_box->labelsize(14);
|
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
|
||||||
stats_box->box(FL_FLAT_BOX);
|
|
||||||
stats_box->color(fl_rgb_color(230, 230, 230)); // 浅灰背景
|
// TODO(步骤 4):result_box 加一个凹陷边框,让它看起来像一张卡片
|
||||||
|
// result_box->box(FL_DOWN_BOX);
|
||||||
|
|
||||||
|
// TODO(步骤 4):把 info_box 改成放在底部 (20, 445, 380, 40),加灰色背景,作为"统计区"
|
||||||
|
|
||||||
window->end();
|
window->end();
|
||||||
window->show();
|
window->show();
|
||||||
|
|||||||
+78
-133
@@ -1,7 +1,16 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 6 课:完整抽卡游戏 —— 起步骨架
|
||||||
|
// 教程文件:第06课-完整抽卡游戏.md
|
||||||
|
// 完整答案:solutions/lesson_gui_6.cpp
|
||||||
|
// 起步状态:第 5 课结束的样子(卡片名字 + 分区布局)
|
||||||
|
// 这一课要做的:金币系统 + 累计统计 + 重置/退出按钮 + 函数复用
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
#include <FL/Fl.H>
|
#include <FL/Fl.H>
|
||||||
#include <FL/Fl_Window.H>
|
#include <FL/Fl_Window.H>
|
||||||
#include <FL/Fl_Button.H>
|
#include <FL/Fl_Button.H>
|
||||||
#include <FL/Fl_Box.H>
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -17,189 +26,125 @@ std::string super_cards[3] = {
|
|||||||
"雷霆神龙", "凤凰王", "深海巨兽"
|
"雷霆神龙", "凤凰王", "深海巨兽"
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== 游戏状态(全局) =====
|
// TODO(步骤 2):在这里加游戏状态变量
|
||||||
int coins = 500;
|
// int coins = 500;
|
||||||
int total_normal = 0;
|
// int total_normal = 0, total_rare = 0, total_super = 0;
|
||||||
int total_rare = 0;
|
|
||||||
int total_super = 0;
|
|
||||||
|
|
||||||
// ===== 全局控件 =====
|
// ===== 全局控件 =====
|
||||||
|
// TODO(步骤 1):把 window 也升级成全局变量,下面 main 里别忘了去掉局部声明
|
||||||
Fl_Box *result_box;
|
Fl_Box *result_box;
|
||||||
Fl_Box *stats_box;
|
Fl_Box *stats_box;
|
||||||
Fl_Box *coins_box;
|
// TODO(步骤 2):再声明一个 Fl_Box *coins_box;
|
||||||
|
|
||||||
// ===== 更新金币显示 =====
|
// TODO(步骤 2):辅助函数 update_coins_display(),把 coins 显示到 coins_box
|
||||||
void update_coins_display() {
|
// TODO(步骤 3):辅助函数 update_stats_display(),把累计统计显示到 stats_box
|
||||||
char buf[50];
|
|
||||||
std::sprintf(buf, "金币:%d", coins);
|
|
||||||
coins_box->label(buf);
|
|
||||||
if (coins < 100) {
|
|
||||||
coins_box->labelcolor(FL_RED);
|
|
||||||
} else {
|
|
||||||
coins_box->labelcolor(fl_rgb_color(0, 100, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 更新统计显示 =====
|
// TODO(步骤 4):把"抽一张卡"的逻辑封装成 std::string get_card(int &rare_level)
|
||||||
void update_stats_display() {
|
// 函数内:随机 → 判断 → 累加 total_* 统计 → 返回卡名
|
||||||
char buf[100];
|
// 两个回调都调用它,不要重复写
|
||||||
std::sprintf(buf, "累计:普通 %d | 稀有 %d | 超稀有 %d",
|
|
||||||
total_normal, total_rare, total_super);
|
|
||||||
stats_box->label(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 获得一张卡的名字和稀有度 =====
|
|
||||||
// rare_level: 0=普通, 1=稀有, 2=超稀有
|
|
||||||
std::string get_card(int &rare_level) {
|
|
||||||
int r = std::rand() % 100;
|
|
||||||
if (r < 70) {
|
|
||||||
rare_level = 0;
|
|
||||||
total_normal++;
|
|
||||||
return normal_cards[std::rand() % 5];
|
|
||||||
} else if (r < 95) {
|
|
||||||
rare_level = 1;
|
|
||||||
total_rare++;
|
|
||||||
return rare_cards[std::rand() % 5];
|
|
||||||
} else {
|
|
||||||
rare_level = 2;
|
|
||||||
total_super++;
|
|
||||||
return super_cards[std::rand() % 3];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 单抽回调 =====
|
|
||||||
void draw_one_callback(Fl_Widget *w, void *data) {
|
void draw_one_callback(Fl_Widget *w, void *data) {
|
||||||
if (coins < 10) {
|
// TODO(步骤 2):先检查金币是否够 10,不够就提示并 return
|
||||||
result_box->label("金币不够!单抽需要 10 金币。\n试试点\"重置\"获得新金币。");
|
// TODO(步骤 2):扣 10 金币 → update_coins_display()
|
||||||
result_box->labelcolor(FL_RED);
|
|
||||||
result_box->labelsize(16);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
coins -= 10;
|
int r = std::rand() % 100;
|
||||||
update_coins_display();
|
std::string result = "";
|
||||||
|
std::string card_name = "";
|
||||||
|
|
||||||
int level = 0;
|
if (r < 70) {
|
||||||
std::string card = get_card(level);
|
result = "☆ 普通卡 ☆";
|
||||||
|
card_name = normal_cards[std::rand() % 5];
|
||||||
std::string result;
|
|
||||||
if (level == 0) {
|
|
||||||
result = "☆ 普通卡 ☆\n " + card;
|
|
||||||
result_box->labelcolor(fl_rgb_color(128, 128, 128));
|
result_box->labelcolor(fl_rgb_color(128, 128, 128));
|
||||||
} else if (level == 1) {
|
} else if (r < 95) {
|
||||||
result = "★ 稀有卡 ★\n " + card;
|
result = "★ 稀有卡 ★";
|
||||||
|
card_name = rare_cards[std::rand() % 5];
|
||||||
result_box->labelcolor(FL_BLUE);
|
result_box->labelcolor(FL_BLUE);
|
||||||
} else {
|
} else {
|
||||||
result = "★★★ 超稀有卡!! ★★★\n " + card + " !";
|
result = "★★★ 超稀有卡!! ★★★";
|
||||||
|
card_name = super_cards[std::rand() % 3];
|
||||||
result_box->labelcolor(FL_RED);
|
result_box->labelcolor(FL_RED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string display = result + "\n " + card_name + "\n";
|
||||||
result_box->labelsize(24);
|
result_box->labelsize(24);
|
||||||
result_box->label(result.c_str());
|
result_box->label(display.c_str());
|
||||||
update_stats_display();
|
|
||||||
|
// TODO(步骤 3):抽完后 update_stats_display()
|
||||||
|
// TODO(步骤 4):用 get_card() 替换上面整个 if/else/else,让代码不重复
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 十连抽回调 =====
|
|
||||||
void draw_ten_callback(Fl_Widget *w, void *data) {
|
void draw_ten_callback(Fl_Widget *w, void *data) {
|
||||||
if (coins < 100) {
|
// TODO(步骤 2):先检查金币是否够 100,不够就提示并 return
|
||||||
result_box->label("金币不够!十连抽需要 100 金币。\n试试点\"重置\"获得新金币。");
|
// TODO(步骤 2):扣 100 金币 → update_coins_display()
|
||||||
result_box->labelcolor(FL_RED);
|
|
||||||
result_box->labelsize(16);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
coins -= 100;
|
|
||||||
update_coins_display();
|
|
||||||
|
|
||||||
std::string all = "";
|
std::string all = "";
|
||||||
int ten_normal = 0, ten_rare = 0, ten_super = 0;
|
int rare_count = 0;
|
||||||
|
int super_count = 0;
|
||||||
|
|
||||||
for (int i = 1; i <= 10; i++) {
|
for (int i = 1; i <= 10; i++) {
|
||||||
int level = 0;
|
int r = std::rand() % 100;
|
||||||
std::string card = get_card(level);
|
|
||||||
|
|
||||||
all += "第" + std::to_string(i) + "抽:";
|
all += "第" + std::to_string(i) + "抽:";
|
||||||
|
|
||||||
if (level == 0) {
|
if (r < 70) {
|
||||||
all += "普通 " + card;
|
all += "普通 " + normal_cards[std::rand() % 5];
|
||||||
ten_normal++;
|
} else if (r < 95) {
|
||||||
} else if (level == 1) {
|
all += "★稀有 " + rare_cards[std::rand() % 5];
|
||||||
all += "★稀有 " + card;
|
rare_count++;
|
||||||
ten_rare++;
|
|
||||||
} else {
|
} else {
|
||||||
all += "★★★超稀有 " + card;
|
all += "★★★超稀有 " + super_cards[std::rand() % 3];
|
||||||
ten_super++;
|
super_count++;
|
||||||
}
|
}
|
||||||
all += "\n";
|
all += "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 汇总行
|
result_box->labelsize(15);
|
||||||
char summary[80];
|
|
||||||
std::sprintf(summary, "\n--- 本次:普通%d 稀有%d 超稀有%d ---",
|
|
||||||
ten_normal, ten_rare, ten_super);
|
|
||||||
all += summary;
|
|
||||||
|
|
||||||
result_box->labelsize(14);
|
|
||||||
result_box->label(all.c_str());
|
result_box->label(all.c_str());
|
||||||
result_box->labelcolor(FL_BLACK);
|
result_box->labelcolor(FL_BLACK);
|
||||||
update_stats_display();
|
|
||||||
|
char stats[100];
|
||||||
|
std::sprintf(stats, "十连:普通 %d | 稀有 %d | 超稀有 %d",
|
||||||
|
10 - rare_count - super_count, rare_count, super_count);
|
||||||
|
stats_box->label(stats);
|
||||||
|
stats_box->labelcolor(super_count > 0 ? FL_RED : FL_BLUE);
|
||||||
|
|
||||||
|
// TODO(步骤 3):改成调用 update_stats_display() 显示**累计**统计
|
||||||
|
// TODO(步骤 4):用 get_card() 替换上面循环里的 if/else,让代码不重复
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 重置回调 =====
|
// TODO(步骤 5):在这里写 reset_callback:金币和统计全部清零、刷新显示
|
||||||
void reset_callback(Fl_Widget *w, void *data) {
|
// TODO(步骤 6):在这里写 quit_callback:window->hide();
|
||||||
coins = 500;
|
|
||||||
total_normal = 0;
|
|
||||||
total_rare = 0;
|
|
||||||
total_super = 0;
|
|
||||||
update_coins_display();
|
|
||||||
update_stats_display();
|
|
||||||
result_box->labelsize(20);
|
|
||||||
result_box->label("游戏已重置!\n金币回到 500,统计清零。\n开始抽卡吧!");
|
|
||||||
result_box->labelcolor(FL_BLACK);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 退出回调 =====
|
|
||||||
void quit_callback(Fl_Widget *w, void *data) {
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
std::srand(std::time(nullptr));
|
std::srand(std::time(nullptr));
|
||||||
|
|
||||||
Fl_Window *window = new Fl_Window(440, 560, "抽卡大冒险");
|
// TODO(步骤 1):把窗口宽改成 440,高改成 560,标题改成 "抽卡大冒险"
|
||||||
|
// 注意:window 已经是全局变量了,这里不要再写 Fl_Window *
|
||||||
|
Fl_Window *window = new Fl_Window(420, 520, "抽卡世界 - 第5课");
|
||||||
|
|
||||||
// ===== 顶部:操作按钮 =====
|
Fl_Button *btn1 = new Fl_Button(40, 15, 150, 45, "抽一张");
|
||||||
Fl_Button *btn1 = new Fl_Button(30, 15, 150, 50, "单抽\n(10金币)");
|
btn1->labelsize(18);
|
||||||
btn1->labelsize(16);
|
|
||||||
btn1->callback(draw_one_callback);
|
btn1->callback(draw_one_callback);
|
||||||
|
|
||||||
Fl_Button *btn10 = new Fl_Button(260, 15, 150, 50, "十连抽\n(100金币)");
|
Fl_Button *btn10 = new Fl_Button(230, 15, 150, 45, "十连抽");
|
||||||
btn10->labelsize(16);
|
btn10->labelsize(18);
|
||||||
btn10->callback(draw_ten_callback);
|
btn10->callback(draw_ten_callback);
|
||||||
|
|
||||||
// ===== 金币显示 =====
|
// TODO(步骤 2):在这里创建 coins_box
|
||||||
coins_box = new Fl_Box(30, 75, 380, 30, "");
|
// 位置 (30, 75)、宽 380、高 30、字号 20、加粗
|
||||||
coins_box->labelsize(20);
|
// 然后调一次 update_coins_display()
|
||||||
coins_box->labelfont(FL_BOLD);
|
|
||||||
update_coins_display();
|
|
||||||
|
|
||||||
// ===== 结果显示区 =====
|
result_box = new Fl_Box(20, 80, 380, 350, "点击按钮开始抽卡!");
|
||||||
result_box = new Fl_Box(20, 115, 400, 270, "欢迎来到抽卡大冒险!\n点击上方按钮开始抽卡~");
|
result_box->labelsize(22);
|
||||||
result_box->labelsize(20);
|
|
||||||
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
|
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
|
||||||
result_box->box(FL_DOWN_BOX);
|
result_box->box(FL_DOWN_BOX);
|
||||||
|
|
||||||
// ===== 累计统计 =====
|
stats_box = new Fl_Box(20, 445, 380, 40, "等待抽卡...");
|
||||||
stats_box = new Fl_Box(20, 400, 400, 30, "");
|
|
||||||
stats_box->labelsize(14);
|
stats_box->labelsize(14);
|
||||||
update_stats_display();
|
stats_box->box(FL_FLAT_BOX);
|
||||||
|
stats_box->color(fl_rgb_color(230, 230, 230));
|
||||||
|
|
||||||
// ===== 底部按钮 =====
|
// TODO(步骤 5):加 "重置" 按钮,位置 (100, 450)、宽 100、高 40,绑定 reset_callback
|
||||||
Fl_Button *btn_reset = new Fl_Button(100, 450, 100, 40, "重置");
|
// TODO(步骤 6):加 "退出" 按钮,位置 (240, 450)、宽 100、高 40,绑定 quit_callback
|
||||||
btn_reset->callback(reset_callback);
|
|
||||||
|
|
||||||
Fl_Button *btn_quit = new Fl_Button(240, 450, 100, 40, "退出");
|
|
||||||
btn_quit->callback(quit_callback);
|
|
||||||
|
|
||||||
window->end();
|
window->end();
|
||||||
window->show();
|
window->show();
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 1 课:GUI 你好窗口 —— 老师完整答案
|
||||||
|
// 自己写完前不要偷看!卡住时再来对一下。
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Window.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界");
|
||||||
|
|
||||||
|
Fl_Box *box = new Fl_Box(50, 100, 300, 100, "欢迎来到抽卡世界!");
|
||||||
|
box->labelsize(24);
|
||||||
|
|
||||||
|
window->end();
|
||||||
|
window->show();
|
||||||
|
return Fl::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 2 课:按钮与回调 —— 老师完整答案
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 3 课:随机抽卡 —— 老师完整答案
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Window.H>
|
||||||
|
#include <FL/Fl_Button.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <cstdio> // sprintf
|
||||||
|
#include <cstdlib> // rand, srand
|
||||||
|
#include <ctime> // time
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 4 课:十连抽 —— 老师完整答案
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Window.H>
|
||||||
|
#include <FL/Fl_Button.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
Fl_Box *result_box;
|
||||||
|
Fl_Box *info_box;
|
||||||
|
|
||||||
|
void draw_one_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);
|
||||||
|
}
|
||||||
|
result_box->labelsize(28);
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::srand(std::time(nullptr));
|
||||||
|
|
||||||
|
Fl_Window *window = new Fl_Window(420, 480, "抽卡世界 - 第4课");
|
||||||
|
|
||||||
|
Fl_Button *btn1 = new Fl_Button(50, 20, 140, 50, "抽一张");
|
||||||
|
btn1->labelsize(18);
|
||||||
|
btn1->callback(draw_one_callback);
|
||||||
|
|
||||||
|
Fl_Button *btn10 = new Fl_Button(230, 20, 140, 50, "十连抽");
|
||||||
|
btn10->labelsize(18);
|
||||||
|
btn10->callback(draw_ten_callback);
|
||||||
|
|
||||||
|
info_box = new Fl_Box(30, 80, 360, 30, "");
|
||||||
|
info_box->labelsize(14);
|
||||||
|
|
||||||
|
result_box = new Fl_Box(30, 120, 360, 330, "点击按钮开始抽卡!");
|
||||||
|
result_box->labelsize(20);
|
||||||
|
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
|
||||||
|
|
||||||
|
window->end();
|
||||||
|
window->show();
|
||||||
|
return Fl::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 5 课:卡片名字与布局 —— 老师完整答案
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Window.H>
|
||||||
|
#include <FL/Fl_Button.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
std::string normal_cards[5] = {
|
||||||
|
"小火龙", "水跃鱼", "绿草龟", "小电鼠", "土拨仔"
|
||||||
|
};
|
||||||
|
std::string rare_cards[5] = {
|
||||||
|
"烈焰飞龙", "冰霜之狼", "暗影豹", "风暴鹰", "水晶鹿"
|
||||||
|
};
|
||||||
|
std::string super_cards[3] = {
|
||||||
|
"雷霆神龙", "凤凰王", "深海巨兽"
|
||||||
|
};
|
||||||
|
|
||||||
|
Fl_Box *result_box;
|
||||||
|
Fl_Box *stats_box;
|
||||||
|
|
||||||
|
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());
|
||||||
|
stats_box->label("单抽模式");
|
||||||
|
stats_box->labelcolor(FL_BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 += "普通 " + 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
result_box->labelsize(15);
|
||||||
|
result_box->label(all.c_str());
|
||||||
|
result_box->labelcolor(FL_BLACK);
|
||||||
|
|
||||||
|
char stats[100];
|
||||||
|
std::sprintf(stats, "十连:普通 %d | 稀有 %d | 超稀有 %d",
|
||||||
|
10 - rare_count - super_count, rare_count, super_count);
|
||||||
|
stats_box->label(stats);
|
||||||
|
stats_box->labelcolor(super_count > 0 ? FL_RED : FL_BLUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::srand(std::time(nullptr));
|
||||||
|
|
||||||
|
Fl_Window *window = new Fl_Window(420, 520, "抽卡世界 - 第5课");
|
||||||
|
|
||||||
|
Fl_Button *btn1 = new Fl_Button(40, 15, 150, 45, "抽一张");
|
||||||
|
btn1->labelsize(18);
|
||||||
|
btn1->callback(draw_one_callback);
|
||||||
|
|
||||||
|
Fl_Button *btn10 = new Fl_Button(230, 15, 150, 45, "十连抽");
|
||||||
|
btn10->labelsize(18);
|
||||||
|
btn10->callback(draw_ten_callback);
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
window->end();
|
||||||
|
window->show();
|
||||||
|
return Fl::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// 第 6 课:完整抽卡游戏 —— 老师完整答案
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Window.H>
|
||||||
|
#include <FL/Fl_Button.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// ===== 卡片数据 =====
|
||||||
|
std::string normal_cards[5] = {
|
||||||
|
"小火龙", "水跃鱼", "绿草龟", "小电鼠", "土拨仔"
|
||||||
|
};
|
||||||
|
std::string rare_cards[5] = {
|
||||||
|
"烈焰飞龙", "冰霜之狼", "暗影豹", "风暴鹰", "水晶鹿"
|
||||||
|
};
|
||||||
|
std::string super_cards[3] = {
|
||||||
|
"雷霆神龙", "凤凰王", "深海巨兽"
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 游戏状态 =====
|
||||||
|
int coins = 500;
|
||||||
|
int total_normal = 0;
|
||||||
|
int total_rare = 0;
|
||||||
|
int total_super = 0;
|
||||||
|
|
||||||
|
// ===== 全局控件 =====
|
||||||
|
Fl_Window *window;
|
||||||
|
Fl_Box *result_box;
|
||||||
|
Fl_Box *stats_box;
|
||||||
|
Fl_Box *coins_box;
|
||||||
|
|
||||||
|
void update_coins_display() {
|
||||||
|
char buf[50];
|
||||||
|
std::sprintf(buf, "金币:%d", coins);
|
||||||
|
coins_box->label(buf);
|
||||||
|
if (coins < 100) {
|
||||||
|
coins_box->labelcolor(FL_RED);
|
||||||
|
} else {
|
||||||
|
coins_box->labelcolor(fl_rgb_color(0, 100, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_stats_display() {
|
||||||
|
char buf[100];
|
||||||
|
std::sprintf(buf, "累计:普通 %d | 稀有 %d | 超稀有 %d",
|
||||||
|
total_normal, total_rare, total_super);
|
||||||
|
stats_box->label(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽一张卡:返回卡名,rare_level 引用参数表示稀有度(0/1/2)
|
||||||
|
std::string get_card(int &rare_level) {
|
||||||
|
int r = std::rand() % 100;
|
||||||
|
if (r < 70) {
|
||||||
|
rare_level = 0;
|
||||||
|
total_normal++;
|
||||||
|
return normal_cards[std::rand() % 5];
|
||||||
|
} else if (r < 95) {
|
||||||
|
rare_level = 1;
|
||||||
|
total_rare++;
|
||||||
|
return rare_cards[std::rand() % 5];
|
||||||
|
} else {
|
||||||
|
rare_level = 2;
|
||||||
|
total_super++;
|
||||||
|
return super_cards[std::rand() % 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_one_callback(Fl_Widget *w, void *data) {
|
||||||
|
if (coins < 10) {
|
||||||
|
result_box->label("金币不够!单抽需要 10 金币。\n点\"重置\"恢复金币。");
|
||||||
|
result_box->labelcolor(FL_RED);
|
||||||
|
result_box->labelsize(16);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
coins -= 10;
|
||||||
|
update_coins_display();
|
||||||
|
|
||||||
|
int level = 0;
|
||||||
|
std::string card = get_card(level);
|
||||||
|
|
||||||
|
std::string result;
|
||||||
|
if (level == 0) {
|
||||||
|
result = "☆ 普通卡 ☆\n " + card;
|
||||||
|
result_box->labelcolor(fl_rgb_color(128, 128, 128));
|
||||||
|
} else if (level == 1) {
|
||||||
|
result = "★ 稀有卡 ★\n " + card;
|
||||||
|
result_box->labelcolor(FL_BLUE);
|
||||||
|
} else {
|
||||||
|
result = "★★★ 超稀有卡!! ★★★\n " + card;
|
||||||
|
result_box->labelcolor(FL_RED);
|
||||||
|
}
|
||||||
|
|
||||||
|
result_box->labelsize(24);
|
||||||
|
result_box->label(result.c_str());
|
||||||
|
update_stats_display();
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_ten_callback(Fl_Widget *w, void *data) {
|
||||||
|
if (coins < 100) {
|
||||||
|
result_box->label("金币不够!十连抽需要 100 金币。\n点\"重置\"恢复金币。");
|
||||||
|
result_box->labelcolor(FL_RED);
|
||||||
|
result_box->labelsize(16);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
coins -= 100;
|
||||||
|
update_coins_display();
|
||||||
|
|
||||||
|
std::string all = "";
|
||||||
|
int ten_normal = 0, ten_rare = 0, ten_super = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i <= 10; i++) {
|
||||||
|
int level = 0;
|
||||||
|
std::string card = get_card(level);
|
||||||
|
|
||||||
|
all += "第" + std::to_string(i) + "抽:";
|
||||||
|
if (level == 0) {
|
||||||
|
all += "普通 " + card;
|
||||||
|
ten_normal++;
|
||||||
|
} else if (level == 1) {
|
||||||
|
all += "★稀有 " + card;
|
||||||
|
ten_rare++;
|
||||||
|
} else {
|
||||||
|
all += "★★★超稀有 " + card;
|
||||||
|
ten_super++;
|
||||||
|
}
|
||||||
|
all += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
char summary[80];
|
||||||
|
std::sprintf(summary, "\n--- 本次:普通%d 稀有%d 超稀有%d ---",
|
||||||
|
ten_normal, ten_rare, ten_super);
|
||||||
|
all += summary;
|
||||||
|
|
||||||
|
result_box->labelsize(14);
|
||||||
|
result_box->label(all.c_str());
|
||||||
|
result_box->labelcolor(FL_BLACK);
|
||||||
|
update_stats_display();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset_callback(Fl_Widget *w, void *data) {
|
||||||
|
coins = 500;
|
||||||
|
total_normal = 0;
|
||||||
|
total_rare = 0;
|
||||||
|
total_super = 0;
|
||||||
|
update_coins_display();
|
||||||
|
update_stats_display();
|
||||||
|
result_box->labelsize(20);
|
||||||
|
result_box->label("游戏已重置!\n金币回到 500,统计清零。\n开始抽卡吧!");
|
||||||
|
result_box->labelcolor(FL_BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
void quit_callback(Fl_Widget *w, void *data) {
|
||||||
|
window->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::srand(std::time(nullptr));
|
||||||
|
|
||||||
|
window = new Fl_Window(440, 560, "抽卡大冒险");
|
||||||
|
|
||||||
|
Fl_Button *btn1 = new Fl_Button(30, 15, 150, 50, "单抽\n(10金币)");
|
||||||
|
btn1->labelsize(16);
|
||||||
|
btn1->callback(draw_one_callback);
|
||||||
|
|
||||||
|
Fl_Button *btn10 = new Fl_Button(260, 15, 150, 50, "十连抽\n(100金币)");
|
||||||
|
btn10->labelsize(16);
|
||||||
|
btn10->callback(draw_ten_callback);
|
||||||
|
|
||||||
|
coins_box = new Fl_Box(30, 75, 380, 30, "");
|
||||||
|
coins_box->labelsize(20);
|
||||||
|
coins_box->labelfont(FL_BOLD);
|
||||||
|
update_coins_display();
|
||||||
|
|
||||||
|
result_box = new Fl_Box(20, 115, 400, 270, "欢迎来到抽卡大冒险!\n点击上方按钮开始抽卡~");
|
||||||
|
result_box->labelsize(20);
|
||||||
|
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
|
||||||
|
result_box->box(FL_DOWN_BOX);
|
||||||
|
|
||||||
|
stats_box = new Fl_Box(20, 400, 400, 30, "");
|
||||||
|
stats_box->labelsize(14);
|
||||||
|
update_stats_display();
|
||||||
|
|
||||||
|
Fl_Button *btn_reset = new Fl_Button(100, 450, 100, 40, "重置");
|
||||||
|
btn_reset->callback(reset_callback);
|
||||||
|
|
||||||
|
Fl_Button *btn_quit = new Fl_Button(240, 450, 100, 40, "退出");
|
||||||
|
btn_quit->callback(quit_callback);
|
||||||
|
|
||||||
|
window->end();
|
||||||
|
window->show();
|
||||||
|
return Fl::run();
|
||||||
|
}
|
||||||
+16
-17
@@ -14,15 +14,15 @@
|
|||||||
| `进行中` | 正在学,还没认为"这一课学完了"。 |
|
| `进行中` | 正在学,还没认为"这一课学完了"。 |
|
||||||
| `已完成` | 这一课的内容已学完,理解主要目标。 |
|
| `已完成` | 这一课的内容已学完,理解主要目标。 |
|
||||||
|
|
||||||
### 课堂练习
|
### 课堂练习(教程里的"玩一玩"环节)
|
||||||
|
|
||||||
| 填写值 | 含义 |
|
| 填写值 | 含义 |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| `未完成` | 文档里的"课堂小练习"还没做完。 |
|
| `未完成` | 文档里的"玩一玩"还没都试过。 |
|
||||||
| `已完成` | 课堂小练习已做完。 |
|
| `已完成` | "玩一玩"里的小修改都试过。 |
|
||||||
| `无` | 该课没有单独列课堂练习。 |
|
| `无` | 该课没有列出"玩一玩"。 |
|
||||||
|
|
||||||
### 课后作业
|
### 课后作业(教程里的"课后挑战"环节)
|
||||||
|
|
||||||
| 填写值 | 含义 |
|
| 填写值 | 含义 |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
@@ -61,17 +61,16 @@
|
|||||||
|
|
||||||
| 文件 | 用途 |
|
| 文件 | 用途 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `教程-FLTK抽卡游戏-GUI-6课.md` | 6 课总目录与概览 |
|
| `教程-FLTK抽卡游戏-GUI-6课.md` | 6 课总目录、环境安装、编译方法 |
|
||||||
| `第01课-GUI你好窗口.md` | 第 1 课:Fl_Window、Fl_Box、Fl::run() |
|
| `第01课-GUI你好窗口.md` | 第 1 课:`Fl_Window`、`Fl_Box`、`Fl::run()` |
|
||||||
| `第02课-按钮与回调.md` | 第 2 课:Fl_Button、callback、全局指针 |
|
| `第02课-按钮与回调.md` | 第 2 课:`Fl_Button`、`callback`、全局指针 |
|
||||||
| `第03课-随机抽卡.md` | 第 3 课:随机数+IF/ELSE 进 GUI |
|
| `第03课-随机抽卡.md` | 第 3 课:随机数 + `if/else` 进 GUI |
|
||||||
| `第04课-十连抽.md` | 第 4 课:for 循环 + 字符串拼接 |
|
| `第04课-十连抽.md` | 第 4 课:`for` 循环 + 字符串拼接 |
|
||||||
| `第05课-卡片名字与布局.md` | 第 5 课:数组 + 卡片名 + 布局 |
|
| `第05课-卡片名字与布局.md` | 第 5 课:数组 + 卡片名 + 布局 |
|
||||||
| `第06课-完整抽卡游戏.md` | 第 6 课:综合实战项目 |
|
| `第06课-完整抽卡游戏.md` | 第 6 课:综合实战项目 |
|
||||||
| `lesson_gui_1.cpp` | 第 1 课代码 |
|
| `lesson_gui_1.cpp` ~ `lesson_gui_6.cpp` | 每课的**起步骨架**,自己一步步往里加代码 |
|
||||||
| `lesson_gui_2.cpp` | 第 2 课代码 |
|
| `solutions/lesson_gui_1.cpp` ~ `lesson_gui_6.cpp` | 每课的**完整答案**,卡住时再看 |
|
||||||
| `lesson_gui_3.cpp` | 第 3 课代码 |
|
| `CMakeLists.txt` | 一份通用 CMake,用 `-DLESSON=N` 切课 |
|
||||||
| `lesson_gui_4.cpp` | 第 4 课代码 |
|
| `.gitignore` | 排除 `build/` 等中间产物 |
|
||||||
| `lesson_gui_5.cpp` | 第 5 课代码 |
|
|
||||||
| `lesson_gui_6.cpp` | 第 6 课代码 |
|
> 💡 **不需要**为每课复制 CMakeLists 了——直接 `cmake -B build -DLESSON=N` 切课次。
|
||||||
| `CMakeLists_lesson1.txt` | 编译模板(每课改名用) |
|
|
||||||
|
|||||||
+76
-36
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
每课约 **30~45 分钟**,建议家长陪同。
|
每课约 **30~45 分钟**,建议家长陪同。
|
||||||
|
|
||||||
|
> 📌 **新版本教程的最大变化**:每课不再"一次给完整源码让你抄",而是**分成 3~5 个小步骤**,每步加几行代码、立刻编译看效果。卡住时再去看 `solutions/` 里的完整答案。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 前置条件(给家长)
|
## 前置条件(给家长)
|
||||||
@@ -18,60 +20,98 @@
|
|||||||
- `void` 函数
|
- `void` 函数
|
||||||
- `while` 循环与 `break`
|
- `while` 循环与 `break`
|
||||||
|
|
||||||
### 环境安装
|
---
|
||||||
|
|
||||||
|
## 一次性环境安装
|
||||||
|
|
||||||
|
### Mac(目标平台)
|
||||||
|
|
||||||
**Mac(目标平台)**:
|
|
||||||
```bash
|
```bash
|
||||||
brew install fltk
|
brew install fltk cmake
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows(开发用)**:
|
> 装 `cmake` 是必需的——FLTK 自己不带编译入口。
|
||||||
已通过 vcpkg 安装:`vcpkg install fltk --triplet x64-windows`
|
> 如果 `brew` 卡在 "Auto-updating",先 `export HOMEBREW_NO_AUTO_UPDATE=1` 再重试。
|
||||||
|
|
||||||
### 编译命令模板
|
### Windows(开发用)
|
||||||
|
|
||||||
每个项目的 `CMakeLists.txt` 模板(保存为项目文件夹下):
|
1. 装 Visual Studio(带 C++ 工作负载)和 CMake。
|
||||||
```cmake
|
2. 装 vcpkg:`git clone https://github.com/microsoft/vcpkg && .\vcpkg\bootstrap-vcpkg.bat`
|
||||||
cmake_minimum_required(VERSION 3.14)
|
3. 装 FLTK:`vcpkg install fltk --triplet x64-windows`
|
||||||
project(gacha_lesson LANGUAGES CXX)
|
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
---
|
||||||
|
|
||||||
# Windows MSVC 中文支持
|
## 仓库结构
|
||||||
if(MSVC)
|
|
||||||
add_compile_options("/utf-8")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
find_package(FLTK CONFIG REQUIRED)
|
```
|
||||||
|
chouka/
|
||||||
add_executable(gacha main.cpp)
|
├── CMakeLists.txt ← 一份通用 CMake,按 -DLESSON=N 切课
|
||||||
target_link_libraries(gacha PRIVATE fltk)
|
├── lesson_gui_1.cpp ~ 6.cpp ← 每课的「起步骨架」(孩子动手填的)
|
||||||
|
├── solutions/
|
||||||
|
│ └── lesson_gui_1.cpp ~ 6.cpp ← 老师的完整答案(卡住再看)
|
||||||
|
├── 第01课-... ~ 第06课-....md ← 6 课文档
|
||||||
|
├── 教程-FLTK抽卡游戏-GUI-6课.md ← 本文件(总目录)
|
||||||
|
└── 学习进度与状态-GUI.md ← 进度追踪
|
||||||
```
|
```
|
||||||
|
|
||||||
**Mac 编译**:
|
---
|
||||||
|
|
||||||
|
## 编译运行(每课都一样)
|
||||||
|
|
||||||
|
**Mac / Linux:**
|
||||||
```bash
|
```bash
|
||||||
cmake -B build && cmake --build build && ./build/gacha
|
cmake -B build -DLESSON=1 # 第一次配置;切课就改这个 1
|
||||||
|
cmake --build build
|
||||||
|
./build/gacha1
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows 编译**(需指定 vcpkg toolchain):
|
**Windows(vcpkg):**
|
||||||
```bash
|
```bash
|
||||||
cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake && cmake --build build --config Release && build\Release\gacha.exe
|
cmake -B build -DLESSON=1 -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
|
||||||
|
cmake --build build --config Release
|
||||||
|
build\Release\gacha1.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
之后改了 `.cpp` 想再运行:
|
||||||
|
```bash
|
||||||
|
cmake --build build && ./build/gacha1 # Mac/Linux
|
||||||
|
cmake --build build --config Release # Windows,然后 build\Release\gacha1.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 看老师写好的完整答案
|
||||||
|
|
||||||
|
加 `-DUSE_SOLUTION=ON` 重新配置,会从 `solutions/` 编译:
|
||||||
|
```bash
|
||||||
|
cmake -B build -DLESSON=1 -DUSE_SOLUTION=ON
|
||||||
|
cmake --build build && ./build/gacha1
|
||||||
|
```
|
||||||
|
|
||||||
|
> 💡 切换课次或切换骨架/答案后,**必须重新跑一次 `cmake -B build -D...`**,否则会继续编译上次的源文件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关于 emoji(🎴💰🎉 等)
|
||||||
|
|
||||||
|
教程里偶尔用了 emoji 让按钮更可爱。如果你看到的是空白方块(FLTK 默认字体不一定带 emoji),把它换成普通文字就行,例如:
|
||||||
|
- `"🎴 抽一张!"` → `"[抽一张]"`
|
||||||
|
- `"💰"` → `"$"`
|
||||||
|
|
||||||
|
效果不变,只是外观区别。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 总说明
|
## 总说明
|
||||||
|
|
||||||
- **原则**:每课都能跑出一个可见的窗口,逐步变好玩
|
- **原则**:每课都能跑出一个**看得见**的进步——从空窗口到完整游戏,每一步都能编译运行。
|
||||||
- **抽卡**:用随机数模拟概率,不涉及真实金钱
|
- **抽卡**:用随机数模拟概率,不涉及真实金钱。
|
||||||
- **FLTK 说明**:FLTK 是一个轻量 GUI 库,不需要安装庞大框架,编译出的程序只有几 MB
|
- **FLTK**:一个轻量 GUI 库,编译出的程序只有几 MB。
|
||||||
- **跨平台**:同一份代码在 Windows / Mac / Linux 都能编译运行
|
- **跨平台**:同一份代码在 Windows / Mac / Linux 都能编译运行。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 第 1 课:GUI 你好窗口
|
## 第 1 课:GUI 你好窗口
|
||||||
|
|
||||||
**目标**:创建第一个有窗口的程序,屏幕上出现欢迎语。
|
**目标**:自己一步步写出第一个 GUI 程序——一个窗口、一行欢迎语。
|
||||||
|
|
||||||
**FLTK 新概念**:`Fl_Window`(窗口)、`Fl_Box`(文字标签)、`Fl::run()`(事件循环)
|
**FLTK 新概念**:`Fl_Window`(窗口)、`Fl_Box`(文字标签)、`Fl::run()`(事件循环)
|
||||||
|
|
||||||
@@ -83,11 +123,11 @@ cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
|
|||||||
|
|
||||||
## 第 2 课:按钮与回调
|
## 第 2 课:按钮与回调
|
||||||
|
|
||||||
**目标**:放一个按钮,点击后文字会变化——理解"事件驱动"。
|
**目标**:放一个按钮,点击后文字会变——理解"事件驱动"。
|
||||||
|
|
||||||
**FLTK 新概念**:`Fl_Button`(按钮)、`callback`(回调函数)、全局指针
|
**FLTK 新概念**:`Fl_Button`(按钮)、`callback`(回调函数)、全局指针
|
||||||
|
|
||||||
**核心思想**:和命令行不同,GUI 程序"等"用户操作,而不是按顺序执行
|
**核心思想**:和命令行不同,GUI 程序"等"用户操作,而不是按顺序执行。
|
||||||
|
|
||||||
详见:`第02课-按钮与回调.md`
|
详见:`第02课-按钮与回调.md`
|
||||||
|
|
||||||
@@ -99,7 +139,7 @@ cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
|
|||||||
|
|
||||||
**复用旧知识**:`rand()` / `srand()`、`if` / `else if` / `else`
|
**复用旧知识**:`rand()` / `srand()`、`if` / `else if` / `else`
|
||||||
|
|
||||||
**FLTK 新概念**:在回调里更新控件、`label()` 方法
|
**FLTK 新概念**:在回调里更新控件、`label()` 方法、`sprintf` 拼字符串
|
||||||
|
|
||||||
详见:`第03课-随机抽卡.md`
|
详见:`第03课-随机抽卡.md`
|
||||||
|
|
||||||
@@ -111,7 +151,7 @@ cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
|
|||||||
|
|
||||||
**复用旧知识**:`for` 循环、`std::string` 拼接
|
**复用旧知识**:`for` 循环、`std::string` 拼接
|
||||||
|
|
||||||
**FLTK 新概念**:多行显示(`\n` 换行)
|
**FLTK 新概念**:多行显示(`\n` 换行)、文字对齐
|
||||||
|
|
||||||
详见:`第04课-十连抽.md`
|
详见:`第04课-十连抽.md`
|
||||||
|
|
||||||
@@ -123,7 +163,7 @@ cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
|
|||||||
|
|
||||||
**复用旧知识**:`std::string` 数组、下标
|
**复用旧知识**:`std::string` 数组、下标
|
||||||
|
|
||||||
**FLTK 新概念**:控件位置计算
|
**FLTK 新概念**:控件位置计算、`box()` 与 `color()`
|
||||||
|
|
||||||
详见:`第05课-卡片名字与布局.md`
|
详见:`第05课-卡片名字与布局.md`
|
||||||
|
|
||||||
@@ -135,7 +175,7 @@ cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
|
|||||||
|
|
||||||
**复用旧知识**:全部旧知识
|
**复用旧知识**:全部旧知识
|
||||||
|
|
||||||
**FLTK 新概念**:多按钮协作、综合布局
|
**FLTK 新概念**:多按钮协作、综合布局、函数复用
|
||||||
|
|
||||||
详见:`第06课-完整抽卡游戏.md`
|
详见:`第06课-完整抽卡游戏.md`
|
||||||
|
|
||||||
@@ -149,7 +189,7 @@ cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
|
|||||||
| 2 | `Fl_Button`、`callback`、全局指针 | `void` 函数 |
|
| 2 | `Fl_Button`、`callback`、全局指针 | `void` 函数 |
|
||||||
| 3 | `label()` 更新、回调中用随机 | `rand`/`srand`、`if`/`else`、`%` |
|
| 3 | `label()` 更新、回调中用随机 | `rand`/`srand`、`if`/`else`、`%` |
|
||||||
| 4 | 多行 label、字符串拼接 | `for`、`std::string`、`+=` |
|
| 4 | 多行 label、字符串拼接 | `for`、`std::string`、`+=` |
|
||||||
| 5 | 控件定位、`Fl_Group` | `string` 数组、下标、`rand()%N` |
|
| 5 | 控件定位、`box()`/`color()` | `string` 数组、下标、`rand()%N` |
|
||||||
| 6 | 多按钮回调、综合布局 | 全部知识综合 |
|
| 6 | 多按钮回调、综合布局 | 全部知识综合 |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -164,4 +204,4 @@ cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*每课详细步骤见对应文档。*
|
*每课详细步骤见对应文档。学完一课,记得在 `学习进度与状态-GUI.md` 里打个勾。*
|
||||||
|
|||||||
+183
-124
@@ -1,143 +1,143 @@
|
|||||||
# 第 1 课:GUI 你好窗口
|
# 第 1 课:GUI 你好窗口
|
||||||
|
|
||||||
**本课目标**:用 FLTK 创建第一个 GUI 程序——一个窗口,里面写着欢迎语。理解 GUI 程序和命令行程序的不同。
|
> **本课目标**:从一个空空的程序开始,**自己一步一步**让屏幕上弹出一个写着 "欢迎来到抽卡世界!" 的窗口。
|
||||||
|
>
|
||||||
**复习**:你会用 `std::cout` 在命令行输出文字。本课把"输出"搬到**窗口**里。
|
> **预计时间**:30~40 分钟
|
||||||
|
>
|
||||||
|
> **学完之后**:你能解释什么是窗口、控件、事件循环,并能凭记忆写出 5~6 行让窗口出现的代码。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 这一课要搞懂什么
|
## 0. 上课前先理理
|
||||||
|
|
||||||
### 命令行 vs GUI
|
### 命令行程序 vs GUI 程序
|
||||||
|
|
||||||
| | 命令行程序 | GUI 程序 |
|
| | 命令行(以前的你) | GUI(现在的你) |
|
||||||
|---|---|---|
|
|---------|-----------------------------|----------------------------------|
|
||||||
| 样子 | 黑底白字 | 有窗口、按钮 |
|
| 长什么样 | 黑底白字一行行 | 有窗口、按钮、彩色文字 |
|
||||||
| 运行方式 | 一口气跑完 | 一直等用户操作 |
|
| 怎么跑 | 一口气从上跑到下,跑完就退出 | 启动后 **一直在等** 你点鼠标 |
|
||||||
| 输出 | `cout` 打印到终端 | 显示在窗口里的控件上 |
|
| 怎么输出 | `std::cout << ...` | 把文字 "贴" 在窗口上的便利贴上 |
|
||||||
| 用户操作 | 键盘输入 | 鼠标点击、键盘 |
|
|
||||||
|
|
||||||
### 本课三个"新朋友"
|
### 这一课会认识 3 个新朋友
|
||||||
|
|
||||||
| FLTK 类 | 比喻(小朋友版) | 作用 |
|
| 名字 | 比喻 | 干什么用 |
|
||||||
|---------|-----------------|------|
|
|---------------|------------------|-------------------------------------|
|
||||||
| `Fl_Window` | 画纸 | 程序的窗口框架 |
|
| `Fl_Window` | **画纸** | 程序的窗口本体 |
|
||||||
| `Fl_Box` | 便利贴 | 在窗口上显示文字 |
|
| `Fl_Box` | **便利贴** | 贴在画纸上的一段文字 |
|
||||||
| `Fl::run()` | 不休息的保安 | 让窗口一直显示,等用户操作 |
|
| `Fl::run()` | **不下班的保安** | 让窗口一直待着,等你操作 |
|
||||||
|
|
||||||
### 你还认识一个老朋友的变身
|
> 💡 还会顺带认识两个 C++ 新写法:`new` 和 `->`。先不用搞懂原理,跟着写就行。
|
||||||
|
|
||||||
- `new`:以前你可能没见过。在 FLTK 里,控件用 `new` 创建。可以简单理解为"变出一个控件来"。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 第一个 GUI 程序
|
## 1. 起步:打开起步骨架,先编译一次
|
||||||
|
|
||||||
保存为 `lesson_gui_1.cpp`:
|
打开 `lesson_gui_1.cpp`。你会看到一个全是 TODO 的骨架。
|
||||||
|
|
||||||
```cpp
|
先在终端里编译一下,确认环境没问题:
|
||||||
#include <FL/Fl.H>
|
|
||||||
#include <FL/Fl_Window.H>
|
|
||||||
#include <FL/Fl_Box.H>
|
|
||||||
|
|
||||||
int main() {
|
**Mac / Linux:**
|
||||||
// 创建窗口:宽 400,高 300,标题"抽卡世界"
|
```bash
|
||||||
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界");
|
cmake -B build -DLESSON=1
|
||||||
|
cmake --build build
|
||||||
// 在窗口上放一个文字标签:位置(50,100),宽300,高100
|
./build/gacha1
|
||||||
Fl_Box *box = new Fl_Box(50, 100, 300, 100, "欢迎来到抽卡世界!");
|
|
||||||
box->labelsize(24); // 字号 24
|
|
||||||
|
|
||||||
// 结束添加控件
|
|
||||||
window->end();
|
|
||||||
// 显示窗口
|
|
||||||
window->show();
|
|
||||||
// 进入事件循环(让窗口一直显示)
|
|
||||||
return Fl::run();
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
配套 `CMakeLists.txt`:
|
**Windows:**
|
||||||
|
```bash
|
||||||
```cmake
|
cmake -B build -DLESSON=1 -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
|
||||||
cmake_minimum_required(VERSION 3.14)
|
cmake --build build --config Release
|
||||||
project(gacha1 LANGUAGES CXX)
|
build\Release\gacha1.exe
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
add_compile_options("/utf-8")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
find_package(FLTK CONFIG REQUIRED)
|
|
||||||
|
|
||||||
add_executable(gacha1 lesson_gui_1.cpp)
|
|
||||||
target_link_libraries(gacha1 PRIVATE fltk)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 💡 第一次 `cmake -B build -DLESSON=1` 之后,**后面每次只要改了 `.cpp`,运行前两行就够**:
|
||||||
|
> ```bash
|
||||||
|
> cmake --build build && ./build/gacha1
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### ✅ 检查点 1
|
||||||
|
- [ ] 编译没报错
|
||||||
|
- [ ] 运行后……什么都没发生?**对的**!程序进了 `main`,立刻就 `return 0` 退出了。我们接下来一步步让它"活"起来。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 代码逐行解释
|
## 2. 步骤 1:变出一个空窗口
|
||||||
|
|
||||||
```cpp
|
### 🤔 先想一下
|
||||||
#include <FL/Fl.H> // FLTK 核心
|
- 一个窗口至少要告诉电脑哪些信息?(提示:长什么样、叫什么)
|
||||||
#include <FL/Fl_Window.H> // 窗口
|
- 程序跑完 `main` 就结束了——那窗口怎么能"一直待着"呢?
|
||||||
#include <FL/Fl_Box.H> // 文字标签
|
|
||||||
```
|
|
||||||
|
|
||||||
以前的 `#include <iostream>` 是请命令行工具,这三个 `#include <FL/...>` 是请窗口工具。
|
### ✏️ 在 `main` 里加这 3 件事
|
||||||
|
|
||||||
|
**(1) 创建窗口对象**:替换掉 `// TODO(步骤 1):在这里创建一个窗口对象 window` 那一行:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界");
|
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界");
|
||||||
```
|
```
|
||||||
|
|
||||||
- `Fl_Window` = 窗口
|
读法:用 `new` "变出" 一个 `Fl_Window`,宽 400、高 300,标题叫 "抽卡世界"。`window` 是它的名字。
|
||||||
- `400, 300` = 宽 400 像素,高 300 像素
|
|
||||||
- `"抽卡世界"` = 窗口标题栏上显示的字
|
**(2) 显示窗口**:替换掉 `// TODO(步骤 1):告诉窗口"控件加完了"…` 那一行:
|
||||||
- `new` = 创建一个窗口(和以前 `int x = 5` 不同,窗口是"对象")
|
|
||||||
- `*` = 指针(先记:FLTK 创建的控件都用 `*`)
|
```cpp
|
||||||
|
window->end();
|
||||||
|
window->show();
|
||||||
|
```
|
||||||
|
|
||||||
|
读法:`->` 念 "的"。`window` 的 `end()` 表示"控件加完啦",`window` 的 `show()` 是"露面"。
|
||||||
|
|
||||||
|
**(3) 让程序"待住"不退出**:把最后的 `return 0;` 改成:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
return Fl::run();
|
||||||
|
```
|
||||||
|
|
||||||
|
`Fl::run()` 就是那个不下班的保安——它会卡在这里,直到你关闭窗口才返回。
|
||||||
|
|
||||||
|
### ▶️ 编译运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake --build build && ./build/gacha1
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ 检查点 2
|
||||||
|
- [ ] 屏幕上出现一个 400×300 的空白窗口
|
||||||
|
- [ ] 窗口标题栏写着 "抽卡世界"
|
||||||
|
- [ ] 关闭窗口,程序退出,终端回到提示符
|
||||||
|
|
||||||
|
🎉 你的第一个 GUI 程序!但里面空空的,下一步贴张便利贴上去。
|
||||||
|
|
||||||
|
> ❓ 如果没出来:是不是忘了把 `return 0;` 改成 `return Fl::run();`?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 步骤 2:在窗口里贴一张便利贴
|
||||||
|
|
||||||
|
### 🤔 先想一下
|
||||||
|
- 贴在窗口里的文字,应该放在窗口的哪个位置?怎么告诉电脑这个位置?
|
||||||
|
- 文字框本身也是一个"东西",那它要不要 `new` 出来?
|
||||||
|
|
||||||
|
### ✏️ 改两个地方
|
||||||
|
|
||||||
|
**(1) 顶上的 include 区**:把 `// TODO(步骤 2)`:还需要再 `#include 一个头文件` 这行替换成:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
```
|
||||||
|
|
||||||
|
便利贴在 FLTK 里叫 `Fl_Box`,要先告诉编译器去哪儿找。
|
||||||
|
|
||||||
|
**(2) 在 `window->end();` **之前**插入这一行**(替换掉对应的 TODO):
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
Fl_Box *box = new Fl_Box(50, 100, 300, 100, "欢迎来到抽卡世界!");
|
Fl_Box *box = new Fl_Box(50, 100, 300, 100, "欢迎来到抽卡世界!");
|
||||||
```
|
```
|
||||||
|
|
||||||
- `50, 100` = 文字在窗口中的位置:从左边数 50 像素,从上边数 100 像素
|
读法:变出一个 `Fl_Box`,位置 (50, 100),宽 300、高 100,文字是 "欢迎来到抽卡世界!"。
|
||||||
- `300, 100` = 文字区域的宽 300,高 100
|
|
||||||
- 最后一个参数 = 显示的文字内容
|
|
||||||
|
|
||||||
```cpp
|
### 📐 坐标到底怎么数?
|
||||||
box->labelsize(24);
|
|
||||||
```
|
|
||||||
|
|
||||||
`->` 读作"的":`box` 的 `labelsize` 设为 24(字号)。
|
FLTK 的坐标系——和你以前数学课上的不一样,Y 是 **向下** 增大的:
|
||||||
|
|
||||||
```cpp
|
|
||||||
window->end(); // 告诉窗口:控件加完了
|
|
||||||
window->show(); // 让窗口显示出来
|
|
||||||
return Fl::run(); // 进入"等待模式",窗口一直显示直到用户关闭
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 编译和运行
|
|
||||||
|
|
||||||
**Mac**:
|
|
||||||
```bash
|
|
||||||
cmake -B build && cmake --build build && ./build/gacha1
|
|
||||||
```
|
|
||||||
|
|
||||||
**Windows**(VS + vcpkg):
|
|
||||||
```bash
|
|
||||||
cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake && cmake --build build --config Release && build\Release\gacha1.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
运行后应看到一个白色窗口,中间写着"欢迎来到抽卡世界!"。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 坐标系统
|
|
||||||
|
|
||||||
FLTK 窗口的坐标系:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
(0,0) ──────────── X →
|
(0,0) ──────────── X →
|
||||||
@@ -145,7 +145,7 @@ FLTK 窗口的坐标系:
|
|||||||
│ (50,100)
|
│ (50,100)
|
||||||
│ ↓
|
│ ↓
|
||||||
│ ┌──────────┐
|
│ ┌──────────┐
|
||||||
│ │ 文字区域 │ 宽 300
|
│ │ 便利贴 │ 宽 300
|
||||||
│ │ │ 高 100
|
│ │ │ 高 100
|
||||||
│ └──────────┘
|
│ └──────────┘
|
||||||
Y
|
Y
|
||||||
@@ -154,41 +154,100 @@ FLTK 窗口的坐标系:
|
|||||||
|
|
||||||
- X 从左到右增大
|
- X 从左到右增大
|
||||||
- Y 从上到下增大
|
- Y 从上到下增大
|
||||||
- 原点 (0,0) 在左上角
|
- 原点 (0,0) 在窗口左上角
|
||||||
|
|
||||||
|
### ▶️ 编译运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake --build build && ./build/gacha1
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ 检查点 3
|
||||||
|
- [ ] 窗口里出现了 "欢迎来到抽卡世界!"
|
||||||
|
- [ ] 文字大概在窗口中间偏上的位置
|
||||||
|
|
||||||
|
> ❓ 文字看起来很小?没事,下一步搞定字号。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 本课课堂小练习
|
## 4. 步骤 3:让字大起来
|
||||||
|
|
||||||
1. 把窗口标题 `"抽卡世界"` 改成你自己起的名字。
|
### ✏️ 在 `new Fl_Box(...)` 那行的**下一行**加:
|
||||||
2. 把欢迎语改成你喜欢的句子。
|
|
||||||
3. 试试把窗口改大(比如 600×400),把 `Box` 的位置也调整一下,让文字居中。
|
```cpp
|
||||||
|
box->labelsize(24);
|
||||||
|
```
|
||||||
|
|
||||||
|
意思是:`box` 的 `labelsize`(标签字号)设为 24。
|
||||||
|
|
||||||
|
### ▶️ 编译运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake --build build && ./build/gacha1
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ 检查点 4
|
||||||
|
- [ ] 文字明显变大了
|
||||||
|
- [ ] 整个窗口看起来终于像个迎宾画面了
|
||||||
|
|
||||||
|
🎉 **第 1 课通关!** 你刚刚自己写出了一个完整 GUI 程序。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 本课课后作业(选做)
|
## 5. 玩一玩:改一改看会发生什么
|
||||||
|
|
||||||
- 把 `labelsize(24)` 改成 36、12、8,看文字大小变化。试试极端值,看会发生什么。
|
每改一个,记得 **保存 → 编译 → 运行**:
|
||||||
- 在窗口上放**两个** `Fl_Box`,一个在上半部分,一个在下半部分,显示不同的文字。
|
|
||||||
- 思考题:和以前 `std::cout` 输出文字相比,GUI 方式有什么不同?(一个提示:GUI 的文字可以"一直显示",不用 `\n` 刷屏。)
|
1. 把 `400, 300` 改成 `600, 400`。窗口变大了吗?
|
||||||
|
2. 把 `labelsize(24)` 改成 `36`、`12`、`80`。极端值会怎样?
|
||||||
|
3. 把 `(50, 100, 300, 100)` 里的 `50` 改成 `0`。文字跑哪儿去了?
|
||||||
|
4. 把窗口标题 `"抽卡世界"` 改成你自己起的名字。
|
||||||
|
5. **思考题**:怎么让文字 **正好居中**?窗口宽 400,文字框宽 300——X 应该填几?
|
||||||
|
|
||||||
|
> 💡 想到答案再往下看:宽 400 减宽 300 等于 100,两边各留 50,所以 X = 50。本课的代码刚好就是居中的。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 常见小问题
|
## 6. 课后挑战(选做)
|
||||||
|
|
||||||
- **窗口一闪就消失**:检查有没有 `return Fl::run();`,不是 `return 0;`。
|
完成以上后还想多玩?挑一个:
|
||||||
- **编译报错找不到 FL**:检查 `CMakeLists.txt` 里的 `find_package(FLTK CONFIG REQUIRED)` 是否正确,以及 FLTK 是否已安装。
|
|
||||||
- **中文乱码**:确保源文件保存为 UTF-8 编码。Mac clang++ 一般默认支持。
|
- **A. 双便利贴**:在窗口的上、下半部分各放一个 `Fl_Box`,写不同的话。
|
||||||
- **窗口出来了但是空的**:检查 `window->show()` 和 `window->end()` 的顺序。`show()` 必须在 `end()` 之后(本课代码顺序已正确)。
|
- **B. 给画纸刷色**:在 `window->end()` 前面加 `window->color(fl_rgb_color(255, 240, 200));`,看看效果。(需要 `#include <FL/fl_draw.H>` 或加在已有头文件里编译试一下,错了就回滚——这就是学编程)
|
||||||
|
- **C. 思考题**:以前 `std::cout` 一打印就显示,再换一行又能继续打印。GUI 的 `Fl_Box` 行为有什么不一样?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 卡住了?常见小问题
|
||||||
|
|
||||||
|
| 现象 | 多半是 |
|
||||||
|
|------|-------|
|
||||||
|
| 窗口闪一下就消失 | `return 0;` 没改成 `return Fl::run();` |
|
||||||
|
| 编译报 "找不到 `Fl_Box`" | 忘了 `#include <FL/Fl_Box.H>` |
|
||||||
|
| 编译报 "找不到 `FL/Fl.H`" | FLTK 没装好;Mac 跑一下 `brew install fltk cmake` |
|
||||||
|
| 窗口出来但是全黑/全空白 | `window->show()` 是不是写在 `window->end()` 前面了?顺序反了 |
|
||||||
|
| 中文变方框 | 文件没存成 UTF-8 编码 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 偷看老师答案
|
||||||
|
|
||||||
|
写不出来、或者想对一下?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DLESSON=1 -DUSE_SOLUTION=ON
|
||||||
|
cmake --build build && ./build/gacha1
|
||||||
|
```
|
||||||
|
|
||||||
|
源文件路径:`solutions/lesson_gui_1.cpp`。
|
||||||
|
**但请尽量自己先动手——卡住 5 分钟以上再看**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 下一课预告
|
## 9. 下一课预告
|
||||||
|
|
||||||
第 2 课学**按钮和回调**:在窗口上放一个"抽卡"按钮,点一下,文字会变——从"只看不动"到"能交互"。
|
第 2 课:加一个 **按钮**,点一下让窗口里的字变样子。你将第一次见到"用户操作 → 程序响应"的 GUI 核心套路:**回调函数**。
|
||||||
|
|
||||||
---
|
> 📝 学完记得在 `学习进度与状态-GUI.md` 里把第 1 课打勾。
|
||||||
|
|
||||||
*学完请在 `学习进度与状态-GUI.md` 里更新第 1 课的状态。*
|
**下一课**:`第02课-按钮与回调.md`
|
||||||
|
|
||||||
**下一课文档**:`第02课-按钮与回调.md`
|
|
||||||
|
|||||||
+162
-148
@@ -1,222 +1,236 @@
|
|||||||
# 第 2 课:按钮与回调
|
# 第 2 课:按钮与回调
|
||||||
|
|
||||||
**本课目标**:在窗口上放一个按钮,点击按钮后文字发生变化——学会"用户点击 → 程序响应"。
|
> **本课目标**:在窗口里放一个按钮,**点一下文字会变**。理解 GUI 程序最核心的套路:**用户操作 → 程序响应**。
|
||||||
|
>
|
||||||
**复习**:第 1 课你做出了一个静态窗口。本课让它"能动"。
|
> **预计时间**: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
|
```bash
|
||||||
// ❌ 这样不行!
|
cmake -B build -DLESSON=2 # 切到第 2 课
|
||||||
void button_clicked() {
|
cmake --build build && ./build/gacha2
|
||||||
box->label("抽卡了!"); // 这个函数里不认识 box!
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
Fl_Box *box = new Fl_Box(50, 100, 300, 100, "还没抽");
|
|
||||||
// box 只在 main 里有效,button_clicked 函数看不到它
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**解决方案**:把 `box` 声明在 `main` 外面——成为"全局变量",到处都能看到。
|
### ✅ 检查点 1
|
||||||
|
- [ ] 看到一个 400×300 的空窗口,标题 "抽卡世界 - 第2课"
|
||||||
```cpp
|
- [ ] 窗口里什么都没有(按钮和文字都还没加)
|
||||||
// ✅ 这样就行!
|
|
||||||
Fl_Box *box; // 全局声明
|
|
||||||
|
|
||||||
void button_clicked(Fl_Widget *, void *) {
|
|
||||||
box->label("抽卡了!"); // 全局 box,函数里也能用
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> 小提示:`(Fl_Widget *, void *)` 是回调要的固定格式,本课先记住写法,不用深究。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 完整示例程序
|
## 2. 步骤 1:先放一个按钮(点了没反应也没关系)
|
||||||
|
|
||||||
保存为 `lesson_gui_2.cpp`:
|
### 🤔 先想一下
|
||||||
|
- 按钮和窗口、便利贴一样都是控件——它要不要 `new`?
|
||||||
|
- 按钮和便利贴比,参数列表多了什么?少了什么?
|
||||||
|
|
||||||
|
### ✏️ 改两个地方
|
||||||
|
|
||||||
|
**(1) 顶上加 include**(替换对应 TODO):
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
#include <FL/Fl.H>
|
|
||||||
#include <FL/Fl_Window.H>
|
|
||||||
#include <FL/Fl_Button.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
|
```cpp
|
||||||
cmake_minimum_required(VERSION 3.14)
|
Fl_Button *btn = new Fl_Button(130, 40, 140, 50, "抽一张!");
|
||||||
project(gacha2 LANGUAGES CXX)
|
btn->labelsize(18);
|
||||||
|
|
||||||
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)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```cpp
|
||||||
Fl_Box *result_box;
|
Fl_Box *result_box;
|
||||||
```
|
```
|
||||||
放在所有函数外面 → 全局 → 回调函数和 main 都能用。
|
|
||||||
|
|
||||||
### 回调函数
|
**(2) 在 `main` 之外也写一个回调函数**(替换对应 TODO):
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void draw_callback(Fl_Widget *w, void *data) {
|
void draw_callback(Fl_Widget *w, void *data) {
|
||||||
result_box->label("你点击了抽卡按钮!");
|
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
|
```cpp
|
||||||
Fl_Button *btn = new Fl_Button(130, 40, 140, 50, "🎴 抽一张!");
|
|
||||||
btn->callback(draw_callback);
|
btn->callback(draw_callback);
|
||||||
```
|
```
|
||||||
- 位置 (130, 40),宽 140,高 50
|
|
||||||
- `callback(draw_callback)` —— 告诉按钮:被按时执行 `draw_callback`
|
|
||||||
|
|
||||||
### labelcolor
|
读法:把 `draw_callback` 这本"说明书"交给按钮,按它的时候就照着做。
|
||||||
```cpp
|
|
||||||
result_box->labelcolor(FL_RED);
|
### ▶️ 编译运行
|
||||||
|
|
||||||
|
```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. **放两个按钮**:一个"抽卡",一个"重置"。点"重置"让文字变回初始状态。
|
- **A. 双按钮**:再加一个按钮 "重置",点了让 `result_box` 恢复成 "请点击按钮抽卡~",颜色变回黑色 (`FL_BLACK`)。
|
||||||
- 提示:需要两个回调函数(或一个回调 + `data` 参数区分)
|
- 提示:需要再写一个回调函数,比如 `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()` 那一行有没有写,函数名有没有拼错。
|
```bash
|
||||||
- **label 变了但颜色没变**:检查 `labelcolor()` 的拼写。
|
cmake -B build -DLESSON=2 -DUSE_SOLUTION=ON
|
||||||
- **`result_box` 未声明**:检查是否在 `main` 外面写了 `Fl_Box *result_box;`。
|
cmake --build build && ./build/gacha2
|
||||||
- **回调函数的参数**:`(Fl_Widget *w, void *data)` 这个写法不能省略参数;即使不用,也要写上。
|
```
|
||||||
|
|
||||||
|
源文件路径:`solutions/lesson_gui_2.cpp`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 下一课预告
|
## 9. 下一课预告
|
||||||
|
|
||||||
第 3 课把**随机数**加进来:点击按钮 → 随机出普通 / 稀有 / 超稀有——这才是真正的抽卡!
|
第 3 课把**随机数**拉进来:点按钮 → 随机出"普通卡 / 稀有卡 / 超稀有卡",**不同稀有度配不同颜色**。这才是真正的抽卡。
|
||||||
|
|
||||||
---
|
> 📝 学完记得在 `学习进度与状态-GUI.md` 里把第 2 课打勾。
|
||||||
|
|
||||||
*学完请在 `学习进度与状态-GUI.md` 里更新第 2 课的状态。*
|
**下一课**:`第03课-随机抽卡.md`
|
||||||
|
|
||||||
**下一课文档**:`第03课-随机抽卡.md`
|
|
||||||
|
|||||||
+208
-145
@@ -1,107 +1,147 @@
|
|||||||
# 第 3 课:随机抽卡——核心来了
|
# 第 3 课:随机抽卡——核心来了
|
||||||
|
|
||||||
**本课目标**:点击按钮 → 随机出"普通卡 / 稀有卡 / 超稀有卡",结果显示在窗口里。把命令行抽卡逻辑搬进 GUI。
|
> **本课目标**:把命令行时学过的 `rand()` + `if/else` **搬进 GUI**。点按钮 → 随机出"普通卡 / 稀有卡 / 超稀有卡",不同稀有度配不同颜色。
|
||||||
|
>
|
||||||
**复习**:第 2 课学会了按钮和回调;命令行时期你已掌握 `rand()`、`srand()`、`if/else`。
|
> **预计时间**: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
|
```cpp
|
||||||
#include <FL/Fl.H>
|
std::srand(std::time(nullptr)); // 设一次种子
|
||||||
#include <FL/Fl_Window.H>
|
int r = std::rand() % 100; // 每次生成 0~99 的随机数
|
||||||
#include <FL/Fl_Button.H>
|
|
||||||
#include <FL/Fl_Box.H>
|
|
||||||
#include <cstdlib> // rand, srand
|
|
||||||
#include <ctime> // time
|
|
||||||
|
|
||||||
// 全局变量
|
if (r < 70) { ... 普通 ... }
|
||||||
Fl_Box *result_box;
|
else if (r < 95) { ... 稀有 ... }
|
||||||
Fl_Box *info_box; // 显示本次点数的辅助信息
|
else { ... 超稀有 ... }
|
||||||
|
|
||||||
// 回调函数——每次点击按钮执行
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
这些你以前用 `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
|
```cpp
|
||||||
char info[50];
|
char info[50];
|
||||||
@@ -109,75 +149,98 @@ std::sprintf(info, "本次点数:%d", r);
|
|||||||
info_box->label(info);
|
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
|
```cpp
|
||||||
fl_rgb_color(128, 128, 128) // R=128, G=128, B=128 → 灰色
|
if (r < 70) {
|
||||||
fl_rgb_color(255, 200, 0) // → 金色
|
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. 玩一玩
|
||||||
|
|
||||||
```
|
1. 把概率改成"超稀有更多":`r < 50` 普通,`r < 80` 稀有,其他超稀有。点 20 次试试感受。
|
||||||
┌───────── 抽卡世界 - 第3课 ────────┐
|
2. 把超稀有改成金色:`fl_rgb_color(255, 200, 0)`。
|
||||||
│ │
|
3. 给"超稀有"再加一行祝贺:在那个 `else` 分支里加 `info_box->label("🎉 恭喜抽到超稀有!🎉");`。
|
||||||
│ ┌──────────────┐ │
|
4. **思考题**:如果把 `srand` 那一行从 `main` 删掉,多次运行程序会发生什么?
|
||||||
│ │ 🎴 抽一张! │ │
|
|
||||||
│ └──────────────┘ │
|
---
|
||||||
│ │
|
|
||||||
│ ★ 稀有卡 ★ │ ← 蓝色大字
|
## 7. 课后挑战(选做)
|
||||||
│ │
|
|
||||||
│ 本次点数:78 │ ← 小字
|
- **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`。看看稀有和超稀有是不是变多了。
|
第 4 课加一个 "十连抽" 按钮:一次抽 10 张全部显示。你会用上**老朋友 `for` 循环**和**字符串拼接**。
|
||||||
2. 把"超稀有卡"的颜色改成金色 `fl_rgb_color(255, 200, 0)`。
|
|
||||||
3. 给超稀有卡多加一行"🎉恭喜!"——提示:可以改 `info_box` 的内容。
|
|
||||||
|
|
||||||
---
|
> 📝 学完记得在 `学习进度与状态-GUI.md` 里把第 3 课打勾。
|
||||||
|
|
||||||
## 7. 本课课后作业(选做)
|
**下一课**:`第04课-十连抽.md`
|
||||||
|
|
||||||
- 增加第四个稀有度:"传说卡"(`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`
|
|
||||||
|
|||||||
+237
-155
@@ -1,99 +1,223 @@
|
|||||||
# 第 4 课:十连抽
|
# 第 4 课:十连抽
|
||||||
|
|
||||||
**本课目标**:加一个"十连抽"按钮,用 `for` 循环一口气抽 10 次,所有结果一起显示。学会在 GUI 里用循环 + 字符串拼接。
|
> **本课目标**:再加一个 "十连抽" 按钮,用 `for` 循环一口气抽 10 次,全部结果一起显示。学会在 GUI 里用循环 + 字符串拼接。
|
||||||
|
>
|
||||||
**复习**:第 3 课单抽;命令行时期你已掌握 `for` 循环和 `string`。
|
> **预计时间**:40 分钟
|
||||||
|
>
|
||||||
|
> **学完之后**:你能写出 "一个动作做很多次" 的 GUI 回调,并把多行结果拼成一个字符串显示。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 这一课要搞懂什么
|
## 0. 上课前先理理
|
||||||
|
|
||||||
### 命令行 vs GUI 的多抽
|
### 命令行 vs GUI 的多抽
|
||||||
|
|
||||||
**命令行时代(第 6 课)**:
|
**命令行**(你以前的代码):
|
||||||
```cpp
|
```cpp
|
||||||
for (int i = 1; i <= 10; i++) {
|
for (int i = 1; i <= 10; i++) {
|
||||||
int r = rand() % 100;
|
int r = rand() % 100;
|
||||||
cout << "第" << i << "抽:";
|
cout << "第" << i << "抽:" << ... << "\n";
|
||||||
// ... 判断稀有度 ...
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
每轮 `cout` 一行,10 行自动排队显示。
|
每轮 `cout` 一行,10 行自动排队。
|
||||||
|
|
||||||
|
**GUI**:你不能在循环里反复 `result_box->label(...)`——每次 `label` 都会把上一次的内容**完全覆盖**,最后只会显示最后一抽。
|
||||||
|
|
||||||
|
### 💡 GUI 的套路:先攒后贴
|
||||||
|
|
||||||
**GUI 时代(本课)**:
|
|
||||||
```cpp
|
|
||||||
for (int i = 1; i <= 10; i++) {
|
|
||||||
int r = rand() % 100;
|
|
||||||
// 不能直接 "cout" → 要把结果攒起来,最后一次性 label
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
GUi 的 `label()` 每次都会覆盖旧内容。所以需要**先把 10 行拼成一个大字符串,再一次性显示**。
|
循环 10 次:
|
||||||
|
把这一抽的结果用 += 拼到一个大字符串后面
|
||||||
### 核心技巧:字符串拼接
|
循环结束后:
|
||||||
|
把大字符串一次性贴到 label
|
||||||
```cpp
|
|
||||||
std::string result = ""; // 空字符串
|
|
||||||
result += "第1抽:普通\n"; // 追加一行
|
|
||||||
result += "第2抽:稀有\n"; // 再追加
|
|
||||||
// ... 最后 label(result.c_str())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`+=` 像胶水,把一段段文字粘在一起。`\n` 是换行。
|
像写信:先在纸上写完 10 行再寄出,而不是每写一行就寄一封。
|
||||||
|
|
||||||
|
### 这一课认识 2 个新朋友
|
||||||
|
|
||||||
|
| 名字 | 干什么 |
|
||||||
|
|-------------------|---------------------------------------------|
|
||||||
|
| `std::string` 拼接 | 用 `+` 和 `+=` 把多段字符串粘起来 |
|
||||||
|
| `std::to_string` | 把数字转成字符串(拼接时用) |
|
||||||
|
| `align()` | 让多行文字左上对齐(看起来像列表) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 程序结构
|
## 1. 起步:先编译看上一课的状态
|
||||||
|
|
||||||
- 保留"单抽"按钮(第 3 课功能)
|
骨架已经是第 3 课的样子。先编译一次:
|
||||||
- 新增"十连抽"按钮 → 调用新的回调函数 `draw_ten_callback`
|
|
||||||
- 两个按钮共用一个结果框
|
|
||||||
|
|
||||||
## 3. 完整示例程序
|
```bash
|
||||||
|
cmake -B build -DLESSON=4
|
||||||
|
cmake --build build && ./build/gacha4
|
||||||
|
```
|
||||||
|
|
||||||
保存为 `lesson_gui_4.cpp`:
|
### ✅ 检查点 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
|
```cpp
|
||||||
#include <FL/Fl.H>
|
|
||||||
#include <FL/Fl_Window.H>
|
|
||||||
#include <FL/Fl_Button.H>
|
|
||||||
#include <FL/Fl_Box.H>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <ctime>
|
|
||||||
#include <string> // for std::string
|
|
||||||
|
|
||||||
// 全局变量
|
|
||||||
Fl_Box *result_box;
|
|
||||||
Fl_Box *info_box;
|
|
||||||
|
|
||||||
// 单抽回调
|
|
||||||
void draw_one_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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 十连抽回调
|
|
||||||
void draw_ten_callback(Fl_Widget *w, void *data) {
|
void draw_ten_callback(Fl_Widget *w, void *data) {
|
||||||
std::string all = ""; // 攒结果的字符串
|
result_box->label("十连抽按钮通了!");
|
||||||
int rare_count = 0; // 统计稀有数量
|
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;
|
int super_count = 0;
|
||||||
|
|
||||||
for (int i = 1; i <= 10; i++) {
|
for (int i = 1; i <= 10; i++) {
|
||||||
int r = std::rand() % 100;
|
int r = std::rand() % 100;
|
||||||
|
|
||||||
all += "第" + std::to_string(i) + "抽:";
|
all += "第" + std::to_string(i) + "抽:";
|
||||||
|
|
||||||
if (r < 70) {
|
if (r < 70) {
|
||||||
@@ -105,16 +229,14 @@ void draw_ten_callback(Fl_Widget *w, void *data) {
|
|||||||
all += "★★★超稀有★★★";
|
all += "★★★超稀有★★★";
|
||||||
super_count++;
|
super_count++;
|
||||||
}
|
}
|
||||||
|
all += "\n";
|
||||||
all += "\n"; // 换行
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用 sprintf 做统计信息
|
// 在 info_box 上显示统计
|
||||||
char stats[100];
|
char stats[100];
|
||||||
std::sprintf(stats, "十连抽结果 | 稀有:%d 超稀有:%d",
|
std::sprintf(stats, "十连抽结果 | 稀有:%d 超稀有:%d",
|
||||||
rare_count, super_count);
|
rare_count, super_count);
|
||||||
info_box->label(stats);
|
info_box->label(stats);
|
||||||
// 统计信息变颜色:有超稀有时红色,否则蓝色
|
|
||||||
if (super_count > 0) {
|
if (super_count > 0) {
|
||||||
info_box->labelcolor(FL_RED);
|
info_box->labelcolor(FL_RED);
|
||||||
} else if (rare_count > 0) {
|
} else if (rare_count > 0) {
|
||||||
@@ -123,105 +245,65 @@ void draw_ten_callback(Fl_Widget *w, void *data) {
|
|||||||
info_box->labelcolor(fl_rgb_color(128, 128, 128));
|
info_box->labelcolor(fl_rgb_color(128, 128, 128));
|
||||||
}
|
}
|
||||||
|
|
||||||
result_box->labelsize(16); // 十连字多,用小一点的字
|
result_box->labelsize(16);
|
||||||
result_box->label(all.c_str());
|
result_box->label(all.c_str());
|
||||||
result_box->labelcolor(FL_BLACK);
|
result_box->labelcolor(FL_BLACK);
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
int main() {
|
### ▶️ 编译运行
|
||||||
std::srand(std::time(nullptr));
|
|
||||||
|
|
||||||
// 窗口加高,容纳十连抽的内容
|
### ✅ 检查点 5
|
||||||
Fl_Window *window = new Fl_Window(420, 480, "抽卡世界 - 第4课");
|
- [ ] 点十连抽 → 10 行从左上开始整齐排列
|
||||||
|
- [ ] 顶上信息栏出现统计 "十连抽结果 | 稀有:X 超稀有:Y"
|
||||||
|
- [ ] 出超稀有时统计变红色,只出稀有变蓝色,全普通变灰色
|
||||||
|
|
||||||
// 单抽按钮
|
🎉 **第 4 课通关!**
|
||||||
Fl_Button *btn1 = new Fl_Button(50, 20, 140, 50, "🎴 抽一张");
|
|
||||||
btn1->labelsize(18);
|
|
||||||
btn1->callback(draw_one_callback);
|
|
||||||
|
|
||||||
// 十连抽按钮
|
---
|
||||||
Fl_Button *btn10 = new Fl_Button(230, 20, 140, 50, "🎴🎴 十连抽");
|
|
||||||
btn10->labelsize(18);
|
|
||||||
btn10->callback(draw_ten_callback);
|
|
||||||
|
|
||||||
// 信息行
|
## 6. 玩一玩
|
||||||
info_box = new Fl_Box(30, 80, 360, 30, "");
|
|
||||||
info_box->labelsize(14);
|
|
||||||
|
|
||||||
// 结果区域(加大高度)
|
1. 把循环改成 `i <= 5`,变成 "五连抽",再把按钮文字改成 "五连抽"。
|
||||||
result_box = new Fl_Box(30, 120, 360, 330, "点击按钮开始抽卡!");
|
2. 在超稀有那行结果前加 `>>> `,让它一眼显眼:`all += ">>> ★★★超稀有★★★ <<<";`。
|
||||||
result_box->labelsize(20);
|
3. 把单抽点完后清空 `info_box` 的统计——在 `draw_one_callback` 末尾加 `info_box->labelcolor(FL_BLACK);`。
|
||||||
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT); // 左上对齐
|
4. **思考题**:如果不写 `result_box->labelsize(16);`,第 4 步十连结果会怎样?为什么单抽要在自己回调里也 `labelsize(28)`?
|
||||||
|
|
||||||
window->end();
|
---
|
||||||
window->show();
|
|
||||||
return Fl::run();
|
## 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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 新知识点
|
## 10. 下一课预告
|
||||||
|
|
||||||
### `std::to_string(i)` —— 数字变字符串
|
第 5 课给每档稀有度配上**具体的卡片名字**(小火龙、冰霜之狼、雷霆神龙……),用**数组**存。还会把界面分区,整齐好看。
|
||||||
|
|
||||||
```cpp
|
> 📝 学完记得在 `学习进度与状态-GUI.md` 里把第 4 课打勾。
|
||||||
all += "第" + std::to_string(i) + "抽:";
|
|
||||||
```
|
|
||||||
|
|
||||||
`std::to_string(5)` 变成 `"5"`。在旧课程里你用 `cout << i`,GUI 里需要用 `to_string` 把数字变成字符串后才能用 `+` 拼接。
|
**下一课**:`第05课-卡片名字与布局.md`
|
||||||
|
|
||||||
> 需要 `#include <string>`
|
|
||||||
|
|
||||||
### `align()` —— 文字对齐方式
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
|
|
||||||
```
|
|
||||||
|
|
||||||
`FL_ALIGN_TOP` = 靠上,`FL_ALIGN_LEFT` = 靠左,`|` 表示"同时用"。
|
|
||||||
|
|
||||||
### `.c_str()` —— 从 `std::string` 到 C 字符串
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
result_box->label(all.c_str());
|
|
||||||
```
|
|
||||||
|
|
||||||
FLTK 的 `label()` 需要 C 风格字符串(`const char*`),`.c_str()` 把 `std::string` 转换过去。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 本课课堂小练习
|
|
||||||
|
|
||||||
1. 把十连抽改成"五连抽":改循环次数,改按钮文字。
|
|
||||||
2. 在十连抽的结果中,把"超稀有"那行用特殊符号标出来(比如加 `>>>` 或 `!!!`)。
|
|
||||||
3. 加一个"重置"按钮,清空结果显示区。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 本课课后作业(选做)
|
|
||||||
|
|
||||||
- 让单抽和十连抽**共用**一个判断稀有度的代码,不重复写两遍(提示:写一个函数返回稀有度名字,回调里调用它)。这是旧课程第 8 课"函数"的 GUI 版应用!
|
|
||||||
- 让窗口可以调整大小——试试 `window->resizable(window)` 的效果。
|
|
||||||
- 十连抽结果显示不下怎么办?试试把窗口再调大一点,把 `result_box` 也调大。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 常见小问题
|
|
||||||
|
|
||||||
- **十连抽结果只显示最后一行**:检查是否在循环里反复调用了 `result_box->label()`(应该先拼完再统一 label)。
|
|
||||||
- **`to_string` 报错**:需要 `#include <string>`。
|
|
||||||
- **`.c_str()` 不认识的报错**:确认变量是 `std::string` 类型。
|
|
||||||
- **中文在十连结果中乱码**:确认文件保存为 UTF-8。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 下一课预告
|
|
||||||
|
|
||||||
第 5 课给每档稀有度配上**具体的卡片名字**(皮卡丘、喷火龙…),用数组存储。还要学会让界面更整齐漂亮。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*学完请在 `学习进度与状态-GUI.md` 里更新第 4 课的状态。*
|
|
||||||
|
|
||||||
**下一课文档**:`第05课-卡片名字与布局.md`
|
|
||||||
|
|||||||
+215
-171
@@ -1,86 +1,108 @@
|
|||||||
# 第 5 课:卡片名字与布局
|
# 第 5 课:卡片名字与布局
|
||||||
|
|
||||||
**本课目标**:给每种稀有度配上具体的卡片名字(火龙、冰狼、雷霆龙……),让抽卡真正像游戏。学会把界面分区布局,更整齐好看。
|
> **本课目标**:给每种稀有度配上具体的卡片名字(小火龙、冰霜之狼、雷霆神龙……)。再把界面分区,让窗口整齐漂亮。
|
||||||
|
>
|
||||||
**复习**:第 3~4 课只显示稀有度等级;命令行时期你已掌握 `std::string` 数组。
|
> **预计时间**:35~45 分钟
|
||||||
|
>
|
||||||
|
> **学完之后**:你能用数组管理一组"内容",并用 `box()`/`color()` 让窗口有层次感。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 这一课要搞懂什么
|
## 0. 上课前先理理
|
||||||
|
|
||||||
### 从"普通卡"到"小火龙"
|
### 从 "★ 稀有卡 ★" 到 "★ 稀有卡 ★ 冰霜之狼"
|
||||||
|
|
||||||
以前的结果:
|
设计思路两步走:
|
||||||
```
|
|
||||||
★ 稀有卡 ★
|
|
||||||
```
|
|
||||||
|
|
||||||
本课的结果:
|
|
||||||
```
|
|
||||||
★ 稀有卡 ★
|
|
||||||
冰霜之狼
|
|
||||||
```
|
|
||||||
|
|
||||||
**设计思路**:每种稀有度维护一个卡片名字数组,随机抽到某档 → 再随机从该档的卡片里挑一张。
|
|
||||||
|
|
||||||
### 卡片表设计
|
|
||||||
|
|
||||||
| 稀有度 | 卡片名数组 | 数量 |
|
|
||||||
|--------|-----------|------|
|
|
||||||
| 普通 | 小火龙、水跃鱼、绿草龟、小电鼠、土拨仔 | 5 张 |
|
|
||||||
| 稀有 | 烈焰飞龙、冰霜之狼、暗影豹、风暴鹰、水晶鹿 | 5 张 |
|
|
||||||
| 超稀有 | 雷霆神龙、凤凰王、深海巨兽 | 3 张 |
|
|
||||||
|
|
||||||
### 界面分区
|
|
||||||
|
|
||||||
本课把窗口分成三个区域,比之前全部控件堆在一起更好看:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────┐
|
1. 随机决定稀有度(这一步和第 3、4 课一样)
|
||||||
│ 操作区(按钮) │ ← 顶部
|
2. 从这一档的卡片数组里再随机选一张
|
||||||
├─────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ 结果显示区 │ ← 中部大面积
|
|
||||||
│ │
|
|
||||||
├─────────────────────────────┤
|
|
||||||
│ 统计/信息区 │ ← 底部
|
|
||||||
└─────────────────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 卡牌表设计
|
||||||
|
|
||||||
|
| 稀有度 | 卡片名 | 张数 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 普通 | 小火龙、水跃鱼、绿草龟、小电鼠、土拨仔 | 5 |
|
||||||
|
| 稀有 | 烈焰飞龙、冰霜之狼、暗影豹、风暴鹰、水晶鹿 | 5 |
|
||||||
|
| 超稀有 | 雷霆神龙、凤凰王、深海巨兽 | 3 |
|
||||||
|
|
||||||
|
### 界面分区目标
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────┐
|
||||||
|
│ [单抽] [十连抽] │ ← 顶部:操作区
|
||||||
|
├────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ★ 稀有卡 ★ │ ← 中部:结果区(凹陷边框)
|
||||||
|
│ 冰霜之狼 │
|
||||||
|
│ │
|
||||||
|
├────────────────────────────┤
|
||||||
|
│ 单抽:普通 0 稀有 1 超 0 │ ← 底部:统计区(灰色背景)
|
||||||
|
└────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 这一课认识 2 个新朋友
|
||||||
|
|
||||||
|
| 名字 | 干什么 |
|
||||||
|
|---------------|---------------------------------------------|
|
||||||
|
| 多组数组 | 每档稀有度一个数组,下标随机选 |
|
||||||
|
| `box()` | 控件的边框样式(凹陷、凸起、扁平……) |
|
||||||
|
| `color()` | 控件的**背景色**(注意和 `labelcolor` 文字色 区分) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 完整示例程序
|
## 1. 起步:先看现状
|
||||||
|
|
||||||
保存为 `lesson_gui_5.cpp`:
|
打开 `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
|
```cpp
|
||||||
#include <FL/Fl.H>
|
|
||||||
#include <FL/Fl_Window.H>
|
|
||||||
#include <FL/Fl_Button.H>
|
|
||||||
#include <FL/Fl_Box.H>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <ctime>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
// ===== 卡片数据 =====
|
|
||||||
// 普通卡名字
|
|
||||||
std::string normal_cards[5] = {
|
std::string normal_cards[5] = {
|
||||||
"小火龙", "水跃鱼", "绿草龟", "小电鼠", "土拨仔"
|
"小火龙", "水跃鱼", "绿草龟", "小电鼠", "土拨仔"
|
||||||
};
|
};
|
||||||
// 稀有卡名字
|
|
||||||
std::string rare_cards[5] = {
|
std::string rare_cards[5] = {
|
||||||
"烈焰飞龙", "冰霜之狼", "暗影豹", "风暴鹰", "水晶鹿"
|
"烈焰飞龙", "冰霜之狼", "暗影豹", "风暴鹰", "水晶鹿"
|
||||||
};
|
};
|
||||||
// 超稀有卡名字
|
|
||||||
std::string super_cards[3] = {
|
std::string super_cards[3] = {
|
||||||
"雷霆神龙", "凤凰王", "深海巨兽"
|
"雷霆神龙", "凤凰王", "深海巨兽"
|
||||||
};
|
};
|
||||||
|
```
|
||||||
|
|
||||||
// ===== 全局控件 =====
|
### ▶️ 编译运行
|
||||||
Fl_Box *result_box;
|
|
||||||
Fl_Box *stats_box;
|
|
||||||
|
|
||||||
// ===== 单抽回调 =====
|
### ✅ 检查点 2
|
||||||
|
- [ ] 编译没报错(即使还没用到这些数组)
|
||||||
|
- [ ] 程序行为同前
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 步骤 2:让单抽显示具体卡名
|
||||||
|
|
||||||
|
### 🤔 先想一下
|
||||||
|
- "普通卡数组有 5 张,要随机选一张"——表达式是?(答案:`normal_cards[std::rand() % 5]`)
|
||||||
|
- 超稀有数组只有 3 张,对应的取模数是?
|
||||||
|
|
||||||
|
### ✏️ 把 `draw_one_callback` 函数体**整体替换**成:
|
||||||
|
|
||||||
|
```cpp
|
||||||
void draw_one_callback(Fl_Widget *w, void *data) {
|
void draw_one_callback(Fl_Widget *w, void *data) {
|
||||||
int r = std::rand() % 100;
|
int r = std::rand() % 100;
|
||||||
std::string result = "";
|
std::string result = "";
|
||||||
@@ -100,150 +122,172 @@ void draw_one_callback(Fl_Widget *w, void *data) {
|
|||||||
result_box->labelcolor(FL_RED);
|
result_box->labelcolor(FL_RED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拼接:稀有度 + 换行 + 卡片名
|
// 拼成两行:稀有度 + 换行 + 卡片名
|
||||||
std::string display = result + "\n " + card_name + "\n";
|
std::string display = result + "\n " + card_name + "\n";
|
||||||
result_box->labelsize(24);
|
result_box->labelsize(24);
|
||||||
result_box->label(display.c_str());
|
result_box->label(display.c_str());
|
||||||
stats_box->label("单抽模式");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 十连抽回调 =====
|
|
||||||
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 += "普通 " + 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";
|
|
||||||
}
|
|
||||||
|
|
||||||
result_box->labelsize(15);
|
|
||||||
result_box->label(all.c_str());
|
|
||||||
result_box->labelcolor(FL_BLACK);
|
|
||||||
|
|
||||||
char stats[100];
|
|
||||||
std::sprintf(stats, "十连:普通 %d | 稀有 %d | 超稀有 %d",
|
|
||||||
10 - rare_count - super_count, rare_count, super_count);
|
|
||||||
stats_box->label(stats);
|
|
||||||
stats_box->labelcolor(super_count > 0 ? FL_RED : FL_BLUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
std::srand(std::time(nullptr));
|
|
||||||
|
|
||||||
Fl_Window *window = new Fl_Window(420, 520, "抽卡世界 - 第5课");
|
|
||||||
|
|
||||||
// ===== 操作区 =====
|
|
||||||
Fl_Button *btn1 = new Fl_Button(40, 15, 150, 45, "🎴 抽一张");
|
|
||||||
btn1->labelsize(18);
|
|
||||||
btn1->callback(draw_one_callback);
|
|
||||||
|
|
||||||
Fl_Button *btn10 = new Fl_Button(230, 15, 150, 45, "🎴🎴 十连抽");
|
|
||||||
btn10->labelsize(18);
|
|
||||||
btn10->callback(draw_ten_callback);
|
|
||||||
|
|
||||||
// ===== 结果显示区 =====
|
|
||||||
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)); // 浅灰背景
|
|
||||||
|
|
||||||
window->end();
|
|
||||||
window->show();
|
|
||||||
return Fl::run();
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 注意:暂时把 `info_box->label(...)` 拿掉了,下一步再统一处理统计行。
|
||||||
|
|
||||||
|
### ▶️ 编译运行
|
||||||
|
|
||||||
|
### ✅ 检查点 3
|
||||||
|
- [ ] 点单抽 → 看到两行:稀有度 + 卡片名
|
||||||
|
- [ ] 多点几次:同一档里的卡片名会随机变化
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 新知识点
|
## 4. 步骤 3:让十连抽也显示卡名
|
||||||
|
|
||||||
### 多组卡片数组
|
### ✏️ 把 `draw_ten_callback` 里的 `for` 循环**整体替换**成:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
std::string normal_cards[5] = { "小火龙", "水跃鱼", ... };
|
for (int i = 1; i <= 10; i++) {
|
||||||
std::string rare_cards[5] = { ... };
|
int r = std::rand() % 100;
|
||||||
std::string super_cards[3] = { ... };
|
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";
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- 三组独立数组,每组长度可以不同
|
只是在每个分支末尾把卡名拼了上去。
|
||||||
- 访问:`normal_cards[rand() % 5]` —— 随机选一张普通卡
|
|
||||||
|
|
||||||
### `box()` —— 控件边框样式
|
### ▶️ 编译运行
|
||||||
|
|
||||||
|
### ✅ 检查点 4
|
||||||
|
- [ ] 十连抽 10 行结果,每行都有卡名
|
||||||
|
- [ ] 同一档里卡名随机变化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 步骤 4:分区布局,让界面变好看 ⭐
|
||||||
|
|
||||||
|
现在界面看着像草稿。我们用 `box()` 和 `color()` 给它分区。
|
||||||
|
|
||||||
|
顺手做一件清理:把 `info_box` 改名 `stats_box`(语义更准——它显示的是统计而不是单次信息)。
|
||||||
|
|
||||||
|
### ✏️ 改 4 处
|
||||||
|
|
||||||
|
**(1) 全局变量改名**(替换对应 TODO):
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
result_box->box(FL_DOWN_BOX); // 凹陷效果,像一张卡片
|
Fl_Box *stats_box;
|
||||||
stats_box->box(FL_FLAT_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_NO_BOX` | 没有边框(默认) |
|
||||||
| `FL_FLAT_BOX` | 平面(默认) |
|
| `FL_FLAT_BOX` | 扁平、有背景 |
|
||||||
| `FL_UP_BOX` | 凸起 |
|
| `FL_UP_BOX` | 凸起 |
|
||||||
| `FL_DOWN_BOX` | 凹陷 |
|
| `FL_DOWN_BOX` | 凹陷 |
|
||||||
| `FL_EMBOSSED_BOX` | 浮雕 |
|
| `FL_EMBOSSED_BOX` | 浮雕 |
|
||||||
|
|
||||||
### `color()` —— 控件背景色
|
⚠ **别混淆**:`color()` 是**背景色**,`labelcolor()` 是**文字色**。
|
||||||
|
|
||||||
```cpp
|
### ▶️ 编译运行
|
||||||
stats_box->color(fl_rgb_color(230, 230, 230));
|
|
||||||
|
### ✅ 检查点 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
|
||||||
```
|
```
|
||||||
|
|
||||||
给控件设置背景颜色。注意:`color()` 是背景色,`labelcolor()` 是文字颜色。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 本课课堂小练习
|
## 10. 下一课预告
|
||||||
|
|
||||||
1. 把三组卡片名字全部换成你自己喜欢的(宝可梦、数码宝贝、自创的都行)。
|
第 6 课是**大作业**:把前面所有功能组合起来——加**金币系统**(每抽消耗金币)、**累计统计**、**重置/退出按钮**,做一个真正像样的抽卡游戏。
|
||||||
2. 给超稀有卡多加 2 张,改数组长度和相关 `% N`。
|
|
||||||
3. 试三种不同的 `box()` 样式,看哪个最好看。
|
|
||||||
|
|
||||||
---
|
> 📝 学完记得在 `学习进度与状态-GUI.md` 里把第 5 课打勾。
|
||||||
|
|
||||||
## 5. 本课课后作业(选做)
|
**下一课**:`第06课-完整抽卡游戏.md`
|
||||||
|
|
||||||
- 加一个"查看图鉴"按钮(先用文字列表显示所有卡片,不要求图片)。提示:把所有卡名拼成一个长字符串显示。
|
|
||||||
- 在单抽结果中,如果抽到超稀有,把 `result_box` 的 `box()` 临时改成 `FL_UP_BOX`(凸起效果,突出显示)。
|
|
||||||
- 把窗口背景色改一下:`window->color(fl_rgb_color(245, 245, 255));`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 常见小问题
|
|
||||||
|
|
||||||
- **`normal_cards` 越界**:`% 5` 对应数组长度 5;如果改了数组长度,别忘了改取余的数。
|
|
||||||
- **卡片名全一样**:检查 `rand() % N` 是否写在需要它的地方——每抽一次应该调一次。
|
|
||||||
- **窗口不够大**:加了更多内容后,适当增大窗口和控件的尺寸。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 下一课预告
|
|
||||||
|
|
||||||
第 6 课把前面所有内容整合成一个**完整的抽卡游戏**——多个按钮(单抽/十连/退出)、金币系统、累计统计。这是本教程的大作业。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*学完请在 `学习进度与状态-GUI.md` 里更新第 5 课的状态。*
|
|
||||||
|
|
||||||
**下一课文档**:`第06课-完整抽卡游戏.md`
|
|
||||||
|
|||||||
+377
-230
@@ -1,18 +1,24 @@
|
|||||||
# 第 6 课:完整抽卡游戏
|
# 第 6 课:完整抽卡游戏
|
||||||
|
|
||||||
**本课目标**:把前 5 课全部整合,做出一个完整的抽卡游戏——多个按钮、金币系统、累计统计。这是 GUI 教程的最终成果。
|
> **本课目标**:把前 5 课全部整合成一个真正的小游戏——**金币系统**、**累计统计**、**重置**、**退出**。最后你还会把重复代码**封装成函数**,体会"代码越改越短"的快乐。
|
||||||
|
>
|
||||||
**复习**:前面所有的 FLTK 知识 + 旧课程的全部 C++ 基础。
|
> **预计时间**:50~70 分钟(最长的一课)
|
||||||
|
>
|
||||||
|
> **学完之后**:你拥有一个完整、可玩的小项目。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 最终游戏长什么样
|
## 0. 上课前先理理
|
||||||
|
|
||||||
|
### 最终长这样
|
||||||
|
|
||||||
```
|
```
|
||||||
┌────────── 抽卡大冒险 ──────────┐
|
┌────────── 抽卡大冒险 ──────────┐
|
||||||
│ │
|
│ │
|
||||||
│ 🎴 单抽 🎴🎴 十连抽 │
|
│ [单抽] [十连抽] │
|
||||||
│ (扣10金币) (扣100金币) │
|
│ (10金币) (100金币) │
|
||||||
|
│ │
|
||||||
|
│ 金币:500 │
|
||||||
│ │
|
│ │
|
||||||
│ ┌── 抽卡结果 ──────────────┐ │
|
│ ┌── 抽卡结果 ──────────────┐ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
@@ -21,91 +27,237 @@
|
|||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ └──────────────────────────┘ │
|
│ └──────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
│ 累计:普通 12 稀有 5 超稀有 2│
|
│ 累计:普通 12 稀有 5 超 2 │
|
||||||
│ │
|
│ │
|
||||||
│ 金币:350 💰 │
|
│ [ 重置 ] [ 退出 ] │
|
||||||
│ │
|
|
||||||
│ [ 重置 ] [ 退出 ] │
|
|
||||||
│ │
|
│ │
|
||||||
└─────────────────────────────────┘
|
└─────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### 游戏规则
|
||||||
|
|
||||||
## 2. 游戏规则
|
| 操作 | 消耗金币 |
|
||||||
|
|------|---------|
|
||||||
|
| 单抽 | 10 |
|
||||||
|
| 十连抽 | 100 |
|
||||||
|
| 重置 | 0(金币回到 500,统计清零) |
|
||||||
|
| 退出 | 0(关闭窗口) |
|
||||||
|
|
||||||
| 操作 | 消耗金币 | 说明 |
|
初始金币 500,金币不够时不能抽卡,提示玩家。
|
||||||
|------|---------|------|
|
|
||||||
| 单抽 | 10 金币 | 抽一张卡 |
|
|
||||||
| 十连抽 | 100 金币 | 抽十张,显示汇总 |
|
|
||||||
| 重置 | 0 金币 | 金币回到初始值,统计清零 |
|
|
||||||
| 退出 | 0 金币 | 关闭窗口 |
|
|
||||||
|
|
||||||
初始金币 500,金币不够时不能抽卡,显示提示。
|
### 这一课的"软"目标
|
||||||
|
|
||||||
|
之前 5 课都是"加一个功能"。这一课除了加功能,还要练**"减代码"**:
|
||||||
|
|
||||||
|
- 把两个回调里都有的 "判断 r → 选卡名 → 累计统计" 抽成一个函数 `get_card()`。
|
||||||
|
- 把 "更新金币显示" 抽成 `update_coins_display()`。
|
||||||
|
- 把 "更新统计显示" 抽成 `update_stats_display()`。
|
||||||
|
|
||||||
|
体会"重复就是 bug 的温床——同一段代码写两份,迟早改其中一份忘了另一份"。
|
||||||
|
|
||||||
|
### 这一课认识 2 个新朋友
|
||||||
|
|
||||||
|
| 名字 | 干什么 |
|
||||||
|
|---------------|---------------------------------------------|
|
||||||
|
| **引用参数** `int &x` | 函数可以**修改**外面传进来的变量 |
|
||||||
|
| `return;` 提前结束 | 条件不满足时让函数立刻退出(金币不够就 return) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 完整示例程序
|
## 1. 起步:先把窗口和 window 全局化
|
||||||
|
|
||||||
保存为 `lesson_gui_6.cpp`。这是教程的最终成果,约 120 行。
|
打开 `lesson_gui_6.cpp`。骨架是第 5 课结束的样子。
|
||||||
|
|
||||||
|
> 💡 这一课要让 "退出按钮" 能调用 `window->hide()`,但 `window` 之前一直是 `main` 里的局部变量。所以第 1 步:**把 `window` 也升级成全局**。
|
||||||
|
|
||||||
|
### ✏️ 改 3 处
|
||||||
|
|
||||||
|
**(1) 在全局控件那一段加**(替换对应 TODO):
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
#include <FL/Fl.H>
|
Fl_Window *window;
|
||||||
#include <FL/Fl_Window.H>
|
```
|
||||||
#include <FL/Fl_Button.H>
|
|
||||||
#include <FL/Fl_Box.H>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <ctime>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
// ===== 卡片数据 =====
|
**(2) 把 `main` 里那行**:
|
||||||
std::string normal_cards[5] = {
|
```cpp
|
||||||
"小火龙", "水跃鱼", "绿草龟", "小电鼠", "土拨仔"
|
Fl_Window *window = new Fl_Window(420, 520, "抽卡世界 - 第5课");
|
||||||
};
|
```
|
||||||
std::string rare_cards[5] = {
|
**改成**:
|
||||||
"烈焰飞龙", "冰霜之狼", "暗影豹", "风暴鹰", "水晶鹿"
|
```cpp
|
||||||
};
|
window = new Fl_Window(440, 560, "抽卡大冒险");
|
||||||
std::string super_cards[3] = {
|
```
|
||||||
"雷霆神龙", "凤凰王", "深海巨兽"
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== 游戏状态(全局) =====
|
注意:①去掉了 `Fl_Window *`(全局已经声明)②窗口加大了③标题改了。
|
||||||
|
|
||||||
|
### ▶️ 编译运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DLESSON=6
|
||||||
|
cmake --build build && ./build/gacha6
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ 检查点 1
|
||||||
|
- [ ] 窗口标题变 "抽卡大冒险"
|
||||||
|
- [ ] 窗口比之前更大(440×560)
|
||||||
|
- [ ] 抽卡按钮还能用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 步骤 2:加金币系统 ⭐核心⭐
|
||||||
|
|
||||||
|
### 🤔 先想一下
|
||||||
|
- 金币是"全程一直存在"的数据,应该用什么变量?(答:全局 `int`)
|
||||||
|
- 不够金币时怎么阻止抽卡继续执行?(答:`return;` 提前退出)
|
||||||
|
|
||||||
|
### ✏️ 改 5 处
|
||||||
|
|
||||||
|
**(1) 在全局区加状态变量**(替换对应 TODO):
|
||||||
|
|
||||||
|
```cpp
|
||||||
int coins = 500;
|
int coins = 500;
|
||||||
int total_normal = 0;
|
```
|
||||||
int total_rare = 0;
|
|
||||||
int total_super = 0;
|
|
||||||
|
|
||||||
// ===== 全局控件 =====
|
**(2) 在全局控件区加**(替换对应 TODO):
|
||||||
Fl_Box *result_box;
|
|
||||||
Fl_Box *stats_box;
|
```cpp
|
||||||
Fl_Box *coins_box;
|
Fl_Box *coins_box;
|
||||||
|
```
|
||||||
|
|
||||||
// ===== 更新金币显示 =====
|
**(3) 在所有回调函数之前加辅助函数**(替换对应 TODO):
|
||||||
|
|
||||||
|
```cpp
|
||||||
void update_coins_display() {
|
void update_coins_display() {
|
||||||
char buf[50];
|
char buf[50];
|
||||||
std::sprintf(buf, "金币:%d 💰", coins);
|
std::sprintf(buf, "金币:%d", coins);
|
||||||
coins_box->label(buf);
|
coins_box->label(buf);
|
||||||
if (coins < 100) {
|
if (coins < 100) {
|
||||||
coins_box->labelcolor(FL_RED);
|
coins_box->labelcolor(FL_RED);
|
||||||
} else {
|
} else {
|
||||||
coins_box->labelcolor(fl_rgb_color(0, 100, 0));
|
coins_box->labelcolor(fl_rgb_color(0, 100, 0)); // 深绿
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
// ===== 更新统计显示 =====
|
**(4) 在 `main` 里、按钮和 `result_box` 之间加金币显示框**(替换对应 TODO):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
coins_box = new Fl_Box(30, 75, 380, 30, "");
|
||||||
|
coins_box->labelsize(20);
|
||||||
|
coins_box->labelfont(FL_BOLD);
|
||||||
|
update_coins_display();
|
||||||
|
```
|
||||||
|
|
||||||
|
同时把 `result_box` 的 Y 坐标改一下(从 80 改到 115),别和金币框重叠:
|
||||||
|
```cpp
|
||||||
|
result_box = new Fl_Box(20, 115, 400, 270, "点击按钮开始抽卡!");
|
||||||
|
```
|
||||||
|
(同时把 `result_box` 宽 380、高 350 也改成 400、270,给底部按钮留位置)
|
||||||
|
|
||||||
|
**(5) 在两个回调函数最前面加金币检查**:
|
||||||
|
|
||||||
|
`draw_one_callback` 开头:
|
||||||
|
```cpp
|
||||||
|
if (coins < 10) {
|
||||||
|
result_box->label("金币不够!单抽需要 10 金币。\n点\"重置\"恢复金币。");
|
||||||
|
result_box->labelcolor(FL_RED);
|
||||||
|
result_box->labelsize(16);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
coins -= 10;
|
||||||
|
update_coins_display();
|
||||||
|
```
|
||||||
|
|
||||||
|
`draw_ten_callback` 开头:
|
||||||
|
```cpp
|
||||||
|
if (coins < 100) {
|
||||||
|
result_box->label("金币不够!十连抽需要 100 金币。\n点\"重置\"恢复金币。");
|
||||||
|
result_box->labelcolor(FL_RED);
|
||||||
|
result_box->labelsize(16);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
coins -= 100;
|
||||||
|
update_coins_display();
|
||||||
|
```
|
||||||
|
|
||||||
|
### ▶️ 编译运行
|
||||||
|
|
||||||
|
### ✅ 检查点 2
|
||||||
|
- [ ] 窗口顶部出现绿色 "金币:500"
|
||||||
|
- [ ] 每点单抽 → 金币减 10
|
||||||
|
- [ ] 每点十连抽 → 金币减 100
|
||||||
|
- [ ] 金币低于 100 → 数字变红
|
||||||
|
- [ ] 金币不够时点按钮 → 提示信息出现,金币不再扣
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 步骤 3:累计统计
|
||||||
|
|
||||||
|
底部的 `stats_box` 之前显示"单次"统计(这十连里的)。改成显示"累计"统计——全部回合加起来。
|
||||||
|
|
||||||
|
### ✏️ 改 4 处
|
||||||
|
|
||||||
|
**(1) 全局区加 3 个变量**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int total_normal = 0;
|
||||||
|
int total_rare = 0;
|
||||||
|
int total_super = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
**(2) 加辅助函数**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
void update_stats_display() {
|
void update_stats_display() {
|
||||||
char buf[100];
|
char buf[100];
|
||||||
std::sprintf(buf, "累计:普通 %d | 稀有 %d | 超稀有 %d",
|
std::sprintf(buf, "累计:普通 %d | 稀有 %d | 超稀有 %d",
|
||||||
total_normal, total_rare, total_super);
|
total_normal, total_rare, total_super);
|
||||||
stats_box->label(buf);
|
stats_box->label(buf);
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
// ===== 获得一张卡的名字和稀有度 =====
|
**(3) 在两个回调里都累加 `total_*` 并在结尾调一次 `update_stats_display()`**:
|
||||||
// rare_level: 0=普通, 1=稀有, 2=超稀有
|
|
||||||
|
例如在 `draw_one_callback` 的 `if/else` 三个分支里分别加:
|
||||||
|
- 普通分支末尾:`total_normal++;`
|
||||||
|
- 稀有分支末尾:`total_rare++;`
|
||||||
|
- 超稀有分支末尾:`total_super++;`
|
||||||
|
|
||||||
|
然后函数末尾加:`update_stats_display();`
|
||||||
|
|
||||||
|
`draw_ten_callback` 里循环每个分支也加同样的 `total_*++;`,循环结束后**删掉原来那行覆盖 `stats_box` 的 `sprintf` 代码**,改成调 `update_stats_display();`。
|
||||||
|
|
||||||
|
**(4) 在 `main` 里、`stats_box` 创建之后加**:
|
||||||
|
```cpp
|
||||||
|
update_stats_display();
|
||||||
|
```
|
||||||
|
|
||||||
|
> 顺便把 stats_box 的位置从 (20, 445, 380, 40) 调整成 (20, 400, 400, 30),给底部按钮留位置。
|
||||||
|
|
||||||
|
### ▶️ 编译运行
|
||||||
|
|
||||||
|
### ✅ 检查点 3
|
||||||
|
- [ ] 底部出现 "累计:普通 0 | 稀有 0 | 超稀有 0"
|
||||||
|
- [ ] 每抽一次 → 对应稀有度计数 +1
|
||||||
|
- [ ] 十连一次 → 三个计数一共 +10
|
||||||
|
- [ ] 数字一直在涨,不会被覆盖
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 步骤 4:用 `get_card()` 消灭重复 ⭐核心⭐
|
||||||
|
|
||||||
|
仔细看你现在的代码——单抽和十连抽里都在写"随机数 → 判断稀有度 → 取卡名 → `total_*++;`"。**这同一段代码出现了两次**。
|
||||||
|
|
||||||
|
### 🤔 先想一下
|
||||||
|
- 怎么把这段重复代码抽成一个函数?
|
||||||
|
- 但是这段代码同时要返回"卡名"和"稀有度等级"——一个函数怎么返回两个东西?
|
||||||
|
|
||||||
|
### 💡 新工具:**引用参数** `int &x`
|
||||||
|
|
||||||
|
```cpp
|
||||||
std::string get_card(int &rare_level) {
|
std::string get_card(int &rare_level) {
|
||||||
int r = std::rand() % 100;
|
int r = std::rand() % 100;
|
||||||
if (r < 70) {
|
if (r < 70) {
|
||||||
rare_level = 0;
|
rare_level = 0; // 通过引用,外面那个变量也被改了
|
||||||
total_normal++;
|
total_normal++;
|
||||||
return normal_cards[std::rand() % 5];
|
return normal_cards[std::rand() % 5];
|
||||||
} else if (r < 95) {
|
} else if (r < 95) {
|
||||||
@@ -118,86 +270,94 @@ std::string get_card(int &rare_level) {
|
|||||||
return super_cards[std::rand() % 3];
|
return super_cards[std::rand() % 3];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
// ===== 单抽回调 =====
|
调用方:
|
||||||
void draw_one_callback(Fl_Widget *w, void *data) {
|
|
||||||
if (coins < 10) {
|
|
||||||
result_box->label("金币不够!单抽需要 10 金币。\n试试点\"重置\"获得新金币。");
|
|
||||||
result_box->labelcolor(FL_RED);
|
|
||||||
result_box->labelsize(16);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
coins -= 10;
|
```cpp
|
||||||
update_coins_display();
|
int level = 0;
|
||||||
|
std::string card = get_card(level); // level 会被函数改成 0/1/2
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✏️ 改 3 处
|
||||||
|
|
||||||
|
**(1) 在所有回调之前加 `get_card` 函数**(替换对应 TODO,代码见上)。
|
||||||
|
|
||||||
|
**(2) 把 `draw_one_callback` 里 `coins -= 10; update_coins_display();` 之后**的所有内容**整体替换成**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int level = 0;
|
||||||
|
std::string card = get_card(level);
|
||||||
|
|
||||||
|
std::string result;
|
||||||
|
if (level == 0) {
|
||||||
|
result = "☆ 普通卡 ☆\n " + card;
|
||||||
|
result_box->labelcolor(fl_rgb_color(128, 128, 128));
|
||||||
|
} else if (level == 1) {
|
||||||
|
result = "★ 稀有卡 ★\n " + card;
|
||||||
|
result_box->labelcolor(FL_BLUE);
|
||||||
|
} else {
|
||||||
|
result = "★★★ 超稀有卡!! ★★★\n " + card;
|
||||||
|
result_box->labelcolor(FL_RED);
|
||||||
|
}
|
||||||
|
|
||||||
|
result_box->labelsize(24);
|
||||||
|
result_box->label(result.c_str());
|
||||||
|
update_stats_display();
|
||||||
|
```
|
||||||
|
|
||||||
|
**(3) 把 `draw_ten_callback` 里 `coins -= 100; update_coins_display();` 之后**的所有内容**整体替换成**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::string all = "";
|
||||||
|
int ten_normal = 0, ten_rare = 0, ten_super = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i <= 10; i++) {
|
||||||
int level = 0;
|
int level = 0;
|
||||||
std::string card = get_card(level);
|
std::string card = get_card(level);
|
||||||
|
|
||||||
std::string result;
|
all += "第" + std::to_string(i) + "抽:";
|
||||||
if (level == 0) {
|
if (level == 0) {
|
||||||
result = "☆ 普通卡 ☆\n " + card;
|
all += "普通 " + card;
|
||||||
result_box->labelcolor(fl_rgb_color(128, 128, 128));
|
ten_normal++;
|
||||||
} else if (level == 1) {
|
} else if (level == 1) {
|
||||||
result = "★ 稀有卡 ★\n " + card;
|
all += "★稀有 " + card;
|
||||||
result_box->labelcolor(FL_BLUE);
|
ten_rare++;
|
||||||
} else {
|
} else {
|
||||||
result = "★★★ 超稀有卡!! ★★★\n " + card + " 🎉";
|
all += "★★★超稀有 " + card;
|
||||||
result_box->labelcolor(FL_RED);
|
ten_super++;
|
||||||
}
|
}
|
||||||
|
all += "\n";
|
||||||
result_box->labelsize(24);
|
|
||||||
result_box->label(result.c_str());
|
|
||||||
update_stats_display();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 十连抽回调 =====
|
// 在 result_box 末尾加一行本次小结
|
||||||
void draw_ten_callback(Fl_Widget *w, void *data) {
|
char summary[80];
|
||||||
if (coins < 100) {
|
std::sprintf(summary, "\n--- 本次:普通%d 稀有%d 超稀有%d ---",
|
||||||
result_box->label("金币不够!十连抽需要 100 金币。\n试试点\"重置\"获得新金币。");
|
ten_normal, ten_rare, ten_super);
|
||||||
result_box->labelcolor(FL_RED);
|
all += summary;
|
||||||
result_box->labelsize(16);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
coins -= 100;
|
result_box->labelsize(14);
|
||||||
update_coins_display();
|
result_box->label(all.c_str());
|
||||||
|
result_box->labelcolor(FL_BLACK);
|
||||||
|
update_stats_display();
|
||||||
|
```
|
||||||
|
|
||||||
std::string all = "";
|
### ▶️ 编译运行
|
||||||
int ten_normal = 0, ten_rare = 0, ten_super = 0;
|
|
||||||
|
|
||||||
for (int i = 1; i <= 10; i++) {
|
### ✅ 检查点 4
|
||||||
int level = 0;
|
- [ ] 功能和上一步一样
|
||||||
std::string card = get_card(level);
|
- [ ] 但是 `get_card()` 函数让两个回调都简短多了
|
||||||
|
- [ ] 累计统计仍然正常累加(因为 `total_*++` 在 `get_card` 里)
|
||||||
|
|
||||||
all += "第" + std::to_string(i) + "抽:";
|
---
|
||||||
|
|
||||||
if (level == 0) {
|
## 5. 步骤 5:重置按钮
|
||||||
all += "普通 " + card;
|
|
||||||
ten_normal++;
|
|
||||||
} else if (level == 1) {
|
|
||||||
all += "★稀有 " + card;
|
|
||||||
ten_rare++;
|
|
||||||
} else {
|
|
||||||
all += "★★★超稀有 " + card;
|
|
||||||
ten_super++;
|
|
||||||
}
|
|
||||||
all += "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 汇总行
|
### ✏️ 改 2 处
|
||||||
char summary[80];
|
|
||||||
std::sprintf(summary, "\n--- 本次:普通%d 稀有%d 超稀有%d ---",
|
|
||||||
ten_normal, ten_rare, ten_super);
|
|
||||||
all += summary;
|
|
||||||
|
|
||||||
result_box->labelsize(14);
|
**(1) 在所有回调之后加**(替换对应 TODO):
|
||||||
result_box->label(all.c_str());
|
|
||||||
result_box->labelcolor(FL_BLACK);
|
|
||||||
update_stats_display();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 重置回调 =====
|
```cpp
|
||||||
void reset_callback(Fl_Widget *w, void *data) {
|
void reset_callback(Fl_Widget *w, void *data) {
|
||||||
coins = 500;
|
coins = 500;
|
||||||
total_normal = 0;
|
total_normal = 0;
|
||||||
@@ -209,153 +369,140 @@ void reset_callback(Fl_Widget *w, void *data) {
|
|||||||
result_box->label("游戏已重置!\n金币回到 500,统计清零。\n开始抽卡吧!");
|
result_box->label("游戏已重置!\n金币回到 500,统计清零。\n开始抽卡吧!");
|
||||||
result_box->labelcolor(FL_BLACK);
|
result_box->labelcolor(FL_BLACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 退出回调 =====
|
|
||||||
void quit_callback(Fl_Widget *w, void *data) {
|
|
||||||
window->hide(); // window 是全局的?
|
|
||||||
// 用 FLTK 的方式退出
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
std::srand(std::time(nullptr));
|
|
||||||
|
|
||||||
Fl_Window *window = new Fl_Window(440, 560, "抽卡大冒险");
|
|
||||||
|
|
||||||
// ===== 顶部:操作按钮 =====
|
|
||||||
Fl_Button *btn1 = new Fl_Button(30, 15, 150, 50, "🎴 单抽\n(10金币)");
|
|
||||||
btn1->labelsize(16);
|
|
||||||
btn1->callback(draw_one_callback);
|
|
||||||
|
|
||||||
Fl_Button *btn10 = new Fl_Button(260, 15, 150, 50, "🎴🎴 十连抽\n(100金币)");
|
|
||||||
btn10->labelsize(16);
|
|
||||||
btn10->callback(draw_ten_callback);
|
|
||||||
|
|
||||||
// ===== 金币显示 =====
|
|
||||||
coins_box = new Fl_Box(30, 75, 380, 30, "");
|
|
||||||
coins_box->labelsize(20);
|
|
||||||
coins_box->labelfont(FL_BOLD);
|
|
||||||
update_coins_display();
|
|
||||||
|
|
||||||
// ===== 结果显示区 =====
|
|
||||||
result_box = new Fl_Box(20, 115, 400, 270, "欢迎来到抽卡大冒险!\n点击上方按钮开始抽卡~");
|
|
||||||
result_box->labelsize(20);
|
|
||||||
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
|
|
||||||
result_box->box(FL_DOWN_BOX);
|
|
||||||
|
|
||||||
// ===== 累计统计 =====
|
|
||||||
stats_box = new Fl_Box(20, 400, 400, 30, "");
|
|
||||||
stats_box->labelsize(14);
|
|
||||||
update_stats_display();
|
|
||||||
|
|
||||||
// ===== 底部按钮 =====
|
|
||||||
Fl_Button *btn_reset = new Fl_Button(100, 450, 100, 40, "重置");
|
|
||||||
btn_reset->callback(reset_callback);
|
|
||||||
|
|
||||||
Fl_Button *btn_quit = new Fl_Button(240, 450, 100, 40, "退出");
|
|
||||||
btn_quit->callback(quit_callback);
|
|
||||||
|
|
||||||
window->end();
|
|
||||||
window->show();
|
|
||||||
return Fl::run();
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**(2) 在 `main` 里 `stats_box` 之后加**(替换对应 TODO):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Fl_Button *btn_reset = new Fl_Button(100, 450, 100, 40, "重置");
|
||||||
|
btn_reset->callback(reset_callback);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ▶️ 编译运行
|
||||||
|
|
||||||
|
### ✅ 检查点 5
|
||||||
|
- [ ] 底部多出 "重置" 按钮
|
||||||
|
- [ ] 把金币抽光 → 点重置 → 金币 500、统计全 0、结果区显示重置提示
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 代码架构总结
|
## 6. 步骤 6:退出按钮
|
||||||
|
|
||||||
回顾这 6 课,你的程序结构从第 1 课的简单窗口,发展到了完整游戏:
|
### ✏️ 改 2 处
|
||||||
|
|
||||||
|
**(1) 在所有回调之后加**(替换对应 TODO):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void quit_callback(Fl_Widget *w, void *data) {
|
||||||
|
window->hide(); // 这里能用 window,是因为步骤 1 我们把它升级成全局了
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**(2) 在 `main` 里 `btn_reset` 之后加**(替换对应 TODO):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Fl_Button *btn_quit = new Fl_Button(240, 450, 100, 40, "退出");
|
||||||
|
btn_quit->callback(quit_callback);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ▶️ 编译运行
|
||||||
|
|
||||||
|
### ✅ 检查点 6
|
||||||
|
- [ ] 底部多出 "退出" 按钮
|
||||||
|
- [ ] 点 "退出" → 窗口关闭,程序退出
|
||||||
|
- [ ] 点 "重置" 还能用
|
||||||
|
|
||||||
|
🎉🎉🎉 **第 6 课通关 = 整个 GUI 教程通关!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 代码架构总结
|
||||||
|
|
||||||
|
回顾你现在的代码结构:
|
||||||
|
|
||||||
```
|
```
|
||||||
全局区域
|
全局区域
|
||||||
├── 卡片数据(数组)
|
├── 卡片数据(3 个数组)
|
||||||
├── 游戏状态(金币、统计)
|
├── 游戏状态(coins、total_*)
|
||||||
├── 全局控件指针
|
├── 全局控件指针(window、result_box、stats_box、coins_box)
|
||||||
│
|
|
||||||
回调函数
|
|
||||||
├── get_card() ← 封装"抽一张卡"的逻辑(函数复用!)
|
|
||||||
├── draw_one_callback() ← 单抽
|
|
||||||
├── draw_ten_callback() ← 十连抽
|
|
||||||
├── reset_callback() ← 重置
|
|
||||||
├── quit_callback() ← 退出
|
|
||||||
│
|
│
|
||||||
辅助函数
|
辅助函数
|
||||||
├── update_coins_display()
|
├── update_coins_display() ← 显示金币
|
||||||
├── update_stats_display()
|
├── update_stats_display() ← 显示累计
|
||||||
|
├── get_card(int &level) ← 抽一张卡(被两个回调复用!)
|
||||||
|
│
|
||||||
|
回调函数
|
||||||
|
├── draw_one_callback() ← 单抽
|
||||||
|
├── draw_ten_callback() ← 十连
|
||||||
|
├── reset_callback()
|
||||||
|
└── quit_callback()
|
||||||
│
|
│
|
||||||
main()
|
main()
|
||||||
├── 创建窗口
|
├── 设种子
|
||||||
├── 创建所有控件
|
├── 创建窗口、所有控件
|
||||||
├── 绑定回调
|
├── 绑定回调
|
||||||
└── Fl::run()
|
└── Fl::run()
|
||||||
```
|
```
|
||||||
|
|
||||||
注意 `get_card()` 函数——它把"随机 + 判断 + 统计"打包成一个函数,单抽和十连抽都调用它。这就是旧课程第 8 课学的**函数复用**!
|
注意 `get_card()` —— 它就是旧课程"函数复用"知识的真正应用。两个回调都调用它,不重复任何逻辑。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 新知识点
|
## 8. 玩一玩
|
||||||
|
|
||||||
### 封装函数提高复用(旧知识的新应用)
|
1. 改初始金币 `coins = 1000`,体会游戏节奏。
|
||||||
|
2. 单抽改成 20 金币,十连抽改 200 金币(涨价)。
|
||||||
|
3. 把卡片名字换成你自己的主题(三国、星座、美食……)。
|
||||||
|
4. **思考题**:为什么 `coins` 要写成全局变量而不是 `main` 里?
|
||||||
|
|
||||||
```cpp
|
---
|
||||||
std::string get_card(int &rare_level) {
|
|
||||||
// 随机 → 判断 → 统计 → 返回卡名
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `int &rare_level` —— 引用参数,函数可以修改外面的变量
|
## 9. 课后挑战(选做 / 拓展)
|
||||||
- 单抽和十连抽都调同一个函数,不重复代码
|
|
||||||
|
|
||||||
### `return` 提前退出
|
挑你最感兴趣的:
|
||||||
|
|
||||||
```cpp
|
- **A. 幸运计数器**:再加一个 `int unlucky_streak = 0;`,每出一次超稀有清零,否则 `+1`。超过 30 时在 `stats_box` 里提示 "保底在望!"。
|
||||||
if (coins < 10) {
|
- **B. 图鉴**:加一个按钮,点了显示三组数组里所有卡名。
|
||||||
result_box->label("金币不够!...");
|
- **C. 颜色主题**:给整个窗口换一套配色,按钮、背景、字色都改。
|
||||||
return; // 直接结束函数,不继续抽卡
|
- **D. 抽卡历史**:用 `std::vector<std::string>`(如果你想接触)保存每次抽到的卡,加一个按钮显示历史。
|
||||||
}
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 卡住了?常见小问题
|
||||||
|
|
||||||
|
| 现象 | 多半是 |
|
||||||
|
|------|-------|
|
||||||
|
| 编译报 `window` 未声明 | 步骤 1 没把 `window` 升级为全局,或 main 里又写了 `Fl_Window *` |
|
||||||
|
| 退出按钮没反应 | 没把 `window` 升全局,或 `quit_callback` 里写错变量名 |
|
||||||
|
| 金币显示不更新 | 改了 `coins` 后没调 `update_coins_display()` |
|
||||||
|
| 累计统计不更新 | 回调里漏调 `update_stats_display()`,或没在 `get_card` 里 `total_*++` |
|
||||||
|
| `get_card` 报错"不能修改 const" | 函数签名忘了写 `int &rare_level`(少了 `&`) |
|
||||||
|
| 控件位置重叠 | y 坐标算错;先在纸上画一张布局图 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 偷看老师答案
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DLESSON=6 -DUSE_SOLUTION=ON
|
||||||
|
cmake --build build && ./build/gacha6
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 本课课堂小练习
|
## 🎉 整个教程总结
|
||||||
|
|
||||||
1. 调整游戏参数:初始金币改 1000,单抽改 20 金币,看游戏节奏变化。
|
你完成了从命令行到 GUI 的跨越。回看 6 课:
|
||||||
2. 在十连抽结果中增加一行"保底提示",如果没有出超稀有,写一句"再接再厉!"。
|
|
||||||
3. 把卡片名字全部换成你自创的主题(比如三国武将、星座、美食……)。
|
|
||||||
|
|
||||||
---
|
| 课 | 学到了什么 |
|
||||||
|
|---|---|
|
||||||
|
| 1 | 怎么让屏幕上出现一个窗口 |
|
||||||
|
| 2 | 让按钮触发代码(回调) |
|
||||||
|
| 3 | 在回调里用随机和判断 |
|
||||||
|
| 4 | 用循环 + 字符串拼接处理多行结果 |
|
||||||
|
| 5 | 用数组管理内容 + 给界面分区 |
|
||||||
|
| 6 | 综合所有知识 + 用函数消灭重复 |
|
||||||
|
|
||||||
## 7. 本课课后作业(选做 / 拓展)
|
从第 1 课 `std::cout << "欢迎来到抽卡世界!"` 到一个完整可玩的游戏,你只用了 6 课!
|
||||||
|
|
||||||
以下是你可以自己加的进阶功能:
|
> 📝 学完记得在 `学习进度与状态-GUI.md` 里把第 6 课打勾。**这一栏打完,整张表就全绿啦。**
|
||||||
|
|
||||||
- **幸运计数器**:统计"已经多少抽没出超稀有了",超过 30 抽提示"保底在望!"
|
|
||||||
- **图鉴系统**:加一个按钮,用文字列表显示你收集到的所有不重复卡片。
|
|
||||||
- **卡片仓库**:用 `std::vector` 存每次抽到的卡,可以查看历史记录。
|
|
||||||
- **皮肤/主题**:改窗口颜色、按钮颜色,做一套你喜欢的主题。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 常见小问题
|
|
||||||
|
|
||||||
- **退出按钮无响应**:检查 `quit_callback` 里是否写了 `exit(0)`。FLTK 中可以用 `exit(0)` 简单退出。
|
|
||||||
- **金币显示不更新**:每次改 `coins` 后是否调了 `update_coins_display()`。
|
|
||||||
- **编译报错 `window` 未声明**:如果退出回调用了 `window->hide()`,`window` 需要是全局变量或传参数。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 恭喜!
|
|
||||||
|
|
||||||
你完成了从命令行到 GUI 的跨越!你现在能够:
|
|
||||||
- 用 FLTK 创建窗口和控件
|
|
||||||
- 用回调函数响应按钮点击
|
|
||||||
- 把随机/判断/循环/数组等 C++ 知识用在 GUI 程序里
|
|
||||||
- 设计一个完整的小游戏
|
|
||||||
|
|
||||||
从第 1 课的 `std::cout << "欢迎来到抽卡世界!"` 到现在这个带窗口、按钮、金币系统的完整游戏,你只用了 6 课!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*学完请在 `学习进度与状态-GUI.md` 里更新第 6 课的状态。*
|
|
||||||
|
|||||||
Reference in New Issue
Block a user