恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

MFC扫雷源码详解:消息映射、GDI双缓冲与经典算法实战

  • 首页
  • 资讯中心
  • /
  • MFC扫雷源码详解:消息映射、GDI双缓冲与经典算法实战

相关资讯

AI Toolbox Image工作台评测:本地化AI图片生成渠道管理的完整指南 2026/10/11 0:06:39
iPhone 终于能装扩展了!Reynard Browser 的 Firefox 插件安装与使用教程 2026/10/11 0:06:39
zotero-AI-Butler快速上手:5分钟完成安装、API配置与首篇论文总结 2026/10/11 0:06:39

最新资讯

TensorRT部署YOLO实例分割与目标检测:从PyTorch到C++/Python跨平台实战
Sqoop处理BLOB/CLOB实战:导入导出、性能调优与踩坑指南
飞机检测数据集实战:VOC转YOLO格式与训练避坑指南
冷库叉车和电池跟常温仓有啥不一样?使用和充电怎么管
深圳货车限行,冷链城配怎么排路线和时间才能不迟到?
OpenAPI 规范 JSON Schema 归档全解析:从 Swagger 1.2 到 OAS 3.0 的验证体系

今日推荐

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

MFC扫雷源码详解:消息映射、GDI双缓冲与经典算法实战

发布时间:2026/10/11 0:11:39
MFC扫雷源码详解:消息映射、GDI双缓冲与经典算法实战 简介一份基于MFC开发的扫雷游戏完整源码包面向C初学者与Windows界面编程爱好者旨在展示如何使用MFC框架构建经典小游戏的完整流程。包内共71个文件涵盖bmp位图资源用于雷区、笑脸等图标、cpp/h源码实现对话框、自定义按钮及游戏逻辑、dll动态库与可直接运行的exe压缩包大小约2.7MB结构清晰便于按模块查阅。代码包含CMyDialog、CMyButton等关键类通过消息映射机制处理点击事件实现雷区随机生成、数字计算、计时统计与胜负判断并配套工程文件、调试信息和说明文档。已有475人学习下载适合用于理解MFC的消息循环、控件封装和游戏状态管理也可作为课程设计或入门实战项目的参考模板。1. MFC扫雷这份源码到底值不值得你花时间啃“MFC扫雷”这个题目在高校课程设计和入职笔试里出现的频率高得惊人。一份含源码的MFC扫雷程序表面是个游戏实际把Windows桌面开发最核心的三件事全练到了消息机制怎么驱动界面、GDI怎么把数据画出来、程序状态怎么维护不崩。如果你正对着源码不知道从哪看起或者自己动手写的时候一布雷就炸、一重绘就闪这篇文章就是按我做这类小工程的复盘顺序来的。适合三类人要交MFC课设的学生、刚接手老MFC维护项目的程序员、以及想通过小游戏把Windows消息映射彻底搞明白的桌面开发者。2. 拆解MFC扫雷工程消息映射、类的分工先把骨架看明白2.1 为什么扫雷适合当MFC练手消息、绘制、定时器一次全碰到有人问我桌面软件开发用MFC还是Qt我的回答一直是新项目选Qt没毛病但存量市场和课设题目里MFC依然活跃。MFC这套东西学了不亏因为你要学的不是那几个封装好的类而是它底下的Windows消息机制。扫雷这个小游戏恰好把MFC里最常见的几样东西全占了窗口类、消息映射宏、GDI绘图、定时器、状态栏。贪吃蛇也能练MFC但它只需要定时器和方向键没法体现鼠标右键的逻辑计算器能练对话框但绘制太简单。扫雷的输入是左键翻开、右键插旗输出是格子颜色和数字中间夹着递归展开、布雷、计时三块逻辑复杂度恰到好处。这也是为什么这么多年课设题目里它一直没被替换掉。网上MFC教程不算少但很多教程讲完消息映射就停在“弹个对话框”的层次真正能落地成完整游戏的源码反而稀缺。拿到一份工程第一件事不是按F5编译而是先看它的文件结构和类划分这决定了你后面三天是顺利还是反复翻车。2.2 工程该建哪几个类CWinApp、CFrameWnd与逻辑类的边界一个结构清晰的MFC扫雷工程至少拆成三个类而且每个类的职责必须单一。我见过最糟糕的写法是所有代码塞在视图类的OnPaint和OnLButtonDown里布雷、递归、绘制全揉在一起改一个数字颜色要找半天。这种代码我一般直接建议重写。常见的做法是这样的类基类职责CScanMineAppCWinApp程序入口创建主窗口CMainFrameCFrameWnd窗口生命周期、消息分发、GDI绘制CMineGame普通C类棋盘数据、布雷、翻开逻辑、胜负判断CScanMineApp是全局唯一的应用对象负责在InitInstance里new出主窗口。CMainFrame是窗口的化身所有鼠标消息、绘制消息最终都落到它头上。CMineGame是纯粹的算法类里面不出现任何HWND、CDC它只管二维数组和状态计算。为什么要单独拆一个CMineGame出来因为逻辑和界面耦合在一起后你没法单独测试算法。布雷对不对、递归展开会不会死循环、胜利判定边界有没有漏洞这些问题在没有窗口的情况下用命令行就能验证。等逻辑类稳定了再做界面接入出问题时能立刻定位到是算法错还是绘制错不用对着黑匣子猜。2.3 最小可跑的MFC窗口骨架从InitInstance到消息映射宏扫雷界面的起点是一个能创建窗口、能接收鼠标消息的框架。基于单文档还是基于对话框都能做扫雷我习惯用CFrameWnd直接派生因为它比对话框少了模板生成的怪代码结构更直白。最小骨架如下// ScanMineApp.h class CScanMineApp : public CWinApp { public: virtual BOOL InitInstance(); }; // ScanMineApp.cpp BEGIN_MESSAGE_MAP(CScanMineApp, CWinApp) END_MESSAGE_MAP() BOOL CScanMineApp::InitInstance() { // 创建主窗口第二个参数是窗口标题 CMainFrame* pFrame new CMainFrame; pFrame-Create(NULL, _T(MFC扫雷)); m_pMainWnd pFrame; pFrame-ShowWindow(SW_SHOW); // 显示窗口 return TRUE; // 返回TRUE才会进入消息循环 }这段代码里pFrame-Create(NULL, ...)的第一个参数是窗口类名传NULL表示使用MFC默认注册的窗口类m_pMainWnd是CWinApp的成员指向主窗口框架退出时会用它做清理。注意InitInstance返回FALSE会导致程序直接退出这是个常见的低级错误点。窗口类本身长这样重点是消息映射宏// MainFrame.h class CMainFrame : public CFrameWnd { public: CMainFrame(); protected: afx_msg void OnPaint(); // 绘制 afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnRButtonDown(UINT nFlags, CPoint point); DECLARE_MESSAGE_MAP() }; // MainFrame.cpp BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_WM_PAINT() ON_WM_LBUTTONDOWN() ON_WM_RBUTTONDOWN() END_MESSAGE_MAP()afx_msg其实是个空宏只是告诉读代码的人“这是消息处理函数”。真正起作用的是BEGIN_MESSAGE_MAP和ON_WM_XXX之间的配对它把Windows消息ID比如WM_LBUTTONDOWN绑定到对应的成员函数。MFC收到鼠标点击后会查这张表找到OnLButtonDown去调用。消息映射是MFC的命根子也是扫雷程序里最值得反复看的机制。你在Win32时代要自己写switch (message)分支MFC用宏表替你做了这件事。后面的绘制、鼠标左右键处理全都挂在这张表上。先把这个骨架跑通再往里面加棋盘等于地基已经打牢。3. 布雷与翻开算法第一次点击不炸、递归展开不崩的核心实现3.1 用二维数组还是vector存棋盘数据结构怎么定扫雷棋盘不管是9x9初级还是16x30高级本质都是二维网格。存法上有两种流派固定大小的二维数组bool board[9][9]或者动态的一维std::vectorCell手动换算下标。我推荐后者因为扫雷通常有初级、中级、高级三档棋盘尺寸是玩家选的固定数组要么浪费空间要么写死尺寸无法扩展。每个格子用一个结构体描述字段要分开别把多个状态塞进一个int里按位运算struct Cell { bool isMine; // 是否是雷 bool isRevealed; // 是否已被翻开 bool isFlagged; // 是否被插旗 int neighbor; // 周边8格雷的数量-1表示是雷 };neighbor初始化为0布雷结束后统一计算。用-1标记雷格这样渲染时拿到neighbor -1直接画地雷图标不用再查isMine。逻辑上前三个bool互不冲突插旗和翻开是两件独立的事这个设计在后面处理右键标记时能省不少事。一维vector的下标换算公式是index row * cols col。这个公式会在布雷、递归展开、绘制三个地方反复出现统一写成内联函数或者宏别到处手写否则改行列数时会漏改某个角落。另外随机数要用rand()就记得设置种子。很多人不写srand结果每局地图一模一样还以为是布雷写错了。正确做法是在InitInstance里调用一次srand((unsigned)time(NULL))整个程序生命周期只调用这一次。3.2 布雷算法与首次安全点击两套常见方案的取舍布雷的逻辑很直白随机挑格子是雷就跳过不是雷就放一颗直到放满指定的雷数。代码如下void CMineGame::InitBoard(int rows, int cols, int mineCount) { m_rows rows; m_cols cols; m_mineCount mineCount; m_board.assign(rows * cols, Cell()); // 全部重置 int placed 0; while (placed mineCount) { int r rand() % rows; int c rand() % cols; Cell cell m_board[r * m_cols c]; if (!cell.isMine) { cell.isMine true; cell.neighbor -1; // 雷格标记为-1 placed; } } RecalcNeighborCount(); // 见下方说明 } void CMineGame::RecalcNeighborCount() { for (int r 0; r m_rows; r) { for (int c 0; c m_cols; c) { Cell cell m_board[r * m_cols c]; if (cell.isMine) continue; 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 r dr, nc c dc; if (nr 0 nr m_rows nc 0 nc m_cols) { if (m_board[nr * m_cols nc].isMine) count; } } } cell.neighbor count; } } }上面代码里while (placed mineCount)用了一个很常规的“抽到非雷才计数”策略。极端情况下如果雷数量接近格子总数后段循环会空转但扫雷的雷占比不会超过50%所以这个损耗可以忽略。注意rand() % rows存在取模偏差这对游戏场景完全够用不用上std::mt19937这么重的方案。布雷完成后还有一个经典需求玩家第一次点击不能踩雷。常见做法有两种。第一种是初始化时不布雷等玩家第一次点击后把点击位置周围3x3排除掉再布雷。第二种是先布雷玩家点击后如果踩雷把雷挪到别处。我倾向于第二种因为棋盘状态从初始化那一刻就完整了不会出现“布雷前数据不可用”的尴尬期。挪雷的代码要排查周围3x3的格子确保玩家点击处以及相邻八格全部没有雷。3.3 翻开格子的洪水填充递归写法与边界控制扫雷最核心的交互是翻开格子。如果翻开的是空白格周边雷数为0需要自动向外扩散把连成片的空白区域全部揭开。这个动作叫洪水填充递归是最直观的写法void CMineGame::OpenCell(int row, int col) { if (row 0 || row m_rows || col 0 || col m_cols) return; // 越界直接返回 Cell cell m_board[row * m_cols col]; if (cell.isRevealed || cell.isFlagged) return; // 已翻开或被插旗不能点 cell.isRevealed true; m_revealedCount; if (cell.isMine) { m_gameOver true; // 踩雷游戏结束 return; } if (cell.neighbor 0) { // 空白格递归翻开周围8格 for (int dr -1; dr 1; dr) for (int dc -1; dc 1; dc) { if (dr 0 dc 0) continue; OpenCell(row dr, col dc); } } }这段代码有四个关键点。第一先标记isRevealed再递归否则相邻两个空白格会互相调用翻个没完最终栈溢出。第二递归前检查isFlagged被插旗的格子即使周围全空也不能翻开这是Windows扫雷的标准行为。第三越界检查在递归入口做保证8个方向的调用不用提前判断。第四整个递归在9x9棋盘上的最大深度不会超过几十层不需要担心调用栈但如果哪天做超大棋盘可以改成显式栈的迭代版本。m_revealedCount每翻开一格就自增它是胜利判定的唯一依据。翻雷时虽然也加了一次计数但m_gameOver已经置位后续不会再走胜利判断。3.4 胜利判定与剩余雷数统计状态算对才能交差胜利条件很明确所有非雷格子全部翻开。因为总格子数、雷数、已翻格数都是已知量判定只需要一条表达式bool CMineGame::IsWin() const { // 翻开的格子数 总格子数 - 雷数 return m_revealedCount m_rows * m_cols - m_mineCount; }这个表达式成立的前提是m_revealedCount只在OpenCell里递增没有别的地方能偷偷改动。如果哪天你加了“右键双击翻开周围”的功能一定要确保那个入口也走OpenCell否则计数会失真。剩余雷数的计算则是用总雷数减去插旗数。注意这里有个坑玩家可以把旗插在没有雷的格子上导致剩余雷数显示成负数。标准的Windows扫雷允许错误插旗但要给剩余雷数加下限保护int CMineGame::GetRemainingMineCount() const { int flagged 0; for (const Cell cell : m_board) if (cell.isFlagged) flagged; return max(0, m_mineCount - flagged); }处理完这几块游戏的“大脑”就完整了完全不依赖MFC也能跑。你甚至可以在黑窗口里写个main函数模拟随机点击测试几千次验证不崩溃然后再回来画界面。这个习惯能帮你省下一大半调试时间。4. GDI绘制与鼠标交互双缓冲抗闪烁、坐标换算别算错4.1 双缓冲绘制OnPaint里先画内存DC再BitBltMFC窗口的绘制集中在OnPaint这是扫雷里观感影响最大的一块。如果你直接在CPaintDC上画格子拖动窗口时会看到界面闪烁得厉害因为每画一个格子显示器就刷新一次中间过程全暴露出来了。双缓冲是标准解法先在内存里建一块和窗口一样大的画布把格子全部画到内存再一次拷贝到屏幕。一次拷贝就一次刷新闪烁肉眼基本看不出来void CMainFrame::OnPaint() { CPaintDC dc(this); // 窗口DC最终输出目标 CDC memDC; // 内存DC CBitmap memBmp; memDC.CreateCompatibleDC(dc); memBmp.CreateCompatibleBitmap(dc, m_width, m_height); CBitmap* pOld memDC.SelectObject(memBmp); // 整块棋盘画到内存DC memDC.FillSolidRect(0, 0, m_width, m_height, RGB(190, 190, 190)); for (int r 0; r m_game.GetRows(); r) { for (int c 0; c m_game.GetCols(); c) { DrawCell(memDC, r, c); // 每个格子的具体绘制 } } // 一次性从内存拷贝到屏幕 dc.BitBlt(0, 0, m_width, m_height, memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOld); // 恢复选中的位图防资源泄漏 }这里CreateCompatibleBitmap创建的位图要能覆盖整个客户区所以m_width和m_height要在OnSize里随窗口大小更新。SelectObject(pOld)那句很多人会省略短时间看不出问题但窗口反复创建销毁时GDI对象会累积泄漏最终导致绘制异常或者程序变慢养成恢复原对象的习惯很重要。DrawCell里面按格子状态画不同内容未翻开画灰色凸起块插旗画小红旗翻开画底和数字。数字的颜色按neighbor值映射1到8分别对应蓝、绿、红、深蓝、棕、青、黑、灰这组颜色是Windows扫雷从老版本沿用下来的直接用能让老玩家一眼觉得“对味”。4.2 鼠标消息映射坐标换算和左右键状态机鼠标处理里最容易翻车的是坐标换算。消息回调拿到的point是客户区像素坐标而棋盘是行列逻辑坐标中间必须经过偏移和整除两步换算void CMainFrame::OnLButtonDown(UINT nFlags, CPoint point) { // point是客户区坐标先减去棋盘左上角偏移再除以格子边长 int col (point.x - m_boardLeft) / m_cellSize; int row (point.y - m_boardTop) / m_cellSize; if (!m_game.IsInside(row, col)) return; // 点在棋盘外忽略 if (m_game.IsGameOver()) return; m_game.OpenCell(row, col); UpdateStatusBar(); // 刷新状态栏数字 Invalidate(); // 触发重绘 }m_boardLeft和m_boardTop是棋盘左上角在客户区里的坐标在OnSize或者创建窗口时算好。如果你忘了减偏移点击位置会整体偏移一个边距左边和上边尤其明显——点第一个格子实际翻的是右边第二个这种问题光靠调试很难一眼看出来直接在纸上画个坐标系对比最快。右键插旗的处理类似走OnRButtonDown。插旗的逻辑是已翻开的格子不能插旗未翻开的格子如果没旗就插旗、有旗就去掉旗。再补一个细节右键对已翻开的数字格在Windows扫雷中会触发“双击翻开周围”这个放到最后一章说。踩雷之后不能直接结束要把所有雷的位置都暴露出来让玩家死得明白。常见做法是在m_gameOver置位后递归翻开所有格子或者在绘制阶段特殊处理游戏结束时强制把所有isMine的格子画成雷。显然后者更简单也更符合“绘制按状态输出”的原则。4.3 状态栏显示剩余雷数与用时SetPaneText的用法网上搜“MFC状态栏怎么显示”答案基本都是围绕CStatusBar转。扫雷里状态栏放两个指标剩余雷数和已用时间。先在MainFrame类里声明一个CStatusBar m_wndStatusBar成员然后在OnCreate里初始化static UINT indicators[] { ID_SEPARATOR, // 第一格默认拉伸的信息区 ID_INDICATOR_MINE, // 第二格剩余雷数 ID_INDICATOR_TIME, // 第三格计时 }; m_wndStatusBar.Create(this); m_wndStatusBar.SetIndicators(indicators, 3); // 三个指示器 m_wndStatusBar.SetPaneInfo(0, ID_SEPARATOR, SBPS_STRETCH, 0); m_wndStatusBar.SetPaneInfo(1, ID_INDICATOR_MINE, SBPS_NORMAL, 80); m_wndStatusBar.SetPaneInfo(2, ID_INDICATOR_TIME, SBPS_NORMAL, 80);SetPaneInfo的第四个参数是面板宽度单位是像素。三个格子一个拉伸占满剩余空间两个固定宽度这样无论窗口怎么拉状态栏都不会挤成一团。ID_INDICATOR_MINE和ID_INDICATOR_TIME要在资源文件里定义成整数ID不想动资源编辑器的话直接在头文件里#define ID_INDICATOR_MINE 102也行。刷新时用SetPaneTextvoid CMainFrame::UpdateStatusBar() { CString str; str.Format(_T(剩余雷数%d), m_game.GetRemainingMineCount()); m_wndStatusBar.SetPaneText(1, str); str.Format(_T(用时%d秒), m_elapsedSeconds); m_wndStatusBar.SetPaneText(2, str); }这里SetPaneText第一个参数是面板下标不是ID。用SetIndicators建立面板后下标从0开始数所以第一个信息区是0剩余雷数是1用时是2。偶尔有人拿ID传进去显示不出来还以为是状态栏坏了其实是把下标和ID搞混了。5. MFC扫雷常见翻车现场5个踩坑记录与排查方法5.1 现象窗口一拖动棋盘就错位或者画不全现象窗口正常打开时棋盘显示完整拖动改变大小后棋盘边缘出现白块或者格子大小没变但位置歪了。原因OnPaint里用了创建窗口时算好的固定棋盘尺寸窗口变大后客户区变宽但棋盘还是按老尺寸画的。另一个常见原因是CS_HREDRAW和CS_VREDRAW窗口样式没有设置拖动窗口时系统不会自动擦除重绘旧画面残留在新位置旁边。解决在OnSize里根据当前客户区重新计算棋盘偏移和格子大小然后Invalidate()强制重绘。窗口类注册时加上CS_HREDRAW | CS_VREDRAW让任何尺寸变化都触发完整重绘。调试时可以在OnSize里用TRACE打印客户区宽高对比点击坐标换算结果错位问题基本当场就能暴露。5.2 现象第一次点击直接踩雷玩家体验极差现象开局第一次左键翻开就是地雷游戏瞬间结束。有人觉得这是概率问题但实际玩Windows扫雷多年的人都知道正规版本第一次点击永远不会炸。原因布雷发生在游戏初始化时玩家第一次点击没有做安全保护点到了随机布下的雷上。严格说这不是bug而是设计缺失但用户感知就是翻车。解决见3.2节布雷后记录玩家第一次点击的坐标调用安全化处理把点击点3x3范围内的所有雷挪到别处。注意挪雷时要保证目标格原本不是雷且不在3x3安全区内否则又会出现新的问题。5.3 现象界面狂闪眼睛看着难受现象每次点击或计时刷新棋盘跳动闪烁尤其是快速连续点击时画面像在抖。原因直接在CPaintDC上逐个画格子每画一格系统就刷新一次屏幕。Windows对客户区的每次写入都会触发显示更新十几二十个格子连续画中间过程全部暴露观感就是闪。解决双缓冲见4.1节。另外还要做一件事重写OnEraseBkgnd直接返回TRUE阻止系统用背景色擦除客户区。擦除和重绘两个动作分开中间露出底色也是闪烁的元凶之一BOOL CMainFrame::OnEraseBkgnd(CDC*) { return TRUE; // 禁止擦背景交给OnPaint整体覆盖 }配合双缓冲和禁止擦背景闪烁问题根治。如果你发现还是有轻微闪检查一下是不是UpdateStatusBar里每次都调了Invalidate状态栏那种局部刷新用RedrawWindow指定区域更合适。5.4 现象右键插旗后剩余雷数不对甚至出现负数现象玩家在非雷格子上插了一堆旗状态栏显示的剩余雷数变成负数或者游戏明明把雷都标出来了却不判胜利。原因剩余雷数用“总雷数减插旗数”计算没有做负数保护胜利判定误把“插旗数等于雷数”当成胜利条件而非“所有非雷格子全翻开”。插旗是玩家的猜测行为不能作为游戏终局依据。解决剩余雷数加max(0, ...)保护见3.4节的GetRemainingMineCount。胜利判定只用m_revealedCount 总格子数 - 雷数这一条。插旗错误提示可以在界面层面处理当玩家对已插旗格子再次右键时去掉旗子不会影响任何数值计算。5.5 现象计时器越走越慢或者刷新不及时现象计时开始后状态栏的时间偶尔跳秒甚至卡住几秒不动然后又突然跳好几秒。原因用了WM_TIMER定时器在OnTimer里把秒数累加。WM_TIMER的精度大约15.6毫秒而且消息队列忙碌时会延迟投递累加次数不等于真实时间。解决用系统时间做差值计算。启动时记录GetTickCount64()的值每次刷新显示(当前值 - 起始值) / 1000秒。定时器只负责触发刷新不负责计时void CMainFrame::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1 m_game.HasStarted()) { ULONGLONG elapsed GetTickCount64() - m_startTick; int seconds (int)(elapsed / 1000); CString str; str.Format(_T(用时%d秒), seconds); m_wndStatusBar.SetPaneText(2, str); } CFrameWnd::OnTimer(nIDEvent); }这样即使定时器延迟几秒投递显示值依然基于真实时间不会有累计误差。6. 把扫雷从“能玩”做到“像样”自测模式与按钮质感6.1 用自测模式跑一万局验证逻辑不死锁界面接完第一件事不是反复手点而是写一个自测入口。把CMineGame单独编译写一个不带MFC的main函数模拟随机点击、随机插旗连续跑上万局。统计有没有崩溃、有没有死循环、有没有“翻开数超过了非雷总数”这类状态错乱。这一步能把你逻辑类的隐藏bug全部逼出来// self_test.cpp独立于MFC工程可单独编译 #include MineGame.h #include cstdlib #include ctime int main() { srand((unsigned)time(NULL)); for (int round 0; round 10000; round) { CMineGame game; game.InitBoard(9, 9, 10); for (int step 0; step 100; step) { int r rand() % 9, c rand() % 9; game.OpenCell(r, c); if (game.IsGameOver()) break; } // 到这里没崩没卡死这一局就通过 } return 0; }这不算什么高深技巧但很多人跳过了这步直接上界面结果一边点一边崩还得通过弹窗去定位问题。逻辑先验证到位再进界面省下来的时间远比你想象得多。我当年交课设时吃过这个亏后来凡是带核心算法的小程序都先做自测这个习惯一直保留到现在。6.2 让未翻开的格子有凸起感DrawEdge画出经典按钮质感最后一个小技巧用DrawEdge画未翻开的格子能立刻让画面接近Windows经典扫雷的观感void CMainFrame::DrawCell(CDC dc, int row, int col) { CRect rc( m_boardLeft col * m_cellSize, m_boardTop row * m_cellSize, m_boardLeft (col 1) * m_cellSize, m_boardTop (row 1) * m_cellSize); if (!m_game.IsRevealed(row, col)) { // 未翻开画凸起边框模拟按钮质感 dc.DrawEdge(rc, EDGE_RAISED, BF_RECT); } else { // 翻开画凹陷边框 dc.DrawEdge(rc, EDGE_SUNKEN, BF_RECT); // 有数字画数字是雷画雷 } }EDGE_RAISED和EDGE_SUNKEN是DrawEdge的经典组合一个凸一个凹视觉上不用颜色就能区分翻开和未翻开。到这里你的MFC扫雷从逻辑、绘制到状态栏已经是一个完整的作品再往后就是加雷区右键双击翻开、扫雷排行榜存档这些锦上添花的功能了。我做这类小工程最大的感触是MFC的坑大多不是框架本身的坑而是对消息机制和绘制时序理解不透导致的。把窗口当成“消息驱动的一个状态机”把算法和界面拆开几乎八成问题都能在设计阶段规避。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号