930 lines
36 KiB
C#
930 lines
36 KiB
C#
using XP.ImageProcessing.RoiControl.Models;
|
|
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Documents;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Imaging;
|
|
using System.Windows.Shapes;
|
|
|
|
namespace XP.ImageProcessing.RoiControl.Controls
|
|
{
|
|
public partial class PolygonRoiCanvas : UserControl
|
|
{
|
|
private bool isDragging = false;
|
|
private Point lastMousePosition;
|
|
private const double ZoomStep = 1.2;
|
|
private Adorner? currentAdorner;
|
|
|
|
public PolygonRoiCanvas()
|
|
{
|
|
InitializeComponent();
|
|
Loaded += PolygonRoiCanvas_Loaded;
|
|
}
|
|
|
|
private void PolygonRoiCanvas_Loaded(object sender, RoutedEventArgs e)
|
|
{
|
|
// 监听ROI集合变化
|
|
if (ROIItems != null)
|
|
{
|
|
ROIItems.CollectionChanged += ROIItems_CollectionChanged;
|
|
foreach (var roi in ROIItems)
|
|
{
|
|
roi.PropertyChanged += ROI_PropertyChanged;
|
|
|
|
// 如果是多边形ROI,监听Points集合变化
|
|
if (roi is PolygonROI polygonROI)
|
|
{
|
|
polygonROI.Points.CollectionChanged += Points_CollectionChanged;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ROIItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (e.NewItems != null)
|
|
{
|
|
foreach (ROIShape roi in e.NewItems)
|
|
{
|
|
roi.PropertyChanged += ROI_PropertyChanged;
|
|
|
|
// 如果是多边形ROI,监听Points集合变化
|
|
if (roi is PolygonROI polygonROI)
|
|
{
|
|
polygonROI.Points.CollectionChanged += Points_CollectionChanged;
|
|
}
|
|
}
|
|
}
|
|
if (e.OldItems != null)
|
|
{
|
|
foreach (ROIShape roi in e.OldItems)
|
|
{
|
|
roi.PropertyChanged -= ROI_PropertyChanged;
|
|
|
|
// 取消监听Points集合变化
|
|
if (roi is PolygonROI polygonROI)
|
|
{
|
|
polygonROI.Points.CollectionChanged -= Points_CollectionChanged;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Points_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
|
{
|
|
// 只在删除或添加顶点时更新Adorner,拖拽时的Replace操作不触发更新
|
|
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove ||
|
|
e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
|
|
{
|
|
// Points集合变化时,如果当前选中的是多边形ROI,更新Adorner
|
|
if (SelectedROI is PolygonROI polygonROI && sender == polygonROI.Points)
|
|
{
|
|
// 使用Dispatcher延迟更新,确保UI已经处理完Points的变化
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
UpdateAdorner();
|
|
}), System.Windows.Threading.DispatcherPriority.Render);
|
|
}
|
|
}
|
|
// Replace操作(拖拽时)不需要重建Adorner,只需要让现有Adorner重新布局
|
|
}
|
|
|
|
private void ROI_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == nameof(ROIShape.IsSelected))
|
|
{
|
|
UpdateAdorner();
|
|
}
|
|
// 监听Points属性变化(整个集合替换的情况)
|
|
else if (e.PropertyName == "Points" && sender is PolygonROI)
|
|
{
|
|
UpdateAdorner();
|
|
}
|
|
}
|
|
|
|
#region Dependency Properties
|
|
|
|
public static readonly DependencyProperty ImageSourceProperty =
|
|
DependencyProperty.Register(nameof(ImageSource), typeof(ImageSource), typeof(PolygonRoiCanvas),
|
|
new PropertyMetadata(null, OnImageSourceChanged));
|
|
|
|
public ImageSource? ImageSource
|
|
{
|
|
get => (ImageSource?)GetValue(ImageSourceProperty);
|
|
set => SetValue(ImageSourceProperty, value);
|
|
}
|
|
|
|
private static void OnImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
var control = (PolygonRoiCanvas)d;
|
|
if (e.NewValue is BitmapSource bitmap)
|
|
{
|
|
// 使用像素尺寸,避免 DPI 不同导致 DIP 尺寸与实际像素不一致
|
|
control.CanvasWidth = bitmap.PixelWidth;
|
|
control.CanvasHeight = bitmap.PixelHeight;
|
|
control.ResetView();
|
|
}
|
|
else if (e.NewValue is ImageSource imageSource)
|
|
{
|
|
control.CanvasWidth = imageSource.Width;
|
|
control.CanvasHeight = imageSource.Height;
|
|
control.ResetView();
|
|
}
|
|
|
|
// 图像尺寸变化后刷新十字线
|
|
if (control.ShowCrosshair)
|
|
control.AddCrosshair();
|
|
}
|
|
|
|
public static readonly DependencyProperty ROIItemsProperty =
|
|
DependencyProperty.Register(nameof(ROIItems), typeof(ObservableCollection<ROIShape>), typeof(PolygonRoiCanvas),
|
|
new PropertyMetadata(null));
|
|
|
|
public ObservableCollection<ROIShape>? ROIItems
|
|
{
|
|
get => (ObservableCollection<ROIShape>?)GetValue(ROIItemsProperty);
|
|
set => SetValue(ROIItemsProperty, value);
|
|
}
|
|
|
|
public static readonly DependencyProperty ZoomScaleProperty =
|
|
DependencyProperty.Register(nameof(ZoomScale), typeof(double), typeof(PolygonRoiCanvas),
|
|
new PropertyMetadata(1.0, OnZoomScaleChanged));
|
|
|
|
public double ZoomScale
|
|
{
|
|
get => (double)GetValue(ZoomScaleProperty);
|
|
set => SetValue(ZoomScaleProperty, Math.Max(0.1, Math.Min(10.0, value)));
|
|
}
|
|
|
|
private static void OnZoomScaleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
var control = (PolygonRoiCanvas)d;
|
|
// 缩放变化时更新Adorner以调整控制点大小
|
|
control.UpdateAdorner();
|
|
}
|
|
|
|
public static readonly DependencyProperty PanOffsetXProperty =
|
|
DependencyProperty.Register(nameof(PanOffsetX), typeof(double), typeof(PolygonRoiCanvas),
|
|
new PropertyMetadata(0.0, OnPanOffsetChanged));
|
|
|
|
public double PanOffsetX
|
|
{
|
|
get => (double)GetValue(PanOffsetXProperty);
|
|
set => SetValue(PanOffsetXProperty, value);
|
|
}
|
|
|
|
public static readonly DependencyProperty PanOffsetYProperty =
|
|
DependencyProperty.Register(nameof(PanOffsetY), typeof(double), typeof(PolygonRoiCanvas),
|
|
new PropertyMetadata(0.0, OnPanOffsetChanged));
|
|
|
|
public double PanOffsetY
|
|
{
|
|
get => (double)GetValue(PanOffsetYProperty);
|
|
set => SetValue(PanOffsetYProperty, value);
|
|
}
|
|
|
|
private static void OnPanOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
var control = (PolygonRoiCanvas)d;
|
|
|
|
// 平移时重建Adorner,确保控制点位置正确
|
|
if (control.SelectedROI != null && control.SelectedROI.IsSelected)
|
|
{
|
|
control.UpdateAdorner();
|
|
}
|
|
}
|
|
|
|
public static readonly DependencyProperty CanvasWidthProperty =
|
|
DependencyProperty.Register(nameof(CanvasWidth), typeof(double), typeof(PolygonRoiCanvas),
|
|
new PropertyMetadata(800.0));
|
|
|
|
public double CanvasWidth
|
|
{
|
|
get => (double)GetValue(CanvasWidthProperty);
|
|
set => SetValue(CanvasWidthProperty, value);
|
|
}
|
|
|
|
public static readonly DependencyProperty CanvasHeightProperty =
|
|
DependencyProperty.Register(nameof(CanvasHeight), typeof(double), typeof(PolygonRoiCanvas),
|
|
new PropertyMetadata(600.0));
|
|
|
|
public double CanvasHeight
|
|
{
|
|
get => (double)GetValue(CanvasHeightProperty);
|
|
set => SetValue(CanvasHeightProperty, value);
|
|
}
|
|
|
|
public static readonly DependencyProperty SelectedROIProperty =
|
|
DependencyProperty.Register(nameof(SelectedROI), typeof(ROIShape), typeof(PolygonRoiCanvas),
|
|
new PropertyMetadata(null, OnSelectedROIChanged));
|
|
|
|
public ROIShape? SelectedROI
|
|
{
|
|
get => (ROIShape?)GetValue(SelectedROIProperty);
|
|
set => SetValue(SelectedROIProperty, value);
|
|
}
|
|
|
|
private static void OnSelectedROIChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
var control = (PolygonRoiCanvas)d;
|
|
|
|
// 更新IsSelected状态
|
|
if (e.OldValue is ROIShape oldROI)
|
|
{
|
|
oldROI.IsSelected = false;
|
|
}
|
|
if (e.NewValue is ROIShape newROI)
|
|
{
|
|
newROI.IsSelected = true;
|
|
}
|
|
|
|
control.UpdateAdorner();
|
|
}
|
|
|
|
#endregion Dependency Properties
|
|
|
|
#region Measurement Config
|
|
|
|
public static readonly DependencyProperty PixelSizeProperty =
|
|
DependencyProperty.Register(nameof(PixelSize), typeof(double), typeof(PolygonRoiCanvas),
|
|
new PropertyMetadata(1.0));
|
|
|
|
/// <summary>每像素对应的物理尺寸(= 探测器像素尺寸 / 放大倍率),默认 1.0</summary>
|
|
public double PixelSize
|
|
{
|
|
get => (double)GetValue(PixelSizeProperty);
|
|
set => SetValue(PixelSizeProperty, value);
|
|
}
|
|
|
|
public static readonly DependencyProperty MeasureUnitProperty =
|
|
DependencyProperty.Register(nameof(MeasureUnit), typeof(string), typeof(PolygonRoiCanvas),
|
|
new PropertyMetadata("px"));
|
|
|
|
/// <summary>测量单位,默认 "px",可设为 "mm"/"μm"/"cm"</summary>
|
|
public string MeasureUnit
|
|
{
|
|
get => (string)GetValue(MeasureUnitProperty);
|
|
set => SetValue(MeasureUnitProperty, value);
|
|
}
|
|
|
|
/// <summary>将像素距离转换为物理距离文本</summary>
|
|
internal string FormatDistance(double pixelDistance)
|
|
{
|
|
string unit = MeasureUnit ?? "px";
|
|
if (unit == "px" || PixelSize <= 0 || PixelSize == 1.0)
|
|
return $"{pixelDistance:F2} px";
|
|
double physical = pixelDistance * PixelSize;
|
|
return $"{physical:F4} {unit}";
|
|
}
|
|
|
|
#endregion Measurement Config
|
|
|
|
#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 CurrentMeasureModeProperty =
|
|
DependencyProperty.Register(nameof(CurrentMeasureMode), typeof(Models.MeasureMode), typeof(PolygonRoiCanvas),
|
|
new PropertyMetadata(Models.MeasureMode.None, OnMeasureModeChanged));
|
|
|
|
public Models.MeasureMode CurrentMeasureMode
|
|
{
|
|
get => (Models.MeasureMode)GetValue(CurrentMeasureModeProperty);
|
|
set => SetValue(CurrentMeasureModeProperty, value);
|
|
}
|
|
|
|
// 保留 IsMeasuring 作为便捷属性
|
|
public bool IsMeasuring => CurrentMeasureMode != Models.MeasureMode.None;
|
|
|
|
private Canvas _measureOverlay;
|
|
private readonly System.Collections.Generic.List<Models.MeasureGroup> _ppGroups = new();
|
|
private readonly System.Collections.Generic.List<Models.PointToLineGroup> _ptlGroups = new();
|
|
|
|
// 点点距临时状态
|
|
private Ellipse _pendingDot;
|
|
private Point? _pendingPoint;
|
|
|
|
// 点线距临时状态
|
|
private int _ptlClickCount;
|
|
private Ellipse _ptlTempDot1, _ptlTempDot2;
|
|
private Line _ptlTempLine;
|
|
private Point? _ptlTempL1, _ptlTempL2;
|
|
|
|
// 拖拽状态
|
|
private Ellipse _mDraggingDot;
|
|
private object _mDraggingOwner; // MeasureGroup 或 PointToLineGroup
|
|
private string _mDraggingRole; // "Dot1","Dot2","DotL1","DotL2","DotP"
|
|
|
|
private static void OnMeasureModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
var c = (PolygonRoiCanvas)d;
|
|
var newMode = (Models.MeasureMode)e.NewValue;
|
|
if (newMode != Models.MeasureMode.None)
|
|
{
|
|
c.EnsureMeasureOverlay();
|
|
}
|
|
else
|
|
{
|
|
// 退出测量模式:清除未完成的临时元素
|
|
c.ClearPendingElements();
|
|
}
|
|
}
|
|
|
|
private void ClearPendingElements()
|
|
{
|
|
if (_measureOverlay == null) return;
|
|
if (_pendingDot != null) { _measureOverlay.Children.Remove(_pendingDot); _pendingDot = null; }
|
|
_pendingPoint = null;
|
|
if (_ptlTempDot1 != null) { _measureOverlay.Children.Remove(_ptlTempDot1); _ptlTempDot1 = null; }
|
|
if (_ptlTempDot2 != null) { _measureOverlay.Children.Remove(_ptlTempDot2); _ptlTempDot2 = null; }
|
|
if (_ptlTempLine != null) { _measureOverlay.Children.Remove(_ptlTempLine); _ptlTempLine = null; }
|
|
_ptlTempL1 = _ptlTempL2 = null;
|
|
_ptlClickCount = 0;
|
|
}
|
|
|
|
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; }
|
|
_ppGroups.Clear();
|
|
_ptlGroups.Clear();
|
|
_pendingDot = null; _pendingPoint = null;
|
|
_ptlTempDot1 = _ptlTempDot2 = null; _ptlTempLine = null;
|
|
_ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0;
|
|
_mDraggingDot = null; _mDraggingOwner = null;
|
|
}
|
|
|
|
public void ClearMeasurements() => RemoveMeasureOverlay();
|
|
public int MeasureCount => _ppGroups.Count + _ptlGroups.Count;
|
|
|
|
// ── 点击分发 ──
|
|
|
|
private void HandleMeasureClick(Point pos)
|
|
{
|
|
if (_measureOverlay == null) EnsureMeasureOverlay();
|
|
if (_measureOverlay == null) return;
|
|
|
|
if (CurrentMeasureMode == Models.MeasureMode.PointDistance)
|
|
HandlePointDistanceClick(pos);
|
|
else if (CurrentMeasureMode == Models.MeasureMode.PointToLine)
|
|
HandlePointToLineClick(pos);
|
|
}
|
|
|
|
// ── 点点距 ──
|
|
|
|
private void HandlePointDistanceClick(Point pos)
|
|
{
|
|
if (!_pendingPoint.HasValue)
|
|
{
|
|
_pendingPoint = pos;
|
|
_pendingDot = CreateMDot(Brushes.Red);
|
|
_measureOverlay.Children.Add(_pendingDot);
|
|
SetDotPos(_pendingDot, pos);
|
|
RaiseMeasureStatusChanged($"点点距 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点");
|
|
}
|
|
else
|
|
{
|
|
var g = CreatePPGroup(_pendingPoint.Value, pos);
|
|
_ppGroups.Add(g);
|
|
_measureOverlay.Children.Remove(_pendingDot);
|
|
_pendingDot = null; _pendingPoint = null;
|
|
RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance");
|
|
CurrentMeasureMode = Models.MeasureMode.None;
|
|
}
|
|
}
|
|
|
|
private Models.MeasureGroup CreatePPGroup(Point p1, Point p2)
|
|
{
|
|
var g = new Models.MeasureGroup { P1 = p1, P2 = p2 };
|
|
g.Line = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false };
|
|
g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false };
|
|
g.Dot1 = CreateMDot(Brushes.Red);
|
|
g.Dot2 = CreateMDot(Brushes.Blue);
|
|
foreach (UIElement el in new UIElement[] { g.Line, g.Label, g.Dot1, g.Dot2 })
|
|
_measureOverlay.Children.Add(el);
|
|
SetDotPos(g.Dot1, p1); SetDotPos(g.Dot2, p2);
|
|
g.UpdateLine(); g.UpdateLabel(FormatDistance(g.Distance));
|
|
return g;
|
|
}
|
|
|
|
// ── 点线距 ──
|
|
|
|
private void HandlePointToLineClick(Point pos)
|
|
{
|
|
_ptlClickCount++;
|
|
|
|
if (_ptlClickCount == 1)
|
|
{
|
|
_ptlTempL1 = pos;
|
|
_ptlTempDot1 = CreateMDot(Brushes.Lime);
|
|
_measureOverlay.Children.Add(_ptlTempDot1);
|
|
SetDotPos(_ptlTempDot1, pos);
|
|
RaiseMeasureStatusChanged($"点线距 - 直线端点1: ({pos.X:F0}, {pos.Y:F0}),请点击直线端点2");
|
|
}
|
|
else if (_ptlClickCount == 2)
|
|
{
|
|
_ptlTempL2 = pos;
|
|
_ptlTempDot2 = CreateMDot(Brushes.Lime);
|
|
_measureOverlay.Children.Add(_ptlTempDot2);
|
|
SetDotPos(_ptlTempDot2, pos);
|
|
_ptlTempLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false,
|
|
X1 = _ptlTempL1.Value.X, Y1 = _ptlTempL1.Value.Y, X2 = pos.X, Y2 = pos.Y };
|
|
_measureOverlay.Children.Add(_ptlTempLine);
|
|
RaiseMeasureStatusChanged($"点线距 - 直线已定义,请点击测量点");
|
|
}
|
|
else if (_ptlClickCount == 3)
|
|
{
|
|
// 完成:创建正式组,移除临时元素
|
|
var g = CreatePTLGroup(_ptlTempL1.Value, _ptlTempL2.Value, pos);
|
|
_ptlGroups.Add(g);
|
|
|
|
if (_ptlTempDot1 != null) _measureOverlay.Children.Remove(_ptlTempDot1);
|
|
if (_ptlTempDot2 != null) _measureOverlay.Children.Remove(_ptlTempDot2);
|
|
if (_ptlTempLine != null) _measureOverlay.Children.Remove(_ptlTempLine);
|
|
_ptlTempDot1 = _ptlTempDot2 = null; _ptlTempLine = null;
|
|
_ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0;
|
|
|
|
var foot = g.FootPoint;
|
|
RaiseMeasureCompleted(g.P, foot, g.Distance, MeasureCount, "PointToLine");
|
|
CurrentMeasureMode = Models.MeasureMode.None;
|
|
}
|
|
}
|
|
|
|
private Models.PointToLineGroup CreatePTLGroup(Point l1, Point l2, Point p)
|
|
{
|
|
var g = new Models.PointToLineGroup { L1 = l1, L2 = l2, P = p };
|
|
g.MainLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false };
|
|
g.ExtLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false,
|
|
StrokeDashArray = new DoubleCollection { 4, 2 }, Visibility = Visibility.Collapsed };
|
|
g.PerpLine = new Line { Stroke = Brushes.Yellow, StrokeThickness = 1, IsHitTestVisible = false,
|
|
StrokeDashArray = new DoubleCollection { 4, 2 } };
|
|
g.FootDot = new Ellipse { Width = 8, Height = 8, Fill = Brushes.Cyan, Stroke = Brushes.White, StrokeThickness = 1, IsHitTestVisible = false };
|
|
g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false };
|
|
g.DotL1 = CreateMDot(Brushes.Lime);
|
|
g.DotL2 = CreateMDot(Brushes.Lime);
|
|
g.DotP = CreateMDot(Brushes.Red);
|
|
foreach (UIElement el in new UIElement[] { g.MainLine, g.ExtLine, g.PerpLine, g.FootDot, g.Label, g.DotL1, g.DotL2, g.DotP })
|
|
_measureOverlay.Children.Add(el);
|
|
SetDotPos(g.DotL1, l1); SetDotPos(g.DotL2, l2); SetDotPos(g.DotP, p);
|
|
g.UpdateVisuals(FormatDistance(g.Distance));
|
|
return g;
|
|
}
|
|
|
|
// ── 共用:圆点创建、定位、拖拽、删除 ──
|
|
|
|
private Ellipse CreateMDot(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 SetDotPos(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 _ppGroups)
|
|
{
|
|
if (g.Dot1 == dot) { _mDraggingOwner = g; _mDraggingRole = "Dot1"; break; }
|
|
if (g.Dot2 == dot) { _mDraggingOwner = g; _mDraggingRole = "Dot2"; break; }
|
|
}
|
|
// 查找点线距组
|
|
if (_mDraggingOwner == null)
|
|
{
|
|
foreach (var g in _ptlGroups)
|
|
{
|
|
if (g.DotL1 == dot) { _mDraggingOwner = g; _mDraggingRole = "DotL1"; break; }
|
|
if (g.DotL2 == dot) { _mDraggingOwner = g; _mDraggingRole = "DotL2"; break; }
|
|
if (g.DotP == dot) { _mDraggingOwner = g; _mDraggingRole = "DotP"; break; }
|
|
}
|
|
}
|
|
if (_mDraggingOwner != null) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; }
|
|
}
|
|
|
|
private void MDot_Move(object sender, MouseEventArgs e)
|
|
{
|
|
if (_mDraggingDot == null || _mDraggingOwner == null || _measureOverlay == null) return;
|
|
if (e.LeftButton != MouseButtonState.Pressed) return;
|
|
var pos = e.GetPosition(_measureOverlay);
|
|
SetDotPos(_mDraggingDot, pos);
|
|
|
|
if (_mDraggingOwner is Models.MeasureGroup ppg)
|
|
{
|
|
if (_mDraggingRole == "Dot1") ppg.P1 = pos; else ppg.P2 = pos;
|
|
ppg.UpdateLine(); ppg.UpdateLabel(FormatDistance(ppg.Distance));
|
|
RaiseMeasureCompleted(ppg.P1, ppg.P2, ppg.Distance, MeasureCount, "PointDistance");
|
|
}
|
|
else if (_mDraggingOwner is Models.PointToLineGroup ptlg)
|
|
{
|
|
if (_mDraggingRole == "DotL1") ptlg.L1 = pos;
|
|
else if (_mDraggingRole == "DotL2") ptlg.L2 = pos;
|
|
else if (_mDraggingRole == "DotP") ptlg.P = pos;
|
|
ptlg.UpdateVisuals(FormatDistance(ptlg.Distance));
|
|
var foot = ptlg.FootPoint;
|
|
RaiseMeasureCompleted(ptlg.P, foot, ptlg.Distance, MeasureCount, "PointToLine");
|
|
}
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void MDot_Up(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (_mDraggingDot != null) { _mDraggingDot.ReleaseMouseCapture(); _mDraggingDot = null; _mDraggingOwner = null; e.Handled = true; }
|
|
}
|
|
|
|
private void MDot_RightClick(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (sender is not Ellipse dot || _measureOverlay == null) return;
|
|
|
|
// 点点距删除
|
|
foreach (var g in _ppGroups)
|
|
{
|
|
if (g.Dot1 == dot || g.Dot2 == dot)
|
|
{
|
|
foreach (var el in new UIElement[] { g.Dot1, g.Dot2, g.Line, g.Label })
|
|
_measureOverlay.Children.Remove(el);
|
|
_ppGroups.Remove(g);
|
|
RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条");
|
|
e.Handled = true; return;
|
|
}
|
|
}
|
|
// 点线距删除
|
|
foreach (var g in _ptlGroups)
|
|
{
|
|
if (g.DotL1 == dot || g.DotL2 == dot || g.DotP == dot)
|
|
{
|
|
foreach (var el in new UIElement[] { g.DotL1, g.DotL2, g.DotP, g.MainLine, g.ExtLine, g.PerpLine, g.FootDot, g.Label })
|
|
_measureOverlay.Children.Remove(el);
|
|
_ptlGroups.Remove(g);
|
|
RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条");
|
|
e.Handled = true; return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 事件 ──
|
|
|
|
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(Point p1, Point p2, double distance, int totalCount, string measureType)
|
|
{
|
|
RaiseEvent(new MeasureCompletedEventArgs(MeasureCompletedEvent, p1, p2, distance, totalCount) { MeasureType = measureType });
|
|
}
|
|
|
|
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()
|
|
{
|
|
// 移除旧的Adorner
|
|
if (currentAdorner != null)
|
|
{
|
|
var adornerLayer = AdornerLayer.GetAdornerLayer(mainCanvas);
|
|
if (adornerLayer != null)
|
|
{
|
|
adornerLayer.Remove(currentAdorner);
|
|
}
|
|
currentAdorner = null;
|
|
}
|
|
|
|
// 为选中的ROI添加Adorner
|
|
if (SelectedROI != null && SelectedROI.IsSelected)
|
|
{
|
|
// 查找对应的UI元素
|
|
var container = FindROIVisual(SelectedROI);
|
|
if (container != null)
|
|
{
|
|
var adornerLayer = AdornerLayer.GetAdornerLayer(mainCanvas);
|
|
if (adornerLayer != null)
|
|
{
|
|
double scaleFactor = 1.0 / ZoomScale;
|
|
|
|
if (SelectedROI is PolygonROI polygonROI)
|
|
{
|
|
currentAdorner = new PolygonAdorner(container, scaleFactor, polygonROI);
|
|
}
|
|
|
|
if (currentAdorner != null)
|
|
{
|
|
adornerLayer.Add(currentAdorner);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private UIElement? FindROIVisual(ROIShape roi)
|
|
{
|
|
// 在ItemsControl中查找对应的视觉元素
|
|
var itemsControl = FindVisualChild<ItemsControl>(mainCanvas);
|
|
if (itemsControl != null)
|
|
{
|
|
for (int i = 0; i < itemsControl.Items.Count; i++)
|
|
{
|
|
if (itemsControl.Items[i] == roi)
|
|
{
|
|
// 尝试获取容器
|
|
var container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as ContentPresenter;
|
|
|
|
// 如果容器还没生成,尝试强制生成
|
|
if (container == null)
|
|
{
|
|
// 强制生成容器
|
|
itemsControl.UpdateLayout();
|
|
container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as ContentPresenter;
|
|
}
|
|
|
|
if (container != null)
|
|
{
|
|
// 查找实际的形状元素(只支持多边形)
|
|
if (roi is PolygonROI)
|
|
{
|
|
return FindVisualChild<Polygon>(container);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private T? FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
|
|
{
|
|
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
|
{
|
|
var child = VisualTreeHelper.GetChild(parent, i);
|
|
if (child is T result)
|
|
{
|
|
return result;
|
|
}
|
|
var childOfChild = FindVisualChild<T>(child);
|
|
if (childOfChild != null)
|
|
{
|
|
return childOfChild;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
#endregion Adorner Management
|
|
|
|
#region Mouse Events
|
|
|
|
private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
|
|
{
|
|
double oldZoom = ZoomScale;
|
|
double newZoom = e.Delta > 0 ? oldZoom * ZoomStep : oldZoom / ZoomStep;
|
|
newZoom = Math.Max(0.1, Math.Min(10.0, newZoom));
|
|
|
|
if (Math.Abs(newZoom - oldZoom) > 0.001)
|
|
{
|
|
ZoomScale = newZoom;
|
|
// RenderTransformOrigin="0.5,0.5" 保证以图像中心等比缩放
|
|
// 拖拽平移偏移保持不变
|
|
}
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
lastMousePosition = e.GetPosition(imageDisplayGrid);
|
|
isDragging = false;
|
|
mainCanvas.CaptureMouse();
|
|
}
|
|
|
|
private void Canvas_MouseMove(object sender, MouseEventArgs e)
|
|
{
|
|
if (e.LeftButton == MouseButtonState.Pressed && mainCanvas.IsMouseCaptured)
|
|
{
|
|
Point currentPosition = e.GetPosition(imageDisplayGrid);
|
|
Vector delta = currentPosition - lastMousePosition;
|
|
|
|
if (delta.Length > 5)
|
|
{
|
|
isDragging = true;
|
|
PanOffsetX += delta.X;
|
|
PanOffsetY += delta.Y;
|
|
lastMousePosition = currentPosition;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
mainCanvas.ReleaseMouseCapture();
|
|
|
|
if (!isDragging)
|
|
{
|
|
Point clickPosition = e.GetPosition(mainCanvas);
|
|
if (IsMeasuring)
|
|
HandleMeasureClick(clickPosition);
|
|
OnCanvasClicked(clickPosition);
|
|
}
|
|
|
|
isDragging = false;
|
|
}
|
|
|
|
private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
// 右键点击完成多边形
|
|
OnRightClick();
|
|
// 不设 e.Handled,让 ContextMenu 正常弹出
|
|
}
|
|
|
|
private void ROI_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
// 选择ROI
|
|
if (sender is FrameworkElement element && element.DataContext is ROIShape roi)
|
|
{
|
|
SelectedROI = roi;
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
#endregion Mouse Events
|
|
|
|
#region Public Methods
|
|
|
|
public void ResetView()
|
|
{
|
|
PanOffsetX = 0;
|
|
PanOffsetY = 0;
|
|
|
|
if (imageDisplayGrid == null || CanvasWidth <= 0 || CanvasHeight <= 0)
|
|
{
|
|
ZoomScale = 1.0;
|
|
return;
|
|
}
|
|
|
|
// 延迟到布局完成后计算,确保 ActualWidth/Height 准确
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
double viewW = imageDisplayGrid.ActualWidth;
|
|
double viewH = imageDisplayGrid.ActualHeight;
|
|
|
|
if (viewW > 0 && viewH > 0)
|
|
{
|
|
ZoomScale = Math.Min(viewW / CanvasWidth, viewH / CanvasHeight);
|
|
}
|
|
else
|
|
{
|
|
ZoomScale = 1.0;
|
|
}
|
|
|
|
PanOffsetX = 0;
|
|
PanOffsetY = 0;
|
|
}), System.Windows.Threading.DispatcherPriority.Render);
|
|
}
|
|
|
|
private void BtnZoomIn_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
double newZoom = ZoomScale * 1.2;
|
|
if (newZoom <= 10.0)
|
|
{
|
|
ZoomScale = newZoom;
|
|
PanOffsetX = 0;
|
|
PanOffsetY = 0;
|
|
}
|
|
}
|
|
|
|
private void BtnZoomOut_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
double newZoom = ZoomScale / 1.2;
|
|
if (newZoom >= 0.1)
|
|
{
|
|
ZoomScale = newZoom;
|
|
PanOffsetX = 0;
|
|
PanOffsetY = 0;
|
|
}
|
|
}
|
|
|
|
private void BtnReset_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
ResetView();
|
|
}
|
|
|
|
#endregion Public Methods
|
|
|
|
#region Events
|
|
|
|
public static readonly RoutedEvent CanvasClickedEvent =
|
|
EventManager.RegisterRoutedEvent(nameof(CanvasClicked), RoutingStrategy.Bubble,
|
|
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
|
|
|
|
public event RoutedEventHandler CanvasClicked
|
|
{
|
|
add { AddHandler(CanvasClickedEvent, value); }
|
|
remove { RemoveHandler(CanvasClickedEvent, value); }
|
|
}
|
|
|
|
protected virtual void OnCanvasClicked(Point position)
|
|
{
|
|
var args = new CanvasClickedEventArgs(CanvasClickedEvent, position);
|
|
RaiseEvent(args);
|
|
}
|
|
|
|
public static readonly RoutedEvent RightClickEvent =
|
|
EventManager.RegisterRoutedEvent(nameof(RightClick), RoutingStrategy.Bubble,
|
|
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
|
|
|
|
public event RoutedEventHandler RightClick
|
|
{
|
|
add { AddHandler(RightClickEvent, value); }
|
|
remove { RemoveHandler(RightClickEvent, value); }
|
|
}
|
|
|
|
protected virtual void OnRightClick()
|
|
{
|
|
RaiseEvent(new RoutedEventArgs(RightClickEvent));
|
|
}
|
|
|
|
#endregion Events
|
|
}
|
|
} |