扫描枪扫码,并获取扫码结果

发布时间 2023-09-12 16:57:48作者: 潇潇烟雨
using Tools;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Timers;
using System.Windows.Input;
using System.Windows.Threading;

namespace Helpers
{
    public class ScanCodeHelper
    {
        private readonly Stopwatch sw = new Stopwatch();
        public delegate void ScanSuccesEventHandler(string barcode, Key key);
        /// <summary>
        /// 扫码成功事件
        /// </summary>
        public event ScanSuccesEventHandler ScanSuccesEvent;
        /// <summary>
        /// 延迟90ms,判断扫码是否完成
        /// </summary>
        private int DelayTime = 200;
        /// <summary>
        /// 扫码内容
        /// </summary>
        private StringBuilder StrResult;
        /// <summary>
        /// 是否为Enter
        /// </summary>
        private bool isEnterFinish;

        private DispatcherTimer timer;

        private Key CurrentKey;
        /// <summary>
        /// 记录每个按键的间隔,间隔单位为毫秒
        /// </summary>
        private List<int> durations = new List<int>();

        public ScanCodeHelper(ScanSuccesEventHandler scanSuccesEvent)
        {
            StrResult = new StringBuilder();
            this.ScanSuccesEvent = scanSuccesEvent;
            timer = new DispatcherTimer();
            //timer = new DispatcherTimer(DispatcherPriority.Send);
            timer.Interval = TimeSpan.FromSeconds(0.06);
            timer.Tick += new System.EventHandler(Timer_Elapsed);
        }

        ~ScanCodeHelper()
        {
            if (timer != null)
            {
                timer.Stop();
                //timer.Dispose();
                //timer = null;
            }

            foreach (var del in this.ScanSuccesEvent.GetInvocationList())
            {
                this.ScanSuccesEvent -= del as ScanSuccesEventHandler;
            }
        }

        private void Timer_Elapsed(object sender, EventArgs e)
        {
            timer?.Stop();
            ScanSuccesEvent.Invoke(PerformScanSuccess(), CurrentKey);
        }

        public void AnalysisKey(Key key)
        {
            sw.Stop();
            AddDuration(key);
            sw.Restart();

            if (isEnterFinish)
                return;

            CurrentKey = key;
            //string keyStr = KeyConveterTool.GetInputKey(key);
            string keyStr = CurrentKey.ToString();
            if (!string.IsNullOrEmpty(keyStr))
                StrResult.Append(keyStr);

            if (key == Key.Enter)
            {
                isEnterFinish = true;
                timer?.Stop();
                //若为回车键,直接返回
                Timer_Elapsed(null, null);
            }
            else
            {
                //延迟,若90ms内,有其他事件
                if (!timer.IsEnabled)
                    timer.Start();
                else
                {
                    timer.Stop();
                    timer.Start();
                }
            }

            //延迟,若90ms内,有其他事件
            //if (!timer.Enabled)
            //    timer.Start();
            //else
            //{
            //    timer.Stop();
            //    timer.Start();
            //}
        }

        private string PerformScanSuccess()
        {
            isEnterFinish = false;
            string barcode = "";
            if (StrResult != null)
                barcode = StrResult.ToString().Trim();

            if (StrResult != null)
                StrResult.Clear();

            if (timer.IsEnabled)
            {
                timer.Stop();
            }

            return barcode;
        }

        public List<int> GetDurations()
        {
            return durations;
        }

        public void ResetDurations()
        {
            durations = new List<int>();
            sw.Reset();
        }

        public void AddDuration(Key key)
        {
            // 第一次进入此事件时,sw尚未启动,此时固定为0
            if (durations.Count == 0 && sw.Elapsed.Milliseconds == 0)
            {
                return;
            }
            // 第一个键为系统键时,例如按LWin切入系统,不作为间隔判断
            if (durations.Count == 0 && key != Key.Enter && key.IsFunctionKey())
            {
                return;
            }

            durations.Add(sw.Elapsed.Milliseconds);

        }

        /// <summary>
        /// 是否是扫码输入
        /// 此方法在查询数据时调用,判断是扫码还是手动输入
        /// 根据以下两个原则
        /// 1.扫描总时间不能过长,例如有数会员码19位,总时长不能超过1s,间隔平均值必须小于52ms,允许一定误差,此处设置为60ms
        /// 2.扫描间隔必须稳定,目前规定能够为平均值加上标准差的N倍,此处判断只有辅助作用
        /// </summary>
        /// <returns></returns>
        public bool IsScan()
        {

            // 输入间隔必须大于2
            // 同时按住1个键和回车的时候,间隔只有1,此时默认为扫码
            if (durations.Count <= 1)
            {
                return false;
            }

            // 输入间隔为2时,两个间隔大小不能超过3倍
            if (durations.Count == 2 && durations.Max() / (durations.Min() == 0 ? 0.01 : durations.Min()) > 3)
            {
                return false;
            }

            var average = durations.Average();
            // 长度大于15,例如有数会员动态码为19位这时候均值小于75ms,即在1.2s完成了15个字符的输入,则认为是刷卡
            if (durations.Count >= 15 && average <= 75)
            {
                return true;
            }

            // 长度大于10小于15,例如13位商品条码,均值在50ms以内,即在675ms,完成了13位字符的输入,可认为时扫码
            if (durations.Count >= 10 && durations.Count < 15 && average <= 50)
            {
                return true;
            }

            // 长度在5-10之间,通常为磁卡未加密的卡号均在在25ms以下,即在175m内完成了7个字符的输入,可认为是扫码
            if (durations.Count >= 5 && durations.Count < 10 && average <= 25)
            {
                return true;
            }

            // 输入间隔平均值大于60ms,则是手动输入
            if (average > 60)
            {
                return false;
            }

            // 方差
            var sumOfSquaresOfDifferences = durations.Select(val => (val - average) * (val - average)).Sum();
            // 标准差
            var standardDeviation = Math.Sqrt(sumOfSquaresOfDifferences / durations.Count);

            // 存在log2N次次大于平均值+N倍标准差,则认为是手动输入
            // 例如长度为10的输入字符,只允许2次不稳定
            // 长度为19的输入字符,只允许4次输入不稳定
            const double multi = 1.75;
            if (durations.Count(x => Math.Abs(x - average) > multi * standardDeviation) >= (int)Math.Log(durations.Count, 2))
            {
                return false;
            }

            return true;

        }

    }
}

 

using K.Tools;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows.Input;

namespace K.Helpers
{   
    /// <summary>
    /// 扫码帮助类
    /// 用于判断输入是扫码还是手输的问题
    /// </summary>
    public class ScanCodesHelper
    {
        private readonly Stopwatch _sw = new Stopwatch();


        private readonly Action<Key> _callBackAction;

        /// <summary>
        /// 扫码内容
        /// </summary>
        private  List<string> _inputCodes = new List<string>();  
        
        
        private Key _currentKey;

        /// <summary>
        /// 记录每个按键的间隔,间隔单位为毫秒
        /// </summary>
        private List<int> _durations = new List<int>();

        public ScanCodesHelper(Action<Key> callbackAction)
        {
            _callBackAction = callbackAction;
           
        }

        public void AnalysisKey(Key key)
        {
            if (_inputCodes.Count == 0 && !key.IsLetterOrDigit())
            {
                _callBackAction(key);
                return;
            }
            _sw.Stop();
            AddDuration(key);
            _sw.Restart();

            _currentKey = key;
        
            var keyStr = _currentKey.ToString();
            if (!string.IsNullOrEmpty(keyStr))
                _inputCodes.Add(keyStr);

            _callBackAction(_currentKey);

        }

        public List<int> GetDurations()
        {
            return _durations;
        }

        public List<string> GetResult()
        {
            return _inputCodes;
        }

        public void Reset()
        {
            _durations = new List<int>();
            _sw.Reset();
            _inputCodes = new List<string>();
        }

        /// <summary>
        /// 是否是扫码输入
        /// 此方法在查询数据时调用,判断是扫码还是手动输入
        /// 根据以下两个原则
        /// 1.扫描总时间不能过长,例如有数会员码19位,总时长不能超过1s,间隔平均值必须小于52ms,允许一定误差,此处设置为60ms
        /// 2.扫描间隔必须稳定,目前规定能够为平均值加上标准差的N倍,此处判断只有辅助作用
        /// </summary>
        /// <returns></returns>
        public bool IsScan()
        {
                // 输入间隔必须大于2
            // 同时按住1个键和回车的时候,间隔只有1,此时默认为扫码
            if (_durations.Count <= 1)
            {
                return false;
            }

            // 输入间隔为2时,两个间隔大小不能超过3倍
            if (_durations.Count == 2 && _durations.Max() / (_durations.Min() == 0 ? 0.01 : _durations.Min()) > 3)
            {
                return false;
            }

             var average = _durations.Average();
            // 长度大于15,例如有数会员动态码为19位这时候均值小于75ms,即在1.2s完成了15个字符的输入,则认为是刷卡
            if (_durations.Count >= 15 && average <= 75)
            {
                return true;
            }

            // 长度大于10小于15,例如13位商品条码,均值在50ms以内,即在675ms,完成了13位字符的输入,可认为时扫码
            if (_durations.Count >= 10 && _durations.Count < 15 && average <= 50)
            {
                return true;
            }

           // 长度在5-10之间,通常为磁卡未加密的卡号均在在25ms以下,即在175m内完成了7个字符的输入,可认为是扫码
            if (_durations.Count >= 5 && _durations.Count < 10 && average <= 25)
            {
                return true;
            }


            // 输入间隔平均值大于60ms,则是手动输入
            if (average > 60)
            {
                return false;
            }

            // 方差
            var sumOfSquaresOfDifferences = _durations.Select(val => (val - average) * (val - average)).Sum();
            // 标准差
            var standardDeviation = Math.Sqrt(sumOfSquaresOfDifferences / _durations.Count);

            // 存在log2N次次大于平均值+N倍标准差,则认为是手动输入
            // 例如长度为10的输入字符,只允许2次不稳定
            // 长度为19的输入字符,只允许4次输入不稳定
            const double multi = 1.75;
            if (_durations.Count(x => Math.Abs(x - average) > multi * standardDeviation) >= (int)Math.Log(_durations.Count, 2))
            {
                return false;
            }

            return true;

        }

        /// <summary>
        /// 是否超过了扫码判断间隔
        /// <para>间隔为1时,存在两种情况</para>
        /// <para>某个键和回车, 此时认为不是扫码</para>
        /// <para>非回车的两个键,此时需要根据间隔</para>
        /// </summary>
        /// <returns></returns>
        private bool IsOneDurationScan()
        {
            if (_durations.Count != 1)
                return false;
            
            if (_currentKey == Key.Enter)
                return false;
            
            if (_durations.Sum() > 100)
                return false;
            
            return true;
        }

         /// <summary>
        /// 是否是严格模式下的扫码输入,用于对扫码识别严格模式下使用,例如预设商品输入框扫码判断
        /// 严格模式下,是否扫码更为严格,因为较短数字下,扫码判断不准确,因此做如下限制
        /// 1.必须4位及以上
        /// 然后根据以下两个原则
        /// 1.扫描总时间不能过长,例如有数会员码19位,总时长不能超过1s,间隔平均值必须小于52ms,允许一定误差,此处设置为60ms
        /// 2.扫描间隔必须稳定,目前规定能够为平均值加上标准差的N倍,此处判断只有辅助作用
        /// </summary>
        /// <returns></returns>
       public bool IsScanStrictMode()
        {
            if (_durations == null || _durations.Count == 0)
                return false;
            
            var average = _durations.Average();

            // 输入间隔为1,此时接收到两个按键
            // 通常为某个按键和回车,此时认为不是扫码
            // 或者是两个单独的按键,此时需要根据间隔
            if (_durations.Count == 1 )
            {
                return IsOneDurationScan();
            }
            
            // 输入间隔数量为2时, 即输入了3个字符,均值必须小于等于50
            if (_durations.Count == 2 && average <= 50 )
            {
                return true;
            }
            
            // 输入间隔为3个或者4个时, 输入间隔平均值大于60ms, 则是手动输入
            if (_durations.Count >= 3 && _durations.Count < 5 && average <= 60)
            {
                return true;
            }
            
            // 长度在5-10之间,通常为磁卡未加密的卡号均在在25ms以下,即在175m内完成了7个字符的输入,可认为是扫码
            if (_durations.Count >= 5 && _durations.Count < 10 && average <= 25)
            {
                return true;
            }

            // 长度大于10小于15,例如13位商品条码,均值在50ms以内,即在675ms,完成了13位字符的输入,可认为时扫码
            if (_durations.Count >= 10 && _durations.Count < 15 && average <= 50)
            {
                return true;
            }

            // 长度大于15,例如有数会员动态码为19位这时候均值小于75ms,即在1.2s完成了15个字符的输入,则认为是刷卡
            if (_durations.Count >= 15 && average <= 75)
            {
                return true;
            }

            
            return false;

        }

        private void AddDuration(Key key)
        {
            // 第一次进入此事件时,sw尚未启动,此时固定为0
            if (_durations.Count == 0 && _sw.Elapsed.Milliseconds == 0)
            {
                return;
            }
            // 第一个键为系统键时,例如按LWin切入系统,不作为间隔判断
            if (_durations.Count == 0 && key != Key.Enter && key.IsFunctionKey())
            {
                return;
            }

            _durations.Add(_sw.Elapsed.Milliseconds);

        }
    }
}

  

.xaml.cs文件中调用1:

    public partial class ItemClsAndItemView : UserControl
    {
        private ItemClsAndItemViewModel _vm;
        //扫码工具类 
        private readonly ScanCodesHelper _codesHelper;

public ItemClsAndItemView()
        {
            InitializeComponent();
            _codesHelper = new ScanCodesHelper(ScanSuccess);
            _vm = (ItemClsAndItemViewModel)DataContext;
            keycontrol.OnCustomKeyDown = OnCustomerKeyDown;
        }

        /// <summary>
        /// 扫码成功回调
        /// </summary>
        /// <param name="key">按键</param>
        private void ScanSuccess(Key key)
        {
            
           var queryStr = _vm.ClsAndItemModel.QueryStr ?? string.Empty;

           // 删除键清空数据时,也需要查询
            if (queryStr.IsNullOrEmpty() && key != Key.Back)
                return;    

            if (key == Key.Enter)
            {
                // 扫码判断
                // 必须满足扫码间隔判断,必须为数字,必须4位以上
                if (_codesHelper.IsScanStrictMode() && queryStr.All(char.IsDigit) && queryStr.Length > 4)
                {
                    Input();
                    _codesHelper.Reset();
                    return;
                }
                Query(null);
                _codesHelper.Reset();
                return;
            }

         }

       /// <summary>
        /// 此处需要用KeyUp事件,KeyDown事件会无法获取QueryStr
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPreviewKeyUp(KeyEventArgs e)
        {
            var key = (e.Key == Key.System ? e.SystemKey : e.Key);
            OnCustomerKeyDown(key);
        }

        /// <summary>
        /// OnCustomerKeyDown
        /// </summary>
        /// <param name="key"></param>
        private void OnCustomerKeyDown(Key key)
        {
            _codesHelper.AnalysisKey(key);
        }

        private void Query(string type)
        {
            _vm.logger.Info($"输入数据:{string.Join(",", _codesHelper.GetResult())},输入框数据{_vm.ClsAndItemModel.QueryStr}");
            _vm.logger.Info($"输入间隔:{string.Join(",", _codesHelper.GetDurations())}");
            _vm.logger.Info($"是否为扫码:{_codesHelper.IsScanStrictMode()}");
            _vm?.StartQueryCommand.Execute(type);
        }

        private void Input()
        {
            _vm.logger.Info($"输入数据:{string.Join(",", _codesHelper.GetResult())},输入框数据{_vm.ClsAndItemModel.QueryStr}");
            _vm.logger.Info($"输入间隔:{string.Join(",", _codesHelper.GetDurations())}");
            _vm.logger.Info($"是否为扫码:{_codesHelper.IsScanStrictMode()}");
            //扫码回车走input接口,加购或者扫支付码
            _vm?.ScaneQueryCommand.Execute();
            keycontrol.FocusEx();
            if (_vm != null) 
                _vm.ClsAndItemModel.QueryStr = "";
        }

        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            if (this.DataContext is ItemClsAndItemViewModel vm)
            {
                this._vm = vm;
                vm.logger.Info("ItemClsAndItemViewModel_Loaded开始");
                InitUI();

                keycontrol.TxtChangeAction += () =>
                {
                    txt_TextChanged(null, null);
                };

                LoadAction();
                vm.logger.Info("ItemClsAndItemViewModel_Loaded完成");
            }
        }

        private void this_Unloaded(object sender, RoutedEventArgs e)
        {
            keycontrol.UpAction = null;
            keycontrol.DownAction = null;
            keycontrol.CloseAction = null;
            keycontrol.TxtChangeAction = null;
        }

  

 

 

.xaml.cs文件中调用:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using static K.CashierModule.Helpers.ScanCodeHelper;

namespace K.Views.Dialogs
{
    /// <summary>
    /// QueryVipView.xaml 的交互逻辑
    /// </summary>
    public partial class PayVipView : UserControl
    {
    private ViewModels.Dialogs.PayVipViewModel vm;
    public ScanCodesHelper CodesHelper;

    public PayVipView()
    {
      InitializeComponent();
      CodesHelper = new ScanCodesHelper(ScanSucces);

      this.Unloaded += new RoutedEventHandler((o, s) =>
      {
        if (vm != null)
        {
          vm.UnLoadData();
          vm.OnUpdateUI -= UpdateUIByData;
        }
      });
    }



    protected override void OnPreviewKeyDown(KeyEventArgs e)
    {
      Key key = (e.Key == Key.System ? e.SystemKey : e.Key);  

      CodesHelper.AnalysisKey(key);//分析按键

      if (fulldetail.Visibility == Visibility.Visible)
      {
        couponView.QuickKeyOperator(e.Key == Key.ImeProcessed ? e.ImeProcessedKey : e.Key);
        if (e.Key == Key.Q || e.Key == Key.W)
        {
          e.Handled = true;
        }
      }

      base.OnKeyDown(e);
    }



    /// <summary>
    /// 扫码成功回调
    /// </summary>
    /// <param name="key"></param>
    private void ScanSucces(Key key)
    {
      if (CodesHelper.IsScan() && key == Key.Enter && !vm.QueryVipModel.VipQuickPay)
      {
        Query(true);
        CodesHelper.Reset();
      }

  

      Core.Definition.QuickKeyManager.Instance.Exec(this.GetType(), Convert.ToInt32(key));

      switch (key)
      {
        // 当手输错误时,需要重新扫码或者输入。这时需要清空时间间隔重新判断
        case Key.Back when string.IsNullOrEmpty(txt.Text):
        case Key.Enter:
        CodesHelper.Reset();
        break;
      }

    }


        void Query(bool isCutStr = false)
        {
            if (this.DataContext is ViewModels.Dialogs.PayVipViewModel vm)
            {
                vm.logger.Info($"输入数据:{string.Join(",", CodesHelper.GetResult())},输入框数据{txt.Text}");
                vm.logger.Info($"输入间隔:{string.Join(",", CodesHelper.GetDurations())}");
                vm.logger.Info($"是否为扫码:{CodesHelper.IsScan()}");

                //扫码枪操作为刷卡
                if (CodesHelper.IsScan() && !string.IsNullOrEmpty(txt.Text) && txt.Text.Length > 1)
                {
                    vm.VipPayMode = "";
                    vm.QueryVipModel.QueryOperator = "3";
                }

                //扫码做截取处理
                if (isCutStr && vm.QueryVipModel.QueryOperator == "3" && Core.IniConfig.IniCardConfig.NeedHandleCardString())
                {
                    vm.QueryVipModel.Code = Core.IniConfig.IniCardConfig.HandleCardString(vm.QueryVipModel.QueryText);
                    vm.QueryVipModel.QueryOperator = "1";
                }