UI 菜单初步完成

This commit is contained in:
2026-09-07 10:48:18 +08:00
parent 89462d8aa1
commit 2b41bdccb5
30 changed files with 2226 additions and 859 deletions
+165 -210
View File
@@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 12 px
* Bpp: 1
* Opts: --font C:\Users\Administrator\AppData\Local\Temp\simsun_regular.ttf --size 12 --bpp 1 -r 0x20-0x7E --symbols ▶三主事交件位作信值元入全出制前动厂压参双变号告回复字定实家开式强归当录恢息手护据操故数无时显本查检模波流测版电界直看示系络维网置自节装警认讯记设询调返通遥部配量间零面默: --no-compress --format lvgl --lv-font-name ui_font_cn_12 -o D:\Code\DTU Code\STM32F4-Base\Drivers\BSP\UI\ui_font_cn_12.c
* Opts: --font C:\Users\ADMINI~1\AppData\Local\Temp\simsun_regular.ttf --size 12 --bpp 1 -r 0x20-0x7E --symbols ▶三主事交作保信值制前动区压参双口启告型复字定实式强归当录息手抖护报据控操故数整无时板查检模死波流测清点界直看类系维网置自行装警讯记设询软运通遥量钟防除面: --no-compress --format lvgl --lv-font-name ui_font_cn_12 -o D:\Code\DTU Code\STM32F4-Base\Drivers\BSP\UI\ui_font_cn_12.c
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@@ -327,21 +327,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x14, 0x50, 0x50, 0x4, 0x1, 0x40, 0xc6, 0x60,
0x30,
/* U+4EF6 "件" */
0x21, 0x5, 0x21, 0x24, 0x27, 0xfd, 0x10, 0x82,
0x10, 0x42, 0xff, 0x41, 0x8, 0x21, 0x4, 0x20,
0x80,
/* U+4F4D "位" */
0x22, 0x4, 0x21, 0x0, 0x2f, 0xfc, 0x0, 0x90,
0x92, 0x12, 0x22, 0x44, 0x88, 0x91, 0x4, 0x2f,
0xf0,
/* U+4F5C "作" */
0x14, 0x2, 0x80, 0x9f, 0x95, 0x6, 0x21, 0x47,
0x88, 0x81, 0x10, 0x23, 0xe4, 0x40, 0x88, 0x11,
0x0,
/* U+4FDD "保" */
0x10, 0x2, 0xf8, 0x91, 0x12, 0x26, 0x7d, 0x42,
0xb, 0xf9, 0x8, 0x23, 0x84, 0xa8, 0xa4, 0x90,
0x80,
/* U+4FE1 "信" */
0x22, 0x4, 0x21, 0x7f, 0xa0, 0xc, 0xfc, 0x80,
0x13, 0xf2, 0x0, 0x4f, 0xc9, 0x9, 0x3f, 0x24,
@@ -352,25 +347,6 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x92, 0x12, 0x7e, 0x48, 0x49, 0xf9, 0x21, 0x2f,
0xf0,
/* U+5143 "元" */
0x7f, 0xc0, 0x0, 0x0, 0x0, 0xf, 0xfe, 0x22,
0x4, 0x40, 0x88, 0x21, 0x28, 0x26, 0x3, 0x80,
/* U+5165 "入" */
0x30, 0x1, 0x0, 0x10, 0x2, 0x0, 0xa0, 0x14,
0x2, 0x80, 0x88, 0x11, 0x4, 0x11, 0x1, 0x40,
0x10,
/* U+5168 "全" */
0x4, 0x1, 0x40, 0x44, 0x10, 0x4c, 0x6, 0x7f,
0x1, 0x0, 0x20, 0x7f, 0xc0, 0x80, 0x10, 0x7f,
0xf0,
/* U+51FA "出" */
0x4, 0x8, 0x89, 0x11, 0x22, 0x27, 0xfc, 0x8,
0x1, 0x4, 0x21, 0x84, 0x30, 0x87, 0xff, 0x80,
0x10,
/* U+5236 "制" */
0x10, 0x2a, 0x5, 0xf4, 0xc8, 0x9f, 0xf2, 0x22,
0x5f, 0x4a, 0xa9, 0x55, 0x2a, 0x85, 0x70, 0x88,
@@ -386,9 +362,9 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x50, 0x4a, 0x89, 0x89, 0x3f, 0x44, 0x8, 0x82,
0x60,
/* U+5382 "厂" */
0x7f, 0xe8, 0x1, 0x0, 0x20, 0x4, 0x0, 0x80,
0x10, 0x2, 0x0, 0x40, 0x8, 0x2, 0x0, 0x0,
/* U+533A "区" */
0xff, 0xe0, 0xa, 0xa, 0x44, 0x8a, 0x21, 0x8,
0xa2, 0x44, 0xa0, 0xa0, 0xf, 0xfc,
/* U+538B "压" */
0x7f, 0xe8, 0x1, 0x8, 0x21, 0x4, 0x20, 0xbf,
@@ -403,24 +379,24 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0xfb, 0xe1, 0x44, 0x28, 0xc5, 0x15, 0x14, 0xa2,
0x88, 0x21, 0x4, 0x51, 0x4a, 0x4a, 0x10, 0x80,
/* U+53D8 "变" */
0x4, 0x1f, 0xfc, 0x44, 0x28, 0xa9, 0x12, 0x0,
0x1f, 0xe1, 0x4, 0x11, 0x1, 0xc0, 0xc6, 0x60,
0x30,
/* U+53E3 "口" */
0xff, 0xc0, 0x60, 0x30, 0x18, 0xc, 0x6, 0x3,
0x1, 0xff, 0xc0, 0x40,
/* U+53F7 "号" */
0x3f, 0x84, 0x10, 0x82, 0x1f, 0xc0, 0x1, 0xff,
0xc4, 0x1, 0x0, 0x3f, 0xc0, 0x8, 0x1, 0x0,
0xc0,
/* U+542F "启" */
0x4, 0x0, 0x40, 0xff, 0x90, 0x12, 0x2, 0x7f,
0xc8, 0x1, 0xff, 0x30, 0x2a, 0x5, 0x7f, 0xc8,
0x10,
/* U+544A "告" */
0x4, 0x4, 0x80, 0xff, 0x22, 0x0, 0x41, 0xff,
0xc0, 0x3, 0xfe, 0x40, 0x48, 0x9, 0xff, 0x20,
0x20,
/* U+56DE "回" */
0xff, 0xe0, 0x18, 0x6, 0x79, 0x92, 0x64, 0x99,
0xe6, 0x1, 0x80, 0x7f, 0xf8, 0x4,
/* U+578B "型" */
0x0, 0x2f, 0xd4, 0x92, 0x92, 0x5f, 0xea, 0x49,
0x51, 0xc, 0x3, 0x4, 0xf, 0xf8, 0x10, 0x7f,
0xf0,
/* U+590D "复" */
0x3f, 0xe8, 0x2, 0xff, 0x10, 0x23, 0xfc, 0x40,
@@ -442,15 +418,6 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x4, 0x87, 0xff, 0x4, 0x1, 0x30, 0xc1, 0x60,
0x10,
/* U+5BB6 "家" */
0x4, 0x1f, 0xfe, 0x0, 0xbf, 0xe0, 0x80, 0x28,
0x9a, 0xa0, 0x98, 0x66, 0x83, 0x4f, 0x88, 0x6,
0x0,
/* U+5F00 "开" */
0x7f, 0xc2, 0x20, 0x44, 0x8, 0x81, 0x11, 0xff,
0xc4, 0x40, 0x88, 0x21, 0x4, 0x21, 0x4, 0x0,
/* U+5F0F "式" */
0x1, 0x40, 0x24, 0x4, 0x7f, 0xf0, 0x10, 0x2,
0x1f, 0x40, 0x88, 0x10, 0x82, 0x14, 0x79, 0xf0,
@@ -475,11 +442,6 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x11, 0x11, 0x74, 0x15, 0x4, 0x93, 0x11, 0x86,
0x0,
/* U+6062 "恢" */
0x24, 0x4, 0x80, 0xbf, 0xda, 0xa, 0x49, 0x49,
0x6a, 0xb1, 0x54, 0x28, 0x85, 0x28, 0xc9, 0x12,
0x10,
/* U+606F "息" */
0x8, 0xf, 0xf9, 0x1, 0x3f, 0xe4, 0x4, 0xff,
0x90, 0x13, 0xfe, 0x4, 0xa, 0x49, 0x42, 0xc7,
@@ -490,16 +452,31 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x1, 0x7, 0xff, 0x4, 0x0, 0x80, 0x10, 0xe,
0x0,
/* U+6296 "抖" */
0x20, 0x44, 0x48, 0x85, 0x7c, 0x22, 0x24, 0x42,
0x8c, 0x17, 0x3, 0x27, 0xc4, 0x8, 0x81, 0x30,
0x20,
/* U+62A4 "护" */
0x21, 0x4, 0x10, 0x9f, 0xfa, 0x12, 0x42, 0x48,
0x4d, 0xff, 0x21, 0x24, 0x4, 0x80, 0x90, 0x34,
0x0,
/* U+62A5 "报" */
0x20, 0x4, 0xfc, 0x90, 0xfa, 0x12, 0x4c, 0x48,
0xd, 0xff, 0x29, 0x25, 0x44, 0x90, 0x95, 0x33,
0x10,
/* U+636E "据" */
0x20, 0x4, 0xfc, 0x90, 0xfb, 0xf2, 0x48, 0x4f,
0xcd, 0x27, 0x3f, 0x26, 0x25, 0x44, 0xaf, 0xb9,
0x10,
/* U+63A7 "控" */
0x42, 0x8, 0x21, 0x3f, 0xf4, 0x14, 0x28, 0x88,
0x9a, 0xe, 0x3e, 0x41, 0x8, 0x21, 0x4, 0x67,
0xf0,
/* U+64CD "操" */
0x27, 0xc4, 0x88, 0x9f, 0x78, 0x2, 0xee, 0x55,
0x4f, 0xbf, 0x8, 0x2f, 0xe4, 0x70, 0x95, 0x34,
@@ -515,6 +492,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0xa5, 0x51, 0xa, 0xf9, 0x49, 0x10, 0xc5, 0x65,
0x10,
/* U+6574 "整" */
0x11, 0x1f, 0xbd, 0x59, 0x3e, 0xa3, 0x88, 0xaa,
0xa4, 0x8f, 0xff, 0x4, 0x4, 0xf8, 0x90, 0x7f,
0xf0,
/* U+65E0 "无" */
0x7f, 0xc0, 0x80, 0x10, 0x2, 0xf, 0xfe, 0x14,
0x2, 0x80, 0x90, 0x22, 0x28, 0x46, 0x7, 0x80,
@@ -524,14 +506,10 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0xa4, 0x94, 0x8a, 0x91, 0x5e, 0xa, 0x41, 0x0,
0xe0,
/* U+663E "显" */
0x7f, 0xc8, 0x9, 0xff, 0x20, 0x27, 0xfc, 0x22,
0x24, 0x4a, 0x8a, 0x31, 0x82, 0x23, 0xff, 0x80,
/* U+672C "本" */
0x4, 0x0, 0x83, 0xff, 0x82, 0x0, 0xe0, 0x2a,
0x5, 0x41, 0x24, 0x44, 0x57, 0xf4, 0x10, 0x2,
0x0,
/* U+677F "板" */
0x20, 0x64, 0xf0, 0x90, 0x7a, 0x2, 0x7e, 0xea,
0x5b, 0x4d, 0x2a, 0x25, 0x44, 0x90, 0xa5, 0x19,
0x10,
/* U+67E5 "查" */
0x4, 0x1f, 0xfc, 0x54, 0x12, 0x4c, 0x46, 0x7f,
@@ -548,6 +526,10 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x9d, 0x15, 0x3e, 0x21, 0x5, 0xfc, 0x8a, 0x16,
0x30,
/* U+6B7B "死" */
0xff, 0xe4, 0x40, 0x88, 0x1d, 0x14, 0xa5, 0x55,
0x4, 0xc0, 0x90, 0x22, 0x28, 0x46, 0x7, 0x80,
/* U+6CE2 "波" */
0x1, 0x10, 0x21, 0x7f, 0x88, 0x91, 0x11, 0x3f,
0x95, 0x10, 0xa2, 0x12, 0x8a, 0x22, 0x4a, 0x16,
@@ -563,15 +545,15 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x5a, 0xa9, 0x55, 0x2a, 0xa9, 0x6, 0x50, 0x91,
0x30,
/* U+7248 "版" */
0x10, 0x6a, 0xf1, 0x50, 0x2a, 0x7, 0xfe, 0x8a,
0x51, 0x4b, 0xaa, 0x55, 0x4a, 0x91, 0x65, 0x49,
0x10,
/* U+6E05 "清" */
0x2, 0x17, 0xfd, 0x8, 0xf, 0xe0, 0x21, 0x7f,
0xd4, 0x10, 0xfe, 0x10, 0x4b, 0xfa, 0x41, 0x8,
0x60,
/* U+7535 "电" */
0x8, 0x1, 0x3, 0xfe, 0x44, 0x48, 0x89, 0xff,
0x22, 0x24, 0x44, 0xff, 0x91, 0x4, 0x20, 0x83,
0xf0,
/* U+70B9 "点" */
0x4, 0x0, 0xfc, 0x10, 0x2, 0x7, 0xfc, 0x80,
0x90, 0x12, 0x2, 0x7f, 0xc0, 0x1, 0x49, 0x44,
0x90,
/* U+754C "界" */
0x7f, 0xc8, 0x89, 0xff, 0x22, 0x27, 0xfc, 0x8,
@@ -587,20 +569,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0xc8, 0x13, 0xfe, 0xa0, 0x47, 0xf8, 0x81, 0x1f,
0xe0,
/* U+793A "示" */
0x3f, 0x80, 0x0, 0x0, 0x7f, 0xf0, 0x40, 0x8,
0x9, 0x21, 0x22, 0x44, 0x30, 0x84, 0x30, 0x0,
/* U+7C7B "类" */
0x4, 0x4, 0x90, 0x54, 0x7f, 0xf1, 0x50, 0x49,
0x10, 0x10, 0x20, 0xff, 0xe1, 0x40, 0xc6, 0x60,
0x30,
/* U+7CFB "系" */
0x3, 0xcf, 0x80, 0x20, 0x8, 0x43, 0xf0, 0xc,
0x6, 0x13, 0xff, 0x4, 0x24, 0x91, 0x11, 0x46,
0x10,
/* U+7EDC "络" */
0x22, 0x4, 0x79, 0x11, 0x4d, 0x4e, 0x10, 0x45,
0x13, 0x1f, 0xbe, 0x4, 0x46, 0x8b, 0x1f, 0x2,
0x20,
/* U+7EF4 "维" */
0x22, 0x84, 0x49, 0x1f, 0xca, 0x4e, 0xc8, 0x4f,
0xd1, 0x27, 0xa4, 0x7, 0xe6, 0x93, 0x12, 0x3,
@@ -620,10 +598,10 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x8, 0x8, 0x3f, 0xf0, 0x18, 0xf, 0xfe, 0x3,
0x1, 0xff, 0xc0, 0x60, 0x3f, 0xf0,
/* U+8282 "节" */
0x11, 0x2, 0x23, 0xff, 0x88, 0x80, 0x1, 0xff,
0x82, 0x10, 0x42, 0x8, 0x41, 0x18, 0x20, 0x4,
0x0,
/* U+884C "行" */
0x10, 0x4, 0xf9, 0x0, 0x48, 0x2, 0xfe, 0xc1,
0x28, 0x21, 0x4, 0x20, 0x84, 0x10, 0x82, 0x11,
0xc0,
/* U+88C5 "装" */
0x91, 0xa, 0x20, 0x7f, 0x98, 0x85, 0x11, 0x2f,
@@ -635,11 +613,6 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x82, 0x8f, 0xff, 0x0, 0xf, 0xf8, 0x82, 0x1f,
0xc0,
/* U+8BA4 "认" */
0x41, 0x4, 0x20, 0x4, 0x0, 0x8e, 0x10, 0x42,
0x8, 0xa1, 0x14, 0x2a, 0x86, 0x88, 0x91, 0x4,
0x10,
/* U+8BAF "讯" */
0x40, 0x5, 0xf0, 0x12, 0x2, 0x4e, 0x48, 0x5f,
0x9, 0x21, 0x24, 0x24, 0x86, 0x94, 0x91, 0x82,
@@ -660,14 +633,14 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x53, 0xea, 0x45, 0x48, 0xad, 0xf5, 0x20, 0x80,
0x60,
/* U+8C03 "调" */
0x8f, 0xe9, 0x4, 0x24, 0x85, 0xdc, 0x92, 0x9f,
0xd2, 0xa, 0x5d, 0x4a, 0xad, 0x75, 0x20, 0x88,
0x70,
/* U+8F6F "软" */
0x22, 0x4, 0x43, 0xcf, 0xa2, 0x16, 0x45, 0x52,
0x3c, 0x41, 0x8, 0x32, 0x9c, 0x50, 0x91, 0x14,
0x10,
/* U+8FD4 "返" */
0x40, 0xe5, 0xe0, 0x20, 0x7, 0xfe, 0x82, 0x54,
0x4a, 0x51, 0x44, 0x29, 0x46, 0x45, 0x40, 0x47,
/* U+8FD0 "运" */
0x40, 0x4, 0xf8, 0x0, 0x0, 0xe, 0xfe, 0x42,
0x8, 0x81, 0x22, 0x2f, 0xe4, 0x5, 0x40, 0x47,
0xf0,
/* U+901A "通" */
@@ -680,44 +653,35 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x17, 0xfa, 0x8, 0x49, 0x29, 0xfd, 0x0, 0x5f,
0xf0,
/* U+90E8 "部" */
0x10, 0xf, 0xbc, 0x4, 0xa2, 0x92, 0x95, 0xfe,
0x80, 0x4b, 0xe9, 0x45, 0x28, 0xb9, 0xf4, 0x22,
0x80,
/* U+914D "配" */
0xfe, 0xe5, 0x7, 0xf8, 0xd5, 0x1a, 0xaf, 0x8d,
0x20, 0xa7, 0xf4, 0x82, 0xbf, 0xd6, 0x9, 0x80,
/* U+91CF "量" */
0x7f, 0xc8, 0x89, 0x9, 0x7f, 0xf4, 0x44, 0xff,
0x91, 0x13, 0xfe, 0x4, 0xf, 0xf8, 0x10, 0x7f,
0xf0,
/* U+95F4 "间" */
0x9f, 0xd0, 0x10, 0x6, 0xfd, 0xa1, 0x68, 0x5b,
0xf6, 0x85, 0xa1, 0x6f, 0xd8, 0x6, 0x7,
/* U+949F "钟" */
0x40, 0x88, 0x11, 0xe2, 0x41, 0xf7, 0xaa, 0x45,
0x48, 0xaf, 0xdf, 0x22, 0xa5, 0x10, 0xc2, 0x10,
0x40,
/* U+96F6 "零" */
0x7f, 0xc0, 0x83, 0xff, 0xda, 0xd0, 0x40, 0xeb,
0x82, 0x81, 0xac, 0xdf, 0x60, 0x40, 0x70, 0x1,
/* U+9632 "防" */
0x2, 0x1e, 0x22, 0x7f, 0xc9, 0xa, 0x21, 0x47,
0xe4, 0x8c, 0x91, 0x92, 0x3c, 0x86, 0x10, 0xc4,
0x60,
/* U+9664 "除" */
0x1, 0x1c, 0x52, 0x91, 0x54, 0x1c, 0x7d, 0x82,
0x2b, 0xfd, 0x8, 0xa5, 0x59, 0x26, 0x44, 0xc1,
0x80,
/* U+9762 "面" */
0xff, 0xe0, 0x80, 0x20, 0x3f, 0xf4, 0x92, 0x9e,
0x52, 0x4a, 0x79, 0x49, 0x2f, 0xfd, 0x0, 0x80,
/* U+9ED8 "默" */
0xfc, 0x92, 0x9b, 0x72, 0xca, 0x4f, 0xde, 0x21,
0x3f, 0x20, 0x84, 0x1d, 0x5c, 0x29, 0x55, 0x55,
0x10,
/* U+FF1A ":" */
0xf0, 0xf0,
/* U+25B6 "▶"(来源:Segoe UI Symbol;宋体无此字形) */
0x80, 0xe0, 0xf0, 0xfc, 0xff, 0xfe, 0xf8, 0xf0,
0xc0, 0x0
0x80, 0xe0, 0xf0, 0xfc, 0xff, 0xfe, 0xf8, 0xf0, 0xc0, 0x00
};
@@ -831,83 +795,75 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 587, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 604, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 621, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 638, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 654, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 671, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 688, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 705, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 722, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 739, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 756, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 772, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 788, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 805, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 821, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 838, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 855, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 872, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 886, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 903, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 920, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 937, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 954, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 971, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 987, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1004, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1021, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1038, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1052, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1069, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1086, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1103, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1120, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1137, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1154, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1171, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1188, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1205, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1221, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1238, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1254, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1271, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1288, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1305, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1322, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1339, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1356, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1373, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1390, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1407, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1424, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1439, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1456, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1472, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1489, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1506, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1523, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1540, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1557, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1571, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1588, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1605, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1622, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1639, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1656, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1673, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1690, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1707, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1724, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1741, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1758, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1775, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1792, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1808, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1825, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1840, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1857, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1873, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1890, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0},
{.bitmap_index = 1892, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 ▶ */
{.bitmap_index = 638, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 655, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 672, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 686, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 702, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 719, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 735, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1},
{.bitmap_index = 747, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 764, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 781, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 798, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 815, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 832, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 849, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 866, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 883, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 900, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 917, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 931, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 948, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 965, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 982, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 999, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1016, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1033, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1050, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1067, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1084, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1101, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1118, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1135, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1151, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1168, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1185, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1202, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1219, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1236, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1252, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1269, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1286, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1303, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1320, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1337, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1354, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1369, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1386, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1403, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1420, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1437, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1454, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1471, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1485, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1502, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1519, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1536, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1553, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1570, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1587, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1604, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1621, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1638, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1655, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1672, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1689, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1706, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1723, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1740, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1756, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0},
{.bitmap_index = 1758, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */
};
/*---------------------
@@ -915,17 +871,16 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
*--------------------*/
static const uint16_t unicode_list_1[] = {
0x0, 0x32, 0x82, 0x9b, 0xed, 0x144, 0x153, 0x1d8,
0x233, 0x33a, 0x35c, 0x35f, 0x3f1, 0x42d, 0x444, 0x49f,
0x579, 0x582, 0x5b9, 0x5c3, 0x5cf, 0x5ee, 0x641, 0x8d5,
0xb04, 0xd4e, 0xd91, 0xd95, 0xdad, 0x10f7, 0x1106, 0x1131,
0x1149, 0x114a, 0x114c, 0x1259, 0x1266, 0x1442, 0x149b, 0x1565,
0x16c4, 0x173c, 0x1767, 0x17d7, 0x17ed, 0x1835, 0x1923, 0x19dc,
0x1ab7, 0x1c18, 0x1ed9, 0x1f38, 0x1f42, 0x243f, 0x272c, 0x2743,
0x28eb, 0x2902, 0x2b31, 0x2ef2, 0x30d3, 0x30eb, 0x3148, 0x3165,
0x33e1, 0x3479, 0x3abc, 0x3d5d, 0x3d9b, 0x3da6, 0x3da7, 0x3db5,
0x3dd9, 0x3dfa, 0x41cb, 0x4211, 0x425c, 0x42df, 0x4344, 0x43c6,
0x47eb, 0x48ed, 0x4959, 0x50cf, 0xb111
0x0, 0x32, 0x82, 0x9b, 0x153, 0x1d4, 0x1d8, 0x233,
0x42d, 0x444, 0x49f, 0x531, 0x582, 0x5b9, 0x5c3, 0x5da,
0x626, 0x641, 0x982, 0xb04, 0xd4e, 0xd91, 0xd95, 0x1106,
0x1131, 0x1149, 0x114a, 0x114c, 0x1266, 0x1442, 0x148d, 0x149b,
0x149c, 0x1565, 0x159e, 0x16c4, 0x173c, 0x1767, 0x176b, 0x17d7,
0x17ed, 0x1976, 0x19dc, 0x1ab7, 0x1c18, 0x1d72, 0x1ed9, 0x1f38,
0x1f42, 0x1ffc, 0x22b0, 0x2743, 0x28eb, 0x2902, 0x2e72, 0x2ef2,
0x30eb, 0x3148, 0x3165, 0x33e1, 0x3a43, 0x3abc, 0x3d5d, 0x3da6,
0x3da7, 0x3db5, 0x3dd9, 0x4166, 0x41c7, 0x4211, 0x425c, 0x43c6,
0x4696, 0x4829, 0x485b, 0x4959, 0xb111
};
/*Collect the unicode lists and glyph_id offsets*/
@@ -937,10 +892,10 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
},
{
.range_start = 19977, .range_length = 45330, .glyph_id_start = 96,
.unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 85, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
.unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 77, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
},
{
.range_start = 9654, .range_length = 1, .glyph_id_start = 181,
.range_start = 9654, .range_length = 1, .glyph_id_start = 173,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
+116 -556
View File
@@ -1,449 +1,129 @@
/*
* 模块名称:UI Menu (菜单页面构建)
* 模块功能:基于 LVGL 基元控件(页面导航)复现 DTU-HMI 树形菜单,支持方向键导航与返回
* 适用平台:STM32F407ZGTx
* 模块名称:UI Menu (菜单导航逻辑层)
* 模块功能:g_menu_tab 父子索引表驱动的横向级联菜单导航——持有导航状态
* (路径/选中/深度/滚动,共 17 B)与键语义(↑↓移选、→/ENTER 进、←/ESC 退),
* 单向驱动显示层 ui_menu_view 建/毁/刷新级联框;本层不持有任何 LVGL 对象
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-08-29
* 修改记录:
* 2026-08-29 王建锋 创建初始版本
* 2026-09-02 王建锋 弃用已废弃的 lv_menu 控件,改为基元控件页面导航
* 2026-09-02 王建锋 修复选中行文字不可见(白底白字):行改用 label 承载,选中态样式直接覆盖文字色
* 2026-09-03 王建锋 主界面按设计稿重构:顶部开口浅 V 装饰 + 状态行 + 左对齐带框 7 行列表(选中行整行反色、无箭头,其余行带 ▶);
* 根页条目收敛为一级 7 项(排除"厂家设置"子树);重命名 定值参数→参数定值、遥测量→三遥设置、通讯设置→通讯参数
* 2026-09-02 王建锋 修复选中行文字不可见(白底白字)
* 2026-09-03 王建锋 主界面按设计稿重构(根页 7 项/装饰/状态行)
* 2026-09-04 王建锋 重构为"横向级联框"模型(菜单页面设计.md):拆出显示层
* ui_menu_view 与数据层 ui_menu_tab;本层只留导航状态机
*/
#include "ui_menu.h"
#include "ui_menu_view.h"
#include "ui_menu_tab.h"
#include <stdint.h>
#include "lvgl.h"
#define DBG_TAG "[UI_MENU]"
#include "dbg_log.h"
/* 菜单节点表(简化自 DTU-HMI menuTab):层级 + 名称 */
typedef struct {
uint8_t level; /* 0=根页面, 1..3=子层次 */
const char *name;
} ui_menu_node_t;
static const ui_menu_node_t g_menu_tab[] = {
{0, "主界面"},
{1, "装置信息"},
{1, "实时数据"},
{2, "交流量"},
{2, "直流量"},
{2, "开入量"},
{1, "参数定值"},
{2, "定值"},
{3, "定值"},
{3, "遥调"},
{3, "零压"},
{3, "电压"},
{2, "查看"},
{3, "定值"},
{3, "遥调"},
{3, "零压"},
{3, "电压"},
{1, "三遥设置"},
{2, "测量值"},
{2, "遥调系数"},
{2, "遥测系数"},
{2, "双字节"},
{2, "装置遥调"},
{1, "装置维护"},
{2, "时间设置"},
{2, "强制复归"},
{2, "手动录波"},
{2, "自动录波"},
{1, "通讯参数"},
{2, "通讯参数"},
{2, "网络配置"},
{2, "SNTP设置"},
{1, "记录查询"},
{2, "SOE记录"},
{2, "事故记录"},
{2, "操作记录"},
{2, "告警记录"},
{2, "变位告警"},
{2, "遥信记录"},
{2, "自检记录"},
{2, "定值记录"},
{2, "录波记录"},
{0, "厂家设置"},
{1, "元件设置"},
{1, "恢复默认"},
{2, "全部恢复"},
{2, "默认参数"},
{2, "默认定值"},
{2, "默认系数"},
{2, "电压"},
{2, "元件参数"},
{1, "版本显示"},
{1, "装置遥调"},
{2, "测量遥调"},
{2, "装置遥调"},
{2, "直流遥调"},
{2, "开出遥调"},
{2, "装置记录"},
{2, "告警记录"},
{2, "遥调记录"},
{2, "信号复归"},
{1, "版本信息"},
};
#define MENU_NODE_COUNT (sizeof(g_menu_tab) / sizeof(g_menu_tab[0]))
#define MENU_NODE_ROOT 0xFFFF /* 虚拟根节点(根页面) */
#define MENU_PAGE_MAX 4 /* 页面深度上限(根页 + 3 级子页) */
#define MENU_ITEM_MAX 16 /* 单页最大条目数 */
#define MENU_HEADER_H 18 /* 子页页头高度(px) */
/* 主界面(根页)布局(设备像素,依据设计稿 mockup 实测) */
#define MENU_ROW_H 15 /* 行高 */
#define MENU_ROW_TXT_TOP 1 /* 行内文字垂直居中偏移((15-13)/2,字体行高 13) */
#define MENU_ROW_PAD_L 8 /* 行内文字左留白 */
#define MENU_ROW_ARROW_X 58 /* 行内箭头 x(紧跟 4 字文本之后) */
#define MENU_ROOT_BOX_X 0 /* 列表框左(左对齐,贴左缘) */
#define MENU_ROOT_BOX_Y 44 /* 列表框顶 */
#define MENU_ROOT_BOX_W 99 /* 列表框宽 */
#define MENU_ROOT_BOX_H 107 /* 列表框高(7 行 × 15 + 上下边框 2) */
#define MENU_ROOT_STATUS_X 17 /* 状态行左 */
#define MENU_ROOT_STATUS_Y 20 /* 状态行顶 */
#define MENU_DECO_LEG_W 3 /* 顶部"浅 V"两腿线宽(1bpp 下斜线边缘易被阈值吃掉,需比水平线粗才接近原图) */
#define MENU_DECO_HORIZ_W 2 /* 顶部"浅 V"水平线宽(≈原图 ~2px) */
/*
* 页面:持久容器 + 按导航动态建毁的条目
* 容器常驻对象树(焦点对象不可删除,避免输入设备悬空指针),
* 条目按钮进入页面时创建、返回时销毁,内存占用 = 当前路径
*
* 每条目 = 行容器(整行背景)+ 文本 label + 可选箭头 label:
* - 行容器承载整行反色高亮(选中态白底)
* - 文本/箭头为子 label(绝对定位);选中态文字色须显式置黑
* (主题对 label 直接给白字,父级继承不可靠,见 2026-09-02 记录)
* 导航状态:唯一"必须持久"的导航历史(共 17 B,见设计 §7.2);
* 条目列表/框几何均可由 g_menu_tab + 本状态随时重算,不落 RAM
*/
typedef struct {
lv_obj_t *cont; /* 页面容器(持久) */
lv_obj_t *row[MENU_ITEM_MAX]; /* 行容器(动态) */
lv_obj_t *row_text[MENU_ITEM_MAX]; /* 行文本 label */
lv_obj_t *row_arrow[MENU_ITEM_MAX]; /* 行箭头 label(NULL=无) */
uint16_t item_node[MENU_ITEM_MAX]; /* 各条目对应 g_menu_tab 下标 */
uint16_t node; /* 本页展示节点(MENU_NODE_ROOT=虚拟根) */
uint8_t item_cnt; /* 条目数 */
uint8_t sel; /* 当前选中条目下标 */
uint8_t depth; /* 页面深度(0=根页) */
} ui_menu_page_t;
static ui_menu_page_t s_pages[MENU_PAGE_MAX];
static ui_menu_page_t *s_cur = NULL; /* 当前显示页面 */
static lv_obj_t *s_header = NULL; /* 子页页头容器(返回按钮 + 标题);根页隐藏 */
static lv_obj_t *s_back_btn = NULL; /* 页头返回按钮 */
static lv_obj_t *s_title = NULL; /* 页头标题 */
static lv_obj_t *s_root_deco = NULL; /* 主界面顶部装饰(开口浅 V) */
static lv_obj_t *s_root_status = NULL; /* 主界面状态行 */
static lv_obj_t *s_root_box = NULL; /* 主界面列表框(1px 边框) */
/* 样式(静态,一次性初始化) */
static lv_style_t s_style_flat; /* 通用:透明、无边框/圆角/内边距 */
static lv_style_t s_style_selected; /* 选中态:整行反色(白底) */
static lv_style_t s_style_back; /* 返回按钮:细边框、小内边距 */
static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */
static lv_style_t s_style_root_box; /* 主界面列表框:透明底 + 1px 白边框 */
static uint8_t s_path[MENU_PAGE_MAX]; /* 路径:s_path[d]=第 d 层展示节点(s_path[0]=0 主界面) */
static uint8_t s_sel[MENU_PAGE_MAX]; /* 各层选中下标(返回时还原,选中不跳) */
static uint8_t s_depth; /* 当前深度(0=根框)= 焦点所在(最右)框 */
static uint16_t s_scroll[MENU_PAGE_MAX]; /* 各框滚动偏移(px);框销毁再进入时还原 */
/*
* 函数功能:初始化菜单样式(静态样式,仅执行一次)
* 函数功能:移选后的层刷新(高亮 + 跟随滚动 + 滚动位回读持久化)
* 入口参数:depth - 层深度 uint8_t
* 返回值:无
* 限定条件:该层框已建
* 函数说明:显示层跟随滚动后,滚动位写回 s_scroll 持久(框重建时还原)
*/
static void menu_refresh_layer(uint8_t depth)
{
ui_menu_view_frame_refresh(depth, s_sel[depth]);
s_scroll[depth] = ui_menu_view_get_scroll(depth);
}
/*
* 函数功能:进入选中条目(压层)
* 入口参数:child - 选中子节点下标 uint8_t
* 返回值:无
* 限定条件:s_depth+1 < MENU_PAGE_MAX
* 函数说明: 1. 叶子(无子项)同样压层,生成空白终点框(占位,无条目)
* 2. 同节点再次进入:还原上次选中与滚动位;换节点则清零
*/
static void menu_open(uint8_t child)
{
uint8_t d = s_depth;
bool same = (s_path[d + 1u] == child);
s_depth = (uint8_t)(d + 1u);
s_path[d + 1u] = child;
s_sel[d + 1u] = same ? s_sel[d + 1u] : 0u;
ui_menu_view_frame_build(d + 1u, child, s_sel[d + 1u],
(uint16_t)(same ? s_scroll[d + 1u] : 0u));
ui_menu_view_focus(d + 1u);
}
/*
* 函数功能:关闭最右框(弹层)
* 入口参数:无
* 返回值:无
* 限定条件:无
* 函数说明: 1. flat 用于剥离主题给对象附加的卡片样式(边框/圆角/内边距)
* 2. selected 为选中条目整行反色高亮(1bpp:白底;文字色在刷新时显式置黑)
* 3. 样式后加优先,可覆盖主题在创建时附加的样式
* 限定条件:s_depth > 0
* 函数说明: 1. 销毁最右框行对象(既有屏上框不动、无需重建)
* 2. 焦点回父框;父层选中/滚动原样保留(选中不跳)
*/
static void menu_styles_init(void)
static void menu_back(void)
{
static bool inited = false;
if (inited) {
return;
}
inited = true;
lv_style_init(&s_style_flat);
lv_style_set_bg_opa(&s_style_flat, LV_OPA_TRANSP);
lv_style_set_border_width(&s_style_flat, 0);
lv_style_set_radius(&s_style_flat, 0);
lv_style_set_pad_all(&s_style_flat, 0);
lv_style_set_pad_gap(&s_style_flat, 0);
lv_style_init(&s_style_selected);
lv_style_set_bg_opa(&s_style_selected, LV_OPA_COVER);
lv_style_set_bg_color(&s_style_selected, lv_color_white());
lv_style_set_text_color(&s_style_selected, lv_color_black());
lv_style_init(&s_style_back);
lv_style_set_bg_opa(&s_style_back, LV_OPA_TRANSP);
lv_style_set_border_width(&s_style_back, 1);
lv_style_set_border_color(&s_style_back, lv_color_white());
lv_style_set_radius(&s_style_back, 0);
lv_style_set_pad_all(&s_style_back, 1);
lv_style_init(&s_style_scrollbar);
lv_style_set_bg_opa(&s_style_scrollbar, LV_OPA_COVER);
lv_style_set_bg_color(&s_style_scrollbar, lv_color_white());
lv_style_set_width(&s_style_scrollbar, 4);
lv_style_init(&s_style_root_box);
lv_style_set_bg_opa(&s_style_root_box, LV_OPA_TRANSP);
lv_style_set_border_width(&s_style_root_box, 1);
lv_style_set_border_color(&s_style_root_box, lv_color_white());
lv_style_set_radius(&s_style_root_box, 0);
lv_style_set_pad_all(&s_style_root_box, 0);
/* mono 主题 card 样式自带 pad_gap=PAD_DEF(4),须显式清零,否则行间距被撑大导致溢出 */
lv_style_set_pad_gap(&s_style_root_box, 0);
ui_menu_view_frame_destroy(s_depth);
s_depth--;
ui_menu_view_focus(s_depth);
}
/*
* 函数功能:向页面添加一个条目(行容器 + 文本 + 可选箭头)
* 入口参数:page - 页面结构指针 ui_menu_page_t *
* parent - 条目父容器 lv_obj_t * 根页=s_root_box,子页=page->cont
* node - g_menu_tab 节点下标 uint16_t
* has_arrow - 是否创建箭头 label bool
* 返回值:无
* 限定条件:page->item_cnt < MENU_ITEM_MAX
* 函数说明:1. 行容器整行宽、高 MENU_ROW_H,承载选中反色高亮
* 2. 文本 label 左留白 MENU_ROW_PAD_L;箭头 label 位于 MENU_ROW_ARROW_X
* 3. 文本/箭头子 label 加 IGNORE_LAYOUT 做行内绝对定位
*/
static void menu_page_add_item(ui_menu_page_t *page, lv_obj_t *parent,
uint16_t node, bool has_arrow)
{
if (page->item_cnt >= MENU_ITEM_MAX) {
return;
}
uint8_t idx = page->item_cnt;
lv_obj_t *row = lv_obj_create(parent);
if (row == NULL) {
return;
}
lv_obj_add_style(row, &s_style_flat, 0);
lv_obj_add_style(row, &s_style_selected, LV_STATE_CHECKED);
lv_obj_set_width(row, lv_pct(100));
lv_obj_set_height(row, MENU_ROW_H);
lv_obj_t *lbl = lv_label_create(row);
if (lbl == NULL) {
lv_obj_delete(row);
return;
}
lv_obj_add_style(lbl, &s_style_flat, 0);
lv_obj_add_flag(lbl, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(lbl, MENU_ROW_PAD_L, MENU_ROW_TXT_TOP);
lv_label_set_text_static(lbl, g_menu_tab[node].name);
lv_obj_t *ar = NULL;
if (has_arrow) {
ar = lv_label_create(row);
if (ar != NULL) {
lv_obj_add_style(ar, &s_style_flat, 0);
lv_obj_add_flag(ar, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(ar, MENU_ROW_ARROW_X, MENU_ROW_TXT_TOP);
lv_label_set_text_static(ar, "▶");
}
}
page->row[idx] = row;
page->row_text[idx] = lbl;
page->row_arrow[idx] = ar;
page->item_node[idx] = node;
page->item_cnt++;
}
/* 前向声明(menu_page_build 先于其定义调用) */
static void menu_page_refresh_sel(ui_menu_page_t *page);
/*
* 函数功能:构建页面条目列表
* 入口参数:page - 页面结构指针 ui_menu_page_t *
* node - 展示节点 uint16_t MENU_NODE_ROOT 表示虚拟根
* 返回值:无
* 限定条件:根页时 s_root_box 已创建
* 函数说明:1. 根页:一级节点 7 项(至第二个根"厂家设置"前截断),带箭头,父容器=s_root_box
* 2. 子页:node+1 起 level 为 (level+1) 的连续段,无箭头,父容器=page->cont
* 3. 先清空父容器旧条目再重建,默认选中首项
*/
static void menu_page_build(ui_menu_page_t *page, uint16_t node)
{
bool is_root = (node == MENU_NODE_ROOT);
lv_obj_t *parent = is_root ? s_root_box : page->cont;
bool has_arrow = is_root;
lv_obj_clean(parent);
page->node = node;
page->item_cnt = 0;
page->sel = 0;
if (is_root) {
for (uint32_t i = 1; i < MENU_NODE_COUNT; i++) {
if (g_menu_tab[i].level == 0) {
break; /* 抵达第二个根(厂家设置),主界面仅取其之前的一级项 */
}
if (g_menu_tab[i].level == 1) {
menu_page_add_item(page, parent, (uint16_t)i, has_arrow);
}
}
}
else {
uint8_t lvl = (uint8_t)(g_menu_tab[node].level + 1);
for (uint32_t i = node + 1; i < MENU_NODE_COUNT; i++) {
if (g_menu_tab[i].level != lvl) {
break;
}
menu_page_add_item(page, parent, (uint16_t)i, has_arrow);
}
}
if (page->item_cnt > 0) {
menu_page_refresh_sel(page);
}
}
/*
* 函数功能:更新选中高亮、文字色与箭头显隐
* 入口参数:page - 页面结构指针 ui_menu_page_t *
* 返回值:无
* 限定条件:page->item_cnt > 0
* 函数说明:1. 选中行整行反色(白底),文字/箭头显式置黑并隐藏箭头
* 2. 非选中行透明底,文字/箭头置白并显示箭头
* 3. 滚动使选中条目进入可视区
*/
static void menu_page_refresh_sel(ui_menu_page_t *page)
{
if (page->item_cnt == 0) {
return;
}
for (uint8_t i = 0; i < page->item_cnt; i++) {
bool sel = (i == page->sel);
if (sel) {
lv_obj_add_state(page->row[i], LV_STATE_CHECKED);
}
else {
lv_obj_clear_state(page->row[i], LV_STATE_CHECKED);
}
lv_obj_set_style_text_color(page->row_text[i],
sel ? lv_color_black() : lv_color_white(), 0);
if (page->row_arrow[i] != NULL) {
if (sel) {
lv_obj_add_flag(page->row_arrow[i], LV_OBJ_FLAG_HIDDEN);
}
else {
lv_obj_remove_flag(page->row_arrow[i], LV_OBJ_FLAG_HIDDEN);
}
lv_obj_set_style_text_color(page->row_arrow[i],
sel ? lv_color_black() : lv_color_white(), 0);
}
}
lv_obj_scroll_to_view(page->row[page->sel], LV_ANIM_OFF);
}
/*
* 函数功能:切换显示页面
* 入口参数:page - 目标页面 ui_menu_page_t *
* 函数功能:按键语义分发(移/进/退)
* 入口参数:depth - 焦点所在层深度 uint8_t
* key - LVGL 键值 lv_key_t
* 返回值:无
* 限定条件:ui_menu_init() 已完成
* 函数说明:1. 隐藏旧页、显示新页(仅切换 HIDDEN 标志)
* 2. 页头仅子页显示;根页隐藏页头(让位顶部装饰 + 状态行,内容区撑满 160)
* 3. 焦点移至新页容器(按键输入入口)
* 函数说明: 1. UP/DOWN:移选(首尾环绕);选中越出可见区由显示层跟随滚动
* 2. ENTER/RIGHT:进子框(叶子 → 空白终点框)
* 3. ESC/LEFT:关最右框(根框不响应)
* 4. 空框(终点页 0 条目)只响应返回
*/
static void menu_show_page(ui_menu_page_t *page)
static void menu_key_handle(uint8_t depth, lv_key_t key)
{
if (s_cur != NULL && s_cur != page) {
lv_obj_add_flag(s_cur->cont, LV_OBJ_FLAG_HIDDEN);
}
s_cur = page;
lv_obj_clear_flag(page->cont, LV_OBJ_FLAG_HIDDEN);
if (page->depth == 0) {
lv_obj_add_flag(s_header, LV_OBJ_FLAG_HIDDEN);
}
else {
lv_obj_remove_flag(s_header, LV_OBJ_FLAG_HIDDEN);
lv_label_set_text_static(s_title, g_menu_tab[page->node].name);
}
lv_group_focus_obj(page->cont);
}
/*
* 函数功能:打开选中条目的子页面
* 入口参数:page - 当前页面 ui_menu_page_t *
* 返回值:无
* 限定条件:page->item_cnt > 0
* 函数说明:1. 在下一深度页面容器上重建条目并显示
* 2. 叶子节点(无子节点)打开空白占位页
* 3. 深度保护:超过 MENU_PAGE_MAX 不再下钻
*/
static void menu_open_selected(ui_menu_page_t *page)
{
if (page->item_cnt == 0) {
return;
}
if (page->depth + 1 >= MENU_PAGE_MAX) {
return;
}
ui_menu_page_t *child = &s_pages[page->depth + 1];
menu_page_build(child, page->item_node[page->sel]);
menu_show_page(child);
}
/*
* 函数功能:返回上级页面
* 入口参数:page - 当前页面 ui_menu_page_t *
* 返回值:无
* 限定条件:page->depth > 0
* 函数说明:1. 销毁本页条目释放 LVGL 池内存(容器保留)
* 2. 显示父页并恢复焦点(父页选中状态保持不变)
*/
static void menu_go_back(ui_menu_page_t *page)
{
if (page->depth == 0) {
return;
}
lv_obj_clean(page->cont);
menu_show_page(&s_pages[page->depth - 1]);
}
/*
* 函数功能:页面容器按键事件处理
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:容器已加入默认输入组并获焦
* 函数说明:1. UP/DOWN:移动选中(环绕)
* 2. ENTER/RIGHT:打开选中条目子页
* 3. ESC/LEFT:返回上级页面
*/
static void menu_page_key_cb(lv_event_t *e)
{
ui_menu_page_t *page = (ui_menu_page_t *)lv_event_get_user_data(e);
lv_key_t key = (lv_key_t)lv_event_get_key(e);
uint8_t kids[MENU_ITEM_MAX];
uint8_t cnt = ui_menu_tab_children(s_path[depth], kids, MENU_ITEM_MAX);
switch (key) {
case LV_KEY_UP:
if (page->item_cnt > 0) {
page->sel = (uint8_t)((page->sel + page->item_cnt - 1) % page->item_cnt);
menu_page_refresh_sel(page);
if (cnt > 0u) {
s_sel[depth] = (uint8_t)((s_sel[depth] + (uint8_t)cnt - 1u) % cnt);
menu_refresh_layer(depth);
}
break;
case LV_KEY_DOWN:
if (page->item_cnt > 0) {
page->sel = (uint8_t)((page->sel + 1) % page->item_cnt);
menu_page_refresh_sel(page);
if (cnt > 0u) {
s_sel[depth] = (uint8_t)((s_sel[depth] + 1u) % cnt);
menu_refresh_layer(depth);
}
break;
case LV_KEY_ENTER:
case LV_KEY_RIGHT:
menu_open_selected(page);
if (cnt > 0u && depth + 1u < MENU_PAGE_MAX) {
menu_open(kids[s_sel[depth]]);
}
break;
case LV_KEY_ESC:
case LV_KEY_LEFT:
menu_go_back(page);
if (depth > 0u) {
menu_back();
}
break;
default:
break;
@@ -451,56 +131,45 @@ static void menu_page_key_cb(lv_event_t *e)
}
/*
* 函数功能:返回按钮点击回调(预留触摸输入)
* 函数功能:页面容器按键事件入口(每层容器各挂一个,user_data=层深度)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:ui_menu_init() 已完成
* 函数说明:返回上级页面;按键输入由页面容器 ESC/LEFT 处理
* 限定条件:容器已加入默认输入组并获焦
* 函数说明:键值经默认输入组送达焦点容器;统一分发到 menu_key_handle
*/
static void menu_back_btn_cb(lv_event_t *e)
static void menu_key_cb(lv_event_t *e)
{
(void)e;
if (s_cur != NULL) {
menu_go_back(s_cur);
uint8_t depth = (uint8_t)(uintptr_t)lv_event_get_user_data(e);
if (depth < MENU_PAGE_MAX) {
menu_key_handle(depth, (lv_key_t)lv_event_get_key(e));
}
}
/*
* 函数功能:应用主界面顶部"浅 V"装饰线段的公共样式
* 入口参数:line - lv_line 对象;width - 线宽(px)
* 返回值:无
* 限定条件:line 已创建且已设点集
* 函数说明:绝对定位于 (0,0)、忽略布局、白色实线、指定线宽
*/
static void deco_line_apply(lv_obj_t * line, int32_t width)
{
lv_obj_add_flag(line, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(line, 0, 0);
lv_obj_set_size(line, 160, 20);
lv_obj_set_style_line_width(line, width, 0);
lv_obj_set_style_line_color(line, lv_color_white(), 0);
lv_obj_set_style_line_opa(line, LV_OPA_COVER, 0);
}
/*
* 函数功能:构建菜单(基元控件页面导航)
* 函数功能:构建菜单(横向级联框)
* 入口参数:无
* 返回值:0 - 成功,其他 - 失败(对象创建失败)
* 返回值:0 - 成功,其他 - 失败(表自检失败/对象创建失败)
* 限定条件:ui_port_init()/sim_port_init() 已成功调用,活动屏幕存在
* 函数说明: 1. 子页页头(返回按钮 + 标题)+ 内容区,4 个页面容器(每深度一个,持久)
* 2. 主界面(根页):顶部开口浅 V 装饰 + 状态行 + 左对齐带框 7 行列表
* 3. 页面按需构建:ENTER 时建、返回时毁,内存 = 当前路径
* 4. 页面容器加入默认输入组,按键由容器统一处理
* 函数说明: 1. 数据表自检(parent 下标均有效,捕获表内笔误)
* 2. 重置导航状态 → 初始化显示层
* 3. 各层容器加入默认输入组并挂键回调(按键由逻辑层统一处理)
* 4. 建根框(主界面 node 0 子项)并置焦点
* 5. 构建前后监控 LVGL 池内存并打印
*/
int ui_menu_init(void)
{
lv_obj_t *scr = lv_screen_active();
if (scr == NULL) {
if (!ui_menu_tab_valid()) {
DBG_ERROR("menu table: INVALID (parent index out of range)");
return -1;
}
menu_styles_init();
for (uint8_t d = 0u; d < MENU_PAGE_MAX; d++) {
s_path[d] = 0u;
s_sel[d] = 0u;
s_scroll[d] = 0u;
}
s_depth = 0u;
lv_mem_monitor_t mon;
lv_mem_monitor(&mon);
@@ -508,132 +177,23 @@ int ui_menu_init(void)
(unsigned)mon.total_size,
(unsigned)mon.free_size, (unsigned)mon.free_biggest_size);
/* 根容器:铺满屏幕,纵向布局 */
lv_display_t *disp = lv_display_get_default();
lv_obj_t *menu = lv_obj_create(scr);
if (menu == NULL) {
if (ui_menu_view_init() != 0) {
return -1;
}
lv_obj_add_style(menu, &s_style_flat, 0);
lv_obj_set_size(menu, lv_display_get_horizontal_resolution(disp),
lv_display_get_vertical_resolution(disp));
lv_obj_set_flex_flow(menu, LV_FLEX_FLOW_COLUMN);
/* 子页页头:返回按钮 + 标题(根页隐藏) */
s_header = lv_obj_create(menu);
if (s_header == NULL) {
return -1;
}
lv_obj_add_style(s_header, &s_style_flat, 0);
lv_obj_set_width(s_header, lv_pct(100));
lv_obj_set_height(s_header, MENU_HEADER_H);
lv_obj_set_flex_flow(s_header, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(s_header, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_gap(s_header, 4, 0);
s_back_btn = lv_button_create(s_header);
if (s_back_btn == NULL) {
return -1;
}
lv_obj_add_style(s_back_btn, &s_style_back, 0);
lv_obj_set_size(s_back_btn, 28, 16);
lv_obj_set_flex_flow(s_back_btn, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(s_back_btn, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *back_lbl = lv_label_create(s_back_btn);
if (back_lbl == NULL) {
return -1;
}
lv_obj_add_style(back_lbl, &s_style_flat, 0);
lv_label_set_text_static(back_lbl, "返回");
lv_obj_add_event_cb(s_back_btn, menu_back_btn_cb, LV_EVENT_CLICKED, NULL);
s_title = lv_label_create(s_header);
if (s_title == NULL) {
return -1;
}
lv_obj_add_style(s_title, &s_style_flat, 0);
lv_label_set_text_static(s_title, "主界面");
lv_obj_add_flag(s_header, LV_OBJ_FLAG_HIDDEN); /* 默认根页:隐藏页头 */
/* 内容区:占满剩余高度(根页隐藏页头时撑满 160) */
lv_obj_t *content = lv_obj_create(menu);
if (content == NULL) {
return -1;
}
lv_obj_add_style(content, &s_style_flat, 0);
lv_obj_set_width(content, lv_pct(100));
lv_obj_set_flex_grow(content, 1);
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);
/* 页面容器:每深度一个,持久存在(条目按导航建毁) */
for (uint8_t i = 0; i < MENU_PAGE_MAX; i++) {
s_pages[i].cont = lv_obj_create(content);
if (s_pages[i].cont == NULL) {
/* 键接线:各层容器加入默认输入组;按键一律由逻辑层处理(显示层纯渲染) */
for (uint8_t d = 0u; d < MENU_PAGE_MAX; d++) {
lv_obj_t *cont = ui_menu_view_container(d);
if (cont == NULL) {
return -1;
}
lv_obj_add_style(s_pages[i].cont, &s_style_flat, 0);
lv_obj_set_size(s_pages[i].cont, lv_pct(100), lv_pct(100));
lv_obj_add_flag(s_pages[i].cont, LV_OBJ_FLAG_CLICK_FOCUSABLE);
lv_group_add_obj(lv_group_get_default(), s_pages[i].cont);
lv_obj_add_event_cb(s_pages[i].cont, menu_page_key_cb, LV_EVENT_KEY, &s_pages[i]);
s_pages[i].depth = i;
s_pages[i].node = MENU_NODE_ROOT;
if (i > 0) {
/* 子页容器:纵向堆叠条目 + 自动滚动条 */
lv_obj_set_flex_flow(s_pages[i].cont, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(s_pages[i].cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START);
lv_obj_set_scrollbar_mode(s_pages[i].cont, LV_SCROLLBAR_MODE_AUTO);
lv_obj_add_style(s_pages[i].cont, &s_style_scrollbar, LV_PART_SCROLLBAR);
lv_obj_add_flag(s_pages[i].cont, LV_OBJ_FLAG_HIDDEN);
}
lv_group_add_obj(lv_group_get_default(), cont);
lv_obj_add_event_cb(cont, menu_key_cb, LV_EVENT_KEY, (void *)(uintptr_t)d);
}
/* 主界面(根页):顶部"开口浅 V"装饰 + 状态行 + 列表框(绝对定位于 s_pages[0].cont)
* 拆 3 段:两腿加粗(1bpp 下斜线边缘易被阈值吃掉,需比水平线粗才接近原图 ~4px),水平线保持细线 */
{
static const lv_point_precise_t g_deco_leg_l[] = { {7, 7}, {16, 17} };
static const lv_point_precise_t g_deco_hoz[] = { {16, 17}, {145, 17} };
static const lv_point_precise_t g_deco_leg_r[] = { {158, 7}, {145, 17} };
lv_obj_t * leg_l = lv_line_create(s_pages[0].cont);
lv_obj_t * leg_r = lv_line_create(s_pages[0].cont);
s_root_deco = lv_line_create(s_pages[0].cont);
if (leg_l == NULL || leg_r == NULL || s_root_deco == NULL) {
return -1;
}
lv_line_set_points(leg_l, g_deco_leg_l, 2);
lv_line_set_points(leg_r, g_deco_leg_r, 2);
lv_line_set_points(s_root_deco, g_deco_hoz, 2);
deco_line_apply(leg_l, MENU_DECO_LEG_W);
deco_line_apply(leg_r, MENU_DECO_LEG_W);
deco_line_apply(s_root_deco, MENU_DECO_HORIZ_W);
}
s_root_status = lv_label_create(s_pages[0].cont);
if (s_root_status == NULL) {
return -1;
}
lv_obj_add_style(s_root_status, &s_style_flat, 0);
lv_obj_add_flag(s_root_status, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(s_root_status, MENU_ROOT_STATUS_X, MENU_ROOT_STATUS_Y);
lv_label_set_text_static(s_root_status, "当前模式 : 无模式");
s_root_box = lv_obj_create(s_pages[0].cont);
if (s_root_box == NULL) {
return -1;
}
lv_obj_add_style(s_root_box, &s_style_root_box, 0);
lv_obj_add_flag(s_root_box, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(s_root_box, MENU_ROOT_BOX_X, MENU_ROOT_BOX_Y);
lv_obj_set_size(s_root_box, MENU_ROOT_BOX_W, MENU_ROOT_BOX_H);
lv_obj_set_flex_flow(s_root_box, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(s_root_box, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START);
/* 构建根页并显示 */
menu_page_build(&s_pages[0], MENU_NODE_ROOT);
menu_show_page(&s_pages[0]);
/* 根框:主界面(node 0)子项;建一次、持久 */
ui_menu_view_frame_build(0u, 0u, 0u, 0u);
ui_menu_view_focus(0u);
lv_mem_monitor(&mon);
DBG_INFO("menu build done: total=%u free=%u big=%u used=%u",
+14 -9
View File
@@ -1,12 +1,16 @@
/*
* 模块名称:UI Menu (菜单页面构建)
* 模块功能:基于 LVGL 基元控件(页面导航)复现 DTU-HMI 树形菜单,支持方向键导航与返回
* 适用平台:STM32F407ZGTx
* 模块名称:UI Menu (菜单导航逻辑层)
* 模块功能:g_menu_tab 父子索引表驱动的横向级联菜单导航——持有导航状态
* (路径/选中/深度/滚动,共 17 B)与键语义(↑↓移选、→/ENTER 进、←/ESC 退),
* 单向驱动显示层 ui_menu_view 建/毁/刷新级联框;本层不持有任何 LVGL 对象
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-08-29
* 修改记录:
* 2026-08-29 王建锋 创建初始版本
* 2026-09-02 王建锋 弃用已废弃的 lv_menu 控件,改为基元控件页面导航
* 2026-09-04 王建锋 重构为"横向级联框"模型(菜单页面设计.md):拆出显示层
* ui_menu_view 与数据层 ui_menu_tab;本层只留导航状态机
*/
#ifndef UI_MENU_H
#define UI_MENU_H
@@ -16,13 +20,14 @@ extern "C" {
#endif
/*
* 函数功能:构建菜单(基元控件页面导航:页头返回按钮 + 页面切换)
* 函数功能:构建菜单(横向级联框:右贴靠 + 自适应宽 + 顶/底对齐)
* 入口参数:无
* 返回值:0 - 成功,其他 - 失败(对象创建失败)
* 限定条件:ui_port_init() 已成功调用,活动屏幕存在
* 函数说明:1. 依据紧凑节点表按层级组织页面,ENTER 进入子页、ESC/LEFT 返回
* 2. 页面容器加入默认输入组,按键由当前页容器统一处理
* 3. 页面按需构建(进入时建、返回时毁),叶子节点为空白占位页
* 返回值:0 - 成功,其他 - 失败(菜单表自检失败或对象创建失败)
* 限定条件:ui_port_init()(或模拟器 sim_port_init)已成功调用,活动屏幕存在
* 函数说明: 1. 数据表自检(parent 下标均有效,捕获表内笔误)
* 2. 初始化显示层(样式/4 层持久容器/根框装饰与框体)
* 3. 各层容器加入默认输入组,按键由逻辑层统一分发(显示层纯渲染)
* 4. 构建根框(主界面 node 0 的子项)并置焦点
*/
int ui_menu_init(void);
+119
View File
@@ -0,0 +1,119 @@
/* 本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 自动生成,请勿手改。
* 修改菜单:编辑 ui_menu_tree.txt 后重跑 `python tools/menu_gen/gen_menu.py`。
*/
#include "ui_menu_tab.h"
const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = {
{ MENU_ROOT, "主界面" }, /* 0 L0 */
{ 0, "装置信息" }, /* 1 L1 */
{ 0, "实时数据" }, /* 2 L1 */
{ 2, "交流量" }, /* 3 L2 */
{ 2, "直流量" }, /* 4 L2 */
{ 2, "遥信量" }, /* 5 L2 */
{ 0, "参数定值" }, /* 6 L1 */
{ 6, "整定" }, /* 7 L2 */
{ 7, "参数" }, /* 8 L3 */
{ 7, "定值" }, /* 9 L3 */
{ 7, "控制字" }, /* 10 L3 */
{ 7, "软压板" }, /* 11 L3 */
{ 6, "查看" }, /* 12 L2 */
{ 12, "参数" }, /* 13 L3 */
{ 12, "定值" }, /* 14 L3 */
{ 12, "控制字" }, /* 15 L3 */
{ 12, "软压板" }, /* 16 L3 */
{ 0, "三遥设置" }, /* 17 L1 */
{ 17, "遥测死区" }, /* 18 L2 */
{ 17, "遥测系数" }, /* 19 L2 */
{ 17, "遥信类型" }, /* 20 L2 */
{ 17, "遥信防抖" }, /* 21 L2 */
{ 17, "双点遥信" }, /* 22 L2 */
{ 0, "装置维护" }, /* 23 L1 */
{ 23, "时钟设置" }, /* 24 L2 */
{ 23, "强制复归" }, /* 25 L2 */
{ 23, "手动录波" }, /* 26 L2 */
{ 23, "清除记录" }, /* 27 L2 */
{ 0, "通讯参数" }, /* 28 L1 */
{ 28, "通讯设置" }, /* 29 L2 */
{ 28, "网口设置" }, /* 30 L2 */
{ 28, "SNTP设置" }, /* 31 L2 */
{ 0, "记录查询" }, /* 32 L1 */
{ 32, "SOE记录" }, /* 33 L2 */
{ 32, "事故记录" }, /* 34 L2 */
{ 32, "操作记录" }, /* 35 L2 */
{ 32, "保护告警" }, /* 36 L2 */
{ 32, "保护启动" }, /* 37 L2 */
{ 32, "遥控记录" }, /* 38 L2 */
{ 32, "自检记录" }, /* 39 L2 */
{ 32, "运行记录" }, /* 40 L2 */
{ 32, "运行报告" }, /* 41 L2 */
};
/*
* 函数功能:取 node 的直接子项(按表序),最多 max 个
* 入口参数:node - 节点下标 uint8_t
* out - 子节点下标输出缓冲 uint8_t *
* max - 缓冲容量 uint8_t
* 返回值:实际子项个数 uint8_t
* 限定条件:node < UI_MENU_TAB_COUNT
* 函数说明:子项 = 表中所有 parent == 本下标的行(表序即显示序);建页核心查询
*/
uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max)
{
uint8_t n = 0u;
for (uint8_t i = 0u; i < UI_MENU_TAB_COUNT && n < max; i++) {
if (g_menu_tab[i].parent == node) {
out[n++] = i;
}
}
return n;
}
/*
* 函数功能:判断 node 有无子项
* 入口参数:node - 节点下标 uint8_t
* 返回值:有子项返回 true(=菜单、行带▶);无子项返回 false(=叶子)
* 限定条件:node < UI_MENU_TAB_COUNT
* 函数说明:菜单/叶子由"是否有子项"自动推导,无单独类型字段
*/
bool ui_menu_tab_has_children(uint8_t node)
{
for (uint8_t i = 0u; i < UI_MENU_TAB_COUNT; i++) {
if (g_menu_tab[i].parent == node) {
return true;
}
}
return false;
}
/*
* 函数功能:取节点显示名
* 入口参数:node - 节点下标 uint8_t
* 返回值:显示文本(Flash 字面量) const char *
* 限定条件:node < UI_MENU_TAB_COUNT
* 函数说明:行文本一律经此取静态字符串,不 strdup
*/
const char *ui_menu_tab_name(uint8_t node)
{
return g_menu_tab[node].name;
}
/*
* 函数功能:菜单表启动自检
* 入口参数:无
* 返回值:true - 表有效;false - 存在越界 parent(表内笔误)
* 限定条件:无
* 函数说明:每个非根节点的 parent 必须为 MENU_ROOT 或有效下标;在 ui_menu_init 调用
*/
bool ui_menu_tab_valid(void)
{
for (uint8_t i = 0u; i < UI_MENU_TAB_COUNT; i++) {
if (i == 0u) {
continue; /* 根 */
}
uint8_t p = g_menu_tab[i].parent;
if (p != MENU_ROOT && p >= UI_MENU_TAB_COUNT) {
return false;
}
}
return true;
}
+34
View File
@@ -0,0 +1,34 @@
/* 本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 自动生成,请勿手改。
* 修改菜单:编辑 ui_menu_tree.txt 后重跑 `python tools/menu_gen/gen_menu.py`。
*/
#ifndef UI_MENU_TAB_H
#define UI_MENU_TAB_H
#include <stdint.h>
#include <stdbool.h>
#define MENU_ROOT 0xFFu /* 根哨兵:仅根节点(主界面)的 parent 取此值;8 位,上限 255 项 */
/* 菜单节点:子项 = parent 等于本下标的所有行(按表序);有子项=菜单,无=叶子 */
typedef struct {
uint8_t parent; /* 父节点下标(0..254);根为 MENU_ROOT(0xFF) */
const char *name; /* 显示文本(字面量,存 Flash) */
} ui_menu_node_t;
#define UI_MENU_TAB_COUNT 42
/* 护栏:8 位索引上限 255 项(哨兵 0xFF 占用 255);超界则编译失败(C99 写法,兼容 ARMCC v5) */
typedef char menu_tab_count_check[(UI_MENU_TAB_COUNT <= 255) ? 1 : -1];
extern const ui_menu_node_t g_menu_tab[];
/*
* 表查询 API(导航逻辑层 ui_menu.c 与显示层 ui_menu_view.c 共用):
* 实现随表生成,改菜单(ui_menu_tree.txt)调用方零代码改动。
*/
uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max); /* 取 node 直接子项(按表序),最多 max 个;返回实际个数 */
bool ui_menu_tab_has_children(uint8_t node); /* 有子项=菜单(带▶);无=叶子 */
const char *ui_menu_tab_name(uint8_t node); /* 显示名 */
bool ui_menu_tab_valid(void); /* 启动自检:非根 parent 下标均有效 */
#endif
+490
View File
@@ -0,0 +1,490 @@
/*
* 模块名称:UI Menu View (菜单显示模型渲染层)
* 模块功能:以 LVGL 基元控件实现《菜单页面设计.md》§四⑧"横向级联子框"显示模型:
* 右贴靠(子框左缘=父框右缘、共享边框列)+ 框宽内容自适应 +
* 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框/行/根框装饰),
* 不持有任何导航状态——状态由逻辑层 ui_menu.c 经本接口驱动
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-09-04
* 修改记录:
* 2026-09-04 王建锋 创建:自旧 ui_menu.c 单页模型拆分出显示层(高内聚低耦合)
*/
#include "ui_menu_view.h"
#include "ui_menu_tab.h"
#include "lvgl.h"
#include <string.h>
#define DBG_TAG "[UI_MENU_VIEW]"
#include "dbg_log.h"
/*
* 布局常量(设备像素;参考图 02~07 实测 + 字体度量速查推导):
* 汉字步进 12px/字、▶ 步进 10px、行高 15(7 行×15+2 边框=107,与参考图根框一致);
* 行内留白取最小值 2px——最深 4 框级联(根 66 + 42 + 42 + 空框 6 - 3 共享边框)
* = 153px,须 < 屏宽 160
*/
#define MENU_ROW_H 15 /* 行高 */
#define MENU_ROW_TXT_TOP 1 /* 行内文字垂直居中偏移((15-13)/2,字体行高 13) */
#define MENU_ROW_PAD_L 2 /* 行内文字左留白(紧留白,保证级联总宽 < 160) */
#define MENU_ROW_PAD_R 2 /* 行内右侧留白 */
#define MENU_ARROW_GAP 2 /* 行内 文本→箭头 间距 */
#define MENU_FRAME_BORDER 1 /* 框边框宽(子框与父框共享该边框列) */
#define MENU_ROOT_BOX_X 0 /* 根框左(左对齐,贴左缘) */
#define MENU_ROOT_BOX_Y 44 /* 根框顶 */
#define MENU_ROOT_STATUS_X 17 /* 状态行左 */
#define MENU_ROOT_STATUS_Y 20 /* 状态行顶 */
#define MENU_DECO_LEG_W 3 /* 顶部"浅 V"两腿线宽(1bpp 下斜线边缘易被阈值吃掉,需加粗) */
#define MENU_DECO_HORIZ_W 2 /* 顶部"浅 V"水平线宽 */
/*
* 框几何缓存:位置/尺寸是"现算"的(不落导航状态),但子框对齐要用父框刚算出的值,
* 故按层缓存一份;sel 为最近一次应用的高亮(子框垂直对齐依据),由逻辑层经
* build/refresh 保持同步
*/
typedef struct {
bool built; /* 框已建(框体 + 行对象存在) */
uint8_t sel; /* 最近应用的选中行 */
uint8_t item_cnt; /* 行数 */
int32_t x, y; /* 框左上角(含边框) */
int32_t w, h; /* 框外廓尺寸 */
} menu_frame_t;
static lv_obj_t *s_cont[MENU_PAGE_MAX]; /* 4 层持久容器(每深度一个;焦点/键入口,永不删) */
static lv_obj_t *s_frame_box[MENU_PAGE_MAX]; /* 框体:d=0 持久(根框);d>=1 进入时建、返回时毁 */
static lv_obj_t *s_row[MENU_PAGE_MAX][MENU_ITEM_MAX]; /* 行容器(整行反色高亮的载体) */
static lv_obj_t *s_row_text[MENU_PAGE_MAX][MENU_ITEM_MAX]; /* 行文本 label */
static lv_obj_t *s_row_arrow[MENU_PAGE_MAX][MENU_ITEM_MAX]; /* 行箭头 label(叶子=NULL) */
static menu_frame_t s_frame[MENU_PAGE_MAX];
static lv_obj_t *s_root_deco = NULL; /* 根框装饰:顶部"浅 V"水平段(两腿为临时对象,样式同源) */
static lv_obj_t *s_root_status = NULL; /* 根框状态行 */
/* 样式(静态,一次性初始化,常驻) */
static lv_style_t s_style_flat; /* 通用:透明、无边框/圆角/内边距(剥离主题 card) */
static lv_style_t s_style_selected; /* 选中态:整行反色(白底) */
static lv_style_t s_style_frame; /* 框体:透明底 + 1px 白边框 */
static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */
/*
* 函数功能:初始化显示层样式(静态样式,仅执行一次)
* 入口参数:无
* 返回值:无
* 限定条件:无
* 函数说明: 1. flat 剥离主题给对象附加的卡片样式(边框/圆角/内边距)
* 2. frame 为所有级联框(含根框)的 1px 白边框
* 3. selected 为选中行整行反色(1bpp 白底);文字色在刷新时显式置黑
*/
static void view_styles_init(void)
{
static bool inited = false;
if (inited) {
return;
}
inited = true;
lv_style_init(&s_style_flat);
lv_style_set_bg_opa(&s_style_flat, LV_OPA_TRANSP);
lv_style_set_border_width(&s_style_flat, 0);
lv_style_set_radius(&s_style_flat, 0);
lv_style_set_pad_all(&s_style_flat, 0);
lv_style_set_pad_gap(&s_style_flat, 0);
lv_style_init(&s_style_selected);
lv_style_set_bg_opa(&s_style_selected, LV_OPA_COVER);
lv_style_set_bg_color(&s_style_selected, lv_color_white());
lv_style_set_text_color(&s_style_selected, lv_color_black());
lv_style_init(&s_style_frame);
lv_style_set_bg_opa(&s_style_frame, LV_OPA_TRANSP);
lv_style_set_border_width(&s_style_frame, MENU_FRAME_BORDER);
lv_style_set_border_color(&s_style_frame, lv_color_white());
lv_style_set_radius(&s_style_frame, 0);
lv_style_set_pad_all(&s_style_frame, 0);
lv_style_set_pad_gap(&s_style_frame, 0);
lv_style_init(&s_style_scrollbar);
lv_style_set_bg_opa(&s_style_scrollbar, LV_OPA_COVER);
lv_style_set_bg_color(&s_style_scrollbar, lv_color_white());
lv_style_set_width(&s_style_scrollbar, 4);
}
/*
* 函数功能:计算框外廓宽(内容自适应)
* 入口参数:kids - 子节点下标数组(各行内容) const uint8_t *
* cnt - 条目数 uint8_t
* 返回值:框外廓宽 px int32_t = 最宽行内容 + 左右边框
* 限定条件:LVGL 已初始化(字体度量可用)
* 函数说明:行内容宽 = 左留白 + 文本 + (菜单 ? 间距 + ▶ : 0) + 右留白;
* 文本/▶ 宽取字体实际步进(汉字 12px、▶ 10px);
* 注意本 LVGL 版 lv_text_get_width 的 length=0 表示"不测量",必须传 strlen
*/
static int32_t view_frame_width(const uint8_t *kids, uint8_t cnt)
{
const lv_font_t *font = LV_FONT_DEFAULT;
int32_t w = 0;
for (uint8_t i = 0; i < cnt; i++) {
const char *name = ui_menu_tab_name(kids[i]);
int32_t tw = lv_text_get_width(name, (uint32_t)strlen(name), font, 0);
int32_t cw = MENU_ROW_PAD_L + tw + MENU_ROW_PAD_R;
if (ui_menu_tab_has_children(kids[i])) {
cw += MENU_ARROW_GAP + lv_text_get_width("▶", (uint32_t)strlen("▶"), font, 0);
}
if (cw > w) {
w = cw;
}
}
return w + 2 * MENU_FRAME_BORDER;
}
/*
* 函数功能:在框体上创建一条条目行(行容器 + 文本 + 可选箭头)
* 入口参数:box - 所属框体 lv_obj_t *
* d - 框所在深度 uint8_t
* i - 行下标 uint8_t
* node - 条目节点下标 uint8_t
* has_arrow - 是否创建箭头(=条目有子项) bool
* 返回值:true - 成功;false - 对象创建失败(LVGL 池耗尽,调用方停止建行)
* 限定条件:d < MENU_PAGE_MAX,i < MENU_ITEM_MAX
* 函数说明: 1. 行容器整行宽、高 MENU_ROW_H,承载选中整行反色高亮
* 2. 文本左留白 MENU_ROW_PAD_L;箭头紧跟文本(间距 MENU_ARROW_GAP,自适应)
* 3. 文本/箭头子 label 加 IGNORE_LAYOUT 做行内绝对定位
*/
static bool view_add_row(lv_obj_t *box, uint8_t d, uint8_t i, uint8_t node, bool has_arrow)
{
lv_obj_t *row = lv_obj_create(box);
if (row == NULL) {
return false;
}
lv_obj_add_style(row, &s_style_flat, 0);
lv_obj_add_style(row, &s_style_selected, LV_STATE_CHECKED);
lv_obj_set_width(row, lv_pct(100));
lv_obj_set_height(row, MENU_ROW_H);
lv_obj_t *lbl = lv_label_create(row);
if (lbl == NULL) {
lv_obj_delete(row);
return false;
}
lv_obj_add_style(lbl, &s_style_flat, 0);
lv_obj_add_flag(lbl, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(lbl, MENU_ROW_PAD_L, MENU_ROW_TXT_TOP);
lv_label_set_text_static(lbl, ui_menu_tab_name(node));
lv_obj_t *ar = NULL;
if (has_arrow) {
const char *name = ui_menu_tab_name(node);
int32_t tw = lv_text_get_width(name, (uint32_t)strlen(name), LV_FONT_DEFAULT, 0);
ar = lv_label_create(row);
if (ar != NULL) {
lv_obj_add_style(ar, &s_style_flat, 0);
lv_obj_add_flag(ar, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(ar, MENU_ROW_PAD_L + tw + MENU_ARROW_GAP, MENU_ROW_TXT_TOP);
lv_label_set_text_static(ar, "▶");
}
}
s_row[d][i] = row;
s_row_text[d][i] = lbl;
s_row_arrow[d][i] = ar;
return true;
}
/*
* 函数功能:应用指定深度框的选中高亮(整行反色 + 文字/箭头颜色)
* 入口参数:depth - 深度 uint8_t
* sel - 选中行 uint8_t
* 返回值:无
* 限定条件:该层行对象已建
* 函数说明:1. 选中行整行反色(白底黑字),箭头随反色变黑(参考图实测:选中行保留 ▶)
* 2. 非选中行透明底白字、白色箭头
*/
static void view_apply_sel(uint8_t depth, uint8_t sel)
{
menu_frame_t *f = &s_frame[depth];
for (uint8_t i = 0; i < f->item_cnt; i++) {
bool is_sel = (i == sel);
if (is_sel) {
lv_obj_add_state(s_row[depth][i], LV_STATE_CHECKED);
}
else {
lv_obj_clear_state(s_row[depth][i], LV_STATE_CHECKED);
}
lv_obj_set_style_text_color(s_row_text[depth][i],
is_sel ? lv_color_black() : lv_color_white(), 0);
if (s_row_arrow[depth][i] != NULL) {
lv_obj_set_style_text_color(s_row_arrow[depth][i],
is_sel ? lv_color_black() : lv_color_white(), 0);
}
}
}
/*
* 函数功能:配置框体布局(纵向 flex 堆行 + 自动滚动条)
* 入口参数:box - 框体 lv_obj_t *
* 返回值:无
* 限定条件:box 已创建
* 函数说明:行内容超框高时框内滚动(本菜单最深 9 行不触发,机制为更长列表保留)
*/
static void view_box_layout(lv_obj_t *box)
{
lv_obj_set_flex_flow(box, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(box, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_scrollbar_mode(box, LV_SCROLLBAR_MODE_AUTO);
lv_obj_add_style(box, &s_style_scrollbar, LV_PART_SCROLLBAR);
}
void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t scroll)
{
menu_frame_t *f = &s_frame[depth];
lv_obj_t *box;
uint8_t kids[MENU_ITEM_MAX];
uint8_t cnt = ui_menu_tab_children(node, kids, MENU_ITEM_MAX);
if (depth == 0u) {
box = s_frame_box[0]; /* 根框框体持久(view_init 已建) */
}
else {
if (f->built) {
ui_menu_view_frame_destroy(depth); /* 防御:重入 = 先毁后建 */
}
const menu_frame_t *p = &s_frame[depth - 1u];
/* X:右贴靠——子框左缘 = 父框右缘(共享边框列、零间隙) */
f->x = p->x + p->w - MENU_FRAME_BORDER;
/* Y:顶对齐父框"选中行"行顶;(顶边+框高) 超屏底 → 上移使本框底边
* 对齐父框"选中行"行底(顶部不钳制,可叠到状态行区);
* 仍超顶(比整屏高)→ 钳制顶部,框高截到可见区、行改框内滚动 */
int32_t row_top = p->y + MENU_FRAME_BORDER + (int32_t)p->sel * MENU_ROW_H;
int32_t h = (int32_t)cnt * MENU_ROW_H + 2 * MENU_FRAME_BORDER;
f->y = row_top;
if (f->y + h > MENU_SCREEN_H) {
f->y = row_top + MENU_ROW_H - h;
}
if (f->y < 0) {
f->y = 0;
}
if (f->y + h > MENU_SCREEN_H) {
h = MENU_SCREEN_H - f->y;
}
f->h = h;
f->w = (cnt > 0u) ? view_frame_width(kids, cnt)
: (2 * MENU_FRAME_BORDER + MENU_ROW_PAD_L + MENU_ROW_PAD_R);
box = lv_obj_create(s_cont[depth]);
if (box == NULL) {
return; /* LVGL 池耗尽:框未建,状态回滚 */
}
lv_obj_add_style(box, &s_style_flat, 0);
lv_obj_add_style(box, &s_style_frame, 0);
lv_obj_add_flag(box, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(box, f->x, f->y);
lv_obj_set_size(box, f->w, f->h);
view_box_layout(box);
s_frame_box[depth] = box;
}
f->built = true;
f->item_cnt = cnt;
f->sel = sel;
/* 行对象:条目现算(= parent==node 过滤),进时建、退时毁 */
lv_obj_clean(box);
for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) {
s_row[depth][i] = NULL;
s_row_text[depth][i] = NULL;
s_row_arrow[depth][i] = NULL;
}
for (uint8_t i = 0; i < cnt; i++) {
if (!view_add_row(box, depth, i, kids[i], ui_menu_tab_has_children(kids[i]))) {
break; /* LVGL 池耗尽:停止建行(池有界,见设计 §7.5) */
}
}
/* 选中高亮 + 滚动位 */
view_apply_sel(depth, sel);
if (scroll > 0u) {
lv_obj_scroll_to_y(box, (int32_t)scroll, LV_ANIM_OFF);
}
else if (cnt > 0u) {
lv_obj_scroll_to_view(s_row[depth][sel], LV_ANIM_OFF);
}
}
void ui_menu_view_frame_destroy(uint8_t depth)
{
menu_frame_t *f = &s_frame[depth];
if (depth == 0u || !f->built) {
return; /* 根框持久不可销毁 */
}
lv_obj_delete(s_frame_box[depth]); /* 框体连行对象整体删除(容器保留) */
s_frame_box[depth] = NULL;
for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) {
s_row[depth][i] = NULL;
s_row_text[depth][i] = NULL;
s_row_arrow[depth][i] = NULL;
}
f->built = false;
f->item_cnt = 0u;
f->sel = 0u;
f->x = 0;
f->y = 0;
f->w = 0;
f->h = 0;
}
void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel)
{
menu_frame_t *f = &s_frame[depth];
if (!f->built || depth >= MENU_PAGE_MAX || sel >= f->item_cnt) {
return;
}
f->sel = sel;
view_apply_sel(depth, sel);
/* 选中变化只改行状态 + 跟随滚动,不重建对象 */
lv_obj_scroll_to_view(s_row[depth][sel], LV_ANIM_OFF);
}
void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll)
{
if (depth >= MENU_PAGE_MAX || !s_frame[depth].built || s_frame_box[depth] == NULL) {
return;
}
lv_obj_scroll_to_y(s_frame_box[depth], (int32_t)scroll, LV_ANIM_OFF);
}
uint16_t ui_menu_view_get_scroll(uint8_t depth)
{
if (depth >= MENU_PAGE_MAX || !s_frame[depth].built || s_frame_box[depth] == NULL) {
return 0u;
}
return (uint16_t)lv_obj_get_scroll_y(s_frame_box[depth]);
}
void ui_menu_view_focus(uint8_t depth)
{
if (depth < MENU_PAGE_MAX && s_cont[depth] != NULL) {
lv_group_focus_obj(s_cont[depth]);
}
}
lv_obj_t *ui_menu_view_container(uint8_t depth)
{
return (depth < MENU_PAGE_MAX) ? s_cont[depth] : NULL;
}
/*
* 函数功能:应用主界面顶部"浅 V"装饰线段的公共样式
* 入口参数:line - lv_line 对象;width - 线宽(px)
* 返回值:无
* 限定条件:line 已创建且已设点集
* 函数说明:绝对定位于 (0,0)、忽略布局、白色实线、指定线宽
*/
static void view_deco_line_apply(lv_obj_t *line, int32_t width)
{
lv_obj_add_flag(line, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(line, 0, 0);
lv_obj_set_size(line, MENU_SCREEN_W, 20);
lv_obj_set_style_line_width(line, width, 0);
lv_obj_set_style_line_color(line, lv_color_white(), 0);
lv_obj_set_style_line_opa(line, LV_OPA_COVER, 0);
}
int ui_menu_view_init(void)
{
lv_obj_t *scr = lv_screen_active();
if (scr == NULL) {
return -1;
}
view_styles_init();
lv_display_t *disp = lv_display_get_default();
int32_t sw = lv_display_get_horizontal_resolution(disp);
int32_t sh = lv_display_get_vertical_resolution(disp);
/* 屏幕根容器:铺满屏幕;子对象全部绝对定位(级联框须同屏叠加,不用 flex 堆叠) */
lv_obj_t *menu = lv_obj_create(scr);
if (menu == NULL) {
return -1;
}
lv_obj_add_style(menu, &s_style_flat, 0);
lv_obj_set_size(menu, sw, sh);
/* 4 层持久容器:每深度一个、全屏覆盖、永不删(焦点/键入口,防悬空) */
for (uint8_t d = 0; d < MENU_PAGE_MAX; d++) {
s_cont[d] = lv_obj_create(menu);
if (s_cont[d] == NULL) {
return -1;
}
lv_obj_add_style(s_cont[d], &s_style_flat, 0);
lv_obj_add_flag(s_cont[d], LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(s_cont[d], 0, 0);
lv_obj_set_size(s_cont[d], sw, sh);
lv_obj_add_flag(s_cont[d], LV_OBJ_FLAG_CLICK_FOCUSABLE);
}
/* 根框装饰:顶部"开口浅 V"(两腿加粗、水平线细)+ 状态行(绝对定位于 d=0 容器)
* 1bpp 下斜线边缘易被阈值吃掉,腿须比水平线粗才接近参考图 */
{
static const lv_point_precise_t deco_leg_l[] = { {7, 7}, {16, 17} };
static const lv_point_precise_t deco_hoz[] = { {16, 17}, {145, 17} };
static const lv_point_precise_t deco_leg_r[] = { {158, 7}, {145, 17} };
lv_obj_t *leg_l = lv_line_create(s_cont[0]);
lv_obj_t *leg_r = lv_line_create(s_cont[0]);
s_root_deco = lv_line_create(s_cont[0]);
if (leg_l == NULL || leg_r == NULL || s_root_deco == NULL) {
return -1;
}
lv_line_set_points(leg_l, deco_leg_l, 2);
lv_line_set_points(leg_r, deco_leg_r, 2);
lv_line_set_points(s_root_deco, deco_hoz, 2);
view_deco_line_apply(leg_l, MENU_DECO_LEG_W);
view_deco_line_apply(leg_r, MENU_DECO_LEG_W);
view_deco_line_apply(s_root_deco, MENU_DECO_HORIZ_W);
}
s_root_status = lv_label_create(s_cont[0]);
if (s_root_status == NULL) {
return -1;
}
lv_obj_add_style(s_root_status, &s_style_flat, 0);
lv_obj_add_flag(s_root_status, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(s_root_status, MENU_ROOT_STATUS_X, MENU_ROOT_STATUS_Y);
lv_label_set_text_static(s_root_status, "当前模式 : 无模式");
/* 根框框体:持久;宽 = 内容自适应(主界面 node 0 的子项) */
uint8_t kids[MENU_ITEM_MAX];
uint8_t cnt = ui_menu_tab_children(0u, kids, MENU_ITEM_MAX);
int32_t w = view_frame_width(kids, cnt);
int32_t h = (int32_t)cnt * MENU_ROW_H + 2 * MENU_FRAME_BORDER;
s_frame_box[0] = lv_obj_create(s_cont[0]);
if (s_frame_box[0] == NULL) {
return -1;
}
lv_obj_add_style(s_frame_box[0], &s_style_flat, 0);
lv_obj_add_style(s_frame_box[0], &s_style_frame, 0);
lv_obj_add_flag(s_frame_box[0], LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(s_frame_box[0], MENU_ROOT_BOX_X, MENU_ROOT_BOX_Y);
lv_obj_set_size(s_frame_box[0], w, h);
view_box_layout(s_frame_box[0]);
s_frame[0].built = true; /* 框体就绪;行对象由逻辑层 frame_build(0) 创建 */
s_frame[0].x = MENU_ROOT_BOX_X;
s_frame[0].y = MENU_ROOT_BOX_Y;
s_frame[0].w = w;
s_frame[0].h = h;
DBG_INFO("view init: root frame w=%d h=%d", (int)w, (int)h);
return 0;
}
+109
View File
@@ -0,0 +1,109 @@
/*
* 模块名称:UI Menu View (菜单显示模型渲染层)
* 模块功能:以 LVGL 基元控件实现《菜单页面设计.md》§四⑧"横向级联子框"显示模型:
* 右贴靠(子框左缘=父框右缘、共享边框列)+ 框宽内容自适应 +
* 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框/行/根框装饰),
* 不持有任何导航状态——状态由逻辑层 ui_menu.c 经本接口驱动
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-09-04
* 修改记录:
* 2026-09-04 王建锋 创建:自旧 ui_menu.c 单页模型拆分出显示层(高内聚低耦合)
*/
#ifndef UI_MENU_VIEW_H
#define UI_MENU_VIEW_H
#include <stdint.h>
#include <stdbool.h>
#include "lvgl.h"
#define MENU_PAGE_MAX 4 /* 页面深度上限(根框 + 3 级子框) */
#define MENU_ITEM_MAX 16 /* 单框最大条目数 */
#define MENU_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */
#define MENU_SCREEN_H 160 /* 屏幕高(px) */
/*
* 函数功能:初始化显示层(样式 + 4 层持久容器 + 根框装饰 + 根框框体)
* 入口参数:无
* 返回值:0 - 成功,其他 - 失败(对象创建失败)
* 限定条件:LVGL 已初始化且活动屏幕存在(ui_port_init / sim_port_init 已完成)
* 函数说明:1. 4 层容器每深度一个、全屏覆盖、持久不删(焦点/键入口,防悬空)
* 2. 根框装饰(浅 V + 状态行)与根框框体只属根层(d=0)
* 3. 行对象不在此建,由逻辑层调 ui_menu_view_frame_build 按需建
*/
int ui_menu_view_init(void);
/*
* 函数功能:取指定深度的持久容器
* 入口参数:depth - 深度 0..MENU_PAGE_MAX-1 uint8_t
* 返回值:容器对象 lv_obj_t * 越界返回 NULL
* 限定条件:ui_menu_view_init() 已完成
* 函数说明:逻辑层在容器上绑定输入组与键事件(显示层不碰输入)
*/
lv_obj_t *ui_menu_view_container(uint8_t depth);
/*
* 函数功能:构建指定深度的框(几何现算 + 行对象创建 + 选中/滚动应用)
* 入口参数:depth - 深度(0=根框) uint8_t
* node - 本框展示节点(行 = 其子项) uint8_t
* sel - 本框选中行 uint8_t
* scroll - 本框滚动偏移(px,0=顶部) uint16_t
* 返回值:无
* 限定条件:depth>0 时 depth-1 层框已建(父框几何是其对齐依据)
* 函数说明: 1. d=0:根框框体已存在,仅(重)建行对象
* 2. d>0:右贴靠父框右缘;顶边=父框选中行行顶,超屏底则
* 上移使本框底边对齐父框选中行行底,仍超顶则钳制顶部并改框内滚动
* 3. 框宽 = 最宽行(文本 + ▶ + 左右留白)+ 边框;条目现算不落地
*/
void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t scroll);
/*
* 函数功能:销毁指定深度的框(框体连同行对象,容器保留)
* 入口参数:depth - 深度 uint8_t
* 返回值:无
* 限定条件:depth>0(根框持久不可销毁)
* 函数说明:返回(关最右框)时调用,LVGL 池内存随即回落
*/
void ui_menu_view_frame_destroy(uint8_t depth);
/*
* 函数功能:刷新指定深度框的选中高亮
* 入口参数:depth - 深度 uint8_t
* sel - 新选中行 uint8_t
* 返回值:无
* 限定条件:ui_menu_view_frame_build(depth,…) 已完成
* 函数说明:只改行状态与文字色 + 跟随滚动,不重建对象
*/
void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel);
/*
* 函数功能:应用指定深度框的滚动偏移
* 入口参数:depth - 深度 uint8_t
* scroll - 滚动偏移(px) uint16_t
* 返回值:无
* 限定条件:该层框已建
* 函数说明:行对象重建后按此还原用户滚动位置
*/
void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll);
/*
* 函数功能:读取指定深度框当前滚动偏移
* 入口参数:depth - 深度 uint8_t
* 返回值:滚动偏移(px) uint16_t 未建框返回 0
* 限定条件:无
* 函数说明:跟随滚动后由逻辑层读回,写入导航状态持久化
*/
uint16_t ui_menu_view_get_scroll(uint8_t depth);
/*
* 函数功能:焦点移至指定深度的容器
* 入口参数:depth - 深度 uint8_t
* 返回值:无
* 限定条件:ui_menu_view_init() 已完成且容器已加入输入组
* 函数说明:焦点=最右框;进/退时由逻辑层调用
*/
void ui_menu_view_focus(uint8_t depth);
#endif /* UI_MENU_VIEW_H */