Quantcast
Channel: ABAP OO Archive - Tricktresor
Viewing all 53 articles
Browse latest View live

Speak!

$
0
0

SAP kann sprechen. Mit der Microsoft Speech API 5.3

https://pixabay.com/de/sound-welle-stimme-h%C3%B6ren-856770/

https://pixabay.com/de/sound-welle-stimme-h%C3%B6ren-856770/

Auf der Seite http://abapdn.blogspot.com bin ich auf folgenden interessanten Code gestoßen:

INCLUDE ole2incl.
DATA : v_objole      TYPE ole2_object,
       v_objolevoice TYPE ole2_object,
       v_strtotalk   TYPE string.

CREATE OBJECT v_objolevoice 'SAPI.SpVoice'.

SET PROPERTY OF v_objolevoice 'Volume' = 80.
SET PROPERTY OF v_objolevoice 'Rate' = -1.

v_strtotalk = 'This is great'.

CALL METHOD OF v_objolevoice 'Speak' = v_objole
EXPORTING #1 = v_strtotalk.

FREE: v_objolevoice,v_objole.

Und SAP kann sprechen... Leider ist es mir nicht gelungen, andere Stimmen zu aktivieren. Es spricht standardmäßig "Anna" ("Mary" laut Doku). Es müsste aber auch noch "Mike" vorhanden sein.

Get Voice

Mit diesem Code-Schnipsel kann man die eingestellte Stimme ermitteln.

DATA v_voicename TYPE string.
DATA v_voice     TYPE ole2_object.
GET PROPERTY OF v_objolevoice 'Voice' = v_voice.
CALL METHOD OF v_voice 'GetDescription' = v_voicename.

Umgestellt werden kann die Stimme leider nicht, denn dafür müsste man per OLE ein indiziertes Array ansprechen, was aber nicht geht. Aufgrund von Lizenzproblemen ist häufig aber auch nur eine Sprache (und Stimme) installiert.

Asynchrone Sprachausgabe

In der Defaulteinstellung wird die Verarbeitung erst fort gesetzt, wenn die Sprachausgabe beendet ist. Mit dem Parameter SVSFlagAsync der Methode SPEAK kann die Sprachausgabe jedoch auch asynchron erfolgen. Das ist beispielsweise dann sinnvoll, wenn eine per MESSAGE ausgegebene Meldung auch gesprochen werden soll. Die Meldung sollte dann natürlich nicht erst erscheinen, wenn sie bereits vorab einmal komplett gesprochen wurde...

CALL METHOD OF v_objolevoice 'Speak' = v_objolevoice
EXPORTING #1 = v_strtotalk
#2 = 1.  "SVSFlagsAsync

Links

Hier der Link zum Artikel:
http://abapdn.blogspot.com/2010/09/sap-knows-how-to-speak.html

Und hier der Link zur API-Dokumentation:
http://msdn.microsoft.com/en-us/library/ms723614(v=vs.85).aspx

Kürzer!

Hier noch eine kürzere Variante von Nalon:

DATA ovoice TYPE ole2_object.
CREATE OBJECT ovoice 'SAPI.SpVoice'.
IF sy-subrc = 0.
  CALL METHOD OF ovoice 'Speak'
    EXPORTING #1 = 'Ich will sprechen'.
ENDIF.

Generic Programming

$
0
0

Alles, was irgendwie mit universeller Programmierung zu tun hat. RTTI, RTTS, RTTC, Create Data, Create Table, Field-Symbols, CL_ABAP_TYPEDESCR, CL_ABAP_TYPEDESCR, CL_ABAP_STRUCTDESCR, CL_ABAP_COMPLEXDESCR, CL_ABAP_REFDESCR, CL_ABAP_ELEMDESCR, CL_ABAP_DATADESCR, CL_ABAP_OBJECTDESCR, CL_ABAP_INTFDESCR, CL_ABAP_CLASSDESCR

Generic class for inserting, updating and deleting records

Purpose of this class is to store and process records that need to be modified in database tables. When the user is working on an object that contains header and detail records, a 'Save' means that records need to be modified in different tables. Imagine that the application shows a grid where the user can insert, update and delete records; the logic for saving these changes can be a bit complex.

This class should provide the necessary functions for this kind of functionality. It holds a table with the database tables and records that we need to update. Methods are available to add entries to this table and this means that during a process, the required database updates are collected until the actual database update takes place. When the application has finished the process, it calls the method that performs the database update.

http://wiki.sdn.sap.com/wiki/display/Snippets/ABAP+OO+-+Generic+class+for+inserting%2C+updating+and+deleting+records

Advanced and Generic Programming in ABAP

Session from SAP TechEd '04 -- Shows you  how to add  dynamic  elements to your ABAP programs and make  them more  flexible with regard to changing  requirements.  You will learn how to write generic services  that  can deal with dynamically structured data that  was unknown at design time. Also, a brand- new  Dynamic Type Creation tool enables you to  create  types on-the-fly, without the need for  program  generation.

http://scn.sap.com/docs/DOC-22613

ABAP OO Freak Show

Autosumme in ALV-Grid – Version 1

$
0
0

In einem Projekt wurde eine Anwendung programmiert, in der viele numerische Daten berechnet und ausgegeben wurden. Die Berechnungsergebnisse wurden auf mehrere Zellen verteilt. Um überprüfen zu können, ob die Verteilung richtig programmiert war, musste man die Summe über die verteilten Werte bilden und mit dem Ausgangswert vergleichen. Dies war immer relativ mühselig, da man entweder die Werte per Copy&Paste nach Excel kopieren musste (dann gab es aber Probleme bei negativen Zahlen...!) oder man musste die komplette Tabelle in Excel öffnen. Alles natürlich möglich, aber mir doch etwas zu umständlich.

Ich erinnerte mich an das Event delayed_changed_sel_callback, mit dem man eine Selektion im Grid verzögert auswerten konnte. Mit Hilfe der Tastenkombination STRG+Y kann man einzelne Zellen markieren. Diese Funktionen wollte ich nutzen, um die markierten Zellenwerte auszulesen und zu summieren.

Herausgekommen ist das unten stehende Programm. Der eigentliche Teil, die Summierung der Zellen, steht komplett in der Methode HANDLE_DELAYED_SELECTION.

2016-10-07_19-06-25

Vorgehen

Event delayed_changed_sel_callback registrieren und einen Eventhandler in der Klasse zuordnen (Handle_Delayed_Selection).

Wie kommt man an die markierten Zellen heran? Dafür gibt es die Methode Get_Selected_Cells. Sie liefert eine Tabelle zurück, in der die markierten Feldnamen und der Zeilenindex stehen. Mittels READ TABLE und ASSIGN COMPONENT kann man also auf einen Zellwert zugreifen.

Nun muss noch mittels DESCRIBE FIELD geprüft werden, ob es sich um ein Feld mit einem numerischen Wert handelt. Wenn das der Fall ist, kann der Wert der Zelle aufsummiert werden.

 

Normale Zellen vs. Summenzeilen

Das Vorgehen bei einem ALV-Grid, in dem keine Summen oder Zwischensummen gebildet wurden, ist einfach und erfolgt nach dem oben genannten Schema. Eine Herausforderung sind jedoch die (Zwischen-) Summenstufen gewesen. Diese werden im ALV-Grid in eigenen - geschützten - Tabellen verwaltet:

Tabelle Bedeutung Bemerkung
MT_CT00 Summetabelle Sie enthält in der Regel nur einen Eintrag. Ausnahme: Es sind in der Summierung der Feldwerte unterschiedliche Einheiten vorhanden.
MT_CT01 Zwischensummentabelle 1 Erste Zwischensummenstufe
MT_CTnn Zwischensummentabelle n Tabellen für Zwischensummenstufe nn
MT_CT09 Zwischensummentabelle 9 9 ist die höchste Stufe. Mehr Zwischensummen können nicht erstellt werden.

Immerhin gibt es in diesem Fall eine Methode, mit der man sich die Zwischensummentabellen - bzw. eine Referenz auf diese - besorgen kann: Get_Subtotals. Im Feld ROW_ID-ROWTYPE der Zellen-Tabelle steht, ob es sich um die Totals-Tabelle handelt (1. Zeichen = T) oder eine Zwischensumme (1. Zeichen = S). Nicht gruppierte Zellen haben den Eintrag SPACE.

Eine Selektion auf ein Summenfeld liefert zum Beispiel diesen ROWTYPE: S 0101X0000000001. Der Vierstellige Code nach dem S sagt aus, um welche Hierarchiestufe es sich handelt (Stellen 1 und 2 des Codes). Die Stellen 3 und 4 des Codes sagen aus, in welcher Tabelle das markierte Feld steht. In diesem Fall ist es Level 1 der Hierarchie und Tabelle MT_CT01.

Anhand dieses Code kann man also herausfinden, in welcher Zwischensummentabelle nachgeschaut werden muss. Dies tue ich hier:

lv_index = ls_cell-row_id-rowtype+4(2).
lv_tablename = 'LD_CT' && lv_index.
ASSIGN (lv_tablename) TO <ref_data>.

Nachdem wir nun wissen, in welcher Tabelle wir nachsehen müssen um den markierten Zellwert zu finden, müssen wir nun noch den richtigen Index ermitteln. Dieser wird leider nicht mitgegeben, sondern muss aus der Tabelle GROUPLEVELS, die über Get_Subtotals geliefert wird, ermittelt werden:

READ TABLE lt_grouplevels INTO ls_grouplevel INDEX ls_cell-row_id-index.
IF sy-subrc = 0.
  ls_cell-row_id-index = ls_grouplevel-cindx_from.
ENDIF.

Achtung! Das Programm funktioniert nur, wenn es sich um reine (Zwischen-) Summen handelt! Zwischensummen, die aus mehreren Zeilen bestehen weil sich die zugehörige Einheit unterscheidet, können (noch) nicht erkannt werden. Hier muss ich noch etwas forschen...

Zwischensummen bilden

Ein kurzer Hinweis, wie man im ALV-Grid Zwischensummen bildet:

Wähle als erstes mindestens eine Spalte über die du dann mit Hilfe des Summenicons 2016-10-07_19-48-00 eine Summe bildest. Danach kannst du weitere Spalten markieren und mit dem Zwischensummenicon 2016-10-07_19-49-31 die Spalten definieren, über die zusätzlich eine Zwischensumme erstellt werden soll.

Aufrisssummenstufe

Über die Aufrisssummenstufe kannst du einfach festlegen, dass nur Zwischensummenzeilen einer bestimmten Hierarchie angezeigt werden sollen:

Auswahl im Menü:

2016-10-07_19-41-49

Auswahl der Hierarchieebene:

2016-10-07_19-06-45

Anzeige der gewählten Zwischensummen:

2016-10-07_19-22-39

Code

REPORT zz_alv_autosumme.

PARAMETERS p_total TYPE p DECIMALS 2.

CLASS lcl_main DEFINITION.

 PUBLIC SECTION.
 METHODS start.
 PROTECTED SECTION.
 DATA mr_grid TYPE REF TO cl_gui_alv_grid.
 DATA mt_data TYPE STANDARD TABLE OF spfli.
 DATA mv_data_table TYPE tabname VALUE 'SPFLI'.
 DATA mr_dock TYPE REF TO cl_gui_docking_container.
 METHODS create_docker.
 METHODS create_grid.
 METHODS handle_delayed_selection
 FOR EVENT delayed_changed_sel_callback
 OF cl_gui_alv_grid
 IMPORTING sender.
 METHODS register_events.
 METHODS select_data.
ENDCLASS.

CLASS lcl_main IMPLEMENTATION.

 METHOD start.
 select_data( ).
 create_docker( ).
 create_grid( ).
 register_events( ).
 ENDMETHOD.

 METHOD create_docker.
 "Create Docking container at bottom
 CREATE OBJECT mr_dock
 EXPORTING
 side = cl_gui_docking_container=>dock_at_bottom
 ratio = 90
 no_autodef_progid_dynnr = abap_false.

 ENDMETHOD.

 METHOD create_grid.
 "Create ALV-Grid
 CREATE OBJECT mr_grid
 EXPORTING
 i_appl_events = abap_true
 i_parent = mr_dock.

 "and display data
 mr_grid->set_table_for_first_display(
 EXPORTING
 i_structure_name = mv_data_table
 CHANGING
 it_outtab = mt_data ).

 "Set focus on grid so user can directly scroll and select cells via CTRL+Y
 cl_gui_container=>set_focus( mr_grid ).

 ENDMETHOD.
 METHOD handle_delayed_selection.

 "Local data
 DATA lt_cells TYPE lvc_t_cell.
 DATA ls_cell LIKE LINE OF lt_cells.
 DATA lv_total TYPE p DECIMALS 2.
 DATA lv_val_type TYPE c.
 DATA lv_index TYPE n LENGTH 2.
 DATA lv_tablename TYPE string.
 DATA lt_grouplevels TYPE lvc_t_grpl.
 DATA ls_grouplevel LIKE LINE OF lt_grouplevels.

 FIELD-SYMBOLS <ref_data> TYPE REF TO data.
 FIELD-SYMBOLS <table> TYPE table.
 FIELD-SYMBOLS <warea> TYPE any.
 FIELD-SYMBOLS <val> TYPE any.

 "data references to sub totals tables
 DATA ld_ct00 TYPE REF TO data.
 DATA ld_ct01 TYPE REF TO data.
 DATA ld_ct02 TYPE REF TO data.
 DATA ld_ct03 TYPE REF TO data.
 DATA ld_ct04 TYPE REF TO data.
 DATA ld_ct05 TYPE REF TO data.
 DATA ld_ct06 TYPE REF TO data.
 DATA ld_ct07 TYPE REF TO data.
 DATA ld_ct08 TYPE REF TO data.
 DATA ld_ct09 TYPE REF TO data.

 "get selected cells (selection via CTRL + Y)
 sender->get_selected_cells( IMPORTING et_cell = lt_cells ).

 "If there is only one cell selected, we do not need to sum that...
 CHECK lines( lt_cells ) > 1.

 "Read all cell values
 LOOP AT lt_cells INTO ls_cell.

 "in case of rowtype (normal cell, total or subtotal) assign correct data table
 CASE ls_cell-row_id-rowtype(1).
 "Total sum of all
 WHEN 'T'.
 sender->get_subtotals( IMPORTING ep_collect00 = ld_ct00 ).

 ASSIGN ld_ct00 TO <ref_data>.
 ls_cell-row_id-index = 1.
 "assign specified data table
 ASSIGN <ref_data>->* TO <table>.

 "subtotals
 WHEN 'S'.
 sender->get_subtotals( IMPORTING
 ep_collect01 = ld_ct01
 ep_collect02 = ld_ct02
 ep_collect03 = ld_ct03
 ep_collect04 = ld_ct04
 ep_collect05 = ld_ct05
 ep_collect06 = ld_ct06
 ep_collect07 = ld_ct07
 ep_collect08 = ld_ct08
 ep_collect09 = ld_ct09
 et_grouplevels = lt_grouplevels ).

 lv_index = ls_cell-row_id-rowtype+4(2).
 lv_tablename = 'LD_CT' && lv_index.
 ASSIGN (lv_tablename) TO <ref_data>.

 READ TABLE lt_grouplevels INTO ls_grouplevel INDEX ls_cell-row_id-index.
 IF sy-subrc = 0.
 ls_cell-row_id-index = ls_grouplevel-cindx_from.
 ENDIF.
 "assign specified data table
 ASSIGN <ref_data>->* TO <table>.

 "Normal cell value
 WHEN space.
 ASSIGN mt_data TO <table>.
 ENDCASE.


 "Only read table line when index changes
 AT NEW row_id.
 READ TABLE <table> ASSIGNING <warea> INDEX ls_cell-row_id-index.
 ENDAT.
 "Assign selected fieldname of workarea
 ASSIGN COMPONENT ls_cell-col_id OF STRUCTURE <warea> TO <val>.
 IF sy-subrc = 0.
 "check correct type of field: Only numeric fields will be taken
 DESCRIBE FIELD <val> TYPE lv_val_type.
 CASE lv_val_type.
 WHEN 'P' "Packed
 OR 'N' "Numchar
 OR 'b' "Integer
 OR 'a' "decfloat
 OR 'e' "decfloat
 OR 'F'. "Float?
 "add cell value to total
 ADD <val> TO lv_total.
 ENDCASE.
 ENDIF.
 ENDLOOP.

 IF lv_total IS NOT INITIAL.
 "There were numeric fields selected and therefor we have a total to show:
 MESSAGE s000(oo) WITH 'TOTAL:' space lv_total.
 "Parameterfeld ebenfalls füllen
 p_total = lv_total.
 ENDIF.
 ENDMETHOD.

 METHOD register_events.
 "Set handler
 SET HANDLER handle_delayed_selection FOR mr_grid.
 "register event for delayed selection
 mr_grid->register_delayed_event( mr_grid->mc_evt_delayed_change_select ).
 ENDMETHOD.

 METHOD select_data.
 "Select data
 SELECT * FROM (mv_data_table) INTO TABLE mt_data UP TO 100 ROWS.
 ENDMETHOD.

ENDCLASS.

INITIALIZATION.
 DATA(gr_main) = NEW lcl_main( ).
 gr_main->start( ).

Autosumme in ALV-Grid – Version 2

$
0
0

Im vorherigen Artikel Autosumme in ALV-Grid (Version 1) habe ich euch gezeigt, wie man das Ereignis set_delay_change_selection nutzen kann, um die Summe von markierten Zellen eines ALV-Grids herausfinden kann. Diese Methode hat leider zwei Nachteile:

  1. Die Reaktionszeit des Ereignisses Delayed_Changed_Sel_Callback ist auf 1,5 Sekunden voreingestellt. Das ist viel zu lange, um schnell mal eben einige Werte zu prüfen
  2. Es funktioniert nur, wenn die Datentabelle bekannt ist, denn die Datentabelle des ALV-Grid MT_OUTTAB ist geschützt. Auf sie kann also nicht zugegriffen werden. Es ist demnach nicht möglich, eine Methode zu schreiben, die die Grid-Instanz einfach entgegen nimmt und darauf die Funktion Autosumme anwendet.

Wie kann man nun dieser Herausforderung begegnen? Es gibt zwei Varianten. Die erste Variante habe ich hier bereits beschrieben. Die Lösung ist in diesem Fall, dass man eine eigene Klasse von CL_GUI_ALV_GRID ableitet (erben lässt) und dann eine neue Methode erstellt, die diese geschützte Methode aufruft.

Die andere Variante ist hier von Lukasz Pegiel beschrieben: http://abapblog.com/articles/tricks/105-how-to-access-private-or-protected-data-and-methods-of-cl-gui-alv-grid

In dieser Variante wird einfach das Interface if_alv_rm_grid_friend eingebunden. Hierdurch wird die eigene Klasse als "Freund des ALV-Grid" bekannt gemacht. Und als Freund ist es möglich, auf die geschützten Attribute und Methoden zuzugreifen!

Vielen Dank an dieser Stelle an Lukasz, der mir sehr geholfen hat, diesen Trick Wirklichkeit werden zu lassen! Ich kann jedem nur empfehlen einen oder besser: mehrere Blicke auf seinen ABAPBlog zu werfen.  Besonders hervorzuheben ist seine Entwicklung Fast ALV-Grid

2016-10-07_19-06-25

1. Verbesserung: Änderung der Reaktionszeit

Um das Event zu behandeln und die markierten Zellen auszulesen, bedarf es keiner großen Tricks. Allerdings ist die Reaktionszeit Das ist deutlich zu lange. Mit Hilfe der Methode set_delay_change_selection kann die Reaktionszeit geändert werden. Diese Methode ist allerdings PROTECTED, also geschützt.

Wir müssen also das oben genannte Interface einbinden und haben so Zugriff auf die Methode set_delay_change_selection mit der man die Reaktionszeit in Millisekunden einstellen kann.

2. Verbesserung: Zugriff auf die Datentabelle des ALV-Grid

Zusätzlich wollte ich an das geschützte Attribute MT_OUTTAB heran, um die aktuellen Daten auszulesen. Auf diese Weise ist es möglich, ein universelles Tool zu schreiben, dass mit jedem ALV-Grid zusammen arbeitet und nicht nur lokal.

Wie sieht's aus?

Beide Verbesserungen sind in dieser Version 2 des Codes enthalten. Das Programm besteht nun aus zwei Klassen:

  1. Die Klasse, LCL_MAIN, die den Grid verwendet und darstellt.
  2. Die Klasse LCL_AUTOSUMME, die die eigentliche Funktionalität zur Verfügung stellt.

LCL_AUTOSUMME kann nun also als globale Klasse universell eingesetzt werden. Jedes Grid, dass die Autosummenfunktion verwenden möchte, muss sich in der Klasse mit der Methode REGISTER registrieren. Und dann kann's los gehen...!

Code

 REPORT zz_grid_autosumme2.

PARAMETERS p_total TYPE p DECIMALS 2.

CLASS lcl_autosumme DEFINITION.
 PUBLIC SECTION.
 INTERFACES if_alv_rm_grid_friend .
 CLASS-METHODS register IMPORTING ir_grid TYPE REF TO cl_gui_alv_grid.
 PROTECTED SECTION.
 CLASS-METHODS handle_delayed_selection
 FOR EVENT delayed_changed_sel_callback
 OF cl_gui_alv_grid
 IMPORTING sender.
ENDCLASS.

CLASS lcl_autosumme IMPLEMENTATION.
 METHOD register.
 "Set handler
 SET HANDLER handle_delayed_selection FOR ir_grid.
 "set delayed selection time
 ir_grid->set_delay_change_selection( time = 100 ). " Time in Milliseconds
 "register event for delayed selection
 ir_grid->register_delayed_event( ir_grid->mc_evt_delayed_change_select ).

 ENDMETHOD.

 METHOD handle_delayed_selection.

 "Local data
 DATA lt_cells TYPE lvc_t_cell.
 DATA ls_cell LIKE LINE OF lt_cells.
 DATA lv_total TYPE p DECIMALS 2.
 DATA lv_val_type TYPE c.
 DATA lv_index TYPE n LENGTH 2.
 DATA lv_tablename TYPE string.
 DATA lt_grouplevels TYPE lvc_t_grpl.
 DATA ls_grouplevel LIKE LINE OF lt_grouplevels.

 FIELD-SYMBOLS <ref_data> TYPE REF TO data.
 FIELD-SYMBOLS <table> TYPE table.
 FIELD-SYMBOLS <warea> TYPE any.
 FIELD-SYMBOLS <val> TYPE any.

 "data references to sub totals tables
 DATA ld_ct01 TYPE REF TO data.
 DATA ld_ct02 TYPE REF TO data.
 DATA ld_ct03 TYPE REF TO data.
 DATA ld_ct04 TYPE REF TO data.
 DATA ld_ct05 TYPE REF TO data.
 DATA ld_ct06 TYPE REF TO data.
 DATA ld_ct07 TYPE REF TO data.
 DATA ld_ct08 TYPE REF TO data.
 DATA ld_ct09 TYPE REF TO data.

 "get selected cells (selection via CTRL + Y)
 sender->get_selected_cells( IMPORTING et_cell = lt_cells ).

 "If there is only one cell selected, we do not need to sum that...
 CHECK lines( lt_cells ) > 1.

 "Read all cell values
 LOOP AT lt_cells INTO ls_cell.

 "in case of rowtype (normal cell, total or subtotal) assign correct data table
 CASE ls_cell-row_id-rowtype(1).
 "Total sum of all
 WHEN 'T'.
 ASSIGN sender->mt_ct00 TO <ref_data>.
 ls_cell-row_id-index = 1.
 "subtotals
 WHEN 'S'.
 sender->get_subtotals( IMPORTING
 ep_collect01 = ld_ct01
 ep_collect02 = ld_ct02
 ep_collect03 = ld_ct03
 ep_collect04 = ld_ct04
 ep_collect05 = ld_ct05
 ep_collect06 = ld_ct06
 ep_collect07 = ld_ct07
 ep_collect08 = ld_ct08
 ep_collect09 = ld_ct09
 et_grouplevels = lt_grouplevels ).

 lv_index = ls_cell-row_id-rowtype+4(2).
 lv_tablename = 'LD_CT' && lv_index.
 ASSIGN (lv_tablename) TO <ref_data>.

 READ TABLE lt_grouplevels INTO ls_grouplevel INDEX ls_cell-row_id-index.
 IF sy-subrc = 0.
 ls_cell-row_id-index = ls_grouplevel-cindx_from.
 ENDIF.
 "Normal cell value
 WHEN space.
 ASSIGN sender->mt_outtab TO <ref_data>.
 ENDCASE.

 "assign specified data table
 ASSIGN <ref_data>->* TO <table>.

 "Only read table line when index changes
 AT NEW row_id.
 READ TABLE <table> ASSIGNING <warea> INDEX ls_cell-row_id-index.
 ENDAT.
 "Assign selected fieldname of workarea
 ASSIGN COMPONENT ls_cell-col_id OF STRUCTURE <warea> TO <val>.
 IF sy-subrc = 0.
 "check correct type of field: Only numeric fields will be taken
 DESCRIBE FIELD <val> TYPE lv_val_type.
 CASE lv_val_type.
 WHEN 'P' "Packed
 OR 'N' "Numchar
 OR 'b' "Integer
 OR 'a' "decfloat
 OR 'e' "decfloat
 OR 'F'. "Float?
 "add cell value to total
 ADD <val> TO lv_total.
 ENDCASE.
 ENDIF.
 ENDLOOP.

 IF lv_total IS NOT INITIAL.
 "There were numeric fields selected and therefor we have a total to show:
 MESSAGE s000(oo) WITH 'TOTAL:' space lv_total.
 p_total = lv_total.
 ENDIF.
 ENDMETHOD.

ENDCLASS.

CLASS lcl_main DEFINITION.

 PUBLIC SECTION.
* INTERFACES if_alv_rm_grid_friend .
 METHODS start.
 PROTECTED SECTION.
 DATA mr_grid TYPE REF TO cl_gui_alv_grid.
 DATA mt_data TYPE STANDARD TABLE OF spfli.
 DATA mv_data_table TYPE tabname VALUE 'SPFLI'.
 DATA mr_dock TYPE REF TO cl_gui_docking_container.
 METHODS create_docker.
 METHODS create_grid.
 METHODS select_data.
 METHODS register_autosumme.
ENDCLASS.

CLASS lcl_main IMPLEMENTATION.

 METHOD start.
 select_data( ).
 create_docker( ).
 create_grid( ).
 register_autosumme( ).
 ENDMETHOD.

 METHOD create_docker.
 "Create Docking container at bottom
 CREATE OBJECT mr_dock
 EXPORTING
 side = cl_gui_docking_container=>dock_at_bottom
 ratio = 90
 no_autodef_progid_dynnr = abap_false.

 ENDMETHOD.

 METHOD create_grid.
 "Create ALV-Grid
 CREATE OBJECT mr_grid
 EXPORTING
 i_appl_events = abap_true
 i_parent = mr_dock.

 "and display data
 mr_grid->set_table_for_first_display(
 EXPORTING
 i_structure_name = mv_data_table
 CHANGING
 it_outtab = mt_data ).

 "Set focus on grid so user can directly scroll and select cells via CTRL+Y
 cl_gui_container=>set_focus( mr_grid ).

 ENDMETHOD.

 METHOD select_data.
 "Select data
 SELECT * FROM (mv_data_table) INTO TABLE mt_data UP TO 100 ROWS.
 ENDMETHOD.

 METHOD register_autosumme.
 lcl_autosumme=>register( mr_grid ).
 ENDMETHOD.

ENDCLASS.

INITIALIZATION.
 DATA(gr_main) = NEW lcl_main( ).
 gr_main->start( ).

Zugriff auf nicht unterstützte Ereignisse des CL_SALV_TABLE

$
0
0

Der SALV wird inzwischen gerne von Programmierern verwendet, da einiges einfacher und einleuchtender ist, als beim ALV-Grid. Allerdings ist auch vieles komplizierter und überhaupt nicht einleuchtend. Zudem hat der SALV einige Funktionen nicht, die der ALV-Grid schon lange zur Verfügung stellt, wie zum Beispiel Editierbarkeit. Ich brauchte neulich in einem Projekt das Ereignis "Verzögertes Reagieren auf Markierungen" in einem SALV-Grid. Leider bietet der SALV dieses Event nicht an. Developer Hero 2016 Łukasz Pęgiel hat mir das unten stehende Coding zur Verfügung gestellt. Vielen Dank dafür!

Was macht das Ereignis Delayed_Changed_Sel_Callback?

Das Ereignis wird immer dann - verzögert! - ausgelöst, wenn der Anwender die Markierung im Grid ändert, also zum Beispiel Zeilen, Spalten oder einzelne Zellen markiert. Standardmäßig ist die Verzögerung von DELAYED_CHANGED_SEL_CALLBACK auf 1,5 Sekunden eingestellt. Die Funktionalität kann dafür verwendet werden, um Daten anhand der markierten Zellen nachzulesen und anzuzeigen, ohne dass der Anwender eine weitere Taste drücken muss. 1,5 Sekunden sind allerdings häufig eine zu lange Zeit, denn häufig wechselt ein Anwender kurz vor dieser Zeitspanne die Markierung wieder, weil er der Meinung war, dass nun eh nichts mehr passiert. Wie diese Einstellung geändert werden kann, habe ich in dem Beispiel AUTOSUMME beschrieben.

Was ist nun der Trick?

Der Trick, um an die Events des unterliegenden ALV-Grids zu gelangen, liegt darin, das Ereignis AFTER_REFRESH FOR ALL INSTANCES zu registrieren, also auf alle aktuell instanziierten Grids anzuwenden. Zusätzlich muss der implizit immer vorhandene Importing-Parameter SENDER bei der Definition angegeben werden:

METHODS evh_refresh FOR EVENT after_refresh
      OF cl_gui_alv_grid
      IMPORTING sender.

In dem unten stehenden Programm wird ein einfacher SALV mit Daten aus der Flugdatenbank erzeugt. Es wird das Ereignis AFTER_REFRESH registriert um in diesem Ereignis für den SENDER - also das im SALV verwendete ALV-Grid - das eigentliche Ereignis Delayed_Change_Sel_Callback zu registrieren. Wird die Markierung im Grid geändert, so wird diese Meldung ausgegeben:

2016-10-10_09-46-32

Wer eine Alternative zu SALV und ALV-Grid sucht, der sollte sich das Open-Source-Tool FALV von Łukasz ansehen!

Achtung

Der Zusatz FOR ALL INSTANCES greift wirklich bei ALLEN Instanzen des ALV-Grid!! Deswegen sollte er wirklich nur mit Bedacht benutzt werden und du solltest dir der möglichen Seiteneffekte bewusst sein, denn viele interne Funktionen des SALV und ALV-Grids nutzen ebenfalls wieder ein Grid (Auswahl Feldkatalog, Auswahl Layout etc.). So kann es zu ungewollten Reaktionen - zum Beispiel bei der Änderung des Layouts - kommen:

2016-10-10_09-11-13

Code

REPORT zsalv_grid_events.

CLASS lcl_grid_trick DEFINITION.

 PUBLIC SECTION.

 DATA spfli TYPE STANDARD TABLE OF spfli.
 DATA salv TYPE REF TO cl_salv_table.

 METHODS create_salv.
 METHODS evh_refresh FOR EVENT after_refresh
 OF cl_gui_alv_grid
 IMPORTING sender.
 METHODS evh_delayed_selection
 FOR EVENT delayed_changed_sel_callback
 OF cl_gui_alv_grid
 IMPORTING sender.
 PRIVATE SECTION.
 DATA mv_event_registered TYPE boolean.

ENDCLASS.

CLASS lcl_grid_trick IMPLEMENTATION.

 METHOD create_salv.

 SELECT * UP TO 100 ROWS
 INTO CORRESPONDING FIELDS OF TABLE @spfli
 FROM spfli.

 cl_salv_table=>factory(
 IMPORTING
 r_salv_table = salv
 CHANGING
 t_table = spfli ).

 salv->get_functions( )->set_default( abap_true ).

 SET HANDLER evh_refresh FOR ALL INSTANCES.

 salv->display( ).

 ENDMETHOD.

 METHOD evh_refresh.

 CHECK mv_event_registered = abap_false.
 SET HANDLER evh_delayed_selection FOR sender.
 sender->register_delayed_event( i_event_id = cl_gui_alv_grid=>mc_evt_delayed_change_select ).
 mv_event_registered = abap_true.

 ENDMETHOD.

 METHOD evh_delayed_selection.

 DATA lr_type_description TYPE REF TO cl_abap_typedescr.
 lr_type_description = cl_abap_typedescr=>describe_by_object_ref( sender ).
 MESSAGE i001(00)
 WITH 'Delayed Selection; SENDER ist vom Typ '
 lr_type_description->absolute_name.

 ENDMETHOD.

ENDCLASS.

START-OF-SELECTION.

 DATA(output) = NEW lcl_grid_trick( ).
 output->create_salv( ).
 

If you wanna be my lover – Das FRIENDS-Konzept

$
0
0

Ich programmiere nun schon einige Zeit objektorientiert. Trotzdem - und das ist ja auch das schöne daran - gibt es immer wieder Themen, die in meiner Vorstellung anders sind, als sie sich dann tatsächlich darstellen. Das Friends-Konzept bedeutet, dass Klassen befreundet sein können. Dies muss in den Klassen definiert werden und bedeutet dann, dass die befreundeten Klassen Zugriff auf die geschützten und privaten Attribute und Methoden der Freunde haben.

Das Friends-Konzept kannte ich zwar, hatte aber bisher noch keine Anwendungsmöglichkeiten dafür, da die Friends-Beziehung immer von zwei Seiten ausgehen muss: Klasse A muss Klasse B als Freund eintragen und Klasse B muss Klasse A als Freund bekannt geben.

So dachte ich bisher.

FRIENDS-Interface

Dass es auch einfacher geht, steht klar und deutlich in der SAP-Hilfe:

Geben Sie ein Interface unter Freunde an, dann bedeutet dies, dass alle Klassen, die dieses Interface implementieren, Zugriff auf die geschützten und privaten Komponenten haben.

Das bedeutet, dass man gar nicht die einzelnen befreundeten Klassen in allen Klassen angeben muss, sondern dass es reicht, ein befreundetes Interface zu verwenden.

Das folgende kleine Demoprogramm verwendet das Interface If_You_Wanna_Be_My_Lover (In Anlehnung an den Hit Wannabe der Spice Girls), damit die Klassen gegenseitig auf das geschützte Attribut der jeweils anderen Klasse zugreifen kann.

Ich hätte zwar gerne auch die anderen drei Spice Girls mit in den Code verarbeitet, aber Mel B. und Scary Spice reichen zum Verständnis aus.

Weiterführende Informationen zum FRIENDS-Konzept findest du im ZEVOLVING-Blog von Naimesh Patel.

Realer Nutzen

Wie man dieses Wissen für die tägliche Arbeit nutzen kann, kannst du im ABAPBLOG von Lukasz Pegiel sehen und auch hier im Tricktresor. In diesen Artikel wird beschrieben, dass man das Interface IF_ALV_RM_GRID_FRIEND in eigene Klassen einbinden kann, um dann Zugriff auf geschützte und private Methoden des ALV-Grid zu bekommen.

2016-10-10_15-34-03

Code

REPORT zz_spice_girls.

INTERFACE if_you_wanna_be_my_lover.
ENDINTERFACE.

CLASS lcl_mel_b DEFINITION FRIENDS if_you_wanna_be_my_lover.
 PROTECTED SECTION.
   DATA text TYPE string VALUE 'You gotta get with my friends'.
ENDCLASS.

CLASS lcl_scary_spice DEFINITION FRIENDS if_you_wanna_be_my_lover.
 PROTECTED SECTION.
   DATA text TYPE string VALUE 'If you wanna be my lover'.
ENDCLASS.

CLASS lcl_wannabe DEFINITION FRIENDS if_you_wanna_be_my_lover.
 PUBLIC SECTION.
   INTERFACES if_you_wanna_be_my_lover.
   CLASS-METHODS sing.
   CLASS-METHODS class_constructor.
 PROTECTED SECTION.
   CLASS-DATA mr_mel_b TYPE REF TO lcl_mel_b.
   CLASS-DATA mr_scary_spice TYPE REF TO lcl_scary_spice.
ENDCLASS.

CLASS lcl_wannabe IMPLEMENTATION.
 METHOD class_constructor.
   CREATE OBJECT mr_mel_b.
   CREATE OBJECT mr_scary_spice.
 ENDMETHOD.
 METHOD sing.
   WRITE: / mr_scary_spice->text.
   WRITE: / mr_mel_b->text.
 ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
 lcl_wannabe=>sing( ).

Global. Lokal. Egal.

$
0
0

Im Rahmen eines Projektes haben wir uns mit der dynamischen Erzeugung von Klassen beschäftigt und Stefan hat dabei einen netten Trick herausgefunden.

Globale Klasse

Normalerweise gibt es in einem Report nur die folgenden zwei Möglichkeiten um eine Klasse zu instantiieren:

  1. Ein Objekt mit Referenz zu einer global definierten Klasse (SE24)
  2. Ein Objekt mit Referenz zu einer im selben Programm definierten lokalen Klasse

Stefan hat nun eine Möglichkeit gefunden, wie man lokale Klassen, die in beliebigen Programmen definiert sind, erzeugen kann. Und das geht so:

Lokale Klasse

Wir brauchen ein Programm mit einer lokal definierten Klasse. In dem folgenden Programm werden zwei lokale Klassen definiert. Beide haben die Methode INFO.

REPORT zttloc01.

CLASS lcl_local_01 DEFINITION.
 PUBLIC SECTION.
 METHODS info RETURNING VALUE(text) TYPE string.
ENDCLASS.

CLASS lcl_local_01 IMPLEMENTATION.

METHOD info.
 text = 'Ich bin Klasse 01'.
 ENDMETHOD.
ENDCLASS.

CLASS lcl_local_02 DEFINITION.
 PUBLIC SECTION.
 METHODS info RETURNING VALUE(text) TYPE string.
ENDCLASS.

CLASS lcl_local_02 IMPLEMENTATION.

METHOD info.
 text = 'Ich bin Klasse 02'.
 ENDMETHOD.
ENDCLASS.

Dynamische Erzeugung von Klasseninstanzen

Wenn man den Namen einer Klasse erst zur Laufzeit ermitteln kann, dann kann man ein Objekt dieser Klasse dynamisch wie folgt erzeugen:

DATA classname TYPE string.
DATA object    TYPE REF TO object.
classname = 'ZCL_CLASS_XYZ'.
CREATE OBJECT object TYPE (classname).

Sinnvoller Weise verwendet man hierfür ein Interface, dass alle in Frage kommenden Klassen implementiert haben.

Ihr Auftritt Herr Kollege

Unser Trick besteht nun darin, dass wir den Klassennamen genauer spezifizieren. Wir geben dem Klassennamen die Information mit, in welchem Programm die Klasse vorhanden ist. Der Klassenname sieht dann zum Beispiel wie folgt aus:

\PROGRAM=ZTTLOC01\CLASS=LCL_LOCAL01

Wir müssen also nur noch die intern verwendete Struktur zusammenbasteln. Das folgende Programm erzeugt je nach Auswahl ein Objekt der im Programm ZTTLOC01 lokale definierten Klasse LCL_LOCAL_01 oder LCL_LOCAL_02.

REPORT zttlocaccess.

PARAMETERS p_01 RADIOBUTTON GROUP cls DEFAULT 'X'.
PARAMETERS p_02 RADIOBUTTON GROUP cls.
PARAMETERS p_repid TYPE syrepid DEFAULT 'ZTTLOC01'.

START-OF-SELECTION.

 DATA classname TYPE string.
 DATA r_object TYPE REF TO object.
 DATA text TYPE string.

CASE 'X'.
 WHEN p_01.
   classname = 'LCL_LOCAL_01'.
 WHEN p_02.
   classname = 'LCL_LOCAL_02'.
 ENDCASE.

 CONCATENATE '\PROGRAM=' p_repid '\CLASS=' classname INTO classname.

 CREATE OBJECT r_object TYPE (classname).

 CALL METHOD r_object->('INFO') RECEIVING text = text.
 MESSAGE text TYPE 'I'.

Screenshot

Das rufende Programm muss natürlich genau wissen, was es tut. Je nachdem wie dynamisch der Aufruf einzelner Methoden sein soll, können ebenfalls dynamisch ermittelte Parameter über die Parameterliste übergeben werden.

Verwendet man ein Interface, dass die zu verwendenden Klassen implementieren, dann kann man verwendete Interface-Methoden direkt aufrufen.


Classname Utilities

$
0
0

Klassen sind inzwischen von ABAP nicht mehr wegzudenken. Die Verwaltung der Klassen erfolgt jedoch immer noch in INCLUDES. Diese Includes interessieren in der Regel niemanden. Hin und wieder tauchen diese Klassen-Include jedoch in Shortdumps auf. Der Name der Klasse selbst ist eindeutig aus dem Includenamen zu erkennen. Der Name der Methode jedoch nicht. Häufig gibt es zwar auch die zusätzlich notwendige Information, um welche Methode es sich handelt, aber das ist nicht immer der Fall.

Einzelne Includes sehen zum Beispiel so aus:

CL_OO_CLASSNAME_SERVICE=======CCDEF
CL_OO_CLASSNAME_SERVICE=======CCMAC
CL_OO_CLASSNAME_SERVICE=======CCIMP
CL_OO_CLASSNAME_SERVICE=======CCAU
CL_OO_CLASSNAME_SERVICE=======CU
CL_OO_CLASSNAME_SERVICE=======CO
CL_OO_CLASSNAME_SERVICE=======CI
CL_OO_CLASSNAME_SERVICE=======CP
CL_OO_CLASSNAME_SERVICE=======CT
CL_OO_CLASSNAME_SERVICE=======CS
CL_OO_CLASSNAME_SERVICE=======CM001
CL_OO_CLASSNAME_SERVICE=======CM002
CL_OO_CLASSNAME_SERVICE=======CM003
CL_OO_CLASSNAME_SERVICE=======CM004

CL_OO_CLASSNAME_SERVICE

Um die einzelnen intern verwendeten Objekte (Includes) verwalten zu können, gibt es die Klasse CL_OO_CLASSNAME_SERVICE. Mit dieser Klasse kannst du herausfinden, welche Methode zu welchem Include gehört und umgekehrt.

GET_METHOD_INCLUDE liefert zum Beispiel für die gleichnamige Methode den Includenamen CL_OO_CLASSNAME_SERVICE=======CM00B.

Umgekehrt liefert die Methode GET_METHOD_BY_INCLUDE die Methode zum Includenamen.

die Methode GET_ALL_CLASS_INCLUDES liefert eine komplette Liste der verwendeten Klassen-Includes.

 

Nummerierung

Interessant finde ich, dass die Methodenincludes nicht numerisch hochgezählt werden, sondern erst numerisch (1-9) und dann alphabetisch (A-Z). Bei einem drei-stelligen numerischen Feld, wie das verwendete (==CMnnn) würden 999 Methoden in eine Klasse passen. Eigentlich mehr als ausreichend, sollte man meinen.

 

REDUCE ABAP750 FOR x = u IN n = 1 THEN brainf*ck

$
0
0

Hoijoijoi. Ich gebe zu, ich tue mich echt schwer mit den neuen Befehlen im ABAP-Sprachschatz. Besonders die ganz neuen Features im ABAP Release 7.50 (oder 7.40 – ich blick da nicht mehr durch) fordern mich ziemlich (REDUCE, COND, FILTER etc).

Angeregt durch den Artikel von Jerry Wang im neuen SCN über REDUCE habe ich mich mit dem Befehl-REDUCE näher beschäftigt. Über die ABAP-Doku bin ich dann auf die Demoprogramme DEMO_REDUCE* gestolpert und beim DEMO_REDUCE_SIMPLE hängen geblieben.

Das Programm ermittelt mit Hilfe des REDUCE-Befehls die Summe von Werten in einer Tabelle. Das Programm DEMO_REDUCE_COND_ITERATION erzeugt mit Hilfe einer FOR-Schleife zusammengesetzte Texte (1 2 3 4 usw).

Ich wollte dann ein bisserl mit den Features herumspielen und hatte die Idee, einen HTML-Text zusammen zusetzen.

Aus der Tabelle

<HTML>
<BODY>
<P>

wollte ich die einzelnen Elemente plus einem separaten Text zu einem String zusammenfügen. Das funktionierte auch noch sehr einfach:

DATA(text) = REDUCE string(
               INIT html = ``
                FOR command IN html_commands 
                NEXT html = |{ html }{ command }| ) 
            && 'Hallo Welt'.

Eigentlich nur zum Spaß habe ich versucht, ob ich mit einem erneuten && auch ein erneutes REDUCE benutzen kann. Obwohl ich einigermaßen überrascht war, dass es anstandslos funktionierte, wäre ich auch maßlos enttäuscht gewesen, wenn es nicht geklappt hätte… 😉

Der nächste Schritt war dann etwas komplizierter: Ich wollte die einzelnen Tags auch wieder schließen. Natürlich in umgekehrter Reihenfolge. Und mit dem SLASH, der ein Ende-Tag anzeigt. Hier brauchte es dann etliche Versuche und verwirrte Blicke in die Doku um zu dem folgenden Ergebnis zu gelangen:

Und hier der Quelltext dazu:

REPORT zdemo_reduce_simple.

CLASS demo DEFINITION.
  PUBLIC SECTION.
  CLASS-METHODS main.
ENDCLASS.

CLASS demo IMPLEMENTATION.
  METHOD main.

  DATA(html_commands) = VALUE string_table(
                              ( `<html>` )
                              ( `<body>` )
                              ( `<p>` ) ).
  cl_demo_output=>write( html_commands ).


  DATA(text) = REDUCE string(
                INIT html = ``
                 FOR command IN html_commands NEXT html = |{ html }{ command }| )
             && 'Hallo Welt'
             && REDUCE string( INIT html = ``
                FOR n = lines( html_commands )
                THEN n - 1
                WHILE n > 0
                 NEXT html = html && COND #( LET command = html_commands[ n ]
                                              IN WHEN command IS NOT INITIAL
                                                 THEN command(1) && '/' && command+1 ) ).

   cl_demo_output=>write( text ).
   cl_demo_output=>display( ).
 ENDMETHOD.

ENDCLASS.

START-OF-SELECTION.
 demo=>main( ).

Ich lasse es einfach so unkommentiert stehen. Wenn man weiß, was der Ausdruck macht, ist es einigermaßen klar.

Hier der Beitrag von Horst Keller zum Thema ABAP-740 Features: https://blogs.sap.com/2014/09/30/abap-news-for-740-sp08-iteration-expressions/

Table Expressions

Sehr gewöhnungsbedürftig für mich ist der Zusatz um die einzelne Tabellenzeile (die Tabelle hat ja keine Struktur) anzusprechen:

COND #( LET command = html_commands[ n ]
         IN WHEN command IS NOT INITIAL
            THEN command(1) && '/' && command+1 )

Mein erster Ansatz war Folgendes (was aber nicht funktionierte):

html_commands[ n ]-TABLE_LINE

Also bin ich zum COND-Ausdruck gekommen. Hier finde ich verwirrend, dass anscheinend zwingend eine WHEN-Klausel angegeben werden muss. Vielleicht gibt es auch eine einfachere Alternative?! Bestimmt. Lasst es mich gerne wissen.

Link zur Horst Kellers Blog: https://blogs.sap.com/2013/05/29/abap-news-for-release-740-table-expressions/

 

Ein komisches Gefühl, auf ein mal wieder Anfänger zu sein…

Der guten alten Zeiten Willen noch mal das Coding mit ABAPvor740. Viel Länger ist es auch nicht. Und ob die Programmierung mit ABAP740 eleganter oder besser ist, wage ich zu bezweifeln.

DATA text TYPE string.
LOOP AT html_commands INTO DATA(command).
  text = text && command.
ENDLOOP.
text = text && 'Hallo Welt'.

DATA line TYPE i.
line = lines( html_commands ).
DO lines( html_commands ) TIMES.
  DATA(cmd) = html_commands[ line ].
  text = text && cmd(1) && '/' && cmd+1.
  SUBTRACT 1 FROM line.
ENDDO.

Der Beitrag REDUCE ABAP750 FOR x = u IN n = 1 THEN brainf*ck erschien zuerst auf Tricktresor.

ABAP 740-Features unter der Lupe

$
0
0

Aus einer einfachen Anfängerfrage im abapforum.com hat sich eine recht spannende Antwortserie entwickelt, die auf die neuen Sprachfeatures von ABAP740 eingeht. Ich habe diese einmal zusammen gefasst und auch Laufzeitmessungen durchgeführt.

Die Frage

Die Frage von debianfan lautete: Wie ermittele ich die Anzahl von Datensätzen bestimmter Ausprägung in einer internen Tabelle?

Die interne Tabelle NAMES besteht nur aus den Feldern

  • NAME (string)
  • TF (boolean)

Die folgenden Lösungen sind teilweise vereinfacht und ohne DATA-Definitionen. Die einzelnen lauffähigen Lösungen sind unten im Beispielprogramm ersichtlich.

Lösung 1 – 2xLOOP+WHERE(DATA)

Die einfachste und auf der Hand liegende Antwort von Tron war:

LOOP AT names INTO name WHERE tf = abap_true.
  ADD 1 TO zaehler_true.
ENDLOOP.

LOOP AT names INTO name WHERE tf = abap_false.
  ADD 1 TO zaehler_false.
ENDLOOP.

Die Lösung ist einfach und verständlich.

Der Einwand von Ralf war, dass bei WHERE die gesamte Tabelle durchlaufen werden muss, wenn kein Index verwendet wird. Das kann sich bei großen Tabellen negativ auf die Laufzeit auswirken.

Mein Gedanke war, dass ich zwei LOOPs nicht schön finde und außerdem ein LOOP mit einer Case-Anweisung noch einen Tacken einfacher und deutlich sein müsste. Dazu später mehr.

Lösung 2 – FILTER

Haubi hat dann den Vorschlag gemacht, die einzelnen Einträge mittels FILTER zu zählen:

DATA(lv_true)  = lines( FILTER #( names WHERE tf = abap_true ) ).
 DATA(lv_false) = lines( FILTER #( names WHERE tf = abap_false ) ).

Diese Lösung finde ich sehr schlank und gut lesbar. Was mich hier stört, ist, dass durch FILTER alle verarbeiteten Tabelleneinträge kopiert werden. Es werden alle Datensätze die der WHERE-Anweisung entsprechen in eine neue Tabelle kopiert. Die Tabelle ist zwar temporär und wird nur für die Zeit der Verarbeitung des FILTER-Befehls verwendet, aber bei großen Tabellen kann sich die zusätzliche Speicherlast negativ auswirken.

Lösung 3 – REDUCE

Ich wollte dann unbedingt noch eins drauf setzen und eine Lösung haben, die auch bei vielen Ausprägungen von TF funktioniert und die Werte von TF nicht bekannt sind. Zudem wollte ich komplett die neuen Sprachfeatures verwenden.

Bei beiden vorhergehenden Lösungen fand ich es nicht gut, dass gezielt im Programm auf ABAP_TRUE und ABAP_FALSE abgefragt wurde. In diesem Beispiel ist es in Ordnung, weil das die Vorgabe war. Der häufigere Fall ist jedoch, dass eine Gruppe viele und gegebenenfalls nicht bekannte Ausprägungen hat (Verkaufsorganisation, Datum, Materialnummer, etc.).

Meine Lösung bestand dann aus einer Kombination aus VALUE und REDUCE:

DATA(sum) = VALUE ttf( FOR GROUPS grp OF <name> IN names
                        WHERE ( name IS NOT INITIAL )
                        GROUP BY ( tf = <name>-tf )
                          ( tf    = grp
                            count = REDUCE #( INIT i = 0
                                       FOR name IN names
                                       WHERE ( tf = grp )
                                       NEXT i = i + 1 ) ) ).

Diese Lösung baut eine Tabelle auf aus TF und COUNT, so dass alle Gruppenwerte mit der entsprechenden Anzahl Einträge in der Tabelle SUM landen.

Eigentlich müsste diese Lösung die langsamste sein, denn es werden zuerst die Gruppen gebildet. Dafür muss die gesamte Tabelle durchlaufen werden. Dann werden zu jedem Gruppeneintrag erneut die zugehörigen Einträge gelesen und gezählt. Deswegen wollte ich zuerst gar keine Laufzeitmessung machen. Die Herausforderung für mich war in erster Linie, die Problemstellung mit den neuen Sprachfeatures abzubilden, da ich mich mit der Syntax eher schwer tue.

Lösung 4 – 1xLOOP+WHERE(DATA)

Ich habe mit den vorhandenen drei Lösungen ein Testprogramm geschrieben um die Laufzeit mit der Transaktion SAT analysieren zu können.

Allerdings habe ich gemerkt, dass ich die Lösung von Tron falsch übernommen hatte, nämlich folgendermaßen:

LOOP AT names INTO name.
  CASE name-tf.
    WHEN abap_true.
      ADD 1 TO zaehler_true.
    WHEN abap_false.
      ADD 1 TO zaehler_false.
  ENDCASE.
ENDLOOP.

Anstatt zweier LOOPs hatte ich nur einen LOOP und eine CASE-Abfrage.

Da ich die schon dabei war zu testen, wollte ich Trons Code genau so übernehmen, da ich davon ausging, dass meine Variante mit CASE schneller sein würde. Allerdings war dem nicht so…

Update

Zusätzlich zu den LOOP-Lösungen, die mit dem Zusatz INTO workarea arbeiten, habe ich noch die Varianten mit ASSIGNING (Feldsymbol) und TRANSPORTING NO FIELDS aufgenommen.

Lösung 5 – 1xLOOP+CASE(Fieldsymbol)

Die Lösung mit einem LOOP und CASE-Anweisung jedoch mit LOOP-ASSIGNING.

Lösung 6 – 2xLOOP+WHERE(Fieldsymbol)

Die Lösung mit zwei LOOPs und entsprechender WHERE-Bedingung jedoch mit LOOP-ASSIGNING.

Lösung 7 – 2xLOOP+WHERE(ohne Feldtransport)

Die Lösung mit zwei LOOPs und entsprechender WHERE-Bedingung jedoch mit dem Zusatz TRANSPORTING NO FIELDS.

Laufzeitanalyse

 

Der Vollständigkeit halber habe ich die Messung auch noch einmal mit der Variante „SORTED TABLE“ durchgeführt. Und wieder war ich überrascht: Die Variante mit Sorted Table ist deutlich langsamer als die Variante mit Standard Table…

Hier das Ergebnis der Laufzeitmessungen mit 100.000 Datensätzen und STANDARD TABLE:

Variante          Laufzeit
P01_REDUCE         76.602
P02_FILTER         36.755
P03_LOOP_CASE      33.891
P04_LOOP_WHERE     27.282
P05_LOOP_CASE_FS   25.097
P06_LOOP_WHERE_FS  18.805
P07_LOOP_WHERE_NO  17.774

Code

Methode rnd_name baut aus zufälligen Buchstaben Fantasienamen auf.

Methode rnd_bool liefert per Zufall den Wert TRUE oder FALSE zurück.

Die Methoden p01 – p07 enthalten die jeweils erwähnten Lösungsvarianten.

REPORT.
" http://www.abapforum.com/forum/viewtopic.php?f=1&t=21900&p=82017#p82017

PARAMETERS p TYPE i DEFAULT 100000.

CLASS help DEFINITION.
 PUBLIC SECTION.
 CLASS-METHODS rnd_name RETURNING VALUE(name) TYPE string.
 CLASS-METHODS rnd_bool RETURNING VALUE(tf) TYPE boolean.
 CLASS-METHODS class_constructor.
 CLASS-METHODS p01_reduce.
 CLASS-METHODS p02_filter.
 CLASS-METHODS p03_loop_case.
 CLASS-METHODS p04_loop_where.
 CLASS-METHODS p05_loop_case_fs.
 CLASS-METHODS p06_loop_where_fs.
 CLASS-METHODS p07_loop_where_no.
 PROTECTED SECTION.
 CLASS-DATA rnd TYPE REF TO cl_abap_random.
 TYPES:
 BEGIN OF lst_names,
 name TYPE string,
 tf TYPE abap_bool,
 END OF lst_names,
 ltt_names TYPE STANDARD TABLE OF lst_names
 WITH NON-UNIQUE KEY name
 WITH NON-UNIQUE SORTED KEY key_tf COMPONENTS tf.

* ltt_names TYPE SORTED TABLE OF lst_names
* WITH NON-UNIQUE KEY name
* WITH NON-UNIQUE SORTED KEY key_tf COMPONENTS tf.
 CLASS-DATA names TYPE ltt_names.
ENDCLASS.

CLASS help IMPLEMENTATION.
 METHOD class_constructor.
 rnd = cl_abap_random=>create( ).
 names = VALUE ltt_names( FOR i = 1 THEN i + 1 WHILE i <= p
 ( name = help=>rnd_name( ) tf = help=>rnd_bool( ) ) ).

 ENDMETHOD.

 METHOD rnd_name.
 DATA(len) = rnd->intinrange( low = 5 high = 40 ).
 DO len TIMES.
 DATA(pos) = rnd->intinrange( low = 0 high = 25 ).
 name = name && sy-abcde+pos(1).
 ENDDO.
 ENDMETHOD.

 METHOD rnd_bool.
 CASE rnd->intinrange( low = 0 high = 1 ).
 WHEN 0.
 tf = abap_false.
 WHEN 1.
 tf = abap_true.
 ENDCASE.
 ENDMETHOD.

 METHOD p01_reduce.
 TYPES:
 BEGIN OF stf,
 tf TYPE abap_bool,
 count TYPE i,
 END OF stf,
 ttf TYPE SORTED TABLE OF stf WITH UNIQUE KEY tf.

 DATA(sum) = VALUE ttf( FOR GROUPS grp OF <name> IN names
 WHERE ( name IS NOT INITIAL )
 GROUP BY ( tf = <name>-tf )
 ( tf = grp
 count = REDUCE #( INIT i = 0
 FOR name IN names
 WHERE ( tf = grp )
 NEXT i = i + 1 ) ) ).
* cl_demo_output=>display_data( sum ).
 ENDMETHOD.

 METHOD p02_filter.
 DATA(lv_true) = lines( FILTER #( names USING KEY key_tf WHERE tf = abap_true ) ).
 DATA(lv_false) = lines( FILTER #( names USING KEY key_tf WHERE tf = abap_false ) ).

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.

 METHOD p03_loop_case.

 DATA lv_true TYPE i.
 DATA lv_false TYPE i.

 LOOP AT names INTO DATA(name).
 CASE name-tf.
 WHEN abap_true. ADD 1 TO lv_true.
 WHEN abap_false. ADD 1 TO lv_false.
 ENDCASE.
 ENDLOOP.

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.

 METHOD p04_loop_where.

 DATA lv_true TYPE i.
 DATA lv_false TYPE i.

 LOOP AT names INTO DATA(name) WHERE tf = abap_true.
 ADD 1 TO lv_true.
 ENDLOOP.
 LOOP AT names INTO name WHERE tf = abap_false.
 ADD 1 TO lv_false.
 ENDLOOP.

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.

 METHOD p05_loop_case_fs.

 DATA lv_true TYPE i.
 DATA lv_false TYPE i.

 LOOP AT names ASSIGNING FIELD-SYMBOL(<name>).
 CASE <name>-tf.
 WHEN abap_true. ADD 1 TO lv_true.
 WHEN abap_false. ADD 1 TO lv_false.
 ENDCASE.
 ENDLOOP.

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.

 METHOD p06_loop_where_fs.

 DATA lv_true TYPE i.
 DATA lv_false TYPE i.

 LOOP AT names ASSIGNING FIELD-SYMBOL(<name>) WHERE tf = abap_true.
 ADD 1 TO lv_true.
 ENDLOOP.
 LOOP AT names ASSIGNING <name> WHERE tf = abap_false.
 ADD 1 TO lv_false.
 ENDLOOP.

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.

 METHOD p07_loop_where_no.

 DATA lv_true TYPE i.
 DATA lv_false TYPE i.

 LOOP AT names TRANSPORTING NO FIELDS WHERE tf = abap_true.
 ADD 1 TO lv_true.
 ENDLOOP.
 LOOP AT names TRANSPORTING NO FIELDS WHERE tf = abap_false.
 ADD 1 TO lv_false.
 ENDLOOP.

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.


ENDCLASS.

START-OF-SELECTION.


 help=>p01_reduce( ).
 help=>p02_filter( ).
 help=>p03_loop_case( ).
 help=>p04_loop_where( ).
 help=>p05_loop_case_fs( ).
 help=>p06_loop_where_fs( ).
 help=>p07_loop_where_no( ).

Der Beitrag ABAP 740-Features unter der Lupe erschien zuerst auf Tricktresor.

REDUCE + SWITCH + COND [ABAP740]

$
0
0

Heute im Code-Dojo hatte ich die Aufgabe gestellt, eine Funktion zu schreiben, die einen String mit variabel zu bestimmender Länge und zufälligen Zeichenfolgen aus Zahlen und Buchstaben zurück liefert. Zum Beispiel „I71B7HJ4BG“ oder „6EE17ICBF54IE486EHD8“.

Idee

Mit VALUE und FOR sollte ein String Zeichen für Zeichen zusammengesetzt werden. Mit einer Zufallsfunktion sollte ermittelt werden, ob ein Buchstabe oder eine Zahl eingesetzt werden soll. Per SWITCH sollte ebenfalls eine Zufallsfunktion aufgerufen werden, die eine Zahl bzw. einen Buchstaben zurück liefert.. Per String-Konkatenation sollten die zufälligen Zeichen zusammengesetzt werden.

Abweichung

die Aufgabe lässt sich mit VALUE nicht lösen. Stattdessen muss REDUCE genommen werden.

Code

Für jede Stelle des zu generierenden Strings (FOR – UNTIL – NEXT) wird eine Funktion RND_TYPE aufgerufen. Diese gibt zufällig den Wert TRUE oder FALSE zurück. Per SWITCH-Anweisung wird entschieden, ob eine Zahl (FALSE) oder ein Buchstabe (TRUE) generiert werden soll. Das generierte Zeichen wird per String-Konkatenation Zeichen für Zeichen zusammengebaut.

 

REPORT.

CLASS main DEFINITION.
   PUBLIC SECTION.
     DATA rnd_num TYPE REF TO cl_abap_random_int.
     DATA rnd_chr TYPE REF TO cl_abap_random_int.
     METHODS constructor.
     METHODS rnd_type
       RETURNING VALUE(type) TYPE boolean.
     METHODS create_random_string
       IMPORTING max           TYPE i
       RETURNING VALUE(string) TYPE string.
     METHODS get_random_char
       RETURNING VALUE(char) TYPE char01.
     METHODS get_random_number
       RETURNING VALUE(number) TYPE numc01.
 ENDCLASS.

CLASS main IMPLEMENTATION.
   METHOD constructor.
     rnd_chr = cl_abap_random_int=>create( seed = CONV #( sy-uzeit ) min = 0 max = 25 ).
     rnd_num = cl_abap_random_int=>create( seed = CONV #( sy-uzeit ) min = 0 max = 9 ).
   ENDMETHOD.
   METHOD rnd_type.

    type = COND #( LET random = get_random_number( ) IN
                    WHEN random <= 5 THEN abap_true
                    ELSE abap_false ).
   ENDMETHOD.

  METHOD get_random_char.
     DATA(offset) = rnd_num->get_next( ).
     char = sy-abcde+offset(1).
   ENDMETHOD.

  METHOD get_random_number.
     number = rnd_num->get_next( ).
   ENDMETHOD.

  METHOD create_random_string.

    string = REDUCE #( INIT text = ``
                        FOR i = 1
                        UNTIL i > max
                        NEXT text = text && SWITCH #( rnd_type( )
                                              WHEN abap_true  THEN get_random_char( )
                                              WHEN abap_false THEN get_random_number( ) ) ).

  ENDMETHOD.

ENDCLASS.

PARAMETERS p_len type i DEFAULT 10.
PARAMETERS p_str TYPE char20 MODIF ID a.

AT SELECTION-SCREEN OUTPUT.
   LOOP AT SCREEN.
     CASE screen-group1.
       WHEN 'A'.
         screen-input = '0'.
         MODIFY SCREEN.
     ENDCASE.
   ENDLOOP.

AT SELECTION-SCREEN.
   p_str = NEW main( )->create_random_string( p_len ).

Lessons Learned

Mit REDUCE können Operationen auf einen Datentyp „reduziert“ werden. Mit VALUE funktioniert das nicht.

string = REDUCE #( INIT text = ``
                   FOR i = 1
                   UNTIL i > max
                   NEXT text = text && SWITCH #( rnd_type( )
                                         WHEN abap_true  THEN get_random_char( )
                                         WHEN abap_false THEN get_random_number( ) ) ).

Mit SWITCH können nur Exakte Werte abgefragt werden (wie bei CASE auch mit OR verknüpft). Es sind jedoch keine „Größer-/ Kleiner-Vergleiche“ möglich.

[...] SWITCH #( rnd_type( )
        WHEN abap_true  THEN get_random_char( )
        WHEN abap_false THEN get_random_number( ) ) ).

Mit COND können beliebige Bedingungen geprüft werden. Allerdings muss hier jede Bedingung separat angegeben werden. Wenn der abzufragende Wert das Ergebnis einer Funktion ist, so sollte mit LET gearbeitet werden, um nicht für jede Bedingung die Funktion aufrufen zu müssen.

var = COND #( LET random = get_random_number( ) IN
              WHEN random <= 5 THEN abap_true
              ELSE abap_false ).

Die implizite Typ-Definition mit INIT (bei der REDUCE-Anweisung) ist mit Vorsicht zu genießen! Ich hatte aus Gewohnheit einen leeren „String“ mit Hochkomma-Space-Hochkomma definiert. In Wirklichkeit hatte ich damit aber einen CHAR(1)-Feld definiert und die Funktion hat immer nur ein Zeichen zurück geliefert. Die String-Konkatenation hat diesen fest definierten Typ also nicht automatisch erweitert, so wie es beim String der Fall ist. Erst die Verwendung eines echten Strings durch die Backticks liefert das gewünschte Ergebnis.

Es kann auch der Typ direkt angegeben werden (INIT text TYPE string) aber dann ist keine Vorbelegung mehr möglich. Eine implizite Definition durch Vorbelegung ist dann jedoch wieder durch die Verwendung von CONV möglich: INIT text = CONV string( ‚hallo‘ )

Bei der FOR-Funktion (FOR i = 1) muss das Hochzählen der Variable (THEN i + 1) nicht zwingend definiert werden! Wird THEN nicht angegeben, so wird implizit die Inkrementierung um Eins vorgenommen:

[...] FOR i = 1 UNTIL i > 10 [...]

 

Der Beitrag REDUCE + SWITCH + COND [ABAP740] erschien zuerst auf Tricktresor.

ALV-Grid um Sortierfunktion erweitern (Vererbung)

$
0
0

Objektorientierte Programmierung ist häufig immer noch ein rotes Tuch für viele. Man weiß zwar, wie Methoden aufgerufen werden und dass ein Objekt mit CREATE OBJECT oder NEW erzeugt werden muss aber die Designprinzipien sind irgendwie unklar. Und SAP-Klassen sind eh unantastbar.

In diesem Artikel möchte ich dir eine Möglichkeit vorstellen, wie du den SAP-Standard mit Standardmitteln, nämlich mit Hilfe der Vererbung, erweitern kannst.

Enjoy und Bedienung

Trotz der GUI-Elemente, die unter dem Schlagwort ENJOY eingeführt wurden, sind viele Elemente immer noch nicht wirklich benutzerfreundlich. Einiges kann man ändern, anderes nicht. Eine Möglichkeit um mit Hilfe der SAP-Standardcontrols ein neues Look & Feel zu erzeugen, habe ich in diesem Beitrag gezeigt: Moderne UI mit altem SAPGUI und ALV-Grid

Hier habe ich das Standard-ALV-Grid verwendet, um eine neue Funktionalität zu erzeugen. Dies ist allerdings ein eigenständiges Objekt und erweitert nicht die Standardfunktionalität des ALV-Grids.

Wie das Überschreiben von geschützten Methoden generell funktioniert, habe ich hier beschrieben: Geschützte Methoden nutzen

Dieser Artikel soll zeigen, dass es sich eventuell lohnt, auch über andere Erweiterungen von SAP-Standardfunktionalitäten nachzudenken. Es gibt Funktionalitäten, die eventuell in jedem ALV-Grid hilfreich wären. Zum Beispiel das einfache Umsortieren von Einträgen.

Umsortierung mittels Drag & Drop

Eine Möglichkeit ist die Sortierung mittels Drag&Drop im ALV-Feldkatalog:

Die Bedienung ist hier zwar auch gewöhnungsbedürftig, denn ein Eintrag, der umsortiert werden soll, muss erst mit einem Klick markiert und kann dann erst mittels Drag & Drop an eine andere Stelle verschoben werden, aber immerhin.

Umsortierung mit Funktionstasten

Eine andere Möglichkeit wäre das Verschieben von Einträgen mit Funktionstasten. Folgender Screenshot ist aus dem unten stehenden Demo-Programm. Das ALV-Grid wurde um die Funktionstasten „Sort Up“ und „Sort Down“ erweitert.

Wie das im Einzelnen geht, erkläre ich gleich.

Redefinition

eine wirklich starke Waffe des objektorientierten Sprachumfangs ist die Vererbung. Sofern die anzupassende Klasse nicht als „Final“ definiert wurde, können geschützte und öffentliche Methoden redefiniert werden. Leider wird bei Anlage einer Klasse das Kennzeichen „Final“ vorbelegt, so dass der Programmierer dieses aktiv entfernen muss. Ist dieses Kennzeichen gesetzt, funktioniert die Ableitung bzw. Vererbung nicht.

Die Klasse CL_GUI_ALV_GRID allerdings darf vererbt werden, denn sie ist nicht als final gekennzeichnet. Nichts desto Trotz muss bei Klassen sehr genau definiert werden, welche Methoden vererbt werden dürfen und welche nicht.

Funktion „Umsortieren“

Die neue Funktion nenne ich „Umsortieren“, denn es ist keine Sortieren-Funktion, die man aus dem Standard kennt. Um Einträge umsortieren zu können, wird in der Regel im SAP mit einem Sortierfeld gearbeitet, das der Anwender manuell pflegen muss. Dazu wird häufig in Zehnerschritten gearbeitet, um später Einträge einfügen zu können. Einträge in dieser Form in eine andere Reihenfolge zu bringen ist in der Regel sehr mühselig.

Die Idee ist, dass der Anwender einen Eintrag markieren kann und diesem mit den Funktionstasten „Sort Up“ und Sort Down“ in der Liste hoch und runter verschieben kann. Ein Feld, in der die aktuelle Reihenfolge festgehalten wird, brauchen wir natürlich trotzdem.

Vererbung

Ich möchte die Klasse CL_GUI_ALV_GRID also für meine Zwecke missbrauchen und muss sie deswegen ableiten bzw. vererben. Dazu lege ich in der Transaktion SE80 oder SE24 eine neue Klasse an: ZCL_GUI_ALV_GRID_SORT und gebe als Oberklasse zu zu beerbende Klasse CL_GUI_ALV_GRID an:

Sortierfeld

Der Programmierer muss angeben können, welches Feld für die Sortierung der Einträge verwendet werden soll. In diesem Feld wird dann die automatische Nummerierung anhand der Reihenfolge gesetzt. Ich lege dafür die Methode SET_SORT_FIELD an mit dem Übergabeparameter FIELDNAME.

Diese Methode muss vor SET_TABLE_FOR_FIRST_DISPLAY aufgerufen werden, damit die Umsortierfunktionalität von Anfang an zur Verfügung steht.

Das Sortierfeld merke ich mir im Attribut MV_SORT_FIELD.

Ereignis TOOLBAR

Da ich das Ereignis TOOLBAR nutzen möchte, um die neuen Funktionstasten einzubauen, muss ich es für meine abgeleitete Klasse registrieren:

SET HANDLER on_toolbar FOR me.

Zusätzlich benötige ich eine Methode, die beim Auslösen des Ereignisses angesprungen wird: ON_TOOLBAR. Diese Methode muss als Ereignisbehandler für das Ereignis TOOLBAR definiert werden:

Der Methode stehen nun theoretisch alle Parameter des Ereignisses zur Verfügung. Allerdings müssen diese manuell übernommen werden. Die Drucktaste „Ereignisparameter“ in der Sicht „Parameter“ erledigt das für mich:

In der Methode füge ich die Drucktasten SORT_UP und SORT_DOWN der Toolbar hinzu.

METHOD on_toolbar.

    check mv_sort_field is NOT INITIAL.

    APPEND VALUE #(   function  = 'Sort_down'
                      icon      = icon_next_page
                      quickinfo = space
                      butn_type = if_sat_ui_button_types=>normal
                      disabled  = space
                      text      = 'Sort down'
                      checked   = space ) TO e_object->mt_toolbar.
    APPEND VALUE #(   function  = 'Sort_up'
                      icon      = icon_previous_page
                      quickinfo = space
                      butn_type = if_sat_ui_button_types=>normal
                      disabled  = space
                      text      = 'Sort up'
                      checked   = space ) TO e_object->mt_toolbar.

  ENDMETHOD.

Redefinition DISPATCH

Um intern auf die Drucktasten reagieren zu können, muss ich die Methode DISPATCH redefinieren und meine Drucktasten SORT_UP und SORT_DOWN für das Ereignis TOOLBAR_BUTTON_CLICK abfangen.

In allen anderen Fällen muss die Methode DISPATCH der abgeleiteten Klasse aufgerufen werden (SUPER->DISPATCH).

Im Falle des Ereignisses TOOLBAR_BUTTON_CLICK muss ich mir noch die Ereignisparameter besorgen in denen der Funktionscode der Drucktaste steht (Methode GET_EVENT_PARAMETER).

Nun gilt es noch, folgendes zu tun:

  • Abfrage auf die Funktionscodes SORT_UP und SORT_DOWN
  • Zugriff auf die Datentabelle erhalten
  • Ermitteln der aktuellen Cursorposition
  • Umsortieren des Eintrags
  • Neunummerierung
  • Cursor auf die umsortierte Zeile setzen
  • Anzeige aktualisieren
METHOD dispatch.

    DATA action TYPE string.
    CASE eventid.
      WHEN evt_toolbar_button_click.
        CALL METHOD get_event_parameter
          EXPORTING
            parameter_id = 0
            queue_only   = space
          IMPORTING
            parameter    = action.
        CALL METHOD cl_gui_cfw=>flush.
        CASE action.
          WHEN 'Sort_up'.
            CALL METHOD get_current_cell
              IMPORTING
                es_row_id = DATA(ls_row)
                es_row_no = DATA(ls_row_no).
            FIELD-SYMBOLS <outtab> TYPE table.
            ASSIGN mt_outtab->* TO <outtab>.
            IF ls_row-index > 1.
              READ TABLE <outtab> ASSIGNING FIELD-SYMBOL(<outline>) INDEX ls_row-index.
              DATA(indx) = ls_row-index - 1.
              ASSIGN COMPONENT 2 OF STRUCTURE <outline> TO FIELD-SYMBOL(<value>).
              IF sy-subrc = 0.
                INSERT <outline> INTO <outtab> INDEX indx.
                indx = indx + 2.
                DELETE <outtab> INDEX indx.
                indx = indx - 2.

                LOOP AT <outtab> ASSIGNING <outline>.
                  ASSIGN COMPONENT mv_sort_field OF STRUCTURE <outline> TO <value>.
                  <value> = sy-tabix.
                ENDLOOP.
                refresh_table_display( is_stable = VALUE #( col = abap_true row = abap_true ) i_soft_refresh = abap_true ).
                set_selected_rows( it_row_no = VALUE #( ( row_id = indx ) ) ).
              ENDIF.
            ENDIF.
            EXIT.
          WHEN 'Sort_down'.
            CALL METHOD get_current_cell
              IMPORTING
                es_row_id = ls_row
                es_row_no = ls_row_no.
            ASSIGN mt_outtab->* TO <outtab>.
            IF ls_row-index < lines( <outtab> ).
              READ TABLE <outtab> ASSIGNING <outline> INDEX ls_row-index.
              indx = ls_row-index + 2.
              ASSIGN COMPONENT 2 OF STRUCTURE <outline> TO <value>.
              IF sy-subrc = 0.
                INSERT <outline> INTO <outtab> INDEX indx.
                indx = indx - 2.
                DELETE <outtab> INDEX indx.
                indx = indx + 1.
                LOOP AT <outtab> ASSIGNING <outline>.
                  ASSIGN COMPONENT mv_sort_field OF STRUCTURE <outline> TO <value>.
                  <value> = sy-tabix.
                ENDLOOP.

                refresh_table_display( is_stable = VALUE #( col = abap_true row = abap_true ) i_soft_refresh = abap_true ).
                set_selected_rows( it_row_no = VALUE #( ( row_id = indx ) ) ).
              ENDIF.
            ENDIF.
            EXIT.
        ENDCASE.
    ENDCASE.


    super->dispatch(
      EXPORTING
        cargo             = cargo
        eventid           = eventid
        is_shellevent     = is_shellevent
        is_systemdispatch = is_systemdispatch
      EXCEPTIONS
        cntl_error        = 1
        OTHERS            = 2 ).

  ENDMETHOD.

Testprogramm

In folgendem Testprogramm kannst du die Verwendung des neuen Standards sehen. Du siehst, dass trotz meines Eingriffs in die Toolbar weiterhin Drucktasten hinzugefügt werden können:

REPORT zz_alv_grid_sort.

DATA gs_data TYPE vbak.

SELECT-OPTIONS s_vbeln FOR gs_data-vbeln.


CLASS main DEFINITION.
  PUBLIC SECTION.
    TYPES ty_data       TYPE vbak.

    TYPES ty_data_t     TYPE STANDARD TABLE OF ty_data
                             WITH DEFAULT KEY.

    DATA ms_data        TYPE ty_data.
    DATA mt_data        TYPE ty_data_t.

    DATA mr_grid        TYPE REF TO zcl_gui_alv_grid_sort.
    METHODS start.
  PROTECTED SECTION.
    METHODS selection.
    METHODS display.
    METHODS handle_toolbar      FOR EVENT toolbar
                  OF cl_gui_alv_grid
      IMPORTING e_object.
    METHODS handle_user_command FOR EVENT user_command
                  OF cl_gui_alv_grid
      IMPORTING e_ucomm sender.

ENDCLASS.

CLASS main IMPLEMENTATION.

  METHOD handle_user_command.

    DATA lt_rows TYPE lvc_t_row.
    DATA ls_row   TYPE lvc_s_row.
    DATA ls_data  TYPE ty_data.

    CASE e_ucomm.
      WHEN 'USER01'.
        sender->get_selected_rows( IMPORTING et_index_rows = lt_rows ).
        LOOP AT lt_rows INTO ls_row.
          READ TABLE mt_data INTO ls_data INDEX ls_row-index.
          IF sy-subrc = 0.
            MESSAGE i000(oo) WITH 'Usercommand 01: Beleg' ls_data-vbeln.
          ENDIF.
        ENDLOOP.
    ENDCASE.
  ENDMETHOD.

  METHOD handle_toolbar.

    DATA: ls_toolbar  TYPE stb_button.

*** Trenner
    CLEAR ls_toolbar.
    MOVE 3 TO ls_toolbar-butn_type.
    APPEND ls_toolbar TO e_object->mt_toolbar.

*** Icon “Test”
    CLEAR ls_toolbar.
    MOVE icon_generate              TO ls_toolbar-icon.
    MOVE 'USER01'                   TO ls_toolbar-function.
    MOVE 'User 01'                  TO ls_toolbar-quickinfo.
    MOVE 'Userbutton 01'            TO ls_toolbar-text.
    APPEND ls_toolbar TO e_object->mt_toolbar.

  ENDMETHOD.


  METHOD start.
    selection( ).
    display( ).
  ENDMETHOD.

  METHOD selection.
    SELECT * FROM vbak INTO TABLE mt_data UP TO 10 ROWS.
  ENDMETHOD.

  METHOD display.

    WRITE 'DUMMY'.

    CREATE OBJECT mr_grid
      EXPORTING
        i_parent      = cl_gui_container=>screen0
        i_appl_events = space.

    mr_grid->set_sort_field( 'ERNAM' ).

    SET HANDLER handle_toolbar      FOR mr_grid.
    SET HANDLER handle_user_command FOR mr_grid.


    DATA lv_structure_name    TYPE dd02l-tabname VALUE 'VBAK'.
    DATA ls_variant           TYPE disvariant.
    DATA lv_save              TYPE char01 VALUE 'U'.
    DATA lv_default           TYPE char01 VALUE abap_true.
    DATA ls_layout            TYPE lvc_s_layo.

    ls_layout-sel_mode       = 'A'.
    ls_layout-grid_title     = 'Titel'.

    mr_grid->set_table_for_first_display(
      EXPORTING
        i_structure_name              = lv_structure_name
        is_variant                    = ls_variant
        i_save                        = lv_save
        i_default                     = lv_default
        is_layout                     = ls_layout
      CHANGING
        it_outtab                     = mt_data ).

  ENDMETHOD.
ENDCLASS.


START-OF-SELECTION.
  NEW main( )->start( ).

 

Ergebnis

Du kannst nun den Cursor auf einen Eintrag stellen und durch Klicken auf „Sort Up“ oder „Sort Down“ den Eintrag umsortieren. Die Sortierung wird in dem Feld „ERNAM – Angelegt von“ vorgehalten.

Fazit

Die Änderung von SAP-Standardfunktionen ist möglich. Allerdings ist das erstens nicht immer so einfach, wie in diesem Artikel beschrieben. In der Regel muss man genau und langwierig debuggen und prüfen, wo welche Methoden verwendet werden können. Zudem müssen die Funktionen natürlich ausgiebig getestet werden. Immerhin sollen sie genau wie die Standardfunktionalität zuverlässig funktionieren.

Des Weiteren sollten Funktionen, die wirklich in einer Vielzahl von eigenen Programmierungen eingesetzt werden sauber ausprogrammiert werden. In dem hier vorgestellten Beispiel sollte zum Beispiel sichergestellt werden, dass das Feld mit der Sortierung auch wirklich im Feldkatalog vorhanden ist. Die Benutzereigene Sortierung muss irgendwie berücksichtigt werden.

Zudem sollte es natürlich möglich sein, auch mehrere Zeilen zu markieren und diese en bloc zu verschieben. Das hängt aber wiederum von der Programmierung ab, ob wirklich mehrere Zeilen markiert werden dürfen oder nicht.

Ebenso wäre die Eingangs erwähnte Sortierung mittels Drag & Drop sinnvoll. Diese könnte dann allerdings einer anderen vom Programmierer erstellten Drag & Drop Funktionalität in die Quere kommen.

Allerdings lohnt es sich, hier Aufwand zu investieren, denn die erweiterte Funktionalität kann eventuell viele separate Programmierungen überflüssig machen oder vorhandene Programmierungen auf einfache Weise benutzerfreundlicher machen.

Der Beitrag ALV-Grid um Sortierfunktion erweitern (Vererbung) erschien zuerst auf Tricktresor.

Mehrfachselektion mit Menu

$
0
0

Möchte man dem Anwender die Möglichkeit geben, aus einer Liste mehrere Einträge auszuwählen, so fällt einem sicherlich zuerst das ALV-Grid ein, in dem man die Einträge per Zeilenmarkierung oder Checkbox markieren kann. Etwas aufgepeppt könnte es so aussehen wie in diesem Beitrag: Moderne UI mit altem SAPGUI und ALV-Grid. Diese Variante nimmt allerdings einiges an Platz ein. Eine Lösung mit der vertikalen Anordnung einer Toolbar (CL_GUI_TOOLBAR) und Menu (CL_CTMENU) käme auch in Frage, nähme jedoch genau so viel Platz ein, wie ein ALV-Grid. Vorteil wäre noch, dass man Drucktasten als „gedrückt“ definieren und dem Anwender so eine gute Rückmeldung geben könnte.

Das Demoprogramm SAPTOOLBAR_DEMO1 zeigt, wie es aussehen könnte (Drucktaste „+ check“):

Menu

Bei der Anforderung, mehrere Einträge aus einer Liste von Optionen auswählen zu können, kam ich auf die Idee, das CL_CTMENU dafür zu verwenden. In einem Menü kann man nämlich einzelne Einträge so markieren, dass sie als „ausgewählt“ zu erkennen sind. Sie haben dann einen „Punkt“ vor dem Eintrag stehen. Den ersten Gedanken daran habe ich jedoch wieder verworfen, denn bei einer Liste mit mehreren Einträgen, kann es ziemlich mühselig sein, für jede zu wählende Option das Menü anzuklicken, die entsprechende Option zu wählen um dann das Menü erneut anzuklicken.

Als zweiten Gedanken hatte ich jedoch die Idee, sofort nach Auswahl eines Eintrags das Ereignis „Dropdown ausgewählt“ erneut zu feuern, so dass das – nun jedoch geänderte Menü – dem Anwender erneut angezeigt wird. Erfreulicherweise hat dies sogar funktioniert:

blogs.sap.com

Hier noch eine kleine Abhandlung auf blogs.sap.com von mir, bei der ich noch andere Arten der Mehrfachselektion aufzeige.

abapGit

https://github.com/tricktresor/multiple_selections

Code

Das unten stehende Beispielprogramm demonstriert die Funktionsweise. Ich verwende an einigen Stellen die neuen Möglichkeiten von ABAP 7.40 und ABAP 7.50.

Um das Modul auch sinnvoll einsetzen zu können, fehlen noch Methoden um die möglichen Optionen zu übergeben und die letztendlich gewählten Einträge wieder zurück zu liefern.

PROGRAM zz_menu_demo.


CLASS lcl_main DEFINITION.
  PUBLIC SECTION.
    METHODS display.

  PROTECTED SECTION.
    TYPES: BEGIN OF ty_option,
             value   TYPE char10,
             text    TYPE string,
             checked TYPE boolean_flg,
           END OF ty_option.
    DATA: mytoolbar    TYPE REF TO cl_gui_toolbar,
          menupos_x    TYPE i,
          menupos_y    TYPE i,
          options      TYPE STANDARD TABLE OF ty_option,
          menu_dynamic TYPE REF TO cl_ctmenu.
    METHODS build_menu.
    METHODS on_function_selected FOR EVENT function_selected OF cl_gui_toolbar
      IMPORTING fcode sender.
    METHODS on_dropdown_clicked  FOR EVENT dropdown_clicked OF cl_gui_toolbar
      IMPORTING fcode posx posy sender.
ENDCLASS.                    "lcl_my_event_handler DEFINITION



CLASS lcl_main IMPLEMENTATION.

  METHOD build_menu.

    IF menu_dynamic IS INITIAL.
      "Create menu
      CREATE OBJECT menu_dynamic.
    ELSE.
      "Clear all entries before rebuild
      menu_dynamic->clear( ).
    ENDIF.

    LOOP AT options ASSIGNING FIELD-SYMBOL(<option>).
      "add menu entry with current status
      menu_dynamic->add_function( fcode   = CONV #( <option>-value )
                                  checked = <option>-checked
                                  text    = CONV #( <option>-text ) ).

    ENDLOOP.

  ENDMETHOD.

  METHOD display.

    "Create docker on Top of the screen
    DATA(docker) = NEW cl_gui_docking_container( side = cl_gui_docking_container=>dock_at_top extension = 30 ).

    "create toolbar object
    mytoolbar = NEW #( parent = docker ).

    "register events
    mytoolbar->set_registered_events( VALUE #( ( eventid = cl_gui_toolbar=>m_id_function_selected )
                                               ( eventid = cl_gui_toolbar=>m_id_dropdown_clicked ) ) ).

    "Set handler
    SET HANDLER on_function_selected FOR mytoolbar.
    SET HANDLER on_dropdown_clicked  FOR mytoolbar.

    "set initial values
    options = VALUE #( ( value = 'ONE'   text = 'Option One' )
                       ( value = 'TWO'   text = 'Option Two' )
                       ( value = 'THREE' text = 'Option Three' )
                       ( value = 'FOUR'  text = 'Option Four' ) ).
    "Build menu
    build_menu( ).

    "Add button for selecting options
    mytoolbar->add_button( EXPORTING
                             icon             = 'ICON_TOOL'
                             fcode            = 'CHOOSE'
                             butn_type        = '1'
                             text             = 'Select options'
                             quickinfo        = 'Select some options...'
                           EXCEPTIONS
                             cntb_error_fcode = 1 ).

  ENDMETHOD.

  METHOD on_function_selected.

    "switch option entry
    LOOP AT options ASSIGNING FIELD-SYMBOL(<option>).
      IF <option>-value = fcode.
        IF <option>-checked = abap_true.
          <option>-checked = abap_false.
        ELSE.
          <option>-checked = abap_true.
        ENDIF.
      ENDIF.
    ENDLOOP.

    "rebuild menu
    build_menu( ).

    "raise event dropdown clicked again
    sender->dispatch( cargo = 'mytoolbar' eventid = cl_gui_toolbar=>m_id_dropdown_clicked is_shellevent = abap_false ).

    "Set coordinates of menu
    sender->track_context_menu(
         context_menu = menu_dynamic
         posx         = menupos_x
         posy         = menupos_y ).

  ENDMETHOD.                    "lcl_my_event_handler

  METHOD on_dropdown_clicked.

    IF fcode = 'CHOOSE'.
      "call of dropdown: remember current position for displaying menu
      menupos_x = posx.
      menupos_y = posy.
    ENDIF.

    "Set coordinates
    mytoolbar->track_context_menu(
        context_menu = menu_dynamic
        posx         = posx
        posy         = posy ).

  ENDMETHOD.                    "lcl_my_event_handler

ENDCLASS.                    "lcl_my_event_handler IMPLEMENTATION


INITIALIZATION.

  new lcl_main( )->display( ).


  PARAMETERS p_test.

 

Der Beitrag Mehrfachselektion mit Menu erschien zuerst auf Tricktresor.

Pflegeview mit Datennavigation

$
0
0

Pflegeviews kennt jeder. Sie werden zu einer Tabelle oder einem View generiert und erlauben eine mehr oder weniger komfortable Dateneingabe. Mit Pflegeviews sind die meisten Customizingfunktionen realisiert worden.

Da der Tabellenpflegedialog generiert wird und von SAP seit Jahren nicht weiterentwickelt wird – ich hätte eine Menge einfacher Verbesserungsvorschläge – muss man mit dem Leben, was vorhanden ist. Die Eingabe oder die Funktionen können durch Zeitpunkte angepasst werden.

Ab einer bestimmten Größe, also wenn ziemlich viele Schlüsselfelder vorhanden sind, wird die Eingabe und die Kontrolle der vorhandenen Daten sehr mühselig.

Datennavigation

Um die Daten besser sichten zu können und sozusagen durch die Daten surfen zu können, hatte ich die Idee, einen ganz bestimmten Tree-Control anzubinden, der die Daten hierarchisch darstellt. Die Darstellung der Daten funktioniert natürlich mit allen Tree-Arten, aber es gibt eine Klasse, die eine ganz besondere Fähigkeit hat: Bei der Klasse CL_GUI_ALV_TREE_SIMPLE kann die Hierarchie zur Laufzeit geändert werden.

Der Anwender kann sich so also eine ganz eigene Sicht auf die Tabelle zusammenklicken. Ein Klick auf den entsprechenden Knoten soll dann die SM30 aufrufen. Die Anzeige wird auf die Daten eingeschränkt, die durch die Hierarchie gegeben sind.

Um das Ganze zu verdeutlichen, habe ich eine Demotabelle mit vielen Schlüsselfeldern gebaut und ein paar fiktive Daten eingefügt. Die Tabelle stellt eine typische Customizingtabelle dar, wo zu einer bestimmten Kombination von organisatorischen Werten Optionen aktiv sind oder nicht:

Wenn man sich hier mit ein paar tausend Einträgen, die durchaus realistisch sind, zurecht finden möchte, dann braucht man schon etwas Geduld und Wissen, wie man die einzelnen Einträge Filtern kann.

Vorbereitung

Um die Daten zu lesen und anzeigen zu können, musste ich zwei grundsätzliche Dinge tun, die, wenn man weiß wie, nicht schwer sind:

  • Erzeugen einer Tabelle mit genau der Struktur der vorgegebenen Tabelle
  • Daten zu einer beliebigen Tabelle/ View lesen

Dynamisch Tabelle erzeugen

Das Erzeugen der Tabelle geht extrem einfach:

DATA mr_data TYPE REF TO data.
FIELD-SYMBOLS <lt_data> TYPE STANDARD TABLE.
CREATE DATA mr_data TYPE STANDARD TABLE OF (tabellenname).
ASSIGN mr_data->* TO <lt_data>.

In TABELLENNAME steht der Name des Views. Im Feldsymbol <LT_DATA> steht nun die Tabelle zur Verfügung, die genau die gleichen Eigenschaften hat, als hätte ich sie direkt im Programm angegeben:

DATA lt_data TYPE STANDARD TABLE OF tabellenname.

Viewdaten lesen

Wenn es sich um eine Tabelle handelt, dann kann ich die Daten einfach mit SELECT ermitteln. Bei einem Tabellenpflegeview geht das nicht. Dieser ist nur für die Verwendung in der SM30 gedacht, nicht für die Datenselektion.

Aber das Problem hatte wohl vor mir auch schon jemand und hat den Funktionsbaustein VIEW_GET_DATA geschrieben.

CALL FUNCTION 'VIEW_GET_DATA'
      EXPORTING
        view_name = tabellenname
      TABLES
        data      = <lt_data>
      EXCEPTIONS
        OTHERS    = 6.

Die Selektion der Daten ist also auch kein Problem.

Klasse CL_GUI_ALV_TREE_SIMPLE

Kommen wir nun zu dem spannenden Teil und meiner eigentlichen Idee zur Navigation in den Daten. Die Darstellung der Daten aus dem Tabellenpflegeview möchte ich hierarchisch darstellen. Die Klasse CL_GUI_ALV_SIMPLE_TREE erstellt die Hierarchie fast automatisch.

Die Klasse benötigt eine Tabelle und eine Information, nach welchen Tabellenfeldern der Aufriss erfolgen soll. Wie bereits erwähnt, hat die Klasse CL_GUI_ALV_TREE_SIMPLE die besondere Eigenschaft, dass der Aufriss zur Laufzeit geändert werden kann:

Wie bei einem normalen ALV üblich, kann das Layout auch gespeichert werden, so dass man sich häufig genutzte Hierarchien speichern und wieder laden kann.

Navigation

Nun ist die bloße Anzeige der Daten nicht sonderlich hilfreich. Deswegen habe ich einen Doppelklick auf die Knoten und Items des Baumes programmiert. Mit einem Doppelklick sollen die Daten bis zu dieser Hierarchiestufe angezeigt werden. Wenn ich also einen Doppelklick auf die oberste Ebene, die Verkaufsorganisation 1000 mache, dann sollen im View nur die Daten mit Verkaufsorganisation 1000 angezeigt werden. Wenn ich einen Doppelklick auf den untergeordneten Vertriebsweg 10 mache, sollen nur die Daten von VkOrg 1000 und Vertriebsweg 10 angezeigt werden:

Das funktioniert auch ganz gut, denn den Tabellenpflegedialog kann man nicht nur über die Transaktion SM30 aufrufen, sondern auch über den Funktionsbaustein VIEW_MAINTENANCE_CALL. Diesem Funktionsbaustein gibt man grob die folgenden Daten mit:

  • Tabellenname
  • Aktion (Anzeige oder Ändern)
  • Selektionstabelle

Der Clou hierbei ist die Selektionstabelle, in der ich anhand der jeweiligen Doppelklick-Position im Baum genau die zugrunde liegenden Daten übergebe. Beim Doppelklick werden folgende beiden Werte geliefert:

  • Die Hierarchiestufe
  • Der Tabellenindex der zugrunde liegenden Datentabelle

Ich ermittele dafür beim Doppelklick die aktuelle Hierarchiedefinition, lese den zugrunde liegenden Tabelleneintrag und nehme dann die Werte aus der aktuellen Hierarchiestufe in die Selektionstabelle auf.

Beispiel

Obige Hierarchie zeigt

  • Verkaufsorganisation
    • Vertriebsweg
      • Sparte

Ich mache einen Doppelklick auf den Eintrag Vertriebsweg 10 der VkOrg 1000. Das Doppelklickereignis des Trees sagt mir als Hierarchiestufe VTWEG und Tabellenzeile 2.

Ich mache einen Loop über die aktuelle Hierarchie und weise per ASSIGN COMPONENT dieses Feld der Tabellenzeile einem weiteren Feldsymbol zu. Den Feldnamen und den Wert dieses Feldes wird an die Selektionstabelle angehängt. So lange, bis ich die aktuelle Hierarchiestufe erreicht habe.

Hierarchie ändern

Wenn ich nun nicht über die Verkaufsorganisation an die Daten ran möchte, sondern zum Beispiel über das Material, dann kann ich einfach die Hierarchie ändern:

Die Darstellung im Baum ist entsprechend und ich kann mit einem Doppelklick auf ein Material schnell alle Einträge auswählen, die dieses Material enthalten:

Wo bin ich

Eine Schwäche der Baumdarstellung ist, dass ich nicht genau, bzw. nicht gut erkennen kann, wo ich mich gerade befinde. Leider sind die Methoden, die den Aufbau der Hierarchie steuern als PRIVATE Methoden angelegt. Es ist also nicht möglich, die Klasse zu beerben und entsprechend anzupassen.

Ich fände es sinnvoll, wenn ich diesem Falle der Eintrag nicht 1000, 2000 usw. heißen würde, sondern „Verkaufsorganisation 1000“ usw. Das würde deutlich machen, welche Hierarchiestufe es ist.

Eine einfache Möglichkeit habe ich jedoch gefunden, um die Darstellung anzupassen. Es kann ein Gruppenstufen-Layout definiert werden. Hier ist es möglich, für jede Stufe der Hierarchie ein Icon zu definieren. Da man im Icon auch eine Quickinfo mitgeben kann, lässt sich folgende Ausgabe erzeugen:

Wenn man im Layout des SAPGUI einstellt, dass die Quickinfo sofort angezeigt wird, ist das eine akzeptable Lösung.

Doppelklick

Um die Navigation so einfach und intuitiv wie möglich zu machen, habe ich nicht nur NODE_DOUBLE_CLICK ausprogrammiert, sondern auch ITEM_DOUBLE_CLICK. Ich finde es immer nervig, wenn man irgendwo draufklickt und nichts passiert. Oder wenn man nur ein Element angeklickt hat und dann die Meldung kommt: „Bitte markieren Sie einen Knoten“.

Call Screen

Leider hat die Lösung eine große Macke: Da mit jedem Doppelklick der Tabellenpflegedialog erneut aufgerufen wird, wird mit jedem Aufruf ein CALL SCREEN ausgeführt. Das ist jedoch nur etwa 50 mal möglich.

Ein LEAVE TO SCREEN 0 sorgt zwar dafür, dass die Aufrufhierarchie wieder abgebaut wird, allerdings gibt es bei der Verwendung von LEAVE TO SCREEN 0 in der Doppelklick-Eventhandlermethode merkwürdige Seiteneffekte beim Blättern im Pflegedialog.

Ich habe leider keine Möglichkeit gefunden, um die Daten direkt im View zu aktualisieren, ohne den VIEW_MAINTENANCE_CALL erneut auszuführen.

Weitere Infos

Um möglichst viele Informationen über den Tabellenpflegedialog zu bekommen – und auch um zu wissen, ob überhaupt ein Pflegedialog existiert 🙂 – rufe ich den Baustein VIEW_GET_DDIC_INFO auf. In der Tabelle TVDIR, die der Baustein unter anderem liest, steht zum Beispiel, in welcher Funktionsgruppe der Pflegedialog erstellt wurde. Das ist wichtig für externe Perform-aufrufe, mit denen man evtl. Daten manipulieren möchte. Es gibt zum Beispiel die Routine VIM_SET_GLOBAL_FIELD_VALUE, mit der globale Felder geändert werden können:

DATA(prog) = |SAPL{ ms_tvdir-area }|.
DATA(rc) TYPE i.
PERFORM vim_set_global_field_value 
     IN PROGRAM (prog) 
  USING 'VIM_NEXT_SCREEN' 
        'N' 
        '0' 
         rc.

Das funktioniert aber nur, wenn auch der Aufruf „extern“ erfolgt. Für einen externen Aufruf müssen ein paar sehr intime Infos übergeben werden, die aber fast alle vom VIEW_GET_DDIC_INFO ermittelt werden.

Mit der Routine TABLE_CALL_INFO und der Funktion „READ“ werden die Daten gelesen und mit der Funktion „EDIT“ werden die Daten im Änderungsmodus dargestellt.

DATA(prog) = |SAPL{ ms_tvdir-area }|.
    PERFORM table_call_function IN PROGRAM (prog)
     TABLES lt_dba_sellist
            lt_dpl_sellist
            mt_x_header
            mt_x_namtab
            lt_excl_func
      USING 'READ'
            'VERY_SHORT'
            lv_updflag.

    PERFORM table_call_function IN PROGRAM (prog)
     TABLES lt_dba_sellist
            lt_dpl_sellist
            mt_x_header
            mt_x_namtab
            lt_excl_func
      USING 'EDIT'
            'VERY_SHORT'
            lv_updflag.

Ich habe es, wie gesagt, leider nicht geschafft, die Daten nur zu aktualisieren, nachdem der View einmal dargestellt wurde.

Filterung

Normalerweise kann man in einem ALV Daten filtern. Der CL_GUI_ALV_SIMPLE_TREE basiert auf einem ALV aber leider kann hier nicht gefiltert werden. Die Funktion müsste aber leicht nachgestellt werden können. Eventuell kümmere ich mich da später noch mal drum.

Select-Options

Sinnvoll wäre es natürlich auch, ein Selektionsbild für den View anzubieten, so dass der Anwender eine Vorauswahl treffen kann.

Dies müsste mit den freien Selektionsbedingungen abbildbar sein, aber da hatte ich bisher noch keine Lust zu. In diesem Beitrag steht jedoch, wie diese zu verwenden sind: Dynamisches Selektionsbild

Mit dem Funktionsbaustein VIEW_RANGETAB_TO_SELLIST können die Selektionsoptionen einfach in die für den Pflegedialog notwendige Selektionstabelle überführt werden.

AbapGit

Der gesamte Code inklusive Tabellendefinition und Tabellenpflegedialog steht bei github.com:

https://github.com/tricktresor/blog

Coding

REPORT ztrcktrsr_sm30_navigation.

PARAMETERS p_table TYPE tabname DEFAULT 'ZTT_DEMO1'.

CLASS lcl_tree DEFINITION.
  PUBLIC SECTION.
    TYPES tt_sellist           TYPE STANDARD TABLE OF vimsellist.

    DATA mo_tree               TYPE REF TO cl_gui_alv_tree_simple.
    DATA mt_sort               TYPE lvc_t_sort. "Sortiertabelle
    DATA mr_data               TYPE REF TO data.
    DATA ms_tvdir              TYPE tvdir.
    DATA mv_callstack_counter  TYPE i.

    DATA mt_sellist            TYPE STANDARD TABLE OF vimsellist.
    DATA mt_x_header           TYPE STANDARD TABLE OF vimdesc.
    DATA mt_x_namtab           TYPE STANDARD TABLE OF vimnamtab.

    METHODS handle_node_double_click
                  FOR EVENT node_double_click OF cl_gui_alv_tree_simple
      IMPORTING grouplevel index_outtab.
    METHODS handle_item_double_click
                  FOR EVENT item_double_click OF cl_gui_alv_tree_simple
      IMPORTING grouplevel index_outtab fieldname.
    METHODS build_sort_table.
    METHODS register_events.
    METHODS set_view IMPORTING viewname TYPE clike RAISING cx_axt.
    METHODS get_view_data.
    METHODS init_tree.
    METHODS constructor.
    METHODS view_maintenance_call IMPORTING it_sellist TYPE tt_sellist.

ENDCLASS.

DATA main TYPE REF TO lcl_tree.

CLASS lcl_tree IMPLEMENTATION.
  METHOD constructor.
  ENDMETHOD.

  METHOD set_view.
    SELECT SINGLE * FROM tvdir INTO ms_tvdir WHERE tabname = viewname.
    IF sy-subrc > 0.
      RAISE EXCEPTION TYPE cx_axt.
    ENDIF.
  ENDMETHOD.

  METHOD handle_item_double_click.
    "Pass click on item to handle_node_double_click
    handle_node_double_click(
      grouplevel   = grouplevel
      index_outtab = index_outtab ).

  ENDMETHOD.

  METHOD handle_node_double_click.

    FIELD-SYMBOLS <lt_data>            TYPE STANDARD TABLE.
    ASSIGN mr_data->* TO <lt_data>.
    DATA lt_dba_sellist                TYPE STANDARD TABLE OF vimsellist.
    DATA ls_dbasellist                 TYPE  vimsellist.

    "Get current hierarchy
    mo_tree->get_hierarchy( IMPORTING et_sort = DATA(lt_sort) ).

    IF grouplevel = space.
      "clicked on entry
      ASSIGN <lt_data>[ index_outtab ] TO FIELD-SYMBOL(<ls_data>).
      CHECK sy-subrc = 0.

      LOOP AT lt_sort INTO DATA(ls_sort).
        ASSIGN COMPONENT ls_sort-fieldname OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_value>).
        IF sy-subrc <> 0.
          EXIT.
        ENDIF.
        APPEND INITIAL LINE TO lt_dba_sellist ASSIGNING FIELD-SYMBOL(<ls_sellist>).
        <ls_sellist>-viewfield = ls_sort-fieldname.
        <ls_sellist>-operator  = 'EQ'.
        <ls_sellist>-value     = <lv_value>.
        <ls_sellist>-and_or    = 'AND'.
        READ TABLE mt_x_namtab TRANSPORTING NO FIELDS WITH KEY viewfield = ls_sort-fieldname.
        <ls_sellist>-tabix     = sy-tabix.
      ENDLOOP.

    ELSE.
      "Clicked on hierarchy node
      ASSIGN <lt_data>[ index_outtab ] TO <ls_data>.
      IF sy-subrc = 0.
        LOOP AT lt_sort INTO ls_sort.
          "Fill up all field from start of hierarchy to clicked node
          ASSIGN COMPONENT ls_sort-fieldname OF STRUCTURE <ls_data> TO <lv_value>.
          IF sy-subrc <> 0.
            EXIT.
          ENDIF.
          APPEND INITIAL LINE TO lt_dba_sellist ASSIGNING <ls_sellist>.
          <ls_sellist>-viewfield = ls_sort-fieldname.
          <ls_sellist>-operator  = 'EQ'.
          <ls_sellist>-value     = <lv_value>.
          <ls_sellist>-and_or    = 'AND'.
          READ TABLE mt_x_namtab TRANSPORTING NO FIELDS WITH KEY viewfield = ls_sort-fieldname.
          <ls_sellist>-tabix     = sy-tabix.
          IF ls_sort-fieldname = grouplevel.
            EXIT.
          ENDIF.
        ENDLOOP.
      ENDIF.
    ENDIF.

    CHECK <ls_data> IS ASSIGNED.

    IF mv_callstack_counter > 50.
      MESSAGE 'Navigation not possible anymore. Sorry' TYPE 'I'.
      RETURN. "handle_double_click
    ENDIF.

    ADD 1 TO mv_callstack_counter.

    view_maintenance_call( lt_dba_sellist ).

  ENDMETHOD.


  METHOD get_view_data.

    FIELD-SYMBOLS <lt_data> TYPE STANDARD TABLE.
    CREATE DATA mr_data TYPE STANDARD TABLE OF (ms_tvdir-tabname).
    ASSIGN mr_data->* TO <lt_data>.


    "Get info about table/ view
    CALL FUNCTION 'VIEW_GET_DDIC_INFO'
      EXPORTING
        viewname        = ms_tvdir-tabname
      TABLES
        sellist         = mt_sellist
        x_header        = mt_x_header
        x_namtab        = mt_x_namtab
      EXCEPTIONS
        no_tvdir_entry  = 1
        table_not_found = 2
        OTHERS          = 3.
    IF sy-subrc = 0.
      "Get data of view
      CALL FUNCTION 'VIEW_GET_DATA'
        EXPORTING
          view_name = ms_tvdir-tabname
        TABLES
          data      = <lt_data>
        EXCEPTIONS
          OTHERS    = 6.
    ENDIF.

  ENDMETHOD.                               " BUILD_OUTTAB

  METHOD build_sort_table.

    DATA ls_sort TYPE lvc_s_sort.
    DATA lv_idx  TYPE i.

    LOOP AT mt_x_namtab INTO DATA(ls_namtab)
    WHERE keyflag   = abap_true
      AND datatype <> 'CLNT'.
      ADD 1 TO lv_idx.
      ls_sort-fieldname = ls_namtab-viewfield.
      ls_sort-seltext   = ls_namtab-scrtext_l.
      ls_sort-spos      = lv_idx.
      ls_sort-up        = abap_true.
      APPEND ls_sort TO mt_sort.
    ENDLOOP.

  ENDMETHOD.                               " BUILD_SORT_TABLE


  METHOD register_events.

    mo_tree->set_registered_events( VALUE #(
          "Used here for applying current data selection
          ( eventid = cl_gui_column_tree=>eventid_node_double_click )
          ( eventid = cl_gui_column_tree=>eventid_item_double_click )
          "Important! If not registered nodes will not expand ->No data
          ( eventid = cl_gui_column_tree=>eventid_expand_no_children ) ) ).

    SET HANDLER handle_node_double_click FOR mo_tree.
    SET HANDLER handle_item_double_click FOR mo_tree.

  ENDMETHOD.                               " register_events


  METHOD init_tree.

    get_view_data( ).
    build_sort_table( ).

    DATA(docker) = NEW cl_gui_docking_container(
                            ratio = 25
                            side  = cl_gui_docking_container=>dock_at_left
                            dynnr = CONV #( ms_tvdir-liste )
                            repid = |SAPL{ ms_tvdir-area }| "'SAPLSVIM'
                            no_autodef_progid_dynnr = abap_false ).

* create tree control
    mo_tree = NEW #( i_parent              = docker
                     i_node_selection_mode = cl_gui_column_tree=>node_sel_mode_multiple
                     i_item_selection      = 'X'
                     i_no_html_header      = ''
                     i_no_toolbar          = '' ).



* register events
    register_events( ).


    FIELD-SYMBOLS <lt_data> TYPE STANDARD TABLE.
    ASSIGN mr_data->* TO <lt_data>.

    DATA lt_grouplevel        TYPE lvc_t_fimg.
    DATA ls_grouplevel        TYPE lvc_s_fimg.
    DATA lv_field_description TYPE text50.
    DATA lt_dba_sellist       TYPE STANDARD TABLE OF vimsellist.

    LOOP AT mt_sort INTO DATA(ls_sort).
      ls_grouplevel-grouplevel = ls_sort-fieldname.
      lv_field_description = mt_x_namtab[ viewfield = ls_sort-fieldname ]-scrtext_l.
      CALL FUNCTION 'ICON_CREATE'
        EXPORTING
          name       = 'ICON_OPEN_FOLDER'
          text       = ls_sort-fieldname
          info       = lv_field_description
          add_stdinf = ' '
        IMPORTING
          result     = ls_grouplevel-exp_image.
      CALL FUNCTION 'ICON_CREATE'
        EXPORTING
          name       = 'ICON_CLOSED_FOLDER'
          text       = ls_sort-fieldname
          info       = lv_field_description
          add_stdinf = ' '
        IMPORTING
          result     = ls_grouplevel-n_image.
      APPEND ls_grouplevel TO lt_grouplevel.
    ENDLOOP.

* create hierarchy
    CALL METHOD mo_tree->set_table_for_first_display
      EXPORTING
        i_save               = 'A'
        is_variant           = value #( report = sy-repid username = sy-uname )
        i_structure_name     = ms_tvdir-tabname
        it_grouplevel_layout = lt_grouplevel
      CHANGING
        it_sort              = mt_sort
        it_outtab            = <lt_data>.

    "expand first level
    mo_tree->expand_tree( 1 ).

    " optimize column-width
    CALL METHOD mo_tree->column_optimize
      EXPORTING
        i_start_column = mt_sort[ 1 ]-fieldname
        i_end_column   = mt_sort[ lines( mt_sort ) ]-fieldname.

    view_maintenance_call( lt_dba_sellist ).

  ENDMETHOD.

  METHOD view_maintenance_call.

    CALL FUNCTION 'VIEW_MAINTENANCE_CALL'
      EXPORTING
        action      = 'S'
        view_name   = ms_tvdir-tabname
      TABLES
        dba_sellist = it_sellist
      EXCEPTIONS
        OTHERS      = 15.

  ENDMETHOD.
ENDCLASS.


START-OF-SELECTION.
  CHECK main IS INITIAL.
  main = NEW #( ).
  TRY.
      main->set_view( viewname = p_table ).
      main->init_tree( ).
    CATCH cx_axt.
  ENDTRY.
 

Der Beitrag Pflegeview mit Datennavigation erschien zuerst auf Tricktresor.


Welche Redefinitionen gibt es?

$
0
0

In einem Projekt habe ich viel mit einer Superklasse und vielen Vererbungen gearbeitet. Die Superklasse besitzt sozusagen die Standardimplementierung für die einzelnen Funktionen und die Unterklassen können das Verhalten durch Redefinition ändern, wenn es erforderlich ist.

mit der Zeit sind viele Unterklassen zusammen gekommen. Das Konzept hat sich gut bewährt. Allerdings stellt sich nun das Problem, dass ich bei einer neuen Klasse nicht mehr wusste, bei welcher anderen Unterklasse es eine Abweichung zum Standard gibt. Um das herauszubekommen musste ich jede einzelne Klasse anklicken, die Methodenliste herunter scrollen, die entsprechende Methode finden und schauen ob und wie die Redefinition aussah.

Für einen Programmierer natürlich ein nicht zu tolerierendes Vorgehen! 8)

Finde Redefinitionen!

Der unten stehende Report nutzt die Klasse CL_OO_CLASS um die Subklassen zu ermitteln. In den Redefinition der Subklasse wird nach der vorgegebenen Methode gesucht. Ist diese vorhanden, also redefiniert, dann wird sie in der Liste ausgegeben.

Mit einem Doppelklick auf eine Methode wird der Quelltext im Docker angezeigt. Zur Anzeige des Quelltextes wird die Klasse CL_GUI_ABAPEDIT verwendet.

Coding

REPORT ztrcktrsr_find_redefinitions.


PARAMETERS p_clas TYPE seoclsname DEFAULT 'CL_GUI_CONTROL'.
PARAMETERS p_meth TYPE seocpdname DEFAULT 'FREE'.


CLASS lcl_main DEFINITION.
  PUBLIC SECTION.
    METHODS on_double_click
                  FOR EVENT double_click OF cl_salv_events_table
      IMPORTING row column.
    METHODS docker.
    METHODS display.
    METHODS do
      IMPORTING
        i_class  TYPE clike
        i_method TYPE clike
        i_start  TYPE boolean_flg.
  PROTECTED SECTION.
    DATA mt_redef     TYPE STANDARD TABLE OF seoredef.
    DATA mo_docker    TYPE REF TO cl_gui_docking_container.
    DATA mo_editor    TYPE REF TO cl_gui_abapedit.
    METHODS display_source IMPORTING is_source TYPE seoredef.

ENDCLASS.

CLASS lcl_main IMPLEMENTATION.
  METHOD on_double_click.
    docker( ).
    DATA(redef) = mt_redef[ row ].

    display_source( redef ).

  ENDMETHOD.

  METHOD display_source.
    DATA lt_source TYPE STANDARD TABLE OF string.

    DATA(include) = cl_oo_classname_service=>get_method_include(
                      EXPORTING mtdkey = VALUE #( clsname = is_source-clsname
                                                  cpdname = is_source-mtdname ) ).
    READ REPORT include INTO lt_source.
    mo_editor->set_text( lt_source ).

  ENDMETHOD.

  METHOD docker.

    CHECK mo_docker IS INITIAL.
    mo_docker = NEW #( side = cl_gui_docking_container=>dock_at_right ratio = 50 ).
    mo_editor = NEW #( parent = mo_docker ).
    mo_editor->set_readonly_mode( 1 ).

  ENDMETHOD.

  METHOD display.

    TRY.
        " create SALV
        CALL METHOD cl_salv_table=>factory
          IMPORTING
            r_salv_table = DATA(lr_table)
          CHANGING
            t_table      = mt_redef.

        lr_table->get_functions( )->set_all( ).

        " register event DOUBLE_CLICK
        SET HANDLER on_double_click FOR lr_table->get_event( ).

        " hide columns which are not relevant
        DATA(lr_columns) = lr_table->get_columns( ).
        lr_columns->get_column( 'VERSION' )->set_technical( ).
        lr_columns->get_column( 'MTDABSTRCT' )->set_technical( ).
        lr_columns->get_column( 'MTDFINAL' )->set_technical( ).
        lr_columns->get_column( 'ATTVALUE' )->set_technical( ).
        lr_columns->get_column( 'EXPOSURE' )->set_technical( ).
        lr_table->display( ).
      CATCH cx_salv_error.
    ENDTRY.


  ENDMETHOD.


  METHOD do.

    DATA lr_class TYPE REF TO cl_oo_class.
    DATA lt_subclasses TYPE seo_relkeys.
    DATA ls_subclass   LIKE LINE OF lt_subclasses.

    TRY .
        lr_class ?= cl_oo_class=>get_instance( i_class ).

        LOOP AT lr_class->redefinitions INTO DATA(ls_redef) WHERE mtdname = i_method.
          APPEND ls_redef TO mt_redef.
        ENDLOOP.
        lt_subclasses = lr_class->get_subclasses( ).

        IF i_start = abap_true.
          " search
          LOOP AT lt_subclasses INTO ls_subclass.
            do( i_class  = ls_subclass-clsname
                i_method = i_method
                i_start  = space ).
          ENDLOOP.
        ENDIF.

      CATCH cx_class_not_existent.

    ENDTRY.

  ENDMETHOD.

ENDCLASS.


START-OF-SELECTION.

  DATA(main) = NEW lcl_main( ).
  main->do( i_class  = p_clas
            i_method = p_meth
            i_start  = abap_true ).
  main->display( ).

abapGit

Der inzwischen obligatorische Link zu Github: https://github.com/tricktresor/find_redefinitions

Der Beitrag Welche Redefinitionen gibt es? erschien zuerst auf Tricktresor.

Hacking SAPGUI

$
0
0

Heute bin ich zufällig auf etwas gestoßen, dass ich erst nicht glauben konnte. Aber eigentlich hätte es klar sein müssen. Ich zeige dir erst einmal ein Bild:

Icon an ungewöhnlicher Stelle…

Vielleicht ist deine erste Reaktion genauso wie meine:

via GIPHY

Folgende Controls verwende ich für diesen kleinen Hack:

  • CL_GUI_PICTURE
  • CL_GUI_GOS_CONTAINER

Normalerweise wird der GOS-Container nicht explizit aufgerufen, sondern nur implizit von der Klasse CL_GOS_MANAGER verwendet.

Container ist Container

Im Generic Object Services Menü wird normalerweise ein Pull-Down-Menü angezeigt:

Das GOS-Menü in Aktion

In einem Menü können jedoch alle Controls angezeigt werden. Sinnvoll sind hier nur wenige, denn der GOS-Container ist ziemlich klein. Ein Menü passt dort gut hinein. Aber auch ein Bild oder Icon.

Anzeige Icon in Container

Die Anzeige eines Bildes oder Icons ist ziemlich simpel und schnell erledigt:

REPORT.

PARAMETERS p_test.

INITIALIZATION.

  DATA(picture) = NEW cl_gui_picture( parent = NEW cl_gui_gos_container( width = 38 ) ).
  DATA url TYPE cndp_url.
  CALL FUNCTION 'DP_PUBLISH_WWW_URL'
    EXPORTING
      objid                 = 'ACHTUNG'
      lifetime              = cndp_lifetime_transaction
    IMPORTING
      url                   = url
    EXCEPTIONS
      dp_invalid_parameters = 1
      no_object             = 2
      dp_error_publish      = 3.
  IF sy-subrc = 0.
    picture->load_picture_from_url_async( url = url ).
    picture->set_display_mode( picture->display_mode_fit ).
  ENDIF.

Da geht noch mehr…

Nachdem ich ein bisschen herumgespielt habe, ist mir eine Eigenschaft aufgefallen, die nicht ganz offensichtlich ist und die ich auch so nicht erwartet hätte. So, wie man mehrere Docking-Container an ein Dynpro andocken kann, so kann man auch den CL_GUI_GOS_CONTAINER mehrfach erzeugen.

Zusätzlich können in einem Picture-Control auch die Klick- und Doppelklick-Ereignisse aktiviert und verwendet werden.

Auch das Ein- und Ausblenden des Controls ist möglich (Methode SET_VISIBLE).

Code

Folgendes kleine Programm zeigt die Möglichkeiten, die sich ergeben:

Mehrere GOS-Container

Durch Klicken des Parameters P_SHOW wird das Bild „ACHTUNG“ ein- und ausgeblendet. Ein Klick auf eines der Icons zeigt eine Info-Meldung. Ich nutze solch kleine Spielereien immer gerne, um mich an die neue Syntax zu gewöhnen und auszutesten, was möglich und sinnvoll ist. Hier habe ich die Gelegenheit am Schopfe gepackt und auch funktional programmiert, so dass Method-Chaining über mehrere Methoden hinweg auf ein und das selbe Objekt möglich ist. 

REPORT.

PARAMETERS p_test.
PARAMETERS p_show AS CHECKBOX DEFAULT 'X' USER-COMMAND dummy.

CLASS info DEFINITION.
  PUBLIC SECTION.
    METHODS icon IMPORTING name TYPE clike RETURNING VALUE(info) TYPE REF TO info.
    METHODS pic  IMPORTING name TYPE clike RETURNING VALUE(info) TYPE REF TO info.
    METHODS constructor IMPORTING width TYPE i.
    METHODS with_text IMPORTING text TYPE clike RETURNING VALUE(info) TYPE REF TO info..
    METHODS hide.
    METHODS show.
  PROTECTED SECTION.
    DATA picture TYPE REF TO cl_gui_picture.
    METHODS handle_click FOR EVENT picture_click OF cl_gui_picture.
    DATA text TYPE string.
ENDCLASS.

CLASS info IMPLEMENTATION.

  METHOD constructor.
    picture = NEW cl_gui_picture( parent = NEW cl_gui_gos_container( width = width ) ).
    picture->set_registered_events( VALUE #(
                ( eventid = cl_gui_picture=>eventid_picture_click )
                ) ).
    SET HANDLER handle_click FOR picture.
  ENDMETHOD.
  METHOD icon.

    picture->load_picture_from_sap_icons( name ).
    picture->set_display_mode( picture->display_mode_fit ).
    info = me.
  ENDMETHOD.
  METHOD pic.
    DATA url TYPE cndp_url.
    CALL FUNCTION 'DP_PUBLISH_WWW_URL'
      EXPORTING
        objid                 = CONV w3objid( name )
        lifetime              = cndp_lifetime_transaction
      IMPORTING
        url                   = url
      EXCEPTIONS
        dp_invalid_parameters = 1
        no_object             = 2
        dp_error_publish      = 3.
    IF sy-subrc = 0.
      picture->load_picture_from_url_async( url = url ).
      picture->set_display_mode( picture->display_mode_fit ).
    ENDIF.

    info = me.

  ENDMETHOD.

  METHOD with_text.
    me->text = text.
    info = me.
  ENDMETHOD.

  METHOD handle_click.
    CHECK text IS NOT INITIAL.
    MESSAGE text TYPE 'I'.
  ENDMETHOD.

  METHOD hide.
    picture->set_visible( space ).
  ENDMETHOD.
  METHOD show.
    picture->set_visible( 'X' ).
  ENDMETHOD.

ENDCLASS.


INITIALIZATION.
  DATA(info)   = NEW info( 38 )->pic( 'ACHTUNG' ).
  DATA(green)  = NEW info( 39 )->icon( icon_led_green )->with_text( 'Alles ok' ).
  DATA(yellow) = NEW info( 39 )->icon( icon_led_yellow )->with_text( 'hmpfffff' ).
  DATA(red)    = NEW info( 39 )->icon( icon_led_red )->with_text( 'error. error. error.' ).


AT SELECTION-SCREEN OUTPUT.
  CASE p_show.
    WHEN abap_true.
      info->show( ).
    WHEN abap_false.
      info->hide( ).
  ENDCASE.

WTF?!

Man kann übrigens jedes Control in den Container packen! Auch Text-Edit-Controls und HTML-Viewer… 😉 

REPORT.
PARAMETERS p_test.
INITIALIZATION.

  DATA(html) = NEW cl_gui_html_viewer( parent = NEW cl_gui_gos_container( width = 400 ) ).

  DATA url TYPE c LENGTH 100.
  DATA data TYPE STANDARD TABLE OF w3_html.

  data = VALUE #( ( '<html><head><style>body { margin: 0; background-color: #f9f9f9; color: #666680; font: 20px "Arial"  }</style>'
        && '<title>Hacking SAPGUI</title></head><body background=#aabbcc><marquee behavior=alternate>tricktresor.de</marquee></body></html>' ) ).


  html->load_data( IMPORTING assigned_url = url
                    CHANGING data_table   = data ).
  html->show_url( url ).

  DATA(text) = NEW cl_gui_textedit( parent = NEW cl_gui_gos_container( width = 400 ) ).
  text->set_statusbar_mode( 0 ).
  text->set_toolbar_mode( 0 ).
  text->set_textstream( `Enter your name` ).
  text->select_lines( 1 ).
  cl_gui_control=>set_focus( text ).

SALV-Grid

Für ein ALV ist sicherlich nicht genug Platz…? Denkste:

CL_SALV_TABLE in CL_GUI_GOS_CONTAINER

Der Beitrag Hacking SAPGUI erschien zuerst auf Tricktresor.

GUI-Designer „guidrasil“

$
0
0

Vor langer Zeit habe ich mich mit der automatischen und generischen Erzeugung und Verwaltung von SAPGUI-Controls beschäftigt.

Was sind Controls?

Controls sind ActiveX-Komponenten (auch OCX-Komponenten genannt), die im SAPGUI verwendet werden können und mit diesem ausgeliefert werden. Diese Windows-Komponenten werden über entsprechende Klassen im SAP angesprochen und erzeugt. Die Erzeugung erfolgt in der Regel ähnlich:

  1. CREATE OBJECT <object reference>
  2. <object reference>-SET_….
  3. Manche Controls benötigen noch ein explizites DISPLAY.

Die typischen GUI-Controls sind:

  • CL_GUI_ALV_GRID
  • CL_GUI_TEXTEDIT
  • CL_GUI_PICTURE
  • CL_GUI_CALENDAR
  • CL_GUI_HTML_VIEWER
  • CL_GUI_SIMPLE_TREE
  • CL_GUI_COLUMN_TREE
  • CL_GUI_LIST_TREE

Controls benötigen einen Container, in dem sie platziert werden können. Lustiger Weise erben die Container-Klassen von der gleichen Klasse wie die Controls selber: CL_GUI_CONTROL. Die Container erben dann alle von CL_GUI_CONTAINER:

  • CL_GUI_DOCKING_CONTAINER
  • CL_GUI_CUSTOM_CONTAINER
  • CL_GUI_DIALOGBOX_CONTAINER

Eine Sonderstellung nehmen die Splitter-Controls ein, denn sie stellen ebenfalls wieder Container zur Verfügung:

  • CL_GUI_SPLITTER_CONTAINER
  • CL_GUI_EASY_SPLITTER_CONTAINER

Programmierung von Controls

Eine typische Programmierung sieht wie folgt aus:

  • Erzeuge einen Container
  • Erzeuge das Control in diesem Container
  • Setze Eigenschaften des Controls

In diesem Demoprogramm zeige ich kurz, wie ein Textedit-Control in einem Docking-Container aufgebaut wird.

Setzen von Eigenschaften

Die Ansteuerung der Controls ist natürlich immer unterschiedlich, da sich die Control unterscheiden. Ein Picture-Control ist nun mal immer read-only, Ein Textedit-Control nicht. Das ist auch genau das Problem: Wenn ich ein Control häufig verwende, dann kenne ich die Eigenschaften und notwendigen Attribute. Wenn nicht, dann muss ich suchen. Zudem ist die Aktivierung von Attributen manchmal per BOOLEAN notwendig (X und space) und manchmal verlangt das Control „1“ und „0“.

Idee eines GUI-Designers

Da die Controls alle von der Klasse CL_GUI_CONTROL abstammen, ist es möglich jedes Control über eine generelle Methodenschnittstelle zu erzeugen. Ich kann also einer Methode irgend einen Container übergeben (egal, ob Docking-Container, Custom-Container oder Dialogbox) und das Control wieder zurück bekommen. Oder ich kann ein Control erzeugen und es in einer Tabelle speichern.

Das folgende Beispielprogramm macht genau das: Durch die Einstellungen auf dem Selektionsbildschirm wird definiert, welche Art von Control auf welcher Seite angedockt werden soll. Mit <ENTER> werden der Container sowie das Control erzeugt und in einer internen Tabelle abgelegt:

Demoprogramm

REPORT zguidrsail_demo_generic_ctrl.

SELECTION-SCREEN BEGIN OF BLOCK ctrl WITH FRAME TITLE TEXT-ctl.
PARAMETERS p_text RADIOBUTTON GROUP ctrl DEFAULT 'X'.
PARAMETERS p_icon RADIOBUTTON GROUP ctrl.
SELECTION-SCREEN END OF BLOCK ctrl.

SELECTION-SCREEN BEGIN OF BLOCK side WITH FRAME TITLE TEXT-sid.
PARAMETERS p_left RADIOBUTTON GROUP side DEFAULT 'X'.
PARAMETERS p_rigt RADIOBUTTON GROUP side.
PARAMETERS p_botm RADIOBUTTON GROUP side.
SELECTION-SCREEN END OF BLOCK side.

CLASS ctrl_demo DEFINITION.
  PUBLIC SECTION.
    METHODS add_text
      IMPORTING
        side TYPE i.
    METHODS add_icon
      IMPORTING
        side TYPE i.
  PROTECTED SECTION.
    TYPES: BEGIN OF ts_object,
             container TYPE REF TO cl_gui_container,
             control   TYPE REF TO cl_gui_control,
           END OF ts_object.

    DATA objects TYPE STANDARD TABLE OF ts_object.
    METHODS append_control
      IMPORTING
        container TYPE REF TO cl_gui_container
        control   TYPE REF TO cl_gui_control.

ENDCLASS.

CLASS ctrl_demo IMPLEMENTATION.
  METHOD add_text.
    DATA(parent) = NEW cl_gui_docking_container( side = side ratio = 20 ).
    DATA(textedit) = NEW cl_gui_textedit( parent = parent ).
    textedit->set_text_as_stream( VALUE texttab( ( tdline = `This is a demonstration` ) ) ).
    append_control( container = parent control = textedit ).
  ENDMETHOD.
  METHOD add_icon.
    DATA(parent) = NEW cl_gui_docking_container( side = side ratio = 20 ).
    DATA(icon) = NEW cl_gui_picture( parent = parent ).
    icon->load_picture_from_sap_icons( icon_message_question ).
    icon->set_display_mode( cl_gui_picture=>display_mode_fit_center ).
    append_control( container = parent control = icon ).
  ENDMETHOD.
  METHOD append_control.
    APPEND VALUE #( container = container control = control ) TO objects.
  ENDMETHOD.
ENDCLASS.

INITIALIZATION.
  DATA(demo) = NEW ctrl_demo( ).

AT SELECTION-SCREEN.

  CASE 'X'.
    WHEN p_left.
      DATA(side) = cl_gui_docking_container=>dock_at_left.
    WHEN p_rigt.
      side = cl_gui_docking_container=>dock_at_right.
    WHEN p_botm.
      side = cl_gui_docking_container=>dock_at_bottom.
  ENDCASE.

  CASE 'X'.
    WHEN p_text.
      demo->add_text( side = side ).
    WHEN p_icon.
      demo->add_icon( side = side ).
  ENDCASE.

Dynamische Verwaltung

Da ich nun alle erzeugten Container und Controls in einer Tabelle habe, kann ich auch auf die Objekte und deren Eigenschaften zugreifen. Ich könnte zum Beispiel die Tabelle durchgehen und fragen: Ist im Feld CONTAINER ein Objekt der Klasse CL_GUI_DOCKING_CONTAINER? Wenn ja, frage ich das Control nach seinen wichtigen Eigenschaften: RATIO und SIDE:

IF itab-container IS INSTANCE OF cl_gui_docking_container.
  DATA dock TYPE REF TO cl_gui_docking_container.
  dock ?= itab-container.
  DATA(side) = dock->get_docking_side( ).
  dock->get_ratio( ratio = DATA(ratio) ).
ENDIF.

Auf diese Weise könnte ich mir alle wichtigen Eigenschaften eines Controls beschaffen und speichern.

Dynamische Erzeugung

Mit Hilfe von RTTI (Run Time Type Information) in Form der Klasse CL_ABAP_TYPEDESCR kann ich sogar den Klassennamen des Objektes ermitteln:

DATA(clsnam) = cl_abap_typedescr=>describe_by_object_ref( itab-container )->get_relative_name( ).
Wenn ich diesen habe, dann ich das Objekt auch dynamisch erzeugen:

DATA: container TYPE REF TO cl_gui_container, 
      exc_ref TYPE REF TO cx_root.

DATA: ptab TYPE abap_parmbind_tab.

ptab = VALUE #( 
                ( name  = 'SIDE' 
                  kind  = cl_abap_objectdescr=>exporting 
                  value = REF #( side ) ) 
                ( name  = 'RATIO' 
                  kind  = cl_abap_objectdescr=>exporting 
                  value = REF #( ratio ) ) ).

TRY. 
    CREATE OBJECT container TYPE (clsnam) 
      PARAMETER-TABLE ptab. 
  CATCH cx_sy_create_object_error INTO exc_ref. 
    MESSAGE exc_ref->get_text( ) TYPE 'I'. 
ENDTRY.

Eine dynamische Erzeugung ist jedoch gar nicht notwendig, denn ich kenne ja den Klassennamen und kann die Erzeugung wiederum an eine Erbauer-Klasse auslagern.

guidrasil

Nach dem oben beschriebenen Prinzip funktioniert der GUI-Designer ungefähr. Eine wichtige Eigenschaft ist jedoch, dass man erst auswählen muss, auf welcher Seite man einen Docking-Container erstellen möchte. In diesem Docking-Container wird dann ein Splitter erzeugt, der oben eine Toolbar integriert und unten erneut einen leeren Container anzeigt.

 

In der Toolbar kann man dann die gewünschten Controls oder Splitter-Container auswählen. Der GUI-Designer merkt sich, welche Objekte an welcher Stelle erzeugt werden. Das Erzeugen der einzelnen Controls sowie das Speichern der unterstützten Eigenschaften übernimmt die Erbauer-Klasse, die es für jedes Control gibt.

Zusätzlich stellt die Erbauer-Klasse auch noch einen Dialog zur Verfügung, in dem die Eigenschaften des Control eingestellt werden können.

I’d rather write code that writes code than write code

Eine weitere Eigenschaft der Erbauer-Klasse ist, dass jede Erbauer-Klasse ja genau weiß, wie das eigene Control erzeugt werden muss. Das heißt, es kann auch Code zur Verfügung stellen, der für die Erzeugung des Controls notwendig ist.

Der GUI-Designer weiß genau, welche Controls in welcher Reihenfolge erzeugt werden müssen. Der Designer muss also nur noch jedes Control nach dem Erzeugungscode fragen…

abapGit

Der GUI-Designer guidrasil ist verfügbar per abapGit auf Github:

https://github.com/tricktresor/guidrasil

 

Der Beitrag GUI-Designer „guidrasil“ erschien zuerst auf Tricktresor.

Classname Utilities

$
0
0

Klassen sind inzwischen von ABAP nicht mehr wegzudenken. Die Verwaltung der Klassen erfolgt jedoch immer noch in INCLUDES. Diese Includes interessieren in der Regel niemanden. Hin und wieder tauchen diese Klassen-Include jedoch in Shortdumps auf. Der Name der Klasse selbst ist eindeutig aus dem Includenamen zu erkennen. Der Name der Methode jedoch nicht. Häufig gibt es zwar auch die zusätzlich notwendige Information, um welche Methode es sich handelt, aber das ist nicht immer der Fall.

Einzelne Includes sehen zum Beispiel so aus:

CL_OO_CLASSNAME_SERVICE=======CCDEF
CL_OO_CLASSNAME_SERVICE=======CCMAC
CL_OO_CLASSNAME_SERVICE=======CCIMP
CL_OO_CLASSNAME_SERVICE=======CCAU
CL_OO_CLASSNAME_SERVICE=======CU
CL_OO_CLASSNAME_SERVICE=======CO
CL_OO_CLASSNAME_SERVICE=======CI
CL_OO_CLASSNAME_SERVICE=======CP
CL_OO_CLASSNAME_SERVICE=======CT
CL_OO_CLASSNAME_SERVICE=======CS
CL_OO_CLASSNAME_SERVICE=======CM001
CL_OO_CLASSNAME_SERVICE=======CM002
CL_OO_CLASSNAME_SERVICE=======CM003
CL_OO_CLASSNAME_SERVICE=======CM004

CL_OO_CLASSNAME_SERVICE

Um die einzelnen intern verwendeten Objekte (Includes) verwalten zu können, gibt es die Klasse CL_OO_CLASSNAME_SERVICE. Mit dieser Klasse kannst du herausfinden, welche Methode zu welchem Include gehört und umgekehrt.

GET_METHOD_INCLUDE liefert zum Beispiel für die gleichnamige Methode den Includenamen CL_OO_CLASSNAME_SERVICE=======CM00B.

Umgekehrt liefert die Methode GET_METHOD_BY_INCLUDE die Methode zum Includenamen.

die Methode GET_ALL_CLASS_INCLUDES liefert eine komplette Liste der verwendeten Klassen-Includes.

Nummerierung

Interessant finde ich, dass die Methodenincludes nicht numerisch hochgezählt werden, sondern erst numerisch (1-9) und dann alphabetisch (A-Z). Bei einem drei-stelligen numerischen Feld, wie das verwendete (==CMnnn) würden 999 Methoden in eine Klasse passen. Eigentlich mehr als ausreichend, sollte man meinen.

Die Nummerierung erfolgt übrigens in Funktionsbaustein SEO_METHOD_GENERATE_INCLUDE mit Hilfe des folgenden System-Calls:

system-call create method mtdkey-cpdname of class mtdkey-clsname
        include into filename.

Die Struktur FILENAME hat dann zum Beispiel folgenden Wert:

Komponente Wert
ROOTNAME ZCL_ABC=======================
CATEGORYA C
CODEA M001
CATEGORYB
CODEB

 

Der Beitrag Classname Utilities erschien zuerst auf Tricktresor.

REDUCE ABAP750 FOR x = u IN n = 1 THEN brainf*ck

$
0
0

Hoijoijoi. Ich gebe zu, ich tue mich echt schwer mit den neuen Befehlen im ABAP-Sprachschatz. Besonders die ganz neuen Features im ABAP Release 7.50 (oder 7.40 – ich blick da nicht mehr durch) fordern mich ziemlich (REDUCE, COND, FILTER etc).

Angeregt durch den Artikel von Jerry Wang im neuen SCN über REDUCE habe ich mich mit dem Befehl-REDUCE näher beschäftigt. Über die ABAP-Doku bin ich dann auf die Demoprogramme DEMO_REDUCE* gestolpert und beim DEMO_REDUCE_SIMPLE hängen geblieben.

Das Programm ermittelt mit Hilfe des REDUCE-Befehls die Summe von Werten in einer Tabelle. Das Programm DEMO_REDUCE_COND_ITERATION erzeugt mit Hilfe einer FOR-Schleife zusammengesetzte Texte (1 2 3 4 usw).

Ich wollte dann ein bisserl mit den Features herumspielen und hatte die Idee, einen HTML-Text zusammen zusetzen.

Aus der Tabelle

<HTML>
<BODY>
<P>

wollte ich die einzelnen Elemente plus einem separaten Text zu einem String zusammenfügen. Das funktionierte auch noch sehr einfach:

DATA(text) = REDUCE string(
               INIT html = ``
                FOR command IN html_commands 
                NEXT html = |{ html }{ command }| ) 
            && 'Hallo Welt'.

Eigentlich nur zum Spaß habe ich versucht, ob ich mit einem erneuten && auch ein erneutes REDUCE benutzen kann. Obwohl ich einigermaßen überrascht war, dass es anstandslos funktionierte, wäre ich auch maßlos enttäuscht gewesen, wenn es nicht geklappt hätte… 😉

Der nächste Schritt war dann etwas komplizierter: Ich wollte die einzelnen Tags auch wieder schließen. Natürlich in umgekehrter Reihenfolge. Und mit dem SLASH, der ein Ende-Tag anzeigt. Hier brauchte es dann etliche Versuche und verwirrte Blicke in die Doku um zu dem folgenden Ergebnis zu gelangen:

Und hier der Quelltext dazu:

REPORT zdemo_reduce_simple.

CLASS demo DEFINITION.
  PUBLIC SECTION.
  CLASS-METHODS main.
ENDCLASS.

CLASS demo IMPLEMENTATION.
  METHOD main.

  DATA(html_commands) = VALUE string_table(
                              ( `<html>` )
                              ( `<body>` )
                              ( `<p>` ) ).
  cl_demo_output=>write( html_commands ).


  DATA(text) = REDUCE string(
                INIT html = ``
                 FOR command IN html_commands NEXT html = |{ html }{ command }| )
             && 'Hallo Welt'
             && REDUCE string( INIT html = ``
                FOR n = lines( html_commands )
                THEN n - 1
                WHILE n > 0
                 NEXT html = html && COND #( LET command = html_commands[ n ]
                                              IN WHEN command IS NOT INITIAL
                                                 THEN command(1) && '/' && command+1 ) ).

   cl_demo_output=>write( text ).
   cl_demo_output=>display( ).
 ENDMETHOD.

ENDCLASS.

START-OF-SELECTION.
 demo=>main( ).

Ich lasse es einfach so unkommentiert stehen. Wenn man weiß, was der Ausdruck macht, ist es einigermaßen klar.

Hier der Beitrag von Horst Keller zum Thema ABAP-740 Features: https://blogs.sap.com/2014/09/30/abap-news-for-740-sp08-iteration-expressions/

Table Expressions

Sehr gewöhnungsbedürftig für mich ist der Zusatz um die einzelne Tabellenzeile (die Tabelle hat ja keine Struktur) anzusprechen:

COND #( LET command = html_commands[ n ]
         IN WHEN command IS NOT INITIAL
            THEN command(1) && '/' && command+1 )

Mein erster Ansatz war Folgendes (was aber nicht funktionierte):

html_commands[ n ]-TABLE_LINE

Also bin ich zum COND-Ausdruck gekommen. Hier finde ich verwirrend, dass anscheinend zwingend eine WHEN-Klausel angegeben werden muss. Vielleicht gibt es auch eine einfachere Alternative?! Bestimmt. Lasst es mich gerne wissen.

Link zur Horst Kellers Blog: https://blogs.sap.com/2013/05/29/abap-news-for-release-740-table-expressions/

 

Ein komisches Gefühl, auf ein mal wieder Anfänger zu sein…

Der guten alten Zeiten Willen noch mal das Coding mit ABAPvor740. Viel Länger ist es auch nicht. Und ob die Programmierung mit ABAP740 eleganter oder besser ist, wage ich zu bezweifeln.

DATA text TYPE string.
LOOP AT html_commands INTO DATA(command).
  text = text && command.
ENDLOOP.
text = text && 'Hallo Welt'.

DATA line TYPE i.
line = lines( html_commands ).
DO lines( html_commands ) TIMES.
  DATA(cmd) = html_commands[ line ].
  text = text && cmd(1) && '/' && cmd+1.
  SUBTRACT 1 FROM line.
ENDDO.

Der Beitrag REDUCE ABAP750 FOR x = u IN n = 1 THEN brainf*ck erschien zuerst auf Tricktresor.

Viewing all 53 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>