• C++面向对象语言自制多级菜单


    因为要做一个小应用,需要一个菜单类,在网上找了许久,也没有找到一款心仪的菜单类,索性用C++语言,自制一个命令行级别的菜单类,并制作成库,现记录下来,供以后借鉴。

    一、特性

    1. 无限制条目
    2. 无限制层级
    3. 用户自定义条目和动作
    4. 脚本式生成菜单类

    二、代码实现

    (一)菜单类

    菜单类主要负责菜单的创建、修改、删除,是包含菜单结构组织和响应函数的模型,用户拥有充分的自主性,可根据需要自定义菜单显示和响应函数。其中菜单项使用vector容器数据结构,根据用户命令可进行菜单创建、修改、删除,而显示和响应函数则利用函数指针进行保存,体现了回调函数的特性。

    /*
    菜单类
    (c) hele 2024
    菜单类主要负责菜单的创建、修改、删除,是包含菜单结构组织和响应函数的模型,用户拥有充分的自主性,需要自定义菜单显示和响应函数
    */
    class HeleMenu {
        private:
            void (*p_action)()=nullptr; //回调函数指针,菜单响应函数
            void (*p_display)(string &name, vector &content, unsigned int &index)=nullptr;//回调函数指针,菜单显示函数
            
        
        public:
            string name; //当前菜单名称
            HeleMenu *p_father; //父节点指针,默认NULL        
            vector p_childrenName; //下级菜单项名称,默认empty无下级菜单
            vector p_children; //下级菜单项,默认empty无下级菜单  
            unsigned int index; //菜单项选择指示位,默认0      
                      
            static HeleMenu * parseMenu(string bat, void (*p_display[])(string&, vector&, unsigned int&), void (*p_action[])()); //解析脚本生成菜单
            HeleMenu(string name = "", HeleMenu *p_father = nullptr); //带菜单名称、父节点指针的构造函数,默认菜单名为无名,默认父节点值为空
            void addToMenus(); //添加菜单项到菜单本
            void addValue(string value); //添加菜单单个叶节点
            int addValues(vector values); //添加菜单叶节点
            void changeValue(string value); //修改菜单叶节点
            HeleMenu *getChild(unsigned int index = 0); //获取指定序号的子节点,默认第0项子节点
            string getValue(); //获取菜单叶节点
            void removeAllItem(); //删除菜单所有节点
            void attachAction(void (*p_action)()); //添加动作回调函数
            void attachDisplay(void (*p_display)(string&, vector&, unsigned int&)); //添加显示回调函数 0.菜单名,1.显示内容,2.选中项
            void action(); //菜单响应函数
            void display(); //菜单显示函数
    };
    
    /*解析脚本生成菜单*/
     
    HeleMenu * HeleMenu::parseMenu(string bat, void (*p_display[])(string&, vector&, unsigned int&), void (*p_action[])()) {
     vector stack_simul; //符号栈
     
     vector stack_p; //指针栈
     
     vector stack_name; //名称栈
     
     HeleMenu * root = nullptr; //初始化根菜单指针
     
     uint8_t countAction = 0;
     
     for (uint8_t i = 0; i < bat.length();) {
      string name = ""; //菜单名
     
      uint8_t step = 0; //菜单名长,即距下次读取的步长
     
      char a = bat[i];
     
      if ('{' == a) { //有子菜单
     
       HeleMenu *father = nullptr;
     
       if (stack_p.size() > 0) { //有父菜单
     
        father = stack_p.back(); //弹出指针栈
     
       }
     
       if (stack_name.size() > 0) {
        name = stack_name.back(); //读取名称
     
       }
     
       HeleMenu * m1 = new HeleMenu(name, father); //构建菜单
     
       m1->attachDisplay(p_display[countAction]);
     
       m1->attachAction(p_action[countAction++]);
     
       m1->addToMenus();
     
       stack_simul.push_back('{'); //压入符号栈
     
       stack_p.push_back(m1); //压入菜单指针
     
       
     
       i++;
     
      } else if ('}' == a) { //子菜单结束
     
       stack_simul.pop_back(); //弹出符号栈
     
       root = stack_p.back(); //弹出菜单指针栈
     
       stack_p.pop_back();
     
       stack_name.pop_back(); //弹出名称栈
     
       i++;
     
      } else if (',' == a) {
       i++;
     
      } else { //若有菜单名或值
     
       for (uint8_t j = i; j < bat.length() && '{' != bat[j] && '}' != bat[j] && ',' != bat[j]; j++,step++) { //读菜单名或值
     
        name.append(&bat[0]+j,1); //适应中文
     
       }
     
       stack_name.push_back(name);
     
       i += step;
     
       if (stack_simul.size() > 0 && '{' == stack_simul.back() && ('}' == bat[i] || ',' == bat[i])) { //若为值
     
        stack_p.back()->addValue(name);
     
       }
     
       
     
      } 
     
     }
     
     return root;
     
    }
     
    /*带菜单名称、父节点指针的构造函数,默认菜单名为无名,默认父节点值为空*/
     
    HeleMenu::HeleMenu(string name, HeleMenu *p_father) {
     this->name = name;
     
        this->p_father = p_father;
     
        this->index = 0;
     
        this->p_action = nullptr;
     
        this->p_display = nullptr;
     
    }
     
    /*添加菜单项到菜单本*/
     
    void HeleMenu::addToMenus() {
     if (this->p_father != nullptr) {
      this->p_father->p_childrenName.push_back(this->name); //赋予菜单内容
     
      this->p_father->p_children.push_back(this); //将菜单指针注册到父菜单 
     
     }
     
    }
     
    /*添加菜单单个叶节点*/
     
    void HeleMenu::addValue(string value) {
     this->p_childrenName.push_back(value);
     
     this->p_children.push_back(nullptr); //添加空指针,表示无下级菜单,即叶节点
     
    }
     
    /*添加菜单叶节点*/
     
    int HeleMenu::addValues(vector values) {
     unsigned int i = 0;
     
     for (; i < values.size(); i++) {
      this->p_childrenName.push_back(values[i]); //赋予值项
     
      this->p_children.push_back(nullptr); //添加空指针,表示无下级菜单,即叶节点
     
     };
     
     return i;
     
    }
     
      
     
    /*添加动作回调函数*/
     
    void HeleMenu::attachAction(void (*p_action)()) {
     this->p_action = p_action;
     
    }
     
    /*添加显示回调函数*/
     
    void HeleMenu::attachDisplay(void (*p_display)(string&, vector&, unsigned int&)) {
     this->p_display = p_display;
     
    }
     
    /*菜单响应函数*/
     
    void HeleMenu::action() {
     this->p_action(); //回调动作函数
     
    }
     
    /*将某项叶节点改成对应值,实现菜单动态显示*/
     
    void HeleMenu::changeValue(string value) {
     this->p_childrenName.at(this->index) = value;
     
    }
     
    /*菜单显示函数*/
     
    void HeleMenu::display() {
     this->p_display(this->name, this->p_childrenName, this->index); //回调显示函数,1.显示内容,2.选中项
     
    }
     
    /*获取指定序号的子节点*/
     
    HeleMenu * HeleMenu::getChild(unsigned int index) {
     return this->p_children.at(index);
     
    }
     
    /*获取某项叶节点值,实现菜单动态显示*/
     
    string HeleMenu::getValue() {
     return this->p_childrenName.at(this->index);
     
    }
     
    /*删除菜单所有节点*/
     
    void HeleMenu::removeAllItem() {
     for(auto i:this->p_children) { //释放子节点内存
     
      free(i);
     
     }
     
     this->p_childrenName.clear();
     
     this->p_children.clear();
     
     this->index = 0; //序号防越界,恢复默认值
     
    }
    

    (二)菜单浏览器类

    菜单浏览器类主要负责菜单结构的浏览导航。私有变量是2个菜单类指针,1个是根目录指针,1个是当前目录指针。

    /*
    
    菜单浏览器类
    
    (c)hele 2024
    
    菜单浏览器主要负责菜单结构的浏览
    
    */
    
     
    
    class HeleMenuViewer {
    
     private:
    
     
    
      static HeleMenu *p_rootMenu, *p_currentMenu; //内置根菜单指针、当前菜单项指针
    
     
    
     public:
    
     
    
      static void init(HeleMenu *p_rootMenu); //初始化函数
    
     
    
      static HeleMenu * getCurrentMenu(); //获取当前菜单指针
    
     
    
      static HeleMenu * getRootMenu(); //获取根菜单指针
    
     
    
      static void gotoFather(); //跳转到父菜单
    
     
    
      static void gotoChild(); //跳转到子菜单,序号指示在菜单类里
    
     
    
      static void gotoChild(unsigned int index, unsigned int selected=0); //跳转到指定序号的菜单,默认其选中子菜单第0项
    
     
    
      static void gotoRoot(); //跳转到根菜单
    
     
    
      static void selectUp(); //向上选择
    
     
    
      static void selectDown(); //向下选择
    
     
    
      static void action(); //当前菜单响应
    
     
    
      static void display(); //显示当前菜单
    
     
    
    };
    
    /*初始化菜单管理类的内置根菜单和当前菜单指针为空指针*/
    
    HeleMenu *HeleMenuViewer::p_rootMenu = nullptr;
    
    HeleMenu *HeleMenuViewer::p_currentMenu = nullptr;
    
     
    
    /*当前菜单响应*/
    
    void HeleMenuViewer::action() {
    
        p_currentMenu->action();
    
    }
    
    /*显示当前菜单*/
    
    void HeleMenuViewer::display() {
    
        p_currentMenu->display();
    
    }
    
     
    
    /*获取当前菜单指针*/
    
    HeleMenu * HeleMenuViewer::getCurrentMenu() {
    
        return p_currentMenu;
    
    }
    
     
    
    /*获取根菜单指针*/
    
    HeleMenu * HeleMenuViewer::getRootMenu() {
    
        return p_rootMenu;
    
    }
    
     
    
    /*跳转到父菜单*/
    
    void HeleMenuViewer::gotoFather() {
    
        if (p_currentMenu->p_father != nullptr) {
    
            p_currentMenu = p_currentMenu->p_father;    
    
        }
    
    }
    
     
    
    /*跳转到子菜单,序号指示在菜单类里*/
    
    void HeleMenuViewer::gotoChild() {
    
        if (p_currentMenu->p_children.size() > 0 && p_currentMenu->p_children[p_currentMenu->index] != nullptr) { 防止无子项
    
            p_currentMenu = p_currentMenu->p_children[p_currentMenu->index];
    
        }
    
    }
    
     
    
    /*跳转到指定序号的菜单,默认其选中子菜单第0项*/
    
    void HeleMenuViewer::gotoChild(unsigned int index, unsigned int selected) {
    
        if (index < p_currentMenu->p_children.size()) { //防止越界
    
            p_currentMenu->index = index; //更新菜单指示位置
    
            gotoChild();
    
            if (selected < p_currentMenu->p_children.size()) {
    
                p_currentMenu->index = selected;
    
            }
    
        }
    
    }
    
     
    
    /*跳转到根菜单*/
    
    void HeleMenuViewer::gotoRoot() {
    
        p_currentMenu = p_rootMenu;
    
    }
    
     
    
    /*初始化函数,为根菜单指针赋值*/
    
    void HeleMenuViewer::init(HeleMenu *p_rootMenu) {
    
        HeleMenuViewer::p_rootMenu = p_rootMenu;
    
    }
    
     
    
    /*向下选择*/
    
    void HeleMenuViewer::selectDown() {
    
        if (p_currentMenu->p_childrenName.size() > 0) { //防除0错误
    
            p_currentMenu->index = (p_currentMenu->index + 1) % p_currentMenu->p_childrenName.size();
    
        }
    
    }
    
     
    
    /*向上选择*/
    
    void HeleMenuViewer::selectUp() {
    
        if (p_currentMenu->p_childrenName.size() > 0) { //防除0错误
    
            p_currentMenu->index = (p_currentMenu->index - 1 + p_currentMenu->p_childrenName.size()) % p_currentMenu->p_childrenName.size();
    
        }
    
     
    
    }
    

    (三)库的生成

    网上的资料有很多了,在此仅简单记录。

    ##静态库生成与调用,用HeleMenu.cpp生成库##
    #编译生成.o链接文件
    g++ -c HeleMenu.cpp
     
    #利用.o文件生成静态库,文件名必须以lib***.a形式命名
    ar -crv libHeleMenu.a HeleMenu.o
     
    #调用静态库生成目标程序
    g++ -o main.exe CreateAndShowMenu.cpp -L . -lHeleMenu
    g++ CreateAndShowMenu.cpp libHeleMenu.a -o main.exe
    -------------------------------------------------
    ##动态库生成与调用,用HeleMenu.cpp生成库##
    #生成.o链接文件
    g++ -c HeleMenu.cpp
     
    #利用.o文件生成动态库,文件名必须以lib***.so形式命名
    g++ -shared -fpic -o libHeleMenu.so HeleMenu.o
     
    #调用动态库生成目标程序
    g++ -o main.exe CreateAndShowMenu.cpp -L . -lHeleMenu
    g++ CreateAndShowMenu.cpp libHeleMenu.so -o main.exe
     
    -------------------------------------------------
    代码编译优化
    -O0 禁止编译器优化,默认此项
    -O1 尝试优化编译时间和可执行文件大小
    -O2 更多的优化,尝试几乎全部的优化功能,但不会进行“空间换时间”的优化方法
    -O3 在-O2基础上再打开一些优化选项
    -Os 对生成文件大小进行优化。会打开-O2开的全部选项,除了那些会增加文件大小的
    

    三、使用示例

    主要有2种方法实现用户自定义的菜单类,共同点是attachDisplay和attachAction所带的参数均为用户自定义的函数。

    (一)手动生成

    /*手动生成菜单*/
    
    HeleMenu *m1 = new HeleMenu();
    
    m1->attachDisplay(display_root);
    
    m1->attachAction(action_root);
    
    HeleMenuViewer::init(m1); //初始化根菜单
    
    //    
    
    HeleMenu *m2 = new HeleMenu("历史记录",m1);
    
    m2->attachDisplay(display);
    
    m2->attachAction(action);
    
    m2->addToMenus();
    
     
    
    m2 = new HeleMenu("操作",m1);
    
    m2->addValues({"保存","不保存"});
    
    m2->attachDisplay(display);
    
    m2->attachAction(action_operate);
    
    m2->addToMenus();
    
     
    
    m2 = new HeleMenu("菜单",m1);
    
    m2->attachDisplay(display);
    
    m2->attachAction(action);
    
    m2->addToMenus();
    
    m1 = m2; //构建下一层子菜单
    
     
    
    m2 = new HeleMenu("对比度", m1);
    
    m2->addValues({"1", "2", "3", "4"});
    
    m2->attachDisplay(display_compare);
    
    m2->attachAction(action_compare);
    
    m2->addToMenus();
    
     
    
    m1->addValues({/*对比度,*/ "全部清除","重启","关机"/*,"校准","关于"*/});
    
     
    
     
    
    m2 = new HeleMenu("校准",m1);
    
    m2->addValue("确认");
    
    m2->attachDisplay(display);
    
    m2->attachAction(action_adjust);
    
    m2->addToMenus();    
    
     
    
    m2 = new HeleMenu("关于",m1);
    
    m2->addValue("(c)hele 2024\n这是一个菜单类,可以帮助你生成自定义菜单,同时还可以设置动作");
    
    m2->attachDisplay(display);
    
    m2->attachAction(action);
    
    m2->addToMenus();
    
     
    
    HeleMenuViewer::gotoRoot();     //到达根菜单
    
    while (true) { //启动
    
        system("cls");
    
        HeleMenuViewer::display();
    
        HeleMenuViewer::action();
    
    }
    

    (二)脚本生成

    主要利用菜单类parseMenu函数实现,写了1个解析器,可以实现菜单类的自动生成。

    /*脚本生成菜单*/
    
    void (*p_display[])(string&, vector<string>&, unsigned int&) = {/*root*/display_root, /*log*/display, /*operate*/display, /*menu*/display, /*constrast*/display_compare, /*adjust*/display, /*about*/display};
    
    void (*p_action[])() = {/*root*/action_root, /*log*/action, /*operate*/action_operate, /*menu*/action, /*constrast*/action_compare, /*adjust*/action_adjust, /*about*/action};
    
    HeleMenu *m1 = HeleMenu::parseMenu("{历史{},操作{save,unsave},菜单{对比度{1,2,3,4},clearAll,rePower,shutdown,校准{confirm},关于{(c)hele 2024,这是一个菜单}}}", p_display, p_action);
    
    HeleMenuViewer::init(m1);
    
     
    
    HeleMenuViewer::gotoRoot();     //到达根菜单
    
    while (true) { //启动
    
        system("cls");
    
        HeleMenuViewer::display();
    
        HeleMenuViewer::action();
    
    }
    

    parseMenu所带的参数共3个,第1个是菜单结构字符串,也就是生成菜单结构的脚本,后面2个参数分别是显示函数指针数组和响应函数指针数组。为便于理解,下面我将用户自定义菜单结构展开:

    {
        log{
        },
        operate{
            save,unsave
        },
        menu{
            constrast{
                1,2,3,4
            },
            clearAll,
            rePower,
            shutdown,
            adjust{
                confirm
            },
            about{
                (c)hele 2024
            }
        }
    }
    

    每一个'{'都意味着该项目有子菜单,每一个'}'意味着该菜单结束,每一个','都意味着有同级菜单,以上这3个符号均是关键词,均是英文字符。所有菜单除内容中间可以有空格外,其余地方不能有多余的空格。支持中文。

    (三)演示

    主要使用上、下、左、右、空格、回车、退出这些按键,只实现部分功能。
    image

    四、不足与展望

    运用C++面向对象思维进行编程,代码体积大,效率低;

    利用脚本生成菜单是个新颖的思路,但容错性不好,没有对脚本进行规范化的检查,对用户不友好;

    可以利用ArduinoSTL库,将此库移植进Arduino系列单片机项目中;

    使用了动态分配内存技术,对于RAM较小的单片机,容易内存溢出。

    五、参考资料

    六、源码下载

  • 相关阅读:
    贪吃蛇和俄罗斯方块游戏
    Python【理解标识符的定义】
    小程序生命周期详解,助你成为开发高手!
    【RK3588】模型精度优化指南和精度问题查找
    【数据结构】堆和优先级队列
    【17代码题】编写函数fun:比较两个字符串长度,返回长的字符串;若长度相等返回第1个字符串
    玩一玩WolframAlpha计算知识引擎
    java(面向对象)的23种设计模式(10)——模板方法模式
    java数组中删除元素或一个数组元素
    【CSS基础语法】CSS基础语法知识学习笔记汇总
  • 原文地址:https://www.cnblogs.com/hele-two/p/18242492