5-2 从查表到控件:造按钮

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 总结

  1. 控件 = 样式 + 功能,本质和上一节的命令表一样。
  2. 清晰的架构非常重要,模块化非常重要。
相关推荐
水饺编程2 小时前
第2章:Linux 基础,解压缩【.tar.gz】压缩包
linux·c语言·汇编
_小许_2 小时前
AI IDE 与 Agent Harness:一次说清 Cursor、Codex 该怎么选
ide·人工智能·ai编程·cursor·codex
恒锐丰科技林技术员2 小时前
EG2106L 高压半桥驱动芯片解析:高性价比 600V 栅极驱动方案
经验分享·嵌入式硬件·硬件工程
cu1433 小时前
细谈IT6653的具体功能与其应用
c语言·c++·单片机·嵌入式硬件
迪康Defender3 小时前
PHP 高危漏洞应急分析:从风险原理到端点侧纵深防护
运维·开发语言·安全·自动化·php·运维开发·远程工作
2601_966949653 小时前
为什么不应该把 QuantDash API Key 直接写进公开代码仓库?从 Git 提交历史理解凭证泄露风险
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
智_永无止境3 小时前
又找到一个纯Java的OCR项目,识别的格式还挺多
java·开发语言·ocr
小灰灰搞电子3 小时前
Rust+Slint 实现动态流光边框源码分享
开发语言·前端·rust·流光边框
threerocks3 小时前
2026 深度盘点:我评估了 369 个 Claude/CodeX AI 视频与自动化剪辑开源项目
aigc·openai·ai编程