Files
HkVisionPro/HkVisionPro.App/MainForm.cs

2594 lines
92 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
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.Configuration;
using HkVisionPro.App.Automation;
using ReaLTaiizor.Enum.Poison;
using ReaLTaiizor.Forms;
using ReaLTaiizor.Manager;
using VM.PlatformSDKCS;
using VM.Core;
using IMVSBcrModuCs;
using PointSetMODU_STDCs;
namespace HkVisionPro.App
{
public partial class MainForm : PoisonForm
{
/// <summary>
/// VisionMaster 方案管理对象(负责加载、执行、保存方案)。
/// </summary>
private readonly SolutionManager _solutionManager;
/// <summary>
/// 当前已加载的方案文件完整路径。
/// </summary>
private string _currentSolutionPath = string.Empty;
/// <summary>
/// 当前待完成加载的方案路径(用于异步加载结束时提交状态)。
/// </summary>
private string _pendingLoadSolutionPath = string.Empty;
/// <summary>
/// 当前是否已成功加载方案。
/// </summary>
private bool _isSolutionLoaded;
/// <summary>
/// 当前是否正在进行加载/执行/保存过程(用于按钮防重入)。
/// </summary>
private bool _isBusy;
/// <summary>
/// 当前是否处于方案加载过程SDK 可能异步加载,需显式防重入)。
/// </summary>
private bool _isSolutionLoading;
/// <summary>
/// 当前是否处于流程连续执行状态。
/// </summary>
private bool _isContinuousRunning;
/// <summary>
/// 当前连续执行的流程名称。
/// </summary>
private string _continuousProcedureName = string.Empty;
/// <summary>
/// 渲染互斥标记0=空闲1=渲染中),用于避免连续执行阶段并发读取结果。
/// </summary>
private int _renderingGuard;
/// <summary>
/// 当前选择的流程对象(用于流程执行与结果读取)。
/// </summary>
private VmProcedure _currentProcedure;
/// <summary>
/// 当前激活的条码模块工具对象(用于结果回调订阅与解绑)。
/// </summary>
private IMVSBcrModuTool _activeBcrTool;
/// <summary>
/// VM.Core 方案事件是否已注册。
/// </summary>
private bool _isVmSolutionEventRegistered;
/// <summary>
/// 关闭阶段资源是否已释放(用于防止 FormClosing 与 OnFormClosed 重复释放)。
/// </summary>
private bool _isClosingResourcesReleased;
/// <summary>
/// 渲染图像输出名称(支持配置,默认 Image
/// </summary>
private readonly string _renderImageOutputName;
/// <summary>
/// 渲染主 ROI 输出名称(支持配置,默认 InputR1
/// </summary>
private readonly string _renderRoiOutputName1;
/// <summary>
/// 渲染次 ROI 输出名称(支持配置,默认 InputR2
/// </summary>
private readonly string _renderRoiOutputName2;
/// <summary>
/// 渲染判定结果1整型输出名称支持配置默认 Result1
/// </summary>
private readonly string _renderResultIntOutputName1;
/// <summary>
/// 渲染判定结果2整型输出名称支持配置默认 Result2
/// </summary>
private readonly string _renderResultIntOutputName2;
/// <summary>
/// VisionMaster: 方案正在加载中的错误码IMVS_EC_SOLUTION_LOADING
/// </summary>
private const int VmErrorCodeSolutionLoading = -536870899;
/// <summary>
/// VisionMaster: 模块已处于连续执行中的错误码IMVS_EC_MODULE_CONTINUE_EXECUTE
/// </summary>
private const int VmErrorCodeModuleContinueExecute = -536870127;
/// <summary>
/// 自动装配状态机配置。
/// </summary>
private readonly AutoAssemblyOptions _autoAssemblyOptions;
/// <summary>
/// 自动装配流程控制器Stateless 状态机)。
/// </summary>
private readonly AutoAssemblyWorkflowController _autoAssemblyController;
/// <summary>
/// 方案加载完成后是否自动启动自动装配流程。
/// </summary>
private readonly bool _autoRunEnabledAfterSolutionLoaded;
/// <summary>
/// ReaLTaiizor Poison 样式管理器(用于统一暗色主题风格元数据)。
/// </summary>
private readonly PoisonStyleManager _poisonStyleManager;
/// <summary>
/// 主背景颜色。
/// </summary>
private static readonly Color DarkThemeMainBackColor = Color.FromArgb(28, 28, 30);
/// <summary>
/// 分组容器背景颜色。
/// </summary>
private static readonly Color DarkThemePanelBackColor = Color.FromArgb(36, 36, 40);
/// <summary>
/// 输入类控件背景颜色。
/// </summary>
private static readonly Color DarkThemeInputBackColor = Color.FromArgb(45, 45, 48);
/// <summary>
/// 前景文字颜色。
/// </summary>
private static readonly Color DarkThemeForeColor = Color.FromArgb(230, 230, 230);
/// <summary>
/// 主题强调色(按钮边框、选中态)。
/// </summary>
private static readonly Color DarkThemeAccentColor = Color.FromArgb(0, 174, 219);
/// <summary>
/// 分组框边框颜色。
/// </summary>
private static readonly Color DarkThemeGroupBorderColor = Color.FromArgb(78, 78, 82);
/// <summary>
/// 分组框标题底色。
/// </summary>
private static readonly Color DarkThemeGroupTitleBackColor = Color.FromArgb(48, 48, 52);
/// <summary>
/// 列表选中项背景色。
/// </summary>
private static readonly Color DarkThemeSelectedBackColor = Color.FromArgb(0, 120, 170);
public MainForm()
{
InitializeComponent();
_poisonStyleManager = components == null ? new PoisonStyleManager() : new PoisonStyleManager(components);
_solutionManager = new SolutionManager();
_renderImageOutputName = GetAppSettingOrDefault("RenderImageOutputName", "ImageData0");
_renderRoiOutputName1 = GetAppSettingOrDefault("RenderRoiOutputName", "InputR1");
_renderRoiOutputName2 = GetAppSettingOrDefault("RenderRoiOutputName2", "InputR2");
_renderResultIntOutputName1 = GetAppSettingOrDefault("RenderResultIntOutputName1", "Result1");
_renderResultIntOutputName2 = GetAppSettingOrDefault("RenderResultIntOutputName2", "Result2");
_autoAssemblyOptions = BuildAutoAssemblyOptions();
_autoRunEnabledAfterSolutionLoaded = GetAppSettingBoolOrDefault("AutoRunEnabledAfterSolutionLoaded", true);
_autoAssemblyController = new AutoAssemblyWorkflowController(
_autoAssemblyOptions,
WriteLog,
(procedure, triggerSource) => RenderProcedureResultToPictureBox(procedure, triggerSource));
_autoAssemblyController.StatusChanged += AutoAssemblyController_StatusChanged;
RegisterSolutionCallbacks();
RegisterVmSolutionCallbacks();
// 统一联动:流程切换时自动刷新模块列表。
cmbProcdure.SelectedIndexChanged += cmbProcdure_SelectedIndexChanged;
// 视图改为 pictureBox 展示时,统一设置渲染行为。
pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
pictureBox1.BackColor = Color.Black;
InitializeDarkTheme();
InitializeAutoRunStatusPanel();
UpdateUiState(false);
txtSolutionAddress.Text = string.Empty;
}
/// <summary>
/// 初始化自动运行结果显示面板。
/// </summary>
private void InitializeAutoRunStatusPanel()
{
lblRunState.AutoSize = false;
lblRunState.Dock = DockStyle.Fill;
lblRunState.TextAlign = ContentAlignment.MiddleCenter;
var initMessage = _autoRunEnabledAfterSolutionLoaded
? "自动流程待启动(等待方案加载)"
: "自动流程已禁用(配置开关关闭)";
UpdateAutoRunResultDisplay(AutoAssemblyState.Idle, initMessage, 0, string.Empty, 0, _autoAssemblyOptions.StableOkMilliseconds);
SyncAutoRunButtonState();
}
/// <summary>
/// 自动装配状态变化事件:刷新“自动运行结果”显示。
/// </summary>
/// <param name="sender">事件源对象。</param>
/// <param name="e">状态变化参数。</param>
private void AutoAssemblyController_StatusChanged(object sender, AutoAssemblyStatusChangedEventArgs e)
{
if (e == null)
{
return;
}
SyncAutoRunButtonState();
UpdateAutoRunResultDisplay(
e.State,
e.Message,
e.ProductIndex,
e.ActiveProcedureName,
e.StableElapsedMilliseconds,
e.StableTargetMilliseconds);
}
/// <summary>
/// 更新自动运行结果显示控件(线程安全)。
/// </summary>
/// <param name="state">自动流程状态。</param>
/// <param name="message">状态描述。</param>
/// <param name="productIndex">当前产品序号(从 1 开始)。</param>
/// <param name="activeProcedureName">当前流程名。</param>
/// <param name="stableElapsedMilliseconds">稳定计时已累计毫秒数。</param>
/// <param name="stableTargetMilliseconds">稳定计时目标毫秒数。</param>
private void UpdateAutoRunResultDisplay(
AutoAssemblyState state,
string message,
int productIndex,
string activeProcedureName,
int stableElapsedMilliseconds,
int stableTargetMilliseconds)
{
if (!IsHandleCreated || IsDisposed)
{
return;
}
void UpdateAction()
{
lblRunState.ForeColor = GetAutoRunStateColor(state);
lblRunState.Text = BuildAutoRunDisplayText(
state,
message,
productIndex,
activeProcedureName,
stableElapsedMilliseconds,
stableTargetMilliseconds);
}
if (lblRunState.InvokeRequired)
{
lblRunState.BeginInvoke(new Action(UpdateAction));
}
else
{
UpdateAction();
}
}
/// <summary>
/// 构造自动运行结果展示文本。
/// </summary>
/// <param name="state">自动流程状态。</param>
/// <param name="message">状态描述。</param>
/// <param name="productIndex">当前产品序号。</param>
/// <param name="activeProcedureName">当前流程名。</param>
/// <param name="stableElapsedMilliseconds">稳定计时累计毫秒数。</param>
/// <param name="stableTargetMilliseconds">稳定计时目标毫秒数。</param>
/// <returns>用于显示的多行文本。</returns>
private static string BuildAutoRunDisplayText(
AutoAssemblyState state,
string message,
int productIndex,
string activeProcedureName,
int stableElapsedMilliseconds,
int stableTargetMilliseconds)
{
var displayProductIndex = productIndex <= 0 ? 1 : productIndex;
var displayProcedure = string.IsNullOrWhiteSpace(activeProcedureName) ? "-" : activeProcedureName;
var elapsed = Math.Max(0, stableElapsedMilliseconds);
var target = Math.Max(0, stableTargetMilliseconds);
if (target > 0)
{
elapsed = Math.Min(elapsed, target);
}
return $"{GetAutoRunStateTitle(state)}\r\n产品序号#{displayProductIndex}\r\n当前流程{displayProcedure}\r\n稳定计时{elapsed}/{target} ms\r\n状态描述{message}";
}
/// <summary>
/// 获取自动状态标题文本。
/// </summary>
/// <param name="state">自动流程状态。</param>
/// <returns>状态标题。</returns>
private static string GetAutoRunStateTitle(AutoAssemblyState state)
{
switch (state)
{
case AutoAssemblyState.LayerAInspecting:
return "A层检测中";
case AutoAssemblyState.LayerBInspecting:
return "B层检测中";
case AutoAssemblyState.LayerCInspecting:
return "C层检测中";
case AutoAssemblyState.ProductCompleted:
return "整机检测OK";
case AutoAssemblyState.Faulted:
return "自动流程异常";
case AutoAssemblyState.Stopped:
return "自动流程已停止";
default:
return "自动流程空闲";
}
}
/// <summary>
/// 获取自动状态标题颜色。
/// </summary>
/// <param name="state">自动流程状态。</param>
/// <returns>状态颜色。</returns>
private static Color GetAutoRunStateColor(AutoAssemblyState state)
{
switch (state)
{
case AutoAssemblyState.LayerAInspecting:
return Color.FromArgb(64, 196, 255);
case AutoAssemblyState.LayerBInspecting:
return Color.FromArgb(255, 179, 71);
case AutoAssemblyState.LayerCInspecting:
return Color.FromArgb(170, 132, 255);
case AutoAssemblyState.ProductCompleted:
return Color.FromArgb(120, 220, 130);
case AutoAssemblyState.Faulted:
return Color.FromArgb(255, 99, 99);
case AutoAssemblyState.Stopped:
return Color.FromArgb(200, 200, 200);
default:
return DarkThemeForeColor;
}
}
/// <summary>
/// 按配置构建自动装配流程参数。
/// </summary>
/// <returns>自动流程配置对象。</returns>
private AutoAssemblyOptions BuildAutoAssemblyOptions()
{
return new AutoAssemblyOptions
{
ProcedureAName = GetAppSettingOrDefault("AutoRunProcedureAName", "A"),
ProcedureBName = GetAppSettingOrDefault("AutoRunProcedureBName", "B"),
ProcedureCName = GetAppSettingOrDefault("AutoRunProcedureCName", "C"),
OkIntOutputName = GetAppSettingOrDefault("AutoRunOkIntOutputName", _renderResultIntOutputName1),
OkIntOutputName2 = GetAppSettingOrDefault("AutoRunOkIntOutputName2", _renderResultIntOutputName2),
OkIntValue = GetAppSettingIntOrDefault("AutoRunOkIntValue", 1),
StableOkMilliseconds = GetAppSettingIntOrDefault("AutoRunStableMilliseconds", 2000),
};
}
/// <summary>
/// 方案加载成功后按配置启动自动装配流程。
/// </summary>
private void StartAutoAssemblyIfConfigured()
{
if (!_autoRunEnabledAfterSolutionLoaded)
{
UpdateAutoRunResultDisplay(
AutoAssemblyState.Idle,
"自动流程已禁用AutoRunEnabledAfterSolutionLoaded=false",
0,
string.Empty,
0,
_autoAssemblyOptions.StableOkMilliseconds);
return;
}
if (!_isSolutionLoaded)
{
return;
}
try
{
// 切入自动模式前清理手动执行上下文,避免两套执行链路并发冲突。
StopContinuousRunIfNeeded("切换到自动装配流程");
UnbindProcedureWorkEndCallback();
_currentProcedure = null;
_autoAssemblyController.Start();
}
catch (Exception ex)
{
HandleException("启动自动装配流程", ex);
}
}
/// <summary>
/// 停止自动装配流程(若运行中)。
/// </summary>
/// <param name="reason">停止原因。</param>
private void StopAutoAssemblyIfRunning(string reason)
{
try
{
_autoAssemblyController.Stop(reason);
SyncAutoRunButtonState();
}
catch (Exception ex)
{
WriteLog($"停止自动装配流程异常:{ex.Message}");
}
}
/// <summary>
/// 同步自动运行按钮文本(线程安全)。
/// 运行中显示“停止自动”,未运行显示“自动运行”。
/// </summary>
private void SyncAutoRunButtonState()
{
if (!IsHandleCreated || IsDisposed)
{
return;
}
void UpdateAction()
{
btnAutoRun.Text = _autoAssemblyController.IsRunning ? "停止自动" : "自动运行";
}
if (btnAutoRun.InvokeRequired)
{
btnAutoRun.BeginInvoke(new Action(UpdateAction));
}
else
{
UpdateAction();
}
}
/// <summary>
/// 初始化 ReaLTaiizor 暗色主题,并将原生 WinForms 控件统一成暗色风格。
/// </summary>
private void InitializeDarkTheme()
{
_poisonStyleManager.Owner = this;
_poisonStyleManager.Theme = ThemeStyle.Dark;
_poisonStyleManager.Style = ColorStyle.Teal;
StyleManager = _poisonStyleManager;
Theme = ThemeStyle.Dark;
Style = ColorStyle.Teal;
ApplyDarkThemeToNativeControls(this);
}
/// <summary>
/// 递归应用暗色主题到原生 WinForms 控件。
/// </summary>
/// <param name="rootControl">递归起始控件。</param>
private void ApplyDarkThemeToNativeControls(Control rootControl)
{
if (rootControl == null)
{
return;
}
if (ReferenceEquals(rootControl, this))
{
BackColor = DarkThemeMainBackColor;
ForeColor = DarkThemeForeColor;
}
foreach (Control control in rootControl.Controls)
{
if (ReferenceEquals(control, vmProcedureConfigControl1) || ReferenceEquals(control, vmParamsConfigControl1))
{
continue;
}
if (control is GroupBox groupBox)
{
ApplyGroupBoxTheme(groupBox);
}
else if (control is Button button)
{
ApplyButtonTheme(button);
}
else if (control is TextBox textBox)
{
ApplyTextBoxTheme(textBox);
}
else if (control is ComboBox comboBox)
{
ApplyComboBoxTheme(comboBox);
}
else if (control is ListBox listBox)
{
ApplyListBoxTheme(listBox);
}
else if (control is Label label)
{
ApplyLabelTheme(label);
}
else if (control is PictureBox pictureBox)
{
ApplyPictureBoxTheme(pictureBox);
}
else
{
control.BackColor = DarkThemeMainBackColor;
control.ForeColor = DarkThemeForeColor;
}
if (control.HasChildren)
{
ApplyDarkThemeToNativeControls(control);
}
}
}
/// <summary>
/// 设置分组框控件暗色样式。
/// </summary>
/// <param name="groupBox">目标分组框。</param>
private void ApplyGroupBoxTheme(GroupBox groupBox)
{
groupBox.BackColor = DarkThemePanelBackColor;
groupBox.ForeColor = DarkThemeForeColor;
groupBox.Paint -= ThemedGroupBox_Paint;
groupBox.Paint += ThemedGroupBox_Paint;
}
/// <summary>
/// 自定义绘制分组框标题与边框,统一暗色视觉层级。
/// </summary>
/// <param name="sender">事件发起对象。</param>
/// <param name="e">绘制事件参数。</param>
private void ThemedGroupBox_Paint(object sender, PaintEventArgs e)
{
var groupBox = sender as GroupBox;
if (groupBox == null)
{
return;
}
e.Graphics.Clear(groupBox.BackColor);
var groupText = groupBox.Text ?? string.Empty;
var textSize = TextRenderer.MeasureText(groupText, groupBox.Font);
var textRect = new Rectangle(12, 0, textSize.Width + 8, textSize.Height);
var borderRect = new Rectangle(
1,
Math.Max(1, textRect.Height / 2),
Math.Max(1, groupBox.Width - 2),
Math.Max(1, groupBox.Height - (textRect.Height / 2) - 2));
using (var borderPen = new Pen(DarkThemeGroupBorderColor))
{
e.Graphics.DrawRectangle(borderPen, borderRect);
}
using (var titleBackBrush = new SolidBrush(DarkThemeGroupTitleBackColor))
{
e.Graphics.FillRectangle(titleBackBrush, textRect);
}
TextRenderer.DrawText(
e.Graphics,
groupText,
groupBox.Font,
textRect,
DarkThemeForeColor,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
}
/// <summary>
/// 设置按钮控件暗色样式。
/// </summary>
/// <param name="button">目标按钮。</param>
private void ApplyButtonTheme(Button button)
{
button.FlatStyle = FlatStyle.Flat;
button.FlatAppearance.BorderSize = 1;
button.FlatAppearance.BorderColor = DarkThemeAccentColor;
button.FlatAppearance.MouseOverBackColor = Color.FromArgb(52, 52, 56);
button.FlatAppearance.MouseDownBackColor = Color.FromArgb(62, 62, 66);
button.BackColor = DarkThemePanelBackColor;
button.ForeColor = DarkThemeForeColor;
button.UseVisualStyleBackColor = false;
button.Paint -= ThemedButton_Paint;
button.Paint += ThemedButton_Paint;
}
/// <summary>
/// 自定义绘制按钮文本,确保暗色主题下文字始终可读。
/// </summary>
/// <param name="sender">事件发起对象。</param>
/// <param name="e">绘制事件参数。</param>
private void ThemedButton_Paint(object sender, PaintEventArgs e)
{
var button = sender as Button;
if (button == null)
{
return;
}
var textColor = button.Enabled ? DarkThemeForeColor : Color.FromArgb(170, 170, 170);
var textBounds = button.ClientRectangle;
TextRenderer.DrawText(
e.Graphics,
button.Text,
button.Font,
textBounds,
textColor,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
}
/// <summary>
/// 设置文本框控件暗色样式。
/// </summary>
/// <param name="textBox">目标文本框。</param>
private void ApplyTextBoxTheme(TextBox textBox)
{
textBox.BorderStyle = BorderStyle.FixedSingle;
textBox.BackColor = DarkThemeInputBackColor;
textBox.ForeColor = DarkThemeForeColor;
}
/// <summary>
/// 设置下拉框控件暗色样式,并接管绘制以统一列表区域颜色。
/// </summary>
/// <param name="comboBox">目标下拉框。</param>
private void ApplyComboBoxTheme(ComboBox comboBox)
{
comboBox.FlatStyle = FlatStyle.Flat;
comboBox.BackColor = DarkThemeInputBackColor;
comboBox.ForeColor = DarkThemeForeColor;
comboBox.DrawMode = DrawMode.OwnerDrawFixed;
comboBox.DrawItem -= ThemedComboBox_DrawItem;
comboBox.DrawItem += ThemedComboBox_DrawItem;
}
/// <summary>
/// 设置列表框控件暗色样式,并接管绘制以统一选中高亮。
/// </summary>
/// <param name="listBox">目标列表框。</param>
private void ApplyListBoxTheme(ListBox listBox)
{
listBox.BorderStyle = BorderStyle.FixedSingle;
listBox.BackColor = DarkThemeInputBackColor;
listBox.ForeColor = DarkThemeForeColor;
listBox.Font = new Font("微软雅黑", 9F, FontStyle.Regular, GraphicsUnit.Point, 134);
listBox.ItemHeight = Math.Max(18, TextRenderer.MeasureText("日志", listBox.Font).Height + 2);
listBox.DrawMode = DrawMode.OwnerDrawFixed;
listBox.DrawItem -= ThemedListBox_DrawItem;
listBox.DrawItem += ThemedListBox_DrawItem;
}
/// <summary>
/// 设置标签控件暗色样式。
/// </summary>
/// <param name="label">目标标签。</param>
private void ApplyLabelTheme(Label label)
{
label.BackColor = Color.Transparent;
label.ForeColor = DarkThemeForeColor;
}
/// <summary>
/// 设置图片显示区域暗色样式。
/// </summary>
/// <param name="pictureBox">目标图片框。</param>
private void ApplyPictureBoxTheme(PictureBox pictureBox)
{
pictureBox.BackColor = Color.Black;
pictureBox.BorderStyle = BorderStyle.FixedSingle;
}
/// <summary>
/// 自定义绘制暗色下拉框项。
/// </summary>
/// <param name="sender">事件发起对象。</param>
/// <param name="e">绘制事件参数。</param>
private void ThemedComboBox_DrawItem(object sender, DrawItemEventArgs e)
{
e.DrawBackground();
if (e.Index < 0)
{
return;
}
var comboBox = sender as ComboBox;
var itemText = comboBox?.Items[e.Index]?.ToString() ?? string.Empty;
var isSelected = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
var backColor = isSelected ? DarkThemeSelectedBackColor : DarkThemeInputBackColor;
using (var backBrush = new SolidBrush(backColor))
using (var textBrush = new SolidBrush(DarkThemeForeColor))
{
e.Graphics.FillRectangle(backBrush, e.Bounds);
e.Graphics.DrawString(itemText, e.Font, textBrush, e.Bounds);
}
e.DrawFocusRectangle();
}
/// <summary>
/// 自定义绘制暗色列表框项。
/// </summary>
/// <param name="sender">事件发起对象。</param>
/// <param name="e">绘制事件参数。</param>
private void ThemedListBox_DrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index < 0)
{
return;
}
var listBox = sender as ListBox;
var itemText = listBox?.Items[e.Index]?.ToString() ?? string.Empty;
var isSelected = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
var backColor = isSelected ? DarkThemeSelectedBackColor : DarkThemeInputBackColor;
var textColor = listBox != null && listBox.Enabled ? DarkThemeForeColor : Color.FromArgb(170, 170, 170);
var textRect = new Rectangle(e.Bounds.X + 6, e.Bounds.Y + 1, Math.Max(1, e.Bounds.Width - 8), Math.Max(1, e.Bounds.Height - 2));
using (var backBrush = new SolidBrush(backColor))
using (var textBrush = new SolidBrush(textColor))
{
e.Graphics.FillRectangle(backBrush, e.Bounds);
e.Graphics.DrawString(itemText, e.Font, textBrush, textRect);
}
e.DrawFocusRectangle();
}
/// <summary>
/// 注册 VisionMaster 方案生命周期回调,用于关键节点日志输出。
/// </summary>
private void RegisterSolutionCallbacks()
{
_solutionManager.OnSolutionLoadBeginCallBack += OnSolutionLoadBegin;
_solutionManager.OnSolutionLoadProgressCallBack += OnSolutionLoadProgress;
_solutionManager.OnSolutionLoadEndCallBack += OnSolutionLoadEnd;
_solutionManager.OnSolutionSaveBeginCallBack += OnSolutionSaveBegin;
_solutionManager.OnSolutionSaveProgressCallBack += OnSolutionSaveProgress;
_solutionManager.OnSolutionSaveEndCallBack += OnSolutionSaveEnd;
}
/// <summary>
/// 注销 VisionMaster 回调,避免窗体关闭后的委托残留。
/// </summary>
private void UnregisterSolutionCallbacks()
{
_solutionManager.OnSolutionLoadBeginCallBack -= OnSolutionLoadBegin;
_solutionManager.OnSolutionLoadProgressCallBack -= OnSolutionLoadProgress;
_solutionManager.OnSolutionLoadEndCallBack -= OnSolutionLoadEnd;
_solutionManager.OnSolutionSaveBeginCallBack -= OnSolutionSaveBegin;
_solutionManager.OnSolutionSaveProgressCallBack -= OnSolutionSaveProgress;
_solutionManager.OnSolutionSaveEndCallBack -= OnSolutionSaveEnd;
}
/// <summary>
/// 注册 VM.Core 执行状态回调。
/// </summary>
private void RegisterVmSolutionCallbacks()
{
if (_isVmSolutionEventRegistered)
{
return;
}
VmSolution.OnWorkStatusEvent += VmSolution_OnWorkStatusEvent;
_isVmSolutionEventRegistered = true;
}
/// <summary>
/// 注销 VM.Core 执行状态回调。
/// </summary>
private void UnregisterVmSolutionCallbacks()
{
if (!_isVmSolutionEventRegistered)
{
return;
}
VmSolution.OnWorkStatusEvent -= VmSolution_OnWorkStatusEvent;
_isVmSolutionEventRegistered = false;
}
/// <summary>
/// 解绑当前流程结束回调,防止重复订阅和对象残留。
/// </summary>
private void UnbindProcedureWorkEndCallback()
{
if (_currentProcedure == null)
{
return;
}
try
{
_currentProcedure.OnWorkEndStatusCallBack -= Process_OnWorkEndStatusCallBack;
}
catch (Exception ex)
{
WriteLog($"解绑流程结束回调异常:{ex.Message}");
}
}
/// <summary>
/// 更新连续执行状态及按钮文案。
/// </summary>
/// <param name="isRunning">是否连续执行中。</param>
/// <param name="procedureName">连续执行流程名称。</param>
private void SetContinuousRunState(bool isRunning, string procedureName)
{
_isContinuousRunning = isRunning;
_continuousProcedureName = isRunning ? (procedureName ?? string.Empty) : string.Empty;
btnProExecAlway.Text = isRunning ? "停止连续" : "连续执行";
}
/// <summary>
/// 停止当前连续执行流程(若有),并重置连续执行状态。
/// </summary>
/// <param name="reason">停止原因(用于日志)。</param>
private void StopContinuousRunIfNeeded(string reason)
{
if (!_isContinuousRunning)
{
return;
}
try
{
if (_currentProcedure != null)
{
_currentProcedure.ContinuousRunEnable = false;
}
if (!string.IsNullOrWhiteSpace(_continuousProcedureName))
{
WriteLog($"连续执行流程已停止:{_continuousProcedureName}{reason}");
}
else
{
WriteLog($"连续执行流程已停止({reason})。");
}
}
catch (Exception ex)
{
WriteLog($"停止连续执行异常:{ex.Message}");
}
finally
{
SetContinuousRunState(false, string.Empty);
}
}
/// <summary>
/// 释放 VisionMaster 相关资源(幂等,允许重复调用)。
/// </summary>
private void ReleaseSdkResources()
{
if (_isClosingResourcesReleased)
{
return;
}
_isClosingResourcesReleased = true;
try
{
UnregisterSolutionCallbacks();
UnregisterVmSolutionCallbacks();
if (_activeBcrTool != null)
{
_activeBcrTool.ModuleResultCallBackArrived -= BcrModuleResultCallBackArrived;
_activeBcrTool = null;
}
StopAutoAssemblyIfRunning("窗体关闭释放资源");
_autoAssemblyController.StatusChanged -= AutoAssemblyController_StatusChanged;
_autoAssemblyController.Dispose();
StopContinuousRunIfNeeded("窗体关闭释放资源");
UnbindProcedureWorkEndCallback();
_currentProcedure = null;
vmParamsConfigControl1.ModuleSource = null;
ClearPictureBoxImage();
if (_isSolutionLoaded)
{
_solutionManager.CloseSolution();
_isSolutionLoaded = false;
}
_currentSolutionPath = string.Empty;
}
catch (Exception ex)
{
WriteLog($"关闭窗体时释放资源异常:{ex.Message}");
}
}
/// <summary>
/// 方案加载开始回调。
/// </summary>
private void OnSolutionLoadBegin(ImvsSdkDefine.IMVS_SOLUTION_LOAD_BEGEIN_INFO beginInfo)
{
_isSolutionLoading = true;
WriteLog("方案加载开始。");
if (!IsHandleCreated || IsDisposed)
{
return;
}
BeginInvoke(new Action(() => UpdateUiState(_isBusy)));
}
/// <summary>
/// 方案加载进度回调。
/// </summary>
private void OnSolutionLoadProgress(ImvsSdkDefine.IMVS_SOLUTION_LOAD_PROCESS_INFO progressInfo)
{
WriteLog($"方案加载进度:{progressInfo.nProcess}%");
}
/// <summary>
/// 方案加载结束回调。
/// </summary>
private void OnSolutionLoadEnd(ImvsSdkDefine.IMVS_SOLUTION_LOAD_END_INFO endInfo)
{
_isSolutionLoading = false;
WriteLog($"方案加载结束,状态码:{endInfo.nStatus}");
if (!IsHandleCreated || IsDisposed)
{
return;
}
BeginInvoke(new Action(() =>
{
try
{
if (endInfo.nStatus == 0)
{
_currentSolutionPath = _pendingLoadSolutionPath;
_isSolutionLoaded = true;
BindFirstModuleToRenderControl();
UpdateProcedureList();
StartAutoAssemblyIfConfigured();
WriteLog($"方案加载成功:{_currentSolutionPath}");
MessageBox.Show(this, "方案加载成功。", "加载方案", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
_isSolutionLoaded = false;
StopAutoAssemblyIfRunning("方案加载失败");
ResetProcedureAndModuleState();
MessageBox.Show(this, $"方案加载失败,状态码:{endInfo.nStatus}", "加载方案", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
catch (Exception ex)
{
_isSolutionLoaded = false;
StopAutoAssemblyIfRunning("加载方案回调异常");
ResetProcedureAndModuleState();
HandleException("加载方案回调处理", ex);
}
finally
{
_pendingLoadSolutionPath = string.Empty;
UpdateUiState(false);
}
}));
}
/// <summary>
/// 方案保存开始回调。
/// </summary>
private void OnSolutionSaveBegin(ImvsSdkDefine.IMVS_SOLUTION_SAVE_BEGEIN_INFO beginInfo)
{
WriteLog("方案保存开始。");
}
/// <summary>
/// 方案保存进度回调。
/// </summary>
private void OnSolutionSaveProgress(ImvsSdkDefine.IMVS_SOLUTION_SAVE_PROCESS_INFO progressInfo)
{
WriteLog($"方案保存进度:{progressInfo.nProcess}%");
}
/// <summary>
/// 方案保存结束回调。
/// </summary>
private void OnSolutionSaveEnd(ImvsSdkDefine.IMVS_SOLUTION_SAVE_END_INFO endInfo)
{
WriteLog($"方案保存结束,状态码:{endInfo.nStatus}");
}
/// <summary>
/// VM.Core 方案执行状态回调。
/// </summary>
/// <param name="workStatusInfo">执行状态信息。</param>
private void VmSolution_OnWorkStatusEvent(ImvsSdkDefine.IMVS_MODULE_WORK_STAUS workStatusInfo)
{
if (!IsHandleCreated || IsDisposed)
{
return;
}
BeginInvoke(new Action(() =>
{
try
{
if (workStatusInfo.nWorkStatus != 0)
{
return;
}
if (_currentProcedure == null)
{
return;
}
// 连续执行阶段仅使用流程结束回调做渲染,避免与流程回调并发读取同一份结果。
if (_isContinuousRunning)
{
return;
}
RenderProcedureResultToPictureBox(_currentProcedure, "方案执行状态回调");
}
catch (Exception ex)
{
WriteLog($"方案执行回调异常:{ex.Message}");
}
}));
}
/// <summary>
/// 流程执行结束回调:读取输出并渲染到 pictureBox。
/// </summary>
private void Process_OnWorkEndStatusCallBack(object sender, EventArgs e)
{
try
{
var procedure = sender as VmProcedure;
if (procedure == null)
{
return;
}
RenderProcedureResultToPictureBox(procedure, "流程结束回调");
}
catch (Exception ex)
{
WriteLog($"流程执行回调异常:{ex.Message}");
}
}
/// <summary>
/// 将流程结果图像、ROI、识别文本渲染到 pictureBox。
/// </summary>
/// <param name="procedure">流程对象。</param>
/// <param name="triggerSource">触发源描述(用于日志)。</param>
private void RenderProcedureResultToPictureBox(VmProcedure procedure, string triggerSource)
{
if (procedure == null)
{
return;
}
if (System.Threading.Interlocked.Exchange(ref _renderingGuard, 1) == 1)
{
return;
}
try
{
var result1 = GetProcedureOutputInt(procedure, _renderResultIntOutputName1);
var result2 = GetProcedureOutputInt(procedure, _renderResultIntOutputName2);
var isResult1Ok = string.Equals(result1?.Trim(), "1", StringComparison.Ordinal);
var isResult2Ok = string.Equals(result2?.Trim(), "1", StringComparison.Ordinal);
var pairResult = isResult1Ok && isResult2Ok ? "OK" : "NG";
var overlayText = $"R1={result1}, R2={result2}, Total={pairResult}";
var bitmap = GetProcedureOutputBitmap(procedure, _renderImageOutputName);
if (bitmap == null)
{
WriteLog($"{triggerSource}未读取到可渲染图像,固定输出名:{_renderImageOutputName}");
return;
}
try
{
var primaryRoiList = GetProcedureOutputBoxList(procedure, _renderRoiOutputName1);
var secondaryRoiList = GetProcedureOutputBoxList(procedure, _renderRoiOutputName2);
var mergedRoiList = MergeRoiLists(primaryRoiList, secondaryRoiList);
var roiColor = pairResult == "OK" ? Color.Lime : Color.Red;
var renderBitmap = DrawROIOnBitmap(bitmap, mergedRoiList, roiColor, 2, overlayText);
SetPictureBoxImage(renderBitmap);
WriteLog($"{triggerSource}渲染完成Result1={result1}Result2={result2}Total={pairResult}ROI数={mergedRoiList.Count}");
}
finally
{
bitmap.Dispose();
}
}
finally
{
System.Threading.Interlocked.Exchange(ref _renderingGuard, 0);
}
}
/// <summary>
/// 从流程输出中读取图像并转换为 Bitmap。
/// </summary>
/// <param name="procedure">流程对象。</param>
/// <param name="outputNames">候选输出名列表。</param>
/// <returns>Bitmap 对象;读取失败返回 null。</returns>
private Bitmap GetProcedureOutputBitmap(VmProcedure procedure, params string[] outputNames)
{
foreach (var outputName in outputNames)
{
if (string.IsNullOrWhiteSpace(outputName))
{
continue;
}
try
{
var imageData = procedure.ModuResult.GetOutputImageV2(outputName);
if (imageData == null)
{
continue;
}
var bitmap = imageData.ToBitmap();
if (bitmap != null)
{
return bitmap;
}
}
catch
{
// 候选输出名逐个容错尝试,避免单个输出不存在导致主流程中断。
}
}
return null;
}
/// <summary>
/// 从流程输出中读取 ROI 列表。
/// </summary>
/// <param name="procedure">流程对象。</param>
/// <param name="outputNames">候选输出名列表。</param>
/// <returns>ROI 列表;读取失败返回空列表。</returns>
private List<RectBox> GetProcedureOutputBoxList(VmProcedure procedure, params string[] outputNames)
{
foreach (var outputName in outputNames)
{
if (string.IsNullOrWhiteSpace(outputName))
{
continue;
}
try
{
var boxList = procedure.ModuResult.GetOutputBoxArray(outputName);
if (boxList != null)
{
return boxList;
}
}
catch
{
// 候选输出名逐个容错尝试,避免单个输出不存在导致主流程中断。
}
}
return new List<RectBox>();
}
/// <summary>
/// 合并多组 ROI 列表,避免流程多目标输出时遗漏展示。
/// </summary>
/// <param name="roiGroups">待合并 ROI 列表集合。</param>
/// <returns>合并后的 ROI 列表。</returns>
private static List<RectBox> MergeRoiLists(params List<RectBox>[] roiGroups)
{
var merged = new List<RectBox>();
if (roiGroups == null || roiGroups.Length == 0)
{
return merged;
}
foreach (var roiGroup in roiGroups)
{
if (roiGroup == null || roiGroup.Count == 0)
{
continue;
}
merged.AddRange(roiGroup);
}
return merged;
}
/// <summary>
/// 在图像上绘制 ROI 轮廓和识别文本。
/// </summary>
/// <param name="sourceBitmap">源图像。</param>
/// <param name="boxList">ROI 列表。</param>
/// <param name="color">ROI 颜色。</param>
/// <param name="thickness">线宽。</param>
/// <param name="code">识别文本。</param>
/// <returns>绘制后的新 Bitmap。</returns>
private Bitmap DrawROIOnBitmap(Bitmap sourceBitmap, List<RectBox> boxList, Color color, int thickness, string code)
{
if (sourceBitmap == null)
{
return null;
}
var bitmap = new Bitmap(sourceBitmap);
using (var graphics = Graphics.FromImage(bitmap))
using (var pen = new Pen(color, thickness))
{
if (boxList != null && boxList.Count > 0)
{
foreach (var rect in boxList)
{
var cx = rect.CenterPoint.X;
var cy = rect.CenterPoint.Y;
var width = rect.BoxWidth;
var height = rect.BoxHeight;
var angle = rect.Angle;
var points = new System.Drawing.PointF[4];
points[0] = new System.Drawing.PointF(-width / 2f, -height / 2f);
points[1] = new System.Drawing.PointF(width / 2f, -height / 2f);
points[2] = new System.Drawing.PointF(width / 2f, height / 2f);
points[3] = new System.Drawing.PointF(-width / 2f, height / 2f);
var rad = angle * Math.PI / 180.0;
for (var i = 0; i < points.Length; i++)
{
var x = points[i].X;
var y = points[i].Y;
points[i].X = (float)(x * Math.Cos(rad) - y * Math.Sin(rad) + cx);
points[i].Y = (float)(x * Math.Sin(rad) + y * Math.Cos(rad) + cy);
}
graphics.DrawPolygon(pen, points);
}
}
if (!string.IsNullOrWhiteSpace(code))
{
using (var font = new Font("Arial", 16, FontStyle.Bold))
using (var textBrush = new SolidBrush(Color.Orange))
using (var backgroundBrush = new SolidBrush(Color.FromArgb(160, 0, 0, 0)))
{
var textSize = graphics.MeasureString(code, font);
var x = Math.Max(0, bitmap.Width - textSize.Width - 12f);
const float y = 10f;
graphics.FillRectangle(backgroundBrush, x - 4f, y - 2f, textSize.Width + 8f, textSize.Height + 4f);
graphics.DrawString(code, font, textBrush, x, y);
}
}
}
return bitmap;
}
/// <summary>
/// 安全更新 pictureBox 图像并释放旧图,避免内存泄漏。
/// </summary>
/// <param name="bitmap">新图像。</param>
private void SetPictureBoxImage(Bitmap bitmap)
{
if (bitmap == null)
{
return;
}
if (!IsHandleCreated || IsDisposed)
{
bitmap.Dispose();
return;
}
void UpdateAction()
{
var oldImage = pictureBox1.Image;
pictureBox1.Image = bitmap;
oldImage?.Dispose();
}
if (pictureBox1.InvokeRequired)
{
pictureBox1.BeginInvoke(new Action(UpdateAction));
}
else
{
UpdateAction();
}
}
/// <summary>
/// 清空 pictureBox 图像并释放旧图。
/// </summary>
private void ClearPictureBoxImage()
{
if (!IsHandleCreated || IsDisposed)
{
return;
}
void ClearAction()
{
var oldImage = pictureBox1.Image;
pictureBox1.Image = null;
oldImage?.Dispose();
}
if (pictureBox1.InvokeRequired)
{
pictureBox1.BeginInvoke(new Action(ClearAction));
}
else
{
ClearAction();
}
}
/// <summary>
/// 条码模块执行结果回调。
/// </summary>
private void BcrModuleResultCallBackArrived(object sender, EventArgs e)
{
if (!(sender is IMVSBcrModuTool tool))
{
return;
}
var codeStr = string.Empty;
try
{
if (tool.ModuResult?.CodeStr != null && tool.ModuResult.CodeStr.Count > 0)
{
codeStr = tool.ModuResult.CodeStr[0];
}
}
catch (Exception ex)
{
WriteLog($"读取条码结果异常:{ex.Message}");
}
if (!IsHandleCreated || IsDisposed)
{
return;
}
BeginInvoke(new Action(() =>
{
WriteLog($"条码识别结果:{codeStr}");
}));
}
/// <summary>
/// 统一日志输出(当前工程无独立日志控件,先写控制台)。
/// </summary>
/// <param name="message">日志内容。</param>
private void WriteLog(string message)
{
var consoleLogText = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}";
var listBoxLogText = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
Console.WriteLine(consoleLogText);
if (!IsHandleCreated || IsDisposed)
{
return;
}
if (listBox1.InvokeRequired)
{
listBox1.BeginInvoke(new Action(() =>
{
listBox1.Items.Add(listBoxLogText);
listBox1.TopIndex = listBox1.Items.Count - 1;
}));
}
else
{
listBox1.Items.Add(listBoxLogText);
listBox1.TopIndex = listBox1.Items.Count - 1;
}
}
/// <summary>
/// 更新按钮启用状态,防止流程重入和非法操作。
/// </summary>
/// <param name="busy">当前是否忙碌。</param>
private void UpdateUiState(bool busy)
{
_isBusy = busy;
var canLoadNewSolution = !busy && !_isSolutionLoading;
var canOperateLoadedSolution = !busy && _isSolutionLoaded && !_isSolutionLoading;
btnSelctedSolution.Enabled = !busy && !_isSolutionLoading;
btnLoadSolution.Enabled = canLoadNewSolution;
btnExecutSolution.Enabled = canOperateLoadedSolution;
btnSaveSolution.Enabled = canOperateLoadedSolution;
btnProImport.Enabled = canOperateLoadedSolution;
btnProExpo.Enabled = canOperateLoadedSolution;
btnProDel.Enabled = canOperateLoadedSolution;
btnProExecOnce.Enabled = canOperateLoadedSolution;
btnProExecAlway.Enabled = canOperateLoadedSolution;
btnProExecStop.Enabled = canOperateLoadedSolution && _isContinuousRunning;
btnModuleBindingPar.Enabled = canOperateLoadedSolution;
btnModuleExec.Enabled = canOperateLoadedSolution;
btnModuleRenderResult.Enabled = canOperateLoadedSolution;
btnAutoRun.Enabled = canOperateLoadedSolution;
cmbProcdure.Enabled = canOperateLoadedSolution;
cmbModule.Enabled = canOperateLoadedSolution;
// 保持连续执行按钮文案与状态一致。
btnProExecAlway.Text = _isContinuousRunning ? "停止连续" : "连续执行";
SyncAutoRunButtonState();
}
/// <summary>
/// 校验并返回文本框中的方案路径。
/// </summary>
/// <returns>合法的完整路径。</returns>
private string GetValidatedSolutionPath()
{
var path = txtSolutionAddress.Text?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(path))
{
throw new InvalidOperationException("请先选择方案文件。\n支持格式*.sol");
}
if (!File.Exists(path))
{
throw new FileNotFoundException("方案文件不存在,请重新选择。", path);
}
return path;
}
/// <summary>
/// 根据已加载方案,尝试把首个模块绑定到渲染控件以显示结果。
/// </summary>
private void BindFirstModuleToRenderControl()
{
// 界面已切换为 pictureBox 渲染,加载新方案时先清空上次显示图像。
ClearPictureBoxImage();
}
/// <summary>
/// 统一异常处理:记录日志并弹出错误提示。
/// </summary>
/// <param name="caption">弹窗标题。</param>
/// <param name="ex">异常对象。</param>
private void HandleException(string caption, Exception ex)
{
var vmException = VmExceptionTool.GetVmException(ex);
var errorMessage = vmException == null
? ex.Message
: $"{vmException.errorMessage} (Code={vmException.errorCode})";
WriteLog($"{caption}失败:{errorMessage}");
MessageBox.Show(this, errorMessage, caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
/// <summary>
/// 校验当前是否已加载方案。
/// </summary>
private bool EnsureSolutionLoaded(string operationName)
{
if (_isSolutionLoaded)
{
return true;
}
MessageBox.Show(this, $"请先加载方案后再{operationName}。", operationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
/// <summary>
/// 判断异常是否为“方案正在加载中”错误。
/// </summary>
/// <param name="ex">异常对象。</param>
/// <returns>若是加载中错误则返回 true。</returns>
private static bool IsSolutionLoadingVmException(Exception ex)
{
if (ex == null)
{
return false;
}
var vmException = VmExceptionTool.GetVmException(ex);
return vmException != null && vmException.errorCode == VmErrorCodeSolutionLoading;
}
/// <summary>
/// 判断异常是否为“模块已处于连续执行”错误。
/// </summary>
/// <param name="ex">异常对象。</param>
/// <returns>若是连续执行中错误则返回 true。</returns>
private static bool IsModuleContinueExecuteVmException(Exception ex)
{
if (ex == null)
{
return false;
}
var vmException = VmExceptionTool.GetVmException(ex);
return vmException != null && vmException.errorCode == VmErrorCodeModuleContinueExecute;
}
/// <summary>
/// 调用 SDK 加载方案,并针对“方案正在加载中”做有限重试。
/// </summary>
/// <param name="solutionPath">方案路径。</param>
/// <param name="retryCount">最大重试次数。</param>
/// <param name="retryDelayMs">每次重试间隔(毫秒)。</param>
private void LoadSolutionWithRetry(string solutionPath, int retryCount, int retryDelayMs)
{
Exception lastException = null;
for (var attempt = 0; attempt <= retryCount; attempt++)
{
try
{
_solutionManager.LoadSolution(solutionPath, string.Empty);
return;
}
catch (Exception ex)
{
if (!IsSolutionLoadingVmException(ex) || attempt >= retryCount)
{
throw;
}
lastException = ex;
WriteLog($"方案仍在加载中,准备重试:第{attempt + 1}/{retryCount}次,间隔{retryDelayMs}ms。");
System.Threading.Thread.Sleep(retryDelayMs);
}
}
if (lastException != null)
{
throw lastException;
}
}
/// <summary>
/// 从 AppSettings 读取配置值,读取失败或为空时返回默认值。
/// </summary>
/// <param name="key">配置键名。</param>
/// <param name="defaultValue">默认值。</param>
/// <returns>配置值或默认值。</returns>
private static string GetAppSettingOrDefault(string key, string defaultValue)
{
try
{
var value = ConfigurationManager.AppSettings[key];
return string.IsNullOrWhiteSpace(value) ? defaultValue : value.Trim();
}
catch
{
return defaultValue;
}
}
/// <summary>
/// 从 AppSettings 读取整型配置,失败时返回默认值。
/// </summary>
/// <param name="key">配置键名。</param>
/// <param name="defaultValue">默认值。</param>
/// <returns>解析后的整型值。</returns>
private static int GetAppSettingIntOrDefault(string key, int defaultValue)
{
try
{
var value = ConfigurationManager.AppSettings[key];
if (string.IsNullOrWhiteSpace(value))
{
return defaultValue;
}
return int.TryParse(value.Trim(), out var parsedValue) ? parsedValue : defaultValue;
}
catch
{
return defaultValue;
}
}
/// <summary>
/// 从 AppSettings 读取布尔配置,失败时返回默认值。
/// </summary>
/// <param name="key">配置键名。</param>
/// <param name="defaultValue">默认值。</param>
/// <returns>解析后的布尔值。</returns>
private static bool GetAppSettingBoolOrDefault(string key, bool defaultValue)
{
try
{
var value = ConfigurationManager.AppSettings[key];
if (string.IsNullOrWhiteSpace(value))
{
return defaultValue;
}
return bool.TryParse(value.Trim(), out var parsedValue) ? parsedValue : defaultValue;
}
catch
{
return defaultValue;
}
}
/// <summary>
/// 尝试获取当前选中的流程对象。
/// </summary>
private bool TryGetSelectedProcedure(out VmProcedure procedure)
{
procedure = null;
var selectedProcedureName = cmbProcdure.Text?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(selectedProcedureName))
{
MessageBox.Show(this, "请先在流程列表中选择目标流程。", "流程操作", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
procedure = VmSolution.Instance[selectedProcedureName] as VmProcedure;
if (procedure == null)
{
MessageBox.Show(this, $"未找到流程:{selectedProcedureName}。请刷新流程列表后重试。", "流程操作", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
return true;
}
/// <summary>
/// 尝试获取当前选中的模块对象。
/// </summary>
private bool TryGetSelectedModule(out VmModule module)
{
module = null;
var selectedProcedureName = cmbProcdure.Text?.Trim() ?? string.Empty;
var selectedModuleName = cmbModule.Text?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(selectedProcedureName) || string.IsNullOrWhiteSpace(selectedModuleName))
{
MessageBox.Show(this, "请先选择流程和模块。", "模块操作", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
module = VmSolution.Instance[$"{selectedProcedureName}.{selectedModuleName}"] as VmModule;
if (module == null)
{
MessageBox.Show(this, $"未找到模块:{selectedProcedureName}.{selectedModuleName}。", "模块操作", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
return true;
}
/// <summary>
/// 刷新流程下拉列表。
/// </summary>
private void UpdateProcedureList()
{
var previous = cmbProcdure.Text?.Trim() ?? string.Empty;
cmbProcdure.Items.Clear();
var processInfoList = VmSolution.Instance.GetAllProcedureList();
if (processInfoList.nNum <= 0 || processInfoList.astProcessInfo == null)
{
WriteLog("方案中未检测到流程。");
cmbModule.Items.Clear();
return;
}
for (var i = 0; i < processInfoList.nNum; i++)
{
cmbProcdure.Items.Add(processInfoList.astProcessInfo[i].strProcessName);
}
if (!string.IsNullOrWhiteSpace(previous) && cmbProcdure.Items.Contains(previous))
{
cmbProcdure.SelectedItem = previous;
}
else
{
cmbProcdure.SelectedIndex = 0;
}
}
/// <summary>
/// 刷新当前流程对应模块列表。
/// </summary>
private void UpdateModuleList()
{
cmbModule.Items.Clear();
if (!TryGetSelectedProcedure(out var procedure))
{
return;
}
var moduleInfoList = procedure.GetAllModuleList();
if (moduleInfoList.nNum <= 0 || moduleInfoList.astModuleInfo == null)
{
WriteLog($"流程[{cmbProcdure.Text}]中未检测到模块。");
return;
}
for (var i = 0; i < moduleInfoList.nNum; i++)
{
cmbModule.Items.Add(moduleInfoList.astModuleInfo[i].strDisplayName);
}
cmbModule.SelectedIndex = 0;
}
/// <summary>
/// 清理流程与模块相关状态。
/// </summary>
private void ResetProcedureAndModuleState()
{
StopAutoAssemblyIfRunning("重置流程与模块状态");
StopContinuousRunIfNeeded("重置流程与模块状态");
UnbindProcedureWorkEndCallback();
_currentProcedure = null;
if (_activeBcrTool != null)
{
_activeBcrTool.ModuleResultCallBackArrived -= BcrModuleResultCallBackArrived;
_activeBcrTool = null;
}
cmbProcdure.Items.Clear();
cmbModule.Items.Clear();
vmParamsConfigControl1.ModuleSource = null;
ClearPictureBoxImage();
}
/// <summary>
/// 读取流程整型输出(多候选容错读取)。
/// </summary>
private string GetProcedureOutputInt(VmProcedure procedure, params string[] outputNames)
{
if (procedure == null || outputNames == null || outputNames.Length == 0)
{
return string.Empty;
}
foreach (var outputName in outputNames)
{
if (string.IsNullOrWhiteSpace(outputName))
{
continue;
}
try
{
var output = procedure.ModuResult.GetOutputInt(outputName);
if (output.pIntVal != null && output.pIntVal.Length > 0)
{
return output.pIntVal[0].ToString();
}
}
catch
{
// 忽略输出不存在等异常,保持流程主链路稳定。
}
}
return string.Empty;
}
/// <summary>
/// 选择方案按钮事件:打开文件选择框并回填路径。
/// </summary>
private void btnSelctedSolution_Click(object sender, EventArgs e)
{
using (var dialog = new OpenFileDialog())
{
dialog.Title = "请选择 VisionMaster 方案文件";
dialog.Filter = "VisionMaster方案文件 (*.sol)|*.sol|所有文件 (*.*)|*.*";
dialog.CheckFileExists = true;
dialog.Multiselect = false;
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
txtSolutionAddress.Text = dialog.FileName;
WriteLog($"已选择方案文件:{dialog.FileName}");
}
}
/// <summary>
/// 加载方案按钮事件:校验路径并调用 SolutionManager.LoadSolution。
/// </summary>
private void btnLoadSolution_Click(object sender, EventArgs e)
{
if (_isBusy || _isSolutionLoading)
{
if (_isSolutionLoading)
{
MessageBox.Show(this, "方案正在加载中,请等待加载完成后再操作。", "加载方案", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
return;
}
var keepBusyUntilLoadEndCallback = false;
try
{
UpdateUiState(true);
var solutionPath = GetValidatedSolutionPath();
// 设置服务就绪,避免部分环境首次调用方案接口时报服务未就绪异常。
_solutionManager.SetServerReadyEvent();
_isSolutionLoading = true;
_pendingLoadSolutionPath = solutionPath;
// 若已有方案,先关闭,避免方案切换场景下状态残留。
if (_isSolutionLoaded)
{
_solutionManager.CloseSolution();
_isSolutionLoaded = false;
ResetProcedureAndModuleState();
WriteLog("已关闭旧方案。\n开始加载新方案。");
}
LoadSolutionWithRetry(solutionPath, retryCount: 12, retryDelayMs: 200);
keepBusyUntilLoadEndCallback = true;
WriteLog($"方案加载请求已提交:{solutionPath}");
}
catch (Exception ex)
{
_isSolutionLoading = false;
_isSolutionLoaded = false;
_pendingLoadSolutionPath = string.Empty;
ResetProcedureAndModuleState();
HandleException("加载方案", ex);
}
finally
{
if (!keepBusyUntilLoadEndCallback)
{
UpdateUiState(false);
}
}
}
/// <summary>
/// 执行方案按钮事件:调用 SolutionManager.ExecuteOnce 执行一次流程。
/// </summary>
private void btnExecutSolution_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!_isSolutionLoaded)
{
MessageBox.Show(this, "请先加载方案后再执行。", "执行方案", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
UpdateUiState(true);
_solutionManager.ExecuteOnce();
WriteLog("方案执行完成ExecuteOnce。");
MessageBox.Show(this, "方案执行完成。", "执行方案", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
HandleException("执行方案", ex);
}
finally
{
UpdateUiState(false);
}
}
/// <summary>
/// 保存方案按钮事件:默认覆盖保存到当前方案路径。
/// </summary>
private void btnSaveSolution_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!_isSolutionLoaded)
{
MessageBox.Show(this, "请先加载方案后再保存。", "保存方案", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
UpdateUiState(true);
var savePath = string.IsNullOrWhiteSpace(_currentSolutionPath)
? GetValidatedSolutionPath()
: _currentSolutionPath;
_solutionManager.SaveSolution(savePath, string.Empty, IntPtr.Zero, 0);
WriteLog($"方案保存成功:{savePath}");
MessageBox.Show(this, "方案保存成功。", "保存方案", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
HandleException("保存方案", ex);
}
finally
{
UpdateUiState(false);
}
}
/// <summary>
/// 关闭窗体时释放 SDK 相关资源。
/// </summary>
/// <param name="e">事件参数。</param>
protected override void OnFormClosed(FormClosedEventArgs e)
{
try
{
ReleaseSdkResources();
}
finally
{
base.OnFormClosed(e);
}
}
/// <summary>
/// 导入流程按钮事件:从外部 <c>.prc</c> 文件导入流程到当前方案,并刷新流程列表。
/// 该方法包含完整的前置状态检查(忙碌态、方案是否已加载)与异常处理,避免 UI 重入导致的状态错乱。
/// </summary>
/// <param name="sender">事件源控件(导入按钮)。</param>
/// <param name="e">标准事件参数。</param>
private void btnProImport_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!EnsureSolutionLoaded("导入流程"))
{
return;
}
try
{
UpdateUiState(true);
string procedurePath;
using (var dialog = new OpenFileDialog())
{
dialog.Title = "请选择流程文件";
dialog.Filter = "流程文件 (*.prc)|*.prc|所有文件 (*.*)|*.*";
dialog.CheckFileExists = true;
dialog.Multiselect = false;
if (dialog.ShowDialog(this) != DialogResult.OK)
{
WriteLog("用户取消导入流程。\n");
return;
}
procedurePath = dialog.FileName;
}
// 通过 VM SDK 加载流程文件到当前方案上下文。
VmProcedure.Load(procedurePath);
UpdateProcedureList();
WriteLog($"流程导入成功:{procedurePath}");
MessageBox.Show(this, "流程导入成功。", "导入流程", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
HandleException("导入流程", ex);
}
finally
{
UpdateUiState(false);
}
}
/// <summary>
/// 导出流程按钮事件:将当前选中的流程导出为独立 <c>.prc</c> 文件。
/// </summary>
/// <param name="sender">事件源控件(导出按钮)。</param>
/// <param name="e">标准事件参数。</param>
private void btnProExpo_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!EnsureSolutionLoaded("导出流程"))
{
return;
}
if (!TryGetSelectedProcedure(out var procedure))
{
return;
}
using (var dialog = new SaveFileDialog())
{
dialog.Title = "请选择流程导出路径";
dialog.Filter = "流程文件 (*.prc)|*.prc|所有文件 (*.*)|*.*";
dialog.FileName = $"{cmbProcdure.Text}.prc";
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
try
{
UpdateUiState(true);
// 仅导出当前选中流程,不影响方案内其它流程。
procedure.SaveAs(dialog.FileName);
WriteLog($"流程导出成功:{dialog.FileName}");
MessageBox.Show(this, "流程导出成功。", "导出流程", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
HandleException("导出流程", ex);
}
finally
{
UpdateUiState(false);
}
}
}
/// <summary>
/// 删除流程按钮事件:删除当前选中的流程,并同步清理界面绑定状态(模块列表、参数面板、渲染图像)。
/// </summary>
/// <param name="sender">事件源控件(删除按钮)。</param>
/// <param name="e">标准事件参数。</param>
private void btnProDel_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!EnsureSolutionLoaded("删除流程"))
{
return;
}
var selectedProcedureName = cmbProcdure.Text?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(selectedProcedureName))
{
MessageBox.Show(this, "请先选择需要删除的流程。", "删除流程", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (MessageBox.Show(this, $"确认删除流程:{selectedProcedureName} 吗?", "删除流程", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
{
return;
}
try
{
UpdateUiState(true);
// 从方案中删除流程,并清理当前流程回调绑定,避免悬挂引用。
VmSolution.Instance.DeleteOneProcedure(selectedProcedureName);
UnbindProcedureWorkEndCallback();
_currentProcedure = null;
vmParamsConfigControl1.ModuleSource = null;
ClearPictureBoxImage();
UpdateProcedureList();
cmbModule.Items.Clear();
WriteLog($"删除流程成功:{selectedProcedureName}");
}
catch (Exception ex)
{
HandleException("删除流程", ex);
}
finally
{
UpdateUiState(false);
}
}
/// <summary>
/// 执行一次按钮事件:对当前选中流程执行单次运行,并在流程结束回调中刷新渲染结果。
/// </summary>
/// <param name="sender">事件源控件(执行一次按钮)。</param>
/// <param name="e">标准事件参数。</param>
private void btnProExecOnce_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!EnsureSolutionLoaded("执行流程"))
{
return;
}
if (!TryGetSelectedProcedure(out var procedure))
{
return;
}
try
{
UpdateUiState(true);
// 执行单次前先停掉连续执行,保证流程运行模式互斥。
StopContinuousRunIfNeeded("执行单次流程");
UnbindProcedureWorkEndCallback();
_currentProcedure = procedure;
_currentProcedure.OnWorkEndStatusCallBack -= Process_OnWorkEndStatusCallBack;
_currentProcedure.OnWorkEndStatusCallBack += Process_OnWorkEndStatusCallBack;
_currentProcedure.Run();
WriteLog($"执行指定流程一次成功:{cmbProcdure.Text}");
}
catch (Exception ex)
{
HandleException("执行指定流程一次", ex);
}
finally
{
UpdateUiState(false);
}
}
/// <summary>
/// 连续执行按钮事件(启停一体):
/// - 若当前未连续运行,则启动选中流程的连续执行;
/// - 若当前已连续运行,则先停止,再根据选择情况决定是否切换到新流程继续运行。
/// </summary>
/// <param name="sender">事件源控件(连续执行按钮)。</param>
/// <param name="e">标准事件参数。</param>
private void btnProExecAlway_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!EnsureSolutionLoaded("连续执行流程"))
{
return;
}
if (!TryGetSelectedProcedure(out var procedure))
{
return;
}
var selectedProcedureName = cmbProcdure.Text?.Trim() ?? string.Empty;
try
{
UpdateUiState(true);
if (_isContinuousRunning)
{
// 当前按钮处于“停止连续”语义:点击后先停止;若选中流程已变更,则继续启动新流程连续执行。
var runningProcedureName = _continuousProcedureName;
StopContinuousRunIfNeeded("用户点击连续执行按钮");
if (string.Equals(runningProcedureName, selectedProcedureName, StringComparison.OrdinalIgnoreCase))
{
return;
}
}
// 显式开启 SDK 连续执行标记,并重新绑定流程结束回调。
procedure.ContinuousRunEnable = true;
UnbindProcedureWorkEndCallback();
_currentProcedure = procedure;
_currentProcedure.OnWorkEndStatusCallBack -= Process_OnWorkEndStatusCallBack;
_currentProcedure.OnWorkEndStatusCallBack += Process_OnWorkEndStatusCallBack;
_currentProcedure.Run();
SetContinuousRunState(true, selectedProcedureName);
WriteLog($"连续执行流程已开启:{cmbProcdure.Text}");
}
catch (Exception ex)
{
if (IsModuleContinueExecuteVmException(ex))
{
SetContinuousRunState(true, selectedProcedureName);
WriteLog($"流程[{selectedProcedureName}]已处于连续执行状态,已同步运行状态。\n若需停止请点击“停止执行”。");
return;
}
HandleException("连续执行流程", ex);
}
finally
{
UpdateUiState(false);
}
}
/// <summary>
/// 停止执行按钮事件:在连续执行模式下,主动停止当前流程连续运行并同步按钮状态。
/// </summary>
/// <param name="sender">事件源控件(停止按钮)。</param>
/// <param name="e">标准事件参数。</param>
private void btnProExecStop_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!_isSolutionLoaded)
{
return;
}
if (!_isContinuousRunning)
{
WriteLog("当前未处于连续执行状态,无需停止。");
return;
}
// 仅处理连续执行停止,不触发新的执行请求。
StopContinuousRunIfNeeded("用户点击停止按钮");
UpdateUiState(false);
}
/// <summary>
/// 绑定参数按钮事件:将当前模块绑定到参数配置控件,供用户查看或调整模块参数。
/// </summary>
/// <param name="sender">事件源控件(绑定参数按钮)。</param>
/// <param name="e">标准事件参数。</param>
private void btnModuleBindingPar_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!EnsureSolutionLoaded("绑定模块参数"))
{
return;
}
if (!TryGetSelectedModule(out var module))
{
return;
}
try
{
// 将模块对象注入参数控件,参数控件内部负责参数树加载与编辑。
vmParamsConfigControl1.ModuleSource = module;
WriteLog($"绑定模块参数成功:{cmbProcdure.Text}.{cmbModule.Text}");
}
catch (Exception ex)
{
HandleException("绑定模块参数", ex);
}
}
/// <summary>
/// 执行模块按钮事件:执行当前模块,并尝试刷新流程渲染结果;
/// 若为条码模块,则自动注册条码结果回调用于实时日志输出。
/// </summary>
/// <param name="sender">事件源控件(执行模块按钮)。</param>
/// <param name="e">标准事件参数。</param>
private void btnModuleExec_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!EnsureSolutionLoaded("执行模块"))
{
return;
}
if (!TryGetSelectedModule(out var module))
{
return;
}
try
{
UpdateUiState(true);
// 先执行模块本体,再根据当前流程输出刷新界面渲染。
module.Run();
if (TryGetSelectedProcedure(out var procedure))
{
_currentProcedure = procedure;
RenderProcedureResultToPictureBox(procedure, "模块执行");
}
if (_activeBcrTool != null)
{
_activeBcrTool.ModuleResultCallBackArrived -= BcrModuleResultCallBackArrived;
_activeBcrTool = null;
}
// 若当前模块支持条码结果回调,启用回调并做去重绑定。
var bcrTool = module as IMVSBcrModuTool;
if (bcrTool != null)
{
_activeBcrTool = bcrTool;
_activeBcrTool.EnableResultCallback();
_activeBcrTool.ModuleResultCallBackArrived -= BcrModuleResultCallBackArrived;
_activeBcrTool.ModuleResultCallBackArrived += BcrModuleResultCallBackArrived;
WriteLog($"模块[{cmbModule.Text}]执行完成,已启用条码结果回调。");
}
else
{
WriteLog($"模块[{cmbModule.Text}]执行完成(非条码模块)。");
}
}
catch (Exception ex)
{
HandleException("执行模块", ex);
}
finally
{
UpdateUiState(false);
}
}
/// <summary>
/// 渲染结果按钮事件:不重新执行模块,仅根据当前流程最新输出重绘图像/ROI/识别结果。
/// </summary>
/// <param name="sender">事件源控件(渲染结果按钮)。</param>
/// <param name="e">标准事件参数。</param>
private void btnModuleRenderResult_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!EnsureSolutionLoaded("渲染模块结果"))
{
return;
}
if (!TryGetSelectedModule(out var module))
{
return;
}
try
{
if (TryGetSelectedProcedure(out var procedure))
{
_currentProcedure = procedure;
RenderProcedureResultToPictureBox(procedure, "模块结果渲染");
}
WriteLog($"模块结果刷新完成:{cmbProcdure.Text}.{cmbModule.Text}");
}
catch (Exception ex)
{
HandleException("渲染模块结果", ex);
}
}
/// <summary>
/// 流程切换联动:自动刷新模块列表,并清空旧图像显示。
/// </summary>
private void cmbProcdure_SelectedIndexChanged(object sender, EventArgs e)
{
if (_isBusy || !_isSolutionLoaded)
{
return;
}
try
{
if (_isContinuousRunning && !string.Equals(_continuousProcedureName, cmbProcdure.Text?.Trim() ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
StopContinuousRunIfNeeded("切换流程");
}
UpdateModuleList();
vmParamsConfigControl1.ModuleSource = null;
ClearPictureBoxImage();
WriteLog($"流程切换完成:{cmbProcdure.Text}");
}
catch (Exception ex)
{
HandleException("切换流程", ex);
}
}
/// <summary>
/// 模块下拉展开事件:在用户展开模块下拉框时动态刷新模块列表,确保显示与当前流程一致。
/// </summary>
/// <param name="sender">事件源控件(模块下拉框)。</param>
/// <param name="e">标准事件参数。</param>
private void cmbModule_DropDown(object sender, EventArgs e)
{
if (_isBusy || !_isSolutionLoaded)
{
return;
}
try
{
UpdateModuleList();
WriteLog($"流程[{cmbProcdure.Text}]模块列表刷新完成。");
}
catch (Exception ex)
{
HandleException("刷新模块列表", ex);
}
}
/// <summary>
/// 流程下拉展开事件:在用户展开流程下拉框时动态刷新流程列表,避免旧缓存造成显示不一致。
/// </summary>
/// <param name="sender">事件源控件(流程下拉框)。</param>
/// <param name="e">标准事件参数。</param>
private void cmbProcdure_DropDown(object sender, EventArgs e)
{
if (_isBusy || !_isSolutionLoaded)
{
return;
}
try
{
UpdateProcedureList();
WriteLog("流程列表刷新完成。");
}
catch (Exception ex)
{
HandleException("刷新流程列表", ex);
}
}
/// <summary>
/// 窗体关闭前释放 SDK 相关资源。
/// </summary>
/// <param name="sender">事件源。</param>
/// <param name="e">关闭事件参数。</param>
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
ReleaseSdkResources();
}
/// <summary>
/// 主窗体加载事件。
/// 当前初始化逻辑已在构造函数中完成主题、回调、UI 状态),
/// 因此此处暂不添加额外业务处理,预留后续扩展入口。
/// </summary>
/// <param name="sender">事件源控件(主窗体)。</param>
/// <param name="e">标准事件参数。</param>
private void MainForm_Load(object sender, EventArgs e)
{
}
/// <summary>
/// 自动运行按钮事件。
/// 点击后执行自动流程启停切换:
/// - 未运行:启动自动状态机;
/// - 运行中:停止自动状态机。
/// </summary>
/// <param name="sender">事件源控件(自动运行按钮)。</param>
/// <param name="e">标准事件参数。</param>
private void btnAutoRun_Click(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (!EnsureSolutionLoaded("自动运行"))
{
return;
}
try
{
UpdateUiState(true);
if (_autoAssemblyController.IsRunning)
{
StopAutoAssemblyIfRunning("用户点击自动运行按钮停止");
WriteLog("自动装配流程已停止(按钮触发)。");
return;
}
// 启动自动流程前清理手动流程执行上下文,确保执行链路互斥。
StopContinuousRunIfNeeded("点击自动运行按钮");
UnbindProcedureWorkEndCallback();
_currentProcedure = null;
_autoAssemblyController.Start();
SyncAutoRunButtonState();
WriteLog("自动装配流程已启动(按钮触发)。");
}
catch (Exception ex)
{
HandleException("自动运行按钮操作", ex);
}
finally
{
UpdateUiState(false);
}
}
}
}