资讯动态

用Windows SDK从零写中国象棋:Win32图形编程实战

发布时间:2026/9/8 11:15:24 来源:尧图企业网站定制
简介这套基于 Windows SDK 的象棋程序开发资料定位给 Windows 桌面应用开发初学者和棋类游戏编程爱好者适合想通过一个完整实战项目将 C 基础、图形界面、Windows 消息机制和人工智能算法串联起来的读者。压缩包共38个文件、约24KB体积虽小但结构完整既有 VC 头文件与实现源码.h/.cpp、工程配置.dsp/.dsw也有界面所用图标与光标.ico/.cur、资源脚本.rc和 readme、说明文档方便对照工程组织方式逐模块理解。已有 123 人学习下载。资料以“Windows SDK 实现中国象棋”为主线覆盖了 GDI 绘制棋盘与棋子、鼠标拖放事件处理、棋局二维数组与棋子类设计、合法走法判定、将军检测、多线程交互以及 Minimax 配合 Alpha-Beta 剪枝乃至蒙特卡洛树搜索的 AI 思路并体现了 Visual Studio 调试与打包发布流程。对想系统掌握 Windows 原生图形编程、并希望将博弈算法落到实际棋类应用中的开发者是一份小巧而完整的参考。 不知道有多少人跟我一样工作前几年写了大量的 Web 页面和 Java 后台突然有一天想给自己补一堂真正的桌面编程课。我当时就是这么想的于是拿了一个再经典不过的题目练手用 Windows SDK也就是 Win32 API从零写一个中国象棋。谈不上多高深但这一趟走完我对 Windows 的消息循环、GDI 绘图、句柄资源这些概念算是彻底摸透了。这期内容就把整个项目的思路、结构、核心实现和踩坑记录完整聊一遍。无论你是刚准备从高级语言转去碰一点底层编程还是想找个周末项目扎实练练 C/C这篇都值得按步骤跟着做一遍。我先说结论用 Windows SDK 做象棋不是最省事的路线但一定是最能把 Windows 图形界面底层机制讲明白的路线。1. 为什么选 Windows SDK 写象棋而不是上 QT 或 Unity我给自己定的要求很简单不用 MFC、不用 Qt、不用任何游戏引擎直接用 SDK 提供的窗口、消息、GDI 图形接口在一个窗口里把棋盘画出来把鼠标交互做出来把象棋规则跑通。这个选择的理由不是为了炫技而是因为 Win32 是 Windows 平台最底层的公开 API 之一你在这里写的每一个CreateWindowEx、每一条WM_PAINT处理逻辑都是在给之后的任何框架打底。1.1 这个项目到底解决了什么问题你去看市面上的象棋源码大部头都在做极小极大搜索算法、评估函数棋盘本身反倒几句话就带过了。我做的这个版本反过来重心放在用原生 GDI 把棋盘画得漂亮、交互做得顺手、走法逻辑写得清楚上AI 只留了一个最简单的版本目的就是把 Windows 客户区绘图、消息响应、窗口重绘这一整套脉络讲透。说白了这个项目最适合的人群是会一点 C 语言基础写过控制台程序但对窗口程序里的程序入口wWinMain长什么样还没有概念的人。或者你已经在用 C#/Java 写业务想看看它们底下的 Win32 层到底干了什么。你要是想直接做一个带人工智能、带开局库的商业级象棋那可以看完本文后继续扩展但第一版别指望一上来就全做完。1.2 技术选型的取舍对比技术方案开发效率对 Windows 原理的覆盖度部署体积依赖Win32 SDK GDI偏低极高极小几 MB系统自带MFC一般中等封装过多小系统 DllQt Widgets高低较大需带运行库Electron / Web高约等于零巨大需带 ChromiumUnity高低大引擎运行库我当时选第一行的动机很直接既然要补 Win32 的课就别让框架替我做本该我做的事。GDI 虽然老但画棋盘、画棋子、处理鼠标命中测试恰恰是它的舒适区。1.3 整体项目结构项目本身不复杂单文件大约 1200 行 C 代码分这几个模块main/wWinMain注册窗口类并进入消息循环Board数据模型9 列 10 行的二维数组0 表示空红黑用正负值区分DrawBoard/DrawPiece用 GDI 画线、画圆、写字OnLButtonDown鼠标点击位置换算成棋盘坐标完成走子逻辑CheckMove按棋子类型分别校验走法GameState回合控制、胜负检测、悔棋用栈保存这种结构的好处是逻辑与绘制分离棋盘数组负责规则到底是什么样绘制函数负责屏幕上长什么样互不干扰。2. 核心细节解析棋盘建模、走法规则与绘制设计的工程取舍在动手写代码之前有几件事值得先想清楚。它们是整个项目能不能顺利走完的关键。2.1 棋盘数据结构不是 10×10而是 9×10中国象棋棋盘是 9 条竖线乘 10 条横线。为保证索引直观绝大多数人会用int board[10][9]下标从 0 开始第 0 列到第 8 列对应棋盘的左到右第 0 行是最上方黑方那一排第 9 行是红方底线。每个格子的值我规定如下0表示空正数表示红方棋子比如1红帅、2红仕、3红相、4红马、5红车、6红炮、7红兵负数表示黑方棋子对应-1黑将、-2黑士……以此类推这样设计有一个很小的好处判断这个棋子是不是我的直接看符号判断两个棋子是不是同一边直接相乘看是否大于 0非常省事。2.2 走法校验把规则拆成共性 个性我见过不少人把每一种棋子的走法独立写死结果一个if-else链写了三百行后来加规则加到自己都晕。我的做法是先抽象共性再拆分个性。共性的部分就三条目标位置不能是自己的棋子目标位置如果是对方的棋子涉及吃子移动后己方的将/帅不能直接暴露在对方火力下这个点我称为将军检测必须全盘扫描实现个性的部分每一种子都很简单我挑几个典型的车先判断是否同行或同列再检查路径中间是否全为 0这个可以用一个IsPathClear函数完成。马走日字即|dx|1 |dy|2或反过来同时检查蹩马腿也就是 dx 为 1 时要保证board[y][xdx]这个中间点是空的。象/相走田字|dx|2 |dy|2还要检查塞象眼以及不能过河。炮走直线但是吃子时中间必须恰好隔了一个棋子不吃子时必须畅通无阻。兵/卒红兵过河前只能前进过河后可以前进或左右不能后退。将/帅走一步直线且不能出九宫如果有机会照面可以直接飞过去吃对方的将也就是两将之间无遮挡时允许直对这个规则我在代码里单独判断了。这里我想强调一个工程上的习惯校验函数最好只写这条规则本身不要把阵营、回合这些全局状态混进去。你把它做成int CheckMove(int board[10][9], int fromX, int fromY, int toX, int toY, int side)后续做 AI 搜索或者做悔棋时都是直接可复用的。2.3 坐标换算客户区像素坐标与棋盘格的映射程序里最容易被新手搞晕的就是鼠标点下来的坐标(x, y)怎么变成第几行第几列。换算逻辑不复杂但必须在WM_SIZE或者初始化时就算好一个基准刻度。我的方案是用固定棋盘格大小来画比如每个格子是 60 像素左上角留边距MARGIN45。那么某个像素点落在哪个格子的计算公式就是int col (mouseX - MARGIN CELL_SIZE / 2) / CELL_SIZE; int row (mouseY - MARGIN CELL_SIZE / 2) / CELL_SIZE;加CELL_SIZE / 2是为了四舍五入这样你点在格子的边界附近时系统倾向于把点判给更近的那个格。如果整体窗口可以缩放就要记录一个缩放系数scale把mouseX先除以缩放系数再往格子坐标上换算。第一版建议做固定大小窗口别一上来就碰多分辨率适配否则排查起来非常痛苦。2.4 绘制流程GDI 怎么把棋盘画明白绘制方面GDI 的常规套路是BeginPaint(hwnd, ps)拿到设备上下文 DC用CreatePen创建画笔SelectObject选入 DC用MoveToExLineTo画线用Ellipse画圆形棋子用DrawText或TextOut绘制楚河汉界的字EndPaint结束绘制这里有个小细节棋盘的线条应该用一个 dc 画完一整帧而不是每画一条线就CreatePen一次。画笔是 GDI 对象反复创建和销毁既浪费资源又不好看。正确做法是在WM_CREATE里创建好g_hPen和g_hBrush在WM_DESTROY里统一释放。另外画棋子的时候我建议直接用Ellipse画实心圆然后用SetBkMode(hdc, TRANSPARENT)DrawText在圆里写字。这个组合比自己算文字坐标要稳得多。文字颜色可以用SetTextColor调整红黑双方分别是RGB(180, 40, 40)和RGB(20, 20, 20)选的颜色别太纯否则屏幕下看久了会刺眼。3. 实操过程与核心环节实现从能画到能下分步走理论讲再多不如代码跑起来。下面我把整个项目的落地过程拆成四个阶段每个阶段都有相对完整的代码片段和说明。3.1 窗口与消息循环先把壳跑起来SDK 程序的入口是wWinMain这一步要做的事是注册窗口类、创建主窗口、进入消息循环。一个最简骨架长这样int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrev, PWSTR pCmdLine, int nCmdShow) { WNDCLASS wc { 0 }; wc.lpfnWndProc WndProc; wc.hInstance hInstance; wc.hCursor LoadCursor(NULL, IDC_ARROW); wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); wc.lpszClassName LChineseChessWindow; RegisterClass(wc); HWND hwnd CreateWindowEx( 0, LChineseChessWindow, L中国象棋 - Win32 SDK 版, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 640, 720, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, nCmdShow); MSG msg; while (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return 0; }注意wWinMain是 Unicode 版本入口如果编译器报找不到入口去项目属性里检查 Character Set 是否选为 Use Unicode Character Set。这一阶段最常见的坑就是窗口类名写错或者WndProc返回值不符合规范导致窗口一闪而过。3.2 棋盘绘制把底图一次性准备到位关于棋盘绘制第一个建议是把静态底图缓存到一个内存位图上不要每次WM_PAINT都重画一遍网格。这个缓存的含义是只在棋盘大小不变的情况下创建一次CreateCompatibleBitmap把棋盘线、河界字、九宫斜线都画上去之后每次重绘只需要BitBlt把缓存底图贴出来再在上面单独画动态的棋子。这个方案的好处一来是流畅二来把绘制逻辑分成静态层 动态层后面做动画或者选中的高亮都非常自然。贴缓存的核心代码是这样void DrawBoardCached(HWND hwnd) { HDC hdc GetDC(hwnd); if (g_hMemDC NULL) { g_hMemDC CreateCompatibleDC(hdc); g_hMemBmp CreateCompatibleBitmap(hdc, WIN_WIDTH, WIN_HEIGHT); SelectObject(g_hMemDC, g_hMemBmp); } // 画线、画九宫、画河界全画到 g_hMemDC 上 // 循环调用 DrawPieceOnDC ReleaseDC(hwnd, hdc); }绘制九宫斜线的时候注意用两个MoveToEx/LineTo分别画两条对角线。河界的楚河 汉界四个字我用了DrawText加DT_CENTER | DT_VCENTER字体用系统默认的CreateFont字号 28加粗。这里有个特容易踩的坑中文字体必须指定DEFAULT_CHARSET或者明确GB2312_CHARSET否则你看到的是一堆问号。3.3 鼠标交互点击、选中、走子一气呵成WM_LBUTTONDOWN是象棋交互的核心。在这个事件里执行三个动作把客户区坐标换算成棋盘坐标col, row判断当前棋子是否为当前回合方如果是就记为选中棋子同时记下它的坐标如果已经有选中棋子而且目标格合法就执行移动然后InvalidateRect重绘这里的选中状态建议单独用四个全局变量维护int g_SelFromX -1; int g_SelFromY -1; BOOL g_bHasSelected FALSE; int g_nTurn 1; // 1 红方-1 黑方选中之后最好给被选中的棋子画一个高亮外框或者直接用半透明绿圈标注这只需要在绘制函数里加一个判断。在移动时CheckMove返回 1 才真正改棋盘数组然后切换回合再做是否将军“是否被将死”的检测。这里我建议加一个轻量级的音效用PlaySound播放系统提示音即可别引入额外的音频库否则第一版会没完没了。3.4 走法规则与胜负判定逻辑闭环对所有棋子按类型分发校验这一段的伪代码如下涉及具体实现时我建议写上一堆switch而非if-else在编译器优化上对跳转表更友好int CheckMove(int board[10][9], int fx, int fy, int tx, int ty, int side) { int piece board[fy][fx]; if (piece 0) return 0; if ((side * piece) 0) return 0; // 不是己方棋子 int target board[ty][tx]; if ((side * target) 0) return 0; // 目标位置是己方 switch (abs(piece)) { case 1: return CheckGeneral(...); case 2: return CheckAdvisor(...); case 3: return CheckElephant(...); case 4: return CheckHorse(...); case 5: return CheckRook(...); case 6: return CheckCannon(...); case 7: return CheckPawn(...); } return 0; }胜负检测每一回合移动完成之后做先定位双方的将/帅然后用可吃将的判定去遍历所有己方棋子如果无论怎么走都无法解除被将军的状态就输出红方胜/黑方胜。一个更简单粗暴的替代方案是只要走完一步后对方的将出现在己方任意棋子的合法移动目标里就算将军下一步如果对方没有合法走法能缓解直接判断胜负。3.5 完整循环的实际体验我自己跑完这套流程之后整个程序已经能完成红黑交替走子、车马炮兵士相帅各种吃子规则、蹩马腿塞象眼全都能正确拦截、将帅不能见面、胜负报错弹窗这些功能全部可玩。核心代码量大概不到 1000 行而且没有引入第三方库Debug 模式编出来的 exe 也就几百 KBRelease 模式更小。4. 常见问题与排查技巧实录这段是我最想分享的部分。很多坑不是看 MSDN 文档能提前预料到的得真刀真枪跑出 bug 才知道。4.1 闪屏GDI 双缓冲的具体姿势新手做图形界面十个有九个会遇到重绘时疯狂闪的问题。原因是WM_PAINT里每次都直接往屏幕 DC 上绘制绘制过程会被系统看到。解决办法就是这一节开头说的内存 DC BitBlt。我把双缓冲的关键点重新总结一下内存 DC 只在创建时生成一次不要每次WM_PAINT都CreateCompatibleDC棋子画到内存 DC 上然后整个内存 DCBitBlt到窗口 DC如果棋盘格子大小会变化内存位图要删掉重建否则画面会花屏这个写上可能只有几行但实际解决闪屏问题了我两天。4.2 中文字体全部变成问号或方框这个问题的原因是CreateFont没有正确指定字符集。正确的调用方式里lfCharSet至少要用DEFAULT_CHARSET最稳的是GB2312_CHARSET。同时注意窗口类名和CreateWindowEx的标题字符串在 Unicode 工程里必须加L前缀。4.3 WM_PAINT 里写了耗时逻辑导致窗口拖动卡顿我第一版把棋子绘制时用了大量GetStockObject和CreateSolidBrush导致每次WM_PAINT都会新建一堆 GDI 对象拖动窗口时 CPU 占用直接拉到 20%。后来改成在WM_CREATE里一次性创建所有固定的画刷WM_DESTROY里释放肉眼可见顺滑很多。GDI 对象要记得用GetObject或DeleteObject清理这点跟内存泄漏一样重要。4.4 窗口大小变化后棋盘坐标错乱如果你没有锁定窗口最小尺寸用户把窗口拉高拉宽棋盘位图画好之后会被拉伸鼠标点击位置全部错位。我的处理方式是在WM_GETMINMAXINFO里锁定最小尺寸比如 600×680。再往后要支持等比例缩放需要引入一个统一的scale变量并在绘制和鼠标换算里同时使用这也是很常规但很容易漏的做法。4.5 Win32 环境配置的几个坑很多初学者卡住不是因为代码而是编译环境。我用的是 Visual Studio 社区版创建一个 C 空项目把主文件后缀改成.cpp并在源文件里写#include windows.h即可。要注意入口点如果提示找不到确认项目使用 Unicode 字符集如果只有一个 cpp 文件不要创建预编译头否则生成时会多出一堆没必要的依赖链接时不需要额外添加任何 libWin32 API 默认已经链接了user32、gdi324.6 消息循环死循环的排查程序启动后窗口白屏没反应十有八九是WndProc没写默认分支。任何你不想处理的WM_XXX消息都要交给DefWindowProc。很多人只处理了WM_DESTROY和WM_PAINT其他消息一概返回 0鼠标消息就全部丢失了。把WM_ERASEBKGND也注意一下如果你不需要系统擦背景返回 1 可以避免闪屏。5. 后续还能怎么扩展我做完基础版本之后明显感觉 Win32 的底子扎实了不少。后面如果继续迭代可以考虑的方向很多。最直接的一档是给电脑加上简单 AI比如最低级的随机走子比拼手速都能玩再往上就是极小极大搜索 Alpha-Beta 剪枝剪枝深度到 3 层已经能对付普通玩家了。再复杂一点可以加悔棋栈、对战计时、网络对战、残局导图这些进阶项全都在现有架构上扩展即可不需要推翻重来。如果追求画面更好看可以把 GDI 换成 GDI 或者 Direct2D加抗锯齿和渐变底色棋子的立体感瞬间上一个档次。不过这一步对整局的算法逻辑没有影响棋盘数组还是同一个board[10][9]只要你当初没有把绘图和逻辑搅在一起替换起来会非常舒服。做了这个项目之后我最大的体会是很多现代框架里拖个控件进去就能跑的功能背后其实都是消息、句柄、设备上下文这些老东西在工作。Windows SDK 的学习曲线确实陡但只要你跨过了wWinMain和WndProc这道坎后面每一行代码都会让你对 Windows 本身的理解加深一点。想找个周末项目打底的话写个象棋绝对是个不错的选择。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价