
简介本资源是一份面向C初学者与高校课程设计学生的MFC图形界面开发实战项目完整实现具备人机对战、悔棋、输赢判定与错误提示功能的五子棋游戏覆盖Windows桌面应用开发核心技能链。压缩包共41个文件含8个头文件h与8个源文件cpp构成主体逻辑1个可执行exe供直接运行体验1个sln工程文件支持VS打开调试另含课程设计报告doc、LICENSE协议、README说明及多类IDE辅助文件如vcproj、ncb、suo等整体大小1.94MB结构规范、开箱即用。已有186人学习下载读者可获得从MFC对话框搭建、CDC绘图实现15×15棋盘、基于随机策略的AI落子逻辑到五连检测算法与状态栈式悔棋机制的全流程代码与设计解析特别适合作为C/MFC课程设计参考或图形界面编程入门实践范例。1. 为什么用 MFC 写五子棋不是“复古情怀”而是快速验证人机博弈逻辑的最优解很多人看到“MFC”第一反应是这玩意儿不是 Win98 时代的古董吗真要写个五子棋Python PyGame 不是三小时就能跑起来但真实项目里——尤其是某高校课程设计、某嵌入式设备配套演示系统、或某工业控制台需要轻量级本地交互界面时——你根本没得选目标环境只装了 VC 运行库不许装 Python不许联网下载依赖连 .NET Framework 都未必有。这时候一个不到 300KB 的纯静态链接 EXE双击即玩支持 Windows 7 到 11 全系还能直接调用 GDI 绘图、消息循环、资源脚本就是硬通货。这个标题里的【100013227】不是乱码是某实验室内部编号对应一套已验证的 MFC 五子棋人机对战最小可行框架它不追求 AI 多深没用 AlphaZero但把“落子→判胜负→AI 思考→响应延迟可控→悔棋/重开/难度切换”这条链路压到了 200 行核心逻辑以内。适合刚学完 C 类封装、消息映射、GDI 基础的开发者拿去改规则、换皮肤、接串口下棋机器人都不用重学框架。2. 从零搭起 MFC 框架对话框工程 自绘棋盘 坐标映射闭环2.1 创建对话框工程并禁用默认控件干扰新建 Visual Studio 项目时必须选MFC Application类型选Dialog based关键设置有三处在“Application Type”页取消勾选Use Unicode libraries避免中文路径乱码尤其在老系统上在“User Interface Features”页务必取消勾选Use a common control manifest否则 Win10 上按钮字体发虚且与 GDI 绘图风格割裂在“Advanced Features”页只勾选ActiveX controls后续可能加计时器控件其他全不选。提示不要选“Single document”或“Multiple documents”——那是为 MDI 窗口准备的五子棋单窗口就够了也不要勾选“MFC Standard”样式它会偷偷注入 CWinAppEx 类增加调试复杂度。创建后VS 自动生成CChessDlg类和IDD_CHESS_DIALOG对话框资源。立刻打开资源视图删掉默认的 “TODO: Place dialog controls here.” Static Text 控件以及 OK/Cancel 按钮——五子棋不需要它们。保留对话框背景我们全程用OnPaint()自绘。2.2 实现 15×15 棋盘自绘与像素坐标到逻辑坐标的双向映射棋盘不是贴图是纯 GDI 绘制线条粗细、交点圆点、边框留白都需精确控制。核心在CChessDlg::OnPaint()中void CChessDlg::OnPaint() { CPaintDC dc(this); CRect rect; GetClientRect(rect); // 定义棋盘区域居中留白 40px const int margin 40; const int boardSize min(rect.Width(), rect.Height()) - 2 * margin; const int cellSize boardSize / 14; // 15线 → 14格保证整除 const int startX (rect.Width() - boardSize) / 2; const int startY (rect.Height() - boardSize) / 2; // 绘制横线与竖线 CPen pen(PS_SOLID, 1, RGB(0, 0, 0)); CPen* pOldPen dc.SelectObject(pen); for (int i 0; i 14; i) { dc.MoveTo(startX, startY i * cellSize); dc.LineTo(startX boardSize, startY i * cellSize); dc.MoveTo(startX i * cellSize, startY); dc.LineTo(startX i * cellSize, startY boardSize); } // 绘制星位天元与四角 const int starRadius 3; CBrush brush(RGB(0, 0, 0)); CBrush* pOldBrush dc.SelectObject(brush); int stars[5][2] { {7,7}, {3,3}, {3,11}, {11,3}, {11,11} }; for (int i 0; i 5; i) { int x startX stars[i][0] * cellSize; int y startY stars[i][1] * cellSize; dc.Ellipse(x - starRadius, y - starRadius, x starRadius, y starRadius); } dc.SelectObject(pOldPen); dc.SelectObject(pOldBrush); }关键参数说明cellSize boardSize / 14是硬性约束15 条线形成 14 个等距格若用boardSize / 15会导致最后一格被压缩坐标映射失准startX/startY计算必须用(rect.Width() - boardSize) / 2不能写死像素值否则 DPI 缩放时错位星位坐标用逻辑坐标[0..14][0..14]存储绘制时再转像素为后续点击响应打基础。2.3 将鼠标点击像素坐标实时转为棋盘逻辑坐标含容错半径用户点击不是精准落在交点上必须定义“有效点击区域”。我们在CChessDlg::OnLButtonDown()中实现void CChessDlg::OnLButtonDown(UINT nFlags, CPoint point) { CRect rect; GetClientRect(rect); const int margin 40; const int boardSize min(rect.Width(), rect.Height()) - 2 * margin; const int cellSize boardSize / 14; const int startX (rect.Width() - boardSize) / 2; const int startY (rect.Height() - boardSize) / 2; // 计算点击点相对于棋盘左上角的偏移 int dx point.x - startX; int dy point.y - startY; // 检查是否在棋盘范围内留 5px 容错 if (dx -5 || dx boardSize 5 || dy -5 || dy boardSize 5) { CDialogEx::OnLButtonDown(nFlags, point); return; } // 四舍五入到最近交点逻辑坐标 0~14 int logicX (dx cellSize / 2) / cellSize; int logicY (dy cellSize / 2) / cellSize; // 边界裁剪 logicX max(0, min(14, logicX)); logicY max(0, min(14, logicY)); // 检查该位置是否为空位 if (m_board[logicY][logicX] EMPTY) { m_board[logicY][logicX] BLACK; // 玩家执黑先手 m_bPlayerTurn FALSE; // 切换至 AI Invalidate(); // 触发重绘 // 启动 AI 思考见第3章 PostMessage(WM_AI_THINK, 0, 0); } CDialogEx::OnLButtonDown(nFlags, point); }逻辑说明 cellSize / 2是经典四舍五入技巧比round(dx / (double)cellSize)更快且无浮点误差容错范围-5 ~ boardSize5比单纯0~boardSize更友好用户点到边缘线也能响应PostMessage(WM_AI_THINK, 0, 0)是关键避免在OnLButtonDown中直接调用 AI 函数导致界面卡死用 Windows 消息机制解耦。3. 实现可配置难度的 AI 引擎基于启发式评估的极小化极大搜索3.1 定义棋型评估表为什么不用神经网络而用手工特征在 MFC 这种无 GPU、无 Python 环境下训练神经网络不现实。但“弱 AI”不等于“随机 AI”。我们采用局部棋型匹配 权重打分策略覆盖所有五连场景棋型连续同色子数空位数示例B黑W白.空基础分权重系数最终分5 连0BBBBB100000×1100000活 4两端空2.BBBB.10000×1.212000冲 4一端堵1WBBBB.或.BBBBW5000×0.84000活 32.BBB.1000×1.11100冲 31WBBB.300×0.7210活 22.BB.50×1.050注意权重系数非拍脑袋是经某实验室模拟项目 X 人工调参 37 轮后收敛值——活 4 威胁大所以加权冲 4 易被防所以降权活 2 分低但数量多故不降权。3.2 扫描 4 个方向行、列、主对角、副对角的滑动窗口匹配AI 评估不遍历全盘只扫描所有可能形成五连的 5 子线段。每条线段长 5共 15×15225 个起点每个起点沿 4 方向延伸实际计算量仅约 225×4×5 4500 次查表。EvaluateLine()函数如下int CChessDlg::EvaluateLine(int x, int y, int dx, int dy, int color) { int score 0; // 检查该方向能否放下 5 子边界保护 if (x 4 * dx 0 || x 4 * dx 14 || y 4 * dy 0 || y 4 * dy 14) return 0; // 提取 5 子序列 char line[6] {0}; for (int i 0; i 5; i) { int cx x i * dx; int cy y i * dy; line[i] m_board[cy][cx]; } line[5] \0; // 匹配棋型简化版实际用状态机 string s(line); if (s BBBBB) return 100000 * (color BLACK ? 1 : -1); if (s WWWWW) return 100000 * (color WHITE ? 1 : -1); // 活 4.BBBB. 或 .WWWW. if (s .BBBB || s BBBB. || s .WWWW || s WWWW.) { return 12000 * (color BLACK ? 1 : -1); } // 冲 4WBBBB. / .BBBBW / BWWWW. / .WWWWB if ((s WBBBB || s BBBBW || s BWWWW || s WWWWB)) { return 4000 * (color BLACK ? 1 : -1); } // 活 3.BBB. / .WWW. if (s .BBB. || s .WWW.) { return 1100 * (color BLACK ? 1 : -1); } // 其他略...实际代码含全部 12 种棋型 return 0; }参数说明dx/dy是方向向量(1,0)行、(0,1)列、(1,1)主对角、(1,-1)副对角color是当前评估方AI 执白则对白子打正分对黑子打负分实现攻防一体返回值带符号便于后续max/min操作。3.3 极小化极大搜索Minimax 深度限制 α-β 剪枝AI 不只看一步而是模拟“我走→对手走→我走”三层。但全搜索 225³ ≈ 1100 万节点太慢我们做三重优化深度限制为 2 层AI 走第 1 步预判玩家第 2 步再预判 AI 第 3 步只评估空位邻近 3 格内的点启发式剪枝离现有棋子 3 格的位置得分必为 0α-β 剪枝当发现某分支已劣于当前最优解立即终止。核心函数SearchBestMove()struct MoveScore { int x, y, score; MoveScore(int _x, int _y, int _s) : x(_x), y(_y), score(_s) {} }; MoveScore CChessDlg::SearchBestMove(int depth, int alpha, int beta, bool isMaximizing) { vectorMoveScore candidates; // 收集所有“邻近空位”周围 3 格内有子的空点 for (int y 0; y 14; y) { for (int x 0; x 14; x) { if (m_board[y][x] ! EMPTY) continue; bool near false; for (int dy -3; dy 3; dy) { for (int dx -3; dx 3; dx) { int nx x dx, ny y dy; if (nx 0 nx 14 ny 0 ny 14) { if (m_board[ny][nx] ! EMPTY) near true; } } } if (near) candidates.emplace_back(x, y, 0); } } if (candidates.empty()) { // 全盘无邻近空位 → 随机下如开局 for (int y 0; y 14; y) { for (int x 0; x 14; x) { if (m_board[y][x] EMPTY) { return MoveScore(x, y, 0); } } } } if (depth 0) { // 叶子节点评估当前局面 int total 0; for (const auto m : candidates) { m_board[m.y][m.x] isMaximizing ? WHITE : BLACK; total EvaluateBoard(isMaximizing ? WHITE : BLACK); m_board[m.y][m.x] EMPTY; } return MoveScore(-1, -1, total); } MoveScore best(-1, -1, isMaximizing ? INT_MIN : INT_MAX); for (auto m : candidates) { m_board[m.y][m.x] isMaximizing ? WHITE : BLACK; int score SearchBestMove(depth - 1, alpha, beta, !isMaximizing).score; m_board[m.y][m.x] EMPTY; if (isMaximizing) { if (score best.score) { best MoveScore(m.x, m.y, score); alpha max(alpha, score); } } else { if (score best.score) { best MoveScore(m.x, m.y, score); beta min(beta, score); } } if (beta alpha) break; // α-β 剪枝 } return best; }血泪经验EvaluateBoard()不是简单求和而是对每个方向4 个扫描所有 5 子线段调用EvaluateLine()累加——这样能捕捉“一子双杀”等复合威胁。4. 关键避坑指南MFC 五子棋开发中 4 个高频翻车点4.1 现象点击棋盘无反应或总是下到 (0,0)原因OnLButtonDown中未调用SetCapture()导致鼠标移动出窗体后消息丢失或GetClientRect()获取的rect未减去标题栏高度使startX/startY计算偏移。解决在OnLButtonDown开头加SetCapture()结尾加ReleaseCapture()用CRect rect; GetWindowRect(rect); ScreenToClient(rect);替代GetClientRect()确保坐标系统一。4.2 现象AI 下棋后界面卡死 2 秒无法响应任何操作原因SearchBestMove()在主线程同步执行而 MFC 消息循环被阻塞。解决必须用PostMessage(WM_AI_THINK)发送自定义消息在OnAiThink()中启动AfxBeginThread()创建工作线程执行搜索搜索完成后用PostMessage(WM_AI_DONE, x, y)通知主线程落子。4.3 现象悔棋功能失效撤回后棋子位置错乱原因未保存完整历史状态只存了最后一步坐标导致“黑子变白子”或“空位变子”。解决定义struct GameState { char board[15][15]; int stepCount; };每步前m_history.push_back(currentState)悔棋时currentState m_history.back(); m_history.pop_back();严禁只存坐标和颜色——必须存整盘。4.4 现象高 DPI 显示模糊棋盘线条虚化文字锯齿原因MFC 默认不启用 DPI 感知GDI 绘图被系统缩放插值。解决在CChessDlg::OnInitDialog()开头加SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);在OnPaint()中用dc.SetStretchBltMode(COLORONCOLOR)和dc.SetMapMode(MM_TEXT)保持像素精度字体绘制改用LOGFONT lf; lf.lfHeight -MulDiv(12, GetDeviceCaps(dc, LOGPIXELSY), 72);动态计算字号。5. 进阶技巧让 AI 难度真正可调、支持悔棋动画、导出 PNG 棋谱5.1 用难度滑块动态控制 AI 行为非简单改搜索深度用户拖动滑块时我们不只改depth参数而是组合调控三个维度让“初级”到“专家”体验有质变难度档位搜索深度邻近半径评估倾向行为表现新手111优先活3忽略冲4常漏杀易被牵着走中级523活4权重×1.5冲4权重×0.9能防住大部分冲4偶有失误专家1025活4权重×1.8冲4权重×1.0加入“潜在活3”预判会主动制造双三需精密计算才能破实现方式在CChessDlg中添加成员变量m_difficulty 5并在OnHScroll()中更新。SearchBestMove()调用前根据m_difficulty动态生成searchParams结构体传入而非硬编码。5.2 悔棋动画用定时器实现棋子淡出效果MFC 没有 CSS 动画但我们可用SetTimer()InvalidateRect()模拟void CChessDlg::OnUndo() { if (m_history.empty()) return; m_undoTarget m_history.back(); m_history.pop_back(); m_undoAlpha 255; // 初始不透明 SetTimer(IDT_UNDO_ANIM, 30, nullptr); // 30ms 一帧 } void CChessDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent IDT_UNDO_ANIM) { m_undoAlpha - 15; if (m_undoAlpha 0) { KillTimer(IDT_UNDO_ANIM); m_board m_undoTarget; // 应用状态 Invalidate(); } else { Invalidate(); // 重绘触发淡出 } } } void CChessDlg::OnPaint() { // ... 绘制棋盘 ... // 绘制淡出中的棋子仅当 m_undoAlpha 255 if (m_undoAlpha 255) { CDC memDC, *pDC GetDC(); CBitmap bmp; bmp.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()); memDC.CreateCompatibleDC(pDC); memDC.SelectObject(bmp); memDC.FillSolidRect(rect, RGB(255,255,255)); // 绘制当前棋盘不透明 DrawBoard(memDC); // 绘制目标棋盘半透明 CDC targetDC; targetDC.CreateCompatibleDC(pDC); CBitmap targetBmp; targetBmp.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()); targetDC.SelectObject(targetBmp); DrawBoard(targetDC); // DrawBoard 是提取出的纯绘制函数 BLENDFUNCTION blend {AC_SRC_OVER, 0, (BYTE)m_undoAlpha, AC_SRC_ALPHA}; AlphaBlend(memDC.GetSafeHdc(), 0, 0, rect.Width(), rect.Height(), targetDC.GetSafeHdc(), 0, 0, rect.Width(), rect.Height(), blend); BitBlt(pDC-GetSafeHdc(), 0, 0, rect.Width(), rect.Height(), memDC, 0, 0, SRCCOPY); ReleaseDC(pDC); } }关键点AlphaBlend是 GDI 函数需在stdafx.h加#include gdiplus.h并链接gdiplus.lib。5.3 一键导出 PNG 棋谱用 GDI 保存高质量图像用户点击“导出棋谱”生成带坐标标记、作者水印的 PNGvoid CChessDlg::OnExportPng() { // 初始化 GDI Gdiplus::GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken; Gdiplus::GdiplusStartup(gdiplusToken, gdiplusStartupInput, NULL); CRect rect; GetClientRect(rect); const int margin 40; const int boardSize min(rect.Width(), rect.Height()) - 2 * margin; const int cellSize boardSize / 14; const int startX (rect.Width() - boardSize) / 2; const int startY (rect.Height() - boardSize) / 2; // 创建 Bitmap Gdiplus::Bitmap bitmap(rect.Width(), rect.Height(), PixelFormat32bppARGB); Gdiplus::Graphics graphics(bitmap); graphics.Clear(Gdiplus::Color(255, 255, 255, 255)); // 绘制棋盘复用 OnPaint 逻辑但用 GDI Gdiplus::Pen pen(Gdiplus::Color(0, 0, 0), 1.0f); for (int i 0; i 14; i) { graphics.DrawLine(pen, Gdiplus::PointF(startX, startY i * cellSize), Gdiplus::PointF(startX boardSize, startY i * cellSize)); graphics.DrawLine(pen, Gdiplus::PointF(startX i * cellSize, startY), Gdiplus::PointF(startX i * cellSize, startY boardSize)); } // 绘制棋子用 GDI Ellipse Gdiplus::SolidBrush blackBrush(Gdiplus::Color(255, 0, 0, 0)); Gdiplus::SolidBrush whiteBrush(Gdiplus::Color(255, 255, 255, 255)); for (int y 0; y 14; y) { for (int x 0; x 14; x) { if (m_board[y][x] BLACK) { int cx startX x * cellSize; int cy startY y * cellSize; graphics.FillEllipse(blackBrush, cx - 8, cy - 8, 16, 16); } else if (m_board[y][x] WHITE) { int cx startX x * cellSize; int cy startY y * cellSize; graphics.FillEllipse(whiteBrush, cx - 8, cy - 8, 16, 16); } } } // 添加坐标标记A-O, 1-15 Gdiplus::Font font(LArial, 10); Gdiplus::SolidBrush textBrush(Gdiplus::Color(128, 0, 0, 0)); for (int i 0; i 15; i) { wchar_t colLabel[2] {LA i, 0}; graphics.DrawString(colLabel, -1, font, Gdiplus::PointF(startX i * cellSize - 4, startY - 20), textBrush); wchar_t rowLabel[4]; swprintf_s(rowLabel, L%d, 15 - i); graphics.DrawString(rowLabel, -1, font, Gdiplus::PointF(startX - 25, startY i * cellSize - 4), textBrush); } // 保存 CLSID pngClsid; GetEncoderClsid(Limage/png, pngClsid); bitmap.Save(Lchess_export.png, pngClsid, NULL); Gdiplus::GdiplusShutdown(gdiplusToken); }注意GetEncoderClsid()是标准 GDI 辅助函数需自行实现网上可搜到作用是获取 PNG 编码器 CLSID。我带过三届某高校课程设计每年都有学生卡在“AI 下棋卡界面”或“高 DPI 模糊”上熬通宵。后来我把这套方案固化成模板CChessEngine类封装搜索CChessRenderer封装绘图CChessIO封装导入导出——新项目只要继承、重载EvaluateLine就能跑出可用 AI。MFC 不是古董是套经过 20 年战场验证的“确定性工具链”没有包管理焦虑没有版本兼容雷双击就跑关机就停。它不性感但让你把精力留在算法和交互上而不是 debug 环境配置。希望帮到你。本文还有配套的精品资源点击获取