C# WinForm超市收银系统落地指南:SQL脚本、扫码结算与避坑技巧
发布时间:2026/10/1 14:39:45来源:尧图网络
简介面向超市零售场景的 C# WinForm 收银/POS 系统完整源码包适合 C# 初学者、课程设计或中小超市的信息化管理参考。系统覆盖商品管理、销售收银、库存预警、报表统计、多支付方式与用户权限等模块并附带数据库初始化 SQL 脚本可在 Visual Studio 中直接打开解决方案运行。资源共 90 个文件以 28 个 C# 源码文件为核心辅以 WinForm 设计资源resx、数据集定义xsd/xss/xsc、数据库实体模型edmx与 SQL 脚本另含少量 DLL、配置文件及单元测试相关文件整包约 12.02MB结构清晰便于按功能分区学习。目前已吸引 87 人学习下载说明具备一定的实用参考价值。通过该源码可完整了解超市收银系统的分层设计与数据库交互方式既能直接用于毕业设计也可在此基础上定制扩展。1. 超市收营系统只有源码加个 SQL 文件就够了吗一次讲清 C# WinForm 收银台的完整落地“超市收营系统源码sql文件-2025”这个标题我在很多课程设计包里见过。它背后其实是三件东西一套 WinForm 界面、一组 C# 业务逻辑、一个 SQL Server 数据库脚本。别把它想成大型系统核心业务就是“扫码—算钱—记流水—管库存”。对刚学 C# 的人来说这是最容易跑通的数据库桌面项目对想接手改造的人来说难点反而不在代码而在 SQL 文件怎么导入、连接字符串怎么改、扫码枪焦点和回车怎么处理。这篇就把这三件事拆开讲从建表、写登录、搭购物车到附加数据库和避坑按一条能照做的路子来。2. SQL 文件该怎么设计商品、会员、销售流水三张表一次建档一个收银系统的 SQL 文件不是把表建出来就完事。它要同时满足“扫码时快查商品”“结算时写单不丢数据”“统计时能按日期聚合”这三个需求。常见的源码包里数据库名会叫 Supermarket 或 SHOP_DB下面这组脚本以 SQL Server 为目标用最基础的语句写成2016 到 2019 的版本都能直接跑。2.1 商品表、会员表、销售流水表三张核心表的建表脚本第一张表是商品表。这里的 Barcode 字段要建唯一索引因为扫码枪读的就是它Price 必须用 DECIMAL(10,2)不要用 float。下面这段是完整的建库和建表脚本CREATE DATABASE Supermarket; GO USE Supermarket; GO CREATE TABLE Product ( ProductId INT IDENTITY(1,1) PRIMARY KEY, Barcode NVARCHAR(32) NOT NULL UNIQUE, Name NVARCHAR(100) NOT NULL, Price DECIMAL(10,2) NOT NULL, Stock INT NOT NULL DEFAULT 0, Category NVARCHAR(50) NULL, CreateTime DATETIME DEFAULT GETDATE() ); GO说明ProductId 用自增主键好处是插入时不用自己算最大编号并发下也不会重复。Barcode 加上 UNIQUE 约束后数据库层面就拦住“同一条码两件商品”的情况。Stock 默认 0防止新增商品时忘了填库存导致负数。Category 允许 NULL收银时并不需要它只有做报表按类汇总时才用得到。接着建会员表和销售流水表。会员表一般只存手机号、余额、积分销售流水要拆成主表和明细表因为一张小票对应多个商品只放一张表会导致大量重复的订单号和金额数据统计起来又慢又容易出错。CREATE TABLE Member ( MemberId INT IDENTITY(1,1) PRIMARY KEY, Phone NVARCHAR(11) UNIQUE NOT NULL, Name NVARCHAR(50) NOT NULL, Balance DECIMAL(10,2) DEFAULT 0, Points INT DEFAULT 0 ); GO CREATE TABLE SaleOrder ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(20) NOT NULL, MemberId INT NULL, TotalAmount DECIMAL(10,2) NOT NULL, PayTime DATETIME DEFAULT GETDATE(), Operator NVARCHAR(50), FOREIGN KEY (MemberId) REFERENCES Member(MemberId) ); GO CREATE TABLE SaleDetail ( DetailId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL, ProductId INT NOT NULL, Qty INT NOT NULL, Price DECIMAL(10,2) NOT NULL, SubTotal DECIMAL(10,2) NOT NULL, FOREIGN KEY (OrderId) REFERENCES SaleOrder(OrderId), FOREIGN KEY (ProductId) REFERENCES Product(ProductId) ); GO这里有三点要注意。OrderNo 是业务编号比如 “SO20250101120000”别用自增主键代替因为客户退货或对账时需要看一个可读的单号。MemberId 允许 NULL表示“非会员买单”如果用 NOT NULL游客就没法结账了。SaleDetail 里的 Price 字段要单独存成交价不要 JOIN Product 取现价否则你改商品价格后历史小票金额就跟着变了这是做日结报表时最容易出幺蛾子的地方。2.2 索引、事务与参数化查询和超卖、SQL 注入对抗的三个设计建好表之后很多源码包就停在这了但实际跑起来会发现两个问题一是销售表数据过万后按日期查日结明显变慢二是多台收银机同时结账时库存可能被减成负数。解决慢查询只要在 PayTime 和 OrderId 上建两个普通索引CREATE NONCLUSTERED INDEX IX_SaleOrder_PayTime ON SaleOrder(PayTime); CREATE NONCLUSTERED INDEX IX_SaleDetail_OrderId ON SaleDetail(OrderId); GOPayTime 索引服务的是“查今天卖了多少钱”这类聚合 SQLOrderId 索引服务的是“按小票号找明细”。这类索引在 INSERT 时会有微小开销但对收银系统这种读多写少的场景收益远大于成本。解决扣库存问题需要在一个事务里同时完成“插入主表、插入明细、扣库存”三个动作并且扣库存时要带上 Stock 数量的条件判断BEGIN TRANSACTION; INSERT INTO SaleOrder(OrderNo, MemberId, TotalAmount, Operator) VALUES(SO20250101120000, NULL, 100.00, admin); DECLARE OrderId INT SCOPE_IDENTITY(); INSERT INTO SaleDetail(OrderId, ProductId, Qty, Price, SubTotal) VALUES(OrderId, 1, 2, 50.00, 100.00); UPDATE Product SET Stock Stock - 2 WHERE ProductId 1 AND Stock 2; IF ROWCOUNT 0 BEGIN ROLLBACK TRANSACTION; THROW 50000, 库存不足, 1; END; COMMIT TRANSACTION;这段 SQL 直接拿到源码里就能当模板用。关键在 UPDATE 语句的 WHERE Stock 2如果库存不够这一行影响 0 条我们就能回滚整个事务否则先插单后扣库存多台收银机同时操作时后一台读到的是旧库存就可能超卖。这里还有一层容易忽略的坑如果两个事务都更新同一行 Product数据库会给它上锁第二个事务要等第一个提交或回滚才继续所以收银系统尽量不要在事务里写“先查库存再更新”的两步操作而是直接用一个 UPDATE 完成判断。最后提一句 SQL 注入。很多课程设计源码里喜欢这样拼 SQL“string sql SELECT * FROM User WHERE name txtName.Text”这在本地演示没问题一旦有人输入1 OR 11就能绕过登录。正确做法是 C# 端用 SqlParameter 参数化把输入当值而不是当 SQL 语句。后面章节的代码全部按参数化写法给出千万不要把用户输入直接拼进去。3. WinForm 收银主界面怎么写从登录窗体到扫码结算的事件链路拿到源码后你会看到几个窗体LoginForm 负责登录MainForm 是收银主界面还有 ProductForm、MemberForm 这类管理窗体。这一章我们不照抄源码而是把最关键的两条链路写清楚登录怎么校验扫码后商品怎么进购物车。3.1 登录窗体用户表校验和回车键登录登录窗体的核心表叫 [User]注意 User 是 SQL Server 关键字建表时要加方括号。下面给出建表和一行示例数据密码列存的是 MD5 散列CREATE TABLE [User] ( UserId INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(50) NOT NULL UNIQUE, UserPwd NVARCHAR(32) NOT NULL ); INSERT INTO [User](UserName, UserPwd) VALUES(admin, 21232f297a57a5a743894a0e4a801fc3);上面的密文是 admin 的 MD5拿到就能直接登录。C# 端在点击登录按钮时用参数化查询去核对用户名和密码private void btnLogin_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtUser.Text) || string.IsNullOrWhiteSpace(txtPass.Text)) { MessageBox.Show(用户名和密码不能为空); return; } string sql SELECT COUNT(1) FROM [User] WHERE UserNameu AND UserPwdp; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(u, txtUser.Text.Trim()); cmd.Parameters.AddWithValue(p, MD5Helper.Md5(txtPass.Text)); conn.Open(); int count (int)cmd.ExecuteScalar(); if (count 0) { this.Hide(); new MainForm(txtUser.Text.Trim()).Show(); } else { MessageBox.Show(用户名或密码错误); } } }逻辑说明ExecuteScalar 返回查询结果第一行第一列这里就是 COUNT 的整数值比 0 大就放行。参数化的两行是关键AddWithValue 会把 TextBox 内容当成一个参数避免拼字符串带来的 SQL 注入。MD5Helper.Md5 是工具类里常见的一个静态方法内部用 System.Security.Cryptography.MD5 生成十六进制字符串你原来的源码里如果用的是明文密码建议顺手改掉。一个体验细节超市收银员习惯输完密码直接按回车所以应该在密码框的 KeyDown 事件里加同样的逻辑或者把窗体的 AcceptButton 设为 btnLogin。我自己一般两种都做避免有人用 Tab 键切换到登录按钮再按空格时卡一下。3.2 商品检索扫码枪在 TextBox 上回车触发搜索扫码枪本质是一把键盘扫到条码后快速输入数字并自动补一个回车。所以收银主界面通常是一个只读或普通 TextBox 作为扫码输入框收到回车就去数据库查商品。注意这里不能放在 TextChanged 事件里否则扫码枪逐字输入时你会触发十几次查询。private void txtBarcode_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode Keys.Enter) { SearchProduct(txtBarcode.Text.Trim()); txtBarcode.Clear(); e.SuppressKeyPress true; } } private void SearchProduct(string keyword) { string sql SELECT ProductId, Name, Price, Stock FROM Product WHERE Barcode barcode OR Name LIKE pattern; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(barcode, keyword); cmd.Parameters.AddWithValue(pattern, % keyword %); SqlDataAdapter da new SqlDataAdapter(cmd); DataTable dt new DataTable(); da.Fill(dt); dgvProducts.DataSource dt; } }这里有两层含义。扫码枪输入完整条码时Barcode 等值匹配恰好命中如果手工搜商品名LIKE 模糊匹配也能用。参数 pattern 里手写的百分号只属于搜索值不是拼 SQL所以没有注入风险。e.SuppressKeyPress true 的用途是截断回车键默认行为避免 TextBox 产生 Windows 提示音。还有一个键盘焦点问题扫码之后要把焦点重新放回 txtBarcode否则收银员用鼠标点了一下 DataGridView下一枪的字符就会打进主界面其他地方。最省事的办法是在 SearchProduct 末尾调用 this.ActiveControl txtBarcode或者在商品表格的 MouseUp 事件里手动转回输入框。3.3 购物车与结算用 DataTable 当临时购物车减少数据库往返收银台界面右侧通常是一个 DataGridView显示当前这张小票的商品。如果每扫一件商品就往数据库插一条临时记录取消结算时清理起来异常痛苦更常见的做法是在内存里放一张 DataTable结算成功后再一次性写进 SaleOrder 和 SaleDetail。private DataTable cartTable; private void InitCart() { cartTable new DataTable(); cartTable.Columns.Add(ProductId, typeof(int)); cartTable.Columns.Add(Name, typeof(string)); cartTable.Columns.Add(Price, typeof(decimal)); cartTable.Columns.Add(Qty, typeof(int)); cartTable.Columns.Add(SubTotal, typeof(decimal)); dgvCart.DataSource cartTable; } private void AddToCart(DataRow productRow) { int productId Convert.ToInt32(productRow[ProductId]); DataRow[] rows cartTable.Select(ProductId productId); if (rows.Length 0) { rows[0][Qty] Convert.ToInt32(rows[0][Qty]) 1; rows[0][SubTotal] Convert.ToInt32(rows[0][Qty]) * Convert.ToDecimal(rows[0][Price]); } else { DataRow newRow cartTable.NewRow(); newRow[ProductId] productId; newRow[Name] productRow[Name]; newRow[Price] productRow[Price]; newRow[Qty] 1; newRow[SubTotal] productRow[Price]; cartTable.Rows.Add(newRow); } }说明cartTable 的列类型和数据库表对齐ProductId 是 intPrice 是 decimal。查询已有行时用了一个简单的 Select 条件因为 ProductId 来自数据库的自增列不存在注入风险如果你想写得更严谨也可以改用 LINQ 遍历 cartTable.Rows。每次扫码加一件Qty 加 1SubTotal 重算避免买了三罐可乐还要手动改数量。结算按钮的逻辑就是把 cartTable 里的数据循环插入 SaleOrder 和 SaleDetail并执行第 2 章那段事务脚本。实际操作里要注意DataGridView 的列顺序不能擅自改动否则 DataPropertyName 对不上保存时所有值都是 0。绑定 DataTable 后每一列的 DisplayIndex 和 DataPropertyName 都要在属性窗口里确认一遍。4. 把源码和 SQL 文件跑起来Visual Studio 附加数据库与连接字符串的五步操作很多下载“源码sql文件”的人第一步就卡在数据库上。这里给出我调试同类项目时的五步套路建数据库、执行脚本、确认身份验证模式、改连接字符串、启动程序。每一步都不难但顺序错了会多折腾半小时。4.1 先执行 SQL 文件从 SSMS 到 sqlcmd 两种方式拿到 sql 文件后不要直接双击打开先用 SQL Server Management Studio 连接本机数据库服务然后按 CtrlO 打开该文件再点击执行。执行前确认当前库是 master 而不是某个业务库因为脚本开头有 CREATE DATABASE Supermarket。如果你的机器没装 SSMS用命令行也可以sqlcmd -S . -d master -i supermarket.sql其中 -S . 表示本机默认实例-d master 指定初始数据库-i 后面跟 SQL 文件的路径。如果文件里没有 GO 分隔符多段批处理可能只执行第一段所以下载到的文件尽量保持原样不要用记事本重新保存防止编码被改成不带 BOM 的 UTF-8。执行后打开 SSMS 左侧数据库列表能看到 Supermarket 就算成功。还有一种情况下载包里的文件不是 .sql而是 .mdf 和 .ldf。这时不需要执行脚本直接把 MDF 文件附加到 SQL Server 实例即可右键“数据库”→“附加”选择主数据库文件。MDF 文件的版本要和你的 SQL Server 匹配旧版本附加到新版本一般没问题反过来会报错“版本不支持”。4.2 连接字符串server.;databaseSupermarket;uidsa;pwd... 的坑源码里连接字符串通常写在 App.config 或某个 DbHelper 类的顶部。最常见的写法是connectionStrings add nameSupermarketConn connectionStringserver.;databaseSupermarket;uidsa;pwd123456; providerNameSystem.Data.SqlClient / /connectionStringsC# 端读取方式using System.Configuration; private static readonly string connStr ConfigurationManager.ConnectionStrings[SupermarketConn].ConnectionString;这段配置有四个容易踩的坑。第一server 后面的 “.” 代表本机默认实例如果你的 SQL Server 是命名实例要改成“服务器名\实例名”。第二uid/pwd 要求 SQL Server 身份验证必须开启否则即使密码正确也会报“用户登录失败”需要在 SSMS 里把服务器属性改成“SQL Server 和 Windows 身份验证模式”并重启 SQL 服务。第三如果本机用 Windows 验证能连换到别的电脑就失败多半是对方没建同名的登录名和密码。第四XML 里的 符号要写 有些源码从网页复制下来这里已经乱了Visual Studio 会报属性错误。如果你只想在本机快速跑通也可以把连接字符串改成string connStr server.;databaseSupermarket;integrated securitytrue;;这种 Windows 验证方式不需要用户名密码但发布到别的收银机上得重新配置所以课程设计演示用没问题真正部署还是用 SQL 身份验证更省心。4.3 源码结构Program.cs、登录窗体、主窗体的调用关系一个标准 WinForm 收银系统的入口在 Program.cs 里代码通常是这样[STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new LoginForm()); }[STAThread] 必须保留不然剪贴板、OpenFileDialog 这些组件会出诡异问题。Application.Run(new LoginForm()) 让登录窗体成为主线程的消息循环登录成功后由登录窗体自己跳转到 MainForm。如果你改成直接 Run MainForm登录校验就没了。常见源码包里除了这两个窗体还会有 ProductForm 商品管理、MemberForm 会员管理、ReportForm 报表。关心调用关系时直接看各窗体构造函数里传了什么参数。比如 MainForm 的构造函数接受一个用户名就是为了在界面上显示当前操作员同时把权限信息传给后续窗体public MainForm(string operatorName) { InitializeComponent(); lblOperator.Text $操作员{operatorName}; }这样你就能顺着“入口 → 登录 → 主窗体”这条线把整个项目读通了。如果你下载的源码没有 App.config而是把连接字符串写死在每个窗体里建议统一收敛到一个静态类否则换数据库时要改十几个文件。这个静态类可以放在 Common 文件夹里属性名就叫 ConnStr所有窗体共用一处。5. 收银系统避坑连接超时、金额精度、扫码枪回车和中文乱码这章记录我在调试超市收银项目时真正遇到过的几个问题每一条都是“现象—原因—解决”。新手上路时对照着查能省下大量时间。5.1 现象本机能连数据库换到收银机就报“用户 sa 登录失败”原因目标 SQL Server 实例没有开启“SQL Server 和 Windows 身份验证模式”或者没有创建 sa 登录名再或者连接字符串里的密码和实际不一致。这三个原因按顺序排查。解决在 SSMS 里用 Windows 身份登录服务器属性 → 安全性 → 勾选“SQL Server 和 Windows 身份验证模式”然后在安全性 → 登录名里找到 sa右键设置密码并把状态里的“启用”改为 True。最后右键服务器选择重启服务。连接字符串里密码要写在 pwd 后面不要带引号也不要有多余空格。这里还有个典型衍生坑收银机防火墙没放行 1433 端口。SQL Server 默认 TCP 端口是 1433客户机连不上时先 telnet 收银机 IP 1433 试一下不通就去防火墙入站规则里新增端口。另外如果源码里连接字符串写的是server.\SQLEXPRESS而目标机器装的是默认实例也会报“找不到服务器”把实例名去掉只留一个点即可。5.2 现象买三件单价 9.99 的商品总计显示 29.969999原因源码里 Price 用的是 float 或 double。十进制小数在二进制里表示不精确多次累加后尾部就会出现 0.99999 这种残差。这是新手最容易忽略的地方因为数据库里看到的是 9.99可内存里已经变了。解决数据库字段改成 DECIMAL(10,2)DataTable 的列类型改成 decimalC# 里所有金额变量声明为 decimal。如果已经有 float 数据用 CONVERT 函数过渡UPDATE Product SET Price CONVERT(DECIMAL(10,2), Price); ALTER TABLE Product ALTER COLUMN Price DECIMAL(10,2);改完代码后还要检查 DataGridView 的 DefaultCellStyle Format 设为 N2不然界面上仍可能显示一堆小数。这里有个习惯任何涉及金额的合计都先 Convert.ToDecimal 再算不要直接拿 DataGridView 的 Value 对象做加法否则可能因为空值或类型不同直接抛异常。5.3 现象扫码枪扫一件商品购物车里总是出现两条原因扫码枪默认在条码后发送一个回车键。如果 TextBox 的 KeyDown 处理了回车同时窗体 AcceptButton 也指向“添加商品”按钮那么回车会先触发 KeyDown 的 SearchProduct再触发按钮的 Click同一件商品就被加两次。解决在 KeyDown 里加上 e.SuppressKeyPress true 截断回车消息并保证扫码输入框的 AcceptButton 设置为空而不是添加按钮。我还习惯加一个防抖标记在 500 毫秒内同一个条码不重复添加private DateTime lastScanTime DateTime.MinValue; private string lastBarcode ; if (barcode lastBarcode DateTime.Now.Subtract(lastScanTime).TotalMilliseconds 500) return; lastBarcode barcode; lastScanTime DateTime.Now;这条能直接解决便宜扫码枪的物理抖动和重复触发。还有一个容易忘的点扫码时如果输入法停留在中文状态扫码枪输入的数字会被组词甚至变成汉字。解决方法是把 txtBarcode 的 ImeMode 属性设为 Disable同时不要在窗体的 KeyPreview 里重复处理回车否则事件会触发两次。5.4 现象DataGridView 绑定 List 后0 和 1 显示成数字而不是 CheckBox原因WinForm 的 DataGridView 绑定 List 时只会根据属性类型自动创建文本列bool 属性才能变成默认复选框int 的 0/1 不会自动映射。这是很多人在搜索“winform datagridview 将 list 的一列0和1显示为checkbox”的根源。解决手动添加 DataGridViewCheckBoxColumn并把列的 DataPropertyName 指向布尔属性。如果你的实体类里是 int可以先加只读属性public bool IsActiveBool { get { return IsActive 1; } }然后在设计器或代码里设置DataGridViewCheckBoxColumn col new DataGridViewCheckBoxColumn(); col.HeaderText 启用; col.DataPropertyName IsActiveBool; dgvUser.Columns.Add(col);保存时再把这个布尔值转回 int。这里有个细节如果实体类同时暴露了 IsActive 和 IsActiveBool 两个属性DataGridView 可能会自动把 IsActiveBool 也生成一列导致重复。可以在自动生成列的事件里手动移除原始列或者把 IsActive 标为 [Browsable(false)]只让布尔属性参与绑定。5.5 现象SQL 文件用 SSMS 打开执行后中文商品名全部变成问号原因文件本身是 UTF-8 编码而 SSMS 在部分系统区域设置下按 ANSIGBK 或 CP1252读取导致中文字符乱掉。这也常见于从网页直接复制出来的 SQL 文件因为网页保存时默认 UTF-8。解决用记事本打开该文件另存为时选择“UTF-8 with BOM”编码再让 SSMS 重新执行。BOM 会让 SSMS 正确识别 UTF-8。命令行方式可以用sqlcmd -S . -d master -i supermarket.sql -f 65001这里 -f 65001 强制以 UTF-8 代码页读取输入文件。如果文件是 GBK 保存的把 65001 换成 936 即可。这条命令在批量导入数据库时尤其管用别忽略。另外插入中文时最好在字符串前面加 N比如N可乐这样即使数据库默认排序规则不是中文相关也能正常存下来。6. 给这套收银系统做三个加分项日结报表、Excel 批量导入和界面美化源码跑通只是第一步想交作业或者面试展示建议再花半天加三个小功能。第一个是日结报表不用额外装报表控件直接用 DataGridView 汇总。第二个是商品批量导入把 Excel 表格塞进数据库省去一条条录入。第三个是界面美化三个参数让窗体看起来不像 2008 年的教程截图。日结报表的偷懒做法是绑定一个存储过程返回当天每个商品的销售数量、销售额然后用一个 DataTable 求和decimal total 0; foreach (DataGridViewRow row in dgvReport.Rows) { total Convert.ToDecimal(row.Cells[SubTotal].Value); } lblTotal.Text 日销售额 total.ToString(N2);这只是展示层的汇总真正的日结数据还是要靠 SQL 聚合否则跨天时报表会算重。SQL 端可以用WHERE PayTime DATEADD(day, DATEDIFF(day, 0, GETDATE()), 0)过滤当天零点再按商品分组。Excel 批量导入我一般用 ExcelDataReader先读到一个 DataTable再循环写入 Product 表。导入前记得做两条校验条码为空直接跳过重复条码用 UPDATE 而不是 INSERT否则导了一半报主键冲突连后悔药都没得吃。文件路径别写死用 OpenFileDialog 让用户自己选并加上后缀名过滤.xlsx|.xls。界面美化的低成本组合是背景色用浅灰蓝、字体换成微软雅黑、开启 DPI 缩放this.BackColor Color.FromArgb(245, 245, 250); this.Font new Font(微软雅黑, 10F); this.AutoScaleMode AutoScaleMode.Dpi;加上这三点整个项目的观感会立刻接近真实商业软件。最后提醒一句数据库的 SQL 文件一定要保留一份可重复执行的版本不要只依赖附加了数据的 MDF。我做这类项目习惯每次改表都另存一个升级脚本留着后悔药遇到翻车时能回滚。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网