Windows 的 MFC 是什么 —— 维护现有资产所需的基础知识
· 小村 豪 · Windows, MFC, VisualC++, Cpp, Win32, NativeApp, DesktopApp, LegacyCode, 现有资产利用
1. 首先需要掌握的内容
在维护 Windows 老旧桌面应用程序时,经常会遇到下面这些名字。
CWinApp
CWnd
CDialog
CDialogEx
CFrameWnd
CDocument
CView
CString
CFile
CArchive
BEGIN_MESSAGE_MAP
ON_COMMAND
ON_BN_CLICKED
DoDataExchange
UpdateData
这些名字经常出现在一个名为 MFC 的 C++ Windows 应用程序框架中。MFC 是 Microsoft Foundation Classes 的缩写,是一个让 Win32 API 更容易以 C++ 类的形式使用的库。
在如今的 Windows 应用开发中,WinUI、WPF、Windows Forms、Electron、Qt、Web 技术等选择越来越多,把 MFC 作为新项目首选的场景已经减少。但它并不是已经消失的技术。在业务应用程序、测量设备、控制软件、CAD/CAM、内部工具,以及历史悠久的套装软件中,仍然有不少场景需要维护 MFC 代码库。
在理解 MFC 之前,先列出几个应该具备的视角。
MFC是解读老旧Windows桌面应用的重要地图
不要把MFC当成隐藏Win32 API的黑箱,而应视为对其做了C++风格的封装
不了解MFC的作法,就很容易比表面代码看到的还严重地误读程序行为
MFC的价值更多体现在维护、延续和逐步迁移现有资产,而非新项目采用
本文将整理 MFC 的概要、应用程序结构、消息映射、Document/View、对话框、DDX/DDV、资源、构建,以及维护时的注意事项。
另外,本文中出现的代码片段,已按章节整理为文件并作为参考代码集发布在 GitHub 上。
windows-mfc-overview - komurasoft-blog-samples (GitHub)
2. 什么是 MFC
MFC 是一套用于以 C++ 编写 Windows 原生桌面应用程序的类库。
如果直接使用 Win32 API,通常会写出这样的代码。
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_PAINT:
// 绘制处理
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
Win32 API 非常强大,但它以 C 函数、句柄、消息和回调为核心构建,在大型应用程序中很容易变得难以把握全貌。MFC 让这些内容能够以 C++ 类的形式来处理:窗口用 CWnd 表示,对话框用 CDialog 表示,整个应用程序用 CWinApp 表示,框架窗口用 CFrameWnd 表示,视图用 CView 表示。
class CMainFrame : public CFrameWnd
{
public:
CMainFrame();
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
DECLARE_MESSAGE_MAP()
};
MFC 并不是把 Win32 API 完全替换成另一套东西,而是一个以 Win32 API 的思路为基础、虽然轻薄但覆盖面很广的 C++ 框架。因此,要读懂 MFC,不能只了解 MFC 的类,还需要具备下面这些方面的知识。
Windows消息
HWND等句柄
GDI/GDI+
资源文件
COM/OLE
DLL与运行时
字符编码
线程与消息循环
与其说 MFC 是「不了解 Windows 也能写代码的魔法库」,不如说它的真实面貌是「把 Windows 的机制用 C++ 的类型和框架加以整理」。
3. MFC 现在还能用吗
MFC 目前仍可以在 Visual Studio 中使用。不过,不能误解它的定位。虽然目前仍在获得支持,但它并不是那种会持续积极添加新功能的最新 UI 框架;微软的 MFC 文档中也说明,MFC 会继续得到支持,但不会再新增功能,也不会再更新文档。
因此,MFC 的定位大致可以概括如下。
维护现有MFC应用 -> 现实中很常见
为现有MFC应用追加功能 -> 是可行的选择
更新MFC应用的构建环境 -> 非常重要
从MFC逐步迁移到其他UI技术 -> 是可行的选择
在全新的普通GUI应用中采用 -> 需要谨慎判断
尤其是在长期使用的业务应用程序中,UI、打印、文件输入输出、设备控制、自定义协议、COM 集成等功能常常都汇集在 MFC 之中。
对于这类代码库,与「抛弃 MFC」相比,更应该先做到「能够读懂 MFC」。
4. MFC 擅长的领域
MFC 一直被使用的代表性领域,是 Windows 的原生桌面应用程序。
具体来说,包括以下这些应用程序。
以对话框为中心的业务工具
打开文件进行编辑的SDI应用
处理多个文档的MDI应用
测量设备或制造装置的控制画面
CAD/CAM类原生应用
大量使用打印或预览的应用
包含ActiveX或OLE集成的应用
与老旧Windows API或COM资产紧密结合的应用
MFC 的优势在于,它能在贴近 Windows 原生组件的层面上运行。窗口、菜单、工具栏、状态栏、对话框、通用控件、打印、文件对话框、注册表、GDI 绘制等,都可以作为 C++ 类来处理。
另一方面,MFC 的弱点是,现代化的 UI 构建、数据绑定、可测试性、异步处理、现代布局、高 DPI 支持、多语言化、无障碍支持等,都不如近年出现的框架那样自然易写。
把特点列出来,大致如下。
贴近Windows原生
可以用C++直接控制
拥有大量现有资产
需要Win32相关知识
残留大量旧式写法
需要自行调整才能形成易于测试的结构
5. 在 Visual Studio 中准备使用 MFC
即使在 Visual Studio 中安装了 C++,也不一定就自动装上了 MFC。因为 MFC 是作为 Visual Studio Installer 的单个组件来处理的。通常需要确认以下这些组件。
使用C++的桌面开发
MSVC v143 - VS 2022 C++ x64/x86 build tools
Windows SDK
C++ MFC for latest v143 build tools
C++ ATL for latest v143 build tools
是否需要Spectre Mitigations版的MFC
如果在构建时找不到 MFC 相关文件,除了检查项目设置外,还要确认 Visual Studio 是否安装了 MFC 组件。
CI 环境或构建服务器上也是同理:如果本机的 Visual Studio 能够构建,但 CI 却失败,原因可能是 MFC 组件缺失,或目标工具集版本不一致。
6. MFC 应用程序的基本结构
MFC 应用程序大致具有以下结构。
CWinApp派生类
负责整个应用程序的初始化与结束
CFrameWnd / CMDIFrameWnd / CDialog派生类
负责主窗口或对话框
CView派生类
负责画面显示与用户操作
CDocument派生类
负责数据与文件保存
资源文件
保存菜单、对话框、图标、字符串等
消息映射
将Windows消息或命令关联到处理函数
例如,在一个简单的 MFC 应用中,会出现这样的 CWinApp 派生类。
class CMyApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
CMyApp theApp;
BOOL CMyApp::InitInstance()
{
CWinApp::InitInstance();
CMainFrame* pFrame = new CMainFrame;
m_pMainWnd = pFrame;
pFrame->Create(nullptr, _T("My MFC Application"));
pFrame->ShowWindow(SW_SHOW);
pFrame->UpdateWindow();
return TRUE;
}
CWinApp 是代表整个应用程序的类。
在 MFC 应用程序中,通常存在一个从 CWinApp 派生出来的对象。
像 theApp 这样的全局对象,第一次看到可能会觉得不太自然,但在 MFC 中这是标准的结构。
7. CWinApp 在做什么
CWinApp 是 MFC 应用程序入口处的重要角色。在普通的 Win32 应用程序中,需要自己编写 WinMain、窗口类注册、消息循环等内容,而在 MFC 中,这些大多由框架承担。开发者主要通过重写 InitInstance 来编写应用程序特有的初始化逻辑。
BOOL CMyApp::InitInstance()
{
CWinApp::InitInstance();
// 加载设置
// 初始化COM
// 创建主窗口
// 注册文档模板
return TRUE;
}
InitInstance 中常见的处理内容如下。
初始化通用控件
设置注册表键
读取最近使用的文件列表
注册文档模板
创建主框架
处理命令行参数
初始化COM/OLE
维护时,先确认 CWinApp 派生类,可以更容易看清整个应用程序的启动顺序。
8. CWnd 是 MFC 的核心类
MFC 中许多 UI 类,都以代表 Windows 窗口的 CWnd 为基类。不过,CWnd 对象和 HWND 并不是同一个东西。
HWND
Windows操作系统管理的窗口句柄
CWnd
用于方便操作HWND的C++封装对象
在 MFC 中,CWnd 内部持有一个 HWND。
HWND hWnd = m_hWnd;
或者,也可以这样获取。
HWND hWnd = GetSafeHwnd();
维护时需要特别注意的是,即使 CWnd* 对象存在,对应的 HWND 也可能已经被销毁。
因此,判断窗口是否有效时,通常会这样写。
if (pWnd != nullptr && ::IsWindow(pWnd->GetSafeHwnd()))
{
pWnd->ShowWindow(SW_SHOW);
}
在排查 MFC 的 bug 时,重要的是要检查 CWnd 这个 C++ 对象的生命周期,与实际 Windows 窗口句柄的生命周期是否发生了错位。
9. 什么是消息映射
最能体现 MFC 特色的机制之一,就是消息映射。
Windows 应用程序会以 Windows 消息的形式接收鼠标点击、按键输入、重绘、窗口尺寸变化、菜单选择等事件。
在 Win32 API 中,通常在 WndProc 的 switch 语句中处理消息。
在 MFC 中,则通过消息映射把这些消息关联到处理函数上。
BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
ON_BN_CLICKED(IDC_BUTTON_OK, &CMyDialog::OnClickedButtonOk)
ON_WM_CLOSE()
END_MESSAGE_MAP()
void CMyDialog::OnClickedButtonOk()
{
AfxMessageBox(_T("Clicked"));
}
这段代码的含义是这样的。
名为IDC_BUTTON_OK的按钮被点击时
调用CMyDialog::OnClickedButtonOk
不熟悉 MFC 的人往往很难看出某个函数是从哪里被调用的。如果搜索代码却找不到直接调用,就应该去看消息映射。
函数没有被直接调用
但事件发生时确实会执行
-> 需要确认BEGIN_MESSAGE_MAP / ON_...宏
在 MFC 的代码审查中,不能只看处理函数本身,而应该结合消息映射一起确认,这一点非常重要。
10. 命令路由
在 MFC 中,菜单和工具栏的操作也会被当作命令来处理。
其中最具代表性的就是 ON_COMMAND。
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)
END_MESSAGE_MAP()
void CMainFrame::OnFileOpen()
{
// 打开文件的处理
}
MFC 内置了将命令派发给合适对象的机制。
例如,同样是 ID_EDIT_COPY,实际由当前活动的视图、文档、框架还是应用程序来处理,情况可能会不同。
活动视图
文档
框架窗口
应用程序
命令会按照这样的顺序,传递给能够处理它的对象。
因此,在 MFC 中,「点击菜单后会调用哪个函数」有时很难仅凭简单的字符串搜索来追踪。
维护时需要关注的要点如下。
命令ID是什么
ON_COMMAND位于哪个类中
ON_UPDATE_COMMAND_UI位于哪里
当前活动的视图是哪一个
是否使用了Document/View结构
11. ON_UPDATE_COMMAND_UI 是什么
在 MFC 中,有时会使用 ON_UPDATE_COMMAND_UI 来更新菜单项或工具栏按钮的启用/禁用状态、选中状态、显示文本等。
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_COMMAND(ID_EDIT_DELETE, &CMainFrame::OnEditDelete)
ON_UPDATE_COMMAND_UI(ID_EDIT_DELETE, &CMainFrame::OnUpdateEditDelete)
END_MESSAGE_MAP()
void CMainFrame::OnUpdateEditDelete(CCmdUI* pCmdUI)
{
pCmdUI->Enable(CanDeleteCurrentItem());
}
这样一来,就可以只在可以删除的状态下才启用相应的菜单或按钮。
在使用 MFC 应用程序时,如果遇到按钮莫名其妙变灰、菜单无法点击、选中状态发生变化等现象,去查找 ON_UPDATE_COMMAND_UI 往往能够找到原因。
12. 基于对话框的 MFC 应用
MFC 中最容易理解的一种形式,就是基于对话框的应用程序。
设置界面、简单的业务工具、设备操作界面等,有时会以对话框为中心构成。
典型情况下,会继承 CDialog 或 CDialogEx。
class CSettingsDialog : public CDialogEx
{
public:
CSettingsDialog(CWnd* pParent = nullptr);
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_SETTINGS_DIALOG };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
afx_msg void OnBnClickedOk();
DECLARE_MESSAGE_MAP()
private:
CString m_name;
int m_interval;
};
在基于对话框的代码中,经常会出现以下这些要素。
IDD_... 对话框资源ID
IDC_... 控件ID
OnInitDialog 初始化处理
DoDataExchange 控件与成员变量的关联
UpdateData 画面与变量的同步
ON_BN_CLICKED 按钮点击处理
对话框并不只是外观,而是由资源、成员变量、消息映射、初始化处理组合在一起共同运作的。
13. DDX 与 DDV
在 MFC 的对话框中经常出现的,是 DDX 与 DDV。
DDX = Dialog Data Exchange
DDV = Dialog Data Validation
DDX 是把对话框上的控件与 C++ 成员变量对应起来的机制,DDV 则是验证输入值的机制。
void CSettingsDialog::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT_NAME, m_name);
DDX_Text(pDX, IDC_EDIT_INTERVAL, m_interval);
DDV_MinMaxInt(pDX, m_interval, 1, 3600);
}
调用 UpdateData(TRUE) 后,界面上的输入值会反映到成员变量中。
void CSettingsDialog::OnBnClickedOk()
{
if (!UpdateData(TRUE))
{
return;
}
// 此时m_name和m_interval中已经是画面输入的值
SaveSettings(m_name, m_interval);
CDialogEx::OnOK();
}
反过来,调用 UpdateData(FALSE) 后,成员变量的值会反映到界面上。
BOOL CSettingsDialog::OnInitDialog()
{
CDialogEx::OnInitDialog();
m_name = _T("default");
m_interval = 60;
UpdateData(FALSE);
return TRUE;
}
当 MFC 对话框的输入值出现异常时,可以按以下几点进行确认。
DoDataExchange中是否定义了DDX
是否调用了UpdateData(TRUE)
UpdateData(FALSE)的调用时机是否正确
DDV是否拒绝了输入
控件ID与资源是否一致
14. Document/View 架构
MFC 的一大特色,就是 Document/View 架构。
这是一种把应用程序处理的数据与其显示方式分离开来的结构。
CDocument
保存数据
负责文件的读写
向多个视图发出更新通知
CView
显示数据
处理用户操作
管理绘制与选中状态
例如,在文本编辑器、图形编辑器、配置文件编辑工具、类似 CAD 的应用中,把数据与显示分离是有意义的。
class CMyDocument : public CDocument
{
public:
std::vector<Item> m_items;
virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);
};
class CMyView : public CView
{
protected:
virtual void OnDraw(CDC* pDC);
CMyDocument* GetDocument() const;
};
视图一侧会获取文档并进行绘制。
void CMyView::OnDraw(CDC* pDC)
{
CMyDocument* pDoc = GetDocument();
if (pDoc == nullptr)
{
return;
}
for (const auto& item : pDoc->m_items)
{
// 使用pDC进行绘制
}
}
Document/View 的优点在于,同一份数据可以方便地展示在多个视图中。
例如,同一份数据可以用以下方式来呈现。
表格视图
图表视图
详细信息视图
预览
打印视图
不过,如果在简单的设置界面或小工具中使用 Document/View,反而会让结构显得沉重。
维护时,先判断这个应用是否使用了 Document/View,还是以对话框为中心,可以让代码更容易追踪。
15. SDI 与 MDI
在 MFC 中,经常会与 Document/View 组合出现的,还有 SDI 和 MDI 这两种结构。
SDI = Single Document Interface
MDI = Multiple Document Interface
SDI 基本上是用一个框架处理一个文档的形式。
主窗口
1个文档
1个或多个视图
MDI 则是在一个父窗口中包含多个子窗口,每个子窗口分别处理一个文档。
MDI父框架
MDI子框架1 -> 文档1
MDI子框架2 -> 文档2
MDI子框架3 -> 文档3
在老旧的 Windows 应用程序中,MDI 曾经被广泛使用。
维护时,只需看类名就能大致判断结构。
CFrameWnd SDI类框架
CMDIFrameWnd MDI父框架
CMDIChildWnd MDI子框架
CSingleDocTemplate SDI用文档模板
CMultiDocTemplate MDI用文档模板
在由 MFC 向导生成的应用中,InitInstance 里通常会有 CSingleDocTemplate 或 CMultiDocTemplate 的注册处理。
16. 理解资源文件
在 MFC 应用中,.rc 文件非常重要。
.rc 是 Windows 的资源文件。
其中定义了以下这些内容。
对话框模板
菜单
加速键
图标
位图
字符串表
版本信息
工具栏
此外,resource.h 中定义了资源 ID。
#define IDD_SETTINGS_DIALOG 101
#define IDC_EDIT_NAME 1001
#define IDC_EDIT_INTERVAL 1002
#define ID_FILE_OPEN 32771
MFC 的代码通过这些 ID,把资源和 C++ 代码关联起来。
DDX_Text(pDX, IDC_EDIT_NAME, m_name);
ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)
维护时常见的问题是资源 ID 不一致。
resource.h中的ID发生了变化
不同分支合并时ID发生冲突
对话框上控件的ID与DDX的ID不一致
本应删除的菜单ID仍然残留
字符串表的ID出现重复
调查 MFC 应用的行为时,不能只看 C++ 代码,还需要同时查看 .rc 和 resource.h。
17. Class Wizard 与手写代码
MFC 与 Visual Studio 的 Class Wizard 有着很深的历史渊源。
使用 Class Wizard,可以自动生成消息处理程序、DDX 变量、虚函数重写等内容。
因此,MFC 代码中往往残留着大量由工具生成的代码形式。
//{{AFX_DATA(CSettingsDialog)
//}}AFX_DATA
//{{AFX_MSG(CSettingsDialog)
//}}AFX_MSG
在较新的 Visual Studio 中,界面表现和生成的代码形式可能有所变化,但在老旧的代码库中,仍会残留这类注释标记。
维护时重要的是,不要粗暴地破坏生成代码与手写代码之间的边界。
不要删除消息映射
不要破坏DDX的对应关系
不要随意更改资源ID
不要轻易删除旧Class Wizard相关的注释
在 MFC 中,仅仅让 C++ 代码能够编译通过是不够的,还需要在一定程度上保持 Visual Studio 资源编辑器和 Class Wizard 所期望的形式。
18. CString 与字符串
在 MFC 中频繁出现的字符串类是 CString。
CString name = _T("Komura");
CString message;
message.Format(_T("Hello, %s"), name.GetString());
CString 是 MFC/ATL 系代码中常用的可变长度字符串类。现代 C++ 中更多使用 std::string 或 std::wstring,但在 MFC 中,出于与 API 及控件的兼容性考虑,CString 被大量使用。
维护时需要注意字符编码问题。
CString 根据项目设置对应CStringA或CStringW
CStringA ANSI / MBCS系
CStringW Unicode / UTF-16系
LPCTSTR 基于TCHAR的字符串指针
LPCSTR char系
LPCWSTR wchar_t系
std::string 通常是char系
std::wstring wchar_t系
目前的 Windows 应用基本上以 Unicode 为前提会更安全,但在老旧的 MFC 应用中,仍可能残留以 MBCS 为前提的处理逻辑。
CString text = _T("中文");
std::wstring ws(text.GetString());
字符串转换引发的 bug,在 MFC 应用的维护中很常见。
尤其要注意以下这些情况。
读取以Shift_JIS为前提的文件
切换到Unicode构建
外部DLL要求char*
COM要求BSTR
安易地转换为std::string导致乱码
看到 CString 时,不应该只把它当作「一个老旧的字符串类」,而应该结合项目的字符集设置、外部 API、文件格式一起确认,这一点很重要。
19. CFile 与 CArchive
MFC 中还提供了用于文件操作和序列化的类,代表性的是 CFile 与 CArchive。
CFile file;
if (file.Open(path, CFile::modeRead))
{
CArchive ar(&file, CArchive::load);
// 从ar中读取
}
CArchive 常用于 MFC 的序列化机制中。
在 CDocument 派生类中,有时会重写 Serialize,把读取和保存写在同一个函数里。
void CMyDocument::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar << m_title;
ar << static_cast<int>(m_items.size());
for (const auto& item : m_items)
{
ar << item.Name;
ar << item.Value;
}
}
else
{
int count = 0;
ar >> m_title;
ar >> count;
m_items.clear();
for (int i = 0; i < count; ++i)
{
Item item;
ar >> item.Name;
ar >> item.Value;
m_items.push_back(item);
}
}
}
MFC 的序列化机制很方便,但在长期运行中需要注意以下几点。
与旧文件格式的兼容性
版本号的管理
读取失败时的恢复
异常处理
字符编码
字节序
是否直接保存了结构体本身
对于长期使用自有二进制格式的 MFC 应用,Serialize 有时实际上就等同于文件规格本身。
在这种情况下,修改代码之前,务必先准备好用于读取现有文件的测试数据。
20. GDI 绘制与 CDC
在 MFC 的画面绘制中,经常使用用于处理 Windows Device Context 的 CDC 类。在 CView::OnDraw 中,会以参数形式传入 CDC*。
void CMyView::OnDraw(CDC* pDC)
{
pDC->TextOut(10, 10, _T("Hello MFC"));
pDC->Rectangle(10, 40, 200, 120);
}
使用画笔或画刷时,需要注意选择与恢复。
void CMyView::OnDraw(CDC* pDC)
{
CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
CPen* pOldPen = pDC->SelectObject(&pen);
pDC->MoveTo(10, 10);
pDC->LineTo(100, 100);
pDC->SelectObject(pOldPen);
}
在 GDI 对象方面,以下这些失误往往会引发问题。
SelectObject之后没有恢复为原来的对象
创建了大量GDI对象却没有释放
混淆了OnPaint与OnDraw的职责
没有做双缓冲导致闪烁
高DPI下以固定像素为前提的绘制发生错位
在排查 MFC 的绘制 bug 时,不能只看 C++ 逻辑,还需要确认 Windows 的 GDI 资源、重绘时机、DPI、字体大小等因素。
21. 模态对话框与非模态对话框
在 MFC 中,对话框的显示方式也需要注意。
模态对话框通过 DoModal 来显示。
CSettingsDialog dlg(this);
if (dlg.DoModal() == IDOK)
{
// OK时的处理
}
在这种情况下,调用方会一直等待,直到对话框关闭为止。
而非模态对话框在创建之后,调用方的处理会立即返回。
m_pToolDialog = new CToolDialog(this);
m_pToolDialog->Create(IDD_TOOL_DIALOG, this);
m_pToolDialog->ShowWindow(SW_SHOW);
对于非模态对话框,生命周期管理非常重要。
new出来的对话框应该何时delete
父窗口是否会先被销毁
对话框内部是否使用了PostNcDestroy
是否会重复创建
关闭后指针是否仍然残留
在排查 MFC 崩溃问题时,非模态对话框的生命周期问题有时正是原因所在。
22. C++ 对象与 Windows 句柄的生命周期
MFC 中一个非常重要的概念,就是 C++ 对象与 Windows 句柄生命周期的差异。例如 CWnd 是一个 C++ 对象,而实际的窗口则是由 Windows 以 HWND 的形式来管理的,这两者并不总是同时创建、同时销毁。
CWnd对象存在但HWND尚未创建
HWND已销毁但CWnd对象仍然残留
创建了临时的CWnd包装对象
通过Attach/Detach更换句柄
例如,下面这样的代码就需要格外小心。
CWnd* pWnd = GetDlgItem(IDC_SOME_CONTROL);
// 把pWnd保存到成员变量中,之后再使用
如果长期持有通过 GetDlgItem 获得的指针,就存在窗口销毁后仍被引用的风险。
如果需要,可以每次都重新调用 GetDlgItem,或者用 DDX 来管理控件对应的成员变量,这样会更安全。
DDX_Control(pDX, IDC_LIST_ITEMS, m_listItems);
在 MFC 中,指针非空并不代表就是安全的。
if (m_pDialog != nullptr && ::IsWindow(m_pDialog->GetSafeHwnd()))
{
m_pDialog->SetWindowText(_T("Running"));
}
这种意识在维护 MFC 时非常重要。
23. 线程与 UI 更新
Windows 的 UI 基本上必须在创建它的 UI 线程中操作,MFC 应用同样如此。如果从工作线程直接操作 UI 控件,可能会导致不稳定的行为甚至崩溃。
应该避免的写法如下。
UINT WorkerThreadProc(LPVOID pParam)
{
CMyDialog* pDlg = static_cast<CMyDialog*>(pParam);
// 避免从工作线程直接操作UI
pDlg->SetDlgItemText(IDC_STATUS, _T("Done"));
return 0;
}
一般会通过 PostMessage 等方式通知 UI 线程。
constexpr UINT WM_APP_WORK_DONE = WM_APP + 1;
UINT WorkerThreadProc(LPVOID pParam)
{
HWND hWnd = static_cast<HWND>(pParam);
// 耗时处理
::PostMessage(hWnd, WM_APP_WORK_DONE, 0, 0);
return 0;
}
在 UI 一侧,则通过消息映射来接收。
BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
ON_MESSAGE(WM_APP_WORK_DONE, &CMyDialog::OnWorkDone)
END_MESSAGE_MAP()
LRESULT CMyDialog::OnWorkDone(WPARAM, LPARAM)
{
SetDlgItemText(IDC_STATUS, _T("Done"));
return 0;
}
在 MFC 的线程相关问题上,需要确认以下几点。
是否有从工作线程直接操作UI
窗口销毁后是否仍在PostMessage
等待线程结束时是否阻塞了UI线程
共享数据的锁是否恰当
是否误解了AfxBeginThread的返回值与生命周期
24. MFC DLL 与模块状态
在使用 MFC 制作 DLL 时,会遇到模块状态这个概念。
在从 MFC DLL 加载资源、显示对话框、制作扩展 DLL 等场景中,「应该去哪个模块查找资源」会成为一个问题。
在 MFC DLL 函数的入口处,经常会看到这样的宏。
AFX_MANAGE_STATE(AfxGetStaticModuleState());
这是为了让 MFC 能够使用正确的模块状态。
如果忘记写这个宏,就会引发如下问题。
找不到DLL内的对话框资源
字符串资源从另一个模块中被读取
找不到图标或菜单
只在调试时正常运行,发布版本却出错
维护 MFC DLL 时,需要理清 EXE、常规 DLL、扩展 DLL、资源 DLL 之间的关系。
尤其是在从非 MFC 应用调用 MFC DLL,或者采用插件结构的情况下,需要格外注意。
25. MFC 应该静态链接还是使用共享 DLL
在 MFC 应用中,项目设置里有一项「Use of MFC」。
代表性的选项有以下两种。
Use MFC in a Shared DLL
Use MFC in a Static Library
如果使用共享 DLL,运行环境中需要对应的 MFC 运行时和 Visual C++ 运行时。
如果采用静态链接,发行物看起来会更简单,但需要考虑可执行文件的体积、更新方式、安全修复的接入、许可与再分发条款等问题。
并不存在哪一种方式永远正确的答案。
判断依据大致包括以下几点。
是否可以在分发环境中安装Visual C++ Redistributable
是否希望应用尽量接近单一exe
如何应对安全更新
是否有多个应用共享同一份运行时
是否能够准备安装程序
目标Windows版本是什么
维护时,首先要确认当前的设置。
Configuration Properties
General
Use of MFC
此外,还需要查看 Runtime Library 的设置。
/MD Multi-threaded DLL
/MDd Multi-threaded Debug DLL
/MT Multi-threaded
/MTd Multi-threaded Debug
如果 MFC 与 CRT 的链接设置混用不当,在库的边界上可能会出现内存分配/释放方面的问题。
26. Unicode、MBCS 与 TCHAR
在老旧的 MFC 代码中,经常会出现 TCHAR、LPCTSTR、_T() 宏。
CString title = _T("设置");
SetWindowText(title);
这是一种为了同时兼容 Unicode 构建和 MBCS 构建而采用的写法。
Unicode构建
TCHAR -> wchar_t
LPCTSTR -> const wchar_t*
_T("...") -> L"..."
MBCS构建
TCHAR -> char
LPCTSTR -> const char*
_T("...") -> "..."
目前 Unicode 构建已经是主流,但在老旧应用中,仍可能残留以 MBCS 为前提的处理逻辑。
尤其是在外部文件、通信协议、老旧 DLL、数据库连接、串口通信等场景中,需要确认字符编码的前提假设。
需要注意的是,不应该把 Unicode 化简单地当成一次替换作业。
char数组的大小是字节数还是字符数
是否使用了strlen
是否把sizeof(buffer)当作字符数使用
外部API接收的是UTF-16还是Shift_JIS
文件保存格式是否允许变更
修改 MFC 的字符串相关代码时,不能只关注画面显示,还需要确认文件兼容性以及外部集成部分。
27. MFC 与 COM/OLE/ActiveX
MFC 也曾在与 COM、OLE、ActiveX 关系密切的应用程序中被使用。
在老旧的业务应用中,可能仍残留以下这些元素。
OLE Automation
ActiveX Control
COM服务器
COM客户端
IDispatch
BSTR
VARIANT
COleDispatchDriver
COleVariant
在 MFC 应用的启动处理中,有时会出现这样的代码。
if (!AfxOleInit())
{
AfxMessageBox(_T("OLE initialization failed"));
return FALSE;
}
在使用 COM/OLE 的情况下,看起来像是 MFC 的问题,实际上往往是 COM 的初始化、线程模型、引用计数、注册信息,以及 32 位/64 位差异所导致的。
尤其需要注意 32 位的 ActiveX 或 COM 组件。
32位MFC应用要使用32位COM
64位MFC应用要使用64位COM
32位/64位的COM注册是分开的
老旧ActiveX有时并不支持64位
将 MFC 应用迁移到 x64 时,不仅要检查 UI 代码,还必须确认 COM/OLE 相关依赖。
28. 高 DPI 支持与现代 Windows
在现代 Windows 上运行老旧的 MFC 应用时,在高 DPI 环境下画面可能会出现错位。
例如,会出现下面这些问题。
文字被截断
按钮过小
固定像素绘制发生偏移
多台显示器缩放比例不同时布局错乱
旧位图变得模糊
对话框布局挤在一起
在 Windows 桌面应用中,应用程序需要明确声明其 DPI 适配模式。
MFC 应用也需要确认清单文件、资源、绘制代码、字体、布局等内容。
尤其是像下面这样直接写死固定坐标的代码,在高 DPI 下很容易出问题。
pDC->TextOut(10, 10, _T("Status"));
pDC->Rectangle(10, 40, 200, 80);
以固定像素为前提的坐标,在 DPI 发生变化时,外观会发生错位。
维护时的确认要点如下。
应用程序清单中的DPI设置
对话框资源的字体
固定像素绘制
图像资源的分辨率
多显示器下的表现
在Windows 10 / Windows 11上的显示效果
MFC 的高 DPI 适配,不一定只靠修改项目设置就能完成。对于老旧界面,往往需要实际确认画面效果并修正布局。
29. 异常处理与错误处理
MFC 有一套独特的异常类与错误处理方式。
在老旧代码中,常常能看到这样的宏。
TRY
{
// 处理
}
CATCH(CFileException, e)
{
e->ReportError();
}
END_CATCH
也有一些代码与现代 C++ 的 try / catch 混用。
try
{
DoSomething();
}
catch (const std::exception& ex)
{
// 输出日志
}
在维护 MFC 时需要注意以下几点。
MFC异常与C++标准异常是否混用
是否误解了异常对象的生命周期
是否理解老旧的THROW/CATCH宏
返回值错误与异常是否混用
是否存在只用AfxMessageBox而没有留下日志的情况
在业务应用中,重要的不仅是在画面上显示错误信息,还要保留日志、操作记录、输入值、外部连接状态等信息。
在老旧的 MFC 应用中,有时错误处理仅止步于 AfxMessageBox。
AfxMessageBox(_T("保存失败"));
如果想提升可维护性,最好把界面显示和日志记录分开处理。
LogError(_T("Save failed"), path);
AfxMessageBox(_T("保存失败,请查看日志。"));
30. 如何让 MFC 与现代 C++ 共存
即使在维护 MFC 应用,也不必把所有代码都改成老旧的 C++ 写法。
UI 层可以尊重 MFC 的作法,而领域逻辑和计算处理则可以用现代 C++ 来整理。
例如,可以这样划分。
MFC层
CDialog
CView
CDocument
CString
消息映射
资源操作
非MFC层
std::string / std::wstring
std::vector
std::optional
std::variant
std::filesystem
可进行单元测试的类
业务逻辑
不好的形式是,所有处理都被塞进了对话框类中。
void CMainDialog::OnBnClickedExecute()
{
// 获取输入
// 读取文件
// 通信
// 计算
// 更新数据库
// 更新画面
// 输出日志
// 异常处理
}
这样的代码很难修改,也很难测试,排查 bug 也会变得困难。
要改善这一点,可以把逻辑从 MFC 类中抽取出来。
void CMainDialog::OnBnClickedExecute()
{
if (!UpdateData(TRUE))
{
return;
}
ExecuteRequest request;
request.Name = ToStdWString(m_name);
request.Interval = m_interval;
ExecuteResult result = m_service.Execute(request);
m_status = ToCString(result.Message);
UpdateData(FALSE);
}
这样一来,m_service.Execute 就可以在没有 MFC 的情况下进行测试。
在维护 MFC 现有资产时,最有效的改进方式,就是把逻辑从 UI 类中一点点抽取出来。
31. 让 MFC 代码更易于测试
MFC 应用如果保持原样,往往很难进行单元测试。
原因在于 UI、Win32、文件、通信、数据库、全局状态很容易紧密耦合在一起。
让代码更易于测试的思路如下。
不要试图直接测试CDialog或CView
先把非UI逻辑抽取出来
在边界处转换MFC类型
把文件或通信抽象为接口
让画面事件处理函数变薄
例如,可以把处理逻辑移到一个纯粹的 C++ 类中。
class PriceCalculator
{
public:
int CalculateTotal(const std::vector<int>& prices) const
{
int total = 0;
for (int price : prices)
{
total += price;
}
return total;
}
};
MFC 一侧只负责输入与输出。
void CPriceDialog::OnBnClickedCalculate()
{
if (!UpdateData(TRUE))
{
return;
}
std::vector<int> prices = ParsePrices(ToStdWString(m_input));
int total = m_calculator.CalculateTotal(prices);
m_result.Format(_T("%d"), total);
UpdateData(FALSE);
}
采用这种结构后,PriceCalculator 和 ParsePrices 就可以用普通的 C++ 测试框架来测试。
不需要一次性重写整个 MFC 应用,仅仅是把事件处理函数中可测试的处理逻辑抽取出来,就已经能带来效果。
32. 固定构建环境
在维护 MFC 应用时,固定构建环境非常重要。
在老旧代码库中,以下这些差异都可能导致构建结果不同。
Visual Studio的版本
MSVC工具集的版本
Windows SDK的版本
是否安装了MFC/ATL组件
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
MFC的静态链接 / 共享DLL
运行时库设置
预编译头
在 MFC 应用中,stdafx.h 或 pch.h 里往往集中了大量依赖。
#include "framework.h"
#include "MyApp.h"
有时会因为某个文件的编译设置不同,或预编译头设置不一致等原因导致构建失败。
在维护项目中,把这些信息明确写入 README,会让后续工作轻松很多。
所需的Visual Studio版本
所需的工作负载与单个组件
所需的Windows SDK
目标平台
MFC的链接方式
构建步骤
CI的执行方法
发行物的制作方法
从「只在自己电脑上能构建」这种状态中脱离出来,是维护 MFC 的第一步。
33. 在 CI 中构建 MFC
MFC 应用也可以在 CI 中构建。
不过,CI 环境中必须安装有 MFC 组件。
如果使用 Visual Studio Build Tools,需要指定 MFC/ATL 的组件 ID 进行安装。
需要确认的要点如下。
Build Tools中是否安装了MFC
目标工具集是否与项目一致
是否安装了Windows SDK
是否同时确认了x86构建和x64构建
资源编译器是否能正常工作
是否有签名处理
安装程序的生成是否也在CI范围内
在 MFC 应用中,把 UI 测试也自动化并不容易,但至少以下这些自动化是有效的。
Debug / Release构建
x86 / x64构建
静态分析
单元测试
安装程序制作
保存产物的哈希值
确认依赖DLL
在长期维护中,仅仅保持「能够构建」这一状态,就已经具有很大的价值。
34. 调试 MFC 应用时该看哪里
在排查 MFC 应用的问题时,按照以下顺序查看会更高效。
1. 是哪个画面
2. 是对话框、View还是Frame
3. 该操作对应的资源ID是什么
4. 消息映射会导向哪个函数
5. UpdateData的方向是否正确
6. 如果是Document/View,Document的状态是什么
7. 命令路由是否被转发到了其他类
8. 是否从工作线程直接操作了UI
9. 异常或错误是否仅被AfxMessageBox吞掉了
10. 是否存在Release构建特有的未初始化或生命周期问题
例如,如果遇到「点击按钮却什么都没发生」的问题,可以确认以下几点。
按钮的IDC是否正确
ON_BN_CLICKED是否存在
处理函数的签名是否正确
对话框资源是否是另一个
按钮是否被禁用
处理过程中UpdateData是否失败
异常是否被吞掉了
如果是「菜单无法点击」的问题,则可以确认以下几点。
是否被ON_UPDATE_COMMAND_UI禁用了
命令ID是否重复
活动视图是否符合预期
处理函数位于Frame/View/Document/App中的哪一个
由于 MFC 表面上的事件与实际处理是通过宏和路由关联起来的,在熟悉之前,把调用路径画成图会更容易理解。
35. 常见的陷阱
整理一下维护 MFC 时常见的陷阱。
不看消息映射,只搜索函数调用
认为CWnd*非空就一定有效
混淆HWND的生命周期与C++对象的生命周期
弄错UpdateData(TRUE/FALSE)的方向
没注意到被ON_UPDATE_COMMAND_UI禁用了
没注意到resource.h中的ID冲突
从工作线程直接操作UI
CString与std::string转换时出现乱码
Unicode化破坏了以MBCS为前提的代码
在MFC DLL中忘记写AFX_MANAGE_STATE
x64化破坏了以x86为前提的COM/ActiveX
GDI对象的选择/释放出错
高DPI下固定坐标布局发生错乱
MFC 的问题,有时光看 C++ 语法本身是看不出来的,还需要连同 Windows 的消息、资源、句柄、模块、运行时设置一起查看。
36. 新项目应该选择 MFC 吗
对于全新开发的项目,是否选择 MFC 需要谨慎判断。
如果有理由选择 MFC,通常是以下这些情况。
需要与现有MFC代码紧密协作
希望复用现有的MFC部件或界面
需要非常贴近Win32/GDI/COM的精细控制
团队内部拥有足够的MFC维护技能
目标平台仅限于Windows桌面
出于长期迁移计划,需要先在MFC上增量开发
反过来,以下情况则应该考虑其他选择。
想构建现代化的UI
需要灵活的布局或动画
以Web集成或云集成为核心
重视可测试性
希望选择让年轻开发者更容易参与的技术
需要跨平台支持
从一开始就重视无障碍或高DPI支持
MFC 既不是「现在学习已经没有价值」的技术,也不是「因为新才选择」的技术,而是一种用于面对现有 Windows 原生资产的技术。
37. 从 MFC 迁移时的思路
如果想把 MFC 应用迁移到其他技术,一开始就打算全面重写,往往容易失败。
首先,可以把应用这样分解来考虑。
UI
业务逻辑
文件格式
通信处理
数据库处理
设备控制
打印
COM/OLE集成
配置管理
日志
其中,最依赖 MFC 的是 UI。
而业务逻辑和文件处理,则有可能被抽取出来。
迁移的现实顺序大致如下。
1. 重现构建环境
2. 用测试数据固定现有行为
3. 把逻辑从UI事件处理函数中抽取出来
4. 迁移到非MFC的C++库中
5. 补充自动化测试
6. 编写外部规格文档
7. 从必要的界面开始逐步替换
与其把「摆脱 MFC」本身当作目标,不如把「把封闭在 MFC 中的重要逻辑抽取出来」当作目标,这样更容易成功。
38. 阅读 MFC 代码的入口
第一次阅读现有 MFC 项目时,可以从以下这些文件入手。
*.vcxproj
查看工具集、MFC设置、字符集、运行时设置
resource.h
查看资源ID
*.rc
查看对话框、菜单、字符串、图标
*App.cpp / *App.h
查看CWinApp派生类与InitInstance
MainFrm.cpp / MainFrm.h
查看主框架以及菜单/工具栏
*Doc.cpp / *Doc.h
如果是Document/View,查看数据结构与保存处理
*View.cpp / *View.h
查看绘制与用户操作
*Dlg.cpp / *Dlg.h
查看对话框、DDX、按钮处理
接下来是常用的搜索关键字。
BEGIN_MESSAGE_MAP
ON_COMMAND
ON_UPDATE_COMMAND_UI
ON_BN_CLICKED
DoDataExchange
UpdateData
OnInitDialog
OnDraw
Serialize
AfxMessageBox
AfxBeginThread
AFX_MANAGE_STATE
搜索这些关键字,可以更容易看清应用程序的运作方式。
39. 维护 MFC 时的设计方针
如果要长期维护 MFC 现有资产,以下方针会很有效。
让UI事件处理函数变薄
不要让CString或CWnd过多地泄漏到非UI层
把业务逻辑迁移到普通的C++类中
建立文件格式的兼容性测试
明确x86/x64之间的差异
对资源ID的变更进行审查
确认MFC DLL的模块状态
完善日志
在CI中固定构建
定期在高DPI或Windows 11环境下确认画面
尤其重要的是,不要把过多处理塞进 CDialog 或 CView 中。
MFC 的画面类应该专注于输入、显示、事件派发。
从画面获取值
传递给服务
把结果反映到画面上
只要能保持在这个程度,即使是 MFC,也能变得相当易于维护。
40. 实务检查清单
以下是处理 MFC 应用时的检查清单。
Visual Studio的版本是否明确
是否安装了MFC/ATL组件
x86/x64的目标是否明确
是否掌握了Unicode/MBCS的设置
MFC是静态链接还是共享DLL
所需的Visual C++ Redistributable是什么
resource.h和.rc是否列入了审查对象
是否通过消息映射追踪了事件路径
UpdateData的方向是否正确
是否使用了Document/View结构
非模态对话框的生命周期管理是否安全
是否有从工作线程直接操作UI
MFC DLL中是否存在需要AFX_MANAGE_STATE却遗漏的地方
是否在高DPI环境下确认过画面
COM/ActiveX依赖是否兼容32位/64位
日志是否会被保留
非UI逻辑能否被测试
MFC 在熟悉之前看起来很特殊,但只要知道该看哪里,其实读起来相当有规律。
41. 总结
MFC 是一个历史悠久的框架,用于以 C++ 构建 Windows 原生桌面应用程序。虽然它已不再是新项目开发的主流选择,但在维护现有资产、更新构建环境、追加功能、逐步迁移等场景中,它至今仍是一项重要的技术。
理解 MFC 时,尤其需要注意以下几点。
MFC让Win32 API更容易以C++方式使用
CWinApp管理整个应用程序
CWnd与HWND的生命周期并不相同
消息映射把事件与函数关联起来
Document/View是分离数据与显示的机制
DDX/DDV用于对话框数值的同步与验证
资源文件与resource.h非常重要
CString和TCHAR需要结合字符编码设置一起理解
MFC DLL中需要注意模块状态
老旧的MFC应用值得从高DPI、x64、CI、测试的角度重新审视
面对 MFC 时,与其一概认定「老旧就是不好」,更重要的是先理解其结构。现有的 MFC 应用中,往往凝聚着多年积累的业务知识、针对各个客户的规格、设备集成、文件兼容性。要在守护这些价值的同时逐步提升可维护性,现实的做法是:先理解 MFC 的作法,再把非 UI 逻辑抽取出来,并完善构建与测试。
如果要用一句话来概括,那就是:
一套用 C++ 的类与框架来处理 Windows 原生应用机制的基础平台。
带着这样的视角看 MFC,它就不再只是一项老旧的技术,而会成为安全解读现有 Windows 资产的重要线索。
参考
- 本文代码片段按章节整理的参考代码集 - komurasoft-blog-samples (GitHub)
- MFC Desktop Applications - Microsoft Learn
- MFC 与 ATL - Microsoft Learn
- Creating an MFC Application - Microsoft Learn
- CWinApp Class - Microsoft Learn
- CWinApp: The Application Class - Microsoft Learn
- Mapping Messages - Microsoft Learn
- Document-View Architecture - Microsoft Learn
- Dialog Data Exchange and Validation - Microsoft Learn
- MFC Library Versions - Microsoft Learn
- Redistribute the MFC Library - Microsoft Learn
- Visual Studio Build Tools workload and component IDs - Microsoft Learn
- High DPI Desktop Application Development on Windows - Microsoft Learn
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
正确处理 Windows 的模拟令牌 ── 线程级权限借用与安全的还原方式
本文围绕 Windows 的模拟令牌,梳理访问令牌、主令牌、线程令牌、模拟级别、RevertToSelf,直至 .NET 的 WindowsIdentity.RunImpersonated,整理在实务中安全处理它们的思路。
Windows为何演变成如今的样子:从开发者视角看历代Windows的演进
本文从兼容性、稳定性、权限管理、驱动程序、Win32、.NET、信息安全等Windows应用程序开发者的视角,梳理Windows 95到Windows 11的变化,而不是单纯以外观年表的方式呈现。
PowerShell 实用命令合集 —— 积累日常工作中常用的小工具
本文整理 PowerShell 日常工作中常用的实用命令,说明 Measure-Object、Group-Object、Select-String、Compare-Object、Tee-Object、Start-Transcript 等命令的使用场景和时机。
摆脱 IE 模式依赖系统的实践指引
本文围绕站点列表集中管理、中立站点、WebView2 封装、分阶段重构、VDI 隔离直到治理设计,梳理出一套让依赖 Microsoft Edge IE 模式的内部 Web 系统能够安全延续使用、并逐步摆脱依赖的实操路径,力求让读者可以直接照着落地执行。
Office 2024/Microsoft 365 中 ActiveX 无法运行的原因与排查步骤
梳理在 Office 2024/Microsoft 365 中 ActiveX 无法运行时,按默认禁用、32bit/64bit、COM 注册、依赖 DLL、IE 模式、Click-to-Run 日志的顺序进行排查的方法。
常见问题
汇总了咨询这一主题时常见的问题。
- MFC 是什么?
- MFC 是 Microsoft Foundation Classes 的缩写,是一个让 Win32 API 更容易以 C++ 类的形式使用的 Windows 应用程序框架。窗口用 CWnd 表示,对话框用 CDialog 表示,整个应用程序用 CWinApp 等类表示。它并不是「不了解 Windows 也能写代码的魔法库」,而是把 Windows 的机制用 C++ 的类型与框架整理出来的东西,因此仍然需要具备 Windows 消息、句柄等 Win32 相关知识。
- MFC 现在还能用吗?还在维护支持吗?
- MFC 目前仍可以在 Visual Studio 中使用,并会继续获得支持。不过微软的文档中说明,MFC 不会再新增功能,也不会再更新文档。就定位而言,在维护现有 MFC 应用、追加功能、更新构建环境,或向其他 UI 技术逐步迁移等场景中,MFC 依然是现实中常见的选择;但对于全新开发的一般 GUI 应用,是否采用 MFC 需要谨慎判断。使用前需要安装 Visual Studio Installer 中的单个组件(C++ MFC for latest build tools)。
- MFC 的消息映射是什么?
- 消息映射是 MFC 中把 Windows 消息或命令与处理函数关联起来的机制。在 BEGIN_MESSAGE_MAP 到 END_MESSAGE_MAP 之间,通过 ON_BN_CLICKED、ON_COMMAND 等宏来描述「点击这个按钮就调用这个函数」这样的对应关系。当搜索代码却找不到直接调用,但该函数确实会在事件发生时执行时,就应该去查看消息映射。在 MFC 的代码审查中,不能只看处理函数本身,还必须结合消息映射一起确认,这一点非常重要。
- 要把 MFC 迁移到其他技术,应该怎么做?
- 一开始就打算全面重写,往往容易失败。比较现实的顺序是:先重现构建环境,用测试数据固定现有行为,把逻辑从 UI 事件处理函数中拆分出来并迁移到非 MFC 的 C++ 库中,再补充自动化测试,编写外部规格文档,最后从必要的界面开始逐步替换。与其把「摆脱 MFC」本身当作目标,不如把「把封闭在 MFC 中的重要逻辑抽取出来」当作目标,这样更容易成功。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。