最近维护一个老项目的时候,我又把VB6翻了出来。说实话,VB6自带的编辑器在代码上色这件事上做得相当到位:关键字是蓝色、注释是绿色、字符串是红色,一眼扫过去结构清清楚楚。可一旦你不想只在这个IDE里看代码——比如要在自己的工具Form里展示一段VB6源码,或者做一个简易脚本编辑器,又或者想把着色逻辑封装成DLL给别的程序调用——IDE那套能力就完全指望不上了。自己动手做一个VB6代码上色方案,就成了绕不开的活儿。这篇文章是我完整实现一遍之后的总结,适合还在维护VB6代码的老兵,也适合准备做编辑器类工具的初中级开发者。
1. 为什么还在折腾VB6的人,迟早都会遇上“代码上色”这档子事
1.1 IDE的着色效果很香,但它“带不走”
VB6 IDE的代码上色,本质上是对源码做过词法分析之后,把关键字、字符串、注释这些Token用不同的颜色渲染到编辑窗口里。这个机制藏在IDE内部,并没有对外暴露任何可以复用的公共接口。很多人以为代码上色只是“编辑器的显示功能”,就跟字体加粗一样简单,实际上它依赖的是编辑器内部的语法解析结果。只要代码离开IDE,进入你自己的Form、第三方控件或者另一个程序,那套颜色就全部归零。
我在项目里遇到的情况很典型:内部工具平台需要展示一批老VB6模块的源码,不能截图,必须能复制、能查看。当时第一反应是“直接放个TextBox把文本填进去”,结果出来的是一片黑压压的文字,可读性极差。后来想调用IDE的着色能力,查了一圈发现根本没有公开API,只能自己重新实现词法扫描。也是从那一刻起我才真正意识到:VB6代码上色不是锦上添花,它直接决定一个代码展示工具的可用性。
还有一个容易被忽略的点:着色本身能当语法检查用。如果你的关键字表漏了某个关键词,或者字符串状态机写得不对,源码里会有一大段该红不红、该蓝不蓝的异常区域。这反而成了定位词法问题的线索。所以我后来调试着色引擎时,都是先盯着颜色找规律,比一行行读代码快得多。
1.2 真实场景:编辑器插件、代码展示组件、培训工具
什么样的人会需要自己写一套VB6代码上色?我归纳了一圈,主要是这几类:
- 给内部工具加“代码预览器”。比如数据字典工具里存了存储过程或VB模块,查看时不能只给纯文本。
- 做代码生成器。脚本生成一段VB6代码之后,希望输出预览也是带颜色的,方便用户确认生成结果。
- 做培训或教学系统。需要把示例代码嵌入课件、问答社区或者内部Wiki,直接贴图不方便,实时渲染最省事。
- 如果要进一步把着色逻辑编译成DLL,还能被Excel VBA、C#、甚至Python调用,做统一的“代码展示组件”。
这些场景的共同点是:代码不再只活在IDE里,而是要进入你自己的界面。只要走到这一步,代码上色就从可选项变成了必选项。
1.3 先明确功能清单
动手前别急,先把功能边界定下来。我给自己列过一个表,实际开发时就是按这个顺序逐项推进的。
| 功能 | 说明 | 后续章节 |
|---|---|---|
| 关键字上色 | Dim、If、Then等基础关键字染成蓝色 | 第2章 |
| 字符串识别 | 双引号包裹的内容染成红色 | 第2章 |
| 注释识别 | 单引号开头到行尾染成绿色 | 第2章 |
| 数字与标识符 | 数字可以单独配色,标识符保持默认色 | 第2章 |
| 实时刷新 | 输入时延迟刷新,不卡顿 | 第3章 |
| 增量扫描 | 大文件只重扫被修改的行,而不是全量text | 第4章 |
| DLL封装 | 把引擎发布成组件,供其他语言调用 | 第5章 |
| 运行库部署 | 在没有VB6环境的机器上跑起来 | 第5章 |
我见过不少项目在最开始追求“和IDE一模一样”,结果陷入无限细节。其实第一版只需要把关键字、字符串、注释这三类识别对,体验就能立起来,剩下的边界情况可以在增量扫描阶段再打磨。
2. 上色引擎的底层逻辑:Token分类与状态机
2.1 先把VB6的Token类型分清楚
实现代码上色,本质上就是做一次词法分析。所谓Token,就是源码里的最小语义单元。拿一行最普通的VB6代码举例:
Dim sName As String ' 用户名拆开来看,大概是这样的Token序列:
| 代码片段 | Token类型 | 默认颜色 |
|---|---|---|
| Dim | 关键字 | 蓝色 |
| sName | 标识符 | 黑色 |
| As | 关键字 | 蓝色 |
| String | 内置类型/关键字 | 蓝色 |
| ' 用户名 | 注释 | 绿色 |
再看一行带字符串和数字的:
MsgBox "共" & 10 & "条记录"MsgBox是关键字,"共"是字符串,10是数字,&是运算符。字符串要染红,数字可以单独给个深红色,运算符保持黑色即可。
这里最关键的设计决策是:先分词,再上色。如果上来就写一堆正则去匹配关键字,后面遇到字符串里的Dim、注释里的If,你怎么处理都不对。正确做法是把代码先切成一段一段的Token,每段带上类型和起止位置,最后统一把颜色刷到控件上。分词和渲染彻底分离,逻辑会清晰很多。
2.2 状态机:字符串、注释为什么不能靠正则硬来
为什么正则方案不行?因为VB6代码是上下文相关的。比如Dim这个单词,在普通代码里是关键字,可一旦它出现在字符串"Dim is a keyword"里面,就不能染成蓝色。再比如单引号',在代码里表示注释,但它在字符串里面就是普通字符。你写十条正则也处理不了这种交叉嵌套的关系。
所以标准做法是逐字符扫描,在扫描过程中维护一个“当前状态”:
- 普通状态(stNormal):遇到双引号进入字符串状态,遇到单引号进入注释状态,遇到字母或下划线就开始累积单词。
- 字符串状态(stString):要么停留在字符串内,要么遇到结束引号回到普通状态。特殊的是VB6里两个连续双引号表示一个转义引号,例如
"He said ""Hi""",中间的""是字符串内容,不是字符串结束。 - 注释状态(stComment):遇到行尾就结束,永远不跨行。
大概的状态流转如下,我用代码注释来描述:
普通状态 |-> 遇到 " -> 字符串状态(记录字符串起点) |-> 遇到 ' -> 注释状态(直到行尾) |-> 遇到字母/数字/下划线 -> 继续累积单词 |-> 遇到其他符号/空白 -> 将累积的单词作为一个Token输出 字符串状态 |-> 遇到 ""(两个连续引号)-> 仍在字符串状态,跳过两个字符 |-> 遇到 "(单个引号)-> 回到普通状态,输出字符串Token |-> 遇到行尾仍未闭合 -> 视为跨行字符串,下一行继续 注释状态 |-> 直接扫描到行尾,输出注释Token,回到普通状态什么叫跨行字符串?严格说VB6源码里一个字符串字面量不能直接跨行。但从着色工具的角度看,用户可能粘贴一段残缺代码,或者正在输入一行还没写完的字符串,你必须容忍这种状态。我在实现里就把未闭合字符串作为“跨行状态”保存下来,下一行继续按字符串处理。注释则相反,每行独立判定,遇到单引号后在本行末尾结束。这个决策直接影响后面增量扫描的复杂度,先想清楚再写代码。
2.3 关键字的边界判定:宁可慢,不可误标
状态机把字符串和注释的问题解决之后,剩下的难点是关键字边界。比如Dimx不能识别成关键字Dim,但Dim x、Dim(x)、Dim,x这些情况下Dim都是独立单词。判断规则很简单:一个单词前后都不能是字母、数字或下划线,它才算是独立标识符。VB6对大小写不敏感,所以dim、Dim、DIM都算关键字,统一用UCase$转大写再查表。
这里有个常见的坑:只判断两边是空格并不够。If (x) Then里面的If后面是空格,没问题;但Call foo(a,b)里面的foo后面是左括号,如果只按空白分词,会把Call和foo混在一个Token里。所以我把非标识符字符(运算符、括号、逗号、分号等)也作为词法边界处理,遇到它们就把前面累积的单词切出去,再决定当前符号要不要单独成Token。这样做虽然扫描时多几次判断,但换来的边界正确率非常值得。
3. 开发落地:用RichTextBox实现实时着色
3.1 建工程、引控件,先把界面跑起来
先新建一个标准EXE工程,在“部件”里勾选Microsoft Rich Textbox Control 6.0。VB6的RichTextBox和普通TextBox最大的区别就是支持对任意选区单独设置字体、字号、颜色,这正是实现代码上色的基础载体。
Form上的布局很简单:一个RichTextBox占满客户区,一个Timer用于延迟刷新,Timer的Interval设为300毫秒。然后我通常专门建一个模块放状态机、Token结构和关键字表,不放Form代码里。这样以后把引擎抽出来编译成DLL,不用动界面层。
字体建议选择等宽字体,比如 Courier New,字号10到12。等宽字体对代码展示非常重要,否则字符串和注释的对齐关系会非常乱。设置ScrollBars为Both,MultiLine为True,这些基础项不用多说。
3.2 核心Tokenize函数:完整实现
下面是整个着色引擎最核心的部分,我把Token结构和状态枚举先定义好,然后给出可运行的Tokenize函数骨架。注意这段代码里我先只做分词,不改任何控件,这是保持性能的第一步。
Private Type TokenInfo Start As Long ' Token在一行内的起始偏移(从0开始) Length As Long ' Token长度 Color As Long ' 被应用的颜色 End Type Private Enum TkState stNormal = 0 stString stComment End Enum Private m_nState As TkState ' 行间传递的状态 Private Sub PushToken(ByRef aToks() As TokenInfo, ByRef nCount As Long, _ ByVal lStart As Long, ByVal lLength As Long, ByVal lColor As Long) aToks(nCount).Start = lStart aToks(nCount).Length = lLength aToks(nCount).Color = lColor nCount = nCount + 1 End Sub Private Function IsIdentChar(ByVal ch As String) As Boolean IsIdentChar = (ch Like "[A-Za-z0-9_]") End Function Private Function Tokenize(ByVal sLine As String, ByRef aToks() As TokenInfo) As Long Dim nLen As Long Dim i As Long Dim ch As String Dim lTokStart As Long Dim nCount As Long Dim sWord As String nLen = Len(sLine) If nLen = 0 Then Tokenize = 0 Exit Function End If ReDim aToks(0 To nLen + 1) nCount = 0 lTokStart = 1 i = 1 Do While i <= nLen ch = Mid$(sLine, i, 1) Select Case m_nState Case stNormal: If ch = """" Then If i > lTokStart Then sWord = Mid$(sLine, lTokStart, i - lTokStart) PushToken aToks, nCount, lTokStart - 1, i - lTokStart, GetNormalColor(sWord) End If lTokStart = i m_nState = stString i = i + 1 ElseIf ch = "'" Then If i > lTokStart Then sWord = Mid$(sLine, lTokStart, i - lTokStart) PushToken aToks, nCount, lTokStart - 1, i - lTokStart, GetNormalColor(sWord) End If lTokStart = i m_nState = stComment i = i + 1 ElseIf ch = " " Or ch = vbTab Then If i > lTokStart Then sWord = Mid$(sLine, lTokStart, i - lTokStart) PushToken aToks, nCount, lTokStart - 1, i - lTokStart, GetNormalColor(sWord) End If lTokStart = i + 1 i = i + 1 ElseIf IsIdentChar(ch) Then ' 标识符的一部分,继续累积 i = i + 1 Else ' 运算符、括号等,作为边界切分 If i > lTokStart Then sWord = Mid$(sLine, lTokStart, i - lTokStart) PushToken aToks, nCount, lTokStart - 1, i - lTokStart, GetNormalColor(sWord) End If lTokStart = i + 1 i = i + 1 End If Case stString: If ch = """" Then If i < nLen Then If Mid$(sLine, i + 1, 1) = """" Then ' 两个连续引号是转义引号,继续留在字符串内 i = i + 2 Else PushToken aToks, nCount, lTokStart - 1, i - lTokStart + 1, vbRed m_nState = stNormal lTokStart = i + 1 i = i + 1 End If Else PushToken aToks, nCount, lTokStart - 1, i - lTokStart + 1, vbRed m_nState = stNormal lTokStart = i + 1 i = i + 1 End If Else i = i + 1 End If Case stComment: PushToken aToks, nCount, lTokStart - 1, nLen - lTokStart + 1, vbGreen m_nState = stNormal Exit Do End Select Loop If m_nState = stNormal Then If lTokStart <= nLen Then sWord = Mid$(sLine, lTokStart, nLen - lTokStart + 1) PushToken aToks, nCount, lTokStart - 1, nLen - lTokStart + 1, GetNormalColor(sWord) End If ElseIf m_nState = stString Then ' 行尾仍未闭合的字符串,继续以红色渲染 PushToken aToks, nCount, lTokStart - 1, nLen - lTokStart + 1, vbRed End If Tokenize = nCount End Function这段代码要表达的核心思想是:一次线性扫描,用m_nState记录跨行状态,把一串字符拆成一个个Token,全部放进数组里。关键字表相关逻辑放到GetNormalColor中处理。
Private Function GetNormalColor(ByVal sWord As String) As Long Dim s As String s = UCase$(sWord) Select Case s Case "DIM", "IF", "THEN", "ELSE", "ELSEIF", "END", "FOR", "NEXT", _ "DO", "LOOP", "WHILE", "WEND", "SUB", "FUNCTION", "PRIVATE", _ "PUBLIC", "OPTION", "SELECT", "CASE", "EXIT", "GOTO", "CALL", _ "CONST", "AS", "NEW", "AND", "OR", "NOT", "XOR", "MOD", "IS", _ "LIKE", "TYPE", "ENUM", "EVENT", "WITH", "BYREF", "BYVAL", _ "CLASS", "STATIC", "GOSUB", "RESUME", "ERROR", "ON" GetNormalColor = vbBlue Case "TRUE", "FALSE", "NOTHING", "NULL", "EMPTY" GetNormalColor = vbMagenta Case Else If IsNumeric(sWord) Then GetNormalColor = vbMagenta Else GetNormalColor = vbBlack End If End Select End Function有个细节需要单独说明:VB里Rem开头的行也是注释。Rem作为关键字染色并不对,因为它后面整行内容都是注释。我的做法是在调用Tokenize之前先做一次行级预处理:去掉行首空白后,如果前三个字符是Rem且后面紧跟空白或行尾,就把整行作为注释Token,染绿后跳过本行。这里为了代码简洁没有把预处理放到Tokenize里,实际工程里单独加一个判断函数即可。
3.3 ApplyColor:着色顺序与光标保护
Tokenize只是计算,真正改变界面的是ApplyColor这一层。这里有个顺序坑,我当年第一次就踩中了:必须先设置SelStart和SelLength,最后再设置SelColor。如果顺序反了,RichTextBox会对你当前光标所在的那一小段字符设置颜色,结果整个文档颜色乱七八糟。
Private Sub ApplyLineColor(ByVal Rtf As RichTextBox, ByVal sLine As String, ByVal lLineStart As Long) Dim aToks() As TokenInfo Dim nCount As Long Dim i As Long nCount = Tokenize(sLine, aToks) For i = 0 To nCount - 1 With Rtf .SelStart = lLineStart + aToks(i).Start .SelLength = aToks(i).Length .SelColor = aToks(i).Color End With Next i End Sub全量刷新时,先把整个文本选中并统一设为黑色,然后再按Token一段一段改颜色。这个“先默认后覆盖”的思路很重要,它可以避免漏Token导致某段文字残留上一次的颜色。着色前记录当前光标位置SelStart,着色后恢复回来,否则用户正在打字的地方会被强制跳到文档末尾。
Private Sub RefreshColorAll() Dim sAll As String Dim aLines() As String Dim i As Long Dim lCur As Long Dim nSave As Long sAll = RichTextBox1.Text If Len(sAll) = 0 Then Exit Sub aLines = Split(sAll, vbCrLf) nSave = RichTextBox1.SelStart With RichTextBox1 .SelStart = 0 .SelLength = Len(sAll) .SelColor = vbBlack End With lCur = 0 For i = 0 To UBound(aLines) ApplyLineColor RichTextBox1, aLines(i), lCur lCur = lCur + Len(aLines(i)) + 2 Next i With RichTextBox1 .SelStart = nSave .SelLength = 0 End With End Sub用Split(sAll, vbCrLf)切行的时候要注意:如果文件里只有LF没有CR,或者只有CR没有LF,Split结果会不对。实际职场环境中很难保证用户粘贴进来的文本换行符统一,所以我后来改成先统一替换vbCr和vbLf为vbCrLf,再Split。这个细节放在踩坑部分细说。
3.4 用什么事件触发着色最稳
有些人习惯在KeyPress事件里直接调RefreshColorAll,我实测下来,500行以内的代码还能忍,1000行以上几乎无法使用。每敲一个字符就全量扫描一遍,还没算上RichTextBox自身的重绘开销,肉眼可见的卡顿。
正确做法是延迟刷新。Change事件只负责把脏标志置True,同时重置Timer:
Private Sub RichTextBox1_Change() m_bDirty = True Timer1.Enabled = False Timer1.Enabled = True End Sub Private Sub Timer1_Timer() Timer1.Enabled = False If m_bDirty Then m_bDirty = False RefreshColorAll End If End Sub这样连续打字时,Timer会被反复重置,只有停止输入200~300毫秒后才真正执行一次刷新。这个方案实现简单,效果却非常明显。再加上后面要讲的锁重绘和增量扫描,整个着色体验可以做到和IDE接近。
4. 性能优化实测:大文件着色不卡顿的调整思路
4.1 先定量测一测
很多人觉得“代码上色不就是循环设置颜色吗”,真做了才知道瓶颈在哪里。我拿一个1000行左右的VB6模块做测试,里面大概有800到1200个Token,不同优化手段的耗时对比如下:
| 优化项 | 典型耗时 | 说明 |
|---|---|---|
| 全量扫描并生成Token | 8~15ms | 纯计算,非常快 |
| 全量设置SelColor(不锁重绘) | 400~900ms | 明显卡顿 |
| 设置颜色前锁定WM_SETREDRAW | 150~350ms | 减轻闪烁,仍有延迟 |
| 增量只处理改动相关的若干行 | 5~50ms | 日常输入几乎无感 |
不同CPU和RichTextBox版本会有差异,但量级是稳定的:扫描不是瓶颈,RichTextBox的SelColor设置才是瓶颈。每次设置颜色都会触发控件的重绘计算和消息通知,几百上千次连续设置,时间都耗在这里了。
4.2 增量扫描:改哪行就重扫哪行
既然瓶颈在设置颜色,那减少设置次数就是最直接的优化方向。增量扫描的思路是:维护每一行扫描结束后的状态m_aState(n),以及每一行在全文中的起始偏移m_aLineStart(n)。当用户改动第n行时,从第n行开始向后重扫,每重扫一行就更新该行的着色。
什么时候能停下来?当新扫描出来的第n+1行状态和旧的m_aState(n+1)完全一致时,说明本行之后不会受到改动影响,可以直接收工。跨行字符串的情况下,这个判断尤其重要:如果改的是字符串起始行,状态会一直保持在stString,直到闭合引号所在行状态恢复为stNormal,并且旧状态同样是stNormal,才能停止。否则后面所有行都处于字符串内,颜色都是同一红色,改前改后没有区别,中断扫描也没问题。
' 伪代码:增量刷新核心流程 Sub IncrementalRefresh(ByVal nChangedRow As Long) Dim i As Long Dim newState As Integer i = nChangedRow Do While i <= m_nRowCount - 1 newState = ScanAndColorRow(i) If newState = stNormal And m_aState(i) = stNormal Then Exit Do End If m_aState(i) = newState i = i + 1 If i - nChangedRow > 200 Then RefreshColorAll Exit Sub End If Loop End Sub这个方案有个安全阀:如果跨行状态持续超过200行,直接退化为全量刷新。正常代码里不会出现几百行字符串不闭合的情况,这个上限足够保守。实际项目里我更倾向于这个方案的简化版,就是每当一行变化时,从该行所在位置往上找连续状态为stNormal的最近一行作为起点,然后从起点开始全量刷新。虽然稍微多刷几行,但代码逻辑更简单,维护成本低。
4.3 锁定重绘和定时去抖的配合
增量扫描能解决“重算哪些行”,却还没解决“设置颜色时界面疯狂闪烁”的问题。RichTextBox在连续SelColor操作时,每次都会触发部分重绘,画面会像波浪一样闪。此时可以用WM_SETREDRAW消息把控件的重绘临时关掉。
Private Const WM_SETREDRAW As Long = &HB Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" _ (ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, _ ByVal lParam As Long) As Long ' 着色前锁住重绘 Call SendMessage(RichTextBox1.hwnd, WM_SETREDRAW, 0, 0) RefreshColorAll ' 着色后恢复重绘 Call SendMessage(RichTextBox1.hwnd, WM_SETREDRAW, 1, 0) RichTextBox1.Refresh用这个API有两点必须注意。第一,锁定期间控件不刷新,所以你绝对不能在里面做耗时的数据库查询或递归操作,否则用户界面会长时间“僵死”。第二,恢复重绘后必须调用一次Refresh强制整个窗口重画,否则可能留下大片空白区域。我见过有人忘了调用Refresh,控件一直白屏,还以为是程序崩溃。
这套组合拳打下来,我的实测效果是:1000行代码里连续输入,肉眼几乎感觉不到着色延迟;如果只在末尾添加新行,那更是毫无压力。
5. 踩坑记录与部署经验:从字符串转义到VB6运行库
5.1 字符串、注释和Rem别乱认
先说说字符串转义。VB6不像C语言那样用反斜杠表示转义字符,它在字符串里写一个英文双引号,是用两个双引号来表示的。比如:
s = "He said ""Hi"""这段代码里,"He said是字符串内容,""是转义出的一个双引号,最后的"Hi和结尾的"是字符串实际内容,整个字符串的值是He said "Hi"。如果状态机看到第二个引号就以为字符串结束了,那后面的Hi会被当成普通标识符染黑,整段颜色全错。所以我在字符串状态里专门判断:遇到引号后如果下一个字符还是引号,就跳过两个字符,继续保持字符串状态。
注释的坑也很典型。单引号注释只在字符串外生效,所以必须先进入字符串状态之后才能判定单引号。还有Rem,它在行首时表示注释,但如果出现在表达式中间,比如x = 1 Rem Comment,这种写法虽然罕见但合法。要想完全兼容,一般建议只处理行首的Rem,其他位置按普通关键字对待。实际老代码里Rem几乎都出现在行首,所以这个策略在项目里跑得很稳。
5.2 SelColor的选区跳变是最隐蔽的坑
RichTextBox的SelColor属性有个特性:它设置的是从当前SelStart到SelEnd这段字符的颜色。如果你不先设置SelLength,它会一直作用到光标位置右侧所有文本。第一次写ApplyLineColor时,我没有按住SelStart -> SelLength -> SelColor这个顺序,结果每次着色都会把从某个Token开始到文档结尾的所有文字全部染成同一颜色,排查了很久才发现是选区没设对。
正确的写法就是第3章代码里展示的顺序:
- 先设
SelStart,定位到Token起点。 - 再设
SelLength,圈住Token长度。 - 最后设
SelColor,应用颜色。
循环里每次都要重复这三步,顺序不能乱。还有,设置颜色本身会改变当前光标位置,如果你不管不顾,用户输入时光标会被拽到文档末尾。所以全量刷新前记住原有SelStart,刷新后恢复,同时把SelLength设为0,避免残留一个选区影响后续输入。
5.3 标准DLL的坎:ActiveX才是VB6的舒适区
很多人搜索“vb6生成标准dll”,是希望编译一个不需要注册COM的DLL,用LoadLibrary和GetProcAddress直接调用。这里要先说清楚一个现实:VB6本身不能直接生成标准DLL。VB6默认能编译的DLL类型是ActiveX DLL,也就是COM组件。想生成标准DLL,官方没有直接选项。
我在项目中实际采用的是ActiveX DLL方案,原因很简单:目标环境允许注册组件。把着色引擎写进一个类模块,公开一个HighlightText方法,编译成ActiveX DLL后,用regsvr32注册,Excel VBA、C#、Python都能通过COM调用。接口也很简单:
' 类模块:Formatter Public Function HighlightText(ByVal sCode As String) As String ' 调用着色引擎,返回带颜色标记的格式化结果 End Function调用方只需要CreateObject("MyEngine.Formatter"),完全不需要依赖VB6环境。如果调用方确实强烈要求标准DLL,我试过两种变通方案:一是用C++写一个薄封装,对VB6生成的COM DLL做转发导出;二是用第三方工具给VB6产物打导出表补丁。后者依赖工具版本,稳定性一般,我不建议在生产环境用。如果你的产品允许注册组件,老老实实用ActiveX DLL,省下来的时间足够你把增量扫描做得更好。
还有一个容易被忽略的细节:64位系统上注册32位COM组件,要使用32位版本的regsvr32,路径一般在C:\Windows\SysWOW64\regsvr32.exe。用管理员权限运行是前提,这一点在部署文档里一定要写清楚。
5.4 目标机器没装VB6,运行库怎么随产品分发
VB6程序在用户机器上能跑起来,依赖的是VB6运行库,核心就是MSVBVM60.DLL。如果程序里还用了RichTextBox这样的第三方控件,那Richtx32.ocx也得一起分发并注册。
常见的部署做法有这几种:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 使用VB6运行库安装包(典型文件名VBRun60sp6.exe) | 一次性装齐运行库,省心 | 需要用户执行安装步骤 |
| 用Inno Setup等打包工具分发 | 安装过程一体化,用户体验好 | 需要维护打包脚本 |
| 绿色版把MSVBVM60.DLL放exe同目录 | 免安装,便携 | 兼容性不确定,正式产品不建议 |
我自己偏向用Inno Setup做一个静默安装包:把MSVBVM60.DLL放到系统目录,把Richtx32.ocx也放好并调用regsvr32注册。注意64位系统下,32位组件目录是SysWOW64,不是System32。如果放错了,注册时会提示模块找不到。调试部署问题最省力的方法是准备一台干净的虚拟机,不装VB6,直接跑安装包验证。我踩过的坑里,十次有八次都是OCX依赖没处理好,而不是VB6运行库本身的问题。
最后再分享一个我实际操练中的体会:写VB6代码上色,看起来是给文字换个颜色的小功能,真正做起来会发现它牵涉词法状态机、控件消息机制、性能优化和部署分发一整条线。先把状态机写对,后面所有优化才有地基;如果一开始就用正则硬凑,后续每个坑都要加倍还债。增量刷新和锁重绘这类优化,可以等基础着色稳定之后再慢慢加。这套东西做完之后,不只是给VB6代码用,稍微改改关键字表和状态规则,同样可以给VBA、SQL甚至简单的INI配置文本做上色,扩展起来很方便。如果你也在维护老系统,值得花一个周末把这条路走通。