编辑
2026-04-22
C#
00

目录

🏭 先搞清楚,SCADA 到底要解决什么问题
👨‍💻 看一下样式
🧱 架构先行:别急着写代码
⚙️ 仿真引擎:让数据"活"起来
🗺️ 导航系统:自绘 ListBox 的正确姿势
📊 四大模块的实现逻辑
🔵 总览看板
📈 工艺趋势
🚨 报警历史
🖥️ 设备状态
🎨 深色主题:工业风的正确打开方式
🔌 参数验证:别让用户输入崩掉你的程序
🚀 如果要进一步扩展……
💬 最后说几句

上周有个做自动化的朋友找我抱怨:他们厂里的上位机软件,界面丑得像 2003 年的网吧管理系统,逻辑全堆在一个 Form 里,动不动就假死。他问我,C# 能不能做出像样的 SCADA 界面?我说,不仅能,还能做得很优雅。这篇文章,就把我的实战思路完整拆给你看。


🏭 先搞清楚,SCADA 到底要解决什么问题

SCADA(数据采集与监视控制系统)这个词听起来很唬人,但本质上,它干的事情就三件:采数据、看数据、管设备

工业现场的开发者最头疼的,不是算法,是结构。你见过那种把所有逻辑全塞进 Form1.cs 的上位机代码吗?定时器回调里直接操 UI,报警判断和趋势绘图混在一起,改一个需求,整个文件都得翻。这玩意儿,维护起来真的是噩梦。

咱们今天要做的这个 Demo,麻雀虽小,五脏俱全——实时趋势、报警历史、设备状态、参数配置,四个模块,一套导航,外加一个仿真引擎驱动数据。


👨‍💻 看一下样式

image.png

🧱 架构先行:别急着写代码

很多人上来就拖控件、写事件,结果代码越写越乱。工业软件有个特点——模块多、状态复杂、需要长期维护。所以在动手之前,先把结构想清楚。

这个项目的核心思路是:单一 Form + 动态模块切换

不是每个功能一个 Form,也不是 UserControl 堆叠,而是用一个 GroupBox 作为内容区,根据导航选择动态换入不同的控件。听起来简单,但细节里藏着不少门道。

布局上,用 TableLayoutPanel 三列分割:左侧导航、中间内容区、右侧参数面板。右侧面板只在"工艺趋势"模块下可见,其余模块直接把列宽收为 0,视觉上干净利落。

csharp
private void SetRightPanelVisible(bool visible) { pnlRight.Visible = visible; // 不是 Hide,而是直接把列宽归零——这样布局不会留白 tlpRoot.ColumnStyles[2].Width = visible ? RightPanelWidth : 0F; }

这个小细节很多人会忽略。直接 Hide 控件,TableLayoutPanel 那一列还是会占位,界面会出现一块莫名其妙的空白。


⚙️ 仿真引擎:让数据"活"起来

没有真实 PLC 的情况下,咱们用 System.Windows.Forms.Timer 模拟数据采集。每秒触发一次,生成一个围绕设定值随机波动的过程值。

csharp
private void SimTimer_Tick(object? sender, EventArgs e) { _tick += 1; // 核心公式:设定值 + 随机方向 × 波动幅度 var value = _setPoint + (_random.NextDouble() * 2 - 1) * _fluctuation; _xs.Add(_tick); _ys.Add(value); // 滑动窗口,只保留最近 120 个点,避免内存无限增长 if (_xs.Count > 120) { _xs.RemoveAt(0); _ys.RemoveAt(0); } // 重绘趋势图 pbTrend.Plot.Clear(); var line = pbTrend.Plot.Add.Scatter(_xs.ToArray(), _ys.ToArray()); line.LineWidth = 2; pbTrend.Plot.Axes.AutoScale(); pbTrend.Refresh(); sslValue.Text = $"当前值:{value:F2}"; UpdateClock(); RefreshCurrentModule(); // 同步刷新当前模块的状态显示 }

这里有个踩坑点需要提醒:_xs_ys 用的是 List<double>,每次 Tick 都在 RemoveAt(0)。数据量小的时候没问题,但如果你把窗口开到几千个点,频繁移除头部元素的性能会很难看——List<T>RemoveAt(0) 是 O(n) 操作。

更优雅的替代方案是用 Queue<double> 或者循环缓冲区(Circular Buffer)。生产项目里,这个细节值得认真对待。


🗺️ 导航系统:自绘 ListBox 的正确姿势

默认的 ListBox 在深色主题下会显示系统蓝色选中框,破坏整体视觉风格。解决方案是开启 OwnerDrawFixed 模式,自己掌控每一个条目的绘制。

csharp
private void LstNavigation_DrawItem(object? sender, DrawItemEventArgs e) { if (e.Index < 0) return; var isSelected = (e.State & DrawItemState.Selected) == DrawItemState.Selected; // 选中态:亮蓝色背景;未选中:深灰 var backColor = isSelected ? Color.FromArgb(30, 144, 255) : Color.FromArgb(45, 52, 60); var textColor = isSelected ? Color.White : Color.Gainsboro; using var backBrush = new SolidBrush(backColor); using var textBrush = new SolidBrush(textColor); e.Graphics.FillRectangle(backBrush, e.Bounds); // 文字左侧留 8px 边距,视觉上不那么拥挤 var textRect = new Rectangle(e.Bounds.X + 8, e.Bounds.Y + 15, e.Bounds.Width - 8, e.Bounds.Height); e.Graphics.DrawString(lstNavigation.Items[e.Index].ToString(), e.Font, textBrush, textRect, StringFormat.GenericDefault); // 选中时,左侧画一条 4px 的亮色指示条——这是很多专业 UI 的标志性细节 if (isSelected) { using var indicator = new SolidBrush(Color.FromArgb(180, 230, 255)); e.Graphics.FillRectangle(indicator, new Rectangle(e.Bounds.Left, e.Bounds.Top, 4, e.Bounds.Height)); } }

那条 4px 的侧边指示条,是从很多现代 UI 框架里借来的设计语言。实现成本极低,但视觉提升非常明显。工业软件不一定要土,细节到位了,照样能做得有质感。


📊 四大模块的实现逻辑

🔵 总览看板

最简单的一个。用一个 Label 居中显示系统状态,包括仿真运行状态、当前设定值、波动幅度。每次 RefreshCurrentModule 都会更新文本内容,确保状态与实际同步。

csharp
case "总览看板": _moduleLabel.Text = $"系统状态:{(_simRunning ? "仿真运行中" : "仿真已停止")}\n" + $"当前设定值:{_setPoint:F1}\n" + $"波动幅度:±{_fluctuation:F1}"; grpProcessView.Controls.Add(_moduleLabel); SetRightPanelVisible(false); break;

📈 工艺趋势

ScottPlotFormsPlot 控件(即 pbTrend)直接塞进内容区,右侧面板显示参数配置。这个模块是整个系统的核心视觉呈现,趋势图的刷新逻辑已经在 SimTimer_Tick 里处理好了。

切换到这个模块时,右侧参数面板才会显示出来——设定值和波动幅度的输入框,点击"应用"后立即生效,下一个 Tick 就能看到曲线变化。

🚨 报警历史

DataGridView 展示历史报警记录。这里有个有意思的逻辑:

csharp
// 如果仿真运行中,且最新值超过了"设定值 + 波动幅度的 80%",触发实时报警 if (_simRunning && _ys.Count > 0 && _ys[^1] > _setPoint + _fluctuation * 0.8) _alarmGrid.Rows.Add(now.ToString("HH:mm:ss"), "PLC-001", "中", "实时值高于预警阈值");

_ys[^1] 是 C# 8.0 引入的索引语法,取列表最后一个元素。比 _ys[_ys.Count - 1] 清爽多了——这种小语法糖,用熟了真的很顺手。

报警阈值设为波动幅度的 80%,而不是 100%,是为了留出预警空间。这个思路在真实 SCADA 里叫"预警级别",比直接报警早一步介入。

🖥️ 设备状态

ListView 以表格形式展示 PLC 设备的在线状态和当前采集值。

csharp
private void LoadDeviceModule() { _deviceList.Items.Clear(); var current = _ys.Count > 0 ? _ys[^1].ToString("F2") : "--"; var status = _simRunning ? "在线" : "待机"; // 三台设备,当前都共享同一个仿真值——实际项目里每台设备有独立数据通道 _deviceList.Items.Add(new ListViewItem(new[] { "PLC-001", status, current })); _deviceList.Items.Add(new ListViewItem(new[] { "PLC-002", status, current })); _deviceList.Items.Add(new ListViewItem(new[] { "PLC-003", status, current })); }

Demo 里三台设备共享同一个仿真值,这是简化处理。实际项目中,每台设备对应独立的数据通道,有各自的采集频率和量程范围。


🎨 深色主题:工业风的正确打开方式

工业软件普遍偏好深色主题,原因不是为了好看——是为了减少操作员长时间盯屏的眼疲劳,以及在强光环境下保持对比度。

这套配色方案的核心色值:

用途色值
主背景RGB(28, 34, 40)
次背景 / 表头RGB(45, 52, 60)
选中高亮RGB(30, 144, 255)
主文字Color.Gainsboro

不是随便选的。深蓝灰的背景降低整体亮度,Gainsboro(浅灰白)的文字在深色背景上对比度适中,不刺眼。亮蓝色的选中高亮足够醒目,又不会抢走内容区的注意力。


🔌 参数验证:别让用户输入崩掉你的程序

csharp
private void BtnApply_Click(object? sender, EventArgs e) { if (!double.TryParse(txtParam1.Text, out var setPoint)) { MessageBox.Show("设定值格式错误,请输入数字。", "参数错误"); return; } if (!double.TryParse(txtParam2.Text, out var fluctuation) || fluctuation < 0) { MessageBox.Show("波动幅度格式错误,请输入大于等于 0 的数字。", "参数错误"); return; } _setPoint = setPoint; _fluctuation = fluctuation; sslMode.Text = _simRunning ? "模式:仿真运行" : "模式:参数已更新"; }

TryParse 而不是 Parse——这是防御性编程的基本素养。工业软件的操作员不一定是程序员,他们会在输入框里打空格、打中文、打各种奇怪的字符。你的代码得能优雅地处理这些情况,而不是直接抛异常崩掉。

波动幅度还加了 < 0 的校验,因为负数在物理上没有意义——波动幅度是一个量级,不带方向。


🚀 如果要进一步扩展……

这套架构往生产级别演进,有几个方向值得考虑:

数据层分离:当前仿真数据直接在 Form 里生成和消费。实际项目应该抽出独立的数据服务层,负责与 PLC 通信(Modbus、OPC UA 等协议),Form 只负责展示。

事件驱动替代轮询Timer 轮询是最简单的方式,但高频数据或多设备场景下,考虑用 async/await + 数据推送模式,避免 UI 线程阻塞。

报警持久化:当前报警数据是每次切换模块重新生成的,没有持久化。生产环境里,报警记录要写入数据库,支持历史查询和导出。

ScottPlot 的进阶用法:当前只用了最基础的散点图。ScottPlot 支持信号图(Add.Signal),对高频连续数据的渲染性能比 Scatter 好得多,数据量大时可以考虑切换。


💬 最后说几句

工业软件这个领域,C# WinForms 至今仍是主流——不是因为技术落后,而是稳定、部署简单、与 Windows 生态深度集成。很多工厂的上位机跑在 Windows 7 上,你跟他说 Electron 或者 Web 前端,人家直接摇头。

这套 Demo 的代码量不大,但每一行都有来由。导航自绘、模块动态切换、列宽归零、滑动窗口缓冲……这些细节,是从真实项目里踩坑踩出来的。

你在做工业上位机或者类似的桌面监控系统吗?遇到过哪些让你头疼的架构问题?欢迎在评论区聊聊,说不定咱们踩过同一个坑。


#C#开发 #WinForms #SCADA #工业软件 #桌面应用

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!