ARTICLE DETAIL

资讯详情

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

C# WinForms图书管理系统开发实战:从数据库设计到增删改查

C# WinForms图书管理系统开发实战:从数据库设计到增删改查 简介这套C# Windows窗体图书信息管理系统源码面向入门开发者基于SQL Server数据库采用Models、DAL、BLL三层架构组织完整覆盖图书信息的增、删、查、改操作既适合课程设计参考也能帮助解决Winform实战与数据库连接调试的入门问题。压缩包共135个文件其中以cs源文件、dll依赖库、resources/resx资源文件与config配置文件为主包含完整项目工程、解决方案以及数据库表结构生成脚本整体仅901KB轻量精简适合快速下载与研究。该资源已有3087人学习浏览实用性较强。系统由Visual Studio编写针对SQL Server版本兼容问题随包附带txt说明与建表代码只需简单生成数据库即可运行无需复杂配置。分层目录结构清晰便于理解界面、业务与数据访问的分离特别适合三层架构知识拓展项目调试简单初学者也能按说明快速搭建环境、顺利跑通项目。1. 为什么 C# Windows 窗体至今仍是图书管理系统的首选落地方式在数据库课程设计和中小型机构内部工具里C# windows窗体图书信息管理系统(含数据库)这一组合出现的频率一直居高不下。原因很直接Windows 窗体WinForms天生就是“拖着控件写界面”的开发模型配合 C# 对 ADO.NET 的深度封装两三天就能把一个能录入、能查询、能改、能删的图书管理系统跑起来。相比 Web 项目要处理前端框架、后端接口、跨域、部署环境一堆琐事WinForms 的调试路径短数据库连接串一配断点一打整条链路肉眼可见。这套方案适合三类人第一类是正在做数据库课程设计的学生要的是“有数据库、有界面、能演示、能答辩”第二类是小型图书室、资料室的管理人员没有专业图书系统预算需要一个能装在自己电脑上的管理工具第三类是刚接触 C# 的开发者想通过一个完整项目把 ADO.NET、DataGridView、增删改查这条主线走通。本文按我的实操习惯把从数据库建表到窗体实现的完整路径拆开讲参数、代码、坑点都给到。2. 技术选型与架构设计先定数据库再谈界面2.1 数据库选型SQL Server 还是 MySQL取舍依据是什么图书信息管理系统最常见的数据库搭配是 SQL Server 或 MySQL。选型不取决于“哪个更好”取决于你的运行环境和学习主线。SQL Server 适合 Windows 本机一键安装用 SSMS 管理图形化程度高和 C# 的 SqlClient 配合零额外配置。MySQL 的优势是跨平台、体积小但需要额外安装 Connector 驱动且本机服务容易遇到端口、字符集、权限等干扰项。我的建议是如果你在 Windows 上做课程设计或内部工具首选 SQL Server Express 或 LocalDB。LocalDB 适合不想装完整数据库服务的场景连接串里指定(localdb)\MSSQLLocalDB即可。若你以后想走 Web 方向MySQL 更贴近生产环境。无论选哪个表结构和 C# 代码的增删改查逻辑完全相同差异只在连接字符串和参数化语句里的占位符写法。2.2 三层结构要不要拆图书管理这个体量拆到哪一层合适很多人拿到这个标题直接在一个窗体的按钮事件里写完所有数据库操作——能跑但项目稍微加点功能就会失控。图书信息管理系统的合理复杂度是“两层半”窗体层负责界面和输入校验数据访问层DAL负责所有 SQL 操作业务规则简单到可以直接写在窗体层但为了让代码能答辩、能扩展最好单独拆一个业务层BLL出来。举例说明图书表包含书名、作者、出版社、ISBN、价格、分类、库存数量、可借数量这些字段。增删改查的 SQL 语句应该集中在 DAL 类里比如BookDAL窗体只调用BookDAL.AddBook(book)这样的方法。这样做的好处是后续如果要从 WinForms 迁移到 WPF 或 Web APIDAL 层几乎原样复用。基于这个分层图书管理窗体的职责只会剩三件事收集输入、调用方法、刷新表格。3. 数据库设计与连接从建表 SQL 到连接字符串全配置3.1 图书管理系统核心表结构字段类型、约束与关系设计图书信息管理系统的核心表一般只需要三张图书表Books、读者表Readers、借阅记录表BorrowRecords。如果只做图书信息管理不做借还流程Books 单表也能交差但建议把另外两张表一并建好成本极低答辩和实际使用时却完整得多。以下是我常用的建表脚本运行环境是 SQL ServerCREATE DATABASE LibraryDB; GO USE LibraryDB; GO CREATE TABLE Books ( BookId INT IDENTITY(1,1) PRIMARY KEY, BookName NVARCHAR(100) NOT NULL, Author NVARCHAR(50) NOT NULL, Publisher NVARCHAR(100) NULL, ISBN CHAR(17) NULL UNIQUE, Category NVARCHAR(30) NULL, Price DECIMAL(8,2) NULL, Quantity INT NOT NULL DEFAULT 1, RemainCount INT NOT NULL DEFAULT 1 ); GO CREATE TABLE Readers ( ReaderId INT IDENTITY(1,1) PRIMARY KEY, ReaderName NVARCHAR(50) NOT NULL, Phone NVARCHAR(20) NULL, RegDate DATETIME DEFAULT GETDATE() ); GO CREATE TABLE BorrowRecords ( RecordId INT IDENTITY(1,1) PRIMARY KEY, BookId INT NOT NULL REFERENCES Books(BookId), ReaderId INT NOT NULL REFERENCES Readers(ReaderId), BorrowDate DATETIME DEFAULT GETDATE(), ReturnDate DATETIME NULL ); GO这段建表脚本有几个要点Books表的主键用IDENTITY(1,1)自增避免应用层手动生成主键RemainCount字段单独记录当前可借数量和Quantity总数量分开维护这是在窗口里做借还操作时最容易漏掉的设计ISBN用CHAR(17)而不是VARCHARISBN 的标准长度是 17 位含连字符固定长度字段存储和检索效率更高价格用DECIMAL(8,2)不要用FLOAT避免浮点误差。BorrowRecords里的BookId和ReaderId建了外键约束但这会在删书时挡住你——后面避坑章节会专门讲这个 Conflict。3.2 连接字符串配置连接池、超时和本地化的坑C# 连接 SQL Server 的连接字符串写法比较固定但有几个参数值得注意。以本机 SQL Server Express 为例string connStr Server.;DatabaseLibraryDB;User Idsa;Password123456;;这里Server.表示本机默认实例如果是命名实例要写成Server.\SQLEXPRESS或localhost\\SQLEXPRESS。User Id和Password是 SQL Server 身份验证方式。常见翻车点是在 SSMS 里能登录但程序里报“用户登录失败”这是因为数据库实例本身开了 Windows 身份验证模式没有启用混合验证模式。解决方法是打开 SSMS右键实例选择“属性 - 安全性”勾选“SQL Server 和 Windows 身份验证模式”然后重启 SQL Server 服务。关于连接池ADO.NET 默认开启连接池连接字符串里加Poolingtrue;Min Pool Size1;Max Pool Size10;可以显式控制连接数。连接池的核心机制是SqlConnection对象被Dispose()后物理连接会被回收到池中而不是真正关闭。所以代码里必须用using语句保证释放否则大量窗口频繁开关会导致连接池耗尽报“连接超时”错误。建议在构建项目时写一个DbHelper类把SqlConnection的创建和释放统一管理后面所有窗体都调它。4. 窗体实现与增删改查从登录窗体到图书管理主界面4.1 登录窗体的实现逻辑账密校验和防 SQL 注入的必改点图书管理系统的第一个窗体通常是登录窗体。很多入门教程会教你把用户名和密码直接拼进 SQL 字符串比如SELECT * FROM Users WHERE UserName txtUser.Text AND Password txtPwd.Text 。这条代码在单机学习环境里能跑通但只要输入 OR 11这种字符串就能直接绕过登录。参数化查询是必须的写法如下private bool CheckLogin(string userName, string password) { string sql SELECT COUNT(*) FROM Users WHERE UserNameu AND Passwordp; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(u, userName); cmd.Parameters.AddWithValue(p, password); conn.Open(); int count (int)cmd.ExecuteScalar(); return count 0; } }这段代码里有两个关键点。第一Parameters.AddWithValue会把用户输入当成参数传给 SQL Server而不是拼进 SQL 文本从根上堵住了注入第二ExecuteScalar()返回第一行第一列的值拿COUNT(*)的结果和 0 比较比用ExecuteReader()再循环判断简洁得多。密码在真实系统里应该加密存储常见的做法是对密码做哈希如 SHA256 加盐用户登录时再哈希对比。课程设计阶段如果不想引额外库MD5 也能接受但面试时至少能说出“MD5 可碰撞生产环境要用 BCrypt 或 SHA256且需要加盐”。4.2 主窗体和图书管理界面DataGridView 绑定数据的最短路径登录成功后进入主窗体。主窗体建议用MenuStrip加一个TabControl的布局菜单栏放“图书管理”“读者管理”“借阅管理”“系统退出”Tab 页放查询区和 DataGridView。图书管理界面是整个系统的核心它的标准结构是上方一个GroupBox放查询条件书名、分类、作者中间放ToolStrip按钮添加、修改、删除、刷新下方一个DataGridView。加载图书列表的方法是DataGridView.DataSource直接绑定DataTableprivate void LoadBookList(string keyword ) { string sql SELECT BookId, BookName, Author, Publisher, Category, Price, Quantity, RemainCount FROM Books WHERE BookName LIKE kw OR Author LIKE kw; using (SqlConnection conn new SqlConnection(connStr)) using (SqlDataAdapter da new SqlDataAdapter(sql, conn)) { da.SelectCommand.Parameters.AddWithValue(kw, % keyword %); DataTable dt new DataTable(); da.Fill(dt); dgvBooks.DataSource dt; } }这里用SqlDataAdapter而不是SqlCommand DataReader是刻意的DataReader是只向前的流式读取适合逐条处理DataGridView需要把数据整体装入内存做排序、翻页和展开DataTable是更合适的容器。Fill()之后DataGridView会自动根据列的ColumnName生成表头并填充数据行。表头要显示中文名在数据库里把字段名定义成英文然后在窗体的Load事件或DataGridView.Columns集合里重新设定HeaderText。添加图书的界面我用一个单独Form窗体实现。private void btnAddBook_Click(object sender, EventArgs e) { string sql INSERT INTO Books (BookName, Author, Publisher, ISBN, Category, Price, Quantity, RemainCount) VALUES (name, author, publisher, isbn, category, price, quantity, quantity); using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(name, txtBookName.Text.Trim()); cmd.Parameters.AddWithValue(author, txtAuthor.Text.Trim()); cmd.Parameters.AddWithValue(publisher, txtPublisher.Text.Trim()); cmd.Parameters.AddWithValue(isbn, txtISBN.Text.Trim()); cmd.Parameters.AddWithValue(category, txtCategory.Text.Trim()); cmd.Parameters.AddWithValue(price, decimal.Parse(txtPrice.Text)); cmd.Parameters.AddWithValue(quantity, int.Parse(txtQuantity.Text)); conn.Open(); cmd.ExecuteNonQuery(); } MessageBox.Show(添加成功); LoadBookList(); }注意VALUES里最后两个字段都写quantity这是让“总数量”和“可借数量”初始值一致。每次录入一本新书时两数量相同以后每次借出一本RemainCount减一归还时加一。Trim()处理输入框首尾空格这能避免用户粘贴时不小心带入空格导致查询时匹配不到。int.Parse和decimal.Parse是简化写法如果输入非数字程序会抛异常可以改成TryParse但课程设计阶段注意演示时别输错格式就可以了。修改和删除的逻辑与添加完全对称修改用UPDATE语句删除用DELETE都靠Where 主键定位UPDATE Books SET BookNamename, Authorauthor, Priceprice WHERE BookIdid; DELETE FROM Books WHERE BookIdid;有一个实用细节DataGridView选中行的CellValue可以直接取出BookId没必要为修改单独做一个带隐藏 ID 的窗体。4.3 查询与联动组合条件查询让系统显得完整单条件查询实现起来很简单但图书信息管理系统里更常见的是组合查询。例如“书名包含某关键词 且 分类为某类别 且 价格在某区间”。SQL 语句预先拼好固定前缀查询条件用AND拼装拼接部分只影响参数对象不影响参数值string sql SELECT * FROM Books WHERE 11; Liststring conditions new Liststring(); if (!string.IsNullOrWhiteSpace(txtBookName.Text)) { conditions.Add(BookName LIKE kw); } if (!string.IsNullOrWhiteSpace(cmbCategory.Text)) { conditions.Add(Category cat); } if (!string.IsNullOrWhiteSpace(txtPriceMin.Text)) { conditions.Add(Price priceMin); } string whereSql string.Join( AND , conditions); sql WHERE whereSql; using (SqlConnection conn new SqlConnection(connStr)) using (SqlDataAdapter da new SqlDataAdapter(sql, conn)) { if (conditions.Contains(BookName LIKE kw)) { da.SelectCommand.Parameters.AddWithValue(kw, % txtBookName.Text.Trim() %); } // 其余参数类似追加 DataTable dt new DataTable(); da.Fill(dt); dgvBooks.DataSource dt; }WHERE 11是一种让条件追加更自然的写法多条件时不需要判断“是不是第一个条件”直接在每个条件前加AND即可。组合条件查询在答辩时被问到的概率很高因为它是从“能跑”到“健壮”的分水岭。5. 图书管理系统的避坑与排查这 5 个问题我几乎每次都会遇到5.1 数据库连接失败的三种典型现象现象一程序运行时抛SqlException: 在建立与服务器的连接时出错。在连接到 SQL Server 时在默认的设置下 SQL Server 不允许进行远程连接。原因是 SQL Server 默认不允许 TCP/IP 远程连接。解决打开“SQL Server 配置管理器”在 SQL Server 网络配置里启用 TCP/IP 协议并重启 SQL Server 服务。本机直连用Server.通常不会触发但用过Serverlocalhost时可能遇到。现象二能连上数据库但执行 SQL 时报“对象名 Books 无效”。原因通常是当前连接的数据库不是你建表的那个库。排查方法在连接字符串里显式加Initial CatalogLibraryDB不要依赖默认数据库。或者在SqlConnection打开后执行USE LibraryDB但前者更规范。现象三程序运行一会儿后报“连接池已达到最大大小”。原因是某个窗体里的SqlConnection没释放。排查方法全项目搜索new SqlConnection确认是否每条代码路径都套了using。最容易漏的是try-catch中间return了finally没写释放。5.2 外键约束导致删除图书失败BorrowRecords表引用了Books.BookId后如果某本书已有借阅记录直接执行DELETE FROM Books WHERE BookIdid会报外键冲突。这是数据库设计正确性的体现但业务上确实存在“这本书已经没人借了但以前借过”的删除需求。解决思路有两类。第一类是业务上允许删除时删除前先删除关联的借阅记录即DELETE FROM BorrowRecords WHERE BookIdid在前再删 Books。第二类方案是给外键加ON DELETE CASCADE建表时在REFERENCES Books(BookId)后追加这个声明。CASCADE 适合“借阅记录随图书一起消失”的场景但要注意会连读者历史一并抹掉对于盘点审计不太友好。我的习惯是在删除按钮里写两行 SQL 用事务包裹避免只删一半造成脏数据。using (SqlConnection conn new SqlConnection(connStr)) using (SqlTransaction tran conn.BeginTransaction()) { SqlCommand cmd new SqlCommand(sql, conn, tran); cmd.Parameters.AddWithValue(id, bookId); cmd.ExecuteNonQuery(); // 第二个删除命令共用事务 tran.Commit(); }5.3 中文乱码问题字符集不一致的层层排查图书管理系统的中文乱码绝大多数情况不是 C# 代码问题而是数据库字段类型问题。SQL Server 里应该用NVARCHAR而不是VARCHAR存中文否则写入时如果连接字符串没指定Character Set或代码页不匹配中文会变成问号。上面的建表语句全部用了NVARCHAR就是为这个。MySQL 场景下需要在连接字符串里追加CharSetutf8mb4并在建表时指定DEFAULT CHARSETutf8mb4。如果数据库字段类型没问题而界面显示乱码检查窗体文件的编码是否被 IDE 搞乱。解决方案是右键.cs文件选择“文件编码”确保是 UTF-8。这个坑看起来低级但在团队协作项目里经常出现因为某个成员用 VS 旧版本打开过文件并保存。5.4 DataGridView 刷新后选中行丢失界面上的需求通常是“修改完某条数据后表格仍选中这一行”。但每次LoadBookList()都重新绑定整个DataSource行号会重置到第一行。解决在重新绑定之前记录当前行的主键值绑定完成后再查找对应行并选中。int currentBookId (int)dgvBooks.CurrentRow.Cells[BookId].Value; LoadBookList(); foreach (DataGridViewRow row in dgvBooks.Rows) { if ((int)row.Cells[BookId].Value currentBookId) { dgvBooks.CurrentCell row.Cells[0]; break; } }这种做法依赖BookId作为行的唯一标识也依赖于DataGridView允许CurrentCell被赋值。数据量大时逐行遍历会有性能问题但课程设计级别的数据量完全够用。5.5 SqlParameter 变量名写错不报编译错误cmd.Parameters.AddWithValue(name, ...)里的占位符和VALUES (name)里的名字不一致时程序不会在编译期报错而是在运行时抛SqlException: 必须声明标量变量 name。这种低级错误最好在写 SQL 时就留意但被坑的人数一直很多。另外一个隐蔽点是AddWithValue的参数名如果漏了前缀也会触发同样的运行时异常。我写参数化 SQL 时的习惯是先写 SQL 里的参数清单再逐一写代码最后统查一遍参数名。6. 三个让图书管理系统从「能交差」到「拿得出手」的进阶改动6.1 借阅与归还的事务实现一个按钮完成锁存与解锁图书管理系统的点睛之笔是把“借阅”和“归还”做成事务操作而不是简单地往BorrowRecords插一条记录。借书的核心操作包含两步往BorrowRecords插入记录同时把Books.RemainCount减一。这两步必须在一个事务里完成否则插入成功但扣减失败就会出现“书没库存但记录有借阅”的脏数据。private void BorrowBook(int bookId, int readerId) { string sqlBorrow INSERT INTO BorrowRecords(BookId, ReaderId) VALUES(bookId, readerId); UPDATE Books SET RemainCount RemainCount - 1 WHERE BookIdbookId AND RemainCount 0; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sqlBorrow, conn)) { cmd.Parameters.AddWithValue(bookId, bookId); cmd.Parameters.AddWithValue(readerId, readerId); conn.Open(); int affectRows cmd.ExecuteNonQuery(); if (affectRows 0) { MessageBox.Show(库存不足或图书不存在); } } }这两条 SQL 一次发送、一个连接、一个事务作用域SQL Server 会把它们当作隐式事务处理。加了AND RemainCount 0的目的是让并发场景下两个窗口同时借同一本书只有一条语句能生效。6.2 导出图书清单到 Excel用数据绑定的思路跨界图书管理系统的报表需求经常是“保图书清单打印出来”或“导出 Excel 给领导看”。WinForms 里做 Excel 导出不用引 NPOI 或 EPPlus 也能做——直接在代码里生成 CSV 文件再用 Excel 打开。CSV 是纯文本不需要装 Office 组件也不容易遇到权限问题。StringBuilder sb new StringBuilder(); sb.AppendLine(书名,作者,出版社,价格,库存); foreach (DataGridViewRow row in dgvBooks.Rows) { if (row.IsNewRow) continue; sb.AppendLine(${row.Cells[BookName].Value},{row.Cells[Author].Value}, ${row.Cells[Publisher].Value},{row.Cells[Price].Value}, ${row.Cells[Quantity].Value}); } File.WriteAllText(图书清单.csv, sb.ToString(), Encoding.UTF8);注意Excel打开 CSV 时默认可能乱码这是因为 Excel 默认用 ANSI 编码读 CSV。UTF-8 的 CSV 要打开正确需要向文件写入带 BOM 的编码new UTF8Encoding(true)。生产环境要真正生成 xlsx 还是用 EPPlus但这套方案胜在零依赖交付给用户时不会因为目标机器没装 Office 而翻车。6.3 参数化查询防注入是底线但别忘了事务隔离级别很多入门项目止步于“增删改查跑通”但如果在借阅事务里加上隔离级别项目的技术含量会明显不一样。默认情况下 SQL Server 的隔离级别是 READ COMMITTED两个窗口同时对同一本书借阅时可能产生“丢失更新”或“不可重复读”。把事务隔离级别提升到 READ COMMITTED 并按上述RemainCount 0的条件约束可以让并发借阅场景数据一致。最终端的验证方法是模拟并发把窗体复制成两个实例同时操作同一本书。如果库存只扣减了一次说明事务控制有效。这个验证过程本身就是答辩时的加分项——很多人的项目经不起这种测试。我在自己的项目里养成的一个习惯是每完成一个小功能先在 SQL Server Management Studio 里把 SQL 语句执行一遍确认影响行数符合预期再去窗体里测试。别嫌慢这能省掉大量“在界面上点一下才发现 SQL 写错”的无谓消耗。图书信息管理系统是一个入门级项目但它覆盖了一个正经业务系统的完整链路需求分析、数据建模、界面设计、连接管理、异常处理、并发考虑。把这个项目做扎实比浮光掠影写十个功能点更有价值。希望帮到你。本文还有配套的精品资源点击获取
返回列表