简介:基于C++与Qt开发的飞机大战小游戏完整工程,面向计算机相关专业在校学生、教师及初级开发者,尤其适合作为课程设计、毕业设计或Qt入门练手项目。工程代码经过测试可正常运行,也支持在此基础上扩展新玩法,帮助读者理解Qt窗口系统、事件循环、定时器、碰撞检测及资源管理等核心机制。压缩包共58个文件,约33.2MB,以31张PNG素材图、7个CPP源文件、7个头文件为主体,同时包含JPG背景图、WAV音效、ICO图标、Qt工程文件(.pro)及资源编译文件(.qrc/.rcc),目录结构清晰,素材与逻辑代码分区明确。当前已有352人学习下载,适合需要动手实践或完成课设、毕设初期演示的读者,下载后可按README引导快速运行并二次开发。
1. 为什么是飞机大战:一份能直接编译的 C++/Qt 练手资源
飞机大战是 C++/Qt 学习者绕不开的经典项目:窗口、定时器、键盘、碰撞、贴图,一轮下来把 Qt 事件驱动的基本功全过了一遍。这份资源是完整跑通的成品,不是只有半截代码的演示残骸——源码、贴图、音效、资源文件都在,配好环境就能编译运行。
值得细看的东西有三处:MainScene 里 QTimer 驱动的游戏循环、HeroPlane 和 EnemyPlane 的移动与碰撞、bomb-1~7 那组逐帧爆炸动画。课程设计、Qt 进阶、或者想看看一个游戏怎么从零件拼成整机,这套代码都是现成的底子。
下载后先别急着双击 .pro——把 AirplaneWar.pro.user 删掉,那是原作者机器的 Qt 配置,留着八成要踩编译坑。下面按工程结构、启动链路、核心逻辑、避坑、进阶改造的顺序逐个拆。
2. 拆开 AirplaneWar.pro:二十多个文件按职责归位,重建工程只需四条命令
把 zip 解压后第一眼,二十多个文件铺了一桌,别慌。这套工程的组成其实就四拨:工程配置、游戏逻辑、全局参数、资源文件。搞清楚每一拨是干什么的,比急着点开 .pro 更重要——后面改代码、调参数、修 bug,找文件靠的就是这张地图。
2.1 先按职责分四类,文件清单不迷路
| 类别 | 文件 | 干什么用 |
|---|---|---|
| 工程配置 | AirplaneWar.pro、AirplaneWar.pro.user | qmake 工程描述;.pro.user 是个人 Qt 配置,跨机器必须删 |
| 启动与主窗口 | main.cpp、mainscene.h/cpp | 程序入口 + 游戏循环调度 |
| 游戏对象 | map.h/cpp、heroplane.h/cpp、enemyplane.h/cpp、bullet.h/cpp、bomb.h | 滚动背景、英雄机、敌机、子弹、爆炸动画 |
| 全局与资源 | config.h、res.qrc、plane.rcc | 速度与窗口参数;图片音效的资源索引 |
对号入座后你会发现一个细节:bomb.h 没有对应的 bomb.cpp。爆炸动画这个类太小——一张位置、一个帧序号、一个「播完没播完」的标记——直接内联在头文件里就够了,这是 Qt 小项目里很常见的写法,不是漏了文件。
2.2 .pro 文件:qmake 怎么知道该编译谁
AirplaneWar.pro 是纯文本,用记事本就能打开。它的作用是告诉 qmake:要哪些模块、编译哪些源文件、打包哪些资源。
QT += core gui widgets TARGET = AirplaneWar TEMPLATE = app SOURCES += main.cpp \ mainscene.cpp \ map.cpp \ heroplane.cpp \ enemyplane.cpp \ bullet.cpp HEADERS += mainscene.h \ map.h \ heroplane.h \ enemyplane.h \ bullet.h \ bomb.h \ config.h RESOURCES += res.qrcQT += widgets 这一行是重点,Qt5 里 QWidget 搬到了 widgets 模块,少了它 mainscene.h 会直接报 QWidget 找不到。SOURCES 里没有 bomb.cpp,对应前面说的 bomb.h 内联实现。RESOURCES 指向 res.qrc,构建时 rcc 编译器会把图片音效编成二进制塞进 exe,所以发布时只需要一个 exe,不用带一堆 png。
另一个文件 AirplaneWar.pro.user 不在上面的清单里,因为它是 Qt Creator 自动生成的个人配置,记录的是原作者机器的 kit、编译器路径、qmake 版本。别人发给你的工程,这个文件十有八九是坑——我拿到任何 Qt 工程的第一件事就是删它。
2.3 config.h:改手感参数先来这
整个游戏的「手感」几乎集中在 config.h 里。窗口多大、飞机多快、敌机多密,全在这:
// config.h —— 全局手感参数 #ifndef CONFIG_H #define CONFIG_H #define GAME_WIDTH 480 // 窗口宽 #define GAME_HEIGHT 700 // 窗口高 #define GAME_INTERVAL 15 // 游戏定时器,单位毫秒 #define HERO_SPEED 10 // 英雄机每帧移动像素 #define BULLET_SPEED 8 // 子弹每帧上移像素 #define ENEMY_SPEED 3 // 敌机每帧下移像素 #define ENEMY_SPAWN_INTERVAL 30 // 每 30 帧生成一架敌机 #endif参数怎么调,给一张参照表:
| 参数 | 默认值 | 影响 | 调法 |
|---|---|---|---|
| GAME_INTERVAL | 15 | 帧率约 66 帧/秒 | 改 10 更跟手但费 CPU,改 30 会肉眼可见卡顿 |
| HERO_SPEED | 10 | 英雄机灵敏度 | 感觉漂就降到 7,嫌肉就提到 12 |
| BULLET_SPEED | 8 | 打击手感 | 小于敌机速度的两倍时,子弹会追不上敌机 |
| ENEMY_SPAWN_INTERVAL | 30 | 敌机密度 | 越小越难,改成 10 就是地狱模式 |
2.4 res.qrc 与 plane.rcc:资源路径的两条路
res.qrc 也是文本,里面是图片音效的索引清单:
<RCC> <qresource prefix="/"> <file>img-plane_1.png</file> <file>hero.png</file> <file>bg.wav</file> </qresource> </RCC>代码里加载图片用的是QPixmap(":/img-plane_1.png"),冒号加斜杠是 Qt 资源系统的固定写法。如果你看到代码里写:/res/img-plane_1.png而 qrc 里没有prefix="/res",加载必然失败——这是后面 5.2 节黑屏问题的来源之一。
plane.rcc 是 rcc --binary 预编译出来的二进制资源包,属于第二条路:要么在 .pro 里直接把 .rcc 加进 RESOURCES 链接进去,要么运行时用 QResource::registerResource() 动态加载。对这份资源来说,你平时改图片只动 res.qrc 就行,不用碰 .rcc。
2.5 命令行重建:四条命令从零编译
不想用 Qt Creator 的话,纯命令行也能把这套工程跑起来:
# 1) 删掉别人的 Qt 配置,强制重新选择 kit rm -f AirplaneWar.pro.user # 2) 核对 qmake 跟你想要的环境是一套 qmake --version # 期望输出类似: QMake version 3.1, Using Qt version 5.15.2 # 3) 生成 Makefile 并编译 qmake AirplaneWar.pro mingw32-make -j4 # 如果是 MinGW 套件 # 或者(nmake 需要先进入 VS 的开发者命令行) nmake # 如果是 MSVC 套件步骤 2 是最容易被忽略的——同一个 Qt 5.15.2 就有 msvc2019_64、mingw73_64 好几个包,qmake 来自哪个包,就必须配哪个编译器。步骤 3 里 mingw32-make 和 nmake 不能混用,混了会刷出一堆语法不认识的错误。编译完的可执行文件在 debug 或 release 子目录下,取决于 qmake 的 CONFIG 配置。Windows 上不想敲 rm 命令,直接去资源管理器里把 .pro.user 删掉也一样。
3. 从 main.cpp 到游戏循环:QTimer、事件驱动与绘制顺序
这一章把程序从双击启动到画出第一帧的过程走一遍。整条链路只有四个环节:main 启动事件循环 → MainScene 构造时启动定时器 → 定时器每 15ms 触发 updateGame → update() 请求重绘,最终在 paintEvent 里把画面画出来。看懂这条链,你就知道该在哪一环插自己的逻辑。
3.1 main.cpp 只有十行:事件循环是怎么转起来的
#include "mainscene.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); // 每个 Qt 程序有且只有一个 QApplication MainScene w; w.show(); // 只负责显示,不阻塞 return a.exec(); // 进入事件循环,窗口关闭才返回 }a.exec() 不会立刻返回,它内部是一个持续分发事件的消息泵:键盘按键、鼠标移动、定时器超时,全都在这里排队分发。这也是 Qt 程序跟传统 C++ 控制台程序最大的思维差异——不是「主函数控制一切」,而是「事件驱动」。
有些同学拿到 Qt 想用 while(1) 包游戏循环,结果按键没反应、窗口一卡一卡的,原因就是死循环把事件循环堵死了。QTimer 每 15ms 从事件循环里取一次 timeout 事件,该画图画图,该响应键盘响应键盘,这才是 Qt 游戏的正确姿势。
3.2 15ms 一帧:QTimer 是游戏的心脏
MainScene::MainScene(QWidget *parent) : QWidget(parent) { setFixedSize(GAME_WIDTH, GAME_HEIGHT); // 固定窗口,防止布局乱套 m_timer.setInterval(GAME_INTERVAL); // config.h 里的 15ms connect(&m_timer, &QTimer::timeout, this, &MainScene::updateGame); // 每帧把游戏逻辑推进一步 m_timer.start(); m_hero.init(); // 英雄机摆到初始位置 }setInterval(15) 表示每 15ms 触发一次 timeout,一秒约 66 帧。要注意 setInterval 在 start 之前设置才有效,运行中改间隔要 restart() 重新启动。课程设计级别的 2D 游戏,15ms 是经典取值:再小到 5ms,帧率上去了但 CPU 占用明显发热;再大到 30ms,33 帧的刷新肉眼可见的掉帧。
也有人问为什么不用 QGraphicsView。这套工程走的是 QWidget + QTimer + paintEvent 路线,每一帧的绘制权完全握在自己手里,代码量少、逻辑直白。QGraphicsView 适合大量可交互物体,但初学者容易在 item 坐标管理和碰撞处理上绕晕——课设答辩时,前者讲起来反而更清楚。
3.3 updateGame:一帧里按顺序干七件事
void MainScene::updateGame() { m_map.update(); // 背景滚动 m_hero.update(); // 按键状态驱动移动 spawnEnemy(); // 按帧计数生成敌机 updateEnemies(); // 敌机下移 + 出屏移除 updateBullets(); // 子弹上移 + 出屏移除 checkCollision(); // 子弹打敌机、敌机撞英雄机 updateBombs(); // 爆炸动画帧推进 update(); // 请求重绘,真正的绘制在 paintEvent }注意最后一行 update() 不是立即画,它只是给窗口打一个「脏标记」。Qt 会把同一帧内的多次 update() 合并成一次 paintEvent,所以哪怕你在一个事件里调两次 update,也不会画两遍。逻辑和绘制分离是这套代码最值得学的习惯——所有状态更新在 updateGame 里完成,paintEvent 只负责把当前状态画出来。
3.4 paintEvent 的绘制顺序:后画的盖先画的
void MainScene::paintEvent(QPaintEvent *) { QPainter painter(this); m_map.draw(painter); // 最底层:滚动背景 for (auto &e : m_enemies) e.draw(painter); // 敌机 for (auto &b : m_bullets) b.draw(painter); // 子弹 m_hero.draw(painter); // 英雄机压在敌机之上 for (auto &b : m_bombs) b.draw(painter); // 爆炸特效置顶 }绘制顺序就是视觉层级:背景必须第一个画,爆炸特效放最后,命中瞬间的反馈才明显。QPainter 不需要手动 delete,出了 paintEvent 作用域自动结束绘制,begin/end 也不用手动调。如果想做「敌机从英雄机头顶飞过」的纵深效果,可以把敌机拆成上层下层两组,画两次。另外,不要在 paintEvent 里做碰撞和移动——那会让绘制和逻辑耦合,paintEvent 一慢,整帧全慢。
4. 键盘状态表、碰撞盒与对象生命周期:核心逻辑改这三处
游戏能不能玩、好不好玩,全看这一章的三个点:按键怎么响应、碰撞判定准不准、对象怎么回收。这也是答辩时老师最爱问的地方。
4.1 按键状态表:按住方向键不掉帧地持续移动
// mainscene.h 里声明一个按键状态表 // QSet<int> m_pressedKeys; void MainScene::keyPressEvent(QKeyEvent *ev) { m_pressedKeys.insert(ev->key()); // 按下记下来,但不直接移动 } void MainScene::keyReleaseEvent(QKeyEvent *ev) { m_pressedKeys.remove(ev->key()); // 松开才清除 } void MainScene::updateHero() { if (m_pressedKeys.contains(Qt::Key_Left)) m_hero.moveLeft(); if (m_pressedKeys.contains(Qt::Key_Right)) m_hero.moveRight(); if (m_pressedKeys.contains(Qt::Key_Up)) m_hero.moveUp(); if (m_pressedKeys.contains(Qt::Key_Down)) m_hero.moveDown(); }为什么不在 keyPressEvent 里直接移动飞机?因为按住方向键时系统只发一次按下事件,不缓存状态飞机就只动一格。用 QSet 缓存按键,在定时器回调里每帧读取,才能实现「按住持续移动」。heroplane 的 moveLeft/Right 内部还要做边界钳制:
void HeroPlane::moveLeft() { m_x -= HERO_SPEED; if (m_x < 0) m_x = 0; // 左边界撞墙 } void HeroPlane::moveRight() { m_x += HERO_SPEED; if (m_x > GAME_WIDTH - m_width) // 右边界,不能半身出屏 m_x = GAME_WIDTH - m_width; }上下方向同理,钳制值换成 GAME_HEIGHT 相关的那组。这里的 m_width 是英雄机贴图的实际宽度,直接用窗口宽度减它,是为了让飞机贴边时还留半身在屏幕内,手感比完全压到边缘好。
4.2 子弹连发节流:每 5 帧一发的限流参数
void MainScene::fireBullet() { if (m_fireCount < 5) // 还没攒够 5 帧,先憋着 { ++m_fireCount; return; } m_fireCount = 0; Bullet b; b.init(m_hero.rect().center()); // 从英雄机中心出膛 m_bullets.append(b); }fireBullet 每帧都会被 updateGame 调用,5 帧一憋就是约 13 发/秒,这是飞行射击类小游戏常见的连发密度。改成 2 就变机关枪,改成 10 手感会明显发绵。资源列表里有 hero_bullet_7.png,对应英雄机专属子弹贴图,init 时把这张图塞给 Bullet 就行。发射位置用 rect().center() 而不是左上角,是为了让子弹从机身中部而不是从机头一侧冒出来,看着不歪。
4.3 碰撞检测:把贴图边界缩一圈再相交判断
void MainScene::checkCollision() { for (int i = m_bullets.size() - 1; i >= 0; --i) { for (int j = m_enemies.size() - 1; j >= 0; --j) { // 贴图四周往往有透明边,碰撞盒向内缩一圈更公平 QRect bulletBox = m_bullets[i].rect().adjusted(4, 4, -4, -4); QRect enemyBox = m_enemies[j].rect().adjusted(8, 8, -8, -8); if (bulletBox.intersects(enemyBox)) { makeBomb(enemyBox); // 命中位置生成爆炸动画 m_enemies.removeAt(j); m_bullets.removeAt(i); break; } } } }QRect::adjusted(4,4,-4,-4) 表示四条边各向内缩 4 像素。为什么敌机缩 8 像素而子弹只缩 4?因为敌机贴图翅膀展开,透明区域大。调参经验是:判定太松,子弹没碰到就炸,看着假;判定太紧,子弹直接穿模。原则是每次 2px 地加,调到「看着该中的都能中」就停。反向遍历是必须的——removeAt 会把后面的元素往前搬,正向遍历会跳过下一个没检查的元素。
4.4 敌机与子弹的生命周期:反向遍历移除
void MainScene::spawnEnemy() { if (m_spawnCount < ENEMY_SPAWN_INTERVAL) { ++m_spawnCount; return; } m_spawnCount = 0; EnemyPlane e; QStringList pilots = {":/img-plane_1.png", ":/img-plane_2.png", ":/img-plane_3.png"}; e.init(pilots[rand() % pilots.size()], // 随机挑一张敌机皮肤 rand() % (GAME_WIDTH - e.width())); // 随机出生横坐标 m_enemies.append(e); }生成逻辑要点:随机 x 必须用 GAME_WIDTH 减掉敌机自身宽度,否则会有一截机身生成在屏幕外。更新和回收逻辑是配套的:
void MainScene::updateEnemies() { for (int i = m_enemies.size() - 1; i >= 0; --i) { m_enemies[i].update(); // 每帧下移 ENEMY_SPEED if (m_enemies[i].rect().top() > GAME_HEIGHT) m_enemies.removeAt(i); // 飞出屏幕下沿就回收 } }敌机、子弹、爆炸全部按值存放在 QVector 里,不 new 不 delete,removeAt 即回收,容器自动管理内存。这套「值对象 + 反向遍历 + 出界移除」的打法,就是这份资源里最值得抄的一段。
爆炸动画 bomb.h 用内联方式封装了逐帧切换逻辑:
// bomb.h 内联实现的爆炸动画 class Bomb { public: void init(const QPoint &pos) { m_pos = pos; m_index = 0; m_interval = 0; m_done = false; } void update() { ++m_interval; if (m_interval % 3 == 0 && !m_done) ++m_index; // 每 3 帧切下一帧贴图 if (m_index >= 7) m_done = true; // bomb-1~7 共 7 帧,播完标记 } bool isDone() const { return m_done; } void draw(QPainter &p) { if (!m_done) p.drawPixmap(m_pos, m_frames[m_index]); } private: QPixmap m_frames[7]; // 构造时一次性加载 bomb-1~7.png QPoint m_pos; int m_index{0}; int m_interval{0}; bool m_done{false}; };注意这里把 7 帧 QPixmap 提前加载进数组,而不是每帧 draw 时临时拼路径构造。QString 拼接加文件读取放在绘制路径里,是新手最常见的性能杀手。爆炸播完要立刻移除:
void MainScene::updateBombs() { for (int i = m_bombs.size() - 1; i >= 0; --i) { m_bombs[i].update(); if (m_bombs[i].isDone()) m_bombs.removeAt(i); // 播完就扔掉,别让容器膨胀 } }5. 避坑记录:拿到工程后最常见的五个翻车现场
下面这五条,是我在实验室、宿舍、公司电脑上前前后后编译同类 Qt 工程时反复撞见的,按出现频率排序。每一条都是「现象 → 原因 → 解决」的格式,对号入座。
5.1 dependent 开头的一长串路径报错:Kit 选错,工程根本编不了
现象:打开工程直接构建,报:-1: error: dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\QtWidgets' not found,一长串 ..\ 看着像路径穿越。
原因:AirplaneWar.pro.user 里记录的 kit 指向原作者机器上的 Qt 5.15.2 msvc2019_64 安装目录,你的机器没有这个路径;或者你本地装的是 MinGW 编译器,但 Qt 包下载成了 MSVC 版,编译器对不上。
解决:第一步删掉 AirplaneWar.pro.user,重新用 Qt Creator 打开工程,让它重新选 kit。第二步确认套件三件套对齐:Qt 包的版本标识(msvc2019_64 还是 mingw73_64)、编译器(MSVC 还是 MinGW)、调试器要来自同一套。msvc2019_64 的包必须配 VS2019 的 MSVC 编译器,单独装个 MinGW 是编不了的。如果本地没装 Qt,去清华开源镜像站下载跟编译器匹配的 5.15.x 包,装完回来重新配置。
5.2 画面全黑或飞机消失:QPixmap::isNull 教你看资源路径
现象:程序能跑,窗口也有,但背景和飞机全都不显示,或者只有部分贴图显示。
原因:最常见有两个。一是 res.qrc 里带了 prefix 而代码没带,例如文件实际路径是:/res/img-plane_1.png,代码里读的却是:/img-plane_1.png;二是文件名大小写不一致,Windows 上不敏感,一换到 Linux 或 macOS 立刻暴露。
解决:双击 res.qrc 展开每个条目,看它带不带前缀。然后在加载贴图的代码后面临时插一行调试语句:
qDebug() << "hero.png is null?" << m_hero.getPixmap().isNull();哪张返回 true,就说明哪张的路径没对上。我一般还会点开图片文件,把实际文件名和代码里的字符串逐字对一遍——空格、连字符、下划线都容易抄错。
5.3 关窗口时程序崩溃:定时器还在,对象却先走了
现象:点关闭按钮后程序闪退,Qt Creator 提示The program has unexpectedly finished。
原因:主窗口析构时 QTimer 还没来得及停,某次 timeout 回调访问了正在析构的成员;或者代码里敌机、子弹用了裸指针 new 出来,析构时又没人统一 delete。
解决:MainScene 的析构函数里第一行写 m_timer.stop()。关窗口先停心脏,再让它自然析构:
MainScene::~MainScene() { m_timer.stop(); // 先停定时器,再让成员自然析构 }对象管理上,保持这份资源现在的做法——敌机、子弹、爆炸都用 QVector 按值存放,removeAt 移除。只要不手动 new/delete,九成的内存崩溃都能直接消失。
5.4 音效不响:bomb.wav 的编码比路径更坑
现象:别的一切正常,bg.wav 和 bomb.wav 一点声音没有;或者某个 wav 响、另一个不响。
原因:QSound 对 wav 编码有硬性要求,必须是 PCM 编码的 16-bit 格式。很多工具导出的 8-bit 或压缩格式 wav,QSound 会直接静音。两个 wav 一个响一个不响,往往就是两个文件的编码不同。
解决:先qDebug() << QSound::isAvailable();确认平台支持,然后用 Audacity 把两个 wav 统一转成 16-bit PCM、22050Hz、单声道,替换掉 res.qrc 里对应的文件重新编译。想在 Qt 5.15 里更省心的话,可以换成 QSoundEffect,接口差别不大,对编码的容忍度更好。
5.5 越玩越卡:爆炸帧每帧重复建 QPixmap 的代价
现象:前 30 秒流畅,一两分钟后明显掉帧,内存占用只涨不降。
原因:两个典型。一是爆炸动画没有移除条件,m_bombs 里堆积一堆播完的帧;二是 draw 里临时拼路径构造 QPixmap,QString 拼接加文件读取每帧都在发生。
解决:按 4.4 的方式,isDone 就 removeAt;QPixmap 在对象构造时一次性加载成成员数组,绘制期只做 drawPixmap。做完这两步,这个规模的 2D 游戏在 15ms 定时器下,全程顶到 60 帧不往下掉。
6. 进阶改造:计分关卡、暂停与 FPS 验证的三个小动作
把这套资源从「能玩」变成「能演示」,我一般会加三个小功能,每个改动量都不大,但对答辩和演示效果提升明显。
6.1 计分驱动的关卡推进:把五张背景图用起来
资源里有 img_bg_level_1~5.jpg 五张背景图,这就是现成的关卡位。按分数切关卡:
void MainScene::updateLevel() { int level = m_score / 2000 + 1; // 每 2000 分升一级 if (level > 5) level = 5; // 资源里只有 5 张背景 if (level == m_level) return; m_level = level; m_map.setBg(QString(":/img_bg_level_%1.jpg").arg(m_level)); m_spawnInterval = qMax(10, ENEMY_SPAWN_INTERVAL - (m_level - 1) * 3); }QString::arg 拼出背景资源名,正好对上 qrc 里的五张 jpg。敌机生成间隔从 30 帧递减到 10 帧封底,第 5 关的极限密度不会把难度顶穿,这就形成了一条平滑的难度曲线。
6.2 一键暂停:Escape 控制定时器,顺便清空按键状态
void MainScene::keyPressEvent(QKeyEvent *ev) { if (ev->key() == Qt::Key_Escape) { // 定时器停 = 暂停,重新 start = 继续 m_timer.isActive() ? m_timer.stop() : m_timer.start(); m_pressedKeys.clear(); // 关键: 清空按键缓存 return; } m_pressedKeys.insert(ev->key()); }有个坑必须提醒:暂停期间玩家可能还按着方向键,恢复的瞬间按键状态还在,英雄机会自己飘一段。所以恢复前强制 clear 一次按键表。这个细节不做,现场演示时很容易被当成 bug 提问。
6.3 把 FPS 显示到标题栏:性能优化前先量化
改性能前先量化,不然不知道改完有没有效果。用 QElapsedTimer 统计真实帧率:
// mainscene.h 里加两个成员 int m_frames = 0; QElapsedTimer m_clock; // 构造函数里 m_clock.start(); // paintEvent 末尾 ++m_frames; if (m_clock.elapsed() >= 1000) { setWindowTitle(QString("飞机大战 - %1 FPS").arg(m_frames)); m_frames = 0; m_clock.restart(); }这比用 QTimer 的 interval 推算靠谱得多——interval 只是理论上限,真实帧率会被绘制耗时和系统调度拉低。标题栏常驻 FPS,答辩时直接对着数字讲「我优化后帧率从多少到多少」,比空口说流畅有说服力得多。
从那以后我每次拿到别人的 Qt 工程,第一件事永远是删掉 .pro.user、核对 qmake 版本、再看一眼 res.qrc 的前缀,这三步走完,八成问题在编译阶段就消失了;剩下两成就按第 5 章的清单逐条对。希望帮到你。
本文还有配套的精品资源,点击获取