我接触MFC的时间不算早但也不算晚正好赶上它由盛转衰的那道分水岭。那时候Windows桌面开发还是微软一家独大C Builder和Delphi虽然能打但在系统级API的亲和力上比不过MFC更别说后来的C#还是个没断奶的孩子。如今再回头看MFC确实老了老到很多人觉得它该进博物馆了可你要是真去工业软件、医疗设备、工控上位机这些领域转一圈会发现MFC依然是不少存量系统的中流砥柱。更别提网上还常年有人问怎么让控制台程序支持MFC、怎么用zxing-cpp在MFC里读二维码、为什么VS2010里List Control第一行设了LVCFMT_LEFT不生效这类问题——这些热搜词本身就说明MFC没有真正死掉只是进入了一种“还在用、但没人吹”的状态。这篇文章我不想做那种四平八稳的回顾什么“MFC是微软基础类库”、“封装了Win32 API”之类的教科书话术网上到处都是。我想从一个还在用它干活的人的角度聊聊MFC的过去为什么能成现在还有哪些场景离不开它以及未来它还能怎么跟这个技术时代共存。中间会穿插一些我实际踩过的坑、调过的错、翻过的车尤其是那些网上搜不到答案的细节希望能给还在维护MFC项目的朋友一点参考。1. MFC的过去Win32的苦力活它帮你干了1.1 那个年代的Windows开发有多痛苦在MFC出现之前用C语言直接调Win32 API写Windows程序是什么体验你写一个最普通的窗口程序要手动注册窗口类、填WNDCLASS结构体、写窗口过程函数、处理消息循环然后在一个巨大的switch-case里处理WM_CREATE、WM_PAINT、WM_SIZE、WM_COMMAND……一个小程序几百行代码起步而且绝大部分都是重复的模板代码。最痛苦的是Win32 API是纯C接口全局函数满天飞所有状态都要你手动管理。窗口句柄HWND、设备上下文HDC、GDI对象HBRUSH这些都是赤裸裸的资源创建了就要释放忘了就是泄漏。更别提子窗口控件的通信你要通过WM_COMMAND消息里的wParam和lParam去猜是哪个控件发的、是哪条命令纯靠手工解析那叫一个酸爽。MFC的价值就在这里它把这一坨苦力活全部封装成了类。CWinApp管应用程序生命周期CFrameWnd管主窗口CWnd封装了所有窗口相关操作你不用再手写窗口过程消息映射宏帮你把Windows消息直接路由到类成员函数。那个年代没有智能指针没有现代CMFC就是当时能让C程序员高效写Windows应用的现实解。1.2 消息映射与文档视图结构的设计智慧MFC早期版本默认支持动态创建、消息映射、命令传递这些机制今天看来有些绕但放在90年代初期的编译器条件下已经是能想到的最优解。消息映射宏是MFC最核心的机制之一。MFC没有用C的虚函数来处理Windows消息因为Windows消息有上百种如果每种消息都做成虚函数每个窗口类都要扛一张巨大的虚函数表内存和效率都扛不住。MFC的解法是搞了一套消息映射表用宏把消息ID和成员函数绑定起来查表分发。这套方案的核心结构是AFX_MSGMAP一个静态数组加一个父类指针一层层往上找。它是一种介于“虚函数”和“纯手工switch”之间的折中设计在当年的技术条件下极度务实。文档视图架构则是另一个标志性设计。Document类管理数据View类负责显示和交互Frame窗口提供边框和菜单这其实是MVC模式的一个变种。对业务软件来说这套架构最大的好处是数据与界面分离你可以换一套视图不影响数据层或者一份文档挂多个视图比如同时显示表格和图表。虽然今天看来这个架构有些笨重但逻辑是清晰的。回头看MFC的过去不只是“微软的一个类库”这么简单它定义了那个年代Windows桌面开发的主流范式影响了整整一代人的编程习惯。很多现在做WinForms、WPF的老程序员脑子里那套“窗口、控件、消息”的思维定势就是从MFC时代带出来的。2. MFC的现在谁还在用以及他们每天在解决什么问题2.1 存量系统的真实面貌我在实际工作中接触过不少MFC项目一句话概括都是“老而弥坚”的系统。这些系统分布在工业控制、医疗设备、电力监控、军工软件、实验室仪器这些领域通常已经稳定运行了十年以上承载着核心业务流程。不是没人想过重写而是重写的成本太高了——动辄几十万行代码业务逻辑复杂测试用例缺失没有人敢拍这个板。所以MFC的现在很大程度上是由存量系统定义的。我们这些维护者不会每天都去研究什么新颖的技术更多的是在跟编译器版本、字符集、控件行为、第三方库集成这些具体问题较劲。比如让控制台程序支持MFC这个问题看着奇怪实际场景比你想的多有些人想把老的MFC类挪到命令行工具里复用有些人是在做自动化测试时需要一个无窗口的MFC环境。做法其实不复杂就是在stdafx.h里包含afx.h然后把入口函数的CWinApp对象创建出来调用AfxWinInit初始化。核心代码大概长这样#include afxwin.h CWinApp theApp; int main() { if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0)) { // 初始化失败 return -1; } // 这里是你的业务代码 return 0; }这里有个坑AfxWinInit必须在首次调用任何MFC机制之前执行否则后面会出各种莫名其妙的状态问题。还有控制台程序的入口是main而不是WinMain所以整个MFC框架的消息循环不会自动启动需要你自己管理——但反过来说如果你只是想把CString、CFile、CArray这些类拿到控制台环境里用那简单得多引个afx.h就够了。2.2 用VSCode编译MFC是折腾还是真需求网上关于“如何使用VSCode编译MFC”的提问一直有人搜这其实折射出一个挺实际的需求Visual Studio这几代版本越来越重启动越来越慢特别是VS2010、VS2015这些老项目用新版VS打开一次要等半天改个文件编译一次又要等半天。有些轻量级的MFC工程确实没必要每次都打开完整的IDE用VSCode配好编译任务改改代码直接CtrlShiftB效率能提升不少。实际操作的关键是MFC的编译本质上是微软C编译器的活VSCode只是个壳。你需要确保命令行环境下能调用cl.exe和nmake.exe这就需要调用开发者命令行环境——用VsDevCmd.bat初始化环境变量。在VSCode里配置tasks.json我的做法是先用外部脚本初始化环境再调用nmake。任务配置大概长这样{ version: 2.0.0, tasks: [ { label: Build MFC Project, type: shell, command: cmd /c \call \\\C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat\\\ nmake\, group: { kind: build, isDefault: true }, problemMatcher: [ $msCompile ] } ] }注意几个细节vcvars64.bat的路径要根据你实际安装的VS版本调整如果项目是老工程可能还需要设置平台工具集比如用v142工具集编译VS2010的工程会有不少警告还有MFC涉及资源编译需要rc.exe在PATH里用VsDevCmd初始化可以一并解决。我个人的体会是VSCode编译MFC适合做快速的语法检查和编译验证但真正的资源编辑、对话框设计还是离不开VS的IDE毕竟.rc资源文件的可视化编辑器在VSCode里没有对应插件。所以我的建议是把它当“轻量编译助手”而不是完全替代VS。3. MFC开发中的高频实战问题与解决方案3.1 基于MFC绘制彩色正方形从GDI到GDI这个问题的实际场景一般出现在自定义控件、仪器面板绘制、或者简单的教学Demo里。MFC里绘图无非就是拿到DC设备上下文选好画笔画刷画一个矩形。最简单的方式是用GDI的Rectangle函数void CMyView::OnDraw(CDC* pDC) { CBrush brush(RGB(255, 0, 0)); // 红色画刷 CBrush* pOldBrush pDC-SelectObject(brush); CPen pen(PS_SOLID, 2, RGB(0, 0, 0)); // 黑色边框 CPen* pOldPen pDC-SelectObject(pen); pDC-Rectangle(50, 50, 250, 250); // 绘制矩形 pDC-SelectObject(pOldBrush); pDC-SelectObject(pOldPen); }这里有个经典坑CBrush和CPen都是栈上对象必须它们在函数作用域内有效。所以老手一般会画完之后立刻SelectObject恢复原来的画刷和画笔否则在选中GDI对象被释放时会触发断言失败。很多人第一次写这段代码直接把SelectObject结果丢了结果调试版崩得死去活来。如果想画“彩色”的正方形而不只是纯色填充有几种思路一是用PatternBrush加位图做纹理二是用渐变填充那就要用GDI或GradientFill API。GDI在MFC里的用法也不难Gdiplus::Graphics graphics(pDC-GetSafeHdc()); Gdiplus::Rect rect(50, 50, 200, 200); Gdiplus::LinearGradientBrush brush(rect, Gdiplus::Color(255, 255, 0, 0), Gdiplus::Color(255, 0, 0, 255), Gdiplus::LinearGradientModeForwardDiagonal); graphics.FillRectangle(brush, rect);用GDI之前要在stdafx.h里加上#include gdiplus.h并在程序初始化时调用GdiplusStartup结束时GdiplusShutdown这是一个经常被漏掉的关键步骤。3.2 GridCtrl单元格能否配置颜色选择控件并显示选定配色线条状图形这个问题的背景应该是基于MFC GridCtrl这个第三方控件在做数据表格类界面。MFC GridCtrl是从CGridCtrl类开始的一个开源表格控件功能相对完整但单元格的嵌入控件和自定义绘制都需要自己扩展。实现单元格里的颜色选择控件本质上是两件事第一是让单元格在进入编辑状态时可以弹出一个颜色选择器比如CColorDialog第二是让单元格在非编辑状态下可以整格绘制颜色线条图形。编辑状态的做法是重写CGridCtrl的OnEditCell虚函数在这里创建控件并设置初始值void CMyGridCtrl::OnEditCell(int nRow, int nCol, RECT rect, POINT point, UINT nID) { if (nCol 颜色列) { CColorDialog dlg(当前颜色, CC_FULLOPEN, this); if (dlg.DoModal() IDOK) { COLORREF clr dlg.GetColor(); SetItemText(nRow, nCol, 把颜色转成字符串); SetItemData(nRow, nCol, clr); } // 画线条图形 RedrawCell(nRow, nCol); return; } CGridCtrl::OnEditCell(nRow, nCol, rect, point, nID); }非编辑状态下的绘制则要重写OnDrawCell在绘制文本之前先检查该单元格是否属于颜色列如果是就画几条代表配色方案的线条或色块BOOL CMyGridCtrl::OnDrawCell(CDC* pDC, int nRow, int nCol, CRect rect, BOOL bEraseBkgnd) { if (nCol 颜色列) { // 自定义绘制填充背景然后画出几道彩色线条 pDC-FillSolidRect(rect, RGB(255, 255, 255)); int lineWidth 6; int startX rect.left 4; int y rect.top (rect.Height() - lineWidth) / 2; for (int i 0; i 配色数量; i) { CPen pen(PS_SOLID, lineWidth, 颜色数组[i]); CPen* pOldPen pDC-SelectObject(pen); pDC-MoveTo(startX, y lineWidth / 2); pDC-LineTo(startX 30, y lineWidth / 2); pDC-SelectObject(pOldPen); startX 36; } return TRUE; } return CGridCtrl::OnDrawCell(pDC, nRow, nCol, rect, bEraseBkgnd); }这些代码思路不复杂但真做起来要处理的重绘细节挺多比如控件失焦时焦点框的绘制、字体和行高的适配、颜色状态的数据持久化。GridCtrl的单元格和数据是分开管理的你要么自己搞一个二维数组或者CMap来存每个单元格的颜色配置要么复用SetItemData别只存在界面层否则一刷新就丢。3.3 MFC中读取二维码zxing-cpp的集成经验MFC项目里要识别图片中的二维码热度很高。现在扫码场景很多工业上扫码枪、医疗设备上读取试剂条二维码、自动化的料单识别等等。要在MFC里实现比较流行的方案是集成zxing-cpp。zxing-cpp是原Google ZXing的C移植版本核心是C代码理论上可以跨平台但在Windows下配合MFC用有几个细节需要处理。集成步骤大概是下载zxing-cpp源码用CMake编译成静态库或动态库然后在MFC工程里链接。源码头文件是C11写的你最好把MFC工程的语言标准调到C14以上老版本VS2010可能需要用老版zxing或者移植补丁这是一个比较麻烦的兼容点。读取二维码的代码核心部分#include zxing/BarcodeFormat.h #include zxing/Result.h #include zxing/DecodeHints.h #include zxing/GenericLuminanceSource.h #include zxing/MultiFormatReader.h #include opencv2/opencv.hpp // 如果你用OpenCV做图像预处理 cv::Mat img 读取图像; zxing::Refzxing::LuminanceSource source(new zxing::GenericLuminanceSource( img.cols, img.rows, img.data, img.cols * img.channels())); zxing::Refzxing::Binarizer binarizer(new zxing::GlobalHistogramBinarizer(source)); zxing::Refzxing::BinaryBitmap bitmap(new zxing::BinaryBitmap(binarizer)); zxing::DecodeHints hints; hints.addFormat(zxing::BarcodeFormat::QR_CODE); zxing::MultiFormatReader reader; zxing::Refzxing::Result result reader.decode(bitmap, hints); if (!result.empty()) { std::string text result-getText()-getText(); }这里最容易踩的坑是图像格式转换。MFC里如果是从文件读取CImage可以把图片加载成DIB但zxing-cpp期望的是灰度图或者原始像素数据你需要自己遍历像素把RGB转成灰度// CImage转灰度数组 CImage img; img.Load(Lqrcode.png); std::vectorunsigned char gray(img.GetWidth() * img.GetHeight()); for (int y 0; y img.GetHeight(); y) { for (int x 0; x img.GetWidth(); x) { COLORREF clr img.GetPixel(x, y); unsigned char r GetRValue(clr); unsigned char g GetGValue(clr); unsigned char b GetBValue(clr); gray[y * img.GetWidth() x] (unsigned char)(0.299 * r 0.587 * g 0.114 * b); } }这个转换如果直接用CImage的GetPixel逐点获取效率在大图上会惨不忍睹我推荐用GetBits直接访问DIB像素数据虽然代码复杂度高一截但速度能快一个数量级。3.4 List Control第一行设置LVCFMT_LEFT无效果一个典型的配置陷阱VS2010 MFC List控件第一行设置LVCFMT_LEFT无效果这个问题在热搜上出现的频率很高。它的表象是你给List Control设置了一个列LVCFMT_LEFT也指定了但第一行的对齐方式就是不对。排查到最后原因一般是发生了“视图类型不匹配”或“列插入顺序不一致”。LVCFMT_LEFT是ListView列对齐风格用CListCtrl::InsertColumn传入LVCOLUMN结构体时如果mask里漏掉了LVCF_FMT那LVFMT对齐信息就不会生效。还有一个更隐蔽的坑如果List Control的扩展样式里设置了LVS_EX_GRIDLINES或者LVS_EX_FULLROWSELECT某些情况下visual theme会接管绘制导致LVCFMT_LEFT样式看起来不生效。我的排查步骤确认InsertColumn时mask包含LVCF_FMT。确认插入后调用了SetExtendedStyle而不是每次插入前Reset扩展样式。确认没有设置LVS_OWNERDRAWFIXEDOwnerDraw会让对齐样式完全失效。检查头文件里LVCFMT_LEFT被别的宏覆盖的情况。代码示例LVCOLUMN lvCol {0}; lvCol.mask LVCF_TEXT | LVCF_WIDTH | LVCF_FMT; lvCol.fmt LVCFMT_LEFT; lvCol.cx 120; lvCol.pszText _T(列名); m_listCtrl.InsertColumn(0, lvCol);注意LVCF_LEFT与LVCFMT_LEFT不是同一个东西前者是“列是左固定列”后者是“文本左对齐”。很多新人在属性窗口里盯着“列”→“格式”这一项改改完了看不到效果其实属性编辑器生成的代码里mask只包含了LVCF_TEXT和LVCF_WIDTH没有包含LVCF_FMT属于IDE生成的坑。这个在VS2010里特别常见。3.5 CWinThread与异步任务的正确打开方式MFC多线程说一说CWinThread。MFC抽象了一个CWinThread类来管理线程主线程那一个CWinThread对象其实程序启动时由CWinApp创建。你自己创建工作线程有两条路一是用AfxBeginThread传一个线程函数入口二是从CWinThread派生子类并动态创建。AfxBeginThread的选择其实很实用尤其是当线程里要用MFC类库的时候。因为MFC为了线程安全给很多类做了线程本地存储TLS直接用CreateThread创建线程MFC的TLS没有被初始化可能会导致一些类的使用出问题。AfxBeginThread会帮忙做好初始化。cpp UINT MyThreadProc(LPVOID pParam) { // 在线程里可以安全使用CString、CFile等MFC类 while (!bStop) { // 后台任务 ::Sleep(100); } return 0; } void StartMyThread() { CWinThread* pThread AfxBeginThread(MyThreadProc, this, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); if (pThread NULL) { // 创建失败 return; } pThread-m_bAutoDelete TRUE; pThread-ResumeThread(); }这里有个重要参数CREATE_SUSPENDED。用挂起方式创建线程可以在Resume之前设置线程优先级或参数避免线程启动后参数还没设置好就开跑的竞态问题。另外线程退出时要记得清理如果你用m_bAutoDeleteTRUE线程结束后CWinThread对象会自动删除但你不能在外部再访问这个指针否则就是悬垂指针。还有一个我踩过多次的坑在工作线程里直接操作UI控件。MFC的UI不是线程安全的跨线程调用SetWindowText导致界面卡死甚至直接崩溃。解决方式是用PostMessage发送自定义消息到UI线程让UI线程来更新控件。4. MFC的未来老瓶装新酒的可能性4.1 与现代技术的共存策略说了这么多“现在”最后聊聊未来。MFC在微软的产品路线图里已经进入维护模式新功能基本不会加了但它在Windows平台上依然有存在的理由一个是性能好启动快内存占用比.NET低得多另一个是直接暴露Win32底层机制方便做深度控制。这在工控实时性、硬件交互、系统级工具这些场景里依然是硬需求。很多MFC项目现在接入了现代化的周边工具用CMake管理构建用Git做版本控制用JSON/YAML做配置文件用现代C特性重写核心算法。MFC的界面框架不变但底层基础设施完全可以现代化。我在自己项目里就是把对话框数据交换机制DDX的钱包留着一个但内部逻辑已经逐渐拆分成可独立测试的类了不再是所有东西都耦合在窗口类里。4.2 维护MFC项目的实用现实话虽如此我要泼一盆现实的冷水。如果你现在是2024年问我“从头学MFC做新项目值不值得”我的建议是除非你的领域明确要求必须用MFC比如对接老系统、客户指定、或者你有大量MFC经验能快速出活否则新项目优先考虑Qt、WPF、Avalonia这些仍然活跃的框架。MFC的技术债是真的重Unicode与ANSI的纠缠、老编译器工具集的不兼容、缺少现代UI特性、没有GPU加速、高DPI支持烂……这些都是实打实的痛点。但如果你已经在维护MFC项目那也不用心烦。MFC没那么容易“突然死掉”Windows的兼容性策略决定了它至少在未来很多年内还能运行。我之前跟一个做医疗仪器的朋友聊他说他们那台设备上的上位机用MFC写的已经出厂几百台了法规认证、软件检验都过了换技术栈等于重新过一遍注册检验那成本没人承受得了。所以只要存量还在MFC的“现在”就不会结束。我个人在实际操作中的体会是MFC的真正问题不是它不能用了而是它太久没有像样的新东西了生态里的插件、示例、第三方库越来越难找年轻开发者不愿意进这个圈子人才断层才是最要命的。用MFC有点像一个经验丰富的老木匠手里的工具还是那几把但活儿仍然能出。只是你得自己多花点心思把现代工具嵌进老框架里让它继续发光发热。最后再分享一个小技巧。如果你在MFC里折腾某个不起眼的功能好比那个LVCFMT_LEFT不生效的问题半天找不到原因时先别急着查代码逻辑先看看微软MSDN里关于这个结构的文档再检查一下IDE自动生成的代码。很多时候问题不在于你不会写MFC而在于MFC把太多细节藏在了宏和模板背后那个藏在背后的东西才是坑你的元凶。