
简介基于QT框架的医院排队叫号系统完整工程来自同济大学20级计科数据结构课程设计。项目面向需要完成同类课程设计、期末大作业或工程实训的高校学生也适合作为学习Qt界面编程与队列数据结构的练手实例。包内共30个文件压缩后约6MB核心源码包含6个cpp实现、5个头文件及界面ui、工程pro、qrc资源文件等覆盖业务逻辑与构建配置另有实验报告PDF和README说明文档便于对照原理与运行步骤。目前已有132人学习浏览说明方案具备一定参考价值。经过测试可正常编译运行拿到后可快速复现系统将患者排队、医生叫号与界面展示分层实现代码结构清晰既可直接用于课程设计提交也能进一步扩展分诊队列、统计报表等功能。 看到这个标题我第一反应是经典选题但想拿高分真不容易。医院排队叫号系统放在数据结构课程设计里几乎是天生为了展示“队列”这个数据结构而存在的题目。但很多同学做出来要么只是套了个界面的数组遍历要么把QT当成画板用数据结构和业务逻辑完全脱节。这篇就按我当时做这类项目的思路从题目分析、数据结构选型、QT实现到踩坑记录完整拆一遍给正在做课设的同学一个可以直接参考的路线。1. 项目定位与核心设计思路1.1 从课程设计角度读懂这个选题医院排队叫号系统是一个典型的生产者-消费者模型病人是生产者不断进入队列医生是消费者不断从队列取出患者。这个题目妙就妙在它把数据结构教材里最核心的几种结构都自然地映射到了业务场景里——队列对应候诊排队优先队列对应急诊插队栈对应医生“返回上一个患者”的操作哈希表对应按诊室快速查找队列。如果做项目的时候能把这些结构真正用上而不是只写一个循环遍历数组那课设答辩的时候基本就稳了。另外要看清一个现实课程设计的评分重点不是界面多华丽而是“数据结构知识点是否落实到位”。评分老师拿到报告第一眼看的是你用了哪些数据结构、算法复杂度分析是否合理、为什么选它不选别的。所以从开始设计就要把“每个模块背后的数据结构”想清楚UI只是承载这些结构的外壳。1.2 系统模块划分与信息流转整体上这类系统可以拆成三个端分诊台取号端、医生叫号端、候诊大屏展示端。三个端看起来是三个界面底层共享同一套队列数据。分诊台取号端的工作是患者选择科室或医生系统生成一个排队号并推入对应的队列同时把患者信息写入存储。医生叫号端的工作是医生点击“下一个”系统从对应队列弹出队首患者状态改为“就诊中”并进行语音播报。候诊大屏展示端则是将多个队列的当前叫号状态滚动展示出来。数据流向是单向的取号端入队叫号端出队大屏端只读。理解了这条主线后面实现起来就不会乱。2. 核心数据结构的选型与实现2.1 患者信息怎么组织更合理患者节点是整个系统的数据基础。我给这类项目设计患者结构体时通常会包含这几个字段排队号、姓名、优先级、所属窗口号、当前状态。状态字段非常关键它标记着这个患者是正在等待、正在被呼叫、已经入诊还是过号未到。struct Patient { int number; // 排队号 QString name; // 姓名 int priority; // 优先级0普通1急诊 int windowId; // 所属窗口/诊室编号 int state; // 0等待 1叫号中 2就诊中 3已完成 4过号 };关于字段设计我想多说一点。很多初学者喜欢用QStringList或者QMap散装存数据但正规的数据结构课设应该体现出“结构体组织数据”的意识。把Patient定义成结构体后续所有队列操作都基于这个结构体来写代码会清晰很多。优先级字段是留给急诊用的后面会讲怎么用优先队列处理。2.2 队列实现用链表手写还是直接用容器这是课设答辩最容易被抓着问的问题。直接用QQueue或者std::queue确实也能跑但老师问一句“队列的入队出队复杂度是多少底层是怎么实现的”你就容易卡壳。我的建议是队列核心逻辑用手写链表实现既能在报告里展示完整的结构体定义和指针操作又能在复杂度分析里写出漂亮的结论。struct QueueNode { Patient data; QueueNode *next; explicit QueueNode(const Patient p) : data(p), next(nullptr) {} }; class ListQueue { private: QueueNode *head; QueueNode *tail; int m_size; public: ListQueue() : head(nullptr), tail(nullptr), m_size(0) {} ~ListQueue(); void enqueue(const Patient p); // 队尾入队 O(1) Patient dequeue(); // 队首出队 O(1) Patient front(); // 取队首元素 O(1) bool isEmpty() const { return m_size 0; } int size() const { return m_size; } };入队操作维护尾指针指向新节点出队操作移动头指针并释放原节点复杂度都是O(1)这是链表队列的核心优势。相比顺序队列它不需要预分配固定长度也不会出现“假溢出”问题。这里可以用现实场景类比候诊队伍是动态增长的上午人少下午人多链表不需要提前规划容量来多少接多少这一点刚好契合门诊流量不稳定的特点。2.3 多队列结构每个科室一条队伍医院场景跟单队列场景最大的区别是不同科室之间不共用一个队列。所以系统里需要维护多组队列每组对应一个科室或一个医生。这里推荐用QHash做索引键是科室编号值是对应科室的ListQueue指针。取号时根据患者选中的科室找到对应的队列对象执行push操作。这个设计的核心意义是查找科室对应队列的时间复杂度是O(1)不需要遍历所有队列。QHashint, ListQueue* m_queueMap; // key: windowId, value: queue初始化阶段直接为每个窗口创建独立的ListQueuefor (int i 1; i 5; i) { m_queueMap[i] new ListQueue(); }平时不觉得这有什么但当系统扩展到几十个科室时这个结构对比线性查找的优势就非常明显了。2.4 过号和急诊队列的“反常规”操作过号处理是我当初踩坑最多的地方。最直观的设计是“过号就出队作废”但这在真实医院行不通——患者走开一下回来你让TA重新排到最后面容易引发纠纷。合理的策略是过号患者不真正出队而是标记为过号状态state 4等当前队列空闲时医生可以手动把过号患者“回诊插入”到队首。在链表队列里插入队首是一个很有技术含量的操作也是答辩时一个很好的加分点void ListQueue::insertFront(const Patient p) { auto *node new QueueNode(p); node-next head; head node; if (tail nullptr) { tail head; } m_size; }急诊优先则有两种做法。课程设计级别推荐用优先队列最小堆priority值越小优先级越高每次取队列头部时先比较优先级。如果用堆实现插入和取出的复杂度都是O(log n)比普通的入队O(1)多了一点开销但换来了“急诊永远排最前面”的业务保障。// 基于最小堆思想priority越小优先级越高 void pushWithPriority(QVectorPatient* heap, Patient* p); Patient* popWithPriority(QVectorPatient* heap);我强烈建议在报告里展示这个堆变化的示意图因为这是整个项目里最容易拉开分数差距的地方。当时和我一起答辩的同学里绝大多数只做了单一队列能讲清楚“多队列优先队列过号重插”的组合几乎一定进优。3. QT界面设计与核心业务流程实现3.1 界面整体布局与组件选择三个端口的界面我推荐用QStackedWidget做一个总入口分诊台、医生端、大屏端各占一页。顶栏放一个窗口切换下拉框方便演示的时候快速跳转。这样结构清晰也便于答辩时展示。分诊台界面的核心组件是科室选择区QComboBox选科室QLineEdit输入姓名和取号按钮QPushButton。取号按钮按下后通过信号槽把这个动作连接到主窗口的槽函数在槽函数里完成“生成排队号并推入对应队列”的操作。医生端界面则是一块QListWidget显示当前队列一个“下一个”按钮以及当前呼叫患者的文字和语音提示。候诊大屏端用QTableWidget或QListWidget对每个科室叫号状态做滚动轮播当前被叫到的行用背景色高亮。3.2 取号流程的完整实现链路取号按钮的点击事件是整个系统的主链路代码逻辑不长但每一步都有讲究void MainWindow::onTakeNumberClicked() { int windowId ui-comboWindow-currentData().toInt(); Patient p; p.number m_nextNumber; p.name ui-lineName-text(); p.priority 0; p.windowId windowId; p.state 0; ListQueue *q m_queueMap.value(windowId); if (!q) return; q-enqueue(p); m_patientMap.insert(p.number, p); refreshDoctorView(); appendHistory(QString(取号成功%1 号请到 %2 号诊室候诊) .arg(p.number).arg(windowId)); }取号成功后除了刷新医生端列表还有一个容易被忽略的动作把取号记录写入历史日志。我用的方案是QFile以追加形式写CSV文件每次取号一行附带时间戳。这个操作在课设答辩中非常加分因为老师可以清楚地看到整个系统的数据是完整的不是只存在内存里一关就没。存储部分还有一个细节值得提不要每次写文件都打开关闭容易因为异常中断破坏文件。用QSaveFile的commit机制或者攒一批再写都会安全很多。3.3 叫号流程与状态流转医生端点击“下一个”时系统需要做的事情按顺序来从当前窗口的ListQueue中dequeue队首患者将该患者状态从“等待”改为“叫号中”更新候诊大屏的当前叫号行触发语音播报“请3号张三到2号诊室就诊”如果启动过号回诊插入还要检查队列里有没有state4的过号患者有则顺带提示医生。这里最核心的就是状态机流转我习惯把状态定义成枚举来写enum PatientState { Waiting 0, Calling 1, InDiagnosis 2, Done 3, Overdue 4 };状态流转关系是Waiting→Calling→InDiagnosis→Done中间Calling超时无人应答则转OverdueOverdue在医生操作下可以转回Calling。这个流转图是答辩时的高频问题建议在报告里画出来。语音播报我用了QT自带的QTextToSpeech模块三行代码就能用起来QTextToSpeech *speech new QTextToSpeech(this); speech-say(请3号张三到2号诊室就诊);如果环境里没有语音库也可以退化成弹窗提示核心逻辑不受影响。3.4 候诊大屏的轮播刷新策略候诊大屏端最容易“一刷新就闪屏”。因为QTableWidget频繁clear()然后重新addItem会肉眼可见地闪烁。解决方法是只更新数据变化的那一行用setItem替换对应位置的文本而不是整表重建。另外大屏要轮播多个窗口的状态我用一个QTimer定时扫描m_queueMap里所有队列的队首信息每2秒刷新一次展示。这里有个经验定时器里不要做太多逻辑只做“读取显示”所有变更操作都放到取号端和叫号端的槽函数里去完成。这样数据竞争和UI卡顿的概率会低很多。4. 常见问题与调试经验实录4.1 跨线程访问UI导致崩溃如果你实现的是“多个医生同时对多个队列操作”大概率会引入多线程。而QT最经典的崩溃问题就是在子线程里直接操作UI组件比如在线程函数里调用ui-listWidget-addItem()。这个问题不只是崩溃这么简单有时候是界面不刷新有时候是偶发闪退排查起来特别费劲。正确做法是通过信号槽跨线程通信。在子线程中发射信号主线程接收信号后再操作UI。连接时要注意第五个参数connect(worker, DoctorWorker::patientFinished, this, MainWindow::onPatientFinished, Qt::QueuedConnection);我当初因为偷懒省掉这个参数程序长时间运行后随机崩溃查了整整一个晚上。这里直接给结论凡是子线程要改界面一律用信号把数据发回主线程主线程的槽函数专门负责刷新。4.2 过号操作时迭代器失效过号回诊插入队首的方案在链表实现里其实还好但如果你改用QList或QVector存队列数据问题就来了。遍历列表时erase掉元素迭代器会失效再往下走就崩。当时有个同学的做法就是过号时调removeAt然后又用同一个索引去访问下一项结果越界。给一个通用处理技巧删除或插入元素后立刻break或者改用从后往前遍历。写这类代码时养成一个习惯——对容器做结构性修改后不要持有任何旧的迭代器或索引。如果非要在遍历时改结构就用一个临时变量先备份需要修改的位置和值遍历完再统一改。4.3 打包发布显示“no qt platform plugin could be initialized”做到最后一步交付很多同学是把整个QT工程拷给老师让老师装环境这个体验很糟。正确的做法是用windeployqt打包成独立exe。运行方式是在命令行进入build目录windeployqt your_app.exe它会自动把QT运行所需的dll和插件复制到exe所在目录。如果出现“no qt platform plugin”的报错多半是platforms文件夹没有正确生成或没有和exe放在同一级目录。检查一下exe同级目录下有没有一个platforms文件夹里面必须有qwindows.dll。如果是跨平台部署比如要在Linux或国产系统上跑QCustomPlot这类第三方库以及中文字体路径都要提前确认。QT下载安装时建议直接用官方镜像比慢慢拖官网快很多。4.4 中文乱码与编码问题QT5默认源码字符集是UTF-8但如果你在Windows上用旧编译器打开带中文的源文件可能出现乱码。解决办法是统一源文件编码格式为UTF-8 with BOM或者在main里设置QTextCodec::setCodecForLocale(QTextCodec::codecForName(UTF-8));还有一个更隐蔽的坑如果你把QString直接写入CSV文件再打开Excel可能显示乱码。原因是没有写入BOM头。写入文件时先写一个QVariant(\\xef\\xbb\\xbf).toByteArray()即可解决。这个细节很多带数据库有的同学也会漏但踩过一次之后就记住了。4.5 界面空闲时CPU占用过高用QTimer刷新候诊大屏时如果刷新间隔设得太短比如100毫秒终端会看到CPU占用一直很高。建议把定时器间隔设在1000毫秒到2000毫秒之间完全满足叫号场景。另外如果候诊大屏不做轮播动画可以只在取号或叫号动作发生时触发刷新而不是定时无限刷新这样CPU占用能降到一个很低的水平。5. 围绕“数据结构”的答辩加分项5.1 复杂度分析怎么写答辩报告里一定要有一张表把系统里每个核心操作的复杂度列出来。这是我当时的表格可以直接复用操作数据结构时间复杂度说明患者取号入队链表队列O(1)维护尾指针直接插入医生按序叫号链表队列O(1)头指针出队过号回诊插入链表队列O(1)插入队首急诊优先入队最小堆O(log n)堆上浮调整按科室查找队列哈希表O(1)以科室ID为键历史记录撤销栈O(1)最近一条弹出这张表能直接回答老师所有关于性能的问题哪怕老师追问“你这哈希冲突怎么解决”这种问题也能顺着话题展开。5.2 扩展功能QCustomPlot动态趋势图如果学有余力可以额外做一个候诊人数实时统计图。用QCustomPlot在候诊大屏底部画一条折线横轴是时间纵轴是当前候诊人数每个科室一条不同颜色的线。这个功能和我看到热搜里“qt时域图转换为频域图”“使用qcustomplot显示”这些搜索需求本质上是同一类技术把动态数据绑定到图表上实时刷新。做出来后不仅界面专业度大大提高还能在答辩时展示“用图表做数据监控”的能力属于明显的加分项。QCustomPlot的曲线刷新很简单只需要在定时器回调里更新数据点再调用replot()。不过注意控制数据点数量超过几百个点后建议只保留最近一段窗口的数据防止性能下降。5.3 代码组织的三条建议第一把数据访问和界面逻辑分离。我建议所有队列操作封装在一个叫DataCenter的类里取号端和医生端都通过这个类去操作队列界面层只是调用按钮和刷新列表。这样后面改界面不会牵连到核心数据结构代码。第二每个文件头部写清楚模块职责注释。这一点是很多课程设计被扣分的隐形原因老师翻代码时如果找不到入口函数印象分直接掉。第三保留一个demo模式。如果你担心答辩现场演示时患者数据不足可以在系统里加一个“模拟取号”按钮启动时自动生成几十个测试患者这样演示效果会丰满很多。这算不上什么高深技巧但现场效果差异真的很大。6. 几个容易被问住的细节问题6.1 数据是存内存还是落盘课程设计级别内存结构一定要有这体现了数据结构功底落盘同样建议有因为能体现工程完整性。我当时是用CSV文件存储每一笔取号记录启动时如果存在上次的记录文件就自动加载回内存这样形成了一个“文件持久化内存队列”的双层结构。虽然复杂度不高但至少证明你考虑了数据持久化的问题不至于被问“重启后数据还在吗”时卡住。6.2 队列为空时按“下一个”怎么办这是个很基础的边界情况但很容易被忽视。处理逻辑很简单判断isEmpty如果是弹提示“当前队列暂无患者等待”。如果不处理dequeue时会出错或者返回一个无效Patient对象演示时就会很尴尬。所有用户可操作的按钮都要把空状态考虑进去。6.3 并发取号和叫号时数据竞争问题如果你最终决定用多线程实现那必须引入互斥锁来保护队列QMutex m_queueMutex; m_queueMutex.lock(); q-enqueue(p); m_queueMutex.unlock();这里有一个原则锁的范围要尽量小锁住的时间要尽量短。如果你把取号、刷新UI、写日志全放在锁里面多线程效果就没意义了还可能因为UI信号阻塞引发死锁。锁只保护队列这个共享资源的读写其他事情都放到锁外面做。我在做这个项目时最大的体会是一个系统看似简单但真正把每个数据结构都用到“该用的位置”需要的思考量一点也不少。特别是过号处理和急诊优先这两个场景几乎是把教材里的队列和堆重新学了一遍才想明白。如果你现在正在为课设发愁不要急着打开QT Designer拖控件先把队列、优先队列、哈希表这几个结构和业务场景对起来代码实现会顺畅很多。后面如果再加入QCustomPlot动态图表这类扩展整个项目的完成度和答辩竞争力还会再上一个档次。本文还有配套的精品资源点击获取