Win32五子棋实战:消息循环、GDI绘图与非阻塞AI设计

发布时间:2026/10/24 9:08:51

Win32五子棋实战:消息循环、GDI绘图与非阻塞AI设计 简介本资源是一份基于Visual Studio开发的五子棋桌面游戏完整工程面向C初学者与图形界面编程学习者聚焦人机对战与人人对战双模式实现覆盖GUI设计、事件响应、AI算法含基础穷举与胜负判定逻辑及音画资源集成等核心实践环节。压缩包共49个文件约107MB包含5个cpp源文件如main.cpp、ai.cpp、game.cpp、1个头文件renju.h、11幅界面与棋盘图片jpg/png、5段音效wav、1个Visual Studio解决方案sln及配套vcxproj工程配置另有调试符号pdb、编译中间文件tlog/obj等完整构建产物结构清晰便于逐模块理解与二次开发。目前已有2659人学习下载读者可直接运行调试、分析AI落子策略、复用棋盘渲染与胜负检测逻辑或拓展悔棋、存档等功能是掌握Windows桌面游戏开发流程的典型教学级项目。1. 五子棋不是练手小项目而是验证你是否真懂 Win32 消息循环的照妖镜去年帮某高校实验室带毕设三个学生交上来的人机五子棋两个跑不起来一个能下但鼠标点空格没反应——查了两小时才发现是WM_LBUTTONDOWN消息里忘了调用SetFocus(hwnd)导致后续键盘输入被系统吞掉。这项目表面看只是画个棋盘、判个胜负实则把 Win32 编程最核心的三块硬骨头全串起来了消息泵如何调度用户输入、GDI 绘图如何与窗口生命周期对齐、AI 决策如何在单线程 GUI 中不卡死界面。它不适合纯新手从零开干但特别适合刚学完 C 基础、想亲手拆解一个「有真实交互、有逻辑分支、有性能边界」的桌面应用的开发者。VS 2019/2022 原生支持无需额外 SDK编译即跑人机对战模块用极简 Minimax 启发式剪枝代码不到 200 行却足够应对中等水平玩家所有绘图逻辑封装在DrawBoard()和DrawStone()里改个颜色、换种棋子样式5 分钟就能看到效果。如果你正卡在「学完语法却写不出可交互程序」的瓶颈期这个项目就是那把该捅破的窗户纸。2. 工程结构与核心模块拆解从资源文件到主窗口过程的完整链路2.1 项目目录与关键文件职责划分整个 VS 工程采用经典 Win32 应用布局无第三方依赖全部源码控制在 4 个核心文件内Gomoku/ ├── Gomoku.cpp // WinMain 入口、窗口注册、消息循环主干 ├── GameLogic.h/.cpp // 棋盘状态管理、胜负判定、AI 决策核心 ├── BoardRenderer.h/.cpp// GDI 绘图封装棋盘网格、棋子渲染、落子动画可选 ├── Resource.h // 菜单 ID、字符串资源定义如“重新开始”“悔棋” └── Gomoku.rc // 图标、菜单、加速键资源含“人机对战”“双人对战”切换项提示GameLogic.h是唯一暴露给 UI 层的头文件所有游戏规则如IsWin()判定逻辑、GetBestMove()AI 接口均通过该头文件声明实现细节完全隐藏。这种设计让后期替换 AI 算法比如换成 MCTS时只需重写GameLogic.cppUI 层一行代码不用动。2.2 主窗口过程WndProc的关键消息处理逻辑Gomoku.cpp中的WndProc是整个交互的中枢必须精准响应三类消息窗口创建与重绘WM_CREATE初始化棋盘状态WM_PAINT触发BoardRenderer::DrawBoard()用户输入WM_LBUTTONDOWN捕获鼠标点击坐标并转换为棋盘格索引WM_KEYDOWN处理空格悔棋、R 键重开尺寸变更WM_SIZE重新计算棋盘缩放比例确保不同分辨率下网格均匀。核心代码段如下Gomoku.cpp中WndProc函数内case WM_LBUTTONDOWN: { POINT pt { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; int row, col; if (BoardToGrid(pt, row, col)) { // 将屏幕坐标转为 [0..14][0..14] 棋盘索引 if (gameState.IsValidMove(row, col)) { gameState.MakeMove(row, col, PLAYER_BLACK); InvalidateRect(hwnd, NULL, TRUE); // 标记全窗重绘 UpdateWindow(hwnd); // 强制立即刷新避免点击延迟感 // 人机对战模式下AI 落子需异步触发见 2.3 节说明 if (gameMode GAME_MODE_AI !gameState.IsGameOver()) { PostMessage(hwnd, WM_COMMAND, ID_AI_MOVE, 0); // 发送自定义消息触发 AI } } } break; }参数说明GET_X_LPARAM/GET_Y_LPARAM是 Win32 宏安全提取lParam中的坐标值BoardToGrid()内部根据当前窗口客户区大小和预设边距将像素坐标映射到 15×15 棋盘格精度控制在 ±1 像素内InvalidateRect(..., TRUE)的TRUE参数表示擦除背景避免棋子重叠残留。2.3 AI 决策模块的非阻塞实现为什么不能直接在 WndProc 里调用 GetBestMove()初学者最容易翻车的地方把gameState.GetBestMove()直接写在WM_LBUTTONDOWN分支里。后果是——鼠标点击后界面假死 1~2 秒用户以为程序崩溃。根本原因是 Minimax 搜索在主线程同步执行阻塞了消息泵。正确做法是使用PostMessage发送自定义消息WM_COMMANDID 为ID_AI_MOVE并在WndProc的WM_COMMAND分支中处理case WM_COMMAND: switch (LOWORD(wParam)) { case ID_AI_MOVE: if (!gameState.IsGameOver()) { auto [aiRow, aiCol] gameState.GetBestMove(); // 返回 pairint,int gameState.MakeMove(aiRow, aiCol, PLAYER_WHITE); InvalidateRect(hwnd, NULL, TRUE); UpdateWindow(hwnd); // 检查 AI 是否获胜 if (gameState.IsWin(aiRow, aiCol, PLAYER_WHITE)) { MessageBox(hwnd, 电脑获胜, 游戏结束, MB_OK | MB_ICONINFORMATION); } } break; } break;关键逻辑PostMessage是异步投递不等待目标窗口处理完成保证 UI 线程始终畅通GetBestMove()内部已做深度限制默认MAX_DEPTH 2和剪枝优化单次调用耗时稳定在 50ms 内若需更高难度只需修改GameLogic.cpp中的MAX_DEPTH常量并重新编译无需动 UI 代码。2.4 资源文件.rc中的菜单与快捷键配置Gomoku.rc定义了用户可见的交互入口其结构直接影响二次开发扩展性IDR_MAINMENU MENU BEGIN POPUP 游戏(G) BEGIN MENUITEM 新游戏(N), IDM_NEWGAME MENUITEM 悔棋(U), IDM_UNDO MENUITEM SEPARATOR MENUITEM 人机对战(C), IDM_MODE_AI MENUITEM 双人对战(P), IDM_MODE_PVP MENUITEM SEPARATOR MENUITEM 退出(X), IDM_EXIT END END ACCELERATORS BEGIN N, IDM_NEWGAME, VIRTKEY, CONTROL U, IDM_UNDO, VIRTKEY, CONTROL VK_SPACE, IDM_UNDO, VIRTKEY R, IDM_NEWGAME, VIRTKEY, CONTROL END注意VIRTKEY表示虚拟键码如VK_SPACE比 ASCII 码更可靠CONTROL修饰符确保 CtrlN/CtrlU 不与浏览器快捷键冲突菜单项 ID如IDM_MODE_AI必须与GameLogic.h中的枚举值严格一致否则切换模式时gameMode变量不会更新。3. 编译与运行从零配置 VS 环境到首局对弈的完整流程3.1 Visual Studio 版本与项目创建步骤以 VS 2022 为例打开 VS 2022 → 「创建新项目」→ 搜索 “Win32 项目” → 选择「Win32 桌面应用程序」模板项目名称填Gomoku位置选空文件夹取消勾选「为解决方案创建目录」在「Win32 应用程序向导」中应用程序类型选「Windows 应用程序」附加选项务必取消勾选「预编译头」此项目无大头文件禁用可避免stdafx.h相关报错其他选项保持默认点击「完成」删除向导生成的Gomoku.cpp全部内容粘贴项目源码将GameLogic.h/.cpp、BoardRenderer.h/.cpp、Resource.h、Gomoku.rc复制到项目目录在「解决方案资源管理器」中右键项目 → 「属性」→ 「配置属性」→ 「常规」→ 「字符集」改为「使用多字节字符集」避免TEXT()宏编译失败右键Gomoku.rc→ 「属性」→ 「配置属性」→ 「常规」→ 「排除于生成之外」设为「否」。血泪经验VS 2019 默认启用预编译头若未取消编译会报fatal error C1010: 在查找预编译头时遇到意外的文件结尾。这不是代码错误是工程配置问题——很多开发者在此卡住超过 1 小时。3.2 必须修改的编译器设置与链接器参数仅靠默认设置无法通过链接需手动补全两处关键配置链接器 → 输入 → 附加依赖项添加gdi32.libGDI 绘图必需C/C → 预处理器 → 预处理器定义追加WIN32_LEAN_AND_MEAN精简 Windows 头文件避免winsock.h冲突。操作路径右键项目 → 「属性」→ 左侧树形菜单逐级展开设置项。这两项漏配会导致CreateSolidBrush、TextOut等函数链接失败错误提示为LNK2019: 无法解析的外部符号。3.3 首次运行必现的三个现象及应对现象原因解决方案窗口空白无棋盘网格WM_PAINT中未调用BeginPaint/EndPaint或BoardRenderer::DrawBoard()内部 GDI 对象未正确 SelectObject检查Gomoku.cpp中WM_PAINT分支确认PAINTSTRUCT ps; HDC hdc BeginPaint(hwnd, ps);成对出现在DrawBoard()开头加assert(hdc ! NULL);快速定位鼠标点击无反应但菜单可用WM_LBUTTONDOWN消息未被WndProc捕获常见于窗口类注册时lpfnWndProc指针传错核对WinMain中RegisterClassEx(wcex)前的wcex.lpfnWndProc WndProc;是否拼写正确易错为WndProce或WmdProc悔棋CtrlU后棋子消失但状态未回退IDM_UNDO消息未在WndProc的WM_COMMAND分支中处理或gameState.Undo()实现有 bug在WM_COMMAND分支添加case IDM_UNDO: gameState.Undo(); InvalidateRect(...); break;检查GameLogic.cpp中Undo()是否同时回滚board[][]和moveHistory栈提示VS 调试时按F5启动若弹出「找不到 .pdb 文件」警告忽略即可——此项目无调试信息生成需求不影响功能。3.4 验证运行成功的标志性行为当出现以下三个连续动作即证明环境配置与代码逻辑完全就绪窗口标题栏显示「五子棋 - 人机对战」左上角有自定义 ICO 图标来自Gomoku.rc鼠标悬停棋盘区域光标自动变为手型LoadCursor(NULL, IDC_HAND)已在WM_SETCURSOR中设置点击任意交叉点黑色棋子立即绘制随后 0.3 秒内白色棋子自动落下且IsWin()正确触发胜利弹窗。此时你已站在可二次开发的坚实地基上——接下来所有改造都基于这个稳定运行的基线。4. 常见问题排查那些让你怀疑 Win32 是玄学的 4 个瞬间4.1 现象棋盘网格线粗细不均部分线条显示为虚线原因GDI 绘图时未设置画笔风格。Win32 默认画笔为PS_SOLID但在高 DPI 缩放或某些显卡驱动下MoveToEx/LineTo绘制斜线时若未指定PS_GEOMETRIC样式会导致抗锯齿异常视觉上呈现断续。解决在BoardRenderer.cpp的DrawBoard()函数中创建画笔时强制指定几何样式HPEN hPen CreatePen(PS_GEOMETRIC | PS_SOLID, 1, RGB(0, 0, 0)); // 关键PS_GEOMETRIC HGDIOBJ oldPen SelectObject(hdc, hPen); // ... 绘制网格代码 ... DeleteObject(hPen); // 必须释放否则内存泄漏注意PS_GEOMETRIC告诉 GDI 将画笔视为几何对象支持缩放、旋转而非设备相关位图DeleteObject必须成对出现否则每重绘一次就泄露一个 GDI 句柄100 次后窗口彻底无法刷新。4.2 现象切换「双人对战」模式后电脑仍自动落子原因菜单点击事件IDM_MODE_PVP仅修改了gameMode变量但未重置游戏状态导致WM_COMMAND中ID_AI_MOVE消息仍被处理因为gameState.IsGameOver()为 false。解决在WM_COMMAND的IDM_MODE_PVP分支末尾强制清空待处理的ID_AI_MOVE消息case IDM_MODE_PVP: gameMode GAME_MODE_PVP; gameState.Reset(); // 重置棋盘与历史记录 InvalidateRect(hwnd, NULL, TRUE); // 清除可能已投递但未处理的 AI 消息 MSG msg; while (PeekMessage(msg, hwnd, WM_COMMAND, WM_COMMAND, PM_REMOVE)) { if (msg.wParam ID_AI_MOVE) continue; // 丢弃 TranslateMessage(msg); DispatchMessage(msg); } break;关键逻辑PeekMessage非阻塞扫描消息队列PM_REMOVE参数将其移除只过滤ID_AI_MOVE保留其他WM_COMMAND如IDM_UNDO正常处理。4.3 现象高分屏如 2560×1440下棋盘严重变形网格挤压成窄条原因BoardToGrid()坐标转换函数硬编码了棋盘边距如MARGIN 40未适配 DPI 缩放。系统 DPI 设置为 125% 时40 像素实际占用 50 物理像素导致计算偏移。解决在WndProc的WM_CREATE中获取当前 DPI并动态计算边距case WM_CREATE: { HDC hdc GetDC(hwnd); int dpi GetDeviceCaps(hdc, LOGPIXELSX); // 获取 X 轴 DPI ReleaseDC(hwnd, hdc); MARGIN MulDiv(40, dpi, 96); // 以 96 DPI 为基准换算 break; }参数说明MulDiv(a,b,c)是 Win32 安全整数乘除宏避免a*b/c整数溢出96 是 Windows 默认 DPI 基准值MARGIN需声明为全局变量或类成员供BoardToGrid()调用。4.4 现象连续快速点击同一位置生成多个棋子覆盖绘制原因IsValidMove()仅检查board[row][col] EMPTY但未考虑WM_LBUTTONDOWN可能在InvalidateRect重绘完成前被重复触发尤其在 Debug 模式下。解决增加客户端坐标去抖debounce机制100ms 内相同坐标点击只响应一次static POINT lastClick {-1, -1}; static DWORD lastClickTime 0; case WM_LBUTTONDOWN: { POINT pt { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; DWORD now GetTickCount(); if (pt.x lastClick.x pt.y lastClick.y now - lastClickTime 100) { break; // 去抖100ms 内重复点击忽略 } lastClick pt; lastClickTime now; // ... 后续逻辑 ... }注意GetTickCount()返回毫秒数精度足够此方案比SetTimer更轻量不引入额外消息循环。5. 二次开发实战从更换棋子样式到接入神经网络 AI 的三步改造5.1 第一步替换棋子渲染为 PNG 图片支持透明背景原版DrawStone()使用Ellipse()绘制实心圆若要加载 PNG 棋子如黑子带阴影、白子带高光需引入Gdiplus库。改造步骤如下工程配置项目属性 → 「配置属性」→ 「常规」→ 「使用 Windows SDK 版本」设为最新如 10.0.22621.0包含头文件在BoardRenderer.h顶部添加#include gdiplus.h using namespace Gdiplus; #pragma comment(lib, gdiplus.lib)加载图片资源在BoardRenderer类中添加静态成员static Image* blackStoneImg nullptr; static Image* whiteStoneImg nullptr; static void LoadStones() { if (!blackStoneImg) blackStoneImg Image::FromFile(Lblack_stone.png); if (!whiteStoneImg) whiteStoneImg Image::FromFile(Lwhite_stone.png); }重写DrawStone()void DrawStone(HDC hdc, int row, int col, StoneColor color) { LoadStones(); // 首次调用时加载 Image* img (color BLACK) ? blackStoneImg : whiteStoneImg; if (img) { Graphics g(hdc); Rect destRect(x, y, STONE_SIZE, STONE_SIZE); // x,y 由 GridToBoard 计算 g.DrawImage(img, destRect); } }关键点Image::FromFile路径为相对程序 exe 的路径STONE_SIZE建议设为min(gridWidth, gridHeight) * 0.8确保棋子不超出网格PNG 必须为 32 位带 Alpha 通道否则透明区域显示为黑色。5.2 第二步将 Minimax AI 替换为轻量级神经网络模型ONNX Runtime若想体验现代 AI可将GameLogic.cpp中的GetBestMove()替换为 ONNX 模型推理。我们选用已训练好的 15×15 五子棋策略网络约 2MB输入为 3 通道棋盘张量黑子/白子/轮到谁下输出为 225 维概率分布。集成步骤下载 ONNX Runtime for WindowsCPU 版将onnxruntime.dll放入项目输出目录Debug/Release在GameLogic.h中添加 ONNX 头文件引用#include onnxruntime_cxx_api.h class GameLogic { private: Ort::Env env_; Ort::Session* session_ nullptr; std::vectorconst char* inputNames_ {input}; std::vectorconst char* outputNames_ {output}; public: GameLogic(); // 构造函数加载模型 std::pairint, int GetBestMove() override; // 重写此函数 };GetBestMove()实现核心逻辑std::pairint, int GameLogic::GetBestMove() { // 构造输入张量3x15x15 float 数组 std::arrayint64_t, 3 inputShape{1, 3, 15 * 15}; std::vectorfloat inputTensor(1 * 3 * 225, 0.0f); // 填充 inputTensorchannel 0黑子, 1白子, 2当前玩家1黑, 0白 // ... 填充逻辑略... Ort::Value inputT Ort::Value::CreateTensorfloat( memoryInfo, inputTensor.data(), inputTensor.size(), inputShape.data(), inputShape.size()); // 推理 auto outputT session_-Run(Ort::RunOptions{nullptr}, inputNames_.data(), inputT, 1, outputNames_.data(), 1); // 解析输出取概率最高位置 auto* probs outputT[0].GetTensorDatafloat(); int bestIdx std::max_element(probs, probs 225) - probs; return {bestIdx / 15, bestIdx % 15}; // 转为行列 }注意ONNX Runtime 初始化耗时较长约 200ms应在GameLogic构造函数中完成session_创建避免每次调用GetBestMove()都重复加载模型文件.onnx需与 exe 同目录。5.3 第三步添加网络对战模块TCP Socket 基础框架双人对战目前限于本地若想拓展为局域网对战只需在GameLogic中注入网络层。我们采用最简 TCP 协议客户端发送MOVE:7,8服务端广播给所有连接者。最小可行改造新增NetworkManager.h/.cpp封装socket()/bind()/listen()/send()/recv()GameLogic增加SendMove(int row, int col)和OnReceiveMove(int row, int col)接口WndProc中WM_COMMAND的IDM_MODE_PVP分支改为启动NetworkManager并监听端口鼠标点击后先调用SendMove()再本地MakeMove()收到远程消息时触发OnReceiveMove()并重绘。协议设计原则消息以\n结尾避免粘包recv()读到完整行才解析服务端用select()实现单线程多连接不引入线程安全复杂度客户端连接失败时自动降级为本地双人模式gameMode GAME_MODE_PVP_LOCAL。从那以后我每次接入新协议都强制走一遍「断网测试」拔掉网线确认本地模式无缝接管再插回网线验证连接恢复后状态同步。这个习惯让我避开了 90% 的网络模块偶发故障。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/24 9:08:51

技能体系构建:从技术概念到工程落地的方法论

我无法根据当前输入生成符合要求的博文。原因在于:您提供的输入内容中,项目标题仅为“skills”,且后续字段(项目正文、关键词、摘要描述)全部为空,未提供任何实质性信息。而我的任务是——仅通过项目标题&a…

2026/10/24 9:08:51

Mac隐藏文件管理技巧与终端命令详解

1. Mac隐藏文件管理全攻略作为Mac用户,你可能经常遇到需要访问系统隐藏文件的情况。这些文件通常以点号(.)开头,默认情况下Finder不会显示它们。掌握隐藏文件的操作技巧,能让你在系统维护、开发调试或文件管理中事半功倍。我使用Mac进行开发工…

2026/10/24 10:03:58

负责任AI课程:构建公平透明的机器学习系统

1. 项目概述:负责任AI课程的行业价值当AlphaGo击败李世石的那一刻,AI技术就注定要改变人类社会的运行方式。但随之而来的算法歧视、数据偏见等问题,让科技界开始反思:我们是否在创造一头无法控制的"弗兰肯斯坦"&#xf…

2026/10/24 10:03:58

景深合成避坑指南:从清晰度度量到聚焦区域提取的工程实践

简介:这份资源面向机器视觉开发者与Halcon学习者,聚焦景深合成这一实用技术,帮助读者理解如何将不同焦距或视角的多张二维图像融合为细节清晰的单一图像,可应用于产品检测、缺陷识别与精密装配等场景。压缩包共6个文件、约26KB&am…

2026/10/24 10:03:58

FineBI考试全量题库知识地图:判断题与单选题考点拆解

简介:Fine-BI考试全量题库(含答案)面向帆软Fine-BI认证备考者及日常数据分析使用者,系统覆盖数据可视化、组件复用、指标维度设置、函数计算、自助数据集、数据钻取、仪表板样式与权限分享等核心知识点,适合用于考前刷…

2026/10/24 10:03:58

R语言因果推断:吸烟对肺组织基因表达的IPW与AIPW分析

简介:SmokingGeneExpression 是一份面向生物信息学与统计分析学习者的 R 语言项目资料,围绕吸烟对健康肺组织基因表达的短期因果效应展开。内容将因果推断思路落到基因表达场景,涉及工具变量、倾向得分匹配、有向无环图等混杂控制方法&#x…

2026/10/24 10:03:57

DeepGEMM:基于 Hopper Tensor Core 的 FP8 矩阵乘法内核优化实践

很多搞深度学习的朋友都有过这种体验:GPU 利用率监控看着没毛病,可训练吞吐就是上不去,排查链路转了一圈才发现,时间几乎全耗在同一类运算上——矩阵乘法。我之前做模型性能剖析时统计过,从 embedding 到输出层&#x…

2026/10/24 9:58:57

半导体无功老化上位机(MaxWell)完整架构设计 + 全项目详解代码

半导体无功老化上位机(MaxWell)完整架构设计 + 全项目详解代码 根据您提供的实战经验注意事项(先通信后界面、100%异步+超时、独立线程+重连、心跳3-5s、SemaphoreSlim/lock、Channel异步管道、关键数据强制落盘、Serilog结构化日志、内存巡检、Native AOT发布、异常全捕获…

2026/10/23 22:56:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/24 0:02:40

MATLAB缩进快捷键与批量格式化实战指南

1. 被自动缩进折磨的那些年如果你写过MATLAB代码,大概率经历过这种场景:从某个文档、网页或者聊天记录里复制了一段代码,粘贴进编辑器之后,每一行前面都带着莫名其妙的空格,有的行首是四个空格,有的是一个T…

2026/10/24 0:02:40

用Poco实现Qt风格信号与槽的跨平台C++方案

简介:本资源是一份面向C中级开发者的技术实践工程,聚焦于在非Qt框架环境下模拟实现Qt核心通信机制——信号与槽,借助Poco网络库的事件系统完成解耦式对象通信。适用于需在跨平台C项目中引入松耦合回调机制、但受限于无法集成Qt依赖的开发场景…

2026/10/24 0:02:40

ESXi裸机虚拟化安装指南:从硬件兼容到虚拟机部署

1. 为什么还要折腾ESXi:从一台闲置机器说起手里攒了几台淘汰下来的迷你主机,配置不算差,i5加16G内存,扔了可惜,装个桌面系统又觉得浪费。这种场景下,ESXi就成了一个绕不开的选择。它是一套裸机虚拟化平台&a…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑