点点距测量工具初步实现
This commit is contained in:
+15
-15
@@ -265,23 +265,23 @@ namespace XplorePlane
|
|||||||
// 主窗口加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化
|
// 主窗口加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化
|
||||||
shell.Loaded += (s, e) =>
|
shell.Loaded += (s, e) =>
|
||||||
{
|
{
|
||||||
TryConnectCamera();
|
//TryConnectCamera();
|
||||||
|
|
||||||
// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误
|
//// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>();
|
// var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>();
|
||||||
cameraVm.OnCameraReady();
|
// cameraVm.OnCameraReady();
|
||||||
}
|
// }
|
||||||
catch (Exception ex)
|
// catch (Exception ex)
|
||||||
{
|
// {
|
||||||
Log.Error(ex, "通知相机 ViewModel 失败");
|
// Log.Error(ex, "通知相机 ViewModel 失败");
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (_cameraError != null)
|
// if (_cameraError != null)
|
||||||
{
|
// {
|
||||||
HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error);
|
// HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
}
|
//}
|
||||||
};
|
};
|
||||||
|
|
||||||
return shell;
|
return shell;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Prism.Events;
|
||||||
|
|
||||||
|
namespace XplorePlane.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 测量工具模式
|
||||||
|
/// </summary>
|
||||||
|
public enum MeasurementToolMode
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
PointDistance,
|
||||||
|
PointLineDistance,
|
||||||
|
Angle,
|
||||||
|
ThroughHoleFillRate
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测量工具激活事件,由 MainViewModel 发布,ViewportPanelViewModel 订阅
|
||||||
|
/// </summary>
|
||||||
|
public class MeasurementToolEvent : PubSubEvent<MeasurementToolMode> { }
|
||||||
|
}
|
||||||
@@ -75,6 +75,12 @@ namespace XplorePlane.ViewModels
|
|||||||
public DelegateCommand OpenRaySourceConfigCommand { get; }
|
public DelegateCommand OpenRaySourceConfigCommand { get; }
|
||||||
public DelegateCommand WarmUpCommand { get; }
|
public DelegateCommand WarmUpCommand { get; }
|
||||||
|
|
||||||
|
// 测量命令
|
||||||
|
public DelegateCommand PointDistanceMeasureCommand { get; }
|
||||||
|
public DelegateCommand PointLineDistanceMeasureCommand { get; }
|
||||||
|
public DelegateCommand AngleMeasureCommand { get; }
|
||||||
|
public DelegateCommand ThroughHoleFillRateMeasureCommand { get; }
|
||||||
|
|
||||||
// 设置命令
|
// 设置命令
|
||||||
public DelegateCommand OpenLanguageSwitcherCommand { get; }
|
public DelegateCommand OpenLanguageSwitcherCommand { get; }
|
||||||
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
|
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
|
||||||
@@ -152,6 +158,12 @@ namespace XplorePlane.ViewModels
|
|||||||
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute()));
|
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute()));
|
||||||
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute()));
|
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute()));
|
||||||
|
|
||||||
|
// 测量命令
|
||||||
|
PointDistanceMeasureCommand = new DelegateCommand(ExecutePointDistanceMeasure);
|
||||||
|
PointLineDistanceMeasureCommand = new DelegateCommand(ExecutePointLineDistanceMeasure);
|
||||||
|
AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure);
|
||||||
|
ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure);
|
||||||
|
|
||||||
// 硬件命令
|
// 硬件命令
|
||||||
AxisResetCommand = new DelegateCommand(ExecuteAxisReset);
|
AxisResetCommand = new DelegateCommand(ExecuteAxisReset);
|
||||||
OpenDetectorConfigCommand = new DelegateCommand(ExecuteOpenDetectorConfig);
|
OpenDetectorConfigCommand = new DelegateCommand(ExecuteOpenDetectorConfig);
|
||||||
@@ -419,6 +431,34 @@ namespace XplorePlane.ViewModels
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region 测量命令实现
|
||||||
|
|
||||||
|
private void ExecutePointDistanceMeasure()
|
||||||
|
{
|
||||||
|
_logger.Info("点点距测量功能已触发");
|
||||||
|
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecutePointLineDistanceMeasure()
|
||||||
|
{
|
||||||
|
_logger.Info("点线距测量功能已触发");
|
||||||
|
// TODO: 实现点线距测量逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteAngleMeasure()
|
||||||
|
{
|
||||||
|
_logger.Info("角度测量功能已触发");
|
||||||
|
// TODO: 实现角度测量逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteThroughHoleFillRateMeasure()
|
||||||
|
{
|
||||||
|
_logger.Info("通孔填锡率测量功能已触发");
|
||||||
|
// TODO: 实现通孔填锡率测量逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region 设置命令实现
|
#region 设置命令实现
|
||||||
|
|
||||||
private void ExecuteOpenLanguageSwitcher()
|
private void ExecuteOpenLanguageSwitcher()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Prism.Commands;
|
||||||
using Prism.Events;
|
using Prism.Events;
|
||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
using System;
|
using System;
|
||||||
@@ -34,16 +35,131 @@ namespace XplorePlane.ViewModels
|
|||||||
set => SetProperty(ref _imageInfo, value);
|
set => SetProperty(ref _imageInfo, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region 测量工具状态
|
||||||
|
|
||||||
|
private MeasurementToolMode _currentMeasurementMode = MeasurementToolMode.None;
|
||||||
|
public MeasurementToolMode CurrentMeasurementMode
|
||||||
|
{
|
||||||
|
get => _currentMeasurementMode;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _currentMeasurementMode, value))
|
||||||
|
{
|
||||||
|
RaisePropertyChanged(nameof(IsMeasuring));
|
||||||
|
RaisePropertyChanged(nameof(MeasurementModeText));
|
||||||
|
// 切换模式时重置状态
|
||||||
|
ResetMeasurementState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMeasuring => CurrentMeasurementMode != MeasurementToolMode.None;
|
||||||
|
|
||||||
|
public string MeasurementModeText => CurrentMeasurementMode switch
|
||||||
|
{
|
||||||
|
MeasurementToolMode.PointDistance => "点点距测量 - 请在图像上点击第一个点",
|
||||||
|
_ => 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 void ResetMeasurementState()
|
||||||
|
{
|
||||||
|
MeasurePoint1 = null;
|
||||||
|
MeasurePoint2 = null;
|
||||||
|
MeasurementResult = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
public ViewportPanelViewModel(IEventAggregator eventAggregator, ILoggerService logger)
|
public ViewportPanelViewModel(IEventAggregator eventAggregator, ILoggerService logger)
|
||||||
{
|
{
|
||||||
_logger = logger?.ForModule<ViewportPanelViewModel>();
|
_logger = logger?.ForModule<ViewportPanelViewModel>();
|
||||||
|
|
||||||
|
CancelMeasurementCommand = new DelegateCommand(() =>
|
||||||
|
{
|
||||||
|
CurrentMeasurementMode = MeasurementToolMode.None;
|
||||||
|
ImageInfo = "测量已取消";
|
||||||
|
});
|
||||||
|
|
||||||
eventAggregator.GetEvent<ImageCapturedEvent>()
|
eventAggregator.GetEvent<ImageCapturedEvent>()
|
||||||
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
|
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
|
||||||
eventAggregator.GetEvent<ManualImageLoadedEvent>()
|
eventAggregator.GetEvent<ManualImageLoadedEvent>()
|
||||||
.Subscribe(OnManualImageLoaded, ThreadOption.UIThread);
|
.Subscribe(OnManualImageLoaded, ThreadOption.UIThread);
|
||||||
eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
|
eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
|
||||||
.Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread);
|
.Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread);
|
||||||
|
|
||||||
|
// 订阅测量工具事件
|
||||||
|
eventAggregator.GetEvent<MeasurementToolEvent>()
|
||||||
|
.Subscribe(OnMeasurementToolActivated, ThreadOption.UIThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMeasurementToolActivated(MeasurementToolMode mode)
|
||||||
|
{
|
||||||
|
CurrentMeasurementMode = mode;
|
||||||
|
_logger?.Info("测量工具模式切换: {Mode}", mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnImageCaptured(ImageCapturedEventArgs args)
|
private void OnImageCaptured(ImageCapturedEventArgs args)
|
||||||
|
|||||||
@@ -400,6 +400,48 @@
|
|||||||
Size="Large"
|
Size="Large"
|
||||||
SmallImage="/Assets/Icons/spiral.png" />
|
SmallImage="/Assets/Icons/spiral.png" />
|
||||||
</telerik:RadRibbonGroup>
|
</telerik:RadRibbonGroup>
|
||||||
|
|
||||||
|
<telerik:RadRibbonGroup Header="测量工具">
|
||||||
|
<telerik:RadRibbonGroup.Variants>
|
||||||
|
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||||
|
</telerik:RadRibbonGroup.Variants>
|
||||||
|
|
||||||
|
<!-- 第一列: 点点距 + 点线距 -->
|
||||||
|
<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="点线距测量" />
|
||||||
|
</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 Header="图像处理">
|
<telerik:RadRibbonGroup Header="图像处理">
|
||||||
<telerik:RadRibbonGroup.Variants>
|
<telerik:RadRibbonGroup.Variants>
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
d:DesignHeight="400"
|
d:DesignHeight="400"
|
||||||
d:DesignWidth="600"
|
d:DesignWidth="600"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVisConverter" />
|
||||||
|
</UserControl.Resources>
|
||||||
<Grid Background="#FFFFFF">
|
<Grid Background="#FFFFFF">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -19,14 +22,28 @@
|
|||||||
|
|
||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
|
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
|
||||||
<TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center"
|
<Grid>
|
||||||
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
|
<TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||||
|
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
|
||||||
|
<!-- 测量模式提示 + 取消按钮 -->
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="4,0"
|
||||||
|
Visibility="{Binding IsMeasuring, Converter={StaticResource BoolToVisConverter}}">
|
||||||
|
<TextBlock VerticalAlignment="Center" Foreground="#E65100" FontSize="11"
|
||||||
|
Text="{Binding MeasurementModeText}" Margin="0,0,8,0" />
|
||||||
|
<Button Content="✕ 取消" FontSize="10" Padding="6,1"
|
||||||
|
Command="{Binding CancelMeasurementCommand}"
|
||||||
|
Background="#FFDDDD" BorderBrush="#CC0000" Foreground="#CC0000" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- 图像显示区域,支持滚动、缩放和ROI -->
|
<!-- 图像显示区域 -->
|
||||||
<roi:PolygonRoiCanvas Grid.Row="1"
|
<Grid Grid.Row="1">
|
||||||
ImageSource="{Binding ImageSource}"
|
<roi:PolygonRoiCanvas x:Name="RoiCanvas"
|
||||||
Background="White" />
|
ImageSource="{Binding ImageSource}"
|
||||||
|
Background="White" />
|
||||||
|
<!-- 测量结果浮层(已移至画布内线段附近显示) -->
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!-- 图像信息栏 -->
|
<!-- 图像信息栏 -->
|
||||||
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0">
|
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0">
|
||||||
|
|||||||
@@ -1,12 +1,324 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using XP.ImageProcessing.RoiControl.Controls;
|
||||||
|
using XplorePlane.Events;
|
||||||
|
using XplorePlane.ViewModels;
|
||||||
|
|
||||||
namespace XplorePlane.Views
|
namespace XplorePlane.Views
|
||||||
{
|
{
|
||||||
|
/// <summary>一次测量的所有视觉元素</summary>
|
||||||
|
internal class MeasureGroup
|
||||||
|
{
|
||||||
|
public Ellipse Dot1, Dot2;
|
||||||
|
public Line Line;
|
||||||
|
public TextBlock Label;
|
||||||
|
public Point P1, P2;
|
||||||
|
|
||||||
|
public void UpdateLine()
|
||||||
|
{
|
||||||
|
Line.X1 = P1.X; Line.Y1 = P1.Y;
|
||||||
|
Line.X2 = P2.X; Line.Y2 = P2.Y;
|
||||||
|
Line.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateLabel()
|
||||||
|
{
|
||||||
|
double dx = P2.X - P1.X, dy = P2.Y - P1.Y;
|
||||||
|
double dist = Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
Label.Text = $"{dist:F2} px";
|
||||||
|
Canvas.SetLeft(Label, (P1.X + P2.X) / 2 + 8);
|
||||||
|
Canvas.SetTop(Label, (P1.Y + P2.Y) / 2 - 18);
|
||||||
|
Label.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public partial class ViewportPanelView : UserControl
|
public partial class ViewportPanelView : UserControl
|
||||||
{
|
{
|
||||||
|
private Canvas _measureOverlay;
|
||||||
|
private readonly List<MeasureGroup> _groups = new();
|
||||||
|
|
||||||
|
// 当前正在创建的测量(只有第一个点,还没完成)
|
||||||
|
private Ellipse _pendingDot;
|
||||||
|
private Point? _pendingPoint;
|
||||||
|
|
||||||
|
// 拖拽状态
|
||||||
|
private Ellipse _draggingDot;
|
||||||
|
private MeasureGroup _draggingGroup;
|
||||||
|
private bool _draggingIsDot1;
|
||||||
|
|
||||||
public ViewportPanelView()
|
public ViewportPanelView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
DataContextChanged += OnDataContextChanged;
|
||||||
|
|
||||||
|
RoiCanvas.CanvasClicked += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e is CanvasClickedEventArgs args)
|
||||||
|
OnCanvasClicked(args.Position);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.OldValue is INotifyPropertyChanged oldVm)
|
||||||
|
oldVm.PropertyChanged -= OnVmPropertyChanged;
|
||||||
|
if (e.NewValue is INotifyPropertyChanged newVm)
|
||||||
|
newVm.PropertyChanged += OnVmPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not ViewportPanelViewModel vm) return;
|
||||||
|
if (e.PropertyName == nameof(ViewportPanelViewModel.CurrentMeasurementMode))
|
||||||
|
{
|
||||||
|
if (vm.CurrentMeasurementMode != MeasurementToolMode.None)
|
||||||
|
Dispatcher.BeginInvoke(new Action(EnsureOverlay),
|
||||||
|
System.Windows.Threading.DispatcherPriority.Loaded);
|
||||||
|
else
|
||||||
|
RemoveOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 覆盖层管理
|
||||||
|
|
||||||
|
private void EnsureOverlay()
|
||||||
|
{
|
||||||
|
if (_measureOverlay != null) return;
|
||||||
|
|
||||||
|
var mainCanvas = FindChildByName<Canvas>(RoiCanvas, "mainCanvas");
|
||||||
|
if (mainCanvas == null) return;
|
||||||
|
|
||||||
|
_measureOverlay = new Canvas
|
||||||
|
{
|
||||||
|
IsHitTestVisible = true,
|
||||||
|
Background = Brushes.Transparent
|
||||||
|
};
|
||||||
|
_measureOverlay.SetBinding(Canvas.WidthProperty,
|
||||||
|
new System.Windows.Data.Binding("CanvasWidth") { Source = RoiCanvas });
|
||||||
|
_measureOverlay.SetBinding(Canvas.HeightProperty,
|
||||||
|
new System.Windows.Data.Binding("CanvasHeight") { Source = RoiCanvas });
|
||||||
|
|
||||||
|
mainCanvas.Children.Add(_measureOverlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveOverlay()
|
||||||
|
{
|
||||||
|
if (_measureOverlay != null)
|
||||||
|
{
|
||||||
|
(VisualTreeHelper.GetParent(_measureOverlay) as Canvas)?.Children.Remove(_measureOverlay);
|
||||||
|
_measureOverlay = null;
|
||||||
|
}
|
||||||
|
_groups.Clear();
|
||||||
|
_pendingDot = null;
|
||||||
|
_pendingPoint = null;
|
||||||
|
_draggingDot = null;
|
||||||
|
_draggingGroup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 点击处理
|
||||||
|
|
||||||
|
private void OnCanvasClicked(Point pos)
|
||||||
|
{
|
||||||
|
if (DataContext is not ViewportPanelViewModel vm || !vm.IsMeasuring) return;
|
||||||
|
if (vm.CurrentMeasurementMode != MeasurementToolMode.PointDistance) return;
|
||||||
|
if (_measureOverlay == null) EnsureOverlay();
|
||||||
|
if (_measureOverlay == null) return;
|
||||||
|
|
||||||
|
if (!_pendingPoint.HasValue)
|
||||||
|
{
|
||||||
|
// 第一个点
|
||||||
|
_pendingPoint = pos;
|
||||||
|
_pendingDot = CreateDot(Brushes.Red);
|
||||||
|
_measureOverlay.Children.Add(_pendingDot);
|
||||||
|
SetDotPos(_pendingDot, pos);
|
||||||
|
vm.ImageInfo = $"点点距测量 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 第二个点 → 完成一次测量
|
||||||
|
var group = CreateGroup(_pendingPoint.Value, pos);
|
||||||
|
_groups.Add(group);
|
||||||
|
|
||||||
|
// 移除临时的第一个点(已被 group.Dot1 替代)
|
||||||
|
_measureOverlay.Children.Remove(_pendingDot);
|
||||||
|
_pendingDot = null;
|
||||||
|
_pendingPoint = null;
|
||||||
|
|
||||||
|
UpdateStatusBar(vm, group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 测量组创建
|
||||||
|
|
||||||
|
private MeasureGroup CreateGroup(Point p1, Point p2)
|
||||||
|
{
|
||||||
|
var g = new MeasureGroup { P1 = p1, P2 = p2 };
|
||||||
|
|
||||||
|
g.Line = new Line
|
||||||
|
{
|
||||||
|
Stroke = Brushes.Lime,
|
||||||
|
StrokeThickness = 2,
|
||||||
|
IsHitTestVisible = false
|
||||||
|
};
|
||||||
|
_measureOverlay.Children.Add(g.Line);
|
||||||
|
|
||||||
|
g.Label = new TextBlock
|
||||||
|
{
|
||||||
|
Foreground = Brushes.Yellow,
|
||||||
|
FontSize = 13,
|
||||||
|
FontWeight = FontWeights.Bold,
|
||||||
|
IsHitTestVisible = false
|
||||||
|
};
|
||||||
|
_measureOverlay.Children.Add(g.Label);
|
||||||
|
|
||||||
|
g.Dot1 = CreateDot(Brushes.Red);
|
||||||
|
g.Dot2 = CreateDot(Brushes.Blue);
|
||||||
|
_measureOverlay.Children.Add(g.Dot1);
|
||||||
|
_measureOverlay.Children.Add(g.Dot2);
|
||||||
|
|
||||||
|
SetDotPos(g.Dot1, p1);
|
||||||
|
SetDotPos(g.Dot2, p2);
|
||||||
|
g.UpdateLine();
|
||||||
|
g.UpdateLabel();
|
||||||
|
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Ellipse CreateDot(Brush fill)
|
||||||
|
{
|
||||||
|
var dot = new Ellipse
|
||||||
|
{
|
||||||
|
Width = 12, Height = 12,
|
||||||
|
Fill = fill,
|
||||||
|
Stroke = Brushes.White,
|
||||||
|
StrokeThickness = 1.5,
|
||||||
|
Cursor = Cursors.Hand
|
||||||
|
};
|
||||||
|
dot.MouseLeftButtonDown += Dot_Down;
|
||||||
|
dot.MouseMove += Dot_Move;
|
||||||
|
dot.MouseLeftButtonUp += Dot_Up;
|
||||||
|
dot.MouseRightButtonDown += Dot_RightClick;
|
||||||
|
return dot;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 拖拽
|
||||||
|
|
||||||
|
private void Dot_Down(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Ellipse dot) return;
|
||||||
|
|
||||||
|
// 找到这个圆点属于哪个测量组
|
||||||
|
foreach (var g in _groups)
|
||||||
|
{
|
||||||
|
if (g.Dot1 == dot) { _draggingGroup = g; _draggingIsDot1 = true; break; }
|
||||||
|
if (g.Dot2 == dot) { _draggingGroup = g; _draggingIsDot1 = false; break; }
|
||||||
|
}
|
||||||
|
if (_draggingGroup != null)
|
||||||
|
{
|
||||||
|
_draggingDot = dot;
|
||||||
|
dot.CaptureMouse();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Dot_Move(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (_draggingDot == null || _draggingGroup == null || _measureOverlay == null) return;
|
||||||
|
if (e.LeftButton != MouseButtonState.Pressed) return;
|
||||||
|
|
||||||
|
var pos = e.GetPosition(_measureOverlay);
|
||||||
|
SetDotPos(_draggingDot, pos);
|
||||||
|
|
||||||
|
if (_draggingIsDot1) _draggingGroup.P1 = pos;
|
||||||
|
else _draggingGroup.P2 = pos;
|
||||||
|
|
||||||
|
_draggingGroup.UpdateLine();
|
||||||
|
_draggingGroup.UpdateLabel();
|
||||||
|
|
||||||
|
if (DataContext is ViewportPanelViewModel vm)
|
||||||
|
UpdateStatusBar(vm, _draggingGroup);
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Dot_Up(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (_draggingDot != null)
|
||||||
|
{
|
||||||
|
_draggingDot.ReleaseMouseCapture();
|
||||||
|
_draggingDot = null;
|
||||||
|
_draggingGroup = null;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Dot_RightClick(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Ellipse dot || _measureOverlay == null) return;
|
||||||
|
|
||||||
|
MeasureGroup target = null;
|
||||||
|
foreach (var g in _groups)
|
||||||
|
{
|
||||||
|
if (g.Dot1 == dot || g.Dot2 == dot) { target = g; break; }
|
||||||
|
}
|
||||||
|
if (target == null) return;
|
||||||
|
|
||||||
|
// 移除该组的所有视觉元素
|
||||||
|
_measureOverlay.Children.Remove(target.Dot1);
|
||||||
|
_measureOverlay.Children.Remove(target.Dot2);
|
||||||
|
_measureOverlay.Children.Remove(target.Line);
|
||||||
|
_measureOverlay.Children.Remove(target.Label);
|
||||||
|
_groups.Remove(target);
|
||||||
|
|
||||||
|
if (DataContext is ViewportPanelViewModel vm)
|
||||||
|
vm.ImageInfo = $"已删除测量线 | 剩余 {_groups.Count} 条";
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 辅助
|
||||||
|
|
||||||
|
private static void SetDotPos(Ellipse dot, Point pos)
|
||||||
|
{
|
||||||
|
Canvas.SetLeft(dot, pos.X - dot.Width / 2);
|
||||||
|
Canvas.SetTop(dot, pos.Y - dot.Height / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateStatusBar(ViewportPanelViewModel vm, MeasureGroup g)
|
||||||
|
{
|
||||||
|
double dx = g.P2.X - g.P1.X, dy = g.P2.Y - g.P1.Y;
|
||||||
|
double dist = Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
vm.MeasurementResult = $"{dist:F2} px";
|
||||||
|
vm.ImageInfo = $"点点距: {dist:F2} px | ({g.P1.X:F0},{g.P1.Y:F0}) → ({g.P2.X:F0},{g.P2.Y:F0}) | 共 {_groups.Count} 条测量";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T FindChildByName<T>(DependencyObject parent, string name) where T : FrameworkElement
|
||||||
|
{
|
||||||
|
int count = VisualTreeHelper.GetChildrenCount(parent);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var child = VisualTreeHelper.GetChild(parent, i);
|
||||||
|
if (child is T t && t.Name == name) return t;
|
||||||
|
var result = FindChildByName<T>(child, name);
|
||||||
|
if (result != null) return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user