news 2026/11/4 8:15:54

Qt数独游戏开发实战:从终盘生成到撤销功能的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt数独游戏开发实战:从终盘生成到撤销功能的完整实现

简介:这份资源面向具备一定 C++ 基础、希望借助 Qt 框架完成桌面 GUI 实战的开发者,以经典数独游戏为载体,串联界面设计、信号与槽通信、MVC 分层、文件存取与多语言切换等核心知识点,适合课程设计、练手项目或 Qt 入门后的进阶练习。压缩包共 141 个文件,约 25.6MB,以 21 个 cpp 源文件与 15 个 h 头文件承载游戏逻辑与界面交互,8 个 ui 文件对应 Qt Designer 布局,另有 47 个 png 图片资源、qrc 资源文件及 pro 工程配置,并保留编译产物便于对照。已有 2562 人学习下载,说明其结构完整、可参考性较强。读者可从中获得一套可编译运行的数独工程,理解盘面生成、答案校验、按钮响应与背景更换的实现方式,并借鉴模型与视图分离的组织思路,用于改造或扩展自己的 Qt 项目。

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

很多人第一次看到「Qt 编写数独游戏」这个标题,脑子里浮现的是控制台里打印一堆数字,或者一个能填格子的简陋窗口。真把源码拉下来跑一遍,会发现它比想象中完整:棋盘生成、难度分级、计时、提示、撤销、冲突高亮、胜利判定,一整套单机游戏该有的东西都在。它适合两类人——想学 Qt 桌面开发但苦于没有完整小项目练手的初学者,以及想拿一个干净骨架改成自己小游戏的老手。数独这个题材的好处是规则封闭、状态清晰,没有网络、没有数据库,所有复杂度都压在界面与算法两层,正好把 Qt 的信号槽、模型视图、事件处理、定时器串一遍。下面按「能干什么、怎么跑、代码长什么样、怎么改」的顺序拆。

2. 技术选型与工程结构:为什么是 Qt Widgets 而不是 QML

2.1 选型判断:Widgets 更适合这个体量

这个项目用 Qt Widgets 而不是 QML,是有道理的。数独界面本质是一张 9×9 的网格,每个格子要响应点击、键盘输入、右键标记候选数,还要按 3×3 宫块画粗边框。Widgets 里用QGridLayout摆 81 个自定义QPushButton或QLabel子类,配合QSS改样式,几十行就能把布局立起来;换成 QML 虽然动画更顺,但网格对齐、焦点管理、键盘事件都要自己补,对一个教学向小项目是负担。常见做法是把每个格子做成继承QLabel的自定义控件,重写mousePressEvent和keyPressEvent,这样点击和输入都在控件内部消化,主窗口只负责调度。

技术栈上,项目依赖 Qt 5 或 Qt 6 的 Core、Gui、Widgets 三个模块,构建用 qmake 的.pro文件或 CMake 的CMakeLists.txt,C++11 起步。没有第三方库,没有资源服务器,图片和字体走 Qt 资源系统.qrc。这意味着你只要装好 Qt 开发环境,编译链是通的,不存在依赖地狱。

2.2 目录结构:一个典型的单工程布局

源码包解压后,结构大致如下(不同版本命名略有出入,但职责划分一致):

sudoku-qt/ ├── sudoku.pro # qmake 工程文件 ├── CMakeLists.txt # 若提供 CMake 构建 ├── main.cpp # 程序入口 ├── mainwindow.h/.cpp # 主窗口:菜单、计时、难度、状态栏 ├── sudokuboard.h/.cpp # 棋盘逻辑:生成、校验、求解 ├── sudokucell.h/.cpp # 单个格子控件:绘制、输入、高亮 ├── generator.h/.cpp # 题目生成器:终盘生成 + 挖洞 ├── resources/ │ ├── icons.qrc # 图标资源 │ └── style.qss # 全局样式表 └── README.md

这个划分的关键在于把「数据」和「视图」分开:SudokuBoard只管 81 个整数的二维数组和规则判断,完全不碰 Qt 控件;SudokuCell只管自己那一格的显示和输入;MainWindow做粘合层。二次开发时想换皮肤只动style.qss和SudokuCell的绘制,想换出题算法只动generator,互不牵连。这是小项目里最值得学的一点——边界划清楚,后面改起来才不痛苦。

2.3 构建与运行:三条命令跑起来

以 qmake 为例,进入源码目录后:

# 确认 Qt 环境,qmake 在 PATH 里 qmake --version # 生成 Makefile 并编译 qmake sudoku.pro make -j4 # Linux 下直接运行;Windows 下是 release/sudoku.exe ./sudoku

如果用的是 CMake:

mkdir build && cd build cmake .. -DCMAKE_PREFIX_PATH=/path/to/Qt/5.15.2/gcc_64 cmake --build . -j4 ./sudoku

CMAKE_PREFIX_PATH指向你本机 Qt 安装目录,这是新手最容易翻车的地方——不指定就报Could not find a package configuration file provided by "Qt5Widgets"。Windows 上用 Qt Creator 打开.pro直接点运行最省事,它会自动配好套件。编译通过后窗口应该直接弹出,菜单栏有「新游戏」「难度」「提示」「撤销」,底部状态栏显示计时和剩余空格数。

3. 核心模块拆解:从终盘生成到一次落子的完整链路

3.1 题目生成:先造终盘,再挖洞

数独出题分两步:生成一个合法终盘,然后按难度挖掉若干格子。终盘生成常见两种做法,一种是回溯随机填,一种是基于基盘做行变换。这个项目用的是回溯加随机打乱,代码骨架如下:

// generator.cpp bool Generator::fillBoard(int board[9][9], int pos) { if (pos == 81) return true; // 填满即成功 int row = pos / 9, col = pos % 9; QVector<int> nums = {1,2,3,4,5,6,7,8,9}; std::shuffle(nums.begin(), nums.end(), rng); // 每格随机顺序尝试 for (int n : nums) { if (isValid(board, row, col, n)) { // 行、列、宫三重校验 board[row][col] = n; if (fillBoard(board, pos + 1)) return true; board[row][col] = 0; // 回溯 } } return false; }

pos是线性下标,从 0 走到 80,把二维坐标换算成row/col。isValid检查同一行、同一列、同一个 3×3 宫里有没有重复数字,这是数独唯一的三条约束。std::shuffle保证每次生成的终盘不同,rng是std::mt19937,用时间戳播种。回溯失败就退回来换下一个数字,最坏情况会退很多层,但 9×9 规模下实测毫秒级完成,不需要担心性能。

挖洞阶段按难度决定保留多少提示数:简单留 40 个左右,中等 32 个,困难 26 个。挖的时候随机选格子置 0,挖完用求解器验证「解唯一」,不唯一就补回去重挖。这一步是保证题目质量的关键,少了它会出现多解,玩家填出另一个合法答案却被判错,体验直接崩。

3.2 棋盘逻辑:校验与求解共用一套规则

SudokuBoard里最核心的是isValid和solve两个函数,前者给输入校验用,后者给「提示」和「验证唯一解」用:

// sudokuboard.cpp bool SudokuBoard::isValid(int b[9][9], int r, int c, int v) { for (int i = 0; i < 9; ++i) { if (b[r][i] == v || b[i][c] == v) return false; // 行、列 } int br = (r / 3) * 3, bc = (c / 3) * 3; // 宫块起点 for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) if (b[br + i][bc + j] == v) return false; // 3×3 宫 return true; } bool SudokuBoard::solve(int b[9][9]) { for (int r = 0; r < 9; ++r) for (int c = 0; c < 9; ++c) if (b[r][c] == 0) { // 找第一个空格 for (int v = 1; v <= 9; ++v) if (isValid(b, r, c, v)) { b[r][c] = v; if (solve(b)) return true; b[r][c] = 0; // 回溯 } return false; // 1~9 都不行 } return true; // 没有空格即解完 }

isValid的三个参数是行、列、待填值,返回布尔。注意它假设b[r][c]当前是 0,否则会把自己和自己比出重复。solve是标准回溯,找到第一个空格就尝试 1 到 9,成功递归、失败回退。这个函数在「提示」功能里被调用——玩家点提示时,把当前盘面复制一份丢进solve,取解里对应格子的值填回去。复制盘面这一步不能省,直接改原盘会把玩家已填的内容搅乱。

3.3 格子控件:一次点击到界面更新的信号链

SudokuCell继承QLabel,重写鼠标和键盘事件,把「用户想填几」通过信号抛给主窗口:

// sudokucell.cpp void SudokuCell::keyPressEvent(QKeyEvent *e) { if (isFixed_) return; // 题目给定格不可改 int k = e->key(); if (k >= Qt::Key_1 && k <= Qt::Key_9) { emit valueEntered(row_, col_, k - Qt::Key_0); // 抛给主窗口 } else if (k == Qt::Key_Backspace || k == Qt::Key_Delete) { emit valueEntered(row_, col_, 0); // 0 表示清空 } } void SudokuCell::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::RightButton) emit candidateToggled(row_, col_); // 右键标记候选数 else emit cellFocused(row_, col_); // 左键聚焦 }

isFixed_标记这一格是不是题目自带的提示数,是的话直接吞掉按键,玩家改不了。valueEntered带三个参数:行、列、值,主窗口收到后先调isValid校验,合法就写进SudokuBoard并刷新格子文字,不合法就把格子背景刷成红色并启动一个 800ms 的QTimer恢复原色——这就是冲突高亮的实现,简单但有效。右键候选数用一个小号字体在格子角落画 3×3 的迷你数字,状态存在QSet<int>里,切换时重绘。

3.4 主窗口调度:计时、难度与撤销栈

MainWindow是粘合层,持有SudokuBoard、81 个SudokuCell指针、一个QTimer做秒表、一个QStack<Move>做撤销。核心连接代码:

// mainwindow.cpp connect(cell, &SudokuCell::valueEntered, this, &MainWindow::onValueEntered); connect(timer_, &QTimer::timeout, this, &MainWindow::tick); void MainWindow::onValueEntered(int r, int c, int v) { int old = board_->valueAt(r, c); if (v != 0 && !board_->isValid(board_->data(), r, c, v)) { flashConflict(r, c); // 冲突闪烁,不落子 return; } undoStack_.push({r, c, old}); // 记录旧值,供撤销 board_->setValue(r, c, v); refreshCell(r, c); if (board_->isComplete()) stopGame(); // 填满且合法即胜利 }

Move是个小结构体,存行、列、旧值三个字段,撤销时弹栈写回旧值再刷新格子。计时器timer_每 1000ms 触发一次tick,把秒数格式化成mm:ss显示在状态栏,游戏结束时stop()。难度切换走菜单的QAction,触发后重新调Generator生成新盘、清空撤销栈、计时归零。这条链路把「输入 → 校验 → 落子 → 记录 → 刷新 → 判胜」串成一条直线,没有多余分支,读起来很顺。

4. 避坑与常见问题:编译和运行阶段最容易踩的五个坑

4.1 编译报找不到 Qt 模块

现象:qmake能跑,make时报fatal error: QWidget: No such file or directory。原因多半是.pro里只写了QT += core,漏了gui widgets。解决:把.pro第一行改成QT += core gui widgets,重新qmake再make。CMake 工程则检查find_package(Qt5 COMPONENTS Widgets REQUIRED)和target_link_libraries里有没有Qt5::Widgets。

4.2 中文显示成方块

现象:菜单和按钮上的中文全是豆腐块。原因是源码用了 UTF-8 但没告诉编译器,或者字体没设。解决:.pro里加QMAKE_CXXFLAGS += -utf-8(MSVC 用/utf-8),main.cpp里在创建QApplication之后设QFont font("Microsoft YaHei", 10); app.setFont(font);。Linux 上换成系统里有的中文字体名即可。

4.3 生成的题目有多解

现象:玩家填出一个合法盘面,程序却判错。原因:挖洞后没做唯一解校验。解决:在Generator挖完洞后调solve数解,数到第二个就判定多解,把刚挖的格子补回去重挖。计数版求解器只需在找到解时不立即返回,而是count++后继续回溯,count >= 2提前退出。

4.4 键盘输入没反应

现象:点格子后按数字键,格子不变。原因:QLabel默认不接收焦点,keyPressEvent根本不会被调用。解决:构造函数里setFocusPolicy(Qt::StrongFocus),点击时setFocus()。如果格子是QPushButton,还要注意按钮自己会吞掉方向键,需要重写keyPressEvent并调基类处理不需要的键。

4.5 撤销后高亮状态错乱

现象:撤销一步后,之前标红的冲突格还红着。原因:冲突高亮用的是定时器恢复,撤销时没清定时器。解决:每个格子的闪烁定时器用成员变量持有,撤销或新游戏时统一stop()并把背景恢复成默认样式。血泪经验是别用全局单例定时器管所有格子,状态一多就互相踩。

5. 二次开发改造:换算法、加功能与验证方法

想在这个骨架上做扩展,最划算的三个方向是换出题算法、加存档、加候选数自动清理。换算法方面,现在的回溯生成在 9×9 上够快,但如果想支持 16×16 或者加「对角线数独」变体,isValid要扩成可配置的约束列表,把行、列、宫、对角线各写成一个Constraint函数对象,生成和校验都遍历这个列表。这样加规则只加一个函数,不动主流程。

加存档最简单:SudokuBoard的 81 个整数序列化成一行 81 字符的字符串,连同计时秒数、难度、撤销栈一起写进QSettings或一个 JSON 文件,启动时读回来重建界面。注意固定格标记也要存,否则读档后玩家能改题目给定的数字。

候选数自动清理是个提升手感的小功能:每次落子后,扫描同行、同列、同宫的格子,把刚填的值从它们的候选集合里删掉。实现上给SudokuCell加一个removeCandidate(int v)方法,主窗口在onValueEntered末尾遍历相关格子调用即可。这个功能不难,但能让玩家少手动擦很多次。

验证改造是否正确的办法,我一般用「三盘对照」:固定随机种子生成三盘不同难度的题,分别用程序求解器和人工填一遍,比对最终盘面是否一致;再对每一盘做 20 步随机落子加撤销,检查盘面能否精确回到初始状态。这套回归跑一遍,基本能挡住大部分逻辑改坏的情况。

从那以后我每次动这类状态型小项目,都强制先把「生成 → 落子 → 撤销 → 判胜」这条主链路写成可重复的测试再改代码,不然改一处崩三处,调试时间比写的时间还长。希望帮到你。

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

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

矩阵旋转算法:原理与LeetCode解题技巧

1. 矩阵旋转问题解析 矩阵旋转是算法面试中的经典题型&#xff0c;LeetCode 48题&#xff08;旋转图像&#xff09;和1886题&#xff08;判断矩阵是否经旋转得到&#xff09;代表了这类问题的两种典型考察方式。作为程序员必备的基础算法能力&#xff0c;掌握矩阵旋转的核心原理…

作者头像 李华
网站建设 2026/11/4 8:14:50

机器学习模型评估:诊断拟合状态与提升泛化能力

1. 模型评估的核心价值 在机器学习项目中&#xff0c;我们常常会遇到这样的困境&#xff1a;训练集上的预测效果非常理想&#xff0c;但实际部署后却表现糟糕。这种"实验室王者&#xff0c;实战青铜"的现象&#xff0c;本质上是因为没有正确评估模型的拟合状态和泛化…

作者头像 李华
网站建设 2026/11/4 8:14:22

HEVC环路滤波实战:DBF/SAO原理、参数调优与块效应排查

胶片颗粒、雨丝、慢速平移的墙面&#xff0c;这几类画面是编码器最容易露馅的地方。做实时视频的人大概都见过这样的场景&#xff1a;码率压到 2 Mbps&#xff0c;分辨率 1080p25&#xff0c;画面里人一走过去&#xff0c;背景墙面就浮出一格一格的方块&#xff0c;方块内部还带…

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

uni-app模板语法常见问题与多端兼容解决方案

1. uni-app模板语法概述uni-app作为跨平台开发框架&#xff0c;其模板语法是Vue.js的扩展实现。在实际开发中&#xff0c;模板语法问题约占日常调试时间的30%。不同于纯Web环境&#xff0c;uni-app需要同时处理小程序、H5等多端差异&#xff0c;这使得模板语法问题更具特殊性。…

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

babypython Python沙箱逃逸复盘:对象模型、过滤绕过与无回显利用

babypython 这个名字在 Python 沙箱逃逸&#xff08;PyJail&#xff09;圈子里基本等同于"入门题的壳、总决赛的心"。很多人第一眼扫到它&#xff0c;脑子里立刻蹦出 ().__class__.__bases__[0].__subclasses__() 那套祖传 payload&#xff0c;复制、粘贴、回车一气…

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

微信聊天记录如何变成可用数据流?开源工具+Codex+Obsidian实操指南

最近的经历让我对“微信聊天记录”这几个字产生了新的执念。一个项目拖了两个月&#xff0c;真正定下来的细节全散在微信里&#xff0c;一屏屏往上翻&#xff0c;想做个交接文档都凑不齐。用过 Obsidian 和 Codex 的朋友应该能共情&#xff1a;这俩工具一个管知识库&#xff0c…

作者头像 李华