Files
XplorePlane/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
T
2026-05-14 15:54:15 +08:00

586 lines
25 KiB
C#

using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
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
{
public partial class ViewportPanelView : UserControl
{
private MainViewModel _mainVm;
private MainViewModel GetMainVm()
{
if (_mainVm == null)
{
try { _mainVm = ContainerLocator.Current?.Resolve<MainViewModel>(); } catch { }
}
return _mainVm;
}
private void SetStatus(string msg)
{
var vm = GetMainVm();
if (vm != null) vm.StatusMessage = msg;
}
public ViewportPanelView()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
// 动态创建右键菜单,支持条件性阻止弹出
var menu = new System.Windows.Controls.ContextMenu();
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "放大" });
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "缩小" });
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "适应窗口" });
menu.Items.Add(new System.Windows.Controls.Separator());
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "保存原始图像" });
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "保存结果图像" });
menu.Items.Add(new System.Windows.Controls.Separator());
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "清除所有测量" });
((System.Windows.Controls.MenuItem)menu.Items[0]).Click += ZoomIn_Click;
((System.Windows.Controls.MenuItem)menu.Items[1]).Click += ZoomOut_Click;
((System.Windows.Controls.MenuItem)menu.Items[2]).Click += ResetView_Click;
((System.Windows.Controls.MenuItem)menu.Items[4]).Click += SaveOriginalImage_Click;
((System.Windows.Controls.MenuItem)menu.Items[5]).Click += SaveResultImage_Click;
((System.Windows.Controls.MenuItem)menu.Items[7]).Click += ClearAllMeasurements_Click;
RoiCanvas.ContextMenu = menu;
RoiCanvas.ContextMenuOpening += (s, e) =>
{
if (RoiCanvas.SuppressContextMenu)
{
RoiCanvas.SuppressContextMenu = false;
e.Handled = true;
}
};
// 测量事件 → 更新主界面状态栏
RoiCanvas.MeasureCompleted += (s, e) =>
{
if (e is MeasureCompletedEventArgs args)
{
string typeLabel = args.MeasureType switch
{
"PointToLine" => "点线距",
"Angle" => "角度",
"FillRate" => "填锡率",
"BgaVoid" => "BGA空隙",
"BubbleVoid" => "气泡空隙",
_ => "点点距"
};
string valueText = args.MeasureType switch
{
"Angle" => $"{args.Distance:F2}°",
"FillRate" => $"{args.Distance:F1}%",
"BgaVoid" => $"{args.Distance:F1}%",
"BubbleVoid" => $"{args.Distance:F1}%",
_ => $"{args.Distance:F2} px"
};
SetStatus($"{typeLabel}: {valueText} | 共 {args.TotalCount} 条测量");
}
};
RoiCanvas.MeasureStatusChanged += (s, e) =>
{
if (e is MeasureStatusEventArgs args)
SetStatus(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,
MeasurementToolMode.Angle => XP.ImageProcessing.RoiControl.Models.MeasureMode.Angle,
MeasurementToolMode.ThroughHoleFillRate => XP.ImageProcessing.RoiControl.Models.MeasureMode.FillRate,
MeasurementToolMode.BgaVoid => XP.ImageProcessing.RoiControl.Models.MeasureMode.BgaVoid,
MeasurementToolMode.BubbleMeasure => XP.ImageProcessing.RoiControl.Models.MeasureMode.BubbleMeasure,
_ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None
};
}, Prism.Events.ThreadOption.UIThread);
}
catch { }
// 光标信息:从 RoiCanvas.CursorInfo 同步到 MainViewModel
var cursorInfoDesc = System.ComponentModel.DependencyPropertyDescriptor.FromProperty(
PolygonRoiCanvas.CursorInfoProperty, typeof(PolygonRoiCanvas));
cursorInfoDesc?.AddValueChanged(RoiCanvas, (s, e) =>
{
var vm = GetMainVm();
if (vm != null) vm.CursorInfoText = RoiCanvas.CursorInfo;
});
// 行灰度分布
try
{
var ea2 = ContainerLocator.Current?.Resolve<Prism.Events.IEventAggregator>();
ea2?.GetEvent<ToggleLineProfileEvent>().Subscribe(() =>
{
ToggleLineProfile();
}, Prism.Events.ThreadOption.UIThread);
// 白底检测:进入ROI绘制模式
ea2?.GetEvent<WhiteBackgroundDetectionEvent>().Subscribe(() =>
{
_whiteDetectDrawing = false;
_whiteDetectMode = true;
// 注册鼠标事件(只注册一次)
RoiCanvas.PreviewMouseLeftButtonDown -= OnMainCanvasPreviewMouseDown;
RoiCanvas.PreviewMouseLeftButtonDown += OnMainCanvasPreviewMouseDown;
RoiCanvas.PreviewMouseMove -= OnMainCanvasPreviewMouseMove;
RoiCanvas.PreviewMouseMove += OnMainCanvasPreviewMouseMove;
RoiCanvas.PreviewMouseLeftButtonUp -= OnMainCanvasPreviewMouseUp;
RoiCanvas.PreviewMouseLeftButtonUp += OnMainCanvasPreviewMouseUp;
}, Prism.Events.ThreadOption.UIThread);
// 白底检测:渲染结果
ea2?.GetEvent<WhiteBackgroundResultEvent>().Subscribe(payload =>
{
RenderWhiteBackgroundResult(payload);
}, Prism.Events.ThreadOption.UIThread);
}
catch { }
}
#region
private bool _lineProfileEnabled;
private System.Windows.Shapes.Line _profileRefLine; // 透明命中区域
private System.Windows.Shapes.Line _profileRefLineVisible; // 1px红线显示
private System.Windows.Shapes.Polyline _profileCurve;
private double _profileLineY;
private bool _profileDragging;
private void ToggleLineProfile()
{
_lineProfileEnabled = !_lineProfileEnabled;
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
if (canvas == null) return;
if (_lineProfileEnabled)
{
// 参考线默认在图像中间
_profileLineY = RoiCanvas.CanvasHeight / 2;
// 创建参考线(红色水平线,可拖动)
// 用透明粗线作为命中区域,叠加1px红线显示
_profileRefLine = new System.Windows.Shapes.Line
{
X1 = 0,
Y1 = _profileLineY,
X2 = RoiCanvas.CanvasWidth,
Y2 = _profileLineY,
Stroke = System.Windows.Media.Brushes.Transparent,
StrokeThickness = 7, // 上下3px命中区域
IsHitTestVisible = true,
Cursor = System.Windows.Input.Cursors.SizeNS
};
_profileRefLineVisible = new System.Windows.Shapes.Line
{
X1 = 0,
Y1 = _profileLineY,
X2 = RoiCanvas.CanvasWidth,
Y2 = _profileLineY,
Stroke = System.Windows.Media.Brushes.Red,
StrokeThickness = 1,
IsHitTestVisible = false
};
_profileRefLine.MouseLeftButtonDown += ProfileLine_MouseDown;
_profileRefLine.MouseMove += ProfileLine_MouseMove;
_profileRefLine.MouseLeftButtonUp += ProfileLine_MouseUp;
canvas.Children.Add(_profileRefLineVisible);
canvas.Children.Add(_profileRefLine);
// 创建灰度折线(固定显示在图像中间位置)
_profileCurve = new System.Windows.Shapes.Polyline
{
Stroke = System.Windows.Media.Brushes.Red,
StrokeThickness = 1,
IsHitTestVisible = false
};
canvas.Children.Add(_profileCurve);
UpdateLineProfile();
SetStatus("行灰度分布:拖动红线改变采样行,再次点击按钮关闭");
}
else
{
if (_profileRefLine != null)
{
canvas.Children.Remove(_profileRefLine);
_profileRefLine = null;
}
if (_profileRefLineVisible != null)
{
canvas.Children.Remove(_profileRefLineVisible);
_profileRefLineVisible = null;
}
if (_profileCurve != null)
{
canvas.Children.Remove(_profileCurve);
_profileCurve = null;
}
SetStatus("行灰度分布已关闭");
}
}
private void ProfileLine_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
_profileDragging = true;
_profileRefLine?.CaptureMouse();
e.Handled = true;
}
private void ProfileLine_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (!_profileDragging || _profileRefLine == null) return;
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
if (canvas == null) return;
var pos = e.GetPosition(canvas);
_profileLineY = Math.Clamp(pos.Y, 0, RoiCanvas.CanvasHeight - 1);
_profileRefLine.Y1 = _profileLineY;
_profileRefLine.Y2 = _profileLineY;
_profileRefLineVisible.Y1 = _profileLineY;
_profileRefLineVisible.Y2 = _profileLineY;
UpdateLineProfile();
}
private void ProfileLine_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
_profileDragging = false;
_profileRefLine?.ReleaseMouseCapture();
e.Handled = true;
}
private void UpdateLineProfile()
{
if (_profileCurve == null) return;
// 从当前显示图像获取像素数据
var viewportVm = DataContext as ViewportPanelViewModel;
var imageSource = viewportVm?.ImageSource as System.Windows.Media.Imaging.BitmapSource;
if (imageSource == null) return;
int imgWidth = imageSource.PixelWidth;
int imgHeight = imageSource.PixelHeight;
int row = (int)Math.Clamp(_profileLineY, 0, imgHeight - 1);
// 转为 Gray8 获取行像素
System.Windows.Media.Imaging.BitmapSource gray8;
if (imageSource.Format != System.Windows.Media.PixelFormats.Gray8)
gray8 = new System.Windows.Media.Imaging.FormatConvertedBitmap(
imageSource, System.Windows.Media.PixelFormats.Gray8, null, 0);
else
gray8 = imageSource;
byte[] rowPixels = new byte[imgWidth];
int stride = imgWidth;
gray8.CopyPixels(new System.Windows.Int32Rect(0, row, imgWidth, 1), rowPixels, stride, 0);
// 构建折线点集:折线固定显示在图像垂直中间位置
// 参考线位置决定采样哪一行,折线位置固定在画布中间
double canvasH = RoiCanvas.CanvasHeight;
double curveCenter = canvasH / 2.0; // 折线基线固定在图像中间
double displayHeight = canvasH * 0.25; // 折线振幅为画布高度的25%
var points = new System.Windows.Media.PointCollection(imgWidth);
for (int x = 0; x < imgWidth; x++)
{
double normalizedGray = rowPixels[x] / 255.0;
double y = curveCenter - normalizedGray * displayHeight;
points.Add(new System.Windows.Point(x, y));
}
_profileCurve.Points = points;
SetStatus($"行灰度分布 | Y={row} | 均值={rowPixels.Select(b => (double)b).Average():F1} | 最大={rowPixels.Max()} | 最小={rowPixels.Min()}");
}
#endregion
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();
RoiCanvas.ROIItems?.Clear();
RoiCanvas.SelectedROI = null;
if (DataContext is ViewportPanelViewModel vm)
vm.ResetMeasurementState();
SetStatus("已清除所有测量");
}
private void SaveOriginalImage_Click(object sender, RoutedEventArgs e)
{
if (DataContext is not ViewportPanelViewModel vm || vm.ImageSource is not BitmapSource bitmap)
{
MessageBox.Show("No image available to save", "Info", 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("No image available to save", "Info", 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
#region
private bool _whiteDetectMode;
private bool _whiteDetectDrawing;
private System.Windows.Point _whiteDetectStart;
private System.Windows.Shapes.Rectangle _whiteDetectPreview;
private readonly System.Collections.Generic.List<System.Windows.UIElement> _whiteDetectOverlays = new();
// 需要在 mainCanvas 的 MouseDown/Move/Up 中处理
// 由于 PolygonRoiCanvas 内部已经处理了鼠标事件,我们通过 PreviewMouse 事件来拦截
private void OnMainCanvasPreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (!_whiteDetectMode || e.LeftButton != System.Windows.Input.MouseButtonState.Pressed) return;
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
if (canvas == null) return;
_whiteDetectStart = e.GetPosition(canvas);
_whiteDetectDrawing = true;
// 创建预览矩形(不清除之前的检测结果)
_whiteDetectPreview = new System.Windows.Shapes.Rectangle
{
Stroke = System.Windows.Media.Brushes.Blue,
StrokeThickness = 1,
StrokeDashArray = new System.Windows.Media.DoubleCollection { 4, 2 }
};
System.Windows.Controls.Canvas.SetLeft(_whiteDetectPreview, _whiteDetectStart.X);
System.Windows.Controls.Canvas.SetTop(_whiteDetectPreview, _whiteDetectStart.Y);
canvas.Children.Add(_whiteDetectPreview);
RoiCanvas.CaptureMouse();
e.Handled = true;
}
private void OnMainCanvasPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (!_whiteDetectDrawing || _whiteDetectPreview == null) return;
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
if (canvas == null) return;
var current = e.GetPosition(canvas);
double x = Math.Min(_whiteDetectStart.X, current.X);
double y = Math.Min(_whiteDetectStart.Y, current.Y);
double w = Math.Abs(current.X - _whiteDetectStart.X);
double h = Math.Abs(current.Y - _whiteDetectStart.Y);
System.Windows.Controls.Canvas.SetLeft(_whiteDetectPreview, x);
System.Windows.Controls.Canvas.SetTop(_whiteDetectPreview, y);
_whiteDetectPreview.Width = Math.Max(1, w);
_whiteDetectPreview.Height = Math.Max(1, h);
}
private void OnMainCanvasPreviewMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (!_whiteDetectDrawing) return;
_whiteDetectDrawing = false;
_whiteDetectMode = false;
RoiCanvas.ReleaseMouseCapture();
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
if (canvas == null) return;
var end = e.GetPosition(canvas);
int x = (int)Math.Min(_whiteDetectStart.X, end.X);
int y = (int)Math.Min(_whiteDetectStart.Y, end.Y);
int w = (int)Math.Abs(end.X - _whiteDetectStart.X);
int h = (int)Math.Abs(end.Y - _whiteDetectStart.Y);
// 移除预览矩形
if (_whiteDetectPreview != null)
{
canvas.Children.Remove(_whiteDetectPreview);
_whiteDetectPreview = null;
}
if (w < 10 || h < 10) return; // 太小忽略
// 发布ROI绘制完成事件
try
{
var ea = ContainerLocator.Current?.Resolve<Prism.Events.IEventAggregator>();
ea?.GetEvent<WhiteBackgroundRoiDrawnEvent>().Publish(new System.Windows.Int32Rect(x, y, w, h));
}
catch { }
e.Handled = true;
}
private void RenderWhiteBackgroundResult(WhiteBackgroundResultPayload payload)
{
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
if (canvas == null || payload?.Detections == null) return;
// 绘制ROI矩形(蓝色实线)
var roiRect = new System.Windows.Shapes.Rectangle
{
Stroke = System.Windows.Media.Brushes.Blue,
StrokeThickness = 1,
Width = payload.RoiRect.Width,
Height = payload.RoiRect.Height,
IsHitTestVisible = false
};
System.Windows.Controls.Canvas.SetLeft(roiRect, payload.RoiRect.X);
System.Windows.Controls.Canvas.SetTop(roiRect, payload.RoiRect.Y);
canvas.Children.Add(roiRect);
_whiteDetectOverlays.Add(roiRect);
// 绘制每个检测到的黑色区域
foreach (var (center, radius, sizeMm) in payload.Detections)
{
// 红色虚线圆
var circle = new System.Windows.Shapes.Ellipse
{
Stroke = System.Windows.Media.Brushes.Red,
StrokeThickness = 1,
StrokeDashArray = new System.Windows.Media.DoubleCollection { 4, 2 },
Width = radius * 2,
Height = radius * 2,
IsHitTestVisible = false
};
System.Windows.Controls.Canvas.SetLeft(circle, center.X - radius);
System.Windows.Controls.Canvas.SetTop(circle, center.Y - radius);
canvas.Children.Add(circle);
_whiteDetectOverlays.Add(circle);
// 45°直径标注线(从圆心向左上到右下)
double offset = radius * 0.707; // cos(45°) * radius
var diamLine = new System.Windows.Shapes.Line
{
X1 = center.X - offset,
Y1 = center.Y - offset,
X2 = center.X + offset,
Y2 = center.Y + offset,
Stroke = System.Windows.Media.Brushes.Red,
StrokeThickness = 1,
IsHitTestVisible = false
};
canvas.Children.Add(diamLine);
_whiteDetectOverlays.Add(diamLine);
// 尺寸标注(在斜线右上方)
string label = sizeMm >= 1000 ? $"{sizeMm / 1000:F2} mm" : $"{sizeMm:F0} μm";
var text = new System.Windows.Controls.TextBlock
{
Text = label,
Foreground = System.Windows.Media.Brushes.Red,
FontSize = 11,
IsHitTestVisible = false
};
System.Windows.Controls.Canvas.SetLeft(text, center.X + offset + 3);
System.Windows.Controls.Canvas.SetTop(text, center.Y - offset - 14);
canvas.Children.Add(text);
_whiteDetectOverlays.Add(text);
}
}
private void ClearWhiteDetectOverlays(System.Windows.Controls.Canvas canvas)
{
foreach (var el in _whiteDetectOverlays)
canvas.Children.Remove(el);
_whiteDetectOverlays.Clear();
}
#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;
}
}
}