如果把“VB标准”当成一场单纯的能力比拼,很容易陷入版本争论。真正接手过 Visual Basic 老项目的程序员会知道,VB 生态里最磨人的不是语法,而是一堆长期存在的“工程边界”:进程边界、窗口边界、输入边界、COM 控件边界。我去年维护一台老设备上位机时,最耗时的不是把代码改通,而是让一个在 Windows 7 时代写好的 VB6 程序在 Windows 10 上稳定运行:要和串口设备按帧收发数据,要定位另一个老进程的窗口标题,要模拟键盘输入后不丢按键,还要在一个没有源码、只有 exe 的界面上猜测内部逻辑。这几个问题,恰好对应了经常被搜到的几组词:VB 模拟键盘输入 SendInput、VB 应用 MSComm 控件实例、VB Decompiler、VB UI 控件、VB 调用 EnumWindows。
这篇文章要说的“VB标准”,不是某个神秘评级体系,而是老 VB 工程维护里反复出现的一套实用判断标准:先看清你能动什么,再决定怎么动。尤其在没有源码、控件缺失、Windows 版本迁移这几类场景里,这套标准比很多新框架的技巧更值钱。
1. 拿到遗留 VB 程序,先从工程还原和授权边界开始
1.1 为什么“反编译”是维护链条的第一环
很多 VB6 项目在交付后并不总是保留完整源码。常见情况是:当年开发的人走了,安装包还在,能跑,但frm、bas、vbp文件不知去向;或者从中途某个版本开始,只有exe被人反复复制,源码早已和旧电脑一起消失。
这时候,直接看反编译结果几乎是必经之路。
VB6 程序在编译时有两种常见输出形式:Native Code 和 P-Code。前者更接近机器指令,后者是一种中间字节码。不同编译方式对反编译工具有直接差异。有一类工具专门针对 VB 程序做逆向还原,典型名字里会带 Decompiler,网上也常看到 VB Decompiler、VB Decompiler Pro 这类名称。它们能做的事情大致包括:列出窗体、模块、类模块,定位事件过程,还原字符串引用,甚至在 P-Code 模式下恢复出和源码非常接近的伪代码。
但这里必须先强调边界:反编译只能用于自己拥有源码权限的旧项目,或者用于获得授权的维护场景。把它当成“破解别人软件”的手段,既不合适,也超出了本文要讨论的技术范围。
1.2 反编译能还原什么,不能还原什么
在实际使用中,反编译的结果更像是一张“施工图纸”,而不是原始源代码。它能告诉你事件流程、按钮名称、控件属性、字符串常量去了哪里,但不能完整还原你已经丢掉的所有变量命名和注释。
在开始反编译前,可以先建立一个预期表格:
| 问题 | 常见情况 |
|---|---|
| 窗体结构 | 能还原出多数控件和布局信息,但图形资源不一定能完美导出 |
| 事件过程 | 按钮点击、窗体加载等事件过程通常能定位 |
| 字符串引用 | 可以顺着字符串反查代码位置,是快速理解业务的关键 |
| 变量名和注释 | 通常不可恢复,还原后可能要按业务语义重新命名 |
| Native Code 程序 | 接近汇编级,阅读成本比 P-Code 高不少 |
所以,正确姿态不是指望“一键恢复源码”,而是把反编译当成业务盘点工具。先找出有哪些窗体、哪些按钮、哪些字符串,再慢慢拼出程序的大致脉络。
1.3 先用“字符串索引 + 事件定位”搭业务清单
拿到一个没有源码的 VB exe,我一般不会马上去读反编译细节,而是先做一轮“黑盒+白盒”混合操作:
- 先运行程序,记录界面上出现的所有按钮、菜单和提示文案。
- 用反编译工具搜索这些提示文案。
- 找到每个字符串在哪个事件过程中被引用。
- 对照触发按钮和输入框,形成一张“界面动作 → 事件过程 → 底层 API/控件”的映射表。
- 在这个映射表基础上,再判断能不能做兼容性修改,还是必须重写。
这个过程有一个额外好处:它会逼着你先理解业务闭环。比如一个串口下位机程序,往往不是发一条命令就完事,而是发送后要等应答、解析帧、超时重试。如果只盯着控件事件,很容易错过真正的核心逻辑。
建议把反编译出来的工程还原结果放在虚拟机或隔离目录里做二次分析,避免误改重要生产环境。
2. 用 SendInput 模拟键盘输入,胜在“真实按键”而不是“发消息”
2.1 模拟键盘输入不是只有 SendKeys 一种选择
老 VB 项目经常需要模拟键盘操作。典型场景是:一个旧程序不提供命令行参数,只能靠人工点击按钮、输入内容;另一个新系统要自动调用它,但又不方便改它的源码,于是程序只能通过模拟键盘来驱动。
很多新手第一反应是用SendKeys。这不是不行,但问题很多:它容易受窗口焦点影响,运行速度不稳定,在输入法状态不同时结果也不一样。对稍复杂的界面,尤其是目标窗口标题经常变化的情况下,SendKeys往往会莫名其妙失败。
相比之下,Windows API 层的能力更可靠一些。常见几个方案是:
| 方案 | 特点 | 适合场景 |
|---|---|---|
| SendKeys | 写法简单,但容易受焦点和时序影响 | 极简单的界面自动化 |
| keybd_event | 老 API,能模拟按下和释放 | 低版本兼容场景中的单键操作 |
| SendInput | 较新的输入模拟 API,能模拟更真实、支持 Unicode | 需要稳定输入的自动化流程 |
| SendMessage / PostMessage | 直接向窗口发消息,不是真正键盘队列 | 已知目标控件句柄时的文本写入 |
这里要特别说明一点:如果只是想给某个输入框设置一个值,用 SendMessage 发送WM_SETTEXT成本最低。但目标程序如果是靠键盘事件来触发业务逻辑的,比如扫码枪输入、按键热键、组合键菜单,那SendMessage就不一定有用,因为它的行为不完全等同于用户真实按键。
这就是 SendInput 的核心价值:它把输入事件投递到系统输入队列,目标程序看起来就像用户在真实键盘上按了一次键。
2.2 最小调用流程与常见写法
在 VB6 里声明 SendInput 会比在 C# 里麻烦一些,因为它涉及结构体,而 VB6 处理联合类型并不方便。经常看到的方法是用 Type 定义INPUT和KEYBDINPUT,再借助CallWindowProc或内存拷贝绕开联合限制。
这里不直接堆一个超长声明,先看最小流程:
' 示意代码,仅体现虚拟键和扫描码思路,不用于直接编译 ' 1. 先找到目标窗口,并把它放到前台 ' 2. 获取窗口句柄后,调用 SetForegroundWindow ' 3. 按下某个键 ' 4. 松开某个键如果你只是临时验证,用老接口 keybd_event 也能跑:
Private Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, ByVal bScan As Byte, ByVal dwFlags As Long, ByVal dwExtraInfo As Long) Private Sub SendKey(vKey As Integer) keybd_event vKey, 0, 0, 0 keybd_event vKey, 0, 2, 0 End Sub这段代码的核心是“按下 + 释放”。虚拟键码代表逻辑键,扫描码代表物理键位。大多数简单按键场景只需要传入虚拟键码,但如果你要模拟组合键或者处理某些特殊程序,扫描码也很关键。
需要说明的是:keybd_event 属于旧接口,遇到带管理员权限、UAC 弹窗、UIPI 隔离这类场景时会受限。生产环境如果追求稳定,仍然应该优先使用 SendInput,只是 VB6 里的声明要花更多功夫。
2.3 最容易翻车的 4 个坑
模拟键盘输入看起来简单,真正稳定跑起来却很难,主要坑集中在四层:
第一,前台焦点。目标窗口如果没有激活,输入会跑到当前前台窗口里。不能只靠AppActivate字符串匹配窗口标题,因为标题可能重复,也可能在启动过程中临时变化。更稳的流程是先FindWindow或EnumWindows找到句柄,再SetForegroundWindow。
第二,权限隔离。普通权限程序不能直接向高权限窗口模拟输入,这是 Windows 的用户界面隔离机制。自动化程序要以管理员身份运行时,目标程序通常也要同一权限级别,否则按键会无效。
第三,输入法。如果模拟的是普通 ASCII 字符,很多时候会被输入法拦截。更稳的做法是模拟 Unicode 输入,也就是 SendInput 里的KEYEVENTF_UNICODE标志。
第四,节奏。输入过快,目标程序处理不过来,会丢字符。不能只关注“发出去了”,还要关注“目标程序收到了”。常见的补偿方式是每次按键之间加一点延时,或者定时检查界面状态确认触发成功。
仿真输入最有价值的不是快,而是可复现。每次输入前先确认窗口存在,输入后再验证结果,比盲目加快速度靠谱得多。
3. MSComm 控件实例:串口不是发字符串,要按照通信协议处理
3.1 控件配置有一套“实用套路”
VB 老项目里,MSComm 控件几乎是串口上位机的标配。它的全称是 Microsoft Communications Control,常见文件是 mscomm32.ocx。虽然现在很多人转向 .NET 里的System.IO.Ports.SerialPort,但存量设备里还有大量基于 MSComm 的上位机程序。
一个典型的 MSComm 配置流程类似这样:
MSComm1.CommPort = 3 MSComm1.Settings = "9600,N,8,1" MSComm1.InputMode = 1 ' 1 表示二进制方式 MSComm1.RThreshold = 1 ' 缓冲区收到 1 个字节就触发 OnComm MSComm1.InputLen = 0 ' 读取时取走整个接收缓冲 MSComm1.PortOpen = True接收事件里通常会这样写:
Private Sub MSComm1_OnComm() Select Case MSComm1.CommEvent Case 2 ' comEvReceive Dim buf() As Byte buf = MSComm1.Input ' 将数据交给缓冲区处理函数 Call ProcessReceivedBytes(buf) End Select End Sub这里有一点经常被忽视:InputLen默认不是 0。如果以前有人设过InputLen = 1,那么每次调用MSComm1.Input只能读一个字节,容易造成接收逻辑混乱。
3.2 为什么不能只看到字符,要看到“帧”
不少初学者把一个串口程序调通后,会极度兴奋,因为 OnComm 事件确实触发了,Input也确实读到了数据。但很快会发现:数据长度经常不完整,或者下次重发后内容错位。
原因很简单:串口是流式传输,没有“消息边界”。设备端发送的是一段字节流,但电脑端在哪一次触发 OnComm、每次收到多少字节,完全取决于串口缓冲和系统调度。
正确做法不是把接收功能写成一个点,而是写成一个“攒数据 + 找帧头 + 按长度提取