首页 / 资讯中心 / 文章详情

Report Machine 2.6报表组件实战:从设计器到PDF导出的避坑指南

Report Machine 2.6报表组件实战:从设计器到PDF导出的避坑指南 ★ FEATURED ARTICLE
简介对于使用 Borland Delphi 3 至 7.1 的开发者Report Machine 2.6 是一款成熟的报表生成工具。这套组件支持在 IDE 内直接设计多列、分组、汇总及自定义样式并可接入数据库、XML、CSV 等数据源帮助项目快速产出高质量打印文件。压缩包共 488 个文件、2.1MB以 139 个 pas 源码、119 个 dcu 已编译单元和 71 个 dfm 表单为主体配合 res 资源、ini 配置、rc 脚本等既便于阅读运行逻辑也可直接安装使用。值得关注的是程序拥有 100% 开放源码开发者不仅能免费用于自身项目还可按需修改或扩展功能尤其适合有特殊报表格式要求或想深入理解报表引擎实现的人群。包内还附带 MAKEALL、MK、MKDLL 等批量构建脚本以及面向不同 Delphi/C Builder 版本的 bpk、dpk 工程文件版本切换与重新编译都很方便。目前已有 287 人学习或下载对正在维护老项目或需要轻量离线报表方案的工程师来说这套体积小巧、源码完整的历史工具仍具备很高的参考和实用价值。1. Report Machine 2.6 还在 C/S 项目里值钱一款能打的老牌报表组件Report Machine 2.6 的年纪比很多开发者的工龄都大但到了今天它在国内大量 C/S 管理信息系统里依然被用于财务打单、仓储备货、业务对账这些“最后两公里”的输出环节。它解决的核心问题是在 Delphi/C Builder 项目里用最小成本把数据集变成一张能打印、能导出、能存档的正式报表。适合三类人看这篇笔记正在维护存量项目、被要求加报表却不想动三层架构的开发者刚接手项目、需要判断这套报表还能不能继续用的新人以及在做技术选型时纠结“旧组件继续用还是推倒重写”的负责人。它不新但稳定、轻、社区踩坑记录足够多这本身就是选型时值得认真权衡的理由。2. 先摸清 RM 2.6 的运行机制设计器、脚本引擎与三段式渲染2.1 三层架构拆解Engine、Report 对象与 DataSet 各管什么用 RM 2.6 之前先理解它的三个层次否则后面调试起来会感觉全是黑匣子。最底层是报表引擎一般以 rmEngine 组件形式存在。它负责把报表对象的描述——行列坐标、字体、边框、单元格内容、表达式——转换成实际的打印坐标和导出指令。开发者平时不直接碰它但它决定了所有渲染环节的行为。第二层是面向开发者的报表对象最常用的是 rmGridReport 和 rmLibReport。rmGridReport 是网格型报表适合结构规整的明细表、分组表rmLibReport 是自由版式适合单据、票据、标签这类需要元素随意摆放的场景。两个对象最终都挂在窗体或数据模块上它们在设计器里编辑在运行期被驱动。第三层是数据桥接RMDataSet、RMDBDataSet 这类组件负责把 BDE、ADO 或自定义数据源里的记录集交给报表对象。这里有个新手容易犯的误解RM 2.6 的报表文件不只是布局描述它把数据源引用和脚本一起打包在 .rmd 文件里。所以运行期加载模板后如果客户机的数据源名称、字段名对不上预览界面会直接白屏而且不报具体错误这是它年代久远、错误提示不够友好的典型表现。报表从加载到输出的完整生命周期可以概括成三步LoadFromFile 读入模板Initialize 根据当前数据集重建行列结构PreviewModal 渲染预览。我一般建议把“加载模板-绑定数据-初始化-预览/导出”固定成一套操作顺序所有报表都按这个顺序走后面排查问题时思路会清晰很多。2.2 两种报表对象的差异rmGridReport 与 rmLibReport 的选型逻辑很多刚接触 RM 2.6 的人在两种报表对象之间拿不准其实判断标准很简单内容能不能规整地落成行列。对比维度rmGridReportrmLibReport布局模型网格行列驱动自由坐标与带区驱动典型场景明细表、分组表、汇总表单据、发票、标签、排班表数据组织每一行绑定记录对象绑定位置随意脚本复杂度较低关注行号与单元格较高关注对象坐标与事件导出兼容性PDF/Excel 导出更稳定自由版式导出易错位具体到选型我一般遵循一条原则只要内容能落成行列优先用 rmGridReport。它在导出 PDF 和 Excel 时几乎不需要额外处理列对齐而 rmLibReport 虽然版式灵活但导出时容易因为字体宽度、边距计算差异出现对象错位调试成本高一个量级。反过来如果做的是送货单、报销单这种“一个单据占一页纸、各个字段散布在固定位置”的版式硬用网格报表去拼反而别扭这时候 rmLibReport 才是对的选择。一个细节值得注意rmGridReport 的列数、行宽在设计器里已经确定但运行期数据记录数变化后需要调用 Initialize 重新计算“这个报表实际要打印多少行”。如果省掉这一步预览时容易出现多页空白或末页数据行被截断。2.3 报表脚本到底写了什么事件钩子决定执行顺序RM 2.6 的脚本是类 Pascal 语法挂在报表对象的事件上。它不算完整的编程语言但支持变量、函数、循环和基本对象访问足够应付分组、累加、动态显隐这些需求。常见事件按执行顺序排列Initialize报表初始化。在这里做全局变量清零、数据集绑定、初始参数设置。BeforeRow / AfterRow行级钩子分别在每行处理前后触发。分组判断、累加器更新都写在这里。BeforePrint页级钩子在每一页真正输出前触发。页脚内容、动态显示隐藏对象在这里控制。OnExport / BeforeExport导出前的干预点适合做字体替换、导出文件名拼接。下面是一个典型的 Initialize 脚本做累加变量清零procedure rmGridReport1Initialize(Sender: TObject); begin // 全局累加变量在报表重建时清零 totalAmount : 0; // 同时把报表标题里的日期刷新到当前系统时间 RMGridReport1.TextTo(0, 0, 统计日期 DateToStr(Date)); end;这段脚本的关键在于 Initialize 触发时机它在数据集绑定之后、分页计算之前执行。也就是说此刻数据集已经打开但报表还没开始逐行处理适合做“整个报表只执行一次”的初始化工作。TextTo 的作用是把文本写入指定坐标的单元格参数分别是列号、行号、内容如果不调用 InitializeTextTo 写入的内容可能被后续的行刷新机制覆盖。脚本编译失败时运行期预览会弹出脚本错误框并定位到具体行号这是全流程里最有价值的排错信息。我见过不少同事在脚本报错后回去翻设计器属性其实应该先看错误框指向的行号那才是问题根源。3. 把第一张报表跑起来安装、最小示例与两种交付方式3.1 环境准备组件的安装顺序决定你后面省不省事在 Delphi 里装 RM 2.6 不算难但顺序错了会消耗大量时间。我的固定步骤如下第一步把组件包解压到不含中文、不含空格的目录比如 D:\Components\RM2_6。老组件对中文路径的兼容非常玄学目录带中文时编译期可能找不到 DCU 文件报错信息又模棱两可。第二步在 IDE 的 Component Install Packages 里添加运行期包和设计期包。先装运行期包再装设计期包顺序反了可能导致组件面板出现重复项或图标缺失。装完后重启 IDE这一步很关键因为设计期包的注册信息需要 IDE 重启后才能生效。第三步把组件目录下的 Lib 子目录加入 Tools Environment Options Library 的搜索路径。否则编译工程时会出现找不到 RM_*.dcu 的错误。第四步新建一个窗体确认组件面板多了一个 RM 页拖一个 RMGridReport 到窗体上。双击组件能打开设计器说明安装成功。安装完成后我建议先做一个最小验证新建一个空工程拖一个 RMGridReport在设计器的数据集属性里随便绑定一个本地表预览一次。这一步花不了五分钟但能把“组件没装好、设计器打不开、脚本引擎异常”这三类环境问题在写业务代码之前全部暴露出来。3.2 最小示例从外部模板跑通“加载-绑数据-初始化-预览-导出”环境没问题后按下面这段代码建立一个最小可运行示例。它的作用是从外部 .rmd 模板加载报表绑定 ADO 数据集预览并导出 PDFprocedure TForm1.Button1Click(Sender: TObject); var ReportPath: string; begin // 模板放在 exe 同级的 templates 目录下 ReportPath : ExtractFilePath(Application.ExeName) templates\rpt_demo.rmd; // 1. 指定模板路径并加载 RMGridReport1.ReportName : ReportPath; RMGridReport1.LoadFromFile; // 2. 绑定数据集必须在 Initialize 之前完成 RMGridReport1.DataSet : ADODataSet1; ADODataSet1.Open; // 3. Initialize 会根据当前数据行数重建报表结构 RMGridReport1.Initialize; // 4. 模态预览由用户在预览窗口里决定打印或导出 RMGridReport1.PreviewModal; end;这段代码的顺序是硬约束先 LoadFromFile、再绑数据集、再 Initialize、最后 PreviewModal。如果数据集还没打开就调用 Initialize报表会按零行数据计算分页预览结果是一张空表如果 LoadFromFile 之后不调用 Initialize数据集换了但报表的行数还是模板里保存的旧值最典型的表现是“预览第一页数据是对的后面全是空行”。参数说明ReportName 传入的是完整路径这里用 ExtractFilePath(Application.ExeName) 拼出当前程序所在目录是为了避免路径写死导致部署到其他机器时报“无法打开模板”。模板加载成功后PreviewModal 是阻塞式调用用户关掉预览窗口后代码才会继续往下走如果不想阻塞可以换 Preview 调用但那样打印任务和界面会同时进行容易触发句柄冲突我一般不用。如果要跳过预览直接导出 PDF可以改成RMGridReport1.ExportToPDF(OutputDir rpt_demo.pdf, True);其中最后那个布尔参数表示“导出完成后是否立即用系统默认 PDF 阅读器打开”。正式环境里我更习惯传 False因为客户机上不一定装了 PDF 阅读器自动打开反而会弹出错误对话框。3.3 两种交付形态内嵌模板与外部 .rmd 的取舍RM 2.6 的报表模板有两种存放方式取舍不同直接影响后期维护成本。第一种是模板内嵌在窗体或数据模块的 DFM 里。做法是设计器里设计完成后不调用 LoadFromFile直接让组件使用设计时保存的模板。优点很明显exe 单文件交付不存在模板文件丢失的问题缺点是每次修改报表样式都要重新编译整个程序实施人员在现场改不了任何布局只能干等开发发新版。第二种是模板作为独立 .rmd 文件放在程序目录下。运行期通过 ReportName 指定路径加载。优点是修改版式只替换文件不需要重新编译实施人员经过简单培训就能调整表头、列宽、字体缺点是要处理好三件事路径一致性、版本覆盖、权限。客户机上模板文件被杀毒软件隔离、旧模板覆盖新模板导致样式回退都是实际发生过的问题。我的常规做法是开发验证阶段全部用外部 .rmd 文件方便反复调整验证通过后把最终模板嵌入到程序资源里随 exe 发布输出端用 LoadFromResource 加载避免客户现场丢文件。这样兼顾了开发效率和交付可靠性但牺牲了一点现场改样式的灵活性。如果项目里实施人员确实需要经常调样式那就保留外部文件方案同时部署一个模板版本管理工具覆盖前自动备份旧文件这就是后悔药。4. 导出 PDF/Excel 的避坑清单字体、合并单元格与五个高频坑4.1 导出器的工作流程为什么“先初始化再导出”是底线RM 2.6 的导出模块在内部把报表引擎的页描述逐页翻译成目标格式。PDF 导出器直接解释页面坐标Excel 导出器则通过单元格接口逐格写入。这意味着一个事实导出效果严重依赖 Initialize 之后的内存页面模型。如果数据没刷新就导出导出文件里可能就是上一次数据的残留如果报表对象内部还处于“设计态”而非“运行态”导出的 PDF 可能是空页。我一般在导出前固定做三件事// 先加载模板 RMGridReport1.ReportName : rpt_sales.rmd; RMGridReport1.LoadFromFile; // 再绑定数据并初始化 RMGridReport1.DataSet : ADODataSet1; RMGridReport1.Initialize; // 最后执行导出 RMGridReport1.ExportToPDF(rpt_sales.pdf, False);这三段顺序里Initialize 是导出前的最后一道门槛。它同时重算行数、列宽、分页和字体映射跳过它导出的文件大概率是“半成品”。尤其在批量导出多个报表时每个报表都要独立走一遍这套流程不能图省事共用一个报表对象连续导出不同模板。4.2 PDF 导出中文字体映射与页边距设置PDF 导出最典型的问题是中文变成方块。原因不复杂2.6 的 PDF 导出器在生成文件时要把字符映射成字体轮廓如果找不到能覆盖汉字的系统字体就会退化为占位符。预览界面用的是 Windows GDI 字体显示正常导出器直接走字符映射字体不匹配就露馅。处理方法有两层。第一层在设计器里解决报表里所有文本对象的字体统一设置为系统中文字体最常见的是宋体。注意字体名必须和客户机系统字体完全一致大小写都别错否则导出器同样不认识。第二层在代码里兜底用字体替换函数把报表里的自定义字体统一替换成中文字体// 把报表中名为 MyFont 的字体统一替换为宋体 RMGridReport1.ReplaceFont(MyFont, 宋体);ReplaceFont 常见做法是遍历报表内所有文本对象当某个对象的字体名匹配第一个参数时改成第二个参数。这里有个易错点ReplaceFont 必须在 Initialize 之后调用否则报表重建时字体又被重置回模板里的默认值。执行完替换后建议补一句 Initialize让替换结果固化到页面模型里。页边距问题也常出现在 PDF 导出中。RM 2.6 的 PDF 页面尺寸默认沿用当前打印机的纸张设置如果客户机没有安装设计时用的打印机PDF 的纸张大小可能退化成 Letter 而不是 A4导致右侧或底部多出空白。解决方法是显式指定纸张RMGridReport1.PageSetup.Orientation : poLandscape; RMGridReport1.PageSetup.PaperSize : 9; // 9 在多数版本里对应 A4注意 PaperSize 的枚举值在不同版本里不一定相同写代码前先在当前安装包里查一下常量定义表别直接抄网上的数值。横向报表尤其要确认 Orientation这是后面第 4.4 节里会展开的坑。4.3 Excel 导出合并单元格、列宽与大数据量导出Excel 导出是另一个重灾区。RM 2.6 的 Excel 导出器按单元格逐格写入对合并单元格支持相当有限跨页表头的合并区域在导出后经常被拆成断裂状态列宽也只能保留整数精度导出后需要人工微调。我的处理原则客户要“继续加工数据”时导 CSV客户要“存档看不改”时导 PDF尽量不导 Excel。如果项目里确实必须导 xls那就接受两个现实一是大数据量导出的时间可能是分钟级两三万行数据卡上半分钟是常态二是合并单元格的样式还原度六成上下关键字段要多看一眼导出结果。此外导出 Excel 前最好关闭客户机上的 Excel 进程否则导出组件与已打开的 Excel 实例交互时可能因为 OLE 冲突直接崩溃。这属于典型的“环境坑”代码里没办法规避只能通过操作规范来约束。4.4 五个高频坑现象、原因、解决坑一PDF 里中文全部变成方块现象报表在预览窗口里一切正常点导出 PDF 后打开文件发现中文全成了方块英文和数字正常。原因导出器在做字符映射时没有可用的中文字体文本对象的字体名在客户机上不存在或字体名称大小写不匹配。解决设计器里所有文本对象统一用系统中文字体导出前用 ReplaceFont 把自定义字体替换成宋体替换完成后再调用一次 Initialize 固化。格式化输出后必须抽查首页和末页确认汉字轮廓正确。坑二导出 Excel 非常慢甚至卡死现象一个两三万行的明细表导出 xls进度条长时间不动CPU 占用高Excel 弹“未响应”。原因导出器走的是 OLE 逐单元格写入数据量越大写入次数越多性能呈线性恶化实时杀毒还会逐格扫描进一步放大耗时。解决数据量大时先导出 CSV 或 TXT 让客户自己转格式如果必须导 xls把报表拆成多个小文件导出再用脚本合并不要在一个报表对象里硬撑。坑三横向报表打印/导出时右边少一列现象设计器里表格完整打印机出纸后最后一列被截断或者在 PDF 里最后一列跑到第二页。原因图纸方向还是纵向报表内容宽度超过页面可用宽度部分客户机默认打印机纸张设成了 Letter可用宽度比 A4 窄。解决在代码和设计器里同时把方向设为横向并显式指定 A4。注意设计器里的 Paper Setup 和代码里的 PageSetup 必须保持一致否则代码会在运行期覆盖设计器设置。RMGridReport1.PageSetup.Orientation : poLandscape; RMGridReport1.PaperSize : 9; // 以当前安装包的常量表为准坑四预览总显示上一次的数据现象同一个报表对象先打开数据集 A 预览正常换成数据集 B 后预览第一页还是 A 的数据刷新无效。原因数据集换了但报表对象没有重新 Initialize行列缓存、分页数据都停留在上一次运行态。解决切换数据集后强制调用 Initialize。如果行数仍不对先把 RowsCount 置为 0 再调用 Initialize强制清空内部行索引。这一步是解决“残留数据”最直接的手段。坑五连续导出多张报表页码不重置现象用同一个报表对象连续导出两个 .rmd第二张 PDF 的第一页页码显示“第 3 页”而不是“第 1 页”。原因页码计数是引擎级变量报表对象切换数据后没有同步重置页码在上次基础上继续累计。解决每张报表导出前独立执行一次 Initialize如果仍不重置查找脚本里有没有手动修改页码变量的代码特别是 BeforePrint 事件里对页码变量的赋值删掉后在设计器里改用系统内置页码字段。5. 真实报表的复杂度分组、交叉表与主子报表的落地做法5.1 分组小计与合计脚本钩子里的累加和换页业务报表十有八九要分组小计RM 2.6 里最常见的实现方式是在行级脚本钩子里做累加和换页判断。下面这段脚本按部门编号分组部门切换时输出小计并重新开始累加procedure rmGridReport1BeforeRow(Sender: TObject); var curDept, prevDept: string; begin // 读取当前行与上一行的部门编号 curDept : RMGridReport1.GetCellValue(0, RMGridReport1.RowNo); prevDept : RMGridReport1.GetCellValue(0, RMGridReport1.RowNo - 1); // 部门发生变化时在上一组末尾写出小计然后重置累加器 if curDept prevDept then begin RMGridReport1.TextTo(2, RMGridReport1.RowNo - 1, 小计: FloatToStr(subTotal)); subTotal : 0; RMGridReport1.NewRow; end; // 把当前行金额累加进小计 subTotal : subTotal StrToFloat(RMGridReport1.GetCellValue(2, RMGridReport1.RowNo)); end;参数说明GetCellValue 返回指定坐标单元格的文本内容第一个参数是列号第二个是行号RowNo 是当前数据处理到的行索引TextTo 把字符串写入指定单元格NewRow 在当前行下方插入一行。这段脚本的逻辑是“边打边判断”遍历每个数据行发现部门变了就把上一组的小计写到上一组最后一行下面然后插入一行空白行把两组隔开。subTotal 是脚本变量区声明的全局变量需要在 Initialize 事件里清零否则多张报表连续预览时累加器会带着上一张的数据跑。这种写法在报表行数不多、分组层级不深时够用维护属于“看得懂但有点碎”的水平。分组层级多、需要多级合计时我更建议在设计器里使用分组头尾带区在带区的数据集属性里指定分组字段运行期引擎会自动处理分组脚本只负责小计计算。把“换页、插行”这类结构逻辑交给带区把“累加、格式化”留给脚本两边职责清楚后期改起来不费劲。5.2 交叉表把翻转逻辑前置到 SQL报表只做渲染交叉表这类“行转列”的需求RM 2.6 的交叉表组件能力有限动态列需要写大量脚本做单元格拼接调试成本很高。我的建议是除非列数固定且业务不会再增加维度否则不要在报表里做交叉把翻转逻辑前置到 SQL。固定列数的典型写法SELECT dept_id, SUM(CASE WHEN MONTH(create_date) 1 THEN amount ELSE 0 END) AS m1, SUM(CASE WHEN MONTH(create_date) 2 THEN amount ELSE 0 END) AS m2, SUM(CASE WHEN MONTH(create_date) 3 THEN amount ELSE 0 END) AS m3 FROM biz_order GROUP BY dept_id这段 SQL 做的事情是把月份作为列方向维度用 CASE WHEN 把每个月的数据拆到独立的 m1、m2、m3 列上。报表端只拿到一个规整的结果集直接在 rmGridReport 里加三列绑定字段即可脚本量趋近于零。报表端动态加列的写法也能实现交叉表但脚本要处理列号自增、列标题动态写入、数据行定位复杂度是指数级上升。我见过有人在脚本里写了七十多行实现动态列交叉表结果隔两个月业务加了两个维度排错排到凌晨。从那以后我的原则就是能在 SQL 里翻的绝不在报表里翻数据库的扩展能力和调试手段都比老报表脚本强得多。5.3 主子报表主表定位变化时子表如何跟着变主子报表的经典场景是订单头加订单明细主表位置滚动时子表要同步显示当前订单对应的明细记录。RM 2.6 里我用的方案是 rmLibReport 做主表版式内部嵌一个 RMGridReport 子对象子对象的定位跟随主表记录。关键代码写在主版的 BeforePrint 事件里procedure rmLibReport1BeforePrint(Sender: TObject); begin // 主表当前记录定位完成后按主键过滤明细 ADOQueryDetail.Close; ADOQueryDetail.SQL.Text : SELECT * FROM order_detail WHERE order_id IntToStr(ADOQueryMaster.FieldByName(order_id).AsInteger); ADOQueryDetail.Open; // 明细数据集变化后子报表需要重新初始化 RMGridReportDetail.Initialize; end;参数说明ADOQueryMaster 是主表查询order_id 是两张表的主外键ADOQueryDetail 是明细查询RMGridReportDetail 是嵌在 rmLibReport 里的子网格报表它在同一张单据页面上固定占一块区域区域大小在设计器里调好。为什么把这段逻辑放在 BeforePrint 而不是 Initialize因为 BeforePrint 在每一页输出前都会触发此时主表记录指针已经定位到当前页对应的订单子报表按这个主键过滤明细才能保证“这一页打印哪个订单子表就是哪个订单的明细”。如果放在 Initialize 里只会执行一次而后面的页面在打印时主表记录已经滚动子报表内容就错位了。要注意的是子报表重新 Initialize 的代价是重新计算分页如果明细行数特别多每页都 Initialize 一次会有性能损耗。遇到这种情况我会把主表记录分组输出让一个订单的所有明细尽量集中在同一页或连续几页减少重复初始化的次数。6. 交付前的三查清单把预览、PDF、打印三个风险点一次摁住前面各章把 RM 2.6 的安装、开发、导出和踩坑都过了一遍最后一章不聊新功能聊一个我已经坚持了很多年的交付前三查清单。每次给客户发版本前我会强制走一遍这三项检查每项都只在容易翻车的地方设检查点。检查项检查内容典型问题表现纸张与方向设计器 PageSetup、代码里的 PageSetup、客户机默认打印机纸张横向报表缺列、PDF 纸张变成 Letter字体映射PDF 导出后抽查首页、中间页、末页的中文与数字中文方块、字体重叠、数字错位数据刷新连续打开多张报表确认每张第一页数据都是新数据残留上一次报表的数据、页码不重置纸张检查不是单纯在代码里看一眼我通常在放行前用批量导出脚本把所有交付模板跑一遍 PDF重点看横向报表和含窄列的报表。这里有一段我用于批量自检的 Delphi 代码procedure TSelfCheckForm.btnCheckClick(Sender: TObject); var i: Integer; begin // FileList 里是所有要交付的 .rmd 模板路径 for i : 0 to FileList.Count - 1 do begin RMGridReport1.ReportName : FileList[i]; RMGridReport1.LoadFromFile; RMGridReport1.Initialize; RMGridReport1.ExportToPDF(OutputDir IntToStr(i) .pdf, False); end; end;这个循环不是自动化测试它只做一件事让每一个模板在无人值守的情况下跑过“加载-初始化-导出”全流程。如果某个模板 PDF 生成失败、导出来是空页、或者内容被截断代码会停在那一轮错误信息就是排查线索。我一般会顺手把 PDF 文件按模板名重命名并编好序号方便对检查结果关联回原始报表文件。这轮自检解决不了所有问题但能在发版前把两大类问题拦下来模板文件结构损坏、数据集绑定字段错误。字体和纸张这类和客户机环境强相关的问题自检代码拦不住需要配合部署说明文档在客户现场装完第一版时人工跑一张典型报表确认。我把字体和方向检查写进部署说明文档的验收一节让实施人员照着打一张“验收专用报表”只要这张表方向对、中文正常、数据对就默认环境没问题。印象最深的一次交付客户现场打出来的所有单据都向右偏移折腾一晚上最后发现是客户机默认打印机纸型设成了 Letter而报表设计器里一直是 A4 横向。从那以后我每次交付报表都强制把三查清单走一遍先看纸、再看字体、最后看数据刷新。这三步看着笨但真的帮我挡掉过好多次低级事故多花十分钟少返一次工。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站