基于C语言与EGE图形库的模拟地铁自动售票系统

前言

众所周知,期末考试周结束后,迎来的不是荒淫放荡的暑假生活,而是依旧忙碌的小学期。
小学期的第一周,全是高级语言设计课。说是上课,其实除了第一天讲了一天的软件工程之外,剩下四天全都是搬着笔记本待在教室里敲代码。为了提供笔记本的电源,助教还得向物业借许多排插。不是很明白为什么非得在教室敲,只要作业完成了在哪里其实都一样吧。
题目有两个,我的题目是模拟地铁自动售票系统,另一个是职工档案管理系统。第二个有点像是大一上C语言实验课做的学生成绩管理系统,不过要比那个多很多功能。两个题目都需要做用户交互界面。
我选到第一个还是比较幸运的,因为第一个核心代码并不是特别的多,主要程序还是在界面设计上,至少对我来说是这样的。而第二个题目之前也做过类似的,感觉并不能学到什么比较新的东西。
这一周感觉大部分时间都在各种乱七八糟的事情上折腾,像是安装图形库,配置sublime,设计交互界面之类的,作业一拖再拖,直到第一周周日才做好。第二周又马上要开始认知实习课程,前四天一天到晚就要往外跑,参观各种企业。加上社团的项目,robomaster的选拔培训,之类的,实验报告到周四才完成。到周五认知实习答辩结束,原以为可以休息一阵子,谁想突然又说这门课也要答辩,报告也需要修改。周末写了几篇博文,再把PPT做好,就过去了。报告是懒得改了……原先是这么想的,结果呢,终究还是逃不过去,答辩结束后还是要修改。好在这次比起之前要求修改多了一些要求,好下手。
说真的,每每想到这门课只有一个学分,而且还不是考察课,我总有一种随便做做就算了的想法。这一而再再而三的要求也着实令人厌烦,只想快点结束。若不是真的学到了一些东西,我必定不会写这篇博文。时到如今,多说也无益了。

任务概述

设计一个具有购买车票、地图查询、系统说明、退出等功能的模拟地铁自动售票的系统。
系统要求具有欢迎界面,界面显示作者信息和版权信息。进入系统主菜单后提供购票选项、地图查询选项、系统说明、退出系统四个选项。
系统说明界面详细的介绍了购票流程,并且附有用户须知。
购票系统实现通过键盘输入起止站和票价,并且以此计算阶梯票价。根据系统提示进入投币找币流程,购票成功后返回欢迎界面。
查询系统通过输入站台编号查询站台信息。查询成功后可选择继续查询、购票或是返回。

运行环境

操作系统:Windows7
开发环境:codeblocks+EGE图形库

系统设计

总体架构设计

整个系统分为地图查询系统和购买车票系统。
由于代码比较多,我将其分写到mian.cpp、map.cpp、data.cpp、sold.cpp、Welcome.cpp、Index.cpp、description.cpp共七个cpp文件中。然后新建了一个名为”kxj.h”的头文件,在里面声明多个文件共用的函数,结构体以及外部变量。
其中每个文件的作用如下:
Welcome.cpp:绘制欢迎界面。
Index.cpp:绘制系统主菜单界面。
description.cpp:绘制系统说明界面。
data.cpp:地铁信息初始化。
Map.cpp:绘制地图查询界面,以及实现查询地图的功能。
Sold.cpp:绘制购买车票界面,以及实现输入起止站和车票数与投币功能。
Main.cpp:调用各个功能以及界面,并提供主界面的按钮点击功能。

模块分析与设计

头文件

  1. 公用函数分析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void DrawOpt(int x,int y,int w,int h,int fnum,int fh,char optTitle[]);//绘制按钮键。
    void Title();//标题
    void Copyright();//版权信息,每一页的标配
    bool checkClick(int *x,int *y);//检测是否有点击,并且获取点击的位置
    void Init();//初始化页面,使之清空,并且有标题与版权信息
    void textBox(int x,int y,int w,int h,int r);//绘制输入框
    int Input(int x,int y,int n,int fh,int (*click)(int x,int y));//在某输入框内输入。每次循环同时检测鼠标消息与键盘消息,确保键盘与鼠标都能够实现所需效果。
    void Delete(int x,int y,int w,int h);//使某一矩形变成背景色,实现删除效果
    bool checkIn(int mx,int my,int x,int y,int w,int h);//检测点击位置是否在某处
  2. 结构体STA是站台信息的存储类型,数组下标就是编号

    1
    2
    3
    4
    5
    6
    struct STA{
    string name;//站点名称
    int line;//站点路线
    string pre;//上一站名称
    string nxt;//下一站名称
    };

地铁信息初始化

通过循环将站台信息整合到结构体STA数组中,数组下标即为编号。换乘点拥有多个编号,不同线路上编号不同。
将八条线路图建成无向连通图。其中每一个编号视为一个点。一条线路上相邻两站之间连通,距离直接打表写入文件。换乘点不同编号之间距离为零。不连通的点之间距离为oo(1000000007)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Query_same(int x){
for(int i=1;i<=number;i++){
if(station[i].name==station[x].name)mp[x][i]=0;
}
}
void data(){
for(int i=0;i<=7;i++){
for(int j=0;j<sta_num[i];j++){
number++;
station[number]=(STA){sta_name[i][j],i+1};
station[number].pre=j?sta_name[i][0]+"方向:"+sta_name[i][j-1]:"此站为终点站";
station[number].nxt=j<sta_num[i]-1?sta_name[i][sta_num[i]-1]+"方向:"+sta_name[i][j+1]:"此站为终点站";
if(j)mp[number][number-1]=dismp[i][j-1];
if(j<sta_num[i]-1)mp[number][number+1]=dismp[i][j];
}
}
for(int i=1;i<=number;i++){
for(int j=1;j<=number;j++){
if(!mp[i][j])mp[i][j]=oo;
}
Query_same(i);
}
}

欢迎界面,系统主菜单以及系统说明界面

通过EGE图形库的库函数设计。
官方文档

地图查询系统

通过EGE图形库中的库函数完成交互界面。
查询站台时,先检测所查询编号是否合理,然后通过暴力查询,遍历每个站台,寻找所有站台名称相等的元素,加入到Line数组中,最后输出。
查询的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void Query(int num){
for(int i=1;num<=number&&i<=number;i++){
if(station[num].name==station[i].name){
Line[cnt++]=station[i];
}
}
InitMap();
setfont(20,0,"微软雅黑");
if(!cnt)outtextxy(WIDTH-200,30,"对不起,您查询的站点并不存在!");
else {
char * name=(char*)("站名:"+Line[0].name).c_str();
outtextrect(WIDTH-200,30,100,20,name);
outtextxy(WIDTH-200,60,"所在线路:");
while(cnt--){
xyprintf(WIDTH-200,90+cnt*120,"%d号线",Line[cnt].line);
xyprintf(WIDTH-200,120+cnt*120,"下一站:");
name=(char*)Line[cnt].pre.data();
xyprintf(WIDTH-200,150+cnt*120,name);
name=(char*)Line[cnt].nxt.data();
xyprintf(WIDTH-200,180+cnt*120,name);
}
delay_fps(60);
cnt=0;
}
}

购买车票系统

通过EGE库函数完成交互界面,实现输入功能。
计算阶梯票价时,先用无优化的最短路径dijstra算法计算出两者间的最短路径,然后将路径向上取整,根据规则计算出单价。
计算票价的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void calcLine(){//dijkstra计算最短路
for(int i=1;i<=number;i++)dis[i]=oo,mark[i]=0;//初始化
dis[startStation]=0;
for(int i=1;i<=number;i++){
int mi=oo,k=-1;
for(int j=1;j<=number;j++){
if(!mark[j]&&dis[j]<mi){
mi=dis[j];
k=j;
}
}
if(k==-1)return ;
mark[k]=1;
for(int j=1;j<=number;j++){
if(!mark[j]&&dis[j]>dis[k]+mp[k][j]){
dis[j]=dis[k]+mp[k][j];
}
}
}
price=calc(ceil(dis[endStation]));//calc是计算票价
return ;
}

系统主菜单

初始化窗口。
用EGE的库函数完成交互功能。点击“地图查询”“购买车票”“系统说明”直接调用相关函数。点击“退出系统”时则退出循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main(){
initgraph(WIDTH,HEIGHT);//初始化窗口
data();//站点信息初始化
//欢迎界面,任意键继续
Welcome();
int num=0;
for ( ; is_run(); delay_fps(60))
//while(1)
{

//系统说明界面,任意键继续
//description();
//显示主界面
Index();
delay_fps(60);
int x,y;
xyprintf(0,0,"%d",num++);
while(!checkClick(&x,&y));//getch();
if(checkIn(x,y,(WIDTH-160)/2, 100, 160, 50))sold();//购买车票
else if(checkIn(x,y,(WIDTH-160)/2, 180, 160, 50))Map();//查询地图
else if(checkIn(x,y,(WIDTH-160)/2, 260, 160, 50))description();//说明介绍
else if(checkIn(x,y,(WIDTH-160)/2, 340, 160, 50))break;//退出系统

}
closegraph();
return 0;
}

软件结构(流程图)

调试过程

我首先编写完系统的核心代码,如地图查询与购买车票。这一块没有遇到太大的问题。改正几处简单的错误之后,便通过了测试。
接下来我开始绘制图形界面。按钮、输入框、文字等只需要稍微计算一下便能够符合我的预期。
较为困难的是用户交互机制的设计。我先是设计键盘交互的部分,这一块较为简单。也没有需要调试的地方。但是当我加入鼠标交互功能是,却遇到了难题:当用户输入的时候,无法通过鼠标点击相应按钮实现功能。必须等到输入结束之后才能实现鼠标点击。而且需要鼠标点击的阶段也无法通过键盘操作。即无法同时用鼠标和键盘进行操作。
为了解决这一问题,我在需要用户操作的阶段设置了一个while(1)循环,里面先检测是否有键盘消息,如果没有再检测是否有鼠标消息。通过这样类似于多线程的方法,系统可以实现同时通过鼠标和键盘操作,代码如下:

1
2
3
4
5
6
7
8
9
10
11
while(1){
if(kbhit()){
char k=getch();
if(k=='\r')break;//确定
}
int x,y;
if(checkClick(&x,&y)){
if(checkIn(x,y,WIDTH/2-210, HEIGHT-340, 160, 50))break;//确定
if(checkIn(x,y,WIDTH/2+50, HEIGHT-340, 160, 50))return 0;//返回
}
}

但是此时又造成了另一个问题——页面无法及时的更新。在之前的代码中,程序只有到getch(),即等待用户按键的时候,才会更新页面。原先因为只有键盘输入所以不会造成影响。而如今只有检测到键盘消息,才读取键盘操作,导致页面无法及时更新。思考片刻后,我认为系统只有等待的时候才会更新界面,所以我在每一处改变页面的代码之后加了delay_fps(60)延时函数。正如我猜想,加入延时函数之后,问题成功解决。

测试结果

  1. 进入系统后,显示欢迎界面:
  2. 按任意键进入系统主菜单,点击对应按钮进入对应的功能界面。
  3. 进入购买车票的界面后,出现起止站的输入框。首先通过键盘输入起始站,将在输入框中出现相应字符。最多输入3个字符,而且必须是数字。点击确定或者按下回车键后,可以在目的地的输入框中输入。点击刷新按钮后可以从起始站重新输入。点击返回或者按下esc键将回到主界面。起止站输入完成后,点击确定键或者回车键,如果站台编号符合要求将进入输入票数的界面。若是不符合将被要求重新输入站台编号。
  4. 输入票数的界面中,标题下方显示两个站点之间最短距离的票价。再下面是询问购买的票数,可通过键盘输入数字字符,最多可输入一个字符。点击返回或者按下esc键回到上一界面。点击确定或者按下回车键进入下一界面。
  5. 显示总价格,并且最后询问是否购买车票。点击返回或者按下esc键回到上一界面。点击确定或者按下回车键进入下一界面。
  6. 投币界面中标题下方出现提示。按数字键可以在界面右下方的输入框中输入数字字符,最多可输入两个字符。点击确定或者回车键,如果投入的钱币小于总价格,则显示“钱币不足,请再次投币”。直到投入钱币总和大于等于总价格,到下一界面。
  7. 购买成功后,显示信息与提示,按任意键返回主界面。
  8. 进入地图查询界面后,显示地铁线路图,并且在线路图下方有输入框,可以输入站点编号。只能输入数字,且最多三个字符。点击查询可以在线路图右侧显示查询的地图信息。在屏幕右下方出现“继续查询”与“购票”按钮。点击“继续查询”,可以继续输入站点编码。点击“购票”按钮直接进入购票界面。点击返回或者按下esc键返回系统主界面。
  9. 系统说明界面显示购票流程与用户须知,按任意键返回系统主界面。

问题

  1. 无法输入中文字符。EGE自带的键盘输入字符函数是直接从读取按下的键盘按键,所以无法从输入法输入中文。唯一的解决方法是通inputbox_getline()函数,用对话框让用户输入一个字符串。但是因为对话框样式风格与系统界面不符,也没有找到改变对话框样式的方法,所以没有通过输入站台名称查询站台信息的功能。
  2. 没能清空鼠标缓存消息。不过好在这一点因为点击之后能够马上切换到新的界面,所以没有构成什么问题。

可提升空间

  1. 当鼠标移到或点击按钮时,出现动画。
  2. 输入站名查询站点信息。
  3. 提供点击站点即可查询信息的功能。

分析总结与心得体会

本次程序设计,不仅让我复习巩固了许久未用的最短路径算法,并且让我熟悉了使用库函数设计用户交互界面。而且将代码写在不同文件中,自己写一个头文件也是第一次尝试。虽然在新事物上折腾了很多时间,但结果还是非常令人满意的。经过此次课程,我的确获得了极大的进步。