10.20PLC+相机2.3视觉修改

This commit is contained in:
17860779768
2025-08-25 16:33:58 +08:00
committed by Maikouce China
commit dca4b2afac
52 changed files with 11698 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
using Check.Main.Camera;
using Check.Main.Dispatch;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
namespace Check.Main.Common
{
/// <summary>
/// 静态全局配置管理器,作为配置数据的“单一数据源”。
/// 负责加载、保存和通知配置变更。
/// </summary>
public static class ConfigurationManager
{
// private static readonly string _configFilePath = Path.Combine(Application.StartupPath, "main_config.xml");
// private static ProcessConfig _currentConfig;
// private static readonly object _lock = new object();
// /// <summary>
// /// 当配置通过 SaveChanges() 方法保存后触发。
// /// 其他模块(如 FormControlPanel可以订阅此事件以响应配置变更。
// /// </summary>
// public static event Action OnConfigurationChanged;
// /// <summary>
// /// 静态构造函数在类首次被访问时自动执行,确保配置只被加载一次。
// /// </summary>
// static ConfigurationManager()
// {
// Load();
// }
// /// <summary>
// /// 获取对当前配置对象的直接引用。
// /// PropertyGrid 可以直接绑定到这个对象进行编辑。
// /// </summary>
// /// <returns>当前的 ProcessConfig 实例。</returns>
// public static ProcessConfig GetCurrentConfig()
// {
// return _currentConfig;
// }
// /// <summary>
// /// 从 XML 文件加载配置。如果文件不存在或加载失败,则创建一个新的默认配置。
// /// </summary>
// private static void Load()
// {
// lock (_lock)
// {
// if (File.Exists(_configFilePath))
// {
// try
// {
// XmlSerializer serializer = new XmlSerializer(typeof(ProcessConfig));
// using (var fs = new FileStream(_configFilePath, FileMode.Open, FileAccess.Read))
// {
// _currentConfig = (ProcessConfig)serializer.Deserialize(fs);
// ThreadSafeLogger.Log("主配置文件加载成功。");
// }
// }
// catch (Exception ex)
// {
// ThreadSafeLogger.Log("加载主配置失败: " + ex.Message + " 将使用默认配置。");
// _currentConfig = new ProcessConfig();
// }
// }
// else
// {
// ThreadSafeLogger.Log("未找到主配置文件,将创建新的默认配置。");
// _currentConfig = new ProcessConfig();
// }
// }
// }
// /// <summary>
// /// 将当前配置保存到文件,并通知所有监听者配置已更改。
// /// 这是响应UI变化的推荐方法。
// /// </summary>
// public static void SaveChanges()
// {
// lock (_lock)
// {
// try
// {
// XmlSerializer serializer = new XmlSerializer(typeof(ProcessConfig));
// using (var fs = new FileStream(_configFilePath, FileMode.Create, FileAccess.Write))
// {
// serializer.Serialize(fs, _currentConfig);
// }
// }
// catch (Exception ex)
// {
// ThreadSafeLogger.Log("保存主配置失败: " + ex.Message);
// }
// }
// // 在锁之外触发事件,以避免监听者中的代码导致死锁
// ThreadSafeLogger.Log("配置已保存,正在触发 OnConfigurationChanged 事件...");
// OnConfigurationChanged?.Invoke();
// }
// 不再自己管理配置,而是直接从 ProductManager 获取
public static ProcessConfig GetCurrentConfig()
{
return ProductManager.CurrentConfig;
}
public static void SaveChanges()
{
ProductManager.SaveCurrentProductConfig();
}
// OnConfigurationChanged 事件现在由 ProductManager.OnProductChanged 替代
// 如果其他地方还依赖这个事件,可以做一个桥接:
public static event Action OnConfigurationChanged
{
add { ProductManager.OnProductChanged += value; }
remove { ProductManager.OnProductChanged -= value; }
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Check.Main.Common
{
public class ImageData : IDisposable
{
public long ProductId { get; }
public int CameraIndex { get; }
public Bitmap Image { get; }
public ImageData(long productId, int cameraIndex, Bitmap image)
{
ProductId = productId;
CameraIndex = cameraIndex;
Image = image;
}
public void Dispose() => Image?.Dispose();
}
// ProductAssembly.cs - 用于组装一个完整的产品
public class ProductAssembly : IDisposable
{
public long ProductId { get; }
private readonly int _expectedCameraCount;
// 使用 ConcurrentDictionary 保证线程安全
private readonly ConcurrentDictionary<int, string> _results = new ConcurrentDictionary<int, string>();
public ProductAssembly(long productId, int expectedCameraCount)
{
ProductId = productId;
_expectedCameraCount = expectedCameraCount;
}
// 添加一个相机的检测结果
public bool AddResult(int cameraIndex, string result)
{
_results.TryAdd(cameraIndex, result);
return _results.Count == _expectedCameraCount;
}
// 获取最终的聚合结果
public string GetFinalResult()
{
// 如果任何一个结果包含 "NG",则最终结果为 "NG"
return _results.Values.Any(r => r.Contains("NG")) ? "NG" : "OK";
}
public void Dispose()
{
// 目前只持有结果字符串,无需释放资源
}
}
}

View File

@@ -0,0 +1,290 @@
//using HalconDotNet;
//using System;
//using System.Collections.Generic;
//using System.Drawing.Imaging;
//using System.IO;
//namespace HalconTemplateMatch
//{
// public class LogoMatcher
// {
// private List<HTuple> modelHandles = new List<HTuple>();
// /// <summary>
// /// 从文件加载多个模板
// /// </summary>
// public void LoadTemplates(string dir)
// {
// foreach (var file in Directory.GetFiles(dir, "*.shm"))
// {
// HTuple modelID;
// HOperatorSet.ReadShapeModel(file, out modelID);
// modelHandles.Add(modelID);
// Console.WriteLine($"加载模板: {file}");
// }
// }
// /// <summary>
// /// 在测试图像中查找 Logo
// /// </summary>
// /// <returns>true = OKfalse = NG</returns>
// public bool FindLogo(string testImagePath)
// {
// HObject ho_TestImage;
// HOperatorSet.ReadImage(out ho_TestImage, testImagePath);
// HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage);
// foreach (var modelID in modelHandles)
// {
// HOperatorSet.FindShapeModel(
// ho_TestImage,
// modelID,
// new HTuple(0).TupleRad(),
// new HTuple(360).TupleRad(),
// 0.5, // 最低分数
// 1, // 最大匹配数
// 0.5, // 重叠度
// "least_squares",
// 0,
// 0.9,
// out HTuple row,
// out HTuple col,
// out HTuple angle,
// out HTuple score);
// if (score.Length > 0 && score[0].D > 0.5)
// {
// Console.WriteLine($"找到 Logo: Row={row[0]}, Col={col[0]}, Score={score[0]}");
// return true; // 找到即返回成功
// }
// }
// return false; // 没找到
// }
// /// <summary>
// /// 重载FindLogo函数double返回
// /// </summary>
// /// <param name="bmp"></param>
// /// <returns></returns>
// public double FindLogo(Bitmap bmp)
// {
// // Bitmap 转 HObject
// HObject ho_TestImage;
// Bitmap2HObject(bmp, out ho_TestImage);
// HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage);
// double bestScore = -1;
// foreach (var modelID in modelHandles)
// {
// HOperatorSet.FindShapeModel(
// ho_TestImage,
// modelID,
// new HTuple(0).TupleRad(),
// new HTuple(360).TupleRad(),
// 0.5, // 最低分数
// 1, // 最大匹配数
// 0.5, // 重叠度
// "least_squares",
// 0,
// 0.9,
// out HTuple row,
// out HTuple col,
// out HTuple angle,
// out HTuple score);
// if (score.Length > 0 && score[0].D > bestScore)
// {
// bestScore = score[0].D;
// }
// }
// ho_TestImage.Dispose();
// return bestScore; // -1 = 没找到
// }
// /// <summary>
// /// Bitmap 转 Halcon HObject
// /// </summary>
// private void Bitmap2HObject(Bitmap bmp, out HObject hobj)
// {
// HOperatorSet.GenEmptyObj(out hobj);
// Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
// BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
// try
// {
// HOperatorSet.GenImageInterleaved(
// out hobj,
// bmpData.Scan0,
// "bgr", // Bitmap 默认是 BGR
// bmp.Width,
// bmp.Height,
// 0,
// "byte",
// bmp.Width,
// bmp.Height,
// 0,
// 0,
// -1,
// 0
// );
// }
// finally
// {
// bmp.UnlockBits(bmpData);
// }
// }
// }
//}
using HalconDotNet;
using NPOI.OpenXmlFormats.Vml;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
namespace HalconTemplateMatch
{
public class LogoMatcher
{
private readonly List<HTuple> modelHandles = new List<HTuple>();
/// <summary>
/// 从指定目录加载所有 .shm 模板文件
/// </summary>
public void LoadTemplates(string dir)
{
try
{
string fullPath = Path.GetFullPath(dir);
if (!Directory.Exists(fullPath))
{
Console.WriteLine($"[警告] 模型目录不存在: {fullPath}");
return;
}
string[] modelFiles = Directory.GetFiles(fullPath, "*.shm", SearchOption.TopDirectoryOnly);
if (modelFiles.Length == 0)
{
Console.WriteLine($"[警告] 模型目录中没有任何 .shm 文件: {fullPath}");
return;
}
foreach (var file in modelFiles)
{
try
{
HTuple modelID;
HOperatorSet.ReadShapeModel(file, out modelID);
modelHandles.Add(modelID);
Console.WriteLine($"[加载成功] 模板: {file}");
}
catch (HOperatorException ex)
{
Console.WriteLine($"[错误] 无法加载模板 {file}: {ex.Message}");
}
}
if (modelHandles.Count == 0)
{
Console.WriteLine($"[警告] 没有成功加载任何模板文件。");
}
}
catch (Exception ex)
{
Console.WriteLine($"[异常] 加载模板目录出错: {ex.Message}");
}
}
/// <summary>
/// 匹配并返回最高得分double返回
/// </summary>
public double FindLogo(Bitmap bmp)
{
if (modelHandles.Count == 0)
{
Console.WriteLine("[警告] 尚未加载任何模板。");
return -1;
}
// Bitmap 转 Halcon 对象
HObject ho_TestImage;
Bitmap2HObject(bmp, out ho_TestImage);
HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage);
double bestScore = -1;
foreach (var modelID in modelHandles)
{
try
{
HOperatorSet.FindScaledShapeModel(
ho_TestImage,
modelID,
new HTuple(0).TupleRad(),
new HTuple(360).TupleRad(),
0.8, 1.2,
0.5, 1, 0.5,
"least_squares_high",
0, 0.9,
out HTuple row, out HTuple col, out HTuple angle, out HTuple scale, out HTuple score
);
if (score.Length > 0 && score[0].D > bestScore)
bestScore = score[0].D;
}
catch (HOperatorException ex)
{
Console.WriteLine($"[错误] 模板匹配失败: {ex.Message}");
}
}
ho_TestImage.Dispose();
return bestScore;
}
/// <summary>
/// Bitmap 转 Halcon HObject
/// </summary>
private void Bitmap2HObject(Bitmap bmp, out HObject hobj)
{
HOperatorSet.GenEmptyObj(out hobj);
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
try
{
HOperatorSet.GenImageInterleaved(
out hobj,
bmpData.Scan0,
"bgr",
bmp.Width,
bmp.Height,
0,
"byte",
bmp.Width,
bmp.Height,
0,
0,
-1,
0);
}
finally
{
bmp.UnlockBits(bmpData);
}
}
}
}

View File

@@ -0,0 +1,62 @@
using HalconDotNet;
using System;
using System.Collections.Generic;
using System.IO;
namespace HalconTemplateMatch
{
public class LogoTemplateTrainer
{
/// <summary>
/// 从多张样本图像生成模板并保存到文件
/// </summary>
/// <param name="imagePaths">训练图片路径集合</param>
/// <param name="saveDir">模板保存目录</param>
public void TrainAndSaveTemplates(List<string> imagePaths, string saveDir)
{
if (!Directory.Exists(saveDir))
Directory.CreateDirectory(saveDir);
int index = 0;
foreach (var path in imagePaths)
{
HObject ho_Image;
HOperatorSet.ReadImage(out ho_Image, path);
// 转灰度
HOperatorSet.Rgb1ToGray(ho_Image, out HObject ho_Gray);
// 二值化
HOperatorSet.Threshold(ho_Gray, out HObject ho_Region, 128, 255);
// 提取连通域
HOperatorSet.Connection(ho_Region, out HObject ho_Connected);
HOperatorSet.SelectShapeStd(ho_Connected, out HObject ho_Selected, "max_area", 70);
// ROI 约束
HOperatorSet.ReduceDomain(ho_Gray, ho_Selected, out HObject ho_ROI);
// 创建形状模板
HTuple modelID;
HOperatorSet.CreateShapeModel(
ho_ROI,
"auto",
new HTuple(0).TupleRad(),
new HTuple(360).TupleRad(),
"auto",
"auto",
"use_polarity",
"auto",
"auto",
out modelID);
// 保存模板到文件
string modelFile = Path.Combine(saveDir, $"logo_model_{index}.shm");
HOperatorSet.WriteShapeModel(modelID, modelFile);
Console.WriteLine($"训练完成并保存模板: {modelFile}");
index++;
}
}
}
}

View File

@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.ComponentModel.TypeConverter;
namespace Check.Main.Common
{
/// <summary>
/// 一个自定义的 TypeConverter用于在 PropertyGrid 中为模型选择提供一个下拉列表。
/// </summary>
public class ModelSelectionConverter : Int32Converter // 我们转换的是整数 (ModelId)
{
// 告诉 PropertyGrid我们支持从一个标准值集合中进行选择
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
return true;
}
// 告诉 PropertyGrid我们只允许用户从列表中选择不允许手动输入
public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
{
return true;
}
// 这是最核心的方法:提供标准值的列表
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
try
{
// 从中央配置管理器获取所有已配置的模型
var modelSettings = ConfigurationManager.GetCurrentConfig().ModelSettings;
if (modelSettings == null || modelSettings.Count == 0)
{
// 如果没有模型,返回一个包含默认值 "0"(代表“无”)的列表
return new StandardValuesCollection(new[] { 0 });
}
// 使用LINQ从模型列表中提取所有模型的ID
// .Prepend(0) 在列表开头添加一个 "0",代表“未选择”的选项
var modelIds = modelSettings.Select(m => m.Id).Prepend(0).ToArray();
return new StandardValuesCollection(modelIds);
}
catch
{
// 如果在获取过程中发生任何异常,返回一个安全的默认值
return new StandardValuesCollection(new[] { 0 });
}
}
// --- 为了让下拉列表显示模型名称而不是ID我们还需要重写以下方法 ---
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
// 将模型ID转换为其对应的名称字符串以便在UI上显示
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string) && value is int modelId)
{
if (modelId == 0)
{
return "未选择"; // 对ID为0的特殊处理
}
// 查找ID对应的模型名称
var model = ConfigurationManager.GetCurrentConfig()
.ModelSettings?
.FirstOrDefault(m => m.Id == modelId);
// 返回 "名称 (ID)" 的格式,更清晰
return model != null ? $"{model.Name} (ID: {model.Id})" : "未知模型";
}
return base.ConvertTo(context, culture, value, destinationType);
}
// 将用户在UI上选择的名称字符串转换回其对应的模型ID以便保存
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string displayString)
{
if (displayString == "未选择")
{
return 0;
}
// 从 "名称 (ID: X)" 格式中解析出ID
if (displayString.Contains("(ID: ") && displayString.EndsWith(")"))
{
var startIndex = displayString.IndexOf("(ID: ") + 5;
var endIndex = displayString.Length - 1;
var idString = displayString.Substring(startIndex, endIndex - startIndex);
if (int.TryParse(idString, out int modelId))
{
return modelId;
}
}
}
return base.ConvertFrom(context, culture, value);
}
}
}

View File

@@ -0,0 +1,108 @@
using HslCommunication;
using HslCommunication.ModBus;
using System;
using System.Net.Sockets;
// ModbusTcp读写服务类线程安全互斥锁
namespace Check.Main.Common
{
public class ModbusTcpService
{
private readonly TcpClient _tcpClient = new();//
private readonly ModbusTcpNet _plc;
private readonly object _lock = new();
public bool IsConnected => _tcpClient != null && _tcpClient.Connected;//
public ModbusTcpService(string ip, int port = 502, byte station = 1)
{
_plc = new ModbusTcpNet(ip, port, station)
{
// CDAB 格式:PLC的不同品牌的modelbus TCP存在正反高低位。如果读写异常。删掉或者补充下面这句函数进行修复
DataFormat = HslCommunication.Core.DataFormat.CDAB
};
}
// 读取 32 位整数
public OperateResult<int> ReadInt32(string address)
{
lock (_lock)
{
return _plc.ReadInt32(address);
}
}
// 写入 32 位整数int
public OperateResult WriteInt32(string address, int value)
{
lock (_lock)
{
return _plc.Write(address, value);
}
}
// 读取 16 位整数short
public OperateResult<short> ReadInt16(string address)
{
lock (_lock)
{
return _plc.ReadInt16(address);
}
}
// 写入 16 位整数short
public OperateResult WriteInt16(string address, short value)
{
lock (_lock)
{
return _plc.Write(address, value);
}
}
// 读取布尔量
public OperateResult<bool> ReadBool(string address)
{
lock (_lock)
{
return _plc.ReadBool(address);
}
}
// 写入布尔量
public OperateResult WriteBool(string address, bool value)
{
lock (_lock)
{
return _plc.Write(address, value);
}
}
// 读取浮点数float
public OperateResult<float> ReadFloat(string address)
{
lock (_lock)
{
return _plc.ReadFloat(address);
}
}
// 写入浮点数float
public OperateResult WriteFloat(string address, float value)
{
lock (_lock)
{
return _plc.Write(address, value);
}
}
// 关闭连接
public void Close()
{
lock (_lock)
{
_plc.ConnectClose();
}
}
}
}

View File

@@ -0,0 +1,161 @@
using Check.Main.Camera;
using Check.Main.Infer;
using Check.Main.UI;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms.Design;
namespace Check.Main.Common
{
public class ProcessConfig
{
private string _logPath = Path.Combine(Application.StartupPath, "Logs");
private List<CameraSettings> _cameraSettings = new List<CameraSettings>();
private List<ModelSettings> _modelaSettings = new List<ModelSettings>();
[Category("常规设置"), DisplayName("日志路径"), Description("设置日志文件的保存目录。")]
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))] // 使用内置的文件夹选择器
public string LogPath
{
get => _logPath;
set => _logPath = value;
}
[Category("核心配置"), DisplayName("相机配置"), Description("点击 '...' 按钮来配置所有相机。")]
[Editor(typeof(CameraConfigEditor), typeof(UITypeEditor))] // 【关键】指定使用我们自定义的编辑器
[TypeConverter(typeof(CameraConfigConverter))] // 【关键】指定使用我们自定义的类型转换器来显示文本
public List<CameraSettings> CameraSettings
{
get => _cameraSettings;
set => _cameraSettings = value;
}
[Category("核心配置"), DisplayName("模型配置"), Description("点击 '...' 按钮来配置所有模型。")]
[Editor(typeof(ModelConfigEditor), typeof(UITypeEditor))] // 【关键】指定使用我们自定义的编辑器
[TypeConverter(typeof(ModelConfigConverter))] // 【关键】指定使用我们自定义的类型转换器来显示文本
public List<ModelSettings> ModelSettings
{
get => _modelaSettings;
set => _modelaSettings = value;
}
}
/// <summary>
/// 自定义TypeConverter用于在PropertyGrid中自定义显示相机列表的文本。
/// </summary>
public class CameraConfigConverter : TypeConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
// 声明可以将值转换为字符串
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
// 如果目标类型是字符串,并且值是相机设置列表
if (destinationType == typeof(string) && value is List<CameraSettings> list)
{
// 返回自定义的描述性文本
return $"已配置 {list.Count} 个相机";
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
/// <summary>
/// 自定义TypeConverter用于在PropertyGrid中自定义显示相机列表的文本。
/// </summary>
public class ModelConfigConverter : TypeConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
// 声明可以将值转换为字符串
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
// 如果目标类型是字符串,并且值是相机设置列表
if (destinationType == typeof(string) && value is List<ModelSettings> list)
{
// 返回自定义的描述性文本
return $"已配置 {list.Count} 个模型";
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
/// <summary>
/// 自定义UITypeEditor用于在点击 "..." 时打开相机配置窗体。
/// </summary>
public class CameraConfigEditor : UITypeEditor
{
// 1. 设置编辑样式为模态对话框
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
// 2. 重写编辑方法
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
// 'value' 就是当前属性的值,即 List<CameraSettings>
var settingsList = value as List<CameraSettings>;
// 使用 FrmCamConfig 作为对话框进行编辑
// 我们需要给 FrmCamConfig 添加一个新的构造函数
using (var camConfigForm = new FrmCamConfig(settingsList))
{
// 以对话框形式显示窗体
if (camConfigForm.ShowDialog() == DialogResult.OK)
{
// 如果用户点击了“确定”,返回修改后的新列表
return camConfigForm._settingsList;
}
}
// 如果用户点击了“取消”或直接关闭了窗口,返回原始值,不做任何更改
return value;
}
}
/// <summary>
/// 【新增】自定义UITypeEditor用于在点击 "..." 时打开模型配置窗体。
/// </summary>
public class ModelConfigEditor : UITypeEditor
{
// 1. 设置编辑样式为模态对话框
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
// 2. 重写编辑方法
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
// 'value' 就是当前属性的值,即 List<CameraSettings>
var settingsList = value as List<ModelSettings>;
// 使用ModelEditor 作为对话框进行编辑
// 我们需要给 FrmCamConfig 添加一个新的构造函数
using (var modelConfigForm = new ModelListEditor(settingsList))
{
// 以对话框形式显示窗体
if (modelConfigForm.ShowDialog() == DialogResult.OK)
{
// 如果用户点击了“确定”,返回修改后的新列表
return modelConfigForm._settingsList;
}
}
// 如果用户点击了“取消”或直接关闭了窗口,返回原始值,不做任何更改
return value;
}
}
}

View File

@@ -0,0 +1,29 @@
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Check.Main.Common
{
public class ProcessingCompletedEventArgs : EventArgs, IDisposable
{
public int CameraIndex { get; }
public long ProductId { get; }
public Bitmap ResultImage { get; } // 原来是SKImage ResultImage
public ProcessingCompletedEventArgs(int cameraIndex, long productId, Bitmap resultImage)//原来是SKImage resultImage
{
CameraIndex = cameraIndex;
ProductId = productId;
ResultImage = resultImage;
}
// 实现 IDisposable 以确保SKImage资源被释放
public void Dispose()
{
ResultImage?.Dispose();
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Check.Main.Common
{
/// <summary>
/// 一个自定义的类型转换器用于让PropertyGrid等控件显示枚举成员的Description特性而不是成员名。
/// </summary>
public class EnumDescriptionTypeConverter : EnumConverter
{
/// <summary>
/// 构造函数,传入枚举类型。
/// </summary>
/// <param name="type">要进行转换的枚举类型。</param>
public EnumDescriptionTypeConverter(Type type) : base(type)
{
}
/// <summary>
/// 重写此方法,将枚举值转换为其描述文本。
/// 这是从 "数据" -> "UI显示" 的过程。
/// </summary>
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
// 如果目标类型是字符串,并且我们有一个有效的枚举值
if (destinationType == typeof(string) && value != null)
{
// 获取枚举值的字段信息
FieldInfo fi = value.GetType().GetField(value.ToString());
if (fi != null)
{
// 查找该字段上的DescriptionAttribute
var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
// 如果找到了Description特性就返回它的描述文本否则返回基类的默认行为即成员名
return ((attributes.Length > 0) && (!String.IsNullOrEmpty(attributes[0].Description)))
? attributes[0].Description
: value.ToString();
}
}
// 对于其他所有情况,使用基类的默认转换逻辑
return base.ConvertTo(context, culture, value, destinationType);
}
/// <summary>
/// 重写此方法,将描述文本转换回其对应的枚举值。
/// 这是从 "UI显示" -> "数据" 的过程。
/// </summary>
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
// 如果传入的值是字符串
if (value is string)
{
// 遍历枚举类型的所有成员
foreach (FieldInfo fi in EnumType.GetFields())
{
// 查找该成员上的DescriptionAttribute
var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
// 如果找到了Description并且它的描述文本与传入的字符串匹配
if ((attributes.Length > 0) && (attributes[0].Description == (string)value))
{
// 返回这个字段(成员)对应的枚举值
return Enum.Parse(EnumType, fi.Name);
}
}
}
// 如果没有找到匹配的描述,或者传入的不是字符串,使用基类的默认转换逻辑
return base.ConvertFrom(context, culture, value);
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Check.Main.Common
{
/// <summary>
/// 封装了所有生产统计数据的模型。
/// 实现了INotifyPropertyChanged未来可用于数据绑定。
/// </summary>
public class StatisticsData : INotifyPropertyChanged
{
private int _goodCount;
private int _ngCount;
public int GoodCount
{
get => _goodCount;
private set { _goodCount = value; OnPropertyChanged(nameof(GoodCount)); OnPropertyChanged(nameof(TotalCount)); OnPropertyChanged(nameof(YieldRate)); }
}
public int NgCount
{
get => _ngCount;
private set { _ngCount = value; OnPropertyChanged(nameof(NgCount)); OnPropertyChanged(nameof(TotalCount)); OnPropertyChanged(nameof(YieldRate)); }
}
public int TotalCount => GoodCount + NgCount;
public double YieldRate => TotalCount == 0 ? 0 : (double)GoodCount / TotalCount;
/// <summary>
/// 根据检测结果更新统计数据。
/// </summary>
public void UpdateWithResult(bool isOk)
{
if (isOk) GoodCount++;
else NgCount++;
}
/// <summary>
/// 重置所有统计数据。
/// </summary>
public void Reset()
{
GoodCount = 0;
NgCount = 0;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// 用于在检测完成事件中传递结果的事件参数。
/// </summary>
public class DetectionResultEventArgs : EventArgs
{
public bool IsOK { get; }
public DetectionResultEventArgs(bool isOk)
{
IsOK = isOk;
}
}
}

View File

@@ -0,0 +1,82 @@
using Check.Main.Camera;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Check.Main.Common
{
public static class StatisticsExporter
{
/// <summary>
/// 将给定的统计数据导出到指定的Excel文件路径。
/// </summary>
/// <param name="data">要导出的统计数据对象。</param>
/// <param name="customFileName">自定义的文件名部分(如 "Reset" 或 "Shutdown")。</param>
public static void ExportToExcel(StatisticsData data, string customFileName)
{
if (data.TotalCount == 0) return; // 如果没有数据,则不导出
try
{
string directory = Path.Combine(Application.StartupPath, "Statistics");
Directory.CreateDirectory(directory); // 确保文件夹存在
string fileName = $"Statistics_{customFileName}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.xlsx";
string filePath = Path.Combine(directory, fileName);
// 创建工作簿和工作表
IWorkbook workbook = new XSSFWorkbook();
ISheet sheet = workbook.CreateSheet("生产统计");
// --- 创建样式 (可选,但能让表格更好看) ---
IFont boldFont = workbook.CreateFont();
boldFont.IsBold = true;
ICellStyle headerStyle = workbook.CreateCellStyle();
headerStyle.SetFont(boldFont);
// --- 创建表头 ---
IRow headerRow = sheet.CreateRow(0);
headerRow.CreateCell(0).SetCellValue("项目");
headerRow.CreateCell(1).SetCellValue("数值");
headerRow.GetCell(0).CellStyle = headerStyle;
headerRow.GetCell(1).CellStyle = headerStyle;
// --- 填充数据 ---
sheet.CreateRow(1).CreateCell(0).SetCellValue("良品数 (OK)");
sheet.GetRow(1).CreateCell(1).SetCellValue(data.GoodCount);
sheet.CreateRow(2).CreateCell(0).SetCellValue("不良品数 (NG)");
sheet.GetRow(2).CreateCell(1).SetCellValue(data.NgCount);
sheet.CreateRow(3).CreateCell(0).SetCellValue("产品总数");
sheet.GetRow(3).CreateCell(1).SetCellValue(data.TotalCount);
sheet.CreateRow(4).CreateCell(0).SetCellValue("良率 (Yield)");
sheet.GetRow(4).CreateCell(1).SetCellValue(data.YieldRate.ToString("P2"));
// 自动调整列宽
sheet.AutoSizeColumn(0);
sheet.AutoSizeColumn(1);
// --- 写入文件 ---
using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
workbook.Write(fs);
}
ThreadSafeLogger.Log($"统计数据已成功导出到: {filePath}");
}
catch (Exception ex)
{
ThreadSafeLogger.Log($"[ERROR] 导出统计数据失败: {ex.Message}");
MessageBox.Show($"导出统计数据失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Check.Main.Common
{
/// <summary>
/// 一个线程安全的、基于队列的日志记录器。
/// 使用一个独立的后台线程来处理文件写入,避免业务线程阻塞。
/// </summary>
public static class ThreadSafeLogger
{
// 使用线程安全的队列作为日志消息的缓冲区
private static readonly BlockingCollection<string> _logQueue = new BlockingCollection<string>();
// 日志写入线程
private static Thread _logWriterThread;
private static StreamWriter _logFileWriter;
// 事件用于将格式化后的日志消息广播给UI等监听者
public static event Action<string> OnLogMessage;
/// <summary>
/// 初始化日志记录器,启动后台写入线程。
/// </summary>
public static void Initialize()
{
try
{
string logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
Directory.CreateDirectory(logDirectory);
string logFileName = $"Log_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt";
string logFilePath = Path.Combine(logDirectory, logFileName);
_logFileWriter = new StreamWriter(logFilePath, append: true, encoding: Encoding.UTF8) { AutoFlush = true };
// 创建并启动后台线程
_logWriterThread = new Thread(ProcessLogQueue)
{
IsBackground = true, // 设置为后台线程,这样主程序退出时它会自动终止
Name = "LogWriterThread"
};
_logWriterThread.Start();
Log("日志系统已初始化。");
}
catch (Exception ex)
{
// 如果初始化失败尝试通过事件通知UI
OnLogMessage?.Invoke($"[CRITICAL] 日志系统初始化失败: {ex.Message}");
}
}
/// <summary>
/// 将一条日志消息添加到队列中。这个方法是线程安全的,且执行速度非常快。
/// </summary>
/// <param name="message">原始日志消息。</param>
public static void Log(string message)
{
string formattedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
_logQueue.Add(formattedMessage);
}
/// <summary>
/// 后台线程的工作方法。它会持续不断地从队列中取出消息并处理。
/// </summary>
private static void ProcessLogQueue()
{
// GetConsumingEnumerable会阻塞等待直到有新的项加入队列或队列被标记为已完成
foreach (string message in _logQueue.GetConsumingEnumerable())
{
try
{
// 1. 写入文件
_logFileWriter?.WriteLine(message);
// 2. 触发事件通知UI
OnLogMessage?.Invoke(message);
}
catch
{
// 忽略在日志线程本身发生的写入错误
}
}
}
/// <summary>
/// 关闭日志记录器,释放资源。
/// </summary>
public static void Shutdown()
{
Log("日志系统正在关闭...");
// 标记队列不再接受新的项目。这会让ProcessLogQueue中的循环在处理完所有剩余项后自然结束。
_logQueue.CompleteAdding();
// 等待日志线程处理完所有剩余的日志最多等待2秒
_logWriterThread?.Join(2000);
// 关闭文件流
_logFileWriter?.Close();
_logFileWriter?.Dispose();
}
}
}