
简介这是一份基于MFC框架开发的简易迷宫游戏源码资源面向正在学习C、Windows程序设计和MFC入门的开发者。项目通过文档/视图架构组织代码利用CWnd进行窗口管理结合CPen、CBrush等GDI类完成迷宫地图绘制并借助ON_COMMAND等消息宏实现键盘或鼠标交互适合作为理解事件驱动编程与面向对象设计的练手案例。压缩包共45个文件约2.13MB包含7个h头文件、6个cpp源文件、6个obj编译中间文件以及png、bmp、ico等图片与图标资源另有rc资源脚本、dsp/dsw工程文件和可执行exe覆盖从源码到运行结果的完整工程结构。资源中附有分步骤的png示意图展示游戏思路与自定义迷宫的创建过程玩家类与迷宫数据结构分离清晰便于读者分析DFS或BFS路径搜索的实现方式。目前已有378人学习下载适合希望从零梳理MFC项目组织、消息映射与图形绘制流程的初学者参考。1. 用 MFC 手搓一个迷宫游戏从对话框到键盘响应的最小闭环很多人第一次接触 Windows 桌面开发都是从 MFC 开始的。但真正让人卡住的往往不是语法而是“消息循环到底怎么转”“键盘按下去了窗口怎么没反应”。这个用 MFC 做的简单迷宫游戏恰好是一个能把这些机制串起来的最小可运行项目。它不依赖任何第三方引擎纯靠 CDC 绘图、键盘消息和定时器就能跑出一个可玩的小游戏。适合刚学完 C 语法、想找一个能直接编译运行的 MFC 练手项目的开发者也适合想回头补一补 Windows 消息机制的老手。整个项目结构不复杂但麻雀虽小消息映射、双缓冲绘图、碰撞检测这些该有的都有。2. 项目结构与技术选型为什么是 MFC 而不是 Qt2.1 技术栈拆解这个项目用的是最经典的 MFC 对话框程序Dialog-based不是单文档/多文档架构。选对话框程序的原因很直接迷宫游戏本质上就是一个固定大小的窗口不需要菜单栏、工具栏、状态栏那一套框架对话框模板反而更轻。核心依赖只有三块MFC 库提供 CWnd、CDC、CPaintDC 这些基础类负责窗口创建和绘图上下文。GDI 绘图用 CDC 的 Rectangle、Ellipse、TextOut 等接口画墙、画玩家、画终点。Windows 消息机制WM_KEYDOWN 处理方向键WM_TIMER 驱动游戏循环WM_PAINT 负责重绘。常见做法是把游戏逻辑和界面代码分开但在这个体量的项目里很多人会直接写在对话框类的成员函数里。我一般会建议至少把地图数据和渲染拆成两个函数不然后面改地图会很痛苦。2.2 目录结构说明项目文件不多典型的 MFC 工程结构MazeGame/ ├── MazeGame.sln ├── MazeGame/ │ ├── MazeGame.h // 主对话框类声明 │ ├── MazeGame.cpp // 主对话框类实现游戏逻辑核心 │ ├── MazeGame.rc // 资源文件对话框模板在这里 │ ├── resource.h // 资源 ID 定义 │ ├── stdafx.h // 预编译头 │ └── res/ │ └── MazeGame.ico真正要看的只有MazeGameDlg.h和MazeGameDlg.cpp两个文件。地图数据一般用一个二维数组表示0 代表通路1 代表墙2 代表终点。玩家位置用两个 int 存行列坐标。2.3 对话框初始化与地图加载在OnInitDialog里做三件事初始化地图数组、设置玩家起点、启动定时器。BOOL CMazeGameDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 初始化地图1 表示墙0 表示通路2 表示终点 int mapData[MAP_ROWS][MAP_COLS] { {1,1,1,1,1,1,1,1,1,1}, {1,0,0,0,1,0,0,0,0,1}, {1,0,1,0,1,0,1,1,0,1}, {1,0,1,0,0,0,1,0,0,1}, {1,0,1,1,1,0,1,0,1,1}, {1,0,0,0,1,0,0,0,0,1}, {1,1,1,0,1,1,1,1,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,1,1,1,1,1,1,2,1}, {1,1,1,1,1,1,1,1,1,1} }; memcpy(m_map, mapData, sizeof(mapData)); m_playerRow 1; // 玩家起始行 m_playerCol 1; // 玩家起始列 m_bGameOver FALSE; SetTimer(TIMER_GAME, 100, NULL); // 100ms 刷新一次 return TRUE; }这里MAP_ROWS和MAP_COLS是宏定义一般设成 10。定时器 ID 用TIMER_GAME这种有名字的常量别直接写 1后面加第二个定时器时会后悔。地图数组用memcpy拷到成员变量里是因为局部数组出了函数就没了而OnPaint里还要用它来画墙。注意OnInitDialog返回值必须是 TRUE除非你手动调用了SetFocus并想返回 FALSE。这个坑我见过不止一个人踩。3. 核心模块拆解绘图、键盘与碰撞检测3.1 双缓冲绘图避免闪烁直接在每个OnPaint里用CPaintDC画图窗口刷新时会闪得厉害。标准做法是双缓冲先在内存 DC 里画好整张图再一次性 BitBlt 到屏幕。void CMazeGameDlg::OnPaint() { CPaintDC dc(this); CRect rect; GetClientRect(rect); // 创建内存 DC 和兼容位图 CDC memDC; memDC.CreateCompatibleDC(dc); CBitmap bmp; bmp.CreateCompatibleBitmap(dc, rect.Width(), rect.Height()); memDC.SelectObject(bmp); // 先填充背景 memDC.FillSolidRect(rect, RGB(240, 240, 240)); // 画迷宫 for (int i 0; i MAP_ROWS; i) { for (int j 0; j MAP_COLS; j) { int x j * CELL_SIZE; int y i * CELL_SIZE; if (m_map[i][j] 1) { memDC.FillSolidRect(x, y, CELL_SIZE, CELL_SIZE, RGB(60, 60, 60)); } else if (m_map[i][j] 2) { memDC.FillSolidRect(x, y, CELL_SIZE, CELL_SIZE, RGB(0, 180, 0)); } } } // 画玩家 int px m_playerCol * CELL_SIZE; int py m_playerRow * CELL_SIZE; memDC.FillSolidRect(px 4, py 4, CELL_SIZE - 8, CELL_SIZE - 8, RGB(220, 60, 60)); // 一次性贴到屏幕 dc.BitBlt(0, 0, rect.Width(), rect.Height(), memDC, 0, 0, SRCCOPY); }CELL_SIZE是每个格子的像素大小一般设 40 或 50。FillSolidRect比Rectangle少画边框性能更好。玩家方块留了 4 像素边距视觉上不会和墙贴死。内存 DC 用完不需要手动 DeleteCDC 和 CBitmap 的析构函数会处理但如果你在循环里反复创建那就要注意了。3.2 键盘消息处理MFC 对话框默认不接收方向键的WM_KEYDOWN因为方向键会被用来在控件之间切换焦点。解决办法是重写PreTranslateMessage。BOOL CMazeGameDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg-message WM_KEYDOWN !m_bGameOver) { int newRow m_playerRow; int newCol m_playerCol; switch (pMsg-wParam) { case VK_UP: newRow--; break; case VK_DOWN: newRow; break; case VK_LEFT: newCol--; break; case VK_RIGHT: newCol; break; default: return CDialogEx::PreTranslateMessage(pMsg); } // 碰撞检测撞墙就不动 if (m_map[newRow][newCol] ! 1) { m_playerRow newRow; m_playerCol newCol; // 到达终点 if (m_map[newRow][newCol] 2) { m_bGameOver TRUE; KillTimer(TIMER_GAME); MessageBox(_T(恭喜通关), _T(提示), MB_OK); } Invalidate(); // 触发重绘 } return TRUE; // 消息已处理不再传递 } return CDialogEx::PreTranslateMessage(pMsg); }PreTranslateMessage是在消息进入窗口过程之前拦截的所以能拿到方向键。Invalidate()会触发OnPaint但不会立即执行Windows 会合并多次无效区域。碰撞检测只判断了目标格子是不是墙没有做边界检查——因为地图最外圈全是墙玩家走不出去。如果你改地图时把外圈拆了记得补上边界判断不然数组越界就是另一个故事了。3.3 定时器与游戏循环这个项目用SetTimer驱动一个简单的游戏循环主要用来做两件事刷新画面和处理计时逻辑。void CMazeGameDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent TIMER_GAME) { // 这里可以加计时、动画等逻辑 // 当前版本只做重绘 Invalidate(); } CDialogEx::OnTimer(nIDEvent); }100ms 的间隔对于迷宫游戏来说足够了人眼看起来是连续的。如果你要做移动动画可以把间隔调到 30ms 左右然后在OnTimer里更新玩家像素偏移量。但那样就得把玩家坐标从格子坐标改成像素坐标复杂度会上一个台阶。这个版本保持格子对齐逻辑最简单。提示KillTimer一定要在OnDestroy或OnCancel里再调一次防止窗口关闭后定时器还在跑。4. 编译运行与常见问题排查4.1 环境配置与编译步骤这个项目需要 Visual Studio推荐 2017 及以上版本社区版就够。安装时记得勾选“使用 C 的桌面开发”工作负载MFC 组件在单个组件里手动勾上。编译步骤用 VS 打开MazeGame.sln。确认解决方案平台是 x86 或 x64Debug 或 Release 都行。如果提示找不到 MFC 头文件右键项目 → 属性 → 配置属性 → 常规 → “使用 MFC”改成“在共享 DLL 中使用 MFC”。F5 运行。如果编译报错fatal error C1083: 无法打开包括文件: afxwin.h就是 MFC 组件没装。打开 VS Installer修改 → 单个组件 → 搜索 MFC → 勾选对应版本。4.2 常见问题排查现象一方向键按了没反应。原因对话框默认把方向键当焦点切换处理了。 解决确认PreTranslateMessage已经重写并且返回了 TRUE。如果还是不行检查对话框上是不是有按钮控件抢了焦点可以在OnInitDialog里把焦点设到对话框本身。现象二窗口闪烁严重。原因OnPaint里直接往CPaintDC上画没有用双缓冲。 解决按 3.1 的写法加内存 DC。另外在OnEraseBkgnd里直接返回 TRUE阻止系统擦背景。现象三玩家能穿墙。原因碰撞检测写成了if (m_map[newRow][newCol] 0)才移动但终点格是 2也会被挡住。 解决判断条件改成! 1这样通路和终点都能走。现象四通关后还能继续走。原因m_bGameOver标志没在键盘处理里判断。 解决在PreTranslateMessage开头加!m_bGameOver条件通关后直接忽略按键。现象五Release 模式下地图显示错乱。原因局部数组mapData在 Debug 下可能被零初始化Release 下不会。 解决确保memcpy的长度用sizeof(mapData)并且成员数组大小和局部数组完全一致。5. 二次开发改造从固定地图到可编辑关卡这个项目最值得改的地方就是地图。现在地图是硬编码在OnInitDialog里的想换个迷宫就得重新编译。我一般会把它改成从文本文件读取这样关卡设计就独立出来了。void CMazeGameDlg::LoadMapFromFile(LPCTSTR filePath) { CStdioFile file; if (!file.Open(filePath, CFile::modeRead | CFile::typeText)) { AfxMessageBox(_T(地图文件打开失败)); return; } CString line; int row 0; while (file.ReadString(line) row MAP_ROWS) { for (int col 0; col MAP_COLS col line.GetLength(); col) { m_map[row][col] line[col] - _T(0); // 字符转数字 } row; } file.Close(); }地图文件格式就是每行一串数字比如1111111111表示一行墙。这样改关卡不用动代码用记事本就能编。CStdioFile是 MFC 自带的文件类处理文本比ifstream在 Unicode 环境下更省心。另一个改造方向是加计时和步数统计。在OnTimer里累加时间在键盘处理里累加步数然后在OnPaint里用TextOut画到窗口顶部。注意TextOut要放在双缓冲的memDC上不然文字会闪。// 在 OnPaint 的双缓冲绘制部分加入 CString info; info.Format(_T(步数: %d 时间: %d 秒), m_stepCount, m_elapsedSeconds); memDC.SetBkMode(TRANSPARENT); memDC.TextOut(10, 10, info);SetBkMode(TRANSPARENT)让文字背景透明不然会有一个白底方块盖住迷宫。m_elapsedSeconds在OnTimer里每 10 次 tick 加 1因为定时器是 100ms。还有一个容易忽略的点窗口大小。对话框模板默认大小可能和地图尺寸对不上导致迷宫画不全。我一般会在OnInitDialog里根据MAP_ROWS * CELL_SIZE和MAP_COLS * CELL_SIZE动态调整窗口客户区大小。// 在 OnInitDialog 末尾调整窗口大小 CRect rect(0, 0, MAP_COLS * CELL_SIZE, MAP_ROWS * CELL_SIZE 40); AdjustWindowRect(rect, GetStyle(), FALSE); SetWindowPos(NULL, 0, 0, rect.Width(), rect.Height(), SWP_NOMOVE | SWP_NOZORDER);40是给顶部信息栏留的空间。AdjustWindowRect会把客户区尺寸换算成包含边框和标题栏的窗口尺寸。这一步不做的话地图右下角会被截掉而且不同 DPI 下表现还不一样。从那以后我每次写 MFC 绘图程序都会先把双缓冲和窗口尺寸自适应这两步做掉再写业务逻辑。不然等地图画出来再回头改OnPaint里已经堆了一堆代码动起来很容易翻车。希望这个迷宫项目能帮你把 MFC 的消息机制和 GDI 绘图跑通后面再改什么功能心里都有底。本文还有配套的精品资源点击获取