2026/11/4 9:39:07

从消息循环到GDI绘图:VC++6.0 MFC底层机制与避坑

从消息循环到GDI绘图:VC++6.0 MFC底层机制与避坑 简介这份VC6.0 MFC入门文档面向具备一定C/C基础、准备进入Windows桌面开发的初学者重点解决从语言基础到MFC框架、消息驱动编程的入门衔接问题。内容围绕C基础巩固、Windows消息机制、窗口句柄与GUI句柄、MFC消息映射、Help Online使用习惯以及参考书选择展开并借消息队列、窗口过程、WM_COMMAND、WM_PAINT等实例说明消息如何产生、投递与处理同时分析MFC借助C封装、派生和消息映射简化Windows开发的优势。压缩包共1个doc文件约521KB篇幅集中适合按主题顺序阅读和查阅。目前已有731人学习可作为VC6.0环境下MFC入门阶段的要点梳理材料帮助读者建立消息映射与句柄概念理解常见消息名称及参数意义并形成阅读他人代码、查阅在线帮助的学习路径。1. VC6.0 配 MFC这套老工具链还值不值得现在动手有人在技术群里甩了一份 VC6.0 的 MFC 入门笔记问还有没有翻的价值。我的判断很直接如果目标只是快速搓一个带按钮的小工具别碰现成框架半天就能出活但如果你想把 Windows 那套消息驱动的底层逻辑真正吃透MFC 反而是条少见的捷径 —— 它用一层很薄的 C 封装把 HWND、WNDPROC、消息队列这些东西原样摆在你面前不像后来那些框架全塞进黑匣子里出问题只能靠猜。这份笔记给的是一条完整闭环C 基本功、消息机制、消息映射、GDI 绘图四块拼起来才立得住缺一块后面全是窟窿。适合两类人刚接触桌面开发、想弄明白窗口背后到底发生了什么的新手以及写过几年上层业务、却始终没搞清消息从哪来到哪去的熟手。2. 从字符界面到 SDI 工程底座怎么打才不返工2.1 先写字符界面程序把 C 特性练到能用笔记里第一条就是要有好的 C/C 基础这句被无数人当成客套话划过去了实际上它是最硬的一条。MFC 的整个类层次完全建立在 C 的封装、派生、虚函数三件套上CWnd派生CFrameWndCView派生你自己的视图类CCmdTarget提供消息映射的虚函数入口。如果对虚函数表和虚析构的语义还含糊调试 MFC 时看到的调用栈会像天书。我一般会让新手先花两三天写字符界面程序不是浪费时间。练这几样东西构造与析构的配对、拷贝语义、虚函数与虚析构、const 正确性、模板容器的基本用法。下面这段代码是我给新人练手用的骨架它和 MFC 没有一行关系但结构上已经预演了消息映射// char_mode_demo.cpp —— 字符界面练手先把语言功底打牢 #include iostream #include string #include map class Command { // 抽象命令基类 public: virtual ~Command() {} // 虚析构派生类资源才能正确释放 virtual void Execute(const std::string arg) 0; virtual const char* Name() const 0; }; class EchoCmd : public Command { public: const char* Name() const { return echo; } void Execute(const std::string arg) { std::cout arg std::endl; } }; class SumCmd : public Command { // 把参数按空格切开求和 public: const char* Name() const { return sum; } void Execute(const std::string arg) { int total 0; std::string::size_type pos 0; while (pos arg.size()) { std::string::size_type next arg.find( , pos); if (next std::string::npos) next arg.size(); if (next pos) total std::atoi(arg.substr(pos, next - pos).c_str()); pos next 1; } std::cout total std::endl; } }; int main() { // 名字 - 命令对象这张表和 MFC 的消息映射表是同一个思路 std::mapstd::string, Command* table; table[echo] new EchoCmd(); table[sum] new SumCmd(); std::string line; while (std::getline(std::cin, line)) { std::string::size_type sp line.find( ); std::string name (sp std::string::npos) ? line : line.substr(0, sp); std::string arg (sp std::string::npos) ? : line.substr(sp 1); std::mapstd::string, Command*::iterator it table.find(name); if (it ! table.end()) it-second-Execute(arg); else std::cout unknown: name std::endl; } std::mapstd::string, Command*::iterator it; for (it table.begin(); it ! table.end(); it) delete it-second; // 归还堆内存 return 0; }这段代码的关键不在功能在结构。std::mapstd::string, Command*这张表负责名字到处理器的映射MFC 的BEGIN_MESSAGE_MAP干的是同一件事只是键从字符串换成了消息 ID 三元组处理器从对象方法换成了成员函数指针。virtual void Execute()这一层虚分发等价于窗口过程里那个switch(uMessageType)。把这段写顺了再去理解ON_COMMAND就不会觉得是魔法。参数上要注意Name()返回const char*指向派生类内部的静态字面量生命周期覆盖整个程序所以安全Execute()的参数按值传const std::string避免每次调用都拷贝一份字符串。std::atoi不检查输入碰到非数字返回 0这在练手阶段可以接受真实项目里要换成带错误检查的解析。这里有个 VC6.0 时代的边界必须提前说清楚它自带的 STL 实现相当老标准里的很多写法编不过。最典型的是for (int i 0; ...)这种循环变量在循环外还可见的作用域问题以及部分模板偏特化写法。我一般的做法是循环变量提前声明不依赖编译器的新特性把可移植性留给自己。另外 VC6 对std::string的substr边界检查几乎为零越界不报错直接读脏内存写完必须自己断言一遍。2.2 用 AppWizard 生成第一个单文档工程并且读懂文件分工语言底子有了就要落到工程上。VC6.0 的 AppWizard 是这个工具链里最值钱的东西它把每个 Windows 程序都必须写的那几百行样板代码一次性生成让你从窗口类注册、CreateWindow、消息循环这些体力活里跳出来。操作路径很固定File → New → Projects 页选 MFC AppWizard (exe)工程名用纯英文路径里不要有中文和空格这一点后面第 5 章会专门讲路径带中文会让类向导直接罢工。第 1 步选 Single document第 2 步数据库选 None第 3 步 OLE 和 ActiveX 全部不勾第 4 步保留 Printing and print preview这一项留着有用 —— 后面验证OnDraw的设备无关性就靠它。第 5 步选项目风格、是否生成注释第 6 步把视图基类留在CView。生成完先别急着写代码把这几个文件的关系理清文件承担的职责出问题时的典型表现工程名.h / .cppCWinApp派生类进程入口在InitInstance启动就退出、主窗口不显示MainFrm.h / .cppCFrameWnd派生主框架窗口挂工具栏和状态栏工具栏按钮全灰、状态栏不刷新工程名Doc.h / .cppCDocument派生管数据存盘丢数据、GetDocument()返回空工程名View.h / .cppCView派生管绘制和交互窗口白屏、重绘丢失StdAfx.h / .cpp预编译头afxwin.h在这里被展开编译奇慢、找不到类型resource.h / .rc菜单、图标、字符串表、对话框模板命令 ID 冲突、菜单项无响应这个 Doc/View 分层是 MFC 的骨架文档管数据视图管显示框架窗口管外框。很多人一开始不习惯把数据直接塞进 View 类里等到要做打印预览或者多视图时就后悔了 —— 一份数据要画两遍只能复制粘贴。第一段正式代码写在 View 的OnDraw里。用 ClassWizardCtrlW确认它已经存在然后加两行输出void CDemoSdiView::OnDraw(CDC* pDC) { CDemoSdiDoc* pDoc GetDocument(); ASSERT_VALID(pDoc); // 调试期检查 Doc 指针是否有效 CRect rcClient; GetClientRect(rcClient); // 客户区矩形原点在左上角 pDC-SetBkMode(TRANSPARENT); // 文字背景透明压在已有内容上不糊 pDC-SetTextColor(RGB(0, 128, 0)); pDC-TextOut(20, 20, _T(first line from OnDraw)); CString sInfo; sInfo.Format(_T(client area: %d x %d), rcClient.Width(), rcClient.Height()); pDC-TextOut(20, 44, sInfo); }GetClientRect的参数类型是LPRECTCRect有隐式转换所以能直接传地址。坐标单位是逻辑单位默认映射模式MM_TEXT下 1 单位就是 1 像素左上角为原点y 轴向下增长 —— 这一点和数学坐标系相反画图前先在纸上标一下方向能省很多调试时间。ASSERT_VALID在 Release 版本里被完全剔除不影响性能只在 Debug 下帮你抓空指针。工程建好之后可以顺手把命令行批量编译也验一遍比每次开 IDE 点半天快# 在工程目录下执行.mak 文件名与工程名一致 nmake /f demoSdi.mak CFGdemoSdi - Win32 ReleaseCFG的值必须和 IDE 里配置名一字不差写错了会直接报NMAKE : fatal error U1073这个报错和源码无关别去翻代码。2.3 三种视图结构怎么选对话框、SDI、CFormView选型错了后面全是返工。笔记里给的经验是读写文件的简单程序用单文档、注重交互的简单程序用对话框这个判断到今天依然成立我把它整理成一张对照表结构适合什么不适合什么上手成本基于对话框CDialog工具类小程序、参数设置、向导式交互需要菜单栏、工具栏、多文档最低SDI CView以绘图、输出为主交互较轻大量表单控件堆叠中SDI CFormView表单加文件读写控件多高频重绘的图形界面中MDI必须同时打开多个文档绝大多数场景高我自己的原则是一句话先问用户主要在打字还是在看图打字多的走CFormView或对话框看图多的走CView。MDI 能躲就躲笔记里那句在对多文档要求不强烈时尽量避免是踩过坑才写得出来的 —— MDI 的子框架窗口会额外引入一层CMDIChildWnd焦点、命令路由、关闭顺序都要多绕一圈收益却往往只是能开多个窗口用分隔条做一个单文档多视图就够了。对话框的调用方式是这套结构里最简单的一环值得单独记一下void CMainFrame::OnToolsSettings() { CSettingsDlg dlg(this); // 传父窗口保证对话框跟随主窗口、居中 if (dlg.DoModal() IDOK) { // 模态阻塞直到用户点确定或取消 m_nRefresh dlg.m_nRefresh; // 只有点确定才回写取消则丢弃 Invalidate(); // 数据变了通知视图重绘 UpdateWindow(); // 立即重绘不等消息队列空转 } }DoModal返回IDOK/IDCANCEL这个返回值是数据是否回写的唯一依据。m_nRefresh是在对话框类里通过 ClassWizard 的 Add Variable 绑到控件上的成员DDX 机制会在DoDataExchange里自动完成控件与成员的同步。Invalidate只是往队列里塞一个WM_PAINTUpdateWindow才会立刻触发重绘两个函数连用是刷新界面的标准组合。3. 消息机制吃透从 HWND 到消息映射表的完整链路3.1 消息的组成UINT 消息名加两个参数Windows 上的一切交互最终都变成一条消息。理解消息的第一步是记住它的结构一个消息编号加上两个 32 位参数。原文里那句一个消息由消息名称UINT和两个参数WPARAM、LPARAM组成是整个 MFC 的地基。MSG结构里几个字段的含义值得背下来字段类型含义hwndHWND接收消息的窗口句柄系统靠它定位窗口过程messageUINT消息编号如WM_PAINT、WM_COMMANDwParamWPARAM32 位附加参数含义由消息类型决定lParamLPARAM32 位附加参数含义由消息类型决定timeDWORD消息投递的时间戳ptPOINT投递时的鼠标屏幕坐标WM_COMMAND是个典型例子它的三个部分各有分工HIWORD(wParam)是命令 ID菜单项就是菜单资源里的 IDLOWORD(wParam)是通知码按钮点击是BN_CLICKEDlParam是控件窗口句柄菜单命令时为零。记住这个拆法看到ON_COMMAND和ON_NOTIFY的区别就一目了然。消息编号分成几个区段写自定义消息不能越界区段范围用途系统消息0x0000–0x03FF系统预定义不能占用WM_USER0x0400–0x7FFF窗口类内部自定义WM_APP0x8000–0xBFFF应用间通信用推荐从这里起步窗口句柄HWND是另一个必须建立直觉的概念。它是系统范围内唯一标识一个窗口的整数发送消息时必须指定目标句柄系统据此找到对应窗口的窗口过程。原文里举的例子很到位两个窗口共用同一份窗口过程代码在窗口一上按下鼠标消息通过窗口一的句柄送到窗口一而不是窗口二 —— 句柄决定了消息投给谁窗口过程只决定收到之后怎么处理。3.2 消息循环与窗口过程谁在什么时候拿到控制权理解消息循环比背 API 更重要。简化到骨头里就是这几行// CWinThread::Run 内部做的事情的简化版 MSG msg; while (::GetMessage(msg, NULL, 0, 0)) { // 取到 WM_QUIT 时返回 0循环结束 if (! AfxPreTranslateMessage(msg)) { // 快捷键、工具提示在这里被提前消化 ::TranslateMessage(msg); // 把 WM_KEYDOWN 转成 WM_CHAR ::DispatchMessage(msg); // 按 msg.hwnd 找到窗口过程并调用 } }GetMessage的行为是这段代码的关键队列里没消息时它阻塞不返回线程挂起不占 CPU。这就是 Windows 能同时跑一堆程序却不把处理器烧掉的原因。对应的PeekMessage不阻塞取不到就立刻返回适合需要在空闲时做事的场景比如游戏主循环或者后台任务的进度刷新。DispatchMessage做的事是根据msg.hwnd找到窗口对象调用它的窗口过程。窗口过程的签名是LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);四个参数和MSG结构一一对应。窗口过程里没处理的消息必须交给DefWindowProc系统会替你处理拖动、缩放、关闭这些默认行为。原文那句正因为有了这个默认窗口过程我们才可以利用窗口进行开发而不必过多关注窗口各种消息的处理说的就是这层兜底。32 位系统上每个线程有自己的消息队列这是抢占式多任务的前提而 16 位时代全系统只有一个队列一个程序死循环整个系统跟着卡死。这个区别解释了一个常见现象为什么在 MFC 里做耗时计算时界面会假死。答案很简单计算占住了消息循环所在线程WM_PAINT、WM_CLOSE全在队列里排着。解决办法只有两个要么在工作线程里算完再PostMessage回主线程要么在长循环里定期PeekMessage抽一下队列。3.3 消息映射宏展开之后长什么样消息映射是 MFC 区别于裸 Win32 最明显的地方也是最多人会用但不懂的地方。原文给了一段展开示意我把它讲得更具体一点。MFC 没有用 C 的虚函数来逐条分发消息 —— 那样每个窗口类都要背一张巨大的虚函数表。它用的是一张静态表加上一套宏。你写的这段// MainFrm.h class CMainFrame : public CFrameWnd { DECLARE_DYNCREATE(CMainFrame) public: CMainFrame(); protected: afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); // afx_msg 是空宏纯标记 afx_msg void OnFontDropdown(); afx_msg void OnUpdateFontDropdown(CCmdUI* pCmdUI); DECLARE_MESSAGE_MAP() }; // MainFrm.cpp BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_WM_CREATE() // 绑定 WM_CREATE ON_COMMAND(ID_FONT_DROPDOWN, OnFontDropdown) // 绑定 WM_COMMAND 该 ID ON_UPDATE_COMMAND_UI(ID_FONT_DROPDOWN, OnUpdateFontDropdown) END_MESSAGE_MAP()宏展开后大致等价于一个静态的AFX_MSGMAP_ENTRY数组每条记录包含消息编号、通知码、命令 ID 和对应的成员函数指针再加一个虚函数GetMessageMap()返回指向这张表的指针。运行时收到消息MFC 从当前类开始沿继承链往上查表匹配到就调用处理函数一路查到CCmdTarget都没命中才交给DefWindowProc。ON_WM_CREATE()展开成{ WM_CREATE, 0, 0, ... }只管消息编号。ON_COMMAND展开成{ WM_COMMAND, CN_COMMAND, ID_FONT_DROPDOWN, ... }多了一个 ID 维度。这就是为什么同一个WM_COMMAND能分发到不同函数 —— 表里按 ID 做了二次区分。ON_UPDATE_COMMAND_UI是 MFC 一个很实用的设计它在菜单弹出前被调用你在这里根据当前状态决定菜单项是否打勾、是否变灰。这种状态驱动界面的写法比手动EnableMenuItem干净得多也避免了状态同步遗漏。3.4 自定义消息与命令消息的完整落地标准消息覆盖不到业务需求时就得自定义。完整链路是这样// 1) 定义放在双方都能 include 的头文件里不要写在某个 cpp 内部 #define WM_MY_PROGRESS (WM_USER 100) // 2) 发送方异步投递立刻返回适合从工作线程更新界面 ::PostMessage(m_hWnd, WM_MY_PROGRESS, (WPARAM)nPercent, (LPARAM)0); // 3) 接收方声明MainFrm.h afx_msg LRESULT OnMyProgress(WPARAM wParam, LPARAM lParam); // 4) 接收方登记MainFrm.cpp必须写在 BEGIN/END 之间 ON_MESSAGE(WM_MY_PROGRESS, OnMyProgress) // 5) 实现 LRESULT CMainFrame::OnMyProgress(WPARAM wParam, LPARAM lParam) { int nPercent (int)wParam; CString s; s.Format(_T(progress: %d%%), nPercent); m_wndStatusBar.SetPaneText(1, s); // 状态栏第二格显示进度 return 0; // 自定义消息约定返回 0 }PostMessage把消息塞进目标线程的队列就返回调用方和被调用方解耦跨线程更新界面必须走这条路。SendMessage是直接调用窗口过程同步阻塞在同一个线程里用没问题跨线程用就有死锁风险 —— 如果目标线程正在等你手里的某个锁两边就僵住了。这里是踩坑频率最高的一处PostMessage的两个参数是按值传递的 32 位整数如果你把栈上字符串的地址塞进lParam投递出去等对方处理时那个栈帧早就没了读到的全是垃圾。我在这上面翻过一次车排查了半天才定位到。要传字符串就用SendMessage同步等结果或者干脆在堆上分配一块内存由接收方负责释放。命令消息和自定义消息的处理路径也不一样。ON_COMMAND走的是命令路由从当前获得焦点的窗口开始依次经过视图、文档、主框架窗口、应用对象ON_MESSAGE是直接投给指定窗口不做路由。这就解释了为什么有时候把菜单处理函数写在 View 类里也能收到 —— 命令路由替你把消息送上去了。4. GDI 与 DC把字体、画笔、刷子在 OnDraw 里画对4.1 DC 与 OnDraw一份代码同时应付屏幕和打印机DC设备环境是 GDI 里最抽象也最关键的概念。它是一层设备无关的绘图环境屏幕、打印机、内存位图在它眼里只有能力差异没有本质区别。你的绘图代码只要面向 DC 写切换到打印机上就不用改。OnDraw是这套设计的落点。它在窗口需要重绘时被框架调用参数CDC* pDC就是当前有效的设备环境。关键在于打印和打印预览时传给OnDraw的是打印机的 DC 或者预览专用的 DC所以同一份绘图代码天然支持三种输出。这也是我在第 2 章建议保留打印预览选项的原因它是验证设备无关性最省事的实验台。在OnDraw之外画图要用CClientDC或CPaintDC。前者适合响应鼠标事件时的即时反馈后者只能在WM_PAINT处理函数里用因为它负责在析构时调用EndPaint完成绘制周期的收尾。有个现象很多人遇到过直接在CClientDC上画的东西窗口最小化再恢复就没了。原因不是代码写错而是系统不保证客户区内容被保留WM_PAINT一到就得全部重画。所以持久的图形一律放OnDraw临时的高亮框之类才用CClientDC配合Invalidate(FALSE)擦除。4.2 字体对象 CFont磅值换算和字符高度是最容易搞错的两件事字体控制着所有文字输出的外观CFont是它的 C 封装。创建方式有两种库存字体用SelectStockObject一行搞定自定义字体要填一个LOGFONT结构再调CreateFontIndirect。后者参数多但有三个是真正决定效果的。void CMyView::OnDraw(CDC* pDC) { CFont font; LOGFONT lf; memset(lf, 0, sizeof(lf)); // 必须清零否则未初始化字段是随机值 // 12 磅字号换算成逻辑单位磅值 * 每英寸像素数 / 72 lf.lfHeight -MulDiv(12, GetDeviceCaps(pDC-m_hDC, LOGPIXELSY), 72); lf.lfWeight FW_BOLD; // 700粗体 lf.lfItalic FALSE; lf.lfCharSet DEFAULT_CHARSET; // 按当前区域自动挑字符集 lf.lfQuality PROOF_QUALITY; // 优先保证字形质量而非尺寸精确 _tcscpy(lf.lfFaceName, _T(SimSun)); if (font.CreateFontIndirect(lf)) { CFont* pOldFont pDC-SelectObject(font); // 选入 DC保存原对象 pDC-TextOut(20, 20, _T(bold 12pt sample)); pDC-SelectObject(pOldFont); // 必须换回来 } // font 在这里析构自动销毁 GDI 对象 }lfHeight的符号非常讲究取负值表示字符本身的高度不含内部行距取正值表示整个字符单元格的高度含行距。想要12 磅字看起来就是 12 磅就取负值。MulDiv加上LOGPIXELSY做磅到逻辑单位的换算这套写法在任何 DPI 下都能保持一致比硬编码像素值靠谱。lfCharSet设成DEFAULT_CHARSET让系统按当前区域选择比强制ANSI_CHARSET灵活如果明确只输出中文设成GB2312_CHARSET也能工作但换到别的语言环境就会出乱码。SelectObject和恢复这两行必须配对而且font对象的生命周期必须覆盖到恢复之后。如果font在还挂在 DC 上时就被析构后续绘制行为是未定义的可能正常可能满屏花屏属于典型的玄学问题。文字输出有两个函数TextOut(x, y, str)只能单行起点坐标是文字左上角DrawText(str, lpRect, nFormat)在指定矩形内排版支持多行、对齐、省略号。DT_SINGLELINE | DT_VCENTER | DT_CENTER这个组合能让文字在矩形内完全居中是按钮风格标签的常用写法DT_END_ELLIPSIS在空间不足时自动加省略号做长文件名列表时几乎必用。多行输出还有个坑手工计算行距时要用GetTextExtent拿到实际字符串的宽高或者用GetTextMetrics填充TEXTMETRIC结构拿到精确的字体度量直接拿字号当行距会重叠。4.3 画笔与刷子选入、恢复、销毁的三拍节奏画笔管线条刷子管填充两者都是 GDI 对象遵守同一套规则创建、选入 DC、用完恢复、销毁。原文给的模板代码里那个创建 → 选入 → 再创建前先销毁的序列是这套规则的准绳。画笔的风格参数里有个硬限制虚线、点线、点划线这几种风格宽度只能是 1宽度大于 1 时这些风格会被系统悄悄替换成实线。我见过有人调了半天为什么虚线画出来是实线最后发现是宽度写了 2。void CMyView::OnDraw(CDC* pDC) { CPen penGray; CBrush brushCross; penGray.CreatePen(PS_SOLID, 2, RGB(128, 128, 128)); brushCross.CreateHatchBrush(HS_CROSS, RGB(0, 0, 255)); // 交叉网纹填充 CPen* pOldPen pDC-SelectObject(penGray); CBrush* pOldBrush pDC-SelectObject(brushCross); pDC-Rectangle(20, 20, 220, 140); // 边框用当前笔内部用当前刷 pDC-SelectObject(pOldPen); // 先恢复再销毁 pDC-SelectObject(pOldBrush); penGray.DeleteObject(); // 显式销毁析构时也会做一次 brushCross.DeleteObject(); }顺序不能乱。SelectObject返回的是 DC 里原来的对象指针必须存下来在对象还挂在 DC 上时调用DeleteObject删除会失败对象泄漏换回来之后才安全。CPen和CBrush的析构函数里会自动调DeleteObject但依赖析构的时机不如显式调用可控尤其在有多个返回分支的函数里显式销毁能避免遗漏。只画边框不填充的场合不必创建空刷子pDC-SelectStockObject(NULL_BRUSH)一行就够。库存对象常驻系统选入之后不需要恢复也不需要销毁是随手一画场景下最省事的选择。库存画笔BLACK_PEN、WHITE_PEN、NULL_PEN库存刷子BLACK_BRUSH到WHITE_BRUSH那一套都在SelectStockObject的可选范围里。刷子的构造还有一层CreateHatchBrush生成的填充图案有六种HS_HORIZONTAL、HS_VERTICAL、HS_CROSS、HS_DIAGCROSS之类在需要区分区域的图表里比纯色更有辨识度。自定义位图刷子用CreatePatternBrush图案会按 8×8 平铺做背景纹理时很实用。5. 避坑与排查VC6.0 MFC 最容易翻车的五个位置5.1 GDI 对象泄漏与选入即失效现象程序开着不动任务管理器里该进程的 GDI 对象数一路往上涨界面重绘越来越慢最后出现绘制残缺甚至创建对象失败。原因CPen、CBrush、CFont创建后没有DeleteObject或者对象还在 DC 里挂着就被析构。更隐蔽的一种是每次OnDraw都new一个对象但不释放窗口一拖就漏几十个。解决所有SelectObject都存返回值并成对恢复对象创建后立刻想清楚谁负责销毁在 Debug 版本里定期观察 GDI 计数涨了就是漏了。写绘图密集的模块可以用第 6 章那个小包装类把选入—恢复自动化。5.2 消息响应函数死活不触发现象菜单点了没反应按钮按下去没动静断点打在处理函数上根本不进。原因通常有三种。一是三处没配齐头文件里的afx_msg声明、cpp 里的函数实现、BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间的ON_登记缺任何一处都静默失败。二是命令消息的路由没走到你写处理函数的那个类比如处理函数写在文档类但当前焦点窗口的上级链把它挡在了更前面。三是控件通知用了ON_COMMAND登记实际该用ON_NOTIFY或者ON_CONTROL因为控件通知码不是CN_COMMAND。解决优先用 ClassWizard 加消息手写容易漏登记确认三处齐全后把处理函数换成AfxMessageBox看是否进入排除函数体自身逻辑问题命令消息不响应时先查焦点窗口和路由链再查 ID 是否和资源文件里的定义一致。5.3 CString 与字符集编译通过运行时乱码现象TextOut输出中文变成问号或方块CString和char*互相转换时编译报错传值给某个函数后字符串内容变成乱码。原因VC6.0 默认走 MBCS工程里混用_T()和非_T的字符串字面量一半宽字符一半窄字符把CString隐式转换成LPCTSTR后临时的CString对象已经析构指针悬空跨模块传递CString对象本身两边的 CRT 版本不一致。解决全工程统一用_T()和TCHAR不要今天写abc明天写Labc需要裸指针时先拷贝到一个活的CString变量上再取LPCTSTR跨模块一律传LPCTSTR或者把内容拷进调用方提供缓冲区不传对象。5.4 类向导失效与工程配置现象CtrlW 打不开 ClassWizard或者能打开但 Add Member Variable 变灰报 Parsing error 之类的解析失败工具栏上的控件 ID 列表是空的。原因工程目录里的.clw文件损坏这是最常见的一条工程路径含中文或空格解析器读不下去手动改过//{{AFX_注释块之间的内容破坏了向导的锚点机器上装过多个版本的开发工具注册表里的类型库关联串了。解决关掉 IDE删掉.clw文件重新打开工程按 CtrlW 让它重建九成情况一次解决问题路径改成纯英文再新建工程//{{AFX_MSG_MAP内部的登记行尽量让向导维护要加自定义消息就加在注释块外面。5.5 刷新问题最小化恢复后画面没了频繁重绘时闪屏现象窗口最小化再还原之前画的内容全部消失拖动窗口或改变大小时整个客户区白闪。原因内容画在CClientDC上而不是OnDraw里系统不保证客户区内容保留WM_PAINT一来就擦干净重画。闪屏则是默认的背景擦除先填一遍背景色再让OnDraw画一遍两次绘制之间的空档被人眼捕捉到了。解决所有需要持久显示的图形挪进OnDraw擦除背景时返回TRUE表示已处理避免系统再刷一遍内容复杂时用内存 DC 做双缓冲全部画完之后一次BitBlt到屏幕。6. 把绘图代码管住一个 RAII 包装与一份自检清单前面反复强调选入必须恢复靠人记迟早会漏。C 有个现成的办法让对象的析构替你干活。这个小模板类在 VC6.0 下能正常编译几十行代码把最容易出错的那对操作封装掉了// 极小的 GDI 选择器构造时选入析构时自动还原 template class T class CGdiSelector { public: CGdiSelector(CDC* pDC, T* pObj) : m_pDC(pDC), m_pOld(NULL) { if (m_pDC ! NULL pObj ! NULL) m_pOld (T*)m_pDC-SelectObject(pObj); } ~CGdiSelector() { if (m_pOld ! NULL) m_pDC-SelectObject(m_pOld); // 出作用域自动换回异常路径也覆盖 } private: CGdiSelector(const CGdiSelector); // 禁止拷贝避免重复还原 CGdiSelector operator(const CGdiSelector); CDC* m_pDC; T* m_pOld; };用法变成了三行不用再写恢复语句CPen pen; pen.CreatePen(PS_SOLID, 1, RGB(200, 0, 0)); { CGdiSelectorCPen sel(pDC, pen); // 进入作用域选入 p a hrefhttps://download.csdn.net/download/liu955x/1879868 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p