save code
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Window.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// 创建窗口:宽 400,高 300,标题"抽卡世界"
|
||||||
|
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界");
|
||||||
|
|
||||||
|
// 在窗口上放一个文字标签:位置(50,100),宽300,高100
|
||||||
|
Fl_Box *box = new Fl_Box(50, 100, 300, 100, "欢迎来到抽卡世界!");
|
||||||
|
box->labelsize(24); // 字号 24
|
||||||
|
|
||||||
|
// 结束添加控件
|
||||||
|
window->end();
|
||||||
|
// 显示窗口
|
||||||
|
window->show();
|
||||||
|
// 进入事件循环(让窗口一直显示)
|
||||||
|
return Fl::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#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,56 @@
|
|||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Window.H>
|
||||||
|
#include <FL/Fl_Button.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <cstdlib> // rand, srand
|
||||||
|
#include <ctime> // time
|
||||||
|
|
||||||
|
// 全局变量
|
||||||
|
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,104 @@
|
|||||||
|
#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) {
|
||||||
|
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"; // 换行
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用 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() {
|
||||||
|
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,117 @@
|
|||||||
|
#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 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("单抽模式");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 十连抽回调 =====
|
||||||
|
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,207 @@
|
|||||||
|
#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 rare_cards[5] = {
|
||||||
|
"烈焰飞龙", "冰霜之狼", "暗影豹", "风暴鹰", "水晶鹿"
|
||||||
|
};
|
||||||
|
std::string super_cards[3] = {
|
||||||
|
"雷霆神龙", "凤凰王", "深海巨兽"
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 游戏状态(全局) =====
|
||||||
|
int coins = 500;
|
||||||
|
int total_normal = 0;
|
||||||
|
int total_rare = 0;
|
||||||
|
int total_super = 0;
|
||||||
|
|
||||||
|
// ===== 全局控件 =====
|
||||||
|
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) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
@@ -0,0 +1,77 @@
|
|||||||
|
# 学习进度与状态(GUI 版)
|
||||||
|
|
||||||
|
用来记录 FLTK GUI 抽卡游戏 **每一课的学习状态**、**课堂练习**和**课后作业**是否完成。建议每学完一课或做完作业后 **马上改一次**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 状态说明
|
||||||
|
|
||||||
|
### 学习状态
|
||||||
|
|
||||||
|
| 填写值 | 含义 |
|
||||||
|
|--------|------|
|
||||||
|
| `未开始` | 还没打开这一课的文档。 |
|
||||||
|
| `进行中` | 正在学,还没认为"这一课学完了"。 |
|
||||||
|
| `已完成` | 这一课的内容已学完,理解主要目标。 |
|
||||||
|
|
||||||
|
### 课堂练习
|
||||||
|
|
||||||
|
| 填写值 | 含义 |
|
||||||
|
|--------|------|
|
||||||
|
| `未完成` | 文档里的"课堂小练习"还没做完。 |
|
||||||
|
| `已完成` | 课堂小练习已做完。 |
|
||||||
|
| `无` | 该课没有单独列课堂练习。 |
|
||||||
|
|
||||||
|
### 课后作业
|
||||||
|
|
||||||
|
| 填写值 | 含义 |
|
||||||
|
|--------|------|
|
||||||
|
| `未完成` | 有布置作业但还没做。 |
|
||||||
|
| `已完成` | 作业已做。 |
|
||||||
|
| `选做未完成` | 作业是选做,还没做。 |
|
||||||
|
| `选做已完成` | 选做作业已完成。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总览表
|
||||||
|
|
||||||
|
| 课次 | 学习状态 | 课堂练习 | 课后作业 | 备注 | 最后更新 |
|
||||||
|
|------|----------|----------|----------|------|----------|
|
||||||
|
| 第01课 GUI你好窗口 | 未开始 | 未完成 | 选做未完成 | | |
|
||||||
|
| 第02课 按钮与回调 | 未开始 | 未完成 | 选做未完成 | | |
|
||||||
|
| 第03课 随机抽卡 | 未开始 | 未完成 | 选做未完成 | | |
|
||||||
|
| 第04课 十连抽 | 未开始 | 未完成 | 选做未完成 | | |
|
||||||
|
| 第05课 卡片名字与布局 | 未开始 | 未完成 | 选做未完成 | | |
|
||||||
|
| 第06课 完整抽卡游戏 | 未开始 | 未完成 | 选做未完成 | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速记录区
|
||||||
|
|
||||||
|
把最近几次学习用一两句话写在这里,方便回顾:
|
||||||
|
|
||||||
|
```
|
||||||
|
日期:____________________ ____________________________________
|
||||||
|
日期:____________________ ____________________________________
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件对应关系
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `教程-FLTK抽卡游戏-GUI-6课.md` | 6 课总目录与概览 |
|
||||||
|
| `第01课-GUI你好窗口.md` | 第 1 课:Fl_Window、Fl_Box、Fl::run() |
|
||||||
|
| `第02课-按钮与回调.md` | 第 2 课:Fl_Button、callback、全局指针 |
|
||||||
|
| `第03课-随机抽卡.md` | 第 3 课:随机数+IF/ELSE 进 GUI |
|
||||||
|
| `第04课-十连抽.md` | 第 4 课:for 循环 + 字符串拼接 |
|
||||||
|
| `第05课-卡片名字与布局.md` | 第 5 课:数组 + 卡片名 + 布局 |
|
||||||
|
| `第06课-完整抽卡游戏.md` | 第 6 课:综合实战项目 |
|
||||||
|
| `lesson_gui_1.cpp` | 第 1 课代码 |
|
||||||
|
| `lesson_gui_2.cpp` | 第 2 课代码 |
|
||||||
|
| `lesson_gui_3.cpp` | 第 3 课代码 |
|
||||||
|
| `lesson_gui_4.cpp` | 第 4 课代码 |
|
||||||
|
| `lesson_gui_5.cpp` | 第 5 课代码 |
|
||||||
|
| `lesson_gui_6.cpp` | 第 6 课代码 |
|
||||||
|
| `CMakeLists_lesson1.txt` | 编译模板(每课改名用) |
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# 用 FLTK 做 GUI 抽卡游戏 · 6 课时教程
|
||||||
|
|
||||||
|
面向 **11 岁左右** 学习者,已完成命令行 C++ 10 课基础(变量、输入、随机、判断、循环、数组、函数、菜单)。本教程用 **FLTK 框架** 制作带窗口和按钮的抽卡游戏。
|
||||||
|
|
||||||
|
每课约 **30~45 分钟**,建议家长陪同。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前置条件(给家长)
|
||||||
|
|
||||||
|
孩子需已掌握:
|
||||||
|
- `int`、`string` 变量
|
||||||
|
- `cin` / `cout`
|
||||||
|
- `rand()` / `srand()`
|
||||||
|
- `if` / `else if` / `else`
|
||||||
|
- `for` 循环
|
||||||
|
- `string` 数组
|
||||||
|
- `void` 函数
|
||||||
|
- `while` 循环与 `break`
|
||||||
|
|
||||||
|
### 环境安装
|
||||||
|
|
||||||
|
**Mac(目标平台)**:
|
||||||
|
```bash
|
||||||
|
brew install fltk
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows(开发用)**:
|
||||||
|
已通过 vcpkg 安装:`vcpkg install fltk --triplet x64-windows`
|
||||||
|
|
||||||
|
### 编译命令模板
|
||||||
|
|
||||||
|
每个项目的 `CMakeLists.txt` 模板(保存为项目文件夹下):
|
||||||
|
```cmake
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
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)
|
||||||
|
|
||||||
|
add_executable(gacha main.cpp)
|
||||||
|
target_link_libraries(gacha PRIVATE fltk)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mac 编译**:
|
||||||
|
```bash
|
||||||
|
cmake -B build && cmake --build build && ./build/gacha
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows 编译**(需指定 vcpkg toolchain):
|
||||||
|
```bash
|
||||||
|
cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake && cmake --build build --config Release && build\Release\gacha.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总说明
|
||||||
|
|
||||||
|
- **原则**:每课都能跑出一个可见的窗口,逐步变好玩
|
||||||
|
- **抽卡**:用随机数模拟概率,不涉及真实金钱
|
||||||
|
- **FLTK 说明**:FLTK 是一个轻量 GUI 库,不需要安装庞大框架,编译出的程序只有几 MB
|
||||||
|
- **跨平台**:同一份代码在 Windows / Mac / Linux 都能编译运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第 1 课:GUI 你好窗口
|
||||||
|
|
||||||
|
**目标**:创建第一个有窗口的程序,屏幕上出现欢迎语。
|
||||||
|
|
||||||
|
**FLTK 新概念**:`Fl_Window`(窗口)、`Fl_Box`(文字标签)、`Fl::run()`(事件循环)
|
||||||
|
|
||||||
|
**对比旧知识**:`std::cout` 把字打在命令行 → `Fl_Box` 把字显示在窗口里
|
||||||
|
|
||||||
|
详见:`第01课-GUI你好窗口.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第 2 课:按钮与回调
|
||||||
|
|
||||||
|
**目标**:放一个按钮,点击后文字会变化——理解"事件驱动"。
|
||||||
|
|
||||||
|
**FLTK 新概念**:`Fl_Button`(按钮)、`callback`(回调函数)、全局指针
|
||||||
|
|
||||||
|
**核心思想**:和命令行不同,GUI 程序"等"用户操作,而不是按顺序执行
|
||||||
|
|
||||||
|
详见:`第02课-按钮与回调.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第 3 课:随机抽卡——核心来了
|
||||||
|
|
||||||
|
**目标**:点击按钮 → 随机出普通/稀有/超稀有,显示在窗口里。
|
||||||
|
|
||||||
|
**复用旧知识**:`rand()` / `srand()`、`if` / `else if` / `else`
|
||||||
|
|
||||||
|
**FLTK 新概念**:在回调里更新控件、`label()` 方法
|
||||||
|
|
||||||
|
详见:`第03课-随机抽卡.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第 4 课:十连抽
|
||||||
|
|
||||||
|
**目标**:一个按钮抽 10 次,结果一起显示。
|
||||||
|
|
||||||
|
**复用旧知识**:`for` 循环、`std::string` 拼接
|
||||||
|
|
||||||
|
**FLTK 新概念**:多行显示(`\n` 换行)
|
||||||
|
|
||||||
|
详见:`第04课-十连抽.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第 5 课:卡片名字与布局
|
||||||
|
|
||||||
|
**目标**:给每种稀有度配上具体的卡片名字,让抽卡更有趣。学会让界面更整齐。
|
||||||
|
|
||||||
|
**复用旧知识**:`std::string` 数组、下标
|
||||||
|
|
||||||
|
**FLTK 新概念**:控件位置计算
|
||||||
|
|
||||||
|
详见:`第05课-卡片名字与布局.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第 6 课:完整抽卡游戏
|
||||||
|
|
||||||
|
**目标**:整合前面全部内容,做一个完整的抽卡游戏——多个按钮、金币系统、统计面板。
|
||||||
|
|
||||||
|
**复用旧知识**:全部旧知识
|
||||||
|
|
||||||
|
**FLTK 新概念**:多按钮协作、综合布局
|
||||||
|
|
||||||
|
详见:`第06课-完整抽卡游戏.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 知识点对照表
|
||||||
|
|
||||||
|
| 课次 | FLTK 新概念 | 复用的 C++ 知识 |
|
||||||
|
|------|------------|----------------|
|
||||||
|
| 1 | `Fl_Window`、`Fl_Box`、`Fl::run()`、`new` | `main`、`#include` |
|
||||||
|
| 2 | `Fl_Button`、`callback`、全局指针 | `void` 函数 |
|
||||||
|
| 3 | `label()` 更新、回调中用随机 | `rand`/`srand`、`if`/`else`、`%` |
|
||||||
|
| 4 | 多行 label、字符串拼接 | `for`、`std::string`、`+=` |
|
||||||
|
| 5 | 控件定位、`Fl_Group` | `string` 数组、下标、`rand()%N` |
|
||||||
|
| 6 | 多按钮回调、综合布局 | 全部知识综合 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 给小朋友的 GUI 比喻
|
||||||
|
|
||||||
|
- **窗口** 像一张画纸。
|
||||||
|
- **控件**(Widget)像贴在画纸上的便利贴——文字标签、按钮。
|
||||||
|
- **回调函数** 像"按钮被按时要做什么"的说明书。
|
||||||
|
- **事件循环**(`Fl::run()`)像一个永不休息的保安,一直等用户操作。
|
||||||
|
- 命令行程序像"一口气读完的清单",GUI 程序像"一直等着你来的游戏机"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*每课详细步骤见对应文档。*
|
||||||
+194
@@ -0,0 +1,194 @@
|
|||||||
|
# 第 1 课:GUI 你好窗口
|
||||||
|
|
||||||
|
**本课目标**:用 FLTK 创建第一个 GUI 程序——一个窗口,里面写着欢迎语。理解 GUI 程序和命令行程序的不同。
|
||||||
|
|
||||||
|
**复习**:你会用 `std::cout` 在命令行输出文字。本课把"输出"搬到**窗口**里。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 这一课要搞懂什么
|
||||||
|
|
||||||
|
### 命令行 vs GUI
|
||||||
|
|
||||||
|
| | 命令行程序 | GUI 程序 |
|
||||||
|
|---|---|---|
|
||||||
|
| 样子 | 黑底白字 | 有窗口、按钮 |
|
||||||
|
| 运行方式 | 一口气跑完 | 一直等用户操作 |
|
||||||
|
| 输出 | `cout` 打印到终端 | 显示在窗口里的控件上 |
|
||||||
|
| 用户操作 | 键盘输入 | 鼠标点击、键盘 |
|
||||||
|
|
||||||
|
### 本课三个"新朋友"
|
||||||
|
|
||||||
|
| FLTK 类 | 比喻(小朋友版) | 作用 |
|
||||||
|
|---------|-----------------|------|
|
||||||
|
| `Fl_Window` | 画纸 | 程序的窗口框架 |
|
||||||
|
| `Fl_Box` | 便利贴 | 在窗口上显示文字 |
|
||||||
|
| `Fl::run()` | 不休息的保安 | 让窗口一直显示,等用户操作 |
|
||||||
|
|
||||||
|
### 你还认识一个老朋友的变身
|
||||||
|
|
||||||
|
- `new`:以前你可能没见过。在 FLTK 里,控件用 `new` 创建。可以简单理解为"变出一个控件来"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 第一个 GUI 程序
|
||||||
|
|
||||||
|
保存为 `lesson_gui_1.cpp`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Window.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// 创建窗口:宽 400,高 300,标题"抽卡世界"
|
||||||
|
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界");
|
||||||
|
|
||||||
|
// 在窗口上放一个文字标签:位置(50,100),宽300,高100
|
||||||
|
Fl_Box *box = new Fl_Box(50, 100, 300, 100, "欢迎来到抽卡世界!");
|
||||||
|
box->labelsize(24); // 字号 24
|
||||||
|
|
||||||
|
// 结束添加控件
|
||||||
|
window->end();
|
||||||
|
// 显示窗口
|
||||||
|
window->show();
|
||||||
|
// 进入事件循环(让窗口一直显示)
|
||||||
|
return Fl::run();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
配套 `CMakeLists.txt`:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 代码逐行解释
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <FL/Fl.H> // FLTK 核心
|
||||||
|
#include <FL/Fl_Window.H> // 窗口
|
||||||
|
#include <FL/Fl_Box.H> // 文字标签
|
||||||
|
```
|
||||||
|
|
||||||
|
以前的 `#include <iostream>` 是请命令行工具,这三个 `#include <FL/...>` 是请窗口工具。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界");
|
||||||
|
```
|
||||||
|
|
||||||
|
- `Fl_Window` = 窗口
|
||||||
|
- `400, 300` = 宽 400 像素,高 300 像素
|
||||||
|
- `"抽卡世界"` = 窗口标题栏上显示的字
|
||||||
|
- `new` = 创建一个窗口(和以前 `int x = 5` 不同,窗口是"对象")
|
||||||
|
- `*` = 指针(先记:FLTK 创建的控件都用 `*`)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Fl_Box *box = new Fl_Box(50, 100, 300, 100, "欢迎来到抽卡世界!");
|
||||||
|
```
|
||||||
|
|
||||||
|
- `50, 100` = 文字在窗口中的位置:从左边数 50 像素,从上边数 100 像素
|
||||||
|
- `300, 100` = 文字区域的宽 300,高 100
|
||||||
|
- 最后一个参数 = 显示的文字内容
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
box->labelsize(24);
|
||||||
|
```
|
||||||
|
|
||||||
|
`->` 读作"的":`box` 的 `labelsize` 设为 24(字号)。
|
||||||
|
|
||||||
|
```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 →
|
||||||
|
│
|
||||||
|
│ (50,100)
|
||||||
|
│ ↓
|
||||||
|
│ ┌──────────┐
|
||||||
|
│ │ 文字区域 │ 宽 300
|
||||||
|
│ │ │ 高 100
|
||||||
|
│ └──────────┘
|
||||||
|
Y
|
||||||
|
↓
|
||||||
|
```
|
||||||
|
|
||||||
|
- X 从左到右增大
|
||||||
|
- Y 从上到下增大
|
||||||
|
- 原点 (0,0) 在左上角
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 本课课堂小练习
|
||||||
|
|
||||||
|
1. 把窗口标题 `"抽卡世界"` 改成你自己起的名字。
|
||||||
|
2. 把欢迎语改成你喜欢的句子。
|
||||||
|
3. 试试把窗口改大(比如 600×400),把 `Box` 的位置也调整一下,让文字居中。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 本课课后作业(选做)
|
||||||
|
|
||||||
|
- 把 `labelsize(24)` 改成 36、12、8,看文字大小变化。试试极端值,看会发生什么。
|
||||||
|
- 在窗口上放**两个** `Fl_Box`,一个在上半部分,一个在下半部分,显示不同的文字。
|
||||||
|
- 思考题:和以前 `std::cout` 输出文字相比,GUI 方式有什么不同?(一个提示:GUI 的文字可以"一直显示",不用 `\n` 刷屏。)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 常见小问题
|
||||||
|
|
||||||
|
- **窗口一闪就消失**:检查有没有 `return Fl::run();`,不是 `return 0;`。
|
||||||
|
- **编译报错找不到 FL**:检查 `CMakeLists.txt` 里的 `find_package(FLTK CONFIG REQUIRED)` 是否正确,以及 FLTK 是否已安装。
|
||||||
|
- **中文乱码**:确保源文件保存为 UTF-8 编码。Mac clang++ 一般默认支持。
|
||||||
|
- **窗口出来了但是空的**:检查 `window->show()` 和 `window->end()` 的顺序。`show()` 必须在 `end()` 之后(本课代码顺序已正确)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 下一课预告
|
||||||
|
|
||||||
|
第 2 课学**按钮和回调**:在窗口上放一个"抽卡"按钮,点一下,文字会变——从"只看不动"到"能交互"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*学完请在 `学习进度与状态-GUI.md` 里更新第 1 课的状态。*
|
||||||
|
|
||||||
|
**下一课文档**:`第02课-按钮与回调.md`
|
||||||
+222
@@ -0,0 +1,222 @@
|
|||||||
|
# 第 2 课:按钮与回调
|
||||||
|
|
||||||
|
**本课目标**:在窗口上放一个按钮,点击按钮后文字发生变化——学会"用户点击 → 程序响应"。
|
||||||
|
|
||||||
|
**复习**:第 1 课你做出了一个静态窗口。本课让它"能动"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 这一课要搞懂什么
|
||||||
|
|
||||||
|
### 最重要的新概念:回调(callback)
|
||||||
|
|
||||||
|
以前命令行程序是这样跑的:
|
||||||
|
```
|
||||||
|
一行接一行执行 → 程序结束
|
||||||
|
```
|
||||||
|
|
||||||
|
GUI 程序是这样跑的:
|
||||||
|
```
|
||||||
|
创建窗口和控件 → 进入"等待模式" → 用户点按钮 → 执行回调函数 → 继续等待...
|
||||||
|
```
|
||||||
|
|
||||||
|
**回调函数**就是"当按钮被按下时,要执行的那段代码"。就像你告诉机器:"如果有人按这个按钮,就做这件事。"
|
||||||
|
|
||||||
|
### 本课新朋友
|
||||||
|
|
||||||
|
| FLTK 类/概念 | 比喻 | 作用 |
|
||||||
|
|-------------|------|------|
|
||||||
|
| `Fl_Button` | 按钮 | 可以点击的控件 |
|
||||||
|
| `callback()` | 说明书 | 告诉按钮"被按时做什么" |
|
||||||
|
| 全局指针 | 留言板 | 让回调函数能找到要改的控件 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 为什么需要"全局指针"?
|
||||||
|
|
||||||
|
看下面的问题:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// ❌ 这样不行!
|
||||||
|
void button_clicked() {
|
||||||
|
box->label("抽卡了!"); // 这个函数里不认识 box!
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
Fl_Box *box = new Fl_Box(50, 100, 300, 100, "还没抽");
|
||||||
|
// box 只在 main 里有效,button_clicked 函数看不到它
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**解决方案**:把 `box` 声明在 `main` 外面——成为"全局变量",到处都能看到。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// ✅ 这样就行!
|
||||||
|
Fl_Box *box; // 全局声明
|
||||||
|
|
||||||
|
void button_clicked(Fl_Widget *, void *) {
|
||||||
|
box->label("抽卡了!"); // 全局 box,函数里也能用
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 小提示:`(Fl_Widget *, void *)` 是回调要的固定格式,本课先记住写法,不用深究。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 完整示例程序
|
||||||
|
|
||||||
|
保存为 `lesson_gui_2.cpp`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Window.H>
|
||||||
|
#include <FL/Fl_Button.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
|
||||||
|
// 全局变量——让回调函数也能访问
|
||||||
|
Fl_Box *result_box;
|
||||||
|
|
||||||
|
// 回调函数——按钮被按时执行
|
||||||
|
void draw_callback(Fl_Widget *w, void *data) {
|
||||||
|
result_box->label("你点击了抽卡按钮!");
|
||||||
|
result_box->labelcolor(FL_RED); // 文字变红
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
Fl_Window *window = new Fl_Window(400, 300, "抽卡世界 - 第2课");
|
||||||
|
|
||||||
|
// 在上方放一个按钮
|
||||||
|
Fl_Button *btn = new Fl_Button(130, 40, 140, 50, "🎴 抽一张!");
|
||||||
|
btn->labelsize(18);
|
||||||
|
btn->callback(draw_callback); // 绑定回调函数
|
||||||
|
|
||||||
|
// 在下方放一个显示结果的标签
|
||||||
|
result_box = new Fl_Box(50, 130, 300, 100, "请点击按钮抽卡~");
|
||||||
|
result_box->labelsize(20);
|
||||||
|
|
||||||
|
window->end();
|
||||||
|
window->show();
|
||||||
|
return Fl::run();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
配套 `CMakeLists.txt`:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(gacha2 LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
add_compile_options("/utf-8")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_package(FLTK CONFIG REQUIRED)
|
||||||
|
|
||||||
|
add_executable(gacha2 lesson_gui_2.cpp)
|
||||||
|
target_link_libraries(gacha2 PRIVATE fltk)
|
||||||
|
```
|
||||||
|
|
||||||
|
Mac 编译:`cmake -B build && cmake --build build && ./build/gacha2`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 代码逐行解释
|
||||||
|
|
||||||
|
### 全局变量
|
||||||
|
```cpp
|
||||||
|
Fl_Box *result_box;
|
||||||
|
```
|
||||||
|
放在所有函数外面 → 全局 → 回调函数和 main 都能用。
|
||||||
|
|
||||||
|
### 回调函数
|
||||||
|
```cpp
|
||||||
|
void draw_callback(Fl_Widget *w, void *data) {
|
||||||
|
result_box->label("你点击了抽卡按钮!");
|
||||||
|
result_box->labelcolor(FL_RED);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `Fl_Widget *w` —— 被点击的那个控件(本课先不关心)
|
||||||
|
- `void *data` —— 可以传额外数据(本课先用不上)
|
||||||
|
- 这两个参数是 FLTK 要求的固定格式
|
||||||
|
|
||||||
|
### 按钮
|
||||||
|
```cpp
|
||||||
|
Fl_Button *btn = new Fl_Button(130, 40, 140, 50, "🎴 抽一张!");
|
||||||
|
btn->callback(draw_callback);
|
||||||
|
```
|
||||||
|
- 位置 (130, 40),宽 140,高 50
|
||||||
|
- `callback(draw_callback)` —— 告诉按钮:被按时执行 `draw_callback`
|
||||||
|
|
||||||
|
### labelcolor
|
||||||
|
```cpp
|
||||||
|
result_box->labelcolor(FL_RED);
|
||||||
|
```
|
||||||
|
- `FL_RED` 是 FLTK 内置的颜色名
|
||||||
|
- 其他常用:`FL_BLUE`、`FL_GREEN`、`FL_BLACK`、`FL_WHITE`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 运行效果
|
||||||
|
|
||||||
|
启动后看到:
|
||||||
|
```
|
||||||
|
┌─────────────── 抽卡世界 ───────────────┐
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ │
|
||||||
|
│ │ 🎴 抽一张! │ │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 请点击按钮抽卡~ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
点击按钮后:
|
||||||
|
```
|
||||||
|
│ ┌──────────────┐ │
|
||||||
|
│ │ 🎴 抽一张! │ │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 你点击了抽卡按钮! ← 变红了 │
|
||||||
|
│ │
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 本课课堂小练习
|
||||||
|
|
||||||
|
1. 修改回调函数,让点击后显示"你又抽了一次!"。
|
||||||
|
2. 把按钮文字改成"抽一抽"或你自己想的名字。
|
||||||
|
3. **放两个按钮**:一个"抽卡",一个"重置"。点"重置"让文字变回初始状态。
|
||||||
|
- 提示:需要两个回调函数(或一个回调 + `data` 参数区分)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 本课课后作业(选做)
|
||||||
|
|
||||||
|
- 点击按钮 5 次后,文字自动变成"次数用完啦!"(提示:加一个全局 `int count` 变量,在回调里累加并判断)
|
||||||
|
- 试试 `FL_BLUE`、`FL_GREEN`、`fl_rgb_color(255, 128, 0)`(自定义橙色)
|
||||||
|
- 思考题:为什么 GUI 程序不按"先执行第一行,再执行第二行"的固定顺序跑?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 常见小问题
|
||||||
|
|
||||||
|
- **点按钮没反应**:检查 `callback()` 那一行有没有写,函数名有没有拼错。
|
||||||
|
- **label 变了但颜色没变**:检查 `labelcolor()` 的拼写。
|
||||||
|
- **`result_box` 未声明**:检查是否在 `main` 外面写了 `Fl_Box *result_box;`。
|
||||||
|
- **回调函数的参数**:`(Fl_Widget *w, void *data)` 这个写法不能省略参数;即使不用,也要写上。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 下一课预告
|
||||||
|
|
||||||
|
第 3 课把**随机数**加进来:点击按钮 → 随机出普通 / 稀有 / 超稀有——这才是真正的抽卡!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*学完请在 `学习进度与状态-GUI.md` 里更新第 2 课的状态。*
|
||||||
|
|
||||||
|
**下一课文档**:`第03课-随机抽卡.md`
|
||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
# 第 3 课:随机抽卡——核心来了
|
||||||
|
|
||||||
|
**本课目标**:点击按钮 → 随机出"普通卡 / 稀有卡 / 超稀有卡",结果显示在窗口里。把命令行抽卡逻辑搬进 GUI。
|
||||||
|
|
||||||
|
**复习**:第 2 课学会了按钮和回调;命令行时期你已掌握 `rand()`、`srand()`、`if/else`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 这一课要搞懂什么
|
||||||
|
|
||||||
|
把以前学过的三样东西组合进一个 GUI 程序:
|
||||||
|
|
||||||
|
| 老朋友 | 在 GUI 里怎么用 |
|
||||||
|
|--------|---------------|
|
||||||
|
| `srand(time(...))` | 在 `main` 开头调一次(和以前一样) |
|
||||||
|
| `rand() % 100` | 放在回调函数里,每次点击执行 |
|
||||||
|
| `if / else if / else` | 放在回调函数里,根据 `r` 决定显示什么 |
|
||||||
|
|
||||||
|
**关键区别**:以前判断完用 `cout` 打印,现在用 `label()` 更新窗口上的文字。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 稀有度与颜色搭配
|
||||||
|
|
||||||
|
本课不仅显示文字,还**用不同颜色**区分稀有度:
|
||||||
|
|
||||||
|
| 随机数 r | 稀有度 | 文字颜色 | 概率 |
|
||||||
|
|----------|--------|---------|------|
|
||||||
|
| 0 ~ 69 | 普通卡 | 灰色 | ~70% |
|
||||||
|
| 70 ~ 94 | 稀有卡 | 蓝色 | ~25% |
|
||||||
|
| 95 ~ 99 | 超稀有卡! | 红色 | ~5% |
|
||||||
|
|
||||||
|
颜色让稀有卡一眼就能看出来——比命令行效果震撼多了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 完整示例程序
|
||||||
|
|
||||||
|
保存为 `lesson_gui_3.cpp`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Window.H>
|
||||||
|
#include <FL/Fl_Button.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <cstdlib> // rand, srand
|
||||||
|
#include <ctime> // time
|
||||||
|
|
||||||
|
// 全局变量
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 新知识点
|
||||||
|
|
||||||
|
### sprintf —— 把数字变成字符串
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
char info[50];
|
||||||
|
std::sprintf(info, "本次点数:%d", r);
|
||||||
|
info_box->label(info);
|
||||||
|
```
|
||||||
|
|
||||||
|
以前你可能会写:
|
||||||
|
```cpp
|
||||||
|
std::cout << "本次点数:" << r << "\n";
|
||||||
|
```
|
||||||
|
|
||||||
|
但 FLTK 的 `label()` 只能接受一个字符串。`sprintf` 把数字塞进字符串里——`%d` 的位置被 `r` 的值替换。
|
||||||
|
|
||||||
|
### fl_rgb_color —— 自定义颜色
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
fl_rgb_color(128, 128, 128) // R=128, G=128, B=128 → 灰色
|
||||||
|
fl_rgb_color(255, 200, 0) // → 金色
|
||||||
|
```
|
||||||
|
|
||||||
|
- R(红)、G(绿)、B(蓝),每个 0~255
|
||||||
|
- 试试不同的数值创造你自己的颜色!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 运行效果
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────── 抽卡世界 - 第3课 ────────┐
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ │
|
||||||
|
│ │ 🎴 抽一张! │ │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ★ 稀有卡 ★ │ ← 蓝色大字
|
||||||
|
│ │
|
||||||
|
│ 本次点数:78 │ ← 小字
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
连续点击多次,观察普通(灰)、稀有(蓝)、超稀有(红)的分布。超稀有大约 20 次出 1 次。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 本课课堂小练习
|
||||||
|
|
||||||
|
1. 调概率:把 `r < 70` 改成 `r < 50`,把 `r < 95` 改成 `r < 85`。看看稀有和超稀有是不是变多了。
|
||||||
|
2. 把"超稀有卡"的颜色改成金色 `fl_rgb_color(255, 200, 0)`。
|
||||||
|
3. 给超稀有卡多加一行"🎉恭喜!"——提示:可以改 `info_box` 的内容。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 本课课后作业(选做)
|
||||||
|
|
||||||
|
- 增加第四个稀有度:"传说卡"(`r >= 99`),颜色用金色,概率约 1%。和旧课程第 5 课的练习类似,但现在是 GUI 版本。
|
||||||
|
- 把按钮文字改成"再抽一次"。
|
||||||
|
- 试着把窗口上所有文字都换成你自己喜欢的卡牌主题(比如"SSR""SR""R"等)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 常见小问题
|
||||||
|
|
||||||
|
- **每次结果都一样**:检查 `srand(time(nullptr))` 是否在 `main` 里、是否只写了一次。写在回调里会导致每次点按钮重置种子。
|
||||||
|
- **缺少 `#include <cstdlib>` 或 `<ctime>`**:报 `rand`/`srand`/`time` 未定义。
|
||||||
|
- **`sprintf` 缓冲区太小**:如果信息很长,`char info[50]` 可以改成 `char info[100]`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 下一课预告
|
||||||
|
|
||||||
|
第 4 课做**十连抽**:加一个"十连抽"按钮,用 `for` 循环一口气生成 10 个结果,全部显示在窗口里。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*学完请在 `学习进度与状态-GUI.md` 里更新第 3 课的状态。*
|
||||||
|
|
||||||
|
**下一课文档**:`第04课-十连抽.md`
|
||||||
+227
@@ -0,0 +1,227 @@
|
|||||||
|
# 第 4 课:十连抽
|
||||||
|
|
||||||
|
**本课目标**:加一个"十连抽"按钮,用 `for` 循环一口气抽 10 次,所有结果一起显示。学会在 GUI 里用循环 + 字符串拼接。
|
||||||
|
|
||||||
|
**复习**:第 3 课单抽;命令行时期你已掌握 `for` 循环和 `string`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 这一课要搞懂什么
|
||||||
|
|
||||||
|
### 命令行 vs GUI 的多抽
|
||||||
|
|
||||||
|
**命令行时代(第 6 课)**:
|
||||||
|
```cpp
|
||||||
|
for (int i = 1; i <= 10; i++) {
|
||||||
|
int r = rand() % 100;
|
||||||
|
cout << "第" << i << "抽:";
|
||||||
|
// ... 判断稀有度 ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
每轮 `cout` 一行,10 行自动排队显示。
|
||||||
|
|
||||||
|
**GUI 时代(本课)**:
|
||||||
|
```cpp
|
||||||
|
for (int i = 1; i <= 10; i++) {
|
||||||
|
int r = rand() % 100;
|
||||||
|
// 不能直接 "cout" → 要把结果攒起来,最后一次性 label
|
||||||
|
}
|
||||||
|
```
|
||||||
|
GUi 的 `label()` 每次都会覆盖旧内容。所以需要**先把 10 行拼成一个大字符串,再一次性显示**。
|
||||||
|
|
||||||
|
### 核心技巧:字符串拼接
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::string result = ""; // 空字符串
|
||||||
|
result += "第1抽:普通\n"; // 追加一行
|
||||||
|
result += "第2抽:稀有\n"; // 再追加
|
||||||
|
// ... 最后 label(result.c_str())
|
||||||
|
```
|
||||||
|
|
||||||
|
`+=` 像胶水,把一段段文字粘在一起。`\n` 是换行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 程序结构
|
||||||
|
|
||||||
|
- 保留"单抽"按钮(第 3 课功能)
|
||||||
|
- 新增"十连抽"按钮 → 调用新的回调函数 `draw_ten_callback`
|
||||||
|
- 两个按钮共用一个结果框
|
||||||
|
|
||||||
|
## 3. 完整示例程序
|
||||||
|
|
||||||
|
保存为 `lesson_gui_4.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) {
|
||||||
|
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"; // 换行
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用 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() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 新知识点
|
||||||
|
|
||||||
|
### `std::to_string(i)` —— 数字变字符串
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
all += "第" + std::to_string(i) + "抽:";
|
||||||
|
```
|
||||||
|
|
||||||
|
`std::to_string(5)` 变成 `"5"`。在旧课程里你用 `cout << i`,GUI 里需要用 `to_string` 把数字变成字符串后才能用 `+` 拼接。
|
||||||
|
|
||||||
|
> 需要 `#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`
|
||||||
+249
@@ -0,0 +1,249 @@
|
|||||||
|
# 第 5 课:卡片名字与布局
|
||||||
|
|
||||||
|
**本课目标**:给每种稀有度配上具体的卡片名字(火龙、冰狼、雷霆龙……),让抽卡真正像游戏。学会把界面分区布局,更整齐好看。
|
||||||
|
|
||||||
|
**复习**:第 3~4 课只显示稀有度等级;命令行时期你已掌握 `std::string` 数组。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 这一课要搞懂什么
|
||||||
|
|
||||||
|
### 从"普通卡"到"小火龙"
|
||||||
|
|
||||||
|
以前的结果:
|
||||||
|
```
|
||||||
|
★ 稀有卡 ★
|
||||||
|
```
|
||||||
|
|
||||||
|
本课的结果:
|
||||||
|
```
|
||||||
|
★ 稀有卡 ★
|
||||||
|
冰霜之狼
|
||||||
|
```
|
||||||
|
|
||||||
|
**设计思路**:每种稀有度维护一个卡片名字数组,随机抽到某档 → 再随机从该档的卡片里挑一张。
|
||||||
|
|
||||||
|
### 卡片表设计
|
||||||
|
|
||||||
|
| 稀有度 | 卡片名数组 | 数量 |
|
||||||
|
|--------|-----------|------|
|
||||||
|
| 普通 | 小火龙、水跃鱼、绿草龟、小电鼠、土拨仔 | 5 张 |
|
||||||
|
| 稀有 | 烈焰飞龙、冰霜之狼、暗影豹、风暴鹰、水晶鹿 | 5 张 |
|
||||||
|
| 超稀有 | 雷霆神龙、凤凰王、深海巨兽 | 3 张 |
|
||||||
|
|
||||||
|
### 界面分区
|
||||||
|
|
||||||
|
本课把窗口分成三个区域,比之前全部控件堆在一起更好看:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 操作区(按钮) │ ← 顶部
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 结果显示区 │ ← 中部大面积
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ 统计/信息区 │ ← 底部
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 完整示例程序
|
||||||
|
|
||||||
|
保存为 `lesson_gui_5.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 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("单抽模式");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 十连抽回调 =====
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 新知识点
|
||||||
|
|
||||||
|
### 多组卡片数组
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::string normal_cards[5] = { "小火龙", "水跃鱼", ... };
|
||||||
|
std::string rare_cards[5] = { ... };
|
||||||
|
std::string super_cards[3] = { ... };
|
||||||
|
```
|
||||||
|
|
||||||
|
- 三组独立数组,每组长度可以不同
|
||||||
|
- 访问:`normal_cards[rand() % 5]` —— 随机选一张普通卡
|
||||||
|
|
||||||
|
### `box()` —— 控件边框样式
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
result_box->box(FL_DOWN_BOX); // 凹陷效果,像一张卡片
|
||||||
|
stats_box->box(FL_FLAT_BOX); // 扁平
|
||||||
|
```
|
||||||
|
|
||||||
|
常用样式:
|
||||||
|
| 样式 | 效果 |
|
||||||
|
|------|------|
|
||||||
|
| `FL_NO_BOX` | 无边框 |
|
||||||
|
| `FL_FLAT_BOX` | 平面(默认) |
|
||||||
|
| `FL_UP_BOX` | 凸起 |
|
||||||
|
| `FL_DOWN_BOX` | 凹陷 |
|
||||||
|
| `FL_EMBOSSED_BOX` | 浮雕 |
|
||||||
|
|
||||||
|
### `color()` —— 控件背景色
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
stats_box->color(fl_rgb_color(230, 230, 230));
|
||||||
|
```
|
||||||
|
|
||||||
|
给控件设置背景颜色。注意:`color()` 是背景色,`labelcolor()` 是文字颜色。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 本课课堂小练习
|
||||||
|
|
||||||
|
1. 把三组卡片名字全部换成你自己喜欢的(宝可梦、数码宝贝、自创的都行)。
|
||||||
|
2. 给超稀有卡多加 2 张,改数组长度和相关 `% N`。
|
||||||
|
3. 试三种不同的 `box()` 样式,看哪个最好看。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 本课课后作业(选做)
|
||||||
|
|
||||||
|
- 加一个"查看图鉴"按钮(先用文字列表显示所有卡片,不要求图片)。提示:把所有卡名拼成一个长字符串显示。
|
||||||
|
- 在单抽结果中,如果抽到超稀有,把 `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`
|
||||||
+361
@@ -0,0 +1,361 @@
|
|||||||
|
# 第 6 课:完整抽卡游戏
|
||||||
|
|
||||||
|
**本课目标**:把前 5 课全部整合,做出一个完整的抽卡游戏——多个按钮、金币系统、累计统计。这是 GUI 教程的最终成果。
|
||||||
|
|
||||||
|
**复习**:前面所有的 FLTK 知识 + 旧课程的全部 C++ 基础。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 最终游戏长什么样
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────── 抽卡大冒险 ──────────┐
|
||||||
|
│ │
|
||||||
|
│ 🎴 单抽 🎴🎴 十连抽 │
|
||||||
|
│ (扣10金币) (扣100金币) │
|
||||||
|
│ │
|
||||||
|
│ ┌── 抽卡结果 ──────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ★ 稀有卡 ★ │ │
|
||||||
|
│ │ 冰霜之狼 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └──────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 累计:普通 12 稀有 5 超稀有 2│
|
||||||
|
│ │
|
||||||
|
│ 金币:350 💰 │
|
||||||
|
│ │
|
||||||
|
│ [ 重置 ] [ 退出 ] │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 游戏规则
|
||||||
|
|
||||||
|
| 操作 | 消耗金币 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 单抽 | 10 金币 | 抽一张卡 |
|
||||||
|
| 十连抽 | 100 金币 | 抽十张,显示汇总 |
|
||||||
|
| 重置 | 0 金币 | 金币回到初始值,统计清零 |
|
||||||
|
| 退出 | 0 金币 | 关闭窗口 |
|
||||||
|
|
||||||
|
初始金币 500,金币不够时不能抽卡,显示提示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 完整示例程序
|
||||||
|
|
||||||
|
保存为 `lesson_gui_6.cpp`。这是教程的最终成果,约 120 行。
|
||||||
|
|
||||||
|
```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 rare_cards[5] = {
|
||||||
|
"烈焰飞龙", "冰霜之狼", "暗影豹", "风暴鹰", "水晶鹿"
|
||||||
|
};
|
||||||
|
std::string super_cards[3] = {
|
||||||
|
"雷霆神龙", "凤凰王", "深海巨兽"
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 游戏状态(全局) =====
|
||||||
|
int coins = 500;
|
||||||
|
int total_normal = 0;
|
||||||
|
int total_rare = 0;
|
||||||
|
int total_super = 0;
|
||||||
|
|
||||||
|
// ===== 全局控件 =====
|
||||||
|
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(); // 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();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 代码架构总结
|
||||||
|
|
||||||
|
回顾这 6 课,你的程序结构从第 1 课的简单窗口,发展到了完整游戏:
|
||||||
|
|
||||||
|
```
|
||||||
|
全局区域
|
||||||
|
├── 卡片数据(数组)
|
||||||
|
├── 游戏状态(金币、统计)
|
||||||
|
├── 全局控件指针
|
||||||
|
│
|
||||||
|
回调函数
|
||||||
|
├── get_card() ← 封装"抽一张卡"的逻辑(函数复用!)
|
||||||
|
├── draw_one_callback() ← 单抽
|
||||||
|
├── draw_ten_callback() ← 十连抽
|
||||||
|
├── reset_callback() ← 重置
|
||||||
|
├── quit_callback() ← 退出
|
||||||
|
│
|
||||||
|
辅助函数
|
||||||
|
├── update_coins_display()
|
||||||
|
├── update_stats_display()
|
||||||
|
│
|
||||||
|
main()
|
||||||
|
├── 创建窗口
|
||||||
|
├── 创建所有控件
|
||||||
|
├── 绑定回调
|
||||||
|
└── Fl::run()
|
||||||
|
```
|
||||||
|
|
||||||
|
注意 `get_card()` 函数——它把"随机 + 判断 + 统计"打包成一个函数,单抽和十连抽都调用它。这就是旧课程第 8 课学的**函数复用**!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 新知识点
|
||||||
|
|
||||||
|
### 封装函数提高复用(旧知识的新应用)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::string get_card(int &rare_level) {
|
||||||
|
// 随机 → 判断 → 统计 → 返回卡名
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `int &rare_level` —— 引用参数,函数可以修改外面的变量
|
||||||
|
- 单抽和十连抽都调同一个函数,不重复代码
|
||||||
|
|
||||||
|
### `return` 提前退出
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
if (coins < 10) {
|
||||||
|
result_box->label("金币不够!...");
|
||||||
|
return; // 直接结束函数,不继续抽卡
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 本课课堂小练习
|
||||||
|
|
||||||
|
1. 调整游戏参数:初始金币改 1000,单抽改 20 金币,看游戏节奏变化。
|
||||||
|
2. 在十连抽结果中增加一行"保底提示",如果没有出超稀有,写一句"再接再厉!"。
|
||||||
|
3. 把卡片名字全部换成你自创的主题(比如三国武将、星座、美食……)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 本课课后作业(选做 / 拓展)
|
||||||
|
|
||||||
|
以下是你可以自己加的进阶功能:
|
||||||
|
|
||||||
|
- **幸运计数器**:统计"已经多少抽没出超稀有了",超过 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