做SAP报表开发的人迟早都会遇到这样一个需求报表展示出来了用户不只要“看”还要在界面上“选”。最常见的就是在ALV表格最前面加一列复选框用户勾选几行再点按钮做批量审批、批量过账、批量打印之类的操作。复选框、全选、取消全选这三个词看着简单但真正落地的时候坑点比想象中多得多。这篇文章我就围绕“基于ALV报表实现复选框、全选、取消全选”这个完整案例把整个思路、代码、排查过程全部拆开讲。内容适合正在做ABAP报表开发的同行也对刚开始接触ALV交互的顾问有帮助。我尽量少说废话直接讲能落地的方案和踩过的坑。1. 项目需求拆解看似简单的一列为什么总做不好1.1 复选框在ALV报表里的典型业务场景先说我接到这个需求时的实际场景。业务部门要求做一个订单清单报表表格里要能勾选订单然后统一执行审核操作。他们给的原始需求很简单“报表里加个复选框能勾选能全选点批量审核的时候只处理勾上的数据。”这种需求在SAP项目里太常见了。类似的还包括销售订单批量审批、批量发货采购订单批量审批、批量收货报工工单批量确认客户主数据批量导出前先筛选勾选用户的期望很朴素打开报表看到数据勾选需要的行一键操作。但“勾选”这个动作在ALV里其实有不止一种实现方式而且每种方式带来的交互体验和数据回写机制都不一样。如果一开始选错了路子后面做全选、取消全选、数据收集时会一直别扭。1.2 两种复选框实现路径的对比我在项目里整理过两条主流的ALV复选框实现路径本质上对应两种不同的交互设计实现方式核心配置勾选数据如何拿到适用场景Layout的BOX_FNAME方式内表加SEL字段layout中指定box_fnameALV自动维护内表SEL字段或通过GET_SELECTED_ROWS按行索引取数勾选列固定在最左侧交互简单推荐绝大多数业务场景使用字段目录EDIT方式字段目录中给某字段设置edit属性勾选值需要依赖DATA_CHANGED事件回写逻辑更重要求复选框列不固定在首位或需要对格子做复杂可编辑控制时使用两种方式我都在真实项目里用过。如果需求没有特别说明“复选框必须放在某一列的后面”我最推荐的是第一种BOX_FNAME方式。原因后面详细说简单先提一句它把“勾选”这个交互直接交给ALV内核处理勾选状态和内表字段是绑定的不需要额外维护一大堆事件回调代码量少、排查容易。这篇文章的主实现我会围绕BOX_FNAME方式展开。在最后一个章节里我会单独把EDIT方式的坑拉出来讲因为很多项目是被这条路坑过的。2. 完整实现从零搭建一个带复选框的ALV报表2.1 数据准备和字段设计实现的第一步是给数据内表增加一个“勾选标志”字段。这里有一个硬性要求字段类型必须是长度为1的字符型也就是C类型。在很多SAP实际项目里这个字段习惯命名为SEL有的也叫MARK、BOX含义是一样的。我下面的示例以内表结构ZTALV_DEMO展示。假设报表核心数据是销售订单相关字段我额外增加一个SEL字段作为勾选标志TYPES: BEGIN OF ty_data, sel TYPE c, 复选框勾选标识X代表勾选 vbeln TYPE vbeln, 销售订单号 erdat TYPE erdat, 创建日期 netwr TYPE netwr, 订单金额 waerk TYPE waerk, 货币单位 vkorg TYPE vkorg, 销售组织 END OF ty_data. DATA: gt_data TYPE TABLE OF ty_data, gs_data LIKE LINE OF gt_data.这里要提醒一下SEL字段不需要放进数据库表它只是ALV展示和数据交互用的临时标志位。如果将来要保留用户的勾选结果比如把勾选情况写入自定义表那时候才需要单独设计持久化字段。接下来就是构造内表数据。实际业务里一般从VBAP、VBAK、LIKP之类单据表取数我这里为了演示清晰直接放几条测试数据。2.2 布局设置让ALV自动渲染勾选列数据准备好了关键是布局对象的设置。注意要让ALV在最左边自动生成一列复选框不是去字段目录里加字段而是在layout中指定box_fname属性。DATA: gs_layout TYPE lvc_s_layo. gs_layout-box_fname SEL. 指定内表字段名为SELALV会自动生成勾选列 gs_layout-zebra X. gs_layout-cwidth_opt X. gs_layout-sel_mode A. 多选模式支持鼠标拖拽批量选择这里的核心就是这句话gs_layout-box_fname SEL。这一行的作用是告诉ALV你在渲染表格时去内表的SEL字段上画复选框。用户勾选某一行的复选框时ALV会自动把内表中对应行SEL字段的值更新为X取消勾选时清空。也就是说ALV内部已经帮我把“勾选状态”和“内表数据”打通了我不需要拦截每个点击事件去手工维护字段。关于gs_layout-sel_mode我再多说两句。SEL_MODE有几种取值不同的值影响选择模式A多选也是默认支持的方式用户点复选框是一个勾法点行首还能按住鼠标拖拽批量选择B单选用户一次只能勾选一行C多选但隐藏了工具栏上的全选等辅助手段实际BOX_FNAME下这个影响有限D单元格选择模式勾选的时候会连带选中的单元格区域大多数业务需求都是多选也就是模式A。如果业务方说“最多只能选一条选第二条时取消第一条”那就改成B。这个在用户操作层面的体感差异很大建议接到需求时先问清楚。2.3 两个事件实现按钮扩展ALV默认的工具栏里有“全选”“取消全选”按钮吗严格说ALV的默认工具栏包含“选择全部”和“取消选择”的功能按钮在标准状态下是可以直接用的但很多实施项目的做法是通过EXCLUDING把默认按钮屏蔽了一部分或者因为界面风格要求重新自定义按钮又或者用户希望按了全选之后还能触发后续业务逻辑而不是仅仅在表格上打勾。所以在这个项目里我们选择在ALV工具栏上增加两个自定义按钮全选、取消全选。这需要通过TOOLBAR事件来做。先定义一个事件处理器类CLASS lcl_event_handler DEFINITION. PUBLIC SECTION. METHODS: handle_toolbar FOR EVENT toolbar OF cl_gui_alv_grid IMPORTING e_object e_interactive, handle_user_command FOR EVENT user_command OF cl_gui_alv_grid IMPORTING e_ucomm. ENDCLASS. CLASS lcl_event_handler IMPLEMENTATION. METHOD handle_toolbar. DATA: ls_toolbar TYPE stb_button. ----- 添加“全选”按钮 ----- CLEAR ls_toolbar. ls_toolbar-function ALL_SEL. ls_toolbar-icon icon_select_all. ls_toolbar-quickinfo 全选. ls_toolbar-text 全选. APPEND ls_toolbar TO e_object-mt_toolbar. ----- 添加“取消全选”按钮 ----- CLEAR ls_toolbar. ls_toolbar-function CANCEL_SEL. ls_toolbar-icon icon_deselect_all. ls_toolbar-quickinfo 取消全选. ls_toolbar-text 取消全选. APPEND ls_toolbar TO e_object-mt_toolbar. ENDMETHOD. METHOD handle_user_command. CASE e_ucomm. WHEN ALL_SEL. PERFORM frm_select_all. WHEN CANCEL_SEL. PERFORM frm_cancel_all. ENDCASE. ENDMETHOD. ENDCLASS.然后在创建ALV控件后注册这两个事件DATA: go_grid TYPE REF TO cl_gui_alv_grid, go_container TYPE REF TO cl_gui_custom_container, go_event_handler TYPE REF TO lcl_event_handler. CREATE OBJECT go_container EXPORTING container_name ALV_CONTAINER. CREATE OBJECT go_grid EXPORTING i_parent go_container. CREATE OBJECT go_event_handler. SET HANDLER go_event_handler-handle_toolbar FOR go_grid. SET HANDLER go_event_handler-handle_user_command FOR go_grid.事件注册这部分新手最容易漏。漏了SET HANDLER按钮加不出来或者按钮点了没反应排查半天发现事件根本没挂上。2.4 全选与取消全选的逻辑实现按我前面的设计内表里SEL字段的值决定复选框状态。因此“全选”和“取消全选”的实现逻辑非常直接把内表所有行的SEL字段置为X或清空然后刷新ALV显示。代码如下FORM frm_select_all. DATA: ls_data LIKE LINE OF gt_data. LOOP AT gt_data INTO ls_data. ls_data-sel X. MODIFY gt_data FROM ls_data TRANSPORTING sel. ENDLOOP. PERFORM frm_refresh_alv. ENDFORM. FORM frm_cancel_all. DATA: ls_data LIKE LINE OF gt_data. LOOP AT gt_data INTO ls_data. CLEAR ls_data-sel. MODIFY gt_data FROM ls_data TRANSPORTING sel. ENDLOOP. PERFORM frm_refresh_alv. ENDFORM.刷新ALV的过程单独抽出来FORM frm_refresh_alv. DATA: ls_stable TYPE lvc_s_stbl. 控制刷新时保持滚动位置 ls_stable-row X. ls_stable-col X. IF go_grid IS NOT INITIAL. CALL METHOD go_grid-refresh_table_display EXPORTING is_stable ls_stable. ENDIF. ENDFORM.到这里复选框显示、全选、取消全选三个主要功能就全部跑通了。交付给用户测试基本能满足90%的业务场景。不过一个真实的报表程序只做到这一步还不够。勾选之后的数据收集、批量操作、以及用户反复查询时的勾选保留都需要继续处理。这些功能往往才是业务方真正关心的部分。3. 细节优化勾选状态、刷新机制与数据回读3.1 刷新后勾选为什么丢两种刷新方式的区别我在项目中不止一次被问到“我勾选了一些行然后点了某个按钮触发刷新为什么勾选全部丢了”这个问题根子基本都在刷新方式上。ALV刷新有两个常见方法刷新方法行为特征对勾选状态的影响refresh_table_display只按当前内表数据重新渲染界面保留ALV内部控制信息勾选状态如果有对应内表字段通常能保留refresh_table_first先重新取出内表数据并重建ALV显示数据相当于整个表格重新装载内部选择状态会清空我见过很多同事图省事或者从老代码里拷贝了一段刷新逻辑用的是refresh_table_first结果每次刷新后用户勾选全没了。我的习惯是默认使用refresh_table_display只有当内表的数据源发生了结构性变化比如重新执行SELECT、内表行数变了、字段结构变了才用refresh_table_first。在我这个方案里复选框状态直接保存在内表SEL字段上因此只要内表数据还在勾选状态其实一直跟着数据走。只要刷新逻辑正确用户勾选不会丢。3.2 重新查询后如何保留用户的勾选还有一种更常见的场景用户勾选了几行数据然后又重新执行了一次查询内表数据被重新填充之前勾选的标识跟着被清掉了。这时候用户往往很恼火“我又得重新勾一遍。”要解决这个问题我通常的做法是在查询动作发生之前先把当前勾选行的主键保存到一个内表等新数据加载完再遍历新内表把主键命中的行自动勾选起来。具体实现如下DATA: gt_selected_keys TYPE TABLE OF vbeln. FORM frm_save_selected_keys. 在重新查询前把已勾选行的订单号保存起来 DATA: ls_data LIKE LINE OF gt_data. REFRESH gt_selected_keys. LOOP AT gt_data INTO ls_data WHERE sel X. APPEND ls_data-vbeln TO gt_selected_keys. ENDLOOP. ENDFORM. FORM frm_restore_selected_keys. 在重新查询后根据保存的订单号恢复勾选状态 DATA: ls_data LIKE LINE OF gt_data. LOOP AT gt_data INTO ls_data. READ TABLE gt_selected_keys TRANSPORTING NO FIELDS WITH KEY table_line ls_data-vbeln. IF sy-subrc 0. ls_data-sel X. ELSE. CLEAR ls_data-sel. ENDIF. MODIFY gt_data FROM ls_data. ENDLOOP. PERFORM frm_refresh_alv. ENDFORM.这段逻辑其实在业务上特别有用。比如用户设置了一组筛选条件查询结果有几百行他辛辛苦苦勾了二十几行发现还要换个筛选条件再看几个数据切换回来后勾选得全部重来。有了这个“勾选记忆”机制体验会好很多。需要注意的是保存勾选的主键要选业务上真正稳定的字段。比如VBELN订单号这种单据编号就是稳定的。如果拿行号INDEX保存重新查询后行号没有意义恢复无从谈起。3.3 如何拿到勾选行并做批处理复选框勾上了最终目的还是拿到这些数据去做业务操作。拿勾选数据的方式有两种我一般按场景二选一。第一种方式直接读内表SEL字段FORM frm_get_checked_data. DATA: lt_checked LIKE TABLE OF gt_data. LOOP AT gt_data INTO gs_data WHERE sel X. APPEND gs_data TO lt_checked. ENDLOOP. IF lt_checked IS INITIAL. MESSAGE 请先勾选需要处理的数据 TYPE S DISPLAY LIKE E. RETURN. ENDIF. 在这里做批量业务处理比如批量修改订单状态 LOOP AT lt_checked INTO gs_data. 业务处理逻辑... ENDLOOP. ENDFORM.这种方式的好处是拿到的数据是“完整行”后面做业务处理时订单号、金额、组织、日期都能直接取用不用再回表查一遍。在有批量审批、批量打印、批量传输这类需求时非常方便。第二种方式通过ALV的GET_SELECTED_ROWS方法拿到选中行的索引再回内表定位数据FORM frm_get_selected_rows. DATA: lt_rows TYPE lvc_t_row, ls_row TYPE lvc_s_row. CALL METHOD go_grid-get_selected_rows IMPORTING et_index_rows lt_rows. IF lt_rows IS INITIAL. MESSAGE 请先选择行 TYPE S DISPLAY LIKE E. RETURN. ENDIF. SORT lt_rows BY index. LOOP AT lt_rows INTO ls_row. READ TABLE gt_data INDEX ls_row-index INTO gs_data. IF sy-subrc 0. 处理选中行 ENDIF. ENDLOOP. ENDFORM.这种方式更贴近ALV的“行选择”语义在部分场景下配合SEL_MODE、拖拽选择很顺手。但它拿到的index和gt_data里数据的对应关系依赖一个前提屏幕上显示的ALV行顺序与内表当前顺序完全一致。在大多数报表程序里这个前提成立但如果中间做过排序、过滤就要小心索引对不上的问题。我的习惯是如果只需要判断哪些行被选中用get_selected_rows如果要基于选中行内容做复杂业务处理并且内表数据可能被ALV显示顺序打乱过就直接遍历内表的SEL字段或者把ALV显示的数据重新取回内表再处理。3.4 多选限制和其他布局参数除了BOX_FNAME之外layout里还有几个常用参数我在这个项目里也一并设置了gs_layout-multiselect X. 允许多选 gs_layout-sel_mode A. 拖拽多选 gs_layout-no_rowmark .multiselect其实是老版本ALV的参数现在多数情况下用sel_mode控制选择模式就够用了。但我在老项目代码里见过两者并存的情况所以列出来供参考。加上sel_mode A以后用户除了点复选框还能直接拖拽选中一片区域这个操作习惯在部分老用户那里很受欢迎。另外no_rowmark如果不设置为X行首会有个小箭头作为当前行标记跟复选框列并存时视觉上不冲突我一般保持默认。还有一点BOX_FNAME方式生成的选择列它的列标题默认取自数据字典中SEL字段的描述。如果内表SEL字段没有描述ALV会显示一个很短的标题甚至空白。如果你希望列标题显示成“选择”可以在数据字典里维护这个字段的描述或者在写内表类型时通过DATA ELEMENT绑定一个有描述的数据元素。这个小细节很多项目直到UAT才发现临时不知道改哪里也是比较常见的坑。4. 踩坑实录这些问题我一定提醒你排查4.1 复选框列不显示或置灰先说不显示。如果layout里设置了box_fname SEL但ALV界面上看不到任何复选框列最常见的原因是内表字段名对不上。检查一下SEL字段是否真的存在于gt_data的内表结构中字段名是否都大写字段类型是否为长度为1的字符型。ABAP内表字段名不区分大小写但变量名如果写错编译阶段往往能测出来如果是用动态字段名传入编译期发现不了运行期才会异常。再说置灰。复选框列出来了但点不动灰的。这个问题分两种如果使用BOX_FNAME方式理论上复选框永远可点如果置灰多半是layout被代码重复赋值导致box_fname被清空或覆盖。检查是否有两处地方给gs_layout赋值第二处漏了box_fname。如果使用字段目录EDIT方式复选框灰掉通常是因为字段目录里该字段的edit属性没有设为X或者layout结了no_edit限制。4.2 点复选框没反应或值写不进去使用BOX_FNAME方式时ALV自动维护内表SEL字段一般不会出现“写不进去”的问题。但如果出现勾选后点批量按钮时读取内表SEL字段仍然为空这时要注意刷新时机。比如在按钮触发时先执行了refresh_table_display再读内表如果刷新过程中发生了数据重建可能把勾选丢了。正常顺序应该是先把勾选状态读到临时内表再做刷新或其他操作。如果使用的是字段目录EDIT方式那就要考虑是否注册了DATA_CHANGED事件并在事件里做了数据同步。EDIT方式的勾选值在用户点击后未必立即回写到内表必须通过DATA_CHANGED回调把ALV缓存的数据导回内表否则你在其他事件里读内表是读不到最新值的。4.3 全选按钮点了没效果全选按钮点了没效果或者按钮根本没出现在工具栏上我排查时通常按下面几步走检查TOOLBAR事件里是否成功把按钮function代码加到了er_toolbar-mt_toolbar。检查是否通过EXCLUDING把默认按钮屏蔽得太狠导致工具栏整体被换掉。检查USER_COMMAND事件里的CASE分支按钮代码是否匹配。检查SET HANDLER是否注册成功事件类实例是否在内存中被释放。其中最隐蔽的问题是第4点。如果在主程序里把事件处理器定义成局部变量然后又用NEW创建了一个局部实例一不小心在方法结束时被垃圾回收了事件会静默失效按钮还是显示的但点击没有任何反应。我的习惯是把所有事件对象定义成全局变量程序生命周期内始终存在。4.4 用EDIT字段实现复选框时的典型坑尽管这篇文章的主推方案是BOX_FNAME但实际项目里仍然会遇到必须用字段目录EDIT方式的情况。比如用户要求复选框列不在最左边而是跟在某个业务关键列后面或者要求单元格可编辑的同时还有下拉框联动。这种时候BOX_FNAME是做不到的只能用EDIT字段。用EDIT字段方式实现复选框要特别注意几个点第一字段目录里要设置ls_fieldcat-fieldname SEL. ls_fieldcat-ref_field SEL. ls_fieldcat-edit X. ls_fieldcat-col_pos 3. 演示放到第3列第二必须注册DATA_CHANGED事件。否则用户勾选后值滞留在ALV的内部数据缓存里内表不变。第三复选框勾选后由于ALV默认会触发单元格光标跳转部分用户觉得体验奇怪。可以通过控制i_checkboxes提交行为来缓解但具体效果跟SAP版本有关没有统一答案。最终建议是如果只是要“勾选、全选、取消全选”这三个基础功能BOX_FNAME就是最稳的路。需要把勾选列嵌入到数据列中间或要做复杂的单元格编辑联动再用EDIT方案并且一定要把DATA_CHANGED事件的数据同步逻辑写完整。5. 从功能到产品把报表做得更顺手的小技巧功能跑通之后我一般还会花点时间把一些交互细节打磨一下。我分享几个在这个案例中实际用到的经验。一是工具栏按钮的图标和文本。TOOLBAR事件里如果只给text不长按钮会显得比较拥挤我给全选、取消全选配置了icon这样按钮显示为图标加短文本整体更整洁。icon_select_all和icon_deselect_all都是SAP标准图标直接赋值即可。二是按钮位置。ALV默认工具栏上已经有不少按钮如果不管自定义按钮会追加到最右边有些用户不容易发现。我处理时有两种方式。一种是调整ALV工具栏结构把自定义按钮往前放需要借助MT_TOOLBAR里的顺序调整另一种是干脆把默认工具栏里不常用的按钮EXCLUDING掉让自定义按钮变得醒目。两种方式在项目里都试过效果都不错。三是业务校验放在按下批量按钮之后。批量操作的按钮不管是通过USER_COMMAND事件触发还是通过独立按钮触发第一步必须先判断是否勾选了数据。如果没有勾选给用户一个清晰的提示而不是直接执行循环代码最后发现什么都没处理。四是对大批量数据的性能关注。全选逻辑本身是遍历内表更新字段如果内表有几万行这个循环耗时可能有点高。但实际情况中用户很少会对几万行做全选更多是筛选后选择几十行。如果项目里确实有大数据量下全选的性能要求可以考虑不逐行更新内表而是利用ALV的选择逻辑动态处理但那套逻辑复杂度有明显提升。我在项目中更倾向于保持代码简单、可读毕竟维护的人不只有我自己。五是勾选记忆功能也就是我在前面3.2节写的主键保存方案。我在好几个项目里都做了这个功能用户的反馈普遍很好。对比一下做了这个功能的报表在业务部门里的接受度明显更高用户会觉得“这个报表聪明”。最后再说一个关于标题的小细节。我拿到这个项目需求时标题叫“基于ALV表报实现复选框、全选、取消全选等功能”看着像“表报”其实就是“报表”的笔误。需求文档里一字之差我们做开发的还是要从上下文理解真实意图。所谓表报说到底就是ALV Grid展示的数据报表。我整篇内容也按照“ALV报表复选框交互”的完整场景来展开这样最后交付出来的东西才是用户真正想要的。如果你此刻手头正有一个类似的报表需求建议按这个顺序做先确认内表加SEL字段再设置layout的box_fname接着写全选取消全选的按钮和逻辑最后补上勾选数据收集。按照这个路线走核心功能半天就能出来而且踩坑概率会低很多。
阅读完成 · 觉得有帮助?