From 5d2a74d64b15c7142c5c7999d8174df7d7591b93 Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Fri, 24 Apr 2026 13:34:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BB=E7=95=8C=E9=9D=A2=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E5=9B=BE=E5=83=8F=E4=B8=8E=E6=8E=A2=E6=B5=8B=E5=99=A8=E5=8F=8C?= =?UTF-8?q?=E9=98=9F=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XplorePlane/App.config | 7 +- XplorePlane/App.xaml.cs | 120 +++++---- .../Services/MainViewport/DetectorFrame.cs | 31 +++ .../DetectorFramePipelineService.cs | 160 ++++++++++++ .../IDetectorFramePipelineService.cs | 15 ++ .../MainViewport/IMainViewportService.cs | 25 ++ .../MainViewport/MainViewportService.cs | 214 ++++++++++++++++ .../MainViewport/MainViewportSourceMode.cs | 8 + XplorePlane/ViewModels/Main/MainViewModel.cs | 233 +++++++++--------- .../ViewModels/Main/ViewportPanelViewModel.cs | 76 ++---- XplorePlane/Views/Main/MainWindow.xaml | 87 +++---- XplorePlane/Views/Main/ViewportPanelView.xaml | 26 +- 12 files changed, 713 insertions(+), 289 deletions(-) create mode 100644 XplorePlane/Services/MainViewport/DetectorFrame.cs create mode 100644 XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs create mode 100644 XplorePlane/Services/MainViewport/IDetectorFramePipelineService.cs create mode 100644 XplorePlane/Services/MainViewport/IMainViewportService.cs create mode 100644 XplorePlane/Services/MainViewport/MainViewportService.cs create mode 100644 XplorePlane/Services/MainViewport/MainViewportSourceMode.cs diff --git a/XplorePlane/App.config b/XplorePlane/App.config index 7d6d23c..7465d52 100644 --- a/XplorePlane/App.config +++ b/XplorePlane/App.config @@ -88,6 +88,11 @@ + + + + + @@ -164,4 +169,4 @@ - \ No newline at end of file + diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs index bd10aa4..57f657f 100644 --- a/XplorePlane/App.xaml.cs +++ b/XplorePlane/App.xaml.cs @@ -13,10 +13,10 @@ using XP.Camera; using XP.Common.Configs; using XP.Common.Database.Implementations; using XP.Common.Database.Interfaces; -using XP.Common.GeneralForm.Views; using XP.Common.Dump.Configs; using XP.Common.Dump.Implementations; using XP.Common.Dump.Interfaces; +using XP.Common.GeneralForm.Views; using XP.Common.Helpers; using XP.Common.Localization.Configs; using XP.Common.Localization.Extensions; @@ -35,9 +35,10 @@ using XplorePlane.Services; using XplorePlane.Services.AppState; using XplorePlane.Services.Camera; using XplorePlane.Services.Cnc; +using XplorePlane.Services.InspectionResults; +using XplorePlane.Services.MainViewport; using XplorePlane.Services.Matrix; using XplorePlane.Services.Measurement; -using XplorePlane.Services.InspectionResults; using XplorePlane.Services.Recipe; using XplorePlane.ViewModels; using XplorePlane.ViewModels.Cnc; @@ -51,10 +52,10 @@ namespace XplorePlane /// public partial class App : Application { - protected override void OnStartup(StartupEventArgs e) - { - // 设置 Telerik Windows11 主题,缩小 Ribbon 整体尺寸 - StyleManager.ApplicationTheme = new Windows11Theme(); + protected override void OnStartup(StartupEventArgs e) + { + // 设置 Telerik Windows11 主题,缩小 Ribbon 整体尺寸 + StyleManager.ApplicationTheme = new Windows11Theme(); // 强制使用中文 UI,确保 ImageProcessing 库显示中文 var zhCN = new CultureInfo("zh-CN"); @@ -66,8 +67,8 @@ namespace XplorePlane // 配置 Serilog 日志系统 ConfigureLogging(); - // 捕获未处理的异常 - AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + // 捕获未处理的异常 + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; DispatcherUnhandledException += OnDispatcherUnhandledException; try @@ -102,12 +103,12 @@ namespace XplorePlane private void ConfigureLogging() { - // 加载Serilog配置 | Load Serilog configuration - SerilogConfig serilogConfig = ConfigLoader.LoadSerilogConfig(); - // 初始化Serilog(全局唯一)| Initialize Serilog (global singleton) - SerilogInitializer.Initialize(serilogConfig); + // 加载 Serilog 配置 | Load Serilog configuration + SerilogConfig serilogConfig = ConfigLoader.LoadSerilogConfig(); + // 初始化 Serilog(全局唯一)| Initialize Serilog (global singleton) + SerilogInitializer.Initialize(serilogConfig); - // 记录应用启动日志 | Log application startup + // 记录应用启动日志 | Log application startup Log.Information("========================================"); Log.Information("XplorePlane 应用程序启动"); Log.Information("========================================"); @@ -183,23 +184,39 @@ namespace XplorePlane Log.Error(ex, "导航相机服务资源释放失败"); } - // 释放SQLite数据库资源 | Release SQLite database resources - try - { - var bootstrapper = AppBootstrapper.Instance; + // 释放主界面探测器帧流水线资源 + try + { + var bootstrapper = AppBootstrapper.Instance; if (bootstrapper != null) { - var dbContext = bootstrapper.Container.Resolve(); // 从Prism容器获取IDbContext实例(单例)| Get IDbContext instance from Prism container (singleton) + var detectorFramePipelineService = bootstrapper.Container.Resolve(); + detectorFramePipelineService?.Dispose(); + Log.Information("主界面探测器帧流水线资源已释放"); + } + } + catch (Exception ex) + { + Log.Error(ex, "主界面探测器帧流水线资源释放失败"); + } + + // 释放SQLite数据库资源 | Release SQLite database resources + try + { + var bootstrapper = AppBootstrapper.Instance; + if (bootstrapper != null) + { + var dbContext = bootstrapper.Container.Resolve(); // 从 Prism 容器获取 IDbContext 实例(单例)| Get IDbContext instance from Prism container (singleton) dbContext?.Dispose(); Log.Information("数据库资源已成功释放 | Database resources released successfully"); } - } - catch (Exception ex) - { - Log.Error(ex, "数据库资源释放失败,忽略该错误继续退出 | Database resource release failed, ignoring error and continuing exit"); - } + } + catch (Exception ex) + { + Log.Error(ex, "数据库资源释放失败,忽略该错误继续退出 | Database resource release failed, ignoring error and continuing exit"); + } - Log.CloseAndFlush(); + Log.CloseAndFlush(); base.OnExit(e); } @@ -208,7 +225,7 @@ namespace XplorePlane /// private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) { - var exception = e.ExceptionObject as Exception; + var exception = e.ExceptionObject as Exception; Log.Fatal(exception, "应用程序发生未处理的异常"); MessageBox.Show( @@ -242,15 +259,15 @@ namespace XplorePlane public new IContainerProvider Container => base.Container; + private bool _modulesInitialized = false; + + private string _cameraError; + public AppBootstrapper() { Instance = this; } - private bool _modulesInitialized = false; - - private string? _cameraError; - protected override Window CreateShell() { // 提前初始化模块,确保硬件服务在 MainWindow XAML 解析前已注册 @@ -267,6 +284,16 @@ namespace XplorePlane { TryConnectCamera(); + // 初始化主界面探测器帧流水线,开始接收探测器图像事件 + try + { + _ = Container.Resolve(); + } + catch (Exception ex) + { + Log.Error(ex, "初始化主界面探测器帧流水线失败"); + } + // 通知 ViewModel 相机状态已确定,启动实时预览或显示错误 try { @@ -329,11 +356,11 @@ namespace XplorePlane // 注册 Serilog 的 ILogger 实例 containerRegistry.RegisterInstance(Log.Logger); - // 注册 XP.Common.ILoggerService 适配器 - containerRegistry.RegisterSingleton(); + // 注册 XP.Common.ILoggerService 适配器 + containerRegistry.RegisterSingleton(); - // 注册视图和视图模型 - containerRegistry.RegisterForNavigation(); + // 注册视图和视图模型 + containerRegistry.RegisterForNavigation(); containerRegistry.Register(); containerRegistry.RegisterSingleton(); @@ -364,16 +391,7 @@ namespace XplorePlane containerRegistry.RegisterInstance(sqliteConfig); containerRegistry.RegisterSingleton(); - // 注册硬件库的 ViewModel(供 ViewModelLocator 自动装配) - //containerRegistry.Register(); - // 手动注册射线源模块的所有服务(确保 DryIoc 容器中可用,避免模块加载顺序问题) - //var raySourceConfig = XP.Hardware.RaySource.Config.ConfigLoader.LoadConfig(); - //containerRegistry.RegisterInstance(raySourceConfig); - //containerRegistry.RegisterSingleton(); - //containerRegistry.RegisterSingleton(); - //containerRegistry.RegisterSingleton(); - - // 手动注册通用模块的服务(本地化、Dump) + // 注册通用模块的服务(本地化、Dump) containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(() => XP.Common.Helpers.ConfigLoader.LoadDumpConfig()); @@ -385,6 +403,10 @@ namespace XplorePlane containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); + // ── 主界面实时图像 / 探测器双队列服务(单例)── + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + // ── CNC / 矩阵 ViewModel(瞬态)── containerRegistry.Register(); containerRegistry.Register(); @@ -405,13 +427,13 @@ namespace XplorePlane protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { - // 所有模块服务已在 RegisterTypes 中手动注册 - // CommonModule: ILocalizationService, IDumpService - // RaySourceModule: IRaySourceService, IRaySourceFactory, IFilamentLifetimeService + // 所有模块服务已在 RegisterTypes 中手动注册 + // CommonModule: ILocalizationService, IDumpService + // RaySourceModule: IRaySourceService, IRaySourceFactory, IFilamentLifetimeService + + // 注册通用模块(必须最先加载)| Register common module (must be loaded first) + moduleCatalog.AddModule(); - // 注册通用模块(必须最先加载)| Register common module (must be loaded first) - moduleCatalog.AddModule(); - // 注册其他模块 | Register other modules moduleCatalog.AddModule(); moduleCatalog.AddModule(); diff --git a/XplorePlane/Services/MainViewport/DetectorFrame.cs b/XplorePlane/Services/MainViewport/DetectorFrame.cs new file mode 100644 index 0000000..9fd7cb0 --- /dev/null +++ b/XplorePlane/Services/MainViewport/DetectorFrame.cs @@ -0,0 +1,31 @@ +using System; +using System.Windows.Media.Imaging; + +namespace XplorePlane.Services.MainViewport +{ + public sealed class DetectorFrame + { + public DetectorFrame( + long frameId, + DateTime captureTime, + int width, + int height, + ushort[] rawPixels, + BitmapSource previewImage) + { + FrameId = frameId; + CaptureTime = captureTime; + Width = width; + Height = height; + RawPixels = rawPixels; + PreviewImage = previewImage; + } + + public long FrameId { get; } + public DateTime CaptureTime { get; } + public int Width { get; } + public int Height { get; } + public ushort[] RawPixels { get; } + public BitmapSource PreviewImage { get; } + } +} diff --git a/XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs b/XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs new file mode 100644 index 0000000..46c5bb5 --- /dev/null +++ b/XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs @@ -0,0 +1,160 @@ +using Prism.Events; +using System; +using System.Collections.Concurrent; +using System.Configuration; +using System.Threading; +using System.Threading.Tasks; +using XP.Common.Converters; +using XP.Common.Logging.Interfaces; +using XP.Hardware.Detector.Abstractions; +using XP.Hardware.Detector.Abstractions.Events; + +namespace XplorePlane.Services.MainViewport +{ + public sealed class DetectorFramePipelineService : IDetectorFramePipelineService + { + private readonly ConcurrentQueue _acquireQueue = new(); + private readonly ConcurrentQueue _processQueue = new(); + private readonly SemaphoreSlim _processSignal = new(0); + private readonly CancellationTokenSource _shutdown = new(); + private readonly IMainViewportService _mainViewportService; + private readonly ILoggerService _logger; + private readonly Task _processConsumerTask; + private int _acquireQueueCount; + private int _processQueueCount; + private long _receivedFrameCount; + private bool _disposed; + + public DetectorFramePipelineService( + IEventAggregator eventAggregator, + IMainViewportService mainViewportService, + ILoggerService logger) + { + ArgumentNullException.ThrowIfNull(eventAggregator); + _mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService)); + _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); + + AcquireQueueCapacity = ReadInt("DetectorPipeline:AcquireQueueCapacity", 16, 1); + ProcessQueueCapacity = ReadInt("DetectorPipeline:ProcessQueueCapacity", 8, 1); + ProcessEveryNFrames = ReadInt("DetectorPipeline:ProcessEveryNFrames", 1, 1); + + eventAggregator.GetEvent() + .Subscribe(OnImageCaptured, ThreadOption.BackgroundThread); + + _processConsumerTask = Task.Run(ProcessLoopAsync); + } + + public int AcquireQueueCount => Volatile.Read(ref _acquireQueueCount); + public int ProcessQueueCount => Volatile.Read(ref _processQueueCount); + public int AcquireQueueCapacity { get; } + public int ProcessQueueCapacity { get; } + public int ProcessEveryNFrames { get; } + + public event EventHandler ProcessFrameDequeued; + + private void OnImageCaptured(ImageCapturedEventArgs args) + { + if (_disposed || args?.ImageData == null || args.Width <= 0 || args.Height <= 0) + return; + + try + { + var rawPixels = new ushort[args.ImageData.Length]; + Array.Copy(args.ImageData, rawPixels, rawPixels.Length); + + var bitmap = XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource(rawPixels, (int)args.Width, (int)args.Height); + bitmap.Freeze(); + + var frame = new DetectorFrame( + frameId: args.FrameNumber, + captureTime: args.CaptureTime, + width: (int)args.Width, + height: (int)args.Height, + rawPixels: rawPixels, + previewImage: bitmap); + + EnqueueBounded(_acquireQueue, frame, AcquireQueueCapacity, ref _acquireQueueCount); + _mainViewportService.UpdateDetectorFrame(frame); + + var sequence = Interlocked.Increment(ref _receivedFrameCount); + if ((sequence - 1) % ProcessEveryNFrames == 0) + { + EnqueueBounded(_processQueue, frame, ProcessQueueCapacity, ref _processQueueCount); + _processSignal.Release(); + } + } + catch (Exception ex) + { + _logger.Error(ex, "探测器帧进入主界面流水线失败"); + } + } + + private async Task ProcessLoopAsync() + { + try + { + while (!_shutdown.IsCancellationRequested) + { + await _processSignal.WaitAsync(_shutdown.Token).ConfigureAwait(false); + + while (_processQueue.TryDequeue(out var frame)) + { + Interlocked.Decrement(ref _processQueueCount); + ProcessFrameDequeued?.Invoke(this, frame); + } + } + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + _logger.Error(ex, "探测器处理队列后台消费者异常退出"); + } + } + + private static void EnqueueBounded( + ConcurrentQueue queue, + DetectorFrame frame, + int capacity, + ref int queueCount) + { + queue.Enqueue(frame); + var count = Interlocked.Increment(ref queueCount); + + while (count > capacity && queue.TryDequeue(out _)) + { + count = Interlocked.Decrement(ref queueCount); + } + } + + private static int ReadInt(string key, int defaultValue, int minValue) + { + var raw = ConfigurationManager.AppSettings[key]; + if (!int.TryParse(raw, out var parsed)) + return defaultValue; + + return parsed < minValue ? minValue : parsed; + } + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _shutdown.Cancel(); + + try + { + _processConsumerTask.Wait(TimeSpan.FromSeconds(2)); + } + catch + { + } + + _processSignal.Dispose(); + _shutdown.Dispose(); + } + } +} diff --git a/XplorePlane/Services/MainViewport/IDetectorFramePipelineService.cs b/XplorePlane/Services/MainViewport/IDetectorFramePipelineService.cs new file mode 100644 index 0000000..24d9cea --- /dev/null +++ b/XplorePlane/Services/MainViewport/IDetectorFramePipelineService.cs @@ -0,0 +1,15 @@ +using System; + +namespace XplorePlane.Services.MainViewport +{ + public interface IDetectorFramePipelineService : IDisposable + { + int AcquireQueueCount { get; } + int ProcessQueueCount { get; } + int AcquireQueueCapacity { get; } + int ProcessQueueCapacity { get; } + int ProcessEveryNFrames { get; } + + event EventHandler ProcessFrameDequeued; + } +} diff --git a/XplorePlane/Services/MainViewport/IMainViewportService.cs b/XplorePlane/Services/MainViewport/IMainViewportService.cs new file mode 100644 index 0000000..a9247bc --- /dev/null +++ b/XplorePlane/Services/MainViewport/IMainViewportService.cs @@ -0,0 +1,25 @@ +using System; +using System.Windows.Media; + +namespace XplorePlane.Services.MainViewport +{ + public interface IMainViewportService + { + MainViewportSourceMode CurrentSourceMode { get; } + bool IsRealtimeDisplayEnabled { get; } + ImageSource CurrentDisplayImage { get; } + string CurrentDisplayInfo { get; } + ImageSource LatestDetectorImage { get; } + ImageSource LatestManualImage { get; } + + event EventHandler StateChanged; + + void SetRealtimeDisplayEnabled(bool isEnabled); + + void SetSourceMode(MainViewportSourceMode sourceMode); + + void UpdateDetectorFrame(DetectorFrame frame); + + void SetManualImage(ImageSource image, string filePath); + } +} diff --git a/XplorePlane/Services/MainViewport/MainViewportService.cs b/XplorePlane/Services/MainViewport/MainViewportService.cs new file mode 100644 index 0000000..ac13c50 --- /dev/null +++ b/XplorePlane/Services/MainViewport/MainViewportService.cs @@ -0,0 +1,214 @@ +using System; +using System.Configuration; +using System.IO; +using System.Windows.Media; +using XP.Common.Logging.Interfaces; + +namespace XplorePlane.Services.MainViewport +{ + public sealed class MainViewportService : IMainViewportService + { + private readonly object _syncRoot = new(); + private readonly ILoggerService _logger; + + private MainViewportSourceMode _currentSourceMode = MainViewportSourceMode.LiveDetector; + private bool _isRealtimeDisplayEnabled; + private ImageSource _currentDisplayImage; + private string _currentDisplayInfo = "等待探测器图像..."; + private ImageSource _latestDetectorImage; + private string _latestDetectorInfo = "等待探测器图像..."; + private ImageSource _latestManualImage; + private string _latestManualInfo = "未加载手动图像"; + + public MainViewportService(ILoggerService logger) + { + _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); + _isRealtimeDisplayEnabled = ReadBoolean("MainViewport:RealtimeEnabledDefault", true); + } + + public MainViewportSourceMode CurrentSourceMode + { + get + { + lock (_syncRoot) + { + return _currentSourceMode; + } + } + } + + public bool IsRealtimeDisplayEnabled + { + get + { + lock (_syncRoot) + { + return _isRealtimeDisplayEnabled; + } + } + } + + public ImageSource CurrentDisplayImage + { + get + { + lock (_syncRoot) + { + return _currentDisplayImage; + } + } + } + + public string CurrentDisplayInfo + { + get + { + lock (_syncRoot) + { + return _currentDisplayInfo; + } + } + } + + public ImageSource LatestDetectorImage + { + get + { + lock (_syncRoot) + { + return _latestDetectorImage; + } + } + } + + public ImageSource LatestManualImage + { + get + { + lock (_syncRoot) + { + return _latestManualImage; + } + } + } + + public event EventHandler StateChanged; + + public void SetRealtimeDisplayEnabled(bool isEnabled) + { + bool changed; + lock (_syncRoot) + { + changed = _isRealtimeDisplayEnabled != isEnabled; + _isRealtimeDisplayEnabled = isEnabled; + + if (_currentSourceMode == MainViewportSourceMode.LiveDetector && isEnabled) + { + ApplyLiveDetectorDisplay_NoLock(); + } + } + + if (!changed) + return; + + _logger.Info("主界面实时刷新已{State}", isEnabled ? "开启" : "关闭"); + RaiseStateChanged(); + } + + public void SetSourceMode(MainViewportSourceMode sourceMode) + { + bool changed; + lock (_syncRoot) + { + changed = _currentSourceMode != sourceMode; + _currentSourceMode = sourceMode; + + switch (sourceMode) + { + case MainViewportSourceMode.LiveDetector: + ApplyLiveDetectorDisplay_NoLock(); + break; + case MainViewportSourceMode.ManualImage: + ApplyManualDisplay_NoLock(); + break; + } + } + + if (!changed) + return; + + _logger.Info("主界面图像来源已切换为 {Mode}", sourceMode); + RaiseStateChanged(); + } + + public void UpdateDetectorFrame(DetectorFrame frame) + { + if (frame == null) + return; + + bool shouldRaise = false; + lock (_syncRoot) + { + _latestDetectorImage = frame.PreviewImage; + _latestDetectorInfo = $"实时探测器图像 {frame.Width}x{frame.Height} 帧#{frame.FrameId} {frame.CaptureTime:HH:mm:ss.fff}"; + + if (_currentSourceMode == MainViewportSourceMode.LiveDetector && _isRealtimeDisplayEnabled) + { + _currentDisplayImage = _latestDetectorImage; + _currentDisplayInfo = _latestDetectorInfo; + shouldRaise = true; + } + } + + if (shouldRaise) + RaiseStateChanged(); + } + + public void SetManualImage(ImageSource image, string filePath) + { + if (image == null) + return; + + var fileName = string.IsNullOrWhiteSpace(filePath) ? "未命名图像" : Path.GetFileName(filePath); + + lock (_syncRoot) + { + _latestManualImage = image; + _latestManualInfo = $"手动加载图像 {fileName}"; + _currentSourceMode = MainViewportSourceMode.ManualImage; + _currentDisplayImage = _latestManualImage; + _currentDisplayInfo = _latestManualInfo; + } + + _logger.Info("主界面已加载手动图像 {FileName}", fileName); + RaiseStateChanged(); + } + + private void ApplyLiveDetectorDisplay_NoLock() + { + _currentDisplayImage = _latestDetectorImage; + _currentDisplayInfo = _latestDetectorImage == null + ? "等待探测器图像..." + : _latestDetectorInfo; + } + + private void ApplyManualDisplay_NoLock() + { + _currentDisplayImage = _latestManualImage; + _currentDisplayInfo = _latestManualImage == null + ? "未加载手动图像" + : _latestManualInfo; + } + + private void RaiseStateChanged() + { + StateChanged?.Invoke(this, EventArgs.Empty); + } + + private static bool ReadBoolean(string key, bool defaultValue) + { + var raw = ConfigurationManager.AppSettings[key]; + return bool.TryParse(raw, out var parsed) ? parsed : defaultValue; + } + } +} diff --git a/XplorePlane/Services/MainViewport/MainViewportSourceMode.cs b/XplorePlane/Services/MainViewport/MainViewportSourceMode.cs new file mode 100644 index 0000000..3c778d0 --- /dev/null +++ b/XplorePlane/Services/MainViewport/MainViewportSourceMode.cs @@ -0,0 +1,8 @@ +namespace XplorePlane.Services.MainViewport +{ + public enum MainViewportSourceMode + { + LiveDetector = 0, + ManualImage = 1 + } +} diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index fdd75c9..fbfa353 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -1,15 +1,15 @@ -using Prism.Commands; +using Microsoft.Win32; +using Prism.Commands; using Prism.Events; using Prism.Ioc; using Prism.Mvvm; -using Microsoft.Win32; using System; using System.Collections.ObjectModel; using System.Configuration; using System.IO; using System.Windows; using System.Windows.Media.Imaging; -using XplorePlane.Events; +using XplorePlane.Services.MainViewport; using XplorePlane.ViewModels.Cnc; using XplorePlane.Views; using XplorePlane.Views.Cnc; @@ -22,85 +22,13 @@ namespace XplorePlane.ViewModels public class MainViewModel : BindableBase { private const double CncEditorHostWidth = 502d; + private readonly ILoggerService _logger; private readonly IContainerProvider _containerProvider; - private readonly IEventAggregator _eventAggregator; + private readonly IMainViewportService _mainViewportService; private readonly CncEditorViewModel _cncEditorViewModel; private readonly CncPageView _cncPageView; - private string _licenseInfo = "当前时间"; - public string LicenseInfo - { - get => _licenseInfo; - set => SetProperty(ref _licenseInfo, value); - } - - public ObservableCollection NavigationTree { get; set; } - - // 导航命令 - public DelegateCommand NavigateHomeCommand { get; set; } - public DelegateCommand NavigateInspectCommand { get; set; } - public DelegateCommand OpenFileCommand { get; set; } - public DelegateCommand ExportCommand { get; set; } - public DelegateCommand ClearCommand { get; set; } - public DelegateCommand EditPropertiesCommand { get; set; } - - // 窗口打开命令 - public DelegateCommand OpenImageProcessingCommand { get; } - public DelegateCommand LoadImageCommand { get; } - public DelegateCommand OpenPipelineEditorCommand { get; } - public DelegateCommand OpenCncEditorCommand { get; } - public DelegateCommand OpenMatrixEditorCommand { get; } - public DelegateCommand OpenToolboxCommand { get; } - public DelegateCommand OpenLibraryVersionsCommand { get; } - public DelegateCommand OpenUserManualCommand { get; } - public DelegateCommand OpenCameraSettingsCommand { get; } - public DelegateCommand NewCncProgramCommand { get; } - public DelegateCommand SaveCncProgramCommand { get; } - public DelegateCommand LoadCncProgramCommand { get; } - public DelegateCommand InsertReferencePointCommand { get; } - public DelegateCommand InsertSavePositionCommand { get; } - public DelegateCommand InsertCompleteProgramCommand { get; } - public DelegateCommand InsertInspectionMarkerCommand { get; } - public DelegateCommand InsertInspectionModuleCommand { get; } - public DelegateCommand InsertSaveNodeCommand { get; } - public DelegateCommand InsertPauseDialogCommand { get; } - public DelegateCommand InsertWaitDelayCommand { get; } - - // 硬件命令 - public DelegateCommand AxisResetCommand { get; } - public DelegateCommand OpenDetectorConfigCommand { get; } - public DelegateCommand OpenMotionDebugCommand { get; } - public DelegateCommand OpenPlcAddrConfigCommand { get; } - public DelegateCommand OpenRaySourceConfigCommand { get; } - public DelegateCommand WarmUpCommand { get; } - - // 设置命令 - public DelegateCommand OpenLanguageSwitcherCommand { get; } - public DelegateCommand OpenRealTimeLogViewerCommand { get; } - - /// 右侧图像区域内容 | Right-side image panel content - public object ImagePanelContent - { - get => _imagePanelContent; - set => SetProperty(ref _imagePanelContent, value); - } - - /// 右侧图像区域宽度 | Right-side image panel width - public GridLength ImagePanelWidth - { - get => _imagePanelWidth; - set => SetProperty(ref _imagePanelWidth, value); - } - - /// 主视图区宽度 | Main viewport width - public GridLength ViewportPanelWidth - { - get => _viewportPanelWidth; - set => SetProperty(ref _viewportPanelWidth, value); - } - - // 窗口引用(单例窗口防止重复打开) private Window _motionDebugWindow; private Window _detectorConfigWindow; private Window _plcAddrConfigWindow; @@ -108,21 +36,28 @@ namespace XplorePlane.ViewModels private Window _toolboxWindow; private Window _raySourceConfigWindow; private object _imagePanelContent; - private GridLength _viewportPanelWidth = new GridLength(1, GridUnitType.Star); - private GridLength _imagePanelWidth = new GridLength(320); + private GridLength _viewportPanelWidth = new(1, GridUnitType.Star); + private GridLength _imagePanelWidth = new(320); private bool _isCncEditorMode; + private string _licenseInfo = "当前时间"; - public MainViewModel(ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator) + public MainViewModel( + ILoggerService logger, + IContainerProvider containerProvider, + IEventAggregator eventAggregator, + IMainViewportService mainViewportService) { _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); _containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider)); - _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); + ArgumentNullException.ThrowIfNull(eventAggregator); + _mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService)); _cncEditorViewModel = _containerProvider.Resolve(); _cncPageView = new CncPageView { DataContext = _cncEditorViewModel }; + _mainViewportService.StateChanged += OnMainViewportStateChanged; + NavigationTree = new ObservableCollection(); - // 导航命令 NavigateHomeCommand = new DelegateCommand(OnNavigateHome); NavigateInspectCommand = new DelegateCommand(OnNavigateInspect); OpenFileCommand = new DelegateCommand(OnOpenFile); @@ -130,7 +65,6 @@ namespace XplorePlane.ViewModels ClearCommand = new DelegateCommand(OnClear); EditPropertiesCommand = new DelegateCommand(OnEditProperties); - // 窗口打开命令 OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理")); LoadImageCommand = new DelegateCommand(ExecuteLoadImage); OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器")); @@ -152,7 +86,6 @@ namespace XplorePlane.ViewModels InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute())); InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute())); - // 硬件命令 AxisResetCommand = new DelegateCommand(ExecuteAxisReset); OpenDetectorConfigCommand = new DelegateCommand(ExecuteOpenDetectorConfig); OpenMotionDebugCommand = new DelegateCommand(ExecuteOpenMotionDebug); @@ -160,9 +93,9 @@ namespace XplorePlane.ViewModels OpenRaySourceConfigCommand = new DelegateCommand(ExecuteOpenRaySourceConfig); WarmUpCommand = new DelegateCommand(ExecuteWarmUp); - // 设置命令 OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher); OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer); + UseLiveDetectorSourceCommand = new DelegateCommand(ExecuteUseLiveDetectorSource); ImagePanelContent = new PipelineEditorView(); ViewportPanelWidth = new GridLength(1, GridUnitType.Star); @@ -171,11 +104,86 @@ namespace XplorePlane.ViewModels _logger.Info("MainViewModel 已初始化"); } - #region 通用窗口辅助方法 + public string LicenseInfo + { + get => _licenseInfo; + set => SetProperty(ref _licenseInfo, value); + } + + public ObservableCollection NavigationTree { get; set; } + + public DelegateCommand NavigateHomeCommand { get; set; } + public DelegateCommand NavigateInspectCommand { get; set; } + public DelegateCommand OpenFileCommand { get; set; } + public DelegateCommand ExportCommand { get; set; } + public DelegateCommand ClearCommand { get; set; } + public DelegateCommand EditPropertiesCommand { get; set; } + + public DelegateCommand OpenImageProcessingCommand { get; } + public DelegateCommand LoadImageCommand { get; } + public DelegateCommand OpenPipelineEditorCommand { get; } + public DelegateCommand OpenCncEditorCommand { get; } + public DelegateCommand OpenMatrixEditorCommand { get; } + public DelegateCommand OpenToolboxCommand { get; } + public DelegateCommand OpenLibraryVersionsCommand { get; } + public DelegateCommand OpenUserManualCommand { get; } + public DelegateCommand OpenCameraSettingsCommand { get; } + public DelegateCommand NewCncProgramCommand { get; } + public DelegateCommand SaveCncProgramCommand { get; } + public DelegateCommand LoadCncProgramCommand { get; } + public DelegateCommand InsertReferencePointCommand { get; } + public DelegateCommand InsertSavePositionCommand { get; } + public DelegateCommand InsertCompleteProgramCommand { get; } + public DelegateCommand InsertInspectionMarkerCommand { get; } + public DelegateCommand InsertInspectionModuleCommand { get; } + public DelegateCommand InsertSaveNodeCommand { get; } + public DelegateCommand InsertPauseDialogCommand { get; } + public DelegateCommand InsertWaitDelayCommand { get; } + + public DelegateCommand AxisResetCommand { get; } + public DelegateCommand OpenDetectorConfigCommand { get; } + public DelegateCommand OpenMotionDebugCommand { get; } + public DelegateCommand OpenPlcAddrConfigCommand { get; } + public DelegateCommand OpenRaySourceConfigCommand { get; } + public DelegateCommand WarmUpCommand { get; } + + public DelegateCommand OpenLanguageSwitcherCommand { get; } + public DelegateCommand OpenRealTimeLogViewerCommand { get; } + public DelegateCommand UseLiveDetectorSourceCommand { get; } + + public bool IsMainViewportRealtimeEnabled + { + get => _mainViewportService.IsRealtimeDisplayEnabled; + set + { + if (_mainViewportService.IsRealtimeDisplayEnabled == value) + return; + + _mainViewportService.SetRealtimeDisplayEnabled(value); + RaisePropertyChanged(); + } + } + + public bool IsUsingLiveDetectorSource => _mainViewportService.CurrentSourceMode == MainViewportSourceMode.LiveDetector; + + public object ImagePanelContent + { + get => _imagePanelContent; + set => SetProperty(ref _imagePanelContent, value); + } + + public GridLength ImagePanelWidth + { + get => _imagePanelWidth; + set => SetProperty(ref _imagePanelWidth, value); + } + + public GridLength ViewportPanelWidth + { + get => _viewportPanelWidth; + set => SetProperty(ref _viewportPanelWidth, value); + } - /// - /// 显示一个新窗口(非模态) - /// private void ShowWindow(Window window, string name) { window.Owner = Application.Current.MainWindow; @@ -183,9 +191,6 @@ namespace XplorePlane.ViewModels _logger.Info("{Name} 窗口已打开", name); } - /// - /// 显示或激活单例窗口(非模态,防止重复打开) - /// private void ShowOrActivate(Window currentWindow, Action setWindow, Func factory, string name) { if (currentWindow != null && currentWindow.IsLoaded) @@ -203,10 +208,6 @@ namespace XplorePlane.ViewModels _logger.Info("{Name} 窗口已打开", name); } - #endregion - - #region 窗口打开命令实现 - private void ExecuteOpenToolbox() { ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "算子工具箱"); @@ -230,7 +231,6 @@ namespace XplorePlane.ViewModels private void ExecuteCncEditorAction(Action action) { ArgumentNullException.ThrowIfNull(action); - ShowCncEditor(); action(_cncEditorViewModel); } @@ -298,15 +298,12 @@ namespace XplorePlane.ViewModels } } - #endregion - - #region 硬件命令实现 - private void ExecuteAxisReset() { var result = MessageBox.Show("确认执行轴复位操作?", "轴复位", MessageBoxButton.OKCancel, MessageBoxImage.Question); - if (result != MessageBoxResult.OK) return; + if (result != MessageBoxResult.OK) + return; try { @@ -379,8 +376,8 @@ namespace XplorePlane.ViewModels bitmap.EndInit(); bitmap.Freeze(); - _eventAggregator.GetEvent() - .Publish(new ManualImageLoadedPayload(bitmap, dialog.FileName)); + _mainViewportService.SetManualImage(bitmap, dialog.FileName); + RaisePropertyChanged(nameof(IsUsingLiveDetectorSource)); } catch (Exception ex) { @@ -389,11 +386,18 @@ namespace XplorePlane.ViewModels } } + private void ExecuteUseLiveDetectorSource() + { + _mainViewportService.SetSourceMode(MainViewportSourceMode.LiveDetector); + RaisePropertyChanged(nameof(IsUsingLiveDetectorSource)); + } + private void ExecuteWarmUp() - { + { var messageBoxResult = MessageBox.Show("确认执行射线源暖机操作?", "暖机", MessageBoxButton.OKCancel, MessageBoxImage.Question); - if (messageBoxResult != MessageBoxResult.OK) return; + if (messageBoxResult != MessageBoxResult.OK) + return; try { @@ -417,10 +421,6 @@ namespace XplorePlane.ViewModels } } - #endregion - - #region 设置命令实现 - private void ExecuteOpenLanguageSwitcher() { try @@ -445,10 +445,6 @@ namespace XplorePlane.ViewModels () => new XP.Common.GeneralForm.Views.RealTimeLogViewer(), "实时日志"); } - #endregion - - #region 导航命令实现 - private void OnNavigateHome() { _logger.Info("导航到主页"); @@ -485,6 +481,13 @@ namespace XplorePlane.ViewModels LicenseInfo = "编辑属性"; } - #endregion + private void OnMainViewportStateChanged(object sender, EventArgs e) + { + Application.Current?.Dispatcher?.BeginInvoke(new Action(() => + { + RaisePropertyChanged(nameof(IsMainViewportRealtimeEnabled)); + RaisePropertyChanged(nameof(IsUsingLiveDetectorSource)); + })); + } } } diff --git a/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs b/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs index 599b638..7e4ce0a 100644 --- a/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs +++ b/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs @@ -1,24 +1,19 @@ -using Prism.Events; using Prism.Mvvm; using System; -using System.Threading; using System.Windows; using System.Windows.Media; -using System.Windows.Media.Imaging; using XP.Common.Logging.Interfaces; -using XP.Hardware.Detector.Abstractions; -using XP.Hardware.Detector.Abstractions.Events; -using XplorePlane.Events; +using XplorePlane.Services.MainViewport; namespace XplorePlane.ViewModels { /// - /// 实时图像 ViewModel,订阅探测器采集事件并显示图像 + /// 主界面实时图像 ViewModel,只绑定主界面视口仲裁后的显示状态。 /// public class ViewportPanelViewModel : BindableBase { private readonly ILoggerService _logger; - private int _isProcessingFrame; + private readonly IMainViewportService _mainViewportService; private ImageSource _imageSource; public ImageSource ImageSource @@ -27,81 +22,38 @@ namespace XplorePlane.ViewModels set => SetProperty(ref _imageSource, value); } - private string _imageInfo = "等待图像..."; + private string _imageInfo = "等待探测器图像..."; public string ImageInfo { get => _imageInfo; set => SetProperty(ref _imageInfo, value); } - public ViewportPanelViewModel(IEventAggregator eventAggregator, ILoggerService logger) + public ViewportPanelViewModel(IMainViewportService mainViewportService, ILoggerService logger) { + _mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService)); _logger = logger?.ForModule(); - eventAggregator.GetEvent() - .Subscribe(OnImageCaptured, ThreadOption.BackgroundThread); - eventAggregator.GetEvent() - .Subscribe(OnManualImageLoaded, ThreadOption.UIThread); - eventAggregator.GetEvent() - .Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread); + _mainViewportService.StateChanged += OnMainViewportStateChanged; + UpdateFromState(); } - private void OnImageCaptured(ImageCapturedEventArgs args) + private void OnMainViewportStateChanged(object sender, EventArgs e) { - if (args?.ImageData == null || args.Width == 0 || args.Height == 0) return; - - // 帧节流:上一帧未消费完则跳过 - if (Interlocked.CompareExchange(ref _isProcessingFrame, 1, 0) != 0) return; - try { - var bitmap = ConvertToBitmapSource(args.ImageData, (int)args.Width, (int)args.Height); - bitmap.Freeze(); - - var info = $"{args.Width}×{args.Height} 帧#{args.FrameNumber} {args.CaptureTime:HH:mm:ss.fff}"; - - Application.Current?.Dispatcher?.BeginInvoke(new Action(() => - { - try - { - ImageSource = bitmap; - ImageInfo = info; - } - finally - { - Interlocked.Exchange(ref _isProcessingFrame, 0); - } - })); + Application.Current?.Dispatcher?.BeginInvoke(new Action(UpdateFromState)); } catch (Exception ex) { - Interlocked.Exchange(ref _isProcessingFrame, 0); - _logger?.Error(ex, "图像转换失败:{Message}", ex.Message); + _logger?.Error(ex, "刷新主界面实时图像失败"); } } - private void OnManualImageLoaded(ManualImageLoadedPayload payload) + private void UpdateFromState() { - if (payload?.Image == null) return; - - ImageSource = payload.Image; - ImageInfo = $"手动加载: {payload.FileName}"; - } - - private void OnPipelinePreviewUpdated(PipelinePreviewUpdatedPayload payload) - { - if (payload?.Image == null) return; - - ImageSource = payload.Image; - ImageInfo = payload.StatusMessage; - } - - /// - /// 16 位灰度数据线性拉伸为 8 位 BitmapSource(委托给 XP.Common 通用转换器) - /// - private static BitmapSource ConvertToBitmapSource(ushort[] data, int width, int height) - { - return XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource(data, width, height); + ImageSource = _mainViewportService.CurrentDisplayImage; + ImageInfo = _mainViewportService.CurrentDisplayInfo; } } } diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml index 3a6da5b..a87b1b3 100644 --- a/XplorePlane/Views/Main/MainWindow.xaml +++ b/XplorePlane/Views/Main/MainWindow.xaml @@ -70,7 +70,6 @@ - - - - - - + + + + + - @@ -197,7 +207,6 @@ - - - - + - - - - - - + telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案" + telerik:ScreenTip.Title="矩阵编排" + Command="{Binding OpenMatrixEditorCommand}" + Size="Large" + SmallImage="/Assets/Icons/matrix.png" + Text="矩阵编排" /> @@ -400,19 +397,8 @@ Size="Large" SmallImage="/Assets/Icons/spiral.png" /> - + @@ -463,7 +449,6 @@ - - - - @@ -501,12 +483,11 @@ - + - - - - - + - - - + + + +