基于C++实现(MFC界面)家谱管理系统

一、题目:家谱管理系统

二、内容:

2.1 概述

2.1.1 选题原因

做此题的原因是因为可以比较方便的记录家族历代成员的情况与关系,能很好的保存家族每一代的信息,而不用人工纸质的方式来存取家谱,更便于人们保存和使用。

2.1.2 题目的理解和分析

该程序带有MFC界面,有树形控件来展示家庭成员的层次关系,还有list控件来浏览家庭成员信息。以及有对家谱成员的添加,删除,修改,查询功能,统计男女比例和求平均年龄,以及有对家谱成员信息保存到文件和读取文件的家谱成员信息。①首先是MFC界面的设计,需要输入输入框让用户输入,需要静态文本提示用户,还有一些展示成员信息的控件,以及按钮,完成对事件的监听功能,以及一些容错的对话框等。②然后通过对用户输入的信息来建立树形结构来存储,写出对应的遍历查找删除等操作。

2.1.3 功能模块划分

  • FamilyRecordDlg.cpp,主要写的是对MFC界面对输入框的数据获取,按钮监听,以及对展示框的数据更新操作。
  • FRTree.cpp,主要是存储家庭成员的信息结构,以及对家庭成员的操作,还有队列的操作。
  • IDD_FAMILYRECODER,家谱管理系统的总界面
  • IDR_MENU1,家谱管理系统的菜单

2.1.4 开发环境

这个家谱管理系统在VS2013编译器上开发。

2.1.5 技术要求

懂得运用c和c++编程,并且会基本的MFC界面的知识,以及文件的读取和保存,还有对树的创建,遍历等操作。

2.2 程序概要设计

2.2.1 程序流程

双击打开家谱信息管理系统的EXE文件可进入系统。进入系统后,该系统顶部有菜单栏,有文件和操作,文件中包括关闭程序,导入信息,导出信息,其中操作有清空系统家谱的操作。

1.首先可以进行导入信息,或者选择逐个添加家庭成员的信息。其中除了第一位祖先的父亲可以设置不输入外,后面的每一个人输入都要带上父亲的名字,并且输入的父亲名字的信息已经添加过在系统中。而且添加性别必须是男或女,否则不让添加。添加完信息后,底部有一块list框可以看添加的信息,以及左侧有一块树可以看添加的成员信息对应的层次关系。

2.添加完数据后可以浏览信息界面的按钮,点击后会由输入框来显示list表中的第一条数据,同时输入框不能再进行输入,然后可以点击上一条记录和下一条记录来查看信息,也是在输入框查看。或者添加完信息后直接点击下一条记录或上一条记录,也能直接进入浏览信息界面进行查看记录。如果此时需要回去添加信息,需要先点击添加信息界面,才能让输入框能进行输入,再开始添加信息。

3.然后是查询,如果用户登录到该系统,没有添加过任何用户,会提示用户添加数据后再进行查询。如果用户添加过数据,则点击查询后,系统没有此人则提示用户输入正确姓名,有此人则会进入到浏览信息界面,让输入框展示查询用户的信息。

4.修改信息,如果用户登录到该系统,没有添加过任何用户,会提示用户添加数据后再进行修改。如果用户添加过数据,则点击修改后,系统会系统会将用户的信息展示到输入框让用户修改其中的数据,但是不能让他更改父亲姓名。如果没有则让用户输入正确的姓名进行修改。同时需要更新list表,树状图的展示数据。

5.删除信息,如果用户登录到该系统,没有添加过任何用户,会提示用户先添加数据后再进行删除。如果添加过数据,点击删除后,确认系统有无此人,无提示要先添加数据,有则进行删除,删除同时也要删除他的孩子节点,同时要更新树和list表。

6.导出信息,如果当前系统没有信息,会提示用户请先添加数据后再进行导出, 如果有信息,提示导出成功。

7.统计男女比例,计算出当前系统的男女比例。

8.平均年龄,计算出当前系统人的平均年龄。

9.退出程序点击文件中的关闭即可。

2.2.2 程序中的类中方法的作用

  • :bool CFRTree::AppendChild(CString fname, CString cname, CString sex, CString edu, CString pro, CString age, CString birthD, CString deathD); 添加用户输入的家庭成员信息函数
  • :void CFRTree::initValue(pointer p); 对节点进行初始化操作
  • :pointer CFRTree::DataFind(CString name); 查找数据
  • :void CFRTree::SaveInformation(pointer info, CString cname, CString fname, CString sex, CString edu, CString pro, CString age, CString birthD, CString deathD); 将信息保存到节点
  • :pointer CFRTree::GetParent( CString name); 获取该节点的父节点
  • :int CFRTree::DataDelete(pointer child); 删除节点
  • :int CFRTree::empty_lkqueue(); 判断队列是否为空
  • :pointer CFRTree::gethead_lkqueue(); 获取对头节点
  • :void CFRTree::en_lkqueue(pointer enQ); 入队
  • :pointer CFRTree::de_lkqueue(); 出队
  • :void CFRTree::WriteData(FILE *fp,pointer temp); 将信息写入文件中
  • :void CFRTree::ImportData(FILE* fp,pointer &temp) 将文件的信息导入系统中
  • :void CFRTree::staticSexData(pointer p); 统计性别和年龄数据
  • :void CFRTree::initCnt(); 初始化年龄性别的统计数据
  • :HTREEITEM createTreeCtrl(HTREEITEM temp, CString fname, CString cname); 创建树控件
  • :HTREEITEM findTreeCtrl(HTREEITEM temp,CString name);通过名字找树控件的节点
  • :void deleteTreeCtrl(HTREEITEM temp); 删除树控件的节点
  • :void OnBnClickedAddBtn(); 点击添加信息响应函数
  • :void OnBnClickedFindButton6(); 点击查询信息响应函数
  • :void OnBnClickedAddInformButton2(); 添加信息界面按钮响应函数
  • :void OnBnClickedLookInformButton3(); 查看信息界面按钮响应函数
  • : void OnBnClickedAmendButton7(); 修改按钮响应函数
  • :void OnBnClickedDeleteButton8(); 删除按钮响应函数
  • :void OnBnClickedSureAmendButton9(); 确定修改按钮响应函数
  • :void OnBnClickedImportButton10(); 导入信息按钮响应函数
  • :void OnBnClickedWriteButton11(); 写入信息按钮响应函数
  • :void OnBnClickedNextButton4(); 下一条记录按钮响应函数
  • :voidOnBnClickedLastButton5(); 上一条记录按钮响应函数
  • :void On32775(); 菜单清除家谱数据响应函数
  • :HTREEITEM createTreeCtrl(HTREEITEM temp, CString fname,CString cname)创建树控件函数
  • :void importTreeCtrl(HTREEITEM temp, pointer p);导入树控件函数
  • : HTREEITEM findTreeCtrl(HTREEITEM temp, CString name);寻找树控件函数
  • :void OnBnClickedStaticDataButton12();计算男女比例按钮响应函数
  • :void OnBnClickedStaticAgeButton13();计算年龄按钮响应函数

2.2.3 程序流程框图

总流程

三、程序详细设计

初始化家谱管理系统界面

BOOL OnInitDialog();
分析:

menu.LoadMenu(IDR_MENU1);首先是加载ID为IDR_MENU1的菜单资源,然后用SetMenu(&menu);将menu加载到指定窗口。

m_promptFont.CreatePointFont(100, _T("宋体"));是创建一个字体大小为100的宋体,然后使用m_prompttext.SetFont(&m_promptFont);m_prompttext是一些提示用户的静态的输入框的变量,设置为对应的字体,其他类似。

m_ListData.SetExtendedStyle(LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP);第一个设置为有网格线条,第二个可以横向选中状态,第三个是表头可以拖拉。

然后就是对ListCtrl插入表头:m_ListData.InsertColumn(0, _T("姓名"), LVCFMT_CENTER);,比如这个就是插入第一列表头为姓名。

最后是设置ListCtrl设置列宽:m_ListData.SetColumnWidth(0, 100);设置第0行宽100。
添加成员信息:

bool AppendChild(CString fname, CString cname, CString sex, CString edu, CString pro, CString age, CString birthD, CString deathD);
分析

根据用户输入信息传入,依次存储到变量中。

而其中的cname存储用户输入的孩子姓名的变量,t是定义的全局变量,是家谱树的根结点,传入查询孩子的函数 pointer DataFindPointer(pointer tree,CString name);首先DataFindPointer()去用递归,树的先根遍历实现的,首先会根据传入的tree是不是为NULL,if会判断这颗树或者子树是否为空,如果为空,直接就return NULL,返回上一层递归,或者当根结点都为空时,直接return NULL,表明该树找不到name这个节点。

如果不为空,进入下一个if判断当前树的根结点的名字是否等于传入的name,如果相等,则将根结点return回去,即找到了名字为name的根结点。

如果不相等,就会进入else去执行里面的递归,首先是去递归左子树,同样是会执行前面的if来判断空或者是相等,但是此时我声明了一个pointer类型的变量p,用来存储递归DataFindPointer()函数的返回值,因为如果在左子树当中找到与name相等的节点,会return回来,存储到p中,不相等的话,会return空值,表明没有找到,再去执行if判断,p是否为NULL,如果不为空,即是在左子树中找到了名字为name的节点,即不用再去右子树递归,如果没有找到,则继续遍历右子树,同理,右子树也需要用p存储DataFindPointer()函数的返回值。最后需要return p回去,让调用该函数知道是否有找到名字为name的结点。

我在函数AppendChild()中定义了pointer类型的child变量,用来存储DataFindPointer的变量,接着便是进入if来判断child是否为NULL,如果查找到name结点已经存在树中的话,即不为NULL,直接return false,表示该成员已经存在,不能再进行添加。

再到DataFindPointer()函数,传入t根结点,和用户输入的fname即是父结点的名字,去树中找,且用pointer类型的变量p存储函数的返回值。当p为NULL并且t不为NULL的时候,即是树此时不为空,但是找不到父结点 ,即返回false,因为除了根结点可以不存在父结点,其他结点都应要有父结点。

最后是分为三种情况,一种是t=NULL,即当前树为空,需要创建根结点,即是祖先,即t=new node,为t存储结点的结构体分配内存,还需要t->data = new information,需要为t内的一个data存储信息的结构体分配内存,然后是调用了一个SaveInformation(),参数即是t为当前创建的结点,还有ApeendChild传入的用户输入的信息,保存到data结构体内,同时结点t->lchild 和t->rbrother 都需要赋值为NULL,因为为了防止下次添加再继续查询没有赋值为NULL的话是个野指针,而导致程序有错,做完return true回去,表明添加结点成功。

第二种是没有长子,p是前面得到的父结点,即是执行if条件p->lchildNULL,
则做跟根结点一样的操作,new结构体为它们分配内存,同时调用SaveInformation() 函数保存信息,同时p->lchild->lchid和p->lchid->rbrother都需要赋值为NULL。
第三种 就是有长子的情况,也即是p->lchild!=NULL,首先,定义了一个pointer类型的变量temp,用来存储p->lchild,即是保存长子。此时有长子也分为两种情况,一种是仅有一个长子,即是执行if,条件为temp->rbrother为NULL,即此时也是需要进行new操作为结构体分配内存同时保存信息,以及temp->rbrother->lchild和temp->rbrother->rbrother都需设置为NULL。然后第二种便是不止一个儿子,因为我用的是孩子兄弟链表,即右边保存的都是自己的兄弟。然后通过while循环找到temp->rbrother!=NULL的时候,即里面是temp不断指向temp->rbrother,即访问的都是长子的右兄弟,知道下一个为NULL,循环跳出,此时temp即指向的是最后一个右兄弟,则进行new操作为结构体分配内存,同时保存信息,且temp->rbrother->lchild和temp->rbrother->rbrother都需要设置为NULL。
找名字为Name的结点:
pointer DataFindPointer(pointer tree,CString name)
上面2以及分析过此代码,此处不再分析。
保存成员信息函数:
void SaveInformation(pointer info, CString cname, CString fname, CString sex, CString edu, CString pro, CString age, CString birthD, CString deathD)
分析
因为很多地方都需要用到将用户传入的值赋值给结点进行保存,所以避免代码冗余,写成了一个函数。同时函数中也做了对年龄的处理,如果用户没有输入年龄,而不是让他为空,而是把它转化为0,使用了CString转数字的方法_ttoi(CString类型字符串)。
获取父结点函数:
pointer GetParent( CString name)
分析
首先是定义了两个pointer类型的变量temp和temp1,temp和temp1都是用来待会用来保存父结点的变量,便于找到子节点后,父结点也被保存,直接return父结点回去即可。首先,先用if判断树t不为NULL,才能去找,如果为NULL,直接return NULL回去。然后便是先用if检查根结点的name等不等于传进来的孩子结点name,如果相等的话直接返回NULL,因为根结点没有父结点。
如果树既不为空,也不是根结点的话,我的思路是通过队列来实现的,一层一层关系的去访问(不是层序遍历),只是关系上的层序,就是找到一个根结点,然后访问他的所有孩子结点有无相等的,有则返回根结点。
首先将根结点入队,en_lkqueue(t),然后是while循环判断队列为不为空,如果空即是所有结点已经访问完,然后用temp变量存储着出队的根结点,temp = de_lkqueue();存储了根结点之后就去访问根结点的所有孩子。首先是访问根结点的左孩子,但是此时要注意,因为根结点不一定有左孩子,所以先需要if判断temp->lchild根结点的左孩子(长子)不为空,如果为空,则此层关系已经遍历完了。当不为空时,则用if判断temp->lchild长子的名字是否与传进来的name相等,相等直接return temp,temp即刚刚保存的根结点。
如果根结点的长子访问了不相等,则此时就需要去循环遍历长子的右兄弟了。首先,再将根结点用temp1存储起来,先将已经访问过的temp->lchild入队(即是关系上的层序),然后temp=temp->lchild,让temp指向长子,然后去循环遍历长子的兄弟,即用while循环,当temp->rbrother不为NULL的时候,temp=temp->rbrother循环指向右兄弟,同时且循环判断右兄弟temp->data->nameStr是不是等于传进来的结点name,相等则返回刚刚存储的根结点temp1.再循环的同时,每检查一个兄弟,都需要入队一个(关系上的层序)。
删除树的结点函数
int DataDelete(pointer child);
t是全局定义的家谱的根结点,首先先执行if条件判断根结点t->data->nameStr和传进来的child->data->nameStr名字是否相等,相等的话,即是删除整一棵树,则调用DeleteTree(t)函数,传入根结点t, 首先DeleteTree()函数是通过递归来后根遍历来实现的删除整棵树的,从左右子树后面开始进行删除,最后删除根结点。首先是递归遍历左子树DeleteTree(t->lchild),然后是递归右子树DeleteTree(t->rbrother);最后便是删除操作delete t,t=NULL。
如果不是根结点,则进行删除首先是要找到他的父结点,才能进行删除。所以,我定义了一个pointer类型的变量parent,用来存储调用GetParent(child->data->nameStr)的返回值。
然后就是除了根结点之外,删除分两种情况,一种是删除的是长子的情况,即用if判断到parent父结点指向的lchild长子的名字等于child结点的名字,即temp存储长子parent->lchild,再DeleteteTree(temp->lchild),即删除要删除结点的左孩子的子树,如果右兄弟不为NULL,则让parent->lchild指回temp->rbrother即可,再delete temp长子。
第二种不是长子的情况,首先先将parent->lchild赋值给temp,记录长子的位置,去找长子的兄弟。首先if判断长子的右兄弟即temp->rbrother不为NULL,再进入while循环找到要删除的孩子,条件即是temp->rbrother的名字等于child的名字,即跳出,循环体则是不断往右兄弟找,temp = temp->rbrother,最后找到temp即为要删除的孩子的上一个兄弟,然后是进行删除操作,我们要删除的是该节点,以及他的子孙节点,但不能删除兄弟,则DeleteTree(temp->rbrother->lchild),删除他的孩子节点,然后再把定义了一个类型为pointer的变量p,保存删除节点temp->rbrother,再把其余的兄弟节点连接起来,temp->rbrother=temp->rbrother->rbrother,最后delete p,p=NULL即可。
删除树的函数:
void DeleteTree(pointer &t);
前面已经分析过,此处不再分析。
向文件写入数据函数
void Write();
首先是定义了一个FILE类型的_fp指针,同时用fopen以wb的方式打开一个二进制文件,只允许写操作。然后执行if,当fp不为NULL的时候,即文件打开成功,执行WriteData(fp,t)函数。而WriteData(FILE _fp,pointer temp)有两个参数,一个是指向文件的指针,还有一个是传入的节点,需要写入文件的节点。我是采用递归,先根遍历用虚节点代替空来导出信息的。首先我定义了一个CString类型的遍历str为"@",还有一个space="¥",首先@是用来填补NULL的节点,然后¥是用来区分每一个数据的。然后是if判断tempNULL,如果NULL的话就用fprintf输出一个@,再输出一个¥区分下一个人的数据。然后如果不为NULL,就是fprintf一个数据,再fprintf一个¥,来隔开每一个不同的数据。然后开始递归写左孩子部分WriteData(fp,temp->lchild),接着就是递归右兄弟Write(fp,temp->rbrother).最后需要关闭文件操作,fclose(fp);
向系统导入文件数据:

void Import();

首先是定义了一个FILE类型的*fp指针,同时用fopen以rb的方式打开一个二进制文件,只允许读操作。然后执行if,当fp不为NULL的时候,表示文件打开成功。且要当前系统没有家谱数据才能进行导入,即用if来判断tNULL,如果不为空,则不能进行导入,如果为空,则t = new node; t->data = new information;为结构体初始化分配内存,然后用ImportData(fp,t);函数来导入数据。ImportData(fp,t)函数参数是fp指向文件的指针,temp是指向树的根节点。首先定义了CString 类型的cstr和ageStr,char类型的c,
cstr是用来存储读取的c然后转化为CString类型的变量,ageStr是读取字符串后再转化为数字的。首先用if判断当前文件的光标有无指到最末尾,指到末尾直接return,否则继续读,fread(&c, sizeof(char), 1, fp);每次从fp指向的文件中读一个字符,大小为1,存到c中,然后用cstr.Format(T("%c"), c);将字符转化为CString类型。先判断读取的是否为@节点,如果是的话,temp赋值为NULL,然后再fread一次,因为@后面我在保存的时候添加了¥来分割数据。如果不等于@,则说明该节点不为空,因为我存了八个数据,所以最外面是一层for循环,需要读八次,然后因为我导出信息的时候,每个数据都用¥隔开,所以里面是一层while循环,条件是不等于¥,即没有读到¥,就继续追加数据,例如名字,即temp->data->nameStr += cstr,而且此时是if(i1),即需要按照顺序写入的顺序来读。然后就是while循环外是如果i2的话,也就是第二次,因为我们写入的是数字年龄,但是读入是字符串,所以进行了一次转化,用了ttoi(ageStr)将ageStr转化为数字即可。然后就是开始去递归创建左右子树。但是递归之前需要先为左右子树的节点先分配号内存,即temp->lchild = new node;temp->lchild->data = new information;然后进行递归ImportData(fp,temp->lchild),右子树同。最后需要关闭文件操作,fclose(fp);
统计性别和年龄
void staticSexData(pointer p);
统计的话即是通过递归去遍历每一个节点。首先,我是定义了三个全局遍历,都是int类型,是menCnt男人的个数,womenCnt女人的个数,ageSum年龄和。首先if判断如果传入的根结点p是空,就return。然后就是一个if和else判断是男还是女,男就menCnt++,女的话就womenCnt++,然后每一次ageSum都需要+=每个节点的年龄。然后是递归左右子树。
void initCnt();
这是我用来初始化三个全局变量,menCnt,womenCnt,ageSum的函数,因为如果多次点击按钮来计算的话,会导致加上上一次的值。
添加信息按钮函数:
void OnBnClickedAddBtn()
首先,我定义了一个全局变量为int类型的judgeLookAndAdd,用来判断在浏览界面还是在添加信息界面,当在浏览的时候操作时就置0,添加为1。即我首先是if判断当前judgeLookAndAdd是否为0,为0提示需要先到添加界面。然后我定义了CString类型的cname等用来存储输入框信息的变量,用GetDlgItem(IDC_EDIT1)->GetWindowText(cname);获取ID为IDC_EDIT1的输入框内容存储到cname中,其他同理。然后是我先用if判断了树的根是不是为NULL,如果NULL的话,可以直接添加,但是如果不为NULL,需要先去检查有没有出现同名情况,直接去调用DataFindPointer()函数,同时我也对输入框做了限制,比如性别输入框,我设置了只可以等于男或女,如果不等于男或者不等于女,弹出提示需要输入男或女MessageBox(_T("性别请输入男或女"), _T("提示"));。还有就是一个cname的判空处理,如果输入的孩子姓名为空,提示先输入姓名再添加信息,调用AppendChild()函数,用bool类型来判断有无添加成功,添加成功这时候来给list框插入数据。插入数据首先我是定义了int类型的itemCount变量,通过list的ID名GetItemCount()获取当前的数目,然后再放到最后一个数据后面m_ListData.SetItemText(itemCount, 1, age);。同时添加成功后需要晴空输入框内容GetDlgItem(IDC_EDIT1)->SetWindowText(_T(""));,然后就是建立树的控件,首先我用树控件的ID获取了树控件的根结点m_TreeCtrl.GetRootItem();存储到了HTREEITEM 类型定义的一个全局变量rootTree;然后判断rootTree是否为NULL来判断是否需要建立根节点,为NULL的话先插入根结点TV1_ROOT, m_TreeCtrl.InsertItem(cname, 1, 0, TVI_ROOT);,否则执行createTreeCtrl(rootTree,fname,cname)建立树控件的函数。这个函数我也是通过递归去建立树控件的,首先我是对判断传进来的树控件句柄temp是否为NULL,为NULL直接return,不为NULL的话就是获取句柄的值来比较fname,是否相等,相等,即插入到该句柄下,m_TreeCtrl.InsertItem(cname,1,0,temp);然后return即可。否则先先获取句柄的下一个孩子节点,GetChildItem(temp句柄)再来递归孩子节点,同理也需要递归兄弟节点。
查询按钮函数:
void OnBnClickedFindButton6()
首先是需要获取用户输入的名字,我定义了CString类型的memberName,用来存储用户输入的名字,然后判断树t是否为空,如果为空,提示添加数据后再查询。再然后如果用户没有输入名字,点了查询,我做了判断如果获取到的用户输入的名字为空,提示先输入查询的姓名。然后是调用DataFind()函数在树中查找结点,如果为NULL,即没有此人,提示输入正确的姓名,如果正确,则进入浏览界面,输入框用来展示数据,全部设置为不可输入GetDlgItem(IDC_EDIT1)->EnableWindow(false);,并修改静态文本,当前是浏览界面,judgeLookAndAdd同时设置为0,最后用SetWindowText把数据展示到输入框上。
修改按钮函数:
void OnBnClickedAmendButton7();
首先我是判断了树t是否为NULL,为NULL即系统没有数据,提示先添加数据。如果有,则同时设置为输入框可输入,因为待会要让用户进行输入,然后获取用户输入的姓名存储到CString类型的memberName变量中,判断如果memberName为空,即提示先输入姓名再修改。如果不为空,则用DataFind()函数用memberName去查询,如果查不到,即返回值我用pointer类型的p存储,即p为NULL,则提示输入正确姓名。正确的话直接把返回的p结点的数据都用SetWindowText赋值到输入框。
确定修改按钮函数:
void OnBnClickedSureAmendButton9();
首先我是先判断当前时浏览还是添加界面,如果浏览界面,不能点击确定修改按钮。
然后获取到用户输入框的修改的姓名,用DataFind()找到的返回值存储到pointer类型的p中,通过获取到输入框用户需要修改的姓名,来赋值给p结点。同时需要修改list展示的数据,我通过循环去找list中的item,首先用int类型存储了list的item数目,接着是for循环,如果名字等于用户输入的名字,则用SetItemText修改。树控件先调用了findTreeCtrl函数,来找到该句柄。这个函数我是通过然后通过递归句柄来实现的。首先是如果传入的句柄为空,直接return,然后就是先匹配根结点是否与结点名字相同,相同返回句柄,否则去递归孩子结点和兄弟结点。再用SetItemText来修改树控件的值。
删除按钮函数
void OnBnClickedDeleteButton8();
首先我是检查了当前系统有无根结点,即没有数据的话tNULL,则提示先添加数据后再删除。否则获取用户输入的姓名存储到memberName中,如果为空,提示先输入姓名。否则用DataFind函数去查找,返回值存储到pointer类型的child中,如果为空,提示输入正确的姓名后再删除。如果找到,同时需要删除树的结点,DataDelete(child)函数,用int类型delJudge来判断是否删除成功,删除成功执行if,同时先去找到树控件的句柄,先通过findTreeCtrl()函数来找到句柄,在通过deleteTreeCtrl(temp)函数来找到句柄,这个函数我是通过递归来实现的,根据传入的temp句柄,去递归删除Item。如果没有删除成功,提示删除失败,同时输入框需要置为空。
导入信息按钮:

void OnBnClickedImportButton10()

首先导入我判断t等不等于NULL,即当前系统没有数据的话才能导入信息。然后调用Import()函数,导入信息,如果导入成功,即此时t!=null,则开始插入List的数据和树控件的数据,首先先插入根句柄,再用importTreeCtrl(rootTree,cfrTree)函数来实现的。这个函数我也是通过递归来实现的,首先根据传入的temp判断为不为空,如果空,则return,否则去递归左孩子,但是递归前需要判断lchild是否为NULL,不为NULL,将左孩子的数据插入到data,同时需要插入树控件到temp下,因为获取的是左孩子,然后是递归右兄弟,即需要获取树控件的父item,通过GetParentItem()获取即可。
导出信息按钮:

void OnBnClickedWriteButton11();

我首先是判断t为不为空,如果不为空,即调用Write()函数导出信息,提示导出成功。否则提示导出失败。
下一条记录按钮:

void OnBnClickedNextButton4();

我首先是用itemNum来存储list的item个数,通过GetItemCount()来获取,然后判断itemnum是否大于0,不是的话则当前系统没有数据,提示用户先添加数据后再查看记录。

然后我是用cstring类型member变量来存储第一个展示用户姓名输入框内容,刚开始的时候是没有的控制,这样,下次我再点击下一条记录按钮的时候,我就能获取到上次一值,然后通过for循环去匹配item的text值,再进行+1取余itemNum,即可获取到下一条数据,同时让输入框展示数据,并且输入框设置不能输入。
上一条记录按钮:

void OnBnClickedLastButton5();

我首先是用itemNum来存储list的item个数,通过GetItemCount()来获取,然后判断itemnum是否大于0,不是的话则当前系统没有数据,提示用户先添加数据后再查看记录。然后也是

然后我是用cstring类型member变量来存储第一个展示用户姓名输入框内容,刚开始的时候是没有的控制,这样,下次我再点击下一条记录按钮的时候,我就能获取到上次一值,然后通过for循环去匹配item的text值,然后首先判断i-1是否小于0,因为是上一次,避免出现负数,如果小于0,就取到最后一个item,即让i=itemNum-1;否则进行-1取余itemNum。同时设置list的data数据,设置输入框不可输入。
清空家谱菜单按钮:

void On32775();

直接调用DeleteTree()函数删除树,以及通过id调用DeleteAllItems()就可以删除树控件的所有结点。然后listData也是通过id调用DeleteAllItems()函数,同时根结点置空。
统计男女比例:

void OnBnClickedStaticDataButton12();

统计前我先调用了initCnt();函数,因为避免多点击统计,造成统计出错,需要全部置0.

然后调用StaticSexData()函数统计即可。

3.1 程序测试

程序添加信息时

第一次输入,因为此时t==NULL,所以父亲名字可以有,也可以没有,直接添加即可,但是输入姓名不能为空,以及性别一定要是男或者女。如果为空,弹出姓名不能空,性别出现其他,弹出性别只能出现男女,成功的话会有提示成功。
修改信息

点击修改后,能查询到下面输入框会让你进行修改信息,除了父亲姓名不能更改,其余都可以更改,当你点击确定修改信息之后,下面展示框会显示你修改后的数据,且会提示修改成功。
删除信息

删除成功后list框也会删除掉数据,同时提示删除成功。
导入家谱

导入成功会提示导出信息成功,否则提示导入失败。
导出信息

导出信息成功也会提示导出信息成功,否则提示失败。
浏览信息界面

首先点击浏览界面会跳到list的第一条数据,点击下一条记录则跳转,上一条则跳到上一条,小于0,则跳到最后一条记录。
平均年龄和性别比例

点击后都会显示对应的数据。
清空家谱

所有的数据都会给清除完成,也可以再次导入数据,或手动添加数据都可以。

四、小结

程序的缺点:

①这一次家谱关于树的操作用的不好,相对比较难,有些bug还没有修改过来

②就是家谱对于用户输入的信息没有限制,暂时只对用户输入的姓名和性别做了限制,这样导致用户输入的数据也可能是不正确的。
程序的优点:

①:这次有写界面,可以让用户更直观

②:如果用户输入错误,以及一些输入的不符合判断前的条件,都会给予用户提示。

③:有控件来展示数据能让用户更清楚的知道层级关系以及添加的数据信息。

④:功能基本能实现
遇到的问题及解决办法:

①:因为MFC没有学过所以这次也都是之前去图书馆借的书,以及去网上看别人怎么写,怎么用来学,边学边实践。

②:然后就是遇到最多的问题就是在删除上,刚开始就是直接是直接传入根结点,然后进行删除它和它的孩子,然后t置为NULL,但是出去后发现不为NULL,最后也找到原因,指针作为参数的话,不能改变地址,它只是改变了指向,只能取修改值,或者使用引用和全局变量都可以。

③:在写界面的时候,没有将#include "stdafx.h"写到第一行,一直报类型转换错误,后面自己去网上找错误才发现。
收获:

这一次数据结构大作业收获还是很大的,从对MFC界面不懂到能实现一个界面,了解了许多界面上的知识。并且在实现这些功能的时候,而且这次是用树的这种结构来实现的, 对树的一些操作也更为的熟悉了。但是也让自己认识到很多不足,对树的许多操作都还是不够熟悉的,以及出现了很多bug,自己也是不断再改,去思考。

相关推荐
网安-轩逸1 小时前
IPv4地址表示法详解
开发语言·php
獨枭4 小时前
CMake 构建项目并整理头文件和库文件
c++·github·cmake
西猫雷婶5 小时前
python学opencv|读取图像(十九)使用cv2.rectangle()绘制矩形
开发语言·python·opencv
liuxin334455665 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
码农W5 小时前
QT--静态插件、动态插件
开发语言·qt
ke_wu5 小时前
结构型设计模式
开发语言·设计模式·组合模式·简单工厂模式·工厂方法模式·抽象工厂模式·装饰器模式
code04号6 小时前
python脚本:批量提取excel数据
开发语言·python·excel
小王爱吃月亮糖6 小时前
C++的23种设计模式
开发语言·c++·qt·算法·设计模式·ecmascript
hakesashou6 小时前
python如何打乱list
开发语言·python
网络风云6 小时前
【魅力golang】之-反射
开发语言·后端·golang