用C语言写一个耦合性低、完全可移植的轻量级菜单框架

本文介绍了一个轻量级的嵌入式菜单框架,它具有低耦合性,能适应不同显示平台和菜单设计,支持多级菜单、多语种切换以及自定义显示效果,提升程序移植性和解耦能力。提供示例代码和下载链接供开发者参考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

作为嵌入式软件开发,可能经常会使用命令行或者显示屏等设备实现人机交互的功能,功能中通常情况都包含 UI 菜单设计;对于复杂的UI设计,可能最多优先考虑的是使用开源的GUI库。

但GUI使用起来复杂,在简单的UI设计中则臃肿或者较难实现(比如OLED这种);基于这种情况,很多开发人员都会有自己的菜单框架模块,避免重复造轮子,网上有很多这种菜单框架的代码,但是大多耦合性太强。

代码层面上大部分都耦合了按键和不同平台(不同尺寸的OLED)等模块;并无法独立出来适配不同的菜单设计。

而多级菜单的设计也使得上层软件被迫耦合,比如一张表包含了多级菜单内容等。

基于以上种种痛点,本文介绍一个耦合性低、完全可移植的轻量级菜单框架,菜单显示风格和显示平台完全由自己根据需求设计,而菜单操作统一由菜单框架处理即可,提高程序的移植性。

二、特点

主要特点就是耦合性低,移植无需修改。且不和任何模块耦合,同时对于上层软件设计,也可以做到解耦实现。

可以为不同菜单设计不同的显示风格。

三、介绍

1、多级菜单

同级菜单以数组的方式体现,父菜单和子菜单的关联则使用链表实现。

数组元素内容有:

  • 菜单选项字符串描述(多语种可设置)

  • 菜单选项进入回调函数:当前菜单选项进入时(从父菜单进入)需要执行一次的函数

  • 菜单选项退出回调函数:当前菜单选项进入后退出时(退出至父菜单)需要执行一次的函数

  • 菜单选项重加载回调函数:当前菜单选项每次加载时(从父菜单进入或子菜单退出)需要执行一次的函数

  • 菜单选项周期调度回调函数:当前菜单选项的周期调度函数

  • 菜单选项的扩展数据

链表内存可以选择采用动态内存分配或者数组实现

方便对不同菜单界面功能解耦:

大部分菜单采用的都是数组中包含了所有不同级别的菜单选项内容实现,无法做到很好的解耦方式;

该模块通过动态绑定子菜单和链表的方式可以达到较好的解耦状态

2、显示效果

该框架只负责菜单选项控制操作,不负责在图像界面显示效果,需要在对应的回调函数中实现菜单显示效果。

设置对应的效果显示函数,即可为不同的菜单设置不同的菜单显示效果,比如图标式、列表式或右侧弹窗式等。

可以在不同显示平台体现,比如LCD、OLED或终端界面等。

3、可扩展

每级菜单选项都可以设置自定义数据,用来实现更多的菜单操作或者显示效果等。

不同级别的菜单可以设置自定义数据(比如菜单选项隐藏/图标数据等)。

4、可配置

配置选项描述
_COT_MENU_USE_MALLOC_定义则采用 malloc/free 的方式实现多级菜单, 否则通过数组的形式
_COT_MENU_USE_SHORTCUT_定义则启用快捷菜单选项进入功能
COT_MENU_MAX_DEPTH多级菜单深度
COT_MENU_MAX_NUM菜单支持的最大选项数目
COT_MENU_SUPPORT_LANGUAGE菜单支持的语种数目

5、功能多样化

多语种。

  • 支持菜单选项多语种切换,至少设置一种语言;

  • 多语种除了该方式,还可以使用多语种配置数据实现,比如键值对,键作为菜单选项字符串体现。

支持快速进入指定菜单界面。

  • 可以通过相对选项索引或者绝对选项索引路径实现。

可以实现有限界面内显示少量的菜单选项内容。

  • 有现成的函数可用,无需担心使用不同尺寸重新实现菜单选项部分可见。

四、使用说明

1、菜单初始化和使用

// 定义菜单信息,函数由主菜单模块定义并提供static cotMainMenuCfg_t sg_tMainMenu = {{"主菜单", "Main Menu"}, Hmi_EnterMainHmi, NULL, NULL, NULL};
int main(void){    cotMenu_Init(&sg_tMainMenu);
    while (1)    {        ...
        if (timeFlag)        {            timeFlag = 0;            cotMenu_Task(); // 周期调度        }    }}
 

2、主菜单定义和绑定

定义一个主菜单选项内容、主菜单显示效果函数和主菜单进入函数等。

// 扩展数据为图标文件名字cotMenuList_t sg_MainMenuTable[] = {    {{"音乐", "Music"},  Hmi_MusicEnter, Hmi_MusicExit, Hmi_MusicLoad, Hmi_MusicTask, "music"},    {{"视频", "Video"},  NULL, Hmi_VideoExit, Hmi_VideoLoad, Hmi_VideoTask, "video"},    {{"摄像机", "Camera"},  Hmi_CameraEnter, Hmi_CameraExit, Hmi_CameraLoad, Hmi_CameraTask, "camera"},    {{"设置", "Setting"}, Hmi_SetEnter, Hmi_SetExit, Hmi_SetLoad,   Hmi_SetTask, "setting"},};
/* 主菜单显示效果 */static void ShowMainMenu(cotMenuShow_t *ptShowInfo){    char *pszSelectDesc = ptShowInfo->pszItemsDesc[ptShowInfo->selectItem];    oledsize_t idx = (128 - 6 * strlen(pszSelectDesc)) / 2;
    cotOled_DrawGraphic(40, 0, (const char *)ptShowInfo->pItemsExData[ptShowInfo->selectItem], 1);
    cotOled_SetText(0, 50, "                ", 0, FONT_12X12);    cotOled_SetText(idx, 50, pszSelectDesc, 0, FONT_12X12);}
void Hmi_EnterMainHmi(void){    cotMenu_Bind(sg_MainMenuTable, COT_GET_MENU_NUM(sg_MainMenuTable), ShowMainMenu);}

3、子菜单定义和绑定

如果菜单选项有子菜单,则该菜单选项调用 cotMenu_Enter,进入回调函数不能为NULL,且该回调函数需调用 cotMenu_Bind进行绑定。

/* 设置的子菜单内容 */cotMenuList_t sg_SetMenuTable[] = {    {{"语言", "Language"},   NULL, NULL, NULL, OnLanguageFunction, NULL},    {{"蓝牙", "Bluetooth"},  NULL, NULL, NULL, OnBluetoothFunction, NULL},    {{"电池", "Battery"},    NULL, NULL, NULL, OnBatteryFunction, NULL},    {{"储存", "Store"},      NULL, NULL, NULL, OnStorageFunction, NULL},    {{"更多", "More"},       Hmi_MoreSetEnter, Hmi_MoreSetExit, Hmi_MoreSetLoad, Hmi_MoreSetTask, NULL},};
/* 设置菜单显示效果 */static void ShowSetMenu(cotMenuShow_t *ptShowInfo){    uint8_t showNum = 3;    menusize_t  tmpselect;
    cotMenu_LimitShowListNum(ptShowInfo, &showNum);
    printf("\e[0;30;46m ------------- %s ------------- \e[0m\n", ptShowInfo->pszDesc);
    for (int i = 0; i < showNum; i++)    {        tmpselect = i + ptShowInfo->showBaseItem;
        if (tmpselect == ptShowInfo->selectItem)        {            printf("\e[0;30;47m %d. %-16s\e[0m |\n", tmpselect + 1, ptShowInfo->pszItemsDesc[tmpselect]);        }        else        {            printf("\e[7;30;47m %d. %-16s\e[0m |\n", tmpselect + 1, ptShowInfo->pszItemsDesc[tmpselect]);        }    }}
void Hmi_SetEnter(void){    // 进入设置选项后绑定子菜单,同时为当前绑定的菜单设置显示效果函数    cotMenu_Bind(sg_SetMenuTable, COT_GET_MENU_NUM(sg_SetMenuTable), ShowSetMenu); }

4、菜单控制

通过调用相关函数实现菜单选项选择、进入、退出等。

​​​​​​​

// 需要先进入主菜单cotMenu_MainEnter();
// 选择上一个,支持循环选择(即第一个可跳转到最后一个)cotMenu_SelectPrevious(true);
// 选择下一个,不支持循环选择(即最后一个不可跳转到第一个)cotMenu_SelectNext(false);
// 进入,会执行菜单选项的 pfnEnterCallFun 回调函数cotMenu_Enter();
// 退出,会执行父菜单该选项的 pfnExitCallFun 回调函数,并在退出后父菜单选项列表复位从头选择cotMenu_Exit(true);

五、Demo显示效果

示例代码采用的平台是命令行输出输入显示效果:

demo中提供了如何实现图形菜单(主菜单有点粗糙)、普通列表菜单、右侧弹窗菜单(更多设置)等效果演示,菜单样式可自由扩展,足够自由;快捷菜单操作、中英文切换演示。(windows中编译需要将 demo.c转 GBK 编码,Linux 转 utf8 编码,不然可能出现汉字乱码的问题)。

图片

以下是通过单片机驱动 OLED 显示的菜单界面显示效果:

图片

六、下载链接

下载链接(点击阅读原文),或更新内容可看:

https://gitee.com/cot_package/cot_menu

LVGL (LittlevGL) 是一款轻量级的图形用户界面库,主要用于嵌入式系统如STM32、ESP32等微控制器平台。它提供了一套简单易用的API来创建和管理GUI界面,包括按键操作。 在LVGL中,切换界面通常涉及到以下几个步骤: 1. **初始化**: 首先,你需要初始化LVGL框架,包括配置屏幕尺寸、颜色主题等。 ```c lv_obj_t scr = lv_screen_create(); lv_style_set_bg_color(scr, LV_COLOR_GRAY); ``` 2. **添加布局**和**界面**: 创建一个新的页面或窗口,并添加所需的布局元素(比如小部件或滚动区域),这可以看作是一个"界面"。 ```c lv_page_t page = lv_page_create(); lv_layout_t layout = lv_layout_create(page); lv_obj_add(lv_page_get_content(page), layout); ``` 3. **定义按键功能**: 使用`lv_button_create()`函数创建按键,设置其文本和回调函数来处理按键事件。当按键被按下,你可以通过回调函数切换到另一个界面。 ```c lv_button btn = lv_button_create(lv_scr_load(), NULL); lv_obj_set_event_cb(btn, switch_to_next_screen); lv_obj_set_text(btn, "Next"); ``` 其中`switch_to_next_screen`是你自定义的函数,负责切换到下一个界面。 4. **响应按键事件**: 当用户按下按钮,LVGL会触发相应的事件处理器,执行你的切换逻辑。 ```c void switch_to_next_screen(lv_obj_t *obj, void* data) { if(obj->get_type() == LV_OBJ_TYPE_BUTTON && strcmp(obj->get_name(), "Next") == 0) { // 清理当前界面并加载新界面 lv_page_remove(page); load_new_screen(); } } ``` 5. **显示和隐藏**: 最后,通过`lv_obj_set_visible()`控制界面的可见性来切换它们。 ```c void load_new_screen() { lv_page_set_hidden(page, true); // 隐藏当前界面 lv_page_set_visible(next_screen, false); // 隐藏即将显示的界面 // 显示新的界面 lv_page_set_visible(next_screen, true); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值