资讯动态

VS2017 MFC对话框开发入门:从零搭建串口助手风格程序

发布时间:2026/10/16 4:28:33 来源:尧图企业网站定制
1. 为什么还要折腾MFC这个老古董先把话说在前头如果你现在打开搜索引擎搜桌面开发跳出来的八成是Electron、Qt、WPF、Avalonia这些名字MFC这三个字母看起来像是上个时代的产物。但现实情况是在工业控制、医疗设备上位机、检测仪器配套软件、老旧产线管理系统这些领域MFC依然是绝对的主力。我接触过的好几个做硬件配套的团队他们的上位机代码库从2008年维护到现在VS2017加上MFC就是标配换不得也懒得换。所以这篇内容写给谁看三类人。第一类是被安排接手一个MFC老项目的开发者之前可能只写过Web或者Python突然要面对一堆CWinApp、CDialog、MessageMap需要一个能跑起来的最小可运行例子来建立信心。第二类是在校学生或者刚入行的朋友课程或者实训要求用VS2017做MFC程序但照着书上的步骤总是编译报错、对话框弹不出来。第三类是想快速验证某个Windows原生API功能不想引入庞大框架用MFC搭个壳子最省事。这篇文章我会从零开始用一个串口助手风格的最小MFC对话框程序作为主线把VS2017下创建项目、理解代码结构、添加控件、响应消息、调试排错的完整链路走一遍。所有步骤都是我在实际环境里反复验证过的包括那些新手最容易卡住的坑比如字符集配置、资源视图打不开、控件变量添加失败等等。看完你应该能独立做出一个能用的MFC对话框程序并且知道每一步为什么这么做。2. 动手之前的准备工作2.1 VS2017安装时的关键勾选项很多人装VS2017的时候一路下一步结果创建项目时发现根本找不到MFC模板。问题出在安装器的工作负载选择上。VS2017的安装器把MFC归在了使用C的桌面开发这个工作负载下面但默认是不勾选的。具体操作打开Visual Studio Installer找到已安装的VS2017实例点击修改在工作负载标签页里确认使用C的桌面开发已经勾选。然后切到右侧的安装详细信息面板展开使用C的桌面开发在里面找到适用于最新v141生成工具的C MFCx86和x64这一项把它勾上。如果你还需要ATL或者C/CLI可以一并勾选但做基础MFC程序只需要MFC这一项。这里有个细节值得注意VS2017的版本号是v141工具集和VS2015的v140、VS2019的v142不一样。如果你拿到的老项目是用v140编译的打开时可能会提示未安装v140工具集。解决办法是在安装器里额外勾选MSVC v140 - VS 2015 C生成工具这样VS2017就能同时支持两套工具集。我建议做工业上位机的朋友直接把v140和v141都装上省得以后来回折腾。安装完成后验证一下新建项目对话框里搜索MFC应该能看到MFC应用程序模板。如果看不到重启一下VS或者检查安装器里MFC组件是否真的装上了。2.2 字符集选择Unicode还是多字节这是MFC新手遇到的第一个大坑也是后面无数编译错误和乱码问题的根源。VS2017创建MFC项目时向导里有一个字符集选项默认是使用Unicode字符集。我的建议是除非你接手的项目明确要求多字节否则一律选Unicode。原因很简单。Windows从NT内核开始内部就是UTF-16Unicode字符集下调用API不需要转换性能更好而且微软的新API只支持Unicode。多字节字符集MBCS在中文环境下用的是GBK编码遇到日文、韩文或者特殊符号就会出问题。我见过一个项目因为用了多字节客户输入一个带音标的法语人名直接乱码排查了半天才发现是字符集的问题。但现实是很多老项目就是多字节的你改不了。这时候要注意CString在Unicode下是CStringW多字节下是CStringA。如果你从网上抄了一段代码里面用了char*和strcpy在Unicode项目里编译就会报错。解决办法是用TCHAR、_T()宏、_tcscpy这些自适应类型或者干脆用CString和它的成员函数让编译器自己去适配。切换字符集的位置在项目属性 - 配置属性 - 高级 - 字符集。改完之后最好把整个解决方案重新生成一遍因为有些预编译头文件里的宏定义会跟着变。2.3 项目目录结构的规划建议MFC向导默认会把所有文件放在项目根目录下小项目无所谓但稍微大一点就会乱。我习惯在项目根目录下建几个子文件夹src放cpp文件include放头文件res放资源文件lib放第三方库。不过MFC的资源文件.rc和资源头文件resource.h最好还是放在项目根目录因为资源编译器对路径比较敏感挪位置容易出问题。另外强烈建议从第一天就用Git或者SVN做版本管理。MFC项目的.vcxproj和.vcxproj.filters文件是文本格式可以正常diff和merge。但.aps资源符号文件和.user文件不要提交前者是二进制缓存后者存的是个人开发环境配置。.gitignore里加上*.aps、*.user、Debug/、Release/、x64/这几项就够了。3. 创建第一个MFC对话框项目3.1 向导里每一步到底在选什么打开VS2017文件 - 新建 - 项目在左侧选Visual C - MFC/ATL右侧选MFC应用程序。项目名我起成DemoMfcApp位置放在一个没有中文和空格的路径下。这一点很重要MFC的资源编译器对中文路径支持不好有时候会报无法打开资源文件的错误排查起来很费时间。点击确定后进入向导。第一页是应用程序类型这里有三个选项单文档、多文档、基于对话框。我们做工具类程序选基于对话框最合适。单文档和多文档适合需要菜单栏、工具栏、状态栏的文档编辑类程序结构复杂得多。基于对话框的程序启动后直接弹出一个窗口没有主框架窗口的概念代码量少适合快速开发。第二页复合文档支持保持默认的无就行。第三页文档模板属性在对话框模式下是灰的不用管。第四页用户界面功能里我一般把粗框架勾上这样窗口有可调整大小的边框方便调试时看布局效果。如果做固定尺寸的工具窗口可以选瘦框架或者对话框框架。关于框可以保留方便加版本信息。第五页高级功能里Windows套接字和ActiveX控件按需勾选。如果后面要用串口或者网络通信套接字可以勾上但其实不勾也能手动加。最后一页生成的类里可以看到向导会生成三个类CDemoMfcApp应用程序类、CDemoMfcDlg对话框类、CAboutDlg关于对话框类。基类分别是CWinApp、CDialogEx、CDialogEx。这里不用改直接点完成。3.2 生成出来的代码结构拆解向导跑完后解决方案资源管理器里会有一堆文件。新手最容易懵的就是不知道从哪看起。我按重要性排个序DemoMfcApp.h和DemoMfcApp.cpp是应用程序类相当于程序的入口。InitInstance()函数是真正的起点里面创建了对话框对象并调用DoModal()。如果你想在程序启动时做点什么比如读配置文件、初始化日志就在这里加。DemoMfcDlg.h和DemoMfcDlg.cpp是主对话框类你后面90%的代码都写在这里。头文件里声明控件变量和消息处理函数cpp文件里实现它们。resource.h是资源ID的定义文件每个控件、每个菜单项都有一个数字ID。向导生成的ID从100开始往上排。你可以手动改但建议用VS的资源编辑器自动分配避免冲突。DemoMfcApp.rc是资源脚本文件定义了对话框的布局、图标、字符串表等。这个文件可以用文本编辑器打开看但修改最好通过资源视图的可视化编辑器手改容易出错。pch.h和pch.cpp是预编译头文件。MFC的头文件很大预编译能显著加快编译速度。注意所有cpp文件的第一行有效代码必须是#include pch.h否则会报预编译头文件不一致的错误。这个坑我踩过好几次特别是从网上拷贝代码进来的时候。framework.h是向导生成的一个中间头文件里面包含了一堆MFC常用头文件。你可以把它理解成一个常用头文件集合省得每个cpp都写一长串include。targetver.h定义了程序支持的最低Windows版本。默认是_WIN32_WINNT 0x0601也就是Windows 7。如果你要用Windows 8或10的新API需要把这个值改大。3.3 第一次编译运行会遇到什么直接按F5编译运行正常情况下会弹出一个空对话框标题是DemoMfcApp上面有两个按钮确定和取消。点确定或者取消程序退出。但实际情况往往没那么顺利。我第一次在新装的VS2017上跑的时候报了这么几个错第一个是MSB8041: 此项目需要MFC库。原因就是前面说的安装时没勾MFC组件。回到安装器勾上就行。第二个是无法打开源文件afxwin.h。同样是MFC组件没装全或者项目属性里的使用MFC设置不对。检查项目属性 - 配置属性 - 常规 - 使用MFC应该是在共享DLL中使用MFC。第三个是链接错误LNK1104: 无法打开文件mfc140ud.lib。这个通常是平台工具集和MFC库版本不匹配。VS2017对应的是mfc140如果你把工具集改成了v142VS2019的但没装对应的MFC库就会报这个错。解决办法是把工具集改回v141或者装上v142的MFC组件。还有一个很隐蔽的问题如果你的项目路径里有中文资源编译器可能会报RC1015: cannot open include file afxres.h。这个错误看起来像是头文件找不到实际上是路径编码问题。把项目挪到纯英文路径下就好了。4. 对话框布局与控件操作4.1 资源视图里拖控件的正确姿势打开资源视图如果找不到在菜单栏视图 - 其他窗口 - 资源视图展开DemoMfcApp.rc-Dialog双击IDD_DEMOMFCAPP_DIALOG就能看到对话框的可视化编辑器。工具箱里拖一个按钮Button到对话框上再拖一个编辑框Edit Control再拖一个静态文本Static Text。这三个控件足够演示基本交互了。拖完之后要改属性。选中按钮在属性窗口里把Caption改成获取文本ID改成IDC_BTN_GETTEXT。选中编辑框ID改成IDC_EDIT_INPUT。选中静态文本ID改成IDC_STATIC_RESULTCaption清空。这里有个经验所有需要代码访问的控件ID一定要改成有意义的名字不要用默认的IDC_BUTTON1、IDC_EDIT1。项目大了之后满屏的IDC_BUTTON1根本分不清谁是谁。命名规范我习惯用IDC_控件类型_功能比如IDC_EDIT_INPUT、IDC_BTN_GETTEXT、IDC_STATIC_RESULT。静态文本控件有个特殊之处如果ID是IDC_STATIC代码里是访问不到的因为所有默认ID的静态文本共享同一个ID。要访问它必须改成唯一的ID比如IDC_STATIC_RESULT。4.2 添加控件变量的两种方式要让代码能操作控件需要给控件添加变量。右键点击编辑框 - 添加变量变量类别选Value变量类型选CString变量名填m_strInput。这样MFC会自动生成DDX_Text的绑定代码在DoDataExchange函数里把控件和变量关联起来。再右键编辑框 - 添加变量这次变量类别选Control变量类型选CEdit变量名填m_editInput。这样就能通过m_editInput调用CEdit的成员函数比如SetSel、GetLineCount等。两种变量的区别Value类型的变量在UpdateData(TRUE)时从控件读到变量在UpdateData(FALSE)时从变量写到控件。Control类型的变量直接操作控件窗口实时生效。我一般两个都加Value用于批量读写Control用于精细控制。给静态文本也加一个Value变量类型CString名字m_strResult。给按钮加一个Control变量类型CButton名字m_btnGetText。添加变量时可能会遇到无法添加变量的错误。常见原因是对话框类没有正确关联或者资源编辑器里的对话框ID和代码里的不匹配。解决办法是关掉资源编辑器重新打开或者清理解决方案后重新生成。4.3 消息映射按钮点击怎么响应双击对话框上的按钮VS会自动跳转到代码视图并生成一个空的OnBnClickedBtnGettext()函数。这个自动跳转的背后是MFC的消息映射机制在起作用。MFC的消息映射本质上是一个宏定义的表。在DemoMfcDlg.h里你会看到这样的代码protected: afx_msg void OnBnClickedBtnGettext(); DECLARE_MESSAGE_MAP()在DemoMfcDlg.cpp里有BEGIN_MESSAGE_MAP(CDemoMfcDlg, CDialogEx) ON_BN_CLICKED(IDC_BTN_GETTEXT, CDemoMfcDlg::OnBnClickedBtnGettext) END_MESSAGE_MAP()ON_BN_CLICKED这个宏把按钮的点击事件和成员函数关联起来。当按钮被点击时Windows发送WM_COMMAND消息MFC的消息循环根据映射表找到对应的函数并调用。理解了这个机制你就能手动添加消息处理了。比如要处理对话框的WM_CLOSE消息可以在类向导里选CDemoMfcDlg在消息列表里找到WM_CLOSE添加处理函数OnClose()。类向导会自动帮你生成声明和映射代码比手动写不容易出错。4.4 实现一个简单的文本回显功能在OnBnClickedBtnGettext()函数里写void CDemoMfcDlg::OnBnClickedBtnGettext() { UpdateData(TRUE); // 从控件读取数据到变量 if (m_strInput.IsEmpty()) { MessageBox(_T(请输入内容), _T(提示), MB_ICONWARNING); return; } m_strResult _T(你输入的是) m_strInput; UpdateData(FALSE); // 从变量写回控件 }UpdateData(TRUE)会触发DoDataExchange里的DDX_Text把编辑框的内容读到m_strInput。UpdateData(FALSE)反向操作把m_strResult写到静态文本控件上。这里有个细节MessageBox的第二个参数是标题第三个参数是图标类型。MB_ICONWARNING会显示一个黄色感叹号图标。常用的还有MB_ICONINFORMATION、MB_ICONERROR、MB_YESNO等。编译运行在编辑框里输入文字点按钮静态文本就会显示你输入的是xxx。如果输入为空弹出警告框。这个流程跑通了MFC的基本交互就掌握了。5. 调试与常见问题排查5.1 断点调试MFC程序的技巧MFC程序的调试和普通C程序差不多但有几个地方要注意。首先DoModal()调用会阻塞在消息循环里如果你在OnInitDialog()里打断点程序启动时会停在那里但对话框还没显示出来。这时候按F10单步能看到对话框的创建过程。其次MFC内部会捕获很多异常。如果你的代码里抛了异常但没处理MFC可能会弹出一个遇到未经处理的异常对话框而不是直接崩溃。这个对话框里的信息有时候不够详细可以在项目属性 - 调试 - 异常里把Win32 Exceptions的引发时中断勾上这样异常一发生就会断下来。还有一个技巧在监视窗口里输入this可以展开当前对话框对象的所有成员变量包括控件变量和自定义成员。输入GetSafeHwnd()可以看窗口句柄确认窗口是否已经创建。5.2 资源ID冲突与清理方法项目做大了之后资源ID冲突是常见问题。表现是你明明改了按钮的ID但代码里访问的还是旧ID或者两个控件响应同一个消息。根源在于resource.h文件。VS的资源编辑器在添加新资源时会扫描现有的最大ID值然后加1。但如果你手动改过ID或者从别的项目拷贝了资源就可能出现重复。排查方法打开resource.h按ID值排序看有没有重复的数字。如果有手动改掉其中一个然后重新生成解决方案。更彻底的办法是用资源符号对话框在资源视图里右键 - 资源符号里面能看到所有符号和对应的值重复的会标红。另外删除控件后resource.h里的ID定义不会自动删除会留下孤儿定义。这些不影响编译但会让文件越来越乱。定期清理一下是个好习惯。5.3 字符编码问题的排查思路中文乱码是MFC开发的高频问题。表现有好几种编辑框输入中文显示成问号、MessageBox里的中文变成乱码、从文件读取的中文路径打不开。排查第一步确认项目字符集。项目属性 - 高级 - 字符集是Unicode还是多字节。如果是Unicode所有字符串字面量应该用_T(中文)或者L中文包裹。直接用中文在Unicode下会被当成窄字符串转换时可能丢失信息。第二步确认源文件编码。VS2017默认用UTF-8保存源文件但如果没有BOM编译器可能按本地代码页GBK解析。解决办法是在文件 - 高级保存选项里把编码改成UTF-8带签名。这样编译器能正确识别中文字符串。第三步如果是从文件或网络读取的数据确认数据的编码格式。CString的Format函数在Unicode下用的是宽字符如果传入的是UTF-8的char*需要先用MultiByteToWideChar转换。我遇到过一个案例程序在开发机上中文正常部署到客户机器上就乱码。原因是开发机的系统区域设置是中文客户机器是英文。解决办法是在程序启动时调用setlocale(LC_ALL, )或者统一用Unicode API不依赖系统区域设置。5.4 常见编译错误速查表错误代码错误信息原因解决办法MSB8041此项目需要MFC库安装时未勾选MFC组件安装器里勾选MFCC1083无法打开源文件afxwin.hMFC头文件路径未配置检查使用MFC设置LNK1104无法打开文件mfc140ud.lib工具集与MFC库版本不匹配改工具集或装对应MFCRC1015cannot open include file afxres.h项目路径含中文挪到英文路径C2065IDC_BUTTON1: 未声明的标识符resource.h未包含或ID被删检查include和资源符号C2664无法将参数从const char*转换为LPCTSTR字符集不匹配用_T()包裹字符串LNK2001无法解析的外部符号消息映射函数未实现检查DECLARE_MESSAGE_MAP和实现C4996函数不安全使用了被标记为deprecated的函数用_CRT_SECURE_NO_WARNINGS或替换函数这张表里的错误我基本都遇到过其中C2664和LNK2001是最常见的。C2664通常是字符集问题LNK2001通常是消息映射的声明和实现不匹配比如头文件里声明了OnBnClickedBtnGettext但cpp里函数名拼错了或者忘了写BEGIN_MESSAGE_MAP。6. 从Demo到实用工具的扩展思路6.1 添加定时器实现周期性任务工具类程序经常需要定时刷新数据比如每隔一秒读一次传感器。MFC里用SetTimer和OnTimer实现。在OnInitDialog()里调用SetTimer(1, 1000, NULL)第一个参数是定时器ID第二个是间隔毫秒数。然后在类向导里添加WM_TIMER消息处理函数void CDemoMfcDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { // 周期性任务 CString strTime; strTime.Format(_T(%02d:%02d:%02d), GetCurrentTime().GetHour(), GetCurrentTime().GetMinute(), GetCurrentTime().GetSecond()); SetDlgItemText(IDC_STATIC_RESULT, strTime); } CDialogEx::OnTimer(nIDEvent); }注意最后要调用基类的OnTimer否则MFC内部的一些定时器可能不工作。程序退出前记得KillTimer(1)虽然对话框销毁时系统会自动清理但显式调用是个好习惯。6.2 读写配置文件的简单方案工具程序通常需要保存一些配置比如上次打开的串口号、窗口位置等。最简单的方式是用WritePrivateProfileString和GetPrivateProfileString读写INI文件。// 写配置 WritePrivateProfileString(_T(Settings), _T(Port), _T(COM3), _T(.\\config.ini)); // 读配置 TCHAR szPort[32] {0}; GetPrivateProfileString(_T(Settings), _T(Port), _T(COM1), szPort, 32, _T(.\\config.ini));INI文件的优点是简单、可读、跨平台虽然Windows API是Windows特有的但格式通用。缺点是只支持字符串数字需要自己转换。对于小型工具足够了。如果配置项比较多可以考虑用JSON。MFC本身不带JSON库需要引入第三方库比如nlohmann/json或者RapidJSON。引入方法把库的头文件放到项目目录下在需要使用的地方#include即可。注意JSON库通常需要C11支持VS2017默认是支持的。6.3 程序打包与部署注意事项MFC程序编译出来是exe但依赖MFC的DLL。如果目标机器没装VC运行库程序会报缺少mfc140u.dll。解决方案有两种。第一种是静态链接MFC项目属性 - 高级 - 使用MFC改成在静态库中使用MFC。这样exe会变大大概几MB但不需要额外的DLL。第二种是动态链接但把VC运行库一起打包。VS2017的安装目录下有vc_redist.x86.exe和vc_redist.x64.exe部署时一起拷过去先装运行库再运行程序。我一般推荐静态链接省事。但要注意静态链接的exe如果用了GPL协议的库可能会有许可证问题。商业项目要确认所有依赖库的许可证兼容性。另外如果程序需要管理员权限比如访问某些硬件需要在项目属性 - 链接器 - 清单文件 - UAC执行级别里改成requireAdministrator。这样程序启动时会弹UAC提示。6.4 后续可以深入的方向这个Demo跑通之后你可以往几个方向深入。一是多线程MFC的AfxBeginThread可以创建工作线程配合自定义消息PostMessage更新UI。注意UI更新必须在主线程做工作线程只负责计算或IO。二是自定义控件继承CWnd或者CButton重写OnPaint实现自绘。工业软件里常见的仪表盘、指示灯、曲线图都是自绘控件。三是数据库MFC可以通过ODBC或者ADO访问数据库。小工具用SQLite最方便把sqlite3.c和sqlite3.h加到项目里就能用不需要额外安装。四是串口通信Windows下用CreateFile打开COMx然后ReadFile和WriteFile。MFC没有封装串口类需要自己写或者用第三方库。注意串口读写最好放在独立线程里避免阻塞UI。这些方向每一个都能展开写一篇长文但核心的MFC框架知识是通用的。把对话框、消息映射、控件操作这三样吃透剩下的就是查API文档和积累经验了。我个人在实际操作中的体会是MFC虽然老但它的消息映射机制和DDX/DDV数据交换设计其实很优雅理解了之后写Windows原生程序会觉得很顺手。新手最大的障碍不是语法而是对Windows消息机制和资源系统的陌生感。多动手拖几个控件多打断点看调用栈很快就能找到感觉。最后再分享一个小技巧VS2017的类向导CtrlShiftX是MFC开发的神器添加消息处理、添加虚函数重写、添加成员变量都可以在里面完成比手动写映射宏靠谱得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑