
简介面向 VS2022 平台初学者的 MFC 编程入门文档系统讲解如何借助 Visual C 开发可视化窗口程序。文档先从概念层面厘清 C 语言与 VC 开发工具的区别介绍 MFC 微软基础类库的由来与作用——它用 C 封装了 Windows SDK 中的结构和功能并提供应用程序框架自动完成窗口、工具栏、菜单的注册、生成与管理从而降低界面开发门槛。同时给出 VC6.0、VS2005、VS2022 等常用版本的特点与选择建议。随后通过 HelloWorld 单文档应用程序示例展示如何利用 MFC 向导创建工程、设置工程名与解决方案路径并理解解决方案与工程的关系逐步学会生成单文档应用程序框架。资源包以 docx 格式封装内含 1 个文档文件体积约 33KB适合下载后随时查阅。目前已有 4545 人学习适合具备一定 C 语法基础、希望接触 Win32/MFC 可视化开发的初学者作为从理论学习过渡到实际界面编程的第一份参考资料。1. VS2022里学MFC先放下“过时”的包袱打开Visual Studio Installer勾上“使用C的桌面开发”后还得在右侧组件列表里把MFC那一项单独勾上否则新建项目页根本搜不到“MFC应用”模板。这个隐藏步骤劝退了不少想入门Windows桌面开发的人。MFC把Win32窗口、消息循环、控件和GDI对象包成可继承的C类编程模型是“消息映射数据交换”和Qt的信号槽、C#的事件委托不是一回事。VS2022里MFC工程模板完整保留基于对话框、单文档、多文档三种骨架都在适合做老系统维护、设备上位机、校内实验或者拿一个轻量窗口壳去验证硬件SDK和通讯协议。下面按一条可复现的路径来带从组件勾选开始跑通第一个对话框工程把Combo Box和List Control接到数据上再处理自绘、控制台调试和几个绕不开的坑。2. VS2022下创建第一个MFC对话框工程组件勾选与工程骨架MFC在VS2022里不是一个单独的安装包它挂在“使用C的桌面开发”工作负载下面默认不选中。很多人装完VS2022后新建项目搜不到MFC模板就是漏了这一步。另一个容易忽略的是老工程升级用VS2022打开VS2017写的MFC工程时IDE会弹“平台工具集重定目标”的窗口点确定后工具集从v142切到v143但MFC库本身不会自动安装照样编译报“无法打开mfc.h”。2.1 安装器里找到MFC组件的两条路修改已安装的VS2022时在工作负载页勾选“使用C的桌面开发”然后在右侧“安装详细信息”里展开这一项找到并勾选“适用于最新v14x生成工具的C MFCx86和x64”。v14x里的x对应当前工具集版本VS2022安装的是v143。如果后续要处理ObjectARX这类CAD二次开发依赖或者老SDK示例代码里用到了ATL建议把“C ATL”也一并勾上它和MFC共享一部分模板化底层代码缺失时会报atlbase.h找不到。组件全名作用不装会出现什么适用于最新v14x生成工具的C MFCx86和x64MFC头文件、导入库和运行库新建项目搜不到MFC模板编译报mfc.h缺失C ATLATL轻量COM模板库老示例编译报atlbase.h缺失Windows 11 SDK系统API头文件和导入库VS2022安装器默认会装缺失则windows.h报错按修改按钮后安装器会在线下载组件。装完重开VS2022新建项目的筛选框里输入MFC就能看到“MFC应用”模板。2.2 用向导生成基于对话框的工程新建项目时选“MFC应用”点下一步后先看“应用程序类型”入门选“基于对话框”。这个骨架没有菜单、工具栏和文档/视图结构窗口上直接拖控件就能跑适合先搞懂MFC最核心的消息映射和DDX。向导里的“静态链接到MFC”我一般默认“在共享DLL中使用MFC”发布时带上运行库如果目标是给没有VC运行库的旧机器用再改成静态链接。向导完成后类视图里有两个关键类一个是CWinApp派生的应用类程序入口一个是CDialogEx派生的对话框类承载控件。资源视图里能看到IDD_MAINDIALOG控件就拖在这个对话框模板上。控件ID由resource.h统一维护类向导改名时ID会同步更新不建议手写容易漏。2.2.1 InitInstance应用从哪里启动MFC程序没有传统意义上的main函数真正的入口逻辑在CWinApp派生类的InitInstance()里。向导生成的代码会做OLE初始化、创建对话框对象、调用DoModal三个动作。下面这个简化版本能看清楚执行顺序BOOL CMfcDemoApp::InitInstance() { // 由MFC完成内部初始化保留这一行 CWinApp::InitInstance(); // 在栈上创建主对话框函数结束自动析构 CMfcDemoDlg dlg; m_pMainWnd dlg; // DoModal进入阻塞式消息泵直到对话框关闭才返回 INT_PTR nResponse dlg.DoModal(); if (nResponse IDOK) { AfxMessageBox(L用户点了确定按钮); } else if (nResponse IDCANCEL) { AfxMessageBox(L用户点了取消按钮); } // 返回FALSE表示结束消息循环退出进程 return FALSE; }DoModal的参数是它自己管理消息循环阻塞在调用处直到对话框调用EndDialog或用户关闭窗口。返回值IDOK和IDCANCEL来自对话框上按钮的ID可以用它判断用户操作结果。m_pMainWnd指向主窗口MFC内部根据它管理窗口生命周期。return FALSE并不是错误而是告诉框架不再进入应用级消息循环对对话框程序来说这正是正确收尾。提示向导生成的InitInstance开头有一大段OLE初始化等内容入门阶段不要删它们由资源文件里的全局对象引用删了链接期会出奇怪错误。2.3 跑通前的三个必查属性直接按F5之前先确认三个属性。第一是活动解决方案平台在“配置管理器”里看是x86还是x64。MFC工程默认按当前活动平台编译如果目标机器或SDK要求32位就改成x86。第二个是“常规→字符集”选“使用Unicode字符集”。VS2022的MFC默认Unicode老教程里的char、CStringA属于旧写法照抄会出中文乱码。第三个是“平台工具集”VS2022默认v143旧工程升级后IDE会提示重定目标。属性项推荐值容易踩的表现活动解决方案平台按目标机器选x64或x86编译通过但启动提示无法加载DLL常规→字符集使用Unicode字符集中文变问号断言报宽字符错误平台工具集v143打开VS2017工程会弹升级对话框平台工具集决定编译器版本第三方静态库依赖它。混用不同v14x编译的静态库时经常冒LNK4098警告本质是CRT运行库配置不一致先在“C/C→代码生成→运行库”里对齐选项再去追究代码问题。这三个属性在新建MFC工程后检查一次基本能淘汰一大半入门阶段的启动失败。3. 用Combo Box和List Control搭交互消息映射与数据交换对话框程序中控件不会自己触发业务逻辑它向窗口发送WM_COMMAND通知消息MFC通过消息映射宏把它们转发到成员函数。这是整个MFC事件模型里最关键的一段写清楚函数签名、把宏放进BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间、在类头文件里声明afx_msg函数。三个条件缺一个编译照过但按钮按了没反应。3.1 拖控件之前先理解ID和资源双击资源视图里的IDD_MAINDIALOG打开对话框编辑器从工具箱拖一个Button、一个Combo Box、一个List Control到画布上。命名规范控件ID不按控件类型命名按用途命名例如IDC_BTN_ADD、IDC_COMBO_PROTOCOL、IDC_LIST_LOG。MFC通过ID把控件和成员变量关联改名后关联关系会断所以先想好用途再顺手改名。对话框编辑器里的Tab顺序不是拖放顺序而是按CtrlD显示数字依次按下的焦点顺序。Combo Box默认不带输入框不对Combo Box本身有三种样式下拉列表、下拉框和简单组合框。常用的是“下拉框”既能输入又能点选读取时用GetWindowText就能取到显示文本。3.2 给Combo Box填数据并读取选中项3.2.1 用DDX把控件和成员变量绑定在对话框类上右键选择“添加成员变量”选择控件ID后整套DDX代码会自动写进DoDataExchange函数void CMfcDemoDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); // 第一个参数是控件ID第二个是成员变量类型第三个是关联的成员变量 DDX_Control(pDX, IDC_COMBO_PROTOCOL, m_comboProtocol); DDX_Control(pDX, IDC_LIST_LOG, m_listLog); DDX_Text(pDX, IDC_EDIT_INPUT, m_strInput); }DDX_Control负责把控件句柄绑定到CComboBox、CListCtrl这类MFC对象上之后代码里直接操作m_comboProtocol成员函数就行。DDX_Text绑定的是文本值不是控件本身初始化时要把数据从控件读进变量调用UpdateData(TRUE)要把变量里的值写回控件调用UpdateData(FALSE)。很多入门代码在OnInitDialog之前调UpdateData断言就炸了原因是控件还没有完成创建。3.2.2 初始化时填充列表并响应CBN_SELCHANGEOnInitDialog里填充下拉项是一次性操作放在Create之后没毛病BOOL CMfcDemoDlg::OnInitDialog() { CDialogEx::OnInitDialog(); CString arrProtocols[] { LTCP, LUDP, L串口, LCAN }; for (int i 0; i _countof(arrProtocols); i) { // AddString把文本加入下拉列表返回下标从0开始 m_comboProtocol.AddString(arrProtocols[i]); } // 默认选中第一项参数是下标 m_comboProtocol.SetCurSel(0); return TRUE; }AddString会把同样文本再次加进去重复添加是下拉框列表越变越长的常见原因。如果列表会动态刷新先调ResetContent清空再填充。SetCurSel(0)表示选中第0项但不会自动触发CBN_SELCHANGE消息所以初始数据还要自己读一次或直接用固定值。选中项变化时给Combo Box右键添加“Selection Change”处理函数向导会生成对应映射。处理函数里用GetCurSel拿下标用GetLBText拿文本void CMfcDemoDlg::OnCbnSelchangeComboProtocol() { int nIndex m_comboProtocol.GetCurSel(); if (nIndex 0) return; CString strProtocol; m_comboProtocol.GetLBText(nIndex, strProtocol); m_strProtocol strProtocol; }GetCurSel返回当前选中下标没有选中时返回CB_ERR也就是-1先判负再取文本。GetLBText把对应文本复制到CString这两步是Combo Box读取的固定套路别用GetWindowText代替它在“下拉框”样式下拿到的可能是用户输入但未选中的文本。3.3 消息映射宏函数入口和窗口消息的连接表宏来自控件处理函数签名ON_BN_CLICKEDButtonafx_msg void OnBnClickedXxx()ON_CBN_SELCHANGECombo Boxafx_msg void OnCbnSelchangeXxx()ON_EN_CHANGEEdit Controlafx_msg void OnEnChangeXxx()ON_WM_PAINT窗口绘制消息afx_msg void OnPaint()宏放在BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间第二个参数是控件ID第三个参数是处理函数地址。手动写映射时必须保证函数签名完全一致参数类型或返回值不对编译能过但链接会失败或编译直接报“无法将参数从...转换到...”。3.4 一个联动示例编辑框Combo BoxList Control把场景做成编辑框输入内容下拉框选协议类型点“记录”按钮把两条信息写入List Control。头文件里声明afx_msg void OnBnClickedBtnAdd();消息映射里加入ON_BN_CLICKED(IDC_BTN_ADD, CMfcDemoDlg::OnBnClickedBtnAdd)按钮处理函数void CMfcDemoDlg::OnBnClickedBtnAdd() { // TRUE表示把编辑框控件内容同步到成员变量 UpdateData(TRUE); if (m_strInput.IsEmpty()) { AfxMessageBox(L先填一条内容); return; } // 插入到List Control末尾返回新行的下标 int nIndex m_listLog.GetItemCount(); m_listLog.InsertItem(nIndex, m_strInput); // 第二列填下拉框当前选中文本 m_listLog.SetItemText(nIndex, 1, m_strProtocol); // 清空编辑框并把焦点还给编辑框 SetDlgItemText(IDC_EDIT_INPUT, L); GetDlgItem(IDC_EDIT_INPUT)-SetFocus(); }InsertItem插入的是第一列内容下标从GetItemCount拿也就是当前行数。SetItemText把子项文本写入第二列列号从0开始。如果List Control还是默认样式什么列都看不见需要在OnInitDialog里把View改成Report并插入列m_listLog.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES); m_listLog.InsertColumn(0, L内容, LVCFMT_LEFT, 260); m_listLog.InsertColumn(1, L协议, LVCFMT_CENTER, 100);SetExtendedStyle设置整行高亮和网格线InsertColumn的参数依次是列下标、列标题、对齐方式、列宽。对齐方式这里是LVCFMT_LEFT注意这个常量本质是一个格式位后面第5章会单独讲它为什么偶尔“失效”。4. 自绘彩色正方形与让控制台程序支持MFCMFC里画图没有Canvas这种对象一切绘图都发生在设备上下文DC上。DC是GDI的句柄包装拿不到DC就画不了画完不释放又会句柄泄漏。入门最容易出问题的不是像素坐标而是画刷和画笔的释放顺序。另一类常见需求是手里的工程已经是控制台程序只是想用CString、CFile这些MFC基础类不想重构整个项目这时可以用AfxWinInit让控制台程序直接支持MFC。4.1 用OnPaint画一个居中的彩色正方形对话框收到WM_PAINT消息会触发OnPaint重绘。在对话框类上右键添加“OnPaint”向导生成的处理函数里会先判断图标是否最小化保留那段系统代码在它下面加入自定义绘制逻辑void CMfcDemoDlg::OnPaint() { if (IsIconic()) { CDialogEx::OnPaint(); return; } CPaintDC dc(this); // 构造时自动BeginPaint析构时自动EndPaint CRect rc; GetClientRect(rc); // 取客户区尺寸坐标相对对话框左上角 // 边长为100像素的正方形居中显示 int nSide 100; int nLeft (rc.Width() - nSide) / 2; int nTop (rc.Height() - nSide) / 2; // 创建纯色画刷颜色用RGB宏 CBrush brush(RGB(255, 128, 0)); CBrush* pOldBrush dc.SelectObject(brush); // 前两个参数是左上角坐标后两个是右下角坐标 dc.Rectangle(nLeft, nTop, nLeft nSide, nTop nSide); // 恢复旧画刷防止GDI句柄残留 dc.SelectObject(pOldBrush); }4.1.1 CPaintDC不会用错的两个细节CPaintDC只能在WM_PAINT处理函数里用因为它在构造时调用BeginPaint而BeginPaint只能配合失效区域存在。想在按钮点击后主动刷新不能自己new一个CPaintDC正确做法是调用Invalidate让系统生成WM_PAINT再用UpdateWindow立即重绘。第二个细节是SelectObject的返回值一定要保存用完恢复。句柄不恢复在窗口重绘多次后会偷偷泄漏任务管理器里看不到但重新缩放窗口后绘制会变花。Rectangle画出来的是矩形边框加内部填充边框颜色由当前画笔决定。上面代码没设置画笔用的是默认黑色笔。如果想要橙色边框再加一个CPen对象CPen pen(PS_SOLID, 3, RGB(0, 128, 255)); CPen* pOldPen dc.SelectObject(pen); dc.Rectangle(nLeft, nTop, nLeft nSide, nTop nSide); dc.SelectObject(pOldPen);CPen构造参数依次是线型、线宽、颜色。PS_SOLID是实线线宽3像素。画刷和画笔分别选择、分别恢复顺序反了也不会立刻崩溃但管理混乱时容易出现“上一次的颜色残留”这种难查的绘制脏问题。4.2 在既有控制台程序里启用MFC支持一个现成的控制台程序要使用CString和CFile不需要复制代码。修改三个属性就能让控制台程序直接用MFC类库。打开项目属性“常规→MFC的使用”从“使用标准Windows库”改成“在共享DLL中使用MFC”此时编译会报找不到WinMain——还需要改入口点。链接器默认用WinMain控制台入口是main两方面配合才能正常链接。属性页路径设置内容说明常规→MFC的使用在共享DLL中使用MFCMFC类库代码在DLL里文件小链接器→系统→子系统控制台保留黑色控制台窗口链接器→高级→入口点mainCRTStartup覆盖WinMain默认入口代码里在main函数开头调用AfxWinInit#include afxwin.h int main() { // 初始化MFC运行环境参数分别是实例句柄、前缀、命令行、显示方式 if (!AfxWinInit(::GetModuleHandle(nullptr), nullptr, ::GetCommandLine(), 0)) { return -1; } CString strMsg; strMsg.Format(L控制系统状态: %d, 42); wprintf(L%s\n, (LPCWSTR)strMsg); return 0; }AfxWinInit的第一个参数传入当前模块句柄在DLL里要传DLL自己的句柄。第三个参数传命令行不传的话CCommandLineInfo无法用来解析参数。返回值FALSE表示初始化失败后面所有MFC类都不能用。CString格式化用Format输出到控制台时强转成LPCWSTR加L前缀打印宽字符。提示控制台程序的字符集也要保持Unicode。如果入口点设置后仍然报MSVCRTD和LIBCMT冲突去“C/C→代码生成→运行库”里把MTd改成MDd这是MFC控制台程序最常见的链接期坑。这套做法也适用于老MFC工程临时加一个命令行调试入口。在Win32控制台项目里写一个main再调用MFC的全局初始化比起新建完整MFC应用要轻得多。对ObjectARX这类需要依赖MFC的二次开发工程调试阶段加一个控制台入口能快速验证核心算法而不必每次启动宿主CAD程序。5. 收尾在坑上UTF-8、List Control首行与启动报错对话框能弹出来、按钮能响应后剩下的问题基本集中在编码、控件细节和运行环境三个方向。这三类问题按教程走一遍根本不触发只有写了中文、调了列表样式、换了机器之后才会冒出来而且报错信息往往不指向真因。5.1 用/utf-8统一源文件和运行编码VS2022默认按系统代码页处理源文件中文Windows下是GBK。当源文件用UTF-8保存而运行编码还是GBK时CString里的中文和直接写在代码里的宽字符串字面量就会对不上现象是调试器里看到的中文正常运行起来显示乱码。在“C/C→命令行→其他选项”加一行即可/utf-8这个开关等价于/source-charset:utf-8和/execution-charset:utf-8一个规定源码怎么读一个规定字符串字面量怎么编码。加了之后源码里统一用UTF-8保存L宽字符串和CString内部UTF-16数据转换由编译器保证不再依赖系统代码页。5.2 List Control第一行设置LVCFMT_LEFT无效果的排查网上经常有人问“InsertColumn设置了LVCFMT_LEFT第一行数据还是居中/格式不对”。这个问题的坑在于LVCOLUMN结构体没有清零mask没有包含LVCF_FMTfmt字段根本不会被写入。正确写法是LVCOLUMN col { 0 }; // 先整体清零 col.mask LVCF_FMT | LVCF_TEXT | LVCF_WIDTH; col.fmt LVCFMT_LEFT; // 注意不要写成 col.fmt | LVCFMT_LEFT col.cx 200; col.pszText L第一列; m_listLog.InsertColumn(0, col);首次插入列时fmt直接用赋值没问题但后续要修改列格式时必须先InsertColumn再SetColumnSetColumn的mask同样要带LVCF_FMT。还有另一种“第一行消失”的情况列宽设成0网格线还在但内容看不见检查col.cx别写小。List Control换到报告视图后第一行显示异常还要顺带确认OnInitDialog里是否先设置了LVS_EX_FULLROWSELECT这两个扩展样式的设置顺序影响空行绘制。5.3 顺带识别VS2022启动报错2146233082安装MFC组件后如果VS2022本身启动失败报错编号2146233082是常见的启动故障点。这个数字本身是错误代号不是C代码抛出来的只提示“应用程序无法正常启动”。优先检查两处一是安装器里是否真有组件损坏进入Visual Studio Installer点“修复”二是Windows事件查看器里的.NET Runtime日志VS2022自身依赖.NET框架MFC组件安装过程中见过把运行时配置改写的情况。修复安装器比重装系统快别一上来就卸载重来。5.4 用CRT宏在输出窗口抓MFC内存泄漏MFC程序的内存泄漏不会在退出时弹窗它会悄悄打印到VS输出窗口的“诊断”面板。要激活这个能力在stdafx.h或主cpp里加两段#ifdef _DEBUG #define new DEBUG_NEW #endif#ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif第一段让new带上文件名和行号信息泄漏报告里能直接定位到分配位置。第二段在进程退出前检查堆内存把未释放的内存块列出来。输出窗口里“检测到内存泄漏”下面每一块就是一次没有对应delete的分配。报告里带花括号数字比如{1023}把这个数字填进_CrtSetBreakAlloc(1023)再次调试会在该次分配处断点此时看调用栈就能找到泄漏源头。这个技巧对自绘代码里漏恢复GDI对象尤其好使因为GDI泄漏不归CRT管但凡是new过CBrush、CPen没有delete的CRT报告里一个都跑不了。本文还有配套的精品资源点击获取