云计算百科
云计算领域专业知识百科平台

CStackGUI 教学示例使用说明

文章目录

  • 一、五课一览
  • 二、五分钟上手
    • 2.1 编译(三种方式,选一种)
    • 2.2 运行 / 自检
    • 2.3 想改界面
  • 三、每课详解
    • 入门 01:编辑框和按钮
    • 入门 02:常用输入控件
    • 入门 03:事件进阶(时钟 / 键盘 / 鼠标 / 关闭)
    • 入门 04:数据控件(列表框 / 树 / 表格)
    • 入门 05:待办清单(综合练习)
  • 四、通用知识(每一课都用到)
    • 4.1 一条程序的骨架
    • 4.2 事件名错了为什么不报编译错
    • 4.3 排错:先看日志文件
    • 4.4 编译 / 验证命令速查
  • 五、常见问题
  • 六、学完之后看什么

这套示例(
examples\\lesson01_edit_button ~
examples\\lesson05_todo)是一份
从零开始的学习路线,
每一课只讲一个主题、只写 100~300 行 C 代码,都能独立编译、独立运行。

学完这五课,你就能自己用画布 + C 写出一个小工具。想直接看"大而全"的参考,跳到最后的 学完之后看什么。

目标读者:会一点 C,没写过 Windows 界面的人。 前置条件:装好 MinGW-w64 gcc(默认找 C:\\mingw64\\bin),在仓库根目录执行命令。


一、五课一览

课目录讲什么关键 API
入门 01 examples\\lesson01_edit_button 编辑框 + 按钮:最小的一次「读输入、改界面」 cg_find cg_widget_text cg_widget_set_text cg_widget_set_enabled cg_str_format/cg_free
入门 02 examples\\lesson02_controls 六种常用输入控件的读与写,以及容器相对坐标 cg_widget_value cg_widget_checked cg_widget_list_item cg_widget_set_value
入门 03 examples\\lesson03_events 窗口级事件:时钟 / 键盘 / 鼠标 / 关闭拦截 app.events cg_widget_set_period cg_event_x/y/key/keys cg_cancel_close cg_app_has_window
入门 04 examples\\lesson04_data 数据控件:列表框 / 树 / 表格的增删查选 cg_widget_list_* cg_tree_* cg_table_* cg_status_setf
入门 05 examples\\lesson05_todo 综合练习:待办清单(菜单 + 文件 + 配置) cg_menu_last_id cg_file_read/write cg_ini_read/write 通用对话框 cg_log

下面是五课的学习路线图,可以看到每一课如何层层递进:

#mermaid-svg-rkle9nKpFDhbIb15{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rkle9nKpFDhbIb15 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rkle9nKpFDhbIb15 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rkle9nKpFDhbIb15 .error-icon{fill:#552222;}#mermaid-svg-rkle9nKpFDhbIb15 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rkle9nKpFDhbIb15 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rkle9nKpFDhbIb15 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rkle9nKpFDhbIb15 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rkle9nKpFDhbIb15 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rkle9nKpFDhbIb15 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rkle9nKpFDhbIb15 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rkle9nKpFDhbIb15 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rkle9nKpFDhbIb15 .marker.cross{stroke:#333333;}#mermaid-svg-rkle9nKpFDhbIb15 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rkle9nKpFDhbIb15 p{margin:0;}#mermaid-svg-rkle9nKpFDhbIb15 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-rkle9nKpFDhbIb15 .cluster-label text{fill:#333;}#mermaid-svg-rkle9nKpFDhbIb15 .cluster-label span{color:#333;}#mermaid-svg-rkle9nKpFDhbIb15 .cluster-label span p{background-color:transparent;}#mermaid-svg-rkle9nKpFDhbIb15 .label text,#mermaid-svg-rkle9nKpFDhbIb15 span{fill:#333;color:#333;}#mermaid-svg-rkle9nKpFDhbIb15 .node rect,#mermaid-svg-rkle9nKpFDhbIb15 .node circle,#mermaid-svg-rkle9nKpFDhbIb15 .node ellipse,#mermaid-svg-rkle9nKpFDhbIb15 .node polygon,#mermaid-svg-rkle9nKpFDhbIb15 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rkle9nKpFDhbIb15 .rough-node .label text,#mermaid-svg-rkle9nKpFDhbIb15 .node .label text,#mermaid-svg-rkle9nKpFDhbIb15 .image-shape .label,#mermaid-svg-rkle9nKpFDhbIb15 .icon-shape .label{text-anchor:middle;}#mermaid-svg-rkle9nKpFDhbIb15 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rkle9nKpFDhbIb15 .rough-node .label,#mermaid-svg-rkle9nKpFDhbIb15 .node .label,#mermaid-svg-rkle9nKpFDhbIb15 .image-shape .label,#mermaid-svg-rkle9nKpFDhbIb15 .icon-shape .label{text-align:center;}#mermaid-svg-rkle9nKpFDhbIb15 .node.clickable{cursor:pointer;}#mermaid-svg-rkle9nKpFDhbIb15 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rkle9nKpFDhbIb15 .arrowheadPath{fill:#333333;}#mermaid-svg-rkle9nKpFDhbIb15 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rkle9nKpFDhbIb15 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rkle9nKpFDhbIb15 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rkle9nKpFDhbIb15 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rkle9nKpFDhbIb15 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rkle9nKpFDhbIb15 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rkle9nKpFDhbIb15 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rkle9nKpFDhbIb15 .cluster text{fill:#333;}#mermaid-svg-rkle9nKpFDhbIb15 .cluster span{color:#333;}#mermaid-svg-rkle9nKpFDhbIb15 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rkle9nKpFDhbIb15 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rkle9nKpFDhbIb15 rect.text{fill:none;stroke-width:0;}#mermaid-svg-rkle9nKpFDhbIb15 .icon-shape,#mermaid-svg-rkle9nKpFDhbIb15 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rkle9nKpFDhbIb15 .icon-shape p,#mermaid-svg-rkle9nKpFDhbIb15 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rkle9nKpFDhbIb15 .icon-shape .label rect,#mermaid-svg-rkle9nKpFDhbIb15 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rkle9nKpFDhbIb15 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rkle9nKpFDhbIb15 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rkle9nKpFDhbIb15 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

入门 01:编辑框和按钮最小闭环

入门 02:常用输入控件广度

入门 03:事件进阶深度

入门 04:数据控件数据

入门 05:待办清单交付小工具

examples\\\\gallery控件字典

examples\\otepad完整参考

二、五分钟上手

2.1 编译(三种方式,选一种)

:: 方式 1:全部编译 + 自动跑一遍自检(推荐第一次这么做)
build-all.bat

:: 方式 2:只编译某一课
build-app.bat lesson01_edit_button

:: 方式 3:双击某一课目录里的 build-app.bat(会自己找到仓库根,整个目录拷走也能编)

刷新运行时库、编译、链接都由脚本处理;产物就在课自己的目录里:

examples\\lesson01_edit_button\\
lesson01_edit_button.exe <- 双击就能跑(界面文件就在旁边)
lesson01_edit_button.cgui <- 界面:用画布打开改,改完不用重编译
main.c <- 业务:你要读、要改的就是这个文件
lib\\ <- 运行时库副本 + 中间产物
build-app.bat <- 双击即可重新编译这一课

2.2 运行 / 自检

examples\\lesson01_edit_button\\lesson01_edit_button.exe :: 双击也行
examples\\lesson01_edit_button\\lesson01_edit_button.exe –selftest examples\\lesson01_edit_button\\lesson01_edit_button.cgui

–selftest 不开窗,只做三件事:解析 .cgui → 打印控件树 → 把每个绑定了回调的事件 真的合成调一次。它是唯一能自动发现「事件名写错了」的手段(事件按名字查表, 名字写错编译不会报错):

  • 返回 0:事件名和 C 函数全对得上;
  • 返回 5:有名字在 C 侧没 CG_BIND(输出里会列出是哪个控件、哪个事件);
  • 返回 2 / 3:界面文件找不到 / 格式有错。

改完 C 代码后跑一次 –selftest,是这套框架里性价比最高的习惯。

2.3 想改界面

用画布(designer\\release\\CStackGUI.exe 或 npm run dev)打开那一课的 .cgui, 改完 Ctrl+S,直接重跑 exe 就能看到效果,不用重新编译 C。 .cgui 是运行时读的,这是本框架最省事的地方。


三、每课详解

入门 01:编辑框和按钮

在这里插入图片描述

一句话:所有 CStackGUI 程序的骨架,都在这一课里了。

界面:一个提示标签、一个单行编辑框、两个按钮(打招呼 / 清空)、一个结果标签、一个状态栏。

回调(3 个)

事件回调干什么
编辑框 change on_input_change 每敲一个字来一次:更新状态栏、决定「打招呼」按钮能不能点
编辑框 submit on_hello_submit 在编辑框里按回车 == 点按钮
两个按钮 click on_button_click 共用一个回调,用 cg_widget_id(self) 分辨是谁被点的

三个要点

  • 回调的第一步永远是 cg_app_of(self):从"被点的控件"拿到应用,再 cg_find(app, "id") 去找别的控件。cg_find 找不到返回 NULL,大部分 cg_widget_* 函数对 NULL 是安全的(安静跳过), 所以不用到处判空。
  • 程序改控件不会触发 change 回调。点「清空」时 cg_widget_set_text(edt, "") 不会让 编辑框发出 change,所以你必须在清空之后自己把按钮状态同步一遍(代码里有注释标出这一点)。 这条规则是故意的:否则"回调改控件"会自己套自己。
  • cg_str_format 返回的是堆内存,用完必须 cg_free。想拼不定长文字就用它; 长度可控(char buf[128] 够)时用 snprintf 更省事。
  • 动手改一改

    • 把结果标签的 style.fg 换成别的颜色(画布属性面板,或直接改 .cgui 里的 #6750A4)。
    • 加第三个按钮「复制结果」,回调里用 cg_clip_set_text() 把结果写进剪贴板。
    • 给编辑框加 maxlength?—— .cgui 没有这个字段,在 on_input_change 里手动截断试试。

    /**
    * @file main.c
    * @brief 入门 01:编辑框 + 按钮 —— 最小的一次「读输入、改界面」
    * @details 这一个例子只讲三件事,也是所有 CStackGUI 程序的共同骨架:
    *
    * 1. `.cgui` 里给控件起 id(这里是 edt_name / btn_hello / lab_result);
    * 2. `.cgui` 的事件格里填 **C 函数名**,C 侧用 CG_BIND 把名字接到函数上;
    * 3. 回调里用 cg_find(app, "id") 找到别的控件,读它 / 改它。
    *
    * 顺带演示四个新手最常问的点:
    * – 两个按钮可以共用一个回调,用 cg_widget_id(self) 分辨是谁被点的;
    * – 程序改控件(set_text / set_enabled)**不会**触发 change 回调,
    * 所以界面状态要自己同步(见 on_button_click 的「清空」分支);
    * – cg_str_format 返回的字符串是堆内存,用完必须 cg_free;
    * – 想弹消息框的地方先问一句 cg_app_has_window(),否则 –selftest 会被弹窗卡住。
    *
    * 界面在 lesson01_edit_button.cgui 里;本文件只有行为。
    * 编译:在项目根跑 build-app.bat lesson01_edit_button
    */

    #include "cstackgui.h"

    #include <string.h>

    /* ———————————————————— 业务状态
    * 例子只有一个计数器,但它放在"文件作用域的静态变量"里 —— 这是 C 里最省事的做法:
    * 回调随时能拿到。状态多的项目就定义一个结构体(见 examples\\notepad)。
    */

    static int s_hello_count;

    /** @brief 数有几个字(UTF-8 编码下续字节不算一个字,这样中文按"字"算而不是按字节) */
    static int count_chars(const char *s)
    {
    int n = 0;

    if (!s) return 0;
    for (; *s; s++) {
    if (((unsigned char)*s & 0xC0) != 0x80) n++; /* 10xxxxxx 是续字节 */
    }
    return n;
    }

    /** @brief 把状态栏第一段改成一句话(控件找不到就安静跳过,别崩) */
    static void set_status(cg_app_t *app, const char *text)
    {
    cg_status_set(cg_find(app, "sb_main"), 0, text);
    }

    /**
    * @brief 打招呼:按钮 click 和输入框 submit 共用这一份逻辑
    * @param app 所属应用
    */

    static void say_hello(cg_app_t *app)
    {
    cg_widget_t *edt = cg_find(app, "edt_name");
    cg_widget_t *lab = cg_find(app, "lab_result");
    const char *name = cg_widget_text(edt); /* 控件当前的文字 */

    if (!name || !*name) {
    cg_widget_set_text(lab, "先在上面输入一个名字吧。");
    return;
    }

    s_hello_count++;
    {
    /* cg_str_format 按 printf 规则拼串,返回堆内存 —— 传出去之后要还回去 */
    char *msg = cg_str_format("你好,%s!\\n\\n这是第 %d 次打招呼。", name, s_hello_count);
    cg_widget_set_text(lab, msg);
    cg_free(msg);
    }
    cg_log(CG_LOG_INFO, "打招呼:%s(第 %d 次)", name, s_hello_count);
    cg_status_setf(cg_find(app, "sb_main"), 0, "已经打了 %d 次招呼", s_hello_count);
    }

    /**
    * @brief 输入框内容变了(每敲一个字都会来一次)
    * @param self 输入框
    * @param ev change
    * @param user 未使用
    */

    static void on_input_change(cg_widget_t *self, cg_event_t ev, void *user)
    {
    cg_app_t *app = cg_app_of(self); /* 回调里第一步:从控件拿到应用 */
    int n = count_chars(cg_widget_text(self));

    (void)ev;
    (void)user;

    /* 「能不能点」由业务决定:没输入名字时按钮直接禁用,用户一眼就知道要先填东西 */
    cg_widget_set_enabled(cg_find(app, "btn_hello"), n > 0);
    if (n > 0) cg_status_setf(cg_find(app, "sb_main"), 0, "已输入 %d 个字", n);
    else set_status(app, "等待输入");
    }

    /**
    * @brief 两个按钮共用的点击回调:用 cg_widget_id() 分辨是谁
    * @param self 被点的按钮
    * @param ev click
    * @param user 未使用
    */

    static void on_button_click(cg_widget_t *self, cg_event_t ev, void *user)
    {
    cg_app_t *app = cg_app_of(self);
    const char *id = cg_widget_id(self);

    (void)ev;
    (void)user;

    if (cg_str_equal(id, "btn_hello")) {
    say_hello(app);
    } else if (cg_str_equal(id, "btn_clear")) {
    cg_widget_set_text(cg_find(app, "edt_name"), "");
    cg_widget_set_text(cg_find(app, "lab_result"), "(结果显示在这里)");
    /* 注意:上面那句 set_text **不会**触发 on_input_change(运行时故意屏蔽了
    * "程序改控件"产生的通知),所以这里自己把按钮再禁用一次,不然会出现
    * "框空了按钮还是亮的"。 */

    cg_widget_set_enabled(cg_find(app, "btn_hello"), 0);
    set_status(app, "已清空");
    cg_log(CG_LOG_INFO, "清空输入");
    }
    }

    /**
    * @brief 在输入框里按了回车(submit 事件)
    * @param self 输入框
    * @param ev submit
    * @param user 未使用
    */

    static void on_hello_submit(cg_widget_t *self, cg_event_t ev, void *user)
    {
    (void)ev;
    (void)user;
    say_hello(cg_app_of(self));
    }

    /** @brief 界面建好之后调一次:把"初始状态"设成和业务一致 */
    static void on_ready(cg_app_t *app, void *user)
    {
    (void)user;
    cg_widget_set_enabled(cg_find(app, "btn_hello"), 0); /* 一开始没输入,按钮先禁用 */
    cg_log(CG_LOG_INFO, "界面就绪,共 %d 个控件", cg_widget_count(app));
    }

    /* ———————————————————— 绑定表
    * 左边是 .cgui 的事件格里写的名字,右边是 C 函数。
    * 名字对不上编译不报错(运行时按名字查表),靠 –selftest 兜底。
    */

    static const cg_binding_t k_bindings[] = {
    CG_BIND("on_input_change", on_input_change),
    CG_BIND("on_button_click", on_button_click),
    CG_BIND("on_hello_submit", on_hello_submit),
    CG_BIND_END
    };

    int main(int argc, char **argv)
    {
    cg_app_desc_t desc;

    memset(&desc, 0, sizeof(desc));
    desc.bindings = k_bindings;
    desc.auto_locate_cgui = 1; /* 双击 exe 也能跑:自动在 exe 旁边找 .cgui */
    desc.on_ready = on_ready;

    if (argc > 1 && strcmp(argv[1], "–selftest") == 0) {
    desc.cgui_path = (argc > 2) ? argv[2] : "lesson01_edit_button.cgui";
    return cg_app_selftest(&desc); /* 不开窗,解析 + 真调一遍所有回调 */
    }
    desc.cgui_path = (argc > 1) ? argv[1] : "lesson01_edit_button.cgui";
    return cg_app_run(&desc);
    }


    下面是入门 01 的程序结构图,展示了 .cgui 界面文件、C 代码和运行时之间的协作关系:

    #mermaid-svg-wagYFCiO967OMEsV{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wagYFCiO967OMEsV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wagYFCiO967OMEsV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wagYFCiO967OMEsV .error-icon{fill:#552222;}#mermaid-svg-wagYFCiO967OMEsV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wagYFCiO967OMEsV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wagYFCiO967OMEsV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wagYFCiO967OMEsV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wagYFCiO967OMEsV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wagYFCiO967OMEsV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wagYFCiO967OMEsV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wagYFCiO967OMEsV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wagYFCiO967OMEsV .marker.cross{stroke:#333333;}#mermaid-svg-wagYFCiO967OMEsV svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wagYFCiO967OMEsV p{margin:0;}#mermaid-svg-wagYFCiO967OMEsV .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-wagYFCiO967OMEsV .cluster-label text{fill:#333;}#mermaid-svg-wagYFCiO967OMEsV .cluster-label span{color:#333;}#mermaid-svg-wagYFCiO967OMEsV .cluster-label span p{background-color:transparent;}#mermaid-svg-wagYFCiO967OMEsV .label text,#mermaid-svg-wagYFCiO967OMEsV span{fill:#333;color:#333;}#mermaid-svg-wagYFCiO967OMEsV .node rect,#mermaid-svg-wagYFCiO967OMEsV .node circle,#mermaid-svg-wagYFCiO967OMEsV .node ellipse,#mermaid-svg-wagYFCiO967OMEsV .node polygon,#mermaid-svg-wagYFCiO967OMEsV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wagYFCiO967OMEsV .rough-node .label text,#mermaid-svg-wagYFCiO967OMEsV .node .label text,#mermaid-svg-wagYFCiO967OMEsV .image-shape .label,#mermaid-svg-wagYFCiO967OMEsV .icon-shape .label{text-anchor:middle;}#mermaid-svg-wagYFCiO967OMEsV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wagYFCiO967OMEsV .rough-node .label,#mermaid-svg-wagYFCiO967OMEsV .node .label,#mermaid-svg-wagYFCiO967OMEsV .image-shape .label,#mermaid-svg-wagYFCiO967OMEsV .icon-shape .label{text-align:center;}#mermaid-svg-wagYFCiO967OMEsV .node.clickable{cursor:pointer;}#mermaid-svg-wagYFCiO967OMEsV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wagYFCiO967OMEsV .arrowheadPath{fill:#333333;}#mermaid-svg-wagYFCiO967OMEsV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wagYFCiO967OMEsV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wagYFCiO967OMEsV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wagYFCiO967OMEsV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wagYFCiO967OMEsV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wagYFCiO967OMEsV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wagYFCiO967OMEsV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wagYFCiO967OMEsV .cluster text{fill:#333;}#mermaid-svg-wagYFCiO967OMEsV .cluster span{color:#333;}#mermaid-svg-wagYFCiO967OMEsV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wagYFCiO967OMEsV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wagYFCiO967OMEsV rect.text{fill:none;stroke-width:0;}#mermaid-svg-wagYFCiO967OMEsV .icon-shape,#mermaid-svg-wagYFCiO967OMEsV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wagYFCiO967OMEsV .icon-shape p,#mermaid-svg-wagYFCiO967OMEsV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wagYFCiO967OMEsV .icon-shape .label rect,#mermaid-svg-wagYFCiO967OMEsV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wagYFCiO967OMEsV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wagYFCiO967OMEsV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wagYFCiO967OMEsV :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    运行时

    C 代码 main.c

    界面文件 .cgui

    控件定义edt_name / btn_hello / lab_result

    事件绑定change -> on_input_changeclick -> on_button_click

    CG_BIND 绑定表事件名 -> 函数指针

    回调函数on_input_change / on_button_click

    cg_app_run()

    事件分发

    cg_find / cg_widget_set_text读写界面

    入门 02:常用输入控件

    在这里插入图片描述

    一句话:表单里最常用的六种控件,各自的"读"和"写"是什么样。

    界面:一个分组框(容器)里放着姓名、年龄、会员、性别、城市、满意度、完成度、重置按钮; 分组框下面是汇总标签和状态栏。

    六种控件的读 / 写对照

    控件读写备注
    编辑框 edit cg_widget_text() cg_widget_set_text() 文字就是值
    数字微调 spinner cg_widget_value() cg_widget_set_value() 范围由 .cgui 的 min/max 决定,写入会自动夹取
    复选框 checkbox cg_widget_checked() cg_widget_set_checked()
    单选钮 radio cg_widget_checked() cg_widget_set_checked() 程序改勾选不会自动取消兄弟,两个都要写
    下拉框 combo cg_widget_value() = 第几项 cg_widget_set_value(i) 文字要用 cg_widget_list_item(cmb, i) 再取一次
    滑块 slider cg_widget_value() cg_widget_set_value() change 拖动中一直发,release 松手才发一次
    进度条 progress —(只读来显示) cg_widget_set_value() 没有任何事件,只被程序写

    三个要点

  • 容器里的子控件,坐标相对容器。grp_form 里的 edt_name 写的是 x:70,y:32, 是相对分组框客户区的,不是相对窗口。换容器位置不用动子控件的坐标。
  • 事件名可以被多个控件共用。六个控件的 change/select 全指向 on_form_change, 因为逻辑就是"有任何一个变了就重算一遍汇总"。这比写六个函数清爽得多。
  • 让界面状态只有一个出口:collect() 从界面读一遍 → 算 → 写回界面。 回调只负责调 collect()。状态一多,这种"单向更新"能省掉大量 bug。
  • 动手改一改

    • 给「姓名」加个非空校验:为空时把「重置」按钮禁用。
    • 把满意度滑块的 max 从 5 改成 10,看看滑块和汇总怎么变(.cgui 改一个数字,不用重编译)。
    • 城市下拉框换成 8 项,再在汇总里显示"第几项 / 共几项"(cg_widget_list_count())。

    入门 03:事件进阶(时钟 / 键盘 / 鼠标 / 关闭)

    一句话:控件之外还有一整套窗口级事件,写在 .cgui 的 app.events 里。

    界面:时钟标签 + 进度条 + 开始/停止按钮 + 两个说明标签 + 事件日志列表框 + 状态栏。

    窗口级事件(这次写的是 app.events,不是控件的 events)

    "app": { "events": { "ready": "on_ready", "close": "on_close",
    "mousemove": "on_mouse_move", "keydown": "on_key_down" } }

    事件什么时候来参数怎么取
    ready 窗口和控件都建好了、还没显示 最适合做初始化(设初始状态、读配置)
    close 用户点关闭(含 Alt+F4、Esc) cg_cancel_close(app) 可以拦下这次关闭
    mousemove 鼠标在窗口上移动 cg_event_x(self) / cg_event_y(self)
    keydown 按下按键 cg_event_key(self) 键码,cg_event_keys(self) 功能键位

    三个要点

  • 回调签名是固定的三参数,所以坐标、键码、滚轮量这些"事件附带信息"只能用 cg_event_* 系列函数去读(读的是"最近一次事件")。self 传回调收到的那个控件就行。
  • 时钟是控件,不是线程。timer 控件没有窗口,靠后端心跳按 period 毫秒派发 tick: cg_widget_set_period(tmr, 1000) 启动、cg_widget_set_period(tmr, 0) 停止。 凡是"每隔一会儿做点什么"都用它,不要去开线程碰界面。
  • 拦关闭要能被自检跳过。close 回调里的 cg_message_box 在 –selftest 下会卡住整个自检, 所以先问一句 cg_app_has_window(app)——自检时它返回 0,就不弹框了。 这套示例里所有弹框都遵守这条。
  • 动手改一改

    • 把时钟周期改成 200 毫秒(.cgui 里 period,或代码里 cg_widget_set_period)。
    • 加一个 Ctrl+S 快捷键:在 on_key_down 里判断 cg_event_key(self) == 'S' 且 cg_event_keys(self) & 1(Ctrl)。
    • 给窗口加 size 事件,把窗口尺寸显示到状态栏(cg_event_x/y 对 size 事件是宽/高)。

    下面是入门 03 的窗口级事件处理流程图,展示了各类事件的来源与处理方式:

    #mermaid-svg-16rG44EWZzZOCPel{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-16rG44EWZzZOCPel .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-16rG44EWZzZOCPel .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-16rG44EWZzZOCPel .error-icon{fill:#552222;}#mermaid-svg-16rG44EWZzZOCPel .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-16rG44EWZzZOCPel .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-16rG44EWZzZOCPel .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-16rG44EWZzZOCPel .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-16rG44EWZzZOCPel .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-16rG44EWZzZOCPel .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-16rG44EWZzZOCPel .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-16rG44EWZzZOCPel .marker{fill:#333333;stroke:#333333;}#mermaid-svg-16rG44EWZzZOCPel .marker.cross{stroke:#333333;}#mermaid-svg-16rG44EWZzZOCPel svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-16rG44EWZzZOCPel p{margin:0;}#mermaid-svg-16rG44EWZzZOCPel .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-16rG44EWZzZOCPel .cluster-label text{fill:#333;}#mermaid-svg-16rG44EWZzZOCPel .cluster-label span{color:#333;}#mermaid-svg-16rG44EWZzZOCPel .cluster-label span p{background-color:transparent;}#mermaid-svg-16rG44EWZzZOCPel .label text,#mermaid-svg-16rG44EWZzZOCPel span{fill:#333;color:#333;}#mermaid-svg-16rG44EWZzZOCPel .node rect,#mermaid-svg-16rG44EWZzZOCPel .node circle,#mermaid-svg-16rG44EWZzZOCPel .node ellipse,#mermaid-svg-16rG44EWZzZOCPel .node polygon,#mermaid-svg-16rG44EWZzZOCPel .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-16rG44EWZzZOCPel .rough-node .label text,#mermaid-svg-16rG44EWZzZOCPel .node .label text,#mermaid-svg-16rG44EWZzZOCPel .image-shape .label,#mermaid-svg-16rG44EWZzZOCPel .icon-shape .label{text-anchor:middle;}#mermaid-svg-16rG44EWZzZOCPel .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-16rG44EWZzZOCPel .rough-node .label,#mermaid-svg-16rG44EWZzZOCPel .node .label,#mermaid-svg-16rG44EWZzZOCPel .image-shape .label,#mermaid-svg-16rG44EWZzZOCPel .icon-shape .label{text-align:center;}#mermaid-svg-16rG44EWZzZOCPel .node.clickable{cursor:pointer;}#mermaid-svg-16rG44EWZzZOCPel .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-16rG44EWZzZOCPel .arrowheadPath{fill:#333333;}#mermaid-svg-16rG44EWZzZOCPel .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-16rG44EWZzZOCPel .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-16rG44EWZzZOCPel .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-16rG44EWZzZOCPel .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-16rG44EWZzZOCPel .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-16rG44EWZzZOCPel .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-16rG44EWZzZOCPel .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-16rG44EWZzZOCPel .cluster text{fill:#333;}#mermaid-svg-16rG44EWZzZOCPel .cluster span{color:#333;}#mermaid-svg-16rG44EWZzZOCPel div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-16rG44EWZzZOCPel .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-16rG44EWZzZOCPel rect.text{fill:none;stroke-width:0;}#mermaid-svg-16rG44EWZzZOCPel .icon-shape,#mermaid-svg-16rG44EWZzZOCPel .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-16rG44EWZzZOCPel .icon-shape p,#mermaid-svg-16rG44EWZzZOCPel .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-16rG44EWZzZOCPel .icon-shape .label rect,#mermaid-svg-16rG44EWZzZOCPel .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-16rG44EWZzZOCPel .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-16rG44EWZzZOCPel .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-16rG44EWZzZOCPel :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    动作

    C 回调函数

    窗口级事件 app.events

    ready窗口就绪

    close用户点关闭

    mousemove鼠标移动

    keydown按下按键

    on_ready初始化

    on_closecg_cancel_close 拦截

    on_mouse_movecg_event_x / cg_event_y

    on_key_downcg_event_key / cg_event_keys

    设置初始状态

    弹确认框需先过 cg_app_has_window

    更新坐标显示

    响应快捷键

    入门 04:数据控件(列表框 / 树 / 表格)

    一句话:装"一组数据"的控件,API 分成"加 / 清 / 数 / 读 / 选中"五类,记住这张表就不会找错函数。

    界面:三个分组框各装一种数据控件,每个配两个按钮,最下面是统一的事件日志。

    API 对照

    操作列表框 listbox树 tree表格 table
    加内容 cg_widget_list_add() / _addf() cg_tree_add(树, parent, 文字) cg_table_add_row(表, cells, n)
    清空 cg_widget_list_clear() cg_tree_clear() cg_table_clear()
    有多少 cg_widget_list_count() cg_tree_count() cg_table_row_count()
    读某一项 cg_widget_list_item(i) cg_tree_item_text(i) cg_table_cell(row, col)
    当前选中 cg_widget_value() cg_tree_selected() cg_table_selected()

    三个要点

  • cg_tree_add 的 parent 是"节点下标",-1 表示加在根上。所以"加在当前选中节点下面" 直接写 cg_tree_add(tre, cg_tree_selected(tre), text) 就行 —— 没选中时它正好返回 -1。
  • 表格的行勾选 / 行数值 / 行图标不是 .cgui 字段,是运行时 API: cg_table_set_checked()(需要 .cgui 里 "check": true)、cg_table_row_value()、 cg_table_set_row_icon()。勾选变化会发 check 事件(不是 select)。
  • 列表框 / 树 / 表格没有"改一项"的 API(只有加 / 清 / 数 / 读)。要改内容只能整表重建: 先把每项文字复制出来 → 清空 → 按需要重新加回去(见入门 05 的 list_rebuild())。 代码里大部分 cg_* 返回的字符串都是堆内存,复制出来记得 cg_free。
  • 动手改一改

    • 给树加"删掉选中节点",看看为什么必须整树重建(cg_tree_clear + 重新 cg_tree_add)。
    • 表格加一列「备注」,注意 cg_table_add_row 的 cells 顺序就是列顺序。
    • 给列表项加双击事件:dblclick 是列表框支持的事件(入门 05 用它做"删除")。

    入门 05:待办清单(综合练习)

    一句话:把前四课串起来,再加"菜单 + 文件 + 配置",就是一个能交付的小工具。

    界面:菜单栏(文件 / 帮助)、输入框、添加按钮、完成按钮、提示标签、清单列表框、状态栏。

    这一课新增的东西

    主题怎么做在哪看
    菜单 .cgui 的 nodes 写菜单树,click 事件里用 cg_menu_last_id(self) 判断点了哪一项 on_menu_click
    通用对话框 cg_open_file_dialog / cg_save_file_dialog,返回 1 = 用户确定 save_ask
    读文件 cg_file_read() 返回堆串(BOM 已去掉),用完 cg_free load_from
    写文件 cg_file_write() 覆盖写 UTF-8 文本,返回 0 = 成功 save_to
    配置持久化 cg_app_data_dir() 拿用户目录 → cg_path_join 拼 INI → cg_ini_read/write on_ready
    脏标记 s_biz.dirty:改过就置 1,保存/打开后清 0,用来标状态栏、拦关闭 refresh / on_close
    日志 cg_log(CG_LOG_INFO, …) 写进 %LOCALAPPDATA%\\<exe名>\\app.log 全篇

    三个要点

  • cg_kit 让业务代码不用碰 Windows API。文件、路径、INI、编码(UTF-8 ↔ GBK)、剪贴板、 启动外部程序、注册表都在 cstackgui.h 里。凡是返回 char* 的都要 cg_free()。 画布右侧「支持库」页签可以按用途搜索这些函数(双击复制调用代码)。
  • "改一项"要整表重建:list_rebuild() 把列表复制出来 → 清空 → 条件性地重新加回。 这是入门 04 那个约定的实战:借的堆内存全部还回去。
  • 关闭前拦截:close 回调里发现 dirty 就弹提示 + cg_cancel_close(app), 用户第二次点关闭(dirty 还在也没关系,因为这次直接放行)才真关。 注意弹框仍要先过 cg_app_has_window(),否则自检会被卡住。
  • 动手改一改

    • 加"全部完成 / 全部取消"菜单项,遍历列表给每项加/去 [x] 前缀。
    • 把清单存成 UTF-8 之外的老式 ANSI 文件:用 cg_gbk_to_utf8()(读)/ cg_utf8_to_gbk()(写)。
    • 给每个待办加优先级:列表文字前面加 [高] ,保存格式不变,只是多一层解析。

    四、通用知识(每一课都用到)

    4.1 一条程序的骨架

    #include "cstackgui.h" /* 业务代码只 include 这一个头 */

    static const cg_binding_t k_bindings[] = {
    CG_BIND("on_button_click", on_button_click), /* 左:.cgui 里的事件名;右:C 函数 */
    CG_BIND_END
    };

    int main(int argc, char **argv)
    {
    cg_app_desc_t desc = {0};
    desc.bindings = k_bindings;
    desc.cgui_path = "xxx.cgui";
    desc.auto_locate_cgui = 1; /* 双击 exe 也能跑:自动在 exe 旁边找 .cgui */
    desc.on_ready = on_ready; /* 等价于 .cgui 里 app.events.ready */
    if (argc > 1 && strcmp(argv[1], "–selftest") == 0) return cg_app_selftest(&desc);
    return cg_app_run(&desc);
    }

    回调签名永远是:

    static void f(cg_widget_t *self, cg_event_t ev, void *user);

    • self 触发事件的控件(不会为 NULL);
    • ev 事件类型(同一个函数绑了多个事件时用它区分,见入门 04 的展开/折叠);
    • user 用 cg_widget_set_user() 挂的业务数据,默认 NULL。

    下面是 4.1 节「一条程序的骨架」的时序图,展示程序从启动到退出的完整生命周期:

    回调函数

    .cgui 文件

    运行时

    cg_app_desc_t

    main()

    回调函数

    .cgui 文件

    运行时

    cg_app_desc_t

    main()

    #mermaid-svg-Vwkq3L8mgFXARc0m{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Vwkq3L8mgFXARc0m .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Vwkq3L8mgFXARc0m .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Vwkq3L8mgFXARc0m .error-icon{fill:#552222;}#mermaid-svg-Vwkq3L8mgFXARc0m .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Vwkq3L8mgFXARc0m .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Vwkq3L8mgFXARc0m .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Vwkq3L8mgFXARc0m .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Vwkq3L8mgFXARc0m .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Vwkq3L8mgFXARc0m .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Vwkq3L8mgFXARc0m .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Vwkq3L8mgFXARc0m .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Vwkq3L8mgFXARc0m .marker.cross{stroke:#333333;}#mermaid-svg-Vwkq3L8mgFXARc0m svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Vwkq3L8mgFXARc0m p{margin:0;}#mermaid-svg-Vwkq3L8mgFXARc0m .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Vwkq3L8mgFXARc0m text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Vwkq3L8mgFXARc0m .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Vwkq3L8mgFXARc0m .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Vwkq3L8mgFXARc0m .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Vwkq3L8mgFXARc0m .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Vwkq3L8mgFXARc0m #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Vwkq3L8mgFXARc0m .sequenceNumber{fill:white;}#mermaid-svg-Vwkq3L8mgFXARc0m #sequencenumber{fill:#333;}#mermaid-svg-Vwkq3L8mgFXARc0m #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Vwkq3L8mgFXARc0m .messageText{fill:#333;stroke:none;}#mermaid-svg-Vwkq3L8mgFXARc0m .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Vwkq3L8mgFXARc0m .labelText,#mermaid-svg-Vwkq3L8mgFXARc0m .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Vwkq3L8mgFXARc0m .loopText,#mermaid-svg-Vwkq3L8mgFXARc0m .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Vwkq3L8mgFXARc0m .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Vwkq3L8mgFXARc0m .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Vwkq3L8mgFXARc0m .noteText,#mermaid-svg-Vwkq3L8mgFXARc0m .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Vwkq3L8mgFXARc0m .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Vwkq3L8mgFXARc0m .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Vwkq3L8mgFXARc0m .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Vwkq3L8mgFXARc0m .actorPopupMenu{position:absolute;}#mermaid-svg-Vwkq3L8mgFXARc0m .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Vwkq3L8mgFXARc0m .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Vwkq3L8mgFXARc0m .actor-man circle,#mermaid-svg-Vwkq3L8mgFXARc0m line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Vwkq3L8mgFXARc0m :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    loop

    [用户交互]

    填充 bindings / cgui_path / on_ready

    cg_app_run(&desc)

    解析界面文件

    控件树 + 事件绑定表

    触发 ready 事件

    初始化界面状态

    分发 change / click / submit 等事件

    读写控件、更新界面

    返回退出码

    4.2 事件名错了为什么不报编译错

    .cgui 里存的是函数名字符串,运行时按名字在 CG_BIND 表里查函数指针。 好处是画布和 C 代码彻底解耦(改界面不用重编译),代价是写错名字编译不报错 —— 所以:

    • 运行时会在启动后汇总弹一次提示:“这些事件不会触发”;
    • –selftest 会以非 0 退出码报出来(这是 CI 里唯一能自动发现它的手段);
    • 画布「事件」页签的「代码」按钮能直接跳到 *_ui_events.c 里对应的函数。

    想让某个控件支持哪些事件,看 .cgui 类型表(docs\\CGUI-FORMAT.md 第 4 节)—— 事件是按控件特性分配的:标签、进度条、状态栏一个事件都没有,只有树有展开/折叠。

    4.3 排错:先看日志文件

    发布版(–release)没有控制台,printf/stderr 用户一个字都看不到。所以:

    • 运行时启动就开一份日志:%LOCALAPPDATA%\\<exe名>\\app.log(超过 256KB 轮转成 .old);
    • 致命错误(找不到 .cgui、格式错、建窗失败、崩溃)会写日志 + 弹中文提示框;
    • 业务代码用 cg_log(CG_LOG_INFO, "%d 项", n) 往同一个文件里补记录。

    用户报"点了没反应",第一件事就是让他把这个文件发过来。

    调试版(默认编译方式)带控制台,printf 直接可见,开发时更省事。

    4.4 编译 / 验证命令速查

    build-all.bat :: 编运行时 + 全部示例,并逐个跑 –selftest
    build-app.bat lesson03_events :: 只编某一课
    build-app.bat lesson03_events –release :: 发布版(无控制台,走日志)

    :: 手动自检某一课
    examples\\lesson03_events\\lesson03_events.exe –selftest examples\\lesson03_events\\lesson03_events.cgui

    :: 仓库级门禁(改控件表 / 改 .cgui 字段后必跑)
    tools\\check-format.bat
    tools\\check-kit.bat


    五、常见问题

    Q:双击 exe 什么都没发生? 按顺序找界面文件:命令行参数 → exe 旁边 → exe目录\\..\\apps\\<exe名>\\。都找不到就写日志 + 弹框。 打开 %LOCALAPPDATA%\\<exe名>\\app.log 能看到它试过哪些路径。

    Q:改了界面(.cgui),程序里没变? .cgui 是运行时读的 —— 确认改的是 exe 旁边那个文件;调试版控制台会打印实际读的是哪个路径。

    Q:事件不触发? 先跑 –selftest。返回 5 就是"事件名在 C 代码里没登记"。另一种可能是绑定的事件该控件不支持 (比如给标签绑 click),启动时会警告。

    Q:–selftest 卡住不动? 多半是某个回调里弹了消息框。所有会弹框的地方都要先 if (cg_app_has_window(app))。

    Q:中文在控制台里是乱码? 控制台代码页问题,不影响界面和日志文件(文件里是 UTF-8)。

    Q:想要圆角 / 换配色,改了没效果? 按钮、开关、勾选、输入框、滑块、进度条、圆环走 GDI+ 自绘,style.radius / style.bg / style.fg 生效; 列表框、树、表格、标签页、月历、下拉框是系统原生控件,不吃自定义外观(Win32 固有限制)。


    下面是常见问题的排查流程图,按顺序检查即可快速定位问题:

    #mermaid-svg-rM5aeowPc1lZRMhn{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rM5aeowPc1lZRMhn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rM5aeowPc1lZRMhn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rM5aeowPc1lZRMhn .error-icon{fill:#552222;}#mermaid-svg-rM5aeowPc1lZRMhn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rM5aeowPc1lZRMhn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rM5aeowPc1lZRMhn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rM5aeowPc1lZRMhn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rM5aeowPc1lZRMhn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rM5aeowPc1lZRMhn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rM5aeowPc1lZRMhn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rM5aeowPc1lZRMhn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rM5aeowPc1lZRMhn .marker.cross{stroke:#333333;}#mermaid-svg-rM5aeowPc1lZRMhn svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rM5aeowPc1lZRMhn p{margin:0;}#mermaid-svg-rM5aeowPc1lZRMhn .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-rM5aeowPc1lZRMhn .cluster-label text{fill:#333;}#mermaid-svg-rM5aeowPc1lZRMhn .cluster-label span{color:#333;}#mermaid-svg-rM5aeowPc1lZRMhn .cluster-label span p{background-color:transparent;}#mermaid-svg-rM5aeowPc1lZRMhn .label text,#mermaid-svg-rM5aeowPc1lZRMhn span{fill:#333;color:#333;}#mermaid-svg-rM5aeowPc1lZRMhn .node rect,#mermaid-svg-rM5aeowPc1lZRMhn .node circle,#mermaid-svg-rM5aeowPc1lZRMhn .node ellipse,#mermaid-svg-rM5aeowPc1lZRMhn .node polygon,#mermaid-svg-rM5aeowPc1lZRMhn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rM5aeowPc1lZRMhn .rough-node .label text,#mermaid-svg-rM5aeowPc1lZRMhn .node .label text,#mermaid-svg-rM5aeowPc1lZRMhn .image-shape .label,#mermaid-svg-rM5aeowPc1lZRMhn .icon-shape .label{text-anchor:middle;}#mermaid-svg-rM5aeowPc1lZRMhn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rM5aeowPc1lZRMhn .rough-node .label,#mermaid-svg-rM5aeowPc1lZRMhn .node .label,#mermaid-svg-rM5aeowPc1lZRMhn .image-shape .label,#mermaid-svg-rM5aeowPc1lZRMhn .icon-shape .label{text-align:center;}#mermaid-svg-rM5aeowPc1lZRMhn .node.clickable{cursor:pointer;}#mermaid-svg-rM5aeowPc1lZRMhn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rM5aeowPc1lZRMhn .arrowheadPath{fill:#333333;}#mermaid-svg-rM5aeowPc1lZRMhn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rM5aeowPc1lZRMhn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rM5aeowPc1lZRMhn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rM5aeowPc1lZRMhn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rM5aeowPc1lZRMhn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rM5aeowPc1lZRMhn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rM5aeowPc1lZRMhn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rM5aeowPc1lZRMhn .cluster text{fill:#333;}#mermaid-svg-rM5aeowPc1lZRMhn .cluster span{color:#333;}#mermaid-svg-rM5aeowPc1lZRMhn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rM5aeowPc1lZRMhn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rM5aeowPc1lZRMhn rect.text{fill:none;stroke-width:0;}#mermaid-svg-rM5aeowPc1lZRMhn .icon-shape,#mermaid-svg-rM5aeowPc1lZRMhn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rM5aeowPc1lZRMhn .icon-shape p,#mermaid-svg-rM5aeowPc1lZRMhn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rM5aeowPc1lZRMhn .icon-shape .label rect,#mermaid-svg-rM5aeowPc1lZRMhn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rM5aeowPc1lZRMhn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rM5aeowPc1lZRMhn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rM5aeowPc1lZRMhn :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    程序出问题

    双击 exe 没反应?

    检查 app.log确认 .cgui 路径

    事件不触发?

    跑 –selftest检查事件名是否绑定

    –selftest 卡住?

    检查回调里是否弹框需先过 cg_app_has_window

    界面改了没生效?

    确认改的是 exe 旁边的 .cgui

    查看 app.log 定位问题

    六、学完之后看什么

    想干什么看哪里
    查"某个控件有哪些事件 / 字段" docs\\CGUI-FORMAT.md 第 2、4 节
    查"某个 C 函数怎么用"(含中文说明与示例) 画布右侧「支持库」页签(150 条,可搜索,双击复制代码)
    看全部 39 种控件的长相与用法 examples\\gallery(控件陈列室,分三页;build-app.bat gallery)
    看"真实工具"的完整结构 examples\\notepad(菜单 / 脏标记 / 状态栏 / 编码识别 / 配置)
    画布本身怎么改 .codebuddy\\skills\\designer-modify\\SKILL.md
    架构与取舍 docs\\ARCHITECTURE.md

    建议的学习顺序:先把这五课都编译、运行、改一处东西跑通; 然后打开 examples\\gallery 当"控件字典"翻; 最后照着 examples\\notepad 的结构写自己的第一个工具(用画布画界面 → 导出 C 骨架 → 填回调)。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » CStackGUI 教学示例使用说明
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!