首页 / 资讯中心 / 文章详情

Windows SDK中国象棋:从双缓冲到Alpha-Beta的完整课设实战

Windows SDK中国象棋:从双缓冲到Alpha-Beta的完整课设实战 ★ FEATURED ARTICLE
简介基于Windows SDK的象棋程序完整工程源码包面向C/C初学者及桌面游戏开发入门者重点演示如何使用Win32 API与GDI实现棋盘绘制、棋子拖放、走棋规则判断和人机对弈。压缩包共38个文件主要包含C源文件(.cpp)、头文件(.h)、图标与光标资源(.ico/.cur)以及资源脚本(.rc)另有工程文件和readme说明整体仅24KB结构紧凑。目前已有123人学习浏览适合课程设计、毕业设计或Win32编程练手。代码按模块划分AI思考、棋盘渲染、规则定义相互独立可系统学习窗口消息循环、GDI绘图、棋盘数据结构以及Alpha-Beta剪枝等算法。内含Debug与Release两种构建配置便于对比分析编译选项对希望掌握Windows原生开发流程、不依赖第三方游戏引擎的读者尤为实用。1. Windows SDK 象棋为什么说它是被低估的课设练手项目最近帮人看课设又见到了 Windows SDK 象棋 这份经典资源。说它经典是因为几乎每个 Windows 编程方向的课程设计清单里都会有它但说它被低估是因为大多数下载者把它拖进 Visual Studio 后第一个晚上基本都在跟编译错误、黑窗口、乱码和闪烁搏斗真正动手写棋规的人反而很少。这份资源本质上不是一个“游戏”而是一套完整的 Win32 消息驱动应用窗口创建、绘图、鼠标命中检测、规则引擎、简单 AI全都要你自己串起来。对新手来说它是把 Windows SDK 零散知识点焊成一个整体的最好练手项目对熟手来说它刚好适合快速验证你对 GDI 双缓冲、消息循环和博弈搜索的理解深度。2. 窗口骨架与双缓冲先让棋盘不闪屏地立起来2.1 WinMain 入口与消息循环窗口“活”过来的前提Windows SDK 程序不是从 main 开始而是从 WinMain 开始的。很多人卡在第一步就是因为把这段当模板背下来了却没有理解每个参数的作用。下面的代码是这份资源里最常见的入口写法// 窗口入口Windows SDK 程序从 WinMain 开始 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPSTR lpCmdLine, int nCmdShow) { WNDCLASS wc {0}; wc.lpfnWndProc WndProc; // 所有消息进这里处理 wc.hInstance hInstance; wc.lpszClassName LChessWnd; wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); RegisterClass(wc); HWND hWnd CreateWindow( LChessWnd, L中国象棋 - Windows SDK, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 760, 820, NULL, NULL, hInstance, NULL); ShowWindow(hWnd, nCmdShow); MSG msg; while (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return (int)msg.wParam; }这段代码里WNDCLASS是关键它相当于告诉系统“我的窗口长什么样、消息由谁处理”。其中lpfnWndProc指向我们自己实现的WndProc后面所有鼠标点击、键盘按键、绘制请求都会回调到这个函数。lpszClassName是窗口类的唯一标识CreateWindow 时第一个参数必须和它一致否则注册了类却创建不出窗口。CreateWindow的宽高参数这里写的是 760×820这是整个窗口的大小不是客户区大小所以棋盘绘制时还要再算客户区尺寸这一点后面会讲到。消息循环这一段也别跳过去理解。GetMessage在收到WM_QUIT时返回 0循环结束其他消息通过TranslateMessage做键盘消息转换再交给DispatchMessage回调到WndProc。如果这里写成了while (true)窗口关闭后程序会卡在循环里任务管理器里能看到进程不退出。常见的做法是把消息循环写成上面这样return 的msg.wParam里带的是退出码。2.2 双缓冲绘图WM_PAINT 里不要直接画棋盘拿到这份资源后新手最容易犯的错误是在 WM_PAINT 里直接拿hdc画棋盘。单看一次绘制没什么问题但你一拖动窗口、一覆盖再露出系统会不断重绘GDI 画一条线、一个圆就要几毫秒整个棋盘画完肉眼就能看到闪烁和残影。正确做法是先把整帧画到内存 DC再一次BitBlt到窗口case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); // 1. 创建与窗口DC兼容的内存DC和位图 HDC memDC CreateCompatibleDC(hdc); HBITMAP memBmp CreateCompatibleBitmap(hdc, 720, 780); HGDIOBJ oldBmp SelectObject(memDC, memBmp); // 2. 在这里画背景、棋盘线、棋子 DrawBoard(memDC); DrawPieces(memDC); // 3. 一次性把整帧复制到窗口DC BitBlt(hdc, 0, 0, 720, 780, memDC, 0, 0, SRCCOPY); // 4. 清理资源 SelectObject(memDC, oldBmp); DeleteObject(memBmp); DeleteDC(memDC); EndPaint(hWnd, ps); return 0; }逻辑说明CreateCompatibleDC创建的内存 DC 一开始什么都没有必须用CreateCompatibleBitmap分配一块与窗口尺寸匹配的画布再SelectObject把它选进内存 DC后续所有绘制才有承载的地方。BitBlt是整块拷贝画布上的内容会一瞬间全部出现在窗口上用户看不到中间绘制过程闪烁自然消失。这里有几个参数要注意。第一720×780 是客户区的宽高不是窗口宽高如果和 CreateWindow 里的 760×820 对应不上棋盘会被裁掉。第二如果窗口大小可变这块就不要写死可以在 WM_SIZE 里记录客户区尺寸或者统一用GetClientRect动态获取。第三BeginPaint和EndPaint必须成对出现BeginPaint 内部会锁定刷新区域少调 EndPaint 会导致绘制区域永远处于“正在绘制”状态。做完双缓冲还闪问题多半出在WM_ERASEBKGND。系统默认在重绘前会用背景色刷一遍窗口这一步和你的双缓冲内容叠加就会出现先白后棋盘的“闪白”。处理方法很直接case WM_ERASEBKGND: return 1; // 告诉系统背景已经处理不需要再擦返回 1 表示“背景我已经擦过了”系统就不再填充背景色。代价是内存 DC 里必须把整个背景画满如果有一块没画就会留下难看的黑斑或残影。2.3 鼠标点击到棋盘坐标四舍五入与边界裁剪鼠标点击处理的第一个坑是坐标来源。很多人习惯用GetCursorPos拿全屏坐标然后转客户区坐标这中间一旦忘记ScreenToClient落子位置就会偏移。其实鼠标消息里lParam直接就是客户区坐标低 16 位是 X高 16 位是 Y直接用就可以case WM_LBUTTONDOWN: { int mx GET_X_LPARAM(lParam); // 客户区 X不要自己 GetCursorPos int my GET_Y_LPARAM(lParam); // 棋盘左上角在客户区的位置以及每个格子的像素宽高 int boardX 40, boardY 40, cell 72; int col (mx - boardX cell / 2) / cell; int row (my - boardY cell / 2) / cell; // 超出棋盘范围直接忽略 if (row 0 || row 10 || col 0 || col 9) break; HandleClick(row, col); break; }逻辑说明(mx - boardX)先算出点击位置相对棋盘左上角的距离然后除以 cell 得到列号。 cell / 2是为了四舍五入不加这个偏移的话点子在格子左半部分会落到上一格视觉上棋子总是“偏左半格”。GET_X_LPARAM是官方推荐的取值宏比直接LOWORD(lParam)更稳因为它能正确处理高 DPI 下的符号扩展问题。boardX、boardY、cell 这三个值必须和 DrawBoard 里画棋盘线时用的是同一组常量。很多项目的点击偏移问题根源不是公式错而是绘制和点击各写了一套偏移量。我一般会把它们放进一个config.h里全局共享避免两个文件里的魔法数字互相打架。2.4 源码目录怎么分窗口、逻辑、AI 分开写拿到这类 Windows SDK 象棋源码我最先看的是它的文件结构。如果所有代码都堆在一个 cpp 里后面改任何一个函数都可能牵连整份代码重编译排查问题更是灾难。我一般会按下面这样拆这也是这套资源里比较常见的组织方式文件职责典型内容main.cpp入口与窗口过程WinMain、WndProc、消息分发board.cpp棋盘与规则棋盘数组、走法生成、将军判断ai.cpp电脑走棋评估函数、Alpha-Beta 搜索draw.cpp绘制DrawBoard、DrawPieces、选中高亮config.h全局常量棋盘偏移、格子大小、棋子尺寸这样拆的好处是改绘图效果只碰 draw.cpp调 AI 只碰 ai.cpp规则出 bug 时也能在 board.cpp 里单独验证不用从头捋几千行窗口代码。对课设来说这个结构也方便在答辩时讲清楚“哪一块是窗口管理哪一块是业务逻辑”。3. 走法生成与规则校验把棋规写成代码的四个边界3.1 棋盘用 10×9 数组还是 9×10下标方向决定后续所有函数中国象棋棋盘有 10 条横线和 9 条竖线所以棋盘数组用int board[10][9]第一维是行row第二维是列col。这个方向看似只是习惯问题但它会决定后面所有走法生成、绘制、点击换算代码的写法。如果绘制时用的是x、y坐标而规则代码里用的是row、col中间就得多一层转换非常容易出错。这份资源里的棋盘定义一般长这样// board[row][col]0 表示空 // 红方 1~7黑方 11~17用个位区分兵种、十位区分阵营 enum { R_KING 1, R_ROOK, R_HORSE, R_ELEPHANT, R_ADVISOR, R_CANNON, R_PAWN, B_KING 11, B_ROOK, B_HORSE, B_ELEPHANT, B_ADVISOR, B_CANNON, B_PAWN };为什么不用负数表示黑方因为用piece 1 piece 7判断红方、piece 11判断黑方比判断符号要直观得多而且搜索 AI 的评估函数里经常要取棋子类型用个位数字能直接piece % 10提取兵种不用再写一堆分支判断。3.2 马的蹩腿和象的塞眼走法表的偏移量设计走法生成是棋规的核心。最容易写错的是马马走“日”字但马腿位置的判断很容易漏。比较省心的写法是把 8 个方向和对应的马脚偏移直接列成一张表// 马的8个落点与对应马脚偏移 // 数组四项依次是落点行偏移、落点列偏移、马脚行偏移、马脚列偏移 static const int horse[8][4] { {-2, -1, -1, 0}, // 左上马脚在 (-1, 0) {-2, 1, -1, 0}, // 右上 { 2, -1, 1, 0}, // 左下 { 2, 1, 1, 0}, // 右下 {-1, -2, 0, -1}, // 左左 { 1, -2, 0, -1}, {-1, 2, 0, 1}, // 右右 { 1, 2, 0, 1} }; for (int i 0; i 8; i) { int nr r horse[i][0]; int nc c horse[i][1]; int lr r horse[i][2]; // 马脚位置 int lc c horse[i][3]; if (nr 0 || nr 10 || nc 0 || nc 9) continue; // 越界跳过 if (board[lr][lc] ! 0) continue; // 蹩马腿 if (board[nr][nc] ! 0 IsSameSide(board[r][c], board[nr][nc])) continue; // 不能吃己方棋子 moves.push_back(Move{r, c, nr, nc}); }这段代码的核心思路是把马的 8 种跳法拆成“落点”和“马脚”两个坐标。生成走法时不靠if判断方向而是查表拿偏移可读性和准确性都高很多后续想加“马什么时候被困”这类逻辑也方便。IsSameSide判断目标位置是否己方棋子是的话就不入走法表空位和敌方棋子都可以走吃子的合法性由后面统一校验。象的塞眼逻辑类似只是落点偏移变成 2 格对角象眼偏移变成 1 格对角而且多了“不能过河”的限制。士和将帅的走法更简单一个小偏移表就能覆盖。3.3 将军检测模拟走子、检测、还原的三段式规则校验里最容易漏掉的是“送将”——走完一步后己方老将会不会直接暴露在对方火力下。标准做法是“三段式”// 判断 side 方老将是否处于被攻击状态 bool IsInCheck(const int board[10][9], int side) { // 1. 找到己方老将位置 int kingRow -1, kingCol -1; for (int r 0; r 10; r) { for (int c 0; c 9; c) { if (board[r][c] (side RED ? R_KING : B_KING)) { kingRow r; kingCol c; break; } } if (kingRow 0) break; } // 2. 用攻击范围判断而不是真的“吃子” // 例如车的攻击路径上如果正好是老将返回 true // 马的攻击要查马脚炮的攻击要查中间隔子 // 3. 将帅对脸也要判为将军 // 检查同一列上两将之间是否没有任何棋子 }这个函数做完之后走子合法性就统一了。任何一步棋先备份整个局面执行移动然后调用IsInCheck判断己方是否被将军如果被将军就还原局面并拒绝这步棋如果安全才提交。有个容易被忽略的规则是将帅对脸。两将之间如果没有任何棋子视为将军状态这步棋不能走。这个检查最好放在IsInCheck里统一处理而不是在车炮的走法生成里单独写否则会有漏网之鱼。3.4 合法走子的提交流程完整顺序与遗漏点点击交互层面的合法走子我一般按这个顺序处理点击己方棋子高亮选中点击空位或敌方棋子先备份整个棋盘执行移动把目标位置的棋子替换掉调用IsInCheck检查己方老将是否被攻击如果被攻击说明这是送将恢复棋盘播放错误提示如果安全切换回合、刷新棋盘、触发 AI 或等待对手输入。这个流程里最容易遗漏的是第 5 步的“恢复”。很多项目只做了“能不能吃”“能不能走”却没检查走完是否送将结果就会出现双方对脸、老将直接被吃掉的诡异局面。另外悔棋功能也必须和这个流程配合悔棋恢复的不只是棋盘还有当前回合标记和选中状态否则会出现红方走完还能继续走黑子的错乱。4. 常见问题排查乱码、闪屏与点击漂移的五个真实踩坑记录4.1 棋盘区域有残影窗口拖动后一片黑现象窗口拖动出屏幕再拖回来棋盘线模糊有些区域直接变成黑色块。原因WM_PAINT 里画了内存 DC但没处理WM_ERASEBKGND。系统默认在重绘前先用背景色刷一遍窗口背景色和你内存 DC 里的透明区域叠加后就会出现黑块和残影。解决在窗口过程里加上一条case WM_ERASEBKGND: return 1;并确保内存 DC 里第一笔就是FillRect铺满整个背景色。铺背景时用客户区实际尺寸不要用写死的 720×780否则窗口大小变化后右下角会露底。4.2 标题和按钮的中文全部乱码现象窗口标题显示成“涓芥”、按钮文字变成“鏉ヨ”棋盘上棋子文字完全不可读。原因源码文件保存为 UTF-8但项目字符集选的是“多字节”编译器按本地代码页去解释字符串或者源码里写了L中文但项目没有定义UNICODE宏宽字符和窄字符混在一处。解决在 Visual Studio 项目属性里把字符集改为“使用 Unicode 字符集”所有字符串统一用L...宽字符。如果是从网上下载的源码还要检查文件编码和#pragma execution_character_set(utf-8)是否匹配。这个预编译指令放在源码最开头能保证 UTF-8 源码里的宽字符串在运行时不乱。4.3 点击落子位置偏移半个格子现象明明点在“炮”的位置棋子却落到旁边格子里越靠近窗口边缘偏移越明显。原因最常见的是没用消息里的lParam而是用GetCursorPos拿全屏坐标其次是没有处理 DPI 缩放系统把窗口内容放大显示后客户区坐标和物理像素对不上。解决在WM_LBUTTONDOWN里只用GET_X_LPARAM(lParam)和GET_Y_LPARAM(lParam)这两个宏返回的已经是客户区坐标。如果窗口支持缩放在 WinMain 开头声明 DPI 感知SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE)或SetProcessDPIAware()然后在 WM_DPICHANGED 里按比例重新计算 cell 和 boardX。4.4 红方走完黑方还能继续走子现象红方走了一步同样的回合里还能继续选中黑方棋子移动回合完全没有交替。原因点击处理里只判断了“选中的棋子是不是自己的”但判断用的不是当前回合标记而是“点中棋子时判断颜色是否等于当前玩家”。更隐蔽的原因是悔棋只恢复了棋盘数组没有恢复currentTurn导致当前回合标记错乱。解决把回合状态封装进一个GameState结构体里面包含棋盘、当前回合、选中坐标、己方被将军标记。悔棋时整体恢复GameState而不是只回滚棋盘。在HandleClick开头先判断if (GetPieceColor(clickedPiece) ! currentTurn) return;这一步能挡住 90% 的回合乱序问题。4.5 启动后界面卡顿AI 走一步要卡几秒现象点击“电脑走棋”后窗口无响应任务管理器里 CPU 占用接近单核 100%。原因AI 搜索是同步执行的深度开到 4 层以上开局阶段每层生成几十种走法再逐层递归整个搜索可能产生上万次局面评估把消息循环阻塞了。Windows 消息循环被阻塞期间窗口既不能重绘也不能响应鼠标看起来就是死机。解决把 AI 深度限制到 2 层调试阶段甚至可以用深度 1。如果坚持要 3 层在走法数量多的时候要加剪枝并且考虑把搜索放到工作线程完成后通过PostMessage通知窗口刷新。课程设计阶段深度 2 的效果已经足够“看起来会下棋”没必要为了那一步棋把程序搞到卡死。5. 让电脑会走棋一个基于 Alpha-Beta 的贪心 AI5.1 评估函数怎么写棋子价值、位置价值与兵卒过河AI 的第一步是让程序能判断一个局面大概谁占优。最朴素的思路是给每种棋子一个基础分值然后遍历棋盘己方棋子加分、对方棋子减分。下面这套价值表是网上资源里最常见的设定数值大小基本符合实战认知棋子基础分值补充说明将/帅2000被吃等于输给高分防止 AI 轻视车1000横竖长距离价值最高马450开局容易被蹩腿中后期价值上升炮450隔子吃有奇效残局弱于马象200不能过河防守为主士200九宫内护卫兵/卒100过河后可以横走价值翻倍有了基础分再叠一层位置分。位置分最简单有效的做法是兵过河后加分马在棋盘中央和河边加分士象不乱跑。实现时不用搞很复杂的表每个棋子按兵种给一个位置加分规则就行// 用 side 视角计算整个局面的分数 // 返回值为正表示 side 占优负表示 side 劣势 int Evaluate(const int board[10][9], int side) { int score 0; for (int r 0; r 10; r) { for (int c 0; c 9; c) { int piece board[r][c]; if (piece 0) continue; int type piece % 10; // 提取兵种 int base pieceValue[type]; // 查基础分值 if (type R_PAWN) { // 兵卒过河加位置分 if (IsRed(piece) r 5) base 100; if (IsBlack(piece) r 4) base 100; } score IsRed(piece) IsRed(side) ? base : -base; } } return score; }逻辑说明piece % 10拿到的是兵种IsRed(piece)判断这颗棋子属于红还是黑。己方棋子增加总分敌方棋子减少总分整个局面就变成一个一维分数。AI 搜索的目标就变成“找到让这个分数最大化的走法路线”。参数说明这套基础分值不是绝对的想让 AI 更激进就把车的分值调高想让 AI 更倾向防守就把将帅的分值调更高。位置分里兵卒过河加 100 这个数值也可以改成按过河的距离递增效果会更细腻但课设阶段没必要。5.2 两层搜索的 Alpha-Beta剪枝的两种写法评估函数只能看“当前局面”AI 要“想一步甚至想两步”才显得聪明。最简单实用的搜索是两层 Alpha-Beta当前 AI 层取最大值对手层取最小值。代码的核心结构一般是负最大写法因为这个写法不用区分 max/min 两个分支// Alpha-Beta 搜索返回 side 视角的局面分 // alpha 表示当前已知最好分数beta 是对手不允许你超过的分数 int AlphaBeta(const GameState state, int depth, int alpha, int beta, int side) { if (depth 0) return Evaluate(state.board, side); std::vectorMove moves; GenerateMoves(state.board, side, moves); // 生成 side 方所有合法走法 if (moves.empty()) { // 无棋可走被将死或困毙 return side RED ? -100000 : 100000; } for (const Move m : moves) { GameState next state; ApplyMove(next.board, m); // 模拟走这步棋 int val -AlphaBeta(next, depth - 1, -beta, -alpha, Opposite(side)); if (val alpha) alpha val; // 更新当前最优 if (alpha beta) break; // 剪枝对手不会给你更好的结果 } return alpha; }逻辑说明负最大写法的本质是我走一步棋后的局面站在对手视角看他的最优分数对我就是负的所以我们递归时调用-AlphaBeta(...)并交换 alpha、beta 的符号。剪枝发生在alpha beta时当前分支已经不可能比已知路线更好继续算下去是浪费。参数说明depth从 2 开始调。深度 2 意味着 AI 想“我走这步对手最可能怎么应”这个复杂度已经能做出吃子防守的基础表现。深度 3 开局阶段会很慢工程上需要在GenerateMoves里做走法排序——先吃车、再吃炮、再吃马把可能产生高分的走法放在前面剪枝效率会显著提升。常见做法是生成走法后按吃子价值降序排列// 简单排序先走吃大子的棋剪枝效率更高 auto MoveValue [](const Move m) { return board[m.toR][m.toC] % 10; // 目标位置的兵种编号 }; std::sort(moves.begin(), moves.end(), [](const Move a, const Move b) { return MoveValue(a) MoveValue(b); });有了这一步深度 2 的搜索在绝大多数局面下都能在几十毫秒内返回。5.3 搜索耗时与回合衔接别让 AI 阻塞消息循环搜索代码写好后调用方式决定了整个程序的流畅度。最省事的方案是在点击事件里直接同步调用case WM_LBUTTONDOWN: // ... 玩家走子逻辑 ... if (game.currentTurn BLACK) { // 同步搜索注意 depth 不要超过 2 Move aiMove SearchBestMove(game, 2, BLACK); ApplyMove(game.board, aiMove); InvalidateRect(hWnd, NULL, TRUE); game.currentTurn RED; } break;这样写的优点是逻辑简单缺点也很明显搜索期间窗口无法响应任何消息如果 depth 开高拖动窗口就会卡顿。我一般会加一个简单的耗时兜底——在进入搜索前记录时间超过 500 毫秒就强制降深度重算一次。另一个细节方面电脑走完后也要立刻刷新棋盘并检查将死或困毙。将死是当前玩家无合法走法且被将军困毙是无合法走法但没被将军。这两个状态在搜索函数里其实已经通过moves.empty()返回了极端分数但界面层需要单独弹提示否则玩家会一脸懵。6. GDI 画棋子抗锯齿与坐标修正的最后一公里Windows SDK 默认的 GDI 画圆边缘锯齿非常明显尤其是棋子这种直径 50 像素以上的圆形放大了看全是“台阶”。最后一步把绘制从 GDI 升级到 GDI是让界面观感提升一个档次的最小改动。GDI 的接入代码比想象中少。先引入库和头文件然后在 WinMain 里启动它#include gdiplus.h #pragma comment(lib, gdiplus.lib) using namespace Gdiplus; // WinMain 开头调用 GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken; GdiplusStartup(gdiplusToken, gdiplusStartupInput, NULL);启动后WM_PAINT 里的绘制逻辑保持不变只是把圆和文字用 GDI 的 Graphics 对象来画case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); Graphics graphics(hdc); graphics.SetSmoothingMode(SmoothingModeAntiAlias); // 关键抗锯齿 // 圆心棋盘左上角 格子中心 for (int r 0; r 10; r) { for (int c 0; c 9; c) { int piece game.board[r][c]; if (piece 0) continue; int cx boardX c * cell cell / 2; int cy boardY r * cell cell / 2; Color ringColor IsRed(piece) ? Color(200, 60, 50) : Color(40, 40, 40); SolidBrush brush(ringColor); graphics.FillEllipse(brush, cx - 28, cy - 28, 56, 56); } } EndPaint(hWnd, ps); return 0; }代码里的SetSmoothingMode是抗锯齿开关不设置这一行GDI 画出来的圆跟 GDI 没区别。圆心坐标用boardX c * cell cell / 2计算和点击命中检测用的是同一套参数这样绘制和输入天然对齐。GDI 还有一个额外好处是文字渲染质量更高棋子上的“车马炮兵”用 GDI 的 DrawString 画出来比 GDI 的 TextOut 平滑很多。我自己第一次做这个项目时在点击和绘制偏移上耗了三个晚上最后发现就是 boardX 写成了 42 而点击函数里写成了 40差了 2 像素。从那以后我每次拿到 Windows SDK 象棋 资源包都先检查三处WM_ERASEBKGND有没有处理、字符集是不是 Unicode、绘图偏移量和点击偏移量是不是同一组常量。这三个点过关了再谈 AI 和界面美化。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站