通用扫码帮助类CommonScanCodeHelper
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows.Input;
using System.Windows.Threading;
namespace Tools
{
/// <summary>
/// 通用扫码
/// </summary>
public class CommonScanCodeHelper
{
private readonly Stopwatch sw;
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;
/// <summary>
/// 扫码操作,默认间隔时间可以设置为20毫秒,根据不同的场景来设置
/// </summary>
/// <param name="scanSuccesEvent"></param>
/// <param name="interval"></param>
public CommonScanCodeHelper(ScanSuccesEventHandler scanSuccesEvent, double interval)
{
if (interval <= 0)
interval = 0.02;
StrResult = new StringBuilder();
this.ScanSuccesEvent = scanSuccesEvent;
timer = new DispatcherTimer();
//timer = new DispatcherTimer(DispatcherPriority.Send);
timer.Interval = TimeSpan.FromSeconds(interval);
timer.Tick += new System.EventHandler(Timer_Elapsed);
durations = new List<int>();
sw = new Stopwatch();
}
~CommonScanCodeHelper()
{
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 Tools;
using ViewModels.Dialogs;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Views.Dialogs
{
/// <summary>
/// AuthorizeDialog.xaml 的交互逻辑
/// </summary>
public partial class AuthorizeDialog : UserControl
{
private AuthorizeDialogViewModel vm;
public CommonScanCodeHelper CodeHelper;
private CommonScanCodeHelper.ScanSuccesEventHandler handler;
public AuthorizeDialog()
{
InitializeComponent();
this.Unloaded += new RoutedEventHandler((o, s) =>
{
if (vm != null)
{
this.vm.OnFocused -= GetTxtFocus;
}
});
handler = new CommonScanCodeHelper.ScanSuccesEventHandler(ScanSucces);
CodeHelper = new CommonScanCodeHelper(handler, 0.09);
}
private bool ScanFlag = false;
/// <summary>
/// 扫码成功回调
/// </summary>
/// <param name="barcode"></param>
/// <param name="key"></param>
private void ScanSucces(string barcode, Key key)
{
vm.logger.Info($"输入码为:{barcode}");
if (barcode.Contains(Key.Enter.ToString()) && barcode.Length > 6)//扫码
{
ScanFlag = true;
cardNumber.Text = tbScane.Text;
btn_Click(btn, null);
tbScane.Text = "";
tbScane.Focus();
}
else//手输
{
ScanFlag = false;
tbScane.Text = "";
}
ScanFlag = false;
}
private void this_Loaded(object sender, RoutedEventArgs e)
{
K.Controls.Controls.AutoControl.Apply(grid);
if (this.DataContext is AuthorizeDialogViewModel vm)
{
this.vm = vm;
this.Focusable = true;
this.Focus();
if(!vm.AuthorizeDialogModel.IsAuthorityCard)
{
server.Focus();
keyboard.Visibility = Visibility.Visible;
}
else
{
tbScane.Focus();
//cardNumber.Focus();
keyboard.Visibility = Visibility.Collapsed;
}
this.vm.OnFocused += GetTxtFocus;
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
if(vm.AuthorizeDialogModel.IsAuthorityCard)
{
Key key = (e.Key == Key.System ? e.SystemKey : e.Key);
CodeHelper.AnalysisKey(e.Key);//分析按键
}
else
{
switch (e.Key)
{
case Key.Escape:
if (this.DataContext is ViewModels.Dialogs.AuthorizeDialogViewModel vm)
vm.CloseCommand?.Execute();
e.Handled = true;
break;
case Key.F1:
server.Focus();
e.Handled = true;
break;
case Key.F5:
port.Focus();
e.Handled = true;
break;
case Key.Enter:
if (server.IsFocused)
{
port.Focus();
}
else if ((port.IsFocused || cardNumber.IsFocused) && btn.IsEnabled)
{
btn_Click(btn, null);
}
e.Handled = true;
break;
}
}
base.OnKeyDown(e);
}
private void GetTxtFocus()
{
server.Focus();
server.SelectAll();
}
private void Grid_TextChanged(object sender, TextChangedEventArgs e)
{
if (!this.IsLoaded)
{
return;
}
if(!vm.AuthorizeDialogModel.IsAuthorityCard)
{
if (string.IsNullOrWhiteSpace(server.Text))
btn.IsEnabled = false;
else
btn.IsEnabled = true;
}
else
{
if (string.IsNullOrWhiteSpace(cardNumber.Text))
btn.IsEnabled = false;
else
btn.IsEnabled = true;
}
}
private void cardNumber_TextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrEmpty(cardNumber.SelectedText))
{
cardNumber.IsPasswordBox = true;
cardNumber.SelectionBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#85FFFFFF"));
cardNumber.Foreground = (Brush)Application.Current.Resources["LoginTbForBrush"];
cardNumber.BorderBrush = (Brush)Application.Current.Resources["LoginBoderBrush"];
}
}
private void btn_Click(object sender, RoutedEventArgs e)
{
if (this.DataContext is ViewModels.Dialogs.AuthorizeDialogViewModel vm)
{
string pwd = string.Empty;
if (!vm.AuthorizeDialogModel.IsAuthorityCard)
{
pwd = port.PasswordStr;
port.SelectAll();
}
else
{
//扫码做截取处理
if (ScanFlag && Core.IniConfig.IniCardConfig.NeedHandleCardString() && Core.Common.GlobalClass.Config?.pos_auth_swipe_card_switch == "1")
{
pwd = Core.IniConfig.IniCardConfig.HandleCardString(cardNumber.PasswordStr);
}
else
{
pwd = cardNumber.PasswordStr;
}
//cardNumber.Focus();
cardNumber.SelectAll();
//tbScane.Focus();
//tbScane.SelectAll();
}
vm.AuthCommand.Execute(pwd);
if(vm.AuthorizeDialogModel.ShowTip == true && vm.AuthorizeDialogModel.IsAuthorityCard)
{
cardNumber.IsPasswordBox = false;
cardNumber.Text = cardNumber.PasswordStr;
cardNumber.SelectionBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#40FF2E47"));
cardNumber.Foreground = (Brush)Application.Current.Resources["CommonErrorBrush"];
cardNumber.BorderBrush = (Brush)Application.Current.Resources["CommonErrorBrush"];
}
}
}
xaml文件:
<UserControl x:Class="K.WindowShell.Views.Dialogs.AuthorizeDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="clr-namespace:J.Wpf.Services.Dialogs;assembly=J.Wpf"
xmlns:convert="clr-namespace:K.Controls.Converters;assembly=K.Controls"
xmlns:controls="clr-namespace:K.Controls.Controls;assembly=K.Controls"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:K.WindowShell.Views.Dialogs"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" Loaded="this_Loaded">
<UserControl.Resources>
<convert:Bool2VisibilityReConverter x:Key="Bool2VisibilityReConverter"/>
<convert:VisibilityConverter x:Key="VisibilityConverter"/>
</UserControl.Resources>
<prism:Dialog.WindowStyle>
<Style TargetType="Window">
<!--<Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterScreen" />-->
<Setter Property="ShowInTaskbar" Value="False"/>
<!--<Setter Property="SizeToContent" Value="WidthAndHeight"/>-->
<Setter Property="WindowStyle" Value="None"/>
<Setter Property="WindowState" Value="Maximized"/>
<Setter Property="AllowsTransparency" Value="True"/>
<Setter Property="Background" Value="Transparent"/>
<!--<Setter Property="ResizeMode" Value="NoResize"/>-->
</Style>
</prism:Dialog.WindowStyle>
<Viewbox>
<Grid x:Name="grid" Background="{DynamicResource PageBackBrush}" TextBoxBase.TextChanged="Grid_TextChanged" Width="1440" Height="1080">
<Grid.RowDefinitions>
<RowDefinition Height="120"/>
<RowDefinition Height="1.5*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1.5*"/>
<RowDefinition Height="5.5*"/>
</Grid.RowDefinitions>
<controls:ButtonUserControl TextStr="Esc" FontSize="36" BtnImg="{DynamicResource BackIcon}" Width="190" Height="120" BtnImgWidth="48" BtnImgHeight="48" IsCancel="True"
HotKeyShow="{Binding IsOpenShortKey}" Foreground="{DynamicResource RechargeEscBtnBackBrush}" HorizontalAlignment="Left" Command="{Binding CloseCommand}"/>
<TextBlock Text="{Binding AuthorizeDialogModel.Name}" FontSize="36" Foreground="{DynamicResource RechargeEscBtnBackBrush}"/>
<!--授权账户、密码-->
<StackPanel Grid.Row="1" HorizontalAlignment="Center" Orientation="Horizontal" Visibility="{Binding AuthorizeDialogModel.IsAuthorityCard,Converter={StaticResource Bool2VisibilityReConverter}}">
<controls:TextBoxEx x:Name="server" Height="100" Grid.Row="1" Text="{Binding AuthorizeDialogModel.UserName,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True,ValidatesOnExceptions=True}" FontSize="36" Width="720" Foreground="{DynamicResource LoginTbForBrush}" WaterMark="请输入有权限的收银员账号" IconDirection="Left" Icon="{DynamicResource NameIcon}" BorderThickness="0,0,0,1" IsAutoWidth="True" BorderBrush="{DynamicResource LoginBoderBrush}"/>
<Canvas>
<TextBlock FontSize="36" Visibility="{Binding IsOpenShortKey,Converter={StaticResource VisibilityConverter}}" Width="50" Height="100" Canvas.Right="-10" Canvas.Top="55" Foreground="{DynamicResource ShortKeyQuerryTxtBrush}" Text="F1"></TextBlock>
</Canvas>
</StackPanel>
<StackPanel Grid.Row="2" HorizontalAlignment="Center" Orientation="Horizontal" Visibility="{Binding AuthorizeDialogModel.IsAuthorityCard,Converter={StaticResource Bool2VisibilityReConverter}}">
<controls:TextBoxEx x:Name="port" Height="100" IsPasswordBox="True" FontSize="36" Width="720" Foreground="{DynamicResource LoginTbForBrush}" WaterMark="请输入有权限的收银员密码" IconDirection="Left" Icon="{DynamicResource PwdIcon}" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource LoginBoderBrush}"/>
<Canvas>
<TextBlock FontSize="36" Visibility="{Binding IsOpenShortKey,Converter={StaticResource VisibilityConverter}}" Width="50" Height="100" Canvas.Right="-10" Canvas.Top="30" Foreground="{DynamicResource ShortKeyQuerryTxtBrush}" Text="F5"></TextBlock>
</Canvas>
</StackPanel>
<!--权限卡-->
<controls:TextBoxEx x:Name="cardNumber" Grid.Row="1" Grid.RowSpan="2" HorizontalAlignment="Center" VerticalAlignment="Center" IsPasswordBox="True" FontSize="40" Width="601"
Foreground="{DynamicResource LoginTbForBrush}" WaterMark="请刷权限卡" IconDirection="Left" Icon="{DynamicResource PwdIcon}" TextChanged="cardNumber_TextChanged"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource LoginBoderBrush}" Visibility="{Binding AuthorizeDialogModel.IsAuthorityCard,Converter={StaticResource Bool2VisibilityConverter}}"
IsReadOnly="True" Focusable="False"/>
<!--扫码文本-->
<TextBox x:Name="tbScane" Grid.Row="1" Grid.RowSpan="2" IsUndoEnabled="False" Width="0"/>
<controls:ButtonCommon x:Name="btn" Focusable="False" Grid.Row="3" Width="720" Height="100" CornerRadius="8" BorderBrush="{DynamicResource SecondBtnBackBrush}"
IsOpenKey="{Binding IsOpenShortKey}" Background="{DynamicResource MainBtnBackBrush}" Foreground="{DynamicResource CommonWhiteBrush}"
Margin="0,10,0,0" Style="{StaticResource ButtonCommon_NoWait}" Text="确定" FontSize="45" Click="btn_Click" IsEnabled="False"/>
<controls:VirtualKeyboard x:Name="keyboard" NumberWidth="720" NumberHegiht="450" Grid.Row="4" KeyType="Number" Margin="0,41,0,10"/>
<controls:PopupEx x:Name="popup" PlacementTarget="{Binding ElementName=grid}" IsOpen="{Binding AuthorizeDialogModel.ShowTip}" Placement="Top" VerticalOffset="100" StaysOpen="True" IsAutoHidden="True" AllowsTransparency="True" HorizontalOffset="0">
<StackPanel Width="{Binding ElementName=grid,Path=ActualWidth}" Height="100" Background="{DynamicResource PopupExBackBrush}">
<StackPanel Orientation="Horizontal" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center">
<Image Source="/CloudPOS.CashierModule;component/Images/warnicon.png" Stretch="None" Margin="10,0,0,0"></Image>
<TextBlock Text="{Binding AuthorizeDialogModel.TipTxt}" Foreground="{DynamicResource PopupExTxtBrush}" FontSize="26" Margin="10,0,0,0"/>
</StackPanel>
</StackPanel>
</controls:PopupEx>
</Grid>
</Viewbox>
</UserControl>
AuthorizeDialogViewModel.cs
public Action OnFocused;
public AuthorizeDialogModel AuthorizeDialogModel { get; set; }
public DelegateCommand<string> AuthCommand { get; set; }
public DelegateCommand CloseCommand { get; set; }
public AuthorizeDialogViewModel(IContainerExtension containerExtension) : base(containerExtension)
{
AuthorizeDialogModel = new AuthorizeDialogModel();
AuthCommand = new DelegateCommand<string>(auth);
CloseCommand = new DelegateCommand(close);
}
void auth(string pwd)
{
}
void close()
{
RequestClose?.Invoke(new DialogResult(ButtonResult.No));
}