简介:VBto Converter V2.89 是一款面向需要迁移、维护遗留 VB6 项目的开发者整理的源代码转换工具包,重点解决 Microsoft Visual Basic 6.0 完整工程向 MS VC++ MFC、VC++.NET(CLR)、VB.NET、C#、J#、Borland C++ Builder 与 Delphi 等平台转换的问题,并支持 VS 2010、Delphi 2010、FileSystemObject 及 VB.Data 等相关特性。压缩包共 2000 个文件,约 21.57MB,内含 h、cpp、cs、vb、pas 等源代码,resx、dfm、frm、bas、vbp、vbproj、csproj、vcproj 等项目与界面资源,以及 gif、bmp、ico、jpg 等配套素材和 txt、html、chm 说明文档,目录结构便于对照转换前后的工程组成。已有 326 人学习下载。读者可获得跨语言项目文件、转换示例与资源素材,用于比较 VB6 到 C#、VB.NET、C++ 等目标的代码组织,辅助旧系统升级、代码审查和迁移方案验证。
1. 手上那套 VB6 老系统,值不值得走 VBtoConverter 这条路
机房角落里还有一台跑着 XP 的虚拟机,里面躺着一套四十多个窗体、六十多个模块的 VB6 业务系统,天天有人用,但开发机已经装不上 IDE 了。这类存量资产最后绕不开 VBtoConverter 代码转换:它把.vbp工程连窗体带模块整体读进来,按目标语言吐出工程骨架,窗体布局、控件属性、事件绑定一并搬过去,省掉的正是最枯燥的那部分体力活。它适合手里有存量 VB6/VB.NET 代码、工期又压在头上的维护团队;不适合指望点一下按钮就交付的人——转换只把七成机械工作做掉,剩下三成语义修补才是真正花钱的地方。下面按我自己跑过几轮的顺序,把能抄的部分和会翻车的地方都摊开。
2. 先摸清能力边界:转换引擎做了什么、没做什么
动手之前必须搞清楚它在哪一层工作。很多人第一次用,把它当成“编译器逆向”,结果拿到一堆语法正确但跑不起来的代码,然后骂工具不行。实际上它更接近一个高度特化的源码重写器:输入是 VB6 的文本工程文件,输出是另一种语言的文本工程文件,中间不经过任何运行期。理解这一点,后面所有的期待和验收标准才立得住。
2.1 三个阶段:工程解析、中间模型、代码生成
VB6 的.vbp是纯文本清单,Form=、Module=、Class=、Reference=一行行列着工程组成。.frm更特殊,前半段是窗体属性块(VERSION 5.00开头,Begin VB.Form Form1嵌套Begin VB.CommandButton Command1),后半段才是真正的 VB 代码。工具第一步就是把这套“属性块 + 代码块”的混合文本拆开,属性块进资源模型,代码块进语法模型。
中间模型是它的黑匣子。窗体上的每个控件变成一条带坐标、尺寸、字体、颜色、事件列表的记录;代码里的每个Sub/Function变成带参数类型、返回值、错误处理的节点。到了生成阶段,目标语言的语法树按模板拼出来,同时把资源模型翻译成目标框架认识的写法——C# 就是InitializeComponent()里那一串this.button1.Location = new Point(...)。
这里有个关键判断:凡是能被“结构化描述”的东西,它转得都不错;凡是依赖运行期语义的东西,它只能猜。Dim x As Variant这种在 VB6 里靠运行期决定类型的东西,转成 C# 只能落到object,后面所有的隐式转换、默认属性访问全得你自己补。所以评估一个工程能不能转,先数 Variant、Object、Collection、默认属性(Text1.Text = "x"这种不带属性名的写法)出现了多少次,这些数字基本等于你的返工量。
2.2 支持矩阵:哪类工程能直接过,哪类只能手工啃
不同目标语言的成熟度差得很远。C# 是主战场,窗体和代码都处理得比较完整;Python、Java 这类没有原生 Windows 窗体模型的,窗体部分基本只剩坐标数据,得靠目标语言的 GUI 库自己重建。把这张表贴在墙上,比看任何宣传材料都管用。
| 源工程类型 | 目标语言 | 实际可用度 | 主要缺口 |
|---|---|---|---|
| VB6 标准 EXE | C# / WinForms | 高 | Variant、默认属性、DAO/ADO 数据绑定需手工重写 |
| VB6 标准 EXE | VB.NET | 高 | 与 C# 相近,On Error语义差异更明显 |
| VB6 ActiveX DLL | C# 类库 | 中高 | COM 注册、Class_Initialize生命周期需确认 |
| VB6 标准 EXE | C++(MFC) | 中 | 窗体转成资源脚本,事件映射要手工接 |
| VB6 标准 EXE | Delphi / Pascal | 中 | 控件映射表覆盖不全,第三方控件基本无解 |
| VB6 标准 EXE | Python / Java | 低 | 只保留结构与算法,界面等于重写 |
| VB.NET 工程 | C# | 高 | 语法层面几乎一一对应,项目引用需重配 |
表格里“中”“低”那几行不是说不能用,而是说要把预期调成“拿它的输出当草稿”。我一般会先拿一个中等复杂度的窗体做试点转换,编一遍,数编译错误和运行时异常的数量,再决定整包怎么推。试点窗体最好挑那种控件种类齐全、但业务逻辑不深的——比如一个查询条件 + 列表 + 导出按钮的界面,它能一次性暴露坐标、字体、数据绑定、文件对话框四类问题。
2.3 三条迁移路线的取舍:别把转换当成唯一答案
同样一套 VB6 系统,业内常见做法有三条。第一条是纯手工重写,慢但结果最干净,适合代码量在几千行之内的模块;第二条是半自动转换,用它出骨架再人工补齐,适合几万行、窗体多的系统;第三条是运行期兼容——保留原程序,用 COM 互操作或进程间调用的方式让新系统去访问旧逻辑,适合那些短期内根本动不了的核心算法。
第二条路线之所以是多数人的选择,原因很实在:窗体布局是纯体力活,一个四十个窗体的系统,手工摆控件、对坐标、设字体,两个人干两周都未必对得齐;而这件事恰恰是工具最擅长的。反过来,业务逻辑里的边界条件、错误处理、特殊数据格式,工具一概不理解,只能人来。把这个分工想明白,就不会在“为什么转换出来的代码还要改这么多”上纠结。
提示:转换前先给原工程打一个能跑起来的基线版本,把每个窗体的关键操作路径录一遍屏。后面验证转换结果时,这份录屏比任何文档都好使。
3. 走一遍完整转换:从.vbp到能编译的 C# 工程
这一章是可抄的部分。整个流程我固定成四步:体检、冻结、转换、首编译。顺序不能乱,尤其是“冻结”这一步,跳过的人最后都在对比两份不一样的源码。
3.1 转换前的工程体检:先把暗礁标出来
体检不是看代码写得好不好,而是找那些必然导致转换失败或语义漂移的东西。我固定查五项:引用列表里的第三方 COM 组件、Declare语句声明的 Win32 API、Variant与Object的使用密度、On Error的分布、以及窗体上用的 ActiveX 控件清单。
前两项决定“能不能编译”,后三项决定“编译完能不能跑”。查引用最直接的办法是打开.vbp,把所有Reference=和Object=行抄出来,逐条确认对应的组件在新机器上是否注册、是否有 .NET 替代品。Declare Function那批 API 声明,工具通常会原样搬到 C# 里变成DllImport,但参数类型和字符集经常对不上,得单独列个清单盯着。
# 扫描 .vbp 里的外部依赖与高风险语法,输出一份体检报告 $vbp = "D:\legacy\OrderSys\OrderSys.vbp" $lines = Get-Content -Path $vbp -Encoding Default # VB6 工程文件多为 ANSI/GBK Write-Host "=== 外部引用 ===" $lines | Where-Object { $_ -match '^(Reference|Object)=' } | ForEach-Object { $_ } Write-Host "=== 高风险语法统计 ===" $srcFiles = Get-ChildItem "D:\legacy\OrderSys" -Include *.bas,*.cls,*.frm -Recurse $patterns = @{ 'Variant' = 'As\s+Variant' 'Object' = 'As\s+Object' 'OnError' = 'On\s+Error' 'Declare' = 'Declare\s+(Function|Sub)' 'DefaultProp' = '^\s*\w+\s*=\s*\w+\.\w+\s*$' } foreach ($k in $patterns.Keys) { $hit = ($srcFiles | Select-String -Pattern $patterns[$k] -AllMatches).Count Write-Host ("{0,-12} {1}" -f $k, $hit) }这段脚本的价值在于把“感觉这个工程挺复杂”变成具体数字。Get-Content必须带-Encoding Default,因为 VB6 工程文件在中文环境下是 GBK 编码,用默认 UTF-8 读会直接把中文注释和字符串读成乱码,后面的统计也跟着失真。
统计结果里,Variant出现超过两百次、On Error遍布每个函数、Declare有十几个,那这个工程的转换工作量基本等于重写业务层,只是界面能省下来。这时候要么调整工期,要么先把这几类代码手工收敛一轮再转——收敛过的代码转出来质量完全不同。
3.2 冻结基线:让转换结果可比对
冻结的意思是:转换期间原工程一个字都不能改,同时把编译产物、依赖组件、数据库脚本全部打包留档。我吃过一次亏——转换到一半,业务方提了个小需求,同事直接在 VB6 里改了三个窗体,等转换结果出来做对比时,怎么都对不上,白折腾两天。
具体做法是给整个源码目录做一次快照,压缩包命名带日期,同时在测试机上装好原程序,把核心流程跑通并录屏。录屏要覆盖登录、查询、新增、修改、导出、打印这几类操作,每个操作记录输入数据和预期结果。这份材料后面直接变成回归清单。
依赖组件也得一起冻结。VB6 程序常见的依赖是msvbvm60.dll、COMDLG32.OCX、MSFLXGRD.OCX这类,转换后的 C# 工程如果还走 COM 互操作,这些组件必须在新机器上注册;如果不走,就得提前确认替代控件的行为差异。把依赖清单写进一个文本文件放在工程根目录,比记在脑子里靠谱。
3.3 转换参数怎么选:项目级设置的四个关键项
工具的项目级转换界面通常有几组选项,我关注四个。第一是目标语言与框架版本,选 C# + .NET Framework 4.8 还是 .NET 6/8,取决于目标机器装了什么——老企业环境里 4.8 的兼容性最稳,新项目才考虑 .NET 8。第二是窗体转换模式,是生成Designer.cs式的分部类,还是把布局写在一个大文件里,前者可维护性好得多,后续用设计器打开也方便。
第三是命名空间与项目名映射。默认往往按源工程名生成,如果原工程名带空格或中文,生成出来的命名空间会很难看甚至非法,得手工改。第四是字符编码与注释保留,这一项直接决定转换后代码里还有没有中文注释——很多版本默认丢弃注释,勾上保留能救回大量业务说明。
设置完先别急着整包跑。拿一个窗体、一个模块做试点,把输出丢进新工程编译,看错误类型集中在哪。如果错误全是“找不到类型”,那是引用没配好;如果全是“无法将 object 隐式转换为 string”,那是 Variant 的锅,得提前规划改造策略。
3.4 产物结构与首次编译:先把错误分类再动手
一次完整转换的输出通常是这样:一个.csproj、每个窗体一对Form1.cs/Form1.Designer.cs、每个模块一个静态类、外加Resources目录和若干.resx。首次编译的错误数量会很吓人,但类型高度集中,分类处理效率最高。
我的分类顺序是:先修“找不到类型/命名空间”,再修“类型转换”,最后修“成员不存在”。第一类通常是缺引用或少using,加完就消失一大批;第二类集中在object到具体类型的转换,是 Variant 遗留;第三类多是 VB6 内置函数(Mid、InStr、Format、IsNull)在 C# 里没有直接对应,需要换成Microsoft.VisualBasic命名空间下的同名方法或者自己写扩展。
// 转换产物里最常见的三类修补,按这个顺序改能少走弯路 using Microsoft.VisualBasic; // 第一类:VB6 内置函数在这里有等价实现 using System.Globalization; public partial class FrmOrder : Form { public FrmOrder() { InitializeComponent(); } // 第二类:Variant 转成了 object,调用点必须显式收敛类型 private void btnQuery_Click(object sender, EventArgs e) { object raw = Session["DeptId"]; // 原 VB6: Dim v As Variant string deptId = raw == null ? "" : raw.ToString(); // 第三类:VB6 的 Format/IsNull/Mid 换成语义明确的写法 string code = Strings.Mid(txtCode.Text.Trim(), 1, 6); string amount = Strings.Format(1234.5, "0.00"); // 注意区域设置 bool empty = Information.IsDBNull(deptId); LoadOrders(deptId, code, amount, empty); } private void LoadOrders(string deptId, string code, string amount, bool empty) { // 数据访问层此处略,重点是类型已经在入口处收敛完毕 } }Strings.Mid和Strings.Format是Microsoft.VisualBasic提供的兼容层,用来承接 VB6 的行为,比自己手写Substring更贴近原语义——尤其是Format对区域设置敏感,直接换成ToString("0.00")在部分环境里小数点会变逗号。Information.IsDBNull对应 VB6 的IsNull,别用== null代替,两者对DBNull.Value的判断结果不一样。
编译过之后不要急着庆祝。编译通过只说明语法对了,行为对不对是另一回事。我习惯把首次编译通过当成中点而不是终点,后面至少还要走一轮界面比对和一轮业务流程回归。
4. 窗体和控件:布局为什么会歪、事件为什么会断
窗体是这套工具最值钱的部分,也是最容易出“看着像但用着不对”的地方。四十个窗体手工摆要两周,工具半小时出结果,代价是每个窗体都得过一遍眼睛。这一章讲清楚偏差从哪来、怎么批量修。
4.1 坐标与字体:Twip、DPI 和 AutoScaleMode 三重叠加
VB6 窗体的默认ScaleMode是 Twip,1 英寸等于 1440 Twip。在 96 DPI 下,1 像素约等于 15 Twip。转换工具要把 Twip 换算成 .NET 的像素,这个换算本身没问题,问题出在字体上:VB6 窗体通常用宋体 9pt,转成 WinForms 后如果窗体的AutoScaleMode设成Font,运行时会按当前系统字体再做一次缩放,等于转了两次,控件就会整体偏大、互相重叠。
解决方式是把窗体的AutoScaleMode统一改成None,坐标完全按转换出来的像素值走,字体单独设。批量改可以用脚本扫一遍所有Designer.cs:
# 把所有窗体的 AutoScaleMode 统一设为 None,避免字体二次缩放 $dir = "D:\new\OrderSys\Forms" Get-ChildItem $dir -Filter *.Designer.cs -Recurse | ForEach-Object { $text = Get-Content $_.FullName -Raw -Encoding UTF8 # 只替换 InitializeComponent 里的自动缩放设置 $new = $text -replace 'this\.AutoScaleMode\s*=\s*System\.Windows\.Forms\.AutoScaleMode\.\w+;', 'this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;' if ($new -ne $text) { Set-Content -Path $_.FullName -Value $new -Encoding UTF8 Write-Host "已处理: $($_.Name)" } }这段脚本只改AutoScaleMode一处,不动其他属性,风险可控。改完要在 100% 和 125% 两种缩放下各跑一次,看控件是否还在原位。有些窗体在高 DPI 下仍然错位,那是因为原 VB6 窗体本身就是按固定像素位置硬摆的,没有布局容器,这种只能手工换成TableLayoutPanel或者接受在高 DPI 下不完美。
字体的处理要单独说一句。VB6 里控件字体常常是“宋体 9pt”,转到 .NET 后如果系统没装宋体,会回退到默认字体,控件宽度就不够了,按钮上的文字会被截断。稳妥做法是统一指定一个必定存在的字体族,并给按钮留出余量。
4.2 控件映射与事件签名:名字对上了,参数未必对上
常用控件的映射关系是固定的:CommandButton→Button,TextBox→TextBox,Label→Label,Frame→GroupBox,MSFlexGrid→DataGridView,CommonDialog→OpenFileDialog/SaveFileDialog,Timer→System.Windows.Forms.Timer,Menu→MenuStrip。名字层面的对应基本不用操心,麻烦在事件签名和属性语义。
VB6 的事件处理子程序是Private Sub Command1_Click(),没有参数。转成 C# 变成private void button1_Click(object sender, EventArgs e),这没问题。但有些事件在 VB6 里带参数,比如MSFlexGrid的Click会带行号,转过来之后DataGridView的事件模型完全不同,行号要从CurrentCell里取,这种就得逐个窗体核对。
属性语义的坑更隐蔽。VB6 的TextBox.MaxLength = 0表示不限制,C# 里MaxLength = 0表示一个字符都不让输——这是真事,转换工具通常会处理这个特例,但不能全信。TextBox.PasswordChar在 VB6 里是字符,在 .NET 里是char,Enabled和Visible的继承行为也略有差异。我的做法是转换完抽三个典型窗体,把每个控件的属性在属性面板里对一遍,重点看MaxLength、PasswordChar、TabIndex、Anchor。
TabIndex值得单独提。VB6 的 Tab 顺序是按TabIndex属性排的,转换工具一般会保留数值,但控件在Controls集合里的添加顺序会影响实际表现。如果用户抱怨“按 Tab 键跳的顺序不对”,去Designer.cs里看this.Controls.Add的调用顺序,那个顺序才是最终生效的。
4.3 资源文件:图片、图标和字符串的迁移
VB6 的窗体资源通常以两种方式存在:.frx二进制附属文件(跟.frm同名),或者工程里的.res资源文件。图片、图标、以及某些控件的二进制属性都存在这里。转换工具会把这些内容抽出来放进Resources目录,并在Designer.cs里生成resources.GetObject("pictureBox1.Image")之类的代码。
出问题最多的是尺寸和格式。VB6 里的.bmp图标转到 .NET 后如果直接当Icon用,会因为尺寸不是标准值(16/32/48)而显示异常;.cur光标文件有时不被识别。稳妥做法是把所有图片资源统一转成 PNG,图标用工具重新生成一套标准尺寸的.ico,然后手工替换resx里的引用。
字符串资源是另一块。VB6 项目里常见LoadResString(101)这种按 ID 取字符串的写法,转换后如果资源没跟着走,运行时会抛异常或者显示空。建议在转换前把所有LoadResString的调用点列出来,转完后逐个确认对应的资源项是否存在于.resx里。这一步没有捷径,只能对清单。
4.4 第三方 ActiveX 控件:三条出路,选哪条看依赖深度
MSFlexGrid、MSHFlexGrid、ListView、TreeView、RichTextBox这些还算好,.NET 有对应物。真正麻烦的是那些小众的 ActiveX 控件——报表控件、图表控件、条码打印控件。三条出路:
第一条是找 .NET 替代品。报表类可以用开源报表引擎重做,图表类用System.Windows.Forms.DataVisualization.Charting,条码类找成熟的 .NET 库。这条路的结果最干净,代价是每个控件都要重写绑定逻辑。
第二条是走 COM 互操作,直接在 C# 工程里引用原 OCX。做法是在项目里添加 COM 引用,让 VS 生成互操作程序集,然后把控件当普通 .NET 控件用。这条路能最快跑起来,但部署时必须保证目标机器注册了同一个 OCX,而且位数要对上——32 位 OCX 只能被 32 位进程加载,工程得强制x86。
第三条是控件隔离,把用到该控件的窗体单独做成一个进程,主程序通过窗口嵌入或者进程间调用的方式集成。这条最重,只在那个控件实在无法替代、业务又完全离不开的时候用。
我的选择顺序是:能替换就替换,替换不了先走互操作,互操作再出问题才考虑隔离。互操作方案要在项目文件里显式固定平台目标,否则在 64 位机器上会报“类未注册”这种看着像环境问题的错误。
5. 避坑与排查:五条踩过的血泪记录
这一章全是翻车现场。每一条都是实际遇到过、并且当时排查了很久的,按“现象 → 原因 → 解决”写,遇到类似症状可以直接对号入座。
5.1 中文全变问号:编码没对上
现象:转换出来的 C# 代码里,所有中文字符串常量、注释全变成????或者一串乱码,但英文部分完全正常。
原因:VB6 的.frm、.bas文件在中文环境下是 ANSI(GBK)编码,文件头没有 BOM。转换工具如果在读取时按 UTF-8 解析,中文字节序列就不合法,落到输出里就成了替换字符。更隐蔽的情况是工具内部读对了,但写文件时按单字节编码写,同样会丢。
解决:转换前把所有源文件批量转成带 BOM 的 UTF-8,工具读取时就不会歧义;或者反过来,明确在工具设置里指定源文件代码页为 936。批量转码用 PowerShell 一行搞定,转完先抽查几个文件的中文是否完整,再开始转换。这一步多花十分钟,能省掉后面几百处手工改字符串。
5.2 编译全过、一跑就崩:Option Explicit缺失留下隐式变量
现象:工程编译零错误,启动后点第一个按钮就抛NullReferenceException,堆栈指向一个看起来完全正常的赋值语句。
原因:原 VB6 工程没开Option Explicit,代码里存在拼写错误的变量名,VB6 会当成新变量自动声明,值默认为Empty。转换工具把它转成 C# 的局部变量声明,但初值是null而不是空字符串或 0,后续字符串拼接或者数值运算就炸。
解决:转换前在 VB6 里临时加上Option Explicit,编译一遍,把所有“变量未定义”的错误修掉再转。这个动作能提前暴露大量潜在问题,而且原工程改完之后行为不变,风险为零。已经转完的工程,就在 C# 侧把所有string局部变量初始化为string.Empty,数值初始化为 0,用编译器警告当线索逐个找。
5.3On Error Resume Next变成吞异常:错误处理语义漂移
现象:转换后功能“能用”,但某些异常输入下程序不报错、也不出结果,界面卡在中间状态,用户以为按钮没反应。
原因:VB6 的On Error Resume Next是“出错就跳过这一句继续往下”,转成 C# 的try { ... } catch { }之后,整个try块里后面的语句全被跳过,跟原语义完全不同。工具只能做结构映射,理解不了“跳过一句”这种粒度。
解决:把所有On Error Resume Next的位置列出来,逐个判断原意。如果是“这一句可能失败,失败也无所谓”,就把它单独包一层try/catch;如果是“整个流程容错”,那就在catch里补日志,至少让失败可见。我一般会在catch里加一行Debug.WriteLine,跑测试时能看到哪里被吞了,比空catch强太多。
5.4 部署到客户机就报“类未注册”:位数和注册都没对
现象:开发机跑得好好的,拷到客户机上启动就弹异常,提示某个 COM 类未注册或者“检索 COM 类工厂失败”。
原因:两种可能。一是目标机器根本没注册那个 OCX/DLL,regsvr32没跑过;二是位数不匹配——工程是AnyCPU,在 64 位系统上默认以 64 位进程启动,而 32 位 OCX 只有 32 位版本,加载必然失败。
解决:先确认依赖清单,每个 COM 组件都在目标机上regsvr32一遍,注意用管理员权限。然后在 C# 项目里把平台目标从AnyCPU改成x86,强制 32 位进程。这两步做完还有问题,就用依赖查看工具看进程实际加载了哪些模块,缺什么补什么。部署包里最好附一个注册脚本,把regsvr32命令都写进去,省得每次现场手敲。
5.5 界面看起来一样,操作顺序一变就出错:DoEvents和定时器
现象:功能单独测都正常,用户快速连点两次查询,或者查询还没结束就切换页签,程序直接卡死或者数据错乱。
原因:VB6 代码里大量使用DoEvents让界面在长循环中保持响应,转换后变成Application.DoEvents(),行为保留了下来。但DoEvents会让消息泵在循环中间重入,用户的操作会在循环还没结束时插进来,原代码假设的“单线程顺序执行”被打破。
解决:把DoEvents的位置全部找出来,能删的删——用异步或者BackgroundWorker替代长循环;不能删的,在进入循环前把关键按钮Enabled = false,循环结束再打开,用最简单的办法挡住重入。定时器同理,Timer的Tick事件里如果做耗时操作,要加一个“正在处理”的标志位,防止重入。
6. 转换后的验证与加固:怎么确认它真的等价
编译通过、界面摆正,只说明这个工程“像”原来那个。真正交付前还得回答一个问题:行为是不是一样。这件事没有自动化捷径,但可以用清单把范围收敛住,让验证有终点。
6.1 把编译门槛抬高:警告当错误用
第一次编译通过之后,我会立刻把工程的警告级别调到最严,把“警告视为错误”打开,然后重新编译。转换产物里大量的警告其实是真问题:未使用的变量往往对应原代码里被注释掉的分支,可能的空引用对应没有初始化的对象,类型转换警告对应 Variant 遗留。把警告清零的过程,等于把一批潜在运行时异常提前消灭。
# 用命令行编译并输出全部警告,便于统计和分批处理 dotnet build OrderSys.csproj -c Release /p:TreatWarningsAsErrors=false -v normal 2>&1 | tee build.log grep -E "warning (CS|CA)[0-9]+" build.log | awk '{print $NF}' | sort | uniq -c | sort -rn命令里先不用TreatWarningsAsErrors=true,否则编译直接中断,看不到全貌。先收集、按告警码统计,从数量最多的那类开始修,修完再开严格模式。grep那一段是为了把告警码抽出来计数,按频次排序,通常前三个告警码能覆盖八成问题。
6.2 回归清单:用录屏反推测试用例
前面冻结基线时录的操作视频,这时候派上用场。每个视频对应一个测试用例,输入数据、操作步骤、预期输出都从视频里抄。清单不用追求全覆盖,覆盖核心业务路径就行——登录、主界面加载、每个页签的查询、新增、修改、删除、导出、打印。
跑回归时重点看三类差异:数值精度(VB6 的Currency和 .NET 的decimal在小数位处理上可能不同)、日期格式(Format函数受区域设置影响)、空值处理(IsNull和DBNull的判断)。这三类差异不会导致崩溃,只会导致结果悄悄不对,最难发现,也最该重点盯。
界面比对可以偷个懒:把原程序和新程序的同一窗体分别截图,用图片比对工具叠一下,控件错位一眼就能看出来。比对着屏幕用尺子量快得多,也比肉眼看靠谱。
6.3 一个容易被忽略的加固点:默认属性访问
VB6 里Text1 = "abc"这种写法是访问控件的默认属性,等价于Text1.Text = "abc"。同一个控件在不同上下文里默认属性可能不同,工具转换时如果判断错了,代码编译能过但行为不对——比如把Label1 = "abc"转成给Label对象赋值而不是给Text属性赋值,编译直接报错还算好的,怕的是转成了别的属性。
我一般会在转换完成后全局搜一遍= new和直接对象赋值的模式,确认没有把属性赋值写成对象赋值。这种问题在编译阶段大多能暴露,但如果有隐式转换参与,就可能漏过去。跑一遍回归清单里的界面初始化用例,基本能兜住。
从那以后我每次做这类迁移,都强制走一遍“体检报告 → 冻结基线 → 试点窗体 → 整包转换 → 警告清零 → 录屏回归”这条流水线,一步不跳。跳过哪一步,后面就得用双倍时间补回来。希望帮到你。
本文还有配套的精品资源,点击获取