cpp/ui/demo_ui.cpp · 14259 字节 · 下载原文
// demo_ui.cpp —— **第 4 层:最新 UI 接口**(`ui_*` / `Lib/c/vml_ui.h`)
// demo_ui.cpp — **Layer 4: the newest UI interface** (`ui_*` / `Lib/c/vml_ui.h`)
//
// 与 `Examples/c/demo_ui.c` 同构:开 UI 窗口 → 查屏幕方向 → 按画布尺寸排版 → 画图元
// It has the same structure as `Examples/c/demo_ui.c`: open a UI window → query the screen orientation → lay out per canvas size → draw primitives
// → `ui_2d_present()` → 消息循环(触摸 / 按键 / 定时器 / 转屏)→ **有界退出**。
// → `ui_2d_present()` → message loop (touch / keys / timer / rotation) → **bounded exit**.
//
// ## 这一层与 BGI(第 3 层)的差别
// ## How this layer differs from BGI (Layer 3)
//
// · **没有固定分辨率**:画布多大由宿主给(`ui_scr_get_w/h`),程序现排版。
// · **No fixed resolution**: the host decides how big the canvas is (`ui_scr_get_w/h`), and the program lays out on the fly.
// · **真彩**:`0xAARRGGBB`,不是 16 个索引色。
// · **True color**: `0xAARRGGBB`, not 16 indexed colors.
// · **有消息循环**:触摸/按键/定时器/窗口被关/被转屏都从 `ui_msg_wait` 出来。
// · **It has a message loop**: touch/key/timer/window-closed/rotation all come out of `ui_msg_wait`.
//
// ## 为什么这一层不走 `graphics.h`
// ## Why this layer does not go through `graphics.h`
//
// `graphics.h` 是给**老程序**的兼容垫层(BGI 那套写死的坐标系)。新写的程序直接用
// `graphics.h` is a compatibility shim for **old programs** (BGI's hardcoded coordinate system). Newly written programs go
// `vml_ui.h` —— 尤其是"要把版面跟着屏幕尺寸排"的程序,BGI 那套固定坐标反而碍事。
// straight to `vml_ui.h` — especially programs that have to lay their layout out following the screen size, where BGI's fixed coordinates get in the way.
//
// ## ⚠ C++ 前端的两条坑(本 demo 的写法就是为绕开它们)
// ## ⚠ Two pitfalls of the C++ frontend (this demo is written the way it is to avoid them)
//
// · `cout << <char* 变量>` 打的是**地址**(详见 `demo_std.cpp` 的文件头)⇒
// · `cout << <char* variable>` prints the **address** (see the file header of `demo_std.cpp` for details) ⇒
// 这里要印字符串变量一律 `printf("%s", …)`。
// here, printing a string variable always goes through `printf("%s", …)`.
// · 类的方法**不生成函数体**(`未定义的函数 'method_…'`)⇒ 全部写成自由函数 +
// · Class methods **do not get a generated body** (an "undefined function 'method_…'" error) ⇒ write everything as free functions +
// 一个文件级数组(与 `cpp/snake.cpp` 同一路子)。
// one file-level array (the same approach as `cpp/snake.cpp`).
//
// 跑法:手机 `vml run examples/cpp/demo_ui.cpp`;
// How to run: on the phone `vml run examples/cpp/demo_ui.cpp`;
// 桌面 `vmlcli Examples/cpp/demo_ui.cpp --frames /tmp/fr`
// on the desktop `vmlcli Examples/cpp/demo_ui.cpp --frames /tmp/fr`
#include <vml_ui.h>
#include <stdio.h>
#define MAX_FRAMES 60 // 到点自己停:60 拍 × 60ms ≈ 3.6 秒
// stops by itself when it is reached: 60 ticks × 60ms ≈ 3.6 seconds
// 版面变量:每次重排都按当前画布尺寸算(**不写死**)
// Layout variables: recomputed from the current canvas size on every re-layout (**never hardcoded**)
static int SW;
static int SH;
static int GY;
// 计数:证明这些事件真的到过
// Counters: proof that these events really did arrive
static int g_frames;
static int g_keys;
static int g_touches;
static int g_orient;
// 最近一次触摸的位置(-1 = 还没点过)
// Position of the most recent touch (-1 = not touched yet)
static int g_tx = -1;
static int g_ty = -1;
// 界面语言:开局查一次(`ui_scr_get_language` 是 syscall,别每帧调)
// UI language: queried once at startup (`ui_scr_get_language` is a syscall, do not call it every frame)
static int gLang;
// 数字 → 字符串。不用 std::string(C++ 前端整体不可用),也不用 sprintf(各前端形态不一),
// Number → string. No std::string (unusable across the C++ frontend), and no sprintf (its form differs per frontend),
// 自己按位拆最省事 —— 与仓库里其它语言的例程同一做法。
// so splitting the digits out yourself is the least hassle — the same approach as the examples in the other languages in this repo.
static char g_num[12];
static char *numstr(int v)
{
int n = 0;
int m = 0;
char tmp[12];
if (v < 0) { g_num[0] = '-'; v = -v; n = 1; }
if (v == 0) { g_num[n] = '0'; g_num[n + 1] = 0; return g_num; }
while (v > 0) { tmp[m] = (char)('0' + v % 10); v = v / 10; m = m + 1; }
while (m > 0) { m = m - 1; g_num[n] = tmp[m]; n = n + 1; }
g_num[n] = 0;
return g_num;
}
static void relayout()
{
SW = ui_scr_get_w();
SH = ui_scr_get_h();
if (SW <= 0) SW = 360;
if (SH <= 0) SH = 620;
/* 内容整块**在画布内垂直居中**(用户 2026-10-07 报的「很多程序文字没居中、很靠上」):
* 这一份的控件是**从 GY 往下逐项堆**的,最后一行落在 GY+276 附近 ⇒ 整块高约 300。
* 原先写死 `GY = 14` ⇒ 内容只占画布上部三成,下面一大条空白 —— 看着就是"全挤在顶上"。
* 居中之后顶部与底部留白相等(`SH` 是**画布**高,见 `relayout` 开头)。
* ⚠ 下限保 14:画布特别矮时(横屏分屏)别把内容推出上边缘。
* Center the whole block vertically (user report, 2026-10-07). The widgets are stacked downward
* from GY and the last line sits near GY+276, so the block is about 300 tall; with a fixed
* `GY = 14` it occupied only the top third. Clamp at 14 so a very short canvas cannot push it off. */
GY = (SH - 300) / 2;
if (GY < 14) GY = 14;
}
static void draw()
{
int cx = SW / 2;
int pad = SW / 16;
int i;
int w;
ui_2d_clear(0xFF101018);
// ── 标题 + 方向 + 画布尺寸 ──
// ── Title + orientation + canvas size ──
ui_2d_text_styled(cx, GY, gLang == 0 ? "UI 接口 / demo_ui.cpp" : "UI interface / demo_ui.cpp", 0xFFE8E8F0, 16,
VML_ANCHOR_CENTER, VML_FONT_BOLD);
if (g_orient == VML_ORIENT_LANDSCAPE) {
ui_2d_text(cx, GY + 26, gLang == 0 ? "屏幕方向 = 横屏 (LANDSCAPE)" : "Orientation = LANDSCAPE", 0xFF51E86E, 13, VML_ANCHOR_CENTER);
} else {
ui_2d_text(cx, GY + 26, gLang == 0 ? "屏幕方向 = 竖屏 (PORTRAIT)" : "Orientation = PORTRAIT", 0xFF51E86E, 13, VML_ANCHOR_CENTER);
}
ui_2d_text(cx, GY + 46, gLang == 0 ? "画布按宿主给的尺寸现排" : "Canvas laid out from the host's size", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER);
// ── 跟随尺寸的方框:旋转后跟着变宽变矮,这就是"不写死坐标"的证明 ──
// ── A box that follows the size: after rotation it gets wider and shorter, and that is the proof of "no hardcoded coordinates" ──
ui_2d_rect(pad, GY + 70, SW - pad * 2, 90, 0xFF1A1A24, 1, 0, 10);
ui_2d_rect(pad + 6, GY + 76, SW - pad * 2 - 12, 30, 0xFF4A90D9, 1, 0, 6);
ui_2d_text(pad + 16, GY + 84, gLang == 0 ? "rect / round-rect(随屏宽伸缩)" : "rect / round-rect (stretches with the width)", 0xFF101018, 12,
VML_ANCHOR_LEFT);
// ── 圆 / 椭圆 / 直线 ──
// ── Circle / ellipse / line ──
ui_2d_circle(cx - SW / 6, GY + 140, SW / 12, 0xFFE06C50, 1, 0);
ui_2d_ellipse(cx + SW / 6, GY + 140, SW / 9, SW / 18, 0xFFD9B44A, 1, 0);
ui_2d_line(pad, GY + 172, SW - pad, GY + 172, 0xFF50C878, 3);
// ── 8 格真彩色带 ──
// ── The 8-cell true-color band ──
w = (SW - pad * 2) / 8;
for (i = 0; i < 8; i++) {
if ((i & 1) != 0) {
ui_2d_rect(pad + i * w, GY + 186, w - 2, 16, 0xFF4A90D9, 1, 0, 2);
} else {
ui_2d_rect(pad + i * w, GY + 186, w - 2, 16, 0xFFE06C50, 1, 0, 2);
}
}
ui_2d_text(cx, GY + 210, gLang == 0 ? "真彩 0xAARRGGBB(不是索引色)" : "True color 0xAARRGGBB (not indexed)", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER);
// ── 事件计数 ──
// ── Event counters ──
ui_2d_text(pad, GY + 236, gLang == 0 ? "帧" : "Frames", 0xFF9AA0B0, 12, VML_ANCHOR_LEFT);
ui_2d_text(pad + 22, GY + 236, numstr(g_frames), 0xFFFFFFFF, 13, VML_ANCHOR_LEFT);
ui_2d_text(pad, GY + 256, gLang == 0 ? "按键" : "Keys", 0xFF9AA0B0, 12, VML_ANCHOR_LEFT);
ui_2d_text(pad + 44, GY + 256, numstr(g_keys), 0xFFFFFFFF, 13, VML_ANCHOR_LEFT);
ui_2d_text(pad, GY + 276, gLang == 0 ? "触摸" : "Touches", 0xFF9AA0B0, 12, VML_ANCHOR_LEFT);
ui_2d_text(pad + 44, GY + 276, numstr(g_touches), 0xFFFFFFFF, 13, VML_ANCHOR_LEFT);
// ── 触摸标记:点哪儿就在哪儿留个圈 ──
// ── Touch marks: wherever you tap, a circle is left there ──
if (g_tx >= 0) {
ui_2d_circle(g_tx, g_ty, 18, 0xFFFFE060, 0, 2);
ui_2d_circle(g_tx, g_ty, 4, 0xFFFFE060, 1, 0);
ui_2d_text(cx, SH - 44, gLang == 0 ? "触摸坐标已经用上了" : "Touch coordinates are in use", 0xFFFFE060, 12, VML_ANCHOR_CENTER);
} else {
ui_2d_text(cx, SH - 44, gLang == 0 ? "点一下屏幕 / 按任意键退出" : "Tap the screen / press any key to exit", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER);
}
ui_2d_text(cx, SH - 24, gLang == 0 ? "退出:按任意键或点任意处(或等 N 帧到点)" : "Exit: any key or any tap (or wait out the frame count)",
0xFF9AA0B0, 12, VML_ANCHOR_CENTER);
ui_2d_present();
}
int main()
{
int msg[4];
int tid;
int done = 0;
int t;
// ── ① 开窗**之前**就问方向 ──
// ── ① Ask for the orientation **before** opening the window ──
g_orient = ui_scr_get_orientation();
gLang = ui_scr_get_language();
// 开窗:尺寸传 0 ⇒ **交给宿主用真实可用区**。开窗**之前** `ui_scr_get_w/h` 只能估算,
// Open the window: pass size 0 ⇒ **the host uses the real available area**. **Before** the window opens,
// 估算与实际总有出入(这就是"内容偏上"的根因);开窗后 `relayout()` 再问一次拿到的是**真实画布**。
// `ui_scr_get_w/h` can only estimate (that is exactly why the content sat high); after the open, `relayout()` re-queries and gets the **real canvas**.
// 其余声明照旧:"支持旋转 + 要手柄"(转屏时宿主会把新坐标空间整个给过来)。
// The rest is unchanged: "supports rotation + wants the gamepad" (on rotation the host hands over the whole new coordinate space).
ui_win_open_ex("demo_ui.cpp", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NEED_GAMEPAD);
relayout();
draw();
// ── ⑤ 消息循环:定时器驱动重画,**有界退出** ──
// ── ⑤ Message loop: the timer drives the redraws, **bounded exit** ──
tid = ui_timer_set(60, 1);
while (done == 0 && ui_win_is_closed() == 0) {
t = ui_msg_wait(msg, 0);
if (t == VML_MSG_NONE) continue;
if (t == VML_MSG_TIMER) {
g_frames = g_frames + 1;
draw();
if (g_frames >= MAX_FRAMES) done = 1; // ← 有界:到点自己停
// ← bounded: stops by itself when the frame budget runs out
continue;
}
if (t == VML_MSG_KEYDOWN) {
g_keys = g_keys + 1;
draw();
done = 1; // ← 有界:收到键就退
// ← bounded: exits as soon as a key arrives
continue;
}
if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) {
g_touches = g_touches + 1;
g_tx = msg[1]; // msg[1]=x msg[2]=y
g_ty = msg[2];
draw();
done = 1; // ← 有界:点到就退
// ← bounded: exits as soon as a tap arrives
continue;
}
if (t == VML_MSG_WINDOWORIENT) {
g_orient = msg[1]; // A = 新方向
// A = the new orientation
continue;
}
if (t == VML_MSG_WINDOWRESIZE) {
relayout(); // 换坐标系了 ⇒ 重排 + 重画
// the coordinate system changed ⇒ re-layout + redraw
draw();
continue;
}
if (t == VML_MSG_WINDOWCLOSE) break; // 用户把窗口关了
// the user closed the window
}
ui_timer_kill(tid);
ui_win_close();
// 给无头验证留一行确定性判据(图形只能肉眼看,这几个数能自动比)
// Leave one line of deterministic criteria for headless verification (graphics can only be eyeballed; these numbers can be compared automatically)
if (g_orient == VML_ORIENT_LANDSCAPE) {
printf("demo_ui: orient=LANDSCAPE\n");
} else {
printf("demo_ui: orient=PORTRAIT\n");
}
printf("demo_ui: canvas=%dx%d\n", SW, SH);
// ⚠ 三次 `numstr` **必须分开 printf**:它返回的是**同一个静态缓冲区**,
// ⚠ The three `numstr` calls **must be printed with separate printf calls**: it returns **the same static buffer**,
// 三个一起当实参传进去会全部指向那块内存、打印出同一个值(经典别名坑)。
// so passing all three together as arguments makes them all point at that memory and print the same value (the classic aliasing trap).
printf("demo_ui: frames=%s\n", numstr(g_frames));
printf("demo_ui: keys=%s\n", numstr(g_keys));
printf("demo_ui: touches=%s\n", numstr(g_touches));
printf("demo_ui: done\n");
return 0;
}