以下是基于现有 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 协同系统设计目标。