diff --git a/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs b/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs index 33b250f..9716bde 100644 --- a/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs +++ b/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs @@ -1,4 +1,5 @@ using Moq; +using Prism.Events; using System; using System.IO; using System.Windows.Media.Imaging; @@ -32,7 +33,7 @@ namespace XplorePlane.Tests.Pipeline } private PipelineEditorViewModel CreateVm() => - new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, _mockLogger.Object); + new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, new EventAggregator(), _mockLogger.Object); // ── 6.1 AddOperatorCommand ──────────────────────────────────── diff --git a/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs b/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs index 9bb7b1a..060b14f 100644 --- a/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs +++ b/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs @@ -2,6 +2,7 @@ using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; using Moq; +using Prism.Events; using System; using System.Collections.Generic; using System.IO; @@ -31,7 +32,7 @@ namespace XplorePlane.Tests.Pipeline var mockPersistSvc = new Mock(); var mockLogger = new Mock(); mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object); - return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object, mockLogger.Object); + return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object, new EventAggregator(), mockLogger.Object); } /// @@ -277,4 +278,4 @@ namespace XplorePlane.Tests.Pipeline }); } } -} \ No newline at end of file +} diff --git a/XplorePlane/Events/ManualImageLoadedEvent.cs b/XplorePlane/Events/ManualImageLoadedEvent.cs new file mode 100644 index 0000000..702e708 --- /dev/null +++ b/XplorePlane/Events/ManualImageLoadedEvent.cs @@ -0,0 +1,22 @@ +using Prism.Events; +using System.Windows.Media.Imaging; + +namespace XplorePlane.Events +{ + public sealed class ManualImageLoadedEvent : PubSubEvent + { + } + + public sealed class ManualImageLoadedPayload + { + public ManualImageLoadedPayload(BitmapSource image, string filePath) + { + Image = image; + FilePath = filePath; + } + + public BitmapSource Image { get; } + public string FilePath { get; } + public string FileName => System.IO.Path.GetFileName(FilePath); + } +} diff --git a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs index b20b57c..6ba4b19 100644 --- a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs @@ -1,4 +1,5 @@ using Microsoft.Win32; +using Prism.Events; using Prism.Commands; using Prism.Mvvm; using System; @@ -9,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Media.Imaging; using XP.Common.Logging.Interfaces; +using XplorePlane.Events; using XplorePlane.Models; using XplorePlane.Services; @@ -22,6 +24,7 @@ namespace XplorePlane.ViewModels private readonly IImageProcessingService _imageProcessingService; private readonly IPipelineExecutionService _executionService; private readonly IPipelinePersistenceService _persistenceService; + private readonly IEventAggregator _eventAggregator; private readonly ILoggerService _logger; private PipelineNodeViewModel _selectedNode; @@ -40,11 +43,13 @@ namespace XplorePlane.ViewModels IImageProcessingService imageProcessingService, IPipelineExecutionService executionService, IPipelinePersistenceService persistenceService, + IEventAggregator eventAggregator, ILoggerService logger) { _imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService)); _executionService = executionService ?? throw new ArgumentNullException(nameof(executionService)); _persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); PipelineNodes = new ObservableCollection(); @@ -64,6 +69,9 @@ namespace XplorePlane.ViewModels OpenToolboxCommand = new DelegateCommand(OpenToolbox); MoveNodeUpCommand = new DelegateCommand(MoveNodeUp); MoveNodeDownCommand = new DelegateCommand(MoveNodeDown); + + _eventAggregator.GetEvent() + .Subscribe(OnManualImageLoaded); } // ── State Properties ────────────────────────────────────────── @@ -306,6 +314,7 @@ namespace XplorePlane.ViewModels PreviewImage = result; StatusMessage = "流水线执行完成"; + PublishPipelinePreviewUpdated(result, StatusMessage); } catch (OperationCanceledException) { @@ -362,6 +371,45 @@ namespace XplorePlane.ViewModels SourceImage = bitmap; PreviewImage = bitmap; StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}"; + PublishManualImageLoaded(bitmap, filePath); + } + + internal void LoadImageFromBitmap(BitmapSource bitmap, string filePath, bool runPipeline = true) + { + if (bitmap == null) + throw new ArgumentNullException(nameof(bitmap)); + + SourceImage = bitmap; + PreviewImage = bitmap; + StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}"; + PublishManualImageLoaded(bitmap, filePath); + + if (runPipeline) + TriggerDebouncedExecution(); + } + + private void PublishManualImageLoaded(BitmapSource bitmap, string filePath) + { + _eventAggregator.GetEvent() + .Publish(new ManualImageLoadedPayload(bitmap, filePath)); + } + + private void PublishPipelinePreviewUpdated(BitmapSource bitmap, string statusMessage) + { + if (bitmap == null) return; + + _eventAggregator.GetEvent() + .Publish(new PipelinePreviewUpdatedPayload(bitmap, statusMessage)); + } + + private void OnManualImageLoaded(ManualImageLoadedPayload payload) + { + if (payload?.Image == null) return; + if (ReferenceEquals(SourceImage, payload.Image)) return; + + SourceImage = payload.Image; + PreviewImage = payload.Image; + StatusMessage = $"已加载图像:{payload.FileName}"; } private void CancelExecution() diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 8854c19..448552b 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -1,11 +1,15 @@ 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 XP.Common.Logging.Interfaces; using XP.Common.PdfViewer.Interfaces; using XP.Hardware.MotionControl.Abstractions; @@ -16,6 +20,7 @@ namespace XplorePlane.ViewModels { private readonly ILoggerService _logger; private readonly IContainerProvider _containerProvider; + private readonly IEventAggregator _eventAggregator; private string _licenseInfo = "当前时间"; public string LicenseInfo @@ -36,6 +41,7 @@ namespace XplorePlane.ViewModels // 窗口打开命令 public DelegateCommand OpenImageProcessingCommand { get; } + public DelegateCommand LoadImageCommand { get; } public DelegateCommand OpenPipelineEditorCommand { get; } public DelegateCommand OpenCncEditorCommand { get; } public DelegateCommand OpenMatrixEditorCommand { get; } @@ -64,10 +70,11 @@ namespace XplorePlane.ViewModels private Window _toolboxWindow; private Window _raySourceConfigWindow; - public MainViewModel(ILoggerService logger, IContainerProvider containerProvider) + public MainViewModel(ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator) { _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); _containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); NavigationTree = new ObservableCollection(); @@ -81,6 +88,7 @@ namespace XplorePlane.ViewModels // 窗口打开命令 OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理")); + LoadImageCommand = new DelegateCommand(ExecuteLoadImage); OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器")); OpenCncEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.CncEditorWindow(), "CNC 编辑器")); OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排")); @@ -260,6 +268,36 @@ namespace XplorePlane.ViewModels () => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "射线源配置"); } + private void ExecuteLoadImage() + { + var dialog = new OpenFileDialog + { + Title = "加载图像", + Filter = "图像文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff|所有文件|*.*" + }; + + if (dialog.ShowDialog() != true) + return; + + try + { + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri(dialog.FileName, UriKind.Absolute); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + bitmap.Freeze(); + + _eventAggregator.GetEvent() + .Publish(new ManualImageLoadedPayload(bitmap, dialog.FileName)); + } + catch (Exception ex) + { + _logger.Error(ex, "加载图像失败:{Path}", dialog.FileName); + MessageBox.Show($"加载图像失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + private void ExecuteWarmUp() { var messageBoxResult = MessageBox.Show("确认执行射线源暖机操作?", "暖机", diff --git a/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs b/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs index 9d31dce..599b638 100644 --- a/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs +++ b/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs @@ -8,6 +8,7 @@ using System.Windows.Media.Imaging; using XP.Common.Logging.Interfaces; using XP.Hardware.Detector.Abstractions; using XP.Hardware.Detector.Abstractions.Events; +using XplorePlane.Events; namespace XplorePlane.ViewModels { @@ -39,6 +40,10 @@ namespace XplorePlane.ViewModels eventAggregator.GetEvent() .Subscribe(OnImageCaptured, ThreadOption.BackgroundThread); + eventAggregator.GetEvent() + .Subscribe(OnManualImageLoaded, ThreadOption.UIThread); + eventAggregator.GetEvent() + .Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread); } private void OnImageCaptured(ImageCapturedEventArgs args) @@ -75,6 +80,22 @@ namespace XplorePlane.ViewModels } } + private void OnManualImageLoaded(ManualImageLoadedPayload payload) + { + 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 通用转换器) /// diff --git a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml index 837f1e0..f9c427c 100644 --- a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml +++ b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml @@ -89,12 +89,14 @@ Content="加载" Style="{StaticResource ToolbarBtn}" ToolTip="加载流水线" /> +