实现加载图像到实时图像区

This commit is contained in:
zhengxuan.zhang
2026-04-20 11:35:40 +08:00
parent e0a7af6226
commit 8c78f805e6
10 changed files with 147 additions and 104 deletions
@@ -1,4 +1,5 @@
using Moq; using Moq;
using Prism.Events;
using System; using System;
using System.IO; using System.IO;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
@@ -32,7 +33,7 @@ namespace XplorePlane.Tests.Pipeline
} }
private PipelineEditorViewModel CreateVm() => 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 ──────────────────────────────────── // ── 6.1 AddOperatorCommand ────────────────────────────────────
@@ -2,6 +2,7 @@ using FsCheck;
using FsCheck.Fluent; using FsCheck.Fluent;
using FsCheck.Xunit; using FsCheck.Xunit;
using Moq; using Moq;
using Prism.Events;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@@ -31,7 +32,7 @@ namespace XplorePlane.Tests.Pipeline
var mockPersistSvc = new Mock<IPipelinePersistenceService>(); var mockPersistSvc = new Mock<IPipelinePersistenceService>();
var mockLogger = new Mock<ILoggerService>(); var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<PipelineEditorViewModel>()).Returns(mockLogger.Object); mockLogger.Setup(l => l.ForModule<PipelineEditorViewModel>()).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);
} }
/// <summary> /// <summary>
@@ -0,0 +1,22 @@
using Prism.Events;
using System.Windows.Media.Imaging;
namespace XplorePlane.Events
{
public sealed class ManualImageLoadedEvent : PubSubEvent<ManualImageLoadedPayload>
{
}
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);
}
}
@@ -1,4 +1,5 @@
using Microsoft.Win32; using Microsoft.Win32;
using Prism.Events;
using Prism.Commands; using Prism.Commands;
using Prism.Mvvm; using Prism.Mvvm;
using System; using System;
@@ -9,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services; using XplorePlane.Services;
@@ -22,6 +24,7 @@ namespace XplorePlane.ViewModels
private readonly IImageProcessingService _imageProcessingService; private readonly IImageProcessingService _imageProcessingService;
private readonly IPipelineExecutionService _executionService; private readonly IPipelineExecutionService _executionService;
private readonly IPipelinePersistenceService _persistenceService; private readonly IPipelinePersistenceService _persistenceService;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private PipelineNodeViewModel _selectedNode; private PipelineNodeViewModel _selectedNode;
@@ -40,11 +43,13 @@ namespace XplorePlane.ViewModels
IImageProcessingService imageProcessingService, IImageProcessingService imageProcessingService,
IPipelineExecutionService executionService, IPipelineExecutionService executionService,
IPipelinePersistenceService persistenceService, IPipelinePersistenceService persistenceService,
IEventAggregator eventAggregator,
ILoggerService logger) ILoggerService logger)
{ {
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService)); _imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
_executionService = executionService ?? throw new ArgumentNullException(nameof(executionService)); _executionService = executionService ?? throw new ArgumentNullException(nameof(executionService));
_persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService)); _persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = logger?.ForModule<PipelineEditorViewModel>() ?? throw new ArgumentNullException(nameof(logger)); _logger = logger?.ForModule<PipelineEditorViewModel>() ?? throw new ArgumentNullException(nameof(logger));
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>(); PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
@@ -64,6 +69,9 @@ namespace XplorePlane.ViewModels
OpenToolboxCommand = new DelegateCommand(OpenToolbox); OpenToolboxCommand = new DelegateCommand(OpenToolbox);
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp); MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown); MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
_eventAggregator.GetEvent<ManualImageLoadedEvent>()
.Subscribe(OnManualImageLoaded);
} }
// ── State Properties ────────────────────────────────────────── // ── State Properties ──────────────────────────────────────────
@@ -306,6 +314,7 @@ namespace XplorePlane.ViewModels
PreviewImage = result; PreviewImage = result;
StatusMessage = "流水线执行完成"; StatusMessage = "流水线执行完成";
PublishPipelinePreviewUpdated(result, StatusMessage);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -362,6 +371,45 @@ namespace XplorePlane.ViewModels
SourceImage = bitmap; SourceImage = bitmap;
PreviewImage = bitmap; PreviewImage = bitmap;
StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}"; 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<ManualImageLoadedEvent>()
.Publish(new ManualImageLoadedPayload(bitmap, filePath));
}
private void PublishPipelinePreviewUpdated(BitmapSource bitmap, string statusMessage)
{
if (bitmap == null) return;
_eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
.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() private void CancelExecution()
+39 -1
View File
@@ -1,11 +1,15 @@
using Prism.Commands; using Prism.Commands;
using Prism.Events;
using Prism.Ioc; using Prism.Ioc;
using Prism.Mvvm; using Prism.Mvvm;
using Microsoft.Win32;
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Configuration; using System.Configuration;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Media.Imaging;
using XplorePlane.Events;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XP.Common.PdfViewer.Interfaces; using XP.Common.PdfViewer.Interfaces;
using XP.Hardware.MotionControl.Abstractions; using XP.Hardware.MotionControl.Abstractions;
@@ -16,6 +20,7 @@ namespace XplorePlane.ViewModels
{ {
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private readonly IContainerProvider _containerProvider; private readonly IContainerProvider _containerProvider;
private readonly IEventAggregator _eventAggregator;
private string _licenseInfo = "当前时间"; private string _licenseInfo = "当前时间";
public string LicenseInfo public string LicenseInfo
@@ -36,6 +41,7 @@ namespace XplorePlane.ViewModels
// 窗口打开命令 // 窗口打开命令
public DelegateCommand OpenImageProcessingCommand { get; } public DelegateCommand OpenImageProcessingCommand { get; }
public DelegateCommand LoadImageCommand { get; }
public DelegateCommand OpenPipelineEditorCommand { get; } public DelegateCommand OpenPipelineEditorCommand { get; }
public DelegateCommand OpenCncEditorCommand { get; } public DelegateCommand OpenCncEditorCommand { get; }
public DelegateCommand OpenMatrixEditorCommand { get; } public DelegateCommand OpenMatrixEditorCommand { get; }
@@ -64,10 +70,11 @@ namespace XplorePlane.ViewModels
private Window _toolboxWindow; private Window _toolboxWindow;
private Window _raySourceConfigWindow; private Window _raySourceConfigWindow;
public MainViewModel(ILoggerService logger, IContainerProvider containerProvider) public MainViewModel(ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator)
{ {
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger)); _logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
_containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider)); _containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
NavigationTree = new ObservableCollection<object>(); NavigationTree = new ObservableCollection<object>();
@@ -81,6 +88,7 @@ namespace XplorePlane.ViewModels
// 窗口打开命令 // 窗口打开命令
OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理")); OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理"));
LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器")); OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器"));
OpenCncEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.CncEditorWindow(), "CNC 编辑器")); OpenCncEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.CncEditorWindow(), "CNC 编辑器"));
OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排")); OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排"));
@@ -260,6 +268,36 @@ namespace XplorePlane.ViewModels
() => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "射线源配置"); () => 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<ManualImageLoadedEvent>()
.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() private void ExecuteWarmUp()
{ {
var messageBoxResult = MessageBox.Show("确认执行射线源暖机操作?", "暖机", var messageBoxResult = MessageBox.Show("确认执行射线源暖机操作?", "暖机",
@@ -8,6 +8,7 @@ using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions; using XP.Hardware.Detector.Abstractions;
using XP.Hardware.Detector.Abstractions.Events; using XP.Hardware.Detector.Abstractions.Events;
using XplorePlane.Events;
namespace XplorePlane.ViewModels namespace XplorePlane.ViewModels
{ {
@@ -39,6 +40,10 @@ namespace XplorePlane.ViewModels
eventAggregator.GetEvent<ImageCapturedEvent>() eventAggregator.GetEvent<ImageCapturedEvent>()
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread); .Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
eventAggregator.GetEvent<ManualImageLoadedEvent>()
.Subscribe(OnManualImageLoaded, ThreadOption.UIThread);
eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
.Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread);
} }
private void OnImageCaptured(ImageCapturedEventArgs args) 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;
}
/// <summary> /// <summary>
/// 16 位灰度数据线性拉伸为 8 位 BitmapSource(委托给 XP.Common 通用转换器) /// 16 位灰度数据线性拉伸为 8 位 BitmapSource(委托给 XP.Common 通用转换器)
/// </summary> /// </summary>
@@ -89,12 +89,14 @@
Content="加载" Content="加载"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="加载流水线" /> ToolTip="加载流水线" />
<!--
<Button <Button
Width="64" Width="64"
Command="{Binding LoadImageCommand}" Command="{Binding LoadImageCommand}"
Content="加载图像" Content="加载图像"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="加载输入图像" /> ToolTip="加载输入图像" />
-->
<Button <Button
Command="{Binding ExecutePipelineCommand}" Command="{Binding ExecutePipelineCommand}"
Content="▶" Content="▶"
@@ -23,7 +23,7 @@ namespace XplorePlane.Views
private void OnLoaded(object sender, RoutedEventArgs e) private void OnLoaded(object sender, RoutedEventArgs e)
{ {
if (DataContext == null) if (DataContext is not PipelineEditorViewModel)
{ {
try try
{ {
+7 -2
View File
@@ -136,7 +136,11 @@
</StackPanel> </StackPanel>
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="快捷工具"> <telerik:RadRibbonGroup Header="快捷工具">
<telerik:RadRibbonButton
Command="{Binding LoadImageCommand}"
Size="Large"
SmallImage="/Assets/Icons/open.png"
Text="加载图像" />
<!-- 快捷工具: 上下两列,带文字 --> <!-- 快捷工具: 上下两列,带文字 -->
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
@@ -384,7 +388,7 @@
Size="Large" Size="Large"
SmallImage="/Assets/Icons/spiral.png" /> SmallImage="/Assets/Icons/spiral.png" />
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<!--
<telerik:RadRibbonGroup Header="图像处理"> <telerik:RadRibbonGroup Header="图像处理">
<telerik:RadRibbonGroup.Variants> <telerik:RadRibbonGroup.Variants>
<telerik:GroupVariant Priority="0" Variant="Large" /> <telerik:GroupVariant Priority="0" Variant="Large" />
@@ -395,6 +399,7 @@
SmallImage="/Assets/Icons/workflow.png" SmallImage="/Assets/Icons/workflow.png"
Text="流水线编辑器" /> Text="流水线编辑器" />
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
-->
</telerik:RadRibbonTab> </telerik:RadRibbonTab>
<telerik:RadRibbonTab Header="关于"> <telerik:RadRibbonTab Header="关于">
<telerik:RadRibbonGroup Header="关于"> <telerik:RadRibbonGroup Header="关于">
-95
View File
@@ -1,95 +0,0 @@
---------------------------------------------------------------
__ __ _ _____ _
\ \ / / | | | __ \| |
\ V / _ __ | | ___ _ __ ___| |__) | | __ _ _ __ ___
> < | '_ \| |/ _ \| '__/ _ \ ___/| |/ _` | '_ \ / _ \
/ . \| |_) | | (_) | | | __/ | | | (_| | | | | __/
/_/ \_\ .__/|_|\___/|_| \___|_| |_|\__,_|_| |_|\___|
| |
|_|
---------------------------------------------------------------
2026.3.14
----------------------
1、主页面的布局与拆分 √
2、硬件层射线源的集成 √
3、图像层集成,包括复刻一个示例界面,优化界面布局及算子中文 √
4、浮动图像处理工具箱调研 √
5、修复图像工具箱拖拽事件,流水线列表没有生成对应的控件 √
2026.3.16
----------------------
1、优化图像处理窗体的页面布局,简洁清晰 √
2、新增打开图像工具箱(修复DataContext问题) √
3、对主界面B方案进行优化 √
2026.3.17
----------------------
1、对界面设计进行优化 ,增加了扫描模式,移除了探测器设置,增加底部工具栏 √
2026.3.18
----------------------
1、全局数据结构的考虑与设计(多个窗体可以调用公共的数据,如射线源状态,探测器状态,运动位置,图像等) √
2、将计划窗体默认隐藏,只有CNC状态下展开 √
2026.3.20
----------------------
1、软件主界面设计讨论,暂定初稿,给出效果图设计 √
2、日志该用XP.Common库和多语言的学习 √
2026.3.26
----------------------
1、各窗体间数据流的传递,全局数据结构的设计(包括一个基本的说明文档)√
2、将telerik 升级到 2024.1.408.310;调整界面和主题;引入 硬件层依赖 √
3、图像算子流程文件,保存文件后缀 .imw, image process workflow 缩写 √
4、CNC保存文件后缀为.xp, 表示 XplorePlane CNC file 的缩写 √
5、硬件层射线源控件的初步集成(采用库层面的自定义控件方式) √
PrismBootstrapper 的执行顺序是:RegisterTypes() → ConfigureModuleCatalog() → InitializeModules() → CreateShell()
2026.4.16
----------------------
CNC及矩阵功能的设计与评审,包含以下功能: √
1、CNC功能设计与实现,包含以下功能:
a. CNC状态的定义和管理
b. CNC界面设计与实现
c. CNC相关数据的传递和处理
2、CNC相关的编排工具,如插入节点,插入位置,图像模块,等
2026.4.20
----------------------
1、图像算子工具箱的图标 √
2、最新的图像算子集成到图像工具箱 √
3、修复流程图编辑器界面及初步的功能 √
4、主页面加载图像的功能
5、
TO-DO
----------------------