简介:一份基于Qt框架开发的校园导游系统完整项目包,面向东北大学秦皇岛分校师生及访客,也可作为数据结构课程设计的参考范例。系统实现校园地图导航、路径规划、地点查询、景点介绍与路线推荐等功能,覆盖教学楼、宿舍、图书馆、食堂等主要场所,并提供多种路径策略。压缩包共四十九个文件,主要包含cpp/h源码、ui界面文件、qrc资源文件、pro工程配置、exe可运行程序、docx/readme说明文档及图片素材,整体约七点七四兆字节,结构清晰便于二次开发。已有四十六人学习下载。通过本项目可掌握Qt图形界面与C语言结合的应用开发思路,理解图结构在路径规划中的实际运用,同时获得一套可直接运行演示的校园导览解决方案,适合需要课程设计参考或校园导航应用开发的学习者。
1. 校园导游系统难的不是画地图,而是怎么把“图”组织起来
被课程设计题目“校园导游系统”带偏的人不在少数——第一反应是找张校园地图、抠几个建筑图标,然后就开始画界面。真正动手才发现,绘图只占工作量一小部分,卡住大家的永远是数据结构和路径规划那一段:景点怎么存、道路怎么建模、两个楼之间最短路径怎么算。用 Qt 做这类项目,从数据层到交互层全都要自己串起来,恰恰是它最适合做数据结构课程设计的原因:你写的图、你写的搜索算法,都能直接在窗口上看到结果。这篇文章会从数据结构选型、Qt 工程骨架、Dijkstra 落地、地图交互到排查经验讲一遍,带你把一个校园导游系统从空窗口做到能查、能导、能推荐。适合正在做同类课程设计的学生,也适合想用 Qt 整理一遍图算法落地细节的开发者。
2. 把校园建模成一张图:数据结构和工程骨架先行
2.1 核心数据结构:无向图还是带权有向图,邻接矩阵还是邻接表
校园道路本身没有单向禁行这一说(先不考虑个别单行路段),所以默认用无向带权图建模是符合直觉的。两个景点之间有路,就建一条边,权值用步行距离或者估算步行时间都行。我一般用步行分钟数当权值,因为界面里可以直接展示“从图书馆到食堂步行约 8 分钟”,比展示 420 米更贴近用户。如果课程设计里强调要展示距离,也可以在边上同时存 distance 和 time 两个属性,Dijkstra 的时候按需取用。
图结构用邻接矩阵还是邻接表?校园景点数量一般控制在 20~40 个节点,这两个结构其实都够用。但从课程设计答辩角度讲,邻接表更容易说清楚“稀疏图里邻接表更省空间”这个知识点;从代码调试角度讲,邻接矩阵的初始化更直观,不容易出现指针问题。我的建议是:如果节点数少于 50,直接用邻接矩阵,省去一堆链表操作,把精力留给算法和界面;如果节点多或者想展示“邻接表 + 优先队列优化”,再上邻接表。
struct Edge { int to; // 目标景点编号 int distance; // 步行距离(米) int time; // 估算步行时间(分钟) }; struct Spot { int id; QString name; // 景点名称,用于查询 QString category; // 分类,如食堂、图书馆、教学楼 QString intro; // 景点介绍文本 QPointF pos; // 在地图图片上的坐标 }; // 图数据:编号从 0 开始,n 为景点数量 int spotCount = 0; QVector<Spot> spots; QVector<QVector<Edge>> graph; // graph[u] 存所有与 u 相连的边这里把 graph 设计成“邻接表式”的 QVector 嵌套,实际上是为了后续遍历方便,也是为了配合 Qt 的容器类自动管理内存。如果你更习惯把 graph 定义成二维 int 数组存权值,也完全可以,但要注意 distance 需要单独开一个二维数组存储,时间权值再开一个,容易字段一多就漏初始化。用一个 Edge 结构体挂在一维邻接表里,字段跟着边走,后续排错时更好定位。
2.2 C++/Qt 工程骨架:pro 文件、主窗口与地图画布的搭建
拿到题目先别急着写界面。先把工程骨架立起来:一个主窗口、一个放置地图的 QGraphicsView、一个查询输入框、一个结果展示区域。这个结构基本就是所有校园导游系统的通用布局。
QT += core gui widgets TARGET = campus_guide TEMPLATE = app SOURCES += main.cpp mainwindow.cpp mapwidget.cpp graphmanager.cpp HEADERS += mainwindow.h mapwidget.h graphmanager.h RESOURCES += resources.qrcpro 文件里最容易漏的是QT += widgets,尤其当你用 Qt 6 或新装的 Qt 5 环境时,默认模板可能只带 core 和 gui,没有 widgets。编译直接报“找不到 QMainWindow”。另一个重点是 RESOURCES,校园导游系统一定会用到一张校园地图图片,把图片放进 .qrc 资源文件里,才能保证程序在其他机器上运行时也能找到图,而不是依赖外部路径。大多数课程设计翻车现场都是因为直接写绝对路径加载图片,换台电脑就白屏。
主窗口的布局不做复杂设计,左侧放地图画布,右侧放查询和介绍区域就够用。地图画布继承 QGraphicsView,地图图片作为一个 QGraphicsPixmapItem 放进 QGraphicsScene,景点标注和路径线作为独立 item 叠加到同一 scene 上。这样设计的好处是:底图、景点、路径互不干扰,移动窗口缩放时只需要对 view 做处理,不需要重建 scene。
// mainwindow.h 关键声明 class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); private: MapWidget *mapView; // 自定义地图画布 QLineEdit *searchEdit; // 地点查询输入框 QListWidget *infoList; // 地点结果列表 };界面结构确定后,编码顺序建议是:先写数据加载,再写算法,再写界面绘制,最后做查询推荐。先数据后界面,可以让你在命令行里先验证路径规划结果正确,再往界面上画。这个顺序能避免“算法错了还在调界面”的双重折腾。
2.3 把校园地图转成图数据:用 JSON 存节点和边的具体方式
校园地图路径规划系统最关键的一步,是让程序认识地图上的点和路。手动写死在代码里当然可以,但几十个景点、几十条边,全写在 .cpp 里又凌乱又难调整。常见做法是单独维护一份 JSON 数据文件,里面按节点和边分开组织,加载时一次性读入内存。
{ "spots": [ { "id": 0, "name": "东门", "category": "校门", "pos": [120, 540], "intro": "校园主出入口,毗邻公交站" }, { "id": 1, "name": "图书馆", "category": "学习", "pos": [210, 260], "intro": "馆藏丰富,自习座位充足" } ], "edges": [ { "from": 0, "to": 1, "distance": 300, "time": 4 }, { "from": 0, "to": 2, "distance": 220, "time": 3 } ] }pos 字段存的是该景点在原始地图图片上的像素坐标,这个坐标不是经纬度,就是图片上的 x/y,用来在 QGraphicsScene 里定位。读取 JSON 用 Qt 自带的 QJsonDocument 解析即可,不需要引入第三方库:
#include <QJsonDocument> #include <QJsonObject> #include <QJsonArray> bool loadMapData(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) return false; QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); QJsonObject root = doc.object(); QJsonArray spotsArr = root.value("spots").toArray(); for (const auto &v : spotsArr) { QJsonObject obj = v.toObject(); Spot s; s.id = obj.value("id").toInt(); s.name = obj.value("name").toString(); s.category = obj.value("category").toString(); s.intro = obj.value("intro").toString(); QJsonArray posArr = obj.value("pos").toArray(); s.pos = QPointF(posArr.at(0).toDouble(), posArr.at(1).toDouble()); spots.append(s); spotCount++; } // edges 同理,注意把无向边拆成正反两条 QJsonArray edgesArr = root.value("edges").toArray(); graph.resize(spotCount); for (const auto &v : edgesArr) { QJsonObject obj = v.toObject(); int from = obj.value("from").toInt(); int to = obj.value("to").toInt(); int dist = obj.value("distance").toInt(); int time = obj.value("time").toInt(); graph[from].append({to, dist, time}); graph[to].append({from, dist, time}); } return true; }这段代码里容易踩坑的地方是边要拆成双向。很多同学只加了 from->to 一条,结果路径规划时反向搜索永远找不到路,界面表现就是“从图书馆导到东门可以,反过来就不行”。加载完成建议先写一个自检函数,统计每个节点的出度并把所有边数打印出来;无向图边数一定是记录条数的两倍,不符就说明 JSON 里漏拆或数据源有重复。
3. 路径规划与地点查询的落地:Dijkstra 和搜索模块
3.1 最短路径算法选择:Dijkstra 的实现与单源路径输出
校园导游系统的最短路径,本质是单源最短路径问题。图里没有负权边,Dijkstra 就是最合适的算法。不需要上 Bellman-Ford,更不需要 Floyd-Warshall——景点数量少,但这些选型在答辩时被问住就麻烦。把 Dijkstra 讲清楚,比堆一堆花哨算法更稳妥。
优先队列优化版本是必须的,因为邻接矩阵的 O(n²) 在 30 个节点时看不出差距,但答辩老师一句“如果节点到 500 个怎么办”就能让你下不来台。用优先队列实现,既能解释清楚“每次取当前距离最小的节点”这个贪心思想,又能顺带展示你掌握了 C++ 的优先队列用法。
#include <queue> #include <QVector> QVector<int> dijkstra(int start, int goal, QVector<int> &prev) { const int INF = 1e9; QVector<int> dist(spotCount, INF); prev.fill(-1); // 用 pair(距离, 节点编号),小顶堆 using P = std::pair<int, int>; std::priority_queue<P, std::vector<P>, std::greater<P>> pq; dist[start] = 0; pq.push({0, start}); while (!pq.empty()) { auto [d, u] = pq.top(); pq.pop(); if (d > dist[u]) continue; // 跳过旧记录,重点优化点 if (u == goal) break; for (const auto &e : graph[u]) { int nd = d + e.distance; if (nd < dist[e.to]) { dist[e.to] = nd; prev[e.to] = u; pq.push({nd, e.to}); } } } // 回溯路径:从 goal 一直找 prev 到 start QVector<int> path; for (int cur = goal; cur != -1; cur = prev[cur]) { path.push_back(cur); if (cur == start) break; } std::reverse(path.begin(), path.end()); return path; }这段代码有两个细节值得解释。第一个是if (d > dist[u]) continue,这是优先队列优化 Dijkstra 防止重复处理过期节点的关键,漏写会出现逻辑正确但“跑得慢且难调试”的情况;第二个是回溯路径时用 prev 数组,注意起点要标记为 -1,否则回溯会在起点处继续向前找。返回值是节点编号数组,界面层拿到它就能按顺序连线。
参数上要关注的是 INF 的取值,我用 1e9,比所有实际路径长度大一个数量级,避免相加溢出;如果改成 INT_MAX,nd = d + e.distance一旦溢出会变成负数,反而进不了nd < dist[e.to]的分支,表现就是某些节点永远“不可达”,这种翻车现象非常隐蔽。
3.2 地点查询:按名称模糊匹配与关键字补全
地点查询模块要支持中文名称的模糊匹配。直接 name == 比较太弱,用户输入“图书”就要能匹配到“图书馆”。合适的做法是直接枚举所有景点,做包含匹配;数据量不大,用不着上倒排索引。想做得顺手一点,可以在 QLineEdit 的 textChanged 信号里做实时过滤,把匹配到的景点填到 QListWidget 里。
void MainWindow::onSearchTextChanged(const QString &text) { infoList->clear(); if (text.isEmpty()) return; for (const auto &s : spots) { if (s.name.contains(text, Qt::CaseInsensitive)) { QListWidgetItem *item = new QListWidgetItem(s.name); item->setData(Qt::UserRole, s.id); // 存 id 用于后续点击 infoList->addItem(item); } } }这里需要注意contains(text, Qt::CaseInsensitive)只对英文有效,中文里不存在大小写问题,保留这个参数是为了兼容英文楼名或缩写。另一个容易忽略的点是Qt::UserRole存 id——用户点击列表项时,你拿到的如果不是 id 而是纯字符串,后续去查景点介绍还得再做一次名字匹配,遇到重名就踩坑。所以数据绑定这一步越早做越好。
查询匹配这块还要注意一个体验问题:实时搜索在中文输入法下会触发“拼音中间态”,比如用户想输入“图书馆”,输入法是“tushuguan”,textChanged 会被连续触发。常见处理是加一个 200ms 的 QTimer 防抖,等用户停止输入 200ms 之后再做过滤,这样既省遍历又不会在拼音阶段把结果列表刷得闪来闪去。
3.3 路线推荐:基于景点标签与经停点的推荐逻辑
路线推荐是校园导游系统区别于普通地图导航的差异点。普通地图只回答“怎么从 A 到 B”,校园导游还承担“校园导览与信息查询”的角色。路线推荐这里我用两类实现:一类是基于景点 category 标签的推荐路线,比如“新生参观路线”“食堂路线”“教学楼路线”,本质是预置若干节点序列;另一类是根据起点和终点自动算出最短路径后,把路径上所有景点简介拼接起来的导览模式。
预置路线在数据结构上就是一个 QVector<QVector >,每条路线存一串景点编号:
struct RecommendRoute { QString title; // 路线名称 QString desc; // 路线说明 QVector<int> spotIds; // 依次经过的景点编号 }; QVector<RecommendRoute> recommendRoutes;在“路线推荐”按钮的响应函数里,读取当前选中的路线,先清空地图上旧的路径线,再沿着 spotIds 逐段调用 Dijkstra。比如一条路线经过 1 -> 3 -> 5,就分别计算 1 到 3、3 到 5 的最短路径,拼成一条完整路径后绘制。这个做法叫“分段最短路线拼接”,它就是经典“旅行商问题”的简版:只保证相邻点之间最短,不保证全程整体最优。答辩被问到“为什么不整体算最优”时,就直接回答“整段做 TSP 优化是 NP 难问题,以校园导览场景的规模,分段贪心已经很实用”——这段对话本身就是加分点。
QVector<int> buildRoutePath(const QVector<int> &ids) { QVector<int> fullPath; for (int i = 0; i < ids.size() - 1; ++i) { QVector<int> prev; QVector<int> seg = dijkstra(ids[i], ids[i + 1], prev); if (seg.size() <= 1) continue; fullPath += seg; fullPath.removeLast(); // 去掉重复端点,避免连线回绕 } return fullPath; }路线推荐逻辑里有个隐蔽问题:如果 dijkstra 里已经 push 了终点(可以不 push),seg 会把终点重复带上,那么两段拼接时中间节点会出现两次,绘制时就会在那个节点处画出一个“折回头”,看起来很怪。removeLast 去掉重复端点正是处理这个问题的常用手段。
4. Qt 地图交互层:把算法结果画出来
4.1 QGraphicsView 还是 QML:从课程设计维护成本看选型
校园导游系统的界面层,最常见的疑问是用 Qt Widgets 还是 QML。我的结论很直接:课程设计场景选 Qt Widgets。理由有三个。第一,你的算法和数据结构是 C++ 写的,Qt Widgets 可以直接在 C++ 里创建控件和场景项,代码逻辑都在一个语言体系里,不需要碰 QML 的 JavaScript 上下文;第二,答辩时最容易讲解的就是 Widgets 的信号槽,而 QML 的信号交互在展示时需要用 QML 语法描述,绕了一层;第三,QGraphicsView 本身就是为“大量图元 + 交互”设计的,校园地图这种场景用它最合适,不存在性能瓶颈。
当然 QML 也有自己的优势,比如界面动画更流畅、皮肤定制更灵活,但对一个课程设计来说,这些不是核心诉求。 QML 的坑在于调试链路长:一旦 QML 层访问 C++ 模型出问题,报错信息往往不会直接提示是类型不匹配还是角色名写错,对新手不友好,我见过不止一个人因为 QML 和 C++ 数据上下文绑定失败而在黑匣子里调了一整晚。
4.2 地图绘制、坐标映射与点击拾取
地图显示分三层:底图、景点标记、路径线。底图用 QGraphicsPixmapItem 加载,景点标记用自定义 QGraphicsItem 画一个小圆点加名称,路径线用 QGraphicsPathItem 或直接 addLine。坐标映射是关键:JSON 里的 pos 是针对原始图片分辨率的像素坐标,如果 QGraphicsView 缩放或者图片被 scaled,你的点击坐标和绘制坐标就全偏了。
class MapWidget : public QGraphicsView { protected: void mousePressEvent(QMouseEvent *event) override { QPointF scenePos = mapToScene(event->pos()); // 遍历所有景点,找最近点 int nearest = -1; double minDist = 25.0; for (int i = 0; i < spots.size(); ++i) { double dx = spots[i].pos.x() - scenePos.x(); double dy = spots[i].pos.y() - scenePos.y(); double dist = std::sqrt(dx * dx + dy * dy); if (dist < minDist) { minDist = dist; nearest = i; } } if (nearest >= 0) { emit spotClicked(nearest); } QGraphicsView::mousePressEvent(event); } };这段代码有三个点要讲。mapToScene把鼠标的视图坐标转成场景坐标,这是所有点击拾取的第一道工序;如果不转,直接用 event->pos() 和 JSON 里的坐标比较,窗口拖动一像素结果全歪。第二,判断最近点时设了 25 像素的阈值,避免鼠标点到空白处也算选中,这个阈值要按地图图片大小调整,越大越好点,但太大容易误触。第三,自己去比较距离而不是依赖 QGraphicsItem 的 boundingRect 碰撞,是为了不受标记绘制大小影响,圈一个小而准的命中范围。
4.3 路径高亮与景点介绍弹窗的联动
路径规划完成后,要把 QVector 路径渲染到地图上。常见做法是遍历路径的每两个相邻节点,在 scene 上添加 line item。如果每次都重复 addLine,上次画过的线还留在场景里,会导致多次规划后线越画越多,看起来特别乱。所以先维护一个 QGraphicsItemGroup,每次规划时把整个 group 从 scene 移除再重建,路径线就干净了。
void MapWidget::showPath(const QVector<int> &path) { // 移除旧路径,防止叠加 if (pathGroup) { scene()->removeItem(pathGroup); delete pathGroup; pathGroup = nullptr; } pathGroup = new QGraphicsItemGroup(); for (int i = 0; i < path.size() - 1; ++i) { QPointF p1 = spots[path[i]].pos; QPointF p2 = spots[path[i + 1]].pos; QGraphicsLineItem *line = scene()->addLine( QLineF(p1, p2), QPen(QColor(0, 150, 255), 4)); line->setZValue(50); // 路径线画在底图和景点之上 pathGroup->addToGroup(line); } scene()->addItem(pathGroup); }这里 QPen 的宽度设 4,是为了在地图上明显可见;如果背景图颜色深,可以改成亮黄或橙色。setZValue(50)保证路径线不会被底图盖住。如果你还画了景点的文字标记,建议标记 z 值设 60,这样一路走过去的地名不会被路径线盖住——这个细节看起来小,实际上线一粗,覆盖住沿线景点名,整体观感会非常差。
景点介绍弹窗不需要用 QMessageBox,那样每次都弹窗很打断操作。我把介绍放在主窗口右侧一个 QTextBrowser 里,点击景点时刷新介绍文本、景点名称和类别信息。如果老师没有特殊要求,这种“点击即显”比弹窗舒服很多。弹窗只在“路线推荐”时用来展示一条完整路线的概述,起到向导作用。
void MainWindow::onSpotClicked(int id) { const Spot &s = spots[id]; infoBrowser->clear(); infoBrowser->append(QString("<h3>%1</h3>").arg(s.name)); infoBrowser->append(QString("<b>类别:</b>%1").arg(s.category)); infoBrowser->append(QString("<b>位置:</b>(%1, %2)") .arg(s.pos.x()).arg(s.pos.y())); infoBrowser->append(QString("<hr>")); infoBrowser->append(s.intro); }需要留意的是 append 支持富文本,但要注意注入问题——这会引入 Qt 富文本解析,本场景数据是本地 JSON 文件,不涉及外部输入,所以问题不大;如果是联网数据,需要做 HTML 转义,涉及 Qt 的toHtmlEscaped,否则会把外部内容当 HTML 解析。
5. 避坑清单:Qt 校园导游系统的 5 个常见问题与排查方法
5.1 现象:Dijkstra 计算结果为空或路径只有起点一个节点
路径规划后,界面上只画出一个点,或者完全没反应。这种多半不是算法写错,而是景点编号对不上。原因主要有三种:JSON 里的 id 不是从 0 开始连续编号;搜索时用了名称匹配返回的实际是另一个 id;或者起点终点传反了。解决方法是先打印起点和终点 id,再打印 dijkstra 返回的路径数组;只有起点没有终点时,去看 prev 数组在目标节点前的值是否都是 -1。我习惯在开发阶段给 dijkstra 加一个 debug 输出,把每个被弹出的节点和当前最短距离打出来,一眼就能看出是不是某条边没连上。
5.2 现象:地图上能显示景点,但点击景点却选不中
点击时最近点距离判断永远超过阈值。典型原因是坐标映射没对齐:JSON 里的 pos 是原始图片的坐标,但加载到 QGraphicsScene 时,如果对 pixmap 做了 scale,而 spots 数据里还是原图坐标,那自然永远点不中。解决的办法是统一坐标体系:要么在加载 pixmap 时不缩放,让图元坐标就是图片原始坐标,然后靠 QGraphicsView 的缩放来控制显示大小;要么在加载数据时就把 pos 乘以缩放系数。我推荐第一种,因为后续画路径线也可以直接用原始坐标,界面缩放由 view 控制,数据模型完全不变。
5.3 现象:路径规划结果明显不是最短路径
A 到 B 规划出来绕了远路。这通常不是算法问题,而是图的边没有正确建全。某个捷径的路段缺失,导致 Dijkstra 只能在现有的残缺图上找“局部最优”,你看着是绕路了,算法在图上却没得选。排查方法:先把图的所有边打印成列表,逐个对照地图检查;再写一个连通性检查函数,从起点做一次 DFS,统计能到达的节点数,如果少于 spotCount,说明有孤立岛屿。这类问题在邻接表实现里尤其常见,就是某条边只加了一个方向。
5.4 现象:编译报错找不到 QGraphicsView 或 QMainWindow 头文件
常见错误是No such file or directory,以及unknown type name 'QGraphicsView'。原因基本都是工程文件缺模块,老的 Qt 4 的 pro 里可能只写了QT += core gui,但从 Qt 5 开始 Widgets 从 gui 里拆出来了,必须显式加 widgets。另一个场景是有人手动创建了 .pro 却忘了把头文件放进 HEADERS,导致 mocs 无法生成信号槽代码,表现是链接时报一堆 undefined reference。解决:检查 pro 里是否有QT += widgets,检查自定义类是否加入了Q_OBJECT宏,检查新增加的头文件是否被列进 HEADERS 后执行过 qmake。
5.5 现象:窗口最大化后,地图底图和景点位置错位
现象是最开始显示正常,但把窗口拉大后,背景图填满窗口了,景点小圆点和文字标记却还在原来位置,看起来像地图和标注分离了。原因是对 QGraphicsView 做了 setDragMode 或 resize 处理,但底图加载用的是固定尺寸;或者你在 resizeEvent 里重新设置了 pixmap 的 size,但 spot 坐标没跟着更新。最省心的解决路径是:不设置图片自动缩放,而是让 QGraphicsView 的 sceneRect 始终等于底图的原始矩形,窗口拉大时 view 自动加滚动条或留白,不做任何坐标变换。如果你希望图片随窗口缩放,那就要维护一个统一的 scaleFactor,所有从 JSON 读到的 pos 绘制时都乘以这个系数,并且这个系数只在窗口变化时更新一次。
6. 进阶技巧:把静态地图升级为可缩放的带路书地图
课程设计的核心需求达标后,想拿高分或者让项目真正逼近“能用的产品”,我建议做两件事:多比例尺缩放和路线经过点提示。先说缩放。你已经在 view 层有了一套坐标体系,在此基础上增加 wheelEvent 缩放是非常低成本的增强:每次滚轮缩放,按比例修改 view 的 scale,同时用setTransformationAnchor(QGraphicsView::AnchorUnderMouse)让缩放跟随鼠标位置,就像主流地图软件的手感。这个交互一加,演示效果立刻上了一个档次,因为老师能看到你在地图上任意放大缩小路径,而不仅仅是静态展示。
void MapWidget::wheelEvent(QWheelEvent *event) { double factor = (event->angleDelta().y() > 0) ? 1.15 : 1 / 1.15; scale(factor, factor); }这个最简单版本有个副作用:缩放后原本 4px 宽的路径线也会被放大变粗,放大两级后线粗得盖住整个道路。正确做法是给路径线的 pen 在 wheelEvent 里做反向补偿,即每次缩放后把当前 line item 的 pen 宽度除以 factor,保证屏幕视觉宽度恒定。我用过一个更简单的实现:所有 line 的 QPen 宽度永远等于baseWidth / currentZoom,这样不管怎么缩放,线条看起来都是固定粗细。这就是地图软件里“屏幕恒定线宽”的思路,也是答辩时能讲明白的一个亮点。
路书提示也不复杂:路径展示出来后,在路径线的转折点(也就是每个景点位置)插入一个小箭头标记,指向下一个景点;同时在一侧的信息栏里按顺序列出“从 A 出发 → 经过 B → 到达 C”,每行配一小段景点简介。这样就从“画了一条线”升级成了“拿着路线边走边看介绍”,完整契合标题里的“校园导览与信息查询”。
到这里一个基于 Qt 的校园导游系统就算完整落地了:从 JSON 数据建模、Dijkstra 路径规划、地点模糊查询到路线推荐与地图交互,每层之间都是通过信号槽连接,代码量控制在两三千行以内,逻辑也清晰。回想我最早做这个题目时,最深刻的教训就是死磕界面而没先意识到数据结构才是系统骨架,把图建对了、算法跑通了,界面永远是水到渠成的事。做这类课程设计,别怕把算法和界面分开来写,分开调试,每一步的输入输出都看得见,翻车的概率自然就低。希望帮到你。
本文还有配套的精品资源,点击获取