气泡测量:工具面板UI、BubbleMeasure模式、矩形ROI绘制/拖动/右下角调整大小、工具切换联动

This commit is contained in:
李伟
2026-04-27 11:32:06 +08:00
parent e7ae7085df
commit 0c8d8afc51
7 changed files with 463 additions and 2 deletions
@@ -370,6 +370,28 @@ namespace XP.ImageProcessing.RoiControl.Controls
private Point? _bgaPendingCenter; // 等待第二次点击定半径 private Point? _bgaPendingCenter; // 等待第二次点击定半径
private Ellipse _bgaPendingDot; private Ellipse _bgaPendingDot;
// 气泡测量状态
public enum BubbleSubTool { Roi, Wand, Brush, Eraser }
private BubbleSubTool _bubbleTool = BubbleSubTool.Roi;
private Rectangle _bubbleRoiRect;
private Ellipse _bubbleRoiHandle; // 右下角调整手柄
private Rect? _bubbleRoi;
private Point? _bubbleRoiStart;
private bool _bubbleRoiDragging;
private bool _bubbleRoiMoving; // 拖动整个 ROI
private bool _bubbleRoiResizing; // 右下角调整大小
private Point _bubbleRoiDragOffset;
private Image _bubbleMaskImage;
private System.Windows.Media.Imaging.WriteableBitmap _bubbleMask;
private int _bubbleThreshold = 128;
private int _bubbleBrushSize = 5;
private bool _bubbleBrushDragging;
public void SetBubbleTool(BubbleSubTool tool) => _bubbleTool = tool;
public void SetBubbleThreshold(int val) => _bubbleThreshold = val;
public void SetBubbleBrushSize(int val) => _bubbleBrushSize = val;
public Rect? BubbleRoi => _bubbleRoi;
// 拖拽状态 // 拖拽状态
private Ellipse _mDraggingDot; private Ellipse _mDraggingDot;
private object _mDraggingOwner; private object _mDraggingOwner;
@@ -448,6 +470,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
HandleFillRateClick(pos); HandleFillRateClick(pos);
else if (CurrentMeasureMode == Models.MeasureMode.BgaVoid) else if (CurrentMeasureMode == Models.MeasureMode.BgaVoid)
HandleBgaVoidClick(pos); HandleBgaVoidClick(pos);
// BubbleMeasure 的点击在 MouseDown/Move/Up 中处理(拖拽画 ROI 和画笔)
} }
// ── 点点距 ── // ── 点点距 ──
@@ -1084,6 +1107,125 @@ namespace XP.ImageProcessing.RoiControl.Controls
} }
} }
// ── 气泡测量辅助 ──
private void EnsureBubbleRoiVisuals()
{
EnsureMeasureOverlay();
if (_bubbleRoiRect == null)
{
_bubbleRoiRect = new Rectangle
{
Stroke = Brushes.Red,
StrokeThickness = 1.5,
Fill = Brushes.Transparent,
Visibility = Visibility.Collapsed,
IsHitTestVisible = false
};
_measureOverlay.Children.Add(_bubbleRoiRect);
}
if (_bubbleRoiHandle == null)
{
_bubbleRoiHandle = new Ellipse
{
Width = 10, Height = 10,
Fill = Brushes.Red, Stroke = Brushes.White, StrokeThickness = 1,
Cursor = Cursors.SizeNWSE,
Visibility = Visibility.Collapsed,
IsHitTestVisible = false // 命中测试由 MouseDown 中的距离判断处理
};
_measureOverlay.Children.Add(_bubbleRoiHandle);
}
}
private void SyncBubbleRoiVisuals()
{
if (_bubbleRoiRect == null || !_bubbleRoi.HasValue) return;
var r = _bubbleRoi.Value;
Canvas.SetLeft(_bubbleRoiRect, r.X);
Canvas.SetTop(_bubbleRoiRect, r.Y);
_bubbleRoiRect.Width = r.Width;
_bubbleRoiRect.Height = r.Height;
_bubbleRoiRect.Visibility = Visibility.Visible;
if (_bubbleRoiHandle != null)
{
Canvas.SetLeft(_bubbleRoiHandle, r.Right - _bubbleRoiHandle.Width / 2);
Canvas.SetTop(_bubbleRoiHandle, r.Bottom - _bubbleRoiHandle.Height / 2);
_bubbleRoiHandle.Visibility = Visibility.Visible;
}
}
private void InitBubbleMask()
{
if (!_bubbleRoi.HasValue) return;
int w = (int)CanvasWidth, h = (int)CanvasHeight;
if (w <= 0 || h <= 0) return;
_bubbleMask = new System.Windows.Media.Imaging.WriteableBitmap(w, h, 96, 96,
PixelFormats.Bgra32, null);
if (_bubbleMaskImage == null)
{
EnsureMeasureOverlay();
_bubbleMaskImage = new Image
{
IsHitTestVisible = false,
Opacity = 0.45,
Stretch = Stretch.Fill
};
_bubbleMaskImage.SetBinding(Image.WidthProperty, new System.Windows.Data.Binding("CanvasWidth") { Source = this });
_bubbleMaskImage.SetBinding(Image.HeightProperty, new System.Windows.Data.Binding("CanvasHeight") { Source = this });
_measureOverlay.Children.Add(_bubbleMaskImage);
}
_bubbleMaskImage.Source = _bubbleMask;
}
private void ApplyBrushAt(Point pos)
{
// 画笔/橡皮:暂时空实现,下一步实现
}
private void UpdateBubbleResult()
{
// 计算空隙率:暂时空实现,下一步实现
}
/// <summary>清除气泡测量所有状态</summary>
public void ClearBubbleMeasure()
{
if (_measureOverlay != null)
{
if (_bubbleRoiRect != null) { _measureOverlay.Children.Remove(_bubbleRoiRect); _bubbleRoiRect = null; }
if (_bubbleRoiHandle != null) { _measureOverlay.Children.Remove(_bubbleRoiHandle); _bubbleRoiHandle = null; }
if (_bubbleMaskImage != null) { _measureOverlay.Children.Remove(_bubbleMaskImage); _bubbleMaskImage = null; }
}
_bubbleRoi = null;
_bubbleMask = null;
_bubbleRoiStart = null;
_bubbleRoiDragging = false;
_bubbleRoiMoving = false;
_bubbleRoiResizing = false;
_bubbleBrushDragging = false;
_bubbleTool = BubbleSubTool.Roi;
}
// 气泡工具切换事件
public static readonly RoutedEvent BubbleToolChangedEvent =
EventManager.RegisterRoutedEvent(nameof(BubbleToolChanged), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
public event RoutedEventHandler BubbleToolChanged
{
add { AddHandler(BubbleToolChangedEvent, value); }
remove { RemoveHandler(BubbleToolChangedEvent, value); }
}
private void RaiseBubbleToolChanged()
{
RaiseEvent(new RoutedEventArgs(BubbleToolChangedEvent));
}
// ── 重编号 ── // ── 重编号 ──
private void RenumberAll() private void RenumberAll()
@@ -1354,6 +1496,57 @@ namespace XP.ImageProcessing.RoiControl.Controls
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{ {
// 气泡测量模式:ROI 拖拽 / 画笔涂抹
if (CurrentMeasureMode == Models.MeasureMode.BubbleMeasure)
{
var pos = e.GetPosition(mainCanvas);
if (_bubbleTool == BubbleSubTool.Roi)
{
// 检查是否点击了右下角手柄
if (_bubbleRoiHandle != null && _bubbleRoi.HasValue)
{
var hx = Canvas.GetLeft(_bubbleRoiHandle) + _bubbleRoiHandle.Width / 2;
var hy = Canvas.GetTop(_bubbleRoiHandle) + _bubbleRoiHandle.Height / 2;
if (Math.Abs(pos.X - hx) < 10 && Math.Abs(pos.Y - hy) < 10)
{
_bubbleRoiResizing = true;
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
}
// 检查是否点击了 ROI 内部(拖动)
if (_bubbleRoi.HasValue && _bubbleRoi.Value.Contains(pos))
{
_bubbleRoiMoving = true;
_bubbleRoiDragOffset = new Point(pos.X - _bubbleRoi.Value.X, pos.Y - _bubbleRoi.Value.Y);
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
// 没有 ROI 时才画新的,有 ROI 时点击外部不拦截(让图像正常拖动)
if (!_bubbleRoi.HasValue)
{
_bubbleRoiStart = pos;
_bubbleRoiDragging = true;
EnsureBubbleRoiVisuals();
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
// 有 ROI 但点击了外部 → 不拦截,走正常拖动逻辑
}
if ((_bubbleTool == BubbleSubTool.Brush || _bubbleTool == BubbleSubTool.Eraser) && _bubbleRoi.HasValue)
{
_bubbleBrushDragging = true;
ApplyBrushAt(pos);
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
// 魔棒在 MouseUpCanvasClicked)中处理
}
lastMousePosition = e.GetPosition(imageDisplayGrid); lastMousePosition = e.GetPosition(imageDisplayGrid);
isDragging = false; isDragging = false;
mainCanvas.CaptureMouse(); mainCanvas.CaptureMouse();
@@ -1361,6 +1554,54 @@ namespace XP.ImageProcessing.RoiControl.Controls
private void Canvas_MouseMove(object sender, MouseEventArgs e) private void Canvas_MouseMove(object sender, MouseEventArgs e)
{ {
// 气泡测量:ROI 拖拽画新矩形
if (_bubbleRoiDragging && _bubbleRoiStart.HasValue && _bubbleRoiRect != null)
{
var pos = e.GetPosition(mainCanvas);
double x = Math.Min(_bubbleRoiStart.Value.X, pos.X);
double y = Math.Min(_bubbleRoiStart.Value.Y, pos.Y);
double w = Math.Abs(pos.X - _bubbleRoiStart.Value.X);
double h = Math.Abs(pos.Y - _bubbleRoiStart.Value.Y);
Canvas.SetLeft(_bubbleRoiRect, x);
Canvas.SetTop(_bubbleRoiRect, y);
_bubbleRoiRect.Width = w;
_bubbleRoiRect.Height = h;
_bubbleRoiRect.Visibility = Visibility.Visible;
if (_bubbleRoiHandle != null) _bubbleRoiHandle.Visibility = Visibility.Collapsed;
e.Handled = true;
return;
}
// 气泡测量:拖动 ROI
if (_bubbleRoiMoving && _bubbleRoi.HasValue)
{
var pos = e.GetPosition(mainCanvas);
double nx = pos.X - _bubbleRoiDragOffset.X;
double ny = pos.Y - _bubbleRoiDragOffset.Y;
_bubbleRoi = new Rect(nx, ny, _bubbleRoi.Value.Width, _bubbleRoi.Value.Height);
SyncBubbleRoiVisuals();
e.Handled = true;
return;
}
// 气泡测量:右下角调整大小
if (_bubbleRoiResizing && _bubbleRoi.HasValue)
{
var pos = e.GetPosition(mainCanvas);
double w = Math.Max(20, pos.X - _bubbleRoi.Value.X);
double h = Math.Max(20, pos.Y - _bubbleRoi.Value.Y);
_bubbleRoi = new Rect(_bubbleRoi.Value.X, _bubbleRoi.Value.Y, w, h);
SyncBubbleRoiVisuals();
e.Handled = true;
return;
}
// 气泡测量:画笔/橡皮拖拽
if (_bubbleBrushDragging && _bubbleRoi.HasValue)
{
var pos = e.GetPosition(mainCanvas);
ApplyBrushAt(pos);
e.Handled = true;
return;
}
if (e.LeftButton == MouseButtonState.Pressed && mainCanvas.IsMouseCaptured) if (e.LeftButton == MouseButtonState.Pressed && mainCanvas.IsMouseCaptured)
{ {
Point currentPosition = e.GetPosition(imageDisplayGrid); Point currentPosition = e.GetPosition(imageDisplayGrid);
@@ -1378,6 +1619,49 @@ namespace XP.ImageProcessing.RoiControl.Controls
private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{ {
// 气泡测量:ROI 拖拽完成
if (_bubbleRoiDragging && _bubbleRoiStart.HasValue)
{
var pos = e.GetPosition(mainCanvas);
double x = Math.Min(_bubbleRoiStart.Value.X, pos.X);
double y = Math.Min(_bubbleRoiStart.Value.Y, pos.Y);
double w = Math.Abs(pos.X - _bubbleRoiStart.Value.X);
double h = Math.Abs(pos.Y - _bubbleRoiStart.Value.Y);
_bubbleRoiDragging = false;
_bubbleRoiStart = null;
mainCanvas.ReleaseMouseCapture();
if (w > 5 && h > 5)
{
_bubbleRoi = new Rect(x, y, w, h);
SyncBubbleRoiVisuals();
InitBubbleMask();
RaiseMeasureStatusChanged($"ROI 已设置: {w:F0}×{h:F0},可拖动/调整大小,或在面板切换魔棒工具");
}
e.Handled = true;
return;
}
// 气泡测量:拖动/调整完成
if (_bubbleRoiMoving || _bubbleRoiResizing)
{
_bubbleRoiMoving = false;
_bubbleRoiResizing = false;
mainCanvas.ReleaseMouseCapture();
if (_bubbleRoi.HasValue)
RaiseMeasureStatusChanged($"ROI: {_bubbleRoi.Value.Width:F0}×{_bubbleRoi.Value.Height:F0}");
e.Handled = true;
return;
}
// 气泡测量:画笔/橡皮松开
if (_bubbleBrushDragging)
{
_bubbleBrushDragging = false;
mainCanvas.ReleaseMouseCapture();
UpdateBubbleResult();
e.Handled = true;
return;
}
mainCanvas.ReleaseMouseCapture(); mainCanvas.ReleaseMouseCapture();
if (!isDragging) if (!isDragging)
@@ -7,6 +7,7 @@ namespace XP.ImageProcessing.RoiControl.Models
PointToLine, PointToLine,
Angle, Angle,
FillRate, FillRate,
BgaVoid BgaVoid,
BubbleMeasure
} }
} }
+2 -1
View File
@@ -12,7 +12,8 @@ namespace XplorePlane.Events
PointLineDistance, PointLineDistance,
Angle, Angle,
ThroughHoleFillRate, ThroughHoleFillRate,
BgaVoid BgaVoid,
BubbleMeasure
} }
/// <summary> /// <summary>
@@ -89,6 +89,7 @@ namespace XplorePlane.ViewModels
public DelegateCommand AngleMeasureCommand { get; } public DelegateCommand AngleMeasureCommand { get; }
public DelegateCommand ThroughHoleFillRateMeasureCommand { get; } public DelegateCommand ThroughHoleFillRateMeasureCommand { get; }
public DelegateCommand BgaVoidMeasureCommand { get; } public DelegateCommand BgaVoidMeasureCommand { get; }
public DelegateCommand BubbleMeasureCommand { get; }
// 辅助线命令 // 辅助线命令
public DelegateCommand ToggleCrosshairCommand { get; } public DelegateCommand ToggleCrosshairCommand { get; }
@@ -176,6 +177,7 @@ namespace XplorePlane.ViewModels
AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure); AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure);
ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure); ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure);
BgaVoidMeasureCommand = new DelegateCommand(ExecuteBgaVoidMeasure); BgaVoidMeasureCommand = new DelegateCommand(ExecuteBgaVoidMeasure);
BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure);
// 辅助线命令 // 辅助线命令
ToggleCrosshairCommand = new DelegateCommand(() => ToggleCrosshairCommand = new DelegateCommand(() =>
@@ -497,6 +499,35 @@ namespace XplorePlane.ViewModels
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BgaVoid); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BgaVoid);
} }
private Window _bubbleMeasurePanel;
private void ExecuteBubbleMeasure()
{
if (!CheckImageLoaded()) return;
_logger.Info("气泡测量功能已触发");
// 进入气泡测量模式
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BubbleMeasure);
// 弹出工具面板
if (_bubbleMeasurePanel != null && _bubbleMeasurePanel.IsVisible)
{
_bubbleMeasurePanel.Activate();
return;
}
_bubbleMeasurePanel = new Views.ImageProcessing.BubbleMeasurePanel
{
Owner = System.Windows.Application.Current.MainWindow
};
_bubbleMeasurePanel.Closed += (s, e) =>
{
// 关闭面板时退出气泡测量模式
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
};
_bubbleMeasurePanel.Show();
}
#endregion #endregion
#region #region
@@ -0,0 +1,58 @@
<Window
x:Class="XplorePlane.Views.ImageProcessing.BubbleMeasurePanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="气泡测量工具"
Width="260" Height="320"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
Topmost="True"
ShowInTaskbar="False">
<StackPanel Margin="10">
<!-- 工具选择 -->
<TextBlock Text="工具" FontWeight="SemiBold" Margin="0,0,0,4" />
<WrapPanel>
<RadioButton x:Name="RbRoi" Content="ROI" IsChecked="True" Margin="0,0,12,4" />
<RadioButton x:Name="RbWand" Content="魔棒" Margin="0,0,12,4" />
<RadioButton x:Name="RbBrush" Content="画笔" Margin="0,0,12,4" />
<RadioButton x:Name="RbEraser" Content="橡皮擦" Margin="0,0,0,4" />
</WrapPanel>
<Separator Margin="0,6" />
<!-- 阈值 -->
<TextBlock Text="灰度阈值" Margin="0,0,0,4" />
<DockPanel>
<TextBox x:Name="TbThreshold" DockPanel.Dock="Right" Width="45" Text="128"
VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider x:Name="SliderThreshold" Minimum="0" Maximum="255" Value="128"
VerticalAlignment="Center" />
</DockPanel>
<!-- 画笔大小 -->
<TextBlock Text="画笔/橡皮大小" Margin="0,8,0,4" />
<DockPanel>
<TextBox x:Name="TbBrushSize" DockPanel.Dock="Right" Width="45" Text="5"
VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider x:Name="SliderBrushSize" Minimum="1" Maximum="30" Value="5"
VerticalAlignment="Center" />
</DockPanel>
<Separator Margin="0,8" />
<!-- 结果 -->
<TextBlock Text="测量结果" FontWeight="SemiBold" Margin="0,0,0,4" />
<TextBlock x:Name="TbResult" Text="空隙率: --" FontSize="14" Margin="0,0,0,4" />
<DockPanel Margin="0,0,0,8">
<TextBlock Text="VoidLimit(%):" VerticalAlignment="Center" />
<TextBox x:Name="TbVoidLimit" Width="50" Text="25.0" Margin="6,0,0,0"
VerticalContentAlignment="Center" />
</DockPanel>
<!-- 操作按钮 -->
<WrapPanel HorizontalAlignment="Center">
<Button Content="清除标记" Padding="12,4" Margin="0,0,8,0" Click="ClearMask_Click" />
<Button Content="完成" Padding="12,4" Click="Finish_Click" />
</WrapPanel>
</StackPanel>
</Window>
@@ -0,0 +1,85 @@
using System.Windows;
using Prism.Ioc;
using XP.ImageProcessing.RoiControl.Controls;
using XplorePlane.ViewModels;
namespace XplorePlane.Views.ImageProcessing
{
public partial class BubbleMeasurePanel : Window
{
private PolygonRoiCanvas _canvas;
public BubbleMeasurePanel()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
// 获取主界面的 RoiCanvas
try
{
var mainWin = Owner as MainWindow;
if (mainWin != null)
{
_canvas = FindChild<PolygonRoiCanvas>(mainWin);
}
}
catch { }
// 工具切换
RbRoi.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Roi);
RbWand.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Wand);
RbBrush.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Brush);
RbEraser.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Eraser);
// 阈值同步
SliderThreshold.ValueChanged += (s, ev) =>
{
TbThreshold.Text = ((int)SliderThreshold.Value).ToString();
_canvas?.SetBubbleThreshold((int)SliderThreshold.Value);
};
// 画笔大小同步
SliderBrushSize.ValueChanged += (s, ev) =>
{
TbBrushSize.Text = ((int)SliderBrushSize.Value).ToString();
_canvas?.SetBubbleBrushSize((int)SliderBrushSize.Value);
};
// 监听 canvas 的工具切换事件(ROI 画完后自动切换时同步面板)
if (_canvas != null)
{
_canvas.BubbleToolChanged += (s, ev) =>
{
// 暂不自动切换面板 radio button
};
}
}
private void ClearMask_Click(object sender, RoutedEventArgs e)
{
_canvas?.ClearBubbleMeasure();
TbResult.Text = "空隙率: --";
}
private void Finish_Click(object sender, RoutedEventArgs e)
{
Close();
}
private static T FindChild<T>(DependencyObject parent) where T : DependencyObject
{
int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < count; i++)
{
var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i);
if (child is T t) return t;
var result = FindChild<T>(child);
if (result != null) return result;
}
return null;
}
}
}
@@ -79,6 +79,7 @@ namespace XplorePlane.Views
MeasurementToolMode.Angle => XP.ImageProcessing.RoiControl.Models.MeasureMode.Angle, MeasurementToolMode.Angle => XP.ImageProcessing.RoiControl.Models.MeasureMode.Angle,
MeasurementToolMode.ThroughHoleFillRate => XP.ImageProcessing.RoiControl.Models.MeasureMode.FillRate, MeasurementToolMode.ThroughHoleFillRate => XP.ImageProcessing.RoiControl.Models.MeasureMode.FillRate,
MeasurementToolMode.BgaVoid => XP.ImageProcessing.RoiControl.Models.MeasureMode.BgaVoid, MeasurementToolMode.BgaVoid => XP.ImageProcessing.RoiControl.Models.MeasureMode.BgaVoid,
MeasurementToolMode.BubbleMeasure => XP.ImageProcessing.RoiControl.Models.MeasureMode.BubbleMeasure,
_ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None _ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None
}; };
}, Prism.Events.ThreadOption.UIThread); }, Prism.Events.ThreadOption.UIThread);