简介:这是一份为具备一定Visual Basic基础、渴望突破初级瓶颈的开发者准备的经典进阶资料,以著名VB专家Matthew Curland的《Advanced Visual Basic》为核心,系统梳理面向对象编程、事件驱动机制、ADO.NET数据库访问、多线程并发、COM组件自动化、Web服务调用、性能优化与调试测试等高级主题,帮助读者从“会写代码”走向“写出高质量、可维护的专业代码”。资源共327个文件,压缩包约10.33MB,文件类型覆盖81个bas标准模块、34个cls类模块、15个frm窗体、9个ctl用户控件、7个ocx控件、9个odl接口定义以及tlb类型库和exe可执行程序等,既有直接可用的源码工程,也有编译好的组件,适合边学边改、按模块拆解。目前已有354人学习下载。配合随附的PDF电子书与VBoost、ROTHook、DispatchDelegator等大量示例,可以深入理解VB内部机制、内存管理与高级调试技巧,掌握构建专业Windows应用和可复用ActiveX组件的能力,是系统提升VB编程水准的优质实战资源。 说实话,做了这么多年Visual Basic相关的开发和培训,我越来越觉得“高级VB”这四个字被很多人误解了。很多人以为学会API声明、会用几个高级控件就是高级,也有人觉得VB已经过时,没必要深究。但真正接手过工业上位机、设备控制、自动化脚本的人都会明白,VB在桌面工具链里的地位依然稳得很——它语法亲民,IDE上手快,和Windows系统层API的亲和力极强,在“快速交付一个小工具”这件事上几乎没有对手。
这篇文章我想用几个真实能落地的方向来聊“高级VB编程”:模拟键盘输入的SendInput、串口通信的MSComm控件、Inet控件下载文件、GDI绘制动态表盘,每个都会给出可直接抄走的完整代码,并把我平时踩过的坑和排查思路一并写上。内容更适合有基础语法知识、想往“能真正解决生产环境问题”方向走的朋友,也适合刚接触老旧VB项目的同学用来补课。
1. 高级VB到底“高”在哪:先拆解思路
1.1 不是语法升级,是调用系统能力
VB的语法二十年几乎没有大变化,你不太可能靠背几个冷门关键字就“高级”起来。真正拉开差距的,是你能不能借助VB去调用操作系统或者底层硬件的能力。比如自动录入一个ERP系统,你没有接口权限,传统做法是让用户一个个填,高级做法就是SendInput把键盘事件直接投递到前台窗口,模拟真人操作——这在报表录入、批量台账维护场景里极其好用。
类似的能力还包括串口通信、网络请求、API调用、GDI绘图、注册表操作、进程管理。本质上,VB是壳,系统API和组件对象才是内核。理解了这一层,“高级VB“学的就不是语法,而是一整套和Windows打交道的方法论。
1.2 选型考量:为什么老项目还用VB
很多朋友问我:现在Python、C#这么流行,何必还守着VB?我一般会反问一句:你的客户是不是还有一台装着Windows XP的老工控机?是不是设备厂商只提供了ActiveX控件?是不是需求明天就要、没有时间搭一套复杂框架?
在这些场景里,VB的快速开发优势是实实在在的。我就接手过一个产线项目,设备方只给了一个MSComm的串口通讯协议,现场PC还是32位Win7,要求在三天内出一个扫码比对程序。用VB只需要拖控件、写事件、加判断,一天半就完成了。反过来说,如果你用C#去调同样的事情也行,但部署环境、DLL依赖、驱动兼容那些坑,会拖慢你很多精力。选工具不选贵的,选省事的,这就是VB还活在很多工业现场的原因。
1.3 我的学习路径建议
如果你是零基础直接看这篇文章,建议先掌握这些前置点:变量类型、数组、循环、函数封装、常用控件属性事件、错误处理。如果你已经会这些,那核心精力放在三件事上:一是Win32 API的调用,二是ActiveX控件的灵活使用,三是代码组织方式——把功能拆成模块、类模块,而不是全部堆在Form里。
我会在后面几节给出几个完整例子,你跟着敲一遍,再改成自己的业务需求,进步会非常快。
2. 核心实战一:SendInput模拟键盘输入
2.1 为什么需要SendInput而不是KeySend
VB自带的SendKeys很简单,但稳定性一般,尤其遇到管理员权限窗口、某些老系统界面时,经常发生按键丢失、无法输入的情况。SendInput是Windows API提供的原生低级键鼠事件接口,直接把输入事件注入系统消息队列,大部分场景下都能稳定模拟。
我记得很早以前用SendKeys给一台老设备的上位机程序填参数,CPU稍微忙一点就会少按键,客户测试时老觉得不靠谱。换成SendInput之后,再也没出现过丢键问题。这两者的差异,就好比你叫朋友帮你传话(SendKeys),和直接走到对方耳边说(SendInput)——虽然都能到,但后者路径更短、不容易被中途打断。
2.2 SendInput的完整代码实现
在VB里调用SendInput需要先定义结构体和Api声明,然后封装一个按键函数。结构体这块不能写错,字段偏移只要错一个,整个调用就会失败甚至崩溃。我直接给出我常用的完整模块代码:
Private Type POINTAPI X As Long Y As Long End Type Private Type MOUSEINPUT dx As Long dy As Long mouseData As Long dwFlags As Long time As Long dwExtraInfo As Long End Type Private Type KEYBDINPUT wVk As Integer wScan As Integer dwFlags As Long time As Long dwExtraInfo As Long End Type Private Type HARDWAREINPUT uMsg As Long wParamL As Long wParamH As Long End Type Private Type INPUTUNION mi As MOUSEINPUT ki As KEYBDINPUT hi As HARDWAREINPUT End Type Private Type INPUT_TYPE dwType As Long u As INPUTUNION End Type Private Declare Function SendInput Lib "user32.dll" _ (ByVal nInputs As Long, pInputs As INPUT_TYPE, _ ByVal cbSize As Long) As Long Private Declare Sub keybd_event Lib "user32.dll" _ (ByVal bVk As Byte, ByVal bScan As Byte, _ ByVal dwFlags As Long, ByVal dwExtraInfo As Long) Private Const INPUT_KEYBOARD = 1 Private Const KEYEVENTF_KEYUP = &H2 Public Sub PressKey(ByVal vKeyCode As Integer) Dim inputEvent As INPUT_TYPE inputEvent.dwType = INPUT_KEYBOARD inputEvent.u.ki.wVk = vKeyCode inputEvent.u.ki.dwFlags = 0 SendInput 1, inputEvent, Len(inputEvent) inputEvent.u.ki.dwFlags = KEYEVENTF_KEYUP SendInput 1, inputEvent, Len(inputEvent) End Sub这段代码定义了一个模拟“按下再松开”的按键流程,核心是SendInput这个API。dwType表示事件类型,1代表键盘事件;wVk是虚拟键码,比如回车是13,Tab是9。需要注意Len(inputEvent)的用法,它告诉API结构体有多大,如果传了错误的字节数,函数会静默失败,到时候你找半天都找不到原因。
2.3 实际使用:批量台账录入脚本
我在这里说一个做过的实际用法:一个仓库系统,每天要把Excel里的几百条编号录入到老旧的Web界面里。系统不提供导入接口,人工录入又累又容易错。我是这么设计的:程序用Excel对象模型逐行读取编号,找到界面的输入框坐标后点击激活,然后调用PressKey发送数字和回车。每个编号间隔0.3秒,跑完整个Excel自动换下一份。
核心代码如下,主要是循环发送:
Dim i As Integer For i = 1 To 500 ' 假设当前输入框已经聚焦 SendText "SN" & Format(i, "0000") PressKey vbKeyReturn ' 等待界面保存 Sleep 300 ' 激活下一行输入框 SendKeys "{TAB}" Next iSendText是我封装的一个字符串输出函数,本质是逐个字符查虚拟键码再调PressKey。还有一个经验:任何自动化脚本都必须有延时控制。界面响应速度有波动,延时太短会丢输入,太长则效率太低。我的建议是先用一个较小的延时跑三轮测试,找到稳定工作的临界值,再适当加50毫秒左右的余量。
2.4 注意:UAC权限与焦点窗口
SendInput最怕两件事:一是前台窗口不是目标窗口。你必须确保执行脚本之前,目标窗口已经激活并且输入框已经获得焦点。如果程序被切到后台,按键会打到别的窗口上,轻则输入错误,重则误操作。我习惯在执行前加一个延迟,给操作人员3秒时间切到目标窗口,再把窗体和按钮的Enabled设为False防止误点。
第二件事是UAC权限。如果目标程序是以管理员权限运行的,而你的VB程序不是,那么SendInput会被直接屏蔽,按键一个都进不去。解决办法是让VB程序也以管理员权限启动,或者用任务计划程序以最高权限运行。这个坑很隐蔽,不提前排查,很容易怀疑是代码问题。
3. 核心实战二:MSComm串口通信
3.1 项目场景:扫码枪数据采集
工业现场的扫码枪、电子秤、PLC,通常走串口通信。MSComm是VB经典串口控件,虽然老,但稳定,配合事件驱动机制,非常适合上位机数据采集。我做过一个小整机设备的项目:产线工人扫产品条码,数据实时进入数据库比对,不合格品立刻亮红灯报警。核心就是MSComm接收扫码枪的ASCII字符串,然后解析条码业务逻辑。
硬件连接其实很省心,一个USB转串口线接上扫码枪,设备管理器里找到COM口号,代码里配置好串口参数就行。真正的技术点在于,怎么可靠地接收和解析不定长的数据包。
3.2 MSComm常用属性配置
一定要先根据设备手册确认波特率、数据位、停止位、校验位。扫码枪大多是9600,8,N,1,某些老PLC可能用19200或4800。接错参数不会报错,只会出现乱码或者收不到数据,这也是排查时要优先排查的。
With MSComm1 .CommPort = 3 ' 串口号,从1开始计数 .Settings = "9600,N,8,1" .InputMode = comInputModeText .RThreshold = 1 ' 每收到一个字节就触发OnComm .SThreshold = 0 .InputLen = 0 .PortOpen = True End WithRThreshold = 1是最常用也最灵敏的配置,能保证每个到达的字节都会触发OnComm事件。InputLen必须设为0,表示一次性读取接收缓冲区的全部内容。InputMode按需设置为文本或二进制,文本模式适合扫码枪这类ASCII设备,二进制模式适合和PLC交换协议帧。
3.3 接收事件与数据解析
OnComm事件是核心入口,我习惯在里面先判断CommEvent的属性值,再把接收到的字符拼接进模块级的接收变量里,最后按结束符判断一条完整的数据。这样做能有效处理数据分包的问题,因为串口数据不一定会一次性到达,可能分好几次事件才能收到一条完整指令。
Private Sub MSComm1_OnComm() Dim strData As String Select Case MSComm1.CommEvent Case comEvReceive strData = MSComm1.Input If strData <> "" Then buffer = buffer & strData If InStr(buffer, vbCrLf) > 0 Then Call ProcessLine(buffer) buffer = "" End If End If End Select End SubProcessLine就是你的业务解析函数,比如取出条形码、查询数据库、控制指示灯。这样设计的好处是主逻辑和通信逻辑完全分离,模块清晰,后续加新设备只要换解析函数就行。
我做项目时还会另加一个串口长时间无数据的看门狗定时器。设备偶尔会掉线,如果上位机没有反应,操作员根本不知道。每10秒检查一下最近收到数据的时间,如果超时,界面提示并报警,这样问题能被现场人员第一时间发现。
3.4 常见坑:串口占用与32位依赖
MSComm控件是ActiveX组件,有些精简版系统可能没有注册。放到新机器上运行时,会报“不能加载控件”的错误。解决办法是拷贝MSComm32.ocx到系统目录并执行regsvr32 MSComm32.ocx。这个组件是32位的,VB6编译出的程序也是32位,在现代64位Windows上需要以x86模式注册,一般不会出大问题。
串口占用也是个高频问题。设备管理器里看到COM3被占用,但你代码里写的是COM3,打开端口就会报“端口无法打开”。先确认没有串口调试助手或者其他软件占住端口,再检查USB转串口的驱动装没装好。我曾经在客户现场排查了半小时,最后发现我自己的调试助手没关,占了串口——这件事后来被我写进项目交付文档,提醒所有调试人员。
4. 核心实战三:Inet控件下载文件与FTP操作
4.1 为什么不用URLDownloadToFile
VB里下载文件有好几种方式:WinINet的InternetOpenUrl、URLDownloadToFile API、还有Inet控件。我推荐Inet控件的主要原因是事件驱动、可控性强,你能随时感知到下载状态,超时也能控制。URLDownloadToFile虽然代码短,但它是同步阻塞的,遇到网络慢或服务器无响应,程序就卡死了。
Inet控件的底层是WinINet,支持HTTP和FTP,可以满足大部分桌面工具的需求。我曾在多台工控机上用它定时下载设备配置参数文件,从服务器拉取最新版本然后本地覆盖,稳定运行了很久。
4.2 Inet下载文件完整示例
先把Inet控件拖到窗体上,不需要放在可见区域,位置无所谓。关键代码主要是两个:发请求,然后在StateChanged事件里判断状态并读取数据流。这里我直接把下载的每个字节写入文件,避免了内存占用过高的问题,特别适合下载大文件。
Private Sub Command1_Click() Inet1.URL = "http://192.168.1.100/version/config.ini" Inet1.AccessType = icUseProxy Inet1.Protocol = icHTTP Inet1.Execute , "GET", "", "Pragma: no-cache" & vbCrLf End Sub Private Sub Inet1_StateChanged(ByVal State As Integer) Dim bytData() As Byte Select Case State Case 12 ' icResponseCompleted 响应完成 bytData = Inet1.GetChunk(1024, icByteArray) Open App.Path & "\config.ini" For Binary As #1 Do While UBound(bytData) >= 0 Put #1, , bytData bytData = Inet1.GetChunk(1024, icByteArray) Loop Close #1 MsgBox "下载完成" End Select End SubState值为12代表响应完成,这时候所有内容都到了缓冲区,可以用GetChunk按块读取。1024的块大小是我常用的,兼顾了速度与内存占用。Binary模式写入文件是通用做法,不管是文本还是二进制文件都能正确落地。要注意的是,GetChunk在事件里可以被调用一次或多次,只要返回的数组长度大于0,就说明还有数据,继续读取直到数组为空。
4.3 FTP上传与目录操作
除了下载,Inet控件还能做FTP上传、创建目录、改名等操作。我的一个日常运维工具,就是用Inet自动把生成的报表上传到局域网FTP服务器,省去人工打开FTP客户端再拖文件的操作。执行方式和下载类似,只要改变Execute的参数结构:
Inet1.Protocol = icFTP Inet1.URL = "ftp://192.168.1.50/reports/" Inet1.Execute , "PUT C:\report.txt report.txt"FTP命令字符串遵循标准FTP指令。PUT后面跟本地路径和远端文件名就可以完成上传。这个控件使用的是连接池机制,只要不调用Cancel,连接可以复用,连续上传多个文件速度也不错。为了安全起见,项目中不要把密码明文写在代码里,我一般放在一个单独的配置文件,启动时读取并动态赋值给UserName和Password属性。
4.4 网络异常的容错处理
网络程序最忌讳静默失败。Servers无响应时,StateChanged里可能一直等不到12状态。我习惯再加一个Timeout属性设置,以及一个定时器做超时兜底。比如30秒还没有完成,就提示用户“网络连接超时,请检查服务器地址和网络状态”。同时把每次下载的日志写到本地文件,方便后续排查。
另一个经验是:在线更新配置时,先下载到临时文件,校验完整后再覆盖正式文件,避免下载到一半程序突然退出导致配置文件损坏。系统运行越久,这种细节就越显得重要,因为它们直接关系到软件的健壮性。
5. 进阶实战四:GDI绘图实现动态表盘
5.1 用VB写一个罗盘时钟的思路
动态表盘、罗盘时钟这类可视化组件,很适合用来展示GDI绘图的进阶技术。如果你接到类似需求,比如给车间的LED看板做一个实时时钟界面,或者给设备做一个液位仪表盘,GDI就是最省事的方案。我演示一个罗盘时钟——外层表盘、刻度线、时针分针秒针,全部用VB的绘图指令实时绘制。
这种程序的难点不是循环画线,而是坐标计算。表盘上的每个点,(x, y)坐标都依赖圆心坐标和半径,以及对应角度。换算公式很简单:x = cx + r * sin(angle),y = cy - r * cos(angle),角度要用弧度制。表盘是360度,小时刻度是30度一格,分钟刻度是6度一格。搞懂这个换算逻辑,绘制的其他复杂图案基本都是套公式。
5.2 核心绘制代码
窗体上放一个Timer,Interval设为1000毫秒,每秒触发一次重绘。为了简单起见,我直接在Form_Paint事件里画,Timer触发时调用Form1.Refresh。代码分为画表盘、画刻度、画指针三部分:
Private Sub DrawClock() Dim cx As Long, cy As Long, r As Long Dim i As Integer Dim angle As Double, rad As Double Dim x1 As Long, y1 As Long, x2 As Long, y2 As Long cx = Me.ScaleWidth / 2 cy = Me.ScaleHeight / 2 r = IIf(cx < cy, cx, cy) - 30 ' 画外圈表盘 Me.Circle (cx, cy), r, vbBlue Me.FillStyle = 0 Me.FillColor = vbBlack Me.Circle (cx, cy), r - 8, vbCyan ' 画60个分刻度,其中大刻度代表小时 For i = 0 To 59 angle = i * 6 ' 每个刻度6度 rad = angle * 3.14159265 / 180 If i Mod 5 = 0 Then x1 = cx + (r - 18) * Sin(rad) y1 = cy - (r - 18) * Cos(rad) x2 = cx + (r - 5) * Sin(rad) y2 = cy - (r - 5) * Cos(rad) Me.Line (x1, y1)-(x2, y2), vbWhite Else x1 = cx + (r - 12) * Sin(rad) y1 = cy - (r - 12) * Cos(rad) x2 = cx + (r - 5) * Sin(rad) y2 = cy - (r - 5) * Cos(rad) Me.Line (x1, y1)-(x2, y2), vbGray End If Next i End Sub这里使用的是ScaleWidth和ScaleHeight而不是窗体宽高,因为ScaleMode不同时,实际绘图坐标会不一样。给刻度分粗细、分颜色的做法,在视觉上能模拟真实表盘的质感,同时也让用户更快识别时间,这是一个很细节但很加分的做法。
5.3 指针绘制与防闪烁技巧
指针的典型画法是Line方法从圆心到指向角度画线,线宽用DrawWidth属性控制。秒针最细,分针中粗,时针最粗。为了让表盘看起来立体,可以给指针层叠一个不同宽度和亮度的线条。绘制顺序也有讲究:先画表盘,再画刻度,再画指针,最后画圆心盖帽,这样指针根部和圆心连接处才整体。
一个避不开的问题是闪烁。Timer每秒触发一次Refresh,整个窗体重绘,如果绘图量很大或者系统性能一般,指针运动时有明显的闪动。解决办法是双缓冲:创建一个内存中的PictureBox或者使用BitBlt将完整图像一次性拷贝到屏幕。原理是把所有绘图先画到内存画布,再将整块区域一次性刷新到界面,避免逐条绘制导致闪烁。代码上相当于在内存APICreateCompatibleDC里操作完毕后再Paint到窗体,VB里最简单的方式是先放在一个临时PictureBox上,再PaintPicture复制过来。
这套GDI技巧不止用于时钟,还常用于仪表盘、示波器波形、趋势图、模拟监控大屏等。把坐标换算和双缓冲两个基础吃透,做可视化组件基本不需要依赖第三方控件。
6. 常见问题与排查技巧实录
6.1 高频问题速查表
我在做VB项目时,遇到问题最多的几个点,整理成一个表方便大家直接排查:
| 现象 | 可能原因 | 排查与处理方法 |
|---|---|---|
| SendInput没反应 | 窗口未聚焦或权限不足 | 检查目标窗口是否前台,确认程序是否以管理员身份运行 |
| 串口收不到数据 | 串口号错误或波特率不匹配 | 检查设备管理器,确认硬件参数,关闭其他占用串口的软件 |
| 串口数据乱码 | 数据位/停止位/校验位不匹配 | 对照设备手册重新设置Settings字符串 |
| Inet下载卡死 | 网络异常或服务器无响应 | 设置Timeout属性,加看门狗定时器做超时判断 |
| GDI闪烁严重 | 没有双缓冲 | 用内存PictureBox或BitBlt先渲染再整体刷新 |
| 提示缺少MSComm控件 | 目标机器未注册OCX | 使用regsvr32注册MSComm32.ocx |
6.2 一个隐蔽问题:API声明字节对齐
很多初学API的朋友会遇到一个诡异问题:代码完全照着例子写,SendInput却返回0,GetLastError返回87(参数错误)。绝大多数原因是结构体对齐字段不匹配。VB6的默认对齐方式和32位C语言结构体不完全一致,需要在结构体定义前面加上Option Explicit,并且在模块顶部加入对齐指令。如果依然不行,可以直接用我上文的代码,那些字段类型都是实测过的。
6.3 越用越顺手的几个习惯
我再分享几个从实战中养成的习惯,看上去很小,但对项目维护帮助极大。第一个是每个模块开启Option Explicit,强制变量先声明再使用。没有这一条,累到深夜时一个拼写错变量就能让你排查半小时。第二个是统一封装API调用,把SendInput、注册表操作、文件操作都做成独立模块,不掺杂在窗体事件里。第三个是所有外部资源连接都写好关闭逻辑——串口关闭、文件关闭、Inet取消,防止用户连续操作时资源泄漏。
最后还有一个习惯,不一定和技术相关,但对项目交付很关键:程序里留下一份简单的“使用与排障说明”弹窗,把常见的“串口被占”“提示缺少控件”这类问题直接告知使用人怎么处理。一线操作员不是开发者,能自己解决的问题不需要打电话找你。这其实也是对项目负责的做法,我在交付核心设备程序时都会加上这样一个说明页。
6.4 用版本管理思维维护老代码
老项目的代码容易越改越乱,特别是没有版本管理习惯的VB项目。我建议哪怕一个人开发,也把工程目录纳入一个简单的版本管理工具里,哪怕只是每次修改前手动复制一份带日期的备份。API声明的调整、控件版本的升级、系统环境变化,都可能导致以前能跑的代码突然罢工,有了版本记录才能快速回滚和对比差异。
如果你维护的设备程序涉及多个客户现场,强烈建议在代码里加一个版本号和编译时间的自动输出。当现场人员报问题时,你拿到版本号就能直接判断是哪一版代码,不用猜来猜去。这在工控领域,等于给自己的售后服务加了一条快车道。
最后的体会
做VB开发这十几年,我最大的体会是:高级从不在语言本身的复杂程度,而在于你用它解决了多少真实的问题。SendInput帮用户免除了大批量录入的重复劳动,MSComm让一台设备连上了数字化的数据流,Inet把运维从手动下载中解放出来,GDI让老掉牙的界面也能呈现出现代化的视觉效果——每一件事背后,都是VB这个“老工具”在几十年的Windows生态里积累下来的扎实功底。
现在编程世界的风向标转得很快,但工业现场、企业内网、老旧系统维护这些角落,VB依然安静地跑着。如果你想接触这类实战场景,不妨从我给出的这几个方向入手,照着代码敲一遍,再改造成自己的业务逻辑。过程中遇到卡壳,回到这篇文章的排查表里翻一翻,多数问题都有答案。等你亲手完成一个“能帮别人省时间”的VB程序,你一定会感受到,这门老语言的生命力远比传闻中顽强。
本文还有配套的精品资源,点击获取