ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C# WinForms 本地图像轮询与 TCP 信号回传实战

C# WinForms 本地图像轮询与 TCP 信号回传实战 简介这份资源面向需要在无相机SDK取图条件下实现检测可视化的C#开发者尤其适合工业监控、远程诊断等场景。程序从本地文件夹周期性读取最新图像并绘制到窗体同时通过TCP接收信号并解码为字符串同步展示将图像处理与网络通信结合形成一套可运行的检测可视化方案。压缩包共80个文件约18.64MB包含9个cs源码、17个dll依赖库、14个xml配置、2个exe可执行文件及csproj、sln工程文件另附resx、ico等界面资源目录结构完整可直接用Visual Studio打开编译运行。已有70人学习下载。读者可从中获取本地图像实时刷新、TCP数据接收与字符串转换、窗体界面布局等关键实现代码并参考线程管理、异常处理与资源释放的写法快速搭建图像与数据同步分析的可视化原型。1. 本地取图 TCP 信号回传这套 C# 窗体方案到底解决什么问题相机不给 SDK、不给取图接口只能把采集到的样本往本地文件夹里丢——这是很多工业现场和实验环境的真实处境。vision_ClientNEW41 这个 C# WinForms 工程就是冲着这个场景来的它周期性扫描本地文件夹里最新的图像文件把图绘制到窗体客户区同时开一条 TCP 连接接收远端发来的信号把二进制流解码成字符串显示在界面上让图像和数据在同一块窗体里对齐。适合谁手上有一台只会存图的相机、又需要把检测结果和图像同步可视化的 C# 开发者。它不解决采集只解决「图已经在本地了怎么实时显示并叠加网络信号」这一段。工程里带了 HslCommunication 12.1.2、McProtocol 1.2.5、Newtonsoft.Json 13.0.1 三个包说明它不只是显示还预留了跟 PLC 和 JSON 数据打交道的口子。2. 工程结构与依赖先看清 Vision_watch 里有什么2.1 从 sln 到 csproj 的文件地图拿到 vision_ClientNEW41.rar 解压后根目录是 Vision_watch.sln用 VS2022 直接双击打开即可。核心文件分布如下文件/目录作用Vision_watch.sln解决方案入口VS2022 可直接打开Vision_watch.csproj工程文件声明目标框架与引用Form1.cs / Form1.Designer.cs / Form1.resx主窗体逻辑、控件布局、资源Program.cs程序入口Application.Run 启动主窗体log.cs日志记录辅助类App.config运行时配置连接参数常放这里packages.configNuGet 包版本锁定packages/HslCommunication、McProtocol、Newtonsoft.Json 实体favicon (3).ico窗体图标.vs / bin / objVS 缓存与编译输出可删packages.config 里锁的三个包版本要留意HslCommunication.12.1.2 负责工业通信McProtocol.1.2.5 是三菱 MC 协议实现Newtonsoft.Json.13.0.1 处理 JSON 序列化。这三个包不是随便塞的说明原始工程有跟 PLC 交换数据的意图你如果只做纯 TCP 字符串显示可以保留 HslCommunication 备用其余按需裁剪。2.2 目标框架与引用检查打开 csproj 先确认 TargetFramework。WinForms 工程常见是 net472 或 net6.0-windows两者对 System.Drawing 和 System.Net.Sockets 的引用方式不同。net472 下 System.Drawing 是框架自带net6.0-windows 需要在 csproj 里显式加UseWindowsFormstrue/UseWindowsForms。如果编译报「找不到 Graphics」或「找不到 TcpClient」八成是目标框架和引用没对上。!-- Vision_watch.csproj 关键片段net6.0-windows 写法 -- PropertyGroup OutputTypeWinExe/OutputType TargetFrameworknet6.0-windows/TargetFramework UseWindowsFormstrue/UseWindowsForms Nullabledisable/Nullable /PropertyGroup逻辑说明OutputType 设为 WinExe 表示这是窗体程序不是控制台UseWindowsForms 打开 WinForms 支持System.Drawing 和 System.Windows.Forms 才会被自动引入。参数说明如果你的机器只装了 .NET Framework 4.8把 TargetFramework 改成 net472同时删掉 UseWindowsForms 那行改用传统引用方式。改完先做一次「生成解决方案」确认零错误再往下走别带着编译错误去调运行期逻辑。3. 本地文件夹实时取图轮询、锁文件与双缓冲3.1 为什么用轮询而不是 FileSystemWatcher理论上 FileSystemWatcher 能监听文件夹变化事件驱动比轮询优雅。但工业现场翻车最多的就是它相机写文件往往先建临时文件再改名FileSystemWatcher 会在文件还没写完时就触发 Created 事件你去读就是一个「文件被占用」异常。血泪经验是样本图动辄几 MB写入耗时几十到几百毫秒事件触发时文件根本没落盘。所以这个工程用轮询更稳固定间隔扫描目录按最后写入时间排序取最新一张读之前先做一次可读性探测。// 周期性扫描本地文件夹取最新一张图 private string GetLatestImage(string folder) { var dir new DirectoryInfo(folder); // 按最后写入时间倒序取第一个 var latest dir.GetFiles(*.jpg) .OrderByDescending(f f.LastWriteTime) .FirstOrDefault(); if (latest null) return null; // 可读性探测能独占打开才认为写完了 try { using (var fs File.Open(latest.FullName, FileMode.Open, FileAccess.Read, FileShare.None)) { return latest.FullName; } } catch (IOException) { return null; // 还在写本轮跳过 } }逻辑说明GetFiles 用通配符过滤扩展名OrderByDescending 按 LastWriteTime 排序FirstOrDefault 拿最新。关键是 FileShare.None 独占打开——如果相机进程还在写这个文件这里会抛 IOException直接返回 null 跳过本轮避免读到半张图。参数说明*.jpg按你相机实际输出改png、bmp 同理轮询间隔建议 200500ms太快空转吃 CPU太慢丢帧。常见做法是把间隔放到 App.config 里现场可调。3.2 用 PictureBox 还是自绘双缓冲防闪烁显示这一层新手最容易踩的坑是直接给 PictureBox.Image 赋值图一大就闪因为每次赋值都触发重绘。工程里如果用的是 PictureBox把 SizeMode 设成 Zoom 让图自适应再在窗体构造函数里开双缓冲// 窗体构造函数里开启双缓冲抑制重绘闪烁 public Form1() { InitializeComponent(); this.DoubleBuffered true; // 窗体级双缓冲 this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); pictureBox1.SizeMode PictureBoxSizeMode.Zoom; // 等比缩放 }逻辑说明DoubleBuffered 让绘制先到内存缓冲再一次性刷到屏幕消除闪烁OptimizedDoubleBuffer 和 AllPaintingInWmPaint 是更细粒度的样式控制配合使用效果更稳。参数说明SizeMode 选 Zoom 是等比缩放留黑边选 StretchImage 会拉伸变形工业检测场景一般要保比例选 Zoom。如果你不用 PictureBox 而是自己重写 OnPaint 用 Graphics.DrawImage记得在绘制前调g.InterpolationMode InterpolationMode.HighQualityBicubic否则缩放后图会糊。3.3 把取图和显示拆到不同线程取图是 IO 操作放 UI 线程会卡界面。正确做法是开一个后台线程或 Timer 做扫描拿到图之后用 Invoke 回到 UI 线程更新控件。跨线程直接操作控件会抛「线程间操作无效」异常这是新手必踩的一脚。// 后台线程扫描Invoke 回 UI 线程刷新 private void ScanLoop() { while (!_cts.IsCancellationRequested) { var path GetLatestImage(_watchFolder); if (path ! null) { // 用 Image.FromFile 会锁文件改用流读取后释放 using (var fs new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { var img Image.FromStream(fs); this.Invoke(new Action(() { pictureBox1.Image?.Dispose(); // 释放上一张 pictureBox1.Image (Image)img.Clone(); })); } } Thread.Sleep(_intervalMs); } }逻辑说明Image.FromStream 从流读取读完流关闭避免 Image.FromFile 长期锁住文件导致相机无法覆盖写。Invoke 把赋值动作切回 UI 线程。参数说明_cts是 CancellationTokenSource窗体关闭时 Cancel 掉循环否则线程不退出进程残留。img.Clone()是因为流关闭后原 Image 可能失效克隆一份交给控件持有。注意每次赋值前 Dispose 旧图不然内存会一路涨上去。4. TCP 接收信号转字符串粘包、编码与线程安全4.1 TcpClient 连接与接收循环TCP 这一层工程用 System.Net.Sockets 里的 TcpClient。连接建立后开一个接收循环把收到的字节流解码成字符串。这里第一个坑是粘包TCP 是字节流协议没有消息边界你发两条「OK」「NG」接收端可能一次收到「OKNG」。常见做法是约定固定长度头或分隔符。// TCP 客户端连接与接收循环 private void TcpReceiveLoop(string ip, int port) { var client new TcpClient(); client.Connect(ip, port); var stream client.GetStream(); var buffer new byte[1024]; while (client.Connected) { int len stream.Read(buffer, 0, buffer.Length); if (len 0) break; // 对端关闭 // 按换行符切分处理粘包 string raw Encoding.UTF8.GetString(buffer, 0, len); foreach (var line in raw.Split(\n)) { if (string.IsNullOrWhiteSpace(line)) continue; AppendText(line.Trim()); // 更新界面 } } client.Close(); }逻辑说明stream.Read 返回实际读到的字节数0 表示对端关闭连接。Encoding.UTF8.GetString 把字节解码成字符串再按\n切分处理粘包。参数说明buffer 大小 1024 是保守值信号短可以调小长报文调大编码要和发送端一致发送端用 GBK 你这里用 UTF8 就会乱码这是第二个高频坑。AppendText 里同样要 Invoke 回 UI 线程。4.2 字符串显示与长度控制接收到的字符串往 TextBox 或 Label 上贴直接textBox1.Text line在高速信号下会卡死因为每次追加都触发重绘和滚动。工程里如果信号频率高建议用 ListBox 或限制 TextBox 行数。// 限制显示行数避免 TextBox 无限增长卡顿 private void AppendText(string line) { this.Invoke(new Action(() { textBox1.AppendText(line Environment.NewLine); // 超过 500 行就砍掉最老的 if (textBox1.Lines.Length 500) { var kept textBox1.Lines.Skip( textBox1.Lines.Length - 500).ToArray(); textBox1.Lines kept; } })); }逻辑说明AppendText 比高效内部做了增量更新。行数超过 500 就截断防止内存和渲染压力无限增长。参数说明500 是经验值信号频率高可以降到 200需要追溯历史就落盘到 log.cs 里而不是全堆在界面。字符串长度本身也要留意超长字符串比如几 KB 的 JSON贴进 TextBox 会明显卡顿这种情况建议只显示摘要或前 N 个字符。4.3 断线重连与异常兜底现场网络不会一直稳TcpClient 断开后如果不重连界面就再也不更新了。接收循环要包在 try-catch 里断开后延时重试。// 断线自动重连指数退避 private void TcpWithReconnect(string ip, int port) { int delay 1000; while (!_cts.IsCancellationRequested) { try { TcpReceiveLoop(ip, port); delay 1000; // 正常断开重置退避 } catch (Exception ex) { Log.Write($TCP 异常: {ex.Message}); } Thread.Sleep(delay); delay Math.Min(delay * 2, 10000); // 最多退到 10 秒 } }逻辑说明外层 while 保证断线后重连内层 try-catch 捕获连接和读取异常。指数退避避免对端没起来时疯狂重连打满 CPU。参数说明初始 1 秒每次翻倍上限 10 秒。Log.Write 来自 log.cs把异常落盘方便事后排查。注意_cts在窗体 FormClosing 里 Cancel否则关窗后线程还在重连。5. 避坑与排查这几处翻车最多5.1 图像文件被占用读出来是半张图现象界面偶尔显示一张下半截是灰的图或者直接抛「文件正由另一进程使用」。原因相机写文件是分块写的你读的时候它还没写完。解决读之前用 FileShare.None 独占探测抛 IOException 就跳过本轮等下一轮再读。别用 File.Exists 判断它只查存在不查可读。5.2 TCP 收到中文乱码现象发送端发「检测通过」界面显示「妫€娴嬮€氳繃」。原因发送端用 GBK 编码接收端用 UTF8 解码字节序列对不上。解决两端约定统一编码国内工业设备默认 GBK 居多把Encoding.UTF8改成Encoding.GetEncoding(GBK)。net6.0 下要额外注册 CodePagesEncodingProvider 才能用 GBK。5.3 跨线程操作控件抛异常现象程序跑一会儿弹「线程间操作无效: 从不是创建控件的线程访问它」。原因后台线程直接改了 TextBox 或 PictureBox。解决所有控件更新都包在this.Invoke(new Action(() {...}))里。别图省事设CheckForIllegalCrossThreadCalls false那是把异常藏起来实际会出更诡异的渲染问题。5.4 内存一路涨跑几小时就崩现象任务管理器里进程内存从几十 MB 涨到几个 GB。原因每次pictureBox1.Image img没释放上一张GDI 对象泄漏。解决赋值前pictureBox1.Image?.Dispose()并且用 Image.FromStream 读流而不是 FromFile读完流立即关闭。长期运行的程序建议加个定时器定期调 GC.Collect 兜底但根治还是靠及时 Dispose。5.5 轮询间隔太短CPU 跑满现象程序一开任务管理器里 CPU 占一个核。原因轮询间隔设成 10ms目录里文件多的时候 GetFiles 加排序开销不小。解决间隔调到 200500ms并且用EnumerateFiles替代GetFiles前者是惰性枚举不一次性建数组。如果目录里文件会累积定期清理旧图或只保留最近 N 张。6. 进阶把图像和信号对齐做成可追溯的检测记录基础功能跑通后真正让这套东西有价值的是「对齐」——某张图对应哪条信号。我一般会在收到 TCP 字符串时打一个时间戳取图时也打时间戳两者差值在阈值内就认为是一组存成一条记录。这样事后追溯「这张 NG 图当时 PLC 报了什么」就有据可查。// 图像与信号按时间戳配对落盘成检测记录 private void PairAndSave(string imgPath, string signal) { var record new { Time DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss.fff), Image imgPath, Signal signal }; // Newtonsoft.Json 序列化工程已引用 13.0.1 string json JsonConvert.SerializeObject(record); File.AppendAllText(detect_log.jsonl, json Environment.NewLine); }逻辑说明用匿名对象承载时间、图像路径、信号三个字段JsonConvert.SerializeObject 序列化成一行 JSON追加到 jsonl 文件。jsonl 每行一条记录方便后续用脚本按行解析。参数说明时间格式精确到毫秒配对阈值建议 500ms 以内超过就认为是两组数据。Newtonsoft.Json 13.0.1 已经在 packages 里直接用不用额外装。验证方法上我习惯先拿一个假数据源跑写个脚本每 2 秒往监控文件夹丢一张图同时用另一个终端nc或 Python socket 往端口发字符串看界面是否同步更新、日志是否成对落盘。这一步过了再接真相机。另外 HslCommunication 12.1.2 和 McProtocol 1.2.5 这两个包如果你现场是跟三菱 PLC 打交道可以直接用它们读寄存器把 PLC 数据也并进这套可视化里比纯 TCP 字符串更结构化。从那以后我每次接这类「相机只存图」的活都强制先把轮询间隔、编码格式、Dispose 这三件事在纸上写一遍再动手省得回头一个个查内存泄漏。希望帮到你。本文还有配套的精品资源点击获取
返回列表