轻量级多级菜单控制框架(C语言)

前言

作为嵌入式软件开发,可能经常会使用命令行或者显示屏等设备实现人机交互的功能,功能中通常情况都包含 UI 菜单设计;对于复杂的UI设计,可能最多优先考虑的是使用开源的GUI库。但是GUI使用起来复杂,在简单的UI设计中则臃肿或者较难实现(比如OLED这种);基于这种情况,很多开发人员都会有自己的菜单框架模块,避免重复造轮子,网上有很多这种菜单框架的代码,但是大多耦合性太强。

代码层面上大部分都耦合了按键和不同平台(不同尺寸的OLED)等模块;并无法独立出来适配不同的菜单设计。而多级菜单的设计也使得上层软件被迫耦合,比如一张表包含了多级菜单内容等。

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

特点

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

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

介绍

多级菜单

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

数组元素内容有:

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

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

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

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

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

菜单选项的扩展数据

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

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

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

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

显示效果

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

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

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

可扩展

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

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

可配置

功能多样化

多语种。

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

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

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

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

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

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

使用说明

菜单初始化和使用

// 定义菜单信息,函数由主菜单模块定义并提供
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(); // 周期调度
        }
    }
}

主菜单定义和绑定

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

// 扩展数据为图标文件名字
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);
}

子菜单定义和绑定

如果菜单选项有子菜单,则该菜单选项调用 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); 
}

菜单控制

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

// 需要先进入主菜单
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

声明:本内容为作者独立观点,不代表电子星球立场。未经允许不得转载。授权事宜与稿件投诉,请联系:editor@netbroad.com
觉得内容不错的朋友,别忘了一键三连哦!
赞 1
收藏 2
关注 23
成为作者 赚取收益
全部留言
0/200
成为第一个和作者交流的人吧