简介:这份资源面向具备一定C/C++基础、准备踏入Windows桌面开发的初学者,系统梳理VC++6.0与MFC框架的入门路径。内容从打好语言基础讲起,强调先通过字符界面程序熟悉C++特性,再深入Windows消息机制,讲解消息组成、窗口句柄、消息队列与消息循环的运作方式,并重点剖析MFC消息映射的原理与宏实现,同时给出参考书选择原则和阅读他人代码等学习方法建议,帮助读者少走弯路。资源包共1个doc文档,约521KB,以文字讲义形式集中呈现上述知识点,便于随时查阅与反复研读。目前已有730人学习,适合希望循序渐进理解MFC开发核心机制、为后续Windows应用开发打牢根基的入门读者。
1. 从消息驱动到 MFC 封装:一张 VC++6.0 入门知识地图
很多人学 VC++6.0 的 MFC,第一反应是打开向导生成一个单文档程序,然后对着满屏自动生成的代码发懵。翻车的根源不在代码量,而在于跳过了底层那层“消息驱动”的世界观。MFC 入门真正要建立的地图其实只有三层:最底下是 Windows 的消息机制与句柄体系,中间是 C++ 的封装、派生与虚函数如何把句柄包装成对象,最上面才是 MFC 的消息映射、文档视图与 GDI 绘图。这三层里,底层决定你能不能看懂报错,中间层决定你能不能写出可复用的代码,顶层决定你能不能快速搭出界面。适合谁?适合已经会写 C/C++ 字符界面程序、准备跨进 Windows 开发门槛的人;也适合学过一阵 MFC 但一直靠抄代码、想回头把体系补全的人。这张地图先立在这里,后面每一章都是往这三层里填细节。
2. 地基三问:C++ 基础、句柄与消息机制到底怎么串起来
2.1 为什么先写字符界面程序再碰 Windows
原文把“好的 C/C++ 基础”放在第一条,这不是客套。MFC 的每一个类几乎都在考你对 C++ 特性的熟练度:CString靠运算符重载做拼接,CRect靠构造函数重载支持四种建法,消息映射靠宏展开,窗口类靠派生复用。如果指针、引用、构造析构、虚函数这些还没写顺,直接进 MFC 就会把“编译不过”和“逻辑不对”混在一起,排查成本翻倍。
所以入门阶段的正确顺序是:先用字符界面把语言练熟,重点练三件事——指针与内存、类与继承、常见运行时错误(越界、野指针、未初始化)。这一步不是浪费时间,而是把后面调试 MFC 时的变量范围缩小。梳理下来会发现,MFC 里大量“莫名其妙崩溃”最后都能追到 C++ 基本功上,比如对象已销毁还在用、临时对象被选进 DC 后失效。
2.2 消息的组成与窗口过程
Windows 是消息驱动的系统,这句话要落到具体结构上才算理解。一条消息由三部分构成:消息名称(UINT类型,如WM_PAINT)、WPARAM、LPARAM两个参数。用户输入或窗口状态变化时,系统把消息投递给某个窗口,窗口过程(WNDPROC)用switch分拣,感兴趣的自己处理,不感兴趣的交给默认窗口过程。
// 窗口过程骨架:只处理关心的消息,其余交还系统 LRESULT CALLBACK MyWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_PAINT: // 窗口需要重绘 DoPaint(hWnd); break; case WM_LBUTTONDOWN: // 鼠标左键按下 DoClick(LOWORD(lParam), HIWORD(lParam)); break; case WM_COMMAND: // 菜单/控件命令,命令 ID 在 HIWORD(wParam) HandleCommand(HIWORD(wParam)); break; default: return DefWindowProc(hWnd, msg, wParam, lParam); // 关键:默认处理 } return 0; }这段代码里最容易被忽略的是default分支。窗口拖动、尺寸变化会产生大量消息,全靠DefWindowProc兜底,你才能只写自己关心的部分。参数含义也要记牢:鼠标坐标在lParam的低字和高字里,命令 ID 在wParam的高字里,取错位置就是典型的“点了没反应”。
2.3 句柄与消息队列:16 位和 32 位的差别
句柄是系统范围内唯一标识一个对象的整数,窗口句柄(HWND)标明消息该送给哪个窗口。两个窗口可以共用同一份窗口过程代码,靠句柄区分实例,这就是“代码复用、实例独立”的雏形。
消息队列则决定多任务模型。16 位系统只有一个系统消息队列,当前任务不处理完,别的程序拿不到控制权,这叫协同式多任务;32 位系统每个程序各有自己的消息队列,系统可以在队列间切换,这叫抢先式多任务。消息循环的写法是:
while (GetMessage(&msg, NULL, 0, 0)) // 无消息时阻塞,不占 CPU { TranslateMessage(&msg); // 把按键消息翻译成字符消息 DispatchMessage(&msg); // 派发到目标窗口过程 }GetMessage在没有消息时不返回,所以循环不会空转烧 CPU,这一点和“死循环卡死系统”是两回事,复习时别混。
3. 消息映射:MFC 把 switch 藏进了宏里
3.1 从 BEGIN_MESSAGE_MAP 到 newWndProc
MFC 没有让你手写窗口过程,而是用一组宏把“消息 → 成员函数”的对应关系登记成表。你写的是:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_WM_CREATE() // WM_CREATE -> OnCreate ON_COMMAND(ID_FONT_DROPDOWN, DoNothing) // 命令 ID -> 成员函数 END_MESSAGE_MAP()编译后,宏展开成一个统一的窗口过程,内部仍是switch:WM_CREATE调OnCreate,WM_COMMAND里再比对HIWORD(wParam)是否等于ID_FONT_DROPDOWN。理解这一层,你就明白两件事:第一,消息映射不是玄学,本质是宏生成的分派表;第二,所有该类的窗口实例共用这一个窗口过程,实例差异靠this指针区分。
3.2 消息映射与虚函数的分工
初学者常问:既然 C++ 有虚函数,为什么还要消息映射?答案是两者解决不同问题。虚函数解决“同一接口、不同实现”的多态;消息映射解决“成百上千种消息,只处理其中几种”的分派。用虚函数为每种消息建一个虚函数,类会臃肿到不可维护;用消息映射,只登记你关心的条目,其余自动落到基类默认处理。复习时把这条分工记成一句话:多态用虚函数,消息分派用映射表。
3.3 常用消息与参数速查
| 消息 | 触发时机 | 关键参数含义 |
|---|---|---|
WM_CREATE | 窗口创建时 | 对应OnCreate,常做初始化 |
WM_PAINT | 需要重绘 | 必须配对BeginPaint/EndPaint |
WM_LBUTTONDOWN | 左键按下 | 坐标在lParam低字/高字 |
WM_COMMAND | 菜单、按钮等命令 | 命令 ID 在HIWORD(wParam) |
WM_SIZE | 窗口尺寸变化 | 新宽高在LOWORD/HIWORD(lParam) |
WM_TIMER | 定时器到期 | 定时器 ID 在wParam |
这张表建议背下来,原文特别强调“记住常用消息名称和参数意义”,因为调试时第一眼看的就是消息类型和参数取值。
4. 绘图与 GDI 对象:DC、字体、画笔刷子的正确姿势
4.1 OnDraw 与设备无关性
绘图入口是CView派生类的OnDraw(CDC* pDC)。它的价值在于设备无关:窗口重绘传屏幕 DC,打印传打印机 DC,打印预览传预览 DC,一份代码三处复用。这就是 Windows 设备无关性的直接体现,也是 MFC 单文档结构最值得先掌握的原因。
4.2 GUI 对象的创建、选入、恢复、销毁
字体CFont、刷子CBrush、画笔CPen、位图CBitmap都是 GDI 对象,使用必须遵守固定节奏:创建合法对象 → 选入 DC 并保存旧对象 → 使用 → 恢复旧对象 → 销毁。原文给的安全写法值得逐行看:
void CMyView::OnDraw(CDC* pDC) { CPen pen1, pen2; pen1.CreatePen(PS_SOLID, 2, RGB(128,128,128)); // 创建 CPen* pOld = pDC->SelectObject(&pen1); // 选入并保存旧笔 DrawSomething(pDC); pen1.DeleteObject(); // 重建前先销毁 pen1.CreatePen(PS_SOLID, 2, RGB(0,0,0)); // 再次创建 pDC->SelectObject(&pen1); DrawAgain(pDC); pDC->SelectObject(pOld); // 恢复,别漏 }最容易翻车的是最后一步:把临时对象的指针留在 DC 里,对象析构后 DC 还指着它,下次绘图就异常。库存对象(BLACK_PEN、NULL_BRUSH、SYSTEM_FONT等)可以安全留在 DC 中,常用来做恢复。
4.3 文字输出与字体参数
单行文字用TextOut,多行、对齐、省略号用DrawText,格式标志可位或组合:DT_CENTER水平居中、DT_VCENTER垂直居中(须配DT_SINGLELINE)、DT_WORDBREAK按词折行、DT_END_ELLIPSIS尾部省略。颜色用SetTextColor,背景用SetBkColor,透明背景用SetBkMode(TRANSPARENT)。
自定义字体走CreateFontIndirect(LOGFONT*)或CreateFont十四个参数,重点记:nHeight为 0 取默认高度、nWeight常用FW_NORMAL(400)与FW_BOLD(700)、bItalic/bUnderline控制样式、lpszFacename指定字体名。文字尺寸用GetTextExtent算宽高,精确度量用GetTextMetrics填TEXTMETRIC。
4.4 画笔风格与画点效率
CreatePen(nPenStyle, nWidth, crColor)中,PS_SOLID实线不限宽,而PS_DASH、PS_DOT、PS_DASHDOT等虚线风格要求宽度为 1,宽度大于 1 时虚线会退化成实线,这是高频踩坑点。画点用SetPixel/GetPixel,但效率低,成片绘图应改用画笔或位图。
5. 常用类与开发套路:CRect、CString 和结构选型
5.1 三个高频类
CRect四个成员left/top/right/bottom,支持四种构造(四坐标、RECT结构、左上角加尺寸、两对角点),成员函数Width/Height/Size/CenterPoint常用,矩形还能与点相加做位移、与矩形相加做并集。CPoint只有x/y,可与点相加。CString自动管理内存,GetLength/GetAt/Find/Compare/CompareNoCase/MakeUpper/MakeLower和Format(相当于sprintf)是主力。CStringArray提供Add/RemoveAt/RemoveAll/GetAt/SetAt/InsertAt。
| 类 | 核心成员 | 最常用操作 |
|---|---|---|
CRect | left/top/right/bottom | Width、Height、CenterPoint、并集 |
CPoint | x/y | 点相加做位移 |
CString | 内部缓冲 | Format、Find、CompareNoCase |
CStringArray | CString 集合 | Add、GetAt、InsertAt |
5.2 全局函数与宏
AfxGetApp取应用对象,AfxGetInstanceHandle取实例句柄,AfxGetResourceHandle取资源句柄,AfxMessageBox弹消息框。宏里RGB组颜色、TRACE输出调试信息、ASSERT断言、VERIFY在发布版仍求值,调试期用它们定位问题比打断点更快。
5.3 结构选型:什么时候用对话框、SDI、多视
原文给了一套很实用的选型经验:读写文件且有简单输入输出,用单文档视结构;交互为主、文件读写简单,用对话框加CFile;交互重且文件读写复杂,用CFormView为基础的单文档;多文档需求不强烈时,用分隔条做单文档多视,别急着上多文档;确实要在多文档间传数据才用多文档结构。另外尽量用子窗口封装多个控件、避免多重继承、别让一个类塞太多功能。这套经验的价值在于帮你少走“结构选错、后期重构”的弯路。
6. 易错点排查:这五处最容易翻车
易漏点一:消息参数取错字。为什么漏:WPARAM/LPARAM是 32 位,高低字各存一份信息,凭印象取容易错位。怎么补:命令 ID 记HIWORD(wParam),鼠标坐标记LOWORD/HIWORD(lParam),写之前先查表。
易漏点二:GDI 对象用完不恢复。为什么漏:代码能跑通,异常只在特定重绘时机出现。怎么补:SelectObject的返回值必须保存并在结束前恢复,临时对象绝不留在 DC 里。
易漏点三:虚线画笔设了大于 1 的宽度。为什么漏:不报错,只是画出来变实线。怎么补:虚线类风格宽度固定为 1,要粗线就用实线。
易漏点四:WM_PAINT里不配对BeginPaint/EndPaint。为什么漏:偶尔能画出来,但刷新异常、消息堆积。怎么补:OnPaint里严格配对,绘图逻辑尽量放OnDraw。
易漏点五:对象销毁后继续使用。为什么漏:C++ 基本功不牢,析构时机没概念。怎么补:重建前先DeleteObject,成员对象注意生命周期,调试期多用ASSERT兜底。
7. 复习清单:按这条线查漏补缺
复习这份笔记,建议按“底层 → 映射 → 绘图 → 选型”的顺序自测,每一层都能用自己的话讲出来才算过。
| 复习项 | 自测问题 | 过关标准 |
|---|---|---|
| 消息机制 | 一条消息由哪几部分组成? | 说清名称、WPARAM、LPARAM 及典型取值 |
| 消息循环 | GetMessage无消息时为何不占 CPU? | 能解释阻塞与派发流程 |
| 消息映射 | 宏展开后是什么结构? | 能画出 switch 分派示意 |
| GDI 使用 | 选入、恢复、销毁的顺序? | 能默写安全绘图代码骨架 |
| 字体参数 | nHeight为 0 表示什么? | 说清默认高度与匹配规则 |
| 结构选型 | 交互重、读写复杂选哪种? | 答CFormView单文档 |
查漏补缺的具体做法:拿一张白纸,先默写窗口过程骨架和消息循环,再默写BEGIN_MESSAGE_MAP到展开的对应关系,最后默写一段带恢复的OnDraw。哪一步卡住,就回到对应章节重读,而不是从头再刷一遍。另一个高效方法是找一段别人写的 MFC 代码,只看消息映射表和OnDraw,试着反推它的交互流程,这比被动看书更能暴露理解盲区。
我自己的血泪经验是:早年学 MFC 时总想跳过消息机制直接抄向导代码,结果一遇到“按钮点了没反应”就只能干瞪眼,后来强制自己每次新建工程都先手写一遍窗口过程和消息循环,再回头看 MFC 的宏,才真正把黑匣子打开。从那以后我每次碰新框架,都强制先走一遍“最小可运行 + 底层机制”的流程,再谈上层封装。这份梳理按同样的思路整理,希望帮到你。
本文还有配套的精品资源,点击获取