069 、报表格式设计(SKIP、ULINE、FORMAT)
今天处理一个客户报上来的问题,程序跑完,ALV那份好好的,偏偏用WRITE输出的清单页面上,横线歪七扭八,空行时多时少,有的行像得了帕金森一样抖。我第一反应是数据算错了?结果一查,根本不是数据的问题,是格式语句没控制住。尤其是SKIP和ULINE这两个,看着人畜无害,用起来全是脾气。正好接着这个调bug的劲儿,把报表格式设计这块儿掰开揉碎讲一讲,都是平时踩过的坑,不是教科书上那种“本语句用于输出空行”的废话。
先说SKIP。它的作用就是往输出光标位置上怼空行,最简单的写法是SKIP,不带参数,等价于换一行。想多换几行就SKIP 3,在屏幕第几行开始输出那就是SKIP TO LINE 10。听起来挺憨的吧?但坑就在这个“TO LINE”上。你写SKIP TO LINE 10,意思是把当前输出行直接跳到第10行,这没问题,可你要是前面已经输出了一堆内容,屏幕滚动到底部,再执行这个跳转,它会在当前页找不到第10行——因为当前页的屏幕行已经被你写满了,它就只能换页后再找第10行,有时候就硬生生给你插出一整页空白。我当时调试那会儿就是循环里写了SKIP TO LINE 2,结果每打一组数据就多出一页纸来,差点把打印机干冒烟。
正确的姿势是,跨页跳行前先想清楚页面结构。比如每页固定打印表头,然后内容往下走,这时候你用SKIP TO LINE就要计算好当前已经占了多少行。另外,SKIP如果带变量,比如SKIP v_lines,v_lines如果是0,那就等于没跳,但是如果是负数,系统会直接当0处理,不会报错——这个你记住,别想着用负数做“回退空行”,世上没有后悔药,屏幕输出方向永远向下。
再有一个细节:SKIP TO LINE之后,你原先设置的光标位置的缩进(比如用POSITION或者WRITE AT设定的列偏移)不会自动重置。也就是说,你跳到第10行,但你上一次WRITE AT 5设置的列起点还在那,下一条WRITE还会从第5列开始。这不算bug,但很容易让你误以为“跳行后应该回到第1列”。解决办法就是每次关键输出前明确写WRITE AT的列号,或者用新的一行开始,别偷懒。我见过好多新同事在这个地方抓耳挠腮,最后发现是上一行的残留位置作祟。
然后说ULINE。它的作用是画一条横线,默认从当前行开头一直画到系统设定的行宽末尾。关键参数是ULINE AT 5(10),意思是从第5列开始,画10个字符那么长的横线。这里有个大坑:括号里的长度是字符个数,不是像素,而且如果长度超过了当前行的可用宽度,它不会自动换行,也不会截断,它会在同一行上继续画,直到把整行都铺满,然后跑到下一行接着画。也就是说你写ULINE AT 1(200),屏幕宽度假设80,它会把第一行画满,然后继续在第二行画到第200个字符位置。客户看到的就是一条莫名其妙的断线再加一条长线。这种问题打印出来特别难看,因为屏幕上还能滚动看出破绽,纸面上就俩字:闹心。
另一个坑是ULINE和WRITE同行混用。比如你想写一段文字然后接一条下划线,写成WRITE 'Total:',再ULINE AT 10(5),这没问题。但你要是写成WRITE: 'Total:', ULINE AT 10(5),那ULINE会被当成一个字段内容输出,而不是画线指令。ABAP的WRITE语句列表里,每个元素都按输出方式处理,你混合了两种语法,它就懵了。所以规矩就是:画线单独用一行语句,别跟WRITE的字段列表混在一起。这个我当年也试过,屏幕上是“Total:——–”,看着还行,但中间没有空格,排版完全是崩的。
ULINE还可以用ULINE AT (col) (len)这种动态形式,括号里的col和len都是变量,这时候注意变量的类型必须和系统平台的字长匹配,最好定义成TYPE I。如果你用了TYPE C的字符串,系统会先尝试转换成数字,转换失败就默认为0,结果画线位置跑到第一列,长度也变成0——等于什么都没画。你看,代码没报错,功能却悄悄消失,这就是ABAP里最阴的那种错误。
现在轮到FORMAT。FORMAT不是用来输出东西的,它是用来设置输出格式的。常见的用法是FORMAT COLOR COL_TOTAL INTENSIFIED ON INVERSE OFF。这一行命令会把之后所有WRITE输出都染上颜色、加亮或者反白。重点在于:FORMAT设置的是“之后所有输出”的全局状态,它不自动恢复。也就是说你写了一个FORMAT COLOR COL_TOTAL,紧接着三个WRITE都是这个颜色,等你想换回默认格式,你得再写一个FORMAT COLOR COL_BACKGROUND INTENSIFIED OFF。很多人忘了这一步,结果整篇报表从头到尾都是蓝底白字,客户还以为你的程序在给大海做宣传。
FORMAT里还有一个隐性陷阱:INVERSE和INTENSIFIED组合时,颜色会变。比如FORMAT COLOR COL_NEGATIVE INVERSE ON,那背景和前景会反转,原来应该是红底白字,反转后变成白底红字。如果你不熟悉这个效果,调试时看到一片刺眼的白色,别急,先看看是不是自己开了INVERSE。另外,FORMAT的ON和OFF是给INTENSIFIED和INVERSE用的,但COLOR后面不跟开关,它直接给颜色代码。常见颜色有:COL_BACKGROUND(默认背景色)、COL_HEADING(表头颜色)、COL_TOTAL(合计行颜色)、COL_KEY(关键字颜色)、COL_POSITIVE(正数颜色)、COL_NEGATIVE(负数颜色)。这些颜色具体是什么RGB,ABAP没告诉你,不同主题下表现还不一样,所以别指望它能在所有电脑上看起来一模一样。
还有一个细节容易忽略:FORMAT也会影响WRITE输出的对齐方式。比如你之前用FORMAT LEFT,那后面所有WRITE都左对齐,如果你中间用了WRITE … RIGHT这种附加项,它只对该条WRITE有效,不影响FORMAT的全局设置。但如果你没有在WRITE里指定对齐,它就用FORMAT的设置。这就意味着,你写了一个FORMAT CENTERED,然后后面所有数据全居中,数字对齐乱七八糟。我自己的习惯是:每个报表的格式控制都在输出主循环里,每次循环开始时用FORMAT RESET先重置所有格式,然后再设置需要的。可是ABAP没有FORMAT RESET这个指令?其实有,但写法是FORMAT RESET,它会把所有格式属性恢复默认值。注意,这个RESET不是官方文档里常提的东西,有些老程序员也不知道,因为默认情况下系统不会主动重置FORMAT状态。
你可能会问,既然FORMAT这么容易污染全局,有没有办法只在一条WRITE上加颜色?有,用WRITE … COLOR COL_TOTAL这种附加项,它只对该条WRITE生效。但问题在于,如果后续还有FORMAT设置的全局颜色,那条WRITE的颜色会被FORMAT覆盖。所以最安全的做法是:要么完全依靠WRITE附加项,要么完全依靠FORMAT,不要混着来。混着用早晚会翻车。
还有一点关于ULINE的颜色,如果前面设置了FORMAT COLOR COL_TOTAL,那么ULINE画出来的线也会是那个颜色。但ULINE本身不能直接指定颜色,除非你用WRITE来模拟线,比如WRITE一串’-'。这个技巧在需要不同颜色的分割线时很管用,但性能差那么一点点,报表里几百行也就无所谓了。
再想想SKIP和FORMAT的交互。如果你用了FORMAT INTENSIFIED OFF,然后执行SKIP,空行不会受影响——空行是空白的,没有颜色属性。但如果输出行里既有文字又有下划线,颜色不一致会显得很破。比如文字是蓝的,线是黑的,客户第一眼就觉得“你这程序是不是有问题”。这时候你得给ULINE也设置相同的颜色,只能通过前面的FORMAT来整体设置,所以你在画线之前写一个FORMAT COLOR COL_TOTAL,画完线之后立刻FORMAT RESET,否则后面所有输出全变色。
写这个长篇大论,不是让你背语法,而是想让你建立一种意识:报表格式设计本质是“状态机”。SKIP改变垂直位置,ULINE画水平线,FORMAT设置渲染状态。三个功能互相影响,而且没有作用域,没有面向对象,没有恢复栈,全靠你手动控制。所以看一个老项目打印程序,前几十行经常是一大堆FORMAT RESET加SKIP TO LINE 1的初始化,就是为了把状态捋顺。
我记得刚学ABAP那阵儿,师傅扔给我一个报表程序让我改格式,我改了半小时,屏幕上一会儿多一条横线一会儿少几个空格,最后还是师傅过来,在循环开头加了一句FORMAT RESET,世界就清净了。那一瞬间我明白,很多格式问题不是你不会写,而是你忘了“复位”。从那以后我写任何输出逻辑,第一行必是FORMAT RESET,哪怕当时根本没用过FORMAT。为什么?因为别人可能在你的代码之前已经污染了环境。你在这函数里做WRITE,鬼知道外面有没有留了个INTENSIFIED ON。
今天的代码示例我就不贴长篇了,给你一个最小可用的循环结构,注释里都写着每个改动的坑:
FORMAT RESET. " 先洗地,防脏东西
LOOP AT gt_data INTO gs_data.
SKIP 1. " 每条数据之间空一行,别用SKIP TO LINE 2,碰页就翻车
WRITE: / gs_data-name.
ULINE AT 5(30). " 画一段30字符长的线,位置在第5列
FORMAT COLOR COL_TOTAL. " 这里之后都变合计色
WRITE: gs_data-amount.
FORMAT RESET. " 用完立刻恢复,不然下条记录也带着颜色
ENDLOOP.
注意ULINE AT 5(30)后面的括号位置,这是固定语法,不能写成ULINE AT (5)(30),除非5和30是变量。别难为一个常量。
最后给你个实战经验:在打印前,先在屏幕上开一个F8预演,把SY-LINNO打到某个调试字段里,看看每一行的真实行号。你会发现很多空行叠加得比你想象的多。另外,如果你用的是CLASSIC LIST(不是ALV),建议把系统默认的页长设置和你的内容长度对照一下,省的最后一页只有一条记录。有些老板喜欢每页固定30行,空白的页脚也得留着,这时候你需要在页面结束时用ULINE画一条底线,然后再NEW-PAGE。这个操作本身不难,难的是你要算清楚当前页已经用了多少行——SY-LINNO只告诉你当前行号,你还要结合SY-PAGNO判断换了页没有。反正我写到现在,最顺手的还是“提前规划行数,不在循环里乱SKIP”的策略。
如果你非要问怎么快速学会这种格式控制,也没什么捷径,你就找一张以前写烂的报表,故意改坏一个SKIP、一个ULINE、一个FORMAT,然后看屏幕输出变成什么样,再改回来。折腾三个来回,比看十篇博客都好使。自己亲手挖的坑,填上之后才能记得住。
写到这里,调试按钮又亮起来了。那个客户问“为什么第一行前边有个空行”?呵,八成是上一段代码在CONDITION里多写了一个SKIP,而且没有FORMAT RESET把背景色洗干净。行了,这就是今天的内容,格式设计没啥高深的,就是细心再细心。






