
做 ABAP 开发的兄弟应该都遇到过这种场景ALV 报表界面上放了一个自定义按钮比如执行特殊处理锁定数据补充审批之类。用户点一次业务逻辑跑完了这个按钮也完成了它的历史使命按理说就不该再出现在工具栏上。但现实是你要是没做任何处理它就一直挂在界面上用户还能再点第二次、第三次轻则重复执行产生脏数据重则直接抛个 dump。把 ALV 执行完自定义按钮后隐藏这个需求拆开揉碎了讲清楚就是这篇文章想干的事。这个需求听起来简单实际做起来牵扯到 ALV 工具栏的事件机制、表格刷新的时机、状态变量的传递还有经典 ALV 和 SALV 两套不同写法的差异。我会从业务场景出发把原理讲明白再分别给出 SALV 和经典 ALV 的完整代码最后把我自己在项目里踩过的坑整理成排查清单保证你照着能落地。1. 先聊清楚需求什么时候真的要用完即藏1.1 典型的业务场景先说一个最常见的场景一张发货清单 ALV工具栏上放了一个批量过账按钮。用户点它之前数据是草稿状态点完之后数据已经过账再点一次就是重复过账系统虽然能校验但体验极差甚至可能因为锁冲突卡住后续操作。这种时候按钮执行完就应该从界面上消失或者至少变成不可点击的状态。还有一种场景是流程类的按钮。比如提交审批用户点完之后单据状态变成审批中同一个操作再点已经没有意义反而容易造成误操作。再比如强制完成这种高权限操作执行完了一般不希望用户反复触发隐藏掉也算是一种保护。这些需求本质上是同一件事按钮的可见性要和业务状态绑定。按钮不是花园里固定的装饰它是业务流程里的一个动作入口动作一旦完成入口就要关闭。1.2 为什么很多开发者第一反应是错的我第一次接这种需求时第一反应是干脆在 USER_COMMAND 里面把按钮删掉不就完事了后来发现完全不是这么回事。ALV 的工具栏不是一棵你可以随意删除节点的树它的按钮是每次刷新时根据事件或回调重新生成的。也就是说你看到的按钮只是一种渲染结果真正的决定权在代码里。删掉一个渲染出来的按钮没有任何意义你必须在生成按钮的那段代码里做控制。想明白这件事后面的实现思路就顺了。很多新手还会想到用HIDE命令。HIRE 那是行隐藏用的跟工具栏按钮八竿子打不着。工具栏按钮的隐藏只能靠生成时不输出刷新界面这两步配合来完成。2. 隐藏按钮前先懂 ALV 按钮的生长方式2.1 SALV 事件驱动套路SALV 是 ABAP 面向对象的新一代列表模型它用事件来处理工具栏。核心套路是创建cl_salv_table实例后取到事件对象然后注册自己的事件处理器。DATA: lr_events TYPE REF TO cl_salv_events_table. lr_events gr_table-get_event( ). SET HANDLER lcl_event_handleron_toolbar FOR lr_events. SET HANDLER lcl_event_handleron_added_function FOR lr_events.TOOLBAR事件在每次 ALV 需要重绘工具栏时触发你在这个处理器里往工具栏里添加按钮。这个事件的触发频率比很多人以为的要高——不只是第一次显示每次refresh、每次排序切换、每次列宽调整都可能重新触发。ADDED_FUNCTION事件则负责处理用户点击自定义按钮后的逻辑。它俩天然是一对一个负责画,一个负责响应。明白了这个机制隐藏的实现思路就很清晰了在 TOOLBAR 事件里加一个判断当某个状态变量为真时直接不添加按钮。然后执行完业务逻辑后把这个状态变量置为真再触发一次刷新。2.2 经典 ALV 的回调套路经典 ALVREUSE_ALV_GRID_DISPLAY的套路完全不同。它通过回调 FORM 挂接逻辑CALL FUNCTION REUSE_ALV_GRID_DISPLAY EXPORTING i_callback_program sy-repid i_callback_user_command FRM_USER_COMMAND i_callback_toolbar FRM_TOOLBAR it_fieldcat gt_fieldcat[] TABLES t_outtab gt_out[]FRM_TOOLBAR这个 FORM 每次生成工具栏时都会被调用参数是一个cl_alv_event_toolbar_set类的引用你把按钮结构STB_BUTTON追加到它的MT_TOOLBAR内表里就行。FRM_USER_COMMAND则接收用户的函数码输入。两套机制殊途同归核心都绕不开状态变量控制生成这个思路。2.3 两条路线共同的刷新依赖不管是 SALV 还是经典 ALV隐藏按钮的最后一步都必须触发界面刷新。不刷新的后果是内部状态已经变了但屏幕还停在老界面按钮看起来还在用户再点一次功能还能执行等于白做了。SALV 的刷新是gr_table-refresh( )经典 ALV 则需要靠运行时获取 grid 实例。前者简单后者有点绕后面实战章节会详细说。3. 三种隐藏方案按场景做取舍3.1 方案A全局状态变量TOOLBAR事件里拦截这是最朴素也最实用的方案。用 ABAP 程序里的全局变量记录按钮是否应该显示然后在生成工具栏的代码里判断这个变量决定是否把按钮追加进去。优势非常明显代码改动量小思路直接不依赖任何复杂 API就算换成别的控件这套思路依然成立。缺点也有——你必须在业务逻辑执行处记得把状态变量改掉否则会出现按钮没隐藏或者按钮提前消失这种低级错误。我的实际建议是状态变量的赋值要紧贴着业务逻辑的执行并且刷新要紧跟着赋值中间不要穿插别的大段代码防止遗漏和时序问题。3.2 方案B动态维护排除列表SALV 和经典 ALV 都提供排除按钮的机制。SALV 可以通过cl_salv_functions控制内置按钮的可见性经典 ALV 可以通过IT_EXCLUDING参数传入需要排除的按钮功能码列表。这个方案对内置按钮排序、过滤、导出等很有效因为内置功能码是系统预设的。但对自定义按钮来说做排除不如控制生成来得干净——自定义按钮本来就是你自己的代码生成的你为什么不让它在生成时就消失而要事后用另一个列表去过滤它呢所以我的结论是方案B适合混搭场景。你可以在 TOOLBAR 里根据状态决定是否生成同时再结合排除列表把某个内置按钮在特定状态下藏起来。3.3 方案C按钮可用性切换灰掉而非隐藏有些需求并不要求按钮消失只是要求不能再点。这时候可以变换思路按钮还在但置灰。经典 ALV 里可以通过在FRM_USER_COMMAND前拦截并直接RETURN也可以借助 ALV 的按钮属性实现。SALV 里则可以通过控制ADD_BUTTON时传入的按钮属性或者运行时修改按钮控件属性。置灰方案的优势是用户能直观感受到这里有功能只是当前不可用对交互引导更友好。代价是需要多维护一个按钮的可用性状态代码上比隐藏方案多几行。到底选隐藏还是置灰我的建议是如果是一次性动作过账、提交、审批必须隐藏如果是条件性动作当前选中数据不满足条件时不可执行优先置灰。条件性动作用隐藏反而会让用户困惑因为按钮时有时无。4. SALV 完整实战一个可抄作业的 Demo这一节我直接给出一个完整可运行的 SALV Demo。场景模拟发货清单界面上有一个批量过账按钮点击执行过账完成后按钮自动从工具栏消失。4.1 全局定义和事件处理器程序最开始定义好全局变量和事件处理器。这个例子我放在一个报表程序里大家可以直接复制到自己的开发机实践。REPORT z_toolbar_hide_demo. DATA: gr_table TYPE REF TO cl_salv_table, gv_hide_btn TYPE abap_bool. 控制按钮是否隐藏 内表定义字段从简 TYPES: BEGIN OF ty_out, vbeln TYPE vbeln_vl, werks TYPE werks_d, status TYPE char10, END OF ty_out. DATA: gt_out TYPE STANDARD TABLE OF ty_out. CLASS lcl_event_handler DEFINITION. PUBLIC SECTION. METHODS: on_toolbar FOR EVENT toolbar OF cl_salv_events_table IMPORTING e_interactive, on_added_function FOR EVENT added_function OF cl_salv_events_table IMPORTING e_salv_function. ENDCLASS. CLASS lcl_event_handler IMPLEMENTATION. ... 具体实现在下面展开 ENDCLASS.这里有个容易忽略的点FOR EVENT后面的事件名和方法参数名必须和类定义里声明的一致。如果不一致激活的时候不会直接报错但运行时会发现事件根本没被捕获按钮出不来。我见过不止一次这种看似代码都写了就是没反应的情况。4.2 动态添加按钮的细节打开 TOOLBAR 事件的实现。创建按钮时stb_button结构的关键字段就是function、icon、text这几个注意function是回调函数码必须和 ADDED_FUNCTION 里判断的一致。METHOD on_toolbar. DATA: lo_toolbar TYPE REF TO cl_salv_interactive_toolbar, ls_button TYPE stb_button. 如果已经执行过直接返回不重新添加按钮 IF gv_hide_btn abap_true. RETURN. ENDIF. lo_toolbar e_interactive. CLEAR ls_button. ls_button-function BTN_POST. ls_button-icon icon_pay_back. ls_button-text 批量过账. ls_button-quickinfo 执行过账后按钮隐藏. ls_button-txt_info 执行过账后按钮隐藏. lo_toolbar-add_button( ls_button ). ENDMETHOD.这里必须强调RETURN不只是省掉add_button那一行而是让整个事件处理函数直接退出。如果你把add_button写在IF外面即使里面做了判断按钮照样会被加进去等于白判断。工具栏按钮的图标、文本这些属性你要是没时间精修至少把text和function写上不然界面上出现一个空白小方块用户根本不知道点它是干嘛的。4.3 业务执行与隐藏触发接下来是用户点击的处理。ADDED_FUNCTION 事件会拿到用户点击的功能码判断到是自己的按钮后执行业务逻辑然后把全局状态变量改成隐藏最后刷新 ALV。METHOD on_added_function. CASE e_salv_function. WHEN BTN_POST. 模拟业务处理把状态改成已过账 LOOP AT gt_out ASSIGNING FIELD-SYMBOL(fs). fs-status POSTED. ENDLOOP. 置隐藏标志触发刷新 gv_hide_btn abap_true. gr_table-refresh( ). WHEN OTHERS. 其他内置函数码SALV 自己会处理不用管 ENDCASE. ENDMETHOD.这段代码的精髓在于顺序先执行业务逻辑再置状态变量最后刷新。刷新会重新触发 TOOLBAR 事件此时变量已经是真按钮自然不会再被添加到工具栏。三步顺序一旦写反比如先刷新再置变量就会出现刷了半天按钮还在的诡异现在象。另外注意一个问题gr_table必须是全局可见的变量因为它是在主流程里创建的而事件处理器里要调用它的refresh方法。如果你在事件处理器里 new 一个cl_salv_table那就完全不是同一个实例了刷新无效。4.4 刷新SALV 的软肋与补偿SALV 的refresh方法本身不带参数它是无条件的全量刷新。这也意味着当前 ALV 的排序、筛选、列宽这些用户操作会被重置。在某些场景下比如用户筛选了一堆数据你刷一下把他的筛选清掉了体验很糟糕。所以我在项目里通常不直接用裸的refresh而是先保存用户的交互状态再恢复。SALV 提供了refresh_display相关的方法族还有cl_salv_elements这样的辅助类可以处理列宽、排序等信息的保存和恢复。如果你的业务对用户体验要求高建议封装一个带状态保存的刷新方法 简单示意刷新前记录当前布局设置 实际项目中可以结合 cl_salv_layout、排序和筛选的 getter/setter 完成 gr_table-refresh( ).这里还有一个关键点如果按钮隐藏的时机发生在 DOWN 或 PAI 的后续处理里要注意刷新必须发生在当前用户交互处理完毕之后。在事件处理器里调用refresh是安全的但在 AT SELECTION-SCREEN 这种静态事件里调用动态控件的刷新就要小心时序问题。5. 经典 ALV 实现完全不同的写法同一个思路5.1 回调 FORM 注册经典 ALV 依然大量存在于存量系统中尤其是那些十年以上的老报表。它的写法虽然不面向对象但思路和 SALV 是相通的。先看注册环节。CALL FUNCTION REUSE_ALV_GRID_DISPLAY EXPORTING i_callback_program sy-repid i_callback_user_command FRM_USER_COMMAND i_callback_toolbar FRM_TOOLBAR it_fieldcat gt_fieldcat[] TABLES t_outtab gt_out EXCEPTIONS program_error 1.注意i_callback_program传的必须是当前程序名sy-repid回调 FORM 名用字符串传进去。经典 ALV 运行时会动态PERFORM这些 FORM如果你传错了名字或者 FORM 根本不存在运行时会直接报错。5.2 工具栏里按状态过滤工具栏回调 FORM 收到的是一个cl_alv_event_toolbar_set引用它的mt_toolbar属性就是当前工具栏按钮内表。我们把原有按钮保留按状态判断决定是否追加自定义按钮。FORM frm_toolbar USING rr_data TYPE REF TO cl_alv_event_toolbar_set. DATA: ls_button TYPE stb_button. IF rr_data IS NOT BOUND OR gv_hide_btn abap_true. RETURN. ENDIF. CLEAR ls_button. ls_button-function BTN_POST. ls_button-icon icon_pay_back. ls_button-text 批量过账. ls_button-quickinfo 执行过账后按钮隐藏. APPEND ls_button TO rr_data-mt_toolbar. ENDFORM.这里有个新手容易踩的坑很多人拿着rr_data-mt_toolbar之后喜欢先把整个内表CLEAR掉再追加。千万别这么干因为mt_toolbar里保存的不只是你自己的按钮还有 ALV 内置的排序、过滤、导出等按钮你一清就全没了。正确的做法是只加不减也就是说你要添加自己需要的按钮但千万不要动 ALV 已经放好的那部分。如果确实想隐藏某些内置按钮用IT_EXCLUDING参数那是官方留给你的通道。5.3 执行后刷新表格经典 ALV 的难点在刷新。全屏 ALV 没有直接暴露 grid 实例你需要通过标准函数拿引用。这是很多老项目里常见的模板代码FORM frm_user_command USING r_ucomm LIKE sy-ucomm rs_selfield TYPE slis_selfield. DATA: gr_grid TYPE REF TO cl_gui_alv_grid. IF r_ucomm BTN_POST. 执行业务逻辑 LOOP AT gt_out ASSIGNING FIELD-SYMBOL(fs). fs-status POSTED. ENDLOOP. 置隐藏标志并刷新 gv_hide_btn abap_true. CALL FUNCTION GET_GLOBALS_FROM_SLVC_FULLSCREEN IMPORTING e_grid gr_grid. IF gr_grid IS BOUND. CALL METHOD gr_grid-refresh_table_display. ENDIF. ENDIF. ENDFORM.GET_GLOBALS_FROM_SLVC_FULLSCREEN是一个比较特殊的函数它可以拿到当前全屏 ALV 的 grid 实例。它有个前提条件调用它的程序必须是当前 ALV 所在程序。如果是在函数组里跨程序调用或者 ALV 是嵌入在自定义容器里的那这个函数就拿不到值你需要改用别的方式。嵌入容器的情况就比较别扭了。经典 ALV 如果显示在自定义屏幕的自定义容器里你在 PAI 里需要自己持有 grid 引用刷新也直接调自己的引用。这种写法在维护老代码时要有心理准备全屏和容器是两套完全不同的刷新方式代码不能共用。6. 我踩过的坑排查清单与经验这部分算是我个人实战里最值钱的部分。这些问题在文档里基本找不到完整解答全是真金白银换来的教训。6.1 按钮闪回刷新没有真正触发 TOOLBAR第一次做隐藏功能的时候我遇到过执行完后按钮确实消失了但用户随便点一下排序按钮又回来了的情况。排查发现原因在于我用了refresh但对经典 ALV 来说触发工具栏重建的不一定是刷新还包括其他交互事件。换句话说某些操作会把工具栏重新渲染一遍而渲染时会再次调用 FRM_TOOLBAR如果你的状态变量没有正确持久化按钮就会闪回来。解法很简单状态变量必须是全局的、持久化的不仅仅在一次交互内有效。标准做法是放 ABAP 内存或者全局变量。如果你在某个局部类里定义临时变量来控制那按钮必然闪回。6.2 事件注册重复导致按钮点一下执行两次SALV 的事件处理器如果注册了两次按钮点击一次ADDED_FUNCTION 会触发两次业务逻辑就会执行两次。这种问题在代码里看非常隐蔽因为SET HANDLER一行一行看都是对的。常见成因是主流程里 SALV 创建和事件注册被放在了循环里。比如你的报表在循环中多次调用某个执行方法每次都创建一次 SALV 表实例也注册一次事件处理器。第二次进入循环时前一个实例被覆盖但事件处理器可能还挂着旧实例的引用。排查方法很简单在 ADDED_FUNCTION 入口打个断点数一下触发次数。一旦确认重复注册把事件注册代码提到循环外确保一个 ALV 实例只注册一次。6.3 光标丢失和视图状态重置隐藏按钮触发的刷新会把用户当前选中的行、光标位置、滚动位置全部重置。对单纯的数据查看报表影响不大但对操作密集型场景很恼火。如果业务允许我建议把隐藏按钮的刷新延迟到下一个必要的交互节点而不是立刻刷。或者采用方案C置灰因为置灰的效果可以通过按钮属性直接改不需要全量刷新。6.4 自定义函数码与系统保留字冲突这个坑比较冷门。ALV 内置了一些保留按钮功能码比如%PC本地导出、%SORT排序、%FILTER过滤等等。如果你的自定义按钮函数码取了类似的名字轻则按钮行为异常重则 ALV 报错。我遇到过有人把自定义按钮命名为%TA结果一点按钮ALV 进入了一个莫名其妙的内置处理分支。后来查资料才知道带有%前缀的功能码基本都是系统保留的。我的经验法则自定义按钮的功能码用纯业务含义的英文缩写比如POST_HD、RE_INIT、APPROVE不要带%前缀也尽量避免使用、空格这样的特殊字符。6.5 排查速查表现象可能原因对策按钮执行后还在刷新没有调用或刷新前没改状态变量检查三步顺序业务逻辑→置变量→刷新按钮闪回过一会又出现状态变量局部化了或触发了其他重绘用全局变量并确保生命周期覆盖整个界面按钮不显示事件未注册、回调名错误、函数码冲突打断点检查事件触发次数和函数码按钮重复执行事件处理器注册了两次把 SET HANDLER 提出循环保证单次注册刷新后选中行丢失refresh 的重置行为记录行号后恢复或用置灰替代隐藏7. 我个人的实操心得这个需求让我最受用的一点是逼着我把界面渲染和业务状态的关系想清楚了。按钮不是静态摆在那儿的装饰品它是业务状态的一种投影。你让一个按钮可见本质上是告诉用户这个动作当前可以做你让它消失本质上是告诉用户这个动作已经结束了或者现在不允许做。理解了这一层再回头看各种实现方案其实都是同一个东西的不同表达。状态变量控制生成也好排除列表过滤也好置灰也好选哪一种取决于你对交互表达的偏好而不是技术难度。最后再提醒一句在事件处理器里千万注意全局变量的生命周期。ABAP 的全局变量在一个会话里是持久的但要注意不同用户、不同会话之间不要互相干扰。如果你的业务场景里同一个程序可能同时被多个会话操作考虑用会话内存或者业务表来存储这个状态而不是只靠一个全局变量。这套代码我在多个项目里验证过SALV 场景下基本稳定经典 ALV 场景下注意函数码前缀问题也没有翻过车。如果你在实践中有更好的思路或者踩到了我没写到的坑欢迎交流。