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();