视觉修改
This commit is contained in:
		
							
								
								
									
										838
									
								
								Check.Main/Camera/CameraManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										838
									
								
								Check.Main/Camera/CameraManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,838 @@ | ||||
| using Check.Main.Common; | ||||
| using Check.Main.Infer; | ||||
| using Check.Main.Result; | ||||
| using Check.Main.UI; | ||||
| using OpenCvSharp; | ||||
| using SkiaSharp; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Drawing; | ||||
| using System.Drawing.Imaging; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Runtime.InteropServices; | ||||
| using System.Text; | ||||
| using System.Text.Json.Serialization; | ||||
| using System.Threading.Tasks; | ||||
| using System.Timers; | ||||
| using System.Windows.Forms; | ||||
|  | ||||
| namespace Check.Main.Camera | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 静态全局相机管理器,负责所有相机的生命周期、配置应用和多相机同步 | ||||
|     /// </summary> | ||||
|     public static class CameraManager | ||||
|     { | ||||
|         // 活动的相机实例字典,键为相机名称 | ||||
|         public static Dictionary<string, HikvisionCamera> ActiveCameras { get; } = new Dictionary<string, HikvisionCamera>(); | ||||
|  | ||||
|         // 相机对应的图像显示窗口字典 | ||||
|         //public static Dictionary<string, FormImageDisplay> CameraDisplays { get; } = new Dictionary<string, FormImageDisplay>(); | ||||
|  | ||||
|         public static Dictionary<string, FormImageDisplay> OriginalImageDisplays { get; } = new Dictionary<string, FormImageDisplay>(); | ||||
|         public static Dictionary<string, FormImageDisplay> ResultImageDisplays { get; } = new Dictionary<string, FormImageDisplay>(); | ||||
|  | ||||
|         // --- 多相机同步逻辑 --- | ||||
|         // 产品检测队列 | ||||
|         private static readonly Queue<ProductResult> ProductQueue = new Queue<ProductResult>(); | ||||
|         // 队列锁,保证线程安全 | ||||
|         private static readonly object QueueLock = new object(); | ||||
|         // 当前启用的相机数量,用于判断产品是否检测完毕 | ||||
|         private static int EnabledCameraCount = 0; | ||||
|  | ||||
|         //public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted; | ||||
|         // public static bool IsDetectionRunning { get; private set; } = false; | ||||
|  | ||||
|         // 产品ID计数器 | ||||
|         private static long _productCounter = 0; | ||||
|         // 用于同步计数器访问的锁,虽然long的自增是原子操作,但为清晰和未来扩展,使用锁是好习惯 | ||||
|         private static readonly object _counterLock = new object(); | ||||
|  | ||||
|         // --- 新增:硬触发模拟器 --- | ||||
|         private static readonly System.Timers.Timer _hardwareTriggerSimulator; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 获取或设置模拟硬触发的间隔时间(毫秒)。 | ||||
|         /// </summary> | ||||
|         public static double TriggerInterval { get; set; } = 1000; // 默认1秒触发一次 | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 获取一个值,该值指示硬件触发模拟器当前是否正在运行。 | ||||
|         /// </summary> | ||||
|         public static bool IsHardwareTriggerSimulating { get; private set; } = false; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 静态构造函数,用于一次性初始化静态资源。 | ||||
|         /// </summary> | ||||
|         static CameraManager() | ||||
|         { | ||||
|             //初始化硬触发模拟器 | ||||
|             _hardwareTriggerSimulator = new System.Timers.Timer(); | ||||
|             _hardwareTriggerSimulator.Elapsed += OnHardwareTriggerTimerElapsed; | ||||
|             _hardwareTriggerSimulator.AutoReset = true; // 确保定时器持续触发 | ||||
|             _hardwareTriggerSimulator.Enabled = false; // 默认不启动 | ||||
|         } | ||||
|  | ||||
|  | ||||
|         //// 事件:用于向UI发送日志消息 | ||||
|         //public static event Action<string> OnLogMessage; | ||||
|         //// 用于写入日志文件的 StreamWriter | ||||
|         //private static StreamWriter _logFileWriter; | ||||
|  | ||||
|         ////私有的、静态的、只读的对象,专门用作线程同步的“锁”。 | ||||
|         //private static readonly object _logLock = new object(); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 初始化文件日志记录器。应在程序启动时调用一次。 | ||||
|         /// </summary> | ||||
|         //public static void InitializeLogger() | ||||
|         //{ | ||||
|         //    try | ||||
|         //    { | ||||
|         //        string logDirectory = Path.Combine(Application.StartupPath, "Logs"); | ||||
|         //        Directory.CreateDirectory(logDirectory); // 确保Logs文件夹存在 | ||||
|  | ||||
|         //        string logFileName = $"Log_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt"; | ||||
|         //        string logFilePath = Path.Combine(logDirectory, logFileName); | ||||
|  | ||||
|         //        // 创建StreamWriter,设置为追加模式和自动刷新 | ||||
|         //        _logFileWriter = new StreamWriter(logFilePath, append: true, encoding: System.Text.Encoding.UTF8) | ||||
|         //        { | ||||
|         //            AutoFlush = true | ||||
|         //        }; | ||||
|  | ||||
|         //        Log("文件日志记录器已初始化。"); | ||||
|         //    } | ||||
|         //    catch (Exception ex) | ||||
|         //    { | ||||
|         //        // 如果文件日志初始化失败,在UI上报告错误 | ||||
|         //        OnLogMessage?.Invoke($"[CRITICAL] 文件日志初始化失败: {ex.Message}"); | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 【新增】一个完整的业务流程方法: | ||||
|         /// 1. 根据配置初始化所有相机并显示它们的窗口。 | ||||
|         /// 2. 在所有窗口都显示后,命令所有相机开始采集。 | ||||
|         /// 这个方法是响应“开启设备”或“应用配置并启动”按钮的理想入口点。 | ||||
|         /// </summary> | ||||
|         /// <param name="settingsList">要应用的相机配置列表。</param> | ||||
|         /// <param name="mainForm">主窗体,用于停靠显示窗口。</param> | ||||
|         //public static void ApplyConfigurationAndStartGrabbing(List<CameraSettings> settingsList, FrmMain mainForm) | ||||
|         //{ | ||||
|         //    ThreadSafeLogger.Log("开始执行“开启设备”流程..."); | ||||
|  | ||||
|         //    // 步骤 1: 初始化所有相机和UI窗口 | ||||
|         //    // Initialize 方法会负责 Shutdown 旧实例、创建新实例、打开硬件、显示窗口等。 | ||||
|         //    // 因为 Initialize 方法中的 displayForm.Show() 是非阻塞的,它会立即返回, | ||||
|         //    // 窗体的创建和显示过程会被调度到UI线程的消息队列中。 | ||||
|         //    Initialize(settingsList, mainForm); | ||||
|  | ||||
|         //    // 检查是否有任何相机成功初始化 | ||||
|         //    if (ActiveCameras.Count == 0) | ||||
|         //    { | ||||
|         //        ThreadSafeLogger.Log("“开启设备”流程中止,因为没有相机被成功初始化。"); | ||||
|         //        return; | ||||
|         //    } | ||||
|  | ||||
|         //    // 步骤 2: 在 Initialize 完成后(意味着所有窗口都已创建并 Show),开始采集 | ||||
|         //    // 这个调用会紧接着 Initialize 执行,此时窗体可能还在绘制过程中,但这没关系。 | ||||
|         //    // StartAll() 会启动后台线程开始拉取图像,一旦图像到达,就会通过事件推送到已经存在的窗体上。 | ||||
|         //    ThreadSafeLogger.Log("所有相机窗口已创建,现在开始采集图像..."); | ||||
|         //    StartAll(); | ||||
|  | ||||
|         //    ThreadSafeLogger.Log("“开启设备”流程已完成。相机正在采集中。"); | ||||
|         //} | ||||
|  | ||||
|         ///// <summary> | ||||
|         ///// 根据配置列表初始化或更新所有相机 | ||||
|         ///// </summary> | ||||
|         //public static void Initialize(List<CameraSettings> settingsList, FrmMain mainForm) | ||||
|         //{ | ||||
|  | ||||
|         //    // 先停止并释放所有旧的相机 | ||||
|         //    Shutdown(); | ||||
|         //    ThreadSafeLogger.Log("开始应用新的相机配置..."); | ||||
|  | ||||
|         //    EnabledCameraCount = settingsList.Count(s => s.IsEnabled); | ||||
|         //    if (EnabledCameraCount == 0) | ||||
|         //    { | ||||
|         //        ThreadSafeLogger.Log("没有启用的相机。"); | ||||
|         //        return; | ||||
|         //    } | ||||
|  | ||||
|         //    var deviceList = new HikvisionCamera().FindDevices();  | ||||
|         //    if (deviceList.Count == 0) | ||||
|         //    { | ||||
|         //        ThreadSafeLogger.Log("错误:未找到任何相机设备!"); | ||||
|         //        return; | ||||
|         //    } | ||||
|  | ||||
|         //    int deviceIndex = 0; | ||||
|         //    foreach (var setting in settingsList) | ||||
|         //    { | ||||
|         //        if (!setting.IsEnabled) continue; | ||||
|  | ||||
|         //        if (deviceIndex >= deviceList.Count) | ||||
|         //        { | ||||
|         //            ThreadSafeLogger.Log($"警告:相机配置'{setting.Name}'无法分配物理设备,因为设备数量不足。"); | ||||
|         //            continue; | ||||
|         //        } | ||||
|  | ||||
|         //        // --- 创建相机实例 --- | ||||
|         //        var cam = new HikvisionCamera { Name = setting.Name }; | ||||
|         //        cam.TriggerMode = setting.TriggerMode; | ||||
|         //        if (!cam.Open(setting)) | ||||
|         //        { | ||||
|         //            ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。"); | ||||
|         //            cam.Dispose(); | ||||
|         //            continue; | ||||
|         //        } | ||||
|         //        // --- 设置触发模式 --- | ||||
|         //        switch (setting.TriggerMode) | ||||
|         //        { | ||||
|         //            case TriggerModeType.Continuous: | ||||
|         //                cam.SetContinuousMode(); | ||||
|         //                break; | ||||
|         //            case TriggerModeType.Software: | ||||
|         //                cam.SetTriggerMode(true); | ||||
|         //                break; | ||||
|         //            case TriggerModeType.Hardware: | ||||
|         //                cam.SetTriggerMode(false); | ||||
|         //                break; | ||||
|         //        } | ||||
|  | ||||
|         //        // --- 订阅事件 --- | ||||
|         //        cam.ImageAcquired += OnCameraImageAcquired; | ||||
|         //        cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}"); | ||||
|  | ||||
|         //        // --- 创建显示窗口 --- | ||||
|         //        var displayForm = new FormImageDisplay { Text = setting.Name, CameraName = setting.Name }; | ||||
|         //        displayForm.OnDisplayEvent += ThreadSafeLogger.Log; | ||||
|         //        displayForm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document); | ||||
|  | ||||
|  | ||||
|         //        ActiveCameras.Add(setting.Name, cam); | ||||
|         //        CameraDisplays.Add(setting.Name, displayForm); | ||||
|         //        mainForm.AddCameraToStatusStrip(setting.Name); | ||||
|         //        ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。"); | ||||
|         //        deviceIndex++; | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。 | ||||
|         /// 这是“启动设备”的第一阶段。 | ||||
|         /// </summary> | ||||
|         public static void PrepareAll(ProcessConfig config, FrmMain mainForm) | ||||
|         { | ||||
|             // 1. 清理旧资源和UI | ||||
|             mainForm.ClearStatusStrip(); | ||||
|             Shutdown(); | ||||
|             ThreadSafeLogger.Log("开始准备设备和模型..."); | ||||
|  | ||||
|             // 2. 初始化检测协调器和AI模型 | ||||
|             // 注意:YoloModelManager 的 Initialize 现在也应在这里被调用,以确保逻辑集中 | ||||
|             YoloModelManager.Initialize(config.ModelSettings); | ||||
|             DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings); | ||||
|  | ||||
|             // 3. 创建相机硬件实例和UI窗口 | ||||
|             var deviceList = new HikvisionCamera().FindDevices(); | ||||
|             if (deviceList.Count == 0) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log("错误:未找到任何相机设备!"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled)) | ||||
|             { | ||||
|                 var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex }; | ||||
|                 cam.TriggerMode = setting.TriggerMode; | ||||
|                 if (!cam.Open(setting)) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。"); | ||||
|                     cam.Dispose(); | ||||
|                     continue; | ||||
|                 } | ||||
|                 // --- 设置触发模式 --- | ||||
|                 switch (setting.TriggerMode) | ||||
|                 { | ||||
|                     case TriggerModeType.Continuous: | ||||
|                         cam.SetContinuousMode(); | ||||
|                         break; | ||||
|                     case TriggerModeType.Software: | ||||
|                         cam.SetTriggerMode(true); | ||||
|                         break; | ||||
|                     case TriggerModeType.Hardware: | ||||
|                         cam.SetTriggerMode(false); | ||||
|                         break; | ||||
|                 } | ||||
|  | ||||
|                 // --- 订阅事件 --- | ||||
|                 cam.ImageAcquired += OnCameraImageAcquired; | ||||
|                 var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex); | ||||
|                 if (processor != null) { processor.OnProcessingCompleted += Processor_OnProcessingCompleted; } | ||||
|  | ||||
|                 // --- 创建【但不显示】图像的UI窗口 --- | ||||
|                 var originalDisplay = new FormImageDisplay { Text = $"{setting.Name} - 原图", CameraName = setting.Name }; | ||||
|                 var resultDisplay = new FormImageDisplay { Text = $"{setting.Name} - 结果", CameraName = setting.Name }; | ||||
|                 originalDisplay.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document); | ||||
|                 resultDisplay.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document); | ||||
|  | ||||
|                 // --- 保存引用 --- | ||||
|                 ActiveCameras.Add(setting.Name, cam); | ||||
|                 OriginalImageDisplays.Add(setting.Name, originalDisplay); | ||||
|                 ResultImageDisplays.Add(setting.Name, resultDisplay); | ||||
|                 mainForm.AddCameraToStatusStrip(setting.Name); | ||||
|             } | ||||
|             ThreadSafeLogger.Log("所有设备和模型已准备就绪。"); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 根据配置列表初始化或更新所有相机 | ||||
|         /// </summary> | ||||
|         public static void Initialize(ProcessConfig config, FrmMain mainForm) | ||||
|         { | ||||
|             mainForm?.ClearStatusStrip(); | ||||
|  | ||||
|             // 先停止并释放所有旧的相机 | ||||
|             Shutdown(); | ||||
|             ThreadSafeLogger.Log("开始应用新的相机配置..."); | ||||
|             // 2. 初始化新的检测协调器 | ||||
|             DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings); | ||||
|  | ||||
|             var deviceList = new HikvisionCamera().FindDevices(); | ||||
|             if (deviceList.Count == 0) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log("错误:未找到任何相机设备!"); | ||||
|                 return; | ||||
|             } | ||||
|             int deviceIndex = 0; | ||||
|             foreach (var device in config.ModelSettings.Where(s => s.IsEnabled)) | ||||
|             { | ||||
|                 if (!device.IsEnabled) continue; | ||||
|                 mainForm.AddCameraToStatusStrip(device.Name); | ||||
|             } | ||||
|             foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled)) | ||||
|             { | ||||
|                 if (!setting.IsEnabled) continue; | ||||
|  | ||||
|                 if (deviceIndex >= deviceList.Count) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"警告:相机配置'{setting.Name}'无法分配物理设备,因为设备数量不足。"); | ||||
|                     continue; | ||||
|                 } | ||||
|                 // --- 创建相机实例 --- | ||||
|                 var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex }; | ||||
|                 cam.TriggerMode = setting.TriggerMode; | ||||
|                 if (!cam.Open(setting)) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。"); | ||||
|                     cam.Dispose(); | ||||
|                     continue; | ||||
|                 } | ||||
|                 // --- 设置触发模式 --- | ||||
|                 switch (setting.TriggerMode) | ||||
|                 { | ||||
|                     case TriggerModeType.Continuous: | ||||
|                         cam.SetContinuousMode(); | ||||
|                         break; | ||||
|                     case TriggerModeType.Software: | ||||
|                         cam.SetTriggerMode(true); | ||||
|                         break; | ||||
|                     case TriggerModeType.Hardware: | ||||
|                         cam.SetTriggerMode(false); | ||||
|                         break; | ||||
|                 } | ||||
|  | ||||
|                 // --- 订阅事件 --- | ||||
|                 cam.ImageAcquired += OnCameraImageAcquired; | ||||
|                 cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}"); | ||||
|                 ActiveCameras.Add(setting.Name, cam); | ||||
|                 // --- 创建显示窗口 --- | ||||
|                 var displayForm = new FormImageDisplay { Text = $"{setting.Name} - 原图", CameraName = setting.Name }; | ||||
|                 var checkFrm = new FormImageDisplay { Text = $"{setting.Name} - 结果", CameraName = setting.Name }; | ||||
|                 displayForm.OnDisplayEvent += ThreadSafeLogger.Log; | ||||
|                 displayForm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document); | ||||
|                 checkFrm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document); | ||||
|  | ||||
|                 OriginalImageDisplays.Add(setting.Name, displayForm); | ||||
|                 ResultImageDisplays.Add(setting.Name, checkFrm); | ||||
|                 mainForm.AddCameraToStatusStrip(setting.Name); | ||||
|                 var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex); | ||||
|                 if (processor != null) | ||||
|                 { | ||||
|                     processor.OnProcessingCompleted += Processor_OnProcessingCompleted; | ||||
|                 } | ||||
|                 ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。"); | ||||
|                 deviceIndex++; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 启动硬件触发模拟器。 | ||||
|         /// </summary> | ||||
|         public static void StartHardwareTriggerSimulator() | ||||
|         { | ||||
|             if (IsHardwareTriggerSimulating) return; | ||||
|  | ||||
|             _hardwareTriggerSimulator.Interval = TriggerInterval; | ||||
|             _hardwareTriggerSimulator.Start(); | ||||
|             IsHardwareTriggerSimulating = true; | ||||
|             ThreadSafeLogger.Log($"硬件触发模拟器已启动,触发间隔: {TriggerInterval} ms。"); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 停止硬件触发模拟器。 | ||||
|         /// </summary> | ||||
|         public static void StopHardwareTriggerSimulator() | ||||
|         { | ||||
|             if (!IsHardwareTriggerSimulating) return; | ||||
|  | ||||
|             _hardwareTriggerSimulator.Stop(); | ||||
|             IsHardwareTriggerSimulating = false; | ||||
|             ThreadSafeLogger.Log("硬件触发模拟器已停止。"); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 定时器触发事件。 | ||||
|         /// </summary> | ||||
|         private static void OnHardwareTriggerTimerElapsed(object sender, ElapsedEventArgs e) | ||||
|         { | ||||
|             // 遍历所有活动的相机 | ||||
|             foreach (var cam in ActiveCameras.Values) | ||||
|             { | ||||
|                 // 仅对配置为“硬件触发”模式的相机执行操作 | ||||
|                 // 重要:我们使用软触发命令(SoftwareTrigger)来“模拟”一个外部硬件信号的到达。 | ||||
|                 if (cam.TriggerMode == TriggerModeType.Software && cam.IsGrabbing) | ||||
|                 { | ||||
|                     // ThreadSafeLogger.Log($"模拟硬触发信号,触发相机: {cam.Name}"); // 如果需要详细日志可以取消注释 | ||||
|                     cam.SoftwareTrigger(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 所有启用的相机开始采集 | ||||
|         /// </summary> | ||||
|         public static void StartAll() | ||||
|         { | ||||
|             foreach (var cam in ActiveCameras.Values) | ||||
|             { | ||||
|                 cam.StartGrabbing(); | ||||
|             } | ||||
|             ThreadSafeLogger.Log("所有相机已开始采集。"); | ||||
|         } | ||||
|  | ||||
|         //public static void StartDetection() | ||||
|         //{ | ||||
|         //    if (!IsDetectionRunning) | ||||
|         //    { | ||||
|         //        IsDetectionRunning = true; | ||||
|         //        ThreadSafeLogger.Log("检测已启动,开始统计数据。"); | ||||
|         //    } | ||||
|         //} | ||||
|         //public static void StopDetection() | ||||
|         //{ | ||||
|         //    if (IsDetectionRunning) | ||||
|         //    { | ||||
|         //        IsDetectionRunning = false; | ||||
|         //        ThreadSafeLogger.Log("检测已停止。"); | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 所有启用的相机停止采集 | ||||
|         /// </summary> | ||||
|         public static void StopAll() | ||||
|         { | ||||
|             foreach (var cam in ActiveCameras.Values) | ||||
|             { | ||||
|                 cam.StopGrabbing(); | ||||
|             } | ||||
|             ThreadSafeLogger.Log("所有相机已停止采集。"); | ||||
|         } | ||||
|  | ||||
|         ///// <summary> | ||||
|         ///// 停止并释放所有相机资源 | ||||
|         ///// </summary> | ||||
|         //public static void Shutdown() | ||||
|         //{ | ||||
|         //    // --- 新增:确保在关闭时停止模拟器并释放资源 --- | ||||
|         //    StopHardwareTriggerSimulator(); | ||||
|         //    //_hardwareTriggerSimulator?.Dispose(); | ||||
|  | ||||
|         //    StopAll(); | ||||
|         //    foreach (var cam in ActiveCameras.Values) | ||||
|         //    { | ||||
|         //        cam.Dispose(); | ||||
|         //    } | ||||
|         //    foreach (var display in CameraDisplays.Values) | ||||
|         //    { | ||||
|         //        display.Close(); | ||||
|         //    } | ||||
|         //    lock (QueueLock) | ||||
|         //    { | ||||
|         //        while (ProductQueue.Count > 0) | ||||
|         //        { | ||||
|         //            ProductQueue.Dequeue()?.Dispose(); | ||||
|         //        } | ||||
|         //    } | ||||
|         //    ResetProductCounter(); | ||||
|         //    ActiveCameras.Clear(); | ||||
|         //    CameraDisplays.Clear(); | ||||
|         //    ThreadSafeLogger.Log("正在关闭文件日志记录器..."); | ||||
|         //    ThreadSafeLogger.Log("所有相机资源已释放。"); | ||||
|  | ||||
|         //} | ||||
|  | ||||
|         // Shutdown 方法也简化 | ||||
|         public static void Shutdown() | ||||
|         { | ||||
|             // 1. 停止硬件和模拟器 | ||||
|             StopAll(); | ||||
|             StopHardwareTriggerSimulator(); | ||||
|             // 2. 关闭相机实例和窗口 | ||||
|             foreach (var cam in ActiveCameras.Values) { cam.Dispose(); } | ||||
|             foreach (var display in OriginalImageDisplays.Values) { display.Close(); } | ||||
|             foreach (var display in ResultImageDisplays.Values) { display.Close(); } | ||||
|             ActiveCameras.Clear(); | ||||
|             OriginalImageDisplays.Clear(); | ||||
|             ResultImageDisplays.Clear(); | ||||
|             // 3. 关闭检测协调器,它会负责清理所有后台线程和队列 | ||||
|             DetectionCoordinator.Shutdown(); | ||||
|  | ||||
|             YoloModelManager.Shutdown(); | ||||
|  | ||||
|             ThreadSafeLogger.Log("所有相机及协调器资源已释放。"); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 重置产品计数器的公共方法 | ||||
|         /// </summary> | ||||
|         public static void ResetProductCounter() | ||||
|         { | ||||
|             lock (_counterLock) | ||||
|             { | ||||
|                 _productCounter = 0; | ||||
|             } | ||||
|             ThreadSafeLogger.Log("产品计数器已重置。"); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 接收到相机图像时的核心处理逻辑 | ||||
|         /// </summary> | ||||
|         //private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp) | ||||
|         //{ | ||||
|  | ||||
|         //    Bitmap bmpForDisplay =null; | ||||
|         //    Bitmap bmpForQueue = null; | ||||
|         //    try | ||||
|         //    { | ||||
|         //        bmpForDisplay?.Dispose(); | ||||
|         //        bmpForQueue?.Dispose(); | ||||
|         //        // 1. 为“显示”和“处理队列”创建独立的深克隆副本 | ||||
|         //        bmpForDisplay = DeepCloneBitmap(bmp, "Display"); | ||||
|         //        bmpForQueue = DeepCloneBitmap(bmp, "Queue"); | ||||
|         //    } | ||||
|         //    finally | ||||
|         //    { | ||||
|         //        // 【关键】无论克隆成功与否,都必须立即释放事件传递过来的原始bmp。 | ||||
|         //        // 这是保证没有泄漏的第一道防线。 | ||||
|         //        bmp?.Dispose(); | ||||
|         //    } | ||||
|         //    // --- 现在我们使用完全独立的副本进行后续操作 --- | ||||
|  | ||||
|         //    // 4. 将显示副本传递给UI | ||||
|         //    if (bmpForDisplay != null && CameraDisplays.TryGetValue(sender.Name, out var display)) | ||||
|         //    { | ||||
|         //        display.UpdateImage(bmpForDisplay); | ||||
|         //    } | ||||
|         //    else | ||||
|         //    { | ||||
|         //        // 如果不需要显示,或者显示失败,必须释放掉为它创建的副本 | ||||
|         //        bmpForDisplay?.Dispose(); | ||||
|         //    } | ||||
|         //    // 5. 将队列副本添加到产品中 | ||||
|         //    if (bmpForQueue != null) | ||||
|         //    { | ||||
|         //        lock (QueueLock) | ||||
|         //        { | ||||
|         //            ProductResult currentProduct; | ||||
|         //            bool isNewProductCycle = ProductQueue.Count == 0 || ProductQueue.Last().CapturedImages.ContainsKey(sender.Name); | ||||
|         //            if (isNewProductCycle) | ||||
|         //            { | ||||
|         //                if (ProductQueue.Count > 0 && !ProductQueue.Peek().IsComplete(EnabledCameraCount)) | ||||
|         //                { | ||||
|         //                    var orphanedProduct = ProductQueue.Dequeue(); // 从队列头部移除这个不完整的产品 | ||||
|         //                    ThreadSafeLogger.Log($"[警告] 产品 #{orphanedProduct.ProductID} 未能集齐所有相机图像而被丢弃,以防止内存泄漏。"); | ||||
|         //                    orphanedProduct.Dispose(); // 确保释放其占用的所有资源 | ||||
|         //                } | ||||
|         //                long newProductId; | ||||
|         //                lock (_counterLock) | ||||
|         //                { | ||||
|         //                    _productCounter++; | ||||
|         //                    newProductId = _productCounter; | ||||
|         //                } | ||||
|         //                currentProduct = new ProductResult(newProductId); | ||||
|         //                ProductQueue.Enqueue(currentProduct); | ||||
|         //            } | ||||
|         //            else | ||||
|         //            { | ||||
|         //                currentProduct = ProductQueue.Last(); | ||||
|         //            } | ||||
|  | ||||
|         //            currentProduct.AddImage(sender.Name, bmpForQueue); // bmpForQueue的所有权转移给了产品队列 | ||||
|  | ||||
|         //            while (ProductQueue.Count > 0 && ProductQueue.Peek().IsComplete(EnabledCameraCount)) | ||||
|         //            { | ||||
|         //                // Peek() 查看队头元素但不移除它 | ||||
|         //                var finishedProduct = ProductQueue.Dequeue(); // Dequeue() 移除队头元素 | ||||
|         //                ThreadSafeLogger.Log($"产品 #{finishedProduct.ProductID} 已完整,出队处理。"); | ||||
|  | ||||
|         //                // --- 这是您已有的处理逻辑 --- | ||||
|         //                bool isOk = new Random().NextDouble() > 0.1; | ||||
|         //                string finalResult = isOk ? "OK" : "NG"; | ||||
|         //                ThreadSafeLogger.Log($"产品 #{finishedProduct.ProductID} 检测结果: {finalResult}"); | ||||
|  | ||||
|         //                if (IsDetectionRunning) | ||||
|         //                { | ||||
|         //                    OnDetectionCompleted?.Invoke(null, new DetectionResultEventArgs(isOk)); | ||||
|         //                } | ||||
|  | ||||
|         //                try | ||||
|         //                { | ||||
|         //                    // 确保完成的产品被完全释放 | ||||
|         //                    finishedProduct.Dispose(); | ||||
|         //                } | ||||
|         //                catch (Exception ex) | ||||
|         //                { | ||||
|         //                    ThreadSafeLogger.Log($"[ERROR] 释放产品 #{finishedProduct.ProductID} 资源时出错: {ex.Message}"); | ||||
|         //                } | ||||
|         //            } | ||||
|         //        } | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|         // 图像回调方法现在极其简单 | ||||
|         private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp) | ||||
|         { | ||||
|             Bitmap bmpForDisplay = null; | ||||
|             Bitmap bmpForProcessing = null; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 // 1. 为“显示”和“处理”创建两个完全独立的深克隆副本 | ||||
|                 bmpForDisplay = DeepCloneBitmap(bmp, "Display"); | ||||
|                 bmpForProcessing = DeepCloneBitmap(bmp, "Processing"); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 // 2.无论克隆成功与否,都必须立即释放事件传递过来的原始bmp,防止泄漏。 | ||||
|                 bmp?.Dispose(); | ||||
|             } | ||||
|             // 分支 A: 将用于显示的副本发送到对应的UI窗口 | ||||
|             if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow)) | ||||
|             { | ||||
|                 // displayWindow.UpdateImage 会处理线程安全问题 | ||||
|                 displayWindow.UpdateImage(bmpForDisplay); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本 | ||||
|                 bmpForDisplay?.Dispose(); | ||||
|             } | ||||
|             // 分支 B: 将用于处理的副本发送到检测协调器的后台队列 | ||||
|             if (bmpForProcessing != null) | ||||
|             { | ||||
|                 // bmpForProcessing 的所有权在这里被转移给了协调器 | ||||
|                 DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing); | ||||
|             } | ||||
|             //// 深度克隆图像以确保线程安全 | ||||
|             //Bitmap bmpForProcessing = DeepCloneBitmap(bmp, "Processing"); | ||||
|             //bmp?.Dispose(); // 立即释放原始图 | ||||
|  | ||||
|             //// 直接将图像和相机编号交给协调器,无需任何本地处理 | ||||
|             //DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing); | ||||
|         } | ||||
|         // 事件处理器 | ||||
|         private static void Processor_OnProcessingCompleted(object sender, ProcessingCompletedEventArgs e) | ||||
|         { | ||||
|  | ||||
|             // 1. 找到与此相机匹配的相机名称 | ||||
|             var cameraEntry = ActiveCameras.FirstOrDefault(kvp => kvp.Value.CameraIndex == e.CameraIndex); | ||||
|             if (cameraEntry.Key == null) | ||||
|             { | ||||
|                 e.Dispose(); // 如果找不到接收者,必须释放事件参数中的图像 | ||||
|                 return; | ||||
|             } | ||||
|             // 2. 找到此相机的结果显示窗口 | ||||
|             if (ResultImageDisplays.TryGetValue(cameraEntry.Key, out var resultDisplay)) | ||||
|             { | ||||
|                 var bmp = ConvertSKImageToBitmap(e.ResultImage); | ||||
|                 if (bmp != null) | ||||
|                 { | ||||
|                     // UpdateImage 会负责克隆并显示,所以这里传递 bmp 即可 | ||||
|                     resultDisplay.UpdateImage(bmp); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // 如果找到了相机但没有对应的结果窗口,也要释放图像 | ||||
|                 e.Dispose(); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 【将 SkiaSharp.SKImage 安全地转换为 System.Drawing.Bitmap。 | ||||
|         /// </summary> | ||||
|         private static Bitmap ConvertSKImageToBitmap(SKImage skImage) | ||||
|         { | ||||
|             if (skImage == null) return null; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 // SKImage -> SKBitmap -> System.Drawing.Bitmap | ||||
|                 using (var skBitmap = SKBitmap.FromImage(skImage)) | ||||
|                 { | ||||
|                     // SKBitmap.ToBitmap() 会创建一个新的 Bitmap 对象 | ||||
|                     return SKBitmapToGdiBitmapFast(skBitmap); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] SKImage to Bitmap 转换失败: {ex.Message}"); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|         public static Bitmap SKBitmapToGdiBitmapFast(SKBitmap skBitmap) | ||||
|         { | ||||
|             if (skBitmap == null) throw new ArgumentNullException(nameof(skBitmap)); | ||||
|             if (skBitmap.ColorType != SKColorType.Bgra8888 || skBitmap.AlphaType != SKAlphaType.Premul) | ||||
|                 throw new ArgumentException("skBitmap must be Bgra8888 + Premul for the fast path."); | ||||
|  | ||||
|             int w = skBitmap.Width; | ||||
|             int h = skBitmap.Height; | ||||
|  | ||||
|             Bitmap bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb); | ||||
|             var rect = new Rectangle(0, 0, w, h); | ||||
|             var bmpData = bmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); | ||||
|             try | ||||
|             { | ||||
|                 IntPtr srcPtr = skBitmap.GetPixels(); | ||||
|                 int srcRowBytes = skBitmap.RowBytes; | ||||
|                 int dstRowBytes = bmpData.Stride; | ||||
|                 int copyBytesPerRow = Math.Min(srcRowBytes, dstRowBytes); | ||||
|  | ||||
|                 byte[] row = new byte[copyBytesPerRow]; // 复用同一行缓冲区,避免每行分配 | ||||
|                 for (int y = 0; y < h; y++) | ||||
|                 { | ||||
|                     IntPtr s = IntPtr.Add(srcPtr, y * srcRowBytes); | ||||
|                     IntPtr d = IntPtr.Add(bmpData.Scan0, y * dstRowBytes); | ||||
|  | ||||
|                     Marshal.Copy(s, row, 0, copyBytesPerRow); | ||||
|                     Marshal.Copy(row, 0, d, copyBytesPerRow); | ||||
|                 } | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 bmp.UnlockBits(bmpData); | ||||
|             } | ||||
|  | ||||
|             return bmp; | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 对Bitmap进行真正的深度克隆,确保内存完全独立。 | ||||
|         /// 这是解决跨线程GDI+问题的最可靠方法。 | ||||
|         /// </summary> | ||||
|         /// <param name="source">源Bitmap对象。</param> | ||||
|         /// <returns>一个与源图像在内存上完全独立的全新Bitmap对象。</returns> | ||||
|         private static Bitmap DeepCloneBitmap(Bitmap source, string cloneFor) | ||||
|         { | ||||
|             if (source == null) return null; | ||||
|  | ||||
|             // 创建一个新的Bitmap对象,具有与源相同的尺寸和像素格式 | ||||
|             Bitmap clone = new Bitmap(source.Width, source.Height, source.PixelFormat); | ||||
|  | ||||
|             // 如果有调色板,复制调色板 | ||||
|             if (source.Palette.Entries.Length > 0) | ||||
|             { | ||||
|                 clone.Palette = source.Palette; | ||||
|             } | ||||
|  | ||||
|             // 锁定源和目标Bitmap的内存区域 | ||||
|             var rect = new Rectangle(0, 0, source.Width, source.Height); | ||||
|             BitmapData sourceData = null; //source.LockBits(rect, ImageLockMode.ReadOnly, source.PixelFormat); | ||||
|             BitmapData cloneData = null;//clone.LockBits(rect, ImageLockMode.WriteOnly, clone.PixelFormat); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 sourceData = source.LockBits(rect, ImageLockMode.ReadOnly, source.PixelFormat); | ||||
|                 cloneData = clone.LockBits(rect, ImageLockMode.WriteOnly, clone.PixelFormat); | ||||
|                 // 计算需要拷贝的字节数 | ||||
|                 int byteCount = Math.Abs(sourceData.Stride) * source.Height; | ||||
|                 byte[] buffer = new byte[byteCount]; | ||||
|  | ||||
|                 // 从源图像拷贝数据到字节数组 | ||||
|                 Marshal.Copy(sourceData.Scan0, buffer, 0, byteCount); | ||||
|  | ||||
|                 // 从字节数组拷贝数据到目标图像 | ||||
|                 Marshal.Copy(buffer, 0, cloneData.Scan0, byteCount); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[克隆错误] 在 DeepCloneBitmap ({cloneFor}) 中发生异常: {ex.Message}"); | ||||
|                 // 如果发生错误,确保返回null,并且释放可能已经创建的clone对象 | ||||
|                 clone?.Dispose(); | ||||
|                 return null; | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 // 确保即使在发生错误时也能尝试解锁 | ||||
|                 if (sourceData != null) | ||||
|                 { | ||||
|                     source.UnlockBits(sourceData); | ||||
|                 } | ||||
|                 if (cloneData != null) | ||||
|                 { | ||||
|                     clone.UnlockBits(cloneData); | ||||
|                 } | ||||
|                 //ThreadSafeLogger.Log($"[克隆完成] 解锁并完成克隆 ({cloneFor})."); | ||||
|             } | ||||
|  | ||||
|             return clone; | ||||
|         } | ||||
|  | ||||
|         // 触发日志事件的辅助方法 | ||||
|         //public static void Log(string message) | ||||
|         //{ | ||||
|         //    string formattedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}"; | ||||
|  | ||||
|         //    // 1. 触发事件,更新UI | ||||
|         //    OnLogMessage?.Invoke(formattedMessage); | ||||
|  | ||||
|         //    // 2. 【关键修改】在写入文件前,先获取锁。 | ||||
|         //    // lock 语句块确保了花括号内的代码在同一时间只能被一个线程执行。 | ||||
|         //    // 如果另一个线程也想执行这段代码,它必须等待前一个线程执行完毕并释放锁。 | ||||
|         //    lock (_logLock) | ||||
|         //    { | ||||
|         //        try | ||||
|         //        { | ||||
|         //            _logFileWriter?.WriteLine(formattedMessage); | ||||
|         //        } | ||||
|         //        catch (Exception) | ||||
|         //        { | ||||
|         //            // 即使有锁,也保留try-catch以防万一(如磁盘满了等IO问题)。 | ||||
|         //            // 可以在这里决定是否要在UI上显示警告。 | ||||
|         //        } | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										211
									
								
								Check.Main/Camera/CameraProcessor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								Check.Main/Camera/CameraProcessor.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,211 @@ | ||||
| using Check.Main.Common; | ||||
| using Check.Main.Infer; | ||||
| using SkiaSharp; | ||||
| using System; | ||||
| using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | ||||
| using System.Drawing.Imaging; | ||||
| using System.Linq; | ||||
| using System.Runtime.InteropServices; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using YoloDotNet.Extensions; | ||||
|  | ||||
| namespace Check.Main.Camera | ||||
| { | ||||
|     public class CameraProcessor : IDisposable | ||||
|     { | ||||
|         private readonly int _cameraIndex; | ||||
|         private readonly int _modeId; | ||||
|         // private readonly ModelSettings _model; | ||||
|         private readonly BlockingCollection<ImageData> _imageQueue = new BlockingCollection<ImageData>(); | ||||
|         private readonly Thread _workerThread; | ||||
|         private volatile bool _isRunning = false; | ||||
|         private long _imageCounter = 0; | ||||
|         private readonly object _counterLock = new object(); // 用于线程安全地重置计数器 | ||||
|  | ||||
|         public event EventHandler<ProcessingCompletedEventArgs> OnProcessingCompleted; | ||||
|  | ||||
|  | ||||
|         public CameraProcessor(int cameraIndex, int modelId)//, ModelSettings model | ||||
|         { | ||||
|             _cameraIndex = cameraIndex; | ||||
|             _modeId = modelId; | ||||
|             //_model = model; | ||||
|             _workerThread = new Thread(ProcessQueue) { IsBackground = true, Name = $"Cam_{_cameraIndex}_Processor" }; | ||||
|         } | ||||
|  | ||||
|         public void Start() | ||||
|         { | ||||
|             _isRunning = true; | ||||
|             _workerThread.Start(); | ||||
|         } | ||||
|  | ||||
|         public void EnqueueImage(Bitmap bmp) | ||||
|         { | ||||
|             if (!_isRunning) | ||||
|             { | ||||
|                 bmp?.Dispose(); | ||||
|                 return; | ||||
|             } | ||||
|             _imageCounter++; | ||||
|             _imageQueue.Add(new ImageData(_imageCounter, _cameraIndex, bmp)); | ||||
|         } | ||||
|  | ||||
|         private void ProcessQueue() | ||||
|         { | ||||
|             // 从模型管理器获取此线程专属的YOLO模型 | ||||
|             var yoloModel = YoloModelManager.GetModel(_modeId); | ||||
|             if (yoloModel == null) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型,处理线程已中止。"); | ||||
|                 return; // 如果没有模型,此线程无法工作 | ||||
|             } | ||||
|             while (_isRunning) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     // 阻塞式地从队列中取出图像,如果队列为空则等待 | ||||
|                     ImageData data = _imageQueue.Take(); | ||||
|                     using (data) | ||||
|                     { | ||||
|                         //SKImage resultSkImage = null; // 用于存储最终绘制好结果的图像 | ||||
|  | ||||
|                         using (var skImage = ConvertBitmapToSKImage(data.Image)) // 转换图像格式并管理其生命周期 | ||||
|                         { | ||||
|                             if (skImage == null) continue; | ||||
|                             var predictions = yoloModel.RunObjectDetection(skImage); | ||||
|                             // 模拟模型处理 | ||||
|                             //Thread.Sleep(50); // 模拟耗时 | ||||
|                             //bool isOk = new Random().NextDouble() > 0.1; | ||||
|                             //string result = isOk ? "OK" : "NG"; | ||||
|  | ||||
|                             string result = predictions.Any() ? "NG" : "OK"; | ||||
|                             ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},检测到 {predictions.Count} 个目标,结果: {result}"); | ||||
|                             // 将处理结果交给协调器进行组装 | ||||
|                             DetectionCoordinator.AssembleProduct(data, result); | ||||
|  | ||||
|                             if (OnProcessingCompleted != null) | ||||
|                             { | ||||
|                                 using (var resultSkImage = skImage.Draw(predictions)) | ||||
|                                 { | ||||
|                                     // 4. 触发事件,将绘制好的 resultSkImage 传递出去 | ||||
|                                     // 所有权在这里被转移 | ||||
|                                     OnProcessingCompleted?.Invoke(this, new ProcessingCompletedEventArgs( | ||||
|                                         _cameraIndex, | ||||
|                                         data.ProductId, | ||||
|                                         resultSkImage | ||||
|                                     )); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                     } | ||||
|                 } | ||||
|                 catch (InvalidOperationException) | ||||
|                 { | ||||
|                     // 当调用 Stop 时,会 CompleteAdding 队列,Take 会抛出此异常,是正常退出流程 | ||||
|                     break; | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[ERROR] 相机 #{_cameraIndex} 处理线程异常: {ex.Message}"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 将 System.Drawing.Bitmap 安全地转换为 SkiaSharp.SKImage。 | ||||
|         /// </summary> | ||||
|         private SKImage ConvertBitmapToSKImage(Bitmap bitmap) | ||||
|         { | ||||
|             if (bitmap == null) return null; | ||||
|             try | ||||
|             { | ||||
|                 // 使用 using 确保 SKBitmap 被正确释放 | ||||
|                 using (var skBitmap = ToSKBitmapFast(bitmap)) | ||||
|                 { | ||||
|                     return SKImage.FromBitmap(skBitmap); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] Bitmap to SKImage 转换失败: {ex.Message}"); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|         public static SKBitmap ToSKBitmapFast(Bitmap bitmap) | ||||
|         { | ||||
|             // 确保是 32bppArgb(BGRA 内存布局) | ||||
|             Bitmap src = bitmap; | ||||
|             if (bitmap.PixelFormat != PixelFormat.Format32bppArgb) | ||||
|             { | ||||
|                 src = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb); | ||||
|                 using (Graphics g = Graphics.FromImage(src)) | ||||
|                 { | ||||
|                     g.DrawImage(bitmap, 0, 0, bitmap.Width, bitmap.Height); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             var rect = new Rectangle(0, 0, src.Width, src.Height); | ||||
|             var bmpData = src.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); | ||||
|             try | ||||
|             { | ||||
|                 var info = new SKImageInfo(src.Width, src.Height, SKColorType.Bgra8888, SKAlphaType.Premul); | ||||
|                 var skBitmap = new SKBitmap(info); | ||||
|  | ||||
|                 IntPtr destPtr = skBitmap.GetPixels();             // 目标内存 | ||||
|                 IntPtr srcRowPtr = bmpData.Scan0;                  // 源行首 | ||||
|                 int srcStride = bmpData.Stride; | ||||
|                 int destRowBytes = skBitmap.RowBytes; | ||||
|                 int copyBytesPerRow = Math.Min(srcStride, destRowBytes); | ||||
|  | ||||
|                 // 使用一次分配的缓冲区并用 Marshal.Copy 行拷贝(不分配每行) | ||||
|                 byte[] row = new byte[copyBytesPerRow]; | ||||
|                 for (int y = 0; y < src.Height; y++) | ||||
|                 { | ||||
|                     IntPtr s = IntPtr.Add(bmpData.Scan0, y * srcStride); | ||||
|                     IntPtr d = IntPtr.Add(destPtr, y * destRowBytes); | ||||
|  | ||||
|                     Marshal.Copy(s, row, 0, copyBytesPerRow); | ||||
|                     Marshal.Copy(row, 0, d, copyBytesPerRow); | ||||
|                 } | ||||
|  | ||||
|                 return skBitmap; | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 src.UnlockBits(bmpData); | ||||
|                 if (!ReferenceEquals(src, bitmap)) | ||||
|                 { | ||||
|                     src.Dispose(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         public void Stop() | ||||
|         { | ||||
|             _isRunning = false; | ||||
|             // 解除阻塞,让线程可以检查 _isRunning 标志并退出 | ||||
|             _imageQueue.CompleteAdding(); | ||||
|             _workerThread.Join(500); // 等待线程结束 | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 线程安全地重置该相机的图像计数器。 | ||||
|         /// </summary> | ||||
|         public void ResetCounter() | ||||
|         { | ||||
|             lock (_counterLock) | ||||
|             { | ||||
|                 _imageCounter = 0; | ||||
|             } | ||||
|         } | ||||
|         // 别忘了在 DetectionCoordinator 中添加一个辅助方法来获取处理器 | ||||
|  | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             Stop(); | ||||
|             _imageQueue.Dispose(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										182
									
								
								Check.Main/Camera/CameraSettings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								Check.Main/Camera/CameraSettings.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| using Check.Main.Common; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Drawing.Design; | ||||
| using System.Linq; | ||||
| using System.Runtime.CompilerServices; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms.Design; | ||||
|  | ||||
| namespace Check.Main.Camera | ||||
| { | ||||
|     // 相机触发方式枚举 | ||||
|     public enum TriggerModeType | ||||
|     { | ||||
|         [Description("连续采集")] | ||||
|         Continuous, | ||||
|         [Description("软件触发")] | ||||
|         Software, | ||||
|         [Description("硬件触发")] | ||||
|         Hardware | ||||
|     } | ||||
|     public enum CheckType | ||||
|     { | ||||
|         [Description("传统算法")] | ||||
|         Traditional, | ||||
|         [Description("深度学习")] | ||||
|         DeepLearning | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 相机配置信息类,用于PropertyGrid显示和编辑 | ||||
|     /// </summary> | ||||
|     public class CameraSettings : INotifyPropertyChanged, ICloneable | ||||
|     { | ||||
|         // 1. 实现 INotifyPropertyChanged 接口所要求的事件 | ||||
|         public event PropertyChangedEventHandler PropertyChanged; | ||||
|  | ||||
|         private int _cameraIndex = 0; | ||||
|         private string _name = "Camera-1"; | ||||
|         private string _ipAddress = "192.168.1.100"; | ||||
|         private string _ipDeviceAddress = "192.168.1.101"; | ||||
|         private TriggerModeType _triggerMode = TriggerModeType.Continuous; | ||||
|         private bool _isEnabled = true; | ||||
|         private CheckType _checkType = CheckType.DeepLearning; | ||||
|         private int _modelID = 0; | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("相机编号"), Description("相机的唯一标识符,用于与模型编号对应。")] | ||||
|         public int CameraIndex | ||||
|         { | ||||
|             get => _cameraIndex; | ||||
|             set | ||||
|             { | ||||
|                 if (_cameraIndex != value) | ||||
|                 { | ||||
|                     _cameraIndex = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         [Category("基本信息"), DisplayName("相机名称"), Description("给相机起一个唯一的别名")] | ||||
|         public string Name | ||||
|         { | ||||
|             get => _name; | ||||
|             set | ||||
|             { | ||||
|                 // 2. 在setter中检查值是否真的改变了 | ||||
|                 if (_name != value) | ||||
|                 { | ||||
|                     _name = value; | ||||
|                     // 3. 如果改变了,就调用通知方法,广播“Name”属性已变更的消息 | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("网络信息"), DisplayName("相机IP地址"), Description("相机的IP地址(GigE相机需要)")] | ||||
|         public string IPAddress | ||||
|         { | ||||
|             get => _ipAddress; | ||||
|             set | ||||
|             { | ||||
|                 if (_ipAddress != value) | ||||
|                 { | ||||
|                     _ipAddress = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("网络信息"), DisplayName("上位机IP地址"), Description("相机对应的上位机的IP地址(GigE相机需要)")] | ||||
|         public string IPDeviceAddress | ||||
|         { | ||||
|             get => _ipDeviceAddress; | ||||
|             set | ||||
|             { | ||||
|                 if (_ipDeviceAddress != value) | ||||
|                 { | ||||
|                     _ipDeviceAddress = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("采集控制"), DisplayName("触发模式"), Description("设置相机的图像采集触发方式")] | ||||
|         [TypeConverter(typeof(EnumDescriptionTypeConverter))] | ||||
|         public TriggerModeType TriggerMode | ||||
|         { | ||||
|             get => _triggerMode; | ||||
|             set | ||||
|             { | ||||
|                 if (_triggerMode != value) | ||||
|                 { | ||||
|                     _triggerMode = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("采集控制"), DisplayName("是否启用"), Description("是否在程序启动时初始化并使用此相机")] | ||||
|         public bool IsEnabled | ||||
|         { | ||||
|             get => _isEnabled; | ||||
|             set | ||||
|             { | ||||
|                 if (_isEnabled != value) | ||||
|                 { | ||||
|                     _isEnabled = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("检测配置"), DisplayName("检测类型"), Description("使用传统算法或深度学习")] | ||||
|         [TypeConverter(typeof(EnumDescriptionTypeConverter))] | ||||
|         public CheckType CheckType | ||||
|         { | ||||
|             get => _checkType; | ||||
|             set | ||||
|             { | ||||
|                 if (_checkType != value) | ||||
|                 { | ||||
|                     _checkType = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("模型编号"), Description("相机调用的模型编号")] | ||||
|         [TypeConverter(typeof(ModelSelectionConverter))] | ||||
|         public int ModelID | ||||
|         { | ||||
|             get => _modelID; | ||||
|             set | ||||
|             { | ||||
|                 if (_modelID != value) | ||||
|                 { | ||||
|                     _modelID = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 4. 触发 PropertyChanged 事件的辅助方法 | ||||
|         /// [CallerMemberName] 特性是一个“语法糖”,它会自动获取调用此方法的属性的名称。 | ||||
|         /// 例如,在Name属性的setter中调用OnPropertyChanged(),C#编译器会自动传入"Name"作为参数。 | ||||
|         /// </summary> | ||||
|         protected void OnPropertyChanged([CallerMemberName] string propertyName = null) | ||||
|         { | ||||
|             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||||
|         } | ||||
|         public object Clone() | ||||
|         { | ||||
|             // MemberwiseClone() 会创建一个新对象,并将当前对象的所有非静态字段的值 | ||||
|             // 复制到新对象中。对于值类型,这是值的拷贝;对于引用类型,这是引用的拷贝。 | ||||
|             // 在本类中,这已经足够了。 | ||||
|             return this.MemberwiseClone(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										502
									
								
								Check.Main/Camera/HikvisionCamera.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										502
									
								
								Check.Main/Camera/HikvisionCamera.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,502 @@ | ||||
| using MvCamCtrl.NET; | ||||
| using Sunny.UI; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Drawing; | ||||
| using System.Drawing.Imaging; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Runtime.InteropServices; | ||||
| using System.Text; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Camera | ||||
| { | ||||
|     public class HikvisionCamera : IDisposable | ||||
|     { | ||||
|         [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)] | ||||
|         private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); | ||||
|         // 事件委托 | ||||
|         public delegate void ImageAcquiredEventHandler(HikvisionCamera sender, Bitmap bmp); | ||||
|         // public delegate void ImageAcquiredEventHandler(object sender, Bitmap bmp); | ||||
|         public delegate void CameraMessageEventHandler(object sender, string message, int errorCode = 0); | ||||
|  | ||||
|         // 事件 | ||||
|         public event ImageAcquiredEventHandler ImageAcquired; | ||||
|         public event CameraMessageEventHandler CameraMessage; | ||||
|  | ||||
|         // 私有成员变量 | ||||
|         private MyCamera m_MyCamera; | ||||
|         private MyCamera.MV_CC_DEVICE_INFO_LIST m_stDeviceList; | ||||
|         private bool m_bGrabbing = false; | ||||
|         private Thread m_hReceiveThread = null; | ||||
|  | ||||
|         private Bitmap m_bitmap = null; | ||||
|         private IntPtr m_ConvertDstBuf = IntPtr.Zero; | ||||
|         private uint m_nConvertDstBufLen = 0; | ||||
|         public string Name { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 由外部逻辑分配的、唯一的相机软件编号。 | ||||
|         /// 主要用作图像路由和与逻辑处理器匹配的键。 | ||||
|         /// </summary> | ||||
|         public int CameraIndex { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 相机是否已经打开 | ||||
|         /// </summary> | ||||
|         public bool IsOpen { get; private set; } = false; | ||||
|  | ||||
|         public TriggerModeType TriggerMode { get; internal set; } // internal set 保证只有 CameraManager 能设置它 | ||||
|  | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 相机是否正在采集 | ||||
|         /// </summary> | ||||
|         public bool IsGrabbing => m_bGrabbing; | ||||
|  | ||||
|         public HikvisionCamera() | ||||
|         { | ||||
|             // 初始化SDK | ||||
|             MyCamera.MV_CC_Initialize_NET(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 查找所有可用的相机设备 | ||||
|         /// </summary> | ||||
|         /// <returns>设备名称列表</returns> | ||||
|         public List<string> FindDevices() | ||||
|         { | ||||
|             var deviceList = new List<string>(); | ||||
|             m_stDeviceList = new MyCamera.MV_CC_DEVICE_INFO_LIST(); | ||||
|             int nRet = MyCamera.MV_CC_EnumDevices_NET(MyCamera.MV_GIGE_DEVICE | MyCamera.MV_USB_DEVICE, ref m_stDeviceList); | ||||
|             if (nRet != MyCamera.MV_OK) | ||||
|             { | ||||
|                 OnCameraMessage("设备枚举失败!", nRet); | ||||
|                 return deviceList; | ||||
|             } | ||||
|  | ||||
|             for (int i = 0; i < m_stDeviceList.nDeviceNum; i++) | ||||
|             { | ||||
|                 var device = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(m_stDeviceList.pDeviceInfo[i], typeof(MyCamera.MV_CC_DEVICE_INFO)); | ||||
|                 if (device.nTLayerType == MyCamera.MV_GIGE_DEVICE) | ||||
|                 { | ||||
|                     var gigeInfo = (MyCamera.MV_GIGE_DEVICE_INFO_EX)MyCamera.ByteToStruct(device.SpecialInfo.stGigEInfo, typeof(MyCamera.MV_GIGE_DEVICE_INFO_EX)); | ||||
|                     if (!string.IsNullOrEmpty(Encoding.Default.GetString(gigeInfo.chUserDefinedName).TrimEnd('\0'))) | ||||
|                         deviceList.Add("GEV: " + gigeInfo.chUserDefinedName + " (" + gigeInfo.chSerialNumber + ")"); | ||||
|                     else | ||||
|                         deviceList.Add("GEV: " + gigeInfo.chManufacturerName + " " + gigeInfo.chModelName + " (" + gigeInfo.chSerialNumber + ")"); | ||||
|                 } | ||||
|                 else if (device.nTLayerType == MyCamera.MV_USB_DEVICE) | ||||
|                 { | ||||
|                     var usbInfo = (MyCamera.MV_USB3_DEVICE_INFO_EX)MyCamera.ByteToStruct(device.SpecialInfo.stUsb3VInfo, typeof(MyCamera.MV_USB3_DEVICE_INFO_EX)); | ||||
|                     if (!string.IsNullOrEmpty(Encoding.Default.GetString(usbInfo.chUserDefinedName).TrimEnd('\0'))) | ||||
|                         deviceList.Add("U3V: " + usbInfo.chUserDefinedName + " (" + usbInfo.chSerialNumber + ")"); | ||||
|                     else | ||||
|                         deviceList.Add("U3V: " + usbInfo.chManufacturerName + " " + usbInfo.chModelName + " (" + usbInfo.chSerialNumber + ")"); | ||||
|                 } | ||||
|             } | ||||
|             return deviceList; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 根据索引打开相机 | ||||
|         /// </summary> | ||||
|         /// <param name="index">设备索引</param> | ||||
|         /// <returns>成功返回true,否则返回false</returns> | ||||
|         public bool Open(CameraSettings camConfig) | ||||
|         { | ||||
|             string ipCam = ""; | ||||
|             string ipDevice = ""; | ||||
|             FindDevices(); | ||||
|             if (camConfig.IsEnabled == true) | ||||
|             { | ||||
|                 ipCam = camConfig.IPAddress; | ||||
|                 ipDevice = camConfig.IPDeviceAddress; | ||||
|             } | ||||
|             try | ||||
|             { | ||||
|                 if (m_stDeviceList.nDeviceNum == 0 || string.IsNullOrEmpty(camConfig.IPAddress)) | ||||
|                 { | ||||
|                     OnCameraMessage("没有找到设备或索引无效。", -1); | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 if (IsOpen) | ||||
|                 { | ||||
|                     OnCameraMessage("相机已经打开。", 0); | ||||
|                     return true; | ||||
|                 } | ||||
|                 MyCamera.MV_CC_DEVICE_INFO device = new MyCamera.MV_CC_DEVICE_INFO(); | ||||
|                 device.nTLayerType = MyCamera.MV_GIGE_DEVICE; | ||||
|                 MyCamera.MV_GIGE_DEVICE_INFO stGigEDev = new MyCamera.MV_GIGE_DEVICE_INFO(); | ||||
|                 var parts = ipCam.Split('.'); | ||||
|                 int nIp1 = Convert.ToInt32(parts[0]); | ||||
|                 int nIp2 = Convert.ToInt32(parts[1]); | ||||
|                 int nIp3 = Convert.ToInt32(parts[2]); | ||||
|                 int nIp4 = Convert.ToInt32(parts[3]); | ||||
|                 stGigEDev.nCurrentIp = (uint)((nIp1 << 24) | (nIp2 << 16) | (nIp3 << 8) | nIp4); | ||||
|  | ||||
|                 parts = ipDevice.Split('.'); | ||||
|                 nIp1 = Convert.ToInt32(parts[0]); | ||||
|                 nIp2 = Convert.ToInt32(parts[1]); | ||||
|                 nIp3 = Convert.ToInt32(parts[2]); | ||||
|                 nIp4 = Convert.ToInt32(parts[3]); | ||||
|                 stGigEDev.nNetExport = (uint)((nIp1 << 24) | (nIp2 << 16) | (nIp3 << 8) | nIp4); | ||||
|                 IntPtr stGigeInfoPtr = Marshal.AllocHGlobal(216); | ||||
|                 Marshal.StructureToPtr(stGigEDev, stGigeInfoPtr, false); | ||||
|                 device.SpecialInfo.stGigEInfo = new Byte[540]; | ||||
|                 Marshal.Copy(stGigeInfoPtr, device.SpecialInfo.stGigEInfo, 0, 540); | ||||
|                 //释放内存空间 | ||||
|                 Marshal.FreeHGlobal(stGigeInfoPtr); | ||||
|  | ||||
|                 //var device = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(m_stDeviceList.pDeviceInfo[index], typeof(MyCamera.MV_CC_DEVICE_INFO)); | ||||
|                 m_MyCamera = new MyCamera(); | ||||
|                 int nRet = m_MyCamera.MV_CC_CreateDevice_NET(ref device); | ||||
|                 if (nRet != MyCamera.MV_OK) | ||||
|                 { | ||||
|                     OnCameraMessage("创建设备句柄失败!", nRet); | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 nRet = m_MyCamera.MV_CC_OpenDevice_NET(); | ||||
|                 if (nRet != MyCamera.MV_OK) | ||||
|                 { | ||||
|                     OnCameraMessage("打开设备失败!", nRet); | ||||
|                     m_MyCamera.MV_CC_DestroyDevice_NET(); | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 // 探测网络最佳包大小(只对GigE相机有效) | ||||
|                 if (device.nTLayerType == MyCamera.MV_GIGE_DEVICE) | ||||
|                 { | ||||
|                     int nPacketSize = m_MyCamera.MV_CC_GetOptimalPacketSize_NET(); | ||||
|                     if (nPacketSize > 0) | ||||
|                     { | ||||
|                         m_MyCamera.MV_CC_SetIntValueEx_NET("GevSCPSPacketSize", nPacketSize); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // 默认设置为连续模式 | ||||
|                 SetContinuousMode(); | ||||
|  | ||||
|                 IsOpen = true; | ||||
|                 OnCameraMessage("相机打开成功。", 0); | ||||
|                 return true; | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 OnCameraMessage("相机打开失败。"+ ex.ToString(), 0); | ||||
|                 return false; | ||||
|             } | ||||
|              | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 关闭相机 | ||||
|         /// </summary> | ||||
|         public void Close() | ||||
|         { | ||||
|             if (!IsOpen) return; | ||||
|  | ||||
|             if (m_bGrabbing) | ||||
|             { | ||||
|                 StopGrabbing(); | ||||
|             } | ||||
|  | ||||
|             m_MyCamera.MV_CC_CloseDevice_NET(); | ||||
|             m_MyCamera.MV_CC_DestroyDevice_NET(); | ||||
|  | ||||
|             IsOpen = false; | ||||
|             m_MyCamera = null; | ||||
|             OnCameraMessage("相机已关闭。", 0); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 开始采集图像 | ||||
|         /// </summary> | ||||
|         /// <returns>成功返回true</returns> | ||||
|         public bool StartGrabbing() | ||||
|         { | ||||
|             if (!IsOpen || m_bGrabbing) | ||||
|             { | ||||
|                 OnCameraMessage(m_bGrabbing ? "已经在采集中。" : "相机未打开。", -1); | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             // 取图前的必要操作 | ||||
|             if (NecessaryOperBeforeGrab() != MyCamera.MV_OK) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             int nRet = m_MyCamera.MV_CC_StartGrabbing_NET(); | ||||
|             if (nRet != MyCamera.MV_OK) | ||||
|             { | ||||
|                 OnCameraMessage("开始采集失败!", nRet); | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             m_bGrabbing = true; | ||||
|             m_hReceiveThread = new Thread(ReceiveThreadProcess); | ||||
|             m_hReceiveThread.IsBackground = true; | ||||
|             m_hReceiveThread.Start(); | ||||
|  | ||||
|             OnCameraMessage("开始采集。", 0); | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 停止采集图像 | ||||
|         /// </summary> | ||||
|         public void StopGrabbing() | ||||
|         { | ||||
|             if (!m_bGrabbing) return; | ||||
|  | ||||
|             m_bGrabbing = false; | ||||
|             if (m_hReceiveThread != null && m_hReceiveThread.IsAlive) | ||||
|             { | ||||
|                 m_hReceiveThread.Join(1000); // 等待线程退出 | ||||
|             } | ||||
|  | ||||
|             int nRet = m_MyCamera.MV_CC_StopGrabbing_NET(); | ||||
|             if (nRet != MyCamera.MV_OK) | ||||
|             { | ||||
|                 OnCameraMessage("停止采集失败!", nRet); | ||||
|             } | ||||
|  | ||||
|             OnCameraMessage("停止采集。", 0); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 设置为连续采集模式 | ||||
|         /// </summary> | ||||
|         public void SetContinuousMode() | ||||
|         { | ||||
|             if (!IsOpen) return; | ||||
|             m_MyCamera.MV_CC_SetEnumValue_NET("AcquisitionMode", (uint)MyCamera.MV_CAM_ACQUISITION_MODE.MV_ACQ_MODE_CONTINUOUS); | ||||
|             m_MyCamera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_OFF); | ||||
|             OnCameraMessage("已设置为连续模式。", 0); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 设置为触发模式 | ||||
|         /// </summary> | ||||
|         /// <param name="isSoftwareTrigger">true为软触发, false为硬触发(Line0)</param> | ||||
|         public void SetTriggerMode(bool isSoftwareTrigger) | ||||
|         { | ||||
|             if (!IsOpen) return; | ||||
|  | ||||
|             m_MyCamera.MV_CC_SetEnumValue_NET("AcquisitionMode", (uint)MyCamera.MV_CAM_ACQUISITION_MODE.MV_ACQ_MODE_CONTINUOUS); // 触发模式也需要在Continuous模式下 | ||||
|             m_MyCamera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_ON); | ||||
|  | ||||
|             if (isSoftwareTrigger) | ||||
|             { | ||||
|                 m_MyCamera.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_SOFTWARE); | ||||
|                 OnCameraMessage("已设置为软触发模式。", 0); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 m_MyCamera.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_LINE0); | ||||
|                 OnCameraMessage("已设置为硬触发(Line0)模式。", 0); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 执行一次软触发 | ||||
|         /// </summary> | ||||
|         /// <returns>成功返回true</returns> | ||||
|         public bool SoftwareTrigger() | ||||
|         { | ||||
|             if (!IsOpen || !m_bGrabbing) | ||||
|             { | ||||
|                 OnCameraMessage("请先打开相机并开始采集。", -1); | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             int nRet = m_MyCamera.MV_CC_SetCommandValue_NET("TriggerSoftware"); | ||||
|             if (nRet != MyCamera.MV_OK) | ||||
|             { | ||||
|                 OnCameraMessage("软触发失败!", nRet); | ||||
|                 return false; | ||||
|             } | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 图像接收线程 | ||||
|         /// </summary> | ||||
|         private void ReceiveThreadProcess() | ||||
|         { | ||||
|             MyCamera.MV_FRAME_OUT stFrameInfo = new MyCamera.MV_FRAME_OUT(); | ||||
|             var stConvertParam = new MyCamera.MV_PIXEL_CONVERT_PARAM(); | ||||
|  | ||||
|             while (m_bGrabbing) | ||||
|             { | ||||
|                 int nRet = m_MyCamera.MV_CC_GetImageBuffer_NET(ref stFrameInfo, 1000); | ||||
|                 if (nRet == MyCamera.MV_OK) | ||||
|                 { | ||||
|                     Bitmap bmpForEvent = null; // 在 try 外部声明 | ||||
|                     try | ||||
|                     { | ||||
|                         // 初始化转换参数 | ||||
|                         stConvertParam.nWidth = stFrameInfo.stFrameInfo.nWidth; | ||||
|                         stConvertParam.nHeight = stFrameInfo.stFrameInfo.nHeight; | ||||
|                         stConvertParam.pSrcData = stFrameInfo.pBufAddr; | ||||
|                         stConvertParam.nSrcDataLen = stFrameInfo.stFrameInfo.nFrameLen; | ||||
|                         stConvertParam.enSrcPixelType = stFrameInfo.stFrameInfo.enPixelType; | ||||
|                         stConvertParam.pDstBuffer = m_ConvertDstBuf; | ||||
|                         stConvertParam.nDstBufferSize = m_nConvertDstBufLen; | ||||
|  | ||||
|                         // 创建用于事件的Bitmap,它的格式与类成员m_bitmap一致 | ||||
|                         bmpForEvent = new Bitmap((int)stConvertParam.nWidth, (int)stConvertParam.nHeight, m_bitmap.PixelFormat); | ||||
|  | ||||
|                         // 判断并设置目标像素格式 | ||||
|                         if (bmpForEvent.PixelFormat == PixelFormat.Format8bppIndexed) | ||||
|                         { | ||||
|                             stConvertParam.enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8; | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|                             stConvertParam.enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_BGR8_Packed; | ||||
|                         } | ||||
|  | ||||
|                         // 执行像素格式转换 | ||||
|                         nRet = m_MyCamera.MV_CC_ConvertPixelType_NET(ref stConvertParam); | ||||
|                         if (nRet == MyCamera.MV_OK) | ||||
|                         { | ||||
|                             // 转换成功,锁定位图内存并拷贝数据 | ||||
|                             BitmapData bmpData = bmpForEvent.LockBits(new Rectangle(0, 0, stConvertParam.nWidth, stConvertParam.nHeight), ImageLockMode.WriteOnly, bmpForEvent.PixelFormat); | ||||
|  | ||||
|                             // 使用更健壮的逐行拷贝 | ||||
|                             int pixelSize = (bmpForEvent.PixelFormat == PixelFormat.Format8bppIndexed) ? 1 : 3; | ||||
|                             for (int i = 0; i < bmpData.Height; ++i) | ||||
|                             { | ||||
|                                 IntPtr pDst = new IntPtr(bmpData.Scan0.ToInt64() + i * bmpData.Stride); | ||||
|                                 IntPtr pSrc = new IntPtr(m_ConvertDstBuf.ToInt64() + i * stConvertParam.nWidth * pixelSize); | ||||
|                                 CopyMemory(pDst, pSrc, (uint)(stConvertParam.nWidth * pixelSize)); | ||||
|                             } | ||||
|                             bmpForEvent.UnlockBits(bmpData); | ||||
|  | ||||
|                             // 触发图像采集事件,转移 bmpForEvent 的所有权 | ||||
|                             ImageAcquired?.Invoke(this, bmpForEvent); | ||||
|  | ||||
|                             // 【关键】所有权已转移,将本地引用设为null,防止它在finally块中被错误地释放 | ||||
|                             bmpForEvent = null; | ||||
|                         } | ||||
|                         // 如果转换失败,不执行任何操作,让 finally 块来处理 bmpForEvent 的释放 | ||||
|                     } | ||||
|                     finally | ||||
|                     { | ||||
|                         // 【关键】如果 bmpForEvent 不为 null,意味着它被成功创建, | ||||
|                         // 但没有被成功传递给事件处理器(可能因为转换失败或发生其他异常), | ||||
|                         // 在这里必须将其释放。 | ||||
|                         bmpForEvent?.Dispose(); | ||||
|  | ||||
|                         // 无论成功与否,都必须释放相机SDK的内部图像缓存 | ||||
|                         m_MyCamera.MV_CC_FreeImageBuffer_NET(ref stFrameInfo); | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     // 超时,继续等待 | ||||
|                     Thread.Sleep(5); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 取图前的内存和参数准备 | ||||
|         /// </summary> | ||||
|         private int NecessaryOperBeforeGrab() | ||||
|         { | ||||
|             var stWidth = new MyCamera.MVCC_INTVALUE_EX(); | ||||
|             int nRet = m_MyCamera.MV_CC_GetIntValueEx_NET("Width", ref stWidth); | ||||
|             if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取宽度失败!", nRet); return nRet; } | ||||
|  | ||||
|             var stHeight = new MyCamera.MVCC_INTVALUE_EX(); | ||||
|             nRet = m_MyCamera.MV_CC_GetIntValueEx_NET("Height", ref stHeight); | ||||
|             if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取高度失败!", nRet); return nRet; } | ||||
|  | ||||
|             var stPixelFormat = new MyCamera.MVCC_ENUMVALUE(); | ||||
|             nRet = m_MyCamera.MV_CC_GetEnumValue_NET("PixelFormat", ref stPixelFormat); | ||||
|             if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取像素格式失败!", nRet); return nRet; } | ||||
|  | ||||
|             PixelFormat bitmapPixelFormat; | ||||
|             // 判断当前像素格式是否为Mono | ||||
|             bool isMono = IsMono(stPixelFormat.nCurValue); | ||||
|  | ||||
|             if (isMono) | ||||
|             { | ||||
|                 bitmapPixelFormat = PixelFormat.Format8bppIndexed; | ||||
|                 m_nConvertDstBufLen = (uint)(stWidth.nCurValue * stHeight.nCurValue); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 bitmapPixelFormat = PixelFormat.Format24bppRgb; | ||||
|                 m_nConvertDstBufLen = (uint)(stWidth.nCurValue * stHeight.nCurValue * 3); | ||||
|             } | ||||
|  | ||||
|             // 分配转换后的图像数据缓存 | ||||
|             if (m_ConvertDstBuf != IntPtr.Zero) Marshal.FreeHGlobal(m_ConvertDstBuf); | ||||
|             m_ConvertDstBuf = Marshal.AllocHGlobal((int)m_nConvertDstBufLen); | ||||
|  | ||||
|             // 创建Bitmap对象 | ||||
|             if (m_bitmap != null) m_bitmap.Dispose(); | ||||
|             m_bitmap = new Bitmap((int)stWidth.nCurValue, (int)stHeight.nCurValue, bitmapPixelFormat); | ||||
|  | ||||
|             // 如果是Mono8格式,设置调色板 | ||||
|             if (isMono) | ||||
|             { | ||||
|                 ColorPalette palette = m_bitmap.Palette; | ||||
|                 for (int i = 0; i < 256; i++) palette.Entries[i] = Color.FromArgb(i, i, i); | ||||
|                 m_bitmap.Palette = palette; | ||||
|             } | ||||
|  | ||||
|             return MyCamera.MV_OK; | ||||
|         } | ||||
|  | ||||
|         private bool IsMono(uint enPixelType) | ||||
|         { | ||||
|             switch (enPixelType) | ||||
|             { | ||||
|                 case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8: | ||||
|                 case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono10: | ||||
|                 case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono10_Packed: | ||||
|                 case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono12: | ||||
|                 case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono12_Packed: | ||||
|                     return true; | ||||
|                 default: | ||||
|                     return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 触发相机消息事件 | ||||
|         /// </summary> | ||||
|         private void OnCameraMessage(string message, int errorCode = 0) | ||||
|         { | ||||
|             CameraMessage?.Invoke(this, message, errorCode); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 释放资源 | ||||
|         /// </summary> | ||||
|         public void Dispose() | ||||
|         { | ||||
|             Close(); | ||||
|             if (m_ConvertDstBuf != IntPtr.Zero) | ||||
|             { | ||||
|                 Marshal.FreeHGlobal(m_ConvertDstBuf); | ||||
|                 m_ConvertDstBuf = IntPtr.Zero; | ||||
|             } | ||||
|             if (m_bitmap != null) | ||||
|             { | ||||
|                 m_bitmap.Dispose(); | ||||
|                 m_bitmap = null; | ||||
|             } | ||||
|             MyCamera.MV_CC_Finalize_NET(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										37
									
								
								Check.Main/Check.Main.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								Check.Main/Check.Main.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <OutputType>WinExe</OutputType> | ||||
|     <TargetFramework>net8.0-windows</TargetFramework> | ||||
|     <Nullable>enable</Nullable> | ||||
|     <UseWindowsForms>true</UseWindowsForms> | ||||
|     <ImplicitUsings>enable</ImplicitUsings> | ||||
|   </PropertyGroup> | ||||
|   <PropertyGroup> | ||||
|     <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> | ||||
|     <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="DockPanelSuite" Version="3.1.1" /> | ||||
|     <PackageReference Include="DockPanelSuite.ThemeVS2015" Version="3.1.1" /> | ||||
|     <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | ||||
|     <PackageReference Include="NPOI" Version="2.7.4" /> | ||||
|     <PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" /> | ||||
|     <PackageReference Include="OpenCvSharp4.Extensions" Version="4.10.0.20241108" /> | ||||
|     <PackageReference Include="OpenCvSharp4.runtime.win" Version="4.10.0.20241108" /> | ||||
|     <PackageReference Include="SkiaSharp" Version="3.116.1" /> | ||||
|     <PackageReference Include="SunnyUI" Version="3.8.7" /> | ||||
|     <PackageReference Include="YoloDotNet" Version="2.3.0" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <Reference Include="HslCommunication"> | ||||
|       <HintPath>..\..\..\HslCommunication-master\HslCommunication.dll</HintPath> | ||||
|     </Reference> | ||||
|     <Reference Include="MvCameraControl.Net"> | ||||
|       <HintPath>C:\Program Files (x86)\MVS\Development\DotNet\win64\MvCameraControl.Net.dll</HintPath> | ||||
|     </Reference> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										124
									
								
								Check.Main/Common/ConfigurationManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								Check.Main/Common/ConfigurationManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| using Check.Main.Camera; | ||||
| using Check.Main.Dispatch; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Xml.Serialization; | ||||
|  | ||||
| namespace Check.Main.Common | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 静态全局配置管理器,作为配置数据的“单一数据源”。 | ||||
|     /// 负责加载、保存和通知配置变更。 | ||||
|     /// </summary> | ||||
|     public static class ConfigurationManager | ||||
|     { | ||||
|         //    private static readonly string _configFilePath = Path.Combine(Application.StartupPath, "main_config.xml"); | ||||
|         //    private static ProcessConfig _currentConfig; | ||||
|         //    private static readonly object _lock = new object(); | ||||
|  | ||||
|         //    /// <summary> | ||||
|         //    /// 当配置通过 SaveChanges() 方法保存后触发。 | ||||
|         //    /// 其他模块(如 FormControlPanel)可以订阅此事件以响应配置变更。 | ||||
|         //    /// </summary> | ||||
|         //    public static event Action OnConfigurationChanged; | ||||
|  | ||||
|         //    /// <summary> | ||||
|         //    /// 静态构造函数在类首次被访问时自动执行,确保配置只被加载一次。 | ||||
|         //    /// </summary> | ||||
|         //    static ConfigurationManager() | ||||
|         //    { | ||||
|         //        Load(); | ||||
|         //    } | ||||
|  | ||||
|         //    /// <summary> | ||||
|         //    /// 获取对当前配置对象的直接引用。 | ||||
|         //    /// PropertyGrid 可以直接绑定到这个对象进行编辑。 | ||||
|         //    /// </summary> | ||||
|         //    /// <returns>当前的 ProcessConfig 实例。</returns> | ||||
|         //    public static ProcessConfig GetCurrentConfig() | ||||
|         //    { | ||||
|         //        return _currentConfig; | ||||
|         //    } | ||||
|  | ||||
|         //    /// <summary> | ||||
|         //    /// 从 XML 文件加载配置。如果文件不存在或加载失败,则创建一个新的默认配置。 | ||||
|         //    /// </summary> | ||||
|         //    private static void Load() | ||||
|         //    { | ||||
|         //        lock (_lock) | ||||
|         //        { | ||||
|         //            if (File.Exists(_configFilePath)) | ||||
|         //            { | ||||
|         //                try | ||||
|         //                { | ||||
|         //                    XmlSerializer serializer = new XmlSerializer(typeof(ProcessConfig)); | ||||
|         //                    using (var fs = new FileStream(_configFilePath, FileMode.Open, FileAccess.Read)) | ||||
|         //                    { | ||||
|         //                        _currentConfig = (ProcessConfig)serializer.Deserialize(fs); | ||||
|         //                        ThreadSafeLogger.Log("主配置文件加载成功。"); | ||||
|         //                    } | ||||
|         //                } | ||||
|         //                catch (Exception ex) | ||||
|         //                { | ||||
|         //                    ThreadSafeLogger.Log("加载主配置失败: " + ex.Message + " 将使用默认配置。"); | ||||
|         //                    _currentConfig = new ProcessConfig(); | ||||
|         //                } | ||||
|         //            } | ||||
|         //            else | ||||
|         //            { | ||||
|         //                ThreadSafeLogger.Log("未找到主配置文件,将创建新的默认配置。"); | ||||
|         //                _currentConfig = new ProcessConfig(); | ||||
|         //            } | ||||
|         //        } | ||||
|         //    } | ||||
|  | ||||
|         //    /// <summary> | ||||
|         //    /// 将当前配置保存到文件,并通知所有监听者配置已更改。 | ||||
|         //    /// 这是响应UI变化的推荐方法。 | ||||
|         //    /// </summary> | ||||
|         //    public static void SaveChanges() | ||||
|         //    { | ||||
|         //        lock (_lock) | ||||
|         //        { | ||||
|         //            try | ||||
|         //            { | ||||
|         //                XmlSerializer serializer = new XmlSerializer(typeof(ProcessConfig)); | ||||
|         //                using (var fs = new FileStream(_configFilePath, FileMode.Create, FileAccess.Write)) | ||||
|         //                { | ||||
|         //                    serializer.Serialize(fs, _currentConfig); | ||||
|         //                } | ||||
|         //            } | ||||
|         //            catch (Exception ex) | ||||
|         //            { | ||||
|         //                ThreadSafeLogger.Log("保存主配置失败: " + ex.Message); | ||||
|         //            } | ||||
|         //        } | ||||
|  | ||||
|         //        // 在锁之外触发事件,以避免监听者中的代码导致死锁 | ||||
|         //        ThreadSafeLogger.Log("配置已保存,正在触发 OnConfigurationChanged 事件..."); | ||||
|         //        OnConfigurationChanged?.Invoke(); | ||||
|         //    } | ||||
|         // 不再自己管理配置,而是直接从 ProductManager 获取 | ||||
|         public static ProcessConfig GetCurrentConfig() | ||||
|         { | ||||
|             return ProductManager.CurrentConfig; | ||||
|         } | ||||
|  | ||||
|         public static void SaveChanges() | ||||
|         { | ||||
|             ProductManager.SaveCurrentProductConfig(); | ||||
|         } | ||||
|  | ||||
|         // OnConfigurationChanged 事件现在由 ProductManager.OnProductChanged 替代 | ||||
|         // 如果其他地方还依赖这个事件,可以做一个桥接: | ||||
|         public static event Action OnConfigurationChanged | ||||
|         { | ||||
|             add { ProductManager.OnProductChanged += value; } | ||||
|             remove { ProductManager.OnProductChanged -= value; } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										236
									
								
								Check.Main/Common/EasyE5Options.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								Check.Main/Common/EasyE5Options.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | ||||
| using NPOI.SS.Formula.Functions; | ||||
| using Sunny.UI; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Net.Sockets; | ||||
| using System.Runtime.CompilerServices; | ||||
| using System.Text; | ||||
| using System.Text.RegularExpressions; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Common | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 汇川 Easy 系列 PLC 客户端 (Modbus TCP) | ||||
|     /// 支持 D/M/X/Y 地址读写,整数、浮点数、字符串 | ||||
|     /// </summary> | ||||
|     public class EasyPlcClient : IDisposable | ||||
|     { | ||||
|         private readonly TcpClient _tcpClient = new(); | ||||
|         private NetworkStream _stream; | ||||
|         private ushort _transactionId = 0; | ||||
|         private readonly SemaphoreSlim _lock = new(1, 1); | ||||
|  | ||||
|         private readonly string _ip; | ||||
|         private readonly int _port; | ||||
|         private readonly byte _slaveId; | ||||
|  | ||||
|         public EasyPlcClient(string ip, int port = 502, byte slaveId = 1) | ||||
|         { | ||||
|             _ip = ip; | ||||
|             _port = port; | ||||
|             _slaveId = slaveId; | ||||
|         } | ||||
|  | ||||
|         public async Task ConnectAsync() | ||||
|         { | ||||
|             await _tcpClient.ConnectAsync(_ip, _port); | ||||
|             _stream = _tcpClient.GetStream(); | ||||
|         } | ||||
|  | ||||
|         #region ---- 基础 Modbus ---- | ||||
|         private ushort GetTransactionId() => | ||||
|             unchecked((ushort)Interlocked.Increment(ref Unsafe.As<ushort, int>(ref _transactionId))); | ||||
|  | ||||
|         private async Task<byte[]> SendAndReceiveAsync(byte[] pdu, ushort startAddr, ushort length = 1) | ||||
|         { | ||||
|             ushort tid = GetTransactionId(); | ||||
|             ushort lengthField = (ushort)(pdu.Length + 1); | ||||
|             byte[] mbap = { | ||||
|                 (byte)(tid >> 8), (byte)tid, | ||||
|                 0x00, 0x00, // Protocol = 0 | ||||
|                 (byte)(lengthField >> 8), (byte)lengthField, | ||||
|                 _slaveId | ||||
|             }; | ||||
|  | ||||
|             byte[] adu = new byte[mbap.Length + pdu.Length]; | ||||
|             Buffer.BlockCopy(mbap, 0, adu, 0, mbap.Length); | ||||
|             Buffer.BlockCopy(pdu, 0, adu, mbap.Length, pdu.Length); | ||||
|  | ||||
|             await _lock.WaitAsync(); | ||||
|             try | ||||
|             { | ||||
|                 await _stream.WriteAsync(adu, 0, adu.Length); | ||||
|  | ||||
|                 byte[] header = new byte[7]; | ||||
|                 await _stream.ReadAsync(header, 0, 7); | ||||
|                 int respLength = (header[4] << 8) + header[5]; | ||||
|                 byte[] resp = new byte[respLength - 1]; | ||||
|                 await _stream.ReadAsync(resp, 0, resp.Length); | ||||
|  | ||||
|                 return resp; | ||||
|             } | ||||
|             finally { _lock.Release(); } | ||||
|         } | ||||
|         #endregion | ||||
|  | ||||
|         #region ---- 地址解析 ---- | ||||
|         private void ParseAddress(string address, out string area, out ushort offset) | ||||
|         { | ||||
|             var letters = Regex.Match(address, @"^[A-Za-z]+").Value.ToUpper(); | ||||
|             var digits = Regex.Match(address, @"\d+").Value; | ||||
|  | ||||
|             if (string.IsNullOrEmpty(letters) || string.IsNullOrEmpty(digits)) | ||||
|                 throw new ArgumentException($"地址 {address} 无效"); | ||||
|  | ||||
|             area = letters; | ||||
|             offset = (ushort)(ushort.Parse(digits) - 1); | ||||
|  | ||||
|             // 👉 注意:这里的偏移需根据你的 PLC Modbus 地址表调整 | ||||
|             // 例如 D 区可能是 40001 起,M 区可能是 00001 起 | ||||
|             // 目前假设 D -> Holding Register, M/Y -> Coil, X -> Discrete Input | ||||
|         } | ||||
|         #endregion | ||||
|  | ||||
|         #region ---- 整数读写 ---- | ||||
|         public async Task WriteAsync(string address, int value) | ||||
|         { | ||||
|             ParseAddress(address, out string area, out ushort offset); | ||||
|  | ||||
|             switch (area) | ||||
|             { | ||||
|                 case "D": // 写寄存器 | ||||
|                     byte[] pdu = { | ||||
|                         0x06, | ||||
|                         (byte)(offset >> 8), (byte)offset, | ||||
|                         (byte)(value >> 8), (byte)value | ||||
|                     }; | ||||
|                     await SendAndReceiveAsync(pdu, offset); | ||||
|                     break; | ||||
|  | ||||
|                 case "M": | ||||
|                 case "Y": // 写单线圈 | ||||
|                     byte[] coil = { | ||||
|                         0x05, | ||||
|                         (byte)(offset >> 8), (byte)offset, | ||||
|                         (byte)(value != 0 ? 0xFF : 0x00), 0x00 | ||||
|                     }; | ||||
|                     await SendAndReceiveAsync(coil, offset); | ||||
|                     break; | ||||
|  | ||||
|                 default: | ||||
|                     throw new NotSupportedException($"写 {area} 区未支持"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<int> ReadAsync(string address) | ||||
|         { | ||||
|             ParseAddress(address, out string area, out ushort offset); | ||||
|  | ||||
|             switch (area) | ||||
|             { | ||||
|                 case "D": // Holding Register | ||||
|                     byte[] pdu = { 0x03, (byte)(offset >> 8), (byte)offset, 0x00, 0x01 }; | ||||
|                     var resp = await SendAndReceiveAsync(pdu, offset); | ||||
|                     return (resp[1] << 8) + resp[2]; | ||||
|  | ||||
|                 case "M": | ||||
|                 case "Y": // Coils | ||||
|                     byte[] pdu1 = { 0x01, (byte)(offset >> 8), (byte)offset, 0x00, 0x01 }; | ||||
|                     var resp1 = await SendAndReceiveAsync(pdu1, offset); | ||||
|                     return (resp1[2] & 0x01) != 0 ? 1 : 0; | ||||
|  | ||||
|                 case "X": // Inputs | ||||
|                     byte[] pdu2 = { 0x02, (byte)(offset >> 8), (byte)offset, 0x00, 0x01 }; | ||||
|                     var resp2 = await SendAndReceiveAsync(pdu2, offset); | ||||
|                     return (resp2[2] & 0x01) != 0 ? 1 : 0; | ||||
|  | ||||
|                 default: | ||||
|                     throw new NotSupportedException($"读 {area} 区未支持"); | ||||
|             } | ||||
|         } | ||||
|         #endregion | ||||
|  | ||||
|         #region ---- 浮点读写 (2寄存器) ---- | ||||
|         public async Task WriteFloatAsync(string address, float value) | ||||
|         { | ||||
|             ParseAddress(address, out string area, out ushort offset); | ||||
|             if (area != "D") throw new NotSupportedException("浮点仅支持 D 区"); | ||||
|  | ||||
|             byte[] bytes = BitConverter.GetBytes(value); | ||||
|             if (BitConverter.IsLittleEndian) Array.Reverse(bytes); | ||||
|  | ||||
|             byte[] pdu = { | ||||
|                 0x10, | ||||
|                 (byte)(offset >> 8), (byte)offset, | ||||
|                 0x00, 0x02, | ||||
|                 0x04, | ||||
|                 bytes[0], bytes[1], bytes[2], bytes[3] | ||||
|             }; | ||||
|             await SendAndReceiveAsync(pdu, offset, 2); | ||||
|         } | ||||
|  | ||||
|         public async Task<float> ReadFloatAsync(string address) | ||||
|         { | ||||
|             ParseAddress(address, out string area, out ushort offset); | ||||
|             if (area != "D") throw new NotSupportedException("浮点仅支持 D 区"); | ||||
|  | ||||
|             byte[] pdu = { 0x03, (byte)(offset >> 8), (byte)offset, 0x00, 0x02 }; | ||||
|             var resp = await SendAndReceiveAsync(pdu, offset, 2); | ||||
|  | ||||
|             byte[] data = { resp[1], resp[2], resp[3], resp[4] }; | ||||
|             if (BitConverter.IsLittleEndian) Array.Reverse(data); | ||||
|  | ||||
|             return BitConverter.ToSingle(data, 0); | ||||
|         } | ||||
|         #endregion | ||||
|  | ||||
|         #region ---- 字符串读写 ---- | ||||
|         public async Task WriteStringAsync(string address, string value, int maxLength) | ||||
|         { | ||||
|             ParseAddress(address, out string area, out ushort offset); | ||||
|             if (area != "D") throw new NotSupportedException("字符串仅支持 D 区"); | ||||
|  | ||||
|             byte[] strBytes = Encoding.ASCII.GetBytes(value); | ||||
|             if (strBytes.Length > maxLength) Array.Resize(ref strBytes, maxLength); | ||||
|  | ||||
|             // 每寄存器2字节 | ||||
|             int regCount = (strBytes.Length + 1) / 2; | ||||
|             byte[] data = new byte[regCount * 2]; | ||||
|             Array.Copy(strBytes, data, strBytes.Length); | ||||
|  | ||||
|             byte[] pdu = new byte[7 + data.Length]; | ||||
|             pdu[0] = 0x10; | ||||
|             pdu[1] = (byte)(offset >> 8); pdu[2] = (byte)offset; | ||||
|             pdu[3] = (byte)(regCount >> 8); pdu[4] = (byte)regCount; | ||||
|             pdu[5] = (byte)(data.Length); | ||||
|             Buffer.BlockCopy(data, 0, pdu, 6, data.Length); | ||||
|  | ||||
|             await SendAndReceiveAsync(pdu, offset, (ushort)regCount); | ||||
|         } | ||||
|  | ||||
|         public async Task<string> ReadStringAsync(string address, int length) | ||||
|         { | ||||
|             ParseAddress(address, out string area, out ushort offset); | ||||
|             if (area != "D") throw new NotSupportedException("字符串仅支持 D 区"); | ||||
|  | ||||
|             int regCount = (length + 1) / 2; | ||||
|             byte[] pdu = { 0x03, (byte)(offset >> 8), (byte)offset, (byte)(regCount >> 8), (byte)regCount }; | ||||
|             var resp = await SendAndReceiveAsync(pdu, offset, (ushort)regCount); | ||||
|  | ||||
|             byte[] data = new byte[resp[0]]; | ||||
|             Array.Copy(resp, 1, data, 0, data.Length); | ||||
|  | ||||
|             return Encoding.ASCII.GetString(data).TrimEnd('\0'); | ||||
|         } | ||||
|         #endregion | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             _stream?.Dispose(); | ||||
|             _tcpClient?.Close(); | ||||
|             _lock?.Dispose(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										58
									
								
								Check.Main/Common/ImageData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								Check.Main/Common/ImageData.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| using System; | ||||
| using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Common | ||||
| { | ||||
|     public class ImageData : IDisposable | ||||
|     { | ||||
|         public long ProductId { get; } | ||||
|         public int CameraIndex { get; } | ||||
|         public Bitmap Image { get; } | ||||
|  | ||||
|         public ImageData(long productId, int cameraIndex, Bitmap image) | ||||
|         { | ||||
|             ProductId = productId; | ||||
|             CameraIndex = cameraIndex; | ||||
|             Image = image; | ||||
|         } | ||||
|         public void Dispose() => Image?.Dispose(); | ||||
|     } | ||||
|  | ||||
|     // ProductAssembly.cs - 用于组装一个完整的产品 | ||||
|     public class ProductAssembly : IDisposable | ||||
|     { | ||||
|         public long ProductId { get; } | ||||
|         private readonly int _expectedCameraCount; | ||||
|         // 使用 ConcurrentDictionary 保证线程安全 | ||||
|         private readonly ConcurrentDictionary<int, string> _results = new ConcurrentDictionary<int, string>(); | ||||
|  | ||||
|         public ProductAssembly(long productId, int expectedCameraCount) | ||||
|         { | ||||
|             ProductId = productId; | ||||
|             _expectedCameraCount = expectedCameraCount; | ||||
|         } | ||||
|  | ||||
|         // 添加一个相机的检测结果 | ||||
|         public bool AddResult(int cameraIndex, string result) | ||||
|         { | ||||
|             _results.TryAdd(cameraIndex, result); | ||||
|             return _results.Count == _expectedCameraCount; | ||||
|         } | ||||
|  | ||||
|         // 获取最终的聚合结果 | ||||
|         public string GetFinalResult() | ||||
|         { | ||||
|             // 如果任何一个结果包含 "NG",则最终结果为 "NG" | ||||
|             return _results.Values.Any(r => r.Contains("NG")) ? "NG" : "OK"; | ||||
|         } | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             // 目前只持有结果字符串,无需释放资源 | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										114
									
								
								Check.Main/Common/ModelSelectionConverter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								Check.Main/Common/ModelSelectionConverter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using static System.ComponentModel.TypeConverter; | ||||
|  | ||||
| namespace Check.Main.Common | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 一个自定义的 TypeConverter,用于在 PropertyGrid 中为模型选择提供一个下拉列表。 | ||||
|     /// </summary> | ||||
|     public class ModelSelectionConverter : Int32Converter // 我们转换的是整数 (ModelId) | ||||
|     { | ||||
|         // 告诉 PropertyGrid,我们支持从一个标准值集合中进行选择 | ||||
|         public override bool GetStandardValuesSupported(ITypeDescriptorContext context) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         // 告诉 PropertyGrid,我们只允许用户从列表中选择,不允许手动输入 | ||||
|         public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         // 这是最核心的方法:提供标准值的列表 | ||||
|         public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 // 从中央配置管理器获取所有已配置的模型 | ||||
|                 var modelSettings = ConfigurationManager.GetCurrentConfig().ModelSettings; | ||||
|  | ||||
|                 if (modelSettings == null || modelSettings.Count == 0) | ||||
|                 { | ||||
|                     // 如果没有模型,返回一个包含默认值 "0"(代表“无”)的列表 | ||||
|                     return new StandardValuesCollection(new[] { 0 }); | ||||
|                 } | ||||
|  | ||||
|                 // 使用LINQ从模型列表中提取所有模型的ID | ||||
|                 // .Prepend(0) 在列表开头添加一个 "0",代表“未选择”的选项 | ||||
|                 var modelIds = modelSettings.Select(m => m.Id).Prepend(0).ToArray(); | ||||
|  | ||||
|                 return new StandardValuesCollection(modelIds); | ||||
|             } | ||||
|             catch | ||||
|             { | ||||
|                 // 如果在获取过程中发生任何异常,返回一个安全的默认值 | ||||
|                 return new StandardValuesCollection(new[] { 0 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // --- 为了让下拉列表显示模型名称而不是ID,我们还需要重写以下方法 --- | ||||
|  | ||||
|         public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) | ||||
|         { | ||||
|             return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); | ||||
|         } | ||||
|  | ||||
|         public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) | ||||
|         { | ||||
|             return destinationType == typeof(string) || base.CanConvertTo(context, destinationType); | ||||
|         } | ||||
|  | ||||
|         // 将模型ID转换为其对应的名称字符串,以便在UI上显示 | ||||
|         public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) | ||||
|         { | ||||
|             if (destinationType == typeof(string) && value is int modelId) | ||||
|             { | ||||
|                 if (modelId == 0) | ||||
|                 { | ||||
|                     return "未选择"; // 对ID为0的特殊处理 | ||||
|                 } | ||||
|  | ||||
|                 // 查找ID对应的模型名称 | ||||
|                 var model = ConfigurationManager.GetCurrentConfig() | ||||
|                                                .ModelSettings? | ||||
|                                                .FirstOrDefault(m => m.Id == modelId); | ||||
|  | ||||
|                 // 返回 "名称 (ID)" 的格式,更清晰 | ||||
|                 return model != null ? $"{model.Name} (ID: {model.Id})" : "未知模型"; | ||||
|             } | ||||
|             return base.ConvertTo(context, culture, value, destinationType); | ||||
|         } | ||||
|  | ||||
|         // 将用户在UI上选择的名称字符串,转换回其对应的模型ID,以便保存 | ||||
|         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) | ||||
|         { | ||||
|             if (value is string displayString) | ||||
|             { | ||||
|                 if (displayString == "未选择") | ||||
|                 { | ||||
|                     return 0; | ||||
|                 } | ||||
|  | ||||
|                 // 从 "名称 (ID: X)" 格式中解析出ID | ||||
|                 if (displayString.Contains("(ID: ") && displayString.EndsWith(")")) | ||||
|                 { | ||||
|                     var startIndex = displayString.IndexOf("(ID: ") + 5; | ||||
|                     var endIndex = displayString.Length - 1; | ||||
|                     var idString = displayString.Substring(startIndex, endIndex - startIndex); | ||||
|                     if (int.TryParse(idString, out int modelId)) | ||||
|                     { | ||||
|                         return modelId; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             return base.ConvertFrom(context, culture, value); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										161
									
								
								Check.Main/Common/ProcessConfig.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								Check.Main/Common/ProcessConfig.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| using Check.Main.Camera; | ||||
| using Check.Main.Infer; | ||||
| using Check.Main.UI; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Drawing.Design; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms.Design; | ||||
|  | ||||
| namespace Check.Main.Common | ||||
| { | ||||
|     public class ProcessConfig | ||||
|     { | ||||
|         private string _logPath = Path.Combine(Application.StartupPath, "Logs"); | ||||
|         private List<CameraSettings> _cameraSettings = new List<CameraSettings>(); | ||||
|         private List<ModelSettings> _modelaSettings = new List<ModelSettings>(); | ||||
|  | ||||
|  | ||||
|         [Category("常规设置"), DisplayName("日志路径"), Description("设置日志文件的保存目录。")] | ||||
|         [Editor(typeof(FolderNameEditor), typeof(UITypeEditor))] // 使用内置的文件夹选择器 | ||||
|         public string LogPath | ||||
|         { | ||||
|             get => _logPath; | ||||
|             set => _logPath = value; | ||||
|         } | ||||
|  | ||||
|         [Category("核心配置"), DisplayName("相机配置"), Description("点击 '...' 按钮来配置所有相机。")] | ||||
|         [Editor(typeof(CameraConfigEditor), typeof(UITypeEditor))] // 【关键】指定使用我们自定义的编辑器 | ||||
|         [TypeConverter(typeof(CameraConfigConverter))]           // 【关键】指定使用我们自定义的类型转换器来显示文本 | ||||
|         public List<CameraSettings> CameraSettings | ||||
|         { | ||||
|             get => _cameraSettings; | ||||
|             set => _cameraSettings = value; | ||||
|         } | ||||
|  | ||||
|         [Category("核心配置"), DisplayName("模型配置"), Description("点击 '...' 按钮来配置所有模型。")] | ||||
|         [Editor(typeof(ModelConfigEditor), typeof(UITypeEditor))] // 【关键】指定使用我们自定义的编辑器 | ||||
|         [TypeConverter(typeof(ModelConfigConverter))]           // 【关键】指定使用我们自定义的类型转换器来显示文本 | ||||
|         public List<ModelSettings> ModelSettings | ||||
|         { | ||||
|             get => _modelaSettings; | ||||
|             set => _modelaSettings = value; | ||||
|         } | ||||
|     } | ||||
|     /// <summary> | ||||
|     /// 自定义TypeConverter,用于在PropertyGrid中自定义显示相机列表的文本。 | ||||
|     /// </summary> | ||||
|     public class CameraConfigConverter : TypeConverter | ||||
|     { | ||||
|         public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) | ||||
|         { | ||||
|             // 声明可以将值转换为字符串 | ||||
|             return destinationType == typeof(string) || base.CanConvertTo(context, destinationType); | ||||
|         } | ||||
|  | ||||
|         public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) | ||||
|         { | ||||
|             // 如果目标类型是字符串,并且值是相机设置列表 | ||||
|             if (destinationType == typeof(string) && value is List<CameraSettings> list) | ||||
|             { | ||||
|                 // 返回自定义的描述性文本 | ||||
|                 return $"已配置 {list.Count} 个相机"; | ||||
|             } | ||||
|             return base.ConvertTo(context, culture, value, destinationType); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 自定义TypeConverter,用于在PropertyGrid中自定义显示相机列表的文本。 | ||||
|     /// </summary> | ||||
|     public class ModelConfigConverter : TypeConverter | ||||
|     { | ||||
|         public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) | ||||
|         { | ||||
|             // 声明可以将值转换为字符串 | ||||
|             return destinationType == typeof(string) || base.CanConvertTo(context, destinationType); | ||||
|         } | ||||
|  | ||||
|         public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) | ||||
|         { | ||||
|             // 如果目标类型是字符串,并且值是相机设置列表 | ||||
|             if (destinationType == typeof(string) && value is List<ModelSettings> list) | ||||
|             { | ||||
|                 // 返回自定义的描述性文本 | ||||
|                 return $"已配置 {list.Count} 个模型"; | ||||
|             } | ||||
|             return base.ConvertTo(context, culture, value, destinationType); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 自定义UITypeEditor,用于在点击 "..." 时打开相机配置窗体。 | ||||
|     /// </summary> | ||||
|     public class CameraConfigEditor : UITypeEditor | ||||
|     { | ||||
|         // 1. 设置编辑样式为模态对话框 | ||||
|         public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) | ||||
|         { | ||||
|             return UITypeEditorEditStyle.Modal; | ||||
|         } | ||||
|  | ||||
|         // 2. 重写编辑方法 | ||||
|         public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) | ||||
|         { | ||||
|             // 'value' 就是当前属性的值,即 List<CameraSettings> | ||||
|             var settingsList = value as List<CameraSettings>; | ||||
|  | ||||
|             // 使用 FrmCamConfig 作为对话框进行编辑 | ||||
|             // 我们需要给 FrmCamConfig 添加一个新的构造函数 | ||||
|             using (var camConfigForm = new FrmCamConfig(settingsList)) | ||||
|             { | ||||
|                 // 以对话框形式显示窗体 | ||||
|                 if (camConfigForm.ShowDialog() == DialogResult.OK) | ||||
|                 { | ||||
|                     // 如果用户点击了“确定”,返回修改后的新列表 | ||||
|                     return camConfigForm._settingsList; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // 如果用户点击了“取消”或直接关闭了窗口,返回原始值,不做任何更改 | ||||
|             return value; | ||||
|         } | ||||
|     } | ||||
|     /// <summary> | ||||
|     /// 【新增】自定义UITypeEditor,用于在点击 "..." 时打开模型配置窗体。 | ||||
|     /// </summary> | ||||
|     public class ModelConfigEditor : UITypeEditor | ||||
|     { | ||||
|         // 1. 设置编辑样式为模态对话框 | ||||
|         public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) | ||||
|         { | ||||
|             return UITypeEditorEditStyle.Modal; | ||||
|         } | ||||
|  | ||||
|         // 2. 重写编辑方法 | ||||
|         public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) | ||||
|         { | ||||
|             // 'value' 就是当前属性的值,即 List<CameraSettings> | ||||
|             var settingsList = value as List<ModelSettings>; | ||||
|  | ||||
|             // 使用ModelEditor 作为对话框进行编辑 | ||||
|             // 我们需要给 FrmCamConfig 添加一个新的构造函数 | ||||
|             using (var modelConfigForm = new ModelListEditor(settingsList)) | ||||
|             { | ||||
|                 // 以对话框形式显示窗体 | ||||
|                 if (modelConfigForm.ShowDialog() == DialogResult.OK) | ||||
|                 { | ||||
|                     // 如果用户点击了“确定”,返回修改后的新列表 | ||||
|                     return modelConfigForm._settingsList; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // 如果用户点击了“取消”或直接关闭了窗口,返回原始值,不做任何更改 | ||||
|             return value; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										29
									
								
								Check.Main/Common/ProcessingCompletedEventArgs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								Check.Main/Common/ProcessingCompletedEventArgs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| using SkiaSharp; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Common | ||||
| { | ||||
|     public class ProcessingCompletedEventArgs : EventArgs, IDisposable | ||||
|     { | ||||
|         public int CameraIndex { get; } | ||||
|         public long ProductId { get; } | ||||
|         public SKImage ResultImage { get; } // 【修改】携带已绘制好结果的SKImage | ||||
|  | ||||
|         public ProcessingCompletedEventArgs(int cameraIndex, long productId, SKImage resultImage) | ||||
|         { | ||||
|             CameraIndex = cameraIndex; | ||||
|             ProductId = productId; | ||||
|             ResultImage = resultImage; | ||||
|         } | ||||
|  | ||||
|         // 实现 IDisposable 以确保SKImage资源被释放 | ||||
|         public void Dispose() | ||||
|         { | ||||
|             ResultImage?.Dispose(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										80
									
								
								Check.Main/Common/StaticMethod.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								Check.Main/Common/StaticMethod.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Reflection; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Common | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 一个自定义的类型转换器,用于让PropertyGrid等控件显示枚举成员的Description特性,而不是成员名。 | ||||
|     /// </summary> | ||||
|     public class EnumDescriptionTypeConverter : EnumConverter | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// 构造函数,传入枚举类型。 | ||||
|         /// </summary> | ||||
|         /// <param name="type">要进行转换的枚举类型。</param> | ||||
|         public EnumDescriptionTypeConverter(Type type) : base(type) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 重写此方法,将枚举值转换为其描述文本。 | ||||
|         /// 这是从 "数据" -> "UI显示" 的过程。 | ||||
|         /// </summary> | ||||
|         public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) | ||||
|         { | ||||
|             // 如果目标类型是字符串,并且我们有一个有效的枚举值 | ||||
|             if (destinationType == typeof(string) && value != null) | ||||
|             { | ||||
|                 // 获取枚举值的字段信息 | ||||
|                 FieldInfo fi = value.GetType().GetField(value.ToString()); | ||||
|                 if (fi != null) | ||||
|                 { | ||||
|                     // 查找该字段上的DescriptionAttribute | ||||
|                     var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); | ||||
|  | ||||
|                     // 如果找到了Description特性,就返回它的描述文本;否则,返回基类的默认行为(即成员名) | ||||
|                     return ((attributes.Length > 0) && (!String.IsNullOrEmpty(attributes[0].Description))) | ||||
|                            ? attributes[0].Description | ||||
|                            : value.ToString(); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // 对于其他所有情况,使用基类的默认转换逻辑 | ||||
|             return base.ConvertTo(context, culture, value, destinationType); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 重写此方法,将描述文本转换回其对应的枚举值。 | ||||
|         /// 这是从 "UI显示" -> "数据" 的过程。 | ||||
|         /// </summary> | ||||
|         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) | ||||
|         { | ||||
|             // 如果传入的值是字符串 | ||||
|             if (value is string) | ||||
|             { | ||||
|                 // 遍历枚举类型的所有成员 | ||||
|                 foreach (FieldInfo fi in EnumType.GetFields()) | ||||
|                 { | ||||
|                     // 查找该成员上的DescriptionAttribute | ||||
|                     var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); | ||||
|  | ||||
|                     // 如果找到了Description,并且它的描述文本与传入的字符串匹配 | ||||
|                     if ((attributes.Length > 0) && (attributes[0].Description == (string)value)) | ||||
|                     { | ||||
|                         // 返回这个字段(成员)对应的枚举值 | ||||
|                         return Enum.Parse(EnumType, fi.Name); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // 如果没有找到匹配的描述,或者传入的不是字符串,使用基类的默认转换逻辑 | ||||
|             return base.ConvertFrom(context, culture, value); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										71
									
								
								Check.Main/Common/StatisticsData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								Check.Main/Common/StatisticsData.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Common | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 封装了所有生产统计数据的模型。 | ||||
|     /// 实现了INotifyPropertyChanged,未来可用于数据绑定。 | ||||
|     /// </summary> | ||||
|     public class StatisticsData : INotifyPropertyChanged | ||||
|     { | ||||
|         private int _goodCount; | ||||
|         private int _ngCount; | ||||
|  | ||||
|         public int GoodCount | ||||
|         { | ||||
|             get => _goodCount; | ||||
|             private set { _goodCount = value; OnPropertyChanged(nameof(GoodCount)); OnPropertyChanged(nameof(TotalCount)); OnPropertyChanged(nameof(YieldRate)); } | ||||
|         } | ||||
|  | ||||
|         public int NgCount | ||||
|         { | ||||
|             get => _ngCount; | ||||
|             private set { _ngCount = value; OnPropertyChanged(nameof(NgCount)); OnPropertyChanged(nameof(TotalCount)); OnPropertyChanged(nameof(YieldRate)); } | ||||
|         } | ||||
|  | ||||
|         public int TotalCount => GoodCount + NgCount; | ||||
|  | ||||
|         public double YieldRate => TotalCount == 0 ? 0 : (double)GoodCount / TotalCount; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 根据检测结果更新统计数据。 | ||||
|         /// </summary> | ||||
|         public void UpdateWithResult(bool isOk) | ||||
|         { | ||||
|             if (isOk) GoodCount++; | ||||
|             else NgCount++; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 重置所有统计数据。 | ||||
|         /// </summary> | ||||
|         public void Reset() | ||||
|         { | ||||
|             GoodCount = 0; | ||||
|             NgCount = 0; | ||||
|         } | ||||
|  | ||||
|         public event PropertyChangedEventHandler PropertyChanged; | ||||
|         protected void OnPropertyChanged(string propertyName) | ||||
|         { | ||||
|             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 用于在检测完成事件中传递结果的事件参数。 | ||||
|     /// </summary> | ||||
|     public class DetectionResultEventArgs : EventArgs | ||||
|     { | ||||
|         public bool IsOK { get; } | ||||
|         public DetectionResultEventArgs(bool isOk) | ||||
|         { | ||||
|             IsOK = isOk; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										82
									
								
								Check.Main/Common/StatisticsExporter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								Check.Main/Common/StatisticsExporter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| using Check.Main.Camera; | ||||
| using NPOI.SS.UserModel; | ||||
| using NPOI.XSSF.UserModel; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms; | ||||
|  | ||||
| namespace Check.Main.Common | ||||
| { | ||||
|     public static class StatisticsExporter | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// 将给定的统计数据导出到指定的Excel文件路径。 | ||||
|         /// </summary> | ||||
|         /// <param name="data">要导出的统计数据对象。</param> | ||||
|         /// <param name="customFileName">自定义的文件名部分(如 "Reset" 或 "Shutdown")。</param> | ||||
|         public static void ExportToExcel(StatisticsData data, string customFileName) | ||||
|         { | ||||
|             if (data.TotalCount == 0) return; // 如果没有数据,则不导出 | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 string directory = Path.Combine(Application.StartupPath, "Statistics"); | ||||
|                 Directory.CreateDirectory(directory); // 确保文件夹存在 | ||||
|  | ||||
|                 string fileName = $"Statistics_{customFileName}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.xlsx"; | ||||
|                 string filePath = Path.Combine(directory, fileName); | ||||
|  | ||||
|                 // 创建工作簿和工作表 | ||||
|                 IWorkbook workbook = new XSSFWorkbook(); | ||||
|                 ISheet sheet = workbook.CreateSheet("生产统计"); | ||||
|  | ||||
|                 // --- 创建样式 (可选,但能让表格更好看) --- | ||||
|                 IFont boldFont = workbook.CreateFont(); | ||||
|                 boldFont.IsBold = true; | ||||
|                 ICellStyle headerStyle = workbook.CreateCellStyle(); | ||||
|                 headerStyle.SetFont(boldFont); | ||||
|  | ||||
|                 // --- 创建表头 --- | ||||
|                 IRow headerRow = sheet.CreateRow(0); | ||||
|                 headerRow.CreateCell(0).SetCellValue("项目"); | ||||
|                 headerRow.CreateCell(1).SetCellValue("数值"); | ||||
|                 headerRow.GetCell(0).CellStyle = headerStyle; | ||||
|                 headerRow.GetCell(1).CellStyle = headerStyle; | ||||
|  | ||||
|                 // --- 填充数据 --- | ||||
|                 sheet.CreateRow(1).CreateCell(0).SetCellValue("良品数 (OK)"); | ||||
|                 sheet.GetRow(1).CreateCell(1).SetCellValue(data.GoodCount); | ||||
|  | ||||
|                 sheet.CreateRow(2).CreateCell(0).SetCellValue("不良品数 (NG)"); | ||||
|                 sheet.GetRow(2).CreateCell(1).SetCellValue(data.NgCount); | ||||
|  | ||||
|                 sheet.CreateRow(3).CreateCell(0).SetCellValue("产品总数"); | ||||
|                 sheet.GetRow(3).CreateCell(1).SetCellValue(data.TotalCount); | ||||
|  | ||||
|                 sheet.CreateRow(4).CreateCell(0).SetCellValue("良率 (Yield)"); | ||||
|                 sheet.GetRow(4).CreateCell(1).SetCellValue(data.YieldRate.ToString("P2")); | ||||
|  | ||||
|                 // 自动调整列宽 | ||||
|                 sheet.AutoSizeColumn(0); | ||||
|                 sheet.AutoSizeColumn(1); | ||||
|  | ||||
|                 // --- 写入文件 --- | ||||
|                 using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)) | ||||
|                 { | ||||
|                     workbook.Write(fs); | ||||
|                 } | ||||
|  | ||||
|                 ThreadSafeLogger.Log($"统计数据已成功导出到: {filePath}"); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[ERROR] 导出统计数据失败: {ex.Message}"); | ||||
|                 MessageBox.Show($"导出统计数据失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										112
									
								
								Check.Main/Common/ThreadSafeLogger.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								Check.Main/Common/ThreadSafeLogger.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| using System; | ||||
| using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Common | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 一个线程安全的、基于队列的日志记录器。 | ||||
|     /// 使用一个独立的后台线程来处理文件写入,避免业务线程阻塞。 | ||||
|     /// </summary> | ||||
|     public static class ThreadSafeLogger | ||||
|     { | ||||
|         // 使用线程安全的队列作为日志消息的缓冲区 | ||||
|         private static readonly BlockingCollection<string> _logQueue = new BlockingCollection<string>(); | ||||
|  | ||||
|         // 日志写入线程 | ||||
|         private static Thread _logWriterThread; | ||||
|  | ||||
|         private static StreamWriter _logFileWriter; | ||||
|  | ||||
|         // 事件,用于将格式化后的日志消息广播给UI等监听者 | ||||
|         public static event Action<string> OnLogMessage; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 初始化日志记录器,启动后台写入线程。 | ||||
|         /// </summary> | ||||
|         public static void Initialize() | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 string logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); | ||||
|                 Directory.CreateDirectory(logDirectory); | ||||
|  | ||||
|                 string logFileName = $"Log_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt"; | ||||
|                 string logFilePath = Path.Combine(logDirectory, logFileName); | ||||
|  | ||||
|                 _logFileWriter = new StreamWriter(logFilePath, append: true, encoding: Encoding.UTF8) { AutoFlush = true }; | ||||
|  | ||||
|                 // 创建并启动后台线程 | ||||
|                 _logWriterThread = new Thread(ProcessLogQueue) | ||||
|                 { | ||||
|                     IsBackground = true, // 设置为后台线程,这样主程序退出时它会自动终止 | ||||
|                     Name = "LogWriterThread" | ||||
|                 }; | ||||
|                 _logWriterThread.Start(); | ||||
|  | ||||
|                 Log("日志系统已初始化。"); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 // 如果初始化失败,尝试通过事件通知UI | ||||
|                 OnLogMessage?.Invoke($"[CRITICAL] 日志系统初始化失败: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 将一条日志消息添加到队列中。这个方法是线程安全的,且执行速度非常快。 | ||||
|         /// </summary> | ||||
|         /// <param name="message">原始日志消息。</param> | ||||
|         public static void Log(string message) | ||||
|         { | ||||
|             string formattedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}"; | ||||
|             _logQueue.Add(formattedMessage); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 后台线程的工作方法。它会持续不断地从队列中取出消息并处理。 | ||||
|         /// </summary> | ||||
|         private static void ProcessLogQueue() | ||||
|         { | ||||
|             // GetConsumingEnumerable会阻塞等待,直到有新的项加入队列或队列被标记为已完成 | ||||
|             foreach (string message in _logQueue.GetConsumingEnumerable()) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     // 1. 写入文件 | ||||
|                     _logFileWriter?.WriteLine(message); | ||||
|  | ||||
|                     // 2. 触发事件,通知UI | ||||
|                     OnLogMessage?.Invoke(message); | ||||
|                 } | ||||
|                 catch | ||||
|                 { | ||||
|                     // 忽略在日志线程本身发生的写入错误 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 关闭日志记录器,释放资源。 | ||||
|         /// </summary> | ||||
|         public static void Shutdown() | ||||
|         { | ||||
|             Log("日志系统正在关闭..."); | ||||
|  | ||||
|             // 标记队列不再接受新的项目。这会让ProcessLogQueue中的循环在处理完所有剩余项后自然结束。 | ||||
|             _logQueue.CompleteAdding(); | ||||
|  | ||||
|             // 等待日志线程处理完所有剩余的日志,最多等待2秒 | ||||
|             _logWriterThread?.Join(2000); | ||||
|  | ||||
|             // 关闭文件流 | ||||
|             _logFileWriter?.Close(); | ||||
|             _logFileWriter?.Dispose(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										243
									
								
								Check.Main/Dispatch/ProductManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								Check.Main/Dispatch/ProductManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,243 @@ | ||||
| using Check.Main.Common; | ||||
| using Newtonsoft.Json; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Text.RegularExpressions; | ||||
| using System.Threading.Tasks; | ||||
| using System.Xml; | ||||
| using System.Xml.Serialization; | ||||
|  | ||||
| namespace Check.Main.Dispatch | ||||
| { | ||||
|     public static class ProductManager | ||||
|     { | ||||
|         private static readonly string _productRootPath = Path.Combine(Application.StartupPath, "Product"); | ||||
|         private static readonly string _productListFilePath = Path.Combine(Application.StartupPath, "products.json"); | ||||
|  | ||||
|         // --- 公共属性和事件 --- | ||||
|         public static List<string> ProductList { get; private set; } = new List<string>(); | ||||
|         public static string CurrentProductName { get; private set; } | ||||
|         public static ProcessConfig CurrentConfig { get; private set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 当产品切换或配置加载后触发。 | ||||
|         /// </summary> | ||||
|         public static event Action OnProductChanged; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 静态构造函数,在程序启动时自动执行。 | ||||
|         /// </summary> | ||||
|         static ProductManager() | ||||
|         { | ||||
|             Directory.CreateDirectory(_productRootPath); // 确保Product文件夹存在 | ||||
|             LoadProductList(); | ||||
|             // 默认加载列表中的第一个产品,或者创建一个默认的 | ||||
|             SwitchToProduct(ProductList.FirstOrDefault() ?? "DefaultProduct"); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 切换到指定的产品,并加载其配置。 | ||||
|         /// </summary> | ||||
|         public static bool SwitchToProduct(string productName) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(productName)) return false; | ||||
|  | ||||
|             ThreadSafeLogger.Log($"正在切换到产品: {productName}"); | ||||
|             var config = LoadConfigForProduct(productName); | ||||
|             if (config == null) | ||||
|             { | ||||
|                 // 如果加载失败(例如新创建的产品还没有配置文件),则创建一个新的 | ||||
|                 config = new ProcessConfig(); | ||||
|             } | ||||
|  | ||||
|             CurrentProductName = productName; | ||||
|             CurrentConfig = config; | ||||
|  | ||||
|             // 广播产品已变更的通知 | ||||
|             OnProductChanged?.Invoke(); | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 添加一个新产品。 | ||||
|         /// </summary> | ||||
|         public static bool AddNewProduct(string newProductName) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(newProductName) || ProductList.Contains(newProductName)) | ||||
|             { | ||||
|                 return false; // 名称无效或已存在 | ||||
|             } | ||||
|  | ||||
|             // 创建一个全新的、空的配置 | ||||
|             var newConfig = new ProcessConfig(); | ||||
|  | ||||
|             // 保存这个新产品的空配置,这也会创建文件夹 | ||||
|             if (SaveConfigForProduct(newProductName, newConfig)) | ||||
|             { | ||||
|                 ProductList.Add(newProductName); | ||||
|                 SaveProductList(); // 更新产品列表文件 | ||||
|                 SwitchToProduct(newProductName); // 创建后立即切换到新产品 | ||||
|                 return true; | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 删除指定的产品及其所有相关配置。 | ||||
|         /// </summary> | ||||
|         /// <param name="productNameToDelete">要删除的产品名称。</param> | ||||
|         /// <returns>成功删除返回 true,否则返回 false。</returns> | ||||
|         public static bool DeleteProduct(string productNameToDelete) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(productNameToDelete) || !ProductList.Contains(productNameToDelete)) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[警告] 尝试删除一个不存在的产品: {productNameToDelete}"); | ||||
|                 return false; // 产品不存在 | ||||
|             } | ||||
|  | ||||
|             ThreadSafeLogger.Log($"正在删除产品: {productNameToDelete}..."); | ||||
|             try | ||||
|             { | ||||
|                 // 1. 从产品列表中移除 | ||||
|                 ProductList.Remove(productNameToDelete); | ||||
|                 SaveProductList(); // 更新列表文件 | ||||
|  | ||||
|                 // 2. 删除对应的配置文件夹 | ||||
|                 var safeFolderName = GetSafeFolderName(productNameToDelete); | ||||
|                 var productDir = Path.Combine(_productRootPath, safeFolderName); | ||||
|                 if (Directory.Exists(productDir)) | ||||
|                 { | ||||
|                     Directory.Delete(productDir, true); // true 表示递归删除所有子文件和子文件夹 | ||||
|                 } | ||||
|  | ||||
|                 ThreadSafeLogger.Log($"产品 '{productNameToDelete}' 已成功删除。"); | ||||
|  | ||||
|                 // 3. 如果被删除的是当前活动产品,则需要切换到一个新的有效产品 | ||||
|                 if (CurrentProductName == productNameToDelete) | ||||
|                 { | ||||
|                     // 切换到列表中的第一个产品,或者如果列表为空,则创建一个默认产品 | ||||
|                     string nextProduct = ProductList.FirstOrDefault() ?? "DefaultProduct"; | ||||
|                     SwitchToProduct(nextProduct); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     // 如果删除的不是当前产品,我们仍然需要触发一次事件, | ||||
|                     // 以便UI(主要是下拉列表)能够刷新它的数据源。 | ||||
|                     OnProductChanged?.Invoke(); | ||||
|                 } | ||||
|                 return true; | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] 删除产品 '{productNameToDelete}' 失败: {ex.Message}"); | ||||
|                 // 如果出错,最好把刚删除的项加回来,保持状态一致性 | ||||
|                 if (!ProductList.Contains(productNameToDelete)) | ||||
|                 { | ||||
|                     ProductList.Add(productNameToDelete); | ||||
|                 } | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 为当前产品保存更改。 | ||||
|         /// </summary> | ||||
|         public static void SaveCurrentProductConfig() | ||||
|         { | ||||
|             if (CurrentConfig != null && !string.IsNullOrWhiteSpace(CurrentProductName)) | ||||
|             { | ||||
|                 SaveConfigForProduct(CurrentProductName, CurrentConfig); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         #region 文件操作辅助方法 | ||||
|  | ||||
|         private static void LoadProductList() | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 if (File.Exists(_productListFilePath)) | ||||
|                 { | ||||
|                     var json = File.ReadAllText(_productListFilePath); | ||||
|                     ProductList = JsonConvert.DeserializeObject<List<string>>(json) ?? new List<string>(); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] 加载产品列表失败: {ex.Message}"); | ||||
|                 ProductList = new List<string>(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private static void SaveProductList() | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var json = JsonConvert.SerializeObject(ProductList, Newtonsoft.Json.Formatting.Indented); | ||||
|                 File.WriteAllText(_productListFilePath, json); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] 保存产品列表失败: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private static ProcessConfig LoadConfigForProduct(string productName) | ||||
|         { | ||||
|             var safeFolderName = GetSafeFolderName(productName); | ||||
|             var configPath = Path.Combine(_productRootPath, safeFolderName, "config.xml"); | ||||
|  | ||||
|             if (!File.Exists(configPath)) return null; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 XmlSerializer serializer = new XmlSerializer(typeof(ProcessConfig)); | ||||
|                 using (var fs = new FileStream(configPath, FileMode.Open, FileAccess.Read)) | ||||
|                 { | ||||
|                     return (ProcessConfig)serializer.Deserialize(fs); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] 加载产品 '{productName}' 的配置失败: {ex.Message}"); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private static bool SaveConfigForProduct(string productName, ProcessConfig config) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var safeFolderName = GetSafeFolderName(productName); | ||||
|                 var productDir = Path.Combine(_productRootPath, safeFolderName); | ||||
|                 Directory.CreateDirectory(productDir); // 确保文件夹存在 | ||||
|  | ||||
|                 var configPath = Path.Combine(productDir, "config.xml"); | ||||
|  | ||||
|                 XmlSerializer serializer = new XmlSerializer(typeof(ProcessConfig)); | ||||
|                 using (var fs = new FileStream(configPath, FileMode.Create, FileAccess.Write)) | ||||
|                 { | ||||
|                     serializer.Serialize(fs, config); | ||||
|                 } | ||||
|                 return true; | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] 保存产品 '{productName}' 的配置失败: {ex.Message}"); | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private static string GetSafeFolderName(string productName) | ||||
|         { | ||||
|             // 移除所有不适合做文件夹名称的字符 | ||||
|             string invalidChars = new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars()); | ||||
|             Regex r = new Regex(string.Format("[{0}]", Regex.Escape(invalidChars))); | ||||
|             return r.Replace(productName, ""); | ||||
|         } | ||||
|  | ||||
|         #endregion | ||||
|     } | ||||
| } | ||||
							
								
								
									
										180
									
								
								Check.Main/FrmMain.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								Check.Main/FrmMain.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| namespace Check.Main | ||||
| { | ||||
|     partial class FrmMain | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// 必需的设计器变量。 | ||||
|         /// </summary> | ||||
|         private System.ComponentModel.IContainer components = null; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 清理所有正在使用的资源。 | ||||
|         /// </summary> | ||||
|         /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param> | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             if (disposing && (components != null)) | ||||
|             { | ||||
|                 components.Dispose(); | ||||
|             } | ||||
|             base.Dispose(disposing); | ||||
|         } | ||||
|  | ||||
|         #region Windows 窗体设计器生成的代码 | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 设计器支持所需的方法 - 不要修改 | ||||
|         /// 使用代码编辑器修改此方法的内容。 | ||||
|         /// </summary> | ||||
|         private void InitializeComponent() | ||||
|         { | ||||
|             System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FrmMain)); | ||||
|             menuStrip1 = new MenuStrip(); | ||||
|             文件ToolStripMenuItem = new ToolStripMenuItem(); | ||||
|             退出ToolStripMenuItem = new ToolStripMenuItem(); | ||||
|             ToolStripMenuSaveLayou = new ToolStripMenuItem(); | ||||
|             视图ToolStripMenuItem = new ToolStripMenuItem(); | ||||
|             配置ToolStripMenuItem = new ToolStripMenuItem(); | ||||
|             日志ToolStripMenuItem = new ToolStripMenuItem(); | ||||
|             控制面板ToolStripMenuItem = new ToolStripMenuItem(); | ||||
|             statusStrip1 = new StatusStrip(); | ||||
|             uiTableLayoutPanel1 = new Sunny.UI.UITableLayoutPanel(); | ||||
|             dockPanel1 = new WeifenLuo.WinFormsUI.Docking.DockPanel(); | ||||
|             menuStrip1.SuspendLayout(); | ||||
|             uiTableLayoutPanel1.SuspendLayout(); | ||||
|             SuspendLayout(); | ||||
|             //  | ||||
|             // menuStrip1 | ||||
|             //  | ||||
|             menuStrip1.Items.AddRange(new ToolStripItem[] { 文件ToolStripMenuItem, 视图ToolStripMenuItem, 控制面板ToolStripMenuItem }); | ||||
|             menuStrip1.Location = new Point(0, 0); | ||||
|             menuStrip1.Name = "menuStrip1"; | ||||
|             menuStrip1.Padding = new Padding(7, 3, 0, 3); | ||||
|             menuStrip1.Size = new Size(933, 27); | ||||
|             menuStrip1.TabIndex = 0; | ||||
|             menuStrip1.Text = "MainMenu"; | ||||
|             //  | ||||
|             // 文件ToolStripMenuItem | ||||
|             //  | ||||
|             文件ToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { 退出ToolStripMenuItem, ToolStripMenuSaveLayou }); | ||||
|             文件ToolStripMenuItem.Name = "文件ToolStripMenuItem"; | ||||
|             文件ToolStripMenuItem.Size = new Size(44, 21); | ||||
|             文件ToolStripMenuItem.Text = "文件"; | ||||
|             //  | ||||
|             // 退出ToolStripMenuItem | ||||
|             //  | ||||
|             退出ToolStripMenuItem.Name = "退出ToolStripMenuItem"; | ||||
|             退出ToolStripMenuItem.Size = new Size(124, 22); | ||||
|             退出ToolStripMenuItem.Text = "退出"; | ||||
|             //  | ||||
|             // ToolStripMenuSaveLayou | ||||
|             //  | ||||
|             ToolStripMenuSaveLayou.Name = "ToolStripMenuSaveLayou"; | ||||
|             ToolStripMenuSaveLayou.Size = new Size(124, 22); | ||||
|             ToolStripMenuSaveLayou.Text = "保存布局"; | ||||
|             ToolStripMenuSaveLayou.Click += ToolStripMenuSaveLayou_Click; | ||||
|             //  | ||||
|             // 视图ToolStripMenuItem | ||||
|             //  | ||||
|             视图ToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { 配置ToolStripMenuItem, 日志ToolStripMenuItem }); | ||||
|             视图ToolStripMenuItem.Name = "视图ToolStripMenuItem"; | ||||
|             视图ToolStripMenuItem.Size = new Size(44, 21); | ||||
|             视图ToolStripMenuItem.Text = "视图"; | ||||
|             //  | ||||
|             // 配置ToolStripMenuItem | ||||
|             //  | ||||
|             配置ToolStripMenuItem.Name = "配置ToolStripMenuItem"; | ||||
|             配置ToolStripMenuItem.Size = new Size(180, 22); | ||||
|             配置ToolStripMenuItem.Text = "配置"; | ||||
|             配置ToolStripMenuItem.Click += 配置ToolStripMenuItem_Click; | ||||
|             //  | ||||
|             // 日志ToolStripMenuItem | ||||
|             //  | ||||
|             日志ToolStripMenuItem.Name = "日志ToolStripMenuItem"; | ||||
|             日志ToolStripMenuItem.Size = new Size(180, 22); | ||||
|             日志ToolStripMenuItem.Text = "日志"; | ||||
|             日志ToolStripMenuItem.Click += 日志ToolStripMenuItem_Click; | ||||
|             //  | ||||
|             // 控制面板ToolStripMenuItem | ||||
|             //  | ||||
|             控制面板ToolStripMenuItem.Name = "控制面板ToolStripMenuItem"; | ||||
|             控制面板ToolStripMenuItem.Size = new Size(68, 21); | ||||
|             控制面板ToolStripMenuItem.Text = "控制面板"; | ||||
|             控制面板ToolStripMenuItem.Click += 控制面板ToolStripMenuItem_Click; | ||||
|             //  | ||||
|             // statusStrip1 | ||||
|             //  | ||||
|             statusStrip1.Location = new Point(0, 616); | ||||
|             statusStrip1.Name = "statusStrip1"; | ||||
|             statusStrip1.Padding = new Padding(1, 0, 16, 0); | ||||
|             statusStrip1.Size = new Size(933, 22); | ||||
|             statusStrip1.TabIndex = 2; | ||||
|             statusStrip1.Text = "statusStrip1"; | ||||
|             //  | ||||
|             // uiTableLayoutPanel1 | ||||
|             //  | ||||
|             uiTableLayoutPanel1.ColumnCount = 2; | ||||
|             uiTableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 76F)); | ||||
|             uiTableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 24F)); | ||||
|             uiTableLayoutPanel1.Controls.Add(dockPanel1, 0, 0); | ||||
|             uiTableLayoutPanel1.Dock = DockStyle.Fill; | ||||
|             uiTableLayoutPanel1.Location = new Point(0, 27); | ||||
|             uiTableLayoutPanel1.Margin = new Padding(4); | ||||
|             uiTableLayoutPanel1.Name = "uiTableLayoutPanel1"; | ||||
|             uiTableLayoutPanel1.RowCount = 1; | ||||
|             uiTableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); | ||||
|             uiTableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); | ||||
|             uiTableLayoutPanel1.Size = new Size(933, 589); | ||||
|             uiTableLayoutPanel1.TabIndex = 3; | ||||
|             uiTableLayoutPanel1.TagString = null; | ||||
|             //  | ||||
|             // dockPanel1 | ||||
|             //  | ||||
|             uiTableLayoutPanel1.SetColumnSpan(dockPanel1, 2); | ||||
|             dockPanel1.Dock = DockStyle.Fill; | ||||
|             dockPanel1.Location = new Point(4, 4); | ||||
|             dockPanel1.Margin = new Padding(4); | ||||
|             dockPanel1.Name = "dockPanel1"; | ||||
|             dockPanel1.Size = new Size(925, 581); | ||||
|             dockPanel1.TabIndex = 0; | ||||
|             //  | ||||
|             // FrmMain | ||||
|             //  | ||||
|             AutoScaleDimensions = new SizeF(7F, 17F); | ||||
|             AutoScaleMode = AutoScaleMode.Font; | ||||
|             ClientSize = new Size(933, 638); | ||||
|             Controls.Add(uiTableLayoutPanel1); | ||||
|             Controls.Add(statusStrip1); | ||||
|             Controls.Add(menuStrip1); | ||||
|             Icon = (Icon)resources.GetObject("$this.Icon"); | ||||
|             MainMenuStrip = menuStrip1; | ||||
|             Margin = new Padding(4); | ||||
|             Name = "FrmMain"; | ||||
|             Text = "星科瑞升视觉检测设备"; | ||||
|             WindowState = FormWindowState.Maximized; | ||||
|             FormClosing += FrmMain_FormClosing; | ||||
|             Load += FrmMain_Load; | ||||
|             menuStrip1.ResumeLayout(false); | ||||
|             menuStrip1.PerformLayout(); | ||||
|             uiTableLayoutPanel1.ResumeLayout(false); | ||||
|             ResumeLayout(false); | ||||
|             PerformLayout(); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         #endregion | ||||
|  | ||||
|         private System.Windows.Forms.MenuStrip menuStrip1; | ||||
|         private System.Windows.Forms.StatusStrip statusStrip1; | ||||
|         private System.Windows.Forms.ToolStripMenuItem 文件ToolStripMenuItem; | ||||
|         private System.Windows.Forms.ToolStripMenuItem 退出ToolStripMenuItem; | ||||
|         private System.Windows.Forms.ToolStripMenuItem 视图ToolStripMenuItem; | ||||
|         private System.Windows.Forms.ToolStripMenuItem 配置ToolStripMenuItem; | ||||
|         private System.Windows.Forms.ToolStripMenuItem 日志ToolStripMenuItem; | ||||
|         private Sunny.UI.UITableLayoutPanel uiTableLayoutPanel1; | ||||
|         private WeifenLuo.WinFormsUI.Docking.DockPanel dockPanel1; | ||||
|         private System.Windows.Forms.ToolStripMenuItem 控制面板ToolStripMenuItem; | ||||
|         private System.Windows.Forms.ToolStripMenuItem ToolStripMenuSaveLayou; | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										297
									
								
								Check.Main/FrmMain.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								Check.Main/FrmMain.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,297 @@ | ||||
| using Check.Main.Camera; | ||||
| using Check.Main.Common; | ||||
| using Check.Main.UI; | ||||
| using HslCommunication; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Data; | ||||
| using System.Drawing; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms; | ||||
| using WeifenLuo.WinFormsUI.Docking; | ||||
|  | ||||
| namespace Check.Main | ||||
| { | ||||
|     public partial class FrmMain : Form | ||||
|     { | ||||
|         // private FrmCamConfig _formCameraConfig; | ||||
|         private FrmConfig _frmConfig; | ||||
|         private FrmLog _formLog; | ||||
|         private ThemeBase _theme = new VS2015BlueTheme(); // 外观主题 | ||||
|         public DockPanel MainDockPanel => this.dockPanel1; | ||||
|  | ||||
|         private FormControlPanel _formControlPanel; | ||||
|  | ||||
|         private FormStatistics _formStatistics; | ||||
|  | ||||
|         private readonly string _layoutConfigFile = Path.Combine(Application.StartupPath, "layout.xml"); | ||||
|         //用于反序列化时创建窗体的委托 | ||||
|         private DeserializeDockContent _deserializeDockContent; | ||||
|  | ||||
|         public FrmMain() | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|             dockPanel1.Theme = _theme; | ||||
|             IsMdiContainer = true; | ||||
|             _deserializeDockContent = new DeserializeDockContent(GetContentFromPersistString); | ||||
|         } | ||||
|  | ||||
|         private void FrmMain_Load(object sender, EventArgs e) | ||||
|         { | ||||
|             EasyPlcClient easyPlcClient = new EasyPlcClient("127.0.0.1", 502, 1); | ||||
|             easyPlcClient.ConnectAsync(); | ||||
|  | ||||
|             _frmConfig = new FrmConfig { Text = "主程序配置" }; | ||||
|             _formLog = new FrmLog { Text = "运行日志" }; | ||||
|             _formStatistics = new FormStatistics { Text = "生产统计" }; | ||||
|             _formControlPanel = new FormControlPanel { Text = "控制面板" }; | ||||
|             // 为每个子窗体订阅 FormClosing 事件 | ||||
|             _frmConfig.FormClosing += DockContent_FormClosing; | ||||
|             _formLog.FormClosing += DockContent_FormClosing; | ||||
|             _formStatistics.FormClosing += DockContent_FormClosing; | ||||
|             _formControlPanel.FormClosing += DockContent_FormClosing; | ||||
|  | ||||
|             ThreadSafeLogger.Initialize(); | ||||
|             ThreadSafeLogger.OnLogMessage += (msg) => { _formLog.AddLog(msg); }; | ||||
|  | ||||
|             // 2. 尝试加载布局文件 | ||||
|             if (File.Exists(_layoutConfigFile)) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     // 使用委托加载布局 | ||||
|                     dockPanel1.LoadFromXml(_layoutConfigFile, _deserializeDockContent); | ||||
|                     ThreadSafeLogger.Log("成功加载用户布局。"); | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"加载布局失败: {ex.Message}。将使用默认布局。"); | ||||
|                     // 如果加载失败,则使用默认布局 | ||||
|                     ShowDefaultLayout(); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // 3. 如果布局文件不存在,则显示默认布局 | ||||
|                 ThreadSafeLogger.Log("未找到布局文件,使用默认布局。"); | ||||
|                 ShowDefaultLayout(); | ||||
|             } | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 将相机名称添加到状态栏 | ||||
|         /// </summary> | ||||
|         public void AddCameraToStatusStrip(string name) | ||||
|         { | ||||
|             if (statusStrip1.InvokeRequired) | ||||
|             { | ||||
|                 statusStrip1.Invoke(new Action(() => AddCameraToStatusStrip(name))); | ||||
|                 return; | ||||
|             } | ||||
|             var label = new ToolStripStatusLabel(name) | ||||
|             { | ||||
|                 Name = "status_" + name, | ||||
|                 BorderSides = ToolStripStatusLabelBorderSides.All, | ||||
|                 Spring = false, | ||||
|             }; | ||||
|             label.Click += StatusLabel_Click; | ||||
|             statusStrip1.Items.Add(label); | ||||
|         } | ||||
|         private void 控制面板ToolStripMenuItem_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             if (_formControlPanel == null || _formControlPanel.IsDisposed) | ||||
|             { | ||||
|                 _formControlPanel = new FormControlPanel { Text = "控制面板" }; | ||||
|                 _formControlPanel.FormClosing += DockContent_FormClosing; | ||||
|             } | ||||
|             _formControlPanel.Show(this.dockPanel1); | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 清空状态栏中的相机名称 | ||||
|         /// </summary> | ||||
|         public void ClearStatusStrip() | ||||
|         { | ||||
|             if (statusStrip1.InvokeRequired) | ||||
|             { | ||||
|                 statusStrip1.Invoke(new Action(ClearStatusStrip)); | ||||
|                 return; | ||||
|             } | ||||
|             // 从后往前删,避免索引问题 | ||||
|             for (int i = statusStrip1.Items.Count - 1; i >= 0; i--) | ||||
|             { | ||||
|                 if (statusStrip1.Items[i] is ToolStripStatusLabel && statusStrip1.Items[i].Name.StartsWith("status_")) | ||||
|                 { | ||||
|                     statusStrip1.Items.RemoveAt(i); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void StatusLabel_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             if (sender is ToolStripStatusLabel label) | ||||
|             { | ||||
|                 string cameraName = label.Text; | ||||
|                 ThreadSafeLogger.Log($"用户点击状态栏标签: {cameraName}"); | ||||
|  | ||||
|                 bool foundOriginal = false; | ||||
|                 bool foundResult = false; | ||||
|  | ||||
|                 // 1. 尝试重新显示“原图”窗口 | ||||
|                 if (CameraManager.OriginalImageDisplays.TryGetValue(cameraName, out var originalDisplayForm)) | ||||
|                 { | ||||
|                     // 使用 Show() 方法来重新激活或显示隐藏的窗口 | ||||
|                     // 传入 DockPanel 确保它知道在哪里显示 | ||||
|                     originalDisplayForm.Show(this.dockPanel1); | ||||
|                     originalDisplayForm.Activate(); // 调用 Activate() 确保它成为当前焦点窗口 | ||||
|                     foundOriginal = true; | ||||
|                 } | ||||
|  | ||||
|                 // 2. 尝试重新显示“结果”窗口 | ||||
|                 if (CameraManager.ResultImageDisplays.TryGetValue(cameraName, out var resultDisplayForm)) | ||||
|                 { | ||||
|                     resultDisplayForm.Show(this.dockPanel1); | ||||
|                     resultDisplayForm.Activate(); | ||||
|                     foundResult = true; | ||||
|                 } | ||||
|  | ||||
|                 // 3. 提供反馈 | ||||
|                 if (foundOriginal || foundResult) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"已重新激活相机 '{cameraName}' 的显示窗口。"); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[警告] 未能找到相机 '{cameraName}' 对应的活动显示窗口。可能设备已关闭。"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 根据持久化字符串创建或返回对应的窗体实例。 | ||||
|         /// 这是DockPanel Suite反序列化布局时需要的回调方法。 | ||||
|         /// </summary> | ||||
|         /// <param name="persistString">在XML中代表一个窗体的唯一字符串(通常是其类型名)。</param> | ||||
|         /// <returns>对应的窗体实例。</returns> | ||||
|         private IDockContent GetContentFromPersistString(string persistString) | ||||
|         { | ||||
|             if (persistString == typeof(FormControlPanel).ToString()) | ||||
|                 return _formControlPanel; | ||||
|             if (persistString == typeof(FrmConfig).ToString()) | ||||
|                 return _frmConfig; | ||||
|             if (persistString == typeof(FrmLog).ToString()) | ||||
|                 return _formLog; | ||||
|             if (persistString == typeof(FormStatistics).ToString()) | ||||
|                 return _formStatistics; | ||||
|  | ||||
|             // 对于图像显示窗口,由于它们是动态创建的,情况会更复杂。 | ||||
|             // 在当前设计中,我们不保存图像窗口的布局,它们会在应用相机配置时重新创建。 | ||||
|             // 如果需要保存它们,需要在CameraManager中管理它们的持久化字符串。 | ||||
|             // 目前的设计下,返回null是安全的。 | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 显示程序的默认窗口布局。 | ||||
|         /// </summary> | ||||
|         private void ShowDefaultLayout() | ||||
|         { | ||||
|             // 确保所有窗体都未被意外关闭 | ||||
|             if (_formControlPanel.IsDisposed) _formControlPanel = new FormControlPanel(); | ||||
|             if (_frmConfig.IsDisposed) _frmConfig = new FrmConfig(); | ||||
|             if (_formLog.IsDisposed) _formLog = new FrmLog(); | ||||
|             if (_formStatistics.IsDisposed) _formStatistics = new FormStatistics(); | ||||
|  | ||||
|             // 显示默认窗口 | ||||
|             _frmConfig.Show(dockPanel1, DockState.DockLeft); | ||||
|             _formLog.Show(dockPanel1, DockState.DockBottom); | ||||
|             _formControlPanel.Show(dockPanel1, DockState.DockRight); | ||||
|             _formStatistics.Show(dockPanel1, DockState.DockTop); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 【新增】保存当前窗口布局到XML文件。 | ||||
|         /// </summary> | ||||
|         private void SaveLayout() | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 // 将当前DockPanel的布局保存到指定文件 | ||||
|                 dockPanel1.SaveAsXml(_layoutConfigFile); | ||||
|                 ThreadSafeLogger.Log("布局已成功保存!"); | ||||
|                 //MessageBox.Show("布局已成功保存!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log("保存布局失败: " + ex.Message); | ||||
|                 //MessageBox.Show("保存布局失败: " + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); | ||||
|             } | ||||
|         } | ||||
|         private void FrmMain_FormClosing(object sender, FormClosingEventArgs e) | ||||
|         { | ||||
|             if (_formStatistics != null && !_formStatistics.IsDisposed) | ||||
|             { | ||||
|                 StatisticsExporter.ExportToExcel(_formStatistics.CurrentStatistics, "Shutdown"); | ||||
|             } | ||||
|  | ||||
|             CameraManager.Shutdown(); | ||||
|             ThreadSafeLogger.Shutdown(); | ||||
|         } | ||||
|  | ||||
|         private void ToolStripMenuSaveLayou_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             SaveLayout(); | ||||
|         } | ||||
|  | ||||
|         private void 配置ToolStripMenuItem_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             //_frmConfig.Show(); | ||||
|             // 如果窗体因意外被销毁,则重新创建它 | ||||
|             if (_frmConfig == null || _frmConfig.IsDisposed) | ||||
|             { | ||||
|                 _frmConfig = new FrmConfig() { Text = "主程序配置" }; | ||||
|                 _frmConfig.FormClosing += DockContent_FormClosing; | ||||
|             } | ||||
|             // 调用 Show() 会自动处理隐藏和显示逻辑 | ||||
|             _frmConfig.Show(this.dockPanel1); | ||||
|         } | ||||
|         private void 日志ToolStripMenuItem_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             if (_formLog == null || _formLog.IsDisposed) | ||||
|             { | ||||
|                 _formLog = new FrmLog { Text = "运行日志" }; | ||||
|                 _formLog.FormClosing += DockContent_FormClosing; | ||||
|             } | ||||
|             _formLog.Show(this.dockPanel1); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 处理所有可停靠子窗体关闭事件的通用方法。 | ||||
|         /// 通过取消关闭并隐藏窗口,来实现“假关闭”,以便后续能重新显示。 | ||||
|         /// </summary> | ||||
|         private void DockContent_FormClosing(object sender, FormClosingEventArgs e) | ||||
|         { | ||||
|             // 检查关闭原因是否为用户点击了关闭按钮 | ||||
|             if (e.CloseReason == CloseReason.UserClosing) | ||||
|             { | ||||
|                 // 1. 取消真正的关闭(Dispose)操作,防止窗体被销毁 | ||||
|                 e.Cancel = true; | ||||
|  | ||||
|                 // 2. 将窗口隐藏起来 | ||||
|                 // 我们需要将 sender 转换为 DockContent 类型来访问 Hide() 方法 | ||||
|                 if (sender is DockContent dockContent) | ||||
|                 { | ||||
|                     if (dockContent is FormImageDisplay imageDisplay) | ||||
|                     { | ||||
|                         ThreadSafeLogger.Log($"用户关闭了窗口 '{imageDisplay.Text}',已将其隐藏。"); | ||||
|                     } | ||||
|                     dockContent.Hide(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3244
									
								
								Check.Main/FrmMain.resx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3244
									
								
								Check.Main/FrmMain.resx
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										149
									
								
								Check.Main/Infer/DetectionCoordinator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								Check.Main/Infer/DetectionCoordinator.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| using Check.Main.Camera; | ||||
| using Check.Main.Common; | ||||
| using System; | ||||
| using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Infer | ||||
| { | ||||
|     public static class DetectionCoordinator | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// 定义存储所有相机处理器的字典 | ||||
|         /// 键是相机的唯一编号 (CameraIndex),值是对应的处理器实例。 | ||||
|         /// </summary> | ||||
|         private static ConcurrentDictionary<int, CameraProcessor> _processors = new ConcurrentDictionary<int, CameraProcessor>(); | ||||
|         /// <summary> | ||||
|         /// 用于在产品组装时进行同步,确保线程安全 | ||||
|         /// </summary> | ||||
|         private static ConcurrentDictionary<long, ProductAssembly> _productAssemblies = new ConcurrentDictionary<long, ProductAssembly>(); | ||||
|         /// <summary> | ||||
|         /// 可用的相机数量 | ||||
|         /// </summary> | ||||
|         private static int _enabledCameraCount = 0; | ||||
|  | ||||
|         public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted; | ||||
|         public static bool IsDetectionRunning { get; private set; } = false; | ||||
|  | ||||
|         // OnDetectionCompleted 事件现在也属于这里 | ||||
|         //public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted; | ||||
|  | ||||
|         public static void StartDetection() | ||||
|         { | ||||
|             if (!IsDetectionRunning) | ||||
|             { | ||||
|                 IsDetectionRunning = true; | ||||
|                 ThreadSafeLogger.Log("检测统计已启动。"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public static void StopDetection() | ||||
|         { | ||||
|             if (IsDetectionRunning) | ||||
|             { | ||||
|                 IsDetectionRunning = false; | ||||
|                 ThreadSafeLogger.Log("检测统计已停止。"); | ||||
|             } | ||||
|         } | ||||
|         public static void Initialize(List<CameraSettings> cameraSettings, List<ModelSettings> modelSettings) | ||||
|         { | ||||
|             Shutdown(); // 先关闭旧的 | ||||
|             var enabledCameras = cameraSettings.Where(c => c.IsEnabled).ToList(); | ||||
|             _enabledCameraCount = enabledCameras.Count; | ||||
|             if (_enabledCameraCount == 0) return; | ||||
|  | ||||
|             foreach (var camSetting in enabledCameras) | ||||
|             { | ||||
|                 // 找到与相机编号匹配的模型 | ||||
|                 var model = modelSettings.FirstOrDefault(m => m.Id == camSetting.ModelID); | ||||
|                 if (model == null) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[警告] 找不到与相机 #{camSetting.CameraIndex} 匹配的模型,该相机将无法处理图像。"); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 var processor = new CameraProcessor(camSetting.CameraIndex,camSetting.ModelID); | ||||
|                 _processors.TryAdd(camSetting.CameraIndex, processor); | ||||
|                 processor.Start(); | ||||
|             } | ||||
|             ThreadSafeLogger.Log($"检测协调器已初始化,启动了 {_processors.Count} 个相机处理线程。"); | ||||
|         } | ||||
|  | ||||
|         public static void EnqueueImage(int cameraIndex, Bitmap bmp) | ||||
|         { | ||||
|             if (_processors.TryGetValue(cameraIndex, out var processor)) | ||||
|             { | ||||
|                 processor.EnqueueImage(bmp); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // 如果找不到处理器,必须释放Bitmap防止泄漏 | ||||
|                 bmp?.Dispose(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 供 CameraProcessor 回调,用以组装产品 | ||||
|         public static void AssembleProduct(ImageData data, string result) | ||||
|         { | ||||
|             var assembly = _productAssemblies.GetOrAdd(data.ProductId, (id) => new ProductAssembly(id, _enabledCameraCount)); | ||||
|  | ||||
|             if (assembly.AddResult(data.CameraIndex, result)) | ||||
|             { | ||||
|                 string finalResult = assembly.GetFinalResult(); | ||||
|                 ThreadSafeLogger.Log($"产品 #{assembly.ProductId} 已检测完毕,最终结果: {finalResult}"); | ||||
|  | ||||
|                 // 只有在检测运行时,才触发事件 | ||||
|                 if (IsDetectionRunning) | ||||
|                 { | ||||
|                     OnDetectionCompleted?.Invoke(null, new DetectionResultEventArgs(finalResult == "OK")); | ||||
|                 } | ||||
|  | ||||
|                 if (_productAssemblies.TryRemove(assembly.ProductId, out var finishedAssembly)) | ||||
|                 { | ||||
|                     finishedAssembly.Dispose(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 命令所有活动的相机处理器重置它们的内部计数器。 | ||||
|         /// </summary> | ||||
|         public static void ResetAllCounters() | ||||
|         { | ||||
|             foreach (var processor in _processors.Values) | ||||
|             { | ||||
|                 processor.ResetCounter(); | ||||
|             } | ||||
|             ThreadSafeLogger.Log("所有相机处理器的产品计数器已重置。"); | ||||
|         } | ||||
|  | ||||
|         public static CameraProcessor GetProcessor(int cameraIndex) | ||||
|         { | ||||
|             _processors.TryGetValue(cameraIndex, out var p); | ||||
|             return p; | ||||
|         } | ||||
|         public static IEnumerable<CameraProcessor> GetAllProcessors() | ||||
|         { | ||||
|             return _processors.Values; | ||||
|         } | ||||
|  | ||||
|  | ||||
|         public static void Shutdown() | ||||
|         { | ||||
|             foreach (var processor in _processors.Values) | ||||
|             { | ||||
|                 processor.Dispose(); | ||||
|             } | ||||
|             _processors.Clear(); | ||||
|  | ||||
|             foreach (var assembly in _productAssemblies.Values) | ||||
|             { | ||||
|                 assembly.Dispose(); | ||||
|             } | ||||
|             _productAssemblies.Clear(); | ||||
|             ThreadSafeLogger.Log("检测协调器已关闭。"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										131
									
								
								Check.Main/Infer/ModelSettings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								Check.Main/Infer/ModelSettings.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,131 @@ | ||||
| using Check.Main.Common; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Linq; | ||||
| using System.Runtime.CompilerServices; | ||||
| using System.Runtime.Serialization; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Infer | ||||
| { | ||||
|     public enum DetectDevice | ||||
|     { | ||||
|         [Description("CPU")] | ||||
|         CPU = 0, | ||||
|  | ||||
|         [Description("GPU")] | ||||
|         GPU, | ||||
|  | ||||
|         [Description("VPU")] | ||||
|         VPU, | ||||
|     } | ||||
|     public enum ModelVersion | ||||
|     { | ||||
|         [Description("v8")] | ||||
|         V8 = 0, | ||||
|  | ||||
|         [Description("v11")] | ||||
|         V11, | ||||
|     } | ||||
|     public enum CheckModelType | ||||
|     { | ||||
|         [Description("分类")] | ||||
|         Classification, | ||||
|  | ||||
|         [Description("检测")] | ||||
|         ObjectDetection, | ||||
|  | ||||
|         [Description("OBB")] | ||||
|         ObbDetection, | ||||
|  | ||||
|         [Description("分割")] | ||||
|         Segmentation, | ||||
|  | ||||
|         [Description("位姿")] | ||||
|         PoseEstimation | ||||
|     } | ||||
|  | ||||
|     [Serializable] // 确保可被XML序列化 | ||||
|     public class ModelSettings : INotifyPropertyChanged, ICloneable | ||||
|     { | ||||
|         public event PropertyChangedEventHandler PropertyChanged; | ||||
|  | ||||
|         private int _id; | ||||
|         private string _name = "New Model"; | ||||
|         private string _path = ""; | ||||
|         private DetectDevice _checkDevice=DetectDevice.CPU; | ||||
|         private ModelVersion _mVersion=ModelVersion.V8; | ||||
|         private CheckModelType _mType = CheckModelType.Classification; | ||||
|         private bool _isEnabled = true; | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("模型编号"), Description("模型的唯一标识符,用于与相机编号对应。")] | ||||
|         public int Id | ||||
|         { | ||||
|             get => _id; | ||||
|             set { if (_id != value) { _id = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("模型名称"), Description("给模型起一个易于识别的别名。")] | ||||
|         public string Name | ||||
|         { | ||||
|             get => _name; | ||||
|             set { if (_name != value) { _name = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|         [Category("基本信息"), DisplayName("推理设备"), Description("推理模型的设备。")] | ||||
|         [TypeConverter(typeof(EnumDescriptionTypeConverter))] | ||||
|         public DetectDevice CheckDevice | ||||
|         { | ||||
|             get => _checkDevice; | ||||
|             set { if (_checkDevice != value) { _checkDevice = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("模型版本"), Description("推理模型的版本。")] | ||||
|         [TypeConverter(typeof(EnumDescriptionTypeConverter))] | ||||
|         public ModelVersion MVersion | ||||
|         { | ||||
|             get => _mVersion; | ||||
|             set { if (_mVersion != value) { _mVersion = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|         [Category("基本信息"), DisplayName("模型类型"), Description("推理模型的类型。")] | ||||
|         [TypeConverter(typeof(EnumDescriptionTypeConverter))] | ||||
|         public CheckModelType MType | ||||
|         { | ||||
|             get => _mType; | ||||
|             set { if (_mType != value) { _mType = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("是否启用"), Description("是否在程序启动时是否启用模型")] | ||||
|         public bool IsEnabled | ||||
|         { | ||||
|             get => _isEnabled; | ||||
|             set | ||||
|             { | ||||
|                 if (_isEnabled != value) | ||||
|                 { | ||||
|                     _isEnabled = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("文件"), DisplayName("模型路径"), Description("选择模型文件(.onnx, .bin, etc., .pt)。")] | ||||
|         [Editor(typeof(System.Windows.Forms.Design.FileNameEditor), typeof(System.Drawing.Design.UITypeEditor))] | ||||
|         public string Path | ||||
|         { | ||||
|             get => _path; | ||||
|             set { if (_path != value) { _path = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|         protected void OnPropertyChanged([CallerMemberName] string propertyName = null) | ||||
|         { | ||||
|             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||||
|         } | ||||
|  | ||||
|         public object Clone() | ||||
|         { | ||||
|             return this.MemberwiseClone(); // 浅克隆对于这个类足够了 | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										102
									
								
								Check.Main/Infer/YoloModelManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								Check.Main/Infer/YoloModelManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| using Check.Main.Common; | ||||
| using System; | ||||
| using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using YoloDotNet; | ||||
| using YoloDotNet.Models; | ||||
|  | ||||
| namespace Check.Main.Infer | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 静态全局YOLO模型管理器。 | ||||
|     /// 负责在程序启动时加载所有模型,并在关闭时释放资源。 | ||||
|     /// </summary> | ||||
|     public static class YoloModelManager | ||||
|     { | ||||
|         // 使用 ConcurrentDictionary 保证线程安全 | ||||
|         private static readonly ConcurrentDictionary<int, Yolo> _loadedModels = new ConcurrentDictionary<int, Yolo>(); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 根据模型配置列表初始化所有YOLO模型实例。 | ||||
|         /// 应在程序启动时调用一次。 | ||||
|         /// </summary> | ||||
|         /// <param name="modelSettings">模型配置列表。</param> | ||||
|         public static void Initialize(List<ModelSettings> modelSettings) | ||||
|         { | ||||
|             Shutdown(); // 先确保清理掉旧实例 | ||||
|  | ||||
|             if (modelSettings == null) return; | ||||
|  | ||||
|             ThreadSafeLogger.Log("开始加载YOLO模型..."); | ||||
|             foreach (var setting in modelSettings) | ||||
|             { | ||||
|                 bool gpuUse = false; | ||||
|                 if (setting.CheckDevice == DetectDevice.GPU) | ||||
|                 { | ||||
|                     gpuUse = true; | ||||
|                 } | ||||
|  | ||||
|                 if (string.IsNullOrEmpty(setting.Path) || !File.Exists(setting.Path)) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[警告] 模型 '{setting.Name}' (ID: {setting.Id}) 路径无效或文件不存在,已跳过加载。"); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 try | ||||
|                 { | ||||
|                     // 创建YOLO实例 | ||||
|                     var yolo = new Yolo(new YoloOptions | ||||
|                     { | ||||
|                         OnnxModel = setting.Path, | ||||
|                         // 您可以根据需要从配置中读取这些值 | ||||
|                         ModelType = (YoloDotNet.Enums.ModelType)setting.MType, | ||||
|                         Cuda = gpuUse, // 推荐使用GPU | ||||
|                         PrimeGpu = false | ||||
|                     }); | ||||
|  | ||||
|                     if (_loadedModels.TryAdd(setting.Id, yolo)) | ||||
|                     { | ||||
|                         ThreadSafeLogger.Log($"成功加载模型 '{setting.Name}' (ID: {setting.Id})。"); | ||||
|                     } | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[错误] 加载模型 '{setting.Name}' (ID: {setting.Id}) 失败: {ex.Message}"); | ||||
|                 } | ||||
|             } | ||||
|             ThreadSafeLogger.Log($"YOLO模型加载完成,共成功加载 {_loadedModels.Count} 个模型。"); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 获取指定ID的已加载YOLO模型。 | ||||
|         /// </summary> | ||||
|         /// <param name="modelId">模型编号。</param> | ||||
|         /// <returns>Yolo实例,如果未找到则返回null。</returns> | ||||
|         public static Yolo GetModel(int modelId) | ||||
|         { | ||||
|             _loadedModels.TryGetValue(modelId, out var yolo); | ||||
|             return yolo; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 释放所有已加载的YOLO模型资源。 | ||||
|         /// 应在程序关闭时调用。 | ||||
|         /// </summary> | ||||
|         public static void Shutdown() | ||||
|         { | ||||
|             if (_loadedModels.Count > 0) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log("正在释放所有YOLO模型..."); | ||||
|                 foreach (var yolo in _loadedModels.Values) | ||||
|                 { | ||||
|                     yolo?.Dispose(); | ||||
|                 } | ||||
|                 _loadedModels.Clear(); | ||||
|                 ThreadSafeLogger.Log("所有YOLO模型已释放。"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										28
									
								
								Check.Main/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Check.Main/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| using System.Diagnostics; | ||||
|  | ||||
| namespace Check.Main | ||||
| { | ||||
|     internal static class Program | ||||
|     { | ||||
|         /// <summary> | ||||
|         ///  The main entry point for the application. | ||||
|         /// </summary> | ||||
|         [STAThread] | ||||
|         static void Main() | ||||
|         { | ||||
|             var current = Process.GetCurrentProcess(); | ||||
|             var others = Process.GetProcessesByName(current.ProcessName) | ||||
|                                 .Where(p => p.Id != current.Id); | ||||
|             if (others.Any()) | ||||
|             { | ||||
|                 MessageBox.Show("<22><><EFBFBD><EFBFBD><EFBFBD>Ѿ<EFBFBD><D1BE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>У<EFBFBD><D0A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>", "<22><>ʾ", | ||||
|                                 MessageBoxButtons.OK, MessageBoxIcon.Information); | ||||
|                 return; | ||||
|             } | ||||
|             // To customize application configuration such as set high DPI settings or default font, | ||||
|             // see https://aka.ms/applicationconfiguration. | ||||
|             ApplicationConfiguration.Initialize(); | ||||
|             Application.Run(new FrmMain()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										74
									
								
								Check.Main/Result/ProductResult.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								Check.Main/Result/ProductResult.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| using Check.Main.Common; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Drawing; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Result | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 代表一个待检测产品的类,存储来自多个相机的图像 | ||||
|     /// </summary> | ||||
|     public class ProductResult | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// 产品唯一ID,可以是时间戳或触发计数 | ||||
|         /// </summary> | ||||
|         public long ProductID { get; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 存储每个相机名称和其拍摄到的图像 | ||||
|         /// </summary> | ||||
|         public Dictionary<string, Bitmap> CapturedImages { get; } | ||||
|  | ||||
|         public ProductResult(long productID) | ||||
|         { | ||||
|             ProductID = productID; | ||||
|             CapturedImages = new Dictionary<string, Bitmap>(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 添加一张某个相机拍摄的图像 | ||||
|         /// </summary> | ||||
|         /// <param name="cameraName">相机名称</param> | ||||
|         /// <param name="image">拍摄的图像</param> | ||||
|         public void AddImage(string cameraName, Bitmap image) | ||||
|         { | ||||
|             if (!CapturedImages.ContainsKey(cameraName)) | ||||
|             { | ||||
|                 CapturedImages.Add(cameraName, image); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // 如果这个键已经存在,说明发生了逻辑错误。 | ||||
|                 // 我们不应该持有这个新的 image 对象,必须释放它以防泄漏。 | ||||
|                 ThreadSafeLogger.Log($"[警告] 相机 {cameraName} 为产品 #{this.ProductID} 发送了重复的图像。多余的图像将被丢弃。"); | ||||
|                 image?.Dispose(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 检查是否所有预期的相机都已完成拍摄 | ||||
|         /// </summary> | ||||
|         /// <param name="expectedCameraCount">预期的相机数量</param> | ||||
|         /// <returns></returns> | ||||
|         public bool IsComplete(int expectedCameraCount) | ||||
|         { | ||||
|             return CapturedImages.Count == expectedCameraCount; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 释放所有图像资源,防止内存泄漏 | ||||
|         /// </summary> | ||||
|         public void Dispose() | ||||
|         { | ||||
|             foreach (var image in CapturedImages.Values) | ||||
|             { | ||||
|                 image?.Dispose(); | ||||
|             } | ||||
|             CapturedImages.Clear(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										113
									
								
								Check.Main/UI/FormControlPanel.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								Check.Main/UI/FormControlPanel.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     partial class FormControlPanel | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Required designer variable. | ||||
|         /// </summary> | ||||
|         private System.ComponentModel.IContainer components = null; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Clean up any resources being used. | ||||
|         /// </summary> | ||||
|         /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             if (disposing && (components != null)) | ||||
|             { | ||||
|                 components.Dispose(); | ||||
|             } | ||||
|             base.Dispose(disposing); | ||||
|         } | ||||
|  | ||||
|         #region Windows Form Designer generated code | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Required method for Designer support - do not modify | ||||
|         /// the contents of this method with the code editor. | ||||
|         /// </summary> | ||||
|         private void InitializeComponent() | ||||
|         { | ||||
|             this.uiTableLayoutPanel1 = new Sunny.UI.UITableLayoutPanel(); | ||||
|             this.btnStartDevice = new Sunny.UI.UIButton(); | ||||
|             this.btnStartCheck = new Sunny.UI.UIButton(); | ||||
|             this.uiTableLayoutPanel1.SuspendLayout(); | ||||
|             this.SuspendLayout(); | ||||
|             //  | ||||
|             // uiTableLayoutPanel1 | ||||
|             //  | ||||
|             this.uiTableLayoutPanel1.ColumnCount = 2; | ||||
|             this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); | ||||
|             this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); | ||||
|             this.uiTableLayoutPanel1.Controls.Add(this.btnStartDevice, 0, 1); | ||||
|             this.uiTableLayoutPanel1.Controls.Add(this.btnStartCheck, 0, 3); | ||||
|             this.uiTableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.uiTableLayoutPanel1.Location = new System.Drawing.Point(0, 0); | ||||
|             this.uiTableLayoutPanel1.Name = "uiTableLayoutPanel1"; | ||||
|             this.uiTableLayoutPanel1.RowCount = 5; | ||||
|             this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.38585F)); | ||||
|             this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 35.35353F)); | ||||
|             this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 7.575758F)); | ||||
|             this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.83838F)); | ||||
|             this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.38585F)); | ||||
|             this.uiTableLayoutPanel1.Size = new System.Drawing.Size(230, 198); | ||||
|             this.uiTableLayoutPanel1.TabIndex = 0; | ||||
|             this.uiTableLayoutPanel1.TagString = null; | ||||
|             //  | ||||
|             // btnStartDevice | ||||
|             //  | ||||
|             this.uiTableLayoutPanel1.SetColumnSpan(this.btnStartDevice, 2); | ||||
|             this.btnStartDevice.Cursor = System.Windows.Forms.Cursors.Hand; | ||||
|             this.btnStartDevice.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.btnStartDevice.FillPressColor = System.Drawing.Color.LimeGreen; | ||||
|             this.btnStartDevice.FillSelectedColor = System.Drawing.Color.LimeGreen; | ||||
|             this.btnStartDevice.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.btnStartDevice.Location = new System.Drawing.Point(3, 25); | ||||
|             this.btnStartDevice.MinimumSize = new System.Drawing.Size(1, 1); | ||||
|             this.btnStartDevice.Name = "btnStartDevice"; | ||||
|             this.btnStartDevice.Size = new System.Drawing.Size(224, 64); | ||||
|             this.btnStartDevice.TabIndex = 0; | ||||
|             this.btnStartDevice.Text = "启动设备"; | ||||
|             this.btnStartDevice.TipsFont = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.btnStartDevice.Click += new System.EventHandler(this.btnStartDevice_Click); | ||||
|             //  | ||||
|             // btnStartCheck | ||||
|             //  | ||||
|             this.uiTableLayoutPanel1.SetColumnSpan(this.btnStartCheck, 2); | ||||
|             this.btnStartCheck.Cursor = System.Windows.Forms.Cursors.Hand; | ||||
|             this.btnStartCheck.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.btnStartCheck.FillPressColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(192)))), ((int)(((byte)(0))))); | ||||
|             this.btnStartCheck.FillSelectedColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(192)))), ((int)(((byte)(0))))); | ||||
|             this.btnStartCheck.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.btnStartCheck.Location = new System.Drawing.Point(3, 110); | ||||
|             this.btnStartCheck.MinimumSize = new System.Drawing.Size(1, 1); | ||||
|             this.btnStartCheck.Name = "btnStartCheck"; | ||||
|             this.btnStartCheck.Size = new System.Drawing.Size(224, 61); | ||||
|             this.btnStartCheck.TabIndex = 1; | ||||
|             this.btnStartCheck.Text = "开始检测"; | ||||
|             this.btnStartCheck.TipsFont = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.btnStartCheck.Click += new System.EventHandler(this.btnStartCheck_Click); | ||||
|             //  | ||||
|             // FormControlPanel | ||||
|             //  | ||||
|             this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); | ||||
|             this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; | ||||
|             this.ClientSize = new System.Drawing.Size(230, 198); | ||||
|             this.ControlBox = false; | ||||
|             this.Controls.Add(this.uiTableLayoutPanel1); | ||||
|             this.MaximizeBox = false; | ||||
|             this.MinimizeBox = false; | ||||
|             this.Name = "FormControlPanel"; | ||||
|             this.Text = "启动管理"; | ||||
|             this.uiTableLayoutPanel1.ResumeLayout(false); | ||||
|             this.ResumeLayout(false); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         #endregion | ||||
|  | ||||
|         private Sunny.UI.UITableLayoutPanel uiTableLayoutPanel1; | ||||
|         private Sunny.UI.UIButton btnStartDevice; | ||||
|         private Sunny.UI.UIButton btnStartCheck; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										264
									
								
								Check.Main/UI/FormControlPanel.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								Check.Main/UI/FormControlPanel.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,264 @@ | ||||
| using Check.Main.Camera; | ||||
| using Check.Main.Common; | ||||
| using Check.Main.Infer; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Data; | ||||
| using System.Drawing; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms; | ||||
| using WeifenLuo.WinFormsUI.Docking; | ||||
|  | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     public partial class FormControlPanel : DockContent | ||||
|     { | ||||
|  | ||||
|         private bool _isDeviceReady = false; // 新的状态:设备是否已准备好 | ||||
|         private bool _isDetecting = false;  // 新的状态:是否正在检测中 | ||||
|  | ||||
|         // 用于跟踪设备运行状态的私有标志 | ||||
|         private bool _isDeviceRunning = false; | ||||
|         public FormControlPanel() | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|  | ||||
|             ConfigurationManager.OnConfigurationChanged += HandleConfigurationChanged; | ||||
|             UpdateUI(); | ||||
|  | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 处理全局配置在其他地方(如 FrmConfig)被更改的事件 | ||||
|         /// </summary> | ||||
|         private void HandleConfigurationChanged() | ||||
|         { | ||||
|             // 这是一个安全措施。如果设备正在运行时配置发生了变化, | ||||
|             // 最安全的做法是停止设备,以防止出现不可预知的行为。 | ||||
|             if (_isDeviceRunning) | ||||
|             { | ||||
|                 // 使用 Invoke 确保UI更新在正确的线程上执行 | ||||
|                 this.Invoke((Action)(() => | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log("相机配置已在运行时发生更改,设备将自动停止。请重新启动设备以应用新配置。"); | ||||
|                     //MessageBox.Show("相机配置已在运行时发生更改,设备将自动停止。请重新启动设备以应用新配置。", | ||||
|                     //                "配置变更", MessageBoxButtons.OK, MessageBoxIcon.Information); | ||||
|                     // 触发与点击“关闭设备”按钮相同的逻辑 | ||||
|                     btnStartDevice_Click(this, EventArgs.Empty); | ||||
|                 })); | ||||
|             } | ||||
|         } | ||||
|         private void btnStartDevice_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             if (_isDeviceReady)//_isDeviceRunning | ||||
|             { | ||||
|                 // --- 关闭流程 --- | ||||
|                 ThreadSafeLogger.Log("用户点击“关闭设备”,开始完整关闭流程..."); | ||||
|                 // 如果正在检测,先停止它 | ||||
|                 if (_isDetecting) | ||||
|                 { | ||||
|                     btnStartCheck_Click(this, EventArgs.Empty); // 调用停止检测的逻辑 | ||||
|                 } | ||||
|                 var mainForm = this.DockPanel.FindForm() as FrmMain; | ||||
|                 CameraManager.Shutdown(); | ||||
|                 _isDeviceReady = false; | ||||
|  | ||||
|                 //// 1. 停止硬触发模拟器 | ||||
|                 //CameraManager.StopHardwareTriggerSimulator(); | ||||
|  | ||||
|                 //// 2. 如果检测正在运行,则停止 | ||||
|                 //if (DetectionCoordinator.IsDetectionRunning) | ||||
|                 //{ | ||||
|                 //    DetectionCoordinator.StopDetection(); | ||||
|                 //    UpdateDetectionButtonUI(); | ||||
|                 //} | ||||
|  | ||||
|                 // 3. 执行完整的系统关闭(包括相机硬件和检测协调器) | ||||
|                 //CameraManager.Shutdown(); | ||||
|                 //YoloModelManager.Shutdown(); | ||||
|                 //_isDeviceRunning = false; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // --- 启动流程 --- | ||||
|                 ThreadSafeLogger.Log("用户点击“启动设备”,开始新的启动流程..."); | ||||
|  | ||||
|                 // 1. 从单一数据源获取完整的配置对象 | ||||
|                 var config = ConfigurationManager.GetCurrentConfig(); | ||||
|                 // 2. 验证相机配置的有效性 | ||||
|                 if (config.CameraSettings == null || !config.CameraSettings.Any(c => c.IsEnabled)) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log("没有已启用的相机配置,启动中止。"); | ||||
|                     return; | ||||
|                 } | ||||
|                 // 3. 获取主窗体引用 | ||||
|                 var mainForm = this.DockPanel.FindForm() as FrmMain; | ||||
|                 if (mainForm == null) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log("无法找到主窗体,启动中止。"); | ||||
|                     return; | ||||
|                 } | ||||
|                 // 4. 执行新的启动流程: | ||||
|                 //    第一步:初始化系统。这会准备好相机硬件、UI窗口和所有后台处理线程。 | ||||
|                 CameraManager.PrepareAll(config, mainForm); | ||||
|                 _isDeviceReady = true; | ||||
|  | ||||
|  | ||||
|                 //YoloModelManager.Initialize(config.ModelSettings); | ||||
|                 //CameraManager.Initialize(config, mainForm); | ||||
|                 ////    第二步:命令所有相机开始采集图像。 | ||||
|                 //CameraManager.StartAll(); | ||||
|  | ||||
|                 //// 5. 如果有任何相机配置为软触发模式,我们启动模拟器来模拟触发信号 | ||||
|                 //if (config.CameraSettings.Any(c => c.IsEnabled && c.TriggerMode == TriggerModeType.Software)) | ||||
|                 //{ | ||||
|                 //    ThreadSafeLogger.Log("检测到软触发相机,启动触发模拟器。"); | ||||
|                 //    CameraManager.TriggerInterval = 100; // 根据需要设置间隔 | ||||
|                 //    CameraManager.StartHardwareTriggerSimulator(); | ||||
|                 //} | ||||
|  | ||||
|                 //_isDeviceRunning = true; | ||||
|             } | ||||
|             UpdateUI();//UpdateDeviceButtonUI(); | ||||
|         } | ||||
|  | ||||
|         private void btnStartCheck_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             if (_isDetecting) | ||||
|             { | ||||
|                 // --- 停止检测 --- | ||||
|                 ThreadSafeLogger.Log("用户点击“停止检测”,暂停数据流..."); | ||||
|                 // 停止硬触发模拟器 | ||||
|                 CameraManager.StopHardwareTriggerSimulator(); | ||||
|                 // 停止相机采集 | ||||
|                 CameraManager.StopAll(); | ||||
|  | ||||
|                 // 停止统计 | ||||
|                 DetectionCoordinator.StopDetection(); | ||||
|  | ||||
|                 _isDetecting = false; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // --- 开始检测 --- | ||||
|                 ThreadSafeLogger.Log("用户点击“开始检测”,启动数据流..."); | ||||
|  | ||||
|                 // 命令相机开始采集 | ||||
|                 CameraManager.StartAll(); | ||||
|  | ||||
|                 // 启动硬触发模拟器(如果需要) | ||||
|                 var config = ConfigurationManager.GetCurrentConfig(); | ||||
|                 if (config.CameraSettings.Any(c => c.IsEnabled && c.TriggerMode == TriggerModeType.Software)) | ||||
|                 { | ||||
|                     CameraManager.TriggerInterval = 100; | ||||
|                     CameraManager.StartHardwareTriggerSimulator(); | ||||
|                 } | ||||
|  | ||||
|                 // 开始统计 | ||||
|                 DetectionCoordinator.StartDetection(); | ||||
|  | ||||
|                 _isDetecting = true; | ||||
|             } | ||||
|             UpdateUI(); | ||||
|  | ||||
|             //if (!_isDeviceRunning && !DetectionCoordinator.IsDetectionRunning) | ||||
|             //{ | ||||
|             //    ThreadSafeLogger.Log("设备未启动,无法开始检测。"); | ||||
|             //    return; | ||||
|             //} | ||||
|  | ||||
|             //// 现在调用 DetectionCoordinator 中的方法 | ||||
|             //if (DetectionCoordinator.IsDetectionRunning) | ||||
|             //{ | ||||
|             //    DetectionCoordinator.StopDetection(); | ||||
|             //} | ||||
|             //else | ||||
|             //{ | ||||
|             //    DetectionCoordinator.StartDetection(); | ||||
|             //} | ||||
|             //UpdateDetectionButtonUI(); | ||||
|         } | ||||
|  | ||||
|         #region UI 更新辅助方法 | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 根据设备运行状态更新“设备”按钮的UI(文本和颜色)。 | ||||
|         /// </summary> | ||||
|         private void UpdateDeviceButtonUI() | ||||
|         { | ||||
|             if (_isDeviceRunning) | ||||
|             { | ||||
|                 btnStartDevice.Text = "关闭设备"; | ||||
|                 btnStartDevice.BackColor = Color.Salmon; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 btnStartDevice.Text = "启动设备"; | ||||
|                 btnStartDevice.BackColor = SystemColors.Control; | ||||
|             } | ||||
|         } | ||||
|         // 统一的UI更新方法 | ||||
|         private void UpdateUI() | ||||
|         { | ||||
|             // --- 更新“设备”按钮 --- | ||||
|             if (_isDeviceReady) | ||||
|             { | ||||
|                 btnStartDevice.Text = "关闭设备"; | ||||
|                 btnStartDevice.BackColor = Color.Salmon; | ||||
|                 btnStartCheck.Enabled = true; // 设备就绪后,检测按钮才可用 | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 btnStartDevice.Text = "启动设备"; | ||||
|                 btnStartDevice.BackColor = SystemColors.Control; | ||||
|                 btnStartCheck.Enabled = false; // 设备未就绪,检测按钮不可用 | ||||
|             } | ||||
|  | ||||
|             // --- 更新“检测”按钮 --- | ||||
|             if (_isDetecting) | ||||
|             { | ||||
|                 btnStartCheck.Text = "停止检测"; | ||||
|                 btnStartCheck.BackColor = Color.LightGreen; | ||||
|                 btnStartDevice.Enabled = false; // 正在检测时,不允许关闭设备 | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 btnStartCheck.Text = "开始检测"; | ||||
|                 btnStartCheck.BackColor = SystemColors.Control; | ||||
|                 if (_isDeviceReady) btnStartDevice.Enabled = true; // 停止检测后,允许关闭设备 | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 根据检测运行状态更新“检测”按钮的UI(文本和颜色)。 | ||||
|         /// </summary> | ||||
|         private void UpdateDetectionButtonUI() | ||||
|         { | ||||
|             //if (CameraManager.IsDetectionRunning) | ||||
|             //{ | ||||
|             //    btnStartCheck.Text = "停止检测"; | ||||
|             //    btnStartCheck.BackColor = Color.LightGreen; | ||||
|             //} | ||||
|             //else | ||||
|             //{ | ||||
|             //    btnStartCheck.Text = "启动检测"; | ||||
|             //    btnStartCheck.BackColor = SystemColors.Control; | ||||
|             //} | ||||
|             if (DetectionCoordinator.IsDetectionRunning) | ||||
|             { | ||||
|                 btnStartCheck.Text = "停止检测"; | ||||
|                 btnStartCheck.BackColor = Color.LightGreen; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 btnStartCheck.Text = "启动检测"; | ||||
|                 btnStartCheck.BackColor = SystemColors.Control; | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         #endregion | ||||
|     } | ||||
| } | ||||
							
								
								
									
										120
									
								
								Check.Main/UI/FormControlPanel.resx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								Check.Main/UI/FormControlPanel.resx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <root> | ||||
|   <!--  | ||||
|     Microsoft ResX Schema  | ||||
|      | ||||
|     Version 2.0 | ||||
|      | ||||
|     The primary goals of this format is to allow a simple XML format  | ||||
|     that is mostly human readable. The generation and parsing of the  | ||||
|     various data types are done through the TypeConverter classes  | ||||
|     associated with the data types. | ||||
|      | ||||
|     Example: | ||||
|      | ||||
|     ... ado.net/XML headers & schema ... | ||||
|     <resheader name="resmimetype">text/microsoft-resx</resheader> | ||||
|     <resheader name="version">2.0</resheader> | ||||
|     <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> | ||||
|     <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> | ||||
|     <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> | ||||
|     <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> | ||||
|     <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> | ||||
|         <value>[base64 mime encoded serialized .NET Framework object]</value> | ||||
|     </data> | ||||
|     <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|         <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> | ||||
|         <comment>This is a comment</comment> | ||||
|     </data> | ||||
|                  | ||||
|     There are any number of "resheader" rows that contain simple  | ||||
|     name/value pairs. | ||||
|      | ||||
|     Each data row contains a name, and value. The row also contains a  | ||||
|     type or mimetype. Type corresponds to a .NET class that support  | ||||
|     text/value conversion through the TypeConverter architecture.  | ||||
|     Classes that don't support this are serialized and stored with the  | ||||
|     mimetype set. | ||||
|      | ||||
|     The mimetype is used for serialized objects, and tells the  | ||||
|     ResXResourceReader how to depersist the object. This is currently not  | ||||
|     extensible. For a given mimetype the value must be set accordingly: | ||||
|      | ||||
|     Note - application/x-microsoft.net.object.binary.base64 is the format  | ||||
|     that the ResXResourceWriter will generate, however the reader can  | ||||
|     read any of the formats listed below. | ||||
|      | ||||
|     mimetype: application/x-microsoft.net.object.binary.base64 | ||||
|     value   : The object must be serialized with  | ||||
|             : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|      | ||||
|     mimetype: application/x-microsoft.net.object.soap.base64 | ||||
|     value   : The object must be serialized with  | ||||
|             : System.Runtime.Serialization.Formatters.Soap.SoapFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.bytearray.base64 | ||||
|     value   : The object must be serialized into a byte array  | ||||
|             : using a System.ComponentModel.TypeConverter | ||||
|             : and then encoded with base64 encoding. | ||||
|     --> | ||||
|   <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> | ||||
|     <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> | ||||
|     <xsd:element name="root" msdata:IsDataSet="true"> | ||||
|       <xsd:complexType> | ||||
|         <xsd:choice maxOccurs="unbounded"> | ||||
|           <xsd:element name="metadata"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" use="required" type="xsd:string" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="assembly"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:attribute name="alias" type="xsd:string" /> | ||||
|               <xsd:attribute name="name" type="xsd:string" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="data"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|                 <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="resheader"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|         </xsd:choice> | ||||
|       </xsd:complexType> | ||||
|     </xsd:element> | ||||
|   </xsd:schema> | ||||
|   <resheader name="resmimetype"> | ||||
|     <value>text/microsoft-resx</value> | ||||
|   </resheader> | ||||
|   <resheader name="version"> | ||||
|     <value>2.0</value> | ||||
|   </resheader> | ||||
|   <resheader name="reader"> | ||||
|     <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
|   <resheader name="writer"> | ||||
|     <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
| </root> | ||||
							
								
								
									
										46
									
								
								Check.Main/UI/FormImageDisplay.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Check.Main/UI/FormImageDisplay.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     partial class FormImageDisplay | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Required designer variable. | ||||
|         /// </summary> | ||||
|         private System.ComponentModel.IContainer components = null; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Clean up any resources being used. | ||||
|         /// </summary> | ||||
|         /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             if (disposing && (components != null)) | ||||
|             { | ||||
|                 components.Dispose(); | ||||
|             } | ||||
|             base.Dispose(disposing); | ||||
|         } | ||||
|  | ||||
|         #region Windows Form Designer generated code | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Required method for Designer support - do not modify | ||||
|         /// the contents of this method with the code editor. | ||||
|         /// </summary> | ||||
|         private void InitializeComponent() | ||||
|         { | ||||
|             this.SuspendLayout(); | ||||
|             //  | ||||
|             // FormImageDisplay | ||||
|             //  | ||||
|             this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); | ||||
|             this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; | ||||
|             this.ClientSize = new System.Drawing.Size(508, 290); | ||||
|             this.Name = "FormImageDisplay"; | ||||
|             this.Text = "FormImageDisplay"; | ||||
|             this.ResumeLayout(false); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         #endregion | ||||
|     } | ||||
| } | ||||
							
								
								
									
										74
									
								
								Check.Main/UI/FormImageDisplay.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								Check.Main/UI/FormImageDisplay.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Data; | ||||
| using System.Drawing; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms; | ||||
| using WeifenLuo.WinFormsUI.Docking; | ||||
|  | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     public partial class FormImageDisplay : DockContent | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// 相机名称 | ||||
|         /// </summary> | ||||
|         public string CameraName { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 当此显示窗口发生特定事件时(如ROI裁剪),触发此事件以通知外部(如日志系统) | ||||
|         /// </summary> | ||||
|         public event Action<string> OnDisplayEvent; | ||||
|  | ||||
|         // 使用我们全新的自定义控件 | ||||
|         private ZoomPictureBox zoomPictureBox; | ||||
|         public FormImageDisplay() | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|  | ||||
|             // 实例化新的控件 | ||||
|             zoomPictureBox = new ZoomPictureBox | ||||
|             { | ||||
|                 Dock = DockStyle.Fill, | ||||
|                 // 其他属性可以在这里设置,例如 | ||||
|                 // RectangleColor = Color.LawnGreen, | ||||
|                 // BackgroundFillColor = Color.FromArgb(45, 45, 48) | ||||
|             }; | ||||
|             this.Controls.Add(zoomPictureBox); | ||||
|  | ||||
|             // 订阅自定义控件的ROI裁剪完成事件 | ||||
|             zoomPictureBox.CroppingEnabled = false; | ||||
|             //zoomPictureBox.Cropped += ZoomPictureBox_Cropped; | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 更新显示的图像(线程安全)。 | ||||
|         /// 此方法现在将图像设置到 ZoomPictureBox1 控件中。 | ||||
|         /// </summary> | ||||
|         /// <param name="image">从相机事件传来的原始Bitmap</param> | ||||
|         public void UpdateImage(Bitmap image) | ||||
|         { | ||||
|             if (zoomPictureBox != null && !zoomPictureBox.IsDisposed) | ||||
|             { | ||||
|                 zoomPictureBox.SetImageThreadSafe(image); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // 如果PictureBox已经被释放,那么我们也应该释放这个多余的图像 | ||||
|                 image?.Dispose(); | ||||
|             } | ||||
|         } | ||||
|         // 重写 Close 方法,确保在窗口关闭时,内部的控件和资源也能被妥善处理 | ||||
|         public new void Close() | ||||
|         { | ||||
|             // 取消事件订阅,防止内存泄漏 | ||||
|             if (zoomPictureBox != null) | ||||
|             { | ||||
|                 //zoomPictureBox.Cropped -= ZoomPictureBox_Cropped; | ||||
|             } | ||||
|             base.Close(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										120
									
								
								Check.Main/UI/FormImageDisplay.resx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								Check.Main/UI/FormImageDisplay.resx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <root> | ||||
|   <!--  | ||||
|     Microsoft ResX Schema  | ||||
|      | ||||
|     Version 2.0 | ||||
|      | ||||
|     The primary goals of this format is to allow a simple XML format  | ||||
|     that is mostly human readable. The generation and parsing of the  | ||||
|     various data types are done through the TypeConverter classes  | ||||
|     associated with the data types. | ||||
|      | ||||
|     Example: | ||||
|      | ||||
|     ... ado.net/XML headers & schema ... | ||||
|     <resheader name="resmimetype">text/microsoft-resx</resheader> | ||||
|     <resheader name="version">2.0</resheader> | ||||
|     <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> | ||||
|     <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> | ||||
|     <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> | ||||
|     <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> | ||||
|     <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> | ||||
|         <value>[base64 mime encoded serialized .NET Framework object]</value> | ||||
|     </data> | ||||
|     <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|         <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> | ||||
|         <comment>This is a comment</comment> | ||||
|     </data> | ||||
|                  | ||||
|     There are any number of "resheader" rows that contain simple  | ||||
|     name/value pairs. | ||||
|      | ||||
|     Each data row contains a name, and value. The row also contains a  | ||||
|     type or mimetype. Type corresponds to a .NET class that support  | ||||
|     text/value conversion through the TypeConverter architecture.  | ||||
|     Classes that don't support this are serialized and stored with the  | ||||
|     mimetype set. | ||||
|      | ||||
|     The mimetype is used for serialized objects, and tells the  | ||||
|     ResXResourceReader how to depersist the object. This is currently not  | ||||
|     extensible. For a given mimetype the value must be set accordingly: | ||||
|      | ||||
|     Note - application/x-microsoft.net.object.binary.base64 is the format  | ||||
|     that the ResXResourceWriter will generate, however the reader can  | ||||
|     read any of the formats listed below. | ||||
|      | ||||
|     mimetype: application/x-microsoft.net.object.binary.base64 | ||||
|     value   : The object must be serialized with  | ||||
|             : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|      | ||||
|     mimetype: application/x-microsoft.net.object.soap.base64 | ||||
|     value   : The object must be serialized with  | ||||
|             : System.Runtime.Serialization.Formatters.Soap.SoapFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.bytearray.base64 | ||||
|     value   : The object must be serialized into a byte array  | ||||
|             : using a System.ComponentModel.TypeConverter | ||||
|             : and then encoded with base64 encoding. | ||||
|     --> | ||||
|   <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> | ||||
|     <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> | ||||
|     <xsd:element name="root" msdata:IsDataSet="true"> | ||||
|       <xsd:complexType> | ||||
|         <xsd:choice maxOccurs="unbounded"> | ||||
|           <xsd:element name="metadata"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" use="required" type="xsd:string" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="assembly"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:attribute name="alias" type="xsd:string" /> | ||||
|               <xsd:attribute name="name" type="xsd:string" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="data"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|                 <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="resheader"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|         </xsd:choice> | ||||
|       </xsd:complexType> | ||||
|     </xsd:element> | ||||
|   </xsd:schema> | ||||
|   <resheader name="resmimetype"> | ||||
|     <value>text/microsoft-resx</value> | ||||
|   </resheader> | ||||
|   <resheader name="version"> | ||||
|     <value>2.0</value> | ||||
|   </resheader> | ||||
|   <resheader name="reader"> | ||||
|     <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
|   <resheader name="writer"> | ||||
|     <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
| </root> | ||||
							
								
								
									
										245
									
								
								Check.Main/UI/FormStatistics.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								Check.Main/UI/FormStatistics.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,245 @@ | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     partial class FormStatistics | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Required designer variable. | ||||
|         /// </summary> | ||||
|         private System.ComponentModel.IContainer components = null; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Clean up any resources being used. | ||||
|         /// </summary> | ||||
|         /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             if (disposing && (components != null)) | ||||
|             { | ||||
|                 components.Dispose(); | ||||
|             } | ||||
|             base.Dispose(disposing); | ||||
|         } | ||||
|  | ||||
|         #region Windows Form Designer generated code | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Required method for Designer support - do not modify | ||||
|         /// the contents of this method with the code editor. | ||||
|         /// </summary> | ||||
|         private void InitializeComponent() | ||||
|         { | ||||
|             System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FormStatistics)); | ||||
|             this.uiTableLayoutPanel1 = new Sunny.UI.UITableLayoutPanel(); | ||||
|             this.toolStrip1 = new System.Windows.Forms.ToolStrip(); | ||||
|             this.uiLabel1 = new Sunny.UI.UILabel(); | ||||
|             this.uiLabel2 = new Sunny.UI.UILabel(); | ||||
|             this.uiLabel3 = new Sunny.UI.UILabel(); | ||||
|             this.uiLabel4 = new Sunny.UI.UILabel(); | ||||
|             this.txtOKNum = new Sunny.UI.UITextBox(); | ||||
|             this.txtNGNum = new Sunny.UI.UITextBox(); | ||||
|             this.txtTotal = new Sunny.UI.UITextBox(); | ||||
|             this.txtYieldRate = new Sunny.UI.UITextBox(); | ||||
|             this.toolStripButtonRest = new System.Windows.Forms.ToolStripButton(); | ||||
|             this.uiTableLayoutPanel1.SuspendLayout(); | ||||
|             this.toolStrip1.SuspendLayout(); | ||||
|             this.SuspendLayout(); | ||||
|             //  | ||||
|             // uiTableLayoutPanel1 | ||||
|             //  | ||||
|             this.uiTableLayoutPanel1.ColumnCount = 2; | ||||
|             this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 45.88745F)); | ||||
|             this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 54.11255F)); | ||||
|             this.uiTableLayoutPanel1.Controls.Add(this.txtYieldRate, 1, 3); | ||||
|             this.uiTableLayoutPanel1.Controls.Add(this.txtTotal, 1, 2); | ||||
|             this.uiTableLayoutPanel1.Controls.Add(this.txtNGNum, 1, 1); | ||||
|             this.uiTableLayoutPanel1.Controls.Add(this.toolStrip1, 0, 4); | ||||
|             this.uiTableLayoutPanel1.Controls.Add(this.uiLabel1, 0, 0); | ||||
|             this.uiTableLayoutPanel1.Controls.Add(this.uiLabel2, 0, 1); | ||||
|             this.uiTableLayoutPanel1.Controls.Add(this.uiLabel3, 0, 2); | ||||
|             this.uiTableLayoutPanel1.Controls.Add(this.uiLabel4, 0, 3); | ||||
|             this.uiTableLayoutPanel1.Controls.Add(this.txtOKNum, 1, 0); | ||||
|             this.uiTableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.uiTableLayoutPanel1.Location = new System.Drawing.Point(0, 0); | ||||
|             this.uiTableLayoutPanel1.Name = "uiTableLayoutPanel1"; | ||||
|             this.uiTableLayoutPanel1.RowCount = 5; | ||||
|             this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); | ||||
|             this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); | ||||
|             this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); | ||||
|             this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); | ||||
|             this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); | ||||
|             this.uiTableLayoutPanel1.Size = new System.Drawing.Size(231, 228); | ||||
|             this.uiTableLayoutPanel1.TabIndex = 0; | ||||
|             this.uiTableLayoutPanel1.TagString = null; | ||||
|             //  | ||||
|             // toolStrip1 | ||||
|             //  | ||||
|             this.uiTableLayoutPanel1.SetColumnSpan(this.toolStrip1, 2); | ||||
|             this.toolStrip1.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { | ||||
|             this.toolStripButtonRest}); | ||||
|             this.toolStrip1.Location = new System.Drawing.Point(0, 180); | ||||
|             this.toolStrip1.Name = "toolStrip1"; | ||||
|             this.toolStrip1.Size = new System.Drawing.Size(231, 48); | ||||
|             this.toolStrip1.TabIndex = 0; | ||||
|             this.toolStrip1.Text = "toolStrip1"; | ||||
|             //  | ||||
|             // uiLabel1 | ||||
|             //  | ||||
|             this.uiLabel1.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.uiLabel1.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.uiLabel1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(48)))), ((int)(((byte)(48)))), ((int)(((byte)(48))))); | ||||
|             this.uiLabel1.Location = new System.Drawing.Point(3, 0); | ||||
|             this.uiLabel1.Name = "uiLabel1"; | ||||
|             this.uiLabel1.Size = new System.Drawing.Size(100, 45); | ||||
|             this.uiLabel1.TabIndex = 1; | ||||
|             this.uiLabel1.Text = "良品数"; | ||||
|             this.uiLabel1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; | ||||
|             //  | ||||
|             // uiLabel2 | ||||
|             //  | ||||
|             this.uiLabel2.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.uiLabel2.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.uiLabel2.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(48)))), ((int)(((byte)(48)))), ((int)(((byte)(48))))); | ||||
|             this.uiLabel2.Location = new System.Drawing.Point(3, 45); | ||||
|             this.uiLabel2.Name = "uiLabel2"; | ||||
|             this.uiLabel2.Size = new System.Drawing.Size(100, 45); | ||||
|             this.uiLabel2.TabIndex = 2; | ||||
|             this.uiLabel2.Text = "不良品数量"; | ||||
|             this.uiLabel2.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; | ||||
|             //  | ||||
|             // uiLabel3 | ||||
|             //  | ||||
|             this.uiLabel3.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.uiLabel3.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.uiLabel3.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(48)))), ((int)(((byte)(48)))), ((int)(((byte)(48))))); | ||||
|             this.uiLabel3.Location = new System.Drawing.Point(3, 90); | ||||
|             this.uiLabel3.Name = "uiLabel3"; | ||||
|             this.uiLabel3.Size = new System.Drawing.Size(100, 45); | ||||
|             this.uiLabel3.TabIndex = 3; | ||||
|             this.uiLabel3.Text = "总数"; | ||||
|             this.uiLabel3.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; | ||||
|             //  | ||||
|             // uiLabel4 | ||||
|             //  | ||||
|             this.uiLabel4.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.uiLabel4.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.uiLabel4.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(48)))), ((int)(((byte)(48)))), ((int)(((byte)(48))))); | ||||
|             this.uiLabel4.Location = new System.Drawing.Point(3, 135); | ||||
|             this.uiLabel4.Name = "uiLabel4"; | ||||
|             this.uiLabel4.Size = new System.Drawing.Size(100, 45); | ||||
|             this.uiLabel4.TabIndex = 4; | ||||
|             this.uiLabel4.Text = "良品率"; | ||||
|             this.uiLabel4.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; | ||||
|             //  | ||||
|             // txtOKNum | ||||
|             //  | ||||
|             this.txtOKNum.Cursor = System.Windows.Forms.Cursors.IBeam; | ||||
|             this.txtOKNum.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.txtOKNum.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.txtOKNum.Location = new System.Drawing.Point(110, 5); | ||||
|             this.txtOKNum.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); | ||||
|             this.txtOKNum.MinimumSize = new System.Drawing.Size(1, 16); | ||||
|             this.txtOKNum.Name = "txtOKNum"; | ||||
|             this.txtOKNum.Padding = new System.Windows.Forms.Padding(5); | ||||
|             this.txtOKNum.ShowText = false; | ||||
|             this.txtOKNum.Size = new System.Drawing.Size(117, 35); | ||||
|             this.txtOKNum.TabIndex = 5; | ||||
|             this.txtOKNum.Text = "0"; | ||||
|             this.txtOKNum.TextAlignment = System.Drawing.ContentAlignment.MiddleCenter; | ||||
|             this.txtOKNum.Watermark = ""; | ||||
|             //  | ||||
|             // txtNGNum | ||||
|             //  | ||||
|             this.txtNGNum.Cursor = System.Windows.Forms.Cursors.IBeam; | ||||
|             this.txtNGNum.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.txtNGNum.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.txtNGNum.Location = new System.Drawing.Point(110, 50); | ||||
|             this.txtNGNum.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); | ||||
|             this.txtNGNum.MinimumSize = new System.Drawing.Size(1, 16); | ||||
|             this.txtNGNum.Name = "txtNGNum"; | ||||
|             this.txtNGNum.Padding = new System.Windows.Forms.Padding(5); | ||||
|             this.txtNGNum.ShowText = false; | ||||
|             this.txtNGNum.Size = new System.Drawing.Size(117, 35); | ||||
|             this.txtNGNum.TabIndex = 6; | ||||
|             this.txtNGNum.Text = "0"; | ||||
|             this.txtNGNum.TextAlignment = System.Drawing.ContentAlignment.MiddleCenter; | ||||
|             this.txtNGNum.Watermark = ""; | ||||
|             //  | ||||
|             // txtTotal | ||||
|             //  | ||||
|             this.txtTotal.Cursor = System.Windows.Forms.Cursors.IBeam; | ||||
|             this.txtTotal.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.txtTotal.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.txtTotal.Location = new System.Drawing.Point(110, 95); | ||||
|             this.txtTotal.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); | ||||
|             this.txtTotal.MinimumSize = new System.Drawing.Size(1, 16); | ||||
|             this.txtTotal.Name = "txtTotal"; | ||||
|             this.txtTotal.Padding = new System.Windows.Forms.Padding(5); | ||||
|             this.txtTotal.ShowText = false; | ||||
|             this.txtTotal.Size = new System.Drawing.Size(117, 35); | ||||
|             this.txtTotal.TabIndex = 7; | ||||
|             this.txtTotal.Text = "0"; | ||||
|             this.txtTotal.TextAlignment = System.Drawing.ContentAlignment.MiddleCenter; | ||||
|             this.txtTotal.Watermark = ""; | ||||
|             //  | ||||
|             // txtYieldRate | ||||
|             //  | ||||
|             this.txtYieldRate.Cursor = System.Windows.Forms.Cursors.IBeam; | ||||
|             this.txtYieldRate.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.txtYieldRate.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.txtYieldRate.Location = new System.Drawing.Point(110, 140); | ||||
|             this.txtYieldRate.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); | ||||
|             this.txtYieldRate.MinimumSize = new System.Drawing.Size(1, 16); | ||||
|             this.txtYieldRate.Name = "txtYieldRate"; | ||||
|             this.txtYieldRate.Padding = new System.Windows.Forms.Padding(5); | ||||
|             this.txtYieldRate.ShowText = false; | ||||
|             this.txtYieldRate.Size = new System.Drawing.Size(117, 35); | ||||
|             this.txtYieldRate.TabIndex = 8; | ||||
|             this.txtYieldRate.Text = "0"; | ||||
|             this.txtYieldRate.TextAlignment = System.Drawing.ContentAlignment.MiddleCenter; | ||||
|             this.txtYieldRate.Watermark = ""; | ||||
|             //  | ||||
|             // toolStripButtonRest | ||||
|             //  | ||||
|             this.toolStripButtonRest.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; | ||||
|             this.toolStripButtonRest.Image = ((System.Drawing.Image)(resources.GetObject("toolStripButtonRest.Image"))); | ||||
|             this.toolStripButtonRest.ImageTransparentColor = System.Drawing.Color.Magenta; | ||||
|             this.toolStripButtonRest.Name = "toolStripButtonRest"; | ||||
|             this.toolStripButtonRest.Size = new System.Drawing.Size(60, 45); | ||||
|             this.toolStripButtonRest.Text = "重置计数"; | ||||
|             this.toolStripButtonRest.Click += new System.EventHandler(this.toolStripButtonRest_Click); | ||||
|             //  | ||||
|             // FormStatistics | ||||
|             //  | ||||
|             this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); | ||||
|             this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; | ||||
|             this.ClientSize = new System.Drawing.Size(231, 228); | ||||
|             this.ControlBox = false; | ||||
|             this.Controls.Add(this.uiTableLayoutPanel1); | ||||
|             this.MaximizeBox = false; | ||||
|             this.MinimizeBox = false; | ||||
|             this.Name = "FormStatistics"; | ||||
|             this.Text = "统计"; | ||||
|             this.uiTableLayoutPanel1.ResumeLayout(false); | ||||
|             this.uiTableLayoutPanel1.PerformLayout(); | ||||
|             this.toolStrip1.ResumeLayout(false); | ||||
|             this.toolStrip1.PerformLayout(); | ||||
|             this.ResumeLayout(false); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         #endregion | ||||
|  | ||||
|         private Sunny.UI.UITableLayoutPanel uiTableLayoutPanel1; | ||||
|         private System.Windows.Forms.ToolStrip toolStrip1; | ||||
|         private Sunny.UI.UITextBox txtYieldRate; | ||||
|         private Sunny.UI.UITextBox txtTotal; | ||||
|         private Sunny.UI.UITextBox txtNGNum; | ||||
|         private System.Windows.Forms.ToolStripButton toolStripButtonRest; | ||||
|         private Sunny.UI.UILabel uiLabel1; | ||||
|         private Sunny.UI.UILabel uiLabel2; | ||||
|         private Sunny.UI.UILabel uiLabel3; | ||||
|         private Sunny.UI.UILabel uiLabel4; | ||||
|         private Sunny.UI.UITextBox txtOKNum; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										89
									
								
								Check.Main/UI/FormStatistics.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								Check.Main/UI/FormStatistics.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| using Check.Main.Camera; | ||||
| using Check.Main.Common; | ||||
| using Check.Main.Infer; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Data; | ||||
| using System.Drawing; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms; | ||||
| using WeifenLuo.WinFormsUI.Docking; | ||||
|  | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     public partial class FormStatistics : DockContent | ||||
|     { | ||||
|         private readonly StatisticsData _statisticsData; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 将私有字段通过公共属性暴露,以便外部(如FormMain)访问。 | ||||
|         /// </summary> | ||||
|         public StatisticsData CurrentStatistics => _statisticsData; | ||||
|         public FormStatistics() | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|             _statisticsData = new StatisticsData(); | ||||
|  | ||||
|             // 订阅CameraManager的检测完成事件 | ||||
|             DetectionCoordinator.OnDetectionCompleted += CameraManager_OnDetectionCompleted; | ||||
|             // 初始化UI显示 | ||||
|             UpdateUI(); | ||||
|         } | ||||
|         // 当CameraManager发布检测结果时,此方法被调用 | ||||
|         private void CameraManager_OnDetectionCompleted(object sender, DetectionResultEventArgs e) | ||||
|         { | ||||
|             // 更新统计数据 | ||||
|             _statisticsData.UpdateWithResult(e.IsOK); | ||||
|  | ||||
|             // 在UI线程上更新界面显示 | ||||
|             UpdateUI(); | ||||
|         } | ||||
|         // 更新所有标签的文本 | ||||
|         private void UpdateUI() | ||||
|         { | ||||
|             // 使用Invoke确保线程安全 | ||||
|             if (this.InvokeRequired) | ||||
|             { | ||||
|                 this.Invoke(new Action(UpdateUI)); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             txtOKNum.Text = _statisticsData.GoodCount.ToString(); | ||||
|             txtNGNum.Text = _statisticsData.NgCount.ToString(); | ||||
|             txtTotal.Text = _statisticsData.TotalCount.ToString(); | ||||
|             // 将良率格式化为百分比 | ||||
|             txtYieldRate.Text = _statisticsData.YieldRate.ToString("P2", CultureInfo.InvariantCulture); | ||||
|  | ||||
|             // 根据良率改变颜色以示提醒 | ||||
|             if (_statisticsData.YieldRate < 0.9 && _statisticsData.TotalCount > 10) | ||||
|             { | ||||
|                 txtYieldRate.ForeColor = Color.Red; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 txtYieldRate.ForeColor = Color.ForestGreen; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void toolStripButtonRest_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             StatisticsExporter.ExportToExcel(CurrentStatistics, "Reset"); | ||||
|  | ||||
|             _statisticsData.Reset(); | ||||
|             // 同时重置产品ID计数器 | ||||
|             DetectionCoordinator.ResetAllCounters(); | ||||
|             UpdateUI(); | ||||
|             ThreadSafeLogger.Log("统计数据已重置。"); | ||||
|         } | ||||
|         // 窗体关闭时,取消事件订阅,防止内存泄漏 | ||||
|         protected override void OnFormClosing(FormClosingEventArgs e) | ||||
|         { | ||||
|             DetectionCoordinator.OnDetectionCompleted -= CameraManager_OnDetectionCompleted; | ||||
|             base.OnFormClosing(e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										139
									
								
								Check.Main/UI/FormStatistics.resx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								Check.Main/UI/FormStatistics.resx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <root> | ||||
|   <!--  | ||||
|     Microsoft ResX Schema  | ||||
|      | ||||
|     Version 2.0 | ||||
|      | ||||
|     The primary goals of this format is to allow a simple XML format  | ||||
|     that is mostly human readable. The generation and parsing of the  | ||||
|     various data types are done through the TypeConverter classes  | ||||
|     associated with the data types. | ||||
|      | ||||
|     Example: | ||||
|      | ||||
|     ... ado.net/XML headers & schema ... | ||||
|     <resheader name="resmimetype">text/microsoft-resx</resheader> | ||||
|     <resheader name="version">2.0</resheader> | ||||
|     <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> | ||||
|     <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> | ||||
|     <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> | ||||
|     <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> | ||||
|     <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> | ||||
|         <value>[base64 mime encoded serialized .NET Framework object]</value> | ||||
|     </data> | ||||
|     <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|         <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> | ||||
|         <comment>This is a comment</comment> | ||||
|     </data> | ||||
|                  | ||||
|     There are any number of "resheader" rows that contain simple  | ||||
|     name/value pairs. | ||||
|      | ||||
|     Each data row contains a name, and value. The row also contains a  | ||||
|     type or mimetype. Type corresponds to a .NET class that support  | ||||
|     text/value conversion through the TypeConverter architecture.  | ||||
|     Classes that don't support this are serialized and stored with the  | ||||
|     mimetype set. | ||||
|      | ||||
|     The mimetype is used for serialized objects, and tells the  | ||||
|     ResXResourceReader how to depersist the object. This is currently not  | ||||
|     extensible. For a given mimetype the value must be set accordingly: | ||||
|      | ||||
|     Note - application/x-microsoft.net.object.binary.base64 is the format  | ||||
|     that the ResXResourceWriter will generate, however the reader can  | ||||
|     read any of the formats listed below. | ||||
|      | ||||
|     mimetype: application/x-microsoft.net.object.binary.base64 | ||||
|     value   : The object must be serialized with  | ||||
|             : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|      | ||||
|     mimetype: application/x-microsoft.net.object.soap.base64 | ||||
|     value   : The object must be serialized with  | ||||
|             : System.Runtime.Serialization.Formatters.Soap.SoapFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.bytearray.base64 | ||||
|     value   : The object must be serialized into a byte array  | ||||
|             : using a System.ComponentModel.TypeConverter | ||||
|             : and then encoded with base64 encoding. | ||||
|     --> | ||||
|   <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> | ||||
|     <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> | ||||
|     <xsd:element name="root" msdata:IsDataSet="true"> | ||||
|       <xsd:complexType> | ||||
|         <xsd:choice maxOccurs="unbounded"> | ||||
|           <xsd:element name="metadata"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" use="required" type="xsd:string" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="assembly"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:attribute name="alias" type="xsd:string" /> | ||||
|               <xsd:attribute name="name" type="xsd:string" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="data"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|                 <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="resheader"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|         </xsd:choice> | ||||
|       </xsd:complexType> | ||||
|     </xsd:element> | ||||
|   </xsd:schema> | ||||
|   <resheader name="resmimetype"> | ||||
|     <value>text/microsoft-resx</value> | ||||
|   </resheader> | ||||
|   <resheader name="version"> | ||||
|     <value>2.0</value> | ||||
|   </resheader> | ||||
|   <resheader name="reader"> | ||||
|     <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
|   <resheader name="writer"> | ||||
|     <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
|   <metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | ||||
|     <value>17, 17</value> | ||||
|   </metadata> | ||||
|   <assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> | ||||
|   <data name="toolStripButtonRest.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|     <value> | ||||
|         iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 | ||||
|         YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG | ||||
|         YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9 | ||||
|         0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw | ||||
|         bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc | ||||
|         VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9 | ||||
|         c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32 | ||||
|         Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo | ||||
|         mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+ | ||||
|         kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D | ||||
|         TgDQASA1MVpwzwAAAABJRU5ErkJggg== | ||||
| </value> | ||||
|   </data> | ||||
| </root> | ||||
							
								
								
									
										175
									
								
								Check.Main/UI/FrmCamConfig.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								Check.Main/UI/FrmCamConfig.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     partial class FrmCamConfig | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Required designer variable. | ||||
|         /// </summary> | ||||
|         private System.ComponentModel.IContainer components = null; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Clean up any resources being used. | ||||
|         /// </summary> | ||||
|         /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             if (disposing && (components != null)) | ||||
|             { | ||||
|                 components.Dispose(); | ||||
|             } | ||||
|             base.Dispose(disposing); | ||||
|         } | ||||
|  | ||||
|         #region Windows Form Designer generated code | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Required method for Designer support - do not modify | ||||
|         /// the contents of this method with the code editor. | ||||
|         /// </summary> | ||||
|         private void InitializeComponent() | ||||
|         { | ||||
|             System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FrmCamConfig)); | ||||
|             tableLayoutPanel1 = new TableLayoutPanel(); | ||||
|             toolStrip1 = new ToolStrip(); | ||||
|             toolBtnAdd = new ToolStripButton(); | ||||
|             toolBtnRemove = new ToolStripButton(); | ||||
|             toolBtnSet = new ToolStripButton(); | ||||
|             splitContainer1 = new SplitContainer(); | ||||
|             listBoxCameras = new ListBox(); | ||||
|             propertyGrid1 = new PropertyGrid(); | ||||
|             tableLayoutPanel1.SuspendLayout(); | ||||
|             toolStrip1.SuspendLayout(); | ||||
|             ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit(); | ||||
|             splitContainer1.Panel1.SuspendLayout(); | ||||
|             splitContainer1.Panel2.SuspendLayout(); | ||||
|             splitContainer1.SuspendLayout(); | ||||
|             SuspendLayout(); | ||||
|             //  | ||||
|             // tableLayoutPanel1 | ||||
|             //  | ||||
|             tableLayoutPanel1.ColumnCount = 1; | ||||
|             tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); | ||||
|             tableLayoutPanel1.Controls.Add(toolStrip1, 0, 0); | ||||
|             tableLayoutPanel1.Controls.Add(splitContainer1, 0, 1); | ||||
|             tableLayoutPanel1.Dock = DockStyle.Fill; | ||||
|             tableLayoutPanel1.Location = new Point(0, 0); | ||||
|             tableLayoutPanel1.Margin = new Padding(4, 4, 4, 4); | ||||
|             tableLayoutPanel1.Name = "tableLayoutPanel1"; | ||||
|             tableLayoutPanel1.RowCount = 2; | ||||
|             tableLayoutPanel1.RowStyles.Add(new RowStyle()); | ||||
|             tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); | ||||
|             tableLayoutPanel1.Size = new Size(632, 560); | ||||
|             tableLayoutPanel1.TabIndex = 0; | ||||
|             //  | ||||
|             // toolStrip1 | ||||
|             //  | ||||
|             toolStrip1.Items.AddRange(new ToolStripItem[] { toolBtnAdd, toolBtnRemove, toolBtnSet }); | ||||
|             toolStrip1.Location = new Point(0, 0); | ||||
|             toolStrip1.Name = "toolStrip1"; | ||||
|             toolStrip1.Size = new Size(632, 25); | ||||
|             toolStrip1.TabIndex = 0; | ||||
|             toolStrip1.Text = "toolStrip1"; | ||||
|             //  | ||||
|             // toolBtnAdd | ||||
|             //  | ||||
|             toolBtnAdd.DisplayStyle = ToolStripItemDisplayStyle.Text; | ||||
|             toolBtnAdd.Image = (Image)resources.GetObject("toolBtnAdd.Image"); | ||||
|             toolBtnAdd.ImageTransparentColor = Color.Magenta; | ||||
|             toolBtnAdd.Name = "toolBtnAdd"; | ||||
|             toolBtnAdd.Size = new Size(36, 22); | ||||
|             toolBtnAdd.Text = "添加"; | ||||
|             toolBtnAdd.Click += toolBtnAdd_Click; | ||||
|             //  | ||||
|             // toolBtnRemove | ||||
|             //  | ||||
|             toolBtnRemove.DisplayStyle = ToolStripItemDisplayStyle.Text; | ||||
|             toolBtnRemove.Image = (Image)resources.GetObject("toolBtnRemove.Image"); | ||||
|             toolBtnRemove.ImageTransparentColor = Color.Magenta; | ||||
|             toolBtnRemove.Name = "toolBtnRemove"; | ||||
|             toolBtnRemove.Size = new Size(36, 22); | ||||
|             toolBtnRemove.Text = "移除"; | ||||
|             toolBtnRemove.Click += toolBtnRemove_Click; | ||||
|             //  | ||||
|             // toolBtnSet | ||||
|             //  | ||||
|             toolBtnSet.DisplayStyle = ToolStripItemDisplayStyle.Text; | ||||
|             toolBtnSet.Image = (Image)resources.GetObject("toolBtnSet.Image"); | ||||
|             toolBtnSet.ImageTransparentColor = Color.Magenta; | ||||
|             toolBtnSet.Name = "toolBtnSet"; | ||||
|             toolBtnSet.Size = new Size(60, 22); | ||||
|             toolBtnSet.Text = "应用配置"; | ||||
|             toolBtnSet.Click += toolBtnSet_Click; | ||||
|             //  | ||||
|             // splitContainer1 | ||||
|             //  | ||||
|             splitContainer1.Dock = DockStyle.Fill; | ||||
|             splitContainer1.Location = new Point(4, 29); | ||||
|             splitContainer1.Margin = new Padding(4, 4, 4, 4); | ||||
|             splitContainer1.Name = "splitContainer1"; | ||||
|             //  | ||||
|             // splitContainer1.Panel1 | ||||
|             //  | ||||
|             splitContainer1.Panel1.Controls.Add(listBoxCameras); | ||||
|             //  | ||||
|             // splitContainer1.Panel2 | ||||
|             //  | ||||
|             splitContainer1.Panel2.Controls.Add(propertyGrid1); | ||||
|             splitContainer1.Size = new Size(624, 527); | ||||
|             splitContainer1.SplitterDistance = 210; | ||||
|             splitContainer1.SplitterWidth = 5; | ||||
|             splitContainer1.TabIndex = 1; | ||||
|             //  | ||||
|             // listBoxCameras | ||||
|             //  | ||||
|             listBoxCameras.Dock = DockStyle.Fill; | ||||
|             listBoxCameras.FormattingEnabled = true; | ||||
|             listBoxCameras.ItemHeight = 17; | ||||
|             listBoxCameras.Location = new Point(0, 0); | ||||
|             listBoxCameras.Margin = new Padding(4, 4, 4, 4); | ||||
|             listBoxCameras.Name = "listBoxCameras"; | ||||
|             listBoxCameras.Size = new Size(210, 527); | ||||
|             listBoxCameras.TabIndex = 0; | ||||
|             listBoxCameras.SelectedIndexChanged += listBoxCameras_SelectedIndexChanged; | ||||
|             //  | ||||
|             // propertyGrid1 | ||||
|             //  | ||||
|             propertyGrid1.Dock = DockStyle.Fill; | ||||
|             propertyGrid1.Location = new Point(0, 0); | ||||
|             propertyGrid1.Margin = new Padding(4, 4, 4, 4); | ||||
|             propertyGrid1.Name = "propertyGrid1"; | ||||
|             propertyGrid1.Size = new Size(409, 527); | ||||
|             propertyGrid1.TabIndex = 0; | ||||
|             //  | ||||
|             // FrmCamConfig | ||||
|             //  | ||||
|             AutoScaleDimensions = new SizeF(7F, 17F); | ||||
|             AutoScaleMode = AutoScaleMode.Font; | ||||
|             ClientSize = new Size(632, 560); | ||||
|             Controls.Add(tableLayoutPanel1); | ||||
|             Margin = new Padding(4, 4, 4, 4); | ||||
|             Name = "FrmCamConfig"; | ||||
|             Text = "属性配置"; | ||||
|             tableLayoutPanel1.ResumeLayout(false); | ||||
|             tableLayoutPanel1.PerformLayout(); | ||||
|             toolStrip1.ResumeLayout(false); | ||||
|             toolStrip1.PerformLayout(); | ||||
|             splitContainer1.Panel1.ResumeLayout(false); | ||||
|             splitContainer1.Panel2.ResumeLayout(false); | ||||
|             ((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit(); | ||||
|             splitContainer1.ResumeLayout(false); | ||||
|             ResumeLayout(false); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         #endregion | ||||
|  | ||||
|         private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; | ||||
|         private System.Windows.Forms.ToolStrip toolStrip1; | ||||
|         private System.Windows.Forms.SplitContainer splitContainer1; | ||||
|         private System.Windows.Forms.ListBox listBoxCameras; | ||||
|         private System.Windows.Forms.PropertyGrid propertyGrid1; | ||||
|         private System.Windows.Forms.ToolStripButton toolBtnAdd; | ||||
|         private System.Windows.Forms.ToolStripButton toolBtnRemove; | ||||
|         private System.Windows.Forms.ToolStripButton toolBtnSet; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										193
									
								
								Check.Main/UI/FrmCamConfig.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								Check.Main/UI/FrmCamConfig.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,193 @@ | ||||
| using Check.Main.Camera; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Data; | ||||
| using System.Drawing; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms; | ||||
| using System.Xml.Serialization; | ||||
| using WeifenLuo.WinFormsUI.Docking; | ||||
|  | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     public partial class FrmCamConfig : DockContent | ||||
|     { | ||||
|        // public List<CameraSettings> _settingsList = new List<CameraSettings>(); | ||||
|         public List<CameraSettings> _settingsList { get; private set; } | ||||
|         private readonly string _configFilePath = Path.Combine(Application.StartupPath, "cameras.xml"); | ||||
|         public FrmCamConfig() | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|             _settingsList = new List<CameraSettings>(); | ||||
|             LoadSettings(); | ||||
|             RefreshListBox(); | ||||
|             AdaptForDialogMode(); // 调整UI为对话框模式 | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 用于从外部接收一个设置列表进行编辑。 | ||||
|         /// </summary> | ||||
|         /// <param name="settingsToEdit">要编辑的相机设置列表</param> | ||||
|         public FrmCamConfig(List<CameraSettings> settingsToEdit) | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|  | ||||
|             // 创建一个现有列表的副本进行编辑,这样如果用户点“取消”,原始列表不会被影响 | ||||
|             _settingsList = settingsToEdit != null | ||||
|                 ? settingsToEdit.Select(s => s.Clone() as CameraSettings).ToList() // 深度克隆更好,这里为简化用浅克隆 | ||||
|                 : new List<CameraSettings>(); | ||||
|  | ||||
|             // 别忘了为列表中的每个对象重新订阅事件 | ||||
|             foreach (var setting in _settingsList) | ||||
|             { | ||||
|                 setting.PropertyChanged += Setting_PropertyChanged; | ||||
|             } | ||||
|  | ||||
|             RefreshListBox(); | ||||
|             AdaptForDialogMode(); // 调整UI为对话框模式 | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 【新增的方法】 | ||||
|         /// 调整UI,使其更像一个对话框。 | ||||
|         /// </summary> | ||||
|         private void AdaptForDialogMode() | ||||
|         { | ||||
|             // 将“应用配置”按钮改为“确定” | ||||
|             toolBtnSet.Text = "确定"; | ||||
|             toolBtnSet.ToolTipText = "保存更改并关闭窗口"; | ||||
|  | ||||
|             // 新增一个“取消”按钮 | ||||
|             var toolBtnCancel = new ToolStripButton("取消") | ||||
|             { | ||||
|                 DisplayStyle = ToolStripItemDisplayStyle.Text, | ||||
|                 Alignment = ToolStripItemAlignment.Right // 靠右对齐 | ||||
|             }; | ||||
|             toolBtnCancel.Click += (sender, e) => { | ||||
|                 this.DialogResult = DialogResult.Cancel; | ||||
|                 this.Close(); | ||||
|             }; | ||||
|             toolStrip1.Items.Add(toolBtnCancel); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 【新增的事件处理方法】 | ||||
|         /// 当PropertyGrid中的属性值被用户修改后触发此事件。 | ||||
|         /// </summary> | ||||
|  | ||||
|         // 刷新左侧的相机列表 | ||||
|         private void RefreshListBox() | ||||
|         { | ||||
|             listBoxCameras.Items.Clear(); | ||||
|             foreach (var setting in _settingsList) | ||||
|             { | ||||
|                 listBoxCameras.Items.Add(setting.Name); | ||||
|             } | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 【全新的事件处理方法】 | ||||
|         /// 监听单个CameraSettings对象的属性变更通知。 | ||||
|         /// </summary> | ||||
|         private void Setting_PropertyChanged(object sender, PropertyChangedEventArgs e) | ||||
|         { | ||||
|             // 检查是不是“Name”属性发生了变化 | ||||
|             if (e.PropertyName == nameof(CameraSettings.Name)) | ||||
|             { | ||||
|                 // 'sender' 就是那个属性发生了变化的 CameraSettings 对象 | ||||
|                 var changedSetting = sender as CameraSettings; | ||||
|                 if (changedSetting == null) return; | ||||
|  | ||||
|                 // 在 _settingsList 中找到这个对象的索引 | ||||
|                 int index = _settingsList.IndexOf(changedSetting); | ||||
|  | ||||
|                 // 如果找到了,就更新ListBox中对应项的显示文本 | ||||
|                 if (index >= 0) | ||||
|                 { | ||||
|                     // 使用Invoke确保线程安全,虽然在此场景下通常不是问题,但这是个好习惯 | ||||
|                     this.Invoke(new Action(() => { | ||||
|                         listBoxCameras.Items[index] = changedSetting.Name; | ||||
|                     })); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void listBoxCameras_SelectedIndexChanged(object sender, EventArgs e) | ||||
|         { | ||||
|             if (listBoxCameras.SelectedIndex >= 0) | ||||
|             { | ||||
|                 propertyGrid1.SelectedObject = _settingsList[listBoxCameras.SelectedIndex]; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 propertyGrid1.SelectedObject = null; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void toolBtnAdd_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             var newSetting = new CameraSettings { Name = $"Camera-{_settingsList.Count + 1}" }; | ||||
|             _settingsList.Add(newSetting); | ||||
|             newSetting.PropertyChanged += Setting_PropertyChanged; | ||||
|             RefreshListBox(); | ||||
|             listBoxCameras.SelectedIndex = listBoxCameras.Items.Count - 1; | ||||
|         } | ||||
|  | ||||
|         private void toolBtnRemove_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             if (listBoxCameras.SelectedIndex >= 0) | ||||
|             { | ||||
|                 _settingsList.RemoveAt(listBoxCameras.SelectedIndex); | ||||
|                 propertyGrid1.SelectedObject = null; | ||||
|                 RefreshListBox(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void toolBtnSet_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             this.DialogResult = DialogResult.OK; | ||||
|             this.Close(); | ||||
|         } | ||||
|         // 加载XML配置文件 | ||||
|         private void LoadSettings() | ||||
|         { | ||||
|             if (!File.Exists(_configFilePath)) return; | ||||
|             try | ||||
|             { | ||||
|                 XmlSerializer serializer = new XmlSerializer(typeof(List<CameraSettings>)); | ||||
|                 using (FileStream fs = new FileStream(_configFilePath, FileMode.Open)) | ||||
|                 { | ||||
|                     _settingsList = (List<CameraSettings>)serializer.Deserialize(fs); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 MessageBox.Show("加载相机配置失败: " + ex.Message); | ||||
|             } | ||||
|             foreach (var setting in _settingsList) | ||||
|             { | ||||
|                 setting.PropertyChanged += Setting_PropertyChanged; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 保存配置到XML文件 | ||||
|         private void SaveSettings() | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 XmlSerializer serializer = new XmlSerializer(typeof(List<CameraSettings>)); | ||||
|                 using (FileStream fs = new FileStream(_configFilePath, FileMode.Create)) | ||||
|                 { | ||||
|                     serializer.Serialize(fs, _settingsList); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 MessageBox.Show("保存相机配置失败: " + ex.Message); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										169
									
								
								Check.Main/UI/FrmCamConfig.resx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								Check.Main/UI/FrmCamConfig.resx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <root> | ||||
|   <!-- | ||||
|     Microsoft ResX Schema | ||||
|  | ||||
|     Version 2.0 | ||||
|  | ||||
|     The primary goals of this format is to allow a simple XML format | ||||
|     that is mostly human readable. The generation and parsing of the | ||||
|     various data types are done through the TypeConverter classes | ||||
|     associated with the data types. | ||||
|  | ||||
|     Example: | ||||
|  | ||||
|     ... ado.net/XML headers & schema ... | ||||
|     <resheader name="resmimetype">text/microsoft-resx</resheader> | ||||
|     <resheader name="version">2.0</resheader> | ||||
|     <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> | ||||
|     <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> | ||||
|     <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> | ||||
|     <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> | ||||
|     <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> | ||||
|         <value>[base64 mime encoded serialized .NET Framework object]</value> | ||||
|     </data> | ||||
|     <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|         <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> | ||||
|         <comment>This is a comment</comment> | ||||
|     </data> | ||||
|  | ||||
|     There are any number of "resheader" rows that contain simple | ||||
|     name/value pairs. | ||||
|  | ||||
|     Each data row contains a name, and value. The row also contains a | ||||
|     type or mimetype. Type corresponds to a .NET class that support | ||||
|     text/value conversion through the TypeConverter architecture. | ||||
|     Classes that don't support this are serialized and stored with the | ||||
|     mimetype set. | ||||
|  | ||||
|     The mimetype is used for serialized objects, and tells the | ||||
|     ResXResourceReader how to depersist the object. This is currently not | ||||
|     extensible. For a given mimetype the value must be set accordingly: | ||||
|  | ||||
|     Note - application/x-microsoft.net.object.binary.base64 is the format | ||||
|     that the ResXResourceWriter will generate, however the reader can | ||||
|     read any of the formats listed below. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.binary.base64 | ||||
|     value   : The object must be serialized with | ||||
|             : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.soap.base64 | ||||
|     value   : The object must be serialized with | ||||
|             : System.Runtime.Serialization.Formatters.Soap.SoapFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.bytearray.base64 | ||||
|     value   : The object must be serialized into a byte array | ||||
|             : using a System.ComponentModel.TypeConverter | ||||
|             : and then encoded with base64 encoding. | ||||
|     --> | ||||
|   <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> | ||||
|     <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> | ||||
|     <xsd:element name="root" msdata:IsDataSet="true"> | ||||
|       <xsd:complexType> | ||||
|         <xsd:choice maxOccurs="unbounded"> | ||||
|           <xsd:element name="metadata"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" use="required" type="xsd:string" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="assembly"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:attribute name="alias" type="xsd:string" /> | ||||
|               <xsd:attribute name="name" type="xsd:string" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="data"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|                 <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="resheader"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|         </xsd:choice> | ||||
|       </xsd:complexType> | ||||
|     </xsd:element> | ||||
|   </xsd:schema> | ||||
|   <resheader name="resmimetype"> | ||||
|     <value>text/microsoft-resx</value> | ||||
|   </resheader> | ||||
|   <resheader name="version"> | ||||
|     <value>2.0</value> | ||||
|   </resheader> | ||||
|   <resheader name="reader"> | ||||
|     <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
|   <resheader name="writer"> | ||||
|     <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
|   <metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | ||||
|     <value>17, 17</value> | ||||
|   </metadata> | ||||
|   <assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> | ||||
|   <data name="toolBtnAdd.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|     <value> | ||||
|         iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 | ||||
|         YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG | ||||
|         YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9 | ||||
|         0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw | ||||
|         bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc | ||||
|         VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9 | ||||
|         c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32 | ||||
|         Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo | ||||
|         mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+ | ||||
|         kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D | ||||
|         TgDQASA1MVpwzwAAAABJRU5ErkJggg== | ||||
| </value> | ||||
|   </data> | ||||
|   <data name="toolBtnRemove.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|     <value> | ||||
|         iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 | ||||
|         YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG | ||||
|         YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9 | ||||
|         0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw | ||||
|         bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc | ||||
|         VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9 | ||||
|         c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32 | ||||
|         Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo | ||||
|         mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+ | ||||
|         kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D | ||||
|         TgDQASA1MVpwzwAAAABJRU5ErkJggg== | ||||
| </value> | ||||
|   </data> | ||||
|   <data name="toolBtnSet.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|     <value> | ||||
|         iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 | ||||
|         YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG | ||||
|         YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9 | ||||
|         0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw | ||||
|         bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc | ||||
|         VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9 | ||||
|         c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32 | ||||
|         Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo | ||||
|         mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+ | ||||
|         kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D | ||||
|         TgDQASA1MVpwzwAAAABJRU5ErkJggg== | ||||
| </value> | ||||
|   </data> | ||||
| </root> | ||||
							
								
								
									
										171
									
								
								Check.Main/UI/FrmConfig.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								Check.Main/UI/FrmConfig.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,171 @@ | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     partial class FrmConfig | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Required designer variable. | ||||
|         /// </summary> | ||||
|         private System.ComponentModel.IContainer components = null; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Clean up any resources being used. | ||||
|         /// </summary> | ||||
|         /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             if (disposing && (components != null)) | ||||
|             { | ||||
|                 components.Dispose(); | ||||
|             } | ||||
|             base.Dispose(disposing); | ||||
|         } | ||||
|  | ||||
|         #region Windows Form Designer generated code | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Required method for Designer support - do not modify | ||||
|         /// the contents of this method with the code editor. | ||||
|         /// </summary> | ||||
|         private void InitializeComponent() | ||||
|         { | ||||
|             System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FrmConfig)); | ||||
|             propertyGrid1 = new PropertyGrid(); | ||||
|             toolStrip1 = new ToolStrip(); | ||||
|             toolBtnApply = new ToolStripButton(); | ||||
|             toolBtnAddProduct = new ToolStripButton(); | ||||
|             tableLayoutPanel1 = new TableLayoutPanel(); | ||||
|             uiLabel1 = new Sunny.UI.UILabel(); | ||||
|             cmbProducts = new Sunny.UI.UIComboBox(); | ||||
|             toolBtnDeleteProduct = new ToolStripButton(); | ||||
|             toolStrip1.SuspendLayout(); | ||||
|             tableLayoutPanel1.SuspendLayout(); | ||||
|             SuspendLayout(); | ||||
|             //  | ||||
|             // propertyGrid1 | ||||
|             //  | ||||
|             tableLayoutPanel1.SetColumnSpan(propertyGrid1, 2); | ||||
|             propertyGrid1.Dock = DockStyle.Fill; | ||||
|             propertyGrid1.Location = new Point(3, 70); | ||||
|             propertyGrid1.Name = "propertyGrid1"; | ||||
|             propertyGrid1.Size = new Size(417, 345); | ||||
|             propertyGrid1.TabIndex = 0; | ||||
|             //  | ||||
|             // toolStrip1 | ||||
|             //  | ||||
|             tableLayoutPanel1.SetColumnSpan(toolStrip1, 2); | ||||
|             toolStrip1.Dock = DockStyle.Fill; | ||||
|             toolStrip1.Items.AddRange(new ToolStripItem[] { toolBtnApply, toolBtnAddProduct, toolBtnDeleteProduct }); | ||||
|             toolStrip1.Location = new Point(0, 0); | ||||
|             toolStrip1.Name = "toolStrip1"; | ||||
|             toolStrip1.Size = new Size(423, 28); | ||||
|             toolStrip1.TabIndex = 1; | ||||
|             toolStrip1.Text = "toolStrip1"; | ||||
|             //  | ||||
|             // toolBtnApply | ||||
|             //  | ||||
|             toolBtnApply.DisplayStyle = ToolStripItemDisplayStyle.Text; | ||||
|             toolBtnApply.Image = (Image)resources.GetObject("toolBtnApply.Image"); | ||||
|             toolBtnApply.ImageTransparentColor = Color.Magenta; | ||||
|             toolBtnApply.Name = "toolBtnApply"; | ||||
|             toolBtnApply.Size = new Size(60, 25); | ||||
|             toolBtnApply.Text = "应用配置"; | ||||
|             toolBtnApply.Click += toolBtnApply_Click; | ||||
|             //  | ||||
|             // toolBtnAddProduct | ||||
|             //  | ||||
|             toolBtnAddProduct.DisplayStyle = ToolStripItemDisplayStyle.Text; | ||||
|             toolBtnAddProduct.Image = (Image)resources.GetObject("toolBtnAddProduct.Image"); | ||||
|             toolBtnAddProduct.ImageTransparentColor = Color.Magenta; | ||||
|             toolBtnAddProduct.Name = "toolBtnAddProduct"; | ||||
|             toolBtnAddProduct.Size = new Size(60, 25); | ||||
|             toolBtnAddProduct.Text = "添加产品"; | ||||
|             toolBtnAddProduct.Click += toolBtnAddProduct_Click; | ||||
|             //  | ||||
|             // tableLayoutPanel1 | ||||
|             //  | ||||
|             tableLayoutPanel1.ColumnCount = 2; | ||||
|             tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 23.4042549F)); | ||||
|             tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 76.59574F)); | ||||
|             tableLayoutPanel1.Controls.Add(propertyGrid1, 0, 2); | ||||
|             tableLayoutPanel1.Controls.Add(toolStrip1, 0, 0); | ||||
|             tableLayoutPanel1.Controls.Add(uiLabel1, 0, 1); | ||||
|             tableLayoutPanel1.Controls.Add(cmbProducts, 1, 1); | ||||
|             tableLayoutPanel1.Dock = DockStyle.Fill; | ||||
|             tableLayoutPanel1.Location = new Point(0, 0); | ||||
|             tableLayoutPanel1.Name = "tableLayoutPanel1"; | ||||
|             tableLayoutPanel1.RowCount = 3; | ||||
|             tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 6.69856453F)); | ||||
|             tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 9.330144F)); | ||||
|             tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 83.97129F)); | ||||
|             tableLayoutPanel1.Size = new Size(423, 418); | ||||
|             tableLayoutPanel1.TabIndex = 2; | ||||
|             //  | ||||
|             // uiLabel1 | ||||
|             //  | ||||
|             uiLabel1.Dock = DockStyle.Fill; | ||||
|             uiLabel1.Font = new Font("宋体", 12F, FontStyle.Regular, GraphicsUnit.Point, 134); | ||||
|             uiLabel1.ForeColor = Color.FromArgb(48, 48, 48); | ||||
|             uiLabel1.Location = new Point(3, 28); | ||||
|             uiLabel1.Name = "uiLabel1"; | ||||
|             uiLabel1.Size = new Size(93, 39); | ||||
|             uiLabel1.TabIndex = 2; | ||||
|             uiLabel1.Text = "当前产品"; | ||||
|             uiLabel1.TextAlign = ContentAlignment.MiddleCenter; | ||||
|             //  | ||||
|             // cmbProducts | ||||
|             //  | ||||
|             cmbProducts.DataSource = null; | ||||
|             cmbProducts.Dock = DockStyle.Left; | ||||
|             cmbProducts.FillColor = Color.White; | ||||
|             cmbProducts.Font = new Font("宋体", 12F, FontStyle.Regular, GraphicsUnit.Point, 134); | ||||
|             cmbProducts.ItemHoverColor = Color.FromArgb(155, 200, 255); | ||||
|             cmbProducts.ItemSelectForeColor = Color.FromArgb(235, 243, 255); | ||||
|             cmbProducts.Location = new Point(103, 33); | ||||
|             cmbProducts.Margin = new Padding(4, 5, 4, 5); | ||||
|             cmbProducts.MinimumSize = new Size(63, 0); | ||||
|             cmbProducts.Name = "cmbProducts"; | ||||
|             cmbProducts.Padding = new Padding(0, 0, 30, 2); | ||||
|             cmbProducts.Size = new Size(225, 29); | ||||
|             cmbProducts.SymbolSize = 24; | ||||
|             cmbProducts.TabIndex = 3; | ||||
|             cmbProducts.TextAlignment = ContentAlignment.MiddleLeft; | ||||
|             cmbProducts.Watermark = ""; | ||||
|             cmbProducts.SelectedIndexChanged += cmbProducts_SelectedIndexChanged; | ||||
|             //  | ||||
|             // toolBtnDeleteProduct | ||||
|             //  | ||||
|             toolBtnDeleteProduct.DisplayStyle = ToolStripItemDisplayStyle.Text; | ||||
|             toolBtnDeleteProduct.Image = (Image)resources.GetObject("toolBtnDeleteProduct.Image"); | ||||
|             toolBtnDeleteProduct.ImageTransparentColor = Color.Magenta; | ||||
|             toolBtnDeleteProduct.Name = "toolBtnDeleteProduct"; | ||||
|             toolBtnDeleteProduct.Size = new Size(60, 25); | ||||
|             toolBtnDeleteProduct.Text = "删除产品"; | ||||
|             toolBtnDeleteProduct.Click += toolBtnDeleteProduct_Click; | ||||
|             //  | ||||
|             // FrmConfig | ||||
|             //  | ||||
|             AutoScaleDimensions = new SizeF(7F, 17F); | ||||
|             AutoScaleMode = AutoScaleMode.Font; | ||||
|             ClientSize = new Size(423, 418); | ||||
|             Controls.Add(tableLayoutPanel1); | ||||
|             Name = "FrmConfig"; | ||||
|             Text = "属性配置"; | ||||
|             toolStrip1.ResumeLayout(false); | ||||
|             toolStrip1.PerformLayout(); | ||||
|             tableLayoutPanel1.ResumeLayout(false); | ||||
|             tableLayoutPanel1.PerformLayout(); | ||||
|             ResumeLayout(false); | ||||
|         } | ||||
|  | ||||
|         #endregion | ||||
|  | ||||
|         private PropertyGrid propertyGrid1; | ||||
|         private ToolStrip toolStrip1; | ||||
|         private ToolStripButton toolBtnApply; | ||||
|         private TableLayoutPanel tableLayoutPanel1; | ||||
|         private ToolStripButton toolBtnAddProduct; | ||||
|         private Sunny.UI.UILabel uiLabel1; | ||||
|         private Sunny.UI.UIComboBox cmbProducts; | ||||
|         private ToolStripButton toolBtnDeleteProduct; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										163
									
								
								Check.Main/UI/FrmConfig.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								Check.Main/UI/FrmConfig.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| using Check.Main.Camera; | ||||
| using Check.Main.Common; | ||||
| using Check.Main.Dispatch; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Data; | ||||
| using System.Drawing; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms; | ||||
| using System.Xml.Serialization; | ||||
| using WeifenLuo.WinFormsUI.Docking; | ||||
|  | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     public partial class FrmConfig : DockContent | ||||
|     { | ||||
|         //private ProcessConfig _mainSettings=new ProcessConfig(); | ||||
|         //private readonly string _configFilePath = Path.Combine(Application.StartupPath, "main_config.xml"); | ||||
|         public FrmConfig() | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|             ProductManager.OnProductChanged += UpdateUIForNewProduct; | ||||
|             // 初始化UI | ||||
|             InitializeProductComboBox(); | ||||
|             //LoadSettings(); // 窗体加载时,读取主配置文件 | ||||
|             propertyGrid1.SelectedObject = ProductManager.CurrentConfig; //_mainSettings; // 将配置对象绑定到属性网格 | ||||
|             propertyGrid1.PropertyValueChanged += (s, e) => { ProductManager.SaveCurrentProductConfig(); }; // 任何属性改变后自动保存 | ||||
|         } | ||||
|         // 【新增】初始化产品下拉列表 | ||||
|         private void InitializeProductComboBox() | ||||
|         { | ||||
|             cmbProducts.DataSource = null; // 先清空数据源 | ||||
|             cmbProducts.DataSource = ProductManager.ProductList; | ||||
|             cmbProducts.SelectedItem = ProductManager.CurrentProductName; | ||||
|         } | ||||
|         // 【新增】当产品管理器中的产品切换后,此方法被调用以更新整个UI | ||||
|         private void UpdateUIForNewProduct() | ||||
|         { | ||||
|             // 使用Invoke确保线程安全 | ||||
|             if (this.InvokeRequired) | ||||
|             { | ||||
|                 this.Invoke(new Action(UpdateUIForNewProduct)); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             ThreadSafeLogger.Log($"UI正在更新以显示产品 '{ProductManager.CurrentProductName}' 的配置。"); | ||||
|             // 更新下拉列表的显示(如果产品列表也变了) | ||||
|             InitializeProductComboBox(); | ||||
|  | ||||
|             // 【关键】将PropertyGrid重新绑定到新产品的配置对象上 | ||||
|             propertyGrid1.SelectedObject = ProductManager.CurrentConfig; | ||||
|             propertyGrid1.Refresh(); // 强制刷新UI | ||||
|         } | ||||
|         private void toolBtnApply_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             ThreadSafeLogger.Log("用户点击“应用”按钮..."); | ||||
|  | ||||
|             // 1. 确保在应用前,任何可能未触发 PropertyValueChanged 的更改都被保存。 | ||||
|             ConfigurationManager.SaveChanges(); | ||||
|  | ||||
|             // 2. 获取主窗体引用 | ||||
|             var mainForm = this.DockPanel.FindForm() as FrmMain; | ||||
|             mainForm?.ClearStatusStrip(); // 清理UI状态 | ||||
|  | ||||
|             // 3. 从 ConfigurationManager 获取最新的相机配置列表 | ||||
|             var cameraSettings = ConfigurationManager.GetCurrentConfig();//.CameraSettings; | ||||
|  | ||||
|             if (cameraSettings != null) | ||||
|             { | ||||
|                 // 使用全局配置来初始化或重新初始化相机。 | ||||
|                 // CameraManager.Initialize 内部会首先调用 Shutdown,所以这是个完整的重启流程。 | ||||
|                 CameraManager.Initialize(cameraSettings, mainForm); | ||||
|             } | ||||
|  | ||||
|             ThreadSafeLogger.Log("中央配置已成功应用到相机系统。"); | ||||
|             //MessageBox.Show("主配置已应用!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); | ||||
|         } | ||||
|  | ||||
|         private void cmbProducts_SelectedIndexChanged(object sender, EventArgs e) | ||||
|         { | ||||
|             if (cmbProducts.SelectedItem is string selectedProduct && selectedProduct != ProductManager.CurrentProductName) | ||||
|             { | ||||
|                 ProductManager.SwitchToProduct(selectedProduct); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void toolBtnAddProduct_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             // 使用一个简单的输入框来获取新产品名称 | ||||
|             string newName = ShowInputDialog("请输入新产品名称:"); | ||||
|             if (!string.IsNullOrWhiteSpace(newName)) | ||||
|             { | ||||
|                 if (ProductManager.AddNewProduct(newName)) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"成功添加新产品: {newName}"); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     MessageBox.Show("产品名称无效或已存在!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|         } | ||||
|         // 一个简单的输入对话框辅助方法 | ||||
|         public static string ShowInputDialog(string text) | ||||
|         { | ||||
|             Form prompt = new Form() | ||||
|             { | ||||
|                 Width = 300, | ||||
|                 Height = 150, | ||||
|                 FormBorderStyle = FormBorderStyle.FixedDialog, | ||||
|                 Text = text, | ||||
|                 StartPosition = FormStartPosition.CenterScreen | ||||
|             }; | ||||
|             Label textLabel = new Label() { Left = 50, Top = 20, Text = "产品名称:" }; | ||||
|             TextBox textBox = new TextBox() { Left = 50, Top = 50, Width = 200 }; | ||||
|             Button confirmation = new Button() { Text = "确定", Left = 150, Width = 100, Top = 80, DialogResult = DialogResult.OK }; | ||||
|             confirmation.Click += (sender, e) => { prompt.Close(); }; | ||||
|             prompt.Controls.Add(textBox); | ||||
|             prompt.Controls.Add(confirmation); | ||||
|             prompt.Controls.Add(textLabel); | ||||
|             prompt.AcceptButton = confirmation; | ||||
|  | ||||
|             return prompt.ShowDialog() == DialogResult.OK ? textBox.Text : ""; | ||||
|         } | ||||
|  | ||||
|         private void toolBtnDeleteProduct_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             // 1. 获取当前选中的产品 | ||||
|             string productToDelete = ProductManager.CurrentProductName; | ||||
|  | ||||
|             if (string.IsNullOrWhiteSpace(productToDelete) || productToDelete == "DefaultProduct") | ||||
|             { | ||||
|                 MessageBox.Show("不能删除默认产品或无效产品!", "操作无效", MessageBoxButtons.OK, MessageBoxIcon.Warning); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // 2. 弹出安全确认对话框 | ||||
|             var confirmResult = MessageBox.Show($"您确定要永久删除产品 '{productToDelete}' 吗?\n\n此操作不可恢复,将删除其所有相关配置!", | ||||
|                                                  "确认删除", | ||||
|                                                  MessageBoxButtons.YesNo, | ||||
|                                                  MessageBoxIcon.Warning); | ||||
|  | ||||
|             if (confirmResult == DialogResult.Yes) | ||||
|             { | ||||
|                 // 3. 调用核心删除逻辑 | ||||
|                 if (ProductManager.DeleteProduct(productToDelete)) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"用户成功删除了产品 '{productToDelete}'。"); | ||||
|                     // 无需在这里手动更新UI,因为DeleteProduct方法内部会触发 OnProductChanged 事件, | ||||
|                     // 而我们的 UpdateUIForNewProduct 方法会自动响应这个事件并刷新整个界面。 | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     MessageBox.Show($"删除产品 '{productToDelete}' 失败,请查看日志获取详细信息。", "删除失败", MessageBoxButtons.OK, MessageBoxIcon.Error); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										151
									
								
								Check.Main/UI/FrmConfig.resx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								Check.Main/UI/FrmConfig.resx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <root> | ||||
|   <!-- | ||||
|     Microsoft ResX Schema | ||||
|  | ||||
|     Version 2.0 | ||||
|  | ||||
|     The primary goals of this format is to allow a simple XML format | ||||
|     that is mostly human readable. The generation and parsing of the | ||||
|     various data types are done through the TypeConverter classes | ||||
|     associated with the data types. | ||||
|  | ||||
|     Example: | ||||
|  | ||||
|     ... ado.net/XML headers & schema ... | ||||
|     <resheader name="resmimetype">text/microsoft-resx</resheader> | ||||
|     <resheader name="version">2.0</resheader> | ||||
|     <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> | ||||
|     <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> | ||||
|     <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> | ||||
|     <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> | ||||
|     <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> | ||||
|         <value>[base64 mime encoded serialized .NET Framework object]</value> | ||||
|     </data> | ||||
|     <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|         <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> | ||||
|         <comment>This is a comment</comment> | ||||
|     </data> | ||||
|  | ||||
|     There are any number of "resheader" rows that contain simple | ||||
|     name/value pairs. | ||||
|  | ||||
|     Each data row contains a name, and value. The row also contains a | ||||
|     type or mimetype. Type corresponds to a .NET class that support | ||||
|     text/value conversion through the TypeConverter architecture. | ||||
|     Classes that don't support this are serialized and stored with the | ||||
|     mimetype set. | ||||
|  | ||||
|     The mimetype is used for serialized objects, and tells the | ||||
|     ResXResourceReader how to depersist the object. This is currently not | ||||
|     extensible. For a given mimetype the value must be set accordingly: | ||||
|  | ||||
|     Note - application/x-microsoft.net.object.binary.base64 is the format | ||||
|     that the ResXResourceWriter will generate, however the reader can | ||||
|     read any of the formats listed below. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.binary.base64 | ||||
|     value   : The object must be serialized with | ||||
|             : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.soap.base64 | ||||
|     value   : The object must be serialized with | ||||
|             : System.Runtime.Serialization.Formatters.Soap.SoapFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.bytearray.base64 | ||||
|     value   : The object must be serialized into a byte array | ||||
|             : using a System.ComponentModel.TypeConverter | ||||
|             : and then encoded with base64 encoding. | ||||
|     --> | ||||
|   <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> | ||||
|     <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> | ||||
|     <xsd:element name="root" msdata:IsDataSet="true"> | ||||
|       <xsd:complexType> | ||||
|         <xsd:choice maxOccurs="unbounded"> | ||||
|           <xsd:element name="metadata"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" use="required" type="xsd:string" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="assembly"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:attribute name="alias" type="xsd:string" /> | ||||
|               <xsd:attribute name="name" type="xsd:string" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="data"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|                 <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="resheader"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|         </xsd:choice> | ||||
|       </xsd:complexType> | ||||
|     </xsd:element> | ||||
|   </xsd:schema> | ||||
|   <resheader name="resmimetype"> | ||||
|     <value>text/microsoft-resx</value> | ||||
|   </resheader> | ||||
|   <resheader name="version"> | ||||
|     <value>2.0</value> | ||||
|   </resheader> | ||||
|   <resheader name="reader"> | ||||
|     <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
|   <resheader name="writer"> | ||||
|     <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
|   <metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | ||||
|     <value>17, 17</value> | ||||
|   </metadata> | ||||
|   <assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> | ||||
|   <data name="toolBtnApply.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|     <value> | ||||
|         iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 | ||||
|         YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACRSURBVDhPY/j27dt/SjDYACcnJ7IwigEf3n8kCZNswPNb | ||||
|         J/+f6DYF0yA+yQac6Db5f6hWCmwIiE+mC0wIu2DS2Vf/F1x6DefjwlgNyNr34r/0wkdgTMgQDAOQNRNj | ||||
|         CIoBOg0rMTTDMLIhIHbriZeYBmDTiIxBGkEYxge5liQDsGGQqykyAISpZwAlmIEywMAAAAc1/Jwvt6sN | ||||
|         AAAAAElFTkSuQmCC | ||||
| </value> | ||||
|   </data> | ||||
|   <data name="toolBtnAddProduct.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|     <value> | ||||
|         iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 | ||||
|         YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACRSURBVDhPY/j27dt/SjDYACcnJ7IwigEf3n8kCZNswPNb | ||||
|         J/+f6DYF0yA+yQac6Db5f6hWCmwIiE+mC0wIu2DS2Vf/F1x6DefjwlgNyNr34r/0wkdgTMgQDAOQNRNj | ||||
|         CIoBOg0rMTTDMLIhIHbriZeYBmDTiIxBGkEYxge5liQDsGGQqykyAISpZwAlmIEywMAAAAc1/Jwvt6sN | ||||
|         AAAAAElFTkSuQmCC | ||||
| </value> | ||||
|   </data> | ||||
|   <data name="toolBtnDeleteProduct.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|     <value> | ||||
|         iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 | ||||
|         YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACRSURBVDhPY/j27dt/SjDYACcnJ7IwigEf3n8kCZNswPNb | ||||
|         J/+f6DYF0yA+yQac6Db5f6hWCmwIiE+mC0wIu2DS2Vf/F1x6DefjwlgNyNr34r/0wkdgTMgQDAOQNRNj | ||||
|         CIoBOg0rMTTDMLIhIHbriZeYBmDTiIxBGkEYxge5liQDsGGQqykyAISpZwAlmIEywMAAAAc1/Jwvt6sN | ||||
|         AAAAAElFTkSuQmCC | ||||
| </value> | ||||
|   </data> | ||||
| </root> | ||||
							
								
								
									
										66
									
								
								Check.Main/UI/FrmLog.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								Check.Main/UI/FrmLog.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     partial class FrmLog | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Required designer variable. | ||||
|         /// </summary> | ||||
|         private System.ComponentModel.IContainer components = null; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Clean up any resources being used. | ||||
|         /// </summary> | ||||
|         /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             if (disposing && (components != null)) | ||||
|             { | ||||
|                 components.Dispose(); | ||||
|             } | ||||
|             base.Dispose(disposing); | ||||
|         } | ||||
|  | ||||
|         #region Windows Form Designer generated code | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Required method for Designer support - do not modify | ||||
|         /// the contents of this method with the code editor. | ||||
|         /// </summary> | ||||
|         private void InitializeComponent() | ||||
|         { | ||||
|             this.txtLog = new Sunny.UI.UIRichTextBox(); | ||||
|             this.SuspendLayout(); | ||||
|             //  | ||||
|             // txtLog | ||||
|             //  | ||||
|             this.txtLog.Dock = System.Windows.Forms.DockStyle.Fill; | ||||
|             this.txtLog.FillColor = System.Drawing.Color.White; | ||||
|             this.txtLog.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); | ||||
|             this.txtLog.Location = new System.Drawing.Point(0, 0); | ||||
|             this.txtLog.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); | ||||
|             this.txtLog.MinimumSize = new System.Drawing.Size(1, 1); | ||||
|             this.txtLog.Name = "txtLog"; | ||||
|             this.txtLog.Padding = new System.Windows.Forms.Padding(2); | ||||
|             this.txtLog.ReadOnly = true; | ||||
|             this.txtLog.ShowText = false; | ||||
|             this.txtLog.Size = new System.Drawing.Size(399, 299); | ||||
|             this.txtLog.TabIndex = 0; | ||||
|             this.txtLog.TextAlignment = System.Drawing.ContentAlignment.MiddleLeft; | ||||
|             //  | ||||
|             // FrmLog | ||||
|             //  | ||||
|             this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); | ||||
|             this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; | ||||
|             this.ClientSize = new System.Drawing.Size(399, 299); | ||||
|             this.Controls.Add(this.txtLog); | ||||
|             this.Name = "FrmLog"; | ||||
|             this.Text = "FrmLog"; | ||||
|             this.ResumeLayout(false); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         #endregion | ||||
|  | ||||
|         private Sunny.UI.UIRichTextBox txtLog; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										35
									
								
								Check.Main/UI/FrmLog.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								Check.Main/UI/FrmLog.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Data; | ||||
| using System.Drawing; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms; | ||||
| using WeifenLuo.WinFormsUI.Docking; | ||||
|  | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     public partial class FrmLog : DockContent | ||||
|     { | ||||
|         public FrmLog() | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|         } | ||||
|         public void AddLog(string message) | ||||
|         { | ||||
|             if (txtLog.InvokeRequired) | ||||
|             { | ||||
|                 txtLog.BeginInvoke(new Action(() => AddLog(message))); | ||||
|                 return; | ||||
|             } | ||||
|             if (txtLog.Lines.Length > 500) | ||||
|             { | ||||
|                 txtLog.Clear(); | ||||
|             } | ||||
|             txtLog.AppendText(message + Environment.NewLine); | ||||
|             txtLog.ScrollToCaret(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										120
									
								
								Check.Main/UI/FrmLog.resx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								Check.Main/UI/FrmLog.resx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <root> | ||||
|   <!--  | ||||
|     Microsoft ResX Schema  | ||||
|      | ||||
|     Version 2.0 | ||||
|      | ||||
|     The primary goals of this format is to allow a simple XML format  | ||||
|     that is mostly human readable. The generation and parsing of the  | ||||
|     various data types are done through the TypeConverter classes  | ||||
|     associated with the data types. | ||||
|      | ||||
|     Example: | ||||
|      | ||||
|     ... ado.net/XML headers & schema ... | ||||
|     <resheader name="resmimetype">text/microsoft-resx</resheader> | ||||
|     <resheader name="version">2.0</resheader> | ||||
|     <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> | ||||
|     <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> | ||||
|     <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> | ||||
|     <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> | ||||
|     <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> | ||||
|         <value>[base64 mime encoded serialized .NET Framework object]</value> | ||||
|     </data> | ||||
|     <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|         <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> | ||||
|         <comment>This is a comment</comment> | ||||
|     </data> | ||||
|                  | ||||
|     There are any number of "resheader" rows that contain simple  | ||||
|     name/value pairs. | ||||
|      | ||||
|     Each data row contains a name, and value. The row also contains a  | ||||
|     type or mimetype. Type corresponds to a .NET class that support  | ||||
|     text/value conversion through the TypeConverter architecture.  | ||||
|     Classes that don't support this are serialized and stored with the  | ||||
|     mimetype set. | ||||
|      | ||||
|     The mimetype is used for serialized objects, and tells the  | ||||
|     ResXResourceReader how to depersist the object. This is currently not  | ||||
|     extensible. For a given mimetype the value must be set accordingly: | ||||
|      | ||||
|     Note - application/x-microsoft.net.object.binary.base64 is the format  | ||||
|     that the ResXResourceWriter will generate, however the reader can  | ||||
|     read any of the formats listed below. | ||||
|      | ||||
|     mimetype: application/x-microsoft.net.object.binary.base64 | ||||
|     value   : The object must be serialized with  | ||||
|             : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|      | ||||
|     mimetype: application/x-microsoft.net.object.soap.base64 | ||||
|     value   : The object must be serialized with  | ||||
|             : System.Runtime.Serialization.Formatters.Soap.SoapFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.bytearray.base64 | ||||
|     value   : The object must be serialized into a byte array  | ||||
|             : using a System.ComponentModel.TypeConverter | ||||
|             : and then encoded with base64 encoding. | ||||
|     --> | ||||
|   <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> | ||||
|     <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> | ||||
|     <xsd:element name="root" msdata:IsDataSet="true"> | ||||
|       <xsd:complexType> | ||||
|         <xsd:choice maxOccurs="unbounded"> | ||||
|           <xsd:element name="metadata"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" use="required" type="xsd:string" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="assembly"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:attribute name="alias" type="xsd:string" /> | ||||
|               <xsd:attribute name="name" type="xsd:string" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="data"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|                 <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="resheader"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|         </xsd:choice> | ||||
|       </xsd:complexType> | ||||
|     </xsd:element> | ||||
|   </xsd:schema> | ||||
|   <resheader name="resmimetype"> | ||||
|     <value>text/microsoft-resx</value> | ||||
|   </resheader> | ||||
|   <resheader name="version"> | ||||
|     <value>2.0</value> | ||||
|   </resheader> | ||||
|   <resheader name="reader"> | ||||
|     <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
|   <resheader name="writer"> | ||||
|     <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
| </root> | ||||
							
								
								
									
										145
									
								
								Check.Main/UI/ModelListEditor.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								Check.Main/UI/ModelListEditor.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     partial class ModelListEditor | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Required designer variable. | ||||
|         /// </summary> | ||||
|         private System.ComponentModel.IContainer components = null; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Clean up any resources being used. | ||||
|         /// </summary> | ||||
|         /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             if (disposing && (components != null)) | ||||
|             { | ||||
|                 components.Dispose(); | ||||
|             } | ||||
|             base.Dispose(disposing); | ||||
|         } | ||||
|  | ||||
|         #region Windows Form Designer generated code | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Required method for Designer support - do not modify | ||||
|         /// the contents of this method with the code editor. | ||||
|         /// </summary> | ||||
|         private void InitializeComponent() | ||||
|         { | ||||
|             System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ModelListEditor)); | ||||
|             propertyGrid1 = new PropertyGrid(); | ||||
|             listBoxModels = new ListBox(); | ||||
|             toolStrip1 = new ToolStrip(); | ||||
|             toolBtnAdd = new ToolStripButton(); | ||||
|             toolBtnRemove = new ToolStripButton(); | ||||
|             toolBtnSet = new ToolStripButton(); | ||||
|             tableLayoutPanel1 = new TableLayoutPanel(); | ||||
|             toolStrip1.SuspendLayout(); | ||||
|             tableLayoutPanel1.SuspendLayout(); | ||||
|             SuspendLayout(); | ||||
|             //  | ||||
|             // propertyGrid1 | ||||
|             //  | ||||
|             propertyGrid1.Dock = DockStyle.Fill; | ||||
|             propertyGrid1.Location = new Point(245, 4); | ||||
|             propertyGrid1.Margin = new Padding(4); | ||||
|             propertyGrid1.Name = "propertyGrid1"; | ||||
|             propertyGrid1.Size = new Size(316, 400); | ||||
|             propertyGrid1.TabIndex = 1; | ||||
|             //  | ||||
|             // listBoxModels | ||||
|             //  | ||||
|             listBoxModels.Dock = DockStyle.Fill; | ||||
|             listBoxModels.FormattingEnabled = true; | ||||
|             listBoxModels.ItemHeight = 17; | ||||
|             listBoxModels.Location = new Point(4, 4); | ||||
|             listBoxModels.Margin = new Padding(4); | ||||
|             listBoxModels.Name = "listBoxModels"; | ||||
|             listBoxModels.Size = new Size(233, 400); | ||||
|             listBoxModels.TabIndex = 2; | ||||
|             listBoxModels.SelectedIndexChanged += listBoxModels_SelectedIndexChanged; | ||||
|             //  | ||||
|             // toolStrip1 | ||||
|             //  | ||||
|             toolStrip1.Items.AddRange(new ToolStripItem[] { toolBtnAdd, toolBtnRemove, toolBtnSet }); | ||||
|             toolStrip1.Location = new Point(0, 0); | ||||
|             toolStrip1.Name = "toolStrip1"; | ||||
|             toolStrip1.Size = new Size(565, 25); | ||||
|             toolStrip1.TabIndex = 3; | ||||
|             toolStrip1.Text = "toolStrip1"; | ||||
|             //  | ||||
|             // toolBtnAdd | ||||
|             //  | ||||
|             toolBtnAdd.DisplayStyle = ToolStripItemDisplayStyle.Text; | ||||
|             toolBtnAdd.Image = (Image)resources.GetObject("toolBtnAdd.Image"); | ||||
|             toolBtnAdd.ImageTransparentColor = Color.Magenta; | ||||
|             toolBtnAdd.Name = "toolBtnAdd"; | ||||
|             toolBtnAdd.Size = new Size(36, 22); | ||||
|             toolBtnAdd.Text = "添加"; | ||||
|             toolBtnAdd.Click += toolBtnAdd_Click; | ||||
|             //  | ||||
|             // toolBtnRemove | ||||
|             //  | ||||
|             toolBtnRemove.DisplayStyle = ToolStripItemDisplayStyle.Text; | ||||
|             toolBtnRemove.Image = (Image)resources.GetObject("toolBtnRemove.Image"); | ||||
|             toolBtnRemove.ImageTransparentColor = Color.Magenta; | ||||
|             toolBtnRemove.Name = "toolBtnRemove"; | ||||
|             toolBtnRemove.Size = new Size(36, 22); | ||||
|             toolBtnRemove.Text = "移除"; | ||||
|             toolBtnRemove.Click += toolBtnRemove_Click; | ||||
|             //  | ||||
|             // toolBtnSet | ||||
|             //  | ||||
|             toolBtnSet.DisplayStyle = ToolStripItemDisplayStyle.Text; | ||||
|             toolBtnSet.Image = (Image)resources.GetObject("toolBtnSet.Image"); | ||||
|             toolBtnSet.ImageTransparentColor = Color.Magenta; | ||||
|             toolBtnSet.Name = "toolBtnSet"; | ||||
|             toolBtnSet.Size = new Size(60, 22); | ||||
|             toolBtnSet.Text = "应用配置"; | ||||
|             toolBtnSet.Click += toolBtnSet_Click; | ||||
|             //  | ||||
|             // tableLayoutPanel1 | ||||
|             //  | ||||
|             tableLayoutPanel1.ColumnCount = 2; | ||||
|             tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 42.65487F)); | ||||
|             tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 57.34513F)); | ||||
|             tableLayoutPanel1.Controls.Add(listBoxModels, 0, 0); | ||||
|             tableLayoutPanel1.Controls.Add(propertyGrid1, 1, 0); | ||||
|             tableLayoutPanel1.Dock = DockStyle.Fill; | ||||
|             tableLayoutPanel1.Location = new Point(0, 25); | ||||
|             tableLayoutPanel1.Name = "tableLayoutPanel1"; | ||||
|             tableLayoutPanel1.RowCount = 1; | ||||
|             tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); | ||||
|             tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); | ||||
|             tableLayoutPanel1.Size = new Size(565, 408); | ||||
|             tableLayoutPanel1.TabIndex = 4; | ||||
|             //  | ||||
|             // ModelListEditor | ||||
|             //  | ||||
|             AutoScaleDimensions = new SizeF(7F, 17F); | ||||
|             AutoScaleMode = AutoScaleMode.Font; | ||||
|             ClientSize = new Size(565, 433); | ||||
|             Controls.Add(tableLayoutPanel1); | ||||
|             Controls.Add(toolStrip1); | ||||
|             Name = "ModelListEditor"; | ||||
|             Text = "模型配置"; | ||||
|             toolStrip1.ResumeLayout(false); | ||||
|             toolStrip1.PerformLayout(); | ||||
|             tableLayoutPanel1.ResumeLayout(false); | ||||
|             ResumeLayout(false); | ||||
|             PerformLayout(); | ||||
|         } | ||||
|  | ||||
|         #endregion | ||||
|  | ||||
|         private PropertyGrid propertyGrid1; | ||||
|         private ListBox listBoxModels; | ||||
|         private ToolStrip toolStrip1; | ||||
|         private ToolStripButton toolBtnAdd; | ||||
|         private ToolStripButton toolBtnRemove; | ||||
|         private ToolStripButton toolBtnSet; | ||||
|         private TableLayoutPanel tableLayoutPanel1; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										175
									
								
								Check.Main/UI/ModelListEditor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								Check.Main/UI/ModelListEditor.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | ||||
| using Check.Main.Camera; | ||||
| using Check.Main.Infer; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Data; | ||||
| using System.Drawing; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms; | ||||
| using System.Xml.Serialization; | ||||
| using WeifenLuo.WinFormsUI.Docking; | ||||
|  | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     public partial class ModelListEditor : DockContent | ||||
|     { | ||||
|         public List<ModelSettings> _settingsList { get; private set; } | ||||
|         private readonly string _configFilePath = Path.Combine(Application.StartupPath, "model.xml"); | ||||
|         public ModelListEditor() | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|             _settingsList = new List<ModelSettings>(); | ||||
|             LoadSettings(); | ||||
|             RefreshListBox(); | ||||
|             AdaptForDialogMode(); // 调整UI为对话框模式 | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 用于从外部接收一个设置列表进行编辑。 | ||||
|         /// </summary> | ||||
|         /// <param name="settingsToEdit">要编辑的相机设置列表</param> | ||||
|         public ModelListEditor(List<ModelSettings> settingsToEdit) | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|  | ||||
|             // 创建一个现有列表的副本进行编辑,这样如果用户点“取消”,原始列表不会被影响 | ||||
|             _settingsList = settingsToEdit != null | ||||
|                 ? settingsToEdit.Select(s => s.Clone() as ModelSettings).ToList() // 深度克隆更好,这里为简化用浅克隆 | ||||
|                 : new List<ModelSettings>(); | ||||
|  | ||||
|             // 别忘了为列表中的每个对象重新订阅事件 | ||||
|             foreach (var setting in _settingsList) | ||||
|             { | ||||
|                 setting.PropertyChanged += Setting_PropertyChanged; | ||||
|             } | ||||
|  | ||||
|             RefreshListBox(); | ||||
|             AdaptForDialogMode(); // 调整UI为对话框模式 | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 【新增的方法】 | ||||
|         /// 调整UI,使其更像一个对话框。 | ||||
|         /// </summary> | ||||
|         private void AdaptForDialogMode() | ||||
|         { | ||||
|             // 将“应用配置”按钮改为“确定” | ||||
|             toolBtnSet.Text = "确定"; | ||||
|             toolBtnSet.ToolTipText = "保存更改并关闭窗口"; | ||||
|  | ||||
|             // 新增一个“取消”按钮 | ||||
|             var toolBtnCancel = new ToolStripButton("取消") | ||||
|             { | ||||
|                 DisplayStyle = ToolStripItemDisplayStyle.Text, | ||||
|                 Alignment = ToolStripItemAlignment.Right // 靠右对齐 | ||||
|             }; | ||||
|             toolBtnCancel.Click += (sender, e) => | ||||
|             { | ||||
|                 this.DialogResult = DialogResult.Cancel; | ||||
|                 this.Close(); | ||||
|             }; | ||||
|             toolStrip1.Items.Add(toolBtnCancel); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 【新增的事件处理方法】 | ||||
|         /// 当PropertyGrid中的属性值被用户修改后触发此事件。 | ||||
|         /// </summary> | ||||
|  | ||||
|         // 刷新左侧的相机列表 | ||||
|         private void RefreshListBox() | ||||
|         { | ||||
|             listBoxModels.Items.Clear(); | ||||
|             foreach (var setting in _settingsList) | ||||
|             { | ||||
|                 listBoxModels.Items.Add(setting.Name); | ||||
|             } | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 【全新的事件处理方法】 | ||||
|         /// 监听单个CameraSettings对象的属性变更通知。 | ||||
|         /// </summary> | ||||
|         private void Setting_PropertyChanged(object sender, PropertyChangedEventArgs e) | ||||
|         { | ||||
|             // 检查是不是“Name”属性发生了变化 | ||||
|             if (e.PropertyName == nameof(CameraSettings.Name)) | ||||
|             { | ||||
|                 // 'sender' 就是那个属性发生了变化的 CameraSettings 对象 | ||||
|                 var changedSetting = sender as ModelSettings; | ||||
|                 if (changedSetting == null) return; | ||||
|  | ||||
|                 // 在 _settingsList 中找到这个对象的索引 | ||||
|                 int index = _settingsList.IndexOf(changedSetting); | ||||
|  | ||||
|                 // 如果找到了,就更新ListBox中对应项的显示文本 | ||||
|                 if (index >= 0) | ||||
|                 { | ||||
|                     // 使用Invoke确保线程安全,虽然在此场景下通常不是问题,但这是个好习惯 | ||||
|                     this.Invoke(new Action(() => | ||||
|                     { | ||||
|                         listBoxModels.Items[index] = changedSetting.Name; | ||||
|                     })); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void listBoxModels_SelectedIndexChanged(object sender, EventArgs e) | ||||
|         { | ||||
|             if (listBoxModels.SelectedIndex >= 0) | ||||
|             { | ||||
|                 propertyGrid1.SelectedObject = _settingsList[listBoxModels.SelectedIndex]; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 propertyGrid1.SelectedObject = null; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void toolBtnAdd_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             var newSetting = new ModelSettings { Name = $"model-{_settingsList.Count + 1}" }; | ||||
|             _settingsList.Add(newSetting); | ||||
|             newSetting.PropertyChanged += Setting_PropertyChanged; | ||||
|             RefreshListBox(); | ||||
|             listBoxModels.SelectedIndex = listBoxModels.Items.Count - 1; | ||||
|         } | ||||
|  | ||||
|         private void toolBtnRemove_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             if (listBoxModels.SelectedIndex >= 0) | ||||
|             { | ||||
|                 _settingsList.RemoveAt(listBoxModels.SelectedIndex); | ||||
|                 propertyGrid1.SelectedObject = null; | ||||
|                 RefreshListBox(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void toolBtnSet_Click(object sender, EventArgs e) | ||||
|         { | ||||
|             this.DialogResult = DialogResult.OK; | ||||
|             this.Close(); | ||||
|         } | ||||
|         // 加载XML配置文件 | ||||
|         private void LoadSettings() | ||||
|         { | ||||
|             if (!File.Exists(_configFilePath)) return; | ||||
|             try | ||||
|             { | ||||
|                 XmlSerializer serializer = new XmlSerializer(typeof(List<ModelSettings>)); | ||||
|                 using (FileStream fs = new FileStream(_configFilePath, FileMode.Open)) | ||||
|                 { | ||||
|                     _settingsList = (List<ModelSettings>)serializer.Deserialize(fs); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 MessageBox.Show("加载相机配置失败: " + ex.Message); | ||||
|             } | ||||
|             foreach (var setting in _settingsList) | ||||
|             { | ||||
|                 setting.PropertyChanged += Setting_PropertyChanged; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										172
									
								
								Check.Main/UI/ModelListEditor.resx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								Check.Main/UI/ModelListEditor.resx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <root> | ||||
|   <!-- | ||||
|     Microsoft ResX Schema | ||||
|  | ||||
|     Version 2.0 | ||||
|  | ||||
|     The primary goals of this format is to allow a simple XML format | ||||
|     that is mostly human readable. The generation and parsing of the | ||||
|     various data types are done through the TypeConverter classes | ||||
|     associated with the data types. | ||||
|  | ||||
|     Example: | ||||
|  | ||||
|     ... ado.net/XML headers & schema ... | ||||
|     <resheader name="resmimetype">text/microsoft-resx</resheader> | ||||
|     <resheader name="version">2.0</resheader> | ||||
|     <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> | ||||
|     <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> | ||||
|     <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> | ||||
|     <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> | ||||
|     <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> | ||||
|         <value>[base64 mime encoded serialized .NET Framework object]</value> | ||||
|     </data> | ||||
|     <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|         <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> | ||||
|         <comment>This is a comment</comment> | ||||
|     </data> | ||||
|  | ||||
|     There are any number of "resheader" rows that contain simple | ||||
|     name/value pairs. | ||||
|  | ||||
|     Each data row contains a name, and value. The row also contains a | ||||
|     type or mimetype. Type corresponds to a .NET class that support | ||||
|     text/value conversion through the TypeConverter architecture. | ||||
|     Classes that don't support this are serialized and stored with the | ||||
|     mimetype set. | ||||
|  | ||||
|     The mimetype is used for serialized objects, and tells the | ||||
|     ResXResourceReader how to depersist the object. This is currently not | ||||
|     extensible. For a given mimetype the value must be set accordingly: | ||||
|  | ||||
|     Note - application/x-microsoft.net.object.binary.base64 is the format | ||||
|     that the ResXResourceWriter will generate, however the reader can | ||||
|     read any of the formats listed below. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.binary.base64 | ||||
|     value   : The object must be serialized with | ||||
|             : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.soap.base64 | ||||
|     value   : The object must be serialized with | ||||
|             : System.Runtime.Serialization.Formatters.Soap.SoapFormatter | ||||
|             : and then encoded with base64 encoding. | ||||
|  | ||||
|     mimetype: application/x-microsoft.net.object.bytearray.base64 | ||||
|     value   : The object must be serialized into a byte array | ||||
|             : using a System.ComponentModel.TypeConverter | ||||
|             : and then encoded with base64 encoding. | ||||
|     --> | ||||
|   <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> | ||||
|     <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> | ||||
|     <xsd:element name="root" msdata:IsDataSet="true"> | ||||
|       <xsd:complexType> | ||||
|         <xsd:choice maxOccurs="unbounded"> | ||||
|           <xsd:element name="metadata"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" use="required" type="xsd:string" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="assembly"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:attribute name="alias" type="xsd:string" /> | ||||
|               <xsd:attribute name="name" type="xsd:string" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="data"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|                 <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> | ||||
|               <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> | ||||
|               <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> | ||||
|               <xsd:attribute ref="xml:space" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|           <xsd:element name="resheader"> | ||||
|             <xsd:complexType> | ||||
|               <xsd:sequence> | ||||
|                 <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||||
|               </xsd:sequence> | ||||
|               <xsd:attribute name="name" type="xsd:string" use="required" /> | ||||
|             </xsd:complexType> | ||||
|           </xsd:element> | ||||
|         </xsd:choice> | ||||
|       </xsd:complexType> | ||||
|     </xsd:element> | ||||
|   </xsd:schema> | ||||
|   <resheader name="resmimetype"> | ||||
|     <value>text/microsoft-resx</value> | ||||
|   </resheader> | ||||
|   <resheader name="version"> | ||||
|     <value>2.0</value> | ||||
|   </resheader> | ||||
|   <resheader name="reader"> | ||||
|     <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
|   <resheader name="writer"> | ||||
|     <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
|   </resheader> | ||||
|   <metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | ||||
|     <value>17, 17</value> | ||||
|   </metadata> | ||||
|   <assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> | ||||
|   <data name="toolBtnAdd.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|     <value> | ||||
|         iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 | ||||
|         YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG | ||||
|         YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9 | ||||
|         0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw | ||||
|         bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc | ||||
|         VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9 | ||||
|         c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32 | ||||
|         Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo | ||||
|         mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+ | ||||
|         kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D | ||||
|         TgDQASA1MVpwzwAAAABJRU5ErkJggg== | ||||
| </value> | ||||
|   </data> | ||||
|   <data name="toolBtnRemove.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|     <value> | ||||
|         iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 | ||||
|         YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG | ||||
|         YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9 | ||||
|         0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw | ||||
|         bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc | ||||
|         VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9 | ||||
|         c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32 | ||||
|         Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo | ||||
|         mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+ | ||||
|         kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D | ||||
|         TgDQASA1MVpwzwAAAABJRU5ErkJggg== | ||||
| </value> | ||||
|   </data> | ||||
|   <data name="toolBtnSet.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||||
|     <value> | ||||
|         iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 | ||||
|         YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG | ||||
|         YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9 | ||||
|         0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw | ||||
|         bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc | ||||
|         VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9 | ||||
|         c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32 | ||||
|         Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo | ||||
|         mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+ | ||||
|         kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D | ||||
|         TgDQASA1MVpwzwAAAABJRU5ErkJggg== | ||||
| </value> | ||||
|   </data> | ||||
|   <metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||||
|     <value>37</value> | ||||
|   </metadata> | ||||
| </root> | ||||
							
								
								
									
										381
									
								
								Check.Main/UI/ZoomPictureBox.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										381
									
								
								Check.Main/UI/ZoomPictureBox.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,381 @@ | ||||
| using OpenCvSharp; | ||||
| using OpenCvSharp.Extensions; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Drawing; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms; | ||||
|  | ||||
| namespace Check.Main.UI | ||||
| { | ||||
|     public class ZoomPictureBox : PictureBox, IDisposable | ||||
|     { | ||||
|         private float zoom = 1.0f; | ||||
|         private System.Drawing.Point imageLocation; | ||||
|         private Image image; | ||||
|         private bool dragging = false; | ||||
|         private System.Drawing.Point mouseDownPos; | ||||
|         private System.Drawing.Point imageLocationOnMouseDown; | ||||
|  | ||||
|         // 矩形绘制相关 | ||||
|         private bool isDrawingRect = false;      // 绘制状态 | ||||
|         private Rectangle currentRect;           // 当前绘制矩形 | ||||
|         private Rectangle storedRect;            // 上次绘制并固定的矩形 | ||||
|         private bool hasStoredRect = false;      // 是否存在固定矩形 | ||||
|         private System.Drawing.Point rectStartPoint; | ||||
|  | ||||
|         // 一个私有的锁对象,专门用于保护对 'image' 字段的访问。 | ||||
|         private readonly object imageLock = new object(); | ||||
|  | ||||
|         private bool croppingEnabled = true; | ||||
|         //private bool croppingEnabled = true; | ||||
|         [Category("Behavior")] | ||||
|         [Description("是否允许通过右键绘制矩形裁剪区域。设置为 false 则不再响应右键并清除已有矩形。")] | ||||
|         public bool CroppingEnabled | ||||
|         { | ||||
|             get => croppingEnabled; | ||||
|             set | ||||
|             { | ||||
|                 croppingEnabled = value; | ||||
|                 // 禁用裁剪时清除所有矩形和裁剪结果 | ||||
|                 if (!croppingEnabled) | ||||
|                 { | ||||
|                     isDrawingRect = false; | ||||
|                     hasStoredRect = false; | ||||
|                     LastCroppedMat?.Dispose(); | ||||
|                     LastCroppedMat = null; | ||||
|                 } | ||||
|                 Invalidate(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 裁剪完成事件,外部可订阅以接收 Mat | ||||
|         public event Action<Mat> Cropped; | ||||
|  | ||||
|         public Mat LastCroppedMat { get; private set; } | ||||
|  | ||||
|         private Color backgroundFillColor = Color.LightSteelBlue; | ||||
|         private Color rectangleColor = Color.Red; | ||||
|         private int rectangleThickness = 2; | ||||
|  | ||||
|         [Category("Appearance")] | ||||
|         [Description("指定控件背景的填充颜色。使用 BackColor 属性同步更新。")] | ||||
|         public Color BackgroundFillColor | ||||
|         { | ||||
|             get => backgroundFillColor; | ||||
|             set | ||||
|             { | ||||
|                 backgroundFillColor = value; | ||||
|                 base.BackColor = value;  // 同步 WinForms BackColor | ||||
|                 Invalidate(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         [Category("Appearance")] | ||||
|         [Description("绘制矩形框的颜色。")] | ||||
|         public Color RectangleColor { get => rectangleColor; set { rectangleColor = value; Invalidate(); } } | ||||
|  | ||||
|         [Category("Appearance")] | ||||
|         [Description("绘制矩形框的线宽。")] | ||||
|         public int RectangleThickness { get => rectangleThickness; set { rectangleThickness = Math.Max(1, value); Invalidate(); } } | ||||
|  | ||||
|         public ZoomPictureBox() | ||||
|         { | ||||
|             DoubleBuffered = true; | ||||
|             ResizeRedraw = true; | ||||
|             base.BackColor = backgroundFillColor; | ||||
|             SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); | ||||
|             MouseWheel += ZoomPictureBox_MouseWheel; | ||||
|             MouseDown += ZoomPictureBox_MouseDown; | ||||
|             MouseMove += ZoomPictureBox_MouseMove; | ||||
|             MouseUp += ZoomPictureBox_MouseUp; | ||||
|             DoubleClick += ZoomPictureBox_DoubleClick; | ||||
|         } | ||||
|         // 重写背景绘制,使用自定义背景色 | ||||
|         protected override void OnPaintBackground(PaintEventArgs e) | ||||
|         { | ||||
|             e.Graphics.Clear(backgroundFillColor); | ||||
|         } | ||||
|         //[Browsable(false)] | ||||
|         //public new Image Image | ||||
|         //{ | ||||
|         //    get => image; | ||||
|         //    set | ||||
|         //    { | ||||
|         //        image = value; | ||||
|         //        zoom = 1.0f; | ||||
|         //        LastCroppedMat?.Dispose(); | ||||
|         //        LastCroppedMat = null; | ||||
|         //        isDrawingRect = false; | ||||
|         //        hasStoredRect = false; | ||||
|         //        FitImage(); | ||||
|         //        Invalidate(); | ||||
|         //    } | ||||
|         //} | ||||
|         // 【关键优化】 | ||||
|         // 2. 重写 Control.Dispose 方法来释放我们自己的资源 | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             if (disposing) | ||||
|             { | ||||
|                 // 在这里释放托管和非托管资源 | ||||
|                 // 清理事件订阅 (虽然在这个类里没有,但这是个好习惯) | ||||
|  | ||||
|                 // 释放我们持有的最后一个图像资源 | ||||
|                 lock (imageLock) | ||||
|                 { | ||||
|                     this.image?.Dispose(); | ||||
|                     this.image = null; | ||||
|                 } | ||||
|  | ||||
|                 // 释放我们持有的最后一个裁剪结果资源 | ||||
|                 this.LastCroppedMat?.Dispose(); | ||||
|                 this.LastCroppedMat = null; | ||||
|             } | ||||
|  | ||||
|             // 调用基类的 Dispose 方法来完成标准清理 | ||||
|             base.Dispose(disposing); | ||||
|         } | ||||
|  | ||||
|         // 重写 Image 属性,但不做任何额外操作,因为我们将通过一个新方法来更新它。 | ||||
|         [Browsable(false)] | ||||
|         public new Image Image | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 // 在访问时也加锁,确保读取的是一个完整的对象 | ||||
|                 lock (imageLock) | ||||
|                 { | ||||
|                     return image; | ||||
|                 } | ||||
|             } | ||||
|             // set 访问器可以保持原样,但我们不再直接使用它来更新图像 | ||||
|             private set | ||||
|             { | ||||
|                 lock (imageLock) | ||||
|                 { | ||||
|                     image = value; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         protected override void OnPaint(PaintEventArgs e) | ||||
|         { | ||||
|             base.OnPaint(e); | ||||
|             var g = e.Graphics; | ||||
|             //在绘制前获取锁,确保在绘制期间,image对象不会被其他线程替换或释放 | ||||
|             lock (imageLock) | ||||
|             { | ||||
|                 if (image != null) | ||||
|                 { | ||||
|                     try | ||||
|                     { | ||||
|                         int w = (int)(image.Width * zoom); | ||||
|                         int h = (int)(image.Height * zoom); | ||||
|                         g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; | ||||
|                         g.DrawImage(image, new Rectangle(imageLocation, new System.Drawing.Size(w, h))); | ||||
|                     } | ||||
|                     catch (Exception) | ||||
|                     { | ||||
|  | ||||
|                         // 如果在绘制时仍然发生罕见的GDI+错误,静默忽略,避免程序崩溃。 | ||||
|                         // 这通常意味着图像状态仍然存在问题,但UI不会因此卡死。 | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             //if (image != null) | ||||
|             //{ | ||||
|             //    int w = (int)(image.Width * zoom); | ||||
|             //    int h = (int)(image.Height * zoom); | ||||
|             //    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; | ||||
|             //    g.DrawImage(image, new Rectangle(imageLocation, new System.Drawing.Size(w, h))); | ||||
|             //} | ||||
|             // 仅当允许裁剪时才绘制矩形 | ||||
|             if (croppingEnabled) | ||||
|             { | ||||
|                 using (var pen = new Pen(rectangleColor, rectangleThickness)) | ||||
|                 { | ||||
|                     if (hasStoredRect) g.DrawRectangle(pen, storedRect); | ||||
|                     else if (isDrawingRect) g.DrawRectangle(pen, currentRect); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 一个线程安全的、用于更新显示图像的公共方法。 | ||||
|         /// 这个方法将替换掉直接的 pictureBox.Image = ... 赋值。 | ||||
|         /// </summary> | ||||
|         /// <param name="newImage">要显示的新图像。此方法会接管该对象的所有权。</param> | ||||
|         public void SetImageThreadSafe(Image newImage) | ||||
|         { | ||||
|             Image oldImage = null; | ||||
|  | ||||
|             lock (imageLock) | ||||
|             { | ||||
|                 // 1. 保存旧图像的引用,以便在锁外部释放它 | ||||
|                 oldImage = this.image; | ||||
|                 // 2. 将新图像赋值给成员字段 | ||||
|                 this.image = newImage; | ||||
|             } | ||||
|             // 3. 【关键】在锁的外部释放旧图像,避免长时间持有锁 | ||||
|             oldImage?.Dispose(); | ||||
|  | ||||
|             // 4. 计算自适应并触发重绘 | ||||
|             FitImage(); | ||||
|             Invalidate(); | ||||
|         } | ||||
|  | ||||
|         private void FitImage() | ||||
|         { | ||||
|             //if (image == null) return; | ||||
|             //float sx = (float)ClientSize.Width / image.Width; | ||||
|             //float sy = (float)ClientSize.Height / image.Height; | ||||
|             //zoom = Math.Min(sx, sy); | ||||
|             //CenterImage(); | ||||
|             //在访问image属性前获取锁 | ||||
|             lock (imageLock) | ||||
|             { | ||||
|                 if (image == null) return; | ||||
|  | ||||
|                 try | ||||
|                 { | ||||
|                     float sx = (float)ClientSize.Width / image.Width; | ||||
|                     float sy = (float)ClientSize.Height / image.Height; | ||||
|                     zoom = Math.Min(sx, sy); | ||||
|                     CenterImage(); | ||||
|                 } | ||||
|                 catch (Exception) | ||||
|                 { | ||||
|  | ||||
|                     // 忽略错误,同 OnPaint | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void CenterImage() | ||||
|         { | ||||
|             if (image == null) return; | ||||
|             int w = (int)(image.Width * zoom); | ||||
|             int h = (int)(image.Height * zoom); | ||||
|             imageLocation = new System.Drawing.Point((ClientSize.Width - w) / 2, (ClientSize.Height - h) / 2); | ||||
|         } | ||||
|  | ||||
|         private void ZoomPictureBox_MouseWheel(object sender, MouseEventArgs e) | ||||
|         { | ||||
|             if (image == null) return; | ||||
|             float oldZoom = zoom; | ||||
|             zoom *= e.Delta > 0 ? 1.1f : 1 / 1.1f; | ||||
|             zoom = Math.Max(0.1f, Math.Min(zoom, 100f)); | ||||
|             var m = e.Location; | ||||
|             float ix = (m.X - imageLocation.X) / oldZoom; | ||||
|             float iy = (m.Y - imageLocation.Y) / oldZoom; | ||||
|             imageLocation = new System.Drawing.Point((int)(m.X - ix * zoom), (int)(m.Y - iy * zoom)); | ||||
|             Invalidate(); | ||||
|         } | ||||
|  | ||||
|         private void ZoomPictureBox_MouseDown(object sender, MouseEventArgs e) | ||||
|         { | ||||
|             if (image == null) return; | ||||
|             if (e.Button == MouseButtons.Left) | ||||
|             { | ||||
|                 hasStoredRect = false; | ||||
|                 LastCroppedMat?.Dispose(); | ||||
|                 LastCroppedMat = null; | ||||
|  | ||||
|                 dragging = true; | ||||
|                 mouseDownPos = e.Location; | ||||
|                 imageLocationOnMouseDown = imageLocation; | ||||
|                 Cursor = Cursors.Hand; | ||||
|             } | ||||
|             else if (e.Button == MouseButtons.Right && croppingEnabled) | ||||
|             { | ||||
|                 // 清除上次固定矩形和裁剪 | ||||
|                 hasStoredRect = false; | ||||
|                 LastCroppedMat?.Dispose(); | ||||
|                 LastCroppedMat = null; | ||||
|  | ||||
|                 // 开始新绘制 | ||||
|                 isDrawingRect = true; | ||||
|                 rectStartPoint = e.Location; | ||||
|                 currentRect = new Rectangle(e.Location, System.Drawing.Size.Empty); | ||||
|                 Invalidate(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void ZoomPictureBox_MouseMove(object sender, MouseEventArgs e) | ||||
|         { | ||||
|             if (dragging) | ||||
|             { | ||||
|                 var delta = new System.Drawing.Point(e.X - mouseDownPos.X, e.Y - mouseDownPos.Y); | ||||
|                 imageLocation = new System.Drawing.Point(imageLocationOnMouseDown.X + delta.X, imageLocationOnMouseDown.Y + delta.Y); | ||||
|                 Invalidate(); | ||||
|             } | ||||
|             else if (isDrawingRect) | ||||
|             { | ||||
|                 int x = Math.Min(rectStartPoint.X, e.X); | ||||
|                 int y = Math.Min(rectStartPoint.Y, e.Y); | ||||
|                 int w = Math.Abs(e.X - rectStartPoint.X); | ||||
|                 int h = Math.Abs(e.Y - rectStartPoint.Y); | ||||
|                 currentRect = new Rectangle(x, y, w, h); | ||||
|                 Invalidate(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void ZoomPictureBox_MouseUp(object sender, MouseEventArgs e) | ||||
|         { | ||||
|             if (e.Button == MouseButtons.Left && dragging) | ||||
|             { | ||||
|                 dragging = false; | ||||
|                 Cursor = Cursors.Default; | ||||
|             } | ||||
|             else if (e.Button == MouseButtons.Right && isDrawingRect) | ||||
|             { | ||||
|                 isDrawingRect = false; | ||||
|                 // 固定当前矩形框 | ||||
|                 storedRect = currentRect; | ||||
|                 hasStoredRect = true; | ||||
|                 if (croppingEnabled && image is Bitmap bmp) | ||||
|                 { | ||||
|                     this.LastCroppedMat?.Dispose(); | ||||
|                     this.LastCroppedMat = null; | ||||
|                     Mat srcMat = null; | ||||
|                     try | ||||
|                     { | ||||
|                         int ix = (int)((storedRect.X - imageLocation.X) / zoom); | ||||
|                         int iy = (int)((storedRect.Y - imageLocation.Y) / zoom); | ||||
|                         int iw = (int)(storedRect.Width / zoom); | ||||
|                         int ih = (int)(storedRect.Height / zoom); | ||||
|                         ix = Math.Max(0, ix); | ||||
|                         iy = Math.Max(0, iy); | ||||
|                         if (ix + iw > bmp.Width) iw = bmp.Width - ix; | ||||
|                         if (iy + ih > bmp.Height) ih = bmp.Height - iy; | ||||
|                         if (iw > 0 && ih > 0) | ||||
|                         { | ||||
|                             srcMat = BitmapConverter.ToMat(bmp); | ||||
|                             var roi = new Rect(ix, iy, iw, ih); | ||||
|                             LastCroppedMat = new Mat(srcMat, roi); | ||||
|                             Cropped?.Invoke(LastCroppedMat); | ||||
|                             bmp.Dispose(); | ||||
|                         } | ||||
|                     } | ||||
|                     finally | ||||
|                     { | ||||
|                         // 5.确保从Bitmap转换来的 srcMat 被释放 | ||||
|                         srcMat?.Dispose(); | ||||
|                     } | ||||
|                 } | ||||
|                 Invalidate(); | ||||
|             } | ||||
|         } | ||||
|         private void ZoomPictureBox_DoubleClick(object sender, EventArgs e) | ||||
|         { | ||||
|             if (image == null) return; | ||||
|             FitImage(); | ||||
|             Invalidate(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user