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

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:

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