VOICE Homepage: http://de.os2voice.org |
April 2003
[Inhaltsverzeichnis]
|
Von Thomas Klein © April 2003 |
Willkommen zum siebten Teil unserer "kleinen" Serie über DrDialog. Heute werden wir das Thema Container abschließen und uns dazu anschauen, wie man auf Benutzeraktionen reagieren kann, die innerhalb eines Containers auftreten. Aber zuerst sollte ich noch auf ein paar Sachen aufmerksam machen, die ich im Artikel der letzten Ausgabe vergessen habe...
Wie alle Controls, die man in DrDialog verwenden kann, so besitzt
auch der Container eine gewisse Auswahl an Einstellungen, die über
den Style-Dialog beeinflußt werden können. Diese
Einstellungen sind übrigens nicht spezifisch nur für DrDialog
- Sie werden genau diese Eigenschaften auch in anderen
Entwicklungsumgebungen für OS/2 wiederfinden. Das liegt daran,
daß es sich beim Container um ein Objekt handelt, welches nicht
DrDialog-eigen ist, sondern vielmehr OS/2-eigen, um es so zu sagen.
Auch auf die Gefahr hin, die Hintergründe und Zusammenhänge
hier nicht korrekt wiederzugeben, erkläre ich es einmal so: Alle
Programme, die unter der Regie des Presentation Managers laufen,
können ein solches Objekt verwenden, da es vom System selbst (bzw.
dem PM) bereitgestellt wird. Somit ist es unerheblich, welches
Entwicklungswerkzeug Sie letztendlich zur Erstellung einer
PM-Applikation verwenden (DrDialog, VX-REXX, Visual Age für
[was-auch-immer] und so weiter), denn die Verwendung erfolgt im Prinzip
nur durch die Übergabe bestimmter Parameter an das im System zur
Verfügung gestellte Objekt.
Der Grund, warum ich das so lang und breit erkläre ist, daß die DrDialog-Hilfe sich bezüglich der style-Dialoge ziemlich ausschweigt, um es einmal freundlich zu beschreiben... Ich konnte jedenfalls keine Angaben zu den Bedeutungen der einzelnen Einstellungen finden (abgesehen von der Erläuterung, daß die Style-Einstellungen über einzelne Bits eines 32-Bit-Werts gespeichert werden und wie man mittels der REXX-Funktion bitor einzelne Einstellungen ändern kann). Da wir aber nun wissen, daß es für ein Objekt wie den Container letztendlich egal ist, ob man DrDialog oder z.B. VX-REXX einsetzt, könnte man ja in einem besser dokumentierten Entwicklungspaket einmal nachschlagen, was die Einstellungen bedeuten.
Bevor Sie sich jetzt aber verzweifelt fragen, warum ich das denn nicht gemacht habe: Ich bin schlichtweg zu faul, um Visual Age C++ 3 zu installieren. Ich hätte zwar noch zwei andere Pakete hier am Start, aber... seufz... ich konnte mich bisher nicht dazu durchringen. Und ehrlich gesagt war es für meine bisherigen Programmierungsarbeiten mit DrDialog auch nicht wirklich notwendig, denn die Standardeinstellungen und die paar Sachen, die ich mir zusammenreimen konnte, haben allemal gereicht. Na mal sehen - vielleicht versuch ich's doch noch...
Kommen wir nun aber endlich auf
den Punkt: Der Style-Dialog des containers enthält drei
Gruppen von Einstellungsmöglichkeiten. Die erste Gruppe selection
styles gibt an, welche Arten der Auswahl von Einträgen dem
Endanwender zur Verfügung steht. Das ist im Prinzip dasselbe wie
das, was wir bereits von den Objekten listbox
oder combobox
kennen:
Single bewirkt, daß der Anwender immer nur eines der im Container enthaltenen Objekte auswählen bzw. "markieren" kann. Wird ein neuer Eintrag gewählt, so wird der vorher ausgewählte Eintrag abgewählt.
Mit Multiple kann der Anwender mehrere Einträge
auswählen, indem ein Eintrag durch Anklicken vom Zustand
"ausgewählt" in "nicht ausgewählt" wechselt und umgekehrt,
ohne den Zustand der anderen Einträge dadurch zu beeinflussen.
Extended
schließlich ist die Luxusvariante der
Selektion - hier stehen bestimmte Kombinationen aus Mausklick und
Tastaturfunktion zur Verfügung. So kann ein fortlaufender Bereich
von Einträgen im Handumdrehen ausgewählt werden, indem der
erste Eintrag angeklickt wird und auf dem letzten gewünschten
Eintrag mit gedrückter Umschalttaste geklickt wird: Alle
Einträge, die sich gemäß der Eintragsreihenfolge
zwischen diesen beiden befinden, werden dann auch gewählt.
Einzelne Einträge werden durch Klicken mit gedrückter
Steuerungstaste ("Strg" bzw. "Ctrl") jeweils an- bzw. abgewählt. Aber
Vorsicht: Ein simpler Mausklick (also ohne eine der beiden o.g.
Tasten) bewirkt, daß alle vorher ausgewählten Einträge
abgewählt werden, bevor der angeklickte Eintrag ausgewählt
wird.
Container Styles beinhaltet zwei Einstellungen, zu denen ich Ihnen leider nichts sagen kann. Ich habe sowohl Mini record als auch Verify pointers einmal ausprobiert, aber keine Wirkung feststellen können. Wahrscheinlich liegt es daran, daß diese Einstellungen etwas "internes" am Verhalten des Objekts ändern oder etwas, das aufgrund meiner nur rudimentären Testprogramme mit DrDialog gar nicht angesprochen wurde. Tja. Kommen wir also zu den beiden anderen Einstellungen:
Auto position sollten Sie bei der Arbeit mit DrDialog
unbedingt immer aktiviert lassen. Das liegt daran, daß bestimmte
Funktionen unter DrDialog nicht zur Verfügung stehen, die beim
Füllen eines Containers in "Bitmap"-Darstellung normalerweise
nötig wären: Wenn ein Eintrag in einen solchen Container
hinzugefügt wird, müßte der Programmierer mitteilen, wo
im Container der neue Eintrag stehen soll, denn - Sie kennen das von den
WPS-Ordnern - man kann Symbole (oder Einträge) mit der Maus
verschieben (das trifft in der WPS natürlich nur auf Ordner zu, die
in der Strukturansicht die Option Wie plaziert anstelle
von gerastert, ein- oder mehrspaltig verwenden). Kurz gesagt
können Sie einen Container in "Bitmap"-Darstellung also mit
einem WPS-Ordner in Symbolanzeige vergleichen. Dazu kommt dann,
daß sich ein Container mit Auto position verhält wie
ein WPS-Ordner mit der Option gerastert, d.h. er kümmert
sich automatisch um das Anordnen der Einträge. Das mag jetzt
vielleicht ein wenig verwirrend sein, daher lassen Sie mich stattdessen
einfach kurz beschreiben, was passiert, wenn man einen Container in
"Bitmap"-Darstellung verwendet und Auto position nicht
verwendet wird:
In diesem Fall benötigt das System vom Entwickler eine Angabe zur
Position des neuen Eintrags. Da man diese Position unter DrDialog aber
nirgends angeben kann, wird für alle Einträge dieselbe
Position verwendet. Sie hätten also beim Hinzufügen von zehn
Einträgen einen schönen "Stapel" von zehn Einträgen, von
denen Sie allerdings nur den letzten ("obersten") sehen. Dumm
gelaufen... beachten Sie aber, daß Auto position nur
Bedeutung hat, wenn sich der Container im "Bitmap"-Darstellungsmodus
befindet. Alle anderen Darstellungen sorgen aufgrund Ihrer Natur ohnehin
für eine automatische Anordnung der Einträge.
Read only sorgt dafür, daß der Anwender keine Einträge durch Anklicken mit gedrückter Alt-Taste ändern kann, was standardmäßig der Fall ist. Dies bezieht sich auf alle Darstellungstypen und sorgt im Fall der "Detailansicht" auch dafür, daß keine Spaltenwerte geändert werden können, selbst wenn die jeweilige Spalte als nicht-schreibgeschützt definiert wurde. Trotzdem hat der Entwickler natürlich volle Kontrolle über die Einträge, d.h. "aus dem Code heraus" können Sie an den Einträgen ändern, was Sie wollen. Dadurch können Sie zum Beispiel erreichen, daß der Endanwender bestimmte Funktionen Ihres Programms verwenden "muß" um die Einträge zu bearbeiten, anstatt die im Container-Control "eingebaute" Funktionalität zu nutzen. Abschließend noch der Hinweis, daß Read Only natürlich auch keinen Einfluß auf die anderen Ereignisse hat - das Anklicken eines Objekts wird nach wie vor als Ereignis gemeldet.
Zu den Optionen unter Basic styles gibt es nicht viel zu
sagen. Visible
bewirkt, daß der Container
angezeigt wird oder nicht. Wird er nicht angezeigt, kann der Anwender
ihn nicht benutzen und es werden dadurch natürlich auch keine
Ereignisse dafür generiert. Vielleicht kommen Sie einmal in eine
Situation, in der Sie einen "versteckten" Container verwenden
können, um bestimmte Aufgaben im Hintergrund auszuführen, aber
im Moment fällt mir dazu kein Beispiel ein.
Disabled ist da schon eher alltagstauglich. Wenn Sie diese
Option verwenden (also "ankreuzen"), kann der Anwender den Container und
dessen Inhalte zwar sehen, aber nichts darin "tun", denn es werden keine
Ereignisse generiert. Das eignet sich hervorragend, um dem Anwender
etwas zu präsentieren, auf das er keinen Einfluß nehmen kann
- außer durch Funktionen, die Sie als Entwickler ihm (z.B. mittels
Menü oder Pushbuttons) zur Verfügung stellen.
Bedenken Sie aber, daß so etwas im Sinne der GUI-Ethik aber
grundsätzlich vermieden werden sollte, denn alle im Container
enthaltenen visuellen und funktionellen Möglichkeiten für den
Anwender werden dadurch wieder zunichte gemacht. Das Ändern eines
Eintrags beispielsweise wird in diesem Fall sehr umständlich
für den Anwender sein, denn er muß "irgendwie" den zu
ändernden Eintrag angeben und eine Auswahl des Eintrags aus dem
Container (per Maus oder Tastatur) scheidet dann natürlich aus.
Überlegen Sie also, ob Sie das angestrebte Anzeige- und
Programmverhalten Ihrer Anwendung nicht anders bewerkstelligen
können.
Zur TAB-Reihenfolge der einzelnen Elemente ist noch etwas wichtiges zu erwähnen: Ich gehöre noch zu der (anscheinend aussterbenden) Gattung der Anwender, die ein GUI-Programm auch prima ohne Maus benutzen können - sofern es Sinn macht, denn ein Grafikprogramm zum Zeichnen ohne Maus zu benutzen, macht beispielsweise absolut keinen Sinn mehr. Selbst in den heutigen modernen Zeiten ist es manchmal recht praktisch, mit der Tastatur arbeiten zu können, denn ich verwende eine drahtlose optische Maus und irgendwann (für gewöhnlich im unpassendsten Moment) geben dann deren Akkus auf. Abgesehen von der ohnehin schnelleren Bearbeitung per Abkürzungstasten oder Hotkeys ist man machmal per Tastatur einfach "besser dran"... allerdings hängt das in erster Linie vom Programm ab.
Ich habe mich aber schon oft schwarz über handgestrickte GUI-Elemente geärgert, die sich nicht per Tastatur anwählen/bedienen lassen, obwohl es ihrer Natur nach kein Problem sein sollte, aber noch schlimmer sind die Programme, bei denen es zwar funktioniert, die TAB-Reihenfolge aber derart verheerend vernachlässigt wurde, daß es eine wahre Wonne ist, dem sinnlos hin- und herspringenden "focus" beim Drücken der TAB-Taste zuzuschauen. Ich weiß, daß z.B. Visual Basic die Möglichkeit bietet, den TAB-Index der Elemente während der Entwicklungszeit festzulegen. Beim Erstellen des Dialogs wird der Tabindex jeweils pro neu eingefügtes Control erhöht... und das ist auf den ersten Blick in Ordnung. Sobald der Entwickler aber die Elemente auf dem Dialog neu anordnet, behalten diese Ihren TAB-Index und der Spaß beginnt... Viele VB-Programmierer vergessen aber, den Tabindex Ihrer Elemente "aufzuräumen", bevor sie ihr Produkt ausliefern (oder testen, wenn überhaupt)... was dann zu dem beschriebenen Symptom führt.
Warum ich das erzähle? Ganz einfach: Bei DrDialog brauchen Sie sich - Gott sei Dank - nicht um so etwas zu kümmern, denn die TAB-Reihenfolge wird von DrDialog zur Laufzeit des Programms automatisch anhand der visuellen Anordnung der Elemente bestimmt: Von oben nach unten und innerhalb dieser Richtung von links nach rechts mit Ausnahme der Controls, die sich innerhalb eines Containers (z.B. einem "Group"-Rahmen) befinden: Diese werden dann zunächst innerhalb ihres Containers in der angegebenen Reihenfolge angesprungen.
Aber um es kurz zu machen: Verschwenden Sie bei DrDialog keinen Gedanken an die TAB-Reihenfolge! DrDialogs Automatismus ist so ziemlich das beste Beispiel für "eigentlich ist es doch ganz einfach...", das ich bis jetzt gesehen habe. In 99% aller Fälle funktioniert er wunderbar und genau so, wie man's erwartet.
Kommen wir zurück zu unserem Container. Letztes mal haben wir
gelernt, wie man Container-Einstellungen definiert und Einträge
hinzufügt. Heute kümmern wir uns um das Entfernen von
Einträgen. Die Funktion (oder "Methode") hierfür heißt delete
und hat folgende Syntax:
rc = [dialog.][control.]Delete([item])
Nach dem Aufruf enthält die Variable rc
die Anzahl
der gelöschten Einträge. Auch hier ist es - wir erinnern uns -
möglich, anstelle der Funktionsschreibweise die CALL-Variante zu
verwenden, wenn uns die Anzahl der gelöschten Einträge nicht
interessiert... In diesem Fall würde die Syntax wie folgt lauten:
call [dialog.][control.]Delete([item])
Um es mit Beispielen zu verdeutlichen, nehmen wir an, Sie haben
einen Dialog namens dlg_test
und einen Container namens cnt_hugo
.
Dann sähen die beiden Schreibweisen so aus (die Angaben von "/*"
bis "*/ " sind Kommentare und für die Verwendung der Funktion
natürlich nicht erforderlich):
rc = dlg_test.cnt_hugo.Delete(eintragsid) /* Funktionsschreibweise */
call dlg_test.cnt_hugo.Delete eintragsid /* CALL-Schreibweise */
In diesem Beispiel gehen wir davon aus, daß die Variable
namens eintragsid
die ID des zu löschenden Eintrags
enthält. Dazu kommen wir gleich - lassen Sie mich erst noch
erklären, daß man delete
auch ohne die Angabe
eines bestimmten Eintrags verwenden kann. Dann werden nämlich alle
Einträge des Containers gelöscht. Das erreicht man ganz
einfach, indem der Parameter item
nicht angegeben wird. Das
würde dann für das obige Beispiel so aussehen:
rc = dlg_test.cnt_hugo.Delete()
bzw. für die CALL-Schreibweise:
call dlg_test.cnt_hugo.Delete
...und schwupp - sind sie alle weg. Das war ja typisch: Um
Einträge erst einmal anzulegen, muß man einen Haufen Arbeit
über sich ergehen lassen, aber beim Löschen geht's ruck-zuck!
Natürlich (aber das wissen Sie ja) kann man den optionalen
Parameter [dialog]
weglassen, wenn delete
im
Code-Kontext desselben Dialogs aufgerufen wird, und zusammen mit [control]
sind beide überflüssig, wenn Sie delete
aus einer
Ereignisroutine des Containers selbst aufrufen würden.
Angenommen, Sie hätten in Ihrem Dialog unter dem Container einen
Pushbutton zum Löschen aller Einträge, dann würden Sie im Click
-Ereignis
des Pushbuttons codieren:
call cnt_hugo.Delete
und schon wäre der Container leer. Wenn Sie die Funktionsschreibweise verwenden würden, wie in
futsch = cnt_hugo.Delete()
...könnten Sie der Variable futsch
danach die
Anzahl der Einträge entnehmen, die gelöscht wurden (falls Sie
das dann noch für irgendwas gebrauchen).
Schön, kommen wir nun aber zurück auf den Parameter [item].
Daß man alle Einträge eines Containers löschen
will, kann ja vorkommen und ist auch oft der Fall, aber seien wir
ehrlich - normalerweise geht es doch darum, bestimmte
Einträge zu löschen, nicht wahr? Wie ich bereits sagte, kann
man delete
auch unter Angabe einer ID eines Eintrags
verwenden. Die ID wird beim Hinzufügen eines Eintrags mittels der add
-Funktion
vom System zurückgegeben.
Hmm... das bedeutet also, daß man diese ID irgendwo speichern
müßte, damit man den Eintrag hinterher auch wieder
löschen kann, richtig? Falsch.
Gehen Sie nicht direkt so weit in die Technik: Überlegen Sie
zuerst einmal, was dazu führen kann, daß ein
bestimmter Eintrag gelöscht wird. In einem gewöhnlichen
Szenario würde der Anwender entweder den gewünschten Eintrag
anklicken oder er würde mehrere zu löschende Einträge
auswählen. Diese Fälle lassen Sich recht einfach mit delete
unter Zuhilfenahme anderer Funktionen abbilden, die wir gleich
besprechen werden.
Natürlich kann es auch sein, daß Sie dem Anwender aus
gutem Grund das Herumklicken im Container anhand der entsprechenden Style-
Einstellungen
(siehe oben) nicht gestattet haben. Dann stehen Sie zwar nicht auf
verlorenem Posten, wenn's um das Löschen eines bestimmten Eintrags
geht, nur wird das Ermitteln der zugehörigen ID etwas
komplizierter. Es gibt keine Funktion, die IDs der Einträge im
Nachhinein zu ermitteln! Sie sollten dann wirklich in einer separaten
Liste oder internen Tabelle die jeweils von der add-
Funktion
zurückgegebenen IDs speichern und Sie mit einem anderen Wert des
Eintrags verknüpfen, den Sie als Ausgang für Ihre Aktion
verwenden wollen. Kurzes Beispiel: Sie wollen es ermöglichen,
Einträge basierend auf dem Namen des Eintrags zu löschen. Das
ist zwar ganz nett, aber bedenken Sie, daß es im Container eine
unbestimmte Anzahl an Einträgen geben kann, die alle denselben
Namen verwenden (außer, Sie haben dafür Sorge getragen,
daß das nicht der Fall sein kann). Technisch gesehen sind dem
Container alle Daten eines Eintrags piepegal, denn intern verwendet er
ohnehin nur die ID. Wenn Sie also alle Einträge löschen wolle,
deren Name mit "A" beginnt, müssen Sie eine Tabelle anlegen, die
den Namen eines Eintrags mit dessen ID verknüpft. Dann können
Sie anhand der Tabelle jeden Eintrag finden, der mit "A" beginnt und
dazu die ID ermitteln. Dann löschen Sie den Containereintrag
mittels delete
und der ID aus der Referenz. Und vergessen
Sie nicht, auch Ihre interne Tabelle entsprechend zu bereinigen -
verwenden Sie also am Besten eine unsichtbare Listbox, da ist das
Bereinigen etwas einfacher.
Sie sehen schon - das erfordert einiges an zusätzlicher Arbeit. Ein Grund mehr, den Container (in Anlehnung an die GUI-Ethik) nur dann zu verwenden, wenn Sie dem Anwender auch die dadurch gebotenen Möglichkeiten zur Verfügung stellen können... schauen wir uns also an, wie man einen vom Anwender ausgewählten Eintrag löschen kann.
Nun kommen wir zu einem etwas komplexen Thema, das einem auf den ersten Blick nicht bewußt ist. Der Unterschied zwischen einem selektierten (ausgewählten) Eintrag und dem Eintrag, der gerade den Cursor enthält (aktueller Eintrag). In diesem Zusammenhang müssen folgende Faktoren berücksichtigt werden:
Das problematische ist nämlich, daß der aktuelle
Eintrag nicht immer ein ausgewählter sein muß und
umgekehrt. Am einfachsten sieht die Geschichte aus, wenn der Container
mit der Style-Einstellung single definiert wurde: Hier
kann immer nur ein Eintrag im Container ausgewählt werden. Egal, ob
man mit der Maus oder der Tastatur arbeiter, ist der aktuelle
Eintrag auch immer der ausgewählte Eintrag. Bei den Styles multiple
und extended
können einzelne Einträge an-
und abgewählt werden und der aktuelle Eintrag (also
der, der gerade mit einem Rähmchen umrandet ist und den Cursor
enthält) kann ein ganz anderer sein.
Jetzt kommt nämlich die Frage: Wenn der Anwender "Löschen"
befiehlt - was löscht man dann? Den aktuellen Eintrag oder
den/die ausgewählten?
...
Hmm... nicht einfach, was? Das Ganze hat nichts mit den IDs zu tun, die
man für die delete
-Funktion benötigt, sondern
ist ein grundsätzliches Verständnisproblem. Wie gesagt: Bei
der Style-Einstellung simple
ist immer
nur ein Eintrag ausgewählt und gleichzeitig auch der
aktuelle. Hier fällt es leicht, auf eine Löschanforderung
zu reagieren. In den anderen Fällen gibt es die folgenden
Möglichkeiten...
Genau dieses Verfahren wird auch abgebildet, wenn Sie in einem
Container auf das select
-Ereignis reagieren und die eventdata()
-Funktion
verwenden.
Eventdata
gibt im Kontext eines select-
Ereignisses
zwei Werte zurück: Die ID des Eintrags, für den ein Ereignis
eintritt und die Art der Änderung am Zustand des Eintrags. Wenn Sie
die Beispielapplikation vom letzten Monat heruntergeladen haben,
können Sie sich das ganze recht einfach anschauen: Laden Sie die
.RES-Datei in DrDialog, rufen Sie im Code-Editor die Registerkarte select
für den Container auf und geben Sie dort folgende Einträge an:
call eventdata
say "----"
say "id:" eventdata.1
say eventdata.2
Lassen Sie das Programm jetzt laufen und beachten Sie, welche
Meldungen im Laufzeitmonitor (wo auch das "laufende Männchen" ist)
ausgegeben werden, wenn Sie im Container von einem Eintrag zum
nächsten Wechseln. Ändern Sie dann den Style des
Containers von simple
auf extended
oder multiple
und lassen Sie das Programm erneut laufen und beobachten erneut die beim
Ab/Anwählen von Einträgen ausgegebenen Meldungen. Sie werde
erkennen, daß eventdata
in Abhängigkeit der Style-Einstellung
und Verwendung von Maus/Tastatur unterschiedliche Zustände
für die jeweiligen IDs (also Einträge) ausgibt und das einzig
wirklich "verläßliche" hierbei die Angaben sind, die sich auf
die aus/abgewählten Einträge beziehen...
Kommen wir jetzt auf die Methoden zu sprechen, die einem Anwender im Normalfall zum Löschen zur Verfügung stehen und wie man am besten darauf reagiert - dieses Verhalten habe ich mir übrigens von einem WPS-Ordner bestätigen lassen, der - Gott sei Dank - unwichtige Dateien enthielt...
Und wie setzt man das jetzt um in entsprechende delete
-Aufrufe?
Nun, wir lassen das Kontextmenü und die Taste "entf" erst einmal
außer Acht, denn Sie benötigen noch etwas zusätzliches
Wissen, was ich an dieser Stelle nicht behandeln will, weil die Sache
dann unter Umständen aus den Fugen gerät und ich es nicht mehr
vernünftig auf die Reihe bekomme oder Sie mit zuviel anderen
Details überhäufe... kümmern wir uns also um eine
Schaltfläche (pushbutton), die sich unterhalb des Containers
befindet und zum Löschen verwendet werden soll.
Wir haben beschlossen, daß sich dies auf die ausgewählten
Einträge beziehen soll. In einem Container mit Style-Einstellungsimple
würde das immer nur einen Eintrag betreffen, bei anderen Style-Optionen
dann entsprechend mehr Einträge. Wir müssen also erst einmal
ermitteln, welche Einträge im Container ausgewählt
sind. Dafür verwenden wir die Funktion getstem
,
welche wir bereits in der letzten Ausgabe angesprochen haben und die
mittels einer Stammvariablen Informationen zu mehreren
Einträgen auf einmal liefert. Die Syntax für getstem
sieht wie folgt aus:
call [dialog.][control.]getstem [stamm] [, "Select" |
"Mark" | "Cursor" | 0 | eintragsid ]
Um das ganze etwas zu entmystifizieren, hier "meine" Variante der Syntax:
call [dialog.] [control.]getstem [stamm] [, Typ]
Wenn Sie getstem
direkt in einer Ereignisroutine des
Containers aufrufen würden, könnten Sie sich die Parameter [dialog]
und [control]
sparen. Aber da wir ja gesagt haben,
daß wir das Löschen über das Klicken eines Pushbuttons
machen, der sich im selben Dialog wie der Container befindet,
können wir uns lediglich den Parameter [dialog]
verkneifen. Würde man ihn dennoch verwenden, wäre das auch
kein Problem. Nehmen wir an, der Container hieße cnt_test
,
dann wäre die Syntax
call cnt_test.getstem stamm,Typ
stamm
steht hierbei für den Namen einer Stammvariable,
die hinterher die Informationen enthalten soll. Hierbei - wie immer,
wenn es um Stammvariablen geht - ist es wichtig festzuhalten, daß
nicht die Variable selbst angegeben wird, sondern Ihr Name in
Hochkommata (einfach oder doppelt ist unerheblich). Wie Sie vielleicht
anhand der ersten Syntaxdiagramme von getstem
bemerkt
haben, ist der Name Stammvariable ein optionaler Parameter, was
bedeutet, daß man ihn auch weglassen kann. In diesem Fall wird von getstem
eine Variable namens "stem" bereitgestellt.
Der Parameter Typ
gibt an, zu welchen Einträgen
wir die Daten anfordern. Typ
kann einer der folgenden Werte
sein:
Wenn Sie für Typ angeben... |
...dann enthalten die Einträge der angegebenen Stammvariable |
"Select" |
die IDs aller momentan selektierten (ausgewählten) Einträge des containers |
"Mark" |
die IDs aller momentan markierten Einträge des containers |
"Cursor" |
den Eintrag, der gerade der aktuelle Eintrag ist, also den cursor enthält |
0 (eine Null) |
die Spaltentitel des containers für die Detailansicht |
eine eintrags-id |
die Spaltenwerte für die Detailansicht des durch eintrags-id
angegebenen Eintrags |
So weit so gut. Um also für unseren Container cnt_test
alle gerade selektierten Einträge in der Stammvariable auswahl
abzulegen, würde der Aufruf lauten:
call cnt_test.getstem "auswahl","Select"
...und würden wir auf die Angabe einer eigenen Variable verzichten (und "stem" verwenden), müßte es heißen:
call cnt_test.getstem ,"Select"
Beachten Sie, daß das Komma vor "select"
erhalten
bleiben muß, damit REXX richtig erkennt, daß der erste
Parameter weggelassen wurde.
Als nächste (und letzte) Vereinfachung sei noch genannt, daß
der Vorgabewert für Typ
(wenn man also nix angibt) "Select"
lautet. Sie können also dasselbe wie im obigen Beispiel auch
bewerkstelligen, indem Sie einfach schreiben
call cnt_test.getstem "auswahl"
wodurch für den fehlenden Typ
-Parameter also intern
den Default "Select"
verwendet wird.
(Wußten Sie übrigens, daß die amts-EDV-deutsche
Bezeichnung für Defaultden schönen Namen
"Fehlwert" trägt? ;) )
Bei Nichtverwendung einer eigenen Stammvariablen können Sie das
Ganze sogar sage und schreibe wie folgt codieren:
call cnt_test.getstem
Na, das ist doch schon ziemlich "griffig", oder? ;)
Schön. jetzt haben wir in den Einträgen der Stammvariablen
also die IDs aller selektierten Einträge des Containers.
Zusätzlich stellt uns getstem
im Eintrag "Nummer Null"
der Stammvariablen die Anzahl der Einträge insgesamt bereit. Wir
können also eine Schleife bauen, die beim ersten Eintrag beginnt
und beim letzten Eintrag endet, da wir ja wissen, wie viele
Einträge vorhanden sind. Schleifenkonstrukte in REXX haben wir zwar
noch nicht besprochen, aber lassen Sie das folgende mal auf sich wirken:
do i = 1 to auswahl.0
[hier die Anweisungen einfügen, die für jeden Eintrag geschehen soll]
end
Dieser Code bewirkt, daß die Variable i
auf den Wert 1
gesetzt wird und danach so lange um 1
hochgezählt
wird, bis ihr Wert so groß ist wie das, was in der Variablen auswahl.0
steht.
Dann endet die Wiederholungssequenz. Bei jedem Lauf werden die
Anweisungen durchgeführt, die "dazwischen" stehen. Um
beispielsweise alle IDs auszugeben, könnten Sie folgende Schleife
verwenden:
do i = 1 to auswahl.0
say auswahl.i
end
Da der Wert für i
sich bei jedem Durchlauf um 1
erhöht, würde im ersten Durchlauf der Inhalt der Variable auswahl.1
ausgegeben, im nächsten Durchlauf der von auswahl.2
,
dann der von auswahl.3
und so weiter, bis der letzte
Eintrag erreicht wird. Die Variable auswahl.0
(genau
genommen der Eintrag Nummer Null der Stammvariable "auswahl")
enthält die Anzahl der Einträge in der Stammvariable
(beginnend mit Eintrag 1 für den ersten). Das ist gleichbedeutend
mit der Anzahl der markierten Einträge.
Damit eignet sich getstem
z.B. auch hervorragend für
eine Statusanzeige unter Ihrem Container, in der Sie angeberisch immer
anzeigen können, wieviele Einträge gerade ausgewählt
sind. Das macht natürlich keinen Sinn, wenn Ihr Container die Style-Auswahloption
single verwendet, denn dann kann's ja immer nur 1 Eintrag sein. ;)
Aber natürlich wollen wir nicht wissen, wie die IDs der
ausgewählten Einträge heißen, nee - wir wollen die
Dinger loswerden. Da wir also wissen, daß auswahl.i
jeweils
die entsprechende ID eines Eintrags enthält, kombinieren wir das
doch mit der weiter oben besprochenen delete
-Funktion und
machen daraus:
do i = 1 to auswahl.0
call cnt_test.delete auswahl.i
end
Wenn Sie jetzt denken, daß das Ganze wegen der Verwendung von IDs
etwas umständlich ist, dann möchte ich noch einen Kommentar
hinterherschicken:
Bei einer Listbox werden die Einträge anhand Ihres Index (der
"Platznummer" oder laufenden Nummer) identifiziert... hier hat das
Löschen von mehreren Einträgen einen ganz anderen Haken: Wenn
Sie beispielsweise die Einträge 7 bis 10 einer Listbox löschen
wollen, liegt es natürlich nahe, dies dadurch zu erreichen,
daß in einer Schleife von 7 bis 10 der jeweilige Eintrag
gelöscht wird. Im Prinzip wäre das ja richtig, nur...
dummerweise ist die Listbox so ausgelegt, daß keine "leeren
Plätze" darin enthalten sind. Nachdem Sie Eintrag Nummer 7
gelöscht haben, wird die Liste intern wieder "aufgeräumt" -
alle folgenden Einträge rücken einen Platz höher... soll
heissen: Der vorherige Eintrag Nummer 8 ist jetzt 7, Eintrag 9 wird zu
8, 10 zu 9 und so weiter...
In Wirklichkeit löschen Sie mit dieser Schleife also die
Einträge 7, 9, 11 und 13 (anstelle 7, 8, 9 und 10). Und wenn das
passiert, haben Sie sogar noch Glück gehabt, denn würde die
Liste nur aus 10 Einträgen bestehen, bekämen Sie schon beim
dritten Aufruf (Lösche Nummer 11) eine nette Fehlermeldung, weil
Sie versucht haben, einen Eintrag zu löschen, den es in der Liste
gar nicht (mehr!) gibt - ganz zu schweigen davon, daß Sie den ja
eigentlich auch gar nicht löschen wollten. ;) Tja - dumm gelaufen.
Stattdessen muß man auch in der Listbox entweder mit selektierten
Einträgen arbeiten oder andere Kniffe anwenden... (vielleicht
besprechen wir so etwas noch mal später - erinnern Sie mich
notfalls daran, wenn Interesse besteht).
Das Problem würde beim Container auch auftreten, wenn man die
Einträge anhand eines Index ("Platznummer") bearbeiten
müßte. Da DrDialog das aber gar nicht erst ermöglicht,
braucht man sich beim Arbeiten mit den IDs also auch keine Sorgen um
dieses Thema zu machen. Sorry für den kleinen Ausflug, aber
vielleicht verstehen Sie jetzt auch die Vorteile, die das
"ID-Verfahrens" mit sich bringt.
Gut - wir sind die Dinger also los, denn der Anwender kann die
Einträge auswählen und auf die Schaltfläche klicken.
Die Ereignisroutine des Click
-Ereignisses des pushbuttons
würde also im Ganzen wie folgt aussehen:
call cnt_test.getstem "auswahl"
do i = 1 to auswahl.0
call cnt_test.delete auswahl.i
end
Nicht schlecht, was dieses "Stückchen" Code so alles bewerkstelligt, hm?
In einem späteren Teil werden wir noch eingehend auf Menüs
und Kontextmenüs zu sprechen kommen. Gerade im Container sind
Kontextmenüs eine sehr gute Bearbeitungshilfe für den
Endanwender - überlegen Sie einmal, wie oft Sie selbst mit einem
Kontextmenü arbeiten... leider benötigt das Thema wieder eine
etwas größere "Einarbeitung", um mit dem Menüeditor von
DrDialog klar zu kommen und zu verstehen, wie man die Gegebenheiten am
besten umsetzt.
Daher möchte ich das Thema Container (vorerst) mit ein paar
Hinweisen abschließen, wie man auf Änderungsaktionen
des Anwenders reagiert - zum Beispiel die Änderung eines
Eintragsnamens oder eines Spaltenwerts (wenn der Container die
Detailansicht verwendet). Kommen wir erst einmal zum Ändern eines
Eintragsnamens - das macht man (Sie kennen das ja...) durch Anklicken
eines Eintrags mit gedrückter ALT-Taste. Der Titel wird dann zu
einem Eingabefeld. Beispiel gefällig?
So sieht das also aus. Das Container-Control bringt die notwendigen
Fähigkeiten zum Ändern von Einträgen auf diese Art gleich
mit - alles was Sie also noch tun müssen, ist, auf das Ändern
zu reagieren. Beachten Sie aber, daß das Container-Control das changed
-Ereignis
auch auslöst, wenn der Name (oder Spaltenwert in Detailansicht) nicht
geändert wurde. Es wird also lediglich signalisiert, daß die
Änderungsaktion beendet wurde.
Natürlich wollen wir bei einer Änderung durch den Anwender
wissen, wie der neue Name lautet - zum Beispiel, um entsprechende
Änderungen in unserer Adreßbuchdatei (wie im Beispiel aus der
letzten Ausgabe) durchzuführen. Wenn Sie einen Container verwenden,
um ein bestimmtes Verzeichnis auf der Festplatte abzubilden, würden
Sie wiederum jetzt den jeweiligen Dateinamen ändern. Um
herauszubekommen wie der neue Name lautet, verwenden wir zwei
Funktionen: eventdata
und item
.
Eventdata
ermöglicht uns festzustellen, welcher
Eintrag geändert wurde und auf was sich die Änderung bezog
(Eintragsname oder Spaltenwert in Detailansicht).item
schließlich gibt uns den neuen Wert
zurück, der am Eintrag geändert wurde.Da wir auf das Ändern eines Eintrags reagieren wollen, müssen
wir mit unserer Arbeit an der entsprechenden Stelle im Code beginnen -
also der changed
-Ereignisroutine für den Container.
Dort rufen wir als erstes einmal eventdata
auf.
Gemäß DrDialog's Online-Hilfe wissen wir, daß eventdata
im Falle des changed
-Ereignisses zwei Einträge einer
Stammvariablen bereitstellt. Geben wir keine eigene Stammvariable an,
verwendet DrDialog eine Stammvariable namens "eventdata". Da wir
notorische Faulpelze sind, die nicht andauernd neue Variablen definieren
wollen, nehmen wir diese Möglichkeit natürlich war. Sie
können dafür natürlich das Beispielprogramm aus der
letzten Ausgabe verwenden (das als separater .ZIP-Download zur
Verfügung steht). Als ersten Test bauen wir in der Ereignisroutine
ein...
call eventdata
say "---"
say "Geändert wurde Eintrag mit ID" eventdata.1
say "Daran wurde geändert:" eventdata.2
...lassen das Progrämmchen laufen und ändern testweise in der Symbolansicht mittels ALT-Klick den Namen eines Eintrags. Im Laufzeitmonitor erkennen Sie dann:
Bei Ihnen wird die ID bestimmt eine andere sein, da dieser Wert
jeweils beim Anlegen eines Eintrags vom System neu ermittelt wird.
Jedoch sollte der Wert VALUE in der letzten Zeile der Ausgabe
auch bei Ihnen ausgegeben werden, denn damit signalisiert DrDialog (bzw. eventdata
oder der Container), daß der Eintragsname geändert wurde.
Jetzt lassen wir das Programm nochmals laufen, wählen aber zuerst
in der Dropdownliste View: die Detailansicht aus. Dann
ändern Sie einen der Spaltenwerte eines Eintrags, beispielsweise:
Wenn Sie die Änderung abgeschlossen haben, wird im Laufzeitmonitor folgende Ausgabe (mehr oder weniger) erscheinen:
Aha! Abgesehen von der wahrscheinlich unterschiedlichen ID und ggf.
einer anderen Ziffer ist der frappierende Unterschied, daß nicht
mehr VALUE
ausgegeben wird, sondern eben eine Ziffer, Diese
Ziffer bezeichnet die Spalte, in der zum Eintrag eine Änderung
gemacht wurde. Wir wissen also nun, was für welchen
Eintrag geändert wurde. Jetzt kommen wir zur Funktion (oder
Methode) item
des Containers, mit der wir den eigentlichen
neuen Wert ermitteln. Die Syntax lautet
wert = [dialog.][control.]item([eintrag [,"VALUE" |
"BITMAP" | "DATA" | spalte] [,neuwert) )
Um es wieder einmal umzuformatieren, hier "meine" Notation der selben Syntax:
wert = [dialog.][control.]item([eintrag [,Infotyp]
[,neuwert) )
Die Funktion kann verwendet werden, um einem Eintrag einen neuen
Wert zuzuordnen und/oder den aktuellen Wert abzurufen. Das geschieht
dann so, daß neuwert
den neuen Wert enthält und wert
nach
dem Funktionsaufruf den vorherigen
Wert enthält. Wenn der
Parameter neuwert
nicht angegeben wird, findet keine
Änderung statt und wert
enthält entsprechend
den aktuellen Inhalt.
Wie Sie der Syntax entnehmen können, sind eigentlich alle
Parameter optional. Wenn Sie keinen einzigen Parameter angeben,
enthält wert
nach dem Funktionsaufruf die Anzahl aller
Einträge im Container. Könnte ja auch mal nützlich sein.
Kommen wir aber nun zur alles entscheidenden Frage, was denn eigentlich
jetzt an einem Eintrag geändert wurde und wie man das rausbekommt
mit item
. Dazu wird der Parameter Infotyp
benutzt:
Infotyp ...
|
...ändert oder ermittelt: |
"VALUE" | den Eintragsnamen |
"BITMAP" | das zugeordnete Symbol |
"DATA" | den einem Eintrag zugeordneten Datenwert |
spalte | den Wert, der sich in der durch "spalte" angegebenen Spalte eines Eintrags befindet |
Somit können wir also auf alle Daten zugreifen, die zu einem
Eintrag existieren. Beachten Sie, daß die item
-Funktion
noch mehr Möglichkeiten bietet, wir diese aber entweder bereits
besprochen haben oder Sie hier erst mal nichts zur Sache tun.
Natürlich brauchen wir die Qualifizierungsparameter [dialog.]
und [control.]
nicht anzugeben, wenn wir die item
-Funktion direkt in der Ereignisroutine changed
des
jeweiligen Containers verwenden. Um nun mit item
zu
ermitteln, wie der geänderte Wert aussieht müßten wir
theoretisch zwei Varianten berücksichtigen - einmal das Ändern
des Eintragsnamens, mit
inhalt = item(eintragsid, "VALUE")
und dann im Falle einer Änderung eines Spaltenwerts der Detailansicht mit
inhalt = item(eintragsid, spaltennummer)
Wobei eintragsid
jeweils den ersten Wert aus eventdata
enthält (nämlich die ID) und spaltennummer
im
zweiten Beispiel die Nummer der Spalte, also den zweiten Wert aus eventdata
.
Aber bevor wir uns jetzt eine entsprechende Abfrage der von eventdata
zurückgegebenen Werte ausdenken, lassen Sie uns einmal genau
schauen, was da von eventdata
im zweiten Wert
zurückgegeben wurde: Entweder steht da VALUE
drin
oder die Spaltennummer. Hmm... das sind ja interessante
Übereinstimmungen zwischen dem zweiten Wert aus eventdata
und dem Parameter infotyp
aus der item-Funktion. ;) Kurz
und knapp könnten wir also genauso gut schreiben:
inhalt = item(eventdata.1, eventdata.2)
Damit hätten wir auf jeden Fall den neuen Inhalt des
geänderten Werts in der Variable inhalt
, egal ob nun
ein Eintragsname oder ein Spaltenwert geändert wurde.
Natürlich kommen wir nicht umhin, in unserer Programmlogik zwischen
beiden Fällen zu unterscheiden, aber das ist ja relativ leicht -
wir brauchen nur zu prüfen, ob eventdata.2
den Wert VALUE
enthält. In diesem Fall wurde dann der Eintragsname geändert,
in allen anderen Fällen der Inhalt der jeweils angegebenen Spalte.
Eine entsprechende komplette Ereignisbehandlungsroutine könnte
beispielsweise so aussehen:
call eventdata
say "-------------------"
wer = eventdata.1
was = eventdata.2
if was = "VALUE" then
say "Eintrag mit ID" wer "heißt jetzt" item(wer, was)
else
do
call getstem "titel", 0
say "Für" item(wer, "VALUE") "wurde" titel.was "geändert in" item(wer, was)
end
Bitte beachten Sie, daß Ihr Browser eventuell einen Zeilenumbruch macht - beide say-Anweisungen sind auf einer einzigen Zeile zu schreiben!
Ein paar Erläuterungen zur obigen Routine:
eventdata
auf, ohne eine eigene
Stammvariable anzugeben. Wir bekommen also eine Stammvariable namens eventdata
.eventdata.1
in
der Variablen wer
. Das machen wir nur aus Gründen der
besseren Lesbarkeit und damit es zu Zeile 4 paßt...eventdata.2
in
der Variablen was
. Das machen wir, weil wir später
mit diesem Wert einen Verweis in die Stammvariable titel
vornehmen. Beachten Sie dazu die Anmerkungen im folgenden Absatz.was
identisch ist mit "VALUE".was
nicht "VALUE"
enthält)DO...END
-Konstruktion
benötigt wird.titel
ab.
Begründung zu Zeile 4:
Im Fall einer Änderung eines Spaltenwerts wollen wir eine Meldung ausgeben, die den Titel der Spalte enthält. Die Spaltenüberschriften ermitteln wir in Zeile 9 mittels der Funktion
getstem
, die uns alle Spaltentitel in einer Stammvariablen ablegt. Um nun den Titel einer bestimmten Spalte zu ermitteln, brauchen wir nur noch das entsprechende Element der Stammvariablen auszuwerten (Element 1 = erste Spalte usw). So weit so gut. Rein theoretisch könnten wir alsotitel.eventdata.2
verwenden, um direkt auf die betreffende Spalte zugreifen... geht aber in dieser Form nicht, denn:REXX geht davon aus, daß die Notation einer Stammvariable aus den Teilen stamm . eintrag besteht, wobei der letzte "Punkt" in der Bezeichnung maßgebend ist.
Das heißt, REXX würde nun versuchen, den
zweiten
Eintrag einer Stammvariable mit dem Namen"titel.eventdata"
zu verwenden. Aber eine Stammvariable dieses Namens gibt es nicht. Wir müssen REXX also irgendwie klar machen, daß der Wert voneventdata.2
das ist, was "hinter dem Punkt" stehen soll. Es gibt (mindestens) zwei Möglichkeiten, dies zu erreichen, aber ich verwende hier die "anschaulichere":
- Wir legen also den Inhalt von
eventdata.2
in einer Variablen ab (Zeile 4:was
).- Diese Variable verwenden wir dann als das, "was hinter dem Punkt steht" (Zeile 10: "
titel.was
").
Uff. Wenn Sie noch nicht so fit mit den Stammvariablen sind, ist das
auf den ersten Blick etwas verwirrend - übrigens wie fast alle
Beispiele und Hilfen zum Thema "Stammvariable" die ich kenne - am besten
lernt man diesen Kram, indem man herumspielt. Sie werden überrascht
sein, wie oft Sie sich "Aha", "Hoppla" oder "Hä?" sagen hören
werden... ;)
Man kann zwar auch ohne Stammvariablen leben, aber... eigentlich
führt kein Weg um Sie herum, wenn Sie komplexere Aufgaben in REXX
lösen wollen. Ich werde den Stammvariablen auf jeden Fall noch
einen Beitrag widmen. Wenn Sie den obigen Kram nicht verstanden haben,
nehmen Sie ihn einfach erst mal so hin, um die Eigenschaften des
Containers verfolgen zu können.
In Abhängigkeit davon, ob Sie nun in der Detailansicht eine
Spalte ändern oder in der Symbolansicht den Namen eines Eintrags
ändern, gibt diese Ereignisroutine entsprechende Meldungen im
Laufzeitmonitor aus. Wenn wir in unserer Beispielanwendung in der
Symbolansicht (view: Bitmap
) den Eintrag "Paul" in "Harry"
ändern würden, dann käme heraus:
Und wenn Sie die Telefonnummer von Peter (in der Detailansicht) in "12345678" ändern, käme raus...:
Tja. Das wär's für heute - viel Spaß beim Nachprogrammieren und/oder Herumspielen. Ich hoffe, es ist mir halbwegs gelungen, Ihnen einen Anhaltspunkt für das Arbeiten mit Containern gegeben zu haben. Falls Sie Fragen zu den Themen dieser Ausgabe haben (oder auch irgendeiner anderen), mailen Sie mich ruhig zu! Nächsten Monat - glaube ich - werden wir mit ein paar grundlegenden REXX-Befehlen, Funktionen und Grundlagen weitermachen. Die können Sie dann sowohl in DrDialog als auch in "puren" REXX-Prozeduren verwenden!
Bis nächsten Monat!
Daten und Quellen:
|
[Artikelverzeichnis]
editor@os2voice.org
[Vorherige Seite] [Inhaltsverzeichnis] [Nächste Seite]
VOICE Homepage: http://de.os2voice.org