简介:这份资源是Matthew Curland所著《高级VB编程》的配套代码与资料合集,面向已掌握VB基础语法、希望深入事件驱动、COM组件与Windows底层交互的开发者。内容围绕面向对象、事件处理、自定义控件、异常处理、多线程、ADO.NET数据访问、网络编程、自动化与COM组件、XML与Web服务、性能优化及调试测试等高级主题展开,并涉及反射、泛型、委托、Lambda表达式等进阶概念。压缩包共327个文件,约10.33MB,以bas模块、cls类模块、vbp工程文件、frm窗体、ctl控件、odl/olb类型库、h/cpp头与源文件、dll/ocx组件及exe示例为主,另含少量bmp、ico、rc资源与pdf文档,便于直接编译运行与二次开发。目前已有355人学习下载。读者可借助完整工程与底层API示例,理解VB调用COM、线程调度与类型库编辑等实战思路,适合作为进阶研读与项目参考。
1. 高级VB编程:一份把VB6底层能力讲透的代码合集
如果你现在还在维护一套VB6的老系统,或者手头有个VB6项目需要做指针操作、接口Hook、内存直接读写这类“越界”需求,那这份《Advanced Visual Basic》含代码的资源,大概率能帮你省下翻MSDN和反复试错的几天时间。VB6本身语法简单,但一旦碰到API声明、回调函数、结构体对齐、COM接口底层调用这些场景,普通教程基本就断档了。这份资源围绕VB6的高级特性展开,从Win32 API封装到类模块设计,再到内存操作和类型库技巧,配套代码可以直接导入工程跑起来。适合两类人:一是被老项目困住、需要快速定位底层写法的维护者;二是想搞明白VB6在Windows体系里到底能挖多深的开发者。它不是入门读物,但如果你已经写过几千行VB代码,这里面的东西会让你重新认识这门语言的上限。
2. 环境准备与工程结构:把代码跑起来之前先理清依赖
2.1 运行环境与工具链确认
这份资源的核心是VB6代码,所以第一步不是急着打开代码,而是确认你的环境能不能编译它。VB6的IDE在Win10/Win11上虽然能装,但有几个坑必须提前处理。常见做法是:安装VB6企业版或专业版,然后手动打上SP6补丁,否则部分API声明和类型库引用会报“未找到工程或库”的错误。如果你用的是64位系统,VB6 IDE本身是32位程序,编译出来的也是32位组件,这点在调用系统API时要注意指针宽度。
工具链方面,你需要准备三样东西:VB6 IDE本体、SP6累积补丁、以及一个能查看API声明的工具(比如API Viewer,或者直接查Win32 SDK头文件)。资源里的代码大量使用Declare语句直接调用kernel32.dll、user32.dll、advapi32.dll中的函数,如果本地缺少对应的类型库引用,编译会直接中断。
提示:在Win10以上系统安装VB6时,建议先关闭UAC或以管理员身份运行安装程序,否则注册表写入可能失败,导致后续无法加载ActiveX控件。
2.2 工程目录与模块划分
把资源解压后,你会看到按功能拆分的多个.vbp工程文件和.bas、.cls、.frm模块。典型的目录结构是这样的:
AdvancedVB/ ├── Chap02_API/ │ ├── APIHook.vbp │ ├── modHook.bas │ └── clsHook.cls ├── Chap05_Memory/ │ ├── MemAccess.vbp │ ├── modMemory.bas │ └── frmMain.frm ├── Chap08_COM/ │ ├── InterfaceDemo.vbp │ ├── clsCustom.cls │ └── modTypeLib.bas └── Common/ ├── modGlobal.bas └── clsError.cls每个章节目录对应一个独立可编译的工程,Common目录放的是公共模块,比如全局变量声明和错误处理类。这种拆分方式的好处是你可以单独打开某一章的工程来验证,不用一次性把所有代码都加载进来。我一般会先打开Chap02_API这个工程,因为它依赖最少,编译通过率最高,适合用来验证环境是否配好。
2.3 引用与组件注册
VB6工程依赖两类外部资源:一是通过“工程→引用”加载的类型库(如Microsoft Scripting Runtime、Microsoft ActiveX Data Objects),二是通过“工程→部件”加载的ActiveX控件(如Microsoft Common Dialog Control)。资源里的代码用到了Scripting.Runtime做文件操作,用MSCOMCTL.OCX做界面控件。如果你的系统里缺少这些组件,工程打开时会提示“找不到工程或库”。
解决办法是手动注册对应的OCX和DLL。以管理员身份打开命令提示符,执行:
regsvr32 MSCOMCTL.OCX regsvr32 COMCTL32.OCX注册完成后,在VB6的“工程→引用”对话框里勾选缺失的库,重新编译即可。如果某个引用显示为“MISSING”,取消勾选后重新浏览到系统目录下的对应文件。
注意:不要随意从网上下载来源不明的OCX文件替换系统组件,版本不匹配会导致IDE崩溃或编译出的程序在别的机器上无法运行。
2.4 编译与运行验证
环境配好后,打开Chap02_API工程,按Ctrl+F5全编译运行。如果一切正常,你会看到一个窗体,上面有几个按钮,分别触发不同的API调用。点击“Hook Demo”按钮,程序会尝试在当前进程内挂接一个API钩子,并在窗体上输出钩子触发次数。这个过程中如果弹出“权限不足”或“地址访问冲突”,说明你的系统开启了DEP(数据执行保护)或者杀毒软件拦截了内存写入操作。
常见做法是把VB6 IDE和编译出的EXE加入杀毒软件白名单,同时在“系统属性→高级→性能设置→数据执行保护”里,把编译出的EXE排除在外。这一步不做,后面内存操作那一章的代码基本跑不起来。
3. 核心模块拆解:API钩子、内存操作与类模块设计
3.1 Win32 API钩子的实现链路
资源里最硬核的部分是API Hook。VB6本身没有指针类型,但通过CopyMemory和AddressOf可以拿到函数地址,再配合VirtualProtect修改内存页属性,就能在运行时替换目标函数的入口。下面这段代码是modHook.bas里的核心逻辑:
' 声明必要的API函数 Private Declare Function VirtualProtect Lib "kernel32" ( _ ByVal lpAddress As Long, _ ByVal dwSize As Long, _ ByVal flNewProtect As Long, _ ByRef lpflOldProtect As Long) As Long Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _ ByRef Destination As Any, _ ByRef Source As Any, _ ByVal Length As Long) ' 安装钩子:把目标函数的前5个字节替换为跳转指令 Public Function InstallHook(ByVal TargetFunc As Long, _ ByVal HookFunc As Long) As Boolean Dim oldProtect As Long Dim jmpCode(0 To 4) As Byte ' 构造跳转指令:E9 + 相对偏移 jmpCode(0) = &HE9 Dim offset As Long offset = HookFunc - TargetFunc - 5 CopyMemory jmpCode(1), offset, 4 ' 修改内存页为可读写 If VirtualProtect(TargetFunc, 5, &H40, oldProtect) = 0 Then InstallHook = False Exit Function End If ' 写入跳转指令 CopyMemory ByVal TargetFunc, jmpCode(0), 5 ' 恢复内存页属性 VirtualProtect TargetFunc, 5, oldProtect, oldProtect InstallHook = True End Function这段代码的逻辑分三步:先用VirtualProtect把目标函数入口的5个字节改成可读写,然后用CopyMemory写入一条E9开头的相对跳转指令,最后恢复内存页保护属性。参数TargetFunc是你要Hook的API函数地址,HookFunc是你自己的替换函数地址。跳转偏移的计算方式是HookFunc - TargetFunc - 5,减5是因为E9指令本身占1字节,后面跟4字节偏移,总共5字节。
实际使用时,你需要在模块里用AddressOf拿到自定义函数的地址,然后传给InstallHook。比如要HookMessageBoxA,先声明一个同签名的函数MyMessageBox,再调用InstallHook AddressOf MessageBoxA, AddressOf MyMessageBox。这样所有对MessageBoxA的调用都会转到你的函数里。
提示:Hook之前一定要保存目标函数的前5个字节原始数据,卸载钩子时需要还原,否则程序退出时会崩溃。
3.2 内存直接读写与结构体操作
VB6没有指针类型,但通过CopyMemory和VarPtr可以绕过类型系统直接操作内存。资源里modMemory.bas封装了一组函数,用来读写任意地址的字节、整数和字符串。核心思路是把目标地址当作Long型传入,然后用CopyMemory在VB变量和该地址之间搬运数据。
' 从指定内存地址读取一个Long型整数 Public Function ReadLong(ByVal Address As Long) As Long Dim value As Long CopyMemory value, ByVal Address, 4 ReadLong = value End Function ' 向指定内存地址写入一个Long型整数 Public Sub WriteLong(ByVal Address As Long, ByVal value As Long) Dim oldProtect As Long VirtualProtect Address, 4, &H40, oldProtect CopyMemory ByVal Address, value, 4 VirtualProtect Address, 4, oldProtect, oldProtect End SubReadLong把目标地址的数据复制到局部变量value里,注意CopyMemory的第二个参数用了ByVal Address,意思是把Address的值当作指针传给API,而不是传变量的地址。WriteLong则反过来,先改内存页属性再写入。这两个函数是后面所有内存操作的基础,比如遍历进程内存、修改结构体字段、读取字符串指针,都是在这两个函数上套一层类型转换。
资源里还演示了如何用CopyMemory在VB的Type结构体和字节数组之间转换。比如定义一个RECT结构体,然后把它复制到Byte数组里,再通过网络发送出去。这种技巧在写底层通信协议或者和C++ DLL交互时特别有用。
3.3 类模块与COM接口的底层调用
VB6的类模块默认是私有类,不能直接暴露给COM。但通过PublicNotCreatable和MultiUse属性,可以控制类的实例化方式和线程模型。资源里Chap08_COM演示了如何手动实现IUnknown接口的三个方法:QueryInterface、AddRef、Release。
' 在类模块中手动实现IUnknown Implements IUnknown Private Sub IUnknown_QueryInterface(ByRef riid As UUID, _ ByRef ppvObj As Long) As Long ' 判断请求的接口ID是否匹配 If IsEqualUUID(riid, IID_MyInterface) Then ppvObj = ObjPtr(Me) IUnknown_AddRef IUnknown_QueryInterface = 0 ' S_OK Else ppvObj = 0 IUnknown_QueryInterface = &H80004002 ' E_NOINTERFACE End If End Sub这段代码的关键在于ObjPtr(Me),它返回当前类实例的IUnknown指针。IsEqualUUID是一个辅助函数,用来比较两个16字节的GUID是否相等。手动实现IUnknown的意义在于,你可以完全控制对象的生命周期和接口暴露方式,而不是依赖VB6自动生成的类型库。这在写插件系统或者需要动态分发接口的场景下非常有用。
资源里还包含了一个用TypeLib信息动态创建对象的例子,通过LoadTypeLib和CreateInstance绕过CreateObject的限制,直接根据GUID实例化COM组件。这种写法在需要延迟绑定或者跨进程调用时能派上用场。
4. 避坑与排查:VB6高级编程里最容易翻车的几个点
4.1 编译时报“未找到工程或库”
现象:打开工程时弹出对话框,提示某个引用缺失,工程无法编译。原因通常是系统里没有注册对应的OCX或DLL,或者注册的版本和工程要求的版本不一致。解决方法是先确认缺失的库名称,然后在系统目录下找到对应的文件,用regsvr32重新注册。如果找不到文件,去VB6安装盘的Common\Tools\VB\Controls目录下找,或者从另一台能正常编译的机器上复制过来。
4.2 Hook后程序崩溃或卡死
现象:调用InstallHook之后,目标函数一执行就崩溃,或者整个进程无响应。原因多半是跳转指令的偏移计算错了,或者目标函数的前5个字节不是完整的指令边界。解决方法是先用调试器查看目标函数的反汇编,确认前5个字节确实是完整的指令。另外,如果目标函数在Hook之前已经被其他钩子修改过,直接覆盖会导致指令错乱。常见做法是先用ReadProcessMemory读出原始字节,保存下来,卸载时还原。
4.3 内存写入触发DEP异常
现象:调用WriteLong或CopyMemory向代码段或只读数据段写入时,程序抛出“0xC0000005”访问冲突。原因是目标内存页没有写权限,而VirtualProtect调用失败或者权限参数不对。解决方法是检查VirtualProtect的第三个参数,&H40表示PAGE_EXECUTE_READWRITE,如果只需要读写可以用&H04(PAGE_READWRITE)。另外,某些系统开启了DEP后,即使改了页属性也无法执行代码段写入,这时需要把EXE加入DEP例外列表。
4.4 类模块的IUnknown实现导致引用计数错误
现象:手动实现IUnknown后,对象被提前释放或者内存泄漏。原因是AddRef和Release没有正确维护引用计数。VB6的类模块本身有隐式的引用计数,手动实现IUnknown时如果直接返回ObjPtr(Me)而不增加计数,外部调用Release时就会把对象销毁。解决办法是在QueryInterface返回指针之前调用一次AddRef,并在Release里判断计数是否归零,归零时再调用ObjPtr对应的释放逻辑。
4.5 编译出的EXE在别的机器上无法运行
现象:在自己机器上编译运行正常,拷贝到另一台机器上双击没反应或者报“部件丢失”。原因是EXE依赖的OCX和DLL没有随程序一起分发,或者目标机器上缺少VB6运行时。解决办法是用VB6自带的“打包和展开向导”生成安装包,把所有依赖项打进去。如果不想做安装包,至少要把MSVBVM60.DLL、MSCOMCTL.OCX、COMCTL32.OCX这几个文件复制到EXE同目录下,并在目标机器上注册。
5. 二次开发改造:从能跑到能用的几个实用技巧
5.1 把零散模块整合成可复用的工具库
资源里的代码是按章节拆分的,每个工程独立编译。如果你想把它们用到实际项目里,第一步是抽出一个公共模块,把CopyMemory、VirtualProtect、ReadLong、WriteLong这些基础函数集中到一个.bas文件里,然后在其他工程里引用。我一般会建一个VBAdvLib目录,里面放modCore.bas、modHook.bas、modMemory.bas三个文件,每个新工程直接添加这三个模块,省得每次重新声明API。
' modCore.bas - 公共基础模块 Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _ ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long) Public Declare Function VirtualProtect Lib "kernel32" ( _ ByVal lpAddress As Long, ByVal dwSize As Long, _ ByVal flNewProtect As Long, ByRef lpflOldProtect As Long) As Long Public Const PAGE_READWRITE As Long = &H04 Public Const PAGE_EXECUTE_READWRITE As Long = &H40这样整理之后,新工程只需要关注业务逻辑,底层操作直接调用公共模块。注意Declare语句要放在标准模块里,不能放在窗体或类模块里,否则作用域会受限。
5.2 用条件编译区分调试版和发布版
Hook和内存操作在调试阶段需要输出大量日志,但发布版里这些日志会影响性能。可以用VB6的#If...Then...#Else条件编译指令,把调试代码包起来。
#If DEBUG_MODE Then Debug.Print "Hook installed at 0x" & Hex(TargetFunc) Debug.Print "Offset = " & offset #End If在“工程→属性→生成”选项卡里,可以设置条件编译参数。调试时加上DEBUG_MODE = -1,发布时去掉,这样同一套代码不用改就能切换行为。
5.3 给Hook函数加异常保护
VB6没有try...catch,但可以用On Error Resume Next配合Err对象做基本的异常捕获。在Hook函数里,任何未处理的错误都会导致整个进程崩溃,所以每个Hook入口都应该包一层错误处理。
Public Function MyMessageBox(ByVal hWnd As Long, _ ByVal lpText As String, _ ByVal lpCaption As String, _ ByVal uType As Long) As Long On Error GoTo ErrHandler ' 自定义处理逻辑 Debug.Print "MessageBox called: " & lpText MyMessageBox = MessageBoxA(hWnd, lpText, lpCaption, uType) Exit Function ErrHandler: Debug.Print "Hook error: " & Err.Description MyMessageBox = 0 End Function这段代码里,On Error GoTo ErrHandler确保任何运行时错误都会被捕获,不会直接崩掉进程。ErrHandler里记录错误信息后返回一个默认值。注意MessageBoxA需要单独声明,不能和Hook函数同名。
5.4 用类型库编辑器扩展接口
如果你需要把自定义的类暴露给其他语言调用,VB6自带的类型库编辑器不够用,常见做法是用MIDL(Microsoft Interface Definition Language)写一个.idl文件,编译成.tlb后再在VB6里引用。这样生成的接口符合COM规范,可以被C++、C#、Delphi等语言直接调用。资源里Chap08_COM包含了一个简单的.idl示例,你可以照着改GUID和接口方法。
5.5 验证改造效果的方法
改完之后怎么确认没改坏?我的习惯是写一个最小验证工程,只包含一个窗体和两个按钮:一个调用原始API,一个调用Hook后的API,对比输出结果。比如HookGetTickCount,原始调用返回系统运行毫秒数,Hook后返回一个固定值。如果两个按钮的输出符合预期,说明Hook生效且没有破坏原有逻辑。这个验证工程我会一直保留,每次改完底层模块都跑一遍。
从那以后我每次动底层代码之前,都会先把这个最小验证工程跑一遍,确认基线是干净的,再开始改。希望帮到你。
本文还有配套的精品资源,点击获取