资讯详情 WinForm停车系统实战:ADO.NET+SQL Server桌面应用开发指南
📅 2026/10/18 4:58:49
简介这是一套面向C#初学者与WinForm桌面开发入门者的实践型源码项目聚焦停车场收费管理这一典型业务场景帮助学习者掌握数据库驱动的GUI应用开发全流程。资源包含95个文件主体为37个C#业务逻辑与界面代码文件.cs、13个本地化资源文件.resx、13个编译资源.resources、1个SQL Server数据库脚本.sql及1个完整数据库文件.mdf辅以解决方案.sln、项目配置.csproj和运行截图等整体压缩包仅1.32MB轻量易部署。已有1323人下载学习适合通过真实模块如车辆入场登记、按停留时间自动计费、出库结算、收费记录查询及管理员权限配置理解数据绑定、事件驱动、SQL Server增删改查及多窗体协作等核心技能。源码结构清晰含完整可运行exe程序与附加数据库脚本开箱即用是巩固C#语法、WinForm控件使用与三层逻辑分离思想的优质练手项目。1. 一个能当天跑通、第二天就能改出自己小区收费规则的 WinForm 停车系统你有没有试过下载了一个标着“C#停车场系统”的压缩包解压后满屏.cs文件和.mdf双击Parking.sln却弹出“无法加载项目”或者好不容易跑起来了界面是灰色的点“车辆入场”没反应查数据库发现Parking_log.ldf居然没自动附加这不是你代码不行——是绝大多数 WinForm 入门项目缺了三样东西可复现的数据库初始化路径、WinForm 控件事件与业务逻辑的明确绑定关系、以及收费计算模块的可插拔设计说明。这个Parking.zip正好补上了。它不是玩具 Demo而是一个真实走完“车辆入场→计时→出场→生成账单→查询历史”全链路的桌面系统所有 SQL 脚本附加脚本.sql已预置Parking.mdf和Parking_log.ldf是完整数据文件连QQ图片202105231852xx.png都是实测运行截图。适合两类人刚学完 WinForm 控件拖拽、正卡在“怎么把 TextBox 里的车牌号存进数据库”的新手以及需要快速交付一个轻量级停车管理模块、但不想从零写 EF Core 连接字符串的老手。它不炫技不堆设计模式但每一步操作都有对应的数据表字段、事件处理函数和 SQL 语句——这才是实践学习该有的样子。2. 项目结构与技术栈WinForm SQL Server 的经典组合如何落地这个系统没有用 Entity Framework也没有上 WPF 或 MAUI它用的是最朴素也最扎实的组合C# 语言 Windows Forms 窗体 ADO.NET 直连 SQL Server。这种选择不是过时而是精准匹配场景部署在单台 Windows 电脑上管理员本地操作数据量在万级以内对实时性要求不高但对稳定性要求极高。整个项目结构干净得像教科书Parking/ ├── Parking.sln # 解决方案文件VS2019 可直接打开 ├── Parking/ # 主项目文件夹 │ ├── Properties/ │ ├── obj/ │ ├── bin/ │ ├── Form1.cs # 主窗体含所有按钮、DataGridView、DateTimePicker │ ├── Form1.Designer.cs # 自动生成功能区定义控件位置与初始属性 │ ├── Form1.resx # 本地化资源本项目未启用留空 │ ├── DBHelper.cs # 核心数据访问类封装 SqlConnection、SqlCommand、SqlDataAdapter │ ├── CarIn.cs # 入场业务逻辑类含车牌校验、时间戳生成、插入语句拼接 │ ├── CarOut.cs # 出场业务逻辑类含停留时长计算、费用生成、状态更新 │ ├── FeeCalculator.cs # 收费算法类支持按小时/天/次三种模式参数可配置 │ └── Program.cs # 应用程序入口仅含 Application.Run(new Form1()) ├── Parking.mdf # 主数据库文件含 CarInfo、ParkingLog、FeeRule 三张表 ├── Parking_log.ldf # 日志文件确保事务一致性 └── 附加脚本.sql # 用于手动附加数据库或重置环境的 T-SQL 脚本提示.mdf和.ldf是 SQL Server Express 的本地数据库文件无需单独安装 SQL Server 服务——Visual Studio 安装时勾选“SQL Server Data Tools”即可支持。但注意VS 默认使用 LocalDB 实例而本项目连接字符串硬编码为(local)\SQLEXPRESS这是第一个必须确认的点。2.1 数据库设计三张表撑起全部业务系统只用三张表却覆盖了从车辆登记到财务对账的全部需求设计非常克制表名关键字段作用说明CarInfoCarID(PK),PlateNumber,CarType,IsParked(bit)存储车辆基础信息IsParked1表示当前在场内避免重复入场ParkingLogLogID(PK),CarID,InTime,OutTime,FeeAmount,Status(tinyint)每次出入生成一条记录Status0为入场未出场Status1为已出场结算FeeRuleRuleID(PK),RuleType(char(2)),UnitPrice,MaxFee,EffectiveDate收费规则表RuleTypeHR小时、DY日、EV次支持多时段规则附加脚本.sql中的建表语句清晰体现了这种设计思想。例如ParkingLog表的创建脚本CREATE TABLE [dbo].[ParkingLog]( [LogID] [int] IDENTITY(1,1) NOT NULL, [CarID] [int] NOT NULL, [InTime] [datetime] NOT NULL, [OutTime] [datetime] NULL, [FeeAmount] [decimal](10, 2) NULL, [Status] [tinyint] NOT NULL DEFAULT ((0)), CONSTRAINT [PK_ParkingLog] PRIMARY KEY CLUSTERED ([LogID] ASC) )注意OutTime和FeeAmount允许为NULL因为入场时只填InTime和Status0出场时才更新这两个字段。这种“分步写入”是桌面系统应对断电、误操作的常见做法比一气呵成的事务更健壮。2.2 WinForm 窗体布局控件命名即逻辑拒绝“button1_Click”主窗体Form1.cs的控件命名严格遵循业务语义这是阅读源码的第一道门槛也是二次开发的起点控件名类型业务含义绑定事件txtPlateNumberTextBox车牌输入框TextChanged实时校验格式cmbCarTypeComboBox车型下拉框小车/大车/货车SelectedIndexChangedbtnCarInButton“车辆入场”按钮Click→ 调用CarIn.ProcessIn()dgvParkingListDataGridView停车记录列表CellDoubleClick双击某行触发出场dtPickerInTimeDateTimePicker入场时间选择器ValueChanged可手动修改默认取系统时间关键不在控件多而在每个控件的Tag属性或注释里都标明了它关联的数据库字段。比如txtPlateNumber.Tag PlateNumbercmbCarType.Tag CarType。当你想新增一个“车主电话”字段时只需三步1在CarInfo表加Phone字段2在窗体拖一个txtPhone并设TagPhone3在CarIn.cs的InsertCarInfo()方法里把txtPhone.Text加入INSERT语句参数。这种命名即契约的设计让新手也能安全地扩功能。2.3 ADO.NET 数据访问层DBHelper.cs 是整个系统的“血管”DBHelper.cs不是简单的连接字符串封装它解决了 WinForm 桌面应用最痛的三个问题连接泄漏、SQL 注入、以及跨窗体数据共享。它的核心方法只有四个但每行都有深意public static class DBHelper { // 1. 连接字符串从配置文件读取本项目写死但留了扩展口 private static readonly string connStr Data Source(local)\SQLEXPRESS;AttachDbFilename|DataDirectory|\Parking.mdf;Integrated SecurityTrue;Connect Timeout30;User InstanceTrue; // 2. ExecuteNonQuery执行 INSERT/UPDATE/DELETE返回影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (var conn new SqlConnection(connStr)) { using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); // 注意这里不 catch 异常由上层处理 } } } // 3. ExecuteDataTable执行 SELECT返回 DataTable供 DataGridView 绑定 public static DataTable ExecuteDataTable(string sql, params SqlParameter[] parameters) { var dt new DataTable(); using (var conn new SqlConnection(connStr)) { using (var da new SqlDataAdapter(sql, conn)) { da.SelectCommand.Parameters.AddRange(parameters); da.Fill(dt); } } return dt; } // 4. GetScalar执行返回单值的查询如 COUNT(*) 或 MAX(FeeAmount) public static object GetScalar(string sql, params SqlParameter[] parameters) { using (var conn new SqlConnection(connStr)) { using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteScalar(); } } } }重点看ExecuteDataTable它返回DataTable而非ListT是因为 WinForm 的DataGridView原生绑定DataTable最稳定无需额外写BindingSource。而GetScalar方法被CarOut.cs用来查“当前车辆最近一次入场时间”避免了SELECT TOP 1 InTime FROM ParkingLog WHERE CarIDid ORDER BY InTime DESC这种易错 SQL 的硬编码。这种“用封装换安全”的思路正是实践项目和课堂作业的本质区别。3. 核心模块拆解一个请求从进入到返回的完整链路我们以最典型的“车辆出场并结算”为例追踪从用户双击dgvParkingList到费用显示在界面上的完整链路。这不仅是功能验证更是理解 WinForm 事件驱动模型的关键切口。3.1 DataGridView 双击事件如何精准定位待结算车辆dgvParkingList的CellDoubleClick事件处理函数是整个出场流程的起点。它不做任何业务计算只做两件事提取当前选中行的CarID并弹出确认对话框private void dgvParkingList_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex 0) return; // 点击列头不处理 // 1. 从 DataGridView 当前行获取 CarID注意不是显示的车牌号而是隐藏的主键 int carId Convert.ToInt32(dgvParkingList.Rows[e.RowIndex].Cells[CarID].Value); // 2. 查询该车辆是否处于“在场”状态Status0 string checkSql SELECT COUNT(*) FROM ParkingLog WHERE CarIDcarId AND Status0; int count (int)DBHelper.GetScalar(checkSql, new SqlParameter(carId, carId)); if (count 0) { MessageBox.Show(该车辆当前未在场无法出场, 提示, MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 3. 弹出确认框防止误操作 DialogResult result MessageBox.Show($确认结算车牌号为 {dgvParkingList.Rows[e.RowIndex].Cells[PlateNumber].Value} 的车辆, 确认出场, MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result DialogResult.Yes) { // 4. 调用出场业务类 CarOut.ProcessOut(carId); // 5. 刷新列表 LoadParkingList(); } }这里有个血泪经验永远不要用dgvParkingList.SelectedRows[0].Cells[0].Value来取值。因为SelectedRows在多选模式下可能为空且索引 0 不一定对应CarID列。正确做法是用列名CarID显式获取哪怕多写几个字符也比后期调试半小时强。3.2 CarOut.ProcessOut()出场逻辑的四步原子操作CarOut.cs是收费计算的核心它把“出场”拆解为四个不可分割的步骤每一步失败都回滚保证数据一致性public static void ProcessOut(int carId) { try { // Step 1: 查询该车辆最近一次入场记录必须是 Status0 的 string inSql SELECT TOP 1 LogID, InTime FROM ParkingLog WHERE CarIDcarId AND Status0 ORDER BY InTime DESC; var inRow DBHelper.ExecuteDataTable(inSql, new SqlParameter(carId, carId)).Rows[0]; int logId (int)inRow[LogID]; DateTime inTime (DateTime)inRow[InTime]; // Step 2: 计算停留时长分钟和费用 TimeSpan duration DateTime.Now.Subtract(inTime); int minutes (int)duration.TotalMinutes; decimal fee FeeCalculator.CalculateFee(minutes); // 调用独立算法类 // Step 3: 更新 ParkingLog 表设置 OutTime、FeeAmount、Status1 string updateSql UPDATE ParkingLog SET OutTimeoutTime, FeeAmountfee, Status1 WHERE LogIDlogId; DBHelper.ExecuteNonQuery(updateSql, new SqlParameter(outTime, DateTime.Now), new SqlParameter(fee, fee), new SqlParameter(logId, logId)); // Step 4: 更新 CarInfo 表标记车辆已离场 string updateCarSql UPDATE CarInfo SET IsParked0 WHERE CarIDcarId; DBHelper.ExecuteNonQuery(updateCarSql, new SqlParameter(carId, carId)); MessageBox.Show($结算成功费用{fee:C}停留时长{minutes} 分钟, 成功, MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($结算失败{ex.Message}, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); // 注意这里没有 throw因为 WinForm 界面层已捕获 } }关键点在于Step 2 的FeeCalculator.CalculateFee()是完全解耦的。它不依赖数据库只接收分钟数返回费用。这意味着你可以轻松替换算法比如把CalculateFee()改成按“前30分钟免费之后每15分钟2元”来算而不用动CarOut.cs里任何一行 SQL。3.3 FeeCalculator.cs收费算法的可配置化实现FeeCalculator.cs是本项目最具二次开发价值的模块。它用一个静态字典缓存当前生效的收费规则避免每次计算都查数据库public static class FeeCalculator { private static Dictionarystring, FeeRule _currentRules new Dictionarystring, FeeRule(); // 初始化应用启动时调用一次从 FeeRule 表加载最新规则 public static void LoadRules() { _currentRules.Clear(); string sql SELECT RuleType, UnitPrice, MaxFee FROM FeeRule WHERE EffectiveDate now ORDER BY EffectiveDate DESC; var dt DBHelper.ExecuteDataTable(sql, new SqlParameter(now, DateTime.Now)); foreach (DataRow row in dt.Rows) { string ruleType row[RuleType].ToString(); _currentRules[ruleType] new FeeRule { UnitPrice (decimal)row[UnitPrice], MaxFee (decimal)row[MaxFee] }; } } // 核心计算方法输入分钟数返回费用 public static decimal CalculateFee(int minutes) { // 默认按小时计费RuleTypeHR if (!_currentRules.ContainsKey(HR)) return 0; var rule _currentRules[HR]; decimal hours Math.Ceiling((double)minutes / 60); // 向上取整到小时 decimal fee hours * rule.UnitPrice; // 不超过封顶价 return Math.Min(fee, rule.MaxFee); } } // 规则实体类仅作数据载体 public class FeeRule { public decimal UnitPrice { get; set; } public decimal MaxFee { get; set; } }注意LoadRules()必须在Program.cs的Main方法中在Application.Run(new Form1())之前调用否则首次计算会因_currentRules为空而报错。这是新手最容易翻车的地方——界面能打开但一点击“出场”就崩。4. 运行与调试从解压到看到“结算成功”的全流程现在我们把前面所有技术点串起来走一遍真正落地的操作。这不是“复制粘贴命令”而是告诉你每一步背后在解决什么问题。4.1 环境准备VS 版本、SQL Server 实例、连接字符串三者必须对齐第一步永远不是写代码而是确认你的开发环境是否匹配。本项目基于 .NET Framework 4.7.2 构建因此Visual Studio 版本必须是 VS2017 或更高版本VS2019 推荐。VS2022 默认创建 .NET 6 项目需手动修改项目属性中的目标框架。SQL Server 实例项目连接字符串为(local)\SQLEXPRESS意味着你电脑上必须有 SQL Server Express 实例。如果没有下载 SQL Server Express 免费版带 Management Studio安装时勾选“SQL Server 数据库引擎”和“SQL Server Management Studio”安装完成后打开 SSMS用 Windows 身份验证连接(local)\SQLEXPRESS确认实例正常。连接字符串校验打开Parking.csproj文件用记事本搜索ConnectionStrings或直接看DBHelper.cs。如果发现Data Source.或Data Sourcelocalhost必须手动改为(local)\SQLEXPRESS。这是 80% 的“无法连接数据库”报错的根源。4.2 数据库附加不是双击 .mdf而是用 SQL 脚本“唤醒”它很多新手以为把Parking.mdf和Parking_log.ldf放进项目文件夹VS 就会自动识别。错。.mdf是 SQL Server 的数据文件必须由 SQL Server 实例“附加”Attach后才能被访问。正确做法是打开 SQL Server Management Studio (SSMS)连接(local)\SQLEXPRESS在“对象资源管理器”中右键“数据库” → “附加”点击“添加”浏览到你解压后的Parking.mdf文件注意选.mdf不是.ldfSSMS 会自动检测并填入对应的.ldf文件路径。确认无误后点“确定”。如果提示“数据库已存在”说明之前附加过。此时右键该数据库 → “删除”勾选“删除数据库文件”再重新附加。这是清理环境的后悔药。附加成功后在 SSMS 中展开“数据库”你会看到名为Parking的数据库。此时再回到 VS打开DBHelper.cs确认连接字符串中的AttachDbFilename路径是否指向你当前解压的位置。如果是相对路径|DataDirectory|\Parking.mdf则需在项目属性 → “发布” → “应用程序文件”中确保Parking.mdf的“发布状态”为“包括”且“安装模式”为“始终包括”。4.3 首次运行绕过编译错误的三个关键修复解压、附加、打开Parking.sln按下 F5——大概率会报错。别慌以下是三个最高频的编译/运行时错误及修复错误 CS0234“类型或命名空间名称 SqlClient 不存在”原因项目未引用System.Data.SqlClient。修复右键项目 → “添加引用” → 勾选System.Data.NET Framework 4.7.2 已内置无需 NuGet。运行时报错“无法打开登录所请求的数据库”原因连接字符串中的|DataDirectory|未被正确解析导致路径错误。修复在Form1.cs的Form1_Load事件开头强制设置数据目录private void Form1_Load(object sender, EventArgs e) { // 强制将 DataDirectory 设为当前可执行文件所在目录 AppDomain.CurrentDomain.SetData(DataDirectory, Path.GetDirectoryName(Application.ExecutablePath)); // ... 其他初始化代码 }DataGridView 显示空白或报“列名不存在”异常原因dgvParkingList的AutoGenerateColumns属性为True但数据源列名与数据库字段不一致。修复在设计器中选中dgvParkingList→ 属性窗口 → 找到AutoGenerateColumns→ 设为False然后手动添加列DataPropertyName必须与DataTable的列名即 SQL 查询的 AS 别名完全一致。完成这三项修复F5 运行主窗体应该能正常弹出dgvParkingList里显示几条测试数据——恭喜你已经跨过了最大的门槛。5. 避坑指南新手必踩的 4 个坑与血泪解决方案这个项目看似简单但每一处“理所当然”的设计背后都藏着初学者要花半天才能绕出来的坑。我把最痛的四个整理出来按“现象→原因→解决”给你写透。5.1 现象点击“车辆入场”按钮提示“对象引用未设置到对象的实例”原因CarIn.cs中的InsertCarInfo()方法里有一行string plate txtPlateNumber.Text.Trim();但txtPlateNumber是窗体Form1上的控件而CarIn是一个静态类它根本拿不到Form1的实例。源码里错误地假设CarIn能直接访问窗体控件这是典型的 WinForm 作用域误解。解决把窗体控件的值作为参数传入业务类。修改CarIn.cs// 错误写法删掉 // string plate txtPlateNumber.Text.Trim(); // 正确写法所有输入都由调用方传入 public static bool ProcessIn(string plateNumber, string carType, DateTime inTime) { if (string.IsNullOrWhiteSpace(plateNumber)) return false; // 后续逻辑不变用传入的 plateNumber // ... }然后在Form1.cs的btnCarIn_Click里调用private void btnCarIn_Click(object sender, EventArgs e) { bool success CarIn.ProcessIn( txtPlateNumber.Text.Trim(), cmbCarType.SelectedItem?.ToString() ?? 小车, dtPickerInTime.Value); if (success) MessageBox.Show(入场成功); }5.2 现象数据库里ParkingLog表的OutTime字段全是1900-01-01费用为 0原因CarOut.cs中DateTime.Now.Subtract(inTime)计算时inTime是从数据库读出的datetime但 SQL Server 的datetime类型精度为 3.33 毫秒而 C# 的DateTime是 100 纳秒。当inTime值为2021-05-23 18:52:20.000时DateTime.Now.Subtract()可能因时区或精度转换产生微小偏差导致minutes计算为负数Math.Ceiling()后变成 0。解决在计算前做防御性检查并统一用TimeSpan的TotalMinutes属性TimeSpan duration DateTime.Now.Subtract(inTime); int minutes (int)Math.Max(0, duration.TotalMinutes); // 强制 0同时在ParkingLog表的InTime字段上添加默认值约束GETDATE()确保入库时间就是精确的当前时间。5.3 现象修改了FeeRule表里的UnitPrice但结算费用没变原因FeeCalculator.LoadRules()只在程序启动时调用一次后续规则变更不会自动刷新内存中的_currentRules字典。解决在FeeRule管理界面如果有或“系统设置”按钮里添加一个刷新规则的方法public static void RefreshRules() { LoadRules(); // 重新加载 MessageBox.Show(收费规则已刷新); }并在CarOut.ProcessOut()开头加上FeeCalculator.RefreshRules();仅用于演示生产环境应加锁或定时刷新。5.4 现象程序关闭后重启dgvParkingList里数据消失原因Parking.mdf文件被设置为“复制到输出目录”但每次编译VS 都会把原始.mdf覆盖掉输出目录里的文件导致所有新增数据丢失。解决在Parking.mdf文件的属性中将“复制到输出目录”改为“如果较新则复制”。这样只有你手动修改了.mdf比如用 SSMS 插入测试数据才会覆盖。日常开发中数据变更应通过 SQL 脚本或程序写入而非直接编辑.mdf文件。6. 二次开发实战如何在 30 分钟内增加“月租车位”功能现在你已经跑通了系统也避开了主要的坑。下一步是把它变成你自己的工具。我以“增加月租车位管理”为例展示一个真实、可落地的二次开发流程——不讲虚的只说你要改哪几个文件、加哪几行代码、测试时看什么。6.1 需求分析月租车位要解决什么问题标准版只支持临时停车按分钟计费。但小区物业需要1车主交一年租金获得固定车位2月租车辆入场不计费只记录进出3到期自动提醒。这需要新增一张表、两个窗体、和一处收费逻辑分支。6.2 数据库改造新增MonthlyRent表并建立关联在 SSMS 中执行以下 SQL或写入附加脚本.sql-- 新增月租表 CREATE TABLE [dbo].[MonthlyRent]( [RentID] [int] IDENTITY(1,1) NOT NULL, [CarID] [int] NOT NULL, [StartDate] [date] NOT NULL, [EndDate] [date] NOT NULL, [Status] [tinyint] NOT NULL DEFAULT ((1)), -- 1有效, 0过期 CONSTRAINT [PK_MonthlyRent] PRIMARY KEY CLUSTERED ([RentID] ASC) ) -- 添加外键约束 ALTER TABLE [dbo].[MonthlyRent] WITH CHECK ADD CONSTRAINT [FK_MonthlyRent_CarInfo] FOREIGN KEY([CarID]) REFERENCES [dbo].[CarInfo] ([CarID]) -- 为 CarInfo 表添加月租标识字段 ALTER TABLE [dbo].[CarInfo] ADD [IsMonthly] [bit] NOT NULL DEFAULT ((0))提示执行前先备份Parking.mdf。IsMonthly字段是关键它让CarInfo表能区分普通车和月租车。6.3 业务逻辑注入修改CarOut.ProcessOut()跳过月租车计费这是最核心的改动。打开CarOut.cs找到ProcessOut()方法在计算费用前插入判断// 在 Step 2 计算费用前插入以下代码 string isMonthlySql SELECT IsMonthly FROM CarInfo WHERE CarIDcarId; bool isMonthly (bool)DBHelper.GetScalar(isMonthlySql, new SqlParameter(carId, carId)); if (isMonthly) { // 月租车只更新 OutTime 和 Status费用为 0 string updateSql UPDATE ParkingLog SET OutTimeoutTime, FeeAmount0, Status1 WHERE LogIDlogId; DBHelper.ExecuteNonQuery(updateSql, new SqlParameter(outTime, DateTime.Now), new SqlParameter(logId, logId)); MessageBox.Show(月租车位结算完成费用0元, 成功, MessageBoxButtons.OK, MessageBoxIcon.Information); return; // 提前退出不执行后续计费逻辑 } // 否则走原有计费流程...6.4 界面增强在主窗体添加“月租管理”按钮和状态标识在Form1.cs设计器中拖一个Button命名为btnMonthlyManageText 设为“月租管理”双击它生成 Click 事件跳转到新窗体需新建FrmMonthly.cs在dgvParkingList中为“车牌号”列添加一个CellFormatting事件根据IsMonthly字段动态改变背景色private void dgvParkingList_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.ColumnIndex dgvParkingList.Columns[PlateNumber].Index e.RowIndex 0) { // 获取当前行的 CarID int carId (int)dgvParkingList.Rows[e.RowIndex].Cells[CarID].Value; string isMonthlySql SELECT IsMonthly FROM CarInfo WHERE CarIDcarId; bool isMonthly (bool)DBHelper.GetScalar(isMonthlySql, new SqlParameter(carId, carId)); if (isMonthly) { e.CellStyle.BackColor Color.LightGreen; e.CellStyle.ForeColor Color.DarkGreen; } } }6.5 验证与收尾三步确认功能可用数据验证用 SSMS 手动给CarInfo表中某辆车的IsMonthly设为1再给MonthlyRent表插入一条记录行为验证在程序中双击该车记录确认弹出的费用为0.00且ParkingLog表中FeeAmount0边界验证修改MonthlyRent.EndDate为昨天再次出场确认仍按月租处理因为IsMonthly字段未变业务逻辑未检查有效期——这是留给你的下一个优化点。从下载解压到完成月租功能全程不需要重装 VS、不需要重启 SQL Server、不需要查 MSDN。你只是在已有骨架上精准地缝合了三块新肉一张表、一段判断、一个颜色。这就是 WinForm 实践项目的魅力——它不追求高大上但每一步都踏在真实的地面。从那以后我每次接到类似需求都强制走一遍“画ER图→写建表SQL→改业务类→加界面反馈”的闭环哪怕只用十分钟。因为我知道跳过任何一环三天后就会回来找我修 bug。希望帮到你。本文还有配套的精品资源点击获取