<< Inhalt >> Workshops Inhalt

Workshops / Hintergrundberichte / Buchvorstellungen

1. Einführung in die Reaction-Programmierung (von Martin R. Elsner)

Sechster und letzter Teil: Eigene Klassen

Die größten Probleme sollten jetzt schon gelöst sein, vielleicht ein eigenes Programm fertiggestellt, das ausgiebig Reaction-Objekte benutzt. Aber vielleicht ist der Eine oder Andere irgendwo auf eine Idee gestoßen, die er mit den Standard-Objekten nicht lösen konnte. Aber auch dafür gibt es eine Lösung: Man programmiert einfach eine eigene Klasse!

Zunächst ein kleiner Hinweis: Man sollte sich zunächst überlegen, ob die Idee wirklich nicht mit den schon vorhandenen Objekten umgesetzt werden könnte. Manchmal findet man in den Autodocs Klassen oder Tags, die man noch nicht genau kennt und bisher unterschätzt hat. Z.B. kann ein Label nicht nur eine einfache Beschriftung, sondern mehrere Zeilen Text und Grafiken darstellen.

Außerdem ist es empfehlenswert, Standardklassen zu verwenden, da diese meist gut getestet sind, weiterentwickelt werden und für einheitliche Oberfläche und Funktionsweise der Programme sorgen.

Falls man trotzdem nicht fündig wird, kann auch im Aminet recherchiert werden. Hier muss man aber auf die eben erwähnten Vorteile verzichten und hat meist auch keinen Zugriff auf die Quelltexte. Dann bleibt keine Wahl: Man muss selber ran!

Eine eigene Klasse wird immer von einer schon bestehenden abgeleitet. Dazu bietet sich eine Klasse an, die möglichst viele der gewünschten Eigenschaften schon mitbringt, sodass nur die Differenzen neu programmiert werden müssen. Man entscheidet sich z.B. dafür, den Button als Basisklasse zu nutzen und eine neue - private - Klasse zu entwickeln.

Eine umfassende Einführung in die Erzeugung eigener Klassen findet man im Kurs von Michael Christoph (http://www.meicky-soft.de/amiga-magazin/reaction.html oder direkt bei http://www.amiga-magazin.de/magazin/a02-00/programmieren.html). Hier möchte ich deswegen nur als Beispiel eine Klasse aufführen, die mir Stephan Rupprecht zur Verfügung gestellt hat, und die ich für den Einsatz in ClassAction etwas modifiziert habe: eine Icon-Klasse, basierend auf Gadgetclass (also der Basisklasse für alle Gadgets), die ein Icon als Grafik verwendet und auf Mausklicks wie ein Button reagiert.

Zum Vergleich: Ein Button kann natürlich mit GA_Image auch mit einer beliebigen Grafik gefüttert werden, die man z.B. über ein Penmap- oder Bitmap-Objekt aus einem Icon (bzw. der .info-Datei) erzeugt hat, vorausgesetzt, ein entsprechender DataType existiert im System. Allerdings ist der aktuelle Icon-Datatype nicht in der Lage, den Hintergrund der Grafik transparent zu zeichnen, und nicht auf allen Systemen ist ein Icon- Datatype installiert. Unser Icon-Button soll dagegen immer funktionieren und das Icon wie auf der Workbench anzeigen. Der Einfachheit halber soll die Klasse nicht extern, sondern im Quellcode des Programms selbst enthalten sein.

Was benötigt man für eine eigene Klasse?

Nun, außer der Festlegung auf eine Basisklasse wird hauptsächlich eine Ereignisbehandlungsfunktion benötigt, und zwar in Form eines Hooks. Diese kann dann mit DoSuperMethod auf die geerbten Methoden zurückgreifen (falls sie existieren), kann aber auch (und das ist ja der Sinn der Sache) neue Funktionen einführen. Bei Gadgets kann dies hauptsächlich die Darstellung oder die Reaktion auf Ereignisse betreffen.

Für unseren Icon-Button müssen zwei Dinge geregelt werden: das Zeichen des Icons, für das wir DrawIconState verwenden, und die Reaktion auf einen Mausklick. Dazu existieren die Methoden GM_RENDER und GM_HANDLEINPUT, die wir in unserer Behandlungsroutine butclass_DISPATCH abfangen und in butclass_RENDER und butclass_HANDLEINPUT bearbeiten. Wir müssen natürlich das Icon selbst (d.h. einen Zeiger auf ein DiskObject) irgendwo speichern; dazu verwenden wir einfach GA_UserData. Wenn wir aber auch einen BackFill- Hook unterstützen und auf einen Doppelklick prüfen wollen, brauchen wir zusätzliche Eigenschaften in unserem Objekt: Dazu definieren wir eine Struktur InstanceData, die von Intuition angelegt und freigegeben wird, aber von uns genutzt werden kann. Diese muss bei OM_NEW bzw.

OM_SET/OM_UPDATE gefüllt werden und kann dann in den sonstigen Routinen mit Hilfe des Makros INST_DATA verwendet werden. Außerdem muss bei GM_DOMAIN die Größe unseres Buttons ausgerechnet werden, damit die übergeordneten Objekte den nötigen Platz reservieren können.

Zunächst die nötigen Includes, Abkürzungen und Bibliotheken:

  #include <intuition/intuition.h>
  #include <intuition/classusr.h>
  #include <intuition/classes.h>
  #include <intuition/gadgetclass.h>
  #include <intuition/cghooks.h>
  #include <workbench/icon.h>
  #include <workbench/workbench.h>
  #include <gadgets/layout.h>
  #include <libraries/gadtools.h>
  #include <clib/intuition_protos.h>
  #include <clib/alib_protos.h>
  #include <clib/icon_protos.h>
  #include <clib/layers_protos.h>
  #include <clib/utility_protos.h>
  #include <clib/dos_protos.h>

  #define REG(reg,arg)    register __##reg arg
  #define G(x)            ((struct Gadget *)x)

  extern struct Library
    *IconBase,
    *DOSBase,
    *IntuitionBase,
    *UtilityBase,
    *LayersBase,
    *GfxBase;

Nun unsere zusätzlichen Daten pro Objekt, und die benötigten Funktionen:

  struct InstanceData{
    ULONG                    secs;      /* nur zur Prüfung ...   */
    ULONG                    micro;     /* ...   auf Doppelklick */
    struct Hook             *backfill;  /* siehe butclass_RENDER */
    Object                  *parent;    /* siehe butclass_SET    */
  };

  ULONG butclass_DISPATCH( REG( a0, Class *cl ), REG( a2, Object *o ), REG( a1, Msg msg ) );
  ULONG butclass_DOMAIN( Class *cl, Object *o, struct gpDomain *gpd );
  ULONG butclass_HANDLEINPUT( Class *cl, Object *o, struct gpInput *gpi );
  ULONG butclass_RENDER( Class *cl, Object *o, struct gpRender *gpr );
  ULONG butclass_SET( Class *cl, Object *o, struct opSet *ops );

Zwei Funktionen erlauben das Anlegen und Freigeben unserer Klasse:

  IClass *MakeIconClass(){
    IClass   *cl;

    if( cl = MakeClass( NULL,
              "gadgetclass",                 /* die Basisklasse */
              NULL,
              sizeof( struct InstanceData ), /* Größe unserer Zusatzdaten */
              0L ) )
      cl->cl_Dispatcher.h_Entry = (HOOKFUNC) butclass_DISPATCH;
      /* Einzige Initialisierung: Einsetzen unseres Dispatchers */
    return cl;
  }

  BOOL RemoveIconClass( IClass *cl ){
    if( cl ) return FreeClass(cl);
    else return FALSE;
  }

D.h. wir speichern den Zeiger (cl=MakeIconClass()). um ihn beim Erzeugen von Gadgets zu verwenden (gadget=(Object*)NewObject( cl,NULL,...)), und geben die Klasse zuletzt mit RemoveIconClass(cl) wieder frei.

Und jetzt der ganze Rest:

  ULONG butclass_DISPATCH( REG( a0, Class *cl ), REG( a2, Object *o ), REG( a1, Msg msg ) ){
    /* allgemeine Ereignisbehandlungroutine */
    ULONG ret = 0L;

    switch( msg->MethodID ){
      case GM_HANDLEINPUT:
      case GM_GOACTIVE: ret = butclass_HANDLEINPUT( cl, o, (struct gpInput *) msg ); break;

      case OM_SET:
      case OM_UPDATE:   ret = butclass_SET( cl, o, (struct opSet *) msg ); break;

      case GM_RENDER:   ret = butclass_RENDER( cl, o, (struct gpRender *) msg ); break;

      case GM_DOMAIN:   ret = butclass_DOMAIN( cl, o, (struct gpDomain *) msg ); break;

      case OM_NEW:{
        if( ret = DoSuperMethodA( cl, o, msg ) ){
          /* zusätzlich zum Erzeugen des Objekts müssen Eigenschaften gesetzt werden */
          butclass_SET( cl, (Object *) ret, (struct opSet *) msg );
        }
        break;
      }
      default: ret = DoSuperMethodA( cl, o, msg );
      /* in JEDEM Fall alle anderen Methoden an die Mutterklasse übergeben! */
    }
    return ret;
  }

  /****************************************************************************/

  void butclass_redraw( Object *o, struct GadgetInfo *gi ){
    /* siehe butclass_HANDLEINPUT */
    struct RastPort *rp;

    if( rp = ObtainGIRPort( gi ) ){
      DoMethod( o, GM_RENDER, (ULONG) gi, (ULONG) rp, GREDRAW_REDRAW );
      ReleaseGIRPort( rp );
    }
  }

  /****************************************************************************/

  ULONG butclass_HANDLEINPUT( Class *cl, Object *o, struct gpInput *gpi ){
    /* Reaktion auf Eingaben (Mausklicks) des Benutzers */
    InputEvent *ie = gpi->gpi_IEvent;
    ULONG retval = GMR_MEACTIVE;

    if( ie ){
      switch( ie->ie_Class ){
        case IECLASS_RAWMOUSE:{
          switch( ie->ie_Code ){
            case SELECTUP: retval = GMR_NOREUSE; break;

            case SELECTDOWN:{
              InstanceData     *id = (InstanceData*)INST_DATA( cl, o );

  #ifdef DOUBLECLICK
              /* Falls das Icon eine GADGETUP-Botschaft nur nach einem  */
              /* Doppelklick liefern soll, müssen wir den Zeitpunkt des */
              /* letzten Klicks prüfen:                                 */
              if( DoubleClick( id->secs, id->micro, ie->ie_TimeStamp.tv_secs,
                               ie->ie_TimeStamp.tv_micro ) ){
                G(o)->Flags |= GFLG_SELECTED;
                butclass_redraw( o, gpi->gpi_GInfo );
                retval = GMR_NOREUSE | GMR_VERIFY;

                id->secs = id->micro = 0UL;
              }else{
                id->secs  = ie->ie_TimeStamp.tv_secs;
                 id->micro = ie->ie_TimeStamp.tv_micro;
                G(o)->Flags ^= GFLG_SELECTED;
                butclass_redraw( o, gpi->gpi_GInfo );
                retval = GMR_NOREUSE;
              }
  #else
              /* GADGETUP nach einfachem Klick */
              G(o)->Flags |= GFLG_SELECTED;
              butclass_redraw( o, gpi->gpi_GInfo );
              retval = GMR_NOREUSE | GMR_VERIFY;
  #endif
            }
            break;

            case MENUDOWN: retval = GMR_REUSE; break;
          }
          break;
        }
      }
    }
    return retval;
  }

  /****************************************************************************/

  BOOL IsIconFrameless( struct DiskObject *icon ){
    /* Prüft, ob das Icon ohne Rahmen dargestellt werden muss */
    /* siehe butclass_RENDER*/
    ULONG frameless;

    IconControl( icon,ICONCTRLA_GetFrameless, (ULONG) &frameless,TAG_DONE );

    if( frameless == FALSE ){
      ULONG   globalfl;
      if( IconControl( NULL,ICONCTRLA_GetGlobalFrameless, (ULONG) &globalfl,TAG_DONE ) )
        frameless = globalfl;
    }

    return frameless;
  }

  /****************************************************************************/

  ULONG butclass_RENDER( Class *cl, Object *o, struct gpRender *gpr ){
    /* Zeichnen des Icons */
    RastPort         *rp = gpr->gpr_RPort;
    DiskObject       *icon = (struct DiskObject *) G(o)->UserData;
    Hook             *hook;
    InstanceData     *id = (InstanceData*)INST_DATA( cl, o );
    Layer            *layer = gpr->gpr_GInfo->gi_Layer;
    WORD             x = G(o)->LeftEdge, y = G(o)->TopEdge;

    /* Unter unserem Icon muss der richtige Hintergrund dargestellt werden: */
    hook = InstallLayerHook( layer, id->backfill );

    DrawIconState(rp, icon, NULL, x,y,
      (G(o)->Flags & GFLG_SELECTED) != 0L,
      ICONDRAWA_DrawInfo, (ULONG) gpr->gpr_GInfo->gi_DrInfo,
      ICONDRAWA_Frameless, IsIconFrameless( icon ),
      TAG_DONE );

    InstallLayerHook( layer, hook );

    return 0L;   }

  /****************************************************************************/

  ULONG butclass_SET( Class *cl, Object *o, struct opSet *ops ){
    /* Setzen der Eigenschaften */
    InstanceData     *id = (InstanceData*)INST_DATA( cl, o );     TagItem          *ti, *tlist = ops->ops_AttrList;
    DiskObject       *icon = NULL;

    while( ti = NextTagItem( &tlist ) ){
      switch( ti->ti_Tag ){
        case GA_BackFill:
          id->backfill = (Hook*) ti->ti_Data;
          break;
        case LAYOUT_Parent:
          id->parent = (Object *) ti->ti_Data;
          break;

        case GA_UserData:
          icon = (struct DiskObject *) ti->ti_Data;
          break;
      }
    }

    if( id->parent && icon ){
      /* Berechnen der Icongröße und Mitteilung an das Parent-Objekt */
      Rectangle rect;
      UWORD     w,h;

      GetIconRectangle( NULL, icon, NULL, &rect,
        ICONDRAWA_Borderless, FALSE, TAG_DONE );
      /* Wir zählen also den Rand mit */

      w = ( rect.MaxX - rect.MinX ) + 1;
      h = ( rect.MaxY - rect.MinY ) + 1;

      SetAttrs( id->parent,
        LAYOUT_ModifyChild, (ULONG) o,
        CHILD_MinWidth, w,
        CHILD_MinHeight, h,
        CHILD_MaxWidth, w,
        CHILD_MaxHeight, h,
        TAG_DONE );
      /* Dies war die einzige Stelle, für die wir id->Parent brauchten. */
      /* Da das Icon nachträglich geändert werden kann, müssen wir es   */
      /* im Objekt speichern.                                           */
    }

    return (ops->MethodID != OM_NEW) ? DoSuperMethodA( cl, o, (Msg) ops ) : 1L;
    /* bei OM_NEW wurde die SuperMethod schon in HANDLEINPUT aufgerufen! */
  }

  /****************************************************************************/

  ULONG butclass_DOMAIN( Class *cl, Object *o, struct gpDomain *gpd ){
    /* Berechnen der Gadget-Ausmaße */
    InstanceData *id = (InstanceData*)INST_DATA( cl, o );
    DiskObject   *icon = (struct DiskObject *) G(o)->UserData;

    if( icon ){
      /* nur wenn das Icon schon geladen wurde, steht die Größe des Buttons fest */
      struct Rectangle rect;
      UWORD            w,h;

      GetIconRectangle( NULL, icon, NULL, &rect,
        ICONDRAWA_Borderless, FALSE,
        TAG_DONE );

      w = rect.MaxX - rect.MinX + 1;
      h = rect.MaxY - rect.MinY + 1;

      switch( gpd->gpd_Which ){
        case GDOMAIN_MINIMUM:
          gpd->gpd_Domain.Width = w;
          gpd->gpd_Domain.Height = h;
          break;

        case GDOMAIN_NOMINAL:
        case GDOMAIN_MAXIMUM:
          gpd->gpd_Domain.Width = w;
          gpd->gpd_Domain.Height = h;
          break;
      }
      return 1L;
    }

    return 0L;
  }

Mit dieser Klasse können nun Objekte erzeugt und in Layouts eingefügt werden, wobei mit GA_UserData ein Zeiger auf das Icon übergeben werden muss, das man mit GetIconTags bei gegebenem Dateinamen laden kann.

Natürlich ist diese Klasse weder besonders trickreich noch völlig ausgereift - man könnte noch weitere Eigenschaften hinzufügen, die die Reaktion auf Doppelklick oder einfachen Klick steuern etc. Aber ich hoffe, sie hat die grundlegende Funktionsweise eigener Klassen verständlich gemacht.

So, das soll's erst mal gewesen sein - wer mehr über Reaction wissen will, findet in den schon erwähnten Dokumenten und im Kurs von Michael Christoph noch viel mehr Informationen.

Ansonsten stehe ich natürlich weiterhin mit meinem beschränkten Wissen zur Verfügung ;)

euer

Martin R. Elsner <email@martin-elsner.de>

<< >>