点线距测量工具实现
This commit is contained in:
@@ -9,6 +9,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
|||||||
public Point P2 { get; }
|
public Point P2 { get; }
|
||||||
public double Distance { get; }
|
public double Distance { get; }
|
||||||
public int TotalCount { get; }
|
public int TotalCount { get; }
|
||||||
|
public string MeasureType { get; set; }
|
||||||
|
|
||||||
public MeasureCompletedEventArgs(RoutedEvent routedEvent, Point p1, Point p2, double distance, int totalCount)
|
public MeasureCompletedEventArgs(RoutedEvent routedEvent, Point p1, Point p2, double distance, int totalCount)
|
||||||
: base(routedEvent)
|
: base(routedEvent)
|
||||||
|
|||||||
@@ -243,6 +243,42 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
|||||||
|
|
||||||
#endregion Dependency Properties
|
#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
|
#region Crosshair
|
||||||
|
|
||||||
public static readonly DependencyProperty ShowCrosshairProperty =
|
public static readonly DependencyProperty ShowCrosshairProperty =
|
||||||
@@ -288,41 +324,63 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
|||||||
|
|
||||||
#region Measurement
|
#region Measurement
|
||||||
|
|
||||||
public static readonly DependencyProperty IsMeasuringProperty =
|
public static readonly DependencyProperty CurrentMeasureModeProperty =
|
||||||
DependencyProperty.Register(nameof(IsMeasuring), typeof(bool), typeof(PolygonRoiCanvas),
|
DependencyProperty.Register(nameof(CurrentMeasureMode), typeof(Models.MeasureMode), typeof(PolygonRoiCanvas),
|
||||||
new PropertyMetadata(false, OnIsMeasuringChanged));
|
new PropertyMetadata(Models.MeasureMode.None, OnMeasureModeChanged));
|
||||||
|
|
||||||
public bool IsMeasuring
|
public Models.MeasureMode CurrentMeasureMode
|
||||||
{
|
{
|
||||||
get => (bool)GetValue(IsMeasuringProperty);
|
get => (Models.MeasureMode)GetValue(CurrentMeasureModeProperty);
|
||||||
set => SetValue(IsMeasuringProperty, value);
|
set => SetValue(CurrentMeasureModeProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保留 IsMeasuring 作为便捷属性
|
||||||
|
public bool IsMeasuring => CurrentMeasureMode != Models.MeasureMode.None;
|
||||||
|
|
||||||
private Canvas _measureOverlay;
|
private Canvas _measureOverlay;
|
||||||
private readonly System.Collections.Generic.List<Models.MeasureGroup> _measureGroups = new();
|
private readonly System.Collections.Generic.List<Models.MeasureGroup> _ppGroups = new();
|
||||||
|
private readonly System.Collections.Generic.List<Models.PointToLineGroup> _ptlGroups = new();
|
||||||
|
|
||||||
|
// 点点距临时状态
|
||||||
private Ellipse _pendingDot;
|
private Ellipse _pendingDot;
|
||||||
private Point? _pendingPoint;
|
private Point? _pendingPoint;
|
||||||
private Ellipse _mDraggingDot;
|
|
||||||
private Models.MeasureGroup _mDraggingGroup;
|
|
||||||
private bool _mDraggingIsDot1;
|
|
||||||
|
|
||||||
private static void OnIsMeasuringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
// 点线距临时状态
|
||||||
|
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 c = (PolygonRoiCanvas)d;
|
||||||
if ((bool)e.NewValue)
|
var newMode = (Models.MeasureMode)e.NewValue;
|
||||||
|
if (newMode != Models.MeasureMode.None)
|
||||||
{
|
{
|
||||||
c.EnsureMeasureOverlay();
|
c.EnsureMeasureOverlay();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 退出测量模式:只清除未完成的临时点,保留已完成的测量线
|
// 退出测量模式:清除未完成的临时元素
|
||||||
if (c._pendingDot != null && c._measureOverlay != null)
|
c.ClearPendingElements();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearPendingElements()
|
||||||
{
|
{
|
||||||
c._measureOverlay.Children.Remove(c._pendingDot);
|
if (_measureOverlay == null) return;
|
||||||
c._pendingDot = null;
|
if (_pendingDot != null) { _measureOverlay.Children.Remove(_pendingDot); _pendingDot = null; }
|
||||||
}
|
_pendingPoint = null;
|
||||||
c._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()
|
private void EnsureMeasureOverlay()
|
||||||
@@ -336,77 +394,134 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
|||||||
|
|
||||||
private void RemoveMeasureOverlay()
|
private void RemoveMeasureOverlay()
|
||||||
{
|
{
|
||||||
if (_measureOverlay != null)
|
if (_measureOverlay != null) { mainCanvas.Children.Remove(_measureOverlay); _measureOverlay = null; }
|
||||||
{
|
_ppGroups.Clear();
|
||||||
mainCanvas.Children.Remove(_measureOverlay);
|
_ptlGroups.Clear();
|
||||||
_measureOverlay = null;
|
_pendingDot = null; _pendingPoint = null;
|
||||||
}
|
_ptlTempDot1 = _ptlTempDot2 = null; _ptlTempLine = null;
|
||||||
_measureGroups.Clear();
|
_ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0;
|
||||||
_pendingDot = null;
|
_mDraggingDot = null; _mDraggingOwner = null;
|
||||||
_pendingPoint = null;
|
|
||||||
_mDraggingDot = null;
|
|
||||||
_mDraggingGroup = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>清除所有测量绘制</summary>
|
public void ClearMeasurements() => RemoveMeasureOverlay();
|
||||||
public void ClearMeasurements()
|
public int MeasureCount => _ppGroups.Count + _ptlGroups.Count;
|
||||||
{
|
|
||||||
RemoveMeasureOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>获取当前测量组数量</summary>
|
// ── 点击分发 ──
|
||||||
public int MeasureCount => _measureGroups.Count;
|
|
||||||
|
|
||||||
/// <summary>处理测量模式下的画布点击</summary>
|
|
||||||
private void HandleMeasureClick(Point pos)
|
private void HandleMeasureClick(Point pos)
|
||||||
{
|
{
|
||||||
if (_measureOverlay == null) EnsureMeasureOverlay();
|
if (_measureOverlay == null) EnsureMeasureOverlay();
|
||||||
if (_measureOverlay == null) return;
|
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)
|
if (!_pendingPoint.HasValue)
|
||||||
{
|
{
|
||||||
_pendingPoint = pos;
|
_pendingPoint = pos;
|
||||||
_pendingDot = CreateMeasureDot(Brushes.Red);
|
_pendingDot = CreateMDot(Brushes.Red);
|
||||||
_measureOverlay.Children.Add(_pendingDot);
|
_measureOverlay.Children.Add(_pendingDot);
|
||||||
SetMeasureDotPos(_pendingDot, pos);
|
SetDotPos(_pendingDot, pos);
|
||||||
RaiseMeasureStatusChanged($"点点距测量 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点");
|
RaiseMeasureStatusChanged($"点点距 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var g = CreateMeasureGroup(_pendingPoint.Value, pos);
|
var g = CreatePPGroup(_pendingPoint.Value, pos);
|
||||||
_measureGroups.Add(g);
|
_ppGroups.Add(g);
|
||||||
_measureOverlay.Children.Remove(_pendingDot);
|
_measureOverlay.Children.Remove(_pendingDot);
|
||||||
_pendingDot = null;
|
_pendingDot = null; _pendingPoint = null;
|
||||||
_pendingPoint = null;
|
RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance");
|
||||||
RaiseMeasureCompleted(g);
|
CurrentMeasureMode = Models.MeasureMode.None;
|
||||||
|
|
||||||
// 完成一条测量后自动退出测量模式
|
|
||||||
IsMeasuring = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Models.MeasureGroup CreateMeasureGroup(Point p1, Point p2)
|
private Models.MeasureGroup CreatePPGroup(Point p1, Point p2)
|
||||||
{
|
{
|
||||||
var g = new Models.MeasureGroup { P1 = p1, P2 = p2 };
|
var g = new Models.MeasureGroup { P1 = p1, P2 = p2 };
|
||||||
g.Line = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false };
|
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 };
|
g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false };
|
||||||
_measureOverlay.Children.Add(g.Label);
|
g.Dot1 = CreateMDot(Brushes.Red);
|
||||||
|
g.Dot2 = CreateMDot(Brushes.Blue);
|
||||||
g.Dot1 = CreateMeasureDot(Brushes.Red);
|
foreach (UIElement el in new UIElement[] { g.Line, g.Label, g.Dot1, g.Dot2 })
|
||||||
g.Dot2 = CreateMeasureDot(Brushes.Blue);
|
_measureOverlay.Children.Add(el);
|
||||||
_measureOverlay.Children.Add(g.Dot1);
|
SetDotPos(g.Dot1, p1); SetDotPos(g.Dot2, p2);
|
||||||
_measureOverlay.Children.Add(g.Dot2);
|
g.UpdateLine(); g.UpdateLabel(FormatDistance(g.Distance));
|
||||||
|
|
||||||
SetMeasureDotPos(g.Dot1, p1);
|
|
||||||
SetMeasureDotPos(g.Dot2, p2);
|
|
||||||
g.UpdateLine();
|
|
||||||
g.UpdateLabel();
|
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Ellipse CreateMeasureDot(Brush fill)
|
// ── 点线距 ──
|
||||||
|
|
||||||
|
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 };
|
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.SetValue(ContextMenuService.IsEnabledProperty, false);
|
||||||
@@ -417,7 +532,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
|||||||
return dot;
|
return dot;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetMeasureDotPos(Ellipse dot, Point pos)
|
private static void SetDotPos(Ellipse dot, Point pos)
|
||||||
{
|
{
|
||||||
Canvas.SetLeft(dot, pos.X - dot.Width / 2);
|
Canvas.SetLeft(dot, pos.X - dot.Width / 2);
|
||||||
Canvas.SetTop(dot, pos.Y - dot.Height / 2);
|
Canvas.SetTop(dot, pos.Y - dot.Height / 2);
|
||||||
@@ -426,51 +541,87 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
|||||||
private void MDot_Down(object sender, MouseButtonEventArgs e)
|
private void MDot_Down(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not Ellipse dot) return;
|
if (sender is not Ellipse dot) return;
|
||||||
foreach (var g in _measureGroups)
|
// 查找点点距组
|
||||||
|
foreach (var g in _ppGroups)
|
||||||
{
|
{
|
||||||
if (g.Dot1 == dot) { _mDraggingGroup = g; _mDraggingIsDot1 = true; break; }
|
if (g.Dot1 == dot) { _mDraggingOwner = g; _mDraggingRole = "Dot1"; break; }
|
||||||
if (g.Dot2 == dot) { _mDraggingGroup = g; _mDraggingIsDot1 = false; break; }
|
if (g.Dot2 == dot) { _mDraggingOwner = g; _mDraggingRole = "Dot2"; break; }
|
||||||
}
|
}
|
||||||
if (_mDraggingGroup != null) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; }
|
// 查找点线距组
|
||||||
|
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)
|
private void MDot_Move(object sender, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
if (_mDraggingDot == null || _mDraggingGroup == null || _measureOverlay == null) return;
|
if (_mDraggingDot == null || _mDraggingOwner == null || _measureOverlay == null) return;
|
||||||
if (e.LeftButton != MouseButtonState.Pressed) return;
|
if (e.LeftButton != MouseButtonState.Pressed) return;
|
||||||
var pos = e.GetPosition(_measureOverlay);
|
var pos = e.GetPosition(_measureOverlay);
|
||||||
SetMeasureDotPos(_mDraggingDot, pos);
|
SetDotPos(_mDraggingDot, pos);
|
||||||
if (_mDraggingIsDot1) _mDraggingGroup.P1 = pos; else _mDraggingGroup.P2 = pos;
|
|
||||||
_mDraggingGroup.UpdateLine();
|
if (_mDraggingOwner is Models.MeasureGroup ppg)
|
||||||
_mDraggingGroup.UpdateLabel();
|
{
|
||||||
RaiseMeasureCompleted(_mDraggingGroup);
|
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;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MDot_Up(object sender, MouseButtonEventArgs e)
|
private void MDot_Up(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
if (_mDraggingDot != null) { _mDraggingDot.ReleaseMouseCapture(); _mDraggingDot = null; _mDraggingGroup = null; e.Handled = true; }
|
if (_mDraggingDot != null) { _mDraggingDot.ReleaseMouseCapture(); _mDraggingDot = null; _mDraggingOwner = null; e.Handled = true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MDot_RightClick(object sender, MouseButtonEventArgs e)
|
private void MDot_RightClick(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not Ellipse dot || _measureOverlay == null) return;
|
if (sender is not Ellipse dot || _measureOverlay == null) return;
|
||||||
Models.MeasureGroup target = null;
|
|
||||||
foreach (var g in _measureGroups)
|
// 点点距删除
|
||||||
|
foreach (var g in _ppGroups)
|
||||||
{
|
{
|
||||||
if (g.Dot1 == dot || g.Dot2 == dot) { target = g; break; }
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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 =
|
public static readonly RoutedEvent MeasureCompletedEvent =
|
||||||
EventManager.RegisterRoutedEvent(nameof(MeasureCompleted), RoutingStrategy.Bubble,
|
EventManager.RegisterRoutedEvent(nameof(MeasureCompleted), RoutingStrategy.Bubble,
|
||||||
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
|
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
|
||||||
@@ -481,9 +632,9 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
|||||||
remove { RemoveHandler(MeasureCompletedEvent, value); }
|
remove { RemoveHandler(MeasureCompletedEvent, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RaiseMeasureCompleted(Models.MeasureGroup g)
|
private void RaiseMeasureCompleted(Point p1, Point p2, double distance, int totalCount, string measureType)
|
||||||
{
|
{
|
||||||
RaiseEvent(new MeasureCompletedEventArgs(MeasureCompletedEvent, g.P1, g.P2, g.Distance, _measureGroups.Count));
|
RaiseEvent(new MeasureCompletedEventArgs(MeasureCompletedEvent, p1, p2, distance, totalCount) { MeasureType = measureType });
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly RoutedEvent MeasureStatusChangedEvent =
|
public static readonly RoutedEvent MeasureStatusChangedEvent =
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ namespace XP.ImageProcessing.RoiControl.Models
|
|||||||
Line.Visibility = Visibility.Visible;
|
Line.Visibility = Visibility.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateLabel(string unit = "px")
|
public void UpdateLabel(string distanceText = null)
|
||||||
{
|
{
|
||||||
Label.Text = $"{Distance:F2} {unit}";
|
Label.Text = distanceText ?? $"{Distance:F2} px";
|
||||||
Canvas.SetLeft(Label, (P1.X + P2.X) / 2 + 8);
|
Canvas.SetLeft(Label, (P1.X + P2.X) / 2 + 8);
|
||||||
Canvas.SetTop(Label, (P1.Y + P2.Y) / 2 - 18);
|
Canvas.SetTop(Label, (P1.Y + P2.Y) / 2 - 18);
|
||||||
Label.Visibility = Visibility.Visible;
|
Label.Visibility = Visibility.Visible;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace XP.ImageProcessing.RoiControl.Models
|
||||||
|
{
|
||||||
|
public enum MeasureMode
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
PointDistance,
|
||||||
|
PointToLine
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
|
||||||
|
namespace XP.ImageProcessing.RoiControl.Models
|
||||||
|
{
|
||||||
|
/// <summary>一次点线距测量的所有视觉元素(直线两端点 + 测量点 + 垂线 + 标签)</summary>
|
||||||
|
public class PointToLineGroup
|
||||||
|
{
|
||||||
|
public Ellipse DotL1 { get; set; } // 直线端点1
|
||||||
|
public Ellipse DotL2 { get; set; } // 直线端点2
|
||||||
|
public Ellipse DotP { get; set; } // 测量点
|
||||||
|
public Line MainLine { get; set; } // 原始线段(实线)
|
||||||
|
public Line ExtLine { get; set; } // 延长线(虚线)
|
||||||
|
public Line PerpLine { get; set; } // 垂线(测量点→垂足)
|
||||||
|
public Ellipse FootDot { get; set; } // 垂足
|
||||||
|
public TextBlock Label { get; set; }
|
||||||
|
public Point L1 { get; set; }
|
||||||
|
public Point L2 { get; set; }
|
||||||
|
public Point P { get; set; }
|
||||||
|
|
||||||
|
public double Distance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
double abx = L2.X - L1.X, aby = L2.Y - L1.Y;
|
||||||
|
double abLen = Math.Sqrt(abx * abx + aby * aby);
|
||||||
|
if (abLen < 0.001) return 0;
|
||||||
|
return Math.Abs(abx * (L1.Y - P.Y) - aby * (L1.X - P.X)) / abLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Point FootPoint
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
double abx = L2.X - L1.X, aby = L2.Y - L1.Y;
|
||||||
|
double abLen2 = abx * abx + aby * aby;
|
||||||
|
if (abLen2 < 0.001) return L1;
|
||||||
|
double t = ((P.X - L1.X) * abx + (P.Y - L1.Y) * aby) / abLen2;
|
||||||
|
return new Point(L1.X + t * abx, L1.Y + t * aby);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateVisuals(string distanceText)
|
||||||
|
{
|
||||||
|
double abx = L2.X - L1.X, aby = L2.Y - L1.Y;
|
||||||
|
double abLen2 = abx * abx + aby * aby;
|
||||||
|
double t = abLen2 < 0.001 ? 0 : ((P.X - L1.X) * abx + (P.Y - L1.Y) * aby) / abLen2;
|
||||||
|
var foot = FootPoint;
|
||||||
|
|
||||||
|
// 主直线:始终画原始线段
|
||||||
|
MainLine.X1 = L1.X; MainLine.Y1 = L1.Y;
|
||||||
|
MainLine.X2 = L2.X; MainLine.Y2 = L2.Y;
|
||||||
|
MainLine.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
// 延长线:垂足在线段外时画虚线延伸
|
||||||
|
if (t < 0)
|
||||||
|
{
|
||||||
|
ExtLine.X1 = foot.X; ExtLine.Y1 = foot.Y;
|
||||||
|
ExtLine.X2 = L1.X; ExtLine.Y2 = L1.Y;
|
||||||
|
ExtLine.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
else if (t > 1)
|
||||||
|
{
|
||||||
|
ExtLine.X1 = L2.X; ExtLine.Y1 = L2.Y;
|
||||||
|
ExtLine.X2 = foot.X; ExtLine.Y2 = foot.Y;
|
||||||
|
ExtLine.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ExtLine.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 垂线
|
||||||
|
PerpLine.X1 = P.X; PerpLine.Y1 = P.Y;
|
||||||
|
PerpLine.X2 = foot.X; PerpLine.Y2 = foot.Y;
|
||||||
|
PerpLine.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
// 垂足
|
||||||
|
Canvas.SetLeft(FootDot, foot.X - FootDot.Width / 2);
|
||||||
|
Canvas.SetTop(FootDot, foot.Y - FootDot.Height / 2);
|
||||||
|
FootDot.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
Label.Text = distanceText ?? $"{Distance:F2} px";
|
||||||
|
Canvas.SetLeft(Label, (P.X + foot.X) / 2 + 8);
|
||||||
|
Canvas.SetTop(Label, (P.Y + foot.Y) / 2 - 18);
|
||||||
|
Label.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateVisuals() => UpdateVisuals(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -449,7 +449,7 @@ namespace XplorePlane.ViewModels
|
|||||||
private void ExecutePointLineDistanceMeasure()
|
private void ExecutePointLineDistanceMeasure()
|
||||||
{
|
{
|
||||||
_logger.Info("点线距测量功能已触发");
|
_logger.Info("点线距测量功能已触发");
|
||||||
// TODO: 实现点线距测量逻辑
|
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointLineDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteAngleMeasure()
|
private void ExecuteAngleMeasure()
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ namespace XplorePlane.Views
|
|||||||
if (e is MeasureCompletedEventArgs args && DataContext is ViewportPanelViewModel vm)
|
if (e is MeasureCompletedEventArgs args && DataContext is ViewportPanelViewModel vm)
|
||||||
{
|
{
|
||||||
vm.MeasurementResult = $"{args.Distance:F2} px";
|
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} 条测量";
|
string typeLabel = args.MeasureType == "PointToLine" ? "点线距" : "点点距";
|
||||||
|
vm.ImageInfo = $"{typeLabel}: {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) =>
|
RoiCanvas.MeasureStatusChanged += (s, e) =>
|
||||||
@@ -47,7 +48,12 @@ namespace XplorePlane.Views
|
|||||||
// 测量模式:直接订阅 Prism 事件
|
// 测量模式:直接订阅 Prism 事件
|
||||||
ea?.GetEvent<MeasurementToolEvent>().Subscribe(mode =>
|
ea?.GetEvent<MeasurementToolEvent>().Subscribe(mode =>
|
||||||
{
|
{
|
||||||
RoiCanvas.IsMeasuring = mode != MeasurementToolMode.None;
|
RoiCanvas.CurrentMeasureMode = mode switch
|
||||||
|
{
|
||||||
|
MeasurementToolMode.PointDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointDistance,
|
||||||
|
MeasurementToolMode.PointLineDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointToLine,
|
||||||
|
_ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None
|
||||||
|
};
|
||||||
}, Prism.Events.ThreadOption.UIThread);
|
}, Prism.Events.ThreadOption.UIThread);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
@@ -63,12 +69,7 @@ namespace XplorePlane.Views
|
|||||||
|
|
||||||
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
|
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not ViewportPanelViewModel vm) return;
|
// 测量模式和十字线通过 Prism 事件直接驱动,不再依赖 PropertyChanged
|
||||||
|
|
||||||
if (e.PropertyName == nameof(ViewportPanelViewModel.CurrentMeasurementMode))
|
|
||||||
{
|
|
||||||
RoiCanvas.IsMeasuring = vm.CurrentMeasurementMode != MeasurementToolMode.None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region 右键菜单
|
#region 右键菜单
|
||||||
|
|||||||
Reference in New Issue
Block a user