QT+C++打地鼠游戏毕业设计实战:从环境搭建到答辩演示

📅 发布时间:2026/10/5 9:29:09
QT+C++打地鼠游戏毕业设计实战:从环境搭建到答辩演示
简介这是一份基于QT与C实现的打地鼠游戏完整源码面向计算机相关专业的毕业设计、课程设计以及入门级项目开发学习者帮助解决缺少可运行小游戏案例、难以理解QT界面与逻辑联动的问题。资源包共19个文件约151KB包含4个cpp源文件、3个头文件、1个ui界面文件、1个pro工程文件与1个qrc资源文件另附jpg、png图片素材及README说明覆盖游戏主逻辑、场景绘制与图片项处理等模块。项目在QT环境下编译依托信号与槽机制完成界面与应用程序的关联并支持动态调整参数以改变地鼠出现速度与游戏难度便于在此基础上扩展玩法或改造为个人作品。目前已有147人学习下载适合希望快速掌握QT事件驱动与图形界面开发思路的读者参考复用。1. 打地鼠游戏遇上 QTC一个能写进简历的桌面项目长什么样很多计算机专业的同学到了大三下学期都会面临同一个问题毕业设计选题到底做什么。做管理系统吧网上一搜全是模板答辩老师一看就知道是抄的做算法研究吧数学底子又不够硬。这时候基于 QTC 开发的打地鼠游戏就成了一个很讨巧的选择——它既有图形界面可以展示又有游戏逻辑能体现编程能力代码量适中一个人两三周就能搞定而且源码结构清晰答辩的时候讲得出东西。这个项目的核心思路其实不复杂用 QT 的 QWidget 或 QGraphicsView 搭建游戏窗口用 QTimer 控制地鼠出现的节奏用 QPushButton 或自定义控件表示地鼠洞玩家点击后触发信号槽完成计分。听起来简单但真正动手做的时候你会发现有一堆细节要处理地鼠出现的随机算法怎么设计才不显得机械倒计时结束怎么优雅地切换场景分数怎么持久化保存这些才是拉开差距的地方。这篇文章面向的是正在准备毕业设计或课程设计的同学也适合想通过一个完整小项目来练手 QT 和 C 的开发者。我会从环境搭建讲到核心模块实现再到打包发布和答辩时可能被问到的问题尽量把每个环节的参数和踩坑点都讲清楚。读完你至少能拿到一个可运行、可演示、可扩展的桌面游戏框架而不是一个只能跑在你自己电脑上的半成品。2. 环境搭建与项目骨架从 QT 安装到第一个可运行窗口2.1 QT 版本选择与安装避坑QT 的版本选择是第一个容易翻车的地方。目前主流的选择是 QT 5.15.2 和 QT 6.x 两个大版本。对于毕业设计来说我一般推荐 QT 5.15.2原因有三一是网上大部分教程和开源项目都基于 QT 5遇到问题容易搜到答案二是 QT 5.15.2 是最后一个提供离线安装包的 LTS 版本安装过程不需要注册账号三是 QT 6 虽然更新但部分模块的 API 有变动比如 QMediaPlayer 的用法就和 QT 5 差别很大如果你只是想做个打地鼠游戏没必要给自己找麻烦。安装的时候注意勾选组件。很多同学装完 QT 发现编译报错:-1: error: dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwid这种路径相关的错误大概率是因为编译器套件没选对。如果你在 Windows 上开发建议勾选 MSVC 2019 64-bit 和 MinGW 8.1 64-bit 两个套件前者用于正式发布后者用于快速调试。另外记得勾选 Qt Creator这是官方 IDE虽然有人喜欢用 VS Code 配置 C/C 环境来写 QT但 Qt Creator 的信号槽自动补全和 UI 设计器确实更方便。安装完成后在 Qt Creator 里新建项目选择 Application (Qt Widgets Application)项目名用英文路径不要有中文和空格。构建系统选 qmake 还是 CMake对于这个小项目qmake 足够了配置简单.pro文件几行就能搞定。如果你以后想往 QT MVVM 框架或者 QT QML 方向发展可以再学 CMake但打地鼠这个体量用不上。2.2 项目文件结构与 .pro 配置一个清晰的目录结构能让你的代码在答辩时加分。我一般会这样组织WhackAMole/ ├── WhackAMole.pro ├── main.cpp ├── mainwindow.h ├── mainwindow.cpp ├── mainwindow.ui ├── gamewidget.h ├── gamewidget.cpp ├── molebutton.h ├── molebutton.cpp ├── resources/ │ ├── images/ │ │ ├── mole.png │ │ ├── hole.png │ │ └── background.png │ └── sounds/ │ ├── hit.wav │ └── miss.wav └── data/ └── scores.json.pro文件是 qmake 的工程描述文件你需要把新增的源文件和资源文件加进去QT core gui widgets multimedia TARGET WhackAMole TEMPLATE app SOURCES \ main.cpp \ mainwindow.cpp \ gamewidget.cpp \ molebutton.cpp HEADERS \ mainwindow.h \ gamewidget.h \ molebutton.h FORMS \ mainwindow.ui RESOURCES \ resources.qrc这里QT multimedia是为了后面加音效用的如果你暂时不需要声音可以先去掉。RESOURCES对应的是.qrc资源文件在 Qt Creator 里右键项目 → Add New → Qt → Qt Resource File 就能创建。把图片和音频加进去之后代码里就可以用:/images/mole.png这样的路径访问不用操心发布时资源文件丢失的问题。2.3 主窗口与游戏场景的切换逻辑打地鼠游戏一般有两个场景开始界面和游戏界面。开始界面放标题、开始按钮和排行榜入口游戏界面放九宫格地鼠洞、分数显示和倒计时。我习惯用QStackedWidget来管理这两个场景而不是开多个窗口。这样做的好处是切换流畅不需要反复创建销毁窗口对象而且状态管理更集中。在mainwindow.ui里拖一个QStackedWidget默认两个页面。第一页放开始界面第二页放游戏界面。然后在mainwindow.cpp里连接开始按钮的clicked信号到槽函数调用ui-stackedWidget-setCurrentIndex(1)完成切换。游戏结束时再切回第一页同时刷新排行榜数据。// mainwindow.cpp void MainWindow::onStartButtonClicked() { ui-stackedWidget-setCurrentIndex(1); ui-gameWidget-startGame(30); // 30秒倒计时 } void MainWindow::onGameOver(int score) { ui-stackedWidget-setCurrentIndex(0); updateScoreBoard(score); }startGame(int seconds)是GameWidget的公开方法参数是游戏时长。onGameOver是自定义信号gameOver(int)的槽函数在GameWidget里倒计时归零时发射。这种信号槽的解耦设计让主窗口不需要知道游戏内部怎么运作只负责场景切换和数据展示。注意QStackedWidget的页面索引从 0 开始如果你在 UI 设计器里调整了页面顺序记得同步修改代码里的索引值否则会出现点击开始按钮没反应的情况。3. 核心玩法实现地鼠生成、点击判定与计分逻辑3.1 地鼠洞控件设计与随机生成算法九宫格是打地鼠最经典的布局用QGridLayout放九个自定义的MoleButton控件就行。MoleButton继承自QPushButton内部维护一个bool m_hasMole状态用来标记当前洞里有没有地鼠。地鼠的显示和隐藏通过切换按钮的图标实现// molebutton.h class MoleButton : public QPushButton { Q_OBJECT public: explicit MoleButton(QWidget *parent nullptr); void showMole(); void hideMole(); bool hasMole() const; signals: void moleHit(); void moleMissed(); private: bool m_hasMole; QTimer *m_hideTimer; };// molebutton.cpp MoleButton::MoleButton(QWidget *parent) : QPushButton(parent), m_hasMole(false) { setIcon(QIcon(:/images/hole.png)); setIconSize(QSize(120, 120)); setFixedSize(140, 140); m_hideTimer new QTimer(this); m_hideTimer-setSingleShot(true); connect(m_hideTimer, QTimer::timeout, this, MoleButton::hideMole); connect(this, QPushButton::clicked, this, [this]() { if (m_hasMole) { emit moleHit(); hideMole(); } else { emit moleMissed(); } }); } void MoleButton::showMole() { m_hasMole true; setIcon(QIcon(:/images/mole.png)); m_hideTimer-start(1200); // 地鼠停留1.2秒后自动消失 } void MoleButton::hideMole() { m_hasMole false; setIcon(QIcon(:/images/hole.png)); m_hideTimer-stop(); }地鼠的随机生成用一个QTimer驱动每隔一个随机间隔就选一个空闲的洞显示地鼠。这里有个细节如果直接用qrand() % 9随机选洞可能会出现同一个洞连续出地鼠的情况玩家体验会很差。我的做法是维护一个“上次出洞索引”变量随机时排除掉它void GameWidget::spawnMole() { QVectorint freeHoles; for (int i 0; i 9; i) { if (!m_buttons[i]-hasMole() i ! m_lastHoleIndex) { freeHoles.append(i); } } if (freeHoles.isEmpty()) return; int idx freeHoles[qrand() % freeHoles.size()]; m_lastHoleIndex idx; m_buttons[idx]-showMole(); }m_lastHoleIndex初始化为 -1保证第一次生成不受限制。地鼠停留时间设为 1200 毫秒这个值可以根据难度调整简单模式 1500 毫秒困难模式 800 毫秒。生成间隔我一般设为 600 到 1000 毫秒之间的随机值用QTimer::start(interval)动态调整。3.2 点击判定、连击与分数计算点击判定在MoleButton的clicked信号里已经处理了如果m_hasMole为真发射moleHit信号否则发射moleMissed。GameWidget接收这两个信号后更新分数void GameWidget::onMoleHit() { m_combo; int baseScore 10; int comboBonus (m_combo 3) ? (m_combo - 2) * 5 : 0; m_score baseScore comboBonus; updateScoreLabel(); playSound(:/sounds/hit.wav); } void GameWidget::onMoleMissed() { m_combo 0; m_score qMax(0, m_score - 5); updateScoreLabel(); playSound(:/sounds/miss.wav); }连击机制是加分项连续命中 3 次以上每次额外加 5 分连击越高加成越多。打空则扣 5 分并重置连击。这个规则简单易懂答辩时也好解释。分数下限用qMax(0, ...)保证不会出现负数避免玩家看到负分时产生困惑。音效播放用QSoundEffect类需要包含QSoundEffect头文件并在.pro里加上QT multimedia。注意QSoundEffect只支持 WAV 格式MP3 需要额外的解码器所以音效文件最好提前转成 WAV。3.3 倒计时与游戏结束处理倒计时用一个QTimer每秒触发一次更新界面上的时间标签。归零时停止所有定时器发射gameOver(m_score)信号void GameWidget::startGame(int seconds) { m_remainingTime seconds; m_score 0; m_combo 0; m_lastHoleIndex -1; updateScoreLabel(); updateTimeLabel(); m_countdownTimer-start(1000); m_spawnTimer-start(800); } void GameWidget::onCountdownTick() { m_remainingTime--; updateTimeLabel(); if (m_remainingTime 0) { m_countdownTimer-stop(); m_spawnTimer-stop(); for (auto *btn : m_buttons) { btn-hideMole(); } emit gameOver(m_score); } }m_spawnTimer的间隔在每次触发后可以动态调整比如随着时间推移逐渐缩短增加难度。但要注意不要低于 400 毫秒否则地鼠出现太频繁玩家根本反应不过来体验反而不好。提示游戏结束时一定要遍历所有按钮调用hideMole()否则残留的地鼠图标会在下一局开始时还显示着看起来像 bug。4. 数据持久化与排行榜把分数存下来才算完整4.1 用 QJsonDocument 读写本地分数文件毕业设计答辩时老师很喜欢问“你的数据存在哪里”。如果回答“只存在内存里关了程序就没了”印象分至少扣一半。最简单的持久化方案是用 JSON 文件QT 自带的QJsonDocument、QJsonObject、QJsonArray就能搞定不需要引入第三方库。分数文件放在程序运行目录下的data/scores.json结构如下{ scores: [ {name: 玩家1, score: 320, date: 2025-01-15}, {name: 玩家2, score: 280, date: 2025-01-14} ] }读取和写入的代码// scoremanager.cpp QVectorScoreRecord ScoreManager::loadScores() { QVectorScoreRecord records; QFile file(data/scores.json); if (!file.open(QIODevice::ReadOnly)) { return records; // 文件不存在时返回空列表 } QByteArray data file.readAll(); file.close(); QJsonParseError error; QJsonDocument doc QJsonDocument::fromJson(data, error); if (error.error ! QJsonParseError::NoError) { qWarning() JSON parse error: error.errorString(); return records; } QJsonArray arr doc.object()[scores].toArray(); for (const auto item : arr) { QJsonObject obj item.toObject(); records.append({ obj[name].toString(), obj[score].toInt(), obj[date].toString() }); } return records; } void ScoreManager::saveScore(const QString name, int score) { QVectorScoreRecord records loadScores(); records.append({name, score, QDate::currentDate().toString(yyyy-MM-dd)}); std::sort(records.begin(), records.end(), [](const ScoreRecord a, const ScoreRecord b) { return a.score b.score; }); if (records.size() 10) { records.resize(10); // 只保留前10名 } QJsonArray arr; for (const auto r : records) { QJsonObject obj; obj[name] r.name; obj[score] r.score; obj[date] r.date; arr.append(obj); } QJsonObject root; root[scores] arr; QDir().mkpath(data); QFile file(data/scores.json); if (file.open(QIODevice::WriteOnly)) { file.write(QJsonDocument(root).toJson()); file.close(); } }loadScores()在文件不存在时返回空列表不会崩溃。saveScore()先读取已有记录追加新分数按降序排序只保留前 10 名然后写回文件。QDir().mkpath(data)确保目录存在避免第一次运行时写入失败。4.2 排行榜界面与数据绑定排行榜用QTableWidget展示三列排名、玩家名、分数。每次从游戏界面切回开始界面时调用refreshScoreBoard()重新加载数据void MainWindow::refreshScoreBoard() { QVectorScoreRecord records ScoreManager::loadScores(); ui-scoreTable-setRowCount(records.size()); for (int i 0; i records.size(); i) { ui-scoreTable-setItem(i, 0, new QTableWidgetItem(QString::number(i 1))); ui-scoreTable-setItem(i, 1, new QTableWidgetItem(records[i].name)); ui-scoreTable-setItem(i, 2, new QTableWidgetItem(QString::number(records[i].score))); } }玩家名的获取可以在游戏结束时弹一个QInputDialog::getText()让玩家输入名字。如果玩家不输入默认用“匿名玩家”。这个交互虽然简单但能让排行榜看起来更真实答辩演示时也更有说服力。注意QTableWidget的setItem每次都会创建新的QTableWidgetItem如果频繁刷新会造成内存泄漏。建议在刷新前先调用clearContents()并设置setRowCount(0)或者用QStandardItemModel来管理数据。4.3 分数文件的异常处理与边界情况JSON 文件虽然简单但有几个边界情况必须处理。第一文件被手动修改成非法 JSON 格式时QJsonDocument::fromJson会返回空文档doc.object()也是空的此时loadScores()应该返回空列表而不是崩溃。第二文件路径问题如果程序安装在C:\Program Files下写入data/目录可能会因为权限不足失败。解决办法是用QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)获取用户数据目录把分数文件存在那里。QString ScoreManager::getScoreFilePath() { QString dataDir QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir().mkpath(dataDir); return dataDir /scores.json; }这样无论程序安装在哪里分数文件都能正常读写。AppDataLocation在 Windows 上对应C:/Users/用户名/AppData/Roaming/应用名在 Linux 上对应~/.local/share/应用名跨平台兼容。5. 避坑与排查编译报错、资源丢失、打包翻车实录5.1 编译时报 “dependent … qtwid” 路径错误现象在 Qt Creator 里编译时输出窗口出现:-1: error: dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwid does not exist项目无法构建。原因这个错误通常是因为.pro文件里引用了不存在的头文件路径或者 QT 安装路径发生了变化而.pro.user文件里还缓存着旧的路径信息。也有可能是你在.pro里手动写了INCLUDEPATH指向了一个已经被移动或删除的目录。解决先删除项目目录下的.pro.user文件重新用 Qt Creator 打开.pro让它重新检测套件。然后检查.pro文件里有没有多余的INCLUDEPATH或LIBS配置把不必要的删掉。如果还不行在 Qt Creator 的“项目” → “构建设置”里把构建目录清空重新执行 qmake。5.2 发布后图片和音效不显示现象在 Qt Creator 里运行一切正常但把 exe 拷贝到其他电脑上地鼠图片和音效全部丢失按钮变成空白。原因图片和音效虽然加到了.qrc资源文件里但如果你在代码里用的是绝对路径比如C:/Users/xxx/images/mole.png发布后路径失效。另外如果.qrc文件没有正确添加到.pro的RESOURCES里资源也不会被编译进 exe。解决确保所有资源引用都用:/开头的资源路径比如:/images/mole.png。检查.pro文件里有没有RESOURCES resources.qrc。如果还是不行在 Qt Creator 里右键.qrc文件 → “Open in Editor”确认图片和音频文件前面的复选框是勾选状态。未勾选的文件不会被编译进资源。5.3 打包后的 exe 缺少 DLL 无法运行现象用windeployqt打包后exe 在开发机上能跑但拷到其他电脑上提示“缺少 Qt5Core.dll”或“无法启动此程序因为计算机中丢失 MSVCP140.dll”。原因windeployqt只会拷贝 QT 相关的 DLL不会自动拷贝 MSVC 运行时库如MSVCP140.dll、VCRUNTIME140.dll。如果目标电脑没有安装 Visual C Redistributable就会报错。解决在打包目录里手动加上MSVCP140.dll、VCRUNTIME140.dll、CONCRT140.dll这几个文件它们通常在C:\Windows\System32下。或者让用户安装 Microsoft Visual C Redistributable 2019。如果用 MinGW 编译则不需要这些但需要带上libgcc_s_seh-1.dll、libstdc-6.dll、libwinpthread-1.dll。5.4 地鼠点击后不消失或连续计分现象玩家点击地鼠后地鼠图标没有立刻消失或者一次点击加了多次分。原因MoleButton的clicked信号在按钮被按下并释放时触发如果玩家快速连点可能会在hideMole()执行前再次触发clicked。另外如果m_hasMole状态没有及时更新也会导致重复计分。解决在clicked的 lambda 里先判断m_hasMole然后立即调用hideMole()把状态置为 false再发射moleHit信号。这样即使连点第二次点击时m_hasMole已经是 false不会重复计分。另外可以在showMole()里调用setEnabled(true)在hideMole()里调用setEnabled(false)从控件层面阻止点击。5.5 倒计时结束后游戏界面卡死现象倒计时归零后游戏界面没有切换回开始界面按钮点不动像卡死一样。原因onCountdownTick()里停止定时器后如果emit gameOver(m_score)的槽函数执行了耗时操作比如写文件、刷新表格会阻塞 UI 线程。另外如果gameOver信号没有正确连接槽函数根本不会执行。解决检查connect(ui-gameWidget, GameWidget::gameOver, this, MainWindow::onGameOver)是否在MainWindow构造函数里正确连接。写文件和刷新表格的操作尽量放在槽函数末尾或者用QTimer::singleShot(0, ...)延迟执行避免阻塞。如果数据量大可以考虑用QtConcurrent放到后台线程但打地鼠这个体量没必要。6. 从能跑到能答辩扩展方向与演示技巧6.1 加难度选择和音效开关基础版本做完之后可以加一个难度选择下拉框简单、普通、困难三档对应不同的地鼠停留时间和生成间隔。用QComboBox放在开始界面startGame时读取当前索引设置对应的参数难度地鼠停留时间生成间隔打空扣分简单1500 ms1000 ms-2普通1200 ms800 ms-5困难800 ms500 ms-10音效开关用一个QCheckBox控制勾选时播放QSoundEffect取消时调用stop()并设置音量为 0。这个功能实现简单但演示时很加分老师会觉得你考虑了用户体验。6.2 用 QSS 美化界面QT 的默认控件样式比较朴素用 QSSQt Style Sheets可以快速提升颜值。比如给按钮加圆角、渐变背景给标签设置字体和颜色QPushButton { background-color: #4CAF50; border: none; border-radius: 10px; color: white; font-size: 18px; padding: 10px 24px; } QPushButton:hover { background-color: #45a049; } QPushButton:pressed { background-color: #3d8b40; } QLabel#scoreLabel { font-size: 24px; font-weight: bold; color: #333333; }把这段样式写在一个.qss文件里在main.cpp里用qApp-setStyleSheet()加载。注意 QSS 的选择器语法和 CSS 类似但部分属性 QT 不支持比如box-shadow需要用QGraphicsDropShadowEffect替代。6.3 答辩演示的 three 个关键点第一提前在答辩电脑上测试。很多教室的电脑没有装 QT 运行环境你的 exe 可能跑不起来。解决办法是提前用windeployqt打包好所有依赖或者带一个绿色版把所有 DLL 和资源文件放在同一个文件夹里。第二准备一个“翻车预案”。如果现场演示时程序崩溃不要慌直接打开 Qt Creator 展示源码和运行日志说明你做了异常处理只是环境问题。第三讲清楚你的代码结构。老师最反感的是“网上抄的”所以你要能说出每个类的作用、信号槽的连接关系、为什么用 JSON 而不是数据库。哪怕代码简单只要逻辑清晰就能拿到不错的分数。我自己的习惯是答辩前把项目里所有qDebug()输出都检查一遍确保没有遗漏的调试信息。另外把main.cpp里的QApplication::setStyle(Fusion)加上可以让界面在不同系统上保持一致避免 Windows 和 Mac 上样式差异过大。希望帮到你。本文还有配套的精品资源点击获取