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();
String myJahrString=year;
if(year.length()==1) myJahrString="000"+year;
if(year.length()==2) myJahrString="00"+year;
if(year.length()==3) myJahrString="0"+year;
this.setText(tagString+"."+monatString+"."+myJahrString);
}
//****** GET METHODEN *******
public int getDay() { // **** get-Methoden
für Int Werte
attributeUpdate(); // Übergibt
den Tag als Int zB 28
return tagInt;
}
public int getMonth() { // Übergibt den Monat
als Int zB 9
attributeUpdate();
return monatInt;
}
public int getYear() { //Übergibt das
Jahr als Int zB 2001
attributeUpdate();
return jahrInt;
}
public String getDayText() { // **** get-Methoden
für String Werte
attributeUpdate();
//Übergibt den Tag als String zB "28"
return tagString;
}
public String getMonthText() { //Übergibt den
Monat als String zB "06"
attributeUpdate();
return monatString;
}
public String getYearText() { //Übergibt
das Jahr als String zB "1999"
attributeUpdate();
return jahrString;
}
public Calendar getDate() {
// **** get-Methode für Calendar Objekte
attributeUpdate(); // Übergibt Inhalt des
Textfeldes als Calendar Objekt
Calendar aktDate = new GregorianCalendar(tagInt,
monatInt, jahrInt);
return aktDate; //hier neues
kalenderobjekt erzeugen
}
//****** GET METHODEN Ende *
//****** Zusatz METHODEN ***
private void attributeUpdate() { // MUSS vor Rückgabe
in
// get-Methoden aufgerufen werden
tagString = this.getText().substring(0, 2);
// Achtung: Wert kann ungültig
monatString = this.getText().substring(3, 5);//
sein. zB 32 für den Tag.
jahrString = this.getText().substring(6);
tagInt = new Integer(tagString).intValue();
monatInt = new Integer(monatString).intValue();
jahrInt = new Integer(jahrString).intValue();
}
public boolean hasZeroField() {
// Methode, die prüft, ob ein Feld
boolean hasZero=false;
// "00" enthält.
attributeUpdate();
if (tagString.equals("00")) hasZero=true;
if (monatString.equals("00")) hasZero=true; //
Bei Jahr ist "0000" erlaubt
return hasZero;
}
public boolean hasNoZeroField() { //
Methode, die prüft, ob ein Feld
boolean hasZero=true;
// nicht "00" enthält.
attributeUpdate();
if (tagString.equals("00")) hasZero=false;
if (monatString.equals("00")) hasZero=false; //
Bei Jahr ist "0000" erlaubt
return hasZero;
}
static boolean isDate(int tag, int monat, int
jahr) { // Klassenmethode !!!!
boolean itIsADate=true;
GregorianCalendar testDate = new GregorianCalendar(jahr,
monat, tag);
for(int i = 0; i < tageMax.length ; i++) { //schauen
ob maximaler tag im
if(monat==i) {
//monat überschritten ist
if(tag>tageMax[i]) itIsADate=false;
}
}
if(testDate.isLeapYear(jahr)) {
//bei schaltjahr auch 29 feb.
if ((monat==1) && (tag==29)) itIsADate=true;
}
if(monat>11) itIsADate=false;
//wenn monat größer 12 dann falsch
if (tag==0) itIsADate=false;
// wenn tag = 0 dann falsch
if ((monat+1)==0) itIsADate=false; // wenn
monat = 0 dann falsch
return itIsADate;
}
static boolean isDate(String checkString) {
// Klassenmethode !!!!!
boolean itIsADate=false;
if(checkString.length()==10) {
int tag = new Integer(checkString.substring(0,
2)).intValue();
int monat = new Integer(checkString.substring(3,
5)).intValue()-1;
int jahr = new Integer(checkString.substring(0,
2)).intValue();
if(isDate(tag, monat, jahr)==true) itIsADate=true;
}
return itIsADate;
}
}
Wow. Ziemlich viel. Aber mit dem bisher gelernten müßten
Sie die Struktur eigentlich auch schon beim Überfliegen verstehen. Die
import-Anweisungen und die Signatur dürften inzwischen
klar sein:
import java.util.*; // Fuer Calendar
Objekte
import javax.swing.*; // Weil JTextField erweitert
wird
public class JDateField extends JTextField { // ****
Beginn Klasse JDateField
Hier wird util für Calendar-Objekte eingebunden
und Swing, wegen der Erweiterung des JTextField. Unsere Klasse erweitert
dieses schließlich. Als nächstes werden die Attribute deklariert,
die zusätzlich zu denen der Klasse JTextField bereitstehen:
private Calendar myDates = new GregorianCalendar();
// Calender fuers init.
private int tagInt = myDates.get(Calendar.DATE);
//Felder, die Teil-
private int monatInt = myDates.get(Calendar.DATE);
//strings des gesamten
private int jahrInt = myDates.get(Calendar.DATE);
//TextFeldes enthalten,
private String tagString = String.valueOf(tagInt);
//bzw. die ent-
private String monatString = String.valueOf(monatInt);
//sprechenden Int-
private String jahrString = String.valueOf(jahrInt);//Werte
dazu
// zur Datumsgültigkeit:
private static final int[] tageMax = {31,28,31,30,31,30,31,31,30,31,30,31};
Es handelt sich natürlich überwiegend um Datums-spezifische
Attribute. Zunächst ein Calendar-Objekt für die Initialisierung.
Dann folgen drei int-Werte und drei Strings. Diese nehmen, ähnlich wie
der Calendar das Datum aus unserem JDateField in drei einzelnen
Feldern auf. Schließlich wird noch ein Array deklariert, welches die
Anzahl der Tage pro Monat aufnimmt, und bei der Methode zur Prüfung
der Gültigkeit von Daten verwendet wird.
Nun folgt wieder eine riesige Anzahl von Konstruktoren. Diese widmen sich
speziell der Initialisierung des Feldes mit einem Datum. Der Aufbau ist überall
ähnlich. Sehen wir uns den ersten Konstruktor an:
public JDateField() {
// Kein Para (Def. Datum: Heute)
super();
// Wie gehabt
this.setDocument(new DateFieldDocument(this));
// Passendes Dokument
}
Dies ist der Konstruktor ohne Argumente. Er ruft einfach den
Konstruktor von JTextField auf (mit super()). Anschließend
setzt er für das hier vorliegende JDateField das DateFieldDocument
ein, und übergibt eine Referenz auf sich selbst. Dieses Setzen des Dokuments
kommt in allen Konstruktoren vor. Damit muß derjenige, der das JDateField
in seinem Programm verwendet, sich nicht selbst um das Dokument kümmern,
sondern kann das JDateField genauso einfach verwenden, wie ein JTextField.
Durch das Setzen des Dokumentes, ist das JDateField auch automatisch
mit dem heutigen Datum initialisiert, da dies ja standardmäßig
vom Konstruktor des DateFieldDocumenteingesetzt wird, wenn nichts
anderes angegeben ist.
Die folgenden Konstruktoren verfahren nach dem gleichen Schema, nur daß
sie Argumente entgegennehmen, die sie in der Regel ungefiltert an die Konstruktoren
der Elternklasse JTextField durchreichen. Ich erläutere hier
nur noch die Konstruktoren, in denen etwas Besonderes zu bemerken ist. Dazu
gehört der Konstruktor, dem ein Calendar-Objekt übergeben wird.
public JDateField(Calendar myDate) {
// SpezialConstructor des Documents
super();
// Wie gehabt
this.setDocument(new DateFieldDocument(this, myDate));
// Passendes Dokum.
}
Hier wird zunächst auch der Standardkonstruktor von JTextField
aufgerufen. Eine Behandlung des übergebenen Calendar-Objektes findet
nicht statt. Stattdessen wird dieses Objekt einfach an den Konstruktor von
DateFieldDocument durchgereicht, welches ja mit diesem
Argument umgehen kann, und dann eben auf das angegebene Datum initialisiert.
Der folgende Konstruktor, der drei int-Werte zum Initialisieren entgegennimmt
funktioniert ähnlich:
public JDateField(int tag, int monat, int jahr)
{ // Mit Ints fürs Datum
super();
// Wie gehabt
this.setDocument(new DateFieldDocument(this,
// Passendes Dokument
new GregorianCalendar(jahr,
monat, tag)));
}
Hier werden bei dem Aufruf des Konstruktors des DateFieldDocument,
die drei Werte eben nur an Ort und Stelle in ein Calendar-Objekt überführt,
welches ja mit int-Werten initialisierbar ist.
Die Konstruktoren machen also nichts weiter, als die Daten an die Konstruktoren
des Elternobjektes JTextField oder des Dokumentmodells DateFieldDocument
durchzureichen. Die hohe Anzahl der Konstruktoren ergibt sich nur daraus,
daß das JDateField flexibel sein soll, und alle möglichen
Datums-spezifischen Aufrufe bereitstellen soll (obwohl eDays selbst
ja später nur eine einzige Variante verwendet). Wenn wir Javaklassen
schreiben, sollten wir immer im Hinterkopf überlegen, ob die Klasse an
der wir gerade arbeiten später vielleicht in einem anderen Projekt gut
wiederzuverwenden wäre. Wenn ja, dann sollte man durchaus direkt beim
Programmieren den Aufwand betreiben, und die Klasse gleich flexibel gestalten.
Ein Nachteil der hier gezeigten Konstruktoren ist, daß Argumente kommentarlos
an die Konstruktoren der anderen Klassen durchgereicht werden. Eine Verbesserungsmöglichkeit
wäre vorher zu prüfen, ob die Argumente sinnvoll sind, also ein
Datum zum Beispiel gültig ist.
Die erweiterten Zugriffsmethoden beziehen sich natürlich auch auf die
Datum's (ich liebe diesen Ausdruck *g*). Am einfachsten sind die set-Methoden
für ganze Daten zu handhaben. Wobei mir gleich in der ersten Methode
ein Bug (Fehler) auffällt:
public void setDate(Calendar myNewDate) {
// Übergabe eines Calender
String myTagString=String.valueOf(myNewDate.get(Calendar.DATE));
String myMonatString=String.valueOf(myNewDate.get(Calendar.MONTH)+1);
String myJahrString=String.valueOf(myNewDate.get(Calendar.YEAR));
this.setText(myTagString+"."+myMonatString+"."+myJahrString);
}
Theoretisch ist alles in Ordnung. Als Argument wird ein Calendar-Objekt
übergeben, und in einen String verwandelt. Wegen unseres Dokumentmodells,
welches die Trennpunkte im Datum an festen Positionen verwaltet, müßen
wir aber darauf achten, daß ein Datumsstring immer 10 Stellen lang ist.
Das ist hier nicht gewährleistet, weil unter Umständen der String
"4.1.2001" anstatt "04.01.2001" entstehen kann. Hier wäre somit die
Verwendung des SimpleDateFormat angebracht. Bessern Sie das ruhig
selber nach (nein, ich bin nicht zu faul, ich besser das bei mir auch nach
*g*). Ansonsten dürfte die Methode klar sein.
Die folgende set-Methode ist kurz und knapp:
public void setDate(String myDateText) {
// Übergabe eines String
// zB "11.12.2000"
if(isDate(myDateText)==true)
this.setText(myDateText);
}
Hier wird ein String im richtigen Format erwartet. Mit der
später erläuterten Methode isDate() wird festgestellt,
ob es sich um ein gültiges Datum handelt. Wenn ja, wird das Datum im
Feld eingesetzt. Aufgrund der Implementation der Methode isDate()
kann hier auch nicht der eben genannte Bug auftreten. Bei der nächsten
set-Methode sieht man sehr schön, wie ich mich bemühe, das zehnstellige
Datumsformat zu erhalten:
public void setDate(int day, int month, int year)
{ // Übergabe
String myTagString=String.valueOf(day);
// dreier int Werte
if(day<10) myTagString="0"+String.valueOf(day);
String myMonatString=String.valueOf(month);
if(month<10) myMonatString="0"+String.valueOf(month);
String myJahrString=String.valueOf(year);
if(year<10) myJahrString="000"+String.valueOf(year);
if(year<100) myJahrString="00"+String.valueOf(year);
if(year<1000) myJahrString="0"+String.valueOf(year);
this.setText(myTagString+"."+myMonatString+"."+myJahrString);
}
Hier werden int-Werte in den Datumsstring verwandelt. Ist
ein int-Wert nicht vierstellig, wird er jeweils mit sovielen Nullen aufgefüllt,
daß er den Kriterien entspricht.
Bei den Methoden, bei denen nur ein Feld mittels int-Wert gesetzt werden kann,
muß man ein wenig tricksen:
public void setDay(int day) {
// **** set-Methoden für Int Werte
attributeUpdate();
// setzt den Tag nach Übergabe eines int
String myTagString=String.valueOf(day);
if(day<10) myTagString="0"+String.valueOf(day);
this.setText(myTagString+"."+monatString+"."+jahrString);
}
Im großen und ganzen entspricht die Verarbeitung der
eben gezeigten bei Übergabe dreier int-Werte. Diesmal halt nur auf ein
einzelnes Feld bezogen. Wir erinnern uns aber, daß unser Dokumentmodell
nur 1-stellige oder 10-stellige Eingaben entgegennimmt. Das Setzen eines
Tages übergibt aber z.B. einen zweistelligen String. Mit der später
erläuterten Methode attributeUpdate() werden unsere Attribute
so eingestellt, wie das JDateField gerade belegt ist. Diese Methode
muß aufgerufen werden, da sonst nicht sichergestellt ist, daß
die Attribute für Monat und Jahr dem gerade angezeigten Wert entsprechen.
Nun wird der übergebene Tag in einen String gewandelt, und zusammen mit
den anderen Attributen wird ein zehnstelliger String gebastelt, der den neuen
Tag enthält. Dieser wird dann übergeben. So können wir mit
dem JDateField auch einzelene Felder ändern, ohne durch die
zehnstellige Beschränkung des Modells behindert zu sein. Genauso werden
auch die set-Methoden für den Monat und das Jahr implementiert.
Methoden zur Übergabe von int-Werten an das JDateFieldsind
schon ganz praktisch. Aber da das JDateField ja von einem Textfeld
abstammt, wäre auch die klassische Übergabe in Form eines Strings
nicht schlecht. Dazu dienen die folgenden drei set-Methoden für die
Felder des Datums, die alle gleich aufgebaut sind:
public void setDayText(String day) {// **** set-Methoden
für String Werte
attributeUpdate();
// Übergabe eines String tag
String myTagString=day;
if(day.length()<2) myTagString="0"+day;
this.setText(myTagString+"."+monatString+"."+jahrString);
}
Der oben genannte Fehler tritt hier übrigens nicht auf,
da immer auf die richtige Anzahl von Stellen des String geachtet wird. Diese
set-Methoden sind eigentlich selbsterklärend und bedürfen keiner
weiteren Erläuterung.
Die folgenden get-Methoden sind, wie das für get-Methoden so oft üblich
ist, ganz simpel gestrickt. Zunächst lassen sich einzelne Felder als
int-Wert abfragen:
public int getDay() { // **** get-Methoden
für Int Werte
attributeUpdate(); // Übergibt
den Tag als Int zB 28
return tagInt;
}
Bei allen get-Methoden wird zunächst sicherheitshalber
die MethodeattributeUpdate()aufgerufen, damit die Attribute der
Klasse auch dem Dargestellten entsprechen. Dann wird einfach das entsprechende
Attribut zurückgeliefert. Dies trifft auf die get-Methoden für
int-Werte genauso zu, wie für die get-Methoden die String-Entsprechungen
der einzelnen Felder zurückliefern.
Zusätzlich habe ich eine get-methode implementiert, die den Inhalt des
JDateField als Calendar-Objekt zurückgibt:
public Calendar getDate() {
// **** get-Methode für Calendar Objekte
attributeUpdate(); // Übergibt Inhalt des
Textfeldes als Calendar Objekt
Calendar aktDate = new GregorianCalendar(tagInt,
monatInt, jahrInt);
return aktDate; //hier neues
kalenderobjekt erzeugen
}
Dabei wird das Calendar-Objekt einfach aus den int-Attributen
der Klasse erzeugt. Möglicherweise sollte diese Methode besser getCalendar()
heißen, und die getDate-Methode ein Date-Objekt zurückgeben.
Dies ist vielleicht eine Verbesserungsmöglichkeit.
Das wars mit den Zugriffsmethoden, die aber wohl fast jeden gewünschten
Zugriff abdecken sollten. Vielleicht vermissen Sie eine get-Methode, die das
gesamte Datum als String zurückgibt? Wenn Sie sich richtig erinnern,
ist unser JDateField ja von JTextField abgeleitet, und
JTextField gibt den gesamten Inhalt mit der Methode getText()zurück.
Diese Methode funktioniert natürlich weiterhin, so daß wir hierfür
keine extra Methode implementieren müssen.
Es folgen nun einige Zusatzmethoden. Die erste ist zwingend für unser
JDateField, die anderen könnten evtl. auch in einer anderen Klasse
implementiert werden. Bei der ersten handelt es sich um die schon erwähnte
Methode attributeUpdate():
private void attributeUpdate() { // MUSS vor
Rückgabe in
// get-Methoden aufgerufen werden
tagString = this.getText().substring(0, 2);
// Achtung: Wert kann ungültig
monatString = this.getText().substring(3, 5);//
sein. zB 32 für den Tag.
jahrString = this.getText().substring(6);
tagInt = new Integer(tagString).intValue();
monatInt = new Integer(monatString).intValue();
jahrInt = new Integer(jahrString).intValue();
}
Sie sorgt dafür, daß die Attribute des JDateField
aktualisiert werden, je nach dem was in dem Feld zur Zeit eingegeben ist.
Dazu setzt sie zuerst die String-Attribute der Klasse durch extrahieren der
einzelnen Werte aus dem dargestellten Gesamtstring. Aus den String-Attributen
leitet sie schließlich die int-Attribute ab. Die Methode dürfte
verständlich sein.
Es folgen zwei Zusatzmethoden, die man als Klassenmethode hätte implementieren
können. In diesem Fall habe ich sie aber so implementiert, daß
sie sich auf eine konkrete Instanz des JDateField beziehen. Sie
prüfen, ob ein Feld mit "00" belegt ist. Wenn dies der Fall
ist, ist ein Datum ungültig:
public boolean hasZeroField() { //
Methode, die prüft, ob ein Feld
boolean hasZero=false;
// "00" enthält.
attributeUpdate();
if (tagString.equals("00")) hasZero=true;
if (monatString.equals("00")) hasZero=true; //
Bei Jahr ist "0000" erlaubt
return hasZero;
}
Hier wird eben einfach auf die entsprechenden Strings geprüft.
Das Jahr wird dabei nicht berücksichtigt, da das Jahr 0 ja in
einem regulären Datum auftauchen darf. Je nach Ergebnis wird ein entsprechender
Wahrheitswert zurückgegeben. Die im Listing folgende Methode macht das
gleiche, liefert aber true zurück, wenn kein Nullfeld enthalten
ist. Dies ist bei manchen Abfragen einfach praktischer zu handhaben. Eigentlich
könnte ich mir die Methoden auch schenken, da das Dokumentmodell bereits
die 00-Felder sperrt. Die Wahrheit ist, daß ich das Modell erst später
entsprechend erweitert habe, und diese beiden Methoden hier einfach noch
übriggeblieben sind.
Die nächste Methode ist aber eine sehr wichtige, und sehr schöne.
Und bei ihr handelt es sich um eine Klassenmethode. Eine Klassenmethode ist
eine Methode, die ich nicht über eine Instanz aufrufe, sondern direkt
mit ihrem Klassennamen. Die Methode hasZeroField() würde ich
zum Beispiel immer mit myField.hasZeroField() aufrufen und niemals
mit JDateField.hasZeroField(). Eine Klassenmethode, wie die folgende,
rufe ich aber immer mit JDateField.isDate(Datum)auf. Ich brauche
zum Verwenden der Methode noch nicht einmal eine Instanz der Klasse. Mit
Klassenmethoden realisiert man so etwas wie eine Funktionsbibliothek. Sie
haben selbst schon häufig Klassenmethoden verwendet. z.B wenn Sie einen
int-Wert in einen String gewandelt haben, haben Sie das KonstruktString.valueOf(year)
verwendet. valueOf() ist dabei eine Klassenmethode der Klasse String
(wie man hier auch sehr schön sieht).
Zurück zu unserer Klassenmethode. Eigentlich sind es zwei, aber die
zweite baut auf der ersten auf: Das Calendar-Objekt hatte ja die Eigenschaft,
wie vorher schon erwähnt, daß es nicht meckert, wenn man ihm ein
unmögliches Datum (31.02.20001) übergibt, sondern die ihm übergebenen
Daten schon irgendwie in seine Felder presst. Mein Modell kann ja nur schwach
validieren. Aber damit korrekte Daten von Edyas ausgerechnet werden, brauche
ich schon ein Methode, die mir klipp und klar sagt, ob das eingegebene Datum
so gültig, oder ungültig ist. Erstmal der Code:
static boolean isDate(int tag, int monat, int jahr)
{ // Klassenmethode !!!!
boolean itIsADate=true;
GregorianCalendar testDate = new GregorianCalendar(jahr,
monat, tag);
for(int i = 0; i < tageMax.length ; i++) { //schauen
ob maximaler tag im
if(monat==i) {
//monat überschritten ist
if(tag>tageMax[i]) itIsADate=false;
}
}
if(testDate.isLeapYear(jahr)) {
//bei schaltjahr auch 29 feb.
if ((monat==1) && (tag==29)) itIsADate=true;
}
if(monat>11) itIsADate=false;
//wenn monat größer 12 dann falsch
if (tag==0) itIsADate=false;
// wenn tag = 0 dann falsch
if ((monat+1)==0) itIsADate=false; // wenn
monat = 0 dann falsch
return itIsADate;
}
Die Methode ist als static deklariert, was bei einer
Klassenmethode immer so ist. Nur static deklarierte Methoden können
direkt über ihre Klasse aufgerufen werden. Die Methode gibt einen boolschen
Wert zurück, womit sie sich gut für if-Abfragen eignet. Als Argument
erwartet die Methode das Datum in Form dreier int-Werte.
Dann wird der spätere itIsADate Rückgabewert erstmal auf
true gesetzt. Für eine der folgenden Abfragen benötigen
wir auch ein Calendar-Objekt. Dies erzeugen wir auch gleich. Die nun folgenden
Abfragen testen jeweils darauf, ob ein Kriterium, welches ein gültiges
Datum erfüllen muß, nicht erfüllt ist. Ist es nicht erfüllt,
dann wird itIsADate auf false gesetzt.
Wir hatten bei den Attributen ein Array deklariert, und mit Werten für
die maximale Zahl der Tage im Monat gefüllt. Eine Schleife zählt
nun zur Anzahl der Elemente hoch. Entspricht der Zähler unserem Monat,
wird in das Array an der entsprechenden Stelle geschaut, welche Anzahl von
Tagen ein Datum dieses Monats maximal haben dürfte. Ist der Tag größer,
dann wird der Rückgabewert auf falsegesetzt.
In einem Fall ist diese Abfrage zu ungenau, nämlich im Schaltjahr. Daher
wird folgend noch einmal mithilfe des Calendar-Objektes geprüft, ob
das eingegebene Datum ein Schaltjahr (wie z.B. 2000) ist. Da dann auch der
29.02.2000 gültig wäre, wird itIsADate "wieder" auf true
gesetzt, falls es sich um den 29.sten Tag handelt (die vorherige
Methode hätte ja in diesem Fall itIsADate auf false
gesetzt). Das Calendar-Objekt bringt freundlicherweise eine Methode isLeapYear()
mit, die uns mitteilt, ob es sich bei einem gegebenen Jahr um ein Schaltjahr
handelt (geht doch, warum nicht auch eine Differenzfunktion im Calendar-Objekt?).
Nun wird noch einmal (man merkt, ich geh auf Nummer sicher) geprüft,
ob der Monat nicht größer als 12 oder Tag und Monat gleich Null
sind (was das Datum auch ungültig macht). Hat das übergebene Datum
all diese Abfragen überstanden, wird itIsADate zurückgegeben.
Diese Methode sollte eigentlich alle Eventualitäten abdecken.
Die zweite Klassenmethode isDate() unetrscheidet sich nur dadurch
von der ersten Klassenmethode, daß sie mit einem zehnstelligen String
aufgerufen wird, diesen in int-Werte zerlegt, und die eben beschriebene Methode
aufruft um festzustellen, ob es sich um ein gültiges Datum handelt:
static boolean isDate(String checkString) { //
Klassenmethode !!!!!
boolean itIsADate=false;
if(checkString.length()==10) {
int tag = new Integer(checkString.substring(0,
2)).intValue();
int monat = new Integer(checkString.substring(3,
5)).intValue()-1;
int jahr = new Integer(checkString.substring(0,
2)).intValue();
if(isDate(tag, monat, jahr)==true) itIsADate=true;
}
return itIsADate;
}
Da kann man sehen, wie man die erste Klassenmethode aufruft.
Außerdem können Sie im DateFieldDocument nachsehen, wie
man diese Klassenmethode aufruft, da das Modell diese wie vorhin erwähnt,
auch nutzt. Daraus ergibt sich, daß JDateFieldund DateFieldDocument
eng miteinander verbunden sind, und eigentlich nur zusammen verwendet werden
können.
Man kann sich natürlich fragen, ob diese Klassenmethode gerade bei einer
grafischen Komponentenklasse von Swing am besten aufgehoben ist. Ist sie
ggf. nicht. Aus verschiedenen Bereichen von Java, oder Klassen von Drittanbietern
kennt man inzwischen das Konzept der "Factory". Möglicherweise
ist es am besten Methoden wie DateDifferenceInDays()und auch isDate()
in eine derartige Klasse auszulagern. Aber so wie wir es hier gemacht haben
funktioniert es auch sehr gut und bedarf keiner unmittelbaren Änderung.
Damit ist nun auch die Klasse
JDateFieldin dieser Version fertig. Wie ich schon sagte, gibt es noch
zahlreiche Verbesserungsmöglichkeiten: Überarbeitung der Konstruktoren,
Internationalisierung, besseres Verhalten bzgl. der Spaltenbreite und so weiter
und so fort. Aber für das Programm Edays, welches wir ja gerade entwickeln,
ist genau dieses JDateField vollkommen ausreichend.
4.5.2. JDateField testen
Um JDateField zu testen ist nun wirklich nicht mehr sehr viel nötig.
Wir nehmen einfach das Programm SwingZwei und erzeugen eben kein
JTextField sondern ein JDateField. Ich zeige
hier noch den Source dafür, aber keinen Screenshot, da dieser genauso
aussehen würde wie bei dem Test des DateFieldDocument:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class SwingDrei extends JFrame {
public SwingDrei() {
// *** Konstruktor
super("SwingDrei");
// Titel
JLabel myHeader = new JLabel("Hier Text eingeben:");//Labelkomponente
JDateField eingabeFeld = new JDateField(20);
//Datumsfeld-Komponente
JButton klicker = new JButton("Beenden");
//Button-Komponente
ActionListener myListener = new ActionListener()
{ // Fuer den Button
public void actionPerformed(ActionEvent e)
{ // in anonymer Klasse
dispose();
// (siehe auch WinV)
System.exit(0);
}
};
klicker.addActionListener(myListener);
// Listener registrieren
JPanel myContainer = new JPanel();
// spezieller Container
myContainer.setLayout(new BorderLayout(5,5));
// Layout einsetzen
myContainer.add(myHeader, BorderLayout.NORTH);
// Die Komponenten dem
myContainer.add(eingabeFeld, BorderLayout.CENTER);
// Container hinzufügen
myContainer.add(klicker, BorderLayout.SOUTH);
getContentPane().add(myContainer);
// Container dem Frame
// hinzufügen
this.enableEvents(AWTEvent.WINDOW_EVENT_MASK);
// Ereignisse ermöglichen
}
// *** Ende Konstruktor
protected void processWindowEvent(WindowEvent
e) { // Fensterereignisse
if (e.getID()==WindowEvent.WINDOW_CLOSING)
{// behandeln
dispose();
// Ressourcen des Fensters freigeben
System.exit(0);
// Programm beenden
}
}
// Ende Methode processWindowEvent
public static void main(String[] arg) {
//Hauptmethode
SwingDrei mySwingapp
= new SwingDrei();
mySwingapp.pack();
mySwingapp.show();
}
}
Ein richtiger Test ist es noch nicht, da wir ja keine der
Methoden oder Konstruktoren prüfen, aber es zeigt, wie einfach ein JDateField
zu verwenden ist. Genauso einfach wie ein JTextField. Und die Zeile
aus SwingDrei.java in der wir den Dokumenttyp explizit zugewiesen
haben, konnten wir uns auch sparen. Eigentlich sieht es mehr aus wie SwingEins.java,
nur das die "Strings" JTextField gegen JDateField
ausgetauscht wurden. Und das Ergebnis? Das neue
Textfeld reagiert wirklich nur auf Daten. Um zu zeigen, daß dies auch
in einer wirklichen praktischen Anwendung stark zum Tragen kommt, widmen
wir uns nun der letzten Klasse, dem Hauptprogramm.
4.6. Mehr Swing
Ich habe das hier mit "Mehr Swing" übertitelt,
weil es jetzt ein langes Listing gibt, in dem etwa 80% Swing relevant sind.
Auf der anderen Seite wird Ihnen das meiste bekannt vorkommen, weil das ganze
Programm zum überwiegenden Teil aus dem Aufbau der grafischen Oberfläche
besteht. Und da dies ähnlich geschieht wie unter AWT sollte das alles
nichts Neues sein.
Im AWT Kapitel habe ich ab und zu davon gesprochen, daß man Container
schachtelt um bestimmte Layouts zu erzielen. Genau das werden wir jetzt tun.
Bisher hatten wir nie mehr als 3 Komponenten in einem Fenster. In diesen
Fällen kommt man gut mit dem BorderLayout zurecht. Werden es jedoch
mehr Komponenten, dann wird es eng, und es wird Zeit zu schachteln. Ganz
am Anfang habe ich beschrieben, was Edays alles anzeigen soll. Das war nicht
wenig. Ich zeige Ihnen erst einmal einen Screenshot von Edays, damit Sie
sich schon einmal überlegen können, wie man schachtelt, und welche
LayoutManager man verwendet um dieses Erscheinungsbild hinzubekommen:
So, das ist gar nicht so trivial. Schon bei einer so kleinen
Oberfläche muß man sich gehörig den Kopf zerbrechen, wie
man die LayoutManager einsetzt. Immerhin haben wir 12 Label-Komponenten,
ein JDateField (*g*) einen Button, und zwei Trennerlinien, die wir verteilen
müßen. Ich habe dazu jetzt noch eine schematische Darstellung
der Oberfläche von Edays vorbereitet, mit der man die Möglichkeiten
einmal durchspielen kann. Ich habe hier einfach mal die Schachtelung vorgegeben.
Dabei habe ich die Ebenen der Schachtelung farbig markiert. Gelb ist der
Frame selbst. Grün sind darin enthaltene Container
und Lila sind die in Containern enthaltenen Container. Auf den ersten Blick
sieht es so aus, als ob man alles mit GridLayouts erschlagen könnte.
Ich habe das getestet, und man kann es nicht. Wer das nicht glaubt, soll
sich mal die Seiteneffekte ansehen, wenn er das folgende Listing entsprechend
ändert, und alle Container auf GridLayouts setzt. Die Fehler treten
deswegen auf, weil jedes Element im Container gleichviel Fläche erhält,
wie das größte Element. Innerhalb des mittleren grünen Containers
würden bei einem dreizeiligen und einspaltigen GridLayout das mittlere
und untere Element gleichviel Platz bekommen wie das obere Element. Ist auch
der Frame als Gridlayout definiert, so wird das ganze Fenster höher
als 1024 Pixel, was schon den Screen des einen oder anderen sprengt. Ab davon
sieht es Scheiße aus. Bitte probiert es einfach einmal aus.
Ich habe mich, wie gesagt anders entschieden. Container die nicht mehr als
eine Spalte und nicht mehr als drei Zeilen haben, machen sich immer gut für
das BorderLayout. Nur Container, die mehr als eine Spalte haben sollte man
mit dem GridLayout testen. Und so ergibt sich, daß der hier gelb dargestellte
Bereich ein BorderLayout mit besetzten Komponenten in NORTH, CENTER und EAST
ist. Für den mittleren grünen Bereich gilt das gleiche.
Der obere grüne Bereich mit der Überschrift und der gelb dargestellten
Linie erhält auch ein BorderLayout. Der obere lila Bereich erhält
aufgrund der Tatsache, daß mehr als zwei Spalten beteiligt sind ein
GridLayout. Der mittlere lila Bereich kommt dahingegen wieder mit einem BorderLayout
aus. Der untere lila Bereich wegen seiner beiden Spalten, braucht wieder
ein GridLayout.
Der untere grüne Bereich soll flüssig linksbündig angeordnet
werden. Daher hat er von mir ein FlowLayout erhalten. Wen die Layout-Begriffe
jetzt irritieren, der sieht noch mal kurz zu den Beispielgrafikenin meinem AWT Kapitel,
und zieht die API-Dokumentation zu Rate. Und wer nicht glaubt, daß
das die beste Lösung für unser Programm ist, der ist angehalten
mit den Layouts einfach rumzuspielen. Es sind nur wenig Änderungen im
Quellcode erforderlich (an der Stelle, an der die Panels deklariert werden,
und die Layouts zugewiesen werden) um mal das eine oder andere auszuprobieren. Wer eine effektivere Layout-Anordnung findet, kann mir
das gerne mailen.. Dieser kurze Abriß diente jetzt erstmal nur dazu,
zu erklären, welche Komponenten eigentlich auftauchen, und wie sie angeordnet
sind. Als nächstes folgt die Klasse Edays.
4.6.1. Klasse 4: Edays.java
Edays ist wie schon gesagt die Hauptklasse,
die das Fenster definiert, und die main-Methode besitzt. Die Komponentenaufteilung
erfolgt nach dem eben erläuterten Schema. Daher erstmal der lange Quelltext
von Edays.java:
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import javax.swing.*;
import javax.swing.text.*;
public class Edays extends JFrame {
// ***** Erstmal die Komponenten definieren, auf
die zugegriffen wird
JDateField eingabeFeld;
JLabel ausgabeLabel;
JLabel gueltigLabel;
JLabel stichtagLabel;
JLabel heuteLabel;
JLabel statusLabel;
Timer myTimer;
Keymap myKeymap;
KeyStroke myStroke;
Calendar meinDatum = new GregorianCalendar();
//Daten die im Ablauf
Calendar stichtag = new GregorianCalendar();
// benötigt werden
Calendar heute = new GregorianCalendar();
SimpleDateFormat datumsFormat = new SimpleDateFormat
("dd.MM.yyyy");
DateDifferenceInDays myDate = new DateDifferenceInDays(
//DifferenzObjekt
meinDatum.get(Calendar.DATE)-1,
meinDatum.get(Calendar.MONTH)-1,
meinDatum.get(Calendar.YEAR));
public Edays() { // ****
Konstruktor
super("eDays 1.2");
getContentPane().setLayout(new BorderLayout(5,5));
//5 Pixel Abstand zw.
// Komponenten
// Zwei Seperators
JSeparator myUpperLine = new JSeparator();
JSeparator myCenterLine = new JSeparator();
// Ueberschrift
JLabel myTitle = new JLabel("eighty Days",JLabel.CENTER);
//Alle Elemente
myTitle.setFont(new Font("Helvetica",Font.BOLD,20));
//initialisieren
myTitle.setForeground(Color.black);
// meinDatum Beschriftung
JLabel myDateFieldLabel = new JLabel("Datum:
",JLabel.RIGHT);
myDateFieldLabel.setFont(new Font("Helvetica",Font.BOLD,12));
myDateFieldLabel.setForeground(Color.black);
// Alter Beschriftung
JLabel myErgebnisLabel = new JLabel("Alter in Tagen:
",JLabel.RIGHT);
myErgebnisLabel.setFont(new Font("Helvetica",Font.BOLD,12));
myErgebnisLabel.setForeground(Color.black);
// Gültigkeits Beschriftung
JLabel myGueltigLabel = new JLabel("Stichtag dazu:
",JLabel.RIGHT);
myGueltigLabel.setFont(new Font("Helvetica",Font.BOLD,12));
myGueltigLabel.setForeground(Color.black);
// Gültigkeit für das konkrete Datum
Beschriftung
JLabel myStichtagLabel = new JLabel("Stichtag dazu:
",JLabel.RIGHT);
myStichtagLabel.setFont(new Font("Helvetica",Font.BOLD,12));
myStichtagLabel.setForeground(Color.black);
// Heutiges Datum Beschriftung
JLabel myHeuteLabel = new JLabel("Datum heute:
",JLabel.RIGHT);
myHeuteLabel.setFont(new Font("Helvetica",Font.BOLD,12));
myHeuteLabel.setForeground(Color.black);
// Statusfeld Beschriftung
JLabel myStatusLabel = new JLabel("Status: ",JLabel.CENTER);
myStatusLabel.setFont(new Font("Helvetica",Font.BOLD,12));
myStatusLabel.setForeground(Color.black);
// Eingabefeld Datum
eingabeFeld = new JDateField(8); //Verwendet meine
Version von DateField
eingabeFeld.setHorizontalAlignment(JTextField.LEFT);
eingabeFeld.setFont(new Font("Helvetica",Font.BOLD,14));
// Fuer das Datumsfeld die Returntaste manipulieren,
so das [return]
// die Aktion des buttons auslöst
myKeymap = eingabeFeld.getKeymap();
myStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,
0);
myKeymap.removeKeyStrokeBinding(myStroke);
// Ausgabe Feld fuer die Differenz
ausgabeLabel = new JLabel("00000",JLabel.LEFT);
ausgabeLabel.setForeground(Color.black);
ausgabeLabel.setFont(new Font("Helvetica",Font.BOLD,24));
// Ausgabe Feld fuer das noch gültige Datum
stichtag.add(Calendar.DATE, -80);
String dateString = datumsFormat.format(stichtag.getTime());
gueltigLabel = new JLabel(dateString, JLabel.LEFT);
gueltigLabel.setForeground(Color.black);
gueltigLabel.setFont(new Font("Helvetica",Font.BOLD,12));
// Ausgabe Feld fuer den Stichtag zum aktuellen
Datum
stichtag.setTime(new Date());
stichtag.add(Calendar.DATE, 80);
dateString = datumsFormat.format(stichtag.getTime());
stichtagLabel = new JLabel(dateString, JLabel.LEFT);
stichtagLabel.setForeground(Color.black);
stichtagLabel.setFont(new Font("Helvetica",Font.BOLD,12));
// Ausgabe Feld fuer das aktuelle Datum
dateString = datumsFormat.format(heute.getTime());
heuteLabel = new JLabel(dateString, JLabel.LEFT);
heuteLabel.setForeground(Color.black);
heuteLabel.setFont(new Font("Helvetica",Font.BOLD,12));
// Ausgabe Feld fuer den Status
statusLabel = new JLabel("Berechnung durchgeführt",JLabel.LEFT);
statusLabel.setForeground(Color.black);
statusLabel.setFont(new Font("Helvetica",Font.BOLD,12));
// Panels definieren und Layout setzen
JPanel topPanel = new JPanel();
JPanel centerPanel = new JPanel();
JPanel footerPanel = new JPanel();
JPanel upperGridPanel = new JPanel();
JPanel middlePanel = new JPanel();
JPanel lowerGridPanel = new JPanel();
topPanel.setLayout(new BorderLayout());
//
centerPanel.setLayout(new BorderLayout());
//3 Pix Abstand zw. Komponenten
footerPanel.setLayout(new FlowLayout(FlowLayout.LEFT));//alles
nach.ander
upperGridPanel.setLayout(new GridLayout(3,2));
//2 zeilen 3 spalten
middlePanel.setLayout(new BorderLayout(5,5));
// 5 Pix Abstand
lowerGridPanel.setLayout(new GridLayout(2,2));
footerPanel.setBorder(BorderFactory.createLoweredBevelBorder());
//Rahmen
// Button definieren und eigentliche Programmfunktion
einbauen
JButton berechne = new JButton("Berechnen");
// Ereignis Button-Click
berechne.addActionListener(new ActionListener()
{ // in anonymer Klasse
public void actionPerformed(ActionEvent e)
{
int frist, tag=02, monat=11, jahr=2000;
// Voreinstellung Datum
boolean noDateError=true;
if(eingabeFeld.hasNoZeroField())
{
//Prüfen ob nicht leer
tag = eingabeFeld.getDay();
monat = eingabeFeld.getMonth()-1;
jahr = eingabeFeld.getYear();
}
else {
noDateError=false;
statusLabel.setText("Kein Nullfeld
möglich!");
ausgabeLabel.setText("##");
}
meinDatum.set(jahr, monat, tag);
//Calenderobjekte aktualisieren
heute.setTime(new Date());
if(!meinDatum.before(heute)) {
//Prüfen ob Datum in der Zukunft
noDateError=false;
statusLabel.setText("Datum in
der Zukunft!");
ausgabeLabel.setText("##");
}
if(!JDateField.isDate(tag, monat, jahr))
{
noDateError=false;
statusLabel.setText("Datum ungültig!");
ausgabeLabel.setText("##");
}
if(noDateError) {
myDate.setEarly(tag, monat, jahr);
myDate.setLater(heute.get(Calendar.DATE),
heute.get(Calendar.MONTH), heute.get(Calendar.YEAR));
frist=myDate.getDifference();
ausgabeLabel.setText(String.valueOf(frist));
//aktuellen Stichtag Updaten
stichtag.set(jahr, monat, tag);
stichtag.add(Calendar.DATE, 80);
String datumsString = datumsFormat.format(stichtag.getTime());
stichtagLabel.setText(datumsString);
statusLabel.setText("Berechnung
durchgeführt.");
}
}
});
SwingUtilities.getRootPane(this).setDefaultButton(berechne);
//Timer für die Anzeige des Stichtages
zum heutigen Datum (alle 5 Minuten)
myTimer = new Timer(300000, new ActionListener()
{ //300000 Millisekunden
public void actionPerformed(ActionEvent evt)
{
stichtag.setTime(new Date());
// *** Stichtag
stichtag.add(Calendar.DATE, -80);
String datumsString = datumsFormat.format(stichtag.getTime());
gueltigLabel.setText(datumsString);
heute.setTime(new Date());
// *** Heute
datumsString = datumsFormat.format(heute.getTime());
heuteLabel.setText(datumsString);
}
});
// Aktive Komponenten ins UpperGrid einbauen
upperGridPanel.add(myDateFieldLabel);
upperGridPanel.add(eingabeFeld);
upperGridPanel.add(myErgebnisLabel);
upperGridPanel.add(ausgabeLabel);
upperGridPanel.add(myStichtagLabel);
upperGridPanel.add(stichtagLabel);
// Berechnen Button und Linie ins Middel Panel
middlePanel.add(berechne, BorderLayout.NORTH);
middlePanel.add(myCenterLine, BorderLayout.SOUTH);
// Gültigkeits Komponenten ins LowerGrid
einbauen
lowerGridPanel.add(myHeuteLabel);
lowerGridPanel.add(heuteLabel);
lowerGridPanel.add(myGueltigLabel);
lowerGridPanel.add(gueltigLabel);
// Überschrift ins Toppanel einbauen
topPanel.add(myTitle, BorderLayout.NORTH);
topPanel.add(myUpperLine, BorderLayout.SOUTH);
// Status ins FooterPanel einbauen
footerPanel.add(myStatusLabel);
footerPanel.add(statusLabel);
// Gridpanels ins übergeordnete Center
Panel bauen
centerPanel.add(upperGridPanel, BorderLayout.NORTH);
centerPanel.add(middlePanel, BorderLayout.CENTER);
centerPanel.add(lowerGridPanel, BorderLayout.SOUTH);
// UnterPanels in den Frame einbauen
getContentPane().add(topPanel, BorderLayout.NORTH);
getContentPane().add(centerPanel, BorderLayout.CENTER);
getContentPane().add(footerPanel, BorderLayout.SOUTH);
this.enableEvents(AWTEvent.WINDOW_EVENT_MASK);
// Schließen ermöglichen
}
protected void processWindowEvent(WindowEvent
e) { // Überschreiben
// um zu Beenden
if (e.getID()==WindowEvent.WINDOW_CLOSING) {
dispose();
// Ressourcen des Fensters freigeben
System.exit(0);
// Programm beenden
}
} // Ende Methode processWindowEvent()
public static void main(String[] arg) {//Hauptmethode
Edays myEdays = new Edays();
myEdays.pack();
myEdays.show();
myEdays.ausgabeLabel.setText("0");
myEdays.statusLabel.setText("Datum eingeben");
myEdays.myTimer.start();
}
}
Und wieder so ein langer Schinken. Niemand hat gesagt das
Programme kurz sind *g*. Wie gesagt befassen sich etwa 80% des Programms
mit der GUI (also dem Graphical User Interface, also der Programmoberfläche,
also der Art und Weise, wie das Programm aussieht). etwa 18% dürften
in die Ereignisbehandlung einfließen und etwa 3% dürfte unser Hauptprogramm
beanspruchen. Dabei sieht man, daß bei objektorientierter Programmierung
die Objekte ihre Aufgaben selbst wahrnehmen, und das Hauptprogramm nur ein
bißchen steuert. Auch wenn es die Reihenfolge des Listings durcheinanderbringt,
werden wir bei der üblichen Analyse des Programms diesmal in der Reihenfolge
vorgehen, nur GUI, nur Ereignisbehandlung, nur Hauptprogramm.
Voraus schicken wir aber, wie üblich, eine Erklärung der
import-Anweisungen und der Attribute:
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import javax.swing.*;
import javax.swing.text.*;
Zu den import-Anweisungen ist nach der Erklärung
der anderen Klassen nicht mehr viel zu sagen. Wir nutzen eben in Edays Klassen
aus all diesen Paketen: Calendar, AWT-LayoutManager etc, Ereignisbehandlung
nach dem AWT, SimpleDateDokumente und Swingkomponenten sowie Teile
der Swing-Textkomponenten.
Die Attribute sind da schon viel interessanter. Variablen, auf die im Laufe
des Programms noch zugegriffen wird, deklariert man am besten als Attribute
der Klasse:
// ***** Erstmal die Komponenten definieren, auf die
zugegriffen wird
JDateField eingabeFeld;
JLabel ausgabeLabel;
JLabel gueltigLabel;
JLabel stichtagLabel;
JLabel heuteLabel;
JLabel statusLabel;
Timer myTimer;
Keymap myKeymap;
KeyStroke myStroke;
Calendar meinDatum = new GregorianCalendar();
//Daten die im Ablauf
Calendar stichtag = new GregorianCalendar();
// benötigt werden
Calendar heute = new GregorianCalendar();
SimpleDateFormat datumsFormat = new SimpleDateFormat
("dd.MM.yyyy");
DateDifferenceInDays myDate = new DateDifferenceInDays(
//DifferenzObjekt
meinDatum.get(Calendar.DATE)-1,
meinDatum.get(Calendar.MONTH)-1,
meinDatum.get(Calendar.YEAR));
Zugegriffen meint dabei, daß die Eigenschaften oder
Werte der Variablen geändert werden. dazu zählen veränderliche
Komponenten, aber auch Daten die immer wieder verwendet werden. Neu sind
für Sie hier die Timer und Key X relevanten Bestandteile,
die aber später erklärt werden (nur deklariert werden müßen
sie hier).
Übrigens sehen sie hier den ersten Einsatz unseres JDateFieldes.
Es wird gleich am Anfang als Instanz eingabeFeld deklariert. Ansonsten
deklarieren wir nur im Programmlauf veränderliche Labels. Darauf folgt
der ominöse Timer und die Key-map -stroke Deklarationen.
Ignorieren Sie das, bis wir bei dem entsprechenden Code angelangt sind.
Daß wir im folgenden noch Calendar-Objekte,
sowie SimpleDateFormate deklarieren, dürfte angesichts des Zwecks des
Programms nicht verwundern. Und schließlich wird auch eine Instanz unserer
Klasse DateDifferenceInDays deklariert und initialisiert, die wir
später noch brauchen. Kommen wir nun zum Konstruktor, und der ist bei
Erweiterungen eines Frames ja in der Regel GUI-spezifisch.
4.6.2. GUI von Edays
Wie wir es aus unseren kleinen Beispielprogrammen ja schon kennen, werden
im Konstruktor die Komponenten eines Fensters deklariert, initialisiert und
ggf. auch Ereignisbehandlungen programmiert. Wir beschränken uns zunächst
auf die GUI-Definition. Die ersten Zeilen lauten:
super("eDays 1.2");
getContentPane().setLayout(new BorderLayout(5,5));
//5 Pixel Abstand zw.
// Komponenten
Die erste Zeile ist klar, der Konstruktor der Elternklasse
wird mit dem Titel des Fensters aufgerufen, der später so erscheint.
In der zweiten Zeile weisen wir dem Frame selbst, einen LayoutManager
zu. Wie gesagt, muß bei Swing-Programmen dazu zunächst der Verweis
auf die JRootPane in Erfahrung gebracht werden, was mit der Methode
getContentPane()bewerkstelligt wird. Dieser wird dann
der LayoutManagerBorderLayoutmit einem horizontalen und vertikalen
Abstand von 5 Pixeln zwischen den Komponenten zugewiesen.
Nun werden für den Rest des Konstruktors, alle verwendeten Komponenten
deklariert, oder die als Attribut erzeugten Komponenten werden initialisiert.
Dann werden die Komponenten entsprechend in den Containern geschachtelt, und
diese wieder im JFrame geschachtelt. Den Anfang macht eine bisher
noch nicht angesprochene Komponente:
// Zwei Seperators
JSeparator myUpperLine = new JSeparator();
JSeparator myCenterLine = new JSeparator();
Ein JSeperator ist so ziemlich die einfachste vorstellbare
Komponente, und entspricht einfach einer horizontalen oder vertikalen Linie.
Diese wird natürlich im entsprechenden Look And Feel gezeichnet, kann
also auch pseudo-dreidimensional ausfallen. Wenn Sie sich noch an unser Schema
für Edays erinnern, dann sind diese Linien, die dort gelb gezeichneten
Linien. Die nächsten Komponenten sind Ihnen im großen und ganzen
schon bekannt, die JLabels:
// Ueberschrift
JLabel myTitle = new JLabel("eighty Days",JLabel.CENTER);
myTitle.setFont(new Font("Helvetica",Font.BOLD,20));
myTitle.setForeground(Color.black);
Hier wird der Titel für das Programm festgelegt. Nicht
der Titel in der Titelleiste, sondern der groß und protzend strahlende
Programmname, wie man ihn im Screenshot schön sehen kann. Wenn Sie sich
an unsere bisherigen Swing Programme erinnern, fällt Ihnen auf, daß
wir Label dort nur definiert haben, ohne diese sonst weiter zu manipulieren.
Die Standardfarbe für diese Label war dann zB. blau. Für unser Programm
wünsche ich mir aber schwarz gezeichnete Beschriftungen. Hier deklarieren
wir in der ersten Zeile ein JLabel und übergeben dem Konstruktor
von JLabel den Inhalts-String, sowie die Orientierung. Dann nutzen
wir Methoden von JLabel, um die Schriftart und die Schriftfarbe
zu setzen. Die Verwendung dieser Methoden dürfte selbsterklärend
sein. Wenn Sie weitere Methoden von JLabel kennenlernen wollen, werfen
Sie doch einmal einen Blick in die API-Dokumentation.
Es folgen nun zahlreiche Labels für die Beschriftung von irgendwas:
JLabel myDateFieldLabel = new JLabel("Datum:
",JLabel.RIGHT);
JLabel myErgebnisLabel = new JLabel("Alter
in Tagen: ",JLabel.RIGHT);
JLabel myGueltigLabel = new JLabel("Stichtag
dazu: ",JLabel.RIGHT);
JLabel myStichtagLabel = new JLabel("Stichtag
dazu: ",JLabel.RIGHT);
JLabel myHeuteLabel = new
JLabel("Datum heute: ",JLabel.RIGHT);
JLabel myStatusLabel = new JLabel("Status:
",JLabel.CENTER);
Die Wiedergabe der Deklaration habe ich jetzt mal gekürzt.
AlleJLabelswerden im Listing oben genauso wie die Überschrift
mit Farbe und Font ausgestattet. Beschriftung heißt hier, daß
diese Label sich im Laufe des Programms niemals mehr ändern werden.
Deswegen wurden sie auch nicht als Attribut deklariert. Sie stehen bei einem
anderen Label oder einem Feld, welches einen Wert darstellt. Mit diesen Labeln
wird eben nur beschrieben, welcher Wert dargestellt wird (sehen Sie auch
das Schema oder den Screenshot).
Nach diesen JLabeln wird das eingabeFeld deklariert. Das
ist in unserem Programm sicher interessant:
// Eingabefeld Datum
eingabeFeld = new JDateField(8); //Verwendet meine
Version von DateField
eingabeFeld.setHorizontalAlignment(JTextField.LEFT);
eingabeFeld.setFont(new Font("Helvetica",Font.BOLD,14));
Tja, das ist interessant, aber relativ unspektakulär.
Wir haben ja unsere Arbeit bereits mit dem Programmieren der Klasse JDateField
und dem dazugehörigen Dokument erledigt. Das JDateField wird
wie ein ganz normales JTextField erzeugt. Im Konstruktor wird ihm
die Breite in Spalten übergeben. Wenn Sie nachher im Programm das DateField
als "Breit" empfinden liegt das daran, daß die Spalten sich am breitesten
Zeichen, und das ist in der Regel das "m" orientieren. Es würden also
in diesem Fall genau acht "m" in das Feld passen (Eine Unwägbarkeit
von sonst sehr schönen proportionalen Zeichensätzen). Anders als
bei den JLabels wird die Orientierung (Left) hier explizit
durch eine geerbte Methode gesetzt. Das Zuweisen des Zeichensatzes geschieht
aber wieder analog zum JLabel.
Beim Deklarieren der Attribute hatte ich die Key* Klassen schon
einmal angesprochen. Jetzt kommen sie zum Einsatz:
// Fuer das Datumsfeld die Returntaste manipulieren,
so das [return]
// die Aktion des buttons auslöst
myKeymap = eingabeFeld.getKeymap();
myStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,
0);
myKeymap.removeKeyStrokeBinding(myStroke);
Was ich hiermit realisiere, ist eine Komfortfunktion. Sie
kennnen das vielleicht aus Windows-Programmen. Es gibt so etwas wie einen
default-Button in einem Dialog oder Fenster. Wenn Sie irgendwo in einem TextFeld
[Return] drücken, dann wird das so behandelt, als wäre der Button
angeklickt worden. Dieses Verhalten möchte ich für mein Programm
auch haben. Nun habe ich im Internet nach einer Lösung gesucht, und
auch eine gefunden. Ich kann sie aber gerade jetzt nicht mehr finden. So
genau weiß ich auch nicht, wie das mit den Keymaps funktioniert. Wir
sehen ja, was ich dort tue. Zuerst habe ich vorhin myKeymap als
Attribut deklariert. Wir haben also ein Objekt vom Typ Keymap. Diesem
weise ich nun einen Verweis auf eine existierende Keymap zu. Ein
Eingabefeld besitzt also standardmäßig auch ein Objekt vom Typ
Keymap. In dieser Keymap sind Tastenkombinationen
(wie zB Alt+X) enthalten, oder bestimmte Tastendrücke, die bestimmte
Aktionen auslösen. Jeder Tastendruck, bzw. jede Kombination stellt dabei
einen Eintrag in Form eines Tabelleneintrags dar. Dieser einzelne Eintrag
ist ein Objekt vom Typ Keystroke. Für bestimmte Kombinationen
oder auch bestimmte Tasten ist dazu eine Konstante in der Klasse KeyEvent
definiert. Worauf ich hinaus will, ist folgendes: Die Keymap vom
eingabeFeldist standardmäßig mit irgendeiner
unbedeutenden Aktion vorbelegt. Durch diese Vorbelegung wird aber verhindert,
daß beim Drücken von [Return] die Standardkomponente des Fensters,
in unserem Fall der Berechnen-Button aufgerufen wird. Aus diesem Grunde muß
die standardmäßige Vorbelegung der [Return]-Taste aus unsererKeymap
einfach entfernt werden. Dazu besorgen wir uns wie gezeigt einen Verweis
auf die Keymapdes Eingabefeldes. Den Wert für den Eintrag von
[Return] bekommen wir über das Keystroke-Objekt. Wenn man den Wert hat,
kann man diesen aus unserer konkreten Keymap mit remove() entfernen.
Es tut mir wirklich leid, daß ich meine Quelle jetzt nicht mehr finde.
Ich hätte Ihnen gern mehr über Keymaps erzählt, als diese
Selbstanalyse. Aber ich nehme an, Sie haben verstanden worum es geht.
Wird im eingabeFeld die Taste [Return] gedrückt, so wird der
Standard-Button des Fensters ausgelöst (wirkt also genauso wie das Klicken
auf den Button).
Sehr umfangreiche Informationen zu Keybindings finden Sie bei Sun
in englischer Sprache.
Im folgenden finden wir wieder einfache JLabel-Deklarationen:
ausgabeLabel = new JLabel("00000",JLabel.LEFT);
stichtag.add(Calendar.DATE, -80);
String dateString = datumsFormat.format(stichtag.getTime());
gueltigLabel = new JLabel(dateString, JLabel.LEFT);
stichtag.setTime(new Date());
stichtag.add(Calendar.DATE, 80);
dateString = datumsFormat.format(stichtag.getTime());
stichtagLabel = new JLabel(dateString, JLabel.LEFT);
dateString = datumsFormat.format(heute.getTime());
heuteLabel = new JLabel(dateString, JLabel.LEFT);
statusLabel = new JLabel("Berechnung durchgeführt",JLabel.LEFT);
Die meisten entsprechen dem oben genannten Schema und sind
gekürzt. Etwas genauer kann man sich die Initialisierung von gueltigLabel,
stichtagLabelund heuteLabel ansehen. Der Stichtag
wurde bei den Attributen deklariert und mit dem heutigen Datum initialisiert.
Bei der Initialisierung des gueltigLabels, welches das Datum vor
80 Tagen darstellen sollen, ziehen wir mit der aus unserer Klasse DateDifferenceInDays
bekannten Methode add() des Calendar-Objektes 80 Tage ab (weil wir
ein negatives Vorzeichen benutzen). Dann deklarieren wir einen neuen String
dateStringund weisen ihm einen mit dem (bei den Attributen
deklarierten) SimpleDateFormatformatierten String des so errechneten
Wertes von Stichtag zu. Mit diesem wird dann das gueltigLabel initialisiert.
Bei der Initialisierung des stichtagLabel setzten wir das Datum
von stichtag wieder auf heute. Dann erhöhen wir das Datum wie vorher
um 80 Tage, diesmal in die Zukunft (stichtag soll ja anzeigen, wann für
eingegebene Daten das Verfallsdatum besteht. Und zu Beginn ist das "eingegebene"
Datum ja das heutige Datum). Wir nutzen wieder den vorher deklarierten String
dateString, um ein formatiertes Datum als String zu
erhalten, und initialisieren damit das stichtagLabel. Bei dem
heuteLabel, welches nur das heutige Datum darstellen soll, werden wir
dateString noch einmal wiederverwenden, und entsprechend
das heutige Datum einsetzen. Wir haben also dateString quasi als
temporären String verwendet und wiederverwendet. Diesem wurde jeweils
ein mit SimpleDateFormat formatiertes Datum übergeben. Damit
wurden dann schließlich unsere Label initialisiert.
Ansonsten wurden all die Label auch mit entsprechender Farbe und Zeichensatz
versorgt (siehe Originallisting). Im Originallisting folgen dann jetzt auch
erstmal all die Container:
// Panels definieren und Layout setzen
JPanel topPanel = new JPanel();
JPanel centerPanel = new JPanel();
JPanel footerPanel = new JPanel();
JPanel upperGridPanel = new JPanel();
JPanel middlePanel = new JPanel();
JPanel lowerGridPanel = new JPanel();
topPanel.setLayout(new BorderLayout());
centerPanel.setLayout(new BorderLayout());
footerPanel.setLayout(new FlowLayout(FlowLayout.LEFT));//alles
nach.ander
upperGridPanel.setLayout(new GridLayout(3,2));
//2 zeilen 3 spalten
middlePanel.setLayout(new BorderLayout(5,5));
// 5 Pix Abstand
lowerGridPanel.setLayout(new GridLayout(2,2));
//2 zeilen 2 spalten
footerPanel.setBorder(BorderFactory.createLoweredBevelBorder());
//Rahmen
Die Logik der Containerauswahl, also wieviele Panels und welche
Layouts ergibt sich aus dem, was wir oben bei der schematischen Grafik von
Edays besprochen haben. Die Erzeugung von Panels dürfte Ihnen inzwischen
auch geläufig sein. Selbstverständlich gibt es aber wieder eine
Besonderheit, die ich hier noch einmal gesondert zeige:
footerPanel.setBorder(BorderFactory.createLoweredBevelBorder());
//Rahmen
Swing bringt eine sogenannte BorderFactory mit. Damit
kann Swing allen Komponenten einen Rahmen zeichnen. Die Art der Rahmen ist
dabei extrem vielfältig. Ich habe hier für das unterste Panel einen
LoweredBevelBordergewählt. Dieser zeichnet einen
pseudo-dreidimensionalen Rahmen, der so aussieht, als wäre das Panel
nicht erhaben oder auf gleicher Höhe wie die anderen Komponenten, sondern
als wäre es etwas tiefer angesiedelt. In diesem Panel wird sich später
die Statuszeile befinden, und für derartige Anzeigen hat sich ein solcher
tiefergesetzter Rahmen eigentlich eingebürgert. Um den Rahmen zu setzten
wird einfach die Methode setBorder()verwendet. Ihr wird ein Rahmen
übergeben, der durch eine Methode derBorderFactoryerzeugt wird.
Als nächstes wird im Konstruktor ein Button erzeugt, wie wir das bereits
kennen:
JButton berechne = new JButton("Berechnen");
// Ereignis Button-Click
Im Argument für den Konstruktor des Buttons wird die
Beschriftung übergeben.
Im Listing folgt nun die Ereignisbehandlung für den Button, die wir
hier noch überspringen. Darauf folgt noch eine Methode, die den Button
selbst betrifft:
SwingUtilities.getRootPane(this).setDefaultButton(berechne);
Ich habe ja oben bei den Keymaps ständig vom
Default-Button eines Fensters gesprochen. Ein zugefügter Button ist
aber nicht sofort ein Default-Button. dies muß künstlich erzeugt
werden. Genau das tut diese Zeile. Sie weist dem Button die Eigenschaft zu,
der Default.-Button des Fensters zu sein. Dazu wird eine Methode aus den
sogenannten SwingUtilitiesverwendet. Sie holt sich eine Instanz
für die JRootPane des entsprechenden Fensters, und setzt dieser
den Button, der als DeafaultButton verwendet werden soll.
Die im Listing folgende Initialisierung des Timers überspringen wir
hier auch noch. Es folgt das Einfügen der ganzen deklarierten Komponenten
in die jeweiligen Container:
// Aktive Komponenten ins UpperGrid einbauen
upperGridPanel.add(myDateFieldLabel);
upperGridPanel.add(eingabeFeld);
upperGridPanel.add(myErgebnisLabel);
upperGridPanel.add(ausgabeLabel);
upperGridPanel.add(myStichtagLabel);
upperGridPanel.add(stichtagLabel);
// Berechnen Button und Linie ins Middel Panel
middlePanel.add(berechne, BorderLayout.NORTH);
middlePanel.add(myCenterLine, BorderLayout.SOUTH);
// Gültigkeits Komponenten ins LowerGrid
einbauen
lowerGridPanel.add(myHeuteLabel);
lowerGridPanel.add(heuteLabel);
lowerGridPanel.add(myGueltigLabel);
lowerGridPanel.add(gueltigLabel);
// Überschrift ins Toppanel einbauen
topPanel.add(myTitle, BorderLayout.NORTH);
topPanel.add(myUpperLine, BorderLayout.SOUTH);
// Status ins FooterPanel einbauen
footerPanel.add(myStatusLabel);
footerPanel.add(statusLabel);
Dabei halten wir uns strikt an die im Schema angegebene Verteilung.
Eine Erklärung sollte nicht nötig sein. Danach schachteln wir die
jeweiligen Untercontainer in ihre jeweiligen Container, bzw. fügen die
entsprechenden Container schließlich dem Frame hinzu:
// Gridpanels ins übergeordnete Center
Panel bauen
centerPanel.add(upperGridPanel, BorderLayout.NORTH);
centerPanel.add(middlePanel, BorderLayout.CENTER);
centerPanel.add(lowerGridPanel, BorderLayout.SOUTH);
// UnterPanels in den Frame einbauen
getContentPane().add(topPanel, BorderLayout.NORTH);
getContentPane().add(centerPanel, BorderLayout.CENTER);
getContentPane().add(footerPanel, BorderLayout.SOUTH);
Auch hier gibt es wohl wenig zu erklären. Die letzte
Zeile des Konstruktors gehört eigentlich nicht hier in die GUI-Erklärung,
aber da sie so kurz ist, nehmen wir sie hier auf
this.enableEvents(AWTEvent.WINDOW_EVENT_MASK); // Schließen
ermöglichen
Diese Zeile kennen Sie schon,
den mit ihr schalten wir die Ereignisbehandlung für die Titelzeile ein,
und können so unser Fenster Schließen, wie wir es schon aus den
anderen Beispielen kennen. Leiten wir damit in den nächsten Abschnitt
über.
4.6.3. Ereignisbehandlung
in Edays
Weil wir gerade mit dem Fenster-Schließen aufgehört haben, machen
wir kurz dort weiter. Da auch Edays irgendwie beendet werden muß, ist
das entsprechende Icon in der Titelleiste aktiviert, und muß nun noch
behandelt werden:
protected void processWindowEvent(WindowEvent
e) { // Überschreiben
// um zu Beenden
if (e.getID()==WindowEvent.WINDOW_CLOSING) {
dispose();
// Ressourcen des Fensters freigeben
System.exit(0);
// Programm beenden
}
} // Ende Methode processWindowEvent()
Sie kennen diese Methode bereits aus den anderen Beispielprogrammen.
Sie folgt direkt auf den Konstruktor, und muß eigentlich nicht weiter
erläutert werden.
Kommen wir nun dazu; ich habe während der GUI-Besprechung im Konstruktor
öfters gesagt, dazu kommen wir später. Es ging dann immer um Ereignisbehandlung.
Fangen wir mit der ersten an, und diese steht hinter der Deklaration des
Buttons berechne.
Unsere gesamte Oberfläche hat ja recht wenig interaktive Elemente. Das
JDateField verwaltet sich, wie wir inziwschen wissen, praktisch selbst.
Ansonsten ist ja nur der Button als interaktives Element vorhanden. Wenn
der Button gedrückt wird (oder wie wir vorhin schon umständlich
beschrieben haben die [Return]-Taste gedrückt wird) werden die eigentlichen
Programmfunktionen ausgeführt. Auf der Basis des im JDateField
eingegebenen Datums wird dann das Alter in Tagen berechnet und das dazu gültige
Verfallsdatum nach 80 Tagen. Dies alles wird in der Ereignisbehandlung des
Buttons durchgeführt. Eine Ereignisbehandlung in einem Button realisieren
wir, wie bereits beschrieben, über einen ActionListner, der alle Reaktionen
auf das Ereignis in einer anonymen Klasse verpackt:
berechne.addActionListener(new ActionListener()
{ // in anonymer Klasse
public void actionPerformed(ActionEvent e)
{
int frist, tag=02, monat=11, jahr=2000;
// Voreinstellung Datum
boolean noDateError=true;
Dies ist erst der Anfang. Zuerst werden für diese Klasse
die wesentlichen int-Variablen deklariert. Die Frist, die schließlich
das Alter in Tagen aufnimmt, sowie ein durch int-Werte ausgedrücktes
Datum mit beliebigen Werten als Voreinstellung (man weiß ja nie was
passiert *g*). Außerdem können wir ja nicht zu hundertprozent
sicher sein, ob das eingegebene ein gültiges Datum ist. Daher verwenden
wir einen boolschen Wert um festzustellen, ob ein fehlerhaftes Datum eingegeben
wurde.
Nun folgen erst einmal Abfragen, die feststestellen, ob das Datum korrekt
im Sinne unseres Programms ist:
if(eingabeFeld.hasNoZeroField())
{
//Prüfen ob nicht leer
tag = eingabeFeld.getDay();
monat = eingabeFeld.getMonth()-1;
jahr = eingabeFeld.getYear();
}
else {
noDateError=false;
statusLabel.setText("Kein Nullfeld
möglich!");
ausgabeLabel.setText("##");
}
Da wir ja ein gültiges Datum erwarten, darf weder der
Tag noch der Monat ein Nullfeld enthalten. Zur Überprüfung nutzen
wir einfach die Methode, die wir in JDateField erstellt haben. Ist
kein Nullfeld vorhanden, werden die drei Datums-Variablen aus unserer anonymen
Klasse mit den Werten aus unserem JDateField belegt (dabei wird bei
dem Monat bereits die Abweichung des Offset berücksichtigt). Sollte ein
Nullfeld vorhanden sein, wird der boolsche Wert auf falsegesetzt,
und es wird in der Statuszeile eine entsprechende Meldung ausgegeben. Das
Alter wird außerdem auf den gezeigten String gesetzt, da dieses Label
groß ist, und einen Fehler gut signalisiert.
Für die nächsten Abfragen benötigen wir ein Calendar-Objekt.
Daher erzeugen wir dies:
meinDatum.set(jahr, monat, tag);
//Calenderobjekte aktualisieren
heute.setTime(new Date());
Hier ist meinDatum dann das eingegebene Datum und
heuteeben das heutige Datum. Die nächste Abfrage
lautet wie folgt:
if(!meinDatum.before(heute)) {
//Prüfen ob Datum in der Zukunft
noDateError=false;
statusLabel.setText("Datum in
der Zukunft!");
ausgabeLabel.setText("##");
}
Wie ich schon sagte, ist die Klasse DateDifferenceInDays
nicht besonders robust, weshalb ich es vermeiden will, dort Daten tauschen
zu müssen. Also habe ich ja in Edays, was auch dem Zweck des Programms
entspricht, Daten aus der Zukunft verboten. Die gerade gezeigte Abfrage realisiert
das. Das "!"-Zeichen bedeutet "Nicht". Ist also der folgende Abgleich wahr,
ist der Gesamtausdruck falsch. Ist der folgende Ausdruck falsch, ist der
Gesamtausdruck wahr. Das "!"-Zeichen dreht also das Ergebnis um (negiert
es). Die Folgen sind bekannt. Ist das Datum in der Zukunft, ist der boolsche
Wert entsprechend falsch, und die Statuszeile und das Label für das
Alter des Datums geben eine entsprechende Meldung aus.
Zum Schluß kommt die alles entscheidende Abfrage:
if(!JDateField.isDate(tag, monat,
jahr)) {
noDateError=false;
statusLabel.setText("Datum ungültig!");
ausgabeLabel.setText("##");
}
Hier wird geprüft, ob es sich beim eingegebenen um ein
gültiges Datum handelt (bzw. um die Negierung). Ist das Datum ungültig,
erwarten uns die bekannten Folgen.
Nachdem wir nun endlich alle Fehlermöglichkeiten und Fehleingaben berücksichtigt
haben, kommen wir zum eigentlichen, und kurzen, Kernstück des Programms:
if(noDateError) {
myDate.setEarly(tag, monat, jahr);
myDate.setLater(heute.get(Calendar.DATE),
heute.get(Calendar.MONTH),
heute.get(Calendar.YEAR));
frist=myDate.getDifference();
ausgabeLabel.setText(String.valueOf(frist));
//aktuellen Stichtag Updaten
stichtag.set(jahr, monat, tag);
stichtag.add(Calendar.DATE, 80);
String datumsString = datumsFormat.format(stichtag.getTime());
stichtagLabel.setText(datumsString);
statusLabel.setText("Berechnung
durchgeführt.");
}
Hier wird, wenn es keinen noDateError gab,
die eigentliche Aufgabe des Programms ausgeführt. Sooo lang ist es nun
auch nicht. Das liegt vor allem daran, daß wir die eigentliche eigentliche
Arbeit ja in andere Klassen ausgelagert haben. Zuerst werden mit den Zugriffsmethoden
die beiden Daten von unserer Instanz von DateDifferenceInDaysgesetzt.
Der Variablen frist aus unserer anonymen Klasse wird nun das Ergebnis
der Differenz-Berechnung zugewiesen. Der int-Wert Frist wird nun mittels
entsprechender Umwandlung in das Label für die Ausgabe des Alters eingesetzt.
Dadurch, daß wir nun ein neues Alter haben, müßen wir auch
das Datum aktualisieren, an dem das eingegebene Datum 80 Tage alt wird. Dies
erledigen die folgenden Zeilen nach der schon aus dem Konstruktor her bekannten
Vorgehensweise.
Damit ist die Ereignisbehandlung für den Button erledigt und das war
dann eigentlich das Programm. Da ich mich aber entschlossen hatte, das heutige
Datum anzuzeigen, sowie den dazu passenden Stichtag vor 80 Tagen, ergab sich
eine weitere Ereignisbehandlung. Sollte jemand diese Programm so regelmäßig
nutzen, daß er es einfach rund um die Uhr laufen läßt, würde
am Tag nach dem Start die Anzeige des aktuellen Datums und des passenden
Stichtags nicht mehr stimmen. Es muß also sichergestellt werden, daß
um Mitternacht der Wert umgestellt wird.
Ich verwende in meinem Beispiel eine unsichtbare Komponente um dies sicherzustellen.
Diese wurde bei den Attributen deklariert, und hat auch eine eigene Ereignisbehandlung
bekommen. Zur Erinnerung die Deklaration:
Timer myTimer;
Bei der Komponente handelt es sich, wie man sieht, um einen
Timer. Ein Timer ist eine Komponente, die einmalig
oder in regelmäßigen Abständen ein Ereignis auslöst.
Auf dieses Ereignis kann man reagieren Würde man eine Uhr programmieren,
so würde man denTimerjede Sekunde einmal auslösen, und
dann die Label für die Sekunden, Minuten und Stunden aktualisieren.
Das Aktualisieren würde in der Ereignisbehandlung stattfinden. Schauen
wir uns die Ereignisbehandlung für unseren Timer an:
//Timer für die Anzeige des Stichtages
zum heutigen Datum (alle 5 Minuten)
myTimer = new Timer(300000, new ActionListener()
{ //300000 Millisekunden
public void actionPerformed(ActionEvent evt)
{
stichtag.setTime(new Date());
// *** Stichtag
stichtag.add(Calendar.DATE, -80);
String datumsString = datumsFormat.format(stichtag.getTime());
gueltigLabel.setText(datumsString);
heute.setTime(new Date());
// *** Heute
datumsString = datumsFormat.format(heute.getTime());
heuteLabel.setText(datumsString);
}
});
Wie üblich wird unserem Timer im Konstruktor
ein ActionListenerals anonyme Klasse übergeben. Vorher jedoch
wird ein Parameter übergeben, der das Intervall des Timers
in Millisekunden spezifieziert. Das Intervall meint dabei die Zeit, die vergeht
bis der Timer erneut ein Ereignis auslöst. Bei uns sind das
300.000 Millisekunden, was genau 5 Minuten entspricht. Der Timer
soll ja unter anderem unser heutiges Datum aktualisieren. Ein Intervall von
5 Minuten bedeutet, daß der Timer im ungünstigsten Fall
auch erst um 0:04:59 aufgerufen werden könnte. Damit nehme ich eine
Ungenauigkeit von 5 Minuten im Kauf. Wer also um 0:03:00 einen Blick auf
unser Programm wirft, kann unter Umständen eine falsche Information
erhalten. Da die Bürozeiten unseres BSE-Lieferanten aber von 09-17 Uhr
sind, ist die Wahrscheinlichkeit gering, daß jemand nachts um 12 auf
das Programm schaut. Um nicht zuviel Systemlast zu erzeugen, wird unser heutiges
Datum nur alle 5 Minuten geprüft.
Danach werden die entsprechenden Anweisungen
ausgeführt. Anstatt abzufragen, ob es schon Mitternacht ist, wird einfach
alle 5 Minuten dasgueltigLabel und das heuteLabelauf den
gerade aktuellen Wert gesetzt. Soviel Rechenzeit sollte alle 5 Minuten übrig
sein (auch bei aktivierten SETI-Berechnungen).
Damit sind alle Ereignisbehandlungen abgehandelt.
4.6.4. Der Rest von
Edays
Ja, wenn wir uns Edays so betrachten, dann haben wir eigentlich alles schon
gesehen. Die import-Anweisungen, die Attribute, den Konstruktor und
die eine Methode für das Fenster-Schließen-Event. Was übrig
bleibt, ist der traurige Rest: Das Hauptprogramm:
public static void main(String[] arg) {//Hauptmethode
Edays myEdays = new Edays();
myEdays.pack();
myEdays.show();
myEdays.ausgabeLabel.setText("0");
myEdays.statusLabel.setText("Datum eingeben");
myEdays.myTimer.start();
}
Wie wir einmal mehr sehen, sind Hauptprogramme in Java im
Verhältnis zum Gesamtcode sehr kurz. Es wird eine Instanz von Edays
erzeugt, also ein JFrame. Dann wird er gepackt (Java ermittelt also
selbst, welche Fläche Edays benötigt, und wie groß das Fenster
und die Container sein müssen).
Anschließend wird das ausgabeLabel (also das für das
Alter) auf Null gesetzt, weil ja auch im Eingabefeld das aktuelle Datum angezeigt
wird. Dann wird ein sinnvoller Statustext angezeigt (der voreingestellte war
nur dafür da, beim pack() den längstmöglichen String
anzuzeigen, und somit ggf. Einfluß auf die Fensterbreite zu nehmen.
Die letzte Anweisungen bezieht
sich auf unseren Timer. Timer werden deklariert, und Ihnen
wird eine Ereignisbehandlungsroutine zugewiesen, aber gestartet werden, müßen
sie immer noch manuell. und das bewerkstelligt die letztgenannte Methode.
4.7. Zusammenfassung
Dieses Kapitel war doch sehr lang. Dabei ging es um Swing als Ersatz von
AWT, aber auch um zusätzliche Fähigkeiten wie Dokumentmodelle. Dafür
ist dann schließlich eine Zusammenfassung des Kapitels gerechtfertigt.
Zunächst einmal haben wir einen Einblick in die Bearbeitung von Datum's
bekommen. Dazu sind die Klassen Date, Calendar undGregorianCalendar
hilfreich. Später haben wir festgestellt, daß die Verwendung von
Strings, die Datum's repräsentieren, das SimpleDateFormaterforderlich
macht.
Als nächstes haben wir die Verwendung des MVC-Prinzips kennenglernt,
welches in Swing eigentlich verwendet wird. Beim JTextField ist das
Dokumentmodell besonders hilfreich. Bei anderen Komponenten sind die Modelle
anders implemeniert, wodurch manchmal gar kein Custom-Model (also ein selbstdefiniertes
Modell) erforderlich ist. Aber gerade weil das Dokumentmodell für JTextfelder
das Prinzip so gut veranschaulicht, hat es sich für dieses Kapitel gut
geeignet (noch besser wäre vermutlich das Model für Slider, welches
in der Literatur oft beschrieben wird, aber gerade weil es in der Literatur
oft vorkommt, habe ich hier ein anderes verwendet).
Schließlich haben wir nochmal für Java generell gezeigt, wie man
eigene Komponenten ohne großen Aufwand erstellt, indem man das objektorientierte
Prinzip der Vererbung ausnutzt, und vorhandene Klassen einfach weiter spezialisiert.
Gerade mit dem JDateField sollte vielen Lesern dieses Tutorials
Ideen gekommen sein, wie man eigene Textfelder erstellen kann, die ganz bestimmte
Daten aufnehmen sollen. Ich gebe zu, daß an der ganzen Konzeption einiges
zu verbessern wäre. Gerade darin sollte auch ein Anreiz liegen. Wenn
man für ein spezielles Projekt eine Komponente oder Klasse erstellt,
dann sollte man sich fragen, ob man diese später evtl. wiederverwenden
kann und will. Sollte die Antwort "Ja" lauten, dann sollten sie auch Wert
darauf legen, die Klasse oder Komponente so robust zu implementieren, daß
sie sich auch bei Wiederverwendung nie unerwartet verhält.
Schließlich haben sie im Konstruktor vom Hauptprogramm Edays noch einmal
gesehen, wie umfangreich eine GUI-Definition werden kann. Das soll sie nicht
abschrecken. Sie werden feststellen, daß GUI-Definitionen im Laufe
der Zeit immer mehr zu einem Routine-Job werden. Das ist zwar nur Fleißarbeit,
aber es ist eben notwendig.
Die eigentliche Arbeit übernehmen bei der Java-Programmierung meist
spezialisierte Klassen, oder die Ereignisbehandlungen von Komponenten.. Das
ist die Schlußfeststellung. Damit wären wir durch ein wirkliches
Swing-Programm durch. Wenn sie sich den Source-Code runterladen, ihn selbst
kompilieren, und das Programm ausführen, dann werden sie feststellen,
daß das alles halb so wild ist. Spielen Sie ruhig am Code herum. Ändern
sie Eigenschaften. Verändern sie das ganze Programm. Erhalten sie Compiler-Fehler
nach den Änderungen, lernen Sie auch dadurch. Versuchen Sie doch einfach
einmal die 80 Tage nicht fest zu kodieren, sondern durch eine Konstante zu
ersetzen. Oder vielleicht durch ein weiteres Eingabefeld, welches die konkrete
Differenz aufnimmt. Vielleicht wollen Sie
sich auch mal mit der Internationalisierbarkeit der Komponenten auseinandersetzen.
Dazu müßten sie die local-Definition abfragen, und zum Beispiel
alle fest kodierten Caret-Positionen im JDateFieldals Variablen
behandeln.
4.8. Distribution (Jar-Files)
Ohne Entwicklungsumgebung und kostenpflichtige Programme wie InstallShield
kann das Weitergeben von Java-Programmen schon problematisch werden. Dafür
gibt es eine gute Nachricht. Man kann komplette Programme, die z.B. auch aus
mehreren Klassen bestehen als einzelne *.jar Datei weitergeben. Ab
Windows 98 oder unter Solaris sind diese Dateien sogar per Doppelklick direkt
ausführbar (genauso wie *.exe-Dateien oder andere Programme unter Solaris).
Unter Linux geht das auch. Der Kernel muß entsprechend kompiliert sein.
Die *.jar Dateien haben ebenfalls den Vorteil, daß Programme,
die aus mehreren Klassen bestehen nur als eine Datei ausgeliefert werden,
anstatt als mehrere Class-Dateien.
Um das jar-File zu erstellen, nutzt man das Tool jar (Java ARchive).
Dieses nutzt die Mechanismen zur Erstellung von ZIP-Dateien. Eine jar-Datei
enthält zusätzlich noch eine Manifestdatei. Diese enthält
Meta-Informationen zu dem konkreten Jar-File. Außerdem werden noch
Signierungsmechanismen und ähnliches von jarunterstützt.
Wir benötigen auch eine Manifestdatei bevor wir unser Jar-File erstellen.
Die Virtual Machine muß ja wissen, in welcher der im Jar-File enthaltenen
Class-Dateien die main-Methode enthalten ist, um unser Programm zu starten.
Dazu legen wir eine neue Datei an. Bei mir hat sie den Namenmani.txt.
Ihr Inhalt ist simpel:
Main-Class: Edays
Mehr brauchen wir in unserer Manifestdatei nicht. Nun rufen
wir das jar-Tool auf, und übergeben ihm den Namen unserer Manifestdatei
sowie die Namen der Class-Dateien, die wir in das Archiv packen wollen:
jar cmf mani.txt Edays.jar Edays.class Edays$1.class
Edays$2.class DateFieldDocument.class JDateField.class DateDifferenceInDays.class
Der ganze Aufruf besteht aus genau einer Zeile. Es
hat sich als sinnvoll erwiesen, diesen Aufruf in eine bat-Datei (zB makeBundle.bat)
oder in ein Shellscript oder Alias zu packen. Anstatt alle Class-Dateien zu
benennen, könnte man auch *.class angeben, um alle Class-Dateien
aus einem Verzeichnis in das jar-Archiv zu verpacken. Ich habe aber auch die
Class-Dateien der kleinen Swing-Beispiele in meinem Verzeichnis, und benenne
daher die vier gewünschten Klassen (die dazugehörigen anonymen
Klassen (..$1..$2..) nicht vergessen) direkt.
Die Argumente cmf, die dem jar-Tool übergeben werden bedeuten
create für c, m für manifest (der erste folgende
Dateiname muß also eine Manifestdatei sein) und fdafür,
daß der zweite folgende Dateiname, der Name des Archivs ist, welches
wir erstellen wollen.
Nachdem Sie dieses Jar-File erstellt haben, können Sie es von der Kommandozeile
aus mit
java -jar Edays.jar
starten. Sie können unter Windows auch eine Verknüpfung
auf dem Desktop erstellen, indem Sie dort den kompletten Pafd zu java.exe
und dem jar-File angeben (zb.c:\jdk12\bin\java.exe -jar d:\myapp\Edays.jar).
Dabei wirkt störend, daß immer auch ein Konsolen-Fenster (unter
Windows eine MS-DOS Eingabeaufforderung) mit geöffnet wird. Jetzt die
gute Nachricht. Klicken Sie einfach mal doppelt auf das jar-File. Edays wird
jetzt wie jede andere Windows-Anwendung (*.exe Dateien) gestartet und es
geht auch keine MS-DOS Eingabeaufforderung auf. Seit Java 2 kann die virtuelle
Maschine auch durch javaw gestartet werden, und nicht nur durch
java. Und javaw verzichtet auf ein Konsolenfenster.
Wir können also problemlos unsere Projekte weitergeben, indem wir sie
in ein jar-Archiv packen. Alle Empfänger
unseres Programms können dieses dann einfach durch Doppelklick starten
(sofern sie das Runtime Environment JRE installiert haben oder ein JDK). So
kommt langsam Komfort in unsere Programme und deren Verwendung.
4.9. Download Quelltexte
Diesmal befinden sich im ZIP alle Sourcecodes
aus diesem Kapitel, die Batchdatei für Windows makeBundle.bat, die das
Jar erzeugt, die dazu passende Manifestdatei und eine icon-Datei für
Windows: Download Quelltexte
Siehe auch Projekt eDays, wo immer die aktuelle
Version von eDays mit komplettem Sourcecode bereitliegt. Darin sind dann
ggf. letzte Änderungen, oder auch komplette Verbesserungen enthalten.
4.10. Übung
Ändern Sie den von Ihnen in der Übungsaufgabe zu AWT entwickelten
Bookmark-Manager so ab, daß er eine Swing-Applikation ist. Verwenden
Sie dazu die jeweiligen J-Versionen der benutzten Komponenten. Vergessen Sie
auch nicht, die entsprechenden Container und Frames in der J-Version zu verwenden.
zurück
zur Hauptseite |