GUI 框架基础需求、设计和实现 - 1 基础元素
以下皆为建议性内容,根据需求制定方案。
这里列出 GUI 开发中的 通用性 的 需求、问题的解法和方案 等,为了能够搭建出复杂 GUI 显示和交互逻辑,满足交互设计需求。
文章所在 Github 仓库 Staok/GUI-Framework-Study: GUI 框架基础需求、设计和实现文章 会保持最新,其它地方的不会跟进。
基础元素
GUI 综合性工程所需 Features
UI feature 基础需求,如 widgets,layout (如 flex,自定义排列、滑动 / 滑动条 等)),style、flag、state 系统(这里为引用 LVGL 的特性),ttf 字体支持(对 freeType 库的支持),图片显示支持(图片数组(未解码或解码的图片数据)、图片文件),event 系统(灵活的 input / driver 设置,事件响应回调设置等),多页面(screen) 等,渐变色支持,anim(obj 的基本变化形式(如位置、长宽、旋转、透明度 等)在时间轴打关键点实现)等,下面给出比较全面的列举。
这里总结 支持较复杂 UI 设计和实现 需求 的 基本元素 和 Features:
-
UI 创建。
参考
交互设计搞确认项一节。 -
足够的基础组件。可进一步封装组件,可自设计组件。
给 obj 添加事件回调(函数),事件类型要够丰富。
够丰富的 style 支持(参考 lvgl 的 style 系统);丰富的 flag、state 支持;layout 支持(如 lvgl 的 flex)。
参考
GUI 设计 常见基础组件 / Widgets一节。 -
多页面。
参考
页面管理一节。 -
多字体 / 多语言支持。
参考
多字体 / 多语言 以及 高度自动化一节。 -
多机型。资产管理。
参考
多机型 / 功能宏 / 素材资产管理一节。 -
UI Debug。
参考
UI 测试 / 自动化测试一节。
GUI 设计 常见基础组件
基础 Widgets
-
Panel 块。矩形等基础形状,长宽,颜色,倒圆角,透明度,边框线,背景图(可用于透明标识(灰白方格马赛克,可以软件生成)),渐变色。
-
画点、画线,画圆(中心点坐标,半径,是否填充,宽度)。
-
文本 Label。使用指定字体、多种大小;换行、省略号、滚动显示;加粗、下划线;(可选)文字中间变颜色、可点击、变字体、变大小 等。
-
按键 Btn(可做多种风格的,用于不同的地方)。事件:短按、长按、按住 等。
-
开关 Switch、选择框 CheckBox。状态:选中、非选中、半选中;选中状态下不可点、非选中状态下不可点。
-
下拉框 Choose。
-
进度条。
-
拖动条 / 滑动条。
-
输入框。
-
键盘 KeyBoard。可定制项:每个键、键排列、多页面切换;数字键盘和全键盘;最短输入、最长输入;占位字符 place holder str;密码模式;输入匹配正则表达式 Regex。
-
ListView 长内容滚动显示。排列方向、间距、对齐子内容,程序可控上下移动,事件有比如 开始滚动、正在滚动 和 结束滚动 等。
-
图片编解码。(多种图片格式解码)
- jpeg、png、gif(可控播放),webp(推荐);图片文件、图片编码二进制数据、图片解码后二进制数据直接显示;图片缓存功能(不必每次解码);图片缩放(用于自适应长宽)、旋转。
-
图片文件大小:
-
可以先尝试,图片所有自动压缩,自动选择最好的压缩率,若压缩后体积还是比较大(如大于 1MB,或者 尺寸过大(如大于屏幕分辨率的),则单独打印出来告警)。
-
图片可以尝试全 webp 格式(有官方的编解码库 libwebp),比 通常 png / gif 小很多,节省空间。
-
对于大图片,使用图片文件形式存储在机器里,解码图片来显示,并使用图片缓存机制,不必反复解码。对于很多小图标图片,可以使用解码后的数组存储,小图标数量多,不必解码占cpu(46x46的图标图片,100张,将近1MB,权衡选择)。
-
-
图片显示长宽:
-
图片显示,尽量给的图片大小放到 屏幕 UI 上不经过缩放等而直接显示就正好。
-
给设图片的API增加功能:则设置图片的 API 可以 可选的 设置 是否缩放 与 其 parent panel 大小对齐(autoFit 模式 或 fill 模式。autoFit 模式:图片缩放顶住 parent panel 边缘,确保是图片整个显示出来(如 图片 比 parent 宽 则图片两边 与 parent 对齐);fill 模式:图片缩放后不给 parent 留空,图片 比 parent 宽 则 图片上下 与 parent 的 上下对齐)。
-
-
二维码 显示。
-
视频显示。指定视频文件,开始、暂停、停止 的 控制,摄像头显示 等。
-
(可选)丰富交互动画。
可以看看 LVGL Demos Demos --- LVGL,由专业交互设计人员进行设计。
-
以上 UI 组件,可整体缩放、旋转、调整透明度等。
-
(添彩项)考虑多分辨率情况下可用。
参考
多分辨率一节。 -
(添彩项)考虑多主题。可定制 UI 主题。
参考
多套主题一节。
自构建组件
-
设置项组件。
-
步骤组件(显示工作的当前步骤,每个步骤有未到、正在进行、执行完成、执行失败等状态图标,有步骤编号,有标题和副标题,多个步骤组件排列显示当前工作进度)。
-
弹窗组件:任务完成、任务失败、信息提示、警告、错误 等。
-
Toast 组件(在屏幕中间上方,显示带背景的指定的文字,指定秒数后消失)。
-
简易图文显示。比如 使用其他库 解析 MarkDown、html 等,进行建议的多段落、标题、图片等的图文页面展示。
-
画线段(传入多个点坐标,在之间画线段,可选传入是否圆弧、圆弧弧度)。
GUI 工程 程序组件
基础软件组件、模块
-
CallRepeatChecker 按键重复点击检测:如果连续调用间隔时间小于指定时间则 不响应 或者 按照指定低频响应。
-
线程池使用,接受提交函数到内部队列进行逐一执行。
UI 刷新线程(提交 UI 操作 的函数 到 单线程 的 线程池 或者 函数队列)。
注意易出现的问题:Cpp-Learning/编程经验-规范, 调试、性能和内存检查工具集合.md at main · Staok/Cpp-Learning。
内部 Debug:
-
若 UI 刷新间隔 过长(比如大于 200ms)则打印(系统有其它进程监控 和 打印 CPU、RAM、最高占用(CPU 和 RAM)的进程 等等信息)。
-
使用线程看门狗。刷新间隔超过如 30s 则认为是发生死锁,主动抛异常 退出,产生 Coredump 进行分析。
线程看门狗。
定时器。后端周期性获取 硬件 或 其它软件模块 状态,或其它需要周期性执行的业务。
-
-
信号槽使用。
UI 前端 留 事件回调(在回调里面发射信号) 给 UI 后端(也可用于埋点,参考后文
埋点一节),并留所有 UI 元素 操作函数(可以均留为 property(槽函数、变量,直接控制 UI 显示 的 变化)) 列清晰;UI 后端 调用 UI 元素 操作函数(UI 前端留的 property)来 操作 UI。更详细的 前后端软件设计模型 描述 参考
基本 UI 开发模式一节。 -
数据抽象,类抽象和继承等,工厂模式创建和管理(可参考 C-Cpp-design-patterns);对软硬件的数据结构进行建模,设计的方便扩展和维护,方便添加新功能,方便进行操作。
使用 Device Manager 概念的类进行统一管理(也可以分多层)。
-
Task Manager(FSM .etc)。
参考 FSM 库。
-
GateWay(管理所有对外的数据通讯交互,UART、Socket、MQTT .etc)。
参考 网络库。
-
Power Manager(屏保、待机、开关机等)。
-
Logger Or User Manager。
-
System Diagnosis(自检、校准、维护提醒 等)。
-
Upgrade(升级检查、固件升级 等)。
-
Debug Utils。参考
GUI 综合性工程所需 Features一节里面列到的。 -
配置参数读写,如 settings.json 文件 读写,保存用户配置、机器信息等。
-
统一的打印信息,基本的,有 提醒(Info)、警告(Warning)、错误(Error)、调试信息(Debug)分类。
-
一般的打印信息格式:
[20XX-0X-0X-24:59:59:999] [INFO | WARN | ERROR] [<file name>] [<function name>():<line num>]: <debug str>。 -
打印调试信息的时机:
-
页面切换的时候(这种类型的信息应该设置一个开关控制是否打印)。
-
设置功用的按键按下的时候(这种类型的信息应该设置一个开关控制是否打印)。
-
每一个函数在返回错误之前视情况要打印错误信息。
-
与其它端进行消息通讯的时候,异步发送 和 异步回调 的这两个时刻都要打印信息。
-
-
-
进程间通讯中间件。DBUS、DDS、unix socket(+ protobuf 定义数据包结构 + 自定义 protocol 打包解包) 等。
UI 相关软件组件、模块
-
具体组件的管理,比如对于弹窗组件,需要搞个全局弹窗管理,就需要包括创建弹窗(传入必要信息(比如传入弹窗id(用于关闭时候引用),弹窗类型、显示文字(和图片路径)、弹窗按键数量和各个的显示文字以及绑定的函数),并且里面会判断是否先存着不显示啊、如何叠层啊之类的)、关闭弹窗等。
具体组件类型具体做。
-
多页面,页面管理。参考
页面管理一节。 -
多图层,图层管理。比如所有 UI 正常 显示在 第 1 层,而 弹窗 等 这种需要全局弹出覆盖当前内容,就在 比如 第 2 层 显示,以此类推。
支持透明图层,其它进程 可以 直接从 DRM 等图层管理 往画面添加显示。
各图层定义和管理,以及获取图层用来创建组件。
-
字体管理,字体创建、使用。
多语言支持,自动化处理。
参考 下面
多语言 / 字体 以及 事务高度自动化一节。 -
屏保。设定屏保时间,到时,先设置背光到一半,延时2秒,关闭背光,屏幕显示黑白切换(防止烧屏),有输入,恢复正常显示,设置半背光(此时还不能响应点击),延时1s后,恢复背光,恢复正常响应点击。
-
待机 / 休眠。首先进入屏保,一段时间没有输入后进入 待机 / 休眠:
-
关闭屏幕背光电源。
-
停掉 UI 渲染和刷新。
-
关闭一些外设,包括关闭屏幕外设,只保留退出休眠相关的外设。
-
处理器进入低功耗模式。
退出 待机 / 休眠 序列与上面相反即可。
-
-
UI 测试。参考 下面
UI 测试 / 自动化测试一节。 -
颜色相关实用函数。以下默认为 32 位颜色,lvgl 支持 各种其它位的颜色,以及不同位的颜色之间的转换,还有混合、变亮、变暗 等。
-
ARGB 和 RGB 相互转换。
-
从 uint32_t 的 ARGB 获得 uint8_t 的 A、R、G 和 B;以及反转换。
-
从 ARGB 或 RGB 字符串值 转 uint32_t 的 ARGB 颜色 整数值。
-
获得两个颜色的相近程度(RGB 转 XYZ 再转 Lab,使用 DeltaE76 算法得到差异值)。
-
获得一些预设的主题颜色,从 颜色名称 返回 颜色 ARGB;以及反转换;反转换,增加模糊匹配(给定一个颜色 整数值,返回最相近的颜色的颜色名称)。
-
RGB 转 HSV,以及反转换(lvgl 的 lv_color.h 有)。
-
-
单位量相关转换实用函数。
-
时间(年月日时分秒毫秒)、容量大小(B、KB、GB、TB)、重量(g、Kg)、长度(mm、m、km) 等等 的 可读化显示。
-
比如,一个文件容量大小为 123456Byte 存储在一个整数变量,写一个转换函数,转换为
xxx.x KB字符串,用于 UI 显示。
-
基本 UI 开发模式
编程设计模式总结
-
https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_(%E8%AE%A1%E7%AE%97%E6%9C%BA)
-
自总结和梳理 C-Cpp-design-patterns/DesignPattern at main · Staok/C-Cpp-design-patterns。
适用于 UI 的设计模式 MVVM
MVVM 是一种将用户界面(GUI)代码与业务逻辑和数据模型分离的架构模式。它的核心目标是实现 UI 与 逻辑的解耦。
关键组成部分:
Model (模型):
代表真实状态的数据内容(类似于领域模型)。
包含业务逻辑和数据验证。
完全不知道 ViewModel 或 View 的存在。
View (视图):
用户看到的结构、布局和外观(GUI)。
被动:它不处理业务逻辑。
通过 数据绑定 (Data Binding) 连接到 ViewModel。
将用户交互(点击、输入)转发给 ViewModel。
ViewModel (视图模型):
MVVM 的核心。它是 View 的抽象,包含 View 的状态(如"按钮是否启用")。
充当"转换器":将 Model 的数据转换为 View 可以显示的格式。
不知道 View 的具体实现(没有 UI 控件的引用),通过 通知机制 (Notification) 或 命令 (Command) 与 View 通信。
核心机制:
数据绑定 (Data Binding): 自动同步 View 和 ViewModel 之间的数据。
通知机制 (INotifyPropertyChanged): 当 ViewModel 数据改变时,自动通知 View 更新。
命令 (Commands): 处理 View 的事件(如点击),而不是在 View 的后台代码(Code-behind)中写逻辑。
可测试性: 因为 ViewModel 不依赖 UI 控件,可以对其进行纯粹的单元测试。
+----------------+ +-------------------+ +----------------+ | View | | ViewModel | | Model | | (UI / Window) | | (Logic / State) | | (Data / Rules) | +----------------+ +-------------------+ +----------------+ | | Binding | | Updates | | | [ Text Box ] <-------------> [ String Prop ] <----------> Data Field | | | | | | | | [ Label ] <-------------> [ Format() ] | | | | | Notification | | | | | [ Button ] --------------> [ Command() ] ----------> SaveToDB() | | | | | | | +----------------+ +-------------------+ +----------------+ ^ ^ ^ | | | Knows ViewModel Knows Model Only Knows Nothing (Observes changes) (Does NOT know View) (Pure Data/Logic)
MVVM 框架的实现例子
总结
(其中的 信号变量 Property 或 信号 Signal 可用 Github 开源库如 KDBindings 来做)
Model(不依赖 View 或 ViewModel)(这里当作 后端程序)
-
定义和实现功能函数接口:
uint32_t execFun()。如果是耗时任务,则应该内部设计超时机制,外部传入超时时间,超时则直接 返回 超时 Code。根据其返回值判断 执行 的 成功(0 表示成功,正数表示带条件的成功)、失败(负值,不同负值表示失败的情况,包括超时的情况)。 -
定义信号变量 Property:
Property<Mode> mode。根据业务直接赋值mode即可,下面有定义其变化的回调函数,将可直接修改 UI 显示。
ViewModel(依赖 Mode)
-
持有 Mode 实例的指针
model。 -
定义 UI 事件 的 信号 Signal,并连接到槽函数如:
Signal signalExecFun; signalExecFun.connect( [this] () { // 注意这里的 上下文 Context 为 View 中触发 signalExecFun 所在的上下文 (下面整体提交到后端执行线程池(单线程或多线程),进行异步处理) 判断各种 UI 交互逻辑。 调用 model.execFun() 根据返回值判断处理是否成功,根据结果进一步执行需求业务逻辑(比如弹窗提示等视觉显示) } );即 View -> 事件回调 -> 触发 ViewModel 信号 -> 处理 UI 逻辑 -> 调用 Model 功能函数(异步执行)。
-
定义 与 UI 显示对应的 Property(也可以为信号,槽函数就是 View 中 修改 UI 执行刷新变化的函数)
比如
Property<bool> isPanelAShow;Property<bool> isPanelBShow;。 -
设置 Model 的 Property 变化 的回调函数,如:
model.mode.onChanged( [this] (Mode mode) { // 注意这里的 上下文 Context 为 Mode 中修改 mode 所在的上下文 switch (mode) { case Mode::MODEA: isPanelAShow = true; isPanelBShow = false; break; case Mode::MODEB: isPanelAShow = false; isPanelBShow = true; break; ... default: break; } } );
View(依赖 ViewModel)
-
持有 ViewModel 实例的指针
viewModel。 -
设置事件回调函数,在里面 触发 ViewModel 的信号,如:
setEventCb( [viewModel] () { viewModel.signalExecFun.emit(); } ); -
设置 ViewModel 中修改 UI 的 Property 的变化的回调函数,如:
viewModel.isPanelAShow.onChanged( [viewModel] (bool is) { // 注意这里的 上下文 Context 为 ViewModel 中修改 isPanelAShow 所在的上下文 ui_obj_set_Visibility(panelA, is); // 注意需要在 UI 线程中执行这类语句,以此类推 } ); viewModel.isPanelBShow.onChanged( [viewModel] (bool is) { ui_obj_set_Visibility(panelB, is); } );
调用图
[ View ] [ ViewModel ] [ Model ]
| | |
(User Click) | |
| | |
Call viewModel.signalExecFun.emit() -------> | |
| (Check Logic) |
| Call model.execFun() ------------------> |
| | (Async Work)
| | <---------- (Callback/Signal) Update mode
| (On mode Changed) |
| Update isPanelAShow .etc |
(On isPanelAShow .etc Changed) <------------- | |
Update UI | |
| | |
复杂 UI 交互逻辑举例
应用 MVVM 设计模式举例。
复杂 UI 交互逻辑 场景 描述:在 屏幕 上 点击 执行格式化 SD 卡,首先检测 SD 卡是否存在,存在则屏幕弹窗提示是否执行,如果点击否则关闭弹窗,如果点击是则关闭弹窗并弹出执行中请等待的弹窗,执行完毕后关闭弹窗,相应的界面上 SD 卡 状态、容量等等均更新显示了;如果 SD 卡 不存在则弹窗表示错误,点击是则关闭弹窗。这一通逻辑可以在 前端(分为两个部分,View 和 ViewModel)里面做,后端(Model 部分) 只提供 具体的功能 / 方法 比如 检查sd 卡是否存在、执行 sd 卡格式化、弹出 sd 卡 等等。
下面是 AI 大模型给出的建议,实际方案可以调整。
在 MVVM 中,弹窗和界面的切换也是数据驱动(状态驱动) 的。我们把"是否显示弹窗"、"弹窗的内容是什么"抽象成 ViewModel 里的状态数据。
1. Model 层 (底层逻辑)
负责真实的硬件/数据操作,完全不管弹窗。
功能 :
CheckExist()返回布尔值;FormatAsync()执行异步格式化;GetCapacity()获取容量;GetStatus()获取状态。这里,实际 Model 可以搞个定时器周期性获取 SD 卡状态等信息 并直接通知 ViewModel 进行更新 UI。
2. ViewModel 层 (状态与交互核心)
维护页面的所有状态,将 UI 的动作转化为命令,并更新状态。
状态属性 (需支持通知更新):
SdCardCapacity(字符串,例如 "64GB")
SdCardStatus(字符串,例如 "正常" / "未挂载")通知全局弹窗管理进行弹窗(传入弹窗id(用于关闭时候引用),弹窗类型、显示文字(和图片路径)、弹窗按键数量和各个的显示文字以及绑定的函数)
命令 (Commands):
RequestFormatCommand(绑定到主界面的"格式化"按钮)弹窗按键的各个回调函数。
3. View 层 (纯粹的显示者)
SD 卡 的 UI 显示部分 绑定
SdCardCapacity和SdCardStatus。按钮的点击事件绑定到 ViewModel 的 Commands。
搞一个 弹窗组件,以及弹窗管理,包括创建弹窗(传入必要信息(比如传入弹窗id(用于关闭时候引用),弹窗类型、显示文字(和图片路径)、弹窗按键数量和各个的显示文字以及绑定的函数),并且里面会判断是否先存着不显示啊、如何叠层啊之类的)、关闭弹窗等。
通用控件事件处理
这里为实验性设计。
下面是通用控件事件处理程序结构,写个统一处理的函数,就是实现下面这一套。
每个用户点击需要响应的控件,如 按键、开关 / 选择框、下拉框,其 回调函数里面 应该怎么执行的信息都打包在一个 结构体 的 实例 里面,然后一个 表格、hash 等结构 管理这些 控件实例 和 其执行信息结构体的实例 的一一对应关系。
1. 前端逻辑
UI 组件配置 (分为三类):
-
按键: 属性包括
enable,checked -
开关 / 选择框: 属性包括
enbale,checked -
下拉框: 属性包括
enbale,textList,currentIndex设计为,每个组件的事件回调里面都统一调用下面的入口函数
handle_user_operate()。
执行函数:
伪代码描述:
struct ExecInfo{
check_fun
task_fun
time_out
callback_fun
execSeqConfig
};
hash<obj, ExecInfo> objExecInfos;
handle_user_operate(obj);
对 handle_user_operate() 传入控件实例的 obj 或者 id,其内部 通过 查表 objExecInfos 得到 其对应的 描述怎么执行的 结构体。
其中 check_fun、task_fun、time_out、callback_fun 几个参数在下面的执行流程中被使用,而 execSeqConfig 是个结构体,里面信息 配置了下面 所有标识 Optional 的项是否执行或跳过。
执行流程步骤:
-
CallRepeatChecker 按键重复点击检测
如果连续调用间隔时间小于指定时间则 不响应 或者 按照指定低频响应。
-
检查是否可以执行 (Optional)
使用 检查函数
check_fun执行检查,根据检查函数返回值,如果不可以执行则弹窗提示 (弹出后 return, 弹窗一个 ok 按键,点击后只关闭弹窗)。 -
是否要开始执行提示弹窗 (二次确认) (Optional)
弹窗有 yes, no 按键,yes 点击后向下执行, no 点击后 return。
-
已经开始执行提示弹窗 (Optional)
弹窗有 ok 按键,点击后只关闭弹窗。
-
弹出示意执行中的转圈弹窗 (Optional)
弹窗提示处理中请等待。
-
执行下面的部分。
2. 后端执行
下面的都提交到后端业务线程池中执行,相对于前端为异步执行、不阻塞前端执行流程。
主要逻辑流程:
-
调用后端功能函数
调用后端功能函数
task_fun(如果是耗时任务,则应该内部设计超时机制,外部传入超时时间time_out,超时则直接 返回 超时 Code),根据其返回值判断 执行 的 成功(0 表示成功,正数表示带条件的成功)、失败(负值,不同负值表示失败的情况,包括超时的情况)。 -
收尾处理
-
执行
callback_fun。 -
如果前面弹出了 示意执行中的转圈弹窗,则在这里关闭。
-
根据后端功能函数的返回值,显示结果提示弹窗 (Optional)
-
是否重试弹窗,重试次数,重试间隔,重试结果弹窗提示 (Optional)
-