简介:一份基于MFC实现的迷宫游戏项目资料包,面向正在学习Windows程序设计、C++面向对象和基础游戏开发的初学者,用来理解MFC窗口框架与迷宫搜索算法的落地写法。包内共45个文件,大小2.13MB,包含7个h头文件与6个cpp源文件,另有bmp/png界面资源、图标、工程配置文件和可执行exe。可直接运行exe查看效果,也能用配套工程重新编译调试。已有378人学习下载。压缩包附ReadMe和游戏思路文档,并提供迷宫修改步骤截图、player类实现等,方便对照学习文档视图结构、消息映射、图形绘制等关键知识点。游戏核心涉及二维数组迷宫建模、DFS/BFS路径搜索、状态记录与键盘交互,代码中均有对应实现,内含从工程建立到迷宫自定义的步骤截图,适合动手拆解和改造。整体目录清晰,既能作为课程设计参考,也能帮助读者把MFC界面与路径搜索逻辑串起来。
1. 一个MFC迷宫游戏:从“画格子”到“可玩的寻路Demo”
如果你翻到过一个用 MFC 写的迷宫游戏源码,大概率会有两种反应:要么觉得“这年头谁还用 MFC”,要么就是打开工程后不知道从哪个文件开始看。我拆过这样一个小项目,它的核心是一个对话框程序,里面用二维数组存迷宫地图,用递归回溯生成迷宫,再用 OnPaint 把墙和路径画出来,最后靠 OnKeyDown 响应方向键控制玩家移动。虽然界面朴素,但麻雀虽小五脏俱全。适合刚学完 C++ 想看看 Windows 窗口程序怎么处理输入输出的人,也适合想快速把算法可视化的人——它让你在 MFC 里同时练习了数据建模、绘图、消息映射三件事。这篇文章会把它的工程结构、核心代码、运行方式和改法全部拆开讲。
2. 项目结构剖析:MFC对话框程序如何组织迷宫核心逻辑
2.1 目录结构与关键文件职责
这个项目不是单文件硬怼,而是标准的 MFC 对话框应用程序结构。创建一个基于对话框的工程后,核心文件长这样:
MazeGame/ ├── MazeGame.sln ├── MazeGame/ │ ├── MazeGame.h // CWinApp 派生类声明 │ ├── MazeGame.cpp // 应用入口,初始化 CWinApp │ ├── MazeGameDlg.h // 主对话框类 CMazeGameDlg 声明 │ ├── MazeGameDlg.cpp // 迷宫生成、绘制、按键处理全在这 │ ├── Resource.h // 资源 ID 定义 │ ├── res/ │ │ └── MazeGame.rc2 // 资源脚本 │ └── res/MazeGame.ico // 图标MazeGameDlg.cpp 是绝对的主角,迷宫的数据、算法、绘制、交互全在这个文件里,加起来大概三四百行。MazeGame.cpp 只是标准模板,负责创建一个 CMazeGameDlg 实例并进入消息循环。也就是说,你要读懂这个项目,百分之八十的时间都在看 MazeGameDlg.cpp。
为什么 MFC 项目喜欢把所有东西堆在 Dlg 文件里?因为对话框程序的消息响应天然集中,OnInitDialog 负责初始化,OnPaint 负责绘制,OnKeyDown 负责按键,逻辑上是一条直线,不像文档视图结构还要拆视图和文档类。对小游戏来说,这种“一切从对话框出发”的写法反而是最直接的。
2.2 MFC的CWinApp、CDialogEx与视图分离思路
这个项目用到了两个核心 MFC 类:CWinApp 和 CDialogEx。CWinApp 是整个应用的“总开关”,它的 InitInstance 函数里写下这样一段:
BOOL CMazeGameApp::InitInstance() { CWinApp::InitInstance(); CMazeGameDlg dlg; m_pMainWnd = &dlg; dlg.DoModal(); // 弹出对话框,进入模态循环 return FALSE; }DoModal 一调用,程序主窗口就是迷宫对话框,关闭对话框等于退出进程。模态对话框的好处是不用自己写窗口过程,MFC 把 WM_PAINT、WM_KEYDOWN 这些消息封装成虚函数,你只需要重写对应的成员函数。
CDialogEx 是对话框基类,OnInitDialog 在这里被调用。你可以在里面做地图初始化、设置窗口标题、限制窗口大小。需要注意,对话框程序默认焦点在一个按钮或编辑框上,方向键消息可能被控件吞掉。这个项目里通常会在 OnInitDialog 的最后调用SetFocus()或者把焦点设回对话框本身,否则后面按键根本进不到 OnKeyDown,这是一个很典型的 MFC 交互坑,后面避坑章节会细说。
视图分离的思路在这里并不明显,但你可以手动拆:把迷宫生成算法放到一个独立的 MazeGenerator 类,把绘制逻辑放到一个自定义控件 CStatic 派生类,对话框只做消息转发。不过原作者没有这样做,全部平铺在 Dlg 里。优点是看代码时上下文连续,缺点是想复用算法时得复制粘贴。所以我建议你在拿到这份源码后先不要急着重构,先把它的流程跑通,再考虑拆类。
2.3 迷宫数据模型:二维数组与墙体表示
迷宫的地图数据是这个项目的“心脏”。它用了一个二维数组来表示每个格子,常见做法是每个格子记录四面墙是否存在。先看定义:
#define ROWS 15 #define COLS 15 // 每个格子的墙标志:上、右、下、左 bool maze[ROWS][COLS][4]; // 格子访问标记,生成迷宫时用 bool visited[ROWS][COLS]; // 玩家坐标 int playerX, playerY;maze[x][y][0] 表示 (x,y) 的上墙,[1] 是右墙,[2] 是下墙,[3] 是左墙。值为 true 表示有墙,false 表示通路。这种表示法非常直观,画墙的时候只需要遍历每个格子,判断相邻格子之间是否有墙即可。另一种做法是用二进制位表示墙,比如 0x01 代表上墙、0x02 代表右墙等,这样占内存小,但可读性差。这份源码用的是 bool 四维数组,牺牲了一点空间换来清晰的逻辑。
玩家坐标也直接存成两个 int,初始位置是 (0,0),终点是 (ROWS-1, COLS-1)。移动时只要修改 playerX 和 playerY,然后调用 Invalidate() 触发重绘,不需要维护复杂的精灵对象。这种“坐标即状态”的做法在简单游戏里非常实用,别一上来就想封装成类,先能跑再抽象。
3. 核心模块拆解:迷宫生成、绘制与玩家移动的完整链路
3.1 迷宫生成算法:递归回溯的代码实现
这个项目生成迷宫用的是递归回溯,也就是深度优先搜索的变种。算法核心思想是:从起点格子出发,随机选择一个未访问的相邻格子,打通它们之间的墙,然后递归进入那个格子;如果当前格子四周都访问过,就回退到上一个格子。最终每个格子都能被访问到,而且任意两个格子之间只有一条通路。
下面这段是典型实现:
// 方向偏移:上、右、下、左 int dx[4] = {0, 1, 0, -1}; int dy[4] = {-1, 0, 1, 0}; // 递归生成迷宫 void CMazeGameDlg::GenerateMaze(int x, int y) { visited[x][y] = true; // 随机打乱四个方向,保证迷宫形状不固定 int order[4] = {0, 1, 2, 3}; for (int i = 3; i > 0; i--) { int j = rand() % (i + 1); std::swap(order[i], order[j]); } for (int i = 0; i < 4; i++) { int dir = order[i]; int nx = x + dx[dir]; int ny = y + dy[dir]; // 必须在地图范围内且未访问 if (nx >= 0 && nx < ROWS && ny >= 0 && ny < COLS && !visited[nx][ny]) { // 打通当前格子和下一个格子之间的墙 maze[x][y][dir] = false; maze[nx][ny][(dir + 2) % 4] = false; // 反向墙也打通 GenerateMaze(nx, ny); } } }参数 x, y 是当前格子坐标。order 数组随机打乱后,每次选择的探索方向都不同,所以每次生成的迷宫都不一样。关键的打通墙逻辑是:当前格子朝 dir 方向的墙设为 false,同时下一个格子朝向当前格子的反向墙也设为 false,这样两个格子之间就完全畅通了。
递归回溯的缺点是深度太深时可能栈溢出,对于 15×15 的迷宫来说完全没有问题。如果你想生成更大的迷宫,可以把递归改成显式栈。另外 rand() 的随机性比较弱,正式做项目可以用std::mt19937替代,但这份源码教学性质更强,用 rand() 反而更容易理解。生成完毕后,记得手动把起点和终点的外墙或者入口位置处理一下,比如把左上角和右下角的边界墙打开,这样玩家才能从窗口外面走进来。
3.2 OnPaint绘制:双缓冲解决闪烁
绘制迷宫用的是 OnPaint 消息处理函数,MFC 默认会创建 CPaintDC。直接画会导致游戏窗口在每次移动时闪得厉害,因为背景不断被擦除重画。常见的解决方式是双缓冲:先在内存中画好整幅图,再一次 BitBlt 到屏幕上。这份代码里也用了这个技巧。
void CMazeGameDlg::OnPaint() { CPaintDC dc(this); // 设备上下文 // 创建内存画布,防止闪烁 CDC memDC; memDC.CreateCompatibleDC(&dc); CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, clientWidth, clientHeight); CBitmap* pOldBmp = memDC.SelectObject(&bmp); // 清空背景 CBrush bgBrush(RGB(240, 240, 240)); memDC.FillRect(CRect(0, 0, clientWidth, clientHeight), &bgBrush); int cellW = clientWidth / COLS; int cellH = clientHeight / ROWS; // 画墙 for (int x = 0; x < ROWS; x++) { for (int y = 0; y < COLS; y++) { CPen pen(PS_SOLID, 2, RGB(0, 0, 0)); memDC.SelectObject(&pen); // 上墙 if (maze[x][y][0]) { memDC.MoveTo(x * cellW, y * cellH); memDC.LineTo((x + 1) * cellW, y * cellH); } // 右墙 if (maze[x][y][1]) { memDC.MoveTo((x + 1) * cellW, y * cellH); memDC.LineTo((x + 1) * cellW, (y + 1) * cellH); } // 下墙 if (maze[x][y][2]) { memDC.MoveTo(x * cellW, (y + 1) * cellH); memDC.LineTo((x + 1) * cellW, (y + 1) * cellH); } // 左墙 if (maze[x][y][3]) { memDC.MoveTo(x * cellW, y * cellH); memDC.LineTo(x * cellW, (y + 1) * cellH); } } } // 画玩家:一个蓝色方块 CBrush playerBrush(RGB(50, 100, 200)); memDC.SelectObject(&playerBrush); memDC.Rectangle(playerX * cellW + cellW/4, playerY * cellH + cellH/4, (playerX + 1) * cellW - cellW/4, (playerY + 1) * cellH - cellH/4); // 画终点:红色方块 CBrush endBrush(RGB(200, 50, 50)); memDC.SelectObject(&endBrush); memDC.Rectangle((ROWS-1) * cellW + cellW/4, (COLS-1) * cellH + cellH/4, ROWS * cellW - cellW/4, COLS * cellH - cellH/4); // 一次性拷贝到屏幕 dc.BitBlt(0, 0, clientWidth, clientHeight, &memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBmp); bmp.DeleteObject(); }代码里的 clientWidth 和 clientHeight 是在 OnInitDialog 里计算的客户区尺寸,cellW 和 cellH 是每个格子的像素宽高。画墙的逻辑是从每个格子的四个边向外画线,但这样画会导致相邻格子的公共墙被画两次,视觉效果上线条会更粗,不过不影响判断。更优雅的做法是只画右墙和下墙,然后额外画一圈外边框,这样墙就不会重复描边。
双缓冲的关键是 CreateCompatibleDC 和 CreateCompatibleBitmap,内存 DC 与屏幕 DC 兼容后,所有绘制都在内存里完成,最后 BitBlt 一次提交。实际跑起来你会发现,即使玩家每帧都在移动,窗口也不会闪烁。这个技巧在 MFC 游戏里是保命级别的,值得单独抄下来。
3.3 键盘响应与碰撞检测:OnKeyDown里的方向处理
玩家操控用的是 OnKeyDown 消息,当对话框拥有焦点时,按下方向键就会触发。处理逻辑是:先算出目标坐标,再检查目标格子与当前格子之间有没有墙,没有墙才允许移动。下面是核心代码:
void CMazeGameDlg::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { int newX = playerX; int newY = playerY; switch (nChar) { case VK_UP: newY = playerY - 1; if (!maze[playerX][playerY][0]) // 上墙没有阻挡 { playerY = newY; } break; case VK_DOWN: newY = playerY + 1; if (!maze[playerX][playerY][2]) // 下墙没有阻挡 { playerY = newY; } break; case VK_LEFT: newX = playerX - 1; if (!maze[playerX][playerY][3]) // 左墙没有阻挡 { playerX = newX; } break; case VK_RIGHT: newX = playerX + 1; if (!maze[playerX][playerY][1]) // 右墙没有阻挡 { playerX = newX; } break; default: break; } // 检查是否到达终点 if (playerX == ROWS - 1 && playerY == COLS - 1) { AfxMessageBox(_T("恭喜通关!")); // 重新生成迷宫并重置玩家位置 ResetGame(); } Invalidate(); // 触发重绘 CDialogEx::OnKeyDown(nChar, nRepCnt, nFlags); }注意这里有一个很容易犯的错:先调用 Invalidate 再调用基类的 OnKeyDown。如果你先让基类处理,消息可能会被默认逻辑吞掉或者产生光标闪烁,所以我们自己处理完立即重绘。newX 和 newY 其实没有真正用上,因为判断墙之后直接修改了 playerX/playerY,这个变量属于冗余代码,新手经常写这种“明明可以直接改,偏要算个临时变量”的代码,无伤大雅。
还有一个隐藏问题:如果玩家站在边界并且方向上是外墙,比如 x=0 时按左键,maze[0][y][3] 是 true,所以不会越界。但生成迷宫时并没有特意把外圈墙全部置为 true,如果你的初始化代码里忘了给外圈赋值,玩家就会走出地图。所以初始化时最好把所有墙先置为 true,然后再生成迷宫打通内部墙,这样边界墙天然是 true,就不会越界了。
4. 运行与调试:从VS编译到键盘控制的完整上手步骤
4.1 环境准备与编译配置
这个项目是 MFC 对话框程序,最适合用 Visual Studio 打开。我用的是 VS2019,配置为“Debug - x86”或“Release - x86”。拿到源码后,不要直接双击 .sln,先检查后缀版本。如果是老版本的 .dsw/.dsp,需要让 VS 做一次转换,转换过程中可能弹出“是否升级”提示,选确定就行。
打开工程后,按 Ctrl+Shift+B 编译。常见错误集中在两点:一是 MFC 头文件找不到,需要在项目属性 → 常规 → MFC 的使用 里选“在静态库中使用 MFC”;二是字符集不匹配,老项目默认使用多字节字符集,而新 VS 默认 Unicode,这会导致AfxMessageBox(_T(""))等宏报错。解决办法是把项目属性 → 常规 → 字符集改成“使用多字节字符集”,或者把所有字符串都包上_T()。由于源码里多半已经用了 _T(),改成多字节字符集通常最省事。
如果编译时提示“无法打开 afxwin.h”,说明工程没有安装 MFC 组件。打开 Visual Studio Installer,找到“使用 C++ 的桌面开发”,右侧勾选“适用于最新 v142 生成工具的 C++ MFC (x86 和 x64)”,装完后重启 VS 再编译。
4.2 运行参数与窗口标题修改
编译通过后,直接按 F5 运行。窗口顶部默认标题是“MazeGame”或者你在工程名里看到的资源字符串。这个标题来自对话框资源的 Caption 属性,不是代码里的。如果要改标题,双击 .rc 文件中的对话框资源,在属性窗口修改 Caption 文本。
窗口的大小同样在资源编辑器里调整。如果有自定义的启动尺寸,在 OnInitDialog 里可以调用SetWindowPos。比如我要让窗口刚好显示 15×15 个格子,每个格子 32 像素,那么客户区宽高至少是 15×32=480,再加上窗口边框和标题栏,最终窗口尺寸可以设为 496×535。这个数值不是固定的,主要看你格子的像素数和迷宫的行列数。
运行后会看到一个灰色背景的格子图,玩家是一个蓝色方块,终点是红色方块。此时用鼠标点击一下对话框窗口(确保焦点在对话框上),然后用方向键移动。如果点击后方向键没反应,多半是焦点在一个看不见的按钮或编辑框上,需要按 Tab 切换焦点或者点击窗口空白区域,这也解释了为什么很多教程在 OnInitDialog 里强制 SetFocus。
4.3 常见问题排查:字符集、资源ID、焦点丢失
先说字符集。如果你在 Debug 模式下遇到一堆 C2664 或 C2440 错误,错误信息里提到const char *转CString,基本就是 Unicode/多字节的问题。我的习惯是直接改用 Unicode,然后把代码里所有字符串字面量包上_T()。老代码可能用LPSTR接收字符串,在 Unicode 下会报类型不匹配,需要改为LPTSTR或者TCHAR[]。
资源 ID 的问题更隐蔽。MFC 对话框程序的 .rc 文件里定义了IDD_MAZEGAME_DIALOG等 ID,这些数字在 Resource.h 里。如果你从别的工程拷贝了 Dlg 文件,但没同步拷贝 Resource.h,编译时会出现IDD_XXXX 未定义。解决办法是打开 Resource.h,检查所有#define IDD_... 数字是否与源文件引用一致,直接照抄#define IDD_MAZEGAME_DIALOG 102这类定义即可。不要小看这个问题,我见过有人把整个 .rc 文件删了然后自己手工写对话框的,纯属自找麻烦。
焦点丢失是运行期最难受的问题。现象是对话框显示正常,但按方向键毫无反应,窗口也不重绘。原因通常是 OnInitDialog 里创建了一个按钮控件,按钮默认抢走了焦点,而按钮不会把方向键传给父窗口。解决方式是在 OnInitDialog 的末尾加上一句:
SetFocus(); // 把焦点还给对话框本身但 SetFocus 之后按钮仍然存在,按 Tab 可能又切走。更稳的办法是把按钮设为不可见,或者干脆用CWnd::OnKeyDown的默认实现。还有一个土办法:在按钮的 WM_KEYDOWN 处理里手动发消息给父窗口,但那样代码会很啰嗦。我建议这个项目里不要用 Button 控件,直接用对话框空白区域响应键盘最省心。
5. 避坑记录:MFC迷宫游戏开发中的五个典型翻车现场
5.1 方向键按下后窗口闪成白板
现象:玩家移动一次后,整个窗口先变白,然后才重新画出来,闪烁严重,甚至出现残影。
原因:没有使用双缓冲,OnPaint 直接画到屏幕 DC 上。每次 Invalidate 触发 WM_ERASEBKGND 和 WM_PAINT,先擦背景再绘制,一擦一画之间屏幕就闪了。
解决:在 OnPaint 里先创建内存 DC 和兼容位图,所有绘制画到内存 DC,最后 BitBlt 到屏幕。另外还可以在 OnEraseBkgnd 里直接返回 TRUE,跳过系统擦背景,减少一次无用的清除。
5.2 玩家走到边界外就消失
现象:从左上角往左按,玩家方块不见了,再按几下程序崩溃或画面乱掉。
原因:生成迷宫时没有把外圈墙全部初始化为 true,只初始化了内部墙,导致 m_map[0][0][3] 是 false,左墙判定失效,玩家坐标变成 -1,越界访问数组。
解决:初始化时把所有墙设为 true,再用生成算法打通的墙设为 false。这里有一个顺序要求:先全量置 true,然后递归生成。如果你先打墙再置 true,会把打通的墙重新堵上。所以代码顺序不能乱。
5.3 AfxMessageBox 弹窗卡死整个游戏
现象:玩家到达终点后弹出“恭喜通关”对话框,关掉后游戏画面恢复正常,但再按方向键没反应。
原因:弹出消息框后焦点被消息框抢走,关掉后焦点没有回到对话框,导致键盘消息不再发送到主窗口。
解决:每次重置游戏或弹窗后,需要主动调用SetFocus()或SetForegroundWindow()。更稳妥的方式是弹窗前先把焦点保存在对话框,弹窗结束后恢复焦点。或者用MessageBox的替代方案,比如状态栏文字提示,而不是模态弹窗。
5.4 每次运行生成的迷宫一模一样
现象:第一次运行迷宫是某条路径,关掉再开还是同一条路径。
原因:没有调用srand((unsigned)time(NULL))初始化随机数种子,rand() 每次从头开始产生同一组伪随机数,打乱方向的顺序自然不变。
解决:在 OnInitDialog 里加srand(static_cast<unsigned>(time(nullptr)))。注意不要放在 GenerateMaze 里,那样每次递归都会重置种子,反而更容易出现重复序列。放在初始化函数开头只调用一次即可。
5.5 MFC 程序编译通过但双击 exe 报缺少 DLL
现象:在 Release 模式下生成 exe,拿到别的电脑运行提示“找不到 mfc140u.dll”或“vcruntime140.dll”。
原因:默认是动态链接 MFC 和 CRT,系统没有安装对应运行库。
解决:在项目属性 → 常规 → MFC 的使用 改成“在静态库中使用 MFC”,同时“C++ 语言标准”保持默认,生成 exe 就不会带外部依赖。如果还需要 CRT 静态链接,在“C/C++ → 代码生成 → 运行库”选“多线程(/MT)”。
6. 二次开发技巧:把静态迷宫升级为可配置关卡与自动寻路
6.1 从硬编码数组改为文件配置
现在的迷宫行列数和墙数据全部写死在代码里,想换地图就得改数组再编译。更实际的改法是支持从文本文件读迷宫。比如文件里用字符表示地图:
# 代表墙 . 代表空地 P 代表玩家起点 E 代表终点void CMazeGameDlg::LoadMazeFromFile(const CString& path) { CFile file; if (!file.Open(path, CFile::modeRead)) { AfxMessageBox(_T("打开地图失败")); return; } CString lineBuf; int row = 0; while (file.ReadString(lineBuf)) { for (int col = 0; col < lineBuf.GetLength(); col++) { TCHAR ch = lineBuf.GetAt(col); if (ch == _T('#')) { SetAllWalls(row, col, true); } else if (ch == _T('P')) { playerX = row; playerY = col; } else if (ch == _T('E')) { endX = row; endY = col; } } row++; } file.Close(); }这样每个关卡就是一个 .txt 文件,做关卡切换只需要换文件路径。我建议把地图渲染逻辑独立成一个DrawMaze()函数,OnPaint 里只负责调用它,这样不管地图来自数组还是文件,绘制部分都不用改。
6.2 自动求解:BFS寻路的高亮显示
想要一键演示通关路径,可以用广度优先搜索(BFS)算出最短路径,然后把路径格子高亮出来。这一步几乎是最能体现“MFC 不只是画格子”的改法:
// 返回路径坐标的 vector std::vector<CPoint> CMazeGameDlg::FindPathBFS() { int dist[ROWS][COLS]; int prev[ROWS][COLS]; std::queue<CPoint> q; std::vector<CPoint> path; memset(dist, -1, sizeof(dist)); dist[playerX][playerY] = 0; q.push(CPoint(playerX, playerY)); while (!q.empty()) { CPoint cur = q.front(); q.pop(); if (cur.x == endX && cur.y == endY) { // 回溯 prev 数组,构造路径 int tx = endX, ty = endY; while (tx != playerX || ty != playerY) { path.push_back(CPoint(tx, ty)); int idx = prev[tx][ty]; tx = tx - dx[idx]; ty = ty - dy[idx]; } std::reverse(path.begin(), path.end()); return path; } for (int d = 0; d < 4; d++) { int nx = cur.x + dx[d]; int ny = cur.y + dy[d]; if (nx >= 0 && nx < ROWS && ny >= 0 && ny < COLS && dist[nx][ny] == -1 && !maze[cur.x][cur.y][d]) { dist[nx][ny] = dist[cur.x][cur.y] + 1; prev[nx][ny] = d; // 记录从哪个方向来的 q.push(CPoint(nx, ny)); } } } return path; // 空路径代表无解 }拿到路径后,在 OnPaint 里遍历路径,用黄色小圆点或半透明方块标记每个格子。注意 BFS 里prev记录的是方向编号,回溯时需要反向操作坐标,这块逻辑容易写错,最好用CPoint对象保存“从哪个格子来”,而不是只保存方向。
6.3 改造验证与收尾习惯
改完以后,验证工作不能只看能不能编译。我一般会准备三套地图:一套 5×5 小地图测试边界,一套 15×15 正常地图测试游戏体验,再故意放一个没有出口的地图测试 BFS 空路径处理。如果 BFS 无解,界面要给出提示而不是空白,这点最容易疏忽。
那次我拆完这个 MFC 迷宫项目后,养成了一个习惯:每次拿到别人的 MFC 源码,第一件事是打开 Dlg 文件搜索OnKeyDown和OnPaint,先看清楚输入和输出,再碰生成算法。因为 MFC 程序的“入口”其实分散在消息处理函数里,不从消息链入手很难理清逻辑。从那以后我每次动手改老项目,都强制自己先跑通原版、再做最小修改、最后才重构,这套流程救了我很多次。希望帮到你。
本文还有配套的精品资源,点击获取