一晃又两个月没有更新了,终于我们不放假大学也放假了,车牌也考完了,博客也可以开始正常更新了。在上学期末,我完成了一些有意思的小工程,下面我就为大家分享我的工程,以供交流学习。

实验要求

项目背景

校园卡管理系统是应用于校园卡管系统和应用的软件,该软件在程序设计中有它不可取代的地位, 校园卡给广大师生的衣食住行带来了极大的便利。 而在这门程序设计课程中,希望同学们为校园卡功能管理系统设计一个或多个类(包扩其成员函数和数据成员)。从用户角度出发,其功能应包括增删查改等核心基本核心功能。

实现场景

根据以下场景中的内容实现相应功能,并合理地作出展示,如每一步操作都有相应的文字显示,每次修改学生信息或校园卡信息时都打印出来等等。

场景一:

校园卡管理员确认学生信息输入的权限:
新生入学,校园卡管理员导入学生名单并绑定校园卡:
创建学生 A(名字,学院,班级,专业,学号…),给学生 A 绑定校园卡 B(卡号,余额,充值记录,消费记录,校园卡状态),这里请注意要求学号具有唯一性,在初始化时需要通过比较查询功能确认学号是否重复,从而维护学号的唯一性,学号一经分配就无法再次更改。

场景二:

学生用户具有查看校园卡状态、充值、消费的权利:
学生 A 登上校园卡管理系统查看自己校园卡状态,给自己校园卡激活,设置了新密码,充值 C 元到校园卡余额,然后去饭堂超市消费了 E 元,接着再登上校园卡管理系统查看了校园卡的相关信息(注意设置学生每次充值限额 500 元,消费限额 500 元,超过限额会提醒学生用户)。

场景三:

学生用户具有查看校园卡挂失、重新补办的权利:
学生 A 的校园卡丢失,登上校园卡管理系统申请挂失,在此期间学生 A尝试充值 C 元到校园卡余额,尝试消费了 E 元,但是校园卡管理系统提醒用户已挂失无法进行充值和消费。两天后,学生 A 重新办理新卡,系统解除校园卡挂失状态,成功充值 C 元到校园卡余额,成功消费了 E 元,然后在系统查看了校园卡的相关信息。

项目基本要求

站在学生用户的角度,设计对中山大学校园卡进行管理的校园卡管理系统,包括创建帐号,户名,学生所在学院,余额,充值,消费等操作(帐号不重复、学号具有唯一性)。

系统功能至少包括以下核心基本功能:
a) 查看卡主与校园卡相关信息。
b) 充值:校园卡余额充值。充值一定金额到校园卡中,设置学生每次充值限额 500 元,超过充值限额,提醒学生用户充值过大。
c) 消费:饭堂超市等消费,从卡中扣除相应金额,设置学生每次消费限额500 元,超过消费限额,提醒学生用户消费已超过限额。
d) 查余:查询本用户的校园卡的余额。
e) 查询个人消费:查询本用户的消费记录。
f) 充值记录:查询本用户的充值记录。

实验概述

这次实验要求实现的是校园卡管理系统。经过三个星期的不懈努力,我们团队按时完成了170k左右的代码量,有质量地完成了改项目。

本次实验主要分成两大部分——前端和后端。后端也就是我负责设计和实现数据结构以存储和查找系统相关数据并实现其衍生功能,通过Treap加树套链表的设计,将管理系统中最常用的查找某一张卡的相关数据操作的复杂度从O(n)降到了O(logn),使系统的运行更加有效率。而前端的我的舍友则负责对程序产生的命令窗口进行交互设计和外观美化,通过实现鼠标交互和键盘交换,极大地还原了现实生活中的管理系统,也给使用者带来了不小的便利。

在功能上,我们实现了项目要求的四个模块——增删查找学生,激活校园卡及设置修改密码,增加及查询消费充值记录,校园卡的冻结和挂失。另外我们还将这些功能分给了三个不同的角色——管理员,学生和机器,还原现实中的实际系统情况。另外,我们还额外实现了图书馆管理系统,即可以统计在图书馆内的学生以方便闭馆时的清场操作;同时我们还实现了存档功能,在使用系统结束后,我们可以将内存中的数据导出到磁盘中,在下次登录系统时导入数据,继续之前的操作,实现了可持续化的系统管理,更加符合现实需要。

下面我们就将从实验目的,实验内容,实验结果和总结回顾四个方面详细记录我们的实验过程。

实验目的

通过设计数据结构存储学生校园卡信息,并设计交互逻辑和界面以能在系统中完成下列三个不同角色的对应操作:

学生:激活校园卡,设置密码,更改密码,查询自己的消费充值记录;
管理员:导入学生信息,冻结与解冻校园卡,补办校园卡,统计图书馆内同学;
机器:产生消费充值记录,产生学生进入或离开图书馆记录。

另外为了符合现实操作逻辑,还需设计存档机制,在退出系统前能选择保存当前校园卡管理系统内的所有数据,并在下次进入系统时可以自动导入保存的数据,以维持系统长时间的工作,也提供了系统关机维护的条件。

最后通过设计学生校园卡信息管理系统,了解和运用类及类间关系,进而体会面向对象编程的内涵;并初步接触一些初等的数据结构来加速运行速度,为日后更大型的信息管理系统的构建打下坚实基础。

实验内容

项目分工

在本次实验中,所需完成的学生信息管理系统项目主要项目主要分为两大部分,其一是设计学生信息管理系统并支持项目目的所提到的所有功能;其二是美化命令窗口,并设计以鼠标和键盘为主的交互逻辑,使得师生能够更加方便快捷地使用我们的系统。

我们团队也是认真讨论了分工。结合我们两个人所擅长的方面,最终决定由我负责整个项目的信息管理系统的设计实现,即“后端”部分。我的舍友则负责整个项目的交互和后期对接,即“前端”部分。

下面我们就将这两个部分分离开来,具体讲述我们学生信息管理系统的构建过程。

后端部分

数据组织与框架

由于学生的数量不是固定的,所以在数据结构的大体框架上,我们选定了链表这种灵活的组织方式。但是在系统中我们最常进行的操作就是查询给定学号/卡号对应的校园卡信息,而普通链表的链状结构会让单词查找的复杂度达到O(n),在学生较多时就会产生查找的卡顿,影响操作速度:

proj1.png

所以我们最后采用了树状结构进行信息存储。我们用链表完成了一个可以增删查找结点的二叉查找树。考虑到学生学号的唯一性,所以我们可以很轻松地定义出二叉查找树上的规则——左儿子学号小于父亲节点,右儿子学号大于父亲节点。得益于二叉树的优异特点,单次查找的复杂度均摊是O(logn)级别的,这样虽然增加了编码的难度,但大大减少了查询的时间:

proj2.png

下面我来进行演示,开始时系统无任何学生信息,我们按顺序插入 19334022 19300001 19320057 19358023 19342333 19380312 19100002:

GIF1.gif

但是简单的二叉查找树还是会存在问题,如果我们学号的添加是单调的,就会导致我们的树退化成线状,以丧失树的优良性质,比如如果我们插入学号为19330001 19330002 19330004 19330005:

GIF2.gif

所以我们就要考虑使用一些更加高级的树形数据结构,比如很经典的Treap。Treap=Binary Search Tree+Heap(二叉搜索树+堆) Treap之所以不同于一般的二叉查找树,是因为它除了有满足二叉查找树性质的存储点值,还有一个满足堆性质的随机的附加值(key)。也就是说Treap是一个随机附加域满足堆的性质的二叉搜索树,其结构相当于以随机数据插入的二叉搜索树。其基本操作的期望时间复杂度为O(logn)。相对于其他的平衡二叉搜索树,Treap的特点是实现简单,且能基本实现随机平衡的结构。Treap的思想就是,在每一次插入的时候,都会给每个点一个随机的key值,然后让整棵树在满足原本存储的值(val)成一棵二叉查找树外,而外的key值能成为一个堆(我们选用的是小根堆),比如上面那个例子,如果加入了随机的key值,就可能变成下面这样:

proj4.png

从而保持树的优良性质。这样我们就完成了对学生卡的存储规划。具体操作就是在插入新的节点时先按二叉查找树的性质插入到叶子节点。接着不断比对它和它父亲节点的key值,如果它的key值比较小,就根据它和其父亲节点的相对位置,完成左旋或右旋操作,让它成为它父亲交换位置而不破坏二叉查找树的性质:

proj5.png

proj6.png

我们还是用回我们之前的例子比如如果我们插入学号为
19330001 19330002 19330004 19330005;假如这几个点的随机为3 5 1 2:

GIF3.gif

除了这棵Treap之外,还有一个很重要的数据链就是学生的消费和充值记录的数据块所组成的链条。因为学生充值和消费的次数也是不确定的,所以我们采用的方法是在每个校园卡块设置两个指针consume_head和recharge_head,分别指向消费和充值的对应链表,所以总的数据结构大致如下图所示:

proj7.png

这就是数据的大致组织形式了。

类的设计及关系

在类的设计上,我们设置了四个类(Class.hpp),分别是存储学生信息的Student类,存储校园卡信息的Card类,存储资金交易信息块的Block类和存储Treap树上节点信息的Node类。

比较有意思的一点是,这次的成员权限设计我们没有按方法为一般的信息存储(比如姓名学号之类的)为private,而指针的信息存储,比如Node和Block都为public。这样设计的原因是对一般数据我们希望它相对稳定,不希望外界直接访问,所以我们设定只能通过我们给定的类函数进行访问;而指针由于需要频繁操作,且查找指定节点和指定消费记录也需要用到指针,再使用函数就会给使用者带来不小的麻烦,所以我们使用public权限,这样也让代码的简洁性有所提升。当然这个有所违背封装性,在以后的工程中我会避免这一点。

其具体类间关系图如下:
proj8.png

可以看到,我们最大的一个类就是树上节点Node类,它包含了一个Card类元素、两个指向Block类的指针、三个指向Node类的指针(指向父亲,左儿子和右儿子)和一些相关函数。而Card类是存储校园卡信息的类,里面包含一个Student类元素、余额信息、激活信息、卡主学号、卡号、该卡的key值(Treap中使用),在图书馆内外信息,冻结信息和密码。 包含的函数都是与之相关的查询、修改及展示函数。Student类则很简单地包含了学生的姓名,学院,专业,班级和学号,包含的函数就是这些信息的查找和修改函数。最后来看看Block类,Block类就是标准的链表,有着指向下一个Block的Block*类型指针,存储的信息就是这次交易的金额、地点和时间。

所以我们类的定义大致如下:

class Student {//学生类 
    private:
        string name;//姓名 
        string college;//学院
        string major;//专业 
        string class_num;//班级 
        int id;//学生证号码  
    public:
        Student();
        Student(string, string, string, string, int);
        Student(const Student&);
        //构造函数 
        int get_id() const; 
        string get_class_num() const;
        string get_name() const;
        string get_college() const;
        string get_major() const;
        //信息获得函数 
        void change_id(int);
        void change_class_num(string);
        void change_name(string);
        void change_college(string);
        void change_major(string);
        //信息修改函数 
};

class Block {//资金块,代表一次交易 
    private:
        double money;//交易金额 
        string site;//交易场所 
        int date;//交易日期 

    public:
        Block* next;//用链表组织Block 
        Block(double, string, int);
        Block(const Block&);
        //构造函数 
        double get_money() const;
        string get_site() const;
        int get_date() const;
        //获得信息函数 
        void show() const;
        //展示函数 
};

class Card {//校园卡类 
    private:
        Student student;//包含一个学生 
        double money;//校园卡余额 
        bool lock;//是否被冻结 
        int id;//学生证号码 
        long long num; //卡号 
        int value;//在Treap上随机的Key值 
        bool indoor;//在图书馆的内与外 
        bool activation;//是否被冻结 
        string password; //该卡密码 

    public:
        Card(string, string, string, string, int); 
        Card(string, string, string, string, int, double, bool, long long, bool, bool, string); 
        Card(const Student&);
        //构造函数 
        int get_id() const;
        int get_value() const;
        double get_money() const;
        bool get_lock() const;
        bool get_activation() const;
        string get_password() const;
        long long get_num() const;
        Student& get_student();
        bool get_indoor() const;
        //信息获得函数 
        void change_money(double);
        void change_lock(); 
        void change_value(int);
        void change_activation();
        void change_password(string);
        void change_num(long long);
        void change_indoor();
        void show();
        //信息修改函数
};

struct Node {//Treap上的节点 
    private:
        int total_consume;//总共的消费记录数 
        int total_recharge;//总共的充值记录数 

    public:
        Card card;//包含一个校园卡 

        Node *lson;//Treap上的左儿子 
        Node *rson;//Treap上的右儿子 
        Node *father;//Treap上的父亲 

        Block* recharge_head;//充值记录的链表 
        Block* consume_head; //消费记录的链表 

        Node(const Student& new_student);
        Node(string, string, string, string, int, double, bool, long long, bool, bool, string);
        Node(); 
        //构造函数 
        int get_total_consume() const;
        int get_total_recharge() const;
        //信息获得函数 
        void change_total_consume();
        void change_total_recharge();
        //信息修改函数
};

各部分实现原理

增删查找学生

在增删查找学生整个模块里,我们设置了三个功能:查找学生,手动添加学生和文件导入学生。在整个系统的最开始,为了方便后续 Treap 的 rotate 操作,我参考链表的头插法,创建了一个空的根节点 root,其 id 和 key 都设置为 0,这样由于二叉查找树的性质,由所有学生的学号都大于 0 知所有的后续新加入的学生所属的 Node 节点都在 root 的右子树。且因为它的 key 是 0,所以由于小根堆的性质,其他节点的 key 都大于 0,所以它一定能一直待在根节点的位置。

所以在查找函数中,对应查找给定学号的学生是否存在时,我就直接从 root节点开始进行递归,如果我待查找的学号刚好等于当前节点的学号,则很幸运直接找到了直接返回。 而当待查找的学号大于当前节点的学号,则递归查询当前节点的右子树;反之递归查询左子树。这样下去就有可能出现两种情况——找到了,直接返回指向待查询节点的指针;找不到,则返回 NULL, 这样我们就能够通过函数是否返回了 NULL 来判断现在已添加的学生中有没有查询的学号存在:

proj9.png

查找学生整个功能就实现了,我们接着来看手动添加学生。添加学生的过程主要分为两部分,第一部分是读入一个学生的相关信息,这里要进行正确性的判断。第二部分就是把带有学生信息的 Node 节点插入到树上。插入的过程也不复杂,就是先按二叉查找树的性质插入到叶子节点,接着不断比对它和它父亲节点的 key 值,如果它的 key 值比较小,根据它和其父亲节点的相对位置运用上面提到过的左旋或右旋操作让它成为它父亲交换位置而不破坏二叉查找树的性质,从而维护小根堆的性质。

最后来看看从文件导入学生,我们要做的就是从如下图所示的文件中输入学生信息,然后一次性导入进系统。实现原理就是一行一行读入判断是否合法,合法则调用刚刚讲到的插入学生的功能,并现实新增成功;不合法则直接现实格式不合法。我们的数据格式是姓名,学院,专业,班级和学号以任意空格分隔开,回车结尾。我们看到第一第四行都是合法的,而第二第三行则因为元素数量不匹配从而是非法的,最终我们下面这个文件就能成功导入两名同学:

具体代码过于冗长我会在最后附上附件供大家交流学习。

激活设置修改密码

在激活设置修改密码这个模块, 我们也实现了三个主要功能:激活校园卡,设置密码和修改密码。

首先是激活校园卡,这是个比较简单的功能,我们要做的就是在树上找到这个校园卡后,去看看它的激活信息,如果已经激活过了就返回激活失败;如果未激活过就将激活信息设置为已激活并返回激活成功的信息。

在设置校园卡密码这个功能上,我们初始时校园卡密码是空字符串。我们在树上找到这张校园卡后就判断这张卡现有的密码是不是空串, 如果其密码是空串就允许它设置不为空的密码。 反之返回已设置过密码的信息。

而在修改密码这个功能,我们也在树上找到这张校园卡后就判断这张卡现有的密码是不是空串,如果其密码不是空串就允许它设置不为新的密码。反之返回请先设置密码的信息。

值得注意的是在密码设置/修改有关操作中,我们的密码都是要输入两次的,只有两次密码一样才能成功设置/修改密码,也更加贴合现实情况。

增加和查询消费充值

由于消费和充值的记录是类似的,只是归属于不同的链表,所以下面的操作我以消费为例进行讲解。在增加消费中,我们在现实生活中是滴卡,所以读取的是卡号而不是学号,我们先从读取的卡号中得到学号,然后在树上找到这张校园卡,如果这张校园卡的卡号和我们读取的卡号一致,我们就去判断卡有没有冻结,余额是否足够,消费金额有没有大于 500……如果这些都合法我们就修改余额减去消费金额,然后在消费记录的链表中加入这次消费。

在查询消费记录中,我们提供了按时间查询和按地点查询两种模式,按时间查询时用户需要提供开始和结束时间,按地点查询用户需要提供地点名称。其实查找的逻辑都是类似的,我们都是循环遍历链表中的所有元素,找到所有符合条件的并输出:

proj10.png

冻结和补办

在冻结和补办模块中,我们设置了三个功能——冻结校园卡,解冻校园卡和补办校园卡。

在冻结校园卡中,我们需要用户给我们需要冻结的校园卡的学号, 我们首先还是在树上找到这张校园卡,接着去看看这张卡是否已经冻结。如果这张卡已经冻结,则冻结失败;反之我们记录该卡冻结状态为已经冻结,并返回成功冻结的信息。

在解冻校园卡中,类似的,我们还是在树上找到这张校园卡,接着去看看这张卡是否已经冻结。如果这张卡没有冻结,则冻结失败;反之我们记录该卡冻结状态为未冻结,并返回成功解冻的信息。

最后是补办校园卡。我们约定,补办校园卡须在原卡已冻结的基础上。所以在树上找到指定学号的校园卡并确认满足条件后,我们将为该学号同学申请新卡,故将该节点的卡号更改为新卡号。这样在以后消费的时候,若使用旧卡消费,就会发现这张卡的主人现有卡卡号与消费卡号不符就会返回交易失败信息,相当于彻底删除了之前那张卡。

图书馆相关操作

在图书馆相关操作中,我们也设置了两大基本功能:进入/离开图书馆和查询图书馆内现有同学。

在进入/离开图书馆时,我们日常生活都是滴卡,所以我们要求用户提供卡号。进入和离开操作类似,下面就以进入为例进行讲解。我们首先根据卡号推算出学号后在树上找到这张校园卡,如果该校园卡的现卡号与当前操作卡号不符,则有可能时他人捡到了之前挂失的校园卡,这时返回错误信息。而且我们知道一个人不能连续重复进入图书馆,所以我们也要查询卡主的状态,如果已在图书馆内,也返回错误信息。如果以上都合法,则修改卡主状态为图书馆内并返回正确信息。

在查询图书馆内现有同学的操作中,我们从 root 开始,运用后序遍历递归判断每个同学的状态并输出那些在图书馆内的同学并计数就完成了。设计这个功能的目的是为了方便图书馆清场时确认有没有同学还没有离开图书馆或通知还在图书馆内的同学离开图书馆。

系统的导入导出

在现实生活中,我们的系统不可能只运行一天,所以在关机的时候,我们要能够把系统中的数据导出,再在下次进入系统时导入。所以我们就设计了这样一个导入/导出功能。

先来看看导出吧,导出其实也不麻烦。我们只需要递归访问所有非 root 外的树上节点,将它们的校园卡信息和所有的消费记录及充值记录全部输出到一个文件中就可以了,我们这里设定的是 save.txt。其中的导出格式大致为第一行为校园卡信息,第二行为一个数 n,代表这个人总的消费记录数,接下来 n 行每行都是一条记录的所有要素;第 n+3 行为一个数 m,代表这个人的总的充值数,接下来 m 行,每行都是一条充值的所有要素。

而导入则恰恰是导出的反过程,我们只需按规则读入,在树上一步一步构建出 Treap 和每个节点的两条交易链表就好了。

最后还有一点值得一提。由于我们的树上节点和每个节点的两个链表都是动态开辟内存空间,所有在退出系统时,一定要释放所有内存空间。

前端部分

前言

通过后端的组织,各种数据都处理得很妥当,但程序并不是只设计给自己用的。优秀的程序一定要让用户在不了解程序的具体实现的情况下也能轻松地使用程序,前端就作为一个中间人在后端与用户之间架起了一座桥梁。

前端开发,要注意的有两点:一是注意程序的具体功能和主要的使用人群,以对特定人群设计良好的交互场景;二是注意与后端代码的结合,即好看的皮囊和有趣的灵魂相结合。在与后端的结合过程中,特别需要注意的是前后端代码的分离,即双方应在信息隐蔽的情况下只通过调用各自的函数即可实现对应的功能,这样做可以在后期处理时不需要做太大的改动就可对程序进行维护升级。

主要结构与框架

首先明白我们所设计的是校园卡管理系统,面向的用户是广大师生,于是结合现实情况,可大致明确用户可以有以下的操作:

(1)刷卡充值/消费
(2)刷卡进入/离开图书馆
(3)设置/修改密码
(4)查询充值记录/消费记录
(5)冻结(挂失)/解冻/补办校园卡
(6)录入一个新同学的资料/查找一个现有同学的资料

由此观之,作细分的话至少有十几种功能给用户操作,显然将各种功能作分类之后再包装是更加明智的做法。结合现实情况,(1)和(2)是师生平时在机器上刷卡就能直接实现的功能,(3)和(4)是学生登录后才可操作的功能,(5)和(6)显然不能由学生来操作,需由学生向管理员申请(找阿姨)才可进行该操作。那么思路就很清晰了。

proj11.png

上图就是总体的结构,剩下的各种具体功能只需接上后端的函数即可。而我们并不满足普通的“黑框框”控制台窗口,我们决定模仿学校的校园卡管理系统,实现真正的有鼠标交互、键盘交互的系统。对于现实的可操作系统,当用户进行点击时,会有界面的跳转。于是程序前端部分整体框架如下:

proj12.png

框架呈树状,在每一个界面安排相应的操作,各界面之间可以互相跳转。

类的设计

在界面的设计中,除了固定显示的内容外,还有可供用户点击的按钮和供用户编辑的文本框,称之为控件。所以,前端部分只需围绕控件的功能来设计类即可。

对于一个控件类,必定有的成员是:

x 坐标, y 坐标,
宽度和高度,
控件的内容

必定有的成员函数是:

各成员的 set、 get 函数
显示函数 print()
响应点击的函数

理论上,先设计一个控件基类,再设计按钮派生类和文本框派生类可以使整体结构严谨,但本项目中设计的控件类型很少,使用继承与否,区别不大,所以在本次实验中两个控件的类实现并无关系。

上图为两个控件的基本实现,相似点在于二者都可被点击,当按钮被点击时。程序产生一定的响应,当文本框被点击时,文本框进入可被编辑的状态,所以,每产生一次鼠标点击,我们就要将鼠标按下与释放的位置依次传给每一个当前界面的每一个控件,通过控件的成员函数判断这个鼠标点击的位置是否是控件自身所在的位置,即可响应用户的操作。

对于文本框,因为其拥有编辑功能,所以文本框类会比正常的空间多一个string 成员 content,用于储存文本框的内容。考虑到用户可以随时通过鼠标使文本框进入编辑状态和退出编辑状态,显然接收鼠标点击之后来一个 cin 并不可取,所以有针对此成员的成员函数 increase 和 decrease ,一个函数接收传入的字符,添加到文本框内容中,另一个函数则是删减内容的最后一个字符。

有一种特殊的文本框,叫密码框,这种文本框会以******类型的字符串代替文本框内的内容,考虑到校园卡系统对这种文本框的需求, Textbox 类中添加了一个 bool 类型成员 isPassword,调用 print()时会根据情况来输出文本内容。

通过这几个函数,可以完整的实现两种控件应有的功能,也提供了良好的函数接口,调用成员函数时的目的明确,分离程度较高,不需要知道特定的函数内部实现也可以轻松地调用。

各部分实现原理

由于程序前端部分要实现各场景之间的切换,最简单的想法就是每进行一次切换,就将相应场景的所有内容输出一次,而控制台窗口的正常输出模式的光标会由上到下不断的换行,因此如果想要模拟现实中的校园卡系统,只在一个窗口内不断切换内容的话,就必须有清屏函数和光标定位函数:

void cls();                   //清屏函数
void go_to_x_y(int x, int y); //将光标移到(x,y)处

前者负责将上一个场景的内容清除干净,后者负责将光标移动到相应的位置以在控制台同一缓冲区内输出下一个场景的内容。

此时基本的场景切换思路就完成了:

void 场景1() {
    控件变量的声明
    cls(); //清除上一个场景
    显示当前场景的各种内容和变量
    接收一个鼠标点击
    if(点到按钮1) {
        进入到下一个场景/其他相应
    }
}

剩下的工作就是解决各种各样的问题,所以,后续的内容主要以解决问题的思路进行。

问题 1:现在能从一个场景跳到下一个场景,那怎么从下一个场景回来呢?
显然,此时的各场景缺乏时续性,不能一直使用,当进入下一个场景再返回时,会产生多个函数连续结束的结果,因此,我们利用一个 while 死循环来使各场景都能一直不停地工作刷新:

void 场景1() {
    控件变量的声明
    while(1) {
        cls(); //清除上一个场景
        显示当前场景的各种内容和变量
        接收一个鼠标点击
        if(点到按钮1) {
            进入到下一个场景/其他相应
        }
        else if(点到返回键) {
            return;
        }
    }   
}

此时,所有不含有文本框的场景函数都可以按照这种模板进行编写,但这时会产生一个视觉上的问题。因为当用户进行了一次鼠标点击时,不一定会点在控件的位置上,则不会发生任何的相应,用户每进行一次鼠标点击, while 循环内的清屏函数和显示语句都会执行一遍,这样会使窗口内的内容不断地进行无意义的闪烁,影响用户体验。

问题 2:如何解决闪烁问题?
只需要一个 while 死循环和几个 break,就可以防止用户乱按而使窗口不断闪烁。

void 场景1() {
    控件变量的声明
    while(1) {
        cls(); //清除上一个场景
        显示当前场景的各种内容和变量
        while(1) {
            接收一个鼠标点击
            if(点到按钮1) {
                进入到下一个场景/其他相应
                break;//记得break
            }
            else if(点到返回键) {
                return;
            }
        }//防止闪烁

    }   
}

这样,当且仅当用户点到了某个按钮上时,才会在执行相应的操作后刷新窗口。

问题 3:有文本框的场景如何实现?
有文本框的场景和普通场景之间的区别就是,前者会对键盘输入作出相应。而实现文本框场景的难点在于,当且仅当一个文本框处于编辑状态时,才会对键盘输入作出相应。其中,重点在于如何判断一个文本框是否处于编辑状态,以及,当一个页面有多个文本框时接收键盘输入,如何识别当前编辑的是哪个文本框。我们创建了一个文本框对象的指针,用于指向被选中的文本框,即开启它的编辑状态。至于键盘输入,其本质上和鼠标点击一样,都是来自于设备的信息,所以可以一起处理。

void 场景1() {
    控件变量的声明
    声明一个指向文本框的指针 ptr//注意!
    while(1) {
        cls(); //清除上一个场景
        显示当前场景的各种内容和变量
        while(1) {
            接收一个鼠标点击/键盘按键
            if(是鼠标信息) {
                if(ptr有指向一个文本框但鼠标没点到这个文本框上) {
                    ptr = NULL; //让这个文本框回到不可编辑状态
                }
                if(点到按钮1) {
                    进入到下一个场景/其他相应
                    break;
                }
                else if(点到文本框1) {
                    ptr = &文本框1; //开启该文本框的编辑状态
                }
                else if(点到返回键) {
                    return;
                }
            }
            else if(是键盘信息) {
                处理ptr所指向的文本框
            }
        }

    }   
}

此时,界面函数的模板基本设计完毕,既可以在不同界面直接来回切换,又不会不断闪烁,也可以编辑文本框,剩下的步骤就是与后端接合,实现相应的功能。

由于不同的界面有不同的空间和功能实现,相对复杂,所以无法使用函数模板,每一个界面都要具体实现,所以,前端的主要任务就是参照上图,写出各种场景的场景函数:

void scene_administrator_choice();               //管理员操作界面
void scene_administrator_import();               //管理员导入学生名单,界面
void scene_administrator_import_byhand();        //管理员手动导入学生
void scene_administrator_import_byfile();        //管理员通过文件导入学生名单;
void scene_administrator_search();               //管理员查找学生界面
void scene_administrator_card();                 //管理员执行校园卡相关操作
void scene_administrator_freeze_card();          //管理员冻结校园卡
void scene_administrator_unfreeze_card();        //管理员解冻校园卡
void scene_administrator_reissue_card();         //管理员给学生补办校园卡
void scene_administrator_count_library_member(); //管理员统计图书馆内人数

void scene_student_login();                                  //学生登录界面
void scene_student_choice(Student student);                  //学生操作界面
void scene_student_consume_record(Student student);          //学生查询消费记录
void scene_student_consume_record_byDate(Student student);   //学生按时间查询消费记录
void scene_student_consume_record_byPlace(Student student);  //学生按地点查询消费记录
void scene_student_recharge_record(Student student);         //学生查询充值记录
void scene_student_recharge_record_byDate(Student student);  //学生按时间查询充值记录
void scene_student_recharge_record_byPlace(Student student); //学生按地点查询充值记录
void scene_student_set_password(Student student);            //学生设置密码
void scene_student_password(Student student);                //学生密码操作界面
void scene_student_change_password(Student student);         //学生改密码界面
void scene_student_active_card(Student student);             //学生激活校园卡

void scene_machine_choice();        //机器操作界面
void scene_student_recharge();      //学生充值界面
void scene_student_consume();       //学生消费界面
void scene_student_enter_library(); //学生进入图书馆
void scene_student_leave_library(); //学生离开图书馆

在此次前端开发中,设计键盘交互、鼠标交互、控制台窗口各种表现和模式的切换等等,这些功能的实现依赖于 win32API 函数(在 Window.h 头文件中),具体功能的实现不在此赘述, win32-API.hpp 和 win32-API.cpp 文件中有详细的注释,可自行查看。

实验结果

通过实验我们完成了预定工作,下为项目展示。开始时,系统会询问我们是否导入已有存档:

任意选择后,我们进入主界面:

我们进入管理员界面:

学生登录界面:

学生操作界面:

机器操作界面:

里面的功能都可以使用,但因为窗口太多这里就不一一展示了,如果想了解详细操作可以下载文末的工程代码进行参考学习。

总结回顾

这是我们第一次组队写工程,也我第一次写这么大的工程。 确实是一次难忘的体验。先来讲讲这次的收获和进步吧。首先我们的码力得到了提升,通过三个星期的代码竞赛,我们对以往学习的数据结构和链表知识有了更加深入的理解;其次我们也对类的设计更有感觉了,这是第一次设计这么多层嵌套的类,对于类的封装性也是深有体会。最后,这次工程提升了我们的代码规范和合作能力,在之前的代码中,代码都是为自己设计的,所以接口名称就很随意,但是这次多人合作为了让他人看懂我的接口,就必须设计简单易懂的接口来使用。

再谈谈这次项目的不足吧。这次我们采用前后端分工,但是我们没有采用齐头并进的开发方法,而是后端完工后前端再结合后端功能进行美化交互开发, 是“串联”的开发模式, 究其原因还是前期的交流有所欠缺,这也造成了一段时间某同学的空闲期。希望在下次的工程总能够解决这个问题,更加有效地运用时间。

最后希望你喜欢这个的工程!喜欢这篇BLOG!下附工程:

PROJECT1.rar

Last modification:September 5th, 2021 at 09:15 pm
If you think my article is useful to you, please feel free to appreciate