Virtual OS/2 International Consumer Education
VOICE Homepage: http://de.os2voice.org
April 2003

[Inhaltsverzeichnis]
[Vorherige Seite] [Nächste Seite]
[Artikelverzeichnis]

editor@os2voice.org


DrDialog, oder wie ich lernte, REXX zu lieben - Teil 7

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...

Die 'style'-Einstellungen des Container-Controls

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...

container stylesKommen 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.

Group ist mir ehrlich gesagt schleierhaft. In Ermangelung von Hilfetexten kann ich nur bezweifeln, daß diese Einstellung unter DrDialog irgendeine "nutzbare" Bedeutung hat.
Tabstop besagt, daß der Anwender den Container mit der TAB-Taste anwählen kann. Ich glaube, daß hatte ich bislang noch nicht erwähnt, da ich davon ausgegangen bin, daß es ohnehin jeder weiß, aber sei's drum: Der Anwender kann alle Controls innerhalb eines Dialogs anwählen, indem er sich von einem Element zum nächsten mit der TAB-Taste bewegt, bzw. in umgekehrter Richtung mittels TAB-Taste und gerdückter Umschalttaste. Das gilt natürlich nur für die Controls, deren Style-Einstellung "Tabstop" aktiviert wurde. Das jeweils angewählte Element (Control) kann dann anstelle des "Klickens" auch mit der Leertaste betätigt werden (z.B. Pushbutton oder Checkbox), bzw. mittels Tastatur entsprechend anders verwendet werden (Eingabefelder, Listen, etc.).

Übrigens: Die TAB-Reihenfolge...

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.

Löschen von Einträgen

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.

Löschen... aber was?

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?

Anlegen, Löschen... und was ist mit Ändern?

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.

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 wertnach 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:

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 also  titel.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 von eventdata.2 das ist, was "hinter dem Punkt" stehen soll. Es gibt (mindestens) zwei Möglichkeiten, dies zu erreichen, aber ich verwende hier die "anschaulichere":

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.

Und was macht diese Routine jetzt?

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:

GuiObjectREXX Yahoo!-Gruppe: http://groups.yahoo.com/group/GuiObjectREXX/
Newsgruppe zur GUI-Programmierung mit REXX: news://news.consultron.ca/jakesplace.warp.visualrexx
Download von Hobbes: http://hobbes.nmsu.edu/cgi-bin/h-search?key=drdialog&pushbutton=Search
Code-Beispiel: http://de.os2voice.org/VNL/past_issues_DE/VNL0303H/cntsamp2_de.zip


[Artikelverzeichnis]
editor@os2voice.org
[Vorherige Seite] [Inhaltsverzeichnis] [Nächste Seite]
VOICE Homepage: http://de.os2voice.org