Event-Loop

Aus Wiki des Deutschsprachige Xbaseentwickler e. V.
Version vom 17. April 2014, 15:55 Uhr von Georg (Diskussion | Beiträge)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Zur Navigation springen Zur Suche springen

Ereignis vs. Tastatur

Während bei "reinem" Clipper die einzige Interaktion mit der Tastatur war (es gab Zusatzbibliotheken wie unter anderem die Frankie.LIB, mit der auch Mausfunktionen verfügbar wurden), gibt es unter Windows jede Menge Events (Ereignisse), die an ein Programm übermittelt werden, und auf die das Programm reagieren kann oder muss.

Ein Ereignis kann ein Tastatur-Anschlag oder ein Mausklick sein, oder der Moment, wo der Mauszeiger einen bestimmten Bereich betritt, oder das Vergrössern eines Fensters.

Jedem dieser Ereignisse sind eigene Codes zugeordnet, und Objekte, auf die sich die Ereignisse beziehen.

Ähnlich dem InKey() unter Clipper "erfragt" eine Xbase-Anwendung ein Ereignis vom System. Der Einfachheit halber gehen wir davon aus, dass wir es hier erst einmal nur mit einem Thread zu tun haben.

Der Event-Loop

LOCAL nEvent, mp1, mp2, oXbp
nEvent := xbe_None
WHILE nEvent <> xbeP_Close
   nEvent := AppEvent(@mp1, @mp2, @oXbp)
   oXbp:handleEvent(nEvent, mp1, mp2)
END

Der einfachste Event-Loop benötigt vier Variablen. Eine Variable für die Entgegennahme des aktuellen Event, zwei Variablen für zusätzliche Informationen (Message Parameters), sowie eine Variable für das Objekt, in dessen Kontext das Event fällt.

Damit die Funktion AppEvent() mehr als eine Information zurückgeben kann, müssen die Parameter by Reference übergegeben werden.

Jedes Xbase-Part hat eine Methode :handleEvent(), die in der Lage ist, mit den übergebenen Parametern auf das Event zu reagieren. Das betroffene Objekt muss nicht als Parameter an :handleEvent() übergeben werden, da das betroffene Objekt aus dem Kontext von :handleEvent() über die Instanzvariable self ansprechbar ist.


zeitgesteuerter Event-Loop

LOCAL nEvent, mp1, mp2, oXbp, nWait
nEvent := xbe_None
nWait := 1000
WHILE nEvent <> xbeP_Close
   nEvent := AppEvent(@mp1, @mp2, @oXbp, nWait)
   IF nEvent = xbe_None
      //
   ELSE
      oXbp:handleEvent(nEvent, mp1, mp2)
   ENDIF
END

AppEvent() akzeptiert einen vierten Parameter. Dieser Parameter gibt an, nach wievielen Millisekunden ohne Event das Warten auf ein Event beendet werden soll. Nach Ablauf der Zeitspanne ohne jedes Event wird xbe_None generiert und - in unserem Beispiel - abgefragt mit der Option, hier bestimmte Aktionen auszuführen. Andernfalls wird das Event wie bisher verarbeitet.

Bei Einsatz dieses Parameters muss immer berücksichtigt werden, dass selbst die Mausbewegung über einem Fenster des Programms ein Event auslöst und (nach Abarbeiten von :handleEvent()) die Wartezeit neu beginnt.


Die Standard-Variablen im Event-Loop

Der Event-Loop verwendet vier Standard-Variablen:

- nEvent - mp1 - mp2 - oXbp

nEvent ist die numerische Entsprechung des Events, das ausgelöst wurde. Es ist der Rückgabewert der Funktion AppEvent().

mp1 und mp2 sind zwei Message-Parameter, deren Bedeutung immer in Abhängigkeit zum auslösenden Event steht. Das xbeP_Activate-Event eines XbpPushButton() z.B. übergibt in diesen beiden Variablen keine Werte:

:activate := {| uNIL1, uNIL2, self | ... }

Sie werden daher in der Dokumentation mit uNIL1 und uNIL2 gekennzeichnet.

Die vierte Variable, oXbp, enthält eine Referenz auf das Xbase-Part, in dessen Kontext das Event ausgelöst wurde. Wird ein XbpPushButton() angeklickt, so wird das xbeP_Activate Event ausgelöst und entsprechend durch AppEvent() gemeldet.


Erweiterungen des Event-Loop

Angenommen, wir haben den Event-Loop eines Browse. Mit Drücken der Taste F5 soll der Browse aktualisiert werden, und mit Drücken der Enter-Taste soll ein Satz zur Bearbeitung angezeigt werden:

LOCAL nEvent, mp1, mp2, oXbp
nEvent := xbe_None
WHILE nEvent <> xbeP_Close
   nEvent := AppEvent(@mp1, @mp2, @oXbp)
   DO CASE
   CASE nEvent = xbeP_Keyboard
      DO CASE
      CASE mp1 = xbeK_ENTER
         EditBrowseRecord(oXbp)
      CASE mp1 = xbeK_F5
         BrowseRefresh(oXbp)
      OTHERWISE
         oXbp:handleEvent(nEvent, mp1, mp2)
      ENDCASE
   OTHERWISE
      oXbp:handleEvent(nEvent, mp1, mp2)
   ENDCASE
END

Nachdem ein Event empfangen wurde, wird geprüft, ob es sich um ein Tastatur-Event handelt. Wenn nicht (und das dürfte die Mehrzahl der Fälle betreffen), wird in den OTHERWISE-Zweig verzweigt und das Event nach Vorgabe verarbeitet.

Wichtig ist auch, dass oXbp:handleEvent() zweimal vorkommt: einmal im OTHERWISE-Zweig des äusseren DO CASE (immer dann, wenn kein Tastatur-Event aufgetreten ist), und einmal im OTHERWISE-Zweig des inneren DO CASE: dort behandeln wir zwei Spezialfälle, F5 und ENTER. Alle anderen Tastatur-Eingaben würden "übersehen" ohne das oXbp:handleEvent() im OTHERWISE-Zweig.

Hierzu ein Ablaufbeispiel: Wir unterstellen, dass ein "a" eingetippt wird. Die Funktion AppEvent() liefert folgende Werte zurück:

nEvent = xbeP_Keyboard
mp1 = "a"
mp2 = NIL
oXbp = betroffenes Objekt 
DO CASE
=> diese CASE-Gruppe bedient die verschiedenen Event-Gruppen 
CASE nEvent = xbeP_Keyboard
=> diese Abfrage ist wahr, die Anweisungen in diesem CASE-Zweig werden ausgeführt:

DO CASE
=> es wird eine neue CASE-Gruppe ausgeführt

CASE mp1 = xbeK_ENTER
=> mp1 hat den Wert "a", Bedingung ist nicht erfüllt

CASE mp1 = xbeK_F5
=> mp1 hat den Wert "a", Bedingung ist nicht erfüllt

OTHERWISE
=> da keine der vorhergehenden Bedingungen zutrifft, wird der Standard-Eventhandler
ausgeführt und somit landet im Normalfall das Zeichen "a" z.B. in einem XbpSLE()
! würde der OTHERWISE-Zweig fehlen, würde die Eingabe des "a" nicht verarbeitet und
ginge somit "verloren".


Da der Event-Loop das "Herz" eines Thread darstellt, muss darauf geachtet werden, dass hier ein schneller Ablauf möglich ist. Dazu gehört auch, dass die Abfragen entsprechend aufgebaut werden, dass ihre Abarbeitung schnell vorgenommen werden kann.

Man könnte den obigen Ablauf auch so formulieren:

DO CASE
CASE nEvent = xbeP_Keyboard .AND. mp1 = xbeK_ENTER
   EditBrowseRecord(oXbp)
CASE nEvent = xbeP_Keyboard .AND. mp1 = xbeK_F5
   BrowseRefresh(oXbp)
OTHERWISE
   oXbp:handleEvent(nEvent, mp1, mp2)
ENDCASE

Es sollte klar sein, dass die zweite Variante zeitaufwändiger ist, denn in der Mehrzahl aller Fälle müssen zwei Abfragen ausgeführt werden, ehe ein Event, das nicht F5 oder ENTER ist, bearbeitet werden kann.


Granularität der Events

An dem vorhergehenden Beispiel erkennen wir, dass Events auch in Gruppen gemeldet werden (xbeP_Keyboard): nicht jede Taste hat ein eigenes Event, sondern die Taste wird als Message Parameter 1 zurückgemeldet. Dies gilt für eine Vielzahl von Events, so dass gegebenenfalls die Dokumentation befragt werden muss, wenn der Event-Loop erweitert werden soll.


Threads

Theoretisch kann man mehr als einen Event-Loop in einem Thread haben, aber es wird dazu führen, dass möglicherweise (immer) der falsche Event-Loop ausgeführt wird.

Getrennte, parallele Event-Loops sind nur dann möglich, wenn auch getrennte Threads vorliegen, da jeder Thread eine eigene Event-Queue hat.

Ein Thread muss nicht zwingend einen Event-Loop haben, wenn z.B. linear eine spezielle Aufgabe wie ein Report o.ä. abgearbeitet wird.

Ein Event-Loop bedient immer nur die Events, die aus dem zugeordneten Thread stammen:

FUNCTION Main()
   Local nEvent, mp1, mp2, oXbp
   RunThread1()
   RunThread2()
   nEvent := xbe_None
   WHILE nEvent <> xbeP_Close
      nEvent := AppEvent(@mp1, @mp2, @oXbp)
      oXbp:handleEvent(nEvent, mp1, mp2)
   END
RETURN(.T.)

Angenommen, die beiden Funktionen RunThread1() und RunThread2() starten jeweils einen zusätzlichen Thread, dann bedient der Event-Loop in Main() nur die Events aus dem Hauptthread des Programms. Wird in einem der beiden gerade gestarteten Threads eine Event-Verarbeitung gebraucht, so muss dort ein eigener Event-Loop innerhalb des Threads programmiert werden.


Beispiele

Spezialtasten

Mit Spezialtasten sind Tastenkombinationen gemeint, die eine bestimmte Funktion auslösen sollen, vergleichbar Strg-C zum Kopieren.

Das SET KEY ... TO aus Clipper wird nur noch aus Kompatibilitätsgründen unterstützt. Sieht man sich die Dokumentation ein wenig genauer an, stellt man fest, dass die Alternative SetKey() nur in Textmodus bzw. Hybrid-Programmen verwendet werden kann, während SetAppEvent() auch in reinen GUI-Programmen angewendet werden kann.

Hier soll gezeigt werden, wie das Verhalten über den Event-Loop realisiert werden kann. Ausgangspunkt für dieses Code-Beispiel ist ein XbpMLE(), in dem eine Replace-Routine aufgerufen wird, mit der bestimmte Zeichen ersetzt werden können. Die Maske bietet drei XbpPushButton() an, die folgende Optionen ermöglichen:

  • Replace All
  • Replace Next
  • Replace From here

Anstelle des Anklickens des betreffenden XbpPushButton() soll die entsprechende Aktion mittels Alt-A, Alt-N oder Alt-F ausgelöst werden.

Hier ein Ausschnitt aus dem Code:

aPos := {10, 10}
aSize := {100, 20}
oPush := XbpPushButton():new(oDlg, oDlg, aPos, aSize)
oPush:caption := "Replace ~next"
oPush:tabStop := .T.
oPush:activate := {|| DoReplace(oXbpIn, oSearch, oReplace, REPLACE_NEXT, oDlgWin)}
oPush:create()

aPos[1] += aSize[1] + 10
oPush := XbpPushButton():new(oDlg, oDlg, aPos, aSize)
oPush:caption := "Replace ~all"
oPush:tabStop := .T.
oPush:activate := {|| DoReplace(oXbpIn, oSearch, oReplace, REPLACE_ALL, oDlgWin)}
oPush:create()

aPos[1] += aSize[1] + 10
oPush := XbpPushButton():new(oDlg, oDlg, aPos, aSize)
oPush:caption := "Replace ~from here"
oPush:tabStop := .T.
oPush:activate := {|| DoReplace(oXbpIn, oSearch, oReplace, REPLACE_FROM_HERE, oDlgWin)}
oPush:create()

SetAppFocus(oSearch)

nEvent := xbe_None
WHILE nEvent <> xbeP_Close
   nEvent := AppEvent(@mp1, @mp2, @oXbp)
   DO CASE
   CASE nEvent = xbeP_Keyboard
      DO CASE
      CASE mp1 = xbeK_ALT_A
         DoReplace(oXbpIn, oSearch, oReplace, REPLACE_ALL, oDlgWin)
      CASE mp1 = xbeK_ALT_F
         DoReplace(oXbpIn, oSearch, oReplace, REPLACE_FROM_HERE, oDlgWin)
      CASE mp1 = xbeK_ALT_N
         DoReplace(oXbpIn, oSearch, oReplace, REPLACE_NEXT, oDlgWin)
      OTHERWISE
         oXbp:handleEvent(nEvent, mp1, mp2)
      ENDCASE
   OTHERWISE
      oXbp:handleEvent(nEvent, mp1, mp2)
   ENDCASE
END

Eine Replace-Aktion kann somit sowohl über das Anklicken des XbpPushButton() als auch durch das Drücken der entsprechenden Spezial-Taste ausgelöst werden.

Die hier geschilderte Aufgabe kann auch über SetAppEvent() abgebildet werden, dabei ist aber zu berücksichtigen, dass SetAppEvent() nur auf das generische Tastatur-Event xbeP_Keyboard reagieren kann, und nicht direkt auf eines der durch mp1 gemeldeten, spezifischen Tastendruck.


xbeP_Close

Eine der grossen "Erfahrungen" der Programmierung in Xbase ist der Moment, wenn man feststellt, was dieser Event-Loop bewirkt:

nEvent := xbe_None
WHILE nEvent <> xbeP_Close
   nEvent := AppEvent(@mp1, @mp2, @oXbp)
   oXbp:handleEvent(nEvent, mp1, mp2)
END

Neben dem Anwendungsfenster wird ein weiteres Dialog-Fenster geöffnet und - da man nur einen Thread hat - über den einen Event-Loop bedient. Nach Beendigung der Aktion klickt der Benutzer auf das rote X um das Child-Fenster zu schliessen, und ... das Programm endet.

Der Grund ist einfach (und damit oft nicht recht erkennbar): dem Event-Loop ist es egal, von welchem Objekt das xbeP_Close Event herrührt. Die Tatsache, dass das Child-Fenster das Event erhielt, hindert den Event-Loop nicht daran, das Programm zu beenden.

Es gibt verschiedene Alternativen, wie man das umgehen kann. Hier einige Varianten:

nFenster := 1
WHILE nFenster > 0
   nEvent := AppEvent(@mp1, @mp2, @oXbp)
   IF nEvent = xbeP_Close
      nFenster --
   ENDIF
   oXbp:handleEvent(nEvent, mp1, mp2)
END

Jedesmal, wenn ein Fenster geschlossen wird, wird die Variable nFenster um eins verringert. Ist kein Fenster mehr offen, wird das Programm beendet. Es ist klar, dass sichergestellt sein muss, dass jedes Öffnen eines Fensters nFenster auch um eins erhöht, sonst bleibt der Effekt der gleiche.

WHILE nEvent <> xbeP_Close .AND. oXbp <> SetAppWindow()
    nEvent := AppEvent(@mp1, @mp2, @oXbp)
   oXbp:handleEvent(nEvent, mp1, mp2)
END

Das ist wunderbare brute force-Programmierung. Bei solchem Code muss sichergestellt sein, dass die Shortcut-Optimierung aktiv ist: der Compiler-Schalter /z darf NICHT angegeben sein. Ansonsten wird bei jedem Durchlauf SetAppWindow() ausgeführt und gegen das Objekt geprüft, das Empfänger des Events ist.

Alternativ kann aber das Erstellen des Child-Fenster in einen eigenen Thread ausgelagert werden. In diesem Fall ist das xbeP_Close Event thread-local und wird nicht an den Haupt-Thread "weitergereicht".