1. 项目概述:为什么今天还要聊VC6?
Visual C++ 6.0,这个诞生于1998年的开发工具,在今天听起来像是“古董”。当大家都在谈论VS Code、Visual Studio 2022、AI编程助手的时候,我为什么还要花时间写一篇关于VC6的“新手实战指南”?这听起来有点反潮流,甚至有点“劝退”。但恰恰相反,我认为对于特定人群,这恰恰是一条最务实、最高效的入门路径。
首先,VC6是一个极其“纯粹”的C++开发环境。它没有现代IDE那些花里胡哨的智能提示、代码重构、实时错误检查(早期版本检查很弱),甚至它的编译器对C++标准的支持还停留在很早期的阶段。这听起来是缺点,但对初学者来说,却是巨大的优点。你必须亲手敲下每一个字符,理解每一个头文件的作用,明确每一个库函数的链接。错误不会在你打字时就被标红,而是在编译、链接、运行时才暴露出来,迫使你从最底层去理解“程序是如何构建并运行的”。这个过程,是理解编程本质的绝佳训练。其次,VC6所代表的MFC(Microsoft Foundation Classes)框架,虽然庞大且略显陈旧,但它构建的“文档/视图”架构、消息映射机制,是理解Windows桌面应用程序开发基石的最佳教材。很多现代GUI框架的思想,都能在MFC中找到影子。最后,也是最重要的一点:大量遗留的工业软件、嵌入式上位机、工控系统(如部分PLC的上位机配置软件)、甚至一些高校的教学实验环境,仍然在使用或基于VC6开发。掌握它,意味着你能看懂、维护甚至修改这些“活化石”级的代码,这在特定领域(如工业自动化、传统制造业)是一项非常稀缺且宝贵的技能。
所以,这篇指南的目标读者很明确:计算机相关专业的低年级学生,希望从底层理解C++和Windows编程;需要维护或学习遗留MFC项目代码的工程师;以及对“一个完整的.exe是如何从代码变出来”充满好奇的硬核爱好者。如果你追求的是开发最新的移动应用或Web服务,那VC6确实不适合你。但如果你想打下坚实的C++基础,并一窥传统Windows桌面开发的堂奥,那么跟我一起,打开这个经典的开发环境吧。
2. 环境准备与第一印象:安装、配置与第一个程序
2.1 获取与安装:在现代系统上运行“老古董”
在Windows 10或11上安装VC6,本身就是一个有趣的“兼容性实战”。你很难从官方渠道找到完整的安装包,通常需要寻找一些爱好者打包的版本,或者从旧的安装光盘镜像中获取。一个常见的版本是“Visual C++ 6.0 with SP6”(Service Pack 6),这个补丁包修复了大量关键问题,是必须安装的。
安装过程可能会遇到几个典型的“坑”:
- 兼容性设置:安装程序
setup.exe可能需要以Windows XP(Service Pack 3)兼容模式运行,并以管理员身份启动。 - 注册表权限:安装过程中,可能会因为注册表访问权限问题而失败。一种常见的解决方法是,临时禁用用户账户控制(UAC),并在安装完成后重新启用。更安全的方法是,手动对安装程序赋予相应的注册表权限,但这操作较为复杂。
- 安装路径:建议不要安装在默认的
C:\Program Files (x86)\...下,因为路径中的空格和括号有时会引起古老编译脚本的解析问题。可以安装到C:\VC6或D:\Dev\VC6这样的简单路径下。 - MSDN帮助文档:VC6的精华之一是其离线的MSDN Library。如果安装包中包含,务必安装。它的离线搜索和本地示例代码,是那个时代开发者最重要的知识库,其内容组织方式至今仍值得学习。
安装成功后,你会在开始菜单看到一个经典的图标。启动它,那个深灰色、像素感十足的界面就是你的新战场了。
2.2 认识IDE:经典的三板斧界面
第一次打开VC6,你会看到几个核心窗口:
- 工作区窗口(Workspace):通常位于左侧,有
ClassView(类视图)、ResourceView(资源视图)、FileView(文件视图)三个标签。这是你项目的导航中心。 - 代码编辑窗口:中间的主区域。
- 输出窗口(Output):底部,用于显示编译、链接、调试等信息。这是你未来会看得最多的地方之一,所有的错误和警告都在这里。
与现代IDE最大的不同在于,VC6的智能感知(IntelliSense)几乎等于没有。它不会自动补全类成员,不会实时提示参数。你需要习惯使用Ctrl+Space(如果可用)进行简单的单词补全,更多时候是依靠记忆和MSDN。这迫使你更熟悉API名称和拼写。
2.3 创建第一个项目:“Hello, World!”的MFC版本
我们不从控制台程序开始,因为VC6在控制台编程上并无特殊优势。我们直接切入其核心领域——用MFC创建图形界面程序。这能让你立刻感受到VC6的“威力”与“繁琐”。
- 新建项目:点击
File->New,选择Projects标签页。 - 选择项目类型:选择
MFC AppWizard (exe),在Project name中输入MyFirstApp,选择一个合适的Location。 - 应用向导(AppWizard):这是VC6/MFC的核心工具之一,它通过一系列对话框帮你生成应用程序骨架。
- 步骤1:选择应用程序类型。选择
Single document(单文档),取消Document/View architecture support?的勾选?不,新手请务必勾选!这是MFC的精华架构,我们先接受它。资源语言选择中文。 - 步骤2:数据库支持。选择
None。 - 步骤3:复合文档支持。选择
None。 - 步骤4:应用程序特性。保持默认,注意观察它默认勾选了哪些工具栏、状态栏、打印等选项。这就是MFC框架为你提供的“基础设施”。
- 步骤5:选择项目风格(
MFC Standard)和是否生成源文件注释。务必选择Yes, please,生成的代码注释是极佳的学习材料。 - 步骤6:查看生成的类。这里列出了AppWizard为你创建的核心类:
CMyFirstApp(应用类)、CMyFirstDoc(文档类)、CMyFirstView(视图类)、CMainFrame(主框架类)。记住它们,这是MFC程序的四大支柱。
- 步骤1:选择应用程序类型。选择
点击Finish,再点击OK。VC6会花一点时间生成一大堆文件。不要被吓到,大部分时间你只需要关注其中几个。
不写一行代码,直接运行:按
F7编译链接,然后按Ctrl+F5运行(不调试)。你会看到一个标准的Windows应用程序窗口,有菜单、工具栏、状态栏。虽然什么都没做,但一个应用程序的架子已经搭好了。这就是MFC框架的力量——它为你处理了Windows应用程序那些繁琐的初始化、消息循环和窗口管理。添加第一行自己的代码:让我们在视图窗口中央显示“Hello, VC6!”。
- 在
Workspace的ClassView中,找到CMyFirstView类,展开它,双击OnDraw函数。这个函数负责在视图上绘制内容。 - 在
OnDraw函数体内,你会看到一行注释// TODO: add draw code for native data here。在这行注释下面,添加代码:pDC->TextOut(100, 100, _T("Hello, VC6!")); - 再次按
F7编译,Ctrl+F5运行。你应该能在窗口客户区的(100, 100)像素位置看到这行文字。
- 在
注意:
pDC是OnDraw的参数,是一个指向设备上下文(Device Context)的指针。你可以把它想象成一张画布和一套画笔的集合。TextOut就是在画布上写字。_T()宏是为了兼容Unicode和ANSI字符集,在VC6的默认设置下,它其实就是将字符串包裹起来,可以暂时理解为和"Hello, VC6!"等价。
3. 核心概念深入:文档/视图架构与消息映射
3.1 理解文档与视图:数据与显示的分离
你刚才生成的程序,就是一个典型的“文档/视图”架构应用。这是MFC最核心的设计模式,理解它至关重要。
- 文档(Document):代表数据。它负责数据的加载、保存(序列化)和管理。在
CMyFirstDoc类中,你可以定义成员变量来存储你的应用程序数据,比如一个字符串列表、一组坐标点等。 - 视图(View):代表显示。它负责将文档中的数据以某种方式呈现给用户(比如在窗口中绘制出来),并接收用户的交互操作(鼠标、键盘)。
CMyFirstView的OnDraw函数,就是用来绘制文档数据的。 - 关系:一个文档可以对应多个视图(例如,同一份数据既用表格显示,又用图表显示)。框架负责在文档数据更新时,通知所有关联的视图进行更新(调用
UpdateAllViews和OnUpdate)。
实战理解:让我们修改程序,让文档存储一个字符串,视图显示它。
- 在
CMyFirstDoc类的头文件(MyFirstDoc.h)的public:属性下,添加一个成员变量:CString m_strData; // CString是MFC中常用的字符串类 - 在
CMyFirstDoc类的构造函数(CMyFirstDoc::CMyFirstDoc())中,初始化这个变量:m_strData = _T("Default Document Data"); - 回到
CMyFirstView::OnDraw函数,将之前的写死字符串,改为显示文档中的数据:// pDC->TextOut(100, 100, _T("Hello, VC6!")); CMyFirstDoc* pDoc = GetDocument(); // 获取与当前视图关联的文档指针 ASSERT_VALID(pDoc); // 断言文档指针有效,调试利器 pDC->TextOut(100, 100, pDoc->m_strData); - 运行程序,你会发现窗口上显示的是“Default Document Data”。你并没有直接操作视图的数据,而是视图从文档中获取数据来显示。这就是分离的好处。
3.2 掌握消息映射:Windows程序的心脏
Windows程序是事件驱动的。用户的点击、按键、移动鼠标,系统发出的定时器、绘制请求,都是“消息”。MFC通过一套称为“消息映射”的机制,将特定的Windows消息关联到你的类的成员函数上。
如何添加一个消息处理函数?这是VC6里非常经典的操作:
- 查看类:在
ClassView中,右键点击CMyFirstView类,选择Add Windows Message Handler...。 - 选择消息:在弹出的对话框中,左侧列表是可供处理的消息。找到
WM_LBUTTONDOWN(鼠标左键按下),双击它或点击Add Handler。 - 接受默认函数名:它会建议一个函数名
OnLButtonDown,点击OK。 - 观察变化:VC6自动做了三件事:
- 在
MyFirstView.h文件的类声明中,添加了函数声明:afx_msg void OnLButtonDown(UINT nFlags, CPoint point); - 在
MyFirstView.cpp文件中,添加了一个宏块BEGIN_MESSAGE_MAP ... END_MESSAGE_MAP,并在其中添加了映射:ON_WM_LBUTTONDOWN()。 - 在
MyFirstView.cpp文件末尾,生成了函数的空实现骨架。
- 在
- 实现功能:在
OnLButtonDown函数体内,添加代码,使得点击鼠标时,改变文档数据并更新视图。void CMyFirstView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default CMyFirstDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // 修改文档数据 pDoc->m_strData.Format(_T("You clicked at (%d, %d)"), point.x, point.y); // 通知文档数据已修改 pDoc->SetModifiedFlag(TRUE); // 更新所有与此文档关联的视图 pDoc->UpdateAllViews(NULL); CView::OnLButtonDown(nFlags, point); } - 运行测试:编译运行后,在窗口客户区任意位置点击鼠标,你会发现显示的文字变成了你点击的坐标。
OnLButtonDown捕获了鼠标消息,修改了文档(数据层),然后通过UpdateAllViews触发了视图的刷新(OnDraw被调用),从而更新了显示。
这个过程完美诠释了MFC的工作流程:消息 -> 消息映射 -> 处理函数 -> 修改数据/状态 -> 更新界面。虽然现代框架有更优雅的事件处理方式,但理解这个底层机制,对理解任何GUI编程都有裨益。
4. 资源编辑与对话框编程:设计用户界面
4.1 使用资源编辑器:所见即所得
VC6内置了强大的资源编辑器,用于设计对话框、菜单、工具栏、图标、位图等。这是“所见即所得”的界面设计。
- 切换到
ResourceView。你会看到MyFirstApp resources,下面有Dialog、Menu、Toolbar、Icon等文件夹。 - 展开
Dialog,你会看到IDD_ABOUTBOX(关于对话框)。双击它,资源编辑器会打开,你可以像画图一样拖放控件(按钮、文本框等)。 - 添加一个新的对话框:右键点击
Dialog文件夹,选择Insert Dialog。一个空白的对话框模板出现了。 - 添加控件:从右侧的控件工具箱(如果没出现,按
Ctrl+Alt+X)拖一个Edit Control(文本框)和一个Button到对话框上。 - 修改属性:右键点击文本框,选择
Properties。将其ID改为IDC_EDIT_INPUT。右键点击按钮,将其Caption(标题)改为“显示”,ID改为IDC_BTN_SHOW。 - 创建对话框类:关闭属性框,在对话框模板的任意空白处双击。VC6会提示你为这个新的对话框资源创建一个类。点击
OK,输入类名CMyDialog,基类选择CDialog。点击OK。
现在,你有了一个对话框资源和一个关联的C++类CMyDialog。
4.2 对话框数据交换与验证:连接界面与代码
如何获取用户在文本框里输入的内容?MFC使用DDX(Dialog Data Exchange)和DDV(Dialog Data Validation)机制。
- 为控件关联变量:在对话框编辑器里,确保
CMyDialog类是激活的(可以通过ClassView双击CMyDialog)。然后,按住Ctrl键并双击文本框控件。或者,通过菜单View->ClassWizard(快捷键Ctrl+W),这是VC6的另一个核心工具。 - 在
MFC ClassWizard对话框中,选择Member Variables标签页。确保Class name是CMyDialog。在Control IDs列表中选择IDC_EDIT_INPUT,点击Add Variable...。 - 在
Add Member Variable对话框中,输入变量名m_strInput,类别选择Value,变量类型选择CString。你可以看到DDX和DDV的选项。我们可以在Maximum Characters输入50,作为简单的验证。点击OK。 - 观察变化:
ClassWizard在CMyDialog类的头文件中添加了CString m_strInput;声明,并在DoDataExchange函数中添加了DDX_Text和DDV_MaxChars的调用。这个函数会在对话框显示前和数据交换时被框架自动调用。
4.3 显示对话框并处理按钮事件
- 添加菜单项触发对话框:在
ResourceView中,打开IDR_MAINFRAME菜单资源。在“编辑”菜单末尾添加一个分隔符和一个新的菜单项,Caption设为“显示对话框(&D)”,ID设为ID_EDIT_SHOWDLG。 - 为菜单项添加消息处理:打开
ClassWizard(Ctrl+W),选择Message Maps标签页。Class name选择CMyFirstView(因为我们想在视图里处理这个菜单)。Object IDs选择ID_EDIT_SHOWDLG,Messages选择COMMAND,点击Add Function,接受默认函数名OnEditShowdlg。 - 实现菜单处理函数:在
OnEditShowdlg函数中,编写显示对话框和获取数据的代码。void CMyFirstView::OnEditShowdlg() { CMyDialog dlg; // 创建对话框对象 if (dlg.DoModal() == IDOK) // 以模态方式显示对话框,并判断是否点击了OK按钮 { // 用户点击了OK,数据已经通过DDX从控件更新到了dlg.m_strInput CMyFirstDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); pDoc->m_strData = dlg.m_strInput; // 将对话框输入赋给文档数据 pDoc->SetModifiedFlag(TRUE); pDoc->UpdateAllViews(NULL); // 更新视图显示 } // 如果点击Cancel,DoModal返回IDCANCEL,则什么都不做 } - 为对话框的按钮添加事件:我们需要在对话框类
CMyDialog中处理“显示”按钮的点击。在对话框编辑器中,双击“显示”按钮。ClassWizard会弹出,询问是否为IDC_BTN_SHOW的BN_CLICKED消息添加处理函数。点击OK,函数名默认为OnBtnShow。 - 实现按钮处理函数:在
CMyDialog::OnBtnShow()中,我们可以做一些即时处理,比如弹出一个消息框显示当前输入。void CMyDialog::OnBtnShow() { UpdateData(TRUE); // 将控件中的数据更新到成员变量(m_strInput) AfxMessageBox(m_strInput); // 弹出一个消息框显示字符串 // UpdateData(FALSE); // 如果需要用变量值更新控件,则调用FALSE }UpdateData(TRUE)是关键,它触发了DoDataExchange函数,将界面上文本框的内容抓取到m_strInput变量中。
编译运行。通过菜单打开对话框,在文本框输入内容,点击“显示”按钮会弹出消息框,点击“确定”关闭消息框,再点击对话框的“OK”,主窗口的显示内容就会更新。这一套流程,是MFC对话框编程的标准模式。
5. 调试技巧与常见问题排查
VC6的调试器虽然古老,但核心功能(断点、单步、监视、调用栈)一应俱全,且非常直接。掌握它是解决编程问题的关键。
5.1 基础调试操作
- 设置断点:在代码行左侧灰色区域点击,出现一个红点。或按
F9。 - 开始调试:按
F5(或Go),程序会运行到断点处停止。 - 单步执行:
F10:单步越过(Step Over),执行当前行,如果遇到函数调用,不进入函数内部。F11:单步进入(Step Into),执行当前行,如果遇到函数调用,则进入该函数内部。Shift+F11:单步跳出(Step Out),执行完当前函数剩余部分,返回到调用它的地方。
- 监视变量:在调试状态下,将鼠标悬停在变量上可以快速查看其值。更正式的方法是打开
Watch窗口(View->Debug Windows->Watch),输入变量名。 - 查看调用栈:
Call Stack窗口(Alt+7)显示当前执行到此处所经过的函数调用链,对于理解程序流程和定位崩溃点至关重要。
5.2 VC6编程典型问题与解决
以下是一些新手在VC6环境下极易遇到的问题及其排查思路:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 编译错误 LNK2001: unresolved external symbol | 这是最常见的链接错误,意味着编译器找到了函数声明,但链接器找不到函数实现(定义)。 | 1.检查库文件:你是否在Project->Settings->Link标签页的Object/library modules中添加了所需的.lib文件?例如,使用WinInet网络功能需要添加wininet.lib。2.检查函数签名:头文件中的声明和.cpp文件中的定义是否完全一致(包括参数类型、常量性 const)?3.检查项目设置:如果是自己编写的类,确保对应的.cpp文件被添加到了项目中(在 FileView中可见)。 |
| 编译错误 C2065: ‘XXXX’ : undeclared identifier | 标识符未声明。 | 1.检查头文件包含:是否包含了声明该标识符的头文件(如#include <afxwin.h>对于MFC基础类)?2.检查拼写和大小写:C/C++区分大小写。 3.检查项目设置: Project->Settings->C/C++标签页,Preprocessor definitions中是否定义了必要的宏?例如,使用MFC需要定义_AFXDLL等。 |
| 运行时错误:Debug Assertion Failed! | 这是MFC调试版本中常见的断言失败。断言是开发者在代码中设置的检查点,失败意味着程序状态违反了预期。 | 1.仔细阅读断言信息:弹出的对话框会告诉你断言发生的文件(如afxwin1.inl)和行号。点击“重试”可以进入调试器。2.查看调用栈:在调试器中,查看调用栈,找到你自己代码中最接近顶层的函数,通常问题就出在那里。 3.常见原因:传递了空的或无效的指针(如 NULL)、窗口句柄无效、在错误的线程中访问MFC对象等。断言是帮你发现bug的朋友,不是敌人。 |
| 程序一运行就崩溃(尤其是Release版) | Release版优化了代码,去掉了许多调试检查,有时会掩盖问题。 | 1.在Debug版下复现:先在Debug模式下运行和调试,利用断言和调试器找到问题根源。 2.检查未初始化的变量:特别是局部指针和类成员变量。 3.检查数组越界:这是导致“莫名其妙”崩溃的元凶之一。使用 CRT调试功能(如_CrtSetDbgFlag)可以帮助检测。4.检查资源泄漏:MFC对象(如 CWnd,CDC)是否配对创建和销毁?new/delete,malloc/free是否匹配? |
| 对话框或控件显示乱码 | 字符集问题。VC6默认使用多字节字符集(MBCS),而现代Windows更倾向于Unicode。 | 1.使用_T()宏包裹所有字符串字面量,例如_T(“你好”)。_T在MBCS配置下就是普通字符串,在Unicode配置下会变成宽字符串L”你好”。2.使用MFC的便携字符串类 CString,它内部处理了字符集转换。3.统一项目字符集设置: Project->Settings->C/C++标签页,Preprocessor definitions中,确认是_MBCS(多字节)还是_UNICODE。通常保持默认的_MBCS对新手更友好。 |
| 无法打开包含文件或找不到库 | 编译器/链接器搜索路径问题。 | 1.检查包含目录:Tools->Options->Directories。确保必要的头文件路径(如Platform SDK的路径)在列表中。2.检查库目录:同上,在 Directories标签页选择Library files,确保必要的.lib文件路径存在。3.绝对路径与相对路径:在代码中包含文件时,使用 #include “相对路径/头文件.h”或#include <系统头文件>。确保相对路径相对于项目目录或配置的包含目录是正确的。 |
5.3 调试心得:利用好“输出窗口”和“TRACE宏”
除了调试器,Output窗口是你的另一个好朋友。你可以在代码中使用TRACE宏输出调试信息,这些信息只在Debug版本中有效,且会显示在Output窗口的Debug标签页下。
int nCount = 10; CString str = _T("Test"); TRACE(_T("当前计数:%d, 字符串:%s\n"), nCount, str);这对于跟踪程序流程、查看变量值而不中断程序运行非常有用。它比MessageBox更轻量,且不会阻塞界面。
6. 进阶实战:自定义控件与文件操作
6.1 绘制自定义视图:简单的图形绘制
MFC的CView类(及其派生类)的OnDraw函数是你的画布。除了TextOut,CDC类提供了丰富的绘图函数。
示例:在视图中绘制一个随数据变化的矩形
- 在
CMyFirstDoc类中添加一个表示矩形大小的成员变量:CSize m_rectSize;,并在构造函数中初始化,例如m_rectSize = CSize(50, 50);。 - 修改
CMyFirstView::OnLButtonDown,让点击鼠标时矩形变大。void CMyFirstView::OnLButtonDown(UINT nFlags, CPoint point) { CMyFirstDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // 修改矩形大小,每次点击长宽各加10 pDoc->m_rectSize.cx += 10; pDoc->m_rectSize.cy += 10; pDoc->SetModifiedFlag(TRUE); pDoc->UpdateAllViews(NULL); CView::OnLButtonDown(nFlags, point); } - 修改
CMyFirstView::OnDraw,绘制这个矩形。void CMyFirstView::OnDraw(CDC* pDC) { CMyFirstDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // 创建一个画笔,红色,2像素宽 CPen newPen(PS_SOLID, 2, RGB(255, 0, 0)); CPen* pOldPen = pDC->SelectObject(&newPen); // 选入设备上下文,并保存旧画笔 // 创建一个画刷,蓝色实心填充 CBrush newBrush(RGB(0, 0, 255)); CBrush* pOldBrush = pDC->SelectObject(&newBrush); // 计算矩形位置(居中) CRect rect; GetClientRect(&rect); // 获取视图客户区矩形 int left = rect.Width() / 2 - pDoc->m_rectSize.cx / 2; int top = rect.Height() / 2 - pDoc->m_rectSize.cy / 2; // 绘制矩形 pDC->Rectangle(left, top, left + pDoc->m_rectSize.cx, top + pDoc->m_rectSize.cy); // 恢复旧的GDI对象(重要!避免资源泄漏) pDC->SelectObject(pOldPen); pDC->SelectObject(pOldBrush); // 画笔和画刷对象在函数结束时析构,自动释放资源 }
运行程序,点击鼠标,你会看到一个居中且不断变大的红边蓝色矩形。这个例子展示了GDI(图形设备接口)的基本使用、资源(画笔、画刷)的管理,以及如何将文档数据与视图绘制动态结合。
6.2 文件序列化:保存与加载数据
MFC通过“序列化”机制简化了文件的保存和加载。文档类CDocument的Serialize函数是核心。
- 为文档类添加可序列化的数据:我们让
m_strData和m_rectSize能够被保存。- 确保你的文档类成员变量是MFC可序列化的类型(如
CString,CSize,DWORD,int等)或你自己实现了序列化的类。 - 修改
CMyFirstDoc::Serialize函数:void CMyFirstDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) // 正在保存 { // TODO: add storing code here ar << m_strData; ar << m_rectSize.cx << m_rectSize.cy; } else // 正在加载 { // TODO: add loading code here ar >> m_strData; ar >> m_rectSize.cx >> m_rectSize.cy; } }
- 确保你的文档类成员变量是MFC可序列化的类型(如
- 触发保存与打开:框架已经为你处理了“文件”->“保存”、“另存为”、“打开”菜单项(
ID_FILE_SAVE,ID_FILE_SAVE_AS,ID_FILE_OPEN)。它们会调用OnSaveDocument,OnOpenDocument,最终调用Serialize。 - 修改标志:注意我们在之前修改数据后调用了
pDoc->SetModifiedFlag(TRUE);。这会在文档被修改后,在窗口标题栏文件名前显示一个星号*,并且在关闭程序时会提示保存。 - 测试:运行程序,修改字符串或点击鼠标改变矩形大小,点击“文件”->“保存”,选择一个位置和文件名(如
test.mfd,.mfd是随意扩展名)。关闭程序再重新打开,通过“文件”->“打开”加载刚才的文件,数据应该被恢复。
序列化是MFC一个非常强大的特性,它用统一的CArchive对象像流水一样处理数据的存入和取出,对于保存程序状态非常方便。
7. 项目构建与发布:从Debug到Release
7.1 理解不同的构建配置
VC6项目默认有两种配置:Win32 Debug和Win32 Release。
- Debug:包含完整的调试符号,关闭了大部分编译器优化,启用了断言(ASSERT)和调试堆(Debug Heap)检查。生成的
.exe文件较大,运行较慢,但便于调试。开发阶段始终使用Debug配置。 - Release:移除了调试符号,开启了编译器优化(速度、大小),禁用了断言和调试堆。生成的
.exe文件较小,运行速度快。准备分发最终程序时使用Release配置。
切换配置:在工具栏的Build工具栏上有一个下拉列表框(通常显示Win32 Debug),可以在这里切换。
7.2 发布前的检查与设置
当你完成开发,准备生成一个可以分发给别人使用的程序时,需要做以下几步:
- 切换到Release配置。
- 检查运行时库:
Project->Settings->C/C++标签页,Category选择Code Generation。在Use run-time library下拉框中,Release配置通常选择Multithreaded或Multithreaded DLL。Multithreaded:静态链接C运行时库(CRT)。生成的.exe不依赖msvcrt.dll等,但文件稍大。Multithreaded DLL:动态链接C运行时库。生成的.exe更小,但需要目标机器上有相应版本的运行时库(通常系统自带或可通过安装VC++ Redistributable解决)。对于VC6,更推荐静态链接以避免依赖问题。
- 清理并重建:
Build->Clean,然后Build->Rebuild All。确保是从零开始编译链接,避免残留的中间文件导致问题。 - 查找生成文件:在项目目录下,会生成一个
Release文件夹(Debug版则在Debug文件夹)。里面的.exe文件就是你的发布文件。 - 依赖项检查:如果你的程序使用了特殊的DLL(非系统自带),如数据库驱动、图像处理库等,你需要将这些DLL与你的
.exe一起分发。可以使用Depends.exe(Dependency Walker,一个老牌工具)来查看你的.exe文件的动态链接依赖。对于纯MFC程序,如果使用静态链接MFC(项目设置Use of MFC选择Use MFC in a Static Library),则.exe是基本独立的。如果动态链接(Use MFC in a Shared DLL),则需要目标机器上有MFC42.DLL等文件(现代Windows系统通常已内置)。
7.3 静态链接MFC
为了最大程度避免依赖问题,对于小型工具,可以采用静态链接。
Project->Settings。- 在左上角的
Settings For下拉框选择All Configurations(这样Debug和Release会一起改)。 - 切换到
General标签页。 - 在
Microsoft Foundation Classes下拉框中,选择Use MFC in a Static Library。 - 点击
OK,然后Rebuild All。
这样生成的.exe文件会变大不少,因为它把用到的MFC代码都打包进去了,但好处是可以在几乎任何Windows机器上运行,无需担心MFC DLL版本问题。
走到这里,你已经完成了从零开始,使用Visual C++ 6.0创建一个具备图形界面、交互功能、数据持久化能力的完整Windows应用程序的全过程。这个过程充满了“手动”的乐趣和挑战,每一个细节都需要你亲自掌控。虽然工具古老,但你所学习的核心概念——消息循环、文档视图、资源管理、GDI绘图、文件序列化——是Windows桌面编程的基石。在现代框架中,这些概念被封装得更优雅,但底层逻辑相通。当你下次使用更现代的工具时,你会感激在VC6中打下的这些扎实基础,因为它让你真正理解了窗口背后的故事。