
简介面向C课程设计、期末大作业与毕业设计场景的高分项目资源基于C与Qt实现经典俄罗斯方块游戏。代码注释完整、模块划分清晰包含可运行的Qt工程与项目文档下载解压并配置Qt环境后即可运行适合直接提交或在此基础上二次开发。资源共42个文件涵盖头文件与源文件、Qt界面相关的ui与json配置、动态链接库与静态库以及docx项目手册和txt说明压缩包约11.87MB。文件层次覆盖游戏逻辑、界面绘制、构建配置与文档说明其中ui与json负责界面及配置lib、dll等支撑编译运行docx手册用于快速了解设计与提交说明。已有316人学习或下载。该项目的突出价值在于以98分获导师认可完整展示了俄罗斯方块从游戏状态管理、方块生成与旋转、碰撞检测、消行计分到界面绘制的全过程。关键函数与算法均配有注释新手也能跟着代码梳理实现思路项目文档则补充设计说明与运行方式为答辩和汇报提供支撑。源码目录和文档命名清晰便于按需查阅对应模块既可作为学习C与Qt协同开发的脱手范例也是课程设计或毕业设计可快速上手的高分参考模板。1. 选俄罗斯方块作为 C/QT 大作业为什么它是高分题库里的常青树俄罗斯方块在 C 课程大作业里属于“看着不新鲜、做完不亏”的类型规则全世界都知道但要把七种方块、旋转、碰撞、消行、计分、等级加速这些要素写干净恰好覆盖了类设计、数组与内存管理、QT 的信号槽与事件机制。比学生管理系统多一层实时交互比射击游戏少一堆素材和物理计算工作量通常两周内可控代码量能控制在 1500 行左右评阅老师也容易看出你的设计思路。这个选题适合既要保成绩又想冲优的同学代码不难但设计空间足够撑起一份高分项目的课程文档。下面按我自己的落地顺序讲先数据结构再界面渲染再游戏逻辑最后把文档和答辩一起收尾。2. 先把数据结构立住方块表示、旋转矩阵与碰撞检测怎么写2.1 三个类拆清楚Tetromino、Board、GameController 各自只干一件事写俄罗斯方块最容易翻车的写法是把所有变量和函数全部塞进 MainWindow界面逻辑和游戏逻辑揉成一团。课程设计阶段我一般会拆三个纯逻辑类加一个界面类分工如下Tetromino描述当前方块的形状、朝向和棋盘坐标提供旋转和取格子的接口。Board负责棋盘内部用一个二维数组 grid[ROW][COL] 记录已落定方块的颜色编号提供碰撞检测、落定合并和消行接口。GameController持有 Board、当前方块、下一块、分数和等级实现下落、左右移动、旋转、硬降和游戏状态切换。这个拆分在评阅时非常加分因为老师几乎必问“如果我要加一种五格方块你要改哪些地方”答案是只改 TetrominoBoard 和 GameController 都不动。这就是分层设计的现场演示比答辩时背概念管用得多。先看 Tetromino 的头文件// tetromino.h —— 七种方块统一用 4x4 模板存储 #ifndef TETROMINO_H #define TETROMINO_H #include QVector #include QPoint class Tetromino { public: enum Shape { NONE, I, O, T, S, Z, J, L }; explicit Tetromino(Shape s); void rotate(); // 顺时针旋转 90 度 void setPos(int x, int y); // 方块左上角在棋盘上的坐标 QPoint pos() const { return m_pos; } QVectorQPoint cells() const; // 返回模板中非空格子的相对坐标 Shape shape() const { return m_shape; } private: Shape m_shape; int m_matrix[4][4]; // 1 表示该格有方块 QPoint m_pos; }; #endif这里用 QVector 和 QPoint 是因为后面要直接把这些坐标映射到 QPainter 绘制省一次转换。如果你想让文档更偏“纯 C”换成 std::arraystd::arrayint,4,4 和自带的坐标结构也一样不影响其他类。构造时每种形状自己填充 m_matrix比如 T 字块在模板中的形态是 (1,0)、(0,1)、(1,1)、(2,1) 四个格子为 1。用 4x4 而不是直接存坐标数组是为了把旋转变成纯矩阵运算不引入额外算法。2.2 用 4×4 模板表示七种方块旋转就是“转置 水平翻转”矩阵旋转是这里最值得写在文档里的细节。顺时针旋转 90 度等价于先对矩阵做转置再把每一行左右翻转写成代码只有四行// tetromino.cpp —— 旋转矩阵的核心逻辑 #include tetromino.h #include cstring void Tetromino::rotate() { if (m_shape O) return; // 正方形的 O 方块旋转后不变直接跳过 int temp[4][4]; std::memset(temp, 0, sizeof(temp)); for (int i 0; i 4; i) { for (int j 0; j 4; j) { temp[i][3 - j] m_matrix[j][i]; // 转置后水平翻转 } } std::memcpy(m_matrix, temp, sizeof(m_matrix)); }新手最容易写错的是下标写成 temp[3 - i][j] 或者 temp[j][i] 不带翻转结果旋转几次后图形发生漂移。验证方法很简单拿一个 I 方块在纸上画一遍旋转后应该是横躺状态而且旋转中心不偏移。这也是用 4x4 而不是 3x3 的原因3x3 矩阵放不下水平的 I 方块4 格强行表示会造成旋转中心偏移S、Z 方块转起来也会有错位感。O 方块在构造函数里就把矩阵填成 2x2 的田字放在 4x4 的左上角旋转时直接 return避免无意义的矩阵拷贝。文档里写清楚这个特判评阅老师会认为你考虑过边界情况。2.3 碰撞检测的“先放后查”策略为什么比逐格判断更不容易出 bug碰撞检测是整个游戏正确性的地基。我见过不少实现给每个方块维护四条边移动时逐边判断是否出界旋转后再重新计算边界表。这个思路的问题是方块状态一变边界数据就要同步多维护一份状态就多一个翻车点。在 10 列 20 行的固定棋盘上最简单可靠的是“先放后查”把方块挪到目标位置然后统一检查越界和重叠。// board.cpp —— 碰撞检测先尝试放置冲突则拒绝该动作 bool Board::canPlace(const Tetromino t, int newX, int newY) const { const QVectorQPoint cells t.cells(); for (const QPoint c : cells) { int x newX c.x(); int y newY c.y(); if (x 0 || x COLS) return false; // 超出左右边界 if (y ROWS) return false; // 超出底部 if (y 0 m_grid[y][x] ! 0) return false; // 与已固定方块重叠 // 注意y 0 时允许存在 } return true; }m_grid 的行索引是 y列索引是 x存的不是布尔值而是颜色编号这样后面绘制和消行都能复用。canPlace 只做查询不修改数据左移、右移、下落、旋转、硬降全部调它判定逻辑只有一份。y 0 放行是关键细节方块生成时有一部分可能还在棋盘上方之外如果把 y 0 也判成碰撞开局第一块就放不下来。这个细节容易被忽略我在测试时踩过屏幕上方明明还有空间方块却卡在半空。有了 canPlace 之后落定和合并的逻辑就顺了。GameController 的 tick 是整个游戏的主循环核心// gamecontroller.cpp —— 每次定时器触发执行一次下落 void GameController::tick() { if (m_state ! PLAYING) return; if (canMove(0, 1)) { m_current.setPos(m_current.pos().x(), m_current.pos().y() 1); } else { merge(); // 当前方块写入棋盘 int n board.clearLines(); if (n 0) linesCleared(n); spawnNext(); // 从预览队列取出下一块 if (!canPlace(m_current, m_current.pos().x(), m_current.pos().y())) gameOver(); // 新方块放不下判负 } }canMove 内部就是调 board.canPlace而 spawnNext 里同样用 canPlace 判断游戏是否结束也就是“生成时预判”的结束方式。这样整个下落流程的所有动作都收口到同一个碰撞函数逻辑只有一条路径排查问题时只需要看 canPlace 一个函数。3. 用 QT Widgets 把棋盘画出来QPainter、QTimer 与键盘事件三步走3.1 工程文件与源码清单一个 QWidget 项目需要哪几个文件界面层我用 QMainWindow 作为主窗口中央放一个自绘的 GameWidget右侧放信息面板。新建项目时在 Qt Creator 里直接选 QWidget Application会自动生成 .pro 或 CMakeLists。这里建议装 Qt 5.15.2 LTS而不是追最新版原因很现实老师机器上的 QT 版本大概率是 5.x网上能搜到的答案也集中在 5.15遇到问题好查。课程设计不推荐用 VSCode 配置 C 环境来写 QTCMake 加套件配置会吃掉你一大半时间老老实实用 Qt Creator 带 qmake 的模板五分钟就能把空项目跑起来。源码文件清单大致如下文件职责关键成员main.cpp创建 QApplication 和主窗口无gamewidget.h/.cpp绘制棋盘、接收按键、驱动定时器paintEvent、keyPressEvent、QTimerboard.h/.cpp棋盘数据、碰撞检测、消行grid、canPlace、clearLinestetromino.h/.cpp方块形状与旋转matrix、rotate、cellsgamecontroller.h/.cpp游戏规则、分数、等级、状态tick、move、rotate、hardDropGameWidget 头文件的骨架长这样// gamewidget.h —— 界面层只做三件事绘制、收按键、驱动定时器 #ifndef GAMEWIDGET_H #define GAMEWIDGET_H #include QWidget #include QTimer #include gamecontroller.h class GameWidget : public QWidget { Q_OBJECT public: explicit GameWidget(QWidget *parent nullptr); void start(); // 开始游戏并启动定时器 protected: void paintEvent(QPaintEvent *e) override; void keyPressEvent(QKeyEvent *e) override; private: GameController m_game; QTimer m_timer; }; #endif注意 m_timer 这里作为成员对象而不是指针省去手动 delete。QTimer 的 timeout 信号用 lambda 槽连接可以少在头文件里声明一个槽函数课程代码也更紧凑。3.2 paintEvent 绘制棋盘与方块像素坐标换算的 CELL_SIZE 参数绘制只写在 paintEvent 里这是 QT 的规矩不要在别的地方直接画。每次游戏状态变化后调用 update()QT 会合并重绘请求并在合适的时机触发 paintEvent。绘制顺序是背景、已固定方块、当前活动方块一层层往上叠。// gamewidget.cpp —— paintEvent 是唯一绘制入口 void GameWidget::paintEvent(QPaintEvent *) { QPainter painter(this); const int CELL 30; // 每格 30x30 像素 const int ROWS 20, COLS 10; // 1. 画背景 painter.fillRect(0, 0, COLS * CELL, ROWS * CELL, QColor(0x1e, 0x1e, 0x1e)); // 2. 画已固定方块 for (int r 0; r ROWS; r) { for (int c 0; c COLS; c) { int id m_game.board().cell(r, c); if (id ! 0) { painter.fillRect(c * CELL, r * CELL, CELL - 1, CELL - 1, m_game.colorOf(id)); } } } // 3. 画当前活动方块 Tetromino cur m_game.current(); QColor clr m_game.colorOf(cur.shape()); for (const QPoint p : cur.cells()) { int px (cur.pos().x() p.x()) * CELL; int py (cur.pos().y() p.y()) * CELL; if (py 0) // 顶部以上的部分不画 painter.fillRect(px, py, CELL - 1, CELL - 1, clr); } }CELL_SIZE 的选取是个体验问题10 列 20 行的棋盘用 30 像素画布就是 300x600右侧再留 150 像素放信息和下一个方块预览主窗口固定成 480x640 比较协调。用 24 会让窗口显得局促演示时分数看不清用 40 又太大笔记本屏幕上放不下。格子填充减 1 像素是为了自然露出背景色当格线不额外画线的省力做法比 drawLine 画 11 条竖线简单且不会出现 1 像素错位。3.3 QTimer 驱动下落与 keyPressEvent 响应按键主循环怎么写游戏主循环不用 while(true)那会卡死 QT 的事件循环。正确姿势是 QTimer 周期性触发每次触发做一次下落逻辑然后调用 update() 请求重绘。// gamewidget.cpp —— 启动游戏、连接定时器和键盘 void GameWidget::start() { setFocusPolicy(Qt::StrongFocus); // 必须设置否则键盘事件进不来 connect(m_timer, QTimer::timeout, this, [this]() { m_game.tick(); update(); }); m_timer.start(m_game.interval()); // interval 由等级计算得出 } void GameWidget::keyPressEvent(QKeyEvent *e) { switch (e-key()) { case Qt::Key_Left: m_game.move(-1); break; case Qt::Key_Right: m_game.move(1); break; case Qt::Key_Down: m_game.move(0, 1); break; case Qt::Key_Up: m_game.rotate(); break; case Qt::Key_Space: m_game.hardDrop(); break; case Qt::Key_P: m_game.togglePause(); break; default: QWidget::keyPressEvent(e); return; } update(); // 每次按键后立即重绘 }两个细节容易踩坑。第一setFocusPolicy 不写的话键盘事件永远进不了这个控件程序跑起来鼠标点哪都没反应这是 QT 新手最常见的黑匣子之一。第二每次按键后都要调用 update()否则逻辑已经变了但画面还停在上一帧。定时器那边也一样tick 之后紧跟 update()缺了它你会看到方块只在消行那一瞬间跳变其他操作都像没响应。QTimer 的 interval 可以在运行中修改等级上升后直接 m_timer.start(newInterval) 就会重设定时周期。不要在 tick 里用 QThread::sleep 或 std::this_thread::sleep_for 模拟减速那是把整个界面线程睡死窗口拖动都会卡成幻灯片。3.4 右侧信息面板与预览区布局代码比 Qt Designer 拖拽更快信息面板只有三个标签加一个预览画布用代码布局比开 Qt Designer 来回拖快也更好控制。预览区可以复用 GameWidget 的绘制思路单独做一个 PreviewWidget只画下一块// previewwidget.cpp —— 只画一个方块的迷你画布 void PreviewWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.fillRect(rect(), QColor(0x12, 0x12, 0x12)); Tetromino next m_game.next(); QColor clr m_game.colorOf(next.shape()); for (const QPoint p : next.cells()) { int px (p.x() 1) * 20; // 4x4 模板在 4*20 的画布上居中 int py (p.y() 1) * 20; painter.fillRect(px, py, 18, 18, clr); } }PreviewWidget 同样要暴露一个 updatePreview() 接口在 GameController 的 spawnNext 之后由外部调用。这个类看起来多余但它把“预览下一块”从主绘制里拆了出去新增一个“未来五块预览”时不用动 GameWidget。主窗口的布局用 QHBoxLayout 放 GameWidget 和右侧 QVBoxLayout右侧依次放分数标签、等级标签、PreviewWidget最后 addStretch 把控件顶到上方。窗口 setFixedSize 禁止拉伸免得用户把窗口拉变形后棋盘比例失调。4. 游戏逻辑细节消行判定、计分规则与等级加速的联动4.1 从底部逐行扫描消行memcpy 整行下移的写法和 y 陷阱消行逻辑写在 Board 里常见做法是每次方块落定后从最后一行往上扫遇到满行就把它上面的所有行整体下移一行。代码本身很短坑都在循环下标上// board.cpp —— 消行并返回消掉的函数 int Board::clearLines() { int lines 0; for (int y ROWS - 1; y 0; --y) { bool full true; for (int x 0; x COLS; x) { if (m_grid[y][x] 0) { full false; break; } } if (!full) continue; // 当前行满上方所有行下移一行顶部补空行 for (int yy y; yy 0; --yy) { std::memcpy(m_grid[yy], m_grid[yy - 1], sizeof(m_grid[0])); } std::memset(m_grid[0], 0, sizeof(m_grid[0])); y; // 下移后当前行换成了新内容需要重新检查 lines; } return lines; }那个 y 是容易看漏的细节。整行下移后原来的 y 行现在装的是 y-1 行的内容这一行可能也是满的所以循环要回退一格重新检查。漏掉 y 的后果是连续两行同时满时只消掉下面的那一行上面的满行残留测试同学常把这当成“消行 bug”其实是循环下标没回退。从底部往上扫比从顶部往底部扫省事因为消掉底行后上方的行位置不变不需要额外处理。用 memcpy 整行拷贝比自己写 for 循环逐列赋值快而且代码少但要注意 sizeof(m_grid[0]) 必须等于一行数组的字节数这里因为 m_grid 是 int grid[ROWS][COLS]sizeof(m_grid[0]) 是 COLS 个 int 的大小正好对应。4.2 计分与等级行列数与下落速度 interval 怎么联动计分规则直接参照经典任天堂规则写在文档里既规范又好编码。规则如下消行数基础分倍率1100当前等级2300当前等级3500当前等级4800当前等级也就是单次消两行得 300 乘以当前等级而不是 100 加 100这个区分让玩家更愿意堆四行一起消也是答辩时可以讲的一个设计点。等级按总消行数每 10 行升一级下落间隔从 500 毫秒起步每级减 30 毫秒最低 80 毫秒防止后期快到人眼跟不上// gamecontroller.cpp —— 计分和等级联动 void GameController::linesCleared(int n) { static const int scoreTable[5] {0, 100, 300, 500, 800}; m_score scoreTable[n] * m_level; m_lines n; if (m_lines / 10 m_level - 1) { m_level; m_interval std::max(80, 500 - (m_level - 1) * 30); emit intervalChanged(m_interval); // 通知界面层重设定时器 } }等级变化后GameWidget 要响应 intervalChanged 信号并重新 m_timer.start。用信号而不是界面层轮询 m_game.interval()是为了让逻辑层不感知界面保持分层。很多同学在这里犯的错是把 interval 写死等级升了速度不变答辩被问“加速在哪”时答不上来。4.3 下一个方块预览用简单成员还是队列两种写法的取舍预告下一块是俄罗斯方块的标配实现上有两种常见做法。一是 GameController 持有一个 Tetromino m_next落定后 current m_nextm_next 再随机生成新形状。二是维护一个 QQueue 每次从队头取一块队尾补新块。课程设计阶段我推荐第一种理由有三个代码少一半不需要处理队列为空的情况随机性对一个课设来说完全够用。只有当你计划做“7-bag”随机算法把七种方块各放一次打乱依次出块时才需要第二种配合一个固定大小的数组。生成随机形状时用 QT 的 QRandomGenerator::global()-bounded(7) 或者 C11 的 std::mt19937这两种都是正经的均匀分布随机数能写进文档。退一步用 rand() % 7 虽然也能跑但答辩被问到随机数质量时不太好解释。spawnNext 的代码// gamecontroller.cpp —— 从预览区取出新方块 void GameController::spawnNext() { m_current m_next; m_next Tetromino(static_castTetromino::Shape( QRandomGenerator::global()-bounded(7) 1)); m_current.setPos(SPAWN_X, SPAWN_Y); // 例如 (3, 0)居中偏上 }SPAWN_X 取 (COLS - 4) / 2 附近让方块大致居中SPAWN_Y 取 0 或 -1 都可以如果取 -1方块会从棋盘顶部上方一格滑下来视觉上更顺滑且恰好利用 canPlace 对 y 0 放行的特性。4.4 游戏结束判定放在哪两个时机生成时预判还是落定后检查游戏结束的判定时机有两个选择。一个是在落定合并后看棋盘是否超出顶部另一个是在生成新方块时看初始位置是否放得下。我推荐后者也就是 spawnNext 之后立刻检查// gamecontroller.cpp —— 新方块无法落位则结束 void GameController::spawnNext() { // ... 设置 m_current 和 m_next ... if (!board.canPlace(m_current, m_current.pos().x(), m_current.pos().y())) { m_state OVER; emit gameOver(m_score); } }这比落定后再检查更早暴露问题而且不需要额外维护“最高行溢出”这种状态。界面层收到 gameOver 信号后弹一个 QMessageBox显示分数并询问是否重开。注意 gameOver 信号发出后定时器要 stop否则 tick 还会继续跑在 OVER 状态下虽然 tick 里有一层防护但停止定时器能省 CPU 也让逻辑更干净。忘记停止定时器你会看到结束后方块还在往下落这是很多人忽略的收尾细节。5. 高分项目避坑指南QT 环境、焦点事件与重绘卡顿的经典事故5.1 平台插件报错qt.qpa.plugin 找不到 windows 或 linuxfb 平台插件现象程序编译通过双击 exe 或命令行运行时直接崩溃终端输出一行类似 qt.qpa.plugin: Could not find the Qt platform plugin windows in 的报错后面还跟着“已终止”或段错误。原因QT 程序运行时需要从 plugins 目录加载平台插件可执行文件拷到别的路径后找不到同目录下的 platforms 文件夹或者系统 PATH 里指向的 QT 安装路径和编译时用的不是同一个。解决课程设计阶段在 Qt Creator 里直接运行就行不要折腾绿色版。需要提交独立可执行文件时用 QT 自带的 windeployqt 工具把依赖 DLL 和 plugins 目录拷到 exe 旁边一句命令搞定。Linux 下如果报的是 linuxfb 找不到多半是嵌入式交叉编译环境的QT_QPA_PLATFORM设了不被支持的平台检查环境变量即可。成熟的老师看课设都在你笔记本上跑不用追求单文件绿色版那反而容易翻车。5.2 版本混用报错cannot mix incompatible qt library (version ex50601)现象编译链接全过运行时弹窗或终端报 Fatal: cannot mix incompatible Qt library (version 0x50601) with this library version 0x50a01程序直接退出。这种报错在装了多个 QT 版本的机器上非常常见。原因链接器找到的是 5.6.1 的头文件和库而运行时加载的 DLL 是 5.10.1 的版本对不上。通常是系统的 QT 路径、Qt Creator 配置的 Kit、以及编译器的链接搜索路径三者不一致导致的比如你之前手动装过某个旧版 QT环境变量里残留 qmake 路径VSCode 配置 C 环境时又指到了另一边。解决打开 Qt Creator 的“工具 - 选项 - Kits - Qt Versions”把用不到的版本路径全部移除只保留 5.15.2 的 qmake。项目构建目录建议删掉重新 qmake 一次避免 Makefile 里残留旧路径。如果你确实在 VSCode 里配过 C 环境那个 QT 路径和 Qt Creator 通常不是同一套两者不要交叉使用。我的习惯是课程设计期间只开 Qt Creator见到这个报错第一时间查 Kit而不是去重装 QT。5.3 按键没反应setFocusPolicy 与焦点丢失的那点事现象界面能正常显示鼠标点按钮有响应方向键、空格键怎么按都没反应游戏像死了一样。原因QWidget 默认不接受键盘焦点keyPressEvent 只有在控件获得焦点时才会被调用。窗口刚显示时焦点可能落在某个按钮或者没有焦点控件上按键事件被分发给焦点对象而不是你的 GameWidget。解决在 GameWidget 构造函数里加一行 setFocusPolicy(Qt::StrongFocus)让这个控件可以用键盘和 Tab 获取焦点。如果界面上有 QPushButton 或 QLineEdit点击它们之后焦点会转移此时继续按方向键就失灵。处理办法是对按钮调用 setFocusPolicy(Qt::NoFocus)或者重写整个主窗口的 eventFilter 统一拦截键盘事件再转给 GameWidget后者更彻底但课设代码里用 NoFocus 就够。这个坑一流出现查代码半小时查不出来其实就是少写了一行。5.4 界面闪烁与掉帧别在 timer 里 new QPainter统一用 update()现象方块移动时窗口闪烁或者移动一顿一顿不跟手消行瞬间尤其明显。原因把绘制代码写在了定时器回调或者按键处理函数里在事件循环外直接往窗口上画东西系统在下一帧重绘时又把你的绘制覆盖掉造成闪烁。另一种情况是逻辑层用了 sleep 或大量拷贝导致事件循环卡顿update() 延迟到很晚才合并触发。解决所有绘制统一收进 paintEvent逻辑变化只调 update()。QT 会合并同一次事件循环里的多个 update()所以按键加定时器连续触发 update 不会造成额外开销不用担心性能。不要在 tick 里做耗时的全棋盘扫描消行时只处理有变化的部分或至少把 clearLines 的实现保持为 O(ROWS*COLS)这个量级完全扛得住。如果 QPainter 开了抗锯齿后觉得卡把 painter.setRenderHint(QPainter::Antialiasing) 关掉这里画的是矩形抗锯齿不仅没用还拖慢速度。5.5 崩溃在 delete 上QT 父子对象机制与栈对象不要混用现象游戏正常运行退出程序时崩溃报 double free 或析构相关断言有时候是 CtrlC 结束没反应关窗口才炸。原因QT 的 QObject 父子机制规定父对象析构时会自动销毁所有子对象。你把一个控件 new 出来并且传了 parent之后又手动 delete 它就造成二次释放。反过来在栈上创建了 QWidget 还给别人当了 parent栈对象析构时父对象可能已经先析构了触发悬垂指针。解决凡是构造函数里传了 parent 的对象一律不手动 delete交给父对象统一管理。如果需要提前销毁用 deleteLater()它会在事件循环安全点执行删除。GameWidget 里的 QTimer m_timer 是成员对象不是 new 出来的析构时随 GameWidget 一起销毁不涉及这个问题。这一条写进文档的“遇到的问题”章节算是展示你理解 QT 内存管理比抄一段智能指针的八股答得实在。6. 项目文档与答辩把“高分项目”的分数落在纸面上6.1 一份课程设计文档的骨架需求分析、概要设计、测试记录怎么写源代码写得再好文档拿不出手还是会被扣分。课程设计文档的标准结构是需求分析、概要设计、详细设计、测试报告、心得五部分其中概要设计和测试报告占的分数最多。概要设计里放一张手绘的类图就够画出 Tetromino、Board、GameController、GameWidget 四个类的关系标注关键的 public 接口比复制十页代码有用。测试报告别只写“测试通过”把几个典型场景列出来方块到边界能否停止、旋转后碰墙能否拒绝、连续消四行得分是否正确、游戏结束后按键是否无响应每条配一张运行截图。截图记得把分数和等级一起截进去评阅老师会看分数是否和消行数对得上。6.2 答辩讲这三个亮点分层架构、碰撞收口、扩展性答辩时不用面面俱到讲三个点足够。第一讲分层界面类不碰游戏数据控制器不碰绘制代码老师问“加功能要改哪里”时你能明确指出改哪个类。第二讲碰撞检测收口到 canPlace 的取舍所有移动、旋转、硬降共用同一个判定函数而不是每个动作各写一堆边界判断。第三讲扩展性加新方块只改 Tetromino加 7-bag 随机只用动 spawnNext加存档只要在 GameController 里序列化分数和棋盘。这三句话比背概念更能让老师相信项目是你自己写的。我当年做这个题翻车就翻在 update() 上逻辑全对但界面纹丝不动查了整整一个晚上最后发现只是忘了在按键后请求重绘。“改代码不如先怀疑重绘没触发”这道坎迈过去之后我对 QT 的绘图机制算是彻底通了。这个项目的完整实现值得你亲手敲一遍别直接抄源码自己把 canPlace 和 clearLines 写对一次收获比跑通十遍别人的代码都大。希望帮到你。本文还有配套的精品资源点击获取