技术演进中的开发沉思-38 MFC系列:关于打印

打印程序也是MFC开发中不能忽视的一个环节,现在做打印开发so easy。但当年做打印开发还是挺麻烦。在当年的桌面程序里就像拼图的最后一块,看着简单,实则要把屏幕上的像素世界,准确映射到打印机的物理纸张上。而MFC 的打印机制就像老照相馆的暗房:你不用懂显影液的配方(打印机驱动),但得知道怎么调整光圈(设备上下文),才能让照片(打印效果)和底片(屏幕显示)一致。

一、打印

MFC 用 “设备上下文”(DC)机制把这套流程封装得严丝合缝。CDC这个类就像个万能画板 —— 往pDC->m_hDC里塞屏幕设备句柄,它就是能显示彩色像素的电子画布;换成打印机句柄,就变成了能输出墨点的物理画布。我第一次掉坑里,是发现屏幕上清晰的折线图,打印出来竟缩成了左上角的小方块。对着调试器看了半天才明白:屏幕用的是 “像素坐标”,100 像素在 17 寸显示器上大概 1.5 厘米;而打印机认的是 “逻辑单位”,同样 100 单位在 A4 纸上能占 3 厘米。就像用惯了厘米尺的人突然换成英寸尺,比例没换算对,画出的东西自然走样。

解决这个问题时,我在OnPrepareDC里加了段坐标转换代码:


void CMyView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo){CView::OnPrepareDC(pDC, pInfo);if (pDC->IsPrinting()) {// 把打印机逻辑单位设为毫米pDC->SetMapMode(MM_LOMETRIC);// 调整原点到左上角(打印机默认原点在左下角)pDC->SetWindowOrg(0, -2970); // A4纸高度297毫米,转换为0.1毫米单位}}

这段代码让打印机突然 “看懂” 了屏幕坐标 —— 就像给两个说不同方言的人配了翻译。改完那天,李教授来试打印,看着纸张缓缓吐出时,他手指在图表边缘比了比:“这下对齐了,比我用尺子量着画强多了。”

二、MFC 的 “默认菜谱”

MFC 最贴心的地方,是把打印的基础流程做成了 “半成品菜”。就像超市里切好的净菜,你不用自己洗菜切菜(写设备初始化代码),只要按口味加点调料(重写绘图函数)就行。默认机制下,CView类的OnDraw函数是个多面手 —— 屏幕刷新时它被调用,打印时 MFC 会自动把打印机 DC 传进来,让同一段绘图代码在两种设备上生效。

早期做实验报告打印时,我就靠这个机制省了不少事。OnDraw里先画标题,再画数据表格,最后加实验员签名栏:


void CMyView::OnDraw(CDC* pDC){CMyDoc* pDoc = GetDocument();ASSERT_VALID(pDoc);if (!pDoc) return;// 标题用粗体CFont font, *pOldFont;font.CreatePointFont(160, _T("宋体"), pDC); // 16pt字体pOldFont = pDC->SelectObject(&font);pDC->TextOut(100, 50, _T("材料抗压实验报告"));pDC->SelectObject(pOldFont);// 画表格线CPen pen(PS_SOLID, 1, RGB(0,0,0)), *pOldPen;pOldPen = pDC->SelectObject(&pen);DrawTableBorder(pDC); // 自定义画边框函数pDC->SelectObject(pOldPen);// 填充数据DrawExperimentData(pDC, pDoc->m_dataList);}

但这套默认机制有个明显短板:它不知道 “一页该装多少内容”。有次李教授导入了三十组实验数据,结果打印机把所有表格挤在一页纸上,字小得要用放大镜看。这就像写文章忘了分段,用户看着费劲。后来我在OnPreparePrinting里加了分页逻辑:


BOOL CMyView::OnPreparePrinting(CPrintInfo* pInfo){if (!DoPreparePrinting(pInfo))return FALSE;// 计算总页数:每页最多显示8组数据CMyDoc* pDoc = GetDocument();int nTotalData = pDoc->m_dataList.GetCount();int nMaxPage = (nTotalData + 7) / 8; // 向上取整pInfo->SetMaxPage(nMaxPage > 0 ? nMaxPage : 1);return TRUE;}

加了这段代码后,数据会自动分到多页,每页底部还能加上页码。客户拿着打印好的报告翻了两页,突然说:“这比我当年用打字机方便多了 —— 那时候换页得手动卷纸。”

三、给 Scribble “加分页”

微软的 Scribble 示例程序,是我们那代程序员的 “技术启蒙教材”。这个能随手涂鸦的小程序,默认打印时却像把整本速写本强行压成一页纸 —— 如果用户画了条横贯三屏的曲线,打印出来就会变成细得像头发丝的线条。我们做项目时,给它加了三个 “增强包”,让它从 “便签纸” 变成能装订的 “画册”。

页设置对话框是第一个要加的。就像去打印店先选纸张,我用CPrintDialog做了个设置界面,让用户能选 A4 还是 B5,横版还是竖版。调试时发现个有趣的细节:当用户选 “横向”,MFC 会自动调整CPrintInfo里的纸张尺寸,这时候绘图坐标得跟着变。我在OnBeginPrinting里加了判断:


void CMyScribbleView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo){CView::OnBeginPrinting(pDC, pInfo);// 记录纸张方向和尺寸m_bLandscape = (pInfo->m_pPD->m_pd.Flags & PD_LANDSCAPE) != 0;m_sizePaper = pInfo->m_sizePaper; // 单位是0.01毫米}

有次测试时,我选了横向打印,曲线突然跑到纸外面去了。查了半天才发现,他画的曲线坐标是按竖版纸张算的。后来我在绘图前加了坐标适配:如果是横向,就把 X 和 Y 轴的绘图范围对调,就像把画布旋转 90 度再下笔。

坐标映射是第二个重点。屏幕上用像素定位,打印机用物理单位,这中间得有个 “换算器”。SetMapMode函数就像把尺子刻度从 “像素” 换成 “毫米”,但实际用的时候要注意:打印机的原点默认在纸张左下角,而屏幕原点在左上角,不调整的话,画出来的东西会上下颠倒。我通常这么处理:


void CMyScribbleView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo){CView::OnPrepareDC(pDC, pInfo);if (pDC->IsPrinting()) {pDC->SetMapMode(MM_LOMETRIC); // 1单位=0.1毫米// 把原点移到左上角,Y轴向下为正pDC->SetWindowOrg(0, -m_sizePaper.cy);}}

这么一改,屏幕上从左到右画的线条,打印出来也是从左到右,不会变成 “镜像效果” 了。

智能分页是最费脑筋的。我给每个涂鸦线段加了 “高度标记”,就像在文章里标上段落长度。打印时程序会累计高度,超过纸张高度就自动分页。代码里用OnPrint代替OnDraw,根据当前页码决定画哪些内容:


void CMyScribbleView::OnPrint(CDC* pDC, CPrintInfo* pInfo){int nCurPage = pInfo->m_nCurPage;int nStartIndex = (nCurPage - 1) * m_nMaxLinesPerPage;int nEndIndex = min(nStartIndex + m_nMaxLinesPerPage, m_lines.GetSize());// 只绘制当前页包含的线段for (int i = nStartIndex; i < nEndIndex; i++) {DrawLine(pDC, m_lines[i]);}}

改完这个功能那天,我画了条从北京到上海的曲线(模拟铁路枢纽线路),程序自动分成 3 页打印,拼接起来刚好是完整的路线。这种 “技术实现想法” 的瞬间,大概就是程序员的快乐时刻。

四、打印预览

打印预览绝对是 MFC 里的 “温柔设计”。早年没这功能时,我们常为了调格式浪费半盒 A4 纸 —— 有时候差一毫米就偏出边框,得改一行代码重新编译,再打印出来看效果。预览功能就像寄信前先看一眼信封,在屏幕上就能看到最终效果。

MFC 的预览原理其实很巧妙:它先创建个内存 DC 当 “虚拟打印机”,把内容画到内存里,再缩放显示到屏幕窗口。CPrintPreviewState这个类会管理预览窗口的缩放比例,你点 “放大”,它就把内存里的图像按 1:1 显示;点 “缩小”,就按比例缩小,像用放大镜看地图。

我做报表系统时,加了个 “实时预览” 功能 —— 用户改了表格样式,预览窗口会立刻刷新。实现时用了OnUpdate消息:


void CMyReportView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint){if (IsPreviewMode()) {// 如果在预览模式,强制刷新Invalidate();}CView::OnUpdate(pSender, lHint, pHint);}

这种方法实现后,客户调整表头格式,就只需要拖动鼠标调整列宽了,在没有打印功能的时候,很多图表,都是客户用尺子量着画表格的, 那时候改一次报表得重新抄一遍。而当年我们开发的应用改变了他们的工作方式,其实技术进步的意义,有时就是把人从机械劳动里解放出来。

最后小结:

现在想想,MFC 的打印机制,其实藏着一套产品思维:默认机制解决 80% 的基础需求(不用重复造轮子);增强功能应对个性化场景(给高手留发挥空间);预览功能则照顾到用户的 “安全感”(避免失误)。MFC 打印的可贵之处,在于它用类封装了复杂的设备交互逻辑,让我们能专注于业务需求 —— 这和现在的云打印 API 的设计理念不谋而合。当年调试OnPrint函数时领悟的 “设备无关性” 原则,现在依然适用于云打印开发:无论底层是激光还是喷墨,代码只需要关心 “打印什么”,而不是 “怎么打印”。技术会迭代,但解决问题的本质从未改变。就像从 MFC 到云打印,变的是实现手段,不变的是让用户 “用着顺手” 的追求。未完待续..........

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/92411.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/92411.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Apache Ignite 长事务终止机制

这段内容讲的是 Apache Ignite 中长事务终止机制&#xff08;Long Running Transactions Termination&#xff09;&#xff0c;特别是关于分区映射交换&#xff08;Partition Map Exchange&#xff09;与事务超时设置&#xff08;Transaction Timeout&#xff09;之间的关系。下…

网络编程---TCP协议

TCP协议基础知识TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是互联网核心协议之一&#xff0c;位于传输层&#xff08;OSI第4层&#xff09;&#xff0c;为应用层提供可靠的、面向连接的、基于字节流的数据传输服务。它与IP协议共同构成…

K 近邻算法(K-Nearest Neighbors, KNN)详解及案例

K近邻算法&#xff08;K-Nearest Neighbors, KNN&#xff09;详解及案例 一、基本原理 K近邻算法是一种监督学习算法&#xff0c;核心思想是“物以类聚&#xff0c;人以群分”&#xff1a;对于一个新样本&#xff0c;通过计算它与训练集中所有样本的“距离”&#xff0c;找出距…

深入理解 Redis 集群化看门狗机制:原理、实践与风险

在分布式系统中&#xff0c;我们常常需要执行一些关键任务&#xff0c;这些任务要么必须成功执行&#xff0c;要么失败后需要明确的状态&#xff08;如回滚&#xff09;&#xff0c;并且它们的执行时间可能难以精确预测。如何确保这些任务不会被意外中断&#xff0c;或者在长时…

Python机器学习:从零基础到项目实战

目录第一部分&#xff1a;思想与基石——万法归宗&#xff0c;筑基问道第1章&#xff1a;初探智慧之境——机器学习世界观1.1 何为学习&#xff1f;从人类学习到机器智能1.2 机器学习的“前世今生”&#xff1a;一部思想与技术的演进史1.3 为何是Python&#xff1f;——数据科学…

数据库:库的操作

1&#xff1a;查看所有数据库SHOW DATABASES;2&#xff1a;创建数据库CREATE DATABASE [ IF NOT EXISTS ] 数据库名 [ CHARACTER SET 字符集编码 | COLLATE 字符集校验规则 | ENCRYPTION { Y | N } ];[]&#xff1a;可写可不写{}&#xff1a;必选一个|&#xff1a;n 选 1ENCR…

AngularJS 动画

AngularJS 动画 引言 AngularJS 是一个流行的JavaScript框架,它为开发者提供了一种构建动态Web应用的方式。在AngularJS中,动画是一个强大的功能,可以帮助我们创建出更加生动和引人注目的用户界面。本文将详细介绍AngularJS动画的原理、用法以及最佳实践。 AngularJS 动画…

SonarQube 代码分析工具

💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 🧠全面掌握 SonarQube:企业代码质量保障的利器 🚀 在当今 DevOps 流水线中,代码…

vmware vsphere esxi6.5 使用工具导出镜像

注&#xff1a;为什么使用这个工具&#xff0c;我这边主要因为esxi6.5自身bug导致web导出镜像会失败一、下载VMware-ovftool到本地系统&#xff08;根据你的操作系统版本到官网下载安装&#xff0c;此处略&#xff09;以下内容默认将VMware-ovftool安装到windows 本地系统为例。…

ES 踩坑记:Set Processor 字段更新引发的 _source 污染

问题背景 社区的一个伙伴想对一个 integer 的字段类型添加一个 keyword 类型的子字段&#xff0c;然后进行精确匹配的查询优化&#xff0c;提高查询的速度。 整个索引数据量不大&#xff0c;并不想进行 reindex 这样的复杂操作&#xff0c;就想到了使用 update_by_query 的存量…

如何彻底搞定 PyCharm 中 pip install 报错 ModuleNotFoundError: No module named ‘requests’ 的问题

如何彻底搞定 PyCharm 中 pip install 报错 ModuleNotFoundError: No module named ‘requests’ 的问题 在使用 PyCharm 开发 Python 项目时&#xff0c;ModuleNotFoundError: No module named requests 是一个常见但令人头疼的问题。本篇博文将从环境配置、原因分析到多种解…

powerquery如何实现表的拼接主键

在做表过程中&#xff0c;有时候没有基表&#xff0c;这个时候就要构造完整的主键&#xff0c;这样才可以使之后匹配的数据不会因为主键不全而丢失数据 我的处理方法是吧多个表的主键拼在一起然后去重&#xff0c;构造一个单单之后之间的表作为基表去匹配数据 所以就哟啊用到自…

今日Github热门仓库推荐 第八期

今日Github热门仓库推荐2025-07-22 如果让AI分别扮演 后端开发人员和前端开发人员&#xff0c;然后看看他们分别对github每天的trending仓库感兴趣的有哪些&#xff0c;并且给出他感兴趣的理由&#xff0c;那会发生什么呢&#xff1f; 本内容通过Python AI生成&#xff0c;项…

Dify-13: 文本生成API端点

本文档提供了有关 Dify 中与文本生成相关的 API 端点的全面信息。文本生成 API 支持无会话持久性的单次请求文本生成&#xff0c;使其适用于翻译、摘要、文章写作等非对话式人工智能应用场景。 概述 文本生成 API 端点允许开发人员将 Dify 的文本生成功能集成到不需要维护对话上…

Leetcode 3620. Network Recovery Pathways

Leetcode 3620. Network Recovery Pathways 1. 解题思路2. 代码实现 题目链接&#xff1a;3620. Network Recovery Pathways 1. 解题思路 这一题我最开始想的是遍历一下所有的网络路径&#xff0c;不过遇到了超时的情况。因此后来调整了一下处理思路&#xff0c;使用二分法的…

链路备份技术(链路聚合、RSTP)

一、链路聚合&#xff01;链路备份技术之一-----链路聚合&#xff08;Link Aggregation&#xff09;被视为链路备份技术&#xff0c;核心原因在于它能通过多条物理链路的捆绑&#xff0c;实现 “一条链路故障时&#xff0c;其他链路自动接管流量” 的冗余备份效果&#xff0c;同…

PyTorch新手实操 安装

PyTorch简介 PyTorch 是一个基于 Python 的开源深度学习框架&#xff0c;由 Meta AI&#xff08;原 Facebook AI&#xff09;主导开发&#xff0c;以动态计算图&#xff08;Define-by-Run&#xff09;为核心&#xff0c;支持灵活构建和训练神经网络模型。其设计理念高度契合科…

Element Plus Table 组件扩展:表尾合计功能详解

前言在现代数据驱动的社会中&#xff0c;数据分析和统计成为了非常重要的任务。为了更有效地分析数据和展示统计结果&#xff0c;前端开发人员可以使用Vue框架和Element Plus组件库来实现数据的统计和分析功能。以下是一个关于如何在 Element Plus 的 el-table 组件中实现行汇总…

神经网络 非线性激活层 正则化层 线性层

神经网络 非线性激活层 作用&#xff1a;增强模型的非线性拟合能力 非线性激活层网络&#xff1a; class activateNet(nn.Module):def __init__(self):super(activateNet,self).__init__()self.relu nn.ReLU()self.sigmoid nn.Sigmoid()def forward(self,input):#output sel…

【Vue进阶学习笔记】组件通信专题精讲

目录前言props 父传子原理说明使用场景代码示例父组件 PropsTest.vue子组件 Child.vue自定义事件 $emit 子传父原理说明使用场景代码示例父组件 EventTest.vue子组件 Event2.vueEvent Bus 兄弟/跨层通信原理说明使用场景代码示例事件总线 bus/index.ts兄弟组件通信示例Child2.v…