车机项目中的 Widget 设计反思:从“能用”到“好用”的改进方向

车机项目中的 Widget 设计反思:从"能用"到"好用"的改进方向

背景:项目中 src/skgui/widget 目录下已有多种控件(TextBox、ProgressBar、LineChart、CarCondition 等),本意是封装通用 UI 控件给业务层调用,但随着需求推进,越来越多业务逻辑直接写进了 widget,导致后期维护成本明显上升。

本文结合当前代码,梳理现状问题,并给出几条可落地的改进方向。


一、现状问题:Widget 正在变成"小业务层"

1. 典型现象

CarCondition.cpp 为例:

  • 直接访问系统数据层:

    cpp 复制代码
    static auto pstSystemMgr = SystemManager::GetInstance();
    
    uint32 u32Num = pstSystemMgr->TIRE_FAULT_PRESSURE_FRONT_LEFT.GetValue();
    uint8 u8State = pstSystemMgr->TIRE_FAULT_FRONT_LEFT.GetValue();
  • 在 widget 内部写完整的业务判断:

    cpp 复制代码
    if (u8State == TIRE_STATUS_HIDDEN) { ... }
    else if (u8State == TIRE_STATUS_RED_WARNING) { ... }
    else { ... }
  • 对安全带、车门、三盖、胎压颜色、日夜模式的处理,都集中在 widget 内。

ProgressBar.cpp 同样如此:

  • 不同业务(油量、水温、电池、功率)通过 DealWithFuel/DealWithWaterTemperature/DealWithBattery/DealWithPower 写在一个控件里;
  • 内部有大量业务阈值(例如 87、90、负功率比例等)。

2. 带来的问题

  • 控件与业务强耦合

    Widget 本应只关心"画什么"和"怎么画",现在却要关心"什么时候报警""温度多少变色",导致很难复用。

  • 维护成本高

    改一个业务规则,需要深入控件代码修改,风险大且容易影响其他使用场景。

  • 难以测试

    Widget 一创建就依赖 SystemManagerResourceMgr 等单例,几乎无法做单元测试,只能在整车 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等),且散落在 CarConditionProgressBarMenuXXXWindowLineChart 中。

问题:

  • 同一个 UI 策略在多个地方重复实现;
  • 想统一颜色风格时,需要全局搜索,将近几十处代码逐个修改;
  • 容易出现风格不统一(有的用 60,有的用 50)的情况。

4. 大量 #if 0 历史代码与测试代码未清理

例如 CarCondition.cpp 中整套旧版 DrawLeftTop/RightTop/... 保留在 #if 0 中,ProgressBar.cppTimer() 里也有测试逻辑。

问题:

  • 影响阅读,真实生效逻辑需要在一堆废弃代码中辨认;
  • 对新同事不友好,也可能被误以为是"备用逻辑"。

三、改进方向:从 widget 到"可复用的 UI 组件"

下面是几条可以在现有工程中逐步推进的改进方向,既不过于理想化,又能明显提高长期可维护性。

改进方向一:业务逻辑上移,Widget 专注渲染

目标:

Widget 不直接访问 SystemManager 等业务数据源,不做复杂业务判断,转而通过"视图状态结构体(ViewState)"驱动渲染。

示例思路:

  1. 在 Window / 业务层中集中从 SystemManager 取值,并计算状态:

    cpp 复制代码
    struct 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;
    };
  2. Widget 提供统一渲染接口:

    cpp 复制代码
    class CarConditionWidget {
    public:
        void Render(const CarCondViewState& viewState);
    };
  3. 业务规则(如胎压阈值、门开关状态逻辑)只在构造 CarCondViewState 处维护,Widget 只负责"根据状态画图"。

收益:

  • 重用方便:其他界面需要简单车况展示时,只要构造同样的 ViewState 即可;
  • 修改业务规则时不必深入渲染层;
  • 单元测试可以直接针对 ViewState 做验证。

改进方向二:用枚举 + 常量消灭魔法数字和魔法下标

针对:CarConditionProgressBarLineChart

  1. 给图标数组配枚举索引:

    cpp 复制代码
    enum CarCondIconIndex : uint8 {
        ICON_CAR_BG = 0,
        ICON_DOOR_HOOD,
        ICON_DOOR_LF,
        ...
        ICON_TIRE_LF_NORMAL,
        ICON_TIRE_LF_ALARM,
        ...
    };

    使用时:

    cpp 复制代码
    auto& icon = gpc8Condition[ICON_TIRE_LF_NORMAL];
    pstResMgr->SetTextureXY(icon.pc8Icon, icon.x, icon.y);
  2. 坐标改为"基准点 + 偏移"的常量,而不是长算式:

    cpp 复制代码
    static 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 持有和管理

TextBoxProgressBarSliderLineChart 等基础控件定义一组"推荐接口":

  • SetPosition(sint32 x, sint32 y);
  • SetSize(sint32 w, sint32 h);
  • SetRange(...) / SetData(...) / SetValue(...)
  • OnPaint() / onDrawSelf(...)

新 widget 尽可能遵循这套约定,上层 Window 可以更容易地替换实现、复用布局逻辑。


改进方向五:逐步清理 #if 0 和测试代码

对于已经明确不会再启用的旧逻辑:

  • 将必要的"设计变化说明"保留在注释中;
  • #if 0 整块删除,减少阅读干扰;

对于有价值的测试代码:

  • 提炼为独立的测试用例(本地工具、小 demo)、或放到独立的测试宏区域,而不是和正式逻辑混在同一个文件里。

四、落地策略:从"小范围试点"开始

为了不影响当前项目节奏,可以采用"渐进式重构"策略:

  1. 新需求优先用新模式

    新增功能(如菜单动态子菜单、曲线优化、车况新图标)直接按"ViewState + 渲染"的方式设计,不再在 widget 里写业务。

  2. 改一处,顺手抽一处

    当需要修改某个 widget 的业务逻辑时,顺手把相关的状态判断提到上层,或补一个 ViewState 结构,而不是继续往 widget 塞逻辑。

  3. 选一个典型模块做"示范性重构"

    优先选择 CarCondition某个 ProgressBar 用例,做一版"只重构结构、不改 UI 效果"的样板,将前后对比写进文档,方便团队统一风格。


五、总结

目前 widget 目录里的代码整体是"能跑、能用,但边界不清"的状态:

  • 控件承担了视图 + 业务 + 数据访问 多重职责;
  • 魔法数字和枚举下标增加了理解和修改成本;
  • 日夜模式、颜色策略等横切逻辑在很多地方重复实现。

改进方向可以概括为四个关键词:

  • 解耦:业务逻辑上移,widget 专注绘制;
  • 抽象:用 ViewState、枚举常量、ThemeHelper 抽掉重复逻辑;
  • 规范 :统一控件接口、去除魔法数字和 #if 0
  • 渐进:从新功能和局部修改入手,避免大范围一次性重写。
相关推荐
Vanranrr2 小时前
表驱动编程实战:让 UI 逻辑既清晰又好维护
c++·ui
2501_941111522 小时前
C++中的适配器模式
开发语言·c++·算法
2501_941111942 小时前
C++中的适配器模式变体
开发语言·c++·算法
纯爱掌门人2 小时前
别再死磕框架了!你的技术路线图该更新了
前端·架构·前端框架
2501_941111772 小时前
C++代码移植性设计
开发语言·c++·算法
kfyty7253 小时前
loveqq 作为网关框架时如何修改请求体 / 响应体,和 spring 又有什么区别?
后端·架构
yy_xzz3 小时前
【OpenCV + VS】C++实现动态下雪特效
c++·人工智能·opencv
橘子真甜~3 小时前
C/C++ Linux网络编程5 - 网络IO模型与select解决客户端并发连接问题
linux·运维·服务器·c语言·开发语言·网络·c++
2501_941111464 小时前
C++中的原型模式
开发语言·c++·算法