diff --git a/XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs b/XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs
new file mode 100644
index 0000000..5a811d7
--- /dev/null
+++ b/XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs
@@ -0,0 +1,30 @@
+using System.Windows;
+
+namespace XP.ImageProcessing.RoiControl.Controls
+{
+ /// 测量完成事件参数
+ public class MeasureCompletedEventArgs : RoutedEventArgs
+ {
+ public Point P1 { get; }
+ public Point P2 { get; }
+ public double Distance { get; }
+ public int TotalCount { get; }
+
+ public MeasureCompletedEventArgs(RoutedEvent routedEvent, Point p1, Point p2, double distance, int totalCount)
+ : base(routedEvent)
+ {
+ P1 = p1; P2 = p2; Distance = distance; TotalCount = totalCount;
+ }
+ }
+
+ /// 测量状态变化事件参数
+ public class MeasureStatusEventArgs : RoutedEventArgs
+ {
+ public string Message { get; }
+
+ public MeasureStatusEventArgs(RoutedEvent routedEvent, string message) : base(routedEvent)
+ {
+ Message = message;
+ }
+ }
+}
diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs
index 9a34686..dd87143 100644
--- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs
+++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs
@@ -243,6 +243,266 @@ namespace XP.ImageProcessing.RoiControl.Controls
#endregion Dependency Properties
+ #region Crosshair
+
+ public static readonly DependencyProperty ShowCrosshairProperty =
+ DependencyProperty.Register(nameof(ShowCrosshair), typeof(bool), typeof(PolygonRoiCanvas),
+ new PropertyMetadata(false, OnShowCrosshairChanged));
+
+ public bool ShowCrosshair
+ {
+ get => (bool)GetValue(ShowCrosshairProperty);
+ set => SetValue(ShowCrosshairProperty, value);
+ }
+
+ private Line _crosshairH, _crosshairV;
+
+ private static void OnShowCrosshairChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var c = (PolygonRoiCanvas)d;
+ if ((bool)e.NewValue)
+ c.AddCrosshair();
+ else
+ c.RemoveCrosshair();
+ }
+
+ private void AddCrosshair()
+ {
+ RemoveCrosshair();
+ double w = CanvasWidth, h = CanvasHeight;
+ if (w <= 0 || h <= 0) return;
+
+ _crosshairH = new Line { X1 = 0, Y1 = h / 2, X2 = w, Y2 = h / 2, Stroke = Brushes.Red, StrokeThickness = 1, Opacity = 0.7, IsHitTestVisible = false };
+ _crosshairV = new Line { X1 = w / 2, Y1 = 0, X2 = w / 2, Y2 = h, Stroke = Brushes.Red, StrokeThickness = 1, Opacity = 0.7, IsHitTestVisible = false };
+ mainCanvas.Children.Add(_crosshairH);
+ mainCanvas.Children.Add(_crosshairV);
+ }
+
+ private void RemoveCrosshair()
+ {
+ if (_crosshairH != null) { mainCanvas.Children.Remove(_crosshairH); _crosshairH = null; }
+ if (_crosshairV != null) { mainCanvas.Children.Remove(_crosshairV); _crosshairV = null; }
+ }
+
+ #endregion Crosshair
+
+ #region Measurement
+
+ public static readonly DependencyProperty IsMeasuringProperty =
+ DependencyProperty.Register(nameof(IsMeasuring), typeof(bool), typeof(PolygonRoiCanvas),
+ new PropertyMetadata(false, OnIsMeasuringChanged));
+
+ public bool IsMeasuring
+ {
+ get => (bool)GetValue(IsMeasuringProperty);
+ set => SetValue(IsMeasuringProperty, value);
+ }
+
+ private Canvas _measureOverlay;
+ private readonly System.Collections.Generic.List _measureGroups = new();
+ private Ellipse _pendingDot;
+ private Point? _pendingPoint;
+ private Ellipse _mDraggingDot;
+ private Models.MeasureGroup _mDraggingGroup;
+ private bool _mDraggingIsDot1;
+
+ private static void OnIsMeasuringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var c = (PolygonRoiCanvas)d;
+ if ((bool)e.NewValue)
+ {
+ c.EnsureMeasureOverlay();
+ }
+ else
+ {
+ // 退出测量模式:只清除未完成的临时点,保留已完成的测量线
+ if (c._pendingDot != null && c._measureOverlay != null)
+ {
+ c._measureOverlay.Children.Remove(c._pendingDot);
+ c._pendingDot = null;
+ }
+ c._pendingPoint = null;
+ }
+ }
+
+ private void EnsureMeasureOverlay()
+ {
+ if (_measureOverlay != null) return;
+ _measureOverlay = new Canvas { IsHitTestVisible = true, Background = Brushes.Transparent };
+ _measureOverlay.SetBinding(Canvas.WidthProperty, new System.Windows.Data.Binding("CanvasWidth") { Source = this });
+ _measureOverlay.SetBinding(Canvas.HeightProperty, new System.Windows.Data.Binding("CanvasHeight") { Source = this });
+ mainCanvas.Children.Add(_measureOverlay);
+ }
+
+ private void RemoveMeasureOverlay()
+ {
+ if (_measureOverlay != null)
+ {
+ mainCanvas.Children.Remove(_measureOverlay);
+ _measureOverlay = null;
+ }
+ _measureGroups.Clear();
+ _pendingDot = null;
+ _pendingPoint = null;
+ _mDraggingDot = null;
+ _mDraggingGroup = null;
+ }
+
+ /// 清除所有测量绘制
+ public void ClearMeasurements()
+ {
+ RemoveMeasureOverlay();
+ }
+
+ /// 获取当前测量组数量
+ public int MeasureCount => _measureGroups.Count;
+
+ /// 处理测量模式下的画布点击
+ private void HandleMeasureClick(Point pos)
+ {
+ if (_measureOverlay == null) EnsureMeasureOverlay();
+ if (_measureOverlay == null) return;
+
+ if (!_pendingPoint.HasValue)
+ {
+ _pendingPoint = pos;
+ _pendingDot = CreateMeasureDot(Brushes.Red);
+ _measureOverlay.Children.Add(_pendingDot);
+ SetMeasureDotPos(_pendingDot, pos);
+ RaiseMeasureStatusChanged($"点点距测量 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点");
+ }
+ else
+ {
+ var g = CreateMeasureGroup(_pendingPoint.Value, pos);
+ _measureGroups.Add(g);
+ _measureOverlay.Children.Remove(_pendingDot);
+ _pendingDot = null;
+ _pendingPoint = null;
+ RaiseMeasureCompleted(g);
+
+ // 完成一条测量后自动退出测量模式
+ IsMeasuring = false;
+ }
+ }
+
+ private Models.MeasureGroup CreateMeasureGroup(Point p1, Point p2)
+ {
+ var g = new Models.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 = CreateMeasureDot(Brushes.Red);
+ g.Dot2 = CreateMeasureDot(Brushes.Blue);
+ _measureOverlay.Children.Add(g.Dot1);
+ _measureOverlay.Children.Add(g.Dot2);
+
+ SetMeasureDotPos(g.Dot1, p1);
+ SetMeasureDotPos(g.Dot2, p2);
+ g.UpdateLine();
+ g.UpdateLabel();
+ return g;
+ }
+
+ private Ellipse CreateMeasureDot(Brush fill)
+ {
+ var dot = new Ellipse { Width = 12, Height = 12, Fill = fill, Stroke = Brushes.White, StrokeThickness = 1.5, Cursor = Cursors.Hand };
+ dot.SetValue(ContextMenuService.IsEnabledProperty, false);
+ dot.MouseLeftButtonDown += MDot_Down;
+ dot.MouseMove += MDot_Move;
+ dot.MouseLeftButtonUp += MDot_Up;
+ dot.PreviewMouseRightButtonUp += MDot_RightClick;
+ return dot;
+ }
+
+ private static void SetMeasureDotPos(Ellipse dot, Point pos)
+ {
+ Canvas.SetLeft(dot, pos.X - dot.Width / 2);
+ Canvas.SetTop(dot, pos.Y - dot.Height / 2);
+ }
+
+ private void MDot_Down(object sender, MouseButtonEventArgs e)
+ {
+ if (sender is not Ellipse dot) return;
+ foreach (var g in _measureGroups)
+ {
+ if (g.Dot1 == dot) { _mDraggingGroup = g; _mDraggingIsDot1 = true; break; }
+ if (g.Dot2 == dot) { _mDraggingGroup = g; _mDraggingIsDot1 = false; break; }
+ }
+ if (_mDraggingGroup != null) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; }
+ }
+
+ private void MDot_Move(object sender, MouseEventArgs e)
+ {
+ if (_mDraggingDot == null || _mDraggingGroup == null || _measureOverlay == null) return;
+ if (e.LeftButton != MouseButtonState.Pressed) return;
+ var pos = e.GetPosition(_measureOverlay);
+ SetMeasureDotPos(_mDraggingDot, pos);
+ if (_mDraggingIsDot1) _mDraggingGroup.P1 = pos; else _mDraggingGroup.P2 = pos;
+ _mDraggingGroup.UpdateLine();
+ _mDraggingGroup.UpdateLabel();
+ RaiseMeasureCompleted(_mDraggingGroup);
+ e.Handled = true;
+ }
+
+ private void MDot_Up(object sender, MouseButtonEventArgs e)
+ {
+ if (_mDraggingDot != null) { _mDraggingDot.ReleaseMouseCapture(); _mDraggingDot = null; _mDraggingGroup = null; e.Handled = true; }
+ }
+
+ private void MDot_RightClick(object sender, MouseButtonEventArgs e)
+ {
+ if (sender is not Ellipse dot || _measureOverlay == null) return;
+ Models.MeasureGroup target = null;
+ foreach (var g in _measureGroups)
+ {
+ 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);
+ _measureGroups.Remove(target);
+ RaiseMeasureStatusChanged($"已删除测量线 | 剩余 {_measureGroups.Count} 条");
+ e.Handled = true;
+ }
+
+ // 测量事件
+ public static readonly RoutedEvent MeasureCompletedEvent =
+ EventManager.RegisterRoutedEvent(nameof(MeasureCompleted), RoutingStrategy.Bubble,
+ typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
+
+ public event RoutedEventHandler MeasureCompleted
+ {
+ add { AddHandler(MeasureCompletedEvent, value); }
+ remove { RemoveHandler(MeasureCompletedEvent, value); }
+ }
+
+ private void RaiseMeasureCompleted(Models.MeasureGroup g)
+ {
+ RaiseEvent(new MeasureCompletedEventArgs(MeasureCompletedEvent, g.P1, g.P2, g.Distance, _measureGroups.Count));
+ }
+
+ public static readonly RoutedEvent MeasureStatusChangedEvent =
+ EventManager.RegisterRoutedEvent(nameof(MeasureStatusChanged), RoutingStrategy.Bubble,
+ typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
+
+ public event RoutedEventHandler MeasureStatusChanged
+ {
+ add { AddHandler(MeasureStatusChangedEvent, value); }
+ remove { RemoveHandler(MeasureStatusChangedEvent, value); }
+ }
+
+ private void RaiseMeasureStatusChanged(string message)
+ {
+ RaiseEvent(new MeasureStatusEventArgs(MeasureStatusChangedEvent, message));
+ }
+
+ #endregion Measurement
+
#region Adorner Management
private void UpdateAdorner()
@@ -387,8 +647,9 @@ namespace XP.ImageProcessing.RoiControl.Controls
if (!isDragging)
{
- // 处理点击事件
Point clickPosition = e.GetPosition(mainCanvas);
+ if (IsMeasuring)
+ HandleMeasureClick(clickPosition);
OnCanvasClicked(clickPosition);
}
diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs
new file mode 100644
index 0000000..c1550d8
--- /dev/null
+++ b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Shapes;
+
+namespace XP.ImageProcessing.RoiControl.Models
+{
+ /// 一次点点距测量的所有视觉元素
+ public class MeasureGroup
+ {
+ public Ellipse Dot1 { get; set; }
+ public Ellipse Dot2 { get; set; }
+ public Line Line { get; set; }
+ public TextBlock Label { get; set; }
+ public Point P1 { get; set; }
+ public Point P2 { get; set; }
+
+ public double Distance
+ {
+ get
+ {
+ double dx = P2.X - P1.X, dy = P2.Y - P1.Y;
+ return Math.Sqrt(dx * dx + dy * dy);
+ }
+ }
+
+ 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(string unit = "px")
+ {
+ Label.Text = $"{Distance:F2} {unit}";
+ Canvas.SetLeft(Label, (P1.X + P2.X) / 2 + 8);
+ Canvas.SetTop(Label, (P1.Y + P2.Y) / 2 - 18);
+ Label.Visibility = Visibility.Visible;
+ }
+ }
+}
diff --git a/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs b/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs
index d41512f..6d022b8 100644
--- a/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs
+++ b/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs
@@ -19,6 +19,7 @@ namespace XplorePlane.ViewModels
public class ViewportPanelViewModel : BindableBase
{
private readonly ILoggerService _logger;
+ private readonly IEventAggregator _eventAggregator;
private int _isProcessingFrame;
private ImageSource _imageSource;
@@ -137,10 +138,12 @@ namespace XplorePlane.ViewModels
public ViewportPanelViewModel(IEventAggregator eventAggregator, ILoggerService logger)
{
_logger = logger?.ForModule();
+ _eventAggregator = eventAggregator;
CancelMeasurementCommand = new DelegateCommand(() =>
{
- CurrentMeasurementMode = MeasurementToolMode.None;
+ // 发布 None 事件,让 View 层也收到
+ _eventAggregator.GetEvent().Publish(MeasurementToolMode.None);
ImageInfo = "测量已取消";
});
diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml b/XplorePlane/Views/Main/ViewportPanelView.xaml
index cb1571e..0101500 100644
--- a/XplorePlane/Views/Main/ViewportPanelView.xaml
+++ b/XplorePlane/Views/Main/ViewportPanelView.xaml
@@ -6,15 +6,11 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
- xmlns:local="clr-namespace:XplorePlane.Views"
prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="400"
d:DesignWidth="600"
mc:Ignorable="d">
-
-
-
-
+
@@ -24,19 +20,8 @@
-
-
-
-
-
-
-
-
+
@@ -58,7 +43,7 @@
-
+
diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
index 94aeaa0..04b621a 100644
--- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
+++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
@@ -1,90 +1,60 @@
using System;
-using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
-using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
-using System.Windows.Shapes;
using Microsoft.Win32;
-using XP.ImageProcessing.RoiControl.Controls;
using Prism.Ioc;
+using XP.ImageProcessing.RoiControl.Controls;
using XplorePlane.Events;
using XplorePlane.ViewModels;
namespace XplorePlane.Views
{
- /// 一次测量的所有视觉元素
- 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
{
- private Canvas _measureOverlay;
- private readonly List _groups = new();
-
- // 当前正在创建的测量(只有第一个点,还没完成)
- private Ellipse _pendingDot;
- private Point? _pendingPoint;
-
- // 拖拽状态
- private Ellipse _draggingDot;
- private MeasureGroup _draggingGroup;
- private bool _draggingIsDot1;
-
public ViewportPanelView()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
- RoiCanvas.CanvasClicked += (s, e) =>
+ // 测量事件 → 更新状态栏
+ RoiCanvas.MeasureCompleted += (s, e) =>
{
- if (e is CanvasClickedEventArgs args)
- OnCanvasClicked(args.Position);
+ if (e is MeasureCompletedEventArgs args && DataContext is ViewportPanelViewModel vm)
+ {
+ vm.MeasurementResult = $"{args.Distance:F2} px";
+ vm.ImageInfo = $"点点距: {args.Distance:F2} px | ({args.P1.X:F0},{args.P1.Y:F0}) → ({args.P2.X:F0},{args.P2.Y:F0}) | 共 {args.TotalCount} 条测量";
+ }
+ };
+ RoiCanvas.MeasureStatusChanged += (s, e) =>
+ {
+ if (e is MeasureStatusEventArgs args && DataContext is ViewportPanelViewModel vm)
+ vm.ImageInfo = args.Message;
};
- // 直接订阅 Prism 事件,不依赖 DataContext 的 PropertyChanged
+ // 十字辅助线:直接订阅 Prism 事件
try
{
- var ea = Prism.Ioc.ContainerLocator.Current?.Resolve();
+ var ea = ContainerLocator.Current?.Resolve();
ea?.GetEvent().Subscribe(() =>
{
- _crosshairVisible = !_crosshairVisible;
- ToggleCrosshairOnCanvas(_crosshairVisible);
+ RoiCanvas.ShowCrosshair = !RoiCanvas.ShowCrosshair;
+ }, Prism.Events.ThreadOption.UIThread);
+
+ // 测量模式:直接订阅 Prism 事件
+ ea?.GetEvent().Subscribe(mode =>
+ {
+ RoiCanvas.IsMeasuring = mode != MeasurementToolMode.None;
}, Prism.Events.ThreadOption.UIThread);
}
- catch { /* 设计时或容器未初始化时忽略 */ }
+ catch { }
}
- private bool _crosshairVisible;
-
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
- System.Diagnostics.Debug.WriteLine($"[VP] DataContextChanged: old={e.OldValue?.GetType().Name}, new={e.NewValue?.GetType().Name}");
if (e.OldValue is INotifyPropertyChanged oldVm)
oldVm.PropertyChanged -= OnVmPropertyChanged;
if (e.NewValue is INotifyPropertyChanged newVm)
@@ -94,300 +64,13 @@ namespace XplorePlane.Views
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (sender is not ViewportPanelViewModel vm) return;
- System.Diagnostics.Debug.WriteLine($"[VP] PropertyChanged: {e.PropertyName}");
+
if (e.PropertyName == nameof(ViewportPanelViewModel.CurrentMeasurementMode))
{
- if (vm.CurrentMeasurementMode != MeasurementToolMode.None)
- Dispatcher.BeginInvoke(new Action(EnsureOverlay),
- System.Windows.Threading.DispatcherPriority.Loaded);
- else
- RemoveOverlay();
- }
- else if (e.PropertyName == nameof(ViewportPanelViewModel.ShowCrosshair))
- {
- // 十字线通过直接订阅 ToggleCrosshairEvent 处理
+ RoiCanvas.IsMeasuring = vm.CurrentMeasurementMode != MeasurementToolMode.None;
}
}
- #region 覆盖层管理
-
- private void EnsureOverlay()
- {
- if (_measureOverlay != null) return;
-
- var mainCanvas = FindChildByName