最近为公司开发一个生产系统,其中用到扫描枪输入条码,结果发现手头的扫描枪居然是模拟键盘输入将条码数据直接发送到焦点控件中的(USB口的),比如TextBox,而由于业务要求,不允许生产线上员工手工输入,因此我将文本框设为只读,想不到扫描枪也无法输入了。
看来想通过控件的键盘事件去识别扫描枪输入与键盘输入是行不通的。百度了下,也没找到好的解决方案,不过得到了一个通过检测按键间隔来识别是否为人工输入的思路,经过多番研究和调试,终于完成了功能,并且将该功能完美封装在类中,实现了降低耦合的要求,并归入自定义DLL中,作为一个通用库的一部分。
基本思路为:使用时间类型变量记录每次按键发生时间,计算两次按键之间的时间间隔,如果超时,则认为是键盘输入,变量初始化为MinValue,用来区分是否是首次按键。间隔限定100毫秒,因为扫描仪输入间隔非常快,以此区分。
类的方法负责接收发送者和发送文本,负责开启计时器跟踪,保存每次调用的时间,计算两次输入间隔,第一次输入或出现超时则发送清空输入事件通知,其次,计时器计算按键后是否超时并发送清空输入事件。窗口程序每当发生输入则调用类方法,并通过对象事件相应清空文本框。
重点:如何判断扫描仪扫描条码结束还是手工按键间隔,仅通过2次按键间隔判断无法检查最后一个字符,因为两次按键间隔是在后一次按键发生时才会被动检查两次按键间隔时间,而如果是到达了最后一个字符,后面就不会再有按键发生,那么按键检查就不会执行。直到下一次扫描或按键才会去检查前一次扫描情况。因此定义了一个计时器来跟踪每次按键后的超时情况,这样即便遇到最够一个按键,没有调用函数,计时器也会发现超时,两者结合解决问题。
现介绍类的定义和给出完整代码,以及调用代码。
类名:ScanningGunMonitor
一、依赖引用
using System.Timers;
二、类的定义
1、内部成员介绍
Timer _Timer
按键后计时器,用于监控按键后的时间段是否超时。
DateTime _TickTime
记录每次按键的时间。
string _TempText
保存处理后的外部控件文本
object _Sender
保存外部控件源对象,用于发送事件时传回
2、属性介绍
int MiniLength
字符串最小长度,限定连续输入时最小长度,以此区分人工输入。因为人工输入按键间隔很难做到保持每个间隔都在时间间隔限定之内,也即做不到连续稳定均匀输入。这是区分按键/扫描枪输入的依据之一。
int TimeOut
输入超时限定时间(毫秒),这是区分按键/扫描枪输入的依据之二,确保每次输入间隔在限定时间内。
int ClockTick
时钟周期(毫秒),内部计时器参数,指定计时器按此时间周期性处理。
3、方法介绍
CheckKeyPress
检查2次按键之间的时间间隔,如果第一次输入,开启计时器,检查是否超时,发送清空文本的事件,停止计时器工作,或保持文本。
StopCheckGap
停止计时器跟踪按键后间隔。
Timer_Elapsed
计时器事件方法,检查按键后是否超时,进一步判断是否是扫描结束还是按键输入。
4、ScanningGunMonitor的类代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/// <summary> /// 扫描枪键盘输入检测类 /// </summary> public class ScanningGunMonitor { #region 内部成员 Timer _Timer = new Timer();//计时器 DateTime _TickTime=DateTime.MinValue;//记录前一次按键周期的时间 string _TempText=string.Empty;//控件文本副本 object _Sender;//保存外部控件源,用于发送事件时传回 #endregion #region 事件 /// <summary> /// 输入超时委托 /// </summary> /// <param name="Sender">来源</param> public delegate void InputTimeOut(object Sender); /// <summary> /// 输入超时事件委托对象 /// </summary> public event InputTimeOut OnInputTimeOut; /// <summary> /// 发送输入超时事件 /// </summary> private void SendInputTimeOutEvent() { if (OnInputTimeOut != null) OnInputTimeOut(_Sender); } #endregion #region 构造函数 /// <summary> /// 构造函数 /// </summary> public ScanningGunMonitor() { _Timer.Elapsed+=Timer_Elapsed;//内置事件对象绑定触发事件方法 } #endregion #region 属性 #region 条码最小长度 int _MiniLength = 20; /// <summary> /// 读取或设置条码最小长度值,当超过一个时钟周期后,如果条码文本不符合最小长度则被丢弃。 /// </summary> public int MiniLength { get { return _MiniLength; } set { _MiniLength = value; } } #endregion #region 按键间隔超时限定值 int _TimeOut=100; /// <summary> /// 读取或设置按键间隔超时限定值 /// </summary> public int TimeOut { get { return _TimeOut; } set { _TimeOut = value; } } #endregion #region 时钟周期 int _ClockTick = 100; /// <summary> /// 读取或设置内置时钟周期 /// </summary> public int ClockTick { get { return _ClockTick; } set { _ClockTick = value; } } #endregion #endregion #region 方法 /// <summary> /// 当发生按键时检查条码文本是否超时(开启内置时钟) /// </summary> /// <param name="sender">发送控件</param> /// <param name="inputText">条码文本</param> /// <returns>有效按键标志</returns> public bool CheckKeyPress(object sender,string inputText) { int gap; _Sender = sender; DateTime thisTime = DateTime.Now; gap = thisTime.Subtract(_TickTime).Milliseconds; if (_TickTime == DateTime.MinValue)//第一次 { _Timer.Interval = _ClockTick; _Timer.Enabled = true;//开启时钟 SendInputTimeOutEvent();//发送输入超时事件 return true;//保留当前输入字符 } else { if (gap > _TimeOut) { StopCheckGap();//停止检查输入间隔 _TempText = "";//清空本地文本 SendInputTimeOutEvent();//发送输入超时事件 return false; //通知取消当前输入 } } _TickTime = thisTime;//保存时间现场,用于下一周期判断依据 _TempText = inputText;//保存文本,提供时钟事件判断依据 return true;//保留当前输入 } /// <summary> /// 停止检测时间间隔,重置状态,停止内置时钟 /// </summary> private void StopCheckGap() { _TickTime= DateTime.MinValue; _Timer.Enabled = false; } /// <summary> /// 时钟事件方法 /// </summary> /// <param name="sender">发送者</param> /// <param name="e">事件对象</param> private void Timer_Elapsed(object sender,ElapsedEventArgs e) { int gap = e.SignalTime.Subtract(_TickTime).Milliseconds; if (gap > _TimeOut) { StopCheckGap();//停止检测,可能是扫描枪扫描结束,也可能是手工输入间隔 if (_TempText.Length < _MiniLength)//进一步检查长度,如果较短,说明为手工输入 { _TempText = "";//清空本地文本 SendInputTimeOutEvent();//发送输入超时事件 } } } #endregion }
三、类的使用
1、定义文本框事件
private void txtProductBarCode_KeyPress(object sender, KeyPressEventArgs e) { if (!ScanerMonitor.CheckKeyPress(txtProductBarCode, txtProductBarCode.Text)) { e.KeyChar = '\0'; } }
调用对象方法,传递当前文本框和文本内容,如果返回false,将当前按键值清除。
2、定义扫描枪监控对象
ScanningGunMonitor ScanerMonitor = new ScanningGunMonitor();
3、定义事件响应
定义事件方法
ScanerMonitor.OnInputTimeOut+= new ScanningGunMonitor.InputTimeOut(ScanerMonitor_OnInputTimeOut);
这里要说明下,可能我将类封装在DLL,所以与主窗口不在同一线程,因此事件触发时总是出现线程不可访问的错误,不得已做了下处理,增加了一个委托并在事件方法内进行了改造。
定义线程委托
private delegate void OnThreadInput(object sender);
定义具有跨线程的事件相应
private void ScanerMonitor_OnInputTimeOut(object sender) { if (InvokeRequired) { OnThreadInput mydelgate = new OnThreadInput(ScanerMonitor_OnInputTimeOut); //异步的委托 this.Invoke(mydelgate, new object[] { sender }); return; } TextBox box = (TextBox)sender; box.Text = ""; }
通过多次调试改进,该类可以接收任何控件源,类不负责清空或处理控件,而是通过事件让调用者自己处理,这样可以降低耦合,不过目前还没有在WEB项目中测试过。