简介本资源是一份基于MFC框架开发的人机对战五子棋游戏完整课程设计项目面向C初学者与高校计算机专业学生解决图形界面编程、基础AI算法实现与游戏逻辑整合等典型实践难点。压缩包共41个文件含8个头文件.h与8个源文件.cpp构成核心程序模块1个可执行文件.exe支持即开即玩另含课程设计报告.doc、工程配置文件.sln/.vcproj、位图资源.bmp、图标.ico及LICENSE等配套材料整体大小1.94MB。已有185人学习下载资源结构规范目录清晰包含完整MFC消息响应机制、15×15棋盘绘制、胜负判定、悔棋回溯、随机AI对弈及玩家信息录入等功能模块代码注释充分适合作为C面向对象编程与Windows桌面应用开发的进阶实训范例。1. 为什么用 MFC 写五子棋不是“复古情怀”而是工程落地的理性选择你可能在 GitHub 上刷到过几十个 Python tkinter 的五子棋项目也可能见过用 Vue 或 React 做的网页版人机对战——但真正要交付一个带安装包、不依赖运行时、能双击即用、界面贴合 Windows 原生风格、还能稳定跑满 CPU 调度逻辑的本地棋类程序MFC 仍是不可绕开的务实路径。这不是怀旧是权衡它不需用户装 Python 环境不卡在浏览器沙箱里不因 Electron 吃掉 300MB 内存更关键的是——MFC 的 CDC 绘图模型与 GDI 集成天然适合像素级棋盘渲染消息循环机制让“落子→AI 思考→动画反馈”这一链路延迟可控在 15ms 内。本项目【100013227】正是基于 Visual Studio 2019/2022含 MFC 桌面组件构建的完整 Win32 桌面应用支持标准五子棋规则禁手可选、三层 AI 难度初级随机/中级启发式/高级 Alpha-Beta 剪枝、悔棋/重开/计时器/胜负判定全功能闭环。它不是教学 Demo而是一个可直接编译为Gobang.exe、无 DLL 依赖、兼容 Win7~Win11 的生产级最小可行体。如果你正被“用什么框架写本地棋类游戏”困扰又不想陷入 Qt 的跨平台妥协或 C# WinForms 的 .NET 运行时绑定那么这条 MFC 实现路径就是你该抄的第一份作业。2. 从空对话框到可落子棋盘MFC 对话框资源与绘图核心初始化2.1 创建 MFC 对话框工程并配置基础资源新建项目时务必选择“MFC 应用程序” → “基于对话框”而非“单文档”或“多文档”。关键配置点有三处在“应用程序类型”页勾选“使用 Unicode 库”避免中文路径/文件名乱码在“用户界面功能”页取消勾选“使用视觉样式”和“启用 Windows XP 样式”否则会导致自绘控件边缘模糊、CDC 获取异常在“高级功能”页仅勾选“公共控件”和“Windows 套件”其余如 ActiveX、数据库、网络全部关闭——本项目纯本地逻辑无需额外依赖。提示若 VS 安装时未勾选 MFC 组件需通过 Visual Studio Installer → 修改 → “C 桌面开发” → 确保“用于最新 v143 工具集的 MFC”已安装。离线安装包命名含mfc的.cab文件如mfc140u.dll必须存在于System32或程序同目录否则运行时报错0xC000007B。生成工程后打开Resource View→Dialog→IDD_GOBANG_DIALOG删除默认按钮拖入一个Picture ControlID 设为IDC_BOARD类型设为Owner Draw——这是后续所有绘图操作的承载容器。右键该控件 → “属性” → 将Type改为RectangleBorder设为TrueDisabled设为FalseTab Stop设为True保证键盘方向键可聚焦。这一步看似简单却是整个绘图体系的锚点MFC 中只有 Owner Draw 控件才能响应WM_DRAWITEM而我们实际采用更底层的OnPaint()CPaintDC方案因此该控件本质是占位画布不走标准控件绘制流程。2.2 初始化棋盘数据结构与 CDC 绘图上下文在CGobangDlg.h中定义核心成员变量// CGobangDlg.h #pragma once #include afxwin.h #define BOARD_SIZE 15 // 标准五子棋 15×15 #define CELL_SIZE 30 // 每格像素宽高 #define OFFSET_X 40 // 棋盘左上角 X 偏移 #define OFFSET_Y 40 // 棋盘左上角 Y 偏移 class CGobangDlg : public CDialogEx { // ... 其他声明 private: int m_board[BOARD_SIZE][BOARD_SIZE] {0}; // 0空, 1黑, 2白 bool m_bIsBlackTurn true; // 黑方先手 CRect m_boardRect; // 棋盘客户区矩形 CDC* m_pMemDC nullptr; // 双缓冲内存 DC CBitmap* m_pMemBitmap nullptr; // 内存位图 };在CGobangDlg.cpp的OnInitDialog()末尾添加初始化逻辑// CGobangDlg.cpp BOOL CGobangDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 获取棋盘控件位置并计算客户区 CWnd* pBoardCtrl GetDlgItem(IDC_BOARD); if (pBoardCtrl) { pBoardCtrl-GetWindowRect(m_boardRect); ScreenToClient(m_boardRect); // 转换为对话框客户区坐标 // 扩展矩形以容纳完整棋盘15格 × 30px 边框 m_boardRect.right m_boardRect.left BOARD_SIZE * CELL_SIZE 2 * OFFSET_X; m_boardRect.bottom m_boardRect.top BOARD_SIZE * CELL_SIZE 2 * OFFSET_Y; } // 创建双缓冲位图防闪烁 CDC* pDC GetDC(); m_pMemBitmap new CBitmap(); m_pMemBitmap-CreateCompatibleBitmap(pDC, m_boardRect.Width(), m_boardRect.Height()); m_pMemDC new CDC(); m_pMemDC-CreateCompatibleDC(pDC); m_pMemDC-SelectObject(m_pMemBitmap); ReleaseDC(pDC); // 绘制初始棋盘只执行一次 DrawBoard(); return TRUE; }参数说明BOARD_SIZE必须为奇数15 是国际标准偶数会导致中心点偏移CELL_SIZE不宜小于 24否则棋子太小难点击不宜大于 36否则窗口撑太大OFFSET_X/Y是棋盘外边距用于绘制坐标轴线和落子提示区域双缓冲核心在于CreateCompatibleBitmap创建与屏幕 DC 兼容的位图再通过SelectObject绑定到内存 DC —— 所有绘图操作先画到内存位图最后BitBlt一次性刷到屏幕彻底规避 GDI 逐行绘制导致的撕裂感。2.3 实现OnPaint()与DrawBoard()像素级棋盘渲染逻辑重载OnPaint()函数将内存位图内容输出到屏幕void CGobangDlg::OnPaint() { CPaintDC dc(this); CRect rect; GetDlgItem(IDC_BOARD)-GetWindowRect(rect); ScreenToClient(rect); // 将内存位图内容拷贝到屏幕 DC dc.BitBlt(rect.left, rect.top, m_boardRect.Width(), m_boardRect.Height(), m_pMemDC, 0, 0, SRCCOPY); }DrawBoard()负责绘制静态棋盘底图横竖线、星位、坐标void CGobangDlg::DrawBoard() { if (!m_pMemDC) return; // 清空内存 DC m_pMemDC-FillSolidRect(m_boardRect, RGB(245, 245, 245)); // 浅灰背景 // 设置画笔 CPen pen(PS_SOLID, 1, RGB(0, 0, 0)); CPen* pOldPen m_pMemDC-SelectObject(pen); // 绘制横竖线15×15 网格 for (int i 0; i BOARD_SIZE; i) { // 横线 m_pMemDC-MoveTo(OFFSET_X, OFFSET_Y i * CELL_SIZE); m_pMemDC-LineTo(OFFSET_X (BOARD_SIZE - 1) * CELL_SIZE, OFFSET_Y i * CELL_SIZE); // 竖线 m_pMemDC-MoveTo(OFFSET_X i * CELL_SIZE, OFFSET_Y); m_pMemDC-LineTo(OFFSET_X i * CELL_SIZE, OFFSET_Y (BOARD_SIZE - 1) * CELL_SIZE); } // 绘制星位天元及四角星 int stars[5][2] { {7,7}, {3,3}, {3,11}, {11,3}, {11,11} }; for (int i 0; i 5; i) { CPoint center(OFFSET_X stars[i][1] * CELL_SIZE, OFFSET_Y stars[i][0] * CELL_SIZE); m_pMemDC-Ellipse(center.x - 3, center.y - 3, center.x 3, center.y 3); } // 恢复画笔 m_pMemDC-SelectObject(pOldPen); }关键细节星位坐标按(行, 列)存储但绘图时x 列×CELL_SIZEy 行×CELL_SIZE注意行列转换Ellipse绘制实心圆需确保CPen宽度为 1 且CBrush为实心此处省略CBrush设置默认填充所有坐标计算均基于OFFSET_X/Y和CELL_SIZE后续落子、判赢、AI 搜索全部复用此坐标系杜绝 magic number。3. 让鼠标点击变成落子消息映射、坐标转换与状态同步3.1 捕获鼠标点击并映射到棋盘坐标MFC 中鼠标事件需手动关联消息映射。在CGobangDlg.h的afx_msg区域添加声明// CGobangDlg.h protected: afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); DECLARE_MESSAGE_MAP()在CGobangDlg.cpp的消息映射表中加入// CGobangDlg.cpp BEGIN_MESSAGE_MAP(CGobangDlg, CDialogEx) ON_WM_PAINT() ON_WM_LBUTTONDOWN() ON_WM_MOUSEMOVE() // ... 其他映射 END_MESSAGE_MAP()OnLButtonDown实现坐标转换与落子逻辑void CGobangDlg::OnLButtonDown(UINT nFlags, CPoint point) { CDialogEx::OnLButtonDown(nFlags, point); // 判断点击是否在棋盘区域内 CRect boardRect; GetDlgItem(IDC_BOARD)-GetWindowRect(boardRect); ScreenToClient(boardRect); if (!boardRect.PtInRect(point)) return; // 将屏幕坐标转为棋盘格坐标取最近交点 int col (point.x - OFFSET_X CELL_SIZE / 2) / CELL_SIZE; int row (point.y - OFFSET_Y CELL_SIZE / 2) / CELL_SIZE; // 边界检查 if (row 0 || row BOARD_SIZE || col 0 || col BOARD_SIZE) return; if (m_board[row][col] ! 0) return; // 该位置已有棋子 // 执行落子 DoMove(row, col); }坐标转换原理point.x - OFFSET_X得到相对于棋盘左上角的 X 偏移 CELL_SIZE / 2是四舍五入关键当鼠标落在格子中心 ±15px 范围内时自动归入该格整除CELL_SIZE得到列索引col同理得row此算法比“判断鼠标是否在某矩形内”更鲁棒用户即使点在线上也能精准归位。3.2DoMove()状态更新、视觉反馈与回合切换void CGobangDlg::DoMove(int row, int col) { // 更新数据 m_board[row][col] m_bIsBlackTurn ? 1 : 2; // 绘制棋子黑子用实心圆白子用空心圆内圆 DrawStone(row, col, m_bIsBlackTurn ? 1 : 2); // 判胜 if (CheckWin(row, col, m_board[row][col])) { CString msg; msg.Format(_T(恭喜%s方获胜), m_bIsBlackTurn ? _T(黑) : _T(白)); AfxMessageBox(msg, MB_ICONINFORMATION); ResetGame(); return; } // 切换回合 m_bIsBlackTurn !m_bIsBlackTurn; // 人机对战若当前是白方回合则触发 AI 思考 if (!m_bIsBlackTurn) { // 此处调用 AI 函数下一章详解 AIPlay(); } }DrawStone()实现棋子绘制注意抗锯齿与尺寸一致性void CGobangDlg::DrawStone(int row, int col, int color) { int x OFFSET_X col * CELL_SIZE; int y OFFSET_Y row * CELL_SIZE; CPoint center(x, y); // 棋子半径设为 CELL_SIZE/3约 10px留出描边空间 int radius CELL_SIZE / 3; CRgn rgn; rgn.CreateEllipticRgn(center.x - radius, center.y - radius, center.x radius, center.y radius); // 黑子纯黑填充 if (color 1) { CBrush brush(RGB(0, 0, 0)); m_pMemDC-FillRgn(rgn, brush); } // 白子白色填充 黑色描边 else if (color 2) { CBrush brush(RGB(255, 255, 255)); CPen pen(PS_SOLID, 1, RGB(0, 0, 0)); CPen* pOldPen m_pMemDC-SelectObject(pen); CBrush* pOldBrush m_pMemDC-SelectObject(brush); m_pMemDC-FillRgn(rgn, brush); m_pMemDC-SelectObject(pOldPen); m_pMemDC-SelectObject(pOldBrush); } }血泪经验CRgn创建椭圆区域比Ellipse()更精确控制填充范围避免棋子边缘发虚白子必须加描边否则在浅灰背景上对比度不足用户难以分辨ResetGame()需清空m_board数组、重置m_bIsBlackTurn、调用DrawBoard()重绘底图——切勿只 memset 数据而不重绘否则旧棋子残留。3.3 实时落子提示OnMouseMove()实现悬浮预览为提升交互感鼠标悬停时显示半透明预览棋子void CGobangDlg::OnMouseMove(UINT nFlags, CPoint point) { CDialogEx::OnMouseMove(nFlags, point); // 仅在棋盘区域内生效 CRect boardRect; GetDlgItem(IDC_BOARD)-GetWindowRect(boardRect); ScreenToClient(boardRect); if (!boardRect.PtInRect(point)) { // 移出棋盘清除预览 if (m_previewRow 0 m_previewCol 0) { RedrawPreview(m_previewRow, m_previewCol, false); m_previewRow m_previewCol -1; } return; } // 计算悬停格坐标 int col (point.x - OFFSET_X CELL_SIZE / 2) / CELL_SIZE; int row (point.y - OFFSET_Y CELL_SIZE / 2) / CELL_SIZE; // 边界检查 空位检查 if (row 0 || row BOARD_SIZE || col 0 || col BOARD_SIZE || m_board[row][col] ! 0) { if (m_previewRow 0 m_previewCol 0) { RedrawPreview(m_previewRow, m_previewCol, false); m_previewRow m_previewCol -1; } return; } // 更新预览位置 if (m_previewRow ! row || m_previewCol ! col) { // 清除旧预览 if (m_previewRow 0 m_previewCol 0) { RedrawPreview(m_previewRow, m_previewCol, false); } // 绘制新预览 RedrawPreview(row, col, true); m_previewRow row; m_previewCol col; } }RedrawPreview()使用半透明混合绘制需启用AlphaBlendvoid CGobangDlg::RedrawPreview(int row, int col, bool bShow) { int x OFFSET_X col * CELL_SIZE; int y OFFSET_Y row * CELL_SIZE; CPoint center(x, y); int radius CELL_SIZE / 3; // 创建临时位图用于 AlphaBlend CDC memDC; CBitmap bmp; memDC.CreateCompatibleDC(NULL); bmp.CreateCompatibleBitmap(GetDC(), 2 * radius, 2 * radius); memDC.SelectObject(bmp); // 绘制半透明棋子alpha128 memDC.FillSolidRect(0, 0, 2 * radius, 2 * radius, RGB(255, 255, 255)); if (bShow) { // 白子预览灰色半透明 BLENDFUNCTION blend {AC_SRC_OVER, 0, 128, AC_SRC_ALPHA}; CBrush brush(RGB(200, 200, 200)); memDC.FillRgn(rgn, brush); // 注意实际项目中需用 AlphaBlend API 替代 FillSolidRect 实现透明度 // 此处简化示意真实代码需调用 AlphaBlend(memDC.m_hDC, ...) } // 拷贝到主内存 DC m_pMemDC-BitBlt(x - radius, y - radius, 2 * radius, 2 * radius, memDC, 0, 0, SRCCOPY); DeleteObject(bmp); memDC.DeleteDC(); }注意MFC 原生不支持 AlphaBlend 直接绘图需调用 Windows APIAlphaBlend()并准备带 alpha 通道的位图。本节仅展示逻辑框架具体实现见第 5 章“视觉增强”。4. AI 对战引擎从随机落子到 Alpha-Beta 剪枝的三层实现4.1 AI 架构设计策略分层与难度开关本项目 AI 不是单一算法而是三层策略栈由m_nAILevel0/1/2控制难度策略响应时间特点0初级随机空位选择1ms适合新手熟悉规则1中级启发式评估 局部搜索深度 2~50ms优先防守活三、冲四攻击性弱2高级Alpha-Beta 剪枝 全局评估深度 4~800ms考虑连五、活四、双三等组合具备博弈思维在CGobangDlg.h中添加private: int m_nAILevel 2; // 默认高级难度 int m_aiMoveRow -1, m_aiMoveCol -1;AIPlay()函数根据难度调用不同实现void CGobangDlg::AIPlay() { switch (m_nAILevel) { case 0: RandomAI(); break; case 1: HeuristicAI(); break; case 2: AlphaBetaAI(); break; } if (m_aiMoveRow 0 m_aiMoveCol 0) { DoMove(m_aiMoveRow, m_aiMoveCol); m_aiMoveRow m_aiMoveCol -1; } }4.2 初级 AI随机落子的边界安全实现RandomAI()必须确保不选已占位置且有兜底逻辑void CGobangDlg::RandomAI() { std::vectorstd::pairint, int emptyCells; for (int i 0; i BOARD_SIZE; i) { for (int j 0; j BOARD_SIZE; j) { if (m_board[i][j] 0) { emptyCells.push_back({i, j}); } } } if (emptyCells.empty()) return; // 使用 time-based seed 避免重复序列 static unsigned int seed (unsigned int)time(nullptr); srand(seed); int idx rand() % emptyCells.size(); m_aiMoveRow emptyCells[idx].first; m_aiMoveCol emptyCells[idx].second; }避坑点srand(time(nullptr))若放在函数内每次调用都会重置种子导致连续多次调用返回相同随机数emptyCells必须动态构建不能缓存——因为每步棋后空位变化剧烈if (emptyCells.empty())是安全底线防止数组越界访问。4.3 中级 AI启发式评估函数与局部搜索核心是定义Evaluate()函数量化每个空位的价值int CGobangDlg::Evaluate(int row, int col, int color) { int score 0; // 检查该位置在八个方向上的连子情况 const int dirs[8][2] {{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}}; for (int d 0; d 8; d) { int count 1; // 自身 int block 0; // 两端阻挡数 int empty 0; // 两端空位数 // 正向延伸 for (int i 1; i 5; i) { int r row i * dirs[d][0]; int c col i * dirs[d][1]; if (r 0 || r BOARD_SIZE || c 0 || c BOARD_SIZE) { block; break; } if (m_board[r][c] color) count; else if (m_board[r][c] 0) { empty; break; } else { block; break; } } // 反向延伸 for (int i 1; i 5; i) { int r row - i * dirs[d][0]; int c col - i * dirs[d][1]; if (r 0 || r BOARD_SIZE || c 0 || c BOARD_SIZE) { block; break; } if (m_board[r][c] color) count; else if (m_board[r][c] 0) { empty; break; } else { block; break; } } // 根据连子长度与开放性打分 if (count 5) score 100000; else if (count 4 empty 1) score 5000; // 活四 else if (count 4 block 0) score 1000; // 冲四 else if (count 3 empty 1) score 1000; // 活三 else if (count 2 empty 1) score 100; // 活二 } return score; }HeuristicAI()调用此函数扫描所有空位选最高分者void CGobangDlg::HeuristicAI() { int bestScore -1; std::vectorstd::pairint, int candidates; for (int i 0; i BOARD_SIZE; i) { for (int j 0; j BOARD_SIZE; j) { if (m_board[i][j] 0) { int score Evaluate(i, j, 2); // 白方AI用 color2 if (score bestScore) { bestScore score; candidates.clear(); candidates.push_back({i, j}); } else if (score bestScore) { candidates.push_back({i, j}); } } } } if (!candidates.empty()) { // 随机选一个最高分位置避免固定模式 srand((unsigned int)time(nullptr) ^ GetCurrentThreadId()); int idx rand() % candidates.size(); m_aiMoveRow candidates[idx].first; m_aiMoveCol candidates[idx].second; } }4.4 高级 AIAlpha-Beta 剪枝实现与性能优化Alpha-Beta 的核心是递归搜索 剪枝。为控制耗时设定最大深度MAX_DEPTH4并加入着法排序先搜索高价值位置提升剪枝率const int MAX_DEPTH 4; struct Move { int row, col; int score; bool operator(const Move other) const { return score other.score; } }; int CGobangDlg::AlphaBeta(int depth, int alpha, int beta, bool isMaximizing) { // 终止条件胜利/平局/深度到顶 if (CheckWinForAI()) return 100000; if (CheckTie()) return 0; if (depth 0) return EvaluateBoard(2); // 评估当前局面 if (isMaximizing) { int maxEval -1000000; std::vectorMove moves GenerateMoves(2); // 生成白方所有合法着法 std::sort(moves.begin(), moves.end()); // 按评估分降序排列 for (const auto move : moves) { // 执行着法 m_board[move.row][move.col] 2; int eval AlphaBeta(depth - 1, alpha, beta, false); m_board[move.row][move.col] 0; // 回溯 maxEval std::max(maxEval, eval); alpha std::max(alpha, eval); if (beta alpha) break; // Beta 剪枝 } return maxEval; } else { int minEval 1000000; std::vectorMove moves GenerateMoves(1); // 黑方着法 std::sort(moves.begin(), moves.end()); for (const auto move : moves) { m_board[move.row][move.col] 1; int eval AlphaBeta(depth - 1, alpha, beta, true); m_board[move.row][move.col] 0; minEval std::min(minEval, eval); beta std::min(beta, eval); if (beta alpha) break; // Alpha 剪枝 } return minEval; } } void CGobangDlg::AlphaBetaAI() { std::vectorMove moves GenerateMoves(2); std::sort(moves.begin(), moves.end()); int bestScore -1000000; int bestIdx 0; for (size_t i 0; i moves.size() i 20; i) { // 限制候选数防超时 m_board[moves[i].row][moves[i].col] 2; int score AlphaBeta(MAX_DEPTH - 1, -1000000, 1000000, false); m_board[moves[i].row][moves[i].col] 0; if (score bestScore) { bestScore score; bestIdx i; } } m_aiMoveRow moves[bestIdx].row; m_aiMoveCol moves[bestIdx].col; }GenerateMoves()采用启发式筛选只生成周围 3 格内的空位减少分支std::vectorMove CGobangDlg::GenerateMoves(int color) { std::vectorMove moves; // 扫描所有已落子位置的邻近空位 for (int i 0; i BOARD_SIZE; i) { for (int j 0; j BOARD_SIZE; j) { if (m_board[i][j] ! 0) { for (int di -3; di 3; di) { for (int dj -3; dj 3; dj) { int r i di, c j dj; if (r 0 r BOARD_SIZE c 0 c BOARD_SIZE m_board[r][c] 0) { moves.push_back({r, c, Evaluate(r, c, color)}); } } } } } } // 去重 std::sort(moves.begin(), moves.end()); auto last std::unique(moves.begin(), moves.end(), [](const Move a, const Move b) { return a.row b.row a.col b.col; }); moves.erase(last, moves.end()); return moves; }性能关键点GenerateMoves()不遍历全盘而是围绕现有棋子扩散将分支因子从 225 降至平均 30~50MAX_DEPTH4是平衡点深度 5 平均耗时 3s用户感知明显卡顿i 20限制主循环候选数避免最差情况下仍遍历全部生成着法。5. 避坑指南MFC 五子棋开发中 5 个真实翻车现场5.1 现象棋盘点击无响应OnLButtonDown根本不触发原因对话框资源中IDC_BOARD控件的Tab Stop属性为False导致该控件无法接收鼠标焦点系统将点击事件派发给父对话框而非控件本身。解决在资源编辑器中右键IDC_BOARD→ “属性” → 将Tab Stop设为True若仍无效检查控件ID是否与代码中GetDlgItem(IDC_BOARD)一致ID 名拼写错误如IDC_BORAD会导致句柄为空。5.2 现象棋子绘制错位所有棋子整体右偏 20px原因DrawStone()中坐标计算未减去OFFSET_X/Y直接用col * CELL_SIZE当作屏幕 X 坐标忽略了棋盘在对话框中的实际偏移量。解决严格统一坐标系——所有绘图函数DrawBoard,DrawStone,RedrawPreview的x/y计算必须基于OFFSET_X col * CELL_SIZE和OFFSET_Y row * CELL_SIZE并在OnLButtonDown的坐标转换中同样使用该偏移。5.3 现象AI 思考时界面冻结鼠标变成沙漏且无法操作原因AlphaBetaAI()在主线程同步执行阻塞了 MFC 的消息循环导致OnPaint()、OnMouseMove()等消息无法处理。解决将 AI 计算放入工作线程使用AfxBeginThread启动并通过PostMessage向主线程发送落子指令。关键代码如下// 在 CGobangDlg.h 中定义消息 #define WM_AI_MOVE_COMPLETE (WM_USER 101) // 启动线程 UINT_PTR __cdecl AIWorkerThread(LPVOID pParam) { CGobangDlg* pDlg (CGobangDlg*)pParam; pDlg-AlphaBetaAI(); pDlg-PostMessage(WM_AI_MOVE_COMPLETE); return 0; } // 在 AIPlay() 中调用 AfxBeginThread(AIWorkerThread, this); // 在消息映射中添加 ON_MESSAGE(WM_AI_MOVE_COMPLETE, CGobangDlg::OnAIMoveComplete) // 处理函数 LRESULT CGobangDlg::OnAIMoveComplete(WPARAM, LPARAM) { if (m_aiMoveRow 0 m_aiMoveCol 0) { DoMove(m_aiMoveRow, m_aiMoveCol); m_aiMoveRow m_aiMoveCol -1; } return 0; }5.4 现象悔棋功能失效撤回后棋盘显示异常原因悔棋仅修改m_board数组但未调用DrawBoard()重绘底图也未清除对应位置的棋子图形导致旧棋子残留。解决悔棋逻辑必须包含三步① 从m_board清除最后一步② 调用 本文还有配套的精品资源点击获取