news 2026/11/4 13:14:28

Qt数独项目实战:自绘棋盘与纯逻辑引擎的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt数独项目实战:自绘棋盘与纯逻辑引擎的完整实现

简介:这份资源面向具备一定 C++ 基础、希望借助 Qt 框架完成桌面 GUI 实战的开发者,以经典数独游戏为载体,串联界面设计、事件响应与游戏逻辑的完整实现。压缩包共 141 个文件,约 25.6MB,以 21 个 cpp 源文件与 15 个 h 头文件承载核心逻辑,8 个 ui 文件对应 Qt Designer 界面布局,另有 47 个 png 图片资源、qrc 资源文件及 pro 工程配置,便于直接编译运行与二次修改。已有 2562 人学习下载,具备一定参考基础。内容围绕信号与槽通信、QWidgets 与 QLayout 布局管理、MVC 分层组织游戏逻辑,并涉及数独盘面生成与校验、存档读写、自定义背景加载等模块,读者可据此理解 Qt 项目从界面搭建到逻辑解耦的完整思路,并借鉴其目录组织与调试方式。

1. 从一张九宫格说起:这个 Qt 数独项目到底能干什么

很多人第一次接触 Qt,都是从写一个能跑起来的小游戏开始的,而数独恰好是最合适的练手对象——它规则简单、界面规整、逻辑清晰,却又足够复杂,能把 Qt 的控件布局、事件处理、信号槽机制、定时器、绘图全都串一遍。这个项目就是一套用 Qt 编写的数独游戏完整工程,打开后是一个 9×9 的棋盘界面,支持生成题目、填入数字、校验冲突、提示答案、计时统计,甚至还能做难度分级。它适合两类人:一类是刚学完 Qt 基础语法、想找一个能真正跑起来、能改得动的小项目练手的开发者;另一类是手里有课程设计或小工具需求,想直接拿一套结构清晰的 Qt 工程做二次开发的人。整个项目不依赖任何第三方库,纯 Qt Widgets 实现,编译门槛低,改起来也不容易翻车。

2. 工程结构与技术选型:一个纯 Widgets 的数独是怎么搭起来的

2.1 技术栈与整体架构

这个项目用的是 Qt Widgets 而不是 QML,原因很直接:数独棋盘本质是一个规整的网格,用QGridLayout加一组自定义按钮或自绘控件就能搞定,没必要引入 QML 的运行时和绑定语法。整个工程围绕三个核心对象展开:主窗口负责界面和交互,棋盘控件负责 9×9 格子的绘制与点击响应,数独引擎负责题目生成、合法性校验和求解。三者之间通过信号槽通信,界面不直接碰算法,算法也不关心界面长什么样,这种分层是这类小项目里最值得保留的设计。

常见做法是把数独引擎写成一个独立的类,不继承任何 Qt 类,只做纯逻辑运算;棋盘控件继承QWidget,重写paintEvent自己画格子;主窗口继承QMainWindow,把菜单、按钮、计时器都挂上去。这样拆的好处是,引擎部分可以单独写单元测试,界面部分换皮也不影响逻辑。

2.2 目录结构长什么样

一个典型的工程目录大致如下,不同人组织方式略有差异,但核心文件基本就这几个:

sudoku-qt/ ├── sudoku.pro # qmake 工程文件 ├── main.cpp # 程序入口 ├── mainwindow.h/.cpp # 主窗口:菜单、按钮、计时 ├── boardwidget.h/.cpp # 棋盘控件:绘制、点击、选中态 ├── sudokuengine.h/.cpp # 数独引擎:生成、校验、求解 └── resources/ └── icons/ # 按钮图标等资源

.pro文件里只需要声明QT += widgets,源文件列表按实际添加即可。如果用的是 CMake,对应写一个CMakeLists.txt,find_package(Qt6 COMPONENTS Widgets REQUIRED)然后链接Qt6::Widgets。两种构建方式都行,qmake 对新手更友好,CMake 更通用,看团队习惯选。

2.3 棋盘控件:为什么选择自绘而不是摆 81 个按钮

摆 81 个QPushButton是最省事的做法,但样式控制很麻烦,选中高亮、冲突标红、固定格与可编辑格的区分都要靠样式表硬调,格子一多性能也一般。这个项目选择继承QWidget自绘,在paintEvent里用QPainter画线、画数字,点击时通过坐标反算行列。核心绘制逻辑大致是这样:

void BoardWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, false); int cell = qMin(width(), height()) / 9; // 每格边长,取短边均分 int offsetX = (width() - cell * 9) / 2; // 居中偏移 int offsetY = (height() - cell * 9) / 2; // 先画 81 个格子的背景,区分固定格、可编辑格、选中格、冲突格 for (int r = 0; r < 9; ++r) { for (int c = 0; c < 9; ++c) { QRect rect(offsetX + c * cell, offsetY + r * cell, cell, cell); painter.fillRect(rect, cellColor(r, c)); // 按状态返回颜色 } } // 再画细线(每格)和粗线(每三格),粗线用更宽的笔 QPen thin(Qt::gray, 1), thick(Qt::black, 2); for (int i = 0; i <= 9; ++i) { painter.setPen(i % 3 == 0 ? thick : thin); painter.drawLine(offsetX, offsetY + i * cell, offsetX + 9 * cell, offsetY + i * cell); painter.drawLine(offsetX + i * cell, offsetY, offsetX + i * cell, offsetY + 9 * cell); } // 最后画数字,固定格用黑色,用户填入用蓝色,冲突用红色 QFont font = painter.font(); font.setPixelSize(cell * 0.6); painter.setFont(font); for (int r = 0; r < 9; ++r) for (int c = 0; c < 9; ++c) if (m_values[r][c] != 0) painter.drawText(QRect(offsetX + c * cell, offsetY + r * cell, cell, cell), Qt::AlignCenter, QString::number(m_values[r][c])); }

这段代码的关键点有三个:cell按窗口短边均分,保证棋盘始终是正方形且随窗口缩放;offsetX/offsetY做居中,避免窗口拉大后棋盘贴左上角;cellColor根据格子状态返回不同底色,把「固定格、可编辑格、当前选中、冲突」四种状态在视觉上分开。m_values是一个int[9][9]数组,0 表示空格。点击响应在mousePressEvent里用同样的坐标公式反算行列,然后发一个自定义信号出去,主窗口收到后决定是填数还是选中。

2.4 数独引擎:生成、校验、求解三件事

引擎部分不依赖 Qt,纯 C++ 实现。题目生成常见有两种思路:一种是随机填满一个完整解,再按难度挖空;另一种是从一个已知终盘出发做变换。这个项目用的是第一种,先生成完整解再挖洞,逻辑更直观。生成完整解用回溯加随机打乱候选顺序:

bool SudokuEngine::fillBoard(int board[9][9], int pos) { if (pos == 81) return true; // 填满即成功 int r = pos / 9, c = pos % 9; if (board[r][c] != 0) return fillBoard(board, pos + 1); QVector<int> nums = {1,2,3,4,5,6,7,8,9}; std::shuffle(nums.begin(), nums.end(), m_rng); // 随机顺序,保证每次题目不同 for (int n : nums) { if (isValid(board, r, c, n)) { board[r][c] = n; if (fillBoard(board, pos + 1)) return true; board[r][c] = 0; // 回溯 } } return false; }

isValid检查同行、同列、同 3×3 宫是否已有该数字,这是数独最基础的约束。挖洞阶段按难度决定保留多少个已知数,简单留 40 个左右,困难留 28 个左右,挖的时候每挖一个就调一次求解器验证「是否仍有唯一解」,不唯一就填回去。这一步是性能大头,常见做法是限制求解器搜索步数,超过阈值就认为不唯一,避免卡死。

求解器本身也是回溯,和生成共用isValid,只是候选顺序固定为 1 到 9。提示功能就是拿当前棋盘跑一次求解,把解里对应格子的数字填回去。

3. 跑起来:从零编译到第一次点击的完整步骤

3.1 环境准备与工程打开

先确认本机装了 Qt,版本 5.15 或 Qt 6 都行,Widgets 模块是标配。用 Qt Creator 打开.pro文件,选择对应的 Kit,直接点构建运行即可。如果命令行编译,qmake 的流程是:

qmake sudoku.pro make # Windows 下是 nmake 或 mingw32-make ./sudoku # 生成的可执行文件

用 CMake 的话:

cmake -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/gcc_64 cmake --build build ./build/sudoku

CMAKE_PREFIX_PATH指向 Qt 安装目录,找不到 Qt 时九成是这里没配对。Windows 下如果运行时报缺少Qt6Widgets.dll,用windeployqt把依赖拷到可执行文件旁边即可。

3.2 主窗口的初始化顺序

主窗口构造里要做的事有固定顺序,顺序错了会出现「计时器先跑、棋盘还没初始化」这类问题。典型写法:

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_engine = new SudokuEngine(this); m_board = new BoardWidget(this); setCentralWidget(m_board); setupMenus(); // 菜单:新游戏、难度、提示、检查 setupToolbar(); // 工具栏:数字键盘、擦除、撤销 setupTimer(); // 计时器,先停着 connect(m_board, &BoardWidget::cellClicked, this, &MainWindow::onCellClicked); connect(m_engine, &SudokuEngine::generationFinished, this, &MainWindow::onPuzzleReady); newGame(Difficulty::Easy); // 最后才发第一局 }

先建引擎和棋盘,再挂菜单和信号槽,最后才触发第一局生成。newGame里把难度传下去,引擎生成完发generationFinished信号,主窗口收到后把题目灌进棋盘并启动计时。这个顺序保证了界面在数据到位之前不会去读空棋盘。

3.3 交互链路:一次点击到底经过了谁

用户点一个格子,BoardWidget::mousePressEvent算出行列,发cellClicked(row, col);主窗口的onCellClicked记录当前选中格,刷新棋盘高亮;用户按数字键或点工具栏数字,主窗口调m_engine->setValue(row, col, n)做合法性校验,合法就写进棋盘并检查是否完成,不合法就把该格标红。整条链路是「控件发信号 → 主窗口协调 → 引擎校验 → 控件重绘」,职责清晰,加功能时知道该往哪一层塞。

计时器用QTimer,每秒发一次timeout,主窗口更新状态栏的用时显示。暂停时停表,恢复时继续,注意暂停期间要屏蔽棋盘点击,否则用户能趁暂停改棋盘,这是很多人第一版会漏的细节。

4. 避坑与排查:这几个坑我踩过,你别再踩

4.1 棋盘缩放后格子错位

现象是窗口拉大后数字和格线对不齐。原因多半是绘制时用了固定像素值,或者cell用width()/9而没考虑高度,窗口非正方形时横竖比例不一致。解决是把cell取qMin(width(), height())/9,所有坐标都基于cell和居中偏移计算,别混用固定值。

4.2 生成题目时界面卡死

点「新游戏」后窗口无响应几秒。原因是题目生成和唯一解校验都在主线程同步跑,困难难度挖洞时求解器搜索量大。解决办法是把生成放到QtConcurrent::run或独立QThread里,生成完通过信号回主线程更新界面;或者退一步,在挖洞校验时限制搜索步数,超过就放弃该次挖洞,用性能换体验。

4.3 信号槽连接了但槽没触发

现象是点了格子没反应。常见原因有三个:connect写在了newGame之后但对象还没建;自定义信号没加Q_OBJECT宏导致 moc 没生成;或者参数类型没注册,跨线程时连接失败。排查时先看编译输出有没有 moc 相关警告,再在槽函数第一行打个日志确认是否进入。

4.4 冲突标红误伤固定格

用户填入一个和固定格冲突的数字,结果固定格也被标红。原因是校验时把整行整列都刷了状态,没区分「用户填入」和「题目给定」。正确做法是只把用户刚填的那一格标红,或者标红时跳过m_fixed[r][c]为真的格子,固定格永远保持黑色。

4.5 撤销功能把题目格也撤了

加了撤销栈之后,用户连按撤销能把题目给定的数字也撤掉。根因是入栈时没判断该格是否可编辑。解决很简单:只有!m_fixed[r][c]的操作才压栈,撤销时也只回退这些格子,题目格作为不可变基线单独存一份。

5. 二次开发:把数独改成你自己的练手项目

这套工程最值钱的地方不是数独本身,而是它把「自绘控件 + 纯逻辑引擎 + 信号槽协调」这个模式跑通了,换任何网格类小游戏都能套。想改成扫雷,棋盘控件几乎不用动,把m_values换成雷区状态,paintEvent里按状态画雷、旗子、数字即可;引擎换成布雷和翻格扩散逻辑。想加难度曲线,就在SudokuEngine里把「保留已知数个数」做成参数,再配一个按时间或错误次数动态调整的策略。

改造时我一般会先做一件事:把引擎单独拎出来写几个断言测试,比如「生成的题目有唯一解」「填入非法数字返回 false」「求解器能在限定步数内解出简单题」。这一步看着多余,但后面改挖洞策略、换随机种子时,全靠它兜底,不然改着改着题目生成出双解,用户一填就报错,排查起来就是黑匣子。

验证方法上,建议加一个隐藏的调试入口,按快捷键把当前题目的完整解打印到日志,人工抽查几局;再写个循环跑 100 次生成,统计平均耗时和唯一解校验的失败率,困难难度失败率高就调搜索步数阈值。这些数字比肉眼看着「差不多」靠谱得多。

还有一个具体技巧:把棋盘状态序列化成字符串(81 个字符,0 表示空),存进QSettings或本地文件,下次启动直接恢复。序列化函数写好后,存档、读档、分享题目链接三件事一次解决,比每个功能单独写一套存取逻辑省事。我当初就是没先做序列化,后来加存档功能时把棋盘、计时、撤销栈各写了一遍,返工返到后悔。

从那以后我每次接这类小工程,都强制先把「状态怎么表示、怎么存、怎么恢复」想清楚再动手写界面,界面可以慢慢调,状态模型一开始歪了,后面全是补丁。希望这套数独工程的拆解能帮到你,拿去跑一遍、改一版,比看十篇教程都实在。

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

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

AI Agent CLI安装与版本管理避坑指南

1. 这不是一份“工具清单”&#xff0c;而是一份AI Agent CLI实战避坑指南你是不是也经历过&#xff1a;刚在GitHub上看到一个标榜“5分钟搭建智能体”的CLI工具&#xff0c;兴冲冲复制粘贴安装命令&#xff0c;结果卡在command not found&#xff1b;或者好不容易装上了&#…

作者头像 李华
网站建设 2026/11/4 13:13:17

Chrome插件MV3迁移实战:Service Worker与请求规则重构

浏览器扩展圈子里最近讨论最多的一件事&#xff0c;就是 Manifest V3 的强制推行。我这套扩展从最初几十个用户的小工具&#xff0c;慢慢长到有几万人在用&#xff0c;后台逻辑堆了三年多&#xff0c;一直靠着常驻的后台页跑着各种定时任务和请求处理。这次平台方不再保留旧版本…

作者头像 李华
网站建设 2026/11/4 13:13:17

从Synthesizer V到Vocoflex:AI歌声转换如何用10秒重塑音乐创作

Dreamtonics 突然甩出 Vocoflex 的时候&#xff0c;我正躺在工作室的椅子上刷手机&#xff0c;差点把茶杯打翻。倒不是这个产品名多吓人&#xff0c;而是 "只需要 10 秒歌声就能模仿声音" 这句话&#xff0c;过去几年我们只在那些不一定靠谱的开源项目里见过。现在做…

作者头像 李华
网站建设 2026/11/4 13:13:00

2026年AI读书工具深度测评与技术解析

1. 项目概述&#xff1a;AI读书工具测评的价值与挑战 去年帮一位视障朋友选听书工具时&#xff0c;我翻遍了全网测评却找不到真正靠谱的对比。这个经历让我意识到&#xff1a;市面上大多数AI读书工具测评要么是厂商软文&#xff0c;要么停留在表面功能罗列。真正从用户体验角度…

作者头像 李华
网站建设 2026/11/4 13:12:05

智能音箱语音下载实战:口令设计与意图识别优化指南

1. 拆解“语音指令下载”这件事&#xff1a;为什么口令设计比功能本身更关键 智能音箱用久了你会发现一个很有意思的现象&#xff1a;同样一个功能&#xff0c;有人喊一嗓子就成&#xff0c;有人喊了十遍音箱还在那“抱歉&#xff0c;我没听懂”。问题往往不出在设备上&#xf…

作者头像 李华
网站建设 2026/11/4 13:11:02

鸿蒙Map Kit地图控件与事件交互实战:避坑与坐标转换

地图页面在真实项目里往往是最容易返工的一个模块。我之前参与过一个园区导览类的模拟项目&#xff0c;第一版上线后收到的反馈集中在两点&#xff1a;一是用户双指放大地图时&#xff0c;整个页面跟着上下滚动&#xff1b;二是点标记卡片十次有一两次没反应。前者属于控件层面…

作者头像 李华