尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C# WinForm五子棋课程设计:GDI+绘图与数据库实战解析

C# WinForm五子棋课程设计:GDI+绘图与数据库实战解析 简介C# 五子棋大学项目源码数据库是一套面向高校计算机专业学生的完整课程设计资源以五子棋为载体覆盖面向对象设计、TCP/IP局域网通信、ADO.NET数据库操作及简单AI算法适合用来完成课程作业或作为毕业设计参考。压缩包共200个文件主要包含53个C#源码文件、界面图片与资源文件、背景音乐MP3、运行所需的DLL以及SQL Server数据库文件mdf/ldf整体约53MB可直接打开工程查看代码结构。目前已有614人学习/下载属于较受欢迎的五子棋实例。代码中实现了人机对战和人人对战两种模式人机部分引入博弈树搜索如Minimax/Alpha-Beta剪枝支持局域网联机、悔棋和背景音乐并配有数据库记录玩家信息与对局历史。透过工程内各功能模块的清晰划分可以理解游戏逻辑与界面交互的拆分方式方便二次开发与学习。1. 项目概览与设计思路拆解1.1 为什么选C# WinForm 数据库的组合C#五子棋这类大学项目我在带课设和帮学弟学妹改代码时见过很多版本但真正把源码数据库都交付完整的其实不多。这个项目的核心价值不在于五子棋本身——五子棋的算法在网上遍地都是而在于你用数据库把游戏过程、玩家信息、历史战绩都管理了起来。对课程设计来说这恰好踩中了C#桌面应用开发 数据库设计与应用两个关键评分点。WinForm做五子棋界面技术上是最稳妥的。控件拖拽就能搭出主窗口GDI绘图能搞定棋盘和棋子渲染事件驱动模型天然适合处理鼠标落子这类交互。比起WPF那套绑定和样式系统WinForm的学习曲线平缓得多对还在上课阶段的学生来说踩坑少出活快。数据库这边项目用的是SQL Server这也是高校机房最常见的环境和Visual Studio同属微软生态连接配置简单课程设计答辩时演示也顺畅。很多人觉得五子棋游戏加数据库有点强行凑功能实际并不是。一个完整的对局系统天然就有玩家管理、对局记录、胜率统计这些需求。不联网的单机五子棋靠数据库记录本地玩家的对战档案、保存残局复盘是完全说得通的场景。把这个关系理清课程设计的需求分析和可行性分析两章就有东西写了。1.2 整体架构与模块划分项目整体是典型的三层结构虽然WinForm项目不会像Web项目那样把分层做得那么明显但逻辑边界还是清楚的表现层主窗体MainForm、登录/注册窗体、排行榜窗体。负责棋盘绘制、鼠标事件响应、数据展示。业务层对局管理类GameManager负责落子逻辑、胜负判定、悔棋处理、回合切换。数据层数据库访问类DBHelper封装SqlConnection、SqlCommand等操作向上层提供玩家增删改查、对局记录写入、排行榜查询等方法。这个划分在项目报告里很好写。每个窗体只干自己的事数据库操作全部走DBHelperGameManager不直接碰SQL语句。答辩时老师问到解耦和可维护性照这个结构讲就立得住。游戏部分包含几个核心模块模块职责棋盘绘制用GDI画15x15网格线绘制黑白棋子落子控制鼠标点击坐标换算、落子合法性检查、回合切换胜负判定每次落子后检测四个方向横、竖、两条对角线是否形成五连悔棋功能栈结构记录落子历史支持多步悔棋数据持久化对局结束自动写入数据库玩家信息管理整套代码量不大核心逻辑加起来几百行就能跑通。但要把每个模块做得完善、注释清楚、边界情况不闪退还是需要下一番功夫的。2. 核心功能与关键代码实现2.1 棋盘绘制与落子坐标换算棋盘是五子棋的门面。15x15的棋盘棋子直径通常在30像素左右格子间距取40像素比较合适。先定义常量避免魔法数字散落各处private const int BOARD_SIZE 15; // 棋盘格数 private const int CELL_SIZE 40; // 每格像素 private const int MARGIN 30; // 棋盘边距 private const int PIECE_RADIUS 14; // 棋子半径 private readonly Point[,] boardPositions new Point[BOARD_SIZE, BOARD_SIZE];窗体加载时先把每个交叉点的坐标算好private void InitializeBoardPositions() { for (int row 0; row BOARD_SIZE; row) { for (int col 0; col BOARD_SIZE; col) { boardPositions[row, col] new Point( MARGIN col * CELL_SIZE, MARGIN row * CELL_SIZE); } } }绘制棋盘的代码放在OnPaint事件里用双重循环画横线和竖线再单独画四个角的星位标记。这里的第一个坑就来了WinForm的OnPaint刷新机制。如果在鼠标点击事件里直接调用画棋子窗口一最小化再恢复棋子就全没了。正确的做法是维护一个二维数组存储落子状态在OnPaint里根据数组内容把棋盘和棋子全部重新画一遍。protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); DrawBoard(e.Graphics); DrawPieces(e.Graphics); } private void DrawPieces(Graphics g) { for (int row 0; row BOARD_SIZE; row) { for (int col 0; col BOARD_SIZE; col) { if (boardState[row, col] ! 0) { Brush brush boardState[row, col] 1 ? Brushes.Black : Brushes.White; g.FillEllipse(brush, boardPositions[row, col].X - PIECE_RADIUS, boardPositions[row, col].Y - PIECE_RADIUS, PIECE_RADIUS * 2, PIECE_RADIUS * 2); } } } }鼠标点击时的坐标换算也要细心。把鼠标坐标减去边距再除以格子间距四舍五入到最近的交叉点最后判断该点是否已有棋子private void MainForm_MouseClick(object sender, MouseEventArgs e) { int col (int)Math.Round((e.X - MARGIN) / (double)CELL_SIZE); int row (int)Math.Round((e.Y - MARGIN) / (double)CELL_SIZE); if (row 0 || row BOARD_SIZE || col 0 || col BOARD_SIZE) return; if (boardState[row, col] ! 0) return; PlacePiece(row, col); }鼠标点到格子缝隙时四舍五入会自动吸附到最近的交叉点不需要额外处理。只有在棋盘边缘区域要加越界判断不然后面数组下标直接溢出。2.2 胜负判定一次落子四条线胜负判定是五子棋代码的核心也是很多人的翻车现场。最容易犯的错误是每次落子后遍历整个棋盘去扫描时间复杂度高不说代码还绕。更合理的做法是只围绕当前落子点向四个方向扩展检查因为五子连珠必然包含刚下的这枚棋子。四个方向的定义水平方向(0, -1) 和 (0, 1)垂直方向(-1, 0) 和 (1, 0)主对角线(-1, -1) 和 (1, 1)副对角线(-1, 1) 和 (1, -1)每个方向从当前点出发向两端分别数连续同色棋子的数量加起来超过5就赢了private bool CheckWin(int row, int col, int player) { int[,] directions new int[,] { { 0, 1 }, { 1, 0 }, { 1, 1 }, { 1, -1 } }; for (int i 0; i 4; i) { int count 1; // 正方向 count CountDirection(row, col, directions[i, 0], directions[i, 1], player); // 反方向 count CountDirection(row, col, -directions[i, 0], -directions[i, 1], player); if (count 5) return true; } return false; } private int CountDirection(int row, int col, int dr, int dc, int player) { int count 0; int r row dr; int c col dc; while (r 0 r BOARD_SIZE c 0 c BOARD_SIZE boardState[r, c] player) { count; r dr; c dc; } return count; }这个算法的好处是每次落子只需要检查最多4x832个格子性能开销小代码也直观。边界条件已经写在while循环里了不会越界。实测下来这个判定逻辑在15x15棋盘上完全没有性能压力哪怕后续扩展成19路棋盘也够用。2.3 悔棋与对局流程控制悔棋功能是一个加分项很多同学的代码里没有。实现思路很简单用一个栈记录每一步的落子坐标和玩家信息悔棋时弹出栈顶把棋盘数组对应位置清零然后刷新界面。栈这种数据结构恰好和撤销最后一步的需求天然匹配课程设计里正好可以说用到了栈结构算是把数据结构和实际应用结合了起来。private StackMoveInfo moveHistory new StackMoveInfo(); private void PlacePiece(int row, int col) { boardState[row, col] currentPlayer; moveHistory.Push(new MoveInfo(row, col, currentPlayer)); Invalidate(); // 触发重绘 if (CheckWin(row, col, currentPlayer)) { MessageBox.Show($玩家{(currentPlayer 1 ? 黑棋 : 白棋)}获胜); SaveGameRecord(currentPlayer); ResetGame(); return; } currentPlayer (currentPlayer 1) ? 2 : 1; UpdateStatusBar(); } private void UndoLastMove() { if (moveHistory.Count 0) return; MoveInfo move moveHistory.Pop(); boardState[move.Row, move.Col] 0; currentPlayer move.Player; Invalidate(); }这里有一个细节要注意悔棋后回合权要归回悔棋方。如果白棋悔棋棋子和回合都应该恢复到白棋落子之前的状态。MoveInfo里存了Player信息直接恢复即可。另一个细节是对局状态的控制。比如一方获胜后应该锁定棋盘不能继续落子。我是用一个bool类型的gameOver标志位控制在获胜分支里置为trueMouseClick事件的第一行就判断if (gameOver) return;3. 数据库设计与接入3.1 表结构设计从玩家到对局记录数据这块的设计要贴合五子棋的实际业务不能为了建表而建表。我的表结构分为两张核心表玩家表和比赛记录表再加一个视图做排行榜。玩家表Players用来存注册用户CREATE TABLE Players ( PlayerId INT IDENTITY(1,1) PRIMARY KEY, Username NVARCHAR(50) NOT NULL UNIQUE, Password NVARCHAR(50) NOT NULL, RegistrationTime DATETIME DEFAULT GETDATE(), TotalGames INT DEFAULT 0, Wins INT DEFAULT 0 )比赛记录表GameRecords用来存每一局的详细结果CREATE TABLE GameRecords ( RecordId INT IDENTITY(1,1) PRIMARY KEY, PlayerId INT FOREIGN KEY REFERENCES Players(PlayerId), OpponentName NVARCHAR(50) NOT NULL, Result NVARCHAR(10) NOT NULL, -- Win, Lose, Draw StepsCount INT DEFAULT 0, PlayTime DATETIME DEFAULT GETDATE() )为什么要把对手名字冗余存储而不是再建一张对手表因为五子棋在这里是单机本地对战两个玩家可能都注册过也可能只是临时对战没登录。用PlayerId关联到其中一个真人玩家OpponentName纯粹记录显示信息这样既保持了数据完整性又不会因为必须两个玩家都在库里注册而阻塞对局流程。作为课程设计这个取舍是可以解释清楚的。3.2 排行榜查询与统计排行榜做的是显示胜率、总场次、获胜场次。用视图封装查询逻辑调用端只写一行代码CREATE VIEW vw_Rankings AS SELECT TOP 10 Username, TotalGames, Wins, CAST(Wins AS FLOAT) / NULLIF(TotalGames, 0) * 100 AS WinRate FROM Players WHERE TotalGames 0 ORDER BY WinRate DESC, Wins DESCNULLIF的处理很关键TotalGames为0时SQL Server会报除零错误用NULLIF把0转成NULL计算结果就是NULL而不是报错。这个细节在答辩时老师问了能说明白是加分的。调用端用C#封装一个方法public DataTable GetRankings() { string sql SELECT * FROM vw_Rankings; return ExecuteQuery(sql); }然后绑定到DataGridView上两三行代码就能出排行榜界面DataTable dt dbHelper.GetRankings(); dataGridView1.DataSource dt; dataGridView1.Columns[Username].HeaderText 玩家; dataGridView1.Columns[TotalGames].HeaderText 总局数; dataGridView1.Columns[Wins].HeaderText 胜场; dataGridView1.Columns[WinRate].HeaderText 胜率(%);3.3 数据库连接与初始化数据库连接字符串建议放在App.config里方便部署时改不用重新编译connectionStrings add nameGomokuDB connectionStringData Source.;Initial CatalogGomokuDB;Integrated SecurityTrue / /connectionStringsDBHelper类封装常用操作public class DBHelper { private static readonly string connStr ConfigurationManager.ConnectionStrings[GomokuDB].ConnectionString; public static SqlConnection GetConnection() { SqlConnection conn new SqlConnection(connStr); conn.Open(); return conn; } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn GetConnection()) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } public static DataTable ExecuteQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn GetConnection()) using (SqlDataAdapter adapter new SqlDataAdapter(sql, conn)) { if (parameters ! null) adapter.SelectCommand.Parameters.AddRange(parameters); DataTable dt new DataTable(); adapter.Fill(dt); return dt; } } }这里用了using语句确保连接对象用完自动释放这是很多初学者容易漏掉的。连接不释放跑几局后SQL Server的连接池满了程序就卡死。4. 运行配置与常见问题排查4.1 环境要求与启动步骤这个项目跑起来需要的环境很常规Visual Studio 2017及以上版本2019/2022都行.NET Framework 4.6.1或更高WinForm的老项目一般用这个版本SQL Server 2012及以上Express版也够用SQL Server Management Studio可选只用来建库时用拿到源码后的启动步骤在SQL Server Management Studio里执行项目目录下的GomokuDB.sql脚本自动创建数据库和表用Visual Studio打开sln解决方案文件检查App.config里的连接字符串如果是别人的机器名或实例名改成自己的直接F5运行如果不想用SQL Server的Windows身份认证也可以改连接串用SQL Server身份认证但要注意SQL Server服务里得开启混合验证模式不然连接报错。4.2 数据库连接常见问题数据库连接是这里报错的重灾区。我把最常见的几类问题整理一下问题一无法连接到数据库报错信息通常是在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误。排查思路先确认SQL Server服务有没有启动。WinR输入services.msc找到SQL Server (MSSQLSERVER)服务启动类型设为自动确认连接字符串的服务器名。默认实例用localhost或.命名实例要写成localhost\实例名确认数据库名存在。在SSMS里执行SELECT name FROM sys.databases看看问题二登录失败用户xxx登录失败这个报错多半是身份验证模式和连接串里的配置不匹配。要么在连接串里加上Integrated SecurityTrue用Windows认证要么把SQL Server改成混合模式并创建一个带密码的登录名。问题三数据库文件附加失败有的课程设计会附带.mdf数据文件而不是SQL脚本要求用附加方式挂库。附加失败通常是因为权限不够把数据文件放到有读权限的目录再用管理员身份操作SQL Server即可。4.3 绘图闪烁和边界问题WinForm里GDI绘图最常见的坑就是闪烁。棋盘刷新时画面一抖一抖的原因在于每次重绘时窗口先擦除背景再画内容这个过程肉眼可见。解决办法是双缓冲在窗体构造函数里加一行SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true);实测这个配置对棋盘重绘的效果立竿见影。有些教程里会建议重写OnPaintBackground为空方法但加了OptimizedDoubleBuffer之后没有必要反而可能引入其他绘制问题。五子棋还有一些游戏逻辑层面的边界问题要留意平局判断棋盘下满225个位置还没有五连要判定平局不能无限下下去。在落子逻辑里每次检测一下是否还有空位禁手问题这个项目是基础版五子棋没有实现禁手规则。如果要对接专业规则黑棋的三三禁手、四四禁手、长连禁手都需要额外处理。悔棋的连线记录悔棋栈如果用List存就不方便撤销用Stack是最自然的做法。但要注意C#里Stack的Pop是LIFO正好符合撤销最近一步的直觉。5. 课程设计答辩与项目扩展建议5.1 答辩时的讲解路线这个项目在答辩时的讲解顺序我建议按用户角度 → 数据角度 → 技术角度来走先演示玩家从注册、登录、开始游戏、下棋到获胜的完整流程展示棋盘交互和对局记录的落库效果。然后打开数据库直接查两张表的数据证明刚才的操作真的写进了数据库。最后再从技术上讲核心代码——落子的坐标换算、CheckWin的判定算法、栈实现悔棋、双缓冲绘图。这个顺序既有用户故事又有数据佐证老师不容易打断思路。每一步要说清楚为什么这样设计。比如坐标换算里的四舍五入吸附是为了提升用户体验CheckWin只检查当前落子点四周是为了避免全盘扫描的性能浪费用栈做悔棋是数据结构在业务逻辑中的实际应用这些都是答辩的加分点。5.2 后续还可以怎么扩展课程设计交完如果还想继续完善有几个方向值得投入AI对战加入简单的人机对战用评分表给每个空位打分活四10000分、活三1000分、活二100分AI选择最高分位置落子。这个逻辑不复杂但效果能明显超越随机落子残局复盘把整盘对局按步骤存进数据库每步记录序号、坐标、玩家再实现一个回放功能逐字重放历史对局局域网对战用TCP/UDP实现双人联机服务端做房间管理和状态同步客户端只管发送落子指令和接收对方落子广播这三个方向分别对应了初级AI算法数据回放网络编程无论以后是考研复试还是找工作面试都能拿出来作为实际项目经验说。从课程设计角度来说这个项目最值得借鉴的经验就是用一个简单的游戏场景把界面编程、数据结构、数据库设计三块知识串起来。很多人的课程设计功能做得比这复杂但文档和答辩讲不清楚。源代码完成度是一方面能不能把设计思路讲明白是另一方面。做这个五子棋项目的过程中把关键的技术决策记录下来答辩的时候就有充分的底气。我自己带过的学生里凡是能把为什么选WinForm而不选WPF为什么悔棋用栈为什么排行榜用视图这几个问题讲透的成绩都不会差。本文还有配套的精品资源点击获取
返回列表