简介:本资源是一份面向Windows桌面应用开发者的MFC打印编程深度指南,专为具备C++基础、熟悉GDI绘图的中级开发者设计,解决在实际项目中快速集成专业级打印与打印预览功能的核心难题。文档系统剖析MFC文档/视图架构下的打印全流程——从OnPreparePrinting初始化、OnBeginPrinting设备上下文准备,到核心OnPrint内容绘制及OnEndPrinting资源清理,并延伸讲解对话框中脱离文档视图结构实现自定义打印的完整框架,含ID_MYPRINT命令映射、内存DC预览机制等实战要点。资源为单文件PDF,大小900KB,内容精炼但覆盖全面,含源码级函数调用链分析、CPrintInfo结构详解及页眉页脚、多页控制等关键场景说明。目前已有145人学习下载,读者可直接复用文中提供的打印框架结构,在各类MFC项目中按需定制报表、日志、图表等多样化打印输出。
1. MFC 打印不是“调个 API 就完事”:对话框里硬刚打印预览的血泪实战笔记
你有没有试过在 MFC 对话框程序里加个「打印」按钮,点下去却弹出报错、预览窗口一按「打印」就闪退、或者干脆连页眉都对不齐?这不是你代码写得差——是 MFC 的打印机制在底层悄悄设了三道关卡:文档视图强耦合、资源 ID 静态链接丢失、预览消息路由断裂。这篇笔记不讲“MFC 是什么”,只拆解一份真实落地的《使用 MFC 编写打印程序.pdf》里埋着的硬核方案:它用两个可复用类(CPrintFrame+CPrintView)绕开文档视图框架,在纯对话框中复现完整打印流——支持页码跳转、双页预览、自定义页边距,甚至能让你在OnMyPrint里用 GDI 画表格、贴位图、套用字体。它适合正在维护老项目、接手遗留 VC6/VS2005 工程、或需要快速交付专业级打印功能的中级开发者。如果你正被VERIFY(pInfo->m_strPageDesc.LoadString(AFX_IDS_PREVIEWPAGEDESC))崩溃卡住,或发现静态链接 MFC 时预览按钮变灰,那这篇就是为你写的“后悔药”。
2. 文档视图结构下的标准打印流程:从OnFilePrint到OnPrint的四层调用链
MFC 的打印不是黑匣子,它的骨架藏在CView::OnFilePrint的源码里。理解这串调用链,是后续在对话框中“借壳打印”的前提。我们不抄书,直接切到关键路径——以 VS2005 的 MFC 源码为基准(路径通常为VC\atlmfc\src\mfc\ViewPrnt.cpp),逐层拆解每个函数的职责、必须重载点、以及参数陷阱。
2.1OnPreparePrinting:打印前的“安检门”,90% 的人漏掉这一步
这个函数是整个打印流程的入口守门员。它表面看只是调用DoPreparePrinting(&printInfo),但实际承担三项不可替代任务:
- 强制初始化
CPrintInfo:printInfo.m_rectDraw在此处被设为打印机可绘区域(非屏幕客户区),若跳过,OnPrint中pDC->GetTextExtent算出的字体宽度会严重失真; - 决定是否进入预览模式:
printInfo.m_bPreview由该函数返回值间接控制,返回FALSE会直接终止流程; - 注入用户自定义逻辑:比如根据当前数据量动态计算总页数(
printInfo.SetMaxPage(nTotalPages)),或校验打印机纸张是否匹配报表尺寸。
提示:
DoPreparePrinting内部会弹出标准打印对话框(CPrintDialog),但若你已在对话框中预设了打印机配置,可在此函数中return DoPreparePrinting(&printInfo, FALSE)跳过对话框,避免二次干扰。
// 示例:在 CMyView 类中重载 OnPreparePrinting BOOL CMyView::OnPreparePrinting(CPrintInfo* pInfo) { // 必须调用基类实现,否则 m_rectDraw 不会被初始化 if (!DoPreparePrinting(pInfo)) return FALSE; // 动态设置总页数:假设每页显示 20 条记录,共 47 条 int nTotalRecords = GetRecordCount(); pInfo->SetMaxPage((nTotalRecords + 19) / 20); // 向上取整 // 强制设置页边距(单位:逻辑英寸 × 100) pInfo->m_rectDraw.left = 100; // 左边距 1 英寸 pInfo->m_rectDraw.top = 100; // 上边距 1 英寸 pInfo->m_rectDraw.right = pInfo->m_rectDraw.Width() - 100; pInfo->m_rectDraw.bottom = pInfo->m_rectDraw.Height() - 100; return TRUE; }参数说明:pInfo->m_rectDraw是打印机设备上下文(DC)的可用绘图矩形,单位为逻辑像素(非毫米或英寸)。其坐标原点在页面左上角,Width()/Height()返回的是可打印区域宽高。若未在此处修正,OnPrint中pDC->TextOut(0,0,"Hello")会直接打到页眉外侧。
2.2OnBeginPrinting:分配资源的“开工仪式”,页数必须在此设定
这是你唯一能合法修改CPrintInfo::m_nNumPages的地方。MFC 默认只设m_nNumPages = 1,若你的报表需分 5 页,却在OnPrint中才判断pInfo->m_nCurPage == 5,则第 5 页永远不会被触发——因为框架根本没计划渲染它。
// 示例:在 CMyView 中重载 OnBeginPrinting void CMyView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo) { CView::OnBeginPrinting(pDC, pInfo); // 关键:必须显式设置总页数! // 此处可读取数据库、计算图表分页等耗时操作 m_nTotalPages = CalculateTotalPages(); pInfo->SetMaxPage(m_nTotalPages); // 创建字体对象(避免在 OnPrint 中反复创建销毁) LOGFONT lf = {0}; _tcscpy_s(lf.lfFaceName, _T("SimSun")); lf.lfHeight = -MulDiv(12, pDC->GetDeviceCaps(LOGPIXELSY), 72); // 12pt m_font.CreateFontIndirect(&lf); }参数说明:pDC是打印机 DC,此时已通过CreateDC初始化完毕,但尚未关联到具体物理设备(pDC->GetSafeHdc()可能为NULL)。所有字体、画笔等 GDI 对象必须在此处创建,否则OnPrint中频繁CreateFont会导致句柄泄漏。
2.3OnPrint:真正的“绘图车间”,GDI 代码的战场与陷阱
这才是你写业务逻辑的地方。但注意:屏幕上画曲线的OnDraw和打印机上画报表的OnPrint必须分离。同一段TextOut代码,在屏幕 DC 上可能居中,在打印机 DC 上却因 DPI 差异偏移 2cm。OnPrint接收两个核心参数:
pDC:打印机设备上下文,DPI 通常为 600dpi(远高于屏幕的 96dpi),所有坐标需按比例缩放;pInfo:包含当前页码m_nCurPage、总页数m_nNumPages、页眉页脚区域m_rectDraw。
// 示例:在 CMyView 中重载 OnPrint void CMyView::OnPrint(CDC* pDC, CPrintInfo* pInfo) { // 1. 保存 DC 状态(重要!预览和打印共用此函数) int nSavedDC = pDC->SaveDC(); // 2. 设置映射模式为 MM_LOENGLISH(1/100 英寸),统一单位 pDC->SetMapMode(MM_LOENGLISH); pDC->SetWindowExt(100, -100); // Y 轴向上为正 pDC->SetViewportExt(pDC->GetDeviceCaps(LOGPIXELSX), pDC->GetDeviceCaps(LOGPIXELSY)); // 3. 绘制页眉(固定位置:距上边 50 单位 = 0.5 英寸) CRect rectHeader(pInfo->m_rectDraw.left, pInfo->m_rectDraw.top - 50, pInfo->m_rectDraw.right, pInfo->m_rectDraw.top); pDC->FillSolidRect(&rectHeader, RGB(240,240,240)); pDC->TextOut(rectHeader.left + 10, rectHeader.top + 10, _T("销售报表 - 第 ") + CString(pInfo->m_nCurPage) + _T(" 页")); // 4. 绘制数据表格(按当前页码分页) DrawDataGrid(pDC, pInfo->m_nCurPage); // 5. 绘制页脚(含页码) CString strFooter; strFooter.Format(_T("第 %d 页,共 %d 页"), pInfo->m_nCurPage, pInfo->m_nNumPages); CSize size = pDC->GetTextExtent(strFooter); pDC->TextOut(pInfo->m_rectDraw.right - size.cx - 10, pInfo->m_rectDraw.bottom + 10, strFooter); // 6. 恢复 DC 状态 pDC->RestoreDC(nSavedDC); }关键逻辑说明:
SetMapMode(MM_LOENGLISH)是规避 DPI 陷阱的核心——它让所有坐标单位变为1/100 英寸,不再依赖设备物理像素。TextOut(100,100,...)永远是距左上角 1 英寸处;DrawDataGrid函数需根据pInfo->m_nCurPage计算本页起始行号,例如:int nStartRow = (pInfo->m_nCurPage - 1) * 20;;SaveDC/RestoreDC必须成对出现,否则预览切换时字体、颜色状态会污染。
2.4OnEndPrinting:收尾的“清场工”,资源释放的生死线
它只做一件事:释放OnBeginPrinting中申请的所有资源。若你在OnBeginPrinting中new了缓冲区、CreateFont了字体、CreatePen了画笔,这里必须delete、DeleteObject。漏掉任一环节,多次打印后必内存泄漏。
// 示例:在 CMyView 中重载 OnEndPrinting void CMyView::OnEndPrinting(CDC* pDC, CPrintInfo* pInfo) { CView::OnEndPrinting(pDC, pInfo); // 释放字体(必须!) if (m_font.GetSafeHandle()) m_font.DeleteObject(); // 释放其他 GDI 对象... if (m_brush.GetSafeHandle()) m_brush.DeleteObject(); }避坑 / 常见问题 / 排查
现象 1:打印预览时文字模糊、线条锯齿,但实际打印清晰
原因:预览使用内存 DC 模拟,若未设置pDC->SetStretchBltMode(COLORONCOLOR)或未启用pDC->SetGraphicsMode(GM_ADVANCED),GDI 会降级渲染。
解决:在OnPrint开头添加pDC->SetStretchBltMode(COLORONCOLOR); pDC->SetGraphicsMode(GM_ADVANCED);
现象 2:
OnPrint中pDC->GetTextExtent返回宽度为 0
原因:pDC尚未选入字体,或SelectObject失败(如字体句柄无效)。
解决:在OnBeginPrinting创建字体后,于OnPrint开头执行pDC->SelectObject(&m_font);,并检查返回值是否非NULL。
现象 3:多页打印时,第 2 页内容与第 1 页完全重复
原因:OnPrint中未根据pInfo->m_nCurPage切换数据源,或CalculateTotalPages()返回值错误导致m_nCurPage超出范围。
解决:在OnPrint开头加断点,验证pInfo->m_nCurPage是否递增;用ASSERT(pInfo->m_nCurPage <= pInfo->m_nNumPages)防御。
现象 4:调用
OnFilePrintPreview后程序崩溃,堆栈指向AFX_IDS_PREVIEWPAGEDESC
原因:静态链接 MFC 时,打印预览所需资源字符串(如页码描述)未被编译进 EXE。
解决:在工程resource.h后、stdafx.h中添加#include "afxprint.rc"(路径需正确,通常为$(VCInstallDir)atlmfc\include\afxprint.rc)。
现象 5:打印预览窗口的「打印」按钮点击无反应或直接关闭窗口
原因:预览窗口(CPreviewView)发送的AFX_ID_PREVIEW_PRINT命令未被主框架捕获,消息路由断裂。
解决:在主对话框类中添加ON_COMMAND(AFX_ID_PREVIEW_PRINT, &CMyDialog::OnPreviewPrint),并在OnPreviewPrint中调用OnFilePrint()。
3. 对话框中“无文档视图”的打印框架:CPrintFrame与CPrintView的构造逻辑
当你的程序是纯对话框(CDialog派生),没有CDocument/CView的天然支撑,MFC 的打印机制就失效了。原文档提出的CPrintFrame+CPrintView方案,本质是伪造一个轻量级文档视图环境:用CPrintFrame模拟CFrameWnd,用CPrintView模拟CView,再通过 Windows 消息把绘图指令“反射”回原始对话框。这不是 hack,而是 MFC 设计者预留的扩展接口。
3.1CPrintFrame:隐形的“框架窗口”,只存在 0.1 秒的打印调度器
它继承自CFrameWnd,但关键点在于:它不显示、不注册、不响应用户输入,仅作为CPrintView的父窗口存在。其Create方法传入(0,0,0,0)坐标,确保窗口不可见;PreCreateWindow中强制设置WS_POPUP避免任务栏图标。
// PrintFrame.h class CPrintFrame : public CFrameWnd { public: CPrintView* m_pView; // 指向子视图 CDialog* m_pCallerDlg; // 指向原始对话框(用于发消息) protected: DECLARE_MESSAGE_MAP() afx_msg void OnClose(); }; // PrintFrame.cpp IMPLEMENT_DYNAMIC(CPrintFrame, CFrameWnd) BEGIN_MESSAGE_MAP(CPrintFrame, CFrameWnd) ON_WM_CLOSE() END_MESSAGE_MAP() BOOL CPrintFrame::PreCreateWindow(CREATESTRUCT& cs) { cs.dwExStyle &= ~WS_EX_APPWINDOW; // 移除任务栏图标 cs.style = WS_POPUP; // 强制弹出式,不显示 return CFrameWnd::PreCreateWindow(cs); } int CPrintFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; // 创建子视图(CPrintView) m_pView = new CPrintView; m_pView->Create(NULL, _T(""), WS_CHILD | WS_VISIBLE, CRect(0,0,1,1), this, AFX_IDW_PANE_FIRST); return 0; } void CPrintFrame::OnClose() { // 不调用 CFrameWnd::OnClose(),直接销毁 DestroyWindow(); }设计逻辑说明:
m_pCallerDlg是桥梁:CPrintView在OnMyPrint中通过m_pCallerDlg->SendMessage(WM_MY_PRINT, ...)把绘图请求发回对话框;OnClose被重写为DestroyWindow(),防止用户误点关闭导致资源泄漏;Create时尺寸设为CRect(0,0,1,1),确保窗口最小化且不占用屏幕空间。
3.2CPrintView:虚拟的“绘图代理”,只负责转发不负责实现
它继承自CView,但所有打印相关虚函数均被重写为转发消息。真正的绘图逻辑在对话框的OnMyPrint中完成。这种解耦让对话框开发者只需关注业务绘图,无需理解CView生命周期。
// PrintView.h class CPrintView : public CView { public: afx_msg void OnMyPrint(); afx_msg void OnMyPrintPreview(); DECLARE_MESSAGE_MAP() }; // PrintView.cpp IMPLEMENT_DYNAMIC(CPrintView, CView) BEGIN_MESSAGE_MAP(CPrintView, CView) ON_COMMAND(ID_MYPRINT, &CPrintView::OnMyPrint) ON_COMMAND(ID_MYPRINTPREVIEW, &CPrintView::OnMyPrintPreview) END_MESSAGE_MAP() void CPrintView::OnMyPrint() { // 转发给对话框:WM_MY_PRINT 消息 if (GetParentFrame()->m_pCallerDlg) { PRINTINFO info = {0}; info.m_bPreview = FALSE; GetParentFrame()->m_pCallerDlg->SendMessage( WM_MY_PRINT, (WPARAM)GetDC(), (LPARAM)&info); } } void CPrintView::OnMyPrintPreview() { // 转发给对话框:WM_MY_PRINT_PREVIEW 消息 if (GetParentFrame()->m_pCallerDlg) { PRINTINFO info = {0}; info.m_bPreview = TRUE; GetParentFrame()->m_pCallerDlg->SendMessage( WM_MY_PRINT_PREVIEW, (WPARAM)GetDC(), (LPARAM)&info); } }关键参数说明:
ID_MYPRINT和ID_MYPRINTPREVIEW是自定义命令 ID,需在resource.h中定义(如#define ID_MYPRINT 32771);PRINTINFO是自定义结构体,用于传递m_bPreview标志,替代原生CPrintInfo的复杂字段;GetDC()返回的 DC 是临时的,仅用于占位,真正绘图在对话框中用CDC*参数完成。
3.3 对话框中的消息响应:WM_MY_PRINT的完整处理链
在你的主对话框类(如CMyDialog)中,需注册三个消息:WM_BEGIN_PRINTING、WM_END_PRINTING、WM_MY_PRINT。它们构成完整的打印生命周期钩子。
// MyDialog.h class CMyDialog : public CDialog { // ... afx_msg LRESULT OnBeginPrinting(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnEndPrinting(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnMyPrint(WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() }; // MyDialog.cpp BEGIN_MESSAGE_MAP(CMyDialog, CDialog) ON_MESSAGE(WM_BEGIN_PRINTING, &CMyDialog::OnBeginPrinting) ON_MESSAGE(WM_END_PRINTING, &CMyDialog::OnEndPrinting) ON_MESSAGE(WM_MY_PRINT, &CMyDialog::OnMyPrint) END_MESSAGE_MAP() LRESULT CMyDialog::OnBeginPrinting(WPARAM wParam, LPARAM lParam) { // 初始化:创建字体、计算总页数、设置 m_nTotalPages m_nTotalPages = CalculateTotalPages(); LOGFONT lf = {0}; _tcscpy_s(lf.lfFaceName, _T("Microsoft YaHei")); lf.lfHeight = -MulDiv(10, 96, 72); // 10pt 屏幕字体 m_font.CreateFontIndirect(&lf); return 0; } LRESULT CMyDialog::OnEndPrinting(WPARAM wParam, LPARAM lParam) { // 清理:删除字体 if (m_font.GetSafeHandle()) m_font.DeleteObject(); return 0; } LRESULT CMyDialog::OnMyPrint(WPARAM wParam, LPARAM lParam) { CDC* pDC = (CDC*)wParam; PRINTINFO* pInfo = (PRINTINFO*)lParam; // 1. 选入字体 pDC->SelectObject(&m_font); // 2. 绘制页眉(同前文逻辑) CRect rectHeader(100, -50, pDC->GetDeviceCaps(HORZRES)-100, 0); pDC->FillSolidRect(&rectHeader, RGB(240,240,240)); pDC->TextOut(120, -40, _T("对话框打印测试")); // 3. 绘制数据(根据 pInfo->m_bPreview 调整样式) if (pInfo->m_bPreview) { // 预览时用浅色边框示意分页 pDC->Rectangle(100, 0, pDC->GetDeviceCaps(HORZRES)-100, 800); } else { // 实际打印用实线 CPen pen(PS_SOLID, 1, RGB(0,0,0)); CPen* pOldPen = pDC->SelectObject(&pen); pDC->Rectangle(100, 0, pDC->GetDeviceCaps(HORZRES)-100, 800); pDC->SelectObject(pOldPen); } return 0; }执行流程图解:
- 用户点击对话框「打印」按钮 →
OnPrint()创建CPrintFrame→CPrintFrame创建CPrintView; CPrintView::OnMyPrint()→ 发送WM_MY_PRINT消息到CMyDialog;CMyDialog::OnMyPrint()执行绘图 → 返回;CPrintView自动调用OnEndPrinting清理(若需);CPrintFrame自动销毁。
3.4 集成步骤:四文件导入与五步调用法
将原文档提供的四个文件(PrintFrame.h,PrintFrame.cpp,PrintView.h,PrintView.cpp)加入工程后,按以下五步集成:
| 步骤 | 操作 | 关键代码/配置 |
|---|---|---|
| 1. 包含头文件 | 在对话框类.h文件顶部添加 | #include "PrintFrame.h"#include "PrintView.h" |
| 2. 定义消息宏 | 在resource.h中添加 | #define WM_MY_PRINT (WM_USER + 101)#define WM_MY_PRINT_PREVIEW (WM_USER + 102)#define WM_BEGIN_PRINTING (WM_USER + 103)#define WM_END_PRINTING (WM_USER + 104) |
| 3. 添加消息映射 | 在对话框类.cpp的BEGIN_MESSAGE_MAP中添加 | ON_MESSAGE(WM_MY_PRINT, &CMyDialog::OnMyPrint)ON_MESSAGE(WM_MY_PRINT_PREVIEW, &CMyDialog::OnMyPrintPreview)ON_MESSAGE(WM_BEGIN_PRINTING, &CMyDialog::OnBeginPrinting)ON_MESSAGE(WM_END_PRINTING, &CMyDialog::OnEndPrinting) |
| 4. 实现按钮响应 | 在对话框.cpp中添加OnBtnPrint() | void CMyDialog::OnBtnPrint(){CPrintFrame* pFrame = new CPrintFrame;pFrame->m_pCallerDlg = this;pFrame->Create(NULL, _T(""), WS_POPUP, CRect(0,0,0,0));pFrame->m_pView->OnMyPrint();} |
| 5. 静态链接修复 | 在stdafx.h末尾添加 | #ifdef _AFXDLL#include "afxprint.rc"#else#include "afxprint.rc"#endif |
注意:
afxprint.rc必须放在stdafx.h中#include "resource.h"之后,否则资源 ID 无法解析。
避坑 / 常见问题 / 排查
现象 1:
CPrintFrame创建后对话框卡死,CPU 占用 100%
原因:CPrintView::OnMyPrint()中SendMessage发送同步消息,而对话框OnMyPrint又调用了pDC->GetTextExtent等需等待 DC 就绪的操作,形成死锁。
解决:改用PostMessage异步发送,并在OnMyPrint中用GetDC()获取临时 DC(非wParam传入的 DC)。
现象 2:打印预览窗口显示空白,无任何内容
原因:CPrintView未正确设置m_pCallerDlg,或OnMyPrintPreview中SendMessage的wParam传入了无效 DC。
解决:在CPrintFrame::OnCreate中添加ASSERT(m_pCallerDlg != NULL);,并在OnMyPrintPreview开头加日志输出m_pCallerDlg地址。
现象 3:多次点击「打印」按钮后,内存持续增长
原因:CPrintFrame* pFrame = new CPrintFrame;分配的内存未被delete,CPrintFrame::OnClose仅销毁窗口,未释放对象。
解决:在CPrintFrame中添加virtual ~CPrintFrame() { delete m_pView; },并在OnClose后手动delete this;。
现象 4:静态链接下
OnMyPrintPreview崩溃在LoadString(AFX_IDS_PREVIEWPAGEDESC)
原因:afxprint.rc未被正确包含,或路径错误导致编译器忽略该文件。
解决:在项目属性 → 配置属性 → 常规 → 附加包含目录中添加$(VCInstallDir)atlmfc\include,并确认afxprint.rc文件存在于该路径。
现象 5:
OnMyPrint中pDC->TextOut文字不显示,但Rectangle正常
原因:字体未被选入 DC,或SelectObject返回NULL(字体创建失败)。
解决:在OnMyPrint开头添加if (!pDC->SelectObject(&m_font)) { TRACE(_T("Font select failed!\n")); }。
4. 打印预览的深度定制:双页模式、页码导航与按钮功能修复
MFC 内置的打印预览(CPrintPreviewState)默认只支持单页,且「打印」按钮被禁用。要达到专业报表软件的体验,必须深入CPreviewView层级进行定制。原文档评论区提到的CNewPreviewView派生方案,正是破解此局的关键。
4.1CNewPreviewView:接管预览窗口的“控制权”
它继承自CPreviewView,重写OnInitialUpdate和OnCmdMsg,目的是捕获预览工具栏的命令(尤其是AFX_ID_PREVIEW_PRINT),并将其路由到你的对话框。
// NewPreviewView.h class CNewPreviewView : public CPreviewView { protected: DECLARE_MESSAGE_MAP() virtual BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo); }; // NewPreviewView.cpp BEGIN_MESSAGE_MAP(CNewPreviewView, CPreviewView) END_MESSAGE_MAP() BOOL CNewPreviewView::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) { // 拦截预览工具栏的「打印」命令 if (nID == AFX_ID_PREVIEW_PRINT && nCode == CN_COMMAND) { // 转发给主对话框 CFrameWnd* pMainFrame = AfxGetMainWnd(); if (pMainFrame && pMainFrame->IsKindOf(RUNTIME_CLASS(CMyDialog))) { ((CMyDialog*)pMainFrame)->OnFilePrint(); return TRUE; } } return CPreviewView::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); }集成方式:在对话框的OnFilePrintPreview中,不再调用DoPrintPreview,而是使用DoPrintPreview的重载版本指定自定义视图类:
void CMyDialog::OnFilePrintPreview() { CPrintPreviewState* pState = new CPrintPreviewState; // 使用 CNewPreviewView 替代默认视图 DoPrintPreview(AFX_IDD_PREVIEW_TOOLBAR, this, RUNTIME_CLASS(CNewPreviewView), pState); }4.2 双页预览模式:CPrintPreviewState的隐藏参数
MFC 预览支持双页显示,但需在CPrintPreviewState构造时传入m_nPages。默认为 1,改为 2 即可:
void CMyDialog::OnFilePrintPreview() { CPrintPreviewState* pState = new CPrintPreviewState; pState->m_nPages = 2; // 关键:设置双页模式 DoPrintPreview(AFX_IDD_PREVIEW_TOOLBAR, this, RUNTIME_CLASS(CNewPreviewView), pState); }效果验证:预览窗口将自动以左右分栏形式显示当前页及下一页,OnPrint中pInfo->m_nCurPage仍为当前页码,无需修改业务逻辑。
4.3 页码导航控件:手动跳转到指定页
预览窗口默认无页码输入框。我们可在CNewPreviewView中添加OnKeyDown处理,监听数字键和回车:
void CNewPreviewView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { if (nChar >= '0' && nChar <= '9') { // 缓存输入的页码数字 m_strPageInput += (TCHAR)nChar; return; } if (nChar == VK_RETURN && !m_strPageInput.IsEmpty()) { int nTargetPage = _ttoi(m_strPageInput); if (nTargetPage >= 1 && nTargetPage <= m_nTotalPages) { // 跳转到指定页 SetPage(nTargetPage); } m_strPageInput.Empty(); return; } CPreviewView::OnKeyDown(nChar, nRepCnt, nFlags); }UI 增强:在预览工具栏添加一个CEdit控件,绑定EN_CHANGE事件实时更新m_strPageInput,体验更接近 Acrobat。
4.4 预览按钮状态修复:启用「打印」与「设置」
默认情况下,预览窗口的「打印」和「页面设置」按钮被禁用。需在CNewPreviewView::OnInitialUpdate中手动启用:
void CNewPreviewView::OnInitialUpdate() { CPreviewView::OnInitialUpdate(); // 启用工具栏按钮 CToolBarCtrl& toolbar = GetToolBarCtrl(); toolbar.EnableButton(AFX_ID_PREVIEW_PRINT, TRUE); toolbar.EnableButton(AFX_ID_PREVIEW_SETUP, TRUE); toolbar.EnableButton(AFX_ID_PREVIEW_ZOOM, TRUE); }参数说明:AFX_ID_PREVIEW_PRINT等 ID 定义在afxpriv.h中,无需额外包含。
避坑 / 常见问题 / 排查
现象 1:双页预览时,第二页内容错位或重复第一页
原因:OnPrint中未根据pInfo->m_nCurPage切换数据,或CalculateTotalPages()返回值错误导致m_nCurPage超限。
解决:在OnPrint开头添加ASSERT(pInfo->m_nCurPage >= 1 && pInfo->m_nCurPage <= m_nTotalPages);。
现象 2:
CNewPreviewView编译报错CPreviewView: class not defined
原因:未包含afxpriv.h头文件,或CPreviewView类未在stdafx.h中前置声明。
解决:在NewPreviewView.h顶部添加#include "afxpriv.h",并确认CPreviewView在afxwin.h中已声明。
现象 3:页码输入框输入后按回车无反应
原因:SetPage(nTargetPage)需要CPrintPreviewState支持,而DoPrintPreview创建的状态对象未暴露m_nTotalPages。
解决:在CMyDialog中保存m_nTotalPages为成员变量,并在CNewPreviewView中通过GetParentFrame()->GetParentFrame()获取对话框指针读取该值。
现象 4:启用「页面设置」按钮后,点击弹出对话框但设置不生效
原因:CPrintDialog的m_pd.nFromPage/m_pd.nToPage未被正确读取,或OnPreparePrinting中未使用这些值。
解决:在OnPreparePrinting中添加pInfo->SetMinPage(m_pd.nFromPage); pInfo->SetMaxPage(m_pd.nToPage);。
现象 5:预览窗口关闭后,
CNewPreviewView对象未销毁,内存泄漏
原因:CPreviewView的析构未被正确调用。
解决:在CNewPreviewView析构函数中添加TRACE(_T("CNewPreviewView destroyed\n"));,并确认 `
本文还有配套的精品资源,点击获取