Windows-Programme (XbpDialog)
Voraussetzungen eines Windows-Programms
Voraussetzung ist, dass das Programm ein XbpDialog() Fenster verwendet und - in der Folge - mit dem Link-Switch /PM:PM gelinkt wird.
Dadurch sind die "klassischen" @ SAY/GET/PROMPT Befehle nicht mehr verwendbar, und "neues" tritt an ihre Stelle, nämlich ein neues Koordinaten-System, das standardmässig unten links seinen Bezugspunkt ({0, 0}) hat.
kleines Demo-Programm
#INCLUDE "AppEvent.ch" FUNCTION AppSys() Local aSize[2], aPos[2], nI, oDlg aSize := AppDesktop():currentSize() FOR nI := 1 TO 2 aSize[nI] := Int(aSize[nI] * 0.8) NEXT aPos := {0, 0} oDlg := XbpDialog():new(AppDesktop(), AppDesktop(), aPos, aSize) oDlg:sysmenu := .T. oDlg:hideButton := .T. oDlg:taskList := .T. oDlg:close := {|| PostAppEvent(xbeP_Close)} oDlg:title := "" oDlg:create() oDlg:setTitle("kleine GUI-Demo") SetAppWindow(oDlg) CenterControl(oDlg) RETURN (.T.) FUNCTION Main() LOCAL nEvent, mp1, mp2, oXbp nEvent := xbe_None WHILE nEvent <> xbeP_Close nEvent := AppEvent(@mp1, @mp2, @oXbp) oXbp:handleEvent(nEvent, mp1, mp2) END RETURN(.T.)
Dieser Programmcode erzeugt ein Fenster, dessen äussere Dimension 80 % des Windows-Desktop beträgt, und das zentriert auf dem Bildschirm angezeigt wird.
Das Programm reagiert auf die Standard-Windows-Befehle, die über das System-Menü eingegeben werden können, wir können es verschieben, die Grösse verändern, minimieren, maximieren, wiederherstellen - alles das ist quasi "im Paket" drin, da das verwendete XbpDialog() Fenster implizit mit den entsprechenden Ereignissen umgehen kann.
Mehr ist definitiv (noch) nicht drin.
Erweiterung des Demo-Programms um ein Xbase-Part
Beginnen wir mit der Erforschung des Fensters. Hierzu implementieren wir eine Control, die uns Informationen liefern soll:
FUNCTION Main() LOCAL nEvent, mp1, mp2, oXbp, aPos, aSize aPos := {10, 10} aSize := {100, 20} oXbp := XbpPushButton():new(SetAppWindow():drawingArea(),; SetAppWindow():drawingArea, aPos, aSize) oXbp:caption := "Klick mich!" oXbp:activate := {|| ShowInfo()} oXbp:create() nEvent := xbe_None WHILE nEvent <> xbeP_Close nEvent := AppEvent(@mp1, @mp2, @oXbp) oXbp:handleEvent(nEvent, mp1, mp2) END RETURN(.T.) FUNCTION ShowInfo(oPush) Local cMsg, oParent, aSize cMsg := "Dimension des Parent: {" oParent := oPush:setParent() aSize := oParent:currentSize() cMsg += LTrim(Str(aSize[1])) + ", " + LTrim(Str(aSize[2])) + "}, " cMsg += "Dimension des Over-Parent {" oParent := oParent:setParent() aSize := oParent:currentSize() cMsg += LTrim(Str(aSize[1])) + ", " + LTrim(Str(aSize[2])) + "}." ConfirmBox(, cMsg, "Information", XBPMB_OK, XBPMB_INFORMATION) RETURN (.T.)
Bevor wir die Einzelheiten betrachten, erstellen wir das Programm, starten es und klicken auf den XbpPushButton():
Dimension des Paren: {1008, 781}, Dimension des Over-Parent {1024, 819}.
Mein Rechner hat einen Monitor mit einer Darstellung von 1280*1024 Pixel. 80 % davon sind 1024*819,2 - wobei wir ja die Dimensionen auf die jeweilige Ganzzahl begrenzt haben, also 1024*819. Dies ist dann auch die Dimension unseres SetAppWindow() und enthält die Titelliste (title bar), und die Rahmen um das Fenster (nochmals der Hinweis: wir haben keine abweichenden Presentation Parameter in der new() Methode definiert, daher "erbt" unser Programm die Standard-Einstellungen, die in Windows definiert sind.
Würden wir versuchen, diesen Bereich zu nutzen, sind Kollisionen mit den Randbereichen fast vorprogrammiert. "Innerhalb" des XbpDialog()-Fensters gibt es einen rechteckigen Bereich, die sogenannte drawingArea, die als Ziel (Parent und/oder Owner) für unsere Xbase-Parts verwendet werden sollte (man muss nicht, sollte aber).
Durch das Klicken auf den XbpPushButton() wird der activate Slot ausgeführt, und der ruft die Funktion ShowInfo() auf und übergibt eine Referenz auf den XbpPushButton() (daher auch als [[self]9 definiert).
In ShowInfo() erstellen wir eine String zur Anzeige mit der Funktion ConfirmBox(). Als erstes weisen wir die Grösse des Parents des XbpPushButton() aus.
Um den Parent eines Xbase-Parts zu ermitteln, verwendet man die Methode setParent(). Diese liefert eine Referenz auf den bisherigen Parent zurück und kann - wenn erforderlich - auch einen neuen Parent definierten. Uns reicht die Referenz auf den bisherigen Parent aus.
Die Methode currentSize() liefert die aktuelle Grösse des Xbase-Parts, mit ein paar kleinen Konvertierungen wird die Nachricht erweitert und der Parent des Parent abgefragt. Die weiteren Schritte wiederholen sich.
Die Verwandschaftsverhältnisse
AppDesktop() + | XbpDialog() + | XbpDialog():drawingArea + | XbpPushButton()
Diese Struktur zeigt auf, welches Element für welches Xbase-Part als Parent dient. Die Abfrage "von unten nach oben" haben wir mit der Methode setParent() durchgeführt, wobei es auch in die andere Richtung geht. Hierfür wird die Methode childList() verwendet, die allerdings als Array zurückgegeben wird, denn:
- es kann kein Child geben (würden wir diese Abfrage z.B. auf XbpPushButton() anwenden - es kann ein Element vorhanden sein (wie in unserem Beispiel) - es kann eine (theoretisch) unbegrenzte Anzahl von Elementen geben
Etwas über die Orientierung
Wer von Clipper kommt, für den mag die Tatsache, dass die Koordinaten "auf den Kopf gestellt sind", nicht so einfach zu verdauen sein.
Es gibt die Möglichkeit, die Koordinaten zu drehen.
Die Methode new() kennt einen fünften Parameter, der die Darstellung des Xbase-Parts beeinflusst. aPP ist ein Array von Arrays. Das könnte dann so aussehen:
aPP := { {XBP_PP_ORIGIN, XBP_ORIGIN_TOPLEFT} }
Damit würde der Ursprung für das Koordinatensystem nach oben links verlegt und entspräche (in etwa) dem von Clipper bekannten System.
Wie geht es weiter?
Über was für ein Programm reden wir jetzt? Haben wir einen einfachen Dialog, dann kann man (theoretisch) alle Dialog-Elemente auf die :drawingArea packen, und gut ist.
Nur sind die meisten Programme nicht so einfach gestrickt, dass das ausreichen würde. Es gibt oft mehrere Dialoge (angefangen von den Programmeinstellungen über verschiedene Stammdaten bis hin zu komplexeren Dialog-Masken), die entsprechend untergebracht werden wollen.
Was unter Clipper zwingende Natur war: es ist immer nur ein Dialog im "Vordergrund" feiert unter Windows sein Begräbnis: der Anwender kann alle Fenster auf einmal aufmachen und darin "rummachen". Mit etwas Glück verschiebt er in Fenster 3 den Datensatz-Zeiger aus Fenster 5, mit hübschen (oder katastrophalen) Konsequenzen.
An dieser Stelle wollen wir noch ein wenig das Augenmerk auf den Dialogen halten, während wir an anderer Stelle über die parallele Bearbeitung von Daten (Thema Threads) sprechen müssen.
Schauen wir uns dazu ein paar Konzepte an:
Parent und Owner
Diese beiden Begriffe sind schon sehr oft gefallen, aber den richtigen Unterschied haben wir noch nicht herausgearbeitet.
Jedes Xbase-Part steht in zwei Beziehung zu einem anderen, zum Zeitpunkt seiner Erzeugung (Methode :new()) bereits vorhandenen Xbase-Part.
Parent und Owner sind - wenn sie bei der Methode :new() nicht explizit benannt werden - immer das Objekt, das von SetAppWindow() zurückgegeben wird. Daher kann es schon sehr wichtig sein, die beiden Referenzen explizit anzugeben, wenn man mehr als ein XbpDialog()-Objekt verwendet.
Parent
Der Parent definiert so etwas wie die physische Beziehung zwischen zwei Xbase-Parts.
Sprechen wir von Fenstern, so ist der Parent ist der Bereich, auf den sich ein Xbase-Part beschränkt, was seine "Beweglichkeit" angeht:
oXbp := XbpDialog():new(oDlg:drawingArea, oDlg:drawingArea, ...)
Diese neue Fenster kann nur innerhalb von oDlg:drawingArea bewegt werden. Versuche, es aus diesem Bereich zu verschieben, führen dazu, dass Teile von oXbp "unter dem Rahmen" verschwinden.
oXbp := XbpDialog():new(AppDesktop(), oDlg:drawingArea, ...)
erzeugt ein XbpDialog()-Fenster, das aus oDlg herausgebewegt werden kann, weil sein "Aktionsradius" nur durch den Desktop definiert wird.
Der Nachteil des Verfahrens ist, dass oXbp nicht über oDlg:childList() lokalisiert werden kann.
Bei anderen Xbase-Parts stellt sich diese Frage nicht, weshalb auf das Xbase-Part verwiesen werden sollte, auf dem das jeweilige Element platziert werden soll (sichtbar sein soll).
Owner
Der Owner definiert so etwas wie die logische Beziehung zwischen zwei Xbase-Parts.
Standardmässig verwendet die :new() Methode den Parent, wenn kein Owner definiert wurde.
Was bedeutet nun "logische Beziehung"? Es gibt Xbase-Parts, die in einer zwingenden Beziehung stehen, dazu gehören z.B. die XbpRadioButton(). Für eine sinnvolle Control müssen mindestens zwei definiert werden. Von den XbpRadioButton() einer Gruppe kann immer nur einer aktiv sein, während alle anderen auf inaktiv gesetzt sind (so, wie immer nur eine Radio-Station an einem alten, klassischen Dampfradio aktiv sein konnte). Um dies zu regeln, erzeugt man ein XbpStatic() vom Typ GroupBox und platziert die XbpRadioButton() auf dem Static. Wenn ich dem XbpStatic() drei XbpRadioButton() zuordne und den XbpStatic() als Owner definiere, kümmert sich das Runtime-System darum, dass eben immer nur eine der drei XbpRadioButton() aktiv sein kann.