
简介这是《精通MFC》一书的配套光盘源代码适合希望系统掌握MFC框架、Windows界面编程与组件开发的C开发者。资源按章节组织覆盖面向对象基础、窗口与消息映射、对话框、文档/视图结构、GDI/GDI绘图、进程线程、动态链接库、COM组件乃至.NET托管扩展等主题对应书中大量可运行示例便于读者边读边练。压缩包共1114个文件以h头文件、cpp源文件、rc资源脚本和vcproj工程文件为主另有少量ico、bmp、jpg等素材与项目配置文件包体约8.01MB可直接配合Visual Studio等环境查看编译。目前已有513人学习下载。对正在研读该书的读者来说这些源代码能帮助理解MFC底层机制、掌握类库调用方式并可作为实际项目开发的参考模板节省从零搭建环境的时间。1. 精通MFC光盘源代码从“能编译”到“能看懂”中间隔着一整条调试链路拿到《精通MFC》光盘源代码第一件要做的事不是打开书而是按F7编译。至少一半人会在这里第一次翻车afxwin.h打不开、char*转CString报错、_WIN32_WINNT不满足。这套源码是学MFC绕不开的经典资源但它的价值不在于把示例编译出来而在于你知道每一行代码为什么存在。它适合已经掌握C基础、想在Windows桌面软件开发里真正用MFC干活的人。如果你还在纠结桌面软件开发用MFC还是Qt先把这套源码里的对话框、消息映射、状态栏跑通再谈选型。2. 读懂这套源码前先看MFC程序的骨架破解WinMain、消息映射和消息路由2.1 先判断源码属于哪个年代目录结构、工程后缀与源代码管理我拿到任何一套旧光盘源码第一件事不是双击工程文件而是先看根目录。旧书配套光盘通常按章铺目录每章一个文件夹下面挂着一个或多个独立工程公共类被单独放在Common或Include目录里多个工程往上两级引用它。如果根目录里躺着ReadMe或安装说明先读它里面往往写着“需要Visual Studio 6.0”这类运行前提。判断年代最直接的方式是看后缀。VC6时代的工作区是.dsw工程是.dsp从VS2002到VS2008工程变成.sln加.vcproj到VS2010以后工程又变成.sln加.vcxproj。后缀决定了你能用什么IDE顺利打开它。即使是.dsw格式也不用急着找VC6虚拟机拿VS2022直接打开它会进入一次工程转换向导转换后另存为新工程。转换有概率失败所以动手之前先把整张光盘复制到本地。复制不是简单拖一遍文件夹而是要过滤掉生成目录。以前的光盘里经常带着编译好的Debug和Release目录体积大且含旧路径放进自己机器会干扰排查。“源代码管理”对老光盘源码尤其重要先初始化一个git仓库把原始文件原封不动提交一次之后每一步改动都留痕。命令行做这一套最干净robocopy E:\光盘\Source D:\MFC_Legacy /E /XD Debug Release /XF *.pdb *.obj cd D:\MFC_Legacy git init git add . git commit -m initial import: legacy MFC samples before migrationrobocopy的/E复制所有子目录/XD排除Debug和Release/XF排除中间产物这样导入git的才是真正的源代码。git提交记录就是你的“后悔药”迁移改坏了大不了回滚。别信光盘自带的一键安装器那些程序多半是十多年前写的会在当前系统里干出预期之外的活。注意替换路径前一定提交一次git这能让你在改坏路径后一分钟内回到原始状态。2.2 MFC程序的最小可运行单元从theApp到InitInstance说回代码本身。一个MFC程序无论界面多复杂入口都长得很像。老光盘示例里最常见的结构一个从CWinApp派生的App类、一个从CDialog或CFrameWnd派生的主窗口类外加一个全局的App对象。全局对象会在main函数之前构造MFC框架里对应的入口是WinMain而你实际写的第一个被调函数是App类的InitInstance。// 常见MFC应用入口写法老示例几乎都长这个骨架 #include stdafx.h #include ExampleApp.h #include ExampleDlg.h BEGIN_MESSAGE_MAP(CExampleApp, CWinApp) ON_COMMAND(ID_HELP, CWinApp::OnHelp) END_MESSAGE_MAP() // 全局对象程序启动时第一个被构造 CExampleApp theApp; BOOL CExampleApp::InitInstance() { // 对话框应用的经典启动路径 CExampleDlg dlg; m_pMainWnd dlg; dlg.DoModal(); return TRUE; }InitInstance里做了三件事构造对话框对象、把对话框指针交给m_pMainWnd、再调用DoModal进入模态循环。DoModal返回之后程序就准备退出。所以阅读源码时如果找不到“从哪开始”先搜全局对象theApp顺着InitInstance往下走整条启动链就通了。SDI和MDI程序会稍有不同主窗口是CFrameWndDocument/View架构还会加上CDocument和CView但万变不离其宗CWinApp负责应用生命周期窗口类负责界面。学习时不要一上来钻进复杂的View类先把对话框这个最小闭环吃透。所谓“精通MFC”真正值钱的不是某段奇技淫巧而是把这条启动链刻进脑子里。2.3 消息映射表看懂宏展开就看清了MFC的调度逻辑MFC和普通Win32程序最大的差异是把“switch(message) case WM_XXX”这堆重复代码折叠成了消息映射宏。每个窗口类里都有一张BEGIN_MESSAGE_MAP到END_MESSAGE_MAP之间的表这张表就是该类的消息路由表。阅读源码时我一般先在类定义里搜索BEGIN_MESSAGE_MAP把这张表单独列出来再去看处理函数比顺着控件事件翻代码高效得多。// 对话框类里的消息映射常见的三种条目 BEGIN_MESSAGE_MAP(CExampleDlg, CDialogEx) ON_WM_LBUTTONDOWN() // 窗口消息鼠标左键按下 ON_BN_CLICKED(IDC_BTN_START, CExampleDlg::OnBnClickedStart) // 控件通知按钮被点击 ON_MESSAGE(WM_USER 1, OnMyMessage) // 自定义消息 END_MESSAGE_MAP()ON_WM_LBUTTONDOWN展开后在消息映射表结构里登记“WM_LBUTTONDOWN由哪个成员函数处理”。控件通知的ON_BN_CLICKED绑定了控件ID和点击事件。自定义消息一定要用ON_MESSAGE手动绑定这是新手最容易漏的消息发出去了窗口却毫无反应。看这张表要同时理解两点。第一消息从上往下找前面的条目优先同一个消息不要重复登记两个处理函数。第二子类没处理的消息会往基类的消息映射继续传这也是为什么宏的第二行要写基类名CDialogEx。如果一条命令注册了但触发不了先查IDC常量是否写对再查控件是否真的属于这个对话框最后查是不是被父窗口的WM_COMMAND拦截了。能按这个顺序排查基本就摸到MFC调度的骨架了。3. 在Visual Studio里把光盘旧工程跑起来从.dsw到.vcxproj的迁移实战3.1 打开.dsw与.sln工程转换的正确姿势与路径替换把光盘源码复制到本地并提交到git后就可以打开工程了。VS2022可以直接打开.dsw或.dsp但严格来说它做的是“转换”而不是“打开”转换向导会生成新的.sln和.vcxproj原文件保持不动。这里有个细节转换完成后VS会提示是否保存新工程如果选否下次打开还是走老文件等于没迁移。我的习惯是转换成功后马上“另存为”到新目录让老工程彻底留作对比。转换过程中最常见的失败提示是“Project xxx could not be loaded”。原因通常是光盘源码里把某些头文件或库写成了硬盘绝对路径例如C:\MFC\Include而当前机器没有这个目录。遇到这种提示先用记事本打开对应.vcxproj或.dsp搜索盘符和反斜杠路径把绝对路径改成相对路径再重新加载。老光盘代码对开发目录要求很高有的甚至要求必须解压到C盘根目录这是当年简化代码的土办法。# 批量把源码中的旧绝对路径替换为当前目录先备份再执行不要全盘替换 $root D:\MFC_Legacy Get-ChildItem $root -Recurse -Include *.dsp,*.dsw,*.vcproj,*.vcxproj | ForEach-Object { (Get-Content $_.FullName -Raw) -replace C:\\LegacySource, $root | Set-Content $_.FullName -NoNewline }替换路径的前提是目录层级一致。VS对工程文件和源文件的相对引用以.vcxproj所在目录为基准源码里如果写的是....\Common复制后只要维持同样的相对层级就不会断。这里最忌讳一键全盘替换因为.dsp和.vcxproj的路径写法不同前者用正斜杠也能用后者对转义更敏感。先挑一两个工程替换、跑通再批量处理。3.2 迁移必调的四个工程设置字符集、工具集、MFC用法、SDK版本打开工程后先别急着按F7把四个关键设置统一检查一遍。它们在旧代码里几乎都不是当前系统需要的值。第一平台工具集。老工程默认为v60或v100当前机器装了哪个版本的VS就选哪个VS2019对应v142VS2022对应v143。第二Windows SDK版本。本机装了更高SDK就选本机版本老代码在Win10/Win11下编译SDK版本太低会触发一堆_WIN32_WINNT不满足的警告。第三MFC的使用方式是“在共享dll中使用MFC”还是“在静态库中使用MFC”。学习用途建议选共享静态库会让每个exe膨胀到几十MB排查问题也更麻烦。第四字符集。这是老代码翻车最密集的地方下一节具体说。四个设置都在“项目属性”里但不同VS版本入口标签有差异VS2022是“配置属性-常规”。我一般先把配置切到“所有配置”把Debug和Release一次性改完避免以后在两种配置间反复对比。改完工具集和SDK后VS会提示需要重新加载项目让它重载然后立刻编译一次记录第一批错误。迁移的耐心这时候最关键不要试图一次性消灭所有错误而是每修一类就编译一次让错误列表自己收敛。老光盘里一个.dsw可能挂着十几个工程直接按F7会把它们全部编译报错信息滚屏。我一般先右键具体示例工程选“生成”让其它工程靠边站。如果某个工程依赖Common项目先编译Common生成的静态库再把依赖路径配好。这一条能省下你在日志海洋里找第一根刺的时间。3.3 批量修复编译错误字符集、C4996与CString转换老代码在VS2022下编译前三类错误基本固定C4996、C2664、以及一串“xxx is not a member of xxx”。C4996是微软把strcpy、sprintf等函数标记为不安全导致的新工程默认报错。最快的手法是加预处理器定义源码不用动// 项目属性 - C/C - 预处理器 - 预处理器定义追加 _CRT_SECURE_NO_WARNINGS加了之后strcpy这类函数不再报C4996。短期用没问题但血泪经验是这只解决了“能编译”没解决“能运行”。如果代码里用sprintf往固定char缓冲区写内容超长数据照样踩坏栈。我一般会在迁移时顺手把明显有风险的sprintf换成CString::Format// 旧写法在Unicode工程里可能报错或隐含截断 char buf[256]; sprintf(buf, value%d, nValue); CString strInfo buf; // 推荐的新写法类型安全且适配Unicode CString strInfo; strInfo.Format(_T(value%d), nValue); SetDlgItemText(IDC_STATIC_INFO, strInfo);第二类C2664多半是字符串类型不匹配。旧代码用char和LPCSTR新工程默认Unicode函数实际要LPCWSTR。修这类错误有个通用口诀源码里的字符串字面量都套上_T()需要传出char的地方改CStringA需要接收宽字符的地方改CStringW。切忌用强制类型转换硬生生把报错压下去那会让运行期得到乱码或直接崩溃。第三类“is not a member”是API改名比较典型的有GetWindowTextLengthA、SetWindowTextA这类带A/W后缀的旧API随着SDK更新很多别名没了。处理办法是全局搜索带A或W后缀的调用按所在工程的字符集去掉后缀或改成CString接口。如果工程暂时不想全面换Unicode也可以先把项目字符集改回“多字节字符集”跑通但必须说清楚Win11环境下多字节字符集的UI中文显示和某些新控件兼容性都在变差这只能作为临时过渡不是长期出路。4. MFC代码里最值得精读的三个模块状态栏、浏览器控件、自绘界面4.1 状态栏显示信息CStatusBar的Pane更新与刷新时机MFC状态栏怎么显示是几乎所有老示例都会碰到的题。很多初学者直接调用SetWindowText但那是改标题栏。真正要在窗口底部灰色条上显示文字得走CStatusBar。状态栏由若干Pane组成通常第一个Pane显示菜单提示后几个显示CapsLock、NumLock等指示器。想在第一个Pane里显示自己的文字用SetPaneText它接受Pane下标。// CMainFrame::OnCreate里的状态栏初始化 static UINT indicators[] { ID_SEPARATOR, // 0号Pane留给动态文字 ID_INDICATOR_CAPS, ID_INDICATOR_NUM, }; if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators) / sizeof(UINT))) { TRACE0(Failed to create status bar\n); return -1; } // 某按钮点击后更新状态栏 void CMainFrame::OnUpdateStatusText() { m_wndStatusBar.SetPaneText(0, _T(正在处理第2步)); // 需要时强制刷新避免文字残留 m_wndStatusBar.Invalidate(); }SetPaneText不会自动清空上一次显示的内容如果前后文字长度不一短文字会留下旧尾巴所以要么每次把Pane清空要么调用Invalidate重绘。需要频繁刷新时不要在每次循环里直接SetPaneText那会造成界面闪烁。常见做法是开一个定时器每300毫秒读一次进度变量再更新状态栏把刷新频率降下来。这个“把刷新频率降下来”的细节就是示例源码和可交付代码之间的差距。更进一步状态栏指示器还可以配合字符串表在资源里定义ID_INDICATOR_RECORD等字符串然后把它加进indicators数组MFC会自动把字符串显示到对应Pane。这种写法比SetPaneText更符合MFC的资源驱动习惯也方便做多语言版本。4.2 IWebBrowser2.CreateEx在对话框里嵌入网页的正确姿势MFC iwebbrowser2 createex是一组出现频率很高的检索词对应的是老版MFC里把IE嵌入对话框的需求。这套代码现在仍能跑但坑比想象中多。先看典型场景在对话框的OnInitDialog里创建一个WebBrowser子窗口。常见做法是先用ClassWizard给对话框添加ActiveX控件VS会生成一个CWebBrowser2包装类如果要从纯代码创建则走CreateEx。// 在对话框类里声明CWebBrowser2 m_web; BOOL CMyDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 1.得到对话框上的占位区域 CRect rc; GetDlgItem(IDC_STATIC_PLACEHOLDER)-GetWindowRect(rc); ScreenToClient(rc); // 2.创建浏览器子窗口 m_web.CreateEx(WS_CHILD | WS_VISIBLE | WS_BORDER, rc, this, 0); // 3.导航注意别在CreateEx同一条调用链里立即Navigate m_web.Navigate(_T(about:blank), nullptr, nullptr, nullptr, nullptr); return TRUE; }CreateEx的第一个参数是窗口样式子窗口至少要WS_CHILD和WS_VISIBLE否则控件创建了也不显示。第三步特别容易翻车把Navigate紧跟在CreateEx后面结果窗口出来了但白屏。原因在于ActiveX控件从CreateEx到内部接口就绪是异步的立即Navigate时浏览器的容器还没完全连接。稳妥写法是PostMessage到本窗口让Navigate延后到下一轮消息循环再执行。线程问题是另一个重灾区。IWebBrowser2的Navigate和内部事件回调都要求在同一线程也就是创建该控件的UI线程。如果你在后台工作线程里调用Navigate轻则崩溃重则整个消息泵被锁死。我一般会在工作线程里只更新一个“待访问地址”的成员变量然后PostMessage给UI线程执行实际导航。这套路同样适用于Print、ShowBrowserBar等其它接口。4.3 自绘控件与双缓冲DrawItem里最容易被忽略的GDI细节老光盘源码里特别喜欢展示自绘按钮、自绘列表框因为那个年代UI美化的技术含量就在这里。自绘的本质是重写DrawItem或OnPaint把所有绘制搬进自己的代码。初学者照抄最容易犯的错是直接在窗口的HDC上画于是“按下”“抬起”的切换过程全是闪烁。原因很直接每次状态变化都会做一次背景擦除和重绘画在屏幕上的中间态一眨眼就过去肉眼却看得到。双缓冲就是“先在内存画完再一次贴到屏幕”。思路是创建兼容DC和兼容位图把全部绘制动作落到内存DC最后用BitBlt一次性输出。这比用FillSolidRect一点点涂要稳定得多代码量也不大void CMyButton::DrawItem(LPDRAWITEMSTRUCT lpDIS) { CDC* pDC CDC::FromHandle(lpDIS-hDC); CRect rc lpDIS-rcItem; CDC memDC; // 内存画布 CBitmap bmp, *pOld; memDC.CreateCompatibleDC(pDC); bmp.CreateCompatibleBitmap(pDC, rc.Width(), rc.Height()); pOld memDC.SelectObject(bmp); // 所有绘制都在memDC上进行 memDC.FillSolidRect(rc, GetSysColor(COLOR_BTNFACE)); memDC.SetBkMode(TRANSPARENT); memDC.DrawText(_T(确定), rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); // 一次性贴到屏幕 pDC-BitBlt(rc.left, rc.top, rc.Width(), rc.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOld); // 恢复旧位图 // bmp和memDC析构时自动释放GDI对象 }这里有几个GDI细节很容易踩坑DrawText的rc会被原地修改如果后面还用rc做命中和布局计算下次拿到的是被压缩后的矩形所以每次绘制都要从GetWindowRect重新取CreateCompatibleBitmap创建的是内存位图画文字前记得SetBkMode(TRANSPARENT)最后SelectObject回旧位图这一步不能省否则位图对象析构时报告资源泄漏Debug窗口会刷出一堆GDI警告。自绘本身不算多高深的技术但它把GDI选择、状态保存、资源释放全部串在一起是检验MFC基本功的好模块。5. 避坑编译光盘源码最常遇到的翻车现场以及排查顺序先说排查顺序这是无数条血泪经验换来的先环境后代码先单个工程后整个工作区先Debug后Release。按这个顺序走能省下一半试错时间反过来一上来就盯着某行代码看大概率看错方向。5.1 现象fatal error C1010预编译头找不到现象是编译某个.cpp时报fatal error C1010在查找预编译头时遇到意外的文件结尾。原因不外乎两种这个.cpp文件第一行没有包含stdafx.h或者工程属性里“预编译头”选的是“使用”而文件本身不符合要求。老光盘示例里许多文件默认依赖stdafx.h一旦工程迁移后预编译头设置丢失整批.cpp都会挂。解决方案在“项目属性-C/C-预编译头”里统一处理绝大多数.cpp应该用“使用/创建”个别不需要的源文件单独设成“未使用”同时保证每个设为使用的cpp都#include stdafx.h。批量检查时我会先搜索工程里所有.cpp文件头部看是不是每个文件都带同一行包含。5.2 现象C2664字符串类型在char*和CString之间互相打架现象是编译报错提示无法将参数从const char转换为LPCTSTR或CString无法转换为LPCSTR。原因是工程字符集从MBCS变成了Unicode而你还用char写字符串。解决时先看报错密集的文件把字符串字面量统一改成_T(...)改用CString和_tcscpy这类双字符版本函数。临时方案是把“字符集”改回“使用多字节字符集”能压掉一大批报错但会让新的Windows API和部分资源编译器产生兼容副作用所以只在判断“代码量过大、需要分几天迁移”时才用它过渡最终还是要回到Unicode。5.3 现象Debug能跑Release一编译就崩现象是Debug配置下运行正常切到Release后崩溃或出现明显逻辑错乱。原因有两类ASSERT和其它调试宏在Release下被编译器删除某些代码路径连着走两条完全不同的流程第二类是Release开启优化后老代码里没初始化的局部变量或被忽略的返回值开始“现原形”。排查时先切到Release并关闭优化如果崩溃消失基本可以断定是未初始化变量问题逐个检查指针和BOOL成员变量并赋初值。如果关闭优化仍然复现导出调用栈看看哪个回调函数在Release里被内联掉了。这步有时会追到MFC源码内部别紧张重点看栈顶是不是有自己写的函数。5.4 现象IWebBrowser2的网页窗口白屏或干脆不显示现象是对话框上有一块占位但创建的浏览器控件既不显示Navigate也没反应。原因来自三个方向控件创建时样式不对缺了WS_CHILD或WS_VISIBLE创建和Navigate在同一个调用链里连续执行COM接口还没就绪或者是ActiveX控件在机器上未注册。排查顺序先确认工具箱里拖WebBrowser到对话框能正常显示能显示说明组件可用再检查代码里CreateEx的返回值S_OK不代表窗口立刻可见需要再调用ShowWindow白屏再往后走用Navigate(_T(about:blank))做最小验证如果about:blank可以显示而目标站点不行局域网、代理设置与IE内核的安全配置各占一半原因。5.5 现象LNK2001 unresolved external symbol链接阶段全军覆没现象是编译全过链接时刷出一串无法解析的外部符号通常是WinMain、AfxWinMain或某个MFC类。原因多半是工程设置里MFC使用方式选错了或链接器没有找到MFC库。新工程默认在“项目属性-常规-使用MFC”选“使用标准Windows库”老示例要改成“在共享dll中使用MFC”才能对上内部的模块定义。另一类原因比较隐蔽老代码把函数定义放在#ifdef _DEBUG的块里Release下那些符号整段消失了。遇到链接错误先查工程设置再查条件编译宏这是唯一能系统化收敛的排查路径。这四条之外还有一个频率高到值得写进习惯的坑目录路径和文件名。老光盘里工程名和文件名经常带空格或中文VS新版本偶尔会触发一些看似玄学的编译错误。我的习惯是把整个源码复制到D:\Work\Legacy这种纯英文、无空格的路径下再迁移。很多“换台机器就编译不过”的怪事最后都能追溯到路径差异。6. 把光盘源码变成自己的代码三个值得长期保持的阅读与改写习惯先回应一个当下流行的问题AI工具能不能帮我读这套源码能用但别全信。AI对MFC老API的掌握容易带幻觉比如把CWnd和CWinApp的职责搞混或生成一个根本不存在的消息映射参数。我的做法是把AI当检索加速器每个结论都在MSDN和代码里核对一遍尤其涉及消息映射和GDI时更要多留个心眼。6.1 每读一个示例在代码里留三个注释问自己三个问题这段代码在演示哪个消息它封装的类解决什么通用问题删掉这段代码程序会怎样把答案写成注释放在代码后面。一百个示例读下来这些注释本身就是你的MFC教程笔记而且比任何别人的笔记都贴合你的知识缺口。6.2 用git diff回放理解过程对任何一段想彻底弄明白的代码先测试原始行为再做一处改动用git diff看这次改动影响的范围。如果程序崩溃二分回滚到最近一次稳定提交。这套习惯能把你从“看起来懂了”推进到“改得出来”也让“源代码管理”真正成为学习工具而不只是备份工具。6.3 花一晚上把示例程序改名重写不要满足于跑通。把CDialog派生类改名、消息处理函数改名、按钮ID改掉再添加一个自己的控件。抄一万行不如自己改通一百行。我早年学MFC有一次为了在自己的程序里显示状态栏翻遍书也没找到直接能抄的例子最后照着SetPaneText的签名一点点试试出来之后那个函数我再没忘过。放心大胆地改git里存着原始版本崩了也能回去。希望帮到你。本文还有配套的精品资源点击获取