首页 / 资讯中心 / 文章详情

C++与Qt打造俄罗斯方块:课程设计高分项目实战指南

C++与Qt打造俄罗斯方块:课程设计高分项目实战指南 ★ FEATURED ARTICLE
简介基于C与QT实现的俄罗斯方块游戏完整项目是面向高校课程设计的高分范例。项目不仅包含全部源代码还配有项目文档和依赖库代码内附详细注释从游戏主循环、方块生成与旋转消除、到QT图形界面布局都有清晰实现适合C初学者阅读模仿也可作为期末大作业或毕业设计的二次开发基础。资源以zip格式打包共42个文件以17个头文件和9个源文件为核心配合UI界面配置、JSON参数设置、运行所需的动态库/静态库以及docx项目手册整体仅11.87MB部署门槛较低。项目中还专门区分了游戏逻辑、视图渲染和主窗口控制等模块便于按模块查阅与调试文档对不同特效与按键交互也做了说明可直接用于课程答辩展示。已有316人学习下载若想快速搭建一个可运行的QT小游戏这份代码能提供完整可复用的示例。1. 基于C与Qt的俄罗斯方块课程大作业高分项目的起步逻辑C课程大作业里俄罗斯方块是出现频率最高的题目之一可每次答辩都能看到同一个现象三分之一的同学能跑代码和逻辑却缠成一团只有少数人拿高分。基于C与Qt做俄罗斯方块真正的门槛不是把方块画出来而是把核心规则和界面渲染拆开——前者用纯C建模棋盘、方块与消行后者用Qt的信号槽、绘图和定时器驱动交互。这篇笔记面向正在找课设题、或在已有源代码上补文档和结构的读者给你一条可复现的落地路径先立住数据结构和规则再套Qt界面最后用文档把工程习惯完整呈现给答辩老师。2. 方块建模与消行判定把俄罗斯方块规则写成C代码俄罗斯方块的规则核心就三件事方块怎么表示、旋转怎么算、消行怎么判。很多教程上来就建窗口画界面结果界面画好了游戏逻辑一塌糊涂。我习惯先把逻辑在纯C里跑通再接Qt。2.1 棋盘与方块的数据结构用二维数组表达游戏世界棋盘是20行乘10列的格子空格为0被占用的格子记方块类型编号。消行、碰撞检测、绘图全都读同一份数据所以数据结构定对了后面少改一半代码。课程作业用std::vectorstd::vectorint最顺手动态扩容和erase都现成如果坚持C风格用int board[ROWS][COLS]也能跑但传参时总是要带行列信息烦。那七种方块怎么存我见过存坐标对的也见过只存旋转状态的最稳的是用4x4矩阵。每块是16个0/1一眼看得出形状旋转也只在这个小矩阵上做运算。下面的constants.h把棋盘尺寸、单元格像素大小和所有方块形状全集中在同一个头文件游戏逻辑、绘图、窗口都要引用它后续调整颜色或形状不用到处翻。// constants.h #pragma once const int ROWS 20; const int COLS 10; const int CELL_SIZE 30; // 每个格子的像素大小UI层用 // 七种标准 tetrominoI O T S Z J L // 每种用 4x4 矩阵表示1 表示有格子0 表示空 const int SHAPES[7][4][4] { { // I {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0} }, { // O {0, 0, 0, 0}, {0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0} }, { // T {0, 0, 0, 0}, {0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0} }, { // S {0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0} }, { // Z {0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0} }, { // J {0, 0, 0, 0}, {1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0} }, { // L {0, 0, 0, 0}, {0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0} } };这里用4x4矩阵而不是更省空间的压缩编码主要图可读性和旋转通用。把每一行打印出来看就是方块本体的样子。I方块写成{1,1,1,1}居中放旋转后仍然落在4x4范围内不会出现旋转导致形状跑到矩阵外面的边界判断。O方块旋转后不变这是它的特性。当前活动方块用结构体更好管理类型、当前4x4矩阵、所在行列。这也是课设代码里“结构体”这个考点的落点我一般这样组织// GameLogic.h struct ActiveBlock { int type; // 对应 SHAPES 的下标也作为颜色索引 int shape[4][4]; // 当前旋转后的矩阵副本 int row; // 方块左上角在棋盘上的行 int col; // 方块左上角在棋盘上的列 };结构体里的shape是旋转后的副本这样旋转时先操作副本校验通过再替换不容易把原数据改坏。type同时作为棋盘格子的非0值和绘图颜色的索引一个值三用省掉一堆并行的数组。2.2 旋转与碰撞检测转置加水平翻转离散矩阵的顺时针旋转不需要任何三角函数两步走先对整个4x4矩阵做转置再把每一行水平翻转。很多教程直接给结论我建议你在草稿纸上画一个S方块验证方向再写代码方向搞反了游戏手感会非常别扭。// GameLogic.cpp #include algorithm void rotateShape(int shape[4][4]) { // 1) 转置行变列 for (int i 0; i 4; i) { for (int j i 1; j 4; j) { std::swap(shape[i][j], shape[j][i]); } } // 2) 水平翻转每行左右颠倒 for (int i 0; i 4; i) { std::reverse(shape[i], shape[i] 4); } }形参shape是当前方块矩阵的副本调用方先copy一份再传进来所以这里的修改不会污染原始SHAPES常量。这个函数依赖C标准库里的std::swap和std::reverse都是STL基础操作比手写三行循环稳得多。旋转完要立刻做碰撞检测。规则也简单遍历4x4矩阵里的非零格子算它映射到棋盘的坐标只要越界或者棋盘上那个位置已经非0就是碰撞。bool checkCollision(const std::vectorstd::vectorint board, int shape[4][4], int row, int col) { for (int i 0; i 4; i) { for (int j 0; j 4; j) { if (shape[i][j] 0) continue; int bx row i; int by col j; if (bx 0 || bx ROWS || by 0 || by COLS) return true; if (board[bx][by] ! 0) return true; } } return false; }注意row是方块矩阵左上角在棋盘里的行号所以遍历时加的是i和j。写偏偏移量是这题最高频的翻车点症状是方块快贴地时总能插进已有格子里。排查办法是临时把当前方块坐标打出来对照棋盘看两个循环的边界。2.3 消行与计分满行检测的三种写法消行的标准做法是从底往上扫哪一行全非0就把它从vector里erase掉再往头部insert一个全0行。从底往上扫的好处是消掉一行后上面的行整体下移循环可以自然跳过刚补进来的空行多行连消时行号也不会错乱。int clearLines(std::vectorstd::vectorint board) { int cleared 0; for (int i ROWS - 1; i 0; --i) { bool full true; for (int j 0; j COLS; j) { if (board[i][j] 0) { full false; break; } } if (full) { board.erase(board.begin() i); board.insert(board.begin(), std::vectorint(COLS, 0)); cleared; i; // 抵消 for 循环的 --i重新检查这一行 } } return cleared; }这个写法对20行乘10列的量级完全够用别一上来就做按行标记平移的优化——课设里老师更看重你能讲清楚erase和insert的时间复杂度而不是把它压到O(n)。注意循环里的i它跟for的--i相互抵消让刚插入的空行不会被误判成满行去掉这行间隔消行会漏掉。计分规则每次作业要求不同最通用的是1行100分、2行300分、3行500分、4行800分。等级把累计消行数除以10每升一级下落间隔缩短最低200毫秒起步这是后面Qt那层QTimer的interval依据。到这一步核心逻辑已经具备剩下的是把这些封装成GameLogic类让UI层只调用moveLeft、moveRight、rotate、stepDown这些方法完全不碰底层数组。这个分层决定了一份课设是能玩还是能答辩第4章细说。3. 用Qt画游戏界面从QPainter绘图到键盘响应核心逻辑固定之后Qt这层的任务是把棋盘格子变成屏幕格子同时把键盘操作翻译成逻辑动作。很多人被信号槽绕晕其实记住三件事就行谁负责画谁负责收按键谁管时间。3.1 窗口架构QMainWindow与QWidget怎么选主窗口建议用QMainWindow因为菜单栏和状态栏白送做“开始/暂停/重新开始”菜单项时不用自己写布局。中央放一个游戏区画布继承QWidget重写paintEvent即可没必要上QGraphicsView那一套俄罗斯方块20x10的格子用QPainter画矩形最省事。右侧用一个普通面板放分数和等级标签。选择理由QMainWindow给了菜单栏QWidget是最轻量的画布容器QLabel做文本展示不用处理事件。这套组合是qt界面设计的入门标配改动量最小。// MainWindow.cpp #include MainWindow.h #include QHBoxLayout #include QVBoxLayout MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { gameArea new GameArea(this); QLabel* scoreCaption new QLabel(QString::fromLocal8Bit(分数)); scoreLabel new QLabel(0); QLabel* levelCaption new QLabel(QString::fromLocal8Bit(等级)); levelLabel new QLabel(1); QVBoxLayout* panelLayout new QVBoxLayout; panelLayout-addWidget(scoreCaption); panelLayout-addWidget(scoreLabel); panelLayout-addWidget(levelCaption); panelLayout-addWidget(levelLabel); panelLayout-addStretch(1); QWidget* sidePanel new QWidget; sidePanel-setLayout(panelLayout); QHBoxLayout* mainLayout new QHBoxLayout; mainLayout-addWidget(gameArea, 1); mainLayout-addWidget(sidePanel); QWidget* central new QWidget; central-setLayout(mainLayout); setCentralWidget(central); }参数说明mainLayout-addWidget(gameArea, 1)的第二个参数是拉伸因子1表示游戏区占满剩余空间右侧面板宽度固定。scoreLabel初始传字符串0后面每次消行后通过setText更新不要在窗口类里到处保存分数副本。画布尺寸建议直接用常量算setFixedSize(COLS * CELL_SIZE, ROWS * CELL_SIZE)窗口不会因为拖拽拉伸变形paintEvent里也不用处理resize偏移。3.2 paintEvent与QPainter把棋盘画成格子重写paintEvent是Qt绘图的核心。逻辑很简单遍历棋盘每个格子按格子的值取颜色fillRect再把当前活动方块的4x4矩阵叠加上去。连续两次绘制的矩形之间减掉1像素格子间自动形成网格线不用额外drawLine。这就是qt绘图的典型用法。// GameArea.cpp #include QPainter #include constants.h #include GameLogic.h void GameArea::paintEvent(QPaintEvent* event) { Q_UNUSED(event); QPainter painter(this); // 1. 棋盘底色 painter.fillRect(rect(), QColor(0x1e, 0x1e, 0x2e)); // 2. 已固定格子 const auto board logic.board(); for (int i 0; i ROWS; i) { for (int j 0; j COLS; j) { int v board[i][j]; if (v 0) continue; painter.fillRect(j * CELL_SIZE, i * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1, blockColor(v)); } } // 3. 当前方块 const ActiveBlock cur logic.currentBlock(); for (int i 0; i 4; i) { for (int j 0; j 4; j) { if (cur.shape[i][j] 0) continue; int px (cur.col j) * CELL_SIZE; int py (cur.row i) * CELL_SIZE; painter.fillRect(px, py, CELL_SIZE - 1, CELL_SIZE - 1, blockColor(cur.type)); } } }这段代码的关键是绘制顺序先棋盘底色再已固定格子最后当前方块。顺序反了当前方块会沉在已固定格子下面视觉上像被吞掉。blockColor是私有函数内部放QColor数组按下标返回颜色I块青色、O块黄色、T块紫色、S块绿色、Z块红色、J块蓝色、L块橙色和经典版一致。这里不需要手动双缓冲。Qt的paintEvent在控件绘制时默认有backing store缓冲逐帧fillRect不会闪。真正造成闪烁的往往是设置了Qt::WA_OpaquePaintEvent又只画局部区域反而让老区域来不及擦除。课程作业保持默认即可别给自己找事。3.3 键盘事件与QTimer让方块动起来键盘控制有两个坑一是QWidget默认不接收键盘事件必须在构造函数里setFocusPolicy(Qt::StrongFocus)二是按住方向键不放系统会连续触发keyPressEvent不加处理方块一次跑好几格。我在按键处理里用event-isAutoRepeat()过滤系统自动重复。void GameArea::keyPressEvent(QKeyEvent* event) { if (event-isAutoRepeat()) { QWidget::keyPressEvent(event); return; } switch (event-key()) { case Qt::Key_Left: logic.moveLeft(); break; case Qt::Key_Right: logic.moveRight(); break; case Qt::Key_Down: logic.softDrop(); break; case Qt::Key_Up: logic.rotate(); break; case Qt::Key_Space: logic.hardDrop(); break; case Qt::Key_P: logic.togglePause(); break; default: QWidget::keyPressEvent(event); } update(); // 统一刷新一次移动失败也不会出错 }logic.xxx在移动失败时不会改数据所以这里统一update()是最省事的写法。软降和硬降的区别要注意软降每次只下移一行、加1分受定时器节奏控制硬降是循环尝试下移到碰壁然后立即固定并检查消行。游戏循环用QTimer驱动不用while加sleep。QTimer是事件驱动timeout到下一次timeout之间界面照常响应鼠标键盘如果在槽函数里写while循环sleep界面会假死拖窗口都白屏。这是课设最容易出现的崩溃级问题。// GameArea 构造函数 timer new QTimer(this); timer-setInterval(500); // 500ms 下落一步等级越高越小 connect(timer, QTimer::timeout, this, GameArea::onTimeout); timer-start();onTimeout里做的事顺序不能乱如果游戏结束或暂停则直接返回尝试软降一行失败说明方块触底固定当前块并生成新块、检查消行、更新分数和等级。分数等级变化后发scoreChanged信号MainWindow里的标签用槽函数刷新。顺序反了会出现方块还在半空就被固定的鬼畜现象。4. 高分项目的文档与工程组织源代码之外的另一半分数前面两层合起来程序已经能玩但课程设计这关源代码能跑只算及格。评分标准里通常有20%到30%的文档分这一章就讲怎么把同一份代码包装成高分项目的模样。4.1 工程目录与代码分层把源代码拆成能答辩的样子先看推荐的目录结构。核心原则是让老师一眼看出哪部分是逻辑、哪部分是界面头文件、实现文件、文档各就其位。Tetris/ ├── Tetris.pro ├── src/ │ ├── constants.h # 棋盘尺寸、方块形状、颜色索引 │ ├── GameLogic.h/.cpp # 纯C核心逻辑不依赖任何Qt头文件 │ ├── GameArea.h/.cpp # QWidget画布绘图和按键 │ ├── MainWindow.h/.cpp # 主窗口和布局 │ └── main.cpp ├── docs/ │ ├── 01_需求分析.md │ ├── 02_概要设计.md │ ├── 03_详细设计.md │ ├── 04_测试报告.md │ └── 05_使用说明.md └── README.mdGameLogic.cpp里禁止出现#include Q...它只依赖标准库的vector和algorithm。这样好处有二编译快且可以在命令行写临时main单测核心逻辑完全不用开Qt界面。答辩老师常问“游戏逻辑怎么测的”分层到位就是送分题。.pro文件长这样注意QT widgets必须写少了它所有QWidget头文件都找不到。配置CONFIG c11让你的C标准库特性在旧编译器上也能用。QT widgets CONFIG c11 TARGET Tetris TEMPLATE app SOURCES src/main.cpp \ src/GameLogic.cpp \ src/GameArea.cpp \ src/MainWindow.cpp HEADERS src/constants.h \ src/GameLogic.h \ src/GameArea.h \ src/MainWindow.h4.2 项目文档的结构与写法需求、设计、测试、说明文档别从网上整段抄。老师真会点开看甚至追问“这个用例你自己跑过吗”。耐着性子写五份每份控制在一到三页。文档核心内容篇幅建议需求分析功能需求、运行环境、操作方式1-2页概要设计模块划分图、各模块职责1页详细设计数据结构定义、核心函数流程、关键代码3-5页测试报告测试环境、测试用例表、结果截图2-3页使用说明编译步骤、操作键位表、常见问题1页需求分析里写清楚运行环境我一般写“Windows 10/11 Qt 5.14.2 MinGW 64 bit”把编译器版本写明白避免老师在自己环境打不开时觉得是你代码的问题。操作方式列一张键位表左/右移动、上旋转、下加速、空格落底、P暂停这张表同时可以复制进README。详细设计把2.2节的rotateShape和2.3节的clearLines贴进去配两段话为什么矩阵旋转用转置加翻转为什么消行从底向上扫。让老师觉得你不只写了代码还能讲清取舍。测试报告列六到八个用例左移越界、右移越界、旋转贴墙、单行消、四行消、硬降、暂停恢复每个用例一行预期结果最后附截图。4.3 注释规范与命名三十分钟给代码“化妆”高分项目的代码未必有炫技写法但一定注释清晰、命名统一。文件头注释模板可以直接抄三十分钟能把所有源文件补齐。下面这个头文件同时展示了头文件守卫、类声明和注释规范。/** * file GameLogic.h * brief 俄罗斯方块核心逻辑与 Qt 界面完全解耦 * author 你的名字 * date 2026-03 */ #ifndef GAMELOGIC_H #define GAMELOGIC_H #include vector class GameLogic { public: GameLogic(); void moveLeft(); void moveRight(); void rotate(); bool softDrop(); void hardDrop(); void togglePause(); bool isGameOver() const; int currentScore() const; int currentLevel() const; const std::vectorstd::vectorint board() const; const struct ActiveBlock currentBlock() const; private: void spawnBlock(); bool move(int dr, int dc); bool tryRotate(); int clearAndScore(); std::vectorstd::vectorint board_; ActiveBlock cur_; bool paused_ false; bool gameOver_ false; int score_ 0; int level_ 1; int linesCleared_ 0; }; #endif // GAMELOGIC_H注释规范就三条。文件头说清这个文件干什么函数头只写外部看不到的行为约定比如“软降成功返回true失败表示已触底”代码里只注释反直觉的步骤比如消行循环里的i。变量命名统一小驼峰成员变量加下划线后缀常量全大写这套约定在C项目里最不招人挑刺。5. 俄罗斯方块Qt项目常见问题与避坑编译、乱码与卡顿这一章是我做过这类项目后采出来的坑全是在课设最后几天最浪费时间的问题。按“现象、原因、解决”顺序写遇到直接对号入座。5.1 编译不过找不到Qt头文件、构建套件混乱、运行库缺失现象拿到别人的.pro文件后直接运行Qt Creator报一堆QMainWindow: No such file or directory链接阶段全是未定义引用把构建目录复制到另一台电脑运行弹窗提示缺libstdc-6.dll或Qt5Widgets.dll偶尔打开项目后Qt Creator直接崩溃。原因最常见是.pro里漏了QT widgets其次是项目路径带中文或空格qmake生成路径异常再就是别人用MSVC编译器你这边套件是MinGW混用旧构建目录就会冲突。运行库缺失是部署问题目标机器缺少对应的Visual C Redistributable2015-2022 x64或Qt动态库。解决先在项目属性确认当前构建套件Kit选的是MinGW还是MSVC删除build目录重新qmake再构建.pro里显式写QT widgets部署时用Qt自带的windeployqt把运行库拷出来。Windows下用MSVC编译的程序目标机器装对应版本的Visual C Redistributable能解决大部分“dll找不到”的弹窗。# Windows 下生成 exe 的部署依赖 cd build windeployqt Tetris.exewindeployqt会自动把用到的 Qt 运行库复制到 exe 同目录省去手动翻Qt安装目录的功夫。执行完检查plugins文件夹是否生成缺失会导致目标机器上qwindows.dll 找不到。5.2 中文乱码注释里全是问号界面按钮显示成菱形块现象源代码文件用记事本打开正常Qt Creator里中文注释变乱码程序运行后QLabel设置的中文变成“”或空方块。原因Qt Creator默认按UTF-8读源文件而文件是GBK/GB2312保存的MSVC编译器在Windows上默认按本地代码页读文件和UTF-8互相不认。两边都读错中文就废了。解决把源文件统一转成UTF-8 with BOMQt Creator里“工具-选项-文本编辑器-默认编码”改成UTF-8如果项目已经全是GBK不想重转在.pro里给MSVC编译器加/utf-8参数。界面字符串涉及中文时用QString::fromLocal8Bit兜底它在Windows上按本地代码页转码兼容老环境。scoreLabel-setText(QString::fromLocal8Bit(分数));这样写比直接scoreLabel-setText(分数)稳至少在你自己的编译环境上不会出现“分数”变“锟斤拷”的问题。统一编码是根治方案fromLocal8Bit只是后悔药。5.3 按住左键方块跑好几格、下落一顿一顿现象按住方向键不松方块被快速刷到边界消行时界面明显卡一下才继续。原因键盘的keyRepeat机制导致keyPressEvent连续触发每触发一次就移动一格卡顿通常是消行循环里做了重活或者用了sleep模拟下落。解决keyPressEvent开头判断event-isAutoRepeat()是就return把连续移动交给QTimer每帧只处理一次。硬降不写while循环加sleep而是不断调用softDrop直到失败每步只操作内存数据最后统一update()。QTimer的间隔不要小于100ms等级加速用“600减等级乘50”这类线性公式最低留200ms太快人眼和键盘都跟不上。5.4 移动后留下残影、窗口缩放错位现象方块移动后老位置留有一块颜色残影旧格子和新格子叠在一起拖拽窗口边缘后棋盘整体错位。原因paintEvent只画了活动方块而没重画整个棋盘或者update()刷新区域只覆盖新旧两个位置但漏了背景窗口拉伸后CELL_SIZE是固定像素画布尺寸和棋盘尺寸失去比例。解决paintEvent第一步就把整个rect()用背景色fillRect填满再画已固定格子和当前方块移动多远都不会残留GameArea构造函数里setFixedSize(COLS * CELL_SIZE, ROWS * CELL_SIZE)固定画布从根上杜绝缩放错位。Qt默认双缓冲足够应付20x10格子不必再上QPixmap离屏缓存。5.5 拷贝项目给老师后资源文件丢失现象项目放到U盘或网盘换台机器打开图片、音效全部加载失败界面空一块。原因代码里用了绝对路径加载资源例如C:/Users/xxx/Desktop/Tetris/images/background.png或者资源文件压根没加进.qrc。解决把图片、音效统一放进Qt资源系统加载路径写成:/images/xxx.png编译后资源直接嵌进可执行文件。.qrc文件加进.pro换电脑就不再依赖盘符和目录。音效如果不想放资源里至少要在文档里写明相对路径别用绝对路径。6. 把作业做成工程验证方法、操作回放与收尾技巧6.1 用一张测试用例表把代码跑透交付前花半小时按用例表过一遍比靠手感随便按按有效得多。这套用例表可以直接复制到测试报告里。编号操作预期结果T01启动程序随机方块出现在顶部中间T02按左/右键不越界时移动一格贴边时不动T03按上键旋转贴墙时尝试旋转碰撞则旋转失败T04软降触底方块固定新方块生成T05填满一行该行消除分数增加T06连消四行计数为4按四行规则计分T07按空格方块立即落底并固定T08按P暂停/继续画面冻结时间不再推进如果GameLogic按第4章独立出来了可以写个临时main_test.cpp直接调用moveLeft接口跑T02不用打开窗口。这也是检验逻辑层和界面层是否真解耦的办法。6.2 两个能加分的进阶功能操作回放与状态机操作回放是俄罗斯方块课设里性价比很高的亮点。实现思路是维护一个QVector记录每次按键的类型和时间戳暂停时清空重录再加一个“播放”键按时间戳重放。答辩时老师会问“你凭什么保证回放和当时操作一致”你正好讲事件流和定时器这是全程最能体现工程感的部分。状态机则是把游戏循环从零散if里解放出来用枚举表示Playing、Paused、GameOver三种状态keyPressEvent和onTimeout第一行先看状态非法操作直接忽略。加上它暂停、重开和结束的代码不再互相打架。我初次做这个课设时吃过一次亏为了代码短把逻辑全写在GameArea里后来想加回放功能发现没法在不重写界面前提下拿到历史操作最后花了一晚上重构。现在回看那个教训反而成了答辩时讲“为什么分层”的最好素材。俄罗斯方块这个题目能承载的技能点有限但把这些点串成闭环这份源代码和项目文档就能稳稳站住。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站