Event-Loop
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".