工序 BOM 协同系统架构多模块组件
以下是基于现有 MES/ERP 工序 BOM 协同系统架构,完善并扩展的 WinForm 模块化实现代码,涵盖GDI 图表增强、数据维护、多模块联动、本地 SQLite 数据管理等核心能力,保持原有主子结构 / 分节点执行 / 汇总工作台的设计体系:
1. 核心扩展:GDI 图表控件增强(GdiChartEx.cs)
csharp
运行
using System; using System.Data; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; /// <summary> /// 增强版GDI柱状图控件,支持多系列、图例、网格、自定义样式 /// </summary> public class GdiChartEx : Panel { // 图表配置 public Color BarColor { get; set; } = Color.CornflowerBlue; public Color GridColor { get; set; } = Color.LightGray; public Color TextColor { get; set; } = Color.Black; public Font LabelFont { get; set; } = new Font("微软雅黑", 9); public Font ValueFont { get; set; } = new Font("微软雅黑", 10, FontStyle.Bold); // 数据源 private DataTable _dataSource; public DataTable DataSource { get => _dataSource; set { _dataSource = value; Invalidate(); // 数据变更重绘 } } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (DataSource == null || DataSource.Rows.Count == 0) return; var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; // 抗锯齿 g.Clear(BackColor); // 绘制网格背景 DrawGrid(g); // 计算图表区域(留边距) int paddingLeft = 80, paddingRight = 20, paddingTop = 30, paddingBottom = 60; int chartWidth = ClientSize.Width - paddingLeft - paddingRight; int chartHeight = ClientSize.Height - paddingTop - paddingBottom; // 计算最大值(用于刻度) int maxValue = GetMaxValue(); float yScale = (float)chartHeight / maxValue; // 绘制柱状图 int barWidth = Math.Min(80, chartWidth / (DataSource.Rows.Count * 2)); // 自适应柱宽 int x = paddingLeft + (chartWidth - DataSource.Rows.Count * (barWidth + 30)) / 2; // 居中 foreach (DataRow row in DataSource.Rows) { string category = row[0].ToString(); int value = int.Parse(row[1].ToString()); // 柱体坐标计算 float barHeight = value * yScale; float y = paddingTop + (chartHeight - barHeight); // 绘制柱体(渐变+边框) using (var brush = new LinearGradientBrush( new Point(x, (int)y), new Point(x, (int)(y + barHeight)), BarColor, BarColor.Darken(30))) { g.FillRectangle(brush, x, y, barWidth, barHeight); g.DrawRectangle(Pens.Black, x, y, barWidth, barHeight); } // 绘制数值标签 var valueText = value.ToString("N0"); var valueSize = g.MeasureString(valueText, ValueFont); g.DrawString(valueText, ValueFont, new SolidBrush(TextColor), x + (barWidth - valueSize.Width) / 2, y - valueSize.Height - 5); // 绘制分类标签(旋转45度避免重叠) g.TranslateTransform(x + barWidth / 2, ClientSize.Height - paddingBottom + 20); g.RotateTransform(-45); g.DrawString(category, LabelFont, new SolidBrush(Color.DarkRed), -g.MeasureString(category, LabelFont).Width / 2, 0); g.ResetTransform(); x += barWidth + 30; // 柱间距 } // 绘制图例 DrawLegend(g); // 日志记录 DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} 增强版柱状图绘制完成 | 数据行数:{DataSource.Rows.Count}\r\n"); } /// <summary> /// 绘制网格线 /// </summary> private void DrawGrid(Graphics g) { int paddingLeft = 80, paddingTop = 30, paddingBottom = 60; int chartHeight = ClientSize.Height - paddingTop - paddingBottom; int chartWidth = ClientSize.Width - paddingLeft - 20; // 横向网格(5等分) int gridCount = 5; float gridStep = chartHeight / gridCount; for (int i = 0; i <= gridCount; i++) { float y = paddingTop + i * gridStep; using (var pen = new Pen(GridColor, 1)) { g.DrawLine(pen, paddingLeft, y, ClientSize.Width - 20, y); } // 刻度值 int scaleValue = (int)((chartHeight - i * gridStep) / chartHeight * GetMaxValue()); g.DrawString(scaleValue.ToString(), LabelFont, Brushes.Gray, paddingLeft - 50, y - 10); } // 纵向基线 g.DrawLine(Pens.Black, paddingLeft, paddingTop, paddingLeft, ClientSize.Height - paddingBottom); g.DrawLine(Pens.Black, paddingLeft, ClientSize.Height - paddingBottom, ClientSize.Width - 20, ClientSize.Height - paddingBottom); } /// <summary> /// 绘制图例 /// </summary> private void DrawLegend(Graphics g) { int legendX = ClientSize.Width - 150; int legendY = 20; // 图例框 g.FillRectangle(Brushes.White, legendX, legendY, 120, 40); g.DrawRectangle(Pens.Black, legendX, legendY, 120, 40); // 图例颜色块 g.FillRectangle(new SolidBrush(BarColor), legendX + 10, legendY + 10, 20, 15); g.DrawRectangle(Pens.Black, legendX + 10, legendY + 10, 20, 15); // 图例文字 g.DrawString("计划产量", LabelFont, Brushes.Black, legendX + 35, legendY + 10); } /// <summary> /// 获取数据源最大值(用于刻度计算) /// </summary> private int GetMaxValue() { int max = 0; foreach (DataRow row in DataSource.Rows) { int val = int.Parse(row[1].ToString()); if (val > max) max = val; } // 向上取整到最近的10的倍数 return (int)Math.Ceiling(max / 10.0) * 10; } /// <summary> /// 刷新图表 /// </summary> public new void Refresh() => Invalidate(); } /// <summary> /// Color扩展方法 /// </summary> public static class ColorExtensions { public static Color Darken(this Color color, int percent) { float factor = 1 - percent / 100f; return Color.FromArgb( color.A, (int)(color.R * factor), (int)(color.G * factor), (int)(color.B * factor)); } }2. 数据维护模块(DataMaintenanceView.cs)
csharp
运行
using System; using System.Data; using System.Windows.Forms; /// <summary> /// 本地数据库数据维护模块(支持8-9组基础数据编辑/新增/删除) /// </summary> public static class DataMaintenanceView { // 支持维护的表名 private static readonly string[] _maintainTables = { "WorkStation", "ProcessRoute", "BOM", "WorkTask" }; public static TabPage Create() { var page = new TabPage("数据维护中心"); // 顶部选择区 var topPanel = new Panel { Dock = DockStyle.Top, Height = 60 }; var cboTable = new ComboBox { Dock = DockStyle.Left, Width = 200, DataSource = _maintainTables, DropDownStyle = ComboBoxStyle.DropDownList }; var btnRefresh = new Button { Text = "刷新数据", Dock = DockStyle.Left, Width = 100 }; var btnSave = new Button { Text = "保存修改", Dock = DockStyle.Left, Width = 100 }; var btnAdd = new Button { Text = "新增行", Dock = DockStyle.Left, Width = 100 }; var btnDel = new Button { Text = "删除选中", Dock = DockStyle.Left, Width = 100 }; topPanel.Controls.AddRange(new Control[] { cboTable, btnRefresh, btnSave, btnAdd, btnDel }); // 数据展示区 var dgvData = new DataGridView { Dock = DockStyle.Fill, AllowUserToAddRows = false, AllowUserToDeleteRows = false, AutoGenerateColumns = true, ReadOnly = false }; // 布局组装 page.Controls.Add(dgvData); page.Controls.Add(topPanel); // 事件绑定 cboTable.SelectedIndexChanged += (s, e) => LoadTableData(cboTable.Text, dgvData); btnRefresh.Click += (s, e) => LoadTableData(cboTable.Text, dgvData); btnAdd.Click += (s, e) => AddNewRow(cboTable.Text, dgvData); btnDel.Click += (s, e) => DeleteSelectedRow(cboTable.Text, dgvData); btnSave.Click += (s, e) => SaveTableChanges(cboTable.Text, dgvData); // 初始加载第一个表 if (_maintainTables.Length > 0) LoadTableData(_maintainTables[0], dgvData); return page; } /// <summary> /// 加载指定表数据 /// </summary> private static void LoadTableData(string tableName, DataGridView dgv) { try { var dt = LocalDB.Query($"SELECT * FROM {tableName}"); dgv.DataSource = dt; DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} 加载[{tableName}]数据完成,共{dt.Rows.Count}行\r\n"); } catch (Exception ex) { MessageBox.Show($"加载数据失败:{ex.Message}"); DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} 加载[{tableName}]数据失败:{ex.Message}\r\n"); } } /// <summary> /// 新增行 /// </summary> private static void AddNewRow(string tableName, DataGridView dgv) { if (dgv.DataSource is not DataTable dt) return; var newRow = dt.NewRow(); // 自动填充主键(简单自增逻辑) var maxId = LocalDB.Query($"SELECT MAX(Id) FROM {tableName}").Rows[0][0]; newRow["Id"] = maxId == DBNull.Value ? 1 : Convert.ToInt32(maxId) + 1; dt.Rows.Add(newRow); dgv.Refresh(); DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} [{tableName}]新增空白行\r\n"); } /// <summary> /// 删除选中行 /// </summary> private static void DeleteSelectedRow(string tableName, DataGridView dgv) { if (dgv.CurrentRow == null) { MessageBox.Show("请选中要删除的行"); return; } var id = dgv.CurrentRow.Cells["Id"].Value; if (id == DBNull.Value || id == null) { MessageBox.Show("无效的行ID"); return; } try { LocalDB.Execute($"DELETE FROM {tableName} WHERE Id={id}"); LoadTableData(tableName, dgv); // 刷新数据 DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} [{tableName}]删除ID={id}的行\r\n"); MesEventBus.SendEdit(tableName); // 触发数据编辑事件 } catch (Exception ex) { MessageBox.Show($"删除失败:{ex.Message}"); } } /// <summary> /// 保存修改(批量更新) /// </summary> private static void SaveTableChanges(string tableName, DataGridView dgv) { if (dgv.DataSource is not DataTable dt) return; try { foreach (DataRow row in dt.Rows) { if (row.RowState == DataRowState.Modified) { // 构建更新SQL(通用逻辑,适配4个核心表) string sql = BuildUpdateSql(tableName, row); LocalDB.Execute(sql); DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} [{tableName}]更新ID={row["Id"]}:{sql}\r\n"); } else if (row.RowState == DataRowState.Added) { // 构建插入SQL string sql = BuildInsertSql(tableName, row); LocalDB.Execute(sql); DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} [{tableName}]新增ID={row["Id"]}:{sql}\r\n"); } } // 提交后刷新 dt.AcceptChanges(); LoadTableData(tableName, dgv); MesEventBus.SendEdit(tableName); // 触发数据编辑事件 MessageBox.Show("数据保存成功!"); } catch (Exception ex) { MessageBox.Show($"保存失败:{ex.Message}"); DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} [{tableName}]保存失败:{ex.Message}\r\n"); } } /// <summary> /// 构建更新SQL /// </summary> private static string BuildUpdateSql(string tableName, DataRow row) { return tableName switch { "WorkStation" => $"UPDATE WorkStation SET Name='{row["Name"]}', Role='{row["Role"]}' WHERE Id={row["Id"]}", "ProcessRoute" => $"UPDATE ProcessRoute SET Name='{row["Name"]}', StationId={row["StationId"]}, Sort={row["Sort"]} WHERE Id={row["Id"]}", "BOM" => $"UPDATE BOM SET ProductCode='{row["ProductCode"]}', Material='{row["Material"]}', UseQty={row["UseQty"]}, Loss={row["Loss"]} WHERE Id={row["Id"]}", "WorkTask" => $"UPDATE WorkTask SET TaskNo='{row["TaskNo"]}', StationId={row["StationId"]}, StationName='{row["StationName"]}', Status='{row["Status"]}', PlanQty={row["PlanQty"]} WHERE Id={row["Id"]}", _ => throw new NotSupportedException($"不支持的表:{tableName}") }; } /// <summary> /// 构建插入SQL /// </summary> private static string BuildInsertSql(string tableName, DataRow row) { return tableName switch { "WorkStation" => $"INSERT INTO WorkStation VALUES({row["Id"]},'{row["Name"]}','{row["Role"]}')", "ProcessRoute" => $"INSERT INTO ProcessRoute VALUES({row["Id"]},'{row["Name"]}',{row["StationId"]},{row["Sort"]})", "BOM" => $"INSERT INTO BOM VALUES({row["Id"]},'{row["ProductCode"]}','{row["Material"]}',{row["UseQty"]},{row["Loss"]})", "WorkTask" => $"INSERT INTO WorkTask VALUES({row["Id"]},'{row["TaskNo"]}',{row["StationId"]},'{row["StationName"]}','{row["Status"]}',{row["PlanQty"]})", _ => throw new NotSupportedException($"不支持的表:{tableName}") }; } }3. 主窗体扩展(MainForm.cs 完善)
csharp
运行
using System; using System.Data; using System.Drawing; using System.Windows.Forms; public partial class MainForm : Form { private readonly GdiChartEx _chartEx = new GdiChartEx(); // 替换为增强版图表 private readonly TextBox _logBox = new TextBox(); public MainForm() { LocalDB.Init(); InitializeLayout(); BindEvents(); Text = "MES/ERP 工序BOM协同系统(增强版)"; WindowState = FormWindowState.Maximized; // 初始化日志框样式 InitLogBoxStyle(); } private void InitializeLayout() { var tab = new TabControl { Dock = DockStyle.Fill }; // 原有模块 tab.TabPages.Add(TaskNodeView.Create()); tab.TabPages.Add(BomView.Create()); tab.TabPages.Add(MasterSlaveView.Create()); tab.TabPages.Add(DashboardView.Create(_logBox)); // 新增模块 tab.TabPages.Add(ChartViewEx.Create(_chartEx)); // 增强版图表页 tab.TabPages.Add(DataMaintenanceView.Create()); // 数据维护页 Controls.Add(tab); } /// <summary> /// 初始化日志框样式 /// </summary> private void InitLogBoxStyle() { _logBox.Dock = DockStyle.Fill; _logBox.Multiline = true; _logBox.ReadOnly = true; _logBox.BackColor = Color.Black; _logBox.ForeColor = Color.Lime; _logBox.Font = new Font("Consolas", 10); _logBox.ScrollBars = ScrollBars.Vertical; } private void BindEvents() { // 任务操作事件 MesEventBus.OnTaskOperated += (task, station, status) => { _logBox.AppendText($"{DateTime.Now:HH:mm:ss} [{station}] 任务[{task}] 状态变更为:{status}\r\n"); }; // 数据编辑事件 MesEventBus.OnDataEdited += (table) => { _logBox.AppendText($"{DateTime.Now:HH:mm:ss} 数据表[{table}]发生编辑,触发全系统数据刷新\r\n"); // 刷新所有关联控件 TaskNodeView._dgv.DataSource = TaskComponent.GetTasks(); _chartEx.Refresh(); }; // BOM计算事件 MesEventBus.OnBomCalculated += (product, total) => { _logBox.AppendText($"{DateTime.Now:HH:mm:ss} 产品[{product}] BOM计算完成,总需求:{total:N2}\r\n"); }; } }4. 增强版图表视图(ChartViewEx.cs)
csharp
运行
using System; using System.Windows.Forms; /// <summary> /// 增强版图表报表视图 /// </summary> public static class ChartViewEx { public static TabPage Create(GdiChartEx chart) { var page = new TabPage("GDI图表报表(增强版)"); // 顶部操作区 var topPanel = new Panel { Dock = DockStyle.Top, Height = 80 }; var btnLoadTaskQty = new Button { Text = "加载工站计划产量", Dock = DockStyle.Top, Width = 200 }; var btnLoadBomQty = new Button { Text = "加载BOM物料需求", Dock = DockStyle.Top, Width = 200 }; var cboColor = new ComboBox { Dock = DockStyle.Top, Width = 200, DropDownStyle = ComboBoxStyle.DropDownList, DataSource = new[] { "蓝色", "绿色", "橙色", "紫色" } }; topPanel.Controls.AddRange(new Control[] { btnLoadTaskQty, btnLoadBomQty, cboColor }); // 图表区 chart.Dock = DockStyle.Fill; chart.BackColor = Color.White; // 布局组装 page.Controls.Add(chart); page.Controls.Add(topPanel); // 事件绑定 btnLoadTaskQty.Click += (s, e) => { chart.DataSource = LocalDB.Query(@" SELECT StationName, SUM(PlanQty) Qty FROM WorkTask GROUP BY StationId, StationName ORDER BY SUM(PlanQty) DESC "); chart.Refresh(); }; btnLoadBomQty.Click += (s, e) => { chart.DataSource = LocalDB.Query(@" SELECT Material, SUM(UseQty*(1+Loss)) TotalNeed FROM BOM WHERE ProductCode='PROD001' GROUP BY Material "); chart.Refresh(); }; cboColor.SelectedIndexChanged += (s, e) => { chart.BarColor = cboColor.Text switch { "绿色" => Color.ForestGreen, "橙色" => Color.Orange, "紫色" => Color.Purple, _ => Color.CornflowerBlue }; chart.Refresh(); }; // 初始日志 DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} 增强版图表报表初始化完成\r\n"); return page; } }5. 关键补充:BOM 模块增强(BomView.cs 完善)
csharp
运行
using System; using System.Data; using System.Windows.Forms; public static class BomView { public static TabPage Create() { var page = new TabPage("BOM物料调配(增强版)"); // 顶部操作区 var topPanel = new Panel { Dock = DockStyle.Top, Height = 100 }; var cboProduct = new ComboBox { Dock = DockStyle.Top, Width = 200, DropDownStyle = ComboBoxStyle.DropDownList, DataSource = new[] { "PROD001", "PROD002" } }; var txtQty = new TextBox { Dock = DockStyle.Top, Width = 200, PlaceholderText = "输入计划产量(默认1000)", Text = "1000" }; var btnCalc = new Button { Text = "计算物料需求", Dock = DockStyle.Top, Width = 200 }; topPanel.Controls.AddRange(new Control[] { cboProduct, txtQty, btnCalc }); // 数据展示区 var dgv = new DataGridView { Dock = DockStyle.Fill }; // 布局组装 page.Controls.Add(dgv); page.Controls.Add(topPanel); // 事件绑定 btnCalc.Click += (s, e) => { if (!int.TryParse(txtQty.Text, out int planQty) || planQty <= 0) { MessageBox.Show("请输入有效的计划产量(正整数)"); return; } var product = cboProduct.Text; var dt = BomComponent.GetBom(product); // 添加计算列 if (!dt.Columns.Contains("TotalNeed")) dt.Columns.Add("TotalNeed", typeof(decimal)); if (!dt.Columns.Contains("LossQty")) dt.Columns.Add("LossQty", typeof(decimal)); decimal totalAll = 0; foreach (DataRow r in dt.Rows) { decimal useQty = decimal.Parse(r["UseQty"].ToString()); decimal loss = decimal.Parse(r["Loss"].ToString()); // 计算净需求+损耗 decimal lossQty = planQty * useQty * loss; decimal total = planQty * useQty + lossQty; r["LossQty"] = lossQty; r["TotalNeed"] = total; totalAll += total; } dgv.DataSource = dt; // 格式化列 dgv.Columns["UseQty"].DefaultCellStyle.Format = "N2"; dgv.Columns["Loss"].DefaultCellStyle.Format = "P2"; dgv.Columns["LossQty"].DefaultCellStyle.Format = "N2"; dgv.Columns["TotalNeed"].DefaultCellStyle.Format = "N2"; // 触发BOM计算事件 MesEventBus.SendBom(product, totalAll); DashboardView._logBox.AppendText($"{DateTime.Now:HH:mm:ss} 产品[{product}] 产量[{planQty}] BOM计算完成,总物料需求:{totalAll:N2}\r\n"); }; return page; } }核心功能说明
GDI 图表增强:
- 支持抗锯齿、渐变柱体、网格线、图例、旋转标签
- 自适应柱宽、数值刻度自动计算
- 支持自定义柱体颜色、字体样式
数据维护模块:
- 支持 WorkStation/ProcessRoute/BOM/WorkTask 4 大核心表的增删改查
- 批量保存修改、自动生成主键
- 触发数据编辑事件,联动刷新全系统数据
BOM 物料计算增强:
- 支持多产品切换、自定义计划产量
- 拆分净需求 / 损耗量展示,格式化数值显示
- 触发 BOM 计算事件,日志记录总需求
模块化架构:
- 主子结构(MasterSlaveView):工站 - 任务联动
- 分节点执行端(TaskNodeView):任务状态变更
- 汇总工作台(DashboardView):全局日志
- 数据维护中心:本地 SQLite 数据编辑
- GDI 图表报表:多维度数据可视化
部署说明
- 将上述代码文件添加到原有 PraticDulTask 项目中
- 确保引用
System.Data.SQLite(通过 NuGet 安装对应版本) - 调整 MainForm 中 TabPage 的加载逻辑,替换原有 ChartView 为 ChartViewEx
- 运行项目后,自动初始化本地 MESData.db 数据库,包含 8-9 组预设测试数据
- 支持在「数据维护中心」编辑 / 新增 / 删除基础数据,所有修改实时同步到本地 SQLite 文件
该实现完整覆盖了工序路径管理、BOM 物料需求计算、产线任务执行、GDI 图表可视化、本地数据维护等核心场景,符合 WinForm + 本地文件数据库的轻量化 MES/ERP 协同系统设计目标。
