ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VC++6.0 MFC老工程维护指南:消息映射、GDI对象与绘图避坑

VC++6.0 MFC老工程维护指南:消息映射、GDI对象与绘图避坑 简介这份资源面向具备一定C/C基础、希望系统入门VC6.0与MFC框架的开发者重点解决从语言基础到Windows消息驱动编程的过渡问题。内容围绕消息机制、窗口句柄、消息映射等核心概念展开并给出学习路径与参考书选择建议适合初学者循序渐进地理解MFC开发思路。资源包内共1个doc文档约521KB以文字讲解为主便于随时查阅与整理笔记。目前已有727人学习下载说明其在入门阶段具有一定参考价值。读者可从中获得C基础巩固方向、Windows消息组成与投递流程的说明、消息映射的实现思路以及如何借助Help Online和阅读他人代码提升编程能力的具体建议为后续MFC项目实践打下基础。1. 为什么今天还有人翻出 VC6.0 的 MFC 笔记如果你手头正维护一套十几年前的工控上位机、医疗设备界面或者某台老机床的配套软件大概率绕不开 VC6.0 和 MFC 这套组合。它不像现在用 Qt 或 WPF 那样有漂亮的文档和社区很多逻辑只能靠翻旧书、读别人留下的代码来啃。我手上这份《VC6.0 MFC 入门学习》资料就是一份典型的“老笔记式”教程从 C/C 基础讲到消息机制再落到 GUI 对象、字体、画笔、刷子这些具体绘图操作最后还给了常用类、宏、函数的速查。它解决的不是“怎么从零搭一个现代桌面应用”而是“怎么读懂并改动手头这套 MFC 老工程”。适合两类人一是被派去维护遗留系统的初中级工程师二是想搞明白 Windows 消息驱动本质、不想只停留在拖控件层面的开发者。下面我按自己拆这套资料的实际顺序把消息机制、消息映射、GUI 对象管理、绘图输出这几块讲透顺带把几个容易翻车的地方标出来。2. 消息机制与消息映射从 WNDPROC 到 ON_COMMAND 的完整链路2.1 消息的组成与投递路径Windows 是消息驱动的操作系统这句话背下来没用得知道一条消息从产生到被处理到底走了哪几步。资料里把消息拆成三部分消息名称UINT、WPARAM、LPARAM。比如菜单被点击时产生WM_COMMANDWPARAM的高字HIWORD(wParam)存放命令 ID对菜单来说就是菜单项 ID。这个细节在排查“菜单点了没反应”时特别关键——你得先确认命令 ID 有没有对上。消息必须由某个窗口接收系统通过窗口句柄HWND在整个系统中唯一标识一个窗口。发送消息时指定句柄消息就投递到对应窗口的窗口过程WNDPROC。如果窗口过程不处理某条消息系统提供的默认窗口过程会接手。这就是为什么拖动窗口、改变大小这些操作你一行代码不写也能正常工作——默认过程替你兜底了。消息队列和消息循环是另一层。32 位系统里每个运行的程序都有自己的消息队列系统在多个队列间切换不必等当前程序处理完才发下一条这叫抢先式多任务。资料里给的伪代码很直白while(1) { id getMessage(...); // 没有消息时阻塞不占 CPU if (id quit) break; translateMessage(...); // 翻译键盘消息 dispatchMessage(...); // 分发到目标窗口过程 }getMessage在没有消息时不会返回所以循环不会空转烧 CPU。这一点在写后台服务或长时间运行的上位机时要注意如果你在消息循环里塞了耗时操作界面就会卡死因为消息队列堵住了。2.2 消息映射的宏展开逻辑MFC 把窗口过程包装成了消息映射。资料里给了一段BEGIN_MESSAGE_MAP/END_MESSAGE_MAP的代码并展示了编译后大致被替换成什么样BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_WM_CREATE() ON_COMMAND(ID_FONT_DROPDOWN, DoNothing) END_MESSAGE_MAP()编译后等价于一个newWndProc内部用switch把WM_CREATE分派给OnCreate把WM_COMMAND且命令 ID 为ID_FONT_DROPDOWN的分派给DoNothing。只要是该类的实例生成的窗口都走这个窗口过程。理解这层展开的意义在于当你在 ClassWizard 里添加一个消息处理函数却没被调用时可以顺着消息映射表检查三件事——宏有没有写对、函数签名有没有匹配、命令 ID 有没有对上。我见过最常见的翻车是手动改了资源里的 ID 但没同步改ON_COMMAND的第二个参数结果消息发过来匹配不上直接落到默认处理里界面毫无反应。2.3 自定义消息与参数传递资料提到用户可以定义自己的消息名称也可以利用自定义消息来发送通知和传送数据。常见做法是#define WM_MY_UPDATE (WM_USER 100) // 发送方 PostMessage(hWndTarget, WM_MY_UPDATE, (WPARAM)nProgress, (LPARAM)pData); // 接收方消息映射 ON_MESSAGE(WM_MY_UPDATE, OnMyUpdate) // 处理函数 LRESULT CMyDlg::OnMyUpdate(WPARAM wParam, LPARAM lParam) { int nProgress (int)wParam; MyStruct* pData (MyStruct*)lParam; // 更新界面 return 0; }这里WM_USER以上的值留给应用自定义避免和系统消息冲突。参数说明wParam和lParam都是 32 位传指针时要注意生命周期——如果用PostMessage异步指针指向的内存必须在接收方处理完之前保持有效否则就是野指针。我一般会改成传值或者用SendMessage同步来规避这个坑。3. GUI 对象管理CFont、CBrush、CPen 的创建、选入与销毁3.1 为什么 GUI 对象不能随便 new 一个就用Windows 里的 GUI 对象字体、刷子、画笔、位图和 C 对象是两码事。C 对象只是栈上或堆上的一个壳真正的系统资源在创建函数调用后才产生。资料里反复强调一条规则创建合法对象 → 选入 DC → 保存原对象 → 使用 → 恢复原对象 → 销毁。漏掉任何一步都可能出问题。最典型的错误是选入一个临时对象用完没恢复临时对象析构后 DC 里还挂着一个已经失效的句柄下次绘图直接异常。资料里给的OnDraw示例就是标准写法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* pPenOld (CPen*)pDC-SelectObject(pen1); // 用 pen1 绘图... pDC-SelectObject(pen2); // 用 pen2 绘图... pen1.DeleteObject(); // 重新创建前先销毁 pen1.CreatePen(PS_SOLID, 2, RGB(0,0,0)); pDC-SelectObject(pen1); // 再用 pen1 绘图... pDC-SelectObject(pPenOld); // 恢复原来的对象 }逻辑说明SelectObject返回的是 DC 中原来的对象指针必须保存。DeleteObject在重新创建同一个 C 对象前调用释放旧资源。最后恢复原对象保证 DC 状态干净。参数方面CreatePen的nWidth对虚线、点线等风格有约束——资料里明确写了PS_DASH、PS_DOT、PS_DASHDOT这些风格宽度必须为 1否则创建出来的笔行为不确定。这个细节很多人不知道画虚线时设了宽度 2结果出来是实线查半天查不出原因。3.2 库存对象作为恢复手段系统提供了一批库存 GUI 对象比如BLACK_BRUSH、WHITE_PEN、SYSTEM_FONT等。资料指出这些对象留在 DC 中是安全的所以可以用SelectStockObject选入库存对象来作为恢复手段。常见做法是pDC-SelectStockObject(NULL_BRUSH); // 恢复为空刷子 pDC-SelectStockObject(BLACK_PEN); // 恢复为黑色笔这比保存原对象指针更省事但要注意库存对象不能DeleteObject它们由系统管理。我一般会在对话框的OnPaint里用库存对象做默认状态自定义绘图前再选入自己的对象画完切回库存对象。3.3 字体创建的参数陷阱资料里详细列了CFont::CreateFont的十几个参数其中几个容易踩坑参数含义常见坑nHeight字体高度逻辑单位传 0 表示默认高度传正值和负值匹配逻辑不同nWeight字体粗细用 FW_NORMAL(400) 还是 FW_BOLD(700) 要查表不能凭感觉填nCharSet字符集中文环境要用GB2312_CHARSET或DEFAULT_CHARSET用ANSI_CHARSET可能显示乱码lpszFacename字体名称写“宋体”还是“SimSun”取决于系统最好用EnumFontFamiliesEx枚举我见过最典型的翻车是字符集设错英文数字正常但中文全是问号。解决方法是把nCharSet设为DEFAULT_CHARSET让系统根据当前区域设置自动匹配。另外CreateFontIndirect配合LOGFONT结构更灵活CFontDialog可以直接拿到用户选择的LOGFONT数据省去手动填参数。4. 绘图输出与坐标计算TextOut、DrawText 与 GetTextExtent 的配合4.1 OnDraw 的三重身份资料里特别强调OnDraw不只是窗口重绘时被调用。打印时传入的是打印机 DC打印预览时传入的是CPreviewDC所以一份OnDraw代码同时完成窗口显示、打印预览、实际打印三重功能。这是 MFC 设备无关性的直接体现。如果你在OnDraw里写了只适合屏幕的代码比如假设分辨率固定打印出来就会错位。4.2 TextOut 与 DrawText 的选择TextOut只能输出单行文字DrawText可以在指定矩形内输出单行或多行并且支持对齐和风格标记。资料列了一长串nFormat标记常用的几个CRect rect(10, 10, 200, 100); CString str _T(这是一段需要自动换行的文字); pDC-DrawText(str, rect, DT_LEFT | DT_WORDBREAK | DT_NOPREFIX);DT_WORDBREAK让文字在单词间自动折行DT_NOPREFIX禁止把当作快捷键前缀处理。如果不加DT_NOPREFIX字符串里的会消失并给后面的字符加下划线显示效果和预期不符。DT_CALCRECT可以在不实际绘制的情况下计算文字所需矩形尺寸常用于布局预计算。4.3 用 GetTextExtent 手工计算行距当需要手工输出多行文字时GetTextExtent返回字符串的宽度和高度CSize size pDC-GetTextExtent(_T(测试文字)); int nLineHeight size.cy 4; // 加 4 像素行间距 for (int i 0; i lines.GetSize(); i) { pDC-TextOut(10, 10 i * nLineHeight, lines[i]); }更精确的字体度量用GetTextMetrics填充TEXTMETRIC结构里面的tmHeight、tmAscent、tmDescent可以算出基线位置。我一般会在输出表格类数据时用GetTextExtent算列宽避免文字重叠。4.4 颜色与背景模式SetTextColor设文字颜色SetBkColor设背景色SetBkMode(TRANSPARENT)让背景透明。默认是OPAQUE文字后面会填充背景色。在绘制叠加文字或图标标注时透明背景几乎是必须的pDC-SetBkMode(TRANSPARENT); pDC-SetTextColor(RGB(255, 0, 0)); pDC-TextOut(50, 50, _T(警告));注意SetBkMode和SetTextColor都是 DC 状态用完最好恢复否则影响后续绘制。5. 避坑与排查MFC 老工程里最常见的五类翻车5.1 菜单/按钮点了没反应现象界面正常点击菜单项或按钮没有任何响应。原因命令 ID 不匹配或者消息映射宏没写对。解决检查资源编辑器里的 ID 和ON_COMMAND第二个参数是否一致检查处理函数是否在BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间检查函数签名是否为afx_msg void OnXxx()。5.2 绘图闪烁严重现象窗口重绘时画面闪烁。原因OnEraseBkgnd和OnPaint双重绘制或者没有使用双缓冲。解决在OnEraseBkgnd里直接返回 TRUE 阻止背景擦除然后在内存 DC 里绘制完再一次性BitBlt到屏幕 DC。5.3 字体中文显示乱码现象英文数字正常中文显示为问号或方块。原因CreateFont的nCharSet参数设成了ANSI_CHARSET。解决改为DEFAULT_CHARSET或GB2312_CHARSET并确认lpszFacename是系统已安装的中文字体。5.4 程序运行一段时间后 GDI 对象泄漏现象任务管理器里 GDI 对象数持续增长最终界面无法绘制。原因CreatePen、CreateFont等创建的对象没有DeleteObject或者选入 DC 后没有恢复。解决每个CreateXxx对应一个DeleteObject选入 DC 的对象在用完后必须恢复原对象。用SelectObject保存返回值是标准做法。5.5 消息循环里做耗时操作导致界面卡死现象点击某个按钮后界面冻结进度条不更新。原因在消息处理函数里执行了耗时计算或文件读写阻塞了消息循环。解决把耗时操作放到工作线程通过PostMessage发送自定义消息更新界面。注意工作线程不能直接操作界面控件必须通过消息通知主线程。6. 从这份笔记里榨出最后一点价值我的查阅习惯这份资料最有用的部分其实是 1.5 节和 2.x 节里的速查表——CRect、CPoint、CString、CStringArray的成员函数以及RGB、TRACE、ASSERT、VERIFY这些宏的用途。我现在的习惯是接手一个 MFC 老工程先不急着改代码而是把这份速查表过一遍把工程里用到的类和方法在表里找到对应确认自己理解的和资料里写的一致。比如CString::Format相当于sprintf但CString会自动管理内存不需要预先分配缓冲区CStringArray的Add、RemoveAt、GetAt、SetAt、InsertAt覆盖了大部分数组操作不用自己写链表。另一个习惯是看别人的代码时对照消息映射表。拿到一个CMyDialog类先翻到.cpp文件末尾找BEGIN_MESSAGE_MAP把里面所有ON_开头的宏列出来就知道这个对话框响应哪些消息、每个消息对应哪个函数。这比从头读代码快得多也是资料里“学会看别人的代码”那条建议的具体落地方式。最后说一个我踩过的坑VC6.0 的Help Online在 Win10/Win11 上经常装不上或打不开因为它是 16 位帮助系统。替代方案是装一个 MSDN 离线包或者直接查微软在线文档里对应 MFC 类的页面。但要注意在线文档的版本可能对应 VS2008 之后的 MFC部分 API 在 VC6.0 里不存在。我一般会先用AfxMessageBox把关键变量打出来确认行为再去查文档避免被版本差异带偏。从那以后我每次改老工程的消息映射都强制走一遍“资源 ID → 映射宏 → 函数签名 → 断点验证”这四步再也没出现过点了没反应的情况。希望帮到你。本文还有配套的精品资源点击获取
返回列表