项目背景
我集团公司流水线上的生产,每一个部件、产品完工后,都会贴上预先打印好的条形码,并且通过扫描枪扫描入系统。公司已有一套外购系统完成对流水线数据的采集。高层从"智能制造"理念出发,需要推广电子看板来监督流水线的作业效率。主要有三点功能要求:
1. 采集扫描枪的数据,绘制实时动态曲线图;
2. 当作业节拍(曲线图纵坐标)超过SAP系统中的规定值,停线报警;
3. 作业单完工后,数据自动保存,网络不佳时保存在本地,不影响流水线作业。
总体设计
由于产线已配置电脑和系统对扫描数据做采集,从节约成本的方向考虑,本系统继续安装在产线电脑上。显示方式上,将采用分屏显示的效果实现(一台电脑接两个显示器显示不同内容,后期其中一台显示器转为接高清电视机显示本系统)。另外产线的xp系统需升级为win7系统。在离线数据存储上,采用MSMQ实现临时数据存储(同时也能极大的减轻数据库压力)。未来的方向是统一将各客户端的临时数据集中发送到一台中转站机器的MSMQ中,再在空闲时段将数据发送至数据库服务器。前期,MSMQ将存储在本地,并在本系统单独开一进程隔一段时间向数据库推送完工作业单的数据。同时,由于鼠标焦点不在本系统上,为方便使用全局钩子获取扫描枪信息,本系统采用Winform技术实现。
原型图&框架
原型图说明:实际操作中,产线用户始终在操作和聚焦右边的主显示器窗口;左边的扩展显示器显示本系统(电子看板)【两台显示器虽共用一个主机,但显示了不同内容】。电子看板将实时绘制曲线图显示作业效率,如果作业效率低于SAP规定的值,则停线报警。(直观理解是两次扫描的时间过长则会想起警报...)
本系统主要的功能点集中在实时动态图表所在的窗口上。即便如此,从后期扩展的方向考虑,我还是搭建了一个系统框架。
使用LinqToSQL做为ORM的简单三层,由于时间紧迫,暂时缺少权限和日志系统,这是现存的不足。
技术难点&核心代码
一. 实时动态图表
本系统最大的难点,包括:a)使用全局钩子收集扫描枪数据; b)是实时动态图表的实现。
a). 全局钩子收集扫描枪数据
修改了下网上流行的"条形码钩子"类,网上流行的版本,当条形码第一个字符是字母的时候,有一些问题。

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Runtime.InteropServices; 6 using System.Reflection; 7 using System.Diagnostics; 8 9 namespace Barcode 10 { 11 /// <summary> 12 /// 条形码钩子 13 /// </summary> 14 public class BarCodeHook 15 { 16 public delegate void BarCodeDelegate(BarCodes barCode); 17 public event BarCodeDelegate BarCodeEvent; 18 19 public struct BarCodes 20 { 21 public int VirtKey; //虚拟码 22 public int ScanCode; //扫描码 23 public string KeyName; //键名 24 public uint AscII; //AscII 25 public char Chr; //字符 26 27 public string BarCode; //条码信息 28 public bool IsValid; //条码是否有效 29 public DateTime Time; //扫描时间 30 } 31 32 private struct EventMsg 33 { 34 public int message; 35 public int paramL; 36 public int paramH; 37 public int Time; 38 public int hwnd; 39 } 40 41 //键盘Hook结构函数 42 [StructLayout(LayoutKind.Sequential)] 43 public class KeyBoardHookStruct 44 { 45 public int vkCode; 46 public int scanCode; 47 public int flags; 48 public int time; 49 public int dwExtraInfo; 50 } 51 52 /// <summary> 53 /// 监控消息窗口的钩子 54 /// </summary> 55 /// <param name="idHook"></param> 56 /// <param name="lpfn"></param> 57 /// <param name="hInstance"></param> 58 /// <param name="threadId"></param> 59 /// <returns></returns> 60 [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] 61 private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId); 62 63 [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] 64 private static extern bool UnhookWindowsHookEx(int idHook); 65 66 [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] 67 private static extern int CallNextHookEx(int idHook, int nCode, Int32 wParam, IntPtr lParam); 68 69 [DllImport("user32", EntryPoint = "GetKeyNameText")] 70 private static extern int GetKeyNameText(int lParam, StringBuilder lpBuffer, int nSize); 71 72 [DllImport("user32", EntryPoint = "GetKeyboardState")] 73 private static extern int GetKeyboardState(byte[] pbKeyState); 74 75 [DllImport("user32", EntryPoint = "ToAscii")] 76 private static extern bool ToAscii(int VirtualKey, int ScanCode, byte[] lpKeyState, ref uint lpChar, int uFlags); 77 78 [DllImport("kernel32.dll")] 79 public static extern IntPtr GetModuleHandle(string name); 80 81 [DllImport("user32.dll")] 82 public static extern void SetCursorPos(int x, int y); 83 84 delegate int HookProc(int nCode, Int32 wParam, IntPtr lParam); 85 86 BarCodes barCode = new BarCodes(); 87 int hKeyboardHook = 0; 88 //此处使用char List 避免了原有代码中扫描出的结果是乱码的情况 89 List<char> _barcode = new List<char>(100); 90 private int KeyboardHookProc(int nCode, Int32 wParam, IntPtr lParam) 91 { 92 if (nCode == 0) 93 { 94 EventMsg msg = (EventMsg)Marshal.PtrToStructure(lParam, typeof(EventMsg)); 95 if (msg.message == (int)System.Windows.Forms.Keys.H && (int)System.Windows.Forms.Control.ModifierKeys == (int)System.Windows.Forms.Keys.Control + (int)System.Windows.Forms.Keys.Alt) //截获Ctrl+Alt+H 96 { 97 SetCursorPos(200, 200);//组合键使鼠标回到主屏幕 98 } 99 if (wParam == 0x100) //WM_KEYDOWN = 0x100 100 { 101 barCode.VirtKey = msg.message & 0xff; //虚拟码 102 barCode.ScanCode = msg.paramL & 0xff; //扫描码 103 104 StringBuilder strKeyName = new StringBuilder(255); 105 if (GetKeyNameText(barCode.ScanCode * 65536, strKeyName, 255) > 0) 106 { 107 barCode.KeyName = strKeyName.ToString().Trim(new char[] { ' ', '