车机项目中的 Widget 设计反思:从"能用"到"好用"的改进方向
背景:项目中
src/skgui/widget目录下已有多种控件(TextBox、ProgressBar、LineChart、CarCondition 等),本意是封装通用 UI 控件给业务层调用,但随着需求推进,越来越多业务逻辑直接写进了 widget,导致后期维护成本明显上升。
本文结合当前代码,梳理现状问题,并给出几条可落地的改进方向。
一、现状问题:Widget 正在变成"小业务层"
1. 典型现象
以 CarCondition.cpp 为例:
-
直接访问系统数据层:
cppstatic auto pstSystemMgr = SystemManager::GetInstance(); uint32 u32Num = pstSystemMgr->TIRE_FAULT_PRESSURE_FRONT_LEFT.GetValue(); uint8 u8State = pstSystemMgr->TIRE_FAULT_FRONT_LEFT.GetValue(); -
在 widget 内部写完整的业务判断:
cppif (u8State == TIRE_STATUS_HIDDEN) { ... } else if (u8State == TIRE_STATUS_RED_WARNING) { ... } else { ... } -
对安全带、车门、三盖、胎压颜色、日夜模式的处理,都集中在 widget 内。
ProgressBar.cpp 同样如此:
- 不同业务(油量、水温、电池、功率)通过
DealWithFuel/DealWithWaterTemperature/DealWithBattery/DealWithPower写在一个控件里; - 内部有大量业务阈值(例如 87、90、负功率比例等)。
2. 带来的问题
-
控件与业务强耦合 :
Widget 本应只关心"画什么"和"怎么画",现在却要关心"什么时候报警""温度多少变色",导致很难复用。
-
维护成本高 :
改一个业务规则,需要深入控件代码修改,风险大且容易影响其他使用场景。
-
难以测试 :
Widget 一创建就依赖
SystemManager、ResourceMgr等单例,几乎无法做单元测试,只能在整车 UI 上联调。 -
职责边界模糊 :
Window 层、Widget 层、数据层之间没有清晰分工,"谁算逻辑、谁只负责渲染"边界不清楚。
二、代码层面的具体问题
1. 魔法数字泛滥,可读性和可维护性差
CarCondition.cpp 中大量硬编码坐标:
cpp
{1424+40-8-21-10, 144+48-42}, // 左上
{1424+306-8-21-10, 144+48-42}, // 右上
ProgressBar.cpp / LineChart.cpp 也存在类似问题(匿名数字阈值、固定宽度、高度等)。
问题:
- 难以看出这些数字的业务含义(基准点?偏移?对齐修正?);
- UI 调整 2 像素,需要去改一堆表达式,极易出错;
- 新同事接手基本只能"硬背或重新算"。
2. 数组下标使用枚举缺失,含义依赖注释和记忆
CarCondition.cpp 中的 gpc8Condition[]:
cpp
{1532, 166, "tier_nor"}, // 7 左前
{1532, 314, "tier_nor"}, // 8 左后
{1620, 166, "tier_nor"}, // 9 右前
{1620, 314, "tier_nor"}, // 10 右后
...
{1532, 166, "tier_alarm"}, // 28 左前
{1532, 314, "tier_alarm"}, // 29 左后
{1620, 166, "tier_alarm"}, // 30 右前
{1620, 314, "tier_alarm"}, // 31 右后
使用时通过数字索引访问:
cpp
pstResMgr->SetTextureXY(gpc8Condition[28].pc8Icon, ...); // 左前报警
pstResMgr->SetTextureXY(gpc8Condition[7].pc8Icon, ...); // 左前正常
问题:
- 下标全靠注释和记忆,稍不留神就写错;
- 中间插入新图标或调整顺序,所有使用点都要小心同步更新;
- Debug 时很难根据数字反推是哪个图标。
3. 日夜模式 & 颜色策略到处复制
在多个文件中都能看到类似写法:
cpp
uint8 u8Daymode = SystemInfo::GetInstance()->GetDayMode();
uint32 color = (u8Daymode == 0) ? SKUI_COLOR_COMMON_LIGHT_BLACK : SKUI_COLOR_COMMON_NIGHT_WHITE;
甚至带不同透明度版本(90、60等),且散落在 CarCondition、ProgressBar、MenuXXXWindow、LineChart 中。
问题:
- 同一个 UI 策略在多个地方重复实现;
- 想统一颜色风格时,需要全局搜索,将近几十处代码逐个修改;
- 容易出现风格不统一(有的用 60,有的用 50)的情况。
4. 大量 #if 0 历史代码与测试代码未清理
例如 CarCondition.cpp 中整套旧版 DrawLeftTop/RightTop/... 保留在 #if 0 中,ProgressBar.cpp 的 Timer() 里也有测试逻辑。
问题:
- 影响阅读,真实生效逻辑需要在一堆废弃代码中辨认;
- 对新同事不友好,也可能被误以为是"备用逻辑"。
三、改进方向:从 widget 到"可复用的 UI 组件"
下面是几条可以在现有工程中逐步推进的改进方向,既不过于理想化,又能明显提高长期可维护性。
改进方向一:业务逻辑上移,Widget 专注渲染
目标:
Widget 不直接访问 SystemManager 等业务数据源,不做复杂业务判断,转而通过"视图状态结构体(ViewState)"驱动渲染。
示例思路:
-
在 Window / 业务层中集中从
SystemManager取值,并计算状态:cppstruct TireViewState { uint32 pressure; TireStatus status; // 正常 / 警告 / 隐藏 bool fault; // 是否有故障图标 }; struct CarCondViewState { TireViewState tires[4]; bool doors[4]; bool hoodOpen; bool trunkOpen; bool tankOpen; SeatBeltState driver; SeatBeltState passenger; }; -
Widget 提供统一渲染接口:
cppclass CarConditionWidget { public: void Render(const CarCondViewState& viewState); }; -
业务规则(如胎压阈值、门开关状态逻辑)只在构造
CarCondViewState处维护,Widget 只负责"根据状态画图"。
收益:
- 重用方便:其他界面需要简单车况展示时,只要构造同样的
ViewState即可; - 修改业务规则时不必深入渲染层;
- 单元测试可以直接针对 ViewState 做验证。
改进方向二:用枚举 + 常量消灭魔法数字和魔法下标
针对:CarCondition、ProgressBar、LineChart 等
-
给图标数组配枚举索引:
cppenum CarCondIconIndex : uint8 { ICON_CAR_BG = 0, ICON_DOOR_HOOD, ICON_DOOR_LF, ... ICON_TIRE_LF_NORMAL, ICON_TIRE_LF_ALARM, ... };使用时:
cppauto& icon = gpc8Condition[ICON_TIRE_LF_NORMAL]; pstResMgr->SetTextureXY(icon.pc8Icon, icon.x, icon.y); -
坐标改为"基准点 + 偏移"的常量,而不是长算式:
cppstatic const uint32 CAR_BASE_X = 1424U; static const uint32 CAR_BASE_Y = 144U; static const uint32 TIRE_OFFSET_X_LEFT = 40U; static const uint32 TIRE_OFFSET_X_RIGHT = 306U; static const uint32 TIRE_OFFSET_Y_TOP = 48U; static const uint32 TIRE_OFFSET_Y_BOTTOM= 216U;这样比
1424+40-8-21-10更容易理解和调整。
改进方向三:抽公共 Theme Helper,统一日夜模式颜色策略
可以在类似 MenuDisplayHelper 的模块里增加主题相关 helper:
cpp
namespace ThemeHelper {
uint32 GetTitleColor(uint8 daymode);
uint32 GetSubTitleColor(uint8 daymode);
uint32 GetProgressBarBgColor(uint8 daymode);
uint32 GetTireNormalColor(uint8 daymode);
}
以后 widget / Window 不直接访问 SystemInfo::GetDayMode() + 硬编码颜色,而是调用这些 helper。
当 UI 设计改颜色风格时,只需要改 helper 实现。
改进方向四:统一基础 widget 接口,便于 Window 持有和管理
为 TextBox、ProgressBar、Slider、LineChart 等基础控件定义一组"推荐接口":
SetPosition(sint32 x, sint32 y);SetSize(sint32 w, sint32 h);SetRange(...)/SetData(...)/SetValue(...)OnPaint()/onDrawSelf(...)
新 widget 尽可能遵循这套约定,上层 Window 可以更容易地替换实现、复用布局逻辑。
改进方向五:逐步清理 #if 0 和测试代码
对于已经明确不会再启用的旧逻辑:
- 将必要的"设计变化说明"保留在注释中;
#if 0整块删除,减少阅读干扰;
对于有价值的测试代码:
- 提炼为独立的测试用例(本地工具、小 demo)、或放到独立的测试宏区域,而不是和正式逻辑混在同一个文件里。
四、落地策略:从"小范围试点"开始
为了不影响当前项目节奏,可以采用"渐进式重构"策略:
-
新需求优先用新模式 :
新增功能(如菜单动态子菜单、曲线优化、车况新图标)直接按"ViewState + 渲染"的方式设计,不再在 widget 里写业务。
-
改一处,顺手抽一处 :
当需要修改某个 widget 的业务逻辑时,顺手把相关的状态判断提到上层,或补一个
ViewState结构,而不是继续往 widget 塞逻辑。 -
选一个典型模块做"示范性重构" :
优先选择 CarCondition 或 某个 ProgressBar 用例,做一版"只重构结构、不改 UI 效果"的样板,将前后对比写进文档,方便团队统一风格。
五、总结
目前 widget 目录里的代码整体是"能跑、能用,但边界不清"的状态:
- 控件承担了视图 + 业务 + 数据访问 多重职责;
- 魔法数字和枚举下标增加了理解和修改成本;
- 日夜模式、颜色策略等横切逻辑在很多地方重复实现。
改进方向可以概括为四个关键词:
- 解耦:业务逻辑上移,widget 专注绘制;
- 抽象:用 ViewState、枚举常量、ThemeHelper 抽掉重复逻辑;
- 规范 :统一控件接口、去除魔法数字和
#if 0; - 渐进:从新功能和局部修改入手,避免大范围一次性重写。