
简介本资源是一份基于MFC框架开发的Windows桌面扫雷游戏完整工程面向C初学者与GUI编程入门学习者帮助理解Windows消息机制、GDI绘图、资源管理及经典游戏逻辑实现。压缩包共45个文件含17个BMP图像资源用于地雷、数字、表情等界面元素、7个头文件与6个CPP源文件构成文档/视图/主框架三层结构、2个ICO图标及可直接运行的EXE程序另有RC资源脚本、工程配置文件DSP/DSW和编译中间产物整体2.85MB结构典型、模块划分清晰。已有315人学习下载提供开箱即用的游戏体验与可调试的完整源码读者可深入分析扫雷核心算法如雷区生成、递归展开、胜负判定、UI响应逻辑鼠标左键挖格、右键插旗及资源加载流程是掌握MFC基础开发与小型游戏项目实践的优质参考案例。1. 这不是怀旧彩蛋而是一份可调试、可扩展的 MFC 游戏工程骨架Windows 7 自带扫雷双击打不开那多半是系统精简或 MFC 运行时缺失——但真正值得深挖的是它背后那套被低估的桌面 GUI 开发范式。这个sweepmine-master.zip项目不是简单复刻而是一个结构清晰、模块分离、完整保留 MFC 文档/视图架构Document/View Architecture的可编译工程。它用标准CWinAppCDocumentCViewCFrameWnd四件套组织逻辑MineSweeperDoc.cpp管理雷区数据与游戏状态MineSweeperView.cpp负责绘制格子、旗帜、数字和爆炸动画SetDlg.cpp实现难度设置对话框。所有资源图标、位图、菜单通过Mine Sweeper.rc统一管理.dsp/.dsw文件表明它原生适配 Visual Studio 6.0 到 VS2010 环境。对刚接触 Windows 桌面开发的 C 工程师来说它比“让控制台程序支持 MFC”更真实——你看到的就是一个能运行、能断点、能改样式、能加音效的最小可行产品MVP而不是教科书里的抽象类图。2. 从资源加载到雷区生成MFC 文档/视图架构的实际落地路径2.1 工程结构解析为什么CDocument是雷区数据的唯一可信源MFC 的文档/视图模式不是装饰性设计而是强制性的职责隔离。在本项目中MineSweeperDoc.h定义了核心数据结构class CMineSweeperDoc : public CDocument { protected: CMineSweeperDoc(); // protected constructor used by dynamic creation DECLARE_DYNCREATE(CMineSweeperDoc) // Attributes public: int m_nRows; // 行数默认9 int m_nCols; // 列数默认9 int m_nMines; // 地雷总数默认10 BYTE* m_pBoard; // 雷区数据缓冲区0空地1雷2已标记3已翻开 BYTE* m_pRevealed; // 显示状态缓冲区0未翻开1已翻开 int* m_pNumbers; // 数字提示缓冲区每个格子周围雷数仅当该格非雷且已翻开时有效 BOOL m_bGameStarted; // 游戏是否已开始影响首次点击不布雷逻辑 BOOL m_bGameOver; // 游戏结束标志 int m_nFlagsLeft; // 剩余旗子数 };注意m_pBoard和m_pRevealed是两个独立缓冲区这是实现“标记-翻开”分离的关键。很多初学者误以为用一个数组存状态即可但会导致无法同时支持“右键标记”和“左键翻开”两种互斥操作——MFC 的设计迫使你把状态拆解为语义明确的字段。CMineSweeperDoc::OnNewDocument()中完成初始化BOOL CMineSweeperDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // 分配内存按行列计算总格子数 int nSize m_nRows * m_nCols; m_pBoard new BYTE[nSize]; m_pRevealed new BYTE[nSize]; m_pNumbers new int[nSize]; // 全部清零未翻开、无雷、无数字 memset(m_pBoard, 0, nSize); memset(m_pRevealed, 0, nSize); memset(m_pNumbers, 0, nSize * sizeof(int)); m_bGameStarted FALSE; m_bGameOver FALSE; m_nFlagsLeft m_nMines; return TRUE; }2.2 雷区布设算法避免首次点击踩雷的“延迟布雷”策略扫雷最反直觉的设计在于第一次点击绝不会踩雷。这并非运气而是工程实现——CMineSweeperDoc::StartGame(int x, int y)在首次点击时才真正布雷并确保(x,y)及其八邻域为空地void CMineSweeperDoc::StartGame(int x, int y) { if (m_bGameStarted) return; m_bGameStarted TRUE; // 步骤1将所有格子标记为“可布雷” int nSize m_nRows * m_nCols; std::vectorBOOL canPlace(nSize, TRUE); // 步骤2禁用首次点击位置及其8邻域防止踩雷保证开局可展开 for (int dy -1; dy 1; dy) { for (int dx -1; dx 1; dx) { int nx x dx, ny y dy; if (nx 0 nx m_nCols ny 0 ny m_nRows) { int idx ny * m_nCols nx; canPlace[idx] FALSE; } } } // 步骤3随机选择 m_nMines 个可布雷位置 int placed 0; while (placed m_nMines) { int r rand() % nSize; if (canPlace[r]) { m_pBoard[r] 1; // 标记为雷 canPlace[r] FALSE; placed; } } // 步骤4计算每个非雷格子的周围雷数 for (int i 0; i nSize; i) { if (m_pBoard[i] 1) continue; // 跳过雷格 int count 0; int row i / m_nCols, col i % m_nCols; for (int dy -1; dy 1; dy) { for (int dx -1; dx 1; dx) { int nr row dy, nc col dx; if (nr 0 nr m_nRows nc 0 nc m_nCols) { int nidx nr * m_nCols nc; if (m_pBoard[nidx] 1) count; } } } m_pNumbers[i] count; } }提示rand()在 MFC 中需在InitInstance()中调用srand((unsigned)time(NULL))初始化种子否则每次启动雷区相同。项目中ReadMe.txt未说明此点是常见遗漏。2.3 视图绘制GDI 双缓冲防闪烁与格子状态映射MineSweeperView.cpp的OnDraw()是性能关键路径。直接CDC::TextOut()或Rectangle()会引发严重闪烁项目采用双缓冲技术void CMineSweeperView::OnDraw(CDC* pDC) { CMineSweeperDoc* pDoc GetDocument(); ASSERT_VALID(pDoc); if (!pDoc) return; // 创建内存DC和兼容位图 CDC memDC; CBitmap bitmap; CRect rect; GetClientRect(rect); memDC.CreateCompatibleDC(pDC); bitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()); CBitmap* pOldBitmap memDC.SelectObject(bitmap); // 填充背景浅灰色 memDC.FillSolidRect(rect, RGB(240, 240, 240)); // 绘制所有格子 const int CELL_SIZE 20; for (int row 0; row pDoc-m_nRows; row) { for (int col 0; col pDoc-m_nCols; col) { CRect cellRect(col * CELL_SIZE, row * CELL_SIZE, (col 1) * CELL_SIZE, (row 1) * CELL_SIZE); DrawCell(memDC, cellRect, row, col, pDoc); } } // 一次性拷贝到屏幕 pDC-BitBlt(0, 0, rect.Width(), rect.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBitmap); }DrawCell()根据m_pRevealed[row*colscol]和m_pBoard[]组合判断状态revealed0, board0→ 未翻开灰格DrawFrameControl(..., DFC_BUTTON, DFCS_BUTTONPUSH)revealed1, board0, numbers0→ 数字格TextOut() 字体加粗revealed1, board1→ 爆炸图标LoadIcon()加载IDI_MINErevealed0, board2→ 小旗图标LoadIcon()加载IDI_FLAG3. 交互逻辑闭环鼠标消息处理、游戏状态机与对话框通信3.1 鼠标事件链从OnLButtonDown到RevealCell的完整调用栈MFC 视图的鼠标响应不是直连 Win32WM_LBUTTONDOWN而是经由框架路由。MineSweeperView.cpp中void CMineSweeperView::OnLButtonDown(UINT nFlags, CPoint point) { CClientDC dc(this); CRect rect; GetClientRect(rect); // 计算点击的格子坐标整除 CELL_SIZE int col point.x / 20; int row point.y / 20; // 边界检查 if (row 0 || row GetDocument()-m_nRows || col 0 || col GetDocument()-m_nCols) { CView::OnLButtonDown(nFlags, point); return; } // 关键触发文档层逻辑而非视图层直接修改 GetDocument()-RevealCell(row, col); // ← 状态变更入口 // 强制重绘触发 OnDraw Invalidate(); UpdateWindow(); CView::OnLButtonDown(nFlags, point); }CMineSweeperDoc::RevealCell()实现递归展开void CMineSweeperDoc::RevealCell(int row, int col) { if (!m_bGameStarted) { StartGame(row, col); // 首次点击触发布雷 } int idx row * m_nCols col; if (m_pRevealed[idx]) return; // 已翻开忽略 if (m_pBoard[idx] 2) return; // 已标记为旗不翻开 m_pRevealed[idx] 1; // 标记为已翻开 // 踩雷判定 if (m_pBoard[idx] 1) { m_bGameOver TRUE; return; } // 若为数字格且为0则递归展开邻域 if (m_pNumbers[idx] 0) { for (int dy -1; dy 1; dy) { for (int dx -1; dx 1; dx) { int nr row dy, nc col dx; if (nr 0 nr m_nRows nc 0 nc m_nCols) { int nidx nr * m_nCols nc; if (!m_pRevealed[nidx] m_pBoard[nidx] ! 2) { RevealCell(nr, nc); // 递归调用 } } } } } }注意递归深度受雷区尺寸限制最大9×981无需担心栈溢出。但若未来扩展至50×50应改用显式栈std::stack替代递归。3.2 对话框驱动参数SetDlg如何安全修改文档数据SetDlg.cpp不直接操作视图而是通过GetDocument()获取文档指针并调用其SetDifficulty()void CSetDlg::OnOK() { UpdateData(TRUE); // 从控件读取值到成员变量 CMineSweeperDoc* pDoc (CMineSweeperDoc*)GetDocument(); if (pDoc) { pDoc-m_nRows m_nRows; // 成员变量来自控件绑定 pDoc-m_nCols m_nCols; pDoc-m_nMines m_nMines; pDoc-OnNewDocument(); // 重置雷区 pDoc-UpdateAllViews(NULL); // 通知所有视图刷新 } CDialog::OnOK(); }UpdateAllViews(NULL)是 MFC 文档/视图通信的核心机制——它遍历文档关联的所有视图调用其OnUpdate()方法本项目中未重写默认触发Invalidate()。这种松耦合设计使添加新视图如雷区统计面板无需修改现有代码。3.3 游戏状态机m_bGameOver如何影响 UI 响应与重绘逻辑状态机隐含在OnDraw()和鼠标处理中。CMineSweeperView::OnDraw()在末尾加入胜负判定渲染// ... 绘制完所有格子后 if (pDoc-m_bGameOver) { // 绘制覆盖层半透明黑色 CBrush brush(RGB(0,0,0)); brush.CreateSolidBrush(RGB(0,0,0)); CRect overlay(0,0,rect.Width(),rect.Height()); pDC-FillRect(overlay, brush); // 绘制文字 CString str pDoc-IsWin() ? _T(You Win!) : _T(Game Over); pDC-SetTextColor(RGB(255,255,255)); pDC-SetTextAlign(TA_CENTER | TA_BASELINE); pDC-TextOut(rect.Width()/2, rect.Height()/2, str); }其中IsWin()判断逻辑BOOL CMineSweeperDoc::IsWin() { if (!m_bGameStarted || m_bGameOver) return FALSE; int nSize m_nRows * m_nCols; int revealed 0; for (int i 0; i nSize; i) { if (m_pRevealed[i] m_pBoard[i] ! 1) revealed; } return revealed nSize - m_nMines; // 所有非雷格均被翻开 }4. 编译与调试实战VS2010 环境下修复常见构建错误4.1 解决 “无法打开包括文件: ‘afxwin.h’” —— MFC 库路径配置VS2010 默认不启用 MFC 支持。需手动开启右键项目 →属性→常规→使用 MFC→ 选择“在共享 DLL 中使用 MFC”配置属性→常规→字符集→ 设为“使用 Unicode 字符集”与resource.h中#define UNICODE一致配置属性→C/C→常规→附加包含目录→ 添加$(VCInstallDir)atlmfc\include;$(VCInstallDir)include;若仍报错检查注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\10.0\Setup\VC下ProductDir路径是否指向正确 VC 安装目录。4.2 处理 “LNK2001: 无法解析的外部符号” —— MFC 库链接修正典型错误如LNK2001: unresolved external symbol public: virtual void __thiscall CWinApp::InitInstance(void)源于链接器未包含 MFC 库。在项目属性中配置属性→链接器→输入→附加依赖项→ 添加mfcs100u.lib;msvcr100.dll;配置属性→链接器→常规→附加库目录→ 添加$(VCInstallDir)atlmfc\lib\amd64;$(VCInstallDir)lib\amd64;x86 平台则用$(VCInstallDir)atlmfc\lib提示.dsw/.dsp文件是 VS6 时代格式VS2010 会自动升级为.sln/.vcproj。若升级失败可删除旧文件新建空 MFC 项目再将*.cpp/*.h/*.rc文件拖入。4.3 调试技巧在RevealCell中设置条件断点定位逻辑错误当发现“点击空白格未展开”时在CMineSweeperDoc::RevealCell()开头设断点观察关键变量变量名期望值首次点击非雷格实际值异常含义m_bGameStartedTRUEStartGame已调用为FALSE→StartGame未执行检查OnLButtonDown是否被拦截m_pNumbers[idx]0确认是空白格0→ 非空白格不应递归展开m_pRevealed[nidx]0邻域格未翻开1→ 已翻开跳过合理在调试器中执行? m_nRows、? m_nCols快速验证尺寸是否被SetDlg正确修改。5. 进阶改造为现代 Windows 添加 DPI 感知与高分屏适配5.1 启用 DPI 感知避免 Win10/11 高分屏下界面模糊MFC 默认非 DPI 感知导致 200% 缩放时按钮挤压、文字糊化。需在Mine Sweeper.rc的VERSIONINFO段后添加// 在 .rc 文件末尾追加 1 24 WINEDPIAWARE并在MineSweeper.cpp的InitInstance()中AfxEnableControlContainer()后插入// 启用每监视器 DPI 感知Win10 1703 if (IsWindows10OrGreater()) { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); } else { SetProcessDPIAware(); }5.2 动态格子尺寸根据 DPI 缩放因子调整CELL_SIZE在CMineSweeperView::OnDraw()中获取当前 DPIvoid CMineSweeperView::OnDraw(CDC* pDC) { // 获取缩放因子 int dpiX, dpiY; pDC-GetDeviceCaps(LOGPIXELSX, dpiX); pDC-GetDeviceCaps(LOGPIXELSY, dpiY); double scale dpiX / 96.0; // 以 96 DPI 为基准 const int BASE_CELL_SIZE 20; const int CELL_SIZE (int)(BASE_CELL_SIZE * scale); // 后续绘制逻辑中所有 20 替换为 CELL_SIZE // ... }5.3 高分屏图标替换用 PNG 替代 ICO 实现无损缩放原项目res\*.ico在 200% 缩放时锯齿严重。可生成多尺寸 PNGflag_16.png,flag_32.png,flag_64.png在CMineSweeperView::DrawCell()中根据scale选择CString iconPath; if (scale 2.0) iconPath _T(res\\flag_64.png); else if (scale 1.5) iconPath _T(res\\flag_32.png); else iconPath _T(res\\flag_16.png); CImage img; img.Load(iconPath); img.Draw(memDC.m_hDC, cellRect.left, cellRect.top, cellRect.Width(), cellRect.Height());注意CImage需在stdafx.h中#include atlimage.h并链接atls.lib。此方案比 ICO 更灵活且 PNG 支持 Alpha 通道可实现阴影、渐变等现代 UI 效果。修改resource.h中图标 ID 定义确保新资源 ID 与代码中LoadImage()调用一致。本文还有配套的精品资源点击获取