ARTICLE DETAIL

资讯详情

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

C#五子棋课程设计:WinForms+SQL Server完整项目实战

C#五子棋课程设计:WinForms+SQL Server完整项目实战 简介这份资源是面向高校计算机专业学生的C#五子棋课程项目完整源码包适合正在做课程设计、需要参考完整项目结构或准备答辩演示的同学。项目覆盖C#面向对象编程、人机对战AI算法、局域网联机通信、数据库存储玩家信息与对战记录以及BGM播放、悔棋、界面交互等模块可作为综合练习或二次开发的基础。压缩包共200个文件约53.01MB以cs源码、png界面素材、resx与resources资源文件为主另含dll依赖、mp3背景音乐、config配置、exe可执行程序及mdf/ldf数据库文件结构完整可直接运行。目前已有615人学习下载能帮助读者快速理解五子棋项目的模块划分、AI决策与网络同步思路并对照数据库设计完善自己的课程作业。1. C# 五子棋大学项目从源码到数据库一套能跑通的完整方案很多计算机专业的学生在做课程设计时都会遇到一个经典选题——五子棋。但真正动手时才发现网上能找到的要么是纯控制台版本要么是只有界面没有数据持久化的半成品想找一个带数据库、能存棋谱、能管理用户信息的完整 C# 项目并不容易。这里要讲的这套方案核心就是用 C# WinForms 做界面交互用 SQL Server 或 SQLite 做数据存储把用户注册登录、对局记录、棋谱回放、胜负统计串成一条完整链路。它适合正在做课程设计的学生也适合想用一个小项目练手 C# 数据库增删改查和 WinForms 绘图的开发者。下面从架构选型开始一步步拆到能复现的程度。2. 架构选型与环境搭建WinForms ADO.NET 为什么比 EF 更适合这个项目2.1 技术栈的取舍逻辑大学项目有一个很现实的特点周期短、要求明确、答辩时需要能讲清楚每一层在做什么。基于这个前提我一般会推荐 WinForms ADO.NET 的组合而不是 Entity Framework 或 WPF。原因有三点第一WinForms 的绘图 API 足够简单五子棋棋盘本质上就是一个 15×15 的网格用 GDI 的 Graphics 对象画线和圆就能搞定不需要复杂的绑定机制第二ADO.NET 的 SqlConnection、SqlCommand、SqlDataReader 这套东西是 C# 数据库操作的基本功答辩时老师问起来你能说清楚每一步在干什么而 EF 的 LINQ 查询虽然优雅但底层发生了什么很多学生讲不明白第三SQL Server Express 或 SQLite 的部署成本低前者在 Visual Studio 安装时自带后者只需要一个 NuGet 包。数据库选型上如果学校机房统一装了 SQL Server那就用 SQL Server如果希望项目能拷到任何电脑上直接跑SQLite 更省事。两者的 C# 操作代码差异很小主要是连接字符串和少量 SQL 方言的区别。下面以 SQL Server 为主来写SQLite 的差异点会单独标注。2.2 开发环境搭建的具体步骤先确认 Visual Studio 版本。VS2019 或 VS2022 社区版都可以安装时勾选「.NET 桌面开发」工作负载这会自带 WinForms 设计器和 SQL Server Express LocalDB。如果你打算用 SQLite还需要在 NuGet 里装 System.Data.SQLite 包。创建项目的步骤# 在 Visual Studio 中操作对应命令行等价描述 # 1. 新建项目 - Windows 窗体应用 (.NET Framework) # 2. 项目名设为 GomokuGame # 3. 目标框架选 .NET Framework 4.7.2 或更高 # 4. 在解决方案资源管理器中右键项目 - 添加 - 新建项 - 类 # 依次添加 GameLogic.cs、DatabaseHelper.cs、UserManager.cs项目结构建议这样划分Form1.cs 负责界面和事件响应GameLogic.cs 放棋盘状态和胜负判断DatabaseHelper.cs 封装所有数据库连接和命令执行UserManager.cs 处理用户注册登录。这样分层的好处是答辩时你可以指着文件说「这是表现层这是业务逻辑层这是数据访问层」老师一听就知道你理解了三层架构。2.3 数据库表的建表语句与字段说明数据库至少需要两张表用户表和棋谱表。用户表存账号信息棋谱表存每一步的落子记录。建表 SQL 如下-- 用户表 CREATE TABLE Users ( UserId INT PRIMARY KEY IDENTITY(1,1), Username NVARCHAR(50) NOT NULL UNIQUE, PasswordHash NVARCHAR(128) NOT NULL, CreatedAt DATETIME DEFAULT GETDATE(), TotalGames INT DEFAULT 0, Wins INT DEFAULT 0 ); -- 棋谱表 CREATE TABLE GameRecords ( RecordId INT PRIMARY KEY IDENTITY(1,1), UserId INT NOT NULL, OpponentName NVARCHAR(50), Result NVARCHAR(10), -- Win / Lose / Draw Moves NVARCHAR(MAX), -- 存储落子序列格式如 7,7;8,8;7,8 PlayedAt DATETIME DEFAULT GETDATE(), FOREIGN KEY (UserId) REFERENCES Users(UserId) );Moves 字段用 NVARCHAR(MAX) 存一个分号分隔的坐标串每个坐标是「行,列」的格式。这样做的好处是读写简单不需要额外建一张落子明细表。如果棋谱很长也可以考虑用 JSON 格式存C# 端用 Newtonsoft.Json 序列化但大学项目里分号分隔已经够用。注意PasswordHash 字段不要存明文密码。哪怕是个课程设计用 SHA256 做一次哈希也是基本操作答辩时这是一个加分项。3. 棋盘绘制与落子逻辑GDI 画格子和鼠标事件处理3.1 用 Graphics 画一个 15×15 的棋盘WinForms 里画棋盘核心是重写窗体的 Paint 事件。棋盘边距留 30 像素格子大小设为 35 像素这样 15×15 的棋盘总宽是 14×35 60 550 像素左右普通窗体放得下。private const int BoardSize 15; private const int CellSize 35; private const int Margin 30; private void Form1_Paint(object sender, PaintEventArgs e) { Graphics g e.Graphics; g.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 画网格线 for (int i 0; i BoardSize; i) { // 横线 g.DrawLine(Pens.Black, Margin, Margin i * CellSize, Margin (BoardSize - 1) * CellSize, Margin i * CellSize); // 竖线 g.DrawLine(Pens.Black, Margin i * CellSize, Margin, Margin i * CellSize, Margin (BoardSize - 1) * CellSize); } // 画天元和星位可选增加真实感 int center Margin 7 * CellSize; g.FillEllipse(Brushes.Black, center - 4, center - 4, 8, 8); }这段代码的逻辑很直接循环 15 次每次画一条横线和一条竖线交点就是落子位置。SmoothingMode 设为 AntiAlias 是为了让线条和棋子边缘不出现锯齿。天元位置在 (7,7)用一个小黑圆点标记。棋子的绘制放在鼠标点击事件里而不是 Paint 里全部重画。每次落子后调用 Invalidate() 触发重绘在 Paint 里根据棋盘状态数组画出所有已落子的棋子。棋盘状态用一个二维数组 int[15,15] 表示0 为空1 为黑子2 为白子。3.2 鼠标点击坐标到棋盘格子的映射鼠标点击的坐标是像素值需要转换成棋盘的行列索引。转换公式是行列 (鼠标坐标 - Margin CellSize/2) / CellSize。加 CellSize/2 是为了让点击落在格子中心附近就算有效。private int[,] board new int[BoardSize, BoardSize]; private bool isBlackTurn true; private void Form1_MouseClick(object sender, MouseEventArgs e) { int col (e.X - Margin CellSize / 2) / CellSize; int row (e.Y - Margin CellSize / 2) / CellSize; // 边界检查 if (row 0 || row BoardSize || col 0 || col BoardSize) return; if (board[row, col] ! 0) return; // 该位置已有棋子 board[row, col] isBlackTurn ? 1 : 2; isBlackTurn !isBlackTurn; // 记录落子到棋谱列表 moveHistory.Add(${row},{col}); this.Invalidate(); // 触发重绘 // 检查胜负 if (CheckWin(row, col)) { string winner board[row, col] 1 ? 黑方 : 白方; MessageBox.Show(${winner}获胜); SaveGameRecord(winner); } }这段代码里几个关键点边界检查防止数组越界已有棋子的位置不能重复落子moveHistory 列表用来后续存数据库。CheckWin 函数需要检查四个方向——水平、垂直、两条对角线每个方向从当前落子点向两边延伸计数达到 5 个就判胜。3.3 胜负判断的四个方向扫描CheckWin 的实现思路是以刚落的棋子为中心分别向四个方向的正反两侧统计同色棋子数量。如果某个方向的总数达到或超过 5就返回 true。private bool CheckWin(int row, int col) { int color board[row, col]; // 四个方向水平、垂直、主对角线、副对角线 int[][] directions new int[][] { new int[] {0, 1}, // 水平 new int[] {1, 0}, // 垂直 new int[] {1, 1}, // 主对角线 new int[] {1, -1} // 副对角线 }; foreach (var dir in directions) { int count 1; // 当前棋子本身算一个 // 正方向延伸 for (int step 1; step 5; step) { int r row dir[0] * step; int c col dir[1] * step; if (r 0 || r BoardSize || c 0 || c BoardSize) break; if (board[r, c] ! color) break; count; } // 反方向延伸 for (int step 1; step 5; step) { int r row - dir[0] * step; int c col - dir[1] * step; if (r 0 || r BoardSize || c 0 || c BoardSize) break; if (board[r, c] ! color) break; count; } if (count 5) return true; } return false; }这个算法的复杂度是常数级因为每个方向最多检查 4 步。注意副对角线的方向向量是 {1, -1}也就是行增加、列减少。很多初学者在这里容易把方向搞反导致斜向五连判断失效这是一个高频翻车点。4. 数据库持久化用户注册登录与棋谱存取4.1 用 ADO.NET 封装数据库连接DatabaseHelper 类的职责是提供统一的数据库访问入口。连接字符串根据数据库类型不同而不同SQL Server LocalDB 的典型连接串是Server(localdb)\MSSQLLocalDB;DatabaseGomokuDB;Integrated Securitytrue;SQLite 则是Data Sourcegomoku.db;Version3;。public static class DatabaseHelper { private static string connStr Server(localdb)\MSSQLLocalDB;DatabaseGomokuDB;Integrated Securitytrue;; public static SqlConnection GetConnection() { var conn new SqlConnection(connStr); conn.Open(); return conn; } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (var conn GetConnection()) using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } public static object ExecuteScalar(string sql, params SqlParameter[] parameters) { using (var conn GetConnection()) using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteScalar(); } } }这里用 using 语句确保连接和命令对象在使用后自动释放避免连接泄漏。参数化查询是必须的不要用字符串拼接 SQL否则会有 SQL 注入风险答辩时老师很可能会问这个问题。4.2 用户注册与登录的完整流程注册时先检查用户名是否已存在如果不存在则插入新记录。密码用 SHA256 哈希后存储。public static bool Register(string username, string password) { // 检查用户名是否已存在 string checkSql SELECT COUNT(*) FROM Users WHERE Username name; int count Convert.ToInt32(DatabaseHelper.ExecuteScalar(checkSql, new SqlParameter(name, username))); if (count 0) return false; // 插入新用户 string hash ComputeSha256Hash(password); string insertSql INSERT INTO Users (Username, PasswordHash) VALUES (name, hash); int rows DatabaseHelper.ExecuteNonQuery(insertSql, new SqlParameter(name, username), new SqlParameter(hash, hash)); return rows 0; } private static string ComputeSha256Hash(string raw) { using (var sha System.Security.Cryptography.SHA256.Create()) { byte[] bytes sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(raw)); return BitConverter.ToString(bytes).Replace(-, ).ToLower(); } }登录逻辑类似先根据用户名查出 PasswordHash然后比对哈希值。如果匹配则登录成功同时可以更新 LastLoginTime 字段。4.3 棋谱的序列化存储与回放读取一局结束后把 moveHistory 列表拼接成分号分隔的字符串连同用户 ID、对手名、胜负结果一起插入 GameRecords 表。public static void SaveGameRecord(int userId, string opponent, string result, Liststring moves) { string movesStr string.Join(;, moves); string sql INSERT INTO GameRecords (UserId, OpponentName, Result, Moves) VALUES (uid, opp, result, moves); DatabaseHelper.ExecuteNonQuery(sql, new SqlParameter(uid, userId), new SqlParameter(opp, opponent), new SqlParameter(result, result), new SqlParameter(moves, movesStr)); }回放时从数据库读出 Moves 字段用 Split(;) 拆成数组然后逐步在棋盘上重绘。回放功能是答辩时的亮点老师看到你能把历史对局重新演示出来印象分会高不少。提示如果棋谱很长NVARCHAR(MAX) 的上限是 2GB完全够用。但建议在插入前检查一下 movesStr 的长度超过 8000 字符时考虑压缩或分段存储。5. 避坑与排查五子棋项目里最容易翻车的五个地方5.1 棋盘重绘闪烁严重现象每次落子后调用 Invalidate()整个棋盘闪烁一下体验很差。原因WinForms 默认的重绘机制会先用背景色擦除再画新内容这个擦除过程被人眼捕捉到了。解决把窗体的 DoubleBuffered 属性设为 true或者在构造函数里设置this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);。双缓冲会让绘制在内存中完成后一次性输出闪烁就消失了。5.2 鼠标点击位置偏移现象点击棋盘的某个交叉点棋子却画在了旁边一格。原因坐标转换公式里没有考虑棋子的半径偏移或者 Margin 值和绘制时的 Margin 不一致。解决确保绘制和点击处理用的是同一组常量并且在坐标转换时加上 CellSize/2 的偏移量。如果还是偏可以在点击位置画一个临时红点调试看看实际映射到了哪个格子。5.3 数据库连接字符串写错导致运行时崩溃现象程序编译通过一运行到数据库操作就抛异常提示「无法打开登录所请求的数据库」。原因连接字符串里的数据库名和实际建库名不一致或者 LocalDB 实例名写错。解决先在 Visual Studio 的「SQL Server 对象资源管理器」里确认数据库确实存在然后复制属性窗口里的连接字符串。如果用 SQLite检查 .db 文件是否被复制到了输出目录必要时在文件属性里设置「复制到输出目录」。5.4 胜负判断在边界位置漏判现象在棋盘边缘形成五连程序却没有判胜。原因CheckWin 里的边界检查写成了r 0 || r BoardSize应该是r BoardSize。这种差一错误在数组操作里非常常见。解决把边界检查单独写成一个函数IsValid(int r, int c)所有地方统一调用避免重复写错。5.5 多局游戏后棋盘状态未重置现象第一局结束后点「新游戏」棋盘上还残留着上一局的棋子。原因重置逻辑只清了 moveHistory 列表没有把 board 数组清零也没有重置 isBlackTurn。解决写一个 ResetGame() 方法把 board 数组用 Array.Clear 清零moveHistory.Clear()isBlackTurn 设回 true然后 Invalidate()。每次新开局前调用一次。6. 进阶技巧用存储过程做排行榜和胜率统计基础功能跑通之后如果想在答辩时多拿几分可以加一个排行榜功能。与其在 C# 里查所有记录再算不如直接用 SQL 的聚合查询甚至写成存储过程。CREATE PROCEDURE GetLeaderboard AS BEGIN SELECT TOP 10 Username, TotalGames, Wins, CASE WHEN TotalGames 0 THEN 0 ELSE CAST(Wins AS FLOAT) / TotalGames * 100 END AS WinRate FROM Users WHERE TotalGames 0 ORDER BY WinRate DESC, Wins DESC; ENDC# 端调用这个存储过程把 CommandType 设为 StoredProcedure然后用 SqlDataReader 读结果绑定到 DataGridView 上。这样排行榜的排序和计算都在数据库端完成C# 代码只需要负责展示。另一个实用技巧是给 GameRecords 表的 UserId 和 PlayedAt 字段加索引。棋谱表随着对局增多会越来越大没有索引的话查询「某用户最近十局」会越来越慢。加索引的语句是CREATE INDEX IX_GameRecords_UserId ON GameRecords(UserId);这个操作在数据量超过几千条时效果明显。我自己做这类项目时最大的教训是不要等到所有界面都画好了才去接数据库。先把 DatabaseHelper 和建表脚本写好用控制台程序测试通过增删改查再回到 WinForms 里调用。这样数据库层的问题不会和界面层的问题混在一起排查起来省一半时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表