简介:面向具备一定C/C++基础、期望上手VC++ 6.0与MFC的初学者,这份入门教程文档从宏观学习方法讲到具体的Windows消息机制。文中先介绍学好VC的九条建议:先编写字符界面程序打牢语言基本功,多使用Help Online少依赖参考书,学会阅读他人代码,并养成循序渐进的学习习惯。随后重点剖析Windows的消息驱动体系,涵盖消息的组成、窗口句柄的作用、窗口过程对消息的分流、默认窗口过程的兜底处理,以及消息队列与消息循环在多任务环境下的调度逻辑,配合伪代码示例便于理解。在MFC部分,文档结合C++的封装、继承与多态性说明其开发优势,并通过BEGIN_MESSAGE_MAP等宏示范消息映射的注册与分派机制,让读者清楚事件响应是如何被框架自动组织的。阅读后可以快速建立Windows窗口程序开发的整体框架,并掌握常见消息处理的排查思路。资源共1个doc文件,压缩包仅521KB,内容紧凑、原理阐述清晰。已有727人学习浏览,适合作为系统化学习MFC的入门读物。
1. VC++6.0 MFC:不是古董,是Windows桌面快速交付的一条老路
很多人一看到VC++6.0就皱眉,觉得那是上个世纪的古董,但实际上,今天仍有大量存量系统、工业控制软件和教学环境跑在这条技术栈上。MFC,也就是微软基础类库,把Win32 API的窗口、消息、控件封装成C++类,让开发者用面向对象的方式写Windows桌面程序。VC++6.0 MFC入门学习这件事,本质上是两件事:一是搞懂MFC的消息机制和类层次,二是用VC++6.0这个老而稳的IDE把对话框、控件、文件读写这些高频需求快速落地。它特别适合那些要维护老项目、接手工控上位机、或者在学校里搞课程设计的人。如果你已经有C++基础,学MFC能让你在一周内做出一个带界面的可用工具,这个速度在今天的Web技术栈里很难复现。
2. MFC四大类与消息映射:程序骨架决定上手速度
2.1 MFC四大类:CWinApp、CFrameWnd、CDocument、CView的分工
MFC程序的基本骨架由四个核心类支撑,网上常说的“MFC四大类”就是指CWinApp、CFrameWnd、CDocument、CView(对话框程序里CDocument和CView会被CDialog替代)。这四者各管一段:CWinApp代表应用程序本身,负责初始化、启动消息循环、处理命令行参数;CFrameWnd是主窗口框架,负责窗口的创建、大小、菜单和工具栏;CDocument管理数据,比如一个文本编辑器里的字符串内容;CView负责把数据渲染到窗口上,并接收用户交互。文档与视图分离,数据变了视图刷新,视图操作了数据再由文档保存,这是一套经典的MVC雏形。
在VC++6.0里,打开一个用AppWizard生成的单文档工程,你会发现源代码里有一个全局对象,比如:
class CMyApp : public CWinApp { public: virtual BOOL InitInstance(); }; CMyApp theApp; // 全局对象,构造在main之前完成程序真正执行的入口是WinMain,但MFC已经在库里把它实现了,它会先去寻找这个全局的theApp对象,调用它的InitInstance。你重写InitInstance,在里面创建主窗口,MFC接着调用Run进入消息循环。这个启动顺序决定了你后续在哪里做初始化、在哪里做清理,如果在InitInstance里忘了调用基类或返回FALSE,窗口就会一闪而过。所以入门第一步不是急着贴控件,而是看懂这一个对象的生命周期。
2.2 消息映射:从BeginMessageMap到END_MESSAGE_MAP的查找顺序
MFC的一大特点是“消息映射表”,它用宏把Windows消息和C++成员函数绑定起来。比如你给一个按钮添加点击响应,ClassWizard会在头文件里声明一个afx_msg函数,在实现文件里用ON_BN_CLICKED宏把消息ID和函数关联起来:
BEGIN_MESSAGE_MAP(CMyDlg, CDialog) ON_BN_CLICKED(IDC_BTN_TEST, &CMyDlg::OnBnClickedTest) END_MESSAGE_MAP()这里的BEGIN_MESSAGE_MAP声明了这张表的开始,ON_BN_CLICKED是消息条目,END_MESSAGE_MAP结束表。MFC在收到WM_COMMAND消息时,会沿着消息映射表逐条查找,先查当前类,查不到就查基类,直到找到对应处理函数。查找顺序是从派生类到基类,所以你在子类里重写处理函数时,基类的同名函数不会被调用,除非你显式调用它。这是MFC新手最容易迷糊的地方:以为像虚函数一样会自动向上传递,实际上消息映射表更像一张静态的查找表,需要你自己决定是否调用基类版本。
消息映射的查找顺序还涉及一个实际调试问题:当你觉得某个控件点击没反应时,先看ID是否写对了。ClassWizard生成的消息映射条目用的是资源头文件里的ID,一旦你在资源编辑器里改了控件ID,老的处理函数还在映射表里,但控件找不到新ID,函数就不会被调用。这类问题比写错代码逻辑更隐蔽,因为编译器不会报错。
2.3 文档视图与对话框二选一:什么项目选哪条路
MFC工程向导会让你在“单文档/多文档/对话框”里选一条路,这个选择直接影响后续开发效率。文档视图程序适合做编辑器、浏览器、看图器这类“一个主窗口容纳可变内容”的应用,它天然支持文档读写和视图切分。而对话框程序适合做计算器、配置面板、串口调试助手、上位机控制界面这类“控件布局密集、交互固定”的工具,对话框本身就是窗口,不需要额外的文档视图框架。
哪种更值得入门学?我的建议是对话框程序优先。VC++6.0的AppWizard里,对话框工程生成代码最少,只有一个CAboutDlg一个主对话框类,你可以把注意力集中在消息映射和控件操作上,而不是被SDI/MDI的框架文件绕晕。等你把对话框程序里的事件处理、控件数据交换搞熟了,再回头学文档视图,骨架更容易理解。文档视图解决的是“数据与界面分离”的问题,对话框解决的是“快速拼出一个能用的界面”的问题,入门阶段后者上手更快,成就感更强。
| 对比项 | 对话框程序 | 单文档程序 |
|---|---|---|
| 生成代码量 | 少,约3个类 | 多,约5个类以上 |
| 适合场景 | 工具型界面、控制面板 | 文档编辑、数据展示 |
| 数据管理 | 成员变量手动维护 | CDocument标准封装 |
| 学习成本 | 低 | 高 |
3. 用向导搭建对话框工程:从AppWizard到“打开按钮不闪退”
3.1 AppWizard的选项怎么勾:从New到Dialog based的完整路径
在VC++6.0里,点File -> New -> Projects,选“MFC AppWizard(exe)”,填好工程名后一路Next,到第三步会让你选程序类型。这里有两个关键选项:一个是“Dialog based”,另一个是“Use MFC in a static library”还是“Use MFC in a shared DLL”。日常入门我一般选Dialog based加Shared DLL,理由是生成的可执行文件小,并且VC6的安装包里自带MFC DLL,开发机上不会缺运行库。如果是要发给没有VC6环境的机器运行,再改成静态库,但静态库的坑我在第5章会专门讲。
向导里还有几个附加选项,比如“ActiveX Controls”和“Windows Sockets”支持。如果只是做本地工具,这两个可以不勾,勾了会多生成一堆无关代码,初学者看着云里雾里。最底部还有一个“生成的注释”选项,保持勾选,它会在代码里生成中文或英文注释,对照着学非常有用。向导走完,VC6会为你生成一个最小可运行的对话框程序,按F7编译、F5运行,你会看到一个空的对话框窗口,上面带一个“确定”按钮。
3.2 生成的工程文件清单:每个文件是干什么的
对话框工程生成后,工作区里会有几个文件,新手最好先认一遍。以你命名成MyDlg的工程为例,主要文件有:
| 文件 | 作用 |
|---|---|
| MyDlg.dsw / MyDlg.dsp | 工程工作区和项目文件,VC6用它管理编译配置 |
| MyDlg.h / MyDlg.cpp | CMyDlgApp类,程序入口,InitInstance所在 |
| MyDlgDlg.h / MyDlgDlg.cpp | 主对话框类,你的大部分代码写在这里 |
| Resource.h | 资源ID定义,所有控件ID都在这里定 |
| MyDlg.rc | 资源描述文件,对话框布局、字符串、图标都在里面 |
Resource.h和MyDlg.rc是一对,你在资源编辑器里拖一个按钮,VC6自动在rc里加一条控件定义,同时在Resource.h里分配一个ID。如果你手动改ID导致两者不一致,轻则编译报错,重则运行时不响应。新手最容易犯的错是:在资源编辑器里删掉一个控件后,代码里还引用它的ID,编译能过但运行时对话框创建失败。
3.3 把按钮接上点击响应:ClassWizard的三步操作
最经典的“Hello MFC”是给按钮加一个点击弹窗。在对话框上放一个Button,双击它或者按Ctrl+W打开ClassWizard,切到Member Variables页签可以给控件绑定变量,切到Message Maps页签可以添加消息处理函数。选好控件ID,再双击BN_CLICKED右侧的消息,VC6会自动在类里生成处理函数声明和空实现。
生成后的代码分三处,头文件里多了一个声明:
// MyDlgDlg.h public: afx_msg void OnBnClickedBtnOpen();实现文件里多了一个消息映射条目和函数体:
// MyDlgDlg.cpp BEGIN_MESSAGE_MAP(CMyDlgDlg, CDialog) ON_BN_CLICKED(IDC_BTN_OPEN, &CMyDlgDlg::OnBnClickedBtnOpen) END_MESSAGE_MAP() void CMyDlgDlg::OnBnClickedBtnOpen() { AfxMessageBox(_T("按钮被点击了")); }这三个部分缺一不可:头文件的声明告诉编译器这个类有这样一个成员函数;实现文件的消息映射条目把按钮ID和函数绑定;函数体才是业务逻辑。如果你自己手写而不是靠ClassWizard生成,很容易漏掉头文件里的afx_msg声明,编译报错还算好的,更隐蔽的是ID写错,函数不被调用。在VC6环境里,能用ClassWizard就尽量用,它作的修改是规范且一致更新的,血泪经验告诉我,手写消息映射是新手“翻车”的最高频原因。
4. 五个直接可抄的控件场景:按钮颜色、BMP显示、CPU ID与列表操作
4.1 CButton按钮颜色设置:OnCtlColor返回画刷的完整步骤
给MFC对话框里的按钮换个背景色,原理是让按钮所在的对话框处理WM_CTLCOLORBTN消息。在ClassWizard里选择主对话框类,找到WM_CTLCOLORBTN消息,添加处理函数OnCtlColor。在这个函数里判断当前控件ID,返回对应画刷:
HBRUSH CMyDlgDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor); if (nCtlColor == CTLCOLOR_BTN && pWnd->GetDlgCtrlID() == IDC_BTN_OPEN) { pDC->SetTextColor(RGB(255, 0, 0)); // 按钮文字红色 pDC->SetBkColor(RGB(255, 255, 0)); // 按钮背景黄色 return m_brYellow; // 返回黄色画刷 } return hbr; }函数里的参数pDC是设备上下文,pWnd是发出该消息的控件指针,nCtlColor告诉你是哪种控件。SetTextColor设置文字颜色,SetBkColor设置背景色,最后返回的画刷必须是一个有效的HBRUSH,所以你要在对话框初始化时创建它:
BOOL CMyDlgDlg::OnInitDialog() { CDialog::OnInitDialog(); m_brYellow.CreateSolidBrush(RGB(255, 255, 0)); // 创建黄色画刷 return TRUE; }一个常见的坑是:如果你在资源编辑器里给按钮勾选了“Owner Draw”属性,WM_CTLCOLORBTN消息不会触发,按钮会走完全不同的自绘流程。颜色不生效时先检查这个属性,把Owner Draw去掉,用OnCtlColor才是正路。
4.2 在MFC对话框里显示BMP图片:LoadImage与CStatic绑定
显示一张外部BMP图片,最简单的做法是把一个Picture Control控件放到对话框上,用LoadImage把BMP文件加载成HBITMAP,再SetBitmap给控件:
void CMyDlgDlg::OnBnClickedShowBmp() { HBITMAP hBmp = (HBITMAP)LoadImage( AfxGetInstanceHandle(), _T("C:\\test.bmp"), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); if (hBmp != NULL) { m_picture.SetBitmap(hBmp); // m_picture是Picture Control的控件变量 } else { AfxMessageBox(_T("图片加载失败")); } }LoadImage的第三个参数是图片类型,固定IMAGE_BITMAP;第四第五个参数是宽高,填0表示按图片原始尺寸加载;第六个参数LR_LOADFROMFILE表示从文件加载,不带这个标志会尝试从资源里找,结果就是加载失败。注意SetBitmap之后,旧的位图句柄如果不打算用了应该DeleteObject释放,否则反复加载几张图后GDI资源会耗尽,程序越跑越卡直到界面白屏。
4.3 获取CPU ID:内联汇编与GetSystemInfo的取舍
获取CPU信息在VC6时代有个老问题:__cpuid这个内部函数在VC6的intrin.h里支持不完整,很多老工程直接用内联汇编。要读取CPU厂商字符串,常规做法是执行cpuid指令一次,从EBX、EDX、ECX三个寄存器拼出12个字符:
char szCpuVendor[16] = {0}; _asm { mov eax, 0 cpuid mov dword ptr [szCpuVendor], ebx mov dword ptr [szCpuVendor + 4], edx mov dword ptr [szCpuVendor + 8], ecx } CString strVendor = szCpuVendor; AfxMessageBox(strVendor);cpuid指令的Eax=0表示请求返回厂商字符串,执行后EBX、EDX、ECX三个寄存器分别存放字符串的前中后四个字节,合起来就是“GenuineIntel”或“AuthenticAMD”。如果你是拿CPU信息做软件授权绑定,建议用内联汇编的版本,保持代码风格跟老工程一致,维护成本低。如果只是拿一个整型编号,也可以用GetSystemInfo,但它只提供OEM ID和处理器架构,不提供CPU序列号,两者用途完全不同。
4.4 获取列表总列数与文件是否存在:两个高频小函数
CListCtrl在Report风格下要获取总列数,有一个容易被埋没的函数链:
int CMyDlgDlg::GetListColumnCount() { CHeaderCtrl* pHeader = m_listCtrl.GetHeaderCtrl(); if (pHeader == NULL) return 0; return pHeader->GetItemCount(); }GetHeaderCtrl返回的就是列表控件顶部的那个表头控件,它内部维护着所有列的信息,GetItemCount直接返回列数。如果列表没有设置LVS_REPORT风格,GetHeaderCtrl返回NULL,所以函数开头要判空。还有另外一种写法是循环GetColumn,每成功一次列数加一,但性能远不如直接拿表头控件数。
判断文件是否存在,用GetFileAttributes比用CFile::Open更干脆:
DWORD dwAttr = GetFileAttributes(_T("C:\\config.ini")); if (dwAttr == INVALID_FILE_ATTRIBUTES) { AfxMessageBox(_T("文件不存在")); } else { AfxMessageBox(_T("文件存在")); }GetFileAttributes除了判断文件是否存在,返回值里还带文件属性标志,比如FILE_ATTRIBUTE_DIRECTORY表示这是目录,FILE_ATTRIBUTE_READONLY表示只读。如果你要区分“文件不存在”和“路径是目录”,比较dwAttr的值即可。
4.5 在MFC程序里使用libxl库:Excel读写的基本套路
如果MFC程序要读取或生成Excel文件,常见做法是用libxl这个第三方库,它不依赖Office COM,部署方便。VC6下使用libxl的标配流程是:把libxl的头文件和lib文件路径加进工程,然后在代码里调用:
#include "libxl.h" using namespace libxl; Book* book = xlCreateBook(); if (book != NULL) { Sheet* sheet = book->addSheet(_T("Sheet1")); if (sheet != NULL) { sheet->writeStr(0, 0, _T("姓名")); sheet->writeNum(1, 0, 1234.5); book->save(_T("C:\\out.xls")); } book->release(); }xlCreateBook创建的是xls格式的book,xlCreateXMLBook才是xlsx格式,这个函数选择是新手最容易犯的错。writeStr的第一个参数是行索引,第二个是列索引,都是从0开始;writeNum写入数字,writeStr写入字符串。最后的release必须调用,否则内存泄漏。库分Debug和Release版本,分别对应不同的lib文件,配置错的话链接阶段会报一堆无法解析的外部符号。
5. MFC入门避坑清单:闪退、创建失败与内存泄漏的排查顺序
5.1 一点击“打开”就闪退:CFileDialog的缓冲区没给够
现象:在VC6.0的MFC对话框里加了一个“打开”按钮,点击后文件对话框弹出来,选好几个文件点确定,程序瞬间闪退,没有任何报错。这个问题在论坛上被反复问,很多人以为是系统兼容性问题,其实是CFileDialog多选时的缓冲区长度的经典坑。CFileDialog内部使用OPENFILENAME结构,其中lpstrFile指向的文件名缓冲默认长度有限,多选文件时所有路径拼在一起超过了缓冲区,越界写入直接导致程序崩溃。
原因明确后解决起来就简单了,自己分配一个足够大的缓冲区,再塞回结构体里:
void CMyDlgDlg::OnBnClickedBtnOpen() { CFileDialog dlg(TRUE, NULL, NULL, OFN_FILEMUSTEXIST | OFN_ALLOWMULTISELECT, NULL, this); TCHAR szBuffer[32768] = {0}; // 32KB缓冲,满足多选需求 dlg.m_ofn.lpstrFile = szBuffer; dlg.m_ofn.nMaxFile = sizeof(szBuffer) / sizeof(TCHAR); if (dlg.DoModal() == IDOK) { POSITION pos = dlg.GetStartPosition(); while (pos != NULL) { CString strPath = dlg.GetNextPathName(pos); AfxMessageBox(strPath); } } }GetStartPosition返回第一个文件的位置,GetNextPathName每调用一次返回一个完整路径并把指针移到下一个。如果你只是想选单个文件,把OFN_ALLOWMULTISELECT去掉,闪退问题自然也不会发生。但如果你勾选了多选,就必须处理缓冲区问题。
5.2 MFC静态库中对话框创建失败:资源ID冲突与模块状态
现象:工程设置为“Use MFC in a static library”后,生成的exe运行到DoModal时返回-1,对话框弹不出来,而换成共享DLL编译就一切正常。
原因:静态链接MFC时,整个MFC库的代码和资源都进到你的exe里。如果工程里存在两个具有相同ID的对话框模板,或者资源ID与MFC内部使用的标准ID重号,DoModal找不到正确的模板就会失败。另一个常见原因是模块状态没有切换,比如你从DLL里调用exe的对话框,静态MFC下必须用AFX_MANAGE_STATE宏管理模块句柄。
void CMyDlgDlg::OnBnClickedCall() { AFX_MANAGE_STATE(AfxGetStaticModuleState()); // 这里的MFC调用如果仍然失败,检查资源ID是否重复 CMyChildDlg dlg; if (dlg.DoModal() == -1) { AfxMessageBox(_T("对话框创建失败")); } }排查顺序是:先打开Resource.h检查所有ID号是否有重叠,特别是对话框ID对着控件的ID段里取值;再检查InitInstance里是否调用了AfxEnableControlContainer。如果这两项都正常,就把工程设置为共享DLL跑一遍,排除静态库特有的链接问题。静态MFC包体积大,但部署省事,适合发给没有VC6环境的目标机器。
5.3 CString内存泄漏:CStringArray的删除陷阱
现象:程序退出时,VC6的输出窗口提示检测到内存泄漏,锁定位置在CString相关代码上。原因多半出在CStringArray的使用方式上。VC6的CString在Debug模式下自带泄漏检测,但它只在“你直接new了CString对象但没delete”或“CStringArray里存的是CString*指针但只清空了数组”的时候才会报告。
例如,你在程序里用了一个CStringArray成员变量来存字符串,每次添加一个新的CString对象:
CStringArray m_arrStrings; // 不要这样存指针,除非你记得释放 m_arrStrings.Add(new CString(_T("hello"))); // 退出前只RemoveAll会泄漏,需要先释放每个元素 for (int i = 0; i < m_arrStrings.GetSize(); i++) { delete m_arrStrings.GetAt(i); } m_arrStrings.RemoveAll();如果数组里存的是CString对象本身,比如Add(str),那RemoveAll就够了,不需要delete,delete反而会崩溃。这条经验的核心是区分数组元素是“对象”还是“对象指针”。VC6的内存泄漏报告在很多情况下并不指向真正泄漏的那一行,它只给你一个分配时的调用栈,你要根据调用栈往前找是谁分配了这块内存,不要盯着CString的构造函数发呆。
5.4 dumpcont.cpp的ATLTRACE警告:调试输出不等于崩溃
现象:程序运行到某个对话框关闭时,Output窗口出现“f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\dumpcont.cpp(23) : atltracegeneral”之类的输出。很多新手看到这条信息以为程序要挂了,其实是MFC的CTrace输出,那行路径来自VS2003/2005时代的MFC头文件,你在VC6里编译老工程时偶尔也会带出来。它只在Output窗口显示,不影响Release运行。
原因是CObject的Dump函数在调试状态下输出对象的内部状态,比如类名和成员变量值。解决方式是:如果你用的是ClassWizard自动生成的代码,Dump函数里默认调用了CDialog::Dump,当你手动调用Dump或调试器求值时就会触发。这不是错误,你只需要记住,看到atltracegeneral开头的行,后面跟着的不是语法箭头“: error”,就可以直接忽略。
5.5 从高版本Visual Studio迁移回VC6的控件消失问题
现象:把一个高版本VS里写的对话框程序复制到VC6里编译,窗口能弹出来,但上面的按钮、编辑框全部消失,只剩一个光秃秃的对话框。原因有两种:一是新版VS生成rc资源文件里控件语法包含VC6不识别的新特性,比如“CONTROL”里带ex样式或者自定义类;二是Resource.h里的ID定义和高版本不同步,VC6读不到正确的ID值。
解决方式:先用记事本打开高版本生成的rc文件,把每个对话框模板里的CONTROL语句逐行检查,去掉VC6不认识的关键词。最稳妥的做法是在VC6里新建一个空对话框工程,然后把高版本工程里的对话框模板用文本方式复制过去,逐个调整控件ID。这个工作很枯燥,但它能同时排查出ID冲突、资源语法兼容两个问题。迁回VC6本身就不是一条顺路,能避免就尽量避免,非做不可时留出半小时手动修rc文件的预算。
6. 综合练习:用对话框做一个能计算的两位数四则运算器
把前面所有知识串起来最好的办法,是做一个类似于Windows自带计算器的简化版,支持两位数的加减乘除。设计故意限定在两位数,是为了彻底避开复杂的输入校验逻辑,把注意力放在消息映射、控件取值和结果显示上,这个范围对入门者最友好。
在对话框上放两个Edit控件用来输入操作数,一个Combo Box用来选择运算符,一个“计算”按钮,一个Static控件显示结果。给两个编辑框绑定int型成员变量,也可以不绑变量,直接通过控件取值。注意GetDlgItemInt只能读整数,如果输入小数或非法字符,它会按0处理,所以这里有一个容易翻车的细节:要做乘法除法,最好的方式是读文本再转数值:
void CCalcDlgDlg::OnBnClickedCalc() { CString strA, strB; GetDlgItemText(IDC_EDIT_A, strA); GetDlgItemText(IDC_EDIT_B, strB); double a = _tstof(strA); double b = _tstof(strB); int nSel = m_comboOp.GetCurSel(); double result = 0; CString strResult; switch (nSel) { case 0: result = a + b; break; case 1: result = a - b; break; case 2: result = a * b; break; case 3: if (b == 0.0) { AfxMessageBox(_T("除数不能为0")); return; } result = a / b; break; default: AfxMessageBox(_T("请选择运算符")); return; } strResult.Format(_T("%.2f"), result); SetDlgItemText(IDC_STATIC_RESULT, strResult); }GetDlgItemText是对话框类的成员函数,把控件ID对应的文本读进CString,_tstof把字符串转成double。Format里的%.2f控制小数点后两位,这是一个很实用的显示精度控制,超出两位自动四舍五入。关键边界条件是除零,必须主动判断b是否等于0,否则double类型会给一个INF,显示出来的结果会让用户觉得程序坏了。
做完这个计算器,你可以试着给自己加需求:支持连续运算,把上一次的结果作为下一个操作数;支持键盘回车触发计算;把计算历史记录到CListCtrl里。每加一个需求,你都会碰到一个具体的坑,比如连续运算需要考虑结果回填、键盘事件要在PreTranslateMessage里拦截、历史记录要处理滚动条。我的习惯是每完成一个功能就F7编译一次,不累积错误,宁可多花时间在断点里走一遍消息分发,也不要在代码堆里猜。MFC入门就是这样一个反复“看得见结果”的过程,对话框弹出来、按钮响应、数据正确,每一步的反馈都直接且及时。希望这些路径和边界能帮你在VC6.0上少走几段弯路,把时间花在真正值得打磨的逻辑上。
本文还有配套的精品资源,点击获取