简介基于C MFC实现的五子棋游戏完整项目面向学习Windows桌面应用开发的技术人员演示如何利用MFC框架完成棋盘棋子绘制、输赢判定、新游戏、悔棋与棋盘背景样式更换等功能。压缩包共81个文件约7.75MB其中包含C源文件cpp/h、MFC工程配置sln/vcxproj、位图与图标资源bmp/ico、可执行程序exe以及PDF介绍文档等工程结构完整既可直接编译运行也可对照源码理解MFC的界面封装与消息映射机制。目前已有42人学习适合C入门到进阶者参考。项目内附有介绍PDF、源码和可执行程序涵盖棋盘绘制、落子逻辑、五子棋输赢判定、悔棋栈操作以及背景样式切换等关键实现细节代码采用模块化组织预留了扩展接口便于继续添加AI对战、网络联机等功能是学习MFC经典桌面应用的实用示例。1. 基于C MFC的五子棋这套源码为什么值得跑一遍做 Windows 桌面应用绕不开 MFC但能体现 MFC 界面交互和 C 逻辑配合的练手项目五子棋是典型的一个。这套基于 C MFC 实现的五子棋源码覆盖了棋盘棋子绘制、输赢判定、新游戏、悔棋和棋盘背景样式修改这几条主功能线工程文件完整不是零散贴在论坛上的片段直接可以用 Visual Studio 打开编译。对我这种需要快速搭一套“有点技术含量但又不至于失控”的桌面小游戏来研究 MFC 消息映射、GDI 绘图和游戏状态管理的人来说这类源码是最省事的参考。如果你正处在学完 C 语法、想找个完整 MFC 工程看整体结构的阶段或者需要改一个五子棋程序做毕业设计、课程作业这份代码值得完整跑一遍再拆开逐段看。2. 工程结构先摸清从 VCXProj 到 View 类的依赖关系2.1 解决方案文件布局与项目依赖打开压缩包先别急着点编译花几分钟把文件层级看明白再动手后面排错会省很多时间。这个项目采用的是 MFC 单文档视图架构SDI核心文件是05.gobang.sln和05.gobang.vcxproj前者是 Visual Studio 的解决方案入口后者是项目文件定义了编译配置、依赖项和源文件清单。工程里几个关键文件的职责需要分清05.gobangView.cpp和05.gobangView.h承担棋盘绘制、鼠标点击响应、棋子落子和悔棋逻辑的界面层05.gobangDoc.cpp和05.gobangDoc.h负责游戏数据的存储与管理比如当前棋盘状态、当前轮到哪一方MainFrm.cpp负责主框架窗口的创建和菜单、工具栏的装配Resource.h和05.gobang.rc管理菜单 ID、对话框资源、图标和位图资源。我一般会先在 VS 里打开类视图把CWuZiQiView、CWuZiQiDoc、CMainFrame这三个类之间的调用关系捋一遍再开始读具体实现。2.2 编译前必须确认的配置项字符集与 MFC 库类型这套源码在 Debug 目录下直接带了编译产物05.gobang.exe说明开发者用的应该是 Debug 配置。自己动手编译时最容易翻车的就是字符集不匹配和 MFC 库链接方式不对。右键项目打开属性页在“配置属性 → 常规 → 字符集”里确认选的是“使用 Unicode 字符集”还是“使用多字节字符集”这决定了代码里CString、TCHAR到底走哪条编码路径。MFC 库的使用方式也有讲究。在“配置属性 → 常规 → MFC 的使用”里有“使用标准 Windows 库”、“在共享 DLL 中使用 MFC”、“在静态库中使用 MFC”三个选项。这个项目默认设置应该是共享 DLL 方式发布 exe 时需要带上mfc140u.dll对应 VS2015 及以上版本或者对应版本的 MFC 运行时。如果目标机器没装 Visual C Redistributable双击 exe 会直接报“无法启动此程序因为计算机中丢失 VCRUNTIME140.dll”之类的错。想避免依赖就把 MFC 使用改成“在静态库中使用 MFC”但 exe 体积会明显变大Debug 模式下尤其明显。提示如果打开解决方案后提示05.gobang.vcxproj加载失败先检查 Visual Studio 版本。老工程用的平台工具集可能默认是 v120VS2013新版本 VS 需要右键项目 →“重定向项目”选择当前已安装的工具集再编译。2.3 初次编译的预期结果与输出物清单配置正确的情况下F7 编译会得到三类主要输出05.gobang.exe可执行文件、05.gobang.ilk增量链接文件、05.gobang.pdb调试符号文件。前两个在 Debug 目录下第三个也在同目录。ilk和pdb是调试用的发布时不需要带上但自己调试时别删。如果出现链接错误LNK2005之类的重复定义问题多半是某个 .cpp 文件被重复添加到了项目里或者头文件里定义了全局变量没加extern。这类报错在 MFC 工程里很常见优先检查05.gobang.vcxproj.filters里的文件列表是不是混入了重复项。3. 棋盘与棋子绘制GDI 绘图和双缓冲的关键细节3.1 棋盘坐标体系与网格计算看05.gobangView.cpp里的OnDraw函数棋盘并不是直接画满整个客户区而是先计算了边距再按固定间距画网格线。标准的五子棋棋盘是 15×15所以横向和纵向各需要画 15 条线落子点就是这些线的交叉点。源码里通常用BoardSize和CellWidth两个常量控制网格密度和间距改变这两个值就能调整棋盘大小。我在读这类代码时习惯先确认坐标换算关系鼠标点击位置是设备坐标判断落在哪个交叉点时需要减去棋盘起始偏移再除以单元格宽度然后四舍五入取整数索引。源码里鼠标响应函数OnLButtonDown通常会先把点击坐标做一次合法性校验——如果点击位置落在棋盘边界外或者距离交叉点太远超过单元格宽度的三分之一会直接忽略这次点击。这个容差范围是影响操作手感的重要参数太多的代码把判定半径写死了我一般在拿到代码后会把它单独抽成一个m_nClickRadius成员变量方便调手感。3.2 棋子绘制空心圆与实心圆结合的立体感棋子的绘制是GDI绘图的典型场景源码里绘制黑白棋子的逻辑不复杂但有一些细节值得注意。黑棋是用SelectObject选中黑色画刷之后用Ellipse直接画实心圆白棋类似但为了和白色棋盘背景区分开通常会在白棋边缘加一圈灰色或黑色边框不然白棋在浅色棋盘上几乎看不见。源码里的实现往往是在DrawPiece这个函数里先画外圈再用FillSolidRect或者用CreateSolidBrush填充内部。参数上需要关注两个点Ellipse的矩形范围用的是“交叉点坐标加减棋子半径”的方式确定棋子半径一般取单元格宽度的 0.4 倍左右太大会导致相邻棋子重叠太小棋盘显得空旷。源码里如果有个m_nPieceRadius变量调整它的值就能控制棋子大小同时要同步检查落子判断时的容差否则会出现视觉上棋子落在线上但程序认为没点到的情况。另一个细节是落子历史的记录这一步通常在鼠标响应里和绘制一起完成每落一子就压入一个历史栈悔棋时从这个栈里弹出。注意MFC 里所有CPaintDC或CClientDC绘制的图形在窗口被遮挡再恢复后会丢失所以绘制逻辑必须放在OnDraw里让框架在需要重绘时自动调用。源码里如果OnDraw里遍历了整个棋盘数组来重绘所有棋子说明设计是正确的如果只在消息响应里画那运行时拖动窗口就会出现棋子消失的 bug。3.3 双缓冲绘制解决棋盘闪烁问题这是 MFC 绘图绕不开的坑。棋盘 15×15 加上棋子如果直接在OnDraw里逐个调用Ellipse窗口被拖动或者频繁刷新时会出现明显的闪烁。原因是每个绘制操作直接写屏幕多次 GDI 调用之间屏幕被反复擦除和重画。源码里如果已经用了双缓冲——在内存中创建一个兼容位图先在位图上画完整个棋盘再一次BitBlt到屏幕——就不会有这个问题。判断代码是否用了双缓冲有个快速方法看OnDraw里有没有CreateCompatibleDC和CreateCompatibleBitmap这两行调用。如果有说明采用了标准做法如果没有可以自己加上核心思路是把原来的绘制目标从pDC切换到内存 DC最后统一拷贝回去。这段改造一般 20 行左右就能完成但效果立竿见影窗口缩放时棋盘不会再闪。背景样式切换也依赖这个机制——源码里允许用户换棋盘背景图UserImages.bmp换图时加载新的位图到内存 DC重绘时直接贴上去棋盘网格和棋子叠加绘制在位图之上。4. 核心逻辑拆解落子判定、胜负检查与悔棋栈4.1 数据结构选择二维数组 vs 状态对象游戏逻辑的数据结构直接决定了后面所有判断代码的复杂度。源码里最朴素也是最好懂的方案是用一个二维数组存储棋盘状态。MFC 标准写法一般是在CWuZiQiDoc类里维护一个int m_board[15][15]0 表示空1 表示黑棋2 表示白棋。数组访问快判断输赢时直接索引遍历不需要额外的数据结构开销。这类小型游戏用二维数组就是最优解设计模式里的什么状态模式、观察者模式在这个场景下都是过度设计。如果扩展成需要支持网络对战或 AI 落子这个数组就要升级成带坐标和颜色信息的结构体同时要考虑并发访问的问题。但拿到这份源码先把它原有的数组逻辑读透再考虑怎么扩展。源码里棋盘数组的初始化和复位在OnNewGame里完成也就是点“新游戏”菜单时循环把二维数组全部置 0同时清空历史栈。4.2 胜负判定四方向连珠扫描输赢判定是五子棋的核心算法。最常见的实现方式是CheckWin(int row, int col, int color)以当前落子点为中心向四个方向分别扫描——水平、垂直、主对角线、副对角线。每个方向从中心点向两边延伸统计同色棋子的连续数量只要任意一个方向上连子数大于等于 5就判定该方获胜。这份源码里应该也是用这个思路写成一个独立的函数返回bool或者int。需要警惕的效率问题是如果每次落子都扫描整个 15×15 棋盘来做全盘判断代码写起来简单但做了很多无用功只扫描当前落子的四个方向最多检查四个方向的 9 个格子中心点左右各 4 个这样效率更高逻辑也更清晰。我一般会看源码用的是哪种方式如果发现每次全盘扫描可以改成局部扫描代码量不增加但判断速度明显提升。4.3 悔棋功能的实现每一步都记得留后悔药悔棋在思路上很简单用一个栈记录每一步的落子坐标和颜色。源码里通常用CArray或者std::vector模拟栈结构每次落子 push 一个结构体进去悔棋时 pop 出来把对应位置置 0然后刷新界面。需要留意的细节是 MFC 的文档视图架构里悔棋操作需要同时更新 View 层的显示和 Doc 层的数据不能只改界面不更新数组。我拆这类代码时特别关注一个边界情况连续悔棋能不能回到开局状态。如果栈的弹出没有做空栈保护悔棋到第 0 步再点“悔棋”轻则没反应重则数组越界崩溃。源码里有没有做这个判断是个关键的代码质量特征。如果没做加一行if (m_stack.IsEmpty()) return;就能解决。另外要注意悔棋之后当前落子方要切换回去否则会出现悔棋后轮到对手下但实际该自己下的错位问题。4.4 新游戏重置与角色切换“新游戏”功能的实现比看上去多一点讲究。界面上好办把棋盘数组清零、清空悔棋栈、重绘窗口就行但有几个状态标记要考虑当前是谁的回合、游戏是否已经结束有人赢了之后棋盘应该被锁定不能继续落子、棋盘背景是否保留用户自定义的那张图。源码里如果没有维护一个m_bGameOver之类的布尔变量那赢棋之后还能继续落子的 bug 大概率存在这是一个需要测的点。角色切换逻辑比较直观每成功落一子m_nCurrentPlayer就从 1 变 2 或从 2 变 1。如果源码里用m_nCurrentPlayer ^ 1这种位运算或者(m_nCurrentPlayer 1) ? 2 : 1三目运算读起来都很清晰。注意悔棋时这个切换也要同步被悔掉的那一方的回合要恢复。5. 改代码避坑指南MFC 五子棋的五个常见翻车现场5.1 点击棋盘没反应落子位置偏了半个格子现象是鼠标点到交叉点附近程序要么没反应要么棋子在偏离点击位置的地方。原因基本是坐标换算出了问题没有做四舍五入或者没有减去棋盘左上角偏移量。解决方法是检查OnLButtonDown里的换算逻辑先把point.x减去棋盘起始 X 坐标再除以CellWidth得到一个浮点数后用(int)(x 0.5)或round()取整再做边界范围检查保证索引落在 0~14 之间。5.2 窗口一拖动棋盘上的棋子全消失这是典型的没把绘制逻辑放在OnDraw里导致的。鼠标响应函数里直接调了绘图函数窗口重绘时OnDraw没有重新画棋盘。解决方法是把棋盘数组的遍历和棋子绘制统一放进OnDraw鼠标落子时只修改数组数据然后调用Invalidate()触发重绘不要直接在消息响应里画图。Invalidate()是异步的它会通知框架尽快重绘窗口所有绘制在OnDraw里完成这样窗口拖动、遮挡恢复都不会丢内容。5.3 迷你棋盘的 Bug棋盘线不完整或者缺边原因通常是把画线循环的边界写错了。画 15 条线需要循环 15 次坐标从 0 到 14绘制线段的起点和终点分别是(i * CellWidth OriginX, OriginY)到(i * CellWidth OriginX, OriginY BoardHeight)。如果循环条件写成i 15数组索引访问是合法但画出的线超出预期范围如果写成i 14棋盘最后一条边就消失了。解决方法是统一用一个BOARD_SIZE 15常量控制数组维度和画线循环次数避免两处数字不一致。5.4 五子连珠了不判赢或者赢了还没弹窗就卡死先说连了不判赢的情况大多是因为胜负判定函数里只扫描了横向和纵向漏掉了对角线方向或者扫描长度边界留多了留少了。一个稳妥的排查方式是写一个临时的调试输出在CheckWin返回 true 的位置加OutputDebugString或者断点手动在棋盘上摆出五连看看能不能走到那个分支。另一个坑是MessageBox弹窗在 MFC 的鼠标消息处理函数里调用时消息循环被阻塞界面会“卡死”这是正常现象关掉弹窗就恢复。如果弹窗后程序持续无法操作检查判断胜负后有没有把m_bGameOver置 true否则弹窗关掉后还能继续点棋盘。5.5 背景图片显示不出来或者显示的是拉伸变形的图背景样式切换的功能通过读取UserImages.bmp这个位图资源实现。最常见的问题是位图尺寸不匹配加载后直接贴到客户区导致拉伸或者只显示局部。解决方法是加载位图后用StretchBlt配合客户区宽高做缩放绘制同时保证绘制顺序——背景图最底下棋盘线次之棋子最上面。需要特别注意位图资源是否存在于res目录下VS 工程里资源加载路径有时会因为相对路径问题找不到文件Debug 和 Release 模式下表现还会不一样遇到图片加载失败优先检查这层。6. 扩展方向给这套五子棋加 AI 对手和网络对战的切入点想把这套源码从“双人对战”升级到“人机对战”切入点放在OnLButtonDown的落子分支里。当前逻辑是玩家点击后直接落子改成 AI 模式后玩家落子调用一次CheckWin如果没结束就交给 AI 计算AI 落子再反过来判断。AI 算法能用最简单的“评估函数 深度搜索”组合评估函数对每个空位打分考虑该位置四个方向上已有的连子数和被堵情况然后取最高分的位置落子。初学者别一开始就上 Alpha-Beta 剪枝先跑通一个会堵、会连的简单 AI再逐步优化搜索深度。网络对战部分MFC 里用 Socket 编程CAsyncSocket或者CSocket类都行。需要设计一个通信协议每次落子发送坐标信息比如MOVE:7,7这种结构化字符串接收方解析后更新本地棋盘。这套源码在 Doc/View 架构下数据都在内存里做网络扩展时注意把棋盘状态序列化格式定好否则两端数据同步会出问题。对学习 MFC 应用开发的人来说把双人对战版本改造成联机对战版本覆盖了 Socket 通信、状态同步、异常处理这些真实开发场景比从零写一个聊天程序有价值得多。另外调试这类 MFC 程序有一点不能省把 VS 的“异常设置”里“Win32 Exceptions”下的c0000005访问违例勾选上程序崩溃时能直接定位到出错的代码行不用靠OutputDebugString一行行猜。从那以后我每次拿到这类 MFC 工程第一件事就是检查字符集配置和OnDraw的绘制逻辑这两处顺了后面读代码就快得多。希望帮到你。本文还有配套的精品资源点击获取