10.20PLC+相机2.3视觉修改
This commit is contained in:
124
Check.Main/Common/ConfigurationManager.cs
Normal file
124
Check.Main/Common/ConfigurationManager.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
58
Check.Main/Common/ImageData.cs
Normal file
58
Check.Main/Common/ImageData.cs
Normal 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()
|
||||
{
|
||||
// 目前只持有结果字符串,无需释放资源
|
||||
}
|
||||
}
|
||||
}
|
||||
290
Check.Main/Common/LogoMatcher.cs
Normal file
290
Check.Main/Common/LogoMatcher.cs
Normal 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 = OK,false = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
Check.Main/Common/LogoTemplateTrainer.cs
Normal file
62
Check.Main/Common/LogoTemplateTrainer.cs
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
Check.Main/Common/ModelSelectionConverter.cs
Normal file
114
Check.Main/Common/ModelSelectionConverter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
108
Check.Main/Common/PLC_Control_Base.cs
Normal file
108
Check.Main/Common/PLC_Control_Base.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
161
Check.Main/Common/ProcessConfig.cs
Normal file
161
Check.Main/Common/ProcessConfig.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Check.Main/Common/ProcessingCompletedEventArgs.cs
Normal file
29
Check.Main/Common/ProcessingCompletedEventArgs.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
80
Check.Main/Common/StaticMethod.cs
Normal file
80
Check.Main/Common/StaticMethod.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
Check.Main/Common/StatisticsData.cs
Normal file
71
Check.Main/Common/StatisticsData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
82
Check.Main/Common/StatisticsExporter.cs
Normal file
82
Check.Main/Common/StatisticsExporter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
Check.Main/Common/ThreadSafeLogger.cs
Normal file
112
Check.Main/Common/ThreadSafeLogger.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user