基于Qt的扫雷游戏设计与实现:从工程结构到AI自动求解
简介这是一份基于 Qt 的 C 扫雷小游戏课程设计源码包面向需要完成 C/Qt 课程设计、实训项目的学生以及希望学习 Qt 图形界面与事件交互的开发者。项目包含完整可编译的工程配置涵盖游戏主界面、自定义对话框、用户登录等模块适合作为从入门到进阶的练手案例。压缩包内共有45个文件以 cpp/h 源码、ui 界面文件、png 图片素材和 mp3 音频为主并附带 pro 工程文件、qrc 资源文件及 README 说明各类型文件分工明确界面布局与业务逻辑分离便于对应阅读和二次修改。资源包大小仅 327KB轻量易用。通过学习可掌握 Qt 窗口设计、信号槽通信、鼠标事件处理、定时器与资源管理等方法素材与模块划分完整既能直接运行演示也可作为课程设计报告与功能扩展的基础。已有 427 人学习/下载具备一定参考热度。1. 说真的课设选扫雷比选贪吃蛇划算C课程设计最怕的不是题目难而是题目烂大街。贪吃蛇、图书管理系统、学生信息管理系统一个班二十个人能撞十八个。相比之下基于QT的扫雷小游戏是个很聪明的选择功能边界清晰但实现难度刚好够撑起一篇像样的课程设计报告而且游戏本身自带天然的可视化反馈——点错了会炸、插旗会变色、计时器在走、笑脸在变评审老师看一眼就知道你这项目做到位了根本不用你多解释。这份源码包我拆过一遍结构比想象中完整得多不是那种只有几个cpp糊弄事的裸代码。包里带了MineSweeper.pro工程文件、resource.qrc资源系统、八个UI界面、独立的AI模块、登录和用户记录模块连音效和全套图片素材都齐了。简单说你拿到手能直接编译运行也能拆开逐模块改着玩。如果你是正在做C课设的学生或者想快速上手Qt项目组织方式的开发者这个包值得照着过一遍。2. 先搭好环境再谈代码工程文件与资源系统要怎么拆2.1 用 qmake 还是 CMake这份项目给了你一个参考答案打开压缩包第一眼看到MineSweeper.pro和MineSweeper.pro.user说明原项目用的是Qt Creator qmake这套组合不是我写CMake不好而是课程设计场景下qmake的简单直接确实占优势。.pro.user是 Qt Creator 生成的文件记录的是某台机器上的本地配置比如编译器路径、构建目录换到别的机器基本不可用可以直接忽略。.pro文件才是核心关键内容大概是这样的QT core gui widgets multimedia TARGET MineSweeper TEMPLATE app SOURCES \ main.cpp \ minesweeper.cpp \ block.cpp \ dialog.cpp \ login.cpp \ user.cpp \ ai.cpp HEADERS \ minesweeper.h \ block.h \ dialog.h \ login.h \ user.h \ ai.h FORMS \ minesweeper.ui \ dialog.ui \ login.ui \ user.ui \ custom.ui \ about.ui \ record.ui RESOURCES resource.qrc我自己习惯把SOURCES/HEADERS/FORMS分行列出来编译报错了能一眼定位是哪组文件出了问题。QT multimedia这行决定了音效能不能放出来后面避坑章节会专门讲——新手最容易在音效模块上翻车不是没加multimedia就是加了却没在pro里声明。如果 你偏要换到 CMake也是在CMakeLists.txt里补find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia)然后把 cpp、头文件、ui 文件位置告诉 AUTOMOC/AUTOUIC本质没有区别只是把编译配置从 qmake 语法换成了 CMake 语法。2.2 resource.qrc 是一张藏宝图图片和音效全靠它索引resource.qrc在Qt里承担的是「资源登记簿」的角色。项目里所有图片、音频、图标子都必须在这里登记了代码里才能用路径访问到。看一眼这个文件的内容RCC qresource prefix/ fileImages/background.png/file fileImages/block.png/file fileImages/mine.png/file fileImages/flag.png/file fileImages/number_1.png/file fileImages/click.mp3/file fileImages/lose.mp3/file /qresource /RCC访问规则很简单prefix/表示根路径Images/background.png对应代码里的:/Images/background.png。注意冒号开头是Qt资源系统的固定语法不是相对路径也不是绝对路径新手经常在这里写成Images/background.png导致加载失败。做课程设计的时候你可能会往里面加自己录的音频或者新画的表情图片。加完文件后记得在.qrc文件里补一条file记录然后右键resource.qrc→Build Resource编译一次资源。我见过太多同学改了图片却忘了更新 qrc运行起来一直显示旧图白白浪费几个小时排查。提示打包发布时用了qrc里的资源就已经内嵌进可执行文件了不需要额外带着Images文件夹拷贝。这一点对课设演示特别友好拷个 exe 就能跑。2.3 Qt 版本确认你的版本和原作者的不一定匹配MineSweeper.pro.user文件里锁定了原来的构建套件信息如果是Qt 5.15.2或Qt 6.x不同版本之间存在几个明显的差异点Qt 5 用REG_EXP_ONE正则表达式语法Qt 6 用QRegularExpression。Qt 6 删掉了QTextStream::flush()之外的一些过时接口比如QRegExp需要额外qt5compat模块。Qt 6 的QMediaPlayer接口变化很大原来的setMedia()变成了setSource()。建议直接用与.pro.user匹配的版本打开或者用较新的 Qt 6.5 以上版本但要注意适配接口变动。课程设计容不得这种环境问题先把版本对齐能省半天折腾。3. 游戏核心逻辑拆解从二维数组到递归展开3.1 迷宫和布雷一维数组的二维思维源码包里的block.h和block.cpp把每个格子封装成了一个Block类这是整份代码里最值得学习的对象设计思路。Block负责记录这块格子自身的状态——是雷、是数字还是空格子有没有被揭开、有没有被插旗这些状态字段从第一个格子初始化时就固定下来了。minesweeper.cpp里initGame()函数实现了布雷逻辑。注意一个细节游戏第一步点击不该踩雷这是扫雷用户体验的底线。源码的做法是延迟布雷——第一次点击事件里拿到点击坐标后把雷布在除该点附近的其余区域里。这样保证了游戏永远不会「第一步就炸」而传统做法是在开局前就布雷要么做「点击后挪雷」的逻辑要么干脆冒着骂名让玩家自认倒霉。接下来是数字生成。每布置一颗雷它周围8个格子的surround_mines字段就加1。这一阶段是全游戏性能开销最小但最容易出错的地方边界格子很容易数组越界。源码里用一个相对坐标偏移表处理8个方向int dx[8] {-1, -1, -1, 0, 0, 1, 1, 1}; int dy[8] {-1, 0, 1, -1, 1, -1, 0, 1}; void MineSweeper::countMines(Block cell) { if (cell.is_mine) return; int mineCount 0; int row cell.row, col cell.col; for (int i 0; i 8; i) { int nr row dx[i]; int nc col dy[i]; if (nr 0 nr rows nc 0 nc cols) { Block neighbor blocks[nr * cols nc]; if (neighbor.is_mine) mineCount; } } cell.mine_count mineCount; }逻辑说明外层循环遍历所有格子内层用dx/dy偏移量取周边8格越界检查是nr 0 nr rows nc 0 nc cols四条件缺一不可。blocks数组是按行优先存储的一维数组所以索引是nr * cols nc而不是[nr][nc]。参数说明dx/dy的第 i 组加起来就是从当前格到第 i 方向邻格的偏移比如dx[0] -1, dy[0] -1表示左上角。这个偏移表写法比一堆if (row0...)嵌套清晰得多也方便后期扩展。3.2 递归揭开当点击空格时的链式反应扫雷最爽的部分就是点开一大片空白格子的瞬间背后是经典的深度优先搜索DFS递归算法。源码的revealBlock()函数思路是当前格是Block::REVEALED或Block::FLAGGED就直接返回防止重复揭开。当前格有数字(mine_count 0)揭开后停止向四周扩散。当前格是空格(mine_count 0)揭开后对所有邻居递归调用revealBlock()直到遇到数字格或边界为止。递归写法短做课设答辩的时候容易讲清楚但有个隐患rows * cols超过几千个格子、同时空格连成一大片时递归层数可能很深。Windows 默认栈空间只有 1MB极限情况下栈溢出不是理论问题而是实测翻车。如果你要扩展成高级模式或者大尺寸地图建议改成显式栈的迭代版void MineSweeper::revealBlockIterative(int row, int col) { QStackQPoint stack; stack.push(QPoint(row, col)); while (!stack.isEmpty()) { QPoint p stack.pop(); int r p.x(), c p.y(); if (!isValid(r, c)) continue; Block cell blocks[r * cols c]; if (cell.state ! Block::HIDDEN || cell.is_mine) continue; cell.state Block::REVEALED; if (cell.mine_count 0) { for (int i 0; i 8; i) { int nr r dx[i], nc c dy[i]; if (isValid(nr, nc)) { Block nb blocks[nr * cols nc]; if (nb.state Block::HIDDEN !nb.is_mine) stack.push(QPoint(nr, nc)); } } } } }逻辑说明QStack模拟系统栈每次取一个坐标合法就揭开并把自己周边未揭开的格子入栈。和递归版效果一样但不受调用栈深度限制。isValid(r,c)封装了行列越界检查避免每次写四行判断。参数说明QPoint用的是 Qt 的坐标类x()对应行、y()对应列写之前和写之后的对应关系要心里有数被这个绕晕的同学不在少数。3.3 胜利判定贴吧论坛里吵过八百遍的细节扫雷的胜利条件是「把所有非雷格子揭开」不是「把所有雷插上旗子」。源码里checkWin()的判断方式是遍历所有格子统计非雷格中已揭开的数量等于非雷总数即获胜。注意它没有走「旗子数量 雷数」的邪路——因为玩家可以瞎插旗子也可能少插以旗子判断胜负完全不可靠。这个设计也引出一个良好的交互习惯右键插旗只是为了玩家自己标记游戏本身并不把插旗当终点。真正写课程设计报告时这个判断逻辑值得单独写一小节因为它体现了需求分析阶段对「胜利」定义的理解深度答辩时是加分项。4. 从UI到音效封装一个逼格够高的扫雷界面4.1 自定义绘制让每一格都能换皮肤项目不靠现成控件堆界面而是用重写paintEvent()的方式自己绘制每一个格子。block.h里定义了BlockType枚举HIDDEN、REVEALED、FLAGGED、QUESTIONpaintEvent()根据格子的state查表选择对应图片资源。minesweeper.cpp里的绘图核心逻辑抽象出来是这么个模式void MineSweeper::paintEvent(QPaintEvent* event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); for (int i 0; i rows * cols; i) { Block cell blocks[i]; int col i % cols; int row i / cols; int x margin col * cellSize; int y margin row * cellSize; QPixmap pixmap; if (cell.state Block::HIDDEN) { pixmap.load(:/Images/block.png); } else if (cell.is_mine) { pixmap.load(:/Images/mine.png); } else if (cell.mine_count 0) { pixmap.load(QString(:/Images/number_%1.png).arg(cell.mine_count)); } else { pixmap.load(:/Images/blank.png); } painter.drawPixmap(x, y, cellSize, cellSize, pixmap); } }逻辑说明paintEvent是每个 QWidget 的核心重绘方法只要触发update()就会调用。i % cols算出列号、i / cols算出行号这就是前面说的按行优先存储的逆向索引。图片路径拼QString(:/Images/number_%1.png)用数字序号直接映射1到8的图片不用写八个if分支。参数说明cellSize是每格边长margin是棋盘距窗口边缘的留白这些值在setBoardSize()里按窗口可用尺寸动态计算。想调格子大小就改这两个变量不用去动绘图逻辑。4.2 鼠标事件与右键插旗交互爽感的关键继承QWidget后重写鼠标事件是Qt做自定义控件的标准姿势。源码里mousePressEvent()分左右键处理左键调用revealBlock()右键切换旗子状态中键对数字格快速清周边。右键切换通常走一个状态循环HIDDEN - FLAGGED - QUESTION - HIDDEN所以插旗和打问号都是同一套逻辑在不同状态下的切换。一个容易被忽略的设计点是左键点击雷后的处理源码的做法是直接触发布局刷新并把雷格标记为MINE_EXPLODED配合lose.mp3音效和弹窗提示。这个状态会被paintEvent()单独处理比如用红色的mine_error.png画爆炸的那一格。 细节做到这里课设可以拿来讲了。4.3 音效播放QMediaPlayer 的正确打开方式resource.qrc里放着click.mp3、lose.mp3、win.mp3和Audio目录。源码里播放音效用的是 QMediaPlayer QAudioOutput 的组合写出来的效果类似于QMediaPlayer* player new QMediaPlayer(this); QAudioOutput* audioOutput new QAudioOutput(this); player-setAudioOutput(audioOutput); void MineSweeper::playSound(const QString soundFile) { player-setSource(QUrl::fromLocalFile(soundFile)); audioOutput-setVolume(0.8); player-play(); }逻辑说明QMediaPlayer 本质是「把媒体资源解码成音频流」的播放器QAudioOutput 负责把音频流送到声卡。播放前要setSource()来加载资源点播放是异步行为你按完play()函数就返回了不会阻塞界面。音效文件要加入到resource.qrc才能用qrc:/Audio/click.mp3路径访问否则就要用绝对路径课设演示时换台机器就翻车了。参数说明setVolume(0.8)是播放音量范围0-10.8是个听感舒适且不盖过现场说话声的默认值。如果不想每次播放时新建对象可以在构造函数里初始化一次player重复调用setSource就行。提示Qt 6 里QMediaPlayer必须配QAudioOutput否则播放不出声。Qt 5 里是setMedia()没有 QAudioOutput。拿到源码后先看版本再看播放代码顺序不能颠倒。5. 用户登录与AI助手课设加分项怎么落地5.1 登录注册模块本地用户信息的存储方案扫雷本身不强制要求账号系统但课程设计加了登录、用户管理、最高分记录答辩时就会让老师觉得你有「工程思维」。源码包的login.cpp、user.cpp、record.ui就是这个作用的。登录模块最简单的实现方式是读写本地文本文件——users.txt每行存一条用户名 密码 MD5值每次登录比对注册时追加一条。QT 原生的QFile读写配合QCryptographicHash做密码MD5代码量很小却能有效引导老师问数据库相关的问题——你可以回答「本项目用文件存储够用了扩展成SQLite只需替换一个访问接口」。record.ui里展示的是历史游戏记录用时、难度、胜负从record.txt读出来塞到QTableWidget里void UserWindow::loadRecords(const QString username) { QFile file(records.txt); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { recordTable-setRowCount(0); return; } QTextStream in(file); int row 0; while (!in.atEnd()) { QString line in.readLine(); QStringList parts line.split(|); if (parts.size() 4 parts[0] username) { recordTable-insertRow(row); recordTable-setItem(row, 0, new QTableWidgetItem(parts[1])); recordTable-setItem(row, 1, new QTableWidgetItem(parts[2])); recordTable-setItem(row, 2, new QTableWidgetItem(parts[3])); row; } } }逻辑说明QTextStream::readLine()按行读split(|)拆字段insertRow动态加行setItem填单元格。用|做分隔符是因为用户名不太可能含这个字符比逗号安全。如果文件不存在直接返回不崩不报错这是文件读写的基本素养。参数说明setColumnCount要在loadRecords之前调好否则setItem时列索引越界。QTableWidgetItem构造的字符串会被直接展示排序功能默认关闭对课设来说不用管。5.2 AI助手模块自动扫雷的人工智能皮ai.cpp和ai.h可能是这份包最大的惊喜很多扫雷课设只有「人能玩」绝没有「AI能自动玩」。这里实现的AI逻辑通常是基于约束传播的推理扫描所有已揭开的数字格对每个数字格统计其周围未揭开格子的数量以及未被标记的旗子数。如果某个数字格的数字等于周围旗子数那么它周围所有未揭开格子都是安全的这是「确定性推进」。如果某个数字格的数字等于周围未揭开格子总数那么所有未揭开格子都是雷这是「确定性标雷」。当无法推理时AI随机点一个没有旗子的未揭开格子。下面是约束传播的经典实现模式QPoint AI::getNextMove(MineSweeper* game) { int rows game-rows, cols game-cols; // 第一轮确定性扫雷 for (int i 0; i rows * cols; i) { Block cell game-blocks[i]; if (!cell.is_revealed || cell.mine_count 0) continue; int hiddenCount 0, flagCount 0; for (int d 0; d 8; d) { int nr i / cols dx[d]; int nc i % cols dy[d]; if (nr 0 nr rows nc 0 nc cols) { Block nb game-blocks[nr * cols nc]; if (nb.state Block::FLAGGED) flagCount; if (nb.state Block::HIDDEN) { hiddenCount; } } } if (hiddenCount 0 cell.mine_count flagCount) { // 数字等于旗数点开所有隐藏格子 for (int d 0; d 8; d) { int nr i / cols dx[d]; int nc i % cols dy[d]; if (nr 0 nr rows nc 0 nc cols) { Block nb game-blocks[nr * cols nc]; if (nb.state Block::HIDDEN) return QPoint(nr, nc); } } } } // 第二轮无法推理随机点一个 QListQPoint candidates; for (int i 0; i rows * cols; i) { Block cell game-blocks[i]; if (cell.state Block::HIDDEN !cell.is_mine) { candidates.append(QPoint(i / cols, i % cols)); } } return candidates.isEmpty() ? QPoint(-1, -1) : candidates.at(QRandomGenerator::global()-bounded(candidates.size())); }逻辑说明先锁定一个已揭开的数字格统计周边隐藏格与旗子数。如果mine_count flagCount说明剩下的所有隐藏格都是安全格是可揭开的返回第一块安全格坐标。如果找遍全场没有任何数字格能提供确定性结论就只能走随机路线。这是扫雷AI最基础也最核心的推理模式。参数说明QRandomGenerator::global()-bounded(n)是 Qt 6 的安全随机接口返回[0, n)之间的整数用来随机选一个候选点。注意bounded的参数是上界不是下界踩过这个坑的同学都知道bounded(0, n)和bounded(n)的区别。这份AI模块虽然只是「单步推理随机兜底」没有用概率分析但对课程设计的展示场景已经足够亮眼——老师看着AI自己玩完一局初级扫雷这个画面本身就有冲击力。你还可以把它扩展成全自动连玩把getNextMove()的返回值通过QTimer::singleShot逐个触发点击模拟真人手速。5.3 自定义难度与扩展空间custom.ui和dialog.cpp支持自定义雷区行列数和雷数。这块实现的核心是setBoardSize()void MineSweeper::setBoardSize(int r, int c, int mineCount) { rows r; cols c; totalMines mineCount; blocks.clear(); for (int i 0; i rows * cols; i) { Block b; b.row i / cols; b.col i % cols; b.state Block::HIDDEN; b.is_mine false; b.mine_count 0; blocks.append(b); } int area (width() - 2 * margin) / cols; int area2 (height() - 2 * margin) / rows; cellSize qMin(area, area2); update(); }逻辑说明blocks.clear()后重建所有格子保证新游戏从零开始。cellSize用qMin保证棋盘整体适配窗口宽高。这里的坑在于必须在重建格子之前重置所有状态否则新棋盘某些格子会继承上一局的state看起来像灵异事件。参数说明width()和height()是当前窗口客户区尺寸规划好留白margin后用窄边决定格子边长防止棋盘超出窗口边界。雷数大于rows * cols时程序会崩UI 层要限制输入的合理性这是后续扩展时最容易忽略的边界条件。6. 踩坑与修复我把课设里最容易翻车的六个坑给你列全下面这些坑不夸张地说十个课设九个踩。每一条都是「现象 → 原因 → 解决」三件套照着查能省不少时间。6.1 图片显示不出来整个棋盘黑乎乎现象编译运行后界面能弹出来但棋盘区域全黑或者某些数字格的图片缺失。原因资源路径不对。最常见的是:/Images/number_1.png写成了Images/number_1.png少了冒号和斜杠或者resource.qrc里没登记这张图。另一个隐蔽原因是文件名大小写不一致——Windows 不区分大小写但 Qt 的资源系统区分。解决先看resource.qrc里有没有这个file条目再看代码里路径前缀。直接按 F5 调试在pixmap.load()之后加一行qDebug() pixmap.isNull();输出true说明路径加载失败立刻能定位问题。6.2 音效只响一下后续全哑现象第一次点格子有声音之后点任何地方都没声了。原因QMediaPlayer 只能同时播放一个源连续调用setSource()会打断当前播放。如果用了delete player重建逻辑更惨。解决不要在每次点击时新建 QMediaPlayer正确姿势是每个音效一个 player 或者在播放前stop()再setSource()player-stop(); player-setSource(...); player-play();6.3 关掉窗口后程序还在后台跑现象点窗口右上角 X 关掉扫雷但任务管理器里MineSweeper.exe还在。原因main.cpp里的QApplication事件循环没退干净通常是有定时器或者线程没停。源码里计时器用QTimer刷新游戏时间如果timer-stop()没写在析构函数里窗口销毁后定时器还在触发。解决在MineSweeper析构函数里调用timer-stop()和delete timer确保所有子对象先销毁再退出MineSweeper::~MineSweeper() { timer-stop(); delete timer; }6.4 递归揭开超大空白区域时崩溃现象高级模式点开空白格群程序直接退出没有报错。原因递归栈溢出。前面 3.2 节提过revealBlock()递归调用自身直到被数字格挡住超大地图上空白区连片递归深度可能到几千层直接打爆系统栈。解决替换成 3.2 节的QStack迭代版本。或者限制最大递归深度达到某个阈值改用迭代方案。6.5 双击太快格子被跳过现象快速双击空白格子有时候只触发了一次左键事件格子没揭开。原因Qt 默认会合成双击事件mouseDoubleClickEvent第二次点击被识别成double click不再触发mousePressEvent。解决两个思路要么重写mouseDoubleClickEvent并在里面调用revealBlock()要么在mousePressEvent里直接处理而不依赖mouseReleaseEvent。源码里用前者比较多void MineSweeper::mouseDoubleClickEvent(QMouseEvent* event) { mousePressEvent(event); }6.6 雷数和棋盘尺寸设置离谱值程序崩溃现象自定义棋盘填了 100 行 100 列 8000 个雷一运行直接闪退。原因blocks数组大小是rows * cols雷数却可能大于可容纳总数或者填了 0 行除零崩溃。解决在setBoardSize()入口做合法性校验if (r 3 || c 3 || mineCount r * c) { QMessageBox::warning(this, 错误, 雷数必须小于格子总数且行列至少3); return; }注意6.3 和 6.5 出现的概率远超你想象。课设答辩现场当着老师的面复现这两个 bug基本就是送命行为提前修掉是稳赚不亏的。7. 验证你的改造是否成功一段可复现的手工测试流程扫雷这种交互型项目光看代码跑一遍不等于没问题。我一般会固定走一段手工测试流程十个检查点过一遍基本能保证核心逻辑没有大 bug启动程序登录账号注册一个新用户再用它登录。初级难度开局第一步点击后先确认计时器从0走起。右键插旗后确认旗子数量跳动正确不会再往旗子上左键触发揭开。连续点击空白格群确认一次展开大区域且数字周围边界清晰。踩中一次雷确认爆炸动画和lose.mp3音效还有「重新开始」按钮能用。插旗插满所有雷点「重开」确认棋盘完全重置为新一局。点「AI演示」让 AI 自己玩一局初级看能不能在合理次数内完成能走到30%以上算合格。切到高级模式双开快速操作5分钟确认处理器占用正常、界面不卡顿。关闭窗口检查任务管理器无残留进程。重新进入游戏确认历史记录里有刚打的一局成绩。如果这十个点全过项目稳定性就基本过关了。再额外花十分钟把README.md里的「环境要求」部分补充一下写明自己测试通过的 Qt 版本和编译器组合这份课设的完整性就很能打了。顺带说一个我自己每次都会用的进阶技巧给block.h里的Block类加一个update()方法每次状态变更后只刷新当前格子的绘制区域而不是全盘update()。这在10x10的小棋盘上体现不出差异但你一秒钟点出二十多次操作时只重绘单个格子比全盘刷新的开销低得多动作快了界面也不闪。改造的思路就是在minesweeper.cpp的mousePressEvent里把原来全量update()改成局部刷新void MineSweeper::refreshCell(int row, int col) { QRect rect(margin col * cellSize, margin row * cellSize, cellSize, cellSize); update(rect); }update(rect)是 QWidget 的局部重绘接口只更新传入的矩形区域事件循环里会自动合并相邻的重绘请求。这种优化讲过很多次——展示给老师看的时候你直接说「我只刷新被点击的那一格而不是整个棋盘」懂行的老师立刻就知道你是真写过代码的人。说到最后想起我最初拆这个包的时候自己在第 6.3 个坑上卡了一晚上怎么都找不到窗口关闭后进程残留的原因最后在析构函数里加了timer-stop()瞬间解决。从那以后我每次拿到 Qt 项目第一件事就是去构造函数里看有没有new出来的定时器或线程对象然后检查对应的析构函数有没有处理干净强制走一遍这个核查流程。希望这篇拆解能帮你在课设答辩前少熬几个夜也希望你拿到源码后不是照搬交差而是真把它跑明白、改成你自己的东西。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

顺序表进阶:动态扩容与边界条件实战指南

顺序表进阶:动态扩容与边界条件实战指南

很多人学顺序表,第一天对着结构定义、初始化、插入、删除一顿操作,跑通了输出,就觉得自己已经拿下了。但真正到了第二天,开始做动态扩容、碰边界条件的时候,才发现代码怎么改怎么崩。我自己当年顺序表学习计划做到day2…

2026/10/18 6:26:51 阅读更多 →
基于Simulink的100kW微型燃气轮机动态建模与控制策略仿真

基于Simulink的100kW微型燃气轮机动态建模与控制策略仿真

1. 项目概述与整体设计思路1.1 这个选题到底在做什么100kW这个功率级别,在微型燃气轮机里是一个非常有意思的区间。它比几kW级的微燃机更接近工业级应用,又比兆瓦级燃机轻巧灵活得多,主要面向冷热电联供、海岛孤网供电、移动电源车、以及一些…

2026/10/18 6:26:51 阅读更多 →
C++输入流cin深度解析:缓冲区、getline与常见坑

C++输入流cin深度解析:缓冲区、getline与常见坑

1. 输入这件事,为什么从cin开始C入门阶段,你其实只需要搞清楚一件事:程序怎么和外面的人打交道。前面两章你可能已经见过cout往屏幕上输出内容,但光能输出远远不够——一个没有输入的交互程序,就像一个只会自说自话的接…

2026/10/18 6:26:51 阅读更多 →

最新新闻

基于机器视觉的SCARA机械臂分拣仿真:VREP与MATLAB联合实现

基于机器视觉的SCARA机械臂分拣仿真:VREP与MATLAB联合实现

"基于机器视觉技术的流水线自动分拣机器人仿真:VREP与MATLAB联合实现SCARA机械臂按色..."——这个标题完整展开其实是一类特别典型的项目:给流水线加一套“长了眼睛的机器人”,让它根据颜色把不同物料分到不同工位。很多搞自动化、…

2026/10/18 7:18:01 阅读更多 →
AI副业实操指南:用技能杠杆打破纯打工模式,实现个人变现

AI副业实操指南:用技能杠杆打破纯打工模式,实现个人变现

这两年朋友圈里讨论AI副业的人越来越多:白天还在写周报的同事,晚上已经在小红书上接定制头像的订单;之前在传统行业做了五年的朋友,靠着AI辅助做垂直内容,现在每月的副业收入已经稳定覆盖房贷。这类故事听多了&#xf…

2026/10/18 7:18:01 阅读更多 →
git 提交代码报 pre-receive hook declined?把 remote 改到 TaoToken 后先查这 3 处

git 提交代码报 pre-receive hook declined?把 remote 改到 TaoToken 后先查这 3 处

/* 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 7:18:01 阅读更多 →
别再让 AI 只会聊天了!Hermes Agent 自带 47 个实用工具,一键解锁智能体真正实力|TaoToken 统一 Key 打通终端后端与 MCP

别再让 AI 只会聊天了!Hermes Agent 自带 47 个实用工具,一键解锁智能体真正实力|TaoToken 统一 Key 打通终端后端与 MCP

/* 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 7:18:00 阅读更多 →
JMeter压测实时监控:Prometheus+Grafana大屏搭建全指南

JMeter压测实时监控:Prometheus+Grafana大屏搭建全指南

做性能压测的朋友应该都有过这种体验:脚本跑起来了,并发线程一拨一拨地往服务器打,屏幕上却只有JMeter那个滚动的日志栏,想知道当前QPS到底多少、错误率涨没涨,要么靠猜,要么等压测结束再去看聚合报告。压测…

2026/10/18 7:18:00 阅读更多 →
Netty实战进阶:粘包半包、EventLoop线程边界与长连接心跳

Netty实战进阶:粘包半包、EventLoop线程边界与长连接心跳

不少人可能和我有同样的感受:Netty的入门教程看了一遍又一遍,Echo服务器、聊天室Demo敲得飞起,但一到真实项目里要“实战应用”了,反而手足无措。客户端发过来的数据一多,消息全黏在一起;连接一挂&#xff…

2026/10/18 7:17:00 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 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/18 0:55:02 阅读更多 →
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/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练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/16 14:23:36 阅读更多 →