Files
XplorePlane/XplorePlane/ViewModels/Main/MainViewModel.cs
T
李伟 e0eec42a2f feat: 模板匹配助手窗口与主视口 ROI 清除逻辑
- 新增模板助手/批量测试窗口、事件与 ViewModel,主窗口入口与 App 注册

- 运行匹配或批量测试前发布清除事件;视口通过 VisualTreeHelper 从父 Panel 移除持久虚线 ROI,避免 FindChild 失败时框残留

- TemplateMatchNative 等相关调整

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 10:42:20 +08:00

1313 lines
54 KiB
C#

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<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 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));
}
}
/// <summary>测量工具是否可用(实时模式关闭时启用)</summary>
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<BuiltInInspectionModuleItem> BuiltInInspectionModules { get; } = new();
public BuiltInInspectionModuleItem SelectedBuiltInInspectionModule
{
get => _selectedBuiltInInspectionModule;
set
{
if (SetProperty(ref _selectedBuiltInInspectionModule, value))
{
InsertBuiltInInspectionModuleCommand?.RaiseCanExecuteChanged();
}
}
}
/// <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 _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<MainViewModel>() ?? 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<CncEditorViewModel>();
_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<PipelinePreviewUpdatedEvent>()
.Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread);
_eventAggregator.GetEvent<WhiteBackgroundRoiDrawnEvent>()
.Subscribe(OnWhiteBackgroundRoiDrawn, ThreadOption.UIThread);
_eventAggregator.GetEvent<BlackBackgroundRoiDrawnEvent>()
.Subscribe(OnBlackBackgroundRoiDrawn, ThreadOption.UIThread);
NavigationTree = new ObservableCollection<object>();
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<ToggleCrosshairEvent>().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<Window> setWindow, Func<Window> 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<CncEditorViewModel> 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<IPdfViewerService>();
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<NavigationPropertyPanelViewModel>();
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<IMotionSystem>();
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<IMotionControlService>();
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<IMotionControlService>();
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<XP.Hardware.PLC.Views.PlcAddrConfigEditorWindow>(), "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<ManualImageLoadedEvent>()
.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<XP.Hardware.RaySource.Services.IRaySourceService>();
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<ViewportPanelViewModel>();
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<MeasurementToolEvent>().Publish(MeasurementToolMode.PointDistance);
}
private void ExecutePointLineDistanceMeasure()
{
if (!CheckImageLoaded()) return;
_logger.Info("Point-line distance measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointLineDistance);
}
private void ExecuteAngleMeasure()
{
if (!CheckImageLoaded()) return;
_logger.Info("Angle measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.Angle);
}
private void ExecuteThroughHoleFillRateMeasure()
{
if (!CheckImageLoaded()) return;
_logger.Info("Through-hole fill-rate measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().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<MeasurementToolEvent>().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<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
};
_bubbleMeasurePanel.Show();
}
private void ExecuteWhiteBackgroundDetection()
{
if (!CheckImageLoaded()) return;
_logger.Info("White background detection: entering ROI draw mode.");
_eventAggregator.GetEvent<WhiteBackgroundDetectionEvent>().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<BlackBackgroundDetectionEvent>().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<TemplateMatchAssistantViewModel>();
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);
/// <summary>
/// 从视口灰度图取 ROI,调用 <see cref="BackgroundDefectAnalyzer"/>,再发布结果事件(全局坐标)。
/// </summary>
private void RunBackgroundRoiDetection(System.Windows.Int32Rect roi, BackgroundDefectMode mode)
{
try
{
var viewportVm = _containerProvider.Resolve<ViewportPanelViewModel>();
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<Gray, byte>(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<BackgroundDefectDetectionItem>(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<WhiteBackgroundResultEvent>().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<BlackBackgroundResultEvent>().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<ToggleLineProfileEvent>().Publish();
}
private void ExecuteSharpen()
{
if (!CheckImageLoaded()) return;
_logger.Info("Sharpen triggered.");
try
{
var viewportVm = _containerProvider.Resolve<ViewportPanelViewModel>();
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<ViewportPanelViewModel>();
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<Gray, byte>? 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<Gray, byte>(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<Gray, byte> 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<XP.Common.Localization.ViewModels.LanguageSwitcherViewModel>();
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
}
}