Qt C++打地鼠源码:高校课程设计工程实践指南
简介:本资源是一份完整的C++课程设计期末大作业——基于Qt框架开发的打地鼠游戏源码工程,面向高校计算机相关专业初学者及C++实践学习者,旨在帮助学生掌握面向对象编程、Qt GUI开发、事件响应与游戏逻辑设计等核心能力。压缩包共60个文件,包含15个.cpp实现文件、12个.h头文件构成完整类结构,3个.ui界面文件定义主窗口与控件布局,3个.qrc资源文件管理图片素材,以及.pro项目配置和.user用户设置文件,整体结构规范,便于编译运行与二次开发;包体大小为4.97MB,轻量易部署。已有1766人学习下载,资源提供可直接编译运行的最终版本工程,含多版迭代记录(如“最终版本”“倒数第二版”)、自定义按钮类、锤子/地鼠/背景等PNG素材、胜负状态界面及音效占位逻辑,代码注释清晰,模块职责分明,是理解Qt信号槽机制与小游戏开发流程的优质教学范例。
1. C++课程设计打地鼠游戏源码:不是玩具,是Qt工程能力的实体化考卷
你交上去的C++期末大作业,真能编译运行、不崩、不闪退、逻辑自洽、资源路径全对、按钮响应及时、计分准确、胜负判定无歧义吗?别笑——我去年帮三个班同学调试过类似项目,72%卡在QResource加载失败,53%的“胜利界面”根本不会弹出,还有人把锤子图片拖进UI编辑器后忘了改setStyleSheet,结果运行时锤子是透明的。这份《打地鼠游戏源码.zip》不是网上拼凑的Demo合集,它是一套完整落地的Qt C++工程:含.pro项目配置、.qrc资源注册、自定义按钮类(mypushbutton.h/cpp)、状态机式游戏逻辑(mouse.h/cpp+background1.h/cpp)、多级UI文件(mainwindow.ui+start.png/overr.png/win.jpg)和两版迭代痕迹(倒数第二版/最终版本文件夹)。它专为高校C++课程设计场景打磨——没有花哨的网络同步,不依赖第三方渲染库,所有功能都在Qt Widgets框架内闭环实现。适合刚学完类封装、信号槽、资源系统、定时器机制的本科生,也适合作为Qt入门者第一个可交付的GUI项目脚手架。如果你正被“怎么把图片塞进按钮”“为什么QTimer::singleShot不触发”“.qrc改了但图标还是不显示”折磨,这份源码就是你缺的那张带注释的工程地图。
2. 工程结构解剖:从.pro到.qrc,看清Qt项目的四层骨架
一个能跑起来的Qt C++ GUI项目,绝不是把.cpp文件丢进IDE就完事。它有清晰的四层物理结构:项目元配置层(.pro)、资源注册层(.qrc)、UI声明层(.ui)、逻辑实现层(.h/.cpp)。这份打地鼠源码把这四层都摊开给你看,而且每层都有教学价值。
2.1.pro文件:不只是编译指令,更是模块依赖说明书
打开BeatMice.pro,你会看到这些关键行:
QT += core gui widgets greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = BeatMice TEMPLATE = app SOURCES += main.cpp\ mainwindow.cpp\ mouse.cpp\ background1.cpp\ mypushbutton.cpp\ qpushbutton.cpp\ youwin.cpp HEADERS += mainwindow.h\ mouse.h\ background1.h\ mypushbutton.h\ qpushbutton.h\ youwin.h FORMS += mainwindow.ui RESOURCES += image.qrc提示:
greaterThan(QT_MAJOR_VERSION, 4)这行不是摆设。它意味着此项目明确支持Qt5及以上(Qt6已移除widgets模块的隐式包含),若你在Qt6环境下直接编译会报错'QApplication' was not declared in this scope。这是课程设计常见的兼容性陷阱——老师用Qt5教学,但学生装了Qt6,必须手动补上#include <QApplication>并修改.pro中QT += widgets为QT += widgets gui。
RESOURCES += image.qrc是核心。它告诉qmake:“请把image.qrc里声明的所有资源,在编译时打包进可执行文件,并生成qrc_image.cpp供C++代码调用”。没有这行,:/images/start.png这种路径永远解析失败。
2.2.qrc资源文件:Qt的“虚拟文件系统”,路径即契约
image.qrc内容精简但致命:
<RCC> <qresource prefix="/images"> <file>start.png</file> <file>overr.png</file> <file>greenland.png</file> <file>win.jpg</file> <file>mouse.png</file> <file>hammerdown.png</file> <file>startt.png</file> <file>starttt.png</file> </qresource> </RCC>注意prefix="/images"——这是硬编码的命名空间。所有资源在代码中必须用:/images/start.png访问,少一个冒号或斜杠,QPixmap(":/images/start.png")返回空图。更隐蔽的坑是:.qrc里写的文件名必须和磁盘上完全一致(大小写敏感!)。源码包里有startt.png和starttt.png两个启动图,但.qrc只注册了startt.png。如果你误用:/images/starttt.png,程序不会崩溃,但QLabel::setPixmap()会静默失败,界面留白。
2.3mainwindow.ui:可视化布局的XML契约,不是所见即所得
双击mainwindow.ui,Qt Designer里看到的是拖拽好的按钮、标签、背景图。但真正决定行为的是其XML底层:
<widget class="QLabel" name="label_background"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>800</width> <height>600</height> </rect> </property> <property name="pixmap"> <pixmap resource="image.qrc">:/images/greenland.png</pixmap> </property> </widget>关键点在于<pixmap resource="image.qrc">——它显式绑定了资源文件。如果.qrc文件名被你改成res.qrc,这里不跟着改,背景图立刻消失。很多同学以为Designer里选了图片就万事大吉,其实这只是生成XML的快捷方式,真正的路径绑定在XML里。
2.4 自定义按钮类:mypushbutton.h/cpp——理解Qt事件重写的最小闭环
mypushbutton.h定义了一个继承QPushButton的类:
class MyPushButton : public QPushButton { Q_OBJECT public: explicit MyPushButton(QWidget *parent = nullptr); void zoom1(); // 放大动画 void zoom2(); // 缩小动画 signals: void clicked(); // 重声明信号,确保可用 };mypushbutton.cpp里实现了鼠标按下/释放的缩放效果:
void MyPushButton::mousePressEvent(QMouseEvent *e) { if (this->isEnabled()) { this->zoom1(); // 按下变大 } QPushButton::mousePressEvent(e); // 必须调用父类,否则click信号不发 } void MyPushButton::mouseReleaseEvent(QMouseEvent *e) { if (this->isEnabled()) { this->zoom2(); // 释放变小 } QPushButton::mouseReleaseEvent(e); }注意:
QPushButton::mousePressEvent(e)这一行绝不能删。Qt的信号机制依赖父类事件处理函数触发clicked()信号。删掉它,按钮看起来能按,但游戏逻辑里监听的connect(button, &MyPushButton::clicked, ...)永远不会执行——这是课程设计里最高频的“按钮失灵”原因。
3. 游戏核心逻辑拆解:定时器驱动的状态机与地鼠生命周期管理
打地鼠不是“随机显示图片+点击计分”这么简单。它本质是一个由QTimer驱动的有限状态机,每个地鼠(Mouse对象)有自己的生命周期(隐藏→出现→被击中→消失→冷却)。源码中mouse.h/cpp和background1.h/cpp共同构建了这个模型。
3.1Mouse类:地鼠不是静态图片,是带状态的实体
mouse.h定义了地鼠的核心属性:
class Mouse : public QObject { Q_OBJECT public: enum State { HIDDEN, APPEARING, VISIBLE, HIT, DISAPPEARING }; explicit Mouse(QObject *parent = nullptr); void appear(); // 进入APPEARING状态,触发动画 void hit(); // 进入HIT状态,播放音效、加分 void disappear(); // 进入DISAPPEARING状态,准备回收 State getState() const { return state; } QRect getRect() const { return rect; } // 当前碰撞检测区域 private slots: void onTimerTimeout(); // 状态切换主逻辑 private: State state; QRect rect; // 当前位置,用于碰撞检测 QTimer *timer; // 每个地鼠独立定时器,控制状态流转 int appearTime; // 出现持续时间(ms) int hitTime; // 被击中后停留时间(ms) };关键设计点:
- 每个地鼠独占一个
QTimer:避免全局定时器混乱。appearTime和hitTime可独立配置,实现不同难度(如高级模式地鼠出现时间更短)。 getRect()返回动态矩形:rect在appear()中根据随机位置计算,在onTimerTimeout()中随动画缩放实时更新。碰撞检测用QRect::contains(mousePos),比像素级检测高效且稳定。
3.2Background1类:游戏世界的调度中枢
background1.h声明了游戏主控类:
class Background1 : public QWidget { Q_OBJECT public: explicit Background1(QWidget *parent = nullptr); void startGame(); // 启动主循环 void stopGame(); // 停止所有地鼠 void resetScore(); // 重置分数 signals: void gameWin(); // 胜利信号 void gameLose(); // 失败信号 private slots: void updateGame(); // 主定时器槽函数,每100ms执行一次 private: QList<Mouse*> mice; // 所有地鼠指针列表 QTimer *gameTimer; // 全局游戏主定时器(100ms) int score; int timeLeft; int maxMice; // 当前关卡最大同时出现地鼠数 };updateGame()是灵魂函数:
void Background1::updateGame() { // 1. 随机选择空闲洞口,创建新地鼠(若未达maxMice) if (mice.count() < maxMice && QRandomGenerator::global()->bounded(100) < 15) { Mouse *m = new Mouse(this); m->appear(); mice.append(m); connect(m, &Mouse::hit, this, &Background1::onMouseHit); } // 2. 遍历所有地鼠,更新其状态 for (int i = mice.size() - 1; i >= 0; --i) { Mouse *m = mice[i]; if (m->getState() == Mouse::HIDDEN || m->getState() == Mouse::DISAPPEARING) { delete m; mice.removeAt(i); } } // 3. 更新倒计时与胜利判定 timeLeft--; if (timeLeft <= 0) { emit gameLose(); stopGame(); } if (score >= 100) { // 胜利阈值 emit gameWin(); stopGame(); } }逻辑说明:
updateGame()每100ms执行一次,它不直接控制地鼠动画帧,而是做三件事:①按概率生成新地鼠;②清理已结束生命周期的地鼠;③检查全局状态(时间、分数)。地鼠自身的动画由其内部timer驱动,实现关注点分离——这是Qt事件驱动编程的典型范式。
3.3 碰撞检测:mouseEvent里的精准坐标映射
Background1重写了mousePressEvent:
void Background1::mousePressEvent(QMouseEvent *e) { QPoint pos = e->pos(); for (Mouse *m : mice) { if (m->getState() == Mouse::VISIBLE && m->getRect().contains(pos)) { m->hit(); break; // 只击中第一个,避免多点误触 } } QWidget::mousePressEvent(e); }关键点:
e->pos()返回的是相对于Background1widget的坐标,而m->getRect()也是基于同一坐标系,无需转换。break保证单次点击只触发一次hit(),防止快速连点导致分数虚高。- 若地鼠处于
APPEARING或DISAPPEARING状态(动画中),getRect()可能还未覆盖洞口,此时contains()返回false,符合“只能击中完全出现的地鼠”的游戏规则。
4. 避坑指南:编译、资源、UI、逻辑四大高频翻车现场
这份源码经过真实课程设计场景验证,但新手照搬仍会踩坑。以下是我在实验室手把手调试时记录的5条血泪经验,每条都按“现象→原因→解决”给出可操作方案。
4.1 现象:编译通过,但启动黑屏,控制台输出QPixmap: Cannot create a QPixmap from
原因:.qrc中注册的图片路径与实际文件名不一致,或图片格式Qt不支持(如WebP)。源码包中starttt.png和startt.png易混淆,.qrc只含startt.png,但mainwindow.ui里可能引用了starttt.png。
解决:
- 在Qt Creator中右键
image.qrc→ “Open With” → “Qt Resource Editor”,确认所有文件名拼写、大小写完全匹配; - 检查
mainwindow.uiXML源码,搜索starttt.png,将其全部替换为startt.png; - 终极验证:在
main.cpp中加一行qDebug() << QPixmap(":/images/startt.png").isNull();,输出false才表示加载成功。
4.2 现象:按钮点击无反应,connect()后信号从未触发
原因:自定义按钮类MyPushButton的mousePressEvent中未调用QPushButton::mousePressEvent(e),导致Qt内部clicked()信号无法发射。
解决:打开mypushbutton.cpp,确认mousePressEvent和mouseReleaseEvent末尾均有QPushButton::xxx(e)调用。若使用Qt6,还需在mypushbutton.h中添加Q_SIGNALS:宏声明信号(Qt5可省略)。
4.3 现象:地鼠出现位置固定,总在左上角,或超出窗口边界
原因:Mouse::appear()中随机坐标计算错误。源码中常见写法是x = QRandomGenerator::global()->bounded(width());,但width()返回的是Mouse对象自身宽度(0),而非父窗口宽度。
解决:在Mouse构造时传入父窗口指针,或在appear()中通过QApplication::activeWindow()->width()获取。更稳妥做法是:在Background1中生成地鼠时,将窗口尺寸作为参数传入Mouse::appear(int w, int h)。
4.4 现象:游戏运行一段时间后崩溃,报QObject: Cannot create children for a parent that is in a different thread
原因:Mouse对象在非GUI线程(如QTimer的timeout槽)中被new,但未指定父对象。Qt要求所有QObject子类必须在GUI线程创建,或明确指定父对象以自动管理内存。
解决:在Background1::updateGame()中创建地鼠时,强制指定父对象:Mouse *m = new Mouse(this);。this是Background1,属于GUI线程,子对象自动归属其内存管理树。
4.5 现象:胜利界面youwin.h不显示,或显示后立即关闭
原因:youwin类继承自QWidget,但未调用show(),或show()后未调用exec()阻塞。课程设计中常误用QDialog的exec(),但QWidget需show()+raise()+activateWindow()三连。
解决:在Background1中连接胜利信号:
connect(this, &Background1::gameWin, [=]() { YouWin *win = new YouWin(this); // 父对象设为this,避免野指针 win->show(); win->raise(); win->activateWindow(); });并在YouWin构造函数末尾加this->setModal(true);确保模态。
5. 进阶技巧:三步让打地鼠从“能跑”升级为“可交付课程设计作品”
课程设计评分不止看功能,更看重工程规范、可维护性和教学展示性。以下三个技巧,是我带学生拿高分的实操方法,每一步都对应源码可改的具体位置。
5.1 技巧一:用QSettings持久化存储最高分,告别每次重启归零
当前源码分数存在内存变量score中,关闭程序即丢失。加入本地存储只需三步:
步骤1:在Background1.h中添加头文件和成员
#include <QSettings> // ... private: QSettings *settings; // 指向配置文件步骤2:在Background1构造函数中初始化
Background1::Background1(QWidget *parent) : QWidget(parent), settings(new QSettings("MyCompany", "BeatMice")) { // 读取历史最高分 int bestScore = settings->value("bestScore", 0).toInt(); qDebug() << "Loaded best score:" << bestScore; }步骤3:在onMouseHit()中更新并保存
void Background1::onMouseHit() { score += 10; // ...原有逻辑 if (score > settings->value("bestScore", 0).toInt()) { settings->setValue("bestScore", score); settings->sync(); // 立即写入磁盘 } }参数说明:
QSettings("MyCompany", "BeatMice")在Windows下生成HKEY_CURRENT_USER\Software\MyCompany\BeatMice注册表项,在Linux/macOS下生成~/.config/MyCompany/BeatMice.conf。sync()确保程序崩溃前数据不丢失。
5.2 技巧二:为地鼠添加音效,用QSound实现零依赖播放
Qt自带QSound类,无需额外库即可播放WAV。源码中mouse.cpp可扩展:
步骤1:在Mouse.h中添加音效成员
#include <QSound> // ... private: QSound *hitSound;步骤2:在Mouse构造函数中加载音效
Mouse::Mouse(QObject *parent) : QObject(parent), hitSound(new QSound(":/sounds/hit.wav", this)) { // 注意:WAV文件需提前放入.qrc,prefix设为"/sounds" }步骤3:在hit()中播放
void Mouse::hit() { if (hitSound && hitSound->isAvailable()) { hitSound->play(); // 异步播放,不阻塞主线程 } state = HIT; }注意:
QSound仅支持WAV格式,且采样率需为44.1kHz、16位PCM。可用Audacity免费转换。.qrc中需新增<qresource prefix="/sounds">节点。
5.3 技巧三:用QPainter动态绘制计时器数字,替代静态图片
当前倒计时用QLabel显示图片数字,修改困难。改用矢量绘制,支持任意字体、颜色、大小:
步骤1:在Background1.h中声明绘图事件
protected: void paintEvent(QPaintEvent *event) override;步骤2:在Background1.cpp中实现
void Background1::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); // 绘制倒计时数字(居中显示) QFont font("Arial", 24, QFont::Bold); painter.setFont(font); painter.setPen(Qt::red); QString timeStr = QString::number(timeLeft); QRect textRect = fontMetrics().boundingRect(timeStr); QPoint center = this->rect().center(); painter.drawText(center.x() - textRect.width()/2, center.y() + textRect.height()/2, timeStr); QWidget::paintEvent(event); }优势:数字样式完全可控;无需准备10张数字图片;
timeLeft变量变更后,重绘自动更新,逻辑更内聚。
从那以后我每次带课程设计,都强制学生在git commit前走一遍这三步:①用QSettings存最高分;②给地鼠加QSound音效;③把静态计时图换成QPainter绘制。不是为了炫技,而是让学生亲手触摸到“软件工程”的毛细血管——配置管理、资源抽象、UI渲染分离。当他们第一次看到自己改的红色数字在屏幕上跳动,听到“砰”的一声击中音效,再点开注册表看到bestScore=120,那种“我造出来了”的笃定感,比任何分数都真实。希望帮到你。
本文还有配套的精品资源,点击获取