
简介面向准备进入 VC6.0 与 MFC 开发的学习者这份入门资料以一篇 doc 文档梳理从 C/C 基础到 Windows 消息驱动编程的关键路径适合刚接触 Windows 桌面开发、需要建立整体认知的初中级读者。压缩包内仅 1 个 doc 文件约 521KB篇幅集中便于通读与检索。内容围绕扎实 C/C 基础、理解消息组成与消息循环、窗口句柄及 GUI 句柄用途、掌握 MFC 消息映射等要点展开并补充借助 Help Online 查资料、阅读他人代码、按阶段选择参考书等学习方法。文中还讨论了 MFC 借助 C 封装、继承与消息映射简化 Windows 开发的优势可帮助读者少走弯路形成可继续深入的学习框架。已有 731 人学习。1. 结论先摆出来MFC 入门的瓶颈不在类库在三条链路第一次打开 VC6.0 建 MFC 工程ClassWizard 弹出一堆复选框我盯了半天点了完成编译运行出来一个空白窗口然后彻底卡住——不知道下一行该写哪儿。后来我把 MFC 的类层次硬背了一遍还是写不出一个能响应按钮的程序。真正让我开窍的是把消息怎么从系统跑到我那个函数里这条链路自己画了三遍。MFC 入门听着像学一个类库实际考的是三件事C 底子够不够、Windows 消息机制懂不懂、消息映射会不会看。下面按我踩过的顺序摊开新手能照着排学习路线已经在写的能对上号找到自己卡在哪一环。2. 打底这一关字符界面练手、常用类速查、程序骨架怎么挑2.1 先写三个字符界面程序再打开 ClassWizard很多人学 VC 的第一反应是赶紧拖控件、赶紧出界面我当年也这么干结果是被一堆编译错误和空白窗口反复教育。字符界面程序的价值在于错了有明确报错崩了有明确行号。MFC 里错了往往是一个窗口没画出来、一个按钮没反应你连从哪儿查都不知道。要给 MFC 打底字符界面阶段至少要练透四样东西。第一是类与对象的生命周期构造函数什么时候跑、析构函数什么时候跑、成员对象和指针成员的区别——MFC 里 CPen、CBrush 这些 GDI 对象的创建销毁顺序出问题程序就是直接异常。第二是指针和引用的传参MFC 的函数签名满屏都是CDC* pDC、LPCTSTR lpszText、CRect rect分不清值传递和引用传递改了半天发现改的是副本。第三是继承与虚函数CPen 继承自 CGdiObject、CView 继承自 CWnd不理解基类指针调派生实现消息映射那章根本读不下去。第四是运算符重载和 const 正确性CRect能和CPoint相加、CString的等价于 strcat这些都是重载出来的。我一般会建议按这个顺序写三个小东西一个用链表或数组做的成绩管理练指针、内存和结构体、一个图形基类 圆 矩形的求面积程序练虚函数正好给后面的 GDI 铺垫、一个字符串处理小工具练自己封装一个类似 CString 的类体会自动扩容的难点在哪。三个写完再打开 ClassWizard你会发现它生成的代码你至少能看懂一半了。提示字符界面阶段不要用任何框架纯控制台。这一步省下的时间后面会用几倍的调试时间还回去。2.2 CRect、CPoint、CString 速查每天都要用的那几个成员MFC 的类有几百个但真正每天都在敲的就那么几个。把下面这张表记住写绘图和字符串处理代码的速度会明显不一样。类常用成员说明CRectWidth()/Height()/Size()取宽、高、尺寸CSizeCRectTopLeft()/BottomRight()/CenterPoint()取左上角、右下角、中心点返回 CPointCRect四种构造四个坐标一个 RECT一个 LPCRECT左上角点 尺寸两个对角点CPointx/y两个公有成员变量可直接读写CPointoperator点和点相加做位移CStringGetLength()/GetAt(int)取长度、取指定位置字符CStringFormat(LPCTSTR, ...)等价于 sprintf但目标是自己CStringFind/Compare/CompareNoCase查找、比较、忽略大小写比较CStringMakeUpper()/MakeLower()转大写、转小写CStringArrayAdd/RemoveAt/RemoveAll增、删指定位置、清空CStringArrayGetAt/SetAt/InsertAt取、改、指定位置插入几个容易记混的点CRect 和 CPoint 相加是整体位移CRect 和另一个 CRect 相加是求并集不是逐坐标相加CString 的内存自己管不要拿free去释放它的缓冲区CStringArray 里存的是 CString 对象的拷贝GetAt返回的是引用改它等于改数组里的元素。2.3 单文档、对话框、CFormView按交互复杂度挑骨架新手最容易犯的错是默认选了 MDI然后一路硬扛。原文给的那套选择方法我基本认同整理成一张按需求查的表更直观程序特征建议骨架理由要读写文件输入输出简单单文档视SDI文档/视图分离文件操作有现成位置放交互为主文件读写也简单对话框为基础省掉文档模板控件直接摆交互为主文件读写复杂以 CFormView 为基础视的 SDI既能摆控件又有文档管理需要在多个文档之间传数据多文档视MDI这是 MDI 真正不可替代的场景要多视图但不需要多文档分隔条做 SDI 多视图比 MDI 简单得多除了结构选择还有几条我吃过大亏的原则。多文档多视结构能不用就不用它的消息路由和视图同步足够让新手怀疑人生不要用多重继承MFC 本身大量依赖单继承加虚函数你再加一层多继承调试器里的调用栈会变得很难读一个类里不要塞太多功能宁可多拆几个自定义子窗口把一组控件封装进去也比在一个 View 类里写两千行强。自定义子窗口这个技巧值得早学它让封装这个概念从书本上的词变成你手上能用的东西。2.4 选书初学者要全面进阶要原理买前一定翻三页关于参考书我的看法和原文一致但要说得更具体。初学者选书看三条内容覆盖面够全别买只讲对话框的、章节安排是循序渐进的前后有依赖关系不是知识点罗列、代码有逐行讲解只有代码没有解释的书等于电子版示例库。翻译的书通常语言更轻松、更易懂作为入门读物是合适的。已经能写程序的阶段选书标准反过来要找讲系统原理和技术要点讲得透的最好带一点应用示例但示例代码要精简——几千行的示例工程你根本读不完只会增加挫败感。买之前一定要在线翻三页正文看它讲一个函数的时候是只列参数还是会告诉你什么场景下该用它、用错了会怎样。买到一本不合用的书损失的不只是钱是学习积极性这个亏我吃过不止一次。3. 消息机制与消息映射把黑匣子拆到 switch 那一层3.1 一条消息的解剖UINT 加两个参数配一个窗口句柄Windows 是消息驱动的系统这句话谁都会背但真正理解它要落到结构上一条消息就是一个消息名称UINT加两个参数WPARAM、LPARAM。消息名称决定发生了什么两个参数承载跟这件事有关的数据。用户输入、窗口状态变化系统都会往某个窗口投递消息。窗口句柄HWND是这套机制的地址簿。系统靠它在整个系统里唯一标识一个窗口投递消息时必须指定句柄才能保证消息进到正确的窗口过程。举个容易想通的例子两个窗口共用同一份窗口过程代码你在窗口一上按鼠标消息通过窗口一的句柄送进去窗口二收不到——代码是同一份但句柄不同实例就不同。WM_COMMAND 是新手最常打交道的一条。菜单被选中、按钮被点击都会走这条消息命令 ID 藏在HIWORD(wParam)里。菜单项的 ID 就是资源编辑器里给它起的那个值按钮的通知码藏在LOWORD(wParam)里lParam则是发出通知的控件句柄。这三个位置分清楚了你写点击按钮改标题这类代码就不会再靠猜。// 窗口过程所有消息最终都会走到这个 switch 上 LRESULT CALLBACK MyWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_PAINT: // 窗口需要重绘 DoPaint(hWnd); return 0; case WM_LBUTTONDOWN: // 左键按下lParam 低字是 x高字是 y DoClick(LOWORD(lParam), HIWORD(lParam)); return 0; case WM_COMMAND: // 菜单/按钮HIWORD(wParam) 是命令 ID OnCommand(LOWORD(wParam), HIWORD(wParam), (HWND)lParam); return 0; default: return DefWindowProc(hWnd, uMsg, wParam, lParam); // 不关心的交给系统 } }这段伪代码的关键在最后那个default分支。系统为每个窗口准备了默认窗口过程你不想处理的消息全都交给它窗口照样能拖动、能最小化、能响应系统菜单。这一点很重要MFC 之所以能让你只写自己关心的那几个函数底层靠的就是这个不处理就下沉的机制。LOWORD和HIWORD是把 32 位参数拆成两个 16 位值的宏鼠标坐标这种一个参数塞两个值的场合到处都是。3.2 消息映射宏展开后到底是什么理解了窗口过程再看 MFC 的消息映射就不玄了。你写的是宏编译器展开的是一张消息到成员函数的分派表。// 你在 .cpp 里写的 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_WM_CREATE() // 对应 WM_CREATE ON_COMMAND(ID_FONT_DROPDOWN, OnFontDropdown) // 对应 WM_COMMAND 且 ID 匹配 END_MESSAGE_MAP()展开之后大致是这样一层结构真实实现更复杂这里只保留分支关系// 编译器展开后的等效逻辑 LRESULT CMainFrame::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_CREATE: return OnCreate((CREATESTRUCT*)lParam); // 参数原样转给成员函数 case WM_COMMAND: if (HIWORD(wParam) ID_FONT_DROPDOWN) // 命令 ID 对上才进自己的函数 { OnFontDropdown(); return 0; } break; } return CFrameWnd::WindowProc(uMsg, wParam, lParam); // 都不匹配就交给父类 }BEGIN_MESSAGE_MAP的第二个参数就是父类展开后变成最后那一行都没命中就交给父类的调用。这就是为什么你只写十几个函数窗口却能响应上百种消息——没写的全顺着继承链往上走了。ON_WM_CREATE()这种不带参数的宏靠的是固定的命名约定WM_CREATE 对应OnCreateWM_PAINT 对应OnPaint签名也必须对得上。搞错一个参数类型编译能过运行时直接崩。3.3 消息循环与 16 位、32 位队列的差别消息产生后进队列程序从队列里取取到就派发。这个循环是所有 Windows 程序的骨架MSG msg; while (GetMessage(msg, NULL, 0, 0)) // 队列空时在这里阻塞不占 CPU { TranslateMessage(msg); // 把 WM_KEYDOWN 翻成 WM_CHAR DispatchMessage(msg); // 按 hwnd 找到窗口过程并调用 } return msg.wParam; // GetMessage 返回 0 表示收到 WM_QUITGetMessage在没有消息时会挂起把控制权交还系统所以你的程序空闲时 CPU 占用接近零。TranslateMessage是把按键消息翻译成字符消息的那一步少了它编辑框里敲键盘出不来字。DispatchMessage负责按句柄找到窗口过程这一步是系统做的你插不上手。16 位系统和 32 位系统的区别值得提一句因为它解释了为什么老程序一卡全卡。16 位系统全系统只有一个消息队列系统必须等当前任务处理完消息才轮到下一个某个程序陷入死循环整个系统就失去响应这种叫协同式多任务。32 位系统每个运行的程序有自己的消息队列系统可以在多个队列之间切换不必等某个程序处理完这种叫抢先式多任务。所以你在自己程序里写了一个耗时循环受害的只是你自己的窗口别的程序照常跑。理解这一层你就明白为什么长时间计算要拆成小块、或者放到别的线程里去做。4. GDI 对象与 DC绘图不崩的三条规矩4.1 DC 是抽象的作图环境OnDraw 一份代码顶三处DC设备环境是一个抽象的作图环境它可能对应屏幕也可能对应打印机。这个抽象带来的好处叫设备无关性同一套作图代码换个 DC 就换个输出设备作图方式一行都不用改。MFC 把这份好处落在了OnDraw上。窗口需要重绘时它自动被调用传进来的CDC* pDC就是当前该用的 DC。打印时传进来的是打印机 DC打印预览时传进来的是一个专门做预览的 DC 派生类。所以你只要把绘图代码写在OnDraw里窗口显示、打印预览、实际打印三件事一次做完。这是 MFC 里少见的写一遍省三遍的地方值得早点养成习惯。输出文字用两个函数就够了。TextOut(x, y, str)只能出单行坐标是起点DrawText(str, lpRect, nFormat)在一个矩形里排版能多行、能对齐、能省略。nFormat是一堆DT_标志的位或组合DT_SINGLELINE单行、DT_CENTER水平居中、DT_VCENTER垂直居中必须配单行、DT_WORDBREAK按单词折行、DT_END_ELLIPSIS尾部超长显示省略号、DT_NOPREFIX关掉的助记符处理。文字颜色用SetTextColor背景色用SetBkColor想去掉背景色块就用SetBkMode(TRANSPARENT)。行距怎么算GetTextExtent(str)拿到这个字符串输出时的宽和高手工排多行就靠它。要更精确的字体度量用GetTextMetrics填出来的结构体能描述字体的各种属性。这两个函数配合比拍脑袋写死行高靠谱得多。4.2 画笔、刷子、字体创建、选入、恢复的顺序不能乱画笔决定线条刷子决定填充字体决定文字长相。三者的使用流程是同一套创建合法对象 → 选进 DC 并保存原对象 → 用完恢复原对象 → 销毁对象。创建画笔用CreatePen(nPenStyle, nWidth, crColor)。风格里PS_SOLID实线、PS_DASH虚线、PS_DOT点线、PS_DASHDOT点划线、PS_NULL空线画了等于没画。这里有一个必须记住的限制虚线、点线、点划线这几种宽度只能是 1填 2 以上系统会画成实线不报错就是效果不对查起来很费时间。字体是最麻烦的一个。两种创建方式CreateFontIndirect填一个 LOGFONT 结构体或者直接调CreateFont传一长串参数。常用的几个参数含义如下参数含义常用取值nHeight字符逻辑高度0 表示用系统默认高度并做匹配nWidth字符逻辑宽度0 表示按可用字体的宽高比匹配nWeight字重粗细FW_NORMAL400FW_BOLD700bItalic / bUnderline / cStrikeOut斜体 / 下划线 / 删除线0 或 1nCharSet字符集ANSI_CHARSET、DEFAULT_CHARSET、GB2312 相关nQuality输出质量DEFAULT_QUALITY 够用PROOF_QUALITY 用于精确排版lpszFacename字体名用EnumFontFamiliesEx枚举系统已装字体颜色和背景色不属于字体属性它们是 DC 的属性别在字体上找。4.3 库存对象是最好的后悔药DeleteObject 的时机要卡准GDI 对象有两个坑一个是忘了销毁一个是销毁得太早。原始那份笔记里的示例把顺序写对了我照着讲透void CMyView::OnDraw(CDC* pDC) { CPen pen1, pen2; pen1.CreatePen(PS_SOLID, 2, RGB(128, 128, 128)); // 创建灰色实线宽 2 pen2.CreatePen(PS_SOLID, 2, RGB(128, 128, 0)); // 创建橄榄色实线 CPen* pOldPen pDC-SelectObject(pen1); // 选入同时记住 DC 里原来的笔 DrawWithGray(pDC); // 这一段用 pen1 pDC-SelectObject(pen2); // 换成 pen2 DrawWithOlive(pDC); pen1.DeleteObject(); // 同一对象重新创建前必须先销毁 pen1.CreatePen(PS_SOLID, 2, RGB(0, 0, 0)); pDC-SelectObject(pen1); DrawWithBlack(pDC); pDC-SelectObject(pOldPen); // 收尾把原来的笔还回 DC }三个动作的因果必须清楚。第一SelectObject会返回 DC 里原来的对象这个返回值一定要存下来不存就没法恢复。第二同一个 CPen 对象在重新CreatePen之前必须先DeleteObject否则底层句柄泄漏短时间看不出问题长时间运行会失败。第三DC 里不能留着一个已经销毁的对象——如果你把局部 CPen 选进 DC函数返回时它析构了DC 里就是一个野句柄下一次绘图直接异常。这类异常没有明确行号排查起来就是玄学所以顺序必须从一开始就写对。库存对象是最好的后悔药。系统预置了一批固定颜色的刷子、画笔和基本字体BLACK_BRUSH、WHITE_BRUSH、HOLLOW_BRUSH、NULL_BRUSH、BLACK_PEN、WHITE_PEN、NULL_PEN、SYSTEM_FONT、ANSI_FIXED_FONT等用SelectStockObject(nIndex)直接选进去。这些对象常驻系统不会因为你没销毁而泄漏所以既能当省事的默认值也能当恢复现场时的兜底。5. 避坑与排查五条血泪经验每条都能省你半天5.1 画笔第二次创建就异常现象程序第一次绘图正常第二次调用同一个 CPen 重新CreatePen时弹异常或者直接崩溃。原因同一个 GDI 对象没调用DeleteObject就再次创建底层句柄冲突。另一种情况是对象被选在 DC 里的时候把它销毁了DC 里留下野句柄。解决把创建—选入—恢复—销毁当成一个固定模板来写每次创建前先if (m_pen.GetSafeHandle()) m_pen.DeleteObject();保险一下成员 CPen 不要放在OnDraw里临时创建挪到OnInitialUpdate里创建一次OnDraw里只做选入和恢复。5.2 加了处理函数却没反应现象手写了一个OnBnClickedOk签名看着没错编译也过了点按钮就是没进断点。原因函数写了但消息映射里没加对应条目。ON_COMMAND(IDC_BTN_OK, OnBnClickedOk)或者ON_BN_CLICKED那行漏了。另一种是宏写对了但 ID 拼错或者函数签名跟宏要求的参数类型不一致展开后类型不匹配进不去。解决优先用 ClassWizard 添加处理函数它会同时改 .h 声明和 .cpp 的消息映射表手写的时候改完立刻在映射表里搜一遍那个函数名搜不到就是没加。签名对不上时从 ClassWizard 生成一个同名空函数把里面的代码搬过去这是最快的确诊方式。5.3 文字只出一行或被截断现象DrawText输出长文本只有第一行后面全没了或者文字旁边多出奇怪的下划线。原因忘了加DT_WORDBREAK多行文字不会自动折行或者 lpRect 给的高度不够。下划线那个是被当成助记符前缀处理了才会输出一个。解决多行输出固定用DT_WORDBREAK或DT_SINGLELINE二选一别都不给排版前先用DT_CALCRECT让DrawText算出需要多高再按算出来的高度绘制不想让被解释就加DT_NOPREFIX。5.4 窗口一拖动画的东西全没了现象客户区画的图形窗口被其他窗口遮住再恢复、或者拖动改变大小之后图形消失。原因绘图代码没有写在OnDraw或OnPaint里而是塞在按钮响应或者其他函数里。窗口重绘时系统只走OnPaint这条链你写在别处的代码不会被重新执行。解决所有针对客户区的绘图都放进OnDraw需要主动刷新时调用Invalidate()或InvalidateRect()请求重绘不要直接去调绘图函数需要立即刷新就补一句UpdateWindow()。5.5 照书抄函数名和注释对不上现象参考书上写MakeUpper是改为小写、MakeLower是改为大写照着理解写逻辑结果大小写正好反过来。原因参考书和官方文档里的笔误、翻译错误或者版本差异导致函数行为和书中描述不一致。解决以在线帮助Help Online的原厂说明为准书只用来建立框架理解具体函数签名和行为一律现查。养成这个习惯之后你会发现查一次的时间比调试半小时短得多。6. 从能跑到能读一个我坚持了很多年的动作现在回头看学 MFC 期间收益最高的一个动作不是把类库背下来而是逼自己写代码时不翻参考书只翻在线帮助。刚开始极其痛苦查一个函数要在索引里翻半天还经常查不到。但坚持两三个月之后你会建立起一种依赖——相信查得到、习惯去查、并且知道该往哪个关键词上查。这个能力直接决定了你后面能不能自学新东西。具体的做法很简单遇到不认识的函数先按 F1 或者打开在线帮助看它的原型、参数、返回值、备注里的限制条件。特别注意备注段那些这个标志只有配合某某标志才有效宽度必须为 1返回值为空不代表失败的限制参考书上经常不写但真出问题全在这儿。常用的消息名称和参数意义也要刻意记WM_COMMAND 的两个参数里装什么、WM_PAINT 的 wParam 什么时候有用记住这几条读别人代码的速度是量级差别。第二件事是学会看别人的代码。找一段能跑起来的、结构干净的 MFC 示例从 WinMain 或 InitInstance 往下跟看它怎么注册窗口类、怎么建主窗口、消息映射表里挂了哪些条目、每个条目最后落到哪个函数。跟三遍你对 MFC 的运行顺序就有了肌肉记忆。我一般还会把消息映射表抄一遍抄的时候把每个宏对应的消息名写在旁边抄完基本就记住了。那份入门笔记本身的价值也在这儿它把消息机制的讲法、GDI 对象的使用顺序、常用类的成员清单、以及程序结构怎么选按从底到上的顺序串成了一条线没有跳到高级话题上去炫技。如果你正卡在窗口出来了但不知道下一步写什么把它的 1.1 到 2.3 照着走一遍比我上面说的任何一条都管用。到现在我写任何一段涉及资源创建和销毁的代码都会强制走一遍创建—选入—保存旧值—恢复—销毁这五步哪怕只是临时画一条线。这个习惯救过我太多次早年间查过的一个 GDI 泄漏从发现到定位花了整整两天最后就是少了一行DeleteObject。希望帮到你。本文还有配套的精品资源点击获取