
简介这份资源面向具备一定C与Windows开发基础的MFC学习者聚焦在MFC应用中实现打印与打印预览这一常见但细节较多的功能点。内容围绕CView派生类的打印机制展开涵盖打印机设备上下文创建、StartDoc/StartPage/EndDoc打印作业流程、OnDraw适配打印机DC、CPrintDialog打印对话框、CPreviewDC打印预览以及分页处理、打印选项扩展、错误恢复与性能优化等实践要点帮助读者理解从屏幕绘制到打印输出的完整链路。资源包共50个文件以h头文件、cpp源文件、rc资源脚本、vcxproj工程文件为主另含exe可执行程序、pdb调试符号、manifest清单及tlog编译日志等压缩包约22.82MB工程结构完整可直接编译运行。目前已有346人学习下载适合需要为MFC项目补充打印模块或对照示例排查打印问题的开发者参考。1. 从一张报表说起MFC打印功能到底能解决什么上周帮朋友看一个老项目需求很朴素把界面上那个 CListCtrl 里的数据打出来。他折腾了一下午要么打印出来是空白要么直接弹异常。问题不在数据在于 MFC 的打印链路和屏幕绘制完全是两套逻辑——屏幕上有 DC 就能画打印机得先拿到打印 DC、再走文档/视图的 OnPrint 流程。这份资源就是围绕这个场景来的在 MFC 单文档或多文档程序里把打印、打印预览、页面设置这一整套跑通。适合谁手头维护着 MFC 老系统、需要补打印模块的开发者或者刚接触 GDI 绘图、想搞明白CDC和OnDraw/OnPrint分工的人。它不依赖第三方库纯 Win32 MFC 自带机制编译出来就能用。2. 打印链路拆解从菜单命令到纸面输出的完整路径MFC 的打印不是调一个Print()就完事它是一套框架级的消息流转。理解这条链路后面改代码才不会瞎猜。2.1 框架默认帮你做了什么在单文档程序里CView派生类天生就带打印能力。你打开资源编辑器ID 为ID_FILE_PRINT、ID_FILE_PRINT_PREVIEW、ID_FILE_PRINT_SETUP这几个菜单项MFC 的CView::OnFilePrint已经接好了。整个流程大致是用户点“打印”触发ID_FILE_PRINT命令。CView::OnFilePrint被调用它内部创建CPrintInfo结构弹出打印对话框。框架调用OnBeginPrinting给你机会创建打印专用的 GDI 对象比如字体、画笔。对每一页框架调用OnPrepareDC调整 DC 的视口原点然后调用OnPrint。OnPrint默认会转调OnDraw这就是为什么很多人只写OnDraw也能打印——但只能打当前屏幕那一页。全部页打完调用OnEndPrinting释放资源。关键点OnDraw收到的 DC 在打印时是打印机 DC不是屏幕 DC。如果你在OnDraw里写了GetClientRect拿客户区大小打印时这个值会变成打印机可打印区域逻辑就乱了。2.2 分页的核心OnPrepareDC 与 CPrintInfo要打多页必须自己控制分页。MFC 用CPrintInfo的m_nCurPage和m_nNumPreviewPages来跟踪。每页打印前框架会调用OnPrepareDC你需要在这里根据当前页码设置映射模式。void CMyView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo) { CView::OnPrepareDC(pDC, pInfo); if (pInfo ! nullptr) // 打印或预览时才处理 { // 设置映射模式为 MM_TEXT1 逻辑单位 1 像素 pDC-SetMapMode(MM_TEXT); // 根据当前页码把原点往上移实现“翻页” // 每页高度按可打印区域算 int nPageHeight pDC-GetDeviceCaps(VERTRES); pDC-SetViewportOrg(0, -(pInfo-m_nCurPage - 1) * nPageHeight); } }逻辑说明SetViewportOrg把逻辑坐标原点沿 Y 轴负方向偏移第 1 页偏移 0第 2 页偏移一个页高这样OnDraw里画的内容就像一条长卷每页露出不同的一段。参数VERTRES是打印机垂直方向的可打印像素数不同打印机不一样所以不能写死。2.3 OnPrint 里该写什么OnPrint是真正决定“这一页画什么”的地方。默认实现是调OnDraw但多页打印时你得自己接管。void CMyView::OnPrint(CDC* pDC, CPrintInfo* pInfo) { // 先画页眉 CFont fontHeader; fontHeader.CreatePointFont(100, _T(宋体)); CFont* pOldFont pDC-SelectObject(fontHeader); pDC-TextOut(0, 0, _T(数据报表)); // 画表格线 int nStartY 200; // 给页眉留出空间 int nRowHeight 300; for (int i 0; i m_arrData.GetSize(); i) { int y nStartY i * nRowHeight; pDC-Rectangle(0, y, 8000, y nRowHeight); pDC-TextOut(100, y 50, m_arrData[i]); } pDC-SelectObject(pOldFont); }逻辑说明这里没有调CView::OnPrint因为我们要完全控制绘制内容。m_arrData是视图类里存数据的CStringArray实际项目里换成你的数据源即可。注意Rectangle的坐标用的是逻辑单位在MM_TEXT下就是像素但打印机分辨率通常比屏幕高得多所以数值要放大——8000 这个宽度在 600 DPI 打印机上大约对应 1.3 英寸具体得按你的纸张调。2.4 打印预览为什么容易翻车打印预览走的是同一套OnPrint但 DC 是屏幕 DC尺寸按预览窗口缩放。很多人预览正常、真打印就错位原因是OnPrepareDC里用了GetDeviceCaps拿打印机参数预览时拿到的是屏幕参数。稳妥做法是在OnBeginPrinting里根据pInfo-m_pPD-GetDeviceName()判断或者干脆用pInfo-m_rectDraw这个框架算好的可打印矩形。void CMyView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo) { // 用框架算好的可打印区域避免自己算错 m_rectPrint pInfo-m_rectDraw; // 创建打印专用字体只创建一次所有页复用 m_pPrintFont new CFont; m_pPrintFont-CreatePointFont(100, _T(宋体)); }m_rectDraw是CRect类型单位是逻辑单位已经考虑了打印机边距。用它来布局预览和实际打印才能一致。3. 动手跑起来从空项目到能出纸的最小闭环这一章按步骤走目标是让你在半小时内看到一个能打印的窗口。3.1 创建项目与菜单资源用 Visual Studio 新建一个 MFC 单文档项目项目名随意。在资源视图里打开IDR_MAINFRAME菜单确认“文件”菜单下有“打印”“打印预览”“页面设置”三项ID 分别是ID_FILE_PRINT、ID_FILE_PRINT_PREVIEW、ID_FILE_PRINT_SETUP。如果没有手动加上ID 必须用这三个否则框架的消息映射接不上。3.2 在视图类里加数据成员打开你的View.h在类声明里加protected: CStringArray m_arrData; // 模拟要打印的数据 CRect m_rectPrint; // 可打印区域 CFont* m_pPrintFont; // 打印字体然后在构造函数里初始化m_pPrintFont nullptr;在析构里delete m_pPrintFont;。数据可以在OnInitialUpdate里塞几条进去void CMyView::OnInitialUpdate() { CView::OnInitialUpdate(); m_arrData.Add(_T(第一行数据)); m_arrData.Add(_T(第二行数据)); m_arrData.Add(_T(第三行数据)); }3.3 重写三个关键虚函数在类向导里添加OnBeginPrinting、OnPrepareDC、OnPrint三个重写。代码按第 2 章的示例填。注意OnPrepareDC里要调基类OnPrint里不调基类因为我们要自己画。3.4 编译运行与验证按 F5 运行点“打印预览”。如果预览窗口里能看到三行数据说明链路通了。再点“打印”选一个虚拟打印机比如系统自带的“Microsoft Print to PDF”输出成 PDF 检查。常见失败预览空白——检查OnPrint是否被调用可以在里面加TRACE输出打印错位——检查OnPrepareDC里SetViewportOrg的偏移量是否用了VERTRES。提示调试打印时用“Microsoft Print to PDF”比真打印机方便输出文件可以直接打开看效果不用浪费纸。4. 避坑与排查打印模块最常见的五个翻车现场4.1 预览正常打印出来只有第一页现象预览能翻页真打印只出一页。 原因OnPrepareDC里没有根据m_nCurPage调整原点所有页画在了同一位置。 解决确认SetViewportOrg的偏移量计算里用了pInfo-m_nCurPage并且OnPrint里没有提前return。4.2 打印内容被截断或缩在角落现象纸上只有左上角一小块有内容。 原因映射模式没设对或者用了屏幕坐标硬编码。打印机 DPI 通常是 600 以上屏幕才 96同样的数值在纸上会小很多。 解决统一用MM_TEXT并在OnBeginPrinting里根据m_rectDraw动态计算布局不要写死像素值。4.3 打印对话框弹不出来现象点“打印”没反应。 原因菜单 ID 不是ID_FILE_PRINT或者视图类没有正确继承CView的打印消息映射。 解决检查BEGIN_MESSAGE_MAP里是否有ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)没有就手动加。4.4 内存泄漏与 GDI 对象耗尽现象连续打印多次后程序变慢或崩溃。 原因OnBeginPrinting里new的字体没在OnEndPrinting里delete或者SelectObject后没恢复旧对象。 解决成对写OnBeginPrinting/OnEndPrintingSelectObject之后务必SelectObject(pOld)恢复。4.5 中文乱码或字体不对现象打印出来中文变成问号。 原因CreatePointFont里字体名用了_T(宋体)但系统没有或者项目字符集不匹配。 解决确认项目属性里字符集是“使用 Unicode 字符集”字体名用_T(SimSun)或_T(宋体)都行但要在目标机器上存在。5. 二次开发改造把打印模块抽成可复用组件跑通最小闭环之后真正有价值的是把这套东西抽出来让多个视图共用。我一般会做一个CPrintHelper类把分页计算、页眉页脚、表格绘制这些逻辑收进去。5.1 设计一个打印辅助类class CPrintHelper { public: CPrintHelper(); ~CPrintHelper(); // 设置数据源用回调方式让调用者决定怎么画 void SetDataProvider(std::functionvoid(CDC*, const CRect, int) painter); // 在 OnPrint 里调这个 void DrawPage(CDC* pDC, CPrintInfo* pInfo, const CRect rectDraw); // 在 OnPrepareDC 里调这个 void PrepareDC(CDC* pDC, CPrintInfo* pInfo); private: std::functionvoid(CDC*, const CRect, int) m_painter; int m_nPageHeight; };SetDataProvider接收一个 lambda调用者只需要关心“第 n 页在 rectDraw 里画什么”分页偏移和 DC 准备由 helper 统一处理。这样换一个视图只要重新传一个 lambda 就行。5.2 验证改造效果改造完之后用两个不同的视图分别调CPrintHelper一个打表格一个打文本。如果两者都能正常预览和打印说明抽象是成功的。验证时重点看页码是否正确递增、最后一页是否有多余空白页、预览缩放比例是否一致。5.3 一个容易忽略的细节OnPrint里拿到的pInfo-m_rectDraw在预览和打印时单位可能不同。稳妥做法是在OnBeginPrinting里把它存下来并且用pDC-DPtoLP转成逻辑单位再用。我吃过这个亏预览时看着刚好打印出来右边少一截查了半天才发现是单位没统一。从那以后我每次写打印模块都强制在OnBeginPrinting里把m_rectDraw转成逻辑单位存成成员变量所有绘制都用这个变量不再直接碰pInfo。希望帮到你。本文还有配套的精品资源点击获取