【应用笔记】Cot Menu 轻量级多级菜单控制框架程序(C语言)

【应用笔记】Cot Menu 轻量级多级菜单控制框架程序(C语言)

前言: 工作需要, 实现一个串口打印的类shell菜单. 如果按照以往的习惯我会自己重新"构思"(狗屎)一个菜单框架.之前用oled和lcd时,我都从零重复造轮子.
作为一个成熟的程序员, 应该要学会使用前人大佬总结的框架库,既能学习也能省时间.
经过ai搜索,最终找到一个今年年初(2024年1月)刚发布的轻量级框架.经过一方学习后感觉很不错,故此写下笔记.

一、准备工作

  • 可以使用vc++6.0测试,也可以使用任意单片机测试,写好的代码是基本通用的.

Vc++安装包_Visual C++ 6.0中文版安装包下载及win11安装教程
https://blog.csdn.net/weixin_46274254/article/details/123255025

  • 推荐在线思维导图工具和在线ai搜索工具;

在线思维导图
https://www.jyshare.com/more/kitymind/index.html
智谱清言
https://chatglm.cn/main/alltoolsdetail?lang=zh

  • 下载源码,作者仓库里还有其他轻量级库.

gitee仓库 cot软件包/cotMenu
https://gitee.com/cot_package/cot_menu
轻量级多级菜单控制框架程序(C语言)
https://blog.csdn.net/qq_24130227/article/details/121167276
用C语言写一个耦合性低、完全可移植的轻量级菜单框架
https://blog.csdn.net/2401_82584055/article/details/139414585

二、介绍

  • cotmenu库下载后打开,里面只有一个例程examples和库的本体cot_menu.c, cot_menu.h.
  • 本体只有2个文件,总共不超过1000行代码. 极简-轻量级.
  • 本身只实现了对菜单的控制,比如多级菜单的进入和退出,不包含菜单按键的输入和处理,还有菜单界面的定义和输出等等.
  • 所以它既可以使用按键,串口,窗口作为输入,也可以使用屏幕,串口,窗口作为输出.这部分代码需要自己实现,库中只提供了一个指针.
  • 文件夹里包含一个examples文件夹,是在电脑端运行的例程.大致演示了效果.

三、实践

  • 第一步.先创建一个工程,然后添加库,编译运行,没有报错.
    在这里插入图片描述
  • 唯一需要注意的就是那几个c语言通用库;
// #include "stdint.h" // 这个库是为了定义数据类型,如果找不到可以直接替换成下面这些
typedef signed char int8_t;
typedef short int int16_t;
typedef int int32_t;

typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;

// #include <stdbool.h> // 这个库是为了定义bool,如果找不到可以直接使用枚举自己定义.
typedef enum Bool{
	false = 0,
	true = 1
}bool;

// 其他遇到再说...

1.规划多级菜单

  • 建议使用图表或是思维导图,随意罗列一下.假设我需要实现以下菜单结构.
    在这里插入图片描述

2.准备文本

  • 无论是使用串口打印,还是屏幕显示,都需要先准备好文本,条件允许还需要中英文双语可以选择.例程cot_menu-master\examples\language中就示例.其中使用到了枚举+数组的指定索引初始化的方法,我还是第一次见,我一直以为数组是不支持乱序初始化,没想到能采用这种语法.
  • 有点小尴尬的是在vc6.0中编译报错,应该不支持这种语法.在iar的单片机工程中倒是正常编译的.
  • 注意到我把英文放在0位,中文放在1位,是为了能代码对齐,所以把中文字样放一行的最后.
// A_language.c 文件内容如下
#include "A_language.h"

// 局部变量 语言类型
static SystemLanguage_e sg_eSystemLanguage = SYSTEM_LANGUAGE_ENGLISH;

// 局部数组 语言文本
const char *(sg_kSystemLanguage[TEXT_ALL])[SYSTEM_LANGUAGE_ALL] =
{
    /*[TEXT_MENU]     = */{"Main menu", "主菜单"},
        
    /*[TEXT_MENU1A]   = */{"L1A menu", "一级菜单A"},
    /*[TEXT_MENU1B]   = */{"L1B menu", "一级菜单B"},
    /*[TEXT_MENU1C]   = */{"L1C menu", "一级菜单C"},
    /*[TEXT_MENU1D]   = */{"L1D menu", "一级菜单D"},
    
    /*[TEXT_MENU2A]   = */{"L2A menu", "二级菜单A"},
    /*[TEXT_MENU2B]   = */{"L2B menu", "二级菜单B"},
    /*[TEXT_MENU2C]   = */{"L2C menu", "二级菜单C"},
    /*[TEXT_MENU2D]   = */{"L2D menu", "二级菜单D"},
    
    /*[TEXT_MENU3A]   = */{"L3A menu", "三级菜单A"},
    /*[TEXT_MENU3B]   = */{"L3B menu", "三级菜单B"},
    
    /*[TEXT_SELECT_OPTION]    = */{"select option",     "选择操作"},
    /*[TEXT_ENTER]            = */{"enter",             "进入"},
    /*[TEXT_EXIT]             = */{"exit",              "退出"},
    /*[TEXT_NEXT]             = */{"next",              "下一个"},
    /*[TEXT_PREVIOUS]         = */{"previous",          "上一个"},
};

// 设置当期语言类型
void set_language(SystemLanguage_e lang)
{
    if (lang >= 0 && lang < SYSTEM_LANGUAGE_ALL)
    {
        sg_eSystemLanguage = lang;
    }
}

// 获取当前语言类型
const char *get_text_by_language(SystemLanguage_e lang, TextId_e id)
{
    static const char *pszNullString = "N/A";

    if (id >= 0 && id < TEXT_ALL)
    {
        return sg_kSystemLanguage[id][lang];
    }

    return pszNullString; // 未找到对应的文本
}

// 获取文本
const char *get_text(TextId_e id)
{
    static const char *pszNullString = "N/A";

    if (id >= 0 && id < TEXT_ALL)
    {
        return sg_kSystemLanguage[id][sg_eSystemLanguage];
    }

    return pszNullString; // 未找到对应的文本
}

// A_language.h 文件内容如下
#ifndef LANGUAGE_H
#define LANGUAGE_H

// 声明全局枚举
typedef enum
{
    SYSTEM_LANGUAGE_ENGLISH = 0,
    SYSTEM_LANGUAGE_CHINESE,

    SYSTEM_LANGUAGE_ALL,
} SystemLanguage_e;

typedef enum
{
    TEXT_MENU = 0,
        
    TEXT_MENU1A,
    TEXT_MENU1B,
    TEXT_MENU1C,
    TEXT_MENU1D,
    
    TEXT_MENU2A,
    TEXT_MENU2B,
    TEXT_MENU2C,
    TEXT_MENU2D,
    
    TEXT_MENU3A,
    TEXT_MENU3B,
    
    TEXT_SELECT_OPTION,
    TEXT_ENTER,
    TEXT_EXIT,
    TEXT_NEXT,
    TEXT_PREVIOUS,

    TEXT_ALL,
} TextId_e;

// 声明全局函数
extern void set_language(SystemLanguage_e lang);    // 设置当期语言类型
extern const char *get_text(TextId_e id);           // 获取文本
extern const char *get_text_by_language(SystemLanguage_e lang, TextId_e id); // 获取当前语言类型

#endif

  • 然后编译没有问题
    在这里插入图片描述

3.分析框架

  • 打开cotmenu.h,开头是c语言头文件导入,略过.然后是宏定义配置项. 只有4个,原本注释也得明明白白了.可以右键搜索哪里使用.是数组长度的定义和函数的定义.
/******************************************* 配置项 ********************************************************************/

/* 定义 _COT_MENU_USE_MALLOC_ 则采用 malloc/free 的方式实现多级菜单, 否则通过数组的形式 */
// #define _COT_MENU_USE_MALLOC_

/* 定义 _COT_MENU_USE_SHORTCUT_ 则启用快捷菜单选项进入功能 */
#define _COT_MENU_USE_SHORTCUT_

/* 多级菜单深度 */
#define COT_MENU_MAX_DEPTH              10

/* 菜单支持的最大选项数目 */
#define COT_MENU_MAX_NUM                20

/******************************************* 配置项 ********************************************************************/
  • 然后直接跳到最后,看函数的定义. 我大致进行如下分类.

在这里插入图片描述

/* Exported functions ------------------------------------------------------------------------------------------------*/

/* 菜单初始化和反初始化 */

extern int cotMenu_Init(cotMainMenuCfg_t *pMainMenu);
extern int cotMenu_DeInit(void);

extern int cotMenu_Bind(cotMenuList_t *pMenuList, menusize_t menuNum, cotShowMenuCallFun_f pfnShowMenuFun);

/* 菜单选项显示时需要使用的功能扩展函数 */

extern int cotMenu_LimitShowListNum(cotMenuShow_t *ptMenuShow, menusize_t *pShowNum);
extern int cotMenu_QueryParentMenu(cotMenuShow_t *ptMenuShow, uint8_t level);

/* 菜单操作 */

extern int cotMenu_MainEnter(void);
extern int cotMenu_MainExit(void);

extern int cotMenu_Reset(void);
extern int cotMenu_Enter(void);
extern int cotMenu_Exit(bool isReset);
extern int cotMenu_SelectPrevious(bool isAllowRoll);
extern int cotMenu_SelectNext(bool isAllowRoll);
extern int cotMenu_Select(menusize_t selectItem);

extern int cotMenu_ShortcutEnter(bool isAbsolute, uint8_t deep, ...);

/* 菜单轮询处理任务 */

extern int cotMenu_Task(void);
  • 举个简单例子, 一开始使用框架先需要列出如下内容.
  • 要使用框架就先初始化, 然后进入菜单, 就可以使用菜单, 不需要时退出, 去初始化, 一条龙标准化流程.
#include <stdio.h> // printf

#include "A_language.h" // txt
#include "cot_menu.h" // menu

static cotMainMenuCfg_t sg_tMainMenu = {NULL, NULL, NULL, NULL, NULL}; // 主菜单对象

int main (void)
{
	printf("你好 世界\n");

    cotMenu_Init(&sg_tMainMenu);    // 初始化

    cotMenu_MainEnter();            // 进入

    cotMenu_Task();     			// 使用

    cotMenu_MainExit();             // 退出

    cotMenu_DeInit();               // 去除初始化
	
    return 0;
}

4.菜单对象

  • 注意到初始化时需要一个菜单对象(结构体)cotMainMenuCfg_t / cotMenuList_t.定义了2个名字,代表主菜单对象其实和子菜单对象是一样的内容格式.
  • 第一个uMenuDesc一般是文本,就是上面定义的中英文, 使用了共用体cotMenuDsecStr_u,具体存数和取数都是自己定义.
  • 最后一个pExtendData一般是一个自定义结构体指针,用来传递所有想传递的东西.也是具体存数和取数都是自己定义.
  • 中间的几个都回调函数,在不同情况下会调用.
  • 进入回调 pfnEnterCallFun, 就是在 cotMenu_MainEntercotMenu_Enter中调用;
  • 退出回调 pfnExitCallFun, 就是在 cotMenu_MainExitcotMenu_Exit中调用;
  • 加载回调 pfnLoadCallFun, 就是在cotMenu_Task轮询内的开头调用, 且只调用一次;
  • 轮询回调 pfnRunCallFun, 就是在cotMenu_Task轮询内的结尾调用, 且每次都会调用;
  • 可以进入上面几个函数中查看具体实现了什么内容.这里不啰嗦了.
  • 还有一个需要额外注册的函数,也是最重要的显示菜单函数,
  • 显示回调,pfnShowMenuFun,在cotMenu_Task轮询内的中间调用, 且每次都会调用;
  • 如果不额外赋值的话,就会沿用上一个显示回调, 在cotMenu_MainEntercotMenu_Enter中实现.
/**
  * @brief 菜单信息注册结构体
  * 
  */
typedef struct
{
    cotMenuDsecStr_u     uMenuDesc;        /*!< 当前菜单的描述 */

	    cotMenuCallFun_f     pfnEnterCallFun;  /*!< 当前菜单选项进入时(从父菜单进入)需要执行一次的函数, 为NULL不执行 */

    cotMenuCallFun_f     pfnExitCallFun;   /*!< 当前菜单选项进入后退出时(退出至父菜单)需要执行一次的函数, 为NULL不执行 */
    
    cotMenuCallFun_f     pfnLoadCallFun;   /*!< 当前菜单选项每次加载时(从父菜单进入或子菜单退出)需要执行一次的函数, 为NULL不执行 */

    cotMenuCallFun_f     pfnRunCallFun;    /*!< 当前菜单选项的周期调度函数 */

    void                *pExtendData;      /*!< 当前选项的菜单显示效果函数扩展数据入参, 可自行设置该内容 */
} cotMenuList_t, cotMainMenuCfg_t;

5.回调函数

  • 如果有具体想执行的内容, 回调函数可以直接写空NULL,我这里为了演示都写一下.
  • 注意,注册函数cotMenu_Bind的对象我填的是0,一会再补上,现在主要是看其他回调函数的使用方法.
// 轮询时每次执行
void Show_Main(cotMenuShow_t *ptShowInfo)
{
}

// 进入时执行一次
void Enter_Main(const cotMenuItemInfo_t *pItemInfo)
{
    // cotMenu_Bind(NULL, 0, Show_Main); // 注册函数, 注册显示函数, 放着执行一次的 load 或 enter 中即可.
}

// 退出时执行一次
void Exit_Main(const cotMenuItemInfo_t *pItemInfo)
{
}

// 轮询前执行一次
void Load_Main(const cotMenuItemInfo_t *pItemInfo)
{
    cotMenu_Bind(NULL, 0, Show_Main); // 注册函数, 注册显示函数, 放着执行一次的 load 或 enter 中即可.
}

// 轮询时每次执行
void Run_Main(const cotMenuItemInfo_t *pItemInfo)
{
}

// 主菜单对象
static cotMainMenuCfg_t sg_tMainMenu = {(void *)TEXT_MENU, // "main menu" // 这个位置是共用体,如果存枚举,取数时就按枚举取,如果存字符串指针,取数时就按字符串指针取数
                                        Enter_Main, //
                                        Exit_Main, 
                                        Load_Main, 
                                        Run_Main}; 

6.轮询函数

  • 打开ccot_menu.c,拉到最后看cotMenu_Task()函数.
  • 可以看到其实主要是就是调用了3个函数.
  • 加载回调pfnLoadCallFun, 显示回调pfnShowMenuFun, 轮询回调pfnRunCallFun.
  • 其中显示回调会先拷贝子菜单列表pMenuList,主要就是用来打印显示的.
  • 而这个显示回调pfnShowMenuFun是需要通过调用注册函数cotMenu_Bind进行修改赋值.
int cotMenu_Task(void)
{
    int i;
    cotMenuList_t *pMenuList;
    cotMenuShow_t tMenuShow;

    if (sg_tMenuManage.pMenuCtrl == NULL || sg_tMenuManage.isEnterMainMenu == 0)
    {
        return -1;
    }

    if (sg_tMenuManage.pfnLoadCallFun != NULL)
    {
        cotMenuItemInfo_t tItemInfo;

        tItemInfo.uMenuDesc = sg_tMenuManage.pMenuCtrl->uMenuDesc;
        tItemInfo.pExtendData = sg_tMenuManage.pMenuCtrl->pExtendData;
        sg_tMenuManage.pfnLoadCallFun(&tItemInfo);
        sg_tMenuManage.pfnLoadCallFun = NULL;
    }
    
    if (sg_tMenuManage.pMenuCtrl->pMenuList != NULL)
    {
        pMenuList = sg_tMenuManage.pMenuCtrl->pMenuList;
        tMenuShow.itemsNum = sg_tMenuManage.pMenuCtrl->itemsNum;
        tMenuShow.selectItem = sg_tMenuManage.pMenuCtrl->selectItem;
        tMenuShow.showBaseItem = sg_tMenuManage.pMenuCtrl->showBaseItem;

        tMenuShow.uMenuDesc = sg_tMenuManage.pMenuCtrl->uMenuDesc;
        tMenuShow.pExtendData = sg_tMenuManage.pMenuCtrl->pExtendData;

        for (i = 0; i < tMenuShow.itemsNum && i < COT_MENU_MAX_NUM; i++)
        {
            tMenuShow.uItemsListDesc[i] = pMenuList[i].uMenuDesc;
            tMenuShow.pItemsListExtendData[i] = pMenuList[i].pExtendData;
        }

        if (sg_tMenuManage.pMenuCtrl->pfnShowMenuFun != NULL)
        {
            sg_tMenuManage.pMenuCtrl->pfnShowMenuFun(&tMenuShow);
        }

        sg_tMenuManage.pMenuCtrl->showBaseItem = tMenuShow.showBaseItem;
    }

    if (sg_tMenuManage.pMenuCtrl->pfnRunCallFun != NULL)
    {
        cotMenuItemInfo_t tItemInfo;

        tItemInfo.uMenuDesc = sg_tMenuManage.pMenuCtrl->uMenuDesc;
        tItemInfo.pExtendData = sg_tMenuManage.pMenuCtrl->pExtendData;
        sg_tMenuManage.pMenuCtrl->pfnRunCallFun(&tItemInfo);
    }

7.注册函数

  • 主菜单的内容基本就这些,接下来就是子菜单相关的内容.使用注册函数写入子菜单数据.
  • 子菜单的格式和主菜单的格式是一样的.就不赘述.
  • 总结,注册函数cotMenu_Bind就是写入子菜单数组sg_Menu1_Table, 总长度COT_GET_MENU_NUM(sg_Menu1_Table), 和显示回调函数Show_Main.
/* 一级菜单 */
cotMenuList_t sg_Menu1_Table[] = 
{
    COT_MENU_ITEM_BIND(TEXT_MENU1A,  NULL, NULL, NULL, NULL, NULL),
    COT_MENU_ITEM_BIND(TEXT_MENU1B,  NULL, NULL, NULL, NULL, NULL),
    COT_MENU_ITEM_BIND(TEXT_MENU1C,  NULL, NULL, NULL, NULL, NULL),
    COT_MENU_ITEM_BIND(TEXT_MENU1D,  NULL, NULL, NULL, NULL, NULL),
    /* 坑爹的vc6.0可能不支持指定结构体元素初始化,所以可以改成下面的写法.    
    {(void *)TEXT_MENU1A, NULL, NULL, NULL, NULL, NULL},
    {(void *)TEXT_MENU1B, NULL, NULL, NULL, NULL, NULL},
    {(void *)TEXT_MENU1C, NULL, NULL, NULL, NULL, NULL},
    {(void *)TEXT_MENU1D, NULL, NULL, NULL, NULL, NULL},
    */
};
// 轮询前执行一次
void Load_Main(const cotMenuItemInfo_t *pItemInfo)
{
    cotMenu_Bind(sg_Menu1_Table, COT_GET_MENU_NUM(sg_Menu1_Table), Show_Main); // 注册函数, 注册显示函数, 放着执行一次的 load 或 enter 中即可.
}

8.显示回调

  • 如果这里面写的LCD程序,那效果就是显示器菜单,如果写的串口,就是shell菜单.
  • 而且想显示横版还是竖版,亦或是图片都可以自己实现.
  • 下面是个简易的轮询显示.
// 轮询时每次执行
void Show_Main(cotMenuShow_t *ptShowInfo)
{
    uint8_t showNum = 3; // 显示最大行数
    cotMenu_LimitShowListNum(ptShowInfo, &showNum); // 根据设定截取菜单内容

    printf("\n ------------- %s ------------- \n", get_text((TextId_e)ptShowInfo->uMenuDesc.textId)); // 显示总标题

    for (int i = 0; i < showNum; i++)   // 轮询显示菜单
    {
        menusize_t tmpselect = i + ptShowInfo->showBaseItem; // 计算当前实际行数

        printf(" %-10s %c \n",                                      // 显示内容
            get_text((TextId_e)ptShowInfo->uItemsListDesc[tmpselect].textId), // 根据枚举索引取出字符串
            (tmpselect == ptShowInfo->selectItem) ? ('<') : (' ')); // 箭头
    }

    printf("\n"); // 
}
  • 最后如果没有出差错,点击运行就能看到初步效果了.
  • 如果有条件可以进行在线调试,逐步卡断点,看看代码运行顺序和逻辑,因为赋值指针很多,会晕.

在这里插入图片描述

// main.c 文件
#include <stdio.h> // printf

#include "A_language.h" // txt
#include "cot_menu.h" // menu

// 轮询时每次执行
void Show_Main(cotMenuShow_t *ptShowInfo)
{
    uint8_t showNum = 3; // 显示最大行数
    cotMenu_LimitShowListNum(ptShowInfo, &showNum); // 根据设定截取菜单内容

    printf("\n ------------- %s ------------- \n", get_text((TextId_e)ptShowInfo->uMenuDesc.textId)); // 显示总标题

    for (int i = 0; i < showNum; i++)   // 轮询显示菜单
    {
        menusize_t tmpselect = i + ptShowInfo->showBaseItem; // 计算当前实际行数

        printf(" %-10s %c \n",                                      // 显示内容
            get_text((TextId_e)ptShowInfo->uItemsListDesc[tmpselect].textId), // 根据枚举索引取出字符串
            (tmpselect == ptShowInfo->selectItem) ? ('<') : (' ')); // 箭头
    }

    printf("\n"); // 
}

// 进入时执行一次
void Enter_Main(const cotMenuItemInfo_t *pItemInfo)
{
    // cotMenu_Bind(NULL, 0, Show_Main); // 注册函数, 注册显示函数, 放着执行一次的 load 或 enter 中即可.
}

// 退出时执行一次
void Exit_Main(const cotMenuItemInfo_t *pItemInfo)
{
}

/* 一级菜单 */
static cotMenuList_t sg_Menu1_Table[] = 
{
    {(void *)TEXT_MENU1A, NULL, NULL, NULL, NULL, NULL},
    {(void *)TEXT_MENU1B, NULL, NULL, NULL, NULL, NULL},
    {(void *)TEXT_MENU1C, NULL, NULL, NULL, NULL, NULL},
    {(void *)TEXT_MENU1D, NULL, NULL, NULL, NULL, NULL},
};

// 轮询前执行一次
void Load_Main(const cotMenuItemInfo_t *pItemInfo)
{
    cotMenu_Bind(sg_Menu1_Table, COT_GET_MENU_NUM(sg_Menu1_Table), Show_Main); // 注册函数, 注册显示函数, 放着执行一次的 load 或 enter 中即可.
}

// 轮询时每次执行
void Run_Main(const cotMenuItemInfo_t *pItemInfo)
{
}

// 主菜单对象
static cotMainMenuCfg_t sg_tMainMenu = {(void *)TEXT_MENU, // "main menu" // 这个位置是共用体,如果存枚举,取数时就按枚举取,如果存字符串指针,取数时就按字符串指针取数
                                        Enter_Main, //
                                        Exit_Main, 
                                        Load_Main, 
                                        Run_Main}; 

int main (void)
{
	printf("你好 世界\n");

    cotMenu_Init(&sg_tMainMenu);    // 初始化

    cotMenu_MainEnter();            // 进入

    cotMenu_Task();                 // 使用

    cotMenu_MainExit();             // 退出

    cotMenu_DeInit();               // 去除初始化
	
    return 0;
}

9.按键控制输入

  • 剩下最后一个就是按键输入了, 例程中,将按键输入放在了轮询回调中执行pfnRunCallFun.
  • 这会有个问题, 是先显示完,然后再等待输入,并处理. 按照常规使用,更多的应该是先等待输入,然后根据输入处理,最后输出.
  • 为此我拆分了pfnRunCallFuncotMenu_Task中的调用. 拆分后就可以在前一半执行按键操作,后一般执行打印等其他操作了.
int cotMenu_Task(void)
{
	// 略...    
    if (sg_tMenuManage.pMenuCtrl->pfnRunCallFun != NULL) // 在加载回调前调用
    {
        cotMenuItemInfo_t tItemInfo;

        tItemInfo.uMenuDesc = 0; // 传入固定空指针, 或者按其他方式区分,
        tItemInfo.pExtendData = sg_tMenuManage.pMenuCtrl->pExtendData;
        sg_tMenuManage.pMenuCtrl->pfnRunCallFun(&tItemInfo); 
    }

    if (sg_tMenuManage.pfnLoadCallFun != NULL)
    {
    	// 略...
    }
    
    if (sg_tMenuManage.pMenuCtrl->pMenuList != NULL)
    {
    	// 略...
    }

    if (sg_tMenuManage.pMenuCtrl->pfnRunCallFun != NULL)
    {
        cotMenuItemInfo_t tItemInfo;

        tItemInfo.uMenuDesc = sg_tMenuManage.pMenuCtrl->uMenuDesc;
        tItemInfo.pExtendData = sg_tMenuManage.pMenuCtrl->pExtendData;
        sg_tMenuManage.pMenuCtrl->pfnRunCallFun(&tItemInfo);
    }

    return 0;
}
  • 完成后就可以开始完善pfnRunCallFun的内容,加入等待按键

偷懒的我直接贴上例程的代码,

// 轮询时每次执行
void Run_Main(const cotMenuItemInfo_t *pItemInfo)
{
    int cmd;

	if (pItemInfo[0].uMenuDesc != 0) // 拆分2部分执行
	{
	    printf("%s(1-%s; 2-%s; 3-%s; 4-%s(%s): ", 
	            get_text(TEXT_SELECT_OPTION), 
	            get_text(TEXT_ENTER), get_text(TEXT_EXIT),
	            get_text(TEXT_NEXT), get_text(TEXT_PREVIOUS));
	    scanf(" %d", &cmd); // 空格作用是忽略上次的回车
    }
 	else  // 拆分2部分执行
 	{
	    switch (cmd)
	    {
	    case 1:
	        cotMenu_Enter();
	        break;
	    case 2:
	        cotMenu_Exit(false);
	        break;
	    case 3:
	        cotMenu_SelectNext(true);
	        break;
	    case 4:
	        cotMenu_SelectPrevious(true);
	        break;        
	    default:
	        break;    
	    }
 	}
}

上述代码还没写完, 剩下日后如果有兴致再补上,断断续续写了一下午,好累,明天周一还要上班.
不过如果看到这的人, 剩下的我不会应该也能自己领会了吧.

补充

1.0

  • 修改 cotMenu_Select函数,内置保护措施应该写错了.没有正确判断索引,防止越位
/**
  * @brief      选择指定的菜单选项
  *
  * @param      selectItem 指定的菜单选项
  * @return     0,成功; -1,失败
  */
int cotMenu_Select(menusize_t selectItem)
{
    if (sg_tMenuManage.pMenuCtrl == NULL || sg_tMenuManage.pMenuCtrl->pMenuList == NULL || sg_tMenuManage.isEnterMainMenu == 0)
    {
        return -1;
    }

    if (sg_tMenuManage.pMenuCtrl->selectItem >= sg_tMenuManage.pMenuCtrl->itemsNum)
    {
        return -1;
    }
    
    if (selectItem >= sg_tMenuManage.pMenuCtrl->itemsNum) // 保护措施
    {
        return -1;
    }

    sg_tMenuManage.pMenuCtrl->selectItem = selectItem; // 直接赋值

    return 0;
}
  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值