ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MFC扫雷游戏完整源码拆解:从工程结构到消息绘制全攻略

MFC扫雷游戏完整源码拆解:从工程结构到消息绘制全攻略 简介《用MFC开发的扫雷游戏程序(含源码)》是一份面向C/Windows初学者的完整项目资源适合学习MFC框架、消息映射与游戏逻辑设计的开发者。包内含基于Visual C 6.0构建的扫雷游戏完整工程涵盖对话框界面、自定义按钮控件、雷区随机生成、计时与剩余雷数统计等核心模块并配有可直接运行的exe程序和相关资源文件。压缩包共71个文件主要包括cpp源码文件、头文件、bmp/ico图标资源、MFC动态链接库及Debug生成文件等整体体积仅2.7MB便于下载与本地编译调试。目前已有475人学习下载兼具实践参考与教学价值。通过阅读源码可以深入理解MFC如何将C与Windows消息机制结合掌握CDialog、CButton、消息映射、定时器等关键技术的实际用法对后续开发Windows桌面应用也有直接帮助。1. 用 MFC 开发的扫雷游戏源码完整工程比零散 Demo 值钱在哪这份用 MFC 开发的扫雷游戏程序源码不是零散的片段教学而是一个能直接编译、能玩、带完整工程文件的课设级项目。扫雷逻辑看着简单——布雷、点开、标旗、判胜负但用 MFC 真正落地时消息分发、GDI 绘制、递归展开、资源管理全都要过一遍写一次相当于把 Windows 客户端编程的骨架摸了一遍。适合谁做 MFC 课程设计或毕设的学生想拿完整工程做参照刚学完 C 想练手的开发者准备面试、短期内要捡起 MFC 的从业者。源码价值不在游戏本身而在「界面 逻辑 资源」怎么拆、怎么接这些套路。后面按工程结构、布雷与点击逻辑、绘制与消息、常见坑、扩展用法逐章拆代码可直接对照你的工程改。2. 先看工程结构MFC 四大类怎么在这份源码里分工2.1 对话框版还是文档视图版框架选型决定一切MFC 程序写起来第一件事是选框架。很多 MFC 教程都会先讲 CWinApp、CFrameWnd、CView、CDocument 这四大类但扫雷这种固定大小棋盘的休闲游戏主流做法是对话框版CDialogEx 派生主界面对话框本身就是棋盘。好处是鼠标消息直接在对话框上拦截不需要经过 CFrameWnd 再分发代码路径短新手不容易迷路。另一条路是 CView CFrameWnd 的 SDI 结构程序入口在 InitInstance 里创建框架窗口。CView 自带 OnDraw 机制窗口刷新时系统帮你调 OnDraw适合做需要滚动、缩放的地图类游戏。但扫雷用不上滚动反而多了一层消息路由很多网上流传的工程在这里写得绕。判断一份 MFC 扫雷源码是哪种结构直接看 InitInstance// 对话框版的入口DoModal 一结束程序就退出 BOOL CMineApp::InitInstance() { CMineDlg dlg; m_pMainWnd dlg; dlg.DoModal(); return FALSE; }这段代码对应对话框版。m_pMainWnd 是 CWinThread 的成员用来关联主窗口和消息循环DoModal 是模态对话框的阻塞调用窗口关闭时它返回 FALSEInitInstance 返回 FALSE 表示不进入消息泵、直接退出进程。换成语义就是这个程序的生命周期完全由主对话框管理。关键参数 m_pMainWnd 如果不赋值某些消息比如托盘、命令路由会找不到宿主窗口课设一般不会踩到但要知道这个关联存在。如果你拿到的工程是 SDI 版InitInstance 里通常会看到 RegisterShellClass、LoadFrame 这类调用或者用 CSingleDocTemplate 绑定 Doc/View 类。两份源码玩起来一样但改代码的位置完全不同——前者所有逻辑都堆在 CMineDlg 里后者要把绘制放 OnDraw、逻辑放 Document。2.2 逻辑与界面分离Cell 结构体的设计我看过不少课设源码最大的通病是把棋盘数组、绘制代码、鼠标处理全部塞进 CDialog 一个类里。这份源码做得比较规矩的地方是单独抽了一个游戏逻辑类界面只负责画和收消息。逻辑类长这样// MineLogic.h —— 纯 C 类不依赖 MFC 头文件 enum CellState { HIDDEN 0, // 未翻开 REVEALED 1, // 已翻开 FLAGGED 2 // 玩家插了旗 }; struct Cell { bool isMine; // 是否地雷 int adjacentCount; // 周围 8 格的地雷数 CellState state; // 当前显示状态 }; class CMineLogic { public: CMineLogic(int rows, int cols, int mineCount); void InitBoard(); // 全棋盘清零 void GenerateMines(int safeRow, int safeCol); // 布雷首击周围留空 void CalcAdjacentCount(); // 计算每格相邻雷数 bool Reveal(int row, int col); // 左键翻开返回 false 表示踩雷 bool ToggleFlag(int row, int col); // 右键插旗 / 拔旗 int GetRevealedCount() const; const Cell GetCell(int row, int col) const; private: int m_rows, m_cols, m_mineCount; std::vectorstd::vectorCell m_cells; };注意几个设计点。第一Cell 用枚举表示显示状态而不是用三个 bool避免出现「又翻开又插旗」这种不可能状态。第二m_cells 用 vector 嵌套而不是 CArray因为逻辑类不依赖 MFC拿到纯 C 环境或者单元测试框架里都能跑这算一个好的架构习惯。第三构造函数接收行列数和雷数棋盘尺寸做成参数而不是写死 9×9后面要加难度就容易了。m_rows、m_cols、m_mineCount 这三个成员就是整个游戏的规模参数改难度本质上就是改这三个数。界面类 CMineDlg 持有 CMineLogic 的实例鼠标消息来了就调逻辑层绘制时只读 GetCell 拿数据。整个数据流是单向的界面 → 逻辑 → 数据 → 界面重新绘制。这种单向依赖看着简单实际调试时省很多事——逻辑出错不用去碰绘图代码绘图出错也不用怀疑逻辑被界面污染。2.3 资源文件里藏着的东西对话框模板、图标、控件 IDMFC 工程除了 .cpp还有 .rc 资源文件。扫雷这类程序资源文件里一般就三样对话框模板定义棋盘布局、按钮、静态文本、图标、可能还有菜单。有两点值得留意。对话框模板里如果放了一个 Static 文本控件用来显示「剩余雷数」和「用时」对应的 ID 一般是 IDC_MINE_COUNTER、IDC_TIME_COUNTER。有的工程不用控件直接在客户区画那就省了控件 ID但要在 OnPaint 里自己计算文字位置。图标资源是程序的窗口小图标改起来就是在资源视图里替换 IDR_MAINFRAME。提示在 Visual Studio 里装 MFC 组件时如果当初装的是离线安装包且没勾选 MFC 相关项打开这份工程会直接报找不到 afxwin.h。解决方法是去 VS Installer 里补装「适用于最新 v143 生成工具的 C MFC」不用重装整个 VS。3. 布雷与点击判定从随机种子到递归展开的实现细节3.1 布雷算法随机数与首击保护扫雷的雷区分布靠随机数但直接 rand() 有几个隐患。最典型的是忘记设种子导致每次启动游戏雷的位置一模一样其次是首击踩雷体验极差——正规扫雷都保证第一步不会踩雷。这份源码的做法是「先点击、后布雷」void CMineLogic::GenerateMines(int safeRow, int safeCol) { // 用当前时间做种子避免每次启动雷区相同 srand(static_castunsigned(time(nullptr))); int placed 0; while (placed m_mineCount) { int r rand() % m_rows; int c rand() % m_cols; // 首击格及其周围 3x3 区域不布雷 if (abs(r - safeRow) 1 abs(c - safeCol) 1) continue; if (!m_cells[r][c].isMine) { m_cells[r][c].isMine true; placed; } } }逻辑说明函数在玩家第一次左键点击后才调用所以参数 safeRow、safeCol 是首击位置。while 循环反复随机取坐标遇到已布雷或落在 3×3 安全区的坐标就跳过直到布雷数量达标。m_rows、m_cols、m_mineCount 都是构造时传进来的改难度只需要改这三个数。几个参数细节安全区用 abs(r - safeRow) 1 判断实际上把首击格周围 8 个邻居以及自己全排除了共 9 格。如果棋盘很小比如 6×6 初级这种拒绝式采样在雷很少时没问题但雷多时 while 可能多循环几次对游戏无感。rand() % n 在数学上有轻微分布偏差扫雷这种量级完全够用想更严谨可以换 std::mt19937。布雷之后必须计算邻居雷数否则展开没法玩void CMineLogic::CalcAdjacentCount() { // 8 个方向的偏移量 const int dr[8] { -1, -1, -1, 0, 0, 1, 1, 1 }; const int dc[8] { -1, 0, 1,-1, 1,-1, 0, 1 }; for (int r 0; r m_rows; r) { for (int c 0; c m_cols; c) { if (m_cells[r][c].isMine) continue; // 雷格不统计 int cnt 0; for (int k 0; k 8; k) { int nr r dr[k]; int nc c dc[k]; // 越界检查边缘格子只有 3~5 个邻居 if (nr 0 nr m_rows nc 0 nc m_cols m_cells[nr][nc].isMine) { cnt; } } m_cells[r][c].adjacentCount cnt; } } }逻辑说明用方向数组遍历 8 个邻居比手动写 8 个 if 清晰。越界判断是关键——棋盘边缘的格子数组下标会变成 -1 或等于行列数不判断直接访问就是越界轻则读到脏数据重则崩溃。dr、dc 这两个方向数组也是后续 BFS 展开要复用的东西建议直接写在类成员里。3.2 左键展开递归 Flood Fill 与边界条件玩家点到数字格只翻开该格点到空白格相邻雷数为 0要连锁展开直到碰到数字格为止。这是扫雷最经典的 Flood Fill泛洪填充逻辑bool CMineLogic::Reveal(int row, int col) { // 越界直接返回递归边界条件之一 if (row 0 || row m_rows || col 0 || col m_cols) return true; Cell cell m_cells[row][col]; if (cell.state REVEALED || cell.state FLAGGED) return true; // 已翻开或插旗的格子不重复处理 if (cell.isMine) { cell.state REVEALED; return false; // 返回值 false 告诉界面层踩雷了 } cell.state REVEALED; // 空白格递归翻开 8 个邻居 if (cell.adjacentCount 0) { for (int i -1; i 1; i) { for (int j -1; j 1; j) { if (i ! 0 || j ! 0) Reveal(row i, col j); } } } return true; }逻辑说明递归的三个终止条件分别是越界、已翻开、已插旗。插旗的格子不能翻开这是扫雷的基本规则很多新手漏掉这个判断导致插旗后点左键直接把旗掀了。空白格展开用双层 for 遍历 3×3跳过中心自己其余 8 格递归。关于递归深度m_rows 和 m_cols 在课设里一般是 9×9 或 16×16空白区域再大连锁展开的递归层级也就几十层栈上完全放得下。但如果有人把棋盘改到 100×100 全空白就要小心。稳妥的替代方案是用 std::queue 做 BFS把待展开的格子入队循环处理。逻辑等价但完全不受栈深度限制。我看到不少工程直接用递归也没翻车只是心里要有个数。3.3 胜负判定翻开数对齐非雷数扫雷的胜利条件是所有非雷格全部翻开。用 GetRevealedCount 统计当前状态为 REVEALED 的格子数和总数减雷数比较bool CMineLogic::IsWin() const { return GetRevealedCount() m_rows * m_cols - m_mineCount; }界面层每次点击展开后调一次 IsWin如果为真就弹出 MessageBox 提示成功同时把计时器停掉。失败判定更简单Reveal 返回 false 即踩雷此时界面层调用 GameOver 函数把所有雷标成 REVEALED 显示出来置 m_gameOver 标志之后所有鼠标点击都不再响应。注意胜负判定要在展开之后立刻做而且要防止重复弹窗。常见写法是在 OnTimer 里每秒也调一次 IsWin这样会有「胜利后还弹第二次」的 bug。正确做法是点击处理的代码路径里只判一次配合 m_gameOver 标志位做互斥。4. 绘制与消息响应双缓冲、坐标换算与计时器4.1 双缓冲绘制为什么棋盘会闪先说明一个现象直接在每个格子绘制时调用 InvalidateRect窗口会闪成白板再重画尤其扫雷这种几十个格子的小区域刷新频率一高就特别明显。原因在于 WM_ERASEBKGND 会先擦除背景再执行 OnPaint。解决思路是双缓冲先在一张内存位图上画完整张棋盘再一次 BitBlt 到屏幕。一份合格的 MFC 扫雷源码基本都会用这个方案。void CMineDlg::DrawBoard(CDC* pDC, int rows, int cols) { // 内存 DC先画整张棋盘再一次性上屏避免闪烁 CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap bmp; bmp.CreateCompatibleBitmap(pDC, cols * CELL_SIZE, rows * CELL_SIZE); CBitmap* pOldBmp memDC.SelectObject(bmp); for (int r 0; r rows; r) { for (int c 0; c cols; c) { DrawCell(memDC, r, c); } } // 一次性把内存位图拷到窗口客户区 pDC-BitBlt(0, 0, cols * CELL_SIZE, rows * CELL_SIZE, memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBmp); bmp.DeleteObject(); memDC.DeleteDC(); }逻辑说明CreateCompatibleDC 创建与目标 DC 兼容的内存 DCCreateCompatibleBitmap 生成同样尺寸的位图SelectObject 把位图选进内存 DC接下来所有绘制都落在位图上。全部画完用一次 BitBlt 把位图内容拷贝到窗口 DCSRCCOPY 表示直接覆盖。函数结尾要 SelectObject 还原旧位图、DeleteObject 释放 GDI 对象——GDI 对象是系统级资源不释放会累积跑几十局以后程序会明显变迟钝。参数说明CELL_SIZE 一般取 24 或 32。取 24 时 16×30 的高级棋盘宽 720 像素普通屏幕放得下取 32 手感更好但窗口要大一圈。如果格子间还要画 1 像素缝隙宽度公式要改成 cols * CELL_SIZE (cols - 1)否则最后一行会超出棋盘。DrawCell 负责单个格子。隐藏格用 Draw3dRect 画凸起边框翻开格画凹陷边框数字格用不同颜色描数字void CMineDlg::DrawCell(CDC* pDC, int row, int col) { CRect rc(col * CELL_SIZE, row * CELL_SIZE, (col 1) * CELL_SIZE, (row 1) * CELL_SIZE); const Cell cell m_logic.GetCell(row, col); if (cell.state HIDDEN || cell.state FLAGGED) { // 凸起效果左上亮、右下暗模拟未翻开的方块 pDC-Draw3dRect(rc, RGB(255, 255, 255), RGB(128, 128, 128)); } else { // 翻开格凹陷效果底色偏灰 pDC-FillSolidRect(rc, RGB(210, 210, 210)); pDC-Draw3dRect(rc, RGB(128, 128, 128), RGB(255, 255, 255)); } if (cell.state FLAGGED) { DrawFlag(pDC, rc); // 旗子图形单独成函数后续换图标方便 } if (cell.state REVEALED cell.adjacentCount 0) { CString str; str.Format(_T(%d), cell.adjacentCount); pDC-SetTextColor(GetNumberColor(cell.adjacentCount)); pDC-TextOut(rc.left CELL_SIZE / 2 - 4, rc.top CELL_SIZE / 2 - 8, str); } }数字颜色按经典扫雷习惯1 蓝色、2 绿色、3 红色、4 深蓝、5 棕色。GetNumberColor 就是 switch 返回 RGB纯粹是表现层细节。如果追求更精致的显示可以准备一组 BMP 资源用 MFC 显示 BMP 图片的方式贴图——DrawCell 里把形状绘制换成 BitBlt 小图即可逻辑层完全不用动。这份源码在这块留的位置比较干净换贴图工作量不大。4.2 鼠标消息坐标换算与左右键分工对话框客户区就是我们自己画的棋盘所以鼠标消息直接在 CMineDlg 里处理不需要子控件。坐标换算是把客户区像素坐标除以格子边长void CMineDlg::OnLButtonDown(UINT nFlags, CPoint point) { // 像素坐标 - 格子行列 int col point.x / CELL_SIZE; int row point.y / CELL_SIZE; // 点到了棋盘外面直接忽略 if (row 0 || row m_rows || col 0 || col m_cols) return; if (m_gameOver || m_win) return; // 首击先布雷再展开保证第一步不踩雷 if (!m_firstClick) { m_logic.GenerateMines(row, col); m_logic.CalcAdjacentCount(); m_firstClick true; StartTimer(); } if (m_logic.Reveal(row, col)) { InvalidateRect(NULL, FALSE); // 触发重绘 if (m_logic.IsWin()) WinGame(); } else { LoseGame(); } CDialogEx::OnLButtonDown(nFlags, point); }逻辑说明point.x 除以 CELL_SIZE 得到列号整数除法自动取整。m_gameOver 和 m_win 两个标志位在游戏结束后拦截点击。首击时先 GenerateMines 再 Reveal顺序不能反——反过来就是第一下必然踩雷。StartTimer 只在首击后启动所以计时从玩家真正开始玩才算这个细节很多工程做得不准有的程序一打开窗口就开始计时体验差一截。右键对应插旗void CMineDlg::OnRButtonDown(UINT nFlags, CPoint point) { int col point.x / CELL_SIZE; int row point.y / CELL_SIZE; if (row 0 || row m_rows || col 0 || col m_cols) return; if (m_gameOver || m_win) return; if (m_logic.ToggleFlag(row, col)) { UpdateMineCounter(); // 刷新剩余雷数显示 InvalidateRect(NULL, FALSE); } CDialogEx::OnRButtonDown(nFlags, point); }ToggleFlag 内部逻辑是格子处于 HIDDEN 才能插旗处于 FLAGGED 则拔旗REVEALED 状态不做任何事并返回 false。剩余雷数 总雷数 - 已插旗数UpdateMineCounter 里用 SetDlgItemInt 写到对应控件上这里用 SetDlgItemText 拼字符串也行效果一样。提示如果你用的是 CView 而不是对话框OnLButtonDown 的 point 是视图客户区坐标加了滚动条或者有偏移量时要先加回滚动偏移再做除法。对话框版没有滚动直接换算即可。4.3 计时器与界面刷新计时用 MFC 的 SetTimer。首击时 SetTimer(1, 1000, NULL)每秒触发一次 OnTimervoid CMineDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { m_elapsedSeconds; CString str; str.Format(_T(%03d), m_elapsedSeconds); SetDlgItemText(IDC_TIME_COUNTER, str); if (m_elapsedSeconds 999) KillTimer(1); // 时间到上限就停防止计数溢出 } CDialogEx::OnTimer(nIDEvent); }游戏结束胜负任一时必须 KillTimer(1)否则对话框关闭后定时器还在跑可能引发访问已销毁窗口的崩溃。这个「窗口销毁但定时器未销毁」是 MFC 实际运行里比较典型的一类问题退出时看着没报错Debug 模式下偶尔弹异常。5. 常见问题与避坑四个让扫雷源码翻车的点这一章是踩坑记录全部来自实际运行 MFC 扫雷工程时会遇到的真实故障。每条按「现象 → 原因 → 解决」展开你可以对照自己的现象直接跳着看。现象程序玩久了越来越卡任务管理器里句柄数持续增长。原因GDI 对象泄漏。最常见的是在绘制函数里频繁 new CPen、CBrush 或调用 CreateSolidBrush 后没有 DeleteObject。每次点击都触发重绘每次重绘都新建一个刷子又不释放句柄数就一路涨最终系统资源耗尽窗口变成白板或者直接无响应。另外如果用了 CString 的 GetBuffer 而没有 ReleaseBuffer内存检测工具会报字符串内存泄漏虽然不至于卡死但配合 GDI 泄漏一起看很迷惑。解决能选栈对象就别用堆对象。绘制旗子时用局部 CPen析构时自动释放系统资源void CMineDlg::DrawFlag(CDC* pDC, const CRect rc) { // 局部 CPen离开作用域自动析构 GDI 对象不会泄漏 CPen pen(PS_SOLID, 1, RGB(0, 0, 0)); CPen* pOldPen pDC-SelectObject(pen); pDC-MoveTo(rc.CenterPoint().x, rc.top 4); pDC-LineTo(rc.CenterPoint().x, rc.bottom - 4); // ... 用 MoveTo/LineTo 补一个三角形旗面 pDC-SelectObject(pOldPen); // 还原旧画笔DC 状态也要复原 }逻辑说明栈对象析构会自动删 GDI 对象但 SelectObject 换出的旧对象要还回去否则 DC 内部状态错乱后续绘制可能串样式。自查手段是打开任务管理器看进程的 GDI 对象数或者用 GetGuiResources 打印GDI 泄漏不像内存泄漏那样退出时才察觉进程活着的时候数字涨得最明显。现象点击格子没反应或点击的位置和实际翻开的格子差一格。原因坐标换算没对齐。常见两种一种是消息从子控件冒出来OnLButtonDown 收到的 point 是控件客户区坐标不是对话框客户区坐标直接除 CELL_SIZE 就对不上另一种是格子在绘制时留了边距或者画了外边框像素坐标除以格子边长后取整边缘位置落到了错误的格子序号。解决确认棋盘是从客户区 (0,0) 开始绘制、格子之间没有留缝point.x / CELL_SIZE 才成立。如果绘制时加了 boardLeft、boardTop 边距换算公式要同步改成 (point.x - boardLeft) / CELL_SIZE。把这两处放在同一个函数里计算避免一处改了一处没改。现象每次启动游戏雷的位置完全一样。原因随机种子没设置或者 srand(time(nullptr)) 写在循环内部。time 返回值是秒级同一秒内多次调用 srand 得到相同种子序列。还有一种常见错误是把 srand 放在 GenerateMines 外面但在程序启动时执行而布雷发生在几秒之后中间没有重新播种正常。解决srand 放在 GenerateMines 入口处是最省心的用当前时间做种子。想要更高随机质量可换 std::random_device 配 std::mt19937。检测方法很简单连续重启程序玩三局看首击位置附近雷的分布是否变化。Debug 和 Release 下 rand 实现一致这个坑与编译模式无关。现象点开大片空白区时程序崩溃或递归展开极慢。原因递归 Flood Fill 缺少状态判断。一种情况是没判断 REVEALED 状态导致已经翻开的格子在相邻空白格的递归里被反复处理形成死循环另一种是棋盘尺寸被人为改大后空白连通区域过大递归深度超出默认栈空间。解决Reveal 函数开头完整检查越界、REVEALED、FLAGGED 三种情况缺一不可。棋盘不超过 30×30 时递归没问题如果改成大棋盘把递归换成 std::queue 的 BFS// 递归改 BFS棋盘大时避免栈溢出 void CMineLogic::RevealBFS(int startRow, int startCol) { std::queuestd::pairint, int q; q.push({ startRow, startCol }); while (!q.empty()) { auto [r, c] q.front(); q.pop(); if (r 0 || r m_rows || c 0 || c m_cols) continue; // 越界直接跳过 Cell cell m_cells[r][c]; if (cell.state REVEALED || cell.state FLAGGED) continue; // 已处理或插旗的格子不入队结果 cell.state REVEALED; if (cell.adjacentCount 0) { for (int i -1; i 1; i) for (int j -1; j 1; j) if (i ! 0 || j ! 0) q.push({ r i, c j }); } } }逻辑说明队列里取出的格子先做越界和状态检查再决定是否展开这三个 if 就是防止死循环和越界的关键。崩溃后先看调用堆栈如果看到 Reveal 反复嵌套基本就是状态判断缺失补上即可。6. 进阶用法难度参数化与十分钟源码自检6.1 把难度做成下拉框逻辑类构造时已经支持传入行列数和雷数所以加难度只动界面层。在对话框模板里放一个 Combo BoxOnInitDialog 里添加三项参数映射如下难度行数列数雷数初级9910中级161640高级163099切换难度时重新构造 CMineLogicInitBoard然后 InvalidateRect 重绘。注意雷数上限高级 16×30 共 480 格99 雷没压力但别超过格子总数的 70%否则拒绝式采样布雷会变得很慢连展开逻辑也会退化。另一个细节是切换难度要重置 m_firstClick 和 m_elapsedSeconds否则换到新棋盘还在走旧的一局。6.2 拿到源码后的十分钟自检法我的习惯是拿到任何一份 MFC 游戏源码先做三件事编译一次看警告Debug 跑三局看胜败分支再看 GDI 句柄数。编译警告里出现 possible loss of data、signed/unsigned mismatch 要逐条看扫雷这类程序常见于 int 和 UINT 比较。Debug 跑三局是为了覆盖首击不踩雷、右键标旗、踩雷失败、全部翻开胜利四条路径每一局结束后检查任务管理器句柄数是否回落到初始值。从那以后我每次拿到 MFC 扫雷源码都会强制走一遍这三步先改难度参数验证扩展性再开任务管理器打三局查 GDI 泄漏最后把窗口缩小再放大连续重绘看有没有闪烁。次数多了就发现一份源码值不值得留不是看功能多炫而是看这些基础面干不干净。希望这篇拆解能帮到你也欢迎拿这份源码当模板把 MFC 的框架、绘制、消息三者关系真正跑通一遍。本文还有配套的精品资源点击获取
返回列表