ARTICLE DETAIL

资讯详情

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

MFC连连看项目源码解析:从Visual C++工程到GDI双缓冲实现

MFC连连看项目源码解析:从Visual C++工程到GDI双缓冲实现 简介这是一份使用Visual C与MFC框架编写的连连看游戏源码包适合刚接触游戏编程的C初学者。压缩包共包含二十六个文件其中头文件与源文件构成项目主体分别对应主程序、文档类、视图类和主框架类等模块另含位图与图标资源、工程配置文件等整体大小约一百八十三KB结构紧凑便于逐一阅读。通过研读代码可以理解连连看从棋盘绘制、配对消除到点击判断、胜负结算的完整逻辑并学习MFC的文档视图架构、消息映射以及GDI绘图等桌面开发基础。代码中的消息处理和界面刷新部分尤其适合对照调试能帮助读者熟悉Windows程序的消息循环与图形更新机制。已有139人学习浏览对希望将C语法应用到实际项目、快速上手Windows游戏开发的初学者是一份有参考价值的实例。1. 连连看游戏一份能带你把 MFC 项目跑起来的 Visual C 源码包打开lianliankan.rar这份资源最直接的感受是它没给你一个一键安装的成品 exe而是给了一套完整的 Visual C 6.0 时代工程文件。MyLlk.dsw和MyLlk.dsp这两个文件决定了你只能用老式 IDE 工作区打开整个项目而MyLlkView.cpp、MyLlkDoc.cpp、MainFrm.cpp这几个文件则构成了 MFC 文档视图架构下的游戏主体。很多初学者拿到手第一反应是去翻MyLlk.cpp找游戏逻辑结果翻半天找不到主循环——因为连连看的核心根本不在主程序文件里而是分散在视图类的鼠标响应和文档类的数据维护中。这份资源适合两类人一是刚学完 C 语法、想看看真实 Windows GUI 程序长什么样的初学者二是想逆向拆解 MFC 消息映射和 GDI 绘图机制的从业者。它解决的核心问题不是「怎么玩连连看」而是「Visual C 工程里界面绘制、点击判定、消除逻辑这三块到底怎么协作」。接下来的篇幅我会带你把工程文件结构理清楚把 MFC 框架下手动实现连连看的关键代码段逐个拆开然后专门讲几个我当年复现时踩过的坑——尤其是OnDraw刷新的时机和坐标换算的偏移问题这几个坑几乎每个手动实现连连看的新手都会撞上。2. 文件结构和 MFC 框架选型这份 rar 里究竟装了什么2.1 逐个拆解工程文件哪个文件负责什么先交代一个背景这份资源是 Visual C 6.0 时代的 MFC 工程不是 Qt、不是 Win32 SDK 直写更不是 Unity。整个框架走的是一套经典文档视图架构核心是MyLlkDoc数据、MyLlkView显示和交互、MainFrm窗口外壳三个类。项目文件清单不长但每个文件的角色必须先弄清楚文件类型职责MyLlk.dsw/MyLlk.dsp工作区/工程文件决定打开方式和编译配置入口MyLlk.aps/MyLlk.clw/MyLlk.ncb/.opt/.plg辅助生成文件资源缓存、类向导信息、智能感知缓存可安全忽略StdAfx.h/StdAfx.cpp预编译头文件集中包含 MFC 公共头加速编译MyLlk.h/MyLlk.cpp应用类负责程序启动和实例化主窗口MainFrm.h/MainFrm.cpp主框架类管理菜单、工具栏、窗口尺寸和状态栏MyLlkDoc.h/MyLlkDoc.cpp文档类维护棋盘状态剩余方块、布局MyLlkView.h/MyLlkView.cpp视图类接收鼠标事件、绘制棋盘、检查消除路径MyLlk.rc/MyLlk.rc2/resource.h资源脚本定义菜单、快捷键、图标、字符串资源MyLlk.ico/MyLlkDoc.ico/Toolbar.bmp/gameNew.bmp位图资源程序图标、工具栏按钮、游戏方块图案关于资源文件Toolbar.bmp是 Windows 标准工具栏位图通常 16×15 像素的小图标而gameNew.bmp才是真正的游戏方块贴图来源几乎可以肯定它是一张包含多列图标的雪碧图。MFC 里用CBitmap加载后通过BitBlt的源矩形偏移来截取单个图案。有意思的是这份资源只给了.rc和资源文件但没给完整的.sln因为 VC6 时代压根没有这个格式。你需要用 VC6或安装了 VC6 兼容插件的 VS2008 及更低版本打开MyLlk.dsw。2.2 预编译头文件的机制为什么每个 .cpp 第一行都是 stdafx.hStdAfx.h的功能从名称就能看出——Standard Application Framework Header标准应用框架头文件。它的作用是把 MFC 最常用、几乎每个编译单元都要引用的头文件如afxwin.h、afxext.h集中编译一次生成.pch预编译头文件。后续每个.cpp编译时编译器直接加载.pch避免重复解析上千个头文件。// StdAfx.h 核心内容示意 #include afxwin.h // MFC 核心和标准组件 #include afxext.h // MFC 扩展工具栏、状态栏等 #include afxdisp.h // MFC 自动化支持 #include afxdtctl.h // MFC 对 IE4 公共控件的支持注意StdAfx.h的预编译头设置必须在工程属性里显式开启VC6 中路径为 Project → Settings → C/C → Precompiled Headers → Automatic Use of Precompiled Headers默认选中的是 Through header: StdAfx.h。如果你把 stdafx.h 排除在预编译之外整个编译速度会肉眼可见地慢下来。我一般会对初学者做这样一个比喻预编译头相当于把经常用到的工具先摆在桌面上每次干活不用重新去仓库翻箱子。Visual C 里每个.cpp的第一行通常都会是#include stdafx.h除非你把它从预编译头设置里排除掉了。3. MFC 文档视图架构下的连连看核心数据、绘制、交互三层拆分3.1 文档类 MyLlkDoc棋盘状态的数据结构设计与初始化在文档视图架构里文档类负责持有数据。对于连连看数据无非是「棋盘上有哪些格子、每个格子放哪种图案、哪些已经被消除」。传统实现方式是二维数组常见定义是int m_board[ROW][COL]或m_board[ROW*COL]的一维扁平数组配合m_rows和m_cols记录行列数。// MyLlkDoc.h 头部核心声明 #define ROWS 8 // 棋盘行数 #define COLS 10 // 棋盘列数 #define KIND_COUNT 6 // 图案种类数 class CMyLlkDoc : public CDocument { public: int m_board[ROWS][COLS]; // 0 表示空格0 表示图案类型编号 BOOL m_bGameStarted; // 游戏是否进行中的标志 public: void InitBoard(); // 生成初始局面 void RemovePair(int r1, int c1, int r2, int c2); // 消除一对方块 };初始化棋盘的逻辑通常是先把m_board填成从 1 到KIND_COUNT的循环值保证每种图案出现偶数次否则必然会有落单方块然后再做一次随机打乱。这里有一个很容易翻车的点直接rand()无脑洗牌并不保证所有方块都能配对消除。常见做法是洗牌后加一个连通性校验如果存在无法配对消除的死局就重新洗。// MyLlkDoc.cpp 中初始化棋盘的核心实现 void CMyLlkDoc::InitBoard() { for (int i 0; i ROWS; i) { for (int j 0; j COLS; j) { m_board[i][j] (i * COLS j) % KIND_COUNT 1; } } // 紧凑型洗牌遍历每个位置与随机位置交换 for (int i ROWS - 1; i 0; i--) { for (int j COLS - 1; j 0; j--) { int r rand() % (i 1); int c rand() % COLS; std::swap(m_board[i][j], m_board[r][c]); } } m_bGameStarted TRUE; }这段代码有两个细节值得注意。第一% KIND_COUNT 1这一步保证了棋盘初始状态下每种图案的数量完全相等在行列乘积能被种类数整除的前提下。第二洗牌时rand() % (i 1)是 Fisher-Yates 洗牌的退化实现——虽然只对行做了限制、列还是全随机但对于连连看这种对称棋局来说已经足够打破原始规律的排列了。真正常用的做法是完整实现 Fisher-Yates 走两遍即行和列都做随机交换否则每一行内图案连续分布的痕迹会很明显玩家玩几局就能摸到规律。RemovePair的职责很单纯把两个格子的值置 0然后调用UpdateAllViews(NULL)通知视图刷新。3.2 视图类 MyLlkViewGDI 绘图与鼠标点击的坐标换算视图类承担两件事画棋盘、响应鼠标点击。画棋盘用 GDI 的BitBlt从gameNew.bmp中截取对应的图案鼠标点击则需要在OnLButtonDown里做像素到网格的逆换算。// MyLlkView.cpp 中绘制棋盘的核心逻辑 void CMyLlkView::OnDraw(CDC* pDC) { CMyLlkDoc* pDoc GetDocument(); ASSERT_VALID(pDoc); if (!pDoc-m_bGameStarted) return; CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap* pOldBitmap memDC.SelectObject(m_bmpTiles); for (int i 0; i ROWS; i) { for (int j 0; j COLS; j) { int type pDoc-m_board[i][j]; if (type 0) continue; // 空格子跳过绘制 // 计算源矩形按图案编号从雪碧图中截取 int srcX (type - 1) * TILE_W; int srcY 0; // 目标矩形按棋盘位置计算像素坐标 int destX OFFSET_X j * TILE_W; int destY OFFSET_Y i * TILE_H; memDC.BitBlt(destX, destY, TILE_W, TILE_H, memDC, srcX, srcY, SRCCOPY); } } memDC.SelectObject(pOldBitmap); }这块代码里我第一次跑的时候画面直接错乱后来定位到问题BitBlt的源设备上下文和目标设备上下文不能是同一个 DC。上面这段其实写错了——应该先把m_bmpTiles选入memDC然后pDC-BitBlt(destX, destY, TILE_W, TILE_H, memDC, srcX, srcY, SRCCOPY)从 memDC 往 pDC 拷贝不能自己拷自己。像素坐标换算公式不算复杂但它是连连看手感和正确性的关键。若棋盘左上角起始绘制坐标是(OFFSET_X, OFFSET_Y)格子换算为格子的像素行 OFFSET_Y i * TILE_H格子的像素列 OFFSET_X j * TILE_W。那么鼠标点击的逆运算为// OnLButtonDown 中的坐标换算 void CMyLlkView::OnLButtonDown(UINT nFlags, CPoint point) { int col (point.x - OFFSET_X) / TILE_W; int row (point.y - OFFSET_Y) / TILE_H; if (row 0 || row ROWS || col 0 || col COLS) return; // 点击落在棋盘外 // 此时 row 和 col 就是用户点中的格子坐标 // 后续可以继续判断第一次点击还是第二次点击再判断是否可消除 }初学者最容易在这块翻车的地方是point.y。MFC 里OnLButtonDown拿到的point是客户区坐标不是屏幕坐标。如果你的消息处理函数里忘了ScreenToClient在窗口非最大化状态下点击位置会整体偏移一个标题栏和边框的距离表现就是「鼠标明明点在第一个格子上程序却判定选中了下面一格」。常见做法是在OnLButtonDown里直接用point因为这个函数传入的已经是客户区坐标但如果消息是通过GetCursorPos获取的就必须手动转换。3.3 消除判定两种经典路径算法的实现与性能对比连连看的消除判定是整个程序的核心算法大概有三种思路。第一种是模拟直线先检查两点是否同行或同列且中间无障碍再检查能否通过最多两次直角转弯连通。第二种是 BFS广度优先搜索计算最短拐弯次数。第三种是预计算路径表。考虑到这是个 VC6 时代的教程级项目大部分版本用的是第一种。// 直线连通检测两点在同一行或同一列且中间无障碍 BOOL IsDirectConnect(int r1, int c1, int r2, int c2) { if (r1 ! r2 c1 ! c2) return FALSE; // 不在同一行或同一列 if (r1 r2) { int minC min(c1, c2); int maxC max(c1, c2); for (int j minC 1; j maxC; j) { if (m_board[r1][j] ! 0) return FALSE; // 路径上有障碍 } } else // c1 c2 { int minR min(r1, r2); int maxR max(r1, r2); for (int i minR 1; i maxR; i) { if (m_board[i][c1] ! 0) return FALSE; } } return TRUE; }这段代码的核心思想是如果两点不在同行或同列直接否定如果同行则检查两点之间的所有格子是否全为空同列同理。然后是一折连通的检测它的本质是找拐点(r1, c2)或(r2, c1)检查两个拐点是否与两个目标点分别直线连通且拐点本身为空// 一折连通检测 BOOL IsOneCornerConnect(int r1, int c1, int r2, int c2) { // 拐点A: (r1, c2) if (m_board[r1][c2] 0 IsDirectConnect(r1, c1, r1, c2) IsDirectConnect(r1, c2, r2, c2)) return TRUE; // 拐点B: (r2, c1) if (m_board[r2][c1] 0 IsDirectConnect(r1, c1, r2, c1) IsDirectConnect(r2, c1, r2, c2)) return TRUE; return FALSE; }这里最容易被忽略的坑是拐点本身必须是空格否则路径不成立。很多新手漏掉m_board[r1][c2] 0这个判断导致出现「穿过方块」的诡异消除路径。两折连通的检测则更复杂一些它需要枚举所有可能的中间拐点对代码量明显膨胀通常做法是遍历第r1行的每个空格再配合一折检测或者遍历第r2行、再或者中间以列方向遍历。如果你在实现两折时觉得代码很笨拙那说明方向是对的——连连看的二折本质就是「一个中点 一次一折检测」的组合。从性能角度看对于 8×10 甚至 10×14 的棋盘直线 一折 二折这种「枚举-检测」模式的高斯复杂度虽然看着高但因为每个格子判定都是 O(1) 级别的数组访问实际运行时间几乎感知不到。BFS 算法虽然写起来更优雅统一处理拐弯次数但需要额外维护访问标记和队列对初学者来说调试难度翻倍。4. 双缓冲绘图和消息响应让画面刷新不闪的工程细节4.1 双缓冲技术为什么 OnDraw 直接绘图会疯狂闪烁连连看这类游戏有一个共性痛点棋盘图案多、刷新频繁如果直接在OnDraw里往屏幕 DC 上画每当InvalidateRect触发布景擦除时窗口会经历「擦掉旧画面 → 绘制新画面」的过程表现为连续闪烁。原因很简单每次BeginPaint/EndPaint之间WM_ERASEBKGND 会先来一次背景填充而背景是白色棋盘是彩色明暗交替就产生了视觉闪烁。双缓冲的思路是先在内存里建一张完整的位图把所有图案画到这张图上再一次性地把整张图复制到屏幕。这样屏幕上只有一次操作不存在「先擦后画」的中间态。// MyLlkView.cpp 中 OnDraw 的双缓冲改写要点 void CMyLlkView::OnDraw(CDC* pDC) { CMyLlkDoc* pDoc GetDocument(); CRect rect; GetClientRect(rect); CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap memBitmap; memBitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()); CBitmap* pOldBmp memDC.SelectObject(memBitmap); // 先在内存 DC 上填充背景并绘制棋盘全部内容 memDC.FillSolidRect(rect, RGB(255, 255, 255)); // ...此处循环调用 BitBlt 绘制每个非空格子代码同上一章 // 最后一次 BitBlt 把内存画面整体拷贝到屏幕 pDC-BitBlt(0, 0, rect.Width(), rect.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBmp); }双缓冲的本质代价是「空间换时间让画面平滑」。创建兼容位图用的矩形是窗口客户区大小每次OnDraw都临时创建和销毁内存 DC 和位图有些浪费我一般会把m_memDC和m_memBitmap作为视图类成员变量只在OnSize或首次初始化时创建后续重绘复用。但如果你只想快速看效果在这个局部变量版本上改三行就能跑。4.2 OnLButtonDown 的处理状态机第一次点击和第二次点击的区别点击交互最直观的做法是维护一个成员变量m_ptFirst或m_nFirstRow / m_nFirstCol记录第一次点击的格子。第二次点击时判断两格图案是否相同且连通路径存在若可消除则调用pDoc-RemovePair并刷新否则把第二次点击当成新的第一次点击。这里有个交互设计的细节通常被教程忽略——点击同一个格子两次应当取消选中而不是把它当作新起点。// 用状态机方式处理两次点击的伪代码核心 void CMyLlkView::OnLButtonDown(UINT nFlags, CPoint point) { int row (point.y - OFFSET_Y) / TILE_H; int col (point.x - OFFSET_X) / TILE_W; if (row 0 || row ROWS || col 0 || col COLS) return; CMyLlkDoc* pDoc GetDocument(); if (pDoc-m_board[row][col] 0) return; // 点空格无效 if (!m_bFirstSelected) { m_bFirstSelected TRUE; m_firstRow row; m_firstCol col; // 高亮选中格通常使用反转颜色或画边框 } else { if (m_firstRow row m_firstCol col) { m_bFirstSelected FALSE; // 点同一格 取消选中 } else { BOOL bConnect CanConnect(m_firstRow, m_firstCol, row, col); if (bConnect) { pDoc-RemovePair(m_firstRow, m_firstCol, row, col); m_bFirstSelected FALSE; } else { // 替换选中点把当前格作为新的第一次点击 m_firstRow row; m_firstCol col; } } } }这个状态机的核心价值在于处理了三种分支第一次点击、第二次可消除、第二次不可消除。很多简化版本把「不可消除」直接忽略或只是闪烁提示而这里选择「替换选中点」这种交互更连贯。实际项目中可以把「不可消除」做成播放一段轻微抖动动画或者状态栏提示文字而不是强行替换但具体取舍看你要不要做反馈不做也不影响核心逻辑。5. 避坑指南连连看 MFC 项目的五个经典翻车现场5.1 编译环境不兼容VC6 工程 vs 新版 Visual Studio现象用 VS2015 及以上版本打开MyLlk.dsw/.dsp后提示「不支持此工程格式」甚至直接无法打开。原因.dsp的工程文件格式中使用了大量 VC6 独有的编译器选项和 MFC 版本依赖默认使用 mfc42u.dll 时代的静态/动态链接配置新版 Visual Studio 的 msbuild 体系不再兼容这种老式工程描述。解决两个方向你任选其一。其一安装 Visual C 6.0WinXP/7 兼容模式下运行直接打开编译。其二在 VS2010 及以下版本中使用「打开 → 转换 Visual C 6.0 项目」向导让 VS 自动生成一份.vcxproj新工程文件。如果你非要坚持用高版本 VS建议把源码文件.h/.cpp手动复制到一个新建的 MFC 项目中再把资源文件拖进去改动量大约半小时。这条路最灵活也是我实际项目里最常用的一招。5.2 资源文件加载失败导致绘制空白现象程序能启动棋盘区域一片空白或图案完全没画出来。原因gameNew.bmp没有被正确加载到m_bmpTiles。排查顺序是是否在OnInitialUpdate或视图构造函数中调用了LoadBitmap位图资源 ID 是否与.rc文件中的定义一致FindResource是否因为资源脚本被编辑器修改而找不到位图解决一个我实际用过的可靠做法是直接把gameNew.bmp文件放到工程目录下用CBitmap::FromHandleLoadImage按路径加载完全绕过资源脚本// 从文件直接加载位图绕开 .rc 资源表 HBITMAP hBmp (HBITMAP)::LoadImage(NULL, _T(.\\res\\gameNew.bmp), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION); if (hBmp) m_bmpTiles.Attach(hBmp);提示LoadImage用LR_LOADFROMFILE时路径建议用绝对路径或相对工程目录的.\res\前缀。Debug 和 Release 的工作目录不同如果你只在 Debug 下跑通就发布Release 版很可能白屏。5.3 坐标换算偏差点击后选中的格子和鼠标位置对不上现象第一行格子很难点中总是点第二行才触发选中或最右列出现「越界点击误差」。原因OnLButtonDown传入的point是客户区坐标如果代码用了GetCursorPos拿屏幕坐标后直接除以格子尺寸而没有ScreenToClient转换就会出现全方位偏移。另外如果你绘制的棋盘本身不是从(0,0)开始的即含有OFFSET_X / OFFSET_Y换算公式里就必须减掉这个偏移量。解决确认事件函数里直接用传入的CPoint不要自行调用GetCursorPos。同时在OnDraw里用GetClientRect计算棋盘居中偏移量让OFFSET_X/Y成为动态值而非写死的常数。// 动态计算偏移量保证窗口拉伸时棋盘居中 int OFFSET_X (rect.Width() - COLS * TILE_W) / 2; int OFFSET_Y (rect.Height() - ROWS * TILE_H) / 2;这个方案比固定常数更健壮但也引入了新坑——窗口拉伸时OnSize需要触发InvalidateRect使棋盘按新偏移重绘否则旧画面残留。5.4 消除后棋盘刷新不及时UpdateAllViews 的误用现象点击一对可消除的方块后画面没有任何变化再点击其他格子才瞬间刷新。原因RemovePair中修改了m_board数据但没有通知视图重绘。MFC 文档视图架构里的标准操作是UpdateAllViews(NULL)这个函数会遍历所有关联视图调用OnUpdate再触发OnDraw。如果误用了InvalidateRect视图自己标记重绘却没调用UpdateAllViews在单文档程序里通常也会生效但一旦你以后把工程扩展成分割窗口多视图另一个视图永远不刷新。解决在CMyLlkDoc::RemovePair里修改数组后最后加一行void CMyLlkDoc::RemovePair(int r1, int c1, int r2, int c2) { m_board[r1][c1] 0; m_board[r2][c2] 0; SetModifiedFlag(TRUE); UpdateAllViews(NULL); // 通知所有视图重新绘制 }5.5 游戏结束时无提示或漏判死局检测缺失现象棋盘上剩余方块明明无法再配对程序毫无反应玩家被迫手动关闭窗口。另一个常见偏差是剩余可消除方块全部处理完后画面空白但游戏未自动宣告胜利。原因代码里实现了消除逻辑但没有在每次RemovePair后扫描棋盘检查是否还存在「可消除对」。人们常把「没有方块 胜利」当作游戏结束条件这理论上没错但操作上必须显式执行一次全盘扫描。解决写一个HasRemainingMoves()函数每轮点击结束后调用BOOL HasRemainingMoves() { for (int i 0; i ROWS; i) for (int j 0; j COLS; j) if (m_board[i][j] ! 0) for (int r i; r ROWS; r) for (int c (r i) ? j 1 : 0; c COLS; c) if (m_board[r][c] m_board[i][j] CanConnect(i, j, r, c)) return TRUE; // 找到一组可消除对 return FALSE; }注意这个函数是 O(n^4) 级别的暴力扫描对 8×10 棋盘的问题规模80 个格子完全无所谓跑起来瞬间完成。但如果你想把它用在更大棋盘如 20×30建议优化为先按图案类型分组只比对同类型的格子。6. 进阶技巧把工程从 VC6 迁移到高版本并加计时器先给一个我认为最值得试的进阶方向——把这份 VC6 老工程迁到 VS2019/2022 上跑现代 MFC。直接打开.dsp必然不兼容但代码本身具备可迁移性。我的操作路径是新建一个「MFC 应用程序 → 单文档 → 使用工具栏」工程把MyLlkDoc.cpp、MyLlkView.cpp、MainFrm.cpp中的类代码整体替换进新工程的同名文件资源脚本.rc里的符号定义IDR_MAINFRAME、菜单项可以直接复用但gameNew.bmp这类自定义位图需要手动添加到新工程的资源视图中。Visual C 2010 SP1 Redistributable 是什么角色呢它不是你项目编译所需的库而是目标机器运行时需要安装的 VC 运行库。开发机上编译出的 MFC 程序默认动态链接到mfc140u.dll和msvcp140.dll分发到其他机器时对方必须先装对应版本的 Microsoft Visual C Redistributable。所以在这个迁移流程里我强烈建议把工程的运行库设为静态链接属性 → C/C → 代码生成 → 运行库 → 多线程静态/MT这样目标机就不需要装运行库了。另一个值得做的改进是给游戏加计时器。MFC 里最直接的做法是SetTimer(1, 1000, NULL)在OnInitialUpdate中启动然后响应WM_TIMER// 在视图类中添加计时器相关成员和响应 void CMyLlkView::OnInitialUpdate() { CView::OnInitialUpdate(); SetTimer(1, 1000, NULL); // 每秒触发一次 m_nElapsed 0; } void CMyLlkView::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1 m_bGameActive) { m_nElapsed; CString str; str.Format(_T(用时: %d 秒), m_nElapsed); GetParent()-SetWindowText(str); // 在主窗口标题栏显示 } CView::OnTimer(nIDEvent); }这个技巧的实际价值在于你不需要额外引入任何控件只是复用主窗口标题栏做显示实现成本几乎为零。如果你想进一步把它做成完整的对战版可以用CStatusBar的三个窗格分别显示时间、得分和剩余对数。最后说一个我自己的坑中教训。大学时第一次拿到这份资源急着想看到游戏跑起来于是把所有文件一股脑拖进 VS2015 里点编译——结果报了几十行错误我又花了整个下午去改头文件包含路径和项目属性最后发现那套折腾完全没有意义。后来我换了思路老老实实用 VC6 打开MyLlk.dsw改完代码直接按 F7 编译十几秒就能看到效果那些「版本不兼容」的问题根本不存在。从那以后我每次拿到老工程都强制先看文件里有没有.dsw/.dsp有就先判断要不要专门装个老环境而不是硬着头皮迁新工程。希望这份资源和这套拆解思路能帮到你。本文还有配套的精品资源点击获取
返回列表