实现的效果
简易又优雅的多级菜单
概要
这里我手把手教大家如何用C语言实现一个简单易管理,效果优雅的多级菜单,(实现逻辑无参考任何文章,如有雷同纯属巧合!!!)在写代码之前,我认为最重要的就是先构思好一个整体的框架,这样我们可以在后期更快的把代码写完。
整体架构流程
前言:
在开始写这个菜单的时候,我的思路其实是很不清晰的,我总认为可以以一个纵观大局的思路,一下子写完这个多级菜单的结构。但是显示很残酷,并不是很好写,也确实没有写出来。于是我换位思考,我们在电脑上创建文件的时候,电脑能提前知道吗?答案肯定是不能,那么他是怎么一步步的去管理这些错综复杂的路径的呢?我第一个想到的就是链表,所以这个多级菜单,我想到用链表来实现。
1.链表:
那我们来复习一下链表的结构,它无非就是一个结构体内部带有一个(或多个)能够指向与自己同类型结构体的指针(长难句)。
链表和数组的各自的优缺点,这里就不多赘述了,简而言之——灵活,这个特性,可以让我们在后期添加和删除上显得格外的灵活。
2.父与子:
一开始我的逻辑就是,一个父类管理他的所有子类,实现的效果就像系统里的文件夹,他只负责管理自己文件夹内的内容,这样我们很快就能得到一个简单的模型,如下图1所示:

图1
其实这样框架就很显而易见了,这里虽然是一个父文件夹携带三个子文件,但是子文件就一定是子文件吗?当然,它也可以是一个父文件。那接下来应该怎么去管理呢?我们可以看到子文件和父文件夹之间的连接线,我们应该赋予这个连接线一个朝向。我们可以设想一下,我们在创建文件的时候,是不是先进入一个文件夹下,再在这个文件夹下创建文件。所以我们可以理解为,我们所创建的文件都是再向父文件夹进行一个注册,然后父文件夹就可以知道,他的下面到底有多少个子文件夹。所以我们创建每个子文件的时候,我们都可以用一个指针,指向我们的父文件夹。这个操作就类似于终端中的cd .. 。

图2
3.大哥和小弟:
解决完父项与子项之间的关系后,我们思考N个子项,我们应该如何去管理呢?难道都交给父项吗?我们设想一下,假设所有子项全部交给父项去管理,父项是不是需要有和子项同数量的指针,分别指向所有的子项。那这样其实在代码中的实现是异常困难的,也许我们需要动态申请内存,或者提前开辟一个足够大的空间存放可能会进入父项的子项,这样就类似于开篇说的“以一个纵观大局的思路,一下子写完这个多级菜单的结构”。这样显然是不可取的,所以我们设计的思路是让子项分别管理与自己相邻的子项目,如图3所示。

图3
我们发现,最左边和最右边的子文件似乎没有相邻的子文件,那么很简单我们让他们相邻就好了,将“最左边文件的左”连接到“右边文件的右”,这样似乎就得到了一个很完美的闭环。 但这样还不够,现在我们每个节点都只做到了连接父节点,连接左和右节点,那作为父节点,我们该如何和子节点建立联系,解决最后一个问题,整体的框架就算完成了。按照之前的思路,父节点对每一个子节点建立联系显然是不切实际的,那我们换一个思路,父节点只和第一个子节点建立联系,剩下的都交给节点间相互联系,这个思路不错。

图4
4.父与长子:
如上图4所示,父节点向第一个子节点1建立了连接,然后前面说过,每一个子节点也可以是父节点,这一点我在图上其实也有所表示,最上面的父节点从原来的方形被我换成和子节点一样的圆形,这样一来,我们从上图中扣出一个节点我们都可以形象的展开为:

菜单整体架构
接下来我们来看看代码是如何实现的,首先我们构造一个节点的结构体,我对每个节点增加了一个属性的功能,使其不仅能作为一个文件夹,也可以作为bool型和整型的存储。
my_menu.h
#ifndef __MY_MENU_H_
#define __MY_MENU_H_
#include <stdio.h>
#include <stdint.h>
//文件类型
typedef enum {
Normal_Folder, //文件夹
Check_Box, //复选框
Number_Box //常数型
}Folder_Class;
typedef struct Folder_Menu {
char *name; //文件名称
uint8_t No; //当前文件夹下的位次
Folder_Class kind; //文件种类
uint8_t sons_Count; //子文件个数
struct Folder_Menu *father; //父文件节点
struct Folder_Menu *son_first; //第一个子文件
struct Folder_Menu *next_brother; //下一个兄弟文件
struct Folder_Menu *last_brother; //上一个兄弟文件
uint8_t *check_box_p; //复选框指向内容
int32_t *number_box_p; //常数指向内容
uint8_t number_box_select; //数值项是否被选中
} Folder_Menu;
extern Folder_Menu myMenu;
extern Folder_Menu *key_menu_p;
void create_Menu_Folder(Folder_Menu *father, Folder_Menu *me, char *name);
void create_Menu_CheckBox(Folder_Menu *father, Folder_Menu *me, char *name, uint8_t *check);
void create_Menu_NumberBox(Folder_Menu *father, Folder_Menu *me, char *name, int32_t *number);
void All_Folder_Menu_Init(Folder_Menu *Menu);
#endif
my_menu.c
#include "my_menu.h"
/*-----------------
/ 菜单头节点 /
------------------*/
Folder_Menu myMenu = {
.father = NULL,
.son_first = NULL,
.next_brother = NULL,
.last_brother = NULL,
.name = "<<Menu>>",
.sons_Count = 0,
.No = 0,
.kind = Normal_Folder,
};
//菜单按键控制索引指针
Folder_Menu *key_menu_p = NULL;
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 向父节点注册文件夹
// 参数说明 father 父节点
// 参数说明 me 文件夹
// 参数说明 name 文件夹名称
// 返回参数 void
// 使用示例 create_Menu_Folder(&myMenu, &folder1, "folder1");
// 备注信息
//-------------------------------------------------------------------------------------------------------------------
void create_Menu_Folder(Folder_Menu *father, Folder_Menu *me, char *name)
{
//无法向非文件夹文件注册子文件
if(father->kind != Normal_Folder) {
return ;
}
//文件属性初始化
me->name = name;
me->sons_Count = 0;
me->kind = Normal_Folder;
//文件链表初始化
me->next_brother = NULL;
me->last_brother = NULL;
me->son_first = NULL;
me->father = father;
father->sons_Count ++;
uint8_t No = 1;
//添加新节点
if(father->sons_Count == 1)
{
father->son_first = me;
me->No = No;
}
else
{
No++;
Folder_Menu *p = father->son_first;
while(p->next_brother != NULL) {
p = p->next_brother;
No++;
}
//双向添加
me->last_brother = p;
me->No = No;
p->next_brother = me;
}
}
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 向父节点注册复选框
// 参数说明 father 父节点
// 参数说明 me 复选框
// 参数说明 name 复选框名称
// 参数说明 bool 复选框指向的变量
// 返回参数 void
// 使用示例 create_Mune_CheckBox(&myMenu, &box, "box1", &box1);
// 备注信息
//-------------------------------------------------------------------------------------------------------------------
void create_Menu_CheckBox(Folder_Menu *father, Folder_Menu *me, char *name, uint8_t *check)
{
//无法向非文件夹文件注册子文件
if(father->kind != Normal_Folder) {
return ;
}
//文件属性初始化
me->name = name;
me->sons_Count = 0;
me->kind = Check_Box;
me->check_box_p = check;
//文件链表初始化
me->next_brother = NULL;
me->last_brother = NULL;
me->son_first = NULL;
me->father = father;
father->sons_Count ++;
uint8_t No = 1;
//添加新节点
if(father->sons_Count == 1)
{
father->son_first = me;
me->No = No;
}
else
{
No++;
Folder_Menu *p = father->son_first;
while(p->next_brother != NULL) {
p = p->next_brother;
No++;
}
//双向添加
me->last_brother = p;
me->No = No;
p->next_brother = me;
}
}
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 向父节点注册数值框
// 参数说明 father 父节点
// 参数说明 me 数值框
// 参数说明 name 数值框名称
// 参数说明 number 数值框指向的变量(必须定义为int32_t 不然读取时格式错误)
// 返回参数 void
// 使用示例 create_Menu_NumberBox(&myMenu, &numbox1, "numbox1", &num1);
// 备注信息
//-------------------------------------------------------------------------------------------------------------------
void create_Menu_NumberBox(Folder_Menu *father, Folder_Menu *me, char *name, int32_t *number)
{
//无法向非文件夹文件注册子文件
if(father->kind != Normal_Folder) {
return ;
}
//文件属性初始化
me->name = name;
me->sons_Count = 0;
me->kind = Number_Box;
me->number_box_p = number;
me->number_box_select = 0;
//文件链表初始化
me->next_brother = NULL;
me->last_brother = NULL;
me->son_first = NULL;
me->father = father;
father->sons_Count ++;
uint8_t No = 1;
//添加新节点
if(father->sons_Count == 1)
{
father->son_first = me;
me->No = No;
}
else
{
No++;
Folder_Menu *p = father->son_first;
while(p->next_brother != NULL) {
p = p->next_brother;
No++;
}
//双向添加
me->last_brother = p;
me->No = No;
p->next_brother = me;
}
}
/*----------------------
/ 所有子项目初始化
----------------------*/
void All_Folder_Menu_Init(Folder_Menu *Menu)
{
if(Menu->son_first == NULL) {
return ;
}
Folder_Menu *hp = Menu->son_first;
Folder_Menu *p = Menu->son_first;
if(hp->next_brother == NULL) {
All_Folder_Menu_Init(p);
}
while(p->next_brother != NULL) {
if(p->kind == Normal_Folder) {
All_Folder_Menu_Init(p);
}
p = p->next_brother;
}
if(hp->next_brother != NULL) {
All_Folder_Menu_Init(p);
}
p->next_brother = hp;
hp->last_brother = p;
}
创建节点+菜单显示+按键控制
我简单的对显示菜单和按键控制进行了封装(总共用到4个按键),大家不妨自己试试效果,由于没有太多时间对显示部分进行优化,所以可能有些参数大家需要自己在内部实现进行调整,我认为菜单的精髓是他的结构,可以参考我提供的代码进行优化,代码中包含了按键控制的逻辑,已经封装好了,只需要将.h中的四个按键函数分配给按键的实现即可。
menuTask.c
#include "menuTask.h"
//重刷新屏幕使能
volatile uint8_t Refresh_enable = 1;
/* 宏定义菜单显示函数(需要自己定义) */
//-----------------------------------------------------------------------------------------------------------------
/* ***********************
* 显示字符串
* 格式 x y string
* ***********************/
#define menu_show_string(_x_, _y_, __string__) ( tft180_show_string( (_x_), (_y_), (__string__) ) )
/* ***********************
* 显示字符
* 格式 x y char
* **********************/
#define menu_show_char(_x_, _y_, _char_) ( tft180_show_char( (_x_), (_y_), (_char_) ) )
/* **********************
* 显示整数
* 格式 x y int len
* **********************/
#define menu_show_int(_x_, _y_, _int_, _len_) ( tft180_show_int( (_x_), (_y_), (_int_), (_len_) ) )
#define MENU_TOTAL (5) //菜单显示项目最大值(创建项目不要超过这个值,没有做图像优化)
#define MENU_STAR_Y (56) //菜单起始Y
#define MENU_Y_INTERVAL (10) //菜单行间距 (要大于字长)
#define DATA_STAR_X (86) //数据显示起始x
#define DATA_LONE_SIZE (5) //5位可现实uint16
#define FONT_SIZE (6) //字宽 (8*16 字宽为8 6*8 字宽为6)
//-------------------------------------------------------------------------------------------------------------------
//------------------------------创建文件夹-------------------------------//
Folder_Menu file1;
Folder_Menu file2;
Folder_Menu file3;
Folder_Menu file4;
Folder_Menu file5;
Folder_Menu file6;
Folder_Menu file7;
Folder_Menu file8;
Folder_Menu file9;
//------------------------------创建文件夹-------------------------------//
//***********************************************************************
//------------------------------创建复选框-------------------------------//
Folder_Menu box1;
Folder_Menu box2;
Folder_Menu box3;
uint8_t box1_bool = 0;
uint8_t box2_bool = 0;
uint8_t box3_bool = 0;
//------------------------------创建复选框-------------------------------//
//***********************************************************************
//------------------------------创建常数型-------------------------------//
Folder_Menu num1;
Folder_Menu num2;
Folder_Menu num3;
int32_t nn1 = 300;
int32_t nn2 = 300;
int32_t nn3 = 725;
//------------------------------创建常数型-------------------------------//
/**
* @brief 在此处创建菜单结构(myMenu为根节点)
* @param
* @return void
*/
void menu_init(void)
{
/* 在下面添加 */
//第一级菜单
create_Menu_Folder(&myMenu, &file1, "Folder1");
create_Menu_Folder(&myMenu, &file2, "Folder2");
create_Menu_Folder(&myMenu, &file3, "Folder3");
//第二级菜单
create_Menu_CheckBox(&file1, &box1, "bool1", &box1_bool);
create_Menu_CheckBox(&file1, &box2, "bool2", &box2_bool);
create_Menu_CheckBox(&file1, &box3, "bool3", &box3_bool);
create_Menu_NumberBox(&file2, &num1, "number1", &nn1);
create_Menu_NumberBox(&file2, &num2, "number2", &nn2);
create_Menu_NumberBox(&file2, &num3, "number3", &nn3);
create_Menu_Folder(&file3, &file4, "Folder4");
create_Menu_Folder(&file3, &file5, "Folder5");
create_Menu_Folder(&file3, &file6, "Folder6");
create_Menu_Folder(&file4, &file7, "Folder7");
create_Menu_Folder(&file4, &file8, "Folder8");
create_Menu_Folder(&file7, &file9, "Folder9");
/* 在上面添加 */
//下面是按键指针初始化
if(myMenu.son_first != NULL)
key_menu_p = myMenu.son_first;
//成员初始化
All_Folder_Menu_Init(&myMenu);
}
/**
* @brief 菜单显示部分
* @param key 控制菜单的索引指针
* @param total 菜单总长度
* @param star_Row 显示起始行位置
* @param Row_interval 行间距
* @param data_Col_Star 数据显示起始列
* @param data_Col_Len 数据显示最大长度
* @param font_size 字宽
* @return
*/
void menu_show(Folder_Menu *key, int total,
int star_Row, int Row_interval,
int data_Col_Star, int data_Col_Len ,int font_size)
{
//清屏用
char clear_s[64];
/*
* 选项显示
*/
//获取当前节点的父节点
Folder_Menu *father_p = key->father;
//获取父节点的第一个子节点
Folder_Menu *son_p = father_p->son_first;
//路径显示
char tmpchar[64];
sprintf(tmpchar, " %s/ ", father_p->name);
menu_show_string(0, star_Row, tmpchar);
//循环所有节点(必须小于total_项)
for(int i = 1; i <= father_p->sons_Count; i++)
{
if(son_p->kind == Normal_Folder)
sprintf(tmpchar, "%s\\[%d] ", son_p->name, son_p->sons_Count);
else
sprintf(tmpchar, "%s ", son_p->name);
menu_show_string(font_size*2, star_Row + i*Row_interval, tmpchar);
son_p = son_p->next_brother;
}
//假设一页最多十个项目, 清除残留图像
for(int i = father_p->sons_Count+1; i <= total; i++)
{
memset(clear_s, ' ', sizeof(clear_s));
clear_s[15] = '\0';
menu_show_string(font_size*2, star_Row + i*Row_interval, clear_s);
}
/*
* 光标显示
*/
//循环所有节点(必须小于10项)
for(int i = 1; i <= key->father->sons_Count; i++)
{
if(key_menu_p->No != i) {
menu_show_string(0, star_Row + i*Row_interval, " ");
}
else {
menu_show_string(0, star_Row + i*Row_interval, "->");
}
}
//假设一页最多十个项目, 清除残留图像
for(int i = key->father->sons_Count+1; i <= total; i++)
{
menu_show_string(0, star_Row + i*Row_interval, " ");
}
/*
* 数据显示
*/
//获取当前节点的父节点
Folder_Menu *fatherp = key->father;
//获取父节点的第一个子节点
Folder_Menu *sonp = fatherp->son_first;
for(int i = 1; i <= fatherp->sons_Count; i++)
{
switch (sonp->kind)
{
case Check_Box:
if(*(sonp->check_box_p) == 1)
menu_show_string(data_Col_Star, star_Row + i*Row_interval, " Y ");
else
menu_show_string(data_Col_Star, star_Row + i*Row_interval, " N ");
break;
case Number_Box:
if(sonp->number_box_select == 0)
menu_show_char(data_Col_Star - font_size, star_Row + i*Row_interval, ' ');
menu_show_int(data_Col_Star, star_Row + i*Row_interval, *(sonp->number_box_p), data_Col_Len);
menu_show_char(data_Col_Star + (data_Col_Len)*font_size, star_Row + i*Row_interval, ' ');
if(sonp->number_box_select == 1) {
menu_show_char(data_Col_Star - font_size, star_Row + i*Row_interval, '<');
menu_show_int(data_Col_Star, star_Row + i*Row_interval, *(sonp->number_box_p), data_Col_Len);
menu_show_char(data_Col_Star + (data_Col_Len)*font_size, star_Row + i*Row_interval, '>');
}
break;
default:
memset(clear_s, ' ', sizeof(clear_s));
clear_s[data_Col_Len] = '\0';
menu_show_string(data_Col_Star, star_Row + i*Row_interval, clear_s);
break;
}
sonp = sonp->next_brother;
}
for(int i = fatherp->sons_Count+1; i <= total; i++)
{
memset(clear_s, ' ', sizeof(clear_s));
clear_s[5] = '\0';
menu_show_string(data_Col_Star + font_size, star_Row + i*Row_interval, clear_s);
}
}
void menu_show_All(void)
{
//刷新标志位
if(Refresh_enable == 1)
{
//选项显示
menu_show(key_menu_p, MENU_TOTAL, MENU_STAR_Y, MENU_Y_INTERVAL, DATA_STAR_X, DATA_LONE_SIZE, FONT_SIZE);
Refresh_enable = 0;
}
}
//上
void menu_function_up(void)
{
//上一项
key_menu_p = key_menu_p->last_brother;
//使能图像刷新
Refresh_enable = 1;
}
//下
void menu_function_down(void)
{
//下一项
key_menu_p = key_menu_p->next_brother;
//使能图像刷新
Refresh_enable = 1;
}
//确定
void menu_function_enter(void)
{
if(key_menu_p->son_first == NULL) return ;
//进入
key_menu_p = key_menu_p->son_first;
//使能图像刷新
Refresh_enable = 1;
}
//退出
void menu_function_quit(void)
{
if(key_menu_p->father->father == NULL) return ;
//退出
key_menu_p = key_menu_p->father;
//使能图像刷新
Refresh_enable = 1;
}
//反转check_box变量
void menu_function_toggle(void)
{
//反转
*(key_menu_p->check_box_p) = !*(key_menu_p->check_box_p);
//使能图像刷新
Refresh_enable = 1;
}
//选中调整变量
void menu_function_select(void)
{
//使能被选中项
key_menu_p->number_box_select = 1;
//使能图像刷新
Refresh_enable = 1;
}
//取消选中变量
void menu_function_unselect(void)
{
//使能被选中项
key_menu_p->number_box_select = 0;
//使能图像刷新
Refresh_enable = 1;
}
//数据++
void menu_function_numPlus(void)
{
//数据++
*(key_menu_p->number_box_p) = *(key_menu_p->number_box_p) + 1;
//使能图像刷新
Refresh_enable = 1;
}
//数据--
void menu_function_numSub(void)
{
//数据--
*(key_menu_p->number_box_p) = *(key_menu_p->number_box_p) - 1;
//使能图像刷新
Refresh_enable = 1;
}
//方向上--控制逻辑
void Menu_upFuntion(void)
{
switch (key_menu_p->kind)
{
case Normal_Folder:
menu_function_up();
break;
case Check_Box:
menu_function_up();
break;
case Number_Box:
//如果被选中 则调整参数
if(key_menu_p->number_box_select == 1)
menu_function_numPlus();
else
menu_function_up();
break;
}
}
//方向下--控制逻辑
void Menu_downFuntion(void)
{
switch (key_menu_p->kind)
{
case Normal_Folder:
menu_function_down();
break;
case Check_Box:
menu_function_down();
break;
case Number_Box:
//如果被选中 则调整参数
if(key_menu_p->number_box_select == 1)
menu_function_numSub();
else
menu_function_down();
break;
}
}
//确定--控制逻辑
void Menu_enterFuntion(void)
{
switch (key_menu_p->kind)
{
case Normal_Folder:
menu_function_enter();
break;
case Check_Box:
menu_function_toggle();
break;
case Number_Box:
menu_function_select();
break;
}
}
//取消--控制逻辑
void Menu_quitFuntion(void)
{
switch (key_menu_p->kind)
{
case Normal_Folder:
menu_function_quit();
break;
case Check_Box:
menu_function_quit();
break;
case Number_Box:
if(key_menu_p->number_box_select == 1)
menu_function_unselect();
else {
menu_function_quit();
}
break;
}
}
menuTask.h
#ifndef __MENU_TASK_H_
#define __MENU_TASK_H_
#include "headfile.h" //需要用到的头文件比如 屏幕显示等
#include "my_menu.h"
extern volatile uint8_t Refresh_enable;
void menu_init(void);
void menu_show_All(void);
//菜单控制部分
void Menu_upFuntion(void); //给按键1
void Menu_downFuntion(void); //给按键2
void Menu_enterFuntion(void); //给按键3
void Menu_quitFuntion(void); //给按键4
#endif
小结
整体的逻辑就是通过按键操控选择指针在菜单列表中来回穿梭,每一级菜单的管理可以通过读取父级菜单中的首个子项目和子项目总和来实现对当前菜单的管理,菜单节点通过使用指针访问我们需要调整的全局变量,我认为有一个好的思路可以让开发过程节省很多时间,可以参考我的代码进行修改,如果有什么好的修改建议欢迎大家转载,代码开源不设置开源协议,希望大家可以共同进步。
6019

被折叠的 条评论
为什么被折叠?



