【CStackGUI 实战】深色主题终端:app.dark 默认深色 + 顶层命名样式表 + 运行时切换

文章目录

一、它演示什么

只讲深色 这一件事:.cgui 的 app 里写 "dark": true,程序一打开就是深色(想跟系统走就写 "dark": "auto");配色集中在顶层 styles{} 命名样式表里;再用 cg_theme_set_dark 在运行时切换并立即重画。

关键认知是"深色只换默认配色 ":窗口底色、没写颜色的文字会跟着变;

控件自己写了 fg / bg 的不跟随 。

界面上还演示了进度条 / 圆环 / 开关 / 徽章这些"哪些跟随、哪些不跟随"的现场边界。

下面是深色主题的配置与切换流程:
#mermaid-svg-MP3gt6nzxB5o5xRQ{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-MP3gt6nzxB5o5xRQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MP3gt6nzxB5o5xRQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MP3gt6nzxB5o5xRQ .error-icon{fill:#552222;}#mermaid-svg-MP3gt6nzxB5o5xRQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MP3gt6nzxB5o5xRQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MP3gt6nzxB5o5xRQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MP3gt6nzxB5o5xRQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MP3gt6nzxB5o5xRQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MP3gt6nzxB5o5xRQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MP3gt6nzxB5o5xRQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MP3gt6nzxB5o5xRQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MP3gt6nzxB5o5xRQ .marker.cross{stroke:#333333;}#mermaid-svg-MP3gt6nzxB5o5xRQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MP3gt6nzxB5o5xRQ p{margin:0;}#mermaid-svg-MP3gt6nzxB5o5xRQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MP3gt6nzxB5o5xRQ .cluster-label text{fill:#333;}#mermaid-svg-MP3gt6nzxB5o5xRQ .cluster-label span{color:#333;}#mermaid-svg-MP3gt6nzxB5o5xRQ .cluster-label span p{background-color:transparent;}#mermaid-svg-MP3gt6nzxB5o5xRQ .label text,#mermaid-svg-MP3gt6nzxB5o5xRQ span{fill:#333;color:#333;}#mermaid-svg-MP3gt6nzxB5o5xRQ .node rect,#mermaid-svg-MP3gt6nzxB5o5xRQ .node circle,#mermaid-svg-MP3gt6nzxB5o5xRQ .node ellipse,#mermaid-svg-MP3gt6nzxB5o5xRQ .node polygon,#mermaid-svg-MP3gt6nzxB5o5xRQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MP3gt6nzxB5o5xRQ .rough-node .label text,#mermaid-svg-MP3gt6nzxB5o5xRQ .node .label text,#mermaid-svg-MP3gt6nzxB5o5xRQ .image-shape .label,#mermaid-svg-MP3gt6nzxB5o5xRQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-MP3gt6nzxB5o5xRQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MP3gt6nzxB5o5xRQ .rough-node .label,#mermaid-svg-MP3gt6nzxB5o5xRQ .node .label,#mermaid-svg-MP3gt6nzxB5o5xRQ .image-shape .label,#mermaid-svg-MP3gt6nzxB5o5xRQ .icon-shape .label{text-align:center;}#mermaid-svg-MP3gt6nzxB5o5xRQ .node.clickable{cursor:pointer;}#mermaid-svg-MP3gt6nzxB5o5xRQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MP3gt6nzxB5o5xRQ .arrowheadPath{fill:#333333;}#mermaid-svg-MP3gt6nzxB5o5xRQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MP3gt6nzxB5o5xRQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MP3gt6nzxB5o5xRQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MP3gt6nzxB5o5xRQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MP3gt6nzxB5o5xRQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MP3gt6nzxB5o5xRQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MP3gt6nzxB5o5xRQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MP3gt6nzxB5o5xRQ .cluster text{fill:#333;}#mermaid-svg-MP3gt6nzxB5o5xRQ .cluster span{color:#333;}#mermaid-svg-MP3gt6nzxB5o5xRQ 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-MP3gt6nzxB5o5xRQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MP3gt6nzxB5o5xRQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-MP3gt6nzxB5o5xRQ .icon-shape,#mermaid-svg-MP3gt6nzxB5o5xRQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MP3gt6nzxB5o5xRQ .icon-shape p,#mermaid-svg-MP3gt6nzxB5o5xRQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MP3gt6nzxB5o5xRQ .icon-shape .label rect,#mermaid-svg-MP3gt6nzxB5o5xRQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MP3gt6nzxB5o5xRQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MP3gt6nzxB5o5xRQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MP3gt6nzxB5o5xRQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} true
auto
false
深色
浅色
是
否
app.dark 配置
dark 值
启动即深色
跟随系统
启动即浅色
cg_theme_set_dark 运行时切换
切换目标
换掉兜底配色
恢复默认配色
立即重画
控件是否写死 fg/bg
颜色不跟随
颜色跟随主题

二、怎么用

  1. 运行即深色。右上角「切换到浅色主题」按钮点一下就在深浅之间切,按钮文字与状态行会同步更新。
  2. 切到浅色时观察:窗口底色、标题 / 状态行 / 底部说明这些没写颜色的文字跟着变;

下面是深色 / 浅色并排对比示意(左侧深色、右侧浅色),一眼看清"哪些跟随、哪些不跟随":

text 复制代码
┌─────────────────────────────┬─────────────────────────────┐
│         深色主题             │         浅色主题             │
├─────────────────────────────┼─────────────────────────────┤
│ ┌─────────────────────────┐ │ ┌─────────────────────────┐ │
│ │ 标题 / 状态行 / 底部说明  │ │ │ 标题 / 状态行 / 底部说明  │ │
│ │ (浅字 → 跟随主题)       │ │ │ (深字 → 跟随主题)       │ │
│ └─────────────────────────┘ │ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │ ┌─────────────────────────┐ │
│ │ 深色面板(#1A191F)      │ │ │ 深色面板(#1A191F)      │ │
│ │ 日志视图(#0E1116)      │ │ │ 日志视图(#0E1116)      │ │
│ │ 按钮 / 输入框 / 徽章      │ │ │ 按钮 / 输入框 / 徽章      │ │
│ │ (写死颜色 → 不跟随)     │ │ │ (写死颜色 → 不跟随)     │ │
│ └─────────────────────────┘ │ └─────────────────────────┘ │
│  progress 槽 / switch 填充   │  progress 槽 / switch 填充   │
│  (皮肤写死浅色 → 不跟随)    │  (皮肤写死浅色 → 不跟随)    │
│  progressring 轨道           │  progressring 轨道           │
│  (背后色 → 跟随主题)        │  (背后色 → 跟随主题)        │
└─────────────────────────────┴─────────────────────────────┘

对照要点:跟随主题 的是窗口底色、没写 fg 的标题 / 状态行 / 底部说明、progressring 轨道;保持写死颜色不变 的是深色面板、日志视图、按钮、输入框、徽章,以及 progress 槽、switch / checkbox 填充------这正是"深色只换默认配色"的直观体现。

深色面板、日志视图、按钮、输入框、徽章保持原样 。

  1. 日志视图:btn_fill「追加几行」灌几行样例日志,btn_clear「清空日志」清空。

  2. sw_scroll「自动滚动到底」开关控制追加后是否滚到底(关了就得自己拖)。

  3. 底部 edt_cmd 输入框里输入(默认 AT+VERSION)回车或点「发送」;「保存日志」把日志存盘。

  4. 界面上几组说明(lab_tip、lab_foot1 / lab_foot2 等)就是这台面"哪些跟随、哪些不跟随"的注脚。

三、一屏总览

控件 id 类型 干什么
btn_theme button 运行时切换深 / 浅主题
edt_log edit(多行,只读) 日志视图,fg/bg 写在 styles.log
btn_fill / btn_clear button 追加样例行 / 清空日志
sw_scroll switch 自动滚动到底
prg_buf / ring_rt progress / progressring 进度条 / 圆环
led_conn led 状态灯
badge_dark / badge_self badge 自绘徽章,bg/fg 自己写
edt_cmd / btn_send edit / button 底部命令输入与发送

四、关键接口

五、接口速查表

接口 完整签名 参数说明 返回值 典型调用场景 --selftest 下能否调用
cg_theme_set_dark void cg_theme_set_dark(cg_app_t *app, int dark) app:所属应用;dark:1=深色,0=浅色,语义与 app.dark 一致 无 运行时切换主题,立即重画 不能(无窗口,切换无意义)
cg_theme_is_dark int cg_theme_is_dark(cg_app_t *app) app:所属应用 当前是否深色(1=深色,0=浅色) 查询当前主题,用于同步按钮文字与状态行 不能(无窗口,查询无意义)
cg_edit_scroll_end void cg_edit_scroll_end(cg_widget_t *edt) edt:多行 edit 控件 无 追加文本后滚到底,露出最新内容 不能(无原生控件)
cg_widget_set_text void cg_widget_set_text(cg_widget_t *w, const char *text) w:目标控件;text:要设置的 UTF-8 文本 无 刷新日志视图、按钮文字、状态行 不能(无原生控件)
cg_app_has_window int cg_app_has_window(cg_app_t *app) app:所属应用 是否有窗口(1=有,0=无) 回调第一件事,自检模式下不动界面、不弹框 可以(正是为自检设计的守卫)

说明:--selftest 会合成触发每个回调 ,那时没有窗口、没有原生控件。所以除 cg_app_has_window 外,其余 4 个接口在自检模式下都不能调用------回调里必须先问 cg_app_has_window(),返回 0 就直接 return,避免碰界面 / 弹对话框把自检搞挂。

  • cg_theme_set_dark / cg_theme_is_dark:运行时切换主题、查询当前主题;语义与 app.dark 完全一致,立即重画。
  • cg_edit_scroll_end:多行 edit 追加文本后要自己滚到底(没有 terminal 那套托管)。
  • cg_widget_set_text:把日志与按钮文字刷成最新状态。
  • cg_app_has_window:回调第一件事,自检模式下不动界面、不弹框。

五、容易踩的坑

  • 深色只换运行时那套兜底配色 ,写死了颜色的控件不跟随 ;progress 的槽、switch / checkbox 的填充仍是浅色系。
  • listbox / table / tree / tabs / 下拉列表 / 状态栏是系统经典控件,公开接口改不动它们的主题,所以本示例一个都没用(免得深色界面里摆一块白)。
  • 多行 edit 没有 terminal 的"自动滚 + 按行着色 + 行数上限":文本得自己攒(本示例用环形缓冲),文本多了要自己收着点。
c 复制代码
/**
 * @file    main.c
 * @brief   深色主题示例:app.dark 默认深色 + 顶层命名样式表 + cg_theme_set_dark 运行时切换
 * @details 这个示例只讲**深色**这一件事,所以界面本身不复杂:一个日志视图(只读多行 edit)
 *          + 几个典型控件(按钮 / 开关 / 进度条 / 圆环 / 状态灯 / 徽章 / 输入框)。
 *
 *          **一、深色是"默认"而不是"开关"。** `.cgui` 的 app 里写了 `"dark": true`,
 *          所以程序一打开就是深色。它走的不是"给每个控件刷一遍颜色",而是换掉运行时那套
 *          **兜底配色**(窗口底色、自绘面板的"背后色"、没写颜色的文字色)------ 见
 *          docs/CGUI-FORMAT.md 的 1.1.1。想跟系统走就写 `"dark": "auto"`。
 *
 *          **二、配色写在顶层 `styles{}` 里,不散落在控件上。** 深色不是一个按键按下去
 *          全都自动变好看:不少控件不跟随主题,所以"为深色写的界面"必须**自己把颜色写清楚**,
 *          于是就需要一个地方集中放这套色(改一行就能统一调):
 *
 *            - `panel`(深底 #1A191F + 浅字 #E8E2F0):两个侧栏分组;
 *            - `oncard`(浅字 #CFC7DB):深色面板上的标签;
 *            - `log`(深底 #0E1116 + 浅绿字 #CDE8C6):中间那个日志视图;
 *            - `input`(深底浅字):底部命令输入框;
 *            - `primary` / `ghost`(按钮填充色写死为深紫 / 深灰):按钮的填充色**不跟随
 *              主题**,不写就是 M3 浅紫,深底上是一块亮紫;
 *            - `chip`(浅绿底 + 近黑字):自绘徽章 ------ "写了 bg/fg 就按自己的来"这条规则
 *              在深色下同样成立(写死就写死)。
 *
 *          控件那边三种引用形态都用到了:`"style": "panel"`(整份取用)、
 *          `"style": { "use": "panel", "bg": "#121117" }`(命名 + 局部覆盖,见日志分组)、
 *          以及纯内联对象(状态灯的亮色、进度条的填充色只有一个字段,不值得起名字)。
 *
 *          **三、运行时切换是本示例最值得看的地方**(bind 在右上角那个按钮上):
 *            cg_theme_set_dark(app, !cg_theme_is_dark(app));
 *          它**立即重画**,语义与 `app.dark` 完全一致。切到浅色时你会看到:
 *            - 跟着变的:窗口底色,以及**没写 fg 的 label**(标题 / 状态行 / 底部两行说明)
 *              从浅字变深字;
 *            - 不跟着变的:写死了颜色的深色面板、日志视图、按钮、输入框、徽章。
 *          这不是 bug,是"深色只改默认值"的直接结果(CGUI-FORMAT 1.1.1 那条)。
 *
 *          **四、几处"看起来不对、其实是运行时边界"的地方,界面上都留了说明**:
 *            - `progress` 的**槽是皮肤里写死的浅色**(填充色能改:给 style.fg 就是填充色),
 *              深底下会看到一条浅槽;同族的 `progressring` 轨道用"背后色",**反而跟随**;
 *            - `switch` / `checkbox` 的填充是 M3 紫(皮肤写死),只有标签文字跟着主题;
 *              **标签文字当天修好了一处**:原先 checkbox / radio 的标签是写死的深色
 *              (`cg_skin.c` 里与开关那处的常量差最后一位:0x1C1B1F / 0x1D1B20),深底上看不见 ------
 *              现在两处共用 `sk_default_text()`,按常量 grep 也搜不全的坑一并收口;
 *            - 本示例的日志视图**用自己写了颜色的只读多行 edit**,起因是当时 `terminal` 的
 *              **行色是给浅底写的**(RX 深绿 / TX 深蓝 / SYS 灰,深底上 RX/TX 对比度只有 2.6:1
 *              ⇒ 看不清)。**这一条当天也修了**:terminal 的行色现在分浅底 / 深底两套
 *              (`runtime/src/win32/cg_win_sync.c` 的 `term_col()`),深色下自动用亮色 ⇒
 *              **新工程可以直接用 terminal**。本示例的多行 edit 保留:它正好演示
 *              "想完全自己控制配色"的那一种写法;
 *            - `listbox` / `table` / `tree` / `tabs` / 下拉列表 / 状态栏是系统经典控件,
 *              公开接口改不动它们的主题 ⇒ 这个示例**一个都没用**,免得深色界面里摆一块白。
 *
 *          工程上的两点注意:
 *            1. 所有回调第一件事都是 `cg_app_has_window()`:`--selftest` 会**合成触发每个
 *               回调**,那时没有窗口、没有原生控件,碰界面 / 弹对话框就会把自检搞挂;
 *            2. 日志视图是普通多行 edit:文本得自己攒(这里用和 serialterm 同一套环形缓冲),
 *               追加后按需 `cg_edit_scroll_end()` 滚到底 ------ 没有 terminal 那种"自动滚 + 按行
 *               着色 + 行数上限"的托管。
 *
 *          编译:在项目根跑 build-app.bat darkterm
 */
#include "cstackgui.h"

#include <stdio.h>
#include <string.h>

/** 日志视图最多留多少字符(环形:超了从最老的地方丢) */
#define LOG_CAP 32000

/** 业务状态:日志文本 + 自动滚动开关 */
typedef struct {
    int  autoscroll;              /**< 追加后是否滚到底(.cgui 里那个开关) */
    int  len;                     /**< log_buf 里当前有效长度 */
    char buf[LOG_CAP + 1];        /**< 日志视图的内容(环形,超了丢最老) */
} biz_t;

static biz_t s_biz;

/* ------------------------------------------------------------ 日志视图 */

/**
 * @brief 往日志视图追加一段文本(超容量从最老处丢),并按需滚到底
 * @param app 所属应用
 * @param data 要追加的字节(UTF-8)
 * @param n 字节数
 * @return void
 * @note 多行 edit 没有 terminal 那种"自动滚到底":set_text 会把视图滚回顶部,
 *       所以后面必须跟一个 cg_edit_scroll_end 把新内容露出来(除非用户关了自动滚动)。
 */
static void log_append(cg_app_t *app, const char *data, int n)
{
    cg_widget_t *edt = cg_find(app, "edt_log");

    if (n <= 0) return;
    if (s_biz.len + n > LOG_CAP) {                  /* 环形:装不下就丢最老的那截 */
        int drop = s_biz.len + n - LOG_CAP;

        if (drop >= s_biz.len) {
            s_biz.len = 0;
        } else {
            memmove(s_biz.buf, s_biz.buf + drop, (size_t)(s_biz.len - drop));
            s_biz.len -= drop;
        }
    }
    memcpy(s_biz.buf + s_biz.len, data, (size_t)n);
    s_biz.len += n;
    s_biz.buf[s_biz.len] = '\0';

    if (edt) {
        cg_widget_set_text(edt, s_biz.buf);
        if (s_biz.autoscroll) cg_edit_scroll_end(edt);
    }
}

/**
 * @brief 追加一行日志(前面带方向记号,和 terminal 的 RX/TX/-- 一个观感)
 * @param app 所属应用
 * @param tag 行首记号("RX " / "TX " / "-- ")
 * @param text 正文(UTF-8)
 * @return void
 */
static void log_line(cg_app_t *app, const char *tag, const char *text)
{
    char tmp[2048];
    int  n = snprintf(tmp, sizeof(tmp), "%s%s\n", tag, text);

    if (n < 0) return;
    if (n > (int)sizeof(tmp) - 1) n = (int)sizeof(tmp) - 1;
    log_append(app, tmp, n);
}

/* ------------------------------------------------------------ 主题 */

/**
 * @brief 把"当前是什么主题"刷到状态行与切换按钮上
 * @param app 所属应用
 * @return void
 * @note 界面文字要跟得上状态,否则按钮点完还写着"切换到浅色",人会以为没生效。
 */
static void sync_theme_ui(cg_app_t *app)
{
    int dark = cg_theme_is_dark(app);

    cg_widget_set_text(cg_find(app, "btn_theme"),
                       dark ? "切换到浅色主题" : "切换到深色主题");
    cg_widget_set_text(cg_find(app, "lab_mode"),
                       dark ? "当前:深色(app.dark: true)"
                            : "当前:浅色(运行时用 cg_theme_set_dark 切的)");
}

/**
 * @brief 「切换主题」按钮:运行时在深色 / 浅色之间来回切
 * @param self 触发的按钮
 * @param ev click
 * @param user 未使用
 * @return void
 * @note 这是整个示例的重点。`cg_theme_set_dark()` 会立即重画:窗口底色、自绘面板的
 *       "背后色"、没写颜色控件的字色一起换;写死了 fg/bg 的控件**保持原样** ------
 *       那条边界就是本示例想让人看见的东西。
 */
static void on_theme_click(cg_widget_t *self, cg_event_t ev, void *user)
{
    cg_app_t *app  = cg_app_of(self);
    int       dark;

    (void)ev;
    (void)user;
    if (!cg_app_has_window(app)) return;      /* --selftest:没有窗口,别动界面 */

    dark = !cg_theme_is_dark(app);
    cg_theme_set_dark(app, dark);             /* 1 = 深色,0 = 浅色;语义与 app.dark 一致 */
    sync_theme_ui(app);
    log_line(app, "-- ", dark ? "已切到深色:cg_theme_set_dark(app, 1)"
                              : "已切到浅色:cg_theme_set_dark(app, 0),写死了颜色的控件不会跟着变。");
}

/* ------------------------------------------------------------ 日志视图的按钮 */

/**
 * @brief 「清空日志」按钮:丢掉缓冲与视图内容
 * @param self 触发的按钮
 * @param ev click
 * @param user 未使用
 * @return void
 */
static void on_clear_click(cg_widget_t *self, cg_event_t ev, void *user)
{
    cg_app_t *app = cg_app_of(self);

    (void)ev;
    (void)user;
    if (!cg_app_has_window(app)) return;

    s_biz.len    = 0;
    s_biz.buf[0] = '\0';
    cg_widget_set_text(cg_find(app, "edt_log"), "");
    log_line(app, "-- ", "日志已清空(缓冲与视图一起丢)。");
}

/**
 * @brief 「追加几行」按钮:往日志视图里塞几行演示内容
 * @param self 触发的按钮
 * @param ev click
 * @param user 未使用
 * @return void
 * @note 多行 edit 只有一个文字色(styles.log 的浅绿),所以 RX / TX 只靠行首记号区分 ------
 *       要按方向上色就得用 terminal(它的行色现在分浅底 / 深底两套,深色下自动用亮色,见文件头说明)。
 */
static void on_fill_click(cg_widget_t *self, cg_event_t ev, void *user)
{
    cg_app_t *app = cg_app_of(self);

    (void)ev;
    (void)user;
    if (!cg_app_has_window(app)) return;

    log_line(app, "RX ", "OK 19200 8N1");
    log_line(app, "TX ", "AT+VERSION");
    log_line(app, "-- ", "上面两行按 RX(收到)/ TX(发出)记,靠行首记号区分方向。");
}

/**
 * @brief 「自动滚动到底」开关:追加日志后要不要滚到最新一行
 * @param self 开关控件
 * @param ev change
 * @param user 未使用
 * @return void
 */
static void on_scroll_change(cg_widget_t *self, cg_event_t ev, void *user)
{
    cg_app_t *app = cg_app_of(self);

    (void)ev;
    (void)user;
    if (!cg_app_has_window(app)) return;

    s_biz.autoscroll = cg_widget_checked(self);
    log_line(app, "-- ", s_biz.autoscroll ? "自动滚动:开(新内容出现时滚到底)。"
                                          : "自动滚动:关(新内容还在,只是视图不跟着滚)。");
}

/**
 * @brief 「保存日志」按钮:把日志视图存成 UTF-8 文本文件
 * @param self 触发的按钮
 * @param ev click
 * @param user 未使用
 * @return void
 * @note 自检模式下绝不能弹"另存为",所以先问 cg_app_has_window()。
 */
static void on_save_log_click(cg_widget_t *self, cg_event_t ev, void *user)
{
    cg_app_t *app = cg_app_of(self);
    char      path[1024];

    (void)ev;
    (void)user;
    if (!cg_app_has_window(app)) return;

    path[0] = '\0';
    if (!cg_save_file_dialog(self, "保存日志", "文本文件(*.txt)|*.txt|所有文件(*.*)|*.*",
                             "txt", path, sizeof(path))) {
        return;
    }
    if (cg_file_write(path, s_biz.buf) != 0) {
        log_line(app, "-- ", "日志没写出去(路径不可写或被占用)。");
        return;
    }
    {
        char *line = cg_str_format("日志(%d 字符)已存到 %s", s_biz.len, path);

        if (line) {
            log_line(app, "-- ", line);
            cg_free(line);
        }
    }
}

/* ------------------------------------------------------------ 命令输入 */

/**
 * @brief 发送输入框里的内容:按 TX 记一行
 * @param app 所属应用
 * @return void
 * @note 这里**不发真数据** ------ 示例只演示深色,命令只是回显,免得没接硬件时让人以为坏了。
 */
static void do_send(cg_app_t *app)
{
    cg_widget_t *edt  = cg_find(app, "edt_cmd");
    const char  *text = cg_widget_text(edt);

    if (!text || !*text) {
        log_line(app, "-- ", "输入框是空的,没东西可发。");
        return;
    }
    log_line(app, "TX ", text);
    cg_widget_set_text(edt, "");              /* text 已经用完了,再清空,避免悬空 */
}

/**
 * @brief 「发送」按钮
 * @param self 触发的按钮
 * @param ev click
 * @param user 未使用
 * @return void
 */
static void on_send_click(cg_widget_t *self, cg_event_t ev, void *user)
{
    cg_app_t *app = cg_app_of(self);

    (void)ev;
    (void)user;
    if (!cg_app_has_window(app)) return;
    do_send(app);
}

/**
 * @brief 输入框里按回车(等价于点「发送」)
 * @param self 输入框
 * @param ev submit
 * @param user 未使用
 * @return void
 */
static void on_cmd_submit(cg_widget_t *self, cg_event_t ev, void *user)
{
    cg_app_t *app = cg_app_of(self);

    (void)ev;
    (void)user;
    if (!cg_app_has_window(app)) return;
    do_send(app);
}

/* ------------------------------------------------------------ 启动 */

/** 事件名 -> C 函数 的登记表(右边这些名字写在 darkterm.cgui 里) */
static const cg_binding_t k_bindings[] = {
    CG_BIND("on_theme_click",   on_theme_click),
    CG_BIND("on_clear_click",   on_clear_click),
    CG_BIND("on_fill_click",    on_fill_click),
    CG_BIND("on_scroll_change", on_scroll_change),
    CG_BIND("on_save_log_click", on_save_log_click),
    CG_BIND("on_send_click",    on_send_click),
    CG_BIND("on_cmd_submit",    on_cmd_submit),
    CG_BIND_END
};

/**
 * @brief 程序入口:按命令行参数走自检或正常运行
 * @param argc 参数个数
 * @param argv 参数数组(argv[1] = .cgui 路径,或 --selftest)
 * @return int 进程退出码(0 正常)
 */
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 */

    s_biz.autoscroll = 1;           /* 与 .cgui 里 sw_scroll 的 checked 一致 */

    if (argc > 1 && strcmp(argv[1], "--selftest") == 0) {
        desc.cgui_path = (argc > 2) ? argv[2] : "darkterm.cgui";
        return cg_app_selftest(&desc);
    }
    desc.cgui_path = (argc > 1) ? argv[1] : "darkterm.cgui";
    return cg_app_run(&desc);
}

问题排查速查表

现象 原因 解决方法
切换深色后,某些控件颜色不变 控件自己写了 fg / bg,深色只换兜底配色,写死的颜色不跟随 去掉控件上的 fg / bg,把颜色统一收敛到顶层 styles{} 命名样式表里
深色界面里出现一块白色区域 用了 listbox / table / tree / tabs / 下拉列表 / 状态栏等系统经典控件,公开接口改不动它们的主题 换用本示例同款的自绘控件(如 badge、progress、switch),或干脆不用这些经典控件
日志追加后不自动滚到底 多行 edit 没有 terminal 的"自动滚 + 按行着色 + 行数上限"托管 追加文本后手动调用 cg_edit_scroll_end;文本用环形缓冲攒着,多了自己收着点

下面是深色主题下控件跟随/不跟随的判定逻辑:
#mermaid-svg-Oi1t0ReSH6fVFfRR{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-Oi1t0ReSH6fVFfRR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Oi1t0ReSH6fVFfRR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Oi1t0ReSH6fVFfRR .error-icon{fill:#552222;}#mermaid-svg-Oi1t0ReSH6fVFfRR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Oi1t0ReSH6fVFfRR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Oi1t0ReSH6fVFfRR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Oi1t0ReSH6fVFfRR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Oi1t0ReSH6fVFfRR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Oi1t0ReSH6fVFfRR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Oi1t0ReSH6fVFfRR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Oi1t0ReSH6fVFfRR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Oi1t0ReSH6fVFfRR .marker.cross{stroke:#333333;}#mermaid-svg-Oi1t0ReSH6fVFfRR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Oi1t0ReSH6fVFfRR p{margin:0;}#mermaid-svg-Oi1t0ReSH6fVFfRR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Oi1t0ReSH6fVFfRR .cluster-label text{fill:#333;}#mermaid-svg-Oi1t0ReSH6fVFfRR .cluster-label span{color:#333;}#mermaid-svg-Oi1t0ReSH6fVFfRR .cluster-label span p{background-color:transparent;}#mermaid-svg-Oi1t0ReSH6fVFfRR .label text,#mermaid-svg-Oi1t0ReSH6fVFfRR span{fill:#333;color:#333;}#mermaid-svg-Oi1t0ReSH6fVFfRR .node rect,#mermaid-svg-Oi1t0ReSH6fVFfRR .node circle,#mermaid-svg-Oi1t0ReSH6fVFfRR .node ellipse,#mermaid-svg-Oi1t0ReSH6fVFfRR .node polygon,#mermaid-svg-Oi1t0ReSH6fVFfRR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Oi1t0ReSH6fVFfRR .rough-node .label text,#mermaid-svg-Oi1t0ReSH6fVFfRR .node .label text,#mermaid-svg-Oi1t0ReSH6fVFfRR .image-shape .label,#mermaid-svg-Oi1t0ReSH6fVFfRR .icon-shape .label{text-anchor:middle;}#mermaid-svg-Oi1t0ReSH6fVFfRR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Oi1t0ReSH6fVFfRR .rough-node .label,#mermaid-svg-Oi1t0ReSH6fVFfRR .node .label,#mermaid-svg-Oi1t0ReSH6fVFfRR .image-shape .label,#mermaid-svg-Oi1t0ReSH6fVFfRR .icon-shape .label{text-align:center;}#mermaid-svg-Oi1t0ReSH6fVFfRR .node.clickable{cursor:pointer;}#mermaid-svg-Oi1t0ReSH6fVFfRR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Oi1t0ReSH6fVFfRR .arrowheadPath{fill:#333333;}#mermaid-svg-Oi1t0ReSH6fVFfRR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Oi1t0ReSH6fVFfRR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Oi1t0ReSH6fVFfRR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Oi1t0ReSH6fVFfRR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Oi1t0ReSH6fVFfRR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Oi1t0ReSH6fVFfRR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Oi1t0ReSH6fVFfRR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Oi1t0ReSH6fVFfRR .cluster text{fill:#333;}#mermaid-svg-Oi1t0ReSH6fVFfRR .cluster span{color:#333;}#mermaid-svg-Oi1t0ReSH6fVFfRR 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-Oi1t0ReSH6fVFfRR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Oi1t0ReSH6fVFfRR rect.text{fill:none;stroke-width:0;}#mermaid-svg-Oi1t0ReSH6fVFfRR .icon-shape,#mermaid-svg-Oi1t0ReSH6fVFfRR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Oi1t0ReSH6fVFfRR .icon-shape p,#mermaid-svg-Oi1t0ReSH6fVFfRR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Oi1t0ReSH6fVFfRR .icon-shape .label rect,#mermaid-svg-Oi1t0ReSH6fVFfRR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Oi1t0ReSH6fVFfRR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Oi1t0ReSH6fVFfRR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Oi1t0ReSH6fVFfRR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
控件
是否写死 fg/bg
颜色不跟随主题
是否为系统经典控件
listbox / table / tree / tabs / 下拉列表 / 状态栏
公开接口改不动主题
深色界面里摆一块白
自绘控件
progress 槽 / switch / checkbox 填充
皮肤写死浅色,不跟随
progressring 轨道 / 没写颜色的文字
跟随主题

相关推荐
新晨单片机设计2 小时前
S008C-基于STM32单片机无线抢答器【Proteus仿真+Keil程序+原理图】
stm32·单片机·proteus·无线抢答器
小诗懂技术3 小时前
【裸机lcd】framebuffer的配置与小型图像库的移植
arm开发·单片机·嵌入式硬件·裸机
comyccccc4 小时前
3.3V稳压电源设计与PCB实现学习笔记
单片机·嵌入式硬件
小程序设计4 小时前
基于STM32森林火灾远程监测模块设计
stm32·单片机·嵌入式硬件
魔猴疯猿4 小时前
香橙派(Orange Pi)入门使用教程:从开箱到跑通第一个项目
单片机·嵌入式硬件·物联网
追涨杀跌老能手5 小时前
GD32H759 + RT-Thread 工控实战--第7篇 OSPI Flash
开源·嵌入式
丸子君君6 小时前
【以麒麟服务器时间为准,定时请求时间同步】
运维·服务器·单片机
沐欣工作室_lvyiyi6 小时前
基于STM32单片机汽车充电桩控制模块设计(论文+源码)
stm32·单片机·汽车·毕业设计
嵌入式-老费6 小时前
esp32开发与应用(用Python+PySide6解决上位机开发)
linux·单片机·嵌入式硬件