☰
Lazarus跨平台打印实战:Printer4Lazarus组件安装、坐标与避坑指南
2026/10/22 8:54:54 网站建设 项目流程

用 Lazarus 写桌面程序,做到某个版本一定逃不掉打印这个需求:随便一张出货单、一份发票、一个统计报表,都要能规规矩矩地从打印机里出来。Printer4Lazarus 就是 Lazarus 官方的打印组件包,它提供的不只是一两个控件,而是一整套跨平台打印方案——从选择打印机、配置纸张方向,到把文字、表格、图片画到打印画布上,再到打印预览和任务执行,全都覆盖。很多第一次接触它的朋友一上来就卡在三件事上:包到底装哪个、坐标为什么对不上、中文为什么乱码。这篇文章我把整个流程从零跑一遍,尽量少用套话,直接上能用的代码和踩坑记录。

1. Printer4Lazarus 组件包到底装了什么

1.1 组件家族的四个主角

先别急着写代码,把包内的几个核心对象搞清楚,后面才不会糊涂。Printer4Lazarus 不是一个单一控件,它是一组组件和类的集合,实际开发中最常用到的是这么几位:TPrinter、TPrintDialog、TPrinterSetupDialog,以及偶尔会用到的 TPrinterPreview。

TPrinter 是整个打印体系的核心,它不是一个可视化控件,而是一个运行时对象。你的程序里可以通过全局函数 Printer 拿到它的唯一实例,然后通过它去操作打印机设备、读取页面尺寸、获取打印画布、发起打印任务。所有绘制操作,比如写字、画线、贴图,都发生在 TPrinter 提供的 Canvas 上。

TPrintDialog 是标准打印对话框,用户点击打印按钮后弹出,让用户选打印机、份数、页码范围。执行 Execute 方法后,用户的这些选择会自动同步到全局 Printer 对象中。TPrinterSetupDialog 则负责纸张和方向的设置,跟打印对话框分工明确。

TPrinterPreview 是包内置的打印预览组件,可以把它理解成一个简化版的纸面模拟器。它能把打印内容按页渲染到屏幕上,适合做简单文档的预览。不过我会在后面的章节里坦白讲,我自己在实际项目里很少拿它处理复杂报表,原因到第五节再展开。

组件/对象主要职责典型使用场景
TPrinter管理打印机设备、页面参数、打印画布,执行打印任务所有打印操作的核心入口
TPrintDialog弹出系统打印对话框,让用户选择打印机、份数、范围点击“打印”按钮后弹出
TPrinterSetupDialog设置纸张大小、打印方向、进纸方式打印前的纸张配置
TPrinterPreview在界面上预览当前打印内容简单文档的打印预览

1.2 为什么不用系统接口直接打印

这个问题几乎每次分享都会有人问。Windows 下可以用 GDI 直接写打印,Linux 下可以走 CUPS API,macOS 下可以调 Quartz,那为什么还要包一层组件?

因为平台太多了。你的程序可能今天跑在 Windows 上,明天就要在 Linux 客户那边上线,后天还要适配 macOS。用系统原生接口,等于每个平台都写一套打印模块,字体映射、纸张枚举、坐标换算全都要各自处理。这个工作量,足够把一个团队拖进维护泥潭。

Printer4Lazarus 做的事情就是把这些差异全部收口。它对上层提供一套统一的 Canvas 绘图模型,你像在窗体上画图一样往纸面上绘制内容,底层到底走的是 GDI 还是 CUPS 还是 Quartz,组件包替你处理。打个比方,这就像你家的插头规格千奇百怪,但只要是往同一个万能转换头上插,到了哪个国家都能通上电。

另外它还有个好处:安装包是 Lazarus 自带的,不需要额外引入一堆 DLL 或动态库,不增加部署负担。从工程化的角度看,这是一笔非常划算的投资。

2. 安装与工程配置:5 分钟跑通第一段打印代码

2.1 在 Lazarus 中安装 Printer4Lazarus 包的两种方式

很多新手在组件面板翻了半天找不到打印组件,其实就是包没装进去。安装方式很简单,打开 Lazarus 菜单栏的 Package -> Install/Uninstall Packages...,在弹出的窗口右侧 Available for installation 列表里搜索 Printer4Lazarus,选中后点 Install,再点 Save 并重启 IDE。

重启时 IDE 通常会弹出一个确认框,问你是否重新编译并加载新包,选 Yes 即可。编译过程大概几十秒到几分钟不等,取决于你的机器和 Lazarus 版本。装完之后组件面板会多出和打印相关的图标,常见的是出现在 Printing 分类下。

另一种方式是手动挂载包文件。在你的 Lazarus 安装目录下找到 components/printers 目录,里面有个 printer4lazarus.lpk 文件。用 IDE 打开这个包文件后,在 Package 窗口里点击 Use -> Add to project,也能把组件引入当前工程。这种方式适合只想在某个项目里临时使用,不想全局安装的场景。

装完包之后记得在项目代码里 uses 引用 Printers 单元。如果编译时报找不到这个单元,先回头检查包是否真的安装成功,这是第一步最常见的坑。

2.2 最小可运行打印代码

包装好,接下来写第一段能跑的代码。我用一个最简单的按钮事件示范,这段代码会在默认打印机上输出一行文字:

procedure TForm1.Button1Click(Sender: TObject); begin Printer.BeginDoc; try Printer.Canvas.Font.Name := 'Microsoft YaHei'; Printer.Canvas.Font.Size := 12; Printer.Canvas.TextOut(100, 100, 'Hello, Printer4Lazarus!'); finally Printer.EndDoc; end; end;

过程非常简单,但有几个点很关键。Printer 是一个全局对象,直接调用它的方法,不用自己 Create,也不用自己去 Free,程序退出时由系统统一管理。这点必须牢记,如果你试图手动创建一个 TPrinter 实例,往往会拿到一个没绑定的空对象,下面调用直接报错。

BeginDoc 表示开始一个新的打印任务,EndDoc 表示结束并真正把数据交给打印机。这两者必须成对出现。一旦 BeginDoc 执行,你又忘了 EndDoc,打印任务就会一直挂在系统队列里,表现为并口任务卡死、打印机没反应。

所以我上面的代码用了 try/finally 结构,即使绘制过程中抛异常也能保证 EndDoc 被调用。这是我在项目里养成的固定写法,强烈建议你也这样写。

2.3 打印对话框怎么接入

实际应用中,用户不会每次打印都直接打默认打印机,尤其公司环境里往往有多台设备,所以接入打印对话框是刚需。把组件面板里的 TPrintDialog 拖到窗体上,在按钮事件里这样写:

procedure TForm1.ButtonPrintClick(Sender: TObject); begin if PrintDialog1.Execute then begin ShowMessage(Format('当前打印机:%s,份数:%d', [Printer.Printers[Printer.PrinterIndex], Printer.Copies])); DoPrint; end; end;

PrintDialog1.Execute 返回 True,说明用户点了确定。此时用户选的打印机、份数、页码范围已经自动写进全局 Printer 对象,后续打印代码不需要再手动处理这些参数。

如果你还需要在打印前让用户设置纸张方向或大小,再拖一个 TPrinterSetupDialog,在打印前的设置流程里执行一次即可。这个对话框的好处是它能直接操作底层打印驱动,把纸张尺寸、进纸方式这些难搞的参数一起封装好,省去你在代码里和纸张常量纠缠的麻烦。

3. 核心对象 TPrinter 的参数与坐标系统

3.1 打印坐标与 DPI 换算

这一节是所有打印功能里的重灾区。很多朋友写的打印程序,屏幕上看着没问题,一打印出来字特别小或者位置特别偏,十有八九是栽在 DPI 换算上。

屏幕和打印机是两种 Resolution 完全不同的画布。屏幕通常只有 96 DPI,也就是一英寸里有 96 个像素点;而一台普通的喷墨打印机是 300 DPI,激光打印机则常见 600 DPI。同样一个 TextOut(100, 100) 的调用,在屏幕上大约排到 2.6 厘米开外,到了 600 DPI 的打印机上就只有 0.4 厘米了。

这就是为什么用屏幕像素直接写打印布局会翻车。正确的思路是用毫米来设计版式,再根据目标打印机的 DPI 换算成像素。换算公式很简单:

function MMToPixels(AMillimeters, ADPI: Integer): Integer; begin Result := Round(AMillimeters * ADPI / 25.4); end;

25.4 是一英寸对应的毫米数。一个 A4 纸宽度 210 毫米,在 600 DPI 设备上就是 Round(210 * 600 / 25.4),约 4961 像素。所以你打印画布上的坐标动辄上千上万,不要慌,这才是打印机世界的常态。

理解了这一点,再去看 Printer.PageWidth 和 PageHeight 就有底了。它们返回的是当前打印页面在打印机像素下的宽高,具体数值跟纸张大小、打印方向、打印机 DPI 三者都有关系。

3.2 选择打印机、纸张方向与份数设置

多打印机环境下,代码里要主动选设备。Printer.Printers 是一个字符串列表,保存了系统当前所有可用打印机名称。通过 PrinterIndex 可以切换当前设备:

procedure SelectPrinterByName(const APrinterName: string); var I: Integer; begin for I := 0 to Printer.Printers.Count - 1 do begin if Printer.Printers[I] = APrinterName then begin Printer.PrinterIndex := I; Exit; end; end; end;

纸张方向用 Orientation 属性控制,它是 poPortrait 或 poLandscape 两个值。设置横向打印就一句话:

Printer.Orientation := poLandscape;

注意,Orientation 必须在 BeginDoc 之前设置,而且在 BeginDoc 之后再改通常不生效。我的习惯是在打印流程入口就把它固定下来,中间不再变动。纸张大小我一般不在代码里硬编码,而是交给 TPrinterSetupDialog 处理,原因是不同系统的打印驱动对纸张常量的支持不完全一致,硬编码 A4 在一些老驱动上可能出现意想不到的行为。份数设置用 Printer.Copies 即可,但要注意有些打印机驱动本身会接管份数逻辑,代码里设了后最好在对话框里也保持一致。

3.3 多页文档的分页策略

打印内容超过一页时,需要自己控制分页。核心逻辑很简单:画之前先算一下这一行会不会超出页面底部,超了就 NewPage 开新页。看这段分页打印长文本的代码:

procedure PrintLongText(AText: TStrings); var I, Y, LineHeight: Integer; begin Printer.BeginDoc; try Printer.Canvas.Font.Name := 'Microsoft YaHei'; Printer.Canvas.Font.Size := 10; LineHeight := Printer.Canvas.TextHeight('高度测试') + 6; Y := 40; for I := 0 to AText.Count - 1 do begin if Y + LineHeight > Printer.PageHeight - 40 then begin Printer.NewPage; Y := 40; end; Printer.Canvas.TextOut(40, Y, AText[I]); Inc(Y, LineHeight); end; finally Printer.EndDoc; end; end;

LineHeight 是通过 TextHeight 实测出来的,再用它来决定行距,比写死一个像素值靠谱得多。分页的判断用 Y + LineHeight 是否超出 PageHeight - 40,40 是我留的页底边距,实际项目里你可以把它设计成一个可配置参数。

这里还有一个很多人容易忽略的点:跨页时要不要重复表头。常规报表,比如数据表格,翻到第二页时用户通常需要再看一眼列标题。这个需求在分页逻辑里要单独处理,我会在下一节的表格打印示例中演示具体写法。

4. 从文本到图文混排:打印内容的绘制方法

4.1 文本打印与字体控制

文本打印是基础,也是最容易出问题的环节。核心就是操作 TPrinter 的 Canvas 属性,设置字体、颜色、位置,然后调用 TextOut 输出。

先解决中文乱码。你在窗体上设的字体是宋体,在打印机画布上未必可用,尤其是 Linux 和 macOS 环境下。稳妥的做法是在打印时动态挑选一个可用的中文字体:

function GetChineseFontName: string; begin if Screen.Fonts.IndexOf('Microsoft YaHei') >= 0 then Result := 'Microsoft YaHei' else if Screen.Fonts.IndexOf('Noto Sans CJK SC') >= 0 then Result := 'Noto Sans CJK SC' else if Screen.Fonts.IndexOf('宋体') >= 0 then Result := '宋体' else Result := Printer.Canvas.Font.Name; end;

Screen.Fonts 收集了系统当前已安装字体,这里按优先级做了一次选择,能兼容常见桌面环境。这个方法我在几个跨平台项目里一直用,效果稳定。

做文本对齐时,Canvas.TextWidth 可以用来测量字符串宽度。想做右对齐效果,就拿页面宽度减去文本宽度和右边距:

procedure DrawTextRight(ACanvas: TCanvas; AText: string; XRight, Y: Integer); begin ACanvas.TextOut(XRight - ACanvas.TextWidth(AText), Y, AText); end;

居中和左对齐的原理完全一样,无非是把起始坐标计算出来。记住一个原则:所有视觉位置都基于实测文本尺寸计算,别靠目测拍脑袋。

4.2 表格打印:边框、表头与行高计算

打印表格比打印纯文本复杂一些,因为要画边框、处理表头重复、控制行高。我给你一个简化但完整的示例,核心数据结构是列宽数组和列标题数组:

procedure PrintTableDemo(AData: TStringList); const Margin = 50; var ColX: array[0..2] of Integer = (50, 220, 390); ColTitle: array[0..2] of string = ('订单号', '客户名称', '金额'); RowH, I, J, YPos: Integer; begin Printer.BeginDoc; try Printer.Canvas.Font.Name := 'Microsoft YaHei'; Printer.Canvas.Font.Size := 10; RowH := Printer.Canvas.TextHeight('行高测试') + 12; YPos := Margin; // 第一页表头 for I := 0 to 2 do begin Printer.Canvas.Brush.Color := clSilver; Printer.Canvas.Rectangle(ColX[I], YPos, ColX[I] + 170, YPos + RowH); Printer.Canvas.TextOut(ColX[I] + 5, YPos + 6, ColTitle[I]); end; Inc(YPos, RowH); for J := 0 to AData.Count - 1 do begin if YPos + RowH > Printer.PageHeight - Margin then begin Printer.NewPage; YPos := Margin; // 新页重新画表头 for I := 0 to 2 do begin Printer.Canvas.Brush.Color := clSilver; Printer.Canvas.Rectangle(ColX[I], YPos, ColX[I] + 170, YPos + RowH); Printer.Canvas.TextOut(ColX[I] + 5, YPos + 6, ColTitle[I]); end; Inc(YPos, RowH); end; // 数据行 for I := 0 to 2 do begin Printer.Canvas.Brush.Color := clWhite; Printer.Canvas.Rectangle(ColX[I], YPos, ColX[I] + 170, YPos + RowH); Printer.Canvas.TextOut(ColX[I] + 5, YPos + 6, AData[J]); // 这里按行数据取列 end; Inc(YPos, RowH); end; finally Printer.EndDoc; end; end;

这段代码把行高建立在字体实测高度上,而非写死数值,这样就算更换字号,表格也不会挤作一团。画边框用的是 Canvas.Rectangle,它会把边框和底色一起处理,所以要注意每次画数据行之前把 Brush.Color 恢复成白色。

跨页重复表头的技巧就在那段 if 判断里:一旦发现新页,重新输出一遍表头,再继续画数据。这个细节直接决定打印出来的报表是不是专业。

4.3 图片打印:缩放、居中、防止图片过大

打印图片时最常遇到的问题就是比例失衡或者超界。直接用 Printer.Canvas.Draw 画原图,图片多大就打多大,超过纸面内容直接被裁掉。正确做法是按页面可用空间等比缩放。

procedure PrintImageFitPage(AImage: TBitmap); var Scale, W, H, X, Y: Double; begin Printer.BeginDoc; try W := AImage.Width; H := AImage.Height; Scale := (Printer.PageWidth - 80) / W; if H * Scale > Printer.PageHeight - 80 then Scale := (Printer.PageHeight - 80) / H; X := (Printer.PageWidth - W * Scale) / 2; Y := (Printer.PageHeight - H * Scale) / 2; Printer.Canvas.StretchDraw( Rect(Round(X), Round(Y), Round(X + W * Scale), Round(Y + H * Scale)), AImage); finally Printer.EndDoc; end; end;

这里先按宽度计算缩放比例,再检查高度是否超界,超界就改按高度缩放,保证图片完整落在页面上。X 和 Y 的计算让图片居中。StretchDraw 用目标矩形直接把图拉伸画出来,是打印图片时最顺手的方法。

补充一句经验:如果图片只是装饰元素,不需要特别高的清晰度;但如果是二维码、条形码这类要求精确识别的图,最好保证原始分辨率足够,不要把位图放大过多,否则扫描设备可能读不出内容。

5. 打印预览:功能与实现思路

5.1 TPrinterPreview 组件的使用与限制

Printer4Lazarus 包里确实带打印预览组件,简单文档可以直接拖到窗体上用。你把它放到界面上,设置好对应关系,它会把当前打印任务的内容逐页渲染出来,用户可以翻页查看。这个组件的优势是省事,不需要自己写绘制逻辑。

但实际使用下来,我发现它的问题也不少。复杂报表通常包含大量自定义字体、边距、图片混排,这个内置组件的渲染效果有时候和真实打印机输出存在偏差。更麻烦的是,某些打印机驱动的 preview 接口差异较大,同一套绘制代码在 Windows 上预览正常,换到 Linux 上就变形。

所以我不太建议把它当作复杂打印功能的唯一预览方案。如果你的打印内容只是几行文字,用它没问题;一旦涉及表格、图片、跨页表头,就老实走下面的自绘方案。

5.2 自绘预览位图的推荐思路

自绘预览的核心思想很简单:把所有绘制逻辑封装成一个通用的绘制过程,打印和预览都调用它,只是传入的画布和缩放比例不同。下面是一个简化的框架:

procedure DrawPage(ACanvas: TCanvas; AScale: Double); begin ACanvas.Font.Name := 'Microsoft YaHei'; ACanvas.Font.Size := Round(12 * AScale); ACanvas.TextOut(Round(50 * AScale), Round(50 * AScale), '销售报表'); // 其余所有绘制代码,坐标和字号全部乘 AScale end; procedure PrintReport; begin Printer.BeginDoc; try DrawPage(Printer.Canvas, 1); finally Printer.EndDoc; end; end; procedure PreviewToBmp(APreviewBmp: TBitmap; AScale: Double); begin APreviewBmp.Width := Round(Printer.PageWidth * AScale); APreviewBmp.Height := Round(Printer.PageHeight * AScale); APreviewBmp.Canvas.Brush.Color := clWhite; APreviewBmp.Canvas.FillRect(0, 0, APreviewBmp.Width, APreviewBmp.Height); DrawPage(APreviewBmp.Canvas, AScale); end;

打印时传入 AScale = 1,真实尺寸;预览时传入 0.25 之类的比例,把整页缩到屏幕上。这个方法最大的好处是所见即所得:预览里出现的内容,打印出来必然一致,因为它们共用同一套绘制代码。

预览比例的选取有个讲究,0.25 或 0.333 都常用,主要是平衡清晰度和页面显示范围。字体大小在缩放时要 Round 取整,否则会出现奇怪的对齐模糊。这个自绘方案我用了很多年,稳定性和可控性都远超内置预览。

6. 常见问题排查与避坑实录

6.1 中文乱码和字体缺失

中文打印成方块、问号,几乎都是字体问题。打印机画布上如果选中一个不支持中文的字体,再好的编码方案也白搭。处理方式就是前文提过的 GetChineseFontName 动态选字体。另外,Lazarus 源码默认用 UTF-8 编码,TPrinter 内部在传送到后端时会做必要的编码转换,这部分一般不用操心,别把精力浪费在错误的排查方向上。

6.2 打印内容尺寸偏小或位置偏移

最常见的原因就是没做 DPI 换算。用屏幕坐标直接布局,到了高分辨率打印机上,内容就会缩小得很夸张。另外一个容易忽视的点是页面方向:设置横向后 PageWidth 和 PageHeight 会互换,如果你的布局代码先取了 PageWidth,可能打印到一半才发现纵横不对。请在 BeginDoc 之前确认方向,并重新读取页面尺寸。

问题现象常见原因解决办法
中文显示为方块打印字体不支持中文动态选择系统可用中文字体
打印内容偏小未做 DPI 换算按打印机 XDPI/YDPI 换算坐标
打印方向未生效BeginDoc 后设置 Orientation在 BeginDoc 前设置方向
打印任务卡住BeginDoc 后异常未 EndDoctry/finally 保证 EndDoc 执行
不同系统布局不同字体缺失导致回退运行时做字体检测

6.3 打印机列表为空或找不到打印机

程序拿到空打印机列表,先别急着怀疑代码。到系统设置里看一眼有没有安装任何打印机驱动,有没有设置默认打印机。虚拟机环境尤其常见这个问题,宿主机没有打印机,虚拟机里自然也是空的。开发调测的时候,最省心的办法是选择系统自带的 PDF 虚拟打印机,比如 Windows 的 Microsoft Print to PDF,或者 Linux 下 CUPS 自带的 PDF 输出设备。选中这个设备,打印过程会直接生成一个 PDF 文件,方便反复检查布局细节,比浪费纸张调试高效太多。

6.4 BeginDoc 后异常导致打印任务挂起

这是一个很隐蔽的坑。绘制过程中如果抛了未处理异常,EndDoc 没有被调用,系统会认为打印任务还在进行中,后续所有打印请求都排不上队。表现就是程序看起来不卡,但打印机队列里有个永远进行中的任务。处理方式就是在所有打印代码的外层套 try/finally,把 EndDoc 放在 finally 里。如果可能会被打断,也要及时调用 Printer.Abort 终止任务。

6.5 跨平台差异与建议

Lazarus 的跨平台能力很强,但打印模块在不同后端的行为不完全一致。Windows 下字体渲染相对丰富,Linux 下某些字体名称不存在,macOS 下打印对话框的外观和交互也和前两者有差异。我的经验是打印功能千万不要只在开发机上验证,至少要在 Windows 和 Linux 两个环境各跑一遍。条件允许的话,可以做一个回归测试脚本,把绘制结果输出成位图文件,人工比对各平台差异,能省下不少折腾时间。

最后说一点架构上的体会。我一开始做打印功能时,把整套画布绘制逻辑直接写在 Form 的按钮事件里,结果后来要加预览、要调整布局、要适配不同纸张方向,改动非常痛苦。现在我的做法是:单独用一个打印单元,接收业务数据和布局参数,对外只暴露 Print、Preview 两个入口,内部共用同一个 DrawPage 过程。Printer4Lazarus 本身并不复杂,把绘制逻辑拆好,后面就只剩接数据的事了。这套组件虽然看起来平平无奇,但在跨平台桌面项目里,它确实是打印环节最靠谱的一张底牌。

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

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

立即咨询