将十字线以及点点距ROI实现迁移到XP.ImageProcessing.RoiControl中
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -243,6 +243,266 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
|||||||
|
|
||||||
#endregion Dependency Properties
|
#endregion Dependency Properties
|
||||||
|
|
||||||
|
#region Crosshair
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ShowCrosshairProperty =
|
||||||
|
DependencyProperty.Register(nameof(ShowCrosshair), typeof(bool), typeof(PolygonRoiCanvas),
|
||||||
|
new PropertyMetadata(false, OnShowCrosshairChanged));
|
||||||
|
|
||||||
|
public bool ShowCrosshair
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(ShowCrosshairProperty);
|
||||||
|
set => SetValue(ShowCrosshairProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Line _crosshairH, _crosshairV;
|
||||||
|
|
||||||
|
private static void OnShowCrosshairChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var c = (PolygonRoiCanvas)d;
|
||||||
|
if ((bool)e.NewValue)
|
||||||
|
c.AddCrosshair();
|
||||||
|
else
|
||||||
|
c.RemoveCrosshair();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddCrosshair()
|
||||||
|
{
|
||||||
|
RemoveCrosshair();
|
||||||
|
double w = CanvasWidth, h = CanvasHeight;
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
|
||||||
|
_crosshairH = new Line { X1 = 0, Y1 = h / 2, X2 = w, Y2 = h / 2, Stroke = Brushes.Red, StrokeThickness = 1, Opacity = 0.7, IsHitTestVisible = false };
|
||||||
|
_crosshairV = new Line { X1 = w / 2, Y1 = 0, X2 = w / 2, Y2 = h, Stroke = Brushes.Red, StrokeThickness = 1, Opacity = 0.7, IsHitTestVisible = false };
|
||||||
|
mainCanvas.Children.Add(_crosshairH);
|
||||||
|
mainCanvas.Children.Add(_crosshairV);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveCrosshair()
|
||||||
|
{
|
||||||
|
if (_crosshairH != null) { mainCanvas.Children.Remove(_crosshairH); _crosshairH = null; }
|
||||||
|
if (_crosshairV != null) { mainCanvas.Children.Remove(_crosshairV); _crosshairV = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Crosshair
|
||||||
|
|
||||||
|
#region Measurement
|
||||||
|
|
||||||
|
public static readonly DependencyProperty IsMeasuringProperty =
|
||||||
|
DependencyProperty.Register(nameof(IsMeasuring), typeof(bool), typeof(PolygonRoiCanvas),
|
||||||
|
new PropertyMetadata(false, OnIsMeasuringChanged));
|
||||||
|
|
||||||
|
public bool IsMeasuring
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(IsMeasuringProperty);
|
||||||
|
set => SetValue(IsMeasuringProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Canvas _measureOverlay;
|
||||||
|
private readonly System.Collections.Generic.List<Models.MeasureGroup> _measureGroups = new();
|
||||||
|
private Ellipse _pendingDot;
|
||||||
|
private Point? _pendingPoint;
|
||||||
|
private Ellipse _mDraggingDot;
|
||||||
|
private Models.MeasureGroup _mDraggingGroup;
|
||||||
|
private bool _mDraggingIsDot1;
|
||||||
|
|
||||||
|
private static void OnIsMeasuringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var c = (PolygonRoiCanvas)d;
|
||||||
|
if ((bool)e.NewValue)
|
||||||
|
{
|
||||||
|
c.EnsureMeasureOverlay();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 退出测量模式:只清除未完成的临时点,保留已完成的测量线
|
||||||
|
if (c._pendingDot != null && c._measureOverlay != null)
|
||||||
|
{
|
||||||
|
c._measureOverlay.Children.Remove(c._pendingDot);
|
||||||
|
c._pendingDot = null;
|
||||||
|
}
|
||||||
|
c._pendingPoint = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureMeasureOverlay()
|
||||||
|
{
|
||||||
|
if (_measureOverlay != null) return;
|
||||||
|
_measureOverlay = new Canvas { IsHitTestVisible = true, Background = Brushes.Transparent };
|
||||||
|
_measureOverlay.SetBinding(Canvas.WidthProperty, new System.Windows.Data.Binding("CanvasWidth") { Source = this });
|
||||||
|
_measureOverlay.SetBinding(Canvas.HeightProperty, new System.Windows.Data.Binding("CanvasHeight") { Source = this });
|
||||||
|
mainCanvas.Children.Add(_measureOverlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveMeasureOverlay()
|
||||||
|
{
|
||||||
|
if (_measureOverlay != null)
|
||||||
|
{
|
||||||
|
mainCanvas.Children.Remove(_measureOverlay);
|
||||||
|
_measureOverlay = null;
|
||||||
|
}
|
||||||
|
_measureGroups.Clear();
|
||||||
|
_pendingDot = null;
|
||||||
|
_pendingPoint = null;
|
||||||
|
_mDraggingDot = null;
|
||||||
|
_mDraggingGroup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>清除所有测量绘制</summary>
|
||||||
|
public void ClearMeasurements()
|
||||||
|
{
|
||||||
|
RemoveMeasureOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>获取当前测量组数量</summary>
|
||||||
|
public int MeasureCount => _measureGroups.Count;
|
||||||
|
|
||||||
|
/// <summary>处理测量模式下的画布点击</summary>
|
||||||
|
private void HandleMeasureClick(Point pos)
|
||||||
|
{
|
||||||
|
if (_measureOverlay == null) EnsureMeasureOverlay();
|
||||||
|
if (_measureOverlay == null) return;
|
||||||
|
|
||||||
|
if (!_pendingPoint.HasValue)
|
||||||
|
{
|
||||||
|
_pendingPoint = pos;
|
||||||
|
_pendingDot = CreateMeasureDot(Brushes.Red);
|
||||||
|
_measureOverlay.Children.Add(_pendingDot);
|
||||||
|
SetMeasureDotPos(_pendingDot, pos);
|
||||||
|
RaiseMeasureStatusChanged($"点点距测量 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var g = CreateMeasureGroup(_pendingPoint.Value, pos);
|
||||||
|
_measureGroups.Add(g);
|
||||||
|
_measureOverlay.Children.Remove(_pendingDot);
|
||||||
|
_pendingDot = null;
|
||||||
|
_pendingPoint = null;
|
||||||
|
RaiseMeasureCompleted(g);
|
||||||
|
|
||||||
|
// 完成一条测量后自动退出测量模式
|
||||||
|
IsMeasuring = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Models.MeasureGroup CreateMeasureGroup(Point p1, Point p2)
|
||||||
|
{
|
||||||
|
var g = new Models.MeasureGroup { P1 = p1, P2 = p2 };
|
||||||
|
g.Line = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false };
|
||||||
|
_measureOverlay.Children.Add(g.Line);
|
||||||
|
|
||||||
|
g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false };
|
||||||
|
_measureOverlay.Children.Add(g.Label);
|
||||||
|
|
||||||
|
g.Dot1 = CreateMeasureDot(Brushes.Red);
|
||||||
|
g.Dot2 = CreateMeasureDot(Brushes.Blue);
|
||||||
|
_measureOverlay.Children.Add(g.Dot1);
|
||||||
|
_measureOverlay.Children.Add(g.Dot2);
|
||||||
|
|
||||||
|
SetMeasureDotPos(g.Dot1, p1);
|
||||||
|
SetMeasureDotPos(g.Dot2, p2);
|
||||||
|
g.UpdateLine();
|
||||||
|
g.UpdateLabel();
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Ellipse CreateMeasureDot(Brush fill)
|
||||||
|
{
|
||||||
|
var dot = new Ellipse { Width = 12, Height = 12, Fill = fill, Stroke = Brushes.White, StrokeThickness = 1.5, Cursor = Cursors.Hand };
|
||||||
|
dot.SetValue(ContextMenuService.IsEnabledProperty, false);
|
||||||
|
dot.MouseLeftButtonDown += MDot_Down;
|
||||||
|
dot.MouseMove += MDot_Move;
|
||||||
|
dot.MouseLeftButtonUp += MDot_Up;
|
||||||
|
dot.PreviewMouseRightButtonUp += MDot_RightClick;
|
||||||
|
return dot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetMeasureDotPos(Ellipse dot, Point pos)
|
||||||
|
{
|
||||||
|
Canvas.SetLeft(dot, pos.X - dot.Width / 2);
|
||||||
|
Canvas.SetTop(dot, pos.Y - dot.Height / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MDot_Down(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Ellipse dot) return;
|
||||||
|
foreach (var g in _measureGroups)
|
||||||
|
{
|
||||||
|
if (g.Dot1 == dot) { _mDraggingGroup = g; _mDraggingIsDot1 = true; break; }
|
||||||
|
if (g.Dot2 == dot) { _mDraggingGroup = g; _mDraggingIsDot1 = false; break; }
|
||||||
|
}
|
||||||
|
if (_mDraggingGroup != null) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MDot_Move(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (_mDraggingDot == null || _mDraggingGroup == null || _measureOverlay == null) return;
|
||||||
|
if (e.LeftButton != MouseButtonState.Pressed) return;
|
||||||
|
var pos = e.GetPosition(_measureOverlay);
|
||||||
|
SetMeasureDotPos(_mDraggingDot, pos);
|
||||||
|
if (_mDraggingIsDot1) _mDraggingGroup.P1 = pos; else _mDraggingGroup.P2 = pos;
|
||||||
|
_mDraggingGroup.UpdateLine();
|
||||||
|
_mDraggingGroup.UpdateLabel();
|
||||||
|
RaiseMeasureCompleted(_mDraggingGroup);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MDot_Up(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (_mDraggingDot != null) { _mDraggingDot.ReleaseMouseCapture(); _mDraggingDot = null; _mDraggingGroup = null; e.Handled = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MDot_RightClick(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Ellipse dot || _measureOverlay == null) return;
|
||||||
|
Models.MeasureGroup target = null;
|
||||||
|
foreach (var g in _measureGroups)
|
||||||
|
{
|
||||||
|
if (g.Dot1 == dot || g.Dot2 == dot) { target = g; break; }
|
||||||
|
}
|
||||||
|
if (target == null) return;
|
||||||
|
_measureOverlay.Children.Remove(target.Dot1);
|
||||||
|
_measureOverlay.Children.Remove(target.Dot2);
|
||||||
|
_measureOverlay.Children.Remove(target.Line);
|
||||||
|
_measureOverlay.Children.Remove(target.Label);
|
||||||
|
_measureGroups.Remove(target);
|
||||||
|
RaiseMeasureStatusChanged($"已删除测量线 | 剩余 {_measureGroups.Count} 条");
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测量事件
|
||||||
|
public static readonly RoutedEvent MeasureCompletedEvent =
|
||||||
|
EventManager.RegisterRoutedEvent(nameof(MeasureCompleted), RoutingStrategy.Bubble,
|
||||||
|
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
|
||||||
|
|
||||||
|
public event RoutedEventHandler MeasureCompleted
|
||||||
|
{
|
||||||
|
add { AddHandler(MeasureCompletedEvent, value); }
|
||||||
|
remove { RemoveHandler(MeasureCompletedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RaiseMeasureCompleted(Models.MeasureGroup g)
|
||||||
|
{
|
||||||
|
RaiseEvent(new MeasureCompletedEventArgs(MeasureCompletedEvent, g.P1, g.P2, g.Distance, _measureGroups.Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly RoutedEvent MeasureStatusChangedEvent =
|
||||||
|
EventManager.RegisterRoutedEvent(nameof(MeasureStatusChanged), RoutingStrategy.Bubble,
|
||||||
|
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
|
||||||
|
|
||||||
|
public event RoutedEventHandler MeasureStatusChanged
|
||||||
|
{
|
||||||
|
add { AddHandler(MeasureStatusChangedEvent, value); }
|
||||||
|
remove { RemoveHandler(MeasureStatusChangedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RaiseMeasureStatusChanged(string message)
|
||||||
|
{
|
||||||
|
RaiseEvent(new MeasureStatusEventArgs(MeasureStatusChangedEvent, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Measurement
|
||||||
|
|
||||||
#region Adorner Management
|
#region Adorner Management
|
||||||
|
|
||||||
private void UpdateAdorner()
|
private void UpdateAdorner()
|
||||||
@@ -387,8 +647,9 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
|||||||
|
|
||||||
if (!isDragging)
|
if (!isDragging)
|
||||||
{
|
{
|
||||||
// 处理点击事件
|
|
||||||
Point clickPosition = e.GetPosition(mainCanvas);
|
Point clickPosition = e.GetPosition(mainCanvas);
|
||||||
|
if (IsMeasuring)
|
||||||
|
HandleMeasureClick(clickPosition);
|
||||||
OnCanvasClicked(clickPosition);
|
OnCanvasClicked(clickPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 unit = "px")
|
||||||
|
{
|
||||||
|
Label.Text = $"{Distance:F2} {unit}";
|
||||||
|
Canvas.SetLeft(Label, (P1.X + P2.X) / 2 + 8);
|
||||||
|
Canvas.SetTop(Label, (P1.Y + P2.Y) / 2 - 18);
|
||||||
|
Label.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ namespace XplorePlane.ViewModels
|
|||||||
public class ViewportPanelViewModel : BindableBase
|
public class ViewportPanelViewModel : BindableBase
|
||||||
{
|
{
|
||||||
private readonly ILoggerService _logger;
|
private readonly ILoggerService _logger;
|
||||||
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private int _isProcessingFrame;
|
private int _isProcessingFrame;
|
||||||
|
|
||||||
private ImageSource _imageSource;
|
private ImageSource _imageSource;
|
||||||
@@ -137,10 +138,12 @@ namespace XplorePlane.ViewModels
|
|||||||
public ViewportPanelViewModel(IEventAggregator eventAggregator, ILoggerService logger)
|
public ViewportPanelViewModel(IEventAggregator eventAggregator, ILoggerService logger)
|
||||||
{
|
{
|
||||||
_logger = logger?.ForModule<ViewportPanelViewModel>();
|
_logger = logger?.ForModule<ViewportPanelViewModel>();
|
||||||
|
_eventAggregator = eventAggregator;
|
||||||
|
|
||||||
CancelMeasurementCommand = new DelegateCommand(() =>
|
CancelMeasurementCommand = new DelegateCommand(() =>
|
||||||
{
|
{
|
||||||
CurrentMeasurementMode = MeasurementToolMode.None;
|
// 发布 None 事件,让 View 层也收到
|
||||||
|
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
|
||||||
ImageInfo = "测量已取消";
|
ImageInfo = "测量已取消";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,11 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
xmlns:prism="http://prismlibrary.com/"
|
||||||
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
|
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
|
||||||
xmlns:local="clr-namespace:XplorePlane.Views"
|
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||||
d:DesignHeight="400"
|
d:DesignHeight="400"
|
||||||
d:DesignWidth="600"
|
d:DesignWidth="600"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources />
|
||||||
<BooleanToVisibilityConverter x:Key="BoolToVisConverter" />
|
|
||||||
<local:HalfValueConverter x:Key="HalfConverter" />
|
|
||||||
</UserControl.Resources>
|
|
||||||
<Grid Background="#FFFFFF">
|
<Grid Background="#FFFFFF">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -24,19 +20,8 @@
|
|||||||
|
|
||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
|
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
|
||||||
<Grid>
|
|
||||||
<TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center"
|
<TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||||
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
|
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
|
||||||
<!-- 测量模式提示 + 取消按钮 -->
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="4,0"
|
|
||||||
Visibility="{Binding IsMeasuring, Converter={StaticResource BoolToVisConverter}}">
|
|
||||||
<TextBlock VerticalAlignment="Center" Foreground="#E65100" FontSize="11"
|
|
||||||
Text="{Binding MeasurementModeText}" Margin="0,0,8,0" />
|
|
||||||
<Button Content="✕ 取消" FontSize="10" Padding="6,1"
|
|
||||||
Command="{Binding CancelMeasurementCommand}"
|
|
||||||
Background="#FFDDDD" BorderBrush="#CC0000" Foreground="#CC0000" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- 图像显示区域 -->
|
<!-- 图像显示区域 -->
|
||||||
@@ -58,7 +43,7 @@
|
|||||||
</roi:PolygonRoiCanvas.ContextMenu>
|
</roi:PolygonRoiCanvas.ContextMenu>
|
||||||
</roi:PolygonRoiCanvas>
|
</roi:PolygonRoiCanvas>
|
||||||
|
|
||||||
<!-- 十字辅助线通过代码绘制到 mainCanvas 内部,跟随图像缩放平移 -->
|
<!-- 十字线和测量功能已内置于 PolygonRoiCanvas -->
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- 图像信息栏 -->
|
<!-- 图像信息栏 -->
|
||||||
|
|||||||
@@ -1,90 +1,60 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using System.Windows.Shapes;
|
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using XP.ImageProcessing.RoiControl.Controls;
|
|
||||||
using Prism.Ioc;
|
using Prism.Ioc;
|
||||||
|
using XP.ImageProcessing.RoiControl.Controls;
|
||||||
using XplorePlane.Events;
|
using XplorePlane.Events;
|
||||||
using XplorePlane.ViewModels;
|
using XplorePlane.ViewModels;
|
||||||
|
|
||||||
namespace XplorePlane.Views
|
namespace XplorePlane.Views
|
||||||
{
|
{
|
||||||
/// <summary>一次测量的所有视觉元素</summary>
|
|
||||||
internal class MeasureGroup
|
|
||||||
{
|
|
||||||
public Ellipse Dot1, Dot2;
|
|
||||||
public Line Line;
|
|
||||||
public TextBlock Label;
|
|
||||||
public Point P1, P2;
|
|
||||||
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
double dx = P2.X - P1.X, dy = P2.Y - P1.Y;
|
|
||||||
double dist = Math.Sqrt(dx * dx + dy * dy);
|
|
||||||
Label.Text = $"{dist:F2} px";
|
|
||||||
Canvas.SetLeft(Label, (P1.X + P2.X) / 2 + 8);
|
|
||||||
Canvas.SetTop(Label, (P1.Y + P2.Y) / 2 - 18);
|
|
||||||
Label.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class ViewportPanelView : UserControl
|
public partial class ViewportPanelView : UserControl
|
||||||
{
|
{
|
||||||
private Canvas _measureOverlay;
|
|
||||||
private readonly List<MeasureGroup> _groups = new();
|
|
||||||
|
|
||||||
// 当前正在创建的测量(只有第一个点,还没完成)
|
|
||||||
private Ellipse _pendingDot;
|
|
||||||
private Point? _pendingPoint;
|
|
||||||
|
|
||||||
// 拖拽状态
|
|
||||||
private Ellipse _draggingDot;
|
|
||||||
private MeasureGroup _draggingGroup;
|
|
||||||
private bool _draggingIsDot1;
|
|
||||||
|
|
||||||
public ViewportPanelView()
|
public ViewportPanelView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
DataContextChanged += OnDataContextChanged;
|
DataContextChanged += OnDataContextChanged;
|
||||||
|
|
||||||
RoiCanvas.CanvasClicked += (s, e) =>
|
// 测量事件 → 更新状态栏
|
||||||
|
RoiCanvas.MeasureCompleted += (s, e) =>
|
||||||
{
|
{
|
||||||
if (e is CanvasClickedEventArgs args)
|
if (e is MeasureCompletedEventArgs args && DataContext is ViewportPanelViewModel vm)
|
||||||
OnCanvasClicked(args.Position);
|
{
|
||||||
|
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} 条测量";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
RoiCanvas.MeasureStatusChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e is MeasureStatusEventArgs args && DataContext is ViewportPanelViewModel vm)
|
||||||
|
vm.ImageInfo = args.Message;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 直接订阅 Prism 事件,不依赖 DataContext 的 PropertyChanged
|
// 十字辅助线:直接订阅 Prism 事件
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ea = Prism.Ioc.ContainerLocator.Current?.Resolve<Prism.Events.IEventAggregator>();
|
var ea = ContainerLocator.Current?.Resolve<Prism.Events.IEventAggregator>();
|
||||||
ea?.GetEvent<ToggleCrosshairEvent>().Subscribe(() =>
|
ea?.GetEvent<ToggleCrosshairEvent>().Subscribe(() =>
|
||||||
{
|
{
|
||||||
_crosshairVisible = !_crosshairVisible;
|
RoiCanvas.ShowCrosshair = !RoiCanvas.ShowCrosshair;
|
||||||
ToggleCrosshairOnCanvas(_crosshairVisible);
|
}, Prism.Events.ThreadOption.UIThread);
|
||||||
|
|
||||||
|
// 测量模式:直接订阅 Prism 事件
|
||||||
|
ea?.GetEvent<MeasurementToolEvent>().Subscribe(mode =>
|
||||||
|
{
|
||||||
|
RoiCanvas.IsMeasuring = mode != MeasurementToolMode.None;
|
||||||
}, Prism.Events.ThreadOption.UIThread);
|
}, Prism.Events.ThreadOption.UIThread);
|
||||||
}
|
}
|
||||||
catch { /* 设计时或容器未初始化时忽略 */ }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _crosshairVisible;
|
|
||||||
|
|
||||||
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine($"[VP] DataContextChanged: old={e.OldValue?.GetType().Name}, new={e.NewValue?.GetType().Name}");
|
|
||||||
if (e.OldValue is INotifyPropertyChanged oldVm)
|
if (e.OldValue is INotifyPropertyChanged oldVm)
|
||||||
oldVm.PropertyChanged -= OnVmPropertyChanged;
|
oldVm.PropertyChanged -= OnVmPropertyChanged;
|
||||||
if (e.NewValue is INotifyPropertyChanged newVm)
|
if (e.NewValue is INotifyPropertyChanged newVm)
|
||||||
@@ -94,300 +64,13 @@ namespace XplorePlane.Views
|
|||||||
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
|
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not ViewportPanelViewModel vm) return;
|
if (sender is not ViewportPanelViewModel vm) return;
|
||||||
System.Diagnostics.Debug.WriteLine($"[VP] PropertyChanged: {e.PropertyName}");
|
|
||||||
if (e.PropertyName == nameof(ViewportPanelViewModel.CurrentMeasurementMode))
|
if (e.PropertyName == nameof(ViewportPanelViewModel.CurrentMeasurementMode))
|
||||||
{
|
{
|
||||||
if (vm.CurrentMeasurementMode != MeasurementToolMode.None)
|
RoiCanvas.IsMeasuring = vm.CurrentMeasurementMode != MeasurementToolMode.None;
|
||||||
Dispatcher.BeginInvoke(new Action(EnsureOverlay),
|
|
||||||
System.Windows.Threading.DispatcherPriority.Loaded);
|
|
||||||
else
|
|
||||||
RemoveOverlay();
|
|
||||||
}
|
|
||||||
else if (e.PropertyName == nameof(ViewportPanelViewModel.ShowCrosshair))
|
|
||||||
{
|
|
||||||
// 十字线通过直接订阅 ToggleCrosshairEvent 处理
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region 覆盖层管理
|
|
||||||
|
|
||||||
private void EnsureOverlay()
|
|
||||||
{
|
|
||||||
if (_measureOverlay != null) return;
|
|
||||||
|
|
||||||
var mainCanvas = FindChildByName<Canvas>(RoiCanvas, "mainCanvas");
|
|
||||||
if (mainCanvas == null) return;
|
|
||||||
|
|
||||||
_measureOverlay = new Canvas
|
|
||||||
{
|
|
||||||
IsHitTestVisible = true,
|
|
||||||
Background = Brushes.Transparent
|
|
||||||
};
|
|
||||||
_measureOverlay.SetBinding(Canvas.WidthProperty,
|
|
||||||
new System.Windows.Data.Binding("CanvasWidth") { Source = RoiCanvas });
|
|
||||||
_measureOverlay.SetBinding(Canvas.HeightProperty,
|
|
||||||
new System.Windows.Data.Binding("CanvasHeight") { Source = RoiCanvas });
|
|
||||||
|
|
||||||
mainCanvas.Children.Add(_measureOverlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveOverlay()
|
|
||||||
{
|
|
||||||
if (_measureOverlay != null)
|
|
||||||
{
|
|
||||||
(VisualTreeHelper.GetParent(_measureOverlay) as Canvas)?.Children.Remove(_measureOverlay);
|
|
||||||
_measureOverlay = null;
|
|
||||||
}
|
|
||||||
_groups.Clear();
|
|
||||||
_pendingDot = null;
|
|
||||||
_pendingPoint = null;
|
|
||||||
_draggingDot = null;
|
|
||||||
_draggingGroup = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 点击处理
|
|
||||||
|
|
||||||
private void OnCanvasClicked(Point pos)
|
|
||||||
{
|
|
||||||
if (DataContext is not ViewportPanelViewModel vm || !vm.IsMeasuring) return;
|
|
||||||
if (vm.CurrentMeasurementMode != MeasurementToolMode.PointDistance) return;
|
|
||||||
if (_measureOverlay == null) EnsureOverlay();
|
|
||||||
if (_measureOverlay == null) return;
|
|
||||||
|
|
||||||
if (!_pendingPoint.HasValue)
|
|
||||||
{
|
|
||||||
// 第一个点
|
|
||||||
_pendingPoint = pos;
|
|
||||||
_pendingDot = CreateDot(Brushes.Red);
|
|
||||||
_measureOverlay.Children.Add(_pendingDot);
|
|
||||||
SetDotPos(_pendingDot, pos);
|
|
||||||
vm.ImageInfo = $"点点距测量 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 第二个点 → 完成一次测量
|
|
||||||
var group = CreateGroup(_pendingPoint.Value, pos);
|
|
||||||
_groups.Add(group);
|
|
||||||
|
|
||||||
// 移除临时的第一个点(已被 group.Dot1 替代)
|
|
||||||
_measureOverlay.Children.Remove(_pendingDot);
|
|
||||||
_pendingDot = null;
|
|
||||||
_pendingPoint = null;
|
|
||||||
|
|
||||||
UpdateStatusBar(vm, group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 测量组创建
|
|
||||||
|
|
||||||
private MeasureGroup CreateGroup(Point p1, Point p2)
|
|
||||||
{
|
|
||||||
var g = new 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 = CreateDot(Brushes.Red);
|
|
||||||
g.Dot2 = CreateDot(Brushes.Blue);
|
|
||||||
_measureOverlay.Children.Add(g.Dot1);
|
|
||||||
_measureOverlay.Children.Add(g.Dot2);
|
|
||||||
|
|
||||||
SetDotPos(g.Dot1, p1);
|
|
||||||
SetDotPos(g.Dot2, p2);
|
|
||||||
g.UpdateLine();
|
|
||||||
g.UpdateLabel();
|
|
||||||
|
|
||||||
return g;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Ellipse CreateDot(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 += Dot_Down;
|
|
||||||
dot.MouseMove += Dot_Move;
|
|
||||||
dot.MouseLeftButtonUp += Dot_Up;
|
|
||||||
dot.PreviewMouseRightButtonUp += Dot_RightClick;
|
|
||||||
return dot;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 拖拽
|
|
||||||
|
|
||||||
private void Dot_Down(object sender, MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is not Ellipse dot) return;
|
|
||||||
|
|
||||||
// 找到这个圆点属于哪个测量组
|
|
||||||
foreach (var g in _groups)
|
|
||||||
{
|
|
||||||
if (g.Dot1 == dot) { _draggingGroup = g; _draggingIsDot1 = true; break; }
|
|
||||||
if (g.Dot2 == dot) { _draggingGroup = g; _draggingIsDot1 = false; break; }
|
|
||||||
}
|
|
||||||
if (_draggingGroup != null)
|
|
||||||
{
|
|
||||||
_draggingDot = dot;
|
|
||||||
dot.CaptureMouse();
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Dot_Move(object sender, MouseEventArgs e)
|
|
||||||
{
|
|
||||||
if (_draggingDot == null || _draggingGroup == null || _measureOverlay == null) return;
|
|
||||||
if (e.LeftButton != MouseButtonState.Pressed) return;
|
|
||||||
|
|
||||||
var pos = e.GetPosition(_measureOverlay);
|
|
||||||
SetDotPos(_draggingDot, pos);
|
|
||||||
|
|
||||||
if (_draggingIsDot1) _draggingGroup.P1 = pos;
|
|
||||||
else _draggingGroup.P2 = pos;
|
|
||||||
|
|
||||||
_draggingGroup.UpdateLine();
|
|
||||||
_draggingGroup.UpdateLabel();
|
|
||||||
|
|
||||||
if (DataContext is ViewportPanelViewModel vm)
|
|
||||||
UpdateStatusBar(vm, _draggingGroup);
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Dot_Up(object sender, MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
if (_draggingDot != null)
|
|
||||||
{
|
|
||||||
_draggingDot.ReleaseMouseCapture();
|
|
||||||
_draggingDot = null;
|
|
||||||
_draggingGroup = null;
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Dot_RightClick(object sender, MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is not Ellipse dot || _measureOverlay == null) return;
|
|
||||||
|
|
||||||
MeasureGroup target = null;
|
|
||||||
foreach (var g in _groups)
|
|
||||||
{
|
|
||||||
if (g.Dot1 == dot || g.Dot2 == dot) { target = g; break; }
|
|
||||||
}
|
|
||||||
if (target == null) return;
|
|
||||||
|
|
||||||
// 移除该组的所有视觉元素
|
|
||||||
_measureOverlay.Children.Remove(target.Dot1);
|
|
||||||
_measureOverlay.Children.Remove(target.Dot2);
|
|
||||||
_measureOverlay.Children.Remove(target.Line);
|
|
||||||
_measureOverlay.Children.Remove(target.Label);
|
|
||||||
_groups.Remove(target);
|
|
||||||
|
|
||||||
if (DataContext is ViewportPanelViewModel vm)
|
|
||||||
vm.ImageInfo = $"已删除测量线 | 剩余 {_groups.Count} 条";
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 辅助
|
|
||||||
|
|
||||||
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 UpdateStatusBar(ViewportPanelViewModel vm, MeasureGroup g)
|
|
||||||
{
|
|
||||||
double dx = g.P2.X - g.P1.X, dy = g.P2.Y - g.P1.Y;
|
|
||||||
double dist = Math.Sqrt(dx * dx + dy * dy);
|
|
||||||
vm.MeasurementResult = $"{dist:F2} px";
|
|
||||||
vm.ImageInfo = $"点点距: {dist:F2} px | ({g.P1.X:F0},{g.P1.Y:F0}) → ({g.P2.X:F0},{g.P2.Y:F0}) | 共 {_groups.Count} 条测量";
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCrosshairLines()
|
|
||||||
{
|
|
||||||
// 十字线位置通过 XAML 绑定自动计算
|
|
||||||
}
|
|
||||||
|
|
||||||
private Line _crosshairH, _crosshairV;
|
|
||||||
|
|
||||||
private void ToggleCrosshairOnCanvas(bool show)
|
|
||||||
{
|
|
||||||
var mainCanvas = FindChildByName<Canvas>(RoiCanvas, "mainCanvas");
|
|
||||||
System.Diagnostics.Debug.WriteLine($"[VP] ToggleCrosshair show={show}, mainCanvas={mainCanvas != null}, W={RoiCanvas.CanvasWidth}, H={RoiCanvas.CanvasHeight}");
|
|
||||||
if (mainCanvas == null) return;
|
|
||||||
|
|
||||||
if (show)
|
|
||||||
{
|
|
||||||
double w = RoiCanvas.CanvasWidth;
|
|
||||||
double h = RoiCanvas.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);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_crosshairH != null) { mainCanvas.Children.Remove(_crosshairH); _crosshairH = null; }
|
|
||||||
if (_crosshairV != null) { mainCanvas.Children.Remove(_crosshairV); _crosshairV = null; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 右键菜单
|
#region 右键菜单
|
||||||
|
|
||||||
private void ZoomIn_Click(object sender, RoutedEventArgs e) => RoiCanvas.ZoomScale = Math.Min(10.0, RoiCanvas.ZoomScale * 1.2);
|
private void ZoomIn_Click(object sender, RoutedEventArgs e) => RoiCanvas.ZoomScale = Math.Min(10.0, RoiCanvas.ZoomScale * 1.2);
|
||||||
@@ -396,14 +79,7 @@ namespace XplorePlane.Views
|
|||||||
|
|
||||||
private void ClearAllMeasurements_Click(object sender, RoutedEventArgs e)
|
private void ClearAllMeasurements_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_measureOverlay != null)
|
RoiCanvas.ClearMeasurements();
|
||||||
{
|
|
||||||
_measureOverlay.Children.Clear();
|
|
||||||
_groups.Clear();
|
|
||||||
}
|
|
||||||
_pendingDot = null;
|
|
||||||
_pendingPoint = null;
|
|
||||||
|
|
||||||
if (DataContext is ViewportPanelViewModel vm)
|
if (DataContext is ViewportPanelViewModel vm)
|
||||||
{
|
{
|
||||||
vm.ResetMeasurementState();
|
vm.ResetMeasurementState();
|
||||||
@@ -423,14 +99,12 @@ namespace XplorePlane.Views
|
|||||||
|
|
||||||
private void SaveResultImage_Click(object sender, RoutedEventArgs e)
|
private void SaveResultImage_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// 截取整个图像区域(含测量标注覆盖层)
|
|
||||||
var target = FindChildByName<Canvas>(RoiCanvas, "mainCanvas");
|
var target = FindChildByName<Canvas>(RoiCanvas, "mainCanvas");
|
||||||
if (target == null)
|
if (target == null)
|
||||||
{
|
{
|
||||||
MessageBox.Show("当前没有可保存的图像", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
MessageBox.Show("当前没有可保存的图像", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var width = (int)target.ActualWidth;
|
var width = (int)target.ActualWidth;
|
||||||
var height = (int)target.ActualHeight;
|
var height = (int)target.ActualHeight;
|
||||||
if (width == 0 || height == 0) return;
|
if (width == 0 || height == 0) return;
|
||||||
@@ -450,18 +124,30 @@ namespace XplorePlane.Views
|
|||||||
};
|
};
|
||||||
if (dialog.ShowDialog() != true) return;
|
if (dialog.ShowDialog() != true) return;
|
||||||
|
|
||||||
BitmapEncoder encoder = System.IO.Path.GetExtension(dialog.FileName).ToLower() switch
|
BitmapEncoder encoder = Path.GetExtension(dialog.FileName).ToLower() switch
|
||||||
{
|
{
|
||||||
".bmp" => new BmpBitmapEncoder(),
|
".bmp" => new BmpBitmapEncoder(),
|
||||||
".jpg" or ".jpeg" => new JpegBitmapEncoder(),
|
".jpg" or ".jpeg" => new JpegBitmapEncoder(),
|
||||||
_ => new PngBitmapEncoder()
|
_ => new PngBitmapEncoder()
|
||||||
};
|
};
|
||||||
encoder.Frames.Add(BitmapFrame.Create(bitmap));
|
encoder.Frames.Add(BitmapFrame.Create(bitmap));
|
||||||
|
|
||||||
using var fs = new FileStream(dialog.FileName, FileMode.Create);
|
using var fs = new FileStream(dialog.FileName, FileMode.Create);
|
||||||
encoder.Save(fs);
|
encoder.Save(fs);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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