最终优化版・完整落地详细操作流程(修复 Timer 报错 + UI 卡死 + 事件重复订阅)一、整体架构回顾
手动扫码:主窗体调用ScannerHelper.ScanUUT1_Manual(),弹出FrmScanner调试窗体,支持手动选串口、扫码、保存配置
自动扫码:调用ScanUUT1_Auto(),后台静默串口扫码,无窗体
单 UUT 使用,XML 默认配置SingleUUT,后续可无缝扩展双 UUT
本次核心优化:修复 Timer 编译报错、UI 线程卡死、事件重复绑定、串口未释放问题
前置目录结构确认plaintextTestingInfo
├─ Common
│  ├─ Device
│  │  └─ Scanner
│  │     ├─ UutScannerMode.cs
│  │     ├─ ScannerConfig.cs
│  │     ├─ FrmScanner.cs(本次重点优化)
│  │     └─ ScannerHelper.cs(优化事件防重复订阅)
│  ├─ Models
│  │  └─ DeviceRootConfig.cs
│  └─ Utils
│     └─ XmlConfigHelper.cs
├─ 业务主窗体 Frmpannello.cs
步骤 1:基础 4 个配置类(代码无需修改,沿用之前正确版本)1.1 UutScannerMode.cscsharp运行namespace TestingInfo.Common.Device.Scanner
{
    public enum UutScannerMode
    {
        SingleUUT,
        DoubleUUT
    }
}
1.2 ScannerConfig.cscsharp运行using System.IO.Ports;

namespace TestingInfo.Common.Device.Scanner
{
    public class ScannerConfig
    {
        public string ComPort { get; set; }
        public int BaudRate { get; set; }
        public Parity Parity { get; set; }
        public int DataBits { get; set; }
        public StopBits StopBits { get; set; }
        public int TimeoutMs { get; set; }
    }

    public class DoubleUutScannerConfig
    {
        public UutScannerMode ScannerMode { get; set; }
        public ScannerConfig UUT1 { get; set; }
        public ScannerConfig UUT2 { get; set; }
    }
}
1.3 DeviceRootConfig.cscsharp运行using TestingInfo.Common.Device.Scanner;

namespace TestingInfo.Common.Models
{
    public class DeviceRootConfig
    {
        public DoubleUutScannerConfig ScannerConfig { get; set; }
    }
}
1.4 XmlConfigHelper.cscsharp运行using System;
using System.IO;
using System.Windows.Forms;
using System.Xml.Serialization;
using TestingInfo.Common.Models;
using TestingInfo.Common.Device.Scanner;

namespace TestingInfo.Common.Utils
{
    public static class XmlConfigHelper
    {
        private static readonly string _configFilePath = Path.Combine(Application.StartupPath, "DeviceConfig.xml");

        public static DeviceRootConfig LoadDeviceConfig()
        {
            if (!File.Exists(_configFilePath))
            {
                var defaultCfg = GetDefaultConfig();
                SaveDeviceConfig(defaultCfg);
                return defaultCfg;
            }
            try
            {
                using (var fs = new FileStream(_configFilePath, FileMode.Open))
                {
                    var xml = new XmlSerializer(typeof(DeviceRootConfig));
                    return xml.Deserialize(fs) as DeviceRootConfig;
                }
            }
            catch
            {
                return GetDefaultConfig();
            }
        }

        public static void SaveDeviceConfig(DeviceRootConfig config)
        {
            using (var fs = new FileStream(_configFilePath, FileMode.Create))
            {
                var xml = new XmlSerializer(typeof(DeviceRootConfig));
                xml.Serialize(fs, config);
            }
        }

        private static DeviceRootConfig GetDefaultConfig()
        {
            return new DeviceRootConfig
            {
                ScannerConfig = new DoubleUutScannerConfig
                {
                    ScannerMode = UutScannerMode.SingleUUT,
                    UUT1 = new ScannerConfig
                    {
                        ComPort = "COM3",
                        BaudRate = 115200,
                        Parity = Parity.None,
                        DataBits = 8,
                        StopBits = StopBits.One,
                        TimeoutMs = 2000
                    },
                    UUT2 = new ScannerConfig
                    {
                        ComPort = "COM4",
                        BaudRate = 115200,
                        Parity = Parity.None,
                        DataBits = 8,
                        StopBits = StopBits.One,
                        TimeoutMs = 2000
                    }
                }
            };
        }
    }
}
步骤 2:最终优化版 FrmScanner.cs(解决 Timer 报错 + 卡死,直接全选替换)前置窗体操作
窗体新增按钮:Name=btnSaveConfig,Text=保存当前工位配置,双击绑定点击事件
原有控件名称不变:cmbPortName、cmbBaudRate、cboParity、cboDataBits、cboStopBits、btnOpenScanner、btnCloseScanner、btnStartScan、btnClearLog、btnResetScan、txt_ScanRec
完整可直接粘贴代码csharp运行using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO.Ports;
using System.Text;
using System.Windows.Forms;
using TestingInfo.Common.Models;
using TestingInfo.Common.Utils;

namespace TestingInfo.Common.Device.Scanner
{
    public partial class FrmScanner : Form
    {
        private readonly ScannerConfig _currentConfig;
        private readonly int _uutNumber;
        private readonly SerialPort _newlandScanner = new SerialPort();

        public event Action<string> BarcodeResultReceived;

        private readonly byte[] _startScanCmd = { 0x01, 0x54, 0x04 };
        private readonly byte[] _stopScanCmd = { 0x01, 0x50, 0x04 };
        private const int ScanTimeoutMs = 2500;

        private string _savedValidBarcode = string.Empty;
        private bool _isScanLocked = false;
        private int _currentFailScanCount = 0;
        private readonly int _maxAllowScanCount = 6;
        private bool _isWaitingScanResult = false;

        public FrmScanner(ScannerConfig scannerConfig, int uutNo)
        {
            InitializeComponent();
            _currentConfig = scannerConfig;
            _uutNumber = uutNo;
            this.Text = $"UUT{_uutNumber} 扫码调试窗口";

            cmbPortName.Text = _currentConfig.ComPort;
            cmbBaudRate.Text = _currentConfig.BaudRate.ToString();
            cboParity.Text = _currentConfig.Parity.ToString();
            cboDataBits.Text = _currentConfig.DataBits.ToString();
            cboStopBits.Text = _currentConfig.StopBits.ToString();

            _newlandScanner.DataReceived += Scanner_DataReceived;
        }

        private void btnSaveConfig_Click(object sender, EventArgs e)
        {
            try
            {
                var root = XmlConfigHelper.LoadDeviceConfig();
                var scannerCfg = root.ScannerConfig;
                var newCfg = new ScannerConfig
                {
                    ComPort = cmbPortName.Text.Trim(),
                    BaudRate = int.Parse(cmbBaudRate.Text),
                    Parity = (Parity)Enum.Parse(typeof(Parity), cboParity.Text),
                    DataBits = int.Parse(cboDataBits.Text),
                    StopBits = (StopBits)Enum.Parse(typeof(StopBits), cboStopBits.Text),
                    TimeoutMs = 2000
                };
                if (_uutNumber == 1)
                    scannerCfg.UUT1 = newCfg;
                else
                    scannerCfg.UUT2 = newCfg;
                XmlConfigHelper.SaveDeviceConfig(root);
                MessageBox.Show("配置保存成功!关闭窗口重新打开即可生效。");
            }
            catch (Exception ex)
            {
                MessageBox.Show($"配置保存失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void FrmScanner_Load(object sender, EventArgs e)
        {
            if (cmbPortName.Items.Count == 0)
            {
                string[] ports = SerialPort.GetPortNames();
                foreach (var p in ports) cmbPortName.Items.Add(p);
            }
            ResetScanStatus();
            txt_ScanRec.AppendText($"【初始化】最大失败次数:{_maxAllowScanCount}次,单次超时{ScanTimeoutMs}ms\r\n");
            txt_ScanRec.AppendText($"启动指令:{BitConverter.ToString(_startScanCmd)} 停止指令:{BitConverter.ToString(_stopScanCmd)}\r\n");
        }

        private void btnOpenScanner_Click(object sender, EventArgs e)
        {
            if (_newlandScanner.IsOpen)
            {
                MessageBox.Show("串口已打开,无需重复连接!");
                return;
            }
            try
            {
                _newlandScanner.PortName = cmbPortName.Text;
                _newlandScanner.BaudRate = Convert.ToInt32(cmbBaudRate.Text);
                _newlandScanner.DataBits = int.Parse(cboDataBits.Text);
                _newlandScanner.Parity = (Parity)Enum.Parse(typeof(Parity), cboParity.Text);
                _newlandScanner.StopBits = (StopBits)Enum.Parse(typeof(StopBits), cboStopBits.Text);
                _newlandScanner.ReadTimeout = 5000;
                _newlandScanner.WriteTimeout = 5000;
                _newlandScanner.Open();
                _newlandScanner.DiscardInBuffer();
                _newlandScanner.DiscardOutBuffer();
                btnOpenScanner.BackColor = Color.Green;
                txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【成功】{_newlandScanner.PortName} 扫码枪已连接\r\n");
            }
            catch (Exception ex)
            {
                MessageBox.Show($"串口打开失败:{ex.Message}", "异常", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void btnCloseScanner_Click(object sender, EventArgs e)
        {
            if (_newlandScanner.IsOpen)
            {
                StopScan();
                _newlandScanner.Close();
                btnOpenScanner.BackColor = Color.White;
                txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【提示】串口已关闭\r\n");
            }
        }

        //=====【优化后无Timer报错的扫码触发方法】=====
        private void btnStartScan_Click(object sender, EventArgs e)
        {
            if (!_newlandScanner.IsOpen)
            {
                MessageBox.Show("请先打开扫码串口!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }
            if (_isScanLocked)
            {
                MessageBox.Show("当前扫码已锁定,请先重置!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }
            if (_isWaitingScanResult)
            {
                txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【提示】正在等待上一次扫码结果\r\n");
                return;
            }

            try
            {
                _newlandScanner.Write(_startScanCmd, 0, _startScanCmd.Length);
                _isWaitingScanResult = true;
                txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【下发指令】启动扫码,超时{ScanTimeoutMs / 1000}秒\r\n");

                System.Windows.Forms.Timer timeoutTimer = new System.Windows.Forms.Timer();
                timeoutTimer.Interval = ScanTimeoutMs;
                EventHandler timerHandler = null;
                timerHandler = (s, args) =>
                {
                    timeoutTimer.Stop();
                    timeoutTimer.Tick -= timerHandler;
                    timeoutTimer.Dispose();

                    if (_isWaitingScanResult)
                    {
                        StopScan();
                        ScanFailHandle();
                        txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【超时】未识别条码,计入失败次数\r\n");
                        this.BeginInvoke(new Action(() =>
                        {
                            BarcodeResultReceived?.Invoke(string.Empty);
                            this.Close();
                        }));
                    }
                };
                timeoutTimer.Tick += timerHandler;
                timeoutTimer.Start();
            }
            catch (Exception ex)
            {
                txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【指令异常】{ex.Message}\r\n");
                _isWaitingScanResult = false;
            }
        }

        private void StopScan()
        {
            if (_newlandScanner.IsOpen)
            {
                try { _newlandScanner.Write(_stopScanCmd, 0, _stopScanCmd.Length); }
                catch { }
            }
            _isWaitingScanResult = false;
        }

        private void Scanner_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            try
            {
                if (_isScanLocked || !_isWaitingScanResult) return;
                int len = _newlandScanner.BytesToRead;
                byte[] buf = new byte[len];
                _newlandScanner.Read(buf, 0, len);
                string code = Encoding.UTF8.GetString(buf).Trim();
                if (string.IsNullOrEmpty(code)) return;

                StopScan();
                this.Invoke(new Action(() =>
                {
                    txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【扫码成功】{code}\r\n");
                    BarcodeResultReceived?.Invoke(code);
                    this.BeginInvoke(new Action(this.Close));
                }));
            }
            catch (Exception ex)
            {
                this.Invoke(new Action(() =>
                {
                    txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【接收异常】{ex.Message}\r\n");
                    StopScan();
                }));
            }
        }

        private void ScanFailHandle()
        {
            _currentFailScanCount++;
            if (_currentFailScanCount >= _maxAllowScanCount)
            {
                _isScanLocked = true;
                txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【锁定】失败次数用尽,请重置扫码\r\n");
            }
        }

        private void btnClearLog_Click(object sender, EventArgs e) => txt_ScanRec.Clear();

        private void btnResetScan_Click(object sender, EventArgs e)
        {
            StopScan();
            ResetScanStatus();
            txt_ScanRec.AppendText($"\n{DateTime.Now:HH:mm:ss} 【已重置】可重新扫码\r\n");
        }

        private void ResetScanStatus()
        {
            _isScanLocked = false;
            _currentFailScanCount = 0;
            _savedValidBarcode = string.Empty;
            _isWaitingScanResult = false;
        }

        private void FrmScanner_FormClosing(object sender, FormClosingEventArgs e)
        {
            StopScan();
            if (_newlandScanner != null)
            {
                _newlandScanner.DataReceived -= Scanner_DataReceived;
                if (_newlandScanner.IsOpen) _newlandScanner.Close();
                _newlandScanner.Dispose();
            }
        }
    }
}

重要:本次已删除独立TimeoutTimer_Tick方法,使用匿名委托自解绑,彻底解决编译报错。
步骤 3:优化版 ScannerHelper.cs(防止事件重复绑定卡死)csharp运行using System;
using System.IO.Ports;
using TestingInfo.Common.Models;
using TestingInfo.Common.Utils;

namespace TestingInfo.Common.Device.Scanner
{
    public class ScannerHelper
    {
        private readonly DoubleUutScannerConfig _scannerConfig;

        public ScannerHelper()
        {
            var root = XmlConfigHelper.LoadDeviceConfig();
            _scannerConfig = root.ScannerConfig;
        }

        #region 手动弹窗扫码(当前使用)
        public string ScanUUT1_Manual()
        {
            return SingleScanner_Manual(_scannerConfig.UUT1, 1);
        }

        public string ScanUUT2_Manual()
        {
            return SingleScanner_Manual(_scannerConfig.UUT2, 2);
        }

        private string SingleScanner_Manual(ScannerConfig config, int uutNo)
        {
            if (config == null) return string.Empty;
            string barcode = string.Empty;
            using (var frm = new FrmScanner(config, uutNo))
            {
                Action<string> receive = code => barcode = code;
                frm.BarcodeResultReceived -= receive;
                frm.BarcodeResultReceived += receive;
                frm.ShowDialog();
                frm.BarcodeResultReceived -= receive;
            }
            return barcode.Trim();
        }
        #endregion

        #region 自动静默后台扫码
        public string ScanUUT1_Auto()
        {
            return SingleScanner_Auto(_scannerConfig.UUT1);
        }

        public string ScanUUT2_Auto()
        {
            return SingleScanner_Auto(_scannerConfig.UUT2);
        }

        private string SingleScanner_Auto(ScannerConfig config)
        {
            if (config == null) return string.Empty;
            SerialPort sp = null;
            string code = string.Empty;
            try
            {
                sp = new SerialPort(config.ComPort, config.BaudRate, config.Parity, config.DataBits, config.StopBits);
                sp.ReadTimeout = config.TimeoutMs;
                sp.DiscardInBuffer();
                sp.DiscardOutBuffer();
                sp.Open();
                sp.DataReceived += (s, e) =>
                {
                    byte[] buf = new byte[sp.BytesToRead];
                    sp.Read(buf, 0, buf.Length);
                    string temp = System.Text.Encoding.UTF8.GetString(buf).Trim();
                    if (!string.IsNullOrEmpty(temp)) code = temp;
                };
                byte[] cmd = { 0x01, 0x54, 0x04 };
                sp.Write(cmd, 0, cmd.Length);
                var end = DateTime.Now.AddMilliseconds(config.TimeoutMs);
                while (string.IsNullOrEmpty(code) && DateTime.Now < end)
                    System.Threading.Thread.Sleep(50);
            }
            catch
            {
                code = string.Empty;
            }
            finally
            {
                if (sp != null)
                {
                    sp.DataReceived -= (s, e) => { };
                    if (sp.IsOpen) sp.Close();
                    sp.Dispose();
                }
            }
            return code;
        }
        #endregion

        public UutScannerMode GetCurrentScannerMode()
        {
            return _scannerConfig.ScannerMode;
        }
    }
}
步骤 4:业务主窗体调用规范
删除主窗体 FrmScanner frmScanner = null; 代码
头部引入命名空间
csharp运行using TestingInfo.Common.Device.Scanner;

手动扫码按钮代码
csharp运行private void btnManualScan_Click(object sender, EventArgs e)
{
    try
    {
        ScannerHelper helper = new ScannerHelper();
        string barcode = helper.ScanUUT1_Manual();
        if (string.IsNullOrEmpty(barcode))
        {
            MessageBox.Show("未扫码或扫码超时");
            return;
        }
        txt_Barcode.Text = barcode.Substring(1, 20);
    }
    catch (Exception ex)
    {
        MessageBox.Show($"扫码异常:{ex.Message}");
    }
}
步骤 5:编译 + 三步验证测试5.1 编译操作
菜单栏:【生成】→ 清理解决方案
重新生成解决方案,确认无编译报错
5.2 功能验证
正常扫码测试:打开串口→点击扫码→识别条码→窗口自动关闭,条码回填主窗体,无卡死
超时测试:打开串口不扫码,等待 2.5 秒,窗口正常关闭,提示未扫码
连续多次弹窗测试:连续打开关闭扫码窗口 5 次,无卡死、无重复回调、无串口占用
避坑检查清单
btnSaveConfig 按钮必须绑定点击事件
所有类命名空间必须和文件夹路径一致
XML 枚举值SingleUUT/DoubleUUT大小写不能修改
关闭扫码窗口前优先关闭串口,避免 COM 端口残留占用

Logo

CANN开发者社区旨在汇聚广大开发者,围绕CANN架构重构、算子开发、部署应用优化等核心方向,展开深度交流与思想碰撞,携手共同促进CANN开放生态突破!

更多推荐