这段时间前后有好几个朋友都在问我同一个问题:MFC编程还有没有戏。我特别能理解这种焦虑。打开招聘网站,前端、人工智能、跨平台开发占了半边天,MFC的岗位看起来越来越小众。但我想说的是,你以为的"夕阳技术",恰恰在很多看不见的地方运转得比谁都稳。银行柜台的业务终端、车间里的工业控制面板、医疗设备的操作台、老牌桌面软件的维护迭代……这些场景动不动就是十几年资历的老系统,底层跑的就是MFC。如果只看标题,你可能觉得MFC编程无非就是拖拖控件、点点消息,但实际上它背后牵扯的东西非常多:消息机制、线程模型、设备上下文、内存管理、控件自绘,每一个拿出来都能单独讲好久。
这篇文章,我打算从一个真实的项目视角切入——用MFC做一个工业设备控制面板,把MFC编程里面最重要的几个环节全部过一遍:整体设计怎么拆、消息映射怎么理解、界面怎么做、线程怎么保证安全、常见坑怎么避开。不管你是刚入门还是接了老项目要维护,应该都能从这里拿到一些能直接用的东西。
1. MFC编程这潭水有多深:先把它拆开看
1.1 大多数人理解的MFC其实是"皮毛层"
MFC是一套用C++封装Windows原生API的框架,说白了就是把Win32那套窗口过程、消息循环、绘图接口,包成一个类库供你使用。很多人一提起MFC编程,想到的就是在对话框上拖几个按钮,双击进去写处理函数。这当然算MFC编程,但那只是它的第一层。MFC给你的不是一个"画布",而是一整套Windows应用框架的骨骼。如果你不了解它底层的消息路由逻辑,一旦界面复杂起来、或者要介入自定义消息处理时,很快就会被各种奇怪的"按钮没反应""消息不触发"折磨。
我见过不少开发者的习惯是:界面用模板硬凑,核心逻辑全塞进按钮的响应函数里。小Demo没问题,但工程量一旦上来,这种写法就是灾难。MFC框架真正值钱的地方,在于它把Windows的窗口过程、消息循环、文档视图交互、控件与对话框的关系都整理成了可重用的结构。用得好,代码是有层次的;用不好,它就是一个背着沉重历史包袱的C++工具集。
1.2 围绕MFC的项目真实形态
我接触过的MFC项目,形态大致有三类。
第一类是工业设备控制。界面看起来简单,后台要和上位机通信、要控制运动控制、要监听传感器状态,很多环节都需要和C或者底层驱动打交道。这类项目对实时性要求高,对界面流畅度也有要求,线程用得非常多。
第二类是运营设备中的客户端程序。典型特征是业务逻辑重、表单复杂,有大量的数据展示、报表和打印需求。这类项目往往是"逻辑比界面多",而且经常要和数据库、文件系统、网络服务同时交互。
第三类是老系统二次开发。代码历史可能超过15年,里面既有古老的C风格函数,也夹杂着后期补进去的现代C++写法。你要在不动原有架构的前提下,往里加新功能、修历史遗留的鬼畜Bug,压力比新写一个项目大得多。
这三种形态都指向一个事实:MFC编程不是课堂作业,而是要贴着硬件、贴着业务、贴着旧代码干活的工程。这篇文章我用的模型是工业设备控制面板,原因很简单:这类项目能把MFC在界面、线程、消息、绘制方面的特点全部覆盖到。后面所有讨论,都围绕这个模型展开。
1.3 一个工业控制面板的技术全景
把典型MFC工业控制面板需要触碰的技术点列一个清单:
| 环节 | 具体内容 | 难点等级 |
|---|---|---|
| 应用框架 | 应用类、主窗口、消息循环生命期管理 | 中 |
| 界面布局 | 对话框、属性表、分割窗口的取舍 | 中 |
| 消息机制 | 消息映射表、命令路由、自定义消息 | 高 |
| 多线程 | 工作线程、跨线程更新UI、线程同步 | 高 |
| 绘图 | 设备上下文、自绘控件、双缓冲 | 高 |
| 数据通信 | 串口、网络、文件日志 | 高 |
| 工程化 | 高DPI适配、内存泄漏排查、异常捕获 | 高 |
这张表里面的每一项,都不是查一下文档写两行代码就能完事的。尤其是消息机制和多线程,这两块是MFC项目的分水岭,能不能吃透,直接决定了项目做到什么程度。
2. 项目整体设计与思路拆解
2.1 选型阶段先回答三个问题
正式开始写代码之前,有一个更实际的问题摆在面前:这个项目凭什么选MFC而不是别的方案。
我的回答通常取决于三个问题。第一,团队手里有没有可复用的历史代码。如果已有模块是用MFC写的,新项目直接延续是最理性的选择,重写带来的风险远大于收益。第二,目标环境是不是封闭的内网环境。很多工业设备和运营终端运行在受控内网,对运行包体积、启动速度、系统兼容性有硬性要求,MFC编译出来的程序依赖少、部署简单,这种场景下优势很明显。第三,交互复杂度是不是适合传统桌面形态。如果你的界面以表单、表格、树形列表、按钮组合为主,MFC在Windows上的表现足够稳定可靠,没必要为了"技术新潮"去引入更重的框架。
做完这三个问题的确认,再决定具体的工程结构。很多MFC项目做不好,不是MFC本身的问题,而是选型的时候没想清楚,用MFC的壳去套别的框架的思路,最后两边都不讨好。
2.2 理解MFC消息映射的底层逻辑
MFC编程绕不开消息映射。很多初学者把它当成"魔法":写一个宏,加一个函数,消息就自动来了。这是误解。消息映射的本质,是把Windows的窗口消息处理机制包装成一张静态查找表。
Windows窗口过程本身是一个巨大的switch/case,收到什么消息就处理什么消息。MFC的做法是,为每个窗口类生成一个消息表,表里的每一项都是一条记录:消息编号、消息对应的处理函数地址。当窗口收到消息时,MFC通过查表的方式找到该消息对应的处理函数,然后调用它。这样做的意义在于,你不用在一个几千行的消息处理函数里写if/else,而是把逻辑分散到各个成员函数中,代码结构更清晰。
这个设计带来的好处是直观,但代价是理解成本。你需要知道消息映射宏展开之后是什么样,否则一旦出现消息不响应、响应错乱、多个窗口共享消息处理函数,排查起来会非常痛苦。后面我在3.1小节会展开讲这个问题。
2.3 架构落地:对话框 vs 文档视图 vs 属性表
MFC提供了几种主要的界面框架模型,选错模型会让项目从一开始就拧巴。
如果界面形态是"单一功能面板",比如一个控制台、一个参数设置窗口,用对话框模型最合适。对话框本身自带消息循环、控件管理和Tab页切换能力,开发效率高,代码量也少。
如果项目有"数据+操作视图"的形态,比如一个设备列表、点进去看到详细参数、再打开历史曲线,文档/视图架构更合适。文档负责数据管理,视图负责显示,两者通过框架层同步,适合数据和界面分离的场景。不过文档视图架构的上手难度明显高于对话框,前期模板代码也多。
如果是"多步骤向导"或者"分类设置"形态,属性表(属性页集合)是更合理的选择。比如设备参数分成通信参数、运动参数、报警参数几个页签,用属性表管理最顺手。
实际项目中我还经常用到混合方案:主窗口用基于文档视图的框架,内部嵌入对话框式的控制页。这种组合不违反任何原则,关键是分清"框架层"和"内容层"的职责边界,别让对话框的逻辑侵入到文档层。
2.4 线程与UI更新设计:不能触碰的边界
工业控制面板几乎绕不开线程。采集数据的工作线程、处理通信的接收线程、控制逻辑的执行线程,这些线程最后都要把状态反馈到界面上。
MFC有一条铁律:任何窗口的UI更新,必须在UI线程中完成。这不是建议,是Windows的消息机制决定的。窗口接收消息、处理重绘、分发输入,全在创建该窗口的线程消息循环里。工作线程直接操作控件,轻则界面闪烁、失效,重则直接崩溃。
正确的做法是把工作线程的数据通过消息投递到UI线程。我通常用PostMessage而不是SendMessage来做这件事。原因很简单,PostMessage把消息放到目标线程的消息队列后就立即返回,不会阻塞工作线程;SendMessage则要等UI线程处理完消息才返回,如果UI线程正好在处理某个耗时的重绘或同步操作,工作线程就会被卡住,甚至在某些极端情况下造成死锁。用PostMessage配合自定义消息,是MFC多线程更新UI最稳妥的方案。
还有一点,工作线程不是不能有窗口,而是不要直接操作别人的窗口。工作线程如果自己创建一个隐藏窗口来做消息泵,这是合法的,但大部分场景不需要这么麻烦。工业项目里,线程和UI边界设计得好,调试时间能少一半。
3. 核心机制深度详解与实操要点
3.1 消息映射表与命令路由的实现细节
MFC的消息映射宏,核心结构是一组静态数组加查找逻辑。拿一段典型的映射代码来说:
BEGIN_MESSAGE_MAP(CDevicePanelDlg, CDialogEx) ON_WM_TIMER() ON_BN_CLICKED(IDC_BTN_START, &CDevicePanelDlg::OnBtnStart) ON_BN_CLICKED(IDC_BTN_STOP, &CDevicePanelDlg::OnBtnStop) ON_MESSAGE(WM_USER_UPDATE, &CDevicePanelDlg::OnDataUpdate) ON_COMMAND(ID_FILE_SAVECONFIG, &CDevicePanelDlg::OnSaveConfig) END_MESSAGE_MAP()每一行宏都对应消息表里的一条记录。第一列是消息标识,第二列是处理函数地址。ON_WM_TIMER是系统消息的简化写法,展开后就是"WM_TIMER消息绑定到OnTimer函数";ON_BN_CLICKED是控件通知的映射,绑定按钮点击事件;ON_MESSAGE是自定义消息的绑定方式;ON_COMMAND用于命令消息,一般配合菜单、快捷键使用。
理解了这个结构,排查消息问题就有了方向。
我举一个真实案例。一个设备面板上有十几个按钮,开发者图省事,把所有按钮的ID都指向同一个处理函数。运行的时候,其中两个按钮怎么点都没反应。排查的时候发现,那两个按钮的子窗口ID和处理函数之间被另一条映射占用了——消息映射表里每个消息标识只能绑定一个处理函数,重复绑定不会报错,但后面的条目会覆盖前面的。这种问题在编译期完全看不出来,只有用调试器断点跟进消息分发表才查得到。所以我的建议是:消息映射表尽量保持一条消息一个函数,不要为了省代码搞映射合并。
还有一点要注意,标题栏如果启用了ON_WM_NCHITTEST之类的系统消息重写,一定要在函数里调用基类版本,否则界面响应会出各种诡异问题。消息映射是"覆盖"逻辑,不是"追加"逻辑,重写之后基类处理被跳过是正常的,但很多系统消息的默认行为一旦丢掉,窗口的命中测试、缩放边框、双击标题栏等功能就全废了。
3.2 控件自定义绘制与双缓冲
工业面板的视觉效果通常和标准控件不一样,最常见的需求是给按钮加上状态指示灯、给面板区域画边框、或者做自定义的曲线显示。这些都涉及MFC的绘图机制。
MFC的绘图核心是设备上下文(DC)。所有的绘图操作都发生在DC上,你可以把它理解成一块"画布"的抽象接口。在MFC里,获取DC有几种方式:OnPaint里用CPaintDC,主动刷新用CClientDC,整个窗口范围用CWindowDC。初学者最容易混淆的是CPaintDC和CClientDC,前者的画布只在重绘期间有效,窗口重绘结束后就被销毁了,后者则在调用期间一直有效。如果搞混了,会出现"画了但看不到"的怪现象。
自绘按钮的典型场景是这样的:
void CStatusButton::OnPaint() { CPaintDC dc(this); CRect rc; GetClientRect(&rc); CDC memDC; memDC.CreateCompatibleDC(&dc); CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height()); CBitmap* pOldBmp = memDC.SelectObject(&bmp); // 先画背景 memDC.FillSolidRect(rc, GetBackColor()); // 再画状态圆点 CBrush brush(GetStatusColor()); CBrush* pOldBrush = memDC.SelectObject(&brush); memDC.Ellipse(10, 10, 30, 30); memDC.SelectObject(pOldBrush); // 最后画文字 CString text; GetWindowText(text); memDC.SetBkMode(TRANSPARENT); memDC.DrawText(text, rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); dc.BitBlt(0, 0, rc.Width(), rc.Height(), &memDC, 0, 0); memDC.SelectObject(pOldBmp); }这段代码是标准双缓冲写法。先在内存DC上画好所有内容,再一次性用BitBlt输出的屏幕DC上。这么做的好处是彻底避免闪烁。如果直接把绘图操作作用在窗口DC上,每次刷新都会先擦除再重画,高速刷新时界面就会肉眼可见地闪。而双缓冲只是把最后的合成结果呈现出来,视觉上会平滑很多。
我在自绘里踩过不少坑,其中一个和背景色有关:内存DC刚创建时默认背景色是白色,如果你不FillSolidRect,画出来的结果会带一块白底。所以每次自绘第一步就是填充背景,这个步骤不能省。另外,控件的自绘响应函数里如果使用了GetDC而不是CPaintDC,一定要成对调用ReleaseDC,否则DC会被耗尽,表现为程序运行一段时间后所有控件都画不出来了。
3.3 内存管理与泄漏排查实操
MFC程序的内存管理,是另一个容易翻车的地方。MFC在调试版本下带了一套内存诊断机制,你可以在调试输出窗口看到内存分配的编号和大小。配合分配编号,可以定位到泄漏的具体代码位置。做法是,在调试器下运行,程序退出时看到类似"memory leak detected"的输出,记录下分配编号,然后把分配编号设为断点。断点触发后,调用堆栈里就能看到泄漏发生的那行代码。
这个流程对95%的内存泄漏都有效。剩下5%的疑难杂症,问题往往出在"泄漏的不是普通内存,而是GDI对象"。GDI对象包括画刷、画笔、位图、字体、区域,它们由Windows内核管理,数量有上限。如果程序每次刷新都新建一个画刷而不销毁,运行一段时间后界面就会突然变白、控件消失,甚至整个进程崩溃。这种问题在任务管理器里看内存占用看不出来,必须靠对象计数来排查。你可以看一下进程的GDI对象数,正常情况下稳定的程序应该在几百以内,如果持续增长且不下降,那基本就是GDI泄漏。
MFC里的CBrush、CPen、CBitmap这些类,原理上在析构时会释放底层GDI对象,但前提是它们真的会被析构。最容易出问题的是:把GDI对象声明在循环内部、却忘记SelectObject回来,或者把GDI对象指针保存到成员变量里,重复赋值导致旧对象被覆盖。这里有一个很好用的习惯:任何SelectObject都要成对出现,用完立刻把旧对象选回去;任何GetDC都要在同一个作用域内ReleaseDC。这个习惯养成之后,GDI泄漏基本就和你无缘了。
4. 从零搭建一个MFC工业控制面板的完整过程
4.1 应用骨架搭建
现在进入实操环节。假设要做一个"电机参数监控面板",功能要求是:能启动和停止一个模拟的数据采集线程,能实时显示转速、电流、温度三个参数,有状态指示灯和日志显示框。这个项目麻雀虽小,但五脏俱全,足够演示MFC的关键机制。
第一步,创建项目时选择MFC应用程序,应用类型选择"基于对话框"。字符集选择Unicode字符集,这是所有新项目的默认选择,老工程如果还在用多字节字符集,建议尽早迁移。MFC的使用方式选择"在静态库中使用MFC",这样发布包不需要额外带上MFC运行库,部署到没有预装环境的设备上也不会缺DLL。
向导生成之后,你会得到一个对话框模板,里面默认有三个控件:确定按钮、取消按钮、以及一段静态文本。先把这三个控件删掉或改掉,然后在资源编辑器里重新布局:上面是三个参数显示区域,用Group Box框起来;下面左边一个状态指示灯,右边两个按钮,分别是"开始采集"和"停止采集";再下方一个多行编辑框作为日志区,设置只读属性。
对话框模板的坐标单位是对话框单位,不是像素。这一点在布局时要心里有数,别在资源编辑器里看着正好,运行起来却发现控件错位。如果你要做高DPI适配,后面对话框单位到像素的换算逻辑会格外重要。
4.2 关键代码实现与实际解析
第一个核心功能是按钮响应。开始按钮的处理函数里,需要创建采集线程。
void CDevicePanelDlg::OnBtnStart() { if (m_bRunning) { return; } m_bRunning = true; CString logText; logText.Format(_T("[%s] 开始采集数据\r\n"), GetTimestamp()); AppendLog(logText); SetStatusColor(RGB(0, 200, 0)); // 绿灯 m_pThread = AfxBeginThread( DataCollectThreadProc, this, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED ); if (m_pThread != nullptr) { m_pThread->m_bAutoDelete = FALSE; m_pThread->ResumeThread(); } }这里的DataCollectThreadProc是工作线程的入口函数,是静态函数。它是C++的普通成员函数不能作为线程入口,因为成员函数隐含了this参数。所以要么用静态函数,要么用全局函数,然后通过参数把this指针传进去。
线程函数的实现:
UINT CDevicePanelDlg::DataCollectThreadProc(LPVOID pParam) { CDevicePanelDlg* pDlg = (CDevicePanelDlg*)pParam; if (pDlg == nullptr) { return 1; } srand((unsigned int)time(nullptr)); while (pDlg->m_bRunning) { // 模拟数据采集 int speed = rand() % 1500 + 500; int current = rand() % 30 + 5; double temperature = 30.0 + (rand() % 300) / 10.0; // 把数据打包后投递到UI线程 DataPacket packet; packet.speed = speed; packet.current = current; packet.temperature = temperature; // 使用SendMessage而不是直接操作控件 ::SendMessage(pDlg->GetSafeHwnd(), WM_USER_UPDATE, 0, (LPARAM)&packet); Sleep(500); } return 0; }注意,这里用的是SendMessage而不是PostMessage。为什么不接着用PostMessage?因为packet是线程栈上的局部变量,如果用PostMessage投递指针过去,消息还没被UI线程取出处理,工作线程的下一轮循环就可能覆盖这块栈内存,造成数据错乱。用SendMessage能保证消息被UI线程处理完之后才返回,变量生命周期自然就延续到了处理结束。
这个设计还有一个细节:m_bRunning是个跨线程访问的BOOL变量,严格来说应该用原子操作或者加锁。在实际项目中我建议用std::atomic或者Windows自带的Interlocked操作来保证内存可见性,MFC老代码里大量直接用普通BOOL变量的做法,在x86上多半能跑,但在ARM或者高优化等级下会出问题。这是老代码维护里特别容易踩的坑。
自定义消息的处理函数:
LRESULT CDevicePanelDlg::OnDataUpdate(WPARAM wParam, LPARAM lParam) { DataPacket* pPacket = (DataPacket*)lParam; if (pPacket == nullptr) { return 0; } m_editSpeed.SetWindowText(FormatNum(pPacket->speed)); m_editCurrent.SetWindowText(FormatNum(pPacket->current)); m_editTemp.SetWindowText(FormatNum((int)(pPacket->temperature * 10))); return 0; }到这里,整个流程就串起来了:点击按钮,创建线程;线程每500毫秒生成一批模拟数据,通过窗口消息发送到UI线程;UI线程在处理函数里更新控件显示。
还有一个必须注意的收尾问题:停止按钮和窗口关闭按钮的逻辑。如果线程还在跑,直接关掉窗口,工作线程访问this指针就会访问到已销毁的对象,必崩。所以要在停止按钮里把m_bRunning置为false,再等待线程退出,在OnDestroy里也要再做一次同样的保护。用AfxBeginThread创建线程后如果设置m_bAutoDelete=FALSE,需要手动清理线程对象,这一步忘了就是内存泄漏。
4.3 高DPI适配与自动缩放
新写的MFC程序最高频的问题之一是高DPI下界面模糊。很多人以为这是MFC框架的先天缺陷,其实是有一个开关没打开。
默认情况下,早期MFC程序被视为"系统DPI感知",在高DPI屏幕上由系统做位图拉伸,看起来模糊。要解决这个问题,必须让程序自己按实际DPI重新布局。具体做法是:在项目属性里把"DPI感知"配置为"每监视器v2",同时用API激活进程级别的高DPI感知。配置之后,对话框的字体、控件位置、控件大小都需要按DPI比例重新计算。
一个通用的做法是在对话框初始化时读取当前DPI,计算缩放比例,然后遍历所有子窗口,按比例缩放位置和大小。前提是,在资源模板里布局时就按96DPI的标准设计好,运行时再等比缩放。这样做能覆盖绝大多数场景,控件在资源编辑器里的坐标与最终显示坐标无关,全都通过缩放矩阵转换。
这个方案有一个提醒:缩放比例不是简单的整数倍,而是实际DPI除以96。比如150%缩放下,缩放因子是1.5。在编写布局代码时,所有乘以缩放因子的计算建议保留double类型,最后再取整,否则随着嵌套缩放,误差会累积到肉眼可察觉的程度。还有,字体大小也要跟着DPI缩放,否则控件放大了字体不放大,文字会溢出。
做高DPI适配最好从头开始,项目中期再补会让你怀疑人生。因为资源编辑器里的对话框单位在缩放规则下会翻倍,按钮之间的间距、最小宽度限制、静态文本的裁剪方式都会出问题。我见过的MFC新项目,基本都是第一版就把DPI适配做进去,后续维护才轻松。
5. 常见问题与排查技巧实录
5.1 高频疑难问题速查表
把我在MFC项目里见过最多的几类问题和排查思路整理成一张表:
| 问题现象 | 可能原因 | 排查与解决方向 |
|---|---|---|
| 按钮点击没反应 | 消息映射重复/ID冲突、控件禁用、对话框被模态阻塞 | 检查消息映射表;断点进入映射分发函数 |
| 界面频繁闪烁 | 直接绘制到窗口DC、缺少双缓冲 | 改成内存DC + BitBlt,所有自绘控件统一处理 |
| 程序退出时崩溃 | 工作线程还在访问已销毁的窗口/对象 | 销毁前先停止线程,WaitForSingleObject等待结束 |
| 内存持续增长 | 创建GDI对象未释放、线程对象未清理 | 检查GDI对象计数;检查m_bAutoDelete设置 |
| 窗口显示错位 | 高DPI未适配、对话框单位误用 | 启用DPI感知,按缩放因子重新布局 |
| 中文乱码 | 字符集不一致、资源文件编码不对 | 统一使用Unicode,资源文件另存为带签名UTF-8 |
| 自绘文字背景是白块 | 未执行SetBkMode(TRANSPARENT) | 绘制文字前设置透明背景模式 |
这张表覆盖了MFC项目日常开发中70%以上的"灵异事件"。剩下30%的问题通常要借助工具才能解决。
5.2 调试与排查工具链
MFC的调试,我有一套固定的组合打法。
第一招是看调试输出窗口。MFC在调试构建下有丰富的诊断输出,包括内存泄漏报告、断言失败信息、消息映射相关警告。内存泄漏报告里包含分配编号和内存块大小,结合_CrtSetBreakAlloc这个函数设置断点,就能抓到泄漏位置。具体用法:在程序初始化时调用_CrtSetBreakAlloc(编号),运行到该编号对应的内存分配处时调试器会自动中断,调用堆栈定位到代码行。
第二招是系统自带的应用程序验证器。它可以检测到许多隐蔽错误,比如句柄泄漏、内存无效访问、可重入性问题。MFC程序在旧机器上行为诡异、偶尔崩溃、运行一段时间后无响应,用验证器通常能抓到真实的违规调用。
第三招是进程和句柄监控。程序界面卡死时,先看CPU占用率和UI线程的调用堆栈。如果UI线程在等待某个锁、某个SendMessage返回,基本就能定位是死锁还是跨线程阻塞。看堆栈是排查卡死问题的第一步,很多新手上来就改代码,其实应该先挂上调试器看让UI线程卡住的函数是哪个。
第四招,自己写日志。在MFC项目里,我习惯在关键入口和异常捕获处加日志,线程入口打一行,错误分支打一行,消息处理函数进出打一行。发布版本不需要这些,但Debug版本保留着能节省大量排查时间。曾经有个设备面板偶发卡顿,最后就是靠消息处理函数的进出日志定位到是某个自定义控件的OnMouseMove里做了耗时计算。
5.3 实操心得:程序员最容易忽略的几个细节
文章最后,分享几个我在MFC项目里反复用到的实操经验。
第一,控件变量和控件ID之间的关系要在DoDataExchange里维护好。对话框的控件变量失效、切换页面后数据没刷新,很多问题都出在DDX机制上。如果你手动创建控件而不是走对话框模板,一定要自己管理ID和指针的对应关系。
第二,不要在生产代码里写三目运算符套三目运算符。MFC框架本身历史悠久,你用现代C++特性可以,但没必要在回调函数里炫技,简洁可读更重要。特别是公共的回调路径上,一行复杂表达式比一百行简单代码难调得多。
第三,接手老项目时,先把编译警告清干净。我见过太多MFC老项目编译时几千个警告视而不见,这些警告里很可能藏着真实问题的线索。把警告当提示而不是噪音,会帮你少踩很多坑。
第四,给程序做一个统一入口的异常捕获。MFC程序不比Web服务,挂了就是整个窗口消失。我习惯在应用类的InitInstance里包一层try/catch,在线程入口函数里也加上。工业设备上没人天天盯着日志,程序崩了还能留下一条完整的错误记录,比什么都强。
最后一个经验,和代码无关但比代码重要。接手MFC老项目的时候,先别急着重构,先完整梳理一遍现有的消息路由、线程模型和数据流。画一张纸上的逻辑图,把每个窗口的职责、线程的归属、数据在各模块间的走向标注清楚。我吃过的亏是:上来就改代码,结果被原有架构里"牵一发而动全身"的关系坑到怀疑人生。MFC代码虽然老,但它的框架结构是有规律的,只要你按规律走,它依旧能给出非常稳定可靠的表现。