news 2026/9/28 16:08:06

基于C++ MFC的五子棋源码解析:从GDI绘图到胜负判定

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于C++ MFC的五子棋源码解析:从GDI绘图到胜负判定

简介:基于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 版本。老工程用的平台工具集可能默认是 v120(VS2013),新版本 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的绘制逻辑,这两处顺了,后面读代码就快得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 16:07:34

AI Agent驱动Android真机测试:ARTEMIS与MCP实战

1. 真机测试这件事&#xff0c;为什么一直让人又爱又恨做过 Android 开发的人大概都有这种体会&#xff1a;模拟器跑得好好的&#xff0c;一上真机就翻车。要么是某个厂商 ROM 把后台服务杀了&#xff0c;要么是权限弹窗的文案和位置跟原生系统完全不一样&#xff0c;要么是某个…

作者头像 李华
网站建设 2026/9/28 16:07:30

目标检测数据集制作:VOC/COCO/YOLO格式深度解析与零误差SOP

1. 为什么“数据集制作”才是目标检测项目里最耗时、最容易翻车的环节你花三天调通了YOLOv8的训练脚本&#xff0c;改好了学习率和anchor&#xff0c;信心满满地跑完第一个epoch——结果mAP只有0.02。你反复检查代码&#xff0c;重装CUDA&#xff0c;甚至怀疑显卡温度过高影响了…

作者头像 李华
网站建设 2026/9/28 16:07:26

基于JSP的车险模拟系统:Tomcat部署+保费计算+PDF保单生成

简介&#xff1a;本资源是一套面向计算机专业本科生的毕业设计级JSP Web应用项目&#xff0c;聚焦车险业务全流程模拟&#xff0c;涵盖保费计算、保单管理、理赔申请等核心功能&#xff0c;适用于Java Web课程设计、毕设参考及JSP技术实战学习。压缩包共950个文件&#xff0c;2…

作者头像 李华
网站建设 2026/9/28 16:04:57

Keil5 RTE快速搭建STM32工程:自动配置外设驱动与启动文件

STM32的入门门槛&#xff0c;说实话&#xff0c;一半卡在硬件接线&#xff0c;另一半就卡在开发环境上。我见过太多人Keil5装好了、芯片包也打了&#xff0c;结果新建工程之后对着空荡荡的左侧目录发呆——外设库文件呢&#xff1f;启动文件呢&#xff1f;难道要一个个手动往工…

作者头像 李华
网站建设 2026/9/28 16:04:25

MCP协议实战:从多客户端适配崩溃到统一工具调用标准

1. 从一次接口对接的崩溃说起&#xff1a;MCP 到底想解决什么问题如果你最近半年在折腾 LLM 应用&#xff0c;大概率经历过这种场景&#xff1a;为了让模型能读到一个本地文件、查一次数据库、调一次内部接口&#xff0c;你得给每个模型客户端单独写一套适配代码。Claude Deskt…

作者头像 李华
网站建设 2026/9/28 16:03:55

Multisim 14.0单相桥式全控整流电路仿真:参数优化与波形调试指南

1. 从一次“波形对不上”的调试说起如果你正在做电力电子课程的仿真作业&#xff0c;或者刚接手一个整流电路的参数验证任务&#xff0c;大概率会遇到这样一个场景&#xff1a;照着教材上的单相桥式全控整流电路在Multisim里搭好了模型&#xff0c;触发脉冲也给了&#xff0c;示…

作者头像 李华