1. 先搞清楚:为什么SAP Script表单里要写preform
1.1 先理解SAP Script表单的执行模型
SAP Script是SAP里很老牌的一套打印表单方案。你在SE71里维护一个窗体,窗体里有主数据、段落格式、字符格式、页面和窗口,窗口里再挂上文本元素。真正打印的时候,后台是靠ABAP程序通过OPEN_FORM、WRITE_FORM、CLOSE_FORM这三个函数把文本元素一行一行解释出来的。
这里有个非常关键的认知误区:很多新手以为SAP Script就是纯文字排版工具,文本元素里只能写写静态文字、放放字段变量号。但实际上,在文本元素中是可以嵌入控制命令的,比如/: PERFORM xxx、/: IF xxx、/: ENDIF这类的行,它们会被表单解释器当指令执行。而这个/: PERFORM xxx调用的子程序,业内通常直接叫它preform,本质就是ABAP子例程(FORM...ENDFORM),只是它跑在SAP Script的上下文里,完成业务逻辑后再把结果输送到文本元素里展示。
明白了这个执行模型,你才能解释为什么有时候文本里放字段变量写不全逻辑,为什么有时要绕一大圈去做中间值处理。凡是遇到那种“要根据某个表里的值动态拼一段话”“要按条件显示不显示某段文字”“要循环输出多行数据”的需求,光靠文本元素里的变量号是不够的,必须借助preform把复杂逻辑算好,再把结果传回给表单打印。
1.2 preform和普通ABAP子例程的关联
preform这个词在SAP历练里叫法很多,有人叫子程序,有人叫打印子例程,其实就是FORM ... ENDFORM定义的本地块,只不过它被SAP Script的文本控制命令引用。你在SE80里写一个include程序,里面放一堆FORM,然后把程序名挂到表单上,文本元素就可以通过/: PERFORM name去调。
它和普通ABAP子例程在三点上是相通的:
- 参数传递机制完全一致,都支持USING、CHANGING、TABLES,引用传递和值传递规则照样适用;
- 内部可以正常使用ABAP语句,查表、循环、字符串拼接、类型转换、排序,想怎么用就怎么用;
- 调试方式也一致,能打断点、能看调用栈、能改参数值。
但preform有一个不太一样的点:它的调用入口既不在正常的ABAP程序里,也不在函数组里,而是由RSTXSCRP这个报表程序在解释文本元素时触发。这导致很多人直接用“从外部调用进入调试”的思维去处理,结果发现断点根本不进,卡了半天不知道问题出在哪。这也是我把调试单独拉出来写一大节的原因——preform开发中真正的痛点,往往不是写FORM,而是不知道怎么让它停下来给你看。
2. 实战起步:搭建一个带preform逻辑的打印表单
2.1 前期准备:表单框架与程序挂接
这里我拿一个非常典型的场景来讲:打印采购订单时,需要在抬头区域加一行动态备注,这行备注不是存在主数据里的,而是运行时候要根据供应商主记录、订单类型、当前日期等条件实时拼出来的。假设表单已经存在,我们要新增这个动态文本区域。
第一步,在SE71里打开目标表单。如果是新建表单,需要先维护基本页、主窗口这些框架;如果只是在已有表单上加功能,直接进入Pages页签,找到你希望显示动态文本的窗口,通常就是MAIN窗口。
第二步,在文本元素中把光标定位到要出现动态备注的位置。这时你直接写一行文字、放字段变量都没问题,但我们要用到preform,所以实际要写的不是最终文本,而是一个控制命令加参数占位。常见写法是这样:
/: PERFORM GET_VENDOR_NOTE 供应商备注:&NOTE&这里要注意,&NOTE&是SAP Script的字段显示语法,它会在运行时去取ABAP程序中同名变量或字段符号的值。问题在于,&NOTE&要能取到NOTE这个变量,这个变量必须存在于当前表单的执行上下文中。对SAP Script来说,表单上下文里的变量通常来自驱动程序的全局变量、表单程序里的全局变量,以及通过PERFORM...CHANGING传进去的参数。所以我们还得写一个专门的子例程来维护NOTE这个值。
第三步,把包含这个子例程的ABAP程序挂到表单上。在SE71菜单里选择“程序/表单”页签(或叫做“表单程序/包含程序”),维护一个程序名。我实际项目中最常用的方式是建一个专门的include程序,比如ZINCL_MY_FORM001,所有跟这个表单有关的FORM都放里面,然后把这个include程序挂上去。这里的坑在于,很多新人会把“挂上去”理解成“编译进去”,以为程序里写了就行。实际上表单不关心程序在哪见的代码,它只认你在“表单程序”维护框里填的那个名字。如果没填或者填的程序名下面找不到对应的FORM,调用时就会直接报错。
2.2 在文本元素中正确调用PERFORM
文本元素里调用preform的标准语法有很多变体,我实际用得最多的是这几种:
/: PERFORM GET_VENDOR_NOTE /: PERFORM CONVERT_NUM USING &KUNNR& CHANGING &NOTE& /: PERFORM FILL_ITEMS TABLES IT_ITEMS第一种是无参调用,看起来最简单,也最值得警惕。因为一旦没有参数传递,子例程里就只能依赖全局变量。全局变量在哪来?要么来自驱动程序的全局变量区,要么在表单程序声明部分用DATA定义的变量,要么通过别的PERFORM里面用CHANGING改的。新手最喜欢写无参调用,然后发现子例程里读不到任何数据,各种莫名其妙。我建议哪怕是简单的逻辑,也至少用CHANGING把一个主变量传进去再传出来,方便排错。
第二种带USING和CHANGING的调用是重点。注意&KUNNR&这种写法在文本元素里会先被解释成变量值,再把值传给FORM。也就是说,你写在文本元素里的&KUNNR&必须事先存在,否则会变成空串。想让这个变量存在,就得保证它或者来自驱动程序的全局变量,或者被之前调用的某个preform设置过。这也是为什么我经常把一连串的preform像一个流水线一样摆在文本元素开头,先预处理各种变量,再逐行输出到正文。
第三种TABLES传内表,在我自定义的打印场景中用得不多,但标准打印程序里很常见。它的调用把程序的内表直接引用传入子例程,子例程内部对表的所有操作都会反映回调用端。如果你不想要这种引用传递效果,就要么用CHANGING传,要么在子例程内部先复制。
这里还有一点要专门提醒:文本元素里写/: PERFORM时,行的开头必须是斜杠加冒号紧挨着,格式上稍微错位都可能导致整行被当成普通文字输出。我见过不止一次有人排版时在斜杠前多打了个空格,结果屏幕上直接把代码那行字打印出来了,半天没反应过来。
2.3 FORM子例程的参数定义与写法
跟文本元素对应的,ABAP端就要写FORM了。拿上面那个供应商备注的例子来说,最简单的版本长这样:
FORM get_vendor_note CHANGING cv_note TYPE string. DATA: lv_kunnr TYPE kunnr, lv_text TYPE string. IF vendor-maktx IS INITIAL. cv_note = '暂无备注信息'. ELSE. CONCATENATE '供应商:' vendor-name1 '备注:' vendor-maktx '日期:' sy-datum INTO cv_note RESPECTING BLANKS. ENDIF. ENDFORM.这里有几个地方是实战中容易翻车的:
第一,FORM名字的大小写问题。SAP Script解释器对PERFORM后面跟的名字,和ABAP里定义的FORM名字,匹配时看起来不区分大小写,但某些版本和某些场景下非常敏感。我自己的经验是:两边统一用小写、中间用下划线,别混用驼峰,省得在奇奇怪怪的地方踩雷。比如你写/: PERFORM Get_Vendor_Note,但在include程序里定义的是FORM GET_VENDOR_NOTE,虽然ABAP编译不报错,打印预览时就可能说找不到子程序。
第二,参数个数和顺序必须完全一致。文本元素里写USING &KUNNR& CHANGING &NOTE&,FORM定义就得是FORM xxx USING p_kunnr CHANGING p_note,个数、顺序差一个都会在执行时报“形式参数与实际参数不一致”之类的错误。这个错误在预览时经常不友好提示,直接弹一个信封差不多的通用消息,需要进调试才看得明白。
第三,在子例程里给参数赋值要注意类型的隐性转换。CHANGING参数如果定义成字符串,传进来的可能是个带前导零的订单号、日期串,使用前最好做格式处理。这里和热词里提到的“abap 检查是否为数值类型”正好对上了:在处理从表单文本元素传进来的数据时,如果你不确定它是字典型还是数值型,先做一次IS NUMERIC判断再转换,否则拼字符串时会出很多幺蛾子。
第四,如果你的preform里要处理内表,最稳妥的方式是把内表定义为STANDARD TABLE类型,然后用TABLES参数传入。但注意,TABLES传入的是引用,preform里对表的修改会影响调用方的原表,所以在打印逻辑里如果你只想临时排序、筛选,最好先复制一份,避免污染作业数据。这里可以合理用上ABAP的SORT语句,处理完再做打印输出,又顺手又不会破坏原表结构。
3. preform调试技巧:从守株待兔到精准命中断点
3.1 在SE71预览中进入调试的三种方法
前边说了,preform跑在RSTXSCRP的上下文里,所以你直接在SE71里点“预览”,默认情况下你放的BREAK-POINT是不会停下来的。这个坑我踩了很久,后来摸出三条路。
第一条路,也是最省事的:在文本元素里临时加一行/: PERFORM DEBUG_STOP,然后在这个子例程里放一个BREAK-POINT,预览时就能停住。但这条路的麻烦在于,你需要额外维护一个调试用的FORM,发布前还得记得删掉,容易遗漏,我一般只在紧急排查时用。
第二条路,利用SAP的调试触发机制。在SE38编辑那个include程序,把光标放在FORM第一行,点击“创建断点”。然后回到SE71预览界面,在表单的“其他设置”里把“调试”勾选上。不同版本这个菜单位置会变,但核心逻辑就是告诉表单解释器,我要进入ABAP调试器。勾选后,预览时系统会弹出一个消息框问你“是否启动调试”,选是,然后表单一执行到你打的断点就会停。这个方法稳定、不用改代码,是我日常最推荐的。
第三条路,如果你已经有一个驱动主程序在跑打印逻辑,比如ZREPORT调用了OPEN_FORM、WRITE_FORM,那你就在主程序里CALL FUNCTION 'OPEN_FORM'之前用/h打开调试模式,然后在调试器里设置断点到include程序的FORM。因为主程序是普通ABAP程序,所有函数调用都会跟着进去,preform自然也会被带进去。这个方法特别适合排查那种“为什么打印任务发不出来”的问题,因为你不仅能看到preform内部,还能看到驱动程序的整个调用链。
3.2 动态条件下的条件断点设置
很多时候preform会被循环调用多次,比如订单里每一行ITEM都调一次同一个子例程,你只在某一行数据异常时才想停下来看。这时候手动打断点然后反复按F8会崩溃,正确姿势是条件断点。
在SE38的调试器里,当你已经进入调试状态后,可以在顶部菜单“调试器→断点→创建”里选择条件断点,输入条件表达式,比如sy-tabix = 5、p_kunnr = '0000123456',或者iv_total > 1000。这样只有当条件满足时才会中断。
对preform来说,有一点和普通ABAP程序不同:你要判断的变量很多时候是FORM的形式参数,不是全局变量。在断点条件里直接写p_kunnr这种形式参数名,有的版本会提示变量不存在。我的办法是:在FORM里面第一行先用全局变量或实例变量接一下,比如gv_check = p_kunnr,然后断点条件写gv_check = 'xxx'。虽然多写一行代码,但调试条件就稳了。
另外,如果你要调试的目标FORM是在include程序里的,SE38的GUI版本里有时候断点不生效,请确认这个include程序有明确的编译单元。有的include只是纯粹被主程序或函数组包含,没有单独的主程序入口,这时候调试器可能不认。解决方法是:把断点打在包含它的主程序里,然后逐步Step Into到include中。
3.3 调用堆栈与参数值检查
断点停住之后,别急着看变量,先看一眼调用堆栈。在ABAP调试器里,调用堆栈窗口能完整展示当前调用路径:从RSTXSCRP的文本解释器,到某个函数模块,再到我们的include子例程,一层一层非常清晰。这个堆栈对定位问题是杀手级的。
我在现场帮同事排过一个怪问题:表单打印时备注里出现了一堆乱码,但是任何人查数据都觉得源表没问题。进调试后,断点停在preform里,堆栈显示这个FORM不是从文本元素的PERFORM进来的,而是被另一个FORM间接调用的,那个父FORM在调用前把某个全局变量改掉了。如果不看堆栈,光看子例程内部,永远找不到变量被谁污染。
参数值检查这一块,建议重点看两类变量:一是CHANGING出参在ENDFORM之前的值,二是文本元素里&变量号对应的字段值。尤其是后者,如果你发现子例程算出来的lv_note明明是“供应商:张三”,但打印出来却是空的,十有八九是你文本元素里写的变量名和子例程里实际赋值给CHANGING的参数名对不上。用调试器把表单上下文里的字段值列出来一眼就能看出来。
关于调试时的参数修改,也顺手说一句:SAP调试器允许你在调试中直接改preform局部变量的值,这在测试不同分支时非常节省时间。比如你有一个价格判断逻辑,正常数据永远是正数,你想验证负数和零的场景,直接在调试器里把传入的金额改成负数再按F8,就能模拟,不需要跑一遍业务去造数据。唯一的坑是:对值传递参数改了只在子例程内部有效,你要验证的是调用方拿到的结果时,得改CHANGING参数并在ENDFORM之后去看调用方的变量。
4. 高频问题排查实录与避坑清单
4.1 preform常见错误速查表
实战中我整理过一张表格,凡是SAP Script表单开发里遇到preform的问题,基本都能在里面找到对应项。
| 现象 | 可能原因 | 处理方案 |
|---|---|---|
| 预览直接报“找不到子程序” | PERFORM名字与FORM名不匹配,或程序未正确挂到表单 | 核对大小写与下划线命名,检查SE71“程序/表单”页签是否维护了include程序 |
| 打印时文本元素里出现了“/: PERFORM”整行文字 | 控制命令行的斜杠前缀前有空格或格式错误 | 删掉行首多余空格,确保该行以/:严格开头 |
| 子例程内变量全为空 | 没有传参,且全局变量未在表单上下文中就绪 | 改用USING/CHANGING明确传参,或在子例程里重新查表赋值 |
| 参数不匹配报错 | USING/CHANGING参数个数、顺序不对 | 调整文本元素调用行与FORM定义的参数保持一致 |
| 条件判断在文本元素里不生效 | 文本元素里的/: IF和/: ENDIF配对缺失,或条件变量未定义 | 检查控制命令配对,提前用调试器确认变量是否存在 |
| 断点不停 | 没在SE71预览设置里开启调试,或断点打在未加载的程序 | 用3.1节的三条路之一重新触发调试 |
| 同样的FORM,一个表单能调一个不能调 | 不同表单挂了不同的include程序 | 两边的“程序/表单”配置需要分别维护 |
| CHANGING参数在调用方没变 | 文本元素传参的是值传递,CHANGING用的是形参副本 | 确认传参类型为引用传递,或改用TABLES传内表 |
这张表我贴在公司内部Wiki上很多次了,每次带新人都先过一遍,能省掉一大半的低级提问。
4.2 两个让人抓狂的隐晦坑
第一个坑和全局变量作用域有关。SAP Script的表单上下文里,并不是所有ABAP全局变量都能直接显示为&变量名&。很多人以为,既然驱动程序的全局变量和include程序的全局变量都在同一次运行上下文中,那我在文本元素里直接>_ITEMS&显示内表总可以吧?结果输出一堆空白。原因是,SAP Script字段引用能访问的变量范围有严格限制,并不等价于“ABAP程序中的所有全局数据”。它通常只能访问表单驱动当前实例已导出的、且在表单上下文中被明确维护过的字段。而preform改写的全局变量,因为是通过引用传递进来的,很多时候是能显示的,但如果你依赖的是程序内部一个纯粹的局部全局变量,就可能什么都输出不了。
解决办法很简单,也符合良好实践:preform里算好的值,务必通过CHANGING参数明确传出去,不要依赖“我在子例程里改了全局变量,表单就该自动认”。我在做迁移和重构时,见过太多原来能跑的老表单,换个新驱动主程序后全局变量上下文变了,打印值就全没了,最后推倒重写一堆FORM才救回来。
第二个坑是文本元素里的行数与窗口大小不对应导致的“内容被截断”假象。这个不属于preform本身,但它经常和preform一起出现:你调用了preform生成了足够长的动态文本,但窗口里只留了固定两行的高度,结果文本被截断,看起来像逻辑没执行对。排错时先别急着怀疑FORM,先看一下窗口的高度和文本行的字号是否足以容纳超长内容。这里调试器帮不上忙,因为它显示的是变量值而不是排版结果,所以逻辑对了、输出截了的情况很误导人。我一般会在预览界面用“显示行结构”这种辅助功能,或者临时把窗口高度调大再预览,确认到底是不是布局问题。
4.3 判断什么时候不该用preform
最后再补一个方向性的经验:preform不是万能的,别什么都往里面塞。SAP Script表单里能直接通过字段变量号输出的值,就尽量不要绕一层FORM去算;只有涉及需要条件判断、循环拼接、查表汇总的时候,才把逻辑下沉到preform里。如果企业项目里有条件用SAP Smart Forms或Adobe Form,那又另当别论——新项目往往直接上更现代的打印方案,preform这种写法更多是在存量SAP Script表单维护中高频出现。
我经历过不少次“明明能显示字段值,但非要用PERFORM包一层,结果把自己绕晕”的案例。比如一个订单行项目金额,明明数据库字段就有,文本元素里直接&VBAP-NETWR&就行,偏要写个FORM查一遍再赋给CHANGING参数。这不仅没有带来任何灵活性,反而增加了调试成本。所以,在动手写preform之前,先问自己三个问题:这个值能不能直接在文本里引用?这个逻辑是不是必须要用ABAP的流程控制?这个子例程会不会被多处复用?如果都不是,那就别写。
5. 经验沉淀:让preform更好用的几个建议
5.1 命名规范:一眼看出这是表单子程序
我见过太多名字起得像随机数的FORM,比如FORM zform001,过两个月再看根本想不起来它是干嘛的。建议在命名上统一前缀,比如F_加业务对象加动作:F_GET_VENDOR_NOTE、F_CONVERT_PRICE、F_FILL_ITEMS。这样在文本元素里看到/: PERFORM F_GET_VENDOR_NOTE,文本编辑器里看到对应FORM,上下文一下就能对上。
还有个实用技巧:把文本元素里所有PERFORM调用集中放在窗口文本的最前部,当作“预计算区”,下面再放真正要输出的排版文本。这样阅读表单时,逻辑前置、输出后置,结构非常清爽。如果你把PERFORM散落在一大段文本中间,维护时容易看漏,而且调试时也不容易定位。
5.2 参数设计:尽量用CHANGING而不是全局变量
前面提到了好几次“通过CHANGING明确传参”,这里我再强调下背后的原因。SAP Script表单这种东西,最难维护的点在于它隐式依赖上下文。如果你在preform里大量使用全局变量,别人接手时根本不知道这个变量在哪个程序、哪段逻辑里被改过。但如果你把每个FORM的输入输出都挂在参数上,表单文本元素的PERFORM行就变成了一份最简单的文档:USING A CHANGING B,清晰告诉阅读者这个子例程吃进什么、吐出什么。
传递参数时还要注意内表和大字符串的性能问题。值传递会在每次调用时复制数据,如果循环里频繁调用,很浪费资源。这时候我建议要么用引用传递(在FORM参数上使用TYPE REF TO),要么把数据先整理好再一次性传给FORM。SAP Script本来就适合输出排版量不大的表单,别让preform把性能拖垮了。
5.3 最后分享一点小技巧
我个人在实际操作中的体会是:凡是涉及preform的功能改造,交付前一定要把“正常数据”“边界数据”“空数据”三种场景各打印一遍预览。这听起来像废话,但preform最容易出的问题就是平时数据都正常,一遇到供应商主记录没有维护备注、日期字段为初始值、内表为空这类边界,就会直接打出空行或者把字段名原样打出来。所以我在写FORM时,开头第一行往往先把参数判个空,能还给调用方一个默认值就还给默认值,宁可在表单里显示一句“暂无”,也不要让客户看到空白区域。
再有一个小技巧,就是善用SY-SUBRC和消息拼接来做运行日志。如果你在preform里查表失败、读不到数据,可以临时在ENDIF之前写一句CONCATENATE 'D' sy-subrc INTO gv_debug之类的代码,把调试信息传给页面某个隐藏的文本区域。等系统上线后,让用户把那个区域的文字截图发过来,比远程反复问“显示的什么”高效得多。这是我多次被一线用户反馈“就是这个界面不对”之后才摸索出来的土办法,但真的管用。
preform这种技术形态在SAP Script表单里还会存在很久,它不新潮,但在存量系统维护中非常实用。掌握好它的开发套路和调试手段,能让你在这类老牌打印表单面前做到心里有数,上手不慌。