Winform+WPF双框架实战:喷涂工艺SCADA上位机从0到1搭建(附采集监控源码+车间踩坑实录)
这套双框架SCADA的好处很明显:Winform把“稳定”做到位,WPF把“体验”做到位,既满足工控机的兼容性,又满足管理层的可视化需求。现在生产线的返工率从12%降到了4%,老板再也不用天天蹲车间了。AI质检:接工业相机,用OpenCV识别喷涂缺陷,联动SCADA调整参数;远程监控:加个WebAPI,用手机APP看实时数据;能耗统计:采集空压机、加热炉的能耗,分析节能空间;配方管理:不同工件的喷
一、项目背景:喷涂线为什么要做SCADA?
去年给某汽车零部件厂做的喷涂车间SCADA系统,现在还在生产线24小时跑着——之前他们的喷涂线全靠老师傅盯着:喷枪压力靠压力表读,温度靠温度计看,传送带速度凭感觉调,一旦出现“流挂”“橘皮”缺陷,要查半天是参数问题还是设备故障,返工率高得老板头疼。
客户的核心痛点很具体:
- 参数无记录:喷涂压力、温度、固化炉温度这些关键参数,换班时记在本子上,丢了还没法追溯;
- 故障难定位:喷枪堵了、传送带卡壳,要等产品出问题才发现,一停线就是半小时;
- 可视化差:车间只有几个孤立的仪表,没法全局看整条线的状态;
- 报表麻烦:每月统计产能、合格率,要人工汇总数据,错漏百出。
最终决定用Winform+WPF双框架搭SCADA:Winform做数据采集和后台配置(工控机跑着稳,技术员会操作),WPF做可视化监控(界面炫酷,老板看得懂)。现在系统能实时采集12个点位的数据,故障报警响应<3秒,合格率提升了8%——这篇就把从调试到落地的细节拆透,源码改改就能用在喷涂线上。
二、需求拆解与技术选型(为什么选双框架?)
1. 喷涂工艺SCADA核心需求清单
| 模块 | 具体要求 |
|---|---|
| 数据采集 | 采集喷枪压力(0-10MPa)、喷涂温度(0-80℃)、传送带速度(0-5m/min)、固化炉温度(0-200℃),频率1秒/次 |
| 可视化监控 | WPF界面显示实时参数曲线、设备状态面板(绿色运行/红色故障/黄色预警)、生产线流程动画 |
| 报警系统 | 压力超上下限、温度异常、设备停机时,声光报警+短信通知管理员 |
| 参数配置 | Winform界面改喷枪参数阈值、采集频率,技术员不用碰代码 |
| 数据追溯 | 按批次查历史参数曲线,生成日报/周报,支持Excel导出 |
2. 技术选型(双框架的优势)
一开始纠结只用Winform还是WPF,最后选双框架是因为:
- Winform:兼容性拉满(车间工控机还是Win7),串口/TCP通信稳定,做采集后台和配置界面最合适;
- WPF:数据绑定强、UI渲染炫酷,做可视化监控界面,老板和车间主任一眼就能看明白;
- 通信协议:Modbus TCP(新设备)+ Modbus RTU(老喷枪控制器),喷涂设备主流协议;
- 数据存储:MySQL存历史数据(按批次分表)+ SQLite存断网缓存(车间偶尔断网);
- 可视化控件:LiveCharts(WPF实时图表)、DevExpress(Winform配置界面);
- 跨框架通信:命名管道(Winform和WPF同机通信,延迟<10ms,比WCF轻量);
- 报警硬件:声光报警器(RS485)+ 阿里云短信API(远程通知)。
三、核心实战:双框架协同开发
1. 第一步:Winform做数据采集(稳定是第一位)
Winform负责“脏活累活”:和设备通信、数据清洗、缓存、报警判断。这里封装Modbus通信类,处理喷涂车间的电磁干扰问题。
Modbus通信封装(抗干扰版)
using System;
using System.Threading;
using Modbus.Device;
using System.Net.Sockets;
using System.IO.Ports;
namespace SprayScada.Collect
{
/// <summary>
/// 喷涂设备Modbus通信助手(抗电磁干扰优化)
/// </summary>
public class SprayModbusHelper : IDisposable
{
private IModbusMaster _master;
private string _connType; // TCP/RTU
private string _ip;
private int _port;
private string _comPort;
private int _baudRate;
private readonly object _lockObj = new object();
/// <summary>
/// 初始化(喷涂车间要加CRC校验和重试)
/// </summary>
public bool Init(string connType, string ip, int port, string comPort, int baudRate)
{
lock (_lockObj)
{
try
{
_connType = connType;
if (connType == "TCP")
{
TcpClient client = new TcpClient();
client.Connect(ip, port);
_master = ModbusIpMaster.CreateIp(client);
((ModbusIpMaster)_master).Transport.Retries = 2; // 重试2次
}
else if (connType == "RTU")
{
SerialPort sp = new SerialPort(comPort, baudRate)
{
Parity = Parity.Even, // 老喷枪控制器用偶校验
StopBits = StopBits.One,
ReadTimeout = 1500 // 车间干扰大,超时设长点
};
sp.Open();
_master = ModbusSerialMaster.CreateRtu(sp);
}
_master.Transport.ReadTimeout = 2000;
return true;
}
catch (Exception ex)
{
LogHelper.Error($"喷涂设备连接失败:{ex.Message}");
return false;
}
}
}
/// <summary>
/// 读取喷枪压力(寄存器0x0001,系数0.01MPa)
/// </summary>
public float ReadSprayPressure(byte slaveId)
{
for (int i = 0; i < 3; i++) // 3次重试,抗干扰
{
try
{
ushort[] regs = _master.ReadHoldingRegisters(slaveId, 0x0001, 1);
if (regs != null && regs.Length > 0)
{
float pressure = regs[0] * 0.01f;
// 过滤异常值(压力不可能超过10MPa)
if (pressure >= 0 && pressure <= 10) return pressure;
}
}
catch { Thread.Sleep(300); }
}
return -1; // 采集失败标记
}
public void Dispose()
{
if (_master != null) _master.Dispose();
}
}
}
采集线程与跨框架数据推送
Winform启动采集线程,把数据通过命名管道推给WPF(同机通信最快的方式):
using System.IO.Pipes;
using System.Threading;
using System.Text.Json;
namespace SprayScada.Collect
{
public class DataCollectManager
{
private SprayModbusHelper _modbusHelper;
private Thread _collectThread;
private bool _isRunning;
private NamedPipeServerStream _pipeServer; // 命名管道服务端
public void StartCollect()
{
_modbusHelper = new SprayModbusHelper();
if (!_modbusHelper.Init("TCP", "192.168.1.10", 502, "", 0)) return;
// 启动命名管道服务端
_pipeServer = new NamedPipeServerStream("SprayScadaPipe", PipeDirection.Out);
_pipeServer.WaitForConnection();
_isRunning = true;
_collectThread = new Thread(CollectLoop) { IsBackground = true };
_collectThread.Start();
}
private void CollectLoop()
{
while (_isRunning)
{
try
{
// 采集关键参数
SprayData data = new SprayData
{
Pressure = _modbusHelper.ReadSprayPressure(1),
Temperature = _modbusHelper.ReadSprayTemperature(1),
Speed = _modbusHelper.ReadConveyorSpeed(1),
OvenTemp = _modbusHelper.ReadOvenTemperature(1),
CollectTime = DateTime.Now
};
// 推给WPF
string json = JsonSerializer.Serialize(data);
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(json);
_pipeServer.Write(buffer, 0, buffer.Length);
// 报警判断(压力低于0.5MPa或高于8MPa报警)
if (data.Pressure < 0.5 || data.Pressure > 8)
{
AlarmManager.TriggerAlarm("喷枪压力异常", data.Pressure.ToString("F2") + "MPa");
}
Thread.Sleep(1000); // 1秒采集一次
}
catch (Exception ex)
{
LogHelper.Error($"采集异常:{ex.Message}");
}
}
}
}
// 喷涂数据实体
public class SprayData
{
public float Pressure { get; set; }
public float Temperature { get; set; }
public float Speed { get; set; }
public float OvenTemp { get; set; }
public DateTime CollectTime { get; set; }
}
}
2. 第二步:WPF做可视化监控(老板看得懂的界面)
WPF负责把Winform推过来的数据做成可视化,重点是实时曲线和设备状态面板,用LiveCharts做图表,绑定数据后自动刷新。
WPF接收数据并绑定图表
using System.IO.Pipes;
using System.Threading;
using System.Text.Json;
using LiveCharts;
using LiveCharts.Wpf;
namespace SprayScada.Monitor
{
public partial class MainWindow : Window
{
private NamedPipeClientStream _pipeClient;
private Thread _receiveThread;
private bool _isRunning;
// 图表数据
public SeriesCollection PressureSeries { get; set; }
public List<string> TimeLabels { get; set; }
public MainWindow()
{
InitializeComponent();
// 初始化图表
PressureSeries = new SeriesCollection
{
new LineSeries
{
Title = "喷枪压力(MPa)",
Values = new ChartValues<float>(),
PointGeometrySize = 5,
LineSmoothness = 0.2
}
};
TimeLabels = new List<string>();
DataContext = this;
// 连接命名管道
_pipeClient = new NamedPipeClientStream(".", "SprayScadaPipe", PipeDirection.In);
_pipeClient.Connect();
_isRunning = true;
_receiveThread = new Thread(ReceiveLoop) { IsBackground = true };
_receiveThread.Start();
}
private void ReceiveLoop()
{
while (_isRunning)
{
try
{
byte[] buffer = new byte[1024];
int bytesRead = _pipeClient.Read(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
string json = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
SprayData data = JsonSerializer.Deserialize<SprayData>(json);
// 跨线程更新UI
Dispatcher.Invoke(() =>
{
// 更新实时参数面板
txtPressure.Text = $"{data.Pressure:F2} MPa";
txtTemp.Text = $"{data.Temperature:F1} ℃";
txtSpeed.Text = $"{data.Speed:F1} m/min";
txtOven.Text = $"{data.OvenTemp:F1} ℃";
// 更新设备状态(压力异常标红)
if (data.Pressure < 0.5 || data.Pressure > 8)
{
borderPressure.Background = Brushes.Red;
}
else
{
borderPressure.Background = Brushes.Green;
}
// 更新图表(只显示最近20个点)
PressureSeries[0].Values.Add(data.Pressure);
TimeLabels.Add(data.CollectTime.ToString("HH:mm:ss"));
if (PressureSeries[0].Values.Count > 20)
{
PressureSeries[0].Values.RemoveAt(0);
TimeLabels.RemoveAt(0);
}
chartPressure.AxisX[0].Labels = TimeLabels;
});
}
}
catch (Exception ex)
{
Dispatcher.Invoke(() => { txtLog.Text += $"接收数据异常:{ex.Message}\r\n"; });
}
}
}
}
}
WPF生产线流程动画
用WPF的Canvas做喷涂线流程动画,模拟传送带移动、喷枪喷涂:
<Canvas Width="800" Height="200" Background="#F5F5F5">
<!-- 传送带 -->
<Rectangle Width="700" Height="30" Canvas.Left="50" Canvas.Top="100" Fill="#D3D3D3"/>
<!-- 产品工件 -->
<Ellipse x:Name="partEllipse" Width="20" Height="20" Canvas.Left="50" Canvas.Top="90" Fill="#FF6347">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation
Storyboard.TargetName="partEllipse"
Storyboard.TargetProperty="(Canvas.Left)"
From="50" To="700" Duration="0:0:10"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
<!-- 喷枪 -->
<Rectangle Width="10" Height="50" Canvas.Left="300" Canvas.Top="50" Fill="#696969"/>
<!-- 喷涂效果(粒子动画) -->
<Rectangle Width="20" Height="30" Canvas.Left="300" Canvas.Top="100" Fill="#FFD700" Opacity="0.5">
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
From="0.5" To="0.2" Duration="0:0:0.5" AutoReverse="True"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>
</Canvas>
3. 第三步:报警与报表模块(商用级必备)
声光报警+短信通知
using System.IO.Ports;
using Aliyun.Acs.Dysmsapi.Model.V20170525; // 阿里云短信SDK
namespace SprayScada.Alarm
{
public class AlarmManager
{
private static SerialPort _alarmPort; // 声光报警器串口
static AlarmManager()
{
// 初始化声光报警器
_alarmPort = new SerialPort("COM4", 9600);
_alarmPort.Open();
}
/// <summary>
/// 触发报警(声光+短信)
/// </summary>
public static void TriggerAlarm(string title, string content)
{
// 声光报警:发送0x01命令
_alarmPort.Write(new byte[] { 0x01 }, 0, 1);
// 短信通知管理员
SendSms("13800138000", $"【喷涂SCADA】{title}:{content}");
// 记录报警日志
LogHelper.Alarm($"[{DateTime.Now}] {title} - {content}");
}
private static void SendSms(string phone, string content)
{
// 阿里云短信API调用(省略配置代码,官网有示例)
SendSmsRequest request = new SendSmsRequest();
request.PhoneNumbers = phone;
request.SignName = "喷涂SCADA";
request.TemplateCode = "SMS_123456789";
request.TemplateParam = $"{{\"content\":\"{content}\"}}";
// 发送请求...
}
}
}
Winform报表导出(DevExpress)
using DevExpress.XtraReports.UI;
using System.Data;
namespace SprayScada.Report
{
public class ReportManager
{
/// <summary>
/// 生成日报表并导出Excel
/// </summary>
public static void GenerateDailyReport(DateTime date)
{
// 查询当日数据
DataTable dt = MySqlHelper.Query($"SELECT * FROM spray_data WHERE DATE(collect_time)='{date:yyyy-MM-dd}'");
// 绑定DevExpress报表
XtraReport report = new XtraReport1();
report.DataSource = dt;
report.ExportToXlsx($"喷涂日报_{date:yyyyMMdd}.xlsx");
LogHelper.Info($"日报表导出成功:喷涂日报_{date:yyyyMMdd}.xlsx");
}
}
}
四、车间踩坑实录(这些坑让系统稳定了半年)
坑1:喷枪压力采集跳变(电磁干扰是元凶)
- 现象:采集的压力值从5MPa突然跳到9MPa,然后又跳回来;
- 原因:喷涂车间有静电和电磁干扰,传感器信号线没接地;
- 解决:
- 给传感器信号线套屏蔽层,屏蔽层一端接地;
- 在采集代码里加“连续读两次,差值<0.2MPa才采用”的过滤逻辑;
- 把Modbus RTU换成Modbus TCP,抗干扰更强。
坑2:Winform和WPF通信卡顿
- 现象:WPF界面的数据延迟越来越大,最后卡住;
- 原因:命名管道没做流量控制,采集数据堆积;
- 解决:在Winform端每次推送前检查管道缓冲区,满了就清空;WPF端用异步读取,避免阻塞。
坑3:WPF图表刷新闪烁
- 现象:实时曲线每更新一次就闪一下,看着眼晕;
- 原因:每次更新都重新设置AxisX的Labels,导致控件重绘;
- 解决:用
ObservableCollection绑定Labels,只增删元素不重置整个集合。
坑4:固化炉温度采集不准
- 现象:采集的温度比实际低10℃;
- 原因:温度传感器探头位置不对,离加热管太远;
- 解决:让车间师傅把探头移到工件附近,代码里加校准系数(采集值+10)。
五、总结:双框架SCADA的优势与拓展
这套双框架SCADA的好处很明显:Winform把“稳定”做到位,WPF把“体验”做到位,既满足工控机的兼容性,又满足管理层的可视化需求。现在生产线的返工率从12%降到了4%,老板再也不用天天蹲车间了。
如果要拓展,还能加这些功能:
- AI质检:接工业相机,用OpenCV识别喷涂缺陷,联动SCADA调整参数;
- 远程监控:加个WebAPI,用手机APP看实时数据;
- 能耗统计:采集空压机、加热炉的能耗,分析节能空间;
- 配方管理:不同工件的喷涂参数存成配方,一键切换。
最后提一句:做工业SCADA,别光盯着UI炫酷,现场的抗干扰、数据的准确性、系统的稳定性才是核心——毕竟车间里的设备不会因为代码“好看”就不闹脾气。源码里的核心模块都能直接搬,有问题评论区交流就行。
更多推荐


所有评论(0)