资讯动态

C# MES生产看板开发实战:数据链路、WPF大屏与SignalR实时刷新

发布时间:2026/10/14 3:47:14 来源:尧图企业网站定制
简介这份资源是一套基于C#开发的MES系统生产看板示例工程面向希望理解制造执行系统架构与车间可视化实现的初学者和.NET开发者。它围绕生产进度、质量控制、物料消耗等关键指标的实时展示演示了数据库设计、实时数据获取、WPF/WinForms界面构建、MVVM模式、多线程处理、异常处理与性能优化等核心知识点可作为学习MES系统基本工作流程的实践起点。压缩包为rar格式共36个文件约199KB其中包含8个cs源码文件、3个resx资源文件、1个csproj项目文件与1个sln解决方案文件另有exe、pdb、suo等编译与配置产物整体结构紧凑便于直接打开运行和调试。目前已有4628人学习下载读者可借此掌握用C#实现生产看板功能的完整思路并了解软件工程中代码规范、测试与文档编写等最佳实践。1. 从一块车间大屏说起MES 生产看板到底在解决什么很多工厂的车间里都挂着大屏但真正能让人停下来看两眼的没几块。大部分所谓“看板”就是一张 Excel 截图循环播放数据滞后半天颜色全靠人工填出了问题还得打电话问计划员。MES 系统生产看板要解决的恰恰是这个断层——把工单进度、设备状态、质量数据、人员产出从数据库里实时拉出来用 C# 写一个能跑在车间工控机或大屏一体机上的客户端让现场的人不用登录 ERP、不用翻报表抬头就能看到当前该干什么、干到哪了、有没有异常。这套东西适合谁一是做 MES 二次开发的 C# 工程师手里有数据库但缺一个像样的可视化出口二是工厂 IT 运维想用最低成本把现有 MES 的数据盘活三是系统集成商需要给客户交付一个能演示、能落地的看板模块。它不追求花哨的 BI 效果核心诉求是稳定、刷新快、断网能扛、部署简单。下面我从数据链路、界面实现、刷新策略到踩坑排查把这块看板拆开讲一遍。2. 数据链路怎么搭从 MES 库到看板客户端的三种取数方式2.1 直连数据库还是走接口先看刷新频率和并发量C# 看板取数常见三条路直连 MES 数据库、调用 MES 提供的 WebAPI、读中间表或消息队列。选型不是拍脑袋先看两个硬指标——刷新频率和并发客户端数。如果看板要求 3 秒刷新一次车间同时有 20 块屏直连数据库意味着每分钟 400 次查询。MES 主库通常还扛着工单下发、报工、物料扣减这种压力很容易把生产事务拖慢。我一般建议刷新间隔低于 10 秒、客户端超过 5 个就别直连主库。常见做法是让 MES 侧建一张看板专用中间表由定时任务或触发器每 5 到 10 秒汇总一次看板只读这张表。数据延迟多几秒但生产库安全。如果 MES 已经提供了 WebAPI优先走接口。好处是解耦MES 升级表结构时看板不用跟着改 SQL。代价是接口响应时间不可控需要加超时和降级。下面是一个典型的接口取数封装// 看板数据服务优先走 API失败降级到本地缓存 public class DashboardDataService { private readonly HttpClient _http; private readonly string _apiBase; private readonly MemoryCache _cache; public async TaskProductionSnapshot GetSnapshotAsync(string workshopCode) { try { // 超时设 2 秒看板不能等 using var cts new CancellationTokenSource(TimeSpan.FromSeconds(2)); var resp await _http.GetAsync( ${_apiBase}/api/board/snapshot?workshop{workshopCode}, cts.Token); resp.EnsureSuccessStatusCode(); var json await resp.Content.ReadAsStringAsync(); var data JsonSerializer.DeserializeProductionSnapshot(json); _cache.Set(snapshot_ workshopCode, data, TimeSpan.FromSeconds(30)); return data; } catch (Exception ex) { // 接口挂了就用最近一次缓存界面上打降级标记 Log.Warn($API 取数失败走缓存: {ex.Message}); return _cache.GetProductionSnapshot(snapshot_ workshopCode) ?? new ProductionSnapshot { IsDegraded true }; } } }这段代码的关键参数有三个超时 2 秒、缓存 30 秒、降级标记。超时不能设长看板刷新是周期性的一次卡住会拖垮整个 UI 线程。缓存时间要大于刷新间隔否则降级时没有数据可用。IsDegraded标记传到界面后在角落显示一个灰色提示让现场知道当前数据不是实时的。2.2 工单进度和设备状态的 SQL 该怎么写才不拖库直连或读中间表时SQL 写法直接决定数据库压力。看板查询有两个特点只读、字段固定。所以索引要围绕看板的 WHERE 和 ORDER BY 建不要照搬业务表的索引。以工单进度为例常见需求是“查当前车间所有未完工工单按计划结束时间排序”。假设中间表叫board_workorder字段有workshop_code、status、plan_end_time、completed_qty、plan_qty。查询这样写-- 看板工单进度查询只取必要字段避免 SELECT * SELECT wo.order_no, wo.product_name, wo.plan_qty, wo.completed_qty, wo.status, wo.plan_end_time, CASE WHEN wo.completed_qty wo.plan_qty THEN 完工 WHEN wo.plan_end_time NOW() AND wo.completed_qty wo.plan_qty THEN 超期 ELSE 进行中 END AS display_status FROM board_workorder wo WHERE wo.workshop_code workshop AND wo.status CLOSED ORDER BY wo.plan_end_time ASC LIMIT 50;几个要点第一SELECT只列看板要显示的字段不要SELECT *减少网络传输和内存占用。第二状态判断放在 SQL 里用CASE WHEN算好客户端不再做逻辑减少 C# 侧分支。第三LIMIT 50是保护车间看板不需要展示几百条工单超出部分用汇总数字代替。第四索引建议建在(workshop_code, status, plan_end_time)上覆盖 WHERE 和 ORDER BY。设备状态查询类似但更强调实时性。如果设备状态存在board_equipment表字段有equip_code、run_state、last_update查询时加一个时间过滤避免把几天没更新的僵尸设备拉出来SELECT equip_code, equip_name, run_state, last_update FROM board_equipment WHERE workshop_code workshop AND last_update DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY run_state DESC, equip_code;last_update过滤是血泪经验。设备离线后状态不会自动变如果不加时间条件看板上会一直显示“运行中”误导现场。5 分钟这个阈值根据设备上报频率调整一般取上报间隔的 3 到 5 倍。2.3 用 SqlSugar 还是 Dapper看板场景下的取舍C# 侧 ORM 选择上看板项目我倾向 Dapper 或 SqlSugar 这类轻量方案不用 EF Core。原因很简单看板查询都是只读、字段固定、SQL 可控EF 的变更追踪和 LINQ 翻译反而增加开销和不确定性。Dapper 的优势是贴近 ADO.NETSQL 写什么就执行什么性能可预期。SqlSugar 的优势是自带分页、缓存和多种数据库适配如果 MES 用的是 Oracle 或达梦SqlSugar 的兼容性省事。下面用 Dapper 做一个批量查询示例// Dapper 批量取看板数据工单 设备 质量一次查完 public async TaskBoardData GetBoardDataAsync(string workshop) { using var conn new MySqlConnection(_connStr); var sql SELECT order_no, product_name, plan_qty, completed_qty, status, plan_end_time FROM board_workorder WHERE workshop_code workshop AND status CLOSED ORDER BY plan_end_time LIMIT 50; SELECT equip_code, equip_name, run_state, last_update FROM board_equipment WHERE workshop_code workshop AND last_update DATE_SUB(NOW(), INTERVAL 5 MINUTE); SELECT pass_rate, output_qty, defect_qty FROM board_quality WHERE workshop_code workshop AND stat_date CURDATE();; using var multi await conn.QueryMultipleAsync(sql, new { workshop }); return new BoardData { WorkOrders (await multi.ReadAsyncWorkOrderDto()).ToList(), Equipments (await multi.ReadAsyncEquipmentDto()).ToList(), Quality await multi.ReadFirstOrDefaultAsyncQualityDto() }; }QueryMultipleAsync一次往返拿三组数据比三次独立查询少两次网络往返。看板刷新频繁这点开销累积起来很可观。参数workshop用匿名对象传入Dapper 自动做参数化避免拼接 SQL。注意连接字符串要开连接池MySqlConnection默认池化不用手动管理。3. 界面怎么画WPF 大屏布局与实时刷新实现3.1 用 Grid 分区而不是 Canvas 绝对定位看板界面第一原则适配不同分辨率的大屏。车间里可能是一块 1920×1080 的工控机也可能是 3840×2160 的拼接屏。用 Canvas 加绝对坐标换块屏就错位。正确做法是用 WPF 的 Grid 做比例分区行高列宽用星号比例字体用相对缩放。一个典型的看板布局分四块顶部标题栏、左侧工单列表、右侧设备状态矩阵、底部质量汇总。Grid 定义如下Grid Grid.RowDefinitions RowDefinition Height60/ !-- 标题栏固定高 -- RowDefinition Height2*/ !-- 主区域 -- RowDefinition Height1*/ !-- 底部汇总 -- /Grid.RowDefinitions Grid.ColumnDefinitions ColumnDefinition Width3*/ !-- 工单列表 -- ColumnDefinition Width2*/ !-- 设备矩阵 -- /Grid.ColumnDefinitions TextBlock Grid.Row0 Grid.ColumnSpan2 Text车间生产看板 FontSize28 HorizontalAlignmentCenter VerticalAlignmentCenter/ ListBox Grid.Row1 Grid.Column0 x:NameWorkOrderList ItemsSource{Binding WorkOrders}/ ItemsControl Grid.Row1 Grid.Column1 x:NameEquipmentGrid ItemsSource{Binding Equipments}/ StackPanel Grid.Row2 Grid.ColumnSpan2 OrientationHorizontal TextBlock Text{Binding Quality.PassRate, StringFormat合格率 {0:P1}}/ TextBlock Text{Binding Quality.OutputQty, StringFormat产量 {0}}/ /StackPanel /Grid行高用2*和1*表示比例列宽用3*和2*。这样无论窗口多大各区域按比例伸缩。字体大小如果也要自适应可以在窗口SizeChanged事件里按宽度比例调整根节点FontSize子控件继承。设备状态矩阵用ItemsControl加WrapPanel或UniformGrid每个设备一个方块颜色由run_state绑定转换器决定。绿色运行、黄色待机、红色故障、灰色离线。转换器写法public class StateToBrushConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return value?.ToString() switch { RUN Brushes.Green, IDLE Brushes.Gold, FAULT Brushes.Red, OFFLINE Brushes.Gray, _ Brushes.LightGray }; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) throw new NotImplementedException(); }颜色不要写死在 XAML 里用转换器集中管理后期改配色只动一个文件。3.2 DispatcherTimer 刷新与 UI 线程安全看板刷新用DispatcherTimer不用System.Timers.Timer。后者回调在后台线程更新 UI 要Dispatcher.Invoke写不好就跨线程异常。DispatcherTimer回调本身在 UI 线程直接改绑定属性即可。private DispatcherTimer _timer; private readonly DashboardDataService _service new(); private void StartRefresh() { _timer new DispatcherTimer { Interval TimeSpan.FromSeconds(5) // 刷新间隔 }; _timer.Tick async (s, e) { // 防止上一次没跑完又进来 _timer.Stop(); try { var data await _service.GetSnapshotAsync(WS01); this.DataContext data; // 整体替换触发绑定更新 } finally { _timer.Start(); } }; _timer.Start(); }关键点在_timer.Stop()和finally里的_timer.Start()。如果取数耗时超过刷新间隔不停止的话会堆积多个并发请求界面反而更卡。先停再取取完再启保证串行。DataContext整体替换比逐个属性通知简单但要注意如果界面有滚动位置整体替换会重置滚动。工单列表如果会滚动改成ObservableCollection增量更新。刷新间隔设多少工单进度 5 到 10 秒够用设备状态 3 到 5 秒质量汇总 30 秒到 1 分钟。不同区域可以用多个 Timer 分开刷新避免一次全量拉取。3.3 断网和接口超时的界面降级处理车间网络不稳定是常态。看板必须做到断网时界面不崩、不白屏、有明确提示。前面数据层已经做了缓存降级界面层要配合显示状态。在 ViewModel 里加一个ConnectionState属性取值Online、Degraded、Offline。数据服务返回时带上状态界面顶部显示一条状态栏// 状态栏绑定在线绿色降级黄色离线红色 public string StatusText ConnectionState switch { Online 数据实时, Degraded 数据延迟接口异常显示缓存, Offline 网络断开显示最后数据, _ 未知 };离线时不要清空界面保留最后一次数据只把状态栏变红。现场人员看到红色提示就知道数据不是最新的不会误判。如果离线超过一定时间比如 10 分钟可以在界面中央叠加一个半透明遮罩写“数据已过期”防止有人盯着旧数据做决策。4. 避坑与排查看板上线后最容易翻车的五个点4.1 现象看板越跑越卡内存持续上涨原因每次刷新都新建DataContext对象旧对象没被回收或者事件订阅没取消。WPF 绑定如果用了PropertyChanged但对象被反复替换旧对象的订阅链可能残留。解决用ObservableCollection增量更新代替整体替换检查DispatcherTimer.Tick是否重复订阅StartRefresh只调一次用dotnet-counters或任务管理器观察 GC 频率如果 Gen2 回收频繁说明有大对象反复分配把 DTO 改成结构复用或对象池。4.2 现象工单列表顺序乱跳现场人员看花眼原因ORDER BY plan_end_time在数据更新后顺序变化或者客户端做了二次排序但排序键不稳定。解决排序键加一个唯一字段做 tie-breaker比如ORDER BY plan_end_time, order_no。如果现场习惯固定顺序可以按工单号排序进度变化只改数字不改位置。另外ListBox的ItemsSource如果是ObservableCollection清空再添加会导致滚动重置用Clear()后批量Add也会闪最好用支持范围通知的集合或直接替换但保持滚动偏移。4.3 现象设备状态颜色不更新一直是灰色原因INotifyPropertyChanged没实现或者run_state字段值大小写和转换器不匹配。数据库里存的是run转换器判断的是RUN。解决DTO 实现INotifyPropertyChanged或者用ObservableObject基类。转换器里做ToUpperInvariant()统一大小写。调试时在转换器里打日志确认实际传入的值。4.4 现象接口偶尔超时看板卡住十几秒原因HttpClient默认超时 100 秒没设CancellationToken一次请求卡住整个刷新周期。解决如 2.1 节所示设 2 秒超时用CancellationTokenSource。另外HttpClient要单例复用不要每次new否则端口耗尽。如果 MES 接口本身慢考虑让 MES 侧加缓存或异步查询看板侧只读结果。4.5 现象部署到工控机后字体模糊、界面错位原因工控机 DPI 缩放不是 100%WPF 默认 DPI 感知可能不匹配。或者大屏分辨率是 4K 但系统缩放 150%Grid 比例布局正常但字体没跟着缩放。解决在app.manifest里声明 DPI 感知或者用PerMonitorV2模式。字体用TextBlock的FontSize绑定到窗口宽度的比例在SizeChanged里统一调整。测试时至少覆盖 1920×1080 和 3840×2160 两种分辨率以及 100% 和 150% 两种缩放。5. 进阶技巧用 SignalR 替代轮询把刷新延迟压到一秒内轮询的极限就是刷新间隔5 秒刷新意味着平均延迟 2.5 秒。如果 MES 侧能推数据用 SignalR 做服务端推送看板延迟可以压到一秒以内。做法是 MES 侧建一个 Hub工单状态变更、设备状态变更时主动推给看板客户端。服务端 Hub 示例public class BoardHub : Hub { // 看板客户端连接时加入对应车间分组 public async Task JoinWorkshop(string workshopCode) { await Groups.AddToGroupAsync(Context.ConnectionId, workshopCode); } } // 在 MES 业务逻辑里状态变更后推送 public class BoardNotifier { private readonly IHubContextBoardHub _hub; public BoardNotifier(IHubContextBoardHub hub) _hub hub; public async Task NotifyWorkOrderChanged(string workshop, WorkOrderDto dto) { await _hub.Clients.Group(workshop).SendAsync(WorkOrderChanged, dto); } }客户端连接和接收var connection new HubConnectionBuilder() .WithUrl(http://mes-server/boardHub) .WithAutomaticReconnect() // 断线自动重连 .Build(); connection.OnWorkOrderDto(WorkOrderChanged, dto { // 收到推送更新对应工单不整体刷新 var item WorkOrders.FirstOrDefault(w w.OrderNo dto.OrderNo); if (item ! null) { item.CompletedQty dto.CompletedQty; item.Status dto.Status; } }); await connection.StartAsync(); await connection.InvokeAsync(JoinWorkshop, WS01);WithAutomaticReconnect()是必须的车间网络抖动后自动重连不用人工干预。推送的粒度要控制只推变更的工单或设备不要推全量。如果变更频繁服务端做合并比如 500 毫秒内的多次变更合并成一次推送。轮询和推送可以共存SignalR 做实时更新轮询做兜底对账每 30 秒拉一次全量防止推送丢失导致数据不一致。我一般会在客户端记录最后一次推送时间超过 60 秒没收到任何推送就触发一次全量拉取。从那以后我每次做看板项目都强制走一遍“断网测试 4K 缩放测试 连续运行 72 小时内存观察”这三步少一步上线后都可能被现场电话叫醒。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑