Sprechen Sie GEM? Anwendungsprogrammierung mit GFA-Basic, Teil 3

Ein Eingabemedium, das heute kaum noch wegzudenken ist, ist die Maus. Die Funktionsvielfalt, die mit ihr erreicht wird (Verschieben, Vergrößern, Verkleinern, Drehen, Anwählen, Öffnen, Löschen - Sie kennen die Vielfalt selbst) erleichtert dem Anwender in vielen Situationen das Leben, verlangt allerdings auf der anderen Seite dem Programmierer einiges ab.

Zum Anwählen von Objekten gibt es Mausklicks, Gummibänder und es müssen Tastaturumschalttasten ausgewertet werden. Angewählte Objekte müssen gekennzeichnet werden. Beim Verschieben sollen die Objekte mindestens umrisshaft skizziert werden. Je nach Zielort der Verschiebung gilt es dann, die Objekte vielleicht ins Klemmbrett oder in den Papierkorb zu „schieben", oder gar an andere Anwendungen zu übersenden. Daher wird sich der aktuelle Teil dieses Kurses mit der Thematik der Mauseingabe beschäftigen und dem was damit in enger Verbindung steht: der Selektion.

Unsere Monatsaufgabe

Bestimmt erinnern Sie sich: In der September-Ausgabe der st-computer war das Ergebnis unserer Programmierarbeit ein von uns so genanntes „DTP-Fenster": ein GEM-Fenster mit scrollbarem Inhalt, der aus diversen Grafikobjekten gebildet wurde. In diesem Teil wollen wir dieses Programm so erweitern, dass die angesprochenen Grafikobjekte mit der Maus an- und abwählbar werden. Beim Klicken und Festhalten der Maustaste soll ein Rähmchen erscheinen, mit dem die Objekte symbolisch verschoben werden können. Bewegt man die Maus über den Fensterrand hinaus, beginnt das Fenster in die passende Richtung nachzuscrollen.

Selektionszustand

Die Überlegung, dass es selektierbare Objekte gibt, führt zu der Frage, wie der Selektionszustand gespeichert und wie er em Benutzer angezeigt wird. Eigentlich ist eine Selektion stets nur temporär und meist sogar lokal. Man denke z.B. an eine Mehrbenutzer-Datenbank, wo jeder Benutzer seine eigenen Selektionen trifft, um mit diesen auf der gemeinsam genutzten Datenbank zu arbeiten. Etwas vergleichbares trifft auch auf unser DTP-Fenster zu: Ausgewählte Objekte sollen nur lokal für den Benutzer am Bildschirm selektiert und daher auch nur ihm gegenüber als ausgewählt gekennzeichnet werden. Beim Drucken einer solchen DTP-Seite möchte man natürlich nicht mehr sehen, welche Objekte am Bildschirm ausgewählt sind.

Von daher sollte man normalerweise zur Sicherung des Selektionszustands einen sogenannten Cursor anlegen, der die Information über die Auswahl enthält. In unserem Fall des DTP-Fensters wäre ein Cursor somit eine Liste mit den Kennungen der aktuell angewählten Grafikobjekte. Die Daten selbst blieben unberührt. So ist es möglich, beliebig viele Cursor anzulegen (wann immer welche gebraucht werden ), die jedoch auf denselben Daten operieren. Zwei Benutzer (oder wie oben angesprochen zwei Geräte) können so unterschiedliche Auswahlen treffen.

Hier allerdings wollen wir nun den anderen, etwas „kurzsichtigen" Weg gehen, und den Selektionszustand nun doch in den Datenspeicher einfügen. Jedes Grafikobjekt wird dazu um ein Flag „selektiert"/„nicht selektiert" erweitert - ein Grafikobjekt ist dann also global an- oder abgewählt.

In der heutigen Aufgabe wollen wir unser Programm vom letzten Monat perfektionieren.

Der Vorteil liegt hier lediglich darin, dass anhand dieses Beispiels gezeigt werden kann, wie nachträgliche Änderungen an einem OT/OB-Lib Objekttyp getätigt werden können.

Erweiterung des gr-Objekts

Jedes Grafikobjekt soll sich also „selbst" merken, ob es angewählt ist oder nicht. Dazu wird eine einzige Boolean-Variable benötigt, die gr_selected genannt werden soll. Falls nicht schon geschehen, muss dazu der Standard-Objekttyp boolean_% der OT/OB-Lib [1] in das Programm des September-Kursteils eingebunden werden. Vergessen Sie dabei nicht, die Zeile „@boolean_typedef" in der Prozedur @typedef zu aktivieren bzw. einzufügen.

Listing 1 zeigt, welche Änderungen am gr-Objekt zu tätigen sind, damit dieses Flag eingeführt wird und ferner gelesen und gesetzt werden kann. Beim Zeichnen soll die Selektion kenntlich gemacht werden.

Die ersten drei Prozeduren in diesem Listing sind nicht neu. Sie existierten bereits im alten Programm. Allerdings muss das neue Attribut gr_selected_% in @gr_typedef angemeldet werden, grjnit wurde erweitert, um das Boolean-Objekt selbst zu initalisieren und mit dem Wert „FALSE" (=nicht selektiert) vorzubelegen. @gr_exit räumt den Objektspeicher der Boolean-Variablen wieder auf.

Nun steht für jedes gr-Objekt das selected-Flag zur Verfügung. Um dieses (gr-interne) Flag auch von außen Lesen und Setzen zu können, werden die gr-Objekte noch um die beiden, in Listing 1 folgenden, Prozeduren @gr_selected und @gr_selected_set erweitert, die genau diesen Zweck erfüllen.

Listing 1

' OB: Gr
PROCEDURE gr_typedef ! call
    '
    LET gr_%=@ot_create
    LET gr_x_%=@ot_attrib_create(gr_%,long_%)
    LET gr_y_%=@ot_attrib_create(gr_%,long_%)
    LET gr_w_%=@ot_attrib_create(gr_%,long„%)
    LET gr_h_%=@ot_attrib_create(gr_%,long_%)
    LET gr_color_%=@ot_attrib_create(gr_%,word_%)
    '
    LET gr_selected_%=@ot_attrib_create(gr_%,boolean_%) ! NEU
    '
    LET gr_typ_%=@ot_attrib_create(gr_%,obtyp_%)
    '
    @gr_filled_typedef 
    @gr_line_typedef
    '
RETURN
PROCEDURE gr_init(ob#,typ%) !call
    '
    @long_init(@ob_attrib(ob#,gr_%,gr_x_%))
    @long_init(@ob_attrib(ob#,gr_%,gr_y_%))
    @long_init(@ob_attrib(ob#,gr_%,gr_w_%))
    @long_init(@ob_attrib(ob#,gr_%,gr_h_%))
    @word_init(@ob_attrib(ob# , gr_%, gr_color_%))
    @boolean_init(@ob_attrib(ob#,gr_%,gr_selected_%)) ! NEU
    @obtyp_init(@ob_attrib(ob#,gr_%,gr_typ_%),typ% >
    '
    @gr_selected_set(ob#,FALSE) ! NEU
    '
RETURN
PROCEDURE gr_exit(ob#) !call
    '
    @long_exit(@ob_attrib(ob#,gr_%,gr_x_%))
    @long_exit(@ob_attrib(ob#,gr_%,gr_y„%))
    @long_exit(@ob_attrib(ob#,gr_%,gr_w_%))
    @long_exit(@ob_attrib(ob#,gr_%,gr_h_%))
    @word_exit(@ob_attrib(ob#,gr_%,gr_color_%))
    @boolean_exit(@ob_attrib(ob#,gr_%,gr_selected_%)) ! NEU
    @obtyp_exit(@ob_attrib(ob#,gr_%,gr_typ_%))
    '
RETURN
FUNCTION gr_selected(ob#) !call ! NEU
    $F%
    RETURN @boolean_get(@ob_attrib(ob#,gr_%,gr_selected_%))
ENDFUNC
PROCEDURE gr_selected_set(ob#,value!) !call ! NEU
    @boolean_set(@ob_attrib(ob#,gr_%,gr_selected_%),value!)
RETURN 
' - DL -
PROCEDURE gr_draw_selection(ob#,output#,off_x%,off_y%) ! NEU
    LOCAL line#,box#
    LOCAL x%,y%,w%,h%
    '
    ' Objektausmaße ermitteln:
    LET x%=@gr_x(ob#)
    LET y%=@gr_y(ob#)
    LET w%=@gr_w(ob#)
    LET h%=@gr_h(ob#)
    '
    ' Umrandungslinien zeichnen:
    LET line#=@gr_line 
    @gr_color_set(line#,1) ! schwarz
    @gr_line_thickness_set(line#,1)
    @gr_line_form_set(line#,gr_line_form_horizontal_&)
    ' obere Linie:
    @gr_x_set(line#,x%)
    @gr_y_set(line#,y%)
    @gr_w_set(line#,w%)
    @gr_h_set(line#,1}
    @gr_draw(line#,output#,off_x%,off_y%)
    ' untere Linie:
    @gr_y_set(line#,y%+h%-1)
    @gr_draw(line#,output#,off_x%,off_y%)
    ' linke Linie:
    @gr_line_form_set(line#,gr_line_form_vertikal_&)
    @gr_y_set(line#,y%)
    @gr_w_set(line#,1)
    @gr_h_set(line#,h%)
    @gr_draw(line#,output#,off_x%,off_y%}
    ' rechte Linie:
    @gr_x_set(line#,x%+w%-l)
    @gr_draw(line#,output#,off_x%,off_y%)
    @gr_line_free(line#)
    '
    ' Rahmenknöpfe zeichnen:
    LET box#=@gr_filled @gr_color_set(box#,1) ! schwarz
    @gr_filled_form_set(box#,gr_filled_form_rectangle_&)
    @gr_w_set(box#,7)
    @gr_h_set(box#,7)
    FOR y&=0 TO 2                   ! 3 mal
        FOR x&=0 TO 2               ! 3 Felder, aber:
            IF NOT (y&=1 AND x&=1)  ! in der Mitte nicht zeichnen
                @gr_x_set(box#,x%+w%*x&\2-3)
                @gr_y_set(box#,y%+h%*y&\2-3)
                @gr_draw(box#,output#,off_x%,off_y%)
            ENDIF 
        NEXT x&
    NEXT y&
    @gr_filled_free(box#)
    '
RETURN
PROCEDURE gr_draw(ob#,output#,off_x%,off_y%) !call
    '
    SELECT @gr_typ(ob#)
    CASE gr_filled_%
        @gr_filled_draw(ob#,output#,off_x%,off_y%)
    CASE gr_line_%
        @gr_line_draw(ob#,output#,off_x%,off_y%)
    ENDSELECT
    '
    IF @gr_selected(ob#)                                ! NEU
        @gr_draw_selection(ob#,output#,off_x%,off_y%)   ! NEU
    ENDIF                                               ! NEU
    '
RETURN

Die Prozedur gr_draw_selection ist nicht von außen aufrufbar. Sie zeichnet einen wie in DTP-Programmen üblichen Objektrahmen und wird von gr_draw für die selektierten Objekte aufgerufen, um diese mit eben diesem Rahmen zu versehen.

Jetzt können alle gr-Objekte über @gr_seiected_set selektiert werden, wodurch sie beim Zeichnen automatisch einen Objektrahmen als Umrandung erhalten.

Erweiterung des DTP-Objektes

Sie erinnern sich bestimmt an die Funktion der Objekte vom Typ dtp_%. Dies war der Objekttyp, den wir im letzten Kursteil konstruiert hatten. Jedes Objekt dieses Typ stellt eine DTP-Seite dar und speichert neben der Seitengröße und Farbe eine Liste von darauf angeordneten Grafikobjekten (gr-Objekte). Die von DTP-Objekten angebotene Operation, die wir dabei bislang genutzt hatten, war @dtp_draw, mit der eine solche DTP-Seite (in unserem Falle auf den Bildschirm) ausgegeben werden kann.

Weil @dtp_draw die Prozedur @gr_draw benutzt, um die gr-Objekte zu zeichnen, welche nun aber - wie an ihrem Namen zu erkennen ist - den gr-Objekten zugerechnet wird, sagt man: Das DTP-Objekt zeichnet in @dtp_draw die Objekte nicht selbst, sondern teilt den gr-Objekten nur mit „sich selbst zu zeichnen". Leichter verständlich wird die Sprechweise, wenn man sich vorstellt, die dtp-Prozeduren (also „der DTP-Objekttyp") wurden von Herrn Mayer programmiert, die gr-Prozeduren (also „der gr-Objekttyp") aber von Herrn Schmidt. Herr Mayer musste bei seiner Arbeit nicht wissen, welche konkreten gr-Objekte es gibt - und insbesondere nicht, welches tatsächliche Aussehen diese nun haben. Herr Mayer lässt die Objekte nämlich von den gr-Prozeduren von Herrn Schmidt zeichnen. Die Prozeduren von Herrn Schmidt gehören dabei also zu den gr-Objekten. Daher sagt man, das jeweilige gr-Objekt übernimmt das Zeichnen - es zeichnet sich selbst. Auch wenn Herr Schmidt jetzt nachträglich auf die Idee kommt, das Aussehen seiner gr-Objekte zu verändern (die Kreise werden noch runder), oder gar neue gr-Objekte einzuführen (etwa Polygone oder Fließtexte), so betrifft dies in keiner Weise die Arbeit von Herrn Mayer.

Genau diesen Vorteil bekommen wir hier zu spüren, denn die Änderung, die an @gr_draw durchgeführt wurde, um den Objektrahmen um die selektierten Objekte zu zeichnen, hat keine Auswirkung auf das DTP-Objekt, insbesondere bei @dtp_draw. Die gr-Objekte werden nach wie vor „von sich selbst" gezeichnet. Sollte ein Objekt jetzt selektiert sein, erhält es durch die neue @gr_draw-Prozedur nun automatisch seinen Objektrahmen, obwohl @dtp_draw nicht verändert wurde.

Nicht automatisch geht natürlich aber das Selektieren der Objekte. Von daher wird das DTP-Objekt um drei dazu nützliche Prozeduren erweitert (Listing 3): Der Funktion @dtp_find wird ein Koordinatenpaar übergeben und sie ermittelt, welches Grafikobjekt sich auf der DTP-Seite an dieser Stelle befindet. Folgender Ablauf sei zum Verständnis noch einmal erklärt: Beim Anlegen eines DTP-Objektes (vgl. @dtp) wird die Seitengröße der DTP-Seite übergeben, anschließend werden über @dtp_gr_ob_add gr-Objekte auf die Seite eingefügt. Dabei werden die gr-Objekte durchnummeriert und liegen an Koordinaten innerhalb der Seite. An @dtp_find wird nun ein Koordinatenpaar übergeben, und zurück kommt die Zahl -1, falls an dieser Stelle kein gr-Objekt auf der Seite liegt, oder andernfalls die laufende Nummer des Objektes, das an dieser Stelle als oberstes zu finden ist. Das Ergebnis wird ermittelt, indem alle Objekte in einer Schleife in umgekehrter Zeichnungs-Reihenfolge überprüft werden. Die Funktion wird später benutzt, um beim Mausklick die Mauskoordinaten umzurechnen in die Nummer des betroffenen gr-Objektes.

@dtp_select dient dem Anwählen eines gr-Objektes auf einer DTP-Seite. Es wird die Nummer des gr-Objektes (so wie sie auch von @dtp_find zurückgegeben wird) übergeben. Ferner stehen zwei Modi zur Auswahl: dtp_select_mode_single_& wählt zuerst alle anderen Objekte ab, und dann das gewünschte Objekt an. Dieser Modus ist für einen normalen Mausklick gedacht. dtp_select_mode_add_& läßt den Zustand der anderen Objekte unangetastet, und schaltet den Selektionszustand des gewählten Objektes um. Dieser Modus ist für einen Shift-Klick gedacht. Damit die beiden Konstanten auch definiert sind, muss Listing 2 in die Prozedur @dtp_typedef eingefügt werden.

@dtp_deselect_all wählt alle angewählten Objekte ab. Das Abwählen geschieht in einer Schleife.

Die beiden Prozeduren @dtp_select und @dtp_deselect_all geben noch die Koordinaten eines Rechtecks zurück: rx%, ry%, rw%, rh%. Hiermit teilen sie ihrem Aufrufer mit, welcher Bereich der DTP-Seite (in Pixel ausgedrückt) sich durch Änderung des Selektionszustandes geändert hat, und daher ggf. neu gezeichnet werden muss.

Erweiterungen der page-Prozeduren

Der äußere Ablauf bei einer Selektionsänderung sollte sich damit erahnen lassen:

Listing 3

FUNCTION dtp_find(ob#,mx%,my%) 'call ! NEU
    $F%
    LOCAL found%
    LOCAL objects#,max%
    LOCAL i%,link#,gr_ob#
    LOCAL ox%,oy%,ow%,oh%
    LOCAL mw%,mh%
    '
    LET found%=-1
    '
    LET objects#=@dtp„objects(ob#)
    LET max%=@array_max(objects#)
    LET mw%=1 
    LET mh%=1
    '
    FOR i%=max% DOWNTO 0
        '
        LET link#=@array_get(objects#, i%)
        LET gr_ob#=@link_get{link#)
        '
        LET ox%=@gr_x(gr_ob#)
        LET oy%=@gr_y(gr_ob#)
        LET ow%=@gr_w(gr_ob#)
        LET oh%=@gr_h(gr_ob#)
        '
        IF RC_INTERSECT(mx%,my%,mw%,mh%,ox%,oy%,ow%,oh%)
        LET found%=i%
            EXIT IF TRUE 
        ENDIF
        '
    NEXT i%
    '
    RETURN found%
ENDFUNC
PROCEDURE dtp_select(ob#,gr_ob%,mode&,VAR rx%,ry%,rw%,rh%) !call ! NEU
    LOCAL gr_obs#,gr_ob#,selected!
    LOCAL x%,y%,w%,h%
    '
    LET gr_obs#=@dtp_objects(ob#)
    LET gr_ob#=@link_get(@array_get(gr_obs#,gr_ob%))
    '
    SELECT mode&
    CASE dtp_select_mode_single_&
        @dtp_deselect_all(ob#,rx%,ry%,rw%,rh%)
        @gr_selected_set(gr_ob#,TRUE)
        LET x%=@gr_x(gr_ob#)-3 
        LET y%=@gr_y(gr_ob#)-3 
        LET w%=@gr_w(gr_ob#)+7 
        LET h%=@gr_h(gr_ob#)+7
        ' Redraw-Koordinaten rx/ry/rw/rh und x/y/w/h verschmelzen:
        IF x%<rx%
            ADD rw%,SUB(rx%,x%)
            LET rx%=x%
        ENDIF 
        IF y%<ry%
            ADD rh%,SUB(ry%,y%)
            LET ry%=y%
        ENDIF
        LET rw%=MAX(x%+w%-rx%,rw%)
        LET rh%=MAX(y%+h%-ry%,rh%)
    CASE dtp_select_mode_add_&
        LET selected!=@gr_selected(gr_ob#)
        @gr_selected_set(gr_ob#,NOT selected!)
        LET rx%=@gr_x(gr_ob#)-3 
        LET ry%=@gr_y(gr_ob#)-3 
        LET rw%=@gr_w(gr_ob#)+7 
        LET rh%=@gr_h(gr_ob#)+7 
    ENDSELECT
    '
RETURN

ROCEDURE dtp_deselect_all(ob#,VAR rx%,ry%,rw%,rh%) !call ! NEU
    LOCAL gr_obs#,max%,gr_ob#
    LOCAL selected!
    LOCAL found!
    LOCAL x1%,y1%,x2%,y2%
    LOCAL xmin%,ymin%,xmax%,ymax%
    '
    LET gr_obs#=@dtp_objects(ob#)
    LET max%=@array_max(gr_obs#)
    '
    CLR found!
    CLR xmin%
    CLR ymin%
    CLR xmax%
    CLR ymax%
    '
    FOR i%=0 TO max%
        LET gr_ob#=@link_get(@array_get(gr_obs#,i%))
        IF @gr_selected(gr_ob#)<>0
            @gr_selected_set(gr_ob#,FALSE)
            LET x1%=@gr_x(gr_ob#)-3 
            LET y1%=@gr_y(gr_ob#)-3 
            LET x2%=x1%+@gr_w(gr_ob#)+7-1 
            LET y2%=y1%+@gr_h(gr_ob#)+7-1 
            IF found!
                LET xmin%=MIN(xmin%,MIN(x1%,x2%))
                LET ymin%=MIN(ymin%,MIN(y1%,y2%))
                LET xmax%=MAX{xmax%,MAX(x1%,x2%))
                LET ymax%=MAX(ymax%,MAX(y1%,y2%))
            ELSE
                LET xmin%=MIN(x1%,x2%)
                LET ymin%=MIN(y1%,y2%)
                LET xmax%=MAX(x1%,x2%)
                LET ymax%=MAX(y1%,y2%)
            ENDIF
            LET found!=TRUE 
        ENDIF 
    NEXT i%
    '
    LET rx%=xmin%
    LET ry%=ymin%
    LET rw%=xmax%-xinin%+1 
    LET rh%=ymax%-yinin%+1 
RETURN

Die Prozeduren, die sich mit der Verwaltung eines DTP-Fensters befaßten, hatten im letzten Teil den Namen @page_xxx. Listing 4 erweitert diese Routinen nun um die Prozedur @page_click, die die drei Aufgaben von oben übernimmt.

An sie übergeben wird die Mausposition (mx&, my&), die Anzahl der Mausklicks (mc&), die Nummer der Maustaste (mb&) sowie der Status der Tastaturumschalttasten (ks&). Anhand dieser Werte ermittelt diese Prozedur das betroffene Objekt, und sie entscheidet, welche Art der Selektion anzuwenden ist. Sie lässt die Umschaltung der Selektion im entsprechenden Modus durchführen und veranlasst ein Neuzeichnen des sich dadurch verändernden Seitenausschnitts.

In der Prozedur wird zunächst nach der Klickanzahl unterschieden. Im Falle eines Doppelklicks (mc&=2) geschieht hier nichts, im Falle eines Einfachklicks (mc&=7) wird sodann zwischen linker (mb&=1) und rechter (mb&=2) Maustaste unterschieden. Der zweite Fall wird als „alles abwählen" interpretiert. Im ersten rail wird das betroffene Objekt über @dtp_find ermittelt und entsprechend der Shifttasten behandelt. Wichtig sind hier die Koordinatenumrechnungen bei @dtp_find. Die Mauskoordinaten sind zunächst nämlich Bildschirmkoordinaten. Die DTP-Seite befindet sich aber in einem GEM-Fenster, also mit verschobenem Ursprung. Die Bildschirmkoordinaten der Innenfläche dieses Fensters werden in @page_click über @win_get_workarea ermittelt. Durch Subtraktion dieser Koordinaten von den Mauskoordinaten erhält man die Mausposition nun relativ zur Fensterinnenfläche. Der Fensterinhalt - bei uns die DTP-Seite - kann allerdings nun seinerseits nochmals verschoben sein, nämlich durch die Scrollmöglichkeit des Fensterinhalts. Dieses Problem wurde bereits im letzten Kursteil angesprochen: Durch Subtraktion der Scrolloffsets (off_x%, off_y%) kam man dort von einer Koordinate auf der DTP-Seite auf eine Koordinate relativ zur Fensterinnenfläche. Hier suchen wir den umgekehrten Weg, denn wir haben bislang die Mauskoordinaten relativ zur Fensterinnenfläche, benötigen sie aber zur Übergabe an @dtp_find relativ zum Ursprung der DTP-Seite. Also werden die Scrolloffsets umgekehrt nicht subtrahiert, sondern addiert.

Nach dem Ändern des Selektionszustandes wird durch Aufruf von @win_send_redraw die Aktualisierung des betroffenen Fensterinhalts veranlasst. Die Aktualisierung selbst geschieht dann aber nicht unmittelbar, sondern erst, wenn die faceVALUE-Engine wieder genügend „Zeit" dazu hat.

Verschieben der ausgewählten Objekte

Der zweite Teil der Prozedur @page_click befasst sich nun damit, was passiert, wenn die Maustaste nicht kurz, sondern länger gedrückt wird, und der Benutzer offenbar Objekte verschieben möchte. Hier wird nun beispielhaft gezeigt, wie sich diese Situation behandeln läßt, wobei hier der Benutzer ein kleines Rähmchen verschieben darf. Am Fensterrand soll das Fenster bei Bedarf nachscrollen. Wichtig ist, dass während des kompletten Vorgangs der Bildschirm und die Maus „gesperrt" sind, damit kein anderes Programm die aktuelle Mauseingabe ebenfalls interpretiert. Dies wird durch Klammerung mit @aes_screen_(un)lock erzielt. Um eine möglicherweise vorangegangene Änderung des Selektionszustandes auch sichtbar zu machen (@win_send_redraw führte ja wie angesprochen nicht zur Aktualisierung, sonder vermerkte nur den Wunsch), folgt sogleich der Aufruf @do_all_redraws, der diesen Zweck erfüllt.

Die folgenden sechs Zeilen zeichnen lediglich einen gepunkteten Rahmen der Größe 100x60 Pixel um die Maus im XOR-Modus. Die Mauskoordinaten werden vermerkt. In der Schleife wird nun ständig die Mausposition überprüft. Sollte diese sich ändern, wird der Rahmen an der alten Stelle (durch erneutes überzeichnen im XOR-Modus) gelöscht und an der neuen Mausposition erneut gezeichnet. Die Mauskoordinaten werden wieder vermerkt, damit das Spiel von vorn beginnen kann.

Mit dem Loslassen der Maustaste (mb&=0) wird die Schleife verlassen und der Rahmen wird endgültig wieder vom Bildschirm entfernt.

Nachscrollender Fensterinhalt

Das Nachscrollen des Fensterinhaltes geschieht in der unteren Hälfte der Schleife, sobald die Maus die Fensterinnenfläche verlässt. Die Scrolloffsets des Fensterinhalts werden je nach Mausposition nach oben, unten, rechts oder links angepaßt. Falls sich eine Änderung ergab, wird dieser über @win_set_offset durchgeführt. Währenddessen wird der Rahmen kurzzeitig entfernt, denn sonst würde dieser Rahmen mitgescrollt werden und sich somit in seiner Position verschieben. Anschließend werden die Scrolloffsets (off_x%, off_y%) neu eingelesen.

Hier im Beispiel geschieht übrigens nach dem Loslassen der Maus keine tatsächliche Verschiebung der selektierten Grafikobjekte. Dies zu implementieren sei dem sich üben wollenden Leser überlassen.

Listing 4

PROCEDURE page_click(index&,mx&,my&,mc&,mb&,ks&) ! NEU
    LOCAL page#,gr_ob%
    LOCAL off_x%,off_y%
    LOCAL wx&,wy&,ww&,wh&
    LOCAL d&,mxa&,mya&
    LOCAL mode&,move!
    LOCAL rx%,ry%,rw%,rh%
    '
    @win_get_workarea(index&,wx&,wy&,ww&,wh&}
    LET off_x%=window_array%(index&,2)
    LET off_y%=window_array%(index&,3)
    '
    @page_load(index&,page#)
    '
    SELECT mc&
    CASE 1      ! einfacher Mausklick
        '
        CLR rw%,rh%
        '
        SELECT mb&
        CASE 1      ! linke Maustaste
            '
            ' Auf welches Objekt wurde geklickt?
            '
            LET gr_ob%=@dtp_find(page#,mx&-wx&+off_x%,my&-wy&+off_y%)
            IF gr_ob%=>0
                IF AND(ks&,&X11)                        ! Shift-Taste
                    LET mode&=dtp_select_mode_add_&     ! ... hinzuf.gen
                ELSE                                    ! keine Shift-Taste
                    LET mode&=dtp_select_mode_single_&  ! ... einzeln anwfchlen
                ENDIF
                @dtp_select(page#,gr_ob%,mode&,rx%,ry%,rw%,rh%)
            ELSE
                @dtp_deselect_all(page#,rx%,ry%,rw%,rh%)
            ENDIF
        CASE 2      ! rechte Maustaste
            @dtp_deselect_all(page#,rx%,ry%,rw%,rh%)
        ENDSELECT
        '
        ADD rx%,wx&-off_x%
        ADD ry%,wy&-off_y%
        '
        IF rw%>0 AND rh%>0
            @win_send_redraw(index&,rx%,ry%,rw%,rh%)
        ENDIF
        '
        ' Maustaste immer noch gedrückt?
        ~GRAF_MKSTATE(mx&,my&,mb&,d&) ! aktueller Maustastenstatus
        IF mb&<>0 ! langer Klick 
            @aes_screen_lock 
            @do_all_redraws
            '
            DEFLINE &X11111111111111110101010101010110,1,0,0 
            GRAPHMODE 3
            CLIP 0,0 TO WORK_OUT(0),WORK_OUT(1) OFFSET 0,0 
            ~GRAF_MOUSE(256,0)          ! Maus abschalten
            BOX mx&-50,my&-30,mx&+50,my&+30 
            ~GRAF_MOUSE(257,0)          ! Maus anschalten
            LET mxa&=mx&
            LET mya&=my&
            DO
                '
                ' Verschiebe-Rahmen zeichnen
                '
                ~GRAF_MKSTATE(mx&,my&,mb&,d&) ! aktueller Maustastenstatus
                IF mx&<>mxa& OR my&<>mya&
                    ~GRAF_MOUSE(256,0)  ! Maus abschalten
                    BOX mxa&-50,mya&-30,mxa&+50,mya&+30 
                    BOX mx&-50,my&-30,mx&+50,my&+30
                    ~GRAF_MOUSE(257,0)  ! Maus anschalten
                    LET mxa&=mx&
                    LET mya&=my&
                ENDIF
                EXIT IF mb&=0
                '
                ' Fenster evtl. nachscrollen:
                '
                CLR move!
                IF mx&<wx&      ! links
                    SUB off_x%,16 
                    LET move!=TRUE 
                ENDIF
                IF mx&>=wx&+ww& ! rechts 
                    ADD off_x%,16 
                    LET move!=TRUE 
                ENDIF
                IF my&<wy&      ! hoch
                    SUB off_y%,16 
                    LET move!=TRUE 
                ENDIF
                IF my&>=wy&+wh& ! runter 
                    ADD off_y%,16 
                    LET move!=TRUE 
                ENDIF 
                IF move!
                    ~GRAF_MOUSE(256,0) ! Maus abschalten
                    BOX mxa&-50,mya&-30,mxa&+50,mya&+30 
                    GRAPHMODE 1 
                    DEFLINE 1
                    @win_set_offset(index&,off_x%,off_y%)
                    DEFLINE &X11111111111111110101010101010110,1,0,0 
                    GRAPHMODE 3
                    CLIP 0,0 TO WORK_OUT(0),WORK_OUT(1) OFFSET 0,0 
                    BOX mxa&-50,mya&-30,mxa&+50,mya& + 30 
                    ~GRAF_MOUSE(257,0) ! Maus anschalten
                    LET off_x%=window_array%(index&,2)
                    LET off_y%=window_array%(index&,3)
                ENDIF
            LOOP
            GRAPHMODE 3
            ~GRAF_MOUSE(256,0) ! Maus abschalten
            BOX mxa&-50,mya&-30,mxa&+50,mya&+30 
            ~GRAF_MOUSE(257,0) ! Maus anschalten
            GRAPHMODE 1 
            DEFLINE 1
            @aes_screen_unlock
        ENDIF
        '
    CASE 2 ! Doppelklick
        ~GEMDOS(2,7) ! bing
    ENDSELECT
    '
    @mouse_wait
    '
RETURN

Anbindung an die fV-Engine

Da nun alles darauf vorbereitet wurde, dass Mausklicks wie gewünscht behandelt werden können, fehlt nur noch der letzte Schritt: die Anbindung an die faceVALUE-Engine. Diese meldet jeden Mausklick in ein Userfenster über die Userprozedur @user_mouse. Die Parameter, die sie dabei übergibt, entsprechen - wer hätte es anders erwartet - genau denen, die die von uns zur Behandlung erstellte Routine @page_click erwartet. Listing 5 zeigt die erwartete Anbindung einschließlich der Unterscheidung nach Userhandle (vgl. Kursteil September).

Listing 5

PROCEDURE user_mouse(handle&,userhandle&,index&,mx&,my&,mc&,mb&,ks&) 
    LOCAL x&,y&,w&,h&
    @win_get_workarea(index&,x&,y&,w&,h&)
    SELECT userhandle& ! NEU
    CASE page_window_& ! NEU
        @page_click(index&,mx&,my&,mc&,mb&,ks&) ! NEU
    ENDSELECT ! NEU
RETURN

Hausaufgaben

Abschließend gibt es dieses Mal eine kleine Aufgabe mit auf den Weg. Wer sich mit der Programmierung auf Multitaskingsystemen auskennt, wird beim Blick in die Schleife der Routine @page_click etwas zusammenzucken. Dort gibt es nämlich gleich zwei Unschönheiten: Die erste ist die, dass die Mausposition unentwegt über GRAF_MOUSE abgefragt wird. Dies belegt entsprechend viel Rechenzeit. Man nennt eine solche Abfrage Polling. Es gibt eine Alternative, z.B. über EVNT_MULTI. Hier kann man dem Betriebssystem mitteilen, worauf man wartet - in diesem Fall darauf, dass die Maus ihre aktuelle Position verlässt oder ihre Tasten gelöst werden. Bei (korrekter) Benutzung von EVNT_ MULTI vergibt das Betriebssystem nun die Rechenzeit derweil an andere Programme.

Die zweite Unschönheit ist die, dass das Nachscrollen des Fensterinhaltes ungebremst erfolgt. Auf schnellen Rechnern kann das Scrolling unter Umständen so schnell sein, dass man nur noch von einem Fensterrand zum nächsten gelangen kann. Hierzu wäre es wichtig, EVNT_TIMER-Aufrufe als Bremse einzubauen.

Bastler seien also angeregt, die Schleife wie angesprochen zu verbessern. In der perfekten Lösung folgt bei minimaler Rechenleistung der Rahmen stets der Maus, während das Fenster bei - trotz eventueller Mausbewegungen - konstanter Geschwindigkeit nachscrollt.

Damit ist die Behandlung der Userfenster in diesem Kurs abgeschlossen. Der nächste Teil ist ganz den Menüs und Dialogen zugeteilt.

Bis dahin viel Erfolg!

[1] OT/OB-Lib, Infos bei: RUN! Software, friendly applications, Vorgartenstraße 9, D-66424 Homburg, e-Mail: info@run-software.de, http://www.run-software.de


Holger Herzog
Aus: ST-Computer 10 / 2000, Seite 30

Links

Copyright-Bestimmungen: siehe Über diese Seite