5-2 从查表到控件:造按钮
⏱ 本节预计用时:50 分钟
本节意义:上一节预告过"查表的思路在图形化编程里优势巨大"。这一节让我们看看为什么这么说,我会向大家说明白"事件驱动"和"控件"的概念,以及我们将从头造一个控件,按钮,进一步感受查表的威力。
本节目标:
- 理解事件驱动和控件是什么;
- 亲手定义
Button结构体(矩形 + 文字 + 回调),把 5-1 命令表换成按钮表;
1 事件驱动是什么?控件是什么?
在命令行软件里,交互的入口只有一个,你可以写成阻塞式的,用户输入一下程序输出一下,输出完之后就卡在那了,这完全可以,因为用户只能操作那一个地方。
但在图形化软件里就不行,因为界面上有很多东西,你不能一直卡在某一个按钮上,用户不点这个按钮,整个程序都不动,这是错误的。
正确的做法是,持续检测整个界面,看哪里被做了操作,然后执行对应的功能。点击、输入这些操作就叫事件,事件触发后,去调用对应函数执行功能,就叫事件驱动。
举个例子,界面上有很多个按钮,正确的做法是,持续检测所有的按钮,哪个按钮被按下,就去执行对应的功能。按下按钮就是事件。根据按钮是哪个去调用对应函数,就叫事件驱动。
那么在代码里应该怎么写呢?你当然可以像之前井字棋项目里我们做的那样,给每个按钮都起一个名字,然后分别画按钮,在主函数里写成switch、case,点击某个按钮之后,调用对应函数。但就如我们上一讲里说的那样,这样按钮如果有很多的话,主函数里的逻辑会非常臃肿。
更好的写法是,像上一讲查表里写的那样。把按钮的样式和功能绑定在一起,写成一个结构体。将所有按钮能做的功能和按钮的名字注册在一张表里。点击按钮,就根据它的名字查表,去调用对应的函数。
这样我要新增一个按钮的话,只需要做三件事:1. 在表里填上信息。2. 在界面中绘制按钮。3. 写按钮被点击后触发的函数,完事了。
我们把样式和功能绑定在一起的界面组件(比如按钮)就叫做控件。
本质和上一讲的 MenuItem 一模一样:"标题"+ "函数"。灵魂还是函数指针。
c
// 命令表(CLI):一项 = 一行数据,我自己查表
{ "查看所有学生", view_show_all },
// 图形(声明):一个按钮 = 一行数据,控件自己叫我
Button { .text = "查看全部", .on_click = view_show_all }
2 造按钮控件Button
新工程 code/5-2-学生系统-图形版/,把 5-1 的工程整个拷过来,新建文件夹ui。
text
code/5-2-学生系统-图形版/
├── model/
├── controller/
├── util/
├── fileio/
├── view/
├── main.c
└── ui/ ← 新建我们自己的控件库
在ui文件夹里新建 ui.h,定义我们第一个控件:
c
/* ui/ui.h ------ 控件库:谁都能用的"零件" */
typedef struct {
Rectangle rect; /* 长相:位置大小 */
const char *text; /* 长相:按钮文字 */
void (*on_click)(void); /* 行为:被点时就调它 */
} Button;
对比上一讲里的 MenuItem :
c
typedef struct {
const char *title; /* 菜单上显示什么 */
void (*handler)(StudentSystem *); /* 选中它,就调它 */
} MenuItem;
一个存"文字+函数指针",一个也存"文字+函数指针",只是 Button 多了一个矩形容器,handler 的参数从 StudentSystem * 变成了"无参数"(回调要用数据时,直接读它所在模块的全局变量就行,本节先不引入传参),本质是一毛一样的。
ui.h 完整代码,因为raylib默认不支持中文,我把加载中文字库的函数也写在了ui里:
加载字符的具体逻辑不用管,换一个库可能就换一种加载方法,这不是本节重点。重点是掌 握函数指针、回调、查表,体会控件将数据和行为绑定在一起的思想。
#ifndef UI_H
#define UI_H
#include "raylib.h"
#include "../model/student.h"
/* ui/ui.h ------ 控件库 + 文字系统:谁都能用的"零件" */
typedef struct {
Rectangle rect; /* 长相:位置大小 */
const char *text; /* 长相:按钮文字 */
void (*on_click)(void); /* 行为:被点时就调它 */
} Button;
/* 文字系统:字库由 ui 统一加载、持有、绘制(view 只管组合控件,画字都找它) */
void ui_load_font(StudentSystem *sys); /* 加载中文字库(界面要用的字 + 学生姓名) */
Font ui_get_font(void); /* 谁要画字,找它拿字体 */
/* 画一个按钮:画框 + 画文字(文字居中),鼠标压着会变亮 */
void button_draw(Button *b);
/* 这里的 button_clicked 抄的第三章 CheckCollisionPointRec */
int button_clicked(Button *b, Vector2 mouse);
/* 核心!!!!!和上一讲里 menu[choice].handler(&sys) 是一个思想:遍历一张表,谁被点,谁干活*/
void handle_buttons(Button btns[], int count, Vector2 mouse);
#endif
在ui文件夹里新建 ui.c,补全配套函数。
c
#include "ui.h"
#include "raylib.h"
#include <string.h>
/* ========== 文字系统:字库由 ui 统一加载、持有、绘制 ========== */
static Font ui_font; /* 全程序共用的字体 */
void ui_load_font(StudentSystem *sys)
{
/* 1. 把程序里用到的字整理成一个数组,之后就把这个数组里的字加入字库 */
char all_text[4096] = "学生成绩管理系统查看全部添加学生删除学生修改成绩"
"按学号查询按姓名查询统计信息退出"
"学号姓名语文数学英语总分平均分"
"添加保存成绩0123456789.-暂无学生,先去添加页试试"
"占位这里暂时还没东西下一节往里加";
/* 2. 把学生的名字也加入数组里;新加学生后,字库里可能缺新名字的字,要重新调用 ui_load_font */
for (int i = 0; i < sys->count && strlen(all_text) < 4000; i++) {
strcat(all_text, sys->students[i].name);
}
/* 3. 更新字库 */
int cp_count = 0;
int *codepoints = LoadCodepoints(all_text, &cp_count);
Font f = LoadFontEx("C:/Windows/Fonts/simhei.ttf", 20, codepoints, cp_count); /* 加载字体文件,生成 raylib 的字体对象。C:/Windows/Fonts/simhei.ttf 是 Windows 自带黑体的文件。路径因系统而异,找不到就问 AI */
UnloadCodepoints(codepoints);
SetTextureFilter(f.texture, TEXTURE_FILTER_BILINEAR);
if (ui_font.texture.id != 0) UnloadFont(ui_font); /* 旧字库先还回去,不然每重造一次就漏一张图集 */
ui_font = f;
}
Font ui_get_font(void)
{
return ui_font;
}
/* ========== 按钮控件:画框 + 画字 ========== */
void button_draw(Button *b)
{
Color bg = GRAY;
if (CheckCollisionPointRec(GetMousePosition(), b->rect)) {
bg = LIGHTGRAY; /* 鼠标压着:变亮,提示"我可点" */
}
DrawRectangleRec(b->rect, bg);
DrawRectangleLinesEx(b->rect, 1, DARKGRAY);
Vector2 sz = MeasureTextEx(ui_font, b->text, 20, 1); /* MeasureTextEx量字多宽,好让文字居中 */
DrawTextEx(ui_font, b->text,
(Vector2){ b->rect.x + (b->rect.width - sz.x) / 2,
b->rect.y + (b->rect.height - 20) / 2 },
20, 1, BLACK); /* 文字水平垂直居中 */
}
/* 这里的 button_clicked 抄的第三章 CheckCollisionPointRec */
int button_clicked(Button *b, Vector2 mouse)
{
return CheckCollisionPointRec(mouse, b->rect); /* 点没点在框里 */
}
/* 核心!!!!!和上一讲里 menu[choice].handler(&sys) 是一个思想:遍历一张表,谁被点,谁干活*/
void handle_buttons(Button btns[], int count, Vector2 mouse)
{
for (int i = 0; i < count; i++) {
if (button_clicked(&btns[i], mouse)) {
if (btns[i].on_click) {
btns[i].on_click(); /* 反手拨号 */
}
return;
}
}
}
3 从命令表到按钮列表
我们先想一下要做什么事情:
1.view里得增加新的绘制函数,要把按钮和信息画到屏幕上。
2.controller得重写,因为流程控制函数flow_xxx里也有大量的printf,我们得让原本呈现在终端里的字呈现在窗口里。
3.main得重写,因为view、controller都重写了;而且把功能都给按钮之后,表也得重做。
ok,让我们来一件一件办吧。先做一个最简的示例:只加入查看学生功能,整个流程跑通之后再一步步的加入所有功能。
这一块难度上升了,咱不急,可以先复制跑通,之后看不懂的地方问ai,慢慢看。
第一步:更新view
绘制不是本节重点,直接复制粘贴,大致知道画啥就行。重点是掌握函数指针、回调、查表,体会控件将数据和行为绑定在一起的思想。
更新 view.h:
#ifndef VIEW_H
#define VIEW_H
#include "../model/student.h"
#include "../ui/ui.h"
/* view/view.h ------ 画师:只管组合控件、决定画什么。画字找 ui 要字体(ui_get_font) */
void view_draw_title(void); /* 画标题 */
void view_draw_menus(Button *btns); /* 画按钮列 */
void view_draw_student_list(StudentSystem *sys); /* 画列表 */
void view_draw_placeholder(void); /* 画占位 */
#endif
在 view.c 中加入函数具体的实现:
c
/* view/view.c ------ 画师:只管组合控件。字库和画字都找 ui(ui_get_font) */
#include "view.h"
#include "raylib.h"
#include "../ui/ui.h"
/* 画标题 */
void view_draw_title(void)
{
DrawTextEx(ui_get_font(), "学生成绩管理系统", (Vector2){ 40, 20 }, 32, 2, DARKBLUE);
}
/* 画菜单按钮列:怎么画是 ui(button_draw)的事,怎么摆是 view 的事 */
void view_draw_menus(Button *btns)
{
for (int i = 0; i < 8; i++) button_draw(&btns[i]);
}
/* 画学生列表:把第四章 view_show_all 的 printf 表格,换成窗口里的表格 */
void view_draw_student_list(StudentSystem *sys)
{
Font f = ui_get_font();
int col_id = 12, col_name = 100, col_s1 = 200, col_s2 = 270, col_s3 = 340, col_avg = 410;
if (sys->count == 0) {
DrawTextEx(f, "暂无学生,先去添加页试试", (Vector2){ 330, 260 }, 20, 1, GRAY);
return;
}
int y = 170; /* 表头 */
DrawTextEx(f, "学号", (Vector2){ 330 + col_id, y }, 18, 1, DARKGRAY);
DrawTextEx(f, "姓名", (Vector2){ 330 + col_name, y }, 18, 1, DARKGRAY);
DrawTextEx(f, "语文", (Vector2){ 330 + col_s1, y }, 18, 1, DARKGRAY);
DrawTextEx(f, "数学", (Vector2){ 330 + col_s2, y }, 18, 1, DARKGRAY);
DrawTextEx(f, "英语", (Vector2){ 330 + col_s3, y }, 18, 1, DARKGRAY);
DrawTextEx(f, "平均分", (Vector2){ 330 + col_avg, y }, 18, 1, DARKGRAY);
DrawLine(330, y + 24, 860, y + 24, LIGHTGRAY);
y += 38;
for (int i = 0; i < sys->count && i < 11; i++) { /* 先画前 11 个(5-3 换滚动) */
Student *s = &sys->students[i];
DrawTextEx(f, TextFormat("%d", s->id), (Vector2){ 330 + col_id, y }, 18, 1, BLACK);
DrawTextEx(f, s->name, (Vector2){ 330 + col_name, y }, 18, 1, BLACK);
DrawTextEx(f, TextFormat("%.1f", s->scores[0]), (Vector2){ 330 + col_s1, y }, 18, 1, BLACK);
DrawTextEx(f, TextFormat("%.1f", s->scores[1]), (Vector2){ 330 + col_s2, y }, 18, 1, BLACK);
DrawTextEx(f, TextFormat("%.1f", s->scores[2]), (Vector2){ 330 + col_s3, y }, 18, 1, BLACK);
DrawTextEx(f, TextFormat("%.1f", s->average), (Vector2){ 330 + col_avg, y }, 18, 1, BLACK);
y += 26;
}
}
/* 占位页(下一节往里加内容) */
void view_draw_placeholder(void)
{
DrawTextEx(ui_get_font(), "占位,这里暂时还没东西,下一节往里加",
(Vector2){ 380, 260 }, 24, 1, GRAY);
}
view_draw_menus 由一堆button_draw 组成。按钮单个怎么画是 ui 的事(零件),一列按钮怎么摆是 view 的事(整体布局)。
第二步:更新 controller
开始上难度。想一下要写哪几个函数?
首先,退出函数要写,我们写一个 cmd_exit 函数,这里的 cmd 是命令的意思,意思是按下按钮,就触发这个回调。退出需要用到 quit 标志位,来通知主循环结束。
点击按钮后要进入对应页面,在controller要写配套函数,当前我们只能查看学生,写进入查看页面的函数 cmd_show_all。点其他按钮,也得切换到占位页面,写进入占位页面的函数 cmd_placeholder。
考虑到主循环里渲染和更新阶段是分开的, cmd_show_all 和 cmd_placeholder 是点下按钮,触发回调,属于更新阶段,里边不能直接写绘制界面的函数,所以也是用页面标志位 typedef enum { VIEW_LIST, VIEW_HOLDER } ViewId; 来通知渲染阶段画哪个界面。
之前的东西我们暂时不用,全删掉(或注释掉),之后我们要用时再拷。
controller.h :
#ifndef CONTROLLER_H
#define CONTROLLER_H
#include "../model/student.h"
extern StudentSystem *g_sys;
extern int quit;
/* 图形版的"当前页面枚举":main 画内容区时按它决定画哪页 */
typedef enum { VIEW_LIST, VIEW_HOLDER } ViewId; /* 当前显示哪一页 */
extern ViewId cur_view;
void cmd_show_all(void);
void cmd_placeholder(void); /* 其它功能先占位,后面一步步接 */
void cmd_exit(void);
#endif
controller.c :
c
/* controller/controller.c ------ 问输入 → 调 model → 按状态码翻译成人话 */
#include <stdio.h>
#include "../fileio/fileio.h"
#include "controller.h"
int quit = 0;
ViewId cur_view = VIEW_LIST;
StudentSystem *g_sys; /* 命令们要用的"大家庭" */
void cmd_show_all(void)
{
cur_view = VIEW_LIST; /* 进入列表页 */
}
void cmd_placeholder(void) /* 其它功能先占位,后面一步步接 */
{
cur_view = VIEW_HOLDER;
}
void cmd_exit(void)
{
save_system(g_sys, "students.dat");
printf("数据已保存,再见!\n");
quit = 1;
}
第三步:更新 main:从命令表到按钮表!
我们得重新做表,原来 menu[] 存的是"菜单名 + handler",现在存的是"按钮 + 回调",循环换成窗口循环(因为在循环里一直在等待事件,所以也叫事件循环)。
c
/* main.c ------ 指挥:开机要地、启动加载、退出保存并还地 */
#include <stdio.h>
#include "raylib.h"
#include "model/student.h"
#include "ui/ui.h"
#include "view/view.h"
#include "fileio/fileio.h"
#include "controller/controller.h"
Button menu_btns[8];
/* 摆按钮:把 5-1 的命令表换成按钮版的 */
void setup_buttons(void)
{
const char *titles[8] = { "查看全部", "添加学生", "删除学生", "修改成绩",
"按学号查询", "按姓名查询", "统计信息", "退出" };
for (int i = 0; i < 8; i++) {
menu_btns[i].rect = (Rectangle){ 40, 150 + i * 50, 250, 42 };
menu_btns[i].text = titles[i];
menu_btns[i].on_click = (i == 7) ? cmd_exit : cmd_placeholder;
}
menu_btns[0].on_click = cmd_show_all; /* 查看全部 */
}
int main(void)
{
StudentSystem sys;
if (!system_init(&sys)) { printf("初始化失败:内存不够。\n"); return 1; }
if (load_system(&sys, "students.dat")) printf("已加载 %d 名学生。\n", sys.count);
g_sys = &sys;
InitWindow(900, 600, "学生成绩管理系统");
SetTargetFPS(60);
ui_load_font(&sys); /* 字库:文字系统归 ui */
setup_buttons();
while (!WindowShouldClose() && !quit)
{
/* 1. 更新阶段 */
Vector2 mouse = GetMousePosition();
bool clicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
if (clicked) handle_buttons(menu_btns, 8, mouse); /* 查按钮:谁被点谁干活 */
/* 2. 渲染阶段 */
BeginDrawing();
ClearBackground(RAYWHITE);
view_draw_title();
view_draw_menus(menu_btns);
/* 画哪一页?看 controller 说的:点了"查看全部"就列表页,点了其它就占位 */
if (cur_view == VIEW_LIST)
view_draw_student_list(&sys);
else
view_draw_placeholder();
EndDrawing();
}
CloseWindow();
system_destroy(&sys);
return 0;
}
全部改完了,编译:
命令大概是这样,根据你的文件和库路径做修改:
powershell
gcc main.c ui/ui.c view/view.c model/student.c fileio/fileio.c controller/controller.c -o student_mgr_gui.exe -I ../raylib/include -L ../raylib/lib -lraylib -lopengl32 -lgdi32 -lwinmm
运行:


成功跑通!
4 精简main、controller重命名为app
现在的main太臃肿了,我们只把"开机准备"和"主循环"的骨架放main里,其它全部搬到流程控制里。
这里我们把 controller/ 重命名为 app/ ,图形项目里习惯把代码里流程控制的部分叫应用层(app 层),我们遵循一般命名习惯(和之前controller本质没区别)。
在这一步,我们将做出大刀阔斧的改动,放一首燃的音乐吧!app只给main暴露出应该暴露的接口:初始化、更新、渲染、退出。分配任务和决定显示哪个界面的脏活全部在内部处理完。
app.h :
c
#ifndef APP_H
#define APP_H
#include "raylib.h"
#include "../model/student.h"
#include "../ui/ui.h"
/* app 层:本应用"流程怎么走"的家
* 第四章的 controller 装的是 scanf 流程函数(flow_xxx);
* 图形版 scanf 退役,流程改由「命令们 + 摆按钮 + 响应点击」接管,
* 名字因图形工程的习惯改成 app(控制器/应用层),作用和 controller 一样。
*/
typedef enum { VIEW_LIST, VIEW_HOLDER } ViewId; /* 界面状态:当前显示哪一页 */
void app_init(StudentSystem *sys, Button *menu_btns); /* 开机接线 */
void app_handle_input(void); /* 响应点击 */
void app_draw(void); /* 编排本帧画什么 */
int app_should_quit(void); /* 用户点退出了吗 */
#endif
app.c :
c
/* app/app.c ------ 应用层:命令们 + 摆按钮 + 响应点击(流程的家) */
#include <stdio.h>
#include "app.h"
#include "../view/view.h"
#include "../fileio/fileio.h"
/* ========== 全局状态(本应用只有一个实例,全局最省事) ========== */
static int quit = 0;
static ViewId cur_view = VIEW_LIST;
static StudentSystem *g_sys; /* main 创建的"大家庭" */
static Button *g_btns; /* main 的按钮数组,这边持有操作 */
/* ========== 命令们:CLI 的 flow_xxx 在图形版"改名换姓" ========== */
static void cmd_show_all(void) { cur_view = VIEW_LIST; }
static void cmd_placeholder(void) { cur_view = VIEW_HOLDER; }
static void cmd_exit(void)
{
save_system(g_sys, "students.dat");
printf("数据已保存,再见!\n");
quit = 1;
}
/* 摆按钮:把 5-1 的命令表"画"成左边一列按钮 */
static void setup_buttons(Button *menu_btns)
{
const char *titles[8] = { "查看全部", "添加学生", "删除学生", "修改成绩",
"按学号查询", "按姓名查询", "统计信息", "退出" };
for (int i = 0; i < 8; i++) {
menu_btns[i].rect = (Rectangle){ 40, 150 + i * 50, 250, 42 };
menu_btns[i].text = titles[i];
menu_btns[i].on_click = (i == 7) ? cmd_exit : cmd_placeholder;
}
menu_btns[0].on_click = cmd_show_all; /* 查看全部 */
}
/* ========== app 对外接口 ========== */
void app_init(StudentSystem *sys, Button *menu_btns)
{
g_sys = sys;
g_btns = menu_btns;
setup_buttons(menu_btns);
}
void app_handle_input(void)
{
Vector2 mouse = GetMousePosition();
bool clicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
if (clicked) handle_buttons(g_btns, 8, mouse); /* 谁被点,谁干活 */
}
/* 只负责"本帧画什么"的编排,具体的画法都交给 view */
void app_draw(void)
{
BeginDrawing();
ClearBackground(RAYWHITE);
view_draw_title();
view_draw_menus(g_btns);
if (cur_view == VIEW_HOLDER) {
view_draw_placeholder();
} else {
view_draw_student_list(g_sys);
}
EndDrawing();
}
int app_should_quit(void) { return quit; }
此时的 main.c 变成了这样,牛逼吧:
c
/* main.c ------ 门面:开门、营业、打烊 */
#include <stdio.h>
#include "raylib.h"
#include "model/student.h"
#include "ui/ui.h"
#include "app/app.h"
#include "view/view.h"
#include "fileio/fileio.h"
int main(void)
{
StudentSystem sys;
if (!system_init(&sys)) { printf("初始化失败:内存不够。\n"); return 1; }
if (load_system(&sys, "students.dat")) {
printf("已加载 %d 名学生。\n", sys.count);
}
InitWindow(900, 600, "学生成绩管理系统");
SetTargetFPS(60);
ui_load_font(&sys); /* 字库:文字系统归 ui */
Button menu_btns[8];
app_init(&sys, menu_btns); /* 接线:把命令表注册成按钮,剩下的交给 app */
while (!WindowShouldClose() && !app_should_quit())
{
app_handle_input(); /* 更新:谁被点,谁干活 */
app_draw(); /* 渲染:本帧画什么(app 编排、view 画) */
}
CloseWindow();
system_destroy(&sys);
return 0;
}
编译运行:
gcc main.c ui/ui.c view/view.c model/student.c fileio/fileio.c app/app.c -o student_mgr_gui.exe -I ../raylib/include -L ../raylib/lib -lraylib -lopengl32 -lgdi32 -lwinmm
跑通!

还不够!把窗口设置、分配内存、加载学生这些脏活也全放在 app_init 里,按钮声明也直接放在app里,再次压缩main。
此时的 app.c (app.h不用改):
c
/* app/app.c ------ 应用层:命令们 + 摆按钮 + 响应点击(流程的家) */
#include <stdio.h>
#include "app.h"
#include "../view/view.h"
#include "../fileio/fileio.h"
/* ========== 全局状态(本应用只有一个实例,全局最省事) ========== */
static int quit = 0;
static ViewId cur_view = VIEW_LIST;
static StudentSystem *g_sys; /* 数据(main 给过来的"大家庭") */
static Button menu_btns[8]; /* 按钮数组:app 内部私有,不往外暴露 */
/* ========== 命令们:CLI 的 flow_xxx 在图形版"改名换姓" ========== */
static void cmd_show_all(void) { cur_view = VIEW_LIST; }
static void cmd_placeholder(void) { cur_view = VIEW_HOLDER; }
static void cmd_exit(void)
{
save_system(g_sys, "students.dat");
printf("数据已保存,再见!\n");
quit = 1;
}
/* 摆按钮:把 5-1 的命令表"画"成左边一列按钮 */
static void setup_buttons(void)
{
const char *titles[8] = { "查看全部", "添加学生", "删除学生", "修改成绩",
"按学号查询", "按姓名查询", "统计信息", "退出" };
for (int i = 0; i < 8; i++) {
menu_btns[i].rect = (Rectangle){ 40, 150 + i * 50, 250, 42 };
menu_btns[i].text = titles[i];
menu_btns[i].on_click = (i == 7) ? cmd_exit : cmd_placeholder;
}
menu_btns[0].on_click = cmd_show_all; /* 查看全部 */
}
/* ========== app 对外接口 ========== */
/* 一切开机准备都在这:数据、窗口、字库、按钮 */
void app_init(StudentSystem *sys)
{
g_sys = sys;
system_init(sys);
if (load_system(sys, "students.dat")) {
printf("已加载 %d 名学生。\n", sys->count);
}
InitWindow(900, 600, "学生成绩管理系统");
SetTargetFPS(60);
ui_load_font(sys); /* 字库:文字系统归 ui */
setup_buttons();
}
void app_handle_input(void)
{
Vector2 mouse = GetMousePosition();
bool clicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
if (clicked) handle_buttons(menu_btns, 8, mouse); /* 谁被点,谁干活 */
}
/* 只负责"本帧画什么"的编排,具体的画法都交给 view */
void app_draw(void)
{
BeginDrawing();
ClearBackground(RAYWHITE);
view_draw_title();
view_draw_menus(menu_btns);
if (cur_view == VIEW_HOLDER) {
view_draw_placeholder();
} else {
view_draw_student_list(g_sys);
}
EndDrawing();
}
int app_should_quit(void) { return quit; }
此时的 main.c:
/* main.c ------ 终极形态:只有主循环 */
#include "raylib.h"
#include "model/student.h"
#include "app/app.h"
int main(void)
{
StudentSystem sys;
app_init(&sys); /* 一切开机准备都在这(数据、窗口、字库、按钮) */
while (!WindowShouldClose() && !app_should_quit())
{
app_handle_input(); /* 更新 */
app_draw(); /* 渲染 */
}
CloseWindow();
system_destroy(&sys);
return 0;
}
编译运行,一切正常:

这也是图形工程中main常见的样子:只有主循环,其余全调用app。
开机调一次 app_init,然后一直转主循环,更新、渲染、更新、渲染...知道用户点退出的时候才退出。具体软件长什么样、点什么按钮干什么,main 一概不知。
现在的代码,用户想去改界面就去找view,想去改底层数据就去找model,想去改调用关系就去找app,只用改一处,别的完全不用动。我井字棋里main.c写成这样了,util里校验数据写成这样了,换在贪吃蛇里能不能用?一样能用。这就是模块化的威力,便于修改,便于复用,自己看着爽,别人也看着舒服。
5 总结
- 控件 = 样式 + 功能,本质和上一节的命令表一样。
- 清晰的架构非常重要,模块化非常重要。