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 26cb81b..b818096 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 解析前已注册 @@ -265,6 +282,33 @@ namespace XplorePlane // 主窗口加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化 shell.Loaded += (s, e) => { + TryConnectCamera(); + + // 初始化主界面探测器帧流水线,开始接收探测器图像事件 + try + { + _ = Container.Resolve(); + } + catch (Exception ex) + { + Log.Error(ex, "初始化主界面探测器帧流水线失败"); + } + + // 通知 ViewModel 相机状态已确定,启动实时预览或显示错误 + try + { + var cameraVm = Container.Resolve(); + cameraVm.OnCameraReady(); + } + catch (Exception ex) + { + Log.Error(ex, "通知相机 ViewModel 失败"); + } + + if (_cameraError != null) + { + HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error); + } //TryConnectCamera(); //// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误 @@ -329,11 +373,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 +408,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 +420,10 @@ namespace XplorePlane containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); + // ── 主界面实时图像 / 探测器双队列服务(单例)── + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + // ── CNC / 矩阵 ViewModel(瞬态)── containerRegistry.Register(); containerRegistry.Register(); @@ -405,13 +444,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/Controls/AnimatedSwitch.cs b/XplorePlane/Controls/AnimatedSwitch.cs new file mode 100644 index 0000000..0a5c029 --- /dev/null +++ b/XplorePlane/Controls/AnimatedSwitch.cs @@ -0,0 +1,95 @@ +using System.Windows; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; + +namespace XplorePlane.Controls +{ + public class AnimatedSwitch : ToggleButton + { + static AnimatedSwitch() + { + DefaultStyleKeyProperty.OverrideMetadata( + typeof(AnimatedSwitch), + new FrameworkPropertyMetadata(typeof(AnimatedSwitch))); + } + + public static readonly DependencyProperty OnBrushProperty = + DependencyProperty.Register( + nameof(OnBrush), + typeof(Brush), + typeof(AnimatedSwitch), + new PropertyMetadata(new SolidColorBrush(Color.FromRgb(45, 204, 112)))); + + public static readonly DependencyProperty OffBrushProperty = + DependencyProperty.Register( + nameof(OffBrush), + typeof(Brush), + typeof(AnimatedSwitch), + new PropertyMetadata(new SolidColorBrush(Color.FromRgb(205, 212, 218)))); + + public static readonly DependencyProperty ThumbBrushProperty = + DependencyProperty.Register( + nameof(ThumbBrush), + typeof(Brush), + typeof(AnimatedSwitch), + new PropertyMetadata(Brushes.White)); + + public static readonly DependencyProperty SwitchWidthProperty = + DependencyProperty.Register( + nameof(SwitchWidth), + typeof(double), + typeof(AnimatedSwitch), + new PropertyMetadata(44d)); + + public static readonly DependencyProperty SwitchHeightProperty = + DependencyProperty.Register( + nameof(SwitchHeight), + typeof(double), + typeof(AnimatedSwitch), + new PropertyMetadata(24d)); + + public Brush OnBrush + { + get => (Brush)GetValue(OnBrushProperty); + set => SetValue(OnBrushProperty, value); + } + + public Brush OffBrush + { + get => (Brush)GetValue(OffBrushProperty); + set => SetValue(OffBrushProperty, value); + } + + public Brush ThumbBrush + { + get => (Brush)GetValue(ThumbBrushProperty); + set => SetValue(ThumbBrushProperty, value); + } + + public double SwitchWidth + { + get => (double)GetValue(SwitchWidthProperty); + set => SetValue(SwitchWidthProperty, value); + } + + public double SwitchHeight + { + get => (double)GetValue(SwitchHeightProperty); + set => SetValue(SwitchHeightProperty, value); + } + + protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) + { + if (!IsEnabled) + { + base.OnPreviewMouseLeftButtonDown(e); + return; + } + + e.Handled = true; + Focus(); + OnClick(); + } + } +} diff --git a/XplorePlane/Properties/AssemblyInfo.cs b/XplorePlane/Properties/AssemblyInfo.cs index f2bf64e..2ae47d0 100644 --- a/XplorePlane/Properties/AssemblyInfo.cs +++ b/XplorePlane/Properties/AssemblyInfo.cs @@ -1,3 +1,7 @@ +using System.Windows; using System.Runtime.CompilerServices; +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, + ResourceDictionaryLocation.SourceAssembly)] [assembly: InternalsVisibleTo("XplorePlane.Tests")] 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/Themes/Generic.xaml b/XplorePlane/Themes/Generic.xaml new file mode 100644 index 0000000..055be1d --- /dev/null +++ b/XplorePlane/Themes/Generic.xaml @@ -0,0 +1,125 @@ + + + + diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 0b3087c..f3db8d7 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -1,8 +1,8 @@ -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; @@ -10,6 +10,7 @@ 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,94 +23,14 @@ 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 PointDistanceMeasureCommand { get; } - public DelegateCommand PointLineDistanceMeasureCommand { get; } - public DelegateCommand AngleMeasureCommand { get; } - public DelegateCommand ThroughHoleFillRateMeasureCommand { get; } - - // 辅助线命令 - public DelegateCommand ToggleCrosshairCommand { 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; @@ -117,21 +38,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)); + _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); @@ -139,7 +67,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(), "流水线编辑器")); @@ -161,17 +88,13 @@ namespace XplorePlane.ViewModels InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute())); InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute())); - // 测量命令 PointDistanceMeasureCommand = new DelegateCommand(ExecutePointDistanceMeasure); PointLineDistanceMeasureCommand = new DelegateCommand(ExecutePointLineDistanceMeasure); AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure); ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure); - - // 辅助线命令 ToggleCrosshairCommand = new DelegateCommand(() => _eventAggregator.GetEvent().Publish()); - // 硬件命令 AxisResetCommand = new DelegateCommand(ExecuteAxisReset); OpenDetectorConfigCommand = new DelegateCommand(ExecuteOpenDetectorConfig); OpenMotionDebugCommand = new DelegateCommand(ExecuteOpenMotionDebug); @@ -179,9 +102,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); @@ -190,11 +113,92 @@ 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 PointDistanceMeasureCommand { get; } + public DelegateCommand PointLineDistanceMeasureCommand { get; } + public DelegateCommand AngleMeasureCommand { get; } + public DelegateCommand ThroughHoleFillRateMeasureCommand { get; } + public DelegateCommand ToggleCrosshairCommand { 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; @@ -202,9 +206,6 @@ namespace XplorePlane.ViewModels _logger.Info("{Name} 窗口已打开", name); } - /// - /// 显示或激活单例窗口(非模态,防止重复打开) - /// private void ShowOrActivate(Window currentWindow, Action setWindow, Func factory, string name) { if (currentWindow != null && currentWindow.IsLoaded) @@ -222,10 +223,6 @@ namespace XplorePlane.ViewModels _logger.Info("{Name} 窗口已打开", name); } - #endregion - - #region 窗口打开命令实现 - private void ExecuteOpenToolbox() { ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "算子工具箱"); @@ -249,7 +246,6 @@ namespace XplorePlane.ViewModels private void ExecuteCncEditorAction(Action action) { ArgumentNullException.ThrowIfNull(action); - ShowCncEditor(); action(_cncEditorViewModel); } @@ -317,15 +313,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 { @@ -398,8 +391,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) { @@ -408,11 +401,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 { @@ -436,10 +436,6 @@ namespace XplorePlane.ViewModels } } - #endregion - - #region 测量命令实现 - private void ExecutePointDistanceMeasure() { _logger.Info("点点距测量功能已触发"); @@ -455,19 +451,15 @@ namespace XplorePlane.ViewModels private void ExecuteAngleMeasure() { _logger.Info("角度测量功能已触发"); - // TODO: 实现角度测量逻辑 + _eventAggregator.GetEvent().Publish(MeasurementToolMode.Angle); } private void ExecuteThroughHoleFillRateMeasure() { _logger.Info("通孔填锡率测量功能已触发"); - // TODO: 实现通孔填锡率测量逻辑 + _eventAggregator.GetEvent().Publish(MeasurementToolMode.ThroughHoleFillRate); } - #endregion - - #region 设置命令实现 - private void ExecuteOpenLanguageSwitcher() { try @@ -492,10 +484,6 @@ namespace XplorePlane.ViewModels () => new XP.Common.GeneralForm.Views.RealTimeLogViewer(), "实时日志"); } - #endregion - - #region 导航命令实现 - private void OnNavigateHome() { _logger.Info("导航到主页"); @@ -532,6 +520,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 6d022b8..39a7d9f 100644 --- a/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs +++ b/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs @@ -2,43 +2,60 @@ using Prism.Commands; 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 readonly IMainViewportService _mainViewportService; private readonly IEventAggregator _eventAggregator; - private int _isProcessingFrame; private ImageSource _imageSource; + private string _imageInfo = "等待探测器图像..."; + private MeasurementToolMode _currentMeasurementMode = MeasurementToolMode.None; + private Point? _measurePoint1; + private Point? _measurePoint2; + private string _measurementResult; + + public ViewportPanelViewModel( + IMainViewportService mainViewportService, + IEventAggregator eventAggregator, + ILoggerService logger) + { + _mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); + _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); + + CancelMeasurementCommand = new DelegateCommand(ExecuteCancelMeasurement); + + _mainViewportService.StateChanged += OnMainViewportStateChanged; + _eventAggregator.GetEvent() + .Subscribe(OnMeasurementToolActivated, ThreadOption.UIThread); + + UpdateFromState(updateInfo: true); + } + public ImageSource ImageSource { get => _imageSource; set => SetProperty(ref _imageSource, value); } - private string _imageInfo = "等待图像..."; public string ImageInfo { get => _imageInfo; set => SetProperty(ref _imageInfo, value); } - #region 测量工具状态 - - private MeasurementToolMode _currentMeasurementMode = MeasurementToolMode.None; public MeasurementToolMode CurrentMeasurementMode { get => _currentMeasurementMode; @@ -48,8 +65,6 @@ namespace XplorePlane.ViewModels { RaisePropertyChanged(nameof(IsMeasuring)); RaisePropertyChanged(nameof(MeasurementModeText)); - // 切换模式时重置状态 - ResetMeasurementState(); } } } @@ -59,189 +74,87 @@ namespace XplorePlane.ViewModels public string MeasurementModeText => CurrentMeasurementMode switch { MeasurementToolMode.PointDistance => "点点距测量 - 请在图像上点击第一个点", + MeasurementToolMode.PointLineDistance => "点线距测量 - 请按工具提示继续操作", + MeasurementToolMode.Angle => "角度测量 - 功能待接入", + MeasurementToolMode.ThroughHoleFillRate => "通孔填锡率测量 - 功能待接入", _ => string.Empty }; - // 测量点坐标(图像像素坐标) - private Point? _measurePoint1; public Point? MeasurePoint1 { get => _measurePoint1; set => SetProperty(ref _measurePoint1, value); } - private Point? _measurePoint2; public Point? MeasurePoint2 { get => _measurePoint2; set => SetProperty(ref _measurePoint2, value); } - private string _measurementResult; public string MeasurementResult { get => _measurementResult; set => SetProperty(ref _measurementResult, value); } - /// - /// 由 View 层调用:用户在画布上点击了一个点(像素坐标) - /// - public void OnMeasurementPointClicked(Point imagePoint) - { - if (CurrentMeasurementMode == MeasurementToolMode.PointDistance) - { - if (MeasurePoint1 == null) - { - MeasurePoint1 = imagePoint; - ImageInfo = $"点点距测量 - 第一点: ({imagePoint.X:F0}, {imagePoint.Y:F0}),请点击第二个点"; - _logger?.Info("测量第一点: ({X}, {Y})", imagePoint.X, imagePoint.Y); - } - else - { - MeasurePoint2 = imagePoint; - CalculatePointDistance(); - } - } - } - - private void CalculatePointDistance() - { - if (MeasurePoint1 == null || MeasurePoint2 == null) return; - - var p1 = MeasurePoint1.Value; - var p2 = MeasurePoint2.Value; - double dx = p2.X - p1.X; - double dy = p2.Y - p1.Y; - double distance = Math.Sqrt(dx * dx + dy * dy); - double angle = Math.Atan2(dy, dx) * 180.0 / Math.PI; - - MeasurementResult = $"{distance:F2} px"; - ImageInfo = $"点点距: {distance:F2} px | 角度: {angle:F2}° | ({p1.X:F0},{p1.Y:F0}) → ({p2.X:F0},{p2.Y:F0})"; - _logger?.Info("点点距测量完成: {Distance:F2} px, 角度: {Angle:F2}°", distance, angle); - } - - /// - /// 取消/重置当前测量 - /// - public DelegateCommand CancelMeasurementCommand { get; private set; } + public DelegateCommand CancelMeasurementCommand { get; } public void ResetMeasurementState() { MeasurePoint1 = null; MeasurePoint2 = null; MeasurementResult = null; + + if (CurrentMeasurementMode == MeasurementToolMode.None) + { + ImageInfo = _mainViewportService.CurrentDisplayInfo; + } } - #endregion - - public ViewportPanelViewModel(IEventAggregator eventAggregator, ILoggerService logger) + private void ExecuteCancelMeasurement() { - _logger = logger?.ForModule(); - _eventAggregator = eventAggregator; - - CancelMeasurementCommand = new DelegateCommand(() => - { - // 发布 None 事件,让 View 层也收到 - _eventAggregator.GetEvent().Publish(MeasurementToolMode.None); - ImageInfo = "测量已取消"; - }); - - eventAggregator.GetEvent() - .Subscribe(OnImageCaptured, ThreadOption.BackgroundThread); - eventAggregator.GetEvent() - .Subscribe(OnManualImageLoaded, ThreadOption.UIThread); - eventAggregator.GetEvent() - .Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread); - - // 订阅测量工具事件 - eventAggregator.GetEvent() - .Subscribe(OnMeasurementToolActivated, ThreadOption.UIThread); - - // 订阅十字辅助线切换事件 - eventAggregator.GetEvent() - .Subscribe(OnToggleCrosshair, ThreadOption.UIThread); + _eventAggregator.GetEvent().Publish(MeasurementToolMode.None); + ImageInfo = "测量已取消"; } private void OnMeasurementToolActivated(MeasurementToolMode mode) { CurrentMeasurementMode = mode; - _logger?.Info("测量工具模式切换: {Mode}", mode); + ResetMeasurementState(); + + if (mode == MeasurementToolMode.None) + { + ImageInfo = _mainViewportService.CurrentDisplayInfo; + } + else + { + ImageInfo = MeasurementModeText; + } + + _logger.Info("测量工具模式切换: {Mode}", mode); } - #region 十字辅助线 - - private bool _showCrosshair; - public bool ShowCrosshair + private void OnMainViewportStateChanged(object sender, EventArgs e) { - get => _showCrosshair; - set => SetProperty(ref _showCrosshair, value); - } - - private void OnToggleCrosshair() - { - ShowCrosshair = !ShowCrosshair; - _logger?.Info("十字辅助线: {State}", ShowCrosshair ? "显示" : "隐藏"); - } - - #endregion - - private void OnImageCaptured(ImageCapturedEventArgs args) - { - 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(updateInfo: CurrentMeasurementMode == MeasurementToolMode.None))); } 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(bool updateInfo) { - if (payload?.Image == null) return; + ImageSource = _mainViewportService.CurrentDisplayImage; - 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); + if (updateInfo) + { + ImageInfo = _mainViewportService.CurrentDisplayInfo; + } } } } diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml index 42597af..d1e32a9 100644 --- a/XplorePlane/Views/Main/MainWindow.xaml +++ b/XplorePlane/Views/Main/MainWindow.xaml @@ -4,6 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:XplorePlane.Controls" xmlns:spreadsheet="clr-namespace:Telerik.Windows.Controls.Spreadsheet;assembly=Telerik.Windows.Controls.Spreadsheet" xmlns:spreadsheetControls="clr-namespace:Telerik.Windows.Controls.Spreadsheet.Controls;assembly=Telerik.Windows.Controls.Spreadsheet" xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" @@ -70,7 +71,6 @@ - - - - - - + + + + + - @@ -182,89 +193,51 @@ - + + + - - - - - - - - - - - - - - - - - - + + SmallImage="/Assets/Icons/crosshair.png" + Text="点点距测量" /> + SmallImage="/Assets/Icons/mark.png" + Text="点线距测量" /> + + + + + + - + - - - - - - + telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案" + telerik:ScreenTip.Title="矩阵编排" + Command="{Binding OpenMatrixEditorCommand}" + Size="Large" + SmallImage="/Assets/Icons/matrix.png" + Text="矩阵编排" /> @@ -402,100 +369,122 @@ SmallImage="/Assets/Icons/spiral.png" /> - + + + + + + + - - - + + + + + + + + - + telerik:ScreenTip.Description="打开相机参数设置窗口" + telerik:ScreenTip.Title="相机设置" + Command="{Binding OpenCameraSettingsCommand}" + Size="Medium" + SmallImage="/Assets/Icons/detector2.png" + Text="相机设置" /> + telerik:ScreenTip.Description="打开 PLC 地址配置窗口" + telerik:ScreenTip.Title="PLC 地址配置" + Command="{Binding OpenPlcAddrConfigCommand}" + Size="Medium" + SmallImage="/Assets/Icons/tools.png" + Text="PLC 地址" /> - + + - - - - - - - - - - - - - + Size="Large" + SmallImage="/Assets/Icons/message.png" + Command="{Binding OpenUserManualCommand}" + Text="帮助文档" /> + + + @@ -506,7 +495,6 @@ - - - - @@ -544,12 +529,11 @@ - + - - - - - + - - + @@ -38,14 +42,10 @@ - + - - - -