资讯动态

MFC扫雷小游戏开发实战:源码拆解与避坑指南

发布时间:2026/9/9 6:26:05 来源:尧图企业网站定制
简介基于 MFC 开发的扫雷游戏完整源码包复刻 Windows 经典扫雷的核心玩法适合正在学习 C 与 Windows 界面程序设计的开发者作为实战练手项目。工程内部实现了雷区随机生成二维数组配合 C 随机数、CButton 自定义格子响应 BN_CLICKED 消息、CTimer 实时计时与剩余雷数统计、工具栏和菜单交互等经典机制全面覆盖对话框设计、消息映射、控件布局、事件分发与图标重绘等 MFC 高频技能点。压缩包共 71 个文件包含 cpp/h 源码涵盖 View、Document、MainFrame、自定义按钮、记录类等多个模块、26 个 bmp 位图资源、dll/pdb 调试库、exe 可执行程序及 dsw/dsp 工程配置等整体约 2.7MB。已有 474 人浏览学习资源附带项目文件和 ReadMe 说明可直接在 Visual C 6.0 环境编译运行也可对照代码逐步拆解 MFC 游戏程序从界面搭建、消息响应到游戏逻辑封装的全过程对希望快速上手 MFC 或积累 Windows 桌面应用开发经验的学习者来说非常实用。 平时逛技术社区看到新人问 MFC 该做点什么练手我基本都会推荐写一个扫雷。别看扫雷是个小游戏它把 Windows 消息驱动、自绘界面、GDI 绘图、简单算法全串起来了做完之后你对 MFC 的窗口类和消息映射会有非常实在的体感。这篇笔记围绕一个用 MFC 开发的扫雷游戏程序含源码从源码层面拆开设计思路、关键算法、界面写法和我自己踩过的几个坑。适合刚开始学 C 和 MFC、想找一个能完整跑起来又有可玩性的课设项目的读者。1. 项目整体设计与类结构拆分1.1 为什么选 MFC 来做扫雷MFCMicrosoft Foundation Classes是把 Win32 API 用 C 类封装起来的一套框架虽然年纪比不少读者都大但在国内高校课程和 Windows 桌面工具开发里还是经常见到。扫雷这类游戏天然是事件驱动你鼠标点一下、按一下右键系统就发来消息界面再去刷新。MFC 的消息映射机制BEGIN_MESSAGE_MAP / ON_WM_LBUTTONDOWN 这类宏恰好对这种交互模型很友好。相比裸写 Win32 SDK注册窗口类、写窗口过程函数那一套MFC 至少省掉三分之一样板代码相比 QtMFC 工程更轻、更贴近传统 Windows 程序很多课程设计和考试环境也指定 MFC。1.2 类怎么划分才不越写越乱我在实现时把整个程序拆成两层界面层和逻辑层。界面层负责显示与交互逻辑层只干算法和状态的事两者通过简单的接口沟通。CMineSweeperApp继承 CWinApp负责程序入口、实例初始化和主窗口创建。CMainFrame主窗口框架我用的是单文档框架实际就是把客户区让给游戏板窗口。CMineWnd继承 CWnd 的游戏板窗口负责画雷区、处理鼠标消息、管理游戏运行状态。CMineLogic纯逻辑类不依赖任何 Windows 数据类型管雷区二维数组、雷的分布、相邻雷数量、翻开状态、旗标状态和胜负判断。这个拆法看起来界面类做的事比逻辑类多但好处很明显以后想把界面换成控制台版、OpenGL 版逻辑层基本不用动。我见过不少同学的扫雷把所有代码堆在 OnPaint 或者窗口类里一个文件上千行改个雷数都要小心翼翼分层这件事不算炫技但能让你后面省大量时间。2. 核心玩法算法与实现细节2.1 布雷算法很简单但第一下不能踩雷扫雷的雷区本质上是一个二维数组。每个格子的值我习惯这样定义-1 表示雷0 到 8 表示周围相邻雷的数量。布雷时最简单的做法是循环格子靠rand()随机选位置直到布置够雷数。但这里有个新人都容易忽略的体验规则游戏通常要保证玩家第一次点击时踩不到雷否则第一局就炸体验非常糟糕。我用的处理办法是不在游戏开始时布雷而是等第一次点击坐标确定后再布雷并把第一次点击的位置从雷区候选里排除。如果第一击刚好落在“应该”是雷的格子上就把这颗雷挪到当前尚未布雷的任意一个非点击格子里。这样实现简单也不破坏后续扫雷逻辑。2.2 相邻雷数统计与空白区域自动展开布雷完成后对每个非雷格计算八个方向上有多少颗雷。直接两层循环遍历全图每个格子检查八个邻居写法很固定int count 0; for (int dr -1; dr 1; dr) { for (int dc -1; dc 1; dc) { if (dr 0 dc 0) continue; int nr row dr, nc col dc; if (IsInside(nr, nc) mineField[nr][nc] MINE) count; } } minesAround[row][col] count;“翻开空格自动展开一片”是扫雷的灵魂本质是洪水填充flood fill。点一个数字为 0 的格子时用 BFS 或递归把相邻的格子打开遇到数字不为 0 的格子就停住。实现时一定要用一个revealed标记数组防止重复访问否则递归会无限下去导致栈溢出即使雷区只有 10x10 也容易出现这种低级崩溃。2.3 胜负判断别靠猜数清楚再判失败很好判断翻开雷格立即触发。胜利我用的是“翻开计数法”每翻开一个非雷格子就加一当revealedCount 总格子数 - 雷数时判胜。另一种常见写法是“剩余的未翻开格子数等于雷数”但那会涉及未被翻开的格子到底有没有旗标很容易出边界问题。计数法逻辑直白测试也方便单测几组数据就能验证。3. MFC 界面交互与消息处理3.1 不用按钮控件自绘更接近原版扫雷很多新手一上来就拖控件给每个格子放一个 CButton10x10 就要 100 个按钮改难度还得动态创建销毁界面又卡又难看。我选择在CMineWnd的 OnPaint 里用 GDI 把整个雷区画出来方块背景、凸起边框、数字颜色、小红旗、地雷的黑球全部用 Rectangle、Ellipse、DrawText 绘制。这样做有两点好处一是视觉效果很像系统原版扫雷二是不用维护上百个控件数组鼠标消息只要在一个窗口里统一处理。界面尺寸计算要提前定好规则每个格子固定为 24 或 32 像素窗口客户区大小在创建时按列数 * 格子像素和行数 * 格子像素算好。鼠标点击时把客户区坐标减去雷区绘制的基准位置再除以格子宽高取整就能得出是第几行第几列。这里最容易出问题的坐标细节我会在后面的踩坑部分专门讲。3.2 鼠标消息映射与右键插旗左键按下时翻开格子右键按下时插旗或取消旗标。用类向导生成的消息映射大概是这样的BEGIN_MESSAGE_MAP(CMineWnd, CWnd) ON_WM_PAINT() ON_WM_LBUTTONDOWN() ON_WM_RBUTTONDOWN() ON_WM_TIMER() END_MESSAGE_MAP()翻格子我建议在 WM_LBUTTONDOWN 里处理而不是 WM_LBUTTONUP。原版扫雷就是按下即生效反应更快在 UP 事件里处理的话鼠标拖到别的格子上很容易误翻。右键插旗时要同步更新“剩余雷数”的显示并且要给插旗的格子加个锁被旗标覆盖的格子左键不能翻开取消旗标后再释放锁。3.3 计时器与剩余雷数显示区游戏计时我用SetTimer(1, 1000, NULL)每秒触发一次 WM_TIMER在回调里秒数加一刷新顶部显示区。这里有几个细节值得注意计时必须在第一次点击后才启动游戏结束后必须KillTimer新开一局时要把计时器停掉、秒数归零。否则会出现玩家重开游戏计时还在继续跳的诡异现象。数字显示直接用 DrawText 画字符就行不需要去找原版扫雷的位图资源。一般顶部留一条状态栏区域左边显示剩余雷数中间放新开局按钮右边显示时间。状态区域可以和雷区一起画在客户区顶部算好高度偏移即可。4. 实际开发中的坑与排查实录4.1 窗口闪烁两下就让人想放弃第一次写完自绘雷区翻开一片格子会发现窗口闪得厉害。原因是每翻开一格就触发 Invalidate而 OnPaint 里每次都先擦背景再绘制Windows 默认的擦除和重绘反复切换就会闪。解决办法是双缓冲在内存中创建兼容位图和内存 DC所有元素先画到内存 DC最后一次性 BitBlt 到客户区。棋盘这种局部刷新频繁的小程序双缓冲几乎是必做的。我最初偷懒没做结果填色、翻格子都看得很明显做完双缓冲之后那种“干净利落”的体验才是自绘程序该有的样子。4.2 随机种子和相邻两局雷区完全相同新手最容易写出的代码是把srand((unsigned)time(NULL))放在游戏构造函数或每次新开局的函数里。如果玩家在第 59 秒开一局下一局也在这一秒内开time(NULL) 返回的是同一个秒级值两局雷完全一样。后来我把srand挪到CMineSweeperApp::InitInstance里统一调用一次并且团队里其他同事会直接用 C11 的random来生成mt19937雷的分布质量更稳定也避免连续多局“似曾相识”。4.3 坐标换算和窗口尺寸是最常被忽略的隐形坑如果程序有菜单、有边框鼠标消息里OnLButtonDown收到的 point 默认是客户区坐标但很多同学在前面刚用 GetWindowRect 取完窗口坐标再来减就会平白多出一截误差。正确做法是统一使用客户区坐标创建窗口时用AdjustWindowRect把想要的客户区大小换算成整个窗口大小别硬写固定宽高。这个坑最难排查的地方在于界面看起来一切正常只有点击偏上的位置时反应错位让人怀疑是算法问题。后来我加了临时坐标打印才发现差的就是标题栏高度。4.4 新开一局的清理工作比想象中多新开一局要做的清理包括取消所有旗标、重置翻开状态、清空翻开计数、杀掉计时器、秒数归零、重新布雷最后再 Invalidate。如果只重置数组忘记刷新界面玩家会困惑为什么旧状态还在。还有一点用 CBrush、CFont、CBitmap 这类 GDI 对象时绘制完要恢复旧对象并释放资源不然长时间运行后 GDI 对象数量不断上涨最终会出现绘制异常或程序卡死。5. 源码复现、打包与扩展建议5.1 从源码到可运行程序的几个动作拿到的扫雷源码一般包括应用类.h/.cpp、主窗口类、游戏板窗口类、逻辑类、资源文件.rc / resource.h / .ico。在 VS2013 或 VS2019/2022 中直接打开 .sln 或 .vcxproj 后如果提示找不到 MFC 头文件先确认安装 C 开发负载时勾选了“适用于最新 v143 生成工具的 C MFCx86 和 x64”。另外留意工程属性里的“使用 MFC”选项选“在共享 DLL 中使用 MFC”编译更快但部署到别的电脑时要带上对应运行库选“在静态库中使用 MFC”生成的可执行文件更独立体积也更大。5.2 版本迁移常见报错速查报错现象常见原因处理办法找不到 afxwin.h 等头文件没安装 MFC 组件用 VS Installer 补装 MFC 组件字符串从 char* 转 CString 报错工程字符集为 Unicode用_T()宏包字符串或改成多字节字符集编译报 C2447 花括号不匹配老版本项目迁移后文件结构异常用括号匹配功能定位检查 .h 与 .cpp 配对程序提示缺少 MFC DLL共享 DLL 方式发布改为静态链接 MFC或随 exe 分发 DLL运行时窗口尺寸乱套没按客户区计算尺寸用 AdjustWindowRect 换算窗口大小5.3 我建议的后续扩展方向基础版做完之后试着做这几个扩展能把 MFC 练得更透。第一是增加初级、中级、高级和自定义难度对应修改行列数和雷数第二是给格子加一个“踩雷后显示所有地雷位置”的动画这能练到状态机设计第三是用 PlaySound 加音效结束游戏时记得停止播放第四是把最高分记录存到 ini 或注册表练简单的持久化和文件读写第五是想好好研究自绘控件的可以做一个自绘“笑脸”按钮当新开局入口这就是之前有人问的 MFC 自定义按钮的实现思路。这些都是很小的增量改动但每一个都会逼着你去查 MSDN、读 MFC 的核心代码学到的细节远比你背下十个 API 有用。最后说点个人体会我当年把这个扫雷程序交了课设后又把逻辑层用其他语言重写了一遍。当时觉得纯属折腾后来才发现在 MFC 里被迫搞清楚的坐标、状态、消息这些基础概念换到任何 UI 框架都通用。你要是也在做类似的 MFC 小游戏别急着抄完一份源码就交差试着把其中某个局部推倒重写一次收获会大得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价