VC++五子棋开发实战:从MFC入门到AI实现
1. 项目概述为什么选择VC开发五子棋如果你对Windows桌面应用开发感兴趣或者想找一个既有挑战性又足够经典的练手项目用VCVisual C来开发一个五子棋游戏绝对是个绝佳的选择。这听起来可能有点“复古”毕竟现在Unity、Godot甚至Python的Pygame都那么流行。但我想说的是正是这种“复古”才让它充满了学习的价值。VC特别是配合微软的MFCMicrosoft Foundation Classes框架能让你深入到Windows GUI程序最核心的机制里理解消息循环、窗口过程、GDI绘图这些底层原理。这些东西是很多现代游戏引擎帮你封装好、你甚至感觉不到它们存在的基石。通过亲手实现一个五子棋你不仅能掌握游戏逻辑比如胜负判断、AI算法更能透彻理解一个Windows桌面程序是如何“活”起来的——从你点击鼠标到棋盘上画出一个棋子这中间到底发生了什么。这个项目适合谁呢首先当然是C的初学者你已经学完了语法和数据结构正愁没有地方把知识串联起来其次是想从控制台程序转向图形界面开发的程序员MFC虽然老但它是理解Windows编程的绝佳教材最后哪怕你是个有经验的开发者想重温一下基础或者给朋友、孩子做个有趣的小工具这个项目也足够有成就感。最终你会得到一个完整的、带图形界面的可执行文件.exe可以独立在Windows电脑上运行这种从零到一创造出一个“产品”的感觉是单纯学习语法无法比拟的。2. 开发环境搭建与项目创建2.1 开发工具选型Visual Studio与VC运行库工欲善其事必先利其器。我们首先需要准备开发环境。这里强烈推荐使用Microsoft Visual Studio它是开发VC程序的事实标准。对于这个项目VS 2019 或 VS 2022 的社区版免费完全够用。安装时在“工作负载”选择界面务必勾选“使用C的桌面开发”。这个选项会安装VC编译器、链接器、标准库以及至关重要的MFC库。这里需要澄清一个常见的混淆点VC运行库和开发环境是两回事。我们搜索热词里出现的“微软 vc 2015-2022 x64 运行库”指的是程序运行时所依赖的动态链接库DLL比如msvcp140.dll,vcruntime140.dll等。你在开发时Visual Studio环境已经包含了这些。而你最终编译出来的程序如果要分发到没有安装过相应运行库的电脑上可能需要让用户单独安装这个运行库包或者采用“静态链接”的方式将库代码打包进你的EXE文件这会使程序体积变大。在项目属性中可以在C/C-代码生成-运行库里选择/MT静态链接而非/MD动态链接来规避运行库依赖问题。2.2 创建MFC应用程序项目打开Visual Studio选择“创建新项目”。在项目模板中搜索并选择“MFC应用程序”给项目起个名字比如Gomoku五子棋的英文。点击“创建”后会弹出一个“MFC应用程序向导”。在这个向导里我们需要做几个关键选择应用程序类型选择“基于对话框”。对于五子棋这种相对简单的单窗口程序基于对话框的模式是最简单的它自动生成了一个对话框窗口作为主界面省去了我们手动管理主框架窗口、菜单栏的麻烦。项目样式保持默认的“MFC标准”即可。高级功能这里可以取消勾选“ActiveX控件”、“公共控件清单”等我们用不到的功能让项目更简洁。但务必确保“使用MFC”的选项是“在共享DLL中使用MFC”对应动态链接或“在静态库中使用MFC”对应静态链接生成独立EXE。初学者为了省事可以先选静态库。点击“完成”Visual Studio就会为我们生成一个最基本的MFC对话框应用程序框架。编译并运行F5你应该能看到一个空白的对话框窗口。我们的五子棋棋盘和所有交互都将在这个对话框上构建。注意很多新手在这一步会被MFC自动生成的一大堆代码吓到特别是Gomoku.cpp,GomokuDlg.cpp等文件。不要慌我们初期大部分工作都集中在对话框类CGomokuDlg中进行。自动生成的代码负责搭建了程序的骨架我们接下来要做的是为这个骨架增添血肉。2.3 界面布局规划与控件添加我们的五子棋游戏界面主要包含以下几个部分棋盘区域用于绘制19x19的网格和棋子。我们将使用MFC的GDI图形设备接口在对话框上直接绘制而不是使用控件。因此我们需要在对话框上预留一块空白区域。信息显示区域显示当前行棋方黑棋或白棋、游戏状态游戏中、结束、获胜信息等。可以用静态文本控件Static Text来实现。控制按钮如“开始新游戏”、“悔棋”、“认输”等。使用按钮控件Button。操作步骤在VS的资源视图里打开IDD_GOMOKU_DIALOG对话框模板。删除向导生成的默认静态文本如“TODO在此放置对话框控件。”。从工具箱拖拽一个“图片控件”Picture Control到对话框上。这个控件我们将用作棋盘绘制的画布。调整其大小至合适尺寸例如400x400像素。在属性面板中将其ID修改为IDC_BOARD_AREA并将“Type”属性设置为“Rectangle”“Color”设置为“White”。将其“Control”属性设置为True这样它才能接收鼠标点击消息。在棋盘区域旁边或下方添加几个静态文本控件ID分别设为IDC_STATIC_PLAYER用于显示“当前黑方”、IDC_STATIC_STATUS用于显示“游戏中”或“白方获胜”。添加按钮控件IDC_BUTTON_NEWGAME新游戏、IDC_BUTTON_UNDO悔棋、IDC_BUTTON_RESIGN认输。适当调整布局使其美观。现在界面框架就有了。接下来我们要将这些控件与代码关联起来。3. 核心数据结构与游戏逻辑设计在开始写绘图和交互代码之前我们必须先设计好游戏的后台逻辑模型。这是整个项目的“大脑”独立于界面存在使得逻辑清晰且易于测试。3.1 棋盘与棋子状态的数据表示五子棋棋盘通常是15x15或19x19。我们选择更通用的19x19。在内存中我们需要一个二维数组来记录每个交叉点的状态。// 在 CGomokuDlg 类的头文件 (GomokuDlg.h) 中定义 #define BOARD_SIZE 19 class CGomokuDlg : public CDialogEx { // ... 其他代码 ... private: // 棋盘状态数组。0空1黑子2白子 int m_board[BOARD_SIZE][BOARD_SIZE]; // 当前行棋方。true黑方false白方 bool m_bIsBlackTurn; // 游戏是否正在进行中 bool m_bGameActive; // 记录每一步的落子位置用于悔棋。可以用std::vectorstd::pairint, int std::vectorstd::pairint, int m_moveHistory; };为什么用int而不是enum当然使用枚举类型enum Piece { EMPTY 0, BLACK 1, WHITE 2 }会让代码可读性更高这是一个值得推荐的改进。这里为了初学理解简便先用整数。3.2 胜负判定算法的实现这是五子棋逻辑的核心。算法思路是在每次落子后以该落子点为中心向四个方向水平、垂直、两个对角线进行搜索统计连续的同色棋子数量。如果任一方向达到5个则判定获胜。我们需要一个辅助函数来检查某个位置是否在棋盘内。// GomokuDlg.cpp 中 bool CGomokuDlg::IsInBoard(int row, int col) { return (row 0 row BOARD_SIZE col 0 col BOARD_SIZE); } // 胜负检查函数传入最后落子的位置 (row, col) bool CGomokuDlg::CheckWin(int row, int col) { int currentPlayer m_board[row][col]; // 当前落子方颜色 if (currentPlayer 0) return false; // 空位不可能赢 // 四个方向向量(dr, dc) // 水平(0,1), 垂直(1,0), 主对角线(1,1), 副对角线(1,-1) int directions[4][2] { {0, 1}, {1, 0}, {1, 1}, {1, -1} }; for (int d 0; d 4; d) { int dr directions[d][0]; int dc directions[d][1]; int count 1; // 当前位置已经有一颗棋子 // 向正方向搜索 for (int step 1; step 5; step) { int newRow row dr * step; int newCol col dc * step; if (IsInBoard(newRow, newCol) m_board[newRow][newCol] currentPlayer) { count; } else { break; } } // 向反方向搜索 for (int step 1; step 5; step) { int newRow row - dr * step; int newCol col - dc * step; if (IsInBoard(newRow, newCol) m_board[newRow][newCol] currentPlayer) { count; } else { break; } } // 如果任意方向连续子数达到5则获胜 if (count 5) { return true; } } return false; }这个算法的时间复杂度是O(1)因为无论棋盘多大它只检查固定最多4*936个位置效率非常高。实操心得在实现胜负判断时新手最容易犯的错误是只检查了一个方向。一定要记住需要从落子点向两个相反方向检查才能得到该方向上的总连续数。例如在水平方向上既要向右查也要向左查。3.3 游戏流程状态机我们需要管理游戏的基本状态通常是一个简单的状态机空闲Idle游戏未开始等待点击“新游戏”。进行中Playing黑方和白方交替落子。结束GameOver一方获胜或和棋。我们用m_bGameActive布尔变量就可以表示“进行中”与“非进行中”包含空闲和结束。更清晰的做法是使用枚举定义状态并在状态改变时更新界面显示。为了简化我们在“进行中”状态下根据m_bIsBlackTurn决定当前行棋方。当游戏结束时设置m_bGameActive false并弹出提示或更新状态文本。4. 图形绘制与鼠标交互实现有了后台逻辑现在我们要让游戏“看得见、摸得着”。这涉及到MFC的核心消息处理与GDI绘图。4.1 使用GDI绘制棋盘与棋子我们将绘图代码写在对话框的OnPaint()函数中或者更专业一点写在图片控件IDC_BOARD_AREA的绘制消息处理函数中。我们需要为这个控件关联一个变量并处理它的WM_PAINT消息。首先为IDC_BOARD_AREA图片控件添加一个控制变量。在对话框编辑界面右键点击该控件 - “添加变量”。变量类型选择CStatic变量名设为m_picBoard。这样我们就可以在代码中操作这个控件了。然后我们需要处理这个静态控件的自绘。一种方法是子类化Subclass这个控件但更简单的方法是直接在对话框的OnPaint()里获取该控件的设备上下文DC进行绘制。我们采用后者。在CGomokuDlg::OnPaint()函数中如果对话框没有重绘可能需要手动添加WM_PAINT消息处理函数添加棋盘绘制逻辑void CGomokuDlg::OnPaint() { CPaintDC dc(this); // 设备上下文用于绘画 // ... 其他绘制代码如果有... // 1. 获取棋盘控件的位置和大小 CRect rectBoard; m_picBoard.GetWindowRect(rectBoard); ScreenToClient(rectBoard); // 转换为客户区坐标 // 2. 创建一个兼容的内存DC用于双缓冲绘图防止闪烁 CDC memDC; memDC.CreateCompatibleDC(dc); CBitmap memBitmap; memBitmap.CreateCompatibleBitmap(dc, rectBoard.Width(), rectBoard.Height()); CBitmap* pOldBitmap memDC.SelectObject(memBitmap); // 3. 用白色填充背景 memDC.FillSolidRect(0, 0, rectBoard.Width(), rectBoard.Height(), RGB(255, 255, 255)); // 4. 计算棋盘格子的间距 int cellWidth rectBoard.Width() / (BOARD_SIZE 1); int cellHeight rectBoard.Height() / (BOARD_SIZE 1); int offsetX cellWidth; int offsetY cellHeight; // 5. 绘制棋盘网格线灰色 CPen gridPen(PS_SOLID, 1, RGB(100, 100, 100)); CPen* pOldPen memDC.SelectObject(gridPen); for (int i 0; i BOARD_SIZE; i) { // 竖线 memDC.MoveTo(offsetX i * cellWidth, offsetY); memDC.LineTo(offsetX i * cellWidth, offsetY (BOARD_SIZE - 1) * cellHeight); // 横线 memDC.MoveTo(offsetX, offsetY i * cellHeight); memDC.LineTo(offsetX (BOARD_SIZE - 1) * cellWidth, offsetY i * cellHeight); } memDC.SelectObject(pOldPen); // 6. 绘制所有棋子 for (int row 0; row BOARD_SIZE; row) { for (int col 0; col BOARD_SIZE; col) { if (m_board[row][col] ! 0) { int centerX offsetX col * cellWidth; int centerY offsetY row * cellHeight; int radius min(cellWidth, cellHeight) / 2 - 2; CBrush brush; if (m_board[row][col] 1) // 黑子 { brush.CreateSolidBrush(RGB(0, 0, 0)); } else // 白子 { brush.CreateSolidBrush(RGB(255, 255, 255)); } CBrush* pOldBrush memDC.SelectObject(brush); CPen pen(PS_SOLID, 1, RGB(0, 0, 0)); CPen* pOldPen memDC.SelectObject(pen); memDC.Ellipse(centerX - radius, centerY - radius, centerX radius, centerY radius); memDC.SelectObject(pOldBrush); memDC.SelectObject(pOldPen); brush.DeleteObject(); pen.DeleteObject(); } } } // 7. 将内存DC的内容一次性绘制到屏幕控件上 dc.BitBlt(rectBoard.left, rectBoard.top, rectBoard.Width(), rectBoard.Height(), memDC, 0, 0, SRCCOPY); // 8. 清理资源 memDC.SelectObject(pOldBitmap); memBitmap.DeleteObject(); memDC.DeleteDC(); }关键技巧双缓冲绘图。直接在一个DC上绘图当图形复杂或频繁刷新时会出现严重的闪烁现象。双缓冲的原理是先在内存中创建一个“画布”内存DC和位图把所有图形都画在这个内存画布上完成后再一次性将这个画布“贴”到屏幕DC上。这个操作BitBlt非常快能有效消除闪烁。这是Windows GDI编程中一个非常重要的优化技巧。4.2 处理鼠标点击落子玩家通过点击棋盘上的交叉点来落子。我们需要处理图片控件的鼠标左键按下消息。首先为IDC_BOARD_AREA控件添加WM_LBUTTONDOWN消息处理函数。在对话框编辑界面右键点击该控件 - “添加事件处理程序”选择消息类型WM_LBUTTONDOWN函数名可以叫OnLButtonDownBoard。在这个函数里我们需要做以下几件事将鼠标点击的客户端坐标转换为棋盘的行列索引。判断该位置是否为空。如果为空则根据当前行棋方更新m_board数组。调用CheckWin检查是否获胜。如果获胜更新游戏状态并提示否则切换行棋方。记录落子历史用于悔棋。强制重绘棋盘区域显示新的棋子。void CGomokuDlg::OnLButtonDownBoard(UINT nFlags, CPoint point) { // 1. 如果游戏未进行则忽略点击 if (!m_bGameActive) { CDialogEx::OnLButtonDown(nFlags, point); return; } // 2. 获取棋盘控件的位置 CRect rectBoard; m_picBoard.GetWindowRect(rectBoard); ScreenToClient(rectBoard); // 3. 判断点击是否在棋盘区域内 if (!rectBoard.PtInRect(point)) { CDialogEx::OnLButtonDown(nFlags, point); return; } // 4. 将点击坐标转换为相对于棋盘控件左上角的坐标 CPoint ptInBoard point; ptInBoard.Offset(-rectBoard.left, -rectBoard.top); // 5. 计算棋盘格子大小 int cellWidth rectBoard.Width() / (BOARD_SIZE 1); int cellHeight rectBoard.Height() / (BOARD_SIZE 1); int offsetX cellWidth; int offsetY cellHeight; // 6. 根据坐标计算行列索引 // 思路找到距离点击点最近的交叉线中心 int col (ptInBoard.x cellWidth / 2 - offsetX) / cellWidth; int row (ptInBoard.y cellHeight / 2 - offsetY) / cellHeight; // 7. 边界检查 if (row 0 || row BOARD_SIZE || col 0 || col BOARD_SIZE) { return; } // 8. 判断该位置是否为空 if (m_board[row][col] ! 0) { // 该位置已有棋子可以播放一个提示音或给出视觉反馈 return; } // 9. 落子 m_board[row][col] m_bIsBlackTurn ? 1 : 2; // 黑方为1白方为2 m_moveHistory.push_back(std::make_pair(row, col)); // 记录历史 // 10. 检查胜负 if (CheckWin(row, col)) { m_bGameActive false; CString strMsg; strMsg.Format(_T(%s方获胜), m_bIsBlackTurn ? _T(黑) : _T(白)); UpdateStatusText(strMsg); // 更新状态显示 MessageBox(strMsg, _T(游戏结束), MB_OK | MB_ICONINFORMATION); } else { // 11. 切换行棋方 m_bIsBlackTurn !m_bIsBlackTurn; UpdatePlayerTurnText(); // 更新当前玩家显示 } // 12. 强制重绘棋盘区域 m_picBoard.Invalidate(FALSE); // FALSE表示不擦除背景由OnPaint处理 CDialogEx::OnLButtonDown(nFlags, point); }注意事项坐标转换是鼠标交互中的关键和易错点。point参数是相对于对话框客户区的坐标。而我们的棋盘绘制是在m_picBoard控件区域内进行的其坐标系的原点是控件自身的左上角。因此必须通过GetWindowRect和ScreenToClient计算出控件在对话框中的位置才能将对话框坐标转换为控件坐标。这一步如果算错会导致点击位置和落子位置对不上。4.3 实现“新游戏”、“悔棋”等功能为“新游戏”等按钮添加事件处理函数就简单多了。以“新游戏”按钮为例void CGomokuDlg::OnBnClickedButtonNewgame() { // 1. 重置棋盘数组 memset(m_board, 0, sizeof(m_board)); // 2. 重置游戏状态 m_bGameActive true; m_bIsBlackTurn true; // 黑方先行 // 3. 清空历史记录 m_moveHistory.clear(); // 4. 更新界面文本 UpdatePlayerTurnText(); UpdateStatusText(_T(游戏中)); // 5. 重绘棋盘 m_picBoard.Invalidate(FALSE); }“悔棋”功能的实现void CGomokuDlg::OnBnClickedButtonUndo() { if (!m_bGameActive || m_moveHistory.empty()) { return; // 游戏结束或没有步数可悔 } // 悔一步棋需要回退两步一人一步 int stepsToUndo 2; if (m_moveHistory.size() 1) { stepsToUndo 1; // 如果只有一步只悔一步 } for (int i 0; i stepsToUndo !m_moveHistory.empty(); i) { std::pairint, int lastMove m_moveHistory.back(); m_board[lastMove.first][lastMove.second] 0; // 清空该位置 m_moveHistory.pop_back(); } // 切换回正确的行棋方悔棋后应该是上一步行棋方的对方 m_bIsBlackTurn (m_moveHistory.size() % 2 0); // 历史步数为偶数是黑方奇数是白方 UpdatePlayerTurnText(); m_picBoard.Invalidate(FALSE); }5. 功能扩展与进阶优化一个基础的五子棋已经完成了。但要让它更像一个完整的游戏我们还可以添加很多功能。5.1 实现简单的人机对战AI这是搜索热词中“人机对战五子棋”关注的点。实现一个有一定棋力的AI是个大课题但我们可以从最简单的“随机落子”或“基于规则的防守”开始。随机AI在轮到电脑下棋时在所有空位中随机选择一个落子。这毫无棋力但能跑通流程。void CGomokuDlg::ComputerMove() { if (!m_bGameActive || m_bIsBlackTurn) // 不是白方电脑的回合 return; // 收集所有空位 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(std::make_pair(i, j)); } } } if (emptyCells.empty()) return; // 随机选择一个空位 srand((unsigned int)time(NULL)); int index rand() % emptyCells.size(); int row emptyCells[index].first; int col emptyCells[index].second; // 模拟落子 m_board[row][col] 2; // 白子 m_moveHistory.push_back(std::make_pair(row, col)); if (CheckWin(row, col)) { // ... 处理电脑获胜 ... } else { m_bIsBlackTurn true; UpdatePlayerTurnText(); } m_picBoard.Invalidate(FALSE); }然后在玩家落子后OnLButtonDownBoard中切换玩家回合后调用ComputerMove()。基于规则的简单AI我们可以定义一些简单的规则比如如果电脑有可以连成五子的机会则落子获胜进攻。如果玩家有可以连成五子的机会则落子阻挡防守。否则随机落子。 实现这个AI需要写一个函数来评估棋盘上所有空位找出对自己或对手最“危险”的位置。这涉及到简单的棋型判断如活四、冲四、活三等代码会复杂很多但已经是一个很好的进阶挑战。5.2 快捷键与声音效果快捷键MFC中实现快捷键通常通过加速键表Accelerator Table或重写PreTranslateMessage函数。对于对话框程序后者更简单。在CGomokuDlg类中重写PreTranslateMessage函数BOOL CGomokuDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg-message WM_KEYDOWN) { switch (pMsg-wParam) { case VK_F2: // F2 开始新游戏 OnBnClickedButtonNewgame(); return TRUE; case Z: // CtrlZ 悔棋 if (GetKeyState(VK_CONTROL) 0) { OnBnClickedButtonUndo(); return TRUE; } break; } } return CDialogEx::PreTranslateMessage(pMsg); }声音效果可以使用Windows APIPlaySound函数。在落子或游戏结束时播放一个简单的WAV文件。#include mmsystem.h #pragma comment(lib, winmm.lib) // 在落子后播放 PlaySound(_T(place.wav), NULL, SND_FILENAME | SND_ASYNC);确保声音文件place.wav放在项目可执行文件相同的目录或指定路径下。5.3 网络对战雏形与存盘读盘网络对战这是一个更大的话题涉及到Socket编程。思路可以是将游戏状态棋盘数组、当前玩家封装成结构体通过网络在两位玩家间同步。一位玩家作为主机Server另一位作为客户端Client。每次落子后将落子位置发送给对方对方收到后更新本地棋盘并重绘。这需要处理连接、数据序列化、同步和异常断开等问题。对于初学者可以先实现本地双人对战网络功能作为未来的扩展目标。存盘/读盘这个功能相对简单。我们可以将棋盘状态m_board、当前行棋方m_bIsBlackTurn和步数历史m_moveHistory保存到一个文件中。可以使用简单的二进制格式或者更易读的文本格式比如每行保存一步棋的行列坐标。void CGomokuDlg::SaveGame(const CString filename) { CFile file; if (file.Open(filename, CFile::modeCreate | CFile::modeWrite)) { // 保存棋盘大小和当前玩家 int size BOARD_SIZE; file.Write(size, sizeof(int)); file.Write(m_bIsBlackTurn, sizeof(bool)); // 保存棋盘状态 file.Write(m_board, sizeof(m_board)); // 保存历史步数 int historySize (int)m_moveHistory.size(); file.Write(historySize, sizeof(int)); for (const auto move : m_moveHistory) { file.Write(move.first, sizeof(int)); file.Write(move.second, sizeof(int)); } file.Close(); } }读盘函数与之对应按相同顺序读取数据并恢复游戏状态最后刷新界面。6. 项目调试、发布与常见问题6.1 调试技巧与常见Bug落子位置不准这是最常见的问题。99%的原因出在鼠标坐标转换上。仔细检查OnLButtonDownBoard函数中从对话框坐标到棋盘控件坐标再到棋盘行列索引的计算过程。可以在关键步骤用TRACE或OutputDebugString输出坐标值进行调试。绘图闪烁如果没有使用双缓冲闪烁会非常严重。确保你的OnPaint或绘图函数中使用了内存DCCreateCompatibleDC和位图CreateCompatibleBitmap最后用BitBlt一次性输出。胜负判断错误重点检查CheckWin函数。确保四个方向向量定义正确并且正反两个方向的搜索都包含了。可以专门写个测试函数设置一些必胜棋型看程序是否能正确识别。程序崩溃Debug Assertion FailedMFC程序在Debug模式下有很多断言检查。常见原因有资源未释放GDI对象CPen,CBrush,CFont,CBitmap使用后没有DeleteObject()。确保每个CreateXXX都有对应的DeleteObject。指针问题使用了未初始化的指针或已经删除的指针。数组越界访问m_board时行列索引超过了BOARD_SIZE-1。在鼠标坐标转换和CheckWin函数中要仔细检查边界。6.2 项目发布与打包当你完成开发并测试无误后就可以发布程序了。编译配置在Visual Studio顶部的工具栏将“解决方案配置”从“Debug”切换到“Release”。Release版本经过了编译器优化去掉了调试信息体积更小运行更快。静态链接为了生成一个可以在没有安装VC运行库的电脑上运行的独立EXE需要在项目属性中设置静态链接。打开“项目属性” - “配置属性” - “C/C” - “代码生成”。将“运行库”从“多线程DLL (/MD)”改为“多线程 (/MT)”。注意如果使用了MFC还需要在“配置属性” - “高级”中将“MFC的使用”从“在共享DLL中使用MFC”改为“在静态库中使用MFC”。生成解决方案按F7或点击“生成” - “生成解决方案”。生成的EXE文件位于项目目录下的x64/Release或Win32/Release文件夹中取决于你的平台。分发将这个EXE文件连同任何你项目依赖的资源文件如图片、声音一起打包就可以分发给别人使用了。6.3 性能优化与小技巧局部刷新我们的棋盘重绘是整体刷新。如果棋盘很大且棋子很多每次重绘全部区域可能效率不高。可以只重绘发生变化的部分区域比如新落子的位置及其周围一小片。通过InvalidateRect函数指定需要重绘的矩形区域然后在OnPaint中根据这个区域进行局部绘制。这需要更精细的绘图逻辑。使用更高效的绘图APIGDI是比较老的技术。对于更复杂的图形或动画可以考虑使用GDI或Direct2D。但对于五子棋这种静态棋盘GDI完全足够。AI思考时间如果你的AI算法比较复杂计算一步棋需要较长时间一定要把这个计算过程放在一个单独的工作线程中避免阻塞主UI线程导致界面“卡死”。在计算期间可以显示一个“思考中…”的提示并禁用棋盘点击。MFC中可以使用AfxBeginThread来创建工作者线程。走到这一步你已经拥有了一个功能完整的、自己亲手打造的VC五子棋游戏。从环境搭建、界面设计、逻辑编码到调试发布你完整地走了一遍Windows桌面应用开发的核心流程。这个过程中积累的经验——消息处理、GDI绘图、数据结构设计、算法实现——远比游戏本身更有价值。你可以以此为起点尝试添加更强大的AI如博弈树搜索、启发式评估或者挑战网络对战功能甚至用类似的框架去开发其他棋类或桌面小工具。编程的乐趣就在于这种从无到有、不断打磨和扩展的创造过程。