IDE:CLion

简介

        最近使用lvgl开发代码时,觉得C语言表现力不够,创建组件的流程有些麻烦,于是打算使用C++的元编程+CRTP模式封装了一些常用的API操作。在链式调用、引用、重载和缺省等功能特性下,使用确实方便了许多,尤其是事件绑定那块,因为GUI Guider的代码编辑器直到1.9.0似乎还无法保存。

        不过坏处也很明显,不能可视化设计界面,毕竟GUI Guider只支持生成lvgl代码。这显然会大大影响开发效率,即使在工程里嵌入一个模拟器。

        于是转念一想,可以写一个脚本来把GUI Guider生成的初始化代码转为自己封装的代码,既能蹭GUI Guider可视化设计界面的便捷,也能享受C++的便捷,除了写脚本时很痛苦外(x_x)

        具体实现效果,可以翻到 3,实际效果

一、元编程+CRTP封装

1,封装的目的

        在LVGL开发中,C语言冗长的初始化代码和事件回调处理显著影响开发效率。例如创建一个按钮需要:

lv_obj_t* btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 100, 50);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);

        而C++封装后的目标形态是:

btn.init().size(100,50).center();

        配合现在IDE(如CLion、VS Code)的代码提示和AI插件的代码补全后,写代码异常的方便。

        即使是代码不多,但自己封装的API更易懂,比如下面是lvgl的事件绑定代码:

// 切换模式
void signalGenerator_switch_mode_event_cb(lv_event_t *e)
{
    lv_obj_t *obj = lv_event_get_target(e);
     /**_**/
}


// 调用处
lv_obj_add_event_cb(btn_mode,signalGenerator_switch_mode_event_cb,LV_EVENT_CLICKED, nullptr);

        使用C++封装后:

class SignalGenerator
{
public:
    // 切换模式
    static inline auto switch_mode() -> void
    {
        lv_obj_t *obj = lv_event_get_target(e);
         /**_**/
    }
};

// 调用处
btn_mode.OnClicked<SignalGenerator::switch_mode>();

        还比如:

    void events()
    {
        // 创建定时器
        tick_timer.create([](Timer_t)
                          {
                              SignalGenerator::handler();
                              SignalGenerator::print_tick();
                          }, Freq_8K);

        // 绑定播放事件
        imgbtn_play.OnPressedReleased<SignalGenerator::start, SignalGenerator::stop>();

        // 绑定频率事件
        btn_freq.OnClicked<SignalGenerator::switch_freq>();

        // 绑定偏置事件
        btn_bias_sub.OnClicked<SignalGenerator::sub_bias>();
        btn_bias_add.OnClicked<SignalGenerator::add_bias>();

        // 绑定占比事件
        btn_ratio_add.OnClicked<SignalGenerator::add_ratio>();
        btn_ratio_sub.OnClicked<SignalGenerator::sub_ratio>();

        // 绑定模式事件
        btn_mode.OnClicked<SignalGenerator::switch_mode>();
    }

        当然,说了这么多并不是贬低C而抬高C++,而是不同编程语言用不同的应用场景。写业务代码,更高抽象层次的C++更适合。不过涉及底层还是C更好,无他,C的abi更稳定,兼容性也远远好于C++。C++的abi一言难尽,不同厂商的编译器编译的abi不同,即使同一个厂商也要注意编译器的版本,不能太高也不能太低……从这来看,使用C的lvgl再合适不过了。

2,CRTP

简介

        CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)是C++中实现编译期多态性的经典设计模式,通过模板继承实现静态多态,并实现链式调用(返回的是派生类对象的引用):

// 基类模板声明 
template <typename Derived>
class Base {
    // 通过static_cast访问派生类 
    Derived& derived() { 
        return static_cast<Derived&>(*this);
    }
};
 
// 派生类继承时将自身作为模板参数 
class MyClass : public Base<MyClass> { 
    // 实现具体功能 
};

于此,可以实现:

  1. 链式调用支持:通过返回Derived&类型(派生类引用类型),使得size()等配置方法可以连续调用
  2. 零成本抽象:所有类型转换在编译期完成,不产生运行时开销

        不过毕竟用到了模板和类,编译生成后,二进制文件的ROM可能会增加,RAM几乎没变化。

成员变量

        此外还需要注意的是,由于是模板基类,如果定义静态成员,那么模板会在每个派生类里都会实例化一份静态成员变量。解决办法简单,可以再定义一个类,专门存放成员变量:

class WidgetModel
{
protected:
    Obj_t obj_;
    // 方便获取父对象,因为一般而言定义多个组件时往往只有一个公共父对象
    static inline Obj_t parent_ = nullptr;
};


template<typename Derived>
class Widget : public WidgetModel
{
public:
    Widget() = default;
    ~Widget() = default;
}

        这里Obj_t是使用using后lv_obj_t*的别名,后文出现的其他变量类型也是如此。同时,如注释所示,定义了一个parent的静态变量是为了存储父对象,因为在初始化代码中,一般组件的父对象都是当前屏幕组件,并不需要频繁切换。

显示创建

        出于实际考虑,一般在单片机开发中,全局变量用得很多,像这样的对象实例化基本会以全局变量的形式出现。如果把lv对象的创建放在构造函数里,那就会出现一个问题,全局变量定义时会自动调用构造函数(在进入main之前),而lvgl的初始化一般是在main里进行的,这可能会导致一些严重问题。

        因此需要确保lv对象的创建必须在lvgl初始化之后,这就要求把lv对象的创建显示封装到一个普通成员函数里。

class Component : public Widget<Component>
{
public:

    // 不主动设置父对象,那么就默认为上一次调用parent函数设置的父对象
    inline Component &init(Obj_t parent = parent_)
    {
        obj_ = lv_obj_create(parent);
        return *this;
    }



};

        这里使用了C++的缺省(默认值),确保该初始化函数既可以使用默认父对象,也能从函数形参里接受。此外要注意,在现代编译器中,加入inline只是建议编译器内联,具体还是看编译器的决定。

        可以添加下面编译标志,确保编译器不打算内联时会发出警告,以便及时解决

# 开启内联警告,当函数内联失败时,编译器会发出警告。
add_compile_options(-Winline)

隐式转换

        为了与原有lvgl对象兼容,这里可以让类对象隐式转换为lvgl对象,可以直接把对象当成lvgl对象使用

template<typename Derived>
class Widget : public WidgetModel
{
public:
    Widget() = default;// 删除构造函数,避免在初始化lvgl之前创建lvgl对象
    ~Widget() = default;

    // 隐式转换为原生对象
    operator Obj_t() const noexcept { return obj_; }

}

        于此,可以确保当前体系与lvgl的API兼容,如果类的API不够丰富,可以使用lvgl暂替

Component comp.init();

// 隐式转换
lv_obj_set_size(comp,20,20);

lv_obj_t* temp = comp;

        考虑到直接重载“=”,让类对象可以被lvgl对象赋值有些过于危险了,就舍弃掉了

    // 没必要且极度危险!!
//    // 重载=运算符,相当于将原生对象赋值给组件对象
//    Derived &operator=(Obj_t obj)noexcept
//    {
//        obj_ = obj;
//        return static_cast<Derived &>(*this);
//    }


// 重载“=”后,类对象可以直接被lvgl_obj_t*赋值
Component comp = lv_obj_create(nullptr);

重载

        重载,以适应更多情况

template<typename Derived>
class Widget : public WidgetModel
{
public:
    Widget() = default;// 删除构造函数,避免在初始化lvgl之前创建lvgl对象
    ~Widget() = default;

    // 隐式转换为原生对象
    operator Obj_t() const noexcept { return obj_; }

    // 返回的是该组件的父对象(不是parent_,还没来得及改)
    Obj get_parent() const noexcept { return parent_; }

    // 设置父对象
    static void parent(Obj parent)
    {
        parent_ = parent;
    }

    static void parent(Obj_t parent)
    {
        parent_ = parent;
    }
}

模板函数

        可以添加一些模板函数,这里使用了模板参数和函数形参来传递外部函数,是因为lambda不支持通过模板参数传入函数内

    template<Event_Handler handler = nullptr, EventCode Event = LV_EVENT_ALL>
    inline Derived &bind_event(Event_Handler lambda_handler = nullptr)
    {
        if constexpr (handler != nullptr)
            lv_obj_add_event_cb(obj_, handler, Event, nullptr);
        else
        {
            lv_obj_add_event_cb(obj_, lambda_handler, Event, nullptr);
        }
        return static_cast<Derived &>(*this);
    }


    // 绑定点击事件
    template<void(*handler)() = nullptr>
    inline Derived &OnClicked(Event_Handler lambda_handler = nullptr)
    {

        if constexpr (handler != nullptr)
        {
            bind_event<[](Event_t e)
            {
                if (lv_event_get_code(e) == LV_EVENT_CLICKED)
                {
                    handler();
                }
            }, LV_EVENT_CLICKED>();
        }
        else
        {
            bind_event<nullptr, LV_EVENT_CLICKED>(lambda_handler);
        }

        return static_cast<Derived &>(*this);
    }

3,继承与派生

        前面已经通过一些特性来创建构建模板基类,后面的一些函数封装也是如此。接下来可以构建派生类。

        比如Label类,除了一些基本lvgl的api被基类封装外,在Label类中,也可以定义“专用api”。如此一来,使用代码提示时,一些对象不能使用的api,比如lv_label_set_text,就不会被别的lvgl对象滥用

        当然,下面封装的一些lvgl的api有些并非是label这个组件专用的,比如lv_obj_set_style_*这样的样式设置函数

class Label : public Widget<Label>
{
public:
    /**
     * @brief 全局设置字体
     * @param font 使用引用类型,在函数内部取地址,避免还要多写一个&符号
     */
    static inline void Font(Font_t &font)
    {
        font_ = &font;
    }

    // 设置全局字体
    static inline void Font(::Font font)
    {
        font_ = font;
    }


    // 初始化文本框,默认为黑色字体
    inline Label &init(Obj_t parent = parent_)
    {
        create_obj(&lv_label_class, parent);
        Label::text_color(lv_color_black());// 设置文本颜色为黑色
        return *this;
    }

    // 只要设置文本,就默认为黑色字体
    inline Label &init(Strings text, Obj parent = parent_)
    {
        init(parent);
        // 初始化文本内容
        lv_label_set_text(obj_, text);
        if (font_)
            lv_obj_set_style_text_font(obj_, font_, selector_default);
        return *this;
    }


    // 文本颜色默认为黑
    inline Label &init(Coord x, Coord y, Coord w, Coord h, Obj parent = parent_)
    {
        init(parent);
        pos_size(x, y, w, h);
        return *this;
    }

    inline Label &init(Coord x, Coord y, Coord w, Coord h, Strings string, Obj parent = parent_)
    {
        init(string, parent);
        pos_size(x, y, w, h);
        return *this;
    }


    // 设置字间距
    inline Label &space(Coord space, Selector selector = selector_default)
    {
        lv_obj_set_style_text_letter_space(obj_, space, selector);
        return *this;
    }
    
    // 设置文本对齐方式
    inline Label &text_align(Align_text align, Selector selector = selector_default)
    {
        lv_obj_set_style_text_align(obj_, align, selector);
        return *this;
    }

    // 设置字体
    inline Label &font(::Font font)
    {
        lv_obj_set_style_text_font(obj_, font, selector_default);
        return *this;
    }

    inline Label &font(Font_t &font)
    {
        lv_obj_set_style_text_font(obj_, &font, selector_default);
        return *this;
    }


    /**
     * @brief 设置文本
     * @param fmt
     * @param ...
     * @note 不使用auto来简化,因为会导致二进制膨胀
     * @return
     */
    Label &text(Strings fmt, ...)
    {
        va_list args;
        va_start(args, fmt);
        lv_label_set_text_fmt(obj_, fmt, args);
        va_end(args);
        return *this;
    }

private:
    static inline ::Font font_{};// 避免别名与本地函数名冲突
};

 4,代码

        于此,提供一些基类和派生类的代码供参考(个别函数注意甄别)

// widgets.hpp

//
// Created by fairy on 2025/2/9 13:37.
//
#ifndef SIMULATOR_WIDGETS_HPP
#define SIMULATOR_WIDGETS_HPP
// 头文件
#include "lvgl.h"

// 定义别名
using Obj = lv_obj_t *&;// 组件对象(用于给函数传参)
using Obj_t = lv_obj_t *;// 组件对象(用于定义组件)
using Coord = lv_coord_t;// 坐标
using Color = lv_color_t;// 颜色
using Selector = lv_style_selector_t;
using Align_text = lv_text_align_t;// 对齐类型
using Align = lv_align_t;
using Flag_t = lv_obj_flag_t;
using Grad_dir = lv_grad_dir_t;
using Border_side = lv_border_side_t;
using Font = const lv_font_t *;// 字体类型
using Font_t = const lv_font_t;
using Strings = const char *;// 字符串
using ImageSrc = const lv_img_dsc_t *;// 图片资源
using ImageSrc_t = const lv_img_dsc_t;
using ChartAxis = lv_chart_axis_t;// 图表坐标
using ChartSeries = lv_chart_series_t *&;
using ChartSeries_t = lv_chart_series_t *;
using Scrollbar_mode =lv_scrollbar_mode_t;
using ImageButton_State = lv_imagebutton_state_t;
using ChartType = lv_chart_type_t;
using AnimExecCallback = lv_anim_exec_xcb_t;// 动画执行事件回调
using AnimCompletedCallback = lv_anim_completed_cb_t;// 动画执行完毕事件回调
using Anim = lv_anim_t &;
using Anim_t = lv_anim_t;
using AnimPath = lv_anim_path_cb_t;


// 色彩
#define Color_Firefly_Green  lv_color_make(133, 238, 223)// 流萤绿
#define Color_Firefly_Gold   lv_color_make(239, 221, 121)// 流萤金


// 定义常量
constexpr const Selector selector_default = (static_cast<uint32_t >(LV_PART_MAIN) |
                                             static_cast<uint32_t >(LV_STATE_DEFAULT));
constexpr const Selector selector_ticks = (static_cast<uint32_t >(LV_STATE_DEFAULT));


using Event_t = lv_event_t *;
using EventCode = lv_event_code_t;
using Event_Handler = lv_event_cb_t;
using Timer_t = lv_timer_t *;


/**
 * @brief 控件基类的数据模型,为了防止实例化多个静态变量而设计的
 */
class WidgetModel
{
protected:
    Obj_t obj_;
    // 方便获取父对象,因为一般而言定义多个组件时往往只有一个公共父对象
    static inline Obj_t parent_ = nullptr;
};


/**
 * @brief 控件基类,采用元编程+CRTP模式
 * @tparam Derived
 */
template<typename Derived>
class Widget : public WidgetModel
{
public:
    Widget() = default;// 删除构造函数,避免在初始化lvgl之前创建lvgl对象
    ~Widget() = default;

    // 隐式转换为原生对象
    operator Obj_t() const noexcept { return obj_; }

    // 返回的是该组件的父对象(不是parent_)
    Obj get_parent() const noexcept { return parent_; }

    // 设置父对象
    static void parent(Obj parent)
    {
        parent_ = parent;
    }

    static void parent(Obj_t parent)
    {
        parent_ = parent;
    }

    // 基础属性链式方法
    inline Derived &size(Coord w, Coord h)
    {
        lv_obj_set_size(obj_, w, h);
        return static_cast<Derived &>(*this);
    }

    inline Derived &pos(Coord x, Coord y)
    {
        lv_obj_set_pos(obj_, x, y);
        return static_cast<Derived &>(*this);
    }

    inline Derived &width(int32_t w)
    {
        lv_obj_set_width(obj_, w);
        return static_cast<Derived &>(*this);
    }

    inline Derived &pos_size(Coord x, Coord y, Coord w, Coord h)
    {
        pos(x, y);
        size(w, h);
        return static_cast<Derived &>(*this);
    }

    // 设置样式大小
    inline Derived &style_size(Coord w, Coord h, Selector selector = selector_default)
    {
        lv_obj_set_style_size(obj_, w, h, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置背景颜色
    inline Derived &bg_color(Color color, Selector selector = selector_default)
    {
        lv_obj_set_style_bg_color(obj_, color, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置背景透明度
    inline Derived &bg_opa(uint8_t opa, Selector selector = selector_default)
    {
        lv_obj_set_style_bg_opa(obj_, opa, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置背景渐变颜色
    inline Derived &bg_grad_color(Color color, Selector selector = selector_default)
    {
        lv_obj_set_style_bg_grad_color(obj_, color, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置背景渐变透明度
    inline Derived &bg_grad_opa(uint8_t opa, Selector selector = selector_default)
    {
        lv_obj_set_style_bg_grad_opa(obj_, opa, selector);
        return static_cast<Derived &>(*this);
    }
    // 设置背景主停止点
    inline Derived &bg_main_stop(Coord start, Selector selector = selector_default)
    {
        lv_obj_set_style_bg_main_stop(obj_, start, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置背景渐变停止点
    inline Derived &bg_grad_stop(Coord stop, Selector selector = selector_default)
    {
        lv_obj_set_style_bg_grad_stop(obj_, stop, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置背景渐变方向
    inline Derived &bg_grad_dir(Grad_dir dir, Selector selector = selector_default)
    {
        lv_obj_set_style_bg_grad_dir(obj_, dir, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置边框宽度
    inline Derived &border_width(Coord w, Selector selector = selector_default)
    {
        lv_obj_set_style_border_width(obj_, w, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置边框颜色
    inline Derived &border_color(Color color, Selector selector = selector_default)
    {
        lv_obj_set_style_border_color(obj_, color, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置边框透明度
    inline Derived &border_opa(uint8_t opa, Selector selector = selector_default)
    {
        lv_obj_set_style_border_opa(obj_, opa, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置边框侧
    inline Derived &border_side(Border_side side, Selector selector = selector_default)
    {
        lv_obj_set_style_border_side(obj_, side, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置边框圆角半径
    inline Derived &border_radius(Coord radius, Selector selector = selector_default)
    {
        lv_obj_set_style_radius(obj_, radius, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置线条宽度
    inline Derived &line_width(Coord w, Selector selector = selector_default)
    {
        lv_obj_set_style_border_width(obj_, w, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置阴影宽度
    inline Derived &shadow_width(Coord w, Selector selector = selector_default)
    {
        lv_obj_set_style_shadow_width(obj_, w, selector);
        return static_cast<Derived &>(*this);
    }

    inline Derived& image_recolor_opa(uint8_t opa=0, Selector selector = selector_default)
    {
        lv_obj_set_style_image_recolor_opa(obj_, opa, selector);
        return static_cast<Derived &>(*this);
    }

    inline Derived& image_opa(uint8_t opa=0, Selector selector = selector_default)
    {
        lv_obj_set_style_image_opa(obj_, opa, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置字间距
    inline Derived &letter_space(Coord space, Selector selector = selector_default)
    {
        lv_obj_set_style_text_letter_space(obj_, space, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置字体颜色
    inline Derived &text_color(Color color, Selector selector = selector_default)
    {
        lv_obj_set_style_text_color(obj_, color, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置字体格式
    inline Derived &font(::Font font, Selector selector = selector_default)
    {
        lv_obj_set_style_text_font(obj_, font, selector);
        return static_cast<Derived &>(*this);
    }

    // 设置字体透明度
    inline Derived &text_opa(uint8_t opa, Selector selector = selector_default)
    {
        lv_obj_set_style_text_opa(obj_, opa, selector);
        return static_cast<Derived &>(*this);
    }

    /**
    * @brief 设置背景渐变
    * @param color 背景起始颜色
    * @param grad_color 背景渐变颜色
    * @param opa 背景透明度
    * @param grad_dir 渐变方向
    */
    inline Derived &
    bg_grad(Color color, Color grad_color, uint8_t opa, Grad_dir grad_dir, Selector selector = selector_default)
    {
        bg_color(color, selector);
        bg_opa(opa, selector);
        bg_grad_dir(grad_dir, selector);// 设置垂直渐变
        bg_grad_color(grad_color, selector);// 设置渐变结束颜色
        return static_cast<Derived &>(*this);
    }

    /**
    * @brief 设置边框样式
    * @param color   边框颜色
    * @param radius  边框圆角半径
    * @param width   边框宽度
    * @param opa     边框透明度
    */
    inline Derived &
    border(Color color, Coord radius, Coord width, uint8_t opa = 255, Selector selector = selector_default)
    {
        border_color(color, selector);
        border_radius(radius, selector);
        border_width(width, selector);
        border_opa(opa, selector);
        return static_cast<Derived &>(*this);
    }

    // 将对象移动到背景层
    inline Derived &to_background()
    {
        lv_obj_move_background(obj_);
        return static_cast<Derived &>(*this);
    }

    // 将对象移动到前景层
    inline Derived &to_foreground()
    {
        lv_obj_move_foreground(obj_);
        return static_cast<Derived &>(*this);
    }

    // 高级布局方法
    inline Derived &align(Align align, Coord x_ofs = 0,
                          Coord y_ofs = 0)
    {
        lv_obj_align(obj_, align, x_ofs, y_ofs);
        return static_cast<Derived &>(*this);
    }

    // 对象居中
    inline Derived &center(Coord x_ofs = 0, Coord y_ofs = 0)
    {
        lv_obj_align(obj_, LV_ALIGN_CENTER, x_ofs, y_ofs);
        return static_cast<Derived &>(*this);
    }

    // 设置半径
    inline Derived &radius(Coord radius, Selector selector = selector_default)
    {
        lv_obj_set_style_radius(obj_, radius, selector);
        return static_cast<Derived &>(*this);
    }

    inline Derived &pad_all(int32_t pad, Selector selector = selector_default)
    {
        lv_obj_set_style_pad_all(obj_, pad, selector);
        return static_cast<Derived  &>(*this);
    }

    // 样式系统(支持普通样式和本地样式)
    template<typename Style>
    inline Derived &add_style(Style &&style, Selector selector = selector_default)
    {
        lv_obj_add_style(obj_, style.get(), selector);
        return static_cast<Derived &>(*this);
    }

    inline Derived &remove_style_all()
    {
        lv_obj_remove_style_all(obj_);
        return static_cast<Derived &>(*this);
    }

    // 圆角裁剪
    inline Derived &clip_corner(Selector selector = selector_default)
    {
        lv_obj_set_style_clip_corner(obj_, true, selector);
        return static_cast<Derived &>(*this);
    }

    // 滚动条
    inline Derived &scrollbar_mode(Scrollbar_mode mode)
    {
        lv_obj_set_scrollbar_mode(obj_, mode);
        return static_cast<Derived &>(*this);
    }


    // 重绘
    inline Derived &invalidate()
    {
        lv_obj_invalidate(obj_);  // 使频谱区域无效,触发重绘
        return static_cast<Derived &>(*this);
    };


    /**
     * @brief 绑定事件
     * @tparam handler 给默认参数nullptr是为了防止刚写就报错
     * @tparam Event 事件类型,默认为全部
     * @note 出于大多数情况的考虑,函数一般都是定义好的,所以用模板参数来传递
     * @return
     */
    template<Event_Handler handler = nullptr, EventCode Event = LV_EVENT_ALL>
    inline Derived &bind_event(Event_Handler lambda_handler = nullptr)
    {
        if constexpr (handler != nullptr)
            lv_obj_add_event_cb(obj_, handler, Event, nullptr);
        else
        {
            lv_obj_add_event_cb(obj_, lambda_handler, Event, nullptr);
        }
        return static_cast<Derived &>(*this);
    }

    /**
     * @brief 移除事件
     * @param handler 即当初绑定的事件回调函数
     * @return
     */
    inline Derived &remove_event(Event_Handler &handler)
    {
        lv_obj_remove_event_cb(obj_, handler);
        return static_cast<Derived &>(*this);
    }

    /**
     * @brief 移除事件
     * @tparam index 事件的索引
     * @return
     */
    template<uint32_t Index>
    inline Derived &remove_event()
    {
        lv_obj_remove_event(obj_, Index);
        return static_cast<Derived &>(*this);
    }


    // 发送事件
    template<EventCode Event, void *param = nullptr>
    inline Derived &send_event()
    {
        lv_obj_send_event(obj_, Event, param);
        return static_cast<Derived &>(*this);
    }

    // 绑定点击事件
    template<void(*handler)() = nullptr>
    inline Derived &OnClicked(Event_Handler lambda_handler = nullptr)
    {

        if constexpr (handler != nullptr)
        {
            bind_event<[](Event_t e)
            {
                if (lv_event_get_code(e) == LV_EVENT_CLICKED)
                {
                    handler();
                }
            }, LV_EVENT_CLICKED>();
        }
        else
        {
            bind_event<nullptr, LV_EVENT_CLICKED>(lambda_handler);
        }

        return static_cast<Derived &>(*this);
    }

    template<void(*handler)(Event_t e) = nullptr>
    inline Derived &OnClicked()
    {
        bind_event<handler, LV_EVENT_CLICKED>();
        return static_cast<Derived &>(*this);
    }


    // 持续按压事件绑定
    template<void(*press)() = nullptr>
    inline Derived &OnPressed()
    {
        bind_event<[](Event_t e)
        {
            if (lv_event_get_code(e) == LV_EVENT_PRESSED)
            {
                press();
            }
        }, LV_EVENT_PRESSED>();

        return static_cast<Derived &>(*this);
    }

    template<void(*pressing)() = nullptr>
    inline Derived &OnPressing()
    {
        bind_event<[](Event_t e)
        {
            if (lv_event_get_code(e) == LV_EVENT_PRESSING)
            {
                pressing();
            }
        }, LV_EVENT_PRESSING>();

        return static_cast<Derived &>(*this);
    }

    template<void(*press)() = nullptr, void(*release)() = nullptr>
    inline Derived &OnPressedReleased()
    {
        bind_event<[](Event_t e)
        {
            if (lv_event_get_code(e) == LV_EVENT_CLICKED)
            {
                if (lv_obj_has_state(lv_event_get_target_obj(e), LV_STATE_CHECKED))
                {
                    press();
                }
                else
                {
                    release();
                }
            }
        }, LV_EVENT_CLICKED>();

        return static_cast<Derived &>(*this);
    }


    // 状态管理方法
    inline Derived &add_flag(Flag_t f)
    {
        lv_obj_add_flag(obj_, f);
        return static_cast<Derived &>(*this);
    }

    inline Derived &clear_flag(Flag_t f)
    {
        lv_obj_clear_flag(obj_, f);
        return static_cast<Derived &>(*this);
    }

    inline Derived& checkable()
    {
        add_flag(LV_OBJ_FLAG_CHECKABLE);
        return static_cast<Derived &>(*this);
    }

    inline Derived &appear()
    {
        clear_flag(LV_OBJ_FLAG_HIDDEN);
        return static_cast<Derived &>(*this);
    }

    inline Derived &hidden()
    {
        add_flag(LV_OBJ_FLAG_HIDDEN);
        return static_cast<Derived &>(*this);
    }

// 更安全的动画实现(暂不可用)
    template<typename T>
    Derived &animate(lv_anim_exec_xcb_t exec_cb, T start, T end, uint16_t time,
                     lv_anim_path_cb_t path = lv_anim_path_linear)
    {
        lv_anim_t a;
        lv_anim_init(&a);
        lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t) exec_cb);
        lv_anim_set_var(&a, obj_); // 直接操作LVGL对象
        lv_anim_set_values(&a, start, end);
        lv_anim_set_time(&a, time);
        lv_anim_set_path_cb(&a, path);
        lv_anim_start(&a);
        return static_cast<Derived &>(*this);
    }

protected:
    // 公共初始化模板(供派生类调用)
    void inline create_obj(const lv_obj_class_t *cls, Obj parent = parent_)
    {
        obj_ = lv_obj_class_create_obj(cls, parent);
        lv_obj_class_init_obj(obj_);
    }
};


#endif //SIMULATOR_WIDGETS_HPP

// imageButton.hpp

//
// Created by fairy on 2024/10/27 14:57.
//
#ifndef SIMULATOR_IMAGEBUTTON_HPP
#define SIMULATOR_IMAGEBUTTON_HPP

#include "widget.hpp"


/**
 * @brief 图片按钮初始化
 */
class ImageButton : public Widget<ImageButton>
{
public:
    // 使用前必须设置父对象
    ImageButton &init(Obj_t parent = parent_)
    {
        create_obj(&lv_imagebutton_class,parent);
        return *this;
    }

    ImageButton &init(Coord x, Coord y, Coord w, Coord h, ImageSrc release_src = nullptr,
                      ImageSrc press_src = nullptr)
    {
        init();
        pos_size(x, y, w, h);
        add_flag(LV_OBJ_FLAG_CHECKABLE);
        src(LV_IMGBTN_STATE_RELEASED, release_src);// 正常状态
        src(LV_IMGBTN_STATE_CHECKED_RELEASED, press_src);// 按下后状态
        return *this;
    }


    // 默认居中,大部分情况下都用不到旁边两个参数,需要时再补充
    ImageButton &src(ImageButton_State state, ImageSrc src)
    {
        lv_imgbtn_set_src(obj_, state, nullptr, src, nullptr);
        return *this;
    }

    // 设置按压图片
    ImageButton &pressed_src(ImageSrc src_left,ImageSrc src_mid= nullptr, ImageSrc src_right= nullptr)
    {
        lv_imgbtn_set_src(obj_, LV_IMGBTN_STATE_CHECKED_RELEASED, src_left, src_mid, src_right);
        return *this;
    }

    // 设置释放图片
    ImageButton &released_src(ImageSrc src_left,ImageSrc src_mid= nullptr, ImageSrc src_right= nullptr)
    {
        lv_imgbtn_set_src(obj_, LV_IMGBTN_STATE_RELEASED, src_left, src_mid, src_right);
        return *this;
    }

    // 设置选中按压图片
    ImageButton &checked_pressed_src(ImageSrc src_left,ImageSrc src_mid= nullptr, ImageSrc src_right= nullptr)
    {
        lv_imgbtn_set_src(obj_, LV_IMGBTN_STATE_CHECKED_RELEASED, src_left, src_mid, src_right);
        return *this;
    }

    // 设置选中释放图片
    ImageButton &checked_released_src(ImageSrc src_left,ImageSrc src_mid= nullptr, ImageSrc src_right= nullptr)
    {
        lv_imgbtn_set_src(obj_, LV_IMGBTN_STATE_CHECKED_RELEASED, src_left, src_mid, src_right);
        return *this;
    }
    


    // 默认发送的事件为LV_EVENT_CLICKED,因为平时用得最多就是这个。后面可自行添加
    ImageButton &press()
    {
        lv_imgbtn_set_state(obj_, LV_IMGBTN_STATE_CHECKED_RELEASED);
        lv_obj_add_state(obj_, LV_STATE_CHECKED); // 选中
        send_event<LV_EVENT_CLICKED>();
        return *this;
    }

    ImageButton &release()
    {
        lv_imgbtn_set_state(obj_, LV_IMGBTN_STATE_RELEASED);
        lv_obj_remove_state(obj_, LV_STATE_CHECKED); // 取消选中
        send_event<LV_EVENT_CLICKED>();
        // 这个发送事件逻辑并未修复
        return *this;
    }






};


#endif //SIMULATOR_IMAGEBUTTON_HPP

        更多代码可以在下面工程的Adapter/GUI/Component目录里找到,不过main分支可能会滞后,可以选择切换ZQ分支

GitCode:项目首页 - STM32F407VET6:stm32f407vet6 - GitCode

Github:GitHub - ichliebedich-DaCapo/STM32F407VET6: stm32f407vet6

5,代码组织

        在GUI Guider生成的代码中,组件的定义是封装在一个结构体里的

typedef struct
{
  
	lv_obj_t *blueCounter;
	bool blueCounter_del;
	lv_obj_t *blueCounter_cont_1;
	lv_obj_t *blueCounter_plus;
	lv_obj_t *blueCounter_plus_label;
	lv_obj_t *blueCounter_minus;
	lv_obj_t *blueCounter_minus_label;
	lv_obj_t *blueCounter_btn_1;
	lv_obj_t *blueCounter_btn_1_label;
	lv_obj_t *blueCounter_logo;
	lv_obj_t *blueCounter_counter;
	lv_obj_t *blueCounter_cb_1;
}lv_ui;

        定义结构体变量后,可以通过“.”来访问其中成员。后来,我一寻思,这不够有区分度,比如定义多个屏幕后,组件就乱了,于是想到再嵌套一层结构体

struct lv_ui_t
{
    // 主屏幕
    struct
    {
        Component screen{}; // 屏幕自身
        Label label_counter; // 计数器显示
        Button btn_plus; // "+" 按钮
        Button btn_minus; // "-" 按钮
        Button btn_reset; // 复位按钮
    } main;// 主屏幕

};
extern struct lv_ui_t* gui;

        可以这般访问不同屏幕的组件:

 gui.main.screen.size(12,20);

        不够这样也有问题,现在我们考虑一个(学习)场景,我们已经封装好了常用的lvgl的API,练习不同项目使用的是同一个板子。那么主屏幕的初始化就会经常重复,总是一样地设置大小(比如480*320),每次定义组件也都会有它。

        那么你会怎么做?一般都会选择把重复的逻辑封装起来,比如下面,前面三行代码:以nullptr为父对象初始化、设置屏幕尺寸、把主屏幕设置为父对象,这些几乎是每次设计界面都要做的,唯一不同的就是其他组件的定义和事件定义。

        于是这里声明了screen_init和events_init函数(GUI_Base.hpp),这两个函数就可以放到ui.cpp里去实现。

    gui->main.screen.init(nullptr);
    lv_obj_set_size(gui->main.screen, DISP_HOR_RES, DISP_VER_RES);
    Component::set_parent(gui->main.screen);// 默认主屏幕为父对象
    GUI_Base::screen_init();// 初始化屏幕
    lv_obj_update_layout(gui->main.screen);
    GUI_Base::events_init();
    lv_scr_load(gui->main.screen.get_obj());

        这样会产生几个矛盾,其一,前面封装好的那些类(label.hpp、button.hpp等)为了方便被ui文件包含,于是就全部在GUI_Base.hpp里包含,这样ui.hpp/cpp就可以通过包含GUI_Base而访问所有封装好的组件类,另一方面,GUI_Base由于需要访问gui里的成员,那么就需要找到gui这个结构体的声明,那么就要包含ui.hpp头文件。这样以来就会导致头文件互相包含,虽然由于预编译宏的存在不会报错,且可以通过创建头文件来解决头文件互相包含,但终究耦合得不像话,乱糟糟的。

        其二,为了能让公共的GUI_Base.hpp不需要来回变动,那么就需要ui.hpp每次都需要定义同样的结构体gui,并且结构体里必须还要有一个叫main的结构体,main结构体里还需要有screen的成员。虽然可以把这作为一种人为准则,但总有着不自由的感觉,而且每次都要定义相同的东西。

        前面的这两个矛盾,不影响最终实现效果,但太过别扭。需要引入别的机制来解决,比如命名空间。

        有了命名空间后,一切就简明了许多,不用每次都定义相同的内容,通过using namespace后,不用每次都输入gui.main前缀来访问组件

// GUI.hpp

namespace gui
{
    // 初始化相关
    namespace init
    {
        void screen();// 初始化界面,需要用户实现
        void events();// 初始化事件,需要用户实现
    }


    // 控件定义
    namespace widgets
    {
        // 主屏幕
        namespace main
        {
            extern Component scr;// 主屏幕,由于会与init里的screen函数重名,所以就简化了名称
        }

        // 占个位置
        namespace others
        {

        }
    }


    // 自己定义各种对外接口,用于app里调用驱动时可以实时更新画面
    namespace interface
    {

    }
}

        如上,在GUI_Base.hpp里定义一个gui的命名空间,里面可以嵌入其他命名空间。在这些命名空间widgets::main里,可以声明一个scr组件(屏幕),这样就不必每次都在ui.hpp里声明一次。

        而且由于命名空间的可拼接性,可以在不同文件里定义相同的命名空间,里面的代码会自动拼接在一起(需要包含头文件)。于是,现在在引入别的文件和命名空间的情况下,GUI_Base.hpp包含了各种组件封装类的头文件和命名空间的声明,而ui.hpp只需要包含GUI_Base.hpp即可,不需要GUI_Base.hpp再包含ui.hpp,并且ui.hpp可以更加自信做自己

// ui.hpp

namespace gui::widgets::main
{
    extern Component rect;//示波器方框
    extern Button btn_mode;// 模式
    extern Label label_mode;
    extern Button btn_freq;// 频率
    extern Label label_ratio;
    extern Button btn_bias_add;// 偏置
    extern Button btn_bias_sub;
    extern Label label_tick;// 时刻
    extern Label label_info;// 信息
    extern Label label_title;// 标题
}

namespace gui::interface
{
    // 是否显示FPS
    auto show_fps(bool is_show) -> void;

    // 是否显示FPS一帧时间
    auto set_fps_mode(bool fps_mode) -> void;

    // 增加波形点数
    auto add_wave_cnt() -> void;

    // 减少波形点数
    auto sub_wave_cnt() -> void;

    // 切换波形类型
    auto switch_wave_type() -> void;

}

        ui.cpp也不需要再直接裸定义screen_init和events_init,现在可以在命名空间里有组织地定义screen和events

// ui.hpp

namespace gui::widgets::main
{
    Component rect;//示波器方框
    Button btn_mode;// 模式
    Label label_mode;
    Button btn_freq;// 频率
    Label label_ratio;
    Button btn_bias_add;// 偏置
    Button btn_bias_sub;
    Label label_tick;// 时刻
    Label label_info;// 信息
    Label label_title;// 标题
}

// 使用命名空间,简化书写。
// 或者使用namespace ui_main = gui::widgets::main;来简写
using namespace gui::widgets::main;
namespace gui::init
{
    void screen()
    {
        rect.size(200, 160)
            .hidden()
            .center();
        
    }

    void events_init()
    {
        // 假设SignalGenerator类已经定义过了
        widgets::main::btn_mode.OnPressed<SignalGenerator::wave_is_generate>();
    }
}


namespace gui::interface
{
    // 是否显示FPS
    auto show_fps(bool is_show) -> void
    {
        
    }

    // 是否显示FPS一帧时间
    auto set_fps_mode(bool fps_mode) -> void
    {
        
    }

    // 增加波形点数
    auto add_wave_cnt() -> void
    {
        
    }

    // 减少波形点数
    auto sub_wave_cnt() -> void
    {
        
    }

    // 切换波形类型
    auto switch_wave_type() -> void
    {
        
    }

}

二、代码转换脚本

1,使用脚本的目的

        前面提到,只靠封装后的API来设计界面比较吃力,即便封装一些常用的组件样式

    inline Button &init(Strings text = nullptr, Obj parent = parent_)
    {
        init(parent);

        // 创建label,以Button为父对象
        label = lv_label_create(obj_);
        Button::font(font_);
        lv_label_set_text(label, text);
        lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);// 设置label居中
        lv_obj_set_style_text_color(label, lv_color_black(), selector_default);// 设置label字体颜色为黑色
        return *this;
    }

        因此,我们需要先由GUI Guider生成初始化代码,再由脚本转换为特定的C++代码

void setup_scr_blueCounter(lv_ui *ui)
{
    //Write codes blueCounter
    ui->blueCounter = lv_obj_create(NULL);
    lv_obj_set_size(ui->blueCounter, 480, 320);
    lv_obj_set_scrollbar_mode(ui->blueCounter, LV_SCROLLBAR_MODE_OFF);

    //Write style for blueCounter, Part: LV_PART_MAIN, State: LV_STATE_DEFAULT.
    lv_obj_set_style_bg_opa(ui->blueCounter, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui->blueCounter, lv_color_hex(0x599cc7), LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_dir(ui->blueCounter, LV_GRAD_DIR_VER, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_color(ui->blueCounter, lv_color_hex(0x8dcdeb), LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_bg_main_stop(ui->blueCounter, 0, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_stop(ui->blueCounter, 255, LV_PART_MAIN|LV_STATE_DEFAULT);

    //Write codes blueCounter_cont_1
    ui->blueCounter_cont_1 = lv_obj_create(ui->blueCounter);
    lv_obj_set_pos(ui->blueCounter_cont_1, 93, 60);
    lv_obj_set_size(ui->blueCounter_cont_1, 294, 200);
    lv_obj_set_scrollbar_mode(ui->blueCounter_cont_1, LV_SCROLLBAR_MODE_OFF);

    /*……*/

}

2,脚本设计

         脚本语言这块,毫无疑问选用python,把大部分精力花在解决问题上,而非语言上,不过都用python了,效率这方面嘛……几秒以内还是可以考虑的哈

        有一点需要提一下,以前使用python时觉得缩进很不方便,因为没有如那些含花括号的语言,可以快速格式化文本。有时候写if后想要去除,那么得把if下的语句全部一个一个取消缩进,巨痛苦。

        后来才发现是使用姿势不对,框选,然后Tab键,即可全部添加一个Tab缩进。框选,然后Shift+Tab键,即可全部取消一个Tab缩进。

定位

        现在我们先分析GUI Guider生成的初始化代码,其实我们真正想要的是setup_scr_*这个函数里面的代码,由于也是用工具生成的代码,所以格式非常齐整。

        想要定位我们需要的部分,我们需要在代码里匹配到setup_scr_*这种C函数,然后把花括号里面的内容提取出来

    # 【定位屏幕初始化代码】:定位 setup_scr_* 函数,并获取工程名和函数体
    c_content = find_c_functions(search_path="E:\Program\Embedded\MCU\GUI\GUI\generated",
                           func_name="setup_scr_*",
                           file_name="setup_scr_*.c")

        如上面这个函数,自然需要获取文件路径、文件和函数名称信息。

        在函数内部,首先需要根据路径和文件参数来把文本内容全部读取出来

    target_file = Path(search_path).glob(file_name)
    matched_files = list(target_file)

    # 文件校验
    if len(matched_files) == 0:
        raise FileNotFoundError(f"No files match '{file_name}' in {search_path}")
    if len(matched_files) > 1:
        raise ValueError(f"Multiple files match '{file_name}': {matched_files}")

    # 读取文件内容
    with open(matched_files[0], 'r', encoding='utf-8') as f:
        content = f.read()

        然后通过正则表达式搜索有没有符合setup_scr_*的函数(在GUI Guider的生成代码里,有且只有一个)

    # 构建正则表达式模式
    name_pattern = re.escape(func_name).replace(r'\*',  '.*').replace(r'\?', '.')
    func_regex = re.compile(
        r'^\s*((?:[\w_]+\s+)+?)'  # 返回类型 
        rf'({name_pattern})\s*'     # 函数名 
        r'\(([^)]*)\)\s*'           # 参数列表 
        r'\{',
        re.MULTILINE
    )

        定位后,接着提取花括号里的内容,并返回元组(对于不变内容,更节省内存)。这里用for循环是查看是否有其它满足条件的函数,以后也方便改装

    functions = []
    for match in func_regex.finditer(content):
        # 提取函数信息
        func_name = match.group(2)
        params = [p.strip() for p in match.group(3).split(',')  if p.strip()]

        # 提取函数体(去花括号)
        start = match.end()
        brace_level = 1
        end_pos = None

        for i in range(start, len(content)):
            if content[i] == '{': brace_level +=1
            elif content[i] == '}': brace_level -=1

            if brace_level == 0:
                end_pos = i
                break

        if end_pos is None:
            continue  # 忽略不完整定义

        # 清理函数体格式
        func_body = content[start:end_pos].strip()
        func_body = re.sub(r'^\s*{ |\s*}$', '', func_body, flags=re.DOTALL).strip()

        functions.append((func_name,  params, func_body))

    if not functions:
        raise ValueError(f"No functions matching '{func_name}' found")

    return tuple(functions)

       

切割代码行

        在获取到文件内容后,根据代码的特点,我是说现在所有初始化代码都只是调用lvgl的API,而且都是一行一个API。每个函数调用,基本是这种格式:

【变量名(可选)】 = 【函数名】(【形参表】);

        有用的信息都在里面,那么可以根据这个特点,我们先从刚才获得的函数体内容提取出包含函数调用的代码行,注释行或者空行需要剔除

def extract_c_code_lines(input_text):
    """
    把文本里的代码提取出一行代码
    :param input_text: 
    :return: 
    """
    code_lines = []
    lines = input_text.splitlines()
    for line in lines:
        stripped_line = line.strip()
        if not stripped_line:
            continue  # 跳过空行
        # 分割行内注释,取注释前的代码部分
        code_part = stripped_line.split('//', 1)[0].strip()
        if code_part:
            code_lines.append(code_part)
    return code_lines

处理代码行

        接下来我们就可以以代码行为研究对象,从中提取有用的信息:变量名(可选)、函数名和形参表。为了方便后面处理,我们还需要用一个结构来处理,那么首选自然是列表。也就是说向一个列表[]里塞入从每一个行代码行里获取的信息。

def parse_function_line(code_line):
    """
    词法解析,把代码行解析为【变量名(可选)】、【函数名】和【形参表(已去除两端空白字符,并分割)】
    :param code_line: 去除换行的代码行
    :return: (var, func_name, args),未匹配到会返回None,var是全匹配,包含ui->
    """

    # 正则表达式模式
    pattern = re.compile(
        r'^\s*'  # 起始空白
        r'(?:([^=\s]+)\s*=\s*)?'  # 变量名(可选)
        r'([a-zA-Z_]\w*)'  # 函数名
        r'\s*\(\s*'  # 左括号
        r'([^()]*(?:\([^()]*\))*[^()]*'  # 形参表(允许一层括号嵌套)
        r')\)\s*;'  # 右括号和分号
        r'\s*$'  # 结束空白
    )

    # 使用正则表达式进行匹配
    match = pattern.match(code_line)
    if match:
        var_name, func_name, params = match.groups()
        # 清理形参表中的多余空格
        params = params.strip() if params else ''
        params = params.split(',') if params else []
        params = [p.strip() for p in params]
        # print(f'code_line:|{code_line}|')
        # print(f"var:{var_name}  func:{func_name}  args:{params}")
        return var_name, func_name, params
    else:
        print(f"code_line:|{code_line}|")
        raise ValueError("Could not find function line")
        # 如果没有匹配到,返回错误提示

       考虑到拥有变量名的函数调用是特殊的,只有调用 lv_obj_create等这种创建组件函数时才会把值赋给变量,而这句给我们三个很重要的信息:子组件、父组件、组件类型,比如调用lv_label_create函数,那么我们就知道它是label组件。

        注意到后面分析组件与函数调用关系时,实际上是分析的是单行代码,这样获取到的信息太过片面。因此,我们需要建立组件与组件的关系、组件和函数调用的关系,组件与组件的关系容易建立,如前面所言,含有变量的代码行,我们可以获取三个重要信息。那么我们就可以建立一个结构来存储这种信息,对于这样单行代码的信息,可以用['子组件名','子组件类型','父组件']来存储。这样方便查找(其实可以用元组),一行行信息如下,另一种意义上也相当于主动进行拓扑排序了

       

[['child_name',[widget_info],'parent_name'],……]

        那么对于不含变量的代码行,我们也可以差不多的结构。形参表第一个参数就是其组件名称,这里把函数名、形参表作为一个整体塞入组件名称旁的元素,做成了函数集合,这是为了方便后续建立链式调用。同时即便代码是东一个西一个,也可以整齐地收集起来,便于后续处理

[['widget_name',['func_name',[args]],……],……]
def process_code_lines(code_lines):
    """
    处理代码行。把代码行里的信息全部存储到初始化信息表和关系表里
    :param code_lines: 已经做过处理,每行只包含代码行
    :note 向widget里添加['widget_name',[[func,[args]],……]]。其中组件名是已经去除ui->
    """
    for line in code_lines:
        # 词法解析,获取变量名、函数名(不为空)和参数表
        var, func_name, args = parse_function_line(line)
        # 剔除无关函数
        if 'events_init' in func_name:
            continue

        if var is None:
            # 初始化代码行:把组件信息提取到widgets里
            # lv_obj_set_scrollbar_mode(ui->blueCounter_cont_1, LV_SCROLLBAR_MODE_OFF);
            # ①先获取组件名称
            widget_name = extract_comp_name_from_var(args[0])
            # ②存储初始化信息  [['widget_name',['func_name',[args]],……],……]
            add_widget_function(widget_name, func_name, args)
        else:
            # 创建组件代码行:组件关系提取到widgets_relations里
            # ui->blueCounter_cont_1 = lv_obj_create(ui->blueCounter);
            # ①先获取父子组件名称
            child_name = extract_comp_name_from_var(var)
            parent_name = extract_comp_name_from_var(args[0])
            # ②获取组件类型
            widget_info = get_widget_info(line)
            # ③添加组件关系   [['child_name',[widget_info],'parent_name']]
            if not add_widget_relations(child_name, widget_info, parent_name):
                # 存在预期外的重复组件关系
                raise ValueError("Failed to add widget relations")

解析信息

        在获取到组件关系和组件函数集合这些主要信息后,就可以解析这些信息,转换为我们自己定义的代码格式。总得来说分成两步,第一步是根据制定的“规则表”或者说“映射表”配合“解析器”来完成单行信息的转换,第二部则是把这些信息按照特定的结构组织存储起来。

        根据初始化函数调用的形式,我们可以分成两种,一种是如lv_obj_set_size这种设置组件属性的函数,有多种多样且形参不同。另一种是如lv_obj_set_style_*这种设置组件样式的函数,函数名都有相同的前缀且形参固定就三个。因此可以分别处理(实际上是规则决定的)

            # 获得处理过后的C++函数代码
            if 'lv_obj_set_style' in func_name:
                # 如果是样式函数
                func_code = convert_style_calls(func_name, args)
            else:

                func_code = convert_function_args(func_name, args)

        先看对样式函数的处理,这里先定义一个列表(可以换成字典或者元组),列表的第一个元素就是函数名,因为样式函数的前缀固定就提前去掉了。第二个元素表示的是缺省值,如果对应的样式函数里有该值,那就表示这个函数可以省去调用。这一点是需要着重强调的,在GUI Guider生成的代码里,有些样式是组件创建时就默认的,但GUI Guider仍会再设置一遍,就导致冗余了。比如下面,在组件创建之初,就已经默认文本是完全不透明(255)了

lv_obj_set_style_text_opa(ui->blueCounter_counter, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
    # 每个列表都有2~3个元素,左边是匹配的函数名(相当于键),第二个元素是默认值(为''表示没有),第三个元素是函数的简写(如果有的话)
    default_config = [
        ['border_width', "0"],
        ['text_color', 'lv_color_hex(0xffffff)'],
        ['text_opa', "255"],
        ['text_font', '', 'font'],
        ['text_line_space', "0"],
        ['text_align', 'LV_TEXT_ALIGN_CENTER'],
        ['bg_opa', "255"],
        ['pad_top', "0"],
        ['pad_right', "0"],
        ['pad_bottom', "0"],
        ['pad_left', "0"],
        ['shadow_width', "0"],
        ['image_recolor_opa', '255'],
        ['image_opa', '255'],
        ['text_letter_space', '0', 'letter_space']
    ]

        不过对这个映射表的处理并没有做到完全规避不必要的调用,因为一开始只是想着按照C++重载的顺序,只有右边第一个参数是默认值是“LV_PART_MAIN|LV_STATE_DEFAULT

”时,才能看左边是否要缺省,如果都缺省那么这个函数就省去调用。现在看来,右边这个selector的判定优先级应该放在后面,逻辑应改为只要样式函数的value符合缺省表里的值,那么就省去调用(返回None)

    # 这里能做到这么简洁是因为lvgl 9.2的所有样式API都只有三个参数,并且前缀相同
    match = re.match(r'lv_obj_set_style_(\w+)', func_name)
    if match is None:
        # 不应该出现的情况
        raise NotImplementedError(f"Function '{func_name}' not implemented.")

    prop = match.group(1)
    value = args[1]
    state = args[2]

    # 如果在默认配置表里找到了对应函数,那么就对照默认配置修改
    index = next((i for i, item in enumerate(default_config) if item[0] == prop), -1)
    # 处理状态,如果是默认状态,那么就不添加
    state = "" if state == 'LV_PART_MAIN|LV_STATE_DEFAULT' else f", {state}"
    if index != -1:
        prop = list_get2(default_config[index], prop)  # 从默认配置里看看有没有第三个元素(简写)
        value = "" if value == default_config[index][1] and state == "" else f"{value}"
        if state == "" and value == "":
            # 如果都为默认值,那么就跳过这次处理
            return None

    converted = f".{prop}({value}{state})"

    return converted

        对属性设置函数的处理就要复杂得多了,这里分成两步,一是先进行形参缺省处理(omit_parameters),二是把进行处理过后的参数再进行函数名和形参的混合处理

    for funcs, config in function_handlers.items():
        # 匹配函数规则
        if func_name in funcs:
            # 处理参数规则
            args_result, handle_args = omit_parameters(args, config['args_map'])
            final_result, final_method, final_args = get_method_from_map(args_result, handle_args, config['method_map'])
            # 判断是否该省略调用
            if final_result:
                return None
            else:
                return f".{final_method}({', '.join(final_args[1:])})"

        之所以定这样的规则,是因为要转换的函数是支持缺省的,比如这个函数,最右边有两个都是NULL,那么完全可以把它们作为要转换的函数src的默认缺省值,为的就是少写不必要的形参

lv_imagebutton_set_src(ui->blueCounter_minus, LV_IMAGEBUTTON_STATE_RELEASED, &_btn_RGB565A8_65x65, NULL, NULL);

        此事在函数映射表中亦有记载,我把函数的缺省表放在了'args_map'中

# 函数映射表
# ①缺省表要注意缺省值是字符串类型
# ②如果缺省表为空,那么默认是全缺省,所以,如果你不想被省略,那么需要把type设为非None,比如''
function_handlers = {
    # 图像按钮
    'lv_imagebutton_set_src': {
        # 参数缺省映射表,由专门的函数进行重载解析,最后返回参数列表。如果无参就返回None
        'args_map': ['NULL', 'NULL'],
        'method_map': {
            # 确定哪些参数能决定改变method,单映射的情况下,handler为None即可
            'index': [1],
            # 映射表可以为空,映射之后这个参数就要去掉,因为我的目的是简化
            'mapping': {
                'LV_IMAGEBUTTON_STATE_RELEASED': 'released_src',
                'LV_IMAGEBUTTON_STATE_PRESSED': 'pressed_src',
                'LV_IMAGEBUTTON_STATE_CHECKED_RELEASED': 'checked_released_src',
                'LV_IMAGEBUTTON_STATE_CHECKED_PRESSED': 'checked_pressed_src',
            },
            # 类型为None表示参数全缺省即可免去调用,不为None表示不缺省就省略(这个优先级是高于args_map为空的)。
            'type': '',
            # method默认在映射表里去找,如果找不到就用默认值
            'default': 'src',
            # 单参数可以自定一个lambda,多参数处理时,必须要自定一个lambda,用于返回处理的结果。
            # 输入的参数是索引(或者处理过的参数)、参数表和mapping,需要返回一个判断结果(以None为标志)和一个最终处理的函数名和参数表
            # 事实上,更复杂的的需求不可能依靠lambda来完成,但是现在并没有遇到情况,就先用lambda代替
            'handler': None
        },
    },

        下面还有个method_map,这就是改变生成的函数名,如果指定Index位置处的参数,满足mapping里的值,那么就使用后面的函数名,如果不满足就使用default所对应的默认函数名。这个规则表是处理更普遍lvgl函数的转换设计的,不过也并不怎么合理,使用yaml的话又有些复杂了,就先凑合凑合。

        与前面的样式表一样,这是可以自行添加或者修改的,或者不用这个表结构也行。

        有了规则表之后,那么就按照前面设定的规则来写“解析器”,两者的思路是一脉的,在设计表的同时,实际上也就知道怎么解析了

def get_method_from_map(all_omitted, args, method_map):
    """
    解析method映射表,返回是否省略调用、方法名和参数列表。
    :param all_omitted: 是否所有参数都被省略(True/False)。
    :param args: 处理后的参数列表。
    :param method_map: 方法的参数映射表,包含index、mapping、handler等键。
    :return: (omit_call, method_name, processed_args)
            第一个参数为True表示可以跳过,最后一个参数是包含了第一个参数,即组件自身,需要主动剔除掉
    """
    # 如果全缺省并且类型为None,那么就跳过函数调用
    if all_omitted and method_map.get('type') is None:
        return True, None, []

    # 获取默认method和handler
    default_method = method_map.get('default', '')
    handler = method_map.get('handler')

    if handler is None:
        # 如果handler不存在
        indices = method_map.get('index', [])
        if not indices:
            # 如果是无参数映射,直接返回默认方法和参数
            return False, default_method, args

        # 处理单参数映射,取第一个索引
        index = indices[0]
        if index >= len(args):
            # 单参数映射超出范围,说明映射表需要修正
            raise ValueError(f"Invalid index {index} for method mapping")

        # 进行单参数映射
        param = args[index]
        mapping = method_map.get('mapping', {})
        method = mapping.get(param, default_method)

        if method == default_method:
            return False, default_method, args
        else:
            # 存在映射,则删除指定元素
            new_args = list(args)
            del new_args[index]
            return False, method, new_args
    else:
        # 如果handler存在(单参数映射或者多参数映射)
        indices = method_map.get('index', [])
        params = [args[i] for i in indices if i < len(args)]
        # 使用映射表里的lambda,输入索引(或者处理过的参数)、参数表和mapping,返回结果、method和参数
        # 映射表里的method的第一个参数要确保是判断结果,结果若为None,那么就使用默认method
        result, method, final_args = handler(params, args, method_map.get('mapping', {}))
        if result is None:
            method = default_method
        return False, method, final_args

        因为避免有些lvgl函数在表里找不到从而不生成,所以改成了“如果表里没有,那么就报错”,来提示使用者在表里添加对应的规则了。

        接下来就是按照规则把处理后的信息存储起来,为了便于后续拼接,这里用列表先按照一行函数一个元素的原则组织,把一个组件的所有函数都放在一个列表里后,再用带缩进的方式拼接成一个代码块。

def iterate_widgets(screen_name):
    """遍历组件结构的通用函数
    Note:
        # 1,链式调用需要一张表,组件在前面,然后init后跟父组件,接着不断往后添加到列表里
            ['widget_name.init(parent_name)','.pos()','size()',]
        # 2,根据实际情况,组件名称前需要'\n\t\t',两个Tab,而其他链式调用则需要三个Tab
        # 3,对widgets_define添加了组件定义的信息,但是没有把里面的代码通过'\n\t'连接
    Args:
        screen_name(string): 项目名称,这里是为了把屏幕名称替换为scr
    Returns:
        widgets_init_code(list):由一段段组件初始化代码组成,最后是需要通过'\n\t'进行拼接
    """
    widgets_init_code = []
    # 处理屏幕组件
    screen_widget = find_widget(screen_name)
    screen_func_list = screen_widget[1]
    screen_init_chain = []
    # 剔除屏幕组件
    remove_widget(screen_name)
    for scr_func_info in screen_func_list:
        func_name = scr_func_info[0]
        args = scr_func_info[1]
        if 'lv_obj_set_style' in func_name:
            # 如果是样式函数
            func_code = convert_style_calls(func_name, args)
            if func_code is None:
                continue
            screen_init_chain.append(func_code)
    # 给组件的第一个样式函数前加上scr
    screen_init_chain[0] = '\t\tscr' + screen_init_chain[0]
    screen_init_chain[-1] += ';'  # 末尾添加分号
    widgets_init_code.append('\n\t\t\t'.join(screen_init_chain))

    # 处理其他组件
    for widget in widgets:
        widget_name = widget[0]
        function_list = widget[1]
        # 链式调用:创建组件的代码
        widget_relation = find_relations(child_name=widget_name)
        widget_info = widget_relation[1]
        parent_name = widget_relation[2]
        # 转为合成名
        widget_name_var_name = widget_info[0] + '_' + widget_name
        if parent_name == screen_name:
            # 空即默认主屏幕
            parent_name_var_name = ''
        else:
            parent_relation = find_relations(child_name=parent_name)
            parent_widget_info = parent_relation[1]
            parent_name_var_name = parent_widget_info[0] + '_' + parent_name
        widget_init_chain = [f'\n\t\t{widget_name_var_name}.init({parent_name_var_name})']
        # 组件定义
        widgets_define.append(f'\t{widget_info[1]} {widget_name_var_name};')
        for func_info in function_list:
            func_name = func_info[0]
            args = func_info[1]

            # 获得处理过后的C++函数代码
            if 'lv_obj_set_style' in func_name:
                # 如果是样式函数
                func_code = convert_style_calls(func_name, args)
            else:

                func_code = convert_function_args(func_name, args)
            extract_resource(func_name, args)# 提取字体、图片资源

            # 判断是否跳过调用
            if func_code is None:
                print(f'Skipped function: \t{extract_comp_name_from_var(args[0])}\t{func_name}')
                continue
            else:
                widget_init_chain.append(func_code)

        # 拼接为组件初始化代码
        widget_init_chain[-1] += ';'  # 末尾添加分号
        widgets_init_code.append('\n\t\t\t'.join(widget_init_chain))
    # 给定义组件代码的首元素添加\n
    # widgets_define[0] = '\n' + widgets_define[0]
    return widgets_init_code,widgets_define

        拼接后的代码就类似于这个形式,已经有完整的格式了

		img_blueCounter_img_1.init()
			.pos(0, 25)
			.size(100, 100)
			.add_flag(LV_OBJ_FLAG_CLICKABLE)
			.pivot(50, 50)
			.image_recolor_opa(0);

        然后每个组件处理成一个代码块(str),最后都存放到一个列表里,由后面步骤进行代码生成。与此同时,另一边,在合成组件定义

        # 组件定义
        widgets_define.append(f'\t{widget_info[1]} {widget_name_var_name};')

        这个格式翻译过来就是这样,为了区分不同变量是什么类型,前面还添加了特定的前缀

	Component obj_blueCounter_cont_1;

        这些前缀实际上是来自于另一张表,专门用于指定组件的类型,右边列表的第一个元素就是变量名前缀,第二个元素就是组件的类型

    widget_map = {
        "lv_obj_create": ["obj", 'Component'],
        "lv_imagebutton_create": ["imgbtn", 'ImageButton'],
        "lv_label_create": ["label", 'Label'],
        "lv_image_create": ["img", 'Image'],
        "lv_button_create": ["btn", 'Button'],
        "lv_checkbox_create": ['chekcbox', 'CheckBox'],
    }

        除了这些外,还有一些别的处理,比如提取资源,从指定函数里提取对应的字体或者图片参数,然后不重复地存储起来,后面生成代码时就可以自动声明资源了。

def extract_resource(func_name: str, args: list) -> None:
    """资源提取器
    Args:
        func_name: LVGL API函数名
        args: 实际调用参数列表
    """
    resource_map = {
        'lv_obj_set_style_text_font': {
            'type': 'font',   # 保持与resources键名一致
            'index': 1
        },
        'lv_imagebutton_set_src':{
            'type': 'image',
            'index': 2
        },
        'lv_image_set_src': {
            'type': 'image',
            'index': 1
        }
    }

        更多细节就不赘述了

代码生成

        代码生成这一步其实就是按照特定的模板,把信息放在对应的位置上,比如这里的C++模板

@dataclass
class CppCodeTemplate:
    """
    代码模板配置容器
    Attributes:
        framework (str): cpp代码框架模板
        widgets_namespace (str): 组件定义命名空间模板
        init_namespace (str): 初始化逻辑命名空间模板
        function_template (str): 函数结构模板
        ui_interface (str): ui接口
    """
    framework: str = """\
{pre_define}
/*!INCLUDES_BEGIN!*/
{includes}

/*!INCLUDES_END!*/

// 组件定义 
{widgets_namespace}
{post_define}

/*!DECLARE_CODE_BEGIN!*/
// 全局变量定义

// 类声明

/*!DECLARE_CODE_END!*/

{pre_init}
{init_namespace}
{post_init}

{ui_interface}

/*!DEFINE_CODE_BEGIN!*/
// 类定义

/*!DEFINE_CODE_END!*/
"""
    widgets_namespace: str = """\
namespace {ns_name} 
{{
{define_blocks}
/*!WIDGETS_DEFINE_BEGIN!*/

/*!WIDGETS_DEFINE_END!*/
}}
using namespace {ns_name};"""


    init_namespace: str = """\
namespace {ns_name}
{{
    void {func_name}() 
    {{
{init_blocks}
    /*!INIT_CUSTOM_BEGIN!*/

    /*!INIT_CUSTOM_END!*/
    }}
    
    void events()
    {{
    /*!EVENTS_CUSTOM_BEGIN!*/
    
    /*!EVENTS_CUSTOM_END!*/
    }}
}}
"""

    ui_interface: str = """\
/*!UI_IMPLEMENT_BEGIN!*/
// 用户自定义接口实现区(安全编辑区) 
namespace {ns_name} {{


}}
/*!UI_IMPLEMENT_END!*/
"""

    # 初始化函数模板
    function_template: str = """\
    void {func_name}() 
    {{
{func_body}
    }}
    """

        里面有些注释你可能看着很眼熟,因为这就是模仿STM32CubeMX生成代码里的注释,可以让你在注释内写东西不会被清除。这一点很容易理解,设计界面时,即使交互逻辑写好了,但界面频繁改动也是存在的。

class GenerateMode(Enum):
    """生成模式枚举"""
    OVERWRITE = 1
    MERGE = 2  # 新增融合模式,移除其他模式

        这是这里设计了两种模式,一是覆写,另一种是融合,融合模式里又分成了三种模式KEEP_WHOLE、KEEP_CONTENT和MERGE,可惜后面两种都没实现。不过只有KEEP_WHOLE也足够了,这就是让注释区间的整个内容保持不变。

        通过下面的表来决定的(实际上是字典,但你应该懂我意思吧,哈哈)

        preserve_rules = {
            'hpp': [
                (r'/\*!UI_INTERFACE_BEGIN!.*?\*!UI_INTERFACE_END!', 'KEEP_WHOLE'),  # 全保留
                (r'/\*!WIDGETS_DECLARE_BEGIN!.*?\*!WIDGETS_DECLARE_END!', 'KEEP_WHOLE')  # 合并内容
            ],
            'cpp': [
                (r'/\*!INCLUDES_BEGIN!.*?\*!INCLUDES_END!', 'KEEP_WHOLE'),
                (r'/\*!WIDGETS_DEFINE_BEGIN!.*?\*!WIDGETS_DEFINE_END!', 'KEEP_WHOLE'),
                (r'/\*!DECLARE_CODE_BEGIN!.*?\*!DECLARE_CODE_END!', 'KEEP_WHOLE'),
                (r'/\*!INIT_CUSTOM_BEGIN!.*?\*!INIT_CUSTOM_END!', 'KEEP_WHOLE'),
                (r'/\*!EVENTS_CUSTOM_BEGIN!.*?\*!EVENTS_CUSTOM_END!', 'KEEP_WHOLE'),
                (r'/\*!UI_IMPLEMENT_BEGIN!.*?\*!UI_IMPLEMENT_END!', 'KEEP_WHOLE'),
                (r'/\*!DEFINE_CODE_BEGIN!.*?\*!DEFINE_CODE_END!', 'KEEP_WHOLE'),

            ]
        }

清一色的KEEP_WHOLE,有时我还在想干脆把另外两个没能实现的模式给清了

        for pattern, mode in preserve_rules.get(file_type, []):
            # 查找现有代码中的匹配区域
            old_match = re.search(pattern, existing, re.DOTALL)
            if not old_match:
                continue

            # 根据模式处理
            if mode == 'KEEP_WHOLE':
                # 用旧区域完全替换新区域
                new = re.sub(pattern, old_match.group(0), new, count=1, flags=re.DOTALL)
            elif mode == 'MERGE_CONTENT':
                # 提取旧内容并插入新框架
                old_content = self._extract_inner_content(old_match.group(0))
                new = self._replace_inner_content(new, pattern, old_content)
            elif mode == 'KEEP_CONTENT':
                # 保留旧内容但保持新框架
                old_content = self._extract_inner_content(old_match.group(0))
                new = self._replace_inner_content(new, pattern, old_content, keep_markers=True)

        除此之外,还存了一些钩子嵌入到模板的不同的位置

class HookSystem:
    """钩子管理系统"""

    HOOK_POINTS = (
        'pre_define',   # 定义块生成前
        'post_define',  # 定义块生成后
        'pre_init',     # 初始化块生成前
        'post_init',    # 初始化块生成后
        'post_generate'# 全部生成完成后
    )

        实际上只用到了一个钩子,在文件的首行写上版本信息

 # 添加版本信息钩子
    generator.hooks.add_hook('pre_define',  lambda: f"// Generated by Fairy on {datetime.now():%Y-%m-%d  %H:%M}\n")

        把代码写到文件中用的是utf-8编码,如果不指定,就默认本地的编码,一般是更适合中国宝宝体质的GB18030

            hpp_path.write_text(final_hpp_code,  encoding='utf-8')
            cpp_path.write_text(final_cpp_code,  encoding='utf-8')

使用

        至于使用的话,在工程的Docs/help/scripts里有,不过应该在ZQ分支里有,暂时没有合并到main上。

        帮助文档上也不是太详细,总之把代码拉到底部,可以看到主函数,主函数的第一行就是指定搜索文件路径、文件和函数名。

        继续下滑就是【生成代码的路径】、【是否让声明的字体加上customer】和【模式】

        在CLion里配置对应的解析器,即可点击上图出现的播放键来运行脚本。解释器下载可以选择.ven的那个,下载过程遭遇失败也很正常,有魔法的用魔法,该换源的换源

        

 3,实际效果

原代码

提取后

// ui.hpp

// ui.cpp

GUI Guider效果:

转换后,CLion中的模拟器:

4,代码

        这里为了省事,所有代码都写在一个文件里,可自行分成多个文件。脚本在前面提到工程的Tools/scripts目录下,注意,如果main分支里没有,就查看ZQ分支

#!/usr/bin/env python3
import argparse
import glob
import os
import re
import textwrap
from datetime import datetime

import networkx as nx
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Dict, Tuple, List, Callable, Optional

# 组件代码块
widgets = []
# 组件的关系, 格式为 [['子组件名', [子组件类型], '父组件名'], ...]
widgets_relations = []
# 组件定义代码块,格式为 ['Component xxx','']
widgets_define = []
# 资源文件,格式为{'font':[],'image':[]}  fonts和images都是对应的名称
resources = {
    'font': [],
    'image':[],
}

# --------------------------------预处理------------------------------------

# 寻找setup_scr_*函数
def parse_setup_function(content):
    pattern = r'void setup_scr_(\w+)\(lv_ui \*ui\)\s*{([^}]+)}'
    match = re.search(pattern, content, re.DOTALL)
    if not match:
        raise ValueError("Could not find setup_scr_* function")
    return match.group(1), match.group(2).strip()


# ---------------------------------工具函数----------------------------------------
# -----------------------文本处理--------------------------

# 输入组件创建代码块,返回组件前缀和组件类型
def get_widget_info(create_line):
    """
    匹配代码行对应的组件类型
    :param create_line: lv_xxx_create函数所在代码行
    :return:
    """
    widget_map = {
        "lv_obj_create": ["obj", 'Component'],
        "lv_imagebutton_create": ["imgbtn", 'ImageButton'],
        "lv_label_create": ["label", 'Label'],
        "lv_image_create": ["img", 'Image'],
        "lv_button_create": ["btn", 'Button'],
        "lv_checkbox_create": ['chekcbox', 'CheckBox'],
    }
    for func, widget_info in widget_map.items():
        if func in create_line:
            return widget_info

    # 报错,确保能及时发现有未入库的函数,以便及时添加
    # TODO: 这里需要进一步检查,把所有匹配上的都添加进去
    raise ValueError(f"Could not find prefix for {create_line}")


# 词法解析,解析代码行的赋值函数语句
# 返回的是变量名,函数名,参数列表(已去除两端空白字符)
def parse_function_line(code_line):
    """
    词法解析,把代码行解析为【变量名(可选)】、【函数名】和【形参表(已去除两端空白字符,并分割)】
    :param code_line: 去除换行的代码行
    :return: (var, func_name, args),未匹配到会返回None,var是全匹配,包含ui->
    """

    # 正则表达式模式
    pattern = re.compile(
        r'^\s*'  # 起始空白
        r'(?:([^=\s]+)\s*=\s*)?'  # 变量名(可选)
        r'([a-zA-Z_]\w*)'  # 函数名
        r'\s*\(\s*'  # 左括号
        r'([^()]*(?:\([^()]*\))*[^()]*'  # 形参表(允许一层括号嵌套)
        r')\)\s*;'  # 右括号和分号
        r'\s*$'  # 结束空白
    )

    # 使用正则表达式进行匹配
    match = pattern.match(code_line)
    if match:
        var_name, func_name, params = match.groups()
        # 清理形参表中的多余空格
        params = params.strip() if params else ''
        params = params.split(',') if params else []
        params = [p.strip() for p in params]
        # print(f'code_line:|{code_line}|')
        # print(f"var:{var_name}  func:{func_name}  args:{params}")
        return var_name, func_name, params
    else:
        print(f"code_line:|{code_line}|")
        raise ValueError("Could not find function line")
        # 如果没有匹配到,返回错误提示


def extract_comp_name_from_var(var):
    """
    从变量名字符串中提取箭头右边的部分。
    :param var: 包含箭头的变量名字符串
    :return: 箭头右边的变量名部分
    """
    # 查找箭头的位置
    arrow_index = var.find('->')

    if arrow_index != -1:
        # 如果找到了箭头,返回箭头右边的部分
        return var[arrow_index + 2:]
    else:
        # 如果没有找到箭头,返回原始字符串
        return var


def extract_c_code_lines(input_text):
    """
    把文本里的代码提取出一行代码
    :param input_text: 
    :return: 
    """
    code_lines = []
    lines = input_text.splitlines()
    for line in lines:
        stripped_line = line.strip()
        if not stripped_line:
            continue  # 跳过空行
        # 分割行内注释,取注释前的代码部分
        code_part = stripped_line.split('//', 1)[0].strip()
        if code_part:
            code_lines.append(code_part)
    return code_lines


# -----------------------------与组件有关系--------------------------------
def check_widget_exists(widget_name):
    """检查组件是否已存在
    Args:
        widget_name (str): 要检查的组件名称
    Returns:
        bool: 是否存在
    """
    return any(widget[0] == widget_name for widget in widgets)


def add_function_to_existing(widget_name, func_name, args):
    """添加函数到已有组件
    Args:
        widget_name (str): 目标组件名称
        func_name (str): 要添加的函数名称
        args (list): 函数参数列表
    """
    for widget in widgets:
        if widget[0] == widget_name:
            widget[1].append([func_name, args.copy()])  # 使用拷贝避免引用问题
            break


def create_new_widget(widget_name, func_name, args):
    """创建新组件结构
    Args:
        widget_name (str): 组件名称
        func_name (str): 函数名称
        args (list): 函数参数列表
    Returns:
        list: 新组件结构
    """
    return [widget_name, [[func_name, args.copy()]]]


def add_widget_function(widget_name, func_name, args):
    """添加组件函数
    实现逻辑:
    1. 存在组件 -> 追加函数
    2. 不存在组件 -> 创建新组件
    Args:
        widget_name (str): 组件名称
        func_name (str): 函数名称
        args (list): 函数参数列表
    """
    if check_widget_exists(widget_name):
        add_function_to_existing(widget_name, func_name, args)
    else:
        new_widget = create_new_widget(widget_name, func_name, args)
        widgets.append(new_widget)


def find_widget(widget_name):
    """
    查找指定组件
    :param widget_name:
    :return: 找到就返回列表,找不到就返回None
    """
    for item in widgets:
        if item[0] == widget_name:
            return item
    return None


def remove_widget(widget_name):
    """
    删除指定组件
    :param widget_name: 组件名称
    :return: 删除后组件的剩余数量
    """
    # 使用列表解析来创建一个新的列表,排除掉要删除的元素
    original_length = len(widgets)
    widgets[:] = [item for item in widgets if item[0] != widget_name]
    removed_count = original_length - len(widgets)
    return removed_count  # 返回已删除项目的数量

# --------添加资源---------
def add_resource(resource_type: str, resource: str) -> None:
    """增强型资源添加函数
    Args:
        resource_type: 资源类型(必须与resources键名一致)
        resource: 资源标识符,自动去除&前缀
    """
    # 参数校验
    if resource_type not in resources:
        raise ValueError(f"Invalid resource type: {resource_type}. Valid types: {list(resources.keys())}")

    # 标准化处理(去除&前缀)
    cleaned = resource.lstrip('&')

    # 去重逻辑
    if cleaned not in resources[resource_type]:
        resources[resource_type].append(cleaned)

def extract_resource(func_name: str, args: list) -> None:
    """增强型资源提取器
    Args:
        func_name: LVGL API函数名
        args: 实际调用参数列表
    """
    resource_map = {
        'lv_obj_set_style_text_font': {
            'type': 'font',   # 保持与resources键名一致
            'index': 1
        },
        'lv_imagebutton_set_src':{
            'type': 'image',
            'index': 2
        },
        'lv_image_set_src': {
            'type': 'image',
            'index': 1
        }
    }

    # 查询资源映射
    if (info := resource_map.get(func_name))  is None:
        return

    # 索引有效性检查
    if info['index'] >= len(args):
        # 映射表里的索引有问题,赶紧修复
        raise IndexError(f"Warning: Invalid arg index {info['index']} for {func_name}")

        # 执行资源添加
    add_resource(info['type'], str(args[info['index']]))

# ------------------------------与组件关系有关------------------------
def add_widget_relations(child_name, child_info=None, parent_name=''):
    """
    添加组件关系(支持类型列表)
    :param child_name: 子组件名
    :param child_info: 子组件类型列表(如 ["UI", "Button"]),None表示没有
    :param parent_name: 父组件名
    :return: True表示添加成功,False表示已存在
    """
    # 自动包装单元素类型为列表(可选)
    if child_info is None:
        child_info = []
    if not isinstance(child_info, list):
        child_info = [child_info]

    new_relation = [child_name, child_info, parent_name]
    if new_relation in widgets_relations:
        return False
    widgets_relations.append(new_relation)
    return True


def find_relations(child_name=None):
    """
    通用查询函数(支持类型包含式查询)
    :param child_name: 子组件(可选)
    :return 返回的是[child_name,child_type,parent_name]类型
    """
    for rel in widgets_relations:
        if child_name is None or rel[0] == child_name:
            return rel


# 函数映射表
# ①缺省表要注意缺省值是字符串类型
# ②如果缺省表为空,那么默认是全缺省,所以,如果你不想被省略,那么需要把type设为非None,比如''
function_handlers = {
    # 图像按钮
    'lv_imagebutton_set_src': {
        # 参数缺省映射表,由专门的函数进行重载解析,最后返回参数列表。如果无参就返回None
        'args_map': ['NULL', 'NULL'],
        'method_map': {
            # 确定哪些参数能决定改变method,单映射的情况下,handler为None即可
            'index': [1],
            # 映射表可以为空,映射之后这个参数就要去掉,因为我的目的是简化
            'mapping': {
                'LV_IMAGEBUTTON_STATE_RELEASED': 'released_src',
                'LV_IMAGEBUTTON_STATE_PRESSED': 'pressed_src',
                'LV_IMAGEBUTTON_STATE_CHECKED_RELEASED': 'checked_released_src',
                'LV_IMAGEBUTTON_STATE_CHECKED_PRESSED': 'checked_pressed_src',
            },
            # 类型为None表示参数全缺省即可免去调用,不为None表示不缺省就省略(这个优先级是高于args_map为空的)。
            'type': '',
            # method默认在映射表里去找,如果找不到就用默认值
            'default': 'src',
            # 单参数可以自定一个lambda,多参数处理时,必须要自定一个lambda,用于返回处理的结果。
            # 输入的参数是索引(或者处理过的参数)、参数表和mapping,需要返回一个判断结果(以None为标志)和一个最终处理的函数名和参数表
            # 事实上,更复杂的的需求不可能依靠lambda来完成,但是现在并没有遇到情况,就先用lambda代替
            'handler': None
        },
    },
    # 不存在缺省参数
    'lv_obj_set_size': {
        'args_map': [],
        'method_map': {
            'index': [],
            'mapping': {},
            'type': '',
            'default': 'size',
            'handler': None
        }
    },
    # 不存在缺省参数
    'lv_obj_set_pos': {
        'args_map': [],
        'method_map': {
            'index': [],
            'mapping': {},
            'type': '',
            'default': 'pos',
            'handler': None
        }
    },
    # 不存在缺省参数
    'lv_obj_set_width': {
        'args_map': [],
        'method_map': {
            'index': [],
            'mapping': {},
            'type': '',
            'default': 'width',
            'handler': None
        }
    },
    # 不存在缺省参数
    'lv_obj_add_flag': {
        'args_map': [],
        'method_map': {
            'index': [1],
            'mapping': {
                'LV_OBJ_FLAG_CHECKABLE': 'checkable',
                'LV_OBJ_FLAG_HIDDEN': 'hidden',
                'LV_FLAG_SCROLLABLE': 'scrollable',
            },
            'type': '',
            'default': 'add_flag',
            'handler': None
        }
    },
    # 全缺省不可免去调用
    'lv_obj_align': {
        'args_map': ['0','0'],
        'method_map': {
            'index': [1],
            'mapping': {
                'LV_ALIGN_CENTER': 'center',
                'LV_ALIGN_RIGHT': 'right',
                'LV_FLAG_LEFT': 'left',
            },
            'type': '',
            'default': 'align',
            'handler': None
        }
    },
    # 全缺省可免调用
    'lv_obj_set_scrollbar_mode': {
        'args_map': ['LV_SCROLLBAR_MODE_AUTO'],
        'method_map': {
            'index': [],
            'mapping': {},
            'type': None,
            'default': 'scrollbar_mode',
            'handler': None
        }
    },
    # 不存在缺省参数
    'lv_label_set_text': {
        'args_map': [],
        'method_map': {
            'index': [],
            'mapping': {},
            'type': '',
            'default': 'text',
            'handler': None
        }
    },
    # 不存在缺省参数
    'lv_checkbox_set_text': {
        'args_map': [],
        'method_map': {
            'index': [],
            'mapping': {},
            'type': '',
            'default': 'text',
            'handler': None
        }
    },
    # 全缺省可免调用
    'lv_label_set_long_mode': {
        'args_map': ['LV_LABEL_LONG_WRAP'],
        'method_map': {
            'index': [],
            'mapping': {},
            'type': None,
            'default': 'long_mode',
            'handler': None
        }
    },
    # 不存在缺省参数
    'lv_image_set_src': {
        'args_map': [],
        'method_map': {
            'index': [],
            'mapping': {},
            'type': '',
            'default': 'src',
            'handler': None
        }
    },
    # 不存在缺省参数
    'lv_image_set_pivot': {
        'args_map': [],
        'method_map': {
            'index': [],
            'mapping': {},
            'type': '',
            'default': 'pivot',
            'handler': None
        }
    },
    # 全缺省可免调用
    'lv_image_set_rotation': {
        'args_map': ['0'],
        'method_map': {
            'index': [],
            'mapping': {},
            'type': None,
            'default': 'rotation',
            'handler': None
        }
    },
}


def get_method_from_map(all_omitted, args, method_map):
    """
    解析method映射表,返回是否省略调用、方法名和参数列表。
    :param all_omitted: 是否所有参数都被省略(True/False)。
    :param args: 处理后的参数列表。
    :param method_map: 方法的参数映射表,包含index、mapping、handler等键。
    :return: (omit_call, method_name, processed_args)
            第一个参数为True表示可以跳过,最后一个参数是包含了第一个参数,即组件自身,需要主动剔除掉
    """
    # 如果全缺省并且类型为None,那么就跳过函数调用
    if all_omitted and method_map.get('type') is None:
        return True, None, []

    # 获取默认method和handler
    default_method = method_map.get('default', '')
    handler = method_map.get('handler')

    if handler is None:
        # 如果handler不存在
        indices = method_map.get('index', [])
        if not indices:
            # 如果是无参数映射,直接返回默认方法和参数
            return False, default_method, args

        # 处理单参数映射,取第一个索引
        index = indices[0]
        if index >= len(args):
            # 单参数映射超出范围,说明映射表需要修正
            raise ValueError(f"Invalid index {index} for method mapping")

        # 进行单参数映射
        param = args[index]
        mapping = method_map.get('mapping', {})
        method = mapping.get(param, default_method)

        if method == default_method:
            return False, default_method, args
        else:
            # 存在映射,则删除指定元素
            new_args = list(args)
            del new_args[index]
            return False, method, new_args
    else:
        # 如果handler存在(单参数映射或者多参数映射)
        indices = method_map.get('index', [])
        params = [args[i] for i in indices if i < len(args)]
        # 使用映射表里的lambda,输入索引(或者处理过的参数)、参数表和mapping,返回结果、method和参数
        # 映射表里的method的第一个参数要确保是判断结果,结果若为None,那么就使用默认method
        result, method, final_args = handler(params, args, method_map.get('mapping', {}))
        if result is None:
            method = default_method
        return False, method, final_args


def omit_parameters(args, default_values):
    """
    根据C++重载规则处理参数列表,省略符合条件的参数。

    :param args: 传入的参数列表
    :param default_values: 默认值列表,对应参数列表中最后几个参数的默认值
    :return: 处理后的参数列表和判断值(True表示全省略,False表示有参数未省略)
    """
    assert len(args) >= len(default_values), "参数列表长度必须大于等于默认值列表长度"
    # 默认值列表长度
    default_count = len(default_values)
    t = 0
    # 从后往前逐个检查参数是否符合默认值
    for i in range(default_count):
        pos = -(i + 1)
        if args[pos] == default_values[i]:
            t += 1
        else:
            break

    # 生成处理后的参数列表
    if t > 0:
        processed_args = args[:len(args) - t]
    else:
        processed_args = args  # 直接使用原列表,避免不必要的拷贝

    # 判断是否所有可省略参数都符合条件
    all_omitted = (t == default_count)
    return all_omitted, processed_args


def convert_function_args(func_name, args):
    """
    转换函数与形参:输入函数名和完整形参表,会返回生成好的C++函数
    :param func_name:
    :param args:为避免影响后续扩展,这里传输整个形参表
    :return: 如果处理正常,就会返回正确的C++函数代码(前面带点)
    """
    for funcs, config in function_handlers.items():
        # 匹配函数规则
        if func_name in funcs:
            # 处理参数规则
            args_result, handle_args = omit_parameters(args, config['args_map'])
            final_result, final_method, final_args = get_method_from_map(args_result, handle_args, config['method_map'])
            # 判断是否该省略调用
            if final_result:
                return None
            else:
                return f".{final_method}({', '.join(final_args[1:])})"

    # 其他未处理情况
    # TODO 暂时不处理,因为目前没有遇到需要处理这种情况
    # 出现这个错误,说明method映射表需要补充,在【function_handlers】字典里补充对应的{func_name}
    raise NotImplementedError(f"Function '{func_name}' not implemented.")
    # return None


# --------------------------------分步处理代码块------------------------------------


# 处理样式块,已经包含里样式的正确代码,不用在前面加上组件名
def convert_style_calls(func_name, args):
    """
    转换样式代码
    :param func_name:
    :param args:
    :return: converted(string) 返回的是转换好的样式代码,如果不需要那么就返回None
    """
    # 每个列表都有2~3个元素,左边是匹配的函数名(相当于键),第二个元素是默认值(为''表示没有),第三个元素是函数的简写(如果有的话)
    default_config = [
        ['border_width', "0"],
        ['text_color', 'lv_color_hex(0xffffff)'],
        ['text_opa', "255"],
        ['text_font', '', 'font'],
        ['text_line_space', "0"],
        ['text_align', 'LV_TEXT_ALIGN_CENTER'],
        ['bg_opa', "255"],
        ['pad_top', "0"],
        ['pad_right', "0"],
        ['pad_bottom', "0"],
        ['pad_left', "0"],
        ['shadow_width', "0"],
        ['image_recolor_opa', '255'],
        ['image_opa', '255'],
        ['text_letter_space', '0', 'letter_space']
    ]

    def list_get2(lst, default=None):
        """
        类似于字典的get方法,用于列表。这里默认去第三个元素
        :param lst: 要检查的列表
        :param default: 如果索引超出范围时返回的默认值
        :return: 列表中指定索引处的元素或默认值
        """
        try:
            return lst[2]
        except IndexError:
            return default

    # 这里能做到这么简洁是因为lvgl 9.2的所有样式API都只有三个参数,并且前缀相同
    match = re.match(r'lv_obj_set_style_(\w+)', func_name)
    if match is None:
        # 不应该出现的情况
        raise NotImplementedError(f"Function '{func_name}' not implemented.")

    prop = match.group(1)
    value = args[1]
    state = args[2]

    # 如果在默认配置表里找到了对应函数,那么就对照默认配置修改
    index = next((i for i, item in enumerate(default_config) if item[0] == prop), -1)
    # 处理状态,如果是默认状态,那么就不添加
    state = "" if state == 'LV_PART_MAIN|LV_STATE_DEFAULT' else f", {state}"
    if index != -1:
        prop = list_get2(default_config[index], prop)  # 从默认配置里看看有没有第三个元素(简写)
        value = "" if value == default_config[index][1] and state == "" else f"{value}"
        if state == "" and value == "":
            # 如果都为默认值,那么就跳过这次处理
            return None

    converted = f".{prop}({value}{state})"

    return converted


# --------------------------------解析代码------------------------------------
# -------------处理代码行----------------
def process_code_lines(code_lines):
    """
    处理代码行。把代码行里的信息全部存储到初始化信息表和关系表里
    :param code_lines: 已经做过处理,每行只包含代码行
    :note 向widget里添加['widget_name',[[func,[args]],……]]。其中组件名是已经去除ui->
    """
    for line in code_lines:
        # 词法解析,获取变量名、函数名(不为空)和参数表
        var, func_name, args = parse_function_line(line)
        # 剔除无关函数
        if 'events_init' in func_name:
            continue

        if var is None:
            # 初始化代码行:把组件信息提取到widgets里
            # lv_obj_set_scrollbar_mode(ui->blueCounter_cont_1, LV_SCROLLBAR_MODE_OFF);
            # ①先获取组件名称
            widget_name = extract_comp_name_from_var(args[0])
            # ②存储初始化信息  [['widget_name',['func_name',[args]],……],……]
            add_widget_function(widget_name, func_name, args)
        else:
            # 创建组件代码行:组件关系提取到widgets_relations里
            # ui->blueCounter_cont_1 = lv_obj_create(ui->blueCounter);
            # ①先获取父子组件名称
            child_name = extract_comp_name_from_var(var)
            parent_name = extract_comp_name_from_var(args[0])
            # ②获取组件类型
            widget_info = get_widget_info(line)
            # ③添加组件关系   [['child_name',[widget_info],'parent_name']]
            if not add_widget_relations(child_name, widget_info, parent_name):
                # 存在预期外的重复组件关系
                raise ValueError("Failed to add widget relations")


# ----------------解析组件信息-------------------
def iterate_widgets(screen_name):
    """遍历组件结构的通用函数
    Note:
        # 1,链式调用需要一张表,组件在前面,然后init后跟父组件,接着不断往后添加到列表里
            ['widget_name.init(parent_name)','.pos()','size()',]
        # 2,根据实际情况,组件名称前需要'\n\t\t',两个Tab,而其他链式调用则需要三个Tab
        # 3,对widgets_define添加了组件定义的信息,但是没有把里面的代码通过'\n\t'连接
    Args:
        screen_name(string): 项目名称,这里是为了把屏幕名称替换为scr
    Returns:
        widgets_init_code(list):由一段段组件初始化代码组成,最后是需要通过'\n\t'进行拼接
    """
    widgets_init_code = []
    # 处理屏幕组件
    screen_widget = find_widget(screen_name)
    screen_func_list = screen_widget[1]
    screen_init_chain = []
    # 剔除屏幕组件
    remove_widget(screen_name)
    for scr_func_info in screen_func_list:
        func_name = scr_func_info[0]
        args = scr_func_info[1]
        if 'lv_obj_set_style' in func_name:
            # 如果是样式函数
            func_code = convert_style_calls(func_name, args)
            if func_code is None:
                continue
            screen_init_chain.append(func_code)
    # 给组件的第一个样式函数前加上scr
    screen_init_chain[0] = '\t\tscr' + screen_init_chain[0]
    screen_init_chain[-1] += ';'  # 末尾添加分号
    widgets_init_code.append('\n\t\t\t'.join(screen_init_chain))

    # 处理其他组件
    for widget in widgets:
        widget_name = widget[0]
        function_list = widget[1]
        # 链式调用:创建组件的代码
        widget_relation = find_relations(child_name=widget_name)
        widget_info = widget_relation[1]
        parent_name = widget_relation[2]
        # 转为合成名
        widget_name_var_name = widget_info[0] + '_' + widget_name
        if parent_name == screen_name:
            # 空即默认主屏幕
            parent_name_var_name = ''
        else:
            parent_relation = find_relations(child_name=parent_name)
            parent_widget_info = parent_relation[1]
            parent_name_var_name = parent_widget_info[0] + '_' + parent_name
        widget_init_chain = [f'\n\t\t{widget_name_var_name}.init({parent_name_var_name})']
        # 组件定义
        widgets_define.append(f'\t{widget_info[1]} {widget_name_var_name};')
        for func_info in function_list:
            func_name = func_info[0]
            args = func_info[1]

            # 获得处理过后的C++函数代码
            if 'lv_obj_set_style' in func_name:
                # 如果是样式函数
                func_code = convert_style_calls(func_name, args)
            else:

                func_code = convert_function_args(func_name, args)
            extract_resource(func_name, args)# 提取字体、图片资源

            # 判断是否跳过调用
            if func_code is None:
                print(f'Skipped function: \t{extract_comp_name_from_var(args[0])}\t{func_name}')
                continue
            else:
                widget_init_chain.append(func_code)

        # 拼接为组件初始化代码
        widget_init_chain[-1] += ';'  # 末尾添加分号
        widgets_init_code.append('\n\t\t\t'.join(widget_init_chain))
    # 给定义组件代码的首元素添加\n
    # widgets_define[0] = '\n' + widgets_define[0]
    return widgets_init_code,widgets_define




# ---------------------------------------文件处理函数-------------------------------------------
def find_c_functions(func_name: str,
                     search_path: str,
                     file_name: str = "setup_scr_*.c") -> Tuple[Tuple[str, List[str], str], ...]:
    """搜索指定C文件并返回结构化函数信息

    Args:
        func_name: 支持通配符的函数名(如"set_src_*")
        search_path: 文件搜索根路径
        file_name: 文件名匹配模式(原pattern参数)

    Returns:
        元组格式: (
            ("函数名", ["参数1类型 参数1", ...], "去花括号函数体"),
            ...
        )

    Raises:
        FileNotFoundError: 文件未找到时抛出
        ValueError: 多个文件匹配或未找到函数时抛出
    """
    target_file = Path(search_path).glob(file_name)
    matched_files = list(target_file)

    # 文件校验
    if len(matched_files) == 0:
        raise FileNotFoundError(f"No files match '{file_name}' in {search_path}")
    if len(matched_files) > 1:
        raise ValueError(f"Multiple files match '{file_name}': {matched_files}")

    # 读取文件内容
    with open(matched_files[0], 'r', encoding='utf-8') as f:
        content = f.read()

        # 未指定函数名时返回完整内容
    if not func_name:
        return (("full_content", [], content),)

    # 构建正则表达式模式
    name_pattern = re.escape(func_name).replace(r'\*',  '.*').replace(r'\?', '.')
    func_regex = re.compile(
        r'^\s*((?:[\w_]+\s+)+?)'  # 返回类型 
        rf'({name_pattern})\s*'     # 函数名 
        r'\(([^)]*)\)\s*'           # 参数列表 
        r'\{',
        re.MULTILINE
    )

    functions = []
    for match in func_regex.finditer(content):
        # 提取函数信息
        func_name = match.group(2)
        params = [p.strip() for p in match.group(3).split(',')  if p.strip()]

        # 提取函数体(去花括号)
        start = match.end()
        brace_level = 1
        end_pos = None

        for i in range(start, len(content)):
            if content[i] == '{': brace_level +=1
            elif content[i] == '}': brace_level -=1

            if brace_level == 0:
                end_pos = i
                break

        if end_pos is None:
            continue  # 忽略不完整定义

        # 清理函数体格式
        func_body = content[start:end_pos].strip()
        func_body = re.sub(r'^\s*{ |\s*}$', '', func_body, flags=re.DOTALL).strip()

        functions.append((func_name,  params, func_body))

    if not functions:
        raise ValueError(f"No functions matching '{func_name}' found")

    return tuple(functions)

def extract_screen_name(func_name: str) -> str:
    """
    从函数名里提取屏幕名称
    :param func_name:
    :return:
    """
    match = re.search(r'setup_scr_(\w+)', func_name)
    if match:
        return match.group(1)
    else:
        raise ValueError("No project name found")

# ----------------------------------输出结果处理--------------------------------------------
"""
UI代码转换系统 v2.5 
简介:
再看一眼就要爆炸
功能特性:
1. 模板驱动的代码结构生成 
2. 智能依赖关系解析(去除)
3. 多级代码校验机制 (去除)
4. 可扩展的钩子系统 
5. 分布式文件输出支持 
"""

class GenerateMode(Enum):
    """生成模式枚举"""
    OVERWRITE = 1
    MERGE = 2  # 新增融合模式,移除其他模式

@dataclass
class CppCodeTemplate:
    """
    代码模板配置容器
    Attributes:
        framework (str): cpp代码框架模板
        widgets_namespace (str): 组件定义命名空间模板
        init_namespace (str): 初始化逻辑命名空间模板
        function_template (str): 函数结构模板
        ui_interface (str): ui接口
    """
    framework: str = """\
{pre_define}
/*!INCLUDES_BEGIN!*/
{includes}

/*!INCLUDES_END!*/

// 组件定义 
{widgets_namespace}
{post_define}

/*!DECLARE_CODE_BEGIN!*/
// 全局变量定义

// 类声明

/*!DECLARE_CODE_END!*/

{pre_init}
{init_namespace}
{post_init}

{ui_interface}

/*!DEFINE_CODE_BEGIN!*/
// 类定义

/*!DEFINE_CODE_END!*/
"""
    widgets_namespace: str = """\
namespace {ns_name} 
{{
{define_blocks}
/*!WIDGETS_DEFINE_BEGIN!*/

/*!WIDGETS_DEFINE_END!*/
}}
using namespace {ns_name};"""


    init_namespace: str = """\
namespace {ns_name}
{{
    void {func_name}() 
    {{
{init_blocks}
    /*!INIT_CUSTOM_BEGIN!*/

    /*!INIT_CUSTOM_END!*/
    }}
    
    void events()
    {{
    /*!EVENTS_CUSTOM_BEGIN!*/
    
    /*!EVENTS_CUSTOM_END!*/
    }}
}}
"""

    ui_interface: str = """\
/*!UI_IMPLEMENT_BEGIN!*/
// 用户自定义接口实现区(安全编辑区) 
namespace {ns_name} {{


}}
/*!UI_IMPLEMENT_END!*/
"""

    # 初始化函数模板
    function_template: str = """\
    void {func_name}() 
    {{
{func_body}
    }}
    """


@dataclass
class HppCodeTemplate:
    """
    代码模板配置容器
    Attributes:
        framework (str): cpp代码框架模板
        widgets_namespace(str): 声明组件的模板
        ui_interface(str): ui接口
    Note:
        declare_namespace和ui_interface需要借助本类里的数据类型
    """
    framework: str = """\
{pre_define}
// Auto-generated UI Code, converted from GUI Guider
{includes}
 
{widgets_namespace}

// ------定义UI接口------
{ui_interface}

// ------加载资源------
{load_resources}  
"""

    widgets_namespace: str = """\
namespace {ns_name}
{{
{declare_blocks}
/*!WIDGETS_DECLARE_BEGIN!*/

/*!WIDGETS_DECLARE_END!*/
}}  
"""

    ui_interface: str = """\
/*!UI_INTERFACE_BEGIN!*/
// 用户自定义接口声明区(安全编辑区)
namespace {ns_name} {{


}}
/*!UI_INTERFACE_END!*/
"""


class HookSystem:
    """钩子管理系统"""

    HOOK_POINTS = (
        'pre_define',   # 定义块生成前
        'post_define',  # 定义块生成后
        'pre_init',     # 初始化块生成前
        'post_init',    # 初始化块生成后
        'post_generate'# 全部生成完成后
    )

    def __init__(self):
        self.hooks  = {point: [] for point in self.HOOK_POINTS}

    def add_hook(self, point: str, callback: Callable) -> None:
        """
        注册钩子函数
        Args:
            point: 钩子点名称,必须是HOOK_POINTS成员
            callback: 无参数的回调函数,返回字符串将插入代码
        """
        if point not in self.HOOK_POINTS:
            raise ValueError(f"无效钩子点: {point}")
        self.hooks[point].append(callback)

    def execute_hooks(self, point: str) -> str:
        """执行钩子并返回第一个非空的结果,如果没有结果则返回空字符串"""
        results = [hook() for hook in self.hooks.get(point, [])]
        # 过滤掉 None 和空字符串,并返回第一个非空结果,如果没有结果则返回空字符串
        filtered_results = list(filter(None, results))
        return filtered_results[0] if filtered_results else ''


def _build_load_resources(resources, is_custom=True) -> str:
    """构建资源加载代码"""
    resources_list = []

    # 处理字体资源
    font_resources = []
    for font in resources.get('font',  []):
        # 应用custom前缀规则
        if is_custom:
            processed_font = font.replace("lv_font",  "lv_customer_font", 1)  # 仅替换第一个匹配项
        else:
            processed_font = font
        font_resources.append(f"LV_FONT_DECLARE({processed_font})")

        # 添加字体区块
    if font_resources:
        resources_list.append("//  字体资源")
        resources_list.extend(font_resources)

        # 处理图片资源
    img_resources = [f"LV_IMG_DECLARE({img})" for img in resources.get('image',  [])]

    # 添加图片区块
    if img_resources:
        resources_list.append("//  图片资源")
        resources_list.extend(img_resources)

        # 生成最终代码
    content = "\n".join(resources_list)
    return content


class TemplateGenerator:
    """模板驱动代码生成器"""

    def __init__(self):
        """
        Args:
        """
        self.cpp_template  = CppCodeTemplate()
        self.hpp_template  = HppCodeTemplate()
        self.placeholders  = {
            'cpp_includes': '#include "ui.hpp"',
            'hpp_includes': '#include "GUI_Base.hpp"',
            'widgets_namespace': 'gui::widgets::main',
            'init_namespace': 'gui::init',
            'func_name': 'screen',
            'ui_interface':'gui::interface'
        }
        self.hooks  = HookSystem()

    def generate(
            self,
            define_blocks: List[str],
            init_blocks: List[str],
            output_path: str,
            is_font_custom: bool = True,
            mode: GenerateMode = GenerateMode.OVERWRITE
    ) -> bool:
        """
        执行代码生成流程
        Args:
            define_blocks: 组件定义代码块列表
            init_blocks: 初始化代码块列表
            output_path: 输出文件路径(最后不用带反斜杠)
            is_font_custom: 决定生成字体声明时,要不要使用自定义的字体类型。默认使用
            mode: 生成模式
        Returns:
            生成是否成功
        """
        # 阶段1:预处理
        # 校验以后可能有用,拓扑排序适合前面的代码处理
        # validated_defines = self._validate_blocks(define_blocks)
        # ordered_inits = self.resolver.get_ordered_blocks(init_blocks)

        # 阶段2:构建代码结构
        # hpp
        declare_widgets_content = self._build_declare_widgets_content(define_blocks)
        declare_ui_interface = self._build_declare_ui_interface([])
        load_resources = _build_load_resources(resources,is_font_custom)
        # cpp
        define_widgets_content = self._build_define_widgets_content(define_blocks)
        init_content = self._build_init_content(init_blocks, mode)
        define_ui_interface = self._build_define_ui_interface([])

        # 阶段3:生成最终代码
        # hpp
        final_hpp_code = self.hpp_template.framework.format(
            pre_define=self.hooks.execute_hooks('pre_define'),
            # 命名空间等
            includes=self.placeholders['hpp_includes'],
            widgets_namespace=declare_widgets_content,
            ui_interface= declare_ui_interface,
            load_resources=load_resources,
        )
        # cpp
        final_cpp_code = self.cpp_template.framework.format(
            # 一群钩子
            pre_define = self.hooks.execute_hooks('pre_define'),
            post_define=self.hooks.execute_hooks('post_define'),
            pre_init=self.hooks.execute_hooks('pre_init'),
            post_init=self.hooks.execute_hooks('post_init'),
            # 组件可以和初始化命名空间
            includes=self.placeholders['cpp_includes'],
            widgets_namespace=define_widgets_content,
            init_namespace=init_content,
            ui_interface=define_ui_interface,
        )

        # 阶段4:后处理
        print(self.hooks.execute_hooks('post_generate'))
        hpp_path = Path(output_path) / 'ui.hpp'
        cpp_path = Path(output_path) / 'ui.cpp'

        if mode == GenerateMode.OVERWRITE:
            hpp_path.write_text(final_hpp_code,  encoding='utf-8')
            cpp_path.write_text(final_cpp_code,  encoding='utf-8')
        elif mode == GenerateMode.MERGE:
            # 处理hpp文件
            if hpp_path.exists():
                existing_hpp = hpp_path.read_text(encoding='utf-8')
                merged_hpp = self._merge_code(existing_hpp, final_hpp_code, 'hpp')
                hpp_path.write_text(merged_hpp,  encoding='utf-8')
            else:
                hpp_path.write_text(final_hpp_code,  encoding='utf-8')
            # 处理cpp文件
            if cpp_path.exists():
                existing_cpp = cpp_path.read_text(encoding='utf-8')
                merged_cpp = self._merge_code(existing_cpp, final_cpp_code, 'cpp')
                cpp_path.write_text(merged_cpp,  encoding='utf-8')
            else:
                cpp_path.write_text(final_cpp_code,  encoding='utf-8')
        return True

    def _build_define_widgets_content(self, blocks: List[str]) -> str:
        """构建定义部分代码"""
        content = '\n'.join(blocks)
        return self.cpp_template.widgets_namespace.format(
            ns_name=self.placeholders['widgets_namespace'],
            define_blocks=content
        )

    def _build_declare_widgets_content(self, blocks: List[str]) -> str:
        """构建声明部分代码
        Note:
            输入的是定义代码块列表
        """
        declare_list = ['\textern '+ line for line in blocks]
        content = '\n'.join(declare_list)
        return self.hpp_template.widgets_namespace.format(
            ns_name=self.placeholders['widgets_namespace'],
            declare_blocks=content
        )

    def _build_declare_ui_interface(self, blocks: List[str]) -> str:
        """构建UI接口声明代码
        Note:
            输入的列表是外部声明的ui接口
        """
        ui_interface_declare = ['extern '+ line for line in blocks if line != '']
        content = '\n'.join(ui_interface_declare)
        return self.hpp_template.ui_interface.format(
            ns_name=self.placeholders['ui_interface'],
            declare_blocks=content
        )

    def _build_define_ui_interface(self, blocks: List[str]) -> str:
        """构建ui接口定义代码

        Note:
            ui定义代码,blocks的每个元素都是一段代码定义
        """
        content = '\n'.join(blocks)
        return self.cpp_template.ui_interface.format(
            ns_name=self.placeholders['ui_interface'],
            define_blocks=content
        )

    def _build_init_content(self, blocks: List[str], mode: GenerateMode) -> str:
        func_body = textwrap.indent('\n'.join(blocks), '')

        # 在初始化函数外围添加标记
        return self.cpp_template.init_namespace.format(
            ns_name=self.placeholders['init_namespace'],
            func_name=self.placeholders['func_name'],
            init_blocks=func_body
        )

    def _merge_code(self, existing: str, new: str, file_type: str) -> str:
        """智能合并策略"""
        # 定义保留规则(目前只有KEEP_WHOLE)可以使用
        preserve_rules = {
            'hpp': [
                (r'/\*!UI_INTERFACE_BEGIN!.*?\*!UI_INTERFACE_END!', 'KEEP_WHOLE'),  # 全保留
                (r'/\*!WIDGETS_DECLARE_BEGIN!.*?\*!WIDGETS_DECLARE_END!', 'KEEP_WHOLE')  # 合并内容
            ],
            'cpp': [
                (r'/\*!INCLUDES_BEGIN!.*?\*!INCLUDES_END!', 'KEEP_WHOLE'),
                (r'/\*!WIDGETS_DEFINE_BEGIN!.*?\*!WIDGETS_DEFINE_END!', 'KEEP_WHOLE'),
                (r'/\*!DECLARE_CODE_BEGIN!.*?\*!DECLARE_CODE_END!', 'KEEP_WHOLE'),
                (r'/\*!INIT_CUSTOM_BEGIN!.*?\*!INIT_CUSTOM_END!', 'KEEP_WHOLE'),
                (r'/\*!EVENTS_CUSTOM_BEGIN!.*?\*!EVENTS_CUSTOM_END!', 'KEEP_WHOLE'),
                (r'/\*!UI_IMPLEMENT_BEGIN!.*?\*!UI_IMPLEMENT_END!', 'KEEP_WHOLE'),
                (r'/\*!DEFINE_CODE_BEGIN!.*?\*!DEFINE_CODE_END!', 'KEEP_WHOLE'),

            ]
        }

        for pattern, mode in preserve_rules.get(file_type, []):
            # 查找现有代码中的匹配区域
            old_match = re.search(pattern, existing, re.DOTALL)
            if not old_match:
                continue

            # 根据模式处理
            if mode == 'KEEP_WHOLE':
                # 用旧区域完全替换新区域
                new = re.sub(pattern, old_match.group(0), new, count=1, flags=re.DOTALL)
            elif mode == 'MERGE_CONTENT':
                # 提取旧内容并插入新框架
                old_content = self._extract_inner_content(old_match.group(0))
                new = self._replace_inner_content(new, pattern, old_content)
            elif mode == 'KEEP_CONTENT':
                # 保留旧内容但保持新框架
                old_content = self._extract_inner_content(old_match.group(0))
                new = self._replace_inner_content(new, pattern, old_content, keep_markers=True)

        return new

    def _extract_inner_content(self, text: str) -> str:
        """提取两个标记之间的内容(不含标记本身)"""
        match = re.search(r'(?<=BEGIN!)[\s\S]*?(?=END!)', text, re.DOTALL)
        return match.group(0).strip() if match else ''

    def _replace_inner_content(self, new_code: str, pattern: str, content: str, keep_markers=False) -> str:
        """替换区域内部内容"""
        def replacer(match):
            if keep_markers:
                return f"{match.group(1)}{content}{match.group(3)}"
            return f"{match.group(1)}{content}{match.group(3)}"

        return re.sub(
            r'(/\*!.*?_BEGIN!)(.*?)(\*!.*?_END!*/)',
            replacer,
            new_code,
            flags=re.DOTALL,
            count=1
        )




# -------------------------------------主函数--------------------------------------------
def main():
    # 【定位屏幕初始化代码】:定位 setup_scr_* 函数,并获取工程名和函数体
    c_content = find_c_functions(search_path="E:\Program\Embedded\MCU\GUI\GUI\generated",
                           func_name="setup_scr_*",
                           file_name="setup_scr_*.c")
    # 获取第一个满足条件的函数
    setup_scr_function = c_content[0]
    # 获取函数体和屏幕名称
    screen_name = extract_screen_name(setup_scr_function[0])
    func_body = setup_scr_function[2]
    # 添加屏幕组件关系
    add_widget_relations(screen_name)

    # 【切割代码行】:从setup_scr_*函数体代码提取出代码行
    code_lines = extract_c_code_lines(func_body)

    # 【处理代码行】:从每行代码里提取到组件初始化信息和组件关系
    process_code_lines(code_lines)

    # 【解析信息】:把组件信息解析为对应的代码,并输出
    widgets_init_code,widgets_define_code = iterate_widgets(screen_name)

    # 【输出代码文件】:把组件相关代码信息,按照特定格式写入文件中
    generator = TemplateGenerator()

    # 添加版本信息钩子
    generator.hooks.add_hook('pre_define',  lambda: f"// Generated by Fairy on {datetime.now():%Y-%m-%d  %H:%M}\n")

    # 执行生成(这个输出路径有些怪,仿佛认定它的父目录所在目录才是相对起始路径)
    generator.generate(
        define_blocks=widgets_define_code,
        init_blocks=widgets_init_code,
        output_path=".",
        is_font_custom=False,
        mode=GenerateMode.OVERWRITE
    )

    print("代码生成成功!输出文件:ui.cpp")




if __name__ == "__main__":
    # 在执行其他命令之前设置代码页为UTF-8
    os.system('chcp 65001')
    main()
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐