ARTICLE DETAIL

资讯详情

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

003011012_WPF 工业上位机系统化模块开发完整指南

003011012_WPF 工业上位机系统化模块开发完整指南 003011012_WPF 工业上位机系统化模块开发完整指南摘要本文围绕WPF 工业视觉上位机的长期维护痛点,系统性地提出了一套基于分层架构 + 接口抽象 + 依赖注入 + 事件聚合器 + MVVM的模块化开发方案。从"为什么必须模块化"出发,逐步展开标准六层架构设计、六大核心模块(硬件抽象、算法、PLC 通讯、数据管理、配置管理、日志)的接口定义与实现、Prism 驱动的 DI/IoC 实践、标准化项目结构与.sln依赖关系,以及超时重试、异步化、资源释放、单元测试等工业级最佳实践。同时提供避坑指南(含反例/正例对比)、常见问题排查(相机/PLC/内存泄漏/UI 卡顿),以及涵盖 WiX/Inno Setup 打包、Git 语义化版本管理、NLog 结构化日志、配置热更新、模块热插拔的实战部署与维护方案,帮助团队构建稳定、可复用、易维护的工业上位机系统。针对工业视觉 / 自动化设备的特点(多硬件交互、长生命周期、团队协作、高稳定性要求),我会从架构设计→模块划分→核心技术→项目结构→实战案例五个维度,给出经过数百台设备验证的系统化开发方案,彻底解决代码混乱、复用性差、维护困难的问题。一、先搞懂:工业上位机为什么必须模块化?工业软件的生命周期通常是5~10 年,期间会经历:硬件更换(换相机品牌、换 PLC 型号)功能迭代(增加新的检测算法、新的工位)团队交接(多人维护同一套代码)项目复制(同一套代码适配不同客户)如果没有系统化的模块设计,代码会变成 “屎山”,最终导致:改一个 bug 引出十个 bug换硬件需要重写一半代码新人上手需要几个月项目复制成本极高二、工业上位机标准分层架构(核心)严格遵守 “上层依赖下层,下层不依赖上层” 的原则,每层只和相邻层交互,绝对不能跨层调用。plaintext:┌─────────────────────────────────┐ │ UI层(Views) │ 只负责界面显示和用户输入 ├─────────────────────────────────┤ │ ViewModel层(ViewModels) │ 只负责UI逻辑和数据绑定 ├─────────────────────────────────┤ │ 业务逻辑层(Business) │ 核心业务流程(检测流程、生产流程) ├─────────────────────────────────┤ │ 服务层(Services) │ 封装通用业务能力(相机服务、PLC服务) ├─────────────────────────────────┤ │ 硬件抽象层(Abstractions) │ 隔离硬件差异(相机接口、PLC接口) ├─────────────────────────────────┤ │ 数据访问层(Data) │ 数据库、文件、配置读写 └─────────────────────────────────┘各层职责与禁止事项层级核心职责绝对禁止UI 层布局、样式、控件、用户输入任何业务逻辑、任何硬件调用、任何数据处理ViewModel 层数据绑定、命令、UI 状态管理直接操作硬件、直接访问数据库、复杂业务逻辑业务逻辑层流程编排、业务规则、状态机直接操作硬件、直接访问数据库服务层封装硬件操作、通用业务能力任何 UI 相关代码、任何业务流程硬件抽象层定义硬件接口、隔离硬件差异任何业务逻辑、任何 UI 代码数据访问层数据持久化、配置读写任何业务逻辑、任何硬件操作三、核心模块划分(工业上位机通用)按照单一职责原则,将系统拆分为以下独立模块,每个模块可以单独开发、测试、部署、复用。1. 硬件抽象模块(最核心,工业软件灵魂)目标:隔离不同品牌硬件的差异,上层代码不需要关心具体用的是什么相机、什么 PLC。定义统一接口csharp:// 相机接口 public interface ICamera { bool Initialize(); bool StartCapture(); bool StopCapture(); Bitmap GetBitmap(int timeoutMs = 3000); bool SetExposureTime(int exposureMs); void Dispose(); } // PLC接口 public interface IPlc { bool Connect(string ip, int port); bool Disconnect(); short ReadShort(string address); bool WriteShort(string address, short value); bool ReadBool(string address); bool WriteBool(string address, bool value); }不同硬件实现csharp:// 埃科相机实现 public class IkCamera : ICamera { public bool Initialize() { // 埃科相机初始化代码 } public Bitmap GetBitmap(int timeoutMs = 3000) { // 埃科相机取图代码 } } // 海康相机实现 public class HikCamera : ICamera { public bool Initialize() { // 海康相机初始化代码 } public Bitmap GetBitmap(int timeoutMs = 3000) { // 海康相机取图代码 } }上层调用(完全不关心具体硬件)csharp:public class CameraService { private readonly ICamera _camera; // 依赖注入,运行时决定用哪个相机 public CameraService(ICamera camera) { _camera = camera; } public Bitmap CaptureImage() { return _camera.GetBitmap(); } }巨大优势:换相机品牌时,只需要写一个新的ICamera实现,上层代码一行都不用改。2. 算法模块目标:隔离算法实现,算法可以独立开发、测试、升级。csharp:// 算法接口 public interface IDefectDetector { ListDefect Detect(Bitmap image); } // 表面缺陷检测算法 public class SurfaceDefectDetector : IDefectDetector { public ListDefect Detect(Bitmap image) { // 算法处理代码 } } // 尺寸测量算法 public class DimensionDetector : IDefectDetector { public ListDefect Detect(Bitmap image) { // 尺寸测量代码 } }3. PLC 通讯模块目标:封装所有 PLC 读写操作,提供强类型的方法,避免魔法字符串。csharp:public class PlcService { private readonly IPlc _plc; public PlcService(IPlc plc) { _plc = plc; } // 强类型方法,隐藏地址细节 public bool WaitForStartTrigger(int timeoutMs = 30000) { return SpinWait.SpinUntil(() = _plc.ReadBool("M100"), timeoutMs); } public void SendEndTrigger(bool result) { _plc.WriteShort("D200", (short)(result ? 3 : 999)); } }4. 数据管理模块目标:封装所有数据读写操作,统一数据格式。csharp:public class DataService { public void SaveDefectRecord(DefectRecord record) { // 保存到数据库 } public ListDefectRecord QueryDefectRecords(DateTime startTime, DateTime endTime) { // 查询数据库 } public void SaveImage(Bitmap image, string barcode) { // 保存图像到文件 } }5. 配置管理模块目标:统一管理所有系统配置,支持运行时修改。csharp:public class ConfigService { public AppConfig LoadConfig() { // 从JSON文件加载配置 } public void SaveConfig(AppConfig config) { // 保存到JSON文件 } } // 强类型配置类 public class AppConfig { public CameraConfig Camera { get; set; } public PlcConfig Plc { get; set; } public AlgorithmConfig Algorithm { get; set; } }6. 日志模块目标:统一日志格式,支持多级别日志输出。csharp:public interface ILogger { void Info(string message); void Warn(string message); void Error(string message, Exception ex = null); } // NLog实现 public class NLogLogger : ILogger { private readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); public void Info(string message) { _logger.Info(message); } public void Error(string message, Exception ex = null) { _logger.Error(ex, message); } }四、核心技术支撑1. 依赖注入(DI):模块化的基石作用:解耦模块之间的依赖关系,让模块可以独立开发和测试。推荐框架:Prism:WPF 工业开发首选,自带模块化、MVVM、事件聚合器Microsoft.Extensions.DependencyInjection:微软官方,轻量易用示例:Prism 中注册模块csharp:public class App : PrismApplication { protected override void RegisterTypes(IContainerRegistry containerRegistry) { // 注册单例服务 containerRegistry.RegisterSingletonILogger, NLogLogger(); containerRegistry.RegisterSingletonIPlc, SiemensPlc(); containerRegistry.RegisterSingletonICamera, IkCamera(); // 注册业务服务 containerRegistry.RegisterCameraService(); containerRegistry.RegisterPlcService(); containerRegistry.RegisterAlgorithmService(); } protected override Window CreateShell() { return Container.ResolveMainWindow(); } }示例:ViewModel 中注入服务csharp:public class MainViewModel : BindableBase { private readonly CameraService _cameraService; private readonly PlcService _plcService; private readonly AlgorithmService _algorithmService; // 构造函数注入 public MainViewModel(CameraService cameraService, PlcService plcService, AlgorithmService algorithmService) { _cameraService = cameraService; _plcService = plcService; _algorithmService = algorithmService; StartCommand = new DelegateCommand(StartProcess); } public ICommand StartCommand { get; } private async void StartProcess() { // 业务流程 await _plcService.WaitForStartTrigger(); var image = _cameraService.CaptureImage(); var defects = _algorithmService.DetectDefects(image); var result = defects.Count == 0; _plcService.SendEndTrigger(result); } }2. 事件聚合器:模块间通信的标准方式问题:模块之间需要通信,但不能直接引用,否则会导致强耦合。解决方案:使用事件聚合器(EventAggregator),通过发布 / 订阅模式实现松耦合通信。示例:定义事件csharp:public class DefectDetectedEvent : PubSubEventListDefect { } public class ProcessCompletedEvent : PubSubEventbool { }示例:发布事件csharp:public class AlgorithmService { private readonly IEventAggregator _eventAggregator; public AlgorithmService(IEventAggregator eventAggregator) { _eventAggregator = eventAggregator; } public ListDefect DetectDefects(Bitmap image) { var defects = new ListDefect(); // 算法处理 // 发布事件 _eventAggregator.GetEventDefectDetectedEvent().Publish(defects); return defects; } }示例:订阅事件csharp:public class MainViewModel { public MainViewModel(IEventAggregator eventAggregator) { // 订阅事件 eventAggregator.GetEventDefectDetectedEvent().Subscribe(OnDefectDetected); } private void OnDefectDetected(ListDefect defects) { // 更新UI显示缺陷 DefectList = new ObservableCollectionDefect(defects); } }优势:发布者和订阅者完全不知道对方的存在,模块之间零耦合。3. MVVM 模式:UI 层和业务层分离核心原则:View 只负责显示,不包含任何业务逻辑ViewModel 只负责 UI 逻辑,不包含任何业务逻辑Model 只负责数据结构,不包含任何逻辑错误写法:csharp:// 错误:在ViewModel中直接操作PLC private void StartButton_Click() { Device.PLC.WriteShort("D100", 1); // 直接操作硬件 var image = Device.GetBitmap(); // 直接调用硬件方法 }正确写法:csharp:// 正确:通过服务调用 private async void StartProcess() { await _plcService.SendStartTrigger(); var image = await _cameraService.CaptureImageAsync(); }五、标准项目结构plaintext:IndustrialVisionSystem/ ├── Abstractions/ # 硬件抽象层 │ ├── ICamera.cs │ ├── IPlc.cs │ └── ILogger.cs ├── Implementations/ # 硬件实现 │ ├── Cameras/ │ │ ├── IkCamera.cs │ │ └── HikCamera.cs │ ├── Plcs/ │ │ ├── SiemensPlc.cs │ │ └── OmronPlc.cs │ └── Loggers/ │ └── NLogLogger.cs ├── Services/ # 服务层 │ ├── CameraService.cs │ ├── PlcService.cs │ ├── AlgorithmService.cs │ ├── DataService.cs │ └── ConfigService.cs ├── Business/ # 业务逻辑层 │ ├── DetectionProcess.cs │ ├── ProductionManager.cs │ └── StateMachine.cs ├── ViewModels/ # ViewModel层 │ ├── MainViewModel.cs │ ├── CameraViewModel.cs │ ├── ParameterViewModel.cs │ └── DataViewModel.cs ├── Views/ # UI层 │ ├── MainWindow.xaml │ ├── CameraView.xaml │ ├── ParameterView.xaml │ └── DataView.xaml ├── Models/ # 数据模型 │ ├── Defect.cs │ ├── DefectRecord.cs │ └── AppConfig.cs ├── Data/ # 数据访问层 │ ├── DatabaseContext.cs │ └── FileManager.cs ├── Common/ # 通用工具 │ ├── Converters/ │ ├── Extensions/ │ └── Helpers/ └── App.xaml在实际开发中,我们推荐将上述目录拆分到多个.csproj项目中,再用一个解决方案(.sln)统一管理。下面是一个完整的 Visual Studio 解决方案文件示例,展示如何引用这些分层项目。解决方案文件:IndustrialVisionSystem.slnMicrosoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndustrialVisionSystem.UI", "src\IndustrialVisionSystem.UI\IndustrialVisionSystem.UI.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndustrialVisionSystem.ViewModels", "src\IndustrialVisionSystem.ViewModels\IndustrialVisionSystem.ViewModels.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndustrialVisionSystem.Business", "src\IndustrialVisionSystem.Business\IndustrialVisionSystem.Business.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndustrialVisionSystem.Services", "src\IndustrialVisionSystem.Services\IndustrialVisionSystem.Services.csproj", "{D4E5F6A7-B8C9-0123-DEF0-234567890123}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndustrialVisionSystem.Abstractions", "src\IndustrialVisionSystem.Abstractions\IndustrialVisionSystem.Abstractions.csproj", "{E5F6A7B8-C9D0-1234-EF01-345678901234}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndustrialVisionSystem.Implementations", "src\IndustrialVisionSystem.Implementations\IndustrialVisionSystem.Implementations.csproj", "{F6A7B8C9-D0E1-2345-F012-456789012345}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndustrialVisionSystem.Data", "src\IndustrialVisionSystem.Data\IndustrialVisionSystem.Data.csproj", "{A7B8C9D0-E1F2-3456-0123-567890123456}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndustrialVisionSystem.Models", "src\IndustrialVisionSystem.Models\IndustrialVisionSystem.Models.csproj", "{B8C9D0E1-F2A3-4567-1234-678901234567}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndustrialVisionSystem.Common", "src\IndustrialVisionSystem.Common\IndustrialVisionSystem.Common.csproj", "{C9D0E1F2-A3B4-5678-2345-789012345678}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|x64 {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|x64 {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|x64 {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|x64 EndGlobalSection EndGlobal各项目间的依赖关系(从上到下,上层依赖下层)项目依赖项说明UIViewModels, Models, Common启动项目,只负责界面渲染ViewModelsBusiness, Services, Models, Common绑定和 UI 逻辑,通过 DI 注入业务服务BusinessServices, Models, Common核心流程编排,不接触硬件与数据库ServicesAbstractions, Implementations, Data, Models, Common封装硬件/算法/数据操作,依赖抽象而非具体实现ImplementationsAbstractions, Common具体硬件 SDK 实现,换硬件时只需替换此项目AbstractionsCommon纯接口定义,不引用任何具体实现DataModels, Common数据库/文件/配置读写,不包含业务规则ModelsCommon纯数据结构,不引用任何其他项目Common无通用工具,不依赖任何业务项目依赖箭头简图
返回列表