简介:面向WPF开发者的智慧工厂数据看板源码包,基于C#实现,属于软件/插件类工程范例,聚焦大数据可视化与生产监控场景,既可学习设计模式,也可套用到实际看板项目。内容覆盖MVVM分层、数据绑定、实时刷新、统计图绘制、页面板块划分及动画触发时机等设计要点;文件以PNG/JPG界面素材、CS/XAML源码、编译缓存及DLL为主,并配有sln、csproj、resources等工程文件,打开即可构建并查看完整目录结构。压缩包为RAR格式,共149个文件,整体13.96MB,可快速解压后配合Visual Studio研读纯源代码,从中理解WPF项目从界面到数据的组织方式,也能拆解样式资源与各模块之间的依赖关系。已有183人浏览学习,适合具备基础WPF知识、希望进阶到工业数据展示与看板开发的读者,亦可作为毕业设计或内部可视化大屏的参考雏形。
1. C# WPF大数据电子看板源码,这类智慧工厂平台到底值不值得上手
车间入口挂着一排液晶屏,主屏滚动生产计划达成率、设备综合效率、不良品趋势,侧屏是设备状态卡片。看板一上线,生产主管不再每天追着Excel问数据,这就是C# WPF大数据电子看板源码最常见的落地场景。所谓WPF智慧工厂数据平台,剥开看就是三块:数据从哪取、怎么送到界面、界面用什么承载,再加上权限控制。它不是要把厂里所有服务器换掉,很多时候只是给现有MES、PLC、OPC服务开一扇“能看”的窗口。
这篇不讲那种几十人开发的商业大平台,只聚焦你手上能搭、能改、能交付的方案。适合谁看?做过WPF基础应用、被上位机数据折腾过、正要给车间上大屏的.NET工程师。我会按数据层、显示层、线程调度、设备对接的顺序讲,每个环节给可复现的代码和能抄的配置,最后把现场最容易翻车的几个坑单列一章。
2. 数据基座先立住:从采集队列、批量落库到推送通道的搭建
大屏项目最容易犯的错,是先把界面画得漂漂亮亮,再回头接数据。等接上真实数据你会发现,界面要的粒度和数据源给的根本不是一回事。做C# WPF电子看板源码,我习惯先确定数据流,把采集、缓存、落库、推送这条链路用空跑数据先打通,再去做视觉。
2.1 看板场景下的“大数据”,不是集群,是吞吐与频率
很多采购方张口就是“大数据平台”,我一般先纠正:车间大屏面临的多数不是海量存量数据,而是高频实时数据。一台设备几十个点位,几十台设备就是上千个点位,每秒钟至少扫两轮,一天几百万条记录,一个月就是亿级。这个量级放SQL Server里确实要建索引、分区或做按日归档,但跟Hadoop集群没关系,重点在于数据入口不能崩塌。
智慧工厂数据平台的源码里,最常出现两类数据源:一类是历史库/关系库,存产量、质量、能耗这些结构化结果;另一类是PLC/OPC/Modbus实时源,直接决定大屏上的数字跳动。前者适合查询,后者适合实时推送。在这两者中间加一层缓存,是我见过的所有稳定交付方案共有的设计,否则界面直接查历史库,查询周期超过两秒,大屏看起来就是卡一帧掉一帧。
还有一个容易漏掉的需求是“行、列权限”。同一张大屏,厂长能看全部产线,车间主管只看自己的班组,这就是典型的行列级权限。WPF端实现不复杂,只要在取数模型上预留角色过滤条件,别把所有数据一股脑拉到客户端再筛选,权限就牢牢把住了。想在客户端过滤的人,往往要吞下“界面一卡就见不到底”的苦果。
2.2 采集与落库:用BlockingCollection挡住生产端的洪峰
我常用的数据入口是:一组采集线程从Modbus/OPC/数据库里读取点位,写入一个阻塞队列;另一组持久化线程从队列里取数据,攒够一批用SqlBulkCopy落到数据库。采集端和落库端解耦,数据库抖动不会反向拖垮采集,瞬时流量高峰也被队列吸收。
using System.Collections.Concurrent; public sealed class DataCollector : IDisposable { private readonly BlockingCollection<PointValue> _queue = new(new ConcurrentQueue<PointValue>(), 20000); private readonly CancellationTokenSource _cts = new(); private readonly int _batchSize = 500; public void Start() { Task.Run(ReadLoop); Task.Run(PersistLoop); } private void ReadLoop() { while (!_cts.IsCancellationRequested) { // 从Modbus/OPC读取一个点位,现场往往有几十毫秒超时。 // 采集线程数一般设为设备数的1/2,不要无脑开线程池 var value = ReadSinglePoint(); if (!_queue.TryAdd(value, 100)) { // 队列满说明消费端跟不上,此时别阻塞采集线程。 // 我记录告警并丢弃最老数据,保证最新数据能进来 Trace.WriteLine("data queue full, drop oldest"); } } } private void PersistLoop() { var batch = new List<PointValue>(_batchSize); foreach (var item in _queue.GetConsumingEnumerable()) { batch.Add(item); if (batch.Count >= _batchSize) { BulkInsert(batch); batch.Clear(); } } } private void BulkInsert(List<PointValue> batch) { using var bulk = new SqlBulkCopy(connectionString) { DestinationTableName = "dbo.PointHistory", BatchSize = _batchSize }; // SqlBulkCopy在表有触发器或变更追踪时性能受明显影响。 // 历史累积表少建普通索引,只留主键聚簇索引,速度差一个量级 bulk.WriteToServer(batch.ToDataTable()); } }队列容量为什么取20000?这跟采集频率和落库耗时有关。假设每秒2000个点位,一批落库耗时200ms,队列里最多积压约400条;20000的缓冲能扛住数据库故障状态下大约10秒的积压,再久就该报警了。TryAdd超时设为100毫秒,超过就说明消费有问题,宁可丢旧值也不能让采集线程卡死。
落库是另一个容易翻车的点。SQL Server的目标表如果建了大量索引,SqlBulkCopy每次追加都可能触发索引维护。生产库和大屏共用实例时,你会发现大屏一开,数据采集跟着变慢。我一般把历史表按天分区,当天热表只保留主键聚簇索引,归档时再补列索引,落库速度快一个量级,查询也不吃亏。
2.3 推送通道:用Channel给UI侧节流,别让界面跟着数据节奏走
采集和落库解决“存下来”,大屏真正需要的是“当前这一刻的值”。如果每采集一次就往UI推一次,界面数字会变成每秒几十次刷新,视觉上反而是闪烁。常见做法是加一个推送层,用System.Threading.Channels做一个带缓存的最新值总线,UI通过定时器只取每个点位的最新值。
using System.Threading.Channels; public sealed class RealTimeHub : IDisposable { private readonly Channel<PointValue> _channel = Channel.CreateBounded<PointValue>(new BoundedChannelOptions(10000) { FullMode = BoundedChannelFullMode.DropOldest, // 满时丢最旧,保证最新 SingleWriter = false, SingleReader = true }); public void Publish(PointValue value) { _channel.Writer.TryWrite(value); } public async IAsyncEnumerable<PointValue> ReadAll() { await foreach (var item in _channel.Reader.ReadAllAsync()) { yield return item; } } }DropOldest对看板特别合适。看板显示的是实时状态,用5秒前的数据都不如最新数据有意义,丢最旧是数字量场景下最好的权衡。这里要忘掉“可靠传输”四个字,大屏实时通道不需要重发机制,丢点没事,关键是别让整个管道的速度慢下来。
// 订阅侧的简化写法 var hub = new RealTimeHub(); _ = Task.Run(async () => { await foreach (var value in hub.ReadAll()) { App.Current.Dispatcher.BeginInvoke(() => ViewModel.ApplyValue(value)); } });这里仍然用后台线程读通道,只是让UI更新委托回到UI线程。后台读通道的好处是队列读不会因UI卡顿而暂停,哪怕界面瞬时繁忙,最新数据也先挂在Dispatcher队列里,等UI线程空闲再补上。这是智慧工厂大屏常见且稳妥的架构。
3. WPF显示层:绑定模型、控件选型与大数据量表格的承载
数据链路搭好后,问题变成“怎么把上万的点位变化显示得又快又稳”。WPF的数据绑定是大屏的命根子,绑定写得不讲究,数据量一上来画面就是肉眼可见的卡。这一章我会把ViewModel设计、DataGrid虚拟化和大屏布局控件的取舍一次讲清楚。
3.1 把采集模型做成可通知对象:INotifyPropertyChanged是底线
WPF里要数字自己变化,实现INotifyPropertyChanged是最基本的路径。源码里如果哪个Model没实现这个接口,那界面一定只用单向赋值,数字不会主动跳。我习惯用一段精简的基类,属性SetValue统一处理赋值与通知,避免每个类都写样板代码。
public abstract class ObservableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected bool SetValue<T>(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (EqualityComparer<T>.Default.Equals(field, value)) return false; field = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); return true; } } public sealed class MachineStatusItem : ObservableObject { private int _output; private string _status = "offline"; public int Output { get => _output; set => SetValue(ref _output, value); } public string Status { get => _status; set => SetValue(ref _status, value); } }这里要提醒两点。第一,属性setter里做好旧值比较,WPF绑定引擎会对属性变更事件做响应,哪怕值没变也触发,高频点位一秒更新几次,没必要的通知会白白消耗UI线程。第二,千万别在属性setter里放业务逻辑,绑定线程和后台线程都可能触发setter,任何重逻辑都会放大卡顿。第三,绑定内部的通知链路像黑匣子,调试时用Snoop这类工具能看到哪个属性让界面重绘,比瞎猜定位快得多。
3.2 表格显示:DataGrid虚拟化参数与行列权限的落点
电子看板里最常见的表格是设备状态明细,几十台设备、每台上千条记录,一次加载十几万行很常见。很多源码里的DataGrid默认设置就能跑几万行,但滚动开始顿,问题多半出在虚拟化开关上。
<DataGrid EnableRowVirtualization="True" EnableColumnVirtualization="True" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" ItemsSource="{Binding MachineRows}" SelectionMode="Single" />VirtualizationMode="Recycling"的含义是滚动时容器复用而不是新建,这是几万行表格流畅的关键。还有一点很少人注意:DataGrid的IsReadOnly要设成True。可编辑状态下虚拟化容器要额外维护编辑状态,性能开销不小,大屏不需要在表格里改数据,直接锁掉。
行、列权限在这个环节落地最合适。常见做法是给表格的查询接口传当前用户的角色,数据库或服务端先过滤出可见的行和列,WPF只收到最终表格。原因很简单:十几万行的权限过滤放到客户端做,过滤逻辑一多,UI线程就扛不住。如果确实要在客户端做,至少用ICollectionView先铺好筛选条件,再绑定ItemsSource。
表格控件也不是只有DataGrid一个选项。追求极致性能的电子看板源码里,常见选择是ReoGrid这类表格控件,它按单元格重绘,滚动性能比DataGrid好,只是绑定MVVM时要自己同步数据;追求界面统一的,会用HandyControl覆盖默认样式。我的选择是:设备明细用普通DataGrid,秒级刷新的趋势明细用ReoGrid,让每个控件干它最擅长的事。
3.3 大屏视觉:布局容器、主题样式与3D动画的理性边界
智慧工厂大屏的布局,通常是上下左右四块加中间一张产线示意图。用WPF的Grid加三行三列铺底,屏幕分辨率一般1920x1080或3840x1080的双屏方案,我习惯用一个Viewbox包住整体,让界面在分辨率变化时等比缩放。虽然会有轻微模糊,但大屏远看基本无感。
第三方主题方面,HandyControl在工厂项目里出现频率很高,它自带的Card、DataGrid、Growl弹窗能很快拼出“科技感”界面,不用自己造按钮样式。MaterialDesign风格偏轻量,适合管理层看板。两者都是成熟的WPF开源控件库,选一个就够,混用反而会让样式冲突。
关于3D动画看板,很多搜“wpf 实现3d 动画看板”的人其实想要的是产线设备的3D姿态模拟。技术上用HelixViewport3D可以加载模型并旋转视角,但真实项目我的建议是谨慎。3D渲染每一帧都要占用GPU资源和UI线程调度,如果大屏数字还在每秒刷新,3D一开实时性就受损。如果确实要3D,只做初始几秒的旋转动画,之后切到静态视角,只更新旁边的数字。这是交付过几个大屏后认下来的做法,宁可少一点动画,换来数字跳得流畅。
4. “活数据”如何进WPF而不卡死界面:线程模型与MVVM推送节奏
很多C#上位机工程师都遇到过同一个症状:后台数据一切正常,界面一旦刷新量上来就白屏、卡顿、无响应。这不一定怪WPF,多半是跨线程访问和UI刷新频率没控制好。看完这一章,你对“怎么把实时数据喂给WPF”会有自己的套路。
4.1 先理解WPF的线程模型:UI线程是唯一能碰控件的线程
WPF的控件继承DispatcherObject,必须由创建它的线程访问,这个线程通常就是UI线程。你在后台线程直接改文本框内容,会得到InvalidOperationException。有人说用Dispatcher.Invoke就行,可Invoke是同步阻塞的,后台线程会一直等UI线程处理完才继续跑。采集频率一高,大量后台线程堵在Invoke上,整个数据链路就卡滞。
所以我在推送代码里一律用BeginInvoke或await切到UI上下文。BeginInvoke是异步排队,后台线程推完立刻返回,UI线程按自己的节奏一个个处理。理解这个区别,大屏卡顿就解决了一半。
4.2 后台线程安全推送UI的两种主流写法
第一种是直接在后台线程里持有一个UI调度器来投递委托。
public sealed class UiThreadPoster { private readonly Dispatcher _dispatcher; public UiThreadPoster() { _dispatcher = Application.Current.Dispatcher; } public void Post(Action action) { if (_dispatcher.CheckAccess()) { action(); } else { _dispatcher.BeginInvoke(action); } } }CheckAccess判断当前线程是不是UI线程,是则直接跑,避免不必要的排队;否则异步投递。这段代码放在基类里,后台线程只要调用Poster.Post(() => vm.Value = item.Value)就能安全更新UI。
第二种是用事件聚合器,让ViewModel只订阅事件,不关心数据从哪个线程来。MVVM项目用Prism时可直接用EventAggregator,也可以自己写一个简化版:
public sealed class UiEventBus { private readonly Dictionary<Type, List<Delegate>> _handlers = new(); private readonly Dispatcher _dispatcher = Application.Current.Dispatcher; public void Subscribe<T>(Action<T> handler) { if (!_handlers.TryGetValue(typeof(T), out var list)) { list = new List<Delegate>(); _handlers[typeof(T)] = list; } list.Add(handler); } public void Publish<T>(T message) where T : class { _dispatcher.BeginInvoke(() => { if (_handlers.TryGetValue(typeof(T), out var list)) { var handlers = list.OfType<Action<T>>().ToArray(); foreach (var handler in handlers) { handler(message); } } }); } }这个事件总线的特点是把所有事件投递集中在一处,订阅者永远跑在UI线程,不会出现某个订阅者忘了切线程的问题。Publish里做了快照,避免UI线程正在遍历时有人取消订阅导致异常。项目里几块大屏同时订阅时,只要事件模型不共享可变状态,这个总线足够稳定。
4.3 刷新节奏的调节:聚合几百条变化,一次推给界面
就算做了跨线程切换,数据量一大,每条都推UI还是会让界面忙不过来。更好的方案是定时器批量刷新:后台线程把最新值写进并发字典,UI线程上的DispatcherTimer每200毫秒拉一次变化,合并成一批更新ViewModel。
public sealed class BatchRefresher { private readonly ConcurrentDictionary<string, object> _latest = new(); private readonly DispatcherTimer _timer; private readonly Action<IDictionary<string, object>> _apply; public BatchRefresher(Action<IDictionary<string, object>> apply) { _apply = apply; _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) }; _timer.Tick += (_, _) => Flush(); _timer.Start(); } public void Push(string key, object value) { _latest[key] = value; } private void Flush() { if (_latest.IsEmpty) return; var snapshot = new Dictionary<string, object>(_latest); _latest.Clear(); _apply(snapshot); } }刷新间隔200毫秒对工业大屏够用。人对10Hz以上的连续变化已经不敏感,200毫秒意味着每秒最多5次重绘,UI线程有充足时间渲染卡片、图表和动画。如果设备状态有报警这类突变事件,可以直接触发一次即时刷新,不用干等下一轮。
最后提醒:DispatcherTimer本身跑在UI线程上,回调里不要写太多重逻辑。_apply(snapshot)会触发一堆绑定更新,如果快照里有几百个点位,单次处理也可能超过20毫秒。这时可以先去掉次要动画,把预算让给数字。
5. 设备对接与大屏交付:Modbus、UVC、SQL批量写入的5个踩坑现场
这一章全是真实教训。C# WPF电子看板源码本身不复杂,项目里耗时最多的往往是设备通讯和现场环境问题。我按“现象 → 原因 → 解决”写五条,你遇到同类问题时能少走几轮弯路。
5.1 Modbus点位跳零:数值看着看着变成0,过几秒又恢复
很多现场拿Modbus大屏对接PLC,第一次跑会出现某几个点位数值每十几秒跳成0的“灵异事件”。多数原因不是通讯断了,而是Modbus读操作和写操作在同一个串口/网口上冲突,写寄存器时读请求返回异常,客户端却按默认值处理。另一个隐藏原因:PLC里未初始化的保持寄存器读出来就是0,而点表里有些地址本来就是预留位。
解决要做两件事。一是给Modbus请求加互斥锁,读和写共用一个SemaphoreSlim(1,1),同一时刻只允许一个事务在同一个设备上执行;二是把失效判定的阈值写好,连续读同一地址N次都超时才置为离线,单次超时不要直接写0。这样既保留实时性,又挡住瞬时异常。
5.2 多路UVC摄像头回调:画面串流、反复退出的元凶
前端用DirectShow/UVC采集多路摄像头做垛位识别,很多人会把回调里拿到的帧直接转Bitmap或推给UI。现象就是两个摄像头轮流花屏、偶尔AccessViolationException。这其实是典型的原生回调线程安全问题:DirectShow的回调跑在采集线程上,UI线程同时访问同一帧缓冲区时,缓冲区正被覆盖。
血泪经验是:回调函数里只做一件事——把帧复制到该摄像头自己队列的队尾,立即返回。每个摄像头一个独立队列,用设备ID作为键;消费端按队列逐一取帧处理。这样即使回调来得再快,UI拿到的都是完整帧拷贝,不会读到半帧,也不会出现两个摄像头共享缓冲区互相覆盖。
private readonly ConcurrentDictionary<int, Channel<byte[]>> _frameQueues = new(); private void OnFrameArrived(int deviceId, byte[] frameData) { var queue = _frameQueues.GetOrAdd(deviceId, _ => Channel.CreateBounded<byte[]>( new BoundedChannelOptions(4) { FullMode = BoundedChannelFullMode.DropOldest })); queue.Writer.TryWrite(frameData); }队列容量设成4,只保留最近几帧。视频识别用不到每一帧,掉几帧无所谓,最怕回调里处理耗时导致采集缓冲全被占光。小容量还能天然形成反压,让摄像头适当丢帧,别把CPU耗在无意义的编码上。
5.3 后台线程异常让整个看板“悄悄退场”
大屏挂在车间一年到头不关,最怕后台线程抛异常后进程直接崩掉。常见原因:回调线程访问了已释放的通道、串口设备拔出后继续读写、数据库连接串临时失效。这些异常发生在非UI线程,默认会直接终结进程,不给界面任何反应时间。
解决至少要三层。第一,每个后台循环主体用try/catch包住,异常记录日志并继续下一轮,不要让它冒泡。第二,在App启动处挂全局兜底:
DispatcherUnhandledException += (_, e) => { Log.Error(e.Exception, "unhandled exception"); if (RestartPolicy.ShouldRestart()) RestartApp(); else e.Handled = true; }; AppDomain.CurrentDomain.UnhandledException += (_, e) => { Log.Error(e.ExceptionObject as Exception, "domain unhandled"); };第三,给大屏进程配一个看门狗:系统服务或计划任务定期检查看板进程是否在跑,不在就从最新发布目录拉起来。这是最后一道保险,防止偶发异常让人跑到车间里手动重启。
5.4 DataGrid数据铺满后滚动卡顿:绑定集合根本没配合虚拟化
有种卡顿是一行行加记录时越来越卡,后来每加一行就像往ListBox里塞一个完整控件。原因是直接把List<T>或ObservableCollection<T>绑给了DataGrid,又没开虚拟化;或者开了虚拟化但外层ScrollViewer把虚拟化吞了。
解决分三层确认:XAML里VirtualizingPanel.IsVirtualizing为True;ScrollViewer.CanContentScroll保持True,不能为了触摸惯性改成False;如果数据每秒新增几十行,把ObservableCollection改成批量插入——用一个CollectionView做缓冲,每200毫秒批量提交一次,而不是来一条就触发一次集合变更。集合变更通知太密集,虚拟化容器来不及复用,卡顿就是必然。
5.5 SqlBulkCopy把采集瓶颈拖住:表上有触发器或变更跟踪
给智慧工厂数据平台配历史库,表上如果带着触发器、变更跟踪或大量索引,SqlBulkCopy每批数据写入都会被额外放大。现象是采集队列频繁积压,数据库CPU直线上升,看板点位突然掉线。很多人第一反应是加大批次数,其实问题不在批大小,而在目标表。
我一般是把历史表按“热/冷”分开:当天数据放在热表,只有主键聚簇索引,SqlBulkCopy直接追加;凌晨由归档作业把前一天数据搬进冷表,再按查询习惯建索引。这样既保留大屏查询热数据的性能,又不拖累实时入库。如果实在不能建两张表,就在SqlBulkCopy前先禁用触发器,写完再启用,但要记得触发器的业务逻辑必须在归档时补做。
6. 交付前最后一步:用回放、看门狗和回滚预案把大屏稳稳装上线
大屏开发完并不等于交付完成。在实验室跑得飞快,装到车间网络一乱、旧电脑一卡,画面就是另一回事。我交付前通常会做三件事:回放测试、资源基线记录、版本回滚准备。
第一件,回放测试。把现场录下来的一整段点位数据存成CSV,开发机上按固定时间倍速放给看板。这种做法不依赖车间网络,能复现某一天的异常波动,比如一台设备强制停机、一条产线突停。我会把回放跑满24小时,中间故意用调试器挂起UI线程几秒,看恢复后会不会追帧、队列积压后是不是能甩掉旧数据。发生过一次真实事故:正常跑没问题,网络抖动半分钟,恢复后看板直接跳到之前的状态,问题就出在推送层没把积压的旧数据丢掉,回放帮我在交付前抓到了这个bug。
第二件,资源基线记录。开屏前记录三组数据:启动后10分钟的CPU占用、内存增长曲线、UI线程帧间隔。内存曲线特别重要——很多WPF程序有隐性内存泄漏,看起来只涨几十MB,挂三天涨到几个GB就崩了。我的习惯是交付时把内存基线和工业PC的硬件配置写在一起,回访发现异常时能快速判断是机器退化还是程序泄漏。
第三件,版本回滚预案。车间大屏不像办公室软件可以随便更新,我部署新版本前永远保留上一版本的完整文件夹,并留一个切换脚本。看板若出现数据不刷新或连续报警,运维运行切换脚本,新版本目录改名,上一版本目录改回,十秒内完成。这叫“后悔药”,在工业现场比任何免责声明都实在。
这门手艺做久了,你会形成直觉:凡是敢开机的看板,都是一步一步喂出来的。从队列长度、刷新周期到异常重启策略,每个参数背后都有一次现场经历。真把这几件事做扎实,智慧工厂的大屏才是真正实时、敢让管理层长期信赖的生产数据窗口。希望帮到你。
本文还有配套的精品资源,点击获取