
简介这份资源面向C语言初学者与课程设计需求者提供利用EasyX图形库实现五子棋程序的完整工程。EasyX基于Windows API简化了窗口创建、图形绘制与鼠标事件处理适合用来练习变量、控制结构、函数、二维数组等C语言核心知识。压缩包共50个文件约8.81MB包含cpp源码、vcxproj与sln工程文件、exe可执行程序、jpg背景图以及pdb、ilk、tlog等调试与编译中间文件可直接在Visual Studio中打开运行。项目围绕棋盘绘制、落子判断、五子连线胜负检测、玩家切换与重置等逻辑展开并涉及SetColor、TextOut等界面美化函数。已有3191人学习下载适合希望从语法练习过渡到图形界面与游戏逻辑设计的入门者参考也可作为课程设计提交的完整范例。1. 用 EasyX 把五子棋跑起来为什么它是 C 语言课程设计里最稳的那条路很多人做 C 语言课程设计卡在同一个地方语法学完了指针、结构体、数组都能看懂但一到「写个能看的东西」就无从下手。控制台里打印一堆字符老师看一眼就皱眉。这时候 EasyX 图形库就成了性价比最高的选择——它把 Windows 图形接口封装成几十个简单函数initgraph开窗口、fillcircle画棋子、getmessage收鼠标C 语言基础够用就能上手。五子棋这个题目又刚好卡在难度甜区规则人人会逻辑不复杂但涉及二维数组、状态判断、鼠标交互、界面刷新足够撑起一份像样的课程设计。这篇笔记就按「能复现」的标准把从环境配置到胜负判定的完整路径讲清楚新手照着敲能跑熟手能看到参数边界和踩坑点。2. 环境配置与 EasyX 最小可运行窗口先把黑匣子打开2.1 为什么选 EasyX 而不是控制台或别的图形库课程设计的第一道坎不是写代码是选技术栈。控制台方案纯printf画棋盘优点是零依赖缺点是交互体验差鼠标点不了只能靠输入坐标答辩时演示效果很弱。SDL、Qt 这类库功能强但配置复杂新手光链接库就能耗掉两天而且和「C 语言课程设计」的定位有点脱节——老师想看的是你对 C 语言本身的掌握不是你会不会用框架。EasyX 的定位刚好它是专门给 C/C 初学者做的图形库头文件就一个graphics.h函数命名直白line、circle、outtextxy底层是 Windows GDI不需要额外运行库。安装方式也简单下载后把include和lib目录里的文件拷到你的编译器对应目录即可。常见做法是配合 Visual Studio 使用因为 EasyX 官方对 VS 各版本都有对应的库文件社区里 VS2019、VS2022 的配置教程最多遇到问题好搜。需要提醒的是EasyX 是 Windows 平台专属如果你用的是 VS Code MinGW 或者 Linux 环境配置会麻烦很多甚至跑不起来。课程设计一般都在 Windows 上做直接用 VS 最省事。热词里提到的「vscode 配置 c 语言环境」在纯语法练习时很好用但做 EasyX 项目我一般还是建议切回 VS少折腾。2.2 装好 EasyX 并跑通第一个窗口配置步骤按顺序来每一步都要确认到位不然报错信息会很迷惑。第一步确认编译器版本。打开 VS菜单栏「帮助」→「关于 Microsoft Visual Studio」记下版本号比如 2022。EasyX 官网下载页会按 VS 版本分文件夹下错了会链接失败。第二步安装。下载解压后找到对应你 VS 版本的目录里面通常有include和lib两个文件夹。把include里的graphics.h、easyx.h等头文件拷到 VS 安装目录下的VC\Tools\MSVC\版本号\include\里把lib里对应架构x86 或 x64的.lib文件拷到VC\Tools\MSVC\版本号\lib\x86\或x64\下。具体路径各版本略有差异找不到就用文件管理器搜graphics.h看现有头文件放哪。第三步新建空项目添加一个.cpp文件注意EasyX 在 C 下兼容性最好虽然它支持 C但用.c文件时某些函数会有警告建议直接用.cpp代码里照样写 C 风格。第四步写最小测试代码#include graphics.h // EasyX 核心头文件 #include conio.h // 用于 _getch() 暂停 int main() { // 创建 640x480 的窗口 initgraph(640, 480); // 画一个圆圆心 (320,240)半径 100 circle(320, 240, 100); // 在指定位置输出文字 outtextxy(200, 50, _T(EasyX OK)); // 按任意键继续防止窗口一闪而过 _getch(); // 关闭图形窗口 closegraph(); return 0; }这段代码的逻辑很直白initgraph负责创建绘图窗口后面所有绘图函数都画在这个窗口上circle用圆心坐标和半径画圆outtextxy在指定像素位置输出字符串注意字符串要用_T()包裹这是为了兼容 Unicode 字符集不加在某些项目配置下会报「无法从 const char 转换」的错误_getch()是暂停不加的话程序画完立刻退出你什么都看不到。参数说明initgraph(width, height)的宽高是像素值五子棋棋盘建议至少 600×600因为要留出边距和状态栏。circle(x, y, r)里 x、y 是屏幕坐标原点在左上角x 向右增大y 向下增大这点和数学坐标系相反画棋盘时容易搞混后面会专门讲。如果这一步报「无法打开 graphics.h」说明头文件没拷对位置如果报「无法解析的外部符号 _initgraph」说明 lib 没链接上检查 lib 目录和项目架构是否匹配x64 项目要放 x64 的 lib。这两个错误占了新手配置问题的九成。2.3 坐标系统与棋盘尺寸的换算关系EasyX 的坐标原点是窗口左上角这和五子棋棋盘的直觉不一样。棋盘是 15×15 的交叉点棋子下在交叉点上不是格子里。所以你需要先定几个常量棋盘边距MARGIN、格子间距GRID_SIZE、棋盘线数BOARD_SIZE。假设窗口 640×640边距 40那么可用绘图区域是 560 像素15 条线有 14 个间隔GRID_SIZE 560 / 14 40。这样每个交叉点的坐标就是MARGIN col * GRID_SIZE和MARGIN row * GRID_SIZErow 和 col 从 0 到 14。这个换算关系必须一开始就定死写成常量或宏后面画棋盘、画棋子、鼠标点击反算行列全都依赖它。我见过有人画棋盘用一套坐标判断鼠标点击用另一套结果棋子总是偏半格调半天。血泪经验把坐标换算封装成两个函数gridToPixel和pixelToGrid所有地方都调这两个函数不要到处手算。3. 棋盘绘制与鼠标落子把交互闭环打通3.1 用双重循环画 15 路棋盘和星位棋盘绘制本身不难就是画横线和竖线但有几个细节决定成品好不好看。#include graphics.h const int BOARD_SIZE 15; // 15 路棋盘 const int MARGIN 40; // 棋盘边距 const int GRID_SIZE 40; // 格子间距 const int WIN_SIZE MARGIN * 2 GRID_SIZE * (BOARD_SIZE - 1); // 窗口边长 // 把行列坐标转成屏幕像素坐标 int gridToPixel(int index) { return MARGIN index * GRID_SIZE; } void drawBoard() { // 设置线条颜色为黑色 setlinecolor(BLACK); // 画 15 条横线和 15 条竖线 for (int i 0; i BOARD_SIZE; i) { int pos gridToPixel(i); // 横线从左边距到右边距 line(MARGIN, pos, WIN_SIZE - MARGIN, pos); // 竖线从上边距到下边距 line(pos, MARGIN, pos, WIN_SIZE - MARGIN); } // 画五个星位天元加四个角星用实心小圆点 setfillcolor(BLACK); int stars[5][2] {{3,3},{3,11},{11,3},{11,11},{7,7}}; for (int i 0; i 5; i) { int x gridToPixel(stars[i][0]); int y gridToPixel(stars[i][1]); solidcircle(x, y, 4); // 半径 4 的小圆点 } }逻辑说明gridToPixel把 0~14 的行列索引映射到像素坐标横线和竖线都用它算位置保证交叉点对齐。星位是五子棋棋盘的传统标记天元在正中间 (7,7)四个角星在 (3,3)、(3,11)、(11,3)、(11,11)画上之后棋盘立刻专业不少。参数说明setlinecolor设置后续画线的颜色setfillcolor设置填充色这两个是「状态设置」函数设置一次后对后面所有绘图生效直到再次修改。solidcircle画实心圆和circle空心区分开。星位半径 4 像素在 40 像素格距下视觉刚好太大显得笨重太小看不见。注意WIN_SIZE用MARGIN * 2 GRID_SIZE * (BOARD_SIZE - 1)算出来是 40×2 40×14 640正好是窗口边长。这样棋盘居中四周留白均匀。如果你改了GRID_SIZE窗口尺寸要跟着改否则棋盘会偏或者被裁掉。3.2 鼠标点击落子与坐标反算棋盘画好后下一步是让鼠标点击能落子。EasyX 用getmessage或peekmessage获取鼠标事件前者阻塞等待后者非阻塞。五子棋这种回合制游戏用阻塞式的getmessage就够逻辑简单。#include graphics.h // 假设棋盘相关常量已定义board 数组记录落子情况 // 0 表示空1 表示黑子2 表示白子 int board[BOARD_SIZE][BOARD_SIZE] {0}; // 把屏幕像素坐标反算成最近的行列索引 // 返回 -1 表示点击位置不在棋盘有效范围内 int pixelToGrid(int pixel) { int index (pixel - MARGIN GRID_SIZE / 2) / GRID_SIZE; if (index 0 || index BOARD_SIZE) return -1; return index; } // 在指定交叉点画一颗棋子 void drawPiece(int row, int col, int player) { int x gridToPixel(col); int y gridToPixel(row); if (player 1) { setfillcolor(BLACK); } else { setfillcolor(WHITE); } // 棋子半径略小于半个格距留出间隙 solidcircle(x, y, GRID_SIZE / 2 - 3); // 白子加个黑边否则在浅色背景上看不清 if (player 2) { setlinecolor(BLACK); circle(x, y, GRID_SIZE / 2 - 3); } } int main() { initgraph(WIN_SIZE, WIN_SIZE); drawBoard(); int currentPlayer 1; // 黑先 while (true) { // 阻塞等待一条消息 ExMessage msg getmessage(); if (msg.message WM_LBUTTONDOWN) { int col pixelToGrid(msg.x); int row pixelToGrid(msg.y); // 点击无效或该位置已有棋子跳过 if (col -1 || row -1) continue; if (board[row][col] ! 0) continue; board[row][col] currentPlayer; drawPiece(row, col, currentPlayer); // 切换玩家 currentPlayer (currentPlayer 1) ? 2 : 1; } } closegraph(); return 0; }逻辑说明pixelToGrid是gridToPixel的逆运算关键在 GRID_SIZE / 2这个偏移——鼠标点不一定精确落在交叉点上加上半个格距再整除相当于「就近取整」点到交叉点附近 20 像素内都算命中。getmessage返回一个ExMessage结构体msg.message是消息类型WM_LBUTTONDOWN表示左键按下msg.x、msg.y是鼠标位置。落子前要检查board[row][col]是否为空防止重复下子。参数说明棋子半径用GRID_SIZE / 2 - 3减 3 是为了让棋子之间留一点缝视觉上更清晰全填满会连成一片。白子额外画一圈黑边因为纯白棋子在白色背景上几乎看不见这是新手最容易忽略的显示问题。注意ExMessage是 EasyX 较新版本的结构体老版本用MOUSEMSG。如果你编译报「ExMessage 未定义」说明 EasyX 版本较旧换成MOUSEMSG msg GetMouseMsg();然后判断msg.uMsg WM_LBUTTONDOWN。两种写法功能一样看你装的版本。4. 胜负判定与状态管理五子棋的核心逻辑4.1 四方向扫描判断五连落子之后要立刻判断这步棋有没有形成五连。最直接的做法是以刚落子的点为中心向四个方向横、竖、两条斜线扫描每个方向往两边数同色棋子加起来够 5 就赢。// 判断在 (row, col) 落子后player 是否获胜 bool checkWin(int row, int col, int player) { // 四个方向横、竖、主对角线、副对角线 int dirs[4][2] {{0,1}, {1,0}, {1,1}, {1,-1}}; for (int d 0; d 4; d) { int count 1; // 包含当前这颗棋子 int dr dirs[d][0]; int dc dirs[d][1]; // 向正方向数 for (int step 1; step 5; step) { int r row dr * step; int c col dc * step; if (r 0 || r BOARD_SIZE || c 0 || c BOARD_SIZE) break; if (board[r][c] ! player) break; count; } // 向反方向数 for (int step 1; step 5; step) { int r row - dr * step; int c col - dc * step; if (r 0 || r BOARD_SIZE || c 0 || c BOARD_SIZE) break; if (board[r][c] ! player) break; count; } if (count 5) return true; } return false; }逻辑说明dirs数组存了四个方向的增量{0,1}是横向列变{1,0}是纵向行变{1,1}是主对角线行列同增{1,-1}是副对角线行增列减。对每个方向从落子点出发正反两个方向各数最多 4 步遇到边界或异色棋子就停。count初始为 1 代表当前棋子本身两边累加后如果达到 5 就获胜。参数说明内层循环step 5是因为五连最多只需要看前后各 4 颗数多了没意义。边界检查r 0 || r BOARD_SIZE必须放在取board[r][c]之前否则数组越界程序可能崩溃或者读到垃圾值导致误判——这是 C 语言里最经典的坑数组越界不报错但行为诡异。注意这个判定只检查「当前这一步是否形成五连」不需要全盘扫描效率高。有人图省事写全盘双重循环找五连代码更长还容易漏判斜线不推荐。4.2 用状态机管理游戏流程一个能用的五子棋不能只有落子和判定还要处理「谁在下」「下完了怎么办」「能不能悔棋」「要不要重开」。这些用状态变量管理最清晰。// 游戏状态 enum GameState { PLAYING, // 对局进行中 BLACK_WIN, // 黑方获胜 WHITE_WIN, // 白方获胜 DRAW // 平局棋盘下满 }; GameState state PLAYING; int currentPlayer 1; int moveCount 0; // 已落子数用于判断平局 // 在落子逻辑里加入状态判断 void handleClick(int x, int y) { if (state ! PLAYING) return; // 已结束不再响应落子 int col pixelToGrid(x); int row pixelToGrid(y); if (col -1 || row -1) return; if (board[row][col] ! 0) return; board[row][col] currentPlayer; drawPiece(row, col, currentPlayer); moveCount; if (checkWin(row, col, currentPlayer)) { state (currentPlayer 1) ? BLACK_WIN : WHITE_WIN; // 在状态栏显示获胜信息 settextcolor(RED); outtextxy(MARGIN, WIN_SIZE - 30, (currentPlayer 1) ? _T(黑方获胜) : _T(白方获胜)); return; } if (moveCount BOARD_SIZE * BOARD_SIZE) { state DRAW; outtextxy(MARGIN, WIN_SIZE - 30, _T(平局)); return; } currentPlayer (currentPlayer 1) ? 2 : 1; }逻辑说明state是全局状态落子函数开头先判断state ! PLAYING就直接返回这样游戏结束后再点棋盘不会继续下子。获胜后设置对应状态并在底部输出提示文字。moveCount累加等于 22515×15时判平局。参数说明enum枚举让状态可读性更好比用 0、1、2 这种魔法数字强。outtextxy的 y 坐标用WIN_SIZE - 30把提示放在窗口底部不遮挡棋盘。settextcolor设置文字颜色获胜提示用红色更醒目。注意状态栏文字会覆盖之前的内容如果反复重开需要先清空那块区域用setfillcolorsolidrectangle填充背景色再输出。这个细节不做的话上一局的「黑方获胜」会和新一局的提示叠在一起看起来像乱码。4.3 悔棋与重开用栈保存落子历史课程设计里加上悔棋功能答辩时是个加分项。实现思路是用一个栈数组模拟即可记录每一步的落子信息悔棋时弹出最后一步把对应位置清空并重绘。// 落子历史记录 struct Move { int row; int col; int player; }; Move history[BOARD_SIZE * BOARD_SIZE]; int historyTop 0; // 栈顶指针也等于已落子数 // 落子时压栈 void pushMove(int row, int col, int player) { history[historyTop].row row; history[historyTop].col col; history[historyTop].player player; historyTop; } // 悔棋撤销最后一步 void undo() { if (historyTop 0) return; // 没有可悔的棋 if (state ! PLAYING) { // 如果已经结束悔棋后恢复对局状态 state PLAYING; } historyTop--; Move last history[historyTop]; board[last.row][last.col] 0; // 清空棋盘数据 // 重绘该位置先用背景色覆盖再补画棋盘线 int x gridToPixel(last.col); int y gridToPixel(last.row); setfillcolor(WHITE); solidrectangle(x - GRID_SIZE/2, y - GRID_SIZE/2, x GRID_SIZE/2, y GRID_SIZE/2); // 补画被覆盖的棋盘线简化处理重画整个棋盘 drawBoard(); // 重画所有剩余棋子 for (int i 0; i historyTop; i) { drawPiece(history[i].row, history[i].col, history[i].player); } // 切换回被撤销的那一方 currentPlayer last.player; moveCount historyTop; }逻辑说明history数组模拟栈historyTop指向栈顶下一个空位。落子时压栈悔棋时先减historyTop再取最后一步。清空棋盘数据后最省事的重绘方式是「重画整个棋盘 重画所有剩余棋子」虽然效率不高但 15×15 的规模完全无感代码简单不易错。参数说明solidrectangle画填充矩形四个参数是左上角和右下角坐标用来覆盖旧棋子。这里其实被后面的drawBoard覆盖了但保留这步是为了逻辑完整如果你改成局部重绘就需要它。注意悔棋后currentPlayer要设回被撤销的那一方否则会出现「黑棋悔棋后还是白棋下」的错乱。moveCount也要同步更新不然平局判断会出错。这些状态变量之间的同步是 bug 高发区建议每改一个都问自己「还有哪些变量依赖它」。5. 避坑与排查那些让课程设计翻车的细节5.1 现象窗口一闪而过什么都看不到原因程序画完就退出没有暂停。EasyX 的图形窗口在main返回后自动关闭。解决在closegraph()前加_getch()或system(pause)。如果是消息循环结构确保循环不会意外退出。我一般用_getch()因为它不依赖系统命令跨环境更稳。5.2 现象鼠标点击位置和棋子落点差半格甚至一格原因坐标反算时没有做「就近取整」或者MARGIN、GRID_SIZE在画棋盘和反算时用了不同的值。解决统一用gridToPixel和pixelToGrid两个函数所有坐标换算都走它们。pixelToGrid里的 GRID_SIZE / 2不能省这是就近取整的关键。另外检查MARGIN是不是常量有没有在某个地方被意外修改。5.3 现象白棋看不见或者棋子边缘有锯齿原因白棋画在白色背景上没有对比solidcircle默认无边框边缘是硬像素。解决白棋额外用circle画一圈黑边。锯齿问题 EasyX 本身不提供抗锯齿但可以通过把棋子半径设小一点、留出间隙来缓解视觉上的粗糙感。如果实在在意可以改用fillcircle配合setlinestyle调整但课程设计级别不必过度追求。5.4 现象胜负判定偶尔误判明明没五连却判赢原因数组越界读到脏数据或者方向增量写错比如副对角线写成{1,1}。解决检查checkWin里的边界判断是否在访问数组前执行。四个方向的增量再核对一遍横{0,1}、竖{1,0}、主对角{1,1}、副对角{1,-1}。副对角线是行增列减容易和主对角线搞混。建议写个测试用例手动摆一个五连验证。5.5 现象悔棋后棋盘出现残影或棋子错位原因只清了棋盘数据没重绘或者重绘时坐标算错。解决悔棋后老老实实「重画棋盘 重画所有剩余棋子」不要试图只擦一个点。残影的本质是旧像素没被覆盖局部擦除很容易漏掉棋盘线。全量重绘在 15×15 规模下开销可以忽略代码还简单。6. 进阶技巧让课程设计从「能跑」到「拿高分」到这一步一个完整的双人对战五子棋已经能跑了。但如果想让课程设计在答辩时更出彩有几个方向可以加成本不高但效果明显。第一个是落子提示音。EasyX 本身不处理音频但可以用 Windows API 的PlaySound或Beep函数。Beep(800, 100)一行代码就能在落子时响一声参数是频率和时长。注意Beep在某些系统上可能被禁用PlaySound需要链接winmm.lib用之前先测试。第二个是最后落子标记。在最新一颗棋子上画一个小红点或方框让对局双方一眼看到上一步下在哪。实现很简单落子后记录坐标重绘时在对应位置加个标记。这个细节对体验提升很大答辩老师通常会注意到。第三个是简单 AI 对手。如果时间充裕可以加一个「人机对战」模式。最基础的 AI 不需要机器学习用评分表就行对每个空位分别计算如果黑棋下这里、白棋下这里能形成多少连子给不同连子数打分活四最高、冲四次之、活三再次选分数最高的位置。这个逻辑大概一百行代码但能让你的课程设计从「双人对战」升级成「带 AI 的五子棋」档次立刻不一样。热词里「五子棋网页版人机」说明大家对 AI 对战有需求C 语言版本实现一个简化版完全可行。第四个是界面美化。给棋盘加个木色背景setbkcolor配合cleardevice棋子用渐变色EasyX 不直接支持渐变但可以画多个同心圆模拟状态栏显示当前轮到谁、已下多少手。这些不涉及复杂逻辑纯粹是体力活但视觉效果提升明显。我自己的习惯是先把核心功能跑通确保落子、判定、悔棋都没问题再花时间做这些锦上添花的东西。顺序反了容易在美化上耗太久结果核心逻辑还有 bug得不偿失。另外每加一个功能就完整测一遍别攒一堆改动再一起调出问题时定位成本会翻倍。最后说一个验证方法写完之后自己跟自己下几盘故意制造横五连、竖五连、两种斜五连、边界五连比如贴着棋盘边缘的五连看判定是否都正确。边界情况是最容易翻车的地方尤其是斜线在角落的处理。我当初就是漏了副对角线在边界的判断测试时才发现希望你别踩这个坑。希望帮到你。本文还有配套的精品资源点击获取