用Qt开发数独游戏:从界面绘制到回溯算法的完整工程实践
简介这是一份基于Qt框架开发的数独游戏完整工程源码面向C/Qt初学者和需要GUI项目练手的开发者覆盖了从界面搭建、事件响应到数独算法实现的全部环节。资源包共141个文件压缩后约25.6MB其中以21个cpp源文件、15个头文件、8个ui界面文件、png图片和table数据文件为主同时提供pro工程、qrc资源、makefile及debug/release配置便于直接构建和二次开发。已有2558人学习下载。源码中可重点学习Qt信号与槽机制、QWidgets与QLayout的界面组织、基于MVC的盘面逻辑处理以及QFile存档读写还包含QPixmap背景切换、规则讲解对话框等细节可延伸思考数独生成与求解算法的实现。整体结构清晰注释完整适合作为课程设计、毕业设计或Qt实战入门参考。1. 为什么用 Qt 写数独从「能玩」到「值得做」的分水岭数独大概是桌面开发里最被低估的练手项目。它表面上只是一个 9×9 的格子但当你真用 Qt 动手写一遍就会发现界面渲染、鼠标交互、状态管理、算法生成、难度控制、撤销重做几乎把桌面应用开发的骨架全过了一遍。很多从业者拿它当 Qt 入门的第一个完整项目不是因为简单而是因为它「麻雀虽小、五脏俱全」——你既要处理视图层的绘制与响应又要在模型层维护一套合法的数独状态机还得在算法层解决「生成一个有唯一解、难度可控的题面」这个不那么 trivial 的问题。我最初写这个项目的时候想的只是做一个能填数字的小工具结果做到一半才发现真正的工程量全在「生成一道好题」和「判断用户填错了没有」这两件事上。就算你用 Qt 自带的 QTableWidget 硬刚也能在两小时内拼出一个能玩的壳子但那个壳子会在三个地方翻车卡死、误判、题目无解。这篇笔记就沿着「界面 → 算法 → 交互 → 排错」的顺序把一套完整可复现的做法讲清楚适合已经会写基本 Qt 信号槽、想完整走一遍项目的人也适合想给自己的作品集加一个「有算法深度、有 UI 细节」的桌面应用的人。2. 界面选型与工程骨架QGraphicsView 还是 QTableWidget2.1 为什么最终选择 QGraphicsView 而不是 QTableWidget第一版我图省事直接用 QTableWidget。填数字、读数字都简单信号槽一套就完事。但做到第三步「标记候选数」就是用户在格子里临时记几个可能的数字的时候QTableWidget 的 cellWidget 方案就变得非常拧巴——你得把每个格子塞进一个自定义 QWidget还要在数字和候选标记之间来回切换性能和代码可读性都会快速劣化。所以我建议直接用 QGraphicsView QGraphicsScene 来自绘棋盘。这不是炫技而是因为数独的视觉表达本质上是「一个表格 多种图层状态」不可编辑的题面数字、用户填的实数字、候选小数字、错误高亮、选中高亮。用 QGraphicsItem 体系最自然的做法是让每个格子成为一个 QGraphicsItem通过重写 paint() 在同一个格子里画出数字和候选标记选中、冲突、不可编辑三种状态也能在 paint() 里统一处理而不是靠堆控件。工程结构上我一般拆成四个文件sudoku/ ├── main.cpp ├── boardwidget.h / boardwidget.cpp // 棋盘视图绘制、鼠标事件 ├── sudokuboard.h / sudokuboard.cpp // 数据模型合法性校验、状态 ├── generator.h / generator.cpp // 生成题面与求解器 └── mainwindow.h / mainwindow.cpp // 主窗口菜单、按键、组装我用的是 QWidget QGraphicsView 而非 QML。原因有两个其一数独的交互逻辑以键盘和鼠标为主QWidget 的信号槽机制写起来更直接其二这一项目如果你是想练 C/Qt Widgets 技能QML 的语法重心会让你的注意力从算法和状态管理上移开。如果你后续想做触屏或者动画效果再考虑换 QML 也不迟。2.2 棋盘自绘的最小代码一个能画格子与数字的 Item这里给出一个可以直接跑起来的核心类骨架。它不依赖任何外部资源画出 9×9 格子、粗线分隔、以及数字// cellitem.h #ifndef CELLITEM_H #define CELLITEM_H #include QGraphicsItem #include QFont class CellItem : public QGraphicsItem { public: CellItem(int row, int col, int cellSize, QGraphicsItem *parent nullptr); void setValue(int v) { m_value v; update(); } // v: 1~9; 0 表示空白 void setEditable(bool e) { m_editable e; update(); } void setSelected(bool s) { m_selected s; update(); } void setConflict(bool c) { m_conflict c; update(); } QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; private: int m_row, m_col; int m_cellSize; int m_value 0; bool m_editable true; bool m_selected false; bool m_conflict false; }; #endif // CELLITEM_H// cellitem.cpp #include cellitem.h #include QPainter #include QPen CellItem::CellItem(int row, int col, int cellSize, QGraphicsItem *parent) : QGraphicsItem(parent), m_row(row), m_col(col), m_cellSize(cellSize) { setPos(col * cellSize, row * cellSize); } QRectF CellItem::boundingRect() const { return QRectF(0, 0, m_cellSize, m_cellSize); } void CellItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); // 1. 背景 painter-fillRect(boundingRect(), m_selected ? QColor(#e8f0fe) : (m_conflict ? QColor(#fdecea) : QColor(#ffffff))); // 2. 边框每 3x3 块边界用粗线其余用细线 QPen pen; pen.setWidth((m_row % 3 0 m_col 0) || (m_col % 3 0) ? 2 : 1); pen.setColor(QColor(#333333)); painter-setPen(pen); painter-drawRect(boundingRect().adjusted(0, 0, -pen.width(), -pen.width())); // 3. 数字 if (m_value ! 0) { QFont font painter-font(); font.setPixelSize(m_cellSize * 0.6); font.setBold(!m_editable); // 题面数字加粗 painter-setFont(font); painter-setPen(m_editable ? QColor(#1a73e8) : QColor(#202124)); painter-drawText(boundingRect(), Qt::AlignCenter, QString::number(m_value)); } }这里的 paint() 逻辑是核心先用QColor区分选中和冲突背景再根据行列位置决定粗线每 3 行/列起点或细线数字绘制时用setBold区分题面不可编辑和用户输入可编辑蓝色。setPos(col * cellSize, row * cellSize)让每个 item 自己定位简单且高效。2.3 什么时候用 QTableWidget 反而更合适如果你的需求是「做个 Excel 风格的表格式输入」或者说你只想快速做一个内部用的填数字工具、不打算把代码演化成完整产品那 QTableWidget 完全够用而且开发速度快得多。QTableWidget 最大的优势是自带了表格编辑、焦点切换和默认的键盘导航最大的劣势是候选数展示、自定义绘制、冲突高亮这三件事都会越改越别扭。一句话建议二维规则表格 自定义视觉状态选 QGraphicsView纯数据录入而不关心视觉定制选 QTableWidget。这个项目我坚定选 QGraphicsView因为后面所有的进阶功能——高亮行/列/宫、快速填候选、颜色标记——都在自绘的体系里更容易实现。3. 数独生成与求解回溯算法和挖洞策略3.1 求解器秒解任意唯一解题面的回溯实现数独生成的根基是一个可靠的求解器。不是让你在界面上做「提示」功能时才需要它而是生成题面时离不开——你得先随机生成一整个完整终盘填满数字的 9×9再 从里面挖掉一些格子每挖一个就要用求解器验证「挖掉之后是否仍然只有唯一解」。所以求解器的正确性和速度直接决定你能不能在用户点击「新游戏」时流畅出题。最稳妥的算法是回溯backtracking。对 9×9 数独来说纯回溯的求解时间通常远小于 1 毫秒完全没必要用舞蹈链这种高级结构。先看代码// solver.h #ifndef SOLVER_H #define SOLVER_H #include QVector class Solver { public: // 棋盘用 QVectorint 存 81 个格子0 表示空白。 // 返回值表示是否有解找到第一个解后 board 即被填充为解。 static bool solve(QVectorint board); static int countSolutions(QVectorint board, int limit 2); // 数解个数默认数到 2 就停 private: static bool isValid(const QVectorint board, int index, int num); static bool solveRecursive(QVectorint board, int pos); static bool countRecursive(QVectorint board, int pos, int count, int limit); }; #endif // SOLVER_H// solver.cpp #include solver.h #include algorithm bool Solver::isValid(const QVectorint board, int index, int num) { int row index / 9; int col index % 9; // 同行、同列 for (int i 0; i 9; i) { if (board[row * 9 i] num i ! col) return false; if (board[i * 9 col] num i ! row) return false; } // 同 3x3 宫 int boxRow (row / 3) * 3; int boxCol (col / 3) * 3; for (int r boxRow; r boxRow 3; r) { for (int c boxCol; c boxCol 3; c) { if (board[r * 9 c] num r * 9 c ! index) return false; } } return true; } bool Solver::solveRecursive(QVectorint board, int pos) { // 找到下一个空白格 while (pos 81 board[pos] ! 0) pos; if (pos 81) return true; // 没有空白格了说明填满求解成功 for (int num 1; num 9; num) { if (isValid(board, pos, num)) { board[pos] num; if (solveRecursive(board, pos 1)) return true; board[pos] 0; // 回溯 } } return false; } bool Solver::solve(QVectorint board) { return solveRecursive(board, 0); }逻辑说明solveRecursive从位置 0 开始扫描遇到空格就尝试 1-9每填一个数都要用isValid检查「这一个数放在这里是否合法」不合法就跳过。如果某个空格 1-9 全试过都不行就回退到上一层把上一格的值换掉再试。这本质上是深度优先搜索最坏情况理论上有 9^81 种状态但数独的强约束让实际搜索空间非常小。参数上注意pos 1的推进方式——不是简单pos 1而是下一轮递归会while跳过已填的格子这样可以避免不必要的递归层级。3.2 生成完整终盘先随机填对角线再做「求解式填充」生成终盘不能靠「随机填 81 个数再检查」那个搜索空间太大了。常见做法是「分别随机填好三个 3×3 对角线宫然后调用求解器把其余格子补完」。因为对角线三个宫互不冲突剩下的空格数只有 54 个回溯速度非常快。这样做的好处是每生成一次终盘都是一个新的随机排列而不是固定模板。// generator.cpp 关键片段 #include generator.h #include solver.h #include QRandomGenerator #include algorithm static void shuffleNine(QVectorint arr) { std::shuffle(arr.begin(), arr.end(), *QRandomGenerator::global()); } QVectorint Generator::generateFinishedBoard() { QVectorint board(81, 0); // 1. 准备 1~9 的随机排列 QVectorint nums {1,2,3,4,5,6,7,8,9}; shuffleNine(nums); // 2. 填三个对角线宫 (左上、正中、右下) for (int box 0; box 3; box) { int startRow box * 3; int startCol box * 3; for (int i 0; i 9; i) { int row startRow i / 3; int col startCol i % 3; board[row * 9 col] nums [(box * 3 i) % 9]; } } // 3. 调用求解器补全 Solver::solve(board); return board; }参数说明对角线宫填满后整个盘面不存在任何行/列冲突只是宫与宫之间还没有对齐。此时调用求解器求解器会在约束下快速填完剩余格子。你可能会问为什么只填对角线不随机填更多格子因为每多填一个非对角线格子都可能引入冲突导致回溯时间变长对角线宫是所有宫中最容易满足行/列约束的位置这个策略在工程上最省事。3.3 挖洞生成题面从唯一解到难度控制有了终盘接下来是「挖洞」去掉部分数字。这里最大的坑在于不是挖得越多越难。挖掉一个数字必须保证题面仍然有唯一解而且挖洞的位置分布决定了实际解题难度。我用的是「逐个挖洞 唯一解验证」的贪心策略QVectorint Generator::generatePuzzle(int cluesToRemove) // 比如想留 30 个数字 { QVectorint puzzle generateFinishedBoard(); // 先得到终盘 // 把所有格子的位置打乱 QVectorint positions(81); for (int i 0; i 81; i) positions[i] i; shuffleNine(positions); // 复用随机函数 int removed 0; for (int idx 0; idx 81 removed cluesToRemove; idx) { int cell positions[idx]; int backup puzzle[cell]; puzzle[cell] 0; // 关键验证唯一解 if (Solver::countSolutions(puzzle, 2) ! 1) { puzzle[cell] backup; // 不能挖恢复 } else { removed; } } return puzzle; }Solver::countSolutions的实现思路是还是走回溯但每找到一个解就计数并立即检查是否超过limit这里传 2即只要发现有两个解就停止。性能上绝大多数情况下countSolutions数到 2 只需要极短时间因为盘面里已经挖了不少洞回溯会很快发散。如果countSolutions(puzzle, 2)返回 2说明挖掉这个格会导致多解必须恢复。这是一种保守但稳定的挖洞策略——它不会尝试「一次挖走多个再验证」因此生成时间完全可预测。难度控制的参数留数字越少越难——一般留 28-32 个是「专家」35-40 个是「中等」45 个以上是「简单」。但更准确地说难度不完全取决于留多少个数字而是取决于挖洞的位置。后续做难度分级时可以在挖洞序列上多做文章先挖掉「一眼能看出来唯一解」的格子保留那些需要链式推理的格这就要引入候选数分析算法了。对这个项目先用「留多少数字」来控制难度足够稳定。4. 交互与状态管理从点击格子到撤销重做4.1 鼠标选中、高亮行/列/宫、键盘填数QGraphicsView 自带的鼠标事件处理在 QGraphicsItem 上并不直观——如果你需要让格子响应点击建议在视图层重写mousePressEvent用QGraphicsScene::itemAt找到对应的 CellItem而不是在每个 item 内部处理鼠标事件。这样所有格子的选中逻辑就集中在一个函数里不会出现多个 item 各自维护状态导致的冲突。// boardwidget.cpp 中重写鼠标事件 void BoardWidget::mousePressEvent(QMouseEvent *event) { QPointF scenePos mapToScene(event-pos()); // 视图坐标 → 场景坐标 QGraphicsItem *item scene()-itemAt(scenePos, QTransform()); CellItem *cell dynamic_castCellItem*(item); if (!cell) return; int row cell-row(); int col cell-col(); setSelectedCell(row, col); // 更新选中状态并通知场景刷新高亮 QWidget::mousePressEvent(event); }点击后要做三件事第一把该格设为选中状态第二高亮它所在的整行、整列和所在 3×3 宫这让用户能快速识别关联格子第三更新界面上的提示信息比如当前格是否题面格、是否可编辑。关于高亮的实现我会在每个 CellItem 里增加一个bool m_highlighted标记由视图统一计算哪些格子需要高亮——而不是让每个 item 自己判断避免重复逻辑。键盘填数的实现更简单在BoardWidget上重写keyPressEvent捕获数字键 1-9、Backspace清空格和方向键移动选中格。这里有一次性别踩坑的事BoardWidget必须调用setFocusPolicy(Qt::StrongFocus)否则键盘事件不会发给它。我一开始没有设置结果发现按数字键毫无反应。4.2 状态管理使用 QVector 作为唯一数据源数独应用最容易写乱的就是状态管理——如果每个格子各自保存自己的值那么「清除错误标记」「高亮行/列」「判断是否完成」都会变得非常麻烦。我用的模式很简单整个棋盘只有一个QVectorint m_board作为唯一数据源它保存用户当前填写的 81 个数字同时保留一份QVectorint m_puzzle作为题目快照用于区分哪些格子是题面、哪些是用户填的。// 数据更新的核心函数 void BoardWidget::setCellValue(int row, int col, int value) { int index row * 9 col; if (!m_editable[index]) return; // 题面格子不可修改 m_board[index] value; // 更新数据源 updateCellDisplay(row, col); // 让对应 CellItem 刷新 checkConflict(row, col); // 检查是否与同行/列/宫冲突 checkFinished(); // 检查是否全部填满且合法 }为什么敢用QVectorint而不是一个二维数组int[9][9]因为QVector在传递给Solver的时候天然就是线性内存你不需要做二维到一维的转换而且在Generator里生成题面的时候返回的也是QVectorint整条链路上数据结构完全一致少一层转换就少一类 bug。m_editable是另一个QVectorbool在载入题面时根据「原题是否为 0」来初始化。4.3 撤销和重做不引入复杂框架的栈式实现撤销重做是数独里用户一定会点的一个功能而且比大多数人的直觉要麻烦。我采用最简单的策略两个QVectorint栈。undoStack存每一步操作前的完整棋盘状态redoStack存撤销时弹出来的状态。用户每填入一个数字就把当前完整棋盘快照压入撤销栈撤销时把当前状态压入重做栈然后从撤销栈弹出一个快照作为当前状态。void BoardWidget::onUndo() { if (undoStack.isEmpty()) return; redoStack.push(m_board); // 当前状态进重做栈 m_board undoStack.pop(); // 回到上一步 refreshAllCells(); // 让所有格子重绘 } void BoardWidget::onRedo() { if (redoStack.isEmpty()) return; undoStack.push(m_board); m_board redoStack.pop(); refreshAllCells(); }这个方案的内存开销是每次快照sizeof(int) * 81 ≈ 324 字节用户可以撤销上千步也占不了几个 MB完全不用担心。但有一个关键细节连续在同一格填多个数字每一步都要压栈——而不是只压「最后一次」的状态。因为用户在解题过程中很可能「先填一个数又改成另一个数」这中间每一步都可能需要撤销回来。如果你只记录最终值用户会困惑为什么撤销一次就跳回了很远的旧状态。撤销栈还有一个参数要考虑栈最大深度。我一般限制在 500 步超出后从底部弹掉最老的状态避免极端情况下内存异常增长。实现上用if (undoStack.size() 500) undoStack.removeFirst();即可。5. 避坑指南Qt 数独开发中最常见的 5 个实际问题5.1 窗口大小没有适配棋盘被切掉一块现象在 1080p 显示器上看起来正常换到小屏笔记本上棋盘的下半部分或右边被裁切或者按比例拉伸时格子变成了长方形。原因直接把QGraphicsView放在窗口里没有设置场景矩形或没有适配窗口大小变化。QGraphicsView默认会显示场景内容的左上角不会自动居中或缩放。解决在mainwindow的resizeEvent里手动计算fitInView。void MainWindow::resizeEvent(QResizeEvent *event) { QMainWindow::resizeEvent(event); if (m_view m_scene) { m_view-fitInView(m_scene-sceneRect(), Qt::KeepAspectRatio); } }注意fitInView必须在场景的sceneRect已经确定之后才能正确工作。我建议在搭建场景时显式设置m_scene-setSceneRect(0, 0, 9 * cellSize, 9 * cellSize);而不是依赖 item 的隐式边界。否则sceneRect会随着 item 的位置自动计算但可能包含额外留白看起来虽然不会切掉但整体偏小。5.2 键盘事件无响应忘记设置焦点策略现象棋盘能点击但按下数字键没有反应方向键也不动。原因QGraphicsView 默认的focusPolicy是Qt::NoFocus焦点一直在别的控件上。解决在BoardWidget构造函数中设置setFocusPolicy(Qt::StrongFocus);。这个坑的隐蔽之处在于如果你的窗口里没有任何其他可聚焦控件比如只有一个 QGraphicsView程序不会崩溃也不会报错只是键盘事件仿佛被吃掉一样。排查方法是打印event-key()如果发现keyPressEvent压根没被调用先检查焦点策略。另一个相关的小坑如果你在CellItem内部也重写了mousePressEvent点击后焦点不会自动转移到视图。建议统一在视图层处理鼠标事件并在点击时调用setFocus()确保键盘后续可以输入。5.3 数独生成死循环或生成时间过长现象点击「新游戏」后界面卡住几秒甚至更久或者生成出来的题目明显比正常题目难得多挖洞数量异常。原因最常见的原因是countSolutions写错了导致求解器把无解和多解混在一起处理。比如countSolutions中回溯后没有重置棋盘状态导致计数时棋盘是脏的或者limit参数没有在递归中正确传递导致第二个解已经被找到却还在继续搜索。另一个原因是挖洞策略过于激进——每挖一个就调用一次求解器但求解器本身效率低比如在isValid中重复做了行/列检查。解决先用一个已知终盘做回归测试挖洞前先验证generateFinishedBoard返回的棋盘是一个合法终盘再验证挖洞过程中countSolutions(puzzle, 2)的结果始终不超过 2。如果你的生成时间超过 200ms先检查isValid是否有循环冗余再检查countSolutions是否正确地「找到两个解就快速退出」。性能优化上还有一个简单技巧把isValid中的行/列/宫检查改为用三个整型位掩码数组每行/列/宫用一个 int 表示哪些数字已出现检查时间从最坏 27 次比较降到 3 次位运算。对于 9×9 数独这个优化在生成 100 道题的时候能明显感知到差异。5.4 撤销重做与高亮不同步界面数据分叉现象点击撤销后棋盘上的格子内容变了但高亮状态、冲突标记还是旧的样子或者撤销后有些格子显示不正确。原因原始版本为了省事在每个CellItem中保存了m_value和m_editable而撤销时只更新了m_board数组没有同步刷新所有 item。这是典型的多数据源 bug——数据有两份一份是QVectorint一份是 item 内部的m_value更新只改了其中一份。解决统一原则——所有格子的值都从m_board读取item 内部的m_value只是渲染缓存。每当m_board变化包括撤销、重做、载入新题必须调用refreshAllCells()它遍历所有格子从m_board取值并调用cell-setValue(...)。这样数据只有一份权威来源item 永远只是「显示层」。5.5 题目唯一性验证不够严格出现的题目有多个解现象生成的题目看起来正常但用户填完后发现不是唯一解或者某个格子填 4 或 7 都能找到完整解。原因挖洞验证时机不对。我最初是在「生成完整终盘」之后一次性挖掉 40 个格子然后整体调用一次求解器发现求解器能找到一个解就认为 OK。但完整终盘挖掉 40 个格子后很可能存在多个解而求解器只返回了第一个解。判断唯一解必须用到countSolutions(puzzle, 2)绝不能只用solve(puzzle)。解决严格按「每挖一个验证一次」的流程走并在挖完后追加一次整体验证bool Generator::verifyPuzzle(const QVectorint puzzle) { return Solver::countSolutions(puzzle, 2) 1; }这道防线加上之后基本杜绝了「多解题」问题。另外注意一个边界如果你把cluesToRemove设置得过大比如试图挖掉 60 个格子挖到最后可能会发现所有格子都不能再挖了因为一旦再挖就出现多解——此时生成器会在removed没达到目标的情况下提前返回。这是正常现象题面最终留下的数字数比设定值多也是可接受的不要把这种情况当 bug 处理。6. 从「能玩」到「有点意思」候选数提示与难度曲线的三种实战加料前五章讲的是一个能完整运行的基础数独游戏但如果你想让这个项目真正在作品集里发光或者让自己的代码能力再上一个台阶我强烈建议你给它加三个「进阶调味料」。这三个都不需要大改架构但每一个都会显著提升项目的完成度。第一个是候选数自动生成功能。在基础版本里用户要靠脑子记哪些格能填哪些数进阶版允许用户点一个「候选模式」按钮然后空白格中用小号灰字显示所有合法候选数字。实现上并不难对每个空白格遍历 1-9用isValid判断是否能填能填就显示。这里有个关键参数候选数字的显示字号一般是实数字的 40% 左右且颜色要更淡避免视觉噪音。我实测下来当候选数超过 4 个时小字会让界面显得非常拥挤——所以很多商业数独应用只显示「你标记过的候选数」而不显示全部。你可以加一个开关让用户自己选择。第二个是难度分级不再只靠留数字多少。前面讲过留 30 个数字和 40 个数字会有难度差异但真正的高难度题往往留 24-28 个数字就够了。更实用的方案是引入「剪枝深度评估」——统计求解器回溯次数回溯次数多的题更难。每次生成题面时让求解器计一个「尝试次数」然后按次数区间分成简单/中等/困难三档。这样做比单纯按留数字数量更准确而且实现成本非常低——只需要在回溯递归里给一个int counter参数。第三个是存档与无痕重开。你需要把当前m_board、m_puzzle、undoStack序列化保存成文件下次启动时恢复。用QDataStream或QJsonDocument都行数独状态极小性能不是问题。关键设计点是恢复存档时要保证m_editable也同步恢复只能根据m_puzzle是否为 0 来判断否则用户会发现题面格子变得可编辑了。这个功能看起来简单但它是区分「Demo」和「产品」的一个非常直观的标尺。最后回到这个项目最值得投入的地方它不是「Qt 能做什么」的展示而是「面对一个带约束的状态空间你怎么拆解数据结构、怎么设计交互、怎么保证数据一致性」的完整演练。我做完这个项目后的一个习惯性动作是每次写完一个模块都重新跑一遍「生成 → 挖洞 → 撤销 → 重做 → 存档恢复」这条链路确保每一步的状态都符合预期。这个习惯帮我筛掉了大量在界面层根本看不出来的逻辑 bug。希望这篇笔记能帮你在两周内搞定一个能拿得出手的数独作品也帮你少走我当年走过的弯路。本文还有配套的精品资源点击获取

相关新闻

LiteLLM 统一多模型 API 接入:网关化改造实践指南

LiteLLM 统一多模型 API 接入:网关化改造实践指南

手头同时维护好几家大模型API接入的时候,最让人崩溃的不是哪个模型更聪明,而是接口格式从来都不统一。有的厂商要求请求体里写max_tokens,另一家偏要写max_output_tokens;有的返回里直接把答案放在choices[0].message.content&…

2026/10/13 4:46:13 阅读更多 →
macOS升级Ruby全攻略:Homebrew与rbenv双路线详解

macOS升级Ruby全攻略:Homebrew与rbenv双路线详解

1. 升级前先定位:你的macOS里Ruby到底从哪来先说个我自己的真实经历。前阵子有个项目一直报You have already activated ...,查了大半天,最后发现根源就是macOS自带的Ruby版本太老,而项目依赖的新版gem不认它。如果你也在Mac上写脚…

2026/10/13 4:46:13 阅读更多 →
AI评测高分可信吗?拆解数据污染与奖励黑客下的防作弊评测体系

AI评测高分可信吗?拆解数据污染与奖励黑客下的防作弊评测体系

最近我在验证一个新模型的实际效果,跑了一套网上流传很广的公开评测题,得分高得吓人,几乎每个维度都是"顶尖水平"。可等我把它放到自己的真实业务场景里,用同样的学科方向、只是换了一批数字和背景数据,表现…

2026/10/13 4:46:13 阅读更多 →

最新新闻

恶意代码检测毕设实战:PE特征提取与机器学习模型训练

恶意代码检测毕设实战:PE特征提取与机器学习模型训练

简介:这份资源是一套基于机器学习的恶意代码检测项目源码,面向计算机、人工智能、通信工程、自动化等专业的在校学生与教师,也适合作为毕设、课程设计或项目立项演示的参考。项目围绕恶意代码的特征提取、向量化与模型训练展开,涵…

2026/10/13 6:58:55 阅读更多 →
入口日志参数缺失导致500故障的根因与防御体系

入口日志参数缺失导致500故障的根因与防御体系

1. 项目概述:一次典型的“入口参数缺失”故障复盘“Agent 第一句就 500,我们查了三轮:入口日志少打了一个参数”——这句话不是段子,是某次线上服务发布后凌晨两点的 Slack 截图标题。它精准击中了现代分布式系统中最常见、最隐蔽…

2026/10/13 6:58:55 阅读更多 →
代码工具悄悄上传 .git 目录?从版本库泄露到安全排查的完整指南

代码工具悄悄上传 .git 目录?从版本库泄露到安全排查的完整指南

这两年做代码安全相关的事,最怕看到的就是这句:“ZCode 疑似被抓到上传工作区/.git 记录到云端”。当时看到这个消息,我第一反应是后背发凉。做过几年开发的人都清楚,工作区里最不能见光的目录就是.git,它不仅仅是版本…

2026/10/13 6:58:55 阅读更多 →
从Java后端到Agent开发:收藏这份转型指南,抓住AI时代机遇!

从Java后端到Agent开发:收藏这份转型指南,抓住AI时代机遇!

本文探讨了Java后端开发者转向Agent开发的可能性与挑战。Agent开发涉及将企业文档转化为知识库、模型工具API集成、多步骤任务流程设计等,不同于简单的LangChain调参。虽然Agent开发岗位火热,但也存在岗位泡沫、技术栈变化快、要求复合技能等风险。建议J…

2026/10/13 6:58:55 阅读更多 →
(187页PPT)贝恩保健品Swisse商业尽职调查项目报告(附下载方式)

(187页PPT)贝恩保健品Swisse商业尽职调查项目报告(附下载方式)

篇幅所限,本文只提供部分资料内容,完整资料请看下面链接 https://download.csdn.net/download/2501_92808811/92962815 资料解读:贝恩保健品 Swisse 商业尽职调查项目报告 详细资料请看本解读文章的最后内容。这份由贝恩公司出具的商业尽职…

2026/10/13 6:58:55 阅读更多 →
Nginx 进阶(HTTPS / 动态站点 / PHP / 反向代理)与 ECshop 实战

Nginx 进阶(HTTPS / 动态站点 / PHP / 反向代理)与 ECshop 实战

Nginx 进阶(HTTPS / 动态站点 / PHP / 反向代理)与 ECshop 实战 本文主要讲解Nginx 进阶(HTTPS / 动态站点 / PHP / 反向代理)与 ECshop 实战,每个概念讲清「是什么、为什么、怎么用」,每条命令解释「意图和…

2026/10/13 6:57:55 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式: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/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/12 18:11:17 阅读更多 →