follow my heart...
分类:
2006-10-30 09:12:49
虽然 Visual Basic 这种语言从来都不用于硬件驱动程序,但是它经常用于控制通讯端口。串行和并行通讯是操作系统的一个功能而不是语言,因此通常情况下,这种通讯是通过一个 ActiveX 控件(如 MsComm)或第三方组件与 kernal32 进行直接通讯而实现的。
升级此类应用程序并非易事。虽然硬件通讯的一般策略对于 Visual Basic 2005 和 Visual Basic 6.0 而言是一样的,但名称已经更改“以避免混淆”并且要找到完成工作所需的类也很难。
本文,我将介绍用于设备编码的通用解决方案。本文还介绍与 Visual Basic 2005 通讯有关的串行设备(例如,调制解调器)、并行设备(例如,打印机),以及视频显示器和红外线端口。您会发现,从一个平台转换到另一个平台是一个令人愉快的过程,最终可以获得可管理性更高的代码。
Visual Basic 6 要求程序员直接与操作系统打交道以便与 PC 直接通讯。.NET Framework 的 2.0 版本(用于 Visual Basic 2005)添加了很多硬件控制。这意味着您可以使用托管代码,而之前通常必须为此创建 Win32 调用。
由于 Visual Basic 并不常用于硬件控制,因此相关文档或 MSDN 库中关这方面的内容很少。您应该知道,这并非因为硬件控制的类不够强大。而是因为 MSDN 文章的受众的主体是程序员,而他们当中的大多数并不使用 Visual Basic 来控制硬件。
这并不是说 Visual Basic 2005 有一个完美的硬件语言表示形式。它的确没有,但它有一个显著改进的模型;在这里,我将为您介绍它可以完成以及不能完成的功能。
例如,下面将为您全面介绍串行端口通讯。实际上,在使用串行端口时,您不再需要查找 Win32 库以获取任何内容。SerialPort 类将为您完成所需的大多数任务。特别地,它也可以处理打印机。视频驱动程序系统也在 Visual Basic 2005 中进行了很好的处理。仅当您遇到更深奥的硬件控制部分时,您才需要使用非托管 Interop 类。
为了进一步简化,My 对象包含了很多用于 PC 硬件的类。My.Computer 在公开主机 PC 的工作方面作了大量工作。键盘、鼠标、显示器、调制解调器和其他硬件功能均在智能感知中,从而几乎不需要其他的工作。
Visual Basic 6 中的串行端口连接器是 MSComm 控件。您可以通过 Interop 在 .NET 中使用 MSComm 控件,但需要将 Visual Basic 6 安装在开发计算机上以避免授权问题。您也可以使用 pinvoke 直接与 kernel.dll 会话,但这样做非常混乱(尽管在 MSDN 中有一个示例)。
相反,我推荐 Visual Basic 2005 中的 System.IO.Ports.SerialPort 类。它非常类似于旧的 MSComm 对象,但它是托管代码,因此会带来它的所有好处。这些方法的结构如下所示:
旧的 MSComm 成员 | 新的 SerialPort 类成员 |
OnComm 事件 |
DataReceived 事件 |
CommPort 属性 |
PortName 属性 |
Settings 属性 |
BaudRate、Parity、DataBits 和 StopBits 属性 |
PortOpen 属性 |
IsOpen 属性 |
Input 属性 |
Read 方法 |
Output 属性 |
Write 方法 |
示例:使用调制解调器
在不使用硬件的情况下编写硬件代码是困难的。Noah Code(Microsoft 程序经理)有一个好主意 — 即,使用两个“USB 到串行接口”的转换器,并在它们之间使用一个空的调制解调器,以便在您自己的计算机上进行测试。当您将一个插入到 PC 中时,它会获取一个 COM 端口号,如果您插入两个,您会获取两个端口号。如果您将它们与一个链接在一起,这将对发送和接收串行消息非常有益!
要将信息发送到选择的串行端口,您需要打开一个新 SerialPort 对象。SerialPort 类的构造函数(即当您实例化一个新对象时运行的 New 方法)接受您建立连接需要的所有值。您也可以使用适当的属性建立一个连接。例如,我们看一下 COM 端口 4 上的一个 56,000 bps 设备,使用构造函数,您将以如下方式实例化该控件:
Dim mySerialPort as SerialPort = new SerialPort("COM4", 56000, Parity.None, 8, StopBits.One)
由于可用属性(新对象的一部分)的增加,您也可以选择创建新 SerialPort,然后设置属性。它们之间并没有功能差异,但是最好了解该对象的创建所涉及的不同思路。
Dim mySerialPort As SerialPort = New SerialPort() With mySerialPort .PortName = "COM4" .BaudRate = 56000 .Parity = Parity.None .DataBits = 8 .StopBits = StopBits.One End With
想了解 Parity 和 StopBits 枚举器的功能吗?没什么特别之处。Parity.None 只是一个为 0 的常量 — 作用仅为在代码中增加可读性并防止出现幻数。如果您问我,我会说这非常棒。这两个代码片段在这两方面是相同的,而且它不像 MSComm 对象所需的生僻连接字符串那样含义模糊。
现在有 SerialPort 对象 — 您能使用它做些什么呢?您可以 Send,可以 Receive,就像使用 MSComm 对象一样。
正如该示例所示,该过程的 Send 部分非常类似于 MSComm 控件的 Send 函数。
Visual Basic 6
'This is assuming you have an MSComm control on your form called myMsComm myMsComm.Settings = "9600,N,8,1" myMsComm.CommPort = 2 myMsComm.PortOpen = True myMsComm.Output = "Hello World!" myMsComm.PortOpen = False
Visual Basic 2005
Dim mySerialPort As SerialPort = New SerialPort("COM4", 56000, Parity.None, 8, StopBits.One) With mySerialPort .Open() .Write("Hello world!") .Close() End With
几乎完全相同,而且这不是巧合。在 Visual Basic .NET 2003 中使用 kernel.dll 写入串行端口所需的代码有 80 行。这是必须完成的。
读取端的情况又如何呢?在 Visual Basic 2005 这方面,一切都非常好。在 Visual Basic 6 中,我们需要处理缓冲区大小,进行循环并等待,而且通常要等很长时间。由于 Visual Basic 6 不以流的方式“思考”,因此我们必须找到一个解决方案。为此,当我们必须从该端口(它高度依赖设备)接收数据时,Visual Basic 6 代码将如下所示。
'Code from MSDN Dim txtBuff As String Dim i As Integer Dim c As Long Dim BuffLength MSComm1.InBufferCount = 0 ' clear inBuffer Do ' wait for incoming bytes DoEvents Loop Until MSComm1.InBufferCount > 0 txtBuff = MSComm1.Input BuffLength = Len(txtBuff) ' number of received bytes ' (<= 4 bytes) bstem = Asc(Mid(txtBuff, 1, 1)) ' get module number bbuff(0) = Asc(Mid(txtBuff, 2, 1)) ' get packet size If BuffLength < 2 + bbuff(0) Then ' if received bytes < packet size MSComm1.InBufferCount = 0 ' do it again Do DoEvents Loop Until MSComm1.InBufferCount > 0 txtBuff = txtBuff & MSComm1.Input End If For i = 3 To Len(txtBuff) ' write received data c = Asc(Mid(txtBuff, i, 1)) ' to array bbuff() bbuff(i - 2) = c Next
现在,我们可以针对讨论的 SerialPort 对象的 DataReceived 事件编写一个新的事件处理程序。该示例只将它写入控制台,但是您可能想将它写入数据库或字符串数组或 Label 控件中的屏幕。
Private Sub mySerialPort_DataReceived(ByVal sender As Object, _ ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) _ Handles mySerialPort.DataReceived Console.Write(mySerialPort.ReadExisting()) End Sub
哪块代码更高效?显而易见。由于 Visual Basic 2005 可通过 .NET Framework 更好地处理操作系统事件,因此它可以很好地处理中断的串行输入,而 Visual Basic 6 却不能。
USB 的情况如何?
某些 USB 设备可作为 .NET Framework 的一部分进行单独处理。例如,闪存驱动器由 System.IO.DriveInfo 处理,且类型是 DriveType Removable。
最后要面对的棘手的问题是硬件驱动程序,但是有一个通用规则。任何使用默认 Microsoft 驱动程序并具有 COM 端口的设备都可由 SerialPort 类使用。例如,如果要使用不支持 RS232 的 USB 设备,那么您必须使用 HID.dll 和 kernel.dll 再次 pinvoke Win32 API。请查看 OpenConnection()、GetExternalHubName() 和 GetNameOf() 方法。有关这方面的更多信息,请访问 。
小技巧
在 SerialPort 控件中有几个非常棒的功能。由于比 MSComm 控件更通用,因此 SerialPort 可以很好地处理来自多种设备的输入。这不仅对于该控件名称而言是显而易见的,而且对于该类的某些成员也如此。
• |
使用 BaseStream 属性捕获具有进出该设备信息流的基础对象。在代码影响原始数据之前可以进行修改。 |
• |
对于那些相同的代码行,您具有 Encoding 选项,以便确定该流的预传输和传输后编码。 |
• |
MSComm 控件只有一个 Input 属性。SerialPort 类有六个读方法,包括 Read、ReadByte、ReadChar、ReadExisting、ReadLine 和 ReadTo。 |
• |
SerialPort 类实现 IContainer 接口。 |
• |
您可以访问 ErrorReceived 事件和 PinChanged 事件。这将使硬件控制器在该字段中更加稳定。 |
Visual Basic 6 中的打印功能具有两个选项 — Printers 集合,以及通过 winspool.drv 调用 Win32 API。这两个选项可以为您带来更强大的功能或灵活性,但需要大量代码。
winspool.drv API 成员包括 OpenPrinter、StartDocPrinter、StartPagePrinter、WritePrinter、EndPagePrinter、EndDocPrinter 和 ClosePrinter,等等。和许多 API 调用一样,这里有很多功能,但配置上会有问题。虽然可以找到您需要的打印机,而且使用 winspool 驱动程序的组件通常选择使用默认的打印机,但这样会严重地降低 PC 的灵活性。
Printers 集合和 Printer 对象是另一个方面 — 功能稍微少一些,但灵活性更大。通常,一个程序循环通过该集合以找到一个匹配所需条件的对象 — 例如,该集合中第一个具有 8x14 纸张的打印机。很明显,这有问题;但是当您具有打印机时,利用它可以做大量您需要的工作。
Visual Basic 2005 并不使用这些方法。相反,其目标是让用户选择打印机,如同一个实际程序一样。之后,您写入生成的流,就像其他多数 Windows 平台的语言一样。该控件(在功能方面替换了旧的 Printers 集合)可取自 Windows 窗体工具箱,称之为 PrintDialog 控件。
一般的 Visual Basic 程序的一个更常用功能是将固定宽度的报告写到行打印机,从而复制 COBOL 程序功能。当 Visual Basic 6 有效时,打印机通常处于 PC 的打印机端口。这些天以来,这台打印机差不多周游了半个世界。这就是 PrintDialog 之所以如此有用的原因。
有了打印机之后,您会发现 PrintDocument 对象的布局类似于 Visual Basic 6 中的 Printer 对象,具有常见的 .NET Twips。PrintDocument 对象提供将 Graphics 对象或 Text 流直接写入打印机的功能。将文本传入打印机通常需要将代码分成行并使用 Win32 调用。现在,Stream 对象与 .NET Framework 共同提供了一个比较不错的解决方案。
Visual Basic 6
'This is assuming there is a Printer object set up in the Form designer. Private Declare Function WritePrinter Lib "winspool.drv" _ (ByVal hPrn As Long, pBuf As Any, ByVal cdBuf As Long, pcWritten As Long) As Long Private Declare Function OpenPrinter Lib "winspool.drv" Alias "OpenPrinterA" _ (ByVal pPrinterName As String, phPrn As Long, pDefault As Any) As Long Private Declare Function StartPagePrinter Lib "winspool.drv" (ByVal hPrn As Long) As Long 'Remember, textToWrite is only one line of text Public Function WriteTextToPrinter(ByVal textToWrite as String) as Long Call OpenPrinter(Printer.DeviceName, printerNumber, ByVal 0&) Printer.FontName = "courier" Printer.FontSize = 12 lTemp = printerNumber If lTemp = 0 Then error = GetLastError GoTo Exit_Err_Routine End If Call StartPagePrinter(printerNumber) Call WritePrinter(printerNumber, textToWrite, Len(textToWrite), Written) WriteTextToPrinter = Written End Function
Visual Basic 2005
Private Sub PrintDocument1_PrintPage(ByVal sender As System.Object, _ ByVal e As System.Drawing.Printing.PrintPageEventArgs) Handles PrintDocument1.PrintPage Dim lineCounter As Integer = 0 Dim linesPerPage As Single = 0 Dim line As String = String.Empty Dim myFont As Font = New Font("Times New Roman", 12) Dim printStream As StreamReader = New StreamReader("c:\text.txt") Dim topOfPage As Single = 0 ' Calculate the number of lines per page. linesPerPage = e.MarginBounds.Height / myFont.GetHeight(e.Graphics) ' Iterate over the file, printing each line. While lineCounter < linesPerPage line = printStream.ReadLine() If line Is Nothing Then Exit While End If topOfPage = 100 + lineCounter * myFont.GetHeight(e.Graphics) e.Graphics.DrawString(line, myFont, Brushes.Black, 100, _ topOfPage, New StringFormat()) lineCounter += 1 End While End Sub
虽然旧的 PrintForm 方法不再受支持,但是有其他更好的方法可以将图像传入打印机。现在,由于有了 System.Drawing 命名空间,您可以将图像直接发送到打印机 — 即使是动态制作的图像也可以!
Private Sub document_PrintPage(ByVal sender As Object, _ ByVal e As System.Drawing.Printing.PrintPageEventArgs) _ Handles PrintDocument1.PrintPage Dim printFont As New System.Drawing.Font _ ("Arial", 35, System.Drawing.FontStyle.Regular) e.Graphics.DrawString("This text is being written to the printer.", _ printFont, Brushes.Black, 10, 10) 'Underline it for fun! e.Graphics.DrawLine(Pens.DarkCyan, 10, 40, 200, 40) End Sub
其中的技巧是,PrintPageEventArgs 提供一个可以引用的 Graphics 对象,而且支持整个 Draw... 方法集,这些方法通常提供 Graphics 类。
遗憾的是,对于其他并行端口设备,您需要查找本地驱动程序并再次调用 Win32 API。如果在应用程序中引用 IO.DLL 库(它是一个 COM 库,因此它将生成一个 Interop 组件),您则可以在并行端口上读取和写入非打印机设备。
Private Declare Sub PortWordOut Lib "IO.DLL" (ByVal Port As Integer, _ ByVal Data As Integer) PortWordOut(23, TextBox1.Text)
您猜对了 — 这和 Visual Basic 代码非常相似。
除了 C++,其他没有什么语言可以直接与视频卡或显示器交互,这与使用调制解调器和打印机的情况不一样。主要是,Visual Basic 6 和 Visual Basic 2005 限制直接与显示器交互来查看分辨率的大小。
在 Visual Basic 6 中,Screen 对象处理该功能的大多数工作。此外,它使用 Twips(在 Visual Basic 中已由更通用的 Pixels 替换)的假设测量法处理它。
虽然这在某些方面很方便,但它隐藏了在不太明显的位置可能需要的功能。但是,对于多数显示器控制功能而言,它都能很好地工作。一个比较常用的功能是替换屏幕上一个准确位置的内容。您可以在 Visual Basic 2005 中进行此操作!
Visual Basic 6
Dim xCenter, yCenter As Single xCenter = ((Screen.Width / 2) - (Form1.Width / 2)) yCenter = ((Screen.Height / 2) - (Form1.Height / 2)) Form1.Move xCenter, yCenter
Visual Basic 2005
Dim xCenter, yCenter As Single xCenter = ((My.Computer.Screen.Bounds.Width / 2) - (Me.Bounds.Width / 2)) yCenter = ((My.Computer.Screen.Bounds.Height / 2) - (Me.Bounds.Height / 2)) Me.Location = New Point(xCenter, yCenter)
很少有人进行其他操作,甚至对于许多开发人员而言,复制它都有可能引出问题,这是因为在 Visual Basic 2005 中,看起来与屏幕有关的任何内容都处在不同的位置。
通常,Visual Basic 6 Screen 对象属性映射为 Framework 的特定部分。如果您想正确复制内容,请参见下表:
Visual Basic 6.0 | Visual Basic 2005 |
ActiveControl |
System.Windows.Forms.Application.ActiveForm.ActiveControl |
ActiveForm |
System.Windows.Forms.Application.ActiveForm |
FontCount |
没有对应物。枚举字体的行为是不同的。有关详细信息,请参阅 。 |
Fonts |
System.Drawing.FontFamilies 注 枚举字体的行为是不同的。有关详细信息,请参阅 。 |
Height |
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height |
MouseIcon |
没有对应物。有关详细信息,请参阅 Cannot set a custom MousePointer。 |
MousePointer |
System.Windows.Forms.Cursor 注 在 Visual Basic 6.0 中,更改 Screen.MousePointer 会更改光标的外观,除非再次更改 Screen.MousePointer。在 Visual Basic .NET 中,仅当再次处理 Windows 消息(直到下一次 DoEvents 调用或该程序的处理完成)时,它才会恢复为更改前的外观。 |
TwipsPerPixelX |
没有对应物。在 Visual Basic .NET 中,坐标以像素计,而 twips 不用作测量单位。 |
TwipsPerPixelY |
没有对应物。在 Visual Basic .NET 中,坐标以像素计,而 twips 不用作测量单位。 |
Width |
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width |
乍看上去,还真有点乱。但仔细看过之后,您会发现它很合理。Screen 类是该窗体的一个属性。为什么呢?多个显示器。现在我就坐在一台多显示器计算机前,我要修改我的 Visual Basic 6 示例应用程序以便使一个窗体居中。Fonts 也应该是一个较大实体的一部分。总之,这非常有意义。
下面简要说明 IrDA。在 Visual Basic 6 中,IrDA 由 Mscomm 控制,就像一个调制解调器,而且它必须具有一个可读的 COM 端口。读取和写入 IrDA 设备类似于前面一节 ( HYPERLINK \l "vb05legacyhard_topic2" 示例:使用调制解调器) 中的操作。然而,该协议有点粗略,因此很多开发人员采用第三方 InfraRed Device 控件,例如,ActiveSMS。
在 .NET Framework 中,IrDA 是一个网络设备,由 System.Net.Sockets 处理。这个极为方便的命名空间将在一个灵活的软件包中处理所有网络设备问题。根据 Sockets 命名空间的 MSDN 文档,支持以下网络协议:
• |
AppleTalk 协议 |
• |
本机 ATM 服务协议 |
• |
Banyan 协议 |
• |
CCITT 协议,例如,X.25 |
• |
MIT CHAOS 协议 |
• |
Microsoft Cluster 产品协议 |
• |
DataKit 协议 |
• |
直接数据链接协议 |
• |
DECNet 协议 |
• |
欧洲计算机厂商协会 (ECMA) 协议 |
• |
FireFox 协议 |
• |
NSC HyperChannel 协议 |
• |
IEEE 1284.4 工作组协议 |
• |
ARPANET IMPi 协议 |
• |
IP 版本 4 协议 |
• |
IP 版本 6 协议 |
• |
IPX 或 SPX 协议 |
• |
IrDA 协议 |
• |
ISO 协议 |
• |
LAT 协议 |
• |
MAX 协议 |
• |
NetBIOS 协议 |
• |
启用了 Network Designers OSI 网关的协议 |
• |
Xerox NS 协议 |
• |
OSI 协议 |
• |
PUP 协议 |
• |
IBM SNA 协议 |
• |
Unix 本地到主机协议 |
• |
VoiceView 协议 |
唉,其中有一些我甚至不明其意,而且我也学习了很长时间。
无论如何,能够实现这些功能的类就在 System.Net.Irda 命名空间(仅由 PocketPCs 和Media Edition of XP 支持)中。不过,这里有一些功能。
Dim myIrdaEndpoint As IrDAEndPoint = New IrDAEndPoint(irDevices(0).DeviceID, "IrDA Test") Dim myIrdaListener As IrDAListener = New IrDAListener(myIrdaEndpoint) myIrdaListener.Start() Dim myIrdaClient As IrDAClient = myIrdaListener.AcceptIrDAClient() MessageBox.Show(String.Format("Connected to {0}", myIrdaClient.RemoteMachineName)) myIrdaListener.Stop()
如果 IrDA 类将能够迁移到运行 Vista 的膝上型电脑,则仍然会保留它 — 这个留作读者的一个练习。
我不想作常规的总结,我要说说我对 My.Computer 命名空间的体会。这里我处理了通讯设备,但我们都知道,对于计算机硬件而言远非这些。
My 对象作为“快速拨号”设计到了 .NET Framework 中,并提高了开发人员的工作效率。许多连接到 PC 上的较简单的设备可以使用 My.Computer 类进行维护。
• |
My.Computer.Audio—媒体和扬声器 |
• |
My.Computer.Info—内存和处理器 |
• |
My.Computer.Keyboard—键盘配置 |
• |
My.Computer.Mouse—指针位置等 |
• |
My.Computer.Network—System.Net 命名空间类的快捷方式 |
• |
My.Computer.Ports—在本文的开头访问串行端口 |
• |
My.Computer.Screen—类似于我讨论的、过于自夸的 Visual Basic 6 Screen 对象 |
My.Computer 对象以某些方式提供了对 Visual Basic 6 所提供硬件的访问,同时,下面的 .NET Framework 提供了集成平台的功能。Jay Roxe 曾说过,Visual Basic 2005“将 Visual Basic 6 的灵魂带给了 .NET”,至少从这方面来讲他是对的。