10.20PLC+相机2.3视觉修改
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 | ||||||
							
								
								
									
										839
									
								
								Check.Main/Camera/CameraManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										839
									
								
								Check.Main/Camera/CameraManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,839 @@ | |||||||
|  | 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 | ||||||
|  |     { | ||||||
|  |         //1、相机与UI管理----管理每台相机对象和对应的图像显示窗口。 | ||||||
|  |         // 活动的相机实例字典(键值对),键 (string):使用相机的名称,值 (HikvisionCamera):存储实际的相机实例,{ get; } 创建了一个只读的公共属性 | ||||||
|  |         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>();//结果图像窗口 | ||||||
|  |  | ||||||
|  |         //2、多相机同步逻辑 | ||||||
|  |         // 【队列】一个产品需要多台相机拍完,才算完整。 | ||||||
|  |         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(); | ||||||
|  |  | ||||||
|  |         // 3、--- 新增:硬触发模拟器 --- | ||||||
|  |         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> | ||||||
|  |         /// 根据配置列表初始化或更新所有相机 | ||||||
|  |         /// 类似 PrepareAll,但更侧重于更新配置,会检查物理设备数量,不足时报 warning。 | ||||||
|  |         /// </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> | ||||||
|  |         /// 定时器触发事件。(定时器 OnHardwareTriggerTimerElapsed 会遍历相机,对处于软件触发模式的相机执行 SoftwareTrigger()。) | ||||||
|  |         /// </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("所有相机资源已释放。"); | ||||||
|  |  | ||||||
|  |         //} | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 停止所有相机 + 关闭窗口 + 清空字典 + 关闭检测协调器 + 销毁模型 | ||||||
|  |         /// </summary> | ||||||
|  |         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. 深度克隆 Bitmap → 分成 显示副本 和 处理副本。 | ||||||
|  |                 bmpForDisplay = DeepCloneBitmap(bmp, "Display"); | ||||||
|  |                 bmpForProcessing = DeepCloneBitmap(bmp, "Processing"); | ||||||
|  |             } | ||||||
|  |             finally | ||||||
|  |             { | ||||||
|  |                 // 2.无论克隆成功与否,都必须立即释放事件传递过来的原始bmp,防止泄漏。 | ||||||
|  |                 bmp?.Dispose();//空条件运算符 ?.-----在访问对象成员前检查对象是否为 null,如果 bmp 不是 null,则正常调用 Dispose() 方法,替代传统的 if (bmp != null) 检查 | ||||||
|  |             } | ||||||
|  |             // 分支 A: 显示副本 → 交给 FormImageDisplay.UpdateImage()。 | ||||||
|  |             if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow)) | ||||||
|  |             { | ||||||
|  |                 // displayWindow.UpdateImage 会处理线程安全问题 | ||||||
|  |                 displayWindow.UpdateImage(bmpForDisplay); | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 // 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本 | ||||||
|  |                 bmpForDisplay?.Dispose(); | ||||||
|  |             } | ||||||
|  |             // 分支 B: 处理副本 → 交给 DetectionCoordinator.EnqueueImage() 进行检测。 | ||||||
|  |             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 (e.ResultImage != null) | ||||||
|  |                 { | ||||||
|  |                     // UpdateImage 会负责克隆并显示,所以这里传递 bmp 即可 | ||||||
|  |                     resultDisplay.UpdateImage(e.ResultImage); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             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上显示警告。 | ||||||
|  |         //        } | ||||||
|  |         //    } | ||||||
|  |         //} | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										328
									
								
								Check.Main/Camera/CameraProcessor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								Check.Main/Camera/CameraProcessor.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,328 @@ | |||||||
|  | using Check.Main.Common; | ||||||
|  | using Check.Main.Infer; | ||||||
|  | using HalconTemplateMatch; | ||||||
|  | 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; | ||||||
|  | using OpenCvSharp; | ||||||
|  | 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)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 图像处理主循环 | ||||||
|  |         /// </summary> | ||||||
|  |         private void ProcessQueue() | ||||||
|  |         { | ||||||
|  |             //// 从模型管理器获取此线程专属的YOLO模型 | ||||||
|  |             //var yoloModel = YoloModelManager.GetModel(_modeId); | ||||||
|  |             //if (yoloModel == null) | ||||||
|  |             //{ | ||||||
|  |             //    ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型,处理线程已中止。"); | ||||||
|  |             //    return; // 如果没有模型,此线程无法工作 | ||||||
|  |             //} | ||||||
|  |  | ||||||
|  |             //训练阶段(相机2) | ||||||
|  |             var trainer = new LogoTemplateTrainer(); | ||||||
|  |  | ||||||
|  |             trainer.TrainAndSaveTemplates( | ||||||
|  |                 new List<string> | ||||||
|  |                 { | ||||||
|  |                 @"D:\HalconTemplateMatch\train2\logo1.bmp", | ||||||
|  |                 @"D:\HalconTemplateMatch\train2\logo2.bmp", | ||||||
|  |                 @"D:\HalconTemplateMatch\train2\logo3.bmp" | ||||||
|  |                 }, | ||||||
|  |                 @"D:\HalconTemplateMatch\model_2"); | ||||||
|  |  | ||||||
|  |             //训练阶段(相机3)9.25修改!! | ||||||
|  |             trainer.TrainAndSaveTemplates( | ||||||
|  |                new List<string> | ||||||
|  |                { | ||||||
|  |                 @"D:\HalconTemplateMatch\train3\3C_1.bmp", | ||||||
|  |                 @"D:\HalconTemplateMatch\train3\3C_2.bmp", | ||||||
|  |                 @"D:\HalconTemplateMatch\train3\3C_3.bmp" | ||||||
|  |                }, | ||||||
|  |                @"D:\HalconTemplateMatch\model_3"); | ||||||
|  |  | ||||||
|  |             if (trainer == null) | ||||||
|  |             { | ||||||
|  |                 ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 未加载模板,处理线程已中止。"); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |             while (_isRunning) | ||||||
|  |             { | ||||||
|  |                 try | ||||||
|  |                 { | ||||||
|  |                     // 阻塞式地从队列中取出图像,如果队列为空则等待 | ||||||
|  |                     ImageData data = _imageQueue.Take(); | ||||||
|  |                     using (data) | ||||||
|  |                     { | ||||||
|  |  | ||||||
|  |                         //using (var skImage = ConvertBitmapToSKImage(data.Image)) // 转换图像格式并管理其生命周期 | ||||||
|  |                         //{ | ||||||
|  |                         //    if (skImage == null) continue; | ||||||
|  |                         //    var predictions = yoloModel.RunObjectDetection(skImage); | ||||||
|  |                         //    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 | ||||||
|  |                         //            )); | ||||||
|  |                         //        } | ||||||
|  |                         //    } | ||||||
|  |                         //} | ||||||
|  |  | ||||||
|  |                         //***********************************使用Halcon模板匹配进行检测**************************************************** | ||||||
|  |                         if (data.Image == null) continue; | ||||||
|  |                         // 统一定义预测结果 | ||||||
|  |                         var matcher = new LogoMatcher(); | ||||||
|  |  | ||||||
|  |                         //9.25(增加一根据不同的相机编号调用不同的模型!) | ||||||
|  |                         string filepath = ""; | ||||||
|  |                         if (_cameraIndex == 2) | ||||||
|  |                         { | ||||||
|  |                             matcher.LoadTemplates(@"D:\HalconTemplateMatch\model_2"); | ||||||
|  |                             filepath = "D:\\HalconTemplateMatch\\train2"; | ||||||
|  |                         } | ||||||
|  |                         else if (_cameraIndex == 3) | ||||||
|  |                         { | ||||||
|  |                             matcher.LoadTemplates(@"D:\HalconTemplateMatch\model_3"); | ||||||
|  |                             filepath = "D:\\HalconTemplateMatch\\train3"; | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                         ////原bool返回的处理 | ||||||
|  |                         //bool found = matcher.FindLogo(data.Image); | ||||||
|  |                         //string result = found ? "OK":"NG"; | ||||||
|  |  | ||||||
|  |                         //double返回的处理 | ||||||
|  |                         double score = matcher.FindLogo(data.Image); | ||||||
|  |                           | ||||||
|  |  | ||||||
|  |                         //Mat cam = ProcessImg.BitmapToMat(data.Image); | ||||||
|  |                          | ||||||
|  |                         //score= ProcessImg.ProcessImagesInFolder(filepath,cam); | ||||||
|  |  | ||||||
|  |                         string result = (score > 0.5) ? "OK" : "NG"; | ||||||
|  |  | ||||||
|  |                         ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},结果: {result},得分: {score}"); | ||||||
|  |  | ||||||
|  |                         // 将处理结果交给协调器进行组装 | ||||||
|  |                         DetectionCoordinator.AssembleProduct(data, result); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |                         //给PLC的M90、M91写值(10.10) | ||||||
|  |                         if (FrmMain.PlcClient != null) | ||||||
|  |                         { | ||||||
|  |                             if (result == "OK") | ||||||
|  |                             { | ||||||
|  |                                 //吹气到合格框 | ||||||
|  |                                 FrmMain.PlcClient.WriteBool("90", true); // 写入M90为1 | ||||||
|  |                                 // 延时复位 | ||||||
|  |                                 Task.Run(async () => | ||||||
|  |                                 { | ||||||
|  |                                     //await Task.Delay(300); // 延时300毫秒,可根据实际气动时间调整 | ||||||
|  |                                     //await FrmMain.PlcClient.WriteAsync("M90", 0); | ||||||
|  |                                 }); | ||||||
|  |                             } | ||||||
|  |                             else | ||||||
|  |                             { | ||||||
|  |                                 //吹气到不合格框 | ||||||
|  |                                 FrmMain.PlcClient.WriteBool("91", true);// 写入M91为1 | ||||||
|  |                                 // 延时复位 | ||||||
|  |                                 Task.Run(async () => | ||||||
|  |                                 { | ||||||
|  |                                     //await Task.Delay(300); | ||||||
|  |                                     //await FrmMain.PlcClient.WriteAsync("M91", 0); | ||||||
|  |                                 }); | ||||||
|  |                             } | ||||||
|  |                             //完成一次检测进行刷新 | ||||||
|  |                             Thread.Sleep(2000); | ||||||
|  |                             FrmMain.PlcClient.WriteBool("90", false); //  | ||||||
|  |                             FrmMain.PlcClient.WriteBool("91", false); // 写入M90为1 | ||||||
|  |  | ||||||
|  |                         } | ||||||
|  |                         else | ||||||
|  |                         { | ||||||
|  |                             ThreadSafeLogger.Log("6,跳过写入。"); | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |                         // ③ 外部订阅事件 | ||||||
|  |                         OnProcessingCompleted?.Invoke( | ||||||
|  |                             this, | ||||||
|  |                             new ProcessingCompletedEventArgs | ||||||
|  |                             ( | ||||||
|  |                                 _cameraIndex, | ||||||
|  |                                 data.ProductId, | ||||||
|  |                                 data.Image // 原图传出去 | ||||||
|  |                             ) | ||||||
|  |                         ); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 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> | ||||||
|  |     /// 相机配置信息类CameraSettings,用于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 = "169.254.51.253"; | ||||||
|  |         private string _ipDeviceAddress = "169.254.51.45"; | ||||||
|  |         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(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										504
									
								
								Check.Main/Camera/HikvisionCamera.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										504
									
								
								Check.Main/Camera/HikvisionCamera.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,504 @@ | |||||||
|  | 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(); | ||||||
|  |                 // 设置为硬触发模式(Line0) | ||||||
|  |                 SetTriggerMode(false); | ||||||
|  |  | ||||||
|  |                 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(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										46
									
								
								Check.Main/Check.Main.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Check.Main/Check.Main.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|  |  | ||||||
|  |   <PropertyGroup> | ||||||
|  |     <OutputType>Exe</OutputType> | ||||||
|  |     <TargetFramework>net8.0-windows</TargetFramework> | ||||||
|  |     <Nullable>enable</Nullable> | ||||||
|  |     <UseWindowsForms>true</UseWindowsForms> | ||||||
|  |     <ImplicitUsings>enable</ImplicitUsings> | ||||||
|  |   </PropertyGroup> | ||||||
|  |   <PropertyGroup> | ||||||
|  |     <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> | ||||||
|  |     <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> | ||||||
|  |     <AllowUnsafeBlocks>True</AllowUnsafeBlocks> | ||||||
|  |   </PropertyGroup> | ||||||
|  |  | ||||||
|  |   <ItemGroup> | ||||||
|  |     <PackageReference Include="DockPanelSuite" Version="3.1.1" /> | ||||||
|  |     <PackageReference Include="DockPanelSuite.ThemeVS2015" Version="3.1.1" /> | ||||||
|  |     <PackageReference Include="HalconDotNet" Version="19.11.0" /> | ||||||
|  |     <PackageReference Include="HslCommunication" Version="7.0.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="halcondotnet"> | ||||||
|  |       <HintPath>C:\Program Files\MVTec\HALCON-10.0\bin\dotnet35\halcondotnet.dll</HintPath> | ||||||
|  |     </Reference> | ||||||
|  |     <Reference Include="hdevenginedotnet"> | ||||||
|  |       <HintPath>C:\Program Files\MVTec\HALCON-10.0\bin\dotnet35\hdevenginedotnet.dll</HintPath> | ||||||
|  |     </Reference> | ||||||
|  |     <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; } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								Check.Main/Common/ImageData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								Check.Main/Common/ImageData.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Concurrent; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace Check.Main.Common | ||||||
|  | { | ||||||
|  |     public class ImageData : IDisposable | ||||||
|  |     { | ||||||
|  |         public long ProductId { get; } | ||||||
|  |         public int CameraIndex { get; } | ||||||
|  |         public Bitmap Image { get; } | ||||||
|  |  | ||||||
|  |         public ImageData(long productId, int cameraIndex, Bitmap image) | ||||||
|  |         { | ||||||
|  |             ProductId = productId; | ||||||
|  |             CameraIndex = cameraIndex; | ||||||
|  |             Image = image; | ||||||
|  |         } | ||||||
|  |         public void Dispose() => Image?.Dispose(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // ProductAssembly.cs - 用于组装一个完整的产品 | ||||||
|  |     public class ProductAssembly : IDisposable | ||||||
|  |     { | ||||||
|  |         public long ProductId { get; } | ||||||
|  |         private readonly int _expectedCameraCount; | ||||||
|  |         // 使用 ConcurrentDictionary 保证线程安全 | ||||||
|  |         private readonly ConcurrentDictionary<int, string> _results = new ConcurrentDictionary<int, string>(); | ||||||
|  |  | ||||||
|  |         public ProductAssembly(long productId, int expectedCameraCount) | ||||||
|  |         { | ||||||
|  |             ProductId = productId; | ||||||
|  |             _expectedCameraCount = expectedCameraCount; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 添加一个相机的检测结果 | ||||||
|  |         public bool AddResult(int cameraIndex, string result) | ||||||
|  |         { | ||||||
|  |             _results.TryAdd(cameraIndex, result); | ||||||
|  |             return _results.Count == _expectedCameraCount; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 获取最终的聚合结果 | ||||||
|  |         public string GetFinalResult() | ||||||
|  |         { | ||||||
|  |             // 如果任何一个结果包含 "NG",则最终结果为 "NG" | ||||||
|  |             return _results.Values.Any(r => r.Contains("NG")) ? "NG" : "OK"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void Dispose() | ||||||
|  |         { | ||||||
|  |             // 目前只持有结果字符串,无需释放资源 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										290
									
								
								Check.Main/Common/LogoMatcher.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								Check.Main/Common/LogoMatcher.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,290 @@ | |||||||
|  | //using HalconDotNet; | ||||||
|  | //using System; | ||||||
|  | //using System.Collections.Generic; | ||||||
|  | //using System.Drawing.Imaging; | ||||||
|  | //using System.IO; | ||||||
|  |  | ||||||
|  | //namespace HalconTemplateMatch | ||||||
|  | //{ | ||||||
|  | //    public class LogoMatcher | ||||||
|  | //    { | ||||||
|  | //        private List<HTuple> modelHandles = new List<HTuple>(); | ||||||
|  |  | ||||||
|  | //        /// <summary> | ||||||
|  | //        /// 从文件加载多个模板 | ||||||
|  | //        /// </summary> | ||||||
|  | //        public void LoadTemplates(string dir) | ||||||
|  | //        { | ||||||
|  | //            foreach (var file in Directory.GetFiles(dir, "*.shm")) | ||||||
|  | //            { | ||||||
|  | //                HTuple modelID; | ||||||
|  | //                HOperatorSet.ReadShapeModel(file, out modelID); | ||||||
|  | //                modelHandles.Add(modelID); | ||||||
|  | //                Console.WriteLine($"加载模板: {file}"); | ||||||
|  | //            } | ||||||
|  | //        } | ||||||
|  |  | ||||||
|  | //        /// <summary> | ||||||
|  | //        /// 在测试图像中查找 Logo | ||||||
|  | //        /// </summary> | ||||||
|  | //        /// <returns>true = OK,false = NG</returns> | ||||||
|  | //        public bool FindLogo(string testImagePath) | ||||||
|  | //        { | ||||||
|  | //            HObject ho_TestImage; | ||||||
|  | //            HOperatorSet.ReadImage(out ho_TestImage, testImagePath); | ||||||
|  | //            HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage); | ||||||
|  |  | ||||||
|  | //            foreach (var modelID in modelHandles) | ||||||
|  | //            { | ||||||
|  | //                HOperatorSet.FindShapeModel( | ||||||
|  | //                    ho_TestImage, | ||||||
|  | //                    modelID, | ||||||
|  | //                    new HTuple(0).TupleRad(), | ||||||
|  | //                    new HTuple(360).TupleRad(), | ||||||
|  | //                    0.5,   // 最低分数 | ||||||
|  | //                    1,     // 最大匹配数 | ||||||
|  | //                    0.5,   // 重叠度 | ||||||
|  | //                    "least_squares", | ||||||
|  | //                    0, | ||||||
|  | //                    0.9, | ||||||
|  | //                    out HTuple row, | ||||||
|  | //                    out HTuple col, | ||||||
|  | //                    out HTuple angle, | ||||||
|  | //                    out HTuple score); | ||||||
|  |  | ||||||
|  | //                if (score.Length > 0 && score[0].D > 0.5) | ||||||
|  | //                { | ||||||
|  | //                    Console.WriteLine($"找到 Logo: Row={row[0]}, Col={col[0]}, Score={score[0]}"); | ||||||
|  | //                    return true; // 找到即返回成功 | ||||||
|  | //                } | ||||||
|  | //            } | ||||||
|  |  | ||||||
|  | //            return false; // 没找到 | ||||||
|  | //        } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | //        /// <summary> | ||||||
|  | //        /// 重载FindLogo函数(double返回) | ||||||
|  | //        /// </summary> | ||||||
|  | //        /// <param name="bmp"></param> | ||||||
|  | //        /// <returns></returns> | ||||||
|  | //        public double FindLogo(Bitmap bmp) | ||||||
|  | //        { | ||||||
|  | //            // Bitmap 转 HObject | ||||||
|  | //            HObject ho_TestImage; | ||||||
|  | //            Bitmap2HObject(bmp, out ho_TestImage); | ||||||
|  |  | ||||||
|  | //            HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage); | ||||||
|  |  | ||||||
|  | //            double bestScore = -1; | ||||||
|  |  | ||||||
|  | //            foreach (var modelID in modelHandles) | ||||||
|  | //            { | ||||||
|  | //                HOperatorSet.FindShapeModel( | ||||||
|  | //                    ho_TestImage, | ||||||
|  | //                    modelID, | ||||||
|  | //                    new HTuple(0).TupleRad(), | ||||||
|  | //                    new HTuple(360).TupleRad(), | ||||||
|  | //                    0.5,   // 最低分数 | ||||||
|  | //                    1,     // 最大匹配数 | ||||||
|  | //                    0.5,   // 重叠度 | ||||||
|  | //                    "least_squares", | ||||||
|  | //                    0, | ||||||
|  | //                    0.9, | ||||||
|  | //                    out HTuple row, | ||||||
|  | //                    out HTuple col, | ||||||
|  | //                    out HTuple angle, | ||||||
|  | //                    out HTuple score); | ||||||
|  |  | ||||||
|  | //                if (score.Length > 0 && score[0].D > bestScore) | ||||||
|  | //                { | ||||||
|  | //                    bestScore = score[0].D; | ||||||
|  | //                } | ||||||
|  | //            } | ||||||
|  |  | ||||||
|  | //            ho_TestImage.Dispose(); | ||||||
|  | //            return bestScore; // -1 = 没找到 | ||||||
|  | //        } | ||||||
|  |  | ||||||
|  | //        /// <summary> | ||||||
|  | //        /// Bitmap 转 Halcon HObject | ||||||
|  | //        /// </summary> | ||||||
|  | //        private void Bitmap2HObject(Bitmap bmp, out HObject hobj) | ||||||
|  | //        { | ||||||
|  | //            HOperatorSet.GenEmptyObj(out hobj); | ||||||
|  | //            Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); | ||||||
|  | //            BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); | ||||||
|  |  | ||||||
|  | //            try | ||||||
|  | //            { | ||||||
|  | //                HOperatorSet.GenImageInterleaved( | ||||||
|  | //                    out hobj, | ||||||
|  | //                    bmpData.Scan0, | ||||||
|  | //                    "bgr",  // Bitmap 默认是 BGR | ||||||
|  | //                    bmp.Width, | ||||||
|  | //                    bmp.Height, | ||||||
|  | //                    0, | ||||||
|  | //                    "byte", | ||||||
|  | //                    bmp.Width, | ||||||
|  | //                    bmp.Height, | ||||||
|  | //                    0, | ||||||
|  | //                    0, | ||||||
|  | //                    -1, | ||||||
|  | //                    0 | ||||||
|  | //                ); | ||||||
|  | //            } | ||||||
|  | //            finally | ||||||
|  | //            { | ||||||
|  | //                bmp.UnlockBits(bmpData); | ||||||
|  | //            } | ||||||
|  | //        } | ||||||
|  | //    } | ||||||
|  | //} | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | using HalconDotNet; | ||||||
|  | using NPOI.OpenXmlFormats.Vml; | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Drawing; | ||||||
|  | using System.Drawing.Imaging; | ||||||
|  | using System.IO; | ||||||
|  |  | ||||||
|  | namespace HalconTemplateMatch | ||||||
|  | { | ||||||
|  |     public class LogoMatcher | ||||||
|  |     { | ||||||
|  |         private readonly List<HTuple> modelHandles = new List<HTuple>(); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 从指定目录加载所有 .shm 模板文件 | ||||||
|  |         /// </summary> | ||||||
|  |         public void LoadTemplates(string dir) | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 string fullPath = Path.GetFullPath(dir); | ||||||
|  |  | ||||||
|  |                 if (!Directory.Exists(fullPath)) | ||||||
|  |                 { | ||||||
|  |                     Console.WriteLine($"[警告] 模型目录不存在: {fullPath}"); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 string[] modelFiles = Directory.GetFiles(fullPath, "*.shm", SearchOption.TopDirectoryOnly); | ||||||
|  |  | ||||||
|  |                 if (modelFiles.Length == 0) | ||||||
|  |                 { | ||||||
|  |                     Console.WriteLine($"[警告] 模型目录中没有任何 .shm 文件: {fullPath}"); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 foreach (var file in modelFiles) | ||||||
|  |                 { | ||||||
|  |                     try | ||||||
|  |                     { | ||||||
|  |                         HTuple modelID; | ||||||
|  |                         HOperatorSet.ReadShapeModel(file, out modelID); | ||||||
|  |                         modelHandles.Add(modelID); | ||||||
|  |                         Console.WriteLine($"[加载成功] 模板: {file}"); | ||||||
|  |                     } | ||||||
|  |                     catch (HOperatorException ex) | ||||||
|  |                     { | ||||||
|  |                         Console.WriteLine($"[错误] 无法加载模板 {file}: {ex.Message}"); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 if (modelHandles.Count == 0) | ||||||
|  |                 { | ||||||
|  |                     Console.WriteLine($"[警告] 没有成功加载任何模板文件。"); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 Console.WriteLine($"[异常] 加载模板目录出错: {ex.Message}"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 匹配并返回最高得分(double返回) | ||||||
|  |         /// </summary> | ||||||
|  |         public double FindLogo(Bitmap bmp) | ||||||
|  |         { | ||||||
|  |             if (modelHandles.Count == 0) | ||||||
|  |             { | ||||||
|  |                 Console.WriteLine("[警告] 尚未加载任何模板。"); | ||||||
|  |                 return -1; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Bitmap 转 Halcon 对象 | ||||||
|  |             HObject ho_TestImage; | ||||||
|  |             Bitmap2HObject(bmp, out ho_TestImage); | ||||||
|  |             HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage); | ||||||
|  |  | ||||||
|  |             double bestScore = -1; | ||||||
|  |  | ||||||
|  |             foreach (var modelID in modelHandles) | ||||||
|  |             { | ||||||
|  |                 try | ||||||
|  |                 { | ||||||
|  |                     HOperatorSet.FindScaledShapeModel( | ||||||
|  |     ho_TestImage, | ||||||
|  |     modelID, | ||||||
|  |     new HTuple(0).TupleRad(), | ||||||
|  |     new HTuple(360).TupleRad(), | ||||||
|  |     0.8, 1.2, | ||||||
|  |     0.5, 1, 0.5, | ||||||
|  |     "least_squares_high", | ||||||
|  |     0, 0.9, | ||||||
|  |     out HTuple row, out HTuple col, out HTuple angle, out HTuple scale, out HTuple score | ||||||
|  | ); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |                     if (score.Length > 0 && score[0].D > bestScore) | ||||||
|  |                         bestScore = score[0].D; | ||||||
|  |                 } | ||||||
|  |                 catch (HOperatorException ex) | ||||||
|  |                 { | ||||||
|  |                     Console.WriteLine($"[错误] 模板匹配失败: {ex.Message}"); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             ho_TestImage.Dispose(); | ||||||
|  |             return bestScore; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Bitmap 转 Halcon HObject | ||||||
|  |         /// </summary> | ||||||
|  |         private void Bitmap2HObject(Bitmap bmp, out HObject hobj) | ||||||
|  |         { | ||||||
|  |             HOperatorSet.GenEmptyObj(out hobj); | ||||||
|  |             Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); | ||||||
|  |             BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 HOperatorSet.GenImageInterleaved( | ||||||
|  |                     out hobj, | ||||||
|  |                     bmpData.Scan0, | ||||||
|  |                     "bgr", | ||||||
|  |                     bmp.Width, | ||||||
|  |                     bmp.Height, | ||||||
|  |                     0, | ||||||
|  |                     "byte", | ||||||
|  |                     bmp.Width, | ||||||
|  |                     bmp.Height, | ||||||
|  |                     0, | ||||||
|  |                     0, | ||||||
|  |                     -1, | ||||||
|  |                     0); | ||||||
|  |             } | ||||||
|  |             finally | ||||||
|  |             { | ||||||
|  |                 bmp.UnlockBits(bmpData); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								Check.Main/Common/LogoTemplateTrainer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								Check.Main/Common/LogoTemplateTrainer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | using HalconDotNet; | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.IO; | ||||||
|  |  | ||||||
|  | namespace HalconTemplateMatch | ||||||
|  | { | ||||||
|  |     public class LogoTemplateTrainer | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// 从多张样本图像生成模板并保存到文件 | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="imagePaths">训练图片路径集合</param> | ||||||
|  |         /// <param name="saveDir">模板保存目录</param> | ||||||
|  |         public void TrainAndSaveTemplates(List<string> imagePaths, string saveDir) | ||||||
|  |         { | ||||||
|  |             if (!Directory.Exists(saveDir)) | ||||||
|  |                 Directory.CreateDirectory(saveDir); | ||||||
|  |  | ||||||
|  |             int index = 0; | ||||||
|  |             foreach (var path in imagePaths) | ||||||
|  |             { | ||||||
|  |                 HObject ho_Image; | ||||||
|  |                 HOperatorSet.ReadImage(out ho_Image, path); | ||||||
|  |  | ||||||
|  |                 // 转灰度 | ||||||
|  |                 HOperatorSet.Rgb1ToGray(ho_Image, out HObject ho_Gray); | ||||||
|  |  | ||||||
|  |                 // 二值化 | ||||||
|  |                 HOperatorSet.Threshold(ho_Gray, out HObject ho_Region, 128, 255); | ||||||
|  |  | ||||||
|  |                 // 提取连通域 | ||||||
|  |                 HOperatorSet.Connection(ho_Region, out HObject ho_Connected); | ||||||
|  |                 HOperatorSet.SelectShapeStd(ho_Connected, out HObject ho_Selected, "max_area", 70); | ||||||
|  |  | ||||||
|  |                 // ROI 约束 | ||||||
|  |                 HOperatorSet.ReduceDomain(ho_Gray, ho_Selected, out HObject ho_ROI); | ||||||
|  |  | ||||||
|  |                 // 创建形状模板 | ||||||
|  |                 HTuple modelID; | ||||||
|  |                 HOperatorSet.CreateShapeModel( | ||||||
|  |                     ho_ROI, | ||||||
|  |                     "auto", | ||||||
|  |                     new HTuple(0).TupleRad(), | ||||||
|  |                     new HTuple(360).TupleRad(), | ||||||
|  |                     "auto", | ||||||
|  |                     "auto", | ||||||
|  |                     "use_polarity", | ||||||
|  |                     "auto", | ||||||
|  |                     "auto", | ||||||
|  |                     out modelID); | ||||||
|  |  | ||||||
|  |                 // 保存模板到文件 | ||||||
|  |                 string modelFile = Path.Combine(saveDir, $"logo_model_{index}.shm"); | ||||||
|  |                 HOperatorSet.WriteShapeModel(modelID, modelFile); | ||||||
|  |  | ||||||
|  |                 Console.WriteLine($"训练完成并保存模板: {modelFile}"); | ||||||
|  |                 index++; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										114
									
								
								Check.Main/Common/ModelSelectionConverter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								Check.Main/Common/ModelSelectionConverter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.ComponentModel; | ||||||
|  | using System.Globalization; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using static System.ComponentModel.TypeConverter; | ||||||
|  |  | ||||||
|  | namespace Check.Main.Common | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// 一个自定义的 TypeConverter,用于在 PropertyGrid 中为模型选择提供一个下拉列表。 | ||||||
|  |     /// </summary> | ||||||
|  |     public class ModelSelectionConverter : Int32Converter // 我们转换的是整数 (ModelId) | ||||||
|  |     { | ||||||
|  |         // 告诉 PropertyGrid,我们支持从一个标准值集合中进行选择 | ||||||
|  |         public override bool GetStandardValuesSupported(ITypeDescriptorContext context) | ||||||
|  |         { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 告诉 PropertyGrid,我们只允许用户从列表中选择,不允许手动输入 | ||||||
|  |         public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) | ||||||
|  |         { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 这是最核心的方法:提供标准值的列表 | ||||||
|  |         public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 // 从中央配置管理器获取所有已配置的模型 | ||||||
|  |                 var modelSettings = ConfigurationManager.GetCurrentConfig().ModelSettings; | ||||||
|  |  | ||||||
|  |                 if (modelSettings == null || modelSettings.Count == 0) | ||||||
|  |                 { | ||||||
|  |                     // 如果没有模型,返回一个包含默认值 "0"(代表“无”)的列表 | ||||||
|  |                     return new StandardValuesCollection(new[] { 0 }); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // 使用LINQ从模型列表中提取所有模型的ID | ||||||
|  |                 // .Prepend(0) 在列表开头添加一个 "0",代表“未选择”的选项 | ||||||
|  |                 var modelIds = modelSettings.Select(m => m.Id).Prepend(0).ToArray(); | ||||||
|  |  | ||||||
|  |                 return new StandardValuesCollection(modelIds); | ||||||
|  |             } | ||||||
|  |             catch | ||||||
|  |             { | ||||||
|  |                 // 如果在获取过程中发生任何异常,返回一个安全的默认值 | ||||||
|  |                 return new StandardValuesCollection(new[] { 0 }); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // --- 为了让下拉列表显示模型名称而不是ID,我们还需要重写以下方法 --- | ||||||
|  |  | ||||||
|  |         public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) | ||||||
|  |         { | ||||||
|  |             return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) | ||||||
|  |         { | ||||||
|  |             return destinationType == typeof(string) || base.CanConvertTo(context, destinationType); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 将模型ID转换为其对应的名称字符串,以便在UI上显示 | ||||||
|  |         public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) | ||||||
|  |         { | ||||||
|  |             if (destinationType == typeof(string) && value is int modelId) | ||||||
|  |             { | ||||||
|  |                 if (modelId == 0) | ||||||
|  |                 { | ||||||
|  |                     return "未选择"; // 对ID为0的特殊处理 | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // 查找ID对应的模型名称 | ||||||
|  |                 var model = ConfigurationManager.GetCurrentConfig() | ||||||
|  |                                                .ModelSettings? | ||||||
|  |                                                .FirstOrDefault(m => m.Id == modelId); | ||||||
|  |  | ||||||
|  |                 // 返回 "名称 (ID)" 的格式,更清晰 | ||||||
|  |                 return model != null ? $"{model.Name} (ID: {model.Id})" : "未知模型"; | ||||||
|  |             } | ||||||
|  |             return base.ConvertTo(context, culture, value, destinationType); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 将用户在UI上选择的名称字符串,转换回其对应的模型ID,以便保存 | ||||||
|  |         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) | ||||||
|  |         { | ||||||
|  |             if (value is string displayString) | ||||||
|  |             { | ||||||
|  |                 if (displayString == "未选择") | ||||||
|  |                 { | ||||||
|  |                     return 0; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // 从 "名称 (ID: X)" 格式中解析出ID | ||||||
|  |                 if (displayString.Contains("(ID: ") && displayString.EndsWith(")")) | ||||||
|  |                 { | ||||||
|  |                     var startIndex = displayString.IndexOf("(ID: ") + 5; | ||||||
|  |                     var endIndex = displayString.Length - 1; | ||||||
|  |                     var idString = displayString.Substring(startIndex, endIndex - startIndex); | ||||||
|  |                     if (int.TryParse(idString, out int modelId)) | ||||||
|  |                     { | ||||||
|  |                         return modelId; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return base.ConvertFrom(context, culture, value); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										108
									
								
								Check.Main/Common/PLC_Control_Base.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								Check.Main/Common/PLC_Control_Base.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | using HslCommunication; | ||||||
|  | using HslCommunication.ModBus; | ||||||
|  | using System; | ||||||
|  | using System.Net.Sockets; | ||||||
|  |  | ||||||
|  | // ModbusTcp读写服务类,线程安全,互斥锁 | ||||||
|  | namespace Check.Main.Common | ||||||
|  | { | ||||||
|  |     public class ModbusTcpService | ||||||
|  |     { | ||||||
|  |         private readonly TcpClient _tcpClient = new();// | ||||||
|  |         private readonly ModbusTcpNet _plc; | ||||||
|  |         private readonly object _lock = new(); | ||||||
|  |         public bool IsConnected => _tcpClient != null && _tcpClient.Connected;// | ||||||
|  |  | ||||||
|  |         public ModbusTcpService(string ip, int port = 502, byte station = 1) | ||||||
|  |         { | ||||||
|  |             _plc = new ModbusTcpNet(ip, port, station) | ||||||
|  |             { | ||||||
|  |                 // CDAB 格式:PLC的不同品牌的modelbus TCP存在正反高低位。如果读写异常。删掉或者补充下面这句函数进行修复 | ||||||
|  |                 DataFormat = HslCommunication.Core.DataFormat.CDAB | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 读取 32 位整数 | ||||||
|  |         public OperateResult<int> ReadInt32(string address) | ||||||
|  |         { | ||||||
|  |             lock (_lock) | ||||||
|  |             { | ||||||
|  |                 return _plc.ReadInt32(address); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 写入 32 位整数(int) | ||||||
|  |         public OperateResult WriteInt32(string address, int value) | ||||||
|  |         { | ||||||
|  |             lock (_lock) | ||||||
|  |             { | ||||||
|  |                 return _plc.Write(address, value); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 读取 16 位整数(short) | ||||||
|  |         public OperateResult<short> ReadInt16(string address) | ||||||
|  |         { | ||||||
|  |             lock (_lock) | ||||||
|  |             { | ||||||
|  |                 return _plc.ReadInt16(address); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 写入 16 位整数(short) | ||||||
|  |         public OperateResult WriteInt16(string address, short value) | ||||||
|  |         { | ||||||
|  |             lock (_lock) | ||||||
|  |             { | ||||||
|  |                 return _plc.Write(address, value); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         // 读取布尔量 | ||||||
|  |         public OperateResult<bool> ReadBool(string address) | ||||||
|  |         { | ||||||
|  |             lock (_lock) | ||||||
|  |             { | ||||||
|  |                 return _plc.ReadBool(address); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 写入布尔量 | ||||||
|  |         public OperateResult WriteBool(string address, bool value) | ||||||
|  |         { | ||||||
|  |             lock (_lock) | ||||||
|  |             { | ||||||
|  |                 return _plc.Write(address, value); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 读取浮点数(float) | ||||||
|  |         public OperateResult<float> ReadFloat(string address) | ||||||
|  |         { | ||||||
|  |             lock (_lock) | ||||||
|  |             { | ||||||
|  |                 return _plc.ReadFloat(address); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 写入浮点数(float) | ||||||
|  |         public OperateResult WriteFloat(string address, float value) | ||||||
|  |         { | ||||||
|  |             lock (_lock) | ||||||
|  |             { | ||||||
|  |                 return _plc.Write(address, value); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 关闭连接 | ||||||
|  |         public void Close() | ||||||
|  |         { | ||||||
|  |             lock (_lock) | ||||||
|  |             { | ||||||
|  |                 _plc.ConnectClose(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										161
									
								
								Check.Main/Common/ProcessConfig.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								Check.Main/Common/ProcessConfig.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | |||||||
|  | using Check.Main.Camera; | ||||||
|  | using Check.Main.Infer; | ||||||
|  | using Check.Main.UI; | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.ComponentModel; | ||||||
|  | using System.Drawing.Design; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using System.Windows.Forms.Design; | ||||||
|  |  | ||||||
|  | namespace Check.Main.Common | ||||||
|  | { | ||||||
|  |     public class ProcessConfig | ||||||
|  |     { | ||||||
|  |         private string _logPath = Path.Combine(Application.StartupPath, "Logs"); | ||||||
|  |         private List<CameraSettings> _cameraSettings = new List<CameraSettings>(); | ||||||
|  |         private List<ModelSettings> _modelaSettings = new List<ModelSettings>(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         [Category("常规设置"), DisplayName("日志路径"), Description("设置日志文件的保存目录。")] | ||||||
|  |         [Editor(typeof(FolderNameEditor), typeof(UITypeEditor))] // 使用内置的文件夹选择器 | ||||||
|  |         public string LogPath | ||||||
|  |         { | ||||||
|  |             get => _logPath; | ||||||
|  |             set => _logPath = value; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         [Category("核心配置"), DisplayName("相机配置"), Description("点击 '...' 按钮来配置所有相机。")] | ||||||
|  |         [Editor(typeof(CameraConfigEditor), typeof(UITypeEditor))] // 【关键】指定使用我们自定义的编辑器 | ||||||
|  |         [TypeConverter(typeof(CameraConfigConverter))]           // 【关键】指定使用我们自定义的类型转换器来显示文本 | ||||||
|  |         public List<CameraSettings> CameraSettings | ||||||
|  |         { | ||||||
|  |             get => _cameraSettings; | ||||||
|  |             set => _cameraSettings = value; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         [Category("核心配置"), DisplayName("模型配置"), Description("点击 '...' 按钮来配置所有模型。")] | ||||||
|  |         [Editor(typeof(ModelConfigEditor), typeof(UITypeEditor))] // 【关键】指定使用我们自定义的编辑器 | ||||||
|  |         [TypeConverter(typeof(ModelConfigConverter))]           // 【关键】指定使用我们自定义的类型转换器来显示文本 | ||||||
|  |         public List<ModelSettings> ModelSettings | ||||||
|  |         { | ||||||
|  |             get => _modelaSettings; | ||||||
|  |             set => _modelaSettings = value; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     /// <summary> | ||||||
|  |     /// 自定义TypeConverter,用于在PropertyGrid中自定义显示相机列表的文本。 | ||||||
|  |     /// </summary> | ||||||
|  |     public class CameraConfigConverter : TypeConverter | ||||||
|  |     { | ||||||
|  |         public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) | ||||||
|  |         { | ||||||
|  |             // 声明可以将值转换为字符串 | ||||||
|  |             return destinationType == typeof(string) || base.CanConvertTo(context, destinationType); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) | ||||||
|  |         { | ||||||
|  |             // 如果目标类型是字符串,并且值是相机设置列表 | ||||||
|  |             if (destinationType == typeof(string) && value is List<CameraSettings> list) | ||||||
|  |             { | ||||||
|  |                 // 返回自定义的描述性文本 | ||||||
|  |                 return $"已配置 {list.Count} 个相机"; | ||||||
|  |             } | ||||||
|  |             return base.ConvertTo(context, culture, value, destinationType); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// 自定义TypeConverter,用于在PropertyGrid中自定义显示相机列表的文本。 | ||||||
|  |     /// </summary> | ||||||
|  |     public class ModelConfigConverter : TypeConverter | ||||||
|  |     { | ||||||
|  |         public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) | ||||||
|  |         { | ||||||
|  |             // 声明可以将值转换为字符串 | ||||||
|  |             return destinationType == typeof(string) || base.CanConvertTo(context, destinationType); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) | ||||||
|  |         { | ||||||
|  |             // 如果目标类型是字符串,并且值是相机设置列表 | ||||||
|  |             if (destinationType == typeof(string) && value is List<ModelSettings> list) | ||||||
|  |             { | ||||||
|  |                 // 返回自定义的描述性文本 | ||||||
|  |                 return $"已配置 {list.Count} 个模型"; | ||||||
|  |             } | ||||||
|  |             return base.ConvertTo(context, culture, value, destinationType); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// 自定义UITypeEditor,用于在点击 "..." 时打开相机配置窗体。 | ||||||
|  |     /// </summary> | ||||||
|  |     public class CameraConfigEditor : UITypeEditor | ||||||
|  |     { | ||||||
|  |         // 1. 设置编辑样式为模态对话框 | ||||||
|  |         public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) | ||||||
|  |         { | ||||||
|  |             return UITypeEditorEditStyle.Modal; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 2. 重写编辑方法 | ||||||
|  |         public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) | ||||||
|  |         { | ||||||
|  |             // 'value' 就是当前属性的值,即 List<CameraSettings> | ||||||
|  |             var settingsList = value as List<CameraSettings>; | ||||||
|  |  | ||||||
|  |             // 使用 FrmCamConfig 作为对话框进行编辑 | ||||||
|  |             // 我们需要给 FrmCamConfig 添加一个新的构造函数 | ||||||
|  |             using (var camConfigForm = new FrmCamConfig(settingsList)) | ||||||
|  |             { | ||||||
|  |                 // 以对话框形式显示窗体 | ||||||
|  |                 if (camConfigForm.ShowDialog() == DialogResult.OK) | ||||||
|  |                 { | ||||||
|  |                     // 如果用户点击了“确定”,返回修改后的新列表 | ||||||
|  |                     return camConfigForm._settingsList; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 如果用户点击了“取消”或直接关闭了窗口,返回原始值,不做任何更改 | ||||||
|  |             return value; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     /// <summary> | ||||||
|  |     /// 【新增】自定义UITypeEditor,用于在点击 "..." 时打开模型配置窗体。 | ||||||
|  |     /// </summary> | ||||||
|  |     public class ModelConfigEditor : UITypeEditor | ||||||
|  |     { | ||||||
|  |         // 1. 设置编辑样式为模态对话框 | ||||||
|  |         public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) | ||||||
|  |         { | ||||||
|  |             return UITypeEditorEditStyle.Modal; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 2. 重写编辑方法 | ||||||
|  |         public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) | ||||||
|  |         { | ||||||
|  |             // 'value' 就是当前属性的值,即 List<CameraSettings> | ||||||
|  |             var settingsList = value as List<ModelSettings>; | ||||||
|  |  | ||||||
|  |             // 使用ModelEditor 作为对话框进行编辑 | ||||||
|  |             // 我们需要给 FrmCamConfig 添加一个新的构造函数 | ||||||
|  |             using (var modelConfigForm = new ModelListEditor(settingsList)) | ||||||
|  |             { | ||||||
|  |                 // 以对话框形式显示窗体 | ||||||
|  |                 if (modelConfigForm.ShowDialog() == DialogResult.OK) | ||||||
|  |                 { | ||||||
|  |                     // 如果用户点击了“确定”,返回修改后的新列表 | ||||||
|  |                     return modelConfigForm._settingsList; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 如果用户点击了“取消”或直接关闭了窗口,返回原始值,不做任何更改 | ||||||
|  |             return value; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								Check.Main/Common/ProcessingCompletedEventArgs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								Check.Main/Common/ProcessingCompletedEventArgs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | using SkiaSharp; | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace Check.Main.Common | ||||||
|  | { | ||||||
|  |     public class ProcessingCompletedEventArgs : EventArgs, IDisposable | ||||||
|  |     { | ||||||
|  |         public int CameraIndex { get; } | ||||||
|  |         public long ProductId { get; } | ||||||
|  |         public Bitmap ResultImage { get; } // 原来是SKImage ResultImage  | ||||||
|  |  | ||||||
|  |         public ProcessingCompletedEventArgs(int cameraIndex, long productId, Bitmap resultImage)//原来是SKImage resultImage | ||||||
|  |         { | ||||||
|  |             CameraIndex = cameraIndex; | ||||||
|  |             ProductId = productId; | ||||||
|  |             ResultImage = resultImage; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 实现 IDisposable 以确保SKImage资源被释放 | ||||||
|  |         public void Dispose() | ||||||
|  |         { | ||||||
|  |             ResultImage?.Dispose(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										80
									
								
								Check.Main/Common/StaticMethod.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								Check.Main/Common/StaticMethod.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.ComponentModel; | ||||||
|  | using System.Globalization; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Reflection; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace Check.Main.Common | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// 一个自定义的类型转换器,用于让PropertyGrid等控件显示枚举成员的Description特性,而不是成员名。 | ||||||
|  |     /// </summary> | ||||||
|  |     public class EnumDescriptionTypeConverter : EnumConverter | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// 构造函数,传入枚举类型。 | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="type">要进行转换的枚举类型。</param> | ||||||
|  |         public EnumDescriptionTypeConverter(Type type) : base(type) | ||||||
|  |         { | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 重写此方法,将枚举值转换为其描述文本。 | ||||||
|  |         /// 这是从 "数据" -> "UI显示" 的过程。 | ||||||
|  |         /// </summary> | ||||||
|  |         public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) | ||||||
|  |         { | ||||||
|  |             // 如果目标类型是字符串,并且我们有一个有效的枚举值 | ||||||
|  |             if (destinationType == typeof(string) && value != null) | ||||||
|  |             { | ||||||
|  |                 // 获取枚举值的字段信息 | ||||||
|  |                 FieldInfo fi = value.GetType().GetField(value.ToString()); | ||||||
|  |                 if (fi != null) | ||||||
|  |                 { | ||||||
|  |                     // 查找该字段上的DescriptionAttribute | ||||||
|  |                     var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); | ||||||
|  |  | ||||||
|  |                     // 如果找到了Description特性,就返回它的描述文本;否则,返回基类的默认行为(即成员名) | ||||||
|  |                     return ((attributes.Length > 0) && (!String.IsNullOrEmpty(attributes[0].Description))) | ||||||
|  |                            ? attributes[0].Description | ||||||
|  |                            : value.ToString(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 对于其他所有情况,使用基类的默认转换逻辑 | ||||||
|  |             return base.ConvertTo(context, culture, value, destinationType); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 重写此方法,将描述文本转换回其对应的枚举值。 | ||||||
|  |         /// 这是从 "UI显示" -> "数据" 的过程。 | ||||||
|  |         /// </summary> | ||||||
|  |         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) | ||||||
|  |         { | ||||||
|  |             // 如果传入的值是字符串 | ||||||
|  |             if (value is string) | ||||||
|  |             { | ||||||
|  |                 // 遍历枚举类型的所有成员 | ||||||
|  |                 foreach (FieldInfo fi in EnumType.GetFields()) | ||||||
|  |                 { | ||||||
|  |                     // 查找该成员上的DescriptionAttribute | ||||||
|  |                     var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); | ||||||
|  |  | ||||||
|  |                     // 如果找到了Description,并且它的描述文本与传入的字符串匹配 | ||||||
|  |                     if ((attributes.Length > 0) && (attributes[0].Description == (string)value)) | ||||||
|  |                     { | ||||||
|  |                         // 返回这个字段(成员)对应的枚举值 | ||||||
|  |                         return Enum.Parse(EnumType, fi.Name); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 如果没有找到匹配的描述,或者传入的不是字符串,使用基类的默认转换逻辑 | ||||||
|  |             return base.ConvertFrom(context, culture, value); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										71
									
								
								Check.Main/Common/StatisticsData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								Check.Main/Common/StatisticsData.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.ComponentModel; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace Check.Main.Common | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// 封装了所有生产统计数据的模型。 | ||||||
|  |     /// 实现了INotifyPropertyChanged,未来可用于数据绑定。 | ||||||
|  |     /// </summary> | ||||||
|  |     public class StatisticsData : INotifyPropertyChanged | ||||||
|  |     { | ||||||
|  |         private int _goodCount; | ||||||
|  |         private int _ngCount; | ||||||
|  |  | ||||||
|  |         public int GoodCount | ||||||
|  |         { | ||||||
|  |             get => _goodCount; | ||||||
|  |             private set { _goodCount = value; OnPropertyChanged(nameof(GoodCount)); OnPropertyChanged(nameof(TotalCount)); OnPropertyChanged(nameof(YieldRate)); } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public int NgCount | ||||||
|  |         { | ||||||
|  |             get => _ngCount; | ||||||
|  |             private set { _ngCount = value; OnPropertyChanged(nameof(NgCount)); OnPropertyChanged(nameof(TotalCount)); OnPropertyChanged(nameof(YieldRate)); } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public int TotalCount => GoodCount + NgCount; | ||||||
|  |  | ||||||
|  |         public double YieldRate => TotalCount == 0 ? 0 : (double)GoodCount / TotalCount; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 根据检测结果更新统计数据。 | ||||||
|  |         /// </summary> | ||||||
|  |         public void UpdateWithResult(bool isOk) | ||||||
|  |         { | ||||||
|  |             if (isOk) GoodCount++; | ||||||
|  |             else NgCount++; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 重置所有统计数据。 | ||||||
|  |         /// </summary> | ||||||
|  |         public void Reset() | ||||||
|  |         { | ||||||
|  |             GoodCount = 0; | ||||||
|  |             NgCount = 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public event PropertyChangedEventHandler PropertyChanged; | ||||||
|  |         protected void OnPropertyChanged(string propertyName) | ||||||
|  |         { | ||||||
|  |             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// 用于在检测完成事件中传递结果的事件参数。 | ||||||
|  |     /// </summary> | ||||||
|  |     public class DetectionResultEventArgs : EventArgs | ||||||
|  |     { | ||||||
|  |         public bool IsOK { get; } | ||||||
|  |         public DetectionResultEventArgs(bool isOk) | ||||||
|  |         { | ||||||
|  |             IsOK = isOk; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										82
									
								
								Check.Main/Common/StatisticsExporter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								Check.Main/Common/StatisticsExporter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | using Check.Main.Camera; | ||||||
|  | using NPOI.SS.UserModel; | ||||||
|  | using NPOI.XSSF.UserModel; | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.IO; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using System.Windows.Forms; | ||||||
|  |  | ||||||
|  | namespace Check.Main.Common | ||||||
|  | { | ||||||
|  |     public static class StatisticsExporter | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// 将给定的统计数据导出到指定的Excel文件路径。 | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="data">要导出的统计数据对象。</param> | ||||||
|  |         /// <param name="customFileName">自定义的文件名部分(如 "Reset" 或 "Shutdown")。</param> | ||||||
|  |         public static void ExportToExcel(StatisticsData data, string customFileName) | ||||||
|  |         { | ||||||
|  |             if (data.TotalCount == 0) return; // 如果没有数据,则不导出 | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 string directory = Path.Combine(Application.StartupPath, "Statistics"); | ||||||
|  |                 Directory.CreateDirectory(directory); // 确保文件夹存在 | ||||||
|  |  | ||||||
|  |                 string fileName = $"Statistics_{customFileName}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.xlsx"; | ||||||
|  |                 string filePath = Path.Combine(directory, fileName); | ||||||
|  |  | ||||||
|  |                 // 创建工作簿和工作表 | ||||||
|  |                 IWorkbook workbook = new XSSFWorkbook(); | ||||||
|  |                 ISheet sheet = workbook.CreateSheet("生产统计"); | ||||||
|  |  | ||||||
|  |                 // --- 创建样式 (可选,但能让表格更好看) --- | ||||||
|  |                 IFont boldFont = workbook.CreateFont(); | ||||||
|  |                 boldFont.IsBold = true; | ||||||
|  |                 ICellStyle headerStyle = workbook.CreateCellStyle(); | ||||||
|  |                 headerStyle.SetFont(boldFont); | ||||||
|  |  | ||||||
|  |                 // --- 创建表头 --- | ||||||
|  |                 IRow headerRow = sheet.CreateRow(0); | ||||||
|  |                 headerRow.CreateCell(0).SetCellValue("项目"); | ||||||
|  |                 headerRow.CreateCell(1).SetCellValue("数值"); | ||||||
|  |                 headerRow.GetCell(0).CellStyle = headerStyle; | ||||||
|  |                 headerRow.GetCell(1).CellStyle = headerStyle; | ||||||
|  |  | ||||||
|  |                 // --- 填充数据 --- | ||||||
|  |                 sheet.CreateRow(1).CreateCell(0).SetCellValue("良品数 (OK)"); | ||||||
|  |                 sheet.GetRow(1).CreateCell(1).SetCellValue(data.GoodCount); | ||||||
|  |  | ||||||
|  |                 sheet.CreateRow(2).CreateCell(0).SetCellValue("不良品数 (NG)"); | ||||||
|  |                 sheet.GetRow(2).CreateCell(1).SetCellValue(data.NgCount); | ||||||
|  |  | ||||||
|  |                 sheet.CreateRow(3).CreateCell(0).SetCellValue("产品总数"); | ||||||
|  |                 sheet.GetRow(3).CreateCell(1).SetCellValue(data.TotalCount); | ||||||
|  |  | ||||||
|  |                 sheet.CreateRow(4).CreateCell(0).SetCellValue("良率 (Yield)"); | ||||||
|  |                 sheet.GetRow(4).CreateCell(1).SetCellValue(data.YieldRate.ToString("P2")); | ||||||
|  |  | ||||||
|  |                 // 自动调整列宽 | ||||||
|  |                 sheet.AutoSizeColumn(0); | ||||||
|  |                 sheet.AutoSizeColumn(1); | ||||||
|  |  | ||||||
|  |                 // --- 写入文件 --- | ||||||
|  |                 using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)) | ||||||
|  |                 { | ||||||
|  |                     workbook.Write(fs); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 ThreadSafeLogger.Log($"统计数据已成功导出到: {filePath}"); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 ThreadSafeLogger.Log($"[ERROR] 导出统计数据失败: {ex.Message}"); | ||||||
|  |                 MessageBox.Show($"导出统计数据失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										112
									
								
								Check.Main/Common/ThreadSafeLogger.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								Check.Main/Common/ThreadSafeLogger.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Concurrent; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.IO; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace Check.Main.Common | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// 一个线程安全的、基于队列的日志记录器。 | ||||||
|  |     /// 使用一个独立的后台线程来处理文件写入,避免业务线程阻塞。 | ||||||
|  |     /// </summary> | ||||||
|  |     public static class ThreadSafeLogger | ||||||
|  |     { | ||||||
|  |         // 使用线程安全的队列作为日志消息的缓冲区 | ||||||
|  |         private static readonly BlockingCollection<string> _logQueue = new BlockingCollection<string>(); | ||||||
|  |  | ||||||
|  |         // 日志写入线程 | ||||||
|  |         private static Thread _logWriterThread; | ||||||
|  |  | ||||||
|  |         private static StreamWriter _logFileWriter; | ||||||
|  |  | ||||||
|  |         // 事件,用于将格式化后的日志消息广播给UI等监听者 | ||||||
|  |         public static event Action<string> OnLogMessage; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 初始化日志记录器,启动后台写入线程。 | ||||||
|  |         /// </summary> | ||||||
|  |         public static void Initialize() | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 string logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); | ||||||
|  |                 Directory.CreateDirectory(logDirectory); | ||||||
|  |  | ||||||
|  |                 string logFileName = $"Log_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt"; | ||||||
|  |                 string logFilePath = Path.Combine(logDirectory, logFileName); | ||||||
|  |  | ||||||
|  |                 _logFileWriter = new StreamWriter(logFilePath, append: true, encoding: Encoding.UTF8) { AutoFlush = true }; | ||||||
|  |  | ||||||
|  |                 // 创建并启动后台线程 | ||||||
|  |                 _logWriterThread = new Thread(ProcessLogQueue) | ||||||
|  |                 { | ||||||
|  |                     IsBackground = true, // 设置为后台线程,这样主程序退出时它会自动终止 | ||||||
|  |                     Name = "LogWriterThread" | ||||||
|  |                 }; | ||||||
|  |                 _logWriterThread.Start(); | ||||||
|  |  | ||||||
|  |                 Log("日志系统已初始化。"); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 // 如果初始化失败,尝试通过事件通知UI | ||||||
|  |                 OnLogMessage?.Invoke($"[CRITICAL] 日志系统初始化失败: {ex.Message}"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 将一条日志消息添加到队列中。这个方法是线程安全的,且执行速度非常快。 | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="message">原始日志消息。</param> | ||||||
|  |         public static void Log(string message) | ||||||
|  |         { | ||||||
|  |             string formattedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}"; | ||||||
|  |             _logQueue.Add(formattedMessage); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 后台线程的工作方法。它会持续不断地从队列中取出消息并处理。 | ||||||
|  |         /// </summary> | ||||||
|  |         private static void ProcessLogQueue() | ||||||
|  |         { | ||||||
|  |             // GetConsumingEnumerable会阻塞等待,直到有新的项加入队列或队列被标记为已完成 | ||||||
|  |             foreach (string message in _logQueue.GetConsumingEnumerable()) | ||||||
|  |             { | ||||||
|  |                 try | ||||||
|  |                 { | ||||||
|  |                     // 1. 写入文件 | ||||||
|  |                     _logFileWriter?.WriteLine(message); | ||||||
|  |  | ||||||
|  |                     // 2. 触发事件,通知UI | ||||||
|  |                     OnLogMessage?.Invoke(message); | ||||||
|  |                 } | ||||||
|  |                 catch | ||||||
|  |                 { | ||||||
|  |                     // 忽略在日志线程本身发生的写入错误 | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// 关闭日志记录器,释放资源。 | ||||||
|  |         /// </summary> | ||||||
|  |         public static void Shutdown() | ||||||
|  |         { | ||||||
|  |             Log("日志系统正在关闭..."); | ||||||
|  |  | ||||||
|  |             // 标记队列不再接受新的项目。这会让ProcessLogQueue中的循环在处理完所有剩余项后自然结束。 | ||||||
|  |             _logQueue.CompleteAdding(); | ||||||
|  |  | ||||||
|  |             // 等待日志线程处理完所有剩余的日志,最多等待2秒 | ||||||
|  |             _logWriterThread?.Join(2000); | ||||||
|  |  | ||||||
|  |             // 关闭文件流 | ||||||
|  |             _logFileWriter?.Close(); | ||||||
|  |             _logFileWriter?.Dispose(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										244
									
								
								Check.Main/Dispatch/ProductManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								Check.Main/Dispatch/ProductManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,244 @@ | |||||||
|  | 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; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										316
									
								
								Check.Main/FrmMain.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								Check.Main/FrmMain.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,316 @@ | |||||||
|  | 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; | ||||||
|  |         public static ModbusTcpService PlcClient;//定义全局PLC对象 --- 10.10添加① | ||||||
|  |  | ||||||
|  |         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); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         //添加 PLC 初始化方法 --- 10.10添加② | ||||||
|  |         private async Task InitPlcConnection() | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 PlcClient = new ModbusTcpService("192.168.1.88", 502);  | ||||||
|  |                // await PlcClient.ConnectAsync(); | ||||||
|  |                 ThreadSafeLogger.Log("[PLC] 已成功连接到汇川PLC"); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 ThreadSafeLogger.Log($"[PLC] 连接失败: {ex.Message}"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private async void FrmMain_Load(object sender, EventArgs e)//添加了一个async,原来是private void FrmMain_Load-----10.10修改 | ||||||
|  |         { | ||||||
|  |             // 初始化PLC连接-----10.10添加 | ||||||
|  |             await InitPlcConnection(); | ||||||
|  |  | ||||||
|  |             ModbusTcpService easyPlcClient = new ModbusTcpService("192.168.1.88", 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("检测协调器已关闭。"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										132
									
								
								Check.Main/Infer/ModelSettings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								Check.Main/Infer/ModelSettings.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | |||||||
|  | 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 AlgorithmType | ||||||
|  |     { | ||||||
|  |         [Description("传统算法")] | ||||||
|  |         Tradition = 0, | ||||||
|  |  | ||||||
|  |         [Description("深度学习")] | ||||||
|  |         DeepLearning, | ||||||
|  |     } | ||||||
|  |     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 AlgorithmType _mAType = AlgorithmType.Tradition; | ||||||
|  |         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 AlgorithmType M_AType | ||||||
|  |         { | ||||||
|  |             get => _mAType; | ||||||
|  |             set { if (_mAType != value) { _mAType = 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模型已释放。"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										150
									
								
								Check.Main/Process_Img.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								Check.Main/Process_Img.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | |||||||
|  | using OpenCvSharp; | ||||||
|  | using System; | ||||||
|  | using System.Drawing; | ||||||
|  | using System.IO; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Drawing.Imaging; | ||||||
|  | using OpenCvSharp.Extensions; | ||||||
|  | using System.Collections.Concurrent; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using System.Diagnostics; | ||||||
|  | using System.Threading; | ||||||
|  |  | ||||||
|  | public class ProcessImg | ||||||
|  | { | ||||||
|  |     // 对单个图像进行模板匹配 | ||||||
|  |     | ||||||
|  |     public static (double score, Rect? coords) MatchTemplate(Mat img, string templatePath) | ||||||
|  |     { | ||||||
|  |         // 确保图像和模板文件存在 | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         // 读取图像和模板 | ||||||
|  |          | ||||||
|  |         var template = Cv2.ImRead(templatePath, ImreadModes.Color); | ||||||
|  |  | ||||||
|  |         // 创建一个模板匹配的结果矩阵 | ||||||
|  |         var result = new Mat(); | ||||||
|  |         Cv2.MatchTemplate(img, template, result, TemplateMatchModes.CCoeffNormed); | ||||||
|  |  | ||||||
|  |         // 查找最大匹配值 | ||||||
|  |         Cv2.MinMaxLoc(result, out _, out var maxVal, out _, out var maxLoc); | ||||||
|  |  | ||||||
|  |         // 如果找到的最大匹配值大于阈值 | ||||||
|  |         double threshold = 0.3; // 可以根据需要调整阈值 | ||||||
|  |         if (maxVal >= threshold) | ||||||
|  |         { | ||||||
|  |             // 计算匹配的坐标      | ||||||
|  |             var topLeft = maxLoc; | ||||||
|  |             var rect = new Rect(topLeft.X, topLeft.Y, template.Width, template.Height); | ||||||
|  |  | ||||||
|  |             return (maxVal, rect); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return (0, null); | ||||||
|  |     }    | ||||||
|  |  | ||||||
|  |     // 遍历文件夹中的所有图像文件进行模板匹配,并找到最佳得分图像 | ||||||
|  |     public static Double ProcessImagesInFolder(string folderPath, Mat img) | ||||||
|  |     { | ||||||
|  |         // 获取所有图像文件 | ||||||
|  |         var imageFiles = Directory.GetFiles(folderPath, "*.*", SearchOption.TopDirectoryOnly) | ||||||
|  |             .Where(file => file.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || | ||||||
|  |                            file.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || | ||||||
|  |                            file.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase)) | ||||||
|  |             .ToList(); | ||||||
|  |  | ||||||
|  |        | ||||||
|  |  | ||||||
|  |         // 确保输出文件夹存在 | ||||||
|  |          | ||||||
|  |  | ||||||
|  |         // 线程数量 | ||||||
|  |         int numThreads = 5; | ||||||
|  |  | ||||||
|  |         // 用于存储每个图像的得分和坐标 | ||||||
|  |         var bestMatch = new ConcurrentBag<(string imagePath, double score, Rect? coords)>(); | ||||||
|  |         DateTime startTime = DateTime.Now; | ||||||
|  |         Stopwatch sw = new Stopwatch(); | ||||||
|  |         sw.Start(); | ||||||
|  |  | ||||||
|  |         Parallel.ForEach(imageFiles, new ParallelOptions { MaxDegreeOfParallelism = numThreads }, picPath => | ||||||
|  |         { | ||||||
|  |             DateTime threadStartTime = DateTime.Now; | ||||||
|  |  | ||||||
|  |             var (score, coords) = MatchTemplate(img, picPath); | ||||||
|  |  | ||||||
|  |              | ||||||
|  |              | ||||||
|  |                 bestMatch.Add((picPath, score, coords)); | ||||||
|  |              | ||||||
|  |  | ||||||
|  |             DateTime threadEndTime = DateTime.Now; | ||||||
|  |             TimeSpan threadElapsed = threadEndTime - threadStartTime; | ||||||
|  |             Console.WriteLine($"线程处理 {picPath} 耗时: {threadElapsed.TotalMilliseconds}ms"); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         sw.Stop(); | ||||||
|  |         TimeSpan totalElapsed = sw.Elapsed; | ||||||
|  |         Console.WriteLine($"处理完成,耗时: {totalElapsed.TotalSeconds}秒"); | ||||||
|  |         // 查找最佳得分 | ||||||
|  |         var best = bestMatch.OrderByDescending(m => m.score).FirstOrDefault(); | ||||||
|  |         return best.score; | ||||||
|  |         //if (best.coords.HasValue) | ||||||
|  |         //{ | ||||||
|  |         //    Rect rect = best.coords.Value; | ||||||
|  |         //    return rect; | ||||||
|  |         //} | ||||||
|  |  | ||||||
|  |         //else | ||||||
|  |         //{ | ||||||
|  |         //    return new Rect(0, 0, 0, 0); | ||||||
|  |         //} | ||||||
|  |         | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |     public static Mat BitmapToMat(Bitmap bitmap) | ||||||
|  |     { | ||||||
|  |         if (bitmap == null) | ||||||
|  |         { | ||||||
|  |             throw new ArgumentException("Bitmap is null"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 根据 Bitmap 的宽度、高度和像素格式创建一个与其相对应的 Mat | ||||||
|  |         Mat mat = new Mat(bitmap.Height, bitmap.Width, MatType.CV_8UC3); // 假设 Bitmap 是 24-bit RGB | ||||||
|  |  | ||||||
|  |         // 锁定 Bitmap 的内存区域以直接访问它的内存 | ||||||
|  |         BitmapData bitmapData = bitmap.LockBits( | ||||||
|  |             new Rectangle(0, 0, bitmap.Width, bitmap.Height), | ||||||
|  |             ImageLockMode.ReadOnly, | ||||||
|  |             bitmap.PixelFormat | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         try | ||||||
|  |         { | ||||||
|  |             // 使用直接内存拷贝将 Bitmap 的数据拷贝到 Mat | ||||||
|  |             unsafe | ||||||
|  |             { | ||||||
|  |                 byte* srcData = (byte*)bitmapData.Scan0; | ||||||
|  |                 byte* dstData = (byte*)mat.DataPointer; | ||||||
|  |  | ||||||
|  |                 int stride = bitmapData.Stride; | ||||||
|  |                 int width = bitmap.Width * 3; // 24bpp 3个字节一个像素 | ||||||
|  |                 int height = bitmap.Height; | ||||||
|  |  | ||||||
|  |                 for (int y = 0; y < height; y++) | ||||||
|  |                 { | ||||||
|  |                     Buffer.MemoryCopy(srcData + y * stride, dstData + y * mat.Step(), width, width); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         finally | ||||||
|  |         { | ||||||
|  |             bitmap.UnlockBits(bitmapData); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return mat; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										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(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										88
									
								
								Check.Main/UI/FormControlPanel.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								Check.Main/UI/FormControlPanel.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | 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() | ||||||
|  |         { | ||||||
|  |             System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FormControlPanel)); | ||||||
|  |             uiTableLayoutPanel1 = new Sunny.UI.UITableLayoutPanel(); | ||||||
|  |             btnStartDevice = new Sunny.UI.UIButton(); | ||||||
|  |             btnStartCheck = new Sunny.UI.UIButton(); | ||||||
|  |             uiTableLayoutPanel1.SuspendLayout(); | ||||||
|  |             SuspendLayout(); | ||||||
|  |             //  | ||||||
|  |             // uiTableLayoutPanel1 | ||||||
|  |             //  | ||||||
|  |             resources.ApplyResources(uiTableLayoutPanel1, "uiTableLayoutPanel1"); | ||||||
|  |             uiTableLayoutPanel1.Controls.Add(btnStartDevice, 0, 1); | ||||||
|  |             uiTableLayoutPanel1.Controls.Add(btnStartCheck, 0, 3); | ||||||
|  |             uiTableLayoutPanel1.Name = "uiTableLayoutPanel1"; | ||||||
|  |             uiTableLayoutPanel1.TagString = null; | ||||||
|  |             //  | ||||||
|  |             // btnStartDevice | ||||||
|  |             //  | ||||||
|  |             uiTableLayoutPanel1.SetColumnSpan(btnStartDevice, 2); | ||||||
|  |             btnStartDevice.Cursor = Cursors.Hand; | ||||||
|  |             resources.ApplyResources(btnStartDevice, "btnStartDevice"); | ||||||
|  |             btnStartDevice.FillPressColor = Color.LimeGreen; | ||||||
|  |             btnStartDevice.FillSelectedColor = Color.LimeGreen; | ||||||
|  |             btnStartDevice.Name = "btnStartDevice"; | ||||||
|  |             btnStartDevice.TipsFont = new Font("宋体", 9F, FontStyle.Regular, GraphicsUnit.Point, 134); | ||||||
|  |             btnStartDevice.Click += btnStartDevice_Click; | ||||||
|  |             //  | ||||||
|  |             // btnStartCheck | ||||||
|  |             //  | ||||||
|  |             uiTableLayoutPanel1.SetColumnSpan(btnStartCheck, 2); | ||||||
|  |             btnStartCheck.Cursor = Cursors.Hand; | ||||||
|  |             resources.ApplyResources(btnStartCheck, "btnStartCheck"); | ||||||
|  |             btnStartCheck.FillPressColor = Color.FromArgb(0, 192, 0); | ||||||
|  |             btnStartCheck.FillSelectedColor = Color.FromArgb(0, 192, 0); | ||||||
|  |             btnStartCheck.Name = "btnStartCheck"; | ||||||
|  |             btnStartCheck.TipsFont = new Font("宋体", 9F, FontStyle.Regular, GraphicsUnit.Point, 134); | ||||||
|  |             btnStartCheck.Click += btnStartCheck_Click; | ||||||
|  |             //  | ||||||
|  |             // FormControlPanel | ||||||
|  |             //  | ||||||
|  |             resources.ApplyResources(this, "$this"); | ||||||
|  |             AutoScaleMode = AutoScaleMode.Font; | ||||||
|  |             ControlBox = false; | ||||||
|  |             Controls.Add(uiTableLayoutPanel1); | ||||||
|  |             MaximizeBox = false; | ||||||
|  |             MinimizeBox = false; | ||||||
|  |             Name = "FormControlPanel"; | ||||||
|  |             uiTableLayoutPanel1.ResumeLayout(false); | ||||||
|  |             ResumeLayout(false); | ||||||
|  |  | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         #endregion | ||||||
|  |  | ||||||
|  |         private Sunny.UI.UITableLayoutPanel uiTableLayoutPanel1; | ||||||
|  |         private Sunny.UI.UIButton btnStartDevice; | ||||||
|  |         private Sunny.UI.UIButton btnStartCheck; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										268
									
								
								Check.Main/UI/FormControlPanel.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								Check.Main/UI/FormControlPanel.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,268 @@ | |||||||
|  | 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.Hardware)) | ||||||
|  |                 { | ||||||
|  |                     ThreadSafeLogger.Log("相机设置为硬件触发模式,将由 PLC 输出脉冲信号控制拍照。"); | ||||||
|  |                     //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 | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										253
									
								
								Check.Main/UI/FormControlPanel.resx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								Check.Main/UI/FormControlPanel.resx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,253 @@ | |||||||
|  | <?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> | ||||||
|  |   <assembly alias="mscorlib" name="mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> | ||||||
|  |   <data name="uiTableLayoutPanel1.ColumnCount" type="System.Int32, mscorlib"> | ||||||
|  |     <value>2</value> | ||||||
|  |   </data> | ||||||
|  |   <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> | ||||||
|  |   <data name="btnStartDevice.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"> | ||||||
|  |     <value>Fill</value> | ||||||
|  |   </data> | ||||||
|  |   <assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> | ||||||
|  |   <data name="btnStartDevice.Font" type="System.Drawing.Font, System.Drawing"> | ||||||
|  |     <value>宋体, 12pt</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartDevice.Location" type="System.Drawing.Point, System.Drawing"> | ||||||
|  |     <value>6, 51</value> | ||||||
|  |   </data> | ||||||
|  |   <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> | ||||||
|  |   <data name="btnStartDevice.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms"> | ||||||
|  |     <value>6, 6, 6, 6</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartDevice.MinimumSize" type="System.Drawing.Size, System.Drawing"> | ||||||
|  |     <value>2, 2</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartDevice.Size" type="System.Drawing.Size, System.Drawing"> | ||||||
|  |     <value>410, 128</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartDevice.TabIndex" type="System.Int32, mscorlib"> | ||||||
|  |     <value>0</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartDevice.Text" xml:space="preserve"> | ||||||
|  |     <value>启动设备</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>btnStartDevice.Name" xml:space="preserve"> | ||||||
|  |     <value>btnStartDevice</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>btnStartDevice.Type" xml:space="preserve"> | ||||||
|  |     <value>Sunny.UI.UIButton, SunnyUI, Culture=neutral, PublicKeyToken=27d7d2e821d97aeb</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>btnStartDevice.Parent" xml:space="preserve"> | ||||||
|  |     <value>uiTableLayoutPanel1</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>btnStartDevice.ZOrder" xml:space="preserve"> | ||||||
|  |     <value>0</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartCheck.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"> | ||||||
|  |     <value>Fill</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartCheck.Font" type="System.Drawing.Font, System.Drawing"> | ||||||
|  |     <value>宋体, 12pt</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartCheck.Location" type="System.Drawing.Point, System.Drawing"> | ||||||
|  |     <value>6, 221</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartCheck.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms"> | ||||||
|  |     <value>6, 6, 6, 6</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartCheck.MinimumSize" type="System.Drawing.Size, System.Drawing"> | ||||||
|  |     <value>2, 2</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartCheck.Size" type="System.Drawing.Size, System.Drawing"> | ||||||
|  |     <value>410, 122</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartCheck.TabIndex" type="System.Int32, mscorlib"> | ||||||
|  |     <value>1</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="btnStartCheck.Text" xml:space="preserve"> | ||||||
|  |     <value>开始检测</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>btnStartCheck.Name" xml:space="preserve"> | ||||||
|  |     <value>btnStartCheck</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>btnStartCheck.Type" xml:space="preserve"> | ||||||
|  |     <value>Sunny.UI.UIButton, SunnyUI, Culture=neutral, PublicKeyToken=27d7d2e821d97aeb</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>btnStartCheck.Parent" xml:space="preserve"> | ||||||
|  |     <value>uiTableLayoutPanel1</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>btnStartCheck.ZOrder" xml:space="preserve"> | ||||||
|  |     <value>1</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="uiTableLayoutPanel1.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"> | ||||||
|  |     <value>Fill</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="uiTableLayoutPanel1.Location" type="System.Drawing.Point, System.Drawing"> | ||||||
|  |     <value>0, 0</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="uiTableLayoutPanel1.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms"> | ||||||
|  |     <value>6, 6, 6, 6</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="uiTableLayoutPanel1.RowCount" type="System.Int32, mscorlib"> | ||||||
|  |     <value>5</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="uiTableLayoutPanel1.Size" type="System.Drawing.Size, System.Drawing"> | ||||||
|  |     <value>422, 396</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="uiTableLayoutPanel1.TabIndex" type="System.Int32, mscorlib"> | ||||||
|  |     <value>0</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>uiTableLayoutPanel1.Name" xml:space="preserve"> | ||||||
|  |     <value>uiTableLayoutPanel1</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>uiTableLayoutPanel1.Type" xml:space="preserve"> | ||||||
|  |     <value>Sunny.UI.UITableLayoutPanel, SunnyUI, Culture=neutral, PublicKeyToken=27d7d2e821d97aeb</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>uiTableLayoutPanel1.Parent" xml:space="preserve"> | ||||||
|  |     <value>$this</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>uiTableLayoutPanel1.ZOrder" xml:space="preserve"> | ||||||
|  |     <value>0</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="uiTableLayoutPanel1.LayoutSettings" type="System.Windows.Forms.TableLayoutSettings, System.Windows.Forms"> | ||||||
|  |     <value><?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="btnStartDevice" Row="1" RowSpan="1" Column="0" ColumnSpan="2" /><Control Name="btnStartCheck" Row="3" RowSpan="1" Column="0" ColumnSpan="2" /></Controls><Columns Styles="Percent,50,Percent,50" /><Rows Styles="Percent,11.38585,Percent,35.35353,Percent,7.575758,Percent,33.83838,Percent,11.38585" /></TableLayoutSettings></value> | ||||||
|  |   </data> | ||||||
|  |   <metadata name="$this.Localizable" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||||||
|  |     <value>True</value> | ||||||
|  |   </metadata> | ||||||
|  |   <data name="$this.AutoScaleDimensions" type="System.Drawing.SizeF, System.Drawing"> | ||||||
|  |     <value>11, 24</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="$this.ClientSize" type="System.Drawing.Size, System.Drawing"> | ||||||
|  |     <value>422, 396</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="$this.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms"> | ||||||
|  |     <value>6, 6, 6, 6</value> | ||||||
|  |   </data> | ||||||
|  |   <data name="$this.Text" xml:space="preserve"> | ||||||
|  |     <value>启动管理</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>$this.Name" xml:space="preserve"> | ||||||
|  |     <value>FormControlPanel</value> | ||||||
|  |   </data> | ||||||
|  |   <data name=">>$this.Type" xml:space="preserve"> | ||||||
|  |     <value>WeifenLuo.WinFormsUI.Docking.DockContent, WeifenLuo.WinFormsUI.Docking, Culture=neutral, PublicKeyToken=5cded1a1a0a7b481</value> | ||||||
|  |   </data> | ||||||
|  | </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