Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 94924da3a4 | |||
| d608a23eac | |||
| d25ef4e481 | |||
| 5d2a74d64b |
@@ -88,6 +88,11 @@
|
||||
<add key="Detector:InitializationTimeout" value="30000" />
|
||||
<add key="Detector:AcquisitionTimeout" value="10000" />
|
||||
<add key="Detector:CorrectionTimeout" value="60000" />
|
||||
<!-- 主界面实时图像与探测器帧流水线 -->
|
||||
<add key="MainViewport:RealtimeEnabledDefault" value="true" />
|
||||
<add key="DetectorPipeline:AcquireQueueCapacity" value="16" />
|
||||
<add key="DetectorPipeline:ProcessQueueCapacity" value="8" />
|
||||
<add key="DetectorPipeline:ProcessEveryNFrames" value="1" />
|
||||
|
||||
<!-- Dump 配置 | Dump Configuration -->
|
||||
<add key="Dump:StoragePath" value="D:\XplorePlane\Dump" />
|
||||
@@ -164,4 +169,4 @@
|
||||
<!-- 允许捕获非托管异常(如 AccessViolationException)以便生成 Dump | Allow catching unmanaged exceptions (e.g. AccessViolationException) for dump generation -->
|
||||
<legacyCorruptedStateExceptionsPolicy enabled="true" />
|
||||
</runtime>
|
||||
</configuration>
|
||||
</configuration>
|
||||
|
||||
+88
-49
@@ -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
|
||||
/// </summary>
|
||||
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<IDbContext>(); // 从Prism容器获取IDbContext实例(单例)| Get IDbContext instance from Prism container (singleton)
|
||||
var detectorFramePipelineService = bootstrapper.Container.Resolve<IDetectorFramePipelineService>();
|
||||
detectorFramePipelineService?.Dispose();
|
||||
Log.Information("主界面探测器帧流水线资源已释放");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "主界面探测器帧流水线资源释放失败");
|
||||
}
|
||||
|
||||
// 释放SQLite数据库资源 | Release SQLite database resources
|
||||
try
|
||||
{
|
||||
var bootstrapper = AppBootstrapper.Instance;
|
||||
if (bootstrapper != null)
|
||||
{
|
||||
var dbContext = bootstrapper.Container.Resolve<IDbContext>(); // 从 Prism 容器获取 IDbContext 实例(单例)| Get IDbContext instance from Prism container (singleton)
|
||||
dbContext?.Dispose();
|
||||
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
|
||||
/// </summary>
|
||||
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<IDetectorFramePipelineService>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "初始化主界面探测器帧流水线失败");
|
||||
}
|
||||
|
||||
// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误
|
||||
try
|
||||
{
|
||||
var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>();
|
||||
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<ILogger>(Log.Logger);
|
||||
|
||||
// 注册 XP.Common.ILoggerService 适配器
|
||||
containerRegistry.RegisterSingleton<ILoggerService, SerilogLoggerService>();
|
||||
// 注册 XP.Common.ILoggerService 适配器
|
||||
containerRegistry.RegisterSingleton<ILoggerService, SerilogLoggerService>();
|
||||
|
||||
// 注册视图和视图模型
|
||||
containerRegistry.RegisterForNavigation<MainWindow>();
|
||||
// 注册视图和视图模型
|
||||
containerRegistry.RegisterForNavigation<MainWindow>();
|
||||
containerRegistry.Register<MainViewModel>();
|
||||
containerRegistry.RegisterSingleton<NavigationPropertyPanelViewModel>();
|
||||
|
||||
@@ -364,16 +408,7 @@ namespace XplorePlane
|
||||
containerRegistry.RegisterInstance(sqliteConfig);
|
||||
containerRegistry.RegisterSingleton<IDbContext, SqliteContext>();
|
||||
|
||||
// 注册硬件库的 ViewModel(供 ViewModelLocator 自动装配)
|
||||
//containerRegistry.Register<XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel>();
|
||||
// 手动注册射线源模块的所有服务(确保 DryIoc 容器中可用,避免模块加载顺序问题)
|
||||
//var raySourceConfig = XP.Hardware.RaySource.Config.ConfigLoader.LoadConfig();
|
||||
//containerRegistry.RegisterInstance(raySourceConfig);
|
||||
//containerRegistry.RegisterSingleton<XP.Hardware.RaySource.Abstractions.IRaySourceFactory, XP.Hardware.RaySource.Factories.RaySourceFactory>();
|
||||
//containerRegistry.RegisterSingleton<IRaySourceService, XP.Hardware.RaySource.Services.RaySourceService>();
|
||||
//containerRegistry.RegisterSingleton<XP.Hardware.RaySource.Services.IFilamentLifetimeService, XP.Hardware.RaySource.Services.FilamentLifetimeService>();
|
||||
|
||||
// 手动注册通用模块的服务(本地化、Dump)
|
||||
// 注册通用模块的服务(本地化、Dump)
|
||||
containerRegistry.RegisterSingleton<ILocalizationConfig, LocalizationConfig>();
|
||||
containerRegistry.RegisterSingleton<ILocalizationService, ResxLocalizationService>();
|
||||
containerRegistry.RegisterSingleton<DumpConfig>(() => XP.Common.Helpers.ConfigLoader.LoadDumpConfig());
|
||||
@@ -385,6 +420,10 @@ namespace XplorePlane
|
||||
containerRegistry.RegisterSingleton<IMeasurementDataService, MeasurementDataService>();
|
||||
containerRegistry.RegisterSingleton<IInspectionResultStore, InspectionResultStore>();
|
||||
|
||||
// ── 主界面实时图像 / 探测器双队列服务(单例)──
|
||||
containerRegistry.RegisterSingleton<IMainViewportService, MainViewportService>();
|
||||
containerRegistry.RegisterSingleton<IDetectorFramePipelineService, DetectorFramePipelineService>();
|
||||
|
||||
// ── CNC / 矩阵 ViewModel(瞬态)──
|
||||
containerRegistry.Register<CncEditorViewModel>();
|
||||
containerRegistry.Register<MatrixEditorViewModel>();
|
||||
@@ -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<CommonModule>();
|
||||
|
||||
// 注册通用模块(必须最先加载)| Register common module (must be loaded first)
|
||||
moduleCatalog.AddModule<CommonModule>();
|
||||
|
||||
// 注册其他模块 | Register other modules
|
||||
moduleCatalog.AddModule<PLCModule>();
|
||||
moduleCatalog.AddModule<DetectorModule>();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
using System.Windows;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None,
|
||||
ResourceDictionaryLocation.SourceAssembly)]
|
||||
[assembly: InternalsVisibleTo("XplorePlane.Tests")]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace XplorePlane.Services.MainViewport
|
||||
{
|
||||
public sealed class DetectorFrame
|
||||
{
|
||||
public DetectorFrame(
|
||||
long frameId,
|
||||
DateTime captureTime,
|
||||
int width,
|
||||
int height,
|
||||
ushort[] rawPixels,
|
||||
BitmapSource previewImage)
|
||||
{
|
||||
FrameId = frameId;
|
||||
CaptureTime = captureTime;
|
||||
Width = width;
|
||||
Height = height;
|
||||
RawPixels = rawPixels;
|
||||
PreviewImage = previewImage;
|
||||
}
|
||||
|
||||
public long FrameId { get; }
|
||||
public DateTime CaptureTime { get; }
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
public ushort[] RawPixels { get; }
|
||||
public BitmapSource PreviewImage { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using Prism.Events;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Configuration;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Converters;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.Detector.Abstractions;
|
||||
using XP.Hardware.Detector.Abstractions.Events;
|
||||
|
||||
namespace XplorePlane.Services.MainViewport
|
||||
{
|
||||
public sealed class DetectorFramePipelineService : IDetectorFramePipelineService
|
||||
{
|
||||
private readonly ConcurrentQueue<DetectorFrame> _acquireQueue = new();
|
||||
private readonly ConcurrentQueue<DetectorFrame> _processQueue = new();
|
||||
private readonly SemaphoreSlim _processSignal = new(0);
|
||||
private readonly CancellationTokenSource _shutdown = new();
|
||||
private readonly IMainViewportService _mainViewportService;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly Task _processConsumerTask;
|
||||
private int _acquireQueueCount;
|
||||
private int _processQueueCount;
|
||||
private long _receivedFrameCount;
|
||||
private bool _disposed;
|
||||
|
||||
public DetectorFramePipelineService(
|
||||
IEventAggregator eventAggregator,
|
||||
IMainViewportService mainViewportService,
|
||||
ILoggerService logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(eventAggregator);
|
||||
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
|
||||
_logger = logger?.ForModule<DetectorFramePipelineService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
AcquireQueueCapacity = ReadInt("DetectorPipeline:AcquireQueueCapacity", 16, 1);
|
||||
ProcessQueueCapacity = ReadInt("DetectorPipeline:ProcessQueueCapacity", 8, 1);
|
||||
ProcessEveryNFrames = ReadInt("DetectorPipeline:ProcessEveryNFrames", 1, 1);
|
||||
|
||||
eventAggregator.GetEvent<ImageCapturedEvent>()
|
||||
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
|
||||
|
||||
_processConsumerTask = Task.Run(ProcessLoopAsync);
|
||||
}
|
||||
|
||||
public int AcquireQueueCount => Volatile.Read(ref _acquireQueueCount);
|
||||
public int ProcessQueueCount => Volatile.Read(ref _processQueueCount);
|
||||
public int AcquireQueueCapacity { get; }
|
||||
public int ProcessQueueCapacity { get; }
|
||||
public int ProcessEveryNFrames { get; }
|
||||
|
||||
public event EventHandler<DetectorFrame> ProcessFrameDequeued;
|
||||
|
||||
private void OnImageCaptured(ImageCapturedEventArgs args)
|
||||
{
|
||||
if (_disposed || args?.ImageData == null || args.Width <= 0 || args.Height <= 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var rawPixels = new ushort[args.ImageData.Length];
|
||||
Array.Copy(args.ImageData, rawPixels, rawPixels.Length);
|
||||
|
||||
var bitmap = XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource(rawPixels, (int)args.Width, (int)args.Height);
|
||||
bitmap.Freeze();
|
||||
|
||||
var frame = new DetectorFrame(
|
||||
frameId: args.FrameNumber,
|
||||
captureTime: args.CaptureTime,
|
||||
width: (int)args.Width,
|
||||
height: (int)args.Height,
|
||||
rawPixels: rawPixels,
|
||||
previewImage: bitmap);
|
||||
|
||||
EnqueueBounded(_acquireQueue, frame, AcquireQueueCapacity, ref _acquireQueueCount);
|
||||
_mainViewportService.UpdateDetectorFrame(frame);
|
||||
|
||||
var sequence = Interlocked.Increment(ref _receivedFrameCount);
|
||||
if ((sequence - 1) % ProcessEveryNFrames == 0)
|
||||
{
|
||||
EnqueueBounded(_processQueue, frame, ProcessQueueCapacity, ref _processQueueCount);
|
||||
_processSignal.Release();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "探测器帧进入主界面流水线失败");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessLoopAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!_shutdown.IsCancellationRequested)
|
||||
{
|
||||
await _processSignal.WaitAsync(_shutdown.Token).ConfigureAwait(false);
|
||||
|
||||
while (_processQueue.TryDequeue(out var frame))
|
||||
{
|
||||
Interlocked.Decrement(ref _processQueueCount);
|
||||
ProcessFrameDequeued?.Invoke(this, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "探测器处理队列后台消费者异常退出");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnqueueBounded(
|
||||
ConcurrentQueue<DetectorFrame> queue,
|
||||
DetectorFrame frame,
|
||||
int capacity,
|
||||
ref int queueCount)
|
||||
{
|
||||
queue.Enqueue(frame);
|
||||
var count = Interlocked.Increment(ref queueCount);
|
||||
|
||||
while (count > capacity && queue.TryDequeue(out _))
|
||||
{
|
||||
count = Interlocked.Decrement(ref queueCount);
|
||||
}
|
||||
}
|
||||
|
||||
private static int ReadInt(string key, int defaultValue, int minValue)
|
||||
{
|
||||
var raw = ConfigurationManager.AppSettings[key];
|
||||
if (!int.TryParse(raw, out var parsed))
|
||||
return defaultValue;
|
||||
|
||||
return parsed < minValue ? minValue : parsed;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
_shutdown.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
_processConsumerTask.Wait(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_processSignal.Dispose();
|
||||
_shutdown.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace XplorePlane.Services.MainViewport
|
||||
{
|
||||
public interface IDetectorFramePipelineService : IDisposable
|
||||
{
|
||||
int AcquireQueueCount { get; }
|
||||
int ProcessQueueCount { get; }
|
||||
int AcquireQueueCapacity { get; }
|
||||
int ProcessQueueCapacity { get; }
|
||||
int ProcessEveryNFrames { get; }
|
||||
|
||||
event EventHandler<DetectorFrame> ProcessFrameDequeued;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace XplorePlane.Services.MainViewport
|
||||
{
|
||||
public interface IMainViewportService
|
||||
{
|
||||
MainViewportSourceMode CurrentSourceMode { get; }
|
||||
bool IsRealtimeDisplayEnabled { get; }
|
||||
ImageSource CurrentDisplayImage { get; }
|
||||
string CurrentDisplayInfo { get; }
|
||||
ImageSource LatestDetectorImage { get; }
|
||||
ImageSource LatestManualImage { get; }
|
||||
|
||||
event EventHandler StateChanged;
|
||||
|
||||
void SetRealtimeDisplayEnabled(bool isEnabled);
|
||||
|
||||
void SetSourceMode(MainViewportSourceMode sourceMode);
|
||||
|
||||
void UpdateDetectorFrame(DetectorFrame frame);
|
||||
|
||||
void SetManualImage(ImageSource image, string filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
using System;
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
using System.Windows.Media;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XplorePlane.Services.MainViewport
|
||||
{
|
||||
public sealed class MainViewportService : IMainViewportService
|
||||
{
|
||||
private readonly object _syncRoot = new();
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
private MainViewportSourceMode _currentSourceMode = MainViewportSourceMode.LiveDetector;
|
||||
private bool _isRealtimeDisplayEnabled;
|
||||
private ImageSource _currentDisplayImage;
|
||||
private string _currentDisplayInfo = "等待探测器图像...";
|
||||
private ImageSource _latestDetectorImage;
|
||||
private string _latestDetectorInfo = "等待探测器图像...";
|
||||
private ImageSource _latestManualImage;
|
||||
private string _latestManualInfo = "未加载手动图像";
|
||||
|
||||
public MainViewportService(ILoggerService logger)
|
||||
{
|
||||
_logger = logger?.ForModule<MainViewportService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_isRealtimeDisplayEnabled = ReadBoolean("MainViewport:RealtimeEnabledDefault", true);
|
||||
}
|
||||
|
||||
public MainViewportSourceMode CurrentSourceMode
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _currentSourceMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRealtimeDisplayEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _isRealtimeDisplayEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ImageSource CurrentDisplayImage
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _currentDisplayImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string CurrentDisplayInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _currentDisplayInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ImageSource LatestDetectorImage
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _latestDetectorImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ImageSource LatestManualImage
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _latestManualImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler StateChanged;
|
||||
|
||||
public void SetRealtimeDisplayEnabled(bool isEnabled)
|
||||
{
|
||||
bool changed;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
changed = _isRealtimeDisplayEnabled != isEnabled;
|
||||
_isRealtimeDisplayEnabled = isEnabled;
|
||||
|
||||
if (_currentSourceMode == MainViewportSourceMode.LiveDetector && isEnabled)
|
||||
{
|
||||
ApplyLiveDetectorDisplay_NoLock();
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
return;
|
||||
|
||||
_logger.Info("主界面实时刷新已{State}", isEnabled ? "开启" : "关闭");
|
||||
RaiseStateChanged();
|
||||
}
|
||||
|
||||
public void SetSourceMode(MainViewportSourceMode sourceMode)
|
||||
{
|
||||
bool changed;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
changed = _currentSourceMode != sourceMode;
|
||||
_currentSourceMode = sourceMode;
|
||||
|
||||
switch (sourceMode)
|
||||
{
|
||||
case MainViewportSourceMode.LiveDetector:
|
||||
ApplyLiveDetectorDisplay_NoLock();
|
||||
break;
|
||||
case MainViewportSourceMode.ManualImage:
|
||||
ApplyManualDisplay_NoLock();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
return;
|
||||
|
||||
_logger.Info("主界面图像来源已切换为 {Mode}", sourceMode);
|
||||
RaiseStateChanged();
|
||||
}
|
||||
|
||||
public void UpdateDetectorFrame(DetectorFrame frame)
|
||||
{
|
||||
if (frame == null)
|
||||
return;
|
||||
|
||||
bool shouldRaise = false;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_latestDetectorImage = frame.PreviewImage;
|
||||
_latestDetectorInfo = $"实时探测器图像 {frame.Width}x{frame.Height} 帧#{frame.FrameId} {frame.CaptureTime:HH:mm:ss.fff}";
|
||||
|
||||
if (_currentSourceMode == MainViewportSourceMode.LiveDetector && _isRealtimeDisplayEnabled)
|
||||
{
|
||||
_currentDisplayImage = _latestDetectorImage;
|
||||
_currentDisplayInfo = _latestDetectorInfo;
|
||||
shouldRaise = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRaise)
|
||||
RaiseStateChanged();
|
||||
}
|
||||
|
||||
public void SetManualImage(ImageSource image, string filePath)
|
||||
{
|
||||
if (image == null)
|
||||
return;
|
||||
|
||||
var fileName = string.IsNullOrWhiteSpace(filePath) ? "未命名图像" : Path.GetFileName(filePath);
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_latestManualImage = image;
|
||||
_latestManualInfo = $"手动加载图像 {fileName}";
|
||||
_currentSourceMode = MainViewportSourceMode.ManualImage;
|
||||
_currentDisplayImage = _latestManualImage;
|
||||
_currentDisplayInfo = _latestManualInfo;
|
||||
}
|
||||
|
||||
_logger.Info("主界面已加载手动图像 {FileName}", fileName);
|
||||
RaiseStateChanged();
|
||||
}
|
||||
|
||||
private void ApplyLiveDetectorDisplay_NoLock()
|
||||
{
|
||||
_currentDisplayImage = _latestDetectorImage;
|
||||
_currentDisplayInfo = _latestDetectorImage == null
|
||||
? "等待探测器图像..."
|
||||
: _latestDetectorInfo;
|
||||
}
|
||||
|
||||
private void ApplyManualDisplay_NoLock()
|
||||
{
|
||||
_currentDisplayImage = _latestManualImage;
|
||||
_currentDisplayInfo = _latestManualImage == null
|
||||
? "未加载手动图像"
|
||||
: _latestManualInfo;
|
||||
}
|
||||
|
||||
private void RaiseStateChanged()
|
||||
{
|
||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private static bool ReadBoolean(string key, bool defaultValue)
|
||||
{
|
||||
var raw = ConfigurationManager.AppSettings[key];
|
||||
return bool.TryParse(raw, out var parsed) ? parsed : defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace XplorePlane.Services.MainViewport
|
||||
{
|
||||
public enum MainViewportSourceMode
|
||||
{
|
||||
LiveDetector = 0,
|
||||
ManualImage = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:XplorePlane.Controls">
|
||||
|
||||
<Style TargetType="{x:Type controls:AnimatedSwitch}">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="ClickMode" Value="Press" />
|
||||
<Setter Property="IsThreeState" Value="False" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type controls:AnimatedSwitch}">
|
||||
<Grid
|
||||
Width="{TemplateBinding SwitchWidth}"
|
||||
Height="{TemplateBinding SwitchHeight}"
|
||||
Background="Transparent"
|
||||
SnapsToDevicePixels="True">
|
||||
<Border
|
||||
x:Name="Track"
|
||||
Background="#D5DCE3"
|
||||
BorderBrush="#C9D1D8"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12" />
|
||||
|
||||
<Border
|
||||
x:Name="Thumb"
|
||||
Width="18"
|
||||
Height="18"
|
||||
Margin="3"
|
||||
HorizontalAlignment="Left"
|
||||
Background="{TemplateBinding ThumbBrush}"
|
||||
CornerRadius="9">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect
|
||||
BlurRadius="10"
|
||||
Direction="270"
|
||||
Opacity="0.2"
|
||||
ShadowDepth="1" />
|
||||
</Border.Effect>
|
||||
<Border.RenderTransform>
|
||||
<TranslateTransform x:Name="ThumbTranslate" X="0" />
|
||||
</Border.RenderTransform>
|
||||
</Border>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="ThumbTranslate"
|
||||
Storyboard.TargetProperty="X"
|
||||
To="20"
|
||||
Duration="0:0:0.18">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<ColorAnimation
|
||||
Storyboard.TargetName="Track"
|
||||
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
|
||||
To="#2DCC70"
|
||||
Duration="0:0:0.18">
|
||||
<ColorAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseInOut" />
|
||||
</ColorAnimation.EasingFunction>
|
||||
</ColorAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsChecked" Value="False">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="ThumbTranslate"
|
||||
Storyboard.TargetProperty="X"
|
||||
To="0"
|
||||
Duration="0:0:0.18">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<ColorAnimation
|
||||
Storyboard.TargetName="Track"
|
||||
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
|
||||
To="#D5DCE3"
|
||||
Duration="0:0:0.18">
|
||||
<ColorAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseInOut" />
|
||||
</ColorAnimation.EasingFunction>
|
||||
</ColorAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Track" Property="Opacity" Value="0.55" />
|
||||
<Setter TargetName="Thumb" Property="Opacity" Value="0.8" />
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Thumb" Property="Effect">
|
||||
<Setter.Value>
|
||||
<DropShadowEffect
|
||||
BlurRadius="12"
|
||||
Direction="270"
|
||||
Opacity="0.24"
|
||||
ShadowDepth="1" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -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<object> NavigationTree { get; set; }
|
||||
|
||||
// 导航命令
|
||||
public DelegateCommand NavigateHomeCommand { get; set; }
|
||||
public DelegateCommand NavigateInspectCommand { get; set; }
|
||||
public DelegateCommand OpenFileCommand { get; set; }
|
||||
public DelegateCommand ExportCommand { get; set; }
|
||||
public DelegateCommand ClearCommand { get; set; }
|
||||
public DelegateCommand EditPropertiesCommand { get; set; }
|
||||
|
||||
// 窗口打开命令
|
||||
public DelegateCommand OpenImageProcessingCommand { get; }
|
||||
public DelegateCommand LoadImageCommand { get; }
|
||||
public DelegateCommand OpenPipelineEditorCommand { get; }
|
||||
public DelegateCommand OpenCncEditorCommand { get; }
|
||||
public DelegateCommand OpenMatrixEditorCommand { get; }
|
||||
public DelegateCommand OpenToolboxCommand { get; }
|
||||
public DelegateCommand OpenLibraryVersionsCommand { get; }
|
||||
public DelegateCommand OpenUserManualCommand { get; }
|
||||
public DelegateCommand OpenCameraSettingsCommand { get; }
|
||||
public DelegateCommand NewCncProgramCommand { get; }
|
||||
public DelegateCommand SaveCncProgramCommand { get; }
|
||||
public DelegateCommand LoadCncProgramCommand { get; }
|
||||
public DelegateCommand InsertReferencePointCommand { get; }
|
||||
public DelegateCommand InsertSavePositionCommand { get; }
|
||||
public DelegateCommand InsertCompleteProgramCommand { get; }
|
||||
public DelegateCommand InsertInspectionMarkerCommand { get; }
|
||||
public DelegateCommand InsertInspectionModuleCommand { get; }
|
||||
public DelegateCommand InsertSaveNodeCommand { get; }
|
||||
public DelegateCommand InsertPauseDialogCommand { get; }
|
||||
public DelegateCommand InsertWaitDelayCommand { get; }
|
||||
|
||||
// 硬件命令
|
||||
public DelegateCommand AxisResetCommand { get; }
|
||||
public DelegateCommand OpenDetectorConfigCommand { get; }
|
||||
public DelegateCommand OpenMotionDebugCommand { get; }
|
||||
public DelegateCommand OpenPlcAddrConfigCommand { get; }
|
||||
public DelegateCommand OpenRaySourceConfigCommand { get; }
|
||||
public DelegateCommand WarmUpCommand { get; }
|
||||
|
||||
// 测量命令
|
||||
public DelegateCommand 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; }
|
||||
|
||||
/// <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;
|
||||
@@ -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<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));
|
||||
_cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>();
|
||||
_cncPageView = new CncPageView { DataContext = _cncEditorViewModel };
|
||||
|
||||
_mainViewportService.StateChanged += OnMainViewportStateChanged;
|
||||
|
||||
NavigationTree = new ObservableCollection<object>();
|
||||
|
||||
// 导航命令
|
||||
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<ToggleCrosshairEvent>().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<object> NavigationTree { get; set; }
|
||||
|
||||
public DelegateCommand NavigateHomeCommand { get; set; }
|
||||
public DelegateCommand NavigateInspectCommand { get; set; }
|
||||
public DelegateCommand OpenFileCommand { get; set; }
|
||||
public DelegateCommand ExportCommand { get; set; }
|
||||
public DelegateCommand ClearCommand { get; set; }
|
||||
public DelegateCommand EditPropertiesCommand { get; set; }
|
||||
|
||||
public DelegateCommand OpenImageProcessingCommand { get; }
|
||||
public DelegateCommand LoadImageCommand { get; }
|
||||
public DelegateCommand OpenPipelineEditorCommand { get; }
|
||||
public DelegateCommand OpenCncEditorCommand { get; }
|
||||
public DelegateCommand OpenMatrixEditorCommand { get; }
|
||||
public DelegateCommand OpenToolboxCommand { get; }
|
||||
public DelegateCommand OpenLibraryVersionsCommand { get; }
|
||||
public DelegateCommand OpenUserManualCommand { get; }
|
||||
public DelegateCommand OpenCameraSettingsCommand { get; }
|
||||
public DelegateCommand NewCncProgramCommand { get; }
|
||||
public DelegateCommand SaveCncProgramCommand { get; }
|
||||
public DelegateCommand LoadCncProgramCommand { get; }
|
||||
public DelegateCommand InsertReferencePointCommand { get; }
|
||||
public DelegateCommand InsertSavePositionCommand { get; }
|
||||
public DelegateCommand InsertCompleteProgramCommand { get; }
|
||||
public DelegateCommand InsertInspectionMarkerCommand { get; }
|
||||
public DelegateCommand InsertInspectionModuleCommand { get; }
|
||||
public DelegateCommand InsertSaveNodeCommand { get; }
|
||||
public DelegateCommand InsertPauseDialogCommand { get; }
|
||||
public DelegateCommand InsertWaitDelayCommand { get; }
|
||||
|
||||
public DelegateCommand AxisResetCommand { get; }
|
||||
public DelegateCommand OpenDetectorConfigCommand { get; }
|
||||
public DelegateCommand OpenMotionDebugCommand { get; }
|
||||
public DelegateCommand OpenPlcAddrConfigCommand { get; }
|
||||
public DelegateCommand OpenRaySourceConfigCommand { get; }
|
||||
public DelegateCommand WarmUpCommand { get; }
|
||||
|
||||
public DelegateCommand 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示一个新窗口(非模态)
|
||||
/// </summary>
|
||||
private void ShowWindow(Window window, string name)
|
||||
{
|
||||
window.Owner = Application.Current.MainWindow;
|
||||
@@ -202,9 +206,6 @@ namespace XplorePlane.ViewModels
|
||||
_logger.Info("{Name} 窗口已打开", name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示或激活单例窗口(非模态,防止重复打开)
|
||||
/// </summary>
|
||||
private void ShowOrActivate(Window currentWindow, Action<Window> setWindow, Func<Window> 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<CncEditorViewModel> 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<ManualImageLoadedEvent>()
|
||||
.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<MeasurementToolEvent>().Publish(MeasurementToolMode.Angle);
|
||||
}
|
||||
|
||||
private void ExecuteThroughHoleFillRateMeasure()
|
||||
{
|
||||
_logger.Info("通孔填锡率测量功能已触发");
|
||||
// TODO: 实现通孔填锡率测量逻辑
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().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));
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 实时图像 ViewModel,订阅探测器采集事件并显示图像
|
||||
/// 主界面实时图像 ViewModel,统一承接显示状态和测量状态。
|
||||
/// </summary>
|
||||
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<ViewportPanelViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
CancelMeasurementCommand = new DelegateCommand(ExecuteCancelMeasurement);
|
||||
|
||||
_mainViewportService.StateChanged += OnMainViewportStateChanged;
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>()
|
||||
.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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 由 View 层调用:用户在画布上点击了一个点(像素坐标)
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消/重置当前测量
|
||||
/// </summary>
|
||||
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<ViewportPanelViewModel>();
|
||||
_eventAggregator = eventAggregator;
|
||||
|
||||
CancelMeasurementCommand = new DelegateCommand(() =>
|
||||
{
|
||||
// 发布 None 事件,让 View 层也收到
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
|
||||
ImageInfo = "测量已取消";
|
||||
});
|
||||
|
||||
eventAggregator.GetEvent<ImageCapturedEvent>()
|
||||
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
|
||||
eventAggregator.GetEvent<ManualImageLoadedEvent>()
|
||||
.Subscribe(OnManualImageLoaded, ThreadOption.UIThread);
|
||||
eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
|
||||
.Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread);
|
||||
|
||||
// 订阅测量工具事件
|
||||
eventAggregator.GetEvent<MeasurementToolEvent>()
|
||||
.Subscribe(OnMeasurementToolActivated, ThreadOption.UIThread);
|
||||
|
||||
// 订阅十字辅助线切换事件
|
||||
eventAggregator.GetEvent<ToggleCrosshairEvent>()
|
||||
.Subscribe(OnToggleCrosshair, ThreadOption.UIThread);
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 16 位灰度数据线性拉伸为 8 位 BitmapSource(委托给 XP.Common 通用转换器)
|
||||
/// </summary>
|
||||
private static BitmapSource ConvertToBitmapSource(ushort[] data, int width, int height)
|
||||
{
|
||||
return XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource(data, width, height);
|
||||
if (updateInfo)
|
||||
{
|
||||
ImageInfo = _mainViewportService.CurrentDisplayInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
|
||||
<!-- 实时控制: Live / Snap / 加载 / 保存 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="新建CNC"
|
||||
@@ -79,8 +79,8 @@
|
||||
SmallImage="/Assets/Icons/new-doc.png"
|
||||
Text="新建CNC" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="保存当前X射线实时图像"
|
||||
telerik:ScreenTip.Title="保存图像"
|
||||
telerik:ScreenTip.Description="保存当前 CNC 配置"
|
||||
telerik:ScreenTip.Title="保存"
|
||||
Size="Medium"
|
||||
Command="{Binding SaveCncProgramCommand}"
|
||||
SmallImage="/Assets/Icons/save.png"
|
||||
@@ -103,11 +103,7 @@
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<telerik:RadRibbonGroup Header="程序">
|
||||
|
||||
<!-- 安全门控 & 系统 -->
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="运行"
|
||||
Size="Large"
|
||||
@@ -115,7 +111,6 @@
|
||||
Text="运行" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="停止"
|
||||
telerik:ScreenTip.Title="停止"
|
||||
@@ -123,6 +118,22 @@
|
||||
SmallImage="/Assets/Icons/stop.png"
|
||||
Text="停止" />
|
||||
</StackPanel>
|
||||
<StackPanel Width="52">
|
||||
<controls:AnimatedSwitch
|
||||
Width="44"
|
||||
Height="24"
|
||||
Margin="4,10,4,4"
|
||||
HorizontalAlignment="Center"
|
||||
ToolTip="主界面实时"
|
||||
IsChecked="True"
|
||||
SwitchWidth="44"
|
||||
SwitchHeight="24" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="11"
|
||||
Text="实时" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="开门"
|
||||
@@ -136,13 +147,13 @@
|
||||
Text="关门" />
|
||||
</StackPanel>
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<telerik:RadRibbonGroup Header="快捷工具">
|
||||
<telerik:RadRibbonButton
|
||||
Command="{Binding LoadImageCommand}"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/open.png"
|
||||
Text="加载图像" />
|
||||
<!-- 快捷工具: 上下两列,带文字 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="中心十字线"
|
||||
@@ -151,19 +162,19 @@
|
||||
SmallImage="/Assets/Icons/crosshair.png"
|
||||
Text="辅助线" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="白背景检测黑区域"
|
||||
telerik:ScreenTip.Title="白底检测"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/film-darken.png"
|
||||
Text="白底检测" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="行灰度分布"
|
||||
telerik:ScreenTip.Title="灰度"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/film-darken.png"
|
||||
Text="灰度" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="黑背景检测白区域"
|
||||
telerik:ScreenTip.Title="黑底检测"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/film-darken.png"
|
||||
Text="黑底检测" />
|
||||
@@ -182,89 +193,51 @@
|
||||
</StackPanel>
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<telerik:RadRibbonGroup
|
||||
telerik:ScreenTip.Description="Show the Alignment tab of the Format Cells dialog box."
|
||||
telerik:ScreenTip.Title="Format Cells: Alignment"
|
||||
DialogLauncherCommand="{Binding Path=ShowFormatCellsDialog.Command}"
|
||||
DialogLauncherCommandParameter="Alignment"
|
||||
DialogLauncherVisibility="{Binding Path=ShowFormatCellsDialog.IsEnabled, Converter={StaticResource BoolToVisibilityValueConverter}}"
|
||||
Header="硬件"
|
||||
IsEnabled="{Binding Path=AlignmentGroup.IsEnabled}">
|
||||
|
||||
|
||||
<telerik:RadRibbonGroup Header="测量工具">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
<telerik:RadRibbonGroup.Resources>
|
||||
<spreadsheetControls:RadHorizontalAlignmentToBooleanConverter x:Key="horizontalAlignmentToBooleanConverter" />
|
||||
<spreadsheetControls:RadVerticalAlignmentToBooleanConverter x:Key="verticalAlignmentToBooleanConverter" />
|
||||
</telerik:RadRibbonGroup.Resources>
|
||||
|
||||
<!-- 第一列: 暖机 + 轴复位 上下排列 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonToggleButton
|
||||
telerik:ScreenTip.Description="暖机"
|
||||
telerik:ScreenTip.Title="暖机"
|
||||
Command="{Binding WarmUpCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/heat-engine.png"
|
||||
Text="暖机" />
|
||||
<telerik:RadRibbonToggleButton
|
||||
x:Name="MergeAndCenterButton"
|
||||
telerik:ScreenTip.Description="轴复位"
|
||||
telerik:ScreenTip.Title="轴复位"
|
||||
Command="{Binding AxisResetCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/home.png"
|
||||
Text="轴复位" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 第二列: 射线源 + 探测器 + 运动控制 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonToggleButton
|
||||
x:Name="MergeAndCenterButton1"
|
||||
telerik:ScreenTip.Description="射线源控制"
|
||||
telerik:ScreenTip.Title="射线源"
|
||||
Command="{Binding OpenRaySourceConfigCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/xray.png"
|
||||
Text="射线源" />
|
||||
<telerik:RadRibbonToggleButton
|
||||
x:Name="MergeAndCenterButton2"
|
||||
telerik:ScreenTip.Description="探测器控制"
|
||||
telerik:ScreenTip.Title="探测器"
|
||||
Command="{Binding OpenDetectorConfigCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/detector2.png"
|
||||
Text="探测器" />
|
||||
<telerik:RadRibbonToggleButton
|
||||
x:Name="MergeAndCenterButton3"
|
||||
telerik:ScreenTip.Description="运动控制"
|
||||
telerik:ScreenTip.Title="运动控制"
|
||||
Command="{Binding OpenMotionDebugCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/xyz.png"
|
||||
Text="运动控制" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 第三列: 相机设置 / PLC 地址配置 -->
|
||||
<!-- 第一列: 点点距 + 点线距 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开相机参数设置对话框"
|
||||
telerik:ScreenTip.Title="相机设置"
|
||||
Command="{Binding OpenCameraSettingsCommand}"
|
||||
telerik:ScreenTip.Description="测量两点之间的距离"
|
||||
telerik:ScreenTip.Title="点点距测量"
|
||||
Command="{Binding PointDistanceMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/detector2.png"
|
||||
Text="相机设置" />
|
||||
SmallImage="/Assets/Icons/crosshair.png"
|
||||
Text="点点距测量" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开 PLC 信号地址定义编辑器"
|
||||
telerik:ScreenTip.Title="PLC 地址配置"
|
||||
Command="{Binding OpenPlcAddrConfigCommand}"
|
||||
telerik:ScreenTip.Description="测量点到直线的距离"
|
||||
telerik:ScreenTip.Title="点线距测量"
|
||||
Command="{Binding PointLineDistanceMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/tools.png"
|
||||
Text="PLC 地址" />
|
||||
SmallImage="/Assets/Icons/mark.png"
|
||||
Text="点线距测量" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 第二列: 角度 + 通孔填锡率 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量两条线之间的角度"
|
||||
telerik:ScreenTip.Title="角度测量"
|
||||
Command="{Binding AngleMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/dynamic-range.png"
|
||||
Text="角度测量" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量通孔填锡率"
|
||||
telerik:ScreenTip.Title="通孔填锡率测量"
|
||||
Command="{Binding ThroughHoleFillRateMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/pores.png"
|
||||
Text="通孔填锡率" />
|
||||
</StackPanel>
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<telerik:RadRibbonGroup telerik:ScreenTip.Title="图像算子" Header="图像算子">
|
||||
<telerik:RadRibbonGroup Header="图像算子" telerik:ScreenTip.Title="图像算子">
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开算子工具箱,拖拽算子到流水线中"
|
||||
telerik:ScreenTip.Title="算子工具箱"
|
||||
@@ -279,7 +252,6 @@
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
|
||||
<!-- CNC 编辑器入口按钮 -->
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开 CNC 编辑器窗口,创建和编辑检测配方程序"
|
||||
telerik:ScreenTip.Title="CNC 编辑器"
|
||||
@@ -288,9 +260,6 @@
|
||||
SmallImage="/Assets/Icons/cnc.png"
|
||||
Text="CNC 编辑" />
|
||||
|
||||
|
||||
|
||||
<!-- CNC 节点快捷工具 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="参考点"
|
||||
@@ -346,15 +315,13 @@
|
||||
Text="插入等待" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 矩阵编排入口按钮 -->
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
|
||||
telerik:ScreenTip.Title="矩阵编排"
|
||||
Command="{Binding OpenMatrixEditorCommand}"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/matrix.png"
|
||||
Text="矩阵编排" />
|
||||
|
||||
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
|
||||
telerik:ScreenTip.Title="矩阵编排"
|
||||
Command="{Binding OpenMatrixEditorCommand}"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/matrix.png"
|
||||
Text="矩阵编排" />
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}">
|
||||
@@ -402,100 +369,122 @@
|
||||
SmallImage="/Assets/Icons/spiral.png" />
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<telerik:RadRibbonGroup Header="测量工具">
|
||||
</telerik:RadRibbonTab>
|
||||
<telerik:RadRibbonTab Header="设置">
|
||||
<telerik:RadRibbonGroup
|
||||
telerik:ScreenTip.Description="Show the Alignment tab of the Format Cells dialog box."
|
||||
telerik:ScreenTip.Title="Format Cells: Alignment"
|
||||
DialogLauncherCommand="{Binding Path=ShowFormatCellsDialog.Command}"
|
||||
DialogLauncherCommandParameter="Alignment"
|
||||
DialogLauncherVisibility="{Binding Path=ShowFormatCellsDialog.IsEnabled, Converter={StaticResource BoolToVisibilityValueConverter}}"
|
||||
Header="硬件"
|
||||
IsEnabled="{Binding Path=AlignmentGroup.IsEnabled}">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
<telerik:RadRibbonGroup.Resources>
|
||||
<spreadsheetControls:RadHorizontalAlignmentToBooleanConverter x:Key="horizontalAlignmentToBooleanConverter" />
|
||||
<spreadsheetControls:RadVerticalAlignmentToBooleanConverter x:Key="verticalAlignmentToBooleanConverter" />
|
||||
</telerik:RadRibbonGroup.Resources>
|
||||
|
||||
<!-- 第一列: 点点距 + 点线距 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量两点之间的距离"
|
||||
telerik:ScreenTip.Title="点点距测量"
|
||||
Command="{Binding PointDistanceMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/crosshair.png"
|
||||
Text="点点距测量" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量点到直线的距离"
|
||||
telerik:ScreenTip.Title="点线距测量"
|
||||
Command="{Binding PointLineDistanceMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/mark.png"
|
||||
Text="点线距测量" />
|
||||
<telerik:RadRibbonToggleButton
|
||||
telerik:ScreenTip.Description="暖机"
|
||||
telerik:ScreenTip.Title="暖机"
|
||||
Command="{Binding WarmUpCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/heat-engine.png"
|
||||
Text="暖机" />
|
||||
<telerik:RadRibbonToggleButton
|
||||
telerik:ScreenTip.Description="轴复位"
|
||||
telerik:ScreenTip.Title="轴复位"
|
||||
Command="{Binding AxisResetCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/home.png"
|
||||
Text="轴复位" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonToggleButton
|
||||
telerik:ScreenTip.Description="射线源控制"
|
||||
telerik:ScreenTip.Title="射线源"
|
||||
Command="{Binding OpenRaySourceConfigCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/xray.png"
|
||||
Text="射线源" />
|
||||
<telerik:RadRibbonToggleButton
|
||||
telerik:ScreenTip.Description="探测器控制"
|
||||
telerik:ScreenTip.Title="探测器"
|
||||
Command="{Binding OpenDetectorConfigCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/detector2.png"
|
||||
Text="探测器" />
|
||||
<telerik:RadRibbonToggleButton
|
||||
telerik:ScreenTip.Description="运动控制"
|
||||
telerik:ScreenTip.Title="运动控制"
|
||||
Command="{Binding OpenMotionDebugCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/xyz.png"
|
||||
Text="运动控制" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 第二列: 角度 + 通孔填锡率 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量两条线之间的角度"
|
||||
telerik:ScreenTip.Title="角度测量"
|
||||
Command="{Binding AngleMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/dynamic-range.png"
|
||||
Text="角度测量" />
|
||||
telerik:ScreenTip.Description="打开相机参数设置窗口"
|
||||
telerik:ScreenTip.Title="相机设置"
|
||||
Command="{Binding OpenCameraSettingsCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/detector2.png"
|
||||
Text="相机设置" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量通孔填锡率"
|
||||
telerik:ScreenTip.Title="通孔填锡率测量"
|
||||
Command="{Binding ThroughHoleFillRateMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/pores.png"
|
||||
Text="通孔填锡率" />
|
||||
telerik:ScreenTip.Description="打开 PLC 地址配置窗口"
|
||||
telerik:ScreenTip.Title="PLC 地址配置"
|
||||
Command="{Binding OpenPlcAddrConfigCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/tools.png"
|
||||
Text="PLC 地址" />
|
||||
</StackPanel>
|
||||
</telerik:RadRibbonGroup>
|
||||
<!--
|
||||
<telerik:RadRibbonGroup Header="图像处理">
|
||||
<telerik:RadRibbonGroup Header="多语言">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
<telerik:RadRibbonButton
|
||||
Command="{Binding OpenPipelineEditorCommand}"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/workflow.png"
|
||||
Text="流水线编辑器" />
|
||||
telerik:ScreenTip.Description="切换应用程序显示语言"
|
||||
telerik:ScreenTip.Title="多语言设置"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/tools.png"
|
||||
Command="{Binding OpenLanguageSwitcherCommand}"
|
||||
Text="多语言设置" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开实时日志查看器"
|
||||
telerik:ScreenTip.Title="查看日志"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/message.png"
|
||||
Command="{Binding OpenRealTimeLogViewerCommand}"
|
||||
Text="查看日志" />
|
||||
</telerik:RadRibbonGroup>
|
||||
-->
|
||||
</telerik:RadRibbonTab>
|
||||
<telerik:RadRibbonTab Header="关于">
|
||||
<telerik:RadRibbonGroup Header="关于">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
|
||||
|
||||
<telerik:RadRibbonButton
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/tools.png"
|
||||
Command="{Binding OpenLibraryVersionsCommand}"
|
||||
Text="关于 XplorePlane" />
|
||||
</telerik:RadRibbonGroup>
|
||||
<telerik:RadRibbonGroup Header="帮助">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
<telerik:RadRibbonButton
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/message.png"
|
||||
Command="{Binding OpenUserManualCommand}"
|
||||
Text="帮助文档" />
|
||||
</telerik:RadRibbonGroup>
|
||||
<telerik:RadRibbonGroup Header="设置">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="切换应用程序显示语言"
|
||||
telerik:ScreenTip.Title="多语言设置"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/tools.png"
|
||||
Command="{Binding OpenLanguageSwitcherCommand}"
|
||||
Text="多语言设置" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开实时日志查看器"
|
||||
telerik:ScreenTip.Title="查看日志"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/message.png"
|
||||
Command="{Binding OpenRealTimeLogViewerCommand}"
|
||||
Text="查看日志" />
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/message.png"
|
||||
Command="{Binding OpenUserManualCommand}"
|
||||
Text="帮助文档" />
|
||||
<telerik:RadRibbonButton
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/tools.png"
|
||||
Command="{Binding OpenLibraryVersionsCommand}"
|
||||
Text="关于" />
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
|
||||
</telerik:RadRibbonTab>
|
||||
|
||||
<telerik:RadRibbonView.ContextualGroups>
|
||||
@@ -506,7 +495,6 @@
|
||||
</telerik:RadRibbonView.ContextualGroups>
|
||||
</telerik:RadRibbonView>
|
||||
|
||||
<!-- Row 1: 主内容区 - 比例布局 -->
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="3"
|
||||
@@ -517,7 +505,6 @@
|
||||
<ColumnDefinition Width="350" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 中间: 2D Viewport -->
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
BorderBrush="#DDDDDD"
|
||||
@@ -525,7 +512,6 @@
|
||||
<views:ViewportPanelView />
|
||||
</Border>
|
||||
|
||||
<!-- 中间: 图像 -->
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
BorderBrush="#DDDDDD"
|
||||
@@ -533,7 +519,6 @@
|
||||
<views:ImagePanelView />
|
||||
</Border>
|
||||
|
||||
<!-- 右侧: 属性面板 -->
|
||||
<Grid Grid.Column="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="350*" />
|
||||
@@ -544,12 +529,11 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<views1:RaySourceOperateView Grid.Row="0" Grid.ColumnSpan="2" />
|
||||
<mcViews:AxisControlView Grid.Row="1" Grid.ColumnSpan="2"/>
|
||||
<mcViews:AxisControlView Grid.Row="1" Grid.ColumnSpan="2" />
|
||||
<views:NavigationPropertyPanelView Grid.Row="2" Grid.ColumnSpan="2" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Row 2: 状态栏 -->
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="3"
|
||||
@@ -563,7 +547,6 @@
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧: 状态信息 -->
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Margin="8,0"
|
||||
@@ -573,7 +556,6 @@
|
||||
Foreground="White"
|
||||
Text="就绪" />
|
||||
|
||||
<!-- 右侧: 鼠标坐标 + RGB -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="8,0"
|
||||
|
||||
@@ -18,17 +18,21 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
|
||||
<TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
|
||||
<TextBlock
|
||||
Margin="4,2"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#333333"
|
||||
Text="实时图像" />
|
||||
</Border>
|
||||
|
||||
<!-- 图像显示区域 -->
|
||||
<Grid Grid.Row="1">
|
||||
<roi:PolygonRoiCanvas x:Name="RoiCanvas"
|
||||
ImageSource="{Binding ImageSource}"
|
||||
Background="White">
|
||||
<roi:PolygonRoiCanvas
|
||||
x:Name="RoiCanvas"
|
||||
Background="White"
|
||||
ImageSource="{Binding ImageSource}">
|
||||
<roi:PolygonRoiCanvas.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="放大" Click="ZoomIn_Click" />
|
||||
@@ -38,14 +42,10 @@
|
||||
<MenuItem Header="保存原始图像" Click="SaveOriginalImage_Click" />
|
||||
<MenuItem Header="保存结果图像" Click="SaveResultImage_Click" />
|
||||
<Separator />
|
||||
<MenuItem Header="清除所有绘制" Click="ClearAllMeasurements_Click" />
|
||||
<MenuItem Header="清除所有测量" Click="ClearAllMeasurements_Click" />
|
||||
</ContextMenu>
|
||||
</roi:PolygonRoiCanvas.ContextMenu>
|
||||
</roi:PolygonRoiCanvas>
|
||||
|
||||
<!-- 十字线和测量功能已内置于 PolygonRoiCanvas -->
|
||||
</Grid>
|
||||
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
Reference in New Issue
Block a user