using Emgu.CV; using Emgu.CV.Structure; using Microsoft.Win32; using Prism.Commands; using Prism.Dialogs; using Prism.Events; using Prism.Ioc; using Prism.Mvvm; using System; using System.Collections.ObjectModel; using System.Configuration; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using XP.Camera.Calibration; using XP.Common.GeneralForm.Views; using XP.Common.Logging.Interfaces; using XP.Common.PdfViewer.Interfaces; using XP.Hardware.MotionControl.Abstractions; using XP.Hardware.MotionControl.Services; using XplorePlane.Events; using XplorePlane.Services.MainViewport; using XP.ImageProcessing.Processors; using XplorePlane.Services.Storage; using XplorePlane.ViewModels.Cnc; using XplorePlane.ViewModels.ImageProcessing; using XplorePlane.Views; using XplorePlane.Views.Cnc; using XplorePlane.Views.ImageProcessing; namespace XplorePlane.ViewModels { public class MainViewModel : BindableBase { private const double CncEditorHostWidth = 452d; private readonly ILoggerService _logger; private readonly IContainerProvider _containerProvider; private readonly IEventAggregator _eventAggregator; private readonly IMainViewportService _mainViewportService; private readonly IXpDataPathService _xpDataPathService; private readonly CncEditorViewModel _cncEditorViewModel; private readonly CncPageView _cncPageView; public string LicenseInfo { get => _licenseInfo; set => SetProperty(ref _licenseInfo, value); } private string _statusMessage = "就绪"; public string StatusMessage { get => _statusMessage; set => SetProperty(ref _statusMessage, 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 OpenSettingsCommand { get; } public DelegateCommand BrowseDataRootPathCommand { get; } public DelegateCommand ResetDataRootPathCommand { get; } public DelegateCommand SaveDataRootPathCommand { 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 InsertBuiltInInspectionModuleCommand { get; } public DelegateCommand InsertSaveNodeCommand { get; } public DelegateCommand InsertPauseDialogCommand { get; } public DelegateCommand InsertWaitDelayCommand { get; } public DelegateCommand RunCncCommand { get; } public DelegateCommand StopCncCommand { get; } //导航相机 public DelegateCommand OpenCameraSettingsCommand { get; } public DelegateCommand OpenCameraChessboardCalibrationCommand { get; } public DelegateCommand OpenCameraCalibrationCommand { get; } // 硬件命令 public DelegateCommand AxisResetCommand { get; } public DelegateCommand OpenDoorCommand { get; } public DelegateCommand CloseDoorCommand { 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 BgaDetectionCommand { get; } public DelegateCommand VoidDetectionCommand { get; } public DelegateCommand BubbleMeasureCommand { get; } private bool _isScaleBarVisible; public bool IsScaleBarVisible { get => _isScaleBarVisible; set => SetProperty(ref _isScaleBarVisible, value); } // 辅助线命令 public DelegateCommand ToggleCrosshairCommand { get; } // 图像处理命令 public DelegateCommand WhiteBackgroundDetectionCommand { get; } public DelegateCommand BlackBackgroundDetectionCommand { get; } public DelegateCommand OpenTemplateMatchAssistantCommand { get; } public DelegateCommand GrayscaleCommand { get; } public DelegateCommand SharpenCommand { get; } public DelegateCommand EnhanceCommand { 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(); RaisePropertyChanged(nameof(IsMeasurementToolsEnabled)); } } /// 测量工具是否可用(实时模式关闭时启用) public bool IsMeasurementToolsEnabled => !IsMainViewportRealtimeEnabled; public bool IsUsingLiveDetectorSource => _mainViewportService.CurrentSourceMode == MainViewportSourceMode.LiveDetector; public string DataRootPath { get => _dataRootPath; set => SetProperty(ref _dataRootPath, value); } public string PlanRootPath => _xpDataPathService.PlanPath; public string ToolsRootPath => _xpDataPathService.ToolsPath; public string ResultsRootPath => _xpDataPathService.DataPath; public string ReportRootPath => _xpDataPathService.ReportPath; public ObservableCollection BuiltInInspectionModules { get; } = new(); public BuiltInInspectionModuleItem SelectedBuiltInInspectionModule { get => _selectedBuiltInInspectionModule; set { if (SetProperty(ref _selectedBuiltInInspectionModule, value)) { InsertBuiltInInspectionModuleCommand?.RaiseCanExecuteChanged(); } } } /// 右侧图像区域内容 | 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; private Window _realTimeLogViewerWindow; private Window _settingsWindow; private Window _toolboxWindow; private Window _raySourceConfigWindow; private Window _templateMatchAssistantWindow; private object _imagePanelContent; private GridLength _viewportPanelWidth = new(1, GridUnitType.Star); private GridLength _imagePanelWidth = new(320); private bool _isCncEditorMode; private string _licenseInfo = "当前时间"; private string _dataRootPath = string.Empty; private BuiltInInspectionModuleItem _selectedBuiltInInspectionModule; public MainViewModel( ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator, IMainViewportService mainViewportService, IXpDataPathService xpDataPathService) { _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)); _xpDataPathService = xpDataPathService ?? throw new ArgumentNullException(nameof(xpDataPathService)); _cncEditorViewModel = _containerProvider.Resolve(); _cncPageView = new CncPageView { DataContext = _cncEditorViewModel }; _mainViewportService.StateChanged += OnMainViewportStateChanged; _cncEditorViewModel.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(CncEditorViewModel.StatusMessage)) RaisePropertyChanged(nameof(CncStatusMessage)); else if (e.PropertyName == nameof(CncEditorViewModel.HasExecutionError)) RaisePropertyChanged(nameof(CncHasExecutionError)); else if (e.PropertyName == nameof(CncEditorViewModel.IsRunning)) { RunCncCommand.RaiseCanExecuteChanged(); StopCncCommand.RaiseCanExecuteChanged(); } else if (e.PropertyName == nameof(CncEditorViewModel.SelectedNode)) { var node = _cncEditorViewModel.SelectedNode; if (node?.ResultImage != null) { _logger.Info("[Image] Switched to node [{Name}], showing cached result image.", node.Name); _mainViewportService.SetManualImage(node.ResultImage, $"CNC node: {node.Name}"); } } }; _cncEditorViewModel.RunCncCommand.CanExecuteChanged += (s, e) => RunCncCommand.RaiseCanExecuteChanged(); _cncEditorViewModel.StopCncCommand.CanExecuteChanged += (s, e) => StopCncCommand.RaiseCanExecuteChanged(); _eventAggregator.GetEvent() .Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread); _eventAggregator.GetEvent() .Subscribe(OnWhiteBackgroundRoiDrawn, ThreadOption.UIThread); _eventAggregator.GetEvent() .Subscribe(OnBlackBackgroundRoiDrawn, ThreadOption.UIThread); NavigationTree = new ObservableCollection(); NavigateHomeCommand = new DelegateCommand(OnNavigateHome); NavigateInspectCommand = new DelegateCommand(OnNavigateInspect); OpenFileCommand = new DelegateCommand(OnOpenFile); ExportCommand = new DelegateCommand(OnExport); ClearCommand = new DelegateCommand(OnClear); EditPropertiesCommand = new DelegateCommand(OnEditProperties); LoadImageCommand = new DelegateCommand(ExecuteLoadImage); OpenCncEditorCommand = new DelegateCommand(ExecuteOpenCncEditor); OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编辑")); OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox); OpenLibraryVersionsCommand = new DelegateCommand(() => ShowWindow(new Views.LibraryVersionsWindow(), "关于")); OpenUserManualCommand = new DelegateCommand(ExecuteOpenUserManual); OpenCameraSettingsCommand = new DelegateCommand(ExecuteOpenCameraSettings); OpenCameraChessboardCalibrationCommand = new DelegateCommand(ExecuteOpenCameraChessboardCalibration); OpenCameraCalibrationCommand = new DelegateCommand(ExecuteOpenCameraCalibration); OpenSettingsCommand = new DelegateCommand(ExecuteOpenSettings); BrowseDataRootPathCommand = new DelegateCommand(ExecuteBrowseDataRootPath); ResetDataRootPathCommand = new DelegateCommand(ExecuteResetDataRootPath); SaveDataRootPathCommand = new DelegateCommand(ExecuteSaveDataRootPath); NewCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.NewProgramCommand.Execute())); SaveCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.SaveProgramCommand.Execute())); LoadCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.LoadProgramCommand.Execute())); InsertReferencePointCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertReferencePointCommand.Execute())); InsertSavePositionCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertSavePositionCommand.Execute())); InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertCompleteProgramCommand.Execute())); InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionMarkerCommand.Execute())); InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionModuleCommand.Execute())); InsertBuiltInInspectionModuleCommand = new DelegateCommand( async () => await ExecuteInsertBuiltInInspectionModuleAsync(), CanExecuteInsertBuiltInInspectionModule); InsertSaveNodeCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertSaveNodeCommand.Execute())); InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute())); InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute())); RunCncCommand = new DelegateCommand( () => ExecuteCncEditorAction(vm => vm.RunCncCommand.Execute()), () => _cncEditorViewModel.RunCncCommand.CanExecute()); StopCncCommand = new DelegateCommand( () => ExecuteCncEditorAction(vm => vm.StopCncCommand.Execute()), () => _cncEditorViewModel.StopCncCommand.CanExecute()); PointDistanceMeasureCommand = new DelegateCommand(ExecutePointDistanceMeasure); PointLineDistanceMeasureCommand = new DelegateCommand(ExecutePointLineDistanceMeasure); AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure); ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure); BgaDetectionCommand = new DelegateCommand(ExecuteBgaDetection); VoidDetectionCommand = new DelegateCommand(ExecuteVoidDetection); BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure); // 辅助线命令 ToggleCrosshairCommand = new DelegateCommand(() => _eventAggregator.GetEvent().Publish()); // 图像处理命令 WhiteBackgroundDetectionCommand = new DelegateCommand(ExecuteWhiteBackgroundDetection); BlackBackgroundDetectionCommand = new DelegateCommand(ExecuteBlackBackgroundDetection); OpenTemplateMatchAssistantCommand = new DelegateCommand(ExecuteOpenTemplateMatchAssistant); GrayscaleCommand = new DelegateCommand(ExecuteGrayscale); SharpenCommand = new DelegateCommand(ExecuteSharpen); EnhanceCommand = new DelegateCommand(ExecuteEnhance); AxisResetCommand = new DelegateCommand(ExecuteAxisReset); OpenDoorCommand = new DelegateCommand(ExecuteOpenDoor); CloseDoorCommand = new DelegateCommand(ExecuteCloseDoor); OpenDetectorConfigCommand = new DelegateCommand(ExecuteOpenDetectorConfig); OpenMotionDebugCommand = new DelegateCommand(ExecuteOpenMotionDebug); OpenPlcAddrConfigCommand = new DelegateCommand(ExecuteOpenPlcAddrConfig); 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); ImagePanelWidth = new GridLength(320); DataRootPath = _xpDataPathService.RootPath; LoadBuiltInInspectionModules(); _logger.Info("MainViewModel 已初始化"); } public string CncStatusMessage => _cncEditorViewModel.StatusMessage; public bool CncHasExecutionError => _cncEditorViewModel.HasExecutionError; private string _cursorInfoText = "X: -- Y: -- Gray: --"; public string CursorInfoText { get => _cursorInfoText; set => SetProperty(ref _cursorInfoText, value); } private void ShowWindow(Window window, string name) { window.Owner = Application.Current.MainWindow; window.Show(); _logger.Info("{Name} 窗口已打开", name); } private void ShowOrActivate(Window currentWindow, Action setWindow, Func factory, string name) { if (currentWindow != null && currentWindow.IsLoaded) { currentWindow.Activate(); return; } var window = factory(); window.Owner = Application.Current.MainWindow; window.ShowInTaskbar = true; window.Closed += (s, e) => setWindow(null); window.Show(); setWindow(window); _logger.Info("{Name} 窗口已打开", name); } private void ExecuteOpenToolbox() { ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "Operator Toolbox"); } private void ExecuteOpenCncEditor() { if (_isCncEditorMode) { ImagePanelContent = new PipelineEditorView(); ViewportPanelWidth = new GridLength(1, GridUnitType.Star); ImagePanelWidth = new GridLength(320); _isCncEditorMode = false; _logger.Info("已退出 CNC 编辑模式"); return; } ShowCncEditor(); } private void ExecuteCncEditorAction(Action action) { ArgumentNullException.ThrowIfNull(action); ShowCncEditor(); action(_cncEditorViewModel); } private void ShowCncEditor() { ImagePanelContent = _cncPageView; ViewportPanelWidth = new GridLength(1, GridUnitType.Star); ImagePanelWidth = new GridLength(CncEditorHostWidth); _isCncEditorMode = true; _logger.Info("CNC 编辑器已切换到主界面图像区域"); } private void ExecuteOpenUserManual() { try { var manualPath = ConfigurationManager.AppSettings["UserManual"]; if (string.IsNullOrEmpty(manualPath)) { _logger.Warn("User manual path is not configured."); MessageBox.Show("User manual path is not configured. Please check the UserManual setting in App.config.", "Info", MessageBoxButton.OK, MessageBoxImage.Warning); return; } if (!File.Exists(manualPath)) { _logger.Warn("User manual file not found: {Path}", manualPath); MessageBox.Show($"User manual file not found:\n{manualPath}", "Info", MessageBoxButton.OK, MessageBoxImage.Warning); return; } var pdfViewerService = _containerProvider.Resolve(); var stream = File.OpenRead(manualPath); var fileName = Path.GetFileName(manualPath); pdfViewerService.OpenViewer(stream, fileName); } catch (Exception ex) { _logger.Error(ex, "Failed to open user manual."); MessageBox.Show($"Failed to open user manual: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private void ExecuteOpenCameraSettings() { try { var vm = _containerProvider.Resolve(); if (!vm.IsCameraConnected) { MessageBox.Show("Please connect the camera first", "Info", MessageBoxButton.OK, MessageBoxImage.Information); return; } var window = new Views.CameraSettingsWindow(vm) { Owner = Application.Current.MainWindow }; window.Show(); } catch (Exception ex) { _logger.Error(ex, "Failed to open camera settings."); } } private void ExecuteOpenCameraChessboardCalibration() { var chessboardWindow = new System.Windows.Window { Title = XP.Common.Resources.Resources.ChessboardToolTitle, Width = 1600, Height = 900, WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen, Icon = System.Windows.Application.Current.MainWindow?.Icon }; var calibrationDialogService = new XP.Camera.Calibration.DefaultCalibrationDialogService(); var chessboardViewModel = new XP.Camera.Calibration.ViewModels.ChessboardCalibrationViewModel(calibrationDialogService); var chessboardControl = new XP.Camera.Calibration.Controls.ChessboardCalibrationControl { DataContext = chessboardViewModel }; chessboardWindow.Content = chessboardControl; chessboardWindow.ShowDialog(); } private void ExecuteOpenCameraCalibration() { var calibrationWindow = new System.Windows.Window { Title = XP.Common.Resources.Resources.CalibrationToolTitle, Width = 1400, Height = 850, WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen, Icon = System.Windows.Application.Current.MainWindow?.Icon }; var calibrationDialogService = new XP.Camera.Calibration.DefaultCalibrationDialogService(); var calibrationViewModel = new XP.Camera.Calibration.ViewModels.CalibrationViewModel(calibrationDialogService); var calibrationControl = new XP.Camera.Calibration.Controls.CalibrationControl { DataContext = calibrationViewModel }; calibrationWindow.Content = calibrationControl; calibrationWindow.ShowDialog(); } private void ExecuteOpenSettings() { try { ShowOrActivate(_settingsWindow, w => _settingsWindow = w, () => new Views.SettingsWindow(this), "Settings"); } catch (Exception ex) { _logger.Error(ex, "Failed to open settings window"); MessageBox.Show($"Failed to open settings window: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private void ExecuteBrowseDataRootPath() { try { var dialog = new OpenFolderDialog { Title = "选择 XP 数据根目录", InitialDirectory = Directory.Exists(DataRootPath) ? DataRootPath : _xpDataPathService.RootPath }; if (dialog.ShowDialog() == true) { DataRootPath = dialog.FolderName; } } catch (Exception ex) { _logger.Error(ex, "Failed to browse XP data root."); MessageBox.Show($"Failed to browse data root: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private void ExecuteResetDataRootPath() { DataRootPath = _xpDataPathService.DefaultRootPath; } private void ExecuteSaveDataRootPath() { try { _xpDataPathService.SaveRootPath(DataRootPath); DataRootPath = _xpDataPathService.RootPath; RaisePropertyChanged(nameof(PlanRootPath)); RaisePropertyChanged(nameof(ToolsRootPath)); RaisePropertyChanged(nameof(ResultsRootPath)); RaisePropertyChanged(nameof(ReportRootPath)); LoadBuiltInInspectionModules(); MessageBox.Show("XP data root saved. New save/load dialogs will use the new path immediately.", "Info", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { _logger.Error(ex, "Failed to save XP data root."); MessageBox.Show($"Failed to save data root: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private bool CanExecuteInsertBuiltInInspectionModule() { return SelectedBuiltInInspectionModule != null; } private async Task ExecuteInsertBuiltInInspectionModuleAsync() { var module = SelectedBuiltInInspectionModule; if (module == null) return; try { ShowCncEditor(); await _cncEditorViewModel.InsertInspectionModuleFromPipelineFileAsync(module.FilePath); } catch (Exception ex) { _logger.Error(ex, "Failed to insert built-in inspection module: {FilePath}", module.FilePath); MessageBox.Show($"Failed to insert built-in inspection module: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private void LoadBuiltInInspectionModules() { BuiltInInspectionModules.Clear(); try { var toolsPath = _xpDataPathService.ToolsPath; if (!Directory.Exists(toolsPath)) { SelectedBuiltInInspectionModule = null; return; } var files = Directory .EnumerateFiles(toolsPath, "*.xpm", SearchOption.AllDirectories) .OrderBy(path => path, StringComparer.OrdinalIgnoreCase) .Select(path => new BuiltInInspectionModuleItem( GetBuiltInModuleDisplayName(toolsPath, path), path)) .ToList(); foreach (var file in files) { BuiltInInspectionModules.Add(file); } SelectedBuiltInInspectionModule = BuiltInInspectionModules.FirstOrDefault(); _logger.Info("Loaded {Count} built-in inspection modules from {ToolsPath}", BuiltInInspectionModules.Count, toolsPath); } catch (Exception ex) { SelectedBuiltInInspectionModule = null; _logger.Error(ex, "Failed to load built-in inspection modules."); } } private static string GetBuiltInModuleDisplayName(string toolsPath, string filePath) { var relativePath = Path.GetRelativePath(toolsPath, filePath); var withoutExtension = Path.ChangeExtension(relativePath, null) ?? relativePath; return withoutExtension.Replace(Path.DirectorySeparatorChar, '/'); } private void ExecuteAxisReset() { var result = MessageBox.Show("Confirm axis reset?", "Axis Reset", MessageBoxButton.OKCancel, MessageBoxImage.Question); if (result != MessageBoxResult.OK) return; try { var motionSystem = _containerProvider.Resolve(); var resetResult = motionSystem.AxisReset.Reset(); if (!resetResult.Success) { MessageBox.Show($"Axis reset failed: {resetResult.ErrorMessage}", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } catch (Exception ex) { _logger.Error(ex, "Axis reset failed."); MessageBox.Show($"Axis reset error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private void ExecuteOpenDoor() { try { var motionService = _containerProvider.Resolve(); var result = motionService.OpenDoor(); if (!result.Success) { MessageBox.Show($"Open door failed: {result.ErrorMessage}", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } catch (Exception ex) { _logger.Error(ex, "Open door failed."); MessageBox.Show($"Open door error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private void ExecuteCloseDoor() { try { var motionService = _containerProvider.Resolve(); var result = motionService.CloseDoor(); if (!result.Success) { MessageBox.Show($"Close door failed: {result.ErrorMessage}", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } catch (Exception ex) { _logger.Error(ex, "Close door failed."); MessageBox.Show($"Close door error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private void ExecuteOpenDetectorConfig() { try { ShowOrActivate(_detectorConfigWindow, w => _detectorConfigWindow = w, () => new XP.Hardware.Detector.Views.DetectorConfigWindow(), "Detector Config"); } catch (Exception ex) { _logger.Error(ex, "Failed to open detector config window."); MessageBox.Show($"Failed to open detector config window:\n{ex.InnerException?.Message ?? ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private void ExecuteOpenMotionDebug() { ShowOrActivate(_motionDebugWindow, w => _motionDebugWindow = w, () => new XP.Hardware.MotionControl.Views.MotionDebugWindow(), "运动调试"); } private void ExecuteOpenPlcAddrConfig() { ShowOrActivate(_plcAddrConfigWindow, w => _plcAddrConfigWindow = w, () => _containerProvider.Resolve(), "PLC 地址配置"); } private void ExecuteOpenRaySourceConfig() { ShowOrActivate(_raySourceConfigWindow, w => _raySourceConfigWindow = w, () => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "Ray Source Config"); } private void ExecuteLoadImage() { var dialog = new OpenFileDialog { Title = "加载图像", Filter = "图像文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff|所有文件|*.*" }; if (dialog.ShowDialog() != true) return; try { var bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.UriSource = new Uri(dialog.FileName, UriKind.Absolute); bitmap.CacheOption = BitmapCacheOption.OnLoad; bitmap.EndInit(); bitmap.Freeze(); _logger.Info("[Image] ExecuteLoadImage loaded image {Path} and will push it to MainViewportService and ManualImageLoadedEvent.", dialog.FileName); _mainViewportService.SetManualImage(bitmap, dialog.FileName); // Publish the image to the pipeline editor at the same time. _eventAggregator.GetEvent() .Publish(new ManualImageLoadedPayload(bitmap, dialog.FileName)); _logger.Info("[Image] ManualImageLoadedEvent published."); RaisePropertyChanged(nameof(IsUsingLiveDetectorSource)); } catch (Exception ex) { _logger.Error(ex, "Failed to load image: {Path}", dialog.FileName); MessageBox.Show($"Failed to load image: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private void ExecuteUseLiveDetectorSource() { _mainViewportService.SetSourceMode(MainViewportSourceMode.LiveDetector); RaisePropertyChanged(nameof(IsUsingLiveDetectorSource)); } private void ExecuteWarmUp() { var messageBoxResult = MessageBox.Show("Confirm X-ray source warm-up?", "Warm-up", MessageBoxButton.OKCancel, MessageBoxImage.Question); if (messageBoxResult != MessageBoxResult.OK) return; try { var raySourceService = _containerProvider.Resolve(); var result = raySourceService.WarmUp(); if (!result.Success) { MessageBox.Show($"Warm-up failed: {result.ErrorMessage}", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } else { _logger.Info("Warm-up command sent."); } } catch (Exception ex) { _logger.Error(ex, "Warm-up failed."); MessageBox.Show($"Warm-up error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } #region 测量命令实现 private bool CheckImageLoaded() { try { var viewportVm = _containerProvider.Resolve(); if (viewportVm?.ImageSource != null) return true; } catch { } HexMessageBox.Show("Please load an image first!", MessageBoxButton.OK, MessageBoxImage.Information); return false; } private void ExecutePointDistanceMeasure() { if (!CheckImageLoaded()) return; _logger.Info("Point distance measurement triggered."); _eventAggregator.GetEvent().Publish(MeasurementToolMode.PointDistance); } private void ExecutePointLineDistanceMeasure() { if (!CheckImageLoaded()) return; _logger.Info("Point-line distance measurement triggered."); _eventAggregator.GetEvent().Publish(MeasurementToolMode.PointLineDistance); } private void ExecuteAngleMeasure() { if (!CheckImageLoaded()) return; _logger.Info("Angle measurement triggered."); _eventAggregator.GetEvent().Publish(MeasurementToolMode.Angle); } private void ExecuteThroughHoleFillRateMeasure() { if (!CheckImageLoaded()) return; _logger.Info("Through-hole fill-rate measurement triggered."); _eventAggregator.GetEvent().Publish(MeasurementToolMode.ThroughHoleFillRate); } private Window _bgaDetectionPanel; private void ExecuteBgaDetection() { if (!CheckImageLoaded()) return; _logger.Info("BGA检测功能已触发"); if (_bgaDetectionPanel != null && _bgaDetectionPanel.IsVisible) { _bgaDetectionPanel.Activate(); return; } _bgaDetectionPanel = new Views.ImageProcessing.BgaDetectionPanel { Owner = System.Windows.Application.Current.MainWindow }; _bgaDetectionPanel.Show(); } private Window _voidDetectionPanel; private void ExecuteVoidDetection() { if (!CheckImageLoaded()) return; _logger.Info("空隙检测功能已触发"); if (_voidDetectionPanel != null && _voidDetectionPanel.IsVisible) { _voidDetectionPanel.Activate(); return; } _voidDetectionPanel = new Views.ImageProcessing.VoidDetectionPanel { Owner = System.Windows.Application.Current.MainWindow }; _voidDetectionPanel.Show(); } private Window _bubbleMeasurePanel; private void ExecuteBubbleMeasure() { if (!CheckImageLoaded()) return; _logger.Info("Bubble measurement triggered."); // Enter bubble measurement mode. _eventAggregator.GetEvent().Publish(MeasurementToolMode.BubbleMeasure); // Open the tool panel. if (_bubbleMeasurePanel != null && _bubbleMeasurePanel.IsVisible) { _bubbleMeasurePanel.Activate(); return; } _bubbleMeasurePanel = new Views.ImageProcessing.BubbleMeasurePanel { Owner = System.Windows.Application.Current.MainWindow }; _bubbleMeasurePanel.Closed += (s, e) => { // Exit bubble measurement mode when the panel closes. _eventAggregator.GetEvent().Publish(MeasurementToolMode.None); }; _bubbleMeasurePanel.Show(); } private void ExecuteWhiteBackgroundDetection() { if (!CheckImageLoaded()) return; _logger.Info("White background detection: entering ROI draw mode."); _eventAggregator.GetEvent().Publish(); StatusMessage = "白底检测:请在图像上拖拽绘制矩形ROI"; } private void OnWhiteBackgroundRoiDrawn(System.Windows.Int32Rect roi) => RunBackgroundRoiDetection(roi, BackgroundDefectMode.WhiteBackground); private void ExecuteBlackBackgroundDetection() { if (!CheckImageLoaded()) return; _logger.Info("Black background detection: entering ROI draw mode."); _eventAggregator.GetEvent().Publish(); StatusMessage = "黑底检测:请在图像上拖拽绘制矩形ROI"; } private void ExecuteOpenTemplateMatchAssistant() { try { if (!CheckImageLoaded()) { StatusMessage = "请先加载图像再使用模板助手。"; return; } if (_templateMatchAssistantWindow != null) { if (_templateMatchAssistantWindow.IsLoaded) { _templateMatchAssistantWindow.Activate(); return; } _templateMatchAssistantWindow = null; } var vm = _containerProvider.Resolve(); var w = new TemplateMatchAssistantWindow { DataContext = vm, Owner = Application.Current?.MainWindow }; w.Closed += (_, _) => { _templateMatchAssistantWindow = null; }; _templateMatchAssistantWindow = w; w.Show(); _logger.Info("Template match assistant opened."); StatusMessage = "已打开模板匹配助手"; } catch (Exception ex) { _logger.Error(ex, "Failed to open template match assistant"); StatusMessage = $"打开模板助手失败: {ex.Message}"; } } private void OnBlackBackgroundRoiDrawn(System.Windows.Int32Rect roi) => RunBackgroundRoiDetection(roi, BackgroundDefectMode.BlackBackground); /// /// 从视口灰度图取 ROI,调用 ,再发布结果事件(全局坐标)。 /// private void RunBackgroundRoiDetection(System.Windows.Int32Rect roi, BackgroundDefectMode mode) { try { var viewportVm = _containerProvider.Resolve(); var imageSource = viewportVm?.ImageSource as System.Windows.Media.Imaging.BitmapSource; if (imageSource == null) return; System.Windows.Media.Imaging.BitmapSource gray8; if (imageSource.Format != System.Windows.Media.PixelFormats.Gray8) gray8 = new System.Windows.Media.Imaging.FormatConvertedBitmap( imageSource, System.Windows.Media.PixelFormats.Gray8, null, 0); else gray8 = imageSource; int imgW = gray8.PixelWidth; int imgH = gray8.PixelHeight; int rx = Math.Clamp(roi.X, 0, imgW - 1); int ry = Math.Clamp(roi.Y, 0, imgH - 1); int rw = Math.Clamp(roi.Width, 1, imgW - rx); int rh = Math.Clamp(roi.Height, 1, imgH - ry); byte[] roiPixels = new byte[rw * rh]; gray8.CopyPixels(new System.Windows.Int32Rect(rx, ry, rw, rh), roiPixels, rw, 0); using var roiImage = new Image(rw, rh); for (int y = 0; y < rh; y++) for (int x = 0; x < rw; x++) roiImage.Data[y, x, 0] = roiPixels[y * rw + x]; const int minArea = 50; const double mmPerPixel = 0.139; var blobs = BackgroundDefectAnalyzer.DetectBlobs(roiImage, mode, minArea, mmPerPixel); var detections = new System.Collections.Generic.List(blobs.Count); foreach (var b in blobs) { var item = new BackgroundDefectDetectionItem { SizeMicrometers = b.MaxChordMicrometers, ChordP1 = new System.Drawing.Point(b.MaxChordEndAInRoi.X + rx, b.MaxChordEndAInRoi.Y + ry), ChordP2 = new System.Drawing.Point(b.MaxChordEndBInRoi.X + rx, b.MaxChordEndBInRoi.Y + ry) }; foreach (var p in b.ContourInRoi) item.Contour.Add(new System.Drawing.Point(p.X + rx, p.Y + ry)); detections.Add(item); } var roiRect = new System.Drawing.Rectangle(rx, ry, rw, rh); if (mode == BackgroundDefectMode.WhiteBackground) { _eventAggregator.GetEvent().Publish( new WhiteBackgroundResultPayload { RoiRect = roiRect, Detections = detections }); StatusMessage = $"白底检测完成:检测到 {detections.Count} 个黑色区域"; _logger.Info("White background detection: found {Count} dark regions in ROI ({X},{Y},{W},{H})", detections.Count, rx, ry, rw, rh); } else { _eventAggregator.GetEvent().Publish( new BlackBackgroundResultPayload { RoiRect = roiRect, Detections = detections }); StatusMessage = $"黑底检测完成:检测到 {detections.Count} 个亮色区域"; _logger.Info("Black background detection: found {Count} bright regions in ROI ({X},{Y},{W},{H})", detections.Count, rx, ry, rw, rh); } } catch (Exception ex) { string label = mode == BackgroundDefectMode.WhiteBackground ? "白底" : "黑底"; _logger.Error(ex, "{Label} background detection failed", label); StatusMessage = $"{label}检测失败: {ex.Message}"; } } private void ExecuteGrayscale() { if (!CheckImageLoaded()) return; _logger.Info("Line profile toggled."); _eventAggregator.GetEvent().Publish(); } private void ExecuteSharpen() { if (!CheckImageLoaded()) return; _logger.Info("Sharpen triggered."); try { var viewportVm = _containerProvider.Resolve(); var imageSource = viewportVm?.ImageSource as BitmapSource; if (imageSource == null) return; var inputImage = BitmapSourceToImage(imageSource); if (inputImage == null) return; var processor = new XP.ImageProcessing.Processors.SharpenProcessor(); processor.SetParameter("Method", "UnsharpMask"); processor.SetParameter("Strength", 0.2); processor.SetParameter("KernelSize", 3); var result = processor.Process(inputImage); var resultBitmap = ImageToBitmapSource(result); _mainViewportService.SetManualImage(resultBitmap, "Sharpen"); inputImage.Dispose(); result.Dispose(); _logger.Info("Sharpen completed."); } catch (Exception ex) { _logger.Error(ex, "Sharpen failed."); HexMessageBox.Show($"Sharpen failed: {ex.Message}", MessageBoxButton.OK, MessageBoxImage.Error); } } private void ExecuteEnhance() { if (!CheckImageLoaded()) return; _logger.Info("Enhance triggered."); try { var viewportVm = _containerProvider.Resolve(); var imageSource = viewportVm?.ImageSource as BitmapSource; if (imageSource == null) return; var inputImage = BitmapSourceToImage(imageSource); if (inputImage == null) return; var processor = new XP.ImageProcessing.Processors.HistogramEqualizationProcessor(); var result = processor.Process(inputImage); var resultBitmap = ImageToBitmapSource(result); _mainViewportService.SetManualImage(resultBitmap, "Enhance"); inputImage.Dispose(); result.Dispose(); _logger.Info("Enhance completed."); } catch (Exception ex) { _logger.Error(ex, "Enhance failed."); HexMessageBox.Show($"Enhance failed: {ex.Message}", MessageBoxButton.OK, MessageBoxImage.Error); } } private Image? BitmapSourceToImage(BitmapSource bitmapSource) { // 转换为可用的图像格式 BitmapSource source = bitmapSource; // 如果不是 Gray8 格式,转换为 Gray8 if (bitmapSource.Format != PixelFormats.Gray8) { source = new FormatConvertedBitmap(bitmapSource, PixelFormats.Gray8, null, 0); } // 获取原始像素数据 int width = source.PixelWidth; int height = source.PixelHeight; int stride = width; // Gray8 每个像素 1 字节 byte[] pixels = new byte[width * height]; source.CopyPixels(pixels, stride, 0); // 创建 Emgu CV Image var image = new Image(width, height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { image.Data[y, x, 0] = pixels[y * stride + x]; } } return image; } private BitmapSource ImageToBitmapSource(Image image) { int width = image.Width; int height = image.Height; int stride = width; byte[] pixels = new byte[width * height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { pixels[y * stride + x] = image.Data[y, x, 0]; } } var bitmapSource = BitmapSource.Create( width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride); bitmapSource.Freeze(); return bitmapSource; } private void ExecuteOpenLanguageSwitcher() { try { var viewModel = _containerProvider.Resolve(); var window = new XP.Common.Localization.Views.LanguageSwitcherWindow(viewModel) { Owner = Application.Current.MainWindow, ShowInTaskbar = true }; window.ShowDialog(); } catch (Exception ex) { _logger.Error(ex, "Failed to open language settings."); } } private void ExecuteOpenRealTimeLogViewer() { ShowOrActivate(_realTimeLogViewerWindow, w => _realTimeLogViewerWindow = w, () => new XP.Common.GeneralForm.Views.RealTimeLogViewer(), "实时日志"); } private void OnNavigateHome() { _logger.Info("Navigated to home."); LicenseInfo = "首页"; } private void OnNavigateInspect() { _logger.Info("Navigated to inspection page."); LicenseInfo = "Inspection"; } private void OnOpenFile() { _logger.Info("Open file."); LicenseInfo = "打开文件"; } private void OnExport() { _logger.Info("Export data."); LicenseInfo = "导出数据"; } private void OnClear() { _logger.Info("Clear data."); LicenseInfo = "清除数据"; } private void OnEditProperties() { _logger.Info("Edit properties."); LicenseInfo = "Edit properties"; } private void OnMainViewportStateChanged(object sender, EventArgs e) { Application.Current?.Dispatcher?.BeginInvoke(new Action(() => { RaisePropertyChanged(nameof(IsMainViewportRealtimeEnabled)); RaisePropertyChanged(nameof(IsUsingLiveDetectorSource)); })); } private void OnPipelinePreviewUpdated(PipelinePreviewUpdatedPayload payload) { if (payload?.Image == null) { _logger.Warn("[Image] OnPipelinePreviewUpdated skipped because payload or image is null."); return; } _logger.Info("[Image] OnPipelinePreviewUpdated received a pipeline preview image and pushed it to MainViewportService."); _mainViewportService.SetManualImage(payload.Image, string.Empty); } public sealed record BuiltInInspectionModuleItem(string DisplayName, string FilePath); #endregion 测量命令实现 } }