ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++ Qt4小游戏2048源码解析:从算法到界面绘制

C++ Qt4小游戏2048源码解析:从算法到界面绘制 简介一份基于C与Qt4框架的2048小游戏完整工程适合C初学者进行图形界面编程实战。游戏实现了经典2048玩法每次滑动后相同数字的方块合并棋盘随机生成2或4玩家持续操作直到拼出2048。源码在结构上划分为棋盘管理、方块实体与流程控制三个模块分别维护棋盘数据、表示可合并的数字块、接收并响应玩家滑动指令其中方块实体通过图形视图框架显示游戏管理器利用信号与槽机制完成界面与逻辑的通信清晰体现出面向对象封装、继承和事件驱动的协作方式。整个压缩包共包含6个文件具体为两个C源文件、一个头文件、一个界面文件、一个工程文件和一个用户配置文件大小仅7KB结构紧凑适合逐文件阅读和修改。目前已有265人学习通过编译调试与二次开发读者可以较快学会Qt4窗口创建、控件布局、基本绘图和事件处理等技能获得一个能直接运行的2048游戏项目同时加深对GUI程序整体架构的认识。对于刚接触面向对象设计的初学者这套代码也能直观展示类之间如何划分职责与协作有助于理解实际项目中的模块化思想。1. 拿到这个 2048qt 小游戏 C 初学.zip先别急着解压看懂它的 Qt4 身份第一次拆这个 2048qt 小游戏 C 初学.zip 时我下意识先看了眼里面的 .pro 文件和 main.cpp确认它真的是个 Qt4 工程才放心往下看。这份源码的价值不在 2048 这个游戏本身而在于它把一个完整小游戏浓缩成两个类一个 QtGui 里的 QWidget 派生游戏界面配一个纯 C 的棋盘类正好覆盖了 Qt4 里最高频的知识点。刚把 C 语法基础学完、却不知道怎么把类、数组、事件、绘图串起来的读者能从这一百来行代码里看到信号槽、键盘事件、QPainter 绘图和一个标准滑动合并算法是如何协作的。它不是一个能惊艳谁的成品但它是 C 初学阶段性价比很高的一课。2. 先把编译环境搞定Qt4 与 Qt5 差异、pro 文件与常见编译命令2.1 这份源码为什么锁死 Qt42048 是典型的 Widget Application核心交互就三件事方向键输入、棋盘数据更新、界面重绘。Qt4 和 Qt5 在这三件事的 API 上几乎一模一样但模块划分完全不同。Qt4 时代QPainter、QKeyEvent、QGridLayout 这些东西全部集中在 QtGui 模块里一个QT core gui就够Qt5 开始把界面控件拆成了 QtWidgets所以拿到这份源码用 Qt5 编译第一件事就得在 pro 里补一行QT widgets。从学习角度讲Qt4 其实更友好。它的类数量比 Qt5 少一圈网上老教程、老博客大量基于 Qt4查QPainter::drawText的用法能看到一堆带实际工程背景的代码而不是官方文档干巴巴的签名。这份 zip 选 Qt4大概率就是作者当年跟着某个 Qt4 教程走下来的所以它里面的写法、类名、头文件都带着那个时代的风格。比如#include QtGui/QApplication这种写法在 Qt5 里就要改成QApplication或者QtWidgets/QApplication这一步改错编译期就是一片红。我的习惯是拿到这种老工程先用系统里已有的 Qt 版本试试看而不是急着装新环境。Ubuntu 上通常可以用apt install qt4-qmake libqt4-dev直接装一套 Qt4装完qmake-qt4这个命令就能用了。如果你手上只有 Qt5也没关系pro 文件加一行greaterThan(QT_MAJOR_VERSION, 4): QT widgets源码里的绘图、按键、布局代码基本能原样跑起来。2.2 pro 文件四行配置背后的含义这份源码的 pro 文件长这样Qt4 项目的标配QT core gui TARGET game2048 TEMPLATE app SOURCES main.cpp board.cpp gamewidget.cpp HEADERS board.h gamewidget.h第一行的core gui决定了链接哪些 Qt 模块Qt4 下这两个模块包含基础容器、信号槽、事件循环和全部绘图控件。TARGET是输出的可执行文件名不写的话默认跟 pro 文件同名。TEMPLATE app表示生成应用程序而不是库。后面两行把源码和头文件交给 qmakeqmake 会根据它们推导出头文件依赖和编译顺序不需要手动写复杂的 make 规则。如果你要迁移到 Qt5把第一行改成这样QT core gui widgets greaterThan(QT_MAJOR_VERSION, 4): QT widgets其中greaterThan(QT_MAJOR_VERSION, 4)是 qmake 内置的版本判断函数意思是只在 Qt5 及以上的环境里追加 widgets。这样同一个 pro 文件在 Qt4 和 Qt5 下都能编译属于老项目里最常见的兼容写法。注意这一行要写在QT core gui的下面否则 Qt5 下 qmake 解析QT widgets时前面的模块声明还没生效顺序错了偶尔会出奇怪链接问题。2.3 编译命令与运行进到源码目录后终端里按这个顺序执行ls /usr/bin/qmake* qmake -v qmake-qt4 make -j4 ./game2048第一步先看你机器上有几个 qmake。ls /usr/bin/qmake*会列出一堆qmake、qmake-qt4、qmake-qt5之类的软链接它们指向不同版本的 Qt。qmake -v直接告诉你当前默认 qmake 是哪个版本如果输出Using Qt version 5.x而这份工程里全是 Qt4 的写法编译基本不会顺利。为什么强调先确认版本因为 qmake 生成 Makefile 的时候会把 Qt 库的绝对路径写进去。如果你的默认 qmake 是 Qt5 的它会生成一套链接 Qt5 库的 Makefile但源码里#include QtGui/QApplication很可能因为头文件搜索路径问题找不到或者更糟找到一个 Qt5 的同名头文件编译通过运行时崩溃。我在很多新手工程里见过这种翻车。正确做法是用qmake-qt4覆盖默认生成或者用 qtchooser 显式指定qtchooser -qt4 -run qmake 2048.pro make clean make -j4make clean很重要。万一你之前不小心用 Qt5 的 qmake 生成过一次 Makefile直接重跑 qmake-qt4 有时候不会完全覆盖旧 Makefile 里的路径编译时就会出现「头文件是 Qt4 的、链接库是 Qt5 的」这种混合状态。先 clean 再重来能省掉一晚上排查时间。运行期如果程序能起来、窗口能画出棋盘那环境就通了。这时候再用ldd game2048 | grep Qt看一眼实际链接的库版本能确认它跑在哪个 Qt 上。这套检查流程做完后面所有排错都建立在「环境干净」这个前提上。3. 2048 的灵魂滑动合并算法与随机生成3.1 四个方向只写一个合并函数2048 这个游戏玩法一句话把所有方块沿某个方向滑动相同数字相撞时合并成它们的和。这背后的核心是「一行四个数的压缩与合并」方向不过是行的读取顺序问题。所以好代码里不应该有四个方向的四套逻辑而应该只写一个mergeRow(int row[4])再在不同方向上让数据按行或列、正序或逆序地喂给它。棋盘数据我用固定数组存// board.h #ifndef BOARD_H #define BOARD_H class Board { public: Board(); bool move(int dir); // 0上 1下 2左 3右 bool canMove() const; // 是否还有合法移动 bool gameOver() const { return !canMove(); } int at(int r, int c) const { return tiles[r][c]; } int score; private: bool addRandomTile(); bool mergeRow(int row[4]); int tiles[4][4]; }; #endif为什么不用 QVector 或 std::vector 存棋盘因为 4x4 是固定大小int tiles[4][4]作为 Board 成员拷贝整个 Board 就是拷贝 64 字节又便宜又快。后面写自动演示、写测试代码时直接Board tmp board;就能复制一份现场这个便利在 Qt4 这种老环境里尤其有用不用管任何内存管理。score放在棋盘类里而不是界面类里是因为合并逻辑需要累加分数如果界面持有分数、棋盘只负责移动接口就得多一个返回值传分数别扭。合并函数是算法的核心它的思路是「先压缩后合并」bool Board::mergeRow(int row[4]) { bool changed false; int tmp[4] {0, 0, 0, 0}; int idx 0; // 第一步压缩把非零数往左挤 for (int i 0; i 4; i) { if (row[i] ! 0) { tmp[idx] row[i]; } } if (idx ! 4) { changed true; // 有空洞说明发生了移动 } // 第二步从左往右相邻相同才合并每个数只参与一次 for (int i 0; i idx - 1; i) { if (tmp[i] tmp[i 1]) { tmp[i] * 2; score tmp[i]; // 把后面的元素整体左移一位 for (int j i 1; j idx - 1; j) { tmp[j] tmp[j 1]; } tmp[idx - 1] 0; --idx; changed true; } } for (int i 0; i 4; i) { row[i] tmp[i]; } return changed; }这段代码的语义是「向 左 合并一行」。第一步压缩把 0 全部滤掉比如输入2 0 2 0压缩后变成2 2 0 0。第二步从左往右扫描tmp[i] tmp[i1]时合并注意循环内部把后面的元素整体前移保证已经合并过的位置不会被再次合并。这就是 2048 规则里「一次滑动中一个方块只能合并一次」的实现。有个典型误写必须提醒如果直接写成if (tmp[idx-1] row[i]) tmp[idx-1] * 2这种边扫边合并的写法输入2 2 2 2会得到4 8 0 0这不符合游戏规则。标准结果应该是4 4 0 0。因为2 2 2 2向左滑动最左边两个 2 先合并成 4剩下两个 2 再合并成另一个 4。上面这段代码先压缩成2 2 2 2i0 时tmp[0]tmp[1]合并成4 2 2 0i1 时tmp[1]tmp[2]合并成4 4 0 0正确。如果你写的是边扫边合并版本跑几万次能看到分数异常这就是算法细节决定成败的地方。3.2 move 的方向语义与旋转技巧有了向左的mergeRow其余三个方向就好办了方向dir 值做法上0按列提取逐列 mergeRow写回列下1按列倒序提取mergeRow再倒序写回列左2逐行 mergeRow直接写回右3逐行倒序提取mergeRow再倒序写回行这里的「倒序提取再倒序写回」是值得记住的技巧向右合并一行等价于把这一行反转后向左合并再反转回来。比如2 2 4 0向右滑动结果是0 2 2 4吗不对正确结果是0 0 2 8因为最右边的两个 2 先合并。反转后是0 4 2 2向左合并得到4 4 0 0再反转回来就是0 0 4 4等一下这个结果不对。这里我故意暴露一下容易绕晕的地方。4 4 0 0的反转是0 0 4 4但我们期望向右滑动2 2 4 0的结果是什么右侧视角从右往左看先是两个 2 相邻合并成 4再和原来的 4 拼在一起结果是0 0 4 4。反转一次是对的0 0 4 4正是期望值。对反转合并再反转这个思路没问题。上下方向同理向上滑动等价于逐列向左合并向下滑动等价于逐列反转后向左合并再反转。move 的完整代码bool Board::move(int dir) { bool changed false; int line[4]; for (int i 0; i 4; i) { // 按方向提取一行或一列 for (int j 0; j 4; j) { line[j] (dir 2 || dir 3) ? tiles[i][j] : tiles[j][i]; } // 右、下需要反转 if (dir 3 || dir 1) { for (int a 0, b 3; a b; a, --b) { qSwap(line[a], line[b]); } } bool rowChanged mergeRow(line); if (dir 3 || dir 1) { for (int a 0, b 3; a b; a, --b) { qSwap(line[a], line[b]); } } for (int j 0; j 4; j) { if (dir 2 || dir 3) { tiles[i][j] line[j]; } else { tiles[j][i] line[j]; } } changed changed || rowChanged; } return changed; }qSwap是 Qt 的交换函数不需要引入额外头文件QtGlobal里就有。参数dir约定成 0 上、1 下、2 左、3 右这个约定必须在整个类和调用方之间保持一致。返回changed的作用是给界面层判断移动后棋盘有没有实质变化。如果没有变化就不需要生成新方块、不需要重绘。这个返回值在键盘事件里直接决定要不要update()一定要用上否则用户按一下方向键就闪一下、白色画面随着方块增加越来越明显。3.3 随机生成与终局判定每次有效移动后要在空白格里随机生成一个新方块。新方块是 2 还是 4遵循一个常见手感90% 出 210% 出 4。这里有个问得很高频的细节用什么随机数。很多初学者用rand()但 Qt4 项目里更统一的写法是qrand()它底层映射到平台相关实现配合qsrand()设种子。bool Board::addRandomTile() { QListQPairint, int empty; for (int r 0; r 4; r) { for (int c 0; c 4; c) { if (tiles[r][c] 0) { empty qMakePair(r, c); } } } if (empty.isEmpty()) return false; int pos qrand() % empty.size(); tiles[empty[pos].first][empty[pos].second] (qrand() % 10 0) ? 4 : 2; return true; }注意qsrand的调用时机如果在 main 里只调用一次qsrand(QTime::currentTime().msec())程序运行期间随机种子不变每局游戏的开局方块的分布比较均匀。如果每次addRandomTile之前都重新设种子而且两次调用间隔很短msec()可能返回相同值就会出现连续两局一模一样的开局。这种问题很难察觉但玩家会觉得「每次玩都长一样」属于玄学级 bug。终局判定canMove也很直白有空位就能移动没有空位、且任意相邻格子没有相同数字就彻底动不了了。bool Board::canMove() const { for (int r 0; r 4; r) { for (int c 0; c 4; c) { if (tiles[r][c] 0) return true; if (c 3 tiles[r][c] tiles[r][c 1]) return true; if (r 3 tiles[r][c] tiles[r 1][c]) return true; } } return false; }这里只检查右方和下方邻居就够因为左、上两个方向的「相邻相同」在遍历到对应格时会被覆盖。如果写成四个方向都检查也不会有问题只是重复劳动。棋盘是 16 个格子最多判 32 次相等比较性能完全不是瓶颈真正需要注意的是canMove要在addRandomTile之后调用否则棋盘可能刚好被填满但仍有相邻相同格这时gameOver会误报。4. 界面层paintEvent 绘制、键盘事件与计分刷新4.1 界面架构为什么用 QWidget 而不是 QMainWindow这份小游戏的主窗口类叫GameWidget直接继承 QWidget。很多人学 Qt 一上来就用 QMainWindow但 QMainWindow 是给带菜单栏、工具栏、状态栏的复杂窗口用的2048 这种界面只需要一个画布和一个计分文本QWidget 完全够。QMainWindow 默认布局里中心 widget 的尺寸策略、以及它的setCentralWidget那套机制对初学者是额外负担。// gamewidget.h #ifndef GAMEWIDGET_H #define GAMEWIDGET_H #include QWidget class Board; class GameWidget : public QWidget { Q_OBJECT public: GameWidget(QWidget *parent 0); protected: void paintEvent(QPaintEvent *event); void keyPressEvent(QKeyEvent *event); private: void drawGameOver(QPainter painter); void newGame(); Board *board; QFont scoreFont; }; #endif把棋盘实例放在界面类里、而不是反过来这个分层能让你后面写自动测试和命令行版 2048 变得轻松。Board不依赖任何 Qt 绘制对象纯逻辑纯计算这意味着你可以单独写一个不弹窗的main在终端里跑几千局验证算法正确性。界面类只做三件事读board的数据、把board的变化画出来、把键盘输入翻译成board的move调用。paintEvent之所以是受保护的成员函数是因为 Qt 的事件框架要求子类重写这个函数接收绘制请求外部代码不能直接调用它。想主动刷新画面正确姿势是调update()Qt 会把多个绘制请求合并避免频繁重绘。这个机制在键盘连按时尤其重要玩家快速按十下方向键update 可能只触发两三次 paintEvent但最终画面不会丢状态因为是 handler 重绘整个棋盘而不是增量补丁。4.2 用 QPainter 手绘棋盘与方块2048 的棋盘可以简单堆 QLabel每个格一个控件但那样要放 16 个 label 再加布局管理代码量和绘制灵活性都不如直接画。用QPainter在 paintEvent 里画 4x4 的格子是这个项目最值得学习的绘图入门void GameWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); const int cellSize 80; const int gap 8; const int boardSize cellSize * 4 gap * 5; const int offsetX (width() - boardSize) / 2; const int offsetY (height() - boardSize) / 2; // 背景 painter.fillRect(rect(), QColor(250, 248, 239)); for (int r 0; r 4; r) { for (int c 0; c 4; c) { int value board-at(r, c); int x offsetX gap c * (cellSize gap); int y offsetY gap r * (cellSize gap); QRect cell(x, y, cellSize, cellSize); painter.fillRect(cell, colorFor(value)); if (value ! 0) { painter.setPen(QColor(119, 110, 101)); painter.setFont(scoreFont); painter.drawText(cell, Qt::AlignCenter, QString::number(value)); } } } if (board-gameOver()) { drawGameOver(painter); } }这里几个参数值得说cellSize80、gap8画出来每个格子 80 像素、间隔 8 像素棋盘总宽80*4 8*5 360像素。这个尺寸在普通桌面窗口上刚好合适太大会让 1024x768 的小屏幕显示不全太小则数字容易挤在一起。如果你要改成 6x6 的扩展版只要把 4 换成 NcellSize按屏幕分辨率等比缩小即可不需要动绘制逻辑本身。colorFor(value)是颜色查表函数用 switch 或 static 数组按 2048 经典配色QColor GameWidget::colorFor(int value) { switch (value) { case 0: return QColor(204, 192, 174); case 2: return QColor(238, 228, 218); case 4: return QColor(237, 224, 200); case 8: return QColor(242, 177, 121); case 16: return QColor(245, 149, 99); case 32: return QColor(246, 124, 95); case 64: return QColor(246, 94, 59); case 128: return QColor(237, 207, 114); case 256: return QColor(237, 204, 97); case 512: return QColor(237, 200, 80); case 1024: return QColor(237, 197, 63); case 2048: return QColor(237, 194, 46); default: return QColor(60, 58, 50); } }数字颜色有个实践经验2 和 4 的格子背景很浅数字要用深棕色8 到 64 的背景是中橙色数字用白色更清楚128 以上的背景偏金色白字黑字都不太理想经典做法是继续用白字加一点阴影。初学者最容易犯的错是给所有数字统一用一种颜色文本到 128 就糊成一团。painter.drawText(cell, Qt::AlignCenter, QString::number(value))是 Qt 里把文本画到矩形中心的通用写法五个参数矩形、对齐方式、文本。它的问题在于不会自动换行、字太长会溢出但 2048 最大也就 131072四位数字在 80 像素格子里用 18 号粗体完全放得下。这里用的scoreFont建议在构造函数里设置scoreFont QFont(Arial, 18, QFont::Bold);QFont::Bold是第三个参数的重载也可以用scoreFont.setBold(true)效果相同。字体大小最好跟cellSize联动比如scoreFont.setPixelSize(cellSize / 4)这样改格子大小时数字不会显得比例失调。4.3 键盘事件与一局游戏的完整流程键盘事件是 Qt 事件处理里最直观的入口。焦点在 GameWidget 上时按方向键会触发keyPressEventvoid GameWidget::keyPressEvent(QKeyEvent *event) { int dir -1; switch (event-key()) { case Qt::Key_Up: dir 0; break; case Qt::Key_Down: dir 1; break; case Qt::Key_Left: dir 2; break; case Qt::Key_Right: dir 3; break; case Qt::Key_R: newGame(); return; default: QWidget::keyPressEvent(event); return; } if (dir 0 board-move(dir)) { board-addRandomTile(); update(); checkGameState(); } }方向键的 key 值在 Qt4 和 Qt5 里是一样的Qt::Key_Up等枚举值定义在Qt::Key命名空间下。注意switch的 default 分支调用了QWidget::keyPressEvent(event)这是把不处理的事件继续传给父类的标准做法。如果不调用某些环境里 Tab、Enter 等键会失去默认行为窗口的焦点切换就不正常。checkGameState负责判断胜负void GameWidget::checkGameState() { if (board-hasWon() !wonShown) { wonShown true; QMessageBox::information(this, tr(2048), tr(You win! Continue?)); } else if (board-gameOver()) { QMessageBox::information(this, tr(2048), tr(Game Over! Score: %1).arg(board-score)); } }hasWon是Board里的另一个查询函数扫描棋盘发现某个格子的值达到 2048 就返回 true。这里有个细节首次达到 2048 后弹一次框玩家选择继续后wonShown置为 true后续移动不再弹游戏继续向 4096、8192 冲刺。如果你把这个标记省了玩家每按一下方向键就弹一次「你赢了」体验很崩溃。这种「只提示一次」的状态在游戏逻辑里很常见值得养成习惯。main.cpp 里的启动流程也很简单#include QApplication #include QTime #include gamewidget.h int main(int argc, char *argv[]) { QApplication app(argc, argv); qsrand(QTime::currentTime().msec() QTime::currentTime().second()); GameWidget w; w.resize(420, 520); w.show(); return app.exec(); }窗口高度留到 520除了棋盘 360 像素还要给顶部计分文本留空间。app.exec()进入事件循环后paintEvent 才会在窗口显示、遮挡、resize 时被触发。这块如果用 Qt Designer 拖界面反而没那么方便因为 2048 的棋盘是变长数据驱动的不是固定控件组手写 40 行 paintEvent 比拖 17 个 label 加一个字体适配器清爽太多。5. 新手避坑指南版本混用、平台插件与中文乱码的排查记录5.1 编译期版本混装与链接库找不到坑 1编译或链接时报fatal: cannot mix incompatible qt library (version ex50601) with this librar现象终端输出一大段以fatal: cannot mix incompatible qt library开头的错误后面跟一个像version ex50601的版本号编译刚起步就中断。原因这是 Qt4 头文件和 Qt5 库混用的典型症状。ex50601对应 Qt 5.6.1 的库版本标记而你的编译命令里-I头文件路径指向的是 Qt4。常见场景是系统装了 Qt4 的 libqt4-dev又装了 Qt5 的 qtbase5-devqmake 用的是 Qt4但编译器默认搜索路径里 Qt5 的头文件排在前面结果 include 到了 Qt5 的 qglobal.h链接时却拿到 Qt4 的库文件。两边版本不一致Qt 的二进制兼容检查直接拒绝。解决统一工具链。先qmake -v确认 qmake 版本再看qmake -query QT_INSTALL_HEADERS输出的头文件路径是不是和编译搜索一致。我的习惯是干脆用qmake-qt4走完整套流程并且把make clean加在前面最后用ldd game2048验证实际链接的 .so 路径。如果必须混装就在 pro 里显式指定INCLUDEPATH /usr/include/qt4 LIBS -L/usr/lib/x86_64-linux-gnu -lQtGui -lQtCore但这是最后的办法新手不推荐容易把依赖关系写死在 pro 里换机器就崩。坑 2链接时报cannot find -lQtGui或者cannot find -lpublic现象make 走到链接阶段报cannot find -lxxx其中-lpublic这种名字一眼就不是 Qt 官方库。原因链接参数里出现-lpublic说明 pro 文件被写坏了要么是有人把库名拼错要么是某个从网上下载的工程里带了一段抄来的LIBS -l...实际系统里根本没有这个库。Qt4 的官方 Gui 模块是-lQtGui即使写成LIBS -lQtGui也要保证-L路径对得上。更常见的情况是qmake 缓存残留旧的 Makefile 里带了一堆过期的LIBSqmake 重跑后没有清除。解决先make clean删掉 Makefile重新执行qmake-qt4再make。如果还报找不到 QtGui多半是libqt4-dev没装全dpkg -l | grep qt4查一下。对于-lpublic这种非 Qt 库直接在 pro 里搜LIBS关键字把不属于这个工程的行全注释掉。记住2048 不需要任何第三方库pro 里有非 Qt 的 LIBS 就是 source 里带进来的脏东西。5.2 运行期平台插件缺失与中文乱码坑 3启动即崩溃终端报qt.qpa.plugin: could not find the Qt platform plugin linuxfb现象程序可以编译但./game2048跑起来立刻段错误终端打印could not find the Qt platform plugin linuxfb或者could not load the Qt platform plugin xcb这类信息。原因这个错误在桌面 Linux 上出现九成是 Qt 插件目录platforms没有被程序找到。常见于把 Qt4 源码拿到 Qt5 环境重新编译后运行环境却不完整。还有人是为了在迷你机、树莓派这类嵌入式设备上跑手动设置了QT_QPA_PLATFORMlinuxfb但对应的插件文件libqlinuxfb.so并不在QT_QPA_PLATFORM_PLUGIN_PATH指向的目录里。QPA 是 Qt5 的架构Qt4 走的是 X11/offscreen 那套所以一旦出现linuxfb关键词基本可以断定是 Qt5 环境下跑的。解决桌面环境不要手动指定 platform让它默认走 xcb。如果已经设置了QT_QPA_PLATFORM环境变量先unset QT_QPA_PLATFORM再运行。若是插件目录问题把 Qt 安装目录下的platforms文件夹拷到可执行文件旁边或者显式设置export QT_QPA_PLATFORM_PLUGIN_PATH/usr/lib/qt5/plugins ./game2048linuxfb插件本身是给没有 X11 的嵌入式环境用的纯桌面开发不碰它。如果你真的在板子上调先确认板子的 Qt 安装里有没有libqlinuxfb.so没有就换QT_QPA_PLATFORMminimal或者其他可用的插件。坑 4drawText 打出来的中文变成????或乱码现象QMessageBox里的「游戏结束」、drawText里的分数显示正常但单独写的中文文本显示成问号或方块。原因Qt4 默认对const char*按fromAscii处理源码文件如果保存成 GBK 编码编译器拿到的是 GBK 字节序列Qt 却当成 Latin-1 或 ASCII 解释字节流被破坏显示自然乱。Qt5 默认按 UTF-8 处理所以同一个源文件从 Qt4 挪到 Qt5中文显示结果可能「由乱变好」反过来从 Qt5 挪到 Qt4 可能由好变乱。解决统一源文件编码为 UTF-8并且在 pro 里加一行让编译器明确知道CODECFORTR UTF-8 CODECFORSRC UTF-8这两行是 qmake 给 moc 和源码解析用的编码声明Qt4 的 moc 如果不加信号槽里带中文的字符串也会被误解码。对已经写进代码里的中文字符串最稳妥的写法是QString text QString::fromLocal8Bit(游戏结束);fromLocal8Bit按当前系统 locale 转换中文 Windows 下就是 GBKLinux 下如果 locale 是 UTF-8 就是 UTF-8虽然还依赖环境但至少不会出现「源码是 UTF-8、系统是 GBK」的双重错位。我拿到那份 zip 的时候里面所有注释都是 GBK编辑器显示正常、编译却报错把整个工程文件转成 UTF-8 后问题一下全清了。6. 进阶技巧QSS 换肤与用 QTest 做自动按键回归界面和算法都跑通之后这颗「初学」资源的进阶空间反而更大了。第一个立刻能动手的是皮肤。paintEvent 里colorFor写死了配色改成从 QSS 读取或做成主题数据结构就能换皮肤void GameWidget::applyTheme() { QColor bg palette().window().color(); QPixmap cache grab(); painter.setPen(Qt::NoPen); update(); }更简单的是直接给 QApplication 设置 QSS 全局背景qApp-setStyleSheet( QWidget#GameWidget { background: #1a1a2e; } );QSS 的语法和 CSS 很像QWidget#GameWidget是 ID 选择器要让它生效GameWidget 的构造函数里要setObjectName(GameWidget)。注意 QSS 只能改 Qt 控件自身的绘制paintEvent 里用 QPainter 画出的格子颜色QSS 管不到所以严格说替换皮肤要动colorFor的颜色表。把颜色表抽成静态数组再写一个setTheme(const QColor colors[14])接口皮肤就变成运行时可切换的了。更有价值的做法是写自动回归测试。2048 的合并逻辑是纯 C非常适合灌数据验证#include QtTest/QtTest #include board.h void testMoveLeftMerge() { Board b; // 手动摆出 2 2 2 2 b.tiles[0][0] 2; b.tiles[0][1] 2; b.tiles[0][2] 2; b.tiles[0][3] 2; QVERIFY(b.move(2)); QCOMPARE(b.at(0, 0), 4); QCOMPARE(b.at(0, 1), 4); QCOMPARE(b.at(0, 2), 0); QCOMPARE(b.at(0, 3), 0); }配合QTest::keyClick可以直接把按键事件灌给 GameWidget模拟玩家按方向键验证界面层有没有把输入正确转发给 BoardQTest::keyClick(w, Qt::Key_Up); QTest::keyClick(w, Qt::Key_Left); QVERIFY(w.board-score 0);这个「qt 模拟鼠标点击事件」的思路在 Qt 自动化测试里很通用QTest::keyClick和QTest::mouseClick是两个最常用的入口。很多老 Qt 工程没有测试你可以把这个灌进一个tests/目录每次改完核心算法跑一遍四个方向分别验证合并、得分、移动检测三个维度。我从那以后每次拿到别人的 Qt 小游戏都强制走一遍「编译、灌一把 QTest 回归、再读代码」的流程这个 zip 里的 2048 就是我用这套流程拆完的。希望你也能顺着这个路径把它玩明白甚至加上自己的皮肤主题这比单纯看懂代码更进一层。本文还有配套的精品资源点击获取
返回列表