基于C++ 实现(界面)校园导游系统

♻️ 资源

大小: 8.26MB

➡️ 资源下载: https://download.csdn.net/download/s1t16/87450285

校园导游系统

概述

设计一个校园导游程序,用户提供各种信息查询服务。

基本要求:

系统中记录了校园中的教学楼、图书馆、食堂、田径场、篮球场、超市、医务室等坐标信息和连接这些坐标的路径信息。

每条路径包含两个坐标间的距离和预计消耗的卡路里。

能进行坐标点的增加和删除。

能够满足不同用户的查询,如:两坐标之间的最高卡路里路线和最短距离路线。

实现提示:

一般情况下,校园的道路是双向通行的,可设校园平面图是一个无向网。顶点和边均含有相关信息。

从我校平面图中选取 10 个大家熟悉的景点,抽象成一个无向带权图。以顶点表示景点,边上的权值表示两地的距离。

软件功能

本程序为一个较为完备的校园导游程序系统,功能主要包括以下几个方面:

图形化显示学校地图,并支持放大、缩小等功能。

查询两地点的路线,支持最短路线查询和最高卡路里路线查询两种模式,并将查询结果显示在和图形化界面上,以及以文字显示当前路径的路程以及预计卡路里消耗。

支持添加、删除校园地点,添加完成后即可用于 2 的功能中。

设计思想

总体设计思想

本题是一道综合性强、涵盖范围广、实用性强的题目。对于这种大型工程,不可能一次设计出完全适合的数据结构和算法。为此,我采用了敏捷开发的思想,结合在上个暑假在短学期实践中学习到的 QT 编码思想,先从整个系统的功能需求大致推导出需要的各个类和数据结构,按照完整的功能链需求列出各个类之间的关系,快速开发出一个基础版本。然后,再对该版本逐步进行完善,得到更加完善的版本。由于本题没有涉及到动画播放、延迟等等方面的内容,故算法和图形界面的代码可以实现完全分离。这对于面向对象设计是一件很好的事情。在代码结构的设计中,我充分利用了面向对象的开发思想,为每个可以抽象出来并且具有一些类似操作的部分都设计了相应的类,如线路类、地图系统类、图形界面管理类等等。各类之间的关系也非常明确,比如线路类中含有两个地点结构体成员,校园导航系统类中含有多个线路类成员以及多地点结构体成员等等。在开发过程中,首先大致设计出后端的各种类和数据结构,并且加以实现。然后再逐步实现前端的界面,过程中将后端操作与前端的按钮等进行连接,实现前后端相连。

各模块具体实现思想

本题的数据结构设计以及类的划分大多是按照实际情况来的,地图是直接选用的学生组织与发展中心做的湖工大地图。地图是由路口和路连接起来的,所以我建立了 Pos 结构体表示一个点的位置,两个连通的点则是一条路,所以我建立了一个 Edge 类,该类中存储了两个点结构体、路线长度等信息。在 Map 类中我建立了 Pos 的集合表示所有路口、Edge 的集合表示所有路,在每个地点都有自己的信息,故我在地图系统类中建立了一个 map 表示地点名和位置的相关信息。此外,整个校园地图是一个整体,故我建立了一个 Map 类,用于表示一整个地图系统,包含多个线路、路口、地点的信息。这种类设计也体现了一种自顶向下、自下而上的思想。在数据结构方面,由于地点是名称和实体对象之间是一一对应的,路口所属路直接也是一一对应的,故我在很多地方使用了 c++ STL 中的 std::map 这一数据结构。std::map 是基于红黑树实现的一种键值相对应的类型,使用效果非常类似于哈希表,但是其查找的复杂度为 O(logn),稍慢于哈希表。程序中由于路口和线路数目有限,故使用 std::map 也可以达到很高的效率。在如地图中所有路和路口方面,我还使用了集合这一数据结构,c++ 中的集合类是 std::set,集合的特性使得元素不能重复插入集合中,非常适合某些特殊场合的要求。在本题的算法设计部分,最关键的部分是如何查询最短路径以及最高卡路里路径。这两个问题我采用了两种不同的实现方法。最短路径问题是典型的使用 Dijkstra 算法进行求解的问题。同时,通过查阅各种资料,我了解到还有一种 SPFA 算法,也可以很好地求解单源最短路径问题。SPFA 算法要对所有的边去进行一次松弛操作,进行了 n-1 次更新,先初始化距离数组,起点赋值为 0,其余赋值为无穷大,先起点入队列,入了队列的被标记,当队列不为空时循环,队首元素出队,松弛与队首元素相连的边,这些被更新的点如果不在队列中就加入队列,队首元素继续出队,松弛与队首元素相连的边,是不需要去找离原点最近的点的,所以 Dijkstra 算法用的是小根堆优化,SPFA 直接用的队列优化。此外,SPFA 算法可以处理边权值为负的情况。由于地铁站点之间的距离一定是正数,因此这一点利用不到。在程序中,我使用了 Dijkstra 求解最短路径问题。算法的主要流程如下(为简化算法流程图,没有标出不存在任何路径的情况):

最高卡路里路径问题无法使用上述算法进行求解,因为无法确定当前为最长路径。如果以仅仅是将小于号改为大于号,那么将会出现到达终点时,但其实并不是最长路径的情况,而且这种情况是很不好判断的,需要记录前面经过的所有站点,因此也不适用 SPFA 算法。程序中,我使用了广度优先遍历思想进行了算法设计。从起始路口开始,依次遍历所有所属路的另一个路口,找到终点则记录当前的卡路里消耗与最大卡路里消耗对比,若该路径卡路里消耗更大,则替换成当前路径。算法流程如下:

逻辑结构与物理结构

本题考察的知识点较为综合,故数据结构使用的也非常丰富。

逻辑结构方面,集合结构、线性结构、树形结构、图形结构均有涉及。集合结构用于记录路口、路等等不可重复、对顺序无要求的信息。线性结构的使用非常广泛,许多地方使用了 Vector 等线性结构,如返回查询的线路结果等等。树形结构没有显式进行使用,但是 Qt 的图形对象都设置了 parent,整个图形界面实际上在逻辑上反映为一颗很大的对象树,MainWindow 是根节点。最后,地图类的表示显然使用了图形结构,Dijkstra、SPFA 等算法也是基于图形结构实现的。

物理结构方面,也是顺序存储、链式存储、散列存储、索引存储均有涉及。前面两项分别对应 std::vector 和 std::list 的使用,较为普遍。对于需要经常随机访问而很少插入、删除的数据列表而言,使用顺序存储结构;对于需要经常插入。删除的数据而言,使用链式存储结构。由于地点的名称与其位置一一对应,故在地铁系统类中,使用名称到内容的 Hash 表来进行存储。这里用到了散列存储方法,使用了 c++ 中的 std::map 结构。关于所有路口和路,使用了索引存储结构进行存储。

类定义如下:

点结构体,记录一个点的信息

复制代码
struct Pos
{
    /* data */
    int x;
    int y;
    Pos(int xx=0,int yy=0) {
        x=xx;
        y=yy;
    }
    void operator=(const Pos &a){
        x=a.x;
        y=a.y;
    }
    bool operator!=(const Pos &a){
        return (x!=a.x && y!=a.y);
    }
    bool operator==(const Pos &a){
        if(std::abs(x-a.x)<=15 && std::abs(y-a.y)<=15)
            return true;
        return false;
    }
    bool operator==(Pos &a){
        if(std::abs(x-a.x)<=15 && std::abs(y-a.y)<=15)
            return true;
        return false;
    }
};

边(路)类,记录路的两个路口以及距离等

复制代码
class Edge{
public:
    Pos start_pos;
    Pos end_pos;
    double len;
    double Len(){ return dist(start_pos,end_pos); }
    Edge( Pos s=Pos(),Pos e=Pos() ){ start_pos=s; end_pos=e; len=dist(start_pos,end_pos); }
    void operator=(Edge& a){
        start_pos=a.start_pos;
        end_pos=a.end_pos;
        len=a.len;
    }
    void operator=(const Edge& a){
        start_pos=a.start_pos;
        end_pos=a.end_pos;
        len=a.len;
    }
    bool operator==(const Edge &a){
        return (start_pos==a.start_pos && end_pos==a.end_pos);
    }
    bool operator!=(const Edge &a){
        return (start_pos!=a.start_pos || end_pos!=a.end_pos);
    }
    bool operator<(const Edge &a){
        return dist(start_pos,end_pos)<dist(a.start_pos,a.end_pos);
    }
    bool operator<=(const Edge &a){
        return dist(start_pos,end_pos)<=dist(a.start_pos,a.end_pos);
    }
    bool operator>(const Edge &a){
        return dist(start_pos,end_pos)>dist(a.start_pos,a.end_pos);
    }
    bool operator>=(const Edge &a){
        return dist(start_pos,end_pos)>=dist(a.start_pos,a.end_pos);
    }
};

地图系统类,记录所有地点、路口、路,以及管理地图的各种方法如地点插入、删除、路径查询等

复制代码
class Map : public QObject{
    Q_OBJECT
private:
public:
    Map();
    bool AddEdge(Pos &Start,Pos &End);
    bool AddEPos(int x,int y);
    bool AddPlace(std::string name,Pos pos);
    bool ErasePlace(std::string name);
    void readpos();
    void readedge();
    void readplace();
    std::vector<Pos> dijkstra(std::string sname,std::string ename);
    std::vector<Pos> best_far(std::string sname,std::string ename);
    std::vector<Pos> ans_far_temp;
    double ans_far_dist_temp;
    std::vector<Pos> ans_far;
    double ans_far_dist;
    int ans_i;
    std::map<Pos,bool,pos_cmp> ans_far_vis;
    void best_far(Pos& s,Pos& e);
    std::set<Pos,pos_cmp> m_pos;
    std::set<Edge,edge_com> m_edge;
    std::map<std::string,Edge> m_place;
    std::map<Pos,std::vector<Edge>,pos_cmp> m_pos_edge;
    int num;
    double ans_len=0;
    double ans_k=0;
};

算法核心类就是上面三个类。另外,我还定义了一些 ui 界面类,便于图形界面绘制。MyQGraphicsView 类是校园地图的绘制窗口的类。这个类的功能比较简单,大多数函数是用来让主窗口通过界面类调用 Map 类方法的,可以支持绘图窗口的缩放。定义如下:

复制代码
class MyQGraphicsView:public QGraphicsView
{
    Q_OBJECT
public:
    MyQGraphicsView(QWidget *parent = nullptr);
    ~MyQGraphicsView();
private:
    MyQGraphicsScene* scene;
    QPixmap* mapPix;
    QGraphicsEllipseItem* Item;
private:
    void wheelEvent(QWheelEvent *event) Q_DECL_OVERRIDE;
    void mousePressEvent(QMouseEvent *event);
public:
    Map mapMap;
    QPointF scencePos;
    QPen pen;   // 定义一个画笔,设置画笔颜色和宽度
    Pos Start;
    Pos End;
    Pos nowPos;
public:
    void AddEdge();
    void AddEPos();
    bool AddPlace(std::string name);
    bool ErasePlace(std::string name);
    void Clear();
    void solve_epos();
    void solve_pos();
    void show_poss();
    void show_edges();
    void show_places();
    void show_edge(Pos s,Pos e);
    bool query_path(std::string sname,std::string ename,int state);
};

最后,主窗口类的定义如下:

复制代码
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
    void AddPlace();
    void ErasePlace();
    void query_path();
    void check_key();
    void updata_box();
    void updata_info_label();
private:
    Ui::MainWindow *ui;
};

开发平台

计算机信息:

  • 计算机型号 :联想扬天 v14
  • 计算机内存 :8.0G
  • 处理器 :AMD Ryzen 5 4500U with Radeon Graphics 2.38GHz
  • 操作系统 :Windows 11 专业版

开发平台:

  • 编程语言 :C++(C++11 标准以上)
  • 开发环境 :Qt Desinger 4.11.1,vscode,cmake
  • 编译器 :mingw64 9.1.0(gcc11.2.0)

运行环境:

可以将代码在上述 Qt Creator 4.11.1 集成环境中打开并运行,注意 Qt 版本需要在 5.12 以上。

同时也支持使用 cmake+mingw 编译运行,注意 mingw 需要支持 C++11 标准以上,以及需要将对应的 qt 目录中 mingw 的 dll 目录添加进环境变量中。

也可以打开文件夹 school_guied_system1.0,直接运行其中的 school_guied_system1.0.exe,该文件包使用 windeployqt 进行了封装,可以直接在普通电脑上运行。

应该注意的是,运行文件时应有与运行程序同级目录的 src 文件夹用于存放资源文件。

系统的运行结果分析说明

调试开发过程

本题系统的调试相对来说是比较综合的过程,调试起来一定也有一定难度。本题的调试主要通过 qDebug 打印信息调试、vscode 调用 gdb 的 debug 方法调试、以及结果显示到图形界面进行调试三种方法。由于本题的核心算法和图形界面完全分开,所以我先编写好这些核心算法的代码,再通过控制台进行算法调试。在算法测试完毕后,再将其综合到图形界面。这样一来,图形界面的各类错误更加容易定位,在调试图形界面的过程中不需要再调试算法部分。先设计好核心算法,实现好后端的核心代码。然后,再通过 Qt Creator 代码拖拽结合的方式设计前端图形界面。如下图:

同时,本题中由于路口、路线显示窗口的图形绘制更为复杂,故我没有使用 QPainter 进行图形绘制,而是使用了 QGraphicsView 结合 QGraphicsScene 进行绘图。

如上图,我自己设计了一个可以放大/缩小的 MyQGraphicsView 类,并将 ui 中的 QGraphicsView 提升为我自己设计的类,然后再使用 Scene 进行布局,最终将结果绘制在 MyQGraphicsView 控件上。这种绘图方法比 QPainter 更加灵活,尤其适合用于元素丰富的绘图需求。

程序正确性展示

经过多次查询线路、添加站点等操作的检测后,程序表现全部正确。其正确运行界面如下:

程序成功运行后,会自动读取 hugong_map.jpg、edge.txt、edge_pos.txt、place.txt 进行初始化,然后在窗口中进行显示。由于初始化的文件读取已经在代码中写好,故这一过程自动完成。如下:

输入起始站和终点站进行查询,如查询图书馆到科技楼的最短路线,查询结果如下,左侧显示路途和预计卡路里消耗,右侧显示图像路线。同样的,可以查询最高卡路里路线,可以看到两条路线都是正确的。

添加、删除地点如下。可以看到添加、删除完地点后,查询下拉列表会同步进行更新。

程序稳定性展示

程序的稳定性表现优秀。所有的类中使用 new 运算符动态申请的内存,全部在析构函数中进行了 delete 操作。图形界面中动态申请的 Qt 类对象,全部设置了 parent。在 parent 关闭时,这些 new 运算符申请的 Qt 类对象会自动进行析构。各类消息对话框全部使用 Qt 自带的 QMessageBox 进行,避免在主窗口关闭前反复触发消息对话框可能导致的内存耗尽情况。另外,每次进行查询操作后,如果图形界面有变化,都会进行 Clear 操作,进行同步更新。

复制代码
void MyQGraphicsView::Clear() {
scene->clear();
scene->addPixmap(*mapPix);
}

程序容错率能力展示

程序对各种错误都进行了详细的、周全的处理。如下:

添加地点、删除地点、查询路径时未输入地点名称,会弹出对话框进行提示。

添加地点重复、删除地点不存在、查询路径起点终点相同会有提示。

还有一些别的错误处理,此处不再列出。

运行案例说明

添加新地点图书馆、科技楼:

查询图书馆到科技楼的最短路径、最高卡路里路径:

  • 可以看到,两条路线被成功且正确显示。
  • 操作说明
  • 程序运行后,展示如下界面

在右侧可以看到上湖工大地图。其图片存储于 src 目录下的 huogng_map.jpg 中。 可以拖拽右下角或者点击最大化按钮来获得更大视图。

可以通过鼠标滚轮进行地图放大和缩小、鼠标左键按下可进行地图拖拽,放大和缩小以当前窗口的中心为 中心进行。

左上角点击"查询"页,在左边的输入框中输入起点和终点,选择查询策略,即可查询线路。

点击左上角"修改地点"页,鼠标左键地图,则会留下一个点表示当前选中点,输入地点名称,点击添加即可添加;输入地点名称,点击删除即可删除已保存的点。

点击左上角"开发者功能"页,输入密码:root 即可使用开发者功能,主要是为了调试、添加路口、路是使用的,不涉及系统使用,便不在此过多描述。

致谢

几经波折终于将这个程序写完,在程序的编码过程中遇到了无数的困难和障碍,都在同学和老师的帮助下度过了。尤其要强烈感谢 GitHub,开源的世界真的很美好,很多优秀的代码值得我们学习。另外,在官方文档查找资料的时候,文档也给我提供了很多方面的支持与帮助。在此向帮助和指导过我的各位老师表示最衷心的感谢!

感谢这个程序所涉及到的所有开源项目,如 qt、cmake 等。本文引用了数位学者的研究文献,如果没有各位学者的研究成果的帮助和启发,我将很难完成本程序的编码。

感谢我的同学和朋友,在我编码的过程中给予我了很多灵感,还在程序测试过程中提供热情的帮助。

学习体会

整个开发过程经过大概是经过了三四天,也是比较幸运可以参加到学习学校的培训项目,在项目中实现实现了一个校园导游咨询模拟系统,对于还有过 Qt 项目开发经历的我来说,还算得心应手,之前的 Qt 项目是基于内网的远程桌面控制系统,与此次的题目截然不同,但一些基本大致的思想还是基本类似的,在开发过程中,我不断思考,不断参考别人的代码、文献,了解并理解别人运用了什么技术,自己应该去学什么,所以这次的机会对我是真的是及时雨,让我可以更全面了解到完成一个完整项目的过程。

在开始开发之前,环境成了我的一个很头疼的问题,使用 QT Creator 固然是一个不错的选择,但是其调试功能不是很友好,以及界面优化的不是很好,所以我选择是 cmake+mingw 编译源码,使用 vscode 调试,达到很好的编码体验,但是这个过程是艰辛的,学习 cmakelist 语法,学习 vscode 配置文件,都花了我很多时间,不过好在,最后还是通过参考别人的帖子、官方文档解决了这个问题。

我采用了敏捷开发的思想,结合在上个暑假在短学期实践中学习到的 QT 编码思想,先从整个系统的功能需求大致推导出需要的各个类和数据结构,按照完整的功能链需求列出各个类之间的关系,快速开发出一个基础版本。然后,再对该版本逐步进行完善,得到更加完善的版本。由于本题没有涉及到动画播放、延迟等等方面的内容,故算法和图形界面的代码可以实现完全分离。这对于面向对象设计是一件很好的事情。在代码结构的设计中,我充分利用了面向对象的开发思想,为每个可以抽象出来并且具有一些类似操作的部分都设计了相应的类,如线路类、地图系统类、图形界面管理类等等。各类之间的关系也非常明确,比如线路类中含有两个地点结构体成员,校园导航系统类中含有多个线路类成员以及多地点结构体成员等等。在开发过程中,首先大致设计出后端的各种类和数据结构,并且加以实现。然后再逐步实现前端的界面,过程中将后端操作与前端的按钮等进行连接,实现前后端相连。

通过网上的视频和文章,不断了解以前没有是用过的 QT 类库,一边感叹于 QT 软件的强大,也开始对其他界面设计的软件有了兴趣,接触如此大型的开源程序,以前都是使用简单的 IDE 对着控制台写数学题。在刚开始的时候,还是有很多的地方很不习惯,很多问题很不理解,虽然身边很多人都说现在不用太搞懂这些,只要知道怎么用就行,但是我还是下了决心之后将它们搞懂了,就比如在 QT 中很常用而且在各大论坛博客都有说很重要的一个功能---信号槽,它的用法十分简单,只需要有信号函数和槽函数就可以通过 connect 函数使用信号槽了,信号函数和槽函数都是可以自己来设立,可以在任何地方 connect 或者 disconnect,系统也有提供了很多 API,很方便实用,刚学会最简单的按键响应时我还很高兴的玩了好一会,但是这些好用方便的背后一定是有着其原理的,这几天的学习中,还让我对线程有了一些了解,之后整合功能的时候用到线程也是我来完成的,在我的印象中,程序运行一直都是阻塞的,但这个信号槽就好像在建立信号槽连接的时候另起了一个线程,用来单独一直监听信号函数的调用,当信号函数被调用,则立即调用槽函数,但是这个似乎是很浪费资源的一种假设,然而在各大平台均被夸赞的信号槽,我认为是没有那么简单的,查了很多资料依然没有找到我想要的答案,我对线程的使用也不是很了解,如果能实时看着线程什么时候开始、结束,或者更暴力一点,我能看懂 QT 的源码,这个问题才能解决吧,折腾这种问题不知道有没有意义,但是现在有这份热情,就是想知道。

得益于 QT 的方便的强大,UI 界面设计并没有花费太多的时间,这为我在其他功能实现上省出来更多时间。

完成程序的编写,决不意味着万事大吉。你认为万无一失的程序,实际上机运行时可能不断出现麻烦。如编译程序检测出一大堆错误。有时程序本身不存在语法错误,也能够顺利运行,但是运行结果显然是错误的。开发环境所提供的编译系统无法发现这种程序逻辑错误,只能靠自己的上机经验分析判断错误所在。程序的调试是一个技巧性很强的工作,对于初学者来说,尽快掌握程序调试方法是非常重要的。有时候一个消耗你几个小时时间的小小错误,调试高手一眼就看出错误所在。不过好在之前接触过一点 QT,调试过程还算顺利,这对我们将来到社会工作将会有莫大的帮助。同时它让我知道,只要你努力,任何东西都不会太难。

最后的最后,我完成了项目的开发,把这个校园导游咨询模拟系统优化到了我目前可以做到的最好,这个过程中,我了解到了很多以前都没有接触到的知识,对一个项目的完整开发过程又有了更深刻的理解,也认识到自己还有许多知识需要学习,很满意这次项目开发中自己的收获!!!