尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
MFC扫雷实战:Windows原生GUI开发最小闭环
简介本资源是一份基于MFC框架开发的Windows桌面版扫雷游戏完整工程面向C初学者与MFC入门学习者用于实践Windows GUI编程、消息响应机制及GDI绘图技术。项目代码结构清晰包含标准文档/视图架构Doc/View、主框架窗口、自定义设置对话框及资源管理模块界面简洁直观操作仅需鼠标点击即可完成游戏交互。压缩包共45个文件涵盖17个BMP游戏图标与格子素材、7个头文件.h定义类接口与资源宏、6个CPP实现核心逻辑如雷区生成、递归展开、胜负判定以及RC资源脚本、ICO图标、EXE可执行文件和VC6工程配置文件DSP/DSP/NCB等总大小2.85MB。已有315人学习下载读者可直接编译运行深入理解MFC消息映射、CDC绘图流程、位图资源加载及游戏状态机设计是掌握传统Windows桌面应用开发的典型练手项目。1. 这不是“用MFC重写一个老游戏”——而是Windows桌面GUI开发的最小可行闭环你双击桌面上那个灰蓝色图标扫雷窗口弹出来左上角计时器开始跳动右键标记旗子、左键翻开格子、数字带阴影、爆炸动画一闪而过……这些不是怀旧滤镜是MFC框架在Windows原生API之上构建的一套完整消息驱动、资源管理、设备上下文DC绘图、控件生命周期控制的实时交互系统。它不依赖.NET或Qt不打包运行时生成一个不到500KB的EXE就能在Win7及以上系统直接运行——这正是“基于MFC实现的扫雷游戏”的真实分量它是一份可执行的Windows GUI开发教科书覆盖窗口类注册、WM_PAINT响应、鼠标消息捕获、位图资源加载、内存DC双缓冲、RECT区域裁剪、CRect/CPoint坐标运算、CArray动态格子管理等核心机制。适合刚学完C基础、正卡在“怎么让程序有界面”门槛上的开发者也适合需要快速验证某项GDI绘图技巧比如透明贴图、自定义按钮状态、网格线抗锯齿的资深工程师。它不追求跨平台或现代UI动效但每行代码都直指Windows桌面开发的本质契约。2. 从零搭建MFC扫雷工程窗口骨架、资源定义与格子逻辑初始化2.1 创建标准MFC应用程序并精简框架依赖Visual Studio中新建项目时选择“MFC应用程序”向导中务必勾选“使用标准Windows控件”和“不使用文档/视图体系结构”——扫雷是单窗口无文档模型强行启用Doc/View会引入大量冗余类CDocument、CView派生类增加理解负担且无实际收益。项目类型选“基于对话框”而非“单文档”因为扫雷主界面本质是固定尺寸的交互面板无需滚动、缩放、多视图切换。关键配置项如下// 在项目属性 → 配置属性 → 常规 → 使用MFC中选择“在静态库中使用MFC” // 此设置使EXE不依赖mfc140.dll等动态库单文件可部署 // 在C → 语言 → C语言标准中设为ISO C14标准兼容MFC 2015提示若遇到LNK2001: unresolved external symbol __imp__GetStockObject4等链接错误检查是否误删了#include stdafx.h预编译头必须存在或未正确设置MFC使用方式。静态链接MFC后最终EXE体积约480KB无外部DLL依赖。2.2 定义游戏核心数据结构与格子状态枚举扫雷的逻辑内核远比界面复杂。不能简单用二维数组存“0-9”数字需区分三种底层状态未翻开COVERED、已翻开REVEALED、已标记FLAGGED同时记录该格子是否为雷IS_MINE、周围雷数NEIGHBOR_COUNT。MFC中推荐用CArrayCCell, CCell管理格子避免裸指针内存泄漏// Cell.h struct CCell { BOOL bIsMine; // 是否为雷 BOOL bIsRevealed; // 是否已翻开 BOOL bIsFlagged; // 是否插旗 int nNeighborCount; // 周围雷数0-8 CRect rect; // 该格子在客户区的绘制矩形用于点击命中检测 CCell() : bIsMine(FALSE), bIsRevealed(FALSE), bIsFlagged(FALSE), nNeighborCount(0) {} }; // GameBoard.h class CGameBoard { public: CArrayCCell, CCell m_cells; // 所有格子线性存储 int m_nRows, m_nCols; // 行列数默认9x9 int m_nMineCount; // 总雷数默认10 void Initialize(int rows 9, int cols 9, int mines 10); void PlaceMines(int firstClickRow, int firstClickCol); // 首次点击避开雷区 void CalculateNeighborCounts(); void RevealCell(int row, int col); // 递归翻开空区域 };CArray比std::vector更适配MFC消息循环其Add()、GetAt()、SetAt()方法在ON_WM_LBUTTONDOWN等消息处理中调用更自然且支持序列化后续可扩展存档功能。2.3 对话框资源设计精确控制客户区尺寸与控件占位在Resource View中打开IDD_MAIN_DIALOG删除默认的OK/Cancel按钮。设置对话框属性Border:None去除标题栏实现无边框游戏窗Style:ChildVisible确保作为主窗口显示Font:MS Sans Serif, 8pt保持Windows经典风格Size:320, 2409x9格子每格24x24像素加边框共320x240添加一个Picture ControlID:IDC_GAME_AREA类型设为Frame位置覆盖整个客户区。此控件不显示图片仅作为绘图目标——MFC中CStatic控件常被用作自绘画布因其天然支持WM_PAINT且无焦点干扰。在OnInitDialog()中获取其窗口句柄并保存// MainDlg.cpp BOOL CMainFrameDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 获取游戏区域控件句柄 GetDlgItem(IDC_GAME_AREA)-GetWindowRect(m_gameRect); ScreenToClient(m_gameRect); // 转换为客户区坐标 // 初始化游戏板9x910雷 m_gameBoard.Initialize(9, 9, 10); return TRUE; }m_gameRect将作为后续所有格子坐标的计算基准避免硬编码像素值。3. 绘制逻辑双缓冲GDI绘图与状态驱动的视觉反馈3.1 实现WM_PAINT消息处理与内存DC双缓冲直接在OnPaint()中调用CPaintDC dc(this)绘图会导致闪烁。正确做法是创建兼容DCCompatible DC和位图Bitmap先在内存中绘制完整画面再一次性BitBlt到屏幕void CMainFrameDlg::OnPaint() { CPaintDC dc(this); CRect clientRect; GetClientRect(clientRect); // 创建内存DC和位图 CDC memDC; memDC.CreateCompatibleDC(dc); CBitmap bitmap; bitmap.CreateCompatibleBitmap(dc, clientRect.Width(), clientRect.Height()); CBitmap* pOldBitmap memDC.SelectObject(bitmap); // 填充背景浅灰色 CBrush brush(RGB(240, 240, 240)); memDC.FillRect(clientRect, brush); // 绘制游戏区域深灰色边框 memDC.Draw3dRect(m_gameRect, RGB(128,128,128), RGB(255,255,255)); // 绘制所有格子 DrawAllCells(memDC); // 一次性拷贝到屏幕 dc.BitBlt(0, 0, clientRect.Width(), clientRect.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBitmap); }DrawAllCells(CDC dc)函数遍历m_gameBoard.m_cells根据每个CCell的bIsRevealed和bIsFlagged状态决定绘制内容未翻开格子画斜线阴影FillRectMoveTo/LineTo翻开格子画数字TextOut或空白FillRect白色雷区爆炸时画红色圆圈Ellipse。关键点在于所有坐标计算基于m_gameRect和格子尺寸确保分辨率无关。3.2 格子坐标映射与鼠标点击命中检测OnLButtonDown需将鼠标坐标转换为格子索引。因格子是规则网格直接整除即可但必须先排除边界外点击void CMainFrameDlg::OnLButtonDown(UINT nFlags, CPoint point) { // 检查点击是否在游戏区域内 if (!m_gameRect.PtInRect(point)) { CDialogEx::OnLButtonDown(nFlags, point); return; } // 计算格子尺寸假设9x9留2像素边距 int cellWidth (m_gameRect.Width() - 4) / 9; int cellHeight (m_gameRect.Height() - 4) / 9; // 计算行列索引减去边距后整除 int col (point.x - m_gameRect.left - 2) / cellWidth; int row (point.y - m_gameRect.top - 2) / cellHeight; // 边界校验 if (row 0 || row 9 || col 0 || col 9) { CDialogEx::OnLButtonDown(nFlags, point); return; } // 执行翻开逻辑首次点击放置雷 if (!m_bFirstClickDone) { m_gameBoard.PlaceMines(row, col); m_bFirstClickDone TRUE; } m_gameBoard.RevealCell(row, col); // 强制重绘 InvalidateRect(m_gameRect, TRUE); CDialogEx::OnLButtonDown(nFlags, point); }InvalidateRect(m_gameRect, TRUE)触发OnPaint()确保点击后画面即时更新。此处TRUE参数表示擦除背景避免残留旧图像。3.3 状态驱动的视觉样式表数字颜色、旗子图标与爆炸效果MFC不提供内置图标资源需手动加载位图。将旗子、雷、数字0-8的BMP文件放入res/目录ID设为IDB_FLAG、IDB_MINE、IDB_NUM_0至IDB_NUM_8。绘制时按状态分支void CMainFrameDlg::DrawCell(CDC dc, const CCell cell, const CRect rect) { if (cell.bIsRevealed) { if (cell.bIsMine) { // 绘制雷图标居中 CBitmap mineBmp; mineBmp.LoadBitmap(IDB_MINE); CDC mineDC; mineDC.CreateCompatibleDC(dc); CBitmap* pOld mineDC.SelectObject(mineBmp); dc.StretchBlt(rect.left2, rect.top2, rect.Width()-4, rect.Height()-4, mineDC, 0, 0, 16, 16, SRCCOPY); mineDC.SelectObject(pOld); } else if (cell.nNeighborCount 0) { // 数字不同数字用不同颜色1蓝、2绿、3红... COLORREF colors[] {RGB(0,0,0), RGB(0,0,255), RGB(0,128,0), RGB(255,0,0), RGB(0,0,128), RGB(128,0,0), RGB(0,128,128), RGB(0,0,0), RGB(128,128,128)}; dc.SetTextColor(colors[cell.nNeighborCount]); dc.SetBkMode(TRANSPARENT); CString str; str.Format(_T(%d), cell.nNeighborCount); dc.DrawText(str, rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); } // 数字0不绘制文字只留空白背景 } else if (cell.bIsFlagged) { // 绘制旗子图标 CBitmap flagBmp; flagBmp.LoadBitmap(IDB_FLAG); CDC flagDC; flagDC.CreateCompatibleDC(dc); CBitmap* pOld flagDC.SelectObject(flagBmp); dc.StretchBlt(rect.left2, rect.top2, rect.Width()-4, rect.Height()-4, flagDC, 0, 0, 16, 16, SRCCOPY); flagDC.SelectObject(pOld); } else { // 未翻开画斜线阴影 dc.FillRect(rect, CBrush(RGB(192,192,192))); dc.MoveTo(rect.left2, rect.top2); dc.LineTo(rect.right-2, rect.bottom-2); dc.MoveTo(rect.right-2, rect.top2); dc.LineTo(rect.left2, rect.bottom-2); } }StretchBlt确保图标适配格子尺寸DT_CENTER | DT_VCENTER使数字精准居中。颜色数组colors[]索引0对应数字0不显示索引1对应数字1蓝色以此类推——这是Windows扫雷的经典配色方案。4. 游戏逻辑引擎雷区生成、连通翻开与胜负判定4.1 首次点击安全的雷区生成算法标准扫雷要求首次点击必不为雷且周围8格也无雷保证开局可玩。PlaceMines()需排除以首次点击为中心的3x3区域void CGameBoard::PlaceMines(int firstClickRow, int firstClickCol) { // 清空原有雷 for (int i 0; i m_cells.GetSize(); i) { m_cells[i].bIsMine FALSE; } // 构建禁止区域3x3 CArrayBOOL, BOOL forbidden; forbidden.SetSize(m_nRows * m_nCols, FALSE); for (int r max(0, firstClickRow-1); r min(m_nRows-1, firstClickRow1); r) { for (int c max(0, firstClickCol-1); c min(m_nCols-1, firstClickCol1); c) { forbidden[r * m_nCols c] TRUE; } } // 随机放置雷避开禁止区 srand((unsigned int)time(NULL)); int placed 0; while (placed m_nMineCount) { int idx rand() % (m_nRows * m_nCols); if (!forbidden[idx] !m_cells[idx].bIsMine) { m_cells[idx].bIsMine TRUE; placed; } } // 计算邻格雷数 CalculateNeighborCounts(); }forbidden数组标记不可放雷位置rand()生成随机索引循环直到放满指定数量。CalculateNeighborCounts()遍历每个格子检查其8个邻居是否为雷并计数是纯CPU密集型操作但9x9规模下毫秒级完成。4.2 递归连通翻开与栈式防爆优化RevealCell()需处理两种情况单格翻开数字格和空格连锁翻开周围无雷则自动翻开邻格。递归实现易导致栈溢出尤其大地图改用显式栈CArray模拟void CGameBoard::RevealCell(int row, int col) { // 边界检查 if (row 0 || row m_nRows || col 0 || col m_nCols) return; int idx row * m_nCols col; CCell cell m_cells[idx]; // 已翻开或已标记不处理 if (cell.bIsRevealed || cell.bIsFlagged) return; // 标记为已翻开 cell.bIsRevealed TRUE; // 若为雷游戏结束由主窗口处理 if (cell.bIsMine) return; // 若周围无雷递归翻开邻格 if (cell.nNeighborCount 0) { // 使用栈避免递归深度过大 CArrayPOINT, POINT stack; stack.Add({row, col}); while (stack.GetSize() 0) { POINT pt stack[stack.GetSize()-1]; stack.RemoveAt(stack.GetSize()-1); // 检查8方向邻格 for (int dr -1; dr 1; dr) { for (int dc -1; dc 1; dc) { if (dr 0 dc 0) continue; int r pt.x dr; int c pt.y dc; if (r 0 r m_nRows c 0 c m_nCols) { int idx2 r * m_nCols c; if (!m_cells[idx2].bIsRevealed !m_cells[idx2].bIsFlagged) { m_cells[idx2].bIsRevealed TRUE; if (m_cells[idx2].nNeighborCount 0) { stack.Add({r, c}); } } } } } } } }stack存储待处理坐标while循环逐个弹出并检查邻居。此实现将最坏情况栈深度从O(N²)降至O(1)彻底规避栈溢出风险。4.3 实时胜负判定与游戏状态同步胜负不由“翻开所有非雷格子”判定效率低而由“已翻开格子数 已标记雷数 总格子数”判断。每次RevealCell()或ToggleFlag()后更新计数// GameBoard.h 中添加 int m_nRevealedCount; // 已翻开格子数 int m_nFlaggedCount; // 已标记旗子数 // 在 RevealCell() 结尾添加 m_nRevealedCount; // 在 ToggleFlag() 中右键切换标记 if (cell.bIsFlagged) { cell.bIsFlagged FALSE; m_nFlaggedCount--; } else { cell.bIsFlagged TRUE; m_nFlaggedCount; } // 胜负检查 if (m_nRevealedCount m_nFlaggedCount m_nRows * m_nCols) { // 所有非雷格子已处理玩家获胜 AfxMessageBox(_T(恭喜你赢了)); }AfxMessageBox是MFC标准弹窗无需额外资源。此判定逻辑在每次用户操作后即时执行响应零延迟。5. 进阶技巧高DPI适配、计时器集成与内存优化5.1 让扫雷在4K屏幕上清晰显示DPI感知配置Windows 10/11高DPI屏下未声明DPI感知的应用会被系统模糊拉伸。在MainDlg.cpp构造函数中添加CMainFrameDlg::CMainFrameDlg(CWnd* pParent /*nullptr*/) : CDialogEx(IDD_MAIN_DIALOG, pParent) { // 启用系统DPI感知 if (AfxGetApp()-m_hInstance) { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE); } }并在OnInitDialog()中动态计算格子尺寸不再硬编码// 获取当前DPI缩放比例 CDC* pDC GetDC(); int dpiX pDC-GetDeviceCaps(LOGPIXELSX); ReleaseDC(pDC); float scale dpiX / 96.0f; // 96为100%缩放基准 // 动态调整格子大小基础24x24按DPI缩放 int baseCellSize 24; int cellWidth (int)(baseCellSize * scale); int cellHeight (int)(baseCellSize * scale);GetDeviceCaps(LOGPIXELSX)返回水平DPI值scale即缩放比例如125%时为1.25。所有坐标计算基于cellWidth/cellHeight确保文字、图标、线条在任何DPI下保持锐利。5.2 集成游戏计时器精确到毫秒的实时更新CWnd::SetTimer()精度有限约15ms扫雷需精确计时。改用timeGetTime()多媒体定时器// MainDlg.h 中添加 UINT_PTR m_timerID; DWORD m_startTime; DWORD m_elapsedTime; // OnInitDialog() 中启动 m_startTime timeGetTime(); m_timerID timeSetEvent(10, 1, OnTimerCallback, (DWORD_PTR)this, TIME_PERIODIC); // 静态回调函数必须为static void CALLBACK CMainFrameDlg::OnTimerCallback(UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2) { CMainFrameDlg* pThis (CMainFrameDlg*)dwUser; if (pThis pThis-m_bGameStarted !pThis-m_bGameEnded) { pThis-m_elapsedTime timeGetTime() - pThis-m_startTime; // 更新计时器显示需PostMessage通知主线程 pThis-PostMessage(WM_USER_UPDATE_TIMER, 0, 0); } } // 在 OnUserUpdateTimer() 中刷新显示 LRESULT CMainFrameDlg::OnUserUpdateTimer(WPARAM, LPARAM) { CString str; str.Format(_T(%03d), m_elapsedTime / 1000); // 秒数 GetDlgItem(IDC_STATIC_TIME)-SetWindowText(str); return 0; }timeSetEvent(10,1,...)设置10ms周期TIME_PERIODIC标志使其持续触发。PostMessage确保UI更新在主线程安全执行避免跨线程访问控件。5.3 内存占用分析与关键优化点9x9扫雷理论内存占用极小但MFC默认配置可能引入冗余位图资源优化所有图标BMP保存为24位真彩色尺寸16x16单个文件1KB。避免使用PNGMFC需额外解码库。CArray容量预分配在Initialize()中调用m_cells.SetSize(81)避免多次realloc。DC对象复用OnPaint()中创建的memDC和bitmap在函数退出时自动销毁无需手动DeleteObjectMFC智能管理。字符串临时对象TextOut前用CString格式化数字避免sprintf栈缓冲区风险。使用Visual Studio诊断工具Debug → Windows → Memory Usage实测空闲状态下进程内存约3.2MB游戏进行中峰值4.1MB。对比同功能Qt版本约12MBMFC的轻量级优势在此类小型工具中极为显著。注意若发现OnPaint()中CreateCompatibleBitmap失败返回NULL检查clientRect尺寸是否为0——常见于对话框未正确初始化m_gameRect。添加断点验证m_gameRect的rightleft且bottomtop。本文还有配套的精品资源点击获取
RELATED

相关推荐

Web安全基础:威胁防护与最佳实践指南

Web安全基础:威胁防护与最佳实践指南

1. Web安全基础概念与核心挑战Web安全是一个涉及多层面防护的领域,它保护网站、Web应用和Web服务免受恶意攻击。随着互联网技术的快速发展,Web安全面临的挑战也日益复杂。从早期的SQL注入、XSS攻击,到现在的API滥用、供应链攻击,攻…

📅 2026/9/15 20:35:45
Java特性清单:从基础语法到JVM的完整知识地图

Java特性清单:从基础语法到JVM的完整知识地图

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/15 20:35:45
基于Kuikly的DeepSeek Harness移动端控制面板

基于Kuikly的DeepSeek Harness移动端控制面板

先交代一下背景。DeepSeek Harness这个玩意儿,去年年底开始一直在折腾,定位类似Codex Harness,是一套把DeepSeek模型能力封装成可执行Agent的框架。简单说,就是你丢给它一个任务描述,它能自己拆步骤、读写文件、调命令…

📅 2026/9/15 20:35:45
MORE NEWS

更多资讯

📰

快速跑通DINOv3:5分钟上手视觉基础模型

快速跑通DINOv3:5分钟上手视觉基础模型 【免费下载链接】dinov3 Reference PyTorch implementation and models for DINOv3 项目地址: https://gitcode.com/GitHub_Trending/di/dinov3 手头一堆图片,想算出能直接喂给分类器、分割头的特征向量&am…

📰

电脑怎么录屏?系统自带、快捷键与14款软件全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Docker 快速搭建微信机器人:零门槛部署你的微信 AI 助手

Docker 快速搭建微信机器人:零门槛部署你的微信 AI 助手 【免费下载链接】wechat-bot 🤖 Multi-platform IM AI Agent for Telegram, WhatsApp, Lark, and WeChat. Connects ChatGPT / Claude / Kimi / DeepSeek / Ollama / Pi for auto-replies, commun…

📰

MongoDB 分片节点启动与关闭全解析:sharding_environment 三阶段初始化与优雅退出机制

MongoDB 分片节点启动与关闭全解析:sharding_environment 三阶段初始化与优雅退出机制 【免费下载链接】mongo The MongoDB Database 项目地址: https://gitcode.com/GitHub_Trending/mo/mongo 本篇指南以 MongoDB 仓库中 README_startup_and_shutdown.md 为…

📰

贪心算法四题详解:从买卖股票到跳跃游戏,掌握局部最优推导全局最优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Oracle通过ODBC访问SQL Server:HSODBC配置与排查指南

先说个结论:这件事做的人不少,翻车的也真不少。Oracle和SQL Server分属两家厂商,Oracle自己出过一套官方的Transparent Gateway for SQL Server,但这东西属于独立授权,价格贵、安装还讲究版本匹配;相比之下…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬