一、系统需求分析
设计一个菜农种植信息管理系统,用来方便地管理蔬菜的种类信息、各类蔬菜的信息以及每年各蔬菜的种植信息。需要实现以下几个功能。
1.数据的加载与保存
数据的加载与保存也就是要实现将当前的数据信息以某种形式存储在文件中,并且程序可以再次加载并成功识别。加载与保存分别都有两种方案。对于加载,一种是系统自动实现,即在系统启动时自动加载上次关闭前保存的数据信息;另一种是用户通过界面菜单手动选择加载文件。对于保存,一种是在用户每次进行对数据的修改后,立即保存对应的数据文件;另一种是在系统退出运行前,更新保存文件。本次课设中我选择的是,系统启动时自动加载上次的文件,用户每次修改数据后自动更新保存文件。
2.数据维护
分别实现对蔬菜种类信息、蔬菜基本信息、蔬菜种植信息等三方面基本信息的录入、修改、删除功能。
3.数据查询
本模块根据用户输入信息,查找相应的蔬菜种类信息、蔬菜基本信息、蔬菜种植信息,分为三个子模块。
(1)蔬菜种类信息查询功能
用户输入分类编码,查找并显示该分类信息。包括分类编码、分类名称、该分类下的蔬菜名称。
(2)蔬菜基本信息查询功能
用户输入蔬菜名称中文字符子串,查找并显示蔬菜名称中包含该子串的所有蔬菜基本信息。
用户输入分类编码和营养成分的子串,查找并显示该分类下包含该营养成分的蔬菜基本信息。
(3)蔬菜种植信息查询功能
用户输入蔬菜名称(可支持模糊查找)和种植年份,查找并显示相应蔬菜下该年份的种植信息。
用户输入蔬菜名称(全称),查找并显示该蔬菜下所有种植信息。
4.数据统计
(1)用户指定年份,统计该年各类蔬菜种植总面积、收获总重量,并按总重量降序排序,输出分类名称、种植面积、收获总重量(按种类统计),输出各类蔬菜收获重量柱状统计图。
(2)用户输入某年份,统计以该年份为起始年份的三年内各种蔬菜总面积、收获总重量,并按总重量降序排序,输出蔬菜名称、分类名称、种植面积、总重量。
(3)统计各种类已有的蔬菜数量并输出。
(4)用户输入营养成分,统计并显示还有该营养成分的蔬菜名称。
(5)用户输入年份,统计该年所有蔬菜种植信息并显示。
5.导入导出
(1)实现导入excel表格:蔬菜种类信息表.csv、蔬菜基本信息表.csv、菜农种植信息表.csv的功能,且导入的数据自动追加到对应的系统保存的文件中。
(2)将指定数据导出到excel表格。
将某年各类蔬菜种植信息(年份、分类名称、种植面积、收获总重量)的统计结果写入到文件(按年份各类蔬菜信息统计表.csv)中。
将某蔬菜三年内种植信息(年份、蔬菜名称、分类名称、面积、收获重量)的统计结果写入文件(按蔬菜名称统计信息表.csv)中。
用户自由查询信息(分类编码、分类名称、蔬菜名称、营养成分、种植面积、收获重量),统计并导入文件(用户自由命名.csv)中。
6.图形化界面
实现友好的图形化界面,提升用户体验。
二、总体设计
根据用户需求以及个人设计想法将系统主要分成五个模块,分别是数据载入、数据查询、数据维护、数据统计和导入导出。但五个模块中有交叉,例如导出excel文件中两项直接添加在统计模块中,方便用户统计完成后直接导出。
数据载入仅用于载入数据文件。系统已设定为启动时自动加载上次的文件,当系统加载失败时,用户可以选择手动加载,作为一个备用方案。
数据查询实现查询功能,也就是三种查询对象、五个查询方式,通过用户输入与按钮点击,显示对应的查询信息结果展示页面。
数据维护以列表形式显示分类、蔬菜、种植信息。首页展示分类信息,该页面包括3个子页面,即分类的详细信息(即其下蔬菜页)、删除、修改,蔬菜页面同样包括3个子页面,即蔬菜的详细信息(即其下种植信息)、删除、修改,种植信息包含2个子页面删除、修改。
数据统计实现统计功能,包含5个子页面,分别为统计某年各类蔬菜种植总量、统计三年内各蔬菜种植总量、统计各类蔬菜的数量、按营养成分统计蔬菜名称、统计某年各蔬菜种植信息这五个统计信息的统计结果页面。其中统计各类蔬菜的数量不需要接受用户的输入,直接统计显示即可,其余需要用户输入要统计的年份、营养成分等相关信息。此外,统计某年各类蔬菜种植总量将根据统计结果生成柱状图,统计某年各类蔬菜种植总量与统计三年内各蔬菜种植总量页面添加导出到文件按钮。
导入导出页包含四个子页面,分别为导入蔬菜种类信息表页、导入蔬菜基本信息表页、导入菜农种植信息表页、自由查询并导出页。其中蔬菜种类信息表页、导入蔬菜基本信息表页不接收用户输入,仅显示导入成功或失败提示。导入菜农种植信息表页、自由查询并导出页需要用户输入相关信息,导入菜农种植信息表页仅提示导入成功或失败,自由查询并导出页显示查询结果,用户自由选择是否导出及导出文件名。
数据的保存设计在每一次修改链表之后,因此不再在总体模块中体现了。
系统框架图如下:
三、数据结构设计
系统涉及种类信息、蔬菜信息、种植信息三个结构体,各结构成员及其声明如表3-1、表3-2、表3-3所示。本系统选用三方向的十字交叉链表结构,如图3-1所示。
整个链表的头结点head的next指针指向第一个分类。分类的next指向下一个分类,一直到空结点。分类的蔬菜指针veg_p指向该分类下第一个蔬菜。蔬菜的next指针指向该分类下的下一个蔬菜,直至空结点,蔬菜的种植信息指针pla_p指向该蔬菜下第一个种植信息。种植信息的next指针指向该蔬菜的下一个种植信息,直至空结点。
头文件中定义了这三个结构为新类型,用于函数中建立结构指针。
表3-1 蔬菜种类信息结构表:
中文字段名 | 成员 | 举例 |
分类编码 | char KindCode; | ‘1’ |
分类名称 | char[8] KindName; | “根茎类” |
蔬菜指针 | struct veg_kind *next; | / |
next分类指针 | VegInfo veg_p; | / |
表3-2 蔬菜基本信息结构表:
中文字段名 | 成员 | 举例 |
蔬菜编号 | int VegNum; | 自增长 |
蔬菜名称 | char VegName[20]; | “白萝卜” |
分类编码 | char KindCode; | ‘1’ |
营养成分 | char component[50]; | “铁、钙、胡萝卜素……” |
种植信息指针 | PlantInfo pla_p; | / |
next蔬菜指针 | struct veg_info *next; | / |
表3-3 菜农种植信息结构表:
中文字段名 | 成员 | 举例 |
编号 | int number; | 自增长 |
蔬菜编号 | int VegNum; | 同蔬菜基本信息表中的蔬菜编号 |
种植面积 | int area; | 2:表示2分地 |
收获重量 | float weight; | 公斤 |
种植年份 | char year[5]; | “2015” 2015年 |
next种植信息指针 | struct plant_info *next; | / |
图3-1 菜农种植信息管理系统三个方向的十字交叉链表:
四、详细设计
1.数据维护模块
(1)插入种类
在表尾插入一个新种类,如果该种类已存在,则直接返回。伪代码如下:
#伪代码如下:
p ← head
while p->next不为空
do p ← p->next
if #编码相同and名称相同 then return
end
#新建结点
#存入数据
#插入到p后
(2)插入蔬菜
与插入种类类似,值得注意的是在遍历蔬菜结点前需要检查该分类下的蔬菜结点是否为空(因为作为蔬菜链的头结点即某个分类结点,与蔬菜结点类型不一致,不能一概而论),如果为空就作为第一个蔬菜插入,否则再去遍历。当遇到蔬菜名称已存在的情况时,认定为蔬菜已存在,默认覆盖该蔬菜原信息,对蔬菜信息进行更新。流程图如下。
图4-1 插入蔬菜基本信息流程图:
/*--------------3.插入蔬菜基本信息--------------*/
int InsertVeg(VegKind *head, char veg_name[20], char kind_code, char nutrition[50])
{
int i;
VegKind p=(*head)->next;
VegInfo r;
while(p!=NULL&&p->KindCode!=kind_code) p=p->next; //找到对应类别
if(p==NULL) return ERROR; //没有找到该分类,插入失败
VegInfo t=(VegInfo)malloc(LENG2); //新建蔬菜基本信息结点
strcpy(t->VegName, veg_name); //存入蔬菜名称
t->KindCode=kind_code; //存入种类编码
strcpy(t->component, nutrition); //存入营养成分
t->pla_p = NULL; //种植信息置为空
r=p->veg_p; //r指向该类别下的第一个蔬菜基本信息
if(r==NULL) //若该类别下蔬菜信息为空
{
t->VegNum=1; //蔬菜1
//t->next = p->veg_p;
p->veg_p=t;
t->next=NULL;
return OK;
}
else //该类别下蔬菜信息不为空
{
i=1;
while(r->next!=NULL) //找到最后一个蔬菜
{
if (strcmp(r->VegName, veg_name) == 0) //蔬菜已存在,执行更新
{
r->KindCode = kind_code;
strcpy(r->component, nutrition);
return OK;
}
r=r->next;
i++;
}
if (strcmp(r->VegName, veg_name) == 0)//蔬菜已存在,执行更新
{
r->KindCode = kind_code;
strcpy(r->component, nutrition);
return OK;
}
i++;
t->VegNum=i;
r->next=t; t->next=NULL; //插入到最后一个蔬菜后
return OK;
}
return ERROR;
}
(3)插入种植信息
与前两个插入类似,同样注意是否为蔬菜下第一个种植信息,通过年份来判断该种植信息是否已存在,若存在,则覆盖原信息。
/*---------------4.插入新的种植信息---------------*/
int InsertPlant(VegKind *head, char kind_code, int veg_num, int area,
float weight, char year[5])
{
int i;
VegKind p=(*head)->next;
while(p!=NULL&&p->KindCode!=kind_code)
p=p->next; //查找类别
if(p==NULL)
return ERROR; //没有该类别
VegInfo r=p->veg_p;
while(r!=NULL&&r->VegNum!=veg_num)
r=r->next; //查找蔬菜
if(r==NULL)
return ERROR; //没有该蔬菜
PlantInfo u=r->pla_p;
PlantInfo v=(PlantInfo)malloc(LENG3);
v->VegNum=veg_num;
v->area=area;
v->weight=weight;
strcpy(v->year, year);
if(u==NULL) //若该蔬菜下没有种植信息
{
v->number=1;
r->pla_p=v;
v->next=NULL;
}//作为第一个种植信息插入
else //该蔬菜下有种植信息
{
i=1;
while(u->next!=NULL)
{
if(strcmp(u->year, year)==0) //种植信息已存在,执行更新
{
u->area=area;
u->weight=weight;
return OK;
}
u=u->next;
i++;
}
if(strcmp(u->year, year)==0) //种植信息已存在,执行更新
{
u->area=area;
u->weight=weight;
return OK;
}
i++;
v->number=i;
v->next = u->next;
u->next=v;
} //新信息插入到最后
return OK;
}
(4)删除一个种类
找到要删除种类的前驱,然后删除结点。
/*-----------------5.删除一个种类-------------------*/
int DeleteKind(VegKind *head, char kind_code)
{
VegKind p=*head;
while(p->next!=NULL&&p->next->KindCode!=kind_code)
p=p->next; //查找到删除种类的前一个结点
if(p->next==NULL)
return ERROR; //无该类别,无需删除
VegKind q=p->next;
p->next=q->next;
free(q); //删除
return OK;
}
(5)删除一个蔬菜
不单单是删除种类的增强版,还需要考虑到自增长的蔬菜编号。在删除一个蔬菜结点后,其后的所有蔬菜结点编号都要减一,也就是在删除结点后增加一个修改编号的动作。流程图如下:
/*-----------------6.删除一个蔬菜-------------------*/
int DeleteVeg(VegKind *head, char kind_code, int veg_num)
{
VegKind p=(*head)->next;
while(p!=NULL&&p->KindCode!=kind_code)
p=p->next; //找到种类
if(p==NULL)
return ERROR; //无该类别,删除失败
VegInfo r=p->veg_p;
if(r==NULL) return ERROR; //该类别下无蔬菜,删除失败
PlantInfo u=NULL;
if(r->VegNum==veg_num) //若删除的是该类别下第一个蔬菜
{
p->veg_p=r->next;
free(r);
VegInfo s=p->veg_p;
while(s!=NULL)//蔬菜编号依次改变
{
s->VegNum-=1;
u=s->pla_p;
while(u!=NULL) //种植信息的蔬菜编号也要改变
{
u->VegNum-=1;
u=u->next;
}
s=s->next;
}
return OK;
}
while(r->next!=NULL&&r->next->VegNum!=veg_num)
r=r->next; //找到蔬菜的前驱
if(r->next==NULL)
return ERROR; //无该蔬菜,删除失败
VegInfo s=r->next;
r->next=s->next;
free(s); //删除蔬菜
r=r->next;
while(r!=NULL) //蔬菜编号依次改变
{
r->VegNum-=1;
u=r->pla_p;
while(u!=NULL) //种植信息的蔬菜编号也要改变
{
u->VegNum-=1;
u=u->next;
}
r=r->next;
}
return OK;
}
(6)删除一个种植信息
与删除蔬菜结点类似,同样需要注意修改自增长的编号。
/*-----------------7.删除一个种植信息----------------*/
int DeletePlant(VegKind *head, char kind_code, int veg_num, int number)
{
VegKind p=(*head)->next;
while(p!=NULL&&p->KindCode!=kind_code)
p=p->next; //找到种类
if(p==NULL)
return ERROR; //无该类别,删除失败
VegInfo r=p->veg_p;
while(r!=NULL&&r->VegNum!=veg_num)
r=r->next; //找到蔬菜
if(r==NULL)
return ERROR; //无该蔬菜,删除失败
PlantInfo u=r->pla_p;
if(u==NULL) return ERROR; //该蔬菜下无种植信息,删除失败
if(u->number==number) //若删除第一个种植信息
{
r->pla_p=u->next;
free(u);
PlantInfo v=r->pla_p;
while(v!=NULL) //编号依次减1
{
v->number-=1;
v=v->next;
}
return OK;
}
if(u->next!=NULL&&u->next->number!=number)
u=u->next; //找到种植信息的前驱
if(u->next==NULL)
return ERROR; //无该种植信息,删除失败
PlantInfo v=u->next;
u->next=v->next;
free(v); //删除种植信息
u=u->next;
while(u!=NULL) //编号依次减1
{
u->number-=1;
u=u->next;
}
return OK;
}
(7)修改分类信息
遍历分类结点,找到要修改的分类,然后修改信息即可。需要注意的是,其下每一个蔬菜信息中都包含分类编码,若分类编码进行了修改,则还需要遍历其下每一个蔬菜结点,修改编码信息。
/*-----------------8.修改分类中的信息---------------*/
int ModifyKind(VegKind *head, char kind_code, char new_kind_code, char new_kind_name[8])
{
VegKind p=(*head)->next;
while(p!=NULL&&p->KindCode!=kind_code)
p=p->next; //找到种类
if(p==NULL)
return ERROR; //无该类别,修改失败
if(new_kind_code!=p->KindCode) //如果要修改分类编码
{
p->KindCode=new_kind_code; //修改分类编码
VegInfo r=p->veg_p; //还要修改该分类下每一个蔬菜的分类编码
while(r!=NULL)
{
r->KindCode=new_kind_code;
r=r->next;
}
}
strcpy(p->KindName, new_kind_name); //修改
return OK;
}
(8)修改蔬菜基本信息
遍历分类结点,找到分类,再遍历蔬菜结点,找到蔬菜,修改蔬菜名称或营养成分。这里不支持修改分类编码,可以通过删除再添加新蔬菜的方式达到效果,否则会过于混乱。
/*-----------------9.修改蔬菜基本信息----------------*/
int ModifyVeg(VegKind *head, char kind_code, int veg_num, char new_veg_name[20],
/*char new_kind_code,*/ char new_component[50])
{
VegKind p=(*head)->next;
while(p!=NULL&&p->KindCode!=kind_code)
p=p->next; //找到种类
if(p==NULL)
return ERROR; //无该类别,修改失败
VegInfo r=p->veg_p;
// VegInfo s=NULL;
while(r!=NULL&&r->VegNum!=veg_num)
r=r->next; //找到蔬菜
if(r==NULL)
return ERROR; //无该蔬菜,修改失败
/*
if(kind_code!=new_kind_code) //若蔬菜类别被修改
{
if(RtoLastVeg(&(*head), new_kind_code)==ERROR)
return ERROR; //新类别不存在,修改失败
if(s==NULL) //代表要修改的蔬菜是原类别下唯一的蔬菜
p->veg_p=NULL; //原类别下蔬菜改为空
else
s->next=NULL; //将蔬菜从原类别移除,但不释放
VegInfo t=RtoLastVeg(&(*head), new_kind_code);
if(t==NULL) //新类别下无蔬菜
{
VegKind q=(*head)->next;
while(q!=NULL&&q->KindCode!=new_kind_code)
q=q->next; //找到种类
q->veg_p=r; //将蔬菜及其种植信息移动到新类别下
}
else
{
r->next=t->next;
t->next=r; //r连接到新类别下最后一个蔬菜
}
}
*/
strcpy(r->VegName, new_veg_name); //修改蔬菜名
strcpy(r->component, new_component); //修改蔬菜成分
return OK;
}
(9)修改种植信息
遍历分类结点,找到分类,再遍历蔬菜结点,找到蔬菜,再遍历种植信息,找到对应编号,修改信息。
/*----------------------11.修改种植信息----------------------*/
int ModifyPlant(VegKind *head, char kind_code, int veg_num, int num, int new_area, float new_weight, char new_year[5])
{
VegKind p=(*head)->next;
while(p!=NULL&&p->KindCode!=kind_code)
p=p->next; //找到种类
if(p==NULL)
return ERROR; //无该类别,修改失败
VegInfo r=p->veg_p;
while(r!=NULL&&r->VegNum!=veg_num)
r=r->next; //找到蔬菜
if(r==NULL)
return ERROR; //无该蔬菜,修改失败
PlantInfo u=r->pla_p;
if(u!=NULL&&u->number!=num)
u=u->next; //找到种植信息
if(u==NULL)
return ERROR; //无该种植信息,修改失败
u->area=new_area; //修改面积
u->weight=new_weight; //修改重量
strcpy(u->year, new_year); //修改年份
return OK;
}
2.载入保存模块
(1)保存
以’#’标记空结点,’*’标记非空结点,按遍历分类(遍历蔬菜(遍历种类信息))的顺序进行存储。即先进入分类1、蔬菜1,遍历存储种植信息,接着进入蔬菜2,遍历存储其种植信息。将分类1的蔬菜遍历完成后,进入分类2,依次遍历下去。每进入一个结点,先标记它是否为空,不为空就在标记后接着存储信息,否则按遍历顺序向后遍历。流程图如下:
/*-----------------------12.保存-------------------------*/
int ListSave(VegKind *head, char filename[50])
{
FILE *fp=NULL;
fp=fopen(filename,"wb");//if(()==NULL)
//return ERROR;
VegKind p=*head;
VegInfo r=NULL;
PlantInfo u=NULL;
char isEnd='#'; //#标记NULL结点
char notEnd='*'; //*标记非空结点
p=p->next;
while(p!=NULL)
{
if(fwrite(¬End,sizeof(char),1,fp)!=1) return ERROR; //非空结点标记
if(fwrite(&(p->KindCode),sizeof(char),1,fp)!=1) return ERROR;
if(fwrite(&(p->KindName),sizeof(p->KindName),1,fp)!=1) return ERROR;
r=p->veg_p;
while(r!=NULL)
{
if(fwrite(¬End,sizeof(char),1,fp)!=1) return ERROR; //非空结点标记
if(fwrite(&(r->VegName),sizeof(r->VegName),1,fp)!=1) return ERROR;
if(fwrite(&(r->component),sizeof(r->component),1,fp)!=1) return ERROR;
u=r->pla_p;
while(u!=NULL)
{
if(fwrite(¬End,sizeof(char),1,fp)!=1) return ERROR; //非空结点标记
if(fwrite(&(u->area),sizeof(int),1,fp)!=1) return ERROR;
if(fwrite(&(u->weight),sizeof(float),1,fp)!=1) return ERROR;
if(fwrite(&(u->year),sizeof(u->year),1,fp)!=1) return ERROR;
u=u->next;
}
if(fwrite(&isEnd,sizeof(char),1,fp)!=1) return ERROR; //标记种植信息NULL
r=r->next;
}
if(fwrite(&isEnd,sizeof(char),1,fp)!=1) return ERROR; //标记蔬菜NULL
p=p->next;
}
if(fwrite(&isEnd,sizeof(char),1,fp)!=1) return ERROR; //标记分类NULL
fclose(fp);
return OK;
}
(2)载入
按照保存函数的标记,每读取一个结点必当先读取到一个char类型的标记来得知此结点是否为空,读取顺序与保存顺序一致,每读取一个结点后调用插入函数插入结点信息。
/*-----------------------13.加载-------------------------*/
int ListLoad(VegKind *head, char filename[50])
{
FILE *fp=NULL;
if((fp=fopen(filename,"rb"))==NULL)
return ERROR;
char kind_code;
char kind_name[8];
char veg_name[20];
char component_t[50];
char year_t[5];
int area_t;
float weight_t;
char mark='\0';
int veg_num=0;
while(1) //读取种类
{
fread(&mark, sizeof(char), 1, fp);
if(mark=='#') break; //空结点,结束
fread(&kind_code, sizeof(char), 1, fp);
fread(&kind_name, sizeof(kind_name), 1, fp);
InsertKind(&(*head), kind_code, kind_name);
veg_num=0;
while(1) //读取蔬菜
{
fread(&mark, sizeof(char), 1, fp);
if(mark=='#') break;
fread(&veg_name, sizeof(veg_name), 1, fp);
fread(&component_t, sizeof(component_t), 1, fp);
InsertVeg(&(*head), veg_name, kind_code, component_t);
veg_num++;
while(1) //读取种植信息
{
fread(&mark, sizeof(char), 1, fp);
if(mark=='#') break;
fread(&area_t, sizeof(int), 1, fp);
fread(&weight_t, sizeof(float), 1, fp);
fread(&year_t, sizeof(year_t), 1, fp);
InsertPlant(&(*head), kind_code, veg_num, area_t, weight_t, year_t);
}
}
}
fclose(fp);
return OK;
}
3.数据查询模块
(1)蔬菜种类信息查询功能
这部分要求根据分类编码查找蔬菜分类信息,即遍历分类结点找到编码符合的分类,返回该结点指针即可。
(2)蔬菜基本信息查询功能
根据蔬菜名称子串查询蔬菜基本信息。以蔬菜基本信息结构指针建立空结果链,遍历蔬菜结点,找到符合的蔬菜,复制该结点信息插入结果链。值得注意的是若传进来的参数(即用户没有输入)时,如果不进行判断那么所有的蔬菜都符合,应当通过合适的判断语句避免此情况。流程图如下:
根据分类码和营养成分查找蔬菜基本信息。以蔬菜基本信息结构指针建立空结果链,首先遍历分类结点,找到符合的分类,再遍历该分类下的蔬菜结点。找到符合条件的蔬菜,生成新结点插入结果链,相对前一个逻辑更简单。
/*-----------------15.查找蔬菜信息1--------------------*/
VegInfo searchVeg1(VegKind head, char veg_name[20])
{
VegKind p=head->next;
VegInfo r;
VegInfo chain=(VegInfo)malloc(LENG2); //查找结果链
chain->next = NULL;
VegInfo q=chain;
while(p!=NULL)
{
r=p->veg_p;
while(r!=NULL)
{
if(strstr(r->VegName,veg_name)!=NULL && strcmp("\0",veg_name)!=0) //包含子串
{
//复制蔬菜信息
VegInfo t=(VegInfo)malloc(LENG2); //新建蔬菜基本信息结点
strcpy(t->VegName, r->VegName); //存入蔬菜名称
t->KindCode=r->KindCode; //存入种类编码
strcpy(t->component, r->component); //存入营养成分
t->pla_p=NULL; //种植信息置为空
t->next = q->next; q->next = t; q = q->next; //插入查找链
}
r=r->next;
}
p=p->next;
}
return chain; //没找到返回NULL
}
/*-----------------16.查找蔬菜信息2--------------------*/
VegInfo searchVeg2(VegKind head, char kind_code, char nutrition[50])
{
VegKind p=head->next;
VegInfo r;
VegInfo chain = (VegInfo)malloc(LENG2); //查找结果链
chain->next = NULL;
VegInfo q=chain;
while(p!=NULL&&p->KindCode!=kind_code) p=p->next; //找到对应类别
if(p==NULL) return chain; //没有找到该分类,查找失败
r=p->veg_p;
while(r!=NULL)
{
if(strstr(r->component,nutrition)!=NULL && strcmp("\0",nutrition)!=0)
{
//复制蔬菜信息
VegInfo t=(VegInfo)malloc(LENG2); //新建蔬菜基本信息结点
strcpy(t->VegName, r->VegName); //存入蔬菜名称
t->KindCode = r->KindCode; //存入种类编码
strcpy(t->component, r->component); //存入营养成分
t->pla_p=NULL; //种植信息置为空
t->next = q->next; q->next = t; q = q->next;
}
r=r->next;
}
return chain;
}
(3)蔬菜种植信息查询功能
根据蔬菜部分名称(模糊查找)和种植年份查找蔬菜种植信息。以蔬菜基本信息结构指针建立空结果链,遍历所有蔬菜结点。如果找到符合的蔬菜名,再遍历其下种植信息链,找到符合的年份,复制蔬菜信息及种植信息,插入结果链(默认一年的种植信息仅一条)。流程图如下。
根据蔬菜名称查找蔬菜种植信息。遍历蔬菜结点,找到蔬菜,将结果链头结点的next指向该节点即可,逻辑相对简单。
/*-----------------17.查找种植信息1--------------------*/
VegInfo searchPlant1(VegKind head, char veg_name[20], char year[5])
{
VegKind p = head->next;
VegInfo r;
PlantInfo u;
VegInfo chain = (VegInfo)malloc(LENG2); //查找结果链
chain->next = NULL;
VegInfo q = chain;
while(p!=NULL)
{
r=p->veg_p;
while(r!=NULL)
{
if(strstr(r->VegName,veg_name)!=NULL && strcmp("\0",veg_name)!=0) //包含子串
{
u=r->pla_p;
while(u!=NULL&&strcmp(u->year,year)!=0) u=u->next; //找符合条件的种植信息
if(u!=NULL)
{
//复制蔬菜信息
VegInfo t=(VegInfo)malloc(LENG2); //新建蔬菜基本信息结点
strcpy(t->VegName, r->VegName); //存入蔬菜名称
t->KindCode=r->KindCode; //存入种类编码
strcpy(t->component, r->component); //存入营养成分
//复制种植信息
PlantInfo v=(PlantInfo)malloc(LENG3);
v->VegNum=u->VegNum;
v->area=u->area;
v->weight=u->weight;
v->next = NULL; //种植信息一年一个
strcpy(v->year, u->year);
t->pla_p = v;
t->next = q->next; q->next = t; q = q->next; //插入结点到结果链
}
}
r=r->next;
}
p=p->next;
}
return chain;
}
/*-----------------18.查找种植信息2--------------------*/
PlantInfo searchPlant2(VegKind head, char veg_name[20])
{
VegKind p=head->next;
VegInfo r=NULL;
PlantInfo chain = (PlantInfo)malloc(LENG3);
chain->next = NULL;
while(p!=NULL)
{
r=p->veg_p;
while(r!=NULL&&strcmp(r->VegName,veg_name)!=0) r=r->next; //查找蔬菜
if (r != NULL) { chain->next = r->pla_p; break; }
else p=p->next; //没找到继续在下一个分类找
}
return chain;
}
4.数据统计模块
考虑到该模块有统计总量要求,且要进行排序,所以新定义了一个结构体类型,用于存放统计信息。结构体成员如下。
表4-1 统计信息结构表
中文字段名 | 成员 | 举例 |
分类名称 | char KindName[8]; | “根茎类” |
蔬菜名称 | char VegName[20]; | “白萝卜” |
种植面积 | int area; | 2:表示2分地 |
收获重量 | float weight; | 公斤 |
next指针 | struct sort_list *next; | / |
(1)统计某年各类蔬菜种植总量
按分类遍历每一个种植信息结点,累积面积和重量,生成新结点,插入结果链。然后按照重量降序排序。这里对单链表排序的想法是先将单链表每一个结点按顺序复制到数组中,对数组采用快速排序,排序结束后按照数组顺序重构链表。流程图如下:
/*-----------------19.分类统计某年蔬菜总量 按总重量降序--------------------*/
SortList countKind(VegKind head, char year[5])
{
VegKind p=head->next;
SortList s = (SortList)malloc(LENG4); //统计链
s->next = NULL;
SortList q=s;
VegInfo r;
PlantInfo u;
int area_sum=0;
float weight_sum=0;
int length=0;
int mark = 0;
while(p!=NULL)
{
area_sum = 0; weight_sum = 0; mark = 0;
r=p->veg_p;
while(r!=NULL)
{
u=r->pla_p;
while(u!=NULL&&strcmp(u->year,year)!=0) u=u->next; //找到年份符合的种植信息
if(u!=NULL)
{
mark = 1; //某分类下有该年种植信息
area_sum+=u->area;
weight_sum+=u->weight; //面积重量累加求和
}
r=r->next;
}
if (mark == 1) //某分类下有该年种植信息
{
SortList l=(SortList)malloc(LENG4); //新建统计链结点
strcpy(l->KindName, p->KindName); //复制分类名
strcpy(l->VegName, "\0"); //没有用到蔬菜名,设为空
l->area=area_sum; l->weight=weight_sum; //结点赋值
length++; //数据结点长度加一
l->next = q->next; q->next = l; q = q->next; //插入统计链
}
p=p->next; //进入下一个分类
}
//降序排序
if(length>1) listSort(&s, length);
return s; //如果没有找到sort->next==NULL
}
2)统计三年内各蔬菜种植总量
整体流程与上图基本一致,仅在进入蔬菜结点后对种植信息结点的遍历有所不同。这里要统计三年内的总量,故不能仅仅满足于找到,还应当找全,所以需要判断每一个结点,对种植信息结点使用while循环遍历。判断年份是否符合使用atoi字符串整型转换函数较方便。
排序:
/*-----------------20.单链表降序--------------------*/
//将链表内容存到数组,对数组执行快排,再重构链表
int listSort(SortList *s, int length)
{
SortArray** sort_ = (SortArray**)malloc(LENG4 * (length + 1)); //指针数组?
SortList p=*s;
int i=0;
for(i=0;i<length+1;i++)
{
sort_[i]=p;
p=p->next;
} //将链表结点依次存入数组
quickSort(sort_, 1, length); //排序
*s = sort_[0];
for(i=0;i<length;i++)
sort_[i]->next = sort_[i + 1];
sort_[length]->next = NULL; //重构链表
return OK;
}
/*-----------------21.关于weight的快排-----------------------*/
void quickSort(SortArray *s[], int low, int high)
{
SortArray *temp;
int i = 0, j = 0;
if(low<high) //有两个以上记录
{
i=low;j=high;temp=s[i]; //保存记录到temp中
do{
while(i<j&&s[j]->weight<=temp->weight) j--; //j从右向左扫描通过小于temp的元素
if(i<j) //i,j未相遇
{
s[i]=s[j]; i++; //此时j指示位置空
while(i<j&&s[i]->weight>=temp->weight) i++; //i从左向右扫描通过大于temp的元素
if(i<j) { s[j]=s[i]; j--; } //此时i指示位置空
}
}while(i!=j);
s[i]=temp;
quickSort(s,low,i-1);
quickSort(s,i+1,high);
}
}
(3)统计各类蔬菜的数量
用一个数组s[n]作为返回值,s[1~n]按十字链表顺序记录蔬菜总数,s[0]记录数据个数。按分类对蔬菜结点遍历,每遇到一个蔬菜对应的统计值就加1。最后记录数据长度(即分类数)。因为在数组初始化时需要确定数组长度,所以需要提前计算分类长度。
(4)按营养成分统计蔬菜名称
新建蔬菜信息结构的结果链单链表,查找营养成分包含的蔬菜,复制信息并插入到结果链中,较为容易。
(5)统计某年各蔬菜种植信息
遍历种植信息结点,在每个蔬菜下找到年份符合的种植信息,就复制相关信息并插入到结果链中,继续找下一个蔬菜。流程图如下:
/*-------------------26.统计某年所有蔬菜的种植信息---------------------*/
SortList countYearPlaInfo(VegKind head, char year[5])
{
VegKind p = head->next;
SortList chain = (SortList)malloc(LENG4); //结果链
chain->next = NULL;
SortList q = chain;
VegInfo r = NULL;
PlantInfo u = NULL;
while (p != NULL)
{
r = p->veg_p;
while (r != NULL)
{
u = r->pla_p;
while (u != NULL && strcmp(u->year, year) != 0) u = u->next; //找到某年种植信息
if (u != NULL)
{
SortList l = (SortList)malloc(LENG4);
strcpy(l->KindName, p->KindName); strcpy(l->VegName, r->VegName);
l->area = u->area; l->weight = u->weight;
l->next = q->next; q->next = l; q = q->next; //插入统计链
} //复制需要的信息添加到结果链
r = r->next;
}
p = p->next;
}
return chain;
}
5.导入导出模块
(1)导入
蔬菜种类信息表.csv文件存储方式如下,单元格间以逗号分隔,空单元格逗号间隔保留,但无内容。读取时用fgets函数一行行读取,再用strtok函数对单元格进行分割。需要注意的是strtok对空单元格直接忽略,在这里我用strstr函数定位逗号,来辅助单元格的分割。利用strstr定位到第一个逗号,若接着后一个字符仍是逗号,则进入下一个单元格的读取,否则使用strtok函数分割出第一个单元格读取数据。此外,行末包含换行符,在分割时要考虑到。
/*-----------------27.导入种类.csv------------------*/
int loadKind(VegKind* head, char filename[50])
{
FILE* fp = NULL;
char* line = NULL;
char buffer[1024];
char* tok = NULL;
char kind_code[50][8];
char kind_name[8];
//char veg_name[20];
char temp[1024];
if ((fp = fopen(filename, "r")) == NULL) return ERROR; //打开文件
line = fgets(buffer, sizeof(buffer), fp); //获取第一行
tok = strtok(line, ","); tok = strtok(NULL, ",");//字符串拆分
//读取分类编号
int i = 0;
while(tok && *tok)
{
strcpy(kind_code[i], tok);
tok = strtok(NULL, ",");
i++;
}
i = 0;
line = fgets(buffer, sizeof(buffer), fp); //获取第二行
tok = strtok(line, ","); tok = strtok(NULL, ",\n");//字符串拆分
//读取分类名称,导入分类
while (tok && *tok)
{
strcpy(kind_name, tok);
//printf("%c %s\n", kind_code[i][0], kind_name);
if(InsertKind(&(*head), kind_code[i][0], kind_name)==ERROR) return ERROR;
tok = strtok(NULL, ",\n");
i++;
}
//读取蔬菜
while ((line = fgets(buffer, sizeof(buffer), fp)) != NULL)//当没有读取到文件末尾时循环继续
{
i = 0;
while (strstr(line, ",") != NULL) //当还有逗号时(一个逗号后跟一个蔬菜)
{
line = strstr(line, ","); //定位到第一个逗号
line++; //定位到逗号下一位
if ((*line) != ',' && (*line) != '\n') //如果是蔬菜
{
//printf("%s\n", line);
strcpy(temp, line);
//printf("%c %s\n", kind_code[i][0], strtok(temp, ",\n"));
if(InsertVeg(&(*head), strtok(temp, ",\n"), kind_code[i][0], "\0")==ERROR) return ERROR; //strtok分割出第一个蔬菜
}
//else printf("%c\n", kind_code[i][0]);
i++;
}
}
fclose(fp);
fp = NULL;
return OK;
}
/*-----------------28.导入蔬菜.csv------------------*/
int loadVeg(VegKind* head, char filename[50])
{
FILE* fp = NULL;
char* line = NULL;
char buffer[1024];
char* tok = NULL;
char kind_code;
char veg_name[20];
char component[50];
//char temp[1024];
if ((fp = fopen(filename, "r")) == NULL) return ERROR; //打开文件
line = fgets(buffer, sizeof(buffer), fp); //跳过第一行表头
while ((line = fgets(buffer, sizeof(buffer), fp)) != NULL)//当没有读取到文件末尾时循环继续
{
tok = strtok(line, ","); tok = strtok(NULL, ","); //字符串拆分,跳过第一个编号
strcpy(veg_name, tok); tok = strtok(NULL, ",");
kind_code = tok[0]; tok = strtok(NULL, ",\n");
strcpy(component, tok);
if(InsertVeg(&(*head), veg_name, kind_code, component)==ERROR) return ERROR; //插入蔬菜
//printf("%s %c %s\n", veg_name, kind_code, component);
//tok = strtok(NULL, ",\n"); printf("%s\n", tok);
}
fclose(fp);
fp = NULL;
return OK;
}
/*-----------------29.导入种植信息.csv------------------*/
int loadPla(VegKind* head, char filename[50], char kind_code)
{
FILE* fp = NULL;
char* line = NULL;
char buffer[1024];
char* tok = NULL;
int veg_num=0;
int area = 0;
float weight = 0;
char year[5];
if ((fp = fopen(filename, "r")) == NULL) return ERROR; //打开文件
line = fgets(buffer, sizeof(buffer), fp); //跳过第一行表头
while ((line = fgets(buffer, sizeof(buffer), fp)) != NULL)//当没有读取到文件末尾时循环继续
{
tok = strtok(line, ","); tok = strtok(NULL, ","); //字符串拆分,跳过第一个编号
veg_num=atoi(tok); tok = strtok(NULL, ",");
area = atoi(tok); tok = strtok(NULL, ",");
weight = atof(tok); tok = strtok(NULL, ",\n");
for (int i = 0; i < 4; i++)
year[i] = tok[i]; //最后的tok占6个字节,直接copy会越界
year[4] = '\0';
//printf("\n%d %d %f %s\n", veg_num, area, weight, year);
if(InsertPlant(&(*head), kind_code, veg_num, area, weight, year)==ERROR) return ERROR;
}
fclose(fp);
fp = NULL;
return OK;
}
蔬菜基本信息表.csv、菜农种植信息表.csv的导入大体上与上述流程图的思路是一样的,仅在读取行与单元格分割的细节上有所不同,两表的存储形式如下。
这两个表的第一行都是表头,在读取时要注意跳过,此外,读取菜农种植信息表.csv的年份时,应当采用字符串数组复制,获得的分割字符串含6个字符,直接调用字符串复制函数会越界。
(2)导出
这部分涉及自由查找与导出。关于导出是十分容易的,以可读可写方式打开文件,先写入表头,再遍历链表、依次写入数据(用逗号分割),注意每条记录行末加入换行符。对于按年份各类蔬菜信息统计表.csv、按蔬菜名称统计信息表.csv两张表,表头已存在与文件中,直接定位到文末追加数据即可。流程图如下:
自由查找相对复杂。首先初始化结果链,添加一条判断:当所有查找参数值均为空或初始值时(即用户没有输入查找信息),直接返回,否则所有结点将均符合,不合常理。设置两个标识符,mark:初始值为0,当每个分类下找到第一个满足条件的种植信息时变为1;flag:初始值为0,当每个蔬菜下每找到一个满足条件的种植信息时flag++。
/*---------------30.导出1.csv-----------------*/
int exportYearKindPla(VegKind head, char filename[50], char year[5])
{
SortList chain = countKind(head, year);
SortList p = chain->next;
if(p==NULL) return ERROR;
FILE* fp = NULL;
if ((fp = fopen(filename, "at+")) == NULL) return ERROR; //打开文件
fseek(fp, 0, SEEK_END); //定位到文末
//fprintf(fp, "种植年份,分类名称,种植面积,收获重量\n");
while (p != NULL)
{
//printf("%s %d %f\n", p->KindName, p->area, p->weight);
fprintf(fp, "%s,%s,%d,%f\n", year, p->KindName, p->area, p->weight);
p = p->next;
}
fclose(fp);
fp = NULL;
return OK;
}
/*---------------31.导出2.csv-----------------*/
int exportVeg3YearPla(VegKind head, char filename[50], char year[5])
{
SortList chain = count3YearVeg(head, year);
SortList p = chain->next;
if(p==NULL) return ERROR;
FILE* fp = NULL;
if ((fp = fopen(filename, "at+")) == NULL) return ERROR; //打开文件
fseek(fp, 0, SEEK_END); //定位到文末
//fprintf(fp, "种植年份,分类名称,蔬菜名称,种植面积,收获重量\n");
while (p != NULL)
{
//printf("%s-%d,%s,%s,%d,%f\n", year, atoi(year)+2, p->KindName, p->VegName, p->area, p->weight);
fprintf(fp, "%s-%d,%s,%s,%d,%f\n", year, atoi(year)+2, p->VegName, p->KindName, p->area, p->weight);
p = p->next;
}
fclose(fp);
fp = NULL;
return OK;
}
遍历链表,判断满足条件的结点,每个条件应当与空相或,也就是当用户没有输入此条件,或者用户输入了此条件且结点满足此条件时,这个结点都应当在考虑范围之内。两个标识符的作用在于:mark记录是否已添加某分类信息,避免重复插入分类;flag记录插入种植信息时所在蔬菜的编号。
此外,添加自定义符号函数,用于判断用户给出的>、=、<符号,并作相应运算判断。
自由查找流程图如下:
/*-------------32.自由查找--------------*/
VegKind freeStyle(VegKind head, char kind_code, char kind_name[8], char veg_name[20], char component[50],
char year[5], int area, char opt1[5], float weight, char opt2[5])//狗头
{
VegKind chain;
InitList(&chain);
if(kind_code=='\0'&&!strcmp(kind_name,"\0")&&!strcmp(veg_name,"\0")&&!strcmp(component,"\0")
&&!strcmp(year,"\0")&&!area&&!weight&&!strcmp(opt1,">")&&!strcmp(opt2,">")) return chain;
VegKind p = head->next;
VegInfo r = NULL;
PlantInfo u = NULL;
int mark = 0, flag = 0; //每个分类下找到第一个满足条件的种植信息,mark变1;每个蔬菜下每找到一个满足条件的种植信息,flag++
int count = 0; //某分类下插入了几个蔬菜
while (p != NULL)
{
mark = 0; count = 0;
if (((kind_code == '\0') || (kind_code == p->KindCode))
&& ((strcmp(kind_name, "\0") == 0) || (strcmp(p->KindName, kind_name) == 0)))
{
r = p->veg_p;
while (r != NULL)
{
flag = 0;
if ((strcmp(veg_name, "\0") == 0 || strstr(r->VegName, veg_name) != NULL)
&& (strcmp(component, "\0") == 0 || strstr(r->component, component) != NULL))
{
u = r->pla_p;
while (u != NULL)
{
if((strcmp(year, "\0") == 0 || strcmp(year, u->year) == 0)
&& operation((float)(u->area), opt1, (float)area) == TRUE
&& operation(u->weight, opt2, weight) == TRUE)
{
if (mark == 0)
{ InsertKind(&chain, p->KindCode, p->KindName); mark = 1; }
if (flag == 0)
{
InsertVeg(&chain, r->VegName, p->KindCode, r->component); flag = 1; count++;
}
InsertPlant(&chain, p->KindCode, count, u->area, u->weight, u->year);
}
u = u->next;
}
}
r = r->next;
}
}
p = p->next;
}
return chain;
}
6.图形界面
图形界面的实现使用了ege图形库,整个界面的实现相对简单但繁琐,难点在于鼠标交互与页面间的关系。
利用全局变量二维数组flag(我觉得这个是点睛之笔!)存放每个页面的显示信息。flag[0][0]=0为初始状态,由左侧导航栏选择决定给flag[0][0]赋值1~5,分别代表加载页、查询页、维护页、统计页、导入导出页。flag[1~5]用于分别用于这些页面的子页面,数组存储结构即对应的内容如下表所示。
表4-2 flag存储结构表
0 | 1 | …… | |
0 | 主页面 | / | / |
1 | 载入页子页 | 子页的子页 | …… |
2 | 查询页子页 | 子页的子页 | …… |
3 | 维护页子页 | 子页的子页 | …… |
4 | 统计页子页 | 子页的子页 | …… |
5 | 导入导出子页 | 子页的子页 | …… |
在用户面对一个页面时,只有点击到了正确的位置(即可点的有功能对应的地方),页面才会发生变化,因此每个页面的函数应当包含页面绘制与鼠标点击跳转两个部分,两部分需要分离开来,避免出错。
在统计某年各类蔬菜种植总量的部分还需要输出相应柱状图。该部分已经实现按照重量降序,因此画图的思路是:取最大重量为整张图高度的5/6,即垂直间隔为最大重量的1/5,每个间隔固定大小为40像素,于是就有了一个像素与值的关系,即可确定其他重量的高度。水平间距根据链表长度(即分类数)决定。颜色共5中,由循环的次数mod5决定。
五、运行测试与结果分析
1.数据载入页
2.数据查询页
查询含“菜”的蔬菜:
查询分类编码为’2‘,营养成分含“铁”的蔬菜:
3.数据维护页
分类页
分类编辑
新增分类
删除分类
蔬菜页
种植信息页
编辑、新增
4.数据统计页
统计三年内各蔬菜种植总量
统计各蔬菜的数量
营养成分查询
导入导出页面
六.回顾
对我来说比较有难度的应该是数据统计和导入导出部分。查询也并不难(自由查询除外),是一种顺序的思路,遍历链表即可。统计部分是很多样的,并且相对繁琐。排序部分为了避免麻烦、清晰了当,直接选择了存到数组里排序,时间复杂度上也比直接操作链表要快。导入导出部分是没有接触过的(指对excel的操作),查询资料的时候也顺带学会了一个新的函数——strtok分割字符串函数,和strstr结合读取csv文件很有效!
源代码见本人github仓库:beichenxin (beichenxin) / Repositories · GitHub