
简介面向WinForm开发者的MES生产数据看板示例工程代码采用C#编写覆盖制造执行系统中设备状态、生产进度、质量信息等数据的采集、处理与可视化展示。项目按模块化思路组织包含数据访问、UI展示、配置读取等部分适合需要快速上手桌面看板开发的初、中级.NET工程师参考。压缩包共31个文件以.cs源代码为主并含.config配置文件、.resx资源文件、.csproj工程文件、.sln解决方案及图标资源整体仅456KB结构精简便于阅读。其中可看到基于SQLite的数据操作、JSON数据转换、自定义UIBarOption封装等实现演示了从数据获取、汇总计算到看板展示与交互的完整链路。已有598人学习浏览对于希望理解WinFormMES看板基础架构的读者是一份轻量而直接的参考。1. 车间大屏总是卡在转圈C#winform-MES生产数据看板.zip 里藏着答案「C#winform-MES生产数据看板.zip」这个压缩包在制造业圈子里流传挺广拆开看就是一套用 Winform 写的车间实时看板源码左侧产线状态、中间工单进度、右侧合格率排行底部再滚一条告警栏。它的出现解决的是一个非常实际的问题——MES 系统里有数但车间大屏上永远慢半拍浏览器看板一到整点换班就转圈运维跑过去按 F5 也没用。Winform 客户端看板自己管连接、自己管渲染断了网还能撑住最后一份快照。适合正在做 MES 实施、上位机开发和车间数字化改造的工程师参考它不追求界面炫技追求的是「生产数据在大屏上持续可见、可控制、可排错」。这篇不假装你手里有这个包直接按这套项目最常见、最可靠的做法讲透——数据从哪来、界面怎么做、哪些坑值得躲。2. 看板数据链路怎么设计直连、接口还是 OPC先定方案再写代码2.1 三种数据来源的选型直连业务库、WebService 和 OPC 采集车间看板的数据来源常见做法是三条路直连 MES 业务数据库、走 WebService 接口、以及从设备层 OPC 或者西门子 S7 协议采集。选哪条路取决于你的 MES 系统是本地部署还是多系统集成以及看板要不要顺带显示设备状态。我一般先画一张对比表再决定数据来源适用场景接入成本实时性主要风险直连业务库只读账号MES 已上线、有 DBA 配合最低一条连接串秒级慢查询拖库、连接数失控WebService / REST 接口有多系统集成、有数据中台中等接口文档秒级到分钟级接口限流、鉴权过期OPC / S7 协议采集看板要带设备运行状态高要配 OPC 服务器毫秒级点位维护麻烦、跨网段难通直连库最常用因为实施期最快WebService 最稳适合企业里有数据中台、不愿意把库暴露给客户端的场景OPC 则解决的是「MES 数据是对的但设备实际开没开、在不在自动运行」这类只有产线才知道的信息。2.2 最小可跑的数据库读取封装连接串、超时和连接池参数定了直连库之后看板的数据访问层不要做得太重一个只读查询类足够。连接串建议放在 App.config 里不要写死在代码里——这条是血泪经验车间设备换 IP、数据库迁移是家常便饭写死在代码里每次都要重新编译。connectionStrings add nameMesReadOnly connectionStringServer192.168.10.20;Databasemes_prod;User Idmes_viewer;Passwordyour_pwd;Connection Timeout5; providerNameSystem.Data.SqlClient / /connectionStrings对应的读取类只要一个静态方法返回 DataTable 即可public static class MesDb { private static readonly string ConnStr ConfigurationManager.ConnectionStrings[MesReadOnly].ConnectionString; public static DataTable Query(string sql) { using (var conn new SqlConnection(ConnStr)) using (var cmd new SqlCommand(sql, conn)) using (var da new SqlDataAdapter(cmd)) { var dt new DataTable(); da.Fill(dt); return dt; } } }注意连接串里 Connection Timeout5 很关键。MES 数据库偶尔重启或者网络抖动如果超时默认 15 秒看板会连续卡死好几个刷新周期车间里的人就以为系统挂了。连接池这边不用刻意加参数默认池就够用但看板是读多写少的场景池过大反而容易把旧连接长时间挂在 MES 库上。2.3 只读访问与统计视图让看板查询不干扰业务很多 MES 看板翻车不是看板代码有问题是查询把业务库拖住了。MES 的报工表、工单表在换班高峰期有大量写入和行锁看板每 5 秒跑一次全表聚合赶上整点报工直接把业务事务堵住。常见做法是给看板建一个只读账号 mes_viewer只能访问视图不能碰表。推荐在 MES 库里建一个看板专用的视图把状态字段和返工标识提前算好-- 看板只读视图只取近30分钟有变动的工单避免全表扫描 CREATE VIEW v_mes_board_current AS SELECT w.WorkOrderNo, w.ProcessCode, w.PlanQty, w.OkQty, w.NgQty, w.StatusCode, w.IsRework, -- 返工/返修单标识后面排错会用到 w.UpdateTime FROM mes_work_order w WHERE w.UpdateTime DATEADD(MINUTE, -30, GETDATE()); GO时间窗这招很实用。生产看板本质上只关心「现在正在发生什么」30 分钟前的数据基本没人看。加了 WHERE 时间条件之后即使工单表到了几十万行查询也走 UpdateTime 上的索引压垮业务库的概率大幅下降。视图里带上 IsRework 字段是后续排查返工返修状态不更新的伏笔建议一开始就预留。3. 让 DataGridView 扛住实时工单列表格式化、后台刷新与局部重绘3.1 状态列渲染把 0/1 变成“合格/不合格”或勾选框看板中间那块的工单明细数据源里存的通常是 0、1、-1 这类状态码直接显示在车间大屏上没人看得懂。Winform 里最通用的做法是用 DataGridView 的 CellFormatting 事件在渲染前拦截值并替换显示文本。private void dgvOrders_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { // col_status 是状态列0 表示不合格1 表示合格-1 表示待检 if (dgvOrders.Columns[e.ColumnIndex].Name ! col_status) return; switch (Convert.ToInt32(e.Value)) { case 1: e.Value OK; e.CellStyle.ForeColor Color.LimeGreen; break; case 0: e.Value NG; e.CellStyle.ForeColor Color.OrangeRed; break; default: e.Value 待检; e.CellStyle.ForeColor Color.Gray; break; } e.FormattingApplied true; }这里有两点值得说明。一是 e.FormattingApplied true 必须写告诉 DataGridView 已经完成格式化别再用默认逻辑处理否则你刚改成「OK」它又给你变回 1。二是如果你确实想要复选框效果把这一列换成 DataGridViewCheckBoxColumn 就行0/1 会自动渲染成勾选状态但要注意数据库 NULL 会显示成灰色方块查询时提前把 NULL 补成 0。3.2 后台轮询刷新用 Task.Run 代替 Timer 里查库实时刷新最土的写法是拖一个 TimerTick 事件里直接查库再绑定 DataSource。这种做法在数据量小的时候没问题一旦 MES 库慢查询超过 2 秒UI 线程就被 SQL 阻塞看板直接变成白屏假死。Timer 的 Tick 本身是在 UI 线程执行的这是 Winform 新手最容易踩的坑。正确做法是把轮询丢到后台线程用 Task.Delay 控制节奏UI 更新再通过 BeginInvoke 回到界面线程public sealed class BoardRefresher { private readonly ISynchronizeInvoke _ui; private readonly CancellationTokenSource _cts new(); private readonly string _boardSql; public BoardRefresher(ISynchronizeInvoke ui, string boardSql) { _ui ui; _boardSql boardSql; } public void Start() Task.Run(() RefreshLoop(_cts.Token), _cts.Token); public void Stop() _cts.Cancel(); private async Task RefreshLoop(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { var dt await Task.Run(() MesDb.Query(_boardSql), ct); // BeginInvoke 不会阻塞后台线程UI 忙时消息排队稍后再处理 _ui.BeginInvoke(new Action(() BindBoard(dt)), null); } catch (Exception ex) { // 车间没人会天天点错误弹窗的“确定”写日志才是正道 File.AppendAllText(D:\logs\board.log, ${DateTime.Now:yyyy-MM-dd HH:mm:ss} {ex.Message}\r\n); } await Task.Delay(TimeSpan.FromSeconds(5), ct); // 轮询间隔按产线节奏调 } } }轮询间隔 5 秒是多数车间看板的经验值既能保证数据延迟在可接受范围又不会把 MES 库打到报警。如果产线节拍快比如汽车零部件装配线可以调到 3 秒如果只是管理看板10 秒也没问题关键是别小于 3 秒否则查询还没返回下一次又开始了连接池会被快速耗尽。3.3 500 行工单不卡的三个开关DataGridView 绑定几百行数据本身不慢慢的是每次刷新都整体重建列、整体重绘。数据源绑定前把这三个开关关上体验完全不一样private void BindBoard(DataTable incoming) { try { dgvOrders.SuspendLayout(); // 挂起布局停止一次性重绘 _bindingSource.DataSource incoming; // BindingSource 长期复用 dgvOrders.AutoGenerateColumns false; // 列结构只在设计期生成一次 dgvOrders.ResumeLayout(); // 恢复布局一次性刷新 } catch (Exception ex) { Log(ex.Message); } }这里有两个关键认知。一是 DataGridView 没有 ListView 那套 BeginUpdate/EndUpdate对应能力靠 SuspendLayout 和 ResumeLayout 实现它抑制的是布局重绘正好解决刷新瞬间黑屏闪烁二是 BindingSource 要定义为窗体成员字段而不是每次刷新 new 一个否则滚动条位置、列宽、排序状态全部重置车间操作工刚往下翻两页就被拉回顶部很恼火。如果明细超过几百行建议再做增量更新——按工单号做主键只把 UpdateTime 变化的行替换进 DataTable这部分逻辑代码量不大但刷新效率和肉眼流畅度能再上一个台阶。4. 把 Winform 界面做成车间大屏自绘仪表盘、分辨率适配与告警闪烁4.1 30 行 GDI 自绘一个圆环仪表盘看板右侧的合格率、设备稼动率很多项目直接引用第三方仪表盘控件好看是好看但控件版本和 .NET 运行时冲突起来非常折腾。其实这种环形进度仪表GDI 自绘 30 行代码就够用还不用背负额外依赖。public sealed class RingGauge : Control { public float Value { get; set; } // 当前值 public float MaxValue { get; set; } // 最大值默认 100 protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); var g e.Graphics; g.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; float sweep (Value / MaxValue) * 300f; // 300度有效范围留60度缺口 using (var bgPen new Pen(Color.FromArgb(40, 50, 65), 16f)) g.DrawArc(bgPen, 6, 6, Width - 12, Height - 12, 120, 300); using (var fgPen new Pen(Color.FromArgb(0, 200, 120), 16f)) g.DrawArc(fgPen, 6, 6, Width - 12, Height - 12, 120, sweep); } }重点参数起始角 120 度、扫过 300 度而不是画满 360 度——留一个缺口才像仪表盘满圆会让人误读成百分百。圆弧粗细 16f 是相对 150px 左右控件尺寸的经验值控件缩小到 100px 以下时记得调成 10f 左右。SmoothingMode 必须设成 AntiAlias否则圆弧边缘全是锯齿车间大屏一放大就露馅。颜色上背景弧用深灰蓝前景用单色不要渐变工业屏对颜色对比度要求高渐变反而看不清。4.2 窗体自适应别用固定像素TableLayoutPanel 按百分比布局车间大屏的分辨率五花八门老的工控机 1366×768新采购的液晶屏 1920×1080还有竖屏挂墙的。看板窗体如果按设计时的分辨率写死控件位置换一台屏就是灾难。布局用 TableLayoutPanel 按百分比分栏而不是直接拖控件定坐标。目标屏类型窗体样式全局字体缩放仪表盘控件直径1366×768 横屏全屏无边框默认140px1920×1080 横屏全屏无边框1.2 倍200px1080×1920 竖屏全屏无边框0.9 倍180px取舍原则主窗体设置 FormBorderStyleNoneWindowStateMaximized开机自启动直接占满整屏不要标题栏车间里没人会去拖窗口。ColumnStyle 和 RowStyle 里的 SizeType 选 Percent左右栏 25%、中间 50%、右侧 25%缩放时控件比例不会变形。字体缩放单独处理因为 Winform 的 AutoScaleMode 在跨分辨率时表现不稳定我习惯在窗体 Load 事件里根据 Screen.PrimaryScreen.Bounds.Width 与 1366 的比值乘以一个 1.0~1.2 的系数统一重置所有 Label 的 Font。4.3 告警闪烁与界面美化的实操状态灯闪烁是最容易被做坏的功能。常见的错误写法是在一个 while 循环里交替改颜色再 Thread.Sleep(500)结果 UI 线程被睡死看板整个卡住。正确做法是放一个 TimerInterval 设在 500 毫秒Tick 里用一个布尔标志位翻转背景色private bool _flashFlag; private void timerAlarm_Tick(object sender, EventArgs e) { // 只要当前有未恢复告警就让红色和深灰交替 bool hasAlarm Convert.ToInt32(lblAlarm.Tag) 1; lblAlarm.BackColor hasAlarm _flashFlag ? Color.Firebrick : Color.FromArgb(30, 30, 30); _flashFlag !_flashFlag; }500 毫秒翻转一次的频率是多数人视觉上最舒服的告警节奏太快会让人觉得屏幕在闪太慢又起不到提醒作用。顺带说一句 Winform 界面美化这件事——看板好看不好看跟换皮肤关系不大主要是配色和字号统一。背景用深色系正文用浅灰色状态值用高饱和色这是车间大屏在日光灯环境下能看清的基础。字体统一用「微软雅黑」或「Arial」字号至少 14pt 起大屏观看距离通常两三米字小了再好看的界面也没意义。5. 生产数据看板踩坑记录五条血泪排错5.1 现象看板运行半小时后窗体整个假死换班高峰期看板突然白屏鼠标移过去变成转圈只能杀进程重启。原因有两层一是前面说的 Timer 里直接查库UI 线程被慢查询堵死二是查询失败后连接没及时释放连接池被耗尽后续所有查询都在排队等超时。解决分两步走查询全部丢给 Task.Run 后台线程MesDb.Query 里用 using 保证连接即用即关。另外把连接串里的 Max Pool Size 显式限制到 8避免看板进程把 MES 库连接数吃到几十条这是我们后来排查数据库侧才发现的问题。5.2 现象MES 表里的中文全部显示成问号看板上所有工单号、产品名称里的中文变成一串问号但 MES 客户端里显示正常。原因在字符集阶段就埋下了老 MES 库是 GBK 编码而 SqlConnection 默认按 UTF-8 跟服务端通信。解决方式是按库的实际编码调整连接串在 SqlConnection 连接串里加 Charsetgbk如果是 MySQL对应加 CharSetgbk。这个坑排查起来最容易走弯路因为数据库客户端和看板显示层都可能有问题我自己的排查顺序是先确认 MES 表字段排序规则再确认连接串编码最后才看 DataGridView 的字体是否支持中文。5.3 现象换班刷新那一刻 DataGridView 整块闪黑每到大屏刷新工单列表先白一块再恢复换班时尤其明显站在屏幕前看就是闪屏。原因是线程里每轮都 new 一个 DataTable 去替换 DataSourceDataGridView 感知到数据结构变化整表重建列、重建行中间经历一次全空状态。解决方法是复用 BindingSource配上 SuspendLayout/ResumeLayout让刷新在界面线程一次性完成。如果仍然闪把 AutoGenerateColumns 设为 false列结构只在设计期生成一次运行时只填数据行重绘压力会小很多。5.4 现象返工返修工单在看板上状态一直不更新正常工单状态实时变但返工、返修单在看板上永远是待检在 MES 里查却已经返工完成。这种业务口径问题最坑人——不是代码错误是 MES 里返工返修单走的是独立流程状态字段压根不在常规报工表里。车间里返工返修是高频业务尤其汽车零部件行业水冷板这类产品压铸不良返修量很大看板漏掉它就等于漏掉一块真实产能。解决方法是跟 MES 实施顾问或产品经理对齐返工单是否复制原工单号、状态存在哪张表、完成后要不要并入合格数统计然后在看板查询里把返工流程的状态集合单独并入。这个坑的修复成本极低但沟通成本很高建议在项目启动时就问清楚。5.5 现象部署到车间工控机杀毒软件把 exe 直接删掉看板拷到车间电脑上双击没反应一看杀毒软件隔离区里躺着主程序 exe。原因很简单Winform 客户端直连数据库、监听端口、频繁读写日志文件这些行为特征跟木马太像360、火绒都容易误报。解决方式不是教车间关杀毒而是走正规发布流程用代码签名证书给 exe 签名然后在集团终端管理后台把看板程序目录加白名单。另外部署目录建议统一放 D:\MESBoard 而不是桌面桌面权限和自动清理策略会带来额外麻烦。6. 把看板再往上一层从 5 秒轮询改成信号表推送给 MES 减负轮询做到最后你会发现瓶颈不在看板在 MES 库。5 秒一次全表扫描每天上万次查询即使有视图和索引DBA 也会来找你聊天。更轻量的做法是引入信号表机制把「查全部业务数据」变成「查变化信号」。思路是这样MES 侧在关键业务节点——工单状态变更、报工完成、产生异常告警时往信号表插入一条记录看板端只轮询这张信号表发现新信号后再去拉对应的业务明细。信号表结构很简单CREATE TABLE mes_signal ( id INT IDENTITY PRIMARY KEY, signal_type VARCHAR(20), -- WO_UPDATE / LINE_CHANGE / ALARM biz_key VARCHAR(40), -- 工单号或产线代码 create_time DATETIME DEFAULT GETDATE() );看板轮询 SQL 从原来的复杂聚合查询变成SELECT TOP 50 * FROM mes_signal WHERE id lastId ORDER BY id查询量直接少一个数量级而且 MES 库里这张表只插不更新索引压力极小。改造后我建议加一道验证工序在刷新代码里用 Stopwatch 记录每次查询耗时超过 2 秒就把 SQL 原文写进日志连续跑一个夜班第二天数一下日志条数就知道哪些时段、哪些查询在拖后腿。var sw Stopwatch.StartNew(); var dt MesDb.Query(_boardSql); sw.Stop(); if (sw.ElapsedMilliseconds 2000) Log($slow query {sw.ElapsedMilliseconds}ms: {_boardSql});这套做法跑顺之后看板就不只是显示工具而是一个有监控、有余量、能定位问题的数据通道。我现在拿到这类看板项目第一句话一定是问对方三个问题数据多久更新一次、断网一瞬间能不能接受、返工返修单要不要进统计口径。这三个问题答完技术方案基本就定了剩下的都是执行细节。看板这东西难点从来不在界面多炫而在它背后那套数据链路稳不稳。希望帮到你。本文还有配套的精品资源点击获取