Tutorial zum Event-Handling
unter PureBasic und Windows.
Version 1.2
Geschrieben von Froggerprogger, Layout von André
1. Allgemeines zur Message-Queue unter Windows und Implementation in PB
4. EventlParam() und EventwParam()
1. Allgemeines zur Message-Queue unter Windows und Implementation in PB
Wird von einem Programm ein Fenster erstellt, so wird für dieses von Windows eine Message-Queue angelegt. Das ist eine Speicherstruktur ähnlich einer Schlange am Kiosk (der Verkäufer ist der ‚WindowCallback’):
In diese Schlange werden aber keine Menschen (ach nee...), sondern alle das Fenster und seine Childs betreffenden auftretenden Ereignisse, wie z.B. Mausveränderungen (Position, Bewegungen, Clicks, ...), Fensterveränderungen (Ort, Größe, Position, ...) u.v.a. eingefügt.
Was ganz vorne steht, kann als nächstes vom WindowCallback verarbeitet werden, der Rest stellt sich an und wartet, bis er ganz vorne ist. Dabei werden die Ereignisse aber nicht einfach nur ‚hinten angefügt’, sondern aufgrund verschiedener Prioritäten teilweise bevorzugt behandelt. (Jaja, immer diese Vordrängler).
Zudem gibt es noch Echtzeit-Ereignisse, die rennen gleich an der ganzen Schlange vorbei und regeln ganz vorne ihre Angelegenheit bis sie fertig sind, während die anderen warten. Mehr dazu unter Punkt 5 – WindowCallback-Prozedur)
Ein Ereignis setzt sich dabei aus 4 Informationen zusammen, welche meist wie folgt bezeichnet werden:
Vom Programm aus muss immer wieder überprüft werden, ob in der Message-Queue neue Anweisungen für das Fenster vorliegen, so dass darauf entsprechend reagiert werden kann.
Dies geschieht unter Windows normalerweise über eine Callback-Prozedur (eine von außen meist häufig wiederkehrend aufgerufene Prozedur). Den WindowCallback.
PB bietet da aber zusätzlich mit WaitWindowEvent() und WindowEvent() zwei bequeme Alternativen. Diese beiden Funktionen verwalten intern den WindowCallback eigenständig und kümmern sich dabei um einen Teil der Ereignisse selbst (z.B. Neumalen des Fensters). Nach ‚Außen’ - als Rückgabewert an das PB-Programm - schicken sie nur solche Ereignisse, die in den meisten Fällen vom Programmierer behandelt werden wollen (spezielle Ereignisse werden also von (Wait)WindowEvent nicht an das PB-Programm weitergegeben, z.B. #WM_NCHITTEST, für diese muss ein eigener WindowCallback verwendet werden.). Dies erlaubt dem Programmierer sofortige Konzentration auf die individuellen Nutzereingaben über Menüs, Gadgets, etc., lässt sich aber trotzdem mit Wissen der OS-spezifischen Message-Konstanten und/oder einem eigenen Window-Callback beliebig erweitern.
Das Wichtigste zuerst: WaitWindowEvent() hält - sobald es aufgerufen wird - die weitere Programmausführung an!
Das bedeutet, in der Hauptschleife kann sich kein Code befinden, der immerwährend ausgeführt wird, um z.B. Informationsanzeigen zu updaten, etc.
Während dieser Pause wird intern laufend die Message-Queue des aktuellen Windows abgefragt, und erst sobald ein Ereignis vorliegt, wird das Programm weiter ausgeführt (mit der Message-ID als Rückgabewert). Und zwar genau für einen Hauptschleifendurchlauf bis zum nächsten Aufruf von WaitWindowEvent(). Das Programm richtet sich also ausschließlich nach Benutzereingaben.
Eine Hauptschleife eines kleines Testprogrammes könnte z.B. so aussehen:
Repeat
oder auch
resume = #True
While resume
event = WaitWindowEvent()
Select event
Case #PB_Event_Menu
;
Case #PB_Event_CloseWindow
EndSelect
Wend
Man sieht, die genaue Implementierung kann sehr anders aussehen und hängt von konkreten Anforderungen und Vorlieben ab. (Ob nun Repeat oder While, Select oder If,...)
WindowEvent() hält im Gegensatz zu WaitWindowEvent() die Programmausführung nicht an, sondern prüft kurz die Message-Queue auf eine weitere vorhandene Message, gibt ggf. deren MessageID oder 0 (#False) zurück und fährt mit dem nächsten Schritt im Programm fort. Dadurch ist es möglich, z.B. Informationen, welche sich unabhängig von Benutzeingaben ändern, dauernd aktualisiert zu halten und anzuzeigen.
Bei der Verwendung dieses Befehls gilt es aber etwas zu beachten:
Die Hauptschleife wird nun nahezu nahtlos mit größter Geschwindigkeit wiederholt. Dies führt dazu - wie z.B. auch bei umfangreichen Berechnungen - dass diese Schleife so viel CPU-Zeit wie möglich für sich mit dem OS ‚aushandelt’ - da bleibt für andere Programme im Hintergrund für z.B. kurze Intensivpassagen nur weit weniger übrig (maximal nur noch 50%), zumal diese Leistung für das bloße Aktuellhalten des Fensters meist nicht wirklich notwendig ist.
Dieses ‚Problem’ wird gelöst, indem in die Hauptschleife ein Delay(xx) eingefügt wird, wobei xx einen Wert von 1 oder mehr erhält.
Z.B. ergibt sich so für unsere Mini-Testschleife folgende Möglichkeit:
Repeat
;
weiterer Code
Delay(1)
Until WindowEvent() = #PB_Event_CloseWindow
Nun springt die CPU-Auslastung im Taskmanager auf ein Minimum zurück, was den anderen Programmen zugute kommt.
Arbeitet man nun aber mit einem Fenster, welches über sehr viele Gadgets verfügt, so kann sich die Anzahl der Messages innerhalb kürzester Zeit schon mal sehr stark aufsummieren. Während der Abarbeitung der Message-Queue mit z.B. 100 schlagartig erhaltenen Ereignissen wird die Abarbeitung allerdings durch die ständigen Aufrufe von z.B. Delay(1) auf schon 100 ms gedehnt (während denen ggf. schon wieder weitere Ereignisse passiert sind.)
Dieser Effekt dehnt sich sehr schnell auf sichtbare Effekte aus.
Lösung hierfür liegt darin, das Delay(xx) - und sofern vertretbar auch den ‚weiteren Code’ - nur dann auszuführen, wenn im aktuellen Schleifendurchlauf kein Ereignis vorliegt – so werden die Ereignisse mit größtmöglicher Geschwindigkeit abgearbeitet.
Zwei Beispiele sagen mehr als 2000 Worte (?):
beenden = #False
Repeat
event = WindowEvent()
Select event
Case #PB_Event_Menu
;
Case #PB_Event_CloseWindow
beenden = #True
Case 0
; #False
Delay(10)
; weiterer Code, der nur ausgeführt wird
; wenn kein Ereignis vorliegt
; (empfohlener Ort)
EndSelect
; weiterer
Code, der auch trotz Ereignisse, also
; immer ausgeführt werden soll
Until beenden
oder
Repeat
event = WindowEvent()
If event
While event
Select event
Case #PB_Event_Menu
;
Case #PB_Event_CloseWindow
Break
EndSelect
; Code, der NUR während eines Ereignisses
; behandelt werden soll
event = WindowEvent()
Wend
EndIf
; weiterer Code, der nur ausgeführt wird,
; wenn kein Ereignis vorliegt
; (empfohlener Ort)
Delay(1)
ForEver ; wird hier durch 'Break' terminiert
Beide obige Schleifen arbeiten nach demselben oben beschriebenen Prinzip, unterscheiden sich nur etwas im Detail, siehe Programmkommentare.
Der Rückgabewert der Funktionen WaitWindowEvent() und WindowEvent() ist die MessageID. Wie schon erwähnt transportiert aber manches Ereignis über die 32-Bit-long-Werte lParam und wParam weitere das Ereignis betreffende Daten.
Um an diese Werte zu kommen, muss in PB eigentlich ein WindowCallback verwendet werden, (der zudem die Möglichkeit bietet, mit Rückgabewerten zu arbeiten) siehe Punkt 5.
PB bietet aber mit EventlParam() und EventwParam() 2 Funktionen, welche zwar nicht offiziell unterstützt werden, aber bisher einige PB-Versionen überstanden haben und wahrscheinlich niemals verschwinden werden, obwohl das theoretisch jederzeit passieren könnte. Diese beiden Funktionen liefern also den jeweiligen Wert des letzten mit WindowEvent oder WaitWindowEvent abgerufenen Ereignisses zurück und funktionieren nur unter Windows.
Hier ein kleines Anwendungsbeispiel:
OpenWindow(0,200,200,400,100,#PB_Window_SystemMenu,"")
CreateGadgetList(WindowID())
TextGadget(0,4,4,392,92,"Click into the window")
Repeat
event = WaitWindowEvent()
Select event
Case #WM_LBUTTONDOWN
xPos = EventlParam() & $FFFF ; hole
das Low
Word (16 Bit)
yPos = EventlParam()>>16 ; hole
das High
Word (16 Bit)
fwKeys = EventwParam() ; hole
die Flags ggf. gedrückter Tasten
SetGadgetText(0,"x:" + Str(xPos) + " y:" + Str(yPos) + Chr(13) + Chr(10) + "Keys: " + RSet(Bin(fwKeys),32,"0"))
EndSelect
Until event = #PB_Event_CloseWindow
Sowohl WaitWindowEvent() als auch WindowEvent() haben eine Einschränkung: Sie können keinen bestimmten Rückgabewert an das OS zurückgeben. Manche Ereignisse benötigen allerdings einen bestimmten Rückgabewert, um korrekt zu funktionieren. So erwartet das OS z.B. zur Message #WM_CTLCOLORSTATIC einen Rückgabewert, der die Farbe definiert und kann ohne diesen gar nicht funktionieren. Aber auch jede andere Message erwartet einen bestimmte Rückgabewert - genaueres verrät das MSDN, bzw. die Win32.hlp.
Desweiteren gibt es - wie bereits erwähnt - einige spezielle Messages, welche von (Wait)WindowEvent nicht an das PB-Programm weitergegeben werden, z.B. #WM_NCHITTEST. Um diese noch vor der PB-eigenen Messageverarbeitung abzufangen, muss ebenfalls ein WindowCallback verwendet werden.
Der dritte Grund für einen eigenen WindowCallback könnte sein, die PB-eigenen Reaktionen auf Ereignisse mit eigenen Anweisungen zu ersetzen.
Und es gibt noch einen triftigen Grund für einen eigenen WindowCallback: Die Behandlung von Echtzeitereignissen wie z.B. #WM_SIZE (dieses wird generiert, während der User die Fenstergröße verändert. Als Reaktion darauf kann man z.B. in Echtzeit die Gadgets der neuen Größe anpassen.). Diese Nachrichten umgehen die Message-Queue und rufen direkt den WindowCallback auf. In der PB-Hauptschleife kommt diese Message dann erst an, wenn das ganze wieder vorbei ist (und im Beispiel der User die Maustaste wieder losgelassen hat).
Trotz der 4 Gründe bleibt festzuhalten: Ein eigener WindowCallback ist nur selten nötig.
Eine WindowCallback-Prozedur wird durch den Aufruf von SetWindowCallback() - welcher nach einem erfolgreichen OpenWindow() erfolgen sollte – definiert. Dabei erwartet SetWindowCallback() als einzigen Parameter einen Pointer auf den Prozedurnamen, welcher mit einem @ vor dem Prozedurnamen übergeben wird.
Z.B. SetWindowCallback(@MyWindowCallback())
Ab sofort wird diese Prozedur fortwährend vom OS mit den jeweils aktuellen Ereignisdaten aufgerufen und erwartet, dass darauf korrekt reagiert wird. Falsche Rückgabewerte auf ein Ereignis können z.B. zu einem endlosen Neuerstellen dieser Message führen.
Der Aufbau dieser WindowCallback-Prozedur ist genau vorgegeben. Das Gerüst sieht folgendermaßen aus (nachzulesen auch unter SetWindowCallback() in der PB-Hilfe):
Procedure MyWindowCallback(WindowID, Message, wParam,
lParam)
Result = #PB_ProcessPureBasicEvents
;
; Dein Code,
der die Ereignisse behandelt und den
; Wert von
'Result' entsprechend verändert.
;
ProcedureReturn Result
EndProcedure
Die 4 Parameter (hier mit den Namen WindowID, Message, wParam, lParam) enthalten also jedesmal die Daten des aktuellen Ereignisses, welche ab sofort z.B. in einem Select-Zweig bearbeitet werden können, wobei der Rückgabewert Result entsprechend korrekt modifiziert werden muss.
Für alle Ereignisse, welche nicht in der dort geschriebenen Ereignisbehandlung auftauchen, ist es absolut ratsam, den Rückgabewert #PB_ProcessPureBasicEvents beizubehalten. Liefert diese Prozedur nämlich diesen Wert zurück, so werden direkt im Anschluss dieselben Ereignisdaten noch durch die PB-eigene Ereignisbehandlung geschickt und so alle systemnahen Events entsprechend korrekt behandelt.
Auch können so die Ereignisdaten zusätzlich noch in der PB-Hauptschleife interpretiert werden.
Also: Selbst wenn Du die Ereignisbehandlung sämtlich über den WindowCallback regelst, sollte doch zumindest ein Aufruf von WindowEvent() in der PB-Hauptschleife auftauchen, und für im WindowCallback nicht behandelte Ereignisse der Rückgabewert #PB_ProcessPureBasicEvents sein.
Abschließend dazu noch ein kleines Beispiel:
; Debugger an
Procedure MyWindowCallback(WindowID, Message, wParam,
lParam)
Result = #PB_ProcessPureBasicEvents
Select Message
Case #WM_CLOSE ; #WM_CLOSE = #PB_Event_CloseWindow
Debug "Fenster
wird geschlossen (behandelt in WindowCallback)"
End
;
ProcedureReturn 0 nicht mehr notwendig
Case #WM_MOVE ; #WM_MOVE = #PB_Event_MoveWindow
Debug "Fenster
wird bewegt (behandelt in WindowCallback)"
ProcedureReturn 0
Default
ProcedureReturn Result
EndSelect
EndProcedure
OpenWindow(0,300,400,300,200,#PB_Window_SystemMenu,"Bewege das Fenster")
CreateMenu(1, WindowID())
MenuItem(1, "Klick mich")
SetWindowCallback(@MyWindowCallback())
Repeat
event = WindowEvent()
If event = #PB_Event_Menu
Debug "Es wurde ins Menu geklickt
(behandelt in PB-Hauptschleife)"
EndIf
Delay(10)
ForEver
So, das wars, ich hoffe dieses Tutorial hat Dir Freude gemacht und etwas gebracht.
Für Anregungen und Kritik aller Art bin ich gerne offen: kurras@schalldesign.de