ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Windows SDK 象棋源码实战:Win32 消息循环与棋盘绘制详解

Windows SDK 象棋源码实战:Win32 消息循环与棋盘绘制详解 简介这份资源面向希望借助 Windows SDK 入门 Windows 桌面开发的编程学习者以一款象棋程序为实践载体帮助理解图形界面、消息机制与游戏逻辑的落地方式。压缩包共 38 个文件约 24KB以 18 个 ico 图标、3 个 cur 光标资源配合 1 个 rc 资源脚本构成界面素材6 个 h 头文件与 4 个 cpp 源文件承载棋盘绘制、走子判断与 AI 思考等核心代码另有 dsw、dsp 工程文件及 txt、htm 说明文档便于直接打开工程阅读与调试。资源围绕 GDI 绘图、鼠标事件响应、棋盘数据结构、合法走法判断、将军检测以及 Minimax 搜索等知识点展开代码结构清晰适合作为课程设计或自学练手项目。目前已有 123 人学习可帮助读者快速把握 Windows SDK 开发流程与象棋程序的基本实现思路。1. 从一份 Windows SDK 象棋源码说起它到底能跑出什么很多人第一次看到「windows sdk 象棋」这个组合会下意识以为是某个象棋引擎的算法仓库或者一个套壳的棋谱查看器。实际上它更接近一个用 Win32 API 从零搭起来的桌面象棋程序窗口、菜单、棋盘绘制、鼠标落子、走子合法性判断全部走原生 SDK 那套消息循环不依赖 MFC、Qt 或 Electron。这类资源的价值在于它把「Windows 桌面程序到底怎么活起来」这件事压缩进了一个你熟悉的场景里——下棋。如果你正在学 Win32 编程看纯文档很容易卡在WndProc和WM_PAINT上因为文档只告诉你消息长什么样不告诉你一个真实程序怎么组织这些消息。这份象棋源码正好补上这一环棋盘重绘、鼠标坐标换算成行列、选中棋子的高亮、走子后的状态刷新每一步都对应一个具体的消息分支。适合谁适合已经会一点 C/C、想搞明白 Windows 消息机制、又不满足于「点一下按钮弹个框」的开发者。下面我按实际拆包和复现的顺序把这份资源从编译到改参数讲透。2. 编译前先看懂结构Win32 消息循环与棋盘绘制怎么落地2.1 为什么这类项目坚持用原生 SDK 而不是框架先说选型理由。象棋程序的核心交互是「在固定网格上响应鼠标点击并重绘局部区域」这个需求用 Win32 原生 API 反而比框架更直接。框架会帮你封装窗口和控件但棋盘这种自绘区域框架的控件体系往往要绕一圈才能拿到HDC。原生 SDK 里WM_PAINT消息一到你直接BeginPaint拿到设备上下文想画线画线、想贴图贴图没有中间层。另一个原因是体积和依赖。原生 SDK 编译出来的 exe 通常几百 KB双击就跑不需要运行时。对于想研究「一个 Windows 程序最小能有多小」的人来说这是最干净的样本。常见做法是主窗口注册一个窗口类WndProc里用switch分发消息棋盘状态放在全局或窗口关联的结构体里。这套结构在象棋项目里体现得特别清楚因为棋盘状态每个格子上是什么棋子本身就是一份数据和界面是分离的。2.2 消息循环的最小骨架下面这段是这类项目里最常见的入口结构我按可编译的方式整理出来关键位置加了注释// WinMain注册窗口类、创建窗口、跑消息循环 int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR cmd, int show) { WNDCLASS wc {0}; wc.lpfnWndProc WndProc; // 消息处理函数核心都在这 wc.hInstance hInst; wc.hCursor LoadCursor(NULL, IDC_ARROW); wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); wc.lpszClassName LChessWnd; RegisterClass(wc); HWND hwnd CreateWindow(LChessWnd, L象棋, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 640, 700, NULL, NULL, hInst, NULL); ShowWindow(hwnd, show); MSG msg; while (GetMessage(msg, NULL, 0, 0)) { // 取消息 TranslateMessage(msg); // 翻译键盘消息 DispatchMessage(msg); // 派发给 WndProc } return msg.wParam; }逻辑说明RegisterClass把窗口类登记到系统CreateWindow按这个类创建实例GetMessage阻塞等待消息DispatchMessage把消息送回WndProc。参数上窗口宽高我按 8×8 棋盘加边距估的 640×700实际项目里棋盘是 9 列 10 行你要按格子尺寸反推。hbrBackground用系统色避免自己管理背景刷。2.3 棋盘绘制与坐标换算WM_PAINT是棋盘显示的关键。象棋棋盘是 9 列 10 行交叉点棋子落在交叉点上不是格子中心这一点和五子棋、围棋的落子逻辑不同换算时容易差半个格。case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hwnd, ps); int cell 60; // 每格像素 int ox 40, oy 40; // 棋盘左上角偏移 // 画 10 条横线、9 条竖线 for (int r 0; r 10; r) { MoveToEx(hdc, ox, oy r * cell, NULL); LineTo(hdc, ox 8 * cell, oy r * cell); } for (int c 0; c 9; c) { MoveToEx(hdc, ox c * cell, oy, NULL); LineTo(hdc, ox c * cell, oy 9 * cell); } // 棋子按 board[r][c] 绘制坐标 ox c*cell, oy r*cell EndPaint(hwnd, ps); break; }逻辑说明cell是格子边长ox/oy是棋盘在窗口里的起点。棋子坐标直接用ox c*cell因为交叉点就是线的交点。参数怎么改想让棋盘变大就调cell但窗口尺寸要同步放大否则右边和下边会被裁掉。失败时看什么如果线画出来是斜的或者错位多半是MoveToEx的起点和LineTo的终点用错了行列变量。2.4 鼠标落子从屏幕坐标到棋盘行列WM_LBUTTONDOWN里拿到的是客户区坐标要反算成行列还得判断点的是不是有效交叉点。case WM_LBUTTONDOWN: { int mx GET_X_LPARAM(lParam); int my GET_Y_LPARAM(lParam); int cell 60, ox 40, oy 40; int c (mx - ox cell / 2) / cell; // 四舍五入到最近交叉点 int r (my - oy cell / 2) / cell; if (c 0 c 9 r 0 r 10) { // 处理选中或走子更新 board[r][c] InvalidateRect(hwnd, NULL, FALSE); // 触发重绘 } break; }逻辑说明 cell/2是为了让点击落在交叉点附近也算命中不然用户必须精确点到线上。InvalidateRect标记区域无效系统随后发WM_PAINT重画。参数上cell必须和绘制时一致否则点击位置和显示位置对不上这是最常见的翻车点。GET_X_LPARAM来自windowsx.h别直接用LOWORD高坐标下会出错。3. 走子规则与状态管理把象棋逻辑从界面里剥出来3.1 棋盘数据结构怎么设计界面归界面规则归规则。这类项目里棋盘通常是一个二维数组每个元素存棋子类型和颜色。常见做法是用整数编码正数代表红方负数代表黑方0 代表空。// 棋子编码1帅/将 2仕/士 3相/象 4马 5车 6炮 7兵/卒 int board[10][9]; // board[r][c]r 是行c 是列 // 初始化黑方在上r0..4红方在下r5..9 void init_board() { memset(board, 0, sizeof(board)); board[0][0] -5; board[0][1] -4; board[0][2] -3; board[0][3] -2; board[0][4] -1; board[0][5] -2; board[0][6] -3; board[0][7] -4; board[0][8] -5; board[2][1] -6; board[2][7] -6; // 炮 for (int c 0; c 9; c 2) board[3][c] -7; // 卒 // 红方镜像符号取正 }逻辑说明用正负号区分阵营比单独存一个color字段省事判断时只看符号。参数上行列顺序要和绘制、点击换算保持一致有的项目用board[c][r]改的时候全局搜一遍不然会出现「棋子画在错误位置」的玄学问题。3.2 走子合法性判断的骨架规则判断是这份源码里最值得读的部分。以车为例走直线且路径无阻挡int is_valid_move(int r1, int c1, int r2, int c2) { int piece board[r1][c1]; if (piece 0) return 0; // 起点无子 if (board[r2][c2] * piece 0) return 0; // 目标是自己人 int type abs(piece); if (type 5) { // 车 if (r1 ! r2 c1 ! c2) return 0; // 必须直线 int dr (r2 r1) - (r2 r1); int dc (c2 c1) - (c2 c1); int r r1 dr, c c1 dc; while (r ! r2 || c ! c2) { // 路径中间不能有子 if (board[r][c] ! 0) return 0; r dr; c dc; } return 1; } // 马、炮、兵等分支类似炮要单独数中间隔子 return 0; }逻辑说明dr/dc用比较运算得到 -1/0/1避免写一堆 if。路径检查从起点下一步走到终点前一步中间有子就非法。炮的判断要数中间隔子数量恰好一个才能吃零个才能走。参数上abs(piece)拿到类型符号已经在上一步排除同阵营。失败时看什么如果马走日被判非法检查有没有处理「蹩马腿」如果炮吃子失败检查隔子计数是不是把起点或终点算进去了。3.3 选中高亮与回合切换交互上第一次点击选中第二次点击走子。状态用一个selected_r/selected_c记录-1表示没选中。static int sel_r -1, sel_c -1; static int red_turn 1; // 1 红方走0 黑方走 // 在 WM_LBUTTONDOWN 里 if (sel_r 0) { if (board[r][c] ! 0 ((board[r][c] 0) red_turn)) { sel_r r; sel_c c; // 选中己方棋子 } } else { if (is_valid_move(sel_r, sel_c, r, c)) { board[r][c] board[sel_r][sel_c]; // 移动 board[sel_r][sel_c] 0; red_turn !red_turn; // 换边 } sel_r sel_c -1; // 取消选中 } InvalidateRect(hwnd, NULL, FALSE);逻辑说明(board[r][c] 0) red_turn保证只能选自己回合的棋子。走子后清空起点、切换回合。参数上red_turn用 1/0 而不是枚举省事但可读性差正式项目建议用enum { RED, BLACK }。高亮绘制在WM_PAINT里根据sel_r/sel_c画个方框或圆环即可。4. 避坑与排查编译、坐标、规则里最容易翻车的地方4.1 现象编译报LNK2019找不到WinMain原因项目用了 Unicode 字符集但入口写的是WinMain链接器找的是wWinMain。或者反过来字符集设成多字节却写了wWinMain。 解决在项目属性里把字符集统一或者入口函数和字符集匹配。字符串常量加L...或TEXT(...)别混用char和wchar_t。4.2 现象点击位置和棋子显示对不上偏半个格原因绘制时棋子画在交叉点点击换算时用了格子中心或者cell/2的取整方向不一致。 解决绘制和点击用同一套ox/oy/cell换算公式统一成(mx - ox cell/2) / cell。改完在一张纸上标几个点验证别靠肉眼。4.3 现象走子后棋盘不刷新要拖动窗口才更新原因改了board但没调InvalidateRect或者WM_PAINT里没重画棋子。 解决任何改棋盘状态的地方后面都跟一句InvalidateRect(hwnd, NULL, FALSE)。第三个参数FALSE表示不擦背景减少闪烁如果残影严重再改TRUE。4.4 现象炮的走子判断时灵时不灵原因数中间隔子时把起点或终点算进去了或者没区分「走」和「吃」两种情况。 解决炮走子要求中间隔子为 0吃子要求恰好为 1。循环从起点下一步开始到终点前一步结束单独计数。4.5 现象窗口缩放后棋盘错位或棋子跑到窗口外原因cell和ox/oy是写死的窗口尺寸变了但绘制参数没跟着变。 解决在WM_SIZE里根据客户区宽高重算cell取宽高能容纳棋盘的最小值再居中算ox/oy。别在WM_PAINT里临时算容易和点击换算不一致。5. 进阶把规则判断抽成独立模块并加一个简单验证5.1 为什么要把规则从 WndProc 里搬出来写到后面你会发现WndProc越来越长规则判断、界面刷新、回合管理全挤在一起改一个规则要翻半天消息分支。我一般会把规则抽成一个独立文件只暴露is_valid_move和apply_move两个函数界面层只负责调用和重绘。这样你甚至可以在控制台里跑一套测试不用开窗口。// chess_logic.h int is_valid_move(int r1, int c1, int r2, int c2); void apply_move(int r1, int c1, int r2, int c2); int get_piece(int r, int c);逻辑说明头文件只声明实现放chess_logic.cboard用static藏在实现里外部通过get_piece读。参数上apply_move内部负责清空起点、写入终点调用方不用管细节。5.2 用控制台做规则回归验证抽出来之后写个main跑几个用例比在窗口里手点快得多#include assert.h #include chess_logic.h int main() { init_board(); // 车从 (9,0) 走到 (9,4)路径上 (9,1)(9,2)(9,3) 应为空 assert(is_valid_move(9, 0, 9, 4) 1); // 车斜走应非法 assert(is_valid_move(9, 0, 8, 1) 0); // 炮吃子中间恰好一个隔子才合法 // 按你的初始布局补用例 return 0; }逻辑说明assert在 Debug 下生效Release 下被裁掉适合做快速回归。参数上用例要覆盖直线、斜线、阻挡、吃子、同阵营这几种边界。失败时看什么断言触发会直接告诉你哪一行比在界面里猜快得多。5.3 一个具体技巧用WM_SETCURSOR做落子提示最后分享一个提升手感的小技巧。鼠标移到己方棋子上时把光标换成手型移到空位换回箭头用户不用猜哪里能点。case WM_SETCURSOR: { if (LOWORD(lParam) HTCLIENT) { POINT p; GetCursorPos(p); ScreenToClient(hwnd, p); int c (p.x - 40 30) / 60, r (p.y - 40 30) / 60; if (r 0 r 10 c 0 c 9 board[r][c] ! 0 ((board[r][c] 0) red_turn)) { SetCursor(LoadCursor(NULL, IDC_HAND)); return TRUE; } } break; // 交给默认处理 }逻辑说明WM_SETCURSOR在鼠标移动时触发HTCLIENT表示在客户区。换算行列后判断是不是己方棋子是就设手型并返回TRUE阻止默认光标。参数上40和60要和绘制参数一致建议抽成宏或全局变量别到处写魔法数字。从那以后我每次拆这类 Win32 项目都先把绘制参数和点击换算抽成同一组常量再写两个断言验证坐标反算不然总会在某个分辨率下翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表