From eefbd1d1c8cb5d12e4636a614917aa893221c950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Fri, 24 Apr 2026 09:25:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E5=8D=81=E5=AD=97=E7=BA=BF=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E7=82=B9=E7=82=B9=E8=B7=9DROI=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=88=B0XP.ImageProcessing.RoiControl?= =?UTF-8?q?=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/MeasureEventArgs.cs | 30 ++ .../Controls/PolygonRoiCanvas.xaml.cs | 263 +++++++++++- .../Models/MeasureGroup.cs | 43 ++ .../ViewModels/Main/ViewportPanelViewModel.cs | 5 +- XplorePlane/Views/Main/ViewportPanelView.xaml | 23 +- .../Views/Main/ViewportPanelView.xaml.cs | 394 ++---------------- 6 files changed, 383 insertions(+), 375 deletions(-) create mode 100644 XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs create mode 100644 XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs 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 @@ - - - - - -