C#入门学习周记3

发布时间:2026/11/2 6:35:42
C#入门学习周记3

本周做项目,智能仓库管理系统(Smart Warehouse Management System, SWMS)

项目搭建

本项目是使用三层架构搭建项目。

三层架构简单说就是把程序分成 UI(界面)/ BLL(业务逻 辑)/ DAL(数据访问) 三层,通常再加一个 Model(实体) 作为各层之间传递数据的载体。

项目结构示例

三层架构的优缺点

1. 职责分离清晰 (可选:工具类、常量、配置)

UI 层:只负责界面显示、用户输入、事件响应。

BLL 层:负责业务规则、校验、计算、事务。

DAL 层:只负责和数据库打交道。

2. 可维护性高

业务逻辑集中在 BLL,数据访问集中在 DAL。

出问题时定位快:界面问题找 UI,规则问题找 BLL,数据库问题找 DAL。

3. 可替换性强

换数据库(SQL Server → MySQL):只改 DAL。

换界面( WinForm → WPF / Web):BLL 和 DAL 可复用。

换 ORM(ADO.NET → EF / Dapper):也只影响 DAL。

4. 便于团队分工

前端/界面人员写 UI

业务人员写 BLL

数据库人员写 DAL

5. 可测试性更好

BLL 不依赖界面,可以单独做单元测试。

如果 DAL 再抽象成接口,还能用 Mock 替换数据库。

6. 适合中大型项目

项目越大、逻辑越复杂,三层架构的优势越明显,能避免“窗体里写 SQL”的混乱。

二、缺点

1. 开发成本变高

要多写 Model、BLL、DAL 多套代码。

简单功能也要层层传递,代码量比“窗体直接连数据库”大很多。

2. 小型项目显得臃肿 如果只是个小工具、几个窗体、几张表,三层架构会显得过度设计,开发反而更慢。

3. 性能上多一层调用 UI → BLL → DAL → 数据库,比 UI 直接访问数据库多一层。

不过对大多数 WinForm 项目来说,这点开销可以忽略。

项目创建

第一步: 创建一个SWMS的解决方案, 项目名为UI , 项目模版: Windows窗体应用(.Net Framework)

第二步: 在SWMS的解决方案,右键创建项目, 模型选择:类库(.Net Framework)

依次创建DAL, Models,Common

第三步: 为相同添加其他项目的依赖:

UI 依赖 BLL 和 Models和Common

BLL 依赖 DAL 和 Models

DAL 依赖 Models

引入第三方UI库

WinForm 虽然是一个成熟的桌面开发框架,但原生控件样式较为传统。以下是几种流行的第三方 UI 库,可以显著提升 WinForm 应用程序的外观和用户体验:

通过 Nuget 包管理器搜索 SunnyUI 安装

业务功能开发

使用 SunnyUI 来搭建一个无边框的登录界面,UI效果如下:

创建一个窗体,名为 FrmLogin

UI相关的设计不在这儿写了,主要解释逻辑部分

private void uiButton1_Click(object sender, EventArgs e) { //测试一下 //MessageBox.Show("测试一下"); //弹一个对话框 //步骤: //1.获取用户名,密码 对应的UserBLL类的代码: string username = tbUsername.Text; string password = tbPwd.Text; //2.进行非空校验 if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) { UIMessageBox.Show("用户名,密码不能为空!"); return; } //3.调用业务类的登录方法进行登录 bool rs = userBLL.Login(username,password); //4.如果登录成功,跳转到主页窗体 if (rs) { //UIMessageBox.ShowAsk("登录成功!"); //设置窗体的返回结果为OK this.DialogResult = DialogResult.OK; //把当前登录的用户名保存到用户上下文UserContext中 UserContext.LoginName = username; //把登录窗体关闭 Close(); return; } //5.如果登录失败,弹出错误提示,并把文本框清空 UIMessageBox.Show("用户名或者密码错误"); tbUsername.Text = ""; tbPwd.Text = ""; } }

对应的UserBLL类的代码

/// <summary> 注意: 上面代码没有去连接数据库, 我们先把业务跑通, 后期我们可以把数据库和ADO引入 为了实现把登录用户名显示到首页窗体,我们使用了UserContext保存用户信息 /// 用户业务类 /// </summary> public class UserBLL { private UserDAL userDAL = new UserDAL(); /// <summary> /// 登录 /// </summary> /// <param name="username">用户名</param> /// <param name="password">密码</param> /// <returns>true: 登录成功, false: 登录失败</returns> public bool Login(string username, string password) { ////1.调用UserDAL类方法,根据用户名到数据库查询记录 //UserModel userModel = userDAL.GetUserByUsername(username); ////2.判断是否存在,如果存在,判断密码是否一样 //if (userModel != null) //{ // //3.如果密码一样, 登录成功 // if (userModel.Password == password) // { // return true; //登录成功 // } //} if (username == password) { return true; } // 否则登录失败 return false; } }

3.2主页窗体的实现

主窗体(FrmMain)为无边框 + 科技/工业风设计,整体用 Dock 布局:上(标题栏 topPanel)→ 中(内容区 mainPanel)→ 下(状态与导航栏 bottomPanel)。标题栏有日期/时间/最小化/最大化/关闭;底部导航栏用 UITableLayoutPanel(8列)放 6 个 UISymbolButton 导航按钮。

【例子】主页窗体效果图+ UI设计(FrmMain.Designer.cs起始)

FrmMain.cs后台逻辑

OnLoad 里:显示登录名/日期/时间、启动定时器、记录窗体正常大小位置、给所有导航按钮统一绑定点击事件,并默认跳转环境监控页。导航用开关 NavBtn_Click 根据 Tag 跳转对应窗体。

FrmMain.cs——OnLoad + NavBtn_Click导航

// ===== FrmMain.cs 核心 ===== private void FrmMain_Load(object sender, EventArgs e) { ShowLoginName(); ShowNowDate(); ShowTime(); timer1.Start(); _normalBounds = this.Bounds; // 记录正常大小位置 // 给底部所有导航按钮统一绑定事件 foreach (var c in buttonLayoutPanel.Controls) if (c is UISymbolButton btn) btn.Click += NavBtn_Click; NavigateUtil.NavigateTo<FrmEnvMonitor>(mainPanel); // 默认显示环境监控 uiSymbolButton1.FillColor = Color.SkyBlue; } // 导航按钮:先全部还原背景色,再高亮点击的按钮,按 Tag 跳转 private void NavBtn_Click(object sender, EventArgs e) { foreach (var c in buttonLayoutPanel.Controls) if (c is UISymbolButton b1) b1.FillColor = Color.FromArgb(49, 96, 133); UISymbolButton btn = sender as UISymbolButton; btn.FillColor = Color.SkyBlue; string tag = btn.Tag?.ToString(); switch (tag) { case "envMonitor": NavigateUtil.NavigateTo<FrmEnvMonitor>(mainPanel); break; case "inbound": NavigateUtil.NavigateTo<FrmInbound>(mainPanel); break; case "outbound": NavigateUtil.NavigateTo<FrmOutbound>(mainPanel); break; case "robotControl":NavigateUtil.NavigateTo<FrmRobotControl>(mainPanel);break; case "inventory": NavigateUtil.NavigateTo<FrmInventory>(mainPanel); break; case "report": NavigateUtil.NavigateTo<FrmReport>(mainPanel); break; } }

时间显示用定时器每秒更新;最大化/还原用 _normalBounds 记忆原大小;关闭前用 UIMessageBox.ShowAsk 确认。

导航工具类NavigateUtil(核心)

把独立的 UIForm 窗体“伪装”成普通控件嵌入到主窗体 UIPanel,实现多页面切换并保持页面状态。关键:childForm.TopLevel = false,使窗体可作为控件添加进容器。

NavigateUtil.cs完整代码+ TopLevel=false原理解释

// ===== UI.Utils/NavigateUtil.cs ===== public static void NavigateTo<F>(UIPanel panel) where F : UIForm, new() { panel.Visible = true; panel.BringToFront(); bool isExist = false; foreach (Control c in panel.Controls) { Form form = c as Form; if (form is F) { form.Show(); isExist = true; break; } else form.Hide(); // 其它窗体隐藏,只显示一个 } if (!isExist) { F childForm = new F(); childForm.TopLevel = false; // 核心:变为非顶级窗体(可作控件) childForm.ShowTitle = false; // 去掉边框 childForm.Dock = DockStyle.Fill; childForm.WindowState = FormWindowState.Maximized; panel.Controls.Add(childForm); childForm.Show(); } }

3.3环境监控业务实现

3.3.1 UI布局设计

布局用 Panel + Dock:顶部标题栏(pnlTitle,高45,深色#2B579A)、上方数据卡片区(pnlTop,高130,4个圆角卡片)、中间图表区(pnlMiddle:左侧 pnlChart 折线图 + 右侧 pnlDevice 设备列表)、底部报警表格区(pnlBottom,高220)。

3.3.2后台逻辑

OnLoad 中拆分为 5 个独立 Init 方法(单一职责):InitTopCards(顶部卡片,代码动态创建,for 循环 + 字典缓存,便于扩展成更多卡片)、InitChart(双 Y 轴曲线:左轴温度红色、右Y2轴湿度蓝色)、InitDeviceList(设备状态列表 + UILight 指示灯)、InitAlarmGrid(报警表格 + 状态高亮)、InitTimer(1 秒定时模拟数据)。

后台逻辑——窗体基础设置+生命周期+顶部卡片动态创建

// ===== FrmEnvMonitor.cs 核心 ===== protected override void OnLoad(EventArgs e) { base.OnLoad(e); InitTopCards(); InitChart(); InitDeviceList(); InitAlarmGrid(); InitTimer(); AddAlarmRecord("温度", "32.5°C", "未确认"); // 示例报警 }

双Y轴曲线配置+设备状态指示灯逻辑

// 双 Y 轴折线(温度/湿度) private void InitChart() { _lineOption = new UILineOption { Title = { Text = "温湿度实时曲线" }, XAxisType = UIAxisType.DateTime, XAxis = { Name = "时间" }, YAxis = { Name = "温度(℃)" }, Y2Axis = { Name = "湿度(%)" } }; _tempSeries = new UILineSeries("温度", Color.FromArgb(231,76,60)) { Smooth=true, Width=2 }; _humiditySeries = new UILineSeries("湿度", Color.FromArgb(52,152,219), true) { Smooth=true, Width=2 }; _tempSeries.SetMaxCount(100); _humiditySeries.SetMaxCount(100); // 防内存溢出 _lineOption.AddSeries(_tempSeries); _lineOption.AddSeries(_humiditySeries); _lineChart.SetOption(_lineOption); }

【报警表格数据绑定+ CellFormatting高亮+定时刷新引擎

// 定时刷新(模拟真实工业数据流) private void RefreshTimer_Tick(object sender, EventArgs e) { double temperature = 22 + _random.NextDouble() * 8; double humidity = 45 + _random.NextDouble() * 20; int airQuality = _random.Next(30, 80); UpdateCardValue("温度", temperature.ToString("F1") + " °C"); UpdateCardValue("湿度", humidity.ToString("F0") + " %"); UpdateCardValue("空气质量", airQuality + " AQI"); AddChartPoint(DateTime.Now, temperature, humidity); UpdateDeviceStatus("温湿度传感器", true); // 约 5% 概率触发报警 if (_random.Next(0, 100) < 5) if (temperature > 28.5) AddAlarmRecord("温度过高", temperature.ToString("F1")+"°C", "未确认"); else if (humidity > 62) AddAlarmRecord("湿度过高", humidity.ToString("F0")+"%", "未确认"); UpdateAlarmCardDisplay(); } protected override void OnFormClosing(FormClosingEventArgs e) { _refreshTimer?.Stop(); _refreshTimer?.Dispose(); base.OnFormClosing(e); }

3.4入库管理

3.4.1 UI布局设计

入库管理窗体同样用 Panel + Dock:顶部标题栏(pnlTitle)、查询过滤区(pnlFilter,高100:单号、日期范围、状态、查询/重置/新增按钮)、底部数据表格区(pnlGrid)。

3.4.2后台逻辑

OnLoad 拆四步:InitGrid(手动 AddColumn 建列 + 操作按钮列)、InitDatePickers(日期默认值)、LoadMockData(模拟数据)、RefreshGrid(绑定 DataSource)。数据过滤用 LINQ Where 实现“单号模糊 + 状态精确 + 日期范围”三重条件。

// ===== FrmInbound.cs 核心 ===== private UIDataGridView _grid; private List<InboundOrder> _allOrders = new List<InboundOrder>(); private List<InboundOrder> _displayOrders = new List<InboundOrder>(); protected override void OnLoad(EventArgs e) { base.OnLoad(e); InitGrid(); InitDatePickers(); LoadMockData(); RefreshGrid(); } private void InitGrid() { _grid = new UIDataGridView { Location=new Point(15,15), Anchor=AnchorStyles.Top|AnchorStyles.Left|AnchorStyles.Right|AnchorStyles.Bottom, AllowUserToAddRows=false, ReadOnly=true, AutoSizeColumnsMode=DataGridViewAutoSizeColumnsMode.Fill, SelectionMode=DataGridViewSelectionMode.FullRowSelect }; _grid.Init(); _grid.AddColumn("入库单号","OrderNo"); _grid.AddColumn("物料名称","MaterialName"); _grid.AddColumn("数量","Quantity"); _grid.AddColumn("供应商","Supplier"); _grid.AddColumn("状态","Status"); _grid.AddColumn("创建时间","CreateTime"); // 操作按钮列(点“详情”弹窗) DataGridViewButtonColumn btnCol = new DataGridViewButtonColumn { HeaderText="操作", Name="Operation", Text="详情", UseColumnTextForButtonValue=true, FlatStyle=FlatStyle.Flat }; _grid.Columns.Add(btnCol); _grid.CellFormatting += Grid_CellFormatting; // 状态颜色 _grid.CellContentClick += Grid_CellContentClick; // 按钮点击 pnlGrid.Controls.Add(_grid); } // 三重过滤:单号模糊 + 状态精确 + 日期范围 private void ApplyFilter() { string orderNo = txtOrderNo.Text.Trim(); string status = cboStatus.Text; DateTime start = dtpStart.Value.Date, end = dtpEnd.Value.Date; if (start > end) { UIMessageBox.ShowWarning("开始日期不能晚于结束日期!"); return; } _displayOrders = _allOrders.Where(o => { bool matchNo = string.IsNullOrEmpty(orderNo) || o.OrderNo.Contains(orderNo); bool matchStatus = status == "全部" || o.Status == status; if (DateTime.TryParse(o.CreateTime, out DateTime createTime)) { bool matchDate = createTime.Date >= start && createTime.Date <= end; return matchNo && matchStatus && matchDate; } return false; }).ToList(); _grid.DataSource = null; _grid.DataSource = _displayOrders; } // 行内“详情”按钮 private void Grid_CellContentClick(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0) return; if (_grid.Columns[e.ColumnIndex].Name == "Operation") { InboundOrder order = _displayOrders[e.RowIndex]; OpenDetailDialog(order); } }

模型类 InboundOrder:OrderNo 入库单号、MaterialName 物料名称、Quantity 数量、Supplier 供应商、Status 状态、CreateTime 创建时间、LocationCode 库位编码。

3.5入库单操作窗体设计

3.5.1 UI设计

入库单操作窗体 FrmInboundDetail 比较简单,直接使用绝对定位(非 Dock)。包含:标题、物料名称、数量(UIIntegerUpDown)、供应商、库位分配(下拉框)、流程步骤区(pnlSteps:①确认验货 ②执行上架 ③完成入库)、以及保存/取消按钮。窗体 MaximizeBox=false、MinimizeBox=false、StartPosition=CenterParent。

3.5.2后台逻辑实现

双构造函数模式:无参构造(新建 Create)实例化全新 InboundOrder 并自动生成流水号;有参构造(编辑 Update)接收外部订单,赋值给 _currentOrder 并回填界面。

// ===== FrmInboundDetail.cs 双构造函数 ===== public InboundOrder ResultOrder { get; private set; } private InboundOrder _currentOrder; private bool _isNew; // 新建 public FrmInboundDetail() { InitializeComponent(); this.Style = UIStyle.Blue; _isNew = true; ResultOrder = new InboundOrder(); InitLocationCombo(); GenerateOrderNo(); } // 修改 public FrmInboundDetail(InboundOrder order) { InitializeComponent(); this.Style = UIStyle.Blue; _isNew = false; _currentOrder = order; ResultOrder = new InboundOrder { OrderNo=order.OrderNo, MaterialName=order.MaterialName, Quantity=order.Quantity, Supplier=order.Supplier, Status=order.Status, CreateTime=order.CreateTime, LocationCode=order.LocationCode }; InitLocationCombo(); LoadOrderData(); }

业务状态机与按钮联动:企业级 WMS 中订单不能随意跳转。UpdateStepButtons() 按四种状态动态控制步骤文字颜色(进行中橙色/完成绿色/未开始灰色)与按钮启用(当前步骤激活、其余禁用)。流转:待验货 →(只能验货)→ 已验货 →(只能上架)→ 已上架 →(只能完成)→ 已完成(全部禁用)。

// ===== 状态机 UpdateStepButtons ===== private void UpdateStepButtons() { string status = ResultOrder.Status; lblStep1.ForeColor = lblStep2.ForeColor = lblStep3.ForeColor = Color.Gray; switch (status) { case "待验货": // 橙色高亮第1步,激活验货按钮 lblStep1.ForeColor = Color.FromArgb(255,140,66); btnVerify.Enabled = true; btnShelve.Enabled = false; btnComplete.Enabled = false; break; case "已验货": lblStep1.ForeColor = Color.FromArgb(39,174,96); lblStep2.ForeColor = Color.FromArgb(255,140,66); btnVerify.Enabled = false; btnShelve.Enabled = true; btnComplete.Enabled = false; break; case "已上架": lblStep1.ForeColor = lblStep2.ForeColor = Color.FromArgb(39,174,96); lblStep3.ForeColor = Color.FromArgb(255,140,66); btnVerify.Enabled = false; btnShelve.Enabled = false; btnComplete.Enabled = true; break; case "已完成": lblStep1.ForeColor = lblStep2.ForeColor = lblStep3.ForeColor = Color.FromArgb(39,174,96); btnVerify.Enabled = false; btnShelve.Enabled = false; btnComplete.Enabled = false; break; } }

核心业务流转(验货→上架→完成)

// 验货 private void btnVerify_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtMaterial.Text) || string.IsNullOrWhiteSpace(txtSupplier.Text)) { UIMessageBox.ShowWarning("请填写完整物料信息"); return; } ResultOrder.MaterialName = txtMaterial.Text.Trim(); ResultOrder.Quantity = numQuantity.Value; ResultOrder.Supplier = txtSupplier.Text.Trim(); ResultOrder.Status = "已验货"; UpdateStepButtons(); UIMessageBox.Show("验货完成,可进行上架操作!", "提示", UIStyle.Green); } // 上架(需先选库位) private void btnShelve_Click(object sender, EventArgs e) { if (cboLocation.SelectedItem == null) { UIMessageBox.ShowWarning("请选择库位"); return; } ResultOrder.LocationCode = cboLocation.SelectedValue.ToString(); ResultOrder.Status = "已上架"; UpdateStepButtons(); UIMessageBox.Show("执行上架完成!", "提示", UIStyle.Green); } // 完成(生命周期结束,全部锁定) private void btnComplete_Click(object sender, EventArgs e) { ResultOrder.Status = "已完成"; UpdateStepButtons(); UIMessageBox.Show("入库已完成!", "提示", UIStyle.Green); }

窗体关闭生命周期与拦截

用户有 4 种关闭姿势:点右上角 ×、点“取消”按钮、点“保存”按钮、按 Alt+F4。为防止误点 × 丢失数据,重写 OnFormClosing:当是用户关闭且未设为 OK 时,弹窗询问是否保存。

// 覆盖关闭行为 protected override void OnFormClosing(FormClosingEventArgs e) { if (e.CloseReason == CloseReason.UserClosing && this.DialogResult != DialogResult.OK) { if (UIMessageBox.ShowAsk("是否保存当前数据?")) btnSave_Click(null, null); // 模拟点击保存 else this.DialogResult = DialogResult.Cancel; } base.OnFormClosing(e); } // 保存 private void btnSave_Click(object sender, EventArgs e) { if (_isNew) { if (string.IsNullOrWhiteSpace(txtMaterial.Text)) { UIMessageBox.ShowWarning("请输入物料名称"); return; } ResultOrder.MaterialName = txtMaterial.Text.Trim(); ResultOrder.Quantity = numQuantity.Value; ResultOrder.Supplier = txtSupplier.Text.Trim(); if (cboLocation.SelectedItem != null) ResultOrder.LocationCode = cboLocation.SelectedValue.ToString(); } this.DialogResult = DialogResult.OK; // 通知主窗体保存成功 this.Close(); }

辅助方法(InitLocationCombo / GenerateOrderNo / LoadOrderData)

// 初始化库位下拉框(模拟库位数据,Code 与 Display 分离) private void InitLocationCombo() { var locations = new List<LocationModel>{ new LocationModel{ Code="A-01-01", Display="A区-01架-01层-01位" }, new LocationModel{ Code="A-01-02", Display="A区-01架-01层-02位" }, new LocationModel{ Code="B-02-03-01", Display="B区-02架-03层-01位" }, new LocationModel{ Code="C-03-01-04", Display="C区-03架-01层-04位" } }; cboLocation.DataSource = locations; cboLocation.DisplayMember = "Display"; cboLocation.ValueMember = "Code"; } // 自动生成流水号与创建时间 private void GenerateOrderNo() { ResultOrder.OrderNo = "IN" + DateTime.Now.ToString("yyyyMMddHHmmss"); ResultOrder.CreateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); ResultOrder.Status = "待验货"; } // 编辑时回填界面并锁定输入 private void LoadOrderData() { txtMaterial.Text = _currentOrder.MaterialName; numQuantity.Value = _currentOrder.Quantity; txtSupplier.Text = _currentOrder.Supplier; txtMaterial.Enabled = false; numQuantity.Enabled = false; txtSupplier.Enabled = false; if (!string.IsNullOrEmpty(_currentOrder.LocationCode)) cboLocation.SelectedValue = _currentOrder.LocationCode; UpdateStepButtons();