十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VC6.0 MFC老项目迁移VS2019踩坑与填坑指南

VC6.0 MFC老项目迁移VS2019踩坑与填坑指南 VC6.0的MFC老项目迁移到VS2019我踩过的坑和填坑步骤手上还有VC6.0时代留下的MFC项目需要维护的兄弟应该能体会那种感受一边是跑了几十年、承载着核心业务逻辑的老代码一边是Win10、Win11下连VC6安装都费劲的新环境。我前段时间刚把一个用VC6.0写的MFC桌面应用完整迁移到了VS2019整个过程花了大概三天。这篇文章就是把这三天里踩过的坑、验证过的步骤、以及最后沉淀下来的排查清单整理出来给同样要面对这场“考古式迁移”的朋友做个参考。先说结论VC6.0的MFC项目迁到VS2019核心难点不在MFC框架本身——MFC从4.2到14.x的接口变动其实没到伤筋动骨的地步——真正的三座大山分别是字符集从ANSI向Unicode切换引发的连锁反应、老编译器习惯的宽松语法在新标准下的收紧、以及依赖库和链接配置的重新梳理。把这三座大山翻过去整个迁移项目就完成了八成。我这次迁移的是一个典型的VC6时代MFC应用多文档视图结构、对话框资源为主、CListCtrl和CComboBox重度使用、还带几个自绘控件代码量大概在十万行级别。如果你手里的项目规模类似那这篇文章里的经验基本都能对上。1. 迁移前先想清楚这几件事1.1 先评估项目规模与依赖别急着点“下一步”很多人的第一反应是把旧工程文件直接甩给VS2019让它自动转换这个动作本身没错但在动手之前我建议你先花十几分钟做一次“依赖盘点”。打开你的项目根目录看看除了 .dsp 和 .dsw 文件之外还带了哪些东西有没有第三方的静态库.lib和动态库.dll有没有用到了微软的Platform SDK里比较偏门的API有没有自己封装的公共类库我这次的项目就带了一个自己写的通信库和一个第三方的报表控件这两个玩意儿的库文件都是VC6时期编译的VC6生成的.lib和DLL在VS2019里直接用是有风险的——尤其是老编译器生成的静态库链接时经常出现新编译器不认识的目标文件格式或者符号修饰规则不一致的问题。另外一个容易被忽略的点是操作系统目标版本。VC6时代的默认目标是Windows 9x/NT4而VS2019默认目标则是最低Windows 7或Windows 10。如果你的客户环境里还有Windows XP的存量设备这里会涉及工具集选择的问题VS2019的v142工具集官方不支持XP目标需要用v141_xp或v120_xp这样的旧平台工具集来配合。我这次没有这个限制直接选了v142但如果你们的产品还有工业电脑、医疗设备这类老系统的存量用户一定要先确认清楚再动手。1.2 环境准备VS2019安装时别漏掉MFC组件VS2019的安装比VC6时代复杂得多因为它是工作负载和单个组件组合的安装方式。我见过好几个朋友安装完VS2019新建MFC工程才发现找不到MFC向导又回头去改安装程序。这里重点说一下在Visual Studio Installer里选择“使用C的桌面开发”这个工作负载之后还要在右侧的“安装详细信息”面板中找到**“适用于最新v142生成工具的C MFCx86和x64”**这一项勾选上。如果你要用到ATL还需要勾选“适用于最新v142生成工具的C ATL”。我建议这个阶段一次性把MFC、ATL、以及“Windows 10 SDK”都装上省得后面编译到一半发现缺组件再关掉VS去改安装很打断节奏。另外如果你打算在迁移后保留旧工具集做对比编译可以顺便把“MSVC v141”工具集也勾上。1.3 备份和版本管理迁移前最后的保命措施这一步可能显得啰嗦但我还是要强调在开始任何转换动作之前给整个项目目录做一次完整备份并确保代码在一个干净的分支上。VC6的项目文件.dsp/.dsw在VS2019转换后会被改写为新的.sln/.vcxproj格式有些场景下甚至会把原文件直接替换掉。虽然VS2019在转换前会提示你并且会生成转换日志但实际操作中我见过不少转换后资源文件.rc或头文件的换行符、编码格式被改动的情况。如果你没有备份一旦转换后的代码出了问题想回到老版本对比就会非常被动。我当时的做法是整个源码目录复制一份到单独目录命名为xxx_backup_vc6放在旁边不动。然后在原目录上做转换迁移所有代码改动通过Git做版本记录。这样万一某个步骤搞砸了随时能回退。2. 打开旧工程转换向导与新工程结构2.1 .dsw/.dsp到.sln/.vcxproj的转换过程在VS2019里直接选择“打开项目或解决方案”定位到VC6工程下的.dsw文件VS会弹出一个“您正在打开一个由较旧版本的Visual Studio创建的工程”的提示点击确定后开始转换。转换过程一般就几十秒完成后会生成一个.sln文件以及每个子项目对应的.vcxproj文件。注意VS2019的转换向导非常“胆小”大部分有疑问的配置不会自动迁移而是写入项目目录下的一个转换报告UpgradeLog.htm。转换完成后务必打开这个日志文件过一遍看哪些配置项被忽略或改写了。我当时在这个日志里发现的一个问题VC6时代设置的预处理器定义_AFXDLL在转换后没有保留导致编译时MFC变成了静态链接方式而出现一堆链接错误。还有VC6里的“使用MFC”选项静态库还是共享DLL在转换后默认值变成了“使用标准Windows库”这个不改回来直接编译你会发现MFC的头文件都找不到。2.2 调整项目配置字符集、平台工具集与MFC用法转换完成后先别急着按F7编译。打开项目属性右键项目 - 属性先把下面这几个配置项确认一遍这是后面所有工作的基础平台工具集确认是Visual Studio 2019v142。这个决定你用什么版本的编译器如果目标环境老可以从这里切换成v141_xp等旧工具集。字符集VC6时代默认是“未设置”也就是ANSI。VS2019创建的新项目默认是“使用Unicode字符集”。转换后的老工程这里往往还是“未设置”或“使用多字节字符集”。我建议直接把字符集切到“使用Unicode字符集”理由后面详细讲。MFC的使用在“配置属性 - 常规”里把“MFC的使用”设置成“在共享DLL中使用MFC”。如果你的目标是部署简单、不想带一堆MFC运行时DLL可以选“在静态库中使用MFC”但这会导致生成的可执行文件体积增大一大截而且如果项目里有老DLL依赖静态MFC还会引发模块间MFC状态不一致的问题。我这次选了“在共享DLL中使用MFC”。配置管理器检查一下活动解决方案平台是x86还是x64。老项目基本都是32位的VS2019默认也保留了x86Win32平台。如果你的项目有原生依赖库都是32位的就保持x86别去新建x64平台——不然编译过了链接第三方库时会因为库位数不匹配而失败。2.3 工作目录与输出目录设置VC6时代的默认输出目录是Debug或ReleaseVS2019的默认则变成了$(SolutionDir)$(Configuration)\这类宏定义。这个差异本身不会导致编译错误但会导致你的程序运行时找不到同目录下的DLL或配置文件。我当时就被这个坑了一下程序编译成功一运行就提示缺这个DLL缺那个DLL后来发现输出文件被放到了Debug和Release目录而我那些第三方DLL还放在原来的输出目录里程序自然找不到。解决方法很简单在项目属性的“常规 - 输出目录”里显式设置为$(SolutionDir)$(Configuration)\或者干脆沿用旧习惯改成Debug/然后把DLL拷贝到对应目录。3. 编译期最常见的四大类错误3.1 Unicode字符集引发的连锁反应我这次迁移过程中数量最多的编译错误全部集中在字符集问题上。VC6时代大家写代码基本都不刻意区分窄字符和宽字符CString默认就是char*底层字符串字面量也全是...。切到Unicode之后CString底层变成了wchar_t*很多函数调用就出现了类型不匹配。一个典型的例子是CString::Format。老代码里这样写CString str; str.Format(当前值: %d, 名称: %s, nValue, szName);在Unicode下编译时第一个参数当前值: %d, 名称: %s会被解释成const char*而CString::Format期望的是LPCTSTR即const wchar_t*编译器会直接报错。解决思路有两类所有字符串字面量加_T()宏包裹格式化字符用_T(%s)这种形式。更彻底的做法是全面改用TCHAR系列宏和_T()让代码同时兼容ANSI和Unicode编译。我推荐第二种思路因为它是微软官方推荐的写法改完之后代码在两种字符集下都能编译通过。改动量看着大其实用VS的“编辑 - 替换”加上正则表达式处理大部分字符串字面量都能自动替换。另一个字符串相关的高频错误是把LPCTSTR直接传给需要char*的第三方库接口。这种地方没有捷径只能逐个检查能转成char*的就用CT2A宏转换能改成TCHAR版本的就改。3.2 CString行为变化与格式化问题除了字符集层面的变化CString自身的行为在MFC 4.2到14.x之间也有一些调整其中影响最大的是GetBuffer和ReleaseBuffer这套接口在异常处理和空指针行为上的差异。老代码里常见的写法char* pBuf str.GetBuffer(1024); GetSomeData(pBuf, 1024); str.ReleaseBuffer();这类代码在VC6下运行正常但到了VS2019环境如果GetSomeData中间抛了异常ReleaseBuffer就不会执行CString内部缓冲区状态就乱了轻则后续字符串操作异常重则崩溃。新版MFC里CString在调试模式下有更严格的状态检查这种问题会直接触发断言。我的建议是尽量用CString::GetBufferSetLength替代GetBuffer前者会正确设置内部长度避免ReleaseBuffer前的迭代器失效问题同时检查所有GetBuffer/ReleaseBuffer之间的代码路径确保成对出现。格式化方面还有个容易被忽略的变化VC6时代的CString::Format对%s的解释取决于字符集而到了现代MFC%s明确表示宽字符串%S表示窄字符串大写S。如果你在Unicode下要格式化一个char*必须写_T(%S)否则打印出来是全乱码或者只有第一个字符。3.3 旧语法与编译器差异VC6的C编译器对标准C的兼容性比较宽松甚至可以说有点“法外狂徒”。很多当年能编译通过的写法到了VS2019的v142编译器这里直接变成错误甚至硬报错。我遇到的几个典型情况for循环变量作用域。VC6时代for循环内声明的变量在循环结束后仍然可见这是非标准行为。新编译器严格遵循标准循环结束后变量就出了作用域。如果老代码里有依赖这个特性的逻辑必须改成在循环外声明变量。临时对象绑定到非const引用。VC6允许将临时对象传递给非const引用参数新标准不允许。遇到这种代码要么把参数类型改成const引用要么先创建一个具名变量再传进去。缺少头文件但能编译通过。VC6的某些头文件会互相包含导致你漏掉了显式包含但没报错。新版本的头文件组织更干净少了包含直接报错。这种错误没什么好办法看编译器提示缺哪个头文件补哪个。new操作符不检查返回值。VC6时代如果new失败返回的是NULL所以老代码里经常看到if (p NULL) return;的防御性写法。VS2019默认情况下new失败会抛出std::bad_alloc异常那些老代码里的NULL检查不但没用反而因为不可达而产生编译警告。如果项目确实要兼容内存分配失败的情况可以改用new (std::nothrow)但大多数桌面应用其实没必要。3.4 头文件、库依赖与链接错误链接错误是迁移过程中最让人头疼的一类问题因为编译器提示的符号名往往跟实际代码对不上号。最常见的是unresolved external symbol错误。原因一般有两类一类是VC6时代用到的库在新版SDK里被拆分成多个库或更名了。例如老代码里调用了一些网络相关API可能在VC6里只需链接wsock32.lib而新环境可能需要ws2_32.lib老代码里用到的GetAdaptersInfo这类函数原本在iphlpapi.lib里新平台又调整到了别的库。处理方式是先看错误提示里的符号名然后在对应的头文件里找到这个API的声明看看声明边上有没有#pragma comment(lib, ...)或者去微软官方文档查该函数属于哪个库把对应的lib加进链接器输入的附加依赖项里。另一类是第三方静态库的编译环境不一致。VC6生成的.lib文件链接到VS2019项目里很多时候会因为C标准库实现不同、运行库/MT、/MD不匹配而报错。这种问题没什么好技巧唯一可靠的方案是拿到第三方库的源码重新在VS2019下编译一遍。我那个自己封装的通信库就是重新编译解决了问题。如果是闭源第三方库联系厂商要新版否则就得考虑绕开这个依赖。3.5 两种字符集路径的选择建议这里单独开一小节说说字符集的选择策略因为这是迁移过程中影响面最大的决策。理论上你可以在VS2019下继续用“多字节字符集”编译老代码这样很多字符串问题可以暂时规避。但我不推荐这么干理由有三个第一新版MFC虽然还支持MBCS但微软已经在多个版本里明确说过MBCS支持是“尽力而为”不会增加新功能未来版本甚至有移出风险。你现在继续用MBCS等于是在给自己攒技术债。第二现代的Windows系统底层全部是Unicode许多系统API的A版本ANSI版本只是内部转成W版本再调用的壳子性能本身就有额外开销而且某些较新的系统API甚至只提供W版本用MBCS编译时根本找不到对应入口。第三Windows 10/11对非Unicode区域的程序有系统级的兼容隐患比如中文系统下良好运行的程序放到非中文区域就乱码这类问题改Unicode后能从根本上解决。所以我的建议是长痛不如短痛宁可多花一天时间改字符串也要在迁移时就把Unicode切换完成。这是一次性投入但收益是长期的。4. UI控件与资源文件的迁移细节4.1 对话框资源与字体处理VC6创建的对话框模板资源在VS2019的资源编辑器里基本能正常打开但有几个细节要注意。最明显的是字体。VC6时代的对话框默认字体是“System”系统字体点阵字体尺寸小。VS2019打开这个资源后会保留原来的字体设置但在高分屏Windows上显示效果就很一般了字小、模糊、布局也可能被截断。我当时的做法是把对话框模板的字体统一改成“微软雅黑”或“Segoe UI”字号设成9号或10号然后逐个对话框检查布局是否正常。这个改动看着麻烦但如果你不做用户在高分屏上看到的界面就是“糊成一团”。批量修改的方式是直接用文本编辑器打开.rc文件查找FONT关键字全局替换。注意替换后要在VS里重新生成一次资源确认没有语法错误。4.2 CListCtrl、CComboBox等控件的兼容性如果你在网上搜“VC6 迁移 VS2019”相关的关键词会看到不少人提到CListCtrl和CComboBox在迁移后出现显示异常或者操作失灵的问题。我这次也遇到了但最终排查下来这些问题大多不是控件本身的接口变了而是字符集切换带来的间接影响。以CComboBox为例老代码里经常是CComboBox* pCombo (CComboBox*)GetDlgItem(IDC_COMBO1); pCombo-AddString(选项A); pCombo-AddString(选项B);Unicode切换后AddString的参数是LPCTSTR字符串字面量如果是选项A这种窄字符形式编译时可能警告甚至报错运行时就可能出现下拉列表空白或者乱码。解决办法是给字符串字面量包上_T()宏或者统一用CString构造后再传进去。CListCtrl的情况类似。InsertColumn、SetItemText这些方法的字符串参数同样受字符集影响。这里有一个老代码里比较经典的坑VC6时代用CListCtrl::SetItemText时经常直接传一个char*变量而不做转换在Unicode下编译器会给你C2664错误但如果用了强制类型转换硬编过去运行时就表现为列表项显示空白。这种问题排查起来很迷惑因为编译没报错运行也不崩溃就是显示不对。我的排查经验是迁移后在Unicode字符集下把所有向控件传字符串的地方都挑出来检查一遍。用VS的“查找所有引用”可以做到重点看字符串的类型是char*还是wchar_t*如果是char*统一用CA2T或CT2A做转换。4.3 菜单、工具栏与图标资源的处理菜单和工具栏在迁移中的问题相对较少主要注意下面几个点图标资源.ico在VC6时代通常是8位或24位色深、256色或65536色。到了VS2019的现代UI项目中建议补充一套高分辨率图标32x32、48x48甚至256x256不然在高DPI显示器下图标会显得模糊。这个不是迁移必须做的事但迁移完顺手做掉体验提升很明显。工具栏按钮的位图在VC6项目里通常是bmp文件VS2019里依然支持但新版MFC推荐使用PNG格式并配合CMFCToolBar系列类。如果你不想动老代码继续用旧的CToolBar加载bmp也是可以的只需要确认资源ID和加载逻辑没有在转换中丢失。4.4 自绘控件的重绘问题老MFC项目里多少都会有几个自绘控件重写OnPaint或DrawItem的那种。迁移后这类控件经常出现的现象是按钮背景变成灰块、文字不显示、或者整个控件内容错位。这个问题的根源主要是**CMFCButton和CButton的自绘逻辑差异**以及新版MFC在控件绘制时的高DPI处理。VC6时代自绘按钮靠DrawItem配合OwnerDraw风格这套机制在VS2019里依然保留但绘制上下文的高DPI缩放行为变了。如果你的屏幕是125%或150%缩放老自绘控件里的GetClientRect、DrawText这些操作的坐标会被系统自动放大但你的绘图片段可能没有按比例适配最后画出来的东西就偏移了。最简单的办法是在OnPaint开头调用CMFCVisualManager或手动处理WM_DPICHANGED稍微复杂一点的办法是把自绘逻辑改为基于逻辑坐标让系统去缩放。我这次项目里的自绘控件不多遇到问题的就一个按钮和一个列表自绘栏最后是用CDC::SetMapMode配合MM_TEXT模式修好了。如果你的自绘代码量很大这可能会是迁移中最耗时的一部分建议提前评估工作量。5. 运行时问题与调试排障5.1 界面显示异常编译链接通过之后真正考验才刚刚开始——运行时问题往往比编译错误更难定位。我遇到的第一类运行时问题是界面显示错乱。具体表现为启动后主窗口标题栏文字正常但对话框里有些标签文本变成乱码下拉框里的中文直接消失。排查后确认是字符集切换后字符串字面量没改全的问题。这类问题用VS2019的调试器很容易定位在出问题的控件所在函数里下断点查看字符串变量的值如果显示为乱码或者空串往前追数据来源基本都能找到是哪个字符串在传参时丢了数据类型转换。第二类是控件尺寸和布局异常。VC6时代对话框的布局是固定的像素坐标但在高分屏和不同DPI缩放下旧的布局逻辑会出问题。表现就是按钮歪了、控件重叠或拉伸变形。解决办法是重构布局代码用OnSize配合MoveWindow实现自适应布局或者用CMFCLayoutManager一类的辅助类。如果时间紧也可以用SetWindowPos在初始化时精确调整一遍控件位置做个“一次性适配”应付过去。这里要提一下MFC在高DPI下的支持在老项目中往往是被忽略的。VC6时代的设计目标屏幕一般是96DPI。如果不做任何适配在Windows 10的150%缩放下MFC对话框会自动缩放但自绘区域和手动定位的控件不会呈现出的效果就是“一半正常一半错位”。最好的办法是给应用程序声明DPI感知在项目资源里加一个manifest设置dpiAwareness为PerMonitorV2然后重新审视布局代码。5.2 崩溃与内存问题迁移后最吓人的问题就是运行崩溃。我这次遇到的几个比较典型的崩溃场景场景一启动即崩溃。排查方式是看调用堆栈。如果堆栈显示崩溃在MFC内部且是CString或CArray相关的第一反应查_CrtIsValidHeapPointer断言。这类崩溃通常是字符串或容器跨模块传递导致的堆不匹配。VC6时代项目里如果有多个DLL每个DLL编译时使用的运行库不一致比如一个用的/MD一个用的/MT就可能出现一个模块new另一个模块delete的情况在VC6下侥幸没崩到了VS2019就崩了。解决方案是统一所有模块的运行库选项一律用/MD动态CRT。场景二操作某个按钮时崩溃。这种问题多半是控件ID或消息映射错误。老代码里如果用了ON_MESSAGE宏且消息ID依赖于枚举值迁移后枚举值的顺序被编译器重新排列过就可能出现错误映射。查ID定义是否稳定、消息处理函数是否有空指针判断。场景三退出程序时崩溃。这类问题通常出现在静态对象的析构顺序变化上。VC6编译器和VS2019编译器对全局对象和静态局部变量的构造、析构顺序实现可能有差异。如果项目里有全局CString或其他带堆内存的静态对象退出时崩溃的概率不小。排查思路是看崩溃堆栈最后停在哪个对象析构上然后在析构里加日志输出确认是不是堆已经释放才去访问。5.3 还在用老DLL这里有个必须踩的坑如果你的程序依赖VC6时代编译的MFC扩展DLL类名一般带AFX_EXT_CLASS宏迁移后十有八九会出问题。原因是VC6生成的MFC扩展DLL链接的是老的MFC42.dll而你的主程序在VS2019下链接的是新版MFC140u.dll两个DLL各自维护一套MFC内部状态跨模块传递CString、CWnd*之类的MFC对象时内部结构定义不一致轻则数据错乱重则直接崩溃。解决途径只有两条要么找DLL的源码用VS2019重新编译要么把这个DLL的功能直接合并进主程序代码。我这次的通信库就是选择了合并进主程序的方式省去了维护DLL接口的额外工作。5.4 常见问题速查表我把这次迁移过程中遇到的高频问题整理成了一张表方便你到时候对照排查问题现象可能原因排查思路编译报错C2664字符串类型不匹配检查函数参数是LPCTSTR还是LPCSTR加_T或用CT2A转换编译报错C4996使用了被标记为废弃的API确认具体接口用安全版本替换或重新评估链接错误LNK2019引用了未定义的符号查头文件声明补依赖库确认库位数和运行库一致性界面文字乱码字符串字面量未做Unicode适配全局搜索字符串字面量统一加_T()或L前缀对话框控件显示错位DPI缩放导致布局失效声明DPI感知重写布局逻辑或用MoveWindow调整启动即崩溃堆不匹配或MFC模块状态不一致检查编译选项统一/MD重建依赖DLL列表控件/下拉框空白数据类型转换错误检查SetItemText、AddString的字符串参数实际类型按钮自绘区域花屏自绘代码未适配高DPI在绘制代码里处理缩放或改用新MFC控件类程序退出时崩溃静态对象析构顺序变化检查全局对象用日志定位崩溃点6. 收尾编译通过了不代表迁移完成到这里整个迁移的技术环节算是走完了但根据我自己的经验编译通过、运行正常只是第一步。我给自己的项目定了一个“迁移完成”的清单供你参考第一在至少两种不同缩放比例的屏幕100%和150%上完整跑一遍核心功能确认界面显示正常、控件操作无误。第二把所有对话框、视图窗口的所有Tab页都点一遍确认没有资源加载失败或控件初始化报错。第三跑一遍项目的完整功能回归尤其是涉及文件读写、网络通信这些和历史业务紧密相关的模块。主力环境的差异让老代码里很多“侥幸工作”的逻辑变得不再受保护这些逻辑往往在回归测试中才会暴露问题。第四重新生成Release版本在干净的环境没有安装VS2019的机器上部署测试确认MFC运行库、VC运行时库都能正常加载。如果目标是简易部署可以考虑用静态链接MFC或在安装包中带上对应版本的运行时库。最后再分享一个我个人的小经验迁移完成后把VS2019的代码分析工具打开对迁移后的代码做一次静态分析。虽然会产生一大堆代码质量警告但其中有不少能帮你发现老代码里潜在的隐患——毕竟编译器能过不代表代码就是安全的。比如VC6时代流行的char buffer[256]之类的定长缓冲区在新平台下虽然编译没问题但代码分析工具能帮你检测出可能的缓冲区溢出和未初始化内存读取。这个分析过程花费不了多少时间却能让你的老项目在新编译环境下更有底气地上线运行。
返回列表