☰
零GDI绘图如何实现打印:TinyRetroPad的EM_FORMATRANGE分页渲染原理详解
2026/10/12 8:55:17 网站建设 项目流程

【免费下载链接】TinyRetroPad

项目地址:https://gitcode.com/gh_mirrors/ti/TinyRetroPad
点击查看免费下载

TinyRetroPad 是一款仅约 2.5 KB 的 Notepad 风格 Windows 文本编辑器,完全用 x86 汇编编写。它的打印功能没有一行自绘 GDI 文字的代码——整个"分页渲染 + 打印"流程都交给 RICHEDIT 控件的一条EM_FORMATRANGE消息完成。本文带你拆解 trpad.asm 中PrintDoc函数的实现原理,看懂零GDI绘图的打印是怎么做到的。

为什么说它是"零GDI绘图"?

打开源码可以看到一个有意思的细节:trpad.asm 中gdi32.inc被直接注释掉了,官方解释是"编辑器不使用 GDI,Rich Edit 的字体通过EM_SETCHARFORMAT设置,无需 GDI 导入"。

换句话说:

  • ✅ 屏幕上显示文字?RICHEDIT 控件自己画
  • ✅ 打印到纸上?RICHEDIT 通过EM_FORMATRANGE自己画
  • ❌ 自己调用TextOut/SetTextColor之类的 GDI 函数?一次都没有

这就是本文的核心:把"渲染"这件事整体委托给控件,程序只负责指挥流程。

EM_FORMATRANGE:一条消息完成分页渲染

EM_FORMATRANGE是 RICHEDIT 的私有消息(WM_USER+57,见 trpad.asm)。它的协议非常巧妙:

  1. 你把一个打印机 DC(设备上下文)和一个FORMATRANGE 结构体发给控件
  2. 控件按页面大小,把指定字符范围的文字排版渲染到这个 DC 上
  3. 返回值 = 这一页用掉了多少字符后的位置,也就是下一页的起点

程序只需要拿这个返回值做循环,就能一页一页打完整篇文档——这就是"分页渲染原理"的全部秘密。

页宽页高是怎么算的?

Rich Edit 内部使用twips(1/1440 英寸)作为排版单位。TinyRetroPad 用GetDeviceCaps查询打印机 DC 的四项指标(trpad.asm):

| 查询项 | 含义 | | - | - | |HORZRES/VERTRES| 纸张宽/高(设备单位) | |LOGPIXELSX/LOGPIXELSY| 每英寸像素密度 |

然后做一条乘法除法:页面宽度(twips) = HORZRES × 1440 ÷ LOGPIXELSX,高度同理。这样无论用户选 A4 还是 Letter、打印机 DPI 是多少,页面尺寸都能自动换算正确。

PrintDoc 完整流程拆解

菜单 File → Print 触发后,PrintDoc(trpad.asm)按以下 6 步走完整个打印任务:

第1步:PrintDlgA 拿到打印机 DC

调用系统打印对话框,并带上PD_RETURNDC标志——用户点"确定"后,PRINTDLGA结构的hDC字段里直接装好了一个可用打印机 DC,省去了手动创建逻辑 DC/物理 DC 的繁琐代码。

第2步:StartDocA 声明打印任务开始

填充DOCINFOA(文档名)后调用StartDocA,告诉打印 spooler"一份新文档要来了"。

第3步:准备 FORMATRANGE 结构体

这是"分页渲染"的配置核心(trpad.asm):

  • hdc/hdcTarget:都填打印机 DC
  • rc/rcPage:上一步换算出的 twips 页面尺寸
  • chrg.cpMin = 0、chrg.cpMax = 全文长度:渲染范围是整篇文档

第4步:StartPage → EM_FORMATRANGE → EndPage 循环

主循环(trpad.asm)每轮做三件事:

  1. StartPage:声明新一页开始
  2. 向控件发送EM_FORMATRANGE,让它把cpMin..cpMax的文字排进当前页
  3. EndPage:本页结束

第5步:用返回值驱动翻页

EM_FORMATRANGE返回的"下一页起点"写回fmt.chrg.cpMin,与全文长度比较——只要没打完整篇,就继续循环。整个分页算法不到 5 条指令。

第6步:清理资源

文档全部输出后,再发一次长度为 0 的EM_FORMATRANGE用于刷新控件内部的格式化缓存,然后EndDoc结束任务、DeleteDC释放 DC。

如何体验这个打印功能

  • 菜单File → Print:菜单 ID 为IDM_FILE_PRINT(trpad.asm),由WndProc中的命令分发路由到PrintDoc
  • 菜单File → Page Setup:调用PageSetupDlgA公共对话框调整纸张与页边距(trpad.asm)

值得一提的是,这套完整打印能力(含打印对话框、分页循环、资源管理)在这个以字节数论英雄的項目里,只花费了固定的 2476 字节基线(增长日志见 trpad.asm),堪称"极小体积下最便宜的打印方案"。

总结:给新手的最短实践路径

| 环节 | TinyRetroPad 的做法 | 复用要点 | | - | - | - | | 拿打印机 DC |PrintDlgA+PD_RETURNDC| 对话框替你建好 DC | | 页面尺寸 |GetDeviceCaps换算 twips | 一次乘法除法,适配任意纸张 | | 排版渲染 |EM_FORMATRANGE| 控件渲染,零 GDI 代码 | | 分页驱动 | 用返回值更新cpMin| 一个 while 循环搞定 |

如果你正在写一个"小而全"的 Windows 文本编辑器,TinyRetroPad 给出的答案很清晰:能不写 GDI 就不写 GDI,让 RICHEDIT 替你排版分页。完整实现可对照 trpad.asm 阅读,背景资料见 README.md。

【免费下载链接】TinyRetroPad

项目地址:https://gitcode.com/gh_mirrors/ti/TinyRetroPad
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询