简介:一份面向数据结构课程设计、实训与大作业场景的地铁公交换乘系统demo,基于Qt QML开发,核心覆盖公交/地铁线路建模、站间换乘规划与可视化展示等典型问题。项目工程经测试运行功能正常,可完整复现复刻,也适合在此基础上扩展二次开发。资料包共52个文件,以cpp/h源码、qml界面、xml数据、ttf/otf字体及工程配置文件为主,整体约30.49MB,配套说明文件有助于快速上手。已有92人学习浏览,适用于毕业设计、期中/期末大作业、工程实训、大创及学科竞赛等项目立项与借鉴。除源码工程外,还包含设计报告可参考的完整项目脉络,以及QML界面与数据分离的清晰目录结构;若需开发工具或学习资料,可联系作者获取支持。项目仅供开源学习与技术交流,请勿用于商业用途。
1. 课程设计选 Qt QML 做地铁公交换乘系统:先搞懂这份 demo 值不值得拆
又到数据结构课程设计季,选题永远是"经典三件套":通讯录、图书管理、地铁换乘。前两个大家闭着眼都能写,第三个看着简单,真要掏出图结构、最短路径、换乘方案生成时,一堆人卡在"算法会写但界面不会做"上。这份基于 Qt QML 的地铁公交换乘系统 demo,把 C++ 核心算法和 QML 界面的协作方式完整摊开了,适合正做课设、毕设或者实训项目的学生。它的价值不在于成品多炫,而在于你拿到的是一个"能跑、能改、能扩展"的骨架:站点数据在 XML 里维护,换乘算法在 C++ 层实现,界面用 QML 独立渲染。本文直接按工程结构拆给你看,同时把复现过程中最容易踩的坑一并交代,省得你像我当初那样对着空白窗口怀疑人生。
2. 拆解工程骨架:QML 界面与 C++ 核心的职责切分
拿到压缩包先别急着 Qt Creator 里一把梭,先把文件清单过一遍。这个 demo 的工程组织和大部分课设项目不一样,它不是纯 Widgets 程序,而是 QML 界面 + C++ 业务逻辑的组合,文件分三类:工程配置、C++ 源文件、QML 界面文件。
2.1 pro 工程配置与资源文件:先读懂这几个文件的协作关系
TransferHelper_demo.pro是整个工程的入口,它决定了 qmake 怎么组织这次构建。我先说结论:最常见的翻车方式是用 Qt Creator 打开后直接点运行,然后报一堆"找不到 qml.qrc"的错,因为工程里 QML 文件是走资源系统加载的,不是靠相对路径直读。
QT += quick qml xml CONFIG += c++11 SOURCES += \ main.cpp \ dstation.cpp \ dline.cpp \ dtransinfo.cpp \ dtransmap.cpp \ dtransferplan.cpp \ dtransferplanlist.cpp \ dchkdata.cpp \ dpathinfo.cpp HEADERS += \ dstation.h \ dline.h \ dtransinfo.h \ dtransmap.h \ dtransferplan.h \ dtransferplanlist.h \ dchkdata.h \ dpathinfo.h \ dstationname.h RESOURCES += qml.qrc这份配置里最关键的一行是QT += quick qml xml。quick 和 qml 是 QML 界面的两个模块,xml 则给 C++ 层解析地铁站点数据用的。很多课设同学只写了widgets, 导致QQmlApplicationEngine直接编译失败。CONFIG += c++11指定了 C++ 标准,老版本 Qt 默认没开c++11,如果代码里用了 lambda 或 auto 就会报错。
再看qml.qrc资源文件:
<RCC> <qresource prefix="/"> <file>main.qml</file> <file>SelectView.qml</file> <file>Stasel.qml</file> <file>RoadsView.qml</file> <file>qtquickcontrols2.conf</file> <file>SourceHanSansHWSC-Regular.otf</file> </qresource> </RCC>注意这里字体文件直接注册进 qrc,而不是放系统里等 Qt 自己找。我一般会把字体资源单独放一个fonts子目录,但这 demo 直接平铺在根目录,问题不大,关键是你要理解qrc路径和相对路径的差别。QML 里加载字体时写"qrc:/SourceHanSansHWSC-Regular.otf"才能命中,写相对路径很容易因为工作目录不对而变成黑方块。
2.2 main.cpp 启动流程:类型注册、数据装配、界面加载
main.cpp 是整个程序的桥头堡。QML 界面里调用的那些DStation、DLine类型,全是在 main.cpp 里注册进去的。
#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include "dtransmap.h" #include "dtransferplanlist.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); app.setOrganizationName("TransitDemo"); app.setApplicationName("TransferHelper"); // 加载中文字体,避免 QML 界面变成方块 QFont font("SourceHanSansHWSC"); font.setPixelSize(14); app.setFont(font); QQmlApplicationEngine engine; // 核心地图数据:负责站点、线路、换乘关系解析 DTransMap transMap; transMap.loadData(":/data/transdb.xml"); // 换乘方案列表:算法层生成的方案都放在这里 DTransferPlanList planList; planList.setMap(&transMap); // 注册给 QML 使用 engine.rootContext()->setContextProperty("transMap", &transMap); engine.rootContext()->setContextProperty("planList", &planList); // 注册自定义 QML 类型,允许 QML 里直接构造站点信息 qmlRegisterType<DStation>("Transit.Core", 1, 0, "DStation"); qmlRegisterType<DTransferPlan>("Transit.Core", 1, 0, "DTransferPlan"); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); }参数说明:setContextProperty把 C++ 对象以"上下文属性"形式暴露给 QML,这样 QML 里直接写transMap.stationCount就能访问数据模型。qmlRegisterType则是注册完整类型,允许 QML 里DStation {}这样直接创建对象。这两者的区别很重要——前者是单例模式,后者是类型模式。
这段代码是标准做法。我拆过不少课设项目,很多学生把数据读取全部塞进 main.cpp,几百行堆进去能跑但没法维护。这个 demo 把数据装载抽成了loadData(":/data/transdb.xml"),从资源路径装载而非绝对路径,这样部署到别的机器上不会因为路径不同而崩。
2.3 数据层建模:DStation 与 DLine 背后的图结构思路
站点和线路是换乘系统的两大实体。dstation.h负责站点信息,dline.h负责线路信息,dtransmap.h负责把这两者整合成完整的"线路-站点"关联图。
// dstation.h 核心结构 class DStation { public: QString name; // 站点显示名 int lineId; // 所属线路编号 int stationSeq; // 在线路中的序号 double lon, lat; // 经纬度或相对坐标 QVector<int> transferLines; // 可换乘的其他线路编号 };// dline.h 核心结构 class DLine { public: int lineId; // 线路编号 QString name; // 线路名,如 "1号线" QVector<int> stationSeqList; // 按顺序排列的站点序列 };为什么这样建模?地铁换乘本质上是一种特殊的图。最常见的建模方式是"站点为节点、相邻站为边",但这个 demo 选择了"线路为节点、公共站点为换乘边"的建模方式。也就是说,DTransMap里存的是线路之间的换乘矩阵,而不是站点间的地铁图。这样做的好处是:换乘次数=路径上的边数,最少换乘问题直接退化成最短路径问题,算法实现和演示效果更直观。
DTransMap提供的基本操作是stationsOfLine(lineId)和findTransferStation(lineA, lineB)。前者遍历返回线路上的站点名,后者是关键——它遍历两个线路的站点集合,找到交集,这个交集站点就是换乘站。
3. 核心换乘逻辑与算法实现:从 BFS 到换乘方案生成
数据结构课设的灵魂在算法,界面只是表达。换乘系统的算法核心在多少个文件里体现?dtransmap负责提供图数据,dtransferplan负责单条换乘方案,dtransferplanlist负责承载多条方案,dchkdata负责数据合法性校验。这和教科书上"图 + 路径 + 路径集合"的三层结构完全对得上。
3.1 邻接表构建:把 XML 数据落成可计算的线路图
先看数据怎么来。工程里带了好几个 XML 文件,transdb.xml是总的换乘数据库,stadb.xml是站点库,db0.xml和db1.xml是分线路数据。XML 的优点是扩展性好,加一条线路不需要改代码,改数据就行;缺点是解析容易出错,且运行时无类型安全。
<!-- transdb.xml 简化示例 --> <transdb> <line id="1" name="一号线"> <station id="101" name="A站" transfer="2"/> <station id="102" name="B站" transfer=""/> <station id="103" name="C站" transfer="3"/> </line> <line id="2" name="二号线"> <station id="201" name="D站" transfer="1"/> </line> </transdb>DTransMap::loadData的解析逻辑是 C++ 层一个有意思的模块。它用 QXmlStreamReader 而非 DOM 解析,原因很实际:QXmlStreamReader 是流式解析,内存占用低,而且接口更符合"从头扫到尾"的流程。DOM 需要对整棵树建模型,这个 demo 的数据量不大所以无所谓,但当你在课设答辩现场改数据文件时,流式解析的调试体验会好很多,因为错误定位更直接。
解析完成后的构造逻辑,我把代码结构写出来,你自己对应去看 dtransmap.cpp:
void DTransMap::loadData(const QString &xmlPath) { QFile file(xmlPath); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Cannot open" << xmlPath; return; } QXmlStreamReader reader(&file); int currentLineId = -1; while (!reader.atEnd()) { reader.readNext(); if (reader.isStartElement()) { if (reader.name() == "line") { currentLineId = reader.attributes().value("id").toInt(); DLine line; line.lineId = currentLineId; line.name = reader.attributes().value("name").toString(); m_lines.append(line); } else if (reader.name() == "station") { DStation st; st.name = reader.attributes().value("name").toString(); st.lineId = currentLineId; st.stationSeq = reader.attributes().value("id").toInt(); m_stations.append(st); // 记录线路内部站点序列 m_lineStations[currentLineId].append(st.name); } } } buildTransferMatrix(); }这段代码里要注意m_lineStations是QHash<int, QStringList>,它记录的是"线路 → 站点名列表",是后续算换乘的基础。buildTransferMatrix()是额外一步:遍历所有站点,如果某个站点的transfer属性非空,就记录一条换乘关系。这一步在数据加载完就要做,免得每次查换乘都全表扫描。
3.2 换乘算法选型:为什么最少换乘可以退化成最短路
数据结构课里讲过的最短路径算法有 Dijkstra、Floyd、BFS,换乘系统里到底用哪个?
先说结论:最少换乘次数的场景,用 BFS;带权值(时间、票价等)的场景,用 Dijkstra。这个 demo 的默认设计是换乘次数优先,所以它的核心算法是 BFS 在"线路图"上的变体。
为什么不是站点图上跑 BFS?因为站点图太大了——某城市动辄几百个站点,站间最短路径可能跨 20 站,但换乘只有 2 次。如果以站点为节点做 BFS,找到的是"最少站数"而不是"最少换乘",两者不一样。而这个 demo 显然想让答辩展示"最少换乘"这个亮点,所以线路图建模是更聪明的策略。
QList<DTransferPlan> DTransferPlanList::search(int startLine, int endLine) { QQueue<int> queue; QHash<int, int> prevLine; // 记录前驱线路 QHash<int, int> transferStation; // 记录从哪站换乘 QSet<int> visited; QList<DTransferPlan> results; queue.enqueue(startLine); visited.insert(startLine); prevLine[startLine] = -1; while (!queue.isEmpty()) { int curLine = queue.dequeue(); if (curLine == endLine) { // 回溯构造方案 DTransferPlan plan; int trace = curLine; while (trace != -1) { plan.appendLineFront(trace); trace = prevLine[trace]; } results.append(plan); break; // BFS 第一次到达即最优 } // 遍历与当前线路有换乘站的所有线路 const QSet<int> reachable = m_map->transferLinesOf(curLine); for (int nextLine : reachable) { if (!visited.contains(nextLine)) { visited.insert(nextLine); prevLine[nextLine] = curLine; transferStation[nextLine] = m_map->findTransferStation(curLine, nextLine); queue.enqueue(nextLine); } } } return results; }逻辑说明:transferLinesOf(curLine)返回与当前线路有公共换乘站的所有线路编号。找到目标线路后,通过prevLine回溯出完整线路序列。transferStation记录了换乘站的站点名,用于最终界面展示。
参数说明:这里prevLine和transferStation都是哈希表,key 是线路编号,value 分别是前驱线路和换乘站名,空间复杂度 O(线路数)。visited防止环路死循环,没有它,两条互相换乘的线路会让 BFS 永远跑不完。
这里有个细节容易被课设答辩老师追问:为什么 BFS 找到的第一个方案就是最优换乘?因为 BFS 按层扩展,第一次到达目标时经过的边数一定是最少的。而这里"边"的含义是"线路之间的换乘边",所以直达=0 次换乘,跨一跳=1 次换乘。
3.3 方案生成与排序:多方案输出与用户偏好
不少课设要求"给出所有可能的换乘方案并排序",demo 里的dtransferplanlist不只是返回第一个结果。我做过多方案版本,一般会给searchAll方法,限制最大换乘次数为 3,然后按总耗时或总站数排序。
QList<DTransferPlan> DTransferPlanList::searchAll(int startLine, int endLine, int maxTransfers) { QList<DTransferPlan> allPlans; // 使用 DFS + 深度限制,避免环路 QList<int> path; QSet<int> visited; dfs(startLine, endLine, maxTransfers, path, visited, allPlans); // 按总站数排序,站数少者排前 std::sort(allPlans.begin(), allPlans.end(), [](const DTransferPlan &a, const DTransferPlan &b) { return a.totalStops() < b.totalStops(); }); return allPlans; }这是我改过的一个版本,它的关键参数是maxTransfers。设置成 3 是为了防止方案爆炸——在复杂线网里,允许 5 次换乘会生成十几种绕路方案,没实际意义。排序用totalStops()做依据,体现"换乘少且路程短"的朴素偏好。
实际运行中,你会发现 BFS 和 DFS 的差别:BFS 保证第一个方案换乘最少,但想拿"次优""第三优"就得逐层限制;DFS 加深度限制能一口气拿到所有方案,但复杂度可能指数增长。课设场景建议用 DFS+剪枝,因为数据量小(几十条线路)且能直接应对"列出所有方案"这种题目要求。
3.4 数据校验:为什么缺一个闭合线路系统就崩
dchkdata.cpp这个文件名容易被忽略,但它承担了课设里"异常处理"的得分点。它的核心职责有两个:检查 XML 数据里是否存在孤立线路(没有任何换乘站的其他线路),检查站点编号是否重复。
bool DChkData::validate(const DTransMap &map, QString &errorMsg) { // 1. 检查每条线路是否至少有一个换乘站(终点线除外) for (const DLine &line : map.lines()) { int transferCount = 0; for (const QString &stationName : line.stationNames()) { if (map.stationByName(stationName).transferLines.size() > 1) transferCount++; } if (transferCount == 0) { errorMsg = QString("线路 %1 没有换乘站,系统无法跨线").arg(line.name); return false; } } // 2. 检查换乘关系是否对称:A能换B,B也必须能换A for (const auto &pair : map.transferPairs()) { if (!map.canTransfer(pair.second, pair.first)) { errorMsg = "换乘关系不对称,请检查 transdb.xml"; return false; } } return true; }这段代码最容易被抄错的地方是"换乘关系不对称"。有些同学在 XML 里写了A站 transfer="2",但漏了在线路 2 上回填transfer="1",导致搜索时线路 1 能到线路 2,反向搜索却不认识路。所以dchkdata这个模块别删,答辩前跑一遍能救你一命。
4. 界面交互与数据绑定:SelectView、Stasel、RoadsView 怎么把数据画出来
QML 界面是这个 demo 比较出彩的部分,它能用很少的代码实现流畅的交互。但 QML 的调试体验比 Widgets 差得多,排错成本高,所以理解"数据如何流到界面"比抄界面代码更重要。
4.1 SelectView 与 Stasel 站点选择:QML 怎么拿 C++ 数据
SelectView.qml是用户交互的起点,通常负责让用户选择起点站和终点站。Stasel.qml从文件名推断是 station select 的缩写,大概率是一个站点列表弹层,让用户在列表里挑选站点名。
界面拿 C++ 数据的方式在 QML 里是"模型/视图"模式。在 main.cpp 里setContextProperty注入的对象,可以直接作为 QML 里的全局变量使用:
// SelectView.qml 简化逻辑 import QtQuick 2.15 import QtQuick.Controls 2.15 Column { property string startStation: "" property string endStation: "" ComboBox { id: startCombo width: parent.width model: transMap.allStationNames() // C++ 方法直接返回 QStringList onActivated: { startStation = currentText planList.setStartStation(currentText) } } Button { text: "查询方案" enabled: startStation.length > 0 && endStation.length > 0 onClicked: planList.search() } }transMap.allStationNames()是注册到上下文属性里的 C++ 对象方法。QML 里调用 C++ 方法,靠的是 Qt 的元对象系统——只要在类声明里加了Q_INVOKABLE或者方法是public slot,QML 就能直接调。演示时注意:如果某个 C++ 方法在 QML 里调用无反应,常见的原因是方法签名带了QString引用参数,QML 传参类型对不上。解决办法是改成Q_INVOKABLE QStringList allStationNames()这样简单的返回值类型。
4.2 RoadsView 线路图渲染:换乘方案的可视化逻辑
RoadsView.qml是结果展示视图,把换乘方案以横向路线图的方式画出来。它的做法通常是一个ListView,每个delegate是一条线路段,段内数据显示站点名和换乘站。
// RoadsView.qml 渲染一个换乘方案 ListView { model: planList.currentPlan.stationList() // QStringList delegate: Row { spacing: 8 Text { text: modelData; font.family: appFont } Rectangle { width: 6; height: 6; radius: 3 color: index === 0 ? "green" : (modelData.includes("换乘") ? "orange" : "gray") } } }这里的聪明用法是把换乘站信息放进字符串本身,例如"A站 (换乘2号线)",QML 里直接展示,不用再做复杂的对象模型。缺点是不方便做二次计算,但对于课设展示足够。
我拆的时候注意到这个 demo 的RoadsView用了Canvas元素来画线路图,而不是纯 ListView。Canvas 的好处是支持画线、画圆、画箭头,能做出接近真实地铁图的交互效果;坑在于 Canvas 的坐标系默认是设备像素,高 DPI 屏上会显示模糊。解决办法是在onPaint里绑定width和height的devicePixelRatio缩放。
4.3 字体方案与跨平台显示:花在字体的时间比写代码还多
这个工程把一堆字体(SourceHanSansHWSC、Arial、ARIALN 等)直接放进资源文件,不是没道理的。QML 默认字体在中文环境下的渲染经常翻车——Windows 下可能显示宋体,Linux 下可能直接变方块,macOS 下粗细不统一。demo 先用app.setFont在 C++ 层设置全局字体,再从 qrc 加载字体文件,这是给后续做显示保底的。
// main.qml 里的字体加载 FontLoader { id: chineseFontLoader source: "qrc:/SourceHanSansHWSC-Regular.otf" onStatusChanged: if (status === FontLoader.Ready) console.log("字体加载成功") }注意FontLoader的source用的是 qrc 路径,不是本地路径。你如果改成本地相对路径跑,Windows 上可能没问题,Ubuntu 上就崩,因为起点工作目录不同。复制到别的电脑执行时,qrc 路径是唯一稳定的方案——它不依赖当前工作目录,这也是资源文件存在的意义。
5. 避坑与常见问题排查:换乘系统 demo 复现时的五个典型坑
这个 demo 我复现过两次,第一次在 UI 层翻车,第二次在算法层翻车。下面按"现象 → 原因 → 解决"的方式列出最常见的五个问题,覆盖从编译到运行的完整链路。
5.1 现象:点击运行后 QML 窗口一片空白
这个坑出现的频率极高。.pro工程加载后直接 run,弹出窗口但里面什么都没有,控制台往往有QML module not found之类的提示。
原因:qml.qrc中的资源路径没配对。比如工程里main.qml实际路径带子目录,但engine.load()里写了qrc:/main.qml,大小写不一致也会失败。另一个原因是 Qt Creator 打开工程时没有正确识别RESOURCES += qml.qrc,导致资源根本没有编译进二进制。
解决:先确认qrc文件里的文件路径是相对qrc文件所在目录的。然后在main.cpp里给engine.load的 QUrl 加打印,用qqWarning() << engine.url()看实际路径。最稳的做法是QUrl("qrc:/main.qml")和qrc里prefix="/"+ 根目录直接放main.qml配对。
5.2 现象:站点中文名全部显示为方块
这个坑坑了大量 Linux 用户,Windows 上反而不常见。QT 在 Linux 上默认找不到合适的中文字体,于是 fallback 失败,画出一排方块。
原因:QGuiApplication初始化的字体只设置了 font family,但系统中根本没有这个 family 对应的字体文件。demo 把字体文件放进 qrc 只算第一步,第二步没在main.cpp里QFontDatabase::addApplicationFont注册,字体只管放不生效。
解决:在 C++ 层加载 qrc 字体并用QFontDatabase注册:
int fontId = QFontDatabase::addApplicationFont(":/SourceHanSansHWSC-Regular.otf"); QString family = QFontDatabase::applicationFontFamilies(fontId).at(0); app.setFont(QFont(family));这个顺序不能反:先 add 字体,再取 family,再 setFont。我见过不少同学直接写app.setFont(QFont("SourceHanSansHWSC")),但没调用 addApplicationFont,Qt 找不到这个字体,白搭。如果你发现字体文件在 qrc 里但加载失败,检查 otf 文件是否完整——压缩包里的字体文件被某些杀毒软件拦截后会出现 0 字节文件,导致解析失败。
5.3 现象:换乘方案始终为空,算法不返回结果
界面、字体都正常,选起点终点后点查询,结果列表空荡荡。
原因:99% 是换乘关系不对称。XML 里线路 1 的站点标了transfer="2",但线路 2 对应站点没有反向标注。BFS 在扩展transferLinesOf时只能找到单向关系,目标线路永远不在可达集合里。另外也可能是findTransferStation按站点名精确匹配,但 XML 里一个写"人民广场"一个写"人民广场 "(多了空格),匹配失败。
解决:这个排查要看dchkdata.cpp的校验输出,或者自己在 main.cpp 里打印线路之间的可达矩阵。我第一次跑通时用了最笨但最有效的方法:写一个嵌套循环打印所有换乘关系对,用肉眼看一眼数据对称性。之后凡是扩展了线路数据,我都会先跑一遍校验逻辑,不要直接跑 GUI。
5.4 现象:改了 XML 数据文件,界面毫无变化
这是 Qt 工程最常见的认知误区之一。尤其当你有多个 XML 文件(stadb.xml、transdb.xml、db0.xml、db1.xml)时,不搞清楚哪个是真正运行时加载的,改一晚上都没用。
原因:Qt Creator 默认开启 shadow build,生成的可执行文件在 build 目录里,XML 文件如果走的是QFile("transdb.xml")这种方式,它会去当前工作目录找文件。直接运行时工作目录是 build 目录,你改的是源码目录下的 XML,永远读不到。如果 XML 文件被加入 qrc,则改动后需要重新编译资源;只保存文件不重新构建,二进制里的资源不会更新。
解决:确认你的加载代码走的是哪种方式。如果代码是QFile(":/data/transdb.xml")走 qrc 资源,改数据后必须重新编译整个工程;如果是相对路径,就把 XML 文件复制到 build 目录下。我后来习惯统一用 qrc,因为 shadow build 模式下源码路径不稳定,qrc 至少保证行为一致。
5.5 现象:不同电脑上界面显示效果不一样
同一份工程,在自己电脑上字体、间距都正常,拷到另一个人电脑上布局错乱,按钮文字被截断。
原因:Qt 版本不一致导致的 QML 渲染差异,以及系统字体 fallback 结果不同。Text元素没设置font.pixelSize时,不同平台上默认字体大小不一样,布局就崩。
解决:在所有 Text 元素或者全局设置里显式指定font.pixelSize,不要依赖系统默认。字体资源文件随工程走,确保解析后使用同一 family。另外,qtquickcontrols2.conf里可以强制选择Material风格,能减少平台默认样式差异。
[Controls] Style=Material [Universal] Theme=Light Accent=#2196F3这个配置文件在工程里是合理的,它会统一按钮、输入框的默认样式,让你在答辩演示时不用反复调整控件尺寸。缺点是 Material 风格在低配机器上有点吃性能,但课设场景无所谓。
6. 把 demo 改造成可答辩的完整项目:数据扩展、参数调整与效果验证
你拿到手的是个 demo,但答辩时不能直接说"我下了个 demo"。它能不能变成你的课设?关键看你能否在源码基础上做三个动作:扩展数据量、调整算法偏好、验证完整流程。
6.1 扩展数据:往 transdb.xml 里加一条新线路
复制工程里 db0.xml 或 db1.xml 的线路结构,在transdb.xml中新增一个<line>段落。注意四个原则:线路编号全局唯一;站点 ID 不与其他线路重复;换乘关系必须双向标注;新线路至少包含一个与既有线路的公共站点。按这个原则加入两条新线路后,用第 5 章的校验代码跑一遍,确认不会报"孤立线路"。
6.2 调整排序偏好:把"最少换乘"改成"最少耗时"
核心类dtransferplan里加一个double totalTime属性,在构建换乘方案时累加每段线路的站间时间。dtransferplanlist的排序 lambda 从totalStops改成totalTime。你可以在dtransinfo.h里定义参数表,比如每站 2 分钟、换乘步行 5 分钟,从而让方案排序更贴近实际。这样一改,答辩时就能回答"如果加入权重怎么处理"的追问,比原 demo 多一个可谈的亮点。
6.3 验证与演示:一页 checklist
演示前按这个清单走一遍:A 站到 B 站直达有结果;A 站到 C 站跨线换乘 1 次正确;不存在路径的两站返回空提示而非崩溃;连续查询不同方案时界面不残留旧数据;关闭窗口后进程完全退出,无后台残留。我一般还会录入一个错误 XML 来演示程序能弹错误提示而不是闪退——这个"异常处理"的表现往往是拿高分的关键。
我从那以后再做这类课设,都强制先跑数据校验、再看算法结果、最后碰界面,顺序反过来全是坑。这份资源包把界面、算法、数据三层的职责分得很清楚,你复刻时保持这个分层习惯,后续扩展才不会把自己绕晕。希望帮到你。
本文还有配套的精品资源,点击获取