Qt Widgets打地鼠游戏开发:源码拆解与课程设计实战
发布时间:2026/10/1 11:57:42 作者:尧图编辑部 阅读量:1,286

简介这是一份面向C课程设计或期末大作业场景的Qt打地鼠游戏完整源码适合需要完成图形界面编程项目、或想学习Qt事件处理与控件定制的在校学生。压缩包共60个文件以15个cpp和12个h源码文件为主配合3个ui界面文件、3个pro工程文件、3个qrc资源文件以及多张png/jpg图片素材整体约4.97MB目录中保留了最终版和倒数第二版两套工程便于对比修改痕迹。目前已有1766人学习下载说明该题目在课设选题中具有较高参考价值。资源包含可运行的打地鼠游戏主体涉及自定义按钮、鼠标点击逻辑、图片切换与游戏状态控制等关键模块既可直接提交作为课程设计成果也能通过研读源码理解Qt项目组织方式并在此基础上扩展计分、关卡等玩法。1. 这是一份能直接跑的 Qt 打地鼠源码拆开看别自己从零写C 课程设计里“打地鼠”是出现率极高的题目——玩法直观、代码量可控、答辩时还能现场演示交互逻辑。你手上这份源码就是一个用 Qt Widgets 写好的完整工程界面里有多个洞位随机出地鼠玩家点击得分倒计时结束进入胜负结算所有图片素材都通过 qrc 资源文件打包进可执行程序。它适合两类人一是 C 期末大作业还没头绪、想把现成工程改成自己版本的同学二是想快速搞懂 Qt 小游戏里 QTimer、随机数、信号槽怎么配合的新手。直接打开源码读一遍能理清整个项目骨架想省事的改改参数和文案也能当大作业交。2. 先把工程看清按文件清单拆 Qt 项目的协作方式拿到一个 Qt 源码包最忌讳的是双击 .pro 之后乱点一通。我的习惯是先把文件清单过一遍搞清楚哪些是代码、哪些是资源、哪些是编辑器生成的垃圾文件。这个习惯能让你在答辩被问“项目结构是什么样的”时三句话讲清楚。2.1 文件清单一次认全所有模块以及哪些可以扔解压后你会看到两个版本目录一个是“最终版本”一个是“倒数第二版”这属于作者迭代留下的备份。交作业前把不要的版本删掉只保留一份避免答辩时拿错目录。以最终版本为例核心文件分工如下文件作用备注BeatMice.proqmake 工程文件声明源文件、头文件、资源、Qt 模块想改项目名改这里image.qrc图片资源索引把 png/jpg 打包进二进制图片不显示先查这里main.cpp程序入口创建 QApplication 并显示主窗口每个 Qt 程序都有mainwindow.h/cpp/ui主窗口类游戏流程与界面布局都在这整个工程的核心mouse.h/cpp地鼠类点击判定逻辑在这里继承自按钮mypushbutton.h/cpp自定义按钮用于洞口或锤子光标样式控制交互手感qpushbutton.h/cpp早期版本的按钮实现最终版里也保留了background1.h/cpp背景绘制负责草地等静态场景可整体替换youwin.h/cpp结束/胜利界面游戏结束跳转各种 .png / .jpg开始界面、草地、地鼠、锤子、结算图全部通过 qrc 引用BeatMice.pro.userQt Creator 生成的本地配置可删别提交提示.pro.user 记录的是你本机的 Qt Kit 路径和构建目录。把它带到老师电脑上不仅没用还会导致“项目配置错误”的弹窗。删掉它让 Qt Creator 重新识别 .pro 文件反而更干净。2.2 main.cpp程序入口的三步固定套路Qt Widgets 程序的入口几乎长一个样这段代码对应 main.cpp#include mainwindow.h #include QApplication int main(int argc, char *argv[]) { QApplication a(argc, argv); // 创建一个 Qt 应用对象管理事件循环 MainWindow w; // 创建主窗口对象 w.show(); // 显示窗口 return a.exec(); // 进入事件循环直到窗口关闭才返回 }逻辑说明QApplication 是 Qt 程序的“心脏”它负责处理鼠标点击、键盘输入、窗口重绘等所有事件。MainWindow 是自定义主窗口类w.show() 让窗口可见。最后一行 a.exec() 会阻塞在这里不断分发事件窗口被关闭时这条语句才会返回。参数说明argc 和 argv 是命令行参数Qt Creator 运行时通常为空但保留它们是为了支持一些 Qt 运行时参数比如高 DPI 缩放。实际项目中这三行基本不用动你要改的都在 MainWindow 里。2.3 mainwindow.h三个槽函数撑起整个游戏循环打开 mainwindow.h你能看到这个游戏最核心的设计——它把整个游戏流程抽象成了三个槽函数class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); ~MainWindow(); private slots: void startGame(); // 点击“开始”按钮初始化分数和倒计时 void showHammer(); // 定时触发随机让一只地鼠钻出来 void updateTime(); // 每秒触发倒计时并刷新界面 private: Ui::MainWindow *ui; QTimer *showTimer; // 地鼠出洞节奏 QTimer *countTimer; // 倒计时 int score; int timeLeft; };逻辑说明这个类把游戏拆成了三个动作——启动、出地鼠、倒计时。startGame 负责把所有状态归零showHammer 由出洞定时器周期性调用updateTime 由倒计时定时器每秒调用。整个游戏不需要额外线程全靠 Qt 的信号槽机制在事件循环里驱动。参数说明score 和 timeLeft 是普通 int 成员变量分别在 startGame 里初始化。showTimer 和 countTimer 是 QTimer 指针通常在构造函数里 new 出来并指定 this 为父对象避免内存泄漏。这两个定时器的间隔分别控制“地鼠出现的快慢”和“游戏总时长”是调整难度的关键参数。3. 核心机制随机出洞、点击判定与 QTimer 游戏循环这一章是整份源码最值得读的地方。打地鼠看起来是个“小游戏”但里面涉及随机数生成、控件事件响应、定时器协作三个基础能力。把这章吃透你就能自己动手改玩法。3.1 地鼠本质是按钮Mouse 类的点击判定第一次写 Qt 游戏的人很容易陷入“自己画动画”的思路。实际上对于课程设计更省事也更好讲的方案是让地鼠直接继承 QPushButton把显示、隐藏、点击事件全部交给 Qt 现成机制处理。class Mouse : public QPushButton { Q_OBJECT public: explicit Mouse(QWidget *parent nullptr); protected: void mousePressEvent(QMouseEvent *event) override; // 重写鼠标按下 signals: void caught(); // 自定义信号表示“地鼠被抓住了” };逻辑说明Mouse 继承 QPushButton 后天然具备按钮的所有能力——可以 setIcon 显示地鼠图片可以 setVisible 控制出没还可以 connect 它的点击信号。这里重写 mousePressEvent目的是在地鼠被点击瞬间发出 caught 信号让主窗口统一处理加分而不是让地鼠自己处理分数职责更清晰。参数说明mousePressEvent 的 event 参数里可以拿到点击位置和按钮类型。课程设计场景下通常不细分左键右键但你可以在 event-button() Qt::LeftButton 时再做计分防止右键误触。parent 参数传主窗口指针保证地鼠控件随主窗口一起释放。实现文件里对应这样的逻辑void Mouse::mousePressEvent(QMouseEvent *event) { if (event-button() Qt::LeftButton) { emit caught(); hide(); // 抓住后立刻藏回去避免重复计分 } QPushButton::mousePressEvent(event); }逻辑说明emit caught() 通知主窗口加分随后立刻 hide()这样同一只地鼠在本次现身周期内只能被点一次不会出现反复点一个洞刷分的漏洞。如果你不想隐藏也可以加一个临时标记间隔一段时间后才能再点但这会增加状态管理复杂度课程设计不推荐。3.2 随机出洞逻辑bounded() 永远比 % 可靠地鼠从哪个洞出来是打地鼠游戏的随机性来源。常见做法是维护一个 QList存放所有洞口按钮然后在定时器槽函数里随机取一个。void MainWindow::showHammer() { // 先把上一只正在显示的地鼠藏回去 for (Mouse *m : miceList) { m-hide(); } // 从所有地鼠中随机选一只显示 int idx QRandomGenerator::global()-bounded(miceList.size()); miceList.at(idx)-show(); }逻辑说明每次触发先全部隐藏再随机显示一只逻辑上最简单也避免了“多只地鼠同时出现导致分数失控”的问题。如果你想做“同时出现多只”的难度模式就改成随机显示 2 只或 3 只但计分和判定逻辑要保持一致。参数说明miceList.size() 是地鼠总数bounded(N) 返回 0 到 N-1 之间的整数切分布均匀。注意这里用的是 QRandomGenerator::global()而不是老代码里的 qrand() % miceList.size()原因下一节细说。3.3 随机数坑qsrand 的种子玄学与 QRandomGenerator 用法很多旧版 Qt 教程里会这样写随机出洞qsrand(QTime::currentTime().msecsSinceStartOfDay()); int idx qrand() % 9;这段代码看着没问题实际运行会出现两个经典翻车现场。第一qsrand 每次调用都会重置随机种子如果你在循环里反复调用 qsrand种子会被反复重置为同一个基准值导致随机序列短、重复率高。第二qrand() % 9 在大部分场景下够用但当取模的除数不是质数时分布会出现轻微偏差虽然打地鼠这种游戏对分布没那么敏感但答辩时老师问“你的随机数怎么保证不均匀”你总不能只说“靠感觉”。Qt 5.10 以后提供了标准答案——QRandomGeneratorint idx QRandomGenerator::global()-bounded(miceList.size());参数说明bounded(N) 取 [0, N) 的左闭右开区间N 是洞口数。global() 返回进程级全局随机数生成器自动用系统熵初始化种子线程安全不需要你关心种子。在 Qt 6 里qrand 和 qsrand 已经被移除新代码再写 qrand 直接编译不过。这也是我强烈建议你统一用 bounded() 的根本原因——它能同时兼容 Qt 5 和 Qt 6答辩换环境编译也不翻车。3.4 游戏循环靠三个 QTimer间隔、暂停与倒计时Qt 小游戏的“心跳”是 QTimer。打地鼠项目里至少会出现两个定时器一个控制出洞节奏一个控制总倒计时。// 构造函数或 initGame() 中初始化 showTimer new QTimer(this); showTimer-setInterval(800); // 每 800ms 触发一次出洞 connect(showTimer, QTimer::timeout, this, MainWindow::showHammer); countTimer new QTimer(this); countTimer-setInterval(1000); // 每 1 秒触发一次倒计时 connect(countTimer, QTimer::timeout, this, MainWindow::updateTime);逻辑说明showTimer 负责“出地鼠”countTimer 负责“倒计时”。两个定时器互不阻塞因为 Qt 的事件循环会交替分发 timeout 信号。startGame 里同时 start() 两个定时器gameOver 里同时 stop()。参数说明setInterval 的单位是毫秒。800ms 意味着平均每 0.8 秒出现一只地鼠这个频率对新手玩家刚好如果想做难度递增在分数增长后动态减小 showTimer 的间隔即可。注意 QTimer 的精度在 Windows 上默认约 15ms 误差对倒计时这种一秒级操作完全无感不用纠结。4. 把工程跑起来环境配置、编译链路与资源路径排查很多同学卡在“源码看懂了但跑不起来”。这一章讲清楚 Qt 环境怎么选、命令行怎么编译、图片为什么老是显示不出来。4.1 环境选型Qt 版本与编译器的搭配先给你一张踩坑多年的经验表环境组合适合场景注意点Qt 5.12/5.15 MinGW 32/64课程设计最稳妥老师电脑兼容性最好安装时务必勾选 MinGW 编译器组件Qt 5.15 MSVC 2019需要配合 Visual Studio 调试Debug 和 Release 必须对应同一套编译器Qt 6.2 MinGW新电脑、新系统老代码如果用了 qrand 需要换成 QRandomGenerator这个降压包是用 Qt Widgets 写的qmake 语法在 Qt 5 和 Qt 6 里都能编译。我的建议是直接用 Qt 5.15 MinGW理由有三个一是网上能搜到的大作业参考基本都是 Qt 5 语法二是 Qt 5.15 是最后支持 Windows 7 的版本兼容范围广三是 MinGW 不需要额外装 Visual Studio安装包自带编译器省事。有人图方便用 VSCode 配 C/C 环境去手动编译 Qt 工程我劝你别折腾。Qt Creator 本来就是为了 qmake/CMake 工程设计的直接打开 .pro 文件自动完成项目解析新手也能在半小时内跑起来省下来的时间不如去改代码。4.2 命令行编译全流程qmake → make → windeployqt即便你平时用 Qt Creator也建议完整走一遍命令行流程这样能真正理解 Qt 程序的构建链路。Windows 上打开 Qt 自带的命令行终端在开始菜单里找 “Qt 5.15.2 (MinGW 8.1.0 64-bit)”。# 1. 进入工程目录注意路径不要带中文和空格 cd D:\BeatMice # 2. 用 qmake 根据 .pro 生成 Makefile qmake BeatMice.pro # 3. 编译并链接-j4 表示 4 个编译任务并行 mingw32-make -j4 # 4. 如果要发布给老师用 windeployqt 拷贝依赖 DLL windeployqt release\BeatMice.exe逻辑说明第 2 步的 qmake 会读取 BeatMice.pro 里的 SOURCES、HEADERS、RESOURCES生成对应平台的 Makefile。第 3 步的 mingw32-make 实际执行编译和链接这一步报错的话错误信息会精确到文件和行号。第 4 步的 windeployqt 是 Qt 自带的部署工具自动把所有需要的 DLL 和插件目录拷到 exe 旁边。参数说明-j4 中的 4 表示并行编译任务数CPU 核心越多可以调越大但课程设计这种小工程编译只要几十秒设 4 够用。如果你的 Qt 安装的是 MSVC 版本编译命令要换成 nmake而不是 mingw32-make。Linux/macOS 上对应的是 make命令本身没有 Windows 那么讲究。4.3 图片不显示的真相qrc 资源与 isNull() 排查打地鼠游戏里图片是命根子地鼠图片丢了玩家就不知道点哪里背景图片丢了窗口白花花一片。图片通过 image.qrc 统一管理运行时 Qt 只关注 qrc 里的逻辑路径不关心图片在磁盘上的实际位置。RCC qresource prefix/ fileimages/mouse.png/file fileimages/hammerdown.png/file fileimages/starttt.png/file /qresource /RCC对应的加载代码QPixmap pix(:/images/mouse.png); if (pix.isNull()) { qWarning() mouse.png 加载失败检查 qrc 路径; } else { ui-startButton-setIcon(pix); }逻辑说明qrc 里写的是相对 qrc 文件所在目录的路径加载时要在前面加冒号前缀。pix.isNull() 是排查图片问题的最重要检查——如果图片没加载成功QPixmap 就是一个空对象isNull() 返回 true。把 qWarning 输出打开能直接在“应用程序输出”窗口里看到失败提示比肉眼猜快得多。参数说明prefix 表示资源前缀默认是“/”所以加载路径是“:/images/mouse.png”。如果你把 prefix 改成“/game”加载路径就得写成“:/game/images/mouse.png”。修改 prefix 后所有引用路径都要同步改否则又是一堆空白窗口这是这个项目里的血泪经验。5. 答辩前排查五个高频翻车现场与对策把源码跑起来只是第一步真正决定你大作业能不能过的是“能不能稳定演示”。这五条是我见过最多人踩的坑每条都按“现象 → 原因 → 解决”给你拆好答辩前对着查一遍。5.1 现象图片一片空白按钮上什么都没有原因qrc 里文件名和实际文件名大小写不一致或者代码里用了绝对磁盘路径“D:/xxx/mouse.png”换一台电脑就失效还有个隐蔽原因是修改了图片文件后没有重新 qmake资源还在用旧缓存。解决先看 qWarning 是否有“加载失败”输出再对比 qrc 里写的路径和磁盘上实际文件路径的每个字符然后删除 build 目录重新 qmake 编译。绝对路径必须改成“:/”开头的 qrc 路径否则一换环境必翻车。5.2 现象程序一启动就崩溃或点了某按钮后闪退原因最常见的是 ui 指针还没初始化就访问控件——比如构造函数里写ui-scoreLabel-setText(0)但ui-setupUi(this)还没执行另一个常见问题是 new 出来的子控件没有指定父对象函数结束后被提前析构后续访问变成悬空指针。解决在 MainWindow 构造函数中第一行就调用ui-setupUi(this)任何对 ui 控件的操作必须在这个调用之后。所有动态 new 的控件统一传 this 作为父对象。如果崩溃只发生在点击地鼠时优先检查 Mouse 的 parent 是否有效以及 caught 信号的接收方是否已经被销毁。5.3 现象地鼠老是出现在同一个洞里随机感很差原因项目里用了 qsrand qrand 的老式随机方案而 qsrand 没有在启动时设置种子或者在一个循环内反复调用 qsrand导致每次随机序列几乎相同。如果用的是 qrand() % 9当 qrand() 返回值固定时取余结果自然固定。解决删除所有 qsrand 调用统一改成QRandomGenerator::global()-bounded(miceList.size())。如果非要保留 qsrand务必只在 main() 或构造函数里调用一次并确保每次启动种子不同比如用QTime::currentTime().msecsSinceStartOfDay()作为种子。5.4 现象点了地鼠没反应不触发计分原因一种情况是地鼠图片放在 QLabel 里而 QLabel 默认不接收鼠标点击事件另一种情况是地鼠按钮被其他透明控件遮挡实际点击被上层控件消化掉了到不了 Mouse。解决优先确认 Mouse 是 QPushButton 子类而不是 QLabel子类。如果必须用 QLabel要重写 mousePressEvent 并调用 setAttribute(Qt::WA_TransparentForMouseEvents) 相关属性。排查遮挡时把疑似遮挡的控件临时 hide() 测试定位后再调整布局层级。5.5 现象拷到老师电脑上打不开或者提示 Qt 相关 DLL 缺失原因只拷贝了 exe 文件没有拷贝 Qt 依赖的 DLL 和插件目录或者把 .pro.user 也拷过去了Qt Creator 加载了本地路径导致项目打不开。还有可能是老师电脑的 Qt 版本比你低你的 Kit 配置比老师的版本新。解决拷贝整个源码目录时先删除 .pro.user 和 build 目录。直接给 exe 的话运行windeployqt 你的exe路径把生成的整体目录拷过去。如果老师那边只是双击 exe把 exe 连同 windeployqt 生成的整个文件夹一起压缩确保 DLL 和 exe 同级。实在不行启动前把 Qt 安装路径加入 PATH但这不是长久之计。6. 改造成能答辩的版本计分面板、倒计时与难度曲线老师看大作业不只是看游戏能不能玩更看你是不是只会“照搬”。给这个源码加三个小功能能让你在答辩时的表现完全不同。6.1 三个加分功能的优先级功能改动量答辩看点得分与倒计时可视化半小时“界面状态实时刷新人机交互完整”难度递增20 分钟“游戏节奏动态调整可玩性提升”连击奖励40 分钟“连续命中得分翻倍玩法有深度”没有计分面板的游戏老师很容易觉得“你只是把图片点来点去”。加上分数和倒计时交互闭环就完整了。6.2 计分与倒计时两个标签撑起实时反馈void MainWindow::updateTime() { timeLeft--; ui-timeLabel-setText(QString(剩余时间: %1 秒).arg(timeLeft)); if (timeLeft 0) { gameOver(); } } void MainWindow::onMouseCaught() { score 1; ui-scoreLabel-setText(QString(得分: %1).arg(score)); }逻辑说明countTimer 每秒触发一次 updateTimetimeLeft 递减后立刻刷新界面。当时间归零调用 gameOver 进入结算。onMouseCaught 连接 Mouse 的 caught 信号每抓住一只地鼠分数加一并同步更新界面。参数说明QString::arg() 是 Qt 推荐的格式化方法比 sprintf 安全不会因为类型不匹配而崩溃。%1 是占位符后面的参数会替换它。如果你想显示两位数比如“05 秒”可以用QString(%1).arg(timeLeft, 2, 10, QChar(0))第一个参数 2 是最小宽度10 是十进制最后一个参数是补位字符。6.3 难度递增一行 setInterval 调出节奏感void MainWindow::onMouseCaught() { score 1; ui-scoreLabel-setText(QString(得分: %1).arg(score)); // 每 20 分加速一次但最短间隔不低于 200ms if (score % 20 0 showTimer-interval() 200) { showTimer-setInterval(showTimer-interval() - 100); qInfo() 游戏加速出洞间隔: showTimer-interval() ms; } }逻辑说明每抓住 20 只地鼠出洞间隔缩短 100ms游戏节奏逐渐加快。判断interval() 200是防止间隔无限缩小到人根本反应不过来这也是一个可讲的“设计边界”问题答辩老师一般会喜欢这个细节。参数说明showTimer-interval() 返回当前间隔毫秒数setInterval 重新设置后从下一次延迟开始生效不需要重启定时器。200ms 的下限是经验值——低于这个值地鼠出现即消失玩家根本点不到游戏失去意义。qInfo() 输出调试日志也可以改成一个界面提示“速度提升”。到了这一步这个打地鼠工程已经不只是一个“能跑的作业”而是一个有计分、有倒计时、有动态难度的完整游戏。我当年第一次交这种课设就漏了难度递减这个逻辑老师当场问“你打算怎么让玩家觉得越来越有意思”我愣在原地。从那以后我每次改完 Qt 小游戏都强制自己过一遍流程先不插电演示通关一次确认随机线索不重复再用 windeployqt 打包到干净目录验证 DLL最后删掉 .pro.user 重新打开工程模拟老师那边的环境。这套习惯帮我在答辩和后续工作中都少踩了很多坑希望帮到你。本文还有配套的精品资源点击获取