恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
MFC打印开发实战:Windows C++精确控制物理纸张输出
首页
资讯中心
/
MFC打印开发实战:Windows C++精确控制物理纸张输出
MFC打印开发实战:Windows C++精确控制物理纸张输出
发布时间:2026/10/22 2:34:56
简介本资源是一份面向Windows桌面开发者的MFC打印编程深度指南专为具备C基础、熟悉GDI绘图的中级开发者设计解决在实际项目中快速集成专业级打印与打印预览功能的痛点。文档系统剖析MFC文档/视图架构下的打印全流程——从OnPreparePrinting初始化、OnBeginPrinting设备上下文准备到核心OnPrint内容绘制逻辑再到OnEndPrinting资源清理并延伸讲解对话框中脱离文档视图结构实现自定义打印的完整框架含ID_MYPRINT命令映射、消息处理及内存DC预览机制等实战细节。资源为单文件PDF大小900KB内容源自南京航空航天大学仿真与控制实验室原创技术文档已获145人学习下载。读者可直接复用文中提供的函数重载范式、CPrintInfo参数解析方法及对话框打印结构模板显著降低SDK级打印开发门槛无需深入GDI底层即可输出格式灵活、页眉页脚可控的专业打印结果。1. MFC打印程序不是“调个PrintDialog就完事”它解决的是Windows桌面端C工程中「精确控制物理纸张输出」的硬需求很多刚接触MFC的开发者看到“打印”二字第一反应是打开CPrintDialog、点确定、等系统弹出打印机选择框——结果发现控件没对齐、页边距错位、中文乱码、多页内容被截断甚至同一台机器换台打印机就崩溃。这不是玄学而是MFC打印机制本质决定的它不封装底层GDI绘图逻辑而是把设备上下文DC的生命周期管理、页面坐标系映射、文档分页策略、打印队列状态监听全交给你手写。真正落地时你得在OnBeginPrinting里创建兼容DC在OnPrint里用TextOut或StretchBlt逐像素绘制还要自己处理MM_LOMETRIC/MM_TWIPS单位换算、SetMapMode与SetWindowExtEx的配合、StartPage/EndPage的边界守卫。这个PDF标题背后是一整套Windows GDI打印流水线的C实现闭环。适合正在维护老旧工业控制软件、医疗设备本地客户端、或需要嵌入报表导出能力的桌面应用的C工程师——尤其当你不能引入Qt或.NET又必须让客户用A4纸打出带公司LOGO、带条形码、带精确毫米级定位的单据时这套方案仍是不可替代的“最后一公里”。2. 从零构建可运行的MFC打印框架四步完成最小可执行单元MFC打印不是靠一个类搞定的而是一组协同工作的成员函数消息响应DC管理组合。下面以Visual Studio 2019 MFC AppWizard生成的单文档项目为基准构建一个能实际触发物理打印、输出“Hello World”并正确分页的最小框架。所有代码均基于CView派生类实现不依赖文档/视图分离模式外的复杂结构。2.1 创建打印支持的CView派生类并注册消息映射首先确保你的视图类如CMyView已声明打印相关虚函数并在消息映射表中添加ON_COMMAND(ID_FILE_PRINT, CMyView::OnFilePrint)和ON_COMMAND(ID_FILE_PRINT_PREVIEW, CMyView::OnFilePrintPreview)。注意不要手动调用DoPrint或DoPreparePrintingMFC框架会自动触发OnPreparePrinting→OnBeginPrinting→OnPrint→OnEndPrinting这一完整链路。// MyView.h class CMyView : public CView { protected: virtual BOOL OnPreparePrinting(CPrintInfo* pInfo) override; virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo) override; virtual void OnPrint(CDC* pDC, CPrintInfo* pInfo) override; virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo) override; // 打印专用资源句柄字体、画笔等 CFont m_fontPrint; CPen m_penBorder; public: afx_msg void OnFilePrint(); afx_msg void OnFilePrintPreview(); DECLARE_MESSAGE_MAP() };提示OnPreparePrinting是唯一允许你修改pInfo-m_pPDCPrintDialog指针的地方但切勿在此处调用DoModal()——MFC会在需要时自动弹出对话框。此处只做准备设置默认页数、启用/禁用预览按钮等。2.2 实现OnPreparePrinting控制打印范围与预览开关此函数决定是否进入打印流程以及预览窗口是否可用。关键点在于CPrintInfo对象的m_nNumPreviewPages字段——设为0则禁用预览设为正整数则预览显示对应页数通常设为3。同时通过pInfo-SetMaxPage()设定最大页码避免无限循环。// MyView.cpp BOOL CMyView::OnPreparePrinting(CPrintInfo* pInfo) { // 设置打印总页数此处固定为2页实际应根据数据动态计算 pInfo-SetMaxPage(2); // 启用打印预览显示前3页若总页数3则显示全部 pInfo-m_nNumPreviewPages 3; // 可选禁用某些打印机选项如双面打印 pInfo-m_pPD-m_pd.nMinPage 1; pInfo-m_pPD-m_pd.nMaxPage 2; // 调用基类准备必须否则框架不识别 return DoPreparePrinting(pInfo); }逻辑说明DoPreparePrinting(pInfo)是MFC内部调度核心它会检查pInfo有效性、初始化打印对话框、设置默认打印机。若返回FALSE整个打印流程终止。参数pInfo-m_nMaxPage必须在调用前设置否则OnPrint中pInfo-m_nCurPage可能越界。2.3 在OnBeginPrinting中初始化打印DC与资源这是打印前最后的资源准备阶段。重点在于必须用pDC创建兼容DCCompatible DC而非直接使用传入的pDC。因为传入的pDC是打印机设备上下文其属性如字体、画笔在跨页时不可继承且部分打印机DC不支持SelectObject操作。void CMyView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo) { CView::OnBeginPrinting(pDC, pInfo); // 创建兼容DC用于绘图缓冲关键 CDC* pMemDC new CDC(); pMemDC-CreateCompatibleDC(pDC); m_pMemDC pMemDC; // 成员变量保存供OnPrint使用 // 创建打印专用字体避免使用屏幕字体导致缩放失真 LOGFONT lf {0}; lf.lfHeight -MulDiv(12, pDC-GetDeviceCaps(LOGPIXELSY), 72); // 12pt转逻辑单位 _tcscpy_s(lf.lfFaceName, _T(SimSun)); // 中文字体必须显式指定 m_fontPrint.CreateFontIndirect(lf); // 创建边框画笔1像素宽黑色实线 m_penBorder.CreatePen(PS_SOLID, 1, RGB(0,0,0)); }参数说明MulDiv(12, pDC-GetDeviceCaps(LOGPIXELSY), 72)是Windows标准DPI换算公式将12磅point转换为当前打印机DC的逻辑像素单位。LOGPIXELSY返回每英寸垂直像素数不同打印机该值差异极大喷墨机常为600激光机可达1200硬编码像素值必然错位。2.4 在OnPrint中执行真实绘图坐标系、字体、分页逻辑三重校准OnPrint是核心绘图函数每页调用一次。必须严格遵循先设置映射模式→再选择字体/画笔→最后调用TextOut等GDI函数。特别注意pInfo-m_nCurPage页码索引从1开始且pDC此时已是打印机DC所有坐标均为物理坐标。void CMyView::OnPrint(CDC* pDC, CPrintInfo* pInfo) { // 步骤1设置映射模式为MM_LOMETRIC1/10毫米单位最易控制物理尺寸 pDC-SetMapMode(MM_LOMETRIC); pDC-SetWindowExt(100, -100); // X正向右Y正向下翻转Y轴 pDC-SetViewportExt(pDC-GetDeviceCaps(LOGPIXELSX), pDC-GetDeviceCaps(LOGPIXELSY)); // 步骤2选择资源 CFont* pOldFont pDC-SelectObject(m_fontPrint); CPen* pOldPen pDC-SelectObject(m_penBorder); // 步骤3计算可打印区域扣除页边距 CRect rectPage; pDC-GetClipBox(rectPage); // 获取当前页有效区域 int leftMargin 200; // 左边距20mm200 * 1/10mm int topMargin 150; // 上边距15mm CRect rectPrint(leftMargin, topMargin, rectPage.right - leftMargin, rectPage.bottom - topMargin); // 步骤4绘制内容按页码差异化 CString strText; if (pInfo-m_nCurPage 1) strText _T(第一页设备运行报告); else strText _T(第二页详细参数列表); // 使用TextOut精确控制位置单位1/10mm pDC-TextOut(rectPrint.left, rectPrint.top, strText); // 绘制边框验证坐标系是否正确 pDC-Rectangle(rectPrint); // 恢复GDI对象 pDC-SelectObject(pOldFont); pDC-SelectObject(pOldPen); }逻辑说明MM_LOMETRIC模式下1单位0.1毫米SetWindowExt(100,-100)定义窗口大小为100×100单位SetViewportExt将其映射到设备物理像素。负号翻转Y轴使坐标原点位于左上角符合常规绘图习惯。GetClipBox返回打印机DC的实际可打印矩形比GetDeviceCaps(HORZSIZE)更可靠——后者返回纸张物理尺寸但打印机驱动可能预留不可打印边距。2.5 在OnEndPrinting中释放资源并清理此函数必须释放OnBeginPrinting中创建的所有GDI对象否则引发句柄泄漏。注意CDC对象需deleteCFont/CPen需调用DeleteObject()。void CMyView::OnEndPrinting(CDC* pDC, CPrintInfo* pInfo) { CView::OnEndPrinting(pDC, pInfo); // 删除兼容DC if (m_pMemDC ! nullptr) { delete m_pMemDC; m_pMemDC nullptr; } // 删除字体和画笔 m_fontPrint.DeleteObject(); m_penBorder.DeleteObject(); }注意CFont::DeleteObject()必须在OnEndPrinting中调用若在OnBeginPrinting中创建后立即删除会导致OnPrint中SelectObject失败。MFC不会自动管理这些资源的生命周期。3. 打印内容精准定位的三大坐标系陷阱为什么“试出来”的数值永远不通用MFC打印中最耗时的调试环节往往卡在“为什么我在屏幕上画得好好的一打印就偏移2厘米”——根源在于混淆了三种坐标系逻辑坐标、设备坐标、物理坐标。它们不是简单的比例关系而是受映射模式SetMapMode、窗口/视口扩展SetWindowExt/SetViewportExt、打印机DPI、纸张尺寸四重影响。下面用具体数值拆解一个典型翻车场景。3.1 陷阱一MM_TEXT模式下用像素值定位导致不同打印机输出位置漂移某开发者在OnPrint中写pDC-SetMapMode(MM_TEXT); pDC-TextOut(100, 100, _T(Test)); // 假设100像素1cm现象在HP LaserJet上文字距左上角1.2cm在Epson喷墨机上却变成0.8cm。原因MM_TEXT模式下1逻辑单位1设备像素而不同打印机的LOGPIXELSX值不同HP为600Epson为360同样100像素在物理长度上相差近一倍。解决永远不用MM_TEXT做打印定位。改用MM_LOMETRIC或MM_TWIPS用物理单位毫米/1/20点编程。3.2 陷阱二忽略打印机不可打印边距Non-Printable Area现象文字紧贴纸张边缘时右侧或底部内容被裁切且GetDeviceCaps(HORZSIZE)返回的纸张宽度比实际小。原因所有打印机都有硬件限制的不可打印边距通常3~5mmGetDeviceCaps返回的是可打印区域尺寸而非纸张物理尺寸。GetClipBox才返回当前驱动允许的实际绘图矩形。验证方法在OnPrint开头添加CRect clip; pDC-GetClipBox(clip); TRACE(_T(ClipBox: %d,%d - %d,%d\n), clip.left, clip.top, clip.right, clip.bottom);你会看到clip的left/top通常为正数如200表示20mm左边距right/bottom小于纸张理论尺寸。3.3 陷阱三SetWindowExt与SetViewportExt参数符号不匹配导致Y轴反向现象文字从页面底部向上绘制或图形上下颠倒。原因SetWindowExt的Y值为正时逻辑Y轴向上SetViewportExt的Y值为正时设备Y轴向下。若两者符号不一致坐标系发生镜像。标准解法统一用负Y值翻转逻辑Y轴使其与设备Y轴同向pDC-SetWindowExt(100, -100); // 逻辑Y轴向下 pDC-SetViewportExt(100, 100); // 设备Y轴向下 → 一致或更稳妥地直接使用SetMapMode(MM_LOMETRIC)它内置了Y轴翻转无需手动调用SetWindowExt。3.4 一张表看懂四大映射模式适用场景映射模式单位Y轴方向是否推荐打印适用场景MM_TEXT1像素向下❌ 不推荐屏幕绘图打印必错位MM_LOMETRIC0.1毫米向下已翻转✅ 强烈推荐需要毫米级精确定位的报表、标签MM_HIMETRIC0.01毫米向下⚠️ 小心溢出超高精度图纸注意int范围±32767MM_TWIPS1/20点≈1/1440英寸向下✅ 推荐兼容Word/Excel单位适合文档类输出提示MM_LOMETRIC下A4纸210×297mm对应逻辑尺寸2100×2970单位完全在int安全范围内且数值直观易记。4. 中文打印必踩的五个坑字体、编码、GDI对象泄漏、多字节混排、打印机驱动兼容性MFC打印中文不是加个_T()宏就完事。从GB2312到UTF-8从宋体到微软雅黑每个环节都埋着深坑。以下是某工业设备客户端项目中血泪总结的五条高频翻车记录按“现象→原因→解决”结构呈现。4.1 现象打印机输出方块或问号但屏幕预览正常原因CPrintDialog默认使用ANSI编码而CString在Unicode工程中存储UTF-16TextOut传入时发生编码截断。更隐蔽的是部分老式打印机驱动根本不支持TrueType字体嵌入仅认点阵字体如SimSun。解决强制指定字体为SimSun宋体或NSimSun新宋体避免用Microsoft YaHei微软雅黑在LOGFONT中设置lfCharSet GB2312_CHARSET对于纯ASCII字符用TextOutA中文用TextOutW并确保CString为Unicode。4.2 现象同一段代码在Debug版能打印Release版崩溃于SelectObject原因Release版优化导致CFont对象析构顺序异常SelectObject返回的旧字体指针在OnPrint结束前已被释放。解决所有GDI对象CFont/CPen/CBrush必须在OnBeginPrinting中创建在OnEndPrinting中销毁OnPrint中SelectObject后必须保存返回值并在函数退出前SelectObject回原对象否则DC状态污染。4.3 现象打印多页时第二页内容与第一页完全重叠原因未在OnPrint开头调用pDC-SetWindowOrg(0,0)重置逻辑坐标原点。OnPrint每页调用时DC的原点可能残留上一页的偏移。解决void CMyView::OnPrint(CDC* pDC, CPrintInfo* pInfo) { pDC-SetWindowOrg(0, 0); // 关键重置原点 // ... 后续绘图 }4.4 现象条形码Code128打印模糊扫描枪无法识别原因GDI绘图抗锯齿开启pDC-SetGraphicsMode(GM_ADVANCED)导致线条宽度不精确而条形码要求模块宽度误差±0.1mm。解决打印前关闭高级图形模式pDC-SetGraphicsMode(GM_COMPATIBLE)用MoveTo/LineTo绘制条形码而非Rectangle后者有边框线宽叠加模块宽度按pDC-GetDeviceCaps(LOGPIXELSX)/25.4换算为像素如600dpi23.6px/mm。4.5 现象连接网络打印机时OnBeginPrinting中CreateCompatibleDC失败返回NULL原因网络打印机DC可能不支持CreateCompatibleDC尤其在Windows Server Core或无GUI环境。解决改用CreateIC创建信息上下文再用CreateCompatibleDC或降级为直接在打印机DC上绘图牺牲性能保功能// 替代方案不创建兼容DC直接绘图 // CDC* pMemDC new CDC(); // pMemDC-CreateCompatibleDC(pDC); // ... // 改为 // 直接使用pDC绘图但需确保每次绘图前重置DC状态 pDC-SetBkMode(OPAQUE); pDC-SetBkColor(RGB(255,255,255));注意CreateCompatibleDC失败时GetLastError()返回ERROR_INVALID_PARAMETER而非ERROR_NOT_ENOUGH_MEMORY需针对性判断。5. 进阶技巧实现带LOGO的页眉页脚、动态分页与打印进度反馈当基础打印跑通后真实项目需求会迅速升级客户要求每页顶部加公司LOGO、底部加页码和时间戳报表数据量大需自动分页用户点击打印后想知道“到底打到第几页了”。这些不是锦上添花而是交付底线。下面给出经过产线验证的三招。5.1 用StretchBlt实现高保真LOGO嵌入绕过GDI缩放失真直接TextOut画文字没问题但插入位图LOGO极易糊成马赛克。根本原因是StretchBlt默认使用最近邻插值而打印机高DPI放大时像素点被粗暴拉伸。解决方案用SetStretchBltMode开启半色调模式并预缩放LOGO到位图尺寸。void CMyView::DrawLogo(CDC* pDC, const CRect rectLogo) { // 步骤1加载LOGO资源假设IDB_LOGO为24位BMP CBitmap bitmap; bitmap.LoadBitmap(IDB_LOGO); BITMAP bmp; bitmap.GetBitmap(bmp); // 步骤2创建兼容DC和位图用于缩放 CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap* pOldBmp memDC.SelectObject(bitmap); // 步骤3设置高质量缩放 pDC-SetStretchBltMode(COLORONCOLOR); // 半色调模式 pDC-SetMapMode(MM_LOMETRIC); // 步骤4计算目标尺寸保持宽高比 int srcWidth bmp.bmWidth; int srcHeight bmp.bmHeight; int dstWidth rectLogo.Width(); int dstHeight MulDiv(dstWidth, srcHeight, srcWidth); // 步骤5执行缩放绘制 pDC-StretchBlt( rectLogo.left, rectLogo.top, dstWidth, dstHeight, memDC, 0, 0, srcWidth, srcHeight, SRCCOPY ); memDC.SelectObject(pOldBmp); }关键参数COLORONCOLOR比HALFTONE更锐利适合线条型LOGOMulDiv确保整数运算不丢精度rectLogo应基于GetClipBox计算避开不可打印区。5.2 动态分页算法按行高与剩余空间实时决策硬编码SetMaxPage(5)不现实。真实报表需根据字体高度、行间距、数据行数动态计算页数。核心是GetTextExtentPoint32测量文本高度int CMyView::CalculatePageCount(CDC* pDC, const std::vectorCString data) { // 测量单行高度含行距 CSize sizeLine pDC-GetTextExtentPoint32(_T(阿), 1); int lineHeight sizeLine.cy 20; // 20为行距0.2mm // 计算每页可容纳行数 CRect rectPage; pDC-GetClipBox(rectPage); int printableHeight rectPage.Height() - 200; // 扣除页眉页脚20mm int linesPerPage printableHeight / lineHeight; // 总页数 数据行数 / 每页行数向上取整 return (data.size() linesPerPage - 1) / linesPerPage; }然后在OnPreparePrinting中调用pInfo-SetMaxPage(CalculatePageCount(pDC, m_reportData));5.3 打印进度反馈用CPrintInfo::m_bContinuePrinting实现用户可控中断MFC默认打印是阻塞的用户无法取消。通过轮询m_bContinuePrinting标志位可在OnPrint中插入检查点void CMyView::OnPrint(CDC* pDC, CPrintInfo* pInfo) { // 每页开头检查是否被用户取消 if (!pInfo-m_bContinuePrinting) { TRACE(_T(用户取消打印当前页%d\n), pInfo-m_nCurPage); return; } // 绘图逻辑... DrawHeader(pDC, pInfo-m_nCurPage); DrawContent(pDC, pInfo-m_nCurPage); DrawFooter(pDC, pInfo-m_nCurPage); // 每页结束更新UI需PostMessage到主线程 AfxGetMainWnd()-PostMessage(WM_USER_PRINT_PROGRESS, (WPARAM)pInfo-m_nCurPage, (LPARAM)pInfo-m_nMaxPage); }配套在主框架中处理消息// 主框架类中 LRESULT CMainFrame::OnPrintProgress(WPARAM wParam, LPARAM lParam) { int curPage (int)wParam; int maxPage (int)lParam; CString str; str.Format(_T(打印中%d / %d), curPage, maxPage); m_wndStatusBar.SetPaneText(0, str); return 0; }血泪经验OnPrint中禁止调用AfxMessageBox或任何UI阻塞操作否则打印线程死锁。所有UI更新必须PostMessage异步进行。希望帮到你。本文还有配套的精品资源点击获取