简介:这份资源面向具备一定C/C++基础、准备踏入Windows桌面开发的学习者,聚焦VC++6.0环境下MFC框架的入门路径,帮助读者理清从语言基础到消息驱动编程的学习顺序,避免一上手就陷入窗口代码的误区。压缩包内为1个doc文档,体积约521KB,以文字讲义形式系统整理了入门要点,便于随时查阅与做笔记。文档围绕C++特性运用、Windows消息机制、窗口句柄与GUI句柄、MFC消息映射等核心概念展开,并给出消息组成、窗口过程、消息队列与消息循环的伪代码示例,同时附有参考书选择原则与阅读他人代码的建议,可作为入门阶段的知识框架与自查清单。目前已有730人学习,适合希望低成本建立MFC整体认知的初学者。
1. 从消息驱动到 MFC 封装:一张 VC++6.0 入门知识地图
很多人学 MFC 的第一反应是打开向导、拖控件、点编译,结果窗口是弹出来了,可一旦要改行为就完全不知道代码该往哪写。问题不在手笨,而在于跳过了中间那层地图:Windows 是消息驱动的,MFC 只是把消息机制、句柄和 API 用 C++ 类包了一层。这张地图大致分四块:C/C++ 语言底子、Windows 消息机制与句柄、MFC 消息映射与常用类、GDI 绘图对象。前两块决定你能不能看懂程序在干什么,后两块决定你能不能写出东西。这份笔记按「基础 → 机制 → 封装 → 绘图 → 查漏」的顺序梳理,适合已经能写 C++ 字符界面程序、正准备进 Windows 开发的人,也适合学过一遍 MFC 但知识点散落一地、需要重新串起来复习的人。
2. 语言与机制地基:C++ 底子和 Windows 消息模型
2.1 为什么先写字符界面程序再碰窗口
原文把「好的 C/C++ 基础」放在第一条,并且明确建议:最开始接触 VC 时不要急于做 Windows 程序,先写一些字符界面程序。这个建议听起来像拖延,其实是在降低后面的调试成本。MFC 开发里大量报错不是框架问题,而是指针、对象生命周期、构造析构顺序没搞清。字符界面阶段练的是三件事:对语法的熟悉度、编程思维、以及对常见错误的敏感度。
更关键的是 C++ 特性在 MFC 里的直接映射。原文说得很清楚:利用封装性更容易理解和操作各种窗口对象;利用派生性可以减少开发自定义窗口的时间、造出可重用代码;利用虚拟性可以在必要时更好地控制窗口活动。换句话说,MFC 的类层次就是继承加虚函数,CWnd 派生、CView 派生、自定义窗口类,全是这套东西。C++ 不熟,看 MFC 头文件就像看天书。
复习时可以用一张对照表把语言特性和 MFC 用法绑在一起记:
| C++ 特性 | 在 MFC 中的体现 | 复习时要能回答的问题 |
|---|---|---|
| 封装 | 句柄被包进 C++ 对象,如 CWnd 包 HWND | 成员函数背后调的是哪个 API |
| 继承 | CView、CFrameWnd、CDialog 的类层次 | 自己的类该从哪个基类派生 |
| 虚函数 | 窗口过程、OnDraw 等可重写行为 | 哪些函数是框架回调、哪些要自己调 |
| 构造/析构 | GUI 对象创建与销毁、DeleteObject | 对象何时创建、何时必须销毁 |
提示:判断自己基础够不够,一个简单标准是——能不能不查书说清一个类的构造、使用、销毁三段生命周期。说不清就先回去补字符界面程序。
2.2 消息的组成、投递与窗口过程
Windows 是消息驱动的操作系统。原文给了一个很实用的拆解:一个消息由消息名称(UINT)和两个参数(WPARAM、LPARAM)组成。当用户输入或窗口状态改变时,系统把消息发送到某个窗口。比如菜单被选中后会有 WM_COMMAND 消息,WPARAM 的高字(HIWORD(wParam))是命令 ID,对菜单来说就是菜单 ID。用户也可以自定义消息名称,用自定义消息来发送通知和传送数据。
消息必须由一个窗口接收,窗口过程(WNDPROC)里对消息做分析,只处理自己感兴趣的部分。没处理的消息去哪了?系统为窗口编写了默认窗口过程,负责处理你不处理的消息。正因为有它,拖动窗口产生的一堆消息可以完全不理,交给系统自己处理。这一点是理解 MFC 消息映射的前提。
窗口句柄是另一根支柱。系统通过窗口句柄在整个系统中唯一标识一个窗口,发送消息时必须指定句柄表明由哪个窗口接收。原文举的例子很到位:两个窗口共用一份窗口过程代码,在窗口一上按下鼠标,消息会通过窗口一的句柄送到窗口一而不是窗口二。句柄是「哪个窗口」,窗口过程是「怎么处理」,两者分开记,后面就不会混。
消息循环的伪代码值得抄一遍,它解释了 Windows 为什么能同时跑多个任务:
// 消息循环:没有消息时 getMessage 不返回,控制权交回系统 while (1) { id = getMessage(...); // 从本程序的消息队列取一条消息 if (id == quit) break; // 收到退出消息则结束循环 translateMessage(...); // 翻译消息(如键盘字符消息) dispatchMessage(...); // 按窗口句柄分派到对应窗口过程 }这段逻辑的要点在最后一句注释:没有消息通知时 getMessage 不返回,也就不占 CPU。原文还对比了 16 位和 32 位系统:16 位系统只有一个消息队列,系统必须等当前任务处理完消息才能发下一条,一个程序死循环系统就拿不到控制权,这叫协同式多任务;32 位系统每个运行的程序都有自己的消息队列,系统可以在多个队列间转换,不必等当前程序处理完,这叫抢先式多任务。这个对比在选择题和面试里都常见,记「队列数量」和「是否等待」两个差异点即可。
2.3 消息映射:宏展开后到底变成了什么
MFC 对消息的处理用消息映射,基础是宏定义,通过宏把消息分派到不同成员函数。原文给了一段典型的映射表:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) //{{AFX_MSG_MAP(CMainFrame) ON_WM_CREATE() //}}AFX_MSG_MAP ON_COMMAND(ID_FONT_DROPDOWN, DoNothing) END_MESSAGE_MAP()编译后它被替换成类似下面的形式(原文说明这只是讲解,实际情况复杂得多):
// 宏展开后的等价结构:窗口过程里用 switch 分派消息 CMainFrame::newWndProc(...) { switch (...) { case WM_CREATE: OnCreate(...); // ON_WM_CREATE 映射到这里 break; case WM_COMMAND: if (HIWORD(wP) == ID_FONT_DROPDOWN) { DoNothing(...); // ON_COMMAND 映射到这里 } break; } }理解这段展开,就理解了 MFC 开发的基本思路:newWndProc 就是窗口过程,只要是该类的实例生成的窗口都使用这个窗口过程。所以「消息机制 + 消息映射」两件事打通,剩下的就是查类、查函数。复习时不要背宏的写法,要能画出「消息 → 映射宏 → 成员函数 → 窗口过程」这条链。
3. MFC 常用类与开发套路:从 CRect 到文档视图选型
3.1 常用类速查:CRect、CPoint、CString、CStringArray
原文列出的常用类不多,但都是高频。CRect 表示矩形,四个成员变量 top、left、bottom、right,分别表示左上角和右下角坐标。构造方式有五种:四个坐标、由 RECT 结构构造(两个重载)、由左上角坐标加尺寸构造、由两个点构造。成员函数要记住 Width、Height、Size、TopLeft、BottomRight、CenterPoint。还有一个容易忽略的点:矩形可以和点相加做位移,和另一个矩形相加得到「并」操作后的矩形。
CPoint 表示点坐标,两个成员 x、y,可以和另一个点相加。CString 表示可变长度字符串,不用指明内存大小,按需自行分配。常用成员函数包括 GetLength、GetAt、operator+(相当于 strcat)、Format(相当于 sprintf)、Find、Compare、CompareNoCase、MakeUpper、MakeLower。CStringArray 是可变长度字符串数组,元素为 CString 实例,常用 Add、RemoveAt、RemoveAll、GetAt、SetAt、InsertAt。
这几个类建议按「构造 → 读写 → 增删 → 转换」四类操作整理成自己的卡片。原文还列了常用宏 RGB、TRACE、ASSERT、VERIFY,以及常用函数 AfxGetApp、AfxGetInstanceHandle、AfxGetResourceHandle、AfxMessageBox。AfxMessageBox 用于弹出消息框,参数为文本、类型(默认 MB_OK)、帮助 ID(默认 0)。这些名字在调试和查错时出现频率极高,混个脸熟比死记签名有用。
3.2 开发通用方法:文档视图结构怎么选
原文 1.4 节给了一组很实在的选型经验,本质是「按交互复杂度和文件读写复杂度选结构」。整理成决策表更好复习:
| 场景特征 | 推荐结构 | 理由 |
|---|---|---|
| 需要读写文件,输入输出简单 | 单文档视结构 | 结构简单,够用 |
| 注重交互的简单程序 | 以对话框为基础的窗口 | 交互集中,文件读写简单时可用 CFile |
| 注重交互且文件读写复杂 | 以 CFormView 为基础视的单文档视结构 | 表单式布局加文档管理 |
| 多文档要求不强烈 | 单文档多视结构(用分隔条) | 避免多文档的复杂度 |
| 需要在多个文档间传递数据 | 多文档视结构 | 这是多文档真正的用武之地 |
其余几条是「避坑型」建议:利用对话框得到用户输入,水平提高后再用就地输入;学会利用子窗口,在自定义子窗口里包含多个控件达到封装功能;尽量避免多文档多视结构;不要使用多重继承,尽量减少一个类中封装过多功能。这几条背后是同一个原则——控制复杂度。复习时可以把它们当成「默认选择 + 例外条件」来记,而不是九条孤立规则。
3.3 参考书怎么挑:初学者与进阶者两套标准
原文对选书给了明确原则,这部分常被跳过,但对自学的人价值很高。初学者应选内容全面、安排合理、能循序渐进的书,代码要有详细讲解;尽量买翻译的书,因为一般比较易懂、语言轻松;买书前一定要慎重,买到不好用的书可能打击学习积极性。已经掌握 VC 的人则应加深对系统原理和技术要点的认识,选原理讲得透彻的书,最好对技术应用有一定阐述;尽量选示范代码精简的书。
配套的学习习惯原文也列了:训练自己写代码时不翻参考书而是用 Help Online;记住一些常用消息名称和参数意义;学会看别人的代码;多看书少买书;闲下来就看参考书。这些不是鸡汤,是降低「查资料成本」的具体手段。Help Online 用熟了,等于随身带一份和编译器版本匹配的文档,比任何二手笔记都准。
4. GDI 绘图对象:DC、字体、画笔与刷子的使用规则
4.1 GUI 对象的通用使用规则与安全写法
Windows 中有各种 GUI 对象,注意不要和 C++ 对象混淆。原文列出的对象包括:字体对象 CFont(风格、大小、斜体、粗体、字体名称、下划线等,颜色和背景色不属于字体属性)、刷子 CBrush(填充区域的颜色或模板,可用 8×8 位图创建自定义模板刷子)、画笔 CPen(颜色、宽度、线风格如虚线实线点划线)、位图 CBitmap(可保存在资源中),以及多边形(用于限制作图区域或改变窗口外型)。
使用规则是这一节的重点,也是最容易翻车的地方:先创建合法对象;把对象选入 DC,同时保存 DC 中原来的对象;选入非法对象会引起异常;使用完恢复原来的对象;如果保存的是临时对象,而临时对象被销毁后可能引起异常;每一个对象在重新创建前必须先销毁。原文给了一段安全写法:
void CMyView::OnDraw(CDC* pDC) { CPen pen1, pen2; pen1.CreatePen(PS_SOLID, 2, RGB(128,128,128)); // 创建第一个画笔 pen2.CreatePen(PS_SOLID, 2, RGB(128,128,0)); // 创建第二个画笔 CPen* pOldPen = (CPen*)pDC->SelectObject(&pen1); // 选入并保存原对象 // drawWithPen1... pDC->SelectObject(&pen2); // 换第二个画笔 // drawWithPen2... pen1.DeleteObject(); // 重新创建前先销毁 pen1.CreatePen(PS_SOLID, 2, RGB(0,0,0)); // 再次创建 pDC->SelectObject(&pen1); // 再次选入 // drawWithPen1... pDC->SelectObject(pOldPen); // 恢复原来的对象 }这段代码的注释就是检查清单:创建、选入、保存旧对象、销毁、重建、恢复,六步一个不能少。另外系统提供库存 GUI 对象,可用 CDC::SelectStockObject 选入,包括 BLACK_BRUSH、DKGRAY_BRUSH、GRAY_BRUSH、HOLLOW_BRUSH、LTGRAY_BRUSH、NULL_BRUSH、WHITE_BRUSH、BLACK_PEN、NULL_PEN、WHITE_PEN 以及若干字体和 DEFAULT_PALETTE。这些对象留在 DC 中是安全的,所以可以用选入库存对象的方式恢复 DC。
DC(设备环境)是抽象的作图环境,可能对应屏幕,也可能对应打印机。它设备无关,对不同设备输出只需换 DC,作图方式不变。原文用照相机和复印机打比方,很贴切。OnDraw 的优点也在这里:窗口重绘时自动调用,传入的 CDC* 对应 DC;打印时传入打印机 DC,打印预览时传入 CPreviewDC,一份代码完成窗口、打印预览、打印机三重功能。
4.2 文字输出:TextOut 与 DrawText 的参数组合
输出文字一般用两个函数:CDC::TextOut(x, y, str) 只能输出单行;CDC::DrawText(str, lpRect, nFormat) 可以在矩形中输出单行或多行,并规定对齐方式和风格。nFormat 是多个标记的位或组合,常用标记整理如下:
| 标记 | 作用 | 使用注意 |
|---|---|---|
| DT_SINGLELINE | 单行输出 | 回车换行不再断行 |
| DT_CENTER / DT_LEFT / DT_RIGHT | 水平对齐 | 居中、左、右 |
| DT_VCENTER | 垂直居中 | 必须与 DT_SINGLELINE 组合 |
| DT_BOTTOM / DT_TOP | 底部/顶部对齐 | 底部对齐须与 DT_SINGLELINE 组合 |
| DT_WORDBREAK | 按单词折行 | 超出矩形自动断行 |
| DT_CALCRECT | 只计算所需矩形尺寸 | 不绘制文字,返回高度 |
| DT_END_ELLIPSIS / DT_PATH_ELLIPSIS | 省略号替换 | 路径省略保留最后反斜杠后文本 |
| DT_NOPREFIX | 禁止 & 前缀处理 | 避免 & 被当作下划线指令 |
| DT_EXPANDTABS / DT_TABSTOP | 展开制表符/设宽度 | 默认每 TAB 八个字符 |
| DT_NOCLIP | 不裁剪绘制 | 速度略快 |
颜色和背景用 CDC::SetTextColor 和 CDC::SetBkColor 设置;需要透明背景用 CDC::SetBkMode,参数 OPAQUE(默认,先用背景色填充)或 TRANSPARENT(不改变背景)。字体分库存字体(CDC::CreateStockObject)和自定义字体,自定义时填充 LOGFONT 结构后用 CFont::CreateFontIndirect,或直接用 CFont::CreateFont 的十四个参数。原文详细列了 nHeight(逻辑单位,零为缺省高度)、nWidth(零则按可用横纵比匹配)、nEscapement(出口矢量与 X 轴角度)、nOrientation(基线与 X 轴角度)、nWeight(FW_NORMAL 为 400、FW_BOLD 为 700 等)、bItalic、bUnderline、cStrikeOut、nCharSet、nOutPrecision、nClipPrecision、nQuality、nPitchAndFamily、lpszFacename。文本坐标计算用 CDC::GetTextExtent 得到字符串宽高,手工输出多行时算行距;更精确的计算用 CDC::GetTextMetrics 填充 TEXTMETRIC 结构。
4.3 画笔与刷子:风格取值和宽度限制
画点用 CDC::SetPixel(x, y, crColor),返回原颜色;取色用 CDC::GetPixel。原文提醒应少用画点函数,因为执行效率比较低。画笔通过 CPen::CreatePen(nPenStyle, nWidth, crColor) 创建,nPenStyle 可取 PS_SOLID(实线)、PS_DASH(虚线)、PS_DOT(点线)、PS_DASHDOT(点划线)、PS_DASHDOTDOT(双点划线)、PS_NULL(空线,不产生输出)、PS_ENDCAP_ROUND(结束处圆形)、PS_ENDCAP_SQUARE(结束处方形)。
这里有一个高频易错点:PS_DASH、PS_DOT、PS_DASHDOT、PS_DASHDOTDOT 都要求宽度为 1 或更小(设备单位),宽度大于 1 时这些风格不生效。刷子用于画封闭曲线时填充内部,固定色刷子的属性是颜色、是否采用网格和网格类型(水平、垂直、交叉等)。画笔和刷子创建后必须选入 DC 才会在绘图中产生效果,这条和 4.1 的规则是同一条,只是对象不同。
5. 易错点清单:消息、映射与 GDI 对象最容易翻车的地方
5.1 易错点一:把句柄和窗口过程混为一谈
为什么漏:句柄和窗口过程经常在同一段代码里出现,初学者容易记成「一个窗口一个处理函数」,忽略了多个窗口可以共用一份窗口过程代码。怎么补:记住分工——句柄回答「消息发给哪个窗口」,窗口过程回答「收到后怎么处理」。复习时用原文那个双窗口例子自测:两个窗口共用窗口过程,在窗口一按下鼠标,消息通过窗口一的句柄送到窗口一。能复述这个例子,概念就稳了。
5.2 易错点二:以为没处理的消息会丢失
为什么漏:写业务代码时习惯「不处理就不管」,于是以为消息没人接就消失了。怎么补:系统有默认窗口过程,负责处理你不处理的消息,拖动窗口产生的大量消息就是交给它。理解这一点,才不会在消息映射表里试图穷举所有消息,也才明白为什么只写自己关心的 ON_ 宏就够了。
5.3 易错点三:GDI 对象用完不恢复、重建前不销毁
为什么漏:代码能跑通、画面看起来正常,问题往往在多次重绘或打印预览时才暴露,属于典型的「当时不报错、后面翻车」。怎么补:把 4.1 那段安全写法当模板,每次写绘图代码强制走一遍「创建 → 选入 → 保存旧对象 → 销毁 → 重建 → 恢复」。特别注意保存的是临时对象时,临时对象销毁后再恢复会出异常。
5.4 易错点四:虚线画笔设了大于 1 的宽度
为什么漏:参数表里 nWidth 和 nPenStyle 并排,很容易随手写个宽度 2 配 PS_DASH,然后疑惑为什么画出来是实线。怎么补:记死「虚线、点线、点划线、双点划线宽度必须为 1 或更小」,需要粗虚线只能自己用画点或分段绘制实现。这类参数限制在原文里是明写的,复习时把画笔风格和宽度限制做成一行对照。
5.5 易错点五:文档视图结构一上来就选最复杂的
为什么漏:向导里多文档、多视选项看起来「功能全」,初学者容易觉得选全一点不会错。怎么补:回到 3.2 的决策表,默认从单文档或对话框起步,只有确实需要在多个文档间传递数据时才上多文档;多文档多视结构尽量避免。结构选错,后面每一行代码都在为这个决定还债。
6. 复习落地:一份可执行的 MFC 查漏补缺清单
复习这件事,最怕的是把笔记从头读一遍,读完还是不会写。我的做法是把这份材料拆成「能自测的动作」,按天推进,每天只验证一个模块,验证不过就回到对应章节补。下面这张清单可以直接抄走:
| 模块 | 自测动作 | 通过标准 |
|---|---|---|
| C++ 地基 | 不查书说清一个类的构造、使用、销毁 | 能说出对象何时创建、何时必须销毁 |
| 消息机制 | 手写消息循环伪代码并解释 getMessage 为何不占 CPU | 能讲清 16 位与 32 位消息队列差异 |
| 消息映射 | 画出「消息 → 映射宏 → 成员函数 → 窗口过程」链 | 能解释 BEGIN_MESSAGE_MAP 展开后长什么样 |
| 常用类 | 默写 CRect 五种构造和 CString 六个常用函数 | 能说出矩形相加的两种含义 |
| 结构选型 | 给五个场景各选一种文档视图结构并说明理由 | 与 3.2 决策表一致 |
| GDI 绘图 | 默写安全使用画笔的六步 | 不漏「保存旧对象」和「恢复」 |
| 文字输出 | 说出 DT_VCENTER、DT_BOTTOM 的组合限制 | 知道二者须配 DT_SINGLELINE |
具体执行上,我建议用「三遍法」。第一遍只做地图:把第 2 章到第 4 章的一级标题默写出来,写不出的章节就是薄弱区。第二遍做细节:每天挑一个模块,合上笔记复述该模块的表格和代码注释,复述卡壳的地方做标记,第二天先补标记。第三遍做串联:找一个最小的模拟项目,比如一个能画线、能输出文字、能响应菜单命令的单文档程序,从向导生成开始,每一步都问自己「这行代码对应地图上哪一块」,对不上就说明还有盲区。
还有一个习惯值得养成:把 Help Online 当第一查询入口。原文把「训练自己写代码时不使用参考书而是使用 Help Online」单列一条,是有道理的——参考书会过时,在线帮助和你的编译环境是配套的。查一个函数时顺手看它的参数表和返回值,比背十遍笔记都牢。常用消息名称和参数意义也要靠这个方式积累,比如 WM_COMMAND 的 WPARAM 高字是命令 ID,这种细节查一次记一次,几次之后就成条件反射了。
最后说个我自己的教训。早些年复习这类框架,我总喜欢把笔记读得很顺,觉得「看懂了就是会了」,结果一上手写绘图代码就忘了恢复旧画笔,窗口一重绘颜色就乱,查了半天才定位到 SelectObject 之后没恢复。从那以后我每次碰 GDI 对象,都强制走一遍「创建、选入、保存、销毁、重建、恢复」这六步,哪怕代码只有三行。复习不是把知识再读一遍,而是把每个容易翻车的动作变成肌肉记忆。希望这份清单能帮到你,把散落的知识点真正串成一张能用的地图。
本文还有配套的精品资源,点击获取