已合并 PR 37: 点点距和点线距功能合并

1.点点距;
2.点线距;
3.十字辅助线;
This commit is contained in:
LI Wei.lw
2026-04-24 13:09:09 +08:00
15 changed files with 1101 additions and 99 deletions
@@ -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.LayoutTransform>
<ScaleTransform x:Name="scaleTransform"
ScaleX="{Binding ZoomScale, ElementName=root}"
ScaleY="{Binding ZoomScale, ElementName=root}" />
</Grid.LayoutTransform>
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="scaleTransform"
ScaleX="{Binding ZoomScale, ElementName=root}"
ScaleY="{Binding ZoomScale, ElementName=root}" />
<TranslateTransform x:Name="translateTransform"
X="{Binding PanOffsetX, ElementName=root}"
Y="{Binding PanOffsetY, ElementName=root}" />
</TransformGroup>
<TranslateTransform x:Name="translateTransform"
X="{Binding PanOffsetX, ElementName=root}"
Y="{Binding PanOffsetY, ElementName=root}" />
</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 延迟执行,确保布局已完成
Dispatcher.BeginInvoke(new Action(() =>
{
// 获取图像显示区域的实际尺寸
double viewportWidth = imageDisplayGrid.ActualWidth;
double viewportHeight = imageDisplayGrid.ActualHeight;
if (viewportWidth > 0 && viewportHeight > 0)
{
// 计算宽度和高度的缩放比例
double scaleX = viewportWidth / CanvasWidth;
double scaleY = viewportHeight / CanvasHeight;
// 选择较小的缩放比例,确保图像完全显示在窗口内(保持宽高比)
ZoomScale = Math.Min(scaleX, scaleY);
// 居中显示由 Grid 的 HorizontalAlignment 和 VerticalAlignment 自动处理
PanOffsetX = 0;
PanOffsetY = 0;
}
}), System.Windows.Threading.DispatcherPriority.Loaded);
ZoomScale = 1.0;
return;
}
// 延迟到布局完成后计算,确保 ActualWidth/Height 准确
Dispatcher.BeginInvoke(new Action(() =>
{
double viewW = imageDisplayGrid.ActualWidth;
double viewH = imageDisplayGrid.ActualHeight;
if (viewW > 0 && viewH > 0)
{
ZoomScale = Math.Min(viewW / CanvasWidth, viewH / CanvasHeight);
}
else
{
ZoomScale = 1.0;
}
PanOffsetX = 0;
PanOffsetY = 0;
}), System.Windows.Threading.DispatcherPriority.Render);
}
private void BtnZoomIn_Click(object sender, RoutedEventArgs e)
@@ -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
View File
@@ -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;
+16
View File
@@ -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();
}
}
+43
View File
@@ -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>
+22 -4
View File
@@ -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"
ImageSource="{Binding ImageSource}"
Background="White" />
<!-- 图像显示区域 -->
<Grid Grid.Row="1">
<roi:PolygonRoiCanvas x:Name="RoiCanvas"
ImageSource="{Binding ImageSource}"
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;
}
}
}
}