主界面实时图像与探测器双队列

This commit is contained in:
zhengxuan.zhang
2026-04-24 13:34:10 +08:00
parent f64a0f7b31
commit 5d2a74d64b
12 changed files with 713 additions and 289 deletions
+6 -1
View File
@@ -88,6 +88,11 @@
<add key="Detector:InitializationTimeout" value="30000" /> <add key="Detector:InitializationTimeout" value="30000" />
<add key="Detector:AcquisitionTimeout" value="10000" /> <add key="Detector:AcquisitionTimeout" value="10000" />
<add key="Detector:CorrectionTimeout" value="60000" /> <add key="Detector:CorrectionTimeout" value="60000" />
<!-- 主界面实时图像与探测器帧流水线 -->
<add key="MainViewport:RealtimeEnabledDefault" value="true" />
<add key="DetectorPipeline:AcquireQueueCapacity" value="16" />
<add key="DetectorPipeline:ProcessQueueCapacity" value="8" />
<add key="DetectorPipeline:ProcessEveryNFrames" value="1" />
<!-- Dump 配置 | Dump Configuration --> <!-- Dump 配置 | Dump Configuration -->
<add key="Dump:StoragePath" value="D:\XplorePlane\Dump" /> <add key="Dump:StoragePath" value="D:\XplorePlane\Dump" />
@@ -164,4 +169,4 @@
<!-- 允许捕获非托管异常(如 AccessViolationException)以便生成 Dump | Allow catching unmanaged exceptions (e.g. AccessViolationException) for dump generation --> <!-- 允许捕获非托管异常(如 AccessViolationException)以便生成 Dump | Allow catching unmanaged exceptions (e.g. AccessViolationException) for dump generation -->
<legacyCorruptedStateExceptionsPolicy enabled="true" /> <legacyCorruptedStateExceptionsPolicy enabled="true" />
</runtime> </runtime>
</configuration> </configuration>
+71 -49
View File
@@ -13,10 +13,10 @@ using XP.Camera;
using XP.Common.Configs; using XP.Common.Configs;
using XP.Common.Database.Implementations; using XP.Common.Database.Implementations;
using XP.Common.Database.Interfaces; using XP.Common.Database.Interfaces;
using XP.Common.GeneralForm.Views;
using XP.Common.Dump.Configs; using XP.Common.Dump.Configs;
using XP.Common.Dump.Implementations; using XP.Common.Dump.Implementations;
using XP.Common.Dump.Interfaces; using XP.Common.Dump.Interfaces;
using XP.Common.GeneralForm.Views;
using XP.Common.Helpers; using XP.Common.Helpers;
using XP.Common.Localization.Configs; using XP.Common.Localization.Configs;
using XP.Common.Localization.Extensions; using XP.Common.Localization.Extensions;
@@ -35,9 +35,10 @@ using XplorePlane.Services;
using XplorePlane.Services.AppState; using XplorePlane.Services.AppState;
using XplorePlane.Services.Camera; using XplorePlane.Services.Camera;
using XplorePlane.Services.Cnc; using XplorePlane.Services.Cnc;
using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.MainViewport;
using XplorePlane.Services.Matrix; using XplorePlane.Services.Matrix;
using XplorePlane.Services.Measurement; using XplorePlane.Services.Measurement;
using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.Recipe; using XplorePlane.Services.Recipe;
using XplorePlane.ViewModels; using XplorePlane.ViewModels;
using XplorePlane.ViewModels.Cnc; using XplorePlane.ViewModels.Cnc;
@@ -51,10 +52,10 @@ namespace XplorePlane
/// </summary> /// </summary>
public partial class App : Application public partial class App : Application
{ {
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)
{ {
// 设置 Telerik Windows11 主题,缩小 Ribbon 整体尺寸 // 设置 Telerik Windows11 主题,缩小 Ribbon 整体尺寸
StyleManager.ApplicationTheme = new Windows11Theme(); StyleManager.ApplicationTheme = new Windows11Theme();
// 强制使用中文 UI,确保 ImageProcessing 库显示中文 // 强制使用中文 UI,确保 ImageProcessing 库显示中文
var zhCN = new CultureInfo("zh-CN"); var zhCN = new CultureInfo("zh-CN");
@@ -66,8 +67,8 @@ namespace XplorePlane
// 配置 Serilog 日志系统 // 配置 Serilog 日志系统
ConfigureLogging(); ConfigureLogging();
// 捕获未处理的异常 // 捕获未处理的异常
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
DispatcherUnhandledException += OnDispatcherUnhandledException; DispatcherUnhandledException += OnDispatcherUnhandledException;
try try
@@ -102,12 +103,12 @@ namespace XplorePlane
private void ConfigureLogging() private void ConfigureLogging()
{ {
// 加载Serilog配置 | Load Serilog configuration // 加载 Serilog 配置 | Load Serilog configuration
SerilogConfig serilogConfig = ConfigLoader.LoadSerilogConfig(); SerilogConfig serilogConfig = ConfigLoader.LoadSerilogConfig();
// 初始化Serilog(全局唯一)| Initialize Serilog (global singleton) // 初始化 Serilog(全局唯一)| Initialize Serilog (global singleton)
SerilogInitializer.Initialize(serilogConfig); SerilogInitializer.Initialize(serilogConfig);
// 记录应用启动日志 | Log application startup // 记录应用启动日志 | Log application startup
Log.Information("========================================"); Log.Information("========================================");
Log.Information("XplorePlane 应用程序启动"); Log.Information("XplorePlane 应用程序启动");
Log.Information("========================================"); Log.Information("========================================");
@@ -183,23 +184,39 @@ namespace XplorePlane
Log.Error(ex, "导航相机服务资源释放失败"); Log.Error(ex, "导航相机服务资源释放失败");
} }
// 释放SQLite数据库资源 | Release SQLite database resources // 释放主界面探测器帧流水线资源
try try
{ {
var bootstrapper = AppBootstrapper.Instance; var bootstrapper = AppBootstrapper.Instance;
if (bootstrapper != null) if (bootstrapper != null)
{ {
var dbContext = bootstrapper.Container.Resolve<IDbContext>(); // 从Prism容器获取IDbContext实例(单例)| Get IDbContext instance from Prism container (singleton) var detectorFramePipelineService = bootstrapper.Container.Resolve<IDetectorFramePipelineService>();
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<IDbContext>(); // 从 Prism 容器获取 IDbContext 实例(单例)| Get IDbContext instance from Prism container (singleton)
dbContext?.Dispose(); dbContext?.Dispose();
Log.Information("数据库资源已成功释放 | Database resources released successfully"); Log.Information("数据库资源已成功释放 | Database resources released successfully");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "数据库资源释放失败,忽略该错误继续退出 | Database resource release failed, ignoring error and continuing exit"); Log.Error(ex, "数据库资源释放失败,忽略该错误继续退出 | Database resource release failed, ignoring error and continuing exit");
} }
Log.CloseAndFlush(); Log.CloseAndFlush();
base.OnExit(e); base.OnExit(e);
} }
@@ -208,7 +225,7 @@ namespace XplorePlane
/// </summary> /// </summary>
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{ {
var exception = e.ExceptionObject as Exception; var exception = e.ExceptionObject as Exception;
Log.Fatal(exception, "应用程序发生未处理的异常"); Log.Fatal(exception, "应用程序发生未处理的异常");
MessageBox.Show( MessageBox.Show(
@@ -242,15 +259,15 @@ namespace XplorePlane
public new IContainerProvider Container => base.Container; public new IContainerProvider Container => base.Container;
private bool _modulesInitialized = false;
private string _cameraError;
public AppBootstrapper() public AppBootstrapper()
{ {
Instance = this; Instance = this;
} }
private bool _modulesInitialized = false;
private string? _cameraError;
protected override Window CreateShell() protected override Window CreateShell()
{ {
// 提前初始化模块,确保硬件服务在 MainWindow XAML 解析前已注册 // 提前初始化模块,确保硬件服务在 MainWindow XAML 解析前已注册
@@ -267,6 +284,16 @@ namespace XplorePlane
{ {
TryConnectCamera(); TryConnectCamera();
// 初始化主界面探测器帧流水线,开始接收探测器图像事件
try
{
_ = Container.Resolve<IDetectorFramePipelineService>();
}
catch (Exception ex)
{
Log.Error(ex, "初始化主界面探测器帧流水线失败");
}
// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误 // 通知 ViewModel 相机状态已确定,启动实时预览或显示错误
try try
{ {
@@ -329,11 +356,11 @@ namespace XplorePlane
// 注册 Serilog 的 ILogger 实例 // 注册 Serilog 的 ILogger 实例
containerRegistry.RegisterInstance<ILogger>(Log.Logger); containerRegistry.RegisterInstance<ILogger>(Log.Logger);
// 注册 XP.Common.ILoggerService 适配器 // 注册 XP.Common.ILoggerService 适配器
containerRegistry.RegisterSingleton<ILoggerService, SerilogLoggerService>(); containerRegistry.RegisterSingleton<ILoggerService, SerilogLoggerService>();
// 注册视图和视图模型 // 注册视图和视图模型
containerRegistry.RegisterForNavigation<MainWindow>(); containerRegistry.RegisterForNavigation<MainWindow>();
containerRegistry.Register<MainViewModel>(); containerRegistry.Register<MainViewModel>();
containerRegistry.RegisterSingleton<NavigationPropertyPanelViewModel>(); containerRegistry.RegisterSingleton<NavigationPropertyPanelViewModel>();
@@ -364,16 +391,7 @@ namespace XplorePlane
containerRegistry.RegisterInstance(sqliteConfig); containerRegistry.RegisterInstance(sqliteConfig);
containerRegistry.RegisterSingleton<IDbContext, SqliteContext>(); containerRegistry.RegisterSingleton<IDbContext, SqliteContext>();
// 注册硬件库的 ViewModel(供 ViewModelLocator 自动装配 // 注册通用模块的服务(本地化、Dump
//containerRegistry.Register<XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel>();
// 手动注册射线源模块的所有服务(确保 DryIoc 容器中可用,避免模块加载顺序问题)
//var raySourceConfig = XP.Hardware.RaySource.Config.ConfigLoader.LoadConfig();
//containerRegistry.RegisterInstance(raySourceConfig);
//containerRegistry.RegisterSingleton<XP.Hardware.RaySource.Abstractions.IRaySourceFactory, XP.Hardware.RaySource.Factories.RaySourceFactory>();
//containerRegistry.RegisterSingleton<IRaySourceService, XP.Hardware.RaySource.Services.RaySourceService>();
//containerRegistry.RegisterSingleton<XP.Hardware.RaySource.Services.IFilamentLifetimeService, XP.Hardware.RaySource.Services.FilamentLifetimeService>();
// 手动注册通用模块的服务(本地化、Dump)
containerRegistry.RegisterSingleton<ILocalizationConfig, LocalizationConfig>(); containerRegistry.RegisterSingleton<ILocalizationConfig, LocalizationConfig>();
containerRegistry.RegisterSingleton<ILocalizationService, ResxLocalizationService>(); containerRegistry.RegisterSingleton<ILocalizationService, ResxLocalizationService>();
containerRegistry.RegisterSingleton<DumpConfig>(() => XP.Common.Helpers.ConfigLoader.LoadDumpConfig()); containerRegistry.RegisterSingleton<DumpConfig>(() => XP.Common.Helpers.ConfigLoader.LoadDumpConfig());
@@ -385,6 +403,10 @@ namespace XplorePlane
containerRegistry.RegisterSingleton<IMeasurementDataService, MeasurementDataService>(); containerRegistry.RegisterSingleton<IMeasurementDataService, MeasurementDataService>();
containerRegistry.RegisterSingleton<IInspectionResultStore, InspectionResultStore>(); containerRegistry.RegisterSingleton<IInspectionResultStore, InspectionResultStore>();
// ── 主界面实时图像 / 探测器双队列服务(单例)──
containerRegistry.RegisterSingleton<IMainViewportService, MainViewportService>();
containerRegistry.RegisterSingleton<IDetectorFramePipelineService, DetectorFramePipelineService>();
// ── CNC / 矩阵 ViewModel(瞬态)── // ── CNC / 矩阵 ViewModel(瞬态)──
containerRegistry.Register<CncEditorViewModel>(); containerRegistry.Register<CncEditorViewModel>();
containerRegistry.Register<MatrixEditorViewModel>(); containerRegistry.Register<MatrixEditorViewModel>();
@@ -405,13 +427,13 @@ namespace XplorePlane
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{ {
// 所有模块服务已在 RegisterTypes 中手动注册 // 所有模块服务已在 RegisterTypes 中手动注册
// CommonModule: ILocalizationService, IDumpService // CommonModule: ILocalizationService, IDumpService
// RaySourceModule: IRaySourceService, IRaySourceFactory, IFilamentLifetimeService // RaySourceModule: IRaySourceService, IRaySourceFactory, IFilamentLifetimeService
// 注册通用模块(必须最先加载)| Register common module (must be loaded first)
moduleCatalog.AddModule<CommonModule>();
// 注册通用模块(必须最先加载)| Register common module (must be loaded first)
moduleCatalog.AddModule<CommonModule>();
// 注册其他模块 | Register other modules // 注册其他模块 | Register other modules
moduleCatalog.AddModule<PLCModule>(); moduleCatalog.AddModule<PLCModule>();
moduleCatalog.AddModule<DetectorModule>(); moduleCatalog.AddModule<DetectorModule>();
@@ -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; }
}
}
@@ -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<DetectorFrame> _acquireQueue = new();
private readonly ConcurrentQueue<DetectorFrame> _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<DetectorFramePipelineService>() ?? 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<ImageCapturedEvent>()
.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<DetectorFrame> 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<DetectorFrame> 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();
}
}
}
@@ -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<DetectorFrame> ProcessFrameDequeued;
}
}
@@ -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);
}
}
@@ -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<MainViewportService>() ?? 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;
}
}
}
@@ -0,0 +1,8 @@
namespace XplorePlane.Services.MainViewport
{
public enum MainViewportSourceMode
{
LiveDetector = 0,
ManualImage = 1
}
}
+118 -115
View File
@@ -1,15 +1,15 @@
using Prism.Commands; using Microsoft.Win32;
using Prism.Commands;
using Prism.Events; 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 System.Windows.Media.Imaging;
using XplorePlane.Events; using XplorePlane.Services.MainViewport;
using XplorePlane.ViewModels.Cnc; using XplorePlane.ViewModels.Cnc;
using XplorePlane.Views; using XplorePlane.Views;
using XplorePlane.Views.Cnc; using XplorePlane.Views.Cnc;
@@ -22,85 +22,13 @@ namespace XplorePlane.ViewModels
public class MainViewModel : BindableBase public class MainViewModel : BindableBase
{ {
private const double CncEditorHostWidth = 502d; private const double CncEditorHostWidth = 502d;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private readonly IContainerProvider _containerProvider; private readonly IContainerProvider _containerProvider;
private readonly IEventAggregator _eventAggregator; private readonly IMainViewportService _mainViewportService;
private readonly CncEditorViewModel _cncEditorViewModel; private readonly CncEditorViewModel _cncEditorViewModel;
private readonly CncPageView _cncPageView; private readonly CncPageView _cncPageView;
private string _licenseInfo = "当前时间";
public string LicenseInfo
{
get => _licenseInfo;
set => SetProperty(ref _licenseInfo, value);
}
public ObservableCollection<object> 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; }
/// <summary>右侧图像区域内容 | Right-side image panel content</summary>
public object ImagePanelContent
{
get => _imagePanelContent;
set => SetProperty(ref _imagePanelContent, value);
}
/// <summary>右侧图像区域宽度 | Right-side image panel width</summary>
public GridLength ImagePanelWidth
{
get => _imagePanelWidth;
set => SetProperty(ref _imagePanelWidth, value);
}
/// <summary>主视图区宽度 | Main viewport width</summary>
public GridLength ViewportPanelWidth
{
get => _viewportPanelWidth;
set => SetProperty(ref _viewportPanelWidth, value);
}
// 窗口引用(单例窗口防止重复打开)
private Window _motionDebugWindow; private Window _motionDebugWindow;
private Window _detectorConfigWindow; private Window _detectorConfigWindow;
private Window _plcAddrConfigWindow; private Window _plcAddrConfigWindow;
@@ -108,21 +36,28 @@ namespace XplorePlane.ViewModels
private Window _toolboxWindow; private Window _toolboxWindow;
private Window _raySourceConfigWindow; private Window _raySourceConfigWindow;
private object _imagePanelContent; private object _imagePanelContent;
private GridLength _viewportPanelWidth = new GridLength(1, GridUnitType.Star); private GridLength _viewportPanelWidth = new(1, GridUnitType.Star);
private GridLength _imagePanelWidth = new GridLength(320); private GridLength _imagePanelWidth = new(320);
private bool _isCncEditorMode; 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<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)); ArgumentNullException.ThrowIfNull(eventAggregator);
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
_cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>(); _cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>();
_cncPageView = new CncPageView { DataContext = _cncEditorViewModel }; _cncPageView = new CncPageView { DataContext = _cncEditorViewModel };
_mainViewportService.StateChanged += OnMainViewportStateChanged;
NavigationTree = new ObservableCollection<object>(); NavigationTree = new ObservableCollection<object>();
// 导航命令
NavigateHomeCommand = new DelegateCommand(OnNavigateHome); NavigateHomeCommand = new DelegateCommand(OnNavigateHome);
NavigateInspectCommand = new DelegateCommand(OnNavigateInspect); NavigateInspectCommand = new DelegateCommand(OnNavigateInspect);
OpenFileCommand = new DelegateCommand(OnOpenFile); OpenFileCommand = new DelegateCommand(OnOpenFile);
@@ -130,7 +65,6 @@ namespace XplorePlane.ViewModels
ClearCommand = new DelegateCommand(OnClear); ClearCommand = new DelegateCommand(OnClear);
EditPropertiesCommand = new DelegateCommand(OnEditProperties); EditPropertiesCommand = new DelegateCommand(OnEditProperties);
// 窗口打开命令
OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理")); OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理"));
LoadImageCommand = new DelegateCommand(ExecuteLoadImage); LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器")); OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器"));
@@ -152,7 +86,6 @@ namespace XplorePlane.ViewModels
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute())); InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute()));
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute())); InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute()));
// 硬件命令
AxisResetCommand = new DelegateCommand(ExecuteAxisReset); AxisResetCommand = new DelegateCommand(ExecuteAxisReset);
OpenDetectorConfigCommand = new DelegateCommand(ExecuteOpenDetectorConfig); OpenDetectorConfigCommand = new DelegateCommand(ExecuteOpenDetectorConfig);
OpenMotionDebugCommand = new DelegateCommand(ExecuteOpenMotionDebug); OpenMotionDebugCommand = new DelegateCommand(ExecuteOpenMotionDebug);
@@ -160,9 +93,9 @@ namespace XplorePlane.ViewModels
OpenRaySourceConfigCommand = new DelegateCommand(ExecuteOpenRaySourceConfig); OpenRaySourceConfigCommand = new DelegateCommand(ExecuteOpenRaySourceConfig);
WarmUpCommand = new DelegateCommand(ExecuteWarmUp); WarmUpCommand = new DelegateCommand(ExecuteWarmUp);
// 设置命令
OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher); OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher);
OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer); OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer);
UseLiveDetectorSourceCommand = new DelegateCommand(ExecuteUseLiveDetectorSource);
ImagePanelContent = new PipelineEditorView(); ImagePanelContent = new PipelineEditorView();
ViewportPanelWidth = new GridLength(1, GridUnitType.Star); ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
@@ -171,11 +104,86 @@ namespace XplorePlane.ViewModels
_logger.Info("MainViewModel 已初始化"); _logger.Info("MainViewModel 已初始化");
} }
#region public string LicenseInfo
{
get => _licenseInfo;
set => SetProperty(ref _licenseInfo, value);
}
public ObservableCollection<object> 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);
}
/// <summary>
/// 显示一个新窗口(非模态)
/// </summary>
private void ShowWindow(Window window, string name) private void ShowWindow(Window window, string name)
{ {
window.Owner = Application.Current.MainWindow; window.Owner = Application.Current.MainWindow;
@@ -183,9 +191,6 @@ namespace XplorePlane.ViewModels
_logger.Info("{Name} 窗口已打开", name); _logger.Info("{Name} 窗口已打开", name);
} }
/// <summary>
/// 显示或激活单例窗口(非模态,防止重复打开)
/// </summary>
private void ShowOrActivate(Window currentWindow, Action<Window> setWindow, Func<Window> factory, string name) private void ShowOrActivate(Window currentWindow, Action<Window> setWindow, Func<Window> factory, string name)
{ {
if (currentWindow != null && currentWindow.IsLoaded) if (currentWindow != null && currentWindow.IsLoaded)
@@ -203,10 +208,6 @@ namespace XplorePlane.ViewModels
_logger.Info("{Name} 窗口已打开", name); _logger.Info("{Name} 窗口已打开", name);
} }
#endregion
#region
private void ExecuteOpenToolbox() private void ExecuteOpenToolbox()
{ {
ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "算子工具箱"); ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "算子工具箱");
@@ -230,7 +231,6 @@ namespace XplorePlane.ViewModels
private void ExecuteCncEditorAction(Action<CncEditorViewModel> action) private void ExecuteCncEditorAction(Action<CncEditorViewModel> action)
{ {
ArgumentNullException.ThrowIfNull(action); ArgumentNullException.ThrowIfNull(action);
ShowCncEditor(); ShowCncEditor();
action(_cncEditorViewModel); action(_cncEditorViewModel);
} }
@@ -298,15 +298,12 @@ namespace XplorePlane.ViewModels
} }
} }
#endregion
#region
private void ExecuteAxisReset() private void ExecuteAxisReset()
{ {
var result = MessageBox.Show("确认执行轴复位操作?", "轴复位", var result = MessageBox.Show("确认执行轴复位操作?", "轴复位",
MessageBoxButton.OKCancel, MessageBoxImage.Question); MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (result != MessageBoxResult.OK) return; if (result != MessageBoxResult.OK)
return;
try try
{ {
@@ -379,8 +376,8 @@ namespace XplorePlane.ViewModels
bitmap.EndInit(); bitmap.EndInit();
bitmap.Freeze(); bitmap.Freeze();
_eventAggregator.GetEvent<ManualImageLoadedEvent>() _mainViewportService.SetManualImage(bitmap, dialog.FileName);
.Publish(new ManualImageLoadedPayload(bitmap, dialog.FileName)); RaisePropertyChanged(nameof(IsUsingLiveDetectorSource));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -389,11 +386,18 @@ namespace XplorePlane.ViewModels
} }
} }
private void ExecuteUseLiveDetectorSource()
{
_mainViewportService.SetSourceMode(MainViewportSourceMode.LiveDetector);
RaisePropertyChanged(nameof(IsUsingLiveDetectorSource));
}
private void ExecuteWarmUp() private void ExecuteWarmUp()
{ {
var messageBoxResult = MessageBox.Show("确认执行射线源暖机操作?", "暖机", var messageBoxResult = MessageBox.Show("确认执行射线源暖机操作?", "暖机",
MessageBoxButton.OKCancel, MessageBoxImage.Question); MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (messageBoxResult != MessageBoxResult.OK) return; if (messageBoxResult != MessageBoxResult.OK)
return;
try try
{ {
@@ -417,10 +421,6 @@ namespace XplorePlane.ViewModels
} }
} }
#endregion
#region
private void ExecuteOpenLanguageSwitcher() private void ExecuteOpenLanguageSwitcher()
{ {
try try
@@ -445,10 +445,6 @@ namespace XplorePlane.ViewModels
() => new XP.Common.GeneralForm.Views.RealTimeLogViewer(), "实时日志"); () => new XP.Common.GeneralForm.Views.RealTimeLogViewer(), "实时日志");
} }
#endregion
#region
private void OnNavigateHome() private void OnNavigateHome()
{ {
_logger.Info("导航到主页"); _logger.Info("导航到主页");
@@ -485,6 +481,13 @@ namespace XplorePlane.ViewModels
LicenseInfo = "编辑属性"; LicenseInfo = "编辑属性";
} }
#endregion private void OnMainViewportStateChanged(object sender, EventArgs e)
{
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
{
RaisePropertyChanged(nameof(IsMainViewportRealtimeEnabled));
RaisePropertyChanged(nameof(IsUsingLiveDetectorSource));
}));
}
} }
} }
@@ -1,24 +1,19 @@
using Prism.Events;
using Prism.Mvvm; using Prism.Mvvm;
using System; using System;
using System.Threading;
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions; using XplorePlane.Services.MainViewport;
using XP.Hardware.Detector.Abstractions.Events;
using XplorePlane.Events;
namespace XplorePlane.ViewModels namespace XplorePlane.ViewModels
{ {
/// <summary> /// <summary>
/// 实时图像 ViewModel订阅探测器采集事件并显示图像 /// 主界面实时图像 ViewModel只绑定主界面视口仲裁后的显示状态。
/// </summary> /// </summary>
public class ViewportPanelViewModel : BindableBase public class ViewportPanelViewModel : BindableBase
{ {
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private int _isProcessingFrame; private readonly IMainViewportService _mainViewportService;
private ImageSource _imageSource; private ImageSource _imageSource;
public ImageSource ImageSource public ImageSource ImageSource
@@ -27,81 +22,38 @@ namespace XplorePlane.ViewModels
set => SetProperty(ref _imageSource, value); set => SetProperty(ref _imageSource, value);
} }
private string _imageInfo = "等待图像..."; private string _imageInfo = "等待探测器图像...";
public string ImageInfo public string ImageInfo
{ {
get => _imageInfo; get => _imageInfo;
set => SetProperty(ref _imageInfo, value); 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<ViewportPanelViewModel>(); _logger = logger?.ForModule<ViewportPanelViewModel>();
eventAggregator.GetEvent<ImageCapturedEvent>() _mainViewportService.StateChanged += OnMainViewportStateChanged;
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread); UpdateFromState();
eventAggregator.GetEvent<ManualImageLoadedEvent>()
.Subscribe(OnManualImageLoaded, ThreadOption.UIThread);
eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
.Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread);
} }
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 try
{ {
var bitmap = ConvertToBitmapSource(args.ImageData, (int)args.Width, (int)args.Height); Application.Current?.Dispatcher?.BeginInvoke(new Action(UpdateFromState));
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);
}
}));
} }
catch (Exception ex) catch (Exception ex)
{ {
Interlocked.Exchange(ref _isProcessingFrame, 0); _logger?.Error(ex, "刷新主界面实时图像失败");
_logger?.Error(ex, "图像转换失败:{Message}", ex.Message);
} }
} }
private void OnManualImageLoaded(ManualImageLoadedPayload payload) private void UpdateFromState()
{ {
if (payload?.Image == null) return; ImageSource = _mainViewportService.CurrentDisplayImage;
ImageInfo = _mainViewportService.CurrentDisplayInfo;
ImageSource = payload.Image;
ImageInfo = $"手动加载: {payload.FileName}";
}
private void OnPipelinePreviewUpdated(PipelinePreviewUpdatedPayload payload)
{
if (payload?.Image == null) return;
ImageSource = payload.Image;
ImageInfo = payload.StatusMessage;
}
/// <summary>
/// 16 位灰度数据线性拉伸为 8 位 BitmapSource(委托给 XP.Common 通用转换器)
/// </summary>
private static BitmapSource ConvertToBitmapSource(ushort[] data, int width, int height)
{
return XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource(data, width, height);
} }
} }
} }
+33 -54
View File
@@ -70,7 +70,6 @@
<telerik:GroupVariant Priority="0" Variant="Large" /> <telerik:GroupVariant Priority="0" Variant="Large" />
</telerik:RadRibbonGroup.Variants> </telerik:RadRibbonGroup.Variants>
<!-- 实时控制: Live / Snap / 加载 / 保存 -->
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="新建CNC" telerik:ScreenTip.Title="新建CNC"
@@ -79,8 +78,8 @@
SmallImage="/Assets/Icons/new-doc.png" SmallImage="/Assets/Icons/new-doc.png"
Text="新建CNC" /> Text="新建CNC" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Description="保存当前X射线实时图像" telerik:ScreenTip.Description="保存当前 CNC 配置"
telerik:ScreenTip.Title="保存图像" telerik:ScreenTip.Title="保存"
Size="Medium" Size="Medium"
Command="{Binding SaveCncProgramCommand}" Command="{Binding SaveCncProgramCommand}"
SmallImage="/Assets/Icons/save.png" SmallImage="/Assets/Icons/save.png"
@@ -103,11 +102,7 @@
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="程序"> <telerik:RadRibbonGroup Header="程序">
<!-- 安全门控 & 系统 -->
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="运行" telerik:ScreenTip.Title="运行"
Size="Large" Size="Large"
@@ -115,7 +110,6 @@
Text="运行" /> Text="运行" />
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Description="停止" telerik:ScreenTip.Description="停止"
telerik:ScreenTip.Title="停止" telerik:ScreenTip.Title="停止"
@@ -123,6 +117,22 @@
SmallImage="/Assets/Icons/stop.png" SmallImage="/Assets/Icons/stop.png"
Text="停止" /> Text="停止" />
</StackPanel> </StackPanel>
<StackPanel>
<telerik:RadRibbonToggleButton
telerik:ScreenTip.Description="控制主界面实时图像是否随探测器新帧刷新"
telerik:ScreenTip.Title="主界面实时"
IsChecked="{Binding IsMainViewportRealtimeEnabled, Mode=TwoWay}"
Size="Medium"
SmallImage="/Assets/Icons/detector2.png"
Text="实时" />
<telerik:RadRibbonButton
telerik:ScreenTip.Description="切换主界面图像来源为实时探测器图像"
telerik:ScreenTip.Title="探测器图像"
Command="{Binding UseLiveDetectorSourceCommand}"
Size="Medium"
SmallImage="/Assets/Icons/open.png"
Text="探测器图像" />
</StackPanel>
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="开门" telerik:ScreenTip.Title="开门"
@@ -136,13 +146,13 @@
Text="关门" /> Text="关门" />
</StackPanel> </StackPanel>
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="快捷工具"> <telerik:RadRibbonGroup Header="快捷工具">
<telerik:RadRibbonButton <telerik:RadRibbonButton
Command="{Binding LoadImageCommand}" Command="{Binding LoadImageCommand}"
Size="Large" Size="Large"
SmallImage="/Assets/Icons/open.png" SmallImage="/Assets/Icons/open.png"
Text="加载图像" /> Text="加载图像" />
<!-- 快捷工具: 上下两列,带文字 -->
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="中心十字线" telerik:ScreenTip.Title="中心十字线"
@@ -150,19 +160,19 @@
SmallImage="/Assets/Icons/crosshair.png" SmallImage="/Assets/Icons/crosshair.png"
Text="辅助线" /> Text="辅助线" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="白背景检测黑区域" telerik:ScreenTip.Title="白底检测"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/film-darken.png" SmallImage="/Assets/Icons/film-darken.png"
Text="白底检测" /> Text="白底检测" />
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="灰度分布" telerik:ScreenTip.Title="灰度"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/film-darken.png" SmallImage="/Assets/Icons/film-darken.png"
Text="灰度" /> Text="灰度" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="黑背景检测白区域" telerik:ScreenTip.Title="黑底检测"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/film-darken.png" SmallImage="/Assets/Icons/film-darken.png"
Text="黑底检测" /> Text="黑底检测" />
@@ -197,7 +207,6 @@
<spreadsheetControls:RadVerticalAlignmentToBooleanConverter x:Key="verticalAlignmentToBooleanConverter" /> <spreadsheetControls:RadVerticalAlignmentToBooleanConverter x:Key="verticalAlignmentToBooleanConverter" />
</telerik:RadRibbonGroup.Resources> </telerik:RadRibbonGroup.Resources>
<!-- 第一列: 暖机 + 轴复位 上下排列 -->
<StackPanel> <StackPanel>
<telerik:RadRibbonToggleButton <telerik:RadRibbonToggleButton
telerik:ScreenTip.Description="暖机" telerik:ScreenTip.Description="暖机"
@@ -207,7 +216,6 @@
SmallImage="/Assets/Icons/heat-engine.png" SmallImage="/Assets/Icons/heat-engine.png"
Text="暖机" /> Text="暖机" />
<telerik:RadRibbonToggleButton <telerik:RadRibbonToggleButton
x:Name="MergeAndCenterButton"
telerik:ScreenTip.Description="轴复位" telerik:ScreenTip.Description="轴复位"
telerik:ScreenTip.Title="轴复位" telerik:ScreenTip.Title="轴复位"
Command="{Binding AxisResetCommand}" Command="{Binding AxisResetCommand}"
@@ -216,10 +224,8 @@
Text="轴复位" /> Text="轴复位" />
</StackPanel> </StackPanel>
<!-- 第二列: 射线源 + 探测器 + 运动控制 -->
<StackPanel> <StackPanel>
<telerik:RadRibbonToggleButton <telerik:RadRibbonToggleButton
x:Name="MergeAndCenterButton1"
telerik:ScreenTip.Description="射线源控制" telerik:ScreenTip.Description="射线源控制"
telerik:ScreenTip.Title="射线源" telerik:ScreenTip.Title="射线源"
Command="{Binding OpenRaySourceConfigCommand}" Command="{Binding OpenRaySourceConfigCommand}"
@@ -227,7 +233,6 @@
SmallImage="/Assets/Icons/xray.png" SmallImage="/Assets/Icons/xray.png"
Text="射线源" /> Text="射线源" />
<telerik:RadRibbonToggleButton <telerik:RadRibbonToggleButton
x:Name="MergeAndCenterButton2"
telerik:ScreenTip.Description="探测器控制" telerik:ScreenTip.Description="探测器控制"
telerik:ScreenTip.Title="探测器" telerik:ScreenTip.Title="探测器"
Command="{Binding OpenDetectorConfigCommand}" Command="{Binding OpenDetectorConfigCommand}"
@@ -235,7 +240,6 @@
SmallImage="/Assets/Icons/detector2.png" SmallImage="/Assets/Icons/detector2.png"
Text="探测器" /> Text="探测器" />
<telerik:RadRibbonToggleButton <telerik:RadRibbonToggleButton
x:Name="MergeAndCenterButton3"
telerik:ScreenTip.Description="运动控制" telerik:ScreenTip.Description="运动控制"
telerik:ScreenTip.Title="运动控制" telerik:ScreenTip.Title="运动控制"
Command="{Binding OpenMotionDebugCommand}" Command="{Binding OpenMotionDebugCommand}"
@@ -244,17 +248,16 @@
Text="运动控制" /> Text="运动控制" />
</StackPanel> </StackPanel>
<!-- 第三列: 相机设置 / PLC 地址配置 -->
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Description="打开相机参数设置对话框" telerik:ScreenTip.Description="打开相机参数设置窗口"
telerik:ScreenTip.Title="相机设置" telerik:ScreenTip.Title="相机设置"
Command="{Binding OpenCameraSettingsCommand}" Command="{Binding OpenCameraSettingsCommand}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/detector2.png" SmallImage="/Assets/Icons/detector2.png"
Text="相机设置" /> Text="相机设置" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Description="打开 PLC 信号地址定义编辑器" telerik:ScreenTip.Description="打开 PLC 地址配置窗口"
telerik:ScreenTip.Title="PLC 地址配置" telerik:ScreenTip.Title="PLC 地址配置"
Command="{Binding OpenPlcAddrConfigCommand}" Command="{Binding OpenPlcAddrConfigCommand}"
Size="Medium" Size="Medium"
@@ -263,7 +266,7 @@
</StackPanel> </StackPanel>
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<telerik:RadRibbonGroup telerik:ScreenTip.Title="图像算子" Header="图像算子"> <telerik:RadRibbonGroup Header="图像算子" telerik:ScreenTip.Title="图像算子">
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Description="打开算子工具箱,拖拽算子到流水线中" telerik:ScreenTip.Description="打开算子工具箱,拖拽算子到流水线中"
telerik:ScreenTip.Title="算子工具箱" telerik:ScreenTip.Title="算子工具箱"
@@ -278,7 +281,6 @@
<telerik:GroupVariant Priority="0" Variant="Large" /> <telerik:GroupVariant Priority="0" Variant="Large" />
</telerik:RadRibbonGroup.Variants> </telerik:RadRibbonGroup.Variants>
<!-- CNC 编辑器入口按钮 -->
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Description="打开 CNC 编辑器窗口,创建和编辑检测配方程序" telerik:ScreenTip.Description="打开 CNC 编辑器窗口,创建和编辑检测配方程序"
telerik:ScreenTip.Title="CNC 编辑器" telerik:ScreenTip.Title="CNC 编辑器"
@@ -287,9 +289,6 @@
SmallImage="/Assets/Icons/cnc.png" SmallImage="/Assets/Icons/cnc.png"
Text="CNC 编辑" /> Text="CNC 编辑" />
<!-- CNC 节点快捷工具 -->
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="参考点" telerik:ScreenTip.Title="参考点"
@@ -345,15 +344,13 @@
Text="插入等待" /> Text="插入等待" />
</StackPanel> </StackPanel>
<!-- 矩阵编排入口按钮 -->
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案" telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
telerik:ScreenTip.Title="矩阵编排" telerik:ScreenTip.Title="矩阵编排"
Command="{Binding OpenMatrixEditorCommand}" Command="{Binding OpenMatrixEditorCommand}"
Size="Large" Size="Large"
SmallImage="/Assets/Icons/matrix.png" SmallImage="/Assets/Icons/matrix.png"
Text="矩阵编排" /> Text="矩阵编排" />
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}"> <telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}">
@@ -400,19 +397,8 @@
Size="Large" Size="Large"
SmallImage="/Assets/Icons/spiral.png" /> SmallImage="/Assets/Icons/spiral.png" />
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<!--
<telerik:RadRibbonGroup Header="图像处理">
<telerik:RadRibbonGroup.Variants>
<telerik:GroupVariant Priority="0" Variant="Large" />
</telerik:RadRibbonGroup.Variants>
<telerik:RadRibbonButton
Command="{Binding OpenPipelineEditorCommand}"
Size="Large"
SmallImage="/Assets/Icons/workflow.png"
Text="流水线编辑器" />
</telerik:RadRibbonGroup>
-->
</telerik:RadRibbonTab> </telerik:RadRibbonTab>
<telerik:RadRibbonTab Header="关于"> <telerik:RadRibbonTab Header="关于">
<telerik:RadRibbonGroup Header="关于"> <telerik:RadRibbonGroup Header="关于">
<telerik:RadRibbonGroup.Variants> <telerik:RadRibbonGroup.Variants>
@@ -463,7 +449,6 @@
</telerik:RadRibbonView.ContextualGroups> </telerik:RadRibbonView.ContextualGroups>
</telerik:RadRibbonView> </telerik:RadRibbonView>
<!-- Row 1: 主内容区 - 比例布局 -->
<Grid <Grid
Grid.Row="1" Grid.Row="1"
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
@@ -474,7 +459,6 @@
<ColumnDefinition Width="350" /> <ColumnDefinition Width="350" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- 中间: 2D Viewport -->
<Border <Border
Grid.Column="0" Grid.Column="0"
BorderBrush="#DDDDDD" BorderBrush="#DDDDDD"
@@ -482,7 +466,6 @@
<views:ViewportPanelView /> <views:ViewportPanelView />
</Border> </Border>
<!-- 中间: 图像 -->
<Border <Border
Grid.Column="1" Grid.Column="1"
BorderBrush="#DDDDDD" BorderBrush="#DDDDDD"
@@ -490,7 +473,6 @@
<views:ImagePanelView /> <views:ImagePanelView />
</Border> </Border>
<!-- 右侧: 属性面板 -->
<Grid Grid.Column="2"> <Grid Grid.Column="2">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="350*" /> <ColumnDefinition Width="350*" />
@@ -501,12 +483,11 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<views1:RaySourceOperateView Grid.Row="0" Grid.ColumnSpan="2" /> <views1:RaySourceOperateView Grid.Row="0" Grid.ColumnSpan="2" />
<mcViews:AxisControlView Grid.Row="1" Grid.ColumnSpan="2"/> <mcViews:AxisControlView Grid.Row="1" Grid.ColumnSpan="2" />
<views:NavigationPropertyPanelView Grid.Row="2" Grid.ColumnSpan="2" /> <views:NavigationPropertyPanelView Grid.Row="2" Grid.ColumnSpan="2" />
</Grid> </Grid>
</Grid> </Grid>
<!-- Row 2: 状态栏 -->
<Border <Border
Grid.Row="2" Grid.Row="2"
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
@@ -520,7 +501,6 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- 左侧: 状态信息 -->
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
Margin="8,0" Margin="8,0"
@@ -530,7 +510,6 @@
Foreground="White" Foreground="White"
Text="就绪" /> Text="就绪" />
<!-- 右侧: 鼠标坐标 + RGB -->
<TextBlock <TextBlock
Grid.Column="1" Grid.Column="1"
Margin="8,0" Margin="8,0"
+18 -8
View File
@@ -17,17 +17,27 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- 标题栏 -->
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1"> <Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
<TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center" <TextBlock
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" /> Margin="4,2"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="#333333"
Text="实时图像" />
</Border> </Border>
<!-- 图像显示区域,支持滚动、缩放和ROI --> <roi:PolygonRoiCanvas
<roi:PolygonRoiCanvas Grid.Row="1" Grid.Row="1"
ImageSource="{Binding ImageSource}" Background="White"
Background="White" /> ImageSource="{Binding ImageSource}" />
<Border Grid.Row="2" Height="24" Padding="8,0" Background="#1E1E1E">
<TextBlock
VerticalAlignment="Center"
FontSize="11"
Foreground="#F2F2F2"
Text="{Binding ImageInfo}" />
</Border>
</Grid> </Grid>
</UserControl> </UserControl>