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