news 2026/10/18 10:32:25

QT C++俄罗斯方块源码拆解:从QGraphicsView到游戏循环实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
QT C++俄罗斯方块源码拆解:从QGraphicsView到游戏循环实现

简介:QT与C++实现的俄罗斯方块完整源代码,面向正在学习Qt桌面开发或经典游戏逻辑的初学者与进阶者。压缩包共12个文件,仅58KB,包含4个cpp源文件、3个h头文件、1个pro工程文件及png/jpg预览截图;cpp与h分离接口实现,配工程文件可直接用qmake构建。项目以Tetris游戏为主线,用QGraphicsView/QGraphicsItem绘制游戏区域、方块和网格,用QTimer控制下落节奏,并通过信号槽机制处理旋转按钮、碰撞检测与消行判断。各方块类型独立成类,主逻辑封装在专门控制器中,随机生成与行消除函数体现模块化设计;动画更新和事件驱动流程清晰,适合逐行研读。已有466人学习浏览,可从中掌握Qt GUI编程、C++面向对象设计、定时器调度和工程构建方法;资源体量小巧而结构完整,也可作为课程设计或二次开发起点。

1. 一份能跑的QT C++俄罗斯方块:从入门到理解图形界面游戏

如果你翻过不少QT教程,脑海里的QGraphicsView还停留在“可以画矩形”的认知,那这份QT C++俄罗斯方块源代码值得下下来拆一遍。它不是那种只有几百行的教学Demo,而是一个把经典游戏完整落地的工程:方块生成、旋转、碰撞检测、行消除、计分逻辑全部串起来了,截图、界面预览、源文件布局图都随包附带,对着代码学比对着视频学要踏实得多。适合两类人:一是刚学完C++和QT面向对象想找个完整项目练手的新手,二是被教程案例喂饱但没接触过QTimer驱动动画和QGraphicsItem绘制实战的人。看完你会明白一个游戏循环在QT里是怎么被信号槽和定时器撑起来的。

2. 项目全景与技术选型:QT的QGraphicsView为什么是这块游戏的正确底座

2.1 资源包内容:先看布局再动手

拿到这个压缩包,先别急着解压跑代码,按文件名把素材捋一遍会更有效率。包里除了源码,还带了三张关键的辅助图:捕获.JPG 是程序运行时的真实截图,界面预览.png 展示的是窗口布局和控件排布,源文件布局.png 则是整个工程的目录树。这三张图对新手特别友好,因为它把“正确答案”直接摆在你面前了——你写完代码跑出来的界面,应该和截图一致;如果类文件组织乱了,看一眼布局图就能对回原来的结构。

我的习惯是先看界面预览再碰代码,这样进入源码时心里已经有一张画面地图:哪里是游戏主区域,哪里是下一块预览区,哪里显示得分。很多初学者一上来就找main.cpp,结果被一堆文件绕晕,其实先花五分钟看图片,后面读代码会顺很多。

2.2 技术栈选型:为什么是QGraphicsView而不是QWidget

实现俄罗斯方块,单纯用QWidget和painterEvent也能画出来,但这份源码选择了QGraphicsView作为游戏画板,这个选择值得反复琢磨。QGraphicsView带来的是一整套“场景-视图-图元”三层架构:QGraphicsScene持有所有游戏元素,QGraphicsView负责渲染呈现,QGraphicsItem则代表每个可交互的方块格子。对于俄罗斯方块这种需要频繁增删大量矩形格子的游戏,这个架构天然适合。

对比一下就知道差距:QWidget方案里,每一次画面更新都要调用update()触发整个窗口重绘,方块一多性能就肉眼可见地掉帧;而QGraphicsScene管理的是一个图元列表,每个图元是独立对象,移动方块时只需要更新对应item的坐标,不需要重绘整个棋盘。源码里的网格线、已落下方块、当前活动方块,都各自以QGraphicsItem的形式存在,这在调试时也是个福利——你可以单步检查每个item的坐标值,而不是面对一整个painter事件。

另外一个务实原因是事件分发。QGraphicsView把鼠标和键盘事件先用scene坐标系统做了一层转换,对于要做点击旋转、拖拽落下的扩展玩法,直接重写scene的event处理方法就行,不必自己写坐标换算,省掉很多重复劳动。

2.3 目录结构与构建方式

从这个项目的源码布局来看,工程遵循了QT里最经典的“界面与控制分离”结构:启动入口main.cpp只负责实例化主窗口并进入事件循环,游戏逻辑全部收敛到Board类里,方块形状数据则抽象成Piece类。头文件(.h)与实现文件(.cpp)严格分开,每个类都保持单一职责,这刚好是C++面向对象设计里最值得学习的部分。

构建方式用的是qmake,这是QT传统工程的标准工具链。在终端里跑到源码目录执行:

qmake tetris.pro make

qmake会读取工程文件里的QT += core gui、TARGET、SOURCES等配置,自动生成对应平台的Makefile。需要说明的是,如果用的是QT Creator,直接打开.pro文件点运行即可;命令行方式则要保证qmake在当前PATH环境变量里。构建完成后,生成的二进制文件会放在Makefile指定的目标路径下,在Linux/Unix系统里通常就是当前目录的tetris可执行文件。

提示:如果你在多版本QT环境里切换过,qmake可能指向旧版本。执行qmake --version确认一下,避免版本不一致导致链接错误。

对于想二次开发的人,这个目录组织的可读性很重要:改游戏规则去Board类找,调整方块样式去Piece类找,动界面布局去MainWindow里找,视线不需要在几十个文件里乱跳。

3. 核心模块拆解:一个方块从生成到消行的完整生命周期

3.1 方块类型设计:用类封装形状、颜色和行为

俄罗斯方块一共有七种标准形状,每种形状的旋转规则和颜色都不相同。这个项目的Piece类用一组相对坐标点来描述形状,这是最直观也最容易扩展的做法。每个方块对象记录着自己当前四个小格子的相对位置,以及属于自己的颜色。

// piece.h —— 单个方块的形状与状态 class TetrisPiece { public: // 用坐标点集合描述形状,每个点相对piece原点的偏移 using Shape = QVector<QPoint>; TetrisPiece(); // 返回当前旋转状态下的形状坐标 Shape shape() const { return coords; } // 顺时针旋转:对每个点做 (x, y) -> (-y, x) 变换 void rotate() { for (QPoint &p : coords) { int x = p.x(); int y = p.y(); p.setX(-y); p.setY(x); } } QColor color() const { return mColor; } private: Shape coords; QColor mColor; };

这里的核心是rotate函数里的坐标变换公式。顺时针旋转90度,本质上是把每个像素点绕原点旋转变换,数学表达式就是新的x等于负的旧y,新的y等于旧的x。这个变换有个特点:连续执行四次会回到初始状态,所以代码里不需要额外维护旋转状态,直接原地修改坐标即可。初次接触的人容易把旋转写成分段枚举,比如为每个方向准备一套坐标,那样代码会膨胀四倍,而且漏掉任何一个方向都会出bug。

使用坐标点而不是二维数组的另一个好处是碰撞检测简单。检测一个方块是否与边界或已填格子冲突,只需要把每个相对坐标叠加到方块当前位置上,再检查目标格子是否越界或已有内容即可,没必要遍历一个4x4矩阵。这个设计直接影响了后面的核心逻辑写法。

3.2 游戏主逻辑:Board类如何拆分职责

游戏的核心状态全部集中在Board类里,它维护了一个二维网格数组、当前活动方块、游戏是否开始的标记,以及计分数据。这个类继承了QGraphicsScene,因为游戏区域本身就是场景,键盘事件和绘制都由场景统一接管。

// board.h —— 游戏主逻辑承载类 class GameBoard : public QGraphicsScene { Q_OBJECT public: GameBoard(QObject *parent = nullptr); void startGame(); void setTimerInterval(int msec); // 外部调节下落速度 protected: void keyPressEvent(QKeyEvent *event) override; private slots: void moveDown(); // 定时器触发的下落动作 private: bool tryMove(const QPoint &newPos); // 尝试把当前方块移动到newPos bool hasCollision(const TetrisPiece &piece, const QPoint &pos); int removeFullRows(); // 消除满行,返回消除行数 void draw(); // 把网格和当前方块绘制到场景 int grid[ROW_COUNT][COL_COUNT]; // 记录已落下方块的网格 TetrisPiece curPiece; QPoint curPos; bool isStarted; };

这段代码最值得学习的是把“游戏规则”和“界面呈现”揉进了同一个类却没有乱。grid负责逻辑,draw负责呈现,tryMove负责校验,moveDown负责定时推进。如果你刚开始接触游戏编程,可能会觉得一个类里既有数据又有界面不太符合“分层”思想,但俄罗斯方块这种单窗口小游戏,把强关联的状态放一起反而是最清晰的写法。真要拆成game engine和renderer两个大类,在这个体量下反而增加理解成本。

startGame时做的第一件事不是马上生成方块,而是把网格数组清零、把旧的图元从场景中移除。这个“重置状态再开始”的流程容易被新手忽略,导致连续玩两局时旧方块还残留在画布上。

3.3 绘制链路:从scene到item再到视图渲染

绘制逻辑的入口是draw函数。它把网格里每个非空格子映射成场景里的一个矩形图元,同时把当前活动方块也画上去。这里利用了QGraphicsScene对重复项的自动管理:新方块需要显示时,直接addItem;方块落定后,它自然变成网格的一部分,不需要额外操作。

// board.cpp —— 网格与方块的绘制逻辑 void GameBoard::draw() { QGraphicsRectItem *rect = nullptr; // 优先清掉上一帧的活动方块图元,避免重叠 for (QGraphicsItem *item : items()) { if (item->data(0).toInt() == 1) { removeItem(item); delete item; } } // 画网格中已固定的方块 for (int row = 0; row < ROW_COUNT; ++row) { for (int col = 0; col < COL_COUNT; ++col) { if (grid[row][col] != EMPTY) { rect = new QGraphicsRectItem(0, 0, CELL_SIZE, CELL_SIZE); rect->setPos(col * CELL_SIZE, row * CELL_SIZE); rect->setBrush(QBrush(gridColor[row][col])); rect->setData(0, 0); // 标记为固定方块 addItem(rect); } } } // 画当前活动方块 for (const QPoint &p : curPiece.shape()) { rect = new QGraphicsRectItem(0, 0, CELL_SIZE, CELL_SIZE); rect->setPos((curPos.x() + p.x()) * CELL_SIZE, (curPos.y() + p.y()) * CELL_SIZE); rect->setBrush(QBrush(curPiece.color())); rect->setData(0, 1); // 标记为活动方块 addItem(rect); } }

这段代码里的setData是一个容易忽略但很重要的细节。它给图元挂了一个自定义整数标记,后续移动当前方块时,就能通过data(0)的值快速从场景里找出需要删除的旧图元,而不是误删已经固定的方块。如果你不用这个标记,draw函数执行两次就会出现“幽灵方块”——同一个位置叠了新旧两层图元,颜色还会变深。项目里用这个技巧保证每次重绘只保留最新状态,对应的更新策略是删除全部活动块图元左右,如果遇到方块平移频繁的场景,也可以改成只更新pos,但原始工程显然更看重代码直白程度。

QGraphicsView配合QGraphicsScene时,还有一个常见玄学问题:为什么画出来的方块不在期望的位置?原因多半是没有正确设置sceneRect。这个项目里,场景的原点对应棋盘的左上角,每个格子的像素尺寸是固定的CELL_SIZE,所以映射关系只要做一次乘法就行。看到坐标错位时,先检查是不是这里少乘了一个值。

4. 信号槽与碰撞检测:游戏交互从按键到状态变更的链路

4.1 QTimer驱动:游戏循环的骨架

俄罗斯方块没有复杂的物理引擎,它的“游戏循环”就是定时器反复触发同一个动作。项目里创建了一个QTimer,设置固定间隔,超时后发射timeout信号,连接的槽函数执行下落一步。这个机制把游戏的节奏感变成了一行配置:

// mainwindow.cpp —— 定时器初始化与信号槽连接 gameTimer = new QTimer(this); gameTimer->setInterval(500); // 每500毫秒下落一格 connect(gameTimer, &QTimer::timeout, board, &GameBoard::moveDown); gameTimer->start();

setInterval的单位是毫秒,这里500表示半秒一格的初始速度。很多新手在这里翻车,把500当成秒,导致方块慢得像蜗牛爬,或者为了加速把间隔设置成50结果快到没法操作。调速的正确做法是保留一个基准速度变量,消行数越多就把间隔设得越小,而不是直接硬编码一个极端值。

QTimer驱动模式还有个容易被忽略的点:它不保证精确的定时精度,如果主线程在短时间内处理大量绘制操作,timeout信号的发射可能会被挤到后面。在俄罗斯方块这个体量下感受不到,但如果以后你拿这套架构去做更复杂的游戏,就得考虑把逻辑计算和渲染分离到不同线程。这里用单线程就够了,因为每次绘制只有最多一两百个矩形,性能压力很小。

moveDown槽函数内部其实不是无脑向下移动,它会先调用tryMove试探下一步位置是否合法,如果不合法就说明当前方块已经落定,这时把它的形状写进grid数组,然后检查消行、生成新方块。这个“先试探再落定”的模式是整个游戏逻辑的枢纽,也是接下来理解碰撞检测的关键入口。

4.2 键盘事件处理:把按键变成旋转和位移

游戏操作依赖keyPressEvent事件重写,所有按键动作都集中在游戏场景类里。方向键控制左右移动,上方向键旋转,下方向键加速下落,空格直接硬降到最低点。每个按键分支都调用tryMove或者rotate,然后更新画面:

void GameBoard::keyPressEvent(QKeyEvent *event) { if (!isStarted) return; switch (event->key()) { case Qt::Key_Left: tryMove(curPos + QPoint(-1, 0)); break; case Qt::Key_Right: tryMove(curPos + QPoint(1, 0)); break; case Qt::Key_Up: curPiece.rotate(); if (!hasCollision(curPiece, curPos)) { draw(); } else { // 旋转后撞墙则回退,避免转出一个非法状态 curPiece.rotate(); curPiece.rotate(); curPiece.rotate(); } break; case Qt::Key_Down: moveDown(); break; case Qt::Key_Space: // 硬降,循环下移直到触底 while (tryMove(curPos + QPoint(0, 1))) { // 每步都更新画面,实现连续下落效果 draw(); } moveDown(); break; default: QGraphicsScene::keyPressEvent(event); } }

旋转处理这里有个细节值得品:执行旋转后马上做了碰撞回退。因为某些方块在靠墙时旋转会越过边界,如果不回退,方块就会“嵌”进墙里。回退的方式是连续旋转三次恢复原状,这利用了旋转公式四倍回到原点的性质,比起额外保存旋转前坐标要省事。硬降的写法也很有代表性,用while循环不断试探往下移动,直到失败那一刻,再调用一次moveDown让方块正式落定。

4.3 碰撞检测与行消除:把边界条件算清楚

tryMove是所有移动操作的唯一通道,它内部先调用hasCollision判断目标位置是否违规,合法才更新坐标。这个收口设计很重要,它保证没有任何移动能绕过碰撞检测,你不需要担心某个快捷键漏写了校验。hasCollision的判断范围包括左右下三面边界,以及网格里已经被占用的格子:

bool GameBoard::hasCollision(const TetrisPiece &piece, const QPoint &pos) { for (const QPoint &p : piece.shape()) { int nx = pos.x() + p.x(); int ny = pos.y() + p.y(); if (nx < 0 || nx >= COL_COUNT) return true; // 左右越界 if (ny >= ROW_COUNT) return true; // 触底 if (ny >= 0 && grid[ny][nx] != EMPTY) return true; // 撞到已固定方块 } return false; }

这里有意放过了ny < 0的情况,也就是允许方块刚生成时有一部分在棋盘顶部的上方。这个设定是俄罗斯方块的通行做法,给新方块进入画面留出缓冲空间,如果你把ny < 0也判为碰撞,游戏开始在生成第一个方块时就会直接判负。

行消除的逻辑稍加变化。从最底层往上扫描,只要发现某一行全部非空,就把它上面的所有行各自下移一格,然后清空最顶行。消除一行后还要原地再看一次当前行,因为上方落下来的行可能是满的。

int GameBoard::removeFullRows() { int removed = 0; for (int row = ROW_COUNT - 1; row >= 0; --row) { bool full = true; for (int col = 0; col < COL_COUNT; ++col) { if (grid[row][col] == EMPTY) { full = false; break; } } if (full) { // 当前整行下移一格 for (int r = row; r > 0; --r) { for (int c = 0; c < COL_COUNT; ++c) grid[r][c] = grid[r - 1][c]; } // 置空最顶行 for (int c = 0; c < COL_COUNT; ++c) grid[0][c] = EMPTY; ++row; // 因为上面落下来一行,原地再检查一次 ++removed; } } return removed; }

这段代码里++row这个操作是我觉得整份源码里最见功力的地方。如果不原地再查一次,连续出现两行同时满时,第二行会被漏判。很多人写消行时只消了一半,问题就出在这里。消除完成后返回的行数可以用来计算得分:消除一行记一分,连续多行可以设计加成分。

5. 避坑指南:编译运行调试中我踩过的五个坑

5.1 界面黑屏或方块全部挤在左上角

现象:程序跑起来,游戏区域一片黑,方块全部出现在窗口左上角巴掌大的地方,网格线也对不齐。

原因:QGraphicsScene默认的原点在(0,0),但如果你没有显式设置sceneRect,视图会按照所有item的内容范围自动缩放,导致坐标系和棋盘网格对不上。另一个常见叠加原因是把网格坐标直接当像素坐标用了,没乘格子边长。

解决:在构造场景时显式调用setSceneRect(0, 0, COL_COUNT * CELL_SIZE, ROW_COUNT * CELL_SIZE),并确认所有item的坐标都是“列索引 * CELL_SIZE”而不是直接用列号。如果之后发现方块位置偏离半格,多半是pos设置时把原点多算了一格。

5.2 qmake成功了,一运行就崩溃

现象:qmake和make都顺利走完,双击生成的exe或者从命令行启动,窗口都没弹出来就崩了。

原因:最常见的是QGraphicsItem对象new出来了但没有addItem到scene里,或者QTimer连接时用了旧式的字符串语法,信号槽参数类型不匹配,直到事件循环触发时才炸。尤其是前者,QT文档里明确写了QGraphicsItem必须依附于scene,否则就是一个悬空对象,任何对它的操作都可能段错误。

解决:所有图元创建后立即调用scene->addItem();connect统一用&QTimer::timeout这类新式函数指针写法,让编译期替你做参数检查。如果还是崩,用调试器看堆栈,我踩过的一次就是卡在绘制函数里访问了一个空指针引用。

5.3 方块落到底之后还能左右移动,甚至嵌进已填充的行

现象:方块明明已经堆到底了,按左右方向键依然会平移,甚至会穿进已经固定好的方块堆里。

原因:tryMove函数只校验了纵向移动,左右平移分支另写了一套移动逻辑,没有复用hasCollision。两边逻辑不一致是这类小游戏最容易埋雷的地方。

解决:让左、右、下三种移动全部走同一个入口tryMove,这个入口内部统一调用hasCollision。hasCollision里把左右边界和下边界、已占用格子的判断一次性算完,不要在三个地方各写一份边界条件。我一般还会加一行调试输出,在移动失败的瞬间把目标坐标打出来,一眼就能看出是哪个方向的判断漏了。

5.4 中文目录下qmake构建失败

现象:项目放在“某个盘/我的文档/项目/tetrix”这种带中文的路径下,make时报错找不到头文件或目标文件,拷到纯英文路径就好了。

原因:Windows下用MinGW工具链时,make生成的依赖规则对中文路径处理不友好,路径串里的中文编码和反斜杠拼接容易断链。这个问题在旧版本的QT Creator里尤其频繁。

解决:所有QT工程一律放在纯英文路径下,路径中也不要带空格。这个习惯后来帮我避开了大量构建玄学问题,毕竟谁都不想在排查了一晚上编译器错误之后,最后的解决方案是改文件夹名。

5.5 消行之后上面的方块没有掉下来,像悬空一样

现象:凑满一行后被清掉了,但头顶上一堆方块没有补齐空白,网格里出现一条悬空的裂痕。

原因:消行时只做了“把当前行清零”,没有把该行上方的所有内容整体下移一行。这在画面上的表现就是行确实消失了,但上面的方块留在原地没跟着挪下来。

解决:严格按“从下往上扫描,满行就把上面的行逐行下移一格,再把顶行置空”的顺序来写。注意下移必须从目标行开始往上遍历,而不是从上往下,否则会把下面的数据覆盖掉。写完后用连续两行同时满的用例来验证,能暴露漏判问题。

6. 二次开发思路:从复刻经典到做一个有自己风格的版本

6.1 改造方向一:增加下一个方块预览

当前工程只显示活动方块,下一个方块的信息藏在逻辑里。实现预览功能并不难:在Piece类里增加一个获取形状和颜色的接口,MainWindow区域右侧放置一个独立的QGraphicsView,每次生成新方块时把下一块的形状画进去。只需要在生成逻辑里多保存一份“预备方块”对象,并在消行后触发的更新函数里同步刷新预览场景。这个改动能让你更早接触“多视图共享同一份数据”的QT设计模式。

6.2 改造方向二:引入计分板与等级变速

现有计费逻辑完成度已经足够跑通,但节奏是固定的。你可以把下落间隔和消除行数绑定:每消满十行,把计时器间隔减少50毫秒,同时更新界面上的等级数字。实现上只需在removeFullRows的返回值基础上,做一个累加判断,然后调用setTimerInterval把新的间隔写到QTimer上。要注意设置一个最低间隔上限,比如100毫秒,否则几十级之后游戏快到完全没法操作,瞬间从方块游戏变成抽风游戏。

6.3 改造方向三:加入暂停与重新开始

暂停功能适合检验你对事件驱动模型的理解。加一个bool变量isPaused,在keyPressEvent里拦截P键,暂停时调用gameTimer->stop(),恢复时再start()。重新开始则要求初始化全部网格数据和场景图元,注意清理残留的item。

从那以后,我每次拿到一个QT小游戏源码,第一件事不是急着改功能,而是先画一遍“按键到状态变更”的链路图,把tryMove、hasCollision、draw这条主链标注出来,再动手。这次拆QT C++俄罗斯方块源码,我按这条路线走,半天就把整个工程吃透了,后面加预览、调速都顺顺利利,没再被碰撞边界的边界条件绊住脚。这份源码包里还带了界面截图和布局图,下载后对照着文件名看,能少走不少弯路。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 10:28:43

Claude Code 命令实战:用 TaoToken 统一 Key 打通斜杠命令与全局记忆

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 10:28:13

AI率检测原理与降AI率工具实战:9类方法让论文更像人写的

写论文用AI辅助&#xff0c;现在已经不算什么新鲜事了。可“AI率检测”这把尺子一压下来&#xff0c;不少本科生直接被整懵&#xff1a;明明自己改了半宿&#xff0c;报告里还是标着一大片“疑似AI生成”。我在学校帮人改稿子这些年&#xff0c;见过太多人被检测结果吓住&#…

作者头像 李华
网站建设 2026/10/18 10:27:05

HarmonyOS智慧农业病虫害图像识别:端侧推理全链路落地实践

做了这么久的HarmonyOS智慧农业应用系列&#xff0c;前面十几篇基本都在跟传感器数据、告警逻辑、生长环境模型较劲。到了第18篇&#xff0c;我想换个切入点&#xff0c;聊聊一个让很多人又爱又恨的功能——图像识别&#xff0c;具体说就是病虫害诊断。这篇开始我没打算直接贴代…

作者头像 李华
网站建设 2026/10/18 10:25:13

基于Flask的中文情感分析系统:从模型训练到Web部署全解析

简介&#xff1a;面向本科毕业设计的中文情感分析系统完整项目包&#xff0c;基于Python、Flask与MySQL实现&#xff0c;用深度学习模型对评论做正面/负面分类。资源覆盖登录注册、后台首页统计看板、文本分析及软件测试流程&#xff0c;适合计算机专业学生快速搭建可演示环境&…

作者头像 李华
网站建设 2026/10/18 10:24:45

时间同步全解析:从NTP原理到chrony实战排查

搞了这么多年运维&#xff0c;最容易被忽视又最能惹祸的系统基础服务&#xff0c;时间同步绝对排得上号。平时安安静静躺在后台&#xff0c;一旦出问题&#xff0c;轻则日志对不上、监控告警乱跳&#xff0c;重则数据库主从复制错乱、分布式任务重复执行、证书校验失败&#xf…

作者头像 李华
网站建设 2026/10/18 10:24:27

CSS class选择器:可重复性、样式复用与命名规范实战

1. 从类名那个“可重复”开始说起每个写过CSS的人&#xff0c;第一天接触的就是选择器。而class选择器&#xff0c;恐怕是日常开发里用得最多、最顺手的一个。标题里“可重复”这三个字&#xff0c;正是它的灵魂所在——一个class可以挂在任意多个元素上&#xff0c;同样一组样…

作者头像 李华