diff --git a/XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs b/XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs
index 5a811d7..7278450 100644
--- a/XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs
+++ b/XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs
@@ -9,6 +9,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
public Point P2 { get; }
public double Distance { get; }
public int TotalCount { get; }
+ public string MeasureType { get; set; }
public MeasureCompletedEventArgs(RoutedEvent routedEvent, Point p1, Point p2, double distance, int totalCount)
: base(routedEvent)
diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs
index dd87143..c3672a5 100644
--- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs
+++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs
@@ -243,6 +243,42 @@ namespace XP.ImageProcessing.RoiControl.Controls
#endregion Dependency Properties
+ #region Measurement Config
+
+ public static readonly DependencyProperty PixelSizeProperty =
+ DependencyProperty.Register(nameof(PixelSize), typeof(double), typeof(PolygonRoiCanvas),
+ new PropertyMetadata(1.0));
+
+ /// 每像素对应的物理尺寸(= 探测器像素尺寸 / 放大倍率),默认 1.0
+ 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"));
+
+ /// 测量单位,默认 "px",可设为 "mm"/"μm"/"cm"
+ public string MeasureUnit
+ {
+ get => (string)GetValue(MeasureUnitProperty);
+ set => SetValue(MeasureUnitProperty, value);
+ }
+
+ /// 将像素距离转换为物理距离文本
+ 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 =
@@ -288,43 +324,65 @@ namespace XP.ImageProcessing.RoiControl.Controls
#region Measurement
- public static readonly DependencyProperty IsMeasuringProperty =
- DependencyProperty.Register(nameof(IsMeasuring), typeof(bool), typeof(PolygonRoiCanvas),
- new PropertyMetadata(false, OnIsMeasuringChanged));
+ public static readonly DependencyProperty CurrentMeasureModeProperty =
+ DependencyProperty.Register(nameof(CurrentMeasureMode), typeof(Models.MeasureMode), typeof(PolygonRoiCanvas),
+ new PropertyMetadata(Models.MeasureMode.None, OnMeasureModeChanged));
- public bool IsMeasuring
+ public Models.MeasureMode CurrentMeasureMode
{
- get => (bool)GetValue(IsMeasuringProperty);
- set => SetValue(IsMeasuringProperty, value);
+ 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 _measureGroups = new();
+ private readonly System.Collections.Generic.List _ppGroups = new();
+ private readonly System.Collections.Generic.List _ptlGroups = 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)
+ // 点线距临时状态
+ 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;
- if ((bool)e.NewValue)
+ var newMode = (Models.MeasureMode)e.NewValue;
+ if (newMode != Models.MeasureMode.None)
{
c.EnsureMeasureOverlay();
}
else
{
- // 退出测量模式:只清除未完成的临时点,保留已完成的测量线
- if (c._pendingDot != null && c._measureOverlay != null)
- {
- c._measureOverlay.Children.Remove(c._pendingDot);
- c._pendingDot = null;
- }
- c._pendingPoint = null;
+ // 退出测量模式:清除未完成的临时元素
+ 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;
@@ -336,77 +394,134 @@ namespace XP.ImageProcessing.RoiControl.Controls
private void RemoveMeasureOverlay()
{
- if (_measureOverlay != null)
- {
- mainCanvas.Children.Remove(_measureOverlay);
- _measureOverlay = null;
- }
- _measureGroups.Clear();
- _pendingDot = null;
- _pendingPoint = null;
- _mDraggingDot = null;
- _mDraggingGroup = null;
+ 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 void ClearMeasurements() => RemoveMeasureOverlay();
+ public int MeasureCount => _ppGroups.Count + _ptlGroups.Count;
- /// 获取当前测量组数量
- public int MeasureCount => _measureGroups.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 = CreateMeasureDot(Brushes.Red);
+ _pendingDot = CreateMDot(Brushes.Red);
_measureOverlay.Children.Add(_pendingDot);
- SetMeasureDotPos(_pendingDot, pos);
- RaiseMeasureStatusChanged($"点点距测量 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点");
+ SetDotPos(_pendingDot, pos);
+ RaiseMeasureStatusChanged($"点点距 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点");
}
else
{
- var g = CreateMeasureGroup(_pendingPoint.Value, pos);
- _measureGroups.Add(g);
+ var g = CreatePPGroup(_pendingPoint.Value, pos);
+ _ppGroups.Add(g);
_measureOverlay.Children.Remove(_pendingDot);
- _pendingDot = null;
- _pendingPoint = null;
- RaiseMeasureCompleted(g);
-
- // 完成一条测量后自动退出测量模式
- IsMeasuring = false;
+ _pendingDot = null; _pendingPoint = null;
+ RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance");
+ CurrentMeasureMode = Models.MeasureMode.None;
}
}
- 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 };
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();
+ 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 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 };
dot.SetValue(ContextMenuService.IsEnabledProperty, false);
@@ -417,7 +532,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
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.SetTop(dot, pos.Y - dot.Height / 2);
@@ -426,51 +541,87 @@ namespace XP.ImageProcessing.RoiControl.Controls
private void MDot_Down(object sender, MouseButtonEventArgs e)
{
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.Dot2 == dot) { _mDraggingGroup = g; _mDraggingIsDot1 = false; break; }
+ if (g.Dot1 == dot) { _mDraggingOwner = g; _mDraggingRole = "Dot1"; 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)
{
- if (_mDraggingDot == null || _mDraggingGroup == null || _measureOverlay == null) return;
+ if (_mDraggingDot == null || _mDraggingOwner == 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);
+ 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; _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)
{
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 =
EventManager.RegisterRoutedEvent(nameof(MeasureCompleted), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
@@ -481,9 +632,9 @@ namespace XP.ImageProcessing.RoiControl.Controls
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 =
diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs
index c1550d8..2e5670d 100644
--- a/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs
+++ b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs
@@ -32,9 +32,9 @@ namespace XP.ImageProcessing.RoiControl.Models
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.SetTop(Label, (P1.Y + P2.Y) / 2 - 18);
Label.Visibility = Visibility.Visible;
diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs
new file mode 100644
index 0000000..c21bce4
--- /dev/null
+++ b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs
@@ -0,0 +1,9 @@
+namespace XP.ImageProcessing.RoiControl.Models
+{
+ public enum MeasureMode
+ {
+ None,
+ PointDistance,
+ PointToLine
+ }
+}
diff --git a/XP.ImageProcessing.RoiControl/Models/PointToLineGroup.cs b/XP.ImageProcessing.RoiControl/Models/PointToLineGroup.cs
new file mode 100644
index 0000000..29163e4
--- /dev/null
+++ b/XP.ImageProcessing.RoiControl/Models/PointToLineGroup.cs
@@ -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
+{
+ /// 一次点线距测量的所有视觉元素(直线两端点 + 测量点 + 垂线 + 标签)
+ 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);
+ }
+}
diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs
index 96fc3bd..d88fe5f 100644
--- a/XplorePlane/ViewModels/Main/MainViewModel.cs
+++ b/XplorePlane/ViewModels/Main/MainViewModel.cs
@@ -449,7 +449,7 @@ namespace XplorePlane.ViewModels
private void ExecutePointLineDistanceMeasure()
{
_logger.Info("点线距测量功能已触发");
- // TODO: 实现点线距测量逻辑
+ _eventAggregator.GetEvent().Publish(MeasurementToolMode.PointLineDistance);
}
private void ExecuteAngleMeasure()
diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
index 04b621a..bd03651 100644
--- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
+++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
@@ -26,7 +26,8 @@ namespace XplorePlane.Views
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} 条测量";
+ 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) =>
@@ -47,7 +48,12 @@ namespace XplorePlane.Views
// 测量模式:直接订阅 Prism 事件
ea?.GetEvent().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);
}
catch { }
@@ -63,12 +69,7 @@ namespace XplorePlane.Views
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
{
- if (sender is not ViewportPanelViewModel vm) return;
-
- if (e.PropertyName == nameof(ViewportPanelViewModel.CurrentMeasurementMode))
- {
- RoiCanvas.IsMeasuring = vm.CurrentMeasurementMode != MeasurementToolMode.None;
- }
+ // 测量模式和十字线通过 Prism 事件直接驱动,不再依赖 PropertyChanged
}
#region 右键菜单