已合并 PR 37: 点点距和点线距功能合并
1.点点距; 2.点线距; 3.十字辅助线;
This commit is contained in:
@@ -1878,7 +1878,10 @@
|
||||
"Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408"
|
||||
},
|
||||
"runtime": {
|
||||
"XP.Common.dll": {}
|
||||
"XP.Common.dll": {
|
||||
"assemblyVersion": "1.4.16.1",
|
||||
"fileVersion": "1.4.16.1"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"en-US/XP.Common.resources.dll": {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl.Controls
|
||||
{
|
||||
/// <summary>测量完成事件参数</summary>
|
||||
public class MeasureCompletedEventArgs : RoutedEventArgs
|
||||
{
|
||||
public Point P1 { get; }
|
||||
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)
|
||||
{
|
||||
P1 = p1; P2 = p2; Distance = distance; TotalCount = totalCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>测量状态变化事件参数</summary>
|
||||
public class MeasureStatusEventArgs : RoutedEventArgs
|
||||
{
|
||||
public string Message { get; }
|
||||
|
||||
public MeasureStatusEventArgs(RoutedEvent routedEvent, string message) : base(routedEvent)
|
||||
{
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,35 +17,20 @@
|
||||
</UserControl.Resources>
|
||||
<Border BorderBrush="Transparent" BorderThickness="1" ClipToBounds="True">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧控制按钮 -->
|
||||
<Border Grid.Column="0" Background="White" Padding="5">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Top">
|
||||
<Button x:Name="btnZoomIn" Content="+" Background="White" BorderBrush="LightGray" Width="40" Height="40" Margin="2" Click="BtnZoomIn_Click" />
|
||||
<Button x:Name="btnZoomOut" Content="-" Background="White" BorderBrush="LightGray" Width="40" Height="40" Margin="2" Click="BtnZoomOut_Click" />
|
||||
<Button x:Name="btnReset" Content="适应" Background="White" BorderBrush="LightGray" Width="40" Height="40" Margin="2" Click="BtnReset_Click" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 图像显示区域 -->
|
||||
<Grid Grid.Column="1" x:Name="imageDisplayGrid" ClipToBounds="True">
|
||||
<Grid x:Name="imageDisplayGrid" ClipToBounds="True">
|
||||
<Grid x:Name="transformGrid"
|
||||
RenderTransformOrigin="0,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<Grid.LayoutTransform>
|
||||
<ScaleTransform x:Name="scaleTransform"
|
||||
ScaleX="{Binding ZoomScale, ElementName=root}"
|
||||
ScaleY="{Binding ZoomScale, ElementName=root}" />
|
||||
</Grid.LayoutTransform>
|
||||
<Grid.RenderTransform>
|
||||
<TranslateTransform x:Name="translateTransform"
|
||||
X="{Binding PanOffsetX, ElementName=root}"
|
||||
Y="{Binding PanOffsetY, ElementName=root}" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
|
||||
<Canvas x:Name="mainCanvas"
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -120,11 +121,23 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
private static void OnImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (PolygonRoiCanvas)d;
|
||||
if (e.NewValue is ImageSource imageSource)
|
||||
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 =
|
||||
@@ -234,6 +247,417 @@ 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));
|
||||
|
||||
/// <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()
|
||||
@@ -334,39 +758,15 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
|
||||
private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
// 获取鼠标在 imageDisplayGrid 中的位置
|
||||
Point mousePos = e.GetPosition(imageDisplayGrid);
|
||||
|
||||
// 获取鼠标在 Canvas 中的位置(缩放前)
|
||||
Point mousePosOnCanvas = e.GetPosition(mainCanvas);
|
||||
|
||||
double oldZoom = ZoomScale;
|
||||
double newZoom = oldZoom;
|
||||
|
||||
if (e.Delta > 0)
|
||||
{
|
||||
newZoom = oldZoom * ZoomStep;
|
||||
}
|
||||
else
|
||||
{
|
||||
newZoom = oldZoom / ZoomStep;
|
||||
}
|
||||
|
||||
// 限制缩放范围
|
||||
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)
|
||||
{
|
||||
// 计算缩放比例变化
|
||||
double scale = newZoom / oldZoom;
|
||||
|
||||
// 更新缩放
|
||||
ZoomScale = newZoom;
|
||||
|
||||
// 调整平移偏移,使鼠标位置保持不变
|
||||
// 新的偏移 = 旧偏移 + 鼠标位置 - 鼠标位置 * 缩放比例
|
||||
PanOffsetX = mousePos.X - (mousePos.X - PanOffsetX) * scale;
|
||||
PanOffsetY = mousePos.Y - (mousePos.Y - PanOffsetY) * scale;
|
||||
// RenderTransformOrigin="0.5,0.5" 保证以图像中心等比缩放
|
||||
// 拖拽平移偏移保持不变
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
@@ -402,8 +802,9 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
|
||||
if (!isDragging)
|
||||
{
|
||||
// 处理点击事件
|
||||
Point clickPosition = e.GetPosition(mainCanvas);
|
||||
if (IsMeasuring)
|
||||
HandleMeasureClick(clickPosition);
|
||||
OnCanvasClicked(clickPosition);
|
||||
}
|
||||
|
||||
@@ -414,7 +815,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
{
|
||||
// 右键点击完成多边形
|
||||
OnRightClick();
|
||||
e.Handled = true;
|
||||
// 不设 e.Handled,让 ContextMenu 正常弹出
|
||||
}
|
||||
|
||||
private void ROI_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
@@ -433,35 +834,33 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
|
||||
public void ResetView()
|
||||
{
|
||||
// 自动适应显示窗口 (类似 PictureBox SizeMode.Zoom)
|
||||
ZoomScale = 1.0;
|
||||
PanOffsetX = 0;
|
||||
PanOffsetY = 0;
|
||||
|
||||
if (imageDisplayGrid != null && CanvasWidth > 0 && CanvasHeight > 0)
|
||||
if (imageDisplayGrid == null || CanvasWidth <= 0 || CanvasHeight <= 0)
|
||||
{
|
||||
// 使用 Dispatcher 延迟执行,确保布局已完成
|
||||
ZoomScale = 1.0;
|
||||
return;
|
||||
}
|
||||
|
||||
// 延迟到布局完成后计算,确保 ActualWidth/Height 准确
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
// 获取图像显示区域的实际尺寸
|
||||
double viewportWidth = imageDisplayGrid.ActualWidth;
|
||||
double viewportHeight = imageDisplayGrid.ActualHeight;
|
||||
double viewW = imageDisplayGrid.ActualWidth;
|
||||
double viewH = imageDisplayGrid.ActualHeight;
|
||||
|
||||
if (viewportWidth > 0 && viewportHeight > 0)
|
||||
if (viewW > 0 && viewH > 0)
|
||||
{
|
||||
// 计算宽度和高度的缩放比例
|
||||
double scaleX = viewportWidth / CanvasWidth;
|
||||
double scaleY = viewportHeight / CanvasHeight;
|
||||
ZoomScale = Math.Min(viewW / CanvasWidth, viewH / CanvasHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
ZoomScale = 1.0;
|
||||
}
|
||||
|
||||
// 选择较小的缩放比例,确保图像完全显示在窗口内(保持宽高比)
|
||||
ZoomScale = Math.Min(scaleX, scaleY);
|
||||
|
||||
// 居中显示由 Grid 的 HorizontalAlignment 和 VerticalAlignment 自动处理
|
||||
PanOffsetX = 0;
|
||||
PanOffsetY = 0;
|
||||
}
|
||||
}), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
}), System.Windows.Threading.DispatcherPriority.Render);
|
||||
}
|
||||
|
||||
private void BtnZoomIn_Click(object sender, RoutedEventArgs e)
|
||||
@@ -470,6 +869,8 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
if (newZoom <= 10.0)
|
||||
{
|
||||
ZoomScale = newZoom;
|
||||
PanOffsetX = 0;
|
||||
PanOffsetY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,6 +880,8 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
if (newZoom >= 0.1)
|
||||
{
|
||||
ZoomScale = newZoom;
|
||||
PanOffsetX = 0;
|
||||
PanOffsetY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>一次点点距测量的所有视觉元素</summary>
|
||||
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 distanceText = null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -265,23 +265,23 @@ namespace XplorePlane
|
||||
// 主窗口加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化
|
||||
shell.Loaded += (s, e) =>
|
||||
{
|
||||
TryConnectCamera();
|
||||
//TryConnectCamera();
|
||||
|
||||
// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误
|
||||
try
|
||||
{
|
||||
var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>();
|
||||
cameraVm.OnCameraReady();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "通知相机 ViewModel 失败");
|
||||
}
|
||||
//// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误
|
||||
// try
|
||||
// {
|
||||
// var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>();
|
||||
// cameraVm.OnCameraReady();
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Error(ex, "通知相机 ViewModel 失败");
|
||||
// }
|
||||
|
||||
if (_cameraError != null)
|
||||
{
|
||||
HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
// if (_cameraError != null)
|
||||
// {
|
||||
// HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
//}
|
||||
};
|
||||
|
||||
return shell;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Prism.Events;
|
||||
|
||||
namespace XplorePlane.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// 测量工具模式
|
||||
/// </summary>
|
||||
public enum MeasurementToolMode
|
||||
{
|
||||
None,
|
||||
PointDistance,
|
||||
PointLineDistance,
|
||||
Angle,
|
||||
ThroughHoleFillRate
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测量工具激活事件,由 MainViewModel 发布,ViewportPanelViewModel 订阅
|
||||
/// </summary>
|
||||
public class MeasurementToolEvent : PubSubEvent<MeasurementToolMode> { }
|
||||
|
||||
/// <summary>
|
||||
/// 十字辅助线切换事件
|
||||
/// </summary>
|
||||
public class ToggleCrosshairEvent : PubSubEvent { }
|
||||
}
|
||||
@@ -75,6 +75,15 @@ namespace XplorePlane.ViewModels
|
||||
public DelegateCommand OpenRaySourceConfigCommand { get; }
|
||||
public DelegateCommand WarmUpCommand { get; }
|
||||
|
||||
// 测量命令
|
||||
public DelegateCommand PointDistanceMeasureCommand { get; }
|
||||
public DelegateCommand PointLineDistanceMeasureCommand { get; }
|
||||
public DelegateCommand AngleMeasureCommand { get; }
|
||||
public DelegateCommand ThroughHoleFillRateMeasureCommand { get; }
|
||||
|
||||
// 辅助线命令
|
||||
public DelegateCommand ToggleCrosshairCommand { get; }
|
||||
|
||||
// 设置命令
|
||||
public DelegateCommand OpenLanguageSwitcherCommand { get; }
|
||||
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
|
||||
@@ -152,6 +161,16 @@ namespace XplorePlane.ViewModels
|
||||
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute()));
|
||||
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute()));
|
||||
|
||||
// 测量命令
|
||||
PointDistanceMeasureCommand = new DelegateCommand(ExecutePointDistanceMeasure);
|
||||
PointLineDistanceMeasureCommand = new DelegateCommand(ExecutePointLineDistanceMeasure);
|
||||
AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure);
|
||||
ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure);
|
||||
|
||||
// 辅助线命令
|
||||
ToggleCrosshairCommand = new DelegateCommand(() =>
|
||||
_eventAggregator.GetEvent<ToggleCrosshairEvent>().Publish());
|
||||
|
||||
// 硬件命令
|
||||
AxisResetCommand = new DelegateCommand(ExecuteAxisReset);
|
||||
OpenDetectorConfigCommand = new DelegateCommand(ExecuteOpenDetectorConfig);
|
||||
@@ -419,6 +438,34 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
#endregion
|
||||
|
||||
#region 测量命令实现
|
||||
|
||||
private void ExecutePointDistanceMeasure()
|
||||
{
|
||||
_logger.Info("点点距测量功能已触发");
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointDistance);
|
||||
}
|
||||
|
||||
private void ExecutePointLineDistanceMeasure()
|
||||
{
|
||||
_logger.Info("点线距测量功能已触发");
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointLineDistance);
|
||||
}
|
||||
|
||||
private void ExecuteAngleMeasure()
|
||||
{
|
||||
_logger.Info("角度测量功能已触发");
|
||||
// TODO: 实现角度测量逻辑
|
||||
}
|
||||
|
||||
private void ExecuteThroughHoleFillRateMeasure()
|
||||
{
|
||||
_logger.Info("通孔填锡率测量功能已触发");
|
||||
// TODO: 实现通孔填锡率测量逻辑
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 设置命令实现
|
||||
|
||||
private void ExecuteOpenLanguageSwitcher()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
@@ -18,6 +19,7 @@ namespace XplorePlane.ViewModels
|
||||
public class ViewportPanelViewModel : BindableBase
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private int _isProcessingFrame;
|
||||
|
||||
private ImageSource _imageSource;
|
||||
@@ -34,9 +36,116 @@ namespace XplorePlane.ViewModels
|
||||
set => SetProperty(ref _imageInfo, value);
|
||||
}
|
||||
|
||||
#region 测量工具状态
|
||||
|
||||
private MeasurementToolMode _currentMeasurementMode = MeasurementToolMode.None;
|
||||
public MeasurementToolMode CurrentMeasurementMode
|
||||
{
|
||||
get => _currentMeasurementMode;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _currentMeasurementMode, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(IsMeasuring));
|
||||
RaisePropertyChanged(nameof(MeasurementModeText));
|
||||
// 切换模式时重置状态
|
||||
ResetMeasurementState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMeasuring => CurrentMeasurementMode != MeasurementToolMode.None;
|
||||
|
||||
public string MeasurementModeText => CurrentMeasurementMode switch
|
||||
{
|
||||
MeasurementToolMode.PointDistance => "点点距测量 - 请在图像上点击第一个点",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
// 测量点坐标(图像像素坐标)
|
||||
private Point? _measurePoint1;
|
||||
public Point? MeasurePoint1
|
||||
{
|
||||
get => _measurePoint1;
|
||||
set => SetProperty(ref _measurePoint1, value);
|
||||
}
|
||||
|
||||
private Point? _measurePoint2;
|
||||
public Point? MeasurePoint2
|
||||
{
|
||||
get => _measurePoint2;
|
||||
set => SetProperty(ref _measurePoint2, value);
|
||||
}
|
||||
|
||||
private string _measurementResult;
|
||||
public string MeasurementResult
|
||||
{
|
||||
get => _measurementResult;
|
||||
set => SetProperty(ref _measurementResult, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 由 View 层调用:用户在画布上点击了一个点(像素坐标)
|
||||
/// </summary>
|
||||
public void OnMeasurementPointClicked(Point imagePoint)
|
||||
{
|
||||
if (CurrentMeasurementMode == MeasurementToolMode.PointDistance)
|
||||
{
|
||||
if (MeasurePoint1 == null)
|
||||
{
|
||||
MeasurePoint1 = imagePoint;
|
||||
ImageInfo = $"点点距测量 - 第一点: ({imagePoint.X:F0}, {imagePoint.Y:F0}),请点击第二个点";
|
||||
_logger?.Info("测量第一点: ({X}, {Y})", imagePoint.X, imagePoint.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
MeasurePoint2 = imagePoint;
|
||||
CalculatePointDistance();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CalculatePointDistance()
|
||||
{
|
||||
if (MeasurePoint1 == null || MeasurePoint2 == null) return;
|
||||
|
||||
var p1 = MeasurePoint1.Value;
|
||||
var p2 = MeasurePoint2.Value;
|
||||
double dx = p2.X - p1.X;
|
||||
double dy = p2.Y - p1.Y;
|
||||
double distance = Math.Sqrt(dx * dx + dy * dy);
|
||||
double angle = Math.Atan2(dy, dx) * 180.0 / Math.PI;
|
||||
|
||||
MeasurementResult = $"{distance:F2} px";
|
||||
ImageInfo = $"点点距: {distance:F2} px | 角度: {angle:F2}° | ({p1.X:F0},{p1.Y:F0}) → ({p2.X:F0},{p2.Y:F0})";
|
||||
_logger?.Info("点点距测量完成: {Distance:F2} px, 角度: {Angle:F2}°", distance, angle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消/重置当前测量
|
||||
/// </summary>
|
||||
public DelegateCommand CancelMeasurementCommand { get; private set; }
|
||||
|
||||
public void ResetMeasurementState()
|
||||
{
|
||||
MeasurePoint1 = null;
|
||||
MeasurePoint2 = null;
|
||||
MeasurementResult = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public ViewportPanelViewModel(IEventAggregator eventAggregator, ILoggerService logger)
|
||||
{
|
||||
_logger = logger?.ForModule<ViewportPanelViewModel>();
|
||||
_eventAggregator = eventAggregator;
|
||||
|
||||
CancelMeasurementCommand = new DelegateCommand(() =>
|
||||
{
|
||||
// 发布 None 事件,让 View 层也收到
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
|
||||
ImageInfo = "测量已取消";
|
||||
});
|
||||
|
||||
eventAggregator.GetEvent<ImageCapturedEvent>()
|
||||
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
|
||||
@@ -44,8 +153,39 @@ namespace XplorePlane.ViewModels
|
||||
.Subscribe(OnManualImageLoaded, ThreadOption.UIThread);
|
||||
eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
|
||||
.Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread);
|
||||
|
||||
// 订阅测量工具事件
|
||||
eventAggregator.GetEvent<MeasurementToolEvent>()
|
||||
.Subscribe(OnMeasurementToolActivated, ThreadOption.UIThread);
|
||||
|
||||
// 订阅十字辅助线切换事件
|
||||
eventAggregator.GetEvent<ToggleCrosshairEvent>()
|
||||
.Subscribe(OnToggleCrosshair, ThreadOption.UIThread);
|
||||
}
|
||||
|
||||
private void OnMeasurementToolActivated(MeasurementToolMode mode)
|
||||
{
|
||||
CurrentMeasurementMode = mode;
|
||||
_logger?.Info("测量工具模式切换: {Mode}", mode);
|
||||
}
|
||||
|
||||
#region 十字辅助线
|
||||
|
||||
private bool _showCrosshair;
|
||||
public bool ShowCrosshair
|
||||
{
|
||||
get => _showCrosshair;
|
||||
set => SetProperty(ref _showCrosshair, value);
|
||||
}
|
||||
|
||||
private void OnToggleCrosshair()
|
||||
{
|
||||
ShowCrosshair = !ShowCrosshair;
|
||||
_logger?.Info("十字辅助线: {State}", ShowCrosshair ? "显示" : "隐藏");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnImageCaptured(ImageCapturedEventArgs args)
|
||||
{
|
||||
if (args?.ImageData == null || args.Width == 0 || args.Height == 0) return;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace XplorePlane.Views
|
||||
{
|
||||
/// <summary>返回输入值的一半,用于十字线居中定位</summary>
|
||||
public class HalfValueConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> value is double d ? d / 2.0 : 0.0;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -146,6 +146,7 @@
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="中心十字线"
|
||||
Command="{Binding ToggleCrosshairCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/crosshair.png"
|
||||
Text="辅助线" />
|
||||
@@ -400,6 +401,48 @@
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/spiral.png" />
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<telerik:RadRibbonGroup Header="测量工具">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
|
||||
<!-- 第一列: 点点距 + 点线距 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量两点之间的距离"
|
||||
telerik:ScreenTip.Title="点点距测量"
|
||||
Command="{Binding PointDistanceMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/crosshair.png"
|
||||
Text="点点距测量" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量点到直线的距离"
|
||||
telerik:ScreenTip.Title="点线距测量"
|
||||
Command="{Binding PointLineDistanceMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/mark.png"
|
||||
Text="点线距测量" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 第二列: 角度 + 通孔填锡率 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量两条线之间的角度"
|
||||
telerik:ScreenTip.Title="角度测量"
|
||||
Command="{Binding AngleMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/dynamic-range.png"
|
||||
Text="角度测量" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量通孔填锡率"
|
||||
telerik:ScreenTip.Title="通孔填锡率测量"
|
||||
Command="{Binding ThroughHoleFillRateMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/pores.png"
|
||||
Text="通孔填锡率" />
|
||||
</StackPanel>
|
||||
</telerik:RadRibbonGroup>
|
||||
<!--
|
||||
<telerik:RadRibbonGroup Header="图像处理">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
d:DesignHeight="400"
|
||||
d:DesignWidth="600"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources />
|
||||
<Grid Background="#FFFFFF">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -23,10 +24,27 @@
|
||||
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
|
||||
</Border>
|
||||
|
||||
<!-- 图像显示区域,支持滚动、缩放和ROI -->
|
||||
<roi:PolygonRoiCanvas Grid.Row="1"
|
||||
<!-- 图像显示区域 -->
|
||||
<Grid Grid.Row="1">
|
||||
<roi:PolygonRoiCanvas x:Name="RoiCanvas"
|
||||
ImageSource="{Binding ImageSource}"
|
||||
Background="White" />
|
||||
Background="White">
|
||||
<roi:PolygonRoiCanvas.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="放大" Click="ZoomIn_Click" />
|
||||
<MenuItem Header="缩小" Click="ZoomOut_Click" />
|
||||
<MenuItem Header="适应窗口" Click="ResetView_Click" />
|
||||
<Separator />
|
||||
<MenuItem Header="保存原始图像" Click="SaveOriginalImage_Click" />
|
||||
<MenuItem Header="保存结果图像" Click="SaveResultImage_Click" />
|
||||
<Separator />
|
||||
<MenuItem Header="清除所有绘制" Click="ClearAllMeasurements_Click" />
|
||||
</ContextMenu>
|
||||
</roi:PolygonRoiCanvas.ContextMenu>
|
||||
</roi:PolygonRoiCanvas>
|
||||
|
||||
<!-- 十字线和测量功能已内置于 PolygonRoiCanvas -->
|
||||
</Grid>
|
||||
|
||||
|
||||
</Grid>
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Microsoft.Win32;
|
||||
using Prism.Ioc;
|
||||
using XP.ImageProcessing.RoiControl.Controls;
|
||||
using XplorePlane.Events;
|
||||
using XplorePlane.ViewModels;
|
||||
|
||||
namespace XplorePlane.Views
|
||||
{
|
||||
@@ -7,6 +18,137 @@ namespace XplorePlane.Views
|
||||
public ViewportPanelView()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
|
||||
// 测量事件 → 更新状态栏
|
||||
RoiCanvas.MeasureCompleted += (s, e) =>
|
||||
{
|
||||
if (e is MeasureCompletedEventArgs args && DataContext is ViewportPanelViewModel vm)
|
||||
{
|
||||
vm.MeasurementResult = $"{args.Distance:F2} px";
|
||||
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) =>
|
||||
{
|
||||
if (e is MeasureStatusEventArgs args && DataContext is ViewportPanelViewModel vm)
|
||||
vm.ImageInfo = args.Message;
|
||||
};
|
||||
|
||||
// 十字辅助线:直接订阅 Prism 事件
|
||||
try
|
||||
{
|
||||
var ea = ContainerLocator.Current?.Resolve<Prism.Events.IEventAggregator>();
|
||||
ea?.GetEvent<ToggleCrosshairEvent>().Subscribe(() =>
|
||||
{
|
||||
RoiCanvas.ShowCrosshair = !RoiCanvas.ShowCrosshair;
|
||||
}, Prism.Events.ThreadOption.UIThread);
|
||||
|
||||
// 测量模式:直接订阅 Prism 事件
|
||||
ea?.GetEvent<MeasurementToolEvent>().Subscribe(mode =>
|
||||
{
|
||||
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 { }
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.OldValue is INotifyPropertyChanged oldVm)
|
||||
oldVm.PropertyChanged -= OnVmPropertyChanged;
|
||||
if (e.NewValue is INotifyPropertyChanged newVm)
|
||||
newVm.PropertyChanged += OnVmPropertyChanged;
|
||||
}
|
||||
|
||||
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
// 测量模式和十字线通过 Prism 事件直接驱动,不再依赖 PropertyChanged
|
||||
}
|
||||
|
||||
#region 右键菜单
|
||||
|
||||
private void ZoomIn_Click(object sender, RoutedEventArgs e) => RoiCanvas.ZoomScale = Math.Min(10.0, RoiCanvas.ZoomScale * 1.2);
|
||||
private void ZoomOut_Click(object sender, RoutedEventArgs e) => RoiCanvas.ZoomScale = Math.Max(0.1, RoiCanvas.ZoomScale / 1.2);
|
||||
private void ResetView_Click(object sender, RoutedEventArgs e) => RoiCanvas.ResetView();
|
||||
|
||||
private void ClearAllMeasurements_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RoiCanvas.ClearMeasurements();
|
||||
if (DataContext is ViewportPanelViewModel vm)
|
||||
{
|
||||
vm.ResetMeasurementState();
|
||||
vm.ImageInfo = "已清除所有测量";
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveOriginalImage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not ViewportPanelViewModel vm || vm.ImageSource is not BitmapSource bitmap)
|
||||
{
|
||||
MessageBox.Show("当前没有可保存的图像", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
SaveBitmapToFile(bitmap, "保存原始图像");
|
||||
}
|
||||
|
||||
private void SaveResultImage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var target = FindChildByName<Canvas>(RoiCanvas, "mainCanvas");
|
||||
if (target == null)
|
||||
{
|
||||
MessageBox.Show("当前没有可保存的图像", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
var width = (int)target.ActualWidth;
|
||||
var height = (int)target.ActualHeight;
|
||||
if (width == 0 || height == 0) return;
|
||||
|
||||
var rtb = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
|
||||
rtb.Render(target);
|
||||
SaveBitmapToFile(rtb, "保存结果图像");
|
||||
}
|
||||
|
||||
private static void SaveBitmapToFile(BitmapSource bitmap, string title)
|
||||
{
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Title = title,
|
||||
Filter = "PNG 图像|*.png|BMP 图像|*.bmp|JPEG 图像|*.jpg",
|
||||
DefaultExt = ".png"
|
||||
};
|
||||
if (dialog.ShowDialog() != true) return;
|
||||
|
||||
BitmapEncoder encoder = Path.GetExtension(dialog.FileName).ToLower() switch
|
||||
{
|
||||
".bmp" => new BmpBitmapEncoder(),
|
||||
".jpg" or ".jpeg" => new JpegBitmapEncoder(),
|
||||
_ => new PngBitmapEncoder()
|
||||
};
|
||||
encoder.Frames.Add(BitmapFrame.Create(bitmap));
|
||||
using var fs = new FileStream(dialog.FileName, FileMode.Create);
|
||||
encoder.Save(fs);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static T FindChildByName<T>(DependencyObject parent, string name) where T : FrameworkElement
|
||||
{
|
||||
int count = VisualTreeHelper.GetChildrenCount(parent);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is T t && t.Name == name) return t;
|
||||
var result = FindChildByName<T>(child, name);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user