已合并 PR 51: 添加手动工具

1.集成测量工具体系:点点距、点线距、角度测量,十字辅助线,右键菜单
2.通孔填锡率测量:四椭圆交互、中心拖拽、长短轴手柄调整、实时计算与 PASS/FAIL 判定
3.BGA 空隙测量:画气泡圆+焊球圆交互、拖拽调整、空隙率计算、VoidLimit 编辑
4.气泡测量完整流程:矩形 ROI 绘制、魔棒 flood fill、画笔涂抹/橡皮擦、撤销功能、空隙率实时计算与判定标签
5.BGA 辅助面板:气泡/焊球模式切换、VoidLimit 面板调节
6.测量工具标号与自动重编号
7.状态栏优化、图像加载检查、ViewModel 单例注册修复
**8.修复自动合并时产生的BUG**
This commit is contained in:
LI Wei.lw
2026-04-28 10:49:45 +08:00
18 changed files with 2073 additions and 344 deletions
@@ -41,178 +41,6 @@
} }
} }
}, },
"Emgu.CV.runtime.windows/4.10.0.5680": {
"dependencies": {
"Emgu.CV": "4.10.0.5680",
"Emgu.runtime.windows.msvc.rt.arm64": "19.42.34435",
"Emgu.runtime.windows.msvc.rt.x64": "19.42.34435",
"Emgu.runtime.windows.msvc.rt.x86": "19.42.34435"
},
"runtimeTargets": {
"runtimes/win-arm64/native/cvextern.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "4.10.0.5680"
},
"runtimes/win-x64/native/cvextern.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "4.10.0.5680"
},
"runtimes/win-x64/native/libusb-1.0.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-x64/native/opencv_videoio_ffmpeg4100_64.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "2024.5.0.0"
},
"runtimes/win-x86/native/cvextern.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "4.10.0.5680"
},
"runtimes/win-x86/native/libusb-1.0.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-x86/native/opencv_videoio_ffmpeg4100.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "2024.5.0.0"
}
}
},
"Emgu.runtime.windows.msvc.rt.arm64/19.42.34435": {
"runtimeTargets": {
"runtimes/win-arm64/native/concrt140.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-arm64/native/msvcp140.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-arm64/native/msvcp140_1.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-arm64/native/msvcp140_2.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-arm64/native/msvcp140_atomic_wait.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-arm64/native/msvcp140_codecvt_ids.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-arm64/native/vcruntime140.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-arm64/native/vcruntime140_1.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
}
}
},
"Emgu.runtime.windows.msvc.rt.x64/19.42.34435": {
"runtimeTargets": {
"runtimes/win-x64/native/concrt140.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x64/native/msvcp140.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x64/native/msvcp140_1.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x64/native/msvcp140_2.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x64/native/msvcp140_atomic_wait.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x64/native/msvcp140_codecvt_ids.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x64/native/vcruntime140.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x64/native/vcruntime140_1.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "14.42.34433.0"
}
}
},
"Emgu.runtime.windows.msvc.rt.x86/19.42.34435": {
"runtimeTargets": {
"runtimes/win-x86/native/concrt140.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x86/native/msvcp140.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x86/native/msvcp140_1.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x86/native/msvcp140_2.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x86/native/msvcp140_atomic_wait.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x86/native/msvcp140_codecvt_ids.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "14.42.34433.0"
},
"runtimes/win-x86/native/vcruntime140.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "14.42.34433.0"
}
}
},
"Microsoft.Bcl.AsyncInterfaces/1.1.1": { "Microsoft.Bcl.AsyncInterfaces/1.1.1": {
"runtime": { "runtime": {
"lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": { "lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": {
@@ -1868,7 +1696,6 @@
"XP.Common/1.0.0": { "XP.Common/1.0.0": {
"dependencies": { "dependencies": {
"Emgu.CV": "4.10.0.5680", "Emgu.CV": "4.10.0.5680",
"Emgu.CV.runtime.windows": "4.10.0.5680",
"Microsoft.Data.Sqlite": "10.0.3", "Microsoft.Data.Sqlite": "10.0.3",
"Prism.Wpf": "9.0.537", "Prism.Wpf": "9.0.537",
"Serilog": "4.3.1", "Serilog": "4.3.1",
@@ -1910,34 +1737,6 @@
"path": "emgu.cv/4.10.0.5680", "path": "emgu.cv/4.10.0.5680",
"hashPath": "emgu.cv.4.10.0.5680.nupkg.sha512" "hashPath": "emgu.cv.4.10.0.5680.nupkg.sha512"
}, },
"Emgu.CV.runtime.windows/4.10.0.5680": {
"type": "package",
"serviceable": true,
"sha512": "sha512-WzBJWWENbF1oQKBo92ODtt9V/Eh4MFo8Y3MxxpWdoKJnZAjStxlE5epy3rB7442ZPAIRAiAaqPPZfrdml5Q9vQ==",
"path": "emgu.cv.runtime.windows/4.10.0.5680",
"hashPath": "emgu.cv.runtime.windows.4.10.0.5680.nupkg.sha512"
},
"Emgu.runtime.windows.msvc.rt.arm64/19.42.34435": {
"type": "package",
"serviceable": true,
"sha512": "sha512-tb+JdhPLkX0MsMweKhL8zY/XUm+opxl6FLP7fyMf93EqUrUmR3YAyxmzcTyDyndh4XQ4bl+Eain9ugm7y/e26g==",
"path": "emgu.runtime.windows.msvc.rt.arm64/19.42.34435",
"hashPath": "emgu.runtime.windows.msvc.rt.arm64.19.42.34435.nupkg.sha512"
},
"Emgu.runtime.windows.msvc.rt.x64/19.42.34435": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3aIW16hRIV05a9oRW899LjPhdea/FJamPwZulwNB2P9YItwq+6XMqMeV3lg80giIQalAHQlVRVbwC54N6q3NMw==",
"path": "emgu.runtime.windows.msvc.rt.x64/19.42.34435",
"hashPath": "emgu.runtime.windows.msvc.rt.x64.19.42.34435.nupkg.sha512"
},
"Emgu.runtime.windows.msvc.rt.x86/19.42.34435": {
"type": "package",
"serviceable": true,
"sha512": "sha512-NrCzZrq+S9jJOLWgh0ybVeHEQgillrASOFhlvV7FRrW1ufxCI3uZgySIpsGYSWQaJYpi5A4jfTP6Ekcjl3shDw==",
"path": "emgu.runtime.windows.msvc.rt.x86/19.42.34435",
"hashPath": "emgu.runtime.windows.msvc.rt.x86.19.42.34435.nupkg.sha512"
},
"Microsoft.Bcl.AsyncInterfaces/1.1.1": { "Microsoft.Bcl.AsyncInterfaces/1.1.1": {
"type": "package", "type": "package",
"serviceable": true, "serviceable": true,
@@ -41,7 +41,8 @@
MouseLeftButtonDown="Canvas_MouseLeftButtonDown" MouseLeftButtonDown="Canvas_MouseLeftButtonDown"
MouseLeftButtonUp="Canvas_MouseLeftButtonUp" MouseLeftButtonUp="Canvas_MouseLeftButtonUp"
MouseMove="Canvas_MouseMove" MouseMove="Canvas_MouseMove"
MouseRightButtonDown="Canvas_MouseRightButtonDown"> MouseRightButtonDown="Canvas_MouseRightButtonDown"
PreviewMouseRightButtonUp="Canvas_PreviewMouseRightButtonUp">
<!-- 背景图像 --> <!-- 背景图像 -->
<Image x:Name="backgroundImage" <Image x:Name="backgroundImage"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace XP.ImageProcessing.RoiControl.Models
{
/// <summary>一次角度测量的所有视觉元素(顶点V + 射线端点A/B + 两条射线 + 弧线 + 标签)</summary>
public class AngleGroup
{
public Ellipse DotV { get; set; } // 顶点
public Ellipse DotA { get; set; } // 射线端点A
public Ellipse DotB { get; set; } // 射线端点B
public Line LineA { get; set; } // 射线VA
public Line LineB { get; set; } // 射线VB
public Path Arc { get; set; } // 角度弧线
public TextBlock Label { get; set; }
public Point V { get; set; }
public Point A { get; set; }
public Point B { get; set; }
public int Index { get; set; }
public double AngleDeg
{
get
{
double vax = A.X - V.X, vay = A.Y - V.Y;
double vbx = B.X - V.X, vby = B.Y - V.Y;
double lenA = Math.Sqrt(vax * vax + vay * vay);
double lenB = Math.Sqrt(vbx * vbx + vby * vby);
if (lenA < 0.001 || lenB < 0.001) return 0;
double dot = vax * vbx + vay * vby;
double cos = Math.Clamp(dot / (lenA * lenB), -1.0, 1.0);
return Math.Acos(cos) * 180.0 / Math.PI;
}
}
public void UpdateVisuals()
{
// 射线
LineA.X1 = V.X; LineA.Y1 = V.Y; LineA.X2 = A.X; LineA.Y2 = A.Y;
LineA.Visibility = Visibility.Visible;
LineB.X1 = V.X; LineB.Y1 = V.Y; LineB.X2 = B.X; LineB.Y2 = B.Y;
LineB.Visibility = Visibility.Visible;
// 弧线
double vax = A.X - V.X, vay = A.Y - V.Y;
double vbx = B.X - V.X, vby = B.Y - V.Y;
double lenA = Math.Sqrt(vax * vax + vay * vay);
double lenB = Math.Sqrt(vbx * vbx + vby * vby);
if (lenA < 1 || lenB < 1) { Arc.Visibility = Visibility.Collapsed; return; }
double arcRadius = Math.Min(30, Math.Min(lenA, lenB) * 0.3);
if (arcRadius < 8) arcRadius = 8;
double angleARad = Math.Atan2(vay, vax);
double angleBRad = Math.Atan2(vby, vbx);
double cross = vax * vby - vay * vbx;
double angleDeg = AngleDeg;
bool isLargeArc = angleDeg > 180;
var sweepDir = cross >= 0 ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;
var startPt = new Point(V.X + arcRadius * Math.Cos(angleARad), V.Y + arcRadius * Math.Sin(angleARad));
var endPt = new Point(V.X + arcRadius * Math.Cos(angleBRad), V.Y + arcRadius * Math.Sin(angleBRad));
var arcSeg = new ArcSegment(endPt, new Size(arcRadius, arcRadius), 0, isLargeArc, sweepDir, true);
var fig = new PathFigure(startPt, new[] { arcSeg }, false);
Arc.Data = new PathGeometry(new[] { fig });
Arc.Visibility = Visibility.Visible;
// 标签位置
double midAngle = (angleARad + angleBRad) / 2.0;
double testX = Math.Cos(midAngle), testY = Math.Sin(midAngle);
double testCross = vax * testY - vay * testX;
if ((cross >= 0 && testCross < 0) || (cross < 0 && testCross >= 0))
midAngle += Math.PI;
double labelDist = arcRadius + 16;
Label.Text = (Index > 0 ? $"#{Index} " : "") + $"{angleDeg:F1}°";
Canvas.SetLeft(Label, V.X + labelDist * Math.Cos(midAngle) - 15);
Canvas.SetTop(Label, V.Y + labelDist * Math.Sin(midAngle) - 8);
Label.Visibility = Visibility.Visible;
}
}
}
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace XP.ImageProcessing.RoiControl.Models
{
/// <summary>BGA 空隙测量中的一个圆(气泡或焊球)</summary>
public class BgaCircle
{
public Ellipse Shape { get; set; } // 圆形轮廓
public Ellipse CenterDot { get; set; } // 中心拖拽点
public Ellipse EdgeDot { get; set; } // 边缘拖拽点(调半径)
public Point Center { get; set; }
public double Radius { get; set; }
public bool IsBall { get; set; } // true=焊球, false=气泡
public double Area => Math.PI * Radius * Radius;
public void UpdateVisuals()
{
double d = Radius * 2;
Shape.Width = d; Shape.Height = d;
Canvas.SetLeft(Shape, Center.X - Radius);
Canvas.SetTop(Shape, Center.Y - Radius);
Canvas.SetLeft(CenterDot, Center.X - CenterDot.Width / 2);
Canvas.SetTop(CenterDot, Center.Y - CenterDot.Height / 2);
var edgePt = new Point(Center.X + Radius, Center.Y);
Canvas.SetLeft(EdgeDot, edgePt.X - EdgeDot.Width / 2);
Canvas.SetTop(EdgeDot, edgePt.Y - EdgeDot.Height / 2);
}
}
/// <summary>一次 BGA 空隙测量组(1个焊球 + N个气泡 + 标签)</summary>
public class BgaVoidGroup
{
public BgaCircle Ball { get; set; }
public List<BgaCircle> Voids { get; } = new();
public TextBlock Label { get; set; }
public int Index { get; set; }
public double VoidLimit { get; set; } = 25.0;
public double VoidRate
{
get
{
if (Ball == null || Ball.Area < 1) return 0;
double totalVoid = 0;
foreach (var v in Voids) totalVoid += v.Area;
return Math.Clamp(totalVoid / Ball.Area * 100.0, 0, 100);
}
}
public string Classification => VoidRate <= VoidLimit ? "PASS" : "FAIL";
public void UpdateLabel()
{
if (Label == null || Ball == null) return;
double rate = VoidRate;
string cls = Classification;
Label.Text = (Index > 0 ? $"#{Index} " : "") +
$"Void: {rate:F1}% | Limit: {VoidLimit:F1}% | {cls}";
Label.Foreground = cls == "PASS" ? Brushes.Lime : Brushes.Red;
Canvas.SetLeft(Label, Ball.Center.X + Ball.Radius + 10);
Canvas.SetTop(Label, Ball.Center.Y - 10);
Label.Visibility = Visibility.Visible;
}
/// <summary>获取所有 UI 元素</summary>
public List<UIElement> AllElements
{
get
{
var list = new List<UIElement>();
if (Ball != null) { list.Add(Ball.Shape); list.Add(Ball.CenterDot); list.Add(Ball.EdgeDot); }
foreach (var v in Voids) { list.Add(v.Shape); list.Add(v.CenterDot); list.Add(v.EdgeDot); }
if (Label != null) list.Add(Label);
return list;
}
}
}
}
@@ -0,0 +1,149 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace XP.ImageProcessing.RoiControl.Models
{
/// <summary>通孔填锡率测量组(4个椭圆中心 + 连线 + 标签)</summary>
public class FillRateGroup
{
// 四个椭圆中心的可拖拽圆点
public Ellipse DotE1 { get; set; } // 底部(蓝)
public Ellipse DotE2 { get; set; } // 顶部(青)
public Ellipse DotE3 { get; set; } // 填锡起点(黄)
public Ellipse DotE4 { get; set; } // 填锡终点(绿)
// 长轴/短轴手柄(每个椭圆2个)
public Ellipse E1AH { get; set; }
public Ellipse E1BH { get; set; }
public Ellipse E2AH { get; set; }
public Ellipse E2BH { get; set; }
public Ellipse E3AH { get; set; }
public Ellipse E3BH { get; set; }
public Ellipse E4AH { get; set; }
public Ellipse E4BH { get; set; }
// 椭圆路径
public Path PathE1 { get; set; }
public Path PathE2 { get; set; }
public Path PathE3 { get; set; }
public Path PathE4 { get; set; }
// 连线:E1→E2(全高度),E3→E4(填锡高度)
public Line FullLine { get; set; }
public Line FillLine { get; set; }
public TextBlock Label { get; set; }
// 四个椭圆中心坐标
public Point E1 { get; set; }
public Point E2 { get; set; }
public Point E3 { get; set; }
public Point E4 { get; set; }
// 椭圆轴参数(默认值,后续可拖拽调整)
public double E1A { get; set; } = 60;
public double E1B { get; set; } = 50;
public double E1Angle { get; set; }
public double E2A { get; set; } = 60;
public double E2B { get; set; } = 50;
public double E2Angle { get; set; }
public double E3A { get; set; } = 60;
public double E3B { get; set; } = 50;
public double E3Angle { get; set; }
public double E4A { get; set; } = 55;
public double E4B { get; set; } = 45;
public double E4Angle { get; set; }
public double THTLimit { get; set; } = 75.0;
public int Index { get; set; }
public double FillRate
{
get
{
double fullDx = E2.X - E1.X, fullDy = E2.Y - E1.Y;
double fullDist = Math.Sqrt(fullDx * fullDx + fullDy * fullDy);
double fillDx = E4.X - E3.X, fillDy = E4.Y - E3.Y;
double fillDist = Math.Sqrt(fillDx * fillDx + fillDy * fillDy);
return fullDist > 0 ? Math.Clamp(fillDist / fullDist * 100.0, 0, 100) : 0;
}
}
public string Classification => FillRate >= THTLimit ? "PASS" : "FAIL";
public void UpdateVisuals()
{
UpdateEllipsePath(PathE1, E1, E1A, E1B, E1Angle);
UpdateEllipsePath(PathE2, E2, E2A, E2B, E2Angle);
UpdateEllipsePath(PathE3, E3, E3A, E3B, E3Angle);
UpdateEllipsePath(PathE4, E4, E4A, E4B, E4Angle);
FullLine.X1 = E1.X; FullLine.Y1 = E1.Y;
FullLine.X2 = E2.X; FullLine.Y2 = E2.Y;
FullLine.Visibility = Visibility.Visible;
FillLine.X1 = E3.X; FillLine.Y1 = E3.Y;
FillLine.X2 = E4.X; FillLine.Y2 = E4.Y;
FillLine.Visibility = Visibility.Visible;
// 轴手柄定位
SetHandle(E1AH, EdgePoint(E1, E1A, E1Angle));
SetHandle(E1BH, EdgePoint(E1, E1B, E1Angle + 90));
SetHandle(E2AH, EdgePoint(E2, E2A, E2Angle));
SetHandle(E2BH, EdgePoint(E2, E2B, E2Angle + 90));
SetHandle(E3AH, EdgePoint(E3, E3A, E3Angle));
SetHandle(E3BH, EdgePoint(E3, E3B, E3Angle + 90));
SetHandle(E4AH, EdgePoint(E4, E4A, E4Angle));
SetHandle(E4BH, EdgePoint(E4, E4B, E4Angle + 90));
double rate = FillRate;
string cls = Classification;
Label.Text = (Index > 0 ? $"#{Index} " : "") + $"Fill: {rate:F1}% | THTLimit: {THTLimit:F1}% | {cls}";
Label.Foreground = cls == "PASS" ? Brushes.Lime : Brushes.Red;
double labelX = Math.Max(Math.Max(E1.X, E2.X), Math.Max(E3.X, E4.X)) + 15;
double labelY = (E1.Y + E2.Y) / 2;
Canvas.SetLeft(Label, labelX);
Canvas.SetTop(Label, labelY - 10);
Label.Visibility = Visibility.Visible;
}
/// <summary>获取所有 UI 元素(用于添加/移除)</summary>
public UIElement[] AllElements => new UIElement[]
{
PathE1, PathE2, PathE3, PathE4, FullLine, FillLine, Label,
DotE1, DotE2, DotE3, DotE4,
E1AH, E1BH, E2AH, E2BH, E3AH, E3BH, E4AH, E4BH
};
private static Point EdgePoint(Point center, double radius, double angleDeg)
{
double rad = angleDeg * Math.PI / 180.0;
return new Point(center.X + radius * Math.Cos(rad), center.Y + radius * Math.Sin(rad));
}
private static void SetHandle(Ellipse h, Point pos)
{
if (h == null) return;
Canvas.SetLeft(h, pos.X - h.Width / 2);
Canvas.SetTop(h, pos.Y - h.Height / 2);
}
public static double Dist(Point a, Point b)
{
double dx = b.X - a.X, dy = b.Y - a.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
private static void UpdateEllipsePath(Path path, Point center, double a, double b, double angleDeg)
{
var eg = new EllipseGeometry(center, a, b);
if (Math.Abs(angleDeg) > 0.01)
eg.Transform = new RotateTransform(angleDeg, center.X, center.Y);
path.Data = eg;
path.Visibility = Visibility.Visible;
}
}
}
@@ -34,10 +34,12 @@ namespace XP.ImageProcessing.RoiControl.Models
public void UpdateLabel(string distanceText = null) public void UpdateLabel(string distanceText = null)
{ {
Label.Text = distanceText ?? $"{Distance:F2} px"; Label.Text = (Index > 0 ? $"#{Index} " : "") + (distanceText ?? $"{Distance:F2} px");
Canvas.SetLeft(Label, (P1.X + P2.X) / 2 + 8); Canvas.SetLeft(Label, (P1.X + P2.X) / 2 + 8);
Canvas.SetTop(Label, (P1.Y + P2.Y) / 2 - 18); Canvas.SetTop(Label, (P1.Y + P2.Y) / 2 - 18);
Label.Visibility = Visibility.Visible; Label.Visibility = Visibility.Visible;
} }
public int Index { get; set; }
} }
} }
@@ -4,6 +4,10 @@ namespace XP.ImageProcessing.RoiControl.Models
{ {
None, None,
PointDistance, PointDistance,
PointToLine PointToLine,
Angle,
FillRate,
BgaVoid,
BubbleMeasure
} }
} }
@@ -20,6 +20,7 @@ namespace XP.ImageProcessing.RoiControl.Models
public Point L1 { get; set; } public Point L1 { get; set; }
public Point L2 { get; set; } public Point L2 { get; set; }
public Point P { get; set; } public Point P { get; set; }
public int Index { get; set; }
public double Distance public double Distance
{ {
@@ -85,7 +86,7 @@ namespace XP.ImageProcessing.RoiControl.Models
FootDot.Visibility = Visibility.Visible; FootDot.Visibility = Visibility.Visible;
// 标签 // 标签
Label.Text = distanceText ?? $"{Distance:F2} px"; Label.Text = (Index > 0 ? $"#{Index} " : "") + (distanceText ?? $"{Distance:F2} px");
Canvas.SetLeft(Label, (P.X + foot.X) / 2 + 8); Canvas.SetLeft(Label, (P.X + foot.X) / 2 + 8);
Canvas.SetTop(Label, (P.Y + foot.Y) / 2 - 18); Canvas.SetTop(Label, (P.Y + foot.Y) / 2 - 18);
Label.Visibility = Visibility.Visible; Label.Visibility = Visibility.Visible;
+5 -20
View File
@@ -282,7 +282,8 @@ namespace XplorePlane
// 主窗口加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化 // 主窗口加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化
shell.Loaded += (s, e) => shell.Loaded += (s, e) =>
{ {
TryConnectCamera(); // [DEV] 导航相机连接已屏蔽,开发阶段跳过以加快启动速度
// TryConnectCamera();
// 初始化主界面探测器帧流水线,开始接收探测器图像事件 // 初始化主界面探测器帧流水线,开始接收探测器图像事件
try try
@@ -294,24 +295,7 @@ namespace XplorePlane
Log.Error(ex, "初始化主界面探测器帧流水线失败"); Log.Error(ex, "初始化主界面探测器帧流水线失败");
} }
// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误 // [DEV] 相机状态通知已屏蔽
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);
}
//TryConnectCamera();
//// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误
// try // try
// { // {
// var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>(); // var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>();
@@ -378,7 +362,8 @@ namespace XplorePlane
// 注册视图和视图模型 // 注册视图和视图模型
containerRegistry.RegisterForNavigation<MainWindow>(); containerRegistry.RegisterForNavigation<MainWindow>();
containerRegistry.Register<MainViewModel>(); containerRegistry.RegisterSingleton<MainViewModel>();
containerRegistry.RegisterSingleton<ViewportPanelViewModel>();
containerRegistry.RegisterSingleton<NavigationPropertyPanelViewModel>(); containerRegistry.RegisterSingleton<NavigationPropertyPanelViewModel>();
// 注册图像处理服务与视图 // 注册图像处理服务与视图
+3 -1
View File
@@ -11,7 +11,9 @@ namespace XplorePlane.Events
PointDistance, PointDistance,
PointLineDistance, PointLineDistance,
Angle, Angle,
ThroughHoleFillRate ThroughHoleFillRate,
BgaVoid,
BubbleMeasure
} }
/// <summary> /// <summary>
+189 -86
View File
@@ -15,6 +15,7 @@ using XplorePlane.ViewModels.Cnc;
using XplorePlane.Views; using XplorePlane.Views;
using XplorePlane.Views.Cnc; using XplorePlane.Views.Cnc;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XP.Common.GeneralForm.Views;
using XP.Common.PdfViewer.Interfaces; using XP.Common.PdfViewer.Interfaces;
using XP.Hardware.MotionControl.Abstractions; using XP.Hardware.MotionControl.Abstractions;
@@ -31,6 +32,113 @@ namespace XplorePlane.ViewModels
private readonly CncEditorViewModel _cncEditorViewModel; private readonly CncEditorViewModel _cncEditorViewModel;
private readonly CncPageView _cncPageView; private readonly CncPageView _cncPageView;
public string LicenseInfo
{
get => _licenseInfo;
set => SetProperty(ref _licenseInfo, value);
}
private string _statusMessage = "就绪";
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
public ObservableCollection<object> NavigationTree { get; set; }
// 导航命令
public DelegateCommand NavigateHomeCommand { get; set; }
public DelegateCommand NavigateInspectCommand { get; set; }
public DelegateCommand OpenFileCommand { get; set; }
public DelegateCommand ExportCommand { get; set; }
public DelegateCommand ClearCommand { get; set; }
public DelegateCommand EditPropertiesCommand { get; set; }
// 窗口打开命令
public DelegateCommand OpenImageProcessingCommand { get; }
public DelegateCommand LoadImageCommand { get; }
public DelegateCommand OpenPipelineEditorCommand { get; }
public DelegateCommand OpenCncEditorCommand { get; }
public DelegateCommand OpenMatrixEditorCommand { get; }
public DelegateCommand OpenToolboxCommand { get; }
public DelegateCommand OpenLibraryVersionsCommand { get; }
public DelegateCommand OpenUserManualCommand { get; }
public DelegateCommand OpenCameraSettingsCommand { get; }
public DelegateCommand NewCncProgramCommand { get; }
public DelegateCommand SaveCncProgramCommand { get; }
public DelegateCommand LoadCncProgramCommand { get; }
public DelegateCommand InsertReferencePointCommand { get; }
public DelegateCommand InsertSavePositionCommand { get; }
public DelegateCommand InsertCompleteProgramCommand { get; }
public DelegateCommand InsertInspectionMarkerCommand { get; }
public DelegateCommand InsertInspectionModuleCommand { get; }
public DelegateCommand InsertSaveNodeCommand { get; }
public DelegateCommand InsertPauseDialogCommand { get; }
public DelegateCommand InsertWaitDelayCommand { get; }
// 硬件命令
public DelegateCommand AxisResetCommand { get; }
public DelegateCommand OpenDetectorConfigCommand { get; }
public DelegateCommand OpenMotionDebugCommand { get; }
public DelegateCommand OpenPlcAddrConfigCommand { get; }
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 BgaVoidMeasureCommand { get; }
public DelegateCommand BubbleMeasureCommand { get; }
// 辅助线命令
public DelegateCommand ToggleCrosshairCommand { get; }
// 设置命令
public DelegateCommand OpenLanguageSwitcherCommand { get; }
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
public DelegateCommand UseLiveDetectorSourceCommand { get; }
public bool IsMainViewportRealtimeEnabled
{
get => _mainViewportService.IsRealtimeDisplayEnabled;
set
{
if (_mainViewportService.IsRealtimeDisplayEnabled == value)
return;
_mainViewportService.SetRealtimeDisplayEnabled(value);
RaisePropertyChanged();
}
}
public bool IsUsingLiveDetectorSource => _mainViewportService.CurrentSourceMode == MainViewportSourceMode.LiveDetector;
/// <summary>右侧图像区域内容 | Right-side image panel content</summary>
public object ImagePanelContent
{
get => _imagePanelContent;
set => SetProperty(ref _imagePanelContent, value);
}
/// <summary>右侧图像区域宽度 | Right-side image panel width</summary>
public GridLength ImagePanelWidth
{
get => _imagePanelWidth;
set => SetProperty(ref _imagePanelWidth, value);
}
/// <summary>主视图区宽度 | Main viewport width</summary>
public GridLength ViewportPanelWidth
{
get => _viewportPanelWidth;
set => SetProperty(ref _viewportPanelWidth, value);
}
// 窗口引用(单例窗口防止重复打开)
private Window _motionDebugWindow; private Window _motionDebugWindow;
private Window _detectorConfigWindow; private Window _detectorConfigWindow;
private Window _plcAddrConfigWindow; private Window _plcAddrConfigWindow;
@@ -92,6 +200,11 @@ namespace XplorePlane.ViewModels
PointLineDistanceMeasureCommand = new DelegateCommand(ExecutePointLineDistanceMeasure); PointLineDistanceMeasureCommand = new DelegateCommand(ExecutePointLineDistanceMeasure);
AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure); AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure);
ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure); ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure);
BgaVoidMeasureCommand = new DelegateCommand(ExecuteBgaVoidMeasure);
BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure);
// 辅助线命令
ToggleCrosshairCommand = new DelegateCommand(() => ToggleCrosshairCommand = new DelegateCommand(() =>
_eventAggregator.GetEvent<ToggleCrosshairEvent>().Publish()); _eventAggregator.GetEvent<ToggleCrosshairEvent>().Publish());
@@ -113,92 +226,6 @@ namespace XplorePlane.ViewModels
_logger.Info("MainViewModel 已初始化"); _logger.Info("MainViewModel 已初始化");
} }
public string LicenseInfo
{
get => _licenseInfo;
set => SetProperty(ref _licenseInfo, value);
}
public ObservableCollection<object> NavigationTree { get; set; }
public DelegateCommand NavigateHomeCommand { get; set; }
public DelegateCommand NavigateInspectCommand { get; set; }
public DelegateCommand OpenFileCommand { get; set; }
public DelegateCommand ExportCommand { get; set; }
public DelegateCommand ClearCommand { get; set; }
public DelegateCommand EditPropertiesCommand { get; set; }
public DelegateCommand OpenImageProcessingCommand { get; }
public DelegateCommand LoadImageCommand { get; }
public DelegateCommand OpenPipelineEditorCommand { get; }
public DelegateCommand OpenCncEditorCommand { get; }
public DelegateCommand OpenMatrixEditorCommand { get; }
public DelegateCommand OpenToolboxCommand { get; }
public DelegateCommand OpenLibraryVersionsCommand { get; }
public DelegateCommand OpenUserManualCommand { get; }
public DelegateCommand OpenCameraSettingsCommand { get; }
public DelegateCommand NewCncProgramCommand { get; }
public DelegateCommand SaveCncProgramCommand { get; }
public DelegateCommand LoadCncProgramCommand { get; }
public DelegateCommand InsertReferencePointCommand { get; }
public DelegateCommand InsertSavePositionCommand { get; }
public DelegateCommand InsertCompleteProgramCommand { get; }
public DelegateCommand InsertInspectionMarkerCommand { get; }
public DelegateCommand InsertInspectionModuleCommand { get; }
public DelegateCommand InsertSaveNodeCommand { get; }
public DelegateCommand InsertPauseDialogCommand { get; }
public DelegateCommand InsertWaitDelayCommand { get; }
public DelegateCommand AxisResetCommand { get; }
public DelegateCommand OpenDetectorConfigCommand { get; }
public DelegateCommand OpenMotionDebugCommand { get; }
public DelegateCommand OpenPlcAddrConfigCommand { get; }
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; }
public DelegateCommand UseLiveDetectorSourceCommand { get; }
public bool IsMainViewportRealtimeEnabled
{
get => _mainViewportService.IsRealtimeDisplayEnabled;
set
{
if (_mainViewportService.IsRealtimeDisplayEnabled == value)
return;
_mainViewportService.SetRealtimeDisplayEnabled(value);
RaisePropertyChanged();
}
}
public bool IsUsingLiveDetectorSource => _mainViewportService.CurrentSourceMode == MainViewportSourceMode.LiveDetector;
public object ImagePanelContent
{
get => _imagePanelContent;
set => SetProperty(ref _imagePanelContent, value);
}
public GridLength ImagePanelWidth
{
get => _imagePanelWidth;
set => SetProperty(ref _imagePanelWidth, value);
}
public GridLength ViewportPanelWidth
{
get => _viewportPanelWidth;
set => SetProperty(ref _viewportPanelWidth, value);
}
private void ShowWindow(Window window, string name) private void ShowWindow(Window window, string name)
{ {
window.Owner = Application.Current.MainWindow; window.Owner = Application.Current.MainWindow;
@@ -436,30 +463,104 @@ namespace XplorePlane.ViewModels
} }
} }
#region
private bool CheckImageLoaded()
{
try
{
var viewportVm = _containerProvider.Resolve<ViewportPanelViewModel>();
if (viewportVm?.ImageSource != null) return true;
}
catch { }
HexMessageBox.Show("请先加载图像", MessageBoxButton.OK, MessageBoxImage.Information);
return false;
}
private void ExecutePointDistanceMeasure() private void ExecutePointDistanceMeasure()
{ {
if (!CheckImageLoaded()) return;
_logger.Info("点点距测量功能已触发"); _logger.Info("点点距测量功能已触发");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointDistance); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointDistance);
} }
private void ExecutePointLineDistanceMeasure() private void ExecutePointLineDistanceMeasure()
{ {
if (!CheckImageLoaded()) return;
_logger.Info("点线距测量功能已触发"); _logger.Info("点线距测量功能已触发");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointLineDistance); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointLineDistance);
} }
private void ExecuteAngleMeasure() private void ExecuteAngleMeasure()
{ {
if (!CheckImageLoaded()) return;
_logger.Info("角度测量功能已触发"); _logger.Info("角度测量功能已触发");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.Angle); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.Angle);
} }
private void ExecuteThroughHoleFillRateMeasure() private void ExecuteThroughHoleFillRateMeasure()
{ {
if (!CheckImageLoaded()) return;
_logger.Info("通孔填锡率测量功能已触发"); _logger.Info("通孔填锡率测量功能已触发");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.ThroughHoleFillRate); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.ThroughHoleFillRate);
} }
private Window _bgaMeasurePanel;
private void ExecuteBgaVoidMeasure()
{
if (!CheckImageLoaded()) return;
_logger.Info("BGA空隙测量功能已触发");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BgaVoid);
if (_bgaMeasurePanel != null && _bgaMeasurePanel.IsVisible)
{
_bgaMeasurePanel.Activate();
return;
}
_bgaMeasurePanel = new Views.ImageProcessing.BgaMeasurePanel
{
Owner = System.Windows.Application.Current.MainWindow
};
_bgaMeasurePanel.Closed += (s, e) =>
{
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
};
_bgaMeasurePanel.Show();
}
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();
}
private void ExecuteOpenLanguageSwitcher() private void ExecuteOpenLanguageSwitcher()
{ {
try try
@@ -528,5 +629,7 @@ namespace XplorePlane.ViewModels
RaisePropertyChanged(nameof(IsUsingLiveDetectorSource)); RaisePropertyChanged(nameof(IsUsingLiveDetectorSource));
})); }));
} }
#endregion
} }
} }
@@ -0,0 +1,74 @@
<Window
x:Class="XplorePlane.Views.ImageProcessing.BgaMeasurePanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="BGA空隙测量"
Width="240" Height="260"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
Topmost="True"
ShowInTaskbar="False">
<Window.Resources>
<Style x:Key="ToolToggleStyle" TargetType="RadioButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="Bd" Background="#EEEEEE" BorderBrush="#CCCCCC"
BorderThickness="1" CornerRadius="3" Padding="14,5" Cursor="Hand">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#0078D7" />
<Setter TargetName="Bd" Property="BorderBrush" Value="#005A9E" />
<Setter Property="Foreground" Value="White" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#DDDDDD" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsChecked" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="Bd" Property="Background" Value="#006CBE" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel Margin="10">
<!-- 绘制模式 -->
<TextBlock Text="绘制模式" FontWeight="SemiBold" Margin="0,0,0,4" />
<WrapPanel>
<RadioButton x:Name="RbVoid" Content="画气泡" IsChecked="True" Margin="0,0,6,4"
Style="{StaticResource ToolToggleStyle}" />
<RadioButton x:Name="RbBall" Content="画焊球" Margin="0,0,0,4"
Style="{StaticResource ToolToggleStyle}" />
</WrapPanel>
<TextBlock Text="左键点两次画圆(圆心+半径),右键删除" FontSize="10" Foreground="Gray" Margin="0,2,0,0" />
<Separator Margin="0,8" />
<!-- VoidLimit -->
<TextBlock Text="VoidLimit(%)" Margin="0,0,0,4" />
<DockPanel>
<TextBox x:Name="TbVoidLimit" DockPanel.Dock="Right" Width="50" Text="25.0"
VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider x:Name="SliderVoidLimit" Minimum="0" Maximum="100" Value="25"
VerticalAlignment="Center" />
</DockPanel>
<Separator Margin="0,8" />
<!-- 结果 -->
<TextBlock x:Name="TbResult" Text="空隙率: --" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,8" />
<!-- 操作 -->
<WrapPanel HorizontalAlignment="Center">
<Button Content="完成" Padding="14,4" Click="Finish_Click" />
</WrapPanel>
</StackPanel>
</Window>
@@ -0,0 +1,74 @@
using System.Windows;
using XP.ImageProcessing.RoiControl.Controls;
namespace XplorePlane.Views.ImageProcessing
{
public partial class BgaMeasurePanel : Window
{
private PolygonRoiCanvas _canvas;
public BgaMeasurePanel()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
try
{
var mainWin = Owner as MainWindow;
if (mainWin != null)
_canvas = FindChild<PolygonRoiCanvas>(mainWin);
}
catch { }
// 模式切换:通知 canvas 切换气泡/焊球
RbVoid.Checked += (s, ev) =>
{
if (_canvas != null) _canvas.SetBgaDrawBall(false);
};
RbBall.Checked += (s, ev) =>
{
if (_canvas != null) _canvas.SetBgaDrawBall(true);
};
// VoidLimit 同步
SliderVoidLimit.ValueChanged += (s, ev) =>
{
TbVoidLimit.Text = SliderVoidLimit.Value.ToString("F1");
_canvas?.SetBgaVoidLimit(SliderVoidLimit.Value);
};
// 监听测量完成事件更新结果
if (_canvas != null)
{
_canvas.MeasureCompleted += (s, ev) =>
{
if (ev is MeasureCompletedEventArgs args && args.MeasureType == "BgaVoid")
{
TbResult.Text = $"空隙率: {args.Distance:F1}%";
}
};
}
}
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;
}
}
}
@@ -0,0 +1,94 @@
<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="340"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
Topmost="True"
ShowInTaskbar="False">
<Window.Resources>
<Style x:Key="ToolToggleStyle" TargetType="RadioButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="Bd" Background="#EEEEEE" BorderBrush="#CCCCCC"
BorderThickness="1" CornerRadius="3" Padding="10,4" Cursor="Hand">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#0078D7" />
<Setter TargetName="Bd" Property="BorderBrush" Value="#005A9E" />
<Setter Property="Foreground" Value="White" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#DDDDDD" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsChecked" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="Bd" Property="Background" Value="#006CBE" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel Margin="10">
<!-- 工具选择 -->
<TextBlock Text="工具" FontWeight="SemiBold" Margin="0,0,0,4" />
<WrapPanel>
<RadioButton x:Name="RbRoi" Content="ROI" IsChecked="True" Margin="0,0,4,4"
Style="{StaticResource ToolToggleStyle}" />
<RadioButton x:Name="RbWand" Content="魔棒" Margin="0,0,4,4"
Style="{StaticResource ToolToggleStyle}" />
<RadioButton x:Name="RbBrush" Content="画笔" Margin="0,0,4,4"
Style="{StaticResource ToolToggleStyle}" />
<RadioButton x:Name="RbEraser" Content="橡皮擦" Margin="0,0,0,4"
Style="{StaticResource ToolToggleStyle}" />
</WrapPanel>
<Separator Margin="0,6" />
<!-- 阈值 -->
<TextBlock Text="灰度阈值(与点击点的最大灰度差)" Margin="0,0,0,4" FontSize="11" Foreground="Gray" />
<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="Undo_Click" />
<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,98 @@
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
};
_canvas.MeasureCompleted += (s, ev) =>
{
if (ev is MeasureCompletedEventArgs args && args.MeasureType == "BubbleVoid")
{
TbResult.Text = $"空隙率: {args.Distance:F1}%";
}
};
}
}
private void ClearMask_Click(object sender, RoutedEventArgs e)
{
_canvas?.ClearBubbleMeasure();
TbResult.Text = "空隙率: --";
}
private void Undo_Click(object sender, RoutedEventArgs e)
{
_canvas?.UndoBubble();
}
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;
}
}
}
+19 -1
View File
@@ -235,6 +235,24 @@
SmallImage="/Assets/Icons/pores.png" SmallImage="/Assets/Icons/pores.png"
Text="通孔填锡率" /> Text="通孔填锡率" />
</StackPanel> </StackPanel>
<!-- 第三列: BGA空隙测量 + 气泡测量 -->
<StackPanel>
<telerik:RadRibbonButton
telerik:ScreenTip.Description="BGA焊球空隙率检测"
telerik:ScreenTip.Title="BGA空隙测量"
Command="{Binding BgaVoidMeasureCommand}"
Size="Medium"
SmallImage="/Assets/Icons/bga.png"
Text="BGA空隙" />
<telerik:RadRibbonButton
telerik:ScreenTip.Description="手动气泡测量(魔棒+画笔)"
telerik:ScreenTip.Title="气泡测量"
Command="{Binding BubbleMeasureCommand}"
Size="Medium"
SmallImage="/Assets/Icons/pores.png"
Text="气泡测量" />
</StackPanel>
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="图像算子" telerik:ScreenTip.Title="图像算子"> <telerik:RadRibbonGroup Header="图像算子" telerik:ScreenTip.Title="图像算子">
@@ -554,7 +572,7 @@
FontFamily="Microsoft YaHei UI" FontFamily="Microsoft YaHei UI"
FontSize="11" FontSize="11"
Foreground="White" Foreground="White"
Text="就绪" /> Text="{Binding StatusMessage}" />
<TextBlock <TextBlock
Grid.Column="1" Grid.Column="1"
@@ -15,25 +15,51 @@ namespace XplorePlane.Views
{ {
public partial class ViewportPanelView : UserControl public partial class ViewportPanelView : UserControl
{ {
private MainViewModel _mainVm;
private void SetStatus(string msg)
{
if (_mainVm == null)
{
try { _mainVm = ContainerLocator.Current?.Resolve<MainViewModel>(); } catch { }
}
if (_mainVm != null) _mainVm.StatusMessage = msg;
}
public ViewportPanelView() public ViewportPanelView()
{ {
InitializeComponent(); InitializeComponent();
DataContextChanged += OnDataContextChanged; DataContextChanged += OnDataContextChanged;
// 测量事件 → 更新状态栏 // 测量事件 → 更新主界面状态栏
RoiCanvas.MeasureCompleted += (s, e) => RoiCanvas.MeasureCompleted += (s, e) =>
{ {
if (e is MeasureCompletedEventArgs args && DataContext is ViewportPanelViewModel vm) if (e is MeasureCompletedEventArgs args)
{ {
vm.MeasurementResult = $"{args.Distance:F2} px"; string typeLabel = args.MeasureType switch
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} 条测量"; "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) => RoiCanvas.MeasureStatusChanged += (s, e) =>
{ {
if (e is MeasureStatusEventArgs args && DataContext is ViewportPanelViewModel vm) if (e is MeasureStatusEventArgs args)
vm.ImageInfo = args.Message; SetStatus(args.Message);
}; };
// 十字辅助线:直接订阅 Prism 事件 // 十字辅助线:直接订阅 Prism 事件
@@ -52,6 +78,10 @@ namespace XplorePlane.Views
{ {
MeasurementToolMode.PointDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointDistance, MeasurementToolMode.PointDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointDistance,
MeasurementToolMode.PointLineDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointToLine, 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 _ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None
}; };
}, Prism.Events.ThreadOption.UIThread); }, Prism.Events.ThreadOption.UIThread);
@@ -82,10 +112,8 @@ namespace XplorePlane.Views
{ {
RoiCanvas.ClearMeasurements(); RoiCanvas.ClearMeasurements();
if (DataContext is ViewportPanelViewModel vm) if (DataContext is ViewportPanelViewModel vm)
{
vm.ResetMeasurementState(); vm.ResetMeasurementState();
vm.ImageInfo = "已清除所有测量"; SetStatus("已清除所有测量");
}
} }
private void SaveOriginalImage_Click(object sender, RoutedEventArgs e) private void SaveOriginalImage_Click(object sender, RoutedEventArgs e)