VC++6.0画图程序开发:从零实现Windows GUI与MFC消息机制

发布时间:2026/8/8 16:31:13
VC++6.0画图程序开发:从零实现Windows GUI与MFC消息机制 1. 项目概述为什么今天还要聊VC6.0如果你是一位有十年以上开发经验的“老鸟”看到“VC6.0”这个标题嘴角可能会不自觉地扬起一丝复杂的微笑。这个诞生于1998年的集成开发环境IDE连同其背后的MFCMicrosoft Foundation Classes框架曾经是无数Windows桌面应用开发的起点也是许多程序员C和Windows编程的启蒙老师。然而在Visual Studio 2022、C20标准大行其道的今天再回头去折腾一个二十多年前的“古董”是不是有点开历史倒车的意思恰恰相反。我认为对于初学者而言从VC6.0入手学习Windows图形界面GUI编程尤其是基础的画图程序依然是一条极具价值的“捷径”。原因有三第一环境纯粹。VC6.0MFC提供了一个相对封闭、简单的开发环境没有现代IDE那么多复杂的项目模板、属性配置和依赖管理能让初学者将注意力完全集中在Windows消息机制、GDI绘图、文档/视图架构等核心概念上。第二反馈直接。你写的每一行绘图代码几乎都能立刻在窗口上看到效果这种即时反馈对学习动力是巨大的鼓舞。第三理解底层。通过它你能最直观地理解一个Windows程序从消息循环到窗口过程再到设备上下文DC绘图的完整生命周期这是理解任何现代GUI框架如Qt、WPF的基石。所以这篇指南不是怀旧而是一份面向初学者的、硬核的Windows GUI编程入门手册。我们将手把手从零开始在VC6.0中构建一个具备基本绘图功能如画线、矩形、圆选择颜色橡皮擦的应用程序。过程中我会穿插大量现代开发中已经淡忘但至关重要的细节和“坑点”。准备好了吗让我们打开这台“时光机”回到那个纯粹的编程年代。2. 环境准备安装、配置与第一个MFC程序在开始写画图程序之前我们必须先把“战场”布置好。VC6.0在今天的Windows 10/11上直接安装和运行会遇到不少兼容性问题我们需要一些技巧来让它“服服帖帖”地工作。2.1 VC6.0的安装与兼容性设置首先你需要获取VC6.0的安装镜像或安装包。由于它早已停止支持请确保从可靠的渠道获取。安装过程本身比较简单但有几个关键点需要注意安装路径尽量避免安装在包含空格或中文的路径下例如直接安装在C:\VC98这样的目录。古老的安装程序对路径处理可能不友好。兼容模式安装完成后找到主程序MSDEV.EXE通常在安装目录的Common\MSDev98\Bin\下。右键点击其快捷方式或可执行文件选择“属性”-“兼容性”选项卡。勾选“以兼容模式运行这个程序”并选择“Windows XP (Service Pack 3)”。勾选“以管理员身份运行此程序”。这对于后续的编译、链接和调试操作至关重要否则可能因权限不足导致失败。在高DPI设置下选择“替代高DPI缩放行为”缩放执行由“系统”或“系统增强”改为“应用程序”。这能缓解界面模糊的问题。2.2 创建你的第一个MFC项目单文档应用程序启动设置好兼容性的VC6.0我们来创建项目骨架。新建项目点击菜单File-New。在Projects标签页下选择MFC AppWizard (exe)。项目设置Project name输入MyPaint。Location选择一个合适的目录。点击OK。应用类型在接下来的向导中Step 1: 选择Single document单文档。多文档MDI对于简单画图程序过于复杂。Step 2 of 6: 数据库支持选择None。Step 3 of 6: 复合文档支持选择None。Step 4 of 6: 这是关键一步。取消Docking toolbar、Initial status bar、Printing and print preview的勾选。我们的画图程序初期不需要这些保持界面干净。确保3D controls是勾选的这会让控件有立体感。Step 5 of 6: 使用默认设置。Step 6 of 6: 这里列出了AppWizard将要生成的类。重点关注CMyPaintView视图类我们大部分的绘图代码都将写在这里。CMyPaintDoc文档类未来可以用来保存图形数据。保持默认点击Finish再点击OK。现在直接按F7编译然后按CtrlF5运行。你应该能看到一个标准的Windows窗口。恭喜你的MFC程序骨架已经搭建成功这个窗口虽然现在什么也画不了但它已经包含了消息循环、窗口过程、文档/视图分离的架构。接下来我们要让它变成一个画板。注意如果编译或运行时遇到“找不到msdev.exe”或“构建工具路径错误”请检查你的VC6.0安装目录是否被添加到了系统PATH环境变量中或者尝试在VC6.0的Tools-Options-Directories中正确设置包含文件和库文件的路径。一个常见的“坑”是在64位系统上某些系统头文件路径可能不同如果遇到编译错误可以尝试在包含路径中额外添加C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include等较新的SDK路径但需谨慎可能引发兼容性问题。3. 核心原理理解Windows绘图与MFC消息映射在动手写代码前我们必须理解两个核心概念设备上下文Device Context, DC和MFC的消息映射机制。这是整个画图程序的灵魂。3.1 设备上下文DC绘图的画布与画笔你可以把DC理解为一个绘图属性的集合和一张画布的抽象。当你想要在窗口、内存或者打印机上画图时你必须先获得一个与之关联的DC。这个DC里定义了画布你要画在哪窗口客户区、内存位图、打印机。画笔Pen用来画线和形状边框的颜色、粗细、线型。画刷Brush用来填充形状内部的颜色和图案。字体Font写文字用的字体。坐标系和映射模式定义坐标原点在哪里一个逻辑单位对应多少像素。在MFC中最常用的是CDC类及其派生类如CClientDC,CWindowDC,CPaintDC。在我们的画图程序中主要使用CClientDC用于在客户区实时绘图如鼠标拖拽画线和CPaintDC用于响应WM_PAINT消息重绘持久图形。3.2 MFC消息映射让程序“活”起来Windows程序是事件消息驱动的。用户的每一次鼠标点击、移动、按键系统都会产生一个消息发送给对应的窗口。MFC通过一套宏将这些消息与类的成员函数关联起来这就是消息映射。例如当用户在视图窗口按下鼠标左键会产生WM_LBUTTONDOWN消息。我们需要在视图类CMyPaintView中处理它。操作如下在CMyPaintView类的头文件MyPaintView.h中声明消息处理函数afx_msg void OnLButtonDown(UINT nFlags, CPoint point);在实现文件MyPaintView.cpp中找到BEGIN_MESSAGE_MAP和END_MESSAGE_MAP宏在中间添加映射ON_WM_LBUTTONDOWN()。在MyPaintView.cpp中实现OnLButtonDown函数。这样当左键按下消息到来时MFC框架会自动调用我们写的OnLButtonDown函数参数point就是鼠标按下的坐标。画图程序的整个交互逻辑就是建立在处理鼠标消息LBUTTONDOWN,MOUSEMOVE,LBUTTONUP的基础上的。4. 实现基础绘图功能从鼠标事件到屏幕图形理论铺垫完毕现在开始实战。我们将实现一个最基本的自由画线功能。4.1 捕获鼠标轨迹实现自由曲线绘制自由画线的逻辑是按下鼠标左键开始画移动鼠标时在旧点和新点之间画线松开左键结束。添加成员变量在CMyPaintView类MyPaintView.h中添加私有成员变量用于记录绘图状态和上一个点。class CMyPaintView : public CView { ... private: CPoint m_ptPrev; // 记录上一个鼠标位置 BOOL m_bDrawing; // 标记是否正在绘图鼠标左键按下状态 };初始化变量在CMyPaintView的构造函数中初始化它们。CMyPaintView::CMyPaintView() { m_bDrawing FALSE; m_ptPrev CPoint(0, 0); }添加消息处理函数使用ClassWizard快捷键CtrlW是VC6.0时代最高效的方式。打开ClassWizard在Class name中选择CMyPaintView在Messages列表中找到WM_LBUTTONDOWN,WM_MOUSEMOVE,WM_LBUTTONUP分别双击或点击Add Function来添加消息处理。这会在头文件中自动添加函数声明在CPP文件中添加消息映射和空函数体。实现消息处理函数在MyPaintView.cpp中void CMyPaintView::OnLButtonDown(UINT nFlags, CPoint point) { // 按下左键开始绘图记录起点 m_bDrawing TRUE; m_ptPrev point; // 起点也是第一个“上一个点” SetCapture(); // 捕获鼠标即使移出窗口外也能收到MOUSEMOVE消息 CView::OnLButtonDown(nFlags, point); } void CMyPaintView::OnMouseMove(UINT nFlags, CPoint point) { if (m_bDrawing) // 只有在绘图状态下才画线 { CClientDC dc(this); // 获取当前窗口客户区的设备上下文 // 创建一支红色的笔PS_SOLID实线宽度1 CPen penRed(PS_SOLID, 1, RGB(255, 0, 0)); CPen* pOldPen dc.SelectObject(penRed); // 选入DC并保存旧笔 // 从上一点画线到当前点 dc.MoveTo(m_ptPrev); dc.LineTo(point); dc.SelectObject(pOldPen); // 恢复旧笔重要 // 更新“上一个点”为当前点 m_ptPrev point; } CView::OnMouseMove(nFlags, point); } void CMyPaintView::OnLButtonUp(UINT nFlags, CPoint point) { if (m_bDrawing) { m_bDrawing FALSE; ReleaseCapture(); // 释放鼠标捕获 // 最后画一条线到终点 CClientDC dc(this); CPen penRed(PS_SOLID, 1, RGB(255, 0, 0)); CPen* pOldPen dc.SelectObject(penRed); dc.MoveTo(m_ptPrev); dc.LineTo(point); dc.SelectObject(pOldPen); } CView::OnLButtonUp(nFlags, point); }编译运行现在运行程序按住鼠标左键在客户区拖动你应该能看到一条红色的线条跟随鼠标轨迹。恭喜你的第一个交互式绘图功能实现了实操心得这里有两个关键点。第一SetCapture()和ReleaseCapture()的配对使用确保了绘图过程的连贯性不会因为鼠标快速移出窗口而中断。第二GDI对象如CPen,CBrush在使用后必须恢复DC原来的对象SelectObject(pOldPen)或者用完后立即删除。否则会导致资源泄漏在长时间运行后可能出现程序崩溃或系统GDI资源耗尽。这是早期Windows编程中一个非常经典的“坑”。4.2 扩展图形类型实现矩形、椭圆与直线绘制只有自由线条太单调了。我们来增加绘制矩形、椭圆和直线的功能。这需要引入一个“绘图模式”的概念。定义绘图模式枚举和变量在CMyPaintView.h中。// 在类定义外部通常头文件顶部定义枚举 enum DrawMode { DM_FREELINE, DM_LINE, DM_RECTANGLE, DM_ELLIPSE, DM_ERASER }; class CMyPaintView : public CView { ... private: CPoint m_ptPrev; CPoint m_ptStart; // 记录图形开始的点按下时 BOOL m_bDrawing; DrawMode m_nDrawMode; // 当前绘图模式 COLORREF m_crPenColor; // 当前画笔颜色 int m_nPenWidth; // 当前画笔宽度 };初始化新变量在构造函数中。CMyPaintView::CMyPaintView() { m_bDrawing FALSE; m_nDrawMode DM_FREELINE; // 默认自由线 m_crPenColor RGB(255, 0, 0); // 默认红色 m_nPenWidth 1; }修改鼠标消息处理逻辑我们需要根据m_nDrawMode来改变绘图行为。OnLButtonDown和OnLButtonUp逻辑类似主要是记录起点和终点。关键在于OnMouseMove我们需要实现“橡皮筋”效果——在拖动鼠标时动态显示图形的预览。void CMyPaintView::OnMouseMove(UINT nFlags, CPoint point) { if (m_bDrawing) { CClientDC dc(this); // 设置画笔 CPen penCurrent(PS_SOLID, m_nPenWidth, m_crPenColor); CPen* pOldPen dc.SelectObject(penCurrent); // 关键设置R2_NOTXORPEN绘图模式用于擦除旧的预览线并画新的 int nOldMode dc.SetROP2(R2_NOTXORPEN); // 根据模式擦除上一帧的预览图形 switch (m_nDrawMode) { case DM_LINE: dc.MoveTo(m_ptStart); dc.LineTo(m_ptPrev); // 擦除旧线 dc.MoveTo(m_ptStart); dc.LineTo(point); // 画新线 break; case DM_RECTANGLE: dc.Rectangle(m_ptStart.x, m_ptStart.y, m_ptPrev.x, m_ptPrev.y); // 擦除旧矩形 dc.Rectangle(m_ptStart.x, m_ptStart.y, point.x, point.y); // 画新矩形 break; case DM_ELLIPSE: dc.Ellipse(m_ptStart.x, m_ptStart.y, m_ptPrev.x, m_ptPrev.y); // 擦除旧椭圆 dc.Ellipse(m_ptStart.x, m_ptStart.y, point.x, point.y); // 画新椭圆 break; case DM_FREELINE: // 自由线模式不需要橡皮筋直接画 dc.MoveTo(m_ptPrev); dc.LineTo(point); m_ptPrev point; // 更新上一个点 break; case DM_ERASER: { // 橡皮擦用背景色画一个实心矩形 CBrush brushBack(::GetSysColor(COLOR_WINDOW)); CBrush* pOldBrush dc.SelectObject(brushBack); dc.SetROP2(R2_COPYPEN); // 橡皮擦用直接覆盖模式 dc.Rectangle(point.x-5, point.y-5, point.x5, point.y5); // 擦除一个小区域 dc.SelectObject(pOldBrush); } break; } // 恢复原来的绘图模式和画笔 dc.SetROP2(nOldMode); dc.SelectObject(pOldPen); // 更新上一个点对于非自由线模式这个点用于下一帧擦除 if (m_nDrawMode ! DM_FREELINE m_nDrawMode ! DM_ERASER) { m_ptPrev point; } } CView::OnMouseMove(nFlags, point); }SetROP2(R2_NOTXORPEN)是实现橡皮筋效果的核心。它的原理是用同一种颜色画两次第一次会显示第二次就会擦除因为NOT XOR操作两次等于还原。这样我们就能在鼠标移动时动态更新预览图形而不会在屏幕上留下残影。在OnLButtonUp中完成最终绘制当鼠标松开时我们需要用最终的坐标和实际的画笔将图形“正式”地画到屏幕上或者更好的做法是保存图形数据到文档然后触发重绘。这里我们先简化直接画。void CMyPaintView::OnLButtonUp(UINT nFlags, CPoint point) { if (m_bDrawing) { m_bDrawing FALSE; ReleaseCapture(); CClientDC dc(this); CPen penFinal(PS_SOLID, m_nPenWidth, m_crPenColor); CPen* pOldPen dc.SelectObject(penFinal); // 根据模式绘制最终图形 switch (m_nDrawMode) { case DM_LINE: dc.MoveTo(m_ptStart); dc.LineTo(point); break; case DM_RECTANGLE: dc.Rectangle(m_ptStart.x, m_ptStart.y, point.x, point.y); break; case DM_ELLIPSE: dc.Ellipse(m_ptStart.x, m_ptStart.y, point.x, point.y); break; case DM_FREELINE: // 自由线在OnMouseMove中已经画完 break; case DM_ERASER: // 橡皮擦在OnMouseMove中已经处理 break; } dc.SelectObject(pOldPen); } CView::OnLButtonUp(nFlags, point); }4.3 添加用户界面工具栏与颜色选择现在功能有了但用户没法切换模式、选择颜色。我们需要一个简单的UI。在VC6.0中最快捷的方式是使用工具栏Toolbar和对话框Dialog。创建工具栏按钮打开资源视图ResourceView展开Toolbar文件夹双击IDR_MAINFRAME进行编辑。在工具栏末尾添加新的按钮分别画上线条、矩形、椭圆、橡皮擦的图标。双击每个新按钮设置其ID例如ID_TOOL_LINE,ID_TOOL_RECT,ID_TOOL_ELLIPSE,ID_TOOL_ERASER。为工具栏按钮添加消息处理打开ClassWizard在CMyPaintView类中Object IDs选择新加的按钮ID如ID_TOOL_LINEMessages选择COMMAND点击Add Function命名为OnToolLine。同理为其他按钮添加处理函数。实现消息处理函数在这些函数中只需修改m_nDrawMode即可。void CMyPaintView::OnToolLine() { m_nDrawMode DM_LINE; } // ... 其他模式类似实现颜色选择我们可以添加一个“颜色”按钮点击后弹出一个颜色选择对话框。首先在工具栏加一个颜色按钮ID设为ID_COLOR_CHOOSE。在CMyPaintView中为其添加COMMAND消息处理函数OnColorChoose。void CMyPaintView::OnColorChoose() { CColorDialog dlg(m_crPenColor); // 传入当前颜色作为默认值 if (dlg.DoModal() IDOK) // 弹出标准颜色对话框 { m_crPenColor dlg.GetColor(); // 获取用户选择的颜色 } }MFC的CColorDialog封装了系统的颜色选择通用对话框非常方便。更新UI状态为了让用户知道当前选中了哪种工具我们需要让工具栏按钮有“按下”的状态反馈。这需要处理UPDATE_COMMAND_UI消息。在ClassWizard中同样选择按钮ID和UPDATE_COMMAND_UI消息添加处理函数。void CMyPaintView::OnUpdateToolLine(CCmdUI* pCmdUI) { // 如果当前模式是画线则设置按钮为按下状态 pCmdUI-SetCheck(m_nDrawMode DM_LINE); } // ... 其他模式类似至此一个具备基本绘图功能自由线、直线、矩形、椭圆、橡皮擦和简单UI工具栏切换、颜色选择的VC6.0画图程序就完成了。运行它你已经可以像使用系统画图板一样进行简单的创作了。5. 进阶与优化让程序更健壮、功能更完整基础功能跑通了但作为一个完整的程序还差得远。下面我们来解决几个关键问题并添加一些实用功能。5.1 解决图形刷新问题重绘与文档数据存储你现在可能会发现一个问题当你画了一些图形后最小化窗口再恢复或者用其他窗口挡住画图区域再移开之前画的图形都消失了这是因为我们没有处理WM_PAINT消息。当窗口需要重绘时系统会发送这个消息我们需要在这个消息处理中把所有的图形数据重新画一遍。这意味着我们不能只在鼠标事件中把图形画到屏幕上就完事必须将图形数据保存下来。这正是MFC文档/视图架构的优势所在文档类CMyPaintDoc负责存储数据视图类CMyPaintView负责显示和交互。定义图形基类和数据存储在文档类头文件MyPaintDoc.h中定义一个图形基类或结构体以及一个对象列表如CPtrArray或CList。// 图形类型 enum ShapeType { ST_LINE, ST_RECTANGLE, ST_ELLIPSE, ST_FREELINE }; // 图形基类简化版实际应用可能需要更复杂的继承体系 class CGraphObject { public: ShapeType m_type; COLORREF m_color; int m_width; CPoint m_ptStart; CPoint m_ptEnd; // 对于自由线需要存储一系列点这里用CPoint数组简化表示 // 实际可以用CPtrArray存储多个CPoint CArrayCPoint, CPoint m_points; // 用于自由线 virtual void Draw(CDC* pDC) 0; // 纯虚函数子类实现绘制 virtual ~CGraphObject() {} }; class CMyPaintDoc : public CDocument { ... public: CTypedPtrArrayCObArray, CGraphObject* m_obArray; // 存储所有图形对象 // 添加一个函数来添加新图形 void AddGraphObject(CGraphObject* pObj); ... };在文档类CPP文件中实现AddGraphObject并记得在析构函数中清理数组内存。实现具体的图形类为每种图形创建派生类如CLineObject,CRectObject并实现其Draw方法。这涉及到序列化保存/加载等更高级的主题为了简化我们可以先用一个简单的结构体数组来存储图形数据。但为了教学完整性这里指出方向真正的工业级程序必须将数据与显示分离。修改绘图逻辑在OnLButtonUp中不再直接画到DC而是根据当前模式和数据创建一个CGraphObject派生类的实例设置好属性起点、终点、颜色等然后调用文档的AddGraphObject方法将其加入列表。实现视图的OnDraw函数视图类有一个虚函数OnDraw(CDC* pDC)。当窗口需要重绘时框架会调用它。我们需要在这里遍历文档中的图形列表调用每个图形的Draw(pDC)方法。void CMyPaintView::OnDraw(CDC* pDC) { CMyPaintDoc* pDoc GetDocument(); ASSERT_VALID(pDoc); // 遍历文档中的所有图形对象并绘制 for (int i 0; i pDoc-m_obArray.GetSize(); i) { CGraphObject* pObj pDoc-m_obArray.GetAt(i); if (pObj) pObj-Draw(pDC); } }通知视图更新在文档数据改变添加了新图形后需要通知所有关联的视图更新显示。可以在AddGraphObject函数末尾调用UpdateAllViews(NULL)。完成以上步骤后你的图形就能持久化了。这是一个从“玩具程序”迈向“真正应用程序”的关键一步。5.2 添加线宽选择与更多绘图工具线宽选择可以通过一个自定义对话框或者工具栏上的组合框来实现。这里以对话框为例插入对话框资源在资源视图中右键Dialog选择Insert Dialog。添加一个静态文本“线宽”和一个编辑框ID设为IDC_EDIT_WIDTH。再添加OK和Cancel按钮。创建对话框类双击对话框MFC会提示创建新类命名为CWidthDlg。为编辑框添加成员变量在ClassWizard中为IDC_EDIT_WIDTH添加一个int类型的成员变量m_nWidth并设置取值范围如1-20。添加工具栏按钮和消息处理在工具栏添加一个“线宽”按钮在其命令处理函数中弹出对话框并更新m_nPenWidth。void CMyPaintView::OnToolWidth() { CWidthDlg dlg; dlg.m_nWidth m_nPenWidth; // 传入当前值 if (dlg.DoModal() IDOK) { m_nPenWidth dlg.m_nWidth; } }你还可以依葫芦画瓢添加更多工具比如填充矩形/椭圆使用CBrush、文本工具使用CDC::TextOut、选择工具实现图形的选中、移动、删除等。每增加一个工具都是对鼠标消息处理、图形数据结构和绘制逻辑的一次深入练习。5.3 常见问题排查与调试技巧在VC6.0开发中你肯定会遇到各种编译、链接和运行时错误。以下是一些经典问题的排查思路“LNK2001: unresolved external symbol” 链接错误最常见原因在CPP文件中实现了函数但在头文件中没有声明或者声明了但忘记在类中afx_msg消息函数或类外全局函数添加。检查消息映射确认消息处理函数在头文件声明为afx_msg在CPP文件中有对应的ON_XXX消息映射宏。检查库文件如果你使用了某些特定的MFC类或API可能需要链接额外的库。在Project-Settings-Link标签页的Object/library modules中添加例如winmm.lib多媒体相关。程序运行时崩溃特别是在操作GDI对象后GDI资源泄漏这是VC6.0/MFC编程的头号杀手。确保每一个通过new或CreateXXX创建的GDI对象CPen,CBrush,CFont,CBitmap都有对应的DeleteObject()。MFC的封装类在析构时会自动调用但如果你在堆上分配了对象指针必须手动删除。野指针或数组越界在OnDraw中遍历数组时确保索引有效。使用GetSize()而不是硬编码数字。使用未初始化的变量确保像m_bDrawing这样的状态变量在构造函数中被正确初始化。程序界面闪烁严重原因在OnMouseMove中频繁且大面积地直接绘制到窗口DC会引发连续的WM_PAINT消息导致闪烁。解决方案使用双缓冲技术。原理是先在内存DC一个CBitmap上绘制所有图形然后一次性将内存位图复制到屏幕DC。这能极大提升绘制效率消除闪烁。这是图形编程中的一个高级但必备的技巧。如何在现代系统上调试VC6.0程序VC6.0自带的调试器功能较弱。一个实用的技巧是使用OutputDebugString函数输出调试信息然后在现代系统上使用DebugViewSysinternals工具集里的来捕获这些输出。这比古老的TRACE宏更方便。对于复杂的内存问题可以尝试使用Visual Studio 较新版本如VS2010以上打开VC6.0的.dsp项目文件可能需要转换利用其更强大的调试器和诊断工具。但要注意代码兼容性。6. 项目总结与延伸思考通过这个VC6.0画图程序的实现我们完整地走了一遍经典的Windows GUI应用开发流程从环境搭建、项目创建到理解消息循环和DC再到实现具体的交互逻辑和图形绘制最后考虑数据持久化和程序优化。这个过程虽然基于一个“古老”的工具但所涉及的核心概念——事件驱动、资源管理、图形设备接口、文档/视图模型——至今仍然是GUI编程的基石。我个人在带新人时常常建议他们用类似的方式入门。在VC6.0这个“简单”的环境里你能清晰地看到每一个齿轮是如何咬合的没有现代框架那些复杂的抽象和“魔法”。当你被Qt的信号槽、WPF的数据绑定弄得晕头转向时回头看看最原始的WM_LBUTTONDOWN和LineTo往往能豁然开朗。当然这个程序还有很多可以完善的地方实现撤销/重做需要命令模式、支持文件保存/打开序列化、更复杂的图形编辑、甚至是一个简单的插件系统。每一个扩展都是对MFC和Windows编程更深层次的理解。最后关于网络热词中提到的“VC运行库”问题这正是从VC6.0时代遗留下来的“DLL地狱”的缩影。你的程序如果要在没有安装VC6.0的电脑上运行必须携带对应的MSVCRT.DLL等运行时库。现代Visual Studio通过“可再发行组件包”vcredist来解决这个问题这也是为什么你经常需要安装那些运行库。理解这一点也就理解了Windows软件部署的一部分历史。希望这份指南不仅能帮你实现一个画图程序更能打开一扇通往Windows桌面开发深处的大门。编程的乐趣往往就藏在这些看似过时却充满智慧的细节之中。