C语言实现树形菜单的管理系统
源代码下载请点击下方链接,里面还包含学生信息管理系统和成绩管理系统,都是在vc6.0下用c语言完成的,一共三份。
下载源代码请点击这里
一、开发环境
vc6.0
二、功能介绍
创建一个树形菜单以及对该菜单进行增删改查,还包含了将一个树结构的菜单存储到文件中,从文件中读取一个树结构的菜单并显示等。
三、代码实现
1、定义树形菜单的结构体
typedef struct TreeNode{//定义树结点的结构体
char menu_name[20];//菜单名称
time_t t;//采用时间为每个菜单生成唯一编号
struct TreeNode *firstchild,*nextbrother,*parent;
int level;//菜单的级数
}node;
树形结构是转化成对应的二叉树存储的,方式如下:
树转化为二叉树规则:每个结点左指针指向它的第一个孩子结点,右指针指向它在树中的相邻兄弟结点,可表示为“左孩子右兄弟”。
由于我不会用c语言生成唯一***,暂且就用当前时间来代替。
"time_t t"得到的是从1970.1.1 00:00:00到现在的秒数;
firstchild指向该结点的第一个孩子结点,nextbrother指向相邻兄弟结点(右侧兄弟),parent指向父菜单也就是上层菜单结点,注意不是转化后在对应二叉树中的父结点,而是树中的父结点。如上图的结点C,C结点的firstchild(第一个孩子结点)指向H,因为H是结点C的孩子结点也是唯一的孩子结点,如果有多个孩子结点,则最左侧的为第一个;C结点的nextbrother(相邻兄弟结点)指向D,D是它右侧的兄弟,如果C右侧没兄弟则指向一个空领域;C结点的parent(上层菜单结点)指向A,不是B,因为A是C的上层菜单,而B只是C在菜单树转化为对应二叉树上的父结点,它们实际上并没有上下级的关系,而是平级。
level表示当前菜单项是几级菜单。
2、定义栈的结构体和相关操作
#define Maxsize 100//定义栈的大小
typedef struct{//定义栈的结构体
node *data[Maxsize];
int top;
}SqStack;
void InitStack(SqStack &s){//初始化栈
s.top = -1;
}
bool StackEmpty(SqStack s){//判栈空
if(s.top == -1){
return true;
}else{
return false;
}
}
bool Push(SqStack &s,node *x){//进栈
if(s.top == Maxsize-1){
return false;
}else{
s.data[++s.top] = x;
return true;
}
}
node *Pop(SqStack &s){//出栈
if(s.top == -1){
return NULL;
}else{
return s.data[s.top--];
}
}
因为要对二叉树进行非递归的遍历,需要借助于栈来实现
3、功能的实现
准备工作已做好,接下来就开始实现各个功能了
1、创建菜单树并返回根结点
node *creat_Tree(){//创建菜单树
node *T,*p;
char menu_id[20];
T = (node *)malloc(sizeof(node));
T->nextbrother = NULL;
T->parent = NULL;
T->firstchild = NULL;
T->t = time(NULL);
T->level = 0;
strcpy(T->menu_name,"主菜单(隐藏)");
while(1){
printf("建立菜单,请输入父菜单编号(一级菜单输入:%d),退出输入#\n",T->t);
scanf("%s",menu_id);
if(strcmp(menu_id,"#")==0){
break;
}else{
p = Inorder(T,strtol(menu_id,NULL,0));
if(p == NULL){
printf("请输入正确的编号!\n");
continue;
}
Insert_node(p);
printf("\n");
printf("当前菜单如下:\n");
display_menu(T->firstchild);
}
}
getchar();
return T;
}
根结点当做一个隐藏的主菜单,不显示,其底下的子菜单是一级菜单。创建菜单是选定一个父菜单的编号并在其下创建子菜单。
2、新增一个菜单项
void add_menu(node *T){//添加子菜单
char menu_id[20];
node *p;
printf("添加菜单,请输入父菜单编号(一级菜单输入:%d):",T->t);
scanf("%s",menu_id);
getchar();
p = Inorder(T,strtol(menu_id,NULL,0));
if(p == NULL){
printf("不存在该编号!\n");
return;
}
Insert_node(p);
printf("\n");
//printf("当前菜单如下:\n");
//display_menu(T->firstchild);
}
3、删除子菜单,当被删除菜单有下级菜单不能删除
void delete_menu(node *T){//删除子菜单
char id[20];
node *p,*parent,*q;
printf("请输入要删除菜单的编号:");
scanf("%s",id);
getchar();
p = Inorder(T,strtol(id,NULL,0));
if(p!=NULL){
if(p->firstchild != NULL){
printf("\n");
printf("该菜单下有子菜单,无法删除!\n");
}else{
parent = p->parent;//获取父菜单结点的指针
if(parent->firstchild == p){//如果该菜单是父菜单的第一个孩子结点
parent->firstchild = p->nextbrother;
free(p);
printf("\n");
printf("删除成功!\n");
}else{
q = parent->firstchild;
while(q&&q->nextbrother!=p){
q = q->nextbrother;
}
q->nextbrother = p->nextbrother;
free(p);
printf("删除成功!\n");
}
}
}else{
printf("无此菜单!\n");
}
}
4、修改菜单的名称
void edit_menu_name(node *T){//修改菜单名称
char id[20];
node *p;
printf("请输入要修改菜单的编号:");
scanf("%s",id);
p = Inorder(T,strtol(id,NULL,0));
if(p!=NULL){
printf("请输入菜单名称(原名称为:%s):",p->menu_name);
scanf("%s",p->menu_name);
printf("修改成功!\n");
}else{
printf("无此菜单!\n");
}
getchar();
}
5、显示菜单
void display_menu(node *T){//递归方式实现先序遍历来显示菜单,菜单树转化为对应的二叉树只有先序遍历能将菜单很好展示出来
if(T!=NULL){
printf("\n");
print_str(' ',(T->level)*3);
printf("%s(%ld)\n",T->menu_name,T->t);
printf("\n");
display_menu(T->firstchild);
display_menu(T->nextbrother);
}
}
注意,只有对转化后的二叉树进行先序遍历才能将对应的菜单按层次展示出来,代码中的print_str(char c,int n)函数表示打印n个字符c,通过每个菜单对应不同级别在它前面打印不同个数的空格,这样才能更好的将菜单层次展示出来。
6、利用栈对二叉树遍历查找对应编号的结点
node *Inorder(node *T,long id){//采用非递归算法(利用栈)方式实现中序遍历查找结点并返回
SqStack s;
InitStack(s);
node *p = T;
while(p||!StackEmpty(s)){
if(p){
Push(s,p);
p = p->firstchild;
}else{
p = Pop(s);
if(p->t == id){
return p;
}
p = p->nextbrother;
}
}
return NULL;
}
7、向父菜单的结点T中插入子菜单项
void Insert_node(node *T){//向父菜单的结点T中插入子菜单项
node *p,*q;
p = (node *)malloc(sizeof(node));
printf("\n");
printf("请输入菜单名称:");
scanf("%s",p->menu_name);
getchar();
p->t = time(NULL);
p->parent = T;
p->firstchild = NULL;
p->nextbrother = NULL;
p->level = T->level+1;
if(T->firstchild == NULL){
T->firstchild = p;
}else{
q = T->firstchild;
while(q->nextbrother != NULL){
q= q->nextbrother;
}
q->nextbrother = p;
}
printf("\n");
printf("添加成功!\n");
}
8、将当前编辑好的菜单保存到文件中,采用先序保持菜单顺序
void savebypro(node *T,FILE *fp){//采用先序遍历存储
if(T!=NULL){
long firstchild_t,nextbrother_t,parent_t;
if(T->firstchild == NULL){
firstchild_t = 0;
}else{
firstchild_t = T->firstchild->t;
}
if(T->nextbrother == NULL){
nextbrother_t = 0;
}else{
nextbrother_t = T->nextbrother->t;
}
if(T->parent == NULL){
parent_t = 0;
}else{
parent_t = T->parent->t;
}
fprintf(fp,"%s %ld %ld %ld %ld %d\n",T->menu_name,T->t,firstchild_t,nextbrother_t,parent_t,T->level);
//指针地址均采用长整型存储,时间t也是,它是1970-1-1 00:00:00到现在的秒数
savebypro(T->firstchild,fp);
savebypro(T->nextbrother,fp);
}
}
void save_menu(node *T){//将菜单保存到文件中
FILE *fp;
if((fp = fopen("e:\\myProject_file\\menu.txt","w"))==NULL){
printf("文件打开失败!\n");
exit(1);
}
savebypro(T,fp);
fclose(fp);
printf("\n");
printf("保存成功!\n");
}
9、从文件中读取菜单,并返回根结点
node *read_file(){//读取文件数据并将其还原成菜单树返回其根结点
FILE *fp;
node *T=NULL,tree,*p,*temp,*q;
if((fp = fopen("e:\\myProject_file\\menu.txt","r"))==NULL){
printf("文件读取失败!请先将数据保存到文件中!\n");
return NULL;
}
while(fscanf(fp,"%s%ld%ld%ld%ld%d",tree.menu_name,&tree.t,&tree.firstchild,&tree.nextbrother,&tree.parent,&tree.level)!=EOF){
//这里的firstchild、nextbrother、parent存储的是对应结点的编号,而不是它的地址,因为地址会改变,再根据它们之间对应关系用地址链接成一个树
if(T == NULL){//第一个读取到的数据为根节点的数据
T = (node *)malloc(sizeof(node));
T->firstchild = NULL;
T->level = tree.level;
strcpy(T->menu_name,tree.menu_name);
T->nextbrother = NULL;
T->parent = NULL;
T->t = tree.t;
}else{//根据firstchild、nextbrother、parent中存储的第一个孩子结点,下一个兄弟结点以及父菜单结点的编号构建菜单树
p = (node *)malloc(sizeof(node));
p->firstchild = NULL;
p->level = tree.level;
strcpy(p->menu_name,tree.menu_name);
p->nextbrother = NULL;
p->t = tree.t;
temp = Inorder(T,(int)tree.parent);
p->parent = temp;
if(temp->firstchild == NULL){
temp->firstchild = p;
}else{
q = temp->firstchild;
while(q->nextbrother!=NULL){
q = q->nextbrother;
}
q->nextbrother = p;
}
}
}
printf("\n");
printf("文件读取成功!\n");
printf("\n");
return T;
}
这一功能花费时间最长,也是感觉最难的部分,将树的结构存到文件中要存下各个结点之间的关系,然后要通过这些关系来恢复之前存储的二叉树。将它们之间的关系存到文件中我是利用结点中的firstchild、nextbrother和parent这三个指针,三个指针本来是存结点地址的,但是重新构造二叉树结点的地址和之间存储的地址不一定相同,此方法行不通,我就不存该结点的地址,而是存它的唯一编号,这样下来,firstchild、nextbrother和parent存储的分别是第一个孩子结点的编号、右侧兄弟的编号以及上层菜单结点的编号,再通过对应关系来恢复二叉树。
10、最后一个功能,查找菜单相关信息以及它的上层菜单和子菜单
void search_menu(node *T){//根据菜单编号查询菜单,显示该菜单相关信息并显示上层菜单以及子菜单
char menu_id[20];
node *p,*q;
printf("\n");
printf("请输入要查询的菜单编号:");
scanf("%s",menu_id);
getchar();
p = Inorder(T,strtol(menu_id,NULL,0));
if(p == NULL){
printf("无此菜单项!\n");
return;
}
printf("\n");
printf("菜单名称:%s,菜单级数:%d级菜单\n",p->menu_name,p->level);
printf("\n");
if(p->parent!=NULL&&p->parent!=T){
printf("上层菜单:%s\n",p->parent->menu_name);
printf("\n");
}
if(p->firstchild != NULL){
printf("其子菜单:\n");
printf("\n");
q = p->firstchild;
while(q){
print_str(' ',10);
printf("%s\n",q->menu_name);
printf("\n");
q = q->nextbrother;
}
}
}
11、主函数
void main(){
node *T = read_file();//程序默认从文件中读取菜单
char chioce;
while(1){
if(T==NULL){//如果文件不存在或从文件中读取失败,就新建一个菜单
printf("\n现重新建立菜单!\n");
printf("\n");
T = creat_Tree();
}
printf("\n");
print_str(' ',10);
printf("菜单管理系统");
print_str(' ',10);
printf("\n");
print_str('*',40);
printf("\n");
print_str(' ',10);
printf("1、新建菜单\n");//重新建立一个菜单
print_str(' ',10);
printf("2、读取菜单\n");//从文件中读取菜单并显示
print_str(' ',10);
printf("3、添加菜单\n");
print_str(' ',10);
printf("4、删除菜单\n");
print_str(' ',10);
printf("5、修改菜单\n");
print_str(' ',10);
printf("6、查询菜单\n");
print_str(' ',10);
printf("7、保存菜单\n");//将当前编辑之后的菜单保存到文件中
print_str(' ',10);
printf("8、退出程序\n");
print_str('*',40);
printf("\n");
printf("\n");
printf("当前菜单如下:\n");
printf("\n");
display_menu(T->firstchild);
printf("\n");
printf("请输入编号选择<1-8>:");
scanf("%c",&chioce);
getchar();
switch(chioce){
case '1':T = creat_Tree();break;
case '2':T = read_file();break;
case '3':add_menu(T);break;
case '4':delete_menu(T);break;
case '5':edit_menu_name(T);break;
case '6':search_menu(T);break;
case '7':save_menu(T);break;
case '8':return;
default:continue;
}
}
}
至此,程序的各个功能都已经实现了,在程序中你会发现使用了大量的一个单独的getchar(),这有什么用呢?getchar()它是用来接收键盘缓冲区的第一个字符,如果你输入“iejgoei”并按下回车,getchar()就会接收到第一个字符 i 。
while(1){
printf("请输入编号选择<1-8>:");
scanf("%c",&chioce);
getchar();
}
如上述代码,你输入一个数字并按下回车,此时键盘缓冲区中会有两个字符,一个是数字,一个是回车符“\n”,要是没有getchar(),首先数字会被chioce接收,第二次循环时,回车符会直接被chioce接收,不会等待你输入数字的,因为键盘缓冲区中已经有了字符它会自动接收。所以getchar()起到了清除缓存的作用,它将键盘缓冲区中的回车符给吸收了,让scanf函数能起到想要的作用。
还有一个函数和getchar()很想,它是getch()函数,这个函数是一个不回显函数,当用户按下某个字符时,函数自动读取,无需按回车,它这样的特性就可以用来实现“按任意键继续的。。。”这一功能,将它放在函数末尾,程序会等待你输入一个字符才会接着执行。
4、效果展示
程序默认从文件中读取菜单并显示,若文件并不存在,则要求重新建立一个菜单
新建之后的效果,括号中的为菜单对应编号
这是从文件中读取后显示的效果
文件中格式
查询效果
其他的就没什么好演示的了,注意,在新建菜单完成之后,再去读取文件中的菜单,文件存在就会显示文件中的菜单,你也只能在这个菜单上进行相关操作了,之前你新建好的菜单会被覆盖掉,因为不管是从文件中读取还是自己新建菜单都会返回一个根结点,而此程序所有操作都会基于根结点来,你根结点变了,所操作的对象也就变了;文件要是不存在又会让你重新建了。同样,从文件中读取的菜单状态下去选择新建也只能操作新建的菜单了,但是你新建好后还可以在读取文件中信息,因为文件中数据还在。
好了,到这里也就结束了,我c语言学的不深,代码质量就这样,也欢迎各位对代码的纠正。