视觉修改
This commit is contained in:
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
||||
363
.gitignore
vendored
Normal file
363
.gitignore
vendored
Normal file
@@ -0,0 +1,363 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
838
Check.Main/Camera/CameraManager.cs
Normal file
838
Check.Main/Camera/CameraManager.cs
Normal file
@@ -0,0 +1,838 @@
|
||||
using Check.Main.Common;
|
||||
using Check.Main.Infer;
|
||||
using Check.Main.Result;
|
||||
using Check.Main.UI;
|
||||
using OpenCvSharp;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Check.Main.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 静态全局相机管理器,负责所有相机的生命周期、配置应用和多相机同步
|
||||
/// </summary>
|
||||
public static class CameraManager
|
||||
{
|
||||
// 活动的相机实例字典,键为相机名称
|
||||
public static Dictionary<string, HikvisionCamera> ActiveCameras { get; } = new Dictionary<string, HikvisionCamera>();
|
||||
|
||||
// 相机对应的图像显示窗口字典
|
||||
//public static Dictionary<string, FormImageDisplay> CameraDisplays { get; } = new Dictionary<string, FormImageDisplay>();
|
||||
|
||||
public static Dictionary<string, FormImageDisplay> OriginalImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();
|
||||
public static Dictionary<string, FormImageDisplay> ResultImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();
|
||||
|
||||
// --- 多相机同步逻辑 ---
|
||||
// 产品检测队列
|
||||
private static readonly Queue<ProductResult> ProductQueue = new Queue<ProductResult>();
|
||||
// 队列锁,保证线程安全
|
||||
private static readonly object QueueLock = new object();
|
||||
// 当前启用的相机数量,用于判断产品是否检测完毕
|
||||
private static int EnabledCameraCount = 0;
|
||||
|
||||
//public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted;
|
||||
// public static bool IsDetectionRunning { get; private set; } = false;
|
||||
|
||||
// 产品ID计数器
|
||||
private static long _productCounter = 0;
|
||||
// 用于同步计数器访问的锁,虽然long的自增是原子操作,但为清晰和未来扩展,使用锁是好习惯
|
||||
private static readonly object _counterLock = new object();
|
||||
|
||||
// --- 新增:硬触发模拟器 ---
|
||||
private static readonly System.Timers.Timer _hardwareTriggerSimulator;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置模拟硬触发的间隔时间(毫秒)。
|
||||
/// </summary>
|
||||
public static double TriggerInterval { get; set; } = 1000; // 默认1秒触发一次
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个值,该值指示硬件触发模拟器当前是否正在运行。
|
||||
/// </summary>
|
||||
public static bool IsHardwareTriggerSimulating { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 静态构造函数,用于一次性初始化静态资源。
|
||||
/// </summary>
|
||||
static CameraManager()
|
||||
{
|
||||
//初始化硬触发模拟器
|
||||
_hardwareTriggerSimulator = new System.Timers.Timer();
|
||||
_hardwareTriggerSimulator.Elapsed += OnHardwareTriggerTimerElapsed;
|
||||
_hardwareTriggerSimulator.AutoReset = true; // 确保定时器持续触发
|
||||
_hardwareTriggerSimulator.Enabled = false; // 默认不启动
|
||||
}
|
||||
|
||||
|
||||
//// 事件:用于向UI发送日志消息
|
||||
//public static event Action<string> OnLogMessage;
|
||||
//// 用于写入日志文件的 StreamWriter
|
||||
//private static StreamWriter _logFileWriter;
|
||||
|
||||
////私有的、静态的、只读的对象,专门用作线程同步的“锁”。
|
||||
//private static readonly object _logLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 初始化文件日志记录器。应在程序启动时调用一次。
|
||||
/// </summary>
|
||||
//public static void InitializeLogger()
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// string logDirectory = Path.Combine(Application.StartupPath, "Logs");
|
||||
// Directory.CreateDirectory(logDirectory); // 确保Logs文件夹存在
|
||||
|
||||
// string logFileName = $"Log_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt";
|
||||
// string logFilePath = Path.Combine(logDirectory, logFileName);
|
||||
|
||||
// // 创建StreamWriter,设置为追加模式和自动刷新
|
||||
// _logFileWriter = new StreamWriter(logFilePath, append: true, encoding: System.Text.Encoding.UTF8)
|
||||
// {
|
||||
// AutoFlush = true
|
||||
// };
|
||||
|
||||
// Log("文件日志记录器已初始化。");
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// // 如果文件日志初始化失败,在UI上报告错误
|
||||
// OnLogMessage?.Invoke($"[CRITICAL] 文件日志初始化失败: {ex.Message}");
|
||||
// }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// 【新增】一个完整的业务流程方法:
|
||||
/// 1. 根据配置初始化所有相机并显示它们的窗口。
|
||||
/// 2. 在所有窗口都显示后,命令所有相机开始采集。
|
||||
/// 这个方法是响应“开启设备”或“应用配置并启动”按钮的理想入口点。
|
||||
/// </summary>
|
||||
/// <param name="settingsList">要应用的相机配置列表。</param>
|
||||
/// <param name="mainForm">主窗体,用于停靠显示窗口。</param>
|
||||
//public static void ApplyConfigurationAndStartGrabbing(List<CameraSettings> settingsList, FrmMain mainForm)
|
||||
//{
|
||||
// ThreadSafeLogger.Log("开始执行“开启设备”流程...");
|
||||
|
||||
// // 步骤 1: 初始化所有相机和UI窗口
|
||||
// // Initialize 方法会负责 Shutdown 旧实例、创建新实例、打开硬件、显示窗口等。
|
||||
// // 因为 Initialize 方法中的 displayForm.Show() 是非阻塞的,它会立即返回,
|
||||
// // 窗体的创建和显示过程会被调度到UI线程的消息队列中。
|
||||
// Initialize(settingsList, mainForm);
|
||||
|
||||
// // 检查是否有任何相机成功初始化
|
||||
// if (ActiveCameras.Count == 0)
|
||||
// {
|
||||
// ThreadSafeLogger.Log("“开启设备”流程中止,因为没有相机被成功初始化。");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // 步骤 2: 在 Initialize 完成后(意味着所有窗口都已创建并 Show),开始采集
|
||||
// // 这个调用会紧接着 Initialize 执行,此时窗体可能还在绘制过程中,但这没关系。
|
||||
// // StartAll() 会启动后台线程开始拉取图像,一旦图像到达,就会通过事件推送到已经存在的窗体上。
|
||||
// ThreadSafeLogger.Log("所有相机窗口已创建,现在开始采集图像...");
|
||||
// StartAll();
|
||||
|
||||
// ThreadSafeLogger.Log("“开启设备”流程已完成。相机正在采集中。");
|
||||
//}
|
||||
|
||||
///// <summary>
|
||||
///// 根据配置列表初始化或更新所有相机
|
||||
///// </summary>
|
||||
//public static void Initialize(List<CameraSettings> settingsList, FrmMain mainForm)
|
||||
//{
|
||||
|
||||
// // 先停止并释放所有旧的相机
|
||||
// Shutdown();
|
||||
// ThreadSafeLogger.Log("开始应用新的相机配置...");
|
||||
|
||||
// EnabledCameraCount = settingsList.Count(s => s.IsEnabled);
|
||||
// if (EnabledCameraCount == 0)
|
||||
// {
|
||||
// ThreadSafeLogger.Log("没有启用的相机。");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// var deviceList = new HikvisionCamera().FindDevices();
|
||||
// if (deviceList.Count == 0)
|
||||
// {
|
||||
// ThreadSafeLogger.Log("错误:未找到任何相机设备!");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// int deviceIndex = 0;
|
||||
// foreach (var setting in settingsList)
|
||||
// {
|
||||
// if (!setting.IsEnabled) continue;
|
||||
|
||||
// if (deviceIndex >= deviceList.Count)
|
||||
// {
|
||||
// ThreadSafeLogger.Log($"警告:相机配置'{setting.Name}'无法分配物理设备,因为设备数量不足。");
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // --- 创建相机实例 ---
|
||||
// var cam = new HikvisionCamera { Name = setting.Name };
|
||||
// cam.TriggerMode = setting.TriggerMode;
|
||||
// if (!cam.Open(setting))
|
||||
// {
|
||||
// ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。");
|
||||
// cam.Dispose();
|
||||
// continue;
|
||||
// }
|
||||
// // --- 设置触发模式 ---
|
||||
// switch (setting.TriggerMode)
|
||||
// {
|
||||
// case TriggerModeType.Continuous:
|
||||
// cam.SetContinuousMode();
|
||||
// break;
|
||||
// case TriggerModeType.Software:
|
||||
// cam.SetTriggerMode(true);
|
||||
// break;
|
||||
// case TriggerModeType.Hardware:
|
||||
// cam.SetTriggerMode(false);
|
||||
// break;
|
||||
// }
|
||||
|
||||
// // --- 订阅事件 ---
|
||||
// cam.ImageAcquired += OnCameraImageAcquired;
|
||||
// cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}");
|
||||
|
||||
// // --- 创建显示窗口 ---
|
||||
// var displayForm = new FormImageDisplay { Text = setting.Name, CameraName = setting.Name };
|
||||
// displayForm.OnDisplayEvent += ThreadSafeLogger.Log;
|
||||
// displayForm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
|
||||
|
||||
|
||||
// ActiveCameras.Add(setting.Name, cam);
|
||||
// CameraDisplays.Add(setting.Name, displayForm);
|
||||
// mainForm.AddCameraToStatusStrip(setting.Name);
|
||||
// ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。");
|
||||
// deviceIndex++;
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。
|
||||
/// 这是“启动设备”的第一阶段。
|
||||
/// </summary>
|
||||
public static void PrepareAll(ProcessConfig config, FrmMain mainForm)
|
||||
{
|
||||
// 1. 清理旧资源和UI
|
||||
mainForm.ClearStatusStrip();
|
||||
Shutdown();
|
||||
ThreadSafeLogger.Log("开始准备设备和模型...");
|
||||
|
||||
// 2. 初始化检测协调器和AI模型
|
||||
// 注意:YoloModelManager 的 Initialize 现在也应在这里被调用,以确保逻辑集中
|
||||
YoloModelManager.Initialize(config.ModelSettings);
|
||||
DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings);
|
||||
|
||||
// 3. 创建相机硬件实例和UI窗口
|
||||
var deviceList = new HikvisionCamera().FindDevices();
|
||||
if (deviceList.Count == 0)
|
||||
{
|
||||
ThreadSafeLogger.Log("错误:未找到任何相机设备!");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled))
|
||||
{
|
||||
var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex };
|
||||
cam.TriggerMode = setting.TriggerMode;
|
||||
if (!cam.Open(setting))
|
||||
{
|
||||
ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。");
|
||||
cam.Dispose();
|
||||
continue;
|
||||
}
|
||||
// --- 设置触发模式 ---
|
||||
switch (setting.TriggerMode)
|
||||
{
|
||||
case TriggerModeType.Continuous:
|
||||
cam.SetContinuousMode();
|
||||
break;
|
||||
case TriggerModeType.Software:
|
||||
cam.SetTriggerMode(true);
|
||||
break;
|
||||
case TriggerModeType.Hardware:
|
||||
cam.SetTriggerMode(false);
|
||||
break;
|
||||
}
|
||||
|
||||
// --- 订阅事件 ---
|
||||
cam.ImageAcquired += OnCameraImageAcquired;
|
||||
var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex);
|
||||
if (processor != null) { processor.OnProcessingCompleted += Processor_OnProcessingCompleted; }
|
||||
|
||||
// --- 创建【但不显示】图像的UI窗口 ---
|
||||
var originalDisplay = new FormImageDisplay { Text = $"{setting.Name} - 原图", CameraName = setting.Name };
|
||||
var resultDisplay = new FormImageDisplay { Text = $"{setting.Name} - 结果", CameraName = setting.Name };
|
||||
originalDisplay.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
|
||||
resultDisplay.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
|
||||
|
||||
// --- 保存引用 ---
|
||||
ActiveCameras.Add(setting.Name, cam);
|
||||
OriginalImageDisplays.Add(setting.Name, originalDisplay);
|
||||
ResultImageDisplays.Add(setting.Name, resultDisplay);
|
||||
mainForm.AddCameraToStatusStrip(setting.Name);
|
||||
}
|
||||
ThreadSafeLogger.Log("所有设备和模型已准备就绪。");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 根据配置列表初始化或更新所有相机
|
||||
/// </summary>
|
||||
public static void Initialize(ProcessConfig config, FrmMain mainForm)
|
||||
{
|
||||
mainForm?.ClearStatusStrip();
|
||||
|
||||
// 先停止并释放所有旧的相机
|
||||
Shutdown();
|
||||
ThreadSafeLogger.Log("开始应用新的相机配置...");
|
||||
// 2. 初始化新的检测协调器
|
||||
DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings);
|
||||
|
||||
var deviceList = new HikvisionCamera().FindDevices();
|
||||
if (deviceList.Count == 0)
|
||||
{
|
||||
ThreadSafeLogger.Log("错误:未找到任何相机设备!");
|
||||
return;
|
||||
}
|
||||
int deviceIndex = 0;
|
||||
foreach (var device in config.ModelSettings.Where(s => s.IsEnabled))
|
||||
{
|
||||
if (!device.IsEnabled) continue;
|
||||
mainForm.AddCameraToStatusStrip(device.Name);
|
||||
}
|
||||
foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled))
|
||||
{
|
||||
if (!setting.IsEnabled) continue;
|
||||
|
||||
if (deviceIndex >= deviceList.Count)
|
||||
{
|
||||
ThreadSafeLogger.Log($"警告:相机配置'{setting.Name}'无法分配物理设备,因为设备数量不足。");
|
||||
continue;
|
||||
}
|
||||
// --- 创建相机实例 ---
|
||||
var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex };
|
||||
cam.TriggerMode = setting.TriggerMode;
|
||||
if (!cam.Open(setting))
|
||||
{
|
||||
ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。");
|
||||
cam.Dispose();
|
||||
continue;
|
||||
}
|
||||
// --- 设置触发模式 ---
|
||||
switch (setting.TriggerMode)
|
||||
{
|
||||
case TriggerModeType.Continuous:
|
||||
cam.SetContinuousMode();
|
||||
break;
|
||||
case TriggerModeType.Software:
|
||||
cam.SetTriggerMode(true);
|
||||
break;
|
||||
case TriggerModeType.Hardware:
|
||||
cam.SetTriggerMode(false);
|
||||
break;
|
||||
}
|
||||
|
||||
// --- 订阅事件 ---
|
||||
cam.ImageAcquired += OnCameraImageAcquired;
|
||||
cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}");
|
||||
ActiveCameras.Add(setting.Name, cam);
|
||||
// --- 创建显示窗口 ---
|
||||
var displayForm = new FormImageDisplay { Text = $"{setting.Name} - 原图", CameraName = setting.Name };
|
||||
var checkFrm = new FormImageDisplay { Text = $"{setting.Name} - 结果", CameraName = setting.Name };
|
||||
displayForm.OnDisplayEvent += ThreadSafeLogger.Log;
|
||||
displayForm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
|
||||
checkFrm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
|
||||
|
||||
OriginalImageDisplays.Add(setting.Name, displayForm);
|
||||
ResultImageDisplays.Add(setting.Name, checkFrm);
|
||||
mainForm.AddCameraToStatusStrip(setting.Name);
|
||||
var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex);
|
||||
if (processor != null)
|
||||
{
|
||||
processor.OnProcessingCompleted += Processor_OnProcessingCompleted;
|
||||
}
|
||||
ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。");
|
||||
deviceIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动硬件触发模拟器。
|
||||
/// </summary>
|
||||
public static void StartHardwareTriggerSimulator()
|
||||
{
|
||||
if (IsHardwareTriggerSimulating) return;
|
||||
|
||||
_hardwareTriggerSimulator.Interval = TriggerInterval;
|
||||
_hardwareTriggerSimulator.Start();
|
||||
IsHardwareTriggerSimulating = true;
|
||||
ThreadSafeLogger.Log($"硬件触发模拟器已启动,触发间隔: {TriggerInterval} ms。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止硬件触发模拟器。
|
||||
/// </summary>
|
||||
public static void StopHardwareTriggerSimulator()
|
||||
{
|
||||
if (!IsHardwareTriggerSimulating) return;
|
||||
|
||||
_hardwareTriggerSimulator.Stop();
|
||||
IsHardwareTriggerSimulating = false;
|
||||
ThreadSafeLogger.Log("硬件触发模拟器已停止。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定时器触发事件。
|
||||
/// </summary>
|
||||
private static void OnHardwareTriggerTimerElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
// 遍历所有活动的相机
|
||||
foreach (var cam in ActiveCameras.Values)
|
||||
{
|
||||
// 仅对配置为“硬件触发”模式的相机执行操作
|
||||
// 重要:我们使用软触发命令(SoftwareTrigger)来“模拟”一个外部硬件信号的到达。
|
||||
if (cam.TriggerMode == TriggerModeType.Software && cam.IsGrabbing)
|
||||
{
|
||||
// ThreadSafeLogger.Log($"模拟硬触发信号,触发相机: {cam.Name}"); // 如果需要详细日志可以取消注释
|
||||
cam.SoftwareTrigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 所有启用的相机开始采集
|
||||
/// </summary>
|
||||
public static void StartAll()
|
||||
{
|
||||
foreach (var cam in ActiveCameras.Values)
|
||||
{
|
||||
cam.StartGrabbing();
|
||||
}
|
||||
ThreadSafeLogger.Log("所有相机已开始采集。");
|
||||
}
|
||||
|
||||
//public static void StartDetection()
|
||||
//{
|
||||
// if (!IsDetectionRunning)
|
||||
// {
|
||||
// IsDetectionRunning = true;
|
||||
// ThreadSafeLogger.Log("检测已启动,开始统计数据。");
|
||||
// }
|
||||
//}
|
||||
//public static void StopDetection()
|
||||
//{
|
||||
// if (IsDetectionRunning)
|
||||
// {
|
||||
// IsDetectionRunning = false;
|
||||
// ThreadSafeLogger.Log("检测已停止。");
|
||||
// }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// 所有启用的相机停止采集
|
||||
/// </summary>
|
||||
public static void StopAll()
|
||||
{
|
||||
foreach (var cam in ActiveCameras.Values)
|
||||
{
|
||||
cam.StopGrabbing();
|
||||
}
|
||||
ThreadSafeLogger.Log("所有相机已停止采集。");
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 停止并释放所有相机资源
|
||||
///// </summary>
|
||||
//public static void Shutdown()
|
||||
//{
|
||||
// // --- 新增:确保在关闭时停止模拟器并释放资源 ---
|
||||
// StopHardwareTriggerSimulator();
|
||||
// //_hardwareTriggerSimulator?.Dispose();
|
||||
|
||||
// StopAll();
|
||||
// foreach (var cam in ActiveCameras.Values)
|
||||
// {
|
||||
// cam.Dispose();
|
||||
// }
|
||||
// foreach (var display in CameraDisplays.Values)
|
||||
// {
|
||||
// display.Close();
|
||||
// }
|
||||
// lock (QueueLock)
|
||||
// {
|
||||
// while (ProductQueue.Count > 0)
|
||||
// {
|
||||
// ProductQueue.Dequeue()?.Dispose();
|
||||
// }
|
||||
// }
|
||||
// ResetProductCounter();
|
||||
// ActiveCameras.Clear();
|
||||
// CameraDisplays.Clear();
|
||||
// ThreadSafeLogger.Log("正在关闭文件日志记录器...");
|
||||
// ThreadSafeLogger.Log("所有相机资源已释放。");
|
||||
|
||||
//}
|
||||
|
||||
// Shutdown 方法也简化
|
||||
public static void Shutdown()
|
||||
{
|
||||
// 1. 停止硬件和模拟器
|
||||
StopAll();
|
||||
StopHardwareTriggerSimulator();
|
||||
// 2. 关闭相机实例和窗口
|
||||
foreach (var cam in ActiveCameras.Values) { cam.Dispose(); }
|
||||
foreach (var display in OriginalImageDisplays.Values) { display.Close(); }
|
||||
foreach (var display in ResultImageDisplays.Values) { display.Close(); }
|
||||
ActiveCameras.Clear();
|
||||
OriginalImageDisplays.Clear();
|
||||
ResultImageDisplays.Clear();
|
||||
// 3. 关闭检测协调器,它会负责清理所有后台线程和队列
|
||||
DetectionCoordinator.Shutdown();
|
||||
|
||||
YoloModelManager.Shutdown();
|
||||
|
||||
ThreadSafeLogger.Log("所有相机及协调器资源已释放。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置产品计数器的公共方法
|
||||
/// </summary>
|
||||
public static void ResetProductCounter()
|
||||
{
|
||||
lock (_counterLock)
|
||||
{
|
||||
_productCounter = 0;
|
||||
}
|
||||
ThreadSafeLogger.Log("产品计数器已重置。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接收到相机图像时的核心处理逻辑
|
||||
/// </summary>
|
||||
//private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp)
|
||||
//{
|
||||
|
||||
// Bitmap bmpForDisplay =null;
|
||||
// Bitmap bmpForQueue = null;
|
||||
// try
|
||||
// {
|
||||
// bmpForDisplay?.Dispose();
|
||||
// bmpForQueue?.Dispose();
|
||||
// // 1. 为“显示”和“处理队列”创建独立的深克隆副本
|
||||
// bmpForDisplay = DeepCloneBitmap(bmp, "Display");
|
||||
// bmpForQueue = DeepCloneBitmap(bmp, "Queue");
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// // 【关键】无论克隆成功与否,都必须立即释放事件传递过来的原始bmp。
|
||||
// // 这是保证没有泄漏的第一道防线。
|
||||
// bmp?.Dispose();
|
||||
// }
|
||||
// // --- 现在我们使用完全独立的副本进行后续操作 ---
|
||||
|
||||
// // 4. 将显示副本传递给UI
|
||||
// if (bmpForDisplay != null && CameraDisplays.TryGetValue(sender.Name, out var display))
|
||||
// {
|
||||
// display.UpdateImage(bmpForDisplay);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // 如果不需要显示,或者显示失败,必须释放掉为它创建的副本
|
||||
// bmpForDisplay?.Dispose();
|
||||
// }
|
||||
// // 5. 将队列副本添加到产品中
|
||||
// if (bmpForQueue != null)
|
||||
// {
|
||||
// lock (QueueLock)
|
||||
// {
|
||||
// ProductResult currentProduct;
|
||||
// bool isNewProductCycle = ProductQueue.Count == 0 || ProductQueue.Last().CapturedImages.ContainsKey(sender.Name);
|
||||
// if (isNewProductCycle)
|
||||
// {
|
||||
// if (ProductQueue.Count > 0 && !ProductQueue.Peek().IsComplete(EnabledCameraCount))
|
||||
// {
|
||||
// var orphanedProduct = ProductQueue.Dequeue(); // 从队列头部移除这个不完整的产品
|
||||
// ThreadSafeLogger.Log($"[警告] 产品 #{orphanedProduct.ProductID} 未能集齐所有相机图像而被丢弃,以防止内存泄漏。");
|
||||
// orphanedProduct.Dispose(); // 确保释放其占用的所有资源
|
||||
// }
|
||||
// long newProductId;
|
||||
// lock (_counterLock)
|
||||
// {
|
||||
// _productCounter++;
|
||||
// newProductId = _productCounter;
|
||||
// }
|
||||
// currentProduct = new ProductResult(newProductId);
|
||||
// ProductQueue.Enqueue(currentProduct);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// currentProduct = ProductQueue.Last();
|
||||
// }
|
||||
|
||||
// currentProduct.AddImage(sender.Name, bmpForQueue); // bmpForQueue的所有权转移给了产品队列
|
||||
|
||||
// while (ProductQueue.Count > 0 && ProductQueue.Peek().IsComplete(EnabledCameraCount))
|
||||
// {
|
||||
// // Peek() 查看队头元素但不移除它
|
||||
// var finishedProduct = ProductQueue.Dequeue(); // Dequeue() 移除队头元素
|
||||
// ThreadSafeLogger.Log($"产品 #{finishedProduct.ProductID} 已完整,出队处理。");
|
||||
|
||||
// // --- 这是您已有的处理逻辑 ---
|
||||
// bool isOk = new Random().NextDouble() > 0.1;
|
||||
// string finalResult = isOk ? "OK" : "NG";
|
||||
// ThreadSafeLogger.Log($"产品 #{finishedProduct.ProductID} 检测结果: {finalResult}");
|
||||
|
||||
// if (IsDetectionRunning)
|
||||
// {
|
||||
// OnDetectionCompleted?.Invoke(null, new DetectionResultEventArgs(isOk));
|
||||
// }
|
||||
|
||||
// try
|
||||
// {
|
||||
// // 确保完成的产品被完全释放
|
||||
// finishedProduct.Dispose();
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// ThreadSafeLogger.Log($"[ERROR] 释放产品 #{finishedProduct.ProductID} 资源时出错: {ex.Message}");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// 图像回调方法现在极其简单
|
||||
private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp)
|
||||
{
|
||||
Bitmap bmpForDisplay = null;
|
||||
Bitmap bmpForProcessing = null;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 为“显示”和“处理”创建两个完全独立的深克隆副本
|
||||
bmpForDisplay = DeepCloneBitmap(bmp, "Display");
|
||||
bmpForProcessing = DeepCloneBitmap(bmp, "Processing");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 2.无论克隆成功与否,都必须立即释放事件传递过来的原始bmp,防止泄漏。
|
||||
bmp?.Dispose();
|
||||
}
|
||||
// 分支 A: 将用于显示的副本发送到对应的UI窗口
|
||||
if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow))
|
||||
{
|
||||
// displayWindow.UpdateImage 会处理线程安全问题
|
||||
displayWindow.UpdateImage(bmpForDisplay);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本
|
||||
bmpForDisplay?.Dispose();
|
||||
}
|
||||
// 分支 B: 将用于处理的副本发送到检测协调器的后台队列
|
||||
if (bmpForProcessing != null)
|
||||
{
|
||||
// bmpForProcessing 的所有权在这里被转移给了协调器
|
||||
DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing);
|
||||
}
|
||||
//// 深度克隆图像以确保线程安全
|
||||
//Bitmap bmpForProcessing = DeepCloneBitmap(bmp, "Processing");
|
||||
//bmp?.Dispose(); // 立即释放原始图
|
||||
|
||||
//// 直接将图像和相机编号交给协调器,无需任何本地处理
|
||||
//DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing);
|
||||
}
|
||||
// 事件处理器
|
||||
private static void Processor_OnProcessingCompleted(object sender, ProcessingCompletedEventArgs e)
|
||||
{
|
||||
|
||||
// 1. 找到与此相机匹配的相机名称
|
||||
var cameraEntry = ActiveCameras.FirstOrDefault(kvp => kvp.Value.CameraIndex == e.CameraIndex);
|
||||
if (cameraEntry.Key == null)
|
||||
{
|
||||
e.Dispose(); // 如果找不到接收者,必须释放事件参数中的图像
|
||||
return;
|
||||
}
|
||||
// 2. 找到此相机的结果显示窗口
|
||||
if (ResultImageDisplays.TryGetValue(cameraEntry.Key, out var resultDisplay))
|
||||
{
|
||||
var bmp = ConvertSKImageToBitmap(e.ResultImage);
|
||||
if (bmp != null)
|
||||
{
|
||||
// UpdateImage 会负责克隆并显示,所以这里传递 bmp 即可
|
||||
resultDisplay.UpdateImage(bmp);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果找到了相机但没有对应的结果窗口,也要释放图像
|
||||
e.Dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【将 SkiaSharp.SKImage 安全地转换为 System.Drawing.Bitmap。
|
||||
/// </summary>
|
||||
private static Bitmap ConvertSKImageToBitmap(SKImage skImage)
|
||||
{
|
||||
if (skImage == null) return null;
|
||||
|
||||
try
|
||||
{
|
||||
// SKImage -> SKBitmap -> System.Drawing.Bitmap
|
||||
using (var skBitmap = SKBitmap.FromImage(skImage))
|
||||
{
|
||||
// SKBitmap.ToBitmap() 会创建一个新的 Bitmap 对象
|
||||
return SKBitmapToGdiBitmapFast(skBitmap);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[错误] SKImage to Bitmap 转换失败: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public static Bitmap SKBitmapToGdiBitmapFast(SKBitmap skBitmap)
|
||||
{
|
||||
if (skBitmap == null) throw new ArgumentNullException(nameof(skBitmap));
|
||||
if (skBitmap.ColorType != SKColorType.Bgra8888 || skBitmap.AlphaType != SKAlphaType.Premul)
|
||||
throw new ArgumentException("skBitmap must be Bgra8888 + Premul for the fast path.");
|
||||
|
||||
int w = skBitmap.Width;
|
||||
int h = skBitmap.Height;
|
||||
|
||||
Bitmap bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb);
|
||||
var rect = new Rectangle(0, 0, w, h);
|
||||
var bmpData = bmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
|
||||
try
|
||||
{
|
||||
IntPtr srcPtr = skBitmap.GetPixels();
|
||||
int srcRowBytes = skBitmap.RowBytes;
|
||||
int dstRowBytes = bmpData.Stride;
|
||||
int copyBytesPerRow = Math.Min(srcRowBytes, dstRowBytes);
|
||||
|
||||
byte[] row = new byte[copyBytesPerRow]; // 复用同一行缓冲区,避免每行分配
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
IntPtr s = IntPtr.Add(srcPtr, y * srcRowBytes);
|
||||
IntPtr d = IntPtr.Add(bmpData.Scan0, y * dstRowBytes);
|
||||
|
||||
Marshal.Copy(s, row, 0, copyBytesPerRow);
|
||||
Marshal.Copy(row, 0, d, copyBytesPerRow);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
bmp.UnlockBits(bmpData);
|
||||
}
|
||||
|
||||
return bmp;
|
||||
}
|
||||
/// <summary>
|
||||
/// 对Bitmap进行真正的深度克隆,确保内存完全独立。
|
||||
/// 这是解决跨线程GDI+问题的最可靠方法。
|
||||
/// </summary>
|
||||
/// <param name="source">源Bitmap对象。</param>
|
||||
/// <returns>一个与源图像在内存上完全独立的全新Bitmap对象。</returns>
|
||||
private static Bitmap DeepCloneBitmap(Bitmap source, string cloneFor)
|
||||
{
|
||||
if (source == null) return null;
|
||||
|
||||
// 创建一个新的Bitmap对象,具有与源相同的尺寸和像素格式
|
||||
Bitmap clone = new Bitmap(source.Width, source.Height, source.PixelFormat);
|
||||
|
||||
// 如果有调色板,复制调色板
|
||||
if (source.Palette.Entries.Length > 0)
|
||||
{
|
||||
clone.Palette = source.Palette;
|
||||
}
|
||||
|
||||
// 锁定源和目标Bitmap的内存区域
|
||||
var rect = new Rectangle(0, 0, source.Width, source.Height);
|
||||
BitmapData sourceData = null; //source.LockBits(rect, ImageLockMode.ReadOnly, source.PixelFormat);
|
||||
BitmapData cloneData = null;//clone.LockBits(rect, ImageLockMode.WriteOnly, clone.PixelFormat);
|
||||
|
||||
try
|
||||
{
|
||||
sourceData = source.LockBits(rect, ImageLockMode.ReadOnly, source.PixelFormat);
|
||||
cloneData = clone.LockBits(rect, ImageLockMode.WriteOnly, clone.PixelFormat);
|
||||
// 计算需要拷贝的字节数
|
||||
int byteCount = Math.Abs(sourceData.Stride) * source.Height;
|
||||
byte[] buffer = new byte[byteCount];
|
||||
|
||||
// 从源图像拷贝数据到字节数组
|
||||
Marshal.Copy(sourceData.Scan0, buffer, 0, byteCount);
|
||||
|
||||
// 从字节数组拷贝数据到目标图像
|
||||
Marshal.Copy(buffer, 0, cloneData.Scan0, byteCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[克隆错误] 在 DeepCloneBitmap ({cloneFor}) 中发生异常: {ex.Message}");
|
||||
// 如果发生错误,确保返回null,并且释放可能已经创建的clone对象
|
||||
clone?.Dispose();
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 确保即使在发生错误时也能尝试解锁
|
||||
if (sourceData != null)
|
||||
{
|
||||
source.UnlockBits(sourceData);
|
||||
}
|
||||
if (cloneData != null)
|
||||
{
|
||||
clone.UnlockBits(cloneData);
|
||||
}
|
||||
//ThreadSafeLogger.Log($"[克隆完成] 解锁并完成克隆 ({cloneFor}).");
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
// 触发日志事件的辅助方法
|
||||
//public static void Log(string message)
|
||||
//{
|
||||
// string formattedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
|
||||
|
||||
// // 1. 触发事件,更新UI
|
||||
// OnLogMessage?.Invoke(formattedMessage);
|
||||
|
||||
// // 2. 【关键修改】在写入文件前,先获取锁。
|
||||
// // lock 语句块确保了花括号内的代码在同一时间只能被一个线程执行。
|
||||
// // 如果另一个线程也想执行这段代码,它必须等待前一个线程执行完毕并释放锁。
|
||||
// lock (_logLock)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// _logFileWriter?.WriteLine(formattedMessage);
|
||||
// }
|
||||
// catch (Exception)
|
||||
// {
|
||||
// // 即使有锁,也保留try-catch以防万一(如磁盘满了等IO问题)。
|
||||
// // 可以在这里决定是否要在UI上显示警告。
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
211
Check.Main/Camera/CameraProcessor.cs
Normal file
211
Check.Main/Camera/CameraProcessor.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using Check.Main.Common;
|
||||
using Check.Main.Infer;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using YoloDotNet.Extensions;
|
||||
|
||||
namespace Check.Main.Camera
|
||||
{
|
||||
public class CameraProcessor : IDisposable
|
||||
{
|
||||
private readonly int _cameraIndex;
|
||||
private readonly int _modeId;
|
||||
// private readonly ModelSettings _model;
|
||||
private readonly BlockingCollection<ImageData> _imageQueue = new BlockingCollection<ImageData>();
|
||||
private readonly Thread _workerThread;
|
||||
private volatile bool _isRunning = false;
|
||||
private long _imageCounter = 0;
|
||||
private readonly object _counterLock = new object(); // 用于线程安全地重置计数器
|
||||
|
||||
public event EventHandler<ProcessingCompletedEventArgs> OnProcessingCompleted;
|
||||
|
||||
|
||||
public CameraProcessor(int cameraIndex, int modelId)//, ModelSettings model
|
||||
{
|
||||
_cameraIndex = cameraIndex;
|
||||
_modeId = modelId;
|
||||
//_model = model;
|
||||
_workerThread = new Thread(ProcessQueue) { IsBackground = true, Name = $"Cam_{_cameraIndex}_Processor" };
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_isRunning = true;
|
||||
_workerThread.Start();
|
||||
}
|
||||
|
||||
public void EnqueueImage(Bitmap bmp)
|
||||
{
|
||||
if (!_isRunning)
|
||||
{
|
||||
bmp?.Dispose();
|
||||
return;
|
||||
}
|
||||
_imageCounter++;
|
||||
_imageQueue.Add(new ImageData(_imageCounter, _cameraIndex, bmp));
|
||||
}
|
||||
|
||||
private void ProcessQueue()
|
||||
{
|
||||
// 从模型管理器获取此线程专属的YOLO模型
|
||||
var yoloModel = YoloModelManager.GetModel(_modeId);
|
||||
if (yoloModel == null)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型,处理线程已中止。");
|
||||
return; // 如果没有模型,此线程无法工作
|
||||
}
|
||||
while (_isRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 阻塞式地从队列中取出图像,如果队列为空则等待
|
||||
ImageData data = _imageQueue.Take();
|
||||
using (data)
|
||||
{
|
||||
//SKImage resultSkImage = null; // 用于存储最终绘制好结果的图像
|
||||
|
||||
using (var skImage = ConvertBitmapToSKImage(data.Image)) // 转换图像格式并管理其生命周期
|
||||
{
|
||||
if (skImage == null) continue;
|
||||
var predictions = yoloModel.RunObjectDetection(skImage);
|
||||
// 模拟模型处理
|
||||
//Thread.Sleep(50); // 模拟耗时
|
||||
//bool isOk = new Random().NextDouble() > 0.1;
|
||||
//string result = isOk ? "OK" : "NG";
|
||||
|
||||
string result = predictions.Any() ? "NG" : "OK";
|
||||
ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},检测到 {predictions.Count} 个目标,结果: {result}");
|
||||
// 将处理结果交给协调器进行组装
|
||||
DetectionCoordinator.AssembleProduct(data, result);
|
||||
|
||||
if (OnProcessingCompleted != null)
|
||||
{
|
||||
using (var resultSkImage = skImage.Draw(predictions))
|
||||
{
|
||||
// 4. 触发事件,将绘制好的 resultSkImage 传递出去
|
||||
// 所有权在这里被转移
|
||||
OnProcessingCompleted?.Invoke(this, new ProcessingCompletedEventArgs(
|
||||
_cameraIndex,
|
||||
data.ProductId,
|
||||
resultSkImage
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// 当调用 Stop 时,会 CompleteAdding 队列,Take 会抛出此异常,是正常退出流程
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[ERROR] 相机 #{_cameraIndex} 处理线程异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 将 System.Drawing.Bitmap 安全地转换为 SkiaSharp.SKImage。
|
||||
/// </summary>
|
||||
private SKImage ConvertBitmapToSKImage(Bitmap bitmap)
|
||||
{
|
||||
if (bitmap == null) return null;
|
||||
try
|
||||
{
|
||||
// 使用 using 确保 SKBitmap 被正确释放
|
||||
using (var skBitmap = ToSKBitmapFast(bitmap))
|
||||
{
|
||||
return SKImage.FromBitmap(skBitmap);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[错误] Bitmap to SKImage 转换失败: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public static SKBitmap ToSKBitmapFast(Bitmap bitmap)
|
||||
{
|
||||
// 确保是 32bppArgb(BGRA 内存布局)
|
||||
Bitmap src = bitmap;
|
||||
if (bitmap.PixelFormat != PixelFormat.Format32bppArgb)
|
||||
{
|
||||
src = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb);
|
||||
using (Graphics g = Graphics.FromImage(src))
|
||||
{
|
||||
g.DrawImage(bitmap, 0, 0, bitmap.Width, bitmap.Height);
|
||||
}
|
||||
}
|
||||
|
||||
var rect = new Rectangle(0, 0, src.Width, src.Height);
|
||||
var bmpData = src.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
||||
try
|
||||
{
|
||||
var info = new SKImageInfo(src.Width, src.Height, SKColorType.Bgra8888, SKAlphaType.Premul);
|
||||
var skBitmap = new SKBitmap(info);
|
||||
|
||||
IntPtr destPtr = skBitmap.GetPixels(); // 目标内存
|
||||
IntPtr srcRowPtr = bmpData.Scan0; // 源行首
|
||||
int srcStride = bmpData.Stride;
|
||||
int destRowBytes = skBitmap.RowBytes;
|
||||
int copyBytesPerRow = Math.Min(srcStride, destRowBytes);
|
||||
|
||||
// 使用一次分配的缓冲区并用 Marshal.Copy 行拷贝(不分配每行)
|
||||
byte[] row = new byte[copyBytesPerRow];
|
||||
for (int y = 0; y < src.Height; y++)
|
||||
{
|
||||
IntPtr s = IntPtr.Add(bmpData.Scan0, y * srcStride);
|
||||
IntPtr d = IntPtr.Add(destPtr, y * destRowBytes);
|
||||
|
||||
Marshal.Copy(s, row, 0, copyBytesPerRow);
|
||||
Marshal.Copy(row, 0, d, copyBytesPerRow);
|
||||
}
|
||||
|
||||
return skBitmap;
|
||||
}
|
||||
finally
|
||||
{
|
||||
src.UnlockBits(bmpData);
|
||||
if (!ReferenceEquals(src, bitmap))
|
||||
{
|
||||
src.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
public void Stop()
|
||||
{
|
||||
_isRunning = false;
|
||||
// 解除阻塞,让线程可以检查 _isRunning 标志并退出
|
||||
_imageQueue.CompleteAdding();
|
||||
_workerThread.Join(500); // 等待线程结束
|
||||
}
|
||||
/// <summary>
|
||||
/// 线程安全地重置该相机的图像计数器。
|
||||
/// </summary>
|
||||
public void ResetCounter()
|
||||
{
|
||||
lock (_counterLock)
|
||||
{
|
||||
_imageCounter = 0;
|
||||
}
|
||||
}
|
||||
// 别忘了在 DetectionCoordinator 中添加一个辅助方法来获取处理器
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_imageQueue.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
182
Check.Main/Camera/CameraSettings.cs
Normal file
182
Check.Main/Camera/CameraSettings.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using Check.Main.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms.Design;
|
||||
|
||||
namespace Check.Main.Camera
|
||||
{
|
||||
// 相机触发方式枚举
|
||||
public enum TriggerModeType
|
||||
{
|
||||
[Description("连续采集")]
|
||||
Continuous,
|
||||
[Description("软件触发")]
|
||||
Software,
|
||||
[Description("硬件触发")]
|
||||
Hardware
|
||||
}
|
||||
public enum CheckType
|
||||
{
|
||||
[Description("传统算法")]
|
||||
Traditional,
|
||||
[Description("深度学习")]
|
||||
DeepLearning
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 相机配置信息类,用于PropertyGrid显示和编辑
|
||||
/// </summary>
|
||||
public class CameraSettings : INotifyPropertyChanged, ICloneable
|
||||
{
|
||||
// 1. 实现 INotifyPropertyChanged 接口所要求的事件
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
private int _cameraIndex = 0;
|
||||
private string _name = "Camera-1";
|
||||
private string _ipAddress = "192.168.1.100";
|
||||
private string _ipDeviceAddress = "192.168.1.101";
|
||||
private TriggerModeType _triggerMode = TriggerModeType.Continuous;
|
||||
private bool _isEnabled = true;
|
||||
private CheckType _checkType = CheckType.DeepLearning;
|
||||
private int _modelID = 0;
|
||||
|
||||
[Category("基本信息"), DisplayName("相机编号"), Description("相机的唯一标识符,用于与模型编号对应。")]
|
||||
public int CameraIndex
|
||||
{
|
||||
get => _cameraIndex;
|
||||
set
|
||||
{
|
||||
if (_cameraIndex != value)
|
||||
{
|
||||
_cameraIndex = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
[Category("基本信息"), DisplayName("相机名称"), Description("给相机起一个唯一的别名")]
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
// 2. 在setter中检查值是否真的改变了
|
||||
if (_name != value)
|
||||
{
|
||||
_name = value;
|
||||
// 3. 如果改变了,就调用通知方法,广播“Name”属性已变更的消息
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Category("网络信息"), DisplayName("相机IP地址"), Description("相机的IP地址(GigE相机需要)")]
|
||||
public string IPAddress
|
||||
{
|
||||
get => _ipAddress;
|
||||
set
|
||||
{
|
||||
if (_ipAddress != value)
|
||||
{
|
||||
_ipAddress = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Category("网络信息"), DisplayName("上位机IP地址"), Description("相机对应的上位机的IP地址(GigE相机需要)")]
|
||||
public string IPDeviceAddress
|
||||
{
|
||||
get => _ipDeviceAddress;
|
||||
set
|
||||
{
|
||||
if (_ipDeviceAddress != value)
|
||||
{
|
||||
_ipDeviceAddress = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Category("采集控制"), DisplayName("触发模式"), Description("设置相机的图像采集触发方式")]
|
||||
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
|
||||
public TriggerModeType TriggerMode
|
||||
{
|
||||
get => _triggerMode;
|
||||
set
|
||||
{
|
||||
if (_triggerMode != value)
|
||||
{
|
||||
_triggerMode = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Category("采集控制"), DisplayName("是否启用"), Description("是否在程序启动时初始化并使用此相机")]
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _isEnabled;
|
||||
set
|
||||
{
|
||||
if (_isEnabled != value)
|
||||
{
|
||||
_isEnabled = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Category("检测配置"), DisplayName("检测类型"), Description("使用传统算法或深度学习")]
|
||||
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
|
||||
public CheckType CheckType
|
||||
{
|
||||
get => _checkType;
|
||||
set
|
||||
{
|
||||
if (_checkType != value)
|
||||
{
|
||||
_checkType = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Category("基本信息"), DisplayName("模型编号"), Description("相机调用的模型编号")]
|
||||
[TypeConverter(typeof(ModelSelectionConverter))]
|
||||
public int ModelID
|
||||
{
|
||||
get => _modelID;
|
||||
set
|
||||
{
|
||||
if (_modelID != value)
|
||||
{
|
||||
_modelID = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 4. 触发 PropertyChanged 事件的辅助方法
|
||||
/// [CallerMemberName] 特性是一个“语法糖”,它会自动获取调用此方法的属性的名称。
|
||||
/// 例如,在Name属性的setter中调用OnPropertyChanged(),C#编译器会自动传入"Name"作为参数。
|
||||
/// </summary>
|
||||
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
public object Clone()
|
||||
{
|
||||
// MemberwiseClone() 会创建一个新对象,并将当前对象的所有非静态字段的值
|
||||
// 复制到新对象中。对于值类型,这是值的拷贝;对于引用类型,这是引用的拷贝。
|
||||
// 在本类中,这已经足够了。
|
||||
return this.MemberwiseClone();
|
||||
}
|
||||
}
|
||||
}
|
||||
502
Check.Main/Camera/HikvisionCamera.cs
Normal file
502
Check.Main/Camera/HikvisionCamera.cs
Normal file
@@ -0,0 +1,502 @@
|
||||
using MvCamCtrl.NET;
|
||||
using Sunny.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Check.Main.Camera
|
||||
{
|
||||
public class HikvisionCamera : IDisposable
|
||||
{
|
||||
[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
|
||||
private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
|
||||
// 事件委托
|
||||
public delegate void ImageAcquiredEventHandler(HikvisionCamera sender, Bitmap bmp);
|
||||
// public delegate void ImageAcquiredEventHandler(object sender, Bitmap bmp);
|
||||
public delegate void CameraMessageEventHandler(object sender, string message, int errorCode = 0);
|
||||
|
||||
// 事件
|
||||
public event ImageAcquiredEventHandler ImageAcquired;
|
||||
public event CameraMessageEventHandler CameraMessage;
|
||||
|
||||
// 私有成员变量
|
||||
private MyCamera m_MyCamera;
|
||||
private MyCamera.MV_CC_DEVICE_INFO_LIST m_stDeviceList;
|
||||
private bool m_bGrabbing = false;
|
||||
private Thread m_hReceiveThread = null;
|
||||
|
||||
private Bitmap m_bitmap = null;
|
||||
private IntPtr m_ConvertDstBuf = IntPtr.Zero;
|
||||
private uint m_nConvertDstBufLen = 0;
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 由外部逻辑分配的、唯一的相机软件编号。
|
||||
/// 主要用作图像路由和与逻辑处理器匹配的键。
|
||||
/// </summary>
|
||||
public int CameraIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 相机是否已经打开
|
||||
/// </summary>
|
||||
public bool IsOpen { get; private set; } = false;
|
||||
|
||||
public TriggerModeType TriggerMode { get; internal set; } // internal set 保证只有 CameraManager 能设置它
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 相机是否正在采集
|
||||
/// </summary>
|
||||
public bool IsGrabbing => m_bGrabbing;
|
||||
|
||||
public HikvisionCamera()
|
||||
{
|
||||
// 初始化SDK
|
||||
MyCamera.MV_CC_Initialize_NET();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找所有可用的相机设备
|
||||
/// </summary>
|
||||
/// <returns>设备名称列表</returns>
|
||||
public List<string> FindDevices()
|
||||
{
|
||||
var deviceList = new List<string>();
|
||||
m_stDeviceList = new MyCamera.MV_CC_DEVICE_INFO_LIST();
|
||||
int nRet = MyCamera.MV_CC_EnumDevices_NET(MyCamera.MV_GIGE_DEVICE | MyCamera.MV_USB_DEVICE, ref m_stDeviceList);
|
||||
if (nRet != MyCamera.MV_OK)
|
||||
{
|
||||
OnCameraMessage("设备枚举失败!", nRet);
|
||||
return deviceList;
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_stDeviceList.nDeviceNum; i++)
|
||||
{
|
||||
var device = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(m_stDeviceList.pDeviceInfo[i], typeof(MyCamera.MV_CC_DEVICE_INFO));
|
||||
if (device.nTLayerType == MyCamera.MV_GIGE_DEVICE)
|
||||
{
|
||||
var gigeInfo = (MyCamera.MV_GIGE_DEVICE_INFO_EX)MyCamera.ByteToStruct(device.SpecialInfo.stGigEInfo, typeof(MyCamera.MV_GIGE_DEVICE_INFO_EX));
|
||||
if (!string.IsNullOrEmpty(Encoding.Default.GetString(gigeInfo.chUserDefinedName).TrimEnd('\0')))
|
||||
deviceList.Add("GEV: " + gigeInfo.chUserDefinedName + " (" + gigeInfo.chSerialNumber + ")");
|
||||
else
|
||||
deviceList.Add("GEV: " + gigeInfo.chManufacturerName + " " + gigeInfo.chModelName + " (" + gigeInfo.chSerialNumber + ")");
|
||||
}
|
||||
else if (device.nTLayerType == MyCamera.MV_USB_DEVICE)
|
||||
{
|
||||
var usbInfo = (MyCamera.MV_USB3_DEVICE_INFO_EX)MyCamera.ByteToStruct(device.SpecialInfo.stUsb3VInfo, typeof(MyCamera.MV_USB3_DEVICE_INFO_EX));
|
||||
if (!string.IsNullOrEmpty(Encoding.Default.GetString(usbInfo.chUserDefinedName).TrimEnd('\0')))
|
||||
deviceList.Add("U3V: " + usbInfo.chUserDefinedName + " (" + usbInfo.chSerialNumber + ")");
|
||||
else
|
||||
deviceList.Add("U3V: " + usbInfo.chManufacturerName + " " + usbInfo.chModelName + " (" + usbInfo.chSerialNumber + ")");
|
||||
}
|
||||
}
|
||||
return deviceList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据索引打开相机
|
||||
/// </summary>
|
||||
/// <param name="index">设备索引</param>
|
||||
/// <returns>成功返回true,否则返回false</returns>
|
||||
public bool Open(CameraSettings camConfig)
|
||||
{
|
||||
string ipCam = "";
|
||||
string ipDevice = "";
|
||||
FindDevices();
|
||||
if (camConfig.IsEnabled == true)
|
||||
{
|
||||
ipCam = camConfig.IPAddress;
|
||||
ipDevice = camConfig.IPDeviceAddress;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (m_stDeviceList.nDeviceNum == 0 || string.IsNullOrEmpty(camConfig.IPAddress))
|
||||
{
|
||||
OnCameraMessage("没有找到设备或索引无效。", -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsOpen)
|
||||
{
|
||||
OnCameraMessage("相机已经打开。", 0);
|
||||
return true;
|
||||
}
|
||||
MyCamera.MV_CC_DEVICE_INFO device = new MyCamera.MV_CC_DEVICE_INFO();
|
||||
device.nTLayerType = MyCamera.MV_GIGE_DEVICE;
|
||||
MyCamera.MV_GIGE_DEVICE_INFO stGigEDev = new MyCamera.MV_GIGE_DEVICE_INFO();
|
||||
var parts = ipCam.Split('.');
|
||||
int nIp1 = Convert.ToInt32(parts[0]);
|
||||
int nIp2 = Convert.ToInt32(parts[1]);
|
||||
int nIp3 = Convert.ToInt32(parts[2]);
|
||||
int nIp4 = Convert.ToInt32(parts[3]);
|
||||
stGigEDev.nCurrentIp = (uint)((nIp1 << 24) | (nIp2 << 16) | (nIp3 << 8) | nIp4);
|
||||
|
||||
parts = ipDevice.Split('.');
|
||||
nIp1 = Convert.ToInt32(parts[0]);
|
||||
nIp2 = Convert.ToInt32(parts[1]);
|
||||
nIp3 = Convert.ToInt32(parts[2]);
|
||||
nIp4 = Convert.ToInt32(parts[3]);
|
||||
stGigEDev.nNetExport = (uint)((nIp1 << 24) | (nIp2 << 16) | (nIp3 << 8) | nIp4);
|
||||
IntPtr stGigeInfoPtr = Marshal.AllocHGlobal(216);
|
||||
Marshal.StructureToPtr(stGigEDev, stGigeInfoPtr, false);
|
||||
device.SpecialInfo.stGigEInfo = new Byte[540];
|
||||
Marshal.Copy(stGigeInfoPtr, device.SpecialInfo.stGigEInfo, 0, 540);
|
||||
//释放内存空间
|
||||
Marshal.FreeHGlobal(stGigeInfoPtr);
|
||||
|
||||
//var device = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(m_stDeviceList.pDeviceInfo[index], typeof(MyCamera.MV_CC_DEVICE_INFO));
|
||||
m_MyCamera = new MyCamera();
|
||||
int nRet = m_MyCamera.MV_CC_CreateDevice_NET(ref device);
|
||||
if (nRet != MyCamera.MV_OK)
|
||||
{
|
||||
OnCameraMessage("创建设备句柄失败!", nRet);
|
||||
return false;
|
||||
}
|
||||
|
||||
nRet = m_MyCamera.MV_CC_OpenDevice_NET();
|
||||
if (nRet != MyCamera.MV_OK)
|
||||
{
|
||||
OnCameraMessage("打开设备失败!", nRet);
|
||||
m_MyCamera.MV_CC_DestroyDevice_NET();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 探测网络最佳包大小(只对GigE相机有效)
|
||||
if (device.nTLayerType == MyCamera.MV_GIGE_DEVICE)
|
||||
{
|
||||
int nPacketSize = m_MyCamera.MV_CC_GetOptimalPacketSize_NET();
|
||||
if (nPacketSize > 0)
|
||||
{
|
||||
m_MyCamera.MV_CC_SetIntValueEx_NET("GevSCPSPacketSize", nPacketSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 默认设置为连续模式
|
||||
SetContinuousMode();
|
||||
|
||||
IsOpen = true;
|
||||
OnCameraMessage("相机打开成功。", 0);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnCameraMessage("相机打开失败。"+ ex.ToString(), 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭相机
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
if (!IsOpen) return;
|
||||
|
||||
if (m_bGrabbing)
|
||||
{
|
||||
StopGrabbing();
|
||||
}
|
||||
|
||||
m_MyCamera.MV_CC_CloseDevice_NET();
|
||||
m_MyCamera.MV_CC_DestroyDevice_NET();
|
||||
|
||||
IsOpen = false;
|
||||
m_MyCamera = null;
|
||||
OnCameraMessage("相机已关闭。", 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始采集图像
|
||||
/// </summary>
|
||||
/// <returns>成功返回true</returns>
|
||||
public bool StartGrabbing()
|
||||
{
|
||||
if (!IsOpen || m_bGrabbing)
|
||||
{
|
||||
OnCameraMessage(m_bGrabbing ? "已经在采集中。" : "相机未打开。", -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 取图前的必要操作
|
||||
if (NecessaryOperBeforeGrab() != MyCamera.MV_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int nRet = m_MyCamera.MV_CC_StartGrabbing_NET();
|
||||
if (nRet != MyCamera.MV_OK)
|
||||
{
|
||||
OnCameraMessage("开始采集失败!", nRet);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_bGrabbing = true;
|
||||
m_hReceiveThread = new Thread(ReceiveThreadProcess);
|
||||
m_hReceiveThread.IsBackground = true;
|
||||
m_hReceiveThread.Start();
|
||||
|
||||
OnCameraMessage("开始采集。", 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止采集图像
|
||||
/// </summary>
|
||||
public void StopGrabbing()
|
||||
{
|
||||
if (!m_bGrabbing) return;
|
||||
|
||||
m_bGrabbing = false;
|
||||
if (m_hReceiveThread != null && m_hReceiveThread.IsAlive)
|
||||
{
|
||||
m_hReceiveThread.Join(1000); // 等待线程退出
|
||||
}
|
||||
|
||||
int nRet = m_MyCamera.MV_CC_StopGrabbing_NET();
|
||||
if (nRet != MyCamera.MV_OK)
|
||||
{
|
||||
OnCameraMessage("停止采集失败!", nRet);
|
||||
}
|
||||
|
||||
OnCameraMessage("停止采集。", 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置为连续采集模式
|
||||
/// </summary>
|
||||
public void SetContinuousMode()
|
||||
{
|
||||
if (!IsOpen) return;
|
||||
m_MyCamera.MV_CC_SetEnumValue_NET("AcquisitionMode", (uint)MyCamera.MV_CAM_ACQUISITION_MODE.MV_ACQ_MODE_CONTINUOUS);
|
||||
m_MyCamera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_OFF);
|
||||
OnCameraMessage("已设置为连续模式。", 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置为触发模式
|
||||
/// </summary>
|
||||
/// <param name="isSoftwareTrigger">true为软触发, false为硬触发(Line0)</param>
|
||||
public void SetTriggerMode(bool isSoftwareTrigger)
|
||||
{
|
||||
if (!IsOpen) return;
|
||||
|
||||
m_MyCamera.MV_CC_SetEnumValue_NET("AcquisitionMode", (uint)MyCamera.MV_CAM_ACQUISITION_MODE.MV_ACQ_MODE_CONTINUOUS); // 触发模式也需要在Continuous模式下
|
||||
m_MyCamera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_ON);
|
||||
|
||||
if (isSoftwareTrigger)
|
||||
{
|
||||
m_MyCamera.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_SOFTWARE);
|
||||
OnCameraMessage("已设置为软触发模式。", 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_MyCamera.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_LINE0);
|
||||
OnCameraMessage("已设置为硬触发(Line0)模式。", 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行一次软触发
|
||||
/// </summary>
|
||||
/// <returns>成功返回true</returns>
|
||||
public bool SoftwareTrigger()
|
||||
{
|
||||
if (!IsOpen || !m_bGrabbing)
|
||||
{
|
||||
OnCameraMessage("请先打开相机并开始采集。", -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
int nRet = m_MyCamera.MV_CC_SetCommandValue_NET("TriggerSoftware");
|
||||
if (nRet != MyCamera.MV_OK)
|
||||
{
|
||||
OnCameraMessage("软触发失败!", nRet);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图像接收线程
|
||||
/// </summary>
|
||||
private void ReceiveThreadProcess()
|
||||
{
|
||||
MyCamera.MV_FRAME_OUT stFrameInfo = new MyCamera.MV_FRAME_OUT();
|
||||
var stConvertParam = new MyCamera.MV_PIXEL_CONVERT_PARAM();
|
||||
|
||||
while (m_bGrabbing)
|
||||
{
|
||||
int nRet = m_MyCamera.MV_CC_GetImageBuffer_NET(ref stFrameInfo, 1000);
|
||||
if (nRet == MyCamera.MV_OK)
|
||||
{
|
||||
Bitmap bmpForEvent = null; // 在 try 外部声明
|
||||
try
|
||||
{
|
||||
// 初始化转换参数
|
||||
stConvertParam.nWidth = stFrameInfo.stFrameInfo.nWidth;
|
||||
stConvertParam.nHeight = stFrameInfo.stFrameInfo.nHeight;
|
||||
stConvertParam.pSrcData = stFrameInfo.pBufAddr;
|
||||
stConvertParam.nSrcDataLen = stFrameInfo.stFrameInfo.nFrameLen;
|
||||
stConvertParam.enSrcPixelType = stFrameInfo.stFrameInfo.enPixelType;
|
||||
stConvertParam.pDstBuffer = m_ConvertDstBuf;
|
||||
stConvertParam.nDstBufferSize = m_nConvertDstBufLen;
|
||||
|
||||
// 创建用于事件的Bitmap,它的格式与类成员m_bitmap一致
|
||||
bmpForEvent = new Bitmap((int)stConvertParam.nWidth, (int)stConvertParam.nHeight, m_bitmap.PixelFormat);
|
||||
|
||||
// 判断并设置目标像素格式
|
||||
if (bmpForEvent.PixelFormat == PixelFormat.Format8bppIndexed)
|
||||
{
|
||||
stConvertParam.enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8;
|
||||
}
|
||||
else
|
||||
{
|
||||
stConvertParam.enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_BGR8_Packed;
|
||||
}
|
||||
|
||||
// 执行像素格式转换
|
||||
nRet = m_MyCamera.MV_CC_ConvertPixelType_NET(ref stConvertParam);
|
||||
if (nRet == MyCamera.MV_OK)
|
||||
{
|
||||
// 转换成功,锁定位图内存并拷贝数据
|
||||
BitmapData bmpData = bmpForEvent.LockBits(new Rectangle(0, 0, stConvertParam.nWidth, stConvertParam.nHeight), ImageLockMode.WriteOnly, bmpForEvent.PixelFormat);
|
||||
|
||||
// 使用更健壮的逐行拷贝
|
||||
int pixelSize = (bmpForEvent.PixelFormat == PixelFormat.Format8bppIndexed) ? 1 : 3;
|
||||
for (int i = 0; i < bmpData.Height; ++i)
|
||||
{
|
||||
IntPtr pDst = new IntPtr(bmpData.Scan0.ToInt64() + i * bmpData.Stride);
|
||||
IntPtr pSrc = new IntPtr(m_ConvertDstBuf.ToInt64() + i * stConvertParam.nWidth * pixelSize);
|
||||
CopyMemory(pDst, pSrc, (uint)(stConvertParam.nWidth * pixelSize));
|
||||
}
|
||||
bmpForEvent.UnlockBits(bmpData);
|
||||
|
||||
// 触发图像采集事件,转移 bmpForEvent 的所有权
|
||||
ImageAcquired?.Invoke(this, bmpForEvent);
|
||||
|
||||
// 【关键】所有权已转移,将本地引用设为null,防止它在finally块中被错误地释放
|
||||
bmpForEvent = null;
|
||||
}
|
||||
// 如果转换失败,不执行任何操作,让 finally 块来处理 bmpForEvent 的释放
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 【关键】如果 bmpForEvent 不为 null,意味着它被成功创建,
|
||||
// 但没有被成功传递给事件处理器(可能因为转换失败或发生其他异常),
|
||||
// 在这里必须将其释放。
|
||||
bmpForEvent?.Dispose();
|
||||
|
||||
// 无论成功与否,都必须释放相机SDK的内部图像缓存
|
||||
m_MyCamera.MV_CC_FreeImageBuffer_NET(ref stFrameInfo);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 超时,继续等待
|
||||
Thread.Sleep(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取图前的内存和参数准备
|
||||
/// </summary>
|
||||
private int NecessaryOperBeforeGrab()
|
||||
{
|
||||
var stWidth = new MyCamera.MVCC_INTVALUE_EX();
|
||||
int nRet = m_MyCamera.MV_CC_GetIntValueEx_NET("Width", ref stWidth);
|
||||
if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取宽度失败!", nRet); return nRet; }
|
||||
|
||||
var stHeight = new MyCamera.MVCC_INTVALUE_EX();
|
||||
nRet = m_MyCamera.MV_CC_GetIntValueEx_NET("Height", ref stHeight);
|
||||
if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取高度失败!", nRet); return nRet; }
|
||||
|
||||
var stPixelFormat = new MyCamera.MVCC_ENUMVALUE();
|
||||
nRet = m_MyCamera.MV_CC_GetEnumValue_NET("PixelFormat", ref stPixelFormat);
|
||||
if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取像素格式失败!", nRet); return nRet; }
|
||||
|
||||
PixelFormat bitmapPixelFormat;
|
||||
// 判断当前像素格式是否为Mono
|
||||
bool isMono = IsMono(stPixelFormat.nCurValue);
|
||||
|
||||
if (isMono)
|
||||
{
|
||||
bitmapPixelFormat = PixelFormat.Format8bppIndexed;
|
||||
m_nConvertDstBufLen = (uint)(stWidth.nCurValue * stHeight.nCurValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
bitmapPixelFormat = PixelFormat.Format24bppRgb;
|
||||
m_nConvertDstBufLen = (uint)(stWidth.nCurValue * stHeight.nCurValue * 3);
|
||||
}
|
||||
|
||||
// 分配转换后的图像数据缓存
|
||||
if (m_ConvertDstBuf != IntPtr.Zero) Marshal.FreeHGlobal(m_ConvertDstBuf);
|
||||
m_ConvertDstBuf = Marshal.AllocHGlobal((int)m_nConvertDstBufLen);
|
||||
|
||||
// 创建Bitmap对象
|
||||
if (m_bitmap != null) m_bitmap.Dispose();
|
||||
m_bitmap = new Bitmap((int)stWidth.nCurValue, (int)stHeight.nCurValue, bitmapPixelFormat);
|
||||
|
||||
// 如果是Mono8格式,设置调色板
|
||||
if (isMono)
|
||||
{
|
||||
ColorPalette palette = m_bitmap.Palette;
|
||||
for (int i = 0; i < 256; i++) palette.Entries[i] = Color.FromArgb(i, i, i);
|
||||
m_bitmap.Palette = palette;
|
||||
}
|
||||
|
||||
return MyCamera.MV_OK;
|
||||
}
|
||||
|
||||
private bool IsMono(uint enPixelType)
|
||||
{
|
||||
switch (enPixelType)
|
||||
{
|
||||
case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8:
|
||||
case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono10:
|
||||
case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono10_Packed:
|
||||
case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono12:
|
||||
case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono12_Packed:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发相机消息事件
|
||||
/// </summary>
|
||||
private void OnCameraMessage(string message, int errorCode = 0)
|
||||
{
|
||||
CameraMessage?.Invoke(this, message, errorCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Close();
|
||||
if (m_ConvertDstBuf != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(m_ConvertDstBuf);
|
||||
m_ConvertDstBuf = IntPtr.Zero;
|
||||
}
|
||||
if (m_bitmap != null)
|
||||
{
|
||||
m_bitmap.Dispose();
|
||||
m_bitmap = null;
|
||||
}
|
||||
MyCamera.MV_CC_Finalize_NET();
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Check.Main/Check.Main.csproj
Normal file
37
Check.Main/Check.Main.csproj
Normal file
@@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DockPanelSuite" Version="3.1.1" />
|
||||
<PackageReference Include="DockPanelSuite.ThemeVS2015" Version="3.1.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NPOI" Version="2.7.4" />
|
||||
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" />
|
||||
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.10.0.20241108" />
|
||||
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.10.0.20241108" />
|
||||
<PackageReference Include="SkiaSharp" Version="3.116.1" />
|
||||
<PackageReference Include="SunnyUI" Version="3.8.7" />
|
||||
<PackageReference Include="YoloDotNet" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="HslCommunication">
|
||||
<HintPath>..\..\..\HslCommunication-master\HslCommunication.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MvCameraControl.Net">
|
||||
<HintPath>C:\Program Files (x86)\MVS\Development\DotNet\win64\MvCameraControl.Net.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
236
Check.Main/Common/EasyE5Options.cs
Normal file
236
Check.Main/Common/EasyE5Options.cs
Normal file
@@ -0,0 +1,236 @@
|
||||
using NPOI.SS.Formula.Functions;
|
||||
using Sunny.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Check.Main.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 汇川 Easy 系列 PLC 客户端 (Modbus TCP)
|
||||
/// 支持 D/M/X/Y 地址读写,整数、浮点数、字符串
|
||||
/// </summary>
|
||||
public class EasyPlcClient : IDisposable
|
||||
{
|
||||
private readonly TcpClient _tcpClient = new();
|
||||
private NetworkStream _stream;
|
||||
private ushort _transactionId = 0;
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
|
||||
private readonly string _ip;
|
||||
private readonly int _port;
|
||||
private readonly byte _slaveId;
|
||||
|
||||
public EasyPlcClient(string ip, int port = 502, byte slaveId = 1)
|
||||
{
|
||||
_ip = ip;
|
||||
_port = port;
|
||||
_slaveId = slaveId;
|
||||
}
|
||||
|
||||
public async Task ConnectAsync()
|
||||
{
|
||||
await _tcpClient.ConnectAsync(_ip, _port);
|
||||
_stream = _tcpClient.GetStream();
|
||||
}
|
||||
|
||||
#region ---- 基础 Modbus ----
|
||||
private ushort GetTransactionId() =>
|
||||
unchecked((ushort)Interlocked.Increment(ref Unsafe.As<ushort, int>(ref _transactionId)));
|
||||
|
||||
private async Task<byte[]> SendAndReceiveAsync(byte[] pdu, ushort startAddr, ushort length = 1)
|
||||
{
|
||||
ushort tid = GetTransactionId();
|
||||
ushort lengthField = (ushort)(pdu.Length + 1);
|
||||
byte[] mbap = {
|
||||
(byte)(tid >> 8), (byte)tid,
|
||||
0x00, 0x00, // Protocol = 0
|
||||
(byte)(lengthField >> 8), (byte)lengthField,
|
||||
_slaveId
|
||||
};
|
||||
|
||||
byte[] adu = new byte[mbap.Length + pdu.Length];
|
||||
Buffer.BlockCopy(mbap, 0, adu, 0, mbap.Length);
|
||||
Buffer.BlockCopy(pdu, 0, adu, mbap.Length, pdu.Length);
|
||||
|
||||
await _lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await _stream.WriteAsync(adu, 0, adu.Length);
|
||||
|
||||
byte[] header = new byte[7];
|
||||
await _stream.ReadAsync(header, 0, 7);
|
||||
int respLength = (header[4] << 8) + header[5];
|
||||
byte[] resp = new byte[respLength - 1];
|
||||
await _stream.ReadAsync(resp, 0, resp.Length);
|
||||
|
||||
return resp;
|
||||
}
|
||||
finally { _lock.Release(); }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ---- 地址解析 ----
|
||||
private void ParseAddress(string address, out string area, out ushort offset)
|
||||
{
|
||||
var letters = Regex.Match(address, @"^[A-Za-z]+").Value.ToUpper();
|
||||
var digits = Regex.Match(address, @"\d+").Value;
|
||||
|
||||
if (string.IsNullOrEmpty(letters) || string.IsNullOrEmpty(digits))
|
||||
throw new ArgumentException($"地址 {address} 无效");
|
||||
|
||||
area = letters;
|
||||
offset = (ushort)(ushort.Parse(digits) - 1);
|
||||
|
||||
// 👉 注意:这里的偏移需根据你的 PLC Modbus 地址表调整
|
||||
// 例如 D 区可能是 40001 起,M 区可能是 00001 起
|
||||
// 目前假设 D -> Holding Register, M/Y -> Coil, X -> Discrete Input
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ---- 整数读写 ----
|
||||
public async Task WriteAsync(string address, int value)
|
||||
{
|
||||
ParseAddress(address, out string area, out ushort offset);
|
||||
|
||||
switch (area)
|
||||
{
|
||||
case "D": // 写寄存器
|
||||
byte[] pdu = {
|
||||
0x06,
|
||||
(byte)(offset >> 8), (byte)offset,
|
||||
(byte)(value >> 8), (byte)value
|
||||
};
|
||||
await SendAndReceiveAsync(pdu, offset);
|
||||
break;
|
||||
|
||||
case "M":
|
||||
case "Y": // 写单线圈
|
||||
byte[] coil = {
|
||||
0x05,
|
||||
(byte)(offset >> 8), (byte)offset,
|
||||
(byte)(value != 0 ? 0xFF : 0x00), 0x00
|
||||
};
|
||||
await SendAndReceiveAsync(coil, offset);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"写 {area} 区未支持");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> ReadAsync(string address)
|
||||
{
|
||||
ParseAddress(address, out string area, out ushort offset);
|
||||
|
||||
switch (area)
|
||||
{
|
||||
case "D": // Holding Register
|
||||
byte[] pdu = { 0x03, (byte)(offset >> 8), (byte)offset, 0x00, 0x01 };
|
||||
var resp = await SendAndReceiveAsync(pdu, offset);
|
||||
return (resp[1] << 8) + resp[2];
|
||||
|
||||
case "M":
|
||||
case "Y": // Coils
|
||||
byte[] pdu1 = { 0x01, (byte)(offset >> 8), (byte)offset, 0x00, 0x01 };
|
||||
var resp1 = await SendAndReceiveAsync(pdu1, offset);
|
||||
return (resp1[2] & 0x01) != 0 ? 1 : 0;
|
||||
|
||||
case "X": // Inputs
|
||||
byte[] pdu2 = { 0x02, (byte)(offset >> 8), (byte)offset, 0x00, 0x01 };
|
||||
var resp2 = await SendAndReceiveAsync(pdu2, offset);
|
||||
return (resp2[2] & 0x01) != 0 ? 1 : 0;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"读 {area} 区未支持");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ---- 浮点读写 (2寄存器) ----
|
||||
public async Task WriteFloatAsync(string address, float value)
|
||||
{
|
||||
ParseAddress(address, out string area, out ushort offset);
|
||||
if (area != "D") throw new NotSupportedException("浮点仅支持 D 区");
|
||||
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
|
||||
|
||||
byte[] pdu = {
|
||||
0x10,
|
||||
(byte)(offset >> 8), (byte)offset,
|
||||
0x00, 0x02,
|
||||
0x04,
|
||||
bytes[0], bytes[1], bytes[2], bytes[3]
|
||||
};
|
||||
await SendAndReceiveAsync(pdu, offset, 2);
|
||||
}
|
||||
|
||||
public async Task<float> ReadFloatAsync(string address)
|
||||
{
|
||||
ParseAddress(address, out string area, out ushort offset);
|
||||
if (area != "D") throw new NotSupportedException("浮点仅支持 D 区");
|
||||
|
||||
byte[] pdu = { 0x03, (byte)(offset >> 8), (byte)offset, 0x00, 0x02 };
|
||||
var resp = await SendAndReceiveAsync(pdu, offset, 2);
|
||||
|
||||
byte[] data = { resp[1], resp[2], resp[3], resp[4] };
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(data);
|
||||
|
||||
return BitConverter.ToSingle(data, 0);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ---- 字符串读写 ----
|
||||
public async Task WriteStringAsync(string address, string value, int maxLength)
|
||||
{
|
||||
ParseAddress(address, out string area, out ushort offset);
|
||||
if (area != "D") throw new NotSupportedException("字符串仅支持 D 区");
|
||||
|
||||
byte[] strBytes = Encoding.ASCII.GetBytes(value);
|
||||
if (strBytes.Length > maxLength) Array.Resize(ref strBytes, maxLength);
|
||||
|
||||
// 每寄存器2字节
|
||||
int regCount = (strBytes.Length + 1) / 2;
|
||||
byte[] data = new byte[regCount * 2];
|
||||
Array.Copy(strBytes, data, strBytes.Length);
|
||||
|
||||
byte[] pdu = new byte[7 + data.Length];
|
||||
pdu[0] = 0x10;
|
||||
pdu[1] = (byte)(offset >> 8); pdu[2] = (byte)offset;
|
||||
pdu[3] = (byte)(regCount >> 8); pdu[4] = (byte)regCount;
|
||||
pdu[5] = (byte)(data.Length);
|
||||
Buffer.BlockCopy(data, 0, pdu, 6, data.Length);
|
||||
|
||||
await SendAndReceiveAsync(pdu, offset, (ushort)regCount);
|
||||
}
|
||||
|
||||
public async Task<string> ReadStringAsync(string address, int length)
|
||||
{
|
||||
ParseAddress(address, out string area, out ushort offset);
|
||||
if (area != "D") throw new NotSupportedException("字符串仅支持 D 区");
|
||||
|
||||
int regCount = (length + 1) / 2;
|
||||
byte[] pdu = { 0x03, (byte)(offset >> 8), (byte)offset, (byte)(regCount >> 8), (byte)regCount };
|
||||
var resp = await SendAndReceiveAsync(pdu, offset, (ushort)regCount);
|
||||
|
||||
byte[] data = new byte[resp[0]];
|
||||
Array.Copy(resp, 1, data, 0, data.Length);
|
||||
|
||||
return Encoding.ASCII.GetString(data).TrimEnd('\0');
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_tcpClient?.Close();
|
||||
_lock?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
{
|
||||
// 目前只持有结果字符串,无需释放资源
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 SKImage ResultImage { get; } // 【修改】携带已绘制好结果的SKImage
|
||||
|
||||
public ProcessingCompletedEventArgs(int cameraIndex, long productId, 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
243
Check.Main/Dispatch/ProductManager.cs
Normal file
243
Check.Main/Dispatch/ProductManager.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using Check.Main.Common;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace Check.Main.Dispatch
|
||||
{
|
||||
public static class ProductManager
|
||||
{
|
||||
private static readonly string _productRootPath = Path.Combine(Application.StartupPath, "Product");
|
||||
private static readonly string _productListFilePath = Path.Combine(Application.StartupPath, "products.json");
|
||||
|
||||
// --- 公共属性和事件 ---
|
||||
public static List<string> ProductList { get; private set; } = new List<string>();
|
||||
public static string CurrentProductName { get; private set; }
|
||||
public static ProcessConfig CurrentConfig { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当产品切换或配置加载后触发。
|
||||
/// </summary>
|
||||
public static event Action OnProductChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 静态构造函数,在程序启动时自动执行。
|
||||
/// </summary>
|
||||
static ProductManager()
|
||||
{
|
||||
Directory.CreateDirectory(_productRootPath); // 确保Product文件夹存在
|
||||
LoadProductList();
|
||||
// 默认加载列表中的第一个产品,或者创建一个默认的
|
||||
SwitchToProduct(ProductList.FirstOrDefault() ?? "DefaultProduct");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到指定的产品,并加载其配置。
|
||||
/// </summary>
|
||||
public static bool SwitchToProduct(string productName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(productName)) return false;
|
||||
|
||||
ThreadSafeLogger.Log($"正在切换到产品: {productName}");
|
||||
var config = LoadConfigForProduct(productName);
|
||||
if (config == null)
|
||||
{
|
||||
// 如果加载失败(例如新创建的产品还没有配置文件),则创建一个新的
|
||||
config = new ProcessConfig();
|
||||
}
|
||||
|
||||
CurrentProductName = productName;
|
||||
CurrentConfig = config;
|
||||
|
||||
// 广播产品已变更的通知
|
||||
OnProductChanged?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加一个新产品。
|
||||
/// </summary>
|
||||
public static bool AddNewProduct(string newProductName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newProductName) || ProductList.Contains(newProductName))
|
||||
{
|
||||
return false; // 名称无效或已存在
|
||||
}
|
||||
|
||||
// 创建一个全新的、空的配置
|
||||
var newConfig = new ProcessConfig();
|
||||
|
||||
// 保存这个新产品的空配置,这也会创建文件夹
|
||||
if (SaveConfigForProduct(newProductName, newConfig))
|
||||
{
|
||||
ProductList.Add(newProductName);
|
||||
SaveProductList(); // 更新产品列表文件
|
||||
SwitchToProduct(newProductName); // 创建后立即切换到新产品
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除指定的产品及其所有相关配置。
|
||||
/// </summary>
|
||||
/// <param name="productNameToDelete">要删除的产品名称。</param>
|
||||
/// <returns>成功删除返回 true,否则返回 false。</returns>
|
||||
public static bool DeleteProduct(string productNameToDelete)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(productNameToDelete) || !ProductList.Contains(productNameToDelete))
|
||||
{
|
||||
ThreadSafeLogger.Log($"[警告] 尝试删除一个不存在的产品: {productNameToDelete}");
|
||||
return false; // 产品不存在
|
||||
}
|
||||
|
||||
ThreadSafeLogger.Log($"正在删除产品: {productNameToDelete}...");
|
||||
try
|
||||
{
|
||||
// 1. 从产品列表中移除
|
||||
ProductList.Remove(productNameToDelete);
|
||||
SaveProductList(); // 更新列表文件
|
||||
|
||||
// 2. 删除对应的配置文件夹
|
||||
var safeFolderName = GetSafeFolderName(productNameToDelete);
|
||||
var productDir = Path.Combine(_productRootPath, safeFolderName);
|
||||
if (Directory.Exists(productDir))
|
||||
{
|
||||
Directory.Delete(productDir, true); // true 表示递归删除所有子文件和子文件夹
|
||||
}
|
||||
|
||||
ThreadSafeLogger.Log($"产品 '{productNameToDelete}' 已成功删除。");
|
||||
|
||||
// 3. 如果被删除的是当前活动产品,则需要切换到一个新的有效产品
|
||||
if (CurrentProductName == productNameToDelete)
|
||||
{
|
||||
// 切换到列表中的第一个产品,或者如果列表为空,则创建一个默认产品
|
||||
string nextProduct = ProductList.FirstOrDefault() ?? "DefaultProduct";
|
||||
SwitchToProduct(nextProduct);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果删除的不是当前产品,我们仍然需要触发一次事件,
|
||||
// 以便UI(主要是下拉列表)能够刷新它的数据源。
|
||||
OnProductChanged?.Invoke();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[错误] 删除产品 '{productNameToDelete}' 失败: {ex.Message}");
|
||||
// 如果出错,最好把刚删除的项加回来,保持状态一致性
|
||||
if (!ProductList.Contains(productNameToDelete))
|
||||
{
|
||||
ProductList.Add(productNameToDelete);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为当前产品保存更改。
|
||||
/// </summary>
|
||||
public static void SaveCurrentProductConfig()
|
||||
{
|
||||
if (CurrentConfig != null && !string.IsNullOrWhiteSpace(CurrentProductName))
|
||||
{
|
||||
SaveConfigForProduct(CurrentProductName, CurrentConfig);
|
||||
}
|
||||
}
|
||||
|
||||
#region 文件操作辅助方法
|
||||
|
||||
private static void LoadProductList()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_productListFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(_productListFilePath);
|
||||
ProductList = JsonConvert.DeserializeObject<List<string>>(json) ?? new List<string>();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[错误] 加载产品列表失败: {ex.Message}");
|
||||
ProductList = new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveProductList()
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(ProductList, Newtonsoft.Json.Formatting.Indented);
|
||||
File.WriteAllText(_productListFilePath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[错误] 保存产品列表失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static ProcessConfig LoadConfigForProduct(string productName)
|
||||
{
|
||||
var safeFolderName = GetSafeFolderName(productName);
|
||||
var configPath = Path.Combine(_productRootPath, safeFolderName, "config.xml");
|
||||
|
||||
if (!File.Exists(configPath)) return null;
|
||||
|
||||
try
|
||||
{
|
||||
XmlSerializer serializer = new XmlSerializer(typeof(ProcessConfig));
|
||||
using (var fs = new FileStream(configPath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
return (ProcessConfig)serializer.Deserialize(fs);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[错误] 加载产品 '{productName}' 的配置失败: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool SaveConfigForProduct(string productName, ProcessConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
var safeFolderName = GetSafeFolderName(productName);
|
||||
var productDir = Path.Combine(_productRootPath, safeFolderName);
|
||||
Directory.CreateDirectory(productDir); // 确保文件夹存在
|
||||
|
||||
var configPath = Path.Combine(productDir, "config.xml");
|
||||
|
||||
XmlSerializer serializer = new XmlSerializer(typeof(ProcessConfig));
|
||||
using (var fs = new FileStream(configPath, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
serializer.Serialize(fs, config);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[错误] 保存产品 '{productName}' 的配置失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetSafeFolderName(string productName)
|
||||
{
|
||||
// 移除所有不适合做文件夹名称的字符
|
||||
string invalidChars = new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars());
|
||||
Regex r = new Regex(string.Format("[{0}]", Regex.Escape(invalidChars)));
|
||||
return r.Replace(productName, "");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
180
Check.Main/FrmMain.Designer.cs
generated
Normal file
180
Check.Main/FrmMain.Designer.cs
generated
Normal file
@@ -0,0 +1,180 @@
|
||||
namespace Check.Main
|
||||
{
|
||||
partial class FrmMain
|
||||
{
|
||||
/// <summary>
|
||||
/// 必需的设计器变量。
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// 清理所有正在使用的资源。
|
||||
/// </summary>
|
||||
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows 窗体设计器生成的代码
|
||||
|
||||
/// <summary>
|
||||
/// 设计器支持所需的方法 - 不要修改
|
||||
/// 使用代码编辑器修改此方法的内容。
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FrmMain));
|
||||
menuStrip1 = new MenuStrip();
|
||||
文件ToolStripMenuItem = new ToolStripMenuItem();
|
||||
退出ToolStripMenuItem = new ToolStripMenuItem();
|
||||
ToolStripMenuSaveLayou = new ToolStripMenuItem();
|
||||
视图ToolStripMenuItem = new ToolStripMenuItem();
|
||||
配置ToolStripMenuItem = new ToolStripMenuItem();
|
||||
日志ToolStripMenuItem = new ToolStripMenuItem();
|
||||
控制面板ToolStripMenuItem = new ToolStripMenuItem();
|
||||
statusStrip1 = new StatusStrip();
|
||||
uiTableLayoutPanel1 = new Sunny.UI.UITableLayoutPanel();
|
||||
dockPanel1 = new WeifenLuo.WinFormsUI.Docking.DockPanel();
|
||||
menuStrip1.SuspendLayout();
|
||||
uiTableLayoutPanel1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// menuStrip1
|
||||
//
|
||||
menuStrip1.Items.AddRange(new ToolStripItem[] { 文件ToolStripMenuItem, 视图ToolStripMenuItem, 控制面板ToolStripMenuItem });
|
||||
menuStrip1.Location = new Point(0, 0);
|
||||
menuStrip1.Name = "menuStrip1";
|
||||
menuStrip1.Padding = new Padding(7, 3, 0, 3);
|
||||
menuStrip1.Size = new Size(933, 27);
|
||||
menuStrip1.TabIndex = 0;
|
||||
menuStrip1.Text = "MainMenu";
|
||||
//
|
||||
// 文件ToolStripMenuItem
|
||||
//
|
||||
文件ToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { 退出ToolStripMenuItem, ToolStripMenuSaveLayou });
|
||||
文件ToolStripMenuItem.Name = "文件ToolStripMenuItem";
|
||||
文件ToolStripMenuItem.Size = new Size(44, 21);
|
||||
文件ToolStripMenuItem.Text = "文件";
|
||||
//
|
||||
// 退出ToolStripMenuItem
|
||||
//
|
||||
退出ToolStripMenuItem.Name = "退出ToolStripMenuItem";
|
||||
退出ToolStripMenuItem.Size = new Size(124, 22);
|
||||
退出ToolStripMenuItem.Text = "退出";
|
||||
//
|
||||
// ToolStripMenuSaveLayou
|
||||
//
|
||||
ToolStripMenuSaveLayou.Name = "ToolStripMenuSaveLayou";
|
||||
ToolStripMenuSaveLayou.Size = new Size(124, 22);
|
||||
ToolStripMenuSaveLayou.Text = "保存布局";
|
||||
ToolStripMenuSaveLayou.Click += ToolStripMenuSaveLayou_Click;
|
||||
//
|
||||
// 视图ToolStripMenuItem
|
||||
//
|
||||
视图ToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { 配置ToolStripMenuItem, 日志ToolStripMenuItem });
|
||||
视图ToolStripMenuItem.Name = "视图ToolStripMenuItem";
|
||||
视图ToolStripMenuItem.Size = new Size(44, 21);
|
||||
视图ToolStripMenuItem.Text = "视图";
|
||||
//
|
||||
// 配置ToolStripMenuItem
|
||||
//
|
||||
配置ToolStripMenuItem.Name = "配置ToolStripMenuItem";
|
||||
配置ToolStripMenuItem.Size = new Size(180, 22);
|
||||
配置ToolStripMenuItem.Text = "配置";
|
||||
配置ToolStripMenuItem.Click += 配置ToolStripMenuItem_Click;
|
||||
//
|
||||
// 日志ToolStripMenuItem
|
||||
//
|
||||
日志ToolStripMenuItem.Name = "日志ToolStripMenuItem";
|
||||
日志ToolStripMenuItem.Size = new Size(180, 22);
|
||||
日志ToolStripMenuItem.Text = "日志";
|
||||
日志ToolStripMenuItem.Click += 日志ToolStripMenuItem_Click;
|
||||
//
|
||||
// 控制面板ToolStripMenuItem
|
||||
//
|
||||
控制面板ToolStripMenuItem.Name = "控制面板ToolStripMenuItem";
|
||||
控制面板ToolStripMenuItem.Size = new Size(68, 21);
|
||||
控制面板ToolStripMenuItem.Text = "控制面板";
|
||||
控制面板ToolStripMenuItem.Click += 控制面板ToolStripMenuItem_Click;
|
||||
//
|
||||
// statusStrip1
|
||||
//
|
||||
statusStrip1.Location = new Point(0, 616);
|
||||
statusStrip1.Name = "statusStrip1";
|
||||
statusStrip1.Padding = new Padding(1, 0, 16, 0);
|
||||
statusStrip1.Size = new Size(933, 22);
|
||||
statusStrip1.TabIndex = 2;
|
||||
statusStrip1.Text = "statusStrip1";
|
||||
//
|
||||
// uiTableLayoutPanel1
|
||||
//
|
||||
uiTableLayoutPanel1.ColumnCount = 2;
|
||||
uiTableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 76F));
|
||||
uiTableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 24F));
|
||||
uiTableLayoutPanel1.Controls.Add(dockPanel1, 0, 0);
|
||||
uiTableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
uiTableLayoutPanel1.Location = new Point(0, 27);
|
||||
uiTableLayoutPanel1.Margin = new Padding(4);
|
||||
uiTableLayoutPanel1.Name = "uiTableLayoutPanel1";
|
||||
uiTableLayoutPanel1.RowCount = 1;
|
||||
uiTableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
|
||||
uiTableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
|
||||
uiTableLayoutPanel1.Size = new Size(933, 589);
|
||||
uiTableLayoutPanel1.TabIndex = 3;
|
||||
uiTableLayoutPanel1.TagString = null;
|
||||
//
|
||||
// dockPanel1
|
||||
//
|
||||
uiTableLayoutPanel1.SetColumnSpan(dockPanel1, 2);
|
||||
dockPanel1.Dock = DockStyle.Fill;
|
||||
dockPanel1.Location = new Point(4, 4);
|
||||
dockPanel1.Margin = new Padding(4);
|
||||
dockPanel1.Name = "dockPanel1";
|
||||
dockPanel1.Size = new Size(925, 581);
|
||||
dockPanel1.TabIndex = 0;
|
||||
//
|
||||
// FrmMain
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(7F, 17F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(933, 638);
|
||||
Controls.Add(uiTableLayoutPanel1);
|
||||
Controls.Add(statusStrip1);
|
||||
Controls.Add(menuStrip1);
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
MainMenuStrip = menuStrip1;
|
||||
Margin = new Padding(4);
|
||||
Name = "FrmMain";
|
||||
Text = "星科瑞升视觉检测设备";
|
||||
WindowState = FormWindowState.Maximized;
|
||||
FormClosing += FrmMain_FormClosing;
|
||||
Load += FrmMain_Load;
|
||||
menuStrip1.ResumeLayout(false);
|
||||
menuStrip1.PerformLayout();
|
||||
uiTableLayoutPanel1.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.MenuStrip menuStrip1;
|
||||
private System.Windows.Forms.StatusStrip statusStrip1;
|
||||
private System.Windows.Forms.ToolStripMenuItem 文件ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 退出ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 视图ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 配置ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 日志ToolStripMenuItem;
|
||||
private Sunny.UI.UITableLayoutPanel uiTableLayoutPanel1;
|
||||
private WeifenLuo.WinFormsUI.Docking.DockPanel dockPanel1;
|
||||
private System.Windows.Forms.ToolStripMenuItem 控制面板ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem ToolStripMenuSaveLayou;
|
||||
}
|
||||
}
|
||||
|
||||
297
Check.Main/FrmMain.cs
Normal file
297
Check.Main/FrmMain.cs
Normal file
@@ -0,0 +1,297 @@
|
||||
using Check.Main.Camera;
|
||||
using Check.Main.Common;
|
||||
using Check.Main.UI;
|
||||
using HslCommunication;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using WeifenLuo.WinFormsUI.Docking;
|
||||
|
||||
namespace Check.Main
|
||||
{
|
||||
public partial class FrmMain : Form
|
||||
{
|
||||
// private FrmCamConfig _formCameraConfig;
|
||||
private FrmConfig _frmConfig;
|
||||
private FrmLog _formLog;
|
||||
private ThemeBase _theme = new VS2015BlueTheme(); // 外观主题
|
||||
public DockPanel MainDockPanel => this.dockPanel1;
|
||||
|
||||
private FormControlPanel _formControlPanel;
|
||||
|
||||
private FormStatistics _formStatistics;
|
||||
|
||||
private readonly string _layoutConfigFile = Path.Combine(Application.StartupPath, "layout.xml");
|
||||
//用于反序列化时创建窗体的委托
|
||||
private DeserializeDockContent _deserializeDockContent;
|
||||
|
||||
public FrmMain()
|
||||
{
|
||||
InitializeComponent();
|
||||
dockPanel1.Theme = _theme;
|
||||
IsMdiContainer = true;
|
||||
_deserializeDockContent = new DeserializeDockContent(GetContentFromPersistString);
|
||||
}
|
||||
|
||||
private void FrmMain_Load(object sender, EventArgs e)
|
||||
{
|
||||
EasyPlcClient easyPlcClient = new EasyPlcClient("127.0.0.1", 502, 1);
|
||||
easyPlcClient.ConnectAsync();
|
||||
|
||||
_frmConfig = new FrmConfig { Text = "主程序配置" };
|
||||
_formLog = new FrmLog { Text = "运行日志" };
|
||||
_formStatistics = new FormStatistics { Text = "生产统计" };
|
||||
_formControlPanel = new FormControlPanel { Text = "控制面板" };
|
||||
// 为每个子窗体订阅 FormClosing 事件
|
||||
_frmConfig.FormClosing += DockContent_FormClosing;
|
||||
_formLog.FormClosing += DockContent_FormClosing;
|
||||
_formStatistics.FormClosing += DockContent_FormClosing;
|
||||
_formControlPanel.FormClosing += DockContent_FormClosing;
|
||||
|
||||
ThreadSafeLogger.Initialize();
|
||||
ThreadSafeLogger.OnLogMessage += (msg) => { _formLog.AddLog(msg); };
|
||||
|
||||
// 2. 尝试加载布局文件
|
||||
if (File.Exists(_layoutConfigFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用委托加载布局
|
||||
dockPanel1.LoadFromXml(_layoutConfigFile, _deserializeDockContent);
|
||||
ThreadSafeLogger.Log("成功加载用户布局。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log($"加载布局失败: {ex.Message}。将使用默认布局。");
|
||||
// 如果加载失败,则使用默认布局
|
||||
ShowDefaultLayout();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 3. 如果布局文件不存在,则显示默认布局
|
||||
ThreadSafeLogger.Log("未找到布局文件,使用默认布局。");
|
||||
ShowDefaultLayout();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 将相机名称添加到状态栏
|
||||
/// </summary>
|
||||
public void AddCameraToStatusStrip(string name)
|
||||
{
|
||||
if (statusStrip1.InvokeRequired)
|
||||
{
|
||||
statusStrip1.Invoke(new Action(() => AddCameraToStatusStrip(name)));
|
||||
return;
|
||||
}
|
||||
var label = new ToolStripStatusLabel(name)
|
||||
{
|
||||
Name = "status_" + name,
|
||||
BorderSides = ToolStripStatusLabelBorderSides.All,
|
||||
Spring = false,
|
||||
};
|
||||
label.Click += StatusLabel_Click;
|
||||
statusStrip1.Items.Add(label);
|
||||
}
|
||||
private void 控制面板ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (_formControlPanel == null || _formControlPanel.IsDisposed)
|
||||
{
|
||||
_formControlPanel = new FormControlPanel { Text = "控制面板" };
|
||||
_formControlPanel.FormClosing += DockContent_FormClosing;
|
||||
}
|
||||
_formControlPanel.Show(this.dockPanel1);
|
||||
}
|
||||
/// <summary>
|
||||
/// 清空状态栏中的相机名称
|
||||
/// </summary>
|
||||
public void ClearStatusStrip()
|
||||
{
|
||||
if (statusStrip1.InvokeRequired)
|
||||
{
|
||||
statusStrip1.Invoke(new Action(ClearStatusStrip));
|
||||
return;
|
||||
}
|
||||
// 从后往前删,避免索引问题
|
||||
for (int i = statusStrip1.Items.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (statusStrip1.Items[i] is ToolStripStatusLabel && statusStrip1.Items[i].Name.StartsWith("status_"))
|
||||
{
|
||||
statusStrip1.Items.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StatusLabel_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is ToolStripStatusLabel label)
|
||||
{
|
||||
string cameraName = label.Text;
|
||||
ThreadSafeLogger.Log($"用户点击状态栏标签: {cameraName}");
|
||||
|
||||
bool foundOriginal = false;
|
||||
bool foundResult = false;
|
||||
|
||||
// 1. 尝试重新显示“原图”窗口
|
||||
if (CameraManager.OriginalImageDisplays.TryGetValue(cameraName, out var originalDisplayForm))
|
||||
{
|
||||
// 使用 Show() 方法来重新激活或显示隐藏的窗口
|
||||
// 传入 DockPanel 确保它知道在哪里显示
|
||||
originalDisplayForm.Show(this.dockPanel1);
|
||||
originalDisplayForm.Activate(); // 调用 Activate() 确保它成为当前焦点窗口
|
||||
foundOriginal = true;
|
||||
}
|
||||
|
||||
// 2. 尝试重新显示“结果”窗口
|
||||
if (CameraManager.ResultImageDisplays.TryGetValue(cameraName, out var resultDisplayForm))
|
||||
{
|
||||
resultDisplayForm.Show(this.dockPanel1);
|
||||
resultDisplayForm.Activate();
|
||||
foundResult = true;
|
||||
}
|
||||
|
||||
// 3. 提供反馈
|
||||
if (foundOriginal || foundResult)
|
||||
{
|
||||
ThreadSafeLogger.Log($"已重新激活相机 '{cameraName}' 的显示窗口。");
|
||||
}
|
||||
else
|
||||
{
|
||||
ThreadSafeLogger.Log($"[警告] 未能找到相机 '{cameraName}' 对应的活动显示窗口。可能设备已关闭。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据持久化字符串创建或返回对应的窗体实例。
|
||||
/// 这是DockPanel Suite反序列化布局时需要的回调方法。
|
||||
/// </summary>
|
||||
/// <param name="persistString">在XML中代表一个窗体的唯一字符串(通常是其类型名)。</param>
|
||||
/// <returns>对应的窗体实例。</returns>
|
||||
private IDockContent GetContentFromPersistString(string persistString)
|
||||
{
|
||||
if (persistString == typeof(FormControlPanel).ToString())
|
||||
return _formControlPanel;
|
||||
if (persistString == typeof(FrmConfig).ToString())
|
||||
return _frmConfig;
|
||||
if (persistString == typeof(FrmLog).ToString())
|
||||
return _formLog;
|
||||
if (persistString == typeof(FormStatistics).ToString())
|
||||
return _formStatistics;
|
||||
|
||||
// 对于图像显示窗口,由于它们是动态创建的,情况会更复杂。
|
||||
// 在当前设计中,我们不保存图像窗口的布局,它们会在应用相机配置时重新创建。
|
||||
// 如果需要保存它们,需要在CameraManager中管理它们的持久化字符串。
|
||||
// 目前的设计下,返回null是安全的。
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示程序的默认窗口布局。
|
||||
/// </summary>
|
||||
private void ShowDefaultLayout()
|
||||
{
|
||||
// 确保所有窗体都未被意外关闭
|
||||
if (_formControlPanel.IsDisposed) _formControlPanel = new FormControlPanel();
|
||||
if (_frmConfig.IsDisposed) _frmConfig = new FrmConfig();
|
||||
if (_formLog.IsDisposed) _formLog = new FrmLog();
|
||||
if (_formStatistics.IsDisposed) _formStatistics = new FormStatistics();
|
||||
|
||||
// 显示默认窗口
|
||||
_frmConfig.Show(dockPanel1, DockState.DockLeft);
|
||||
_formLog.Show(dockPanel1, DockState.DockBottom);
|
||||
_formControlPanel.Show(dockPanel1, DockState.DockRight);
|
||||
_formStatistics.Show(dockPanel1, DockState.DockTop);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【新增】保存当前窗口布局到XML文件。
|
||||
/// </summary>
|
||||
private void SaveLayout()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 将当前DockPanel的布局保存到指定文件
|
||||
dockPanel1.SaveAsXml(_layoutConfigFile);
|
||||
ThreadSafeLogger.Log("布局已成功保存!");
|
||||
//MessageBox.Show("布局已成功保存!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log("保存布局失败: " + ex.Message);
|
||||
//MessageBox.Show("保存布局失败: " + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
private void FrmMain_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
if (_formStatistics != null && !_formStatistics.IsDisposed)
|
||||
{
|
||||
StatisticsExporter.ExportToExcel(_formStatistics.CurrentStatistics, "Shutdown");
|
||||
}
|
||||
|
||||
CameraManager.Shutdown();
|
||||
ThreadSafeLogger.Shutdown();
|
||||
}
|
||||
|
||||
private void ToolStripMenuSaveLayou_Click(object sender, EventArgs e)
|
||||
{
|
||||
SaveLayout();
|
||||
}
|
||||
|
||||
private void 配置ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
//_frmConfig.Show();
|
||||
// 如果窗体因意外被销毁,则重新创建它
|
||||
if (_frmConfig == null || _frmConfig.IsDisposed)
|
||||
{
|
||||
_frmConfig = new FrmConfig() { Text = "主程序配置" };
|
||||
_frmConfig.FormClosing += DockContent_FormClosing;
|
||||
}
|
||||
// 调用 Show() 会自动处理隐藏和显示逻辑
|
||||
_frmConfig.Show(this.dockPanel1);
|
||||
}
|
||||
private void 日志ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (_formLog == null || _formLog.IsDisposed)
|
||||
{
|
||||
_formLog = new FrmLog { Text = "运行日志" };
|
||||
_formLog.FormClosing += DockContent_FormClosing;
|
||||
}
|
||||
_formLog.Show(this.dockPanel1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理所有可停靠子窗体关闭事件的通用方法。
|
||||
/// 通过取消关闭并隐藏窗口,来实现“假关闭”,以便后续能重新显示。
|
||||
/// </summary>
|
||||
private void DockContent_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
// 检查关闭原因是否为用户点击了关闭按钮
|
||||
if (e.CloseReason == CloseReason.UserClosing)
|
||||
{
|
||||
// 1. 取消真正的关闭(Dispose)操作,防止窗体被销毁
|
||||
e.Cancel = true;
|
||||
|
||||
// 2. 将窗口隐藏起来
|
||||
// 我们需要将 sender 转换为 DockContent 类型来访问 Hide() 方法
|
||||
if (sender is DockContent dockContent)
|
||||
{
|
||||
if (dockContent is FormImageDisplay imageDisplay)
|
||||
{
|
||||
ThreadSafeLogger.Log($"用户关闭了窗口 '{imageDisplay.Text}',已将其隐藏。");
|
||||
}
|
||||
dockContent.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
3244
Check.Main/FrmMain.resx
Normal file
3244
Check.Main/FrmMain.resx
Normal file
File diff suppressed because it is too large
Load Diff
149
Check.Main/Infer/DetectionCoordinator.cs
Normal file
149
Check.Main/Infer/DetectionCoordinator.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using Check.Main.Camera;
|
||||
using Check.Main.Common;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Check.Main.Infer
|
||||
{
|
||||
public static class DetectionCoordinator
|
||||
{
|
||||
/// <summary>
|
||||
/// 定义存储所有相机处理器的字典
|
||||
/// 键是相机的唯一编号 (CameraIndex),值是对应的处理器实例。
|
||||
/// </summary>
|
||||
private static ConcurrentDictionary<int, CameraProcessor> _processors = new ConcurrentDictionary<int, CameraProcessor>();
|
||||
/// <summary>
|
||||
/// 用于在产品组装时进行同步,确保线程安全
|
||||
/// </summary>
|
||||
private static ConcurrentDictionary<long, ProductAssembly> _productAssemblies = new ConcurrentDictionary<long, ProductAssembly>();
|
||||
/// <summary>
|
||||
/// 可用的相机数量
|
||||
/// </summary>
|
||||
private static int _enabledCameraCount = 0;
|
||||
|
||||
public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted;
|
||||
public static bool IsDetectionRunning { get; private set; } = false;
|
||||
|
||||
// OnDetectionCompleted 事件现在也属于这里
|
||||
//public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted;
|
||||
|
||||
public static void StartDetection()
|
||||
{
|
||||
if (!IsDetectionRunning)
|
||||
{
|
||||
IsDetectionRunning = true;
|
||||
ThreadSafeLogger.Log("检测统计已启动。");
|
||||
}
|
||||
}
|
||||
|
||||
public static void StopDetection()
|
||||
{
|
||||
if (IsDetectionRunning)
|
||||
{
|
||||
IsDetectionRunning = false;
|
||||
ThreadSafeLogger.Log("检测统计已停止。");
|
||||
}
|
||||
}
|
||||
public static void Initialize(List<CameraSettings> cameraSettings, List<ModelSettings> modelSettings)
|
||||
{
|
||||
Shutdown(); // 先关闭旧的
|
||||
var enabledCameras = cameraSettings.Where(c => c.IsEnabled).ToList();
|
||||
_enabledCameraCount = enabledCameras.Count;
|
||||
if (_enabledCameraCount == 0) return;
|
||||
|
||||
foreach (var camSetting in enabledCameras)
|
||||
{
|
||||
// 找到与相机编号匹配的模型
|
||||
var model = modelSettings.FirstOrDefault(m => m.Id == camSetting.ModelID);
|
||||
if (model == null)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[警告] 找不到与相机 #{camSetting.CameraIndex} 匹配的模型,该相机将无法处理图像。");
|
||||
continue;
|
||||
}
|
||||
|
||||
var processor = new CameraProcessor(camSetting.CameraIndex,camSetting.ModelID);
|
||||
_processors.TryAdd(camSetting.CameraIndex, processor);
|
||||
processor.Start();
|
||||
}
|
||||
ThreadSafeLogger.Log($"检测协调器已初始化,启动了 {_processors.Count} 个相机处理线程。");
|
||||
}
|
||||
|
||||
public static void EnqueueImage(int cameraIndex, Bitmap bmp)
|
||||
{
|
||||
if (_processors.TryGetValue(cameraIndex, out var processor))
|
||||
{
|
||||
processor.EnqueueImage(bmp);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果找不到处理器,必须释放Bitmap防止泄漏
|
||||
bmp?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// 供 CameraProcessor 回调,用以组装产品
|
||||
public static void AssembleProduct(ImageData data, string result)
|
||||
{
|
||||
var assembly = _productAssemblies.GetOrAdd(data.ProductId, (id) => new ProductAssembly(id, _enabledCameraCount));
|
||||
|
||||
if (assembly.AddResult(data.CameraIndex, result))
|
||||
{
|
||||
string finalResult = assembly.GetFinalResult();
|
||||
ThreadSafeLogger.Log($"产品 #{assembly.ProductId} 已检测完毕,最终结果: {finalResult}");
|
||||
|
||||
// 只有在检测运行时,才触发事件
|
||||
if (IsDetectionRunning)
|
||||
{
|
||||
OnDetectionCompleted?.Invoke(null, new DetectionResultEventArgs(finalResult == "OK"));
|
||||
}
|
||||
|
||||
if (_productAssemblies.TryRemove(assembly.ProductId, out var finishedAssembly))
|
||||
{
|
||||
finishedAssembly.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 命令所有活动的相机处理器重置它们的内部计数器。
|
||||
/// </summary>
|
||||
public static void ResetAllCounters()
|
||||
{
|
||||
foreach (var processor in _processors.Values)
|
||||
{
|
||||
processor.ResetCounter();
|
||||
}
|
||||
ThreadSafeLogger.Log("所有相机处理器的产品计数器已重置。");
|
||||
}
|
||||
|
||||
public static CameraProcessor GetProcessor(int cameraIndex)
|
||||
{
|
||||
_processors.TryGetValue(cameraIndex, out var p);
|
||||
return p;
|
||||
}
|
||||
public static IEnumerable<CameraProcessor> GetAllProcessors()
|
||||
{
|
||||
return _processors.Values;
|
||||
}
|
||||
|
||||
|
||||
public static void Shutdown()
|
||||
{
|
||||
foreach (var processor in _processors.Values)
|
||||
{
|
||||
processor.Dispose();
|
||||
}
|
||||
_processors.Clear();
|
||||
|
||||
foreach (var assembly in _productAssemblies.Values)
|
||||
{
|
||||
assembly.Dispose();
|
||||
}
|
||||
_productAssemblies.Clear();
|
||||
ThreadSafeLogger.Log("检测协调器已关闭。");
|
||||
}
|
||||
}
|
||||
}
|
||||
131
Check.Main/Infer/ModelSettings.cs
Normal file
131
Check.Main/Infer/ModelSettings.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using Check.Main.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Check.Main.Infer
|
||||
{
|
||||
public enum DetectDevice
|
||||
{
|
||||
[Description("CPU")]
|
||||
CPU = 0,
|
||||
|
||||
[Description("GPU")]
|
||||
GPU,
|
||||
|
||||
[Description("VPU")]
|
||||
VPU,
|
||||
}
|
||||
public enum ModelVersion
|
||||
{
|
||||
[Description("v8")]
|
||||
V8 = 0,
|
||||
|
||||
[Description("v11")]
|
||||
V11,
|
||||
}
|
||||
public enum CheckModelType
|
||||
{
|
||||
[Description("分类")]
|
||||
Classification,
|
||||
|
||||
[Description("检测")]
|
||||
ObjectDetection,
|
||||
|
||||
[Description("OBB")]
|
||||
ObbDetection,
|
||||
|
||||
[Description("分割")]
|
||||
Segmentation,
|
||||
|
||||
[Description("位姿")]
|
||||
PoseEstimation
|
||||
}
|
||||
|
||||
[Serializable] // 确保可被XML序列化
|
||||
public class ModelSettings : INotifyPropertyChanged, ICloneable
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
private int _id;
|
||||
private string _name = "New Model";
|
||||
private string _path = "";
|
||||
private DetectDevice _checkDevice=DetectDevice.CPU;
|
||||
private ModelVersion _mVersion=ModelVersion.V8;
|
||||
private CheckModelType _mType = CheckModelType.Classification;
|
||||
private bool _isEnabled = true;
|
||||
|
||||
[Category("基本信息"), DisplayName("模型编号"), Description("模型的唯一标识符,用于与相机编号对应。")]
|
||||
public int Id
|
||||
{
|
||||
get => _id;
|
||||
set { if (_id != value) { _id = value; OnPropertyChanged(); } }
|
||||
}
|
||||
|
||||
[Category("基本信息"), DisplayName("模型名称"), Description("给模型起一个易于识别的别名。")]
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set { if (_name != value) { _name = value; OnPropertyChanged(); } }
|
||||
}
|
||||
[Category("基本信息"), DisplayName("推理设备"), Description("推理模型的设备。")]
|
||||
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
|
||||
public DetectDevice CheckDevice
|
||||
{
|
||||
get => _checkDevice;
|
||||
set { if (_checkDevice != value) { _checkDevice = value; OnPropertyChanged(); } }
|
||||
}
|
||||
|
||||
[Category("基本信息"), DisplayName("模型版本"), Description("推理模型的版本。")]
|
||||
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
|
||||
public ModelVersion MVersion
|
||||
{
|
||||
get => _mVersion;
|
||||
set { if (_mVersion != value) { _mVersion = value; OnPropertyChanged(); } }
|
||||
}
|
||||
[Category("基本信息"), DisplayName("模型类型"), Description("推理模型的类型。")]
|
||||
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
|
||||
public CheckModelType MType
|
||||
{
|
||||
get => _mType;
|
||||
set { if (_mType != value) { _mType = value; OnPropertyChanged(); } }
|
||||
}
|
||||
|
||||
[Category("基本信息"), DisplayName("是否启用"), Description("是否在程序启动时是否启用模型")]
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _isEnabled;
|
||||
set
|
||||
{
|
||||
if (_isEnabled != value)
|
||||
{
|
||||
_isEnabled = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Category("文件"), DisplayName("模型路径"), Description("选择模型文件(.onnx, .bin, etc., .pt)。")]
|
||||
[Editor(typeof(System.Windows.Forms.Design.FileNameEditor), typeof(System.Drawing.Design.UITypeEditor))]
|
||||
public string Path
|
||||
{
|
||||
get => _path;
|
||||
set { if (_path != value) { _path = value; OnPropertyChanged(); } }
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
public object Clone()
|
||||
{
|
||||
return this.MemberwiseClone(); // 浅克隆对于这个类足够了
|
||||
}
|
||||
}
|
||||
}
|
||||
102
Check.Main/Infer/YoloModelManager.cs
Normal file
102
Check.Main/Infer/YoloModelManager.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Check.Main.Common;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using YoloDotNet;
|
||||
using YoloDotNet.Models;
|
||||
|
||||
namespace Check.Main.Infer
|
||||
{
|
||||
/// <summary>
|
||||
/// 静态全局YOLO模型管理器。
|
||||
/// 负责在程序启动时加载所有模型,并在关闭时释放资源。
|
||||
/// </summary>
|
||||
public static class YoloModelManager
|
||||
{
|
||||
// 使用 ConcurrentDictionary 保证线程安全
|
||||
private static readonly ConcurrentDictionary<int, Yolo> _loadedModels = new ConcurrentDictionary<int, Yolo>();
|
||||
|
||||
/// <summary>
|
||||
/// 根据模型配置列表初始化所有YOLO模型实例。
|
||||
/// 应在程序启动时调用一次。
|
||||
/// </summary>
|
||||
/// <param name="modelSettings">模型配置列表。</param>
|
||||
public static void Initialize(List<ModelSettings> modelSettings)
|
||||
{
|
||||
Shutdown(); // 先确保清理掉旧实例
|
||||
|
||||
if (modelSettings == null) return;
|
||||
|
||||
ThreadSafeLogger.Log("开始加载YOLO模型...");
|
||||
foreach (var setting in modelSettings)
|
||||
{
|
||||
bool gpuUse = false;
|
||||
if (setting.CheckDevice == DetectDevice.GPU)
|
||||
{
|
||||
gpuUse = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(setting.Path) || !File.Exists(setting.Path))
|
||||
{
|
||||
ThreadSafeLogger.Log($"[警告] 模型 '{setting.Name}' (ID: {setting.Id}) 路径无效或文件不存在,已跳过加载。");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 创建YOLO实例
|
||||
var yolo = new Yolo(new YoloOptions
|
||||
{
|
||||
OnnxModel = setting.Path,
|
||||
// 您可以根据需要从配置中读取这些值
|
||||
ModelType = (YoloDotNet.Enums.ModelType)setting.MType,
|
||||
Cuda = gpuUse, // 推荐使用GPU
|
||||
PrimeGpu = false
|
||||
});
|
||||
|
||||
if (_loadedModels.TryAdd(setting.Id, yolo))
|
||||
{
|
||||
ThreadSafeLogger.Log($"成功加载模型 '{setting.Name}' (ID: {setting.Id})。");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[错误] 加载模型 '{setting.Name}' (ID: {setting.Id}) 失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
ThreadSafeLogger.Log($"YOLO模型加载完成,共成功加载 {_loadedModels.Count} 个模型。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定ID的已加载YOLO模型。
|
||||
/// </summary>
|
||||
/// <param name="modelId">模型编号。</param>
|
||||
/// <returns>Yolo实例,如果未找到则返回null。</returns>
|
||||
public static Yolo GetModel(int modelId)
|
||||
{
|
||||
_loadedModels.TryGetValue(modelId, out var yolo);
|
||||
return yolo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放所有已加载的YOLO模型资源。
|
||||
/// 应在程序关闭时调用。
|
||||
/// </summary>
|
||||
public static void Shutdown()
|
||||
{
|
||||
if (_loadedModels.Count > 0)
|
||||
{
|
||||
ThreadSafeLogger.Log("正在释放所有YOLO模型...");
|
||||
foreach (var yolo in _loadedModels.Values)
|
||||
{
|
||||
yolo?.Dispose();
|
||||
}
|
||||
_loadedModels.Clear();
|
||||
ThreadSafeLogger.Log("所有YOLO模型已释放。");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Check.Main/Program.cs
Normal file
28
Check.Main/Program.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Check.Main
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
var current = Process.GetCurrentProcess();
|
||||
var others = Process.GetProcessesByName(current.ProcessName)
|
||||
.Where(p => p.Id != current.Id);
|
||||
if (others.Any())
|
||||
{
|
||||
MessageBox.Show("<22><><EFBFBD><EFBFBD><EFBFBD>Ѿ<EFBFBD><D1BE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>У<EFBFBD><D0A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>", "<22><>ʾ",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
// To customize application configuration such as set high DPI settings or default font,
|
||||
// see https://aka.ms/applicationconfiguration.
|
||||
ApplicationConfiguration.Initialize();
|
||||
Application.Run(new FrmMain());
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Check.Main/Result/ProductResult.cs
Normal file
74
Check.Main/Result/ProductResult.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Check.Main.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Check.Main.Result
|
||||
{
|
||||
/// <summary>
|
||||
/// 代表一个待检测产品的类,存储来自多个相机的图像
|
||||
/// </summary>
|
||||
public class ProductResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 产品唯一ID,可以是时间戳或触发计数
|
||||
/// </summary>
|
||||
public long ProductID { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 存储每个相机名称和其拍摄到的图像
|
||||
/// </summary>
|
||||
public Dictionary<string, Bitmap> CapturedImages { get; }
|
||||
|
||||
public ProductResult(long productID)
|
||||
{
|
||||
ProductID = productID;
|
||||
CapturedImages = new Dictionary<string, Bitmap>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加一张某个相机拍摄的图像
|
||||
/// </summary>
|
||||
/// <param name="cameraName">相机名称</param>
|
||||
/// <param name="image">拍摄的图像</param>
|
||||
public void AddImage(string cameraName, Bitmap image)
|
||||
{
|
||||
if (!CapturedImages.ContainsKey(cameraName))
|
||||
{
|
||||
CapturedImages.Add(cameraName, image);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果这个键已经存在,说明发生了逻辑错误。
|
||||
// 我们不应该持有这个新的 image 对象,必须释放它以防泄漏。
|
||||
ThreadSafeLogger.Log($"[警告] 相机 {cameraName} 为产品 #{this.ProductID} 发送了重复的图像。多余的图像将被丢弃。");
|
||||
image?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否所有预期的相机都已完成拍摄
|
||||
/// </summary>
|
||||
/// <param name="expectedCameraCount">预期的相机数量</param>
|
||||
/// <returns></returns>
|
||||
public bool IsComplete(int expectedCameraCount)
|
||||
{
|
||||
return CapturedImages.Count == expectedCameraCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放所有图像资源,防止内存泄漏
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var image in CapturedImages.Values)
|
||||
{
|
||||
image?.Dispose();
|
||||
}
|
||||
CapturedImages.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
113
Check.Main/UI/FormControlPanel.Designer.cs
generated
Normal file
113
Check.Main/UI/FormControlPanel.Designer.cs
generated
Normal file
@@ -0,0 +1,113 @@
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
partial class FormControlPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.uiTableLayoutPanel1 = new Sunny.UI.UITableLayoutPanel();
|
||||
this.btnStartDevice = new Sunny.UI.UIButton();
|
||||
this.btnStartCheck = new Sunny.UI.UIButton();
|
||||
this.uiTableLayoutPanel1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// uiTableLayoutPanel1
|
||||
//
|
||||
this.uiTableLayoutPanel1.ColumnCount = 2;
|
||||
this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
|
||||
this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
|
||||
this.uiTableLayoutPanel1.Controls.Add(this.btnStartDevice, 0, 1);
|
||||
this.uiTableLayoutPanel1.Controls.Add(this.btnStartCheck, 0, 3);
|
||||
this.uiTableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.uiTableLayoutPanel1.Location = new System.Drawing.Point(0, 0);
|
||||
this.uiTableLayoutPanel1.Name = "uiTableLayoutPanel1";
|
||||
this.uiTableLayoutPanel1.RowCount = 5;
|
||||
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.38585F));
|
||||
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 35.35353F));
|
||||
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 7.575758F));
|
||||
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.83838F));
|
||||
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.38585F));
|
||||
this.uiTableLayoutPanel1.Size = new System.Drawing.Size(230, 198);
|
||||
this.uiTableLayoutPanel1.TabIndex = 0;
|
||||
this.uiTableLayoutPanel1.TagString = null;
|
||||
//
|
||||
// btnStartDevice
|
||||
//
|
||||
this.uiTableLayoutPanel1.SetColumnSpan(this.btnStartDevice, 2);
|
||||
this.btnStartDevice.Cursor = System.Windows.Forms.Cursors.Hand;
|
||||
this.btnStartDevice.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.btnStartDevice.FillPressColor = System.Drawing.Color.LimeGreen;
|
||||
this.btnStartDevice.FillSelectedColor = System.Drawing.Color.LimeGreen;
|
||||
this.btnStartDevice.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.btnStartDevice.Location = new System.Drawing.Point(3, 25);
|
||||
this.btnStartDevice.MinimumSize = new System.Drawing.Size(1, 1);
|
||||
this.btnStartDevice.Name = "btnStartDevice";
|
||||
this.btnStartDevice.Size = new System.Drawing.Size(224, 64);
|
||||
this.btnStartDevice.TabIndex = 0;
|
||||
this.btnStartDevice.Text = "启动设备";
|
||||
this.btnStartDevice.TipsFont = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.btnStartDevice.Click += new System.EventHandler(this.btnStartDevice_Click);
|
||||
//
|
||||
// btnStartCheck
|
||||
//
|
||||
this.uiTableLayoutPanel1.SetColumnSpan(this.btnStartCheck, 2);
|
||||
this.btnStartCheck.Cursor = System.Windows.Forms.Cursors.Hand;
|
||||
this.btnStartCheck.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.btnStartCheck.FillPressColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(192)))), ((int)(((byte)(0)))));
|
||||
this.btnStartCheck.FillSelectedColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(192)))), ((int)(((byte)(0)))));
|
||||
this.btnStartCheck.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.btnStartCheck.Location = new System.Drawing.Point(3, 110);
|
||||
this.btnStartCheck.MinimumSize = new System.Drawing.Size(1, 1);
|
||||
this.btnStartCheck.Name = "btnStartCheck";
|
||||
this.btnStartCheck.Size = new System.Drawing.Size(224, 61);
|
||||
this.btnStartCheck.TabIndex = 1;
|
||||
this.btnStartCheck.Text = "开始检测";
|
||||
this.btnStartCheck.TipsFont = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.btnStartCheck.Click += new System.EventHandler(this.btnStartCheck_Click);
|
||||
//
|
||||
// FormControlPanel
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(230, 198);
|
||||
this.ControlBox = false;
|
||||
this.Controls.Add(this.uiTableLayoutPanel1);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "FormControlPanel";
|
||||
this.Text = "启动管理";
|
||||
this.uiTableLayoutPanel1.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Sunny.UI.UITableLayoutPanel uiTableLayoutPanel1;
|
||||
private Sunny.UI.UIButton btnStartDevice;
|
||||
private Sunny.UI.UIButton btnStartCheck;
|
||||
}
|
||||
}
|
||||
264
Check.Main/UI/FormControlPanel.cs
Normal file
264
Check.Main/UI/FormControlPanel.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
using Check.Main.Camera;
|
||||
using Check.Main.Common;
|
||||
using Check.Main.Infer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using WeifenLuo.WinFormsUI.Docking;
|
||||
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
public partial class FormControlPanel : DockContent
|
||||
{
|
||||
|
||||
private bool _isDeviceReady = false; // 新的状态:设备是否已准备好
|
||||
private bool _isDetecting = false; // 新的状态:是否正在检测中
|
||||
|
||||
// 用于跟踪设备运行状态的私有标志
|
||||
private bool _isDeviceRunning = false;
|
||||
public FormControlPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
ConfigurationManager.OnConfigurationChanged += HandleConfigurationChanged;
|
||||
UpdateUI();
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// 处理全局配置在其他地方(如 FrmConfig)被更改的事件
|
||||
/// </summary>
|
||||
private void HandleConfigurationChanged()
|
||||
{
|
||||
// 这是一个安全措施。如果设备正在运行时配置发生了变化,
|
||||
// 最安全的做法是停止设备,以防止出现不可预知的行为。
|
||||
if (_isDeviceRunning)
|
||||
{
|
||||
// 使用 Invoke 确保UI更新在正确的线程上执行
|
||||
this.Invoke((Action)(() =>
|
||||
{
|
||||
ThreadSafeLogger.Log("相机配置已在运行时发生更改,设备将自动停止。请重新启动设备以应用新配置。");
|
||||
//MessageBox.Show("相机配置已在运行时发生更改,设备将自动停止。请重新启动设备以应用新配置。",
|
||||
// "配置变更", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
// 触发与点击“关闭设备”按钮相同的逻辑
|
||||
btnStartDevice_Click(this, EventArgs.Empty);
|
||||
}));
|
||||
}
|
||||
}
|
||||
private void btnStartDevice_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (_isDeviceReady)//_isDeviceRunning
|
||||
{
|
||||
// --- 关闭流程 ---
|
||||
ThreadSafeLogger.Log("用户点击“关闭设备”,开始完整关闭流程...");
|
||||
// 如果正在检测,先停止它
|
||||
if (_isDetecting)
|
||||
{
|
||||
btnStartCheck_Click(this, EventArgs.Empty); // 调用停止检测的逻辑
|
||||
}
|
||||
var mainForm = this.DockPanel.FindForm() as FrmMain;
|
||||
CameraManager.Shutdown();
|
||||
_isDeviceReady = false;
|
||||
|
||||
//// 1. 停止硬触发模拟器
|
||||
//CameraManager.StopHardwareTriggerSimulator();
|
||||
|
||||
//// 2. 如果检测正在运行,则停止
|
||||
//if (DetectionCoordinator.IsDetectionRunning)
|
||||
//{
|
||||
// DetectionCoordinator.StopDetection();
|
||||
// UpdateDetectionButtonUI();
|
||||
//}
|
||||
|
||||
// 3. 执行完整的系统关闭(包括相机硬件和检测协调器)
|
||||
//CameraManager.Shutdown();
|
||||
//YoloModelManager.Shutdown();
|
||||
//_isDeviceRunning = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// --- 启动流程 ---
|
||||
ThreadSafeLogger.Log("用户点击“启动设备”,开始新的启动流程...");
|
||||
|
||||
// 1. 从单一数据源获取完整的配置对象
|
||||
var config = ConfigurationManager.GetCurrentConfig();
|
||||
// 2. 验证相机配置的有效性
|
||||
if (config.CameraSettings == null || !config.CameraSettings.Any(c => c.IsEnabled))
|
||||
{
|
||||
ThreadSafeLogger.Log("没有已启用的相机配置,启动中止。");
|
||||
return;
|
||||
}
|
||||
// 3. 获取主窗体引用
|
||||
var mainForm = this.DockPanel.FindForm() as FrmMain;
|
||||
if (mainForm == null)
|
||||
{
|
||||
ThreadSafeLogger.Log("无法找到主窗体,启动中止。");
|
||||
return;
|
||||
}
|
||||
// 4. 执行新的启动流程:
|
||||
// 第一步:初始化系统。这会准备好相机硬件、UI窗口和所有后台处理线程。
|
||||
CameraManager.PrepareAll(config, mainForm);
|
||||
_isDeviceReady = true;
|
||||
|
||||
|
||||
//YoloModelManager.Initialize(config.ModelSettings);
|
||||
//CameraManager.Initialize(config, mainForm);
|
||||
//// 第二步:命令所有相机开始采集图像。
|
||||
//CameraManager.StartAll();
|
||||
|
||||
//// 5. 如果有任何相机配置为软触发模式,我们启动模拟器来模拟触发信号
|
||||
//if (config.CameraSettings.Any(c => c.IsEnabled && c.TriggerMode == TriggerModeType.Software))
|
||||
//{
|
||||
// ThreadSafeLogger.Log("检测到软触发相机,启动触发模拟器。");
|
||||
// CameraManager.TriggerInterval = 100; // 根据需要设置间隔
|
||||
// CameraManager.StartHardwareTriggerSimulator();
|
||||
//}
|
||||
|
||||
//_isDeviceRunning = true;
|
||||
}
|
||||
UpdateUI();//UpdateDeviceButtonUI();
|
||||
}
|
||||
|
||||
private void btnStartCheck_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (_isDetecting)
|
||||
{
|
||||
// --- 停止检测 ---
|
||||
ThreadSafeLogger.Log("用户点击“停止检测”,暂停数据流...");
|
||||
// 停止硬触发模拟器
|
||||
CameraManager.StopHardwareTriggerSimulator();
|
||||
// 停止相机采集
|
||||
CameraManager.StopAll();
|
||||
|
||||
// 停止统计
|
||||
DetectionCoordinator.StopDetection();
|
||||
|
||||
_isDetecting = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// --- 开始检测 ---
|
||||
ThreadSafeLogger.Log("用户点击“开始检测”,启动数据流...");
|
||||
|
||||
// 命令相机开始采集
|
||||
CameraManager.StartAll();
|
||||
|
||||
// 启动硬触发模拟器(如果需要)
|
||||
var config = ConfigurationManager.GetCurrentConfig();
|
||||
if (config.CameraSettings.Any(c => c.IsEnabled && c.TriggerMode == TriggerModeType.Software))
|
||||
{
|
||||
CameraManager.TriggerInterval = 100;
|
||||
CameraManager.StartHardwareTriggerSimulator();
|
||||
}
|
||||
|
||||
// 开始统计
|
||||
DetectionCoordinator.StartDetection();
|
||||
|
||||
_isDetecting = true;
|
||||
}
|
||||
UpdateUI();
|
||||
|
||||
//if (!_isDeviceRunning && !DetectionCoordinator.IsDetectionRunning)
|
||||
//{
|
||||
// ThreadSafeLogger.Log("设备未启动,无法开始检测。");
|
||||
// return;
|
||||
//}
|
||||
|
||||
//// 现在调用 DetectionCoordinator 中的方法
|
||||
//if (DetectionCoordinator.IsDetectionRunning)
|
||||
//{
|
||||
// DetectionCoordinator.StopDetection();
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// DetectionCoordinator.StartDetection();
|
||||
//}
|
||||
//UpdateDetectionButtonUI();
|
||||
}
|
||||
|
||||
#region UI 更新辅助方法
|
||||
|
||||
/// <summary>
|
||||
/// 根据设备运行状态更新“设备”按钮的UI(文本和颜色)。
|
||||
/// </summary>
|
||||
private void UpdateDeviceButtonUI()
|
||||
{
|
||||
if (_isDeviceRunning)
|
||||
{
|
||||
btnStartDevice.Text = "关闭设备";
|
||||
btnStartDevice.BackColor = Color.Salmon;
|
||||
}
|
||||
else
|
||||
{
|
||||
btnStartDevice.Text = "启动设备";
|
||||
btnStartDevice.BackColor = SystemColors.Control;
|
||||
}
|
||||
}
|
||||
// 统一的UI更新方法
|
||||
private void UpdateUI()
|
||||
{
|
||||
// --- 更新“设备”按钮 ---
|
||||
if (_isDeviceReady)
|
||||
{
|
||||
btnStartDevice.Text = "关闭设备";
|
||||
btnStartDevice.BackColor = Color.Salmon;
|
||||
btnStartCheck.Enabled = true; // 设备就绪后,检测按钮才可用
|
||||
}
|
||||
else
|
||||
{
|
||||
btnStartDevice.Text = "启动设备";
|
||||
btnStartDevice.BackColor = SystemColors.Control;
|
||||
btnStartCheck.Enabled = false; // 设备未就绪,检测按钮不可用
|
||||
}
|
||||
|
||||
// --- 更新“检测”按钮 ---
|
||||
if (_isDetecting)
|
||||
{
|
||||
btnStartCheck.Text = "停止检测";
|
||||
btnStartCheck.BackColor = Color.LightGreen;
|
||||
btnStartDevice.Enabled = false; // 正在检测时,不允许关闭设备
|
||||
}
|
||||
else
|
||||
{
|
||||
btnStartCheck.Text = "开始检测";
|
||||
btnStartCheck.BackColor = SystemColors.Control;
|
||||
if (_isDeviceReady) btnStartDevice.Enabled = true; // 停止检测后,允许关闭设备
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据检测运行状态更新“检测”按钮的UI(文本和颜色)。
|
||||
/// </summary>
|
||||
private void UpdateDetectionButtonUI()
|
||||
{
|
||||
//if (CameraManager.IsDetectionRunning)
|
||||
//{
|
||||
// btnStartCheck.Text = "停止检测";
|
||||
// btnStartCheck.BackColor = Color.LightGreen;
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// btnStartCheck.Text = "启动检测";
|
||||
// btnStartCheck.BackColor = SystemColors.Control;
|
||||
//}
|
||||
if (DetectionCoordinator.IsDetectionRunning)
|
||||
{
|
||||
btnStartCheck.Text = "停止检测";
|
||||
btnStartCheck.BackColor = Color.LightGreen;
|
||||
}
|
||||
else
|
||||
{
|
||||
btnStartCheck.Text = "启动检测";
|
||||
btnStartCheck.BackColor = SystemColors.Control;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
120
Check.Main/UI/FormControlPanel.resx
Normal file
120
Check.Main/UI/FormControlPanel.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
46
Check.Main/UI/FormImageDisplay.Designer.cs
generated
Normal file
46
Check.Main/UI/FormImageDisplay.Designer.cs
generated
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
partial class FormImageDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// FormImageDisplay
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(508, 290);
|
||||
this.Name = "FormImageDisplay";
|
||||
this.Text = "FormImageDisplay";
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
74
Check.Main/UI/FormImageDisplay.cs
Normal file
74
Check.Main/UI/FormImageDisplay.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using WeifenLuo.WinFormsUI.Docking;
|
||||
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
public partial class FormImageDisplay : DockContent
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机名称
|
||||
/// </summary>
|
||||
public string CameraName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当此显示窗口发生特定事件时(如ROI裁剪),触发此事件以通知外部(如日志系统)
|
||||
/// </summary>
|
||||
public event Action<string> OnDisplayEvent;
|
||||
|
||||
// 使用我们全新的自定义控件
|
||||
private ZoomPictureBox zoomPictureBox;
|
||||
public FormImageDisplay()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 实例化新的控件
|
||||
zoomPictureBox = new ZoomPictureBox
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
// 其他属性可以在这里设置,例如
|
||||
// RectangleColor = Color.LawnGreen,
|
||||
// BackgroundFillColor = Color.FromArgb(45, 45, 48)
|
||||
};
|
||||
this.Controls.Add(zoomPictureBox);
|
||||
|
||||
// 订阅自定义控件的ROI裁剪完成事件
|
||||
zoomPictureBox.CroppingEnabled = false;
|
||||
//zoomPictureBox.Cropped += ZoomPictureBox_Cropped;
|
||||
}
|
||||
/// <summary>
|
||||
/// 更新显示的图像(线程安全)。
|
||||
/// 此方法现在将图像设置到 ZoomPictureBox1 控件中。
|
||||
/// </summary>
|
||||
/// <param name="image">从相机事件传来的原始Bitmap</param>
|
||||
public void UpdateImage(Bitmap image)
|
||||
{
|
||||
if (zoomPictureBox != null && !zoomPictureBox.IsDisposed)
|
||||
{
|
||||
zoomPictureBox.SetImageThreadSafe(image);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果PictureBox已经被释放,那么我们也应该释放这个多余的图像
|
||||
image?.Dispose();
|
||||
}
|
||||
}
|
||||
// 重写 Close 方法,确保在窗口关闭时,内部的控件和资源也能被妥善处理
|
||||
public new void Close()
|
||||
{
|
||||
// 取消事件订阅,防止内存泄漏
|
||||
if (zoomPictureBox != null)
|
||||
{
|
||||
//zoomPictureBox.Cropped -= ZoomPictureBox_Cropped;
|
||||
}
|
||||
base.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Check.Main/UI/FormImageDisplay.resx
Normal file
120
Check.Main/UI/FormImageDisplay.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
245
Check.Main/UI/FormStatistics.Designer.cs
generated
Normal file
245
Check.Main/UI/FormStatistics.Designer.cs
generated
Normal file
@@ -0,0 +1,245 @@
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
partial class FormStatistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FormStatistics));
|
||||
this.uiTableLayoutPanel1 = new Sunny.UI.UITableLayoutPanel();
|
||||
this.toolStrip1 = new System.Windows.Forms.ToolStrip();
|
||||
this.uiLabel1 = new Sunny.UI.UILabel();
|
||||
this.uiLabel2 = new Sunny.UI.UILabel();
|
||||
this.uiLabel3 = new Sunny.UI.UILabel();
|
||||
this.uiLabel4 = new Sunny.UI.UILabel();
|
||||
this.txtOKNum = new Sunny.UI.UITextBox();
|
||||
this.txtNGNum = new Sunny.UI.UITextBox();
|
||||
this.txtTotal = new Sunny.UI.UITextBox();
|
||||
this.txtYieldRate = new Sunny.UI.UITextBox();
|
||||
this.toolStripButtonRest = new System.Windows.Forms.ToolStripButton();
|
||||
this.uiTableLayoutPanel1.SuspendLayout();
|
||||
this.toolStrip1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// uiTableLayoutPanel1
|
||||
//
|
||||
this.uiTableLayoutPanel1.ColumnCount = 2;
|
||||
this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 45.88745F));
|
||||
this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 54.11255F));
|
||||
this.uiTableLayoutPanel1.Controls.Add(this.txtYieldRate, 1, 3);
|
||||
this.uiTableLayoutPanel1.Controls.Add(this.txtTotal, 1, 2);
|
||||
this.uiTableLayoutPanel1.Controls.Add(this.txtNGNum, 1, 1);
|
||||
this.uiTableLayoutPanel1.Controls.Add(this.toolStrip1, 0, 4);
|
||||
this.uiTableLayoutPanel1.Controls.Add(this.uiLabel1, 0, 0);
|
||||
this.uiTableLayoutPanel1.Controls.Add(this.uiLabel2, 0, 1);
|
||||
this.uiTableLayoutPanel1.Controls.Add(this.uiLabel3, 0, 2);
|
||||
this.uiTableLayoutPanel1.Controls.Add(this.uiLabel4, 0, 3);
|
||||
this.uiTableLayoutPanel1.Controls.Add(this.txtOKNum, 1, 0);
|
||||
this.uiTableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.uiTableLayoutPanel1.Location = new System.Drawing.Point(0, 0);
|
||||
this.uiTableLayoutPanel1.Name = "uiTableLayoutPanel1";
|
||||
this.uiTableLayoutPanel1.RowCount = 5;
|
||||
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.uiTableLayoutPanel1.Size = new System.Drawing.Size(231, 228);
|
||||
this.uiTableLayoutPanel1.TabIndex = 0;
|
||||
this.uiTableLayoutPanel1.TagString = null;
|
||||
//
|
||||
// toolStrip1
|
||||
//
|
||||
this.uiTableLayoutPanel1.SetColumnSpan(this.toolStrip1, 2);
|
||||
this.toolStrip1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.toolStripButtonRest});
|
||||
this.toolStrip1.Location = new System.Drawing.Point(0, 180);
|
||||
this.toolStrip1.Name = "toolStrip1";
|
||||
this.toolStrip1.Size = new System.Drawing.Size(231, 48);
|
||||
this.toolStrip1.TabIndex = 0;
|
||||
this.toolStrip1.Text = "toolStrip1";
|
||||
//
|
||||
// uiLabel1
|
||||
//
|
||||
this.uiLabel1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.uiLabel1.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.uiLabel1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(48)))), ((int)(((byte)(48)))), ((int)(((byte)(48)))));
|
||||
this.uiLabel1.Location = new System.Drawing.Point(3, 0);
|
||||
this.uiLabel1.Name = "uiLabel1";
|
||||
this.uiLabel1.Size = new System.Drawing.Size(100, 45);
|
||||
this.uiLabel1.TabIndex = 1;
|
||||
this.uiLabel1.Text = "良品数";
|
||||
this.uiLabel1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// uiLabel2
|
||||
//
|
||||
this.uiLabel2.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.uiLabel2.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.uiLabel2.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(48)))), ((int)(((byte)(48)))), ((int)(((byte)(48)))));
|
||||
this.uiLabel2.Location = new System.Drawing.Point(3, 45);
|
||||
this.uiLabel2.Name = "uiLabel2";
|
||||
this.uiLabel2.Size = new System.Drawing.Size(100, 45);
|
||||
this.uiLabel2.TabIndex = 2;
|
||||
this.uiLabel2.Text = "不良品数量";
|
||||
this.uiLabel2.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// uiLabel3
|
||||
//
|
||||
this.uiLabel3.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.uiLabel3.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.uiLabel3.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(48)))), ((int)(((byte)(48)))), ((int)(((byte)(48)))));
|
||||
this.uiLabel3.Location = new System.Drawing.Point(3, 90);
|
||||
this.uiLabel3.Name = "uiLabel3";
|
||||
this.uiLabel3.Size = new System.Drawing.Size(100, 45);
|
||||
this.uiLabel3.TabIndex = 3;
|
||||
this.uiLabel3.Text = "总数";
|
||||
this.uiLabel3.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// uiLabel4
|
||||
//
|
||||
this.uiLabel4.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.uiLabel4.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.uiLabel4.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(48)))), ((int)(((byte)(48)))), ((int)(((byte)(48)))));
|
||||
this.uiLabel4.Location = new System.Drawing.Point(3, 135);
|
||||
this.uiLabel4.Name = "uiLabel4";
|
||||
this.uiLabel4.Size = new System.Drawing.Size(100, 45);
|
||||
this.uiLabel4.TabIndex = 4;
|
||||
this.uiLabel4.Text = "良品率";
|
||||
this.uiLabel4.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// txtOKNum
|
||||
//
|
||||
this.txtOKNum.Cursor = System.Windows.Forms.Cursors.IBeam;
|
||||
this.txtOKNum.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.txtOKNum.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.txtOKNum.Location = new System.Drawing.Point(110, 5);
|
||||
this.txtOKNum.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.txtOKNum.MinimumSize = new System.Drawing.Size(1, 16);
|
||||
this.txtOKNum.Name = "txtOKNum";
|
||||
this.txtOKNum.Padding = new System.Windows.Forms.Padding(5);
|
||||
this.txtOKNum.ShowText = false;
|
||||
this.txtOKNum.Size = new System.Drawing.Size(117, 35);
|
||||
this.txtOKNum.TabIndex = 5;
|
||||
this.txtOKNum.Text = "0";
|
||||
this.txtOKNum.TextAlignment = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
this.txtOKNum.Watermark = "";
|
||||
//
|
||||
// txtNGNum
|
||||
//
|
||||
this.txtNGNum.Cursor = System.Windows.Forms.Cursors.IBeam;
|
||||
this.txtNGNum.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.txtNGNum.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.txtNGNum.Location = new System.Drawing.Point(110, 50);
|
||||
this.txtNGNum.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.txtNGNum.MinimumSize = new System.Drawing.Size(1, 16);
|
||||
this.txtNGNum.Name = "txtNGNum";
|
||||
this.txtNGNum.Padding = new System.Windows.Forms.Padding(5);
|
||||
this.txtNGNum.ShowText = false;
|
||||
this.txtNGNum.Size = new System.Drawing.Size(117, 35);
|
||||
this.txtNGNum.TabIndex = 6;
|
||||
this.txtNGNum.Text = "0";
|
||||
this.txtNGNum.TextAlignment = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
this.txtNGNum.Watermark = "";
|
||||
//
|
||||
// txtTotal
|
||||
//
|
||||
this.txtTotal.Cursor = System.Windows.Forms.Cursors.IBeam;
|
||||
this.txtTotal.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.txtTotal.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.txtTotal.Location = new System.Drawing.Point(110, 95);
|
||||
this.txtTotal.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.txtTotal.MinimumSize = new System.Drawing.Size(1, 16);
|
||||
this.txtTotal.Name = "txtTotal";
|
||||
this.txtTotal.Padding = new System.Windows.Forms.Padding(5);
|
||||
this.txtTotal.ShowText = false;
|
||||
this.txtTotal.Size = new System.Drawing.Size(117, 35);
|
||||
this.txtTotal.TabIndex = 7;
|
||||
this.txtTotal.Text = "0";
|
||||
this.txtTotal.TextAlignment = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
this.txtTotal.Watermark = "";
|
||||
//
|
||||
// txtYieldRate
|
||||
//
|
||||
this.txtYieldRate.Cursor = System.Windows.Forms.Cursors.IBeam;
|
||||
this.txtYieldRate.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.txtYieldRate.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.txtYieldRate.Location = new System.Drawing.Point(110, 140);
|
||||
this.txtYieldRate.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.txtYieldRate.MinimumSize = new System.Drawing.Size(1, 16);
|
||||
this.txtYieldRate.Name = "txtYieldRate";
|
||||
this.txtYieldRate.Padding = new System.Windows.Forms.Padding(5);
|
||||
this.txtYieldRate.ShowText = false;
|
||||
this.txtYieldRate.Size = new System.Drawing.Size(117, 35);
|
||||
this.txtYieldRate.TabIndex = 8;
|
||||
this.txtYieldRate.Text = "0";
|
||||
this.txtYieldRate.TextAlignment = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
this.txtYieldRate.Watermark = "";
|
||||
//
|
||||
// toolStripButtonRest
|
||||
//
|
||||
this.toolStripButtonRest.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.toolStripButtonRest.Image = ((System.Drawing.Image)(resources.GetObject("toolStripButtonRest.Image")));
|
||||
this.toolStripButtonRest.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.toolStripButtonRest.Name = "toolStripButtonRest";
|
||||
this.toolStripButtonRest.Size = new System.Drawing.Size(60, 45);
|
||||
this.toolStripButtonRest.Text = "重置计数";
|
||||
this.toolStripButtonRest.Click += new System.EventHandler(this.toolStripButtonRest_Click);
|
||||
//
|
||||
// FormStatistics
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(231, 228);
|
||||
this.ControlBox = false;
|
||||
this.Controls.Add(this.uiTableLayoutPanel1);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "FormStatistics";
|
||||
this.Text = "统计";
|
||||
this.uiTableLayoutPanel1.ResumeLayout(false);
|
||||
this.uiTableLayoutPanel1.PerformLayout();
|
||||
this.toolStrip1.ResumeLayout(false);
|
||||
this.toolStrip1.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Sunny.UI.UITableLayoutPanel uiTableLayoutPanel1;
|
||||
private System.Windows.Forms.ToolStrip toolStrip1;
|
||||
private Sunny.UI.UITextBox txtYieldRate;
|
||||
private Sunny.UI.UITextBox txtTotal;
|
||||
private Sunny.UI.UITextBox txtNGNum;
|
||||
private System.Windows.Forms.ToolStripButton toolStripButtonRest;
|
||||
private Sunny.UI.UILabel uiLabel1;
|
||||
private Sunny.UI.UILabel uiLabel2;
|
||||
private Sunny.UI.UILabel uiLabel3;
|
||||
private Sunny.UI.UILabel uiLabel4;
|
||||
private Sunny.UI.UITextBox txtOKNum;
|
||||
}
|
||||
}
|
||||
89
Check.Main/UI/FormStatistics.cs
Normal file
89
Check.Main/UI/FormStatistics.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using Check.Main.Camera;
|
||||
using Check.Main.Common;
|
||||
using Check.Main.Infer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using WeifenLuo.WinFormsUI.Docking;
|
||||
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
public partial class FormStatistics : DockContent
|
||||
{
|
||||
private readonly StatisticsData _statisticsData;
|
||||
|
||||
/// <summary>
|
||||
/// 将私有字段通过公共属性暴露,以便外部(如FormMain)访问。
|
||||
/// </summary>
|
||||
public StatisticsData CurrentStatistics => _statisticsData;
|
||||
public FormStatistics()
|
||||
{
|
||||
InitializeComponent();
|
||||
_statisticsData = new StatisticsData();
|
||||
|
||||
// 订阅CameraManager的检测完成事件
|
||||
DetectionCoordinator.OnDetectionCompleted += CameraManager_OnDetectionCompleted;
|
||||
// 初始化UI显示
|
||||
UpdateUI();
|
||||
}
|
||||
// 当CameraManager发布检测结果时,此方法被调用
|
||||
private void CameraManager_OnDetectionCompleted(object sender, DetectionResultEventArgs e)
|
||||
{
|
||||
// 更新统计数据
|
||||
_statisticsData.UpdateWithResult(e.IsOK);
|
||||
|
||||
// 在UI线程上更新界面显示
|
||||
UpdateUI();
|
||||
}
|
||||
// 更新所有标签的文本
|
||||
private void UpdateUI()
|
||||
{
|
||||
// 使用Invoke确保线程安全
|
||||
if (this.InvokeRequired)
|
||||
{
|
||||
this.Invoke(new Action(UpdateUI));
|
||||
return;
|
||||
}
|
||||
|
||||
txtOKNum.Text = _statisticsData.GoodCount.ToString();
|
||||
txtNGNum.Text = _statisticsData.NgCount.ToString();
|
||||
txtTotal.Text = _statisticsData.TotalCount.ToString();
|
||||
// 将良率格式化为百分比
|
||||
txtYieldRate.Text = _statisticsData.YieldRate.ToString("P2", CultureInfo.InvariantCulture);
|
||||
|
||||
// 根据良率改变颜色以示提醒
|
||||
if (_statisticsData.YieldRate < 0.9 && _statisticsData.TotalCount > 10)
|
||||
{
|
||||
txtYieldRate.ForeColor = Color.Red;
|
||||
}
|
||||
else
|
||||
{
|
||||
txtYieldRate.ForeColor = Color.ForestGreen;
|
||||
}
|
||||
}
|
||||
|
||||
private void toolStripButtonRest_Click(object sender, EventArgs e)
|
||||
{
|
||||
StatisticsExporter.ExportToExcel(CurrentStatistics, "Reset");
|
||||
|
||||
_statisticsData.Reset();
|
||||
// 同时重置产品ID计数器
|
||||
DetectionCoordinator.ResetAllCounters();
|
||||
UpdateUI();
|
||||
ThreadSafeLogger.Log("统计数据已重置。");
|
||||
}
|
||||
// 窗体关闭时,取消事件订阅,防止内存泄漏
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
{
|
||||
DetectionCoordinator.OnDetectionCompleted -= CameraManager_OnDetectionCompleted;
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
139
Check.Main/UI/FormStatistics.resx
Normal file
139
Check.Main/UI/FormStatistics.resx
Normal file
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="toolStripButtonRest.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG
|
||||
YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9
|
||||
0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw
|
||||
bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc
|
||||
VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9
|
||||
c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32
|
||||
Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo
|
||||
mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+
|
||||
kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D
|
||||
TgDQASA1MVpwzwAAAABJRU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
</root>
|
||||
175
Check.Main/UI/FrmCamConfig.Designer.cs
generated
Normal file
175
Check.Main/UI/FrmCamConfig.Designer.cs
generated
Normal file
@@ -0,0 +1,175 @@
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
partial class FrmCamConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FrmCamConfig));
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
toolStrip1 = new ToolStrip();
|
||||
toolBtnAdd = new ToolStripButton();
|
||||
toolBtnRemove = new ToolStripButton();
|
||||
toolBtnSet = new ToolStripButton();
|
||||
splitContainer1 = new SplitContainer();
|
||||
listBoxCameras = new ListBox();
|
||||
propertyGrid1 = new PropertyGrid();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
toolStrip1.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
|
||||
splitContainer1.Panel1.SuspendLayout();
|
||||
splitContainer1.Panel2.SuspendLayout();
|
||||
splitContainer1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.ColumnCount = 1;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.Controls.Add(toolStrip1, 0, 0);
|
||||
tableLayoutPanel1.Controls.Add(splitContainer1, 0, 1);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(0, 0);
|
||||
tableLayoutPanel1.Margin = new Padding(4, 4, 4, 4);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 2;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.Size = new Size(632, 560);
|
||||
tableLayoutPanel1.TabIndex = 0;
|
||||
//
|
||||
// toolStrip1
|
||||
//
|
||||
toolStrip1.Items.AddRange(new ToolStripItem[] { toolBtnAdd, toolBtnRemove, toolBtnSet });
|
||||
toolStrip1.Location = new Point(0, 0);
|
||||
toolStrip1.Name = "toolStrip1";
|
||||
toolStrip1.Size = new Size(632, 25);
|
||||
toolStrip1.TabIndex = 0;
|
||||
toolStrip1.Text = "toolStrip1";
|
||||
//
|
||||
// toolBtnAdd
|
||||
//
|
||||
toolBtnAdd.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
toolBtnAdd.Image = (Image)resources.GetObject("toolBtnAdd.Image");
|
||||
toolBtnAdd.ImageTransparentColor = Color.Magenta;
|
||||
toolBtnAdd.Name = "toolBtnAdd";
|
||||
toolBtnAdd.Size = new Size(36, 22);
|
||||
toolBtnAdd.Text = "添加";
|
||||
toolBtnAdd.Click += toolBtnAdd_Click;
|
||||
//
|
||||
// toolBtnRemove
|
||||
//
|
||||
toolBtnRemove.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
toolBtnRemove.Image = (Image)resources.GetObject("toolBtnRemove.Image");
|
||||
toolBtnRemove.ImageTransparentColor = Color.Magenta;
|
||||
toolBtnRemove.Name = "toolBtnRemove";
|
||||
toolBtnRemove.Size = new Size(36, 22);
|
||||
toolBtnRemove.Text = "移除";
|
||||
toolBtnRemove.Click += toolBtnRemove_Click;
|
||||
//
|
||||
// toolBtnSet
|
||||
//
|
||||
toolBtnSet.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
toolBtnSet.Image = (Image)resources.GetObject("toolBtnSet.Image");
|
||||
toolBtnSet.ImageTransparentColor = Color.Magenta;
|
||||
toolBtnSet.Name = "toolBtnSet";
|
||||
toolBtnSet.Size = new Size(60, 22);
|
||||
toolBtnSet.Text = "应用配置";
|
||||
toolBtnSet.Click += toolBtnSet_Click;
|
||||
//
|
||||
// splitContainer1
|
||||
//
|
||||
splitContainer1.Dock = DockStyle.Fill;
|
||||
splitContainer1.Location = new Point(4, 29);
|
||||
splitContainer1.Margin = new Padding(4, 4, 4, 4);
|
||||
splitContainer1.Name = "splitContainer1";
|
||||
//
|
||||
// splitContainer1.Panel1
|
||||
//
|
||||
splitContainer1.Panel1.Controls.Add(listBoxCameras);
|
||||
//
|
||||
// splitContainer1.Panel2
|
||||
//
|
||||
splitContainer1.Panel2.Controls.Add(propertyGrid1);
|
||||
splitContainer1.Size = new Size(624, 527);
|
||||
splitContainer1.SplitterDistance = 210;
|
||||
splitContainer1.SplitterWidth = 5;
|
||||
splitContainer1.TabIndex = 1;
|
||||
//
|
||||
// listBoxCameras
|
||||
//
|
||||
listBoxCameras.Dock = DockStyle.Fill;
|
||||
listBoxCameras.FormattingEnabled = true;
|
||||
listBoxCameras.ItemHeight = 17;
|
||||
listBoxCameras.Location = new Point(0, 0);
|
||||
listBoxCameras.Margin = new Padding(4, 4, 4, 4);
|
||||
listBoxCameras.Name = "listBoxCameras";
|
||||
listBoxCameras.Size = new Size(210, 527);
|
||||
listBoxCameras.TabIndex = 0;
|
||||
listBoxCameras.SelectedIndexChanged += listBoxCameras_SelectedIndexChanged;
|
||||
//
|
||||
// propertyGrid1
|
||||
//
|
||||
propertyGrid1.Dock = DockStyle.Fill;
|
||||
propertyGrid1.Location = new Point(0, 0);
|
||||
propertyGrid1.Margin = new Padding(4, 4, 4, 4);
|
||||
propertyGrid1.Name = "propertyGrid1";
|
||||
propertyGrid1.Size = new Size(409, 527);
|
||||
propertyGrid1.TabIndex = 0;
|
||||
//
|
||||
// FrmCamConfig
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(7F, 17F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(632, 560);
|
||||
Controls.Add(tableLayoutPanel1);
|
||||
Margin = new Padding(4, 4, 4, 4);
|
||||
Name = "FrmCamConfig";
|
||||
Text = "属性配置";
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
toolStrip1.ResumeLayout(false);
|
||||
toolStrip1.PerformLayout();
|
||||
splitContainer1.Panel1.ResumeLayout(false);
|
||||
splitContainer1.Panel2.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit();
|
||||
splitContainer1.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
|
||||
private System.Windows.Forms.ToolStrip toolStrip1;
|
||||
private System.Windows.Forms.SplitContainer splitContainer1;
|
||||
private System.Windows.Forms.ListBox listBoxCameras;
|
||||
private System.Windows.Forms.PropertyGrid propertyGrid1;
|
||||
private System.Windows.Forms.ToolStripButton toolBtnAdd;
|
||||
private System.Windows.Forms.ToolStripButton toolBtnRemove;
|
||||
private System.Windows.Forms.ToolStripButton toolBtnSet;
|
||||
}
|
||||
}
|
||||
193
Check.Main/UI/FrmCamConfig.cs
Normal file
193
Check.Main/UI/FrmCamConfig.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using Check.Main.Camera;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.Xml.Serialization;
|
||||
using WeifenLuo.WinFormsUI.Docking;
|
||||
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
public partial class FrmCamConfig : DockContent
|
||||
{
|
||||
// public List<CameraSettings> _settingsList = new List<CameraSettings>();
|
||||
public List<CameraSettings> _settingsList { get; private set; }
|
||||
private readonly string _configFilePath = Path.Combine(Application.StartupPath, "cameras.xml");
|
||||
public FrmCamConfig()
|
||||
{
|
||||
InitializeComponent();
|
||||
_settingsList = new List<CameraSettings>();
|
||||
LoadSettings();
|
||||
RefreshListBox();
|
||||
AdaptForDialogMode(); // 调整UI为对话框模式
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于从外部接收一个设置列表进行编辑。
|
||||
/// </summary>
|
||||
/// <param name="settingsToEdit">要编辑的相机设置列表</param>
|
||||
public FrmCamConfig(List<CameraSettings> settingsToEdit)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 创建一个现有列表的副本进行编辑,这样如果用户点“取消”,原始列表不会被影响
|
||||
_settingsList = settingsToEdit != null
|
||||
? settingsToEdit.Select(s => s.Clone() as CameraSettings).ToList() // 深度克隆更好,这里为简化用浅克隆
|
||||
: new List<CameraSettings>();
|
||||
|
||||
// 别忘了为列表中的每个对象重新订阅事件
|
||||
foreach (var setting in _settingsList)
|
||||
{
|
||||
setting.PropertyChanged += Setting_PropertyChanged;
|
||||
}
|
||||
|
||||
RefreshListBox();
|
||||
AdaptForDialogMode(); // 调整UI为对话框模式
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【新增的方法】
|
||||
/// 调整UI,使其更像一个对话框。
|
||||
/// </summary>
|
||||
private void AdaptForDialogMode()
|
||||
{
|
||||
// 将“应用配置”按钮改为“确定”
|
||||
toolBtnSet.Text = "确定";
|
||||
toolBtnSet.ToolTipText = "保存更改并关闭窗口";
|
||||
|
||||
// 新增一个“取消”按钮
|
||||
var toolBtnCancel = new ToolStripButton("取消")
|
||||
{
|
||||
DisplayStyle = ToolStripItemDisplayStyle.Text,
|
||||
Alignment = ToolStripItemAlignment.Right // 靠右对齐
|
||||
};
|
||||
toolBtnCancel.Click += (sender, e) => {
|
||||
this.DialogResult = DialogResult.Cancel;
|
||||
this.Close();
|
||||
};
|
||||
toolStrip1.Items.Add(toolBtnCancel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【新增的事件处理方法】
|
||||
/// 当PropertyGrid中的属性值被用户修改后触发此事件。
|
||||
/// </summary>
|
||||
|
||||
// 刷新左侧的相机列表
|
||||
private void RefreshListBox()
|
||||
{
|
||||
listBoxCameras.Items.Clear();
|
||||
foreach (var setting in _settingsList)
|
||||
{
|
||||
listBoxCameras.Items.Add(setting.Name);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 【全新的事件处理方法】
|
||||
/// 监听单个CameraSettings对象的属性变更通知。
|
||||
/// </summary>
|
||||
private void Setting_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
// 检查是不是“Name”属性发生了变化
|
||||
if (e.PropertyName == nameof(CameraSettings.Name))
|
||||
{
|
||||
// 'sender' 就是那个属性发生了变化的 CameraSettings 对象
|
||||
var changedSetting = sender as CameraSettings;
|
||||
if (changedSetting == null) return;
|
||||
|
||||
// 在 _settingsList 中找到这个对象的索引
|
||||
int index = _settingsList.IndexOf(changedSetting);
|
||||
|
||||
// 如果找到了,就更新ListBox中对应项的显示文本
|
||||
if (index >= 0)
|
||||
{
|
||||
// 使用Invoke确保线程安全,虽然在此场景下通常不是问题,但这是个好习惯
|
||||
this.Invoke(new Action(() => {
|
||||
listBoxCameras.Items[index] = changedSetting.Name;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void listBoxCameras_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (listBoxCameras.SelectedIndex >= 0)
|
||||
{
|
||||
propertyGrid1.SelectedObject = _settingsList[listBoxCameras.SelectedIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
propertyGrid1.SelectedObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void toolBtnAdd_Click(object sender, EventArgs e)
|
||||
{
|
||||
var newSetting = new CameraSettings { Name = $"Camera-{_settingsList.Count + 1}" };
|
||||
_settingsList.Add(newSetting);
|
||||
newSetting.PropertyChanged += Setting_PropertyChanged;
|
||||
RefreshListBox();
|
||||
listBoxCameras.SelectedIndex = listBoxCameras.Items.Count - 1;
|
||||
}
|
||||
|
||||
private void toolBtnRemove_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (listBoxCameras.SelectedIndex >= 0)
|
||||
{
|
||||
_settingsList.RemoveAt(listBoxCameras.SelectedIndex);
|
||||
propertyGrid1.SelectedObject = null;
|
||||
RefreshListBox();
|
||||
}
|
||||
}
|
||||
|
||||
private void toolBtnSet_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
// 加载XML配置文件
|
||||
private void LoadSettings()
|
||||
{
|
||||
if (!File.Exists(_configFilePath)) return;
|
||||
try
|
||||
{
|
||||
XmlSerializer serializer = new XmlSerializer(typeof(List<CameraSettings>));
|
||||
using (FileStream fs = new FileStream(_configFilePath, FileMode.Open))
|
||||
{
|
||||
_settingsList = (List<CameraSettings>)serializer.Deserialize(fs);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("加载相机配置失败: " + ex.Message);
|
||||
}
|
||||
foreach (var setting in _settingsList)
|
||||
{
|
||||
setting.PropertyChanged += Setting_PropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置到XML文件
|
||||
private void SaveSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
XmlSerializer serializer = new XmlSerializer(typeof(List<CameraSettings>));
|
||||
using (FileStream fs = new FileStream(_configFilePath, FileMode.Create))
|
||||
{
|
||||
serializer.Serialize(fs, _settingsList);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("保存相机配置失败: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
169
Check.Main/UI/FrmCamConfig.resx
Normal file
169
Check.Main/UI/FrmCamConfig.resx
Normal file
@@ -0,0 +1,169 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="toolBtnAdd.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG
|
||||
YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9
|
||||
0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw
|
||||
bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc
|
||||
VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9
|
||||
c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32
|
||||
Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo
|
||||
mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+
|
||||
kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D
|
||||
TgDQASA1MVpwzwAAAABJRU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolBtnRemove.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG
|
||||
YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9
|
||||
0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw
|
||||
bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc
|
||||
VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9
|
||||
c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32
|
||||
Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo
|
||||
mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+
|
||||
kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D
|
||||
TgDQASA1MVpwzwAAAABJRU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolBtnSet.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG
|
||||
YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9
|
||||
0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw
|
||||
bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc
|
||||
VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9
|
||||
c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32
|
||||
Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo
|
||||
mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+
|
||||
kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D
|
||||
TgDQASA1MVpwzwAAAABJRU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
</root>
|
||||
171
Check.Main/UI/FrmConfig.Designer.cs
generated
Normal file
171
Check.Main/UI/FrmConfig.Designer.cs
generated
Normal file
@@ -0,0 +1,171 @@
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
partial class FrmConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FrmConfig));
|
||||
propertyGrid1 = new PropertyGrid();
|
||||
toolStrip1 = new ToolStrip();
|
||||
toolBtnApply = new ToolStripButton();
|
||||
toolBtnAddProduct = new ToolStripButton();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
uiLabel1 = new Sunny.UI.UILabel();
|
||||
cmbProducts = new Sunny.UI.UIComboBox();
|
||||
toolBtnDeleteProduct = new ToolStripButton();
|
||||
toolStrip1.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// propertyGrid1
|
||||
//
|
||||
tableLayoutPanel1.SetColumnSpan(propertyGrid1, 2);
|
||||
propertyGrid1.Dock = DockStyle.Fill;
|
||||
propertyGrid1.Location = new Point(3, 70);
|
||||
propertyGrid1.Name = "propertyGrid1";
|
||||
propertyGrid1.Size = new Size(417, 345);
|
||||
propertyGrid1.TabIndex = 0;
|
||||
//
|
||||
// toolStrip1
|
||||
//
|
||||
tableLayoutPanel1.SetColumnSpan(toolStrip1, 2);
|
||||
toolStrip1.Dock = DockStyle.Fill;
|
||||
toolStrip1.Items.AddRange(new ToolStripItem[] { toolBtnApply, toolBtnAddProduct, toolBtnDeleteProduct });
|
||||
toolStrip1.Location = new Point(0, 0);
|
||||
toolStrip1.Name = "toolStrip1";
|
||||
toolStrip1.Size = new Size(423, 28);
|
||||
toolStrip1.TabIndex = 1;
|
||||
toolStrip1.Text = "toolStrip1";
|
||||
//
|
||||
// toolBtnApply
|
||||
//
|
||||
toolBtnApply.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
toolBtnApply.Image = (Image)resources.GetObject("toolBtnApply.Image");
|
||||
toolBtnApply.ImageTransparentColor = Color.Magenta;
|
||||
toolBtnApply.Name = "toolBtnApply";
|
||||
toolBtnApply.Size = new Size(60, 25);
|
||||
toolBtnApply.Text = "应用配置";
|
||||
toolBtnApply.Click += toolBtnApply_Click;
|
||||
//
|
||||
// toolBtnAddProduct
|
||||
//
|
||||
toolBtnAddProduct.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
toolBtnAddProduct.Image = (Image)resources.GetObject("toolBtnAddProduct.Image");
|
||||
toolBtnAddProduct.ImageTransparentColor = Color.Magenta;
|
||||
toolBtnAddProduct.Name = "toolBtnAddProduct";
|
||||
toolBtnAddProduct.Size = new Size(60, 25);
|
||||
toolBtnAddProduct.Text = "添加产品";
|
||||
toolBtnAddProduct.Click += toolBtnAddProduct_Click;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.ColumnCount = 2;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 23.4042549F));
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 76.59574F));
|
||||
tableLayoutPanel1.Controls.Add(propertyGrid1, 0, 2);
|
||||
tableLayoutPanel1.Controls.Add(toolStrip1, 0, 0);
|
||||
tableLayoutPanel1.Controls.Add(uiLabel1, 0, 1);
|
||||
tableLayoutPanel1.Controls.Add(cmbProducts, 1, 1);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(0, 0);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 3;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 6.69856453F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 9.330144F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 83.97129F));
|
||||
tableLayoutPanel1.Size = new Size(423, 418);
|
||||
tableLayoutPanel1.TabIndex = 2;
|
||||
//
|
||||
// uiLabel1
|
||||
//
|
||||
uiLabel1.Dock = DockStyle.Fill;
|
||||
uiLabel1.Font = new Font("宋体", 12F, FontStyle.Regular, GraphicsUnit.Point, 134);
|
||||
uiLabel1.ForeColor = Color.FromArgb(48, 48, 48);
|
||||
uiLabel1.Location = new Point(3, 28);
|
||||
uiLabel1.Name = "uiLabel1";
|
||||
uiLabel1.Size = new Size(93, 39);
|
||||
uiLabel1.TabIndex = 2;
|
||||
uiLabel1.Text = "当前产品";
|
||||
uiLabel1.TextAlign = ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// cmbProducts
|
||||
//
|
||||
cmbProducts.DataSource = null;
|
||||
cmbProducts.Dock = DockStyle.Left;
|
||||
cmbProducts.FillColor = Color.White;
|
||||
cmbProducts.Font = new Font("宋体", 12F, FontStyle.Regular, GraphicsUnit.Point, 134);
|
||||
cmbProducts.ItemHoverColor = Color.FromArgb(155, 200, 255);
|
||||
cmbProducts.ItemSelectForeColor = Color.FromArgb(235, 243, 255);
|
||||
cmbProducts.Location = new Point(103, 33);
|
||||
cmbProducts.Margin = new Padding(4, 5, 4, 5);
|
||||
cmbProducts.MinimumSize = new Size(63, 0);
|
||||
cmbProducts.Name = "cmbProducts";
|
||||
cmbProducts.Padding = new Padding(0, 0, 30, 2);
|
||||
cmbProducts.Size = new Size(225, 29);
|
||||
cmbProducts.SymbolSize = 24;
|
||||
cmbProducts.TabIndex = 3;
|
||||
cmbProducts.TextAlignment = ContentAlignment.MiddleLeft;
|
||||
cmbProducts.Watermark = "";
|
||||
cmbProducts.SelectedIndexChanged += cmbProducts_SelectedIndexChanged;
|
||||
//
|
||||
// toolBtnDeleteProduct
|
||||
//
|
||||
toolBtnDeleteProduct.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
toolBtnDeleteProduct.Image = (Image)resources.GetObject("toolBtnDeleteProduct.Image");
|
||||
toolBtnDeleteProduct.ImageTransparentColor = Color.Magenta;
|
||||
toolBtnDeleteProduct.Name = "toolBtnDeleteProduct";
|
||||
toolBtnDeleteProduct.Size = new Size(60, 25);
|
||||
toolBtnDeleteProduct.Text = "删除产品";
|
||||
toolBtnDeleteProduct.Click += toolBtnDeleteProduct_Click;
|
||||
//
|
||||
// FrmConfig
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(7F, 17F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(423, 418);
|
||||
Controls.Add(tableLayoutPanel1);
|
||||
Name = "FrmConfig";
|
||||
Text = "属性配置";
|
||||
toolStrip1.ResumeLayout(false);
|
||||
toolStrip1.PerformLayout();
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private PropertyGrid propertyGrid1;
|
||||
private ToolStrip toolStrip1;
|
||||
private ToolStripButton toolBtnApply;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private ToolStripButton toolBtnAddProduct;
|
||||
private Sunny.UI.UILabel uiLabel1;
|
||||
private Sunny.UI.UIComboBox cmbProducts;
|
||||
private ToolStripButton toolBtnDeleteProduct;
|
||||
}
|
||||
}
|
||||
163
Check.Main/UI/FrmConfig.cs
Normal file
163
Check.Main/UI/FrmConfig.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using Check.Main.Camera;
|
||||
using Check.Main.Common;
|
||||
using Check.Main.Dispatch;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.Xml.Serialization;
|
||||
using WeifenLuo.WinFormsUI.Docking;
|
||||
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
public partial class FrmConfig : DockContent
|
||||
{
|
||||
//private ProcessConfig _mainSettings=new ProcessConfig();
|
||||
//private readonly string _configFilePath = Path.Combine(Application.StartupPath, "main_config.xml");
|
||||
public FrmConfig()
|
||||
{
|
||||
InitializeComponent();
|
||||
ProductManager.OnProductChanged += UpdateUIForNewProduct;
|
||||
// 初始化UI
|
||||
InitializeProductComboBox();
|
||||
//LoadSettings(); // 窗体加载时,读取主配置文件
|
||||
propertyGrid1.SelectedObject = ProductManager.CurrentConfig; //_mainSettings; // 将配置对象绑定到属性网格
|
||||
propertyGrid1.PropertyValueChanged += (s, e) => { ProductManager.SaveCurrentProductConfig(); }; // 任何属性改变后自动保存
|
||||
}
|
||||
// 【新增】初始化产品下拉列表
|
||||
private void InitializeProductComboBox()
|
||||
{
|
||||
cmbProducts.DataSource = null; // 先清空数据源
|
||||
cmbProducts.DataSource = ProductManager.ProductList;
|
||||
cmbProducts.SelectedItem = ProductManager.CurrentProductName;
|
||||
}
|
||||
// 【新增】当产品管理器中的产品切换后,此方法被调用以更新整个UI
|
||||
private void UpdateUIForNewProduct()
|
||||
{
|
||||
// 使用Invoke确保线程安全
|
||||
if (this.InvokeRequired)
|
||||
{
|
||||
this.Invoke(new Action(UpdateUIForNewProduct));
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadSafeLogger.Log($"UI正在更新以显示产品 '{ProductManager.CurrentProductName}' 的配置。");
|
||||
// 更新下拉列表的显示(如果产品列表也变了)
|
||||
InitializeProductComboBox();
|
||||
|
||||
// 【关键】将PropertyGrid重新绑定到新产品的配置对象上
|
||||
propertyGrid1.SelectedObject = ProductManager.CurrentConfig;
|
||||
propertyGrid1.Refresh(); // 强制刷新UI
|
||||
}
|
||||
private void toolBtnApply_Click(object sender, EventArgs e)
|
||||
{
|
||||
ThreadSafeLogger.Log("用户点击“应用”按钮...");
|
||||
|
||||
// 1. 确保在应用前,任何可能未触发 PropertyValueChanged 的更改都被保存。
|
||||
ConfigurationManager.SaveChanges();
|
||||
|
||||
// 2. 获取主窗体引用
|
||||
var mainForm = this.DockPanel.FindForm() as FrmMain;
|
||||
mainForm?.ClearStatusStrip(); // 清理UI状态
|
||||
|
||||
// 3. 从 ConfigurationManager 获取最新的相机配置列表
|
||||
var cameraSettings = ConfigurationManager.GetCurrentConfig();//.CameraSettings;
|
||||
|
||||
if (cameraSettings != null)
|
||||
{
|
||||
// 使用全局配置来初始化或重新初始化相机。
|
||||
// CameraManager.Initialize 内部会首先调用 Shutdown,所以这是个完整的重启流程。
|
||||
CameraManager.Initialize(cameraSettings, mainForm);
|
||||
}
|
||||
|
||||
ThreadSafeLogger.Log("中央配置已成功应用到相机系统。");
|
||||
//MessageBox.Show("主配置已应用!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
private void cmbProducts_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (cmbProducts.SelectedItem is string selectedProduct && selectedProduct != ProductManager.CurrentProductName)
|
||||
{
|
||||
ProductManager.SwitchToProduct(selectedProduct);
|
||||
}
|
||||
}
|
||||
|
||||
private void toolBtnAddProduct_Click(object sender, EventArgs e)
|
||||
{
|
||||
// 使用一个简单的输入框来获取新产品名称
|
||||
string newName = ShowInputDialog("请输入新产品名称:");
|
||||
if (!string.IsNullOrWhiteSpace(newName))
|
||||
{
|
||||
if (ProductManager.AddNewProduct(newName))
|
||||
{
|
||||
ThreadSafeLogger.Log($"成功添加新产品: {newName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("产品名称无效或已存在!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// 一个简单的输入对话框辅助方法
|
||||
public static string ShowInputDialog(string text)
|
||||
{
|
||||
Form prompt = new Form()
|
||||
{
|
||||
Width = 300,
|
||||
Height = 150,
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog,
|
||||
Text = text,
|
||||
StartPosition = FormStartPosition.CenterScreen
|
||||
};
|
||||
Label textLabel = new Label() { Left = 50, Top = 20, Text = "产品名称:" };
|
||||
TextBox textBox = new TextBox() { Left = 50, Top = 50, Width = 200 };
|
||||
Button confirmation = new Button() { Text = "确定", Left = 150, Width = 100, Top = 80, DialogResult = DialogResult.OK };
|
||||
confirmation.Click += (sender, e) => { prompt.Close(); };
|
||||
prompt.Controls.Add(textBox);
|
||||
prompt.Controls.Add(confirmation);
|
||||
prompt.Controls.Add(textLabel);
|
||||
prompt.AcceptButton = confirmation;
|
||||
|
||||
return prompt.ShowDialog() == DialogResult.OK ? textBox.Text : "";
|
||||
}
|
||||
|
||||
private void toolBtnDeleteProduct_Click(object sender, EventArgs e)
|
||||
{
|
||||
// 1. 获取当前选中的产品
|
||||
string productToDelete = ProductManager.CurrentProductName;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(productToDelete) || productToDelete == "DefaultProduct")
|
||||
{
|
||||
MessageBox.Show("不能删除默认产品或无效产品!", "操作无效", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 弹出安全确认对话框
|
||||
var confirmResult = MessageBox.Show($"您确定要永久删除产品 '{productToDelete}' 吗?\n\n此操作不可恢复,将删除其所有相关配置!",
|
||||
"确认删除",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Warning);
|
||||
|
||||
if (confirmResult == DialogResult.Yes)
|
||||
{
|
||||
// 3. 调用核心删除逻辑
|
||||
if (ProductManager.DeleteProduct(productToDelete))
|
||||
{
|
||||
ThreadSafeLogger.Log($"用户成功删除了产品 '{productToDelete}'。");
|
||||
// 无需在这里手动更新UI,因为DeleteProduct方法内部会触发 OnProductChanged 事件,
|
||||
// 而我们的 UpdateUIForNewProduct 方法会自动响应这个事件并刷新整个界面。
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show($"删除产品 '{productToDelete}' 失败,请查看日志获取详细信息。", "删除失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
Check.Main/UI/FrmConfig.resx
Normal file
151
Check.Main/UI/FrmConfig.resx
Normal file
@@ -0,0 +1,151 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="toolBtnApply.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACRSURBVDhPY/j27dt/SjDYACcnJ7IwigEf3n8kCZNswPNb
|
||||
J/+f6DYF0yA+yQac6Db5f6hWCmwIiE+mC0wIu2DS2Vf/F1x6DefjwlgNyNr34r/0wkdgTMgQDAOQNRNj
|
||||
CIoBOg0rMTTDMLIhIHbriZeYBmDTiIxBGkEYxge5liQDsGGQqykyAISpZwAlmIEywMAAAAc1/Jwvt6sN
|
||||
AAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolBtnAddProduct.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACRSURBVDhPY/j27dt/SjDYACcnJ7IwigEf3n8kCZNswPNb
|
||||
J/+f6DYF0yA+yQac6Db5f6hWCmwIiE+mC0wIu2DS2Vf/F1x6DefjwlgNyNr34r/0wkdgTMgQDAOQNRNj
|
||||
CIoBOg0rMTTDMLIhIHbriZeYBmDTiIxBGkEYxge5liQDsGGQqykyAISpZwAlmIEywMAAAAc1/Jwvt6sN
|
||||
AAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolBtnDeleteProduct.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACRSURBVDhPY/j27dt/SjDYACcnJ7IwigEf3n8kCZNswPNb
|
||||
J/+f6DYF0yA+yQac6Db5f6hWCmwIiE+mC0wIu2DS2Vf/F1x6DefjwlgNyNr34r/0wkdgTMgQDAOQNRNj
|
||||
CIoBOg0rMTTDMLIhIHbriZeYBmDTiIxBGkEYxge5liQDsGGQqykyAISpZwAlmIEywMAAAAc1/Jwvt6sN
|
||||
AAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
</root>
|
||||
66
Check.Main/UI/FrmLog.Designer.cs
generated
Normal file
66
Check.Main/UI/FrmLog.Designer.cs
generated
Normal file
@@ -0,0 +1,66 @@
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
partial class FrmLog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.txtLog = new Sunny.UI.UIRichTextBox();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// txtLog
|
||||
//
|
||||
this.txtLog.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.txtLog.FillColor = System.Drawing.Color.White;
|
||||
this.txtLog.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.txtLog.Location = new System.Drawing.Point(0, 0);
|
||||
this.txtLog.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.txtLog.MinimumSize = new System.Drawing.Size(1, 1);
|
||||
this.txtLog.Name = "txtLog";
|
||||
this.txtLog.Padding = new System.Windows.Forms.Padding(2);
|
||||
this.txtLog.ReadOnly = true;
|
||||
this.txtLog.ShowText = false;
|
||||
this.txtLog.Size = new System.Drawing.Size(399, 299);
|
||||
this.txtLog.TabIndex = 0;
|
||||
this.txtLog.TextAlignment = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// FrmLog
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(399, 299);
|
||||
this.Controls.Add(this.txtLog);
|
||||
this.Name = "FrmLog";
|
||||
this.Text = "FrmLog";
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Sunny.UI.UIRichTextBox txtLog;
|
||||
}
|
||||
}
|
||||
35
Check.Main/UI/FrmLog.cs
Normal file
35
Check.Main/UI/FrmLog.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using WeifenLuo.WinFormsUI.Docking;
|
||||
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
public partial class FrmLog : DockContent
|
||||
{
|
||||
public FrmLog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
public void AddLog(string message)
|
||||
{
|
||||
if (txtLog.InvokeRequired)
|
||||
{
|
||||
txtLog.BeginInvoke(new Action(() => AddLog(message)));
|
||||
return;
|
||||
}
|
||||
if (txtLog.Lines.Length > 500)
|
||||
{
|
||||
txtLog.Clear();
|
||||
}
|
||||
txtLog.AppendText(message + Environment.NewLine);
|
||||
txtLog.ScrollToCaret();
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Check.Main/UI/FrmLog.resx
Normal file
120
Check.Main/UI/FrmLog.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
145
Check.Main/UI/ModelListEditor.Designer.cs
generated
Normal file
145
Check.Main/UI/ModelListEditor.Designer.cs
generated
Normal file
@@ -0,0 +1,145 @@
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
partial class ModelListEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ModelListEditor));
|
||||
propertyGrid1 = new PropertyGrid();
|
||||
listBoxModels = new ListBox();
|
||||
toolStrip1 = new ToolStrip();
|
||||
toolBtnAdd = new ToolStripButton();
|
||||
toolBtnRemove = new ToolStripButton();
|
||||
toolBtnSet = new ToolStripButton();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
toolStrip1.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// propertyGrid1
|
||||
//
|
||||
propertyGrid1.Dock = DockStyle.Fill;
|
||||
propertyGrid1.Location = new Point(245, 4);
|
||||
propertyGrid1.Margin = new Padding(4);
|
||||
propertyGrid1.Name = "propertyGrid1";
|
||||
propertyGrid1.Size = new Size(316, 400);
|
||||
propertyGrid1.TabIndex = 1;
|
||||
//
|
||||
// listBoxModels
|
||||
//
|
||||
listBoxModels.Dock = DockStyle.Fill;
|
||||
listBoxModels.FormattingEnabled = true;
|
||||
listBoxModels.ItemHeight = 17;
|
||||
listBoxModels.Location = new Point(4, 4);
|
||||
listBoxModels.Margin = new Padding(4);
|
||||
listBoxModels.Name = "listBoxModels";
|
||||
listBoxModels.Size = new Size(233, 400);
|
||||
listBoxModels.TabIndex = 2;
|
||||
listBoxModels.SelectedIndexChanged += listBoxModels_SelectedIndexChanged;
|
||||
//
|
||||
// toolStrip1
|
||||
//
|
||||
toolStrip1.Items.AddRange(new ToolStripItem[] { toolBtnAdd, toolBtnRemove, toolBtnSet });
|
||||
toolStrip1.Location = new Point(0, 0);
|
||||
toolStrip1.Name = "toolStrip1";
|
||||
toolStrip1.Size = new Size(565, 25);
|
||||
toolStrip1.TabIndex = 3;
|
||||
toolStrip1.Text = "toolStrip1";
|
||||
//
|
||||
// toolBtnAdd
|
||||
//
|
||||
toolBtnAdd.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
toolBtnAdd.Image = (Image)resources.GetObject("toolBtnAdd.Image");
|
||||
toolBtnAdd.ImageTransparentColor = Color.Magenta;
|
||||
toolBtnAdd.Name = "toolBtnAdd";
|
||||
toolBtnAdd.Size = new Size(36, 22);
|
||||
toolBtnAdd.Text = "添加";
|
||||
toolBtnAdd.Click += toolBtnAdd_Click;
|
||||
//
|
||||
// toolBtnRemove
|
||||
//
|
||||
toolBtnRemove.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
toolBtnRemove.Image = (Image)resources.GetObject("toolBtnRemove.Image");
|
||||
toolBtnRemove.ImageTransparentColor = Color.Magenta;
|
||||
toolBtnRemove.Name = "toolBtnRemove";
|
||||
toolBtnRemove.Size = new Size(36, 22);
|
||||
toolBtnRemove.Text = "移除";
|
||||
toolBtnRemove.Click += toolBtnRemove_Click;
|
||||
//
|
||||
// toolBtnSet
|
||||
//
|
||||
toolBtnSet.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
toolBtnSet.Image = (Image)resources.GetObject("toolBtnSet.Image");
|
||||
toolBtnSet.ImageTransparentColor = Color.Magenta;
|
||||
toolBtnSet.Name = "toolBtnSet";
|
||||
toolBtnSet.Size = new Size(60, 22);
|
||||
toolBtnSet.Text = "应用配置";
|
||||
toolBtnSet.Click += toolBtnSet_Click;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.ColumnCount = 2;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 42.65487F));
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 57.34513F));
|
||||
tableLayoutPanel1.Controls.Add(listBoxModels, 0, 0);
|
||||
tableLayoutPanel1.Controls.Add(propertyGrid1, 1, 0);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(0, 25);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 1;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel1.Size = new Size(565, 408);
|
||||
tableLayoutPanel1.TabIndex = 4;
|
||||
//
|
||||
// ModelListEditor
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(7F, 17F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(565, 433);
|
||||
Controls.Add(tableLayoutPanel1);
|
||||
Controls.Add(toolStrip1);
|
||||
Name = "ModelListEditor";
|
||||
Text = "模型配置";
|
||||
toolStrip1.ResumeLayout(false);
|
||||
toolStrip1.PerformLayout();
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private PropertyGrid propertyGrid1;
|
||||
private ListBox listBoxModels;
|
||||
private ToolStrip toolStrip1;
|
||||
private ToolStripButton toolBtnAdd;
|
||||
private ToolStripButton toolBtnRemove;
|
||||
private ToolStripButton toolBtnSet;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
}
|
||||
}
|
||||
175
Check.Main/UI/ModelListEditor.cs
Normal file
175
Check.Main/UI/ModelListEditor.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using Check.Main.Camera;
|
||||
using Check.Main.Infer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.Xml.Serialization;
|
||||
using WeifenLuo.WinFormsUI.Docking;
|
||||
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
public partial class ModelListEditor : DockContent
|
||||
{
|
||||
public List<ModelSettings> _settingsList { get; private set; }
|
||||
private readonly string _configFilePath = Path.Combine(Application.StartupPath, "model.xml");
|
||||
public ModelListEditor()
|
||||
{
|
||||
InitializeComponent();
|
||||
_settingsList = new List<ModelSettings>();
|
||||
LoadSettings();
|
||||
RefreshListBox();
|
||||
AdaptForDialogMode(); // 调整UI为对话框模式
|
||||
}
|
||||
/// <summary>
|
||||
/// 用于从外部接收一个设置列表进行编辑。
|
||||
/// </summary>
|
||||
/// <param name="settingsToEdit">要编辑的相机设置列表</param>
|
||||
public ModelListEditor(List<ModelSettings> settingsToEdit)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 创建一个现有列表的副本进行编辑,这样如果用户点“取消”,原始列表不会被影响
|
||||
_settingsList = settingsToEdit != null
|
||||
? settingsToEdit.Select(s => s.Clone() as ModelSettings).ToList() // 深度克隆更好,这里为简化用浅克隆
|
||||
: new List<ModelSettings>();
|
||||
|
||||
// 别忘了为列表中的每个对象重新订阅事件
|
||||
foreach (var setting in _settingsList)
|
||||
{
|
||||
setting.PropertyChanged += Setting_PropertyChanged;
|
||||
}
|
||||
|
||||
RefreshListBox();
|
||||
AdaptForDialogMode(); // 调整UI为对话框模式
|
||||
}
|
||||
/// <summary>
|
||||
/// 【新增的方法】
|
||||
/// 调整UI,使其更像一个对话框。
|
||||
/// </summary>
|
||||
private void AdaptForDialogMode()
|
||||
{
|
||||
// 将“应用配置”按钮改为“确定”
|
||||
toolBtnSet.Text = "确定";
|
||||
toolBtnSet.ToolTipText = "保存更改并关闭窗口";
|
||||
|
||||
// 新增一个“取消”按钮
|
||||
var toolBtnCancel = new ToolStripButton("取消")
|
||||
{
|
||||
DisplayStyle = ToolStripItemDisplayStyle.Text,
|
||||
Alignment = ToolStripItemAlignment.Right // 靠右对齐
|
||||
};
|
||||
toolBtnCancel.Click += (sender, e) =>
|
||||
{
|
||||
this.DialogResult = DialogResult.Cancel;
|
||||
this.Close();
|
||||
};
|
||||
toolStrip1.Items.Add(toolBtnCancel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【新增的事件处理方法】
|
||||
/// 当PropertyGrid中的属性值被用户修改后触发此事件。
|
||||
/// </summary>
|
||||
|
||||
// 刷新左侧的相机列表
|
||||
private void RefreshListBox()
|
||||
{
|
||||
listBoxModels.Items.Clear();
|
||||
foreach (var setting in _settingsList)
|
||||
{
|
||||
listBoxModels.Items.Add(setting.Name);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 【全新的事件处理方法】
|
||||
/// 监听单个CameraSettings对象的属性变更通知。
|
||||
/// </summary>
|
||||
private void Setting_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
// 检查是不是“Name”属性发生了变化
|
||||
if (e.PropertyName == nameof(CameraSettings.Name))
|
||||
{
|
||||
// 'sender' 就是那个属性发生了变化的 CameraSettings 对象
|
||||
var changedSetting = sender as ModelSettings;
|
||||
if (changedSetting == null) return;
|
||||
|
||||
// 在 _settingsList 中找到这个对象的索引
|
||||
int index = _settingsList.IndexOf(changedSetting);
|
||||
|
||||
// 如果找到了,就更新ListBox中对应项的显示文本
|
||||
if (index >= 0)
|
||||
{
|
||||
// 使用Invoke确保线程安全,虽然在此场景下通常不是问题,但这是个好习惯
|
||||
this.Invoke(new Action(() =>
|
||||
{
|
||||
listBoxModels.Items[index] = changedSetting.Name;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void listBoxModels_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (listBoxModels.SelectedIndex >= 0)
|
||||
{
|
||||
propertyGrid1.SelectedObject = _settingsList[listBoxModels.SelectedIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
propertyGrid1.SelectedObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void toolBtnAdd_Click(object sender, EventArgs e)
|
||||
{
|
||||
var newSetting = new ModelSettings { Name = $"model-{_settingsList.Count + 1}" };
|
||||
_settingsList.Add(newSetting);
|
||||
newSetting.PropertyChanged += Setting_PropertyChanged;
|
||||
RefreshListBox();
|
||||
listBoxModels.SelectedIndex = listBoxModels.Items.Count - 1;
|
||||
}
|
||||
|
||||
private void toolBtnRemove_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (listBoxModels.SelectedIndex >= 0)
|
||||
{
|
||||
_settingsList.RemoveAt(listBoxModels.SelectedIndex);
|
||||
propertyGrid1.SelectedObject = null;
|
||||
RefreshListBox();
|
||||
}
|
||||
}
|
||||
|
||||
private void toolBtnSet_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
// 加载XML配置文件
|
||||
private void LoadSettings()
|
||||
{
|
||||
if (!File.Exists(_configFilePath)) return;
|
||||
try
|
||||
{
|
||||
XmlSerializer serializer = new XmlSerializer(typeof(List<ModelSettings>));
|
||||
using (FileStream fs = new FileStream(_configFilePath, FileMode.Open))
|
||||
{
|
||||
_settingsList = (List<ModelSettings>)serializer.Deserialize(fs);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("加载相机配置失败: " + ex.Message);
|
||||
}
|
||||
foreach (var setting in _settingsList)
|
||||
{
|
||||
setting.PropertyChanged += Setting_PropertyChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
172
Check.Main/UI/ModelListEditor.resx
Normal file
172
Check.Main/UI/ModelListEditor.resx
Normal file
@@ -0,0 +1,172 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="toolBtnAdd.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG
|
||||
YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9
|
||||
0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw
|
||||
bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc
|
||||
VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9
|
||||
c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32
|
||||
Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo
|
||||
mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+
|
||||
kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D
|
||||
TgDQASA1MVpwzwAAAABJRU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolBtnRemove.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG
|
||||
YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9
|
||||
0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw
|
||||
bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc
|
||||
VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9
|
||||
c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32
|
||||
Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo
|
||||
mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+
|
||||
kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D
|
||||
TgDQASA1MVpwzwAAAABJRU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolBtnSet.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG
|
||||
YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9
|
||||
0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw
|
||||
bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc
|
||||
VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9
|
||||
c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32
|
||||
Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo
|
||||
mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+
|
||||
kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D
|
||||
TgDQASA1MVpwzwAAAABJRU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>37</value>
|
||||
</metadata>
|
||||
</root>
|
||||
381
Check.Main/UI/ZoomPictureBox.cs
Normal file
381
Check.Main/UI/ZoomPictureBox.cs
Normal file
@@ -0,0 +1,381 @@
|
||||
using OpenCvSharp;
|
||||
using OpenCvSharp.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Check.Main.UI
|
||||
{
|
||||
public class ZoomPictureBox : PictureBox, IDisposable
|
||||
{
|
||||
private float zoom = 1.0f;
|
||||
private System.Drawing.Point imageLocation;
|
||||
private Image image;
|
||||
private bool dragging = false;
|
||||
private System.Drawing.Point mouseDownPos;
|
||||
private System.Drawing.Point imageLocationOnMouseDown;
|
||||
|
||||
// 矩形绘制相关
|
||||
private bool isDrawingRect = false; // 绘制状态
|
||||
private Rectangle currentRect; // 当前绘制矩形
|
||||
private Rectangle storedRect; // 上次绘制并固定的矩形
|
||||
private bool hasStoredRect = false; // 是否存在固定矩形
|
||||
private System.Drawing.Point rectStartPoint;
|
||||
|
||||
// 一个私有的锁对象,专门用于保护对 'image' 字段的访问。
|
||||
private readonly object imageLock = new object();
|
||||
|
||||
private bool croppingEnabled = true;
|
||||
//private bool croppingEnabled = true;
|
||||
[Category("Behavior")]
|
||||
[Description("是否允许通过右键绘制矩形裁剪区域。设置为 false 则不再响应右键并清除已有矩形。")]
|
||||
public bool CroppingEnabled
|
||||
{
|
||||
get => croppingEnabled;
|
||||
set
|
||||
{
|
||||
croppingEnabled = value;
|
||||
// 禁用裁剪时清除所有矩形和裁剪结果
|
||||
if (!croppingEnabled)
|
||||
{
|
||||
isDrawingRect = false;
|
||||
hasStoredRect = false;
|
||||
LastCroppedMat?.Dispose();
|
||||
LastCroppedMat = null;
|
||||
}
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
// 裁剪完成事件,外部可订阅以接收 Mat
|
||||
public event Action<Mat> Cropped;
|
||||
|
||||
public Mat LastCroppedMat { get; private set; }
|
||||
|
||||
private Color backgroundFillColor = Color.LightSteelBlue;
|
||||
private Color rectangleColor = Color.Red;
|
||||
private int rectangleThickness = 2;
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("指定控件背景的填充颜色。使用 BackColor 属性同步更新。")]
|
||||
public Color BackgroundFillColor
|
||||
{
|
||||
get => backgroundFillColor;
|
||||
set
|
||||
{
|
||||
backgroundFillColor = value;
|
||||
base.BackColor = value; // 同步 WinForms BackColor
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("绘制矩形框的颜色。")]
|
||||
public Color RectangleColor { get => rectangleColor; set { rectangleColor = value; Invalidate(); } }
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("绘制矩形框的线宽。")]
|
||||
public int RectangleThickness { get => rectangleThickness; set { rectangleThickness = Math.Max(1, value); Invalidate(); } }
|
||||
|
||||
public ZoomPictureBox()
|
||||
{
|
||||
DoubleBuffered = true;
|
||||
ResizeRedraw = true;
|
||||
base.BackColor = backgroundFillColor;
|
||||
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true);
|
||||
MouseWheel += ZoomPictureBox_MouseWheel;
|
||||
MouseDown += ZoomPictureBox_MouseDown;
|
||||
MouseMove += ZoomPictureBox_MouseMove;
|
||||
MouseUp += ZoomPictureBox_MouseUp;
|
||||
DoubleClick += ZoomPictureBox_DoubleClick;
|
||||
}
|
||||
// 重写背景绘制,使用自定义背景色
|
||||
protected override void OnPaintBackground(PaintEventArgs e)
|
||||
{
|
||||
e.Graphics.Clear(backgroundFillColor);
|
||||
}
|
||||
//[Browsable(false)]
|
||||
//public new Image Image
|
||||
//{
|
||||
// get => image;
|
||||
// set
|
||||
// {
|
||||
// image = value;
|
||||
// zoom = 1.0f;
|
||||
// LastCroppedMat?.Dispose();
|
||||
// LastCroppedMat = null;
|
||||
// isDrawingRect = false;
|
||||
// hasStoredRect = false;
|
||||
// FitImage();
|
||||
// Invalidate();
|
||||
// }
|
||||
//}
|
||||
// 【关键优化】
|
||||
// 2. 重写 Control.Dispose 方法来释放我们自己的资源
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// 在这里释放托管和非托管资源
|
||||
// 清理事件订阅 (虽然在这个类里没有,但这是个好习惯)
|
||||
|
||||
// 释放我们持有的最后一个图像资源
|
||||
lock (imageLock)
|
||||
{
|
||||
this.image?.Dispose();
|
||||
this.image = null;
|
||||
}
|
||||
|
||||
// 释放我们持有的最后一个裁剪结果资源
|
||||
this.LastCroppedMat?.Dispose();
|
||||
this.LastCroppedMat = null;
|
||||
}
|
||||
|
||||
// 调用基类的 Dispose 方法来完成标准清理
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
// 重写 Image 属性,但不做任何额外操作,因为我们将通过一个新方法来更新它。
|
||||
[Browsable(false)]
|
||||
public new Image Image
|
||||
{
|
||||
get
|
||||
{
|
||||
// 在访问时也加锁,确保读取的是一个完整的对象
|
||||
lock (imageLock)
|
||||
{
|
||||
return image;
|
||||
}
|
||||
}
|
||||
// set 访问器可以保持原样,但我们不再直接使用它来更新图像
|
||||
private set
|
||||
{
|
||||
lock (imageLock)
|
||||
{
|
||||
image = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPaint(PaintEventArgs e)
|
||||
{
|
||||
base.OnPaint(e);
|
||||
var g = e.Graphics;
|
||||
//在绘制前获取锁,确保在绘制期间,image对象不会被其他线程替换或释放
|
||||
lock (imageLock)
|
||||
{
|
||||
if (image != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
int w = (int)(image.Width * zoom);
|
||||
int h = (int)(image.Height * zoom);
|
||||
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
g.DrawImage(image, new Rectangle(imageLocation, new System.Drawing.Size(w, h)));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
// 如果在绘制时仍然发生罕见的GDI+错误,静默忽略,避免程序崩溃。
|
||||
// 这通常意味着图像状态仍然存在问题,但UI不会因此卡死。
|
||||
}
|
||||
}
|
||||
}
|
||||
//if (image != null)
|
||||
//{
|
||||
// int w = (int)(image.Width * zoom);
|
||||
// int h = (int)(image.Height * zoom);
|
||||
// g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
// g.DrawImage(image, new Rectangle(imageLocation, new System.Drawing.Size(w, h)));
|
||||
//}
|
||||
// 仅当允许裁剪时才绘制矩形
|
||||
if (croppingEnabled)
|
||||
{
|
||||
using (var pen = new Pen(rectangleColor, rectangleThickness))
|
||||
{
|
||||
if (hasStoredRect) g.DrawRectangle(pen, storedRect);
|
||||
else if (isDrawingRect) g.DrawRectangle(pen, currentRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一个线程安全的、用于更新显示图像的公共方法。
|
||||
/// 这个方法将替换掉直接的 pictureBox.Image = ... 赋值。
|
||||
/// </summary>
|
||||
/// <param name="newImage">要显示的新图像。此方法会接管该对象的所有权。</param>
|
||||
public void SetImageThreadSafe(Image newImage)
|
||||
{
|
||||
Image oldImage = null;
|
||||
|
||||
lock (imageLock)
|
||||
{
|
||||
// 1. 保存旧图像的引用,以便在锁外部释放它
|
||||
oldImage = this.image;
|
||||
// 2. 将新图像赋值给成员字段
|
||||
this.image = newImage;
|
||||
}
|
||||
// 3. 【关键】在锁的外部释放旧图像,避免长时间持有锁
|
||||
oldImage?.Dispose();
|
||||
|
||||
// 4. 计算自适应并触发重绘
|
||||
FitImage();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void FitImage()
|
||||
{
|
||||
//if (image == null) return;
|
||||
//float sx = (float)ClientSize.Width / image.Width;
|
||||
//float sy = (float)ClientSize.Height / image.Height;
|
||||
//zoom = Math.Min(sx, sy);
|
||||
//CenterImage();
|
||||
//在访问image属性前获取锁
|
||||
lock (imageLock)
|
||||
{
|
||||
if (image == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
float sx = (float)ClientSize.Width / image.Width;
|
||||
float sy = (float)ClientSize.Height / image.Height;
|
||||
zoom = Math.Min(sx, sy);
|
||||
CenterImage();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
// 忽略错误,同 OnPaint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CenterImage()
|
||||
{
|
||||
if (image == null) return;
|
||||
int w = (int)(image.Width * zoom);
|
||||
int h = (int)(image.Height * zoom);
|
||||
imageLocation = new System.Drawing.Point((ClientSize.Width - w) / 2, (ClientSize.Height - h) / 2);
|
||||
}
|
||||
|
||||
private void ZoomPictureBox_MouseWheel(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (image == null) return;
|
||||
float oldZoom = zoom;
|
||||
zoom *= e.Delta > 0 ? 1.1f : 1 / 1.1f;
|
||||
zoom = Math.Max(0.1f, Math.Min(zoom, 100f));
|
||||
var m = e.Location;
|
||||
float ix = (m.X - imageLocation.X) / oldZoom;
|
||||
float iy = (m.Y - imageLocation.Y) / oldZoom;
|
||||
imageLocation = new System.Drawing.Point((int)(m.X - ix * zoom), (int)(m.Y - iy * zoom));
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void ZoomPictureBox_MouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (image == null) return;
|
||||
if (e.Button == MouseButtons.Left)
|
||||
{
|
||||
hasStoredRect = false;
|
||||
LastCroppedMat?.Dispose();
|
||||
LastCroppedMat = null;
|
||||
|
||||
dragging = true;
|
||||
mouseDownPos = e.Location;
|
||||
imageLocationOnMouseDown = imageLocation;
|
||||
Cursor = Cursors.Hand;
|
||||
}
|
||||
else if (e.Button == MouseButtons.Right && croppingEnabled)
|
||||
{
|
||||
// 清除上次固定矩形和裁剪
|
||||
hasStoredRect = false;
|
||||
LastCroppedMat?.Dispose();
|
||||
LastCroppedMat = null;
|
||||
|
||||
// 开始新绘制
|
||||
isDrawingRect = true;
|
||||
rectStartPoint = e.Location;
|
||||
currentRect = new Rectangle(e.Location, System.Drawing.Size.Empty);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void ZoomPictureBox_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (dragging)
|
||||
{
|
||||
var delta = new System.Drawing.Point(e.X - mouseDownPos.X, e.Y - mouseDownPos.Y);
|
||||
imageLocation = new System.Drawing.Point(imageLocationOnMouseDown.X + delta.X, imageLocationOnMouseDown.Y + delta.Y);
|
||||
Invalidate();
|
||||
}
|
||||
else if (isDrawingRect)
|
||||
{
|
||||
int x = Math.Min(rectStartPoint.X, e.X);
|
||||
int y = Math.Min(rectStartPoint.Y, e.Y);
|
||||
int w = Math.Abs(e.X - rectStartPoint.X);
|
||||
int h = Math.Abs(e.Y - rectStartPoint.Y);
|
||||
currentRect = new Rectangle(x, y, w, h);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void ZoomPictureBox_MouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Left && dragging)
|
||||
{
|
||||
dragging = false;
|
||||
Cursor = Cursors.Default;
|
||||
}
|
||||
else if (e.Button == MouseButtons.Right && isDrawingRect)
|
||||
{
|
||||
isDrawingRect = false;
|
||||
// 固定当前矩形框
|
||||
storedRect = currentRect;
|
||||
hasStoredRect = true;
|
||||
if (croppingEnabled && image is Bitmap bmp)
|
||||
{
|
||||
this.LastCroppedMat?.Dispose();
|
||||
this.LastCroppedMat = null;
|
||||
Mat srcMat = null;
|
||||
try
|
||||
{
|
||||
int ix = (int)((storedRect.X - imageLocation.X) / zoom);
|
||||
int iy = (int)((storedRect.Y - imageLocation.Y) / zoom);
|
||||
int iw = (int)(storedRect.Width / zoom);
|
||||
int ih = (int)(storedRect.Height / zoom);
|
||||
ix = Math.Max(0, ix);
|
||||
iy = Math.Max(0, iy);
|
||||
if (ix + iw > bmp.Width) iw = bmp.Width - ix;
|
||||
if (iy + ih > bmp.Height) ih = bmp.Height - iy;
|
||||
if (iw > 0 && ih > 0)
|
||||
{
|
||||
srcMat = BitmapConverter.ToMat(bmp);
|
||||
var roi = new Rect(ix, iy, iw, ih);
|
||||
LastCroppedMat = new Mat(srcMat, roi);
|
||||
Cropped?.Invoke(LastCroppedMat);
|
||||
bmp.Dispose();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 5.确保从Bitmap转换来的 srcMat 被释放
|
||||
srcMat?.Dispose();
|
||||
}
|
||||
}
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
private void ZoomPictureBox_DoubleClick(object sender, EventArgs e)
|
||||
{
|
||||
if (image == null) return;
|
||||
FitImage();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
CheckDevice.sln
Normal file
25
CheckDevice.sln
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36310.24 d17.14
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Check.Main", "Check.Main\Check.Main.csproj", "{3AEFC44E-BD2D-48A9-8C0D-BF4D429E1166}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{3AEFC44E-BD2D-48A9-8C0D-BF4D429E1166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3AEFC44E-BD2D-48A9-8C0D-BF4D429E1166}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3AEFC44E-BD2D-48A9-8C0D-BF4D429E1166}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3AEFC44E-BD2D-48A9-8C0D-BF4D429E1166}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {53CBAFF8-60D2-4B61-B76A-2C11D7DA949E}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Reference in New Issue
Block a user