简介:一份基于MFC的南京地铁查询系统完整工程源码,面向自学C++/MFC的开发者,也适合作为课程设计与毕业设计的参考模板。压缩包共43个文件,仅2.44MB,包含h头文件、cpp源文件、rc资源描述、bmp地铁运营示意图、ico图标以及可直接运行的exe和调试符号文件(pdb/obj等),便于对照源代码逐步调试。工程中通过属性页对话框与地图显示对话框组织界面,涉及CDialog/CPropertySheet、消息映射、控件绑定与GDI位图绘制等典型MFC技术;线路查询的核心逻辑集中在主要源文件中,可以清晰地看到程序初始化、对话框交互与查询结果展示的完整流程。已有958人学习/下载,适合希望快速上手Windows桌面应用开发、并希望基于现成框架二次扩展的读者。 最近帮一个学弟把他的课程设计“基于MFC的南京地铁查询系统”从能跑调到能交,前前后后踩了不少坑,也把MFC那套老掉牙但关键时刻真能顶上的控件和画图机制又摸了一遍。这个题目听起来很简单,就是做个地铁线路查询,但真做起来涉及的东西不少:界面布局、数据库存线路站点、最短路径算法、甚至还有打包发布时那堆运行时库的幺蛾子。这篇就把整个项目的设计思路、核心实现、常见坑位一次讲清楚,给正在做类似MFC课设或者想复习传统Win32桌面开发的朋友一个参考。
先说结论:如果你的课设题目带“XX查询系统”,而且指定用MFC,那么本质就是在考察三件事——C++面向对象的基本功、MFC的控件和消息映射机制、以及最简单的那层数据库增删改查。地铁查询系统的难点不在UI也不在数据库,而在换乘路径的计算。苏州、南京、北京这种地铁成网的城市,站点之间的最短路径和最少换乘是图论里最经典的场景,用Dijkstra或者BFS都能搞定,关键是数据结构怎么设计,以及MFC里怎么把计算结果塞给界面去显示。
1. 整体设计与技术选型
1.1 为什么MFC至今仍是课设和部分工业项目的首选
在开始写代码之前,先想一个问题:为什么这个题目指定MFC?现在已经2025年了,Web前端随便拖个组件库都能做得比MFC好看十倍,Python配PyQt也能快速出界面,但MFC依然活在大量课程设计、老旧系统维护、工业上位机项目里,原因就三个:
第一是历史包袱。很多学校和企业积累了大量的MFC代码库,新项目迁到别的框架成本太高。第二是学起来确实能练到C++底层功底,MFC帮你把Win32 API包了一层,但消息映射、句柄管理、指针生命周期这些概念全得自己扛,对理解Windows程序运行机制帮助很大。第三是发布简单,一个exe加几个DLL就能跑,不像Qt动不动几十MB的依赖。
不过说实话,如果你是自己选型做类似项目,我反倒建议考虑Qt或C#,开发效率高得多。但这个题目既然定了MFC,那就得把MFC的脾性摸透,用最合适的方式实现。
我做这个项目时用的是VS2013 + MFC,数据库选的MySQL 5.7,原因是学弟的课设环境就是这么配的。如果你用的是VS2019或VS2022,需要注意MFC的向导生成代码会有细微差异,比如高DPI支持、字符集默认设置不同,但核心开发逻辑完全一样。
1.2 系统功能模块划分
拿到这个题目别急着双击打开VS拖控件,先把功能模块拆清楚。一个南京地铁查询系统,至少要覆盖以下功能:
- 地铁线路和站点的浏览,比如看到底有哪些线路,每条线经过哪些站
- 站点信息查询,输入站名能查到它属于哪条线、下一站是哪里
- 换乘查询,给定起点站和终点站,输出一条合理的乘车路线(经过哪些站、在哪换乘、总共多少站)
- 数据管理功能(加分项),比如管理员可以增删站点或线路
把这个列出来后你会发现,前两个是纯展示和简单查询,第三个才是核心算法模块。很多人的课设就挂在换乘查询上,因为站点之间不是简单的线性关系,而是网状的图结构。
界面布局我用的是左边的树控件(CTreeCtrl)显示所有线路和站点,右上角是查询条件和按钮,右下角是结果输出的静态文本框或列表控件。这样一层是线路,二层是站点,点任何一个站点都能在文本框显示详细信息,逻辑很清晰。
提示:功能拆分一定要写进课设报告里,这个模块划分图即使画得再丑,老师也会觉得你思路清楚。哪怕最后只实现了部分功能,报告里的模块结构也要完整,这是加分项。
2. 数据库设计与核心算法
2.1 表结构设计:别把关系数据库当文本文件用
数据库是整个系统的数据底座。南京地铁的站点和线路数据不算大,总共也就十几条线两三百个站,完全可以用更轻量的文件存储方式,但课设题目既然写了“系统”,数据库是标配,而且答辩时老师大概率会问表怎么设计的。
我的表结构设计成三张表,规范化程度适中,好查也好写。
第一张是line线路表,字段很简单:
line_id主键,自增line_name线路名,比如“1号线”“S1号线”line_color线路颜色,这个非常重要,因为后面画线路图或做界面展示时要用颜色区分线路
第二张是station站点表:
station_id主键,自增station_name站点名,比如“新街口”“南京南站”interchange_flag是否换乘站,这个字段可以不在数据库里存,通过业务逻辑判断,但存了之后查询会快很多,课设规模无所谓,但报告里写“冗余设计以空间换时间”很加分
第三张是关键的表line_station线路站点关系表,用于描述“某条线路经过哪些站点,顺序是什么”:
id主键自增line_id线路ID,关联line表station_id站点ID,关联station表station_order站点在该线路上的顺序号,从1开始递增
这三张表的结构非常经典,也是真实地铁线路数据库设计的原型。很多同学会想省事,只建一张表把每条线的站点用逗号拼在一起,也能实现功能,但换乘查询和路径计算会变得极其痛苦。数据规范化虽然要多写几条SQL,但后面的代码会简单十倍。
注意:
station_order这个字段是灵魂,没有它你就没法知道线路方向,不知道方向就没法计算“坐几站”。
2.2 换乘查询的核心:BFS和Dijkstra都需要先建图
换乘查询本质上是一个图的最短路径问题。南京地铁的每个站是一个节点,如果两个站属于同一条线而且是相邻站,那它们之间就有一条边,边的权重可以设为1(站数)或者设为换乘时的额外时间惩罚。
对课设来说,我建议用BFS广度优先搜索而不是Dijkstra,原因有两个:第一,如果所有边的权重都是1,BFS就能找到最短路,不需要引入优先队列;第二,BFS的代码量比Dijkstra少得多,而且写起来直观,方便在报告里解释。不过如果你想让系统“看起来更高级”,可以用Dijkstra并给换乘边加权重,比如换乘一次加5分钟,然后求总时间最短路径。这个优化能让答辩老师眼前一亮。
建图的过程是这样的:从数据库加载所有线路和站点,构建两个映射容器。第一个是map<string, int>把站点名映射到节点编号,第二个是vector<vector<int>>或vector<set<int>>表示邻接表。每读取line_station表里的相邻两条记录,就在两个站点之间加一条无向边。
BFS算法的实现我用了标准库的queue。队列初始放入起点站编号,用一个vector<int>数组记录每个节点的前驱节点,这样回溯时就能得到完整路径。如果走到终点就直接跳出,此时前驱数组中记录的就是一条最短路。因为BFS是逐层扩展的,首次到达终点的路径一定是最短路径。
换乘信息怎么提取?路径得到之后,遍历路径上的每个节点,如果某个节点同时属于两条或以上线路,说明这里可以换乘。但这里有个细节:一个换乘站可能连接多条线路,实际坐车时乘客可能不需要换乘也经过这个站,比如新街口是1号线和2号线换乘站,但你从珠江路坐到张府园,也会经过新街口,这时不需要换乘提醒。所以提取换乘点时要看路径中该站之前和之后分别属于哪条线,如果前一段和后一段线路ID不同,才需要换乘。
// 核心函数:根据前驱数组回溯并生成换乘点集合 void GenerateRoute(int startIdx, int endIdx, const vector<int>& parent, const vector<StationInfo>& stations, vector<RoutePath>& paths) { vector<int> route; for (int cur = endIdx; cur != -1; cur = parent[cur]) { route.push_back(cur); } reverse(route.begin(), route.end()); int currentLine = -1; for (size_t i = 0; i < route.size(); i++) { int lineId = stations[route[i]].lineId; if (i == 0 || lineId != currentLine) { // 记录换乘点或者起点 paths.emplace_back(RoutePath{route[i], lineId, currentLine}); currentLine = lineId; } } }上面这段代码是一个简化版本,实际项目中站点信息要更复杂一些,需要从数据库加载每个站点与线路的对应关系。但思路就是这样:先找出路径中的线路切换点,然后在这些位置生成换乘提示。
3. 关键功能的代码实现
3.1 MFC中连接MySQL的正确姿势
MFC连接MySQL的方式有很多种,常见的是用ODBC、MySQL Connector/C++,或者直接用最原始的MySQL C API。课设建议用MySQL C API,因为配置最简单:在VS里把MySQL安装目录下的include和lib路径加进来,链接libmysql.lib,然后include<mysql.h>就能用了。
但这里面有几个坑,第一个就是字符集问题。MFC程序默认用的字符集是Unicode,也就是所有CString内部是宽字符wchar_t,而MySQL的C API接口默认用的是UTF-8或gbk的单字节字符集。如果你直接通过mysql_query()执行中文SQL,从MySQL取回来的中文字段是乱码的。千万别试图用CString直接转char*,因为CString在Unicode编码下转出来的char*根本不是UTF-8。
我最终的解决方案是:所有数据库操作全部走UTF-8编码。在连接数据库后先执行mysql_query(mysql, "SET NAMES UTF8MB4"),然后程序内部所有需要传给MySQL的CString,用CW2A转换宏把宽字符转成UTF-8的char*。从MySQL取回来的是UTF-8编码的字符串,用CA2W转成宽字符给界面显示。这一步搞定了,整个系统的中文显示就再也不用操心了。
// 连接数据库的初始化代码 void CDBManager::Connect() { mysql_init(&m_mysql); if (!mysql_real_connect(&m_mysql, "localhost", "root", "123456", "nanjing_metro", 3306, NULL, CLIENT_MULTI_STATEMENTS)) { AfxMessageBox(_T("数据库连接失败!")); return; } // 统一使用UTF-8编码,避免中文乱码 mysql_query(&m_mysql, "SET NAMES UTF8MB4"); } // CString转UTF-8 CStringA Utf8FromCString(const CString& str) { int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL); char* buf = new char[len]; WideCharToMultiByte(CP_UTF8, 0, str, -1, buf, len, NULL, NULL); CStringA result(buf); delete[] buf; return result; }第二个坑是VS2013的“字符集”设置。MFC工程创建时向导会让你选“使用Unicode字符集”还是“使用多字节字符集”,虽然现在新项目都默认Unicode,但网上下载的旧项目很多是多字节。如果你用的是多字节模式,很多宽字符函数都要改成单字节版本,麻烦得很。建议创建工程时直接选Unicode,然后所有界面字符串都用_T()宏包裹,这样切换字符集时不用改代码。
3.2 列表框控件使用:显示查询结果的最佳选择
MFC里的CListBox(列表框)是显示线路查询结果的利器。你可以用CListBox::AddString()往里面添加字符串,用CListBox::ResetContent()清空已有内容。它比CEdit更适合展示多条数据。
但默认的CListBox只能显示单列,而且不能调整列宽,对地铁系统这种“每条查询结果都是一个包含多字段的字符串”来说足够了,只要你自己拼好格式。比列表控件CListCtrl轻量得多。
我的实现思路是:用户输入起点站和终点站后,点击查询按钮,系统先调算法计算路径,然后把路径上的每个站点都包装成一条字符串,格式类似“第1站: 新街口 (1号线)”,依次添加到CListBox里。如果用户只输入了单个站点名,则查询该站点所在的所有线路信息,每一行显示一条线路及它在线上是第几站。
如果你想让界面更美观,可以用CListCtrl列表视图控件,设置成Report模式,添加三列:序号、站名、所属线路。这样看起来就像一张表格,老师看了也觉得专业。但CListCtrl的操作比CListBox复杂一些,要先设置列头再插入行,而且插入时要手动管理Item的状态,对初学者不太友好。我建议先保证功能,然后在时间充裕的情况下再升级,把CListBox替换成CListCtrl。
3.3 静态文本覆盖问题:自定义控件的起点
你可能在网上看到“MFC静态文本 覆盖”这个热搜词,这说的是一个很经典的问题:MFC的CStatic控件不支持背景透明,如果要在它上面叠加其他控件或者绘制内容,就会出现文字被背景挡住的“覆盖”现象。
地铁查询系统的界面上,我需要在背景图上叠一层文字,显示当前选中的站点信息。直接用CStatic会发现在运行时背景是灰色的,完全破坏视觉。解决办法是把静态文本控件的SS_NOTIFY风格打开,然后重写OnCtlColor,让静态文本的背景色变成和父窗口一样。有些资料说可以通过SetBkMode(TRANSPARENT)让背景透明,实测下来在MFC里直接操作CDC设透明背景,会导致文字叠影、刷新闪烁,体验很差。
实操经验:如果你只是想让某个静态文本和父窗口背景一色,最简单的方式是在
OnCtlColor里处理,而不是OnEraseBkgnd。这个做法的本质是告诉Windows“这个控件的背景我自己画”,而不是用默认的灰色。文字有重影就去掉CStatic的SS_SUNKEN风格,那个风格自带边框和阴影。
4. 界面美化与绘图细节
4.1 自定义按钮:三步实现圆角高光效果
MFC自带的CButton长得很丑,默认风格是上世纪九十年代的灰色方块,和现代审美完全不搭。网上搜“MFC自定义按钮”,实际就三步:继承CButton写一个子类,重写DrawItem虚函数,再给按钮设置BS_OWNERDRAW自绘风格。
第一步,写一个CMyButton类,继承CButton。第二步,在DrawItem里用CDC类绘制按钮的外观,比如画个圆角矩形、填充渐变、根据按钮状态(按下、悬停、禁用)切换颜色。第三步,在对话框模板里或者动态创建时给按钮设置BS_OWNERDRAW标志。
这个自绘按钮实现起来不难,真正难的是画圆角矩形时对CDC::RoundRect的用法不熟悉,导致圆角半径变成了矩形一半变成圆形,或者边框画出来不均匀。我的建议是,用CRect定义一个内缩2像素的矩形,然后调用RoundRect(rect, CPoint(8, 8))固定圆角半径8像素,这样看起来最自然。
自定义按钮做完之后,系统界面瞬间脱离课设感。老师对界面印象分提高,答辩时也可以说“我扩展了MFC的控件体系,实现了自绘按钮”,这是一个非常亮眼的加分点。
4.2 在MFC中画一个彩色正方形:GDI绘图基础
“基于MFC绘制一个彩色正方形”是网上常见的学习示例,它背后的正式内容是GDI(图形设备接口)。地铁查询系统的线路图如果不想用图片素材,可以直接用GDI画出来,动态绘制站点和线路。
实现方式是在对话框的OnPaint函数里获取CPaintDC,利用CDC::MoveTo和CDC::LineTo画线,用CDC::Rectangle或Ellipse画站点圆圈。如果要给正方形填充颜色,用CBrush对象,传入RGB()宏,然后调用CDC::FillRect或SelectObject选入画刷。
如果你想让线路图支持鼠标拖动、放大缩小,就需要在OnMouseMove里处理坐标变换,把逻辑坐标和屏幕坐标做映射。课设阶段做一个静态的、固定坐标的线路图就够了,拖拽缩放是加分的表演项。但我实测下来,如果时间不充裕,不要轻易加拖拽缩放,因为MFC的绘图重绘机制会让你处理大量的消息,而且双缓冲没做好的话界面会闪烁得像霓虹灯。
4.3 双缓冲绘图:告别闪烁的秘诀
画地铁线路图时,如果直接在OnPaint里往屏幕DC上画,窗口一刷新整个图就闪得不行。原因是每次重绘时先擦掉背景再慢慢画新内容,人眼能明显看到这个过程。
解决方法就是双缓冲(Double Buffering):先创建一个和窗口DC兼容的内存DC和位图,把所有图形画到内存DC里,最后一次性BitBlt到屏幕DC。这个操作在GDI编程里是基本素养,在MFC里可以通过CDC memDC; memDC.CreateCompatibleDC(pDC);配合CBitmap实现。
我在地铁系统里用双缓冲画线路图,站点多的时候拖动窗口也完全不闪。这个技术点老师很可能在答辩时问你,公式就是三段:创建内存DC -> 画图 -> 一次性拷贝。
5. 打包发布与常见问题排查
5.1 MFC项目如何打包:既要能跑又要小
课设交完后往往要交一个能直接运行的演示程序,这就涉及到MFC项目打包。MFC程序打包的核心问题就是运行时库。MFC默认是动态链接到mfc140u.dll(VS2015及以后)或mfc120u.dll(VS2013)这类动态库,如果目标机器上没有这些DLL,程序一打开就报0xc000007b或者“缺少mfc120u.dll”。
最简单粗暴的方式是在VS的“项目属性 -> 常规 -> MFC的使用”里选择“在静态库中使用MFC”,再把“运行时库”改成“多线程(/MT)”,这样编译出来的exe不再依赖MFC的DLL和运行时库DLL,体积会从不到1MB膨胀到几十MB,但拿到任何Windows机器上都能跑。这是课设交作业前最省心的发布方式。
如果你选择动态库方式,就要把工程目录下的Debug或Release文件夹里除了exe之外的DLL全部拷贝到目标文件夹,常见的是mfc120u.dll、msvcr120.dll、msvcp120.dll,还有你用的MySQL的libmysql.dll。注意,libmysql.dll不在系统目录,必须和exe放一起。打包发布时,千万别只拷exe,这是最常被坑的地方。
另外还要注意如果你用了MySQL,目标机器上必须装了MySQL客户端或者至少能通过网络访问到MySQL服务器。如果你的数据库是本地服务器且设置了服务自动启动,那演示时记得把MySQL服务提前打开,不然程序启动时数据库连接失败,界面一片空白。
5.2 典型的运行报错和排查方案
在实际开发和答辩演示中,有几个报错几乎是必现的。我把它们总结成一张速查表,每个做MFC+MySQL的人都该收藏。
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 编译时报“无法打开包含文件mysql.h” | 没有正确配置include目录 | 项目属性 -> VC++目录 -> 包含目录,加上MySQL的include路径 |
| 链接时报“无法解析的外部符号mysql_init” | 没有链接libmysql.lib | 项目属性 -> 链接器 -> 输入 -> 附加依赖项,加上libmysql.lib |
| 运行时报“0xc000007b” | 系统缺少运行库或架构不匹配 | 检查exe是x86还是x64,libmysql.dll也要匹配同架构 |
| 界面中文全部乱码 | 字符集编码不匹配 | 连接后执行SET NAMES UTF8MB4,程序内用CW2A/CA2W转换 |
| 数据库连接超时 | MySQL服务未启动或端口不对 | 检查mysql服务状态,确认端口3306可访问 |
注意一个非常隐蔽的坑:如果你的系统是64位的,但MFC工程是x86平台(32位),那么链接libmysql.lib时也必须使用32位的lib文件,也就是MySQL安装目录下的lib\vs12或lib\vs14文件夹中的32位版本。用64位的lib去链接32位程序,链接阶段会报大量错误,这个错误信息很迷惑人,容易让人以为是代码问题。
注意:发布时如果用了MySQL C API,目标机器还必须能访问到
libmysql.dll。就算所有MFC运行库都齐了,缺少这个动态库,程序照样弹窗报“无法启动因为计算机中丢失libmysql.dll”。
5.3 界面卡死的排查:别让数据库操作阻塞UI
MFC的程序是单线程的,消息循环跑在主线程上。如果你的mysql_query查询耗时太长(比如站表数据量极大或者网络连接MySQL服务器延迟高),界面就会卡死,表现为鼠标能移动但点不动任何控件,任务管理器里显示“未响应”。
解决方式是开一个工作线程做数据库查询,查询完成后通过PostMessage发送自定义消息给主线程,然后在主线程的消息处理函数里把查询结果更新到界面。这个模式在MFC里叫“工作者线程+自定义消息”,很多初学者听都没听说过,但在实际项目中是必须掌握的。代码核心就两句话:AfxBeginThread(QueryThreadProc, this)开启线程,PostMessage(WM_QUERY_DONE)通知界面更新。
课设阶段数据量不大,主线程查询延时几乎为零,但如果连的是远程MySQL数据库(比如你在实验室连老师的服务器),网络延时就不可忽视了。我当时帮学弟测试时就发现,连本地数据库秒出,连远程数据库要卡个两三秒,这种体验太差了。
6. 踩坑记录与答辩心得
最后说几个我在实际开发中遇到的、网上教程不会写的细节。
第一个是对话框背景色和控件背景色不一致的心得。MFC对话框的默认背景色是COLOR_BTNFACE浅灰色,但如果你用OnCtlColor改了主对话框的背景色为白色,那么所有按钮和编辑框的默认背景还是会保持灰色,看起来很突兀。处理方式是在OnCtlColor里判断控件类型,对CTLCOLOR_BTN和CTLCOLOR_STATIC单独设置背景为白色,这样整体风格才统一。
第二个是CSplitterWnd切分窗口的用法。如果想做一个类似资源管理器的界面,左边是树形控件、右边是信息显示,可以用CSplitterWnd把主窗口分成左右两栏。但CSplitterWnd和对话框不能直接混合使用,它基于CFrameWnd。所以如果你用的是对话框类CDialogEx,做不了切分窗口,只能用CTreeCtrl固定放在左边然后调整右边控件的位置。这个限制很多初学都没意识到,花了一晚上怎么都拖不进去切分条,查资料才发现是框架类型不对。
第三个是给树控件加右键菜单。站点查询系统里加一个右键菜单,右键点击某个站点可以弹出“查看相邻站”“设为起点”等选项,这个交互细节很加分。实现原理是响应NM_RCLICK通知,获取鼠标位置并调用CTreeCtrl::HitTest判断点中的是哪个项,然后CMenu加载菜单资源并TrackPopupMenu显示。整个过程不难,但需要细心处理坐标转换,因为GetCursorPos获取的是屏幕坐标,树控件里操作需要转换成客户区坐标,否则菜单弹出的位置是乱的。
写到这里,这个项目的核心内容基本覆盖完了。做MFC地铁查询系统这个课设,最大的收获不是数据库有多熟练、算法有多高级,而是把所有零散的知识点串成了一个完整的桌面应用:控件怎么布局、数据库怎么设计、算法怎么和界面交互、打包发布怎么处理依赖。这个过程走一遍,你就不再看不起MFC这种“老古董”了——它虽然老,但该有的框架思想一个都不缺,消息驱动、事件响应、数据与界面分离,这些概念放到今天任何一个GUI框架里都是通用的。
最后再分享一个答辩时的经验:演示时优先展示换乘查询功能,输入一个跨越三四条线的起点终点,让程序转一圈算出来,这时候UI的响应速度、结果的正确性、换乘信息的可读性,就是整个项目最好的门面。然后把数据库连接字符串那里稍微改一改,现场把MySQL服务断掉再查询,弹出友好的“数据库连接失败”提示而不是崩溃,就这一个细节就能让老师觉得你考虑得很周全。
本文还有配套的精品资源,点击获取