近日在完成最后的 POS 打印模块时有所斩获。POS 机的小票打印是一个不太好控制的内容,原因是小票的长度从来不固定,它要根据用户购买的货品种类,以及附加信息的不同(例如可能需要打印折扣/优惠/会员等汇总信息);用报表+自定义纸张也许可以解决,不过长度的精确计算是件不容易实现的事,且动态自定义纸张要求当前用户拥有管理员或打印机操作员的权限,受到的限制颇多;另一方面,POS 打印机通常都自带字库,而且只能用直接输出控制码的方法才能控制钱箱接口的脉冲输出,所以用直接输出字符和控制码到打印机的方法才是 POS 中最合理的解决方案。
起初是采用 Fopen / Fwrite 等指令来直接输出的,在打印机联机的时候都很正常;然而,一旦打印机未开机或出现故障时,这种方法就出现问题了!不像 DOS 下直接输出到打印机,它可以通过 On Error 陷阱加 PrintStatus 函数来检测到打印错误和状态,超时长短也可以在 Config.fp 中通过 Timeout 设置项来控制;然而 Windows 下用 Fwrite 函数时的打印超时错误却从不出现,一旦打印机关闭或出错就使程序陷入无穷的等待状态,至少在我可以忍耐的时间内不会返回。
是否在 Windows 下就真的无法检测到打印超时了呢?关于这个问题,我想很多人都碰到过,然而却从来没有人找到过解决的办法,就算找到了方法也从来没人公布过,这是我在网络上经过无数次的搜索后得出的结论。
仔细分析过 Windows 中的输入输出控制函数后可以发现,通过 CreateFile 可以打开一个 DOS LPT 设备端口,进而用 WriteFile 就可以直接将数据输出到打印机中,但是它与使用 Fopen / Fwrite 的效果是一样的,也同样会在打印超时时陷入等待状态;但是,熟悉 win32 api 的好处是可以利用系统提供所有功能!api 中除了使用 WriteFile 这种同步输出方式以外,还提供了一种用于异步输出方式的函数 WriteFileEx,这个函数在它的最后一个参数中需要提供一个回调函数,当使用它来启动输出后,不像 WriteFile 需要一直等待输出完成才返回,而是立即返回,在它输出成功后,系统会调用你提供的回调函数来通知你操作完成。当然要得到回调通知你还需要让你的程序进入一种可等待的状态,而不能让 vfp 代码一直运行下去,否则系统是没有机会来启动回调的,这可以通过像 SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectsEx, MsgWaitForMultipleObjectsEx, SignalObjectAndWait 等函数来让系统进入可中断等待状态,这些函数通常用于线程同步。这里我使用了 SleepEx,在调用 WriteFileEx 后,立即调用 SleepEx 进入可中断等待状态,让它最多等待 5 秒(自定的超时值),如果系统在 5 秒内执行 WriteFileEx 成功,会调用回调函数并发送一个消息让 SleepEx 提前退出。上面就是不让输出进入无穷等待状态的方法,下面是这一部分的示例代码:
其中使用了 vfp2c32.fll 来实现回调函数,可以在这里找到它。
另外,如果系统中已安装了使用本地 LPT1 端口的打印驱动,Windows 仍会将打印输出导向这个驱动程序,有些打印驱动程序会将输出数据放入打印缓存中(跟设置也有关系),这样即使关闭了打印机,输出也同样会成功;所以,最佳的测试方法是删除所有已安装的驱动程序。不过,如果 Windows 打印驱动缓存了打印数据,返回成功的话,也就不会导致进入无穷等待状态了,所以我们也就不用考虑它带来的副作用了,因为我要解决的是进入超时等待的死锁问题。
- #define OUTPUT_PORT 'LPT1:'
- #define VFP2C_INIT_CALLBACK 0x00000100
- #define GMEM_ZEROINIT 0x0040
- #define GENERIC_READ 0x80000000
- #define GENERIC_WRITE 0x40000000
- #define FILE_SHARE_READ 0x00000001
- #define FILE_SHARE_WRITE 0x00000002
- #define FILE_FLAG_WRITE_THROUGH 0x80000000
- #define FILE_FLAG_OVERLAPPED 0x40000000
- #define FILE_FLAG_NO_BUFFERING 0x20000000
- #define OPEN_EXISTING 3
- #define OPEN_ALWAYS 4
- m.cPath = JUSTPATH( SYS(16))
- SET LIBRARY TO ( m.cPath + '/vfp2c32.fll' ) ADDITIVE
- INITVFP2C32( VFP2C_INIT_CALLBACK )
- m.oCallBack = CREATEOBJECT( 'MyCallBack') && 创建回调函数
- DECLARE Long CreateFile IN WIN32API ;
- String lpFileName, ;
- Long dwDesiredAccess, ;
- Long dwShareMode, ;
- Long lpSecurityAttributes, ;
- Long dwCreationDisposition, ;
- Long dwFlagsAndAttributes, ;
- Long hTemplateFile
- DECLARE Long WriteFileEx IN WIN32API ;
- Long hFile, ;
- String lpBuffer, ;
- Long nNumberOfBytesToWrite, ;
- Long lpOverlapped, ;
- Long lpCompletionRoutine
- DECLARE Long CancelIo IN WIN32API ;
- Long hFile
- DECLARE Long CloseHandle IN WIN32API Long hObject
- DECLARE Long SleepEx IN WIN32API ;
- Long dwMilliseconds, ;
- Long bAlertable
- DECLARE Long GlobalAlloc IN WIN32API ;
- Long uFlags, Long dwBytes
- DECLARE Long GlobalFree IN WIN32API ;
- Long hMem
- m.hh = CreateFile( ; && 以异步操作方式打开本地打印机端口
- OUTPUT_PORT, ;
- GENERIC_WRITE, ;
- FILE_SHARE_WRITE+FILE_SHARE_READ, ;
- 0, ;
- OPEN_EXISTING, ;
- FILE_FLAG_WRITE_THROUGH+FILE_FLAG_OVERLAPPED+FILE_FLAG_NO_BUFFERING, ;
- 0 )
- IF ( -1 == m.hh )
- MESSAGEBOX( '打开端口失败' )
- ELSE
- m.ol = GlobalAlloc( GMEM_ZEROINIT, 4*5 )
- WriteFileEx( m.hh, CHR(13), 1, m.ol, m.oCallback.Address ) && 启动异步写操作
- SleepEx( 5000, 1 ) && 设置 5 秒定为超时
- GlobalFree( m.ol )
- IF ( -1 == m.oCallback.ErrorNo ) && 如果是超时退出
- CancelIo( m.hh ) && 取消输出
- ENDIF
- CloseHandle( m.hh )
- IF ( -1 == m.oCallback.ErrorNo )
- MESSAGEBOX( '打印超时' )
- ELSE
- MESSAGEBOX( '打印正常' )
- ENDIF
- ENDIF
- m.oCallBack = NULL
- SET LIBRARY TO
- RETURN
- *!* 回调函数实现类
- DEFINE CLASS MyCallBack AS Exception
- Address = 0
- ErrorNo = -1
- FUNCTION Init
- This.Address = CreateCallbackFunc( ;
- 'CompleteCallback', 'Long', 'Long,Long,Long', This )
- ENDFUNC
- FUNCTION Destroy
- IF ( 0 != This.Address )
- DestroyCallbackFunc( This.Address )
- ENDIF
- ENDFUNC
- *!* 回调函数
- FUNCTION CompleteCallback( dwErrCode, dwBytesWritten, lpOverlapped )
- This.ErrorNo = 0 && 回调成功, 表示输出已完成
- RETURN 0
- ENDFUNC
- ENDDEFINE