简介:这是一份面向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.pro | qmake 工程文件,声明源文件、头文件、资源、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.user | Qt 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 以后提供了标准答案——QRandomGenerator:
int 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="/"> <file>images/mouse.png</file> <file>images/hammerdown.png</file> <file>images/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 每秒触发一次 updateTime,timeLeft 递减后立刻刷新界面。当时间归零,调用 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 重新打开工程,模拟老师那边的环境。这套习惯帮我在答辩和后续工作中都少踩了很多坑,希望帮到你。
本文还有配套的精品资源,点击获取