AG-Intra.net Arbeitsgemeinschaft
Intranet

Home
Was ist ein Intranet
Grundlagen
Netzwerke
Linux
Windows
Java
Sicherheit
Datenbanken
Projekte
Links
Impressum
Mitmachen ?
Diskussionsforum
Start:
28.12.2000
Letztes Update:
08.01.2001
4. Beispiel - Swing Anwendung . Java Tutorial
Copyright 2000 by Frank Gehde
<= vorherige Seite Inhaltsverzeichnis nächste Seite =>

Liebe Besucher, ein aktueller Hinweis in eigener Sache:
Es ist beabsichtigt, diese Seiten und die Domain im Januar/Februar 2004 auf einen anderen Server umzuziehen. Es ist leider nicht auszuschließen, daß es während des Umzugs zu technischen Problemen mit diesen Seiten kommen wird. Insbesondere im eMail-Bereich wird es vermutlich Probleme geben. Wenn Sie fragen haben oder mich sonstwie erreichen wollen empfehle ich an rebel@snafu.de zu posten.
Nachdem der Umzug abgeschlossen ist, wird es allerdings auch inhaltliche Änderungen während des ersten Halbjahrs 2004 geben. Keine Angst. Es werden keine Inhalte verlorengehen, aber die Struktur der Seiten wird komplett geändert. Diese Seite hat eben eine andere Entwicklung genommen seit 2000, als das Projekt gestartet wurde ;-) Ich werde mich bemühen, daß bei ihnen vorhandene alte Bookmarks wenigstens zu einem Verweis auf die Neustruktur führen, und die gesuchten Inhalte für sie trotzdem leicht und schnell auffindbar sein werden.
Die eigentlich zu dieser Seite gehörenden Domains ag-intra.com, ag-intra.org und ag-intra.de werden von mir geschlossen bzw. gelöscht und unregistriert.

4.1. Überblick
Ich hoffe, ich verspreche jetzt nicht zuviel, wenn ich behaupte, es geht jetzt ans Eingemachte. Erstens wagen wir uns an Swing heran. Zweitens werden wir diesmal nicht mit einem ganz Einfachst-Programm arbeiten, sondern einer halbwegs sinnvollen Anwendung. Und das bedeutet auch eine ganze Menge Code. Wenn das Beispiel also etwas länger ist, dann nicht abschrecken lassen, sondern wirklich etwas lernen. Schließlich werden wir zum Schluß eine neue Komponente erstellt haben, die man in anderen Programmen recht gut wiederverwenden kann.

Das Programm, welches wir schreiben, jongliert mit Datumsangaben. Jetzt kommt mein Eingeständnis: Ich bin mir nicht sicher, ob ich das Umgehen mit "Datum's" in Java richtig verstanden habe. Vielleicht mache ich deswegen im Programm einiges komplizierter als man es machen könnte. Aber funktionieren tut es trotzdem *g*. Bei dem folgenden Beispiel tauchen die Swing-spezifischen Erklärungen erst später auf, aber dafür lernen Sie zum Anfang mal etwas zum Umgang mit einem Datum in Java kennen. Und ich verspreche, auch bei Swing kommen Sie nicht zu kurz.

Genug des Vorgeplänkels. Erstmal nähere Angaben zum Programm. Es handelt sich um ein kleines Tool, welches zu einem eingegebenen Datum das Alter und das Datum berechnet, an dem das eingegebene Datum 80 Tage alt wird. Deswegen heißt das Programm eighty days (Edays). Außerdem zeigt das Programm das heutige Datum an, und das Datum, welches vor 80 Tagen vorlag. Sinn und Zweck ist es hier zum Beispiel festzustellen, ob ein Verfallsdatum bereits erreicht ist, wann es erreicht werden würde oder wird, oder wann das letzte gültige Verfallsdatum vorgelegen hat. In diesem Fall handelt es sich um hart kodierte 80 Tage (übrigens eine Verbesserungsmöglichkeit). Eine weitere Einschränkung besteht darin, daß keine Daten aus der Zukunft berechnet werden können (das hat nichts mit einem Y2K Problem zu tun, sondern mit der Implementation von DateDifferenceInDays wie Sie später sehen werden.).

Ich habe das Programm innerhalb des letzten Monats programmiert, deswegen hat es schon die Versionsnummer 1.2 mit der wir hier arbeiten. Es besteht aus insgesamt 4 Klassen:

  • Edays.java
  • JDateField.java
  • DateFieldDocument.java
  • DateDifferenceInDays.java
Edays ist dabei das Hauptprogramm. Dort wird die grafische Oberfläche mit Swing erstellt und die Ereignisbehandlung wird durchgeführt.
JDateField ist unsere eigene Erweiterung der Swing-Komponente JTextField. Es handelt sich dabei um ein Texteingabefeld, welches aber so geändert wurde, daß nur Datumseingaben möglich sind. Außerdem wurden einige Datums-spezifische Zugriffsmethoden und zwei Klassenmethoden implementiert. Diese Komponente ist nicht internationalisiert und meines Wissens mittelprächtig robust. Trotzdem bietet sie sich zur Weiterverwendung in anderen Projekten an.
DateFieldDocument ist eine Klasse die zum JDateField gehört. Sie ändert das Swing eigene Dokumentmodell des JTextField so ab, daß es speziell für Datumsangaben zu nutzen ist.
DateDifferenceInDays erledigt, wie der Name schon sagt, nach Übergabe von zwei Daten die Berechnung des Alters in Tagen.

Soviel nur zur ersten Orientierung, weil das Ganze ja wie gesagt etwas umfangreicher ist. Wenn man ein Programm, wie das Beschriebene erstellen will, fragt man sich wo man anfangen soll. Nun, ich habe mich entschieden, mit der Klasse DateDifferenceInDays zu beginnen, weil sie ja die eigentliche Arbeit übernehmen sollte.

4.2. Datumsbehandlung
Immer wieder hat man es beim Programmieren mit dem Manipulieren von Datumsangaben zu tun. Bei unserem Programm Edays trifft dies natürlich besonders zu. Eine der vier Klassen von Edays hat nichts mit Swing zu tun, aber sehr wohl mit Datum's (ich schreibe hier übrigens oft "Datum's", weil man sonst den Plural von Datum auch mit der Allerweltsbezeichnung "Daten" im allgemeinen verwechseln könnte). Die ersten Informationen zur Datum's-Verarbeitung finden Sie bei der folgend beschriebenen KlasseDateDifferenceInDays, aber auch bei den restlichen Klassen taucht immer wieder eine Neuigkeit auf.

4.2.1. Klasse 1: DateDifferenceInDays.java
Die Aufgabe für diese Klasse war eigentlich klar. Sie sollte zwei Attribute für Daten haben, und eines für das Alter zwischen den Daten. Der Klasse sollen zwei Daten übergeben werden können, und mit einer get-Methode sollte das Alter abgefragt werden können. Die Daten können über einen entsprechenden Konstruktor übergeben werden, oder spezielle set-Methoden.
Das hört sich erst einfach an, aber es stellt sich die Frage, wie sollen Daten übergeben werden? Als String? Als int-Werte? Oder gibt es etwa einen speziellen Datumstypen? Letzteres erwarte ich eigentlich von jeder Programmiersprache, daß es einen Objekttypen für ein Datum gibt, und eine riesengroße Bibliothek mit Funktionen, um dieses Datum manipulieren zu können.
Wie ich es oben schon erwähnte, vermute ich, daß ich irgendwas mit den Daten in Java nicht verstanden habe. Ich habe jedenfalls keine große Funktionsbibliothek gefunden. Denn die Funktion Differenz zwischen zwei gegebenen Daten in Tagen erwarte ich einfach von einer derartigen Biblithek. Was ich gefunden habe, ist die Klasse Date, die Klasse Calendar und die Klasse GregorianCalendar. Alle befinden sich im Paket java.util.*. Die beiden Calendar-Objekte sind schon ganz hilfreich. Deswegen habe ich sie auch oft verwendet. Die Klasse Date hingegen ist eigentlich Müll. An jeder zweiten Stelle in der Java-API-Dokumentation zu Date steht, daß diese oder jene Methode deprecated ist, und durch Methoden der Klasse Calendar ersetzt wurde. Deprecated heißt, daß sich Sun etwas Neues ausgedacht hat, und Programmierer in Zukunft nur die neue Variante nutzen sollen. Die alten Methoden werden aus Kompatibilitätsgründen noch mitgeschleppt, aber der aktuelle Compiler meckert schon, wenn man sie trotzdem benutzt. Das Dumme ist, daß ich bei den Methoden vonCalender(und auch GregorianCalendar) konkret diejenigen Methoden aus Date, die ich hätte gut gebrachen können und die deprecatedsind, nicht gefunden habe!
Im Ergebnis verwenden wir nun alle drei Klassen in unserem Programm. Und dabei findet sich keine deprecated-Methode aus Date. Dadurch werden im Endeffekt einige Zeilen nur länger :)
Soviel zu den verwendeten Klassen in DateDifferenceInDays.java. Und hier erstmal wie gewohnt das Listing:

/**
 *  Klasse, die die Differenz zwischen zwei Daten (Calendar) in Tagen berechnet
 */

import java.util.*;

public class DateDifferenceInDays {    //Klassensignatur
 // ******** Attribute Anfang
 private Calendar earlyDate = new GregorianCalendar(); 
 private Calendar laterDate = new GregorianCalendar();
 private int difference;
 // ******** Attribute Ende

 // ******** Konstruktoren Anfang
 // Konstruktor 1
 public DateDifferenceInDays() { 
  earlyDate.setTime(new Date());    // setzt beide Daten auf HEUTE
  laterDate.setTime(new Date());
 }

 // Konstruktor 2
 public DateDifferenceInDays(int earlyDay, int earlyMonth, int earlyYear) { 
  earlyDate.set(earlyYear, earlyMonth, earlyDay); // early auf Parameter
  laterDate.setTime(new Date());                  // late auf HEUTE
  if(laterDate.before(earlyDate)) toggeleDates(); // Daten vertauschen
 }

 // Konstruktor 3
 public DateDifferenceInDays(int earlyDay, int earlyMonth, int earlyYear,
        int laterDay, int laterMonth, int laterYear) {
  earlyDate.set(earlyYear, earlyMonth, earlyDay); // early auf Parameter
  laterDate.set(laterYear, laterMonth, laterDay); // late auf Parameter
  if(laterDate.before(earlyDate)) toggeleDates(); // Daten vertauschen
 } 
 // ******** Konstruktoren Ende

 // ******** Methoden Anfang
 // Early Datum setzen
 public void setEarly(int earlyDay, int earlyMonth, int earlyYear) { 
  earlyDate.set(earlyYear, earlyMonth, earlyDay);
  if(laterDate.before(earlyDate)) toggeleDates(); // Daten vertauschen
 }
 // Later Datum setzen
 public void setLater(int laterDay, int laterMonth, int laterYear) { 
  laterDate.set(laterYear, laterMonth, laterDay);
  if(laterDate.before(earlyDate)) toggeleDates(); //Daten vertauschen
 }
 public int getEarlyDay() {     // Tag von Early zurückgeben
  return earlyDate.get(Calendar.DATE);
 }
 public int getEarlyMonth() {   // Monat von Early zurückgeben
  return earlyDate.get(Calendar.MONTH);
 }
 public int getEarlyYear() {    // Jahr von Early zurückgeben
  return earlyDate.get(Calendar.YEAR);
 }

 public int getLaterDay() {     // Tag von Later zurückgeben
  return laterDate.get(Calendar.DATE);
 }
 public int getLaterMonth() {   // Monat von Later zurückgeben
  return laterDate.get(Calendar.MONTH);
 }
 public int getLaterYear() {    // Jahr von Later zurückgeben
  return laterDate.get(Calendar.YEAR);
 }

 public int getDifference() {   // Die Methode zum Abfragen der Differenz
  this.compute();
  return difference; 
 } 

 private void compute() {                         // Differenz in Tagen 
                                                  // berechnen
  difference = 0; 

  while(earlyDate.before(laterDate)) {
   earlyDate.add(Calendar.DATE, 1);
   difference++;
  }
 }

 private void toggeleDates() {
  Calendar tempDate = new GregorianCalendar();    // Vertauscht Early und
  tempDate.set(earlyDate.get(Calendar.YEAR),      // Later Date
        earlyDate.get(Calendar.MONTH),
        earlyDate.get(Calendar.DATE));
  earlyDate.set(laterDate.get(Calendar.YEAR),
        laterDate.get(Calendar.MONTH),
        laterDate.get(Calendar.DATE));
  laterDate.set(tempDate.get(Calendar.YEAR),
        tempDate.get(Calendar.MONTH),
        tempDate.get(Calendar.DATE));
 } // ******** Methoden Ende
}

So, das ist schon mal ein gutes Stück lang. Sehen wir uns das an, und erklären Eigenheiten. Die import-Anweisung ist klar. Die genannten Datums-Klassen befinden sich ja in java.util.*
Dann sehen wir drei Attribute:

private Calendar earlyDate = new GregorianCalendar(); 
private Calendar laterDate = new GregorianCalendar();
private int difference;

Die beiden fürs Datum, sind beides Objekte der Klasse GregorianCalendar. Diese bietet sich ja zum Umgang mit Daten an. Hier sogar besonders. Es ist hinzuzufügen, daß die Klasse Calendar mehr oder weniger nur eine abstrakte Klassendefinition ist, und daher nicht direkt verwendet werden kann. Wegen der Vielzahl von Klassenmethoden wird aber dennoch oft auf Calendar zugegriffen. GregorianCalenderist eine konkrete Implementierung von Calendar. Diese können wir verwenden und Instanzen bilden. In diesem Beispiel habe ich meine ObjekteearlyDateund laterDate wie üblich erzeugt. Dazu ist hinzuzufügen, daß das nicht empfohlen ist. Derartige Objekte sollen laut API-Dokumentation lieber mit der Methode getInstance() erzeugt werden. Nun, so wie ich es gemacht habe, gibt es jedenfalls keinen Fehler, und das Programm arbeitet fehlerfrei.
Das dritte Attribut ist der Wert für die Differenz in Tagen, den wir ja hier berechnen wollen.
Alle Attribute sind private, weswegen wir Zugriffsmethoden benötigen werden.

Es folgen drei Konstruktoren. Für unser konkretes Programm bräuchten wir eigentlich nur einen, aber da man immer etwas vorausschauend plant, versucht man eine Klasse flexibel zu machen, damit man sie später vielleicht in anderen Programmen wiederverwenden kann. Wir haben zwei "Inputwerte" (Datum's), mit denen die Klasse arbeitet. Es bietet sich also an ohne Parameter, mit einem oder eben mit zwei Parametern zu initialisieren. Zur genaueren Analyse der Konstruktoren betrachten wir nur einmal den Konstruktor 2, da die beiden anderen dann selbsterklärend sind:

 // Konstruktor 2
 public DateDifferenceInDays(int earlyDay, int earlyMonth, int earlyYear) { 
  earlyDate.set(earlyYear, earlyMonth, earlyDay); // early auf Parameter
  laterDate.setTime(new Date());                  // late auf HEUTE
  if(laterDate.before(earlyDate)) toggeleDates(); // Daten vertauschen
 }

Man sieht, daß ich mich entschieden habe, die Daten als einzelne int-Werte zu übergeben. Das hat mit den Eigenschaften von Calendar-Objekten zu tun. Calender-Objekte haben die Eigenschaft, ein Datum (und auch eine Zeit) in einzelnen Feldern abzulegen, wobei alle Bestandteile als int-Wert abgelegt werden. Wenn die Klasse, die meine Klasse verwendet auch mit einem Calendar-Objekt arbeitet, kann sie die int-Werte ganz einfach mit den Zugriffsmethoden von Calendar extrahieren, und als Parameter an meine Konstruktoren übergeben (das sehen wir später noch).
In dieser Klasse habe ich mich entschieden, daß wenn genau drei Interger-Werte als Parameter übergeben werden, diese als earlyDateinterpretiert werden. Um einen geordneten Zustand meines Objektes zu erreichen, werde ich Daten die nicht übergeben wurden jeweils auf das heutige Datum setzen. earlyDate ist ja ein Calendar-Objekt. Die KlasseCalendar stellt die Methode set() bereit, der drei int-Werte in der Reihenfolge Jahr, Monat, Tag übergeben werden können, um ein Datum einzustellen (beachten Sie die im Vergleich zu deutschen Verhältnissen umgekehrte Reihenfolge). Das machen wir auch genau so in der ersten Befehlszeile. Ferner existiert auch die Methode setTime(), die als Parameter ein Date-Objekt erwartet. Das verwenden wir in der zweiten Zeile für das laterDate. Wie ich schon sagte, initialisieren neue Date-Objekte in Java 2 freundlicherweise auf das heutige Datum. Wir schaffen also im Parameter ein neues Date-Objekt, und unser Calendar-Objekt initialisiert so auf jeden Fall auf heute.
Die dritte Befehlszeile ist aufgrund unserer Differenz-Berechnung notwendig. Diese hat die Eigenheit nur Differenzen nach "vorne" auf der Zeitleiste berechnen zu können. Es ist bei meiner Implementierung also notwendig, daß das earlyDate wirklich vor dem laterDate liegt. Die Klasse Calendar bringt auch hier wieder eine praktische Funktion mit, die feststellt, ob ein Datum vor dem anderen liegt. Es handelt sich um die Methode before(), der als Parameter ein anderes Calendar-Objekt übergeben wird. Ist das übergebene Datum dann wirklich vorher, ist der Ausdruck true, und kann somit prima in einer if-Abfrage verwendet werden. Sollten die Daten "falschrum" übergeben worden sein, so wird eine private Methode toggleDates() aufgerufen, die wir etwas weiter unten in unserem Programm geschrieben haben, und dort erklären.
Mit den genannten Konstruktoren haben wir also immer einen wohlgeordneten Zustand. Daten werden entweder spezifiziert, oder auf heute gesetzt. Derjenige der unsere Klasse verwendet muß nur wissen, daß Daten ggf. vertauscht werden können. Weiß er das nicht, können in seinem Programm Logik-Fehler auftreten, wenn er nicht damit rechnet, daß Daten vertauscht werden. Alternativ könnte unsere Klasse auch einen Fehler auswerfen, wenn Daten "falschrum" eingegeben werden. Soviel zu den Konstruktoren. Diese dürften damit klar sein.
Kommen wir nun zu den Zugriffsmethoden. Da unsere Attribute ja private sind, brauchen wir Zugriffsmethoden zum Arebiten mit der Klasse während der Laufzeit des Programms. Zunächst haben wir zwei set-Methoden, jeweils für das earlyDate und das laterDate. Diese erwarten als Parameter immer drei int-Werte für Tag, Monat und Jahr.

// Early Datum setzen
 public void setEarly(int earlyDay, int earlyMonth, int earlyYear) { 
  earlyDate.set(earlyYear, earlyMonth, earlyDay);
  if(laterDate.before(earlyDate)) toggeleDates(); // Daten vertauschen
 }

Die dort programmierte Funktionalität dürfte klar sein, da sie genauso funktioniert, wie in den Konstruktoren. Jetzt folgen die get-Methoden. Hier habe ich einige mehr vorgesehen. Das liegt daran, daß ich noch nicht genau weiß, wie man mehrere Werte auf einmal als Rückgabewert einer Methode zurückgibt (obwohl ich weiß, daß das geht). Wir haben dadurch aber für jedes Feld (Tag, Monat und Jahr für jeweils beide Daten) eine eigene get-Methode. Beispielhaft sehen wir uns die erste Methode an:

 public int getEarlyDay() {     // Tag von Early zurückgeben
  return earlyDate.get(Calendar.DATE);
 }

Wie oben schon erwähnt, besitzt ein Calendar-Objekt Zugriffsmethoden auf die einzelnen Felder, in denen es Daten verwaltet. Der Calendar macht es etwas freundlicher als unsere Klasse, und hat Konstanten vordefiniert, die der get-Methode übergeben werden. So braucht Calendarkeine drei verscheidenen Zugriffsmethoden, sondern erkennt an der übergebenen Konstante, welches Feld gewünscht ist. Calendar.DATE steht dabei für den Tag. Für Monat und Jahr gibt es zum Beispiel auch die Konstanten Calendar.MONTH und Calendar.YEAR (weitere Konstanten sehen Sie bitte in der API-Dokumentation nach).
Auf diese Art und Weise kann unsere Klasse jedenfalls, die aktuell eingestellten Datenfelder einzeln zurückgeben. Und wie man im Listing oben sieht, sind es genau sechs get-Methoden, die je nach Name, den entsprechenden Wert zurückliefern.
Interessant ist jetzt unsere letzte öffentliche get-Methode:

 public int getDifference() {   //Die Methode zum Abfragen der Differenz
   this.compute();
   return difference; 
 }

Sie ist fast das eigentliche Arbeitspferd. Sie läßt die Differenz zwischen den aktuell gesetzten early- und later-Daten berechnen, und gibt diese dann zurück. Dazu verwendet sie eine Methode, die ich als privatedeklariert habe. Sie folgt direkt im Listing:

 private void compute() {                // Differenz in Tagen 
  difference = 0;                       // berechnen

  while(earlyDate.before(laterDate)) {
   earlyDate.add(Calendar.DATE, 1);
   difference++;
  }
 }

Private heißt, daß diese Methode nicht von außen, also einer anderen Klasse oder einem Programm aufgerufen werden kann. Sie dient nur internen Zwecken der Klasse DateDifferenceInDays. Zuerst wird die Difference auf 0 gesetzt. Dann wird die while Schleife ausgeführt, und zwar solange, wie das Kriterium in Klammern true ist. Und jetzt komm ich zu der Funktionalität, die zum Bespiel das Vertauschen der Daten notwendig macht. Bei der Suche nach einer Möglichkeit das Datum zu erhöhen oder zu erniedrigen, bin ich im Internet auf eine Beschreibung der roll-Methode von Calendar gestoßen. Mit roll() kann man ein Feld in einem Calendar-Objekt beliebig erhöhen. Bei Versuchen mußte ich aber feststellen, daß im Falle eines Überlaufs, also zB. beim Hochzählen vom 31.01.2000 nicht zum 01.02.2000 "gerollt" wird, sondern wieder auf den 01.01.2000. Dieser Überlauf ist also nicht berücksichtigt. Beim Durchsehen der API-Dokumentation entdeckte ich dann aber die Methode add(). Und siehe da, bei dieser Methode wird der Überlauf berücksichtigt, und der Monat entsprechend erhöht, wenn das notwendig ist. In unserem Beispiel addieren wir also immer einen Tag zum Datum hinzu, und setzen anschließend auch den Wert von Difference um einen hoch. Und dies solange, wie das earlyDate noch vor dem laterDate liegt.
Man kann übrigens auch negative Werte hinzuaddieren, und so daß Datum rückwärts laufen lassen. Dann ist aber das Hochzählen von difference anders zu handlen. Da diese Funktion für die Berechnung des Alters in Tagen jedenfalls so implementiert ist, wie sie es ist, ergibt sich daraus der Umstand mit dem Vertauschen der Daten. Man könnte in die gleich beschriebene Methode toggleDates() noch ein Flag einbauen, bzw. ein zusätzliches Attribut hinzufügen. Dann ließe sich von außen feststellen, ob die Differenze nach hinten besteht, oder nach vorne in die Zukunft. Dies ist ein Verbesserungsvorschlag. *g*
Die letzte Methode in unsere Klasse ist die eben genannte toggleDates(). Diese ist ebenfalls private, und wurde nur deswegen als eigene Methode implementiert, weil sie in meiner Klasse öfters benötigt wird:

private void toggeleDates() {
  Calendar tempDate = new GregorianCalendar();    // Vertauscht Early und
  tempDate.set(earlyDate.get(Calendar.YEAR),      // Later Date
        earlyDate.get(Calendar.MONTH),
        earlyDate.get(Calendar.DATE));
  earlyDate.set(laterDate.get(Calendar.YEAR),
        laterDate.get(Calendar.MONTH),
        laterDate.get(Calendar.DATE));
  laterDate.set(tempDate.get(Calendar.YEAR),
        tempDate.get(Calendar.MONTH),
        tempDate.get(Calendar.DATE));
 } 

Hier werden einfach die Daten von earlyDate und laterDate vertauscht. Man sieht hier übrigens schön, daß eine Zeile Sourcecode nicht immer auch genau eine Zeile im Editor belegen muß. Leerzeichen und Tabs werden einfach vom Compiler ignoriert. Als Markierung für das Ende einer Zeile wünscht der Compiler sich nur ein Semikolon, oder bei Blöcken die abschließende Klammer.
Wie die Methode funktioniert, erklärt sich wohl inzwischen von selbst. Unter Nutzung der get-Methode des Calendar-Objektes, sowie den Feldkonstanten, wird das earlyDate gesichert, dann dem earlyDatedas laterDate zugewiesen und die Sicherung schließlich demlaterDatezugewiesen.

4.2.2. DateDifferenceInDays testen
Wie wir sehen, hat die Klasse DateDifferenceInDays keine main-Methode, und ist somit auch kein ausführbares Programm. Wir können nun Instanzen von DateDifferenceInDays erzeugen, zwei Daten übergeben und mit getDifference() die Differenz zwischen den Daten abfragen. Hier ein kurzes Programm zum Testen der Klasse:

// Testprogramm fuer Klasse DateDifferenceInDays
public class Test1 { 
 public static void main(String[] args) {
  DateDifferenceInDays myDate = new DateDifferenceInDays();
  int tagInt = 24;                            // Datum festlegen,
  int monatInt = 12-1;                        // hier: Weihnachten 2000
  int jahrInt = 2000;

  myDate.setEarly(tagInt, monatInt, jahrInt); // EarlyDate setzen
  myDate.setLater(31, 12-1, 2000);            // LaterDate setzen (Sylvester)

  System.out.println("Differenz : " + myDate.getDifference());
 } 
}

Hier schaffen wir uns eine Instanz der Klasse DateDifferenceInDays. Dann belegen wir mit Zugriffsmethoden die beiden Daten. Schließlich geben wir an der Konsole das Ergebnis der Abfrage getDifference() aus. Das Ergebnis heißt richtigerweise 7.
Wo liegen die Schwächen der Klasse? Nun, es wird zum Beispiel nicht geprüft, ob es sich um ein gültiges Datum handelt. Die Eingabe von 31.02.2000 wäre also möglich. Das Calendar-Objekt meckert dabei nicht, und würde zum Beispiel diese Eingabe dann als 02.03.2000 interpretieren.
Eine weitere Schwäche ist eben, daß es kein Feedback gibt, ob die Daten vertauscht worden sind. Daher muß das aufrufende Programm selbst dafür sorgen, daß Daten in der richtigen Reihenfolge angeliefert werden.
Eine weitere Besonderheit sehen wir am Testprogramm. Den Monaten wird vor der Verarbeitung immer 1 abgezogen. Dies liegt an der bisher noch nicht erwähnten Eigenheit der Calendar-Objekte, daß der Startwert für Monate die 0 ist, und nicht die 1. Die Nummer des Monats Januar ist also die 0. Bei Tagen tritt dieses Verhalten nicht auf, und ich kann es mir auch nicht erklären. Wie dem auch sei, aufrufende Programme müßen also immer selbst dafür sorgen, daß der Monat entsprechend -1 gehandhabt wird, und bei der Ausgabe von Daten muß man eben wieder einen dazu addieren.

So, die restlichen drei Klassen von Edays sind jetzt endlich hauptsächlich Swing-relevant. Da wir mit diesen aber schon ganz schön loslegen, gibt es erstmal ein kleines Swing-Progrämmchen zur Einstimmung. Wir nehmen einfach die AWT-Programme Nummero drei und vier und bauen uns die Swing-Variante.

4.3. Abstecher: Ein kleines Swing-Programm
Edays verwendet eine ganze Menge von Swing-spezifischen Eigenheiten. Außerdem ist das Listing der folgenden drei Edays-Klassen relativ lang. Damit Ihnen die Übersicht nicht verloren geht, und Sie erstmal einige grundsätzliche Eigenschaften von Swing kennenlernen können, unterbrechen wir Edays hier mit einem kleinen vorbereitenden Swing Programm. Vorher gibt es ein klein wenig Theorie, die aber nicht schaden kann.
.
4.3.1. Unterschiede AWT und Swing
Warum bringt Java eigentlich zwei Varianten zur Programmierung von grafischen Oberflächen mit? Nun, zunächst wurde Java bei Sun als hausinterne Alternative zu C++ bzw. zu Forschungszwecken erstellt. Hauptsächlich sah man damals das Einsatzgebiet auch bei kleinen Devices wie etwa PDA's. Gründe für das Bereitstellen einer grafischen Oberfläche gab es eigentlich nicht. Mit dem Boom des WWW, wurde auf einmal der Ruf nach Interaktivität auf Web-Seiten laut, und Sun (und auch Netscape) erkannten, daß Java dafür eigentlich prädestiniert ist, weil es ja ebenso auf Plattformunabhängigkeit ausgelegt ist, wie das WWW selbst. Jetzt war es war es nur noch notwendig, eine Schnittstelle für grafische Oberflächen bereitzustellen, die ebenfalls plattformunabhängig ist und zwar pronto. Nach nur 6 Wochen hatten die Sun-Entwickler dieses Kunststück vollbracht, und die erste Version von AWT war fertig. Wie macht man das so schnell? Die Ingenieure von Sun verwendeten das native Peer-Konzept. Plattformunabhängig heißt ja nicht, daß es tausende von Plattformen gibt. Zusammenfassend kann man sagen, daß man sich auf Windows, Unix (Motif) und den Mac konzentrieren konnte, um den größten Teil der installierten Plattformen abzudecken. Nun sah man sich an, was die angesprochenen Plattformen an GUI-Komponenten selbst mitbringen. Dazu gehören eben Fenster, Buttons, Checkboxen, Listen etc. Dann sucht man sich den kleinsten gemeinsamen Nenner aus, den alle Systeme mitbringen. Dieser Nenner wurde zum AWT. Man hat für die verschiedenen Plattformen dann eine ganz dünne Schicht Java über die eigentlich vom jeweiligen Betriebssystem zur Verfügung gestellten Komponenten gestellt. Wenn also mit dem AWT ein Button programmiert wird, dann wird in Wahrheit z.B. unter Windows, ein echter Windows-Button dargestellt. Dieser ist das native Peer des AWT Button. Eine AWT Anwendung, die unter Windows gestartet wird, sieht daher wie ein normales Windows-Programm aus. Wird genau die gleiche Anwendung unter Linux gestartet, sieht die Anwendung eben wie eine Motif (Linux) Anwendung aus.
Auf diese Art und Weise war es möglich das AWT so schnell zu entwickeln. Man erkaufte sich aber auch alle Nachteile, zu denen eben gehörte, daß die Komponenten nur auf dem kleinsten gemeinsamen Nenner basieren, und vor allem war das Modell zur Ereignisbehandlung alles andere als wirklich objektorientiert. Weil die Entwickler aus diesen Gründen zur GUI-Programmierung immer mehr Pakete von Drittanbietern verwendet haben, und die Gefahr bestand, daß es verschiedene Arten von Java-Programmen gibt (läuft auf Plattform X aber nicht auf Y), reagierte Sun. Zunächst wurde das Paket AWT verbessert. Insbesondere gab es seit Java 1.1 eine wesentlich verbesserte Ereignisbehandlung. Außerdem begannen die Arbeiten an dem Project Swing. Mit Swing sollte es die Möglichkeit der Oberflächenprogrammierung geben, die vollkommen vom darunterliegenden Betriebssystem unabhängig ist. Das heißt, daß im wesentlichen nicht mehr die nativen Peers, also die vom Betriebssystem vorgegebenen Komponenten verwendet werden, sondern Komponenten die von Java selbst gezeichnet werden, die ComponentUI's (die auch dafür zuständig sind, daß man das Erscheinungsbild bei Swing Komponenten ändern kann). Diese Komponenten werden auch als Leightweight-Komponenten bezeichnet. Swing selbst ist zu 100% in Java geschrieben, was die Portierung erleichtert. Swing existiert auch nicht komplett neben dem AWT, sondern nutzt die Infrastruktur von AWT, wozu insbesondere die Container gehören, und das Ereignisbehandlungsmodell. Swing basiert auch noch auf einem weiteren ganz wichtigen objektorientierten Konzept, welches es zB. bei Smalltalk abgeschaut hat, dem sogenannten Modell-View-Controller Konzept (MVC). Hierbei existiert ein Datenmodell, welches die eigentlichen Daten der Komponente verwaltet und Zugriffsmethoden bereitstellt, dann gibt es eine oder mehrere Ansichten (Views), die nur für die Darstellung (also das Zeichnen) der Daten zuständig sind, sowie einen Controller, der dafür sorgt, daß Modell und View immer synchron laufen, also die Ereignisse behandeln. Ändert sich das Modell, wird durch den Controller der View benachrichtigt, damit er sich automatisch aktualisiert. Ändert sich durch Interaktion des Nutzers der View, wird das Modell informiert, damit die Daten aktualisiert werden. Von dieser Eigenschaft machen wir bei Edays noch heftig Gebrauch.
Etwas erleichternd kommt hinzu, daß Sun versucht hat, AWT und Swing einigermaßen kompatibel zu halten. Angeblich sollten nur geringfügige Änderungen an bestehenden AWT-Anwendungen nötig sein, damit diese von Swing Gebrauch machen können.
Dieser Abriß über Swing und AWT ist wirklich nur kurz gehalten. Eine relativ vollständige Erklärung und Dokumentation finden Sie in zwei Bänden zu je 1400 Seiten, von denen einer in der Einleitung genannt wurde.
Ich glaube für den Anfang reicht soviel Theorie erst einmal aus. Damit es nicht zu trocken wird, und Sie auch mal sehen, daß Swing trotzdem einfach anzuwenden ist, schreiben wir nun ein kleines Beispiel-Swingprogramm.

4.3.2. WinX als Swing-Programm
Wenn Sie sich noch an unsere AWT-Beispiele erinnern, kommt Ihnen die Swing-Anwendung jetzt bekannt vor. Wir werden eine Mischung aus WinVier und WinDrei schreiben, nur diesmal eben mit Swing. Dabei werden wir die Varianten 1 und 2 der Ereignisbehandlung gleichzeitig verwenden. Das Schließensymbol des Fensters wird also durch Überschreiben der processXxxEvent-Methode behandelt, und das Drücken des Button durch Registrieren eines Listeners mit einer anonymen Klasse.
Zunächst einmal das Listing von SwingEins.java:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class SwingEins extends JFrame {

 public SwingEins() {                                  // *** Konstruktor
  super("SwingEins");                                  // Titel

  JLabel myHeader = new JLabel("Hier Text eingeben:"); // Labelkomponente
  JTextField eingabeFeld = new JTextField("hier", 20); // Textfeld-Komponente
  JButton klicker = new JButton("Beenden");            // Button-Komponente

  ActionListener myListener = new ActionListener() {   // Fuer den Button
   public void actionPerformed(ActionEvent e) {        // in anonymer Klasse
    dispose();                                         // (siehe auch WinV)
    System.exit(0);
   }
  };

  klicker.addActionListener(myListener);            // Listener registrieren

  JPanel myContainer = new JPanel();                // spezieller Container
  myContainer.setLayout(new BorderLayout(5,5));     // Layout einsetzen

  myContainer.add(myHeader, BorderLayout.NORTH);    // Die Komponenten dem
  myContainer.add(eingabeFeld, BorderLayout.CENTER);// Container hinzufügen
  myContainer.add(klicker, BorderLayout.SOUTH);

  getContentPane().add(myContainer);                // Container dem Frame
                                                    // hinzufügen
  this.enableEvents(AWTEvent.WINDOW_EVENT_MASK);    // Ereignisse ermöglichen
 }                                                  // *** Ende Konstruktor

 protected void processWindowEvent(WindowEvent e) {    // Fensterereignisse
         if (e.getID()==WindowEvent.WINDOW_CLOSING) {  // behandeln
   dispose();                           // Ressourcen des Fensters freigeben
   System.exit(0);                      // Programm beenden
  }
 } // Ende Methode processWindowEvent()

 public static void main(String[] arg) {            // Hauptmethode
        SwingEins mySwingapp = new SwingEins();
        mySwingapp.pack();
        mySwingapp.show();
 }
}

Das sieht zwar auf den ersten Blick etwas länger aus als unser allererstes AWT-Programm, aber dafür sind wir ja auch schon etwas weiter und haben gleich zwei Varianten von Ereignisbehandlung bereits mit eingebaut. Nachdem Sie das Programm kompiliert haben, starten Sie es einmal. Sehen wir uns einfach einmal die optischen Unterschiede zwischen WinVier.java aus dem AWT-Kapitel und dem hier vorliegenden SwingEins.java an:
 
Grafik von WinVier.java Grafik von SwingEins.java
WinVier.java (AWT) SwingEins.java (Swing)

Auf den ersten Blick wird deutlich, daß das Swing-Programm abgesehen von der Titelleiste keine wirkliche Ähnlichkeit mit dem Windows-GUI hat. Der Button wird z.B. ganz anders gezeichnet als ein Windows-Button. Die hinzugefügten Komponenten sehen auch sofort homogener aus, da bereits alles in grau erscheint. Bei der AWT-Variante hätten wir uns darum noch selbst kümmern müßen. Das defaultmäßige Look & Feel von Swing, ist das Swing-eigene "Metal"-Erscheinungsbild (man könnten dies auch zur Laufzeit oder generell ändern). Die unterschiedliche Breite dürfte wohl auf der unterschiedlichen Interpretation des Begriffs "Spalte" beruhen, die dem Textfeld als Parameter mit auf den Weg gegeben wird (bei Swing ist eine Spalte immer so breit wie der Buchstabe "m").
Wenn wir uns jetzt den Quellcode ansehen, dann stellen wir fest, daß es kaum Unterschiede zur AWT-Variante gibt. Im wesentlichen ist eigentlich nur allen Komponenten, von Frame über Label, TextField, Buttonund Pane ein großes "J" vorangestellt. Wer also die Komponenten des AWT schon kennt, kennt sofort auch die Namen der Swing Komponenten. Die Ereignisbehandlungsroutinen funktionieren genauso wie bei unsere AWT-Anwendung. Wo liegen hier nun die konkreten Unterschiede? Zunächst in den import-Anweisungen.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

Vielleicht fragen Sie sich wieso auch das AWT-Paket importiert wird? Nun, wie ich schon sagte, verwendet Swing Teile der AWT-Infrastruktur. Unsere Ereignisbehandlungsroutinen haben wir ja unverändert aus dem AWT-Programm übernommen. Diese verwenden eben auch Konstanten die im AWT-Paket definiert sind. Ohne AWT kommen wir also nicht aus. Die ganzen Listener-Geschichten stammen aus dem Paket java.awt.event.* Daher können wir auch auf diese nicht verzichten.
Die dritte import-Anweisung ist nun wirklich neu. Sie stellt uns das Swing-Paket zur Verfügung. Ich habe mal gewußt, warum das paket mit javax statt java beginnt, aber inzwischen vergessen. Ich glaube es hatte damit zu tun, daß es Swing bereits für Java 1.1 gab, es sich damals aber nicht um einen offiziellen, sondern nur einen optionalen zusätzlichen Bestandteil von Java handelte.
Nun, wie dem auch sei, mit dieser import-Anweisung steht uns Swing letztendlich zur Verfügung. Die nächste Änderung, die wir beobachten können, zieht sich durch das gesamte Programm:

public class SwingEins extends JFrame {

In der AWT-Anwendung haben wir geschrieben, ... extends Frame... Wenn wir mit Swing arbeiten, dann verwenden wir nicht mehr den Frameaus dem AWT-Paket, sondern den JFrame von Swing. Es ist in der Praxis ohne weiteres möglich, AWT und Swing Komponenten bunt zu mischen. Es ergeben sich aber dadurch nicht vorherzusehende Seiteneffekte, wie Verdeckung u.ä. Wenn man ein Programm schreibt sollte man sich für Swing oder AWT entscheiden, und dann auch nur entsprechende Komponenten durchgängig benutzen.
Genauso wie wir jetzt den JFrame verwenden, verwenden wir im folgenden auch das JLabel, das JTextField und denJButton. Abgesehen von der Tatsache, daß wir nun überall ein großes J davorgeschrieben haben, programmieren wir die Komponenten genauso wie in der entsprechenden AWT-Anwendung. Dies trifft, wie schon erwähnt, auch auf die Ereignisbehandlung zu. Lediglich am Schluß des Konstruktors sehen wir noch einen deutlichen Unterschied:

getContentPane().add(myContainer);

In der entsprechenden AWT-Anwendung haben wir immer

this.add(myContainer);

geschrieben. Dies ist die Zeile, in der wir das Panel mit all seinen enthaltenen Komponenten dem Fenster hinzufügen. Der Grund für diese Änderung liegt darin, daß in Swing den Fenstern oder Anwendungen keine Komponenten hinzugefügt werden können! Klingt komisch? Naja fast. Ein Fenster, bzw. die Anwendung verfügt bei Swing immer über eine einzige Komponente, nämlich JRootPane. DieseJRootPaneist dafür gedacht, den gesamten Inhalt einer Anwendung aufzunehmen. Daher muß man Komponenten bei Swing nicht dem JFrame hinzufügen, sondern diesem Inhaltsbereich. Um einen Verweis auf den Inhaltsbereich zu bekommen, verwendet man die Methode getContentPane(). Merken Sie sich das einfach mal so, und hinterfragen es nicht. Es hat mit der komplexen Theorie hinter Swing zu tun (die man ja aber in der Praxis meist vernachlässigen kann, wie man an diesem Beispielprogramm sieht )  :-)

Ja, das waren Sie schon, die wesentlichen Unterschiede zwischen der AWT-Version und der Swing-Version unseres Beispielprogramms. Sie sehen also, daß man Swing zunächst einmal als Ersatz für AWT verwenden kann, und die Programmierung nicht unbedingt komplizierter ist.. Im folgenden werden Sie noch sehen, daß Swing das AWT eben nicht ersetzt, sondern noch sehr viel weitere zusätzliche Eigenschaften mitbringt.

4.4. Dokumentmodelle
In meiner ersten Version von Edays habe ich für die Eingabe eines Datums drei Textfelder verwendet. Eines für den Tag, eines für den Monat und eines für die letzten beiden Ziffern des Jahres (Horror :).
Erstens zeigte sich schnell, daß bei der Anpassung des Komforts sehr viel redundanter Code zu schreiben wäre. Ferner kennt man aus anderen Anwendungen sogenannte maskierte Textfelder, die bereits ein bestimmtes Format zur Eingabe vorgeben. So etwas wollt ich eigentlich auch haben. Bei der Suche nach der Problemlösung stieß ich bereits schnell auf eine schöne Eigenschaft von Swing.

4.4.1. Klasse 2: DateFieldDocument.java
Ich habe oben schon einmal erwähnt, daß Swing bei seinen Komponenten die MVC-Architektur in gewisser Weise umsetzt. Ein JTextFieldzum Beispiel verwendet ein zugrunde liegendes PlainDocument als Dokumentmodell, welches die Daten des JTextField verwaltet, und bestimmte Zugriffsmethoden bereitsstellt oder verwendet. Unter anderem verfügt das PlainDocument über eine Methode insertString(). Jedesmal wenn ein Nutzer Zeichen eingibt, sei es per Tastatur oder Copy&Paste, wird diese Methode aufgerufen und sorgt dafür, daß die Daten an der richtigen Stelle in das Dokument eingefügt werden. Ist das vollbracht, sendet das Modell eine Nachricht an das JTextField, welches die aktuellen Daten nun in sich selbst zeichnet. Wenn Sie sich das ganze ansehen, dann werden Sie schnell feststellen, daß dies ein optimaler Platz für Filterungen ist. Wenn man die Methode insertString()entsprechend überschreibt, kann man dafür sorgen, daß nicht alle Daten an das Dokument weitergereicht werden.
Ich fing zunächst an, die Eingabe nur auf Ziffern zu beschränken, sowie die Stellen zu schützen, die die Punkte im Datum (deutsches Format) enthalten. Schließlich konnte man noch für eine schwache Validierung der Daten sorgen, indem man prüfte, ob wenigstens theoretisch der entsprechende Wert an dieser Stelle überhaupt sinnvoll ist. Zum Schluß hatte ich ein Dokumentmodell für Textfelder, welches zumindest für deutsche Verhältnisse das optimale herausholt, wie ich finde. Sehen wir uns den Sourcecode von DateFieldDocument.javaeinmal an:

import java.awt.Toolkit;   // für das Beepen 
import javax.swing.text.*; // für das PlainDocument
import java.util.*;        // für den Calendar
import java.text.*;        // für das SimpleDateFormat

public class DateFieldDocument extends PlainDocument {
 // **** Attribute
 private static final String JAHR  = "0123456789";// Erlaubte Ziffern Jahr
 private static final String DREI  = "0123";// Erlaubte Ziffern Tag 10er
 private static final String MONAT = "01";  // Erlaubte Zeichen Monat 10er
 private Calendar initDate = new GregorianCalendar(); // Calender fuers init
 private String initString;                 // Voreingestellter String
 private static int trenner1 = 2, trenner2 = 5;  // Position vor dem Trenner
 private JTextComponent textComponent;      // Für Referenz auf das TextFeld
 private int newOffset;                     // Caret Position bei Trennern
 SimpleDateFormat datumsFormat = new SimpleDateFormat ("dd.MM.yyyy"); //Konv.
 // **** Attribute Ende

 // **** Konstruktor 1
 public DateFieldDocument(JTextComponent textComponent) { 
  this.textComponent = textComponent;       // Hiermit wird jetzt gearbeitet
  initDate.setTime(new Date());             // Kalender auf heute
  initString = datumsFormat.format(initDate.getTime()); // Nach String
  try {                                     // Jetzt den Inhalt mit dem Datum
   insertString(0, initString, null);       // initialisieren
  }
  catch(Exception KonstrEx) { KonstrEx.printStackTrace(); }
 }
 // **** Konstruktor 1 Ende
 // **** Konstruktor 2
 public DateFieldDocument(JTextComponent textComponent, Calendar givenDate){ 
  this.textComponent = textComponent;       // Hiermit wird jetzt gearbeitet
  initDate=givenDate;                       // Kalender auf Parameter
  initString = datumsFormat.format(initDate.getTime()); // Nach String
  try {                                     // Jetzt den Inhalt mit dem Datum
   insertString(0, initString, null);       // initialisieren
  }
  catch(Exception KonstrEx) { KonstrEx.printStackTrace(); }
 }
 // **** Konstruktor 2 Ende

 // **** Überschreiben Insert-Methode
 public void insertString(int offset, String zeichen, 
       AttributeSet attributeSet) 
       throws BadLocationException {

  if(zeichen.equals(initString)) {          // Wenn initString, gleich rein
   super.insertString(offset, zeichen, attributeSet);
  }
  else if(zeichen.length()==10) {           // Wenn komplettes Datum, und
   if (JDateField.isDate(zeichen)) {        // richtig, dann rein
    super.remove(0, 10);
    super.insertString(0, zeichen, attributeSet);
   }
  }
  else if(zeichen.length()==1) {            // Wenn nicht, nur Einzelzeichen
   try {                                    // annehmen
    Integer.parseInt(zeichen);
   }
   catch(Exception NumEx) {                 // Kein Integer?
    return;                                 // Keine Verarbeitung!
   }
   if(offset==0) {                          // Tage auf 10 20 30 prüfen
    if( DREI.indexOf( zeichen.valueOf(zeichen.charAt(0) ) ) == -1 ) {
     Toolkit.getDefaultToolkit().beep();
     return;
    }
   }
   if(offset==1) {                          // Tage 32-39 unterbinden
    if(textComponent.getText().substring(0, 1).equals("3")) {
     int tag = new Integer(zeichen).intValue();
     if(tag>1) {
      Toolkit.getDefaultToolkit().beep();
      return;
     }
    }
   }
   if(offset==1) {                          // Tag 00 unterbinden
    if(textComponent.getText().substring(0, 1).equals("0")) {
     int tag = new Integer(zeichen).intValue();
     if(tag==0) {
      Toolkit.getDefaultToolkit().beep();
      return;
     }
    }
   }
   if(offset==2) {                         // Monate auf 0x-1x prüfen
                                           // (Caret links vom Trenner)
    if( MONAT.indexOf( zeichen.valueOf(zeichen.charAt(0) ) ) == -1 ) {
     Toolkit.getDefaultToolkit().beep();
     return;
    }
   }
   if(offset==3) {                         // Monate auf 0x-1x prüfen
                                           // (Caret rechts vom Trenner)
    if( MONAT.indexOf( zeichen.valueOf(zeichen.charAt(0) ) ) == -1 ) {
     Toolkit.getDefaultToolkit().beep();
     return;
    }
   }
   if(offset==4) {                         // Monate 13-19 unterbinden
    if(textComponent.getText().substring(3, 4).equals("1")) {
     int monat = new Integer(zeichen).intValue();
     if(monat>2) {
      Toolkit.getDefaultToolkit().beep();
      return;
     }
    }
   }
   if(offset==4) {                         // Monat 00 unterbinden
         if(textComponent.getText().substring(3, 4).equals("0")) {
     int monat = new Integer(zeichen).intValue();
     if(monat==0) {
      Toolkit.getDefaultToolkit().beep();
      return;
     }
    }
   }

   newOffset = offset;
   if(atSeparator(offset)) {             // Wenn am trenner, dann den offset
    newOffset++;                         // vor dem einfügen um 1 verschieben
    textComponent.setCaretPosition(newOffset);
   }
   super.remove(newOffset, 1);           // Aktuelles zeichen entfernen
   super.insertString(newOffset, zeichen, attributeSet);    // Neues einfügen
  }
 }
 // **** Überschreiben Insert Ende

 // **** Überschreiben Remove
 public void remove(int offset, int length) 
       throws BadLocationException {
  if(atSeparator(offset)) 
   textComponent.setCaretPosition(offset-1);
  else
   textComponent.setCaretPosition(offset);
 }
 // **** Überschreiben Remove Ende

 // **** Hilfsmethode für die Punkte zwischen den Feldern
 private boolean atSeparator(int offset) {
  return offset == trenner1 || offset == trenner2;
 }
 // **** Hilfsmethode Ende
}

Jaja, ich weiß, wieder so ein langes Konstrukt. Sieht auch erstmal ziemlich kompliziert und durcheinander aus. Aber wenn man es erklärt, wird es bald völlig transparent.
Fangen wir einfach vorne an:

import java.awt.Toolkit;   // für das Beepen 
import javax.swing.text.*; // für das PlainDocument
import java.util.*;        // für den Calendar
import java.text.*;        // für das SimpleDateFormat

Die erste import-Anweisung ist neu für uns. Sie importiert das Paket java.awt.Toolkit.* Dort ist unter anderem eine Funktion enthalten, mit der man den typischen System-Beep, z.B. bei Fehlern auslösen kann. Diese benötigen wir später.
Swing beeinhaltet ein Unterpaket text. Weil Textbehandlung sehr umfangreich sein kann (denken Sie nur an größere Textfelder, die in Editoren verwendet werden, oder auch Textfelder von Swing, die formatierte RTF Dokumente anzeigen können) und in der Folge dessen die Klassen zur Textbehandlung eine große Anzahl erreichten, wurden diese in das genannte Unterpakte ausgelagert. Auch das Dokumentmodell PlainDocumentbefindet sich dort. Daher müssen wir das Paket auch extra importieren. Da wir für die Initialisierung des Modells ein gültiges, nämlich das heutige Datum verwenden, wird auch wieder java.util.* importiert.
Java hat ebenfalls ein eigenes Unterpaket text. Hier finden sich Methoden, die die Formatierung von Werten übernehmen, die international unterschiedlich sein können. Dazu gehört sicher auch das Datum. Wir verwenden später die Klasse SimpleDateFormat, mit der wir ein Datum in einen formatierten String überführen.
So, nun folgt eine im Quelltext etwas unübersichtliche Definition von Attributen:

 // **** Attribute
 private static final String JAHR  = "0123456789";// Erlaubte Ziffern Jahr
 private static final String DREI  = "0123";// Erlaubte Ziffern Tag 10er
 private static final String MONAT = "01";  // Erlaubte Zeichen Monat 10er
 private Calendar initDate = new GregorianCalendar(); // Calender fuers init
 private String initString = "13.12.2000";  // Voreingestellter String
 private static int trenner1 = 2, trenner2 = 5;// Position vor dem Trenner
 private JTextComponent textComponent;      // Für Referenz auf das TextFeld
 private int newOffset;                     // Caret Position bei Trennern
 SimpleDateFormat datumsFormat = new SimpleDateFormat ("dd.MM.yyyy"); //Konv.
 // **** Attribute Ende

In den ersten drei Zeilen werden Konstanten definiert, die in einem String jeweils die erlaubten Ziffern für bestimmte Stellen eines Datums enthalten. Diese Konstanten werden wir später bei der Validierung nutzen: (übrigens bei der später erklärten Feststellung, ob nur erlaubte Ziffern eingegeben wurden, hätte man dann auch JAHR benutzen können. Dort habe ich aber einfach mal eine andere Variante benutzt).
Anschließend wird ein Calendar-Objekt für die Initialisierung erzeugt, sowie ein String für den gleichen Zweck. Der String enthält ein beliebiges Datum, wird aber später im Laufe des Programms aktualisiert. In den Integer-Variablen trenner1 und trenner2 werden die Positionen für die Datumstrennzeichen festgelegt. Die Positionen beruhen auf dem Caret, welches später erklärt wird.
Nach den Trennern deklarieren wir ein eine TextKomponente. Auf dieser basiert das TextField, und das Dokument greift auf so eine Komponente zu. Damit auch unser Document auf die zugrundeliegende Komponente zugreifen kann, brauchen wir eine entsprechende Deklaration.
Um die Trenner-Positionen richtig verwalten zu können, brauchen wir noch einen Offset, den wir verschieben können (siehe unten). Dafür nutzen wir das entsprechende Attribut.
Als letztes erstellen wir eine Instanz von SimpleDateFormat, der wir gleich ein Pattern mit auf den Weg geben, wie wir den die Datumsausgabe gerne formatiert hätten. Über datumsFormat können wir ab jetzt Objekte des Typs Date in einen String mit genau dem gewünschten Format überführen. Wenn Sie mehr Informationen über die umfangreichen Möglichkeiten von SimpleDateFormat haben wollen, sehen Sie unter java.text.* in der API-Dokumentation nach.
Auch wenn noch nicht für jedes Attribut der Sinn klar ist, sei das an dieser Stelle erstmal alles zu den Attributen. Der Sinn geht schon auf, wenn wir die Methoden erläutern.
Fangen wir mit den beiden Konstruktoren an:

 // **** Konstruktor 1
 public DateFieldDocument(JTextComponent textComponent) { 
  this.textComponent = textComponent;       // Hiermit wird jetzt gearbeitet
  initDate.setTime(new Date());             // Kalender auf heute
  initString = datumsFormat.format(initDate.getTime()); // Nach String
  try {                                     // Jetzt den Inhalt mit dem Datum
   insertString(0, initString, null);       // initialisieren
  }
  catch(Exception KonstrEx) { KonstrEx.printStackTrace(); }
 }
 // **** Konstruktor 1 Ende
 // **** Konstruktor 2
 public DateFieldDocument(JTextComponent textComponent, Calendar givenDate){ 
  this.textComponent = textComponent;       // Hiermit wird jetzt gearbeitet
  initDate=givenDate;                       // Kalender auf Parameter
  initString = datumsFormat.format(initDate.getTime()); // Nach String
  try {                                     // Jetzt den Inhalt mit dem Datum
   insertString(0, initString, null);       // initialisieren
  }
  catch(Exception KonstrEx) { KonstrEx.printStackTrace(); }
 }
 // **** Konstruktor 2 Ende

Wir nutzen zwei Konstruktoren, die sich sehr ähneln. Beiden wird ein Verweis auf eine Textkomponente übergeben. Wie gesagt, orientiert sich unsere Arbeit in der Basis an einer derartigen Textkomponente. Also benötigen wir von genau dem TextFeld, für das dieses Dokumentmodell gelten soll, den Verweis auf seine konkrete Textkomponente. Der erste Konstruktor, initialisiert dann mit dem aktuellen Datum, und der zweite wird mit einem Datum vom Typ Calendar "gefüttert", um mit einem vorgegebenen Datum zu initialisieren.
Zuerst wird die Textkomponente dem Modell zugewiesen. An genau dieser Komponente werden jetzt alle Operationen durchgeführt. Beim Konstruktor 1 wird jetzt das Attribut initDate mit der bekannten Vorgehensweise auf heute gesetzt, und beim Konstruktor 2 wird das übergebene Datum dem initDate zugewiesen. Dieses initDate wird jetzt mit der erwähnten Funktionalität von SimpleDateFormat deminitStringzugewiesen, der schließlich die klassische Form "tt.mm.jjjj" (in Deutschland) aufweisen soll. Die Methode vonSimpleDateFormat dazu lautet einfach format().
Normalerweise sollte dabei nichts schiefgehen, und der initString könnte in unser Modell eingefügt werden. Wir sehen hier aber eine besondere Art des Aufrufs. Hier treffen wir zum ersten Mal in diesem Tutorial auf die Fehlerbehandlung in Java. Methoden können so programmiert werden, daß sie einen Fehler auswerfen, wenn ein solcher auftritt. Zumindest die überschriebene Methode insertString() verwendet einen derartigen Mechanismus. Wird so ein Fehler erzeugt, und der Aufrufer der Methode reagiert nicht auf ihn, führt der Fehler zum Programmabbruch. Wie kann man nun auf einen derartigen Fehler reagieren? Das sehen wir genau hier in unseren beiden Konstruktoren. Man verwendet eine try {} catch {} Klausel. Im try-Block könnnen beliebig viele Anweisungen stehen. Wird der Block ausgeführt, ohne daß ein Fehler auftritt, so wird der catch-Block komplett ignoriert. Tritt jedoch im try-Block ein Fehler auf, so werden die Anweisungen im catch-Block ausgeführt. Die genaue Art des Fehlers wird dem catch-Block als Argument mit auf den Weg gegeben, so daß dieser auch sehr genau ausgewertet werden könnte. In unserem Beispiel wird die Methode insertString() aufgerufen (und zwar genau unsere, die wir weiter unten noch überschreiben, und nicht die von der Elternklasse PlainDocument), und sollte diese einen Fehler auswerfen, wird nichts weiter getan als der aktuelle Stack der JVM ausgegeben (muß man jetzt nicht verstehen). Diese Art der Fehlerbehandlung kann man durchaus produktiv einsetzen. Jede selbstgeschriebene Klasse kann beliebig Fehler auswerfen. Durch die try-catch Klausel, kann man auf diese Fehler geordnet reagieren. Die Fehler für die mitgelieferten Klassen des JDK sind übrigens in der API dokumentiert, so daß man tatsächlich auf konkrete Fehler auch konkret reagieren kann. Denken Sie nur an einen File-Not-Found-Error, der kommt doch wesentlich besser, wenn das Programm daraufhin nicht beendet wird, sondern eine Meldung ausgibt, und dann weiterfunktioniert.
In unserem konkreten Beispiel machen wir ja tatsächlich nicht viel in diesem try-catch-Block. Aber durch die Behandlung des Fehlers vermeiden wir, daß das Programm abgebrochen wird. In unserem konkreten Programm sollte eigentlich auch kein besonderer Fehler auftreten, somit halte ich diese Konstruktoren erstmal für ausreichend.
Der Zweck der Konstruktoren, um es noch einmal zusammenfassend zu sagen, liegt eigentlich darin, das heutige, oder ein übergebenes Datum vorab in das Modell einzusetzen (ja, ich stehe auf geordnete Zustände. Wenn kein Datum übergeben wird, kann das heutige auch nicht schaden).
Kommen wir jetzt zum Kernstück des Modells, dem Überschreiben der Methode insertString() des PlainDocument. Sagen wir es mal so, dieser ganze Abschnitt ist geprägt durch viele if-Abfragen. Davon sind viele sehr ähnlich. Deswegen werde ich an dieser Stelle auch nicht den kompletten Source der Methode noch einmal zeigen, sondern im wesentlichen immer die jeweilige if-Abfrage. Sehen wir uns vorher noch einmal die Signatur der Methode an:

public void insertString(int offset, String zeichen, 
       AttributeSet attributeSet) 
       throws BadLocationException {

Sie sehen einmal mehr, daß eine Java Zeile nicht auch genau eine Zeile im Source darstellen muß :-) Die Original-Methode erwartet drei Argumente. Den offset, also die Position im Modell, an die der übergebene String eingefügt werden soll, den String selbst, den wir hier zeichen genannt haben und ein attributeSet, welches wir nicht weiter berücksichtigen, aber an die Originalmethode im Endeffekt durchreichen. Und sie sehen eine Eigenart der vorhin schon angesprochenen Fehlerbehandlung. Wenn eine Methode Fehler auswirft, dann wird dies in der Signatur kundgetan. Das Schlüsselwort dafür, daß Fehler ausgeworfen werden, heißt throws. Darauf folgt die Syntax der Fehlermeldung, mit der diese auch in catch-Blöcken abgefragt werden kann. Wir selbst implementieren keine Fehlermeldung, aber die überschriebene Methode tut es. Daher müßen wir diese Syntax auch verwenden. Außerdem rufen wir die überschriebene Methode später selbst noch auf. Dadurch, daß auch wir die throws-Klausel verwenden, gehen keine Fehler der Original-Methode verloren.
Kommen wir jetzt zur ersten if-Abfrage:

  if(zeichen.equals(initString)) {          // Wenn initString, gleich rein
   super.insertString(offset, zeichen, attributeSet);
  }
  else if(zeichen.length()==10) {           // Wenn komplettes Datum, und
   if (JDateField.isDate(zeichen)) {        // richtig, dann rein
    super.remove(0, 10);
    super.insertString(0, zeichen, attributeSet);
   }
  }
  else if(zeichen.length()==1) {            // Wenn nicht, nur Einzelzeichen
   try {                                    // annehmen
    Integer.parseInt(zeichen);
   }
   catch(Exception NumEx) {                 // Kein Integer?
    return;                                 // Keine Verarbeitung!
   }

Hier wird zunächst festgestellt, ob der übergebene String der vorgegebene initString ist. Da wir leichtsinnigerweise davon ausgehen, daß der initString immer ein gültiges Datum mit 10 Stellen darstellt, rufen wir auch gleich die Original-Methode der Elternklasse PlainDocument auf, um den String in das Modell einzufügen. Die nächste else if-Abfrage wird aufgerufen, wenn es nicht der initString war.. Zunächst wird darauf geprüft, ob der String 10 Stellen lang ist. Dies entspricht dem "tt.mm.jjjj" Format. Sollte die Länge entsprechend sein, so wird eine Klassenmethode von JDateField aufgerufen. Das greift natürlich etwas vor, weil wir diese Klasse ja erst etwas später vorstellen. Dort habe ich eine Methode programmiert, die einen String darauf prüft, ob er wirklich ein gültiges Datum ist. Ist dies der Fall, wird die remove-Methode der Elternklasse aufgerufen, und löscht das gesamte Modell, worauf die insertString-Methode der Elternklasse aufgerufen wird, und den kompletten String in das Modell einsetzt.
War der String nicht 10 Zeichen lang kommt die nächste else if-Abfrage an die Reihe, die die folgenden if-Abfragen schachtelt. Sie ist also für alle folgenden Abfragen bedeutend, da sie darauf prüft, ob zeichen die Länge 1 hat. Die Konsequenzen sind schnell klar. Es kann entweder nur ein kompletter String in das Modell eingesetzt werden, oder ein einzelnes Zeichen. Bei der händischen Eingabe in das Textfeld, wird dies im Normalfall immer Zeichen für Zeichen gemacht. Die Möglichkeit für den kompletten String wurde für Copy&Paste Operationen eingefügt. Es ist schon denkbar, daß man aus einem anderen Fenster ein Datum per Copy&Paste übernehmen will. Daher die Unterscheidung auf 1 oder 10 Zeichen. Daraus ergibt aber sich auch, daß man per Copy&Paste nicht NUR den Monat oder NUR den Tag oder NUR das Jahr übernehmen kann. Eine Einschränkung mit der man meiner Meinung nach leben kann.
Nach dieser Abfrage auf die Länge 1 von zeichen, sehen wir die nächste entscheidende, und für unsere Klasse unglaublich nützliche Funktion. Das Zeichen wird darauf abgeklopft, ob es sich um einen Integer-Wert handelt. Sollte das nicht der Fall sein, wird die Methode mit return abgebrochen. Somit können nur Integer-Werte eingegeben werden. Hierbei wird die Fehlerbehandlung von Java einmal richtig produktiv ausgenutzt. Es wird eine Klassenmethode von Integer genutzt, die einen String in einen Integer-Wert wandelt. Diese Methode wirft einen Fehler aus, wenn in dem String Werte enthalten sind, die nicht nach Integer gewandelt werden können. Eben alle anderen Zeichen als "0123456789". Sollte dieser Fehler auftreten, dann wird die return-Anweisung im catch-Block aufgerufen, und damit wird praktisch die falsche Eingabe ignoriert.
Alle folgenden if-Abfragen konzentrieren sich nicht mehr auf das zeichen (da dies ja eine einzelne Ziffer als String enthalten muß), sondern mehr auf den offset. Die ersten Abfragen (9 an der Zahl) versuchen sich an der schwachen Validierung. Was ist schwache Validierung? Nun, wenn man schon filtern kann, dann kommt man auf die Idee gleich Code einzufügen, der prüft, ob es sich beim Eingegebenen um ein gültiges Datum handelt. Jetzt stellt sich aber eine entscheidene Frage? Kann man direkt während der Eingabe schon überprüfen, ob ein Datum gültig ist? Kann man nicht. Wenn man von vorne anfängt, und eine 31 eingibt, dann wissen wir schon vorab, daß es mindestens 5 Monate gibt, für die diese Eingabe nicht zutreffend ist. Das Datumsfeld kann natürlich an dieser Stelle dann nicht meckern, weil es ja noch nicht weiß, welcher Monat nun folgend eingegeben wird. Es könnte natürlich bei den falschen Monaten anschließend den Tag entsprechend nach unten korrigieren. Vielleicht will der Anwender aber beim Monat mit der Eingabe anfangen und findet die Automatik gar nicht lustig?
Im Ergebnis stelle ich fest, daß WÄHREND der Eingabe nur für das jeweilige Feld unmögliche Eingaben unterbunden werden können. Daher folgen if-Abfragen, die alle wie folgt aussehen:

   if(offset==0) {                          // Tage auf 10 20 30 prüfen
    if(DREI.indexOf(zeichen.valueOf(zeichen.charAt(0))) == -1 ) {
     Toolkit.getDefaultToolkit().beep();
     return;
    }
   }

Dies ist die erste, und prüft die erste Stelle der Tagesangabe ab, die sich ja auf die Zehnerstelle bezieht. Kriterium für die Stelle ist der offset, und dieser wiederum bezieht sich auf das sogenannte Caret. Das Caret ist der Cursor im Textfeld. Wenn ich einen String einfügen will, dann kann ich das ja nicht an der Stelle eines Zeichens in dem String machen, sondern nur an der Stelle zwischen zwei Zeichen. Die Nummerierung der Caretpositionen erfolgt folgendermaßen:

Grafik Caretnummerierung

Das heißt, daß unsere erste Abfrage feststellt, ob das Caret, also der Cursor vor dem ersten Zeichen steht. Ist das der Fall, beabsichtigt der Nutzer das erste Zeichen zu überschreiben. Hier wird nun geprüft, ob die einzugebene Ziffer eine 0, 1, 2 oder 3 ist. Bei den Zehnerstellen des Tages ist ja schließlich nichts anderes möglich. Wenn Sie sich noch an unsere Attribute erinnern, erinnern sie sich vielleicht auch an die Konstante DREI. Diese wurde mit "0123" belegt. Strings haben eine Methode, die das erste Auftreten eines Zeichens in einem String zurückgibt. Bei dem Wert handelt es sich um die Position des Zeichens (diesmal nicht zu verwechseln mit der Position des Caret). Tritt das Zeichen nicht auf, wird -1 zurückgegeben. Unsere if-Abfrage prüft also ob indexOf == -1 wahr ist. Ist es wahr, dann wissen wir, daß zeichenin DREI nicht vorkommt, mithin keine der vier Ziffern ist. Für diesen Fall stellt die Abfrage fest, daß die Eingabe illegal ist. Es wird die erwähnte Funktion für den Systembeep ausgeführt (merkwürdig: das Programm läuft bei mir auf zwei Rechnern, auf einem piept es auf einem nicht, hm), die ich nicht erklären kann, sondern nur irgendwo abgeschrieben habe. Danach wird die gesamte Methode durch returnabgebrochen.
Die nächste Abfrage bezieht sich auf Caretposition 1:

   if(offset==1) {                          // Tage 32-39 unterbinden
    if(textComponent.getText().substring(0, 1).equals("3")) {
     int tag = new Integer(zeichen).intValue();
     if(tag>1) {
      Toolkit.getDefaultToolkit().beep();
      return;
     }
    }
   }

Bei einer Eingabe, wo das Caret dort steht, wird auf die bestehnde Textkomponente zugegriffen, um festzustellen ob die Zehnerstelle vielleicht auf drei steht.  Dazu wird die getText-Methode von textComponent verwendet. Auf das Ergebnis wird die String-Methode substring() angewendet. Dieser wird der gewünschte Anfangsindex und Endindex des Teilstrings als Argument übergeben. Auf den so extrahierten String wenden wir schließlich die bekannte equals-Methode an. Ist die Zehnerstelle 3, wird für die Einerstelle jeder Wert größer als 1 verboten. Der Tag wird nur in einen int umgewandelt, weil sich so leichter auf größer vergleichen läßt. Die Konsequenz einer fehlerhaften Eingabe kennen wir ja schon (beep, return).
Die im Listing darauf folgende if-Abfrage wird auch an Caretposition 1 ausgeführt, und testet darauf, ob versucht wird 00 für einen Tag einzugeben, was nicht erlaubt ist. Diese Methode funktioniert genauso wie die eben erläuterte.
Die beiden nun folgenden if-Abfragen werden an Caretposition 2 und 3 ausgeführt, und widmen sich dem gleichen Thema (der Code wird nicht gezeigt, weil er dem eben gezeigten genau gleicht). Genauso, wie wir die Zehnerstellen des Tages auf die Wert 0-3 geprüft haben, werden hier die Zehnerstellen des Monats auf 0 und 1 geprüft. Warum wird die Abfrage an zwei Caretpositionen durchgeführt ? Nun, je nachdem, wie der Nutzer sich vorher durch das Textfeld bewegt hat (Cursortasten, Backspace) kann es vorkommen, daß er vor dem ersten Punkt zur Trennung von Tag oder Monat, oder dahinter steht. Da wir ja ein maskiertes Textfeld programmieren, ist der Punkt unantastbar, und  wird durch das Modell verwaltet. Die Caretposition beim Einfügen einer Ziffer für den Zehner des Monats kann aber wie gesagt bei 2 oder 3 stehen. daher muß diese Abfrage an beiden Stellen durchgeführt werden (eine einzelne mit Oderverknüpfte Abfrage wäre auch gegangen).
Nun wird in der nächsten Abfrage die Eingabe der Monate 13-19 unterbunden. Dies wird genauso getan, wie bei den Tagen 32-39. Ebenso wird in der Folgeabfrage der Monat 00 unterbunden.
So, das war die schwache Validation der Eingabe. Es ist immer noch möglich ungültige Daten wie den 31.02.2001 einzugeben, aber völlig unsinnige Werte (wie 39.19.2004) sind ausgeschlossen.
Als nächstes, muß das zeichen, welches unsere ganzen Abfragen überstanden hat endlich in das Modell eingefügt werden.
Hierzu kommt folgender Code zum Einsatz:

   newOffset = offset;
   if(atSeparator(offset)) {             // Wenn am trenner, dann den offset
    newOffset++;                         // vor dem einfügen um 1 verschieben
    textComponent.setCaretPosition(newOffset);
   }
   super.remove(newOffset, 1);           // Aktuelles zeichen entfernen
   super.insertString(newOffset, zeichen, attributeSet);    // Neues einfügen

Der als Attribut deklarierte newOffset wird mit dem Wert desoffsetbelegt. Jetzt wird geprüft, ob sich der Caret zufällig an einem unserer Trennpunkte befindet. Ist dies der Fall, wird der newOffsetum einen erhöht, und die Caret-Position in der eigentlichen Textkomponente einen weiter geschoben. War dies nicht der Fall, behält newOffsetseinen Wert. Mit dem originalen oder dem manipulierten newOffsetwird schließlich das auf den Caret folgende Zeichen gelöscht, und das vom Nutzer eingegeben Zeichen wird eingefügt (durch Aufruf der Originalmethoden in der Elternklasse PlainDocument). Auf diese Art und Weise wirkt unser Textfeld immer überschreibend und unseren Punkten kann nichts passieren. Der Nutzer braucht nur die Ziffern einzugeben, und muß sich um das Format nur in soweit kümmern, als das er auch immer zwei Ziffern für Tag und Monat eingeben muß (im Zweifel halt 03 01 2001).
Damit ist die Methode insertString() so überschrieben, daß nur Ziffern eingegeben werden können, nur einzelne Zeichen oder ein kompletter Datumsstring, keine wirklich ungültigen Zeichen und unsere Punkte sind auch geschützt. Was will man mehr. Öh ja, man will, nein, man muß noch die remove-Methode überschreiben, damit es nicht zu Inkonsitenzen kommt. Dies ist jedoch schnell erledigt:

 public void remove(int offset, int length) 
       throws BadLocationException {
  if(atSeparator(offset)) 
   textComponent.setCaretPosition(offset-1);
  else
   textComponent.setCaretPosition(offset);
 }

Da wir durch die insertString-Methode ja dem Textfeld die Eigenschaft gegeben haben, daß es überschreibend ist, braucht man keine Entfernen-Methode. Die entsprechenden Tasten können sich also so wie Cursortasten verhalten. Dies erreicht man wie gezeigt, dadurch daß man lediglich die Caretposition verschiebt, und dabei unsere beiden Trennpunkte berücksichtigt. Dadurch, daß die Elternmethode nicht aufgerufen wird, wird nicht wirklich ein Zeichen gelöscht.
Jetzt fehlt noch eine Hilfsmethode. Dies ist die Methode, mit der ich geprüft habe, ob das Caret an der Trennerposition ist:

private boolean atSeparator(int offset) {
  return offset == trenner1 || offset == trenner2;
 }
 

Hier sehen wir eine kurze Methode. Der Gesamtausdruck ist wahr, wenn entweder die rechte oder die linke Seite neben dem Oder-Operator wahr ist. Dazu ist eigentlich nichts weiter zu sagen.

4.4.2. DateFieldDocument testen
Die Klasse DateFieldDocument hat mal wieder keine main-Methode. Aber zum Testen können wir einfach das SwingEins-Programm verwenden. Dort ist ja ein Textfeld enthalten. Diesem weisen wir einfach unser Dokument zu. Dafür brauchen wir nur eine Zeile zusätzlich einzufügen (die im folgenden grün eingefärbt ist):

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class SwingEins extends JFrame {

 public SwingEins() {                                  // *** Konstruktor
  super("SwingEins");                                  // Titel

  JLabel myHeader = new JLabel("Hier Text eingeben:"); // Labelkomponente
  JTextField eingabeFeld = new JTextField("hier", 20); // Textfeld-Komponente

  eingabeFeld.setDocument(new DateFieldDocument(eingabeFeld));

  JButton klicker = new JButton("Beenden");            // Button-Komponente

  ActionListener myListener = new ActionListener() {   // Fuer den Button
   public void actionPerformed(ActionEvent e) {        // in anonymer Klasse
    dispose();                                         // (siehe auch WinV)
    System.exit(0);
   }
  };

  klicker.addActionListener(myListener);            // Listener registrieren

  JPanel myContainer = new JPanel();                // spezieller Container
  myContainer.setLayout(new BorderLayout(5,5));     // Layout einsetzen

  myContainer.add(myHeader, BorderLayout.NORTH);    // Die Komponenten dem
  myContainer.add(eingabeFeld, BorderLayout.CENTER);// Container hinzufügen
  myContainer.add(klicker, BorderLayout.SOUTH);

  getContentPane().add(myContainer);                // Container dem Frame
                                                    // hinzufügen
  this.enableEvents(AWTEvent.WINDOW_EVENT_MASK);    // Ereignisse ermöglichen
 }                                                  // *** Ende Konstruktor

 protected void processWindowEvent(WindowEvent e) {    // Fensterereignisse
         if (e.getID()==WindowEvent.WINDOW_CLOSING) {  // behandeln
   dispose();                           // Ressourcen des Fensters freigeben
   System.exit(0);                      // Programm beenden
  }
 } // Ende Methode processWindowEvent()

 public static void main(String[] arg) {            // Hauptmethode
        SwingEins mySwingapp = new SwingEins();
        mySwingapp.pack();
        mySwingapp.show();
 }
}

So, die eine Zeile sorgt dafür, das das ganz normale JTextField eingabeFeld von nun an unser Dokument benutzt:

eingabeFeld.setDocument(new DateFieldDocument(eingabeFeld));

Beim Zuweisen mit der Methode setDocument() übergeben wir unserem Dokument eine Referenz auf die Textkomponente eingabeFeld. Damit klappt das alles. Führen Sie dieses aus zwei Klassen bestehende Programm einmal aus, und spielen Sie im Eingabefeld herum:

Screenshot Swing-Dokument

Auf die gleiche Art und Weise kann man auch Dokumentmodelle für Vorgangsnummern, Schadensnummern, Telefonnummern etc. etc. programmieren.
Sollten Sie übrigens bei diesem Beispiel eine Compiler- oder JVM Fehlermeldung erhalten, so übersetzten sie zunächst die KlasseJDateField .java im selben Verzeichnis (wegen der Klassenmethode von JDateField, die das Dokumentmodell nutzt).

4.5. Eine eigene Komponente
Wo ich nun grad dabei war, dachte ich mir, daß man nun auch gleich eine eigene Erweiterung von JTextField anfertigen könnte, die automatisch das Dokumentmodell von DateFieldDocument benutzt, und einige weitere Datums-spezifische Methoden mitbringt. Diese Zusatzmethoden benötigte ich sowieso für Edays. Die Erweiterung könnte man dann genauso einfach in ein Projekt einbinden, wie einJTextField.

4.5.1. Klasse 3: JDateField.java
In Anlehnung an das JTextField habe ich meine Klasse jetzt einfach mal JDateField genannt. Das ganze Listing ist ziemlich lang. Ein Textfeld, daß sich nur um die Eingabe von Daten kümmert, sollte auch Attribute besitzen, die Daten reflektieren.  Also gibt es int- und String-spezifische Datumsattribute. Ferner gibt es natürlich Calendar-spezifische Attribute. Dazu noch die entsprechenden Zugriffsmethoden und drei, vier nützliche weitere Methoden. Wie üblich erschlage ich Sie erst einmal mit dem kompletten Listing:

import java.util.*;    // Fuer Calendar Objekte
import javax.swing.*;  // Weil JTextField erweitert wird

public class JDateField extends JTextField { // **** Beginn Klasse JDateField

 //**** zusätzliche Attribute
 private Calendar myDates = new GregorianCalendar(); // Calender fuers init.
 private int tagInt = myDates.get(Calendar.DATE);    //Felder, die Teil-
 private int monatInt = myDates.get(Calendar.DATE);  //strings des gesamten
 private int jahrInt = myDates.get(Calendar.DATE);   //TextFeldes enthalten, 
 private String tagString = String.valueOf(tagInt);  //bzw. die ent-
 private String monatString = String.valueOf(monatInt); //sprechenden Int- 
 private String jahrString = String.valueOf(jahrInt);//Werte dazu
 // zur Datumsgültigkeit:
 private static final int[] tageMax = {31,28,31,30,31,30,31,31,30,31,30,31}; 
 //**** zusätzliche Attribute Ende

 //****** Konstruktoren ******
 // Konstruktor initialisiert mit Datum = HEUTE
 public JDateField() {              // Kein Para (Def. Datum: Heute)
  super();                          // Wie gehabt
  this.setDocument(new DateFieldDocument(this));  // Passendes Dokument
 }

 // Konstruktor spezifiziert Anzahl Spalten und heutiges Datum
 public JDateField(int columns) {   // Breite (Default Datum: Heute)
  super(columns);                   // Wie gehabt
  this.setDocument(new DateFieldDocument(this));  // Passendes Dokument
 }

 // Konstruktor für einen Datums-String der Form "tt.mm.jjjj"
 public JDateField(String text) {  // Datum als String der Form xx.xx.xxxx
  super(text);                     // Wie gehabt
  this.setDocument(new DateFieldDocument(this));  // Passendes Dokument
 }

 // Konstruktor für einen Datums-String der Form "tt.mm.jjjj" 
 // und die Anzahl der Spalten 
 public JDateField(String text, int columns) {  // Kombination aus 2 und 3
  super(text, columns);                         // Wie gehabt
  this.setDocument(new DateFieldDocument(this));  // Passendes Dokument
 }

 // Konstruktor der mit einem Calendar Objekt initialisiert wird
 public JDateField(Calendar myDate) {   // SpezialConstructor des Documents
  super();                              // Wie gehabt
  this.setDocument(new DateFieldDocument(this, myDate)); // Passendes Dokum.
 }

 // Konstruktor der mit einem Calendar Objekt initialisiert wird
 // und der Anzahl der Spalten
 public JDateField(Calendar myDate, int columns) { // Spez.Const. des Doc.
  super(columns);                                  // Wie gehabt
  this.setDocument(new DateFieldDocument(this, myDate)); // Passendes Dokum.
 }

 // Konstruktor mit drei int-Werten für Tag, Monat und Jahr 
 public JDateField(int tag, int monat, int jahr) { // Mit Ints fürs Datum
  super();                                         // Wie gehabt
  this.setDocument(new DateFieldDocument(this,     // Passendes Dokument
         new GregorianCalendar(jahr, monat, tag)));
 }

 // Konstruktor mit drei int-Werten für Tag, Monat und Jahr 
 // und einem int-Wert für die Anzahl der Spalten
 public JDateField(int tag, int monat, int jahr, int columns) { // Mit Int 
  super(columns);                              // Wertn für Datum und Spalten
  this.setDocument(new DateFieldDocument(this, // Passendes Dokument
         new GregorianCalendar(jahr, monat, tag)));
 }
 //****** Konstruktoren Ende *

 //****** SET METHODEN *******
 // **** set-Methoden für ganze Daten
 public void setDate(Calendar myNewDate) {   // Übergabe eines Calender
  String myTagString=String.valueOf(myNewDate.get(Calendar.DATE));
  String myMonatString=String.valueOf(myNewDate.get(Calendar.MONTH)+1);
  String myJahrString=String.valueOf(myNewDate.get(Calendar.YEAR)); 
  this.setText(myTagString+"."+myMonatString+"."+myJahrString);
 } 
 public void setDate(String myDateText) {    // Übergabe eines String
                                             // zB "11.12.2000"
  if(isDate(myDateText)==true)
   this.setText(myDateText); 
 }
 public void setDate(int day, int month, int year) {   // Übergabe 
  String myTagString=String.valueOf(day);              // dreier int Werte 
  if(day<10) myTagString="0"+String.valueOf(day);
  String myMonatString=String.valueOf(month);
  if(month<10) myMonatString="0"+String.valueOf(month);
  String myJahrString=String.valueOf(year);
  if(year<10) myJahrString="000"+String.valueOf(year);
  if(year<100) myJahrString="00"+String.valueOf(year);
  if(year<1000) myJahrString="0"+String.valueOf(year);
  this.setText(myTagString+"."+myMonatString+"."+myJahrString);
 }

 public void setDay(int day) {     // **** set-Methoden für Int Werte
  attributeUpdate();               // setzt den Tag nach Übergabe eines int 
  String myTagString=String.valueOf(day);
  if(day<10) myTagString="0"+String.valueOf(day);
  this.setText(myTagString+"."+monatString+"."+jahrString);
 }
 public void setMonth(int month) { // setzt den Monat nach Übergabe eines int
  attributeUpdate();
  String myMonatString=String.valueOf(month);
  if(month<10) myMonatString="0"+String.valueOf(month);
  this.setText(tagString+"."+myMonatString+"."+jahrString);
 }
 public void setYear(int year) {   // setzt das Jahr nach Übergabe eines int
  attributeUpdate();
  String myJahrString=String.valueOf(year);
  if(year<10) myJahrString="000"+String.valueOf(year);
  if(year<100) myJahrString="00"+String.valueOf(year);
  if(year<1000) myJahrString="0"+String.valueOf(year);
  this.setText(tagString+"."+monatString+"."+myJahrString);
 }

 public void setDayText(String day) {// **** set-Methoden für String Werte
  attributeUpdate();                // Übergabe eines String tag
  String myTagString=day;
  if(day.length()<2) myTagString="0"+day;
  this.setText(myTagString+"."+monatString+"."+jahrString);
 }
 public void setMonthText(String month) { // Übergabe eines String monat
  attributeUpdate(); 
  String myMonatString=month;
  if(month.length()<2) myMonatString="0"+month;
  this.setText(tagString+"."+myMonatString+"."+jahrString);
 }
 public void setYearText(String year) {   // Übergabe eines String jahr
  attributeUpdate();
  String myJahrString=year;
  if(year.length()==1) myJahrString="000"+year;
  if(year.length()==2) myJahrString="00"+year;
  if(year.length()==3) myJahrString="0"+year;
  this.setText(tagString+"."+monatString+"."+myJahrString);
 }

 //****** GET METHODEN *******

 public int getDay() {   // **** get-Methoden für Int Werte
  attributeUpdate();     // Übergibt den Tag als Int zB 28
  return tagInt;
 }
 public int getMonth() { // Übergibt den Monat als Int zB 9
  attributeUpdate();
  return monatInt;
 }
 public int getYear() {  //Übergibt das Jahr als Int zB 2001
  attributeUpdate();
  return jahrInt;
 }

 public String getDayText() { // **** get-Methoden für String Werte
  attributeUpdate();            //Übergibt den Tag als String zB "28"
  return tagString;
 }
 public String getMonthText() { //Übergibt den Monat als String zB "06"
  attributeUpdate();
  return monatString; 
 }
 public String getYearText() {  //Übergibt das Jahr als String zB "1999"
  attributeUpdate();
  return jahrString;
 }

 public Calendar getDate() {    // **** get-Methode für Calendar Objekte
  attributeUpdate(); // Übergibt Inhalt des Textfeldes als Calendar Objekt
  Calendar aktDate = new GregorianCalendar(tagInt, monatInt, jahrInt);
  return aktDate;    //hier neues kalenderobjekt erzeugen
 }
 //****** GET METHODEN Ende *

 //****** Zusatz METHODEN ***
 private void attributeUpdate() { // MUSS vor Rückgabe in 
                                  // get-Methoden aufgerufen werden
  tagString = this.getText().substring(0, 2);  // Achtung: Wert kann ungültig
  monatString = this.getText().substring(3, 5);// sein. zB 32 für den Tag.
  jahrString = this.getText().substring(6);

  tagInt = new Integer(tagString).intValue(); 
  monatInt = new Integer(monatString).intValue(); 
  jahrInt = new Integer(jahrString).intValue(); 
 }
 public boolean hasZeroField() {    // Methode, die prüft, ob ein Feld
  boolean hasZero=false;            // "00" enthält.
  attributeUpdate();
  if (tagString.equals("00")) hasZero=true;
  if (monatString.equals("00")) hasZero=true; // Bei Jahr ist "0000" erlaubt
  return hasZero; 
 }
 public boolean hasNoZeroField() {   // Methode, die prüft, ob ein Feld
  boolean hasZero=true;              // nicht "00" enthält.
  attributeUpdate();
  if (tagString.equals("00")) hasZero=false;
  if (monatString.equals("00")) hasZero=false; // Bei Jahr ist "0000" erlaubt
  return hasZero; 
 }

 static boolean isDate(int tag, int monat, int jahr) { // Klassenmethode !!!!
  boolean itIsADate=true;
  GregorianCalendar testDate = new GregorianCalendar(jahr, monat, tag);
  for(int i = 0; i < tageMax.length ; i++) { //schauen ob maximaler tag im
   if(monat==i) {                            //monat überschritten ist
    if(tag>tageMax[i]) itIsADate=false;
   }
  }
  if(testDate.isLeapYear(jahr)) {            //bei schaltjahr auch 29 feb.
   if ((monat==1) && (tag==29)) itIsADate=true;
  }
  if(monat>11) itIsADate=false;       //wenn monat größer 12 dann falsch
  if (tag==0) itIsADate=false;        // wenn tag = 0 dann falsch
  if ((monat+1)==0) itIsADate=false;  // wenn monat = 0 dann falsch
  return itIsADate;
 }
 static boolean isDate(String checkString) {  // Klassenmethode !!!!!
  boolean itIsADate=false;
  if(checkString.length()==10) {
   int tag = new Integer(checkString.substring(0, 2)).intValue();
   int monat = new Integer(checkString.substring(3, 5)).intValue()-1;
   int jahr = new Integer(checkString.substring(0, 2)).intValue();
   if(isDate(tag, monat, jahr)==true) itIsADate=true;
  }
  return itIsADate;
 }
}

Wow. Ziemlich viel. Aber mit dem bisher gelernten müßten Sie die Struktur eigentlich auch schon beim Überfliegen verstehen. Die import-Anweisungen und die Signatur dürften inzwischen klar sein:

import java.util.*;    // Fuer Calendar Objekte
import javax.swing.*;  // Weil JTextField erweitert wird

public class JDateField extends JTextField { // **** Beginn Klasse JDateField

Hier wird util für Calendar-Objekte eingebunden und Swing, wegen der Erweiterung des JTextField. Unsere Klasse erweitert dieses schließlich. Als nächstes werden die Attribute deklariert, die zusätzlich zu denen der Klasse JTextField bereitstehen:

 private Calendar myDates = new GregorianCalendar(); // Calender fuers init.
 private int tagInt = myDates.get(Calendar.DATE);    //Felder, die Teil-
 private int monatInt = myDates.get(Calendar.DATE);  //strings des gesamten
 private int jahrInt = myDates.get(Calendar.DATE);   //TextFeldes enthalten, 
 private String tagString = String.valueOf(tagInt);  //bzw. die ent-
 private String monatString = String.valueOf(monatInt); //sprechenden Int- 
 private String jahrString = String.valueOf(jahrInt);//Werte dazu
 // zur Datumsgültigkeit:
 private static final int[] tageMax = {31,28,31,30,31,30,31,31,30,31,30,31};

Es handelt sich natürlich überwiegend um Datums-spezifische Attribute. Zunächst ein Calendar-Objekt für die Initialisierung. Dann folgen drei int-Werte und drei Strings. Diese nehmen, ähnlich wie der Calendar das Datum aus unserem JDateField in drei einzelnen Feldern auf. Schließlich wird noch ein Array deklariert, welches die Anzahl der Tage pro Monat aufnimmt, und bei der Methode zur Prüfung der Gültigkeit von Daten verwendet wird.
Nun folgt wieder eine riesige Anzahl von Konstruktoren. Diese widmen sich speziell der Initialisierung des Feldes mit einem Datum. Der Aufbau ist überall ähnlich. Sehen wir uns den ersten Konstruktor an:

public JDateField() {              // Kein Para (Def. Datum: Heute)
  super();                          // Wie gehabt
  this.setDocument(new DateFieldDocument(this));  // Passendes Dokument
 }

Dies ist der Konstruktor ohne Argumente. Er ruft einfach den Konstruktor von JTextField auf (mit super()). Anschließend setzt er für das hier vorliegende JDateField das DateFieldDocument ein, und übergibt eine Referenz auf sich selbst. Dieses Setzen des Dokuments kommt in allen Konstruktoren vor. Damit muß derjenige, der das JDateField in seinem Programm verwendet, sich nicht selbst um das Dokument kümmern, sondern kann das JDateField genauso einfach verwenden, wie ein JTextField. Durch das Setzen des Dokumentes, ist das JDateField auch automatisch mit dem heutigen Datum initialisiert, da dies ja standardmäßig vom Konstruktor des DateFieldDocumenteingesetzt wird, wenn nichts anderes angegeben ist.
Die folgenden Konstruktoren verfahren nach dem gleichen Schema, nur daß sie Argumente entgegennehmen, die sie in der Regel ungefiltert an die Konstruktoren der Elternklasse JTextField durchreichen. Ich erläutere hier nur noch die Konstruktoren, in denen etwas Besonderes zu bemerken ist. Dazu gehört der Konstruktor, dem ein Calendar-Objekt übergeben wird.

 public JDateField(Calendar myDate) {   // SpezialConstructor des Documents
  super();                              // Wie gehabt
  this.setDocument(new DateFieldDocument(this, myDate)); // Passendes Dokum.
 }

Hier wird zunächst auch der Standardkonstruktor von JTextField aufgerufen. Eine Behandlung des übergebenen Calendar-Objektes findet nicht statt. Stattdessen wird dieses Objekt einfach an den Konstruktor von DateFieldDocument durchgereicht, welches ja mit diesem Argument umgehen kann, und dann eben auf das angegebene Datum initialisiert.
Der folgende Konstruktor, der drei int-Werte zum Initialisieren entgegennimmt funktioniert ähnlich:

 public JDateField(int tag, int monat, int jahr) { // Mit Ints fürs Datum
  super();                                         // Wie gehabt
  this.setDocument(new DateFieldDocument(this,     // Passendes Dokument
         new GregorianCalendar(jahr, monat, tag)));
 }

Hier werden bei dem Aufruf des Konstruktors des DateFieldDocument, die drei Werte eben nur an Ort und Stelle in ein Calendar-Objekt überführt, welches ja mit int-Werten initialisierbar ist.
Die Konstruktoren machen also nichts weiter, als die Daten an die Konstruktoren des Elternobjektes JTextField oder des Dokumentmodells DateFieldDocument durchzureichen. Die hohe Anzahl der Konstruktoren ergibt sich nur daraus, daß das JDateField flexibel sein soll, und alle möglichen Datums-spezifischen Aufrufe bereitstellen soll (obwohl eDays selbst ja später nur eine einzige Variante verwendet). Wenn wir Javaklassen schreiben, sollten wir immer im Hinterkopf überlegen, ob die Klasse an der wir gerade arbeiten später vielleicht in einem anderen Projekt gut wiederzuverwenden wäre. Wenn ja, dann sollte man durchaus direkt beim Programmieren den Aufwand betreiben, und die Klasse gleich flexibel gestalten.
Ein Nachteil der hier gezeigten Konstruktoren ist, daß Argumente kommentarlos an die Konstruktoren der anderen Klassen durchgereicht werden. Eine Verbesserungsmöglichkeit wäre vorher zu prüfen, ob die Argumente sinnvoll sind, also ein Datum zum Beispiel gültig ist.
Die erweiterten Zugriffsmethoden beziehen sich natürlich auch auf die Datum's (ich liebe diesen Ausdruck *g*). Am einfachsten sind die set-Methoden für ganze Daten zu handhaben. Wobei mir gleich in der ersten Methode ein Bug (Fehler) auffällt:

 public void setDate(Calendar myNewDate) {   // Übergabe eines Calender
  String myTagString=String.valueOf(myNewDate.get(Calendar.DATE));
  String myMonatString=String.valueOf(myNewDate.get(Calendar.MONTH)+1);
  String myJahrString=String.valueOf(myNewDate.get(Calendar.YEAR)); 
  this.setText(myTagString+"."+myMonatString+"."+myJahrString);
 }

Theoretisch ist alles in Ordnung. Als Argument wird ein Calendar-Objekt übergeben, und in einen String verwandelt. Wegen unseres Dokumentmodells, welches die Trennpunkte im Datum an festen Positionen verwaltet, müßen wir aber darauf achten, daß ein Datumsstring immer 10 Stellen lang ist. Das ist hier nicht gewährleistet, weil unter Umständen der String "4.1.2001" anstatt "04.01.2001" entstehen kann. Hier wäre somit die Verwendung des SimpleDateFormat angebracht. Bessern Sie das ruhig selber nach (nein, ich bin nicht zu faul, ich besser das bei mir auch nach *g*). Ansonsten dürfte die Methode klar sein.
Die folgende set-Methode ist kurz und knapp:

 public void setDate(String myDateText) {    // Übergabe eines String
                                             // zB "11.12.2000"
  if(isDate(myDateText)==true)
   this.setText(myDateText); 
 }

Hier wird ein String im richtigen Format erwartet. Mit der später erläuterten Methode isDate() wird festgestellt, ob es sich um ein gültiges Datum handelt. Wenn ja, wird das Datum im Feld eingesetzt. Aufgrund der Implementation der Methode isDate() kann hier auch nicht der eben genannte Bug auftreten. Bei der nächsten set-Methode sieht man sehr schön, wie ich mich bemühe, das zehnstellige Datumsformat zu erhalten:

 public void setDate(int day, int month, int year) {   // Übergabe 
  String myTagString=String.valueOf(day);              // dreier int Werte 
  if(day<10) myTagString="0"+String.valueOf(day);
  String myMonatString=String.valueOf(month);
  if(month<10) myMonatString="0"+String.valueOf(month);
  String myJahrString=String.valueOf(year);
  if(year<10) myJahrString="000"+String.valueOf(year);
  if(year<100) myJahrString="00"+String.valueOf(year);
  if(year<1000) myJahrString="0"+String.valueOf(year);
  this.setText(myTagString+"."+myMonatString+"."+myJahrString);
 }

Hier werden int-Werte in den Datumsstring verwandelt. Ist ein int-Wert nicht vierstellig, wird er jeweils mit sovielen Nullen aufgefüllt, daß er den Kriterien entspricht.
Bei den Methoden, bei denen nur ein Feld mittels int-Wert gesetzt werden kann, muß man ein wenig tricksen:

 public void setDay(int day) {     // **** set-Methoden für Int Werte
  attributeUpdate();               // setzt den Tag nach Übergabe eines int 
  String myTagString=String.valueOf(day);
  if(day<10) myTagString="0"+String.valueOf(day);
  this.setText(myTagString+"."+monatString+"."+jahrString);
 }

Im großen und ganzen entspricht die Verarbeitung der eben gezeigten bei Übergabe dreier int-Werte. Diesmal halt nur auf ein einzelnes Feld bezogen. Wir erinnern uns aber, daß unser Dokumentmodell nur 1-stellige oder 10-stellige Eingaben entgegennimmt. Das Setzen eines Tages übergibt aber z.B. einen zweistelligen String. Mit der später erläuterten Methode attributeUpdate() werden unsere Attribute so eingestellt, wie das JDateField gerade belegt ist. Diese Methode muß aufgerufen werden, da sonst nicht sichergestellt ist, daß die Attribute für Monat und Jahr dem gerade angezeigten Wert entsprechen. Nun wird der übergebene Tag in einen String gewandelt, und zusammen mit den anderen Attributen wird ein zehnstelliger String gebastelt, der den neuen Tag enthält. Dieser wird dann übergeben. So können wir mit dem JDateField auch einzelene Felder ändern, ohne durch die zehnstellige Beschränkung des Modells behindert zu sein. Genauso werden auch die set-Methoden für den Monat und das Jahr implementiert.
Methoden zur Übergabe von int-Werten an das JDateFieldsind schon ganz praktisch. Aber da das JDateField ja von einem Textfeld abstammt, wäre auch die klassische Übergabe in Form eines Strings nicht schlecht. Dazu dienen die folgenden drei set-Methoden für die Felder des Datums, die alle gleich aufgebaut sind:

 public void setDayText(String day) {// **** set-Methoden für String Werte
  attributeUpdate();                // Übergabe eines String tag
  String myTagString=day;
  if(day.length()<2) myTagString="0"+day;
  this.setText(myTagString+"."+monatString+"."+jahrString);
 }

Der oben genannte Fehler tritt hier übrigens nicht auf, da immer auf die richtige Anzahl von Stellen des String geachtet wird. Diese set-Methoden sind eigentlich selbsterklärend und bedürfen keiner weiteren Erläuterung.
Die folgenden get-Methoden sind, wie das für get-Methoden so oft üblich ist, ganz simpel gestrickt. Zunächst lassen sich einzelne Felder als int-Wert abfragen:

 public int getDay() {   // **** get-Methoden für Int Werte
  attributeUpdate();     // Übergibt den Tag als Int zB 28
  return tagInt;
 }

Bei allen get-Methoden wird zunächst sicherheitshalber die MethodeattributeUpdate()aufgerufen, damit die Attribute der Klasse auch dem Dargestellten entsprechen. Dann wird einfach das entsprechende Attribut zurückgeliefert. Dies trifft auf die get-Methoden für int-Werte genauso zu, wie für die get-Methoden die String-Entsprechungen der einzelnen Felder zurückliefern.
Zusätzlich habe ich eine get-methode implementiert, die den Inhalt des JDateField als Calendar-Objekt zurückgibt:

 public Calendar getDate() {    // **** get-Methode für Calendar Objekte
  attributeUpdate(); // Übergibt Inhalt des Textfeldes als Calendar Objekt
  Calendar aktDate = new GregorianCalendar(tagInt, monatInt, jahrInt);
  return aktDate;    //hier neues kalenderobjekt erzeugen
 }

Dabei wird das Calendar-Objekt einfach aus den int-Attributen der Klasse erzeugt. Möglicherweise sollte diese Methode besser getCalendar() heißen, und die getDate-Methode ein Date-Objekt zurückgeben. Dies ist vielleicht eine Verbesserungsmöglichkeit.
Das wars mit den Zugriffsmethoden, die aber wohl fast jeden gewünschten Zugriff abdecken sollten. Vielleicht vermissen Sie eine get-Methode, die das gesamte Datum als String zurückgibt? Wenn Sie sich richtig erinnern, ist unser JDateField ja von JTextField abgeleitet, und JTextField gibt den gesamten Inhalt mit der Methode getText()zurück. Diese Methode funktioniert natürlich weiterhin, so daß wir hierfür keine extra Methode implementieren müssen.
Es folgen nun einige Zusatzmethoden. Die erste ist zwingend für unser JDateField, die anderen könnten evtl. auch in einer anderen Klasse implementiert werden. Bei der ersten handelt es sich um die schon erwähnte Methode attributeUpdate():

 private void attributeUpdate() { // MUSS vor Rückgabe in 
                                  // get-Methoden aufgerufen werden
  tagString = this.getText().substring(0, 2);  // Achtung: Wert kann ungültig
  monatString = this.getText().substring(3, 5);// sein. zB 32 für den Tag.
  jahrString = this.getText().substring(6);

  tagInt = new Integer(tagString).intValue(); 
  monatInt = new Integer(monatString).intValue(); 
  jahrInt = new Integer(jahrString).intValue(); 
 }

Sie sorgt dafür, daß die Attribute des JDateField aktualisiert werden, je nach dem was in dem Feld zur Zeit eingegeben ist. Dazu setzt sie zuerst die String-Attribute der Klasse durch extrahieren der einzelnen Werte aus dem dargestellten Gesamtstring. Aus den String-Attributen leitet sie schließlich die int-Attribute ab. Die Methode dürfte verständlich sein.
Es folgen zwei Zusatzmethoden, die man als Klassenmethode hätte implementieren können. In diesem Fall habe ich sie aber so implementiert, daß sie sich auf eine konkrete Instanz des JDateField beziehen. Sie prüfen, ob ein Feld mit "00" belegt ist. Wenn dies der Fall ist, ist ein Datum ungültig:

public boolean hasZeroField() {    // Methode, die prüft, ob ein Feld
  boolean hasZero=false;            // "00" enthält.
  attributeUpdate();
  if (tagString.equals("00")) hasZero=true;
  if (monatString.equals("00")) hasZero=true; // Bei Jahr ist "0000" erlaubt
  return hasZero; 
 }

Hier wird eben einfach auf die entsprechenden Strings geprüft. Das Jahr wird dabei nicht berücksichtigt, da das Jahr 0 ja in einem regulären Datum auftauchen darf. Je nach Ergebnis wird ein entsprechender Wahrheitswert zurückgegeben. Die im Listing folgende Methode macht das gleiche, liefert aber true zurück, wenn kein Nullfeld enthalten ist. Dies ist bei manchen Abfragen einfach praktischer zu handhaben. Eigentlich könnte ich mir die Methoden auch schenken, da das Dokumentmodell bereits die 00-Felder sperrt. Die Wahrheit ist, daß ich das Modell erst später entsprechend erweitert habe, und diese beiden Methoden hier einfach noch übriggeblieben sind.
Die nächste Methode ist aber eine sehr wichtige, und sehr schöne. Und bei ihr handelt es sich um eine Klassenmethode. Eine Klassenmethode ist eine Methode, die ich nicht über eine Instanz aufrufe, sondern direkt mit ihrem Klassennamen. Die Methode hasZeroField() würde ich zum Beispiel immer mit myField.hasZeroField() aufrufen und niemals mit JDateField.hasZeroField(). Eine Klassenmethode, wie die folgende, rufe ich aber immer mit JDateField.isDate(Datum)auf. Ich brauche zum Verwenden der Methode noch nicht einmal eine Instanz der Klasse. Mit Klassenmethoden realisiert man so etwas wie eine Funktionsbibliothek. Sie haben selbst schon häufig Klassenmethoden verwendet. z.B wenn Sie einen int-Wert in einen String gewandelt haben, haben Sie das KonstruktString.valueOf(year) verwendet. valueOf() ist dabei eine Klassenmethode der Klasse String (wie man hier auch sehr schön sieht).
Zurück zu unserer Klassenmethode. Eigentlich sind es zwei, aber die zweite baut auf der ersten auf: Das Calendar-Objekt hatte ja die Eigenschaft, wie vorher schon erwähnt, daß es nicht meckert, wenn man ihm ein unmögliches Datum (31.02.20001) übergibt, sondern die ihm übergebenen Daten schon irgendwie in seine Felder presst. Mein Modell kann ja nur schwach validieren. Aber damit korrekte Daten von Edyas ausgerechnet werden, brauche ich schon ein Methode, die mir klipp und klar sagt, ob das eingegebene Datum so gültig, oder ungültig ist. Erstmal der Code:

static boolean isDate(int tag, int monat, int jahr) { // Klassenmethode !!!!
  boolean itIsADate=true;
  GregorianCalendar testDate = new GregorianCalendar(jahr, monat, tag);
  for(int i = 0; i < tageMax.length ; i++) { //schauen ob maximaler tag im
   if(monat==i) {                            //monat überschritten ist
    if(tag>tageMax[i]) itIsADate=false;
   }
  }
  if(testDate.isLeapYear(jahr)) {            //bei schaltjahr auch 29 feb.
   if ((monat==1) && (tag==29)) itIsADate=true;
  }
  if(monat>11) itIsADate=false;       //wenn monat größer 12 dann falsch
  if (tag==0) itIsADate=false;        // wenn tag = 0 dann falsch
  if ((monat+1)==0) itIsADate=false;  // wenn monat = 0 dann falsch
  return itIsADate;
 }

Die Methode ist als static deklariert, was bei einer Klassenmethode immer so ist. Nur static deklarierte Methoden können direkt über ihre Klasse aufgerufen werden. Die Methode gibt einen boolschen Wert zurück, womit sie sich gut für if-Abfragen eignet. Als Argument erwartet die Methode das Datum in Form dreier int-Werte.
Dann wird der spätere itIsADate Rückgabewert erstmal auf true gesetzt. Für eine der folgenden Abfragen benötigen wir auch ein Calendar-Objekt. Dies erzeugen wir auch gleich. Die nun folgenden Abfragen testen jeweils darauf, ob ein Kriterium, welches ein gültiges Datum erfüllen muß, nicht erfüllt ist. Ist es nicht erfüllt, dann wird itIsADate auf false gesetzt.
Wir hatten bei den Attributen ein Array deklariert, und mit Werten für die maximale Zahl der Tage im Monat gefüllt. Eine Schleife zählt nun zur Anzahl der Elemente hoch. Entspricht der Zähler unserem Monat, wird in das Array an der entsprechenden Stelle geschaut, welche Anzahl von Tagen ein Datum dieses Monats maximal haben dürfte. Ist der Tag größer, dann wird der Rückgabewert auf falsegesetzt.
In einem Fall ist diese Abfrage zu ungenau, nämlich im Schaltjahr. Daher wird folgend noch einmal mithilfe des Calendar-Objektes geprüft, ob das eingegebene Datum ein Schaltjahr (wie z.B. 2000) ist. Da dann auch der 29.02.2000 gültig wäre, wird itIsADate "wieder" auf true gesetzt, falls es sich um den 29.sten Tag handelt (die vorherige Methode hätte ja in diesem Fall itIsADate auf false gesetzt). Das Calendar-Objekt bringt freundlicherweise eine Methode isLeapYear() mit, die uns mitteilt, ob es sich bei einem gegebenen Jahr um ein Schaltjahr handelt (geht doch, warum nicht auch eine Differenzfunktion im Calendar-Objekt?).
Nun wird noch einmal (man merkt, ich geh auf Nummer sicher) geprüft, ob der Monat nicht größer als 12 oder Tag und Monat gleich Null sind (was das Datum auch ungültig macht). Hat das übergebene Datum all diese Abfragen überstanden, wird itIsADate zurückgegeben. Diese Methode sollte eigentlich alle Eventualitäten abdecken.
Die zweite Klassenmethode isDate() unetrscheidet sich nur dadurch von der ersten Klassenmethode, daß sie mit einem zehnstelligen String aufgerufen wird, diesen in int-Werte zerlegt, und die eben beschriebene Methode aufruft um festzustellen, ob es sich um ein gültiges Datum handelt:

static boolean isDate(String checkString) {  // Klassenmethode !!!!!
  boolean itIsADate=false;
  if(checkString.length()==10) {
   int tag = new Integer(checkString.substring(0, 2)).intValue();
   int monat = new Integer(checkString.substring(3, 5)).intValue()-1;
   int jahr = new Integer(checkString.substring(0, 2)).intValue();
   if(isDate(tag, monat, jahr)==true) itIsADate=true;
  }
  return itIsADate;
 }

Da kann man sehen, wie man die erste Klassenmethode aufruft. Außerdem können Sie im DateFieldDocument nachsehen, wie man diese Klassenmethode aufruft, da das Modell diese wie vorhin erwähnt, auch nutzt. Daraus ergibt sich, daß JDateFieldund DateFieldDocument eng miteinander verbunden sind, und eigentlich nur zusammen verwendet werden können.
Man kann sich natürlich fragen, ob diese Klassenmethode gerade bei einer grafischen Komponentenklasse von Swing am besten aufgehoben ist. Ist sie ggf. nicht. Aus verschiedenen Bereichen von Java, oder Klassen von Drittanbietern kennt man inzwischen das Konzept der "Factory".  Möglicherweise ist es am besten Methoden wie DateDifferenceInDays()und auch isDate() in eine derartige Klasse auszulagern. Aber so wie wir es hier gemacht haben funktioniert es auch sehr gut und bedarf keiner unmittelbaren Änderung.
Damit ist nun auch die Klasse JDateFieldin dieser Version fertig. Wie ich schon sagte, gibt es noch zahlreiche Verbesserungsmöglichkeiten: Überarbeitung der Konstruktoren, Internationalisierung, besseres Verhalten bzgl. der Spaltenbreite und so weiter und so fort. Aber für das Programm Edays, welches wir ja gerade entwickeln, ist genau dieses JDateField vollkommen ausreichend.

4.5.2. JDateField testen
Um JDateField zu testen ist nun wirklich nicht mehr sehr viel nötig. Wir nehmen einfach das Programm SwingZwei und erzeugen eben kein JTextField sondern ein JDateField. Ich zeige hier noch den Source dafür, aber keinen Screenshot, da dieser genauso aussehen würde wie bei dem Test des DateFieldDocument:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class SwingDrei extends JFrame {

 public SwingDrei() {                                 // *** Konstruktor
  super("SwingDrei");                                 // Titel

  JLabel myHeader = new JLabel("Hier Text eingeben:");//Labelkomponente
  JDateField eingabeFeld = new JDateField(20);        //Datumsfeld-Komponente
  JButton klicker = new JButton("Beenden");           //Button-Komponente

  ActionListener myListener = new ActionListener() {  // Fuer den Button
   public void actionPerformed(ActionEvent e) {       // in anonymer Klasse
    dispose();                                        // (siehe auch WinV)
    System.exit(0);
   }
  };

  klicker.addActionListener(myListener);             // Listener registrieren

  JPanel myContainer = new JPanel();                 // spezieller Container
  myContainer.setLayout(new BorderLayout(5,5));      // Layout einsetzen

  myContainer.add(myHeader, BorderLayout.NORTH);     // Die Komponenten dem
  myContainer.add(eingabeFeld, BorderLayout.CENTER); // Container hinzufügen
  myContainer.add(klicker, BorderLayout.SOUTH);

  getContentPane().add(myContainer);                 // Container dem Frame
                                                     // hinzufügen
  this.enableEvents(AWTEvent.WINDOW_EVENT_MASK);     // Ereignisse ermöglichen
 }                                                   // *** Ende Konstruktor

 protected void processWindowEvent(WindowEvent e) {  // Fensterereignisse
         if (e.getID()==WindowEvent.WINDOW_CLOSING) {// behandeln
   dispose();                      // Ressourcen des Fensters freigeben
   System.exit(0);                 // Programm beenden
  }
 }                                 // Ende Methode processWindowEvent

 public static void main(String[] arg) {             //Hauptmethode
        SwingDrei mySwingapp = new SwingDrei();
        mySwingapp.pack();
        mySwingapp.show();
 }
}

Ein richtiger Test ist es noch nicht, da wir ja keine der Methoden oder Konstruktoren prüfen, aber es zeigt, wie einfach ein JDateField zu verwenden ist. Genauso einfach wie ein JTextField. Und die Zeile aus SwingDrei.java in der wir den Dokumenttyp explizit zugewiesen haben, konnten wir uns auch sparen. Eigentlich sieht es mehr aus wie SwingEins.java, nur das die "Strings" JTextField gegen JDateField ausgetauscht wurden. Und das Ergebnis? Das neue Textfeld reagiert wirklich nur auf Daten. Um zu zeigen, daß dies auch in einer wirklichen praktischen Anwendung stark zum Tragen kommt, widmen wir uns nun der letzten Klasse, dem Hauptprogramm.

4.6. Mehr Swing
Ich habe das hier mit "Mehr Swing" übertitelt, weil es jetzt ein langes Listing gibt, in dem etwa 80% Swing relevant sind. Auf der anderen Seite wird Ihnen das meiste bekannt vorkommen, weil das ganze Programm zum überwiegenden Teil aus dem Aufbau der grafischen Oberfläche besteht. Und da dies ähnlich geschieht wie unter AWT sollte das alles nichts Neues sein.
Im AWT Kapitel habe ich ab und zu davon gesprochen, daß man Container schachtelt um bestimmte Layouts zu erzielen. Genau das werden wir jetzt tun. Bisher hatten wir nie mehr als 3 Komponenten in einem Fenster. In diesen Fällen kommt man gut mit dem BorderLayout zurecht. Werden es jedoch mehr Komponenten, dann wird es eng, und es wird Zeit zu schachteln. Ganz am Anfang habe ich beschrieben, was Edays alles anzeigen soll. Das war nicht wenig. Ich zeige Ihnen erst einmal einen Screenshot von Edays, damit Sie sich schon einmal überlegen können, wie man schachtelt, und welche LayoutManager man verwendet um dieses Erscheinungsbild hinzubekommen:

Screenshot von Edays

So, das ist gar nicht so trivial. Schon bei einer so kleinen Oberfläche muß man sich gehörig den Kopf zerbrechen, wie man die LayoutManager einsetzt. Immerhin haben wir 12 Label-Komponenten, ein JDateField (*g*) einen Button, und zwei Trennerlinien, die wir verteilen müßen. Ich habe dazu jetzt noch eine schematische Darstellung der Oberfläche von Edays vorbereitet, mit der man die Möglichkeiten einmal durchspielen kann. Edays Layout SchemaIch habe hier einfach mal die Schachtelung vorgegeben. Dabei habe ich die Ebenen der Schachtelung farbig markiert. Gelb ist der Frame selbst. Grün sind darin enthaltene Container und Lila sind die in Containern enthaltenen Container. Auf den ersten Blick sieht es so aus, als ob man alles mit GridLayouts erschlagen könnte. Ich habe das getestet, und man kann es nicht. Wer das nicht glaubt, soll sich mal die Seiteneffekte ansehen, wenn er das folgende Listing entsprechend ändert, und alle Container auf GridLayouts setzt. Die Fehler treten deswegen auf, weil jedes Element im Container gleichviel Fläche erhält, wie das größte Element. Innerhalb des mittleren grünen Containers würden bei einem dreizeiligen und einspaltigen GridLayout das mittlere und untere Element gleichviel Platz bekommen wie das obere Element. Ist auch der Frame als Gridlayout definiert, so wird das ganze Fenster höher als 1024 Pixel, was schon den Screen des einen oder anderen sprengt. Ab davon sieht es Scheiße aus. Bitte probiert es einfach einmal aus.
Ich habe mich, wie gesagt anders entschieden. Container die nicht mehr als eine Spalte und nicht mehr als drei Zeilen haben, machen sich immer gut für das BorderLayout. Nur Container, die mehr als eine Spalte haben sollte man mit dem GridLayout testen. Und so ergibt sich, daß der hier gelb dargestellte Bereich ein BorderLayout mit besetzten Komponenten in NORTH, CENTER und EAST ist. Für den mittleren grünen Bereich gilt das gleiche.
Der obere grüne Bereich mit der Überschrift und der gelb dargestellten Linie erhält auch ein BorderLayout. Der obere lila Bereich erhält aufgrund der Tatsache, daß mehr als zwei Spalten beteiligt sind ein GridLayout. Der mittlere lila Bereich kommt dahingegen wieder mit einem BorderLayout aus. Der untere lila Bereich wegen seiner beiden Spalten, braucht wieder ein GridLayout.
Der untere grüne Bereich soll flüssig linksbündig angeordnet werden. Daher hat er von mir ein FlowLayout erhalten. Wen die Layout-Begriffe jetzt irritieren, der sieht noch mal kurz zu den Beispielgrafikenin meinem AWT Kapitel, und zieht die API-Dokumentation zu Rate. Und wer nicht glaubt, daß das die beste Lösung für unser Programm ist, der ist angehalten mit den Layouts einfach rumzuspielen. Es sind nur wenig Änderungen im Quellcode erforderlich (an der Stelle, an der die Panels deklariert werden, und die Layouts zugewiesen werden) um mal das eine oder andere auszuprobieren. Wer eine effektivere Layout-Anordnung findet, kann mir das gerne mailen.. Dieser kurze Abriß diente jetzt erstmal nur dazu, zu erklären, welche Komponenten eigentlich auftauchen, und wie sie angeordnet sind. Als nächstes folgt die Klasse Edays.

4.6.1. Klasse 4: Edays.java
Edays ist wie schon gesagt die Hauptklasse, die das Fenster definiert, und die main-Methode besitzt. Die Komponentenaufteilung erfolgt nach dem eben erläuterten Schema. Daher erstmal der lange Quelltext von Edays.java:

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import javax.swing.*;
import javax.swing.text.*;

public class Edays extends JFrame {
 // ***** Erstmal die Komponenten definieren, auf die zugegriffen wird
 JDateField eingabeFeld;
 JLabel ausgabeLabel;
 JLabel gueltigLabel;
 JLabel stichtagLabel;
 JLabel heuteLabel;
 JLabel statusLabel;

 Timer myTimer;

 Keymap myKeymap;
 KeyStroke myStroke;

 Calendar meinDatum = new GregorianCalendar();  //Daten die im Ablauf
 Calendar stichtag = new GregorianCalendar();   // benötigt werden
 Calendar heute = new GregorianCalendar();
 SimpleDateFormat datumsFormat = new SimpleDateFormat ("dd.MM.yyyy");
 DateDifferenceInDays myDate = new DateDifferenceInDays( //DifferenzObjekt
   meinDatum.get(Calendar.DATE)-1,
   meinDatum.get(Calendar.MONTH)-1,           meinDatum.get(Calendar.YEAR));

 public Edays() {     // **** Konstruktor
  super("eDays 1.2");

  getContentPane().setLayout(new BorderLayout(5,5)); //5 Pixel Abstand zw.
                                                     // Komponenten

  // Zwei Seperators
  JSeparator myUpperLine = new JSeparator();
  JSeparator myCenterLine = new JSeparator();

  // Ueberschrift
  JLabel myTitle = new JLabel("eighty Days",JLabel.CENTER); //Alle Elemente
  myTitle.setFont(new Font("Helvetica",Font.BOLD,20));      //initialisieren
  myTitle.setForeground(Color.black);

  // meinDatum Beschriftung
  JLabel myDateFieldLabel = new JLabel("Datum:   ",JLabel.RIGHT);
  myDateFieldLabel.setFont(new Font("Helvetica",Font.BOLD,12));
  myDateFieldLabel.setForeground(Color.black);

  // Alter Beschriftung
  JLabel myErgebnisLabel = new JLabel("Alter in Tagen:   ",JLabel.RIGHT);
  myErgebnisLabel.setFont(new Font("Helvetica",Font.BOLD,12));
  myErgebnisLabel.setForeground(Color.black);

  // Gültigkeits Beschriftung
  JLabel myGueltigLabel = new JLabel("Stichtag dazu:   ",JLabel.RIGHT);
  myGueltigLabel.setFont(new Font("Helvetica",Font.BOLD,12));
  myGueltigLabel.setForeground(Color.black);

  // Gültigkeit für das konkrete Datum Beschriftung
  JLabel myStichtagLabel = new JLabel("Stichtag dazu:   ",JLabel.RIGHT);
  myStichtagLabel.setFont(new Font("Helvetica",Font.BOLD,12));
  myStichtagLabel.setForeground(Color.black);

  // Heutiges Datum Beschriftung
  JLabel myHeuteLabel = new JLabel("Datum heute:   ",JLabel.RIGHT);
  myHeuteLabel.setFont(new Font("Helvetica",Font.BOLD,12));
  myHeuteLabel.setForeground(Color.black);

  // Statusfeld Beschriftung
  JLabel myStatusLabel = new JLabel("Status: ",JLabel.CENTER);
  myStatusLabel.setFont(new Font("Helvetica",Font.BOLD,12));
  myStatusLabel.setForeground(Color.black);

  // Eingabefeld Datum
  eingabeFeld = new JDateField(8); //Verwendet meine Version von DateField
  eingabeFeld.setHorizontalAlignment(JTextField.LEFT);
  eingabeFeld.setFont(new Font("Helvetica",Font.BOLD,14));

  // Fuer das Datumsfeld die Returntaste manipulieren, so das [return] 
  // die Aktion des buttons auslöst
  myKeymap = eingabeFeld.getKeymap();
  myStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); 
  myKeymap.removeKeyStrokeBinding(myStroke);

  // Ausgabe Feld fuer die Differenz
  ausgabeLabel = new JLabel("00000",JLabel.LEFT);
  ausgabeLabel.setForeground(Color.black);
  ausgabeLabel.setFont(new Font("Helvetica",Font.BOLD,24));

  // Ausgabe Feld fuer das noch gültige Datum
  stichtag.add(Calendar.DATE, -80);
  String dateString = datumsFormat.format(stichtag.getTime());
  gueltigLabel = new JLabel(dateString, JLabel.LEFT);
  gueltigLabel.setForeground(Color.black);
  gueltigLabel.setFont(new Font("Helvetica",Font.BOLD,12));

  // Ausgabe Feld fuer den Stichtag zum aktuellen Datum
  stichtag.setTime(new Date());
  stichtag.add(Calendar.DATE, 80);
  dateString = datumsFormat.format(stichtag.getTime());
  stichtagLabel = new JLabel(dateString, JLabel.LEFT);
  stichtagLabel.setForeground(Color.black);
  stichtagLabel.setFont(new Font("Helvetica",Font.BOLD,12));

  // Ausgabe Feld fuer das aktuelle Datum
  dateString = datumsFormat.format(heute.getTime());
  heuteLabel = new JLabel(dateString, JLabel.LEFT);
  heuteLabel.setForeground(Color.black);
  heuteLabel.setFont(new Font("Helvetica",Font.BOLD,12));

  // Ausgabe Feld fuer den Status
  statusLabel = new JLabel("Berechnung durchgeführt",JLabel.LEFT);
  statusLabel.setForeground(Color.black);
  statusLabel.setFont(new Font("Helvetica",Font.BOLD,12));

  // Panels definieren und Layout setzen
  JPanel topPanel = new JPanel();
  JPanel centerPanel = new JPanel();
  JPanel footerPanel = new JPanel();
  JPanel upperGridPanel = new JPanel();
  JPanel middlePanel = new JPanel();
  JPanel lowerGridPanel = new JPanel();

  topPanel.setLayout(new BorderLayout());  //
  centerPanel.setLayout(new BorderLayout());  //3 Pix Abstand zw. Komponenten
  footerPanel.setLayout(new FlowLayout(FlowLayout.LEFT));//alles nach.ander
  upperGridPanel.setLayout(new GridLayout(3,2));         //2 zeilen 3 spalten
  middlePanel.setLayout(new BorderLayout(5,5));          // 5 Pix Abstand
  lowerGridPanel.setLayout(new GridLayout(2,2));
  footerPanel.setBorder(BorderFactory.createLoweredBevelBorder()); //Rahmen

  // Button definieren und eigentliche Programmfunktion einbauen
  JButton berechne = new JButton("Berechnen");      // Ereignis Button-Click
  berechne.addActionListener(new ActionListener() { // in anonymer Klasse
   public void actionPerformed(ActionEvent e) {
    int frist, tag=02, monat=11, jahr=2000;         // Voreinstellung Datum
    boolean noDateError=true;

    if(eingabeFeld.hasNoZeroField()) {              //Prüfen ob nicht leer
     tag = eingabeFeld.getDay();
     monat = eingabeFeld.getMonth()-1;
     jahr = eingabeFeld.getYear();
    }
    else {
     noDateError=false;
     statusLabel.setText("Kein Nullfeld möglich!");
     ausgabeLabel.setText("##");
    }

    meinDatum.set(jahr, monat, tag);         //Calenderobjekte aktualisieren
    heute.setTime(new Date());

    if(!meinDatum.before(heute)) {           //Prüfen ob Datum in der Zukunft
     noDateError=false;
     statusLabel.setText("Datum in der Zukunft!");
     ausgabeLabel.setText("##");
    }
    if(!JDateField.isDate(tag, monat, jahr)) {
     noDateError=false;
     statusLabel.setText("Datum ungültig!");
     ausgabeLabel.setText("##");
    }
    if(noDateError) {
     myDate.setEarly(tag, monat, jahr);
     myDate.setLater(heute.get(Calendar.DATE),
             heute.get(Calendar.MONTH), heute.get(Calendar.YEAR));
     frist=myDate.getDifference();
     ausgabeLabel.setText(String.valueOf(frist));

     //aktuellen Stichtag Updaten
     stichtag.set(jahr, monat, tag);
     stichtag.add(Calendar.DATE, 80);
     String datumsString = datumsFormat.format(stichtag.getTime());
     stichtagLabel.setText(datumsString);
     statusLabel.setText("Berechnung durchgeführt.");
    }
   }
  });
  SwingUtilities.getRootPane(this).setDefaultButton(berechne);

  //Timer für die Anzeige des Stichtages zum heutigen Datum (alle 5 Minuten)
  myTimer = new Timer(300000, new ActionListener() {  //300000 Millisekunden
   public void actionPerformed(ActionEvent evt) {
    stichtag.setTime(new Date());     // *** Stichtag
    stichtag.add(Calendar.DATE, -80);
    String datumsString = datumsFormat.format(stichtag.getTime());
    gueltigLabel.setText(datumsString);
    heute.setTime(new Date());                        // *** Heute
    datumsString = datumsFormat.format(heute.getTime());
    heuteLabel.setText(datumsString);
   } 
  });

  // Aktive Komponenten ins UpperGrid einbauen
  upperGridPanel.add(myDateFieldLabel);
  upperGridPanel.add(eingabeFeld);
  upperGridPanel.add(myErgebnisLabel);
  upperGridPanel.add(ausgabeLabel);
  upperGridPanel.add(myStichtagLabel);
  upperGridPanel.add(stichtagLabel);

  // Berechnen Button und Linie ins Middel Panel
  middlePanel.add(berechne, BorderLayout.NORTH);
  middlePanel.add(myCenterLine, BorderLayout.SOUTH);

  // Gültigkeits Komponenten ins LowerGrid einbauen
  lowerGridPanel.add(myHeuteLabel);
  lowerGridPanel.add(heuteLabel);
  lowerGridPanel.add(myGueltigLabel);
  lowerGridPanel.add(gueltigLabel); 

  // Überschrift ins Toppanel einbauen
  topPanel.add(myTitle, BorderLayout.NORTH);
  topPanel.add(myUpperLine, BorderLayout.SOUTH);

  // Status ins FooterPanel einbauen
  footerPanel.add(myStatusLabel);
  footerPanel.add(statusLabel);

  // Gridpanels ins übergeordnete Center Panel bauen
  centerPanel.add(upperGridPanel, BorderLayout.NORTH);
  centerPanel.add(middlePanel, BorderLayout.CENTER);
  centerPanel.add(lowerGridPanel, BorderLayout.SOUTH);

  // UnterPanels  in den Frame einbauen
  getContentPane().add(topPanel, BorderLayout.NORTH);
  getContentPane().add(centerPanel, BorderLayout.CENTER);
  getContentPane().add(footerPanel, BorderLayout.SOUTH);

  this.enableEvents(AWTEvent.WINDOW_EVENT_MASK); // Schließen ermöglichen
 }

 protected void processWindowEvent(WindowEvent e) { // Überschreiben
                                                    // um zu Beenden
  if (e.getID()==WindowEvent.WINDOW_CLOSING) { 
   dispose();                           // Ressourcen des Fensters freigeben 
   System.exit(0);                      // Programm beenden 
  } 
 } // Ende Methode processWindowEvent() 

 public static void main(String[] arg) {//Hauptmethode
  Edays myEdays = new Edays();
  myEdays.pack();
  myEdays.show();
  myEdays.ausgabeLabel.setText("0");
  myEdays.statusLabel.setText("Datum eingeben");
  myEdays.myTimer.start();
 }
}

Und wieder so ein langer Schinken. Niemand hat gesagt das Programme kurz sind *g*. Wie gesagt befassen sich etwa 80% des Programms mit der GUI (also dem Graphical User Interface, also der Programmoberfläche, also der Art und Weise, wie das Programm aussieht). etwa 18% dürften in die Ereignisbehandlung einfließen und etwa 3% dürfte unser Hauptprogramm beanspruchen. Dabei sieht man, daß bei objektorientierter Programmierung die Objekte ihre Aufgaben selbst wahrnehmen, und das Hauptprogramm nur ein bißchen steuert. Auch wenn es die Reihenfolge des Listings durcheinanderbringt, werden wir bei der üblichen Analyse des Programms diesmal in der Reihenfolge vorgehen, nur GUI, nur Ereignisbehandlung, nur Hauptprogramm.
Voraus schicken wir aber, wie üblich,  eine Erklärung der import-Anweisungen und der Attribute:

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import javax.swing.*;
import javax.swing.text.*;

Zu den import-Anweisungen ist nach der Erklärung der anderen Klassen nicht mehr viel zu sagen. Wir nutzen eben in Edays Klassen aus all diesen Paketen: Calendar, AWT-LayoutManager etc, Ereignisbehandlung nach dem AWT, SimpleDateDokumente und Swingkomponenten sowie Teile der Swing-Textkomponenten.
Die Attribute sind da schon viel interessanter. Variablen, auf die im Laufe des Programms noch zugegriffen wird, deklariert man am besten als Attribute der Klasse:

// ***** Erstmal die Komponenten definieren, auf die zugegriffen wird
 JDateField eingabeFeld;
 JLabel ausgabeLabel;
 JLabel gueltigLabel;
 JLabel stichtagLabel;
 JLabel heuteLabel;
 JLabel statusLabel;

 Timer myTimer;

 Keymap myKeymap;
 KeyStroke myStroke;

 Calendar meinDatum = new GregorianCalendar();  //Daten die im Ablauf
 Calendar stichtag = new GregorianCalendar();   // benötigt werden
 Calendar heute = new GregorianCalendar();
 SimpleDateFormat datumsFormat = new SimpleDateFormat ("dd.MM.yyyy");
 DateDifferenceInDays myDate = new DateDifferenceInDays( //DifferenzObjekt
   meinDatum.get(Calendar.DATE)-1,
   meinDatum.get(Calendar.MONTH)-1,           meinDatum.get(Calendar.YEAR));

Zugegriffen meint dabei, daß die Eigenschaften oder Werte der Variablen geändert werden. dazu zählen veränderliche Komponenten, aber auch Daten die immer wieder verwendet werden. Neu sind für Sie hier die Timer und Key X relevanten Bestandteile, die aber später erklärt werden (nur deklariert werden müßen sie hier).
Übrigens sehen sie hier den ersten Einsatz unseres JDateFieldes. Es wird gleich am Anfang als Instanz eingabeFeld deklariert. Ansonsten deklarieren wir nur im Programmlauf veränderliche Labels. Darauf folgt der ominöse Timer und die Key-map -stroke Deklarationen. Ignorieren Sie das, bis wir bei dem entsprechenden Code angelangt sind.
Daß wir im folgenden noch Calendar-Objekte, sowie SimpleDateFormate deklarieren, dürfte angesichts des Zwecks des Programms nicht verwundern. Und schließlich wird auch eine Instanz unserer Klasse DateDifferenceInDays deklariert und initialisiert, die wir später noch brauchen. Kommen wir nun zum Konstruktor, und der ist bei Erweiterungen eines Frames ja in der Regel GUI-spezifisch.

4.6.2. GUI von Edays
Wie wir es aus unseren kleinen Beispielprogrammen ja schon kennen, werden im Konstruktor die Komponenten eines Fensters deklariert, initialisiert und ggf. auch Ereignisbehandlungen programmiert. Wir beschränken uns zunächst auf die GUI-Definition. Die ersten Zeilen lauten:

  super("eDays 1.2");
  getContentPane().setLayout(new BorderLayout(5,5)); //5 Pixel Abstand zw.
                                                     // Komponenten

Die erste Zeile ist klar, der Konstruktor der Elternklasse wird mit dem Titel des Fensters aufgerufen, der später so erscheint. In der zweiten Zeile weisen wir dem Frame selbst, einen LayoutManager zu. Wie gesagt, muß bei Swing-Programmen dazu zunächst der Verweis auf die JRootPane in Erfahrung gebracht werden, was mit der Methode getContentPane()bewerkstelligt wird. Dieser wird dann der LayoutManagerBorderLayoutmit einem horizontalen und vertikalen Abstand von 5 Pixeln zwischen den Komponenten zugewiesen.
Nun werden für den Rest des Konstruktors, alle verwendeten Komponenten deklariert, oder die als Attribut erzeugten Komponenten werden initialisiert. Dann werden die Komponenten entsprechend in den Containern geschachtelt, und diese wieder im JFrame geschachtelt. Den Anfang macht eine bisher noch nicht angesprochene Komponente:

  // Zwei Seperators
  JSeparator myUpperLine = new JSeparator();
  JSeparator myCenterLine = new JSeparator();

Ein JSeperator ist so ziemlich die einfachste vorstellbare Komponente, und entspricht einfach einer horizontalen oder vertikalen Linie. Diese wird natürlich im entsprechenden Look And Feel gezeichnet, kann also auch pseudo-dreidimensional ausfallen. Wenn Sie sich noch an unser Schema für Edays erinnern, dann sind diese Linien, die dort gelb gezeichneten Linien. Die nächsten Komponenten sind Ihnen im großen und ganzen schon bekannt, die JLabels:

  // Ueberschrift
  JLabel myTitle = new JLabel("eighty Days",JLabel.CENTER);
  myTitle.setFont(new Font("Helvetica",Font.BOLD,20));
  myTitle.setForeground(Color.black);

Hier wird der Titel für das Programm festgelegt. Nicht der Titel in der Titelleiste, sondern der groß und protzend strahlende Programmname, wie man ihn im Screenshot schön sehen kann. Wenn Sie sich an unsere bisherigen Swing Programme erinnern, fällt Ihnen auf, daß wir Label dort nur definiert haben, ohne diese sonst weiter zu manipulieren. Die Standardfarbe für diese Label war dann zB. blau. Für unser Programm wünsche ich mir aber schwarz gezeichnete Beschriftungen. Hier deklarieren wir in der ersten Zeile ein JLabel und übergeben dem Konstruktor von JLabel den Inhalts-String, sowie die Orientierung. Dann nutzen wir Methoden von JLabel, um die Schriftart und die Schriftfarbe zu setzen. Die Verwendung dieser Methoden dürfte selbsterklärend sein. Wenn Sie weitere Methoden von JLabel kennenlernen wollen, werfen Sie doch einmal einen Blick in die API-Dokumentation.
Es folgen nun zahlreiche Labels für die Beschriftung von irgendwas:

  JLabel myDateFieldLabel = new JLabel("Datum:   ",JLabel.RIGHT); 
  JLabel myErgebnisLabel  = new JLabel("Alter in Tagen:   ",JLabel.RIGHT); 
  JLabel myGueltigLabel   = new JLabel("Stichtag dazu:   ",JLabel.RIGHT);
  JLabel myStichtagLabel  = new JLabel("Stichtag dazu:   ",JLabel.RIGHT);
  JLabel myHeuteLabel     = new JLabel("Datum heute:   ",JLabel.RIGHT);
  JLabel myStatusLabel    = new JLabel("Status: ",JLabel.CENTER);

Die Wiedergabe der Deklaration habe ich jetzt mal gekürzt. AlleJLabelswerden im Listing oben genauso wie die Überschrift mit Farbe und Font ausgestattet. Beschriftung heißt hier, daß diese Label sich im Laufe des Programms niemals mehr ändern werden. Deswegen wurden sie auch nicht als Attribut deklariert. Sie stehen bei einem anderen Label oder einem Feld, welches einen Wert darstellt. Mit diesen Labeln wird eben nur beschrieben, welcher Wert dargestellt wird (sehen Sie auch das Schema oder den Screenshot).
Nach diesen JLabeln wird das eingabeFeld deklariert. Das ist in unserem Programm sicher interessant:

  // Eingabefeld Datum
  eingabeFeld = new JDateField(8); //Verwendet meine Version von DateField
  eingabeFeld.setHorizontalAlignment(JTextField.LEFT);
  eingabeFeld.setFont(new Font("Helvetica",Font.BOLD,14));

Tja, das ist interessant, aber relativ unspektakulär. Wir haben ja unsere Arbeit bereits mit dem Programmieren der Klasse JDateField und dem dazugehörigen Dokument erledigt. Das JDateField wird wie ein ganz normales JTextField erzeugt. Im Konstruktor wird ihm die Breite in Spalten übergeben. Wenn Sie nachher im Programm das DateField als "Breit" empfinden liegt das daran, daß die Spalten sich am breitesten Zeichen, und das ist in der Regel das "m" orientieren. Es würden also in diesem Fall genau acht "m" in das Feld passen (Eine Unwägbarkeit von sonst sehr schönen proportionalen Zeichensätzen). Anders als bei den JLabels wird die Orientierung (Left) hier explizit durch eine geerbte Methode gesetzt. Das Zuweisen des Zeichensatzes geschieht aber wieder analog zum JLabel.
Beim Deklarieren der Attribute hatte ich die Key* Klassen schon einmal angesprochen. Jetzt kommen sie zum Einsatz:

  // Fuer das Datumsfeld die Returntaste manipulieren, so das [return] 
  // die Aktion des buttons auslöst
  myKeymap = eingabeFeld.getKeymap();
  myStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); 
  myKeymap.removeKeyStrokeBinding(myStroke);

Was ich hiermit realisiere, ist eine Komfortfunktion. Sie kennnen das vielleicht aus Windows-Programmen. Es gibt so etwas wie einen default-Button in einem Dialog oder Fenster. Wenn Sie irgendwo in einem TextFeld [Return] drücken, dann wird das so behandelt, als wäre der Button angeklickt worden. Dieses Verhalten möchte ich für mein Programm auch haben. Nun habe ich im Internet nach einer Lösung gesucht, und auch eine gefunden. Ich kann sie aber gerade jetzt nicht mehr finden. So genau weiß ich auch nicht, wie das mit den Keymaps funktioniert. Wir sehen ja, was ich dort tue. Zuerst habe ich vorhin myKeymap als Attribut deklariert. Wir haben also ein Objekt vom Typ Keymap. Diesem weise ich nun einen Verweis auf eine existierende Keymap zu. Ein Eingabefeld besitzt also standardmäßig auch ein Objekt vom Typ Keymap. In dieser Keymap sind Tastenkombinationen (wie zB Alt+X) enthalten, oder bestimmte Tastendrücke, die bestimmte Aktionen auslösen. Jeder Tastendruck, bzw. jede Kombination stellt dabei einen Eintrag in Form eines Tabelleneintrags dar. Dieser einzelne Eintrag ist ein Objekt vom Typ Keystroke. Für bestimmte Kombinationen oder auch bestimmte Tasten ist dazu eine Konstante in der Klasse KeyEvent definiert. Worauf ich hinaus will, ist folgendes: Die Keymap vom eingabeFeldist standardmäßig mit irgendeiner unbedeutenden Aktion vorbelegt. Durch diese Vorbelegung wird aber verhindert, daß beim Drücken von [Return] die Standardkomponente des Fensters, in unserem Fall der Berechnen-Button aufgerufen wird. Aus diesem Grunde muß die standardmäßige Vorbelegung der [Return]-Taste aus unsererKeymap einfach entfernt werden. Dazu besorgen wir uns wie gezeigt einen Verweis auf die Keymapdes Eingabefeldes. Den Wert für den Eintrag von [Return] bekommen wir über das Keystroke-Objekt. Wenn man den Wert hat, kann man diesen aus unserer konkreten Keymap mit remove() entfernen.
Es tut mir wirklich leid, daß ich meine Quelle jetzt nicht mehr finde. Ich hätte Ihnen gern mehr über Keymaps erzählt, als diese Selbstanalyse. Aber ich nehme an, Sie haben verstanden worum es geht. 
Wird im eingabeFeld die Taste [Return] gedrückt, so wird der Standard-Button des Fensters ausgelöst (wirkt also genauso wie das Klicken auf den Button).
Sehr umfangreiche Informationen zu Keybindings finden Sie bei Sun in englischer Sprache.

Im folgenden finden wir wieder einfache JLabel-Deklarationen:

  ausgabeLabel = new JLabel("00000",JLabel.LEFT);

  stichtag.add(Calendar.DATE, -80);
  String dateString = datumsFormat.format(stichtag.getTime());
  gueltigLabel = new JLabel(dateString, JLabel.LEFT);

  stichtag.setTime(new Date());
  stichtag.add(Calendar.DATE, 80);
  dateString = datumsFormat.format(stichtag.getTime());
  stichtagLabel = new JLabel(dateString, JLabel.LEFT);

  dateString = datumsFormat.format(heute.getTime());
  heuteLabel = new JLabel(dateString, JLabel.LEFT);

  statusLabel = new JLabel("Berechnung durchgeführt",JLabel.LEFT);

Die meisten entsprechen dem oben genannten Schema und sind gekürzt. Etwas genauer kann man sich die Initialisierung von gueltigLabel, stichtagLabelund heuteLabel ansehen. Der Stichtag wurde bei den Attributen deklariert und mit dem heutigen Datum initialisiert. Bei der Initialisierung des gueltigLabels, welches das Datum vor 80 Tagen darstellen sollen, ziehen wir mit der aus unserer Klasse DateDifferenceInDays bekannten Methode add() des Calendar-Objektes 80 Tage ab (weil wir ein negatives Vorzeichen benutzen). Dann deklarieren wir einen neuen String dateStringund weisen ihm einen mit dem (bei den Attributen deklarierten) SimpleDateFormatformatierten String des so errechneten Wertes von Stichtag zu. Mit diesem wird dann das gueltigLabel initialisiert.
Bei der Initialisierung des stichtagLabel setzten wir das Datum von stichtag wieder auf heute. Dann erhöhen wir das Datum wie vorher um 80 Tage, diesmal in die Zukunft (stichtag soll ja anzeigen, wann für eingegebene Daten das Verfallsdatum besteht. Und zu Beginn ist das "eingegebene" Datum ja das heutige Datum). Wir nutzen wieder den vorher deklarierten String dateString, um ein formatiertes Datum als String zu erhalten, und initialisieren damit das stichtagLabel. Bei dem heuteLabel, welches nur das heutige Datum darstellen soll, werden wir dateString noch einmal wiederverwenden, und entsprechend das heutige Datum einsetzen. Wir haben also dateString quasi als temporären String verwendet und wiederverwendet. Diesem wurde jeweils ein mit SimpleDateFormat formatiertes Datum übergeben. Damit wurden dann schließlich unsere Label initialisiert.
Ansonsten wurden all die Label auch mit entsprechender Farbe und Zeichensatz versorgt (siehe Originallisting). Im Originallisting folgen dann jetzt auch erstmal all die Container:

  // Panels definieren und Layout setzen
  JPanel topPanel = new JPanel();
  JPanel centerPanel = new JPanel();
  JPanel footerPanel = new JPanel();
  JPanel upperGridPanel = new JPanel();
  JPanel middlePanel = new JPanel();
  JPanel lowerGridPanel = new JPanel();

  topPanel.setLayout(new BorderLayout()); 
  centerPanel.setLayout(new BorderLayout()); 
  footerPanel.setLayout(new FlowLayout(FlowLayout.LEFT));//alles nach.ander
  upperGridPanel.setLayout(new GridLayout(3,2));         //2 zeilen 3 spalten
  middlePanel.setLayout(new BorderLayout(5,5));          // 5 Pix Abstand
  lowerGridPanel.setLayout(new GridLayout(2,2));         //2 zeilen 2 spalten
  footerPanel.setBorder(BorderFactory.createLoweredBevelBorder()); //Rahmen

Die Logik der Containerauswahl, also wieviele Panels und welche Layouts ergibt sich aus dem, was wir oben bei der schematischen Grafik von Edays besprochen haben. Die Erzeugung von Panels dürfte Ihnen inzwischen auch geläufig sein. Selbstverständlich gibt es aber wieder eine Besonderheit, die ich hier noch einmal gesondert zeige:

footerPanel.setBorder(BorderFactory.createLoweredBevelBorder()); //Rahmen

Swing bringt eine sogenannte BorderFactory mit. Damit kann Swing allen Komponenten einen Rahmen zeichnen. Die Art der Rahmen ist dabei extrem vielfältig. Ich habe hier für das unterste Panel einen LoweredBevelBordergewählt. Dieser zeichnet einen pseudo-dreidimensionalen Rahmen, der so aussieht, als wäre das Panel nicht erhaben oder auf gleicher Höhe wie die anderen Komponenten, sondern als wäre es etwas tiefer angesiedelt. In diesem Panel wird sich später die Statuszeile befinden, und für derartige Anzeigen hat sich ein solcher tiefergesetzter Rahmen eigentlich eingebürgert. Um den Rahmen zu setzten wird einfach die Methode setBorder()verwendet. Ihr wird ein Rahmen übergeben, der durch eine Methode derBorderFactoryerzeugt wird.
Als nächstes wird im Konstruktor ein Button erzeugt, wie wir das bereits kennen:

JButton berechne = new JButton("Berechnen");      // Ereignis Button-Click

Im Argument für den Konstruktor des Buttons wird die Beschriftung übergeben.
Im Listing folgt nun die Ereignisbehandlung für den Button, die wir hier noch überspringen. Darauf folgt noch eine Methode, die den Button selbst betrifft:

SwingUtilities.getRootPane(this).setDefaultButton(berechne);

Ich habe ja oben bei den Keymaps ständig vom Default-Button eines Fensters gesprochen. Ein zugefügter Button ist aber nicht sofort ein Default-Button. dies muß künstlich erzeugt werden. Genau das tut diese Zeile. Sie weist dem Button die Eigenschaft zu, der Default.-Button des Fensters zu sein. Dazu wird eine Methode aus den sogenannten SwingUtilitiesverwendet. Sie holt sich eine Instanz für die JRootPane des entsprechenden Fensters, und setzt dieser den Button, der als DeafaultButton verwendet werden soll.
Die im Listing folgende Initialisierung des Timers überspringen wir hier auch noch. Es folgt das Einfügen der ganzen deklarierten Komponenten in die jeweiligen Container:

  // Aktive Komponenten ins UpperGrid einbauen
  upperGridPanel.add(myDateFieldLabel);
  upperGridPanel.add(eingabeFeld);
  upperGridPanel.add(myErgebnisLabel);
  upperGridPanel.add(ausgabeLabel);
  upperGridPanel.add(myStichtagLabel);
  upperGridPanel.add(stichtagLabel);

  // Berechnen Button und Linie ins Middel Panel
  middlePanel.add(berechne, BorderLayout.NORTH);
  middlePanel.add(myCenterLine, BorderLayout.SOUTH);

  // Gültigkeits Komponenten ins LowerGrid einbauen
  lowerGridPanel.add(myHeuteLabel);
  lowerGridPanel.add(heuteLabel);
  lowerGridPanel.add(myGueltigLabel);
  lowerGridPanel.add(gueltigLabel); 

  // Überschrift ins Toppanel einbauen
  topPanel.add(myTitle, BorderLayout.NORTH);
  topPanel.add(myUpperLine, BorderLayout.SOUTH);

  // Status ins FooterPanel einbauen
  footerPanel.add(myStatusLabel);
  footerPanel.add(statusLabel);

Dabei halten wir uns strikt an die im Schema angegebene Verteilung. Eine Erklärung sollte nicht nötig sein. Danach schachteln wir die jeweiligen Untercontainer in ihre jeweiligen Container, bzw. fügen die entsprechenden Container schließlich dem Frame hinzu:

  // Gridpanels ins übergeordnete Center Panel bauen
  centerPanel.add(upperGridPanel, BorderLayout.NORTH);
  centerPanel.add(middlePanel, BorderLayout.CENTER);
  centerPanel.add(lowerGridPanel, BorderLayout.SOUTH);

  // UnterPanels  in den Frame einbauen
  getContentPane().add(topPanel, BorderLayout.NORTH);
  getContentPane().add(centerPanel, BorderLayout.CENTER);
  getContentPane().add(footerPanel, BorderLayout.SOUTH);

Auch hier gibt es wohl wenig zu erklären. Die letzte Zeile des Konstruktors gehört eigentlich nicht hier in die GUI-Erklärung, aber da sie so kurz ist, nehmen wir sie hier auf

this.enableEvents(AWTEvent.WINDOW_EVENT_MASK); // Schließen ermöglichen

Diese Zeile kennen Sie schon, den mit ihr schalten wir die Ereignisbehandlung für die Titelzeile ein, und können so unser Fenster Schließen, wie wir es schon aus den anderen Beispielen kennen. Leiten wir damit in den nächsten Abschnitt über.

4.6.3. Ereignisbehandlung in  Edays
Weil wir gerade mit dem Fenster-Schließen aufgehört haben, machen wir kurz dort weiter. Da auch Edays irgendwie beendet werden muß, ist das entsprechende Icon in der Titelleiste aktiviert, und muß nun noch behandelt werden:

 protected void processWindowEvent(WindowEvent e) { // Überschreiben
                                                    // um zu Beenden
  if (e.getID()==WindowEvent.WINDOW_CLOSING) { 
   dispose();                           // Ressourcen des Fensters freigeben 
   System.exit(0);                      // Programm beenden 
  } 
 } // Ende Methode processWindowEvent()

Sie kennen diese Methode bereits aus den anderen Beispielprogrammen. Sie folgt direkt auf den Konstruktor, und muß eigentlich nicht weiter erläutert werden.
Kommen wir nun dazu; ich habe während der GUI-Besprechung im Konstruktor öfters gesagt, dazu kommen wir später. Es ging dann immer um Ereignisbehandlung. Fangen wir mit der ersten an, und diese steht hinter der Deklaration des Buttons berechne.
Unsere gesamte Oberfläche hat ja recht wenig interaktive Elemente. Das JDateField verwaltet sich, wie wir inziwschen wissen, praktisch selbst. Ansonsten ist ja nur der Button als interaktives Element vorhanden. Wenn der Button gedrückt wird (oder wie wir vorhin schon umständlich beschrieben haben die [Return]-Taste gedrückt wird) werden die eigentlichen Programmfunktionen ausgeführt. Auf der Basis des im JDateField eingegebenen Datums wird dann das Alter in Tagen berechnet und das dazu gültige Verfallsdatum nach 80 Tagen. Dies alles wird in der Ereignisbehandlung des Buttons durchgeführt. Eine Ereignisbehandlung in einem Button realisieren wir, wie bereits beschrieben, über einen ActionListner, der alle Reaktionen auf das Ereignis in einer anonymen Klasse verpackt:

  berechne.addActionListener(new ActionListener() { // in anonymer Klasse
   public void actionPerformed(ActionEvent e) {
    int frist, tag=02, monat=11, jahr=2000;         // Voreinstellung Datum
    boolean noDateError=true;

Dies ist erst der Anfang. Zuerst werden für diese Klasse die wesentlichen int-Variablen deklariert. Die Frist, die schließlich das Alter in Tagen aufnimmt, sowie ein durch int-Werte ausgedrücktes Datum mit beliebigen Werten als Voreinstellung (man weiß ja nie was passiert *g*). Außerdem können wir ja nicht zu hundertprozent sicher sein, ob das eingegebene ein gültiges Datum ist. Daher verwenden wir einen boolschen Wert um festzustellen, ob ein fehlerhaftes Datum eingegeben wurde.
Nun folgen erst einmal Abfragen, die feststestellen, ob das Datum korrekt im Sinne unseres Programms ist:

    if(eingabeFeld.hasNoZeroField()) {              //Prüfen ob nicht leer
     tag = eingabeFeld.getDay();
     monat = eingabeFeld.getMonth()-1;
     jahr = eingabeFeld.getYear();
    }
    else {
     noDateError=false;
     statusLabel.setText("Kein Nullfeld möglich!");
     ausgabeLabel.setText("##");
    }

Da wir ja ein gültiges Datum erwarten, darf weder der Tag noch der Monat ein Nullfeld enthalten. Zur Überprüfung nutzen wir einfach die Methode, die wir in JDateField erstellt haben. Ist kein Nullfeld vorhanden, werden die drei Datums-Variablen aus unserer anonymen Klasse mit den Werten aus unserem JDateField belegt (dabei wird bei dem Monat bereits die Abweichung des Offset berücksichtigt). Sollte ein Nullfeld vorhanden sein, wird der boolsche Wert auf falsegesetzt, und es wird in der Statuszeile eine entsprechende Meldung ausgegeben. Das Alter wird außerdem auf den gezeigten String gesetzt, da dieses Label groß ist, und einen Fehler gut signalisiert.
Für die nächsten Abfragen benötigen wir ein Calendar-Objekt. Daher erzeugen wir dies:

    meinDatum.set(jahr, monat, tag);         //Calenderobjekte aktualisieren
    heute.setTime(new Date());

Hier ist meinDatum dann das eingegebene Datum und heuteeben das heutige Datum. Die nächste Abfrage lautet wie folgt:

    if(!meinDatum.before(heute)) {           //Prüfen ob Datum in der Zukunft
     noDateError=false;
     statusLabel.setText("Datum in der Zukunft!");
     ausgabeLabel.setText("##");
    }

Wie ich schon sagte, ist die Klasse DateDifferenceInDays nicht besonders robust, weshalb ich es vermeiden will, dort Daten tauschen zu müssen. Also habe ich ja in Edays, was auch dem Zweck des Programms entspricht, Daten aus der Zukunft verboten. Die gerade gezeigte Abfrage realisiert das. Das "!"-Zeichen bedeutet "Nicht". Ist also der folgende Abgleich wahr, ist der Gesamtausdruck falsch. Ist der folgende Ausdruck falsch, ist der Gesamtausdruck wahr. Das "!"-Zeichen dreht also das Ergebnis um (negiert es). Die Folgen sind bekannt. Ist das Datum in der Zukunft, ist der boolsche Wert entsprechend falsch, und die Statuszeile und das Label für das Alter des Datums geben eine entsprechende Meldung aus.
Zum Schluß kommt die alles entscheidende Abfrage:

    if(!JDateField.isDate(tag, monat, jahr)) {
     noDateError=false;
     statusLabel.setText("Datum ungültig!");
     ausgabeLabel.setText("##");
    }

Hier wird geprüft, ob es sich beim eingegebenen um ein gültiges Datum handelt (bzw. um die Negierung). Ist das Datum ungültig, erwarten uns die bekannten Folgen.
Nachdem wir nun endlich alle Fehlermöglichkeiten und Fehleingaben berücksichtigt haben, kommen wir zum eigentlichen, und kurzen, Kernstück des Programms:

    if(noDateError) {
     myDate.setEarly(tag, monat, jahr);
     myDate.setLater(heute.get(Calendar.DATE),
       heute.get(Calendar.MONTH), heute.get(Calendar.YEAR));
     frist=myDate.getDifference();
     ausgabeLabel.setText(String.valueOf(frist));

     //aktuellen Stichtag Updaten
     stichtag.set(jahr, monat, tag);
     stichtag.add(Calendar.DATE, 80);
     String datumsString = datumsFormat.format(stichtag.getTime());
     stichtagLabel.setText(datumsString);
     statusLabel.setText("Berechnung durchgeführt.");
    }

Hier wird, wenn es keinen noDateError gab,  die eigentliche Aufgabe des Programms ausgeführt. Sooo lang ist es nun auch nicht. Das liegt vor allem daran, daß wir die eigentliche eigentliche Arbeit ja in andere Klassen ausgelagert haben. Zuerst werden mit den Zugriffsmethoden die beiden Daten von unserer Instanz von DateDifferenceInDaysgesetzt. Der Variablen frist aus unserer anonymen Klasse wird nun das Ergebnis der Differenz-Berechnung zugewiesen. Der int-Wert Frist wird nun mittels entsprechender Umwandlung in das Label für die Ausgabe des Alters eingesetzt.
Dadurch, daß wir nun ein neues Alter haben, müßen wir auch das Datum aktualisieren, an dem das eingegebene Datum 80 Tage alt wird. Dies erledigen die folgenden Zeilen nach der schon aus dem Konstruktor her bekannten Vorgehensweise.
Damit ist die Ereignisbehandlung für den Button erledigt und das war dann eigentlich das Programm. Da ich mich aber entschlossen hatte, das heutige Datum anzuzeigen, sowie den dazu passenden Stichtag vor 80 Tagen, ergab sich eine weitere Ereignisbehandlung. Sollte jemand diese Programm so regelmäßig nutzen, daß er es einfach rund um die Uhr laufen läßt, würde am Tag nach dem Start die Anzeige des aktuellen Datums und des passenden Stichtags nicht mehr stimmen. Es muß also sichergestellt werden, daß um Mitternacht der Wert umgestellt wird.
Ich verwende in meinem Beispiel eine unsichtbare Komponente um dies sicherzustellen. Diese wurde bei den Attributen deklariert, und hat auch eine eigene Ereignisbehandlung bekommen. Zur Erinnerung die Deklaration:

Timer myTimer;

Bei der Komponente handelt es sich, wie man sieht, um einen Timer. Ein Timer ist eine Komponente, die einmalig oder in regelmäßigen Abständen ein Ereignis auslöst. Auf dieses Ereignis kann man reagieren Würde man eine Uhr programmieren, so würde man denTimerjede Sekunde einmal auslösen, und dann die Label für die Sekunden, Minuten und Stunden aktualisieren. Das Aktualisieren würde in der Ereignisbehandlung stattfinden. Schauen wir uns die Ereignisbehandlung für unseren Timer an:

  //Timer für die Anzeige des Stichtages zum heutigen Datum (alle 5 Minuten)
  myTimer = new Timer(300000, new ActionListener() {  //300000 Millisekunden
   public void actionPerformed(ActionEvent evt) {
    stichtag.setTime(new Date());     // *** Stichtag
    stichtag.add(Calendar.DATE, -80);
    String datumsString = datumsFormat.format(stichtag.getTime());
    gueltigLabel.setText(datumsString);
    heute.setTime(new Date());                        // *** Heute
    datumsString = datumsFormat.format(heute.getTime());
    heuteLabel.setText(datumsString);
   } 
  });

Wie üblich wird unserem Timer im Konstruktor ein ActionListenerals anonyme Klasse übergeben. Vorher jedoch wird ein Parameter übergeben, der das Intervall des Timers in Millisekunden spezifieziert. Das Intervall meint dabei die Zeit, die vergeht bis der Timer erneut ein Ereignis auslöst. Bei uns sind das 300.000 Millisekunden, was genau 5 Minuten entspricht. Der Timer soll ja unter anderem unser heutiges Datum aktualisieren. Ein Intervall von 5 Minuten bedeutet, daß der Timer im ungünstigsten Fall auch erst um 0:04:59 aufgerufen werden könnte. Damit nehme ich eine Ungenauigkeit von 5 Minuten im Kauf. Wer also um 0:03:00 einen Blick auf unser Programm wirft, kann unter Umständen eine falsche Information erhalten. Da die Bürozeiten unseres BSE-Lieferanten aber von 09-17 Uhr sind, ist die Wahrscheinlichkeit gering, daß jemand nachts um 12 auf das Programm schaut. Um nicht zuviel Systemlast zu erzeugen, wird unser heutiges Datum nur alle 5 Minuten geprüft.
Danach werden die entsprechenden Anweisungen ausgeführt. Anstatt abzufragen, ob es schon Mitternacht ist, wird einfach alle 5 Minuten dasgueltigLabel und das heuteLabelauf den gerade aktuellen Wert gesetzt. Soviel Rechenzeit sollte alle 5 Minuten übrig sein (auch bei aktivierten SETI-Berechnungen).
Damit sind alle Ereignisbehandlungen abgehandelt.

4.6.4. Der Rest von Edays
Ja, wenn wir uns Edays so betrachten, dann haben wir eigentlich alles schon gesehen. Die import-Anweisungen, die Attribute, den Konstruktor und die eine Methode für das Fenster-Schließen-Event. Was übrig bleibt, ist der traurige Rest: Das Hauptprogramm:

 public static void main(String[] arg) {//Hauptmethode
  Edays myEdays = new Edays();
  myEdays.pack();
  myEdays.show();
  myEdays.ausgabeLabel.setText("0");
  myEdays.statusLabel.setText("Datum eingeben");
  myEdays.myTimer.start();
 }

Wie wir einmal mehr sehen, sind Hauptprogramme in Java im Verhältnis zum Gesamtcode sehr kurz. Es wird eine Instanz von Edays erzeugt, also ein JFrame. Dann wird er gepackt (Java ermittelt also selbst, welche Fläche Edays benötigt, und wie groß das Fenster und die Container sein müssen).
Anschließend wird das ausgabeLabel (also das für das Alter) auf Null gesetzt, weil ja auch im Eingabefeld das aktuelle Datum angezeigt wird. Dann wird ein sinnvoller Statustext angezeigt (der voreingestellte war nur dafür da, beim pack() den längstmöglichen String anzuzeigen, und somit ggf. Einfluß auf die Fensterbreite zu nehmen.
Die letzte Anweisungen bezieht sich auf unseren Timer. Timer werden deklariert, und Ihnen wird eine Ereignisbehandlungsroutine zugewiesen, aber gestartet werden, müßen sie immer noch manuell. und das bewerkstelligt die letztgenannte Methode.

4.7. Zusammenfassung
Dieses Kapitel war doch sehr lang. Dabei ging es um Swing als Ersatz von AWT, aber auch um zusätzliche Fähigkeiten wie Dokumentmodelle. Dafür ist dann schließlich eine Zusammenfassung des Kapitels gerechtfertigt.
Zunächst einmal haben wir einen Einblick in die Bearbeitung von Datum's bekommen. Dazu sind die Klassen Date, Calendar undGregorianCalendar hilfreich. Später haben wir festgestellt, daß die Verwendung von Strings, die Datum's repräsentieren, das SimpleDateFormaterforderlich macht.
Als nächstes haben wir die Verwendung des MVC-Prinzips kennenglernt, welches in Swing eigentlich verwendet wird. Beim JTextField ist das Dokumentmodell besonders hilfreich. Bei anderen Komponenten sind die Modelle anders implemeniert, wodurch manchmal gar kein Custom-Model (also ein selbstdefiniertes Modell) erforderlich ist. Aber gerade weil das Dokumentmodell für JTextfelder das Prinzip so gut veranschaulicht, hat es sich für dieses Kapitel gut geeignet (noch besser wäre vermutlich das Model für Slider, welches in der Literatur oft beschrieben wird, aber gerade weil es in der Literatur oft vorkommt, habe ich hier ein anderes verwendet).
Schließlich haben wir nochmal für Java generell gezeigt, wie man eigene Komponenten ohne großen Aufwand erstellt, indem man das objektorientierte Prinzip der Vererbung ausnutzt, und vorhandene Klassen einfach weiter spezialisiert. Gerade mit dem JDateField sollte vielen Lesern dieses Tutorials Ideen gekommen sein, wie man eigene Textfelder erstellen kann, die ganz bestimmte Daten aufnehmen sollen. Ich gebe zu, daß an der ganzen Konzeption einiges zu verbessern wäre. Gerade darin sollte auch ein Anreiz liegen. Wenn man für ein spezielles Projekt eine Komponente oder Klasse erstellt, dann sollte man sich fragen, ob man diese später evtl. wiederverwenden kann und will. Sollte die Antwort "Ja" lauten, dann sollten sie auch Wert darauf legen, die Klasse oder Komponente so robust zu implementieren, daß sie sich auch bei Wiederverwendung nie unerwartet verhält.
Schließlich haben sie im Konstruktor vom Hauptprogramm Edays noch einmal gesehen, wie umfangreich eine GUI-Definition werden kann. Das soll sie nicht abschrecken. Sie werden feststellen, daß GUI-Definitionen im Laufe der Zeit immer mehr zu einem Routine-Job werden. Das ist zwar nur Fleißarbeit, aber es ist eben notwendig.
Die eigentliche Arbeit übernehmen bei der Java-Programmierung meist spezialisierte Klassen, oder die Ereignisbehandlungen von Komponenten.. Das ist die Schlußfeststellung. Damit wären wir durch ein wirkliches Swing-Programm durch. Wenn sie sich den Source-Code runterladen, ihn selbst kompilieren, und das Programm ausführen, dann werden sie feststellen, daß das alles halb so wild ist. Spielen Sie ruhig am Code herum. Ändern sie Eigenschaften. Verändern sie das ganze Programm. Erhalten sie Compiler-Fehler nach den Änderungen, lernen Sie auch dadurch. Versuchen Sie doch einfach einmal die 80 Tage nicht fest zu kodieren, sondern durch eine Konstante zu ersetzen. Oder vielleicht durch ein weiteres Eingabefeld, welches die konkrete Differenz aufnimmt. Vielleicht wollen Sie sich auch mal mit der Internationalisierbarkeit der Komponenten auseinandersetzen. Dazu müßten sie die local-Definition abfragen, und zum Beispiel alle fest kodierten Caret-Positionen im JDateFieldals Variablen behandeln.

4.8. Distribution (Jar-Files)
Ohne Entwicklungsumgebung und kostenpflichtige Programme wie InstallShield kann das Weitergeben von Java-Programmen schon problematisch werden. Dafür gibt es eine gute Nachricht. Man kann komplette Programme, die z.B. auch aus mehreren Klassen bestehen als einzelne *.jar Datei weitergeben. Ab Windows 98 oder unter Solaris sind diese Dateien sogar per Doppelklick direkt ausführbar (genauso wie *.exe-Dateien oder andere Programme unter Solaris). Unter Linux geht das auch. Der Kernel muß entsprechend kompiliert sein. 
Die *.jar Dateien haben ebenfalls den Vorteil, daß Programme, die aus mehreren Klassen bestehen nur als eine Datei ausgeliefert werden, anstatt als mehrere Class-Dateien.
Um das jar-File zu erstellen, nutzt man das Tool jar (Java ARchive). Dieses nutzt die Mechanismen zur Erstellung von ZIP-Dateien. Eine jar-Datei enthält zusätzlich noch eine Manifestdatei. Diese enthält Meta-Informationen zu dem konkreten Jar-File. Außerdem werden noch Signierungsmechanismen und ähnliches von jarunterstützt.
Wir benötigen auch eine Manifestdatei bevor wir unser Jar-File erstellen. Die Virtual Machine muß ja wissen, in welcher der im Jar-File enthaltenen Class-Dateien die main-Methode enthalten ist, um unser Programm zu starten. Dazu legen wir eine neue Datei an. Bei mir hat sie den Namenmani.txt. Ihr Inhalt ist simpel:

Main-Class: Edays

Mehr brauchen wir in unserer Manifestdatei nicht. Nun rufen wir das jar-Tool auf, und übergeben ihm den Namen unserer Manifestdatei sowie die Namen der Class-Dateien, die wir in das Archiv packen wollen:

jar cmf mani.txt Edays.jar Edays.class Edays$1.class Edays$2.class DateFieldDocument.class JDateField.class DateDifferenceInDays.class

Der ganze Aufruf besteht aus genau einer Zeile. Es hat sich als sinnvoll erwiesen, diesen Aufruf in eine bat-Datei (zB makeBundle.bat) oder in ein Shellscript oder Alias zu packen. Anstatt alle Class-Dateien zu benennen, könnte man auch *.class angeben, um alle Class-Dateien aus einem Verzeichnis in das jar-Archiv zu verpacken. Ich habe aber auch die Class-Dateien der kleinen Swing-Beispiele in meinem Verzeichnis, und benenne daher die vier gewünschten Klassen (die dazugehörigen anonymen Klassen (..$1..$2..) nicht vergessen) direkt.
Die Argumente cmf, die dem jar-Tool übergeben werden bedeuten create für c, m für manifest (der erste folgende Dateiname muß also eine Manifestdatei sein) und fdafür, daß der zweite folgende Dateiname, der Name des Archivs ist, welches wir erstellen wollen.
Nachdem Sie dieses Jar-File erstellt haben, können Sie es von der Kommandozeile aus mit

java -jar Edays.jar

starten. Sie können unter Windows auch eine Verknüpfung auf dem Desktop erstellen, indem Sie dort den kompletten Pafd zu java.exe und dem  jar-File angeben (zb.c:\jdk12\bin\java.exe -jar d:\myapp\Edays.jar). Dabei wirkt störend, daß immer auch ein Konsolen-Fenster (unter Windows eine MS-DOS Eingabeaufforderung) mit geöffnet wird. Jetzt die gute Nachricht. Klicken Sie einfach mal doppelt auf das jar-File. Edays wird jetzt wie jede andere Windows-Anwendung (*.exe Dateien) gestartet und es geht auch keine MS-DOS Eingabeaufforderung auf. Seit Java 2 kann die virtuelle Maschine auch durch javaw gestartet werden, und nicht nur durch java. Und javaw verzichtet auf ein Konsolenfenster. Wir können also problemlos unsere Projekte weitergeben, indem wir sie in ein jar-Archiv packen. Alle Empfänger unseres Programms können dieses dann einfach durch Doppelklick starten (sofern sie das Runtime Environment JRE installiert haben oder ein JDK). So kommt langsam Komfort in unsere Programme und deren Verwendung.

4.9. Download Quelltexte
Diesmal befinden sich im ZIP alle Sourcecodes aus diesem Kapitel, die Batchdatei für Windows makeBundle.bat, die das Jar erzeugt, die dazu passende Manifestdatei und eine icon-Datei für Windows: Download Quelltexte
Siehe auch Projekt eDays, wo immer die aktuelle Version von eDays mit komplettem Sourcecode bereitliegt. Darin sind dann ggf. letzte Änderungen, oder auch komplette Verbesserungen enthalten.

4.10. Übung
Ändern Sie den von Ihnen in der Übungsaufgabe zu AWT entwickelten Bookmark-Manager so ab, daß er eine Swing-Applikation ist. Verwenden Sie dazu die jeweiligen J-Versionen der benutzten Komponenten. Vergessen Sie auch nicht, die entsprechenden Container und Frames in der J-Version zu verwenden.


<= vorherige Seite Inhaltsverzeichnis nächste Seite =>

zurück zur Hauptseite
Copyright 2000 by Frank Gehde