文章目录
- 一、五课一览
- 二、五分钟上手
-
- 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),在仓库根目录执行命令。
一、五课一览
| 入门 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) 分辨是谁被点的 |
三个要点
动手改一改
- 把结果标签的 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() | 没有任何事件,只被程序写 |
三个要点
动手改一改
- 给「姓名」加个非空校验:为空时把「重置」按钮禁用。
- 把满意度滑块的 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) 功能键位 |
三个要点
动手改一改
- 把时钟周期改成 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 对照
| 加内容 | 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_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 | 全篇 |
三个要点
动手改一改
- 加"全部完成 / 全部取消"菜单项,遍历列表给每项加/去 [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 骨架 → 填回调)。
网硕互联帮助中心



评论前必须登录!
注册