首页/新闻资讯/正文详情

Win32象棋源码拆解:消息驱动、双缓冲与GDI绘图

发布时间:2026/11/5 22:11:22 来源:云帆数科 栏目:资讯中心
Win32象棋源码拆解:消息驱动、双缓冲与GDI绘图
简介这份压缩包围绕Windows SDK下的象棋程序开发面向具备C基础、希望理解Win32桌面应用与棋类人工智能实现的初中级开发者。内容涉及GDI/GDI绘制棋盘与棋子、窗口消息响应、棋盘状态与棋子数据结构、合法走子与将军判断以及Minimax配合Alpha-Beta剪枝、蒙特卡洛树搜索等AI思路并延伸到多线程交互、错误处理和打包发布可作为课程设计或小型棋类项目的参考。整包共38个文件、约24KB包含18个ico图标、6个头文件、4个cpp实现、3个cur光标资源以及dsw/dsp工程文件、rc资源脚本和说明文本分别对应界面素材、接口声明、核心逻辑与工程组织能帮助读者观察资源脚本与窗口过程的配合关系。已有126人学习下载适合按模块阅读代码、理解Win32窗口与资源组织方式并借鉴消息驱动和棋类搜索算法的落地写法目录中调试、发布与资源分区清晰便于按界面、逻辑和工程配置逐层查阅。1. windows sdk 象棋拆包一个纯 Win32 的棋盘程序到底能练什么windows sdk 象棋这套源码最容易被低估的地方是它把“消息驱动”这四个字落到了实处棋盘不是画在一张静态图上而是靠 WM_PAINT、WM_LBUTTONDOWN、WM_TIMER 三条线串起来的。打开工程你会发现没有 MFC、没有 Qt、没有第三方 UI 库窗口、画笔、字体、鼠标命中全走原生 API。这类资源适合两类人一类是刚写完 Hello World 窗口、想找个完整交互程序练手的另一类是要做课程设计或小工具需要一份能讲清楚“状态怎么流转、界面怎么刷新”的参考实现。下面按我实际拆代码的顺序走先把窗口壳立住再画棋盘棋子然后写规则引擎最后接搜索中间把翻车点单独拎一章讲透。2. 窗口壳先立住消息循环、双缓冲与像素坐标Win32 程序的骨架十几年没变过但象棋这种“整块客户区自绘 高频鼠标交互”的场景对骨架有几个额外要求背景不能由系统刷、绘制不能直接落在屏幕 DC 上、坐标换算必须只有一个入口。这三件事没做对后面画得再漂亮也会闪、会偏、会点不准。2.1 从 wWinMain 到 WndProc能跑起来的最小骨架先看入口。这段代码的作用是注册窗口类、创建窗口、进消息循环所有绘制和交互都从 WndProc 分发出去。// 最小窗口骨架资源释放略 int APIENTRY wWinMain(HINSTANCE hInst, HINSTANCE, LPWSTR, int nCmdShow) { WNDCLASSEXW wc { sizeof(wc) }; wc.style CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc WndProc; wc.hInstance hInst; wc.hCursor LoadCursor(nullptr, IDC_ARROW); wc.hbrBackground (HBRUSH)GetStockObject(NULL_BRUSH); // 背景交给自绘 wc.lpszClassName LXiangqiWnd; RegisterClassExW(wc); HWND hWnd CreateWindowExW(0, wc.lpszClassName, L象棋, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 760, 860, nullptr, nullptr, hInst, nullptr); ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); MSG msg; while (GetMessageW(msg, nullptr, 0, 0)) { TranslateMessage(msg); DispatchMessageW(msg); } return (int)msg.wParam; }hbrBackground设成NULL_BRUSH是关键一步。默认情况下系统会用背景刷先把客户区擦一遍然后 WM_PAINT 再把棋盘覆盖上去两次填充之间就是肉眼可见的白闪。窗口类里声明“背景我不需要系统管”配合后面的双缓冲闪屏问题一次性解决。CS_HREDRAW | CS_VREDRAW表示宽高变化时整窗重绘对这个项目是合适的如果你要做局部刷新优化可以去掉但拉伸窗口时容易留残影。CreateWindowExW里给的 760×860 只是初始尺寸真正可用的客户区要减掉标题栏和边框。很多新手的棋盘画出来总是偏下就是因为拿窗口矩形当客户区算了正确做法是在 WM_SIZE 或 WM_PAINT 里GetClientRect现取。2.2 双缓冲别在 WM_PAINT 里直接往屏幕 DC 上画棋子在拖动、选中高亮在切换的时候如果每一笔都直接写到屏幕 DC用户看到的是“画一半”的中间态。双缓冲的思路是先在内存位图上画完整帧再一次 BitBlt 贴到屏幕。case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); RECT rc; GetClientRect(hWnd, rc); HDC memDC CreateCompatibleDC(hdc); HBITMAP memBmp CreateCompatibleBitmap(hdc, rc.right, rc.bottom); HBITMAP oldBmp (HBITMAP)SelectObject(memDC, memBmp); FillRect(memDC, rc, (HBRUSH)GetStockObject(WHITE_BRUSH)); DrawBoard(memDC, rc); DrawPieces(memDC, rc); DrawSelection(memDC, rc); BitBlt(hdc, 0, 0, rc.right, rc.bottom, memDC, 0, 0, SRCCOPY); SelectObject(memDC, oldBmp); DeleteObject(memBmp); DeleteDC(memDC); EndPaint(hWnd, ps); return 0; } case WM_ERASEBKGND: return 1; // 告诉系统背景已处理省掉一次整窗填充CreateCompatibleBitmap的第一个参数必须是窗口 DChdc不能是刚创建出来的memDC。内存 DC 初始选中的是一张 1×1 的单色位图用它兼容出来的位图也是单色的画出来整个棋盘只有黑白两色线是黑的、底是白的棋子全变成黑块。这个坑几乎每个第一次写双缓冲的人都踩过而且报错信息为零只能靠肉眼发现。WM_ERASEBKGND直接返回 1 是配套动作。返回非零表示“背景我已经画了”系统就跳过填充。两个动作少一个闪屏都会回来。每次 WM_PAINT 都新建位图是有代价的窗口频繁重绘时 GDI 对象创建销毁的开销不小。常见做法是把 memDC 和 memBmp 做成全局或窗口属性在 WM_SIZE 里重建、在 WM_DESTROY 里释放。但要注意窗口尺寸变化时必须重新创建位图否则 BitBlt 贴出来的还是旧尺寸的画面右边和下边会出现黑边。2.3 棋盘坐标与像素坐标互转只留一个入口象棋棋盘是 9 条竖线、10 条横线共 90 个交叉点。所有绘制和点击判断都依赖“第几列第几行”和“屏幕像素”之间的换算。这段换算如果不收敛到一个函数里后面必然出现“画的位置和点的位置差半格”的问题。struct BoardGeom { int margin; // 外框到第一条线的距离 int cell; // 相邻两条线的间距 int left, top, width, height; }; // 以短边为准算几何保证棋盘不被拉变形 BoardGeom CalcGeom(const RECT rc) { BoardGeom g{}; int w rc.right - rc.left; int h rc.bottom - rc.top; int side min(w, h); g.margin side / 20; g.cell (side - 2 * g.margin) / 8; // 横向 8 格 g.width g.cell * 8 2 * g.margin; g.height g.cell * 9 2 * g.margin; // 纵向 9 格 g.left (w - g.width) / 2; g.top (h - g.height) / 2; return g; } POINT CellToPixel(const BoardGeom g, int col, int row) { return { g.left g.margin col * g.cell, g.top g.margin row * g.cell }; } bool PixelToCell(const BoardGeom g, int x, int y, int col, int row) { int cx x - g.left - g.margin; int cy y - g.top - g.margin; if (cx -g.cell / 3 || cy -g.cell / 3) return false; col (cx g.cell / 2) / g.cell; row (cy g.cell / 2) / g.cell; return col 0 col 9 row 0 row 10; }横向 8 格、纵向 9 格这是中国象棋和其他棋类在画法上最容易写错的常数。9 条竖线之间有 8 个间隔10 条横线之间有 9 个间隔写反了棋盘会被压扁或拉长而棋子位置看起来“差不多对”特别难排查。PixelToCell里加g.cell / 2是四舍五入到最近的交叉点。不加的话点在交叉点右下方 1 像素就会落到下一格玩起来像“点不中”。cell / 3的容差是给棋盘外沿留一点点击余量边线上的棋子不至于因为点偏两像素就选不中。参数怎么调margin用短边的 1/20棋盘大约占客户区 90%视觉上比较饱满想留白多改成 1/12。cell必须是整数否则每条线的累计误差会让最后一条线偏 1~2 像素。单看线不明显但棋子圆心和交叉点会越靠右越不重合强迫症看着很难受。高 DPI 是这一段最容易被忽略的延伸问题。不声明 DPI 感知的话系统会把整个窗口拉伸文字和圆形边缘都会糊。启动时调一次SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)并在WM_DPICHANGED里按新的 DPI 重新调用CalcGeom棋盘和字体才会始终清晰。3. GDI 画棋盘九宫、河界、兵炮位与棋子的字形居中窗口壳立住之后绘制层就是纯 GDI 的活了。这一层的难点不在 API而在几何细节哪些线要断开、九宫斜线连哪两个点、兵炮位的小标记画几笔、字怎么在圆里居中。每一个都是“画错一眼能看出来但不知道错在哪”的类型。3.1 棋盘几何九条竖线、十条横线、两处九宫外框用 Rectangle横线十条逐条画竖线九条要在河界处断开九宫的斜线只在上下两个 3×3 区域里出现。void DrawBoard(HDC hdc, const BoardGeom g) { HPEN pen CreatePen(PS_SOLID, 2, RGB(60, 40, 20)); HPEN oldPen (HPEN)SelectObject(hdc, pen); HBRUSH oldBr (HBRUSH)SelectObject(hdc, GetStockObject(NULL_BRUSH)); POINT p00 CellToPixel(g, 0, 0); POINT p89 CellToPixel(g, 8, 9); Rectangle(hdc, p00.x, p00.y, p89.x, p89.y); // 外框 for (int r 1; r 9; r) { // 内部 9 条横线 POINT a CellToPixel(g, 0, r); POINT b CellToPixel(g, 8, r); MoveToEx(hdc, a.x, a.y, nullptr); LineTo(hdc, b.x, b.y); } for (int c 0; c 9; c) { // 9 条竖线 POINT top CellToPixel(g, c, 0); POINT r4 CellToPixel(g, c, 4); POINT r5 CellToPixel(g, c, 5); POINT bot CellToPixel(g, c, 9); if (c 0 || c 8) { // 边线贯通 MoveToEx(hdc, top.x, top.y, nullptr); LineTo(hdc, bot.x, bot.y); } else { // 内线在河界断开 MoveToEx(hdc, top.x, top.y, nullptr); LineTo(hdc, r4.x, r4.y); MoveToEx(hdc, r5.x, r5.y, nullptr); LineTo(hdc, bot.x, bot.y); } } // 上下九宫的斜线 POINT t1 CellToPixel(g, 3, 0), t2 CellToPixel(g, 5, 2); POINT t3 CellToPixel(g, 5, 0), t4 CellToPixel(g, 3, 2); MoveToEx(hdc, t1.x, t1.y, nullptr); LineTo(hdc, t2.x, t2.y); MoveToEx(hdc, t3.x, t3.y, nullptr); LineTo(hdc, t4.x, t4.y); POINT b1 CellToPixel(g, 3, 7), b2 CellToPixel(g, 5, 9); POINT b3 CellToPixel(g, 5, 7), b4 CellToPixel(g, 3, 9); MoveToEx(hdc, b1.x, b1.y, nullptr); LineTo(hdc, b2.x, b2.y); MoveToEx(hdc, b3.x, b3.y, nullptr); LineTo(hdc, b4.x, b4.y); SelectObject(hdc, oldBr); SelectObject(hdc, oldPen); DeleteObject(pen); }Rectangle会同时用当前画刷填充内部如果画刷没切成NULL_BRUSH整个棋盘内部会被刷成实心色块线全被盖住。这个顺序问题是 GDI 绘制的通用陷阱任何“描边 填充”的图形都要先把填充画刷和描边画笔分别选好。兵位和炮位的小标记容易被省掉但它是棋盘的辨识度来源。兵位在 row 3 和 row 6 的偶数列炮位在 row 2 和 row 7 的第 1、7 列。每个位置画四段短线边线上的col 0 和 col 8只画朝向棋盘内侧的一半否则短线会画到棋盘外面去。河界两条边线之间通常还会写“楚河”“汉界”四个字。SetTextAlign(TA_CENTER)配合TextOutW在河界中间位置输出就行字号取cell * 0.8太大挤到边上太小看不见。3.2 棋子圆、字、颜色以及那个 0.20 的基线偏移棋子是圆底加一个字。圆用Ellipse字用TextOutW难点是让“車”“馬”这种笔画多的字在圆里视觉居中。void DrawPiece(HDC hdc, const BoardGeom g, int col, int row, int piece) { POINT c CellToPixel(g, col, row); int r (int)(g.cell * 0.45); HBRUSH face CreateSolidBrush(RGB(245, 222, 179)); HBRUSH oldBr (HBRUSH)SelectObject(hdc, face); HPEN edge CreatePen(PS_SOLID, 2, RGB(80, 50, 20)); HPEN oldPen (HPEN)SelectObject(hdc, edge); Ellipse(hdc, c.x - r, c.y - r, c.x r, c.y r); LOGFONTW lf {}; lf.lfHeight -(int)(g.cell * 0.55); // 负值表示字符高度 lf.lfWeight FW_BOLD; wcscpy_s(lf.lfFaceName, LKaiTi); HFONT font CreateFontIndirectW(lf); HFONT oldFont (HFONT)SelectObject(hdc, font); SetBkMode(hdc, TRANSPARENT); SetTextColor(hdc, piece 0 ? RGB(200, 0, 0) : RGB(0, 0, 0)); SetTextAlign(hdc, TA_CENTER | TA_BASELINE); const wchar_t* name PieceName(piece); TextOutW(hdc, c.x, c.y (int)(g.cell * 0.20), name, (int)wcslen(name)); SelectObject(hdc, oldFont); DeleteObject(font); SelectObject(hdc, oldBr); DeleteObject(face); SelectObject(hdc, oldPen); DeleteObject(edge); }lfHeight取负值是“字符高度”取正值是“单元格高度”。用正值的话字体自带的上下留白会让字整体偏高、顶到圆的上沿看起来像没居中。负值配合TA_BASELINE再用cell * 0.20下移基线才能让字形在圆里视觉平衡。这个 0.20 不是通用常数。换本文还有配套的精品资源点击获取

相关推荐

蓝桥杯备赛进阶:数据结构与算法精要
蓝桥杯备赛进阶:数据结构与算法精要

1. 蓝桥杯备赛策略概述蓝桥杯作为国内最具影响力的IT类赛事之一,每年吸引数万名计算机相关专业学生参与。备赛过程需要系统性的知识梳理和针对性的技能训练,特别是对于"蓝桥杯备赛2-2"这样的进阶阶段,意味着参赛者已经具备基础编程… · 2026/11/4 12:21:49

SQL Server学习笔记:三层知识地图、核心语法与易错点自测
SQL Server学习笔记:三层知识地图、核心语法与易错点自测

简介:《SQL Server学习笔记》是一份面向数据库初学者与日常运维人员的入门型整理资料,围绕SQL Server的核心概念、对象管理与基础操作展开,适合课堂复习、岗前自学及知识查漏。文档系统梳理了Create、Drop、Alter等库表操作,数据库… · 2026/11/3 9:58:31

IBM-PC汇编语言程序设计答案:数制转换、补码与寻址方式详解
IBM-PC汇编语言程序设计答案:数制转换、补码与寻址方式详解

简介:《IBM-PC汇编语言程序设计答案》面向学习80x86汇编语言、需要核对课后习题与巩固基础概念的读者,适合配合沈美明、温冬婵教材使用。内容覆盖十进制、二进制与十六进制互转,8位补码加减及CF、OF标志判断,位运算和ASCII字符编码… · 2026/11/3 21:02:20

EasyU优启通PE维护盘实战指南:制作、启动与核心操作全解析
EasyU优启通PE维护盘实战指南:制作、启动与核心操作全解析

1. 为什么一个PE维护盘值得单独拿出来聊装系统、修引导、救数据、清病毒,这些活儿但凡干过几次的人都知道,真正决定效率的往往不是你手速多快,而是手边那套PE环境靠不靠谱。EasyU(圈内习惯叫优启通)就是在这个场景下被… · 2026/11/5 16:49:35

Rust原来很简单二:所有权系统
Rust原来很简单二:所有权系统

Part 2 核心篇 —— 所有权系统 本篇覆盖 第 6 ~ 10 章:所有权、借用与引用、生命周期、字符串与切片、内存模型。 ⚠️ 这是全书最重要的一篇。 把它吃透,Rust 从此“很简单”;跳过它,你会处处碰壁。请慢下来,动手敲每一段代码。 第 6 章 所有权:Rust 的第一性原理 6… · 2026/11/5 16:26:08

基于Hadoop+Spark的民宿智能推荐系统设计与实现
基于Hadoop+Spark的民宿智能推荐系统设计与实现

1. 项目概述:大数据时代的民宿智能推荐这个毕业设计项目融合了当前大数据领域最主流的技术栈,构建了一个完整的民宿推荐系统解决方案。作为一名长期从事大数据系统开发的工程师,我认为这个选题非常贴合实际应用场景——随着旅游业的复苏和共享… · 2026/11/5 14:59:25

ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计
ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41

UART接口24位ADC搭配RS485,解决远距离温度监控难题
UART接口24位ADC搭配RS485,解决远距离温度监控难题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41

UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践
UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07

多智能体协作系统架构设计与实践优化
多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39

大模型系统性入门实战指南:从原理到部署微调的全链路路线
大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39

鸿蒙文件系统沙箱机制解析与开发实践
鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码