C# ActiveX开发及安装部署
最近项目中,因为需要在WEB页面上操作串口,包括串口查询、打开、发送指令、接收数据、关闭串口等功能。如下所示:
考虑使用ActiveX来实现。因为以前没有这方面的经验,开发过程中也是遇到各种问题。废话不多说,下面进入正题:
1:打开VS2008,新建项目,以下是具体代码:
1 using System; 2 using System.Collections.Generic; 3 using System.Windows.Forms; 4 using System.IO.Ports; 5 using System.IO; 6 using System.Reflection; 7 using System.Runtime.InteropServices; 8 using mshtml; 9 using System.Text; 10 using Microsoft.Win32; 11 using System.Threading; 12 13 namespace WebSerial 14 { 15 [ProgId("WebSerial")]//控件名称 16 [ClassInterface(ClassInterfaceType.AutoDual), ComSourceInterfaces(typeof(ControlEvents))] 17 [Guid("6C6A0DE4-193A-48f5-BA91-3C180558785B")]//控件的GUID,用于COM注册和HTML中Object对象classid引用 18 [ComVisible(true)] 19 public partial class SerialPortControl : UserControl,IObjectSafety 20 { 21 ExSerialPort serialPort; 22 List<byte> buffer = new List<byte>(4096); 23 24 /// <summary> 25 /// 是否准备关闭串口 26 /// </summary> 27 private static bool m_IsTryToClosePort = false; 28 /// <summary> 29 /// 是否正在接收数据 30 /// </summary> 31 private static bool m_IsReceiving = false; 32 public SerialPortControl() 33 { 34 35 } 36 37 /// <summary> 38 /// 获取本地串口列表,以逗号隔开 39 /// </summary> 40 /// <returns></returns> 41 public string getComPorts() 42 { 43 string ports = ""; 44 foreach (string s in ExSerialPort.GetPortNames()) 45 { 46 ports += "," + s; 47 } 48 return ports.Length > 0 ? ports.Substring(1) : ""; 49 } 50 51 /// <summary> 52 /// 以指定串口号和波特率连接串口 53 /// </summary> 54 /// <param name="com">端口号</param> 55 /// <param name="baudRate">波特率</param> 56 /// <returns></returns> 57 [ComVisible(true)] 58 public string connect(string com, int baudRate) 59 { 60 close(); 61 serialPort = null; 62 serialPort = new ExSerialPort(com); 63 serialPort.BaudRate = baudRate; 64 serialPort.Parity = Parity.None; 65 serialPort.DataBits = 8; 66 serialPort.Encoding = Encoding.ASCII; 67 serialPort.ReceivedBytesThreshold = 5; 68 serialPort.ReadBufferSize = 102400; 69 70 try 71 { 72 serialPort.Open(); 73 if (serialPort.IsOpen) 74 { 75 m_IsTryToClosePort = false; 76 this.clear(); 77 serialPort.DataReceived += new SerialDataReceivedEventHandler(serialPort_DataReceived); 78 return "true"; 79 } 80 } 81 catch { } 82 83 return "false"; 84 } 85 86 /// <summary> 87 /// 清理串口数据并关闭串口 88 /// </summary> 89 [ComVisible(true)] 90 public void close() 91 { 92 m_IsTryToClosePort = true; 93 while (m_IsReceiving) 94 { 95 Application.DoEvents(); 96 } 97 98 if (serialPort != null) 99 { 100 serialPort.Dispose(); 101 } 102 } 103 104 /// <summary> 105 /// 清理串口数据 106 /// </summary> 107 [ComVisible(true)] 108 public void clear() 109 { 110 if (serialPort != null && serialPort.IsOpen) 111 { 112 serialPort.Clear(); 113 } 114 } 115 116 /// <summary> 117 /// 发送字符串 118 /// </summary> 119 /// <param name="s"></param> 120 [ComVisible(true)] 121 public void writeString(string hexString) 122 { 123 if (serialPort != null && serialPort.IsOpen) 124 { 125 byte[] bytes = strToToHexByte(hexString); 126 serialPort.Write(bytes, 0, bytes.Length); 127 } 128 } 129 130 /// <summary> 131 /// 字符串转16进制字节数组 132 /// </summary> 133 /// <param name="hexString"></param> 134 /// <returns></returns> 135 private static byte[] strToToHexByte(string hexString) 136 { 137 hexString = hexString.Replace(" ", ""); 138 if ((hexString.Length % 2) != 0) 139 hexString += " "; 140 byte[] returnBytes = new byte[hexString.Length / 2]; 141 for (int i = 0; i < returnBytes.Length; i++) 142 returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16); 143 return returnBytes; 144 } 145 146 /// <summary> 147 /// 字节数组转字符串16进制 148 /// </summary> 149 /// <param name="InBytes"> 二进制字节 </param> 150 /// <returns>类似"01 02 0F" </returns> 151 public static string ByteToString(byte[] InBytes) 152 { 153 string StringOut = ""; 154 foreach (byte InByte in InBytes) 155 { 156 //StringOut += String.Format("{0:X2}", InByte) + " "; 157 StringOut += " " + InByte.ToString("X").PadLeft(2, '0'); 158 } 159 160 return StringOut.Trim(); 161 } 162 163 /// <summary> 164 /// 接收数据 165 /// </summary> 166 /// <param name="sender"></param> 167 /// <param name="e"></param> 168 void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) 169 { 170 if (m_IsTryToClosePort) 171 { 172 return; 173 } 174 175 m_IsReceiving = true; 176 177 try 178 { 179 int n = serialPort.BytesToRead; 180 if (n > 0) 181 { 182 byte[] buf = new byte[n]; 183 serialPort.Read(buf, 0, n); 184 string dataString = ByteToString(buf); 185 Receive(dataString); 186 } 187 188 ////1.缓存数据 189 //buffer.AddRange(buf); 190 ////2.完整性判断 191 //while (buffer.Count >= 5) //至少包含帧头(2字节)、长度(1字节)、校验位(1字节);根据设计不同而不同 192 //{ 193 // //2.1 查找数据头 194 // if (buffer[0] == 0xff && buffer[1] == 0x55) //传输数据有帧头,用于判断 195 // { 196 // int len = buffer[2]; 197 // if (buffer.Count < len + 4) //数据区尚未接收完整 198 // { 199 // break; 200 // } 201 // //得到完整的数据,复制到ReceiveBytes中进行校验 202 // byte[] ReceiveBytes = new byte[len + 4]; 203 // buffer.CopyTo(0, ReceiveBytes, 0, len + 4); 204 205 // byte checkByte = ReceiveBytes[len + 3];//获取校验字节 206 // byte realCheckByte = 0x00; 207 // realCheckByte -= buffer[2]; 208 // for (int packIndex = 0; packIndex < len; packIndex++)//将后面的数据加起来 209 // { 210 // realCheckByte -= ReceiveBytes[packIndex + 3]; 211 // } 212 213 // if (checkByte == realCheckByte)//验证,看数据是否合格 214 // { 215 // string dataString = ByteToString(ReceiveBytes); 216 // Receive(dataString); 217 // } 218 // buffer.RemoveRange(0, len + 4); 219 // } 220 // else //帧头不正确时,清除 221 // { 222 // buffer.RemoveAt(0); 223 // } 224 225 //} 226 } 227 finally 228 { 229 m_IsReceiving = false; 230 } 231 } 232 233 public event ControlEventHandler OnReceive; 234 [ComVisible(true)] 235 private void Receive(string dataString) 236 { 237 if (OnReceive != null) 238 { 239 OnReceive(dataString); //Calling event that will be catched in JS 240 } 241 } 242 243 /// <summary> 244 /// Register the class as a control and set it's CodeBase entry 245 /// </summary> 246 /// <param name="key">The registry key of the control</param> 247 [ComRegisterFunction()] 248 public static void RegisterClass(string key) 249 { 250 // Strip off HKEY_CLASSES_ROOT\ from the passed key as I don't need it 251 StringBuilder sb = new StringBuilder(key); 252 253 sb.Replace(@"HKEY_CLASSES_ROOT\", ""); 254 // Open the CLSID\{guid} key for write access 255 RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true); 256 257 // And create the 'Control' key - this allows it to show up in 258 // the ActiveX control container 259 RegistryKey ctrl = k.CreateSubKey("Control"); 260 ctrl.Close(); 261 262 // Next create the CodeBase entry - needed if not string named and GACced. 263 RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true); 264 inprocServer32.SetValue("CodeBase", Assembly.GetExecutingAssembly().CodeBase); 265 inprocServer32.Close(); 266 // Finally close the main key 267 k.Close(); 268 MessageBox.Show("Registered"); 269 } 270 271 /// <summary> 272 /// Called to unregister the control 273 /// </summary> 274 /// <param name="key">Tke registry key</param> 275 [ComUnregisterFunction()] 276 public static void UnregisterClass(string key) 277 { 278 StringBuilder sb = new StringBuilder(key); 279 sb.Replace(@"HKEY_CLASSES_ROOT\", ""); 280 281 // Open HKCR\CLSID\{guid} for write access 282 RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true); 283 284 // Delete the 'Control' key, but don't throw an exception if it does not exist 285 k.DeleteSubKey("Control", false); 286 287 // Next open up InprocServer32 288 //RegistryKey inprocServer32 = 289 k.OpenSubKey("InprocServer32", true); 290 291 // And delete the CodeBase key, again not throwing if missing 292 k.DeleteSubKey("CodeBase", false); 293 294 // Finally close the main key 295 k.Close(); 296 MessageBox.Show("UnRegistered"); 297 } 298 299 #region IObjectSafety 成员 300 public void GetInterfacceSafyOptions(int riid, out int pdwSupportedOptions, out int pdwEnabledOptions) 301 { 302 pdwSupportedOptions = 1; 303 pdwEnabledOptions = 2; 304 } 305 public void SetInterfaceSafetyOptions(int riid, int dwOptionsSetMask, int dwEnabledOptions) 306 { 307 throw new NotImplementedException(); 308 } 309 #endregion 310 311 #region IObjectSafety 成员 312 313 private const string _IID_IDispatch = "{00020400-0000-0000-C000-000000000046}"; 314 private const string _IID_IDispatchEx = "{a6ef9860-c720-11d0-9337-00a0c90dcaa9}"; 315 private const string _IID_IPersistStorage = "{0000010A-0000-0000-C000-000000000046}"; 316 private const string _IID_IPersistStream = "{00000109-0000-0000-C000-000000000046}"; 317 private const string _IID_IPersistPropertyBag = "{37D84F60-42CB-11CE-8135-00AA004BB851}"; 318 319 private const int INTERFACESAFE_FOR_UNTRUSTED_CALLER = 0x00000001; 320 private const int INTERFACESAFE_FOR_UNTRUSTED_DATA = 0x00000002; 321 private const int S_OK = 0; 322 private const int E_FAIL = unchecked((int)0x80004005); 323 private const int E_NOINTERFACE = unchecked((int)0x80004002); 324 325 private bool _fSafeForScripting = true; 326 private bool _fSafeForInitializing = true; 327 328 329 public int GetInterfaceSafetyOptions(ref Guid riid, ref int pdwSupportedOptions, ref int pdwEnabledOptions) 330 { 331 int Rslt = E_FAIL; 332 333 string strGUID = riid.ToString("B"); 334 pdwSupportedOptions = INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA; 335 switch (strGUID) 336 { 337 case _IID_IDispatch: 338 case _IID_IDispatchEx: 339 Rslt = S_OK; 340 pdwEnabledOptions = 0; 341 if (_fSafeForScripting == true) 342 pdwEnabledOptions = INTERFACESAFE_FOR_UNTRUSTED_CALLER; 343 break; 344 case _IID_IPersistStorage: 345 case _IID_IPersistStream: 346 case _IID_IPersistPropertyBag: 347 Rslt = S_OK; 348 pdwEnabledOptions = 0; 349 if (_fSafeForInitializing == true) 350 pdwEnabledOptions = INTERFACESAFE_FOR_UNTRUSTED_DATA; 351 break; 352 default: 353 Rslt = E_NOINTERFACE; 354 break; 355 } 356 357 return Rslt; 358 } 359 360 public int SetInterfaceSafetyOptions(ref Guid riid, int dwOptionSetMask, int dwEnabledOptions) 361 { 362 int Rslt = E_FAIL; 363 364 string strGUID = riid.ToString("B"); 365 switch (strGUID) 366 { 367 case _IID_IDispatch: 368 case _IID_IDispatchEx: 369 if (((dwEnabledOptions & dwOptionSetMask) == INTERFACESAFE_FOR_UNTRUSTED_CALLER) && 370 (_fSafeForScripting == true)) 371 Rslt = S_OK; 372 break; 373 case _IID_IPersistStorage: 374 case _IID_IPersistStream: 375 case _IID_IPersistPropertyBag: 376 if (((dwEnabledOptions & dwOptionSetMask) == INTERFACESAFE_FOR_UNTRUSTED_DATA) && 377 (_fSafeForInitializing == true)) 378 Rslt = S_OK; 379 break; 380 default: 381 Rslt = E_NOINTERFACE; 382 break; 383 } 384 385 return Rslt; 386 } 387 388 #endregion 389 } 390 391 /// <summary> 392 /// Event handler for events that will be visible from JavaScript 393 /// </summary> 394 public delegate void ControlEventHandler(string dataString); 395 396 /// <summary> 397 /// This interface shows events to javascript 398 /// </summary> 399 [Guid("68BD4E0D-D7BC-4cf6-BEB7-CAB950161E79")] 400 [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] 401 public interface ControlEvents 402 { 403 //Add a DispIdAttribute to any members in the source interface to specify the COM DispId. 404 [DispId(0x60020001)] 405 void OnReceive(string dataString); //This method will be visible from JS 406 } 407 408 public class ExSerialPort : SerialPort 409 { 410 public ExSerialPort(string name) 411 : base(name) 412 { 413 } 414 415 public void Clear() 416 { 417 try 418 { 419 if (this.IsOpen) 420 { 421 this.DiscardInBuffer(); 422 this.DiscardOutBuffer(); 423 } 424 } 425 catch { } 426 } 427 428 protected override void Dispose(bool disposing) 429 { 430 Clear(); 431 432 var stream = (Stream)typeof(SerialPort).GetField("internalSerialStream", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this); 433 434 if (stream != null) 435 { 436 stream.Dispose(); 437 } 438 439 base.Dispose(disposing); 440 } 441 } 442 }
2:新建安装项目,将上面项目DLL打包生成Msi安装文件。这里不多说,网上大把文章教你怎么做。
3:新建Web项目,具体代码如下:
1 <html> 2 <head> 3 <title>JavaScript串口测试</title> 4 <meta http-equiv="Content-Type" content="text/html; charset=GB2312" /> 5 <script language="javascript" type="text/javascript"> 6 <!-- 7 function getComPorts() 8 { 9 var ports = webSerial.getComPorts(); 10 var arr = ports.split(','); 11 var ctl = document.getElementById("ComName"); 12 if(ctl) 13 { 14 ctl.options.length = 0; 15 for(var i=0;i<arr.length;i++) 16 { 17 ctl.options[i] = new Option(arr[i],arr[i]); 18 } 19 } 20 } 21 22 function connectComPort() 23 { 24 var com = document.getElementById("ComName").value.toString(); 25 var baudRate = document.getElementById("BaudRate").value; 26 var result = webSerial.connect(com,baudRate); 27 alert(result); 28 } 29 30 function clearComPort() 31 { 32 webSerial.clear(); 33 alert("清理串口成功!"); 34 } 35 36 function closeComPort() 37 { 38 webSerial.close(); 39 alert("关闭串口成功!"); 40 } 41 42 function writeString() 43 { 44 var hexString = document.getElementById("txtSend").value.toString(); 45 webSerial.writeString(hexString); 46 alert("发送指令成功!"); 47 } 48 49 function clearSend() 50 { 51 document.getElementById("txtSend").innerText=""; 52 } 53 54 function clearReceive() 55 { 56 document.getElementById("txtReceive").innerText=""; 57 } 58 59 function startTime() 60 { 61 var today=new Date() 62 var h=today.getHours() 63 var m=today.getMinutes() 64 var s=today.getSeconds() 65 // add a zero in front of numbers<10 66 m=checkTime(m) 67 s=checkTime(s) 68 document.getElementById('lblTime').innerHTML=h+":"+m+":"+s 69 t=setTimeout('startTime()',500) 70 } 71 72 function checkTime(i) 73 { 74 if (i<10) 75 {i="0" + i} 76 return i 77 } 78 79 --> 80 </script> 81 82 </head> 83 <body onload="startTime()" onunload="closeComPort()"> 84 <form name="form1"> 85 <fieldset style="width:225px;height:180px;text-align:center;"> 86 <legend>串口</legend> 87 <div style="float:left;width:250px"> 88 <br/> 89 <span>串口号:</span> 90 <select name="ComName" id="ComName" style="width:100px" > 91 </select> 92 <br/> 93 <span>波特率:</span> 94 <select name="BaudRate" id="BaudRate" style="width:100px" > 95 <option value="9600" selected="selected">9600</option> 96 <option value="57600" >57600</option> 97 <option value="115200" >115200</option> 98 <option value="1382400" >1382400</option> 99 <option value="3000000" >3000000</option> 100 </select> 101 <br/> 102 <br/> 103 <input type="button" id="btnGetPort" style="width:80px;height:30px;font-size:13px" name="btnGetPort" value="获取串口" onclick="getComPorts()"/> 104 <input type="button" id="btnOpenPort" style="width:80px;height:30px;font-size:13px" name="btnOpenPort" value="打开串口" onclick="connectComPort()"/> 105 <input type="button" id="btnClearPort" style="width:80px;height:30px;font-size:13px" name="btnClearPort" value="清理串口" onclick="clearComPort()"/> 106 <input type="button" id="btnClosePort" style="width:80px;height:30px;font-size:13px" name="btnClosePort" value="关闭串口" onclick="closeComPort()"/> 107 </div> 108 </fieldset> 109 <br /><br /> 110 <fieldset style="width:800px;height:150px;text-align:center;"> 111 <legend>发送区域</legend> 112 <div align="left">V6-握手指令: FF 55 01 01 FE</div> 113 <div align="left">V6-50HZ采集:FF 55 05 03 20 4E 00 00 8A</div> 114 <div align="left">V6-停止指令: FF 55 01 10 EF</div> 115 <div style="float:left;"> 116 <textarea id="txtSend" name="txtSend" style="width:800px;height:80px"></textarea> 117 <br/> 118 <input type="button" id="btnSend" style="width:100px;height:30px" name="btnSend" value="发送" onclick="writeString()"/> 119 <input type="button" id="btnClearSend" style="width:100px;height:30px" name="btnClearSend" value="清空" onclick="clearSend()"/> 120 </div> 121 </fieldset> 122 <br /><br /> 123 <fieldset style="width:800px;height:500px;text-align:center;"> 124 <legend>接收区域</legend> 125 <div style="float:left;"> 126 <textarea id="txtReceive" readonly="readonly" name="txtReceive" style="width:800px;height:430px"></textarea> 127 <br/> 128 <input type="button" id="btnClearReceive" style="width:100px;height:30px" name="btnClearReceive" value="清空" onclick="clearReceive()"/> 129 </div> 130 </fieldset> 131 <span id="lblTime"></span> 132 </form> 133 134 <p> 135 136 <object classid="clsid:6C6A0DE4-193A-48f5-BA91-3C180558785B" codebase="../WebSerialSetup.msi" width="442" height="87" id="webSerial" name="webSerial"> 137 </object> 138 </p> 139 140 <!-- Attaching to an ActiveX event--> 141 <script language="javascript" type="text/javascript"> 142 function webSerial::OnReceive(dataString) 143 { 144 document.getElementById("txtReceive").value += dataString+"\r\n";; 145 } 146 </script> 147 </body> 148 </html>
在使用JS调用ActiveX的时候碰上问题一:方法可以成功调用,而事件却调用失败。网上文章大都是说JS如何调ActiveX,而ActiveX这边的方法或者事件需要满足什么条件才能被JS成功调用却少有涉及。正当我山穷水尽疑无路的时候,事情有了转折,无意中看到一篇老外写的文章。链接地址是:http://www.codeproject.com/Articles/24089/Create-ActiveX-in-NET-Step-by-Step。才知道事件需要实现一个接口才能被JS识别。所以这部分代码后面被加上去了:
/// <summary> /// Event handler for events that will be visible from JavaScript /// </summary> public delegate void ControlEventHandler(string dataString); /// <summary> /// This interface shows events to javascript /// </summary> [Guid("68BD4E0D-D7BC-4cf6-BEB7-CAB950161E79")] [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] public interface ControlEvents { //Add a DispIdAttribute to any members in the source interface to specify the COM DispId. [DispId(0x60020001)] void OnReceive(string dataString); //This method will be visible from JS }
控件类名前面也加上这个
[ClassInterface(ClassInterfaceType.AutoDual), ComSourceInterfaces(typeof(ControlEvents))]
事件定义如下:
public event ControlEventHandler OnReceive; [ComVisible(true)] private void Receive(string dataString) { if (OnReceive != null) { OnReceive(dataString); //Calling event that will be catched in JS } }
本地打开网页,执行全部功能,操作正常,大功告成!!!
随后我把网页在本地发布并使用局域网中其他电脑(操作系统WIN7)IE访问该网页。那么问题来了:
1:机器弹出对话框拒绝安装当前ActiveX。经过对IE internet选项设置,放开对无签名ActiveX访问限制后。有的机器能弹出安装对话框,有的机器仍然坚决禁止。而且就算是弹出安装对话框,在确认安装后,对话框消失,插件也没装上。。。
好吧,这个问题也是没搞明白啥原因。后面时间紧迫,只好给客户一个下载链接,自己去点击下载。
2:下载安装包并安装完毕后,在客户机器上操作网页功能。前面几个按钮功能都OK,但是在填入指令点击发送,网页出现崩溃重新刷新的情况,而且换了几台机器都是这样。后面想起在生成安装包的时候,有弹出一个对话框,提示Visual Studio registry capture utility 已停止工作。百度一番后,找到解决方法:在Microsoft Visual Studio 9.0/Common7/Tools/Deployment 路径下面的regcap.exe文件,点击右键在属性页面中,选择兼容性页面,选中“以兼容模式运行”框就好了。兼容win7 就行。
相应设置后,重新生成安装文件。。。在客户机上安装后,一切正常!!!
最后附上完整项目代码:http://pan.baidu.com/s/1hq8MzJ2