
简介这份PDF面向具备一定Visual C基础的中级开发者系统讲解如何借助MFC的文档视图框架实现打印与打印预览功能。内容围绕OnPreparePrinting、OnBeginPrinting、OnPrepareDC、OnPrint、OnEndPrinting等关键环节展开并进一步探讨在对话框上通过继承CDialog完成打印的框架结构还涉及自定义打印命令ID、消息映射替换等源码级技巧帮助读者理解MFC打印机制而无需深究底层GDI细节。资源包共1个文件为PDF格式大小约900KB便于随时查阅与对照实践。目前已有145人学习适合需要在Windows应用中快速定制打印格式、复用文档视图打印框架的开发者参考。1. 从一份 2008 年的 MFC 打印文档说起为什么今天还有人翻它翻到这份《使用MFC编写打印程序》的 PDF 时我第一反应是这玩意儿还有人用但仔细读完发现它解决的是一个至今没被淘汰的问题在对话框程序里做打印和打印预览。现在桌面软件开发用 MFC 还是 Qt 的争论一直没停但如果你手上维护的是一个跑了十几年的老工程或者接到的需求就是给某个工控上位机加一个打印报表按钮那 MFC 这套东西你绕不开。这份文档的价值在于它没有停留在文档视图结构怎么打印这种教科书内容上而是给出了一个可复用的 CPrintFrame CPrintView 方案让对话框程序也能用上 CView 里那套成熟的打印和预览机制。适合谁看适合已经会用 MFC 画界面、但对打印流程一知半解的中级开发者尤其是那些被打印预览按钮点了没反应折磨过的人。文档附带源码核心就是两个类四个文件拿来就能往工程里塞。2. 文档视图结构下的打印流程从 OnFilePrint 源码拆到三个必须重载的函数2.1 打印本质是往打印机 DC 上做 GDI 绘图很多人觉得打印是个黑匣子其实剥开看就一句话打印程序的编写本质上是一种 GDI 绘图只是绘图的对象从屏幕的设备描述表换成了打印机的设备描述表。你在 OnDraw 里怎么画曲线、怎么填表格在 OnPrint 里就能用同样的逻辑画到纸上。区别在于打印机 DC 的坐标系、分辨率、可绘制区域和屏幕不一样所以需要根据 CPrintInfo 里的信息来调整。MFC 的文档视图框架之所以适合做打印是因为它已经把打印流程封装好了。CView 类里有一个 OnFilePrint 函数它不是虚函数而是响应默认 COMMAND 消息的处理函数。MFC 向导生成的菜单和工具栏里打印命令的 ID 是 ID_FILE_PRINT视图类的 MessageMap 里有一项 ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)。这意味着你可以不用原来的 ID换成自己的 ID_MYPRINT再在 MessageMap 里加一行 ON_COMMAND(ID_MYPRINT, CView::OnFilePrint)功能完全一样。这个技巧在需要自定义打印入口时很实用。2.2 OnFilePrint 内部调了哪几个函数直接看 MFC 源码里 CView::OnFilePrint 的实现逻辑它按顺序做了这几件事// MFC 源码 ViewPrnt.cpp 中 CView::OnFilePrint 的简化流程 void CView::OnFilePrint() { // 1. 准备打印信息弹出打印对话框 if (OnPreparePrinting(printInfo)) { if (dlg.DoModal() ! IDOK) return; } // 2. 打印开始前的初始化 OnBeginPrinting(dcPrint, printInfo); // 3. 准备设备上下文 OnPrepareDC(dcPrint, printInfo); // 4. 实际绘制每一页内容 OnPrint(dcPrint, printInfo); // 5. 打印结束后的清理 OnEndPrinting(dcPrint, printInfo); }这段代码里OnPreparePrinting 必须有 return DoPreparePrinting(pInfo)这是打印过程中最先调用的负责弹出打印机选择对话框并获取默认打印信息。真正需要重载的是三个函数OnBeginPrinting、OnPrint、OnEndPrinting。其中 OnPrint 最复杂是写大量打印代码的地方。OnBeginPrinting 通常用来设定要打印的总页数。默认情况下 MFC 认为只有一页如果你要打印的内容超过一页必须在这里调用 pInfo-SetMaxPage(n) 设定页数否则后面的页根本不会触发 OnPrint。这个坑我踩过当时打印一个多页报表死活只出一页查了半天才发现是没设页数。OnPrint 的第二个参数是 CPrintInfo 指针从中可以拿到当前页码 m_nCurPage、总页数、可绘制矩形 m_rectDraw 等信息。页眉页脚就靠这些信息来定位。OnEndPrinting 用来释放在 OnBeginPrinting 里申请的资源没申请就不用重载。2.3 打印预览怎么和打印共用一套绘制代码打印预览的实现思路是在内存 DC 上模拟打印过程。关键操作是在视图类的 MessageMap 里加一行// 将打印预览命令关联到 CView 的默认实现 ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview)只要重载了 OnPrint 函数打印和打印预览就能共用同一套绘制逻辑。MFC 在预览时会调用 OnPrint只是传入的 DC 是内存 DC 而非打印机 DC。这就是为什么文档里说只要重载了 OnPrint在打印和打印预览中就可以重用该函数。实际开发中我一般会在 OnPrint 里根据 pInfo-m_bPreview 判断当前是预览还是实际打印对某些只在纸上显示的内容做差异化处理。3. 对话框程序里没有文档视图怎么办虚拟框架 消息回传的完整实现3.1 为什么要建一个看不见的框架窗口对话框程序没有文档视图结构但 MFC 的打印功能是 CView 提供的而 CView 对象必须依附于某个框架窗口。解决办法是建立一个虚拟的框架窗口和视图这个窗口不需要显示只用来承载打印流程。文档里建立了两个类CPrintFrame 和 CPrintView。CPrintFrame 继承自 CFrameWndCPrintView 继承自 CView。创建时把对话框指针传给框架窗口这样框架窗口在收到 WM_BEGIN_PRINTING 和 WM_END_PRINTING 消息时可以通知对话框完成打印的初始化和释放。这个设计的巧妙之处在于打印流程交给 CView 走但具体画什么内容又通过消息传回对话框。CPrintView::OnPrint 里不直接绘制而是把 pDC 和 pInfo 通过 SendMessage 发给对话框// CPrintView::OnPrint 中将绘制任务回传给对话框 void CPrintView::OnPrint(CDC* pDC, CPrintInfo* pInfo) { // 将设备上下文和打印信息打包发给调用者对话框 pFrame-m_pCallerDlg-SendMessage(WM_MY_PRINT, (WPARAM)pDC, (LPARAM)pInfo); }对话框收到 WM_MY_PRINT 后在响应函数里写真正的绘制代码。这样开发人员只需要关注画什么不用管怎么调打印流程。3.2 对话框端要做的五步操作在对话框程序里使用这套方案步骤很固定。第一步建立 CPrintFrame 对象并把对话框指针传进去// 在对话框的打印按钮响应函数中 void CDlgPrintDlg::OnPrint() { CPrintFrame* pFrame new CPrintFrame; pFrame-m_pCallerDlg this; // 回传对话框指针 // 创建隐藏的框架窗口尺寸无所谓 pFrame-Create(NULL, _T(CurvePrint), WS_OVERLAPPEDWINDOW, CRect(0, 0, 0, 0)); // 触发打印 pFrame-m_pView-OnMyPrint(); }第二步调用 Create 创建框架窗口。注意这里传的矩形是 CRect(0,0,0,0)窗口不可见只是用来承载视图。第三步执行打印调 OnMyPrint()执行预览调 OnMyPrintPreview()。第四步在对话框的消息映射里加三个消息响应// 对话框消息映射中注册打印相关消息 ON_MESSAGE(WM_BEGIN_PRINTING, OnBeginPrinting) ON_MESSAGE(WM_END_PRINTING, OnEndPrinting) ON_MESSAGE(WM_MY_PRINT, OnMyPrint)第五步在 OnMyPrint 里写实际绘制代码。这个函数收到的参数就是 CDC* 和 CPrintInfo*可以打印表格、图片、数据只要 GDI 绘图能做的操作在这里都能完成。打印预览不需要另外写代码因为预览的一部分工作在 CView 类里已经完成了对话框只需要响应 WM_MY_PRINT 就能同时支持打印和预览。3.3 可复用类的文件组织与集成方式文档提供的 CPrintFrame 和 CPrintView 是两个可重用的类对应四个文件PrintFrame.h、PrintView.h、PrintFrame.cpp、PrintView.cpp。集成到现有工程时把这四个文件拷贝到工程目录在工程里添加进去然后在需要打印的代码处加上// 在对话框实现文件中引入打印类头文件 #include PrintFrame.h #include PrintView.h之后按上面五步操作就能实现完整的打印和预览功能。我实测下来这套方案确实省时间尤其是打印预览部分不用自己从头写预览窗口和翻页逻辑。但有一个硬性限制必须使用 MFC 动态链接库Use MFC in a Shared DLL并且程序必须是 Debug 版本。如果用了静态链接的 MFC 库运行时会出错。这个限制在文档里明确写了但很多人集成时容易忽略。4. 避坑与排查静态链接崩溃、预览按钮失效、多页打印只出一页4.1 静态链接 MFC 时预览崩溃现象程序在 Debug 动态链接下运行正常改成 Release 静态链接后一按打印预览就崩溃。原因MFC 的打印预览功能依赖一些内置资源比如 AFX_IDS_PREVIEWPAGEDESC 这个字符串资源。静态链接时包含这些资源的 afxprint.rc 没有被自动包含进来导致 LoadString 加载资源失败程序在 VERIFY 处崩溃。解决在工程的 .rc 文件最后一行添加 #include l.chs\afxprint.rc路径根据实际语言版本调整把打印相关的资源文件手动包含进来。这个问题在文档的评论里有多人反馈算是一个经典坑。4.2 预览界面上的打印按钮点了没反应现象打印预览窗口工具栏上的打印按钮是灰的或者点了之后直接把预览窗口关掉并不弹出打印机选择对话框。原因预览窗口的打印命令 ID 是 AFX_ID_PREVIEW_PRINT默认没有对应的处理函数。解决有两种方式。第一种在对话框里添加对 ID_FILE_PRINT 命令的响应// 在对话框消息映射中捕获打印命令 ON_COMMAND(ID_FILE_PRINT, OnFilePrint) void CDlgPrintDlg::OnFilePrint() { Print(); // 调用实际的打印逻辑 }第二种从 CPreviewView 派生一个自己的预览视图类重载 OnPreviewPrint 处理 AFX_ID_PREVIEW_PRINT 命令。第一种方式改动小适合快速修复第二种方式更灵活适合需要自定义预览工具栏行为的场景。4.3 多页内容只打印出第一页现象OnPrint 里明明写了根据页码绘制不同内容的逻辑但实际打印时只输出第一页后面的页没有。原因OnBeginPrinting 里没有设定总页数。MFC 默认认为打印任务只有一页所以 m_nCurPage 永远是 1不会触发后续页的 OnPrint 调用。解决在 OnBeginPrinting 里根据数据量计算页数并设定void CDlgPrintDlg::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo) { // 假设每页打印 20 行数据 int nTotalLines m_arrData.GetSize(); int nPages (nTotalLines 19) / 20; pInfo-SetMaxPage(nPages); // 必须设定否则只打印一页 }4.4 预览时弹出多个对话框导致崩溃现象连续点击打印预览按钮会弹出多个预览窗口关掉其中任意一个就报错。原因每次点击都 new 了一个 CPrintFrame 对象但没有在预览结束后释放。多个框架窗口共享同一个对话框指针关闭时消息回传出现混乱。解决在对话框里用一个成员变量记录当前的 CPrintFrame 指针如果已经存在就先关闭再新建或者在 OnEndPrinting 里 delete 掉框架对象。我一般会在对话框的 OnDestroy 里统一清理避免内存泄漏。5. 进阶技巧用 CPrintInfo 做页眉页脚和打印范围控制5.1 从 CPrintInfo 里能拿到哪些可用信息CPrintInfo 结构体里除了 m_nCurPage还有几个字段在实际开发中很有用。m_rectDraw 定义了当前页的可绘制矩形区域页眉页脚就画在这个矩形之外。m_strPageDesc 是页码显示格式字符串默认是第 %d 页。m_bPreview 标识当前是否处于预览模式。m_nNumPreviewPages 控制预览时同时显示几页文档里的示例支持两页同时预览就是设置了这个值。// 在 OnPrint 中利用 CPrintInfo 信息绘制页眉 void CDlgPrintDlg::OnMyPrint(CDC* pDC, CPrintInfo* pInfo) { // 获取可绘制区域 CRect rectPage pInfo-m_rectDraw; // 在页面顶部绘制页眉 pDC-TextOut(rectPage.left, rectPage.top - 200, _T(报表打印 - 第) CString(pInfo-m_nCurPage) _T(页)); // 根据当前页码决定绘制哪部分数据 int nStartLine (pInfo-m_nCurPage - 1) * 20; DrawDataRows(pDC, nStartLine, 20); }5.2 打印范围控制的实现方式MFC 的打印对话框默认允许用户选择全部打印或打印指定页范围。如果用户选了范围CPrintInfo 的 GetFromPage() 和 GetToPage() 会返回对应的页码。在 OnPrint 里需要根据这个范围来判断当前页是否应该输出// 在 OnPrint 中判断当前页是否在用户选择的范围内 void CDlgPrintDlg::OnMyPrint(CDC* pDC, CPrintInfo* pInfo) { UINT nFrom pInfo-GetFromPage(); UINT nTo pInfo-GetToPage(); UINT nCur pInfo-m_nCurPage; // 不在打印范围内则跳过 if (nCur nFrom || nCur nTo) { pInfo-m_bContinuePrinting FALSE; return; } // 正常绘制当前页内容 DrawPageContent(pDC, nCur); }这里有个细节如果当前页超出范围要把 m_bContinuePrinting 设为 FALSE 来提前结束打印任务否则 MFC 会继续调用 OnPrint 直到达到最大页数。5.3 一个我常用的调试习惯打印程序最麻烦的地方在于调试。屏幕上看着好好的打出来可能偏了、缺了、或者比例不对。我的习惯是在 OnPrint 开头加一段条件编译的调试代码把当前页码、可绘制区域尺寸、DC 的分辨率输出到调试窗口// 调试打印参数仅在 Debug 版本生效 #ifdef _DEBUG TRACE(_T(Print Page: %d, Rect: %d,%d,%d,%d\n), pInfo-m_nCurPage, pInfo-m_rectDraw.left, pInfo-m_rectDraw.top, pInfo-m_rectDraw.right, pInfo-m_rectDraw.bottom); TRACE(_T(DC DPI: %d x %d\n), pDC-GetDeviceCaps(LOGPIXELSX), pDC-GetDeviceCaps(LOGPIXELSY)); #endif从那以后我每次集成新的打印模块都强制先在 OnBeginPrinting 里把页数设对再在 OnPrint 里用 TRACE 确认每一页的参数最后才去调格式。这套流程帮我省了很多返工的时间。希望帮到你。本文还有配套的精品资源点击获取