视觉修改

This commit is contained in:
17860779768
2025-08-25 16:33:58 +08:00
commit 2e46747ba9
49 changed files with 11062 additions and 0 deletions

113
Check.Main/UI/FormControlPanel.Designer.cs generated Normal file
View File

@@ -0,0 +1,113 @@
namespace Check.Main.UI
{
partial class FormControlPanel
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.uiTableLayoutPanel1 = new Sunny.UI.UITableLayoutPanel();
this.btnStartDevice = new Sunny.UI.UIButton();
this.btnStartCheck = new Sunny.UI.UIButton();
this.uiTableLayoutPanel1.SuspendLayout();
this.SuspendLayout();
//
// uiTableLayoutPanel1
//
this.uiTableLayoutPanel1.ColumnCount = 2;
this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
this.uiTableLayoutPanel1.Controls.Add(this.btnStartDevice, 0, 1);
this.uiTableLayoutPanel1.Controls.Add(this.btnStartCheck, 0, 3);
this.uiTableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.uiTableLayoutPanel1.Location = new System.Drawing.Point(0, 0);
this.uiTableLayoutPanel1.Name = "uiTableLayoutPanel1";
this.uiTableLayoutPanel1.RowCount = 5;
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.38585F));
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 35.35353F));
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 7.575758F));
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.83838F));
this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.38585F));
this.uiTableLayoutPanel1.Size = new System.Drawing.Size(230, 198);
this.uiTableLayoutPanel1.TabIndex = 0;
this.uiTableLayoutPanel1.TagString = null;
//
// btnStartDevice
//
this.uiTableLayoutPanel1.SetColumnSpan(this.btnStartDevice, 2);
this.btnStartDevice.Cursor = System.Windows.Forms.Cursors.Hand;
this.btnStartDevice.Dock = System.Windows.Forms.DockStyle.Fill;
this.btnStartDevice.FillPressColor = System.Drawing.Color.LimeGreen;
this.btnStartDevice.FillSelectedColor = System.Drawing.Color.LimeGreen;
this.btnStartDevice.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnStartDevice.Location = new System.Drawing.Point(3, 25);
this.btnStartDevice.MinimumSize = new System.Drawing.Size(1, 1);
this.btnStartDevice.Name = "btnStartDevice";
this.btnStartDevice.Size = new System.Drawing.Size(224, 64);
this.btnStartDevice.TabIndex = 0;
this.btnStartDevice.Text = "启动设备";
this.btnStartDevice.TipsFont = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnStartDevice.Click += new System.EventHandler(this.btnStartDevice_Click);
//
// btnStartCheck
//
this.uiTableLayoutPanel1.SetColumnSpan(this.btnStartCheck, 2);
this.btnStartCheck.Cursor = System.Windows.Forms.Cursors.Hand;
this.btnStartCheck.Dock = System.Windows.Forms.DockStyle.Fill;
this.btnStartCheck.FillPressColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(192)))), ((int)(((byte)(0)))));
this.btnStartCheck.FillSelectedColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(192)))), ((int)(((byte)(0)))));
this.btnStartCheck.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnStartCheck.Location = new System.Drawing.Point(3, 110);
this.btnStartCheck.MinimumSize = new System.Drawing.Size(1, 1);
this.btnStartCheck.Name = "btnStartCheck";
this.btnStartCheck.Size = new System.Drawing.Size(224, 61);
this.btnStartCheck.TabIndex = 1;
this.btnStartCheck.Text = "开始检测";
this.btnStartCheck.TipsFont = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnStartCheck.Click += new System.EventHandler(this.btnStartCheck_Click);
//
// FormControlPanel
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(230, 198);
this.ControlBox = false;
this.Controls.Add(this.uiTableLayoutPanel1);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "FormControlPanel";
this.Text = "启动管理";
this.uiTableLayoutPanel1.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private Sunny.UI.UITableLayoutPanel uiTableLayoutPanel1;
private Sunny.UI.UIButton btnStartDevice;
private Sunny.UI.UIButton btnStartCheck;
}
}

View File

@@ -0,0 +1,264 @@
using Check.Main.Camera;
using Check.Main.Common;
using Check.Main.Infer;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using WeifenLuo.WinFormsUI.Docking;
namespace Check.Main.UI
{
public partial class FormControlPanel : DockContent
{
private bool _isDeviceReady = false; // 新的状态:设备是否已准备好
private bool _isDetecting = false; // 新的状态:是否正在检测中
// 用于跟踪设备运行状态的私有标志
private bool _isDeviceRunning = false;
public FormControlPanel()
{
InitializeComponent();
ConfigurationManager.OnConfigurationChanged += HandleConfigurationChanged;
UpdateUI();
}
/// <summary>
/// 处理全局配置在其他地方(如 FrmConfig被更改的事件
/// </summary>
private void HandleConfigurationChanged()
{
// 这是一个安全措施。如果设备正在运行时配置发生了变化,
// 最安全的做法是停止设备,以防止出现不可预知的行为。
if (_isDeviceRunning)
{
// 使用 Invoke 确保UI更新在正确的线程上执行
this.Invoke((Action)(() =>
{
ThreadSafeLogger.Log("相机配置已在运行时发生更改,设备将自动停止。请重新启动设备以应用新配置。");
//MessageBox.Show("相机配置已在运行时发生更改,设备将自动停止。请重新启动设备以应用新配置。",
// "配置变更", MessageBoxButtons.OK, MessageBoxIcon.Information);
// 触发与点击“关闭设备”按钮相同的逻辑
btnStartDevice_Click(this, EventArgs.Empty);
}));
}
}
private void btnStartDevice_Click(object sender, EventArgs e)
{
if (_isDeviceReady)//_isDeviceRunning
{
// --- 关闭流程 ---
ThreadSafeLogger.Log("用户点击“关闭设备”,开始完整关闭流程...");
// 如果正在检测,先停止它
if (_isDetecting)
{
btnStartCheck_Click(this, EventArgs.Empty); // 调用停止检测的逻辑
}
var mainForm = this.DockPanel.FindForm() as FrmMain;
CameraManager.Shutdown();
_isDeviceReady = false;
//// 1. 停止硬触发模拟器
//CameraManager.StopHardwareTriggerSimulator();
//// 2. 如果检测正在运行,则停止
//if (DetectionCoordinator.IsDetectionRunning)
//{
// DetectionCoordinator.StopDetection();
// UpdateDetectionButtonUI();
//}
// 3. 执行完整的系统关闭(包括相机硬件和检测协调器)
//CameraManager.Shutdown();
//YoloModelManager.Shutdown();
//_isDeviceRunning = false;
}
else
{
// --- 启动流程 ---
ThreadSafeLogger.Log("用户点击“启动设备”,开始新的启动流程...");
// 1. 从单一数据源获取完整的配置对象
var config = ConfigurationManager.GetCurrentConfig();
// 2. 验证相机配置的有效性
if (config.CameraSettings == null || !config.CameraSettings.Any(c => c.IsEnabled))
{
ThreadSafeLogger.Log("没有已启用的相机配置,启动中止。");
return;
}
// 3. 获取主窗体引用
var mainForm = this.DockPanel.FindForm() as FrmMain;
if (mainForm == null)
{
ThreadSafeLogger.Log("无法找到主窗体,启动中止。");
return;
}
// 4. 执行新的启动流程:
// 第一步初始化系统。这会准备好相机硬件、UI窗口和所有后台处理线程。
CameraManager.PrepareAll(config, mainForm);
_isDeviceReady = true;
//YoloModelManager.Initialize(config.ModelSettings);
//CameraManager.Initialize(config, mainForm);
//// 第二步:命令所有相机开始采集图像。
//CameraManager.StartAll();
//// 5. 如果有任何相机配置为软触发模式,我们启动模拟器来模拟触发信号
//if (config.CameraSettings.Any(c => c.IsEnabled && c.TriggerMode == TriggerModeType.Software))
//{
// ThreadSafeLogger.Log("检测到软触发相机,启动触发模拟器。");
// CameraManager.TriggerInterval = 100; // 根据需要设置间隔
// CameraManager.StartHardwareTriggerSimulator();
//}
//_isDeviceRunning = true;
}
UpdateUI();//UpdateDeviceButtonUI();
}
private void btnStartCheck_Click(object sender, EventArgs e)
{
if (_isDetecting)
{
// --- 停止检测 ---
ThreadSafeLogger.Log("用户点击“停止检测”,暂停数据流...");
// 停止硬触发模拟器
CameraManager.StopHardwareTriggerSimulator();
// 停止相机采集
CameraManager.StopAll();
// 停止统计
DetectionCoordinator.StopDetection();
_isDetecting = false;
}
else
{
// --- 开始检测 ---
ThreadSafeLogger.Log("用户点击“开始检测”,启动数据流...");
// 命令相机开始采集
CameraManager.StartAll();
// 启动硬触发模拟器(如果需要)
var config = ConfigurationManager.GetCurrentConfig();
if (config.CameraSettings.Any(c => c.IsEnabled && c.TriggerMode == TriggerModeType.Software))
{
CameraManager.TriggerInterval = 100;
CameraManager.StartHardwareTriggerSimulator();
}
// 开始统计
DetectionCoordinator.StartDetection();
_isDetecting = true;
}
UpdateUI();
//if (!_isDeviceRunning && !DetectionCoordinator.IsDetectionRunning)
//{
// ThreadSafeLogger.Log("设备未启动,无法开始检测。");
// return;
//}
//// 现在调用 DetectionCoordinator 中的方法
//if (DetectionCoordinator.IsDetectionRunning)
//{
// DetectionCoordinator.StopDetection();
//}
//else
//{
// DetectionCoordinator.StartDetection();
//}
//UpdateDetectionButtonUI();
}
#region UI
/// <summary>
/// 根据设备运行状态更新“设备”按钮的UI文本和颜色
/// </summary>
private void UpdateDeviceButtonUI()
{
if (_isDeviceRunning)
{
btnStartDevice.Text = "关闭设备";
btnStartDevice.BackColor = Color.Salmon;
}
else
{
btnStartDevice.Text = "启动设备";
btnStartDevice.BackColor = SystemColors.Control;
}
}
// 统一的UI更新方法
private void UpdateUI()
{
// --- 更新“设备”按钮 ---
if (_isDeviceReady)
{
btnStartDevice.Text = "关闭设备";
btnStartDevice.BackColor = Color.Salmon;
btnStartCheck.Enabled = true; // 设备就绪后,检测按钮才可用
}
else
{
btnStartDevice.Text = "启动设备";
btnStartDevice.BackColor = SystemColors.Control;
btnStartCheck.Enabled = false; // 设备未就绪,检测按钮不可用
}
// --- 更新“检测”按钮 ---
if (_isDetecting)
{
btnStartCheck.Text = "停止检测";
btnStartCheck.BackColor = Color.LightGreen;
btnStartDevice.Enabled = false; // 正在检测时,不允许关闭设备
}
else
{
btnStartCheck.Text = "开始检测";
btnStartCheck.BackColor = SystemColors.Control;
if (_isDeviceReady) btnStartDevice.Enabled = true; // 停止检测后,允许关闭设备
}
}
/// <summary>
/// 根据检测运行状态更新“检测”按钮的UI文本和颜色
/// </summary>
private void UpdateDetectionButtonUI()
{
//if (CameraManager.IsDetectionRunning)
//{
// btnStartCheck.Text = "停止检测";
// btnStartCheck.BackColor = Color.LightGreen;
//}
//else
//{
// btnStartCheck.Text = "启动检测";
// btnStartCheck.BackColor = SystemColors.Control;
//}
if (DetectionCoordinator.IsDetectionRunning)
{
btnStartCheck.Text = "停止检测";
btnStartCheck.BackColor = Color.LightGreen;
}
else
{
btnStartCheck.Text = "启动检测";
btnStartCheck.BackColor = SystemColors.Control;
}
}
#endregion
}
}

View 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>

View 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
}
}

View 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();
}
}
}

View 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
View 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;
}
}

View 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);
}
}
}

View 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
View 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;
}
}

View 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);
}
}
}
}

View 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
View 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
View 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);
}
}
}
}
}

View 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
View 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
View 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
View 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
View 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;
}
}

View 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;
}
}
}
}

View 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>

View 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();
}
}
}